diff --git a/backend/app/config.py b/backend/app/config.py index c405734..58297c9 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -82,7 +82,7 @@ class Settings(BaseSettings): martingale_max_doubles: int = 3 # 最多翻倍次数(如 2→4→8→16 为 3 次) # 对冲模式:perp_option=永期(默认)| option_option=期期 hedge_mode: str = "perp_option" - oo_amplitude_pct: float = 1.5 # 振幅最小 %(回看窗内高低) + oo_amplitude_pct: float = 1.5 # 振幅最大 %(回看窗内高低;超过则不开) oo_amplitude_hours: float = 12.0 # 振幅回看小时 oo_min_option_hours: float = 24.0 # 期期:最短剩余到期小时 oo_min_leverage: float = 200.0 # 期期:单腿最低杠杆 diff --git a/backend/app/strategy/oo_selection.py b/backend/app/strategy/oo_selection.py index b0f3d27..a894ac7 100644 --- a/backend/app/strategy/oo_selection.py +++ b/backend/app/strategy/oo_selection.py @@ -118,7 +118,7 @@ def build_oo_pick_core( 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): + if float(amp.range_pct) > float(amplitude_pct) + 1e-12: return None if spot <= 0: spot = float(amp.mid) diff --git a/backend/app/strategy/session.py b/backend/app/strategy/session.py index 1bf45a8..0ccba8e 100644 --- a/backend/app/strategy/session.py +++ b/backend/app/strategy/session.py @@ -442,7 +442,7 @@ class StrategySession: return self._apply_pair(pair, mark=float(mark), idx=idx) def align_oo_instruments(self) -> OptionPair | None: - """期期监控:按振幅高低点选虚值 Call/Put(展示用;振幅不足仍对齐候选)。""" + """期期监控:按振幅高低点选虚值 Call/Put(展示用;振幅超限仍对齐候选)。""" from ..exchange.candles import fetch_amplitude_hl_for_runtime from .oo_selection import select_oo_pair @@ -465,8 +465,8 @@ class StrategySession: "mid": float(amp.mid), "range_pct": float(amp.range_pct), "hours": float(amp_hours), - "min_pct": float(amp_pct), - "ok": float(amp.range_pct) + 1e-12 >= float(amp_pct), + "max_pct": float(amp_pct), + "ok": float(amp.range_pct) <= float(amp_pct) + 1e-12, } contracts = self.ex.list_option_contracts(s.option_inst_family) skip = _skip_expiry_ymds_for_next() @@ -518,9 +518,9 @@ class StrategySession: if amp is None: logger.info("oo: amplitude candles unavailable") return None - if float(amp.range_pct) + 1e-12 < float(amp_pct): + if float(amp.range_pct) > float(amp_pct) + 1e-12: logger.info( - "oo: amplitude %.3f%% < need %.3f%% (H=%.2f L=%.2f)", + "oo: amplitude %.3f%% > max %.3f%% (H=%.2f L=%.2f)", amp.range_pct, amp_pct, amp.high, @@ -584,7 +584,7 @@ class StrategySession: "mid": float(amp.mid), "range_pct": float(amp.range_pct), "hours": float(amp_hours), - "min_pct": float(amp_pct), + "max_pct": float(amp_pct), "ok": True, } self._apply_pair(pair, mark=underlying, idx=idx) @@ -837,8 +837,8 @@ class StrategySession: "mid": float(amp.mid), "range_pct": float(amp.range_pct), "hours": float(amp_hours), - "min_pct": float(_amp_pct), - "ok": float(amp.range_pct) + 1e-12 >= float(_amp_pct), + "max_pct": float(_amp_pct), + "ok": float(amp.range_pct) <= float(_amp_pct) + 1e-12, } contracts = self.ex.list_option_contracts(self.settings.option_inst_family) picked = select_oo_pair( diff --git a/backend/tests/test_oo_selection_sizing.py b/backend/tests/test_oo_selection_sizing.py index 9bd14d5..79ac0d2 100644 --- a/backend/tests/test_oo_selection_sizing.py +++ b/backend/tests/test_oo_selection_sizing.py @@ -64,3 +64,49 @@ def test_compute_oo_sizing_1_1_and_reward() -> None: def test_amplitude_range_pct() -> None: a = AmplitudeHL(high=2030, low=1970, mid=2000, hours=12, bar_count=12) assert abs(a.range_pct - 3.0) < 1e-9 + + +def test_amplitude_max_gate() -> None: + from app.strategy.oo_selection import build_oo_pick_core + + contracts = [] + for k in (1900, 2000, 2100): + for side, letter in (("call", "C"), ("put", "P")): + contracts.append( + { + "expiry_ymd": "260810", + "expiry_ms": 1_786_320_000_000, + "strike": float(k), + "side": letter, + "inst_id": f"ETH-{k}-{letter}", + } + ) + amp = AmplitudeHL(high=2030, low=1970, mid=2000, hours=12, bar_count=12) + # 3% > 上限 1.5% → 拒 + assert ( + build_oo_pick_core( + contracts=contracts, + spot=2000, + call_ask=5, + put_ask=5, + min_hours=1, + min_leverage=1, + amplitude_hours=12, + amplitude_pct=1.5, + amplitude=amp, + ) + is None + ) + # 3% ≤ 上限 3.5% → 可过振幅门(杠杆/卖一足够) + ok = build_oo_pick_core( + contracts=contracts, + spot=2000, + call_ask=5, + put_ask=5, + min_hours=1, + min_leverage=1, + amplitude_hours=12, + amplitude_pct=3.5, + amplitude=amp, + ) + assert ok is not None diff --git a/docs/期期对冲说明.md b/docs/期期对冲说明.md index 9aaf66b..6b65db9 100644 --- a/docs/期期对冲说明.md +++ b/docs/期期对冲说明.md @@ -12,7 +12,7 @@ | 参数 | 默认 | 含义 | |------|------|------| -| 振幅最小 % | 1.5 | 回看窗内 `(高-低)/中价` 须 ≥ 该值 | +| 振幅最大 % | 1.5 | 回看窗内 `(高-低)/中价` 须 ≤ 该值,超过不开 | | 振幅回看小时 | 12 | 用 1H K 线取真实高低点 | | 最短剩余到期 | 24 | 期权剩余小时 | | 单腿最低杠杆 | 200 | `指数 / 卖一` | diff --git a/docs/策略说明.md b/docs/策略说明.md index e613e24..7d92140 100644 --- a/docs/策略说明.md +++ b/docs/策略说明.md @@ -458,7 +458,7 @@ LIVE 特殊态:`option_closed_perp_pending`(期权已在交易所卖掉、 | `residual_min_premium_pct` | 20 | 残留权利金回收门槛% | | `residual_close_check_sec` | 300 | 残留巡检间隔秒 | | `hedge_mode` | perp_option | 永期 / 期期二选一 | -| `oo_amplitude_pct` | 1.5 | 期期振幅最小% | +| `oo_amplitude_pct` | 1.5 | 期期振幅最大%(超过不开) | | `oo_amplitude_hours` | 12 | 期期振幅回看小时 | | `oo_min_option_hours` | 24 | 期期最短剩余到期 | | `oo_min_leverage` | 200 | 期期单腿最低杠杆 | @@ -471,7 +471,7 @@ LIVE 特殊态:`option_closed_perp_pending`(期权已在交易所卖掉、 详见 [期期对冲说明](./期期对冲说明.md)。摘要: -- 回看 `oo_amplitude_hours` 的真实高低;振幅不足不开。 +- 回看 `oo_amplitude_hours` 的真实高低;振幅超过 `oo_amplitude_pct` 不开。 - 虚值 Call 贴高、Put 贴低;同到期;杠杆与剩余小时门槛。 - 预算 1:1、qty 一位小数、目标 = B × `oo_reward_ratio`。 - 达标只平盈利腿;亏损腿 residual(20% 回升或到期)。 diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 26fbe84..f12fe12 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -211,6 +211,8 @@ export type MarketSnapshot = { mid?: number; range_pct?: number; hours?: number; + max_pct?: number; + /** @deprecated 兼容旧字段,等同 max_pct */ min_pct?: number; ok?: boolean; } | null; diff --git a/frontend/src/pages/Plan.tsx b/frontend/src/pages/Plan.tsx index da70cad..6a2bdea 100644 --- a/frontend/src/pages/Plan.tsx +++ b/frontend/src/pages/Plan.tsx @@ -261,7 +261,7 @@ export default function PlanPage() { ? `ATM偏差≤${fmt(plan?.max_atm_open_offset ?? 3, 0)}` : "ATM偏差关"; const ooSelectLabel = [ - `振幅≥${fmt(plan?.oo_amplitude_pct ?? 1.5, 1)}%/${fmt(plan?.oo_amplitude_hours ?? 12, 0)}h`, + `振幅≤${fmt(plan?.oo_amplitude_pct ?? 1.5, 1)}%/${fmt(plan?.oo_amplitude_hours ?? 12, 0)}h`, "虚值Call@高·Put@低", `剩余≥${fmt(plan?.oo_min_option_hours ?? 24, 0)}h`, `杠杆≥${fmt(plan?.oo_min_leverage ?? 200, 0)}x`, @@ -897,13 +897,20 @@ export default function PlanPage() { {snap?.oo_amplitude?.range_pct != null ? `${fmt(snap.oo_amplitude.range_pct, 2)}%` : "—"} - {snap?.oo_amplitude?.min_pct != null - ? ` · 门限≥${fmt(snap.oo_amplitude.min_pct, 1)}%` + {(snap?.oo_amplitude?.max_pct ?? snap?.oo_amplitude?.min_pct) != + null + ? ` · 上限≤${fmt( + Number( + snap.oo_amplitude?.max_pct ?? + snap.oo_amplitude?.min_pct, + ), + 1, + )}%` : ""} {snap?.oo_amplitude?.ok === false - ? " · 不足" + ? " · 超限" : snap?.oo_amplitude?.ok === true - ? " · 达标" + ? " · 可开" : ""} diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index e5a5e8e..9e72cf2 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -1220,7 +1220,7 @@ export default function SettingsPage() { {isOo ? ( <>
+ 回看窗内振幅超过该上限则不开仓。 +