278 lines
43 KiB
HTML
278 lines
43 KiB
HTML
{% 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-completed" type="button" onclick="setTradeTab('completed')" 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-completed" role="tabpanel" aria-labelledby="tab-completed">
|
|
<section class="panel">
|
|
<div class="panel-head"><div><div class="panel-title">已完成</div><div class="panel-note" id="completedInfo">已平仓交易与已结束挂单</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="completedTradeRows"><tr><td colspan="15" class="loading">加载中...</td></tr></tbody>
|
|
</table>
|
|
</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="completedOrderRows"><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 {'&':'&','<':'<','>':'>','"':'"',"'":'''}[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','completed','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),loadCompleted(),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&¤t===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,''')+'\')">'+
|
|
'<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,''')+'\')">删除</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)||({main_composite_v1:'综合确认策略',box_retest_1h_v1:'1H箱体突破回踩',box_retest_4h_v1:'4H箱体突破回踩',volume_ignition_1h_v1:'1H放量突破启动',compression_breakout_4h_v1:'4H压缩蓄力突破',intraday_momentum_15m_v1:'15m日内动量延续',breakdown_retest_short_1h_v1:'1H破位反抽做空'}[(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 loadCompleted(){await Promise.all([loadCompletedTrades(),loadCompletedOrders()])}
|
|
async function loadCompletedTrades(){$('completedTradeRows').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('completedTradeRows',d.items||[],'暂无已平仓策略交易')}catch(e){$('completedTradeRows').innerHTML='<tr><td colspan="15" class="empty">'+esc(e.message)+'</td></tr>'}}
|
|
async function loadCompletedOrders(){$('completedOrderRows').innerHTML='<tr><td colspan="11" class="loading">加载中...</td></tr>';try{var sets=await Promise.all(['filled','expired','canceled','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))});renderCompletedOrders(items)}catch(e){$('completedOrderRows').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,''')+'\',\''+esc(String(x.status||'')).replace(/'/g,''')+'\')">删除</button></td>'+
|
|
'</tr>'}).join('')}
|
|
function renderCompletedOrders(items){if(!items.length){$('completedOrderRows').innerHTML='<tr><td colspan="11" class="empty">暂无已结束策略挂单</td></tr>';return}$('completedOrderRows').innerHTML=items.map(function(x){var latest=x.latest_price||x.current_price_at_create||0,dist=Number(x.distance_to_target_pct||0),ended=x.filled_at||x.canceled_at||x.updated_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><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(ended)+'</td>'+
|
|
'<td><div>'+esc(strategyName(x))+'</div><div class="muted">'+esc(cancelReasonLabel(x.cancel_reason||x.status))+'</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,''')+'\')">删除</button></td>'+
|
|
'</tr>'}).join('')}
|
|
function orderStatus(x){return {filled:'已成交',expired:'已过期',canceled:'已取消',rejected:'已拒绝'}[x.status]||x.status||'--'}
|
|
function cancelReasonLabel(r){return {global_risk_rejected:'全局风控拒绝:市场/账户风险过高,未转持仓',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 %}
|