Add option-option hedge mode with SIM/LIVE parity.

Mutual hedge_mode, amplitude OTM selection, 1:1 risk sizing, win-leg/full close, dual audits and docs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-08-07 16:01:16 +08:00
parent 15fe2f72dc
commit ec244c63c6
22 changed files with 2640 additions and 83 deletions
+170
View File
@@ -0,0 +1,170 @@
"""期期对冲选约:振幅高低点匹配虚值 Call + Put。"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from typing import Any
from ..exchange.candles import AmplitudeHL, fetch_amplitude_hl_for_runtime
from .selection import (
_complete_by_expiry,
hours_until_ms,
list_eligible_expiry_ymds,
option_leverage,
)
@dataclass(frozen=True, slots=True)
class OoLeg:
side: str # call|put
strike: float
inst_id: str
ask: float
leverage: float
@dataclass(frozen=True, slots=True)
class OoPickCore:
expiry_ymd: str
expiry_ms: int
hours_left: float
underlying_px: float
amplitude: AmplitudeHL
call: OoLeg
put: OoLeg
detail: str = "ok"
def pick_otm_call_strike(strikes: list[float], *, spot: float, high: float) -> float | None:
"""虚值 CallK > spot,优先贴近振幅高点。"""
cands = [float(s) for s in strikes if float(s) > float(spot) + 1e-9]
if not cands:
return None
return min(cands, key=lambda s: (abs(s - float(high)), s))
def pick_otm_put_strike(strikes: list[float], *, spot: float, low: float) -> float | None:
"""虚值 PutK < spot,优先贴近振幅低点。"""
cands = [float(s) for s in strikes if float(s) < float(spot) - 1e-9]
if not cands:
return None
return min(cands, key=lambda s: (abs(s - float(low)), s))
def select_oo_pair(
contracts: list[dict[str, Any]],
*,
spot: float,
high: float,
low: float,
min_hours: float,
now: datetime | None = None,
skip_expiry_ymds: set[str] | None = None,
) -> tuple[str, int, float, float, str, str] | None:
"""
返回 (expiry_ymd, expiry_ms, call_strike, put_strike, call_inst, put_inst)。
Call/Put 可不同行权价;须同到期且均为虚值。
"""
if spot <= 0 or high <= 0 or low <= 0 or high < low:
return None
complete = _complete_by_expiry(contracts)
if not complete:
return None
skip = skip_expiry_ymds or set()
eligible = [
y
for y in list_eligible_expiry_ymds(contracts, min_hours=min_hours, now=now)
if y not in skip
]
for ymd in eligible:
ems, strikes_map = complete[ymd]
strikes = list(strikes_map.keys())
ck = pick_otm_call_strike(strikes, spot=spot, high=high)
pk = pick_otm_put_strike(strikes, spot=spot, low=low)
if ck is None or pk is None:
continue
call_inst = strikes_map[ck].get("C")
put_inst = strikes_map[pk].get("P")
if not call_inst or not put_inst:
continue
hours_left = hours_until_ms(ems, now)
return (
ymd,
int(ems),
float(ck),
float(pk),
str(call_inst),
str(put_inst),
)
return None
def build_oo_pick_core(
*,
contracts: list[dict[str, Any]],
spot: float,
call_ask: float,
put_ask: float,
min_hours: float,
min_leverage: float,
amplitude_hours: float,
amplitude_pct: float,
amplitude: AmplitudeHL | None = None,
skip_expiry_ymds: set[str] | None = None,
now: datetime | None = None,
) -> OoPickCore | None:
"""完整期期选约:振幅门 + 虚值双腿 + 杠杆。"""
amp = amplitude or fetch_amplitude_hl_for_runtime(amplitude_hours)
if amp is None:
return None
if float(amp.range_pct) + 1e-12 < float(amplitude_pct):
return None
if spot <= 0:
spot = float(amp.mid)
picked = select_oo_pair(
contracts,
spot=float(spot),
high=float(amp.high),
low=float(amp.low),
min_hours=float(min_hours),
now=now,
skip_expiry_ymds=skip_expiry_ymds,
)
if picked is None:
return None
ymd, ems, ck, pk, call_inst, put_inst = picked
if call_ask <= 0 or put_ask <= 0:
return None
c_lev = option_leverage(float(spot), float(call_ask))
p_lev = option_leverage(float(spot), float(put_ask))
if c_lev is None or p_lev is None:
return None
if c_lev + 1e-12 < float(min_leverage) or p_lev + 1e-12 < float(min_leverage):
return None
hours_left = hours_until_ms(ems, now)
return OoPickCore(
expiry_ymd=ymd,
expiry_ms=int(ems),
hours_left=float(hours_left),
underlying_px=float(spot),
amplitude=amp,
call=OoLeg(
side="call",
strike=float(ck),
inst_id=call_inst,
ask=float(call_ask),
leverage=float(c_lev),
),
put=OoLeg(
side="put",
strike=float(pk),
inst_id=put_inst,
ask=float(put_ask),
leverage=float(p_lev),
),
detail=(
f"amp={amp.range_pct:.2f}% H={amp.high:.2f} L={amp.low:.2f} "
f"C@{ck:g} P@{pk:g}"
),
)