/* 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 = '
';
html += '
' + (payload.symbol || '') + ' '
+ (payload.timeframe || '') + '
';
html += '
当前 C' + a.cycle_id + ' '
+ ''
+ _wcsLifecycleZh(a.lifecycle) + '
';
html += '
';
html += '
结构'
+ _wcsStructureZh(a.structure) + '
';
html += '
阶段'
+ (a.phase_candidate
? ('阶段 ' + a.phase_candidate + '(候选)')
: (a.phase_confirmed ? ('阶段 ' + a.phase_confirmed) : '—'))
+ '
';
html += '
事件'
+ (a.event_candidate
? (_wcsEventZh(a.event_candidate) + '(候选)')
: _wcsEventZh(a.event_confirmed))
+ '
';
if (a.event_confirmed && a.event_candidate) {
html += '
已确认'
+ _wcsEventZh(a.event_confirmed) + '
';
}
html += '
区间'
+ fmtPx(a.range && a.range.low) + ' – ' + fmtPx(a.range && a.range.high) + '
';
html += '
置信度'
+ pct(a.confidence_live != null ? a.confidence_live : a.confidence_confirmed) + '
';
if (a.next_expected) {
html += '
下一步'
+ _wcsEventZh(a.next_expected) + '
';
}
html += '
';
if (payload.confirmed_history && payload.confirmed_history.length) {
html += '
已确认历史
';
payload.confirmed_history.forEach(function(h) {
const ev = (h.events && h.events.length)
? h.events.map(_wcsEventZh).join('、')
: '—';
html += '
C' + h.cycle_id + ' ' + _wcsStructureZh(h.structure) + ' · ' + ev + '
';
});
html += '
';
}
html += '
';
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)) {
const $select = $('#symbol');
const currentSymbol = $select.val(); // 保存当前选中的值
$select.empty();
data.forEach(function(symbol) {
$select.append($('