alphax/static/paper_trading.html
2026-06-04 23:33:21 +08:00

283 lines
43 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 — 策略交易{% 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,.input{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)}.input{width:150px}.btn{cursor:pointer}.btn.danger{border-color:rgba(229,62,62,.25);background:rgba(229,62,62,.06);color:var(--red)}.row-action{height:30px;border:1px solid rgba(229,62,62,.22);background:rgba(229,62,62,.05);color:var(--red);border-radius:10px;padding:0 9px;font-size:11px;font-weight:900;cursor:pointer;white-space:nowrap}.maintenance-title{font-size:13px;font-weight:950;color:var(--ink)}.maintenance-copy{margin-top:3px;color:var(--stone);font-size:11px;font-weight:800;line-height:1.45}.maintenance-menu{position:relative}.maintenance-popover{display:none;position:absolute;right:0;top:44px;width:min(360px,calc(100vw - 32px));z-index:30;border:1px solid rgba(229,62,62,.16);background:var(--canvas);box-shadow:0 18px 50px rgba(15,23,42,.16);border-radius:var(--radius-md);padding:12px}.maintenance-popover.open{display:block}.maintenance-popover .maintenance-copy{margin-bottom:10px}.maintenance-popover .actions{align-items:stretch}.maintenance-popover .select{flex:1;min-width:170px}.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}.perf-panel{border:1px solid var(--hairline-soft);background:linear-gradient(180deg,var(--canvas),var(--surface));border-radius:var(--radius-md);padding:14px;margin-bottom:14px}.perf-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:10px}.perf-title{font-size:14px;font-weight:950;color:var(--ink)}.perf-meta{display:flex;gap:8px;flex-wrap:wrap}.perf-pill{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:999px;padding:5px 9px;font-size:11px;font-weight:900;color:var(--slate)}.perf-chart{width:100%;height:260px}.perf-chart svg{width:100%;height:100%;display:block}.chart-grid{stroke:rgba(148,163,184,.25);stroke-width:1}.equity-line{fill:none;stroke:var(--blue);stroke-width:3;stroke-linecap:round;stroke-linejoin:round}.equity-area{fill:rgba(66,98,255,.08)}.bar-pos{fill:rgba(0,180,115,.45)}.bar-neg{fill:rgba(229,62,62,.42)}.chart-label{fill:var(--stone);font-size:11px;font-weight:800}.tabs{display:flex;gap:8px;margin:14px 0 0;padding:6px;border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);width:max-content;max-width:100%;overflow:auto}.tab-btn{height:38px;border:0;background:transparent;border-radius:10px;padding:0 14px;font-size:13px;font-weight:950;color:var(--stone);cursor:pointer;white-space:nowrap}.tab-btn.active{background:var(--canvas);color:var(--ink);box-shadow:0 10px 24px rgba(15,23,42,.08)}.tab-panel{display:none}.tab-panel.active{display:block}.panel{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-md);overflow:hidden;margin-top:14px}.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:1320px}.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,.sym-link{font-weight:950;color:var(--ink);font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.sym-link{text-decoration:none}.sym-link:hover{color:var(--blue);text-decoration:underline}.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)}.badge.side-long{background:rgba(0,180,115,.08);border-color:rgba(0,180,115,.18);color:var(--green)}.badge.side-short{background:rgba(239,68,68,.08);border-color:rgba(239,68,68,.18);color:var(--red)}.badge.event-open{background:rgba(66,98,255,.08);border-color:rgba(66,98,255,.18);color:var(--blue)}.badge.event-close{background:var(--green-light);border-color:rgba(0,180,115,.18);color:var(--green)}.badge.event-trailing{background:rgba(245,158,11,.12);border-color:rgba(245,158,11,.22);color:#a05a00}.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}.trail-line{display:inline-flex;align-items:center;gap:4px;width:max-content;max-width:100%;border:1px solid rgba(245,158,11,.24);background:rgba(245,158,11,.11);color:#a05a00;border-radius:999px;padding:2px 7px;font-weight:950}.trail-line.off{border-color:var(--hairline-soft);background:var(--surface);color:var(--stone);font-weight:850}.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}.event-list{padding:4px 14px 12px}.event{display:grid;grid-template-columns:130px 140px 1fr 170px;gap:12px;align-items:flex-start;padding:13px 0;border-bottom:1px solid var(--hairline-soft)}.event:last-child{border-bottom:0}.event-time{font-size:12px;color:var(--stone);font-weight:850}.event-main{min-width:0}.event-title{font-size:13px;font-weight:950;color:var(--ink);margin-bottom:4px}.event-msg{font-size:12px;color:var(--slate);line-height:1.45}.event-detail{font-size:11px;color:var(--stone);line-height:1.5}.event-price{text-align:right;font-size:12px}.event-price b{display:block;color:var(--ink);font-size:13px}@media(max-width:1100px){.kpis{grid-template-columns:repeat(3,minmax(0,1fr))}.event{grid-template-columns:110px 120px 1fr}}@media(max-width:700px){.kpis{grid-template-columns:repeat(2,minmax(0,1fr))}.shell{width:min(100% - 24px,1280px)}.event{grid-template-columns:1fr}.event-price{text-align:left}.tabs{width:100%}.tab-btn{flex:1}.perf-chart{height:220px}.perf-head{display:block}.perf-meta{margin-top:8px}.maintenance-popover{position:fixed;left:12px;right:12px;top:76px;width:auto}.maintenance-popover .actions{display:grid;grid-template-columns:1fr}}@media(max-width:520px){.kpis{grid-template-columns:1fr}.page-head h1{font-size:22px}}
.strategy-context{display:flex;flex-wrap:wrap;gap:5px;margin-top:7px}.ctx-pill{display:inline-flex;align-items:center;max-width:100%;border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:999px;padding:3px 7px;font-size:10px;font-weight:900;color:var(--slate)}.ctx-pill.risk{color:var(--red);background:var(--red-light);border-color:rgba(229,62,62,.18)}.ctx-pill.good{color:var(--green);background:var(--green-light);border-color:rgba(0,180,115,.18)}.ctx-pill.warn{color:#a05a00;background:rgba(245,158,11,.11);border-color:rgba(245,158,11,.22)}
.strategy-board{border:1px solid var(--hairline-soft);background:linear-gradient(180deg,var(--canvas),var(--surface));border-radius:var(--radius-md);padding:14px;margin-bottom:14px}.strategy-board-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:10px}.strategy-board-title{font-size:14px;font-weight:950;color:var(--ink)}.strategy-board-copy{margin-top:3px;color:var(--stone);font-size:11px;font-weight:800;line-height:1.5}.strategy-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}.strategy-card{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:14px;padding:12px;text-align:left;cursor:pointer;transition:transform .16s ease,border-color .16s ease,box-shadow .16s ease}.strategy-card:hover{transform:translateY(-1px);border-color:rgba(66,98,255,.22);box-shadow:0 12px 26px rgba(15,23,42,.08)}.strategy-card.active{border-color:rgba(66,98,255,.38);box-shadow:0 0 0 3px rgba(66,98,255,.08)}.strategy-card-head{display:flex;align-items:center;justify-content:space-between;gap:8px}.strategy-card-name{font-size:13px;font-weight:950;color:var(--ink);line-height:1.25}.strategy-card-desc{margin-top:6px;color:var(--stone);font-size:11px;font-weight:800;line-height:1.45;min-height:32px}.strategy-stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px;margin-top:10px}.strategy-stat{border:1px solid var(--hairline-soft);background:var(--surface);border-radius:10px;padding:7px}.strategy-stat span{display:block;color:var(--stone);font-size:10px;font-weight:900}.strategy-stat b{display:block;margin-top:3px;color:var(--ink);font-size:14px;font-weight:950}.strategy-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:10px}.strategy-score{font-size:11px;font-weight:950;color:var(--slate)}.strategy-decision{font-size:11px;font-weight:950}.strategy-decision.good{color:var(--green)}.strategy-decision.warn{color:#a05a00}.strategy-decision.risk{color:var(--red)}.strategy-empty{padding:18px;border:1px dashed var(--hairline-strong);border-radius:14px;color:var(--stone);font-size:12px;font-weight:850;text-align:center}@media(max-width:1180px){.strategy-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:700px){.strategy-grid{grid-template-columns:1fr}.strategy-board-head{display:block}.strategy-board .actions{margin-top:8px}}
</style>
{% endblock %}
{% block content %}
<div class="shell">
<div class="page-head">
<div>
<h1>策略交易</h1>
<p>这里展示策略信号进入交易账本后的表现:只有系统把可买信号转入持仓或挂单后,才会进入收益统计。推荐历史和观察池不会直接产生收益率。</p>
</div>
<div class="actions">
<select class="select" id="strategyFilter" title="按策略筛选" onchange="onStrategyFilterChange()">
<option value="">全部策略</option>
</select>
<select class="select" id="sideFilter" title="按方向筛选" onchange="onSideFilterChange()">
<option value="">全部方向</option>
<option value="long">只看多</option>
<option value="short">只看空</option>
</select>
<button class="btn" id="sendReportBtn" onclick="sendReport()">发送策略交易报告</button>
<button class="btn" onclick="loadAll()">刷新</button>
<div class="maintenance-menu">
<button class="btn" type="button" onclick="toggleMaintenanceMenu(event)">账本维护</button>
<div class="maintenance-popover" id="maintenancePopover" aria-label="策略交易账本维护">
<div class="maintenance-title">账本维护</div>
<div class="maintenance-copy">用于清理异常测试数据或重置策略交易账本。删除只影响策略交易表,不会删除推荐、筛选和行情数据。</div>
<div class="actions">
<select class="select" id="resetScope" title="重置范围">
<option value="all">全部账本数据</option>
<option value="open_trades">仅持仓中</option>
<option value="closed_trades">仅已平仓</option>
<option value="orders">仅挂单</option>
<option value="completed">已完结持仓和取消订单</option>
<option value="events">仅操作日志</option>
</select>
<button class="btn danger" type="button" onclick="resetLedger()">重置所选数据</button>
</div>
</div>
</div>
</div>
</div>
<div class="note" id="paperNote">策略交易只统计已经进入交易账本的信号。页面用账户余额、持仓价值、累计杠杆和实际盈亏展示策略表现,不再把观察池或推荐归档当作收益。</div>
<div class="note" id="reportNote" style="display:none"></div>
<section class="strategy-board">
<div class="strategy-board-head">
<div>
<div class="strategy-board-title">运行策略看板</div>
<div class="strategy-board-copy">按策略独立看信号、机会、交易、胜率和收益;点击卡片可筛选下方持仓、挂单、完结持仓、取消订单和日志。</div>
</div>
<div class="actions">
<button class="btn" type="button" onclick="clearStrategyAndSide()">查看全部</button>
</div>
</div>
<div class="strategy-grid" id="strategyCards"><div class="strategy-empty">策略数据加载中...</div></div>
</section>
<div class="kpis" id="kpis"><div class="kpi"><span>状态</span><b>加载中</b></div></div>
<section class="perf-panel">
<div class="perf-head">
<div>
<div class="perf-title">每日收益与权益曲线</div>
<div class="panel-note">蓝线展示账户权益变化,绿色/红色柱展示每日实现收益,右侧同步最大回撤。</div>
</div>
<div class="perf-meta" id="perfMeta"><span class="perf-pill">加载中...</span></div>
</div>
<div class="perf-chart" id="performanceChart"><div class="loading">加载中...</div></div>
</section>
<div class="tabs" role="tablist" aria-label="策略交易视图切换">
<button class="tab-btn active" id="tab-open" type="button" onclick="setTradeTab('open')" role="tab" aria-selected="true">持仓中</button>
<button class="tab-btn" id="tab-orders" type="button" onclick="setTradeTab('orders')" role="tab" aria-selected="false">挂单中</button>
<button class="tab-btn" id="tab-closed" type="button" onclick="setTradeTab('closed')" role="tab" aria-selected="false">已完结持仓</button>
<button class="tab-btn" id="tab-canceled" type="button" onclick="setTradeTab('canceled')" role="tab" aria-selected="false">取消订单</button>
<button class="tab-btn" id="tab-events" type="button" onclick="setTradeTab('events')" role="tab" aria-selected="false">操作日志</button>
</div>
<div class="tab-panel active" id="panel-open" role="tabpanel" aria-labelledby="tab-open">
<section class="panel">
<div class="panel-head"><div class="panel-title">持仓中</div><div class="panel-note" id="openPageInfo">--</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><th>价格收益</th><th>账户收益</th><th>退出原因</th><th>来源</th><th>操作</th></tr></thead>
<tbody id="openRows"><tr><td colspan="15" class="loading">加载中...</td></tr></tbody>
</table>
</div>
<div class="pagination" id="openPager"></div>
</section>
</div>
<div class="tab-panel" id="panel-orders" role="tabpanel" aria-labelledby="tab-orders">
<section class="panel">
<div class="panel-head"><div><div class="panel-title">挂单中</div><div class="panel-note">等回踩机会通过闸门后进入这里,价格触达后再转成策略持仓</div></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><th>操作</th></tr></thead>
<tbody id="orderRows"><tr><td colspan="11" class="loading">加载中...</td></tr></tbody>
</table>
</div>
</section>
</div>
<div class="tab-panel" id="panel-closed" role="tabpanel" aria-labelledby="tab-closed">
<section class="panel">
<div class="panel-head"><div><div class="panel-title">已完结持仓</div><div class="panel-note" id="closedInfo">已经开仓并完成平仓的策略交易</div></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><th>价格收益</th><th>账户收益</th><th>退出原因</th><th>来源</th><th>操作</th></tr></thead>
<tbody id="closedTradeRows"><tr><td colspan="15" class="loading">加载中...</td></tr></tbody>
</table>
</div>
</section>
</div>
<div class="tab-panel" id="panel-canceled" role="tabpanel" aria-labelledby="tab-canceled">
<section class="panel">
<div class="panel-head"><div><div class="panel-title">取消订单</div><div class="panel-note">未转成持仓的挂单:风控取消、过期、拒绝等</div></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><th>操作</th></tr></thead>
<tbody id="canceledOrderRows"><tr><td colspan="11" class="loading">加载中...</td></tr></tbody>
</table>
</div>
</section>
</div>
<div class="tab-panel" id="panel-events" role="tabpanel" aria-labelledby="tab-events">
<section class="panel">
<div class="panel-head">
<div><div class="panel-title">操作日志</div><div class="panel-note">开仓、平仓、移动止盈激活与上移都会记录在这里</div></div>
<div class="actions">
<input class="input" id="eventSymbol" placeholder="币种,如 LINK/USDT" onkeydown="if(event.key==='Enter')loadEvents(0)">
<select class="select" id="eventType" onchange="loadEvents(0)">
<option value="">全部动作</option>
<option value="open">开仓</option>
<option value="close">平仓</option>
<option value="trailing_activate">移动止盈激活</option>
<option value="trailing_move">移动止盈上移</option>
</select>
<button class="btn" onclick="loadEvents(0)">查询</button>
</div>
</div>
<div class="event-list" id="eventRows"><div class="loading">加载中...</div></div>
<div class="pagination" id="eventPager"></div>
</section>
</div>
</div>
{% endblock %}
{% block extra_script %}
<script src="/static/chart_widgets.js"></script>
<script>
var LIMIT=50,openOffset=0,openTotal=0,EVENT_LIMIT=80,eventOffset=0,eventTotal=0,strategyData=[];
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')}
function durationText(start,end){if(!start)return'--';var a=new Date(start),b=end?new Date(end):new Date();if(isNaN(a.getTime())||isNaN(b.getTime()))return'--';var mins=Math.max(0,Math.floor((b-a)/60000));var days=Math.floor(mins/1440),hours=Math.floor((mins%1440)/60),m=mins%60;if(days>0)return days+'天 '+hours+'小时';if(hours>0)return hours+'小时 '+m+'分';return Math.max(1,m)+'分'}
function opportunityHref(x){var symbol=encodeURIComponent((x&&x.symbol)||'');var rec=(x&&x.recommendation_id)!=null?encodeURIComponent(x.recommendation_id):'';return '/opportunity?symbol='+symbol+(rec?'&rec_id='+rec:'')}
function symbolCell(x){return '<a class="sym-link" href="'+opportunityHref(x)+'">'+esc((x&&x.symbol)||'--')+'</a><div class="muted">#'+esc((x&&x.id)||'--')+' · Rec '+esc((x&&x.recommendation_id)||'--')+'</div>'}
function toggleMaintenanceMenu(ev){if(ev)ev.stopPropagation();var pop=$('maintenancePopover');if(pop)pop.classList.toggle('open')}
document.addEventListener('click',function(ev){var menu=document.querySelector('.maintenance-menu'),pop=$('maintenancePopover');if(pop&&menu&&!menu.contains(ev.target))pop.classList.remove('open')})
function sideText(v){return String(v||'long').toLowerCase()==='short'?'空':'多'}
function sideBadge(v){var s=String(v||'long').toLowerCase();return '<span class="badge '+(s==='short'?'side-short':'side-long')+'">'+sideText(s)+'</span>'}
function setTradeTab(tab){['open','orders','closed','canceled','events'].forEach(function(k){var on=k===tab;$('tab-'+k).classList.toggle('active',on);$('tab-'+k).setAttribute('aria-selected',on?'true':'false');$('panel-'+k).classList.toggle('active',on)})}
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 postApi(url){var r=await fetch(url,{method:'POST'});var d=await r.json().catch(function(){return{}});if(!r.ok)throw new Error(d.detail||d.error||'请求失败');return d}
async function deleteApi(url){var r=await fetch(url,{method:'DELETE'});var d=await r.json().catch(function(){return{}});if(!r.ok)throw new Error((d.detail&&d.detail.reason)||d.detail||d.error||'请求失败');return d}
function selectedStrategy(){return $('strategyFilter')?($('strategyFilter').value||''):''}
function selectedSide(){return $('sideFilter')?($('sideFilter').value||''):''}
function strategyQuery(){var code=selectedStrategy();return code?'&strategy_code='+encodeURIComponent(code):''}
function sideQuery(){var side=selectedSide();return side?'&side='+encodeURIComponent(side):''}
function tradeQuery(){return strategyQuery()+sideQuery()}
async function loadAll(){await Promise.all([loadStrategies(),loadSummary(),loadPerformance(),loadOrders(),loadOpenTrades(openOffset),loadClosedTrades(),loadCanceledOrders(),loadEvents(eventOffset)])}
function onStrategyFilterChange(){openOffset=0;eventOffset=0;loadAll()}
function onSideFilterChange(){openOffset=0;eventOffset=0;loadAll()}
function clearStrategyAndSide(){if($('strategyFilter'))$('strategyFilter').value='';if($('sideFilter'))$('sideFilter').value='';openOffset=0;eventOffset=0;loadAll()}
async function loadStrategies(){try{var d=await api('/api/paper-trading/strategies?days=120');strategyData=d.strategies||[];var sel=$('strategyFilter');var current=sel.value||'';sel.innerHTML='<option value="">全部策略</option>'+strategyData.map(function(x){return '<option value="'+esc(x.strategy_code)+'">'+esc(x.strategy_name||x.strategy_code)+' · '+esc(x.direction_label||'多')+' · '+esc(x.decision_label||'观察')+'</option>'}).join('');sel.value=Array.prototype.some.call(sel.options,function(o){return o.value===current})?current:'';renderStrategyCards()}catch(e){$('strategyCards').innerHTML='<div class="strategy-empty">策略数据加载失败:'+esc(e.message)+'</div>'}}
function decisionClass(x){var d=String((x&&x.decision)||'');if(d==='promote'||d==='keep')return'good';if(d==='pause')return'risk';return'warn'}
function modeText(v){return {paper_enabled:'策略交易启用',paper_only:'仅策略交易',observe_only:'仅观察'}[v]||v||'--'}
function renderStrategyCards(){var box=$('strategyCards');if(!box)return;if(!strategyData.length){box.innerHTML='<div class="strategy-empty">暂无已注册策略</div>';return}var current=selectedStrategy();box.innerHTML=strategyData.map(function(x){var active=current&&current===x.strategy_code;var pnl=Number(x.realized_pnl_usdt||0);var direction=x.direction||'long';return '<button class="strategy-card '+(active?'active':'')+'" type="button" onclick="selectStrategyCard(\''+esc(String(x.strategy_code||'')).replace(/'/g,'&#39;')+'\')">'+
'<div class="strategy-card-head"><div class="strategy-card-name">'+esc(x.strategy_name||x.strategy_code)+'</div>'+sideBadge(direction)+'</div>'+
'<div class="strategy-card-desc">'+esc(x.description||modeText(x.mode))+'</div>'+
'<div class="strategy-stats">'+
'<div class="strategy-stat"><span>信号</span><b>'+fmt(x.signal_count||0,0)+'</b></div>'+
'<div class="strategy-stat"><span>机会/交易</span><b>'+fmt(x.opportunity_count||0,0)+' / '+fmt(x.trade_count||0,0)+'</b></div>'+
'<div class="strategy-stat"><span>胜率</span><b>'+fmt(x.win_rate_pct||0,1)+'%</b></div>'+
'</div>'+
'<div class="strategy-stats">'+
'<div class="strategy-stat"><span>持仓</span><b>'+fmt(x.open_trade_count||0,0)+'</b></div>'+
'<div class="strategy-stat"><span>挂单</span><b>'+fmt(x.order_count||0,0)+'</b></div>'+
'<div class="strategy-stat"><span>收益</span><b class="'+(pnl>=0?'pos':'neg')+'">'+(pnl>0?'+':'')+fmt(pnl,1)+'U</b></div>'+
'</div>'+
'<div class="strategy-foot"><span class="strategy-score">评分 '+fmt(x.evaluation_score||0,1)+' · '+esc(modeText(x.mode))+'</span><span class="strategy-decision '+decisionClass(x)+'">'+esc(x.decision_label||'观察')+'</span></div>'+
'</button>'}).join('')}
function selectStrategyCard(code){if($('strategyFilter'))$('strategyFilter').value=code||'';openOffset=0;eventOffset=0;loadAll()}
async function sendReport(){var btn=$('sendReportBtn'),note=$('reportNote');btn.disabled=true;btn.textContent='发送中...';note.style.display='block';note.textContent='正在汇总当前交易数据并发送飞书报告...';try{var d=await postApi('/api/paper-trading/report?days=30');note.textContent=d.ok?'报告已发送到飞书。':'报告生成完成,但飞书发送未成功:'+String(d.push_result||'未知原因');await loadSummary()}catch(e){note.textContent='发送失败:'+e.message}finally{btn.disabled=false;btn.textContent='发送策略交易报告'}}
async function resetLedger(){var scope=$('resetScope').value||'all';var label=$('resetScope').selectedOptions[0]?$('resetScope').selectedOptions[0].textContent:scope;if(!confirm('确认重置“'+label+'”?这个操作会删除策略交易账本里的对应数据,不能从页面恢复。'))return;try{var d=await postApi('/api/paper-trading/reset?scope='+encodeURIComponent(scope));var del=d.deleted||{};alert('已重置:持仓/历史 '+(del.trades||0)+' 条,挂单 '+(del.orders||0)+' 条,日志 '+(del.events||0)+' 条。');var pop=$('maintenancePopover');if(pop)pop.classList.remove('open');await loadAll()}catch(e){alert('重置失败:'+e.message)}}
async function deleteTrade(id,symbol,status){if(!confirm('确认删除 '+symbol+' 的'+(status==='open'?'持仓':'历史仓位')+'记录?相关操作日志也会一起删除。'))return;try{await deleteApi('/api/paper-trading/trades/'+encodeURIComponent(id));await loadAll()}catch(e){alert('删除失败:'+e.message)}}
async function deleteOrder(id,symbol){if(!confirm('确认删除 '+symbol+' 的挂单记录?'))return;try{await deleteApi('/api/paper-trading/orders/'+encodeURIComponent(id));await loadAll()}catch(e){alert('删除失败:'+e.message)}}
async function loadSummary(){try{var d=await api('/api/paper-trading/summary?days=30');var totalPnl=Number(d.total_pnl_usdt||0),realized=Number(d.realized_pnl_usdt||0),unrealized=Number(d.open_unrealized_pnl_usdt||0),ret=Number(d.account_total_return_pct||0);$('paperNote').textContent='当前账户余额 = 初始本金 + 已实现收益 + 持仓浮动收益。累计杠杆按当前持仓名义价值 ÷ 当前账户余额计算,用来衡量账户整体暴露。';$('kpis').innerHTML=[
card('当前账户余额',fmt(d.current_balance_usdt||d.account_equity_usdt||0,2)+'U',totalPnl>=0?'green':'red','初始本金 '+fmt(d.initial_equity_usdt||d.account_equity_usdt||0,0)+'U'),
card('当前持仓价值',fmt(d.open_position_value_usdt||0,0)+'U','blue',(d.open_count||0)+' 个持仓中'),
card('策略挂单',fmt(d.pending_order_count||0,0),'blue','等待触价成交'),
card('持仓累计杠杆',fmt(d.cumulative_leverage||0,2)+'x','', '占账户余额的名义暴露'),
card('收益率',fmt(ret,2)+'%',ret>=0?'green':'red','按初始本金计算'),
card('收益额',(totalPnl>=0?'+':'')+fmt(totalPnl,2)+'U',totalPnl>=0?'green':'red','已实现 '+fmt(realized,2)+'U · 浮动 '+fmt(unrealized,2)+'U')
].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 loadPerformance(){try{var d=await api('/api/paper-trading/performance?days=30');renderPerformance(d)}catch(e){$('performanceChart').innerHTML='<div class="empty">'+esc(e.message)+'</div>';$('perfMeta').innerHTML='<span class="perf-pill">加载失败</span>'}}
function renderPerformance(d){var points=d.points||[];if(!points.length){$('performanceChart').innerHTML='<div class="empty">暂无收益曲线数据</div>';$('perfMeta').innerHTML='<span class="perf-pill">暂无数据</span>';return}var ret=Number(d.total_return_pct||0),dd=Number(d.max_drawdown_pct||0),pnl=Number(d.total_pnl_usdt||0);$('perfMeta').innerHTML=[
'<span class="perf-pill">当前权益 '+fmt(d.current_equity_usdt,2)+'U</span>',
'<span class="perf-pill '+(ret>=0?'pos':'neg')+'">总收益率 '+(ret>0?'+':'')+fmt(ret,2)+'%</span>',
'<span class="perf-pill">最大回撤 '+fmt(dd,2)+'%</span>',
'<span class="perf-pill '+(pnl>=0?'pos':'neg')+'">总收益 '+(pnl>0?'+':'')+fmt(pnl,2)+'U</span>'
].join('');AlphaXCharts.renderEquity($('performanceChart'),d)}
async function loadOrders(){$('orderRows').innerHTML='<tr><td colspan="11" class="loading">加载中...</td></tr>';try{var d=await api('/api/paper-trading/orders?limit=50&offset=0&status=pending'+tradeQuery());renderOrders(d.items||[])}catch(e){$('orderRows').innerHTML='<tr><td colspan="11" class="empty">'+esc(e.message)+'</td></tr>'}}
function renderOrders(items){if(!items.length){$('orderRows').innerHTML='<tr><td colspan="11" class="empty">暂无等待触价的策略挂单</td></tr>';return}$('orderRows').innerHTML=items.map(function(x){var latest=x.latest_price||x.current_price_at_create||0,dist=Number(x.distance_to_target_pct||0);return '<tr>'+
'<td>'+symbolCell(x)+'</td>'+
'<td><span class="badge open">'+esc(x.status==='pending'?'等待成交':x.status)+'</span></td>'+
'<td>'+sideBadge(x.side)+'</td>'+
'<td><div class="mono">$'+fmt(x.target_price,6)+'</div></td>'+
'<td><div class="mono">$'+fmt(latest,6)+'</div><div class="muted">'+(x.latest_price_updated_at?time(x.latest_price_updated_at):'创建时')+'</div></td>'+
'<td><span class="mono '+(dist>0?'neg':'pos')+'">'+(dist>0?'+':'')+fmt(dist,2)+'%</span></td>'+
'<td><div class="riskline"><span>TP $'+fmt(x.tp1,6)+'</span><span>SL $'+fmt(x.stop_loss,6)+'</span></div></td>'+
'<td>'+time(x.created_at)+'</td>'+
'<td>'+time(x.expires_at)+'</td>'+
'<td><div>'+esc(strategyName(x))+'</div><div class="muted">'+esc(x.source_status||'--')+' · '+esc(x.source_action||'')+'</div></td>'+
'<td><button class="row-action" type="button" onclick="deleteOrder('+Number(x.id)+',\''+esc(String(x.symbol||'')).replace(/'/g,'&#39;')+'\')">删除</button></td>'+
'</tr>'}).join('')}
function protectionCell(x){var trail=Number(x.trailing_stop||0);var trailHtml=trail>0?'<span class="trail-line">移动止盈 $'+fmt(trail,6)+'</span>':'<span class="trail-line off">移动止盈未启动</span>';return '<div class="riskline"><span>TP $'+fmt(x.tp1,6)+'</span><span>SL $'+fmt(x.stop_loss,6)+'</span>'+trailHtml+'</div>'}
function strategyName(x){return (x&&x.strategy_name)||({long_momentum_breakout_15m_1h_v1:'多头动量启动',long_second_wave_pullback_1h_v1:'多头二波回踩',short_breakdown_retest_1h_v1:'空头破位反抽'}[(x&&x.strategy_code)||'']||((x&&x.strategy_code)||'--'))}
async function loadOpenTrades(nextOffset){openOffset=Math.max(0,nextOffset||0);$('openRows').innerHTML='<tr><td colspan="15" class="loading">加载中...</td></tr>';try{var d=await api('/api/paper-trading/trades?limit='+LIMIT+'&offset='+openOffset+'&status=open'+tradeQuery());openTotal=d.total||0;renderTradeRows('openRows',d.items||[],'暂无持仓中的策略交易');renderOpenPager()}catch(e){$('openRows').innerHTML='<tr><td colspan="15" class="empty">'+esc(e.message)+'</td></tr>'}}
async function loadClosedTrades(){$('closedTradeRows').innerHTML='<tr><td colspan="15" class="loading">加载中...</td></tr>';try{var d=await api('/api/paper-trading/trades?limit=80&offset=0&status=closed'+tradeQuery());renderTradeRows('closedTradeRows',d.items||[],'暂无已完结持仓')}catch(e){$('closedTradeRows').innerHTML='<tr><td colspan="15" class="empty">'+esc(e.message)+'</td></tr>'}}
async function loadCanceledOrders(){$('canceledOrderRows').innerHTML='<tr><td colspan="11" class="loading">加载中...</td></tr>';try{var sets=await Promise.all(['canceled','expired','rejected'].map(function(s){return api('/api/paper-trading/orders?limit=50&offset=0&status='+s+tradeQuery())}));var items=[];sets.forEach(function(d){items=items.concat(d.items||[])});items.sort(function(a,b){return String(b.updated_at||b.created_at).localeCompare(String(a.updated_at||a.created_at))});renderCanceledOrders(items)}catch(e){$('canceledOrderRows').innerHTML='<tr><td colspan="11" class="empty">'+esc(e.message)+'</td></tr>'}}
function renderTradeRows(targetId,items,emptyText){if(!items.length){$(targetId).innerHTML='<tr><td colspan="15" class="empty">'+esc(emptyText||'暂无策略交易')+'</td></tr>';return}$(targetId).innerHTML=items.map(function(x){var st=x.status==='open'?'持仓中':'已平仓';var latest=x.latest_price||x.current_price||0;var pnlUsdt=x.status==='closed'?x.realized_pnl_usdt:x.unrealized_pnl_usdt;return '<tr>'+
'<td>'+symbolCell(x)+'</td>'+
'<td><span class="badge '+esc(x.status)+'">'+st+'</span></td>'+
'<td><div class="mono">'+esc(durationText(x.opened_at,x.closed_at))+'</div><div class="muted">'+(x.status==='closed'?'已结束':'持仓中')+'</div></td>'+
'<td>'+sideBadge(x.side)+'</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>'+protectionCell(x)+'</td>'+
'<td><div class="mono">$'+fmt(latest,6)+'</div><div class="muted">'+(x.latest_price_updated_at?time(x.latest_price_updated_at):'最新')+'</div></td>'+
'<td><div class="mono">'+(x.status==='closed'?'$'+fmt(x.exit_price,6):'--')+'</div><div class="muted">'+(x.status==='closed'?'实际退出':'未平仓')+'</div></td>'+
'<td><div class="mono">'+(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(strategyName(x))+'</div><div class="muted">'+esc(x.source_status||'--')+' · '+esc(x.strategy_version||'')+'</div></td>'+
'<td><button class="row-action" type="button" onclick="deleteTrade('+Number(x.id)+',\''+esc(String(x.symbol||'')).replace(/'/g,'&#39;')+'\',\''+esc(String(x.status||'')).replace(/'/g,'&#39;')+'\')">删除</button></td>'+
'</tr>'}).join('')}
function renderCanceledOrders(items){if(!items.length){$('canceledOrderRows').innerHTML='<tr><td colspan="11" class="empty">暂无取消或过期订单</td></tr>';return}$('canceledOrderRows').innerHTML=items.map(function(x){var latest=x.latest_price||x.current_price_at_create||0,ended=x.canceled_at||x.updated_at||x.filled_at;return '<tr>'+
'<td>'+symbolCell(x)+'</td>'+
'<td><span class="badge '+(x.status==='filled'?'closed':'')+'">'+esc(orderStatus(x))+'</span></td>'+
'<td>'+sideBadge(x.side)+'</td>'+
'<td><div class="mono">$'+fmt(x.target_price,6)+'</div></td>'+
'<td><div class="mono">$'+fmt(latest,6)+'</div><div class="muted">'+(x.latest_price_updated_at?time(x.latest_price_updated_at):'--')+'</div></td>'+
'<td><div>'+esc(cancelReasonLabel(x.cancel_reason||x.status))+'</div></td>'+
'<td><div class="riskline"><span>TP $'+fmt(x.tp1,6)+'</span><span>SL $'+fmt(x.stop_loss,6)+'</span></div></td>'+
'<td>'+time(x.created_at)+'</td>'+
'<td>'+time(ended)+'</td>'+
'<td><div>'+esc(strategyName(x))+'</div><div class="muted">'+esc(x.source_status||'--')+' · '+esc(x.source_action||'')+'</div></td>'+
'<td><button class="row-action" type="button" onclick="deleteOrder('+Number(x.id)+',\''+esc(String(x.symbol||'')).replace(/'/g,'&#39;')+'\')">删除</button></td>'+
'</tr>'}).join('')}
function orderStatus(x){return {filled:'已成交',expired:'已过期',canceled:'已取消',rejected:'已拒绝'}[x.status]||x.status||'--'}
function cancelReasonLabel(r){return {global_risk_rejected:'方向风控拒绝:市场方向、账户风险或仓位集中度不允许开仓',risk_paused_at_touch:'触价时风控暂停:目标价已到但硬风控不允许开仓',touch_critical_risk:'触价时 critical 硬风控:暂停新增仓位',touch_position_multiplier_zero:'触价时仓位系数为 0暂停新增仓位',touch_max_open_positions:'触价时持仓数量超限:暂停新增仓位',touch_same_direction_concentration:'触价时同方向仓位超限:暂停新增同方向仓位',touch_same_sector_concentration:'触价时同板块仓位超限:暂停新增同板块仓位',stop_loss_leverage_risk_exceeded:'止损杠杆风险超限:按当前止损和杠杆亏损过大',portfolio_drawdown_pause:'账户回撤保护:暂停新增仓位',weak_entries_pause:'近期同类弱入场过多:暂停新增仓位',recommendation_invalid:'原机会已失效:机会过期/归档后撤单',too_far_from_entry:'价格远离计划价:继续等待意义不大',expired:'挂单超时:超过有效期未成交',upgraded_to_buy_now:'信号升级为入场窗口:旧挂单已撤销',canceled:'已取消',filled:'已成交',rejected:'已拒绝'}[r]||r||'--'}
function renderOpenPager(){var page=Math.floor(openOffset/LIMIT)+1,totalPages=Math.max(1,Math.ceil(openTotal/LIMIT));$('openPageInfo').textContent='第 '+page+' / '+totalPages+' 页 · 共 '+openTotal+' 条';$('openPager').innerHTML='<button '+(openOffset===0?'disabled':'')+' onclick="loadOpenTrades('+(openOffset-LIMIT)+')">上一页</button><span>第 '+page+' / '+totalPages+' 页</span><button '+((openOffset+LIMIT>=openTotal)?'disabled':'')+' onclick="loadOpenTrades('+(openOffset+LIMIT)+')">下一页</button>'}
async function loadEvents(nextOffset){eventOffset=Math.max(0,nextOffset||0);$('eventRows').innerHTML='<div class="loading">加载中...</div>';try{var sym=$('eventSymbol').value||'';var typ=$('eventType').value||'';var d=await api('/api/paper-trading/events?limit='+EVENT_LIMIT+'&offset='+eventOffset+'&symbol='+encodeURIComponent(sym)+'&event_type='+encodeURIComponent(typ)+tradeQuery());eventTotal=d.total||0;renderEvents(d.items||[]);renderEventPager()}catch(e){$('eventRows').innerHTML='<div class="empty">'+esc(e.message)+'</div>'}}
function eventLabel(t){return {open:'开仓',close:'平仓',trailing_activate:'移动止盈激活',trailing_move:'移动止盈上移'}[t]||t||'动作'}
function eventCls(t){if(t==='open')return'event-open';if(t==='close')return'event-close';if(String(t).indexOf('trailing')===0)return'event-trailing';return''}
function contextPill(text,cls){return '<span class="ctx-pill '+esc(cls||'')+'">'+esc(text)+'</span>'}
function renderStrategyContext(d){var bits=[];var rg=d.market_regime||{};var gr=d.global_risk||{};var sc=d.score_components||{};if(rg.regime){var rc=rg.risk_level==='critical'||rg.risk_level==='high'?'risk':(rg.regime==='altcoin_rotation'?'good':'warn');bits.push(contextPill((rg.label||rg.regime)+' · '+(rg.risk_level||'medium'),rc))}if(gr.decision){bits.push(contextPill('风控 '+gr.decision,(gr.allow_new_entries===false?'risk':'good')))}if(sc.opportunity_score!=null||sc.entry_score!=null||sc.risk_score!=null){bits.push(contextPill('机会 '+fmt(sc.opportunity_score||0,1)+' / 买点 '+fmt(sc.entry_score||0,1)+' / 风险 '+fmt(sc.risk_score||0,1),'warn'))}var dl=d.decision_log||{};if(dl.decision){bits.push(contextPill('决策 '+dl.decision,''))}return bits.length?'<div class="strategy-context">'+bits.join('')+'</div>':''}
function renderEvents(items){if(!items.length){$('eventRows').innerHTML='<div class="empty">暂无操作日志</div>';return}$('eventRows').innerHTML=items.map(function(e){var d=e.detail||{};var detail=[];if(d.notional_usdt)detail.push('名义仓位 '+fmt(d.notional_usdt,0)+'U');if(d.margin_usdt)detail.push('保证金 '+fmt(d.margin_usdt,0)+'U');if(d.leverage)detail.push(fmt(d.leverage,1)+'x');if(d.trailing_stop)detail.push('保护价 $'+fmt(d.trailing_stop,6));if(d.previous_trailing_stop)detail.push('原保护 $'+fmt(d.previous_trailing_stop,6));if(d.distance_pct)detail.push('回撤距离 '+fmt(d.distance_pct,2)+'%');if(d.realized_pnl_usdt!=null)detail.push('实现盈亏 '+fmt(d.realized_pnl_usdt,2)+'U');return '<div class="event">'+
'<div class="event-time">'+time(e.event_time)+'<br><span class="muted">#'+esc(e.trade_id)+'</span></div>'+
'<div><a class="sym-link" href="'+opportunityHref(e)+'">'+esc(e.symbol)+'</a><br><span class="badge '+eventCls(e.event_type)+'">'+esc(eventLabel(e.event_type))+'</span> '+sideBadge(e.side)+'</div>'+
'<div class="event-main"><div class="event-title">'+esc(e.message||eventLabel(e.event_type))+'</div><div class="event-msg">交易状态 '+esc(e.trade_status||'--')+' · 策略 '+esc(strategyName(e))+' · 来源推荐 '+esc(e.recommendation_id||'--')+'</div><div class="event-detail">'+esc(detail.join(' · ')||'无附加参数')+'</div>'+renderStrategyContext(d)+'</div>'+
'<div class="event-price"><b>$'+fmt(e.price,6)+'</b><span>'+pct(e.pnl_pct)+'</span></div>'+
'</div>'}).join('')}
function renderEventPager(){var page=Math.floor(eventOffset/EVENT_LIMIT)+1,totalPages=Math.max(1,Math.ceil(eventTotal/EVENT_LIMIT));$('eventPager').innerHTML='<button '+(eventOffset===0?'disabled':'')+' onclick="loadEvents('+(eventOffset-EVENT_LIMIT)+')">上一页</button><span>第 '+page+' / '+totalPages+' 页 · 共 '+eventTotal+' 条</span><button '+((eventOffset+EVENT_LIMIT>=eventTotal)?'disabled':'')+' onclick="loadEvents('+(eventOffset+EVENT_LIMIT)+')">下一页</button>'}
loadAll();
</script>
{% endblock %}