Fix post-close UI: persist closing state in DB, defer trade log until CTP fill.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+14
-31
@@ -66,6 +66,7 @@ from modules.trading.sl_tp_guard import (
|
|||||||
monitor_order_status,
|
monitor_order_status,
|
||||||
monitor_source_label,
|
monitor_source_label,
|
||||||
place_monitor_exit_orders,
|
place_monitor_exit_orders,
|
||||||
|
position_close_in_progress,
|
||||||
reconcile_monitors_without_position,
|
reconcile_monitors_without_position,
|
||||||
should_skip_monitor_revive,
|
should_skip_monitor_revive,
|
||||||
start_sl_tp_guard_worker,
|
start_sl_tp_guard_worker,
|
||||||
@@ -1234,8 +1235,6 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
break
|
break
|
||||||
if should_skip_monitor_revive(symbol, direction) and not ctp_still_open:
|
if should_skip_monitor_revive(symbol, direction) and not ctp_still_open:
|
||||||
return None
|
return None
|
||||||
if ctp_still_open and close_pending_active(symbol, direction):
|
|
||||||
clear_close_pending(symbol, direction)
|
|
||||||
for r in conn.execute(
|
for r in conn.execute(
|
||||||
"SELECT * FROM trade_order_monitors WHERE status='closed' ORDER BY id DESC LIMIT 40"
|
"SELECT * FROM trade_order_monitors WHERE status='closed' ORDER BY id DESC LIMIT 40"
|
||||||
).fetchall():
|
).fetchall():
|
||||||
@@ -2447,12 +2446,12 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
direction = p.get("direction") or "long"
|
direction = p.get("direction") or "long"
|
||||||
if not mon:
|
if not mon:
|
||||||
mon = _find_pending_monitor(conn, ths, direction)
|
mon = _find_pending_monitor(conn, ths, direction)
|
||||||
if not mon and not close_pending_active(ths, direction):
|
if not mon and not position_close_in_progress(conn, mode, ths, direction):
|
||||||
if fast:
|
if fast:
|
||||||
mon = _find_active_monitor(conn, ths, direction)
|
mon = _find_active_monitor(conn, ths, direction)
|
||||||
else:
|
else:
|
||||||
mon = _find_or_revive_monitor(conn, ths, direction)
|
mon = _find_or_revive_monitor(conn, ths, direction)
|
||||||
if mon and not close_pending_active(ths, direction):
|
if mon and not position_close_in_progress(conn, mode, ths, direction):
|
||||||
if fast:
|
if fast:
|
||||||
mon = _overlay_sl_tp_readonly(conn, mon, ths, direction) or mon
|
mon = _overlay_sl_tp_readonly(conn, mon, ths, direction) or mon
|
||||||
else:
|
else:
|
||||||
@@ -2466,9 +2465,9 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
conn, mon.get("symbol") or ths, mon.get("direction") or direction,
|
conn, mon.get("symbol") or ths, mon.get("direction") or direction,
|
||||||
) or mon
|
) or mon
|
||||||
mon = _restore_monitor_sl_tp_if_missing(conn, mon, ths, direction) or mon
|
mon = _restore_monitor_sl_tp_if_missing(conn, mon, ths, direction) or mon
|
||||||
elif fast and not close_pending_active(ths, direction):
|
elif fast and not position_close_in_progress(conn, mode, ths, direction):
|
||||||
mon = _overlay_sl_tp_readonly(conn, None, ths, direction)
|
mon = _overlay_sl_tp_readonly(conn, None, ths, direction)
|
||||||
elif close_pending_active(ths, direction):
|
elif position_close_in_progress(conn, mode, ths, direction):
|
||||||
mon = mon or {"symbol": ths, "direction": direction}
|
mon = mon or {"symbol": ths, "direction": direction}
|
||||||
try:
|
try:
|
||||||
row = _compose_position_row(
|
row = _compose_position_row(
|
||||||
@@ -2476,7 +2475,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
now_iso=now_iso, fast=fast,
|
now_iso=now_iso, fast=fast,
|
||||||
)
|
)
|
||||||
if row:
|
if row:
|
||||||
if close_pending_active(ths, direction):
|
if position_close_in_progress(conn, mode, ths, direction):
|
||||||
row = _row_as_closing_state(row)
|
row = _row_as_closing_state(row)
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -3404,14 +3403,9 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
if not sym or price <= 0:
|
if not sym or price <= 0:
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({"ok": False, "error": "品种或价格无效"}), 400
|
return jsonify({"ok": False, "error": "品种或价格无效"}), 400
|
||||||
if close_pending_active(sym, direction):
|
if close_pending_active(sym, direction, conn=conn):
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({"ok": False, "error": "平仓处理中,请稍候查看柜台委托"}), 400
|
return jsonify({"ok": False, "error": "平仓处理中,请稍候查看柜台委托"}), 400
|
||||||
from modules.trading.sl_tp_guard import _has_pending_close_order
|
|
||||||
if _has_pending_close_order(mode, sym, direction):
|
|
||||||
mark_close_pending(sym, direction)
|
|
||||||
conn.close()
|
|
||||||
return jsonify({"ok": False, "error": "已有平仓委托在柜台排队,请勿重复提交"}), 400
|
|
||||||
offset = "close_long" if direction == "long" else "close_short"
|
offset = "close_long" if direction == "long" else "close_short"
|
||||||
capital = _capital(conn)
|
capital = _capital(conn)
|
||||||
mon = None
|
mon = None
|
||||||
@@ -3434,6 +3428,12 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
mon = row
|
mon = row
|
||||||
mid = int(row["id"])
|
mid = int(row["id"])
|
||||||
break
|
break
|
||||||
|
from modules.trading.sl_tp_guard import _has_pending_close_order
|
||||||
|
if _has_pending_close_order(mode, sym, direction):
|
||||||
|
mark_close_pending(sym, direction, conn=conn, monitor_id=mid)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return jsonify({"ok": False, "error": "已有平仓委托在柜台排队,请勿重复提交"}), 400
|
||||||
entry = float(mon.get("entry_price") or 0) if mon else 0.0
|
entry = float(mon.get("entry_price") or 0) if mon else 0.0
|
||||||
if entry <= 0:
|
if entry <= 0:
|
||||||
for p in _ctp_positions(mode):
|
for p in _ctp_positions(mode):
|
||||||
@@ -3450,24 +3450,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
lots=lots, price=price, settings=_settings_dict(),
|
lots=lots, price=price, settings=_settings_dict(),
|
||||||
order_type="market", urgency="stop_loss",
|
order_type="market", urgency="stop_loss",
|
||||||
)
|
)
|
||||||
mark_close_pending(sym, direction)
|
mark_close_pending(sym, direction, conn=conn, monitor_id=mid)
|
||||||
# 始终写本地记录:CTP 同步依赖内存开平配对,重启后或成交回报延迟时会漏记
|
|
||||||
write_manual_close_trade_log(
|
|
||||||
conn,
|
|
||||||
mon,
|
|
||||||
symbol=sym,
|
|
||||||
direction=direction,
|
|
||||||
lots=lots,
|
|
||||||
close_price=price,
|
|
||||||
entry_price=entry or price,
|
|
||||||
trading_mode=mode,
|
|
||||||
capital=capital,
|
|
||||||
stop_loss=float(mon["stop_loss"]) if mon and mon.get("stop_loss") is not None else None,
|
|
||||||
take_profit=float(mon["take_profit"]) if mon and mon.get("take_profit") is not None else None,
|
|
||||||
open_time=(mon.get("open_time") or "") if mon else "",
|
|
||||||
symbol_name=(mon.get("symbol_name") or "") if mon else "",
|
|
||||||
market_code=(mon.get("market_code") or "") if mon else "",
|
|
||||||
)
|
|
||||||
if mon:
|
if mon:
|
||||||
cancel_monitor_exit_orders(conn, mon, mode=mode)
|
cancel_monitor_exit_orders(conn, mon, mode=mode)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ MONITOR_ORDER_COLUMNS = (
|
|||||||
"ALTER TABLE trade_order_monitors ADD COLUMN vt_order_id TEXT",
|
"ALTER TABLE trade_order_monitors ADD COLUMN vt_order_id TEXT",
|
||||||
"ALTER TABLE trade_order_monitors ADD COLUMN order_price REAL",
|
"ALTER TABLE trade_order_monitors ADD COLUMN order_price REAL",
|
||||||
"ALTER TABLE trade_order_monitors ADD COLUMN open_fee REAL",
|
"ALTER TABLE trade_order_monitors ADD COLUMN open_fee REAL",
|
||||||
|
"ALTER TABLE trade_order_monitors ADD COLUMN close_pending_until REAL",
|
||||||
)
|
)
|
||||||
|
|
||||||
TRADE_RESULTS = ("止损", "止盈", "移动止盈", "保本止盈", "手动平仓")
|
TRADE_RESULTS = ("止损", "止盈", "移动止盈", "保本止盈", "手动平仓")
|
||||||
@@ -77,7 +78,7 @@ def _monitor_columns_exist(conn) -> bool:
|
|||||||
cols.add(r.get("name") or "")
|
cols.add(r.get("name") or "")
|
||||||
else:
|
else:
|
||||||
cols.add(r[1])
|
cols.add(r[1])
|
||||||
return "open_fee" in cols
|
return "close_pending_until" in cols
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -181,27 +182,109 @@ def _position_key(sym: str, direction: str) -> str:
|
|||||||
return f"{(sym or '').strip().lower()}|{(direction or 'long').strip().lower()}"
|
return f"{(sym or '').strip().lower()}|{(direction or 'long').strip().lower()}"
|
||||||
|
|
||||||
|
|
||||||
def mark_close_pending(sym: str, direction: str, *, secs: int = CLOSE_PENDING_SEC) -> None:
|
def mark_close_pending(
|
||||||
|
sym: str,
|
||||||
|
direction: str,
|
||||||
|
*,
|
||||||
|
secs: int = CLOSE_PENDING_SEC,
|
||||||
|
conn=None,
|
||||||
|
monitor_id: int = 0,
|
||||||
|
) -> None:
|
||||||
key = _position_key(sym, direction)
|
key = _position_key(sym, direction)
|
||||||
|
until = time.time() + max(30, int(secs))
|
||||||
with _closing_lock:
|
with _closing_lock:
|
||||||
_close_pending_until[key] = time.time() + max(30, int(secs))
|
_close_pending_until[key] = until
|
||||||
|
if conn is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
mid = int(monitor_id or 0)
|
||||||
|
if mid > 0:
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE trade_order_monitors SET close_pending_until=? WHERE id=?",
|
||||||
|
(until, mid),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
direction = (direction or "long").strip().lower()
|
||||||
|
for r in conn.execute(
|
||||||
|
"SELECT id, symbol, direction FROM trade_order_monitors "
|
||||||
|
"WHERE status IN ('active', 'closed')"
|
||||||
|
):
|
||||||
|
if (r["direction"] or "long").strip().lower() != direction:
|
||||||
|
continue
|
||||||
|
if _match_symbol(sym, r["symbol"] or ""):
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE trade_order_monitors SET close_pending_until=? WHERE id=?",
|
||||||
|
(until, int(r["id"])),
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("persist close_pending: %s", exc)
|
||||||
|
|
||||||
|
|
||||||
def clear_close_pending(sym: str, direction: str) -> None:
|
def clear_close_pending(sym: str, direction: str, conn=None) -> None:
|
||||||
key = _position_key(sym, direction)
|
key = _position_key(sym, direction)
|
||||||
with _closing_lock:
|
with _closing_lock:
|
||||||
_close_pending_until.pop(key, None)
|
_close_pending_until.pop(key, None)
|
||||||
|
if conn is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
direction = (direction or "long").strip().lower()
|
||||||
|
for r in conn.execute(
|
||||||
|
"SELECT id, symbol, direction FROM trade_order_monitors "
|
||||||
|
"WHERE close_pending_until IS NOT NULL"
|
||||||
|
):
|
||||||
|
if (r["direction"] or "long").strip().lower() != direction:
|
||||||
|
continue
|
||||||
|
if _match_symbol(sym, r["symbol"] or ""):
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE trade_order_monitors SET close_pending_until=NULL WHERE id=?",
|
||||||
|
(int(r["id"]),),
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("clear close_pending db: %s", exc)
|
||||||
|
|
||||||
|
|
||||||
def close_pending_active(sym: str, direction: str) -> bool:
|
def close_pending_active(sym: str, direction: str, conn=None) -> bool:
|
||||||
key = _position_key(sym, direction)
|
key = _position_key(sym, direction)
|
||||||
|
now = time.time()
|
||||||
with _closing_lock:
|
with _closing_lock:
|
||||||
until = float(_close_pending_until.get(key) or 0)
|
until = float(_close_pending_until.get(key) or 0)
|
||||||
if until > time.time():
|
if until > now:
|
||||||
return True
|
return True
|
||||||
if until:
|
if until:
|
||||||
_close_pending_until.pop(key, None)
|
_close_pending_until.pop(key, None)
|
||||||
|
if conn is None:
|
||||||
return False
|
return False
|
||||||
|
try:
|
||||||
|
direction = (direction or "long").strip().lower()
|
||||||
|
for r in conn.execute(
|
||||||
|
"SELECT id, symbol, direction, close_pending_until FROM trade_order_monitors "
|
||||||
|
"WHERE close_pending_until IS NOT NULL AND close_pending_until > ?",
|
||||||
|
(now,),
|
||||||
|
):
|
||||||
|
if (r["direction"] or "long").strip().lower() != direction:
|
||||||
|
continue
|
||||||
|
if not _match_symbol(sym, r["symbol"] or ""):
|
||||||
|
continue
|
||||||
|
db_until = float(r["close_pending_until"] or 0)
|
||||||
|
if db_until > now:
|
||||||
|
with _closing_lock:
|
||||||
|
_close_pending_until[key] = db_until
|
||||||
|
return True
|
||||||
|
except Exception as exc:
|
||||||
|
logger.debug("read close_pending db: %s", exc)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def position_close_in_progress(
|
||||||
|
conn,
|
||||||
|
mode: str,
|
||||||
|
sym: str,
|
||||||
|
direction: str,
|
||||||
|
) -> bool:
|
||||||
|
"""平仓委托已提交或柜台仍有平仓挂单(含进程重启后)。"""
|
||||||
|
if close_pending_active(sym, direction, conn=conn):
|
||||||
|
return True
|
||||||
|
return _has_pending_close_order(mode, sym, direction)
|
||||||
|
|
||||||
|
|
||||||
def should_skip_monitor_revive(sym: str, direction: str) -> bool:
|
def should_skip_monitor_revive(sym: str, direction: str) -> bool:
|
||||||
@@ -804,7 +887,7 @@ def reconcile_monitors_without_position(conn, mode: str, *, grace_sec: int = 120
|
|||||||
cancel_monitor_exit_orders(conn, mon, mode=mode)
|
cancel_monitor_exit_orders(conn, mon, mode=mode)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("cancel exit orders monitor=%s: %s", mon.get("id"), exc)
|
logger.warning("cancel exit orders monitor=%s: %s", mon.get("id"), exc)
|
||||||
clear_close_pending(ms, md)
|
clear_close_pending(ms, md, conn=conn)
|
||||||
conn.execute("UPDATE trade_order_monitors SET status='closed' WHERE id=?", (mon["id"],))
|
conn.execute("UPDATE trade_order_monitors SET status='closed' WHERE id=?", (mon["id"],))
|
||||||
closed += 1
|
closed += 1
|
||||||
if closed:
|
if closed:
|
||||||
@@ -843,12 +926,12 @@ def _execute_local_close(
|
|||||||
float(margin_raw),
|
float(margin_raw),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
clear_close_pending(sym, direction)
|
clear_close_pending(sym, direction, conn=conn)
|
||||||
_close_all_monitors_for_symbol(conn, sym, direction)
|
_close_all_monitors_for_symbol(conn, sym, direction)
|
||||||
reconcile_monitors_without_position(conn, mode)
|
reconcile_monitors_without_position(conn, mode)
|
||||||
return
|
return
|
||||||
if _has_pending_close_order(mode, sym, direction):
|
if _has_pending_close_order(mode, sym, direction):
|
||||||
mark_close_pending(sym, direction)
|
mark_close_pending(sym, direction, conn=conn, monitor_id=int(mon.get("id") or 0))
|
||||||
cancel_monitor_exit_orders(conn, mon, mode=mode)
|
cancel_monitor_exit_orders(conn, mon, mode=mode)
|
||||||
return
|
return
|
||||||
lots = int(pos.get("lots") or mon.get("lots") or 1)
|
lots = int(pos.get("lots") or mon.get("lots") or 1)
|
||||||
@@ -865,7 +948,9 @@ def _execute_local_close(
|
|||||||
order_type="market",
|
order_type="market",
|
||||||
urgency="stop_loss",
|
urgency="stop_loss",
|
||||||
)
|
)
|
||||||
mark_close_pending(sym, direction)
|
mark_close_pending(
|
||||||
|
sym, direction, conn=conn, monitor_id=int(mon.get("id") or 0),
|
||||||
|
)
|
||||||
_close_all_monitors_for_symbol(conn, sym, direction)
|
_close_all_monitors_for_symbol(conn, sym, direction)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
result_label = _result_for_close(mon, reason)
|
result_label = _result_for_close(mon, reason)
|
||||||
|
|||||||
Reference in New Issue
Block a user