From d3955309d97897be71535bc213c81aa7babba1ed Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 26 Jun 2026 11:36:56 +0800 Subject: [PATCH] Persist CTP margin, ratio, and fees to DB; use exchange commission in trade logs. Co-authored-by: Cursor --- ctp_trade_sync.py | 38 ++++++++++++++++++++++++++++++++----- install_trading.py | 47 +++++++++++++++++++++++++++++++++++++++++++++- sl_tp_guard.py | 1 + 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/ctp_trade_sync.py b/ctp_trade_sync.py index 4fedab8..992a2b0 100644 --- a/ctp_trade_sync.py +++ b/ctp_trade_sync.py @@ -49,6 +49,12 @@ def _to_ths_code(symbol: str) -> str: return sym.lower() +def _allocate_commission(total_comm: float, matched: int, total_lots: int) -> float: + if total_comm <= 0 or matched <= 0 or total_lots <= 0: + return 0.0 + return round(total_comm * matched / total_lots, 2) + + def build_round_trips(trades: list[dict[str, Any]]) -> list[dict[str, Any]]: """按 FIFO 将开/平仓成交配对为完整回合。""" stacks: dict[tuple[str, str], list[dict[str, Any]]] = defaultdict(list) @@ -70,25 +76,41 @@ def build_round_trips(trades: list[dict[str, Any]]) -> list[dict[str, Any]]: stacks[key].append({ **t, "remaining": lots, + "commission_remaining": float(t.get("commission") or 0), }) continue + close_lots_total = lots close_lots_left = lots close_price = float(t.get("price") or 0) close_time = t.get("datetime") or "" close_trade_id = str(t.get("trade_id") or "") + close_comm_total = float(t.get("commission") or 0) while close_lots_left > 0 and stacks[key]: open_t = stacks[key][0] - matched = min(close_lots_left, int(open_t.get("remaining") or 0)) + open_rem = int(open_t.get("remaining") or 0) + matched = min(close_lots_left, open_rem) if matched <= 0: stacks[key].pop(0) continue - open_t["remaining"] = int(open_t.get("remaining") or 0) - matched + open_comm_rem = float(open_t.get("commission_remaining") or 0) + open_comm_share = ( + _allocate_commission(open_comm_rem, matched, open_rem) + if open_rem > 0 else 0.0 + ) + close_comm_share = _allocate_commission( + close_comm_total, matched, close_lots_total, + ) + open_t["remaining"] = open_rem - matched + open_t["commission_remaining"] = round( + max(0.0, open_comm_rem - open_comm_share), 2, + ) if open_t["remaining"] <= 0: stacks[key].pop(0) close_lots_left -= matched open_trade_id = str(open_t.get("trade_id") or "") ctp_key = f"{open_trade_id}|{close_trade_id}|{sym}|{pos_dir}|{matched}" + trip_fee = round(open_comm_share + close_comm_share, 2) trips.append({ "ctp_trade_key": ctp_key, "symbol": sym, @@ -101,6 +123,8 @@ def build_round_trips(trades: list[dict[str, Any]]) -> list[dict[str, Any]]: "close_time": close_time, "open_trade_id": open_trade_id, "close_trade_id": close_trade_id, + "fee": trip_fee, + "fee_from_ctp": trip_fee > 0, }) return trips @@ -202,9 +226,13 @@ def sync_trade_logs_from_ctp( direction, entry, sl_f, tp_f, lots, close_px, capital, ths, ) pnl = float(metrics.get("float_pnl") or 0) - fee = calc_round_trip_fee( - ths, entry, close_px, lots, open_time, close_time, trading_mode=trading_mode, - ) + trip_fee = float(trip.get("fee") or 0) + if trip_fee > 0: + fee = round(trip_fee, 2) + else: + fee = calc_round_trip_fee( + ths, entry, close_px, lots, open_time, close_time, trading_mode=trading_mode, + ) pnl_net = round(pnl - fee, 2) margin_pct = metrics.get("position_pct") equity_after = calc_equity_after(capital, pnl_net) diff --git a/install_trading.py b/install_trading.py index 4234d7b..e3d8d7e 100644 --- a/install_trading.py +++ b/install_trading.py @@ -299,6 +299,37 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se row["position_pct"] = round(margin / capital * 100, 2) return rows + def _persist_ctp_snapshot_to_monitors( + conn, + rows: list[dict], + mode: str, + ) -> None: + """将柜台校正后的保证金、仓位占比、已扣开仓手续费写入 trade_order_monitors。""" + if not ctp_status(mode).get("connected"): + return + ensure_monitor_order_columns(conn) + for row in rows: + mid = row.get("monitor_id") + if not mid or row.get("order_state") == "pending": + continue + margin = row.get("margin") + position_pct = row.get("position_pct") + open_fee = row.get("est_fee") + if margin is None and position_pct is None and open_fee is None: + continue + try: + execute_retry( + conn, + """UPDATE trade_order_monitors SET + margin=COALESCE(?, margin), + position_pct=COALESCE(?, position_pct), + open_fee=COALESCE(?, open_fee) + WHERE id=? AND status='active'""", + (margin, position_pct, open_fee, int(mid)), + ) + except Exception as exc: + logger.debug("persist monitor ctp snapshot %s: %s", mid, exc) + def _ensure_monitors_from_ctp(conn, mode: str) -> None: """CTP 有持仓但本地无监控时,自动补写一条 active 记录供展示。""" if not ctp_status(mode).get("connected"): @@ -742,10 +773,18 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se position_pct = None if margin and capital > 0: position_pct = round(float(margin) / float(capital) * 100, 2) + open_commission = _open_commission_from_ctp_trades(mode, sym, direction) + if open_commission is None: + fee_info = calc_fee_breakdown( + sym, entry, entry, lots, open_time_val or "", "", + trading_mode=mode, + ) + open_commission = fee_info.get("open_fee") execute_retry( conn, """UPDATE trade_order_monitors SET lots=?, entry_price=?, - open_time=?, margin=?, position_pct=?, mark_price=?, float_pnl=? + open_time=?, margin=?, position_pct=?, mark_price=?, float_pnl=?, + open_fee=? WHERE id=?""", ( lots, @@ -755,6 +794,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se position_pct, float(mark) if mark else None, float_pnl, + open_commission, mid, ), ) @@ -854,6 +894,10 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se sym, entry, close_est, lots, open_time or now_iso, now_iso, trading_mode=mode, ) open_commission = _open_commission_from_ctp_trades(mode, sym, direction) + if open_commission is None and mon and mon.get("open_fee") is not None: + cached_fee = float(mon.get("open_fee") or 0) + if cached_fee > 0: + open_commission = cached_fee if open_commission is not None: display_fee = open_commission fee_source = "ctp" @@ -1170,6 +1214,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se _ensure_monitors_from_ctp(conn, mode) rows = _build_trading_live_rows(conn, fast=fast) rows = _apply_account_margin_to_rows(rows, mode, capital) + _persist_ctp_snapshot_to_monitors(conn, rows, mode) pending_orders = _build_pending_orders(conn, mode) risk = get_risk_status(conn, active_count=_effective_active_position_count(conn, mode)) return { diff --git a/sl_tp_guard.py b/sl_tp_guard.py index 55a8fd6..391b1e2 100644 --- a/sl_tp_guard.py +++ b/sl_tp_guard.py @@ -53,6 +53,7 @@ MONITOR_ORDER_COLUMNS = ( "ALTER TABLE trade_order_monitors ADD COLUMN float_pnl REAL", "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 open_fee REAL", ) TRADE_RESULTS = ("止损", "止盈", "移动止盈", "保本止盈", "手动平仓")