Skip to content
All library documents

Mean-Reversion Trading in a Three-Leg Refining Spread

Code TqSdk

Summary

The strategy models a refining spread using crude oil, fuel oil, and a third petroleum product in a 3:2:1 weighting. It calculates the spread as the weighted value of the two product legs minus the weighted crude leg, then compares the current spread with its historical mean and standard deviation over a 60-day lookback. A z-score beyond 1.5 standard deviations opens a position betting on convergence: sell the spread when it is high and buy it when low. Positions target fixed leg quantities and close at the mean or after a maximum holding period of 10 days.

The document includes a backtest setup for November 2023 through April 2024, but reports no results. The example uses selected contract symbols and hard-coded ratios, and does not establish that the legs are economically or statistically hedged. It also lacks explicit treatment of transaction costs, slippage, contract rolls, changing spread relationships, or protection against zero or unstable standard deviation. These omissions limit what can be inferred from the sample implementation.

Key ideas

  • The spread is the weighted value of two refined-product futures legs minus the weighted crude-oil leg.
  • A 60-day historical mean and standard deviation define the spread’s z-score.
  • The strategy enters when the z-score exceeds 1.5 standard deviations in either direction.
  • It closes positions at the mean or after a stated maximum holding period of 10 days.
  • The sample specifies a backtest period but gives no performance results or cost analysis.

Tags

Full text
# crack_spread_arbitrage.py


```py
#!/usr/bin/env python
# coding=utf-8
"""
裂解价差均值回归策略
基于原油及其主要炼化产品之间的价格关系在短期内可能偏离其长期均衡水平,并最终回归的假设
"""

from tqsdk import TqApi, TqAuth, TqKq, TargetPosTask, TqBacktest, BacktestFinished
from datetime import date
import numpy as np
import time

# === 用户参数 ===
# 合约参数
CRUDE_OIL = "SHFE.sc2406"  # 原油期货合约
GASOLINE = "SHFE.fu2406"   # 燃料油期货合约
DIESEL = "INE.nr2406"      # 柴油期货合约

# 回测参数
START_DATE = date(2023, 11, 1)  # 回测开始日期
END_DATE = date(2024, 4, 1)     # 回测结束日期

# 裂解比例 - 3:2:1 裂解价差
OIL_RATIO = 3        # 原油比例
GAS_RATIO = 2        # 汽油比例
DIESEL_RATIO = 1     # 柴油比例

# 套利参数
LOOKBACK_DAYS = 60         # 计算历史价差的回溯天数
DEVIATION_THRESHOLD = 1.5  # 偏离阈值(标准差倍数)
OIL_LOTS = 5               # 原油交易手数
CLOSE_AT_MEAN = True       # 是否在价差回归到均值时平仓
MAX_HOLDING_DAYS = 10      # 最大持仓天数

# === 初始化API ===
api = TqApi(backtest=TqBacktest(start_dt=START_DATE, end_dt=END_DATE),
            auth=TqAuth("你的天勤账号", "你的天勤密码"))

# 获取合约行情和K线
crude_quote = api.get_quote(CRUDE_OIL)
gasoline_quote = api.get_quote(GASOLINE)
diesel_quote = api.get_quote(DIESEL)

crude_klines = api.get_kline_serial(CRUDE_OIL, 60*60*24, LOOKBACK_DAYS)
gasoline_klines = api.get_kline_serial(GASOLINE, 60*60*24, LOOKBACK_DAYS)
diesel_klines = api.get_kline_serial(DIESEL, 60*60*24, LOOKBACK_DAYS)

# 创建目标持仓任务
crude_pos = TargetPosTask(api, CRUDE_OIL)
gasoline_pos = TargetPosTask(api, GASOLINE)
diesel_pos = TargetPosTask(api, DIESEL)

# 获取合约乘数
crude_volume_multiple = crude_quote.volume_multiple
gasoline_volume_multiple = gasoline_quote.volume_multiple
diesel_volume_multiple = diesel_quote.volume_multiple

# 计算汽油和柴油的交易手数(基于原油手数和裂解比例)
GAS_LOTS = round(GAS_RATIO / OIL_RATIO * OIL_LOTS)
DIESEL_LOTS = round(DIESEL_RATIO / OIL_RATIO * OIL_LOTS)

# 初始化状态变量
position_time = 0  # 建仓时间
in_position = False  # 是否有持仓
last_trade_direction = ""  # 上次交易方向 "BUY_SPREAD" 或 "SELL_SPREAD"
mean_spread = 0  # 历史价差均值
std_spread = 0  # 历史价差标准差

print(f"裂解价差套利策略启动")
print(f"监控合约: 原油({CRUDE_OIL}) - {OIL_LOTS}手, 汽油({GASOLINE}) - {GAS_LOTS}手, 柴油({DIESEL}) - {DIESEL_LOTS}手")
print(f"裂解比例: {OIL_RATIO}:{GAS_RATIO}:{DIESEL_RATIO}")

# === 主循环 ===
try:
    # 初始计算历史统计值
    spreads = []
    for i in range(len(crude_klines) - 1):
        crude_price = crude_klines.close.iloc[i] * crude_volume_multiple * OIL_RATIO
        gasoline_price = gasoline_klines.close.iloc[i] * gasoline_volume_multiple * GAS_RATIO
        diesel_price = diesel_klines.close.iloc[i] * diesel_volume_multiple * DIESEL_RATIO
        
        # 裂解价差 = (汽油价值 + 柴油价值) - 原油价值
        spread = (gasoline_price + diesel_price) - crude_price
        spreads.append(spread)
    
    mean_spread = np.mean(spreads)
    std_spread = np.std(spreads)
    print(f"历史裂解价差 - 均值: {mean_spread:.2f}, 标准差: {std_spread:.2f}")

    # 主循环
    while True:
        api.wait_update()
        
        # 当K线数据有变化时进行计算
        if api.is_changing(crude_klines) or api.is_changing(gasoline_klines) or api.is_changing(diesel_klines):
            # 重新计算历史价差统计
            spreads = []
            for i in range(len(crude_klines) - 1):
                crude_price = crude_klines.close.iloc[i] * crude_volume_multiple * OIL_RATIO
                gasoline_price = gasoline_klines.close.iloc[i] * gasoline_volume_multiple * GAS_RATIO
                diesel_price = diesel_klines.close.iloc[i] * diesel_volume_multiple * DIESEL_RATIO
                
                spread = (gasoline_price + diesel_price) - crude_price
                spreads.append(spread)
            
            mean_spread = np.mean(spreads)
            std_spread = np.std(spreads)
            
            # 计算当前裂解价差
            crude_price = crude_klines.close.iloc[-1] * crude_volume_multiple * OIL_RATIO
            gasoline_price = gasoline_klines.close.iloc[-1] * gasoline_volume_multiple * GAS_RATIO
            diesel_price = diesel_klines.close.iloc[-1] * diesel_volume_multiple * DIESEL_RATIO
            
            current_spread = (gasoline_price + diesel_price) - crude_price
            
            # 计算z-score (标准化的价差)
            z_score = (current_spread - mean_spread) / std_spread
            
            print(f"当前裂解价差: {current_spread:.2f}, Z-score: {z_score:.2f}, 均值: {mean_spread:.2f}")
            
            # 获取当前持仓
            crude_position = api.get_position(CRUDE_OIL)
            gasoline_position = api.get_position(GASOLINE)
            diesel_position = api.get_position(DIESEL)
            
            current_crude_pos = crude_position.pos_long - crude_position.pos_short
            current_gasoline_pos = gasoline_position.pos_long - gasoline_position.pos_short
            current_diesel_pos = diesel_position.pos_long - diesel_position.pos_short
            
            # === 交易信号判断 ===
            if not in_position:  # 如果没有持仓
                if z_score > DEVIATION_THRESHOLD:  # 价差显著高于均值
                    # 卖出裂解价差:卖出原油,买入汽油和柴油
                    print(f"信号: 卖出裂解价差 (Z-score: {z_score:.2f})")
                    print(f"操作: 卖出原油{OIL_LOTS}手,买入汽油{GAS_LOTS}手,买入柴油{DIESEL_LOTS}手")
                    crude_pos.set_target_volume(-OIL_LOTS)
                    gasoline_pos.set_target_volume(GAS_LOTS)
                    diesel_pos.set_target_volume(DIESEL_LOTS)
                    position_time = time.time()
                    in_position = True
                    last_trade_direction = "SELL_SPREAD"
                    
                elif z_score < -DEVIATION_THRESHOLD:  # 价差显著低于均值
                    # 买入裂解价差:买入原油,卖出汽油和柴油
                    print(f"信号: 买入裂解价差 (Z-score: {z_score:.2f})")
                    print(f"操作: 买入原油{OIL_LOTS}手,卖出汽油{GAS_LOTS}手,卖出柴油{DIESEL_LOTS}手")
                    crude_pos.set_target_volume(OIL_LOTS)
                    gasoline_pos.set_target_volume(-GAS_LOTS)
                    diesel_pos.set_target_volume(-DIESEL_LOTS)
                    position_time = time.time()
                    in_position = True
                    last_trade_direction = "BUY_SPREAD"
            
            elif in_position:  # 如果已有持仓
                # 检查是否应当平仓
                if CLOSE_AT_MEAN:  # 在价差回归均值时平仓
                    if (last_trade_direction == "BUY_SPREAD" and current_spread >= mean_spread) or \
                       (last_trade_direction == "SELL_SPREAD" and current_spread <= mean_spread):
                        print(f"信号: 价差回归均值,平仓所有头寸")
                        print(f"当前价差: {current_spread:.2f}, 均值: {mean_spread:.2f}")
                        crude_pos.set_target_volume(0)
                        gasoline_pos.set_target_volume(0)
                        diesel_pos.set_target_volume(0)
                        in_position = False
                else:  # 在价差回归(穿过阈值)时平仓
                    if (last_trade_direction == "BUY_SPREAD" and z_score >= 0) or \
                       (last_trade_direction == "SELL_SPREAD" and z_score <= 0):
                        print(f"信号: 价差穿过均值,平仓所有头寸")
                        print(f"当前价差: {current_spread:.2f}, Z-score: {z_score:.2f}")
                        crude_pos.set_target_volume(0)
                        gasoline_pos.set_target_volume(0)
                        diesel_pos.set_target_volume(0)
                        in_position = False
                
                # 持仓时间监控
                position_duration = (time.time() - position_time) / (60*60*24)  # 天数
                if position_duration > MAX_HOLDING_DAYS:  # 持仓超过最大天数
                    print(f"警告: 持仓时间已超过{MAX_HOLDING_DAYS}天 ({position_duration:.1f}天)")
                    print(f"强制平仓所有头寸")
                    crude_pos.set_target_volume(0)
                    gasoline_pos.set_target_volume(0)
                    diesel_pos.set_target_volume(0)
                    in_position = False

except BacktestFinished as e:
    print("回测结束")
    api.close()
except KeyboardInterrupt:
    print("用户中断程序执行")
    api.close()
    print("策略已停止运行") 
```

Shown in full with attribution under the source's licence. Licence: Apache-2.0

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.