4. 时间序列数据:OHLCV数据格式、复权因子处理与时间索引对齐

在量化回测中,时间序列数据是构建样本与标签对齐的基础。原始行情数据通常以OHLCV格式存储,但直接使用原始价格进行回测会因分红、送股、配股等事件导致价格缺口,从而产生虚假信号。因此,必须通过复权因子对价格序列进行修正,并确保时间索引严格对齐。

4.1 OHLCV数据格式与标准字段

OHLCV是金融时间序列最基础的5维数据,分别代表:开盘价(Open)、最高价(High)、最低价(Low)、收盘价(Close)和成交量(Volume)。标准日线数据通常还包含成交额(Amount)和涨跌幅(PctChange)。

字段 符号 含义 数据类型 常见单位
开盘价 Open 当日第一笔成交价 float64 元
最高价 High 当日最高成交价 float64 元
最低价 Low 当日最低成交价 float64 元
收盘价 Close 当日最后一笔成交价 float64 元
成交量 Volume 当日成交总股数 int64 股
成交额 Amount 当日成交总金额 float64 元
数据质量提示: 在构建回测样本时,务必检查OHLCV数据的完整性。常见问题包括:缺失交易日、价格为零或负数、成交量异常跳变。建议在数据加载阶段执行空值检测和价格单调性校验(High ≥ Open/Close ≥ Low)。

4.2 复权因子处理:前复权与后复权

股票在存续期内会发生分红、送股、配股、拆细等事件,导致除权除息后价格出现断崖式下跌。这种缺口并非市场真实波动,而是权益分配造成的名义价格调整。复权的目的就是消除这些事件对价格连续性的影响。

4.2.1 复权因子定义

复权因子(Adjust Factor)是一个累积乘数序列,用于将历史价格调整到与当前价格可比的水平。设复权因子序列为 adj_factor[t],则复权价格计算公式为:

# 复权价格 = 原始价格 × 复权因子
adj_close[t] = close[t] * adj_factor[t]

# 复权因子通常由除权除息事件计算得出
# 对于前复权:adj_factor[t] = 当前复权因子 / t时刻复权因子
# 对于后复权:adj_factor[t] = t时刻复权因子 / 基准日复权因子

4.2.2 前复权 vs 后复权

对比维度 前复权(Forward Adjust) 后复权(Backward Adjust)
调整方向 调整历史价格,保持最新价格不变 调整历史价格,保持最早价格不变
当前价格 等于实际交易价格 不等于实际交易价格
历史价格 被下调(分红后历史价更低) 被上调(分红后历史价更高)
收益率计算 直接使用复权价计算,无需额外处理 直接使用复权价计算,无需额外处理
回测适用性 最常用,回测信号与实盘价格一致 较少用于回测,历史价格失真
成交量调整 通常不调整成交量 通常不调整成交量
回测实践建议: 绝大多数量化框架(如Backtrader、Zipline、vnpy)默认使用前复权数据。前复权的优势在于:最新价格与市场实时价格一致,便于信号执行和滑点计算。后复权主要用于长期收益率分析或指数编制。

4.2.3 复权因子计算流程

复权因子的计算依赖于除权除息事件表。以下是一个简化的计算步骤:

  1. 获取事件数据: 从数据源获取分红、送股、配股、拆细等事件的除权除息日、每股分红、送股比例、配股比例、配股价等信息。
  2. 计算除权参考价: 除权参考价 = (前收盘价 - 每股现金红利 + 配股价 × 配股比例) / (1 + 送股比例 + 配股比例)
  3. 计算单次复权因子: 单次因子 = 前收盘价 / 除权参考价
  4. 累积复权因子: 从最早交易日开始,将单次因子累积连乘,得到每日的累积复权因子。
  5. 生成前复权价格: 前复权价格 = 原始价格 × (最新累积因子 / 当日累积因子)
import pandas as pd
import numpy as np

def calc_forward_adjust_factor(events_df, price_df):
    """
    计算前复权因子
    events_df: 包含除权除息事件的DataFrame,字段:date, cash_div, stock_div, rights_ratio, rights_price
    price_df: 包含原始价格的DataFrame,字段:date, close
    """
    # 按日期排序
    events = events_df.sort_values('date')
    
    # 计算单次复权因子
    events['factor'] = (events['pre_close'] / 
                        (events['pre_close'] - events['cash_div'] + 
                         events['rights_ratio'] * events['rights_price']) /
                        (1 + events['stock_div'] + events['rights_ratio']))
    
    # 累积复权因子(从最早到最晚)
    events['cum_factor'] = events['factor'].cumprod()
    
    # 前复权:最新因子为1,历史因子 = 最新累积因子 / 当日累积因子
    latest_cum = events['cum_factor'].iloc[-1]
    events['adj_factor'] = latest_cum / events['cum_factor']
    
    # 合并到价格数据
    price_df = price_df.merge(events[['date', 'adj_factor']], on='date', how='left')
    price_df['adj_factor'] = price_df['adj_factor'].fillna(1.0)
    
    # 计算前复权价格
    price_df['adj_close'] = price_df['close'] * price_df['adj_factor']
    
    return price_df
常见陷阱: 复权因子计算时,成交量(Volume)和成交额(Amount)通常不做复权调整。但送股后流通股本增加,若需计算换手率等指标,应手动调整成交量:复权成交量 = 原始成交量 × (1 + 送股比例)。此外,复权因子序列必须严格按时间顺序计算,不能跨股票合并。

4.3 时间索引对齐

在回测样本构建中,时间索引对齐是确保特征、标签、价格三者时间一致性的关键步骤。常见问题包括:交易日历不一致、时区偏移、数据频率不匹配、停牌日缺失等。

4.3.1 对齐原则

  • 严格时间戳: 所有数据使用统一的datetime64[ns]或Timestamp类型,避免字符串比较。
  • 交易日历统一: 使用统一的交易日历(如A股使用上交所/深交所交易日历),剔除周末和法定节假日。
  • 前向填充: 对于停牌日或缺失日,使用前一个交易日的价格进行填充,避免引入未来信息。
  • 后向填充: 仅在标签计算时使用,确保标签窗口内数据完整。

4.3.2 对齐流程

def align_time_index(price_df, feature_df, label_df, trade_calendar):
    """
    对齐价格、特征、标签的时间索引
    trade_calendar: 交易日列表(datetime64)
    """
    # 1. 统一索引类型
    price_df.index = pd.to_datetime(price_df.index)
    feature_df.index = pd.to_datetime(feature_df.index)
    label_df.index = pd.to_datetime(label_df.index)
    
    # 2. 对齐到交易日历
    aligned_price = price_df.reindex(trade_calendar, method='ffill')
    aligned_feature = feature_df.reindex(trade_calendar, method='ffill')
    aligned_label = label_df.reindex(trade_calendar, method='ffill')
    
    # 3. 删除非交易日(如果日历包含非交易日)
    aligned_price = aligned_price.loc[aligned_price.index.isin(trade_calendar)]
    aligned_feature = aligned_feature.loc[aligned_feature.index.isin(trade_calendar)]
    aligned_label = aligned_label.loc[aligned_label.index.isin(trade_calendar)]
    
    # 4. 合并为一个DataFrame
    combined = pd.concat({
        'price': aligned_price,
        'feature': aligned_feature,
        'label': aligned_label
    }, axis=1)
    
    # 5. 删除任何包含NaN的行(可选,根据策略决定)
    combined = combined.dropna()
    
    return combined

4.3.3 时间索引对齐流程图

时间索引对齐流程 原始OHLCV数据 特征数据 标签数据 步骤1:统一索引类型 → datetime64[ns] 步骤2:使用统一交易日历 reindex + ffill 步骤3:按时间轴 concat 合并,删除缺失行 对齐后的回测样本 DataFrame 关键:前向填充(ffill)避免未来信息;后向填充(bfill)仅用于标签窗口

4.4 实战:构建对齐后的回测样本

以下是一个完整的实战流程,将OHLCV数据、复权因子处理和时间索引对齐整合在一起:

import pandas as pd
import numpy as np
from datetime import datetime

def build_backtest_sample(raw_price_path, events_path, trade_calendar_path, 
                          start_date='2018-01-01', end_date='2023-12-31'):
    """
    构建对齐后的回测样本
    """
    # 1. 加载原始数据
    price_df = pd.read_csv(raw_price_path, parse_dates=['date'], index_col='date')
    events_df = pd.read_csv(events_path, parse_dates=['date'])
    trade_calendar = pd.read_csv(trade_calendar_path, parse_dates=['date'])['date']
    
    # 2. 过滤日期范围
    price_df = price_df.loc[start_date:end_date]
    events_df = events_df[(events_df['date'] >= start_date) & 
                          (events_df['date'] <= end_date)]
    trade_calendar = trade_calendar[(trade_calendar >= start_date) & 
                                    (trade_calendar <= end_date)]
    
    # 3. 计算前复权因子
    price_df = calc_forward_adjust_factor(events_df, price_df)
    
    # 4. 生成特征(示例:过去5日收益率)
    price_df['ret_5d'] = price_df['adj_close'].pct_change(5)
    price_df['vol_ma5'] = price_df['volume'].rolling(5).mean()
    
    # 5. 生成标签(示例:未来5日收益率)
    price_df['label_fwd_5d'] = price_df['adj_close'].pct_change(-5).shift(-5)
    
    # 6. 时间索引对齐
    feature_cols = ['ret_5d', 'vol_ma5']
    label_cols = ['label_fwd_5d']
    
    aligned = align_time_index(
        price_df[['adj_close', 'adj_factor']], 
        price_df[feature_cols], 
        price_df[label_cols], 
        trade_calendar
    )
    
    # 7. 删除标签为NaN的行(未来数据不可用)
    aligned = aligned.dropna(subset=['label'])
    
    return aligned

# 使用示例
sample = build_backtest_sample(
    raw_price_path='stock_000001.csv',
    events_path='events_000001.csv',
    trade_calendar_path='trade_calendar.csv'
)
print(f"回测样本形状: {sample.shape}")
print(sample.head())
关键检查点: 在构建完成后,务必验证以下三点:
1. 时间索引连续性: 相邻交易日之间的间隔应为1个自然日(排除周末和节假日)。
2. 复权价格单调性: 前复权价格序列不应出现因除权导致的跳空缺口。
3. 标签无未来信息: 标签计算时使用的未来数据必须严格位于当前时间之后,且不能包含当前时刻的信息。

4.5 常见问题与避坑指南

问题 现象 原因 解决方案
复权价格出现负值 历史价格变为负数 复权因子计算错误,或除权除息事件数据缺失 检查事件数据完整性,确保每股分红不超过前收盘价
时间索引错位 特征与标签日期不匹配 使用了不同的交易日历或时区未统一 统一使用UTC时间,并强制对齐到同一交易日历
未来信息泄露 回测收益异常高 标签计算时使用了未来数据,或前向填充不当 严格使用shift()确保标签滞后;前向填充只用于价格
停牌日数据缺失 样本数量减少 停牌日没有交易数据 使用前向填充填充价格,但特征和标签应标记为NaN
重要提醒: 在回测中,绝对不要使用后复权价格进行信号计算。后复权价格会扭曲历史价格水平,导致基于价格阈值的策略(如突破策略)产生虚假信号。始终使用前复权价格,并确保最新价格与实盘一致。