96e8e5cb70
Co-authored-by: Cursor <cursoragent@cursor.com>
162 lines
5.3 KiB
Python
162 lines
5.3 KiB
Python
#!/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())
|