Implement CTP-authoritative trading UI with event-driven state.

Add in-memory order/position books fed by CTP events, split active orders above positions in the UI, tick-triggered local SL/TP, and 30-second full calibration.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-26 18:12:11 +08:00
parent 4ef33a367f
commit 3a150dd3d6
8 changed files with 1056 additions and 210 deletions
+96 -8
View File
@@ -25,6 +25,7 @@ from vnpy_bridge import (
ctp_list_active_orders,
ctp_list_positions,
ctp_status,
ctp_account_margin_used,
execute_order,
get_bridge,
)
@@ -564,6 +565,13 @@ def reconcile_monitors_without_position(conn, mode: str, *, grace_sec: int = 120
"""持仓已平时:关闭监控并撤销残留止盈止损挂单(新开仓 grace_sec 内不清理)。"""
if not ctp_status(mode).get("connected"):
return 0
try:
bridge = get_bridge()
since_connect = time.time() - float(getattr(bridge, "_last_connect_ok_ts", 0) or 0)
if since_connect < 90:
return 0
except Exception:
pass
positions = ctp_list_positions(mode, refresh_if_empty=False, refresh_margin=False)
position_keys: set[tuple[str, str]] = set()
for p in positions:
@@ -573,14 +581,9 @@ def reconcile_monitors_without_position(conn, mode: str, *, grace_sec: int = 120
direction = p.get("direction") or "long"
position_keys.add((sym, direction))
if not position_keys:
try:
acc = get_bridge().get_account()
margin_used = float(acc.get("balance") or 0) - float(acc.get("available") or 0)
if margin_used > 500:
return 0
except Exception:
return 0
margin_used = ctp_account_margin_used(mode) or 0.0
if not position_keys and margin_used > 300:
return 0
now_ts = time.time()
@@ -669,6 +672,91 @@ def _execute_local_close(
logger.debug("SL/TP notify failed: %s", exc)
def check_sl_tp_on_tick(
conn,
mode: str,
exchange: str,
symbol: str,
mark: float,
*,
capital: float = 0.0,
notify_fn: Callable[[str], None] | None = None,
be_tick_mult: int = 2,
) -> int:
"""EVENT_TICK 触发:仅检查与 tick 品种匹配的 active 监控。"""
ensure_monitor_order_columns(conn)
if not ctp_status(mode).get("connected") or not is_trading_session():
return 0
if mark <= 0:
return 0
sym_l = (symbol or "").lower()
ex_u = (exchange or "").upper()
closed = 0
rows = [dict(r) for r in conn.execute(
"SELECT * FROM trade_order_monitors WHERE status='active'"
).fetchall()]
for mon in rows:
mid = int(mon.get("id") or 0)
ms = (mon.get("symbol") or "").strip()
direction = (mon.get("direction") or "long").strip().lower()
try:
vnpy_sym, ex2 = ths_to_vnpy_symbol(ms)
if sym_l != vnpy_sym.lower():
continue
if ex_u and ex2 and ex_u != ex2.upper():
continue
except Exception:
if sym_l != ms.lower():
continue
sl = mon.get("stop_loss")
tp = mon.get("take_profit")
try:
sl_f = float(sl) if sl is not None else None
tp_f = float(tp) if tp is not None else None
except (TypeError, ValueError):
sl_f, tp_f = None, None
if sl_f is None and tp_f is None:
continue
positions = ctp_list_positions(mode)
if not _find_position(positions, ms, direction):
continue
tick = _tick_size(ms)
if mon.get("trailing_be"):
mon = _update_trailing_stop_loss(conn, mon, mark, be_tick_mult=be_tick_mult)
try:
sl_f = float(mon["stop_loss"]) if mon.get("stop_loss") is not None else sl_f
except (TypeError, ValueError):
pass
reason = None
if tp_f is not None and _tp_triggered(direction, tp_f, mark, tick):
reason = "take_profit"
elif sl_f is not None and _sl_triggered(direction, sl_f, mark, tick):
reason = "stop_loss"
if not reason:
continue
if mid > 0 and not _can_close_now(mid):
continue
if not _try_acquire_close_symbol(ms, direction):
continue
try:
_execute_local_close(
conn, mon, mode=mode, mark=mark, reason=reason,
capital=capital, notify_fn=notify_fn,
)
if mid > 0:
_mark_close_attempt(mid)
closed += 1
except Exception as exc:
logger.warning("SL/TP tick close failed monitor=%s: %s", mid, exc)
finally:
_release_close_symbol(ms, direction)
return closed
def check_monitors_locally(
conn,
mode: str,