Reject OO strikes more than 1% from amplitude high/low.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -297,6 +297,12 @@ class StrategyEngine:
|
||||
)
|
||||
or s.oo_reward_ratio
|
||||
),
|
||||
"oo_strike_max_dev_pct": float(
|
||||
self.ledger.get_setting_float(
|
||||
"oo_strike_max_dev_pct", s.oo_strike_max_dev_pct
|
||||
)
|
||||
or s.oo_strike_max_dev_pct
|
||||
),
|
||||
"risk_perp_unit": risk_perp_unit,
|
||||
"risk_option_unit": risk_option_unit,
|
||||
"risk_exit_unit": risk_exit_unit,
|
||||
|
||||
@@ -36,17 +36,42 @@ class OoPickCore:
|
||||
detail: str = "ok"
|
||||
|
||||
|
||||
def pick_otm_call_strike(strikes: list[float], *, spot: float, high: float) -> float | None:
|
||||
"""虚值 Call:K > spot,优先贴近振幅高点。"""
|
||||
def _within_ref_pct(strike: float, ref: float, max_dev_pct: float) -> bool:
|
||||
"""|K−ref|/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:
|
||||
"""虚值 Call:K > 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) -> float | None:
|
||||
"""虚值 Put:K < spot,优先贴近振幅低点。"""
|
||||
def pick_otm_put_strike(
|
||||
strikes: list[float],
|
||||
*,
|
||||
spot: float,
|
||||
low: float,
|
||||
max_dev_pct: float = 1.0,
|
||||
) -> float | None:
|
||||
"""虚值 Put:K < 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))
|
||||
@@ -61,10 +86,11 @@ def select_oo_pair(
|
||||
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 可不同行权价;须同到期且均为虚值。
|
||||
Call/Put 可不同行权价;须同到期、均为虚值,且相对高低点偏离不超过 max_dev_pct%。
|
||||
"""
|
||||
if spot <= 0 or high <= 0 or low <= 0 or high < low:
|
||||
return None
|
||||
@@ -80,8 +106,12 @@ def select_oo_pair(
|
||||
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)
|
||||
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")
|
||||
@@ -113,8 +143,9 @@ def build_oo_pick_core(
|
||||
amplitude: AmplitudeHL | None = None,
|
||||
skip_expiry_ymds: set[str] | None = None,
|
||||
now: datetime | None = None,
|
||||
max_dev_pct: float = 1.0,
|
||||
) -> OoPickCore | None:
|
||||
"""完整期期选约:振幅门 + 虚值双腿 + 杠杆。"""
|
||||
"""完整期期选约:振幅门 + 虚值双腿(贴高低≤max_dev%) + 杠杆。"""
|
||||
amp = amplitude or fetch_amplitude_hl_for_runtime(amplitude_hours)
|
||||
if amp is None:
|
||||
return None
|
||||
@@ -130,6 +161,7 @@ def build_oo_pick_core(
|
||||
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
|
||||
|
||||
@@ -192,8 +192,8 @@ def _hedge_mode() -> str:
|
||||
return "perp_option"
|
||||
|
||||
|
||||
def _oo_settings() -> tuple[float, float, float, float]:
|
||||
"""amplitude_pct, amplitude_hours, min_option_hours, min_leverage"""
|
||||
def _oo_settings() -> tuple[float, float, float, float, float]:
|
||||
"""amplitude_pct, amplitude_hours, min_option_hours, min_leverage, strike_max_dev_pct"""
|
||||
s = get_settings()
|
||||
try:
|
||||
from ..models.db import get_db
|
||||
@@ -210,6 +210,12 @@ def _oo_settings() -> tuple[float, float, float, float]:
|
||||
or s.oo_min_option_hours
|
||||
),
|
||||
float(db.get_setting("oo_min_leverage", str(s.oo_min_leverage)) or s.oo_min_leverage),
|
||||
float(
|
||||
db.get_setting(
|
||||
"oo_strike_max_dev_pct", str(s.oo_strike_max_dev_pct)
|
||||
)
|
||||
or s.oo_strike_max_dev_pct
|
||||
),
|
||||
)
|
||||
except Exception:
|
||||
return (
|
||||
@@ -217,6 +223,7 @@ def _oo_settings() -> tuple[float, float, float, float]:
|
||||
s.oo_amplitude_hours,
|
||||
s.oo_min_option_hours,
|
||||
s.oo_min_leverage,
|
||||
s.oo_strike_max_dev_pct,
|
||||
)
|
||||
|
||||
|
||||
@@ -450,7 +457,7 @@ class StrategySession:
|
||||
try:
|
||||
from ..exchange.candles import fetch_amplitude_hl_for_runtime
|
||||
|
||||
amp_pct, amp_hours, _, _ = _oo_settings()
|
||||
amp_pct, amp_hours, _, _, _ = _oo_settings()
|
||||
amp = fetch_amplitude_hl_for_runtime(amp_hours)
|
||||
if amp is None:
|
||||
return self._oo_amp
|
||||
@@ -477,7 +484,7 @@ class StrategySession:
|
||||
if _has_open_position():
|
||||
return self.align_to_held_position()
|
||||
s = self.settings
|
||||
amp_pct, amp_hours, min_hours, _min_lev = _oo_settings()
|
||||
amp_pct, amp_hours, min_hours, _min_lev, max_dev = _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:
|
||||
@@ -496,10 +503,11 @@ class StrategySession:
|
||||
low=amp_low,
|
||||
min_hours=float(min_hours),
|
||||
skip_expiry_ymds=skip,
|
||||
max_dev_pct=float(max_dev),
|
||||
)
|
||||
if picked is None:
|
||||
raise RuntimeError(
|
||||
f"未找到剩余≥{min_hours}h 的虚值 Call@高/Put@低"
|
||||
f"未找到剩余≥{min_hours}h 且贴高低≤{max_dev:g}% 的虚值 Call/Put"
|
||||
)
|
||||
ymd, ems, ck, pk, call_inst, put_inst = picked
|
||||
pair = OptionPair(
|
||||
@@ -527,7 +535,7 @@ class StrategySession:
|
||||
from .selection import _complete_by_expiry, option_leverage
|
||||
|
||||
s = self.settings
|
||||
amp_pct, amp_hours, min_hours, min_lev = _oo_settings()
|
||||
amp_pct, amp_hours, min_hours, min_lev, max_dev = _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:
|
||||
@@ -565,9 +573,13 @@ class StrategySession:
|
||||
low=float(amp.low),
|
||||
min_hours=float(min_hours),
|
||||
skip_expiry_ymds=skip,
|
||||
max_dev_pct=float(max_dev),
|
||||
)
|
||||
if picked is None:
|
||||
logger.info("oo: no OTM call/put pair for amplitude HL")
|
||||
logger.info(
|
||||
"oo: no OTM call/put within %.2f%% of amplitude HL",
|
||||
max_dev,
|
||||
)
|
||||
return None
|
||||
ymd, ems, ck, pk, call_inst, put_inst = picked
|
||||
call_bids, call_asks, _ = self.ex.fetch_book(call_inst, depth=5)
|
||||
@@ -840,7 +852,7 @@ class StrategySession:
|
||||
def oo_needs_realign(self) -> bool:
|
||||
if self._pair is None:
|
||||
return True
|
||||
_amp_pct, amp_hours, min_hours, _ = _oo_settings()
|
||||
_amp_pct, amp_hours, min_hours, _, max_dev = _oo_settings()
|
||||
if (
|
||||
hours_until_expiry(self._pair.expiry_ymd, expiry_ms=self._pair.expiry_ms)
|
||||
+ 1e-9
|
||||
@@ -877,6 +889,7 @@ class StrategySession:
|
||||
low=float(amp.low),
|
||||
min_hours=float(min_hours),
|
||||
skip_expiry_ymds=skip,
|
||||
max_dev_pct=float(max_dev),
|
||||
)
|
||||
if picked is None:
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user