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 ..env_store import live_ready from ..live import get_executor from ..market import get_gateway from ..models.db import get_db from ..sim.ledger import Ledger 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 = get_executor() 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: ok, reason = live_ready() if not get_settings().is_sim and not ok: raise HTTPException(status_code=400, detail=reason) from ..strategy import get_engine st = get_engine().state() if st.get("running"): raise HTTPException( status_code=409, detail="策略自动运行中,禁止手动开仓;请先暂停", ) if not Ledger().get_setting_bool("show_manual_trade_buttons", False): raise HTTPException( status_code=403, detail="未开启「显示手动开仓」;请在策略设置中开启后再用", ) ex = get_executor() if ex.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="无合格期权:请检查剩余时长、ATM开仓偏差(若已开启)与杠杆(现价/卖一)", ) 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) engine = get_engine() async with engine._lock: if ex.has_open_position(): raise HTTPException(status_code=409, detail="有未平仓,禁止开下一组") r = ex.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) try: from ..notify import wecom wecom.notify_open( group_id=gid, detail=r.detail, extra={ "bias": bias, "option_side": option_side, "option_inst_id": option_inst, "strike": pick.pair.strike, "expiry_ymd": pick.pair.expiry_ymd, }, ) except Exception: pass 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: if not Ledger().get_setting_bool("show_manual_trade_buttons", False): raise HTTPException( status_code=403, detail="未开启「显示手动开仓」;请在策略设置中开启后再用", ) from ..strategy import get_engine engine = get_engine() # 与策略引擎共用锁,避免与自动平仓/开仓竞态 async with engine._lock: r = get_executor().close_group(reason="manual") if not r.ok and not r.liquidity_wait: raise HTTPException(status_code=400, detail=r.detail) if r.ok: # 与自动/紧急全平一致:成功全平后进入组间休息 engine.enter_rest_after_close() try: from ..notify import wecom wecom.notify_close( reason="manual", detail=r.detail, data=r.data or {}, ) except Exception: pass return { "ok": r.ok, "liquidity_wait": r.liquidity_wait, "detail": r.detail, "data": r.data, }