主题
ADR-0003: DataProvider 抽象
- 状态:Accepted
- 日期:2026-05-11
背景
v0.2.0 数据层是 core/data/schemas.py 里几个直接调 Tushare / akshare 的函数, 配 24h TTL 的 DuckDB cache。v0.2.0 上线第一周触发 5 起事故(见 tushare-integration-design §1):
- 永鼎股份 — 收盘后 24h cache 返回前日 close
- 璞泰来 — 从 ETF 持仓表读到了"价格"字段,语义不是 OHLC
- 大位科技 — 返回 6 天前的 close
- 浙文互联 — akshare
成交额单位是元,被当成万元 - 浙文互联 — akshare 没返回
昨收列,下游 KeyError
候选方案:
- 只缩短 TTL — 修事故 1 / 3
- 加一层 SourceRouter,每个 source 出独立函数 — 修 2 / 4 / 5 但容易回潮
- Provider ABC + Capability 枚举 + Composer 路由 — 全部 5 起 + 复权 + WarehouseProvider 钩子
- 换 LLM 自审 / 重试 — 不解决数据层 bug,把 cost 推给推理
决策
采用方案 3:DataProvider ABC + ProviderRegistry + QuoteComposer。
- 每个数据源实现
DataProviderABC(base.py),声明Capability集合 ProviderRegistry是单例注册表,lifespan 注入QuoteComposer持有ROUTES表(按(intent, MarketState)索引),按链顺序尝试 provider- 5 种 typed exception (
ProviderUnavailable/ProviderQuotaExceeded/CapabilityNotSupported/CanonicalUnitViolation/ProviderTimeout) 决定 Composer 是降级还是直接抛 - 单位 (yuan / hand / kyuan) 锁在
Quotedataclass,provider 边界负责把上游单位转 canonical - 复权 (raw / qfq / hfq) 通过
Quoteproperty 计算,不在 provider 内做
实现见 PR #24(ABC)、#30(MarketStateResolver)、#32(TushareDailyProvider)、 #33(AkshareSpotProvider)、#34(QuoteComposer + ROUTES)。
理由
- 强类型契约。
Quote是 frozen dataclass;dict 不能越过 provider 边界。事故 5 自然消失 - 强 capability。 Composer 只把请求路由到声明该能力的 provider。事故 2 不可能再发生
- 强 canonical 单位。 Provider 负责单位转换,错则
CanonicalUnitViolation内部抛。事故 4 不可能再静默通过 - 强 market-state 感知。 路由 + cache TTL 都看
MarketState,事故 1/3 一并修 - WarehouseProvider 钩子留好了。 W6 上线一份本地仓库实现,不动 Composer / 不动调用方
取舍
❌ 目录从 1 个文件 (schemas.py) 变成 1 个 package + 5 个文件
- 缓解:每个文件单一职责 ≤200 行;现行 spec / ADR 直接指向文件
❌ Composer 多 1 跳,latency 多 ~5-15ms
- 缓解:W3 起 Composer 命中本地
QuoteCache,外层延迟仍 < v0.2.0 实测 p95 (~400ms)
❌ Provider 注册写在代码里(lifespan register),不是 YAML 配置
- 缓解:本来就是单租户 Python monolith;YAML 的 ergonomics 不抵 import-cycle 风险
❌ Composer 默认"首个成功就 return",不主动跨源对账
- 缓解:跨源对账放进 AuditLog (W7 TDX,见 ADR 未来)。监测告警,不自动选边
为什么不是其他方案
- 只缩短 TTL:只修 5 起里的 2 起,剩下 3 起是结构性 bug
- 每个 source 独立函数 + 调用方挑:又把"选哪个 source"的决定推回到
routes/price.py和 skill 脚本,5 处不一致 - LLM 自审 / 重试:编造数字 90% 能被代码兜住(见 ADR-0002);事故里没有一起是推理错
影响
core/data/schemas.py现存函数标记为 v0.3.0 W6 删除(替代品:Composer + Cache)/priceroute W4 切流时改成app.state.composer.get_quote(...)(见 W4 runbook)src/service/cache.py(DailyCache/FundamentalsCache) 在 W6 删除,替换为core/data/quote_cache.py- 所有新数据源必须实现
DataProviderABC;不能再直接写"调用 X 库"的函数 - AuditLog (PR #41) / cross-source diff (W7 TDX) 都挂在 Composer 边界,不在 provider 内