From 0b924fca871221259a7d8ae59ab423c37e113840 Mon Sep 17 00:00:00 2001 From: dekun Date: Tue, 30 Jun 2026 21:41:37 +0800 Subject: [PATCH] Fix trade log equity_after to chain from initial capital by close time. Co-authored-by: Cursor --- app.py | 11 ++++++++- ctp_trade_sync.py | 11 ++++++++- sl_tp_guard.py | 6 ++++- trade_log_lib.py | 59 +++++++++++++++++++++++++++++++++++++++-------- 4 files changed, 74 insertions(+), 13 deletions(-) diff --git a/app.py b/app.py index f105799..ed59873 100644 --- a/app.py +++ b/app.py @@ -1241,7 +1241,7 @@ def close_position(pid): result = classify_close_result(direction, close_price, sl, tp) minutes = holding_to_minutes(open_time, close_time) margin_pct = metrics.get("position_pct") - from trade_log_lib import calc_equity_after + from trade_log_lib import calc_equity_after, refresh_trade_log_equity_chain equity_after = calc_equity_after(capital, pnl_net) conn.execute( """INSERT INTO trade_logs @@ -1258,6 +1258,10 @@ def close_position(pid): ), ) conn.execute("DELETE FROM position_monitors WHERE id=?", (pid,)) + try: + refresh_trade_log_equity_chain(conn, capital if capital > 0 else None) + except Exception as exc: + app.logger.debug("equity chain refresh after close: %s", exc) conn.commit() conn.close() touch_stats_cache() @@ -1301,6 +1305,11 @@ def update_trade(tid): tid, ), ) + try: + cap = float(get_setting("live_capital", "0") or 0) + refresh_trade_log_equity_chain(conn, cap if cap > 0 else None) + except Exception as exc: + app.logger.debug("equity chain refresh after trade edit: %s", exc) conn.commit() conn.close() touch_stats_cache() diff --git a/ctp_trade_sync.py b/ctp_trade_sync.py index e8ecb14..4256107 100644 --- a/ctp_trade_sync.py +++ b/ctp_trade_sync.py @@ -16,7 +16,12 @@ from contract_specs import calc_position_metrics from ctp_symbol import ths_to_vnpy_symbol from fee_specs import calc_round_trip_fee from symbols import ths_to_codes -from trade_log_lib import calc_equity_after, purge_duplicate_local_trade_logs, ensure_trade_log_columns +from trade_log_lib import ( + calc_equity_after, + purge_duplicate_local_trade_logs, + ensure_trade_log_columns, + refresh_trade_log_equity_chain, +) from vnpy_bridge import ctp_list_trades, ctp_status logger = logging.getLogger(__name__) @@ -325,4 +330,8 @@ def sync_trade_logs_from_ctp( purged = purge_duplicate_local_trade_logs(conn) if purged: stats["purged"] = purged + try: + refresh_trade_log_equity_chain(conn) + except Exception as exc: + logger.debug("equity chain refresh after ctp sync: %s", exc) return stats diff --git a/sl_tp_guard.py b/sl_tp_guard.py index bde697e..1291d13 100644 --- a/sl_tp_guard.py +++ b/sl_tp_guard.py @@ -16,7 +16,7 @@ from zoneinfo import ZoneInfo from contract_specs import calc_position_metrics from ctp_symbol import ths_to_vnpy_symbol from fee_specs import calc_round_trip_fee -from trade_log_lib import calc_equity_after +from trade_log_lib import calc_equity_after, refresh_trade_log_equity_chain from market_sessions import is_trading_session from symbols import ths_to_codes from vnpy_bridge import ( @@ -336,6 +336,10 @@ def write_trade_log( result if result in TRADE_RESULTS else "手动平仓", ), ) + try: + refresh_trade_log_equity_chain(conn, capital if capital > 0 else None) + except Exception as exc: + logger.debug("equity chain refresh after trade log: %s", exc) try: from stats_engine import refresh_stats_cache refresh_stats_cache(conn, capital) diff --git a/trade_log_lib.py b/trade_log_lib.py index d276fc5..1ec8dbb 100644 --- a/trade_log_lib.py +++ b/trade_log_lib.py @@ -32,6 +32,42 @@ def calc_equity_after(capital: float, pnl_net: float) -> float | None: return round(cap + float(pnl_net or 0), 2) +def _read_initial_capital(conn, initial_capital: float | None = None) -> float: + if initial_capital is not None and initial_capital > 0: + return float(initial_capital) + try: + row = conn.execute("SELECT value FROM settings WHERE key='live_capital'").fetchone() + return float(row[0] or 0) if row else 0.0 + except (TypeError, ValueError): + return 0.0 + + +def refresh_trade_log_equity_chain( + conn, + initial_capital: float | None = None, +) -> int: + """按平仓时间顺序重算 trade_logs.equity_after(起始=参考资金 live_capital)。""" + base = _read_initial_capital(conn, initial_capital) + rows = [ + dict(r) + for r in conn.execute( + "SELECT id, close_time, pnl_net FROM trade_logs ORDER BY close_time ASC, id ASC" + ).fetchall() + ] + running = float(base or 0) + updated = 0 + for row in rows: + if running <= 0: + break + running = round(running + float(row.get("pnl_net") or 0), 2) + conn.execute( + "UPDATE trade_logs SET equity_after=? WHERE id=?", + (running, int(row["id"])), + ) + updated += 1 + return updated + + def _norm_symbol(symbol: str) -> str: return (symbol or "").split(".")[0].strip().lower() @@ -105,23 +141,21 @@ def enrich_trades_for_records( ) running = float(initial_capital or 0) curve: list[dict[str, Any]] = [] + equity_by_id: dict[int, float | None] = {} for t in chrono: _attach_symbol_meta(t) pnl_net = float(t.get("pnl_net") or 0) - eq = t.get("equity_after") - if eq is None: - if running > 0: - eq = round(running + pnl_net, 2) - else: - eq = None - t["equity_after"] = eq - if eq is not None: - running = float(eq) + if running > 0: + running = round(running + pnl_net, 2) + eq: float | None = running + else: + eq = None + equity_by_id[int(t.get("id") or 0)] = eq + cap_before = float(eq or 0) - pnl_net if eq is not None else 0.0 if t.get("margin_pct") is None: margin = float(t.get("margin") or 0) - cap_before = float(eq or 0) - pnl_net if eq is not None else 0.0 if margin > 0 and cap_before > 0: t["margin_pct"] = round(margin / cap_before * 100, 2) @@ -132,4 +166,9 @@ def enrich_trades_for_records( "id": int(t.get("id") or 0), }) + for t in rows: + tid = int(t.get("id") or 0) + if tid in equity_by_id: + t["equity_after"] = equity_by_id[tid] + return rows, curve