Compare commits
2 Commits
b0dca59681
...
e3671d9798
| Author | SHA1 | Date | |
|---|---|---|---|
| e3671d9798 | |||
| a58d97938c |
+142
-37
@@ -12,7 +12,13 @@ from ..models.db import Database, get_db
|
|||||||
from ..strategy.session import get_session
|
from ..strategy.session import get_session
|
||||||
from .ledger import Ledger
|
from .ledger import Ledger
|
||||||
from .liquidity import bid_covers_eth, bid_mark_ok, contracts_for_eth
|
from .liquidity import bid_covers_eth, bid_mark_ok, contracts_for_eth
|
||||||
from .pricing import option_fill, perp_fill
|
from .pricing import (
|
||||||
|
option_expiry_settle,
|
||||||
|
option_fill,
|
||||||
|
option_intrinsic,
|
||||||
|
perp_fill,
|
||||||
|
resolve_option_close_bid,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
@@ -71,6 +77,61 @@ class Matcher:
|
|||||||
)
|
)
|
||||||
return CloseResult(ok=False, detail=detail, liquidity_wait=True)
|
return CloseResult(ok=False, detail=detail, liquidity_wait=True)
|
||||||
|
|
||||||
|
def _group_strike(self, group_id: str, option_inst_id: str) -> float | None:
|
||||||
|
g = self.db.fetchone(
|
||||||
|
"SELECT strike FROM groups WHERE group_id=?", (group_id,)
|
||||||
|
)
|
||||||
|
if g is not None and g["strike"] is not None:
|
||||||
|
try:
|
||||||
|
return float(g["strike"])
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
from ..exchange.okx.parse import parse_option_inst_id
|
||||||
|
|
||||||
|
_, stk, _ = parse_option_inst_id(option_inst_id)
|
||||||
|
if stk is not None:
|
||||||
|
return float(stk)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
from ..exchange.binance.parse import parse_option_symbol
|
||||||
|
|
||||||
|
_, stk, _ = parse_option_symbol(option_inst_id)
|
||||||
|
if stk is not None:
|
||||||
|
return float(stk)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _close_spot_px(self, snap: Any) -> float | None:
|
||||||
|
if getattr(snap, "index_px", None) is not None:
|
||||||
|
try:
|
||||||
|
px = float(snap.index_px)
|
||||||
|
if px > 0:
|
||||||
|
return px
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
perp = getattr(snap, "perp", None)
|
||||||
|
if not perp:
|
||||||
|
return None
|
||||||
|
for attr in ("mark_px", "last"):
|
||||||
|
v = getattr(perp, attr, None)
|
||||||
|
if v is not None:
|
||||||
|
try:
|
||||||
|
px = float(v)
|
||||||
|
if px > 0:
|
||||||
|
return px
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
if perp.bid is not None and perp.ask is not None:
|
||||||
|
return (float(perp.bid) + float(perp.ask)) / 2.0
|
||||||
|
if perp.bid is not None:
|
||||||
|
return float(perp.bid)
|
||||||
|
if perp.ask is not None:
|
||||||
|
return float(perp.ask)
|
||||||
|
return None
|
||||||
|
|
||||||
def open_group(
|
def open_group(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -269,8 +330,9 @@ class Matcher:
|
|||||||
def close_group(self, *, reason: str, bypass_liquidity: bool = False) -> CloseResult:
|
def close_group(self, *, reason: str, bypass_liquidity: bool = False) -> CloseResult:
|
||||||
"""
|
"""
|
||||||
全平一组。默认校验期权买一深度 + 买一/标记偏差(默认≤30%)。
|
全平一组。默认校验期权买一深度 + 买一/标记偏差(默认≤30%)。
|
||||||
bypass_liquidity=True:紧急全平可绕过(仍需有可用买一价才能成交;无买一时用标记近似)。
|
reason=expiry:对齐实盘,期权按标的结算价的内在价值入账(不吃盘口)。
|
||||||
成交顺序:先平期权(薄)→ 再瞬时平永续(对冲先留着);永续盘口失败则回滚期权入账。
|
bypass_liquidity=True(紧急):绕过深度闸门,价取 max(买一, 标记, 内在价值)。
|
||||||
|
成交顺序:先平期权 → 再瞬时平永续;永续盘口失败则回滚期权入账。
|
||||||
"""
|
"""
|
||||||
s = get_settings()
|
s = get_settings()
|
||||||
pos = self.current_position()
|
pos = self.current_position()
|
||||||
@@ -288,12 +350,6 @@ class Matcher:
|
|||||||
oq = get_exchange().quote(option_inst_id) or (
|
oq = get_exchange().quote(option_inst_id) or (
|
||||||
snap.call if option_side == "call" else snap.put
|
snap.call if option_side == "call" else snap.put
|
||||||
)
|
)
|
||||||
if not oq:
|
|
||||||
return CloseResult(
|
|
||||||
ok=False,
|
|
||||||
detail="期权盘口不可用",
|
|
||||||
liquidity_wait=not bypass_liquidity,
|
|
||||||
)
|
|
||||||
|
|
||||||
ct_mult = self._ct_mult(option_inst_id)
|
ct_mult = self._ct_mult(option_inst_id)
|
||||||
need_eth = float(pos["option_qty_eth"] or s.option_qty_eth)
|
need_eth = float(pos["option_qty_eth"] or s.option_qty_eth)
|
||||||
@@ -301,43 +357,89 @@ class Matcher:
|
|||||||
"close_bid_mark_max_pct", s.close_bid_mark_max_pct
|
"close_bid_mark_max_pct", s.close_bid_mark_max_pct
|
||||||
)
|
)
|
||||||
|
|
||||||
close_bid = oq.bid
|
strike = self._group_strike(group_id, option_inst_id)
|
||||||
if not bypass_liquidity:
|
spot = self._close_spot_px(snap)
|
||||||
if close_bid is None:
|
intrinsic: float | None = None
|
||||||
return self._liquidity_wait(group_id, "期权买一不可用")
|
if strike is not None and spot is not None:
|
||||||
if not bid_covers_eth(
|
intrinsic = option_intrinsic(
|
||||||
bid_sz_contracts=oq.bid_sz,
|
option_side=option_side, strike=strike, spot=spot
|
||||||
ct_mult=ct_mult,
|
|
||||||
need_eth=need_eth,
|
|
||||||
):
|
|
||||||
return self._liquidity_wait(group_id, "期权买一流动性不足")
|
|
||||||
ok_dev, why = bid_mark_ok(
|
|
||||||
bid=close_bid, mark=oq.mark_px, max_dev_pct=max_dev
|
|
||||||
)
|
)
|
||||||
if not ok_dev:
|
|
||||||
return self._liquidity_wait(group_id, why)
|
|
||||||
else:
|
|
||||||
# 紧急:优先买一,否则用标记价近似成交(SIM)
|
|
||||||
if close_bid is None:
|
|
||||||
close_bid = oq.mark_px
|
|
||||||
if close_bid is None:
|
|
||||||
return CloseResult(ok=False, detail="紧急全平失败:无买一/标记价")
|
|
||||||
|
|
||||||
fee_rate = self._fee_rate()
|
fee_rate = self._fee_rate()
|
||||||
|
is_expiry = reason == "expiry"
|
||||||
|
|
||||||
|
if is_expiry:
|
||||||
|
# 实盘到期:直接按内在价值结算,不依赖盘口
|
||||||
|
if intrinsic is None:
|
||||||
|
return CloseResult(
|
||||||
|
ok=False,
|
||||||
|
detail="到期结算失败:缺少行权价或标的结算价",
|
||||||
|
)
|
||||||
|
of = option_expiry_settle(
|
||||||
|
intrinsic=float(intrinsic),
|
||||||
|
qty_eth=float(pos["option_qty_eth"]),
|
||||||
|
fee_rate=fee_rate,
|
||||||
|
)
|
||||||
|
close_bid = float(intrinsic)
|
||||||
|
else:
|
||||||
|
if not oq:
|
||||||
|
return CloseResult(
|
||||||
|
ok=False,
|
||||||
|
detail="期权盘口不可用",
|
||||||
|
liquidity_wait=not bypass_liquidity,
|
||||||
|
)
|
||||||
|
close_bid = oq.bid
|
||||||
|
if not bypass_liquidity:
|
||||||
|
if close_bid is None:
|
||||||
|
return self._liquidity_wait(group_id, "期权买一不可用")
|
||||||
|
if not bid_covers_eth(
|
||||||
|
bid_sz_contracts=oq.bid_sz,
|
||||||
|
ct_mult=ct_mult,
|
||||||
|
need_eth=need_eth,
|
||||||
|
):
|
||||||
|
return self._liquidity_wait(group_id, "期权买一流动性不足")
|
||||||
|
ok_dev, why = bid_mark_ok(
|
||||||
|
bid=close_bid, mark=oq.mark_px, max_dev_pct=max_dev
|
||||||
|
)
|
||||||
|
if not ok_dev:
|
||||||
|
return self._liquidity_wait(group_id, why)
|
||||||
|
resolved = resolve_option_close_bid(
|
||||||
|
bid=float(close_bid),
|
||||||
|
mark=oq.mark_px,
|
||||||
|
intrinsic=intrinsic,
|
||||||
|
bypass_liquidity=False,
|
||||||
|
)
|
||||||
|
if resolved is None:
|
||||||
|
return self._liquidity_wait(group_id, "期权平仓价不可用")
|
||||||
|
close_bid = resolved
|
||||||
|
else:
|
||||||
|
resolved = resolve_option_close_bid(
|
||||||
|
bid=close_bid,
|
||||||
|
mark=oq.mark_px,
|
||||||
|
intrinsic=intrinsic,
|
||||||
|
bypass_liquidity=True,
|
||||||
|
)
|
||||||
|
if resolved is None:
|
||||||
|
return CloseResult(
|
||||||
|
ok=False,
|
||||||
|
detail="紧急全平失败:无买一/标记/内在价值",
|
||||||
|
)
|
||||||
|
close_bid = resolved
|
||||||
|
of = option_fill(
|
||||||
|
action="close",
|
||||||
|
bid=float(close_bid),
|
||||||
|
ask=float(oq.ask or close_bid),
|
||||||
|
qty_eth=float(pos["option_qty_eth"]),
|
||||||
|
fee_rate=fee_rate,
|
||||||
|
)
|
||||||
|
|
||||||
perp_side = str(pos["perp_side"])
|
perp_side = str(pos["perp_side"])
|
||||||
perp_qty = float(pos["perp_qty_eth"])
|
perp_qty = float(pos["perp_qty_eth"])
|
||||||
opt_qty = float(pos["option_qty_eth"])
|
opt_qty = float(pos["option_qty_eth"])
|
||||||
perp_entry = float(pos["perp_entry_px"])
|
perp_entry = float(pos["perp_entry_px"])
|
||||||
opt_entry = float(pos["option_entry_px"])
|
opt_entry = float(pos["option_entry_px"])
|
||||||
|
|
||||||
# 1) 先平期权(买一流动性差);永续对冲暂留
|
# 1) 先平期权;永续对冲暂留
|
||||||
of = option_fill(
|
|
||||||
action="close",
|
|
||||||
bid=float(close_bid),
|
|
||||||
ask=float(oq.ask or close_bid),
|
|
||||||
qty_eth=opt_qty,
|
|
||||||
fee_rate=fee_rate,
|
|
||||||
)
|
|
||||||
opt_pnl = (of.fill_px - opt_entry) * opt_qty
|
opt_pnl = (of.fill_px - opt_entry) * opt_qty
|
||||||
opt_cash = of.notional - of.fee
|
opt_cash = of.notional - of.fee
|
||||||
self.ledger.apply_cash(
|
self.ledger.apply_cash(
|
||||||
@@ -455,6 +557,9 @@ class Matcher:
|
|||||||
"net": net,
|
"net": net,
|
||||||
"close_sequence": ["option", "perp"],
|
"close_sequence": ["option", "perp"],
|
||||||
"cash_delta": opt_cash + perp_pnl - pf.fee,
|
"cash_delta": opt_cash + perp_pnl - pf.fee,
|
||||||
|
"option_close_bid": float(close_bid),
|
||||||
|
"option_intrinsic": intrinsic,
|
||||||
|
"settle_spot": spot,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,63 @@ class PriceResult:
|
|||||||
return asdict(self)
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
|
def option_intrinsic(*, option_side: str, strike: float, spot: float) -> float:
|
||||||
|
"""多头期权内在价值(USDT/ETH)。call=max(S−K,0),put=max(K−S,0)。"""
|
||||||
|
s = float(spot)
|
||||||
|
k = float(strike)
|
||||||
|
side = str(option_side).lower().strip()
|
||||||
|
if side in ("call", "c"):
|
||||||
|
return max(s - k, 0.0)
|
||||||
|
if side in ("put", "p"):
|
||||||
|
return max(k - s, 0.0)
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def option_expiry_settle(
|
||||||
|
*,
|
||||||
|
intrinsic: float,
|
||||||
|
qty_eth: float,
|
||||||
|
fee_rate: float,
|
||||||
|
) -> PriceResult:
|
||||||
|
"""到期结算:按内在价值入账(对齐实盘),无买卖价差滑点,仅扣手续费。"""
|
||||||
|
base = max(float(intrinsic), 0.0)
|
||||||
|
fill = base
|
||||||
|
f = float(fee_rate)
|
||||||
|
notional = abs(fill * float(qty_eth))
|
||||||
|
fee = notional * f
|
||||||
|
return PriceResult(base_px=base, fill_px=fill, fee=fee, slip=0.0, notional=notional)
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_option_close_bid(
|
||||||
|
*,
|
||||||
|
bid: float | None,
|
||||||
|
mark: float | None,
|
||||||
|
intrinsic: float | None,
|
||||||
|
bypass_liquidity: bool,
|
||||||
|
) -> float | None:
|
||||||
|
"""
|
||||||
|
非到期平仓用买一价;多头卖出不得低于内在价值(SIM)。
|
||||||
|
紧急 bypass:max(买一, 标记, 内在价值)。
|
||||||
|
到期请用 option_expiry_settle,不要走本函数。
|
||||||
|
"""
|
||||||
|
candidates: list[float] = []
|
||||||
|
if bid is not None and bid >= 0:
|
||||||
|
candidates.append(float(bid))
|
||||||
|
if bypass_liquidity and mark is not None and mark >= 0:
|
||||||
|
candidates.append(float(mark))
|
||||||
|
if intrinsic is not None and intrinsic >= 0:
|
||||||
|
candidates.append(float(intrinsic))
|
||||||
|
if not candidates:
|
||||||
|
return None
|
||||||
|
if bypass_liquidity:
|
||||||
|
return max(candidates)
|
||||||
|
if bid is None:
|
||||||
|
return None
|
||||||
|
if intrinsic is not None and intrinsic >= 0:
|
||||||
|
return max(float(bid), float(intrinsic))
|
||||||
|
return float(bid)
|
||||||
|
|
||||||
|
|
||||||
def perp_fill(
|
def perp_fill(
|
||||||
*,
|
*,
|
||||||
side: str,
|
side: str,
|
||||||
|
|||||||
@@ -142,3 +142,51 @@ def test_expiry_close() -> None:
|
|||||||
assert d2.reason == "expiry"
|
assert d2.reason == "expiry"
|
||||||
d3 = check_expiry_close(expiry_ms=1_000, now_ms=1_001)
|
d3 = check_expiry_close(expiry_ms=1_000, now_ms=1_001)
|
||||||
assert d3.should_close is True
|
assert d3.should_close is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_option_intrinsic_and_close_bid_floor() -> None:
|
||||||
|
from app.sim.pricing import (
|
||||||
|
option_expiry_settle,
|
||||||
|
option_intrinsic,
|
||||||
|
resolve_option_close_bid,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert option_intrinsic(option_side="call", strike=1860, spot=1882) == 22.0
|
||||||
|
assert option_intrinsic(option_side="put", strike=1860, spot=1882) == 0.0
|
||||||
|
assert option_intrinsic(option_side="put", strike=1860, spot=1840) == 20.0
|
||||||
|
|
||||||
|
# 到期:严格按内在价值,无滑点
|
||||||
|
settled = option_expiry_settle(intrinsic=22.0, qty_eth=2.0, fee_rate=0.0005)
|
||||||
|
assert settled.fill_px == 22.0
|
||||||
|
assert settled.slip == 0.0
|
||||||
|
assert settled.notional == 44.0
|
||||||
|
assert abs(settled.fee - 44.0 * 0.0005) < 1e-12
|
||||||
|
|
||||||
|
# 紧急垃圾买一 0.2,内在价值 22 → 抬到 22
|
||||||
|
assert (
|
||||||
|
resolve_option_close_bid(
|
||||||
|
bid=0.2, mark=0.2, intrinsic=22.0, bypass_liquidity=True
|
||||||
|
)
|
||||||
|
== 22.0
|
||||||
|
)
|
||||||
|
# 常规也有内在价值地板
|
||||||
|
assert (
|
||||||
|
resolve_option_close_bid(
|
||||||
|
bid=0.2, mark=0.2, intrinsic=22.0, bypass_liquidity=False
|
||||||
|
)
|
||||||
|
== 22.0
|
||||||
|
)
|
||||||
|
# 买一高于内在价值,保留买一
|
||||||
|
assert (
|
||||||
|
resolve_option_close_bid(
|
||||||
|
bid=25.0, mark=24.0, intrinsic=22.0, bypass_liquidity=True
|
||||||
|
)
|
||||||
|
== 25.0
|
||||||
|
)
|
||||||
|
# bypass 无买一,用标记与内在价值
|
||||||
|
assert (
|
||||||
|
resolve_option_close_bid(
|
||||||
|
bid=None, mark=3.0, intrinsic=22.0, bypass_liquidity=True
|
||||||
|
)
|
||||||
|
== 22.0
|
||||||
|
)
|
||||||
|
|||||||
+4
-1
@@ -134,7 +134,9 @@
|
|||||||
- 买一相对标记偏差默认 ≤ **30%**(`close_bid_mark_max_pct`);
|
- 买一相对标记偏差默认 ≤ **30%**(`close_bid_mark_max_pct`);
|
||||||
- 不满足 → `liquidity_wait`,继续等待,不改开仓。
|
- 不满足 → `liquidity_wait`,继续等待,不改开仓。
|
||||||
|
|
||||||
**到期 / 紧急全平**:绕过上述闸门。
|
**到期自动全平**:对齐实盘,期权按标的结算价计算 **内在价值** 入账(不吃盘口、无价差滑点);永续仍市价平掉。策略暂停时仍执行。
|
||||||
|
|
||||||
|
**紧急全平**:绕过流动性闸门;期权价取 max(买一, 标记, 内在价值)。
|
||||||
|
|
||||||
### 4.4 其它平仓入口
|
### 4.4 其它平仓入口
|
||||||
|
|
||||||
@@ -257,3 +259,4 @@
|
|||||||
|------|------|
|
|------|------|
|
||||||
| 2026-07-25 | 初稿:对齐当前开平仓、周末跳过、到期全平、净盈利口径与资金建议 |
|
| 2026-07-25 | 初稿:对齐当前开平仓、周末跳过、到期全平、净盈利口径与资金建议 |
|
||||||
| 2026-07-25 | 平仓顺序改为先期权后永续(与开仓同理:薄腿优先) |
|
| 2026-07-25 | 平仓顺序改为先期权后永续(与开仓同理:薄腿优先) |
|
||||||
|
| 2026-07-26 | 到期按内在价值结算(对齐实盘);紧急平仓仍用 max(买一,标记,内在价值) |
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
#!/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
|
||||||
|
if abs(new_base - old_base) < 1e-9 and abs(float(o_close["fill_px"]) - new_fill) < 1e-9:
|
||||||
|
print("already ok", old_base, intrinsic)
|
||||||
|
raise SystemExit(0)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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"])
|
||||||
|
net = perp_pnl + opt_pnl - float(p_close["fee"]) - new_fee
|
||||||
|
|
||||||
|
# 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, 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),
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
"cash_delta": cash_delta,
|
||||||
|
"new_equity": eq,
|
||||||
|
"new_realized_pnl": net,
|
||||||
|
})
|
||||||
|
''' % {"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())
|
||||||
Reference in New Issue
Block a user