alphax/static/paper_trading.html
2026-05-16 21:47:36 +08:00

75 lines
10 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.

{% extends "base.html" %}
{% block title %}AlphaX Agent Crypto — 模拟交易{% endblock %}
{% block extra_head_css %}
<style>
.shell{width:min(100% - 40px,1280px);margin:0 auto;padding:24px 0 48px}.page-head{display:flex;align-items:flex-end;justify-content:space-between;gap:14px;flex-wrap:wrap;margin-bottom:16px}.page-head h1{font-size:28px;font-weight:950;color:var(--ink);letter-spacing:-.7px}.page-head p{margin-top:5px;color:var(--stone);font-size:13px;line-height:1.55;max-width:860px}.actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.btn,.select{height:38px;border:1px solid var(--hairline-strong);background:var(--canvas);border-radius:var(--radius-md);padding:0 12px;font-size:13px;font-weight:850;color:var(--ink)}.btn{cursor:pointer}.kpis{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:10px;margin-bottom:14px}.kpi{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-md);padding:14px;min-width:0}.kpi span{display:block;color:var(--stone);font-size:11px;font-weight:900}.kpi b{display:block;margin-top:7px;color:var(--ink);font-size:23px;line-height:1;font-weight:950;letter-spacing:-.5px}.kpi small{display:block;margin-top:7px;color:var(--stone);font-size:11px;font-weight:800}.kpi b.green{color:var(--green)}.kpi b.red{color:var(--red)}.kpi b.blue{color:var(--blue)}.note{border:1px solid rgba(66,98,255,.14);background:rgba(66,98,255,.045);border-radius:var(--radius-md);padding:11px 12px;color:var(--slate);font-size:12px;line-height:1.55;margin-bottom:14px}.panel{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-md);overflow:hidden}.panel-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:13px 14px;border-bottom:1px solid var(--hairline-soft)}.panel-title{font-size:14px;font-weight:950;color:var(--ink)}.panel-note{font-size:11px;color:var(--stone);font-weight:850}.table-wrap{overflow:auto}.table{width:100%;border-collapse:collapse;min-width:1180px}.table th,.table td{padding:11px 10px;border-bottom:1px solid var(--hairline-soft);text-align:left;font-size:12px;vertical-align:middle}.table th{font-size:11px;color:var(--stone);font-weight:900;background:var(--surface)}.sym{font-weight:950;color:var(--ink);font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.badge{display:inline-flex;align-items:center;height:24px;border-radius:999px;padding:0 8px;font-size:11px;font-weight:900;border:1px solid var(--hairline-soft);background:var(--surface);color:var(--slate);white-space:nowrap}.badge.open{background:rgba(66,98,255,.08);border-color:rgba(66,98,255,.18);color:var(--blue)}.badge.closed{background:var(--green-light);border-color:rgba(0,180,115,.18);color:var(--green)}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-weight:850}.pos{color:var(--green)}.neg{color:var(--red)}.muted{color:var(--stone)}.riskline{display:grid;gap:3px}.pagination{display:flex;justify-content:center;align-items:center;gap:12px;padding:14px;color:var(--stone);font-size:12px}.pagination button{height:34px;border:1px solid var(--hairline-strong);background:var(--canvas);border-radius:var(--radius-md);padding:0 12px;font-size:12px;font-weight:850;cursor:pointer}.pagination button:disabled{opacity:.45;cursor:default}.empty,.loading{text-align:center;padding:34px 14px;color:var(--stone);font-size:13px}@media(max-width:1100px){.kpis{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(max-width:700px){.kpis{grid-template-columns:repeat(2,minmax(0,1fr))}.shell{width:min(100% - 24px,1280px)}}@media(max-width:520px){.kpis{grid-template-columns:1fr}.page-head h1{font-size:22px}}
</style>
{% endblock %}
{% block content %}
<div class="shell">
<div class="page-head">
<div>
<h1>模拟交易</h1>
<p>这里展示的是 paper trading 账本:只有系统把可买信号模拟成交后,才会进入收益统计。推荐历史和观察池不会直接产生收益率。</p>
</div>
<div class="actions">
<select class="select" id="statusFilter" onchange="loadTrades(0)">
<option value="">全部</option>
<option value="open">持仓中</option>
<option value="closed">已平仓</option>
</select>
<button class="btn" onclick="loadAll()">刷新</button>
</div>
</div>
<div class="note" id="paperNote">模拟成交默认使用 20,000U 本金、每笔 5,000U 名义仓位、5x 杠杆、1,000U 保证金,用来验证策略真实交易口径。它不代表真实账户持仓,也不会反写推荐收益。</div>
<div class="kpis" id="kpis"><div class="kpi"><span>状态</span><b>加载中</b></div></div>
<section class="panel">
<div class="panel-head"><div class="panel-title">交易账本</div><div class="panel-note" id="pageInfo">--</div></div>
<div class="table-wrap">
<table class="table">
<thead><tr><th>币种</th><th>状态</th><th>仓位</th><th>开仓</th><th>止盈 / 止损</th><th>最新 / 平仓</th><th>价格收益</th><th>账户收益</th><th>退出原因</th><th>来源</th></tr></thead>
<tbody id="tradeRows"><tr><td colspan="10" class="loading">加载中...</td></tr></tbody>
</table>
</div>
<div class="pagination" id="pager"></div>
</section>
</div>
{% endblock %}
{% block extra_script %}
<script>
var LIMIT=50,offset=0,total=0;
function $(id){return document.getElementById(id)}
function esc(v){return String(v==null?'':v).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c];});}
function fmt(v,d){v=Number(v||0);return v.toLocaleString(undefined,{maximumFractionDigits:d==null?4:d,minimumFractionDigits:0})}
function pct(v){v=Number(v||0);var cls=v>0?'pos':v<0?'neg':'';return '<span class="mono '+cls+'">'+(v>0?'+':'')+fmt(v,2)+'%</span>'}
function money(v){v=Number(v||0);var cls=v>0?'pos':v<0?'neg':'';return '<span class="mono '+cls+'">'+(v>0?'+':'')+fmt(v,2)+' USDT</span>'}
function time(t){if(!t)return'--';var d=new Date(t);if(isNaN(d.getTime()))return String(t).slice(0,16).replace('T',' ');return (d.getMonth()+1)+'/'+d.getDate()+' '+String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0')}
async function api(url){var r=await fetch(url);var d=await r.json().catch(function(){return{}});if(!r.ok)throw new Error(d.detail||d.error||'请求失败');return d}
async function loadAll(){await Promise.all([loadSummary(),loadTrades(offset)])}
async function loadSummary(){try{var d=await api('/api/paper-trading/summary?days=30');$('paperNote').textContent='模拟成交使用 '+fmt(d.account_equity_usdt||20000,0)+'U 本金、每笔 '+fmt(d.notional_usdt||5000,0)+'U 名义仓位、'+fmt(d.leverage||5,1)+'x 杠杆、'+fmt(d.margin_usdt||1000,0)+'U 保证金。账户收益率按已实现/浮动盈亏除以模拟本金计算。';$('kpis').innerHTML=[
card('模拟本金',fmt(d.account_equity_usdt||0,0)+'U','blue','账户收益率基准'),
card('单笔仓位',fmt(d.notional_usdt||0,0)+'U','','名义仓位'),
card('杠杆 / 保证金',fmt(d.leverage||1,1)+'x','',fmt(d.margin_usdt||0,0)+'U 保证金'),
card('持仓 / 已平仓',(d.open_count||0)+' / '+(d.closed_count||0),''),
card('账户总收益率',fmt(d.account_total_return_pct||0,2)+'%',(d.account_total_return_pct||0)>=0?'green':'red',money(d.total_pnl_usdt||0).replace(/<[^>]+>/g,'')),
card('胜率',(d.win_rate||0)+'%','green','已平仓样本')
].join('')}catch(e){$('kpis').innerHTML='<div class="kpi"><span>状态</span><b>加载失败</b></div>'}}
function card(label,value,cls,sub){return '<div class="kpi"><span>'+esc(label)+'</span><b class="'+esc(cls||'')+'">'+esc(value)+'</b>'+(sub?'<small>'+esc(sub)+'</small>':'')+'</div>'}
async function loadTrades(nextOffset){offset=Math.max(0,nextOffset||0);$('tradeRows').innerHTML='<tr><td colspan="10" class="loading">加载中...</td></tr>';try{var s=$('statusFilter').value;var d=await api('/api/paper-trading/trades?limit='+LIMIT+'&offset='+offset+'&status='+encodeURIComponent(s));total=d.total||0;renderTrades(d.items||[]);renderPager()}catch(e){$('tradeRows').innerHTML='<tr><td colspan="10" class="empty">'+esc(e.message)+'</td></tr>'}}
function renderTrades(items){if(!items.length){$('tradeRows').innerHTML='<tr><td colspan="10" class="empty">暂无模拟交易</td></tr>';return}$('tradeRows').innerHTML=items.map(function(x){var st=x.status==='open'?'持仓中':'已平仓';var latest=x.status==='open'?x.current_price:x.exit_price;var pnlUsdt=x.status==='closed'?x.realized_pnl_usdt:x.unrealized_pnl_usdt;return '<tr>'+
'<td><div class="sym">'+esc(x.symbol)+'</div><div class="muted">#'+esc(x.id)+' · Rec '+esc(x.recommendation_id)+'</div></td>'+
'<td><span class="badge '+esc(x.status)+'">'+st+'</span></td>'+
'<td><div class="mono">'+fmt(x.notional_usdt,0)+'U</div><div class="muted">'+fmt(x.leverage,1)+'x · 保证金 '+fmt(x.margin_usdt,0)+'U</div></td>'+
'<td><div class="mono">$'+fmt(x.entry_price,6)+'</div><div class="muted">'+time(x.opened_at)+'</div></td>'+
'<td><div class="riskline"><span>TP $'+fmt(x.tp1,6)+'</span><span>SL $'+fmt(x.stop_loss,6)+'</span></div></td>'+
'<td><div class="mono">$'+fmt(latest,6)+'</div><div class="muted">'+(x.closed_at?time(x.closed_at):'最新')+'</div></td>'+
'<td>'+pct(x.status==='closed'?x.realized_pnl_pct:x.pnl_pct)+'</td>'+
'<td><div>'+money(pnlUsdt)+'</div><div class="muted">账户 '+(x.account_return_pct>0?'+':'')+fmt(x.account_return_pct,2)+'% · 保证金 '+(x.margin_roi_pct>0?'+':'')+fmt(x.margin_roi_pct,2)+'%</div></td>'+
'<td>'+esc(x.exit_reason||'--')+'</td>'+
'<td><div>'+esc(x.source_status||'--')+'</div><div class="muted">'+esc(x.strategy_version||'')+'</div></td>'+
'</tr>'}).join('')}
function renderPager(){var page=Math.floor(offset/LIMIT)+1,totalPages=Math.max(1,Math.ceil(total/LIMIT));$('pageInfo').textContent='第 '+page+' / '+totalPages+' 页 · 共 '+total+' 条';$('pager').innerHTML='<button '+(offset===0?'disabled':'')+' onclick="loadTrades('+(offset-LIMIT)+')">上一页</button><span>第 '+page+' / '+totalPages+' 页</span><button '+((offset+LIMIT>=total)?'disabled':'')+' onclick="loadTrades('+(offset+LIMIT)+')">下一页</button>'}
loadAll();
</script>
{% endblock %}