139 lines
23 KiB
HTML
139 lines
23 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: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:360px 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{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{background:var(--green-light);border-color:rgba(0,180,115,.18);color:var(--green)}.badge.red{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{background:rgba(245,158,11,.12);border-color:rgba(245,158,11,.22);color:#a05a00}.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{color:var(--green)}.kpi b.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(2,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('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 style="height:12px"></div>
|
||
<div class="table-wrap" id="positions"></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 {'&':'&','<':'<','>':'>','"':'"',"'":'''}[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 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||{},risk=o.risk||{},b=(o.balance||{}).usdt||{},pos=o.positions||[];$('kpis').innerHTML=[card('当前账号',a.account_code||'未选择','blue',a.exchange?esc(a.exchange)+' · '+esc(a.market_type):'--'),card('USDT 总额',fmt(b.total,2),'','可用 '+fmt(b.free,2)+' / 占用 '+fmt(b.used,2)),card('持仓数量',pos.length,'','当前未平仓合约'),card('币种权限',risk.symbol_policy==='all'?'全部币种':'白名单 '+(risk.allowed_symbols||[]).length+' 个',risk.symbol_policy==='all'?'green':'blue','留空即不限制')].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||{},errors=o.errors||[],cache=o.exchange_cache||{};if($('exchangeCacheNote'))$('exchangeCacheNote').textContent=cache.loaded?(cache.cached?'交易所数据来自缓存':'交易所数据已刷新'):(cache.reason||'默认显示本地缓存,手动刷新才请求交易所');$('accountInfo').innerHTML=[info('账号状态',a.status||'--'),info('API Key 变量',a.api_key_env||'--'),info('每单保证金上限',fmt(risk.max_order_margin_usdt,2)+' USDT'),info('单币杠杆上限',fmt(risk.max_symbol_leverage,2)+'x'),info('累计杠杆上限',fmt(risk.max_cumulative_leverage,2)+'x'),info('允许交易币种',risk.symbol_policy==='all'?'全部币种':(risk.allowed_symbols||[]).join(', '))].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">为避免页面打开被 Binance API 拖慢,余额、持仓和订单默认不自动刷新。点击“刷新交易所数据”后读取。</div>':'');renderPositions();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>'+badge(x.side_label||sideText(x.side))+'</td><td>'+fmt(x.contracts,6)+'</td><td>'+fmt(x.position_value_usdt,2)+' U</td><td>'+fmt(x.entry_price,6)+'</td><td>'+fmt(x.mark_price,6)+'</td><td>'+fmt(x.unrealized_pnl,4)+'</td><td>'+fmt(lev,2)+'x</td></tr>'});$('positions').innerHTML=table(['币种','方向','数量','仓位价值','开仓价','标记价','未实现盈亏','杠杆'],rows,'当前账号暂无持仓')}
|
||
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>'+badge(sideText(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>'+badge(sideText(x.side))+'</td><td>'+fmt(x.average||x.price,8)+'</td><td>'+fmt(x.filled||x.amount,6)+'</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 %}
|