(function () { var sizingMode = window.TRADE_SIZING_MODE || 'risk'; var list = document.getElementById('position-live-list'); var recommendList = document.getElementById('recommend-list'); var symInput = document.getElementById('trade-symbol'); var dirSelect = document.getElementById('trade-direction'); var lotsInput = document.getElementById('trade-lots'); var lotsCalc = document.getElementById('trade-lots-calc'); var priceInput = document.getElementById('trade-price'); var slInput = document.getElementById('trade-sl'); var tpInput = document.getElementById('trade-tp'); var marketHint = document.getElementById('market-hint'); var metricsHint = document.getElementById('trade-metrics-hint'); var pollTimer = null; var recommendSource = null; var quoteTimer = null; var calcTimer = null; var lastQuotePrice = null; var priceType = 'limit'; var lastCtpReconnectAt = 0; var ctpReconnecting = false; 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 isRiskMode() { return sizingMode === 'risk'; } function effectiveLots() { if (isRiskMode()) { var v = parseInt(lotsCalc && lotsCalc.value, 10); return v > 0 ? v : 0; } return parseInt(lotsInput && lotsInput.value, 10) || 1; } function entryPrice() { if (priceType === 'market') return lastQuotePrice; return parseFloat(priceInput && priceInput.value) || 0; } function setPriceType(type) { priceType = type === 'market' ? 'market' : 'limit'; document.querySelectorAll('.price-tab').forEach(function (btn) { btn.classList.toggle('active', btn.getAttribute('data-type') === priceType); }); if (priceInput) { priceInput.disabled = priceType === 'market'; if (priceType === 'market' && lastQuotePrice) priceInput.value = lastQuotePrice; } if (marketHint) marketHint.hidden = priceType !== 'market'; } function updateCtpBadge(connected) { var ctpBadge = document.getElementById('ctp-badge'); var btnConnect = document.getElementById('btn-ctp-connect'); if (ctpBadge) { ctpBadge.textContent = connected ? 'CTP 已连接' : 'CTP 未连接'; ctpBadge.className = 'badge ' + (connected ? 'profit' : 'planned'); } if (btnConnect && connected) { btnConnect.textContent = '重连 CTP'; } } function refreshQuote() { var sym = selectedSymbol(); var lots = isRiskMode() ? (effectiveLots() || 1) : (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; lastQuotePrice = data.price; if (priceType === 'market' && priceInput && data.price) { priceInput.value = data.price; } else if (priceInput && !priceInput.dataset.manual && data.price) { priceInput.value = data.price; } if (metricsHint && data.metrics) { var m = data.metrics; metricsHint.innerHTML = '' + (data.name || sym) + ' 精度 ' + m.price_precision + ' 位 · 每跳 ' + m.tick_value_total + ' 元(' + lots + ' 手)'; } scheduleAutoCalc(); }).catch(function () {}); } function scheduleQuote() { clearTimeout(quoteTimer); quoteTimer = setTimeout(refreshQuote, 400); } function scheduleAutoCalc() { if (!isRiskMode()) return; clearTimeout(calcTimer); calcTimer = setTimeout(autoCalcLots, 450); } function autoCalcLots() { if (!isRiskMode() || !lotsCalc) return; var sym = selectedSymbol(); var entry = entryPrice() || parseFloat(priceInput && priceInput.value) || 0; var sl = parseFloat(slInput && slInput.value) || 0; if (!sym || !entry || !sl) { lotsCalc.value = ''; lotsCalc.placeholder = '填写止损后自动计算'; return; } lotsCalc.placeholder = '计算中…'; fetch('/api/trade/preview', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ symbol: sym, direction: dirSelect ? dirSelect.value : 'long', entry: entry, price: entry, stop_loss: sl, take_profit: parseFloat(tpInput && tpInput.value) || 0 }) }).then(function (r) { return r.json(); }).then(function (data) { if (!data.ok) { lotsCalc.value = ''; lotsCalc.placeholder = data.error || '无法计算'; return; } lotsCalc.value = data.lots; lotsCalc.placeholder = '填写止损后自动计算'; scheduleQuote(); }).catch(function () { lotsCalc.placeholder = '计算失败'; }); } function tryAutoCtpReconnect() { if (ctpReconnecting) return; var now = Date.now(); if (now - lastCtpReconnectAt < 30000) return; lastCtpReconnectAt = now; ctpReconnecting = true; fetch('/api/ctp/connect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ auto: true }) }) .then(function (r) { return r.json(); }) .then(function (d) { if (d.ok && d.status && d.status.connected) { updateCtpBadge(true); var avail = document.getElementById('avail-display'); if (avail && d.account && d.account.available != null) { avail.textContent = Number(d.account.available).toFixed(2); } pollPositions(); } }) .catch(function () { /* ignore */ }) .finally(function () { ctpReconnecting = false; }); } function postOrder(offset) { var sym = selectedSymbol(); if (!sym) { alert('请选择品种'); return; } var direction = dirSelect ? dirSelect.value : 'long'; var price = entryPrice(); if (!price || price <= 0) { alert('无法获取有效价格,请先填写或刷新行情'); return; } var lots = effectiveLots(); if (offset === 'open') { if (isRiskMode() && lots <= 0) { alert('请填写止损,系统将自动计算手数'); return; } if (!isRiskMode() && lots <= 0) { alert('请填写手数'); return; } } else { lots = parseInt(lotsInput && lotsInput.value, 10) || 1; } var body = { symbol: sym, offset: offset, direction: direction, lots: lots, price: price, order_type: priceType, stop_loss: slInput && slInput.value ? parseFloat(slInput.value) : null, take_profit: tpInput && tpInput.value ? 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((offset === 'open' ? '开仓' : '平仓') + '已提交 ' + (data.lots || lots) + ' 手'); pollPositions(); refreshQuote(); }); } function buildPosCard(row) { var pnlClass = row.float_pnl > 0 ? 'pnl-pos' : (row.float_pnl < 0 ? 'pnl-neg' : ''); var pnlText = row.float_pnl != null ? ((row.float_pnl >= 0 ? '+' : '') + fmtNum(row.float_pnl) + ' 元') : '--'; var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空'); var closeBtn = row.can_close ? '' : ''; return ( '