数据模块进阶:多周期数据管理、Tick级数据回测、数据缓存与预加载策略
数据模块,说白了就是回测引擎的「粮草」。粮草跟不上,再好的策略也白搭。这一章我们聊聊进阶玩法——多周期怎么管、Tick级回测怎么做、缓存和预加载怎么搞。嗯,都是我在实战中踩过坑的地方。
多周期数据管理:别让时间错位坑了你
做量化的人,很少只盯着一个周期看。日线定方向,小时线找入场,15分钟线抓细节——这是标配。但多周期数据管理,远比你想象中麻烦。
核心问题就一个:时间对齐。
举个例子。你有一个策略,日线判断趋势,1小时线决定入场。回测时,日线收盘价是16:00,1小时线最后一根是15:00-16:00。这两个数据的时间戳怎么对齐?
我个人习惯的做法是:统一使用「左闭右开」的时间区间。也就是说,一根K线的时间戳,代表这根K线的起始时间。这样,日线2024-01-01 00:00:00,就包含了当天所有数据。1小时线2024-01-01 15:00:00,就包含了15:00到16:00的数据。对齐时,直接按起始时间匹配就行。
多周期数据对齐原则:
- 所有周期统一使用起始时间作为时间戳
- 长周期数据在短周期数据中「展开」时,使用前向填充(forward fill)
- 短周期数据向长周期「聚合」时,注意使用正确的聚合函数(OHLC)
我曾经在项目中遇到过一个问题:日线数据用的是收盘时间(16:00),1小时线用的是起始时间(15:00)。结果回测时,策略在15:30发出了买入信号,但日线数据还没更新——因为日线要等到16:00才收盘。这一错位,导致回测结果和实盘差了十万八千里。嗯,从那以后,我再也不敢混用时间戳了。
Tick级数据回测:细节是魔鬼
Tick级回测,说白了就是模拟每一笔成交。这玩意儿对系统性能要求极高,但有些策略非它不可——比如高频做市、盘口套利。
Tick数据的结构,一般长这样:
{
'symbol': 'BTCUSDT',
'timestamp': 1704067200000, // 毫秒级时间戳
'price': 43215.6,
'volume': 0.023,
'side': 'buy', // 主动买还是主动卖
'bid_price': 43215.5,
'ask_price': 43215.7,
'bid_volume': 12.5,
'ask_volume': 8.3
}
你想想看,如果每秒有几百笔Tick,一天下来就是几千万条数据。怎么处理?
我的做法是:按天分片存储,内存中只保留当天数据。回测时,按时间顺序逐笔回放。每来一笔Tick,就更新当前价格、买卖盘口,然后检查策略是否有信号。
Tick回测的优化技巧:
- 使用NumPy数组存储Tick数据,避免Python对象开销
- 预计算时间戳的差值,避免重复计算
- 只保留策略需要的字段,不要全量加载
这里有个坑。Tick数据里经常有「脏数据」——比如价格异常跳变、买卖盘口倒挂。我曾经在回测时没做过滤,结果策略在某个异常Tick上开了仓,后面价格又跳回来,直接亏了一笔。所以,Tick级回测一定要加数据清洗层。
数据缓存与预加载策略:让回测飞起来
回测引擎跑起来,最耗时的就是数据读取。尤其是多周期、多品种的回测,每次都要从磁盘读数据,那速度简直让人崩溃。
缓存策略,说白了就是「用空间换时间」。我常用的缓存架构是这样的:
class DataCache:
def __init__(self):
self._cache = {} # {symbol_period: DataFrame}
self._lru = [] # 最近使用顺序
def get(self, key):
if key in self._cache:
# 更新LRU顺序
self._lru.remove(key)
self._lru.append(key)
return self._cache[key]
return None
def put(self, key, data, max_size=100):
if len(self._cache) >= max_size:
# 淘汰最久未使用的
oldest = self._lru.pop(0)
del self._cache[oldest]
self._cache[key] = data
self._lru.append(key)
这个缓存用了LRU(最近最少使用)策略。回测时,如果策略同时用到BTC的日线和1小时线,那这两份数据就会被缓存起来。当内存不够时,最久没用的数据会被淘汰。
预加载策略,则是「提前把可能要用的数据准备好」。我个人习惯的做法是:
- 按时间窗口预加载:回测到第100天时,提前把第101-110天的数据加载到内存
- 按品种预加载:如果策略同时交易BTC和ETH,那这两个品种的数据一起加载
- 按周期预加载:如果策略用到日线和4小时线,那这两个周期的数据一起加载
注意:预加载不是越多越好。加载太多数据,内存撑不住,反而会因为频繁的GC(垃圾回收)拖慢速度。我一般控制在内存使用不超过总内存的30%。
为什么会这样?因为Python的内存管理有个特点——内存占用越高,GC越频繁。你想想看,如果回测引擎每跑几秒就触发一次GC,那速度还不如不缓存。所以,缓存和预加载要「适度」。
数据模块的整体架构
说了这么多,我们来画一张图,看看数据模块的整体架构长什么样。
这张图展示了数据模块的四层架构。从下往上,数据从原始来源经过清洗、缓存,最终提供给回测引擎使用。每一层都有明确的职责,层与层之间通过接口解耦。
我在项目中遇到过一个问题:数据清洗层和缓存层耦合太紧,导致清洗逻辑改了,缓存也要跟着改。后来我把它们拆开,清洗层只管「把数据洗干净」,缓存层只管「存和取」。这样改起来就轻松多了。
实战建议
说了这么多理论,来点实际的。如果你现在要搭建数据模块,我建议你按这个顺序来:
- 先搞定单周期数据:把日线数据读进来,能跑通回测再说
- 再扩展多周期:加上小时线、分钟线,注意时间对齐
- 最后上Tick级:如果策略需要,再引入Tick数据
- 缓存和预加载:等数据量大了,再考虑优化
一个小技巧:在开发阶段,可以用「数据采样」来加速调试。比如只加载最近100天的数据,或者只加载1%的Tick数据。等逻辑调通了,再全量跑。
嗯,数据模块这块,说白了就是「细节决定成败」。时间对齐、脏数据过滤、缓存策略——每一个环节都可能成为回测的瓶颈。但只要你把架构搭好了,后面就省心了。
公众号:蓝海资料掘金营,微信deep3321