diff --git a/backend/app/api/fleet.py b/backend/app/api/fleet.py index d37e087..185f8a1 100644 --- a/backend/app/api/fleet.py +++ b/backend/app/api/fleet.py @@ -322,6 +322,19 @@ async def fleet_status(_tok: Annotated[str, Depends(require_fleet_token)]) -> di "oo_amplitude_hours": _pick( "oo_amplitude_hours", float(settings.oo_amplitude_hours) ), + "oo_amplitude_filter_enabled": ( + str( + st.get("oo_amplitude_filter_enabled") + if "oo_amplitude_filter_enabled" in st + else db.get_setting( + "oo_amplitude_filter_enabled", + str(settings.oo_amplitude_filter_enabled), + ) + ) + .strip() + .lower() + in ("1", "true", "yes", "on") + ), "oo_min_option_hours": _pick( "oo_min_option_hours", float(settings.oo_min_option_hours) ), diff --git a/backend/app/api/settings.py b/backend/app/api/settings.py index ce57137..c0e5f05 100644 --- a/backend/app/api/settings.py +++ b/backend/app/api/settings.py @@ -67,6 +67,7 @@ KEYS = ( "hedge_mode", "oo_amplitude_pct", "oo_amplitude_hours", + "oo_amplitude_filter_enabled", "oo_min_option_hours", "oo_min_leverage", "oo_reward_ratio", @@ -123,6 +124,7 @@ class StrategySettingsBody(BaseModel): ) oo_amplitude_pct: float | None = Field(default=None, ge=0.1, le=50) oo_amplitude_hours: float | None = Field(default=None, ge=1, le=168) + oo_amplitude_filter_enabled: bool | None = None oo_min_option_hours: float | None = Field(default=None, ge=1, le=720) oo_min_leverage: float | None = Field(default=None, ge=1, le=10000) oo_reward_ratio: float | None = Field(default=None, ge=0.5, le=20) @@ -365,6 +367,13 @@ def _read_settings() -> dict: db.get_setting("oo_amplitude_hours", str(s.oo_amplitude_hours)) or s.oo_amplitude_hours ), + "oo_amplitude_filter_enabled": _as_bool( + db.get_setting( + "oo_amplitude_filter_enabled", + str(s.oo_amplitude_filter_enabled), + ), + s.oo_amplitude_filter_enabled, + ), "oo_min_option_hours": float( db.get_setting("oo_min_option_hours", str(s.oo_min_option_hours)) or s.oo_min_option_hours @@ -476,6 +485,7 @@ async def put_strategy_settings( "hedge_mode", "oo_amplitude_pct", "oo_amplitude_hours", + "oo_amplitude_filter_enabled", "oo_min_option_hours", "oo_min_leverage", "oo_reward_ratio", diff --git a/backend/app/config.py b/backend/app/config.py index 23e994a..83e85da 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -82,8 +82,9 @@ 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_hours: float = 12.0 # 振幅回看小时 + oo_amplitude_pct: float = 1.5 # 振幅最大 %(过滤开启时:超过则不开) + oo_amplitude_hours: float = 12.0 # 振幅回看小时(选约高低点;过滤关也用) + oo_amplitude_filter_enabled: bool = False # 振幅过滤开关(默认关) oo_min_option_hours: float = 24.0 # 期期:最短剩余到期小时 oo_min_leverage: float = 200.0 # 期期:单腿最低杠杆 oo_reward_ratio: float = 2.0 # 盈亏比:出场目标 = 预算 × 比 diff --git a/backend/app/strategy/engine.py b/backend/app/strategy/engine.py index 3f85d55..dd08156 100644 --- a/backend/app/strategy/engine.py +++ b/backend/app/strategy/engine.py @@ -279,6 +279,16 @@ class StrategyEngine: ) or s.oo_amplitude_hours ), + "oo_amplitude_filter_enabled": str( + self.ledger.get_setting_str( + "oo_amplitude_filter_enabled", + str(s.oo_amplitude_filter_enabled), + ) + or s.oo_amplitude_filter_enabled + ) + .strip() + .lower() + in ("1", "true", "yes", "on"), "oo_min_option_hours": float( self.ledger.get_setting_float( "oo_min_option_hours", s.oo_min_option_hours diff --git a/backend/app/strategy/oo_selection.py b/backend/app/strategy/oo_selection.py index ad0ccb0..6bf0be8 100644 --- a/backend/app/strategy/oo_selection.py +++ b/backend/app/strategy/oo_selection.py @@ -144,12 +144,13 @@ def build_oo_pick_core( 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%) + 杠杆。""" + """完整期期选约:振幅门(可关)+ 虚值双腿(贴高低≤max_dev%) + 杠杆。""" amp = amplitude or fetch_amplitude_hl_for_runtime(amplitude_hours) if amp is None: return None - if float(amp.range_pct) > float(amplitude_pct) + 1e-12: + if amplitude_filter_enabled and 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 944d53f..fd3b587 100644 --- a/backend/app/strategy/session.py +++ b/backend/app/strategy/session.py @@ -192,13 +192,24 @@ def _hedge_mode() -> str: return "perp_option" -def _oo_settings() -> tuple[float, float, float, float, float]: - """amplitude_pct, amplitude_hours, min_option_hours, min_leverage, strike_max_dev_pct""" +def _oo_settings() -> tuple[float, float, float, float, float, bool]: + """amplitude_pct, amplitude_hours, min_option_hours, min_leverage, strike_max_dev_pct, amp_filter_on""" s = get_settings() try: from ..models.db import get_db db = get_db() + filt_raw = db.get_setting( + "oo_amplitude_filter_enabled", str(s.oo_amplitude_filter_enabled) + ) + filt_on = str(filt_raw or "").strip().lower() in ( + "1", + "true", + "yes", + "on", + ) + if filt_raw is None or filt_raw == "": + filt_on = bool(s.oo_amplitude_filter_enabled) return ( float(db.get_setting("oo_amplitude_pct", str(s.oo_amplitude_pct)) or s.oo_amplitude_pct), float( @@ -216,6 +227,7 @@ def _oo_settings() -> tuple[float, float, float, float, float]: ) or s.oo_strike_max_dev_pct ), + filt_on, ) except Exception: return ( @@ -224,6 +236,7 @@ def _oo_settings() -> tuple[float, float, float, float, float]: s.oo_min_option_hours, s.oo_min_leverage, s.oo_strike_max_dev_pct, + bool(s.oo_amplitude_filter_enabled), ) @@ -457,7 +470,7 @@ class StrategySession: try: from ..exchange.candles import fetch_amplitude_hl_for_runtime - amp_pct, amp_hours, _, _, _ = _oo_settings() + amp_pct, amp_hours, _, _, _, amp_filt = _oo_settings() amp = fetch_amplitude_hl_for_runtime(amp_hours) if amp is None: return self._oo_amp @@ -468,7 +481,12 @@ class StrategySession: "range_pct": float(amp.range_pct), "hours": float(amp_hours), "max_pct": float(amp_pct), - "ok": float(amp.range_pct) <= float(amp_pct) + 1e-12, + "filter_enabled": bool(amp_filt), + "ok": ( + True + if not amp_filt + else float(amp.range_pct) <= float(amp_pct) + 1e-12 + ), } return self._oo_amp except Exception: @@ -484,7 +502,7 @@ class StrategySession: if _has_open_position(): return self.align_to_held_position() s = self.settings - amp_pct, amp_hours, min_hours, _min_lev, max_dev = _oo_settings() + amp_pct, amp_hours, min_hours, _min_lev, max_dev, _amp_filt = _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: @@ -535,7 +553,7 @@ class StrategySession: from .selection import _complete_by_expiry, option_leverage s = self.settings - amp_pct, amp_hours, min_hours, min_lev, max_dev = _oo_settings() + amp_pct, amp_hours, min_hours, min_lev, max_dev, amp_filt = _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: @@ -553,9 +571,14 @@ class StrategySession: "range_pct": float(amp.range_pct), "hours": float(amp_hours), "max_pct": float(amp_pct), - "ok": float(amp.range_pct) <= float(amp_pct) + 1e-12, + "filter_enabled": bool(amp_filt), + "ok": ( + True + if not amp_filt + else float(amp.range_pct) <= float(amp_pct) + 1e-12 + ), } - if float(amp.range_pct) > float(amp_pct) + 1e-12: + if amp_filt and float(amp.range_pct) > float(amp_pct) + 1e-12: logger.info( "oo: amplitude %.3f%% > max %.3f%% (H=%.2f L=%.2f)", amp.range_pct, @@ -852,7 +875,7 @@ class StrategySession: def oo_needs_realign(self) -> bool: if self._pair is None: return True - _amp_pct, amp_hours, min_hours, _, max_dev = _oo_settings() + _amp_pct, amp_hours, min_hours, _, max_dev, amp_filt = _oo_settings() if ( hours_until_expiry(self._pair.expiry_ymd, expiry_ms=self._pair.expiry_ms) + 1e-9 @@ -879,7 +902,12 @@ class StrategySession: "range_pct": float(amp.range_pct), "hours": float(amp_hours), "max_pct": float(_amp_pct), - "ok": float(amp.range_pct) <= float(_amp_pct) + 1e-12, + "filter_enabled": bool(amp_filt), + "ok": ( + True + if not amp_filt + else 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 89679f9..665dc96 100644 --- a/backend/tests/test_oo_selection_sizing.py +++ b/backend/tests/test_oo_selection_sizing.py @@ -175,3 +175,18 @@ def test_amplitude_max_gate() -> None: assert ok is not None assert ok.call.strike == 2025.0 assert ok.put.strike == 1975.0 + # 过滤关闭:振幅 3% 超过 1.5% 上限也可过 + ok_off = 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, + max_dev_pct=1.0, + amplitude_filter_enabled=False, + ) + assert ok_off is not None diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index e9e91d5..3f1402f 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -370,6 +370,7 @@ export type PlanState = { hedge_mode?: "perp_option" | "option_option"; oo_amplitude_pct?: number; oo_amplitude_hours?: number; + oo_amplitude_filter_enabled?: boolean; oo_min_option_hours?: number; oo_min_leverage?: number; oo_reward_ratio?: number; @@ -425,12 +426,12 @@ export type StrategySettings = { hedge_mode?: "perp_option" | "option_option"; oo_amplitude_pct?: number; oo_amplitude_hours?: number; + oo_amplitude_filter_enabled?: boolean; oo_min_option_hours?: number; oo_min_leverage?: number; oo_reward_ratio?: number; oo_budget_cushion?: number; oo_strike_max_dev_pct?: number; - oo_strike_max_dev_pct?: number; exchange?: string; }; diff --git a/frontend/src/pages/Plan.tsx b/frontend/src/pages/Plan.tsx index afb7b2b..a804849 100644 --- a/frontend/src/pages/Plan.tsx +++ b/frontend/src/pages/Plan.tsx @@ -264,7 +264,9 @@ 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`, + plan?.oo_amplitude_filter_enabled + ? `振幅≤${fmt(plan?.oo_amplitude_pct ?? 1.5, 1)}%/${fmt(plan?.oo_amplitude_hours ?? 12, 0)}h` + : `振幅回看${fmt(plan?.oo_amplitude_hours ?? 12, 0)}h(过滤关)`, `贴高低≤${fmt(plan?.oo_strike_max_dev_pct ?? 1, 1)}%`, "虚值Call@高·Put@低", `剩余≥${fmt(plan?.oo_min_option_hours ?? 24, 0)}h`, diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index 29409a2..acc65a7 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -113,6 +113,7 @@ export default function SettingsPage() { ); const [ooAmpPct, setOoAmpPct] = useState(1.5); const [ooAmpHours, setOoAmpHours] = useState(12); + const [ooAmpFilterOn, setOoAmpFilterOn] = useState(false); const [ooMinHours, setOoMinHours] = useState(24); const [ooMinLev, setOoMinLev] = useState(200); const [ooStrikeDev, setOoStrikeDev] = useState(1); @@ -235,6 +236,7 @@ export default function SettingsPage() { ); setOoAmpPct(s.oo_amplitude_pct ?? 1.5); setOoAmpHours(s.oo_amplitude_hours ?? 12); + setOoAmpFilterOn(Boolean(s.oo_amplitude_filter_enabled)); setOoMinHours(s.oo_min_option_hours ?? 24); setOoMinLev(s.oo_min_leverage ?? 200); setOoStrikeDev(s.oo_strike_max_dev_pct ?? 1); @@ -412,6 +414,7 @@ export default function SettingsPage() { hedge_mode: hedgeMode, oo_amplitude_pct: ooAmpPct, oo_amplitude_hours: ooAmpHours, + oo_amplitude_filter_enabled: ooAmpFilterOn, oo_min_option_hours: ooMinHours, oo_min_leverage: ooMinLev, oo_strike_max_dev_pct: ooStrikeDev, @@ -739,29 +742,68 @@ export default function SettingsPage() { -
- 二选一。期期:近 N 小时振幅高低匹配虚值 Call/Put;达标只平盈利腿。 -
{isOo ? ( -- 目标盈利 = 以损预算 × 盈亏比(例预算 100U、比 2 → 目标 - 200U)。预算平分给 Call/Put,各按卖一独立定仓。 -
-- 选约杠杆:按「指数÷期权杠杆≥」估权利金算 - k,权利金再便宜也不放大仓位。 - 实际杠杆:按盘口卖一定仓,便宜时 k 会变大。 -
- {riskLossPct > 3 - ? "亏损幅度超过 3% 时不可启用倍投。" - : "连续亏损日达阈值后翻倍。到期结算无论盈亏(含小盈利)都按亏损日计;达标平仓盈利才打断连亏。"} -
{martingaleOn && riskLossPct <= 3 ? ( <> @@ -957,26 +989,6 @@ export default function SettingsPage() { ) } /> -- 例:幅度 {riskLossPct}%、连亏{" "} - {martingaleStartAfter} 天起翻、最多{" "} - {martingaleMaxDoubles} 次 →{" "} - {[0, 1, 2, 3] - .filter((d) => d <= martingaleMaxDoubles) - .map( - (d) => - `${Number( - ( - riskLossPct * - 2 ** d - ).toPrecision(6), - )}%`, - ) - .join(" → ")} -
> ) : null} @@ -1230,35 +1242,6 @@ export default function SettingsPage() {- 回看窗内振幅超过该上限则不开仓。 -
-- 虚值 Call/Put 行权价相对振幅高/低点 |K−HL|/HL - 不超过该比例(默认 1%)。 -
- 净盈 ≥ 预算×盈亏比时只平盈利腿;亏损腿进残留,按下方回收比例或到期结算。 -
- 开启后:历史上已开过某到期日(如 0803)后,不论今天还是明天都不能再开同到期,只能监控/开下一到期(如 0804)。 -