docs(plan): 富回测结果页实施计划—8任务TDD(metrics→数据→引擎→API→前端→部署)

This commit is contained in:
2026-07-11 13:20:30 +08:00
parent 6c461d4d9c
commit e1b8c4ac68
@@ -0,0 +1,476 @@
# 富回测结果页(聚宽级)实施计划
> **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 引用一致。✓