diff --git a/sanguo_api/routes_paper.py b/sanguo_api/routes_paper.py index 207de6d..9249f54 100644 --- a/sanguo_api/routes_paper.py +++ b/sanguo_api/routes_paper.py @@ -88,3 +88,11 @@ def get_trades(aid: int): from sanguo_trader.persistence import list_trades return list_trades(_db_path["path"], aid) + + +@router.get("/paper/{aid}/strategies", dependencies=[Depends(verify_token)]) +def get_strategies(aid: int): + """分策略归因:成交/拒单/费用聚合(spec §7)。""" + from sanguo_trader.persistence import list_strategy_summary + + return list_strategy_summary(_db_path["path"], aid) diff --git a/sanguo_trader/persistence.py b/sanguo_trader/persistence.py index 73ca300..85e15da 100644 --- a/sanguo_trader/persistence.py +++ b/sanguo_trader/persistence.py @@ -171,3 +171,22 @@ def list_daily_balance(db_path: str, account_id: int) -> list[dict]: (account_id,), ) return [dict(r) for r in cur.fetchall()] + + +def list_strategy_summary(db_path: str, account_id: int) -> list[dict]: + """按 strategy_id 聚合成交/拒单/费用(分户归因,spec §7)。""" + with sqlite3.connect(db_path) as conn: + conn.row_factory = sqlite3.Row + cur = conn.execute( + """SELECT strategy_id, + COUNT(*) AS total_orders, + SUM(CASE WHEN rejected=0 THEN 1 ELSE 0 END) AS filled, + SUM(CASE WHEN rejected=1 THEN 1 ELSE 0 END) AS rejected, + SUM(commission) AS commission, + SUM(stamp_duty) AS stamp_duty, + SUM(transfer_fee) AS transfer_fee + FROM paper_trades WHERE account_id=? + GROUP BY strategy_id ORDER BY strategy_id""", + (account_id,), + ) + return [dict(r) for r in cur.fetchall()] diff --git a/tests/api/test_paper_routes.py b/tests/api/test_paper_routes.py index 5b4aa33..ef3091c 100644 --- a/tests/api/test_paper_routes.py +++ b/tests/api/test_paper_routes.py @@ -59,3 +59,34 @@ def test_get_paper_404(tmp_path): def test_unauthorized_401(tmp_path): c, _ = _client(tmp_path) assert c.get("/api/v1/paper/1").status_code == 401 + + +def test_strategy_summary_aggregation(tmp_path): + """C-S2 归因:分策略成交/拒单/费用聚合(spec §7)。""" + from sanguo_trader.persistence import init_db, save_account, save_trade + + c, token = _client(tmp_path) + db = os.path.join(str(tmp_path), "p.db") + 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"] + # 造假 2 笔成交 + 1 拒单(s1),1 成交(s2) + save_trade(db, aid, {"strategy_id": "s1", "symbol": "600000", "price": 10, + "volume": 100, "commission": 5, "transfer_fee": 0.02}) + save_trade(db, aid, {"strategy_id": "s1", "symbol": "600000", "price": 11, + "volume": 100, "commission": 5, "stamp_duty": 0.55, + "transfer_fee": 0.02}) + save_trade(db, aid, {"strategy_id": "s1", "symbol": "300750"}, + rejected=True, reject_reason="limit_up_locked") + save_trade(db, aid, {"strategy_id": "s2", "symbol": "000001", "price": 15, + "volume": 100, "commission": 5}) + resp = c.get(f"/api/v1/paper/{aid}/strategies", headers=_auth(token)) + assert resp.status_code == 200 + summary = {s["strategy_id"]: s for s in resp.json()} + assert summary["s1"]["filled"] == 2 + assert summary["s1"]["rejected"] == 1 + assert summary["s2"]["filled"] == 1