Realign ATM to current mark before open; skip while in position.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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="行情未就绪")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user