835 lines
43 KiB
HTML
835 lines
43 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
|
||
<meta name="theme-color" content="#0d1117">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||
<link rel="manifest" href="manifest.json">
|
||
<title>Beast Trader</title>
|
||
<style>
|
||
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
||
:root{
|
||
--bg:#0d1117;--surface:#161b22;--surface2:#21262d;
|
||
--border:#30363d;--text:#e6edf3;--text2:#8b949e;--text3:#6e7681;
|
||
--green:#3fb950;--red:#f85149;--yellow:#d29922;--blue:#58a6ff;
|
||
}
|
||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--text);font-size:14px;padding-bottom:70px;overflow-x:hidden}
|
||
.header{padding:16px 16px 8px;display:flex;justify-content:space-between;align-items:center;border-bottom:0.5px solid var(--border);background:var(--surface)}
|
||
.header h1{font-size:18px;font-weight:500}
|
||
.header .sub{font-size:12px;color:var(--text2)}
|
||
.st-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px}
|
||
.st-ok{background:var(--green)}.st-warn{background:var(--yellow)}.st-err{background:var(--red)}
|
||
.tabs{display:flex;background:var(--surface);border-bottom:0.5px solid var(--border);position:sticky;top:0;z-index:10}
|
||
.tab{flex:1;text-align:center;padding:12px;font-size:13px;color:var(--text2);cursor:pointer;border-bottom:2px solid transparent}
|
||
.tab.active{color:var(--blue);border-bottom-color:var(--blue)}
|
||
.tc{display:none;padding:12px 16px}.tc.active{display:block}
|
||
.mg{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px}
|
||
.mc{background:var(--surface);border-radius:10px;padding:12px;border:0.5px solid var(--border)}
|
||
.mc .l{font-size:11px;color:var(--text3);margin-bottom:4px}
|
||
.mc .v{font-size:18px;font-weight:500}.v.gn{color:var(--green)}.v.rd{color:var(--red)}.v.bl{color:var(--blue)}.v.yl{color:var(--yellow)}
|
||
.ca{background:var(--surface);border-radius:10px;padding:12px;margin-bottom:12px;border:0.5px solid var(--border);overflow:hidden}
|
||
.ca .ct{font-size:13px;font-weight:500;margin-bottom:8px;display:flex;justify-content:space-between}
|
||
svg{width:100%;height:auto}
|
||
.sr-badge{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:6px;font-size:12px;font-weight:500}
|
||
.sr-support{background:rgba(63,185,80,0.15);color:var(--green)}
|
||
.sr-resist{background:rgba(248,81,73,0.15);color:var(--red)}
|
||
.sr-price{background:rgba(88,166,255,0.15);color:var(--blue)}
|
||
.sr-row{display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap}
|
||
.fl{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}
|
||
.fi{background:var(--surface);border-radius:8px;padding:10px 12px;border:0.5px solid var(--border);display:flex;justify-content:space-between;align-items:center}
|
||
.fi .nm{font-size:13px;font-weight:500}.fi .ds{font-size:11px;color:var(--text3);margin-top:2px}
|
||
.fi .st{font-size:12px;font-weight:500;padding:3px 8px;border-radius:5px}
|
||
.st-p{background:rgba(63,185,80,0.2);color:var(--green)}
|
||
.st-f{background:rgba(248,81,73,0.2);color:var(--red)}
|
||
.cb{padding:12px;border-radius:10px;margin-bottom:12px;text-align:center;font-size:15px;font-weight:500}
|
||
.cb.rdy{background:rgba(63,185,80,0.15);color:var(--green);border:0.5px solid var(--green)}
|
||
.cb.wt{background:rgba(210,153,34,0.15);color:var(--yellow);border:0.5px solid var(--yellow)}
|
||
.cb.no{background:rgba(248,81,73,0.1);color:var(--text3);border:0.5px solid var(--border)}
|
||
.ftr{position:fixed;bottom:0;left:0;right:0;background:var(--surface);border-top:0.5px solid var(--border);padding:8px 16px;display:flex;justify-content:space-between;align-items:center;z-index:20}
|
||
.ftr .fi2{font-size:11px;color:var(--text3)}
|
||
.rf{background:var(--surface2);border:0.5px solid var(--border);color:var(--text);padding:6px 16px;border-radius:8px;font-size:13px;cursor:pointer}
|
||
.sym{display:flex;gap:6px;margin-bottom:12px}
|
||
.sb{flex:1;padding:8px;text-align:center;border-radius:8px;border:0.5px solid var(--border);font-size:13px;cursor:pointer;color:var(--text2);background:var(--surface)}
|
||
.sb.ac{border-color:var(--blue);color:var(--blue);background:rgba(88,166,255,0.1)}
|
||
.ta{font-size:16px}.ta.up{color:var(--green)}.ta.dn{color:var(--red)}.ta.nt{color:var(--text3)}
|
||
.leg{display:flex;gap:12px;font-size:11px;color:var(--text3);margin-bottom:4px}
|
||
.leg span{display:flex;align-items:center;gap:4px}
|
||
.dot{width:8px;height:8px;border-radius:50%;display:inline-block}.dot.gn{background:var(--green)}.dot.rd{background:var(--red)}
|
||
.ld{text-align:center;padding:30px;color:var(--text3);font-size:14px}
|
||
.er{text-align:center;padding:20px;color:var(--red);font-size:13px}
|
||
.sec-title{font-size:12px;font-weight:500;color:var(--text2);margin:8px 0 4px}
|
||
.tr{background:var(--surface);border-radius:8px;padding:10px 12px;margin-bottom:6px;border:0.5px solid var(--border);font-size:12px}
|
||
.tr .tp{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}
|
||
.tr .td{display:flex;gap:16px;color:var(--text3);font-size:11px}
|
||
.tr .pnl{font-weight:600;font-size:13px}
|
||
.tbl{width:100%;border-collapse:collapse;font-size:11px}
|
||
.tbl th{color:var(--text3);font-weight:400;text-align:left;padding:4px 8px;border-bottom:0.5px solid var(--border)}
|
||
.tbl td{padding:6px 8px;border-bottom:0.5px solid var(--border);color:var(--text2)}
|
||
.tbl .num{text-align:right}
|
||
.tbl .win{color:var(--green)}.tbl .lose{color:var(--red)}
|
||
.pos-badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:10px;font-weight:500}
|
||
.pos-long{background:rgba(63,185,80,0.2);color:var(--green)}
|
||
.pos-short{background:rgba(248,81,73,0.2);color:var(--red)}
|
||
.empty-state{text-align:center;padding:24px;color:var(--text3);font-size:13px}
|
||
.g3{grid-template-columns:1fr 1fr 1fr}
|
||
.g4{grid-template-columns:1fr 1fr 1fr 1fr}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="header">
|
||
<div>
|
||
<h1>Beast Trader</h1>
|
||
<div class="sub">v2.2d · Dashboard V1.2 · 直连 Binance</div>
|
||
</div>
|
||
<div><span class="st-dot st-warn" id="stDot"></span><span style="font-size:12px;color:var(--text2)" id="stTxt">初始化</span></div>
|
||
</div>
|
||
<div id="csBanner" style="display:none;background:rgba(248,81,73,0.15);border:1px solid var(--red);border-radius:8px;padding:8px 12px;margin-bottom:8px;font-size:12px;color:var(--red);text-align:center">
|
||
⚠️ CloudStudio 不支持 HTTPS→HTTP 请求,请用手机浏览器打开<br>
|
||
<a href="http://43.163.225.30:9000/" style="color:var(--green);text-decoration:underline">http://43.163.225.30:9000/</a>
|
||
</div>
|
||
|
||
<div class="tabs">
|
||
<div class="tab active" onclick="swTab('st')">结构分析</div>
|
||
<div class="tab" onclick="swTab('diag')">信号诊断</div>
|
||
<div class="tab" onclick="swTab('trade')">交易监测</div>
|
||
</div>
|
||
|
||
<div class="tc active" id="tab-st">
|
||
<div class="sym">
|
||
<div class="sb ac" onclick="swSym('ETH/USDT')">ETH/USDT</div>
|
||
<div class="sb" onclick="swSym('BTC/USDT')">BTC/USDT</div>
|
||
</div>
|
||
<div class="mg" id="mGrid">
|
||
<div class="mc"><div class="l">当前价格</div><div class="v bl" id="cPrice">--</div></div>
|
||
<div class="mc"><div class="l">D1 趋势</div><div class="v" id="cTrend"><span class="ta nt">→</span> <span>计算中</span></div></div>
|
||
<div class="mc"><div class="l">支撑</div><div class="v gn" id="cSup">--</div></div>
|
||
<div class="mc"><div class="l">阻力</div><div class="v rd" id="cRes">--</div></div>
|
||
</div>
|
||
<div class="sr-row" id="srRow"></div>
|
||
<div class="ca">
|
||
<div class="ct"><span id="chP">ETH/USDT</span><span style="color:var(--text3);font-size:11px">最近 48 根 1H K线</span></div>
|
||
<div class="leg"><span><span class="dot gn"></span>Swing High</span><span><span class="dot rd"></span>Swing Low</span></div>
|
||
<svg viewBox="0 0 340 160" id="chSvg"><rect width="340" height="160" fill="var(--surface)" rx="6"/><text x="170" y="80" text-anchor="middle" fill="var(--text3)" font-size="12">获取数据中...</text></svg>
|
||
</div>
|
||
<div class="ca">
|
||
<div class="ct"><span>条件检查</span><span style="color:var(--text3);font-size:11px">实时</span></div>
|
||
<div id="condCard"><div class="ld">加载中...</div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tc" id="tab-diag">
|
||
<div style="margin-bottom:8px;font-size:13px;color:var(--text2)">ETH/USDT · 实时过滤条件检查</div>
|
||
<div id="conclBar"></div>
|
||
<div class="fl" id="fList"><div class="ld">加载中...</div></div>
|
||
</div>
|
||
|
||
<div class="tc" id="tab-trade">
|
||
<div class="mg g4" id="accGrid">
|
||
<div class="mc"><div class="l">当前余额</div><div class="v bl" id="accBalance">--</div></div>
|
||
<div class="mc"><div class="l">总收益</div><div class="v" id="accPnl">--</div></div>
|
||
<div class="mc"><div class="l">胜率</div><div class="v" id="accWinRate">--</div></div>
|
||
<div class="mc"><div class="l">交易数</div><div class="v" id="accTrades">--</div></div>
|
||
</div>
|
||
<div style="display:flex;gap:16px;margin-bottom:8px;font-size:11px;color:var(--text3)">
|
||
<span id="accStrategy">策略: --</span>
|
||
<span id="accUpdate">更新: --</span>
|
||
</div>
|
||
|
||
<div class="sec-title">📌 当前持仓</div>
|
||
<div id="openPos"><div class="ld">加载中...</div></div>
|
||
|
||
<div class="sec-title">📋 近期交易 (<span id="tradeTotal">0</span>笔)</div>
|
||
<div style="overflow-x:auto">
|
||
<table class="tbl"><thead>
|
||
<tr><th>时间</th><th>方向</th><th>入场价</th><th>出场价</th><th class="num">盈亏</th><th>原因</th></tr>
|
||
</thead><tbody id="tradeBody"><tr><td colspan="6" class="empty-state">暂无交易记录</td></tr></tbody></table>
|
||
</div>
|
||
|
||
<div class="sec-title" style="margin-top:12px">📊 每日盈亏 (近30天)</div>
|
||
<div class="ca">
|
||
<svg viewBox="0 0 340 120" id="pnlSvg"><rect width="340" height="120" fill="var(--surface)" rx="6"/><text x="170" y="60" text-anchor="middle" fill="var(--text3)" font-size="12">暂无数据</text></svg>
|
||
</div>
|
||
|
||
<div class="sec-title">📉 盈亏分布</div>
|
||
<div class="ca">
|
||
<svg viewBox="0 0 340 100" id="distSvg"><rect width="340" height="100" fill="var(--surface)" rx="6"/><text x="170" y="50" text-anchor="middle" fill="var(--text3)" font-size="12">暂无数据</text></svg>
|
||
</div>
|
||
|
||
<div class="sec-title" style="margin-top:12px">📊 回测年化表现(2021-2026)</div>
|
||
<div style="font-size:12px;color:var(--text2);margin-bottom:12px">
|
||
<table style="width:100%;border-collapse:collapse;font-size:12px">
|
||
<tr style="background:var(--surface2)">
|
||
<th style="padding:6px 8px;text-align:left;border-bottom:1px solid var(--border)">年份</th>
|
||
<th style="padding:6px 8px;text-align:right;border-bottom:1px solid var(--border)">交易数</th>
|
||
<th style="padding:6px 8px;text-align:right;border-bottom:1px solid var(--border)">收益率</th>
|
||
<th style="padding:6px 8px;text-align:right;border-bottom:1px solid var(--border)">终值</th>
|
||
</tr>
|
||
<tr><td style="padding:6px 8px">2021</td><td style="padding:6px 8px;text-align:right">172</td><td style="padding:6px 8px;text-align:right;color:var(--up)">+251.16%</td><td style="padding:6px 8px;text-align:right">$35,116</td></tr>
|
||
<tr style="background:var(--surface2)"><td style="padding:6px 8px">2022</td><td style="padding:6px 8px;text-align:right">204</td><td style="padding:6px 8px;text-align:right;color:var(--up)">+110.91%</td><td style="padding:6px 8px;text-align:right">$21,091</td></tr>
|
||
<tr><td style="padding:6px 8px">2023</td><td style="padding:6px 8px;text-align:right">182</td><td style="padding:6px 8px;text-align:right;color:var(--up)">+49.35%</td><td style="padding:6px 8px;text-align:right">$14,935</td></tr>
|
||
<tr style="background:var(--surface2)"><td style="padding:6px 8px">2024</td><td style="padding:6px 8px;text-align:right">232</td><td style="padding:6px 8px;text-align:right;color:var(--up)">+185.84%</td><td style="padding:6px 8px;text-align:right">$28,584</td></tr>
|
||
<tr><td style="padding:6px 8px">2025</td><td style="padding:6px 8px;text-align:right">221</td><td style="padding:6px 8px;text-align:right;color:var(--up)">+608.24%</td><td style="padding:6px 8px;text-align:right">$70,824</td></tr>
|
||
<tr style="background:var(--surface2)"><td style="padding:6px 8px">2026</td><td style="padding:6px 8px;text-align:right">54</td><td style="padding:6px 8px;text-align:right;color:var(--dn)">-11.87%</td><td style="padding:6px 8px;text-align:right">$8,813</td></tr>
|
||
</table>
|
||
<div style="margin-top:6px;padding:6px 8px;background:var(--surface2);border-radius:4px">
|
||
<b>v2.2d 全周期 (2021-2026) 总收益:+205,684%</b> · CAGR 309.01% · Sharpe 1.03 · 最大回撤20.58%
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="ftr">
|
||
<span class="fi2" id="ftInfo">上次更新: --</span>
|
||
<button class="rf" onclick="refresh()">刷新</button>
|
||
</div>
|
||
|
||
<script>
|
||
// ============================================================
|
||
// 指标计算(移植自 v2.2b 策略)
|
||
// ============================================================
|
||
|
||
function detectSwingPoints(high, low, window=5) {
|
||
const sh = new Array(high.length).fill(NaN);
|
||
const sl = new Array(low.length).fill(NaN);
|
||
for (let i = window; i < high.length - window; i++) {
|
||
const leftH = high.slice(i - window, i);
|
||
const rightH = high.slice(i + 1, i + window + 1);
|
||
if (high[i] > Math.max(...leftH) && high[i] > Math.max(...rightH)) sh[i] = high[i];
|
||
const leftL = low.slice(i - window, i);
|
||
const rightL = low.slice(i + 1, i + window + 1);
|
||
if (low[i] < Math.min(...leftL) && low[i] < Math.min(...rightL)) sl[i] = low[i];
|
||
}
|
||
return [sh, sl];
|
||
}
|
||
|
||
function buildStructure(high, low, close, sh, sl) {
|
||
const n = high.length;
|
||
const tu = new Array(n).fill(false);
|
||
const td = new Array(n).fill(false);
|
||
const sup = new Array(n).fill(NaN);
|
||
const res = new Array(n).fill(NaN);
|
||
const idm = new Array(n).fill(false);
|
||
const isp = new Array(n).fill(false);
|
||
const shP = [], slP = [];
|
||
for (let i = 0; i < n; i++) {
|
||
if (!isNaN(sh[i])) { shP.push(sh[i]); if (shP.length > 4) shP.shift(); }
|
||
if (!isNaN(sl[i])) { slP.push(sl[i]); if (slP.length > 4) slP.shift(); }
|
||
if (shP.length >= 2 && slP.length >= 2) {
|
||
if (shP[shP.length-1] > shP[shP.length-2] && slP[slP.length-1] > slP[slP.length-2]) tu[i] = true;
|
||
else if (shP[shP.length-1] < shP[shP.length-2] && slP[slP.length-1] < slP[slP.length-2]) td[i] = true;
|
||
else if (i > 0) { tu[i] = tu[i-1]; td[i] = td[i-1]; }
|
||
} else if (i > 0) { tu[i] = tu[i-1]; td[i] = td[i-1]; }
|
||
if (slP.length) sup[i] = slP[slP.length-1];
|
||
if (shP.length) res[i] = shP[shP.length-1];
|
||
const c = close[i];
|
||
if (!isNaN(sup[i]) && !isNaN(res[i]) && res[i] - sup[i] > 0) {
|
||
const pct = (c - sup[i]) / (res[i] - sup[i]);
|
||
idm[i] = pct < 0.35;
|
||
isp[i] = pct > 0.65;
|
||
}
|
||
}
|
||
return {tu, td, sup, res, idm, isp};
|
||
}
|
||
|
||
function trendStrength(high, low, sh, sl, minStr=-0.20) {
|
||
const n = high.length;
|
||
const up = new Array(n).fill(NaN), dn = new Array(n).fill(NaN);
|
||
const supS = new Array(n).fill(false), sdn = new Array(n).fill(false);
|
||
const shP = [], slP = [];
|
||
for (let i = 0; i < n; i++) {
|
||
if (!isNaN(sh[i])) { shP.push(sh[i]); if (shP.length > 4) shP.shift(); }
|
||
if (!isNaN(sl[i])) { slP.push(sl[i]); if (slP.length > 4) slP.shift(); }
|
||
if (shP.length >= 2 && slP.length >= 2) {
|
||
const hhD = (shP[shP.length-1] - shP[shP.length-2]) / shP[shP.length-2];
|
||
const hlD = (slP[slP.length-1] - slP[slP.length-2]) / slP[slP.length-2];
|
||
up[i] = hhD + hlD;
|
||
dn[i] = -(hhD + hlD);
|
||
}
|
||
}
|
||
for (let i = 0; i < n; i++) {
|
||
supS[i] = !isNaN(up[i]) && up[i] > minStr;
|
||
sdn[i] = !isNaN(dn[i]) && dn[i] > minStr;
|
||
}
|
||
return {up, dn, supS, sdn};
|
||
}
|
||
|
||
function supportAlive(low, close, support) {
|
||
const n = low.length;
|
||
const r = new Array(n).fill(false);
|
||
let found = false;
|
||
for (let i = 0; i < n; i++) {
|
||
if (isNaN(support[i])) { r[i] = found; continue; }
|
||
const touched = low[i] <= support[i] * 1.005 && low[i] >= support[i] * 0.995;
|
||
const held = close[i] > support[i];
|
||
if (touched && held) found = true;
|
||
r[i] = found;
|
||
if (i >= 3 && !touched) {
|
||
let keep = false;
|
||
for (let j = Math.max(0, i-2); j <= i; j++) {
|
||
const tj = low[j] <= support[j] * 1.005 && low[j] >= support[j] * 0.995;
|
||
const hj = close[j] > support[j];
|
||
if (tj && hj) keep = true;
|
||
}
|
||
r[i] = keep;
|
||
}
|
||
}
|
||
return r;
|
||
}
|
||
|
||
function resistAlive(high, close, resist) {
|
||
const n = high.length;
|
||
const r = new Array(n).fill(false);
|
||
for (let i = 0; i < n; i++) {
|
||
if (isNaN(resist[i])) { r[i] = i > 0 ? r[i-1] : false; continue; }
|
||
let found = false;
|
||
for (let j = Math.max(0, i-2); j <= i; j++) {
|
||
const touched = high[j] >= resist[j] * 0.995 && high[j] <= resist[j] * 1.005;
|
||
const held = close[j] < resist[j];
|
||
if (touched && held) found = true;
|
||
}
|
||
r[i] = found;
|
||
}
|
||
return r;
|
||
}
|
||
|
||
function computeAll(pair, df1h, df4h, df1d) {
|
||
const params = {swingD1:10, swingH4:8, pbr:0.6, msd:0.50, tsm:-0.20};
|
||
|
||
// D1
|
||
const [sh1d, sl1d] = detectSwingPoints(df1d.high, df1d.low, params.swingD1);
|
||
const st1d = buildStructure(df1d.high, df1d.low, df1d.close, sh1d, sl1d);
|
||
const li1d = st1d.tu.length-1;
|
||
const trUp1d = st1d.tu[li1d], trDn1d = st1d.td[li1d];
|
||
|
||
// 4H
|
||
const [sh4h, sl4h] = detectSwingPoints(df4h.high, df4h.low, params.swingH4);
|
||
const st4h = buildStructure(df4h.high, df4h.low, df4h.close, sh4h, sl4h);
|
||
const ts4h = trendStrength(df4h.high, df4h.low, sh4h, sl4h, params.tsm);
|
||
const li4h = st4h.tu.length-1;
|
||
const sup4h = st4h.sup[li4h], res4h = st4h.res[li4h];
|
||
const idx4h = st4h.idm[li4h], isp4h = st4h.isp[li4h];
|
||
const sa4h = supportAlive(df4h.low, df4h.close, st4h.sup)[li4h];
|
||
const ra4h = resistAlive(df4h.high, df4h.close, st4h.res)[li4h];
|
||
const sUp4h = ts4h.supS[li4h], sDn4h = ts4h.sdn[li4h];
|
||
const tsUp = ts4h.up[li4h], tsDn = ts4h.dn[li4h];
|
||
|
||
// 1H
|
||
const lh = df1h.close.length-1;
|
||
const cPrice = df1h.close[lh];
|
||
const bod = df1h.close.map((c,i) => Math.abs(c - df1h.open[i]));
|
||
const tr = df1h.high.map((h,i) => Math.max(h - df1h.low[i], 0.0001));
|
||
const uw = df1h.high.map((h,i) => h - (df1h.close[i] > df1h.open[i] ? df1h.close[i] : df1h.open[i]));
|
||
const lw = df1h.high.map((h,i) => (df1h.close[i] > df1h.open[i] ? df1h.open[i] : df1h.close[i]) - df1h.low[i]);
|
||
const isPin = tr.map((t,i) => (uw[i] + lw[i]) / t > params.pbr);
|
||
const bp = isPin.map((p,i) => p && df1h.close[i] > df1h.open[i] && lw[i] > uw[i]);
|
||
const bsh = isPin.map((p,i) => p && df1h.close[i] < df1h.open[i] && uw[i] > lw[i]);
|
||
const be = df1h.close.map((c,i) => i>0 && c > df1h.open[i-1] && df1h.open[i] < df1h.close[i-1] && c > df1h.open[i]);
|
||
const bse = df1h.close.map((c,i) => i>0 && c < df1h.open[i-1] && df1h.open[i] > df1h.close[i-1] && c < df1h.open[i]);
|
||
const buS = bp.map((p,i) => p || (be[i]||false));
|
||
const beS = bsh.map((p,i) => p || (bse[i]||false));
|
||
|
||
// Distances
|
||
const lsd = !isNaN(sup4h) && sup4h > 0 ? (cPrice - sup4h) / cPrice : null;
|
||
const ssd = !isNaN(res4h) && res4h > 0 ? (res4h - cPrice) / cPrice : null;
|
||
const ldOk = lsd !== null && lsd <= params.msd && lsd > 0.003;
|
||
const sdOk = ssd !== null && ssd <= params.msd && ssd > 0.003;
|
||
|
||
// Zone info
|
||
let zoneW = null, posPct = null;
|
||
if (!isNaN(sup4h) && !isNaN(res4h) && sup4h > 0 && res4h > sup4h) {
|
||
zoneW = ((res4h - sup4h) / sup4h) * 100;
|
||
posPct = ((cPrice - sup4h) / (res4h - sup4h)) * 100;
|
||
}
|
||
|
||
// Swing points history
|
||
const sHighs = [], sLows = [];
|
||
for (let i = 0; i < sh4h.length; i++) {
|
||
if (!isNaN(sh4h[i])) sHighs.push({price:sh4h[i], time:df4h.time[i]});
|
||
if (!isNaN(sl4h[i])) sLows.push({price:sl4h[i], time:df4h.time[i]});
|
||
}
|
||
|
||
// Diagnosis
|
||
const longOk = trUp1d && idx4h && ldOk && sa4h && sUp4h;
|
||
const shortOk = trDn1d && isp4h && sdOk && ra4h && sDn4h;
|
||
|
||
return {
|
||
cPrice: Math.round(cPrice * 100) / 100,
|
||
sup4h: !isNaN(sup4h) ? Math.round(sup4h * 100) / 100 : null,
|
||
res4h: !isNaN(res4h) ? Math.round(res4h * 100) / 100 : null,
|
||
zoneW: zoneW !== null ? Math.round(zoneW * 100) / 100 : null,
|
||
posPct: posPct !== null ? Math.round(posPct * 10) / 10 : null,
|
||
trUp1d: trUp1d, trDn1d: trDn1d,
|
||
idx4h, isp4h, sa4h, ra4h, sUp4h, sDn4h,
|
||
tsUp: !isNaN(tsUp) ? Math.round(tsUp * 10000) / 100 : null,
|
||
tsDn: !isNaN(tsDn) ? Math.round(tsDn * 10000) / 100 : null,
|
||
lsd: lsd !== null ? Math.round(lsd * 10000) / 100 : null,
|
||
ssd: ssd !== null ? Math.round(ssd * 10000) / 100 : null,
|
||
ldOk, sdOk, longOk, shortOk,
|
||
buS: buS[lh], beS: beS[lh],
|
||
priceHist: df1h.close,
|
||
sHighs: sHighs.slice(-6), sLows: sLows.slice(-6),
|
||
};
|
||
}
|
||
|
||
// ============================================================
|
||
// 数据获取 (通过服务器后端 API)
|
||
// ============================================================
|
||
|
||
let cache = {};
|
||
let curSym = 'ETH/USDT';
|
||
const IS_CLOUDSTUDIO = window.location.hostname.includes('codebuddy.work') || window.location.hostname.includes('app.codebuddy');
|
||
const SERVER_URL = IS_CLOUDSTUDIO ? 'http://43.163.225.30:9000' : window.location.origin;
|
||
const API_KEY = 'beast2025';
|
||
|
||
async function fetchFromServer() {
|
||
const headers = { 'X-API-Key': API_KEY, 'Accept': 'application/json' };
|
||
const [structRes, diagRes] = await Promise.all([
|
||
fetch(SERVER_URL + '/api/market-structure', { headers }),
|
||
fetch(SERVER_URL + '/api/signal-diagnosis', { headers }),
|
||
]);
|
||
if (!structRes.ok || !diagRes.ok) throw new Error('API ' + structRes.status);
|
||
const struct = await structRes.json();
|
||
const diag = await diagRes.json();
|
||
return { struct, diag };
|
||
}
|
||
|
||
async function loadData() {
|
||
document.getElementById('stDot').className = 'st-dot st-warn';
|
||
document.getElementById('stTxt').textContent = '获取数据';
|
||
try {
|
||
const data = await fetchFromServer();
|
||
const symKey = curSym === 'ETH/USDT' ? 'eth' : 'btc';
|
||
cache[curSym] = data;
|
||
cache[curSym + '_key'] = symKey;
|
||
document.getElementById('stDot').className = 'st-dot st-ok';
|
||
document.getElementById('stTxt').textContent = '在线';
|
||
render();
|
||
} catch(e) {
|
||
document.getElementById('stDot').className = 'st-dot st-err';
|
||
if (e.message && (e.message.includes('Failed to fetch') || e.message.includes('NetworkError'))) {
|
||
document.getElementById('stTxt').textContent = IS_CLOUDSTUDIO ? 'HTTPS阻截' : '连接失败';
|
||
document.getElementById('csBanner').style.display = IS_CLOUDSTUDIO ? 'block' : 'none';
|
||
document.getElementById('mGrid').innerHTML = '<div class="er">⚠️ 无法连接服务器<br><small>请用手机浏览器直接打开<br><a href="http://43.163.225.30:9000/" style="color:var(--green)">http://43.163.225.30:9000/</a></small></div>';
|
||
document.getElementById('fList').innerHTML = '<div class="er">连接失败 — 请用直连 IP 访问</div>';
|
||
} else {
|
||
document.getElementById('stTxt').textContent = '离线';
|
||
document.getElementById('mGrid').innerHTML = '<div class="er">数据获取失败: ' + e.message + '</div>';
|
||
document.getElementById('fList').innerHTML = '<div class="er">连接失败</div>';
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 渲染(适配服务器 API 格式)
|
||
// ============================================================
|
||
|
||
function render() {
|
||
const raw = cache[curSym];
|
||
if (!raw) return;
|
||
const key = cache[curSym + '_key'] || 'eth';
|
||
const st = raw.struct ? raw.struct[key] : null;
|
||
const di = raw.diag ? raw.diag[key] : null;
|
||
if (!st || !di) return;
|
||
|
||
// Price cards
|
||
document.getElementById('cPrice').textContent = st.current_price ? '$' + Number(st.current_price).toLocaleString('en-US',{minimumFractionDigits:2}) : '--';
|
||
const tEl = document.getElementById('cTrend');
|
||
if (st.trend_1d === 'up') tEl.innerHTML = '<span class="ta up">↑</span> 上升趋势';
|
||
else if (st.trend_1d === 'down') tEl.innerHTML = '<span class="ta dn">↓</span> 下降趋势';
|
||
else tEl.innerHTML = '<span class="ta nt">→</span> 震荡';
|
||
document.getElementById('cSup').textContent = st.support ? '$' + Number(st.support).toLocaleString('en-US',{minimumFractionDigits:2}) : '--';
|
||
document.getElementById('cRes').textContent = st.resistance ? '$' + Number(st.resistance).toLocaleString('en-US',{minimumFractionDigits:2}) : '--';
|
||
|
||
// SR badges
|
||
let sr = '';
|
||
if (st.support) sr += '<span class="sr-badge sr-support">▼ 支撑 $' + Number(st.support).toFixed(2) + '</span>';
|
||
if (st.resistance) sr += '<span class="sr-badge sr-resist">▲ 阻力 $' + Number(st.resistance).toFixed(2) + '</span>';
|
||
if (st.zone_width_pct) sr += '<span class="sr-badge sr-price">◆ 带宽 ' + Number(st.zone_width_pct).toFixed(1) + '%</span>';
|
||
if (st.price_position_pct !== null && st.price_position_pct !== undefined) sr += '<span class="sr-badge sr-price">◆ 位置 ' + Number(st.price_position_pct).toFixed(0) + '%</span>';
|
||
document.getElementById('srRow').innerHTML = sr;
|
||
|
||
// Chart
|
||
try { renderChart(st); } catch(e) { console.error("Chart error:", e); }
|
||
|
||
// Condition cards
|
||
try { renderConditions(di, st); } catch(e) { console.error("Conditions error:", e); }
|
||
}
|
||
|
||
function renderChart(st) {
|
||
const hist = st.price_history_1h;
|
||
const p = hist ? hist.close : null;
|
||
const ts = hist ? hist.timestamps : null;
|
||
if (!p || p.length < 3) return;
|
||
|
||
const w = 340, h = 200, pad = 20, topPad = 22, botPad = 20;
|
||
const cw = w - pad*2, ch = h - topPad - botPad;
|
||
|
||
const vals = p.concat(st.support || []).concat(st.resistance || []);
|
||
const mn = Math.min(...vals), mx = Math.max(...vals);
|
||
const rg = (mx - mn) || 1;
|
||
|
||
const xS = (i) => pad + (i / (p.length - 1)) * cw;
|
||
const yS = (v) => topPad + ch - ((v - mn) / rg) * ch;
|
||
|
||
const pts = p.map((v,i) => xS(i).toFixed(0)+','+yS(v).toFixed(0)).join(' ');
|
||
|
||
let svg = '<rect width="'+w+'" height="'+h+'" fill="var(--surface)" rx="6"/>';
|
||
|
||
if (st.support && st.resistance && st.resistance > st.support) {
|
||
const supY = yS(st.support), resY = yS(st.resistance);
|
||
const zoneH = supY - resY;
|
||
svg += '<rect x="'+(w-12)+'" y="'+resY+'" width="6" height="'+zoneH+'" rx="3" fill="rgba(136,136,136,0.12)"/>';
|
||
const supplyH = zoneH * 0.35;
|
||
svg += '<rect x="'+(w-12)+'" y="'+resY+'" width="6" height="'+supplyH+'" rx="3" fill="rgba(248,81,73,0.2)"/>';
|
||
const demandH = zoneH * 0.35;
|
||
svg += '<rect x="'+(w-12)+'" y="'+(supY - demandH)+'" width="6" height="'+demandH+'" rx="3" fill="rgba(63,185,80,0.2)"/>';
|
||
|
||
svg += '<text x="'+pad+'" y="'+(resY + supplyH/2 + 3)+'" fill="var(--red)" font-size="9" opacity="0.6">供给区</text>';
|
||
svg += '<text x="'+pad+'" y="'+(supY - demandH/2 + 3)+'" fill="var(--green)" font-size="9" opacity="0.6">需求区</text>';
|
||
|
||
svg += '<line x1="'+pad+'" y1="'+resY.toFixed(0)+'" x2="'+(w-pad)+'" y2="'+resY.toFixed(0)+'" stroke="var(--red)" stroke-width="1" stroke-dasharray="4,3" opacity="0.8"/>';
|
||
svg += '<text x="'+(w-pad-14)+'" y="'+(resY-4)+'" text-anchor="end" fill="var(--red)" font-size="11">阻力 $'+Number(st.resistance).toFixed(0)+'</text>';
|
||
svg += '<line x1="'+pad+'" y1="'+supY.toFixed(0)+'" x2="'+(w-pad)+'" y2="'+supY.toFixed(0)+'" stroke="var(--green)" stroke-width="1" stroke-dasharray="4,3" opacity="0.8"/>';
|
||
svg += '<text x="'+(w-pad-14)+'" y="'+(supY-4)+'" text-anchor="end" fill="var(--green)" font-size="11">支撑 $'+Number(st.support).toFixed(0)+'</text>';
|
||
}
|
||
|
||
svg += '<polyline points="'+pts+'" fill="none" stroke="var(--blue)" stroke-width="1.5"/>';
|
||
|
||
const lx = xS(p.length-1), ly = yS(p[p.length-1]);
|
||
svg += '<circle cx="'+lx.toFixed(0)+'" cy="'+ly.toFixed(0)+'" r="3.5" fill="var(--blue)" stroke="var(--surface)" stroke-width="1.5"/>';
|
||
svg += '<text x="'+(lx).toFixed(0)+'" y="'+(ly-6).toFixed(0)+'" text-anchor="middle" fill="var(--blue)" font-size="10" font-weight="500">$'+Number(p[p.length-1]).toFixed(0)+'</text>';
|
||
|
||
if (st.swing_points) {
|
||
if (st.swing_points.highs) {
|
||
st.swing_points.highs.slice(-8).forEach(sp => {
|
||
const ix = p.findIndex(c => Math.abs(c - sp.price) / Math.max(sp.price,1) < 0.008);
|
||
if (ix >= 0) {
|
||
const sx = xS(ix), sy = yS(sp.price);
|
||
svg += '<circle cx="'+sx.toFixed(0)+'" cy="'+sy.toFixed(0)+'" r="2.5" fill="var(--green)" opacity="0.6"/>';
|
||
}
|
||
});
|
||
}
|
||
if (st.swing_points.lows) {
|
||
st.swing_points.lows.slice(-8).forEach(sp => {
|
||
const ix = p.findIndex(c => Math.abs(c - sp.price) / Math.max(sp.price,1) < 0.008);
|
||
if (ix >= 0) {
|
||
const sx = xS(ix), sy = yS(sp.price);
|
||
svg += '<circle cx="'+sx.toFixed(0)+'" cy="'+sy.toFixed(0)+'" r="2.5" fill="var(--red)" opacity="0.6"/>';
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
svg += '<text x="'+pad+'" y="'+(topPad-4)+'" fill="var(--text3)" font-size="9">$'+Number(mx).toFixed(0)+'</text>';
|
||
svg += '<text x="'+pad+'" y="'+(h-botPad+14)+'" fill="var(--text3)" font-size="9">$'+Number(mn).toFixed(0)+'</text>';
|
||
|
||
if (ts && ts.length > 1) {
|
||
svg += '<text x="'+pad+'" y="'+(h-2)+'" fill="var(--text3)" font-size="9">'+(ts[0]||'').slice(0,11).replace(' ',' ')+'</text>';
|
||
svg += '<text x="'+(w-pad)+'" y="'+(h-2)+'" text-anchor="end" fill="var(--text3)" font-size="9">'+(ts[ts.length-1]||'').slice(0,11).replace(' ',' ')+'</text>';
|
||
}
|
||
|
||
document.getElementById('chSvg').innerHTML = svg;
|
||
document.getElementById('chP').textContent = curSym;
|
||
}
|
||
|
||
// ============================================================
|
||
// 条件卡片渲染(V1.2 — 同时显示做多+做空两套条件)
|
||
// ============================================================
|
||
|
||
function renderConditions(di, st) {
|
||
const el = document.getElementById('condCard');
|
||
if (!di || !di.filters || !st) { el.innerHTML = '<div class="ld">加载中...</div>'; return; }
|
||
const f = di.filters;
|
||
|
||
const inSupply = f['in_supply_1h'] ? f['in_supply_1h'].pass : false;
|
||
const inDemand = f['in_demand_1h'] ? f['in_demand_1h'].pass : false;
|
||
const zoneLabel = inSupply ? '供给区' : (inDemand ? '需求区' : '中间区');
|
||
const zonePct = st.price_position_pct !== null && st.price_position_pct !== undefined ? st.price_position_pct : null;
|
||
|
||
const distToSup = st.current_price && st.support ? (st.current_price - st.support) : null;
|
||
const distToRes = st.resistance && st.current_price ? (st.resistance - st.current_price) : null;
|
||
|
||
const shortChecks = [
|
||
{key:'trend_down_1d', label:'D1 下降趋势'},
|
||
{key:'in_supply_1h', label:'在供给区'},
|
||
{key:'resistance_alive_1h', label:'阻力有效'},
|
||
{key:'strong_downtrend_4h', label:'4H 下降趋势'},
|
||
{key:'short_stop_distance', label:'止损距离合理'},
|
||
];
|
||
const longChecks = [
|
||
{key:'trend_up_1d', label:'D1 上升趋势'},
|
||
{key:'in_demand_1h', label:'在需求区'},
|
||
{key:'support_alive_1h', label:'支撑有效'},
|
||
{key:'strong_uptrend_4h', label:'4H 上升趋势'},
|
||
{key:'long_stop_distance', label:'止损距离合理'},
|
||
];
|
||
|
||
function renderCheckGroup(checks, title, titleColor) {
|
||
let gHtml = '<div style="margin-bottom:8px">';
|
||
gHtml += '<div style="font-size:12px;font-weight:500;color:'+titleColor+';margin-bottom:4px">'+title+'</div>';
|
||
gHtml += '<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:4px">';
|
||
let passN = 0;
|
||
checks.forEach(c => {
|
||
const fl = f[c.key];
|
||
if (!fl) return;
|
||
if (fl.pass) passN++;
|
||
const bg = fl.pass ? 'rgba(63,185,80,0.12)' : 'rgba(248,81,73,0.08)';
|
||
const cl = fl.pass ? 'var(--green)' : 'var(--red)';
|
||
const icon = fl.pass ? '✓' : '✗';
|
||
gHtml += '<span style="padding:3px 8px;border-radius:4px;background:'+bg+';color:'+cl+';font-size:11px;font-weight:500">'+icon+' '+c.label+'</span>';
|
||
});
|
||
gHtml += '</div>';
|
||
const allPass = passN === checks.length;
|
||
const barCls = allPass ? 'cb rdy' : 'cb no';
|
||
gHtml += '<div class="'+barCls+'" style="margin-bottom:0;font-size:13px;padding:8px">'+(allPass ? '✅ '+title+'条件全部通过' : '❌ '+title+'条件 '+passN+'/'+checks.length+' 通过')+'</div>';
|
||
gHtml += '</div>';
|
||
return {html: gHtml, passN: passN, total: checks.length};
|
||
}
|
||
|
||
let html = '';
|
||
|
||
html += '<div style="display:flex;align-items:center;justify-content:space-between;padding:8px 10px;background:var(--surface);border-radius:8px;border:0.5px solid var(--border);margin-bottom:6px;">';
|
||
html += '<div style="font-size:12px;color:var(--text)">当前 <strong style="color:var(--blue)">$'+Number(st.current_price).toFixed(0)+'</strong>';
|
||
if (zonePct !== null) html += ' <span style="color:var(--text3);margin:0 4px">·</span> '+zoneLabel+'('+Number(zonePct).toFixed(0)+'%)';
|
||
html += '</div>';
|
||
html += '<div style="display:flex;gap:4px;">';
|
||
if (distToRes !== null) html += '<span style="padding:2px 8px;border-radius:4px;font-size:11px;background:rgba(248,81,73,0.12);color:var(--red);font-weight:500">距阻力 $'+Number(distToRes).toFixed(0)+'</span>';
|
||
if (distToSup !== null) html += '<span style="padding:2px 8px;border-radius:4px;font-size:11px;background:rgba(63,185,80,0.08);color:var(--green)">距支撑 $'+Number(distToSup).toFixed(0)+'</span>';
|
||
html += '</div></div>';
|
||
|
||
// 做空条件组
|
||
const shortResult = renderCheckGroup(shortChecks, '↓ 做空条件(需全部通过)', 'var(--red)');
|
||
html += shortResult.html;
|
||
// 做多条件组
|
||
const longResult = renderCheckGroup(longChecks, '↑ 做多条件(需全部通过)', 'var(--green)');
|
||
html += longResult.html;
|
||
|
||
// D1 趋势状态提示
|
||
const d1Up = f['trend_up_1d'] ? f['trend_up_1d'].pass : false;
|
||
const d1Down = f['trend_down_1d'] ? f['trend_down_1d'].pass : false;
|
||
if (!d1Up && !d1Down) {
|
||
html += '<div style="padding:8px;margin-top:6px;background:rgba(210,153,34,0.1);border:0.5px solid var(--yellow);border-radius:6px;font-size:12px;color:var(--yellow);text-align:center">⚠ D1 趋势处于<strong>震荡</strong> — 做多做空总闸门均关闭,策略等待方向明确</div>';
|
||
}
|
||
|
||
el.innerHTML = html;
|
||
|
||
// Also update diagnosis tab
|
||
const bar = document.getElementById('conclBar');
|
||
if (bar) {
|
||
const shortOk = shortResult.passN === shortResult.total;
|
||
const longOk = longResult.passN === longResult.total;
|
||
if (shortOk) { bar.className = 'cb rdy'; bar.textContent = '做空条件全部通过'; }
|
||
else if (longOk) { bar.className = 'cb rdy'; bar.textContent = '做多条件全部通过'; }
|
||
else { bar.className = 'cb no'; bar.textContent = '无方向满足 — 做空 '+shortResult.passN+'/5,做多 '+longResult.passN+'/5'; }
|
||
}
|
||
const fl = document.getElementById('fList');
|
||
if (fl) {
|
||
let diagHtml = '';
|
||
diagHtml += '<div class="sec-title">↑ 做多过滤</div>';
|
||
longChecks.forEach(c => {
|
||
const fl2 = f[c.key];
|
||
if (!fl2) return;
|
||
diagHtml += '<div class="fi"><div><div class="nm">'+c.label+'</div><div class="ds">'+fl2.desc+'</div></div><div class="st '+(fl2.pass?'st-p':'st-f')+'">'+(fl2.pass?'通过':'拒绝')+'</div></div>';
|
||
});
|
||
diagHtml += '<div class="sec-title" style="margin-top:8px">↓ 做空过滤</div>';
|
||
shortChecks.forEach(c => {
|
||
const fl2 = f[c.key];
|
||
if (!fl2) return;
|
||
diagHtml += '<div class="fi"><div><div class="nm">'+c.label+'</div><div class="ds">'+fl2.desc+'</div></div><div class="st '+(fl2.pass?'st-p':'st-f')+'">'+(fl2.pass?'通过':'拒绝')+'</div></div>';
|
||
});
|
||
fl.innerHTML = diagHtml;
|
||
}
|
||
}
|
||
|
||
let tradeInterval = null;
|
||
function swTab(n) {
|
||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||
document.querySelectorAll('.tc').forEach(t => t.classList.remove('active'));
|
||
const tabEl = document.querySelector(`.tab[onclick="swTab('${n}')"]`);
|
||
if (tabEl) tabEl.classList.add('active');
|
||
const contentEl = document.getElementById('tab-' + (n==='st'?'st':n==='diag'?'diag':'trade'));
|
||
if (contentEl) contentEl.classList.add('active');
|
||
if (n === 'trade') {
|
||
loadTradeData();
|
||
if (tradeInterval) clearInterval(tradeInterval);
|
||
tradeInterval = setInterval(loadTradeData, 30000);
|
||
} else {
|
||
if (tradeInterval) { clearInterval(tradeInterval); tradeInterval = null; }
|
||
}
|
||
}
|
||
|
||
function swSym(s) {
|
||
curSym = s;
|
||
document.querySelectorAll('.sb').forEach(b => b.classList.remove('ac'));
|
||
document.querySelector('.sb[onclick="swSym(\''+s+'\')"]').classList.add('ac');
|
||
if (cache[s]) render(); else loadData();
|
||
}
|
||
|
||
function refresh() { loadData(); }
|
||
|
||
// ============================================================
|
||
// 交易监测(Tab 3)
|
||
// ============================================================
|
||
let tradeCache = null;
|
||
|
||
async function loadTradeData() {
|
||
const headers = { 'X-API-Key': API_KEY, 'Accept': 'application/json' };
|
||
try {
|
||
const [accRes, tradeRes, statsRes] = await Promise.all([
|
||
fetch(SERVER_URL + '/api/account', { headers }),
|
||
fetch(SERVER_URL + '/api/trades?limit=50', { headers }),
|
||
fetch(SERVER_URL + '/api/trades/stats', { headers }),
|
||
]);
|
||
if (!accRes.ok || !tradeRes.ok || !statsRes.ok) throw new Error('API error');
|
||
tradeCache = {
|
||
account: await accRes.json(),
|
||
trades: await tradeRes.json(),
|
||
stats: await statsRes.json(),
|
||
};
|
||
renderTrade();
|
||
} catch(e) {
|
||
document.getElementById('accGrid').innerHTML = '<div class="er" style="grid-column:1/-1">数据获取失败: ' + e.message + '</div>';
|
||
}
|
||
}
|
||
|
||
function renderTrade() {
|
||
if (!tradeCache) return;
|
||
const a = tradeCache.account.account;
|
||
const s = tradeCache.account.stats;
|
||
const open = tradeCache.account.open_positions || [];
|
||
const trades = tradeCache.trades.trades || [];
|
||
const daily = tradeCache.account.daily_pnl || [];
|
||
const dist = tradeCache.stats.profit_distribution || [];
|
||
|
||
document.getElementById('accBalance').textContent = '$' + (a.current_balance || 0).toFixed(2);
|
||
const pnlEl = document.getElementById('accPnl');
|
||
const pnl = a.total_pnl_pct || 0;
|
||
pnlEl.textContent = (pnl >= 0 ? '+' : '') + pnl.toFixed(2) + '%';
|
||
pnlEl.className = 'v ' + (pnl > 0 ? 'gn' : pnl < 0 ? 'rd' : '');
|
||
document.getElementById('accWinRate').textContent = (s.win_rate || 0) + '%';
|
||
document.getElementById('accTrades').textContent = s.total_trades || 0;
|
||
document.getElementById('accStrategy').textContent = '策略: ' + (a.strategy || '--');
|
||
document.getElementById('accUpdate').textContent = '更新: ' + new Date().toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit'});
|
||
|
||
let posHtml = '';
|
||
if (open.length === 0) {
|
||
posHtml = '<div class="empty-state">无持仓</div>';
|
||
} else {
|
||
open.forEach(p => {
|
||
const dir = p.is_short ? 'SHORT' : 'LONG';
|
||
const dirCls = p.is_short ? 'pos-short' : 'pos-long';
|
||
const pct = p.unrealized_pnl_pct ? (p.unrealized_pnl_pct * 100).toFixed(2) + '%' : '--';
|
||
const pctCls = p.unrealized_pnl_pct > 0 ? 'win' : p.unrealized_pnl_pct < 0 ? 'lose' : '';
|
||
posHtml += '<div class="tr">'
|
||
+ '<div class="tp">'
|
||
+ '<span><span class="pos-badge ' + dirCls + '">' + dir + '</span> ' + p.pair + '</span>'
|
||
+ '<span class="pnl ' + pctCls + '">' + pct + '</span>'
|
||
+ '</div>'
|
||
+ '<div class="td">'
|
||
+ '<span>入场: $' + (p.open_rate || 0).toFixed(2) + '</span>'
|
||
+ '<span>数量: ' + (p.amount || 0).toFixed(4) + '</span>'
|
||
+ '<span>止损: $' + (p.stop_loss || 0).toFixed(2) + '</span>'
|
||
+ '</div>'
|
||
+ '</div>';
|
||
});
|
||
}
|
||
document.getElementById('openPos').innerHTML = posHtml;
|
||
|
||
document.getElementById('tradeTotal').textContent = tradeCache.trades.pagination.total;
|
||
let tbody = '';
|
||
if (trades.length === 0) {
|
||
tbody = '<tr><td colspan="6" class="empty-state">暂无交易记录(策略刚开始运行或尚未开仓)</td></tr>';
|
||
} else {
|
||
trades.forEach(t => {
|
||
const dir = t.is_short ? '空' : '多';
|
||
const dirCls = t.is_short ? 'lose' : 'win';
|
||
const pnlPct = t.close_profit ? (t.close_profit * 100).toFixed(2) + '%' : '--';
|
||
const pnlCls = (t.close_profit || 0) > 0 ? 'win' : (t.close_profit || 0) < 0 ? 'lose' : '';
|
||
const openTime = t.open_date ? t.open_date.slice(5,16).replace('T',' ') : '--';
|
||
const exit = t.exit_reason || (t.is_open ? '持仓中' : '--');
|
||
tbody += '<tr>'
|
||
+ '<td>' + openTime + '</td>'
|
||
+ '<td class="' + dirCls + '">' + dir + '</td>'
|
||
+ '<td class="num">$' + (t.open_rate || 0).toFixed(2) + '</td>'
|
||
+ '<td class="num">' + (t.close_rate ? '$' + t.close_rate.toFixed(2) : '持仓中') + '</td>'
|
||
+ '<td class="num ' + pnlCls + '">' + pnlPct + '</td>'
|
||
+ '<td>' + exit + '</td>'
|
||
+ '</tr>';
|
||
});
|
||
}
|
||
document.getElementById('tradeBody').innerHTML = tbody;
|
||
|
||
renderDailyPnl(daily);
|
||
renderDistChart(dist);
|
||
}
|
||
|
||
function renderDailyPnl(daily) {
|
||
const svg = document.getElementById('pnlSvg');
|
||
const w = 340, h = 120, pad = 20;
|
||
if (!daily || daily.length < 2) {
|
||
svg.innerHTML = '<rect width="'+w+'" height="'+h+'" fill="var(--surface)" rx="6"/><text x="170" y="60" text-anchor="middle" fill="var(--text3)" font-size="12">暂无数据</text>';
|
||
return;
|
||
}
|
||
const rev = daily.slice().reverse();
|
||
const vals = rev.map(d => d.pnl_abs || 0);
|
||
const mn = Math.min(0, ...vals), mx = Math.max(0, ...vals);
|
||
const rg = (mx - mn) || 1;
|
||
const cw = w - pad*2, ch = h - pad*2 - 10;
|
||
const bw = rev.length > 1 ? Math.min(cw / rev.length, 20) : 10;
|
||
|
||
let bars = '';
|
||
const zero = pad + ch - ((0 - mn) / rg) * ch;
|
||
rev.forEach((d, i) => {
|
||
const x = pad + i * (cw / Math.max(rev.length - 1, 1));
|
||
const v = d.pnl_abs || 0;
|
||
const barH = Math.abs(v) / rg * ch;
|
||
const y = v >= 0 ? zero - barH : zero;
|
||
const color = v >= 0 ? 'var(--green)' : 'var(--red)';
|
||
bars += '<rect x="' + (x - bw/2) + '" y="' + y + '" width="' + bw + '" height="' + barH + '" fill="' + color + '" opacity="0.8" rx="1"/>';
|
||
});
|
||
|
||
svg.innerHTML = '<rect width="'+w+'" height="'+h+'" fill="var(--surface)" rx="6"/>'
|
||
+ '<line x1="'+pad+'" y1="'+zero+'" x2="'+(w-pad)+'" y2="'+zero+'" stroke="var(--border)" stroke-width="0.5"/>'
|
||
+ bars
|
||
+ '<text x="'+pad+'" y="'+(h-4)+'" fill="var(--text3)" font-size="9">' + rev[0].day + '</text>'
|
||
+ '<text x="'+(w-pad)+'" y="'+(h-4)+'" text-anchor="end" fill="var(--text3)" font-size="9">' + rev[rev.length-1].day + '</text>';
|
||
}
|
||
|
||
function renderDistChart(dist) {
|
||
const svg = document.getElementById('distSvg');
|
||
const w = 340, h = 100, pad = 30;
|
||
if (!dist || dist.length === 0) {
|
||
svg.innerHTML = '<rect width="'+w+'" height="'+h+'" fill="var(--surface)" rx="6"/><text x="170" y="50" text-anchor="middle" fill="var(--text3)" font-size="12">暂无数据</text>';
|
||
return;
|
||
}
|
||
const maxCount = Math.max(...dist.map(d => d.count || 0), 1);
|
||
const cw = w - pad*2, ch = h - pad - 16;
|
||
const bw = cw / dist.length - 4;
|
||
|
||
let bars = '';
|
||
dist.forEach((d, i) => {
|
||
const x = pad + i * (cw / dist.length) + 2;
|
||
const barH = ((d.count || 0) / maxCount) * ch;
|
||
const y = h - 16 - barH;
|
||
const isLoss = d.profit_range && (d.profit_range.startsWith('\u2264') || d.profit_range.startsWith('-'));
|
||
const color = isLoss ? 'var(--red)' : 'var(--green)';
|
||
bars += '<rect x="' + x + '" y="' + y + '" width="' + bw + '" height="' + barH + '" fill="' + color + '" opacity="0.7" rx="1"/>';
|
||
bars += '<text x="' + (x + bw/2) + '" y="' + (h - 2) + '" text-anchor="middle" fill="var(--text3)" font-size="7" transform="rotate(-45,'+(x+bw/2)+','+(h-2)+')">' + (d.profit_range || '') + '</text>';
|
||
});
|
||
|
||
svg.innerHTML = '<rect width="'+w+'" height="'+h+'" fill="var(--surface)" rx="6"/>' + bars;
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
if (IS_CLOUDSTUDIO) {
|
||
document.getElementById('csBanner').style.display = 'block';
|
||
}
|
||
loadData();
|
||
setInterval(loadData, 60000);
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|