# A 股多数据源融合层设计 > 日期:2026-07-21 | 基于 brainstorming + 4 源全能力调查(akshare / baostock / miniQMT / csindex) > 状态:设计草案,待用户评审 → writing-plans --- ## 1. 背景与痛点 多数据源(akshare / baostock / xtdata / miniQMT)各不全,整合时格式有偏差: - **symbol 格式**:`600519` vs `sh.600000` vs `600519.SH` - **exchange 命名**:`SSE` vs `SH` - **复权口径**:raw vs qfq - **volume 单位**:xtdata÷100 vs 原值 - **价格源间漂移**:同股同日不同源 close 微差 ### 用户约束(明确) - ❌ 不要查询时网络源切换(源变化/限流不可控) - ✅ VPS 本地一份稳定数据,日常只读本地 - ✅ 网络源只用于「采集时拼凑完整本地」 - ✅ 使用层无感(本地缺才网络兜底,罕见) - ✅ 优先 miniQMT + baostock --- ## 2. 设计原则(三层) | 层 | 职责 | 原则 | |---|---|---| | **采集层** | 多网络源 → 拼凑完整本地 | 各源 adapter + 定时 schtask;源不可控隔离在采集(失败重试,不影响使用层) | | **数据层** | 整理(重叠定权威,特定保留) | **不强合物理表**(vnpy 回归风险);每类定权威源(优先 baostock+miniqmt) | | **使用层** | `LocalUnifiedProvider` 逻辑融合 | 读权威表 + 归一化 + 本地缺网络兜底;策略无感 | **核心**:网络源不稳的风险只影响采集层(定时跑、可重试),使用层永远读本地 —— 风险隔离。 --- ## 3. 数据源全能力盘点(综合调查) ### 3.1 已下(稳定源) | 表/源 | 内容 | 范围 | 增量 | |---|---|---|---| | `dbbardata` | 日线+5m/15m(xtdata/akshare/baostock) | 日线2010+/分钟2020+ | ✅ 16:30 | | `daily_baostock_full` | 日线 18 字段(pe/pb/turn/pctChg) | 1990–2026 | 待 #7 | | `bs_index_constituent` | 成份股 300/500/50(含退市) | 2006+ | 待增量 | | `bs_adjust_factor` | 复权因子 | 全史 | 待增量 | | akshare 静态表 | 三表/估值/北向/龙虎榜/融资融券/股本/解禁/业绩预告 | - | 部分增量 | | miniQMT xtdata | 日线+5m/15m 全周期(实时 T+0,零漂移) | 16年 | 按需 | | miniQMT PershareIndex | ROE/毛利率/EPS | 季频 | 按需 | | parquet | data/raw、daily_baostock、minute_5/15、static/index_const、qfq+raw ETF(5只) | - | 部分 | ### 3.2 重叠(5 处) 1. 日线 OHLCV:dbbardata ∩ daily_baostock_full ∩ parquet raw(三处) 2. 估值 PE/PB:akshare valuation ∩ daily_baostock_full(peTTM/pbMRQ) 3. 15min:dbbardata ∩ parquet minute_15 4. 基本面:akshare 三表 ∩ miniQMT PershareIndex ∩ baostock 季频 5. 成份股:bs_index_constituent ∩ parquet index_const ### 3.3 新发现缺口(本次调查) - **ETF 全市场日线**(现仅 5 只,策略资产类别缺口) - **深证/中证历史成份股**(治幸存者偏差,当前仅最新快照 = latent bug) - **退市股 K 线**(baostock 有,从未提取;反幸存者偏差核心) - 申万行业 SW1/2/3 + 历史变动 - 龙虎榜 / 合约信息(涨跌停/ST)/ 可转债 / 研报一致预期 / 股东户数 / 除权明细 --- ## 4. 权威源地图(数据层整理) | 数据类 | 权威源 | 物理存储 | 重叠处理 / 备注 | |---|---|---|---| | 日线 OHLCV(个股,历史) | **baostock** | `daily_baostock_full` | dbbardata 日线保留(vnpy 回测硬依赖);parquet raw 冗余 | | 日线(盘中实时) | **miniQMT xtdata** | xtdata API | 独有(T+0 实时) | | **日线 ETF(全市场)** | **miniQMT xtdata** | parquet/dbbardata | universe 加 `沪深ETF∪沪深基金`,`dividend_type='front'` 自动复权 | | 估值 PE/PB/turn | **baostock** | `daily_baostock_full` | akshare valuation 兜底/校验 | | 基本面指标 ROE/毛利率 | **miniQMT PershareIndex** | miniQMT API | akshare 三表补原始报表 | | 基本面三表(原始) | akshare | akshare 表 | miniQMT Balance/Income/CashFlow + baostock 季频交叉 | | 15min | **baostock** | `dbbardata` | parquet minute_15 冗余可清 | | 成份股 300/500/50(含退市) | **baostock** | `bs_index_constituent` | `query_*_stocks(date)` 任意时点 | | **成份股 深证/国证(399xxx)** | **akshare(国证源)** | parquet | `index_detail_hist_cni` + `index_detail_hist_adjust_cni` | | **成份股 中证1000/2000(000852/932000)** | **新浪** | parquet | `vII_HistoryComponent`(gb2312,含退市) | | **退市股 K 线** | **baostock** | `daily_baostock_full` | `query_all_stock` status=0 + `query_stock_basic` 退市日期 + 逐只 K | | 复权因子 | **baostock** | `bs_adjust_factor` | 独有 | | 申万行业 SW1/2/3 | **miniQMT xtdata** | parquet | `get_sector_list` + `get_stock_list_in_sector`;akshare 补历史变动 | | 龙虎榜 | **miniQMT xtdata** | parquet | `get_longhubang`;akshare 兜底 | | 合约信息(涨跌停/ST/上市日) | **miniQMT xtdata** | parquet | `get_instrument_detail` 全 A 一入库 | | 可转债 | akshare | parquet | `bond_zh_hs_cov_min` + `bond_cb_adj_logs_jsl`(转股价) | | 研报/一致预期 EPS | akshare | parquet | `stock_research_info_em` | | 股东户数 | miniQMT/akshare | parquet | `Holdernum` 表 / `stock_zh_a_gdhs_detail` | | 除权明细 | miniQMT xtdata | parquet | `get_divid_factors` | | 龙虎榜/北向/融资融券/解禁 | akshare | akshare 表 | 独有(保留) | --- ## 5. 归一化规则 | 维度 | 统一标准 | 源映射 | |---|---|---| | symbol | `600519.SH`(数字+交易所后缀) | baostock `sh.600000`→`600000.SH`;dbbardata 纯数字+exchange 字段 | | exchange | `SH`/`SZ` | dbbardata `SSE`/`SZSE` → `SH`/`SZ` | | 日期 | ISO `2026-07-21` | 各源统一 | | 复权 | raw 存储 + `factor` 字段(QLib:`factor=adj/raw`) | 查询时按需 qfq(`$close/$factor`);治 raw/qfq 冲突 | | volume | 原值(股) | xtdata ÷100 还原 | | 停牌 | OHLCV 全 NaN | QLib 约定 | | 溯源 | `source` 字段 | 每行标来源 + 主源/补丁标记 | --- ## 6. 使用层:`LocalUnifiedProvider`(逻辑融合) **接口**: ```python get_daily(symbol, start, end, adjust='raw') # 个股日线 get_etf_daily(symbol, ...) # ETF 日线 get_fundamentals(symbol, fields, date) # 财务指标/三表 get_constituent(index, date) # 成份股(含历史,治幸存者偏差) get_industry(symbol, date) # 申万行业(含历史变动) get_longhubang(symbol, start, end) # 龙虎榜 get_instrument_detail(symbol) # 涨跌停/ST get_delisted_kline(...) # 退市股 K 线 ``` **职责**: - 按数据类路由到权威表(§4 地图) - 归一化(§5 规则):源格式 → 统一 vt_symbol/exchange/复权/单位 - 本地缺 → `network_fetcher` 透明兜底(罕见,如新股未及采集)→ 写本地 → 返回 - `source` 字段溯源;可选多源交叉校验 - 使用层 API 不变,不知数据来自哪个源 **实现**:扩展现有 `sanguo_portfolio/providers/` 的 `DataProvider` 接口(BaostockProvider/LocalParquetProvider 已有)。 --- ## 7. 增量 schtask 清单 | schtask | 数据 | 源 | 时间 | |---|---|---|---| | `sanguo-daily-update`(已有) | 日线+分钟→dbbardata | xtdata | 16:30 | | `sanguo-bs-daily-increment`(#7 待建) | 日线→daily_baostock_full | baostock | 17:00 | | `sanguo-etf-daily-increment`(新) | ETF 全市场日线 | xtdata(universe 沪深ETF) | 17:30 | | `sanguo-akshare-static-increment`(新) | 估值/龙虎榜/三表/北向 | akshare | 18:00 | | `sanguo-index-hist`(新,半年度) | 历史成份股调样 | akshare 国证 + 新浪 | 调样后(6/12 月) | | `sanguo-delisted`(新,月度) | 退市股列表+K 线 | baostock | 月初 | **query 预算守 48000/天/IP**(baostock 硬限):各 baostock schtask 错开 + 日计数器。 --- ## 8. 分阶段实现 ### P0 — 治幸存者偏差 + 策略核心缺口 1. **历史成份股**:深证/国证(`index_detail_hist_cni`)+ 中证1000/2000(新浪 `vII_HistoryComponent`)+ 300/500/50(baostock 已有) 2. **ETF 全市场日线**:xtdata universe 改 `沪深A股∪沪深ETF∪沪深基金` + `dividend_type='front'`(改 `build_daily_from_xtdata.py:40` + `daily_update_xtdata.py:114`) 3. **退市股 K 线**:baostock `query_all_stock` 筛 status=0 + `query_stock_basic` 退市日期 + 逐只 K → `daily_baostock_full` ### P1 — 策略增强 4. 申万行业 SW1/2/3(xtdata `get_sector_list`)+ 历史变动 5. 龙虎榜(xtdata `get_longhubang`) 6. 合约信息涨跌停/ST(xtdata `get_instrument_detail`)全 A 入库 7. 可转债(akshare `bond_zh_hs_cov_min` + 转股价调整) 8. 研报/一致预期 EPS(akshare `stock_research_info_em`) ### P2 — 按需 9. 股东户数 / 除权明细 / 业绩快报 / 大宗交易 / 宏观 / 期货 ### 融合层(贯穿) 10. 归一化库(vt_symbol/exchange/factor/volume 映射) 11. `LocalUnifiedProvider`(读权威表 + 归一化 + 网络兜底) 12. 完整度监控报表(每类覆盖率/缺口,源退化早发现) --- ## 9. 陷阱清单(实证) - `ak.index_stock_hist` **已下线**(akshare 1.10.37,2024 初)—— 别抄 2022-23 旧博客 - `ak.index_stock_cons` 的"纳入日期"字段有迷惑性 —— 只是当前 300 只各自最初纳入日,不含被剔除,治不了幸存者偏差 - csindex.com.cn 是 Vue SPA —— `requests.get` 拿空壳,官网只当前 Excel 无历史 - 新浪 `fund_etf_hist_sina` **不复权** —— 不适合回测;ETF 复权走 xtdata `dividend_type='front'` - 东财 `fund_etf_hist_em` **封 IP** —— 单线程限速或避用 - baostock `query_all_stock` 不列退市日期 —— 配合 `query_stock_basic` - 申万历史板块有变更 —— 回测用当时分类 - `ak.index_detail_cni`(非 hist 版)2025-11-25 起只近期 —— 必须用 hist 版 --- ## 10. YAGNI(不做) - ❌ 不强合物理表(冲突解决/历史一致性/vnpy 回归风险,代价大) - ❌ 不引 QLib/OpenBB 框架(几百 MB,只摘模式:factor/source/归一) - ❌ CS 截面归一(中期按需,先解决不全+格式) - ❌ 实时 tick/盘口(非日终策略才需) --- ## 11. 风险与对策 | 风险 | 对策 | |---|---| | vnpy 回测硬读 dbbardata | 不动 dbbardata,provider 层 SSE↔SH 映射 | | baostock query 预算 48000/天 | 增量 schtask 错开 + 日计数器(#7+day2b 同天 44296<48000) | | 东财封 IP | 避用东财,优先 baostock/xtdata/新浪/国证 | | 接口下线(如 index_stock_hist) | 调查实证,不抄旧文 | | 数据源漂移/幽灵尖峰 | source 溯源 + 涨跌停/量异常校验 | | 历史成份股缺口致回测幸存者偏差 | P0 优先补(深证+中证1000/2000+退市) | --- ## 12. 实现路径(writing-plans 拆) - **Phase 1(P0 数据补全)**:历史成份股 + ETF 全市场 + 退市 K 线(3 个采集脚本 + 灌库) - **Phase 2(融合层)**:归一化库 + `LocalUnifiedProvider`(读现有+新表) - **Phase 3(P1 增强)**:板块/龙虎榜/合约/可转债/研报 - **Phase 4(运维)**:增量 schtask 全套 + 完整度监控 --- ## 13. 开放问题与默认决策(自行决策,你可推翻) | # | 问题 | 默认(我定) | 备选 | 理由 | |---|---|---|---|---| | 1 | exchange 统一格式 | **SH/SZ**(provider 映射 dbbardata SSE→SH) | 保留 SSE/SZSE | baostock/xtdata 都用 SH/SZ,主流;vnpy SSE 在 provider 层映射 | | 2 | P0 三项优先级 | **全做**(历史成份股 + ETF + 退市 K 线) | 先 ETF(策略即用) | 三项都治幸存者偏差/核心缺口,并行不冲突 | | 3 | ETF 复权方式 | **xtdata `dividend_type='front'`**(前复权) | raw + factor(精确还原) | 前复权够策略用;raw+factor 中期按需 | | 4 | 退市股 K 线范围 | **近 5 年退市**(守 baostock 48000/天预算) | 全退市(几千只,慢) | 近 5 年覆盖绝大多数回测;全量可后补 | | 5 | `LocalUnifiedProvider` 接口 | §6 签名(get_daily/get_etf/get_fundamentals/get_constituent/get_industry/...) | 精简 | 覆盖全天候 + CTA 需求 | | 6 | 增量 schtask 时间 | 17:00(baostock 日线)/ 17:30(ETF)/ 18:00(akshare 静态) | 调整 | 错开 sanguo-daily-update 16:30 + day2b 02:00 | | 7 | 物理表 | **不新建统一表**,provider 读现有(dbbardata/daily_baostock_full/各 parquet) | 建 daily_unified | 避免 vnpy 回归 + 数据迁移(§10 YAGNI) | **默认推进路径**:按以上默认 → spec 定稿 → 转 writing-plans(P0 拆 3 个采集脚本实现计划)。你审 spec 时可推翻任一项,我改。 --- ## 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/.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/.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 - akshare 指数:https://akshare.akfamily.xyz/data/index/index.html - akshare 基金:https://akshare.akfamily.xyz/data/fund/fund_public.html - akshare 债券:https://akshare.akfamily.xyz/data/bond/bond.html - baostock API:https://www.baostock.com/mainContent?file=pythonAPI.md - 国证指数网(深证历史):http://www.cnindex.com.cn/module/index-detail.html?indexCode=399001 - 新浪历史成份:http://vip.stock.finance.sina.com.cn/corp/go.php/vII_HistoryComponent/indexid/000852.phtml - QLib 数据层:https://qlib.readthedocs.io/en/latest/component/data.html - 现有缺口设计:`docs/static_data_gaps_design.md`