Files

204 lines
5.8 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.
"""期期对冲选约:振幅高低点匹配虚值 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 _within_ref_pct(strike: float, ref: float, max_dev_pct: float) -> bool:
"""|Kref|/ref ≤ max_dev_pct%"""
if ref <= 0 or max_dev_pct < 0:
return False
return abs(float(strike) - float(ref)) / float(ref) * 100.0 <= float(
max_dev_pct
) + 1e-12
def pick_otm_call_strike(
strikes: list[float],
*,
spot: float,
high: float,
max_dev_pct: float = 1.0,
) -> float | None:
"""虚值 CallK > spot,贴近振幅高点,且 |K−高|/高 ≤ max_dev_pct%"""
cands = [float(s) for s in strikes if float(s) > float(spot) + 1e-9]
if max_dev_pct >= 0 and high > 0:
cands = [s for s in cands if _within_ref_pct(s, high, max_dev_pct)]
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,
max_dev_pct: float = 1.0,
) -> float | None:
"""虚值 PutK < spot,贴近振幅低点,且 |K−低|/低 ≤ max_dev_pct%"""
cands = [float(s) for s in strikes if float(s) < float(spot) - 1e-9]
if max_dev_pct >= 0 and low > 0:
cands = [s for s in cands if _within_ref_pct(s, low, max_dev_pct)]
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,
max_dev_pct: float = 1.0,
) -> tuple[str, int, float, float, str, str] | None:
"""
返回 (expiry_ymd, expiry_ms, call_strike, put_strike, call_inst, put_inst)。
Call/Put 可不同行权价;须同到期、均为虚值,且相对高低点偏离不超过 max_dev_pct%
"""
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, max_dev_pct=max_dev_pct
)
pk = pick_otm_put_strike(
strikes, spot=spot, low=low, max_dev_pct=max_dev_pct
)
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,
max_dev_pct: float = 1.0,
amplitude_filter_enabled: bool = True,
) -> OoPickCore | None:
"""完整期期选约:振幅门(可关)+ 虚值双腿(贴高低≤max_dev% + 杠杆。"""
amp = amplitude or fetch_amplitude_hl_for_runtime(amplitude_hours)
if amp is None:
return None
if amplitude_filter_enabled and float(amp.range_pct) > float(amplitude_pct) + 1e-12:
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,
max_dev_pct=float(max_dev_pct),
)
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}"
),
)