4. 数据模型设计(下):缓慢变化维度处理、SCD类型1/2/3实现、拉链表设计、历史数据回溯机制

好,咱们接着聊数据模型。上一节我们把星型模型和雪花模型搭好了骨架,这一节要往里面填“会变”的数据。

做量化回测最头疼的是什么?不是策略写不出来,而是历史数据“变脸”。你今天拉到的股票行业分类,跟三个月前可能完全不一样。公司重组了、退市了、代码变更了……这些变化如果处理不好,回测结果就是自欺欺人。

我刚开始做回测系统时,就踩过这个坑。回测跑出来年化30%,兴奋得不行。结果一查,是因为某只股票在回测期内被借壳了,数据源自动把新公司的财务数据填了进去。嗯,那30%的收益其实是幻觉。

4.1 缓慢变化维度(SCD)—— 回测数据的“时间褶皱”

缓慢变化维度,英文叫 Slowly Changing Dimensions,简称 SCD。说白了,就是维度表里的数据会随时间变化,但变化频率不高。

在量化场景里,典型的SCD场景有:

  • 股票代码变更:比如“600001”变成了“600010”
  • 行业分类调整:申万行业每两年调整一次
  • 公司名称变更:从“XX科技”变成“XX控股”
  • 上市状态变化:正常交易 → ST → 退市

你想想看,如果回测时用今天的行业分类去算三年前的股票,那结果能准吗?肯定不准。所以我们必须记录这些变化,并且让回测引擎能“穿越”到当时的维度状态。

核心原则:回测时,必须使用数据发生时刻的维度信息,而不是查询时刻的维度信息。

4.2 SCD 类型1/2/3 实现 —— 三种武器,各有用途

业界有标准的SCD处理方式,一共三种类型。我分别讲一下在量化回测里的用法。

4.2.1 类型1:直接覆盖

最简单粗暴。新数据来了,直接把旧数据覆盖掉。不保留历史。

适用场景:错误修正、无关紧要的属性。

比如某只股票的英文名拼错了,直接改掉就行。回测时不需要知道它以前叫啥。

-- 类型1:直接更新
UPDATE dim_stock
SET stock_name_en = 'Ping An Insurance'
WHERE stock_code = '601318';

我个人习惯,类型1只用于“纯纠错”场景。但凡跟业务逻辑相关的属性,我都不用类型1。

注意:类型1会丢失历史信息。如果回测需要追溯旧值,千万别用。

4.2.2 类型2:增加新行

这是量化回测里最常用的方式。每次维度发生变化,就插入一条新记录,并标记生效时间范围。

举个例子,某只股票从“制造业”改成了“信息技术业”。在类型2里,我们会这样记录:

-- 类型2:增加新行,记录有效期
INSERT INTO dim_stock_history (
    stock_code, stock_name, industry,
    effective_date, end_date, is_current
) VALUES
('000001', '平安银行', '金融业', '2020-01-01', '2022-06-30', 0),
('000001', '平安银行', '信息技术', '2022-07-01', '9999-12-31', 1);

回测时,只需要根据交易日期,JOIN 到对应时间范围内的那条记录即可。

-- 回测查询示例
SELECT f.*, d.industry
FROM fact_trade f
JOIN dim_stock_history d
  ON f.stock_code = d.stock_code
 AND f.trade_date BETWEEN d.effective_date AND d.end_date;

我在项目中遇到过一个问题:如果一天内维度变了多次怎么办?比如公司上午改名,下午又改回来。我的做法是,按时间戳精确到秒,保证每条记录的时间区间不重叠。

4.2.3 类型3:增加新列

类型3不增加行,而是增加列。比如在维度表里同时保留“当前行业”和“原始行业”两列。

这种方式的优点是查询快,不需要JOIN历史表。但缺点也很明显:只能记录有限次数的变化。

-- 类型3:增加列
ALTER TABLE dim_stock ADD COLUMN industry_original VARCHAR(50);
ALTER TABLE dim_stock ADD COLUMN industry_current VARCHAR(50);

说实话,类型3在量化回测里用得不多。除非你明确知道只需要对比“变化前”和“变化后”两个状态,否则我建议优先用类型2。

我的建议:80%的场景用类型2,15%用类型1,5%用类型3。别纠结,类型2最稳。

4.3 拉链表设计 —— 回测数据的“时间机器”

拉链表,本质上就是类型2的工程化实现。但它更强调“全量快照”的概念。

什么叫拉链表?就是一张表,记录了每个维度在每个时间点的完整状态。你可以把它想象成一张“拉开的链条”,每个链环代表一个时间段。

拉链表的核心字段:

字段名 类型 说明
dim_id BIGINT 代理主键,自增
natural_key VARCHAR 业务主键,如股票代码
effective_date DATE 生效日期
end_date DATE 失效日期,默认'9999-12-31'
is_current BOOLEAN 是否当前有效
attribute_1 VARCHAR 维度属性1
attribute_2 VARCHAR 维度属性2
... ... 其他属性

拉链表的设计要点:

  • 代理主键:不要用业务主键做主键,否则历史记录会冲突
  • 闭开区间:我习惯用 [effective_date, end_date) 左闭右开,查询时用 WHERE trade_date >= effective_date AND trade_date < end_date
  • 当前记录标记:is_current=1 表示当前有效,方便快速查询最新状态

拉链表 vs 全量快照表:拉链表只存储变化的部分,节省空间。全量快照表每天存一份完整数据,查询快但存储大。量化回测中,我建议日频数据用拉链表,高频数据用快照表。

4.4 历史数据回溯机制 —— 让回测引擎“穿越”回过去

有了拉链表,回溯就简单了。核心思路是:回测引擎在计算某一天的信号时,只能看到那一天之前的数据。

具体实现分三步:

  1. 确定回测日期:比如今天是2024-01-15,回测日期是2022-06-01
  2. 查询维度快照:从拉链表中查出2022-06-01当天有效的维度记录
  3. 拼接事实数据:用维度快照和事实表做JOIN,生成回测数据集

代码示例:

-- 历史回溯查询
WITH dim_snapshot AS (
    SELECT natural_key, attribute_1, attribute_2
    FROM dim_stock_zk
    WHERE effective_date <= '2022-06-01'
      AND end_date > '2022-06-01'
)
SELECT f.trade_date, f.stock_code, f.volume, d.attribute_1
FROM fact_trade f
JOIN dim_snapshot d
  ON f.stock_code = d.natural_key
WHERE f.trade_date = '2022-06-01';

我曾经犯过一个低级错误:在回溯时忘了过滤 end_date,结果把未来的维度信息也拉进来了。回测结果看起来很美,实则是“未来函数”。

避坑指南:一定要在SQL里显式加上 end_date 过滤条件。别偷懒用 is_current=1,那只能查到最新状态,不是历史状态。

另外,还有一个容易被忽略的点:数据延迟。比如今天是2024-01-15,但最新的财务数据可能只到2024-01-10。回测时,如果用了2024-01-15的维度信息去算2024-01-10的信号,那又引入了未来数据。

我的做法是:在回测引擎里维护一个“数据可用性日历”,记录每个数据源的最新可用日期。回测时,只使用可用日期之前的数据。

-- 数据可用性检查
SELECT data_source, max(available_date) as latest_date
FROM data_availability
GROUP BY data_source;

嗯,这里要注意:数据延迟不是bug,是feature。处理好了,你的回测结果才真实可信。

4.5 本章小结

这一节内容不少,我帮你捋一下:

  • SCD类型1:直接覆盖,用于纠错
  • SCD类型2:增加新行,记录历史,最常用
  • SCD类型3:增加新列,有限历史,少用
  • 拉链表:类型2的工程化实现,核心是生效时间和失效时间
  • 历史回溯:用拉链表+数据可用性日历,保证回测不引入未来数据

说白了,数据模型设计就是跟“时间”打交道。你处理好了时间维度,回测结果才有说服力。否则,再漂亮的策略也是空中楼阁。

个人经验:我建议在数据仓库里专门建一个 dim_time 表,包含日期、交易日标志、季度末标志等。很多回溯查询都要用到它,省得每次临时算。

量化回测数据模型设计(下)核心逻辑 缓慢变化维度 SCD 类型1:直接覆盖 不保留历史,用于纠错 类型2:增加新行 记录历史,最常用 类型3:增加新列 有限历史,少用 拉链表设计 生效日期 + 失效日期 + 当前标记 历史数据回溯机制 回测引擎“穿越”回过去,避免未来函数 ⚠ 避坑指南 别忘了过滤 end_date 💡 个人建议 80%场景用类型2

好了,这一节就到这里。数据模型设计的下半部分,核心就是处理“变化”。你掌握了SCD和拉链表,回测数据的准确性就有了保障。


公众号:蓝海数据掘金营,微信deep3321