feat(ECR-009): Crypto Wyckoff Screener 独立页(D/W/M)

移植 A_Share_DP 引擎;本地缓存与 60s tip;月线由日线 UTC 聚合;不碰主站 analyze/缠论叠层。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-07 15:46:35 +08:00
co-authored by Cursor
parent 6c627f009a
commit ec08de098e
34 changed files with 3212 additions and 9 deletions
+210
View File
@@ -0,0 +1,210 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Crypto Wyckoff Screener</title>
<style>
:root{--bg:#0d1117;--panel:#161b22;--border:#30363d;--text:#e6edf3;--muted:#8b949e;--accent:#58a6ff;--green:#3fb950;--red:#f85149;--orange:#d2991d}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'PingFang SC','Microsoft YaHei',sans-serif;background:var(--bg);color:var(--text);height:100vh;display:flex;flex-direction:column}
header{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap}
header h1{font-size:15px;font-weight:700}
header .meta{font-size:11px;color:var(--muted)}
header a{color:var(--accent);font-size:12px;text-decoration:none}
.filters{padding:8px 14px;border-bottom:1px solid var(--border);display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;background:var(--panel)}
.filters label{font-size:10px;color:var(--muted);display:flex;flex-direction:column;gap:2px}
.filters select,.filters input{background:#0d1117;border:1px solid var(--border);color:var(--text);border-radius:4px;padding:4px 6px;font-size:11px;min-width:100px}
.filters button{background:var(--accent);border:0;color:#fff;border-radius:4px;padding:6px 12px;font-size:11px;cursor:pointer}
.filters button.secondary{background:transparent;border:1px solid var(--border);color:var(--muted)}
.main{flex:1;display:grid;grid-template-columns:1fr 340px;min-height:0}
@media(max-width:900px){.main{grid-template-columns:1fr}}
.pane{min-height:0;overflow:auto;border-right:1px solid var(--border)}
.pane:last-child{border-right:0;background:var(--panel)}
.pane h2{font-size:10px;color:var(--muted);letter-spacing:1px;padding:8px 10px;border-bottom:1px solid var(--border);position:sticky;top:0;background:var(--panel)}
table{width:100%;border-collapse:collapse;font-size:11px}
th{text-align:left;padding:6px 8px;color:var(--muted);font-weight:600;position:sticky;top:28px;background:var(--panel)}
td{padding:6px 8px;border-top:1px solid rgba(255,255,255,.04);cursor:pointer}
tr:hover td{background:rgba(88,166,255,.08)}
tr.active td{background:rgba(88,166,255,.16)}
.tag{display:inline-block;padding:1px 6px;border-radius:3px;font-size:10px}
.tag.StrongBuy,.tag.Buy{background:rgba(63,185,80,.15);color:var(--green)}
.tag.Sell,.tag.Avoid{background:rgba(248,81,73,.15);color:var(--red)}
.tag.Watch{background:rgba(210,153,29,.15);color:var(--orange)}
#detail{padding:12px;font-size:12px;line-height:1.55;color:var(--muted)}
#detail h3{color:var(--text);font-size:13px;margin-bottom:8px}
#detail .kv{margin:4px 0}
#detail .kv b{color:var(--text);display:inline-block;min-width:88px}
#detail ul{margin:8px 0 0 16px}
.empty{padding:24px;color:var(--muted);font-size:12px}
</style>
</head>
<body>
<header>
<div>
<h1>Crypto Wyckoff Screener</h1>
<div class="meta" id="metaLine">加载中…</div>
</div>
<div class="meta">
<a href="/">← 主站</a>
· D/W/M · UTC · 每分钟 tip
</div>
</header>
<div class="filters">
<label>决策
<select id="fDecision"><option value="">全部</option></select>
</label>
<label>月 Cycle
<select id="fCycle"><option value="">全部</option></select>
</label>
<label>周 Phase
<select id="fPhase"><option value="">全部</option></select>
</label>
<label>日 Event
<select id="fEvent"><option value="">全部</option></select>
</label>
<label>最低分
<input id="fMinScore" type="number" placeholder="0" step="1">
</label>
<label>排序
<select id="fSort">
<option value="overall_score">overall_score</option>
<option value="alignment">alignment</option>
<option value="entry_score">entry_score</option>
<option value="stars">stars</option>
</select>
</label>
<button id="btnRefresh" type="button">刷新</button>
<button id="btnTick" class="secondary" type="button">手动 Tick</button>
</div>
<div class="main">
<div class="pane">
<h2>SCAN <span id="rowCount"></span></h2>
<table>
<thead>
<tr>
<th>Symbol</th><th>Decision</th><th>M Cycle</th><th>W Phase</th>
<th>D Event</th><th>Score</th><th>Align</th><th></th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
<div class="empty" id="emptyHint" style="display:none">暂无数据。调度器正在回填/扫描,稍后点刷新;或点「手动 Tick」(可先设 CRYPTO_WYCKOFF_MAX_SYMBOLS 做小样本)。</div>
</div>
<div class="pane">
<h2>DETAIL</h2>
<div id="detail">点选左侧一行</div>
</div>
</div>
<script>
(function(){
var meta = null;
var rows = [];
var active = null;
function fillSelect(id, values){
var el = document.getElementById(id);
values.forEach(function(v){
var o = document.createElement('option');
o.value = v; o.textContent = v; el.appendChild(o);
});
}
function loadMeta(){
return fetch('/api/wyckoff_crypto/meta').then(function(r){return r.json()}).then(function(m){
meta = m;
if (!document.getElementById('fDecision').options.length || document.getElementById('fDecision').options.length===1){
fillSelect('fDecision', m.decision_signals||[]);
fillSelect('fCycle', m.cycles||[]);
fillSelect('fPhase', m.phases||[]);
fillSelect('fEvent', m.events||[]);
}
var st = m.status||{};
document.getElementById('metaLine').textContent =
'engine '+m.engine_version+' · date '+(m.latest_trade_date||'-')+
' · rows '+m.scan_count+
' · tick '+(st.last_tick_at||'pending')+
(st.running?' · RUNNING':'')+
(st.last_error?' · err '+st.last_error:'');
});
}
function qs(){
var p = new URLSearchParams();
var d=document.getElementById('fDecision').value; if(d) p.set('decision_signal',d);
var c=document.getElementById('fCycle').value; if(c) p.set('m_cycle',c);
var ph=document.getElementById('fPhase').value; if(ph) p.set('w_phase',ph);
var e=document.getElementById('fEvent').value; if(e) p.set('d_event',e);
var ms=document.getElementById('fMinScore').value; if(ms) p.set('min_overall_score',ms);
p.set('sort', document.getElementById('fSort').value);
p.set('limit','200');
return p.toString();
}
function loadScan(){
return fetch('/api/wyckoff_crypto/scan?'+qs()).then(function(r){return r.json()}).then(function(data){
rows = data.rows||[];
document.getElementById('rowCount').textContent = '('+rows.length+')';
document.getElementById('emptyHint').style.display = rows.length? 'none':'block';
var tb = document.getElementById('tbody');
tb.innerHTML = '';
rows.forEach(function(row, i){
var tr = document.createElement('tr');
if (active && active.ts_code===row.ts_code) tr.className='active';
tr.innerHTML =
'<td>'+row.ts_code+'</td>'+
'<td><span class="tag '+row.decision_signal+'">'+row.decision_signal+'</span></td>'+
'<td>'+row.m_cycle+'</td>'+
'<td>'+row.w_phase+'</td>'+
'<td>'+row.d_current_event+'</td>'+
'<td>'+(row.overall_score!=null?Number(row.overall_score).toFixed(1):'-')+'</td>'+
'<td>'+(row.alignment!=null?Number(row.alignment).toFixed(0):'-')+'</td>'+
'<td>'+(row.stars||'')+'</td>';
tr.onclick = function(){ showDetail(row); Array.prototype.forEach.call(tb.querySelectorAll('tr'),function(x){x.className='';}); tr.className='active'; };
tb.appendChild(tr);
});
});
}
function showDetail(row){
active = row;
var reasons=[];
try{ reasons = JSON.parse(row.reasons_json||'[]'); }catch(e){}
var plan = [];
if(row.entry!=null) plan.push('entry '+row.entry);
if(row.stop!=null) plan.push('stop '+row.stop);
if(row.target1!=null) plan.push('t1 '+row.target1);
if(row.target2!=null) plan.push('t2 '+row.target2);
if(row.rr!=null) plan.push('rr '+row.rr);
document.getElementById('detail').innerHTML =
'<h3>'+row.ts_code+'</h3>'+
'<div class="kv"><b>Decision</b> '+row.decision_signal+' · risk '+row.risk+'</div>'+
'<div class="kv"><b>Month</b> '+row.m_cycle+'</div>'+
'<div class="kv"><b>Week</b> '+row.w_cycle+' / '+row.w_phase+' / '+row.w_current_event+'</div>'+
'<div class="kv"><b>Day</b> '+row.d_current_event+'</div>'+
'<div class="kv"><b>Scores</b> overall '+fmt(row.overall_score)+' align '+fmt(row.alignment)+' entry '+fmt(row.entry_score)+'</div>'+
'<div class="kv"><b>Plan</b> '+(plan.join(' · ')||'无(非 tradable')+'</div>'+
'<div class="kv"><b>Updated</b> '+(row.scanned_at||'')+'</div>'+
'<ul>'+reasons.map(function(x){return '<li>'+x+'</li>';}).join('')+'</ul>';
}
function fmt(v){ return v==null?'-':Number(v).toFixed(1); }
function refresh(){ return loadMeta().then(loadScan); }
document.getElementById('btnRefresh').onclick = function(){ refresh(); };
document.getElementById('btnTick').onclick = function(){
fetch('/api/wyckoff_crypto/tick', {method:'POST'}).then(function(){
document.getElementById('metaLine').textContent = '手动 tick 已启动…';
setTimeout(refresh, 3000);
});
};
['fDecision','fCycle','fPhase','fEvent','fMinScore','fSort'].forEach(function(id){
document.getElementById(id).addEventListener('change', loadScan);
});
refresh();
setInterval(refresh, 30000);
})();
</script>
</body>
</html>