#!/usr/bin/env python3 """一键修复服务器上因到期垃圾盘口导致的期权错结算(G-20260725-01)。 用法(本机): $env:DEPLOY_PASS='...' python scripts/repair_expiry_settle.py """ from __future__ import annotations import os import sys import time HOST = os.environ.get("DEPLOY_HOST", "47.236.184.99") USER = os.environ.get("DEPLOY_USER", "root") PASSWORD = os.environ.get("DEPLOY_PASS", "") ROOT = os.environ.get("DEPLOY_ROOT", "/opt/eth_hedge_sim") DB = f"{ROOT}/backend/data/hedge.db" GROUP = os.environ.get("REPAIR_GROUP_ID", "G-20260725-01") REMOTE_PY = r''' import sqlite3, time DB = "%(db)s" GROUP = "%(group)s" FEE = 0.0005 con = sqlite3.connect(DB) con.row_factory = sqlite3.Row g = con.execute("SELECT * FROM groups WHERE group_id=?", (GROUP,)).fetchone() if not g: print("group missing", GROUP) raise SystemExit(1) if g["status"] != "closed" or g["close_reason"] != "expiry": print("skip: not closed expiry", dict(g)) raise SystemExit(0) fills = list(con.execute( "SELECT * FROM fills WHERE group_id=? ORDER BY ts_ms", (GROUP,) ).fetchall()) o_open = next(f for f in fills if f["leg"]=="option" and f["action"]=="open") o_close = next(f for f in fills if f["leg"]=="option" and f["action"]=="close") p_open = next(f for f in fills if f["leg"]=="perp" and f["action"]=="open") p_close = next(f for f in fills if f["leg"]=="perp" and f["action"]=="close") strike = float(g["strike"] or 1860) # 永续平仓为买回空头:fill≈ask*(1+f) → spot≈ask perp_fill = float(p_close["fill_px"]) spot = perp_fill / (1.0 + FEE) intrinsic = max(spot - strike, 0.0) if str(g["option_side"])=="call" else max(strike - spot, 0.0) old_base = float(o_close["base_px"] or o_close["fill_px"]) # 到期对齐实盘:严格内在价值,无盘口滑点 new_base = intrinsic new_fill = intrinsic qty = float(o_close["qty_eth"]) new_notional = new_fill * qty new_fee = new_notional * FEE new_slip = 0.0 old_cash = float(o_close["notional"]) - float(o_close["fee"]) new_cash = new_notional - new_fee cash_delta = new_cash - old_cash # 即使价已修好,也继续同步 realized_pnl(扣完全部手续费) opt_entry = float(o_open["fill_px"]) opt_pnl = (new_fill - opt_entry) * qty perp_entry = float(p_open["fill_px"]) perp_side = str(p_open["side"]) if perp_side == "long": perp_pnl = (float(p_close["fill_px"]) - perp_entry) * float(p_open["qty_eth"]) else: perp_pnl = (perp_entry - float(p_close["fill_px"])) * float(p_open["qty_eth"]) fees_all = ( float(o_open["fee"]) + float(p_open["fee"]) + float(p_close["fee"]) + new_fee ) net_after_all_fees = perp_pnl + opt_pnl - fees_all # fees on group: replace option close fee contribution old_opt_fee = float(o_close["fee"]) old_opt_slip = float(o_close["slip"] or 0) fees = float(g["fees"] or 0) - old_opt_fee + new_fee slip = float(g["slip_cost"] or 0) - old_opt_slip + new_slip now = int(time.time() * 1000) meta = con.execute("SELECT equity, available FROM ledger_meta WHERE id=1").fetchone() eq = float(meta["equity"]) + cash_delta av = float(meta["available"]) + cash_delta con.execute("BEGIN") con.execute( "UPDATE fills SET base_px=?, fill_px=?, fee=?, slip=?, notional=? WHERE group_id=? AND leg='option' AND action='close'", (new_base, new_fill, new_fee, new_slip, new_notional, GROUP), ) con.execute( "UPDATE groups SET realized_pnl=?, fees=?, slip_cost=?, note=? WHERE group_id=?", (net_after_all_fees, fees, slip, f"repaired_intrinsic:{intrinsic:.4f}", GROUP), ) con.execute( "UPDATE ledger_meta SET equity=?, available=?, updated_at_ms=? WHERE id=1", (eq, av, now), ) if abs(cash_delta) > 1e-12: con.execute( "INSERT INTO ledger_entries(group_id, kind, amount, balance_after, note, ts_ms) VALUES (?,?,?,?,?,?)", (GROUP, "repair_option_intrinsic", cash_delta, eq, f"repair {GROUP}: option close {old_base:.4f}->{new_base:.4f} intrinsic={intrinsic:.4f}", now), ) # also fix close_option entry amount if present row = con.execute( "SELECT id, amount FROM ledger_entries WHERE group_id=? AND kind='close_option' ORDER BY id DESC LIMIT 1", (GROUP,), ).fetchone() if row: con.execute( "UPDATE ledger_entries SET amount=?, note=? WHERE id=?", (new_cash, f"close option expiry (repaired intrinsic {intrinsic:.4f})", row["id"]), ) con.commit() print({ "group": GROUP, "spot": spot, "intrinsic": intrinsic, "old_base": old_base, "new_base": new_base, "option_pnl": opt_pnl, "perp_pnl": perp_pnl, "fees_all": fees_all, "cash_delta": cash_delta, "new_equity": eq, "new_realized_pnl": net_after_all_fees, }) ''' % {"db": DB, "group": GROUP} def main() -> int: if not PASSWORD: print("Set DEPLOY_PASS", file=sys.stderr) return 2 import paramiko client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(HOST, username=USER, password=PASSWORD, timeout=30) cmd = "python3 - <<'PY'\n" + REMOTE_PY + "\nPY" _, stdout, stderr = client.exec_command(cmd) out = stdout.read().decode("utf-8", errors="replace") err = stderr.read().decode("utf-8", errors="replace") code = stdout.channel.recv_exit_status() print(out) if err: print(err, file=sys.stderr) client.close() return code if __name__ == "__main__": raise SystemExit(main())