58e9a3b053
承接 OpenBB 调研的可落地设计: - 实证本项目兜底坑(NaN当停牌/dbbardata双行/裸查询误报) - TET三段式契约+fail-fast治兜底映射 - 本项目落地方案:extract读本地(绕过网络)+transform pydantic校验 - 明确不照搬OpenBB网络层/Registry/Router(KISS)
8.7 KiB
8.7 KiB
Provider Fetcher 化设计笔记:用 TET 三段式治「兜底会乱」
设计日期 2026-07-30。来源:OpenBB Fetcher TET 三段式 + 本项目数据层历史踩坑。配套调研见
docs/research/openbb-platform-research.md。一句话定位:把数据层的容错「兜底」范式,改成 TET 三段式「严格校验、不行就报错」范式,让数据质量问题在取数时暴露,而不是被掩盖后在策略下单时记成大祸。
一、动机:本项目踩过的「兜底会乱」
本项目 provider 层(取数层)历史上为了「让数据能跑下去」做过各种容错/补默认/兜底。这些兜底掩盖了数据层的真问题,且兜底逻辑之间互相干扰、行为不可预测。三个真实实证:
实证 1:NaN 被当停牌 → 订单全取消
- 数据层某些字段缺失,provider 兜底补了
NaN - 下游
bool(NaN) == True(Python 坑:NaN 布尔值为 True) - bullet_trade 策略判断
if paused: 取消订单→ 全部订单被取消,0 交易 - 兜底的 NaN 本意「没数据」,被误解成「停牌」
实证 2:dbbardata 双行
- 数据层同一交易日存两行(日期格式:datetime 带时间 vs 纯日期)
- 若在 provider 层「兜底去重」→ 掩盖「数据层为何有双行」这个真 bug
- 正解是数据层根治(统一纯日期),不在 provider 掩盖
实证 3:裸查询误报
- 调研时手搓 SQL
symbol='000534'查 dbbardata(实为双列 symbol+exchange) - 误报「19 只股票无日线」;实际调
provider.get_price数据完整 - 兜底/手搓查询的口径 ≠ provider 真实查询口径
「乱」的本质
- 兜底逻辑散落在取数路径各处(补默认、转格式、去重、try-except 吞错)
- 让「本该报错的脏数据」被悄悄修正 → 数据质量问题被掩盖,直到酿成大祸
- 出了问题难定位:是数据脏?还是兜底错?还是两者叠加?
一句话:兜底 =「尽量让数据能用」(容错导向),代价是掩盖问题 + 行为不可预测。
二、TET 三段式设计契约
TET = Transform-Extract-Transform。取数拆成三个职责单一的步骤(源自 OpenBB Fetcher[Q, R]):
调用 get_price(symbol, start, end)
│
▼
① transform_query(参数) 校验 + 补默认 + 翻译
│ · 参数合法性(日期格式/symbol)
│ · 补默认值
│ · 通用参数 → 数据源特定参数
▼ 产出:QueryParams 对象
② extract_data(query) ★唯一 IO 入口
│ · 读本地 dbbardata/parquet(本项目已落库,不打网络)
│ · 返回:原始数据(可能脏)
▼ 产出:raw data
③ transform_data(query, raw) 把脏数据洗成标准 + 校验
│ · 字段映射(vendor 字段名 → 标准名)
│ · 类型转换
│ · pydantic 严格校验:不符合 schema 直接报错
▼ 产出:list[标准 Data 模型]
关键约束
- ②是唯一 IO 点:所有读盘/网络调用只能在这里。好处:易测试(mock 掉②单测①③)、易缓存(只缓存②)
- ①③是纯函数:给定输入确定输出,可缓存可重放
- 职责单一:改「取数」不动「清洗」,改「清洗」不动「取数」
- ③用 pydantic 校验 fail-fast:不符合就抛
ValidationError,不静默兜底
三、TET 如何治「兜底会乱」(问题 → 对策映射)
| 兜底会乱的问题 | TET 对策 |
|---|---|
| 清洗逻辑散落各处 | 集中到 transform_data 一个钩子 |
| 脏数据静默补默认/吞错 | pydantic 校验,报错 fail-fast |
| 字段映射过程式 if-else 易错 | 声明式 __alias_dict__,一眼看全 |
| 取数与清洗混在一起 | 严格分离(extract 只取,transform 只洗) |
| 难测试(要真打网络) | extract 是唯一 IO,mock 它即可单测 transform |
| 问题暴露晚(策略下单时才爆) | 问题暴露早(取数校验时就报) |
治本原理:把「尽量让数据能用」(容错)改成「严格校验、不行就报错」(fail-fast)。数据脏在取数时就暴露,而不是被兜底掩盖后在下游(下单、回测)记成大祸。
四、伪代码对比(直观)
Before:当前兜底模式(取数 + 兜底混在一起)
def get_price(symbol, start, end):
df = read_dbbardata(symbol, start, end) # 取数
if 'close' not in df: df['close'] = np.nan # 兜底1:补 NaN ← 掩盖缺失
df = df.drop_duplicates() # 兜底2:去重 ← 掩盖双行 bug
try: df['date'] = pd.to_datetime(df['date']) # 兜底3:吞错
except: pass
return df
问题:NaN 被下游当停牌、去重掩盖数据层 bug、except 吞掉真错误。
After:TET 化(职责分离 + fail-fast)
class PriceQueryParams(QueryParams):
symbol: str
start: date
end: date
class PriceData(Data):
date: date
open: float | None
close: float # 必填 → 缺失直接 ValidationError,不补 NaN
volume: float
class PriceFetcher(Fetcher[PriceQueryParams, list[PriceData]]):
@staticmethod
def extract_data(query) -> list[dict]:
# 唯一 IO:读本地 dbbardata/parquet
return read_dbbardata(query.symbol, query.start, query.end)
@staticmethod
def transform_data(query, raw) -> list[PriceData]:
# 校验 + 映射,不符合就抛错
return [PriceData(**row) for row in raw]
close缺失 → pydantic 报错 → 数据层 bug 立刻暴露,不补 NaN 掩盖- extract 只读盘,transform 只校验,职责清晰可单测
五、本项目落地方案
现状
LocalUnifiedProvider直读本地(dbbardata/parquet),符合provider-local-data-only铁律- 但取数与兜底/清洗逻辑混在
get_price/get_fundamentals等方法里,散落容错
目标
每个数据源封装成一个 Fetcher,取数走三段式:
- extract_data:读本地 dbbardata/parquet(绕过网络——这是本项目对 TET 的关键改造)
- transform_data:pydantic 校验 +
__alias_dict__字段归一,取代散落兜底
关键认知:本项目用 TET 但 extract 读本地
OpenBB 的 extract_data 打网络 API(实时点菜);本项目已落库,extract_data 内部读 sqlite/parquet(提前囤货)。TET 的精髓是「IO 集中」,不是「必须打网络」。所以本项目:
- 借鉴:TET 三段式结构 + transform 的严格校验(治兜底)
- 不照搬:OpenBB 的网络层、Registry/entry_points、Router、FastAPI(单仓库过度工程,违反 KISS/YAGNI)
不照搬清单(明确划界)
| OpenBB 有 | 本项目是否需要 | 理由 |
|---|---|---|
| Fetcher TET 三段式 | ✅ 借鉴 | 治兜底会乱,核心价值 |
__alias_dict__ 字段归一 |
✅ 借鉴 | 多源(baostock/akshare/miniQMT)字段统一 |
| Registry / entry_points | ❌ 不需要 | 单仓库单开发者,字典注册即可 |
| Router 命令树 / FastAPI | ❌ 不需要 | 策略层直调 provider,不给第三方 REST |
| MCP 多出口代码生成 | ❌ 不需要 | 工具 <30 不必,可单独按需暴露 |
| 网络实时取数 | ❌ 不照搬 | 本项目落库导向,extract 读本地 |
六、设计原则(可复用)
- fail-fast > 容错兜底:数据不符合 schema 就报错,不静默补默认。掩盖问题比报错危险得多。
- 数据质量问题在取数时暴露,不下沉:transform_data 是数据质量的「海关」,脏数据在这里被拦,不让它流到策略层。
- 数据层瑕疵报数据层根治,不在 provider 掩盖(呼应
feedback-no-provider-workaround):双行、格式不一、缺失——这些是数据层的 bug,治在数据层(如统一纯日期、补数),不在 provider 层兜底掩盖。 - IO 集中:所有读盘只在一个地方(extract_data),其余纯函数。便于测试、缓存、替换数据源。
七、改造优先级建议
- 先在数据质量最痛的入口试点:选一个出过坑的(如曾被 NaN 当停牌的行情取数),重构成 Fetcher 三段式,验证 fail-fast 能否抓住数据问题
- 定义本项目标准 Data 模型:参考 OpenBB standard_models,定义
PriceData/FundamentalsData/ConstituentData等,字段必填/可选明确 - 逐步迁移:baostock/akshare/miniQMT/parquet 各源封装 Fetcher,统一走 transform_data 校验
- 保留现有 LocalUnifiedProvider 作为门面:内部委托给各 Fetcher,对外 API 不变(策略层无感)
相关
- OpenBB 调研报告:
docs/research/openbb-platform-research.md/ wikireferences/openbb-platform-research - 本项目实证坑:
unified-provider-paused-nan-bug(NaN 当停牌)、dbbardata-dedup-pending(双行根治)、feedback-no-provider-workaround(数据层根治不在 provider 兜底)、feedback-verify-via-provider-exact-query(验证复刻 provider 确切查询) - provider 本地只读铁律:
provider-local-data-only