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:
@@ -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
@@ -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, ')');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user