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,
|
body: ManualOpenBody | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
gw = get_gateway()
|
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()
|
snap = gw.snapshot()
|
||||||
if not snap.pair or not snap.call or not snap.put:
|
if not snap.pair or not snap.call or not snap.put:
|
||||||
raise HTTPException(status_code=503, detail="行情未就绪")
|
raise HTTPException(status_code=503, detail="行情未就绪")
|
||||||
|
|||||||
@@ -15,6 +15,19 @@ from .types import MarketSnapshot, OptionPair
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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:
|
class MarketGateway:
|
||||||
def __init__(self, settings: Settings | None = None) -> None:
|
def __init__(self, settings: Settings | None = None) -> None:
|
||||||
@@ -109,6 +122,41 @@ class MarketGateway:
|
|||||||
)
|
)
|
||||||
return pair
|
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:
|
def snapshot(self) -> MarketSnapshot:
|
||||||
return self.cache.snapshot(self.settings.perp_inst_id)
|
return self.cache.snapshot(self.settings.perp_inst_id)
|
||||||
|
|
||||||
@@ -116,7 +164,7 @@ class MarketGateway:
|
|||||||
return self.snapshot().to_dict()
|
return self.snapshot().to_dict()
|
||||||
|
|
||||||
async def _refresh_loop(self) -> None:
|
async def _refresh_loop(self) -> None:
|
||||||
"""周期性刷新指数价;跨日到期切换时重对齐。"""
|
"""周期性刷新指数价;空仓时按到期/ATM 偏离重对齐。"""
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(30)
|
await asyncio.sleep(30)
|
||||||
try:
|
try:
|
||||||
@@ -124,10 +172,12 @@ class MarketGateway:
|
|||||||
self.rest.fetch_index_ticker, self.settings.index_inst_id
|
self.rest.fetch_index_ticker, self.settings.index_inst_id
|
||||||
)
|
)
|
||||||
self.cache.set_index_px(idx)
|
self.cache.set_index_px(idx)
|
||||||
want = next_session_expiry_ymd()
|
mark = await asyncio.to_thread(
|
||||||
if self._pair and self._pair.expiry_ymd != want:
|
self.rest.fetch_mark_price, self.settings.perp_inst_id
|
||||||
logger.info("expiry rollover %s -> %s", self._pair.expiry_ymd, want)
|
)
|
||||||
await self.realign_async()
|
if mark:
|
||||||
|
self.cache.set_mark_px(self.settings.perp_inst_id, mark)
|
||||||
|
await self.ensure_atm_async(force=False)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -412,7 +412,11 @@ class Matcher:
|
|||||||
perp_upl = (perp_entry - float(mark)) * perp_qty
|
perp_upl = (perp_entry - float(mark)) * perp_qty
|
||||||
|
|
||||||
option_side = str(pos["option_side"])
|
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
|
opt_mark = None
|
||||||
if oq:
|
if oq:
|
||||||
opt_mark = oq.bid or oq.mark_px
|
opt_mark = oq.bid or oq.mark_px
|
||||||
|
|||||||
@@ -135,6 +135,11 @@ class StrategyEngine:
|
|||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
continue
|
continue
|
||||||
async with self._lock:
|
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)
|
await asyncio.to_thread(self._tick)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
raise
|
raise
|
||||||
|
|||||||
Reference in New Issue
Block a user