Files
sanguo_vnpy_v2/docs/superpowers/plans/2026-07-11-backtest-result-page.md
T

477 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 富回测结果页(聚宽级)实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 把 CTA 回测结果页升级到聚宽级(10 指标卡 + 5 图 + 4 tab + 时间缩放),后端用 empyrical 补齐相对基准指标(Alpha/Beta/Sortino/IR),基准可选沪深300/中证500。
**Architecture:** vnpy 跑完回测产出 `daily_df` → 新增 `sanguo_backtest/metrics.py`empyrical 纯函数)算 10 标量指标 + 5 逐日时序 → 存 DB+json → FastAPI 扩端点返回 → 前端 `Result.vue` 重构渲染(echarts)。不碰回测引擎撮合逻辑,只加结果计算层。
**Tech Stack:** Python 3.10(容器)/3.14(本机)、vnpy_ctastrategy、empyrical(新增)、pandas、FastAPI、pytestVue3 `<script setup>`、element-plus、echarts、vitest。
## Global Constraints
- **vnpy 零修改**:不碰 `vnpy_v4.4.0/` 源码,仅在其产出 `daily_df` 之上加计算
- **数据下载硬约束**:下载沪深300 直连不走代理(`unset http_proxy https_proxy`)、单线程限速、优先 baostock
- **rsync 同步**:到 NAS **不排除 `tests/data`**(见记忆 rsync-tests-data-sync
- **NAS docker 全路径**`/var/packages/Docker/target/usr/bin/docker`
- **不引未确认依赖**:仅新增 `empyrical`;前端不新增依赖(echarts/element-plus 已有)
- **基准编码**:沪深300=`sh000300`(下载补齐),中证500=`sz000905`(现成)
- **benchmark 入参字面量**`"hs300"` / `"zz500"`
- **提交规范**`feat/fix/docs/test:` 前缀,**不加** Co-Authored-By(全局已禁 attribution
---
## File Structure
**新增(后端)**
- `sanguo_backtest/metrics.py` — 指标计算纯函数模块(empyrical)。**核心**
- `sanguo_data/index_downloader.py` — 沪深300 指数日线下载(baostock,一次性/补齐)
- `tests/backtest/test_metrics.py` — metrics 单测
- `tests/data/test_index_downloader.py` — 下载器单测(mock baostock
**修改(后端)**
- `sanguo_data/datareader.py` — 加 `read_index_daily(code, start, end)`
- `sanguo_backtest/cta_engine.py``run_cta_backtest` 跑完后调 `compute_metrics`,结果落盘
- `sanguo_api/routes.py``/backtest/cta``benchmark` 入参;`/task/:id/result``relative_metrics`;新增 4 端点
- `config/backtest.yaml` — 加默认 `benchmark: hs300`
- `requirements-docker.txt` — 加 `empyrical`
**新增(前端 `frontend/src/`**
- `components/backtest/MetricCards.vue` — 10 指标卡
- `components/backtest/BenchmarkCurve.vue` — 策略 vs 基准累计收益
- `components/backtest/AlphaChart.vue` — 逐日 alpha
- `components/backtest/BetaChart.vue` — 逐日 beta
- `components/backtest/VolatilityChart.vue` — 策略 vs 基准波动率
- `components/backtest/DrawdownChart.vue` — 逐日回撤
- 对应 `*.spec.ts` vitest 测试
**修改(前端)**
- `views/backtest/Result.vue` — 重构为指标卡+5图+4tab+缩放布局
- `api/backtest.ts`(或现有 api 封装)— 加新端点调用
---
## Task 1: metrics.py 指标计算模块(核心,TDD)
**Files:**
- Create: `sanguo_backtest/metrics.py`
- Test: `tests/backtest/test_metrics.py`
**Interfaces:**
- Consumes: vnpy `daily_df`(含 `"return"` 日收益列,index 为日期)+ 基准日收益 `pd.Series`
- Produces:
- `MetricsResult` dataclass`scalars: dict[str,float]` + `series: dict[str,pd.Series]`
- `compute_metrics(daily_df: pd.DataFrame, benchmark_returns: pd.Series, period=252) -> MetricsResult`
- `BenchmarkCode = Literal["hs300","zz500"]``BENCHMARK_SYMBOL = {"hs300":"sh000300","zz500":"sz000905"}`
- [ ] **Step 1: 加依赖 empyrical**
`requirements-docker.txt` 追加 `empyrical`;本机 `pip install empyrical`(容器侧 Task 8 部署时装)。
- [ ] **Step 2: 写失败测试**
`tests/backtest/test_metrics.py`
```python
import sys, os
_VNPY_SRC = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "vnpy_v4.4.0"))
sys.path.insert(0, _VNPY_SRC)
import pandas as pd
import numpy as np
import empyrical
from sanguo_backtest.metrics import compute_metrics, MetricsResult, BENCHMARK_SYMBOL
def _make_daily(returns):
idx = pd.date_range("2024-01-01", periods=len(returns), freq="B")
return pd.DataFrame({"return": returns}, index=idx)
def test_compute_metrics_scalars_match_empyrical():
np.random.seed(42)
strat = pd.Series(np.random.normal(0.001, 0.02, 100),
index=pd.date_range("2024-01-01", periods=100, freq="B"))
bench = pd.Series(np.random.normal(0.0005, 0.015, 100), index=strat.index)
daily_df = pd.DataFrame({"return": strat.values}, index=strat.index)
res = compute_metrics(daily_df, bench)
assert isinstance(res, MetricsResult)
# 标量口径与 empyrical 直接计算一致
assert abs(res.scalars["alpha"] - empyrical.alpha(strat, bench)) < 1e-9
assert abs(res.scalars["beta"] - empyrical.beta(strat, bench)) < 1e-9
assert abs(res.scalars["sharpe_ratio"] - empyrical.sharpe_ratio(strat)) < 1e-9
assert abs(res.scalars["sortino_ratio"] - empyrical.sortino_ratio(strat)) < 1e-9
assert abs(res.scalars["max_drawdown"] - empyrical.max_drawdown(strat)) < 1e-9
assert abs(res.scalars["annual_volatility"] - empyrical.annual_volatility(strat)) < 1e-9
def test_compute_metrics_has_all_required_scalars():
strat = pd.Series([0.01, -0.005, 0.02, 0.0],
index=pd.date_range("2024-01-01", periods=4, freq="B"))
bench = pd.Series([0.005, 0.001, 0.01, -0.002], index=strat.index)
res = compute_metrics(pd.DataFrame({"return": strat.values}, index=strat.index), bench)
required = {"total_return","annual_return","alpha","beta","sharpe_ratio",
"sortino_ratio","information_ratio","annual_volatility","max_drawdown",
"benchmark_return","benchmark_volatility"}
assert required.issubset(res.scalars.keys())
def test_compute_metrics_series_keys_and_length():
strat = pd.Series(np.random.normal(0, 0.01, 50),
index=pd.date_range("2024-01-01", periods=50, freq="B"))
bench = pd.Series(np.random.normal(0, 0.01, 50), index=strat.index)
res = compute_metrics(pd.DataFrame({"return": strat.values}, index=strat.index), bench)
for key in ["equity_curve","benchmark_curve","alpha","beta","drawdown"]:
assert key in res.series
assert len(res.series[key]) == 50
assert res.series["drawdown"].max() <= 1e-9 # 回撤 <= 0
def test_benchmark_symbol_map():
assert BENCHMARK_SYMBOL["hs300"] == "sh000300"
assert BENCHMARK_SYMBOL["zz500"] == "sz000905"
```
- [ ] **Step 3: 跑测试确认失败**
`pytest tests/backtest/test_metrics.py -v` → FAIL(模块不存在)
- [ ] **Step 4: 实现 metrics.py**
`sanguo_backtest/metrics.py`
```python
"""回测相对/绝对指标计算(empyrical,聚宽同源口径)。纯函数。"""
from dataclasses import dataclass, field
from typing import Dict, Literal
import numpy as np
import pandas as pd
import empyrical
BenchmarkCode = Literal["hs300", "zz500"]
BENCHMARK_SYMBOL: Dict[str, str] = {"hs300": "sh000300", "zz500": "sz000905"}
@dataclass
class MetricsResult:
scalars: Dict[str, float] = field(default_factory=dict)
series: Dict[str, pd.Series] = field(default_factory=dict)
def compute_metrics(
daily_df: pd.DataFrame,
benchmark_returns: pd.Series,
period: int = 252,
) -> MetricsResult:
"""对 vnpy daily_df + 基准日收益计算聚宽级指标。
daily_df: vnpy calculate_result() 产出,须含 "return" 列(日收益率),index 为日期。
benchmark_returns: 基准日收益率 Seriesindex 对齐 daily_df。
"""
strat = daily_df["return"].astype(float)
# 对齐
aligned = pd.concat([strat.rename("s"), benchmark_returns.rename("b")], axis=1).dropna()
s, b = aligned["s"], aligned["b"]
scalars = {
"total_return": float(empyrical.cum_returns_final(s)),
"annual_return": float(empyrical.annual_return(s, period=period)),
"alpha": float(empyrical.alpha(s, b, period=period)),
"beta": float(empyrical.beta(s, b, period=period)),
"sharpe_ratio": float(empyrical.sharpe_ratio(s, period=period)),
"sortino_ratio": float(empyrical.sortino_ratio(s, period=period)),
"information_ratio": float(empyrical.excess_sharpe(s, b)),
"annual_volatility": float(empyrical.annual_volatility(s, period=period)),
"max_drawdown": float(empyrical.max_drawdown(s)),
"benchmark_return": float(empyrical.cum_returns_final(b)),
"benchmark_volatility": float(empyrical.annual_volatility(b, period=period)),
}
equity = empyrical.cum_returns(s)
bench_curve = empyrical.cum_returns(b)
# rolling alpha/beta (63 日窗口,不足则 expanding)
window = min(63, len(s))
if window >= 2:
cov = aligned.rolling(window, min_periods=2).cov()
# 用简单 rolling beta/alpha 近似(逐日时序用于画图,口径由 scalars 保证)
roll_beta = pd.Series(index=s.index, dtype=float)
roll_alpha = pd.Series(index=s.index, dtype=float)
for i in range(len(s)):
sub = aligned.iloc[: i + 1]
if len(sub) >= 2 and sub["b"].var() > 0:
beta = sub["s"].cov(sub["b"]) / sub["b"].var()
alpha = sub["s"].mean() - beta * sub["b"].mean()
roll_beta.iloc[i] = beta
roll_alpha.iloc[i] = alpha * period
else:
roll_beta = pd.Series([np.nan] * len(s), index=s.index)
roll_alpha = pd.Series([np.nan] * len(s), index=s.index)
drawdown = empyrical.drawdown(s)
series = {
"equity_curve": equity,
"benchmark_curve": bench_curve,
"alpha": roll_alpha,
"beta": roll_beta,
"drawdown": drawdown,
}
return MetricsResult(scalars=scalars, series=series)
```
- [ ] **Step 5: 跑测试确认通过**
`pytest tests/backtest/test_metrics.py -v` → 4 PASS
- [ ] **Step 6: Commit**
`git add sanguo_backtest/metrics.py tests/backtest/test_metrics.py requirements-docker.txt && git commit -m "feat(backtest): metrics模块—empyrical算10指标+5时序(聚宽同源口径)"`
---
## Task 2: 沪深300 数据下载 + datareader.read_index_daily
**Files:**
- Create: `sanguo_data/index_downloader.py`
- Modify: `sanguo_data/datareader.py`(加 `read_index_daily`
- Test: `tests/data/test_index_downloader.py`
**Interfaces:**
- Produces: `download_index(symbol="sh000300", start_year, end_year, out_dir)``datareader.read_index_daily(code, start, end) -> pd.DataFrame`(列含 `datetime/close`,复用现有 parquet 读取路径 `{daily_dir}/{year}/{code}_daily.parquet`
- [ ] **Step 1: 写失败测试(下载器 mock baostock**
`tests/data/test_index_downloader.py`mock `baostock.query_history_k_data_plus` 返回固定 DataFrame,断言写出 `sh000300_daily.parquet` 且含 close 列、行数正确。另写 `test_read_index_daily_reads_parquet`:造一个临时 parquet,断言 `read_index_daily` 读回正确。
- [ ] **Step 2: 跑确认失败**
- [ ] **Step 3: 实现 index_downloader.py**
用 baostock`bs.query_history_k_data_plus("sh.000300", "date,close", ...)`)下载沪深300 收盘,按年切分写 `{out_dir}/{year}/sh000300_daily.parquet`。**直连不走代理**:函数入口 `os.environ.pop("http_proxy", None); os.environ.pop("https_proxy", None)`。单线程、`time.sleep` 限速。复用项目现有下载模式(参考 baostock-15min-source 记忆)。
- [ ] **Step 4: datareader 加 read_index_daily**
```python
def read_index_daily(self, code: str, start: date, end: date) -> pd.DataFrame:
"""读指数日线(sh000300/sz000905),复用 read_parquet_daily 的年分片 parquet 路径。"""
# 复用现有 read_parquet_daily 的 {daily_dir}/{year}/{code}_daily.parquet 逻辑
```
(实现者:读 `datareader.py` 现有 `read_parquet_daily`,提取/复用其按年读取逻辑,code 直接用 `sh000300`/`sz000905`。)
- [ ] **Step 5: 跑测试通过**
- [ ] **Step 6: 实际下载沪深300(一次性补数据)**
容器或本机执行 `download_index("sh000300", 2010, 2026, daily_dir)` → 写到 NAS `/volume1/stock/A股数据/日线数据/daily/{year}/sh000300_daily.parquet`。校验:`ssh sanguo-nas "ls /volume1/stock/A股数据/日线数据/daily/2024/sh000300_daily.parquet"`
- [ ] **Step 7: Commit**
`git add sanguo_data/index_downloader.py sanguo_data/datareader.py tests/data/test_index_downloader.py && git commit -m "feat(data): 沪深300指数下载+read_index_daily(补基准数据缺口)"`
---
## Task 3: 回测流程集成 metrics
**Files:**
- Modify: `sanguo_backtest/cta_engine.py``run_cta_backtest` 跑完后算 metrics
- Modify: `config/backtest.yaml`(加 `benchmark: hs300`
- Test: `tests/backtest/test_cta_engine.py`(新增/扩展)
**Interfaces:**
- Consumes: Task 1 `compute_metrics`、Task 2 `read_index_daily`
- Produces: `run_cta_backtest` 返回值/落盘含 `metrics: MetricsResult`scalars 入 DBseries 写 `{task_id}_metrics.json`
- [ ] **Step 1: 写失败测试**
mock 一个 vnpy `daily_df` + mock `read_index_daily`,断言 `run_cta_backtest` 结果含 `relative_metrics`alpha/beta 键)且写了 `{task_id}_metrics.json`
- [ ] **Step 2: 跑确认失败**
- [ ] **Step 3: 改 cta_engine.run_cta_backtest**
`engine.calculate_statistics(daily_df)` 之后:
1. 从 config 读 `benchmark`(默认 hs300)→ `BENCHMARK_SYMBOL` 映射 code
2. `read_index_daily(code, start, end)` → 算基准日收益(`close.pct_change().dropna()`
3. `metrics = compute_metrics(daily_df, benchmark_returns)`
4. scalars 合入现有 statisticsseries `metrics.series` 序列化写 `{task_id}_metrics.json`
注意:`daily_df` 的 index 须是日期;若 vnpy 用 int index,先转。基准日期与策略日期对齐在 `compute_metrics` 内已 dropna 处理。
- [ ] **Step 4: config/backtest.yaml 加默认 benchmark**
```yaml
backtest:
...
benchmark: hs300 # hs300 | zz500
```
- [ ] **Step 5: 跑测试通过**
- [ ] **Step 6: Commit**
`git add sanguo_backtest/cta_engine.py config/backtest.yaml tests/backtest/test_cta_engine.py && git commit -m "feat(backtest): 回测流程集成基准对比—产出相对指标+时序json"`
---
## Task 4: API 扩展端点
**Files:**
- Modify: `sanguo_api/routes.py`
- Test: `tests/api/test_routes.py`(扩展)
**Interfaces:**
- Consumes: Task 3 落盘的 metrics
- Produces:
- `POST /backtest/cta` 入参 `CtaBacktestRequest``benchmark: str = "hs300"`
- `GET /task/:id/result` 出参加 `relative_metrics: dict`10 标量)
- `GET /task/:id/benchmark-curve``{dates:[], strategy:[], benchmark:[]}`
- `GET /task/:id/risk-series``{dates:[], alpha:[], beta:[], drawdown:[]}`
- `GET /task/:id/daily-holdings` → 每日持仓 DataFrame 记录(vnpy daily_df 已有 end_value 等)
- `GET /task/:id/log` → 回测日志文本
- [ ] **Step 1: 写失败测试**
`test_backtest_cta_accepts_benchmark`POST 带 benchmark=zz500,断言接受)、`test_task_result_includes_relative_metrics``test_benchmark_curve_endpoint``test_risk_series_endpoint``test_daily_holdings_endpoint``test_log_endpoint`。用现有 test_routes.py 的 mock 模式(参考已有的 task/result 测试)。
- [ ] **Step 2: 跑确认失败**
- [ ] **Step 3: 实现 routes.py**
- `CtaBacktestRequest``benchmark: str = "hs300"`,校验 `benchmark in ("hs300","zz500")`
- `/task/:id/result``{task_id}_metrics.json`,附加 `relative_metrics`
- 4 个新端点从 `{task_id}_metrics.json` / daily_df 读对应序列返回(JSON 可序列化:dates→strSeries→list
- [ ] **Step 4: 跑测试通过** `pytest tests/api/test_routes.py -v`
- [ ] **Step 5: Commit**
`git add sanguo_api/routes.py tests/api/test_routes.py && git commit -m "feat(api): 回测结果API加relative_metrics+基准曲线/风险序列/持仓/日志4端点"`
---
## Task 5: 前端 — MetricCards + API 层
**Files:**
- Create: `frontend/src/components/backtest/MetricCards.vue`
- Modify: `frontend/src/api/`(加新端点调用,找到现有 api 封装文件按其模式加)
- Test: `frontend/src/components/backtest/MetricCards.spec.ts`
- [ ] **Step 1: 写失败测试(vitest**
mount MetricCards,传固定 metrics prop,断言渲染 10 个指标卡且数值/标签正确。
- [ ] **Step 2: 跑确认失败** `cd frontend && npx vitest run MetricCards`
- [ ] **Step 3: 实现 MetricCards.vue**
`<script setup>``props: { metrics: {total_return, annual_return, alpha, beta, sharpe_ratio, sortino_ratio, information_ratio, annual_volatility, max_drawdown, benchmark_return, benchmark_volatility} }`。用 `el-card` 网格布局,数值格式化(百分比/小数)。**先读** 现有组件(如 `views/backtest/Result.vue` 顶部、`EquityChart.vue`)匹配风格。
- [ ] **Step 4: 加 API 调用**
在现有 api 封装文件按 axios 模式加:`getResult(id)``getBenchmarkCurve(id)``getRiskSeries(id)``getDailyHoldings(id)``getLog(id)`
- [ ] **Step 5: 跑测试通过**
- [ ] **Step 6: Commit**
`git add frontend/src/components/backtest/MetricCards.vue frontend/src/components/backtest/MetricCards.spec.ts frontend/src/api/ && git commit -m "feat(frontend): MetricCards指标卡组件+结果页API封装"`
---
## Task 6: 前端 — 5 图组件
**Files:**
- Create: `BenchmarkCurve.vue``AlphaChart.vue``BetaChart.vue``VolatilityChart.vue``DrawdownChart.vue`(均 `frontend/src/components/backtest/`
- Test: 每个 `*.spec.ts`
- [ ] **Step 1: 写失败测试**
每组件 mount + 传固定 series prop,断言 echarts init 被调用/容器渲染(参考现有 `EquityChart.spec` 若有,否则断言容器 DOM + prop 透传)。
- [ ] **Step 2: 跑确认失败**
- [ ] **Step 3: 实现 5 图组件**
每个 `<script setup>`props 接 `{dates, values[]...}``onMounted` 用 echarts 初始化、`watch` 数据更新。**先读现有 `EquityChart.vue`** 完全照搬其 echarts 初始化/resize/销毁模式(DRY)。颜色:策略=红、基准=蓝、alpha/beta=绿、回撤=橙(对齐聚宽结果页截图)。
- [ ] **Step 4: 跑测试通过** `npx vitest run`
- [ ] **Step 5: Commit**
`git add frontend/src/components/backtest/{BenchmarkCurve,AlphaChart,BetaChart,VolatilityChart,DrawdownChart}.vue frontend/src/components/backtest/*.spec.ts && git commit -m "feat(frontend): 5个结果页图组件(基准曲线/Alpha/Beta/波动率/回撤)"`
---
## Task 7: 前端 — Result.vue 重构整合(4 tab + 缩放)
**Files:**
- Modify: `frontend/src/views/backtest/Result.vue`
- Test: `frontend/src/views/backtest/Result.spec.ts`(若无则造)
- [ ] **Step 1: 写失败测试**
mount Resultmock API 返回固定数据,断言:10 指标卡渲染、4 tab 可切换、时间缩放选择器存在、图容器渲染。
- [ ] **Step 2: 跑确认失败**
- [ ] **Step 3: 重构 Result.vue**
布局(高仿聚宽官方截图 edit_alg6_1.png):
- 顶部:`<MetricCards :metrics="result.relative_metrics" />`
- 中部:时间缩放 `el-radio-group`1周/1月/6月/1年/全部,按 dates 过滤)+ 5 图纵向堆叠
- tab`el-tabs`):收益概述(5图) / 交易详情(复用 TradesTable) / 每日持仓&收益(新表) / 日志输出(pre)
- `onMounted` 并发拉 result + benchmark-curve + risk-series + daily-holdings + log
- [ ] **Step 4: 跑测试通过 + `npm run build`vue-tsc 类型检查)**
- [ ] **Step 5: Commit**
`git add frontend/src/views/backtest/Result.vue frontend/src/views/backtest/Result.spec.ts && git commit -m "feat(frontend): Result.vue重构—聚宽级10指标+5图+4tab+时间缩放"`
---
## Task 8: 部署 + 验收
- [ ] **Step 1: 本机全量测试**
`pytest -v`(本机 Python 3.14,预期 metrics/api/data 测试通过;容器专用测试可能 skip)+ `cd frontend && npm test && npm run build`。全绿才继续。
- [ ] **Step 2: rsync 到 NAS(不排除 tests/data**
```
rsync -avz -e ssh --exclude='.git' --exclude='vnpy_v4.4.0' --exclude='__pycache__' --exclude='.superpowers' --exclude='node_modules' --exclude='data_cache' ./ sanguo-nas:/volume1/homes/admin/.sanguo_projects/sanguo_vnpy_v2/
```
(注意:**不含** `--exclude='tests/data'`
- [ ] **Step 3: 容器装 empyrical + 重启**
```
ssh sanguo-nas "/var/packages/Docker/target/usr/bin/docker exec sanguo_vnpy_v2 pip install empyrical"
ssh sanguo-nas "/var/packages/Docker/target/usr/bin/docker restart sanguo_vnpy_v2"
```
- [ ] **Step 4: 容器内 pytest 复验**
`ssh sanguo-nas "/var/packages/Docker/target/usr/bin/docker exec sanguo_vnpy_v2 pytest -v"` → 全绿(容器 Python 3.10 应跑通含 vnpy.alpha 的测试)
- [ ] **Step 5: 实测验收**
容器跑一个真实 CTA 回测(双均线策略,benchmark=hs300),确认结果页:10 指标卡有值、收益曲线策略vs基准、alpha/beta/回撤图正常、4 tab 可切换。抽查 alpha/beta 数值合理性。
- [ ] **Step 6: 三向一致性检查 + 收尾 commit**
需求(10指标+5图+4tab+基准可选) ↔ 设计(spec §1/§5) ↔ 编码 实现一致。更新 spec 状态为"已验收"。若 spec/docs 有变动一并 commit。
---
## Self-Review
**1. Spec 覆盖:**
- §1 成功标准 10指标+5图+4tab+基准可选 → Task 1,4,5,6,7 ✓
- §2 非目标(组合/编辑器/自定义基准/Tick)→ 均未建对应任务 ✓
- §3 沪深300下载 + read_index_daily → Task 2 ✓
- §4.1 metrics.py empyrical → Task 1 ✓(完整代码+测试)
- §4.2 回测流程集成 → Task 3 ✓
- §4.3 API 扩展 → Task 4 ✓
- §5 前端组件/布局 → Task 5,6,7 ✓
- §6 数据流 → Task 3,4 串起 ✓
- §7 测试 TDD → 每任务均先写测试 ✓
- §8 部署 → Task 8 ✓
- §9 验收 → Task 8 Step 5,6 ✓
**2. 占位符扫描:** Task 2/3/4/5/6/7 对现有文件的修改用"先读现有文件照搬模式"而非凭空写代码——这是对现有代码库的合理处理(非占位符,是明确的实现指令)。Task 1 含完整代码+测试。无 TBD/TODO。✓
**3. 类型一致:** `compute_metrics(daily_df, benchmark_returns) -> MetricsResult``MetricsResult.scalars/series``BENCHMARK_SYMBOL``benchmark: "hs300"|"zz500"``read_index_daily(code,start,end)` 在 Task 1→2→3→4 引用一致。✓