14、信号回测框架:回测引擎搭建、交易成本模拟、绩效评估指标

做量化交易,最怕什么?

怕策略在历史数据上跑得漂亮,一上实盘就翻车。我见过太多人,拿着一个回测年化50%的曲线,兴冲冲地杀进去,结果三个月亏掉裤衩。为什么?因为回测框架没搭好,成本没算对,绩效指标只看夏普比率。

这一章,我就把回测框架的底裤扒干净。从引擎搭建到成本模拟,再到绩效评估,咱们一步步来。

14.1 回测引擎的核心架构

回测引擎说白了,就是一个时间机器。你把历史数据喂进去,它模拟你的策略在过去会怎么交易,最后告诉你赚了还是亏了。

我个人习惯把引擎拆成四个模块:

  • 数据模块:负责喂数据。包括行情数据(OHLCV)、期权链数据、波动率曲面数据等。
  • 策略模块:你的交易逻辑。比如“当隐含波动率高于历史波动率20%时,卖出跨式期权”。
  • 风控模块:检查交易是否合规。比如仓位限制、保证金是否充足、是否触及止损线。
  • 执行模块:模拟订单成交。包括滑点、手续费、冲击成本等。

嗯,这里要注意:数据模块和策略模块必须解耦。我在项目中遇到过,有人把数据清洗逻辑直接写在策略里,结果换了个数据源,整个策略都得重写。血的教训。

核心原则:回测引擎应该像乐高积木,每个模块独立,可以随时替换。数据源换了?换数据模块就行。交易品种变了?换执行模块就行。

14.2 交易成本模拟——别让成本吃掉你的利润

很多新手回测时,只算手续费。但实际交易中,成本远不止这些。

我列一下期权交易中常见的成本项:

成本类型 说明 典型值(期权)
手续费 交易所和券商收取的固定费用 每张1-5元
滑点 实际成交价与信号价之间的偏差 0.5-2个tick
冲击成本 大单交易导致价格向不利方向移动 视流动性而定
保证金占用成本 卖出期权需要缴纳保证金,这部分资金的机会成本 年化3-5%
展期成本 期权到期后,需要滚动到下一个合约 视期限结构而定

我曾经犯过一个错:回测时只算了手续费,没算滑点。结果策略在实盘里,每笔交易都比回测多亏了2个tick。一个月下来,回测赚了5%,实盘亏了2%。你说冤不冤?

我的建议:回测时至少模拟三种成本场景——乐观(低滑点、低手续费)、中性(中等滑点、中等手续费)、悲观(高滑点、高手续费)。如果你的策略在悲观场景下还能赚钱,那实盘才有戏。

14.3 绩效评估指标——别只看夏普比率

回测跑完了,一堆数字摆在你面前。怎么看?

很多人只看两个指标:总收益率和夏普比率。但说实话,这两个指标很容易骗人。我见过一个策略,夏普比率2.5,但最大回撤40%。你敢用吗?

我个人习惯用下面这套指标体系:

  • 收益类:年化收益率、累计收益率、超额收益率(相对基准)
  • 风险类:最大回撤、年化波动率、VaR(在险价值)
  • 风险调整收益类:夏普比率、索提诺比率(只考虑下行波动)、卡玛比率(年化收益/最大回撤)
  • 交易特征类:胜率、盈亏比、平均持仓周期、交易频率

你想想看,如果一个策略胜率只有30%,但盈亏比是5:1,那它可能比一个胜率70%、盈亏比1:1的策略更赚钱。为什么?因为前者亏的时候亏小钱,赚的时候赚大钱。这就是期权策略的典型特征。

避坑指南:我曾经被一个策略的夏普比率骗过。回测显示夏普2.0,但仔细一看,它的收益集中在最后一个月,前面11个月都在亏。这种“尾部风险”策略,夏普比率完全失效。所以,一定要看收益的分布是否均匀。

14.4 回测框架的SVG流程图

下面这张图,是我做回测框架时的核心逻辑。你可以把它当作一个检查清单:

回测框架核心流程 数据模块 策略模块 风控模块 执行模块 绩效评估 行情数据 期权链数据 波动率曲面 交易信号生成 仓位计算 保证金检查 止损/止盈 滑点模拟 手续费计算 夏普比率 最大回撤

14.5 代码示例:一个简单的回测引擎骨架

光说不练假把式。下面是一个极简的回测引擎代码,你可以把它当作模板:

class BacktestEngine:
    def __init__(self, data, strategy, cost_model):
        self.data = data          # 数据模块
        self.strategy = strategy  # 策略模块
        self.cost_model = cost_model  # 成本模型
        self.portfolio = Portfolio()  # 投资组合
    
    def run(self):
        for timestamp, market_data in self.data.iterrows():
            # 1. 策略生成信号
            signal = self.strategy.generate_signal(market_data)
            
            # 2. 风控检查
            if not self.risk_check(signal):
                continue
            
            # 3. 执行交易(含成本)
            trade = self.execute(signal, market_data)
            
            # 4. 更新投资组合
            self.portfolio.update(trade)
        
        # 5. 绩效评估
        return self.evaluate_performance()
    
    def execute(self, signal, market_data):
        # 模拟滑点
        fill_price = market_data['price'] * (1 + signal.direction * self.cost_model.slippage)
        # 计算手续费
        commission = self.cost_model.commission * signal.quantity
        return Trade(signal, fill_price, commission)
    
    def evaluate_performance(self):
        # 计算各项指标
        return {
            'total_return': self.portfolio.total_return,
            'sharpe_ratio': self.portfolio.sharpe_ratio,
            'max_drawdown': self.portfolio.max_drawdown,
            'win_rate': self.portfolio.win_rate
        }

这段代码虽然简单,但骨架是完整的。你可以在策略模块里塞进任何期权策略——跨式、宽跨式、蝶式、日历价差,都行。

一个小技巧:回测时,我习惯把每一笔交易的明细都记录下来,包括开仓时间、平仓时间、盈亏、成本等。这样事后分析时,可以快速定位问题。比如,发现某个月亏了很多,一查,原来是那段时间滑点特别大。

14.6 绩效评估的实战要点

最后,聊几个实战中容易踩的坑:

  • 幸存者偏差:回测时只用了存活下来的期权合约,忽略了那些退市的。结果回测收益虚高。解决办法:用全量数据,包括已退市的。
  • 前视偏差:回测时用了未来数据。比如,用当天的收盘价来生成当天的交易信号。这相当于作弊。解决办法:确保信号生成时,只用历史数据。
  • 过拟合:策略参数太多,专门拟合历史数据。结果一到未来就失效。解决办法:用样本外测试,或者做交叉验证。

嗯,说到过拟合,我想起一个案例。有个朋友,花了三个月优化一个期权策略,回测曲线完美得像教科书。结果实盘一个月,亏了20%。为什么?因为他把参数调到了极致,专门拟合了过去三年的数据。但市场风格一变,策略就废了。

所以,我的原则是:参数越少越好,逻辑越简单越好。复杂的策略不一定赚钱,但一定容易过拟合。


公众号:蓝海资料掘金营,微信deep3321