feat(api): /paper/* 路由(create建account+equity/trades查询,JWT)

This commit is contained in:
2026-07-07 12:06:26 +08:00
parent 42877213ae
commit 041dca59e2
3 changed files with 154 additions and 0 deletions
+3
View File
@@ -3,6 +3,7 @@ FastAPI application factory for Sanguo Quant API
"""
from fastapi import FastAPI
from .routes import router, set_orchestrator, set_auth_config
from .routes_paper import router as paper_router, set_db_path
from .auth import set_jwt_config
from .ws import manager
from sanguo_orchestrator.runner import Orchestrator
@@ -33,5 +34,7 @@ def create_app(db_path: str, file_dir=None, auth_config=None, max_workers: int =
# Include routes
app.include_router(router, prefix="/api/v1")
app.include_router(paper_router, prefix="/api/v1")
set_db_path(db_path)
return app
+90
View File
@@ -0,0 +1,90 @@
"""模拟盘 API 路由(spec §10)。
create 建 paper_account(持久化配置);GET 查询净值/成交/状态。
回放执行(engine.run)由 orchestrator 异步触发或容器内同步跑,端到端冒烟在容器
(本机无 NAS parquet + vnpy 完整依赖),本模块只做 account 管理 + 查询。
"""
import sqlite3
from fastapi import APIRouter, Depends, Header, HTTPException
from pydantic import BaseModel
from .auth import verify_token as verify_token_impl
router = APIRouter()
_db_path = {"path": None}
def set_db_path(p):
_db_path["path"] = p
if p:
from sanguo_trader.persistence import init_db
init_db(p) # app 启动建表(幂等),保证 GET 查询不报 no such table
async def verify_token(authorization: str | None = Header(None)):
if authorization is None or not authorization.startswith("Bearer "):
raise HTTPException(401, "Missing/invalid authorization")
return verify_token_impl(authorization.split(" ", 1)[1])
class StrategyCfg(BaseModel):
name: str
params: dict = {}
match_session: str = "next_open"
symbol: str
listing_days: int = 0
class PaperCreateRequest(BaseModel):
name: str = "paper"
mode: str = "replay"
interval: str = "d"
symbols: list[str]
strategies: list[StrategyCfg]
initial_capital: float = 1_000_000
rate: float = 0.0003
slippage: float = 0.0
pricetick: float = 0.01
stamp_duty_rate: float = 0.0005
transfer_fee_rate: float = 0.00001
min_commission: float = 5.0
start: str
end: str
@router.post("/paper/create", dependencies=[Depends(verify_token)])
def create_paper(req: PaperCreateRequest):
from sanguo_trader.persistence import init_db, save_account
db = _db_path["path"] or ":memory:"
init_db(db)
aid = save_account(db, req.model_dump())
return {"account_id": aid, "status": "created"}
@router.get("/paper/{aid}", dependencies=[Depends(verify_token)])
def get_paper(aid: int):
db = _db_path["path"]
with sqlite3.connect(db) as conn:
conn.row_factory = sqlite3.Row
row = conn.execute(
"SELECT * FROM paper_accounts WHERE id=?", (aid,)
).fetchone()
if not row:
raise HTTPException(404, "account not found")
return dict(row)
@router.get("/paper/{aid}/equity", dependencies=[Depends(verify_token)])
def get_equity(aid: int):
from sanguo_trader.persistence import list_daily_balance
return list_daily_balance(_db_path["path"], aid)
@router.get("/paper/{aid}/trades", dependencies=[Depends(verify_token)])
def get_trades(aid: int):
from sanguo_trader.persistence import list_trades
return list_trades(_db_path["path"], aid)
+61
View File
@@ -0,0 +1,61 @@
"""模拟盘 API 路由测试(spec §10)。"""
import os
from fastapi.testclient import TestClient
from sanguo_api.app import create_app
from sanguo_api.auth import create_token, set_jwt_config
from sanguo_api.routes_paper import set_db_path
def _client(tmp_path):
set_jwt_config(secret="t", expire_minutes=60)
db = os.path.join(str(tmp_path), "p.db")
app = create_app(db_path=db)
set_db_path(db)
return TestClient(app), create_token("admin")
def _auth(token):
return {"Authorization": f"Bearer {token}"}
def test_create_paper(tmp_path):
c, token = _client(tmp_path)
resp = c.post(
"/api/v1/paper/create",
json={
"symbols": ["600000"],
"strategies": [{"name": "DoubleMa", "symbol": "600000",
"match_session": "next_open"}],
"start": "2024-01-01", "end": "2024-06-30",
"initial_capital": 1_000_000,
},
headers=_auth(token),
)
assert resp.status_code == 200
assert "account_id" in resp.json()
def test_get_paper_and_empty_trades(tmp_path):
c, token = _client(tmp_path)
aid = c.post(
"/api/v1/paper/create",
json={"symbols": ["600000"],
"strategies": [{"name": "S", "symbol": "600000"}],
"start": "2024-01-01", "end": "2024-06-30"},
headers=_auth(token),
).json()["account_id"]
assert c.get(f"/api/v1/paper/{aid}", headers=_auth(token)).status_code == 200
assert c.get(f"/api/v1/paper/{aid}/trades", headers=_auth(token)).json() == []
assert c.get(f"/api/v1/paper/{aid}/equity", headers=_auth(token)).json() == []
def test_get_paper_404(tmp_path):
c, token = _client(tmp_path)
assert c.get("/api/v1/paper/999", headers=_auth(token)).status_code == 404
def test_unauthorized_401(tmp_path):
c, _ = _client(tmp_path)
assert c.get("/api/v1/paper/1").status_code == 401