4. 数据结构设计:OHLCV数据结构、DataFrame操作技巧、时间序列索引、数据对齐与重采样

做回测系统,说白了就是跟数据打交道。我见过太多人策略写得漂亮,结果数据一塌糊涂,回测结果根本没法看。今天咱们就把数据这块地基打牢。

4.1 OHLCV数据结构:金融数据的“通用语言”

OHLCV,这五个字母你肯定不陌生。Open(开盘价)、High(最高价)、Low(最低价)、Close(收盘价)、Volume(成交量)。这是金融时间序列最基础的数据结构。

我个人习惯,拿到数据第一件事就是检查这五列的完整性。缺一列,后面的计算全得跑偏。

核心要点:

  • Open:每个时间周期的第一笔成交价
  • High:周期内的最高成交价
  • Low:周期内的最低成交价
  • Close:周期内的最后一笔成交价
  • Volume:周期内的总成交量

为什么是这五个?你想想看,一根K线图就是靠它们画出来的。没有High和Low,你就不知道波动范围;没有Volume,你就看不出资金参与度。

我在项目中遇到过一个问题:某数据源把“复权因子”混在OHLCV里,导致计算收益率时出现负数。嗯,这里要注意,OHLCV必须是原始价格,复权操作要单独处理。

4.2 DataFrame操作技巧:Pandas才是真功夫

Python做量化,Pandas是绕不开的。DataFrame操作熟练了,回测效率能翻倍。

4.2.1 数据加载与清洗

我建议你从一开始就养成好习惯:数据加载后立刻做类型检查和缺失值处理。

import pandas as pd
import numpy as np

# 加载数据
df = pd.read_csv('stock_data.csv', parse_dates=['date'], index_col='date')

# 快速检查
print(df.info())
print(df.isnull().sum())

# 处理缺失值:前向填充
df.fillna(method='ffill', inplace=True)

4.2.2 列操作与计算

计算收益率、移动平均线这些,用Pandas的向量化操作,比循环快几十倍。

# 计算日收益率
df['return'] = df['close'].pct_change()

# 计算5日均线
df['ma5'] = df['close'].rolling(window=5).mean()

# 计算ATR(平均真实波幅)
df['tr'] = np.maximum(
    df['high'] - df['low'],
    np.maximum(
        abs(df['high'] - df['close'].shift(1)),
        abs(df['low'] - df['close'].shift(1))
    )
)
df['atr'] = df['tr'].rolling(window=14).mean()

我曾经犯过一个低级错误:用循环计算移动平均线,数据量一大,回测跑了半小时还没出结果。后来改成向量化操作,几秒钟搞定。说白了,能用Pandas内置函数就别自己写循环。

4.2.3 条件筛选与切片

回测中经常要筛选特定条件的数据,比如“涨幅超过5%的交易日”。

# 筛选涨幅超过5%的交易日
high_return_days = df[df['return'] > 0.05]

# 筛选成交量放大2倍以上的交易日
volume_surge = df[df['volume'] > df['volume'].shift(1) * 2]

# 多条件组合
signal = df[(df['ma5'] > df['ma20']) & (df['volume'] > df['volume'].rolling(20).mean())]

小技巧:.loc进行标签索引,用.iloc进行位置索引。混合使用时容易搞混,我建议统一用.loc,配合布尔索引更直观。

4.3 时间序列索引:回测的“时间轴”

时间序列索引是回测系统的灵魂。没有正确的时间索引,你的策略就不知道什么时候该买,什么时候该卖。

4.3.1 设置时间索引

我习惯把日期列设为索引,这样切片、重采样都方便。

# 设置时间索引
df.set_index('date', inplace=True)

# 确保索引是datetime类型
df.index = pd.to_datetime(df.index)

# 排序
df.sort_index(inplace=True)

4.3.2 时间切片

回测时经常要取某段时间的数据,比如“2020年1月到2021年12月”。

# 字符串切片
df_2020 = df['2020']
df_2020_2021 = df['2020-01':'2021-12']

# 精确到日
df_specific = df['2020-06-01':'2020-06-30']

# 用loc
df_range = df.loc['2020-01-01':'2020-12-31']

注意:时间切片是左闭右闭的,跟Python列表的切片不一样。我刚开始做回测时就被这个坑过,取出来的数据总是多一天或少一天。

4.3.3 频率转换

不同策略需要不同频率的数据。日线策略用日数据,分钟策略用分钟数据。

# 日线转周线
df_weekly = df.resample('W').agg({
    'open': 'first',
    'high': 'max',
    'low': 'min',
    'close': 'last',
    'volume': 'sum'
})

# 日线转月线
df_monthly = df.resample('M').agg({
    'open': 'first',
    'high': 'max',
    'low': 'min',
    'close': 'last',
    'volume': 'sum'
})

4.4 数据对齐与重采样:多品种回测的关键

做多品种回测时,数据对齐是个大问题。不同股票的交易日期不一样,停牌、节假日都会导致数据缺失。

4.4.1 数据对齐

我建议用pd.concatmerge来对齐多个品种的数据。

# 假设有两个品种的数据
df1 = pd.read_csv('stock_a.csv', index_col='date', parse_dates=True)
df2 = pd.read_csv('stock_b.csv', index_col='date', parse_dates=True)

# 合并对齐
combined = pd.concat({'stock_a': df1['close'], 'stock_b': df2['close']}, axis=1)

# 只保留共同交易日
common_dates = combined.dropna()

4.4.2 重采样技巧

重采样不只是转换频率,还能做很多有用的操作。

# 日线转小时线(需要分钟数据)
df_hourly = df_minute.resample('1H').agg({
    'open': 'first',
    'high': 'max',
    'low': 'min',
    'close': 'last',
    'volume': 'sum'
})

# 自定义重采样:取最后5分钟的均价
df_last5min = df_minute.resample('1D').apply(lambda x: x.tail(5).mean())

避坑指南:我曾经在重采样时忘记处理非交易时间的数据,结果把夜盘和日盘的数据混在一起,算出来的指标完全不对。记住,重采样前先过滤掉非交易时段的数据。

知识体系总览

下面这张图帮你理清本章的知识结构:

数据结构设计 OHLCV数据结构 Open / High / Low Close / Volume DataFrame操作技巧 数据加载 / 清洗 向量化计算 条件筛选 时间序列索引 索引设置 时间切片 频率转换 数据对齐与重采样 多品种对齐 重采样技巧 核心逻辑 数据是回测系统的基石 OHLCV → DataFrame → 时间索引 → 对齐重采样 每一步都影响回测结果的准确性 数据质量 = 回测质量

数据这块,说白了就是“进来的是垃圾,出去的也是垃圾”。把数据结构设计好,后面的回测才能跑得稳。我个人经验是,花70%的时间在数据处理上都不为过。嗯,今天就先聊到这儿,你先把这些基础操作练熟,后面咱们再深入策略实现。

专注资料整理