Fix UI stuck after manual close: fast API return and closing state instead of SL/TP revive.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-03 21:54:35 +08:00
parent b888a670b6
commit f2bd76d970
2 changed files with 83 additions and 9 deletions
+55 -8
View File
@@ -59,6 +59,7 @@ from modules.trading.order_pending import (
from modules.core.db_conn import commit_retry, execute_retry
from modules.trading.sl_tp_guard import (
cancel_monitor_exit_orders,
clear_close_pending,
close_pending_active,
ensure_monitor_order_columns,
mark_close_pending,
@@ -1170,6 +1171,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
direction: str,
) -> Optional[dict]:
"""只读:从已关闭监控补全止盈止损,不写库。"""
if should_skip_monitor_revive(sym, direction):
return {"symbol": sym, "direction": direction}
if not mon:
rsl, rtp, rtrail, rinitial = _restore_sl_tp_from_closed(conn, sym, direction)
if rsl is None and rtp is None and not rtrail:
@@ -1197,6 +1200,24 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
merged["initial_stop_loss"] = rinitial
return merged
def _row_as_closing_state(row: dict) -> dict:
"""手动/程序平仓已提交、柜台持仓未清零时的展示状态。"""
out = dict(row)
out["order_state"] = "closing"
out["source_label"] = "平仓处理中"
out["stop_loss"] = None
out["take_profit"] = None
out["sl_monitoring"] = False
out["tp_monitoring"] = False
out["sl_order_active"] = False
out["tp_order_active"] = False
out["pending_orders"] = []
out["can_close"] = False
out["close_allowed"] = False
out["can_place_orders"] = False
out["trailing_be"] = False
return out
def _revive_closed_monitor(conn, symbol: str, direction: str) -> Optional[dict]:
"""柜台仍有持仓但本地监控被误关时,恢复最近一条同品种记录。"""
if should_skip_monitor_revive(symbol, direction):
@@ -2413,12 +2434,12 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
direction = p.get("direction") or "long"
if not mon:
mon = _find_pending_monitor(conn, ths, direction)
if not mon:
if not mon and not close_pending_active(ths, direction):
if fast:
mon = _find_active_monitor(conn, ths, direction)
else:
mon = _find_or_revive_monitor(conn, ths, direction)
if mon:
if mon and not close_pending_active(ths, direction):
if fast:
mon = _overlay_sl_tp_readonly(conn, mon, ths, direction) or mon
else:
@@ -2432,14 +2453,18 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
conn, mon.get("symbol") or ths, mon.get("direction") or direction,
) or mon
mon = _restore_monitor_sl_tp_if_missing(conn, mon, ths, direction) or mon
elif fast:
elif fast and not close_pending_active(ths, direction):
mon = _overlay_sl_tp_readonly(conn, None, ths, direction)
elif close_pending_active(ths, direction):
mon = mon or {"symbol": ths, "direction": direction}
try:
row = _compose_position_row(
conn, mon=mon, ctp=p, mode=mode, capital=capital,
now_iso=now_iso, fast=fast,
)
if row:
if close_pending_active(ths, direction):
row = _row_as_closing_state(row)
rows.append(row)
except Exception as exc:
logger.warning("compose ctp position row failed: %s", exc)
@@ -3433,14 +3458,36 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
_close_all_monitors_for_sym_dir(conn, sym, direction)
conn.commit()
try:
from modules.ctp.ctp_trade_sync import sync_trade_logs_from_ctp
sync_trade_logs_from_ctp(conn, mode, capital=capital, trading_mode=mode)
on_user_initiated_close(conn, trading_day=trading_day_label())
conn.commit()
except Exception as exc:
logger.debug("sync trades after close: %s", exc)
logger.debug("user initiated close hook: %s", exc)
cap_snapshot = capital
conn.close()
_push_position_snapshot_async()
return jsonify({"ok": True, "message": "已平仓,交易记录已写入"})
def _after_close() -> None:
try:
bg = get_db()
try:
init_strategy_tables(bg)
from modules.ctp.ctp_trade_sync import sync_trade_logs_from_ctp
sync_trade_logs_from_ctp(
bg, mode, capital=cap_snapshot, trading_mode=mode,
)
bg.commit()
finally:
bg.close()
except Exception as exc:
logger.debug("sync trades after close: %s", exc)
_push_position_snapshot_async(fast=True)
threading.Thread(target=_after_close, daemon=True, name="close-finalize").start()
_push_position_snapshot_async(fast=True)
return jsonify({
"ok": True,
"message": "平仓委托已提交",
"closing": True,
})
except ValueError as exc:
conn.close()
return jsonify({"ok": False, "error": str(exc)}), 400
+28 -1
View File
@@ -1232,10 +1232,32 @@
);
}
function buildClosingCard(row) {
var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空');
var openT = (row.open_time || '').replace('T', ' ').slice(0, 16);
return (
'<div class="pos-card pos-card-closing">' +
'<div class="pos-card-head"><div><div class="title">' + posSymbolTitleHtml(row,
' <span class="badge dir">' + dirBadge + '</span>') + '</div>' +
'<div class="text-muted pos-symbol-sub">' + posSymbolSubHtml(row) + '</div></div></div>' +
'<div class="pos-card-meta pos-card-meta-line">来源 <strong>' +
(row.source_label || '平仓处理中') + '</strong> · <span class="text-accent">平仓委托已提交,等待柜台成交…</span></div>' +
'<div class="pos-metrics">' +
'<div class="cell"><label>手数</label><div><strong>' + row.lots + ' 手</strong></div></div>' +
'<div class="cell"><label>均价</label><div>' + fmtNum(row.entry_price) + '</div></div>' +
'<div class="cell"><label>当前价格</label><div>' + (row.current_price != null ? fmtNum(row.current_price) : '--') + '</div></div>' +
'<div class="cell"><label>开仓</label><div>' + (openT || '--') + '</div></div>' +
'</div></div>'
);
}
function buildPosCard(row) {
if (row.order_state === 'pending') {
return buildPendingOrderCard(row);
}
if (row.order_state === 'closing') {
return buildClosingCard(row);
}
var pnlClass = row.float_pnl > 0 ? 'pnl-pos' : (row.float_pnl < 0 ? 'pnl-neg' : '');
var pnlText = row.float_pnl != null ? ((row.float_pnl >= 0 ? '+' : '') + fmtNum(row.float_pnl) + ' 元') : '--';
var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空');
@@ -1529,9 +1551,14 @@
}
return;
}
if (btn) btn.textContent = '已平仓';
if (btn) {
btn.disabled = true;
btn.textContent = '平仓中…';
}
startPosFastPoll(90);
pollPositions();
}).catch(function () {
alert('平仓请求失败,请查看持仓是否已变化');
if (btn) {
btn.disabled = false;
btn.textContent = '平仓';