feat(data): 方案A 数据层落地(spec §14)— DB唯一表+治幸存者偏差+权威源

spec §14 方案A 数据层迁移完成 + E2E 验证(read_db_daily: 在市/退市治偏差/ETF 全OK):
- dbbardata('d') 1826万含退市(治回测幸存者偏差, INSERT OR REPLACE staging迁移, WHERE OHLC NOT NULL+COALESCE)
- constituent_unified 7110行/9指数(300/500/50 baostock全集 + 深证4指 akshare cni union + 中证1000/2000 snapshot)
- pe/pb 不进DB -> valuation_baostock/<year>.parquet 按年宽表(2003-2026)
- 废弃 daily_baostock_full/bs_index_constituent(rename _old 保留); 旧4 schtask disabled
- 新 schtask sanguo-bs-eod 18:05(baostock个股日线+15min+拆pe/pb DAILY_LIMIT 48000) + sanguo-xt-eod 18:40(ETF/基金xtata)
- 权威源: baostock个股日线+估值+15min+复权+300/500/50 / xtata ETF+基金+当天实时 / akshare三表+事件+深证中证成份股
- 全程备份+staging+_old保留可回滚; 脚本 audit/probe/migrate/merge/cleanup/fix_config/verify/bs_eod/xt_eod/wrapper/register_schtasks
- 待办(spec §6 使用层): LocalParquetProvider 接 constituent_unified+valuation_baostock + 实时拼接
This commit is contained in:
2026-07-23 07:20:34 +08:00
parent b270faf4b9
commit c2a89d01a4
27 changed files with 2062 additions and 0 deletions
@@ -0,0 +1,106 @@
# 数据架构方案A迁移 + schtask 改造 实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans。Steps use checkbox。
**Goal:** 落地 spec §14 方案A定稿 — DB 唯一表、每类数据唯一权威源、4 个新 schtask、迁移 5 单元,全程备份+staging+可回滚+审计。
**Architecture:** 以本地 DB 迁移为主(`daily_baostock_full`→dbbardata/parquet,无网络),schtask 改造(废弃旧 4 个新建 4 个)。每单元独立可回滚,按风险升序。
**Tech Stack:** Python3.10 / sqlite3(WAL) / pandas parquet / Windows schtasks / baostock+xtata+akshare
## Global Constraints
- baostock:单进程单登录,`DAILY_LIMIT=48000`,sleep 限速,login 探针 graceful skip,直连不走代理(unset proxy)
- xtata:单进程 download 不并发,无限流
- akshare:interval 4s 单线程,防东财封 IP
- 每迁移单元前:`sqlite3 .backup` 全库 + rsync 到 NAS `/volume1/stock/backup/` + WAL checkpoint
- 每单元:staging 隔离 → 验证探针 → 用户确认合并 → 旧 rename `_old` 保留 7 天
- 全程 nohup + 审计日志 `data/migration_logs/<unit>_<ts>.log`
- 不破坏 vnpy 回测:dbbardata schema 不动(只灌数据),`dbbardata` 12 列保持
## 文件结构
- 迁移脚本:`scripts/data_platform/migrate_*.py`(每单元一个)
- 验证脚本:`scripts/data_platform/verify_*.py`
- schtask wrapper:`scripts/data_platform/*_wrapper.ps1`
- 审计日志:`data/migration_logs/`
---
## 前置 Task 0:全库备份(所有单元前必做)
**Files:** `scripts/data_platform/backup_db.py`(新建,可复用)
- [ ] 写脚本:`sqlite3 .backup``quant_trading.db.bak_<YYYYMMDD>`(在线一致);WAL checkpoint;rsync 到 NAS
- [ ] 执行
- [ ] **verify**:`.bak` 存在 + 大小≈28GB + `PRAGMA integrity_check` ok
---
## 单元 1:存量垃圾清理(零风险)
**Files:** `scripts/data_platform/cleanup_staging.py`(新建)
- [ ] 写脚本:`--dry-run` 先列清单 → 删 `_staging_xtdata/`(14万)、`_xtdata.tar`(1.4G);移 `cta_*/dbg_*/smoke_*/trace_*``backtest_files/`
- [ ] dry-run 输出清单给用户确认
- [ ] 执行删除/移动
- [ ] **verify**:`data/` 根目录无散落 json/log;`backtest_files/` 收纳;`du -sh data/` 体积下降
- [ ] **回滚**:staging 可由 `build_daily_from_xtdata` 重建(已合并到 qfq/raw)
---
## 单元 2:config 统一 VPS 路径
**Files:** `config/data_platform.yaml`(VPS 实例)
- [ ] 核实 VPS 实际 config 路径(当前仓库版指 NAS /volume1,是容器版遗留)
- [ ] `daily_dir/raw_dir/qfq_dir/minute_15_dir``C:\sanguo_vnpy_v2\data\...`
- [ ] `daily_dir` 统一指 qfq(消除 `daily/` vs `qfq/` 分叉,`daily/`68文件归档)
- [ ] NAS config 保留 + 注释"备份用"
- [ ] **verify**:`datareader.read_parquet_daily` 抽样能读 + LocalParquetProvider 抽样
- [ ] **回滚**:yaml 改回
---
## 单元 3:成份股合并 → `constituent_unified`
**Files:** `scripts/data_platform/migrate_constituent.py` + `verify_constituent.py`
**Interfaces:**`bs_index_constituent`(baostock 300/500/50)+ `data/index_const_hist/*_union.parquet`(akshare cni 深证);写 `constituent_unified(date,index_code,code,code_name,source)`
- [ ] 写迁移脚本:按指数代码去重(300/500/50=baostock;深证 399xxx=akshare cni union;新浪 300/50 作校验丢弃);schema 映射 INSERT
- [ ] staging:先写 `constituent_unified_staging`
- [ ] **verify**:行数 / 指数覆盖 / 抽样某指数某日成份集 vs 源一致 / 无同指数同日重复
- [ ] 合并:rename staging → `constituent_unified`;`bs_index_constituent``_old`
- [ ] 7 天后删 `_old`
- [ ] **回滚**:rename `bs_index_constituent_old` 回来
---
## 单元 4:`daily_baostock_full` 拆分(本地 DB 迁移,无网络)
**Files:** `scripts/data_platform/migrate_daily_baostock.py` + `verify_daily_migration.py`
**Interfaces:**`daily_baostock_full`(含退市);写 `dbbardata('d')`(OHLCV 12 列)+ `data/valuation_baostock/<year>.parquet`
- [ ] 写迁移脚本:
- OHLCV:`daily_baostock_full` → dbbardata INSERT OR REPLACE(interval='d',exchange SH/SZ→SSE/SZSE,datetime=date)。含退市(治偏差)。ETF 不碰(已在 dbbardata)
- pe/pb:按年 group → `valuation_baostock/<year>.parquet` 宽表
- [ ] staging:先写 `dbbardata_staging_daily` 表 + parquet staging 目录,不动 dbbardata
- [ ] **verify**:
- 退市股(000005 等)在 dbbardata('d') 有了(治偏差验证)
- 在市股(600519)日线行数 / 抽样价格 vs daily_baostock_full 一致
- pe/pb parquet 按年覆盖 + 抽样值合理
- dbbardata 总行数变化合理(+退市日线)
- [ ] 合并:staging → dbbardata;`daily_baostock_full``_old`;valuation parquet → 正式目录
- [ ] 7 天后删 `_old`
- [ ] **回滚**:`daily_baostock_full_old` 还原 + dbbardata 从 `.bak` 恢复
---
## 单元 5:schtask 改造(废弃旧 4 个,新建 4 个)
**Files:** `scripts/data_platform/bs_eod.py`(日线+15min+pe/pb 拆)+ `xt_eod.py`(ETF+实时)+ `*_wrapper.ps1`
- [ ]`bs_eod.py`:基于 `daily_update_static.py` 扩展,+15min 增量,+pe/pb 拆 parquet;落 dbbardata('d'/'15m');`DAILY_LIMIT=48000`
- [ ]`xt_eod.py`:基于 `daily_update_xtdata.py`,universe 收窄 ETF/基金 + 个股当天实时;落 dbbardata('d')
- [ ] **verify**:`--limit 10` 小样本跑通 + 数据到当天
- [ ] 部署 schtask:废弃 `sanguo-daily-update`/`sanguo-bs-daily-increment`/`sanguo-index-hist`;新建 `sanguo-bs-eod`(18:05)/`sanguo-xt-eod`(18:40);`sanguo-bs-akshare` 调到 19:00;`sanguo-index`(月度 19:50)
- [ ] **verify**:`schtasks /query` + 首日运行结果码 + 数据抽查到当天
- [ ] **回滚**:重新注册旧 schtask
---
## 收尾:E2E 验证
- [ ] 回测 all_weather 一轮(读 dbbardata 日线含退市 + valuation parquet + constituent_unified)无回归
- [ ] LocalParquetProvider 接 constituent_unified + valuation_baostock 单测
- [ ] 更新 memory:`data-fusion-design-finalized`(标方案A落地)+ 新建 `data-arch-migration-done`
## 执行节奏
- 每单元独立提交 + 用户 review staging 再合并(单元 4/5 关键)
- 全程 VPS nohup 跑(Mac Mini 防休眠 caffeinate,长迁移)
- 顺序:0 → 1 → 2 → 3 → 4 → 5 → 收尾(严格风险升序)
@@ -230,6 +230,68 @@ get_delisted_kline(...) # 退市股 K 线
---
## 14. 方案 A 定稿(2026-07-22 讨论收敛 — 本节为最新,覆盖前文相关决策)
### 14.1 背景
P0 补全 + schtask 部署完成后,审计(`audit_data_layout.py` + `dbbardata_probe.py` 实证)发现「同类多源、多份落地、DB 多表」混乱。讨论收敛为方案 A:**每类数据唯一权威源(允许互补 fallback,禁止并行)、DB 表唯一、估值/三表/事件不进 DB、DB 只放高频随机读取的 K 线+成份股+复权**。
### 14.2 权威源最终分工
| 数据类 | 权威源 | 备注 |
|---|---|---|
| 个股日线 OHLCV(含退市) | **baostock** | raw 真实价 → dbbardata('d') |
| ETF/基金日线 | **xtata** | baostock 盲区(只取 type=1) |
| 个股当天实时(盘后 baostock 未更窗口/盘中) | **xtata** | 拼 baostock 历史,同为 raw |
| 15min | **baostock** | 历史+增量同源 |
| 5min/1min | baostock(暂停) | 战略占位,size 扩容续 |
| 估值 pe/pb/ps/pcf/turn/pctChg/isST | **baostock** | 日线18字段拆出 |
| 基本面三表 | **akshare** | |
| 成份股 300/500/50 | **baostock**(历史) | |
| 成份股 深证 399xxx | **akshare cni**(历史 union) | |
| 成份股 中证1000/2000 | akshare csindex(当前快照) | 历史不可补=永久 gap |
| 复权因子 | **baostock** | 全链路复权基准 |
| 事件类(龙虎榜/大宗/北向/两融/解禁/预告) | **akshare** | |
三大源:xtata(ETF+实时) / baostock(个股日线+估值+15min+复权+300.500.50) / akshare(三表+事件+深证中证成份股)。同类不并行,按标的/字段互补。
### 14.3 DB 边界(只放)
- `dbbardata`:日线('d')+15min('15m')+5min('5m'占位)— **唯一行情表**
- `constituent_unified`:成份股合并唯一表(date,index_code,code,code_name,source)
- `bs_adjust_factor`:复权
- `backtest_stats`:回测(已有)
- **不进 DB**:基本面三表、估值 pe/pb、事件类、回测明细(独立 db)
- **废弃** `daily_baostock_full`(OHLCV→dbbardata,pe/pb→parquet)、`bs_index_constituent`(合入 constituent_unified)
### 14.4 pe/pb 落地:parquet 按年宽表
`data/valuation_baostock/<year>.parquet`,列 `date,symbol,pe_ttm,pb_mrq,ps_ttm,pcf_ncf_ttm,turn,pct_chg,is_st`。选股排序(全市场某日)列存宽表秒级。baostock 日线增量同源拆出。
### 14.5 增量 schtask(4 个)
| schtask | 时间 | 源 | 内容 | 落点 | 预算 |
|---|---|---|---|---|---|
| sanguo-bs-eod | 18:05 | baostock | 个股日线+15min 增量+拆 pe/pb | dbbardata('d'/'15m')+valuation_baostock/ | ~11000/48000 |
| sanguo-xt-eod | 18:40 | xtata | ETF/基金日线+个股当天实时补 | dbbardata('d') | 无限流 |
| sanguo-bs-akshare | 19:00 | akshare | 三表+事件类 | parquet | 限频单线程 |
| sanguo-index | 月度 19:50 | baostock+akshare | 成份股合并 | constituent_unified | 小 |
baostock 单进程单登录,`DAILY_LIMIT=48000`,sleep 限速,login 探针 graceful skip,直连不走代理。5min/1min 不设 schtask。
### 14.6 dbbardata 补退市(本次审计发现,顺带治回测幸存者偏差)
实证(`dbbardata_probe`):退市股(000005/000023/600811)在 dbbardata **只有 15m,无日线** → 回测读 dbbardata 日线天然幸存者偏差。迁移:`daily_baostock_full` OHLCV 全量(含退市)→ dbbardata('d') INSERT OR REPLACE(本地 DB 迁移,无网络)。ETF 已在 dbbardata(不碰)。
### 14.7 复权统一
全链路 raw 真实价存储(dbbardata 存 raw),前复权由消费端按 `bs_adjust_factor`(baostock)统一算。xtata 当天实时同为 raw,可拼接。禁止混 xtata dividend_type=front。
### 14.8 数据迁移(5 单元,风险升序,备份+staging+可回滚+审计日志)
1. 存量垃圾清理(`_staging_xtdata` 14万文件 / `_xtdata.tar` 1.4G / 归拢 cta_* dbg_* → backtest_files/)
2. config 统一 VPS 路径(NAS 仅备份)
3. 成份股合并(bs_index_constituent + index_const_hist union → constituent_unified,按指数代码去重,新浪300/50丢弃)
4. daily_baostock_full 拆分(OHLCV→dbbardata('d') 含退市;pe/pb→valuation_baostock/<year>.parquet;旧表 rename _old)
5. dbbardata 个股日线切 baostock 源(单元4已覆盖:daily_baostock_full 含全量,在市股 REPLACE 覆盖 xtata,数值一致)
每单元:全库 `sqlite3 .backup` + rsync NAS → 脚本写 staging(新表/新目录)→ 验证探针(行数/distinct symbol/抽样价格/成功率)→ 用户确认合并 → 旧数据 rename _old 保留 7 天。全程 nohup + 审计日志 `data/migration_logs/`
### 14.9 config 清理
VPS config 的 daily_dir/raw_dir/qfq_dir/minute_15_dir 改 `C:\sanguo_vnpy_v2\data\...`(当前 yaml 指 NAS /volume1,容器版遗留)。NAS config 保留作备份。read_parquet_daily 的 daily_dir 统一指向 qfq(消除 daily/ vs qfq/ 分叉)。
## 参考(调查来源)
- xtdata 官方:https://dict.thinktrader.net/nativeApi/xtdata.html