Crude Oil–Gasoline Futures Mean-Reversion Pair Strategy
Summary
This example trades a spread between continuous Light Sweet Crude Oil and RBOB Gasoline futures. It estimates a return relationship by regressing crude returns on gasoline returns over a rolling history, then measures the recent spread against its mean and standard deviation. A z-score beyond either side of a threshold opens a paired position with equal and opposite portfolio weights; crossing back through zero closes it. Rebalancing is scheduled daily after the market opens.
The source also records prices, scaling gasoline by its contract multiplier for display. Its published setup sets commissions and slippage to zero, so the implementation omits important trading costs. The code offers no performance figures or robustness analysis, and its spread calculation and position sizing should be reviewed before use. In particular, continuous futures rolls, hedge-ratio stability, and realistic execution can materially affect results.
Key ideas
- The pair uses crude oil and gasoline continuous futures with calendar rolls.
- A rolling regression of returns defines a residual spread, which is standardized as a z-score.
- The strategy enters equal and opposite positions when the z-score exceeds either threshold and exits after it crosses zero.
- The published example assumes zero commissions and slippage and reports no performance evidence.
Tags
Full text
# futures_pairs_trading
# futures_pairs_trading
## Source (Apache-2.0)
```python
import numpy as np
import scipy as sp
from zipline.api import (
continuous_future,
schedule_function,
date_rules,
time_rules,
record,
order_target_percent,
set_benchmark,
set_commission,
commission,
set_slippage,
slippage
)
def initialize(context):
# Get continuous futures for Light Sweet Crude Oil...
context.crude_oil = continuous_future('CL', roll='calendar')
# ... and RBOB Gasoline
context.gasoline = continuous_future('RB', roll='calendar')
# If Zipline has trouble pulling the default benchmark, try setting the
# benchmark to something already in your bundle
set_benchmark(context.crude_oil)
# Ignore commissions and slippage for now
set_commission(us_futures=commission.PerTrade(cost=0))
set_slippage(us_futures=slippage.FixedSlippage(spread=0.0))
# Long and short moving average window lengths
context.long_ma = 65
context.short_ma = 5
# True if we currently hold a long position on the spread
context.currently_long_the_spread = False
# True if we currently hold a short position on the spread
context.currently_short_the_spread = False
# Rebalance pairs every day, 30 minutes after market open
schedule_function(func=rebalance_pairs,
date_rule=date_rules.every_day(),
time_rule=time_rules.market_open(minutes=30))
# Record Crude Oil and Gasoline Futures prices everyday
schedule_function(record_price,
date_rules.every_day(),
time_rules.market_open())
def rebalance_pairs(context, data):
# Calculate how far away the current spread is from its equilibrium
zscore = calc_spread_zscore(context, data)
# Get target weights to rebalance portfolio
target_weights = get_target_weights(context, data, zscore)
if target_weights:
# If we have target weights, rebalance portfolio
cl_contract, rb_contract = data.current(
[context.crude_oil, context.gasoline],
'contract'
)
order_target_percent(cl_contract, target_weights[cl_contract])
order_target_percent(rb_contract, target_weights[rb_contract])
def calc_spread_zscore(context, data):
# Get pricing data for our pair of continuous futures
prices = data.history([context.crude_oil,
context.gasoline],
'price',
context.long_ma,
'1d')
cl_price = prices[context.crude_oil]
rb_price = prices[context.gasoline]
# Calculate returns for each continuous future
cl_returns = cl_price.pct_change()[1:]
rb_returns = rb_price.pct_change()[1:]
# Calculate the spread
regression = sp.stats.linregress(
rb_returns[-context.long_ma:],
cl_returns[-context.long_ma:],
)
spreads = cl_returns - (regression.slope * rb_returns)
# Calculate zscore of current spread
zscore = (np.mean(spreads[-context.short_ma]) - np.mean(spreads)) / np.std(spreads, ddof=1)
return zscore
def get_target_weights(context, data, zscore):
# Get current contracts for both continuous futures
cl_contract, rb_contract = data.current(
[context.crude_oil, context.gasoline],
'contract'
)
# Initialize target weights
target_weights = {}
if context.currently_short_the_spread and zscore < 0.0:
# Update target weights to exit position
target_weights[cl_contract] = 0
target_weights[rb_contract] = 0
context.currently_long_the_spread = False
context.currently_short_the_spread = False
elif context.currently_long_the_spread and zscore > 0.0:
# Update target weights to exit position
target_weights[cl_contract] = 0
target_weights[rb_contract] = 0
context.currently_long_the_spread = False
context.currently_short_the_spread = False
elif zscore < -1.0 and (not context.currently_long_the_spread):
# Update target weights to long the spread
target_weights[cl_contract] = 0.5
target_weights[rb_contract] = -0.5
context.currently_long_the_spread = True
context.currently_short_the_spread = False
elif zscore > 1.0 and (not context.currently_short_the_spread):
# Update target weights to short the spread
target_weights[cl_contract] = -0.5
target_weights[rb_contract] = 0.5
context.currently_long_the_spread = False
context.currently_short_the_spread = True
return target_weights
def record_price(context, data):
# Get current price of primary crude oil and gasoline contracts.
crude_oil_price = data.current(context.crude_oil, 'price')
gasoline_price = data.current(context.gasoline, 'price')
# Adjust price of gasoline (42x) so that both futures have same scale.
record(Crude_Oil=crude_oil_price, Gasoline=gasoline_price*42)
```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.