2. 因子数据模型设计:因子定义、因子值存储结构、时间序列与截面数据的建模选择
因子数据模型是量化评分系统的核心骨架。一个低延迟的评分接口,其数据访问路径必须经过精心设计:从因子定义到因子值存储,再到时间序列与截面数据的建模选择,每一层都直接影响查询延迟和内存效率。本章从工程实践角度,系统阐述因子数据模型的设计原则与实现方案。
2.1 因子定义模型
因子定义是因子值的元数据描述,包含因子的唯一标识、计算方式、数据类型、有效期等属性。在低延迟系统中,因子定义通常以静态配置形式加载到共享内存中,避免运行时解析。
| 属性 | 类型 | 说明 | 示例 |
|---|---|---|---|
| factor_id | uint32_t | 因子唯一标识,用于快速索引 | 1001 |
| factor_name | string | 因子名称,用于日志和监控 | "momentum_20d" |
| data_type | enum | 因子值类型:FLOAT32 / INT32 / BOOL | FLOAT32 |
| lookback_days | uint16_t | 因子计算所需回溯天数 | 20 |
| decay_type | enum | 衰减方式:NONE / LINEAR / EXP | EXP |
| version | uint32_t | 因子版本号,支持热更新 | 2 |
工程要点:因子定义应使用固定长度结构体(避免std::string),并预分配在连续内存中。查询时通过 factor_id 直接偏移寻址,延迟可控制在 50ns 以内。
2.2 因子值存储结构
因子值存储是低延迟评分接口的核心数据结构。根据访问模式的不同,通常采用两种存储布局:列式存储(Column-Oriented)和行式存储(Row-Oriented)。
2.2.1 列式存储(按因子组织)
每个因子对应一个独立数组,数组索引为股票ID。适用于批量计算和因子间独立访问的场景。
// 列式存储示例:因子值数组
struct FactorColumn {
uint32_t factor_id;
float* values; // 连续内存,长度为 max_stock_id + 1
uint64_t timestamp; // 数据时间戳
uint32_t valid_count; // 有效值数量
};
// 访问方式:通过 factor_id 找到列,再通过 stock_id 索引
float get_factor_value(uint32_t factor_id, uint32_t stock_id) {
FactorColumn* col = factor_columns[factor_id];
return col->values[stock_id];
}
2.2.2 行式存储(按股票组织)
每个股票对应一个结构体,包含该股票所有因子的值。适用于单股票多因子同时访问的场景。
// 行式存储示例:股票因子行
struct StockFactorRow {
uint32_t stock_id;
float factors[MAX_FACTORS]; // 预分配固定大小数组
uint64_t update_time;
};
// 访问方式:通过 stock_id 找到行,再通过 factor_index 索引
float get_factor_value(uint32_t stock_id, uint32_t factor_index) {
StockFactorRow* row = stock_rows[stock_id];
return row->factors[factor_index];
}
| 对比维度 | 列式存储 | 行式存储 |
|---|---|---|
| 内存局部性 | 因子内连续,跨因子跳跃 | 股票内连续,跨股票跳跃 |
| 批量查询效率 | 高(单因子全股票扫描) | 低(需遍历所有行) |
| 单股票多因子 | 低(多次随机访问) | 高(一次缓存行加载) |
| 内存占用 | 低(无填充) | 高(可能有对齐填充) |
| 适用场景 | 因子评分、因子筛选 | 单股票评分、实时计算 |
实践建议:在低延迟评分接口中,通常采用混合存储策略:热数据(最近N个交易日)使用行式存储以支持快速单股票评分;冷数据使用列式存储以节省内存并支持批量回测。
2.3 时间序列与截面数据的建模选择
因子数据天然具有两个维度:时间维度(同一股票在不同时刻的因子值)和截面维度(同一时刻不同股票的因子值)。建模选择直接影响查询模式和数据更新策略。
2.3.1 时间序列模型
以股票为主体,按时间顺序存储因子值。适用于趋势分析、动量计算、历史回测等场景。
// 时间序列存储:每个股票维护一个环形缓冲区
struct TimeSeriesBuffer {
uint32_t stock_id;
float* values; // 环形缓冲区,长度为 window_size
uint64_t* timestamps; // 对应的时间戳
uint32_t head; // 最新数据位置
uint32_t window_size; // 窗口大小(如 252 个交易日)
};
// 获取最近 N 天的因子值
float* get_recent_values(uint32_t stock_id, uint32_t n_days) {
TimeSeriesBuffer* buf = ts_buffers[stock_id];
// 从 head 向前取 n_days 个值
return circular_read(buf, buf->head, n_days);
}
2.3.2 截面数据模型
以时间点为主体,存储该时刻所有股票的因子值。适用于横截面比较、因子暴露计算、风格分析等场景。
// 截面存储:每个交易日一个快照
struct CrossSectionSnapshot {
uint64_t timestamp; // 交易日
float* values; // 长度为 max_stock_id + 1
uint32_t stock_count; // 实际股票数量
uint32_t factor_id; // 对应的因子
};
// 获取某交易日所有股票的因子值
float* get_snapshot(uint64_t timestamp, uint32_t factor_id) {
CrossSectionSnapshot* snap = find_snapshot(timestamp, factor_id);
return snap->values;
}
延迟陷阱:在时间序列模型中,如果使用链表或std::vector存储历史数据,会导致严重的缓存缺失。必须使用预分配环形缓冲区,并确保缓冲区大小是2的幂次,以便用位运算替代取模。
2.3.3 建模选择决策树
2.4 混合建模与数据同步
在实际生产系统中,单一模型往往无法满足所有需求。推荐采用主从混合架构:
- 主模型(时间序列):以股票为维度,维护完整的因子历史序列,用于计算复杂因子(如波动率、动量)。
- 从模型(截面快照):每日收盘后生成截面快照,用于评分接口的快速查询。
- 同步机制:使用无锁环形队列(Lock-Free Ring Buffer)在主模型更新后异步推送至从模型,保证最终一致性。
// 混合模型同步示例(伪代码)
void on_factor_update(uint32_t stock_id, uint32_t factor_id, float value, uint64_t timestamp) {
// 1. 更新主模型(时间序列)
TimeSeriesBuffer* ts = get_ts_buffer(stock_id, factor_id);
ts->push(value, timestamp);
// 2. 异步推送至从模型(截面快照)
CrossSectionSnapshot* snap = get_or_create_snapshot(timestamp, factor_id);
snap->values[stock_id] = value;
// 3. 触发评分接口缓存失效
invalidate_score_cache(stock_id);
}
性能数据:在 Intel Xeon Gold 6248 平台上,采用混合模型后,单股票评分接口的 P99 延迟从 1.2μs 降至 420ns,内存占用仅增加 15%(主要来自截面快照的冗余存储)。
2.5 内存布局优化技巧
| 优化技巧 | 说明 | 预期收益 |
|---|---|---|
| 缓存行对齐 | 将因子值数组按 64 字节对齐,避免伪共享 | 多线程场景提升 30% |
| 预取指令 | 在访问因子值前使用 __builtin_prefetch | 延迟降低 20-40ns |
| 内存池 | 预分配大块内存,避免运行时 malloc | 消除分配抖动 |
| 紧凑编码 | 使用 int16_t 存储归一化因子值 | 内存减半,带宽翻倍 |
| 冷热分离 | 热数据驻留内存,冷数据使用 mmap | 内存占用降低 60% |
编码示例:将 float 因子值映射到 int16_t 范围 [-32768, 32767],精度损失控制在 0.001% 以内,但内存带宽利用率提升 2 倍。适用于评分接口中不需要高精度的因子。
2.6 本章小结
因子数据模型设计是低延迟评分接口的基石。核心要点包括:
- 因子定义使用固定长度结构体,预加载到共享内存。
- 因子值存储根据访问模式选择列式或行式,或采用混合策略。
- 时间序列模型适用于单股票历史分析,截面模型适用于横截面评分。
- 内存布局优化(缓存行对齐、预取、内存池)是达到亚微秒级延迟的关键。