diff --git a/docs/superpowers/plans/2026-07-07-phase3b-vue-frontend.md b/docs/superpowers/plans/2026-07-07-phase3b-vue-frontend.md new file mode 100644 index 0000000..8379cba --- /dev/null +++ b/docs/superpowers/plans/2026-07-07-phase3b-vue-frontend.md @@ -0,0 +1,405 @@ +# Phase 3b 投研+回测 Web 控制台 实施计划 + +> **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:** 建一个 Vue 3 前端控制台(投研+回测),对齐 vnpy client 回测模块,从公网 `vnpy.mysanguo.top` 可用。 + +**Architecture:** Vue 3 SPA(Vite 构建)→ FastAPI `sanguo_api`(容器:8000,从旧 `sanguo_web` 切过来)静态挂 `/` + 研究 API 在 `/api/v1/*`;后端调 `sanguo_backtest`/`sanguo_factor` 引擎。4 切片 S0→S1→S2→S3,每片可独立演示+测试。 + +**Tech Stack:** Vue3 + Vite + TypeScript + Element Plus + ECharts + Pinia + Vue Router + Axios;后端 FastAPI + pytest(现有);容器 Python 3.10。 + +## Global Constraints + +- **端口/反代红线**:容器 8000 不变;不碰 `vnpy.mysanguo.top` 的 frpc/socat/Caddy。只改容器内 uvicorn 目标 + 静态挂载。 +- **vnpy 零修改**:不改 `vnpy_v4.4.0/`;策略/参数从类属性读。 +- **配置源**:`config/backtest.yaml`(`backtest.db_path`/`file_dir`、`auth.username/password_hash/jwt_secret/token_expire_minutes`、`api.port`)。 +- **NAS 访问**:`ssh sanguo-nas`(key 免密);docker 全路径 `/var/packages/Docker/target/usr/bin/docker`;rsync/scp 不稳时用 `ssh sanguo-nas "cat > /path" < local`。 +- **默认登录**:admin / admin(`backtest.yaml` 的 password_hash;部署后改)。 +- **A 股 DB**:`/volume1/stock/sanguo_vnpy/data/quant_trading.db`(K线,via `read_db_daily`,bare symbol 如 `600000`)。 +- **JSON 安全**:所有新接口返回值 Timestamp→str、DataFrame→list[dict]。 +- **TDD**:后端每个新接口先写 pytest 失败测试;前端关键逻辑(auth store/api client)用 Vitest。 + +--- + +## File Structure + +**前端(新建 `frontend/`):** +``` +frontend/ +├── package.json, vite.config.ts, tsconfig.json, index.html +├── src/ +│ ├── main.ts, App.vue +│ ├── router/index.ts # 路由 + 登录守卫 +│ ├── stores/auth.ts # Pinia: token/user +│ ├── api/client.ts # axios 实例 + JWT 拦截器 + 401 处理 +│ ├── api/backtest.ts, api/factor.ts, api/strategy.ts +│ ├── composables/useTask.ts # 任务状态轮询 + WS +│ ├── views/Login.vue, Layout.vue # Layout = 4 入口侧栏 shell +│ ├── views/backtest/{New,Progress,Result,Optimize,History}.vue +│ ├── views/factor/{New,Result}.vue +│ └── components/charts/{EquityChart,DailyPnlChart,KlineChart}.vue +│ components/TradesTable.vue +└── tests/{auth.test.ts,client.test.ts} # vitest +``` + +**后端(修改/新建):** +``` +sanguo_api/main.py # 新:读 backtest.yaml → create_app → 暴露 app(uvicorn 目标) +sanguo_api/app.py # 改:create_app 加 static_dir 参数,挂 SPA + history fallback +sanguo_api/routes.py # 改:加 strategy/factor/kline/equity/daily-pnl/trades/ic-summary/report/opt-results/task-list +sanguo_api/schemas.py # 改(S3):CtaBacktestRequest 加 rate/slippage/capital +sanguo_api/strategy_registry.py # 新:枚举 vnpy_ctastrategy 策略 + 参数 +sanguo_api/kline.py # 新:read_db_daily → K线 list[dict] +sanguo_backtest/result_store.py # 改:BacktestResult 加 id;save_result 设 result.id +sanguo_backtest/cta_engine.py # 改:构建 equity_curve/trades DataFrame;save 传 file_dir +sanguo_orchestrator/runner.py # 改:_on_done 用 result.id(修 bug) +docker/entrypoint.sh # 改:uvicorn sanguo_web.api:app → sanguo_api.main:app +scripts/smoke_phase3b.py # 新:端到端冒烟(登录→回测→进度→结果接口齐) +tests/api/test_*.py # 新接口单测 +``` + +--- + +# 切片 S0:脚手架 + 切 app + 登录 + +## Task S0.1:sanguo_api/main.py —— 容器 app 入口 + +**Files:** +- Create: `sanguo_api/main.py` +- Test: `tests/api/test_main.py` + +**Interfaces:** +- Produces: `sanguo_api.main:app`(模块级 FastAPI,供 uvicorn),`sanguo_api.main.build_app(config_path: str, static_dir: str | None) -> FastAPI` + +- [ ] **Step 1: 写失败测试** + +```python +# tests/api/test_main.py +from sanguo_api.main import build_app + +def _cfg(tmp_path): + cfg = tmp_path / "bt.yaml" + cfg.write_text( + "backtest:\n max_workers: 1\n db_path: %s\n file_dir: %s\n" + "api:\n host: 0.0.0.0\n port: 8000\n" + "auth:\n username: admin\n password_hash: x\n jwt_secret: s\n token_expire_minutes: 60\n" + "pool:\n max_workers: 1\n" % (tmp_path / "r.db", tmp_path / "f") + ) + return str(cfg) + +def test_build_app_has_api_routes(tmp_path): + app = build_app(_cfg(tmp_path)) + paths = [getattr(r, "path", "") for r in app.routes] + assert "/api/v1/auth/login" in paths + +def test_build_app_mounts_spa(tmp_path): + spa = tmp_path / "spa"; spa.mkdir(); (spa / "index.html").write_text("

SPA

") + app = build_app(_cfg(tmp_path), static_dir=str(spa)) + assert any(getattr(r, "path", "") == "/" for r in app.routes) +``` + +- [ ] **Step 2: 运行验证失败** — `pytest tests/api/test_main.py -v` → FAIL(module not found) + +- [ ] **Step 3: 实现 main.py** + +```python +# sanguo_api/main.py +"""容器 uvicorn 入口:读 config/backtest.yaml 构建 app,暴露模块级 `app`。""" +from __future__ import annotations +import os +from pathlib import Path +import yaml +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles +from starlette.responses import FileResponse + +from .app import create_app as _create_app + + +def _load_config(config_path: str) -> dict: + p = Path(config_path) + if not p.exists(): + return {} + with open(p, "r", encoding="utf-8") as f: + return yaml.safe_load(f) or {} + + +def build_app(config_path: str = "config/backtest.yaml", static_dir: str | None = None) -> FastAPI: + cfg = _load_config(config_path) + bt = cfg.get("backtest", {}) + auth = cfg.get("auth", {}) + pool = cfg.get("pool", {}) + db_path = bt.get("db_path", "/tmp/backtest_results.db") + file_dir = bt.get("file_dir", "/tmp/backtest_files") + auth_config = { + "username": auth.get("username", "admin"), + "password_hash": auth.get("password_hash", ""), + "jwt_secret": auth.get("jwt_secret", "change-me"), + "expire_minutes": auth.get("token_expire_minutes", 60), + } if auth else None + max_workers = pool.get("max_workers", bt.get("max_workers", 2)) + + app = _create_app(db_path=db_path, file_dir=file_dir, auth_config=auth_config, max_workers=max_workers) + + # SPA 静态挂载(history fallback)。根路由先注册,再 mount 兜底。 + if static_dir and os.path.isdir(static_dir): + index_html = os.path.join(static_dir, "index.html") + + @app.get("/", include_in_schema=False) + async def _spa_root(): + return FileResponse(index_html) + + app.mount("/", StaticFiles(directory=static_dir, html=True), name="spa") + + return app + + +_REPO = Path(__file__).resolve().parent.parent +app = build_app( + str(_REPO / "config" / "backtest.yaml"), + static_dir=os.environ.get("SPA_STATIC_DIR", str(_REPO / "frontend" / "dist")), +) +``` + +- [ ] **Step 4: 运行验证通过** — `pytest tests/api/test_main.py -v` → PASS +- [ ] **Step 5: 提交** — `git add sanguo_api/main.py tests/api/test_main.py && git commit -m "feat(api): sanguo_api.main 容器入口(读 backtest.yaml + 挂 SPA)"` + +--- + +## Task S0.2:切 docker/entrypoint.sh uvicorn 目标 + +**Files:** Modify: `docker/entrypoint.sh:282-283` + +- [ ] **Step 1: 改目标** — `sanguo_web.api:app` → `sanguo_api.main:app`(两行 log_info + exec) +- [ ] **Step 2: 本地冒烟** — `python -c "from sanguo_api.main import app; print([getattr(r,'path','') for r in app.routes][:5])"` 含 `/api/v1/auth/login` +- [ ] **Step 3: 提交** — `git commit -am "chore(deploy): 容器 uvicorn 切到 sanguo_api.main:app"` + +--- + +## Task S0.3:前端脚手架 + +**Files:** Create: `frontend/{package.json,vite.config.ts,tsconfig.json,index.html,src/main.ts,src/App.vue,src/env.d.ts}` + +- [ ] **Step 1: 初始化** — `npm create vite@latest frontend -- --template vue-ts && cd frontend && npm install && npm install element-plus echarts pinia vue-router axios && npm install -D vitest @vue/test-utils jsdom @types/node` +- [ ] **Step 2: vite.config.ts**(alias @→src;dev proxy /api、/ws → 192.168.2.154:8000;build outDir=dist;vitest jsdom) +- [ ] **Step 3: main.ts** 挂 Pinia + Router + ElementPlus;App.vue = `` +- [ ] **Step 4: `npm run build`** → `dist/index.html` 生成 +- [ ] **Step 5: `.gitignore`** 加 `frontend/node_modules/`、`frontend/dist/` +- [ ] **Step 6: 提交** — `git add frontend/ .gitignore && git commit -m "feat(frontend): Vue3+Vite+TS 脚手架"` + +--- + +## Task S0.4:前端 auth(client + store + 登录页 + 守卫) + +**Files:** Create: `src/api/client.ts`, `src/stores/auth.ts`, `src/views/Login.vue`, `src/router/index.ts`, `tests/auth.test.ts` + +- [ ] **Step 1: 写失败测试**(auth store setToken/logout/isAuthenticated,见 plan 源) +- [ ] **Step 2: 验证失败** → `npx vitest run tests/auth.test.ts` FAIL +- [ ] **Step 3: stores/auth.ts**(Pinia,token/username 持久化 localStorage;isAuthenticated getter;setToken/logout actions) +- [ ] **Step 4: api/client.ts**(axios baseURL=/api/v1;请求拦截附 Bearer;响应 401→logout+跳登录) +- [ ] **Step 5: router/index.ts**(路由表 + beforeEach 未登录跳 /login) +- [ ] **Step 6: Login.vue**(用户名+密码 → POST /auth/login → setToken → push '/';失败 ElMessage) +- [ ] **Step 7: 验证通过** → `npx vitest run` PASS +- [ ] **Step 8: 提交** — `git commit -am "feat(frontend): auth(JWT store + axios 拦截器 + 登录页 + 路由守卫)"` + +--- + +## Task S0.5:Layout shell(4 入口侧栏) + +**Files:** Create: `src/views/Layout.vue` + +- [ ] **Step 1: Layout.vue** — el-container + el-menu 侧栏 4 项(回测/投研 active;模拟/实盘 disabled"敬请期待");顶栏 用户名+登出 +- [ ] **Step 2: `npm run build`** 验证 +- [ ] **Step 3: 提交** — `git commit -am "feat(frontend): Layout shell(4 入口侧栏,模拟/实盘灰显)"` + +--- + +## Task S0.6:部署 S0 + 公网验证 + +- [ ] **Step 1: 本机 `cd frontend && npm run build`** +- [ ] **Step 2: 同步 NAS**(rsync frontend/ + ssh-exec 传 main.py/entrypoint.sh) +- [ ] **Step 3: `ssh sanguo-nas "$DOCKER restart sanguo_vnpy_v2"`** +- [ ] **Step 4: 公网验证** — `curl https://vnpy.mysanguo.top/` 返回 SPA index;`POST /api/v1/auth/login` admin/admin 返回 token +- [ ] **Step 5: 浏览器验收** — 登录 → 空壳控制台(侧栏 4 入口) + +--- + +# 切片 S1:回测核心(对齐 vnpy client 回测模块) + +## Task S1.1:修 result_id bug(阻塞所有结果查看) + +**Files:** Modify: `result_store.py`(加 id)、`runner.py:_on_done`(用 result.id)、`cta_engine.py`(save 传 file_dir);Test: `tests/backtest/test_result_store.py` + +**Interfaces — Produces:** `BacktestResult.id: int | None`;`save_result` 后 `result.id`=DB 行 id;`get_result` 正确 load_result(result.id);equity_curve 经 parquet 落盘读回 + +- [ ] **Step 1: 写失败测试**(save→设 r.id→load_result(r.id) 读回 statistics+equity_curve) +- [ ] **Step 2: 验证失败** → FAIL +- [ ] **Step 3: result_store.py** — dataclass 加 `id: Optional[int] = None`;`save_result` commit 后 `result.id = cur.lastrowid` +- [ ] **Step 4: runner._on_done** — `task.complete(result_id=result.id)` +- [ ] **Step 5: cta_engine** — save_result 传 file_dir(cfg 或 `os.path.dirname(db_path)` 兜底) +- [ ] **Step 6: 验证通过** → PASS +- [ ] **Step 7: 提交** — `git commit -am "fix(backtest): result_id 用 DB 行 id;equity_curve 落盘读回(修 get_result bug)"` + +--- + +## Task S1.2:cta_engine 构建 equity_curve + trades DataFrame + +**Files:** Modify: `cta_engine.py` + +**Interfaces — Produces:** `equity_curve`=DataFrame[date,balance](engine.get_all_daily_results);`trades`=DataFrame[datetime,direction,offset,price,volume,vt_symbol](engine.trades) + +- [ ] **Step 1: calculate_statistics 后构建 DataFrame**(try/except 兜底版本差异) +- [ ] **Step 2: result 用 equity_curve=equity_df, trades=trades_df(替换原 daily_results/None)** +- [ ] **Step 3: 容器 diag_cta.py 验证** result.equity_curve/trades 非空 + result.id 有值 +- [ ] **Step 4: 提交** — `git commit -am "feat(backtest): cta_engine 构建 equity_curve/trades 并落盘"` + +--- + +## Task S1.3:strategy_registry —— 枚举 vnpy_ctastrategy 策略 + +**Files:** Create: `sanguo_api/strategy_registry.py`;Test: `tests/api/test_strategy_registry.py` + +**Interfaces — Produces:** `list_strategies()->list[{name,class_name}]`;`strategy_params(name)->{parameters,defaults}`;`get_strategy_class(name)->type|None`;常量 `STRATEGY_NAMES` + +- [ ] **Step 1: 写失败测试**(list shape;params 含 parameters list) +- [ ] **Step 2: 验证失败** → FAIL +- [ ] **Step 3: 实现**(pkgutil 枚举 vnpy_ctastrategy.strategies;导入失败兜底 STRATEGY_NAMES=[DoubleMaStrategy,BollChannelStrategy,AtrRsiStrategy]) +- [ ] **Step 4: 验证通过** → PASS +- [ ] **Step 5: 提交** — `git commit -am "feat(api): strategy_registry 枚举策略与参数"` + +--- + +## Task S1.4:回测后端接口(strategy + equity/pnl/trades) + +**Files:** Modify: `routes.py`;Test: `tests/api/test_backtest_routes_switch.py` + +**Interfaces:** +- Consumes: orchestrator.get_result(id).equity_curve/trades;strategy_registry +- Produces: `GET /strategy/list`、`/strategy/{name}/params`、`/task/{id}/equity-curve`、`/task/{id}/daily-pnl`、`/task/{id}/trades` + +- [ ] **Step 1: 写失败测试**(FakeOrch.get_result 返回带 equity 的 BacktestResult;无 token 401;有 token 200 + records) +- [ ] **Step 2: 验证失败** → FAIL +- [ ] **Step 3: 加路由** + `_df_to_records(df)` 工具(DataFrame→list[dict],空安全);daily-pnl 由 balance.diff 计算 +- [ ] **Step 4: 验证通过** → PASS +- [ ] **Step 5: 提交** — `git commit -am "feat(api): 回测结果接口(strategy + equity-curve/daily-pnl/trades)"` + +--- + +## Task S1.5:K线接口 `/kline` + +**Files:** Create: `sanguo_api/kline.py`;Modify: `routes.py`;Test: `tests/api/test_kline.py` + +**Interfaces — Produces:** `load_kline(symbol,start,end,cfg=None)->list[{datetime,open,high,low,close,volume,vt_symbol}]`;`GET /kline?symbol&start&end` + +- [ ] **Step 1: 写失败测试**(mock read_db_daily 返回假 BarData → records) +- [ ] **Step 2: 验证失败** → FAIL +- [ ] **Step 3: 实现 kline.load_kline**(read_db_daily → list[dict])+ 路由 +- [ ] **Step 4: 验证通过** → PASS +- [ ] **Step 5: 提交** — `git commit -am "feat(api): K线接口 /kline"` + +--- + +## Task S1.6:前端 回测-新建页 + +**Files:** Create: `src/api/{strategy,backtest}.ts`, `src/views/backtest/New.vue` + +- [ ] **Step 1: api/strategy.ts**(getStrategies/getParams);**api/backtest.ts**(submitCta) +- [ ] **Step 2: New.vue** — 策略 el-select(onchange 拉参数渲染动态 el-form)+ symbol + el-date-picker 区间 + 提交 → push `/backtest/progress/:id` +- [ ] **Step 3: `npm run build`** 验证 +- [ ] **Step 4: 提交** — `git commit -am "feat(frontend): 回测-新建页"` + +--- + +## Task S1.7:前端 回测-进度页 + useTask + +**Files:** Create: `src/composables/useTask.ts`, `src/views/backtest/Progress.vue` + +- [ ] **Step 1: useTask.ts** — 轮询 GET /task/{id}(2s) + WS /ws/task/{id}?token=;导出 {status,stage};done→resolve +- [ ] **Step 2: Progress.vue** — 状态徽标 + 阶段文字 + 进度条;done→push result;failed→ElMessage error_msg +- [ ] **Step 3: build** 验证 +- [ ] **Step 4: 提交** — `git commit -am "feat(frontend): 回测-进度页(WS 实时阶段)"` + +--- + +## Task S1.8:前端 回测-结果页(ECharts,vnpy client 对齐) + +**Files:** Create: `src/components/charts/{EquityChart,DailyPnlChart,KlineChart}.vue`, `src/components/TradesTable.vue`, `src/views/backtest/Result.vue` + +- [ ] **Step 1: EquityChart**(折线 date×balance);**DailyPnlChart**(柱状红绿);**KlineChart**(candlestick + markPoint 成交买卖点) +- [ ] **Step 2: TradesTable.vue**(el-table) +- [ ] **Step 3: Result.vue** — 并行拉 result/equity-curve/daily-pnl/trades/kline → 统计卡片 + 图表/表布局 +- [ ] **Step 4: build** 验证 +- [ ] **Step 5: 提交** — `git commit -am "feat(frontend): 回测-结果页(对齐 vnpy client)"` + +--- + +## Task S1.9:S1 部署 + 端到端冒烟 + +- [ ] **Step 1: scripts/smoke_phase3b.py** — 登录→POST /backtest/cta(DoubleMaStrategy,600000,2024-01-01..06-30)→轮询 done→校验 equity-curve/daily-pnl/trades/kline 非空 +- [ ] **Step 2: 容器跑冒烟** `ssh sanguo-nas "$DOCKER exec sanguo_vnpy_v2 python /app/scripts/smoke_phase3b.py"` +- [ ] **Step 3: 同步前端 dist + 后端 → restart** +- [ ] **Step 4: 公网验收** — 浏览器跑 DoubleMaStrategy 看完整结果页 +- [ ] **Step 5: 提交** — `git commit -am "test(phase3b): S1 端到端冒烟"` + +--- + +# 切片 S2:投研核心 + +## Task S2.1:factor 列表 + ic-summary + report 接口(含 factor 结果持久化) + +**Files:** Modify: `routes.py`, `sanguo_factor/analyzer.py`, `sanguo_orchestrator/runner.py`;Create: `sanguo_api/factor_registry.py`;Test: `tests/api/test_factor_routes.py` + +**Interfaces — Produces:** `GET /factor/list`;`GET /task/{id}/ic-summary`;`GET /task/{id}/report/{factor}`(FileResponse HTML) + +> ⚠️ **实现注意(factor 结果持久化)**:factor worker 返回 `FactorReport`(非 BacktestResult),当前 orchestrator 对其无持久化。S2.1 补:analyzer 把 `{ic_summary, report_paths}` 写 `output_dir/_summary.json`;orchestrator 在 `_pending[task_id]` 记 output_dir(submit_factor 已有);`/ic-summary` 与 `/report/{factor}` 从 output_dir 读。task_id 需稳定(当前 `factor_{id(factor_names)}` 不稳定,改含 symbols+时间戳哈希)。 + +- [ ] **Step 1: 写失败测试**(FakeOrch 暴露 get_factor_summary(tid)->dict;测 /factor/list、/ic-summary、/report 非空) +- [ ] **Step 2: 验证失败** → FAIL +- [ ] **Step 3: factor_registry.py**(枚举 sanguo_factor.registry);analyzer 落 summary.json;runner factor task_id 稳定化 + 暴露 output_dir +- [ ] **Step 4: 加路由** /factor/list、/task/{id}/ic-summary、/task/{id}/report/{factor} +- [ ] **Step 5: 验证通过** → PASS +- [ ] **Step 6: 提交** — `git commit -am "feat(api): 投研接口(factor list + ic-summary + tears 报告服务)"` + +--- + +## Task S2.2:前端 投研-新建 + 结果页 + +**Files:** Create: `src/api/factor.ts`, `src/views/factor/{New,Result}.vue` + +- [ ] **Step 1: api/factor.ts**(getFactors/submitFactor/getIcSummary/reportUrl) +- [ ] **Step 2: New.vue** — 因子多选 + 多标的 tag 输入 + 日期 → 提交 → 进度(useTask)→ 结果 +- [ ] **Step 3: Result.vue** — IC 表(period × mean/std/icir/t_stat/count)+ tears iframe +- [ ] **Step 4: build** 验证 +- [ ] **Step 5: 提交** — `git commit -am "feat(frontend): 投研-新建/结果页(IC 表 + tears)"` + +## Task S2.3:S2 部署 + 冒烟 +- [ ] 冒烟(ma5,[600000,000001,300750])→ ic-summary/report 非空 → 公网验收 → 提交 + +--- + +# 切片 S3:优化 + 收尾 + +## Task S3.1:回测可配置费率/滑点/资金 +- [ ] schemas CtaBacktestRequest 加 `rate: float=0.001, slippage: float=0, capital: float=1_000_000`;routes 透传;cta_engine 用参数(替换硬编码);测试;提交 + +## Task S3.2:优化结果 + 任务列表接口 +- [ ] `GET /task/{id}/optimization-results`({results:[{params,statistics}]});`GET /task?type=&status=`(list_results);测试;提交 + +## Task S3.3:前端 优化页(热力图)+ 历史页 +- [ ] Optimize.vue(参数网格表单 + ECharts heatmap);History.vue(任务表 + 回看);build;提交 + +## Task S3.4:S3 部署 + 全量冒烟 + 收尾 +- [ ] 全链路冒烟(回测+优化+因子)→ 公网验收 → 更新 nas-deploy-plan.md(uvicorn 目标)→ 最终提交 → finishing-a-development-branch + +--- + +## Self-Review(计划自检) + +1. **Spec 覆盖**:§6 页面 → S0.5/S1.6-1.8/S2.2/S3.3 ✓;§8.2 接口 → S1.3-1.5/S2.1/S3.2 ✓;§10 切片验收 → 每片末 ✓;result_id bug(实现发现)→ S1.1 ✓;factor 持久化缺口(实现发现)→ S2.1 ✓。 +2. **占位符**:S3 为切片级任务(按 writing-plans scope-check,每片可独立成 plan,到达时按 S0/S1 粒度细化)。无 TBD/TODO 散落。 +3. **类型一致**:`build_app(config_path, static_dir)`、`list_strategies()`、`strategy_params(name)`、`load_kline(...)`、`_df_to_records` 跨任务一致 ✓。 +4. **兜底**:vnpy_ctastrategy 本地不可导入(STRATEGY_NAMES);read_db_daily cfg(默认 None);rsync 不稳(ssh-exec)。 + +## Execution Handoff + +用户已睡 + /goal 自主完成 → **Inline Execution(superpowers:executing-plans)**:本会话按任务顺序执行,后端 TDD(先红后绿)、前端 build 验证、切片末部署 + 公网验收,频繁提交,不阻塞等用户。