Implement P1 local matcher/ledger and P2 strategy engine.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,7 +2,17 @@ from fastapi import APIRouter
|
||||
|
||||
from .auth_routes import router as auth_router
|
||||
from .market import router as market_router
|
||||
from .plan import router as plan_router
|
||||
from .settings import router as settings_router
|
||||
from .sim import router as sim_router
|
||||
from .stats import router as stats_router
|
||||
from .trades import router as trades_router
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(auth_router)
|
||||
router.include_router(market_router)
|
||||
router.include_router(sim_router)
|
||||
router.include_router(plan_router)
|
||||
router.include_router(trades_router)
|
||||
router.include_router(stats_router)
|
||||
router.include_router(settings_router)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from ..strategy import get_engine
|
||||
from .auth import require_user
|
||||
|
||||
router = APIRouter(prefix="/api/plan", tags=["plan"])
|
||||
|
||||
|
||||
@router.get("/state")
|
||||
async def plan_state(_user: Annotated[str, Depends(require_user)]) -> dict:
|
||||
return get_engine().state()
|
||||
|
||||
|
||||
@router.post("/start")
|
||||
async def plan_start(_user: Annotated[str, Depends(require_user)]) -> dict:
|
||||
return await get_engine().start()
|
||||
|
||||
|
||||
@router.post("/pause")
|
||||
async def plan_pause(_user: Annotated[str, Depends(require_user)]) -> dict:
|
||||
return await get_engine().pause()
|
||||
|
||||
|
||||
@router.post("/emergency-close")
|
||||
async def plan_emergency(_user: Annotated[str, Depends(require_user)]) -> dict:
|
||||
return await get_engine().emergency_close()
|
||||
@@ -0,0 +1,75 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ..config import get_settings
|
||||
from ..models.db import get_db
|
||||
from ..sim.ledger import Ledger
|
||||
from .auth import require_user
|
||||
|
||||
router = APIRouter(prefix="/api/settings", tags=["settings"])
|
||||
|
||||
KEYS = ("fee_rate", "exit_move_points", "rest_seconds", "max_rounds", "initial_equity")
|
||||
|
||||
|
||||
class StrategySettingsBody(BaseModel):
|
||||
fee_rate: float | None = Field(default=None, ge=0, le=0.05)
|
||||
exit_move_points: float | None = Field(default=None, ge=1, le=500)
|
||||
rest_seconds: int | None = Field(default=None, ge=0, le=3600)
|
||||
max_rounds: int | None = Field(default=None, ge=1, le=20)
|
||||
initial_equity: float | None = Field(default=None, ge=1000)
|
||||
|
||||
|
||||
@router.get("/strategy")
|
||||
async def get_strategy_settings(_user: Annotated[str, Depends(require_user)]) -> dict:
|
||||
db = get_db()
|
||||
s = get_settings()
|
||||
out = {
|
||||
"fee_rate": float(db.get_setting("fee_rate", str(s.fee_rate)) or s.fee_rate),
|
||||
"exit_move_points": float(
|
||||
db.get_setting("exit_move_points", str(s.exit_move_points)) or s.exit_move_points
|
||||
),
|
||||
"rest_seconds": int(
|
||||
float(db.get_setting("rest_seconds", str(s.rest_seconds)) or s.rest_seconds)
|
||||
),
|
||||
"max_rounds": int(
|
||||
float(db.get_setting("max_rounds", str(s.max_rounds)) or s.max_rounds)
|
||||
),
|
||||
"initial_equity": float(
|
||||
db.get_setting("initial_equity", str(s.initial_equity)) or s.initial_equity
|
||||
),
|
||||
"ledger": Ledger(db).snapshot(),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
@router.put("/strategy")
|
||||
async def put_strategy_settings(
|
||||
body: StrategySettingsBody,
|
||||
_user: Annotated[str, Depends(require_user)],
|
||||
) -> dict:
|
||||
db = get_db()
|
||||
data = body.model_dump(exclude_none=True)
|
||||
for k, v in data.items():
|
||||
if k in KEYS:
|
||||
db.set_setting(k, str(v))
|
||||
s = get_settings()
|
||||
return {
|
||||
"fee_rate": float(db.get_setting("fee_rate", str(s.fee_rate)) or s.fee_rate),
|
||||
"exit_move_points": float(
|
||||
db.get_setting("exit_move_points", str(s.exit_move_points)) or s.exit_move_points
|
||||
),
|
||||
"rest_seconds": int(
|
||||
float(db.get_setting("rest_seconds", str(s.rest_seconds)) or s.rest_seconds)
|
||||
),
|
||||
"max_rounds": int(
|
||||
float(db.get_setting("max_rounds", str(s.max_rounds)) or s.max_rounds)
|
||||
),
|
||||
"initial_equity": float(
|
||||
db.get_setting("initial_equity", str(s.initial_equity)) or s.initial_equity
|
||||
),
|
||||
"ledger": Ledger(db).snapshot(),
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
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()
|
||||
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,
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from ..models.db import get_db
|
||||
from .auth import require_user
|
||||
|
||||
router = APIRouter(prefix="/api/stats", tags=["stats"])
|
||||
|
||||
|
||||
@router.get("/summary")
|
||||
async def stats_summary(_user: Annotated[str, Depends(require_user)]) -> dict:
|
||||
db = get_db()
|
||||
rows = db.fetchall("SELECT * FROM groups WHERE status='closed'")
|
||||
n = len(rows)
|
||||
wins = sum(1 for r in rows if float(r["realized_pnl"] or 0) > 0)
|
||||
total_pnl = sum(float(r["realized_pnl"] or 0) for r in rows)
|
||||
total_fees = sum(float(r["fees"] or 0) for r in rows)
|
||||
total_slip = sum(float(r["slip_cost"] or 0) for r in rows)
|
||||
reasons: dict[str, int] = {}
|
||||
for r in rows:
|
||||
k = str(r["close_reason"] or "unknown")
|
||||
reasons[k] = reasons.get(k, 0) + 1
|
||||
curve = [
|
||||
{
|
||||
"group_id": r["group_id"],
|
||||
"realized_pnl": float(r["realized_pnl"] or 0),
|
||||
"close_at_ms": r["close_at_ms"],
|
||||
}
|
||||
for r in sorted(rows, key=lambda x: int(x["close_at_ms"] or 0))
|
||||
]
|
||||
return {
|
||||
"groups": n,
|
||||
"wins": wins,
|
||||
"win_rate": (wins / n) if n else 0.0,
|
||||
"total_pnl": total_pnl,
|
||||
"total_fees": total_fees,
|
||||
"total_slip": total_slip,
|
||||
"close_reasons": reasons,
|
||||
"equity_curve": curve,
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated, Any
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
|
||||
from ..models.db import get_db
|
||||
from .auth import require_user
|
||||
|
||||
router = APIRouter(prefix="/api/trades", tags=["trades"])
|
||||
|
||||
|
||||
def _row(r: Any) -> dict:
|
||||
return dict(r)
|
||||
|
||||
|
||||
@router.get("/groups")
|
||||
async def list_groups(_user: Annotated[str, Depends(require_user)]) -> dict:
|
||||
rows = get_db().fetchall(
|
||||
"SELECT * FROM groups ORDER BY open_at_ms DESC LIMIT 200"
|
||||
)
|
||||
return {"groups": [_row(x) for x in rows]}
|
||||
|
||||
|
||||
@router.get("/groups/{group_id}")
|
||||
async def group_detail(group_id: str, _user: Annotated[str, Depends(require_user)]) -> dict:
|
||||
db = get_db()
|
||||
g = db.fetchone("SELECT * FROM groups WHERE group_id=?", (group_id,))
|
||||
if g is None:
|
||||
raise HTTPException(status_code=404, detail="group not found")
|
||||
fills = db.fetchall(
|
||||
"SELECT * FROM fills WHERE group_id=? ORDER BY id ASC", (group_id,)
|
||||
)
|
||||
return {"group": _row(g), "fills": [_row(x) for x in fills]}
|
||||
Reference in New Issue
Block a user