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 建模选择决策树

因子数据建模选择决策树 查询模式分析 单股票多时间点 多股票单时间点 时间序列模型 截面数据模型 环形缓冲区 行式存储 列式快照 内存映射文件 延迟:~100ns 延迟:~80ns 延迟:~120ns 延迟:~200ns 决策依据:查询模式 → 模型选择 → 存储结构 → 延迟预算

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 本章小结

因子数据模型设计是低延迟评分接口的基石。核心要点包括:

  • 因子定义使用固定长度结构体,预加载到共享内存。
  • 因子值存储根据访问模式选择列式或行式,或采用混合策略。
  • 时间序列模型适用于单股票历史分析,截面模型适用于横截面评分。
  • 内存布局优化(缓存行对齐、预取、内存池)是达到亚微秒级延迟的关键。