#!/usr/bin/env python3 # -*- coding: utf-8 -*- """从模拟盘流水重建买/卖回 VWAP,并按真盈亏公式重算 realized_pnl_usdt. 用于 close_sell_* 漏记、开仓价被 budget/premium 抬飞的历史单. 默认 dry-run;加 --apply 才写库. python scripts/repair_options_spot_vwap_from_sim_ledger.py --db crypto.db python scripts/repair_options_spot_vwap_from_sim_ledger.py --db crypto.db --apply """ from __future__ import annotations import argparse import sqlite3 import sys from pathlib import Path from typing import Any def _safe_float(v: Any) -> float | None: if v is None or v == "": return None try: return float(v) except (TypeError, ValueError): return None def _ledger_buy_sell( conn: sqlite3.Connection, *, underly: str, inst_id: str, open_ts: str, close_ts: str | None, ) -> tuple[float | None, float | None, float | None, float | None]: """在开仓前后窗口内汇总 USDT→币 与 平仓后币→USDT.""" u = (underly or "").upper() inst = str(inst_id or "").strip() buy_usdt = 0.0 buy_coin = 0.0 opens = conn.execute( """ SELECT id, ts FROM sim_ledger_entries WHERE kind='option_open' AND note LIKE ? ORDER BY id """, (f"%{inst}%",), ).fetchall() if inst else [] if opens: open_id = int(opens[0]["id"]) # 开仓前最近一笔 USDT→币(含缓冲买币) buy_row = conn.execute( """ SELECT id, amount, ts FROM sim_ledger_entries WHERE ccy='USDT' AND account='trading' AND kind='convert' AND amount < 0 AND note LIKE ? AND id < ? ORDER BY id DESC LIMIT 1 """, (f"{u}/USDT%", open_id), ).fetchone() if buy_row: coin_row = conn.execute( """ SELECT amount FROM sim_ledger_entries WHERE ccy=? AND account='trading' AND kind='convert' AND amount > 0 AND ts=? AND note LIKE 'from usdt%' AND id < ? ORDER BY id DESC LIMIT 1 """, (u, buy_row["ts"], open_id), ).fetchone() if coin_row: buy_usdt = abs(float(buy_row["amount"])) buy_coin = float(coin_row["amount"]) sell_usdt = 0.0 sell_coin = 0.0 # 该合约 option_close 的 id 区间:本笔最后一笔 close → 下一笔任意 option_open/close 之前 closes = conn.execute( """ SELECT id, ts FROM sim_ledger_entries WHERE kind='option_close' AND note LIKE ? ORDER BY id """, (f"%{inst}%",), ).fetchall() if inst else [] if closes: start_id = int(closes[-1]["id"]) nxt = conn.execute( """ SELECT id FROM sim_ledger_entries WHERE id > ? AND kind IN ('option_open','option_close') ORDER BY id LIMIT 1 """, (start_id,), ).fetchone() end_id = int(nxt["id"]) if nxt else 10**12 sells = conn.execute( """ SELECT id, amount, ts FROM sim_ledger_entries WHERE ccy=? AND account='trading' AND kind='convert' AND amount < 0 AND note LIKE ? AND id > ? AND id < ? ORDER BY id """, (u, f"{u}/USDT%", start_id, end_id), ).fetchall() # 同秒多笔卖回时,按「币卖出 id → 下一笔 USDT 入账」配对,避免重复吃到同一笔 USDT used_usdt_ids: set[int] = set() for row in sells: sid = int(row["id"]) usdt_row = conn.execute( """ SELECT id, amount FROM sim_ledger_entries WHERE id = ? AND ccy='USDT' AND account='trading' AND kind='convert' AND amount > 0 AND note LIKE 'from %' """, (sid + 1,), ).fetchone() if usdt_row is None or int(usdt_row["id"]) in used_usdt_ids: cands = conn.execute( """ SELECT id, amount FROM sim_ledger_entries WHERE ccy='USDT' AND account='trading' AND kind='convert' AND amount > 0 AND note LIKE 'from %' AND id > ? AND id < ? ORDER BY id """, (sid, end_id), ).fetchall() usdt_row = next( (r for r in cands if int(r["id"]) not in used_usdt_ids), None, ) if not usdt_row: continue uid = int(usdt_row["id"]) used_usdt_ids.add(uid) sell_coin += abs(float(row["amount"])) sell_usdt += float(usdt_row["amount"]) return ( buy_usdt if buy_usdt > 0 else None, buy_coin if buy_coin > 0 else None, sell_usdt if sell_usdt > 0 else None, sell_coin if sell_coin > 0 else None, ) def repair(conn: sqlite3.Connection, *, apply: bool) -> list[dict[str, Any]]: sys.path.insert(0, str(Path(__file__).resolve().parents[1])) from lib.options.options_pnl_settle_lib import true_pnl_usdt_premium_move from lib.options.options_spot_bridge_lib import ensure_bridge_table ensure_bridge_table(conn) rows = conn.execute( """ SELECT id, inst_id, underlying, premium_paid, premium_received, realized_pnl, open_index_px, close_index_px, realized_pnl_usdt, bridge_id, created_at, closed_at, budget_usdt FROM options_trades WHERE status='closed' ORDER BY id """ ).fetchall() report: list[dict[str, Any]] = [] for r in rows: tid = int(r["id"]) underly = str(r["underlying"] or (str(r["inst_id"] or "").split("-")[0]) or "").upper() open_ts = str(r["created_at"] or "") close_ts = str(r["closed_at"] or "") or None buy_u, buy_c, sell_u, sell_c = _ledger_buy_sell( conn, underly=underly, inst_id=str(r["inst_id"] or ""), open_ts=open_ts, close_ts=close_ts, ) open_px = (buy_u / buy_c) if buy_u and buy_c and buy_c > 0 else None close_px = (sell_u / sell_c) if sell_u and sell_c and sell_c > 0 else None paid = _safe_float(r["premium_paid"]) coin = _safe_float(r["realized_pnl"]) old_open = _safe_float(r["open_index_px"]) old_close = _safe_float(r["close_index_px"]) old_u = _safe_float(r["realized_pnl_usdt"]) new_u = true_pnl_usdt_premium_move( coin_pnl=coin, premium_paid=paid, open_index_px=open_px or old_open, sell_back_px=close_px or old_close, ) item = { "id": tid, "inst_id": r["inst_id"], "buy_vwap": round(open_px, 4) if open_px else None, "sell_vwap": round(close_px, 4) if close_px else None, "old_open": old_open, "old_close": old_close, "old_u": old_u, "new_u": new_u, "bridge_id": r["bridge_id"], "sell_usdt": sell_u, "sell_coin": sell_c, "buy_usdt": buy_u, "buy_coin": buy_c, } report.append(item) if not apply: continue bid = r["bridge_id"] if bid is not None and buy_u and buy_c: conn.execute( """ UPDATE options_spot_bridge SET buy_usdt_spent = ?, buy_coin_accum = ? WHERE id = ? """, (float(buy_u), float(buy_c), int(bid)), ) if bid is not None and sell_u and sell_c: conn.execute( """ UPDATE options_spot_bridge SET close_sell_usdt = ?, close_sell_coin = ?, usdt_recovered = ?, coin_sold = ? WHERE id = ? """, (float(sell_u), float(sell_c), float(sell_u), float(sell_c), int(bid)), ) conn.execute( """ UPDATE options_trades SET open_index_px = COALESCE(?, open_index_px), close_index_px = COALESCE(?, close_index_px), realized_pnl_usdt = COALESCE(?, realized_pnl_usdt), initial_premium_usdt = CASE WHEN ? IS NOT NULL AND premium_paid IS NOT NULL THEN round(premium_paid * ?, 4) ELSE initial_premium_usdt END, pnl_settled = 1 WHERE id = ? """, ( open_px, close_px, new_u, open_px, open_px, tid, ), ) if apply: conn.commit() # 复盘行跟着刷 try: from lib.options.options_review_lib import sync_options_from_local_trades sync_options_from_local_trades(conn, None) conn.commit() except Exception as e: print("review_sync_warn", e) return report def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--db", required=True) ap.add_argument("--apply", action="store_true") args = ap.parse_args() path = Path(args.db) if not path.is_file(): print("db missing", path) return 1 conn = sqlite3.connect(str(path)) conn.row_factory = sqlite3.Row report = repair(conn, apply=bool(args.apply)) conn.close() import json print(json.dumps(report, ensure_ascii=False, indent=2)) print("apply" if args.apply else "dry-run", "rows", len(report)) return 0 if __name__ == "__main__": raise SystemExit(main())