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:
@@ -59,6 +59,7 @@ from modules.trading.order_pending import (
|
|||||||
from modules.core.db_conn import commit_retry, execute_retry
|
from modules.core.db_conn import commit_retry, execute_retry
|
||||||
from modules.trading.sl_tp_guard import (
|
from modules.trading.sl_tp_guard import (
|
||||||
cancel_monitor_exit_orders,
|
cancel_monitor_exit_orders,
|
||||||
|
clear_close_pending,
|
||||||
close_pending_active,
|
close_pending_active,
|
||||||
ensure_monitor_order_columns,
|
ensure_monitor_order_columns,
|
||||||
mark_close_pending,
|
mark_close_pending,
|
||||||
@@ -1170,6 +1171,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
direction: str,
|
direction: str,
|
||||||
) -> Optional[dict]:
|
) -> Optional[dict]:
|
||||||
"""只读:从已关闭监控补全止盈止损,不写库。"""
|
"""只读:从已关闭监控补全止盈止损,不写库。"""
|
||||||
|
if should_skip_monitor_revive(sym, direction):
|
||||||
|
return {"symbol": sym, "direction": direction}
|
||||||
if not mon:
|
if not mon:
|
||||||
rsl, rtp, rtrail, rinitial = _restore_sl_tp_from_closed(conn, sym, direction)
|
rsl, rtp, rtrail, rinitial = _restore_sl_tp_from_closed(conn, sym, direction)
|
||||||
if rsl is None and rtp is None and not rtrail:
|
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
|
merged["initial_stop_loss"] = rinitial
|
||||||
return merged
|
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]:
|
def _revive_closed_monitor(conn, symbol: str, direction: str) -> Optional[dict]:
|
||||||
"""柜台仍有持仓但本地监控被误关时,恢复最近一条同品种记录。"""
|
"""柜台仍有持仓但本地监控被误关时,恢复最近一条同品种记录。"""
|
||||||
if should_skip_monitor_revive(symbol, direction):
|
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"
|
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:
|
if not mon and not close_pending_active(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:
|
if mon and not close_pending_active(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:
|
||||||
@@ -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,
|
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:
|
elif fast and not close_pending_active(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):
|
||||||
|
mon = mon or {"symbol": ths, "direction": direction}
|
||||||
try:
|
try:
|
||||||
row = _compose_position_row(
|
row = _compose_position_row(
|
||||||
conn, mon=mon, ctp=p, mode=mode, capital=capital,
|
conn, mon=mon, ctp=p, mode=mode, capital=capital,
|
||||||
now_iso=now_iso, fast=fast,
|
now_iso=now_iso, fast=fast,
|
||||||
)
|
)
|
||||||
if row:
|
if row:
|
||||||
|
if close_pending_active(ths, direction):
|
||||||
|
row = _row_as_closing_state(row)
|
||||||
rows.append(row)
|
rows.append(row)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.warning("compose ctp position row failed: %s", 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)
|
_close_all_monitors_for_sym_dir(conn, sym, direction)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
try:
|
try:
|
||||||
from modules.ctp.ctp_trade_sync import sync_trade_logs_from_ctp
|
on_user_initiated_close(conn, trading_day=trading_day_label())
|
||||||
sync_trade_logs_from_ctp(conn, mode, capital=capital, trading_mode=mode)
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
except Exception as exc:
|
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()
|
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:
|
except ValueError as exc:
|
||||||
conn.close()
|
conn.close()
|
||||||
return jsonify({"ok": False, "error": str(exc)}), 400
|
return jsonify({"ok": False, "error": str(exc)}), 400
|
||||||
|
|||||||
@@ -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) {
|
function buildPosCard(row) {
|
||||||
if (row.order_state === 'pending') {
|
if (row.order_state === 'pending') {
|
||||||
return buildPendingOrderCard(row);
|
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 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 pnlText = row.float_pnl != null ? ((row.float_pnl >= 0 ? '+' : '') + fmtNum(row.float_pnl) + ' 元') : '--';
|
||||||
var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空');
|
var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空');
|
||||||
@@ -1529,9 +1551,14 @@
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (btn) btn.textContent = '已平仓';
|
if (btn) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = '平仓中…';
|
||||||
|
}
|
||||||
|
startPosFastPoll(90);
|
||||||
pollPositions();
|
pollPositions();
|
||||||
}).catch(function () {
|
}).catch(function () {
|
||||||
|
alert('平仓请求失败,请查看持仓是否已变化');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = '平仓';
|
btn.textContent = '平仓';
|
||||||
|
|||||||
Reference in New Issue
Block a user