回测系统架构:回测引擎核心组件
做量化交易这些年,我见过太多人一上来就写回测代码。结果呢?数据对不上,策略逻辑跑偏,绩效算出来自己都不敢信。说白了,回测引擎不是随便写个循环就能糊弄过去的。它得有清晰的架构,每个模块各司其职。
我个人习惯把回测引擎拆成四个核心模块:数据模块、策略模块、风控模块、绩效模块。你想想看,这就像一条流水线——数据进来,策略做决策,风控把关,最后算账。哪个环节出问题,结果都不靠谱。
数据模块:地基要稳
数据模块负责两件事:数据清洗和数据对齐。
我在项目中遇到过最坑的事——某次回测跑出来年化收益30%,结果发现是数据里混入了未来信息。嗯,前复权价格没处理好,把未来的分红算进去了。从那以后,我强制要求数据模块必须做三件事:
- 去重:同一时间戳出现多条记录,保留最后一条
- 填充:缺失值用前向填充,绝不插值
- 对齐:多品种数据按时间戳对齐,缺失的直接丢弃
策略模块:核心逻辑
策略模块就是你的交易逻辑。我建议把它设计成纯函数——输入行情数据,输出交易信号。别把状态混进去,否则调试起来想死的心都有。
举个例子,一个简单的均线策略:
def strategy(data, params):
# data: DataFrame, 包含 close 列
# params: {'fast': 5, 'slow': 20}
fast_ma = data['close'].rolling(params['fast']).mean()
slow_ma = data['close'].rolling(params['slow']).mean()
# 生成信号:1 买入,-1 卖出,0 持仓
signals = pd.Series(0, index=data.index)
signals[fast_ma > slow_ma] = 1
signals[fast_ma < slow_ma] = -1
return signals
你看,逻辑清晰,没有副作用。测试起来也方便,换参数直接传就行。
风控模块:保命用的
风控模块很多人会忽略。我刚开始做回测时也不重视,直到有一次回测曲线完美,实盘却因为单笔亏损过大直接爆仓。
风控模块至少要做这几件事:
- 最大持仓限制:单品种仓位不超过总资金的20%
- 单笔止损:亏损超过2%强制平仓
- 最大回撤限制:总回撤超过15%暂停交易
绩效模块:算账的
绩效模块就是算账的。别只看年化收益率,那玩意儿骗人。我一般看这几个指标:
| 指标 | 说明 | 我的经验值 |
|---|---|---|
| 夏普比率 | 风险调整后收益 | 大于1.5才算合格 |
| 最大回撤 | 账户从峰值跌落的幅度 | 超过20%直接毙掉 |
| 胜率 | 盈利交易占比 | 别低于40% |
| 盈亏比 | 平均盈利/平均亏损 | 至少2:1 |
为什么我强调这些?因为年化30%但回撤40%的策略,实盘你根本拿不住。心理压力太大,半夜都睡不着。
事件驱动架构 vs 向量化回测
说到回测实现方式,主要有两种:事件驱动和向量化。很多人纠结选哪个,其实没那么复杂。
事件驱动架构
事件驱动,说白了就是模拟真实交易环境。市场每来一个tick或bar,引擎就触发一次事件,策略根据当前数据做决策。
优点很明显:
- 更接近实盘逻辑
- 支持复杂订单类型(限价单、止损单等)
- 可以处理异步事件(比如成交回报)
缺点也明显:
- 速度慢,尤其是回测几年数据时
- 代码复杂度高
我在项目中遇到过这样的情况——用事件驱动回测一个高频策略,跑完三年数据花了整整两个小时。后来换成向量化,十秒搞定。
向量化回测
向量化回测,就是一次性计算所有信号,然后模拟成交。用pandas的向量化操作,效率极高。
举个例子:
# 向量化计算信号
signals = strategy(data, params)
# 模拟成交
positions = signals.shift(1) # 信号延迟一期
returns = positions * data['close'].pct_change()
# 计算绩效
total_return = (1 + returns).cumprod()
你看,几行代码就搞定了。但向量化也有局限:
- 无法处理滑点和部分成交
- 不支持条件订单(比如止损单)
- 假设所有信号都能按收盘价成交
回测性能优化思路
回测跑得慢,很影响开发效率。我优化回测性能时,主要从这几个方向入手:
1. 数据预处理
别在回测循环里做数据清洗。提前把数据处理好,存成parquet或hdf5格式。我试过,读取速度比CSV快10倍以上。
2. 避免循环
能用向量化就别用for循环。pandas的rolling、apply、groupby这些操作,底层是C语言实现的,比Python循环快两个数量级。
3. 缓存中间结果
如果策略需要计算多个指标,把中间结果缓存起来。别每次都重新算。我习惯用lru_cache装饰器,简单有效。
from functools import lru_cache
@lru_cache(maxsize=128)
def compute_indicator(data_hash, params):
# 计算复杂指标
pass
4. 并行回测
参数优化时,用多进程并行跑。Python的GIL限制多线程,但多进程可以充分利用CPU。我一般用joblib库,几行代码就能实现并行。
from joblib import Parallel, delayed
def backtest(params):
# 单次回测
pass
results = Parallel(n_jobs=-1)(
delayed(backtest)(params) for params in param_grid
)
5. 精简日志
回测过程中别打印太多日志。IO操作很慢,尤其是控制台输出。我一般只在关键节点打印,比如每个品种回测完成时。
嗯,说到这里,我想起一个项目。当时回测一个多因子策略,参数组合有10万组。一开始没优化,跑完要三天。后来做了数据预处理、并行化、缓存中间结果,时间压缩到两小时。你想想看,这效率提升有多大。
回测架构这件事,说到底就是模块化、可扩展、高性能。别想着一步到位,先跑起来,再逐步优化。毕竟,能跑的回测才有意义,跑得快的回测才能帮你快速迭代策略。