feat(portfolio): LocalUnifiedProvider spec §6 使用层落地 + VPS E2E(Task6)

spec §6 使用层 provider — 读方案A 权威数据层, 零 online, 治幸存者偏差:
- get_price: dbbardata('d') raw + bs_adjust_factor 前复权(asof, qfq[t]=raw[t]*factor[t])
- get_index_stocks/get_constituent: constituent_unified 并集治偏差(300=940含被踢, 无date时点)
- get_fundamentals_df: baostock pe/pb/ps/pcf + akshare 市值 + 三表委托 LocalParquetProvider
- 辅助: trade_days/security_info/current_tick/split_dividend/all_securities

VPS E2E 实证修复(Mac fixture 盲区):
- dbbardata datetime 混合格式("2024-09-26" vs "2024-09-26 00:00:00")
  → pd.to_datetime format='mixed' + SQL substr(datetime,1,10) 比日期(字符串比漏边界)
- 补 TestMixedDatetimeFormat 单测覆盖

验证: VPS 真数据 E2E 全通过(600519在市raw/qfq复权/000005退市治偏差/510300ETF/
fundamentals市值+pe+eps全字段/辅助方法); Mac 37单测+149回归绿

交付: 使用说明 docs/portfolio_local_unified_provider.md(其他 session 直用)+
plan+probe+E2E 脚本
This commit is contained in:
2026-07-23 08:25:44 +08:00
parent b89eb0f941
commit 41cc6d13bf
6 changed files with 979 additions and 6 deletions
@@ -174,19 +174,21 @@ class LocalUnifiedProvider(DataProvider): # type: ignore[misc]
frames: Dict[str, pd.DataFrame] = {}
for jq_code in secs:
sym, exc = jq_to_dbbardata(jq_code)
# substr(datetime,1,10) 取日期部分比 — datetime 列混合格式(有只日期有带时间),
# 纯字符串比 "2024-09-25" < "2024-09-25 00:00:00" 会漏边界行; 比日期(YYYY-MM-DD)规避
q = (
"SELECT datetime, open_price, high_price, low_price, close_price, "
"volume, turnover FROM dbbardata WHERE symbol=? AND exchange=? "
"AND interval='d' AND datetime>=? AND datetime<=? ORDER BY datetime"
)
df = pd.read_sql(
q, conn,
params=(sym, exc, start_str + " 00:00:00", end_str + " 23:59:59"),
"AND interval='d' AND substr(datetime,1,10)>=? AND substr(datetime,1,10)<=? "
"ORDER BY datetime"
)
df = pd.read_sql(q, conn, params=(sym, exc, start_str, end_str))
if df.empty:
frames[jq_code] = df
continue
df["datetime"] = pd.to_datetime(df["datetime"])
# dbbardata datetime 混合格式(有的 "2024-09-26" 有的 "2024-09-26 00:00:00",
# 不同 schtask/迁移写入);pandas 2.3 严格模式要 format="mixed"
df["datetime"] = pd.to_datetime(df["datetime"], format="mixed")
df = df.set_index("datetime")
df.index.name = None
if count: