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 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 |
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.
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.
Reference
- 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