alphax/static/live_trading.html
2026-06-08 00:41:46 +08:00

159 lines
26 KiB
HTML
Raw Permalink 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:880px}.actions{display:flex;gap:8px;align-items:flex-end;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);outline:none}.btn{cursor:pointer}.btn.primary{background:var(--primary);color:var(--on-primary);border-color:var(--primary)}.btn.danger{background:var(--red-light);color:var(--red);border-color:rgba(229,62,62,.22)}.btn:disabled{opacity:.55;cursor:default}.layout{display:grid;grid-template-columns:320px minmax(0,1fr);gap:14px;align-items:start}.panel{border:1px solid var(--hairline-soft);background:var(--canvas);border-radius:var(--radius-md);overflow:hidden;margin-bottom: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}.panel-body{padding:14px}.account-list{display:grid;gap:8px}.account{border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);padding:12px;cursor:pointer;transition:.16s ease}.account:hover,.account.active{border-color:rgba(66,98,255,.35);box-shadow:0 12px 28px rgba(15,23,42,.08);background:var(--canvas)}.account-head{display:flex;justify-content:space-between;gap:10px;align-items:flex-start}.account b{font-size:13px;color:var(--ink);font-weight:950}.account p{margin-top:6px;color:var(--stone);font-size:11px;line-height:1.45}.meta{display:flex;gap:6px;flex-wrap:wrap;margin-top:9px}.badge,.side-pill,.result-pill{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.green,.result-pill.profit{background:var(--green-light);border-color:rgba(0,180,115,.18);color:var(--green)}.badge.red,.result-pill.loss{background:var(--red-light);border-color:rgba(229,62,62,.18);color:var(--red)}.badge.blue{background:rgba(66,98,255,.08);border-color:rgba(66,98,255,.18);color:var(--blue)}.badge.warn,.result-pill.flat{background:rgba(245,158,11,.12);border-color:rgba(245,158,11,.22);color:#a05a00}.side-pill.long{background:rgba(0,180,115,.1);border-color:rgba(0,180,115,.2);color:var(--green)}.side-pill.short{background:rgba(229,62,62,.1);border-color:rgba(229,62,62,.22);color:var(--red)}.kpis{display:grid;grid-template-columns:repeat(4,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,.money.green{color:var(--green)}.kpi b.red,.money.red{color:var(--red)}.kpi b.blue{color:var(--blue)}.tabs{display:flex;gap:8px;margin:0 0 14px;padding:6px;border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);overflow:auto}.tab{height:36px;border:0;background:transparent;border-radius:10px;padding:0 12px;font-size:12px;font-weight:950;color:var(--stone);cursor:pointer;white-space:nowrap}.tab.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}.form{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.field{display:flex;flex-direction:column;gap:6px;min-width:0}.field.wide{grid-column:1/-1}.field span{font-size:11px;color:var(--stone);font-weight:900}.field small{margin-top:-2px;color:var(--stone);font-size:10px;line-height:1.35}.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.info{border:1px solid var(--hairline-soft);background:var(--surface);border-radius:var(--radius-md);padding:12px}.info span{display:block;font-size:11px;font-weight:900;color:var(--stone)}.info b{display:block;margin-top:6px;font-size:14px;color:var(--ink);font-weight:950;word-break:break-word}.table-wrap{overflow:auto;border:1px solid var(--hairline-soft);border-radius:var(--radius-md)}table{width:100%;border-collapse:collapse;background:var(--canvas)}th,td{padding:11px 12px;border-bottom:1px solid var(--hairline-soft);text-align:left;font-size:12px;white-space:nowrap}th{color:var(--stone);font-weight:950;background:var(--surface)}td{color:var(--slate);font-weight:800}tr:last-child td{border-bottom:0}.empty,.loading{text-align:center;padding:30px 14px;color:var(--stone);font-size:13px}.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}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-weight:850}.maint{border-top:1px dashed var(--hairline-strong);margin-top:14px;padding-top:14px}@media(max-width:1100px){.layout{grid-template-columns:1fr}.kpis{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:700px){.shell{width:min(100% - 24px,1280px)}.page-head h1{font-size:22px}.kpis,.grid,.form{grid-template-columns:1fr}}
</style>
{% endblock %}
{% block content %}
<div class="shell">
<div class="page-head">
<div>
<h1>实盘控制台</h1>
<p>以交易账号为核心管理 API 环境变量、资金余额、仓位、订单和账号级风控。币种限制留空时表示不限制,策略同步到实盘前会按当前账号配置逐一校验。</p>
</div>
<div class="actions">
<button class="btn" onclick="resetForm()">新增账号</button>
<button class="btn primary" onclick="loadAll()">刷新</button>
</div>
</div>
<div class="layout">
<aside>
<section class="panel">
<div class="panel-head"><div><div class="panel-title">交易账号</div><div class="panel-note">选择一个账号查看资金与订单</div></div></div>
<div class="panel-body"><div class="account-list" id="accounts"><div class="loading">加载中...</div></div></div>
</section>
</aside>
<main>
<div class="kpis" id="kpis"><div class="kpi"><span>账号</span><b>加载中</b></div></div>
<div class="tabs">
<button class="tab active" onclick="showTab('overview',this)">账户表现</button>
<button class="tab" onclick="showTab('positions',this)">当前持仓</button>
<button class="tab" onclick="showTab('history',this)">历史仓位</button>
<button class="tab" onclick="showTab('orders',this)">订单</button>
<button class="tab" onclick="showTab('config',this)">账号设置</button>
<button class="tab" onclick="showTab('maintenance',this)">维护</button>
</div>
<section class="tab-panel active" id="overviewPane">
<section class="panel">
<div class="panel-head"><div><div class="panel-title">账号权益表现</div><div class="panel-note" id="exchangeCacheNote">默认读取数据库快照,后台定时同步交易所数据</div></div><button class="btn" id="refreshExchangeBtn" onclick="refreshExchangeData()">立即同步</button></div>
<div class="panel-body">
<div class="grid" id="accountInfo"></div>
</div>
</section>
</section>
<section class="tab-panel" id="positionsPane">
<section class="panel">
<div class="panel-head"><div><div class="panel-title">当前持仓</div><div class="panel-note">每个仓位的方向、保证金、浮盈亏和收益率</div></div></div>
<div class="panel-body"><div class="table-wrap" id="positions"></div></div>
</section>
</section>
<section class="tab-panel" id="historyPane">
<section class="panel">
<div class="panel-head"><div><div class="panel-title">历史仓位</div><div class="panel-note">基于交易所订单历史中可识别的已完成记录</div></div></div>
<div class="panel-body"><div class="table-wrap" id="historicalPositions"></div></div>
</section>
</section>
<section class="tab-panel" id="ordersPane">
<section class="panel">
<div class="panel-head"><div><div class="panel-title">当前挂单</div><div class="panel-note">交易所未完成订单</div></div></div>
<div class="panel-body"><div class="table-wrap" id="openOrders"></div></div>
</section>
<section class="panel">
<div class="panel-head"><div><div class="panel-title">订单历史</div><div class="panel-note">最近交易所订单与系统记录</div></div></div>
<div class="panel-body"><div class="table-wrap" id="orderHistory"></div></div>
</section>
</section>
<section class="tab-panel" id="configPane">
<section class="panel">
<div class="panel-head">
<div><div class="panel-title">账号配置</div><div class="panel-note">API Key、杠杆、保证金和币种范围按账号独立保存</div></div>
<div class="actions">
<button class="btn danger" onclick="deleteAccount()">删除账号</button>
<button class="btn primary" id="saveAccountBtn" onclick="saveAccount()">保存账号配置</button>
</div>
</div>
<div class="panel-body">
<div class="note">API Key 和 Secret 只保存环境变量名。允许交易币种留空时,表示该账号允许全部币种;填写后只允许列表内币种。</div>
<div class="form">
<label class="field"><span>账号编码</span><input class="input" id="accountCode" value="binance_um_futures" placeholder="例如 binance_um_futures"></label>
<label class="field"><span>账号状态</span><select class="select" id="accountStatus"><option value="enabled">启用</option><option value="disabled">禁用</option></select></label>
<label class="field"><span>交易所</span><select class="select" id="accountExchange"><option value="binance">Binance</option></select></label>
<label class="field"><span>账户类型</span><select class="select" id="accountMarket"><option value="um_futures">U 本位合约</option><option value="spot">现货</option></select></label>
<label class="field wide"><span>API Key 环境变量名</span><input class="input" id="apiKeyEnv" value="ALPHAX_BINANCE_DEMO_API_KEY" placeholder="例如 ALPHAX_BINANCE_API_KEY"><small>页面只保存变量名,不保存真实 Key。</small></label>
<label class="field wide"><span>API Secret 环境变量名</span><input class="input" id="apiSecretEnv" value="ALPHAX_BINANCE_DEMO_API_SECRET" placeholder="例如 ALPHAX_BINANCE_API_SECRET"><small>Secret 从后端环境变量读取,不在页面展示明文。</small></label>
<label class="field"><span>每单保证金上限</span><input class="input" id="maxOrderMargin" value="10" placeholder="USDT"><small>单笔最多占用多少保证金。</small></label>
<label class="field"><span>单币杠杆上限</span><input class="input" id="maxSymbolLeverage" value="1" placeholder="倍数"><small>单个币种允许的最大杠杆。</small></label>
<label class="field"><span>持仓累计杠杆上限</span><input class="input" id="maxCumulativeLeverage" value="1" placeholder="倍数"><small>账户整体风险敞口上限。</small></label>
<label class="field"><span>允许交易币种</span><input class="input" id="allowedSymbols" value="" placeholder="留空=全部,或 BTC/USDT,DOGE/USDT"><small>空值表示全部币种可以交易。</small></label>
</div>
</div>
</section>
</section>
<section class="tab-panel" id="maintenancePane">
<section class="panel">
<div class="panel-head"><div><div class="panel-title">接口验收</div><div class="panel-note">仅用于接入或排障时手动运行</div></div></div>
<div class="panel-body">
<div class="note">这不是日常看板。只在新增账号、换 Key、换 endpoint 或排查交易所接口时运行,会真实调用当前账号配置的 API。</div>
<div class="actions">
<label class="field"><span>验收币种</span><input class="input" id="smokeSymbol" value="BTC/USDT" placeholder="BTC/USDT" style="width:140px"></label>
<label class="field"><span>测试保证金</span><input class="input" id="smokeMargin" value="10" placeholder="USDT" style="width:140px"></label>
<button class="btn danger" id="smokeBtn" onclick="runSmoke()">开始验收</button>
</div>
<div class="maint" id="events"></div>
</div>
</section>
</section>
</main>
</div>
</div>
{% endblock %}
{% block extra_script %}
<script>
var state={summary:{},accounts:[],events:[],overview:null,selectedId:0};
function $(id){return document.getElementById(id)}
function esc(s){return String(s==null?'':s).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]})}
function fmt(n,d){n=Number(n||0);return n.toLocaleString(undefined,{maximumFractionDigits:d==null?4:d})}
function time(s){return esc(String(s||'').replace('T',' ').slice(0,19))}
function badge(v){var cls=v==='enabled'||v==='ok'||v==='closed'?'green':(v==='disabled'||v==='error'?'red':(v==='open'||v==='exchange_api'?'blue':'warn'));return '<span class="badge '+cls+'">'+esc(v||'--')+'</span>'}
function sideText(v){v=String(v||'').toLowerCase();if(v==='long'||v==='buy')return '多';if(v==='short'||v==='sell')return '空';return v||'--'}
function sidePill(v){var s=String(v||'').toLowerCase(),isShort=s==='short'||s==='sell'||s==='空';return '<span class="side-pill '+(isShort?'short':'long')+'">'+(isShort?'空':'多')+'</span>'}
function pnlClass(n){n=Number(n||0);return n>0?'green':n<0?'red':''}
function signed(n,d,suffix){n=Number(n||0);var txt=(n>0?'+':'')+fmt(n,d==null?2:d)+(suffix||'');return '<span class="money '+pnlClass(n)+'">'+txt+'</span>'}
function selectedAccountObj(){return (state.accounts||[]).find(function(x){return Number(x.id)===Number(state.selectedId)})||{}}
function showTab(id,btn){document.querySelectorAll('.tab').forEach(function(x){x.classList.remove('active')});document.querySelectorAll('.tab-panel').forEach(function(x){x.classList.remove('active')});btn.classList.add('active');$(id+'Pane').classList.add('active')}
function card(k,v,cls,s){return '<div class="kpi"><span>'+esc(k)+'</span><b class="'+(cls||'')+'">'+esc(v)+'</b><small>'+esc(s||'')+'</small></div>'}
function renderAccounts(){var rows=state.accounts||[];if(!rows.length){$('accounts').innerHTML='<div class="empty">暂无账号配置</div>';return}$('accounts').innerHTML=rows.map(function(x){var r=x.risk_config||{},allowed=r.allowed_symbols||[];return '<div class="account '+(Number(x.id)===Number(state.selectedId)?'active':'')+'" onclick="selectAccount('+esc(x.id)+')"><div class="account-head"><b>'+esc(x.account_code)+'</b>'+badge(x.status)+'</div><p>'+esc(x.exchange)+' · '+esc(x.market_type)+' · '+esc(x.api_key_env||'--')+'</p><div class="meta"><span class="badge">保证金 '+fmt(r.max_order_margin_usdt||0,2)+'U</span><span class="badge">杠杆 '+fmt(r.max_symbol_leverage||1,2)+'x</span><span class="badge '+(allowed.length?'blue':'green')+'">'+(allowed.length?'限制 '+allowed.length+' 个币':'全部币种')+'</span></div></div>'}).join('')}
function renderKpis(){var a=selectedAccountObj(),o=state.overview||{},p=o.performance||{},pos=o.positions||[];$('kpis').innerHTML=[card('当前净值',fmt(p.equity_usdt,2)+'U','blue',a.account_code||'未选择账号'),card('总盈亏',(Number(p.total_pnl_usdt||0)>0?'+':'')+fmt(p.total_pnl_usdt,2)+'U',pnlClass(p.total_pnl_usdt),'按首次同步净值计算'),card('收益率',(Number(p.return_pct||0)>0?'+':'')+fmt(p.return_pct,2)+'%',pnlClass(p.return_pct),'基准 '+fmt(p.baseline_equity_usdt,2)+'U'),card('当前/最大回撤',fmt(p.current_drawdown_pct,2)+'% / '+fmt(p.max_drawdown_pct,2)+'%','red','持仓 '+pos.length+' 个 · 浮盈 '+(Number(p.unrealized_pnl_usdt||0)>0?'+':'')+fmt(p.unrealized_pnl_usdt,2)+'U')].join('')}
function fillAccountForm(id){var x=(state.accounts||[]).find(function(a){return Number(a.id)===Number(id)})||{},r=x.risk_config||{};$('accountCode').value=x.account_code||'';$('accountStatus').value=x.status||'disabled';$('accountExchange').value=x.exchange||'binance';$('accountMarket').value=x.market_type||'um_futures';$('apiKeyEnv').value=x.api_key_env||'ALPHAX_BINANCE_API_KEY';$('apiSecretEnv').value=x.api_secret_env||'ALPHAX_BINANCE_API_SECRET';$('maxOrderMargin').value=r.max_order_margin_usdt||10;$('maxSymbolLeverage').value=r.max_symbol_leverage||1;$('maxCumulativeLeverage').value=r.max_cumulative_leverage||1;$('allowedSymbols').value=(r.allowed_symbols||[]).join(',');if($('saveAccountBtn'))$('saveAccountBtn').textContent=Number(id)>0?'保存修改':'新增账号'}
function resetForm(){state.selectedId=0;state.overview=null;renderAccounts();fillAccountForm(0);renderKpis();renderOverview();document.querySelectorAll('.tab').forEach(function(x){x.classList.remove('active')});document.querySelectorAll('.tab-panel').forEach(function(x){x.classList.remove('active')});document.querySelector('.tab[onclick*="config"]').classList.add('active');$('configPane').classList.add('active')}
function info(k,v){return '<div class="info"><span>'+esc(k)+'</span><b>'+esc(v)+'</b></div>'}
function renderOverview(){var a=selectedAccountObj(),o=state.overview||{},risk=o.risk||{},p=o.performance||{},b=(o.balance||{}).usdt||{},errors=o.errors||[],cache=o.exchange_cache||{},syncText=cache.synced_at?(' · 同步 '+time(cache.synced_at)):'';
if($('exchangeCacheNote'))$('exchangeCacheNote').textContent=cache.loaded?(cache.source==='database'?'读取数据库快照'+syncText:'交易所数据已同步'+syncText):(cache.reason||'等待后台同步生成账户快照');
$('accountInfo').innerHTML=[info('可用资金',fmt(b.free,2)+' USDT'),info('保证金占用',fmt(b.used||p.used_margin_usdt,2)+' USDT'),info('持仓名义价值',fmt(p.open_position_value_usdt,2)+' USDT'),info('浮动盈亏',(Number(p.unrealized_pnl_usdt||0)>0?'+':'')+fmt(p.unrealized_pnl_usdt,2)+' USDT'),info('权益峰值',fmt(p.peak_equity_usdt,2)+' USDT'),info('统计口径',p.basis||'等待首次同步')].join('')+(errors.length?'<div class="note" style="grid-column:1/-1">账户数据读取异常:'+esc(errors[0])+'</div>':'')+(!cache.loaded&&a.status==='enabled'?'<div class="note" style="grid-column:1/-1">后台实盘同步还没有生成该账号的账户快照。可以等待调度器下一轮同步,或点击“立即同步”。</div>':'');renderPositions();renderHistoricalPositions();renderOpenOrders();renderOrderHistory();renderEvents()}
function table(headers,rows,empty){if(!rows.length)return '<div class="empty">'+esc(empty||'暂无数据')+'</div>';return '<table><thead><tr>'+headers.map(function(h){return '<th>'+esc(h)+'</th>'}).join('')+'</tr></thead><tbody>'+rows.join('')+'</tbody></table>'}
function renderPositions(){var rows=(state.overview?.positions||[]).map(function(x){var lev=Number(x.leverage||0);return '<tr><td>'+esc(x.symbol)+'</td><td>'+sidePill(x.side)+'</td><td>'+fmt(x.position_value_usdt,2)+' U</td><td>'+fmt(x.margin_usdt,2)+' U</td><td>'+fmt(x.entry_price,6)+'</td><td>'+fmt(x.mark_price,6)+'</td><td>'+signed(x.unrealized_pnl,4,' U')+'</td><td>'+signed(x.pnl_pct,2,'%')+'</td><td>'+fmt(lev,2)+'x</td></tr>'});$('positions').innerHTML=table(['币种','方向','仓位价值','保证金','开仓价','标记价','浮盈亏','收益率','杠杆'],rows,'当前账号暂无持仓')}
function renderHistoricalPositions(){var rows=(state.overview?.historical_positions||[]).map(function(x){var cls=Number(x.realized_pnl||0)>0?'profit':Number(x.realized_pnl||0)<0?'loss':'flat';return '<tr><td>'+time(x.time)+'</td><td>'+esc(x.symbol)+'</td><td>'+sidePill(x.side)+'</td><td>'+fmt(x.entry_price,8)+'</td><td>'+fmt(x.exit_price||x.price,8)+'</td><td>'+fmt(x.amount,6)+'</td><td>'+signed(x.realized_pnl,4,' U')+'</td><td>'+signed(x.realized_pnl_pct,2,'%')+'</td><td><span class="result-pill '+cls+'">'+esc(x.result||'未知')+'</span></td></tr>'});$('historicalPositions').innerHTML=table(['时间','币种','方向','开仓价','平仓价','数量','已实现盈亏','收益率','结果'],rows,'暂无可识别的历史仓位盈亏。若交易所订单未返回 realizedPnl只能在订单历史查看成交记录。')}
function renderOpenOrders(){var rows=(state.overview?.open_orders||[]).map(function(x){return '<tr><td>'+time(x.timestamp)+'</td><td>'+esc(x.symbol)+'</td><td>'+esc(x.type)+'</td><td>'+sidePill(x.side)+'</td><td>'+fmt(x.price,8)+'</td><td>'+fmt(x.amount,6)+'</td><td>'+badge(x.status)+'</td></tr>'});$('openOrders').innerHTML=table(['时间','币种','类型','方向','价格','数量','状态'],rows,'当前账号暂无挂单')}
function renderOrderHistory(){var orders=(state.overview?.order_history||[]).map(function(x){return '<tr><td>'+time(x.timestamp)+'</td><td>'+esc(x.symbol)+'</td><td>'+esc(x.type)+'</td><td>'+sidePill(x.side)+'</td><td>'+fmt(x.average||x.price,8)+'</td><td>'+fmt(x.filled||x.amount,6)+'</td><td>'+signed(x.realized_pnl,4,' U')+'</td><td>'+badge(x.status)+'</td></tr>'});$('orderHistory').innerHTML=table(['时间','币种','类型','方向','均价/价格','成交/数量','已实现盈亏','状态'],orders,'当前账号暂无订单历史')}
function renderEvents(){var rows=(state.events||[]).slice(0,20).map(function(e){return '<tr><td>'+time(e.event_time)+'</td><td>'+esc(e.event_type)+'</td><td>'+badge(e.status)+'</td><td>'+esc(e.message||'--')+'</td></tr>'});$('events').innerHTML=table(['时间','事件','状态','说明'],rows,'暂无维护日志')}
function renderAll(){if(!state.selectedId && state.accounts[0])state.selectedId=state.accounts[0].id;renderAccounts();fillAccountForm(state.selectedId);renderKpis();renderOverview()}
async function selectAccount(id){state.selectedId=Number(id);renderAccounts();fillAccountForm(id);await loadOverview()}
async function saveAccount(){var allowed=String($('allowedSymbols').value||'').split(',').map(function(x){return x.trim().toUpperCase()}).filter(Boolean);var lev=Number($('maxSymbolLeverage').value||1),margin=Number($('maxOrderMargin').value||10);var body={account_code:$('accountCode').value,exchange:$('accountExchange').value,market_type:$('accountMarket').value,status:$('accountStatus').value,api_key_env:$('apiKeyEnv').value,api_secret_env:$('apiSecretEnv').value,testnet:true,permissions:{read:true,trade:true},risk_config:{sandbox_mode:'demo',max_order_margin_usdt:margin,max_order_notional_usdt:margin*Math.max(1,lev),max_symbol_leverage:lev,max_cumulative_leverage:Number($('maxCumulativeLeverage').value||1),allowed_symbols:allowed}};var editing=Number(state.selectedId)>0,url=editing?('/api/live-trading/accounts/'+state.selectedId):'/api/live-trading/accounts',method=editing?'PUT':'POST';var resp=await fetch(url,{method:method,headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});var saved=await resp.json().catch(function(){return {}});if(!resp.ok){alert((editing?'修改':'新增')+'账号失败:'+(saved.detail||'unknown_error'));return}state.selectedId=saved.id||state.selectedId;await loadAll()}
async function deleteAccount(){if(!state.selectedId){alert('请先选择要删除的账号');return}var acct=selectedAccountObj();if(!confirm('确认删除账号配置:'+(acct.account_code||state.selectedId)+'\\n历史订单和调用日志会保留。'))return;var resp=await fetch('/api/live-trading/accounts/'+state.selectedId,{method:'DELETE'});var d=await resp.json().catch(function(){return {}});if(!resp.ok){alert('删除失败:'+(d.detail||'unknown_error'));return}state.selectedId=0;await loadAll()}
async function runSmoke(){if(!state.selectedId){alert('请先选择账号');return}var acct=selectedAccountObj(),risk=acct.risk_config||{},lev=Number(risk.max_symbol_leverage||1),margin=Number($('smokeMargin').value||risk.max_order_margin_usdt||10),notional=margin*Math.max(1,lev);var btn=$('smokeBtn');btn.disabled=true;btn.textContent='验收中...';try{var resp=await fetch('/api/live-trading/smoke/binance',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({account_id:state.selectedId,symbol:$('smokeSymbol').value,notional_usdt:notional,leverage:lev})});var d=await resp.json();if(!resp.ok||d.detail){alert('接口验收失败:'+(d.detail||JSON.stringify(d).slice(0,220)))}await loadAll()}catch(e){alert('接口验收请求失败:'+e.message)}finally{btn.disabled=false;btn.textContent='开始验收'}}
async function loadOverview(refresh){if(!state.selectedId){state.overview=null;renderAll();return}state.overview=await (await fetch('/api/live-trading/accounts/'+state.selectedId+'/overview?refresh='+(refresh?1:0)+'&_ts='+Date.now(),{cache:'no-store'})).json();renderAll()}
async function refreshExchangeData(){if(!state.selectedId){alert('请先选择账号');return}var btn=$('refreshExchangeBtn');if(btn){btn.disabled=true;btn.textContent='同步中...'}try{await loadOverview(true)}finally{if(btn){btn.disabled=false;btn.textContent='立即同步'}}}
async function loadAll(){try{var s=await (await fetch('/api/live-trading/summary?_ts='+Date.now(),{cache:'no-store'})).json();var a=await (await fetch('/api/live-trading/accounts?_ts='+Date.now(),{cache:'no-store'})).json();var e=await (await fetch('/api/live-trading/events?limit=100&_ts='+Date.now(),{cache:'no-store'})).json();state.summary=s;state.accounts=a.items||[];state.events=e.items||[];if(!state.selectedId&&state.accounts[0])state.selectedId=state.accounts[0].id;await loadOverview()}catch(e){$('kpis').innerHTML='<div class="kpi"><span>状态</span><b>加载失败</b><small>'+esc(e.message)+'</small></div>'}}
loadAll();
</script>
{% endblock %}