Abstract
In this tutorial, we implement an intraday arbitrage strategy that capitalizes on deviations between two closely correlated index ETFs. Even though at times both ETFs may hold different constituents and different weights of securities while tracking the index, they are both highly correlated and extremely liquid. Researchers have shown these two properties are essential to an arbitrage system's success. The algorithm we implement here is inspired by the work of Kakushadze and Serur (2018) and Marshall, Nguyen, and Visaltanachoti (2010).
Background
Marshall et al (2010) define an arbitrage opportunity as when the bid price of ETF A (B) diverts high enough away from the ask price of ETF B (A) such that their quotient reaches a threshold. In their paper, an arbitrage opportunity is only acted upon when the threshold is satisfied for 15 seconds. When these criteria are met, the algorithm enters the arbitrage trade by going long ETF B (A) and short ETF A (B). When the spread reverts back to where the bid of ETF B (A) >= the ask of ETF A (B) for 15 seconds, the positions are liquidated. An overview of the trade process is illustrated in the image below.
Method
Universe Selection
We implement a manual universe selection model that includes our two ETFs, SPY and IVV. The attached research notebook finds the correlation of daily returns to be >0.99.
tickers = ['IVV', 'SPY']
symbols = [ Symbol.create(t, SecurityType.EQUITY, Market.USA) for t in tickers ]
self.set_universe_selection( ManualUniverseSelectionModel(symbols) )
Spread Adjustments
Plotting the ratio of the security prices shows its trending behavior.
Without adjusting this ratio over time, an arbitrage system would be stuck in a single trade for majority of the backtest. To resolve this, we subtract a trailing mean from each data point.
Both of the above plots can be reproduced in the attached research notebook. During backtesting, this adjustment is done during trading by setting up a QuoteBarConsolidator for each security in our universe. On each new consolidated QuoteBar, we update the trailing window of L1 data, then calculate the latest spread adjustment values.
# In OnSecuritiesChanged
for symbol in self.symbols:
self.consolidators[symbol] = QuoteBarConsolidator(1)
self.consolidators[symbol].data_consolidated += self.custom_daily_handler
algorithm.subscription_manager.add_consolidator(symbol, self.consolidators[symbol])
def custom_daily_handler(self, sender, consolidated):
# Add new data point to history while removing expired history
self.history[consolidated.symbol]['bids'] = np.append(self.history[consolidated.symbol]['bids'][1:], consolidated.bid.close)
self.history[consolidated.symbol]['asks'] = np.append(self.history[consolidated.symbol]['asks'][1:], consolidated.ask.close)
self.update_spread_adjusters()
def update_spread_adjusters(self):
for i in range(2):
numerator_history = self.history[self.symbols[i]]['bids']
denominator_history = self.history[self.symbols[abs(i-1)]]['asks']
self.spread_adjusters[i] = (numerator_history / denominator_history).mean()
Alpha Construction
The ArbitrageAlphaModel
monitors the intraday bid and ask prices of the securities in the universe. In the constructor, we
can specify the model parameters. In this tutorial, we select a shorter window an arbitrage opportunity must be active before we act on it by setting order_delay
to 3.
class ArbitrageAlphaModel(AlphaModel):
symbols = [] # IVV, SPY
entry_timer = [0, 0]
exit_timer = [0, 0]
spread_adjusters = [0, 0]
long_side = -1
consolidators = {}
history = {}
def __init__(self, order_delay = 3, profit_pct_threshold = 0.02, window_size = 400):
self.order_delay = order_delay
self.pct_threshold = profit_pct_threshold / 100
self.window_size = window_size
Trade Signals
To emit Insight objects, we check if either side of the arbitrage strategy warrants an entry. If no new entries are to be made, the algorithm then looks to exit any current positions. With this design, we can flip our long/short bias without first flattening our position. We use a practically-infinite insight durations as we do not know how long the algorithm will be in an arbitrage trade.
# Search for entries
for i in range(2):
if quotebars[abs(i-1)].bid.close / quotebars[i].ask.close - self.spread_adjusters[abs(i-1)] >= self.pct_threshold:
self.entry_timer[i] += 1
if self.entry_timer[i] == self.order_delay:
self.exit_timer = [0, 0]
if self.long_side == i:
return []
self.long_side = i
return [Insight.price(self.symbols[i], timedelta(days=9999), InsightDirection.UP),
Insight.price(self.symbols[abs(i-1)], timedelta(days=9999), InsightDirection.DOWN)]
else:
return []
self.entry_timer[i] = 0
# Search for an exit
if self.long_side >= 0: # In a position
if quotebars[self.long_side].bid.close / quotebars[abs(self.long_side-1)].ask.close - self.spread_adjusters[self.long_side] >= 0: # Exit signal
self.exit_timer[self.long_side] += 1
if self.exit_timer[self.long_side] == self.order_delay: # Exit signal lasted long enough
self.exit_timer[self.long_side] = 0
i = self.long_side
self.long_side = -1
return [Insight.price(self.symbols[i], timedelta(days=9999), InsightDirection.FLAT),
Insight.price(self.symbols[abs(i-1)], timedelta(days=9999), InsightDirection.FLAT)]
else:
return []
return []
Portfolio Construction & Trade Execution
We utilize the EqualWeightingPortfolioConstructionModel and the ImmediateExecutionModel.
Relative Performance
We analyze the performance of this strategy by comparing it to the S&P 500 ETF benchmark, SPY. We notice that the strategy has a lower Sharpe ratio over all of our testing periods than the benchmark, except for the Fall 2015 crisis where it achieved a 2.8 Sharpe ratio. The strategy also has a lower annual standard deviation of returns when compared to the SPY, implying more consistent returns over time. A breakdown of the strategy's performance across all our testing periods is displayed in the table below.
Period Name | Start Date | End Date | Strategy | Sharpe | ASD |
---|---|---|---|---|---|
Backtest | 8/11/2015 | 8/11/2020 | Strategy | -0.447 | 0.053 |
Benchmark | 0.732 | 0.192 | |||
Fall 2015 | 8/10/2015 | 10/10/2015 | Strategy | 2.837 | 0.225 |
Benchmark | -0.724 | 0.251 | |||
2020 Crash | 2/19/2020 | 3/23/2020 | Strategy | -4.196 | 0.209 |
Benchmark | -1.243 | 0.793 | |||
2020 Recovery | 3/23/2020 | 6/8/2020 | Strategy | -3.443 | 0.013 |
Benchmark | 13.761 | 0.386 |
Derek Melchin
See the attached backtest for an updated version of the algorithm in PEP8 style.
The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
Derek Melchin
The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
To unlock posting to the community forums please complete at least 30% of Boot Camp.
You can continue your Boot Camp training progress from the terminal. We hope to see you in the community soon!