diff --git a/backend/app/api/settings.py b/backend/app/api/settings.py index d6c6bb7..dc0a647 100644 --- a/backend/app/api/settings.py +++ b/backend/app/api/settings.py @@ -31,6 +31,7 @@ KEYS = ( "leverage", "min_option_hours", "min_option_leverage", + "atm_open_offset_enabled", "max_atm_open_offset", "close_bid_mark_max_pct", "perp_qty_eth", @@ -50,6 +51,7 @@ class StrategySettingsBody(BaseModel): leverage: float | None = Field(default=None, ge=1, le=125) min_option_hours: float | None = Field(default=None, ge=1, le=720) min_option_leverage: float | None = Field(default=None, ge=1, le=10000) + atm_open_offset_enabled: bool | None = None max_atm_open_offset: float | None = Field(default=None, ge=0, le=100) close_bid_mark_max_pct: float | None = Field(default=None, ge=1, le=100) perp_qty_eth: float | None = Field(default=None, ge=0.01, le=100) @@ -102,6 +104,12 @@ def _read_settings() -> dict: db.get_setting("min_option_leverage", str(s.min_option_leverage)) or s.min_option_leverage ), + "atm_open_offset_enabled": _as_bool( + db.get_setting( + "atm_open_offset_enabled", str(s.atm_open_offset_enabled) + ), + s.atm_open_offset_enabled, + ), "max_atm_open_offset": float( db.get_setting("max_atm_open_offset", str(s.max_atm_open_offset)) or s.max_atm_open_offset diff --git a/backend/app/api/sim.py b/backend/app/api/sim.py index ddce85c..dacad66 100644 --- a/backend/app/api/sim.py +++ b/backend/app/api/sim.py @@ -52,7 +52,7 @@ async def sim_open_group( if pick is None: raise HTTPException( status_code=409, - detail="无合格期权:请检查剩余时长、ATM距现价(≤开仓偏差上限)与杠杆(现价/卖一)", + detail="无合格期权:请检查剩余时长、ATM开仓偏差(若已开启)与杠杆(现价/卖一)", ) force = (body.force_option_side if body else None) or None diff --git a/backend/app/config.py b/backend/app/config.py index 8c59583..32ccb6e 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -61,7 +61,8 @@ class Settings(BaseSettings): leverage: float = 3.0 # 永续杠杆 min_option_hours: float = 12.0 # 期权最小剩余小时 min_option_leverage: float = 100.0 # 现价/卖一权利金 下限 - max_atm_open_offset: float = 3.0 # 开仓:|ATM行权价−标的| 上限(点) + atm_open_offset_enabled: bool = False # 开仓 ATM 偏差限制开关(默认关) + max_atm_open_offset: float = 3.0 # 开启后:|ATM行权价−标的| 上限(点) close_bid_mark_max_pct: float = 30.0 # 平仓:买一相对标记最大偏差% perp_qty_eth: float = 1.0 option_qty_eth: float = 2.0 diff --git a/backend/app/models/db.py b/backend/app/models/db.py index b511ccb..af27207 100644 --- a/backend/app/models/db.py +++ b/backend/app/models/db.py @@ -159,6 +159,7 @@ class Database: "leverage": str(s.leverage), "min_option_hours": str(s.min_option_hours), "min_option_leverage": str(s.min_option_leverage), + "atm_open_offset_enabled": str(s.atm_open_offset_enabled), "max_atm_open_offset": str(s.max_atm_open_offset), "close_bid_mark_max_pct": str(s.close_bid_mark_max_pct), "perp_qty_eth": str(s.perp_qty_eth), diff --git a/backend/app/strategy/engine.py b/backend/app/strategy/engine.py index fed1d2e..68fe085 100644 --- a/backend/app/strategy/engine.py +++ b/backend/app/strategy/engine.py @@ -52,6 +52,9 @@ class StrategyEngine: min_opt_lev = self.ledger.get_setting_float( "min_option_leverage", s.min_option_leverage ) + atm_off_on = self.ledger.get_setting_bool( + "atm_open_offset_enabled", s.atm_open_offset_enabled + ) max_atm_off = self.ledger.get_setting_float( "max_atm_open_offset", s.max_atm_open_offset ) @@ -80,6 +83,7 @@ class StrategyEngine: "leverage": leverage, "min_option_hours": min_hours, "min_option_leverage": min_opt_lev, + "atm_open_offset_enabled": atm_off_on, "max_atm_open_offset": max_atm_off, "can_open": allow_open, "last_error": last_error, @@ -310,7 +314,7 @@ class StrategyEngine: pick = await get_session().pick_for_open_async() if pick is None: self._set_state( - last_error="无合格期权:需剩余时长、ATM开仓偏差与杠杆同时满足" + last_error="无合格期权:需剩余时长、杠杆(及已开启的ATM偏差)同时满足" ) return diff --git a/backend/app/strategy/selection.py b/backend/app/strategy/selection.py index cb51d66..5e25e6a 100644 --- a/backend/app/strategy/selection.py +++ b/backend/app/strategy/selection.py @@ -52,9 +52,15 @@ def atm_open_offset(strike: float, mark_px: float) -> float: def atm_allows_open( - strike: float, mark_px: float, *, max_offset: float + strike: float, + mark_px: float, + *, + max_offset: float, + enabled: bool = False, ) -> bool: - """|strike − mark| ≤ max_offset 才允许开仓。""" + """开启限制时:|strike − mark| ≤ max_offset 才允许开仓;关闭则始终允许。""" + if not enabled: + return True if mark_px <= 0 or max_offset < 0: return False return atm_open_offset(strike, mark_px) <= float(max_offset) + 1e-9 diff --git a/backend/app/strategy/session.py b/backend/app/strategy/session.py index cee3c0c..b89e724 100644 --- a/backend/app/strategy/session.py +++ b/backend/app/strategy/session.py @@ -36,8 +36,14 @@ def _has_open_position() -> bool: return False -def _strategy_floats() -> tuple[float, float, float]: - """min_hours, min_leverage, max_atm_open_offset""" +def _as_bool_setting(raw: str | None, default: bool) -> bool: + if raw is None or raw == "": + return default + return str(raw).strip().lower() in ("1", "true", "yes", "on") + + +def _strategy_floats() -> tuple[float, float, float, bool]: + """min_hours, min_leverage, max_atm_open_offset, atm_open_offset_enabled""" s = get_settings() try: from ..models.db import get_db @@ -55,9 +61,18 @@ def _strategy_floats() -> tuple[float, float, float]: db.get_setting("max_atm_open_offset", str(s.max_atm_open_offset)) or s.max_atm_open_offset ) - return hours, lev, atm_off + atm_on = _as_bool_setting( + db.get_setting("atm_open_offset_enabled", str(s.atm_open_offset_enabled)), + s.atm_open_offset_enabled, + ) + return hours, lev, atm_off, atm_on except Exception: - return s.min_option_hours, s.min_option_leverage, s.max_atm_open_offset + return ( + s.min_option_hours, + s.min_option_leverage, + s.max_atm_open_offset, + s.atm_open_offset_enabled, + ) @dataclass(slots=True) @@ -142,7 +157,7 @@ class StrategySession: mark = self.ex.fetch_mark(s.perp_inst_id) or idx if mark is None or mark <= 0: raise RuntimeError("无法获取标的标记/指数价格,无法选 ATM") - min_hours, _, _ = _strategy_floats() + min_hours, _, _, _ = _strategy_floats() contracts = self.ex.list_option_contracts(s.option_inst_family) pair = select_option_pair(contracts, mark_px=float(mark), min_hours=min_hours) if pair is None: @@ -155,7 +170,7 @@ class StrategySession: from .signal import decide s = self.settings - min_hours, min_lev, max_atm_off = _strategy_floats() + min_hours, min_lev, max_atm_off, atm_off_on = _strategy_floats() 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: @@ -173,7 +188,10 @@ class StrategySession: continue offset = atm_open_offset(pair.strike, underlying) if not atm_allows_open( - pair.strike, underlying, max_offset=max_atm_off + pair.strike, + underlying, + max_offset=max_atm_off, + enabled=atm_off_on, ): logger.info( "skip expiry=%s strike=%.0f atm_offset=%.1f > max=%.1f", @@ -281,7 +299,7 @@ class StrategySession: def atm_needs_realign(self, mark_px: float | None = None) -> bool: if self._pair is None: return True - min_hours, _, _ = _strategy_floats() + min_hours, _, _, _ = _strategy_floats() if ( hours_until_expiry(self._pair.expiry_ymd, expiry_ms=self._pair.expiry_ms) + 1e-9 diff --git a/backend/tests/test_instruments.py b/backend/tests/test_instruments.py index fb15618..01a010e 100644 --- a/backend/tests/test_instruments.py +++ b/backend/tests/test_instruments.py @@ -80,6 +80,8 @@ def test_atm_allows_open_offset() -> None: from app.strategy.selection import atm_allows_open, atm_open_offset assert atm_open_offset(1850, 1852.5) == 2.5 - assert atm_allows_open(1850, 1852.5, max_offset=3) is True - assert atm_allows_open(1850, 1854, max_offset=3) is False - assert atm_allows_open(1850, 1860, max_offset=3) is False + # 默认关闭:不限制 + assert atm_allows_open(1850, 1860, max_offset=3, enabled=False) is True + assert atm_allows_open(1850, 1852.5, max_offset=3, enabled=True) is True + assert atm_allows_open(1850, 1854, max_offset=3, enabled=True) is False + assert atm_allows_open(1850, 1860, max_offset=3, enabled=True) is False diff --git a/docs/策略说明.md b/docs/策略说明.md index c095219..24f8212 100644 --- a/docs/策略说明.md +++ b/docs/策略说明.md @@ -58,7 +58,7 @@ → 非周末跳过(若开启) → 选到期:剩余时长 ≥ min_option_hours(默认 12h) → 该到期 ATM 行权价(最接近标的) - → |ATM − 标的| ≤ max_atm_open_offset(默认 3)否则跳过该到期 + → 若开启 atm_open_offset_enabled:|ATM − 标的| ≤ max_atm_open_offset(默认 3)否则跳过 → 选向:ATM 偏下→Call+空;偏上→Put+多;贴平→卖一比价 → 期权杠杆 = 标的价 ÷ 卖一权利金 ≥ min_option_leverage(默认 100) → 开永续 + 开期权(一组) @@ -71,8 +71,9 @@ |------|------|------| | `min_option_hours` | 12 | 过滤过近到期,减少刚开仓就到期 | | `min_option_leverage` | 100 | 权利金相对标的不能太贵(现价/卖一) | -| `max_atm_open_offset` | 3 | 开仓:\|ATM 行权价 − 标的\| 超过则不开 | -| ATM | — | 同到期、最接近指数/标记价的行权价(展示可偏离;开仓另受偏差上限约束) | +| `atm_open_offset_enabled` | false | 开仓 ATM 偏差限制开关(默认关) | +| `max_atm_open_offset` | 3 | 开关开启后:\|ATM − 标的\| 超过则不开 | +| ATM | — | 同到期、最接近指数/标记价的行权价 | 无合格合约时:状态停留等待,记录「无合格期权…」,不硬开。 @@ -240,7 +241,8 @@ | `skip_weekends` | true | 时间 | | `min_option_hours` | 12 | 选约 | | `min_option_leverage` | 100 | 选约 | -| `max_atm_open_offset` | 3 | 开仓 ATM 偏差 | +| `atm_open_offset_enabled` | false | 开仓 ATM 偏差开关 | +| `max_atm_open_offset` | 3 | 开仓 ATM 偏差上限 | | `close_bid_mark_max_pct` | 30 | 平仓流动性 | --- diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 13ad890..c2b7d4e 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -131,6 +131,7 @@ export type PlanState = { leverage: number; min_option_hours: number; min_option_leverage: number; + atm_open_offset_enabled?: boolean; max_atm_open_offset?: number; can_open: boolean; last_error: string | null; @@ -180,6 +181,7 @@ export type StrategySettings = { leverage: number; min_option_hours: number; min_option_leverage: number; + atm_open_offset_enabled: boolean; max_atm_open_offset: number; close_bid_mark_max_pct: number; perp_qty_eth: number; diff --git a/frontend/src/pages/Plan.tsx b/frontend/src/pages/Plan.tsx index a698583..85b1b56 100644 --- a/frontend/src/pages/Plan.tsx +++ b/frontend/src/pages/Plan.tsx @@ -197,8 +197,10 @@ export default function PlanPage() { 选约条件 剩余≥{fmt(plan?.min_option_hours, 0)}h · 期权杠杆≥ - {fmt(plan?.min_option_leverage, 0)}x · ATM偏差≤ - {fmt(plan?.max_atm_open_offset ?? 3, 0)} + {fmt(plan?.min_option_leverage, 0)}x + {plan?.atm_open_offset_enabled + ? ` · ATM偏差≤${fmt(plan?.max_atm_open_offset ?? 3, 0)}` + : " · ATM偏差关"}
+ 关闭时不限制 |ATM − 现价|;开启后超过下方点数则不开仓。 +
+- |ATM 行权价 − 标的价| 超过该值则不开仓。默认 3;币安行权价步进较粗时可能长时间无开仓机会。 + 仅在上方开关开启时生效。默认 3;币安行权价步进较粗时可能长时间无开仓机会。