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.concat或merge来对齐多个品种的数据。
# 假设有两个品种的数据
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())
避坑指南:我曾经在重采样时忘记处理非交易时间的数据,结果把夜盘和日盘的数据混在一起,算出来的指标完全不对。记住,重采样前先过滤掉非交易时段的数据。
知识体系总览
下面这张图帮你理清本章的知识结构:
数据这块,说白了就是“进来的是垃圾,出去的也是垃圾”。把数据结构设计好,后面的回测才能跑得稳。我个人经验是,花70%的时间在数据处理上都不为过。嗯,今天就先聊到这儿,你先把这些基础操作练熟,后面咱们再深入策略实现。