from __future__ import annotations from typing import Annotated from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from ..market import get_gateway from ..models.db import get_db from ..sim.matcher import Matcher from ..strategy.clock import window_key from ..strategy.group import next_group_id from ..strategy.signal import decide from .auth import require_user router = APIRouter(prefix="/api/sim", tags=["sim"]) class ManualOpenBody(BaseModel): """可选强制方向;默认按卖一比价自动选。""" force_option_side: str | None = Field(default=None, description="call|put") @router.get("/ledger") async def sim_ledger(_user: Annotated[str, Depends(require_user)]) -> dict: return Ledger().snapshot() @router.get("/position") async def sim_position(_user: Annotated[str, Depends(require_user)]) -> dict: m = Matcher() return {"position": m.current_position(), "unrealized": m.unrealized()} @router.post("/open-group") async def sim_open_group( _user: Annotated[str, Depends(require_user)], 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="行情未就绪") force = (body.force_option_side if body else None) or None if force in ("call", "put"): option_side = force perp_side = "short" if force == "call" else "long" bias = "manual_" + force else: sig = decide(snap.call.ask, snap.put.ask) if sig is None: raise HTTPException(status_code=409, detail="Call/Put 卖一相等,跳过") option_side = sig.option_side perp_side = sig.perp_side bias = sig.bias option_inst = ( snap.pair.call_inst_id if option_side == "call" else snap.pair.put_inst_id ) entry_idx = snap.index_px or (snap.perp.mark_px if snap.perp else None) if entry_idx is None: raise HTTPException(status_code=503, detail="无指数/标记价") wkey = window_key() db = get_db() count = len(db.fetchall("SELECT group_id FROM groups WHERE group_id LIKE ?", (f"G-{wkey}-%",))) gid = next_group_id(count) r = Matcher().open_group( group_id=gid, bias=bias, option_side=option_side, perp_side=perp_side, option_inst_id=option_inst, entry_index_px=float(entry_idx), strike=snap.pair.strike, expiry_ymd=snap.pair.expiry_ymd, ) if not r.ok: raise HTTPException(status_code=400, detail=r.detail) return {"ok": True, **(r.data or {}), "detail": r.detail} @router.post("/close-group") async def sim_close_group(_user: Annotated[str, Depends(require_user)]) -> dict: r = Matcher().close_group(reason="manual") if not r.ok and not r.liquidity_wait: raise HTTPException(status_code=400, detail=r.detail) return { "ok": r.ok, "liquidity_wait": r.liquidity_wait, "detail": r.detail, "data": r.data, }