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
+174
View File
@@ -157,6 +157,49 @@ def _option_side_for_perp(perp_side: str) -> str:
return "put" if (perp_side or "").strip().lower() == "long" else "call"
def _hedge_mode() -> str:
s = get_settings()
try:
from ..models.db import get_db
raw = str(
get_db().get_setting("hedge_mode", s.hedge_mode) or s.hedge_mode
).strip().lower()
if raw in ("perp_option", "option_option"):
return raw
except Exception:
pass
return "perp_option"
def _oo_settings() -> tuple[float, float, float, float]:
"""amplitude_pct, amplitude_hours, min_option_hours, min_leverage"""
s = get_settings()
try:
from ..models.db import get_db
db = get_db()
return (
float(db.get_setting("oo_amplitude_pct", str(s.oo_amplitude_pct)) or s.oo_amplitude_pct),
float(
db.get_setting("oo_amplitude_hours", str(s.oo_amplitude_hours))
or s.oo_amplitude_hours
),
float(
db.get_setting("oo_min_option_hours", str(s.oo_min_option_hours))
or s.oo_min_option_hours
),
float(db.get_setting("oo_min_leverage", str(s.oo_min_leverage)) or s.oo_min_leverage),
)
except Exception:
return (
s.oo_amplitude_pct,
s.oo_amplitude_hours,
s.oo_min_option_hours,
s.oo_min_leverage,
)
@dataclass(slots=True)
class OpenPick:
pair: OptionPair
@@ -169,6 +212,17 @@ class OpenPick:
option_leverage: float
hours_left: float
underlying_px: float
hedge_mode: str = "perp_option"
call_inst_id: str | None = None
put_inst_id: str | None = None
call_strike: float | None = None
put_strike: float | None = None
call_leverage: float | None = None
put_leverage: float | None = None
amplitude_high: float | None = None
amplitude_low: float | None = None
amplitude_range_pct: float | None = None
oo_detail: str | None = None
class StrategySession:
@@ -323,6 +377,125 @@ class StrategySession:
return self._apply_pair(pair, mark=float(mark), idx=idx)
def pick_for_open(self) -> OpenPick | None:
if _hedge_mode() == "option_option":
return self._pick_for_open_oo()
return self._pick_for_open_perp()
def _pick_for_open_oo(self) -> OpenPick | None:
from ..exchange.candles import fetch_amplitude_hl_for_runtime
from .oo_selection import (
pick_otm_call_strike,
pick_otm_put_strike,
select_oo_pair,
)
from .selection import _complete_by_expiry, option_leverage
s = self.settings
amp_pct, amp_hours, min_hours, min_lev = _oo_settings()
idx = self.ex.fetch_index(s.index_inst_id)
mark = self.ex.fetch_mark(s.perp_inst_id) or idx
if mark is None or mark <= 0:
return None
underlying = float(mark)
amp = fetch_amplitude_hl_for_runtime(amp_hours)
if amp is None:
logger.info("oo: amplitude candles unavailable")
return None
if float(amp.range_pct) + 1e-12 < float(amp_pct):
logger.info(
"oo: amplitude %.3f%% < need %.3f%% (H=%.2f L=%.2f)",
amp.range_pct,
amp_pct,
amp.high,
amp.low,
)
return None
contracts = self.ex.list_option_contracts(s.option_inst_family)
skip = _skip_expiry_ymds_for_next()
picked = select_oo_pair(
contracts,
spot=underlying,
high=float(amp.high),
low=float(amp.low),
min_hours=float(min_hours),
skip_expiry_ymds=skip,
)
if picked is None:
logger.info("oo: no OTM call/put pair for amplitude HL")
return None
ymd, ems, ck, pk, call_inst, put_inst = picked
call_bids, call_asks, _ = self.ex.fetch_book(call_inst, depth=5)
put_bids, put_asks, _ = self.ex.fetch_book(put_inst, depth=5)
call_ask = call_asks[0].px if call_asks else None
put_ask = put_asks[0].px if put_asks else None
if call_ask is None:
cq = self.ex.quote(call_inst)
call_ask = cq.ask if cq else None
if put_ask is None:
pq = self.ex.quote(put_inst)
put_ask = pq.ask if pq else None
if call_ask is None or put_ask is None or call_ask <= 0 or put_ask <= 0:
logger.info("oo: missing ask call=%s put=%s", call_ask, put_ask)
return None
c_lev = option_leverage(underlying, float(call_ask))
p_lev = option_leverage(underlying, float(put_ask))
if (
c_lev is None
or p_lev is None
or c_lev + 1e-9 < min_lev
or p_lev + 1e-9 < min_lev
):
logger.info(
"oo: leverage too low call=%s put=%s need>=%.0f",
f"{c_lev:.1f}" if c_lev else "n/a",
f"{p_lev:.1f}" if p_lev else "n/a",
min_lev,
)
return None
# 监控用:用 Call 行权价构造假 pair(两腿不同 strikecall/put inst 正确)
pair = OptionPair(
expiry_ymd=ymd,
expiry_ms=int(ems),
strike=float(ck),
call_inst_id=call_inst,
put_inst_id=put_inst,
)
self._apply_pair(pair, mark=underlying, idx=idx)
if hasattr(self.ex, "cache"):
from ..exchange.book_cache import BookCache
cache: BookCache = self.ex.cache # type: ignore[attr-defined]
cache.upsert_book(call_inst, bids=call_bids, asks=call_asks)
cache.upsert_book(put_inst, bids=put_bids, asks=put_asks)
hours_left = hours_until_expiry(ymd, expiry_ms=ems)
return OpenPick(
pair=pair,
option_side="call",
perp_side="",
bias="option_option",
call_ask=float(call_ask),
put_ask=float(put_ask),
option_ask=float(call_ask),
option_leverage=float(min(c_lev, p_lev)),
hours_left=hours_left,
underlying_px=underlying,
hedge_mode="option_option",
call_inst_id=call_inst,
put_inst_id=put_inst,
call_strike=float(ck),
put_strike=float(pk),
call_leverage=float(c_lev),
put_leverage=float(p_lev),
amplitude_high=float(amp.high),
amplitude_low=float(amp.low),
amplitude_range_pct=float(amp.range_pct),
oo_detail=(
f"amp={amp.range_pct:.2f}% H={amp.high:.2f} L={amp.low:.2f} "
f"C@{ck:g} P@{pk:g}"
),
)
def _pick_for_open_perp(self) -> OpenPick | None:
from .signal import decide, decide_fixed
s = self.settings
@@ -443,6 +616,7 @@ class StrategySession:
option_leverage=float(lev),
hours_left=hours_left,
underlying_px=underlying,
hedge_mode="perp_option",
)
return None