From 14088eac1346dc78d21723fe636d73e17665cd07 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Tue, 7 Jul 2026 12:07:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(paper):=20C-S2=20=E5=88=86=E7=AD=96?= =?UTF-8?q?=E7=95=A5=E5=BD=92=E5=9B=A0(/strategies=20=E8=81=9A=E5=90=88?= =?UTF-8?q?=E6=88=90=E4=BA=A4/=E6=8B=92=E5=8D=95/=E8=B4=B9=E7=94=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sanguo_api/routes_paper.py | 8 ++++++++ sanguo_trader/persistence.py | 19 +++++++++++++++++++ tests/api/test_paper_routes.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) 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