docs(plan): 富回测结果页实施计划—8任务TDD(metrics→数据→引擎→API→前端→部署)
This commit is contained in:
@@ -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、pytest;Vue3 `<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: 基准日收益率 Series,index 对齐 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 入 DB,series 写 `{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 合入现有 statistics;series `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→str,Series→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 Result,mock 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 引用一致。✓
|
||||
Reference in New Issue
Block a user