Files
Chan/web/templates/wyckoff_crypto.html
T
jackyu66gitandCursor ec08de098e feat(ECR-009): Crypto Wyckoff Screener 独立页(D/W/M)
移植 A_Share_DP 引擎;本地缓存与 60s tip;月线由日线 UTC 聚合;不碰主站 analyze/缠论叠层。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 15:46:35 +08:00

211 lines
9.3 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>