第二十九节:量化工具——Python实现基差交易策略回测框架

做基差交易,光有理论可不够。

你得有工具,能快速验证你的想法。我见过太多人,脑子里有个策略觉得稳赚,结果一跑回测,亏得底裤都不剩。所以今天,咱们就动手搭一个基差交易策略的回测框架。

说白了,就是用Python把「库存-价差」的逻辑,变成可执行的代码。

29.1 回测框架的核心模块

一个完整的回测框架,至少需要四个部分:

  • 数据模块:负责拉取期货价格、现货价格、库存数据
  • 信号模块:根据库存和价差的关系,生成买卖信号
  • 执行模块:模拟下单、成交、滑点
  • 绩效模块:计算收益率、夏普比率、最大回撤

我个人习惯,先把数据模块搞定。数据不干净,后面全是白搭。

核心逻辑:基差 = 期货价格 - 现货价格。库存高,基差往往走弱;库存低,基差容易走强。我们就是抓这个规律。

29.2 数据准备与预处理

先看数据长什么样。我一般用pandas的DataFrame来存。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# 模拟数据:日期、期货价格、现货价格、库存
dates = pd.date_range('2023-01-01', periods=500, freq='D')
df = pd.DataFrame({
    'date': dates,
    'futures': np.random.randn(500).cumsum() + 100,
    'spot': np.random.randn(500).cumsum() + 98,
    'inventory': np.random.randn(500).cumsum() + 200
})

# 计算基差
df['basis'] = df['futures'] - df['spot']

# 计算库存的Z-score(标准化)
df['inv_z'] = (df['inventory'] - df['inventory'].rolling(60).mean()) / df['inventory'].rolling(60).std()

# 计算基差的Z-score
df['basis_z'] = (df['basis'] - df['basis'].rolling(60).mean()) / df['basis'].rolling(60).std()

print(df.head())

嗯,这里要注意。滚动窗口选多少?我试过20天、60天、120天。对于大多数商品,60天比较稳。太短了噪音大,太长了反应慢。

29.3 信号生成逻辑

信号怎么来?核心就一句话:库存极端 + 基差极端 = 交易机会

我常用的规则是这样的:

  • 当库存Z-score > 1.5 且 基差Z-score < -1.0,做多基差(买现货,卖期货)
  • 当库存Z-score < -1.5 且 基差Z-score > 1.0,做空基差(卖现货,买期货)
  • 其他情况,持仓不动或平仓

你想想看,库存高说明供应过剩,基差弱说明期货比现货贵得不多,这时候做多基差,赌的就是库存回归。

# 生成信号
df['signal'] = 0

# 做多基差信号
long_condition = (df['inv_z'] > 1.5) & (df['basis_z'] < -1.0)
df.loc[long_condition, 'signal'] = 1

# 做空基差信号
short_condition = (df['inv_z'] < -1.5) & (df['basis_z'] > 1.0)
df.loc[short_condition, 'signal'] = -1

# 信号不能频繁变动,我习惯做一次持仓判断
df['position'] = df['signal'].replace(0, method='ffill').fillna(0)

print(df[df['signal'] != 0].head())

避坑指南:我曾经犯过一个错,信号生成后直接用于回测,结果发现每天买卖几十次,手续费把利润全吃掉了。后来我加了持仓判断,信号变了才交易,这才像样。

29.4 回测执行与绩效计算

执行模块,说白了就是算每天的盈亏。基差交易,我们赚的是基差回归的钱。

# 计算每日基差变化
df['basis_change'] = df['basis'].diff()

# 策略收益 = 持仓方向 * 基差变化
df['strategy_return'] = df['position'].shift(1) * df['basis_change']

# 累计收益
df['cum_return'] = (1 + df['strategy_return']).cumprod()

# 绩效指标
total_return = df['cum_return'].iloc[-1] - 1
annual_return = total_return / (len(df) / 252)
sharpe = np.sqrt(252) * df['strategy_return'].mean() / df['strategy_return'].std()
max_drawdown = (df['cum_return'] / df['cum_return'].cummax() - 1).min()

print(f"总收益率: {total_return:.2%}")
print(f"年化收益率: {annual_return:.2%}")
print(f"夏普比率: {sharpe:.2f}")
print(f"最大回撤: {max_drawdown:.2%}")

夏普比率低于1,我基本不会实盘。低于0.5,直接放弃。

29.5 可视化与框架图

光看数字不够直观。我习惯把策略表现画出来。

fig, axes = plt.subplots(3, 1, figsize=(12, 10))

# 基差与库存
axes[0].plot(df['date'], df['basis'], label='Basis', color='blue')
axes[0].plot(df['date'], df['inventory']/10, label='Inventory/10', color='green')
axes[0].legend()
axes[0].set_title('Basis vs Inventory')

# 信号
axes[1].plot(df['date'], df['position'], label='Position', color='red', drawstyle='steps-post')
axes[1].legend()
axes[1].set_title('Trading Signal')

# 累计收益
axes[2].plot(df['date'], df['cum_return'], label='Cumulative Return', color='purple')
axes[2].legend()
axes[2].set_title('Strategy Performance')

plt.tight_layout()
plt.show()

下面这张SVG图,是我自己总结的基差回测框架流程。你看一眼就明白了。

基差交易策略回测框架 数据输入 期货/现货/库存 数据预处理 计算基差/Z-score 信号生成 库存+基差阈值 回测执行 模拟交易/计算盈亏 绩效评估 夏普/回撤/收益 参数优化迭代 核心逻辑:库存驱动基差回归

29.6 完整框架封装

最后,我把上面这些代码封装成一个类。以后换品种,改参数就行。

class BasisTradingBacktest:
    def __init__(self, data, window=60, inv_threshold=1.5, basis_threshold=1.0):
        self.data = data.copy()
        self.window = window
        self.inv_threshold = inv_threshold
        self.basis_threshold = basis_threshold
        
    def prepare_data(self):
        self.data['basis'] = self.data['futures'] - self.data['spot']
        self.data['inv_z'] = (self.data['inventory'] - 
                              self.data['inventory'].rolling(self.window).mean()) / \
                              self.data['inventory'].rolling(self.window).std()
        self.data['basis_z'] = (self.data['basis'] - 
                                self.data['basis'].rolling(self.window).mean()) / \
                                self.data['basis'].rolling(self.window).std()
        
    def generate_signals(self):
        self.data['signal'] = 0
        long_cond = (self.data['inv_z'] > self.inv_threshold) & \
                    (self.data['basis_z'] < -self.basis_threshold)
        short_cond = (self.data['inv_z'] < -self.inv_threshold) & \
                     (self.data['basis_z'] > self.basis_threshold)
        self.data.loc[long_cond, 'signal'] = 1
        self.data.loc[short_cond, 'signal'] = -1
        self.data['position'] = self.data['signal'].replace(0, method='ffill').fillna(0)
        
    def run(self):
        self.prepare_data()
        self.generate_signals()
        self.data['basis_change'] = self.data['basis'].diff()
        self.data['strategy_return'] = self.data['position'].shift(1) * self.data['basis_change']
        self.data['cum_return'] = (1 + self.data['strategy_return']).cumprod()
        return self.data

# 使用示例
bt = BasisTradingBacktest(df, window=60, inv_threshold=1.5, basis_threshold=1.0)
result = bt.run()
print(result[['date', 'basis', 'position', 'cum_return']].tail())

注意:这个框架是简化版。实盘要考虑滑点、手续费、保证金、展期成本。我见过有人回测年化30%,实盘一跑直接亏10%,就是因为没算这些细节。

好了,框架搭完了。你拿自己的数据跑一跑,调调参数,很快就能找到感觉。记住,回测只是起点,不是终点。

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