From 530738ae939d61706a0b4e2fbfae16160b502a90 Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 2 Jul 2026 22:12:04 +0800 Subject: [PATCH] Fix roll first-lots display and make market add use pending orders. Store initial_lots on roll groups, submit market roll as CTP pending legs with cancel closing empty groups, and backfill first-lots for existing active groups. Co-authored-by: Cursor --- modules/strategy/strategy_db.py | 2 + modules/strategy/strategy_roll_monitor_lib.py | 24 + modules/trading/install.py | 450 ++++++++++++++---- modules/web/static/js/strategy.js | 19 +- modules/web/templates/strategy.html | 2 +- 5 files changed, 392 insertions(+), 105 deletions(-) diff --git a/modules/strategy/strategy_db.py b/modules/strategy/strategy_db.py index a248166..d82d5bd 100644 --- a/modules/strategy/strategy_db.py +++ b/modules/strategy/strategy_db.py @@ -130,6 +130,8 @@ ROLL_LEG_EXTRA_COLUMNS = ( "ALTER TABLE roll_legs ADD COLUMN last_mark_price REAL", "ALTER TABLE roll_legs ADD COLUMN invalidated_reason TEXT", "ALTER TABLE roll_legs ADD COLUMN capital_snapshot REAL", + "ALTER TABLE roll_legs ADD COLUMN vt_order_id TEXT", + "ALTER TABLE roll_groups ADD COLUMN initial_lots INTEGER", "ALTER TABLE trade_order_monitors ADD COLUMN risk_percent REAL", ) diff --git a/modules/strategy/strategy_roll_monitor_lib.py b/modules/strategy/strategy_roll_monitor_lib.py index 193e46a..1bdec2f 100644 --- a/modules/strategy/strategy_roll_monitor_lib.py +++ b/modules/strategy/strategy_roll_monitor_lib.py @@ -50,6 +50,25 @@ def roll_sync_after_external_close(conn, *, monitor_id: int) -> None: ) +def close_roll_group_if_idle(conn, gid: int) -> None: + """无已成交腿且无 pending 腿时关闭滚仓组(如市价挂单撤单后)。""" + gid = int(gid) + filled = int(conn.execute( + "SELECT COUNT(*) AS n FROM roll_legs WHERE roll_group_id=? AND status=?", + (gid, LEG_STATUS_FILLED), + ).fetchone()["n"] or 0) + pending = int(conn.execute( + "SELECT COUNT(*) AS n FROM roll_legs WHERE roll_group_id=? AND status=?", + (gid, LEG_STATUS_PENDING), + ).fetchone()["n"] or 0) + if filled > 0 or pending > 0: + return + conn.execute( + "UPDATE roll_groups SET status='closed', updated_at=? WHERE id=? AND status='active'", + (_now(), gid), + ) + + def cancel_roll_leg(conn, leg_id: int) -> tuple[bool, str]: row = conn.execute( "SELECT * FROM roll_legs WHERE id=? AND status=?", @@ -57,10 +76,12 @@ def cancel_roll_leg(conn, leg_id: int) -> tuple[bool, str]: ).fetchone() if not row: return False, "仅可删除监控中的腿" + leg = dict(row) conn.execute( "UPDATE roll_legs SET status=? WHERE id=?", (LEG_STATUS_CANCELLED, int(leg_id)), ) + close_roll_group_if_idle(conn, int(leg["roll_group_id"])) return True, "已删除" @@ -89,6 +110,9 @@ def check_roll_monitors( ).fetchall() for raw in rows: leg = dict(raw) + mode = (leg.get("add_mode") or "").strip().lower() + if mode == ADD_MODE_MARKET: + continue if (leg.get("mon_status") or "").strip().lower() != "active": _invalidate_leg(conn, leg, "监控已结束") continue diff --git a/modules/trading/install.py b/modules/trading/install.py index c68af75..74302e3 100644 --- a/modules/trading/install.py +++ b/modules/trading/install.py @@ -93,6 +93,7 @@ from strategy.strategy_roll_lib import ( from strategy.strategy_roll_monitor_lib import ( cancel_roll_leg, check_roll_monitors, + close_roll_group_if_idle, roll_sync_after_external_close, ) from strategy.strategy_snapshot_lib import list_snapshots, save_snapshot @@ -1802,7 +1803,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se } def _reconcile_pending(conn, mode: str, *, capital: float = 0.0) -> dict[str, int]: - return reconcile_pending_orders( + stats = reconcile_pending_orders( conn, mode, match_symbol_fn=_match_ctp_symbol, @@ -1811,6 +1812,310 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se list_positions_fn=_ctp_positions, timeout_sec=get_pending_order_timeout_sec(get_setting), ) + roll_stats = _reconcile_roll_market_orders(conn, mode, capital=capital) + if roll_stats: + stats = {**stats, **{f"roll_{k}": v for k, v in roll_stats.items()}} + return stats + + def _roll_insert_group( + conn, + *, + mon: dict, + preview: dict, + initial_lots: int, + leg_count: int = 0, + ) -> int: + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + risk_budget = float(preview.get("risk_budget") or get_fixed_amount(get_setting)) + cur = conn.execute( + """INSERT INTO roll_groups ( + order_monitor_id, symbol, direction, initial_take_profit, initial_stop_loss, + current_stop_loss, risk_percent, leg_count, initial_lots, status, created_at, updated_at + ) VALUES (?,?,?,?,?,?,?,?,?,'active',?,?) RETURNING id""", + ( + int(mon["id"]), mon["symbol"], mon["direction"], mon["take_profit"], mon["stop_loss"], + preview["new_stop_loss"], risk_budget, int(leg_count), int(initial_lots), now, now, + ), + ) + row = cur.fetchone() + return int(row["id"] if isinstance(row, dict) else row[0]) + + def _finalize_roll_fill( + conn, + *, + mon: dict, + preview: dict, + add_mode: str, + pending_leg_id: Optional[int] = None, + fill_price: Optional[float] = None, + ) -> tuple[bool, str]: + sym = mon["symbol"] + mon_id = int(mon["id"]) + mode = get_trading_mode(get_setting) + price = float(fill_price if fill_price is not None else preview["add_price"]) + new_lots = int(mon["lots"]) + int(preview["add_lots"]) + new_avg = preview["avg_entry_after"] + new_sl = preview["new_stop_loss"] + conn.execute( + "UPDATE trade_order_monitors SET lots=?, entry_price=?, stop_loss=? WHERE id=?", + (new_lots, new_avg, new_sl, mon_id), + ) + grp = _roll_group_for_monitor(conn, mon_id) + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if grp: + gid = int(grp["id"]) + leg_n = int(grp["leg_count"] or 0) + 1 + conn.execute( + "UPDATE roll_groups SET leg_count=?, current_stop_loss=?, updated_at=? WHERE id=?", + (leg_n, new_sl, now, gid), + ) + else: + gid = _roll_insert_group( + conn, + mon=mon, + preview=preview, + initial_lots=max(1, int(mon["lots"]) - int(preview["add_lots"])), + leg_count=1, + ) + leg_n = 1 + if pending_leg_id: + conn.execute( + """UPDATE roll_legs SET status=?, fill_price=?, lots=?, new_stop_loss=?, created_at=? + WHERE id=?""", + ( + LEG_STATUS_FILLED, price, int(preview["add_lots"]), new_sl, now, + int(pending_leg_id), + ), + ) + else: + conn.execute( + """INSERT INTO roll_legs ( + roll_group_id, leg_index, add_mode, fill_price, lots, new_stop_loss, + status, created_at, limit_price, breakthrough_price, last_mark_price, capital_snapshot + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""", + ( + gid, leg_n, add_mode, price, int(preview["add_lots"]), new_sl, + LEG_STATUS_FILLED, now, + preview.get("limit_price"), preview.get("breakthrough_price"), + preview.get("mark_price"), _capital(conn), + ), + ) + conn.commit() + send_wechat_msg( + f"滚仓成交 {sym} {add_mode_label(add_mode)} +{preview['add_lots']}手 " + f"新止损 {new_sl} 合计 {new_lots}手" + ) + _schedule_roll_entry_sync(mon_id, sym, mon["direction"], mode) + return True, "成交" + + def _commit_roll_fill( + conn, + *, + mon: dict, + preview: dict, + add_mode: str, + mode: str, + pending_leg_id: Optional[int] = None, + ) -> tuple[bool, str]: + sym = mon["symbol"] + price = float(preview["add_price"]) + try: + execute_order( + conn, mode=mode, offset="open", symbol=sym, + direction=mon["direction"], lots=int(preview["add_lots"]), price=price, + settings=_settings_dict(), + order_type="market", + ) + except ValueError as exc: + return False, str(exc) + return _finalize_roll_fill( + conn, + mon=mon, + preview=preview, + add_mode=add_mode, + pending_leg_id=pending_leg_id, + fill_price=price, + ) + + def _submit_roll_market_order( + conn, + *, + mon: dict, + preview: dict, + mode: str, + ) -> tuple[bool, str]: + mon_id = int(mon["id"]) + initial_lots = int(mon.get("lots") or 0) + if initial_lots <= 0: + return False, "持仓手数为 0" + grp = _roll_group_for_monitor(conn, mon_id) + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if grp: + gid = int(grp["id"]) + else: + gid = _roll_insert_group( + conn, mon=mon, preview=preview, initial_lots=initial_lots, leg_count=0, + ) + pending_n = conn.execute( + "SELECT COUNT(*) AS n FROM roll_legs WHERE roll_group_id=? AND status=?", + (gid, LEG_STATUS_PENDING), + ).fetchone()["n"] + if int(pending_n or 0) > 0: + return False, "已有监控中的加仓腿" + try: + result = execute_order( + conn, + mode=mode, + offset="open", + symbol=mon["symbol"], + direction=mon["direction"], + lots=int(preview["add_lots"]), + price=float(preview["add_price"]), + settings=_settings_dict(), + order_type="market", + ) + vt_order_id = str(result.get("order_id") or "") + except ValueError as exc: + close_roll_group_if_idle(conn, gid) + return False, str(exc) + leg_n = int(conn.execute( + "SELECT COUNT(*) AS n FROM roll_legs WHERE roll_group_id=? AND status=?", + (gid, LEG_STATUS_FILLED), + ).fetchone()["n"]) + 1 + conn.execute( + """INSERT INTO roll_legs ( + roll_group_id, leg_index, add_mode, lots, new_stop_loss, status, created_at, + limit_price, breakthrough_price, last_mark_price, capital_snapshot, vt_order_id + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""", + ( + gid, leg_n, ADD_MODE_MARKET, int(preview["add_lots"]), preview["new_stop_loss"], + LEG_STATUS_PENDING, now, + preview.get("limit_price"), preview.get("breakthrough_price"), + preview.get("mark_price"), _capital(conn), vt_order_id or None, + ), + ) + conn.commit() + _reconcile_roll_market_orders(conn, mode, capital=_capital(conn)) + return True, "市价加仓委托已提交,成交后自动更新;撤单后滚仓组将消失" + + def _reconcile_roll_market_orders( + conn, mode: str, *, capital: float = 0.0, + ) -> dict[str, int]: + """同步市价滚仓 pending 腿:成交→入账;撤单/拒单→删除腿并关闭空组。""" + from modules.trading.order_pending import PENDING_ORDER_SETTLE_GRACE_SEC + + stats = {"filled": 0, "cancelled": 0} + if not ctp_status(mode).get("connected"): + return stats + rows = conn.execute( + """SELECT l.*, g.order_monitor_id, g.symbol, g.direction, g.initial_take_profit, + g.leg_count AS group_leg_count, + m.lots AS mon_lots, m.entry_price AS mon_entry, m.take_profit AS mon_tp, + m.status AS mon_status + FROM roll_legs l + JOIN roll_groups g ON g.id = l.roll_group_id + JOIN trade_order_monitors m ON m.id = g.order_monitor_id + WHERE l.status=? AND l.add_mode=? AND g.status='active'""", + (LEG_STATUS_PENDING, ADD_MODE_MARKET), + ).fetchall() + if not rows: + return stats + positions = list(_ctp_positions(mode, refresh_if_empty=False) or []) + try: + active_orders = { + str(k): o + for o in (ctp_list_active_orders(mode) or []) + for k in (o.get("order_id"), o.get("vt_order_id")) + if k + } + except Exception: + active_orders = {} + now_ts = time.time() + for raw in rows: + leg = dict(raw) + if (leg.get("mon_status") or "").strip().lower() != "active": + continue + sym = (leg.get("symbol") or "").strip() + direction = (leg.get("direction") or "long").strip().lower() + mon = { + "id": leg["order_monitor_id"], + "symbol": sym, + "direction": direction, + "lots": leg["mon_lots"], + "entry_price": leg["mon_entry"], + "take_profit": leg["mon_tp"] or leg["initial_take_profit"], + "stop_loss": leg.get("new_stop_loss"), + } + leg_lots = int(leg.get("lots") or 0) + mon_lots = int(leg.get("mon_lots") or 0) + pos = None + for p in positions: + if int(p.get("lots") or 0) <= 0: + continue + if (p.get("direction") or "long").strip().lower() != direction: + continue + if _match_ctp_symbol(p.get("symbol") or "", sym): + pos = p + break + pos_lots = int(pos.get("lots") or 0) if pos else mon_lots + vt_oid = (leg.get("vt_order_id") or "").strip() + if pos_lots >= mon_lots + leg_lots and leg_lots > 0: + mark = _roll_mark_price(sym, mon, mode, allow_ctp=True) + preview, err = preview_roll( + direction=direction, + symbol=sym, + qty_existing=float(mon_lots), + entry_existing=float(mon.get("entry_price") or 0), + initial_take_profit=float(mon.get("take_profit") or 0), + add_mode=ADD_MODE_MARKET, + new_stop_loss=float(leg.get("new_stop_loss") or 0), + risk_budget=float(leg.get("capital_snapshot") or get_fixed_amount(get_setting)), + mult=int(get_contract_spec(sym).get("mult") or 1), + mark_price=mark, + add_price=mark, + legs_done=int(leg.get("group_leg_count") or 0), + ) + if err or not preview: + continue + preview["add_lots"] = leg_lots + ok, _ = _finalize_roll_fill( + conn, + mon=mon, + preview=preview, + add_mode=ADD_MODE_MARKET, + pending_leg_id=int(leg["id"]), + fill_price=mark, + ) + if ok: + stats["filled"] += 1 + continue + if vt_oid and vt_oid in active_orders: + continue + created = (leg.get("created_at") or "").strip() + age = 9999.0 + if created: + for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"): + try: + age = now_ts - datetime.strptime(created[:19], fmt).timestamp() + break + except ValueError: + continue + if vt_oid and age < PENDING_ORDER_SETTLE_GRACE_SEC: + continue + if vt_oid: + try: + ctp_cancel_order(mode, vt_oid) + except Exception: + pass + conn.execute( + "UPDATE roll_legs SET status=? WHERE id=?", + (LEG_STATUS_CANCELLED, int(leg["id"])), + ) + close_roll_group_if_idle(conn, int(leg["roll_group_id"])) + stats["cancelled"] += 1 + if stats["filled"] or stats["cancelled"]: + conn.commit() + return stats def _build_active_orders( conn, @@ -3043,7 +3348,13 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se gid = int(out.get("id") or 0) filled_add_lots = int(filled_map.get(gid) or 0) out["add_lots_filled"] = filled_add_lots - out["first_lots"] = max(0, int(lots) - filled_add_lots) + stored_initial = int(out.get("initial_lots") or 0) + if stored_initial > 0: + out["first_lots"] = stored_initial + elif int(lots) > 0 and filled_add_lots > 0 and int(lots) <= filled_add_lots: + out["first_lots"] = int(lots) + else: + out["first_lots"] = max(0, int(lots) - filled_add_lots) out["total_lots"] = int(lots) out["avg_entry"] = round(entry, 4) if entry > 0 else None if lots > 0 and entry > 0 and tp > 0: @@ -3239,6 +3550,27 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se capital = _capital(conn) mode = get_trading_mode(get_setting) _ensure_strategy_monitors(conn, mode) + for grow in conn.execute( + "SELECT id, order_monitor_id, initial_lots FROM roll_groups WHERE status='active'" + ).fetchall(): + gfix = dict(grow) + if int(gfix.get("initial_lots") or 0) > 0: + continue + mon_row = conn.execute( + "SELECT lots FROM trade_order_monitors WHERE id=?", + (int(gfix["order_monitor_id"]),), + ).fetchone() + if not mon_row: + continue + gid = int(gfix["id"]) + filled = _roll_filled_lots_map(conn, [gid]).get(gid, 0) + lots = int(mon_row["lots"] or 0) + initial = lots if lots > 0 and filled > 0 and lots <= filled else max(1, lots - filled) + conn.execute( + "UPDATE roll_groups SET initial_lots=? WHERE id=?", + (initial, gid), + ) + commit_retry(conn) active_trend = conn.execute( "SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC LIMIT 1" ).fetchone() @@ -4085,87 +4417,6 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se return None, merr return preview, None - def _commit_roll_fill( - conn, - *, - mon: dict, - preview: dict, - add_mode: str, - mode: str, - pending_leg_id: Optional[int] = None, - ) -> tuple[bool, str]: - sym = mon["symbol"] - mon_id = int(mon["id"]) - price = float(preview["add_price"]) - try: - execute_order( - conn, mode=mode, offset="open", symbol=sym, - direction=mon["direction"], lots=int(preview["add_lots"]), price=price, - settings=_settings_dict(), - ) - except ValueError as exc: - return False, str(exc) - new_lots = int(mon["lots"]) + int(preview["add_lots"]) - new_avg = preview["avg_entry_after"] - new_sl = preview["new_stop_loss"] - conn.execute( - "UPDATE trade_order_monitors SET lots=?, entry_price=?, stop_loss=? WHERE id=?", - (new_lots, new_avg, new_sl, mon_id), - ) - grp = _roll_group_for_monitor(conn, mon_id) - now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - risk_budget = float(preview.get("risk_budget") or get_fixed_amount(get_setting)) - if grp: - gid = int(grp["id"]) - leg_n = int(grp["leg_count"] or 0) + 1 - conn.execute( - "UPDATE roll_groups SET leg_count=?, current_stop_loss=?, updated_at=? WHERE id=?", - (leg_n, new_sl, now, gid), - ) - else: - cur = conn.execute( - """INSERT INTO roll_groups ( - order_monitor_id, symbol, direction, initial_take_profit, initial_stop_loss, - current_stop_loss, risk_percent, leg_count, status, created_at, updated_at - ) VALUES (?,?,?,?,?,?,?,1,'active',?,?) RETURNING id""", - ( - mon_id, sym, mon["direction"], mon["take_profit"], mon["stop_loss"], - new_sl, risk_budget, now, now, - ), - ) - row = cur.fetchone() - gid = int(row["id"] if isinstance(row, dict) else row[0]) - leg_n = 1 - if pending_leg_id: - conn.execute( - """UPDATE roll_legs SET status=?, fill_price=?, lots=?, new_stop_loss=?, created_at=? - WHERE id=?""", - ( - LEG_STATUS_FILLED, price, int(preview["add_lots"]), new_sl, now, - int(pending_leg_id), - ), - ) - else: - conn.execute( - """INSERT INTO roll_legs ( - roll_group_id, leg_index, add_mode, fill_price, lots, new_stop_loss, - status, created_at, limit_price, breakthrough_price, last_mark_price, capital_snapshot - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""", - ( - gid, leg_n, add_mode, price, int(preview["add_lots"]), new_sl, - LEG_STATUS_FILLED, now, - preview.get("limit_price"), preview.get("breakthrough_price"), - preview.get("mark_price"), _capital(conn), - ), - ) - conn.commit() - send_wechat_msg( - f"滚仓成交 {sym} {add_mode_label(add_mode)} +{preview['add_lots']}手 " - f"新止损 {new_sl} 合计 {new_lots}手" - ) - _schedule_roll_entry_sync(mon_id, sym, mon["direction"], mode) - return True, "成交" - def _schedule_roll_entry_sync( mon_id: int, sym: str, direction: str, mode: str, ) -> None: @@ -4209,25 +4460,16 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se add_mode: str, ) -> tuple[bool, str]: mon_id = int(mon["id"]) + initial_lots = int(mon.get("lots") or 0) grp = _roll_group_for_monitor(conn, mon_id) now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") capital = _capital(conn) - risk_budget = float(preview.get("risk_budget") or get_fixed_amount(get_setting)) if grp: gid = int(grp["id"]) else: - cur = conn.execute( - """INSERT INTO roll_groups ( - order_monitor_id, symbol, direction, initial_take_profit, initial_stop_loss, - current_stop_loss, risk_percent, leg_count, status, created_at, updated_at - ) VALUES (?,?,?,?,?,?,?,0,'active',?,?) RETURNING id""", - ( - mon_id, mon["symbol"], mon["direction"], mon["take_profit"], mon["stop_loss"], - preview["new_stop_loss"], risk_budget, now, now, - ), + gid = _roll_insert_group( + conn, mon=mon, preview=preview, initial_lots=initial_lots, leg_count=0, ) - row = cur.fetchone() - gid = int(row["id"] if isinstance(row, dict) else row[0]) leg_n = int(conn.execute( "SELECT COUNT(*) AS n FROM roll_legs WHERE roll_group_id=? AND status=?", (gid, LEG_STATUS_FILLED), @@ -4403,6 +4645,12 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se if not _cached_ctp_status(mode).get("connected"): conn.close() return jsonify({"ok": False, "error": "请先连接 CTP"}), 400 + if add_mode == ADD_MODE_MARKET: + ok, msg = _submit_roll_market_order(conn, mon=mon_d, preview=preview, mode=mode) + conn.close() + if not ok: + return jsonify({"ok": False, "error": msg}), 400 + return jsonify({"ok": True, "message": msg, "pending": True}) ok, msg = _commit_roll_fill( conn, mon=mon_d, preview=preview, add_mode=add_mode, mode=mode, ) @@ -4416,6 +4664,18 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se def api_roll_cancel(leg_id: int): conn = get_db() init_strategy_tables(conn) + mode = get_trading_mode(get_setting) + leg = conn.execute( + "SELECT * FROM roll_legs WHERE id=? AND status=?", + (int(leg_id), LEG_STATUS_PENDING), + ).fetchone() + if leg: + vt_oid = (dict(leg).get("vt_order_id") or "").strip() + if vt_oid and ctp_status(mode).get("connected"): + try: + ctp_cancel_order(mode, vt_oid) + except Exception as exc: + logger.debug("roll leg cancel order: %s", exc) ok, msg = cancel_roll_leg(conn, leg_id) if ok: conn.commit() diff --git a/modules/web/static/js/strategy.js b/modules/web/static/js/strategy.js index 9fa5a9a..da0c494 100644 --- a/modules/web/static/js/strategy.js +++ b/modules/web/static/js/strategy.js @@ -119,7 +119,7 @@ } if (execHint) execHint.hidden = false; if (btnExec) { - btnExec.textContent = mode === 'market' ? '执行滚仓' : '提交监控'; + btnExec.textContent = mode === 'market' ? '提交委托' : '提交监控'; } if (btnPreview) { btnPreview.disabled = false; @@ -257,21 +257,22 @@ btnRollE.addEventListener('click', function () { var payload = rollPayload || formData(rollForm); var mode = (payload.add_mode || 'market'); - if (mode === 'market') { - if (!inTradingSession) { - alert('休盘期间请切换为「突破加仓」后提交监控'); - return; - } - if (!confirm('确认执行市价滚仓?')) return; - startRollCountdown(btnRollE, payload); + if (mode === 'market' && !inTradingSession) { + alert('休盘期间请切换为「突破加仓」后提交监控'); return; } + var confirmMsg = mode === 'market' + ? '确认提交市价加仓委托?可在「正在滚仓」中撤单。' + : '确认提交突破加仓监控?'; + if (!confirm(confirmMsg)) return; btnRollE.disabled = true; btnRollE.textContent = '提交中…'; jsonPost('/api/strategy/roll/execute', payload).then(function (d) { if (!d.ok) { alert(d.error || '失败'); return; } - alert(d.message || '已提交监控'); + alert(d.message || (mode === 'market' ? '委托已提交' : '已提交监控')); location.reload(); + }).catch(function () { + alert('提交失败,请稍后重试'); }).finally(function () { btnRollE.disabled = false; syncRollModeUi(); diff --git a/modules/web/templates/strategy.html b/modules/web/templates/strategy.html index 476b9d5..ee77095 100644 --- a/modules/web/templates/strategy.html +++ b/modules/web/templates/strategy.html @@ -139,7 +139,7 @@ - + {% if not trading_session %}

当前{{ session_clock.status_label or '休盘' }}:市价加仓须交易时段内执行;休盘可先预览,或选「突破加仓」提交监控。

{% else %}