这篇面向想理解 Betalens 数据层设计细节的读者。如果你只想快速跑策略,可以跳过这篇,等需要优化查询或做数据治理时再回来。
导言
Betalens 把所有研究数据存在 PostgreSQL 里,但这个数据库不是简单的”一张大宽表塞所有字段”,而是遵循了一套 fact/dim 架构(事实表 + 维表)。理解这套架构,能帮助你:
- 在查询报错时快速定位是哪个表/字段出了问题。
- 理解 PIT(Point-in-Time)查询的原理。
- 在做数据导入时知道该往哪里写、怎么写。
- 在查询性能出问题时判断瓶颈在 SQL 层面还是数据本身。
十五张基础表一览
Betalens 的 betalens schema 下有 15 张物理表,分为三类:
维度表(6 张)
| 表名 | 作用 |
|---|---|
entity_dim |
证券实体维表,存储代码(如 000001.SZ)与数据库内部 ID 的映射 |
entity_name_history |
证券名称历史(包含更名、上市/退市状态) |
metric_dim |
指标维表,存储指标名(如 "收盘价(元)")与 ID 的映射 |
metric_alias |
指标别名映射(如 "close" → "收盘价(元)") |
industry_scheme_dim |
行业分类体系(如”申万一级行业”、”证监会行业”) |
industry_dim |
行业维度,存储每个行业分类体系下的具体行业 |
trade_calendar_day |
交易日历,按交易所(exchange)存储每个交易日 |
事实表(7 张)
| 表名 | 作用 |
|---|---|
market_daily_fact |
行情日频事实表,以 (entity_id, trade_date) 为主键,存储所有行情字段 |
observation_fact |
观测值事实表,通用宽表,存储任意指标的时间序列,支持 PIT |
industry_membership |
行业成分关系,某股票在某日期属于哪个行业 |
index_snapshot |
指数快照,某个指数在某日期的成分股列表 |
index_constituent |
指数成分关系,某股票在某日期是否是某指数的成分股 |
trade_status_event |
交易状态事件,记录停牌/复牌/上市/退市的时间点 |
管理表(2 张)
| 表名 | 作用 |
|---|---|
schema_migration |
Migration 版本记录,确保数据库 schema 版本一致 |
dataset_coverage |
数据集覆盖情况,记录哪些 entity/metric 在哪些日期有数据 |
fact/dim 架构解析
行情事实表:market_daily_fact
这是最常用的表之一,结构大概是:
1 | market_daily_fact ( |
daily_market、daily_index、daily_fund、daily_bond 本质上都是对 market_daily_fact 的视图映射,通过 entity_dim 中的 entity_type 字段区分股票/指数/基金/债券。
通用观测值表:observation_fact
fundamentals 表对应的底层存储是 observation_fact。它的结构是行式存储(每行一个 entity × metric × date):
1 | observation_fact ( |
value_end_date 是 PIT 查询的核心。举例:某公司在 2021-04-29 披露年报,股息率为 3.2%,年报截止日是 2020-12-31。那么:
trade_date = 2021-04-29(披露日)value_end_date = 2020-12-31(数据对应的事实期末)metric_id对应 “股息率(报告期)”
如果我们在 2020-06-01 查询”当时能看到的股息率”,由于 value_end_date=2020-12-31 还没到,理论上这条数据当时不可见(年报还没披露)。PIT 查询通过比较 trade_date 和 value_end_date 来实现严格的历史可见性控制。
交易状态表:trade_status_event
这张表不是每天每只股票的完整状态,而是一张事件表(稀疏存储):
1 | trade_status_event ( |
解读逻辑在查询端还原:
1 | # 查询端根据事件表还原每天的状态 |
这就是 trade_status 表(trade_status_matrix 在内存中的表示)的来源。框架根据事件表动态还原每天的状态。
PIT(Point-in-Time)查询原理
PIT 查询的意思是:在某个历史时点,我知道什么信息,就只能用什么信息做决策。
以股息率为例:
- 2021-04-30 披露的年报(对应 2020 年末数据),在 2021-04-30 之前是不可见的。
- 如果你在 2021-04-01 做调仓决策,不应该看到这条年报数据。
- Betalens 通过
observation_fact.value_end_date和trade_date的比较,在 SQL 层面过滤掉”历史上不可见”的数据。
1 | # Betalens 的 PIT 查询等价 SQL 逻辑(简化) |
在 pre_query_characteristic_data 中,time_tolerance 控制的是”最多往前找多少小时的数据”——比如设 time_tolerance=24*2*365(两年),意思是:如果某天没有找到该指标的数据,最多往前找两年内的最近一条。
行业维度与成分映射
行业信息存储在两张表里:
1 | industry_scheme_dim (scheme_id, scheme_name) |
industry_membership 记录某股票在某段时间内属于哪个行业(允许历史上变更行业)。
preprocess_factor 的 industry_scheme 参数(如 "申万一级行业")会在查询时自动做 PIT join,找到某股票在某调仓日的正确行业分类。
开发者侧:Schema 创建流程
Schema 不需要手动写 SQL,全部由 betalens_db_manager 管理。核心流程:
1 | # 查看建表计划(dry-run) |
内部用版本化的 migration 文件(0001 ~ 0009 等),每个 migration 有 checksum。如果数据库 checksum 不匹配,会拒绝启动,保证团队多人协作时 schema 一致。
常见误解
- “fact/dim 是银弹”:这套架构适合因子研究,但不代表任何场景都最优。如果你的数据是分钟级 Tick 量,
market_daily_fact按天存储的设计就会成为瓶颈,需要额外的分区策略。 - “trade_status 是每天的完整状态表”:它是事件表,不是每天每只股票一行。框架在查询时会实时还原。
- “fundamentals 表是一张物理表”:实际上它是对
observation_fact的视图,物理表只有observation_fact一张,所有非行情因子(财务、情绪、技术指标)都在这里。
延伸阅读
- Betalens 新手系列 · 06:Datafeed API 实战——查询接口详解。
- Betalens 新手系列 · 07:数据治理工具 betalens_db_manager——Schema、Import、GUI 详解。
- Betalens 新手系列 · 08:PostgreSQL 查询优化——索引与分区。
docs/guide/db-manager.rst——官方数据库管理文档。docs/guide/datafeed.rst——官方数据访问文档。