# LocalUnifiedProvider 使用说明 > spec §6 使用层 provider。读**方案A 权威数据层**,零 online,治幸存者偏差。**方案A 数据层落地后的推荐 provider**。 > 实现见 `sanguo_portfolio/providers/local_unified_provider.py`,测试 `tests/portfolio/test_local_unified_provider.py`(36 用例)。 ## 一句话定位 一个 provider,内部按数据类路由方案A 的权威表(dbbardata / constituent_unified / valuation_baostock / static akshare),**零 online**(不调 baostock HTTP,纯读本地 sqlite/parquet),**治幸存者偏差**(成份股并集含退市/被踢 + dbbardata 日线含退市),喂 `all_weather` 等策略。 ## 快速使用 ```python from sanguo_portfolio.providers import LocalUnifiedProvider # VPS(默认路径 C:\sanguo_vnpy_v2\data) p = LocalUnifiedProvider() # Mac 测试 / 自定义路径 p = LocalUnifiedProvider({ "db_path": "/path/to/quant_trading.db", "data_dir": "/path/to/data", # 含 valuation_baostock/ + static/ }) # 回测入口(runner) # python -m sanguo_portfolio.runner_backtest --provider unified --start 2024-01-01 --end 2024-12-31 ``` ## 数据源映射(每接口 → 方案A 权威表) | 方法 | 数据源 | 表 / 文件 | 归一化 | |---|---|---|---| | `get_price` | dbbardata('d') raw + bs_adjust_factor | `quant_trading.db` | jq_code↔symbol+exchange; `SSE→SH`; raw 默认, `fq='qfq'` 按 foreAdjustFactor 算 | | `get_index_stocks` / `get_constituent` | constituent_unified 并集 | `quant_trading.db` | code(纯6位)→jq_code; 返回 `in_current=1 ∪ was_removed=1` | | `get_fundamentals_df` | pe/pb/ps/pcf ← valuation_baostock; 市值+三表 ← static akshare | `.parquet` + `static/{valuation,balance,income}/` | 对齐 `_FUNDAMENTAL_COLUMNS`; 市值元→亿 | | `get_trade_days` | dbbardata('d') 600519 distinct datetime | `quant_trading.db` | — | | `get_all_securities` | dbbardata distinct symbol | `quant_trading.db` | — | | `get_security_info` | dbbardata min/max datetime + constituent_unified code_name | `quant_trading.db` | — | | `get_current_tick` | dbbardata 最近 close × 1.1/0.9 | `quant_trading.db` | ST/创业/科创精确规则 v2 | | `get_split_dividend` | bs_adjust_factor 除权事件 | `quant_trading.db` | dividOperateDate + factor | ## 接口清单 ```python # K 线(日线 raw 真实价,按需前复权) get_price(security, start_date=None, end_date=None, frequency="daily", fields=None, skip_paused=False, fq="raw", count=None, panel=True, fill_paused=True) -> pd.DataFrame # - frequency 非 daily/day/1d/d → 返空(1m 数据层无,day 频率回测降级) # - panel=False → 长表含 time + code 列(供策略 pivot) # - fq='qfq'/'pre' → 按 bs_adjust_factor 算前复权 # - fields 缺失列(如 high_limit)补 NaN(策略涨停识别降级) # 成份股(spec §6 治偏差核心) get_index_stocks(index_symbol, date=None) -> List[str] # date 忽略(并集模型) get_constituent(index, date=None) -> List[str] # 语义别名 # 基本面(列对齐 _FUNDAMENTAL_COLUMNS,策略选股核心) get_fundamentals_df(stocks, date=None) -> pd.DataFrame # 辅助 get_trade_days(start_date=None, end_date=None, count=None) -> List[datetime] get_all_securities(types=None) -> pd.DataFrame get_security_info(security) -> Dict get_current_tick(security) -> Optional[Dict] # 回测从 K 线推涨跌停 get_split_dividend(security, start_date=None, end_date=None) -> List[Dict] ``` ## 复权(方案A §14.7 最终目标) - **dbbardata 存 raw 真实价**(不复权)。`get_price` 默认 `fq='raw'` 返 raw。 - **前复权消费端算**:`get_price(fq='qfq')` 按 `bs_adjust_factor.foreAdjustFactor` 算。 - **asof 语义**:每个日期找 `≤ 该日` 的最大除权日的 `foreAdjustFactor`;早于所有除权日用最早因子;晚于所有用最新(=1.0)。 - **公式**:`qfq[t] = raw[t] × factor[t]`(open/high/low/close 同乘,volume/turnover 不乘)。 - 例:600519 最新除权 2026-06-26 factor=1.0;历史递减(2020-06-24=0.856)。 - 策略 `_trend_mean` 算 N 日涨幅是比率,raw/qfq 等价(除权日 raw 跳水除外);要精确除权连续性用 `fq='qfq'`。 ## 幸存者偏差治理(关键!) **`constituent_unified` 是"全时期并集"模型**(无 date 列): - 9 指数分布:`000300`=940只(300当前+640被踢) / `000905`=1803(500+1303) / `000016`=195(50+145) / 深证 399001=702,399005=145,399006=175,399330=150 - **治"纯当前幸存者"偏差**:含已退市/被踢股票(如 000005 退市、600811 被踢都在 300 并集) - **轻微前视**:`get_index_stocks(date)` 的 `date` 参数**被忽略**(表无时点数据),回测 2020 年选股池 = 历史上所有曾在该指数的股票(含 2024 才纳入的)。比纯当前快照好,但不如 baostock `query_hs300_stocks(date)` 时点精确。 - **永久 gap**:中证1000(`000852`)/2000(`932000`)只当前快照(1000/2000 全当前,0 被踢),历史成份股不可补(csindex SPA 封/akshare 只快照)。 - **dbbardata 日线也治偏差**:含退市股 K 线(000005 退市到 2024-04-26,600811 等),回测能真实反映"当时买入现已退市"的标的。 ## Mac 测试(零 VPS 依赖) `tests/portfolio/test_local_unified_provider.py` 用 `tmp_path` + `sqlite3` + tmp parquet fixture,完全不依赖 VPS 数据: ```python def test_get_index_stocks_union(tmp_path): db = tmp_path / "t.db" c = sqlite3.connect(str(db)) c.execute("CREATE TABLE constituent_unified(...)") # 造 in_current + was_removed 样本 ... p = LocalUnifiedProvider({"db_path": str(db), "data_dir": str(tmp_path)}) assert set(p.get_index_stocks("000300.XSHG")) == {...} # 含被踢 ``` ```bash python3 -m pytest tests/portfolio/test_local_unified_provider.py -v # 36 passed python3 -m pytest tests/portfolio/ -q # 全回归 149 passed ``` ## 部署 / 运行 **VPS 数据依赖**(方案A 已落地,见 memory `data-fusion-design-finalized` / `vps-local-data-layout`): - `C:\sanguo_vnpy_v2\data\quant_trading.db` — 含 dbbardata / constituent_unified / bs_adjust_factor - `C:\sanguo_vnpy_v2\data\valuation_baostock\.parquet` — 1990-2026 全年份 - `C:\sanguo_vnpy_v2\data\static\{valuation,balance,income,cashflow}\*.parquet` — akshare 三表+市值 - 日增量:`sanguo-bs-eod`(18:05 baostock 日线+15min+pe/pb)+ `sanguo-xt-eod`(18:40 ETF/基金)已部署 **rsync 同步代码到 VPS**: ```bash rsync -avz -e ssh --exclude='.git' --exclude='vnpy_v4.4.0' --exclude='__pycache__' \ --exclude='.superpowers' --exclude='docs' --exclude='tests/data' \ ./ 49.232.102.198:C:/sanguo_vnpy_v2/ ``` ⚠️ config 不在排除列表,会覆盖 VPS config(方案A §14.9 已知 TODO:部署前 `--exclude config` 或靠 SANGUO_DATA_ROOT)。 **回测**: ```bash ssh 49.232.102.198 'cd C:\sanguo_vnpy_v2 && C:\Python310\python.exe -X utf8 -m sanguo_portfolio.runner_backtest --provider unified --start 2024-01-01 --end 2024-12-31 --cash 1000000 --max-pool 20' ``` ## 已知限制(v1) | 限制 | 影响 | 对策 | |---|---|---| | `high_limit` 列 NaN | 策略 `prepare_stock_list` 昨日涨停识别降级(close==high_limit 不命中) | dbbardata 不存涨跌停;`get_current_tick` 另算;v2 可从 valuation pctChg 推 | | 1m 频率返空 | `_intraday_high_low` 降级 | 数据层无 1m;day 频率回测不触发;15m 在 dbbardata('15m') 可扩展支持 | | `gross_profit_margin`/`roic` NaN | fundamentals 两字段空 | 委托 LocalParquetProvider 读 `financial_abstract`,fixture 未造则 NaN(非新缺口) | | 成份股轻微前视 | 回测早期选股池含未来纳入股 | 方案A 既定取舍(并集模型);要精确时点需 baostock online(违反铁律) | | `get_current_tick` 涨跌停 ±10% 简化 | ST/创业板/科创板精确涨跌停未区分 | v2 从 valuation `isST` + 代码段识别 | ## 与旧 provider 的关系 | provider | 数据源 | 用途 | 状态 | |---|---|---|---| | **`LocalUnifiedProvider`** | 方案A 权威层(dbbardata/constituent_unified/valuation_baostock) | **方案A 后推荐** | ✅ 新增 | | `LocalParquetProvider`(`--provider local`) | 旧 parquet(qfq 日线/index_const 快照/akshare valuation) | MVP 验证遗留 | 保留(向后兼容,unittest 仍在) | | `BaostockProvider`(`--provider baostock`) | baostock online HTTP | Mac 跨平台调试 | 保留(违反"读本地"铁律,非生产推荐) | | `SanguoMiniQmtProvider`(`--provider miniqmt`) | miniQMT xtquant | VPS 实盘 | 保留(实盘 runner_live 用) | **迁移建议**:新回测/策略用 `--provider unified`。`local` 是方案A 前的 MVP 链路(读旧 parquet, index_const 仅当前快照有幸存者偏差),`unified` 读方案A 权威层治偏差。 ## 设计文档 - spec:`docs/superpowers/specs/2026-07-21-data-source-fusion-design.md` §6(使用层)+ §14(方案A 数据层) - plan:`docs/superpowers/plans/2026-07-23-local-unified-provider.md`(TDD 拆解) - 关联 memory:`data-fusion-design-finalized` / `vps-local-data-layout` / `provider-local-data-only` / `db-primary-parquet-fallback`