mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 15:44:18 +08:00
feat: 组合回测分析体系对齐单标的 — 组合级WF/一条龙/完整25项绩效/AI解读
组合回测(一策略×多标的)此前只能看 4 个数字,本轮把单标的的整条 分析链路在组合端补齐(WebUI/REST 双端): - portfolio_engine:合并净值+汇总成交喂 PerformanceAnalyzer,输出 完整 25 项指标(SQN/最大连胜连亏/Ulcer/VaR/CVaR 等)+ 组合层 trades(symbol 列);修复假年化与回撤口径(负值+固定分母 → 逐点峰值,与单标的/多策略一致) - walkforward:新增 PortfolioWalkForwardEngine,按标的日期并集切窗、 每窗独立开仓、合成组合窗内净值,复用 WalkForwardResult 结构 - benchmark:新增 evaluate_portfolio 一条龙(组合回测+组合WF+ 跨标的多数口径适配性体检+综合评分+组合评级+等权买入持有基准对比), 报告结构与单标的 evaluate_strategy 同构 - performance:FIFO 持仓天数配对支持 symbol 分组 - Web:新增 POST /backtest/portfolio/wf/run/async 与 /backtest/portfolio/evaluate/run/async;组合回测响应附带 grade(组合净值口径)与 score;新增 _normalize_bars_dt 修复 按标的取数路径的字符串日期/遗留 date 列崩溃(E2E 揭露) - 前端:组合页新增附加分析勾选区与组合绩效指标/WF/一条龙/成交明细 区块;buildPortfolioAiPrompt 组合版 Prompt;抽通用 AiInterpretModal(回测页迁移共用,行为不变);TradeTable 支持 showSymbol;EvaluatePanel 支持 gradeOverride - 测试:后端 +17 例(pytest 1603 绿)、aiPrompt 组合版 2 例、 Playwright 组合页 E2E 2 例(9/9 绿)
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
@@ -194,3 +195,59 @@ def test_evaluate_strategy_auto_fees_for_etf():
|
||||
report = evaluate_strategy(_BuyFirstBar, _df(300), symbol="SH:510300", auto_fees=True)
|
||||
assert report["config"]["symbol"] == "SH:510300"
|
||||
assert report["config"]["auto_fees"] is True
|
||||
|
||||
|
||||
# ── evaluate_portfolio(v1.31 组合级一条龙)───────────────────────────────────
|
||||
def _stocks_for_portfolio() -> list[Any]:
|
||||
from easy_tdx.backtest.portfolio_engine import StockData
|
||||
|
||||
return [
|
||||
StockData("000001", "SZ", _df(400, drift=0.002)),
|
||||
StockData("600000", "SH", _df(400, drift=0.003)),
|
||||
]
|
||||
|
||||
|
||||
def test_evaluate_portfolio_full_report_structure():
|
||||
"""组合一条龙报告与单标的 evaluate_strategy 同构(前端面板可复用)。"""
|
||||
from easy_tdx.backtest.benchmark import evaluate_portfolio
|
||||
|
||||
report = evaluate_portfolio(_CycleTrader(), _stocks_for_portfolio(), total_cash=500_000)
|
||||
for key in ("performance", "score", "grade", "walkforward", "fitness", "benchmark", "config"):
|
||||
assert key in report
|
||||
# 组合绩效:完整指标 + 组合字段
|
||||
assert "sqn" in report["performance"]
|
||||
assert "max_consecutive_losses" in report["performance"]
|
||||
assert report["performance"]["total_stocks"] == 2
|
||||
# 评分/评级
|
||||
assert 0 <= report["score"]["total"] <= 100
|
||||
assert report["score"]["wf_provided"] is True
|
||||
assert report["grade"]["grade"] in ("S", "A", "B", "C", "D")
|
||||
assert report["grade"]["scenario"] == "portfolio"
|
||||
# 组合 WF
|
||||
assert len(report["walkforward"]["windows"]) > 0
|
||||
# 适配性(跨标的聚合)
|
||||
assert report["fitness"]["total_checks"] == 8
|
||||
assert "只标的通过" in report["fitness"]["checks"][0]["detail"]
|
||||
# 基准
|
||||
assert "buy_hold" in report["benchmark"]
|
||||
assert "excess_return" in report["benchmark"]
|
||||
# config 记录标的清单
|
||||
assert report["config"]["stocks"] == ["SZ000001", "SH600000"]
|
||||
|
||||
|
||||
def test_evaluate_portfolio_buy_hold_excess_near_zero():
|
||||
"""首根买入持有策略 ≈ 等权买入持有基准,excess_return 接近 0(扣费差异)。"""
|
||||
from easy_tdx.backtest.benchmark import evaluate_portfolio
|
||||
|
||||
report = evaluate_portfolio(_BuyFirstBar(), _stocks_for_portfolio(), total_cash=500_000)
|
||||
assert abs(report["benchmark"]["excess_return"]) < 0.05
|
||||
|
||||
|
||||
def test_evaluate_portfolio_serializable():
|
||||
from easy_tdx.backtest.benchmark import evaluate_portfolio
|
||||
|
||||
report = evaluate_portfolio(
|
||||
_CycleTrader(), _stocks_for_portfolio(), total_cash=500_000, n_windows=3
|
||||
)
|
||||
text = json.dumps(report, default=str)
|
||||
assert "excess_return" in text
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from easy_tdx.backtest.portfolio_engine import (
|
||||
PortfolioBacktestEngine,
|
||||
@@ -195,3 +196,106 @@ class TestCombinedEquity:
|
||||
"drawdown",
|
||||
"drawdown_pct",
|
||||
}
|
||||
|
||||
|
||||
class TestPortfolioFullMetrics:
|
||||
"""v1.31:组合级完整绩效指标(合并净值 + 汇总成交喂 PerformanceAnalyzer)。"""
|
||||
|
||||
def test_total_performance_has_full_metrics(self) -> None:
|
||||
"""组合整体绩效应含与单标的同口径的完整指标(SQN/连胜连亏等)。"""
|
||||
stocks = [
|
||||
StockData("000001", "SZ", _make_df(100, seed=42)),
|
||||
StockData("600000", "SH", _make_df(100, seed=99)),
|
||||
]
|
||||
result = PortfolioBacktestEngine(
|
||||
strategy=SimpleBuyStrategy, stocks=stocks, total_cash=200000
|
||||
).run()
|
||||
|
||||
perf = result.total_performance
|
||||
# 单标的 PerformanceAnalyzer 的全部关键键 + 组合字段
|
||||
for key in (
|
||||
"total_return",
|
||||
"annual_return",
|
||||
"max_drawdown",
|
||||
"sharpe",
|
||||
"sortino",
|
||||
"calmar",
|
||||
"volatility",
|
||||
"win_rate",
|
||||
"profit_factor",
|
||||
"sqn",
|
||||
"max_consecutive_wins",
|
||||
"max_consecutive_losses",
|
||||
"total_stocks",
|
||||
"total_cash",
|
||||
):
|
||||
assert key in perf, f"缺少指标 {key}"
|
||||
assert perf["total_stocks"] == 2
|
||||
assert perf["total_cash"] == 200000
|
||||
|
||||
def test_annual_return_is_annualized(self) -> None:
|
||||
"""年化收益应基于时间长度换算,不再等于总收益(旧版直接赋值的简化)。"""
|
||||
stocks = [StockData("000001", "SZ", _make_df(400, seed=42))]
|
||||
result = PortfolioBacktestEngine(
|
||||
strategy=SimpleBuyStrategy, stocks=stocks, total_cash=100000
|
||||
).run()
|
||||
perf = result.total_performance
|
||||
assert perf["annual_return"] != perf["total_return"]
|
||||
|
||||
def test_drawdown_pct_positive_and_relative_to_peak(self) -> None:
|
||||
"""drawdown/drawdown_pct 应为正值且相对逐点峰值(与单标的/多策略口径一致)。"""
|
||||
stocks = [
|
||||
StockData("000001", "SZ", _make_df(100, seed=42)),
|
||||
StockData("600000", "SH", _make_df(100, seed=7)),
|
||||
]
|
||||
result = PortfolioBacktestEngine(
|
||||
strategy=SimpleBuyStrategy, stocks=stocks, total_cash=200000
|
||||
).run()
|
||||
ce = result.combined_equity
|
||||
assert (ce["drawdown_pct"] >= 0).all()
|
||||
assert (ce["drawdown"] >= 0).all()
|
||||
# 回撤比例 = 回撤额 / 当时峰值
|
||||
peak = ce["total"].cummax()
|
||||
expected = (peak - ce["total"]) / peak.where(peak != 0, 1.0)
|
||||
np.testing.assert_allclose(ce["drawdown_pct"], expected, rtol=1e-9)
|
||||
|
||||
def test_combined_trades_have_symbol_column(self) -> None:
|
||||
"""组合层汇总成交应附 symbol 列(FIFO 按标的分组 + 前端明细表用)。"""
|
||||
stocks = [
|
||||
StockData("000001", "SZ", _make_df(100, seed=42)),
|
||||
StockData("600000", "SH", _make_df(100, seed=99)),
|
||||
]
|
||||
result = PortfolioBacktestEngine(
|
||||
strategy=SimpleBuyStrategy, stocks=stocks, total_cash=200000
|
||||
).run()
|
||||
assert "symbol" in result.trades.columns
|
||||
assert set(result.trades["symbol"]) == {"SZ000001", "SH600000"}
|
||||
# 每个标的的成交数 == 该标的独立回测的成交数
|
||||
for key, res in result.individual_results.items():
|
||||
n = (result.trades["symbol"] == key).sum()
|
||||
assert n == len(res.trades)
|
||||
|
||||
def test_total_return_matches_capital_weighted(self) -> None:
|
||||
"""组合 total_return 应等于各标的资金加权收益(合并曲线首值=总资金)。"""
|
||||
stocks = [
|
||||
StockData("000001", "SZ", _make_df(100, seed=42)),
|
||||
StockData("600000", "SH", _make_df(100, seed=99)),
|
||||
]
|
||||
result = PortfolioBacktestEngine(
|
||||
strategy=SimpleBuyStrategy, stocks=stocks, total_cash=200000
|
||||
).run()
|
||||
weighted = sum(
|
||||
0.5 * res.performance.get("total_return", 0.0)
|
||||
for res in result.individual_results.values()
|
||||
)
|
||||
assert result.total_performance["total_return"] == pytest.approx(weighted, abs=1e-9)
|
||||
|
||||
def test_to_dict_contains_trades(self) -> None:
|
||||
"""to_dict 应包含组合层成交表(REST/AI 解读消费)。"""
|
||||
stocks = [StockData("000001", "SZ", _make_df(100, seed=42))]
|
||||
result = PortfolioBacktestEngine(
|
||||
strategy=SimpleBuyStrategy, stocks=stocks, total_cash=100000
|
||||
).run()
|
||||
d = result.to_dict()
|
||||
assert "trades" in d
|
||||
assert isinstance(d["trades"], list)
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
"""单元测试:组合级 Walk-Forward 引擎(PortfolioWalkForwardEngine,v1.31)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from easy_tdx.backtest.portfolio_engine import StockData
|
||||
from easy_tdx.backtest.strategy import Strategy
|
||||
from easy_tdx.backtest.walkforward import PortfolioWalkForwardEngine
|
||||
|
||||
|
||||
class PeriodicStrategy(Strategy):
|
||||
"""每 10 根切换一次持仓,保证窗口内有成交(与单标的 WF 测试同思路)。"""
|
||||
|
||||
def init(self) -> None:
|
||||
self._holding = False
|
||||
|
||||
def next(self) -> None:
|
||||
if self._bar_index % 10 == 0 and not self._holding:
|
||||
self.buy(size=0)
|
||||
self._holding = True
|
||||
elif self._bar_index % 10 == 5 and self._holding:
|
||||
self.sell(size=0)
|
||||
self._holding = False
|
||||
|
||||
|
||||
def _make_df(n: int = 400, seed: int = 42, start: str = "2023-01-01") -> pd.DataFrame:
|
||||
rng = np.random.default_rng(seed)
|
||||
close = 100.0 + np.cumsum(rng.normal(0, 1, n))
|
||||
high = close + rng.uniform(0, 1, n)
|
||||
low = close - rng.uniform(0, 1, n)
|
||||
open_ = low + rng.uniform(0, high - low, n)
|
||||
vol = rng.integers(1_000_000, 10_000_000, n).astype(float)
|
||||
return pd.DataFrame(
|
||||
{
|
||||
"datetime": pd.date_range(start, periods=n, freq="D"),
|
||||
"open": open_,
|
||||
"high": high,
|
||||
"low": low,
|
||||
"close": close,
|
||||
"vol": vol,
|
||||
"amount": vol * close,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _stocks() -> list[StockData]:
|
||||
return [
|
||||
StockData("000001", "SZ", _make_df(400, seed=42)),
|
||||
StockData("600000", "SH", _make_df(400, seed=99)),
|
||||
]
|
||||
|
||||
|
||||
class TestPortfolioWalkForward:
|
||||
def test_basic_structure(self) -> None:
|
||||
"""切窗数量、窗口字段与聚合指标齐全。"""
|
||||
wf = PortfolioWalkForwardEngine(
|
||||
strategy=PeriodicStrategy, stocks=_stocks(), n_windows=4, total_cash=200_000
|
||||
).run()
|
||||
assert len(wf.windows) == 4
|
||||
for i, w in enumerate(wf.windows):
|
||||
assert w.index == i
|
||||
assert w.start <= w.end
|
||||
assert w.bars > 0
|
||||
# 窗口时间升序且不重叠
|
||||
starts = [pd.Timestamp(w.start) for w in wf.windows]
|
||||
assert starts == sorted(starts)
|
||||
assert wf.total_trades > 0
|
||||
|
||||
def test_aggregates_consistency_and_chained(self) -> None:
|
||||
"""consistency = 盈利窗占比,chained = 各窗连乘 - 1。"""
|
||||
wf = PortfolioWalkForwardEngine(
|
||||
strategy=PeriodicStrategy, stocks=_stocks(), n_windows=5
|
||||
).run()
|
||||
rets = [w.total_return for w in wf.windows]
|
||||
assert wf.consistency == sum(1 for r in rets if r > 0) / len(rets)
|
||||
chained = float(np.prod([1.0 + r for r in rets]) - 1.0)
|
||||
assert wf.chained_return == pd.Series([chained]).iloc[0]
|
||||
|
||||
def test_insufficient_data_returns_empty(self) -> None:
|
||||
"""数据不足以切窗时返回空结果(windows 为空、聚合指标为 0)。"""
|
||||
stocks = [StockData("000001", "SZ", _make_df(50, seed=1))]
|
||||
wf = PortfolioWalkForwardEngine(strategy=PeriodicStrategy, stocks=stocks, n_windows=7).run()
|
||||
assert wf.windows == []
|
||||
assert wf.consistency == 0.0
|
||||
|
||||
def test_empty_stocks_returns_empty(self) -> None:
|
||||
wf = PortfolioWalkForwardEngine(strategy=PeriodicStrategy, stocks=[], n_windows=3).run()
|
||||
assert wf.windows == []
|
||||
|
||||
def test_late_listing_stock_tolerated(self) -> None:
|
||||
"""晚上市的标的不该拖垮整窗(该窗跳过它,其余照常)。"""
|
||||
stocks = [
|
||||
StockData("000001", "SZ", _make_df(400, seed=42)),
|
||||
StockData("688981", "SH", _make_df(100, seed=7, start="2024-02-01")),
|
||||
]
|
||||
wf = PortfolioWalkForwardEngine(strategy=PeriodicStrategy, stocks=stocks, n_windows=4).run()
|
||||
assert len(wf.windows) == 4
|
||||
assert all(w.total_trades > 0 for w in wf.windows)
|
||||
|
||||
def test_window_independent_opening(self) -> None:
|
||||
"""每窗独立开仓:窗口总交易数应等于窗内各标的回合数(无跨窗结转)。"""
|
||||
stocks = _stocks()
|
||||
n_windows = 4
|
||||
wf = PortfolioWalkForwardEngine(
|
||||
strategy=PeriodicStrategy, stocks=stocks, n_windows=n_windows
|
||||
).run()
|
||||
# PeriodicStrategy 每 10 根一个回合,窗长约 56 根 → 每标的每窗 5 回合上下,
|
||||
# 总交易数应为正且与窗口长度量级一致(防止持仓跨窗导致的重复/丢失计数)。
|
||||
assert wf.total_trades > 0
|
||||
assert wf.total_trades == sum(w.total_trades for w in wf.windows)
|
||||
|
||||
def test_to_dict_serializable(self) -> None:
|
||||
wf = PortfolioWalkForwardEngine(
|
||||
strategy=PeriodicStrategy, stocks=_stocks(), n_windows=3
|
||||
).run()
|
||||
d = wf.to_dict()
|
||||
assert len(d["windows"]) == len(wf.windows)
|
||||
# JSON 兼容(numpy 标量已清洗)
|
||||
json.dumps(d)
|
||||
# 每窗 performance 为完整指标 dict(含 SQN 等深度指标)
|
||||
assert "sqn" in d["windows"][0]["performance"]
|
||||
assert "max_consecutive_wins" in d["windows"][0]["performance"]
|
||||
|
||||
def test_min_windows_guard(self) -> None:
|
||||
"""n_windows < 2 至少取 2(与单标的 WF 同保护)。"""
|
||||
wf = PortfolioWalkForwardEngine(
|
||||
strategy=PeriodicStrategy, stocks=_stocks(), n_windows=0
|
||||
).run()
|
||||
assert wf.n_windows == 2
|
||||
@@ -1048,3 +1048,151 @@ def test_list_tasks_limit(client, sample_ohlcv):
|
||||
|
||||
resp = client.get("/api/v1/backtest/tasks?limit=2")
|
||||
assert resp.json()["count"] <= 2
|
||||
|
||||
|
||||
# ── 组合级 WF / 一条龙评估端点(v1.31)────────────────────────────────────────
|
||||
def _wait_task(client, task_id: str, rounds: int = 400):
|
||||
import time as _time
|
||||
|
||||
final = None
|
||||
for _ in range(rounds):
|
||||
poll = client.get(f"/api/v1/backtest/tasks/{task_id}")
|
||||
final = poll.json()
|
||||
if final["status"] in ("done", "failed"):
|
||||
break
|
||||
_time.sleep(0.05)
|
||||
return final
|
||||
|
||||
|
||||
def test_portfolio_backtest_includes_grade_score_trades(client, monkeypatch):
|
||||
"""组合回测响应附带 grade/score(对齐单标的)与组合层 trades。"""
|
||||
import pandas as pd
|
||||
|
||||
import easy_tdx.web.routers.backtest as bt_router
|
||||
from easy_tdx.backtest.portfolio_engine import StockData
|
||||
|
||||
async def fake_fetch(client_arg, stocks, category, start, end): # noqa: ANN001
|
||||
result = []
|
||||
for sym in stocks:
|
||||
mkt, code = sym.split(":")
|
||||
n = 100
|
||||
close = 10 + np.cumsum(np.random.randn(n) * 0.3 + 0.05)
|
||||
df = pd.DataFrame(
|
||||
{
|
||||
"datetime": pd.date_range("2024-01-01", periods=n, freq="B"),
|
||||
"open": close - 0.1,
|
||||
"high": close + 0.2,
|
||||
"low": close - 0.2,
|
||||
"close": close,
|
||||
"vol": np.full(n, 5000.0),
|
||||
"amount": close * 5000,
|
||||
}
|
||||
)
|
||||
result.append(StockData(code=code, market=mkt, df=df))
|
||||
return result
|
||||
|
||||
monkeypatch.setattr(bt_router, "_fetch_portfolio_bars", fake_fetch)
|
||||
|
||||
resp = client.post(
|
||||
"/api/v1/backtest/portfolio/run/async",
|
||||
json={"strategy": "ma_cross", "cash": 200000, "stocks": ["SZ:000001", "SH:600519"]},
|
||||
)
|
||||
assert resp.status_code == 202, resp.text
|
||||
final = _wait_task(client, resp.json()["task_id"])
|
||||
assert final["status"] == "done", final
|
||||
result = final["result"]
|
||||
# 评级 + 评分(与单标的响应同构)
|
||||
assert result["grade"]["grade"] in ("S", "A", "B", "C", "D")
|
||||
assert result["grade"]["scenario"] == "portfolio"
|
||||
assert 0 <= result["score"]["total"] <= 100
|
||||
# 完整绩效指标(含 SQN/连胜连亏)+ 组合层成交
|
||||
assert "sqn" in result["total_performance"]
|
||||
assert "max_consecutive_losses" in result["total_performance"]
|
||||
assert isinstance(result["trades"], list)
|
||||
|
||||
|
||||
def test_portfolio_walkforward_endpoint(client, monkeypatch):
|
||||
"""POST /backtest/portfolio/wf/run/async 端到端(mock 行情取数)。"""
|
||||
import pandas as pd
|
||||
|
||||
import easy_tdx.web.routers.backtest as bt_router
|
||||
from easy_tdx.backtest.portfolio_engine import StockData
|
||||
|
||||
async def fake_fetch(client_arg, stocks, category, start, end): # noqa: ANN001
|
||||
result = []
|
||||
for sym in stocks:
|
||||
mkt, code = sym.split(":")
|
||||
n = 400
|
||||
close = 10 + np.cumsum(np.random.randn(n) * 0.3 + 0.02)
|
||||
df = pd.DataFrame(
|
||||
{
|
||||
"datetime": pd.date_range("2023-01-02", periods=n, freq="B"),
|
||||
"open": close - 0.1,
|
||||
"high": close + 0.2,
|
||||
"low": close - 0.2,
|
||||
"close": close,
|
||||
"vol": np.full(n, 5000.0),
|
||||
"amount": close * 5000,
|
||||
}
|
||||
)
|
||||
result.append(StockData(code=code, market=mkt, df=df))
|
||||
return result
|
||||
|
||||
monkeypatch.setattr(bt_router, "_fetch_portfolio_bars", fake_fetch)
|
||||
|
||||
resp = client.post(
|
||||
"/api/v1/backtest/portfolio/wf/run/async?n_windows=3",
|
||||
json={"strategy": "ma_cross", "cash": 200000, "stocks": ["SZ:000001", "SH:600519"]},
|
||||
)
|
||||
assert resp.status_code == 202, resp.text
|
||||
final = _wait_task(client, resp.json()["task_id"])
|
||||
assert final["status"] == "done", final
|
||||
wf = final["result"]["walkforward"]
|
||||
assert wf["n_windows"] == 3
|
||||
assert len(wf["windows"]) == 3
|
||||
assert "consistency" in wf
|
||||
assert "sqn" in wf["windows"][0]["performance"]
|
||||
|
||||
|
||||
def test_portfolio_evaluate_endpoint(client, monkeypatch):
|
||||
"""POST /backtest/portfolio/evaluate/run/async 端到端(mock 行情取数)。"""
|
||||
import pandas as pd
|
||||
|
||||
import easy_tdx.web.routers.backtest as bt_router
|
||||
from easy_tdx.backtest.portfolio_engine import StockData
|
||||
|
||||
async def fake_fetch(client_arg, stocks, category, start, end): # noqa: ANN001
|
||||
result = []
|
||||
for sym in stocks:
|
||||
mkt, code = sym.split(":")
|
||||
n = 400
|
||||
close = 10 + np.cumsum(np.random.randn(n) * 0.3 + 0.02)
|
||||
df = pd.DataFrame(
|
||||
{
|
||||
"datetime": pd.date_range("2023-01-02", periods=n, freq="B"),
|
||||
"open": close - 0.1,
|
||||
"high": close + 0.2,
|
||||
"low": close - 0.2,
|
||||
"close": close,
|
||||
"vol": np.full(n, 5000.0),
|
||||
"amount": close * 5000,
|
||||
}
|
||||
)
|
||||
result.append(StockData(code=code, market=mkt, df=df))
|
||||
return result
|
||||
|
||||
monkeypatch.setattr(bt_router, "_fetch_portfolio_bars", fake_fetch)
|
||||
|
||||
resp = client.post(
|
||||
"/api/v1/backtest/portfolio/evaluate/run/async",
|
||||
json={"strategy": "ma_cross", "cash": 200000, "stocks": ["SZ:000001", "SH:600519"]},
|
||||
)
|
||||
assert resp.status_code == 202, resp.text
|
||||
final = _wait_task(client, resp.json()["task_id"])
|
||||
assert final["status"] == "done", final
|
||||
report = final["result"]
|
||||
for key in ("performance", "score", "grade", "walkforward", "fitness", "benchmark", "config"):
|
||||
assert key in report
|
||||
assert report["grade"]["scenario"] == "portfolio"
|
||||
assert report["fitness"]["total_checks"] == 8
|
||||
assert report["config"]["stocks"] == ["SZ000001", "SH600519"]
|
||||
|
||||
Reference in New Issue
Block a user