fix(web): 小周期切换时对齐标记,避免 LWC Value is null
主周期笔/KLC 分型标记在切到 1m/2m 主图时未对齐 K 线 time;过滤均线无效点并钳制视窗恢复。顺带统一 BI 中枢计算路径。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -25,8 +25,8 @@ def analyze_chan(df, symbol=None, timeframe=None):
|
|||||||
zs_list = chan.calculate_seg_zs(seg_list)
|
zs_list = chan.calculate_seg_zs(seg_list)
|
||||||
# 计算笔中枢(BI中枢)并拍平成列表
|
# 计算笔中枢(BI中枢)并拍平成列表
|
||||||
|
|
||||||
#bi_zs_list = chan.cal_bi_zs_list_pure(bi_list)
|
bi_zs_list = chan.cal_bi_zs_list_pure(bi_list)
|
||||||
bi_zs_list = chan.cal_bi_zs(seg_list)
|
#bi_zs_list = chan.cal_bi_zs(seg_list)
|
||||||
bsp_list = []
|
bsp_list = []
|
||||||
if len(bi_zs_list) > 0:
|
if len(bi_zs_list) > 0:
|
||||||
bsp_list = chan.find_all_bsp(bi_list, bi_zs_list)
|
bsp_list = chan.find_all_bsp(bi_list, bi_zs_list)
|
||||||
|
|||||||
@@ -87,6 +87,21 @@ function updateTradingViewData() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LWC 不允许 null/NaN;时间用整秒,避免 Line 渲染抛 Value is null
|
||||||
|
candles = (candles || []).filter(function (c) {
|
||||||
|
return c && c.time != null &&
|
||||||
|
isFinite(Number(c.open)) && isFinite(Number(c.high)) &&
|
||||||
|
isFinite(Number(c.low)) && isFinite(Number(c.close));
|
||||||
|
}).map(function (c) {
|
||||||
|
return {
|
||||||
|
time: Math.floor(Number(c.time)),
|
||||||
|
open: Number(c.open),
|
||||||
|
high: Number(c.high),
|
||||||
|
low: Number(c.low),
|
||||||
|
close: Number(c.close)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const newBarCount = candles.length;
|
const newBarCount = candles.length;
|
||||||
const barDelta = (oldBarCount > 0 && newBarCount > 0) ? (newBarCount - oldBarCount) : 0;
|
const barDelta = (oldBarCount > 0 && newBarCount > 0) ? (newBarCount - oldBarCount) : 0;
|
||||||
|
|
||||||
@@ -306,14 +321,28 @@ function updateTradingViewData() {
|
|||||||
|
|
||||||
const applyPosition = function (tag) {
|
const applyPosition = function (tag) {
|
||||||
let ok = false;
|
let ok = false;
|
||||||
if (lr && lr.from !== undefined && lr.to !== undefined) {
|
if (lr && lr.from !== undefined && lr.to !== undefined && newBarCount > 0) {
|
||||||
|
// 视窗超出当前 K 线数量时,LWC Line 绘制会抛 Value is null
|
||||||
|
const span = Math.max(1, lr.to - lr.from);
|
||||||
|
let to = lr.to;
|
||||||
|
let from = lr.from;
|
||||||
|
const maxTo = newBarCount - 1 + 8;
|
||||||
|
if (to > maxTo) {
|
||||||
|
to = maxTo;
|
||||||
|
from = to - span;
|
||||||
|
}
|
||||||
|
if (from < -8) {
|
||||||
|
from = -8;
|
||||||
|
to = from + span;
|
||||||
|
}
|
||||||
|
const clamped = { from: from, to: to };
|
||||||
charts.forEach(c => {
|
charts.forEach(c => {
|
||||||
try {
|
try {
|
||||||
c.timeScale().setVisibleLogicalRange({ from: lr.from, to: lr.to });
|
c.timeScale().setVisibleLogicalRange(clamped);
|
||||||
ok = true;
|
ok = true;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
});
|
});
|
||||||
if (ok) console.log('🔄 恢复位置 logical' + (tag || '') + ':', lr);
|
if (ok) console.log('🔄 恢复位置 logical' + (tag || '') + ':', clamped);
|
||||||
}
|
}
|
||||||
if (!ok && vr && vr.from !== undefined && vr.to !== undefined) {
|
if (!ok && vr && vr.from !== undefined && vr.to !== undefined) {
|
||||||
charts.forEach(c => {
|
charts.forEach(c => {
|
||||||
|
|||||||
@@ -1,5 +1,77 @@
|
|||||||
/* chart_tv_overlays.js — structure zones / wyckoff / BSP / FX / bollinger */
|
/* chart_tv_overlays.js — structure zones / wyckoff / BSP / FX / bollinger */
|
||||||
|
|
||||||
|
/** 标记 time 必须落在主 series 的 K 线 time 上,否则 LWC 会抛 Value is null */
|
||||||
|
function alignMarkersToCandles(markers, candles) {
|
||||||
|
if (!Array.isArray(markers) || !markers.length) return [];
|
||||||
|
if (!Array.isArray(candles) || !candles.length) return [];
|
||||||
|
var times = [];
|
||||||
|
for (var i = 0; i < candles.length; i++) {
|
||||||
|
var ct = candles[i] && candles[i].time;
|
||||||
|
if (ct == null || !isFinite(Number(ct))) continue;
|
||||||
|
times.push(Math.floor(Number(ct)));
|
||||||
|
}
|
||||||
|
if (!times.length) return [];
|
||||||
|
var set = {};
|
||||||
|
for (var j = 0; j < times.length; j++) set[times[j]] = true;
|
||||||
|
var nearest = function (target) {
|
||||||
|
var best = times[0];
|
||||||
|
var bestDiff = Math.abs(best - target);
|
||||||
|
// 两端夹逼:大数据量时比全扫略好
|
||||||
|
var lo = 0, hi = times.length - 1;
|
||||||
|
while (lo <= hi) {
|
||||||
|
var mid = (lo + hi) >> 1;
|
||||||
|
var t = times[mid];
|
||||||
|
var d = Math.abs(t - target);
|
||||||
|
if (d < bestDiff) { best = t; bestDiff = d; }
|
||||||
|
if (t < target) lo = mid + 1;
|
||||||
|
else hi = mid - 1;
|
||||||
|
}
|
||||||
|
if (lo < times.length) {
|
||||||
|
var d2 = Math.abs(times[lo] - target);
|
||||||
|
if (d2 < bestDiff) best = times[lo];
|
||||||
|
}
|
||||||
|
if (hi >= 0) {
|
||||||
|
var d3 = Math.abs(times[hi] - target);
|
||||||
|
if (d3 < bestDiff) best = times[hi];
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
};
|
||||||
|
var out = [];
|
||||||
|
for (var k = 0; k < markers.length; k++) {
|
||||||
|
var m = markers[k];
|
||||||
|
if (!m || m.time == null || !isFinite(Number(m.time))) continue;
|
||||||
|
var t0 = Math.floor(Number(m.time));
|
||||||
|
var aligned = set[t0] ? t0 : nearest(t0);
|
||||||
|
var copy = Object.assign({}, m, { time: aligned });
|
||||||
|
out.push(copy);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeOverlayLineSetData(series, points) {
|
||||||
|
if (!series || typeof series.setData !== 'function' || !Array.isArray(points) || points.length < 2) return;
|
||||||
|
try {
|
||||||
|
var a = points[0], b = points[1];
|
||||||
|
if (!a || !b || a.time == null || b.time == null) return;
|
||||||
|
var t0 = Math.floor(Number(a.time));
|
||||||
|
var t1 = Math.floor(Number(b.time));
|
||||||
|
var v0 = Number(a.value);
|
||||||
|
var v1 = Number(b.value);
|
||||||
|
if (!isFinite(t0) || !isFinite(t1) || !isFinite(v0) || !isFinite(v1)) return;
|
||||||
|
if (t0 === t1) {
|
||||||
|
// 竖边:同 time 两点 LWC 不接受,跳过(横边仍保留)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (t0 > t1) {
|
||||||
|
series.setData([{ time: t1, value: v1 }, { time: t0, value: v0 }]);
|
||||||
|
} else {
|
||||||
|
series.setData([{ time: t0, value: v0 }, { time: t1, value: v1 }]);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('叠层线 setData 跳过:', e && e.message ? e.message : e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function chartTvRenderOverlays(ctx) {
|
function chartTvRenderOverlays(ctx) {
|
||||||
var symbol = ctx.symbol;
|
var symbol = ctx.symbol;
|
||||||
var timeframe = ctx.timeframe;
|
var timeframe = ctx.timeframe;
|
||||||
@@ -1642,7 +1714,7 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
crosshairMarkerVisible: false,
|
crosshairMarkerVisible: false,
|
||||||
});
|
});
|
||||||
topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]);
|
safeOverlayLineSetData(topSeries, [{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]);
|
||||||
|
|
||||||
const bottomSeries = mainChart.addLineSeries({
|
const bottomSeries = mainChart.addLineSeries({
|
||||||
color: boxColor,
|
color: boxColor,
|
||||||
@@ -1652,7 +1724,7 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
crosshairMarkerVisible: false,
|
crosshairMarkerVisible: false,
|
||||||
});
|
});
|
||||||
bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]);
|
safeOverlayLineSetData(bottomSeries, [{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]);
|
||||||
|
|
||||||
const leftSeries = mainChart.addLineSeries({
|
const leftSeries = mainChart.addLineSeries({
|
||||||
color: boxColor,
|
color: boxColor,
|
||||||
@@ -1662,8 +1734,7 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
crosshairMarkerVisible: false,
|
crosshairMarkerVisible: false,
|
||||||
});
|
});
|
||||||
// 左边竖线:同一 time 上下两个点(和你已有ZS绘制写法保持一致)
|
safeOverlayLineSetData(leftSeries, [{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]);
|
||||||
leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]);
|
|
||||||
|
|
||||||
const rightSeries = mainChart.addLineSeries({
|
const rightSeries = mainChart.addLineSeries({
|
||||||
color: boxColor,
|
color: boxColor,
|
||||||
@@ -1673,7 +1744,7 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
crosshairMarkerVisible: false,
|
crosshairMarkerVisible: false,
|
||||||
});
|
});
|
||||||
rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]);
|
safeOverlayLineSetData(rightSeries, [{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]);
|
||||||
|
|
||||||
if (!tvWidget.series.mainKlcFxBoxSeries) tvWidget.series.mainKlcFxBoxSeries = [];
|
if (!tvWidget.series.mainKlcFxBoxSeries) tvWidget.series.mainKlcFxBoxSeries = [];
|
||||||
tvWidget.series.mainKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries);
|
tvWidget.series.mainKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries);
|
||||||
@@ -1849,7 +1920,7 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
crosshairMarkerVisible: false,
|
crosshairMarkerVisible: false,
|
||||||
});
|
});
|
||||||
topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]);
|
safeOverlayLineSetData(topSeries, [{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]);
|
||||||
|
|
||||||
const bottomSeries = mainChart.addLineSeries({
|
const bottomSeries = mainChart.addLineSeries({
|
||||||
color: boxColor,
|
color: boxColor,
|
||||||
@@ -1859,7 +1930,7 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
crosshairMarkerVisible: false,
|
crosshairMarkerVisible: false,
|
||||||
});
|
});
|
||||||
bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]);
|
safeOverlayLineSetData(bottomSeries, [{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]);
|
||||||
|
|
||||||
const leftSeries = mainChart.addLineSeries({
|
const leftSeries = mainChart.addLineSeries({
|
||||||
color: boxColor,
|
color: boxColor,
|
||||||
@@ -1869,7 +1940,7 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
crosshairMarkerVisible: false,
|
crosshairMarkerVisible: false,
|
||||||
});
|
});
|
||||||
leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]);
|
safeOverlayLineSetData(leftSeries, [{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]);
|
||||||
|
|
||||||
const rightSeries = mainChart.addLineSeries({
|
const rightSeries = mainChart.addLineSeries({
|
||||||
color: boxColor,
|
color: boxColor,
|
||||||
@@ -1879,7 +1950,7 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
crosshairMarkerVisible: false,
|
crosshairMarkerVisible: false,
|
||||||
});
|
});
|
||||||
rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]);
|
safeOverlayLineSetData(rightSeries, [{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]);
|
||||||
|
|
||||||
if (!tvWidget.series.elementKlcFxBoxSeries) tvWidget.series.elementKlcFxBoxSeries = [];
|
if (!tvWidget.series.elementKlcFxBoxSeries) tvWidget.series.elementKlcFxBoxSeries = [];
|
||||||
tvWidget.series.elementKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries);
|
tvWidget.series.elementKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries);
|
||||||
@@ -2002,7 +2073,7 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
crosshairMarkerVisible: false,
|
crosshairMarkerVisible: false,
|
||||||
});
|
});
|
||||||
topSeries.setData([{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]);
|
safeOverlayLineSetData(topSeries, [{ time: startTs, value: boxHigh }, { time: endTs, value: boxHigh }]);
|
||||||
|
|
||||||
const bottomSeries = mainChart.addLineSeries({
|
const bottomSeries = mainChart.addLineSeries({
|
||||||
color: boxColor,
|
color: boxColor,
|
||||||
@@ -2012,7 +2083,7 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
crosshairMarkerVisible: false,
|
crosshairMarkerVisible: false,
|
||||||
});
|
});
|
||||||
bottomSeries.setData([{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]);
|
safeOverlayLineSetData(bottomSeries, [{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]);
|
||||||
|
|
||||||
const leftSeries = mainChart.addLineSeries({
|
const leftSeries = mainChart.addLineSeries({
|
||||||
color: boxColor,
|
color: boxColor,
|
||||||
@@ -2022,7 +2093,7 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
crosshairMarkerVisible: false,
|
crosshairMarkerVisible: false,
|
||||||
});
|
});
|
||||||
leftSeries.setData([{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]);
|
safeOverlayLineSetData(leftSeries, [{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]);
|
||||||
|
|
||||||
const rightSeries = mainChart.addLineSeries({
|
const rightSeries = mainChart.addLineSeries({
|
||||||
color: boxColor,
|
color: boxColor,
|
||||||
@@ -2032,7 +2103,7 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
crosshairMarkerVisible: false,
|
crosshairMarkerVisible: false,
|
||||||
});
|
});
|
||||||
rightSeries.setData([{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]);
|
safeOverlayLineSetData(rightSeries, [{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]);
|
||||||
|
|
||||||
if (!tvWidget.series.subSubKlcFxBoxSeries) tvWidget.series.subSubKlcFxBoxSeries = [];
|
if (!tvWidget.series.subSubKlcFxBoxSeries) tvWidget.series.subSubKlcFxBoxSeries = [];
|
||||||
tvWidget.series.subSubKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries);
|
tvWidget.series.subSubKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries);
|
||||||
@@ -2180,7 +2251,7 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
else if (klineType === 'klc') targetSeries = tvWidget.series.klcSeries;
|
else if (klineType === 'klc') targetSeries = tvWidget.series.klcSeries;
|
||||||
if (targetSeries) {
|
if (targetSeries) {
|
||||||
try {
|
try {
|
||||||
targetSeries.setMarkers(combinedMarkers);
|
targetSeries.setMarkers(alignMarkersToCandles(combinedMarkers, candles));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('设置主系列标记失败(可能series已释放):', e);
|
console.warn('设置主系列标记失败(可能series已释放):', e);
|
||||||
}
|
}
|
||||||
@@ -2309,7 +2380,7 @@ function chartTvRenderOverlays(ctx) {
|
|||||||
else if (klineType2 === 'klc') targetSeries2 = tvWidget.series.klcSeries;
|
else if (klineType2 === 'klc') targetSeries2 = tvWidget.series.klcSeries;
|
||||||
if (targetSeries2) {
|
if (targetSeries2) {
|
||||||
try {
|
try {
|
||||||
targetSeries2.setMarkers(onlyMainAndU);
|
targetSeries2.setMarkers(alignMarkersToCandles(onlyMainAndU, candles));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('设置主系列标记失败(可能series已释放):', e);
|
console.warn('设置主系列标记失败(可能series已释放):', e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ function chartTvBuildShell(ctx) {
|
|||||||
}
|
}
|
||||||
candles = klineDataSource.map((kline) => {
|
candles = klineDataSource.map((kline) => {
|
||||||
const date = new Date(kline.date);
|
const date = new Date(kline.date);
|
||||||
const timestamp = date.getTime() / 1000;
|
const timestamp = Math.floor(date.getTime() / 1000);
|
||||||
return {
|
return {
|
||||||
time: timestamp,
|
time: timestamp,
|
||||||
open: parseFloat(kline.open),
|
open: parseFloat(kline.open),
|
||||||
@@ -46,7 +46,7 @@ function chartTvBuildShell(ctx) {
|
|||||||
low: parseFloat(kline.low),
|
low: parseFloat(kline.low),
|
||||||
close: parseFloat(kline.close),
|
close: parseFloat(kline.close),
|
||||||
};
|
};
|
||||||
});
|
}).filter((c) => isFinite(c.time) && isFinite(c.open) && isFinite(c.high) && isFinite(c.low) && isFinite(c.close));
|
||||||
} else {
|
} else {
|
||||||
if (!currentData.kline_data || !Array.isArray(currentData.kline_data)) {
|
if (!currentData.kline_data || !Array.isArray(currentData.kline_data)) {
|
||||||
console.error('主周期K线数据不存在或不是数组:', currentData.kline_data);
|
console.error('主周期K线数据不存在或不是数组:', currentData.kline_data);
|
||||||
@@ -54,7 +54,7 @@ function chartTvBuildShell(ctx) {
|
|||||||
}
|
}
|
||||||
candles = currentData.kline_data.map((kline) => {
|
candles = currentData.kline_data.map((kline) => {
|
||||||
const date = new Date(kline.date);
|
const date = new Date(kline.date);
|
||||||
const timestamp = date.getTime() / 1000;
|
const timestamp = Math.floor(date.getTime() / 1000);
|
||||||
return {
|
return {
|
||||||
time: timestamp,
|
time: timestamp,
|
||||||
open: parseFloat(kline.open),
|
open: parseFloat(kline.open),
|
||||||
@@ -62,7 +62,7 @@ function chartTvBuildShell(ctx) {
|
|||||||
low: parseFloat(kline.low),
|
low: parseFloat(kline.low),
|
||||||
close: parseFloat(kline.close),
|
close: parseFloat(kline.close),
|
||||||
};
|
};
|
||||||
});
|
}).filter((c) => isFinite(c.time) && isFinite(c.open) && isFinite(c.high) && isFinite(c.low) && isFinite(c.close));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据交易对类型过滤数据(仅用于显示优化)
|
// 根据交易对类型过滤数据(仅用于显示优化)
|
||||||
|
|||||||
@@ -874,14 +874,14 @@ $('#showElementMacdDiv').change(function() {
|
|||||||
refreshChartOnly();
|
refreshChartOnly();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 绑定分型类型显示开关
|
// 绑定分型类型显示开关(与笔一致:全量重建,避免增量路径标记未对齐)
|
||||||
$('#showKlcFxType').change(function() {
|
$('#showKlcFxType').change(function() {
|
||||||
refreshChartOnly();
|
updateChartDisplay();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 绑定小周期分型显示开关
|
// 绑定小周期分型显示开关
|
||||||
$('#showElementKlcFxType').change(function() {
|
$('#showElementKlcFxType').change(function() {
|
||||||
refreshChart(currentData);
|
updateChartDisplay();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -894,10 +894,7 @@ $('#showElementBollinger').change(function() {
|
|||||||
updateChartDisplay();
|
updateChartDisplay();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 绑定K线周期切换
|
// K线周期切换由 macd_ui.js 统一走 updateChartDisplay(勿再绑 refreshChart,会重复且易漏对齐)
|
||||||
$('input[name="klinePeriod"]').change(function() {
|
|
||||||
refreshChart(currentData);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 绑定主图U显示开关
|
// 绑定主图U显示开关
|
||||||
$('#toggleUOnMain').change(function() {
|
$('#toggleUOnMain').change(function() {
|
||||||
|
|||||||
+21
-5
@@ -4,6 +4,16 @@ window.App.Charts = (function() {
|
|||||||
// 依赖 Indicators
|
// 依赖 Indicators
|
||||||
const Indicators = (window.App && window.App.Indicators) || {};
|
const Indicators = (window.App && window.App.Indicators) || {};
|
||||||
|
|
||||||
|
function sanitizeLinePoints(points) {
|
||||||
|
if (!Array.isArray(points)) return [];
|
||||||
|
return points.filter(function (p) {
|
||||||
|
return p && p.time != null && p.value != null &&
|
||||||
|
isFinite(Number(p.time)) && isFinite(Number(p.value));
|
||||||
|
}).map(function (p) {
|
||||||
|
return { time: Math.floor(Number(p.time)), value: Number(p.value) };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function addMovingAveragesToChart(candleData) {
|
function addMovingAveragesToChart(candleData) {
|
||||||
if (!window.tvWidget || !tvWidget.mainChart || !candleData || candleData.length === 0) return;
|
if (!window.tvWidget || !tvWidget.mainChart || !candleData || candleData.length === 0) return;
|
||||||
if (!window.movingAverages) return;
|
if (!window.movingAverages) return;
|
||||||
@@ -21,6 +31,8 @@ window.App.Charts = (function() {
|
|||||||
try {
|
try {
|
||||||
const maData = Indicators.calculateMA(candleData, maConfig.type, maConfig.length, maConfig.source);
|
const maData = Indicators.calculateMA(candleData, maConfig.type, maConfig.length, maConfig.source);
|
||||||
const smoothedData = maConfig.smoothType !== 'none' ? (window.applySmoothToMA ? window.applySmoothToMA(maData, maConfig.smoothType, maConfig.smoothLength) : maData) : maData;
|
const smoothedData = maConfig.smoothType !== 'none' ? (window.applySmoothToMA ? window.applySmoothToMA(maData, maConfig.smoothType, maConfig.smoothLength) : maData) : maData;
|
||||||
|
const cleanData = sanitizeLinePoints(smoothedData);
|
||||||
|
if (!cleanData.length) return;
|
||||||
const maSeries = tvWidget.mainChart.addLineSeries({
|
const maSeries = tvWidget.mainChart.addLineSeries({
|
||||||
color: maConfig.color,
|
color: maConfig.color,
|
||||||
lineWidth: maConfig.lineWidth || 2,
|
lineWidth: maConfig.lineWidth || 2,
|
||||||
@@ -30,8 +42,8 @@ window.App.Charts = (function() {
|
|||||||
priceLineVisible: false,
|
priceLineVisible: false,
|
||||||
crosshairMarkerVisible: true,
|
crosshairMarkerVisible: true,
|
||||||
});
|
});
|
||||||
maSeries.setData(smoothedData);
|
maSeries.setData(cleanData);
|
||||||
maConfig.data = smoothedData;
|
maConfig.data = cleanData;
|
||||||
tvWidget.series.maSeries.push(maSeries);
|
tvWidget.series.maSeries.push(maSeries);
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
});
|
});
|
||||||
@@ -51,12 +63,16 @@ window.App.Charts = (function() {
|
|||||||
if (!bbConfig.visible) return;
|
if (!bbConfig.visible) return;
|
||||||
try {
|
try {
|
||||||
const bbData = Indicators.calculateBB(candleData, bbConfig.length, bbConfig.upperMultiplier, bbConfig.lowerMultiplier, bbConfig.source);
|
const bbData = Indicators.calculateBB(candleData, bbConfig.length, bbConfig.upperMultiplier, bbConfig.lowerMultiplier, bbConfig.source);
|
||||||
|
const upper = sanitizeLinePoints(bbData.map(item => ({ time: item.time, value: item.upper })));
|
||||||
|
const middle = sanitizeLinePoints(bbData.map(item => ({ time: item.time, value: item.middle })));
|
||||||
|
const lower = sanitizeLinePoints(bbData.map(item => ({ time: item.time, value: item.lower })));
|
||||||
|
if (!upper.length || !middle.length || !lower.length) return;
|
||||||
const upperSeries = tvWidget.mainChart.addLineSeries({ color: bbConfig.upperColor, lineWidth: bbConfig.lineWidth || 2, lineStyle: bbConfig.lineStyle || 0, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: true });
|
const upperSeries = tvWidget.mainChart.addLineSeries({ color: bbConfig.upperColor, lineWidth: bbConfig.lineWidth || 2, lineStyle: bbConfig.lineStyle || 0, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: true });
|
||||||
const middleSeries = tvWidget.mainChart.addLineSeries({ color: bbConfig.middleColor, lineWidth: bbConfig.lineWidth || 2, lineStyle: bbConfig.lineStyle || 0, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: true });
|
const middleSeries = tvWidget.mainChart.addLineSeries({ color: bbConfig.middleColor, lineWidth: bbConfig.lineWidth || 2, lineStyle: bbConfig.lineStyle || 0, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: true });
|
||||||
const lowerSeries = tvWidget.mainChart.addLineSeries({ color: bbConfig.lowerColor, lineWidth: bbConfig.lineWidth || 2, lineStyle: bbConfig.lineStyle || 0, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: true });
|
const lowerSeries = tvWidget.mainChart.addLineSeries({ color: bbConfig.lowerColor, lineWidth: bbConfig.lineWidth || 2, lineStyle: bbConfig.lineStyle || 0, lastValueVisible: false, priceLineVisible: false, crosshairMarkerVisible: true });
|
||||||
upperSeries.setData(bbData.map(item => ({ time: item.time, value: item.upper })));
|
upperSeries.setData(upper);
|
||||||
middleSeries.setData(bbData.map(item => ({ time: item.time, value: item.middle })));
|
middleSeries.setData(middle);
|
||||||
lowerSeries.setData(bbData.map(item => ({ time: item.time, value: item.lower })));
|
lowerSeries.setData(lower);
|
||||||
bbConfig.data = bbData;
|
bbConfig.data = bbData;
|
||||||
tvWidget.series.bbSeries.push(upperSeries, middleSeries, lowerSeries);
|
tvWidget.series.bbSeries.push(upperSeries, middleSeries, lowerSeries);
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
|
|||||||
@@ -63,7 +63,10 @@ window.App.Indicators = (function() {
|
|||||||
default:
|
default:
|
||||||
value = sourceData[i];
|
value = sourceData[i];
|
||||||
}
|
}
|
||||||
result.push({ time: data[i].time, value });
|
if (value == null || !isFinite(value) || data[i].time == null || !isFinite(Number(data[i].time))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result.push({ time: Math.floor(Number(data[i].time)), value: Number(value) });
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<!-- TradingView Widget BEGIN -->
|
<!-- TradingView Widget BEGIN -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/lightweight-charts@4.0.1/dist/lightweight-charts.standalone.production.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/lightweight-charts@4.0.1/dist/lightweight-charts.standalone.production.js"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/indicators.js') }}"></script>
|
<script defer src="{{ url_for('static', filename='js/indicators.js') }}?v=20260809i"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/charts.js') }}"></script>
|
<script defer src="{{ url_for('static', filename='js/charts.js') }}?v=20260809i"></script>
|
||||||
<!-- TradingView Widget END -->
|
<!-- TradingView Widget END -->
|
||||||
<script>
|
<script>
|
||||||
window.AVAILABLE_TIMEFRAMES = JSON.parse('{{ timeframe_keys_json | safe }}');
|
window.AVAILABLE_TIMEFRAMES = JSON.parse('{{ timeframe_keys_json | safe }}');
|
||||||
@@ -1390,15 +1390,15 @@
|
|||||||
<script defer src="{{ url_for('static', filename='js/app/chart_format.js') }}?v=20260808i"></script>
|
<script defer src="{{ url_for('static', filename='js/app/chart_format.js') }}?v=20260808i"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/chart_view.js') }}?v=20260809c"></script>
|
<script defer src="{{ url_for('static', filename='js/app/chart_view.js') }}?v=20260809c"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_lifecycle.js') }}?v=20260808i"></script>
|
<script defer src="{{ url_for('static', filename='js/app/chart_tv_lifecycle.js') }}?v=20260808i"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_shell.js') }}?v=20260808i"></script>
|
<script defer src="{{ url_for('static', filename='js/app/chart_tv_shell.js') }}?v=20260809j"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_indicators.js') }}?v=20260808i"></script>
|
<script defer src="{{ url_for('static', filename='js/app/chart_tv_indicators.js') }}?v=20260808i"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_chan.js') }}?v=20260808i"></script>
|
<script defer src="{{ url_for('static', filename='js/app/chart_tv_chan.js') }}?v=20260808i"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_overlays.js') }}?v=20260808i"></script>
|
<script defer src="{{ url_for('static', filename='js/app/chart_tv_overlays.js') }}?v=20260809j"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_finalize.js') }}?v=20260809d"></script>
|
<script defer src="{{ url_for('static', filename='js/app/chart_tv_finalize.js') }}?v=20260809d"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv.js') }}?v=20260808i"></script>
|
<script defer src="{{ url_for('static', filename='js/app/chart_tv.js') }}?v=20260808i"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/chart_sync.js') }}?v=20260808i"></script>
|
<script defer src="{{ url_for('static', filename='js/app/chart_sync.js') }}?v=20260809i"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/chart_tables.js') }}?v=20260808i"></script>
|
<script defer src="{{ url_for('static', filename='js/app/chart_tables.js') }}?v=20260808i"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/ui.js') }}?v=20260809d"></script>
|
<script defer src="{{ url_for('static', filename='js/app/ui.js') }}?v=20260809j"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/overlays.js') }}?v=20260808i"></script>
|
<script defer src="{{ url_for('static', filename='js/app/overlays.js') }}?v=20260808i"></script>
|
||||||
<script defer src="{{ url_for('static', filename='js/app/main.js') }}?v=20260808i"></script>
|
<script defer src="{{ url_for('static', filename='js/app/main.js') }}?v=20260808i"></script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user