From 7b8a660309dee1f15b282faa65d85311604b8e57 Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 24 Jun 2026 10:18:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=88=E5=B9=B6=E6=9C=9F=E8=B4=A7=E4=B8=8B?= =?UTF-8?q?=E5=8D=95=E4=B8=8E=E6=8C=81=E4=BB=93=E7=9B=91=E6=8E=A7=E4=B8=BA?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E7=95=8C=E9=9D=A2=EF=BC=8C=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=89=8B=E5=B7=A5=E5=BD=95=E5=85=A5=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 策略与 CTP 自动同步持仓,新增 /api/trading/live 聚合展示与平仓接口。 Co-authored-by: Cursor --- app.py | 41 +------ install_trading.py | 243 +++++++++++++++++++++++++++++++++++++++- static/css/trade.css | 21 +--- static/js/trade.js | 230 +++++++++++++++++++++---------------- templates/base.html | 3 +- templates/settings.html | 2 +- templates/strategy.html | 2 +- templates/trade.html | 87 +++----------- 8 files changed, 396 insertions(+), 233 deletions(-) diff --git a/app.py b/app.py index 9ad1403..7c55abe 100644 --- a/app.py +++ b/app.py @@ -886,16 +886,6 @@ def keys(): return render_template("keys.html", keys=key_list, history=history) -@app.route("/positions") -@login_required -def positions(): - conn = get_db() - pos_list = conn.execute( - "SELECT * FROM position_monitors WHERE status='active' ORDER BY id DESC" - ).fetchall() - conn.close() - return render_template("positions.html", positions=pos_list) - @app.route("/add_key", methods=["POST"]) @login_required @@ -928,36 +918,7 @@ def add_key(): @app.route("/add_position", methods=["POST"]) @login_required def add_position(): - d = request.form - symbol = d.get("symbol", "").strip() - symbol_name = d.get("symbol_name", "").strip() - market_code = d.get("market_code", "").strip() - sina_code = d.get("sina_code", "").strip() - if not symbol or not market_code: - flash("请从下拉列表选择品种") - return redirect(url_for("positions")) - entry = float(d["entry_price"]) - sl = float(d["stop_loss"]) - tp = float(d["take_profit"]) - direction = d.get("direction", "").strip() - if not direction: - direction = "long" if sl < entry else "short" - open_time = d.get("open_time", "").strip() - lots = float(d.get("lots") or 1) - conn = get_db() - conn.execute( - """INSERT INTO position_monitors - (symbol, symbol_name, market_code, sina_code, direction, - lots, entry_price, stop_loss, take_profit, open_time) - VALUES (?,?,?,?,?,?,?,?,?,?)""", - ( - symbol, symbol_name, market_code, sina_code, direction, - lots, entry, sl, tp, open_time, - ), - ) - conn.commit() - conn.close() - flash("持仓已添加") + flash("持仓由策略交易或 CTP 自动同步,无需手工录入") return redirect(url_for("positions")) diff --git a/install_trading.py b/install_trading.py index 371baaf..537a24b 100644 --- a/install_trading.py +++ b/install_trading.py @@ -8,6 +8,7 @@ from typing import Any, Callable from flask import flash, jsonify, redirect, render_template, request, url_for from contract_specs import calc_position_metrics, get_contract_spec +from fee_specs import calc_fee_breakdown from position_sizing import ( MODE_FIXED, MODE_RISK, @@ -98,32 +99,264 @@ def install_trading(app, *, login_required, get_db, get_setting, set_setting, fe except Exception: return False + def _holding_duration(open_time: str, now_iso: str) -> str: + try: + from app import calc_holding_duration + return calc_holding_duration(open_time, now_iso) + except Exception: + return "" + + def _build_trading_live_rows(conn) -> list[dict]: + from zoneinfo import ZoneInfo + tz = ZoneInfo("Asia/Shanghai") + now_iso = datetime.now(tz).strftime("%Y-%m-%dT%H:%M") + capital = _capital(conn) + mode = get_trading_mode(get_setting) + ctp_st = ctp_status(mode) + rows: list[dict] = [] + seen: set[str] = set() + + ctp_pairs: list[tuple[str, str]] = [] + + if ctp_st.get("connected"): + for p in _ctp_positions(mode): + sym = (p.get("symbol") or "").strip() + direction = p.get("direction") or "long" + lots = int(p.get("lots") or 0) + if lots <= 0: + continue + ctp_pairs.append((sym, direction)) + key = f"ctp:{sym.lower()}:{direction}" + seen.add(key) + entry = float(p.get("avg_price") or 0) + codes = ths_to_codes(sym) + mark = fetch_price( + sym, + codes.get("market_code", "") if codes else "", + codes.get("sina_code", "") if codes else "", + ) + spec = get_contract_spec(sym) + mult = spec["mult"] + float_pnl = p.get("pnl") + if mark is not None and entry > 0: + if direction == "long": + float_pnl = round((mark - entry) * mult * lots, 2) + else: + float_pnl = round((entry - mark) * mult * lots, 2) + tick = calc_order_tick_metrics(sym, lots, mark or entry) + rows.append({ + "key": key, + "source": "ctp", + "source_label": "CTP 柜台", + "symbol": codes.get("name", sym) if codes else sym, + "symbol_code": sym, + "direction": direction, + "direction_label": "做多" if direction == "long" else "做空", + "lots": lots, + "entry_price": entry, + "stop_loss": None, + "take_profit": None, + "mark_price": mark, + "float_pnl": float_pnl, + "tick_value_total": tick.get("tick_value_total"), + "price_precision": tick.get("price_precision"), + "tick_size": tick.get("tick_size"), + "can_close": True, + }) + + def _dup_ctp(ths_sym: str, direction: str) -> bool: + for cs, d in ctp_pairs: + if d != direction: + continue + if cs.lower() == ths_sym.lower() or _match_ctp_symbol(cs, ths_sym): + return True + return False + + monitors = conn.execute( + "SELECT * FROM trade_order_monitors WHERE status='active' ORDER BY id DESC" + ).fetchall() + for r in monitors: + sym = r["symbol"] + direction = r["direction"] + if _dup_ctp(sym, direction): + continue + key = f"mon:{sym.lower()}:{direction}" + entry = float(r["entry_price"] or 0) + sl = float(r["stop_loss"]) if r["stop_loss"] is not None else None + tp = float(r["take_profit"]) if r["take_profit"] is not None else None + lots = float(r["lots"] or 1) + codes = ths_to_codes(sym) + market = r["market_code"] or (codes.get("market_code", "") if codes else "") or "" + sina = codes.get("sina_code", "") if codes else "" + mark = fetch_price(sym, market, sina) + metrics = calc_position_metrics(direction, entry, sl or entry, tp or entry, lots, mark, capital, sym) + fee_info = calc_fee_breakdown(sym, entry, mark or entry, lots, r["open_time"] or "", now_iso) + est_net = None + if metrics.get("float_pnl") is not None: + est_net = round(metrics["float_pnl"] - fee_info["total_fee"], 2) + rows.append({ + "key": key, + "source": "program", + "source_label": r["monitor_type"] or "程序监控", + "monitor_id": r["id"], + "symbol": r["symbol_name"] or sym, + "symbol_code": sym, + "direction": direction, + "direction_label": "做多" if direction == "long" else "做空", + "lots": lots, + "entry_price": entry, + "stop_loss": sl, + "take_profit": tp, + "mark_price": mark, + "open_time": r["open_time"], + "holding_duration": _holding_duration(r["open_time"] or "", now_iso), + "float_pnl": metrics.get("float_pnl"), + "float_pct": metrics.get("float_pct"), + "risk_pct": metrics.get("risk_pct"), + "risk_amount": metrics.get("risk_amount"), + "rr_ratio": metrics.get("rr_ratio"), + "margin": metrics.get("margin"), + "position_pct": metrics.get("position_pct"), + "est_fee": fee_info["total_fee"], + "est_pnl_net": est_net, + "can_close": ctp_st.get("connected"), + }) + + legacy = conn.execute( + "SELECT * FROM position_monitors WHERE status='active' ORDER BY id DESC" + ).fetchall() + for r in legacy: + sym = r["symbol"] + direction = r["direction"] + key = f"leg:{sym.lower()}:{direction}" + if any(x.get("symbol_code", "").lower() == sym.lower() and x.get("direction") == direction for x in rows): + continue + entry = float(r["entry_price"]) + sl = float(r["stop_loss"]) + tp = float(r["take_profit"]) + lots = float(r["lots"] or 1) + market = r["market_code"] or "" + sina = r["sina_code"] or "" + mark = fetch_price(sym, market, sina) + metrics = calc_position_metrics(direction, entry, sl, tp, lots, mark, capital, sym) + fee_info = calc_fee_breakdown(sym, entry, mark or entry, lots, r["open_time"] or "", now_iso) + est_net = None + if metrics.get("float_pnl") is not None: + est_net = round(metrics["float_pnl"] - fee_info["total_fee"], 2) + rows.append({ + "key": key, + "source": "legacy", + "source_label": "历史录入", + "legacy_id": r["id"], + "symbol": r["symbol_name"] or sym, + "symbol_code": sym, + "direction": direction, + "direction_label": "做多" if direction == "long" else "做空", + "lots": lots, + "entry_price": entry, + "stop_loss": sl, + "take_profit": tp, + "mark_price": mark, + "open_time": r["open_time"], + "holding_duration": _holding_duration(r["open_time"] or "", now_iso), + "float_pnl": metrics.get("float_pnl"), + "float_pct": metrics.get("float_pct"), + "risk_pct": metrics.get("risk_pct"), + "risk_amount": metrics.get("risk_amount"), + "rr_ratio": metrics.get("rr_ratio"), + "margin": metrics.get("margin"), + "position_pct": metrics.get("position_pct"), + "est_fee": fee_info["total_fee"], + "est_pnl_net": est_net, + "can_close": True, + "close_url": f"/close_position/{r['id']}", + }) + return rows + @app.route("/trade") @login_required def trade_page(): + return redirect(url_for("positions")) + + @app.route("/positions") + @login_required + def positions(): conn = get_db() init_strategy_tables(conn) mode = get_trading_mode(get_setting) ctp_st = ctp_status(mode) capital = _capital(conn) - sizing = get_sizing_mode(get_setting) risk = get_risk_status(conn) ctp_acc = _ctp_account(mode) if ctp_st.get("connected") else {} - positions = _ctp_positions(mode) if ctp_st.get("connected") else [] conn.close() return render_template( "trade.html", trading_mode=mode, trading_mode_label=trading_mode_label(get_setting), - sizing_mode=sizing, - risk_percent=get_risk_percent(get_setting), capital=capital, risk_status=risk, ctp_status=ctp_st, ctp_account=ctp_acc, - ctp_positions=positions, ) + @app.route("/api/trading/live") + @login_required + def api_trading_live(): + conn = get_db() + init_strategy_tables(conn) + mode = get_trading_mode(get_setting) + ctp_st = ctp_status(mode) + rows = _build_trading_live_rows(conn) + conn.close() + return jsonify({ + "rows": rows, + "capital": _capital(get_db()), + "ctp_status": ctp_st, + "trading_mode_label": trading_mode_label(get_setting), + }) + + @app.route("/api/trading/close", methods=["POST"]) + @login_required + def api_trading_close(): + d = request.get_json(silent=True) or {} + source = (d.get("source") or "").strip() + conn = get_db() + init_strategy_tables(conn) + mode = get_trading_mode(get_setting) + if not ctp_status(mode).get("connected") and source in ("ctp", "program"): + conn.close() + return jsonify({"ok": False, "error": "请先连接 CTP"}), 400 + sym = (d.get("symbol_code") or d.get("symbol") or "").strip() + direction = (d.get("direction") or "long").strip().lower() + try: + lots = max(1, int(d.get("lots") or 1)) + price = float(d.get("price") or 0) + except (TypeError, ValueError): + conn.close() + return jsonify({"ok": False, "error": "参数无效"}), 400 + if not sym or price <= 0: + conn.close() + return jsonify({"ok": False, "error": "品种或价格无效"}), 400 + offset = "close_long" if direction == "long" else "close_short" + try: + execute_order( + conn, mode=mode, offset=offset, symbol=sym, direction=direction, + lots=lots, price=price, settings=_settings_dict(), + ) + if source == "program": + mid = int(d.get("monitor_id") or 0) + if mid: + conn.execute( + "UPDATE trade_order_monitors SET status='closed' WHERE id=?", + (mid,), + ) + conn.commit() + conn.close() + return jsonify({"ok": True}) + except ValueError as exc: + conn.close() + return jsonify({"ok": False, "error": str(exc)}), 400 + @app.route("/recommend") @login_required def recommend_page(): diff --git a/static/css/trade.css b/static/css/trade.css index d715958..29d9822 100644 --- a/static/css/trade.css +++ b/static/css/trade.css @@ -1,20 +1,9 @@ -.trade-page{max-width:720px;margin:0 auto} +.trade-page{max-width:960px;margin:0 auto} .trade-top-bar{display:flex;flex-wrap:wrap;gap:.65rem;align-items:center;margin-bottom:1rem} -.trade-order-card{padding:1.25rem} -.trade-tabs{display:flex;gap:1rem;margin-bottom:1rem;font-size:.88rem} -.trade-tabs span.active{color:var(--accent);font-weight:600;border-bottom:2px solid var(--accent);padding-bottom:.25rem} -.trade-tabs a{color:var(--text-muted);text-decoration:none} -.trade-input-row,.trade-risk-row{display:grid;grid-template-columns:2fr 1fr 1fr;gap:.65rem;margin-bottom:.75rem} -.trade-field label{display:block;font-size:.72rem;margin-bottom:.25rem;color:var(--text-label)} -.trade-btn-row{display:grid;grid-template-columns:repeat(4,1fr);gap:.5rem;margin:1rem 0} -.trade-btn{border:none;border-radius:8px;padding:.75rem .35rem;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:.15rem;color:#fff;font-weight:600} -.trade-btn .btn-price{font-size:1.1rem} -.trade-btn .btn-label{font-size:.85rem} -.trade-btn .btn-sub{font-size:.68rem;opacity:.85;font-weight:400} -.trade-btn.long{background:linear-gradient(180deg,#e74c3c,#c0392b)} -.trade-btn.lock{background:linear-gradient(180deg,#27ae60,#1e8449)} -.trade-btn.close{background:linear-gradient(180deg,#3498db,#2980b9)} -.trade-footer{background:var(--card-inner);border-radius:8px;padding:.75rem 1rem;font-size:.82rem;line-height:1.55;border:1px solid var(--card-border)} +.trade-subnav{display:flex;gap:1rem;margin-bottom:1rem;font-size:.88rem} +.trade-subnav span.active{color:var(--accent);font-weight:600;border-bottom:2px solid var(--accent);padding-bottom:.25rem} +.trade-subnav a{color:var(--text-muted);text-decoration:none} +.trade-footer{background:var(--card-inner);border-radius:8px;padding:.75rem 1rem;font-size:.82rem;line-height:1.55;border:1px solid var(--card-border);margin-top:1rem} .trade-footer strong{color:var(--accent)} .rec-blocked td{opacity:.55} .rec-ok td:first-child{font-weight:600} diff --git a/static/js/trade.js b/static/js/trade.js index 33bed49..814ea46 100644 --- a/static/js/trade.js +++ b/static/js/trade.js @@ -1,95 +1,142 @@ (function () { - 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 debounceTimer; + var list = document.getElementById('position-live-list'); + var pollTimer = null; - function selectedSymbol() { - return (symInput && symInput.value || '').trim(); + function fmtNum(v, digits) { + if (v === null || v === undefined) return '--'; + return Number(v).toFixed(digits === undefined ? 2 : digits); } - 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 ml = document.getElementById('max-long'); - var ms = document.getElementById('max-short'); - if (ml) ml.textContent = '≤' + (data.max_open_long || '—'); - if (ms) ms.textContent = '≤' + (data.max_open_short || '—'); - document.getElementById('pos-long').textContent = '≤' + (data.pos_long || 0); - document.getElementById('pos-short').textContent = '≤' + (data.pos_short || 0); - if (footer && data.metrics) { - var m = data.metrics; - footer.innerHTML = - '

' + (data.name || sym) + ' ' + (data.footer_text || '') + '

' + - '

价格精度 ' + m.price_precision + ' 位 · ' + - '最小变动 ' + m.tick_size + ' · ' + - '每跳 ' + m.tick_value_per_lot + ' 元/手 · ' + - '当前 ' + lots + ' 手每跳合计 ' + m.tick_value_total + '

' + - (m.margin_total ? '

预估保证金约 ' + m.margin_total + ' 元

' : ''); - } - }).catch(function () {}); + function buildPosCard(row) { + var pnlClass = ''; + if (row.float_pnl > 0) pnlClass = 'pnl-pos'; + if (row.float_pnl < 0) pnlClass = '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 = + ''; + } + + var metaParts = ['来源 ' + (row.source_label || row.source) + '']; + if (row.risk_pct != null) { + metaParts.push('风险 ' + fmtNum(row.risk_pct) + '%≈' + fmtNum(row.risk_amount) + '元'); + } + if (row.tick_value_total != null) { + metaParts.push('每跳 ' + fmtNum(row.tick_value_total) + '元'); + } + + var slTp = + '
' + (row.stop_loss != null ? fmtNum(row.stop_loss) : '--') + '
' + + '
' + (row.take_profit != null ? fmtNum(row.take_profit) : '--') + '
'; + + var footerParts = ['张数 ' + row.lots]; + if (row.margin != null) footerParts.push('保证金 ' + fmtNum(row.margin) + '元'); + if (row.position_pct != null) footerParts.push('仓位占比 ' + fmtNum(row.position_pct) + '%'); + if (openT) footerParts.push('开仓 ' + openT); + if (row.holding_duration) footerParts.push('持仓 ' + row.holding_duration); + if (row.est_fee != null) footerParts.push('手续费(估) ' + fmtNum(row.est_fee) + '元'); + + return ( + '
' + + '
' + + '
' + row.symbol + ' ' + dirBadge + '
' + + (row.symbol_code && row.symbol_code !== row.symbol ? '
' + row.symbol_code + '
' : '') + + '
' + closeBtn + '
' + + '
' + metaParts.join(' · ') + '
' + + '
' + + '
' + fmtNum(row.entry_price) + '
' + + slTp + + '
' + rr + '
' + + '
' + (row.mark_price != null ? fmtNum(row.mark_price) : '--') + '
' + + '
' + pnlText + '
' + + (row.est_fee != null ? + '
' + fmtNum(row.est_fee) + '元
' + + '
' + + '
' + (row.est_pnl_net != null ? fmtNum(row.est_pnl_net) + '元' : '--') + '
' + : '') + + '
' + + '' + + '
' + ); } - function scheduleRefresh() { - clearTimeout(debounceTimer); - debounceTimer = setTimeout(refreshQuote, 400); - } - - if (symInput) symInput.addEventListener('input', scheduleRefresh); - if (lotsInput) lotsInput.addEventListener('input', scheduleRefresh); - if (priceInput) { - priceInput.addEventListener('input', function () { - priceInput.dataset.manual = '1'; - }); - } - - 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', { + 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(body) - }).then(function (r) { return r.json(); }).then(function (data) { - if (!data.ok) { alert(data.error || '下单失败'); return; } - alert('已提交 ' + (data.lots || '') + ' 手'); - location.reload(); - }); + body: JSON.stringify({ + source: payload.source, + symbol_code: payload.symbol_code, + direction: payload.direction, + lots: payload.lots, + price: price, + monitor_id: payload.monitor_id + }) + }).then(function (r) { return r.json(); }).then(function (d) { + if (!d.ok) { alert(d.error || '平仓失败'); return; } + pollPositions(); + }).catch(function () { alert('平仓请求失败'); }); } - 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'); }); + function pollPositions() { + if (!list) return; + fetch('/api/trading/live') + .then(function (r) { 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 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 = '
暂无持仓。请先在「策略交易」开仓,或连接 CTP 同步柜台持仓。
'; + 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 = '
加载失败,请刷新页面
'; + } + }); + } var btnConnect = document.getElementById('btn-ctp-connect'); if (btnConnect) { @@ -109,19 +156,8 @@ }); } - setInterval(function () { - fetch('/api/account_snapshot').then(function (r) { return r.json(); }).then(function (d) { - var cap = document.getElementById('cap-display'); - if (cap && d.capital != null) cap.textContent = Number(d.capital).toFixed(2); - var badge = document.getElementById('risk-badge'); - if (badge && d.risk_status) badge.textContent = d.risk_status.status_label; - var ctpBadge = document.getElementById('ctp-badge'); - if (ctpBadge && d.ctp_status) { - ctpBadge.textContent = d.ctp_status.connected ? 'CTP 已连接' : 'CTP 未连接'; - ctpBadge.className = 'badge ' + (d.ctp_status.connected ? 'profit' : 'planned'); - } - }).catch(function () {}); - }, 5000); - - scheduleRefresh(); + document.addEventListener('DOMContentLoaded', function () { + pollPositions(); + pollTimer = setInterval(pollPositions, 2000); + }); })(); diff --git a/templates/base.html b/templates/base.html index cb8a385..39e54bf 100644 --- a/templates/base.html +++ b/templates/base.html @@ -483,12 +483,11 @@

国内期货 · 交易监控 + 复盘FUTURES MONITOR SYSTEM