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
+3
View File
@@ -1,6 +1,9 @@
/* chart_format.js — split from chart.js */
/* chart.js */
function updateChartDisplay() {
if (typeof renderWyckoffCycleSummary === 'function') {
renderWyckoffCycleSummary();
}
if (currentData) {
// 检测K线周期是否切换
const curPeriod = $('#subSubPeriodKline').is(':checked') ? 'subsub' :
+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, '');
+6 -3
View File
@@ -13,7 +13,7 @@ function updateChart(options) {
symbol = $('#astockSymbol').val() || '000001';
}
const timeframe = $('#timeframe').val() || window.DEFAULT_MAIN_TIMEFRAME || '5m';
const timeframe = $('#timeframe').val() || window.DEFAULT_MAIN_TIMEFRAME || '4h';
const timezone = $('#timezone').val() || 'Asia/Shanghai';
const elementTimeframe = $('#elementTimeframe').val() || window.DEFAULT_ELEMENT_TIMEFRAME || '1m';
const subSubTimeframe = $('#subSubTimeframe').val() || '';
@@ -62,8 +62,8 @@ function updateChart(options) {
end_time: endTimeMs,
elements_only: false,
zone_kl_lines: parseInt($('#zoneKlLines').val()) || 1000,
include_structure_zones: $('#showMainStructureZone').is(':checked') ? 1 : 0,
include_wyckoff: $('#showWyckoff').is(':checked') ? 1 : 0
include_structure_zones: $('#showMainStructureZone').is(':checked') ? 1 : 0
// 威科夫随主分析一并返回;开关仅控制绘制,不再传 include_wyckoff
},
success: function(data) {
// 隐藏加载图标
@@ -81,6 +81,9 @@ function updateChart(options) {
delete currentData.original_macd;
}
currentData = data;
if (typeof renderWyckoffCycleSummary === 'function') {
renderWyckoffCycleSummary();
}
refreshChart(data, {
incremental: options.incremental !== undefined
+8 -17
View File
@@ -93,25 +93,16 @@ $(document).on('change', '#showMainStructureZone', function() {
}
});
// 威科夫主开关:勾选才请求;子项仅本地重绘
function syncWyckoffSubControls() {
const on = $('#showWyckoff').is(':checked');
$('#showWyckoffRange, #showWyckoffPhases, #showWyckoffEvents, #showWyckoffVP').prop('disabled', !on);
}
$(document).on('change', '#showWyckoff', function() {
const on = $('#showWyckoff').is(':checked');
syncWyckoffSubControls();
console.log('威科夫切换为:', on);
if (on) {
updateChart();
} else {
// 区间/阶段/时间/VP:与缠论笔开关一样,本地重绘
$(document).on(
'change',
'#showMainWrRange, #showMainWrPhases, #showMainWrEvents, #showMainWrVP,' +
'#showElementWrRange, #showElementWrPhases, #showElementWrEvents, #showElementWrVP,' +
'#showSubSubWrRange, #showSubSubWrPhases, #showSubSubWrEvents, #showSubSubWrVP',
function() {
updateChartDisplay();
}
});
$(document).on('change', '#showWyckoffRange, #showWyckoffPhases, #showWyckoffEvents, #showWyckoffVP', function() {
updateChartDisplay();
});
$(function() { syncWyckoffSubControls(); });
);
// 添加趋势显示复选框变更事件(主/元素),变更后刷新主图
$('#showMainTrend').change(function() {
+252 -3
View File
@@ -1,4 +1,252 @@
/* ui.js */
/** Trading OS 可消费的威科夫 Cycle 摘要(Confirmed + Live 分区;cycles[0]=ACTIVE */
function buildWyckoffCycleSummaryPayload(w, tf) {
if (!w) return null;
const cycles = (w.cycles && w.cycles.length)
? w.cycles
: (w.trading_range ? [{
id: 0, status: 'ACTIVE', role: 'latest', lifecycle: w.lifecycle || 'UNKNOWN',
trading_range: w.trading_range, bias: w.bias,
phases: w.phases || [], events: w.events || [],
confirmed: { phases: w.phases || [], events: w.events || [] },
live: w.live || null,
confidence: { overall: null },
period: {
start_time: w.trading_range.start_time,
end_time: w.trading_range.end_time,
bars: w.trading_range.bars
}
}] : []);
if (!cycles.length) return null;
const active = cycles[0]; // 禁止 cycles[-1]
const confirmed = active.confirmed || {
phases: active.phases || w.phases || [],
events: active.events || w.events || []
};
const live = active.live || w.live || null;
const cPhases = confirmed.phases || [];
const cEvents = confirmed.events || [];
const lastPhase = cPhases.length ? cPhases[cPhases.length - 1] : null;
const lastEvent = cEvents.length ? cEvents[cEvents.length - 1] : null;
const tr = active.trading_range || {};
const prev = cycles.length > 1 ? cycles[1] : null;
const biasLabel = ({
accumulation: 'Accumulation',
distribution: 'Distribution',
unknown: 'Unknown'
})[active.bias] || (active.bias || 'Unknown');
const liveCand = (live && live.event_candidates && live.event_candidates[0]) || null;
const liveConf = live && live.confidence ? live.confidence.overall : null;
return {
symbol: (typeof currentData !== 'undefined' && currentData && currentData.symbol) || $('#symbol').val() || '',
timeframe: (tf || w.timeframe || $('#timeframe').val() || '').toString().toUpperCase(),
active: {
cycle_id: active.id != null ? active.id : 0,
status: active.status || 'ACTIVE',
lifecycle: active.lifecycle || (live && live.lifecycle) || 'UNKNOWN',
structure: biasLabel,
phase_confirmed: lastPhase ? String(lastPhase.phase || '') : null,
event_confirmed: lastEvent ? String(lastEvent.type || '') : null,
phase_candidate: live ? live.phase_candidate : null,
event_candidate: liveCand ? liveCand.type : null,
event_candidate_confidence: liveCand ? liveCand.confidence : null,
next_expected: live ? live.next_expected : null,
range: {
low: tr.low,
high: tr.high,
start_time: (active.period && active.period.start_time) || tr.start_time,
end_time: (active.period && active.period.end_time) || tr.end_time,
bars: (active.period && active.period.bars) != null ? active.period.bars : tr.bars
},
confidence_confirmed: (active.confidence && active.confidence.overall != null)
? active.confidence.overall
: null,
confidence_live: liveConf
},
confirmed_history: cycles.slice(1, 4).map(function(c) {
const evs = ((c.confirmed && c.confirmed.events) || c.events || [])
.map(function(e) { return e.type; }).filter(Boolean);
return {
cycle_id: c.id,
structure: ({
accumulation: 'Accumulation',
distribution: 'Distribution',
unknown: 'Unknown'
})[c.bias] || c.bias,
events: evs,
lifecycle: c.lifecycle || 'COMPLETED'
};
}),
live: live,
cycle_count: cycles.length
};
}
function _wrLayerTogglesOn(prefix) {
// prefix: Main | Element | SubSub
return $('#show' + prefix + 'WrRange').is(':checked')
|| $('#show' + prefix + 'WrPhases').is(':checked')
|| $('#show' + prefix + 'WrEvents').is(':checked')
|| $('#show' + prefix + 'WrVP').is(':checked');
}
/** 面板展示用中文(机器可读 payload 仍保留英文原值) */
function _wcsLifecycleZh(v) {
return ({
UNKNOWN: '未知',
FORMING: '形成中',
CONFIRMED: '已确认',
COMPLETED: '已完成',
ACTIVE: '当前'
})[v] || v || '未知';
}
function _wcsStructureZh(v) {
if (!v) return '—';
const key = String(v).toLowerCase();
return ({
accumulation: '吸筹',
distribution: '派发',
unknown: '未知'
})[key] || ({
Accumulation: '吸筹',
Distribution: '派发',
Unknown: '未知'
})[v] || v;
}
function _wcsEventZh(v) {
if (v == null || v === '') return '—';
return ({
Spring: '弹簧',
UTAD: '上升后派发',
SOS: '强势信号',
SOW: '弱势信号',
LPS: '最后支撑',
LPSY: '最后供应',
Test: '回测',
PSY: '初步供应',
BC: '买气高潮',
AR: '自动回落',
ST: '二次测试',
SC: '卖气高潮'
})[v] || v;
}
function _htmlWyckoffSummaryBlock(payload, blockClass) {
if (!payload || !payload.active) return '';
const a = payload.active;
const fmtPx = function(v) {
if (v == null || isNaN(Number(v))) return '—';
const n = Number(v);
return n >= 1000 ? n.toFixed(1) : n.toFixed(4);
};
const pct = function(v) {
if (v == null || isNaN(Number(v))) return '—';
return Math.round(Number(v) * 100) + '%';
};
let html = '<div class="wcs-block ' + (blockClass || '') + '">';
html += '<div class="wcs-title">' + (payload.symbol || '') + ' '
+ (payload.timeframe || '') + '</div>';
html += '<div><span class="wcs-badge">当前 C' + a.cycle_id + '</span> '
+ '<span class="wcs-badge" style="background:#fff8c5;color:#9a6700;">'
+ _wcsLifecycleZh(a.lifecycle) + '</span></div>';
html += '<div class="wcs-active">';
html += '<div class="wcs-row"><span class="wcs-k">结构</span><span class="wcs-v">'
+ _wcsStructureZh(a.structure) + '</span></div>';
html += '<div class="wcs-row"><span class="wcs-k">阶段</span><span class="wcs-v">'
+ (a.phase_candidate
? ('阶段 ' + a.phase_candidate + '(候选)')
: (a.phase_confirmed ? ('阶段 ' + a.phase_confirmed) : '—'))
+ '</span></div>';
html += '<div class="wcs-row"><span class="wcs-k">事件</span><span class="wcs-v">'
+ (a.event_candidate
? (_wcsEventZh(a.event_candidate) + '(候选)')
: _wcsEventZh(a.event_confirmed))
+ '</span></div>';
if (a.event_confirmed && a.event_candidate) {
html += '<div class="wcs-row"><span class="wcs-k">已确认</span><span class="wcs-v">'
+ _wcsEventZh(a.event_confirmed) + '</span></div>';
}
html += '<div class="wcs-row"><span class="wcs-k">区间</span><span class="wcs-v">'
+ fmtPx(a.range && a.range.low) + ' ' + fmtPx(a.range && a.range.high) + '</span></div>';
html += '<div class="wcs-row"><span class="wcs-k">置信度</span><span class="wcs-v">'
+ pct(a.confidence_live != null ? a.confidence_live : a.confidence_confirmed) + '</span></div>';
if (a.next_expected) {
html += '<div class="wcs-row"><span class="wcs-k">下一步</span><span class="wcs-v">'
+ _wcsEventZh(a.next_expected) + '</span></div>';
}
html += '</div>';
if (payload.confirmed_history && payload.confirmed_history.length) {
html += '<div class="wcs-prev"><div style="margin-bottom:2px;">已确认历史</div>';
payload.confirmed_history.forEach(function(h) {
const ev = (h.events && h.events.length)
? h.events.map(_wcsEventZh).join('、')
: '—';
html += '<div>C' + h.cycle_id + ' ' + _wcsStructureZh(h.structure) + ' · ' + ev + '</div>';
});
html += '</div>';
}
html += '</div>';
return html;
}
function renderWyckoffCycleSummary() {
const $el = $('#wyckoffCycleSummary');
if (!$el.length) return;
if (!currentData) {
$el.hide().empty();
window.wyckoffCycleSummary = null;
return;
}
const layers = [];
if (_wrLayerTogglesOn('Main') && currentData.wyckoff) {
layers.push({
key: 'main',
cls: 'wcs-main',
payload: buildWyckoffCycleSummaryPayload(
currentData.wyckoff,
currentData.timeframe || currentData.wyckoff.timeframe || $('#timeframe').val()
)
});
}
if (_wrLayerTogglesOn('Element') && currentData.element_wyckoff) {
layers.push({
key: 'element',
cls: 'wcs-element',
payload: buildWyckoffCycleSummaryPayload(
currentData.element_wyckoff,
currentData.element_timeframe || currentData.element_wyckoff.timeframe || $('#elementTimeframe').val()
)
});
}
if (_wrLayerTogglesOn('SubSub') && currentData.sub_sub_wyckoff) {
layers.push({
key: 'sub_sub',
cls: 'wcs-subsub',
payload: buildWyckoffCycleSummaryPayload(
currentData.sub_sub_wyckoff,
currentData.sub_sub_timeframe || currentData.sub_sub_wyckoff.timeframe || $('#subSubTimeframe').val()
)
});
}
const valid = layers.filter(function(L) { return L.payload && L.payload.active; });
if (!valid.length) {
$el.hide().empty();
window.wyckoffCycleSummary = null;
return;
}
const bag = {};
let html = '';
valid.forEach(function(L) {
bag[L.key] = L.payload;
html += _htmlWyckoffSummaryBlock(L.payload, L.cls);
});
window.wyckoffCycleSummary = bag;
$el.html(html).show();
}
function loadSymbols() {
$.get('/api/symbols', function(data) {
if (Array.isArray(data)) {
@@ -24,14 +272,15 @@ function loadSymbols() {
});
}
// 设置默认时间范围
// 设置默认时间范围(需覆盖威科夫 lookback;1 天在 4h/1h 上几乎检不出区间)
function setDefaultTimeRange() {
const now = new Date();
const oneDayAgo = new Date(now.getTime() - (24 * 60 * 60 * 1000));
const daysBack = 14;
const start = new Date(now.getTime() - (daysBack * 24 * 60 * 60 * 1000));
// 格式化为datetime-local输入框所需的格式 YYYY-MM-DDThh:mm
$('#end_time').val(formatDatetimeLocal(now));
$('#start_time').val(formatDatetimeLocal(oneDayAgo));
$('#start_time').val(formatDatetimeLocal(start));
}
// 格式化日期为datetime-local输入框格式
function formatDatetimeLocal(date) {