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.

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 Return28.91%
Volatility14.69%
Beta-0.002
Sharpe Ratio1.7
Sortino RatioN/A
Maximum DrawdownN/A
Win Rate40%

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

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading