(function () { var list = document.getElementById('position-live-list'); var recommendList = document.getElementById('recommend-list'); var symInput = document.getElementById('trade-symbol'); var lotsInput = document.getElementById('trade-lots'); var priceInput = document.getElementById('trade-price'); var footer = document.getElementById('trade-footer'); var slInput = document.getElementById('trade-sl'); var tpInput = document.getElementById('trade-tp'); var pollTimer = null; var recommendSource = null; var quoteTimer = null; function runWhenReady(fn) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', fn); } else { fn(); } } function fmtNum(v, digits) { if (v === null || v === undefined) return '--'; return Number(v).toFixed(digits === undefined ? 2 : digits); } function selectedSymbol() { return (symInput && symInput.value || '').trim(); } function refreshQuote() { var sym = selectedSymbol(); var lots = lotsInput ? lotsInput.value : '1'; if (!sym) return; fetch('/api/trade/quote?symbol=' + encodeURIComponent(sym) + '&lots=' + encodeURIComponent(lots)) .then(function (r) { return r.json(); }) .then(function (data) { if (!data.ok) return; if (priceInput && !priceInput.dataset.manual && data.price) { priceInput.value = data.price; } var px = data.price != null ? data.price : '—'; ['px-long', 'px-short'].forEach(function (id) { var el = document.getElementById(id); if (el) el.textContent = px; }); var pl = document.getElementById('pos-long'); var ps = document.getElementById('pos-short'); if (pl) pl.textContent = '≤' + (data.pos_long || 0); if (ps) ps.textContent = '≤' + (data.pos_short || 0); if (footer && data.metrics) { var m = data.metrics; var hint = footer.querySelector('.hint'); var extra = '

' + (data.name || sym) + ' 精度 ' + m.price_precision + ' 位 · 每跳 ' + m.tick_value_total + ' 元(' + lots + ' 手)

'; if (hint) { hint.insertAdjacentHTML('afterend', extra); var olds = footer.querySelectorAll('p:not(.hint):not(.text-loss)'); for (var i = 0; i < olds.length - 1; i++) olds[i].remove(); } } }).catch(function () {}); } function scheduleQuote() { clearTimeout(quoteTimer); quoteTimer = setTimeout(refreshQuote, 400); } function postOrder(offset, direction) { var sym = selectedSymbol(); if (!sym) { alert('请选择品种'); return; } var body = { symbol: sym, offset: offset, direction: direction, lots: parseInt(lotsInput.value, 10) || 1, price: parseFloat(priceInput.value) || 0, stop_loss: slInput ? parseFloat(slInput.value) : null, take_profit: tpInput ? parseFloat(tpInput.value) : null }; fetch('/api/trade/order', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then(function (r) { return r.json(); }).then(function (data) { if (!data.ok) { alert(data.error || '下单失败'); return; } alert('已提交 ' + (data.lots || '') + ' 手'); pollPositions(); refreshQuote(); }); } function buildPosCard(row) { var pnlClass = row.float_pnl > 0 ? 'pnl-pos' : (row.float_pnl < 0 ? 'pnl-neg' : ''); var pnlText = '--'; if (row.float_pnl != null) { var sign = row.float_pnl >= 0 ? '+' : ''; pnlText = sign + fmtNum(row.float_pnl) + '元'; if (row.float_pct != null) pnlText += ' (' + sign + fmtNum(row.float_pct) + '%)'; } var rr = row.rr_ratio != null ? row.rr_ratio + ':1' : '--'; var openT = (row.open_time || '').replace('T', ' ').slice(0, 16); var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空'); var closeBtn = ''; if (row.close_url) { closeBtn = '
' + '
'; } else if (row.can_close) { closeBtn = ''; } return ( '
' + '
' + row.symbol + ' ' + dirBadge + '
' + closeBtn + '
' + '
来源 ' + (row.source_label || row.source) + '
' + '
' + '
' + fmtNum(row.entry_price) + '
' + '
' + (row.stop_loss != null ? fmtNum(row.stop_loss) : '--') + '
' + '
' + (row.take_profit != null ? fmtNum(row.take_profit) : '--') + '
' + '
' + pnlText + '
' + '
' ); } function closePosition(payload) { var price = payload.mark_price; if (!price || price <= 0) { alert('无法获取现价'); return; } if (!confirm('确认以 ' + price + ' 限价平仓 ' + payload.lots + ' 手?')) return; fetch('/api/trading/close', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }).then(function (r) { return r.json(); }).then(function (d) { if (!d.ok) { alert(d.error || '平仓失败'); return; } pollPositions(); }); } function pollPositions() { if (!list) return; fetch('/api/trading/live') .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }) .then(function (data) { var cap = document.getElementById('cap-display'); if (cap && data.capital != null) cap.textContent = Number(data.capital).toFixed(2); var recCap = document.getElementById('rec-capital'); if (recCap && data.capital != null) recCap.textContent = Number(data.capital).toFixed(2); var riskBadge = document.getElementById('risk-badge'); if (riskBadge && data.risk_status) { riskBadge.textContent = data.risk_status.status_label; riskBadge.className = 'badge ' + (data.risk_status.can_trade ? 'profit' : 'loss'); } var ctpBadge = document.getElementById('ctp-badge'); if (ctpBadge && data.ctp_status) { ctpBadge.textContent = data.ctp_status.connected ? 'CTP 已连接' : 'CTP 未连接'; ctpBadge.className = 'badge ' + (data.ctp_status.connected ? 'profit' : 'planned'); } var rows = data.rows || []; if (!rows.length) { list.innerHTML = '
暂无持仓。可在左侧下单,或通过策略交易开仓。
'; return; } list.innerHTML = rows.map(buildPosCard).join(''); list.querySelectorAll('[data-close]').forEach(function (btn) { btn.addEventListener('click', function () { closePosition(JSON.parse(btn.getAttribute('data-close'))); }); }); }) .catch(function () { if (list.innerHTML.indexOf('pos-card') < 0) { list.innerHTML = '
持仓加载失败
'; } }); } function badgeClass(status) { if (status === 'ok') return 'profit'; return 'planned'; } function renderRecommendations(data) { if (!recommendList || !data) return; var recCap = document.getElementById('rec-capital'); if (recCap && data.capital != null) recCap.textContent = Number(data.capital).toFixed(2); var recUpd = document.getElementById('rec-updated'); if (recUpd && data.updated_at) recUpd.textContent = '更新 ' + data.updated_at; var rows = data.rows || []; if (!rows.length) { recommendList.innerHTML = '当前资金下暂无推荐品种'; return; } recommendList.innerHTML = rows.map(function (r) { return ( '' + '' + (r.name || '') + ' ' + (r.ths || '') + '' + '' + (r.exchange || '') + '' + '' + (r.price != null ? r.price : '—') + '' + '' + (r.margin_one_lot != null ? r.margin_one_lot : '—') + '' + '' + (r.min_capital_one_lot != null ? r.min_capital_one_lot : '—') + '' + '' + (r.status_label || '') + '' + '' ); }).join(''); } function connectRecommendStream() { if (recommendSource) { recommendSource.close(); recommendSource = null; } recommendSource = new EventSource('/api/recommend/stream'); recommendSource.addEventListener('recommend', function (ev) { try { renderRecommendations(JSON.parse(ev.data)); } catch (e) { /* ignore */ } }); recommendSource.onerror = function () { if (recommendSource) { recommendSource.close(); recommendSource = null; } setTimeout(connectRecommendStream, 5000); }; } if (symInput) symInput.addEventListener('input', scheduleQuote); if (lotsInput) lotsInput.addEventListener('input', scheduleQuote); if (priceInput) { priceInput.addEventListener('input', function () { priceInput.dataset.manual = '1'; }); } var btnLong = document.getElementById('btn-open-long'); var btnShort = document.getElementById('btn-open-short'); var btnCloseL = document.getElementById('btn-close-long'); var btnCloseS = document.getElementById('btn-close-short'); if (btnLong) btnLong.addEventListener('click', function () { postOrder('open', 'long'); }); if (btnShort) btnShort.addEventListener('click', function () { postOrder('open', 'short'); }); if (btnCloseL) btnCloseL.addEventListener('click', function () { postOrder('close', 'long'); }); if (btnCloseS) btnCloseS.addEventListener('click', function () { postOrder('close', 'short'); }); var btnConnect = document.getElementById('btn-ctp-connect'); if (btnConnect) { btnConnect.addEventListener('click', function () { btnConnect.disabled = true; btnConnect.textContent = '连接中…'; fetch('/api/ctp/connect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }) .then(function (r) { return r.json(); }) .then(function (d) { if (!d.ok) { alert(d.error || '连接失败'); return; } location.reload(); }) .finally(function () { btnConnect.disabled = false; btnConnect.textContent = '连接 CTP'; }); }); } runWhenReady(function () { pollPositions(); connectRecommendStream(); pollTimer = setInterval(pollPositions, 3000); scheduleQuote(); }); })();