Realign ATM to current mark before open; skip while in position.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-25 07:52:37 +08:00
parent 1155cf082d
commit 7db4c9c8a3
4 changed files with 70 additions and 6 deletions
+5
View File
@@ -38,6 +38,11 @@ async def sim_open_group(
body: ManualOpenBody | None = None,
) -> dict:
gw = get_gateway()
# 开仓前按现价强制重选 ATM,避免沿用启动时的旧行权价
try:
await gw.ensure_atm_async(force=True)
except Exception as e:
raise HTTPException(status_code=503, detail=f"ATM 对齐失败: {e}") from e
snap = gw.snapshot()
if not snap.pair or not snap.call or not snap.put:
raise HTTPException(status_code=503, detail="行情未就绪")
+55 -5
View File
@@ -15,6 +15,19 @@ from .types import MarketSnapshot, OptionPair
logger = logging.getLogger(__name__)
# 现价偏离当前行权价超过该点数则重选 ATM(ETH 期权常见步进 5)
_ATM_DRIFT_POINTS = 5.0
def _has_open_position() -> bool:
try:
from ..models.db import get_db
row = get_db().fetchone("SELECT status FROM positions WHERE id=1")
return bool(row and row["status"] == "open")
except Exception:
return False
class MarketGateway:
def __init__(self, settings: Settings | None = None) -> None:
@@ -109,6 +122,41 @@ class MarketGateway:
)
return pair
def _mark_for_atm(self) -> float | None:
snap = self.snapshot()
if snap.perp and snap.perp.mark_px:
return float(snap.perp.mark_px)
if snap.index_px:
return float(snap.index_px)
if snap.perp and snap.perp.bid and snap.perp.ask:
return (float(snap.perp.bid) + float(snap.perp.ask)) / 2
return None
def atm_needs_realign(self, mark_px: float | None = None) -> bool:
"""到期日变了,或现价已偏离当前行权价超过阈值。"""
if self._pair is None:
return True
want = next_session_expiry_ymd()
if self._pair.expiry_ymd != want:
return True
mark = mark_px if mark_px is not None else self._mark_for_atm()
if mark is None or mark <= 0:
return False
return abs(float(self._pair.strike) - float(mark)) >= _ATM_DRIFT_POINTS
async def ensure_atm_async(self, *, force: bool = False) -> OptionPair | None:
"""空仓时按现价对齐 ATM。有持仓时不切换,避免盯市合约被换掉。"""
if _has_open_position():
return self._pair
if force or self.atm_needs_realign():
logger.info(
"ATM realign force=%s old_strike=%s",
force,
self._pair.strike if self._pair else None,
)
return await self.realign_async()
return self._pair
def snapshot(self) -> MarketSnapshot:
return self.cache.snapshot(self.settings.perp_inst_id)
@@ -116,7 +164,7 @@ class MarketGateway:
return self.snapshot().to_dict()
async def _refresh_loop(self) -> None:
"""周期性刷新指数价;跨日到期切换时重对齐。"""
"""周期性刷新指数价;空仓时按到期/ATM 偏离重对齐。"""
while True:
await asyncio.sleep(30)
try:
@@ -124,10 +172,12 @@ class MarketGateway:
self.rest.fetch_index_ticker, self.settings.index_inst_id
)
self.cache.set_index_px(idx)
want = next_session_expiry_ymd()
if self._pair and self._pair.expiry_ymd != want:
logger.info("expiry rollover %s -> %s", self._pair.expiry_ymd, want)
await self.realign_async()
mark = await asyncio.to_thread(
self.rest.fetch_mark_price, self.settings.perp_inst_id
)
if mark:
self.cache.set_mark_px(self.settings.perp_inst_id, mark)
await self.ensure_atm_async(force=False)
except asyncio.CancelledError:
raise
except Exception as e:
+5 -1
View File
@@ -412,7 +412,11 @@ class Matcher:
perp_upl = (perp_entry - float(mark)) * perp_qty
option_side = str(pos["option_side"])
oq = snap.call if option_side == "call" else snap.put
# 优先用持仓合约盘口,避免 ATM 切换后盯错合约
opt_inst = str(pos.get("option_inst_id") or "")
oq = gw.cache.get(opt_inst) if opt_inst else None
if oq is None:
oq = snap.call if option_side == "call" else snap.put
opt_mark = None
if oq:
opt_mark = oq.bid or oq.mark_px
+5
View File
@@ -135,6 +135,11 @@ class StrategyEngine:
await asyncio.sleep(1)
continue
async with self._lock:
# 空仓且 ATM 偏离现价时先重选,再跑开仓逻辑
try:
await get_gateway().ensure_atm_async(force=False)
except Exception as e:
logger.warning("ATM ensure before tick failed: %s", e)
await asyncio.to_thread(self._tick)
except asyncio.CancelledError:
raise