feat(paper): 组合策略接入模拟盘实走(E1+E2): paper_accounts加strategy_type列(含迁移); portfolio_paper每晚20:30全量重放→当日成交/末日持仓/净值落paper表(幂等,回测引擎为单一真相源); run_live_step按类型分流; create支持portfolio(仅live); 前端新建模拟盘策略类型选择+组合字段(策略/池/上限/基准) [vps]
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
"""组合策略模拟盘实走 step 单测(E1):mock run_backtest_json 验证落库/幂等/分流。"""
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from sanguo_trader import portfolio_paper
|
||||
from sanguo_trader.persistence import (
|
||||
init_db, load_last_balance, load_positions, list_trades, save_account,
|
||||
update_account_status,
|
||||
)
|
||||
|
||||
|
||||
def _mk_account(db, strategy_type="portfolio", status="running"):
|
||||
aid = save_account(db, {
|
||||
"name": "pf1", "mode": "live", "strategy_type": strategy_type,
|
||||
"symbols": ["hs300_subset"],
|
||||
"strategies": [{"name": "all_weather",
|
||||
"params": {"max_pool": 30, "benchmark": "000300.XSHG"}}],
|
||||
"initial_capital": 1_000_000, "start": "2026-01-01", "end": "2026-12-31",
|
||||
})
|
||||
update_account_status(db, aid, status)
|
||||
return aid
|
||||
|
||||
|
||||
def _fake_result(date="2026-08-13", equity=1_050_000.0):
|
||||
return {
|
||||
"strategy": "all_weather",
|
||||
"equity_curve": [{"date": "2026-08-12", "equity": 1_040_000.0},
|
||||
{"date": date, "equity": equity}],
|
||||
"stocks_selected": [
|
||||
{"code": "600000", "amount": 1000, "avg_cost": 10.0, "price": 10.5, "value": 10500.0},
|
||||
],
|
||||
"trades": [
|
||||
{"date": f"{date} 14:50:00", "code": "600000", "side": "buy",
|
||||
"filled_amount": 1000, "filled_price": 10.5, "commission": 5.0},
|
||||
{"date": "2026-08-12 14:50:00", "code": "000001", "side": "buy",
|
||||
"filled_amount": 500, "filled_price": 11.0},
|
||||
],
|
||||
"metrics": {},
|
||||
}
|
||||
|
||||
|
||||
def test_step_writes_balance_positions_today_trades(tmp_path, monkeypatch):
|
||||
db = str(tmp_path / "paper.db")
|
||||
init_db(db)
|
||||
aid = _mk_account(db)
|
||||
monkeypatch.setattr("sanguo_portfolio.runner_backtest.run_backtest_json",
|
||||
lambda params: _fake_result())
|
||||
out = portfolio_paper.run_portfolio_live_step(db, aid, today="2026-08-13")
|
||||
assert out["date"] == "2026-08-13"
|
||||
assert out["trades"] == 1 # 只落当日成交
|
||||
|
||||
bal = load_last_balance(db, aid)
|
||||
assert bal["date"] == "2026-08-13"
|
||||
assert bal["total_equity"] == pytest.approx(1_050_000.0)
|
||||
assert bal["cash"] == pytest.approx(1_050_000.0 - 10500.0)
|
||||
|
||||
pos = load_positions(db, aid, "account")
|
||||
assert pos["600000"]["volume"] == 1000
|
||||
|
||||
trades = list_trades(db, aid)
|
||||
assert len(trades) == 1
|
||||
assert trades[0]["symbol"] == "600000"
|
||||
assert trades[0]["strategy_id"] == "all_weather"
|
||||
|
||||
|
||||
def test_step_idempotent_same_day(tmp_path, monkeypatch):
|
||||
db = str(tmp_path / "paper.db")
|
||||
init_db(db)
|
||||
aid = _mk_account(db)
|
||||
calls = []
|
||||
monkeypatch.setattr("sanguo_portfolio.runner_backtest.run_backtest_json",
|
||||
lambda p: calls.append(1) or _fake_result())
|
||||
r1 = portfolio_paper.run_portfolio_live_step(db, aid, today="2026-08-13")
|
||||
r2 = portfolio_paper.run_portfolio_live_step(db, aid, today="2026-08-13")
|
||||
assert r1.get("date") == "2026-08-13"
|
||||
assert r2.get("skipped") == "already stepped today"
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
def test_step_skips_no_new_trading_day(tmp_path, monkeypatch):
|
||||
db = str(tmp_path / "paper.db")
|
||||
init_db(db)
|
||||
aid = _mk_account(db)
|
||||
# 数据滞后:回放末日仍 08-12(账户已结算到 08-13 前先结算到 12)
|
||||
monkeypatch.setattr("sanguo_portfolio.runner_backtest.run_backtest_json",
|
||||
lambda p: _fake_result(date="2026-08-11", equity=1_030_000.0))
|
||||
portfolio_paper.run_portfolio_live_step(db, aid, today="2026-08-12")
|
||||
monkeypatch.setattr("sanguo_portfolio.runner_backtest.run_backtest_json",
|
||||
lambda p: _fake_result(date="2026-08-11", equity=1_030_000.0))
|
||||
r = portfolio_paper.run_portfolio_live_step(db, aid, today="2026-08-13")
|
||||
assert r.get("skipped") == "no new trading day"
|
||||
|
||||
|
||||
def test_step_stopped_account_skipped(tmp_path, monkeypatch):
|
||||
db = str(tmp_path / "paper.db")
|
||||
init_db(db)
|
||||
aid = _mk_account(db, status="stopped")
|
||||
monkeypatch.setattr("sanguo_portfolio.runner_backtest.run_backtest_json",
|
||||
lambda p: (_ for _ in ()).throw(AssertionError("不应跑引擎")))
|
||||
r = portfolio_paper.run_portfolio_live_step(db, aid, today="2026-08-13")
|
||||
assert r.get("skipped") == "status=stopped"
|
||||
|
||||
|
||||
def test_step_rejects_cta_account(tmp_path):
|
||||
db = str(tmp_path / "paper.db")
|
||||
init_db(db)
|
||||
aid = _mk_account(db, strategy_type="cta")
|
||||
with pytest.raises(ValueError):
|
||||
portfolio_paper.run_portfolio_live_step(db, aid)
|
||||
|
||||
|
||||
def test_is_portfolio_branch(tmp_path):
|
||||
from sanguo_trader.live_orchestrator import _is_portfolio_account
|
||||
db = str(tmp_path / "paper.db")
|
||||
init_db(db)
|
||||
p_aid = _mk_account(db, strategy_type="portfolio")
|
||||
c_aid = _mk_account(db, strategy_type="cta")
|
||||
assert _is_portfolio_account(db, p_aid) is True
|
||||
assert _is_portfolio_account(db, c_aid) is False
|
||||
Reference in New Issue
Block a user