3520fc0214
Co-authored-by: Cursor <cursoragent@cursor.com>
180 lines
5.4 KiB
Python
180 lines
5.4 KiB
Python
"""回填期期到期组的 settle_index(及可选内在价值成交)。
|
|
|
|
用法:
|
|
$env:DEPLOY_PASS='...'
|
|
$env:REPAIR_GROUP_ID='G-20260810-01'
|
|
# 可选: $env:SETTLE_INDEX='1877.8' 不设则用币安 ETHUSDT 1m 收盘
|
|
python scripts/repair_oo_settle_index.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.request
|
|
|
|
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-20260810-01")
|
|
SETTLE = os.environ.get("SETTLE_INDEX", "").strip()
|
|
REPAIR_FILLS = os.environ.get("REPAIR_FILLS", "1").strip() not in ("0", "false", "no")
|
|
|
|
|
|
def _eth_close(ms: int) -> float | None:
|
|
minute = (int(ms) // 60_000) * 60_000
|
|
url = (
|
|
"https://api.binance.com/api/v3/klines"
|
|
f"?symbol=ETHUSDT&interval=1m&startTime={minute}&limit=1"
|
|
)
|
|
with urllib.request.urlopen(url, timeout=10) as resp:
|
|
rows = json.loads(resp.read().decode())
|
|
if not rows:
|
|
return None
|
|
return float(rows[0][4])
|
|
|
|
|
|
REMOTE = r'''
|
|
import sqlite3
|
|
DB = %(db)r
|
|
GROUP = %(group)r
|
|
SETTLE = %(settle)s
|
|
REPAIR_FILLS = %(repair_fills)s
|
|
|
|
con = sqlite3.connect(DB)
|
|
con.row_factory = sqlite3.Row
|
|
g = con.execute("SELECT * FROM groups WHERE group_id=?", (GROUP,)).fetchone()
|
|
if not g:
|
|
print("missing", GROUP)
|
|
raise SystemExit(1)
|
|
if str(g["close_reason"] or "") != "expiry":
|
|
print("not expiry", GROUP)
|
|
raise SystemExit(1)
|
|
settle = float(SETTLE)
|
|
strike = float(g["strike"] or 0)
|
|
strike2 = float(g["strike2"] or 0) if g["strike2"] is not None else None
|
|
side = str(g["option_side"] or "call").lower()
|
|
side2 = str(g["option2_side"] or "put").lower()
|
|
|
|
def intrinsic(side, spot, k):
|
|
if k is None:
|
|
return None
|
|
if side in ("call", "c"):
|
|
return max(spot - k, 0.0)
|
|
if side in ("put", "p"):
|
|
return max(k - spot, 0.0)
|
|
return 0.0
|
|
|
|
c_iv = intrinsic(side, settle, strike)
|
|
p_iv = intrinsic(side2, settle, strike2) if strike2 is not None else None
|
|
con.execute("BEGIN")
|
|
con.execute(
|
|
"UPDATE groups SET settle_index_px=?, note=COALESCE(note,'') || ? WHERE group_id=?",
|
|
(settle, f" | settle_backfill={settle}", GROUP),
|
|
)
|
|
updated = []
|
|
if REPAIR_FILLS:
|
|
for leg, iv in (("option", c_iv), ("option2", p_iv)):
|
|
if iv is None:
|
|
continue
|
|
row = con.execute(
|
|
"SELECT id, qty_eth, fill_px FROM fills WHERE group_id=? AND leg=? AND action='close'",
|
|
(GROUP, leg),
|
|
).fetchone()
|
|
if not row:
|
|
continue
|
|
old_px = float(row["fill_px"] or 0)
|
|
tol = max(0.5, abs(iv) * 0.05)
|
|
if abs(old_px - iv) <= tol:
|
|
continue
|
|
qty = float(row["qty_eth"] or 0)
|
|
con.execute(
|
|
"UPDATE fills SET base_px=?, fill_px=?, notional=?, slip=0 WHERE id=?",
|
|
(iv, iv, iv * qty, row["id"]),
|
|
)
|
|
updated.append((leg, old_px, iv, qty))
|
|
# 重算 realized
|
|
fills = list(con.execute("SELECT * FROM fills WHERE group_id=?", (GROUP,)))
|
|
opt = {}
|
|
for f in fills:
|
|
key = (f["leg"], f["action"])
|
|
opt[key] = f
|
|
net = 0.0
|
|
fees = 0.0
|
|
for leg in ("option", "option2"):
|
|
o = opt.get((leg, "open"))
|
|
c = opt.get((leg, "close"))
|
|
if not o or not c:
|
|
continue
|
|
qty = float(o["qty_eth"] or c["qty_eth"] or 0)
|
|
net += (float(c["fill_px"]) - float(o["fill_px"])) * qty
|
|
fees += float(o["fee"] or 0) + float(c["fee"] or 0)
|
|
net_after = net - fees
|
|
con.execute(
|
|
"UPDATE groups SET realized_pnl=? WHERE group_id=?",
|
|
(net_after, GROUP),
|
|
)
|
|
con.commit()
|
|
print({
|
|
"group": GROUP,
|
|
"settle": settle,
|
|
"call_iv": c_iv,
|
|
"put_iv": p_iv,
|
|
"fills_updated": updated,
|
|
"realized_pnl": net_after,
|
|
})
|
|
'''
|
|
|
|
|
|
def main() -> int:
|
|
if not PASSWORD:
|
|
print("Set DEPLOY_PASS", file=sys.stderr)
|
|
return 2
|
|
import paramiko
|
|
|
|
settle = SETTLE
|
|
if not settle:
|
|
# peek close_at from remote first
|
|
client = paramiko.SSHClient()
|
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
client.connect(HOST, username=USER, password=PASSWORD, timeout=30)
|
|
_, stdout, _ = client.exec_command(
|
|
f"sqlite3 {DB} \"SELECT close_at_ms FROM groups WHERE group_id='{GROUP}'\""
|
|
)
|
|
raw = stdout.read().decode().strip()
|
|
client.close()
|
|
if not raw:
|
|
print("cannot read close_at_ms", file=sys.stderr)
|
|
return 1
|
|
px = _eth_close(int(raw))
|
|
if px is None:
|
|
print("cannot fetch ETHUSDT close", file=sys.stderr)
|
|
return 1
|
|
settle = f"{px:.4f}"
|
|
print("using public ETHUSDT close", settle)
|
|
|
|
body = REMOTE % {
|
|
"db": DB,
|
|
"group": GROUP,
|
|
"settle": settle,
|
|
"repair_fills": "True" if REPAIR_FILLS else "False",
|
|
}
|
|
client = paramiko.SSHClient()
|
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
client.connect(HOST, username=USER, password=PASSWORD, timeout=30)
|
|
_, stdout, stderr = client.exec_command("python3 - <<'PY'\n" + body + "\nPY")
|
|
print(stdout.read().decode("utf-8", "replace"))
|
|
err = stderr.read().decode("utf-8", "replace")
|
|
if err:
|
|
print(err, file=sys.stderr)
|
|
code = stdout.channel.recv_exit_status()
|
|
client.close()
|
|
return code
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|