Add ATM open-offset toggle default off.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-25 12:42:20 +08:00
parent 79c074ec6c
commit fff285c1f9
12 changed files with 93 additions and 24 deletions
+8
View File
@@ -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
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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
+1
View File
@@ -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),
+5 -1
View File
@@ -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
+8 -2
View File
@@ -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
+26 -8
View File
@@ -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
+5 -3
View File
@@ -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