Crack Spread Mean Reversion Across Crude Oil and Refined Products
Summary
This futures spread strategy assumes the combined value of gasoline and diesel relative to crude oil fluctuates around a historical mean. It forms a weighted spread using contract multipliers and a 3:2:1 ratio, then compares the latest spread with its historical mean and standard deviation over a 60-day lookback. When the spread’s z-score exceeds a threshold of 1.5 standard deviations in either direction, it opens opposing positions across crude and the two product contracts. It targets a return to the mean and can force an exit after a maximum holding period of 10 days.
The document supplies Python source and a backtest date range from November 2023 to April 2024, but no performance statistics. The code uses fixed contract ratios and a binary position flag, so it offers limited treatment of changing hedge relationships and execution risk. It also does not demonstrate that the spread is stationary or that the selected threshold and lookback generalize beyond the specified contracts and period.
Key ideas
- The strategy trades a weighted spread between crude oil and two refined-product futures.
- It estimates a historical mean and standard deviation over a 60-day lookback and uses a 1.5-standard-deviation threshold for entries.
- Positions oppose one another across the three contracts, with sizes based on a 3:2:1 ratio.
- Exits occur on mean reversion or after a maximum holding period of 10 days.
- The document gives a backtest interval but no results or evidence that the spread relationship is stable.
Tags
Full text
# crack_spread_arbitrage
# crack_spread_arbitrage
## Source (Apache-2.0)
```python
#!/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.