(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; var isTradingSession = false; var hasSlTpMonitoring = false; var ctpConnected = false; var pollIntervalMs = 0; var selectedMaxLots = null; var recommendMaxByProduct = {}; var recommendMaxByCode = {}; 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() { var codeEl = document.getElementById('trade-symbol-code'); var code = codeEl && codeEl.value ? codeEl.value.trim() : ''; if (code) return code; 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 updateRecommendMaxMaps(data) { recommendMaxByProduct = {}; recommendMaxByCode = {}; (data && data.rows || []).forEach(function (r) { if (!r || r.max_lots <= 0) return; if (r.status !== 'ok' && r.status !== 'margin_ok') return; if (r.ths) recommendMaxByProduct[String(r.ths).toLowerCase()] = r.max_lots; if (r.main_code) recommendMaxByCode[String(r.main_code).toLowerCase()] = r.max_lots; }); checkLotsLimit(); } function maxLotsForSymbol(sym) { if (selectedMaxLots > 0) return selectedMaxLots; var code = (sym || '').trim().toLowerCase(); if (!code) return 0; if (recommendMaxByCode[code]) return recommendMaxByCode[code]; var m = code.match(/^([a-z]+)/i); if (m && recommendMaxByProduct[m[1].toLowerCase()]) { return recommendMaxByProduct[m[1].toLowerCase()]; } return 0; } function checkLotsLimit() { var warn = document.getElementById('lots-warn'); if (!warn) return; var sym = selectedSymbol(); var maxLots = maxLotsForSymbol(sym); var lots = effectiveLots(); if (maxLots > 0 && lots > maxLots) { warn.hidden = false; warn.textContent = '已超过最大手数 ' + maxLots + ' 手,请调整手数'; } else { warn.hidden = true; warn.textContent = ''; } } function schedulePositionPoll() { var nextMs = 0; if (hasSlTpMonitoring && isTradingSession) { nextMs = 1000; } else if (!ctpConnected) { nextMs = 5000; } if (nextMs === pollIntervalMs && pollTimer) return; pollIntervalMs = nextMs; if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } if (nextMs > 0) { pollTimer = setInterval(pollPositions, nextMs); } } function updateSessionUi() { var btnOpen = document.getElementById('btn-open'); var sessionHint = document.getElementById('session-hint'); if (btnOpen) { btnOpen.disabled = !isTradingSession; btnOpen.classList.toggle('btn-session-off', !isTradingSession); } if (sessionHint) { sessionHint.hidden = !!isTradingSession; } } 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, connecting) { var ctpBadge = document.getElementById('ctp-badge'); var btnConnect = document.getElementById('btn-ctp-connect'); if (ctpBadge) { if (connecting) { ctpBadge.textContent = 'CTP 连接中'; ctpBadge.className = 'badge planned'; } else { ctpBadge.textContent = connected ? 'CTP 已连接' : 'CTP 未连接'; ctpBadge.className = 'badge ' + (connected ? 'profit' : 'planned'); } } if (btnConnect) { if (connecting) { btnConnect.textContent = '连接中…'; btnConnect.disabled = true; } else { btnConnect.disabled = false; btnConnect.textContent = connected ? '重连 CTP' : '连接 CTP'; } } } function waitForCtpConnected(maxMs) { var deadline = Date.now() + (maxMs || 35000); function tick() { return fetch('/api/ctp/status') .then(function (r) { return r.json(); }) .then(function (d) { var st = d.status || {}; if (st.connected) { updateCtpBadge(true, false); if (d.account && d.account.available != null) { var avail = document.getElementById('avail-display'); if (avail) avail.textContent = Number(d.account.available).toFixed(2); } pollPositions(); return true; } if (st.connecting && Date.now() < deadline) { updateCtpBadge(false, true); return new Promise(function (resolve) { setTimeout(function () { resolve(tick()); }, 2000); }); } updateCtpBadge(false, false); if (st.last_error) { var hint = document.querySelector('.ctp-install-hint'); if (hint) hint.textContent = st.last_error; } return false; }) .catch(function () { updateCtpBadge(false, false); return false; }); } return tick(); } function requestCtpConnect(force) { updateCtpBadge(false, true); return fetch('/api/ctp/connect', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ force: !!force, auto: !force }) }) .then(function (r) { return r.json(); }) .then(function (d) { if (d.status && d.status.connected) { updateCtpBadge(true, false); pollPositions(); return d; } if (d.connecting || (d.status && d.status.connecting)) { return waitForCtpConnected(35000).then(function (ok) { if (!ok && d.error) alert(d.error); else if (!ok && d.status && d.status.last_error) alert(d.status.last_error); return d; }); } if (!d.ok) { updateCtpBadge(false, false); alert(d.error || (d.status && d.status.last_error) || '连接失败'); } return d; }) .catch(function () { updateCtpBadge(false, false); }); } 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 = '填写止损后自动计算'; checkLotsLimit(); 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 = '填写止损后自动计算'; checkLotsLimit(); scheduleQuote(); }).catch(function () { lotsCalc.placeholder = '计算失败'; }); } function tryAutoCtpReconnect() { if (ctpReconnecting) return; var now = Date.now(); if (now - lastCtpReconnectAt < 60000) return; lastCtpReconnectAt = now; ctpReconnecting = true; requestCtpConnect(false).finally(function () { ctpReconnecting = false; }); } function showOrderMsg(text, ok) { var el = document.getElementById('order-msg'); if (!el) return; if (!text) { el.hidden = true; el.textContent = ''; el.className = 'trade-order-msg'; return; } el.hidden = false; el.textContent = text; el.className = 'trade-order-msg ' + (ok ? 'ok' : 'err'); } function postOrder(offset) { var sym = selectedSymbol(); if (!sym) { showOrderMsg('请选择品种', false); return; } var direction = dirSelect ? dirSelect.value : 'long'; var price = entryPrice(); if (!price || price <= 0) { showOrderMsg('无法获取有效价格,请先填写或刷新行情', false); return; } var lots = effectiveLots(); var trailingBeEl = document.getElementById('trailing-be'); if (offset === 'open') { if (!isTradingSession) { showOrderMsg('不在交易时间段', false); return; } var trailingOn = !!(trailingBeEl && trailingBeEl.checked); if (trailingOn && !(slInput && slInput.value)) { showOrderMsg('开启移动保本须填写止损价', false); return; } if (isRiskMode() && lots <= 0) { showOrderMsg('请填写止损,系统将自动计算手数', false); return; } if (!isRiskMode() && lots <= 0) { showOrderMsg('请填写手数', false); return; } var maxLots = maxLotsForSymbol(sym); if (maxLots > 0 && lots > maxLots) { showOrderMsg('手数 ' + lots + ' 超过最大手数 ' + maxLots + ' 手', false); return; } } var btnOpen = document.getElementById('btn-open'); if (btnOpen) { btnOpen.disabled = true; btnOpen.textContent = '开仓中…'; } showOrderMsg('开仓中…', true); 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, trailing_be: !!(trailingBeEl && trailingBeEl.checked) }; 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) { showOrderMsg(data.error || '下单失败', false); return; } var msg = data.message || ('开仓成功 · ' + (data.lots || lots) + ' 手'); showOrderMsg(msg, true); pollPositions(); refreshQuote(); setTimeout(function () { showOrderMsg(''); }, 4000); }).catch(function () { showOrderMsg('网络错误,请重试', false); }).finally(function () { if (btnOpen) { btnOpen.textContent = '开仓'; updateSessionUi(); } }); } function buildPendingHtml(items) { if (!items || !items.length) return ''; var rows = items.map(function (p) { var cls = p.order_kind === 'stop_loss' ? 'sl' : (p.order_kind === 'take_profit' ? 'tp' : 'ctp'); var dismissBtn = p.monitor_id ? '' : ''; return ( '
' + '' + (p.label || '挂单') + '' + '' + '' + fmtNum(p.price) + ' · ' + (p.lots || 1) + ' 手' + dismissBtn + '' + '
' ); }).join(''); return '
止盈止损监控
' + rows + '
'; } function dismissMonitor(monitorId, btn) { if (!monitorId) return; if (!confirm('取消该本地止盈止损监控?(不影响柜台委托)')) return; if (btn) { btn.disabled = true; btn.textContent = '取消中…'; } fetch('/api/trading/monitor/dismiss', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ monitor_id: monitorId }) }) .then(function (r) { return r.json(); }) .then(function (d) { if (!d.ok) throw new Error(d.error || '取消失败'); pollPositions(); }) .catch(function (e) { alert(e.message || '取消失败'); if (btn) { btn.disabled = false; btn.textContent = '取消'; } }); } function bindPendingDismiss(root) { if (!root) return; root.querySelectorAll('[data-monitor-id]').forEach(function (btn) { btn.addEventListener('click', function () { dismissMonitor(parseInt(btn.getAttribute('data-monitor-id'), 10), btn); }); }); } 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 netClass = row.est_pnl_net > 0 ? 'pnl-pos' : (row.est_pnl_net < 0 ? 'pnl-neg' : ''); var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空'); var openT = (row.open_time || '').replace('T', ' ').slice(0, 16); var slTpBtn = (!row.stop_loss && !row.take_profit && row.can_close) ? '' : ''; var orderBtn = ''; if (row.monitor_id && (row.stop_loss != null || row.take_profit != null) && row.can_place_orders) { orderBtn = ''; } var closePayload = encodeURIComponent(JSON.stringify({ source: row.source, symbol_code: row.symbol_code, direction: row.direction, lots: row.lots, mark_price: row.mark_price, monitor_id: row.monitor_id || null })); var closeBtn = row.can_close ? '' : ''; var actionBtns = (orderBtn || closeBtn) ? '
' + orderBtn + closeBtn + '
' : ''; return ( '
' + '
' + row.symbol + ' ' + dirBadge + '
' + '
' + (row.symbol_code || '') + '
' + actionBtns + '
' + '
来源 ' + (row.source_label || 'CTP') + ' · 柜台浮盈' + (slTpBtn ? ' · ' + slTpBtn : '') + (row.sl_order_active ? ' · 止损监控中' : '') + (row.tp_order_active ? ' · 止盈监控中' : '') + (row.trailing_be ? ' · 移动保本' + (row.trailing_r_locked ? '(锁' + row.trailing_r_locked + 'R)' : '') + '' : '') + '
' + '
' + '
' + fmtNum(row.entry_price) + '
' + '
' + (row.current_price != null ? fmtNum(row.current_price) : '--') + '
' + '
' + (row.stop_loss != null ? fmtNum(row.stop_loss) : '--') + '
' + '
' + (row.take_profit != null ? fmtNum(row.take_profit) : '--') + '
' + '
' + (row.margin != null ? fmtNum(row.margin) + ' 元' : '--') + '
' + '
' + (row.position_pct != null ? fmtNum(row.position_pct) + '%' : '--') + '
' + '
' + pnlText + '
' + '
' + (row.est_fee != null ? fmtNum(row.est_fee) + ' 元' : '--') + '
' + '
' + (row.est_pnl_net != null ? fmtNum(row.est_pnl_net) + ' 元' : '--') + '
' + '
' + buildPendingHtml(row.pending_orders) + '
' ); } function placeMonitorOrders(monitorId, btn) { if (!monitorId) return; if (!confirm('清理该持仓在柜台残留的旧版止盈/止损挂单?')) return; if (btn) { btn.disabled = true; btn.textContent = '委托中…'; } fetch('/api/trading/monitor/place-orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ monitor_id: monitorId }) }) .then(function (r) { return r.json(); }) .then(function (d) { if (!d.ok) throw new Error(d.error || d.message || '委托失败'); var msg = d.message || '委托已提交'; if (d.skipped && d.skipped.length) msg += '\n' + d.skipped.join('\n'); alert(msg); pollPositions(); }) .catch(function (e) { alert(e.message || '委托失败'); if (btn) { btn.disabled = false; btn.textContent = '委托'; } }); } function bindPlaceOrderButtons(root) { if (!root) return; root.querySelectorAll('[data-place-orders]').forEach(function (btn) { btn.addEventListener('click', function () { placeMonitorOrders(parseInt(btn.getAttribute('data-place-orders'), 10), btn); }); }); } function promptStopTakeProfit(payload, btn) { var slRaw = prompt('止损价(可留空)', ''); if (slRaw === null) return; var tpRaw = prompt('止盈价(可留空)', ''); if (tpRaw === null) return; var sl = slRaw.trim() ? parseFloat(slRaw) : null; var tp = tpRaw.trim() ? parseFloat(tpRaw) : null; if (sl == null && tp == null) { alert('请至少填写止损或止盈'); return; } if (btn) { btn.disabled = true; btn.textContent = '保存中…'; } fetch('/api/trading/monitor/upsert', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ symbol_code: payload.symbol_code, direction: payload.direction, lots: payload.lots, entry_price: payload.entry_price, stop_loss: sl, take_profit: tp }) }) .then(function (r) { return r.json(); }) .then(function (d) { if (!d.ok) throw new Error(d.error || '保存失败'); pollPositions(); }) .catch(function (e) { alert(e.message || '保存失败'); if (btn) { btn.disabled = false; btn.textContent = '设置止盈止损'; } }); } function bindSlTpButtons(root) { if (!root) return; root.querySelectorAll('[data-sl-tp]').forEach(function (btn) { btn.addEventListener('click', function () { promptStopTakeProfit(JSON.parse(decodeURIComponent(btn.getAttribute('data-sl-tp'))), btn); }); }); } function closePosition(payload, btn) { function doClose(price) { if (!price || price <= 0) { alert('无法获取现价'); return; } if (!confirm('确认平仓 ' + payload.lots + ' 手?')) return; if (btn) { btn.disabled = true; btn.textContent = '平仓中…'; } fetch('/api/trading/close', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(Object.assign({}, payload, { price: price })) }).then(function (r) { return r.json(); }).then(function (d) { if (!d.ok) { alert(d.error || '平仓失败'); if (btn) { btn.disabled = false; btn.textContent = '平仓'; } return; } if (btn) btn.textContent = '已平仓'; pollPositions(); }).catch(function () { if (btn) { btn.disabled = false; btn.textContent = '平仓'; } }); } if (payload.mark_price > 0) { doClose(payload.mark_price); return; } fetch('/api/trade/quote?symbol=' + encodeURIComponent(payload.symbol_code) + '&lots=' + payload.lots) .then(function (r) { return r.json(); }) .then(function (d) { doClose(d.price); }); } 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 connected = data.ctp_status && data.ctp_status.connected; var connecting = data.ctp_status && data.ctp_status.connecting; ctpConnected = !!connected; isTradingSession = !!data.trading_session; updateCtpBadge(!!connected, !!connecting); 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 rows = data.rows || []; hasSlTpMonitoring = rows.some(function (row) { return row.stop_loss != null || row.take_profit != null; }); schedulePositionPoll(); updateSessionUi(); if (!connected) { if (connecting) { list.innerHTML = '
CTP 连接中,请稍候…
'; return; } list.innerHTML = '
CTP 未连接,正在尝试自动重连…
'; tryAutoCtpReconnect(); return; } if (!rows.length) { var pendingOnly = data.pending_orders || []; if (pendingOnly.length) { list.innerHTML = '
柜台暂无持仓
' + pendingOnly.map(function (p) { var dismissBtn = p.monitor_id ? '' : ''; return ( '
' + (p.label || '挂单') + ' · ' + (p.symbol || p.symbol_code) + '' + '' + fmtNum(p.price) + ' · ' + (p.lots || 1) + ' 手' + dismissBtn + '
' ); }).join(''); bindPendingDismiss(list); } else { list.innerHTML = '
柜台暂无持仓。
'; } return; } list.innerHTML = rows.map(buildPosCard).join(''); bindPendingDismiss(list); bindSlTpButtons(list); bindPlaceOrderButtons(list); list.querySelectorAll('[data-close]').forEach(function (btn) { btn.addEventListener('click', function () { closePosition(JSON.parse(decodeURIComponent(btn.getAttribute('data-close'))), btn); }); }); }) .catch(function () { if (list.innerHTML.indexOf('pos-card') < 0) { list.innerHTML = '
持仓加载失败
'; } }); } function renderRecommendations(data) { if (!recommendList || !data) return; updateRecommendMaxMaps(data); var recCap = document.getElementById('rec-capital'); if (recCap && data.capital != null) recCap.textContent = Number(data.capital).toFixed(2); var recUpdated = document.getElementById('rec-updated'); if (recUpdated && data.updated_at) { recUpdated.textContent = '每日后台更新 · 最近 ' + data.updated_at; } var rows = data.rows || []; if (!rows.length) { recommendList.innerHTML = '当前资金下暂无推荐品种(每日后台刷新)'; return; } recommendList.innerHTML = rows.map(function (r) { return ( '' + '' + (r.name || '') + ' ' + (r.main_code || r.ths || '') + '' + '' + (r.exchange || '') + '' + '' + (r.price != null ? r.price : '—') + '' + '' + (r.ref_stop_loss != null ? r.ref_stop_loss : '—') + '' + '' + (r.ref_take_profit != null ? r.ref_take_profit : '—') + '' + '' + (r.margin_one_lot != null ? r.margin_one_lot : '—') + '' + '' + (r.open_fee_one_lot != null ? r.open_fee_one_lot : '—') + '' + '' + (r.max_lots != null && r.max_lots > 0 ? r.max_lots : '—') + '' + '' + (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); }; } document.querySelectorAll('.price-tab').forEach(function (btn) { btn.addEventListener('click', function () { setPriceType(btn.getAttribute('data-type')); scheduleQuote(); }); }); if (symInput) { symInput.addEventListener('input', function () { selectedMaxLots = null; scheduleQuote(); scheduleAutoCalc(); checkLotsLimit(); }); symInput.addEventListener('symbol-selected', function (ev) { var item = ev.detail || {}; selectedMaxLots = item.max_lots > 0 ? item.max_lots : null; scheduleQuote(); scheduleAutoCalc(); checkLotsLimit(); }); } if (lotsInput) lotsInput.addEventListener('input', function () { scheduleQuote(); checkLotsLimit(); }); if (lotsCalc) lotsCalc.addEventListener('input', checkLotsLimit); if (slInput) slInput.addEventListener('input', scheduleAutoCalc); if (tpInput) tpInput.addEventListener('input', scheduleAutoCalc); if (dirSelect) dirSelect.addEventListener('change', scheduleAutoCalc); if (priceInput) { priceInput.addEventListener('input', function () { if (priceType === 'limit') priceInput.dataset.manual = '1'; scheduleAutoCalc(); }); } var btnOpen = document.getElementById('btn-open'); if (btnOpen) btnOpen.addEventListener('click', function () { postOrder('open'); }); var btnConnect = document.getElementById('btn-ctp-connect'); if (btnConnect) { btnConnect.addEventListener('click', function () { requestCtpConnect(true); }); } runWhenReady(function () { setPriceType('limit'); pollPositions(); connectRecommendStream(); fetch('/api/recommend/list') .then(function (r) { return r.json(); }) .then(function (data) { if (data.ok) renderRecommendations(data); }) .catch(function () {}); document.addEventListener('visibilitychange', function () { if (document.visibilityState === 'visible') pollPositions(); }); updateSessionUi(); scheduleQuote(); }); })();