# Copyright (c) 2025-2026 马建军. All rights reserved. # 专有软件 — 未经授权禁止复制、传播、转售。 # 严禁用于:带单/代客理财、向他人推荐期货品种或买卖建议、融资配资等业务。 # 详见 LICENSE.zh-CN.txt 与 docs/软件购买与使用协议.md """交易记录:字段补全、资金曲线数据。""" from __future__ import annotations from typing import Any TRADE_LOG_EXTRA_COLUMNS = ( "ALTER TABLE trade_logs ADD COLUMN margin_pct REAL", "ALTER TABLE trade_logs ADD COLUMN equity_after REAL", "ALTER TABLE trade_logs ADD COLUMN source TEXT DEFAULT 'local'", "ALTER TABLE trade_logs ADD COLUMN ctp_trade_key TEXT", ) def ensure_trade_log_columns(conn) -> None: for sql in TRADE_LOG_EXTRA_COLUMNS: try: conn.execute(sql) except Exception: pass def calc_equity_after(capital: float, pnl_net: float) -> float | None: cap = float(capital or 0) if cap <= 0: return None return round(cap + float(pnl_net or 0), 2) def recalc_trade_log_pnl( *, symbol: str, direction: str, entry_price: float, close_price: float, lots: float, stop_loss: float | None = None, take_profit: float | None = None, open_time: str = "", close_time: str = "", trading_mode: str = "simulation", capital: float = 0.0, ) -> dict[str, float]: """按开/平仓价重算盈亏与手续费(跨日持仓可手动改价后核对)。""" from contract_specs import calc_position_metrics from fee_specs import calc_round_trip_fee sym = (symbol or "").strip() direction = (direction or "long").strip().lower() entry = float(entry_price or close_price or 0) close_px = float(close_price or 0) lots_f = float(lots or 0) sl = float(stop_loss) if stop_loss is not None else entry tp = float(take_profit) if take_profit is not None else entry metrics = calc_position_metrics( direction, entry, sl, tp, lots_f, close_px, capital, sym, ) pnl = round(float(metrics.get("float_pnl") or 0), 2) fee = calc_round_trip_fee( sym, entry, close_px, lots_f, open_time, close_time, trading_mode=trading_mode, ) pnl_net = round(pnl - fee, 2) return {"pnl": pnl, "fee": round(fee, 2), "pnl_net": pnl_net} 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() if row and row[0]: val = float(row[0] or 0) if val > 0: return val except (TypeError, ValueError): pass try: from product_recommend import DISCONNECTED_RECOMMEND_CAPITAL return float(DISCONNECTED_RECOMMEND_CAPITAL) except Exception: return 100_000.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() def _norm_close_minute(ts: str) -> str: """统一 close_time 到分钟粒度,兼容 ISO `T` 与空格分隔。""" return (ts or "").strip().replace("T", " ")[:16] def purge_duplicate_local_trade_logs(conn) -> int: """删除已被 CTP 柜台记录覆盖的本地重复成交。""" removed = 0 ctp_rows = [ dict(r) for r in conn.execute("SELECT * FROM trade_logs WHERE source='ctp'").fetchall() ] local_rows = [ dict(r) for r in conn.execute( """SELECT * FROM trade_logs WHERE COALESCE(source, 'local') != 'ctp' AND (ctp_trade_key IS NULL OR ctp_trade_key = '')""" ).fetchall() ] for ctp in ctp_rows: ct16 = _norm_close_minute(ctp.get("close_time") or "") sym_n = _norm_symbol(ctp.get("symbol") or "") lots = float(ctp.get("lots") or 0) direction = (ctp.get("direction") or "long").strip().lower() for loc in local_rows: if loc.get("id") == ctp.get("id"): continue if _norm_symbol(loc.get("symbol") or "") != sym_n: continue if (loc.get("direction") or "long").strip().lower() != direction: continue if _norm_close_minute(loc.get("close_time") or "") != ct16: continue if abs(float(loc.get("lots") or 0) - lots) > 0.01: continue conn.execute("DELETE FROM trade_logs WHERE id=?", (loc["id"],)) removed += 1 return removed def _attach_symbol_meta(t: dict[str, Any]) -> None: try: from symbols import position_symbol_meta sym = (t.get("symbol") or "").strip() meta = position_symbol_meta(sym) if not t.get("symbol_name"): t["symbol_name"] = meta.get("name") or sym t["symbol_exchange"] = meta.get("exchange") or "" t["symbol_is_main"] = bool(meta.get("is_main")) except Exception: t.setdefault("symbol_exchange", "") t.setdefault("symbol_is_main", False) def enrich_trades_for_records( trades: list[dict[str, Any]], *, initial_capital: float = 0.0, ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: """表格仍按 id 降序;资金曲线按平仓时间升序用最新资金绘制。""" rows = [dict(t) for t in trades] chrono = sorted( rows, key=lambda t: ((t.get("close_time") or ""), int(t.get("id") or 0)), ) 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) 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) if margin > 0 and cap_before > 0: t["margin_pct"] = round(margin / cap_before * 100, 2) if eq is not None: curve.append({ "time": (t.get("close_time") or "")[:19], "value": float(eq), "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