Mirror option expiry settlements from exchange bills; debit OO LIVE open premiums.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,180 @@
|
||||
"""LIVE 期权到期交割:从交易所账单/行权记录取结算现金,禁止本地 intrinsic 发明。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# OKX bills subType:行权 / 对手行权 / 虚值到期
|
||||
_OKX_EXERCISE_SUBTYPES = frozenset({"170", "171", "172"})
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class OptionSettlement:
|
||||
"""qty_eth>0 时 fill_px = notional/qty_eth;cash 为入账净额(已扣费)。"""
|
||||
|
||||
found: bool
|
||||
notional: float = 0.0 # 结算毛额(入账前)
|
||||
fee: float = 0.0
|
||||
cash: float = 0.0 # notional - fee
|
||||
fill_px: float = 0.0
|
||||
source: str = ""
|
||||
detail: str = ""
|
||||
|
||||
|
||||
def settlement_to_fill(
|
||||
st: OptionSettlement, *, qty_eth: float
|
||||
) -> tuple[float, float, float]:
|
||||
"""返回 (fill_px, fee, notional)。未找到则全 0。"""
|
||||
if not st.found:
|
||||
return 0.0, 0.0, 0.0
|
||||
q = float(qty_eth)
|
||||
if q > 1e-12 and st.fill_px <= 0 and st.notional > 0:
|
||||
return float(st.notional) / q, float(st.fee), float(st.notional)
|
||||
return float(st.fill_px), float(st.fee), float(st.notional)
|
||||
|
||||
|
||||
def fetch_option_settlement(
|
||||
client: Any,
|
||||
*,
|
||||
exchange: str,
|
||||
option_inst_id: str,
|
||||
qty_eth: float,
|
||||
begin_ms: int | None,
|
||||
end_ms: int | None = None,
|
||||
) -> OptionSettlement:
|
||||
"""查交易所期权交割/行权入账。查不到 → found=False(调用方零价镜像、不发明)。"""
|
||||
inst = str(option_inst_id or "").strip()
|
||||
if not inst or client is None:
|
||||
return OptionSettlement(found=False, detail="no_inst_or_client")
|
||||
begin = int(begin_ms or 0)
|
||||
end = int(end_ms or int(time.time() * 1000))
|
||||
if begin <= 0:
|
||||
# 无开仓时间:收窄到近 48h,避免扫全量
|
||||
begin = end - 48 * 3600 * 1000
|
||||
ex = (exchange or "").strip().lower()
|
||||
try:
|
||||
if ex in ("binance", "bn"):
|
||||
return _bn_settlement(client, inst, qty_eth=qty_eth, begin=begin, end=end)
|
||||
return _okx_settlement(client, inst, qty_eth=qty_eth, begin=begin, end=end)
|
||||
except Exception as e:
|
||||
logger.warning("fetch_option_settlement failed %s %s: %s", ex, inst, e)
|
||||
return OptionSettlement(found=False, detail=str(e)[:160])
|
||||
|
||||
|
||||
def _okx_settlement(
|
||||
client: Any, inst_id: str, *, qty_eth: float, begin: int, end: int
|
||||
) -> OptionSettlement:
|
||||
from .money import to_usdt
|
||||
from ..exchange.okx.parse import safe_float
|
||||
|
||||
getter = getattr(client, "get_option_settlement_bills", None)
|
||||
if callable(getter):
|
||||
rows = getter(inst_id, begin_ms=begin, end_ms=end)
|
||||
else:
|
||||
rows = None
|
||||
if rows is None:
|
||||
return OptionSettlement(found=False, detail="okx_bills_unavailable")
|
||||
|
||||
cash = 0.0
|
||||
fee = 0.0
|
||||
hit = False
|
||||
for row in rows:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
sub = str(row.get("subType") or "")
|
||||
typ = str(row.get("type") or "")
|
||||
if sub not in _OKX_EXERCISE_SUBTYPES and typ not in ("3",):
|
||||
continue
|
||||
ts = int(safe_float(row.get("ts")) or 0)
|
||||
if ts and (ts < begin - 120_000 or ts > end + 120_000):
|
||||
continue
|
||||
hit = True
|
||||
raw = safe_float(row.get("balChg"))
|
||||
if raw is None:
|
||||
raw = safe_float(row.get("pnl"))
|
||||
ccy = str(row.get("ccy") or "USDT")
|
||||
if raw is not None:
|
||||
cash += to_usdt(float(raw), ccy)
|
||||
fraw = safe_float(row.get("fee"))
|
||||
if fraw is not None:
|
||||
# OKX fee 常为负
|
||||
fee += abs(to_usdt(float(fraw), ccy))
|
||||
# 有些账单 pnl 已含费,fee 字段为 0
|
||||
|
||||
if not hit:
|
||||
return OptionSettlement(found=False, detail="okx_no_exercise_bill")
|
||||
|
||||
# balChg/pnl 视为账户净变动;fee 另计时用净额+费还原毛额作 fill notional
|
||||
net_cash = float(cash)
|
||||
fee = float(fee)
|
||||
if net_cash >= 0:
|
||||
notional = float(net_cash) + fee
|
||||
else:
|
||||
notional = 0.0
|
||||
|
||||
q = float(qty_eth)
|
||||
fill_px = (notional / q) if q > 1e-12 and notional > 0 else 0.0
|
||||
return OptionSettlement(
|
||||
found=True,
|
||||
notional=float(notional),
|
||||
fee=float(fee),
|
||||
cash=float(net_cash),
|
||||
fill_px=float(fill_px),
|
||||
source="okx_bills",
|
||||
detail=f"subTypes exercise bills cash={net_cash:.6f}",
|
||||
)
|
||||
|
||||
|
||||
def _bn_settlement(
|
||||
client: Any, symbol: str, *, qty_eth: float, begin: int, end: int
|
||||
) -> OptionSettlement:
|
||||
from .money import to_usdt
|
||||
from ..exchange.okx.parse import safe_float
|
||||
|
||||
getter = getattr(client, "get_option_exercise_records", None)
|
||||
if not callable(getter):
|
||||
return OptionSettlement(found=False, detail="bn_exercise_api_missing")
|
||||
rows = getter(symbol, begin_ms=begin, end_ms=end)
|
||||
if rows is None:
|
||||
return OptionSettlement(found=False, detail="bn_exercise_unavailable")
|
||||
if not rows:
|
||||
return OptionSettlement(found=False, detail="bn_no_exercise_record")
|
||||
|
||||
amount = 0.0
|
||||
fee = 0.0
|
||||
hit = False
|
||||
for row in rows:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
if str(row.get("symbol") or "") and str(row.get("symbol")) != symbol:
|
||||
continue
|
||||
hit = True
|
||||
ccy = str(row.get("currency") or row.get("quoteAsset") or "USDT")
|
||||
am = safe_float(row.get("amount"))
|
||||
if am is not None:
|
||||
amount += to_usdt(float(am), ccy)
|
||||
fr = safe_float(row.get("fee"))
|
||||
if fr is not None:
|
||||
fee += abs(to_usdt(float(fr), ccy))
|
||||
|
||||
if not hit:
|
||||
return OptionSettlement(found=False, detail="bn_no_matching_record")
|
||||
|
||||
notional = max(0.0, float(amount))
|
||||
net = float(amount) - float(fee)
|
||||
q = float(qty_eth)
|
||||
fill_px = (notional / q) if q > 1e-12 and notional > 0 else 0.0
|
||||
return OptionSettlement(
|
||||
found=True,
|
||||
notional=notional,
|
||||
fee=float(fee),
|
||||
cash=float(net),
|
||||
fill_px=float(fill_px),
|
||||
source="binance_exerciseRecord",
|
||||
detail=f"amount={amount:.6f} fee={fee:.6f}",
|
||||
)
|
||||
Reference in New Issue
Block a user