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:
+96
-8
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user