Files

181 lines
5.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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_ethcash 为入账净额(已扣费)。"""
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}",
)