Files
eth_hedge_sim/backend/app/api/sim.py
T
2026-08-03 07:35:44 +08:00

218 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
ledger = Ledger()
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开仓偏差(若已开启)与杠杆(现价/卖一)",
)
one_expiry = ledger.get_setting_bool("one_expiry_per_day", s.one_expiry_per_day)
if one_expiry:
from ..strategy.clock import (
expiry_blocked_by_one_per_day,
used_expiry_ymds,
)
used = used_expiry_ymds(get_db())
if expiry_blocked_by_one_per_day(pick.pair.expiry_ymd, used, enabled=True):
raise HTTPException(
status_code=409,
detail=(
f"每个到期只开一次:已用过 {pick.pair.expiry_ymd}"
"请等下一到期日"
),
)
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
)
# 强制方向时用该腿卖一估权利金;否则用选向结果
sizing_ask = float(
option_ask
if force in ("call", "put")
else pick.option_ask
)
wkey = window_key()
db = get_db()
from ..strategy.open_pipeline import size_and_gate
prep = size_and_gate(
index_px=float(pick.underlying_px),
option_ask=sizing_ask,
db=db,
)
if not prep.ok:
raise HTTPException(status_code=409, detail=prep.detail)
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,
"perp_side": perp_side,
"option_inst_id": option_inst,
"strike": pick.pair.strike,
"expiry_ymd": pick.pair.expiry_ymd,
**(r.data or {}),
},
)
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,
}