Files
eth_hedge_sim/backend/app/api/sim.py
T
2026-07-25 07:52:37 +08:00

99 lines
3.2 KiB
Python

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,
}