from __future__ import annotations from typing import Annotated from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from ..config import get_settings from ..market import get_gateway from ..models.db import get_db from ..sim.ledger import Ledger from ..sim.matcher import Matcher from ..strategy.clock import can_open_new, window_key from ..strategy.group import next_group_id 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: if Matcher().has_open_position(): raise HTTPException(status_code=409, detail="有未平仓,禁止开下一组") s = get_settings() skip_weekends = Ledger().get_setting_bool("skip_weekends", s.skip_weekends) if not can_open_new(skip_weekends=skip_weekends): raise HTTPException( status_code=409, detail="周六/周日跳过开仓(上海时区)", ) gw = get_gateway() pick = await gw.pick_for_open_async() if pick is None: raise HTTPException( status_code=409, 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 option_ask = pick.call_ask if force == "call" else pick.put_ask from ..strategy.selection import option_leverage min_lev = Ledger().get_setting_float("min_option_leverage", s.min_option_leverage) lev = option_leverage(pick.underlying_px, option_ask) if lev is None or lev < min_lev: raise HTTPException( status_code=409, detail=f"强制方向杠杆不足: {lev or 0:.1f} < {min_lev:.0f}", ) else: option_side = pick.option_side perp_side = pick.perp_side bias = pick.bias option_inst = ( pick.pair.call_inst_id if option_side == "call" else pick.pair.put_inst_id ) 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(pick.underlying_px), strike=pick.pair.strike, expiry_ymd=pick.pair.expiry_ymd, ) if not r.ok: raise HTTPException(status_code=400, detail=r.detail) return { "ok": True, **(r.data or {}), "detail": r.detail, "option_leverage": pick.option_leverage, "hours_left": pick.hours_left, "expiry_ymd": pick.pair.expiry_ymd, "strike": pick.pair.strike, } @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, }