High-Frequency Arbitrage with ETF Twins
Log in to collectAcademic paper
The Microstructure of Arbitrage: ETF Evidence
Ben R. Marshall; Nhut H. Nguyen; Nuttawat Visaltanachoti
- NZMassey University
- ?Massey University - School of Economics and Finance
- NZAuckland University of Technology
- ?Massey University - Department of Economics and Finance
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.
Economic rationale
ETF prices occasionally deviate from underlying indices due to misweighting, creating temporary arbitrage opportunities that allow investors to capture risk-free profits.
Backtest performance
Annualised return28.91%
Volatility14.69%
Beta-0.002
Sharpe ratio1.7
Win rate40%
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