feat(ECR-007): Wyckoff Live Structure with Confirmed/Live isolation

Add live.py lifecycle and event candidates; assemble confirmed vs live
in engine; Summary partition; execution_signal source=confirmed only.
Keep strategies untouched; do not lower Confirmed thresholds for Live.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-07 03:14:19 +08:00
co-authored by Cursor
parent 1e60ab3bfa
commit 276481e02c
33 changed files with 2527 additions and 401 deletions
+244 -166
View File
@@ -40,6 +40,12 @@ function disposeTradingViewCharts() {
var chartRoot = document.getElementById('tradingview_chart');
if (chartRoot) {
// 重建前救出 Cycle Summary,避免 innerHTML 清空时被销毁
var summaryEl = document.getElementById('wyckoffCycleSummary');
var chartHost = chartRoot.parentElement;
if (summaryEl && chartRoot.contains(summaryEl) && chartHost) {
chartHost.appendChild(summaryEl);
}
chartRoot.innerHTML = '';
}
} catch (e) {
@@ -415,6 +421,21 @@ function initTradingView(symbol, timeframe) {
// 创建主图表
const mainChart = LightweightCharts.createChart(mainChartContainer, createChartOptions(true, 'main'));
// Cycle Summary 挂到主图左下角(相对 K 线主图 pane,而非整图底边)
(function mountWyckoffCycleSummary() {
var summaryEl = document.getElementById('wyckoffCycleSummary');
if (!summaryEl) {
summaryEl = document.createElement('div');
summaryEl.id = 'wyckoffCycleSummary';
summaryEl.className = 'wyckoff-cycle-summary';
summaryEl.setAttribute('aria-live', 'polite');
}
mainChartContainer.appendChild(summaryEl);
if (typeof renderWyckoffCycleSummary === 'function') {
try { renderWyckoffCycleSummary(); } catch (e) {}
}
})();
// 创建成交量图表 - 只显示底部的时间轴
const volumeChart = LightweightCharts.createChart(volumeChartContainer, createChartOptions(false, 'volume'));
@@ -2199,175 +2220,229 @@ function initTradingView(symbol, timeframe) {
}
} catch (e) { console.error('结构区整体绘制出错:', e); }
}
// 威科夫叠层:区间 / 阶段 / 事件 / VP
if ($('#showWyckoff').is(':checked') && currentData.wyckoff) {
try {
const w = currentData.wyckoff;
const tr = w.trading_range;
const parseTs = function(t) {
if (t == null) return NaN;
if (typeof t === 'number') return Math.floor(t > 1e12 ? t / 1000 : t);
const ms = new Date(t).getTime();
return isNaN(ms) ? NaN : Math.floor(ms / 1000);
};
const kd = currentData.kline_data || [];
const chartEnd = kd.length
? Math.floor(new Date(kd[kd.length - 1].date).getTime() / 1000)
: NaN;
// 区间/阶段/时间/VP:框线同中枢;标记并入主 K(同 BSP),时间对齐 candles
(function drawWrLikeChan() {
const candleSeries = tvWidget.series.candleSeries
|| tvWidget.series.barSeries
|| tvWidget.series.lineSeries
|| tvWidget.series.areaSeries
|| tvWidget.series.heikinSeries
|| tvWidget.series.renkoSeries;
const candleTimes = (candles || []).map(function(c) { return c.time; })
.filter(function(t) { return t != null && !isNaN(t); });
const barStep = (candleTimes.length >= 2)
? Math.max(1, candleTimes[1] - candleTimes[0])
: 3600;
const lastKlineTime = candleTimes.length ? candleTimes[candleTimes.length - 1] : NaN;
const chartStart = candleTimes.length ? candleTimes[0] : NaN;
if ($('#showWyckoffRange').is(':checked') && tr) {
const t0 = parseTs(tr.start_time);
const t1 = tr.end_time ? parseTs(tr.end_time) : chartEnd;
const hi = parseFloat(tr.high), lo = parseFloat(tr.low), mid = parseFloat(tr.mid);
if (!isNaN(t0) && !isNaN(t1) && !isNaN(hi) && !isNaN(lo)) {
const fill = 'rgba(52, 152, 219, 0.07)';
const border = 'rgba(52, 152, 219, 0.75)';
// ECR-004:填充线 6→3,减 series
const fillLines = 3;
const step = (hi - lo) / (fillLines + 1);
for (let fi = 1; fi <= fillLines; fi++) {
const fy = lo + step * fi;
mainChart.addLineSeries({ color: fill, lineWidth: 2, lastValueVisible: false, priceLineVisible: false })
.setData([{ time: t0, value: fy }, { time: t1, value: fy }]);
}
mainChart.addLineSeries({ color: border, lineWidth: 2, lastValueVisible: false, priceLineVisible: false })
.setData([{ time: t0, value: hi }, { time: t1, value: hi }]);
mainChart.addLineSeries({ color: border, lineWidth: 2, lastValueVisible: false, priceLineVisible: false })
.setData([{ time: t0, value: lo }, { time: t1, value: lo }]);
if (!isNaN(mid)) {
mainChart.addLineSeries({ color: border, lineWidth: 1, lineStyle: 2, lastValueVisible: false, priceLineVisible: false })
.setData([{ time: t0, value: mid }, { time: t1, value: mid }]);
}
mainChart.addLineSeries({ color: border, lineWidth: 1, lastValueVisible: false, priceLineVisible: false })
.setData([{ time: t0, value: lo }, { time: t0, value: hi }]);
mainChart.addLineSeries({ color: border, lineWidth: 1, lastValueVisible: false, priceLineVisible: false })
.setData([{ time: t1, value: lo }, { time: t1, value: hi }]);
}
const toSec = function(t) {
if (t == null) return NaN;
if (typeof t === 'number') return t > 1e12 ? Math.floor(t / 1000) : Math.floor(t);
const ms = new Date(t).getTime();
return isNaN(ms) ? NaN : Math.floor(ms / 1000);
};
// 标记必须落在主 series 的 time 上(与 KLC 趋势 nearestTime 同思路)
const snapToCandle = function(t) {
if (!candleTimes.length || isNaN(t)) return t;
let best = candleTimes[0], bd = Math.abs(candleTimes[0] - t);
for (let i = 1; i < candleTimes.length; i++) {
const d = Math.abs(candleTimes[i] - t);
if (d < bd) { bd = d; best = candleTimes[i]; }
}
if ($('#showWyckoffPhases').is(':checked') && w.phases && w.phases.length) {
const phaseColors = {
A: 'rgba(241, 196, 15, 0.85)',
B: 'rgba(155, 89, 182, 0.85)',
C: 'rgba(230, 126, 34, 0.85)',
D: 'rgba(46, 204, 113, 0.85)',
E: 'rgba(52, 152, 219, 0.85)'
};
const phaseMarkers = [];
w.phases.forEach(function(ph) {
const t0 = parseTs(ph.start_time);
const t1 = ph.end_time ? parseTs(ph.end_time) : chartEnd;
if (isNaN(t0) || isNaN(t1) || !tr) return;
const hi = parseFloat(tr.high);
if (isNaN(hi)) return;
const col = phaseColors[ph.phase] || 'rgba(149,165,166,0.85)';
// 阶段顶部分段色带(略高于区间高)
const y = hi * 1.002;
mainChart.addLineSeries({ color: col, lineWidth: 3, lastValueVisible: false, priceLineVisible: false })
.setData([{ time: t0, value: y }, { time: t1, value: y }]);
phaseMarkers.push({
time: t0,
position: 'aboveBar',
color: col,
shape: 'square',
text: String(ph.phase || ph.label || ''),
size: 1
});
return best;
};
const ensureSpan = function(t0, t1) {
if (isNaN(t0) || isNaN(t1)) return [t0, t1];
if (t1 < t0) { const x = t0; t0 = t1; t1 = x; }
if (t1 <= t0) t1 = t0 + barStep;
return [t0, t1];
};
const drawBox = function(t0, t1, hi, lo, color) {
const span = ensureSpan(t0, t1);
t0 = span[0]; t1 = span[1];
if (isNaN(t0) || isNaN(t1) || isNaN(hi) || isNaN(lo)) return;
const opt = { color: color, lineWidth: 1, lastValueVisible: false, priceLineVisible: false };
mainChart.addLineSeries(opt).setData([{ time: t0, value: hi }, { time: t1, value: hi }]);
mainChart.addLineSeries(opt).setData([{ time: t0, value: lo }, { time: t1, value: lo }]);
mainChart.addLineSeries(opt).setData([{ time: t0, value: lo }, { time: t0, value: hi }]);
mainChart.addLineSeries(opt).setData([{ time: t1, value: lo }, { time: t1, value: hi }]);
};
const mkPL = function(price, color, title, style) {
if (!candleSeries) return;
const p = parseFloat(price);
if (isNaN(p)) return;
try {
candleSeries.createPriceLine({
price: p, color: color,
lineWidth: style === 2 ? 1 : 2,
lineStyle: style || 0,
axisLabelVisible: true,
title: title
});
if (phaseMarkers.length) {
const phSeries = mainChart.addLineSeries({ lastValueVisible: false, priceLineVisible: false });
phSeries.setMarkers(phaseMarkers);
}
}
} catch (e) { console.warn('价位线失败', title, e); }
};
const phaseColors = { A: '#f1c40f', B: '#9b59b6', C: '#e67e22', D: '#2ecc71', E: '#3498db' };
const eventColors = {
Spring: '#27ae60', SOS: '#2ecc71', LPS: '#16a085',
UTAD: '#e74c3c', SOW: '#c0392b', LPSY: '#d35400'
};
if ($('#showWyckoffEvents').is(':checked') && w.events && w.events.length) {
const eventColors = {
Spring: '#27ae60',
SOS: '#2ecc71',
LPS: '#16a085',
UTAD: '#e74c3c',
SOW: '#c0392b',
LPSY: '#d35400'
};
const checks = (w.volume_confirm && w.volume_confirm.event_checks) || {};
const markers = [];
w.events.forEach(function(ev) {
const t = parseTs(ev.time);
if (isNaN(t)) return;
const typ = ev.type || '';
const chk = checks[typ] || {};
const volOk = (chk.volume_ok != null) ? chk.volume_ok : ev.volume_ok;
const ratioVal = (chk.volume_ratio != null) ? chk.volume_ratio : ev.volume_ratio;
const ok = volOk === true ? '✓' : (volOk === false ? '✗' : '');
const note = ev.note || '';
const ratio = (ratioVal != null) ? (' vol×' + Number(ratioVal).toFixed(2)) : '';
markers.push({
time: t,
position: (typ === 'Spring' || typ === 'LPS' || typ === 'SOW') ? 'belowBar' : 'aboveBar',
color: eventColors[typ] || '#7f8c8d',
shape: 'arrowUp',
text: typ + (ok ? ' ' + ok : '') + (note ? ' ' + note : '') + ratio,
size: 1
});
});
if (markers.length) {
const evSeries = mainChart.addLineSeries({ lastValueVisible: false, priceLineVisible: false });
evSeries.setMarkers(markers);
}
}
const wrMarkers = [];
window.wrMarkers = [];
const pushMarker = function(m) {
if (!m || isNaN(m.time)) return;
m.time = snapToCandle(m.time);
if (!isNaN(chartStart) && (m.time < chartStart || m.time > lastKlineTime)) return;
wrMarkers.push(m);
};
if ($('#showWyckoffVP').is(':checked') && w.volume_profile && tr) {
const vp = w.volume_profile;
const t1 = tr.end_time ? parseTs(tr.end_time) : chartEnd;
if (!isNaN(t1)) {
const bins = vp.bins || [];
// ECR-004 A+C:只画有量 Top-N,避免每 bin 一条 series
const TOP_N = 8;
const ranked = bins
.filter(function(b) { return b && b.volume > 0; })
.slice()
.sort(function(a, b) { return b.volume - a.volume; })
.slice(0, TOP_N);
let maxVol = 0;
ranked.forEach(function(b) { if (b.volume > maxVol) maxVol = b.volume; });
const tStart = parseTs(tr.start_time);
const maxWidthSec = Math.max(60, Math.floor((t1 - (isNaN(tStart) ? t1 : tStart)) * 0.15));
ranked.forEach(function(b) {
if (!b.volume || maxVol <= 0) return;
const wSec = Math.max(1, Math.floor(maxWidthSec * (b.volume / maxVol)));
const alpha = 0.2 + 0.55 * (b.volume / maxVol);
const leftT = Math.max(isNaN(tStart) ? (t1 - wSec) : tStart, t1 - wSec);
mainChart.addLineSeries({
color: 'rgba(142, 68, 173, ' + alpha.toFixed(2) + ')',
lineWidth: 1,
lastValueVisible: false,
priceLineVisible: false
}).setData([
{ time: leftT, value: b.price },
{ time: t1, value: b.price }
]);
});
const levels = [
{ p: vp.poc, c: 'rgba(142, 68, 173, 0.95)', w: 2, style: 0 },
{ p: vp.vah, c: 'rgba(155, 89, 182, 0.7)', w: 1, style: 2 },
{ p: vp.val, c: 'rgba(155, 89, 182, 0.7)', w: 1, style: 2 }
];
const t0 = parseTs(tr.start_time);
levels.forEach(function(lv) {
const p = parseFloat(lv.p);
if (isNaN(p) || isNaN(t0)) return;
mainChart.addLineSeries({
color: lv.c,
lineWidth: lv.w,
lineStyle: lv.style,
lastValueVisible: false,
priceLineVisible: false
}).setData([{ time: t0, value: p }, { time: t1, value: p }]);
});
}
}
} catch (e) { console.error('威科夫绘制出错:', e); }
}
const drawOne = function(w, cfg) {
if (!w) return;
const showR = $(cfg.rangeSel).is(':checked');
const showP = $(cfg.phasesSel).is(':checked');
const showE = $(cfg.eventsSel).is(':checked');
const showV = $(cfg.vpSel).is(':checked');
if (!showR && !showP && !showE && !showV) return;
// WYCKOFF-MULTI-CYCLE-001:遍历 cycles;无则退化为顶层单段
const cycles = (w.cycles && w.cycles.length)
? w.cycles
: (w.trading_range ? [{
id: 0, status: 'ACTIVE', role: 'latest',
trading_range: w.trading_range, phases: w.phases, events: w.events,
volume_profile: w.volume_profile, volume_confirm: w.volume_confirm,
period: { start_time: w.trading_range.start_time, end_time: w.trading_range.end_time, bars: w.trading_range.bars }
}] : []);
const tfLabel = (cfg.tfLabel || cfg.tag || 'TF').toString().toUpperCase();
cycles.forEach(function(cycle) {
const tr = cycle.trading_range;
if (!tr) return;
const cid = (cycle.id != null) ? cycle.id : 0;
const isActive = String(cycle.status || '').toUpperCase() === 'ACTIVE';
const cTag = tfLabel + ' C' + cid + ' ';
try {
let t0 = toSec(tr.start_time || (cycle.period && cycle.period.start_time));
let t1 = toSec(tr.end_time || (cycle.period && cycle.period.end_time));
// 仅 ACTIVE 可拉到最新 K;历史用 period.end
if (isActive && !isNaN(lastKlineTime)) {
t1 = lastKlineTime;
} else if (cycle.period && cycle.period.end_time) {
t1 = toSec(cycle.period.end_time);
}
const hi = parseFloat(tr.high), lo = parseFloat(tr.low);
if (showR && !isNaN(hi) && !isNaN(lo)) {
drawBox(t0, t1, hi, lo, cfg.color);
if (isActive) {
mkPL(hi, cfg.color, cfg.hiTag, 0);
mkPL(lo, cfg.color, cfg.loTag, 0);
mkPL(tr.mid, cfg.color, cfg.midTag, 2);
}
}
if (showP && cycle.phases && cycle.phases.length && !isNaN(hi)) {
cycle.phases.forEach(function(ph) {
let p0 = toSec(ph.start_time);
let p1 = ph.end_time ? toSec(ph.end_time) : t1;
const sp = ensureSpan(p0, p1);
p0 = sp[0]; p1 = sp[1];
if (isNaN(p0) || isNaN(p1)) return;
const col = phaseColors[ph.phase] || '#95a5a6';
mainChart.addLineSeries({
color: col, lineWidth: 3, lastValueVisible: false, priceLineVisible: false
}).setData([{ time: p0, value: hi }, { time: p1, value: hi }]);
pushMarker({
time: p0, position: 'aboveBar', color: col, shape: 'square',
text: cTag + 'Phase ' + String(ph.phase || ''), size: 1
});
});
}
if (showV && isActive && cycle.volume_profile) {
const vp = cycle.volume_profile;
mkPL(vp.poc, cfg.vpColor, cfg.pocTag, 0);
mkPL(vp.vah, cfg.vpColor, cfg.vahTag, 2);
mkPL(vp.val, cfg.vpColor, cfg.valTag, 2);
const bins = (vp.bins || []).filter(function(b) { return b && b.volume > 0; })
.slice().sort(function(a, b) { return b.volume - a.volume; }).slice(0, 8);
let maxVol = 0;
bins.forEach(function(b) { if (b.volume > maxVol) maxVol = b.volume; });
const span = (!isNaN(t0) && !isNaN(t1) && t1 > t0) ? (t1 - t0) : barStep * 12;
bins.forEach(function(b) {
if (!b.volume || maxVol <= 0 || isNaN(t1)) return;
const wSec = Math.max(barStep, Math.floor(span * 0.12 * (b.volume / maxVol)));
let leftT = Math.max(isNaN(t0) ? (t1 - wSec) : t0, t1 - wSec);
if (leftT >= t1) leftT = t1 - barStep;
if (leftT >= t1) return;
const alpha = 0.25 + 0.55 * (b.volume / maxVol);
mainChart.addLineSeries({
color: cfg.vpRgb.replace('ALPHA', alpha.toFixed(2)),
lineWidth: 2, lastValueVisible: false, priceLineVisible: false
}).setData([{ time: leftT, value: b.price }, { time: t1, value: b.price }]);
});
}
if (showE && cycle.events && cycle.events.length) {
const checks = (cycle.volume_confirm && cycle.volume_confirm.event_checks) || {};
cycle.events.forEach(function(ev) {
const t = toSec(ev.time);
if (isNaN(t)) return;
const typ = ev.type || '';
const chk = checks[typ] || {};
const volOk = (chk.volume_ok != null) ? chk.volume_ok : ev.volume_ok;
const ok = volOk === true ? '✓' : (volOk === false ? '✗' : '');
pushMarker({
time: t,
position: (typ === 'Spring' || typ === 'LPS' || typ === 'SOW') ? 'belowBar' : 'aboveBar',
color: eventColors[typ] || '#7f8c8d',
shape: (typ === 'Spring' || typ === 'SOW' || typ === 'LPS') ? 'arrowDown' : 'arrowUp',
text: cTag + typ + ok,
size: 1
});
});
}
} catch (e) { console.error('区间叠层出错', cfg.name, 'C' + cid, e); }
});
};
if ($('#showMainWrRange').is(':checked') || $('#showMainWrPhases').is(':checked')
|| $('#showMainWrEvents').is(':checked') || $('#showMainWrVP').is(':checked')) {
drawOne(currentData.wyckoff, {
name: '主', tag: '', tfLabel: (currentData.timeframe || '4H'), color: '#3498db', vpColor: '#8e44ad',
vpRgb: 'rgba(142, 68, 173, ALPHA)',
rangeSel: '#showMainWrRange', phasesSel: '#showMainWrPhases',
eventsSel: '#showMainWrEvents', vpSel: '#showMainWrVP',
hiTag: 'WR.H', loTag: 'WR.L', midTag: 'WR.M',
pocTag: 'POC', vahTag: 'VAH', valTag: 'VAL'
});
}
if ($('#showElementWrRange').is(':checked') || $('#showElementWrPhases').is(':checked')
|| $('#showElementWrEvents').is(':checked') || $('#showElementWrVP').is(':checked')) {
drawOne(currentData.element_wyckoff, {
name: '次', tag: 'e', tfLabel: (currentData.element_timeframe || '2H'), color: '#e67e22', vpColor: '#d35400',
vpRgb: 'rgba(211, 84, 0, ALPHA)',
rangeSel: '#showElementWrRange', phasesSel: '#showElementWrPhases',
eventsSel: '#showElementWrEvents', vpSel: '#showElementWrVP',
hiTag: 'eWR.H', loTag: 'eWR.L', midTag: 'eWR.M',
pocTag: 'ePOC', vahTag: 'eVAH', valTag: 'eVAL'
});
}
if ($('#showSubSubWrRange').is(':checked') || $('#showSubSubWrPhases').is(':checked')
|| $('#showSubSubWrEvents').is(':checked') || $('#showSubSubWrVP').is(':checked')) {
drawOne(currentData.sub_sub_wyckoff, {
name: '次次', tag: 's', tfLabel: (currentData.sub_sub_timeframe || '1H'), color: '#27ae60', vpColor: '#16a085',
vpRgb: 'rgba(22, 160, 133, ALPHA)',
rangeSel: '#showSubSubWrRange', phasesSel: '#showSubSubWrPhases',
eventsSel: '#showSubSubWrEvents', vpSel: '#showSubSubWrVP',
hiTag: 'sWR.H', loTag: 'sWR.L', midTag: 'sWR.M',
pocTag: 'sPOC', vahTag: 'sVAH', valTag: 'sVAL'
});
}
// 同 BSP:写入 window,稍后与分型/买卖点一并 setMarkers
wrMarkers.sort(function(a, b) { return a.time - b.time; });
window.wrMarkers = wrMarkers;
if (typeof renderWyckoffCycleSummary === 'function') {
renderWyckoffCycleSummary();
}
})();
// 显示未完成中枢 - 分别处理主周期、次周期和次次周期
if ($('#showMainZs').is(':checked') || $('#showElementZs').is(':checked') || $('#showSubSubZs').is(':checked') || $('#showSubSubBiZs').is(':checked')) {
console.log('绘制未完成中枢 - 已启用');
@@ -4223,7 +4298,8 @@ function initTradingView(symbol, timeframe) {
...(window.kluDivMarkersElement || []),
...(window.kluDivMarkersSubSub || []),
...trendMarkersToUse,
...(window.bspMarkers || [])
...(window.bspMarkers || []),
...(window.wrMarkers || [])
];
if (combinedMarkers.length > 0) {
console.log(
@@ -4232,6 +4308,7 @@ function initTradingView(symbol, timeframe) {
'个,小周期分型:', allElementFxMarkers.length,
'个,UnitTF:', (window.unittfMarkers || []).length,
'个,BSP标记:', (window.bspMarkers || []).length,
'个,区间标记:', (window.wrMarkers || []).length,
'个)'
);
@@ -4358,7 +4435,8 @@ function initTradingView(symbol, timeframe) {
...(window.kluDivMarkersElement || []),
...(window.kluDivMarkersSubSub || []),
...trendMarkersToUse,
...(window.bspMarkers || [])
...(window.bspMarkers || []),
...(window.wrMarkers || [])
];
if (onlyMainAndU.length > 0) {
console.log('仅设置', onlyMainAndU.length, '个主周期/UnitTF标记(主周期分型:', (window.mainFxMarkers || []).length, 'UnitTF:', (window.unittfMarkers || []).length, '');