
The strategy arbitrages SPY and IUSA ETFs by shorting overvalued and buying undervalued positions when spreads exceed 1.002, closing trades upon convergence, with 40 trades annually.
ASSET CLASS: ETFs | REGION: Global | FREQUENCY:
Intraday | MARKET: equities | KEYWORD: High-Frequency, Arbitrage , ETF Twins
I. STRATEGY IN A NUTSHELL
Exploit SPY–IUSA ETF price divergences >0.2% via cross-exchange arbitrage; buy undervalued, short overvalued ETFs, closing trades when spreads converge.
II. ECONOMIC RATIONALE
ETF prices occasionally deviate from underlying indices due to misweighting, creating temporary arbitrage opportunities that allow investors to capture risk-free profits.
III. SOURCE PAPER
ETF Arbitrage: Intraday Evidence [Click to Open PDF]
Ben R. Marshall, Massey University – School of Economics and Finance; Nhut H. Nguyen, Auckland University of Technology; Nuttawat Visaltanachoti, Massey University – Department of Economics and Finance
<Abstract>
We use two extremely liquid S&P 500 ETFs to analyze the prevailing trading conditions when mispricing allowing arbitrage opportunities is created. While these ETFs are not perfect substitutes, we show that their minor differences are not responsible for the mispricing. Spreads increase just before arbitrage opportunities, consistent with a decrease in liquidity. Order imbalance increases as markets become more one-sided and spread changes become more volatile which suggests an increase in liquidity risk. The price deviations are economically significant (mean profit of 6.6% p.a. net of spreads) and are followed by a tendency to quickly correct back towards parity.


IV. BACKTEST PERFORMANCE
| Annualised Return | 28.91% |
| Volatility | 14.69% |
| Beta | -0.002 |
| Sharpe Ratio | 1.7 |
| Sortino Ratio | N/A |
| Maximum Drawdown | N/A |
| Win Rate | 40% |
V. FULL PYTHON CODE
from AlgorithmImports import *
from typing import List, Union
# endregion
class HighFrequencyArbitragewithETFTwins(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.spread_threshold:float = 1.002
self.spy_voo_ratio:Union[None, float] = None
self.voo_spy_ratio:Union[None, float] = None
self.symbols:List[Symbol] = [self.AddEquity(x, Resolution.Minute).Symbol for x in ['SPY', 'VOO']]
self.trade_direction_flag:Union[None, bool] = None
def OnData(self, data: Slice) -> None:
if self.symbols[0] in data and data[self.symbols[0]] and self.symbols[1] in data and data[self.symbols[1]]:
# get ratio of etfs
if self.Time.hour == 9 and self.Time.minute == 35:
self.spy_voo_ratio = self.Securities[self.symbols[0]].BidPrice / self.Securities[self.symbols[1]].AskPrice
self.voo_spy_ratio = self.Securities[self.symbols[1]].BidPrice / self.Securities[self.symbols[0]].AskPrice
if self.spy_voo_ratio is not None and self.voo_spy_ratio is not None and not self.Portfolio.Invested:
# decide on trading direction
self.trade_direction_flag = True \
if (self.Securities[self.symbols[0]].BidPrice / self.Securities[self.symbols[1]].AskPrice) >= self.spy_voo_ratio * self.spread_threshold \
else False \
if (self.Securities[self.symbols[1]].BidPrice / self.Securities[self.symbols[0]].AskPrice) >= self.voo_spy_ratio * self.spread_threshold \
else None
# trade execution
if self.trade_direction_flag is not None:
self.SetHoldings(self.symbols[0], (-1 if self.trade_direction_flag else 1) * 1)
self.SetHoldings(self.symbols[1], (-1 if self.trade_direction_flag else 1) * -1)
# closing trade
if self.Portfolio.Invested:
if self.trade_direction_flag:
if (self.Securities[self.symbols[0]].BidPrice / self.Securities[self.symbols[1]].AskPrice) < self.spy_voo_ratio * self.spread_threshold:
self.Liquidate()
self.trade_direction_flag = None
self.spy_voo_ratio = None
self.voo_spy_ratio = None
else:
if (self.Securities[self.symbols[1]].BidPrice / self.Securities[self.symbols[0]].AskPrice) < self.voo_spy_ratio * self.spread_threshold:
self.Liquidate()
self.trade_direction_flag = None
self.spy_voo_ratio = None
self.voo_spy_ratio = None
# close before market close
if self.Time.hour == 15 and self.Time.minute == 59 and self.Portfolio.Invested:
self.Liquidate()
self.trade_direction_flag = None
self.spy_voo_ratio = None
self.voo_spy_ratio = None
VI. Backtest Performance