Gate opens when ATM-spot offset exceeds configurable max (default 3).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -31,6 +31,7 @@ KEYS = (
|
||||
"leverage",
|
||||
"min_option_hours",
|
||||
"min_option_leverage",
|
||||
"max_atm_open_offset",
|
||||
"close_bid_mark_max_pct",
|
||||
"perp_qty_eth",
|
||||
"option_qty_eth",
|
||||
@@ -49,6 +50,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)
|
||||
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)
|
||||
option_qty_eth: float | None = Field(default=None, ge=0.01, le=100)
|
||||
@@ -100,6 +102,10 @@ def _read_settings() -> dict:
|
||||
db.get_setting("min_option_leverage", str(s.min_option_leverage))
|
||||
or s.min_option_leverage
|
||||
),
|
||||
"max_atm_open_offset": float(
|
||||
db.get_setting("max_atm_open_offset", str(s.max_atm_open_offset))
|
||||
or s.max_atm_open_offset
|
||||
),
|
||||
"close_bid_mark_max_pct": float(
|
||||
db.get_setting("close_bid_mark_max_pct", str(s.close_bid_mark_max_pct))
|
||||
or s.close_bid_mark_max_pct
|
||||
|
||||
@@ -52,7 +52,7 @@ async def sim_open_group(
|
||||
if pick is None:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="无合格期权:请检查剩余时长(≥设置小时)与杠杆(现价/卖一)",
|
||||
detail="无合格期权:请检查剩余时长、ATM距现价(≤开仓偏差上限)与杠杆(现价/卖一)",
|
||||
)
|
||||
|
||||
force = (body.force_option_side if body else None) or None
|
||||
|
||||
@@ -61,6 +61,7 @@ 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行权价−标的| 上限(点)
|
||||
close_bid_mark_max_pct: float = 30.0 # 平仓:买一相对标记最大偏差%
|
||||
perp_qty_eth: float = 1.0
|
||||
option_qty_eth: float = 2.0
|
||||
|
||||
@@ -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),
|
||||
"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),
|
||||
"option_qty_eth": str(s.option_qty_eth),
|
||||
|
||||
@@ -52,6 +52,9 @@ class StrategyEngine:
|
||||
min_opt_lev = self.ledger.get_setting_float(
|
||||
"min_option_leverage", s.min_option_leverage
|
||||
)
|
||||
max_atm_off = self.ledger.get_setting_float(
|
||||
"max_atm_open_offset", s.max_atm_open_offset
|
||||
)
|
||||
rest_until = row["rest_until_ms"]
|
||||
rest_left = 0
|
||||
if rest_until:
|
||||
@@ -77,6 +80,7 @@ class StrategyEngine:
|
||||
"leverage": leverage,
|
||||
"min_option_hours": min_hours,
|
||||
"min_option_leverage": min_opt_lev,
|
||||
"max_atm_open_offset": max_atm_off,
|
||||
"can_open": allow_open,
|
||||
"last_error": last_error,
|
||||
"position": upl,
|
||||
@@ -306,7 +310,7 @@ class StrategyEngine:
|
||||
pick = await get_session().pick_for_open_async()
|
||||
if pick is None:
|
||||
self._set_state(
|
||||
last_error="无合格期权:需剩余时长与杠杆倍数同时满足"
|
||||
last_error="无合格期权:需剩余时长、ATM开仓偏差与杠杆同时满足"
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
@@ -46,6 +46,20 @@ def pick_atm_strike(strikes: list[float], mark_px: float) -> float | None:
|
||||
return min(strikes, key=lambda s: (abs(s - mark_px), s))
|
||||
|
||||
|
||||
def atm_open_offset(strike: float, mark_px: float) -> float:
|
||||
"""开仓用:ATM 行权价相对标的的绝对点差。"""
|
||||
return abs(float(strike) - float(mark_px))
|
||||
|
||||
|
||||
def atm_allows_open(
|
||||
strike: float, mark_px: float, *, max_offset: float
|
||||
) -> bool:
|
||||
"""|strike − mark| ≤ max_offset 才允许开仓。"""
|
||||
if mark_px <= 0 or max_offset < 0:
|
||||
return False
|
||||
return atm_open_offset(strike, mark_px) <= float(max_offset) + 1e-9
|
||||
|
||||
|
||||
def option_leverage(underlying_px: float, premium_ask: float) -> float | None:
|
||||
if underlying_px <= 0 or premium_ask is None or premium_ask <= 0:
|
||||
return None
|
||||
|
||||
@@ -12,6 +12,8 @@ from ..exchange import get_exchange, set_exchange, build_exchange
|
||||
from ..exchange.protocol import ExchangeMarket
|
||||
from ..exchange.types import MarketSnapshot, OptionPair
|
||||
from .selection import (
|
||||
atm_allows_open,
|
||||
atm_open_offset,
|
||||
hours_until_expiry,
|
||||
list_eligible_expiry_ymds,
|
||||
option_leverage,
|
||||
@@ -34,7 +36,8 @@ def _has_open_position() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _strategy_floats() -> tuple[float, float]:
|
||||
def _strategy_floats() -> tuple[float, float, float]:
|
||||
"""min_hours, min_leverage, max_atm_open_offset"""
|
||||
s = get_settings()
|
||||
try:
|
||||
from ..models.db import get_db
|
||||
@@ -48,9 +51,13 @@ def _strategy_floats() -> tuple[float, float]:
|
||||
db.get_setting("min_option_leverage", str(s.min_option_leverage))
|
||||
or s.min_option_leverage
|
||||
)
|
||||
return hours, lev
|
||||
atm_off = 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
|
||||
except Exception:
|
||||
return s.min_option_hours, s.min_option_leverage
|
||||
return s.min_option_hours, s.min_option_leverage, s.max_atm_open_offset
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -135,7 +142,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:
|
||||
@@ -148,7 +155,7 @@ class StrategySession:
|
||||
from .signal import decide
|
||||
|
||||
s = self.settings
|
||||
min_hours, min_lev = _strategy_floats()
|
||||
min_hours, min_lev, max_atm_off = _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:
|
||||
@@ -164,6 +171,18 @@ class StrategySession:
|
||||
pair = select_option_pair(contracts, mark_px=underlying, expiry_ymd=ymd)
|
||||
if pair is None:
|
||||
continue
|
||||
offset = atm_open_offset(pair.strike, underlying)
|
||||
if not atm_allows_open(
|
||||
pair.strike, underlying, max_offset=max_atm_off
|
||||
):
|
||||
logger.info(
|
||||
"skip expiry=%s strike=%.0f atm_offset=%.1f > max=%.1f",
|
||||
ymd,
|
||||
pair.strike,
|
||||
offset,
|
||||
max_atm_off,
|
||||
)
|
||||
continue
|
||||
call_bids, call_asks, _ = self.ex.fetch_book(pair.call_inst_id, depth=5)
|
||||
put_bids, put_asks, _ = self.ex.fetch_book(pair.put_inst_id, depth=5)
|
||||
call_ask = call_asks[0].px if call_asks else None
|
||||
@@ -257,7 +276,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
|
||||
|
||||
Reference in New Issue
Block a user