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:
@@ -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(两腿不同 strike,call/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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user