Contents
Strategy Library
Intraday Arbitrage Between Index ETFs
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.SetUniverseSelection( 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].DataConsolidated += self.CustomDailyHandler algorithm.SubscriptionManager.AddConsolidator(symbol, self.consolidators[symbol]) def CustomDailyHandler(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 insights, 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
Following the guidelines of Alpha Streams and the Quant League competition, 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 |
The lack of performance for this arbitrage strategy is mostly attributed to the fees it incurs while trading. This is common for an intraday arbitrage strategy, but we discuss ways to reduces these fees in the conclusion of this tutorial. After removing the costs of commissions, crossing the spread, and slippage, the strategy outperforms the SPY over the entire backtesting period. Without these costs, the strategy generates a 1.09 Share ratio while the SPY generates a 0.732 Sharpe ratio. See the backtest performance without fees below.
Market & Competition Qualification
Although this strategy passes several of the metrics required for Alpha Streams and the Quant League competition, it requires further work to pass the following requirements:
- Profitable
- PSR >= 80%
- Max drawdown duration <= 6 months
- Insights contain the following properties: Symbol, Duration, Direction, and Weight
- Alphas need to place at least 10 trades per month for the majority of the backtest
The algorithm currently places trades during 12 unique months throughout the backtest. Since the backtest spans across 61 months, it places trades through a minority of the backtest.
Conclusion
The intraday arbitrage strategy we built and tested throughout this tutorial underperforms the SPY benchmark in terms of Sharpe ratio when including trading costs. Without these costs, we found the strategy outperforms the SPY in terms of Sharpe ratio. In our implementation, we specified the alpha model to initiate trading when atleast a 0.02% profit threshold is reached for 3 seconds. Both of these parameters are set lower than the strategy examined in Marshall et al (2010) for demonstration purposes. Increasing the profit threshold will lead to more profitable, but fewer, trades that may overcome the cost of trading. We leave this area of study for future research. Additional areas of future research include increasing the resolution of data from second to tick and incorportating an execution model that utilizes limit orders to reduce fees.
References
- Marshall, Ben R. and Nguyen, Nhut (Nick) Hoang and Visaltanachoti, Nuttawat, ETF Arbitrage: Intraday Evidence (November 16, 2010). Online copy
- Kakushadze, Zura and Serur, Juan Andrés, 151 Trading Strategies (August 17, 2018). Z. Kakushadze and J.A. Serur. 151 Trading Strategies. Cham, Switzerland: Palgrave Macmillan, an imprint of Springer Nature, 1st Edition (2018), XX, 480 pp; ISBN 978-3-030-02791-9. Online copy
You can also see our Documentation and Videos. You can also get in touch with us via Chat.
Did you find this page helpful?