176 lines
20 KiB
HTML
176 lines
20 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}AlphaX Agent — 策略交易{% endblock %}
|
|
{% block extra_head_css %}
|
|
<style>
|
|
.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}
|
|
</style>
|
|
{% endblock %}
|
|
{% block content %}
|
|
<div class="shell">
|
|
<div class="page-head">
|
|
<div>
|
|
<h1>策略交易</h1>
|
|
<p>这里展示策略信号进入交易账本后的表现:只有系统把可买信号转入持仓或挂单后,才会进入收益统计。推荐历史和观察池不会直接产生收益率。</p>
|
|
</div>
|
|
<div class="actions">
|
|
<button class="btn" id="sendReportBtn" onclick="sendReport()">发送策略交易报告</button>
|
|
<button class="btn" onclick="loadAll()">刷新</button>
|
|
</div>
|
|
</div>
|
|
<div class="note" id="paperNote">策略交易只统计已经进入交易账本的信号。页面用账户余额、持仓价值、累计杠杆和实际盈亏展示策略表现,不再把观察池或推荐归档当作收益。</div>
|
|
<div class="note" id="reportNote" style="display:none"></div>
|
|
<div class="kpis" id="kpis"><div class="kpi"><span>状态</span><b>加载中</b></div></div>
|
|
<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></tr></thead>
|
|
<tbody id="openRows"><tr><td colspan="13" 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></tr></thead>
|
|
<tbody id="orderRows"><tr><td colspan="10" 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></tr></thead>
|
|
<tbody id="completedTradeRows"><tr><td colspan="13" 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></tr></thead>
|
|
<tbody id="completedOrderRows"><tr><td colspan="10" 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>
|
|
var LIMIT=50,openOffset=0,openTotal=0,EVENT_LIMIT=80,eventOffset=0,eventTotal=0;
|
|
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 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 loadAll(){await Promise.all([loadSummary(),loadOrders(),loadOpenTrades(openOffset),loadCompleted(),loadEvents(eventOffset)])}
|
|
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 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 loadOrders(){$('orderRows').innerHTML='<tr><td colspan="10" class="loading">加载中...</td></tr>';try{var d=await api('/api/paper-trading/orders?limit=50&offset=0&status=pending');renderOrders(d.items||[])}catch(e){$('orderRows').innerHTML='<tr><td colspan="10" class="empty">'+esc(e.message)+'</td></tr>'}}
|
|
function renderOrders(items){if(!items.length){$('orderRows').innerHTML='<tr><td colspan="10" 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><div class="sym">'+esc(x.symbol)+'</div><div class="muted">#'+esc(x.id)+' · Rec '+esc(x.recommendation_id)+'</div></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(x.source_status||'--')+'</div><div class="muted">'+esc(x.source_action||'')+'</div></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>'}
|
|
async function loadOpenTrades(nextOffset){openOffset=Math.max(0,nextOffset||0);$('openRows').innerHTML='<tr><td colspan="13" class="loading">加载中...</td></tr>';try{var d=await api('/api/paper-trading/trades?limit='+LIMIT+'&offset='+openOffset+'&status=open');openTotal=d.total||0;renderTradeRows('openRows',d.items||[],'暂无持仓中的策略交易');renderOpenPager()}catch(e){$('openRows').innerHTML='<tr><td colspan="13" class="empty">'+esc(e.message)+'</td></tr>'}}
|
|
async function loadCompleted(){await Promise.all([loadCompletedTrades(),loadCompletedOrders()])}
|
|
async function loadCompletedTrades(){$('completedTradeRows').innerHTML='<tr><td colspan="13" class="loading">加载中...</td></tr>';try{var d=await api('/api/paper-trading/trades?limit=80&offset=0&status=closed');renderTradeRows('completedTradeRows',d.items||[],'暂无已平仓策略交易')}catch(e){$('completedTradeRows').innerHTML='<tr><td colspan="13" class="empty">'+esc(e.message)+'</td></tr>'}}
|
|
async function loadCompletedOrders(){$('completedOrderRows').innerHTML='<tr><td colspan="10" 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)}));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="10" class="empty">'+esc(e.message)+'</td></tr>'}}
|
|
function renderTradeRows(targetId,items,emptyText){if(!items.length){$(targetId).innerHTML='<tr><td colspan="13" 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><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>'+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(x.source_status||'--')+'</div><div class="muted">'+esc(x.strategy_version||'')+'</div></td>'+
|
|
'</tr>'}).join('')}
|
|
function renderCompletedOrders(items){if(!items.length){$('completedOrderRows').innerHTML='<tr><td colspan="10" 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><div class="sym">'+esc(x.symbol)+'</div><div class="muted">#'+esc(x.id)+' · Rec '+esc(x.recommendation_id)+'</div></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(x.cancel_reason||x.source_status||'--')+'</div><div class="muted">'+esc(x.source_action||'')+'</div></td>'+
|
|
'</tr>'}).join('')}
|
|
function orderStatus(x){return {filled:'已成交',expired:'已过期',canceled:'已取消',rejected:'已拒绝'}[x.status]||x.status||'--'}
|
|
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));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 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><div class="sym">'+esc(e.symbol)+'</div><span class="badge '+eventCls(e.event_type)+'">'+esc(eventLabel(e.event_type))+'</span></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(e.recommendation_id||'--')+'</div><div class="event-detail">'+esc(detail.join(' · ')||'无附加参数')+'</div></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 %}
|