回测系统架构:回测引擎核心组件

做量化交易这些年,我见过太多人一上来就写回测代码。结果呢?数据对不上,策略逻辑跑偏,绩效算出来自己都不敢信。说白了,回测引擎不是随便写个循环就能糊弄过去的。它得有清晰的架构,每个模块各司其职。

我个人习惯把回测引擎拆成四个核心模块:数据模块、策略模块、风控模块、绩效模块。你想想看,这就像一条流水线——数据进来,策略做决策,风控把关,最后算账。哪个环节出问题,结果都不靠谱。

数据模块:地基要稳

数据模块负责两件事:数据清洗数据对齐

我在项目中遇到过最坑的事——某次回测跑出来年化收益30%,结果发现是数据里混入了未来信息。嗯,前复权价格没处理好,把未来的分红算进去了。从那以后,我强制要求数据模块必须做三件事:

  • 去重:同一时间戳出现多条记录,保留最后一条
  • 填充:缺失值用前向填充,绝不插值
  • 对齐:多品种数据按时间戳对齐,缺失的直接丢弃
我曾经... 因为数据对齐没做好,导致策略在A股和期货上同时回测时,信号错位了整整一个交易日。结果回测曲线漂亮得像假的,实盘一跑就崩。

策略模块:核心逻辑

策略模块就是你的交易逻辑。我建议把它设计成纯函数——输入行情数据,输出交易信号。别把状态混进去,否则调试起来想死的心都有。

举个例子,一个简单的均线策略:

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万组。一开始没优化,跑完要三天。后来做了数据预处理、并行化、缓存中间结果,时间压缩到两小时。你想想看,这效率提升有多大。

回测架构这件事,说到底就是模块化、可扩展、高性能。别想着一步到位,先跑起来,再逐步优化。毕竟,能跑的回测才有意义,跑得快的回测才能帮你快速迭代策略。

回测引擎核心架构图 数据模块 清洗 · 对齐 · 存储 策略模块 信号生成 · 参数管理 风控模块 止损 · 仓位 · 回撤 绩效模块 夏普 · 回撤 · 胜率 事件驱动架构 逐笔模拟 · 高精度 · 慢 向量化回测 批量计算 · 低精度 · 快 性能优化:数据预处理 · 向量化 · 缓存 · 并行