Quant BuffetRelax, Not Over Thinking

ETF Flows Predict Subsequent ETF Performance

Log in to collect

Academic paper

Are Authorized Participants of Exchange-Traded Funds Informed Traders?

AuthorsLiao Xu; Xiangkang Yin; Jing Zhao

Institute
  • Zhejiang Gongshang University
  • ?Zhejiang Gongshang University (ZJGSU)
  • Research Network (United States)
  • Deakin University
  • ?Financial Research Network (FIRN)
  • La Trobe University

Strategy in a nutshell

Construct a value-weighted intraday portfolio of ETFs based on flows. Buy ETFs with unexpected negative flows and sell those with positive flows; weights are proportional to ETF AUM.

Economic rationale

Exploits information asymmetry in ETF markets. ETF flows reflect trading activity by authorized participants, capturing market-wide signals that can indicate mispricing and intraday profit opportunities.

Backtest performance

Annualised return9.58%
Volatility8.48%
Beta0.292
Sharpe ratio1.13
Win rate51%

Full Python code

from AlgorithmImports import *
from typing import List, Dict
from data_tools import CustomFeeModel, QuantpediaSharesOutstandingETFs, SymbolData
# endregion

class ETFFlowsPredictSubsequentETFPerformance(QCAlgorithm):

def Initialize(self):
self.SetStartDate(2010, 1, 1)
self.SetCash(100000)

self.leverage:int = 5

self.market_close_minute:int = 0
self.market_close_hour:int = 16

self.market_open_minute:int = 31
self.market_open_hour:int = 9

self.max_missing_days:int = 5

self.open_trades:List[List[Symbol, float]] = []
self.data:Dict[Symbol, SymbolData] = {}

csv_file:str = self.Download('data.quantpedia.com/backtesting_data/equity/etf_shares_outstanding.csv')
lines:List[str] = csv_file.split('\r\n')

tickers:List[str] = lines[0].split(';')[1:]

for ticker in tickers:
    data = self.AddEquity(ticker, Resolution.Minute)
    data.SetFeeModel(CustomFeeModel())
    data.SetLeverage(self.leverage)

    self.data[ticker] = SymbolData(data.Symbol)

self.etf_shares_outstanding:Symbol = self.AddData(QuantpediaSharesOutstandingETFs,
    'etf_shares_outstanding', Resolution.Daily).Symbol

def OnData(self, data: Slice):
if self.etf_shares_outstanding in data and data[self.etf_shares_outstanding]:
    shares_outstanding:Dict[str, float] = data[self.etf_shares_outstanding].GetProperty('shares_outstanding')

    curr_date:datetime.date = self.Time.date()

    for ticker, shares_outstanding in shares_outstanding.items():
        self.data[ticker].update_shares_outstanding(curr_date, shares_outstanding)

if self.Time.minute == self.market_close_minute and self.Time.hour == self.market_close_hour:
    # on market close calculate flow values and create MarketOnOper orders
    long_leg:List[str] = []
    short_leg:List[str] = []
    curr_date:datetime.date = self.Time.date()
    
    for ticker, symbol_data in self.data.items():
        symbol:Symbol = symbol_data.get_symbol()

        if not symbol_data.data_still_coming(curr_date, self.max_missing_days):
            # make sure shares outstanding data are still coming
            symbol_data.reset()

        elif symbol_data.shares_outstanding_ready() and symbol in data and data[symbol]:
            # trade only ETFs, which have price and shares outstanding
            price:float = data[symbol].Value
            symbol_data.update_price(price)
            symbol_data.update_market_cap()

            flow_value:float = symbol_data.get_flow()
            
            if flow_value > 0:
                long_leg.append(ticker)
            elif flow_value < 0:
                short_leg.append(ticker)

    weight:float = self.Portfolio.TotalPortfolioValue / 2 \
        if len(long_leg) != 0 and len(short_leg) != 0 else self.Portfolio.TotalPortfolioValue

    # trade execution
    total_long_cap:float = sum(map(lambda ticker: self.data[ticker].get_market_cap(), long_leg))
    for ticker in long_leg:
        symbol:Symbol = self.data[ticker].get_symbol()
        quantity:float = -self.get_quantity(ticker, weight, total_long_cap)

        self.MarketOnOpenOrder(symbol, quantity)
        self.open_trades.append((symbol, -quantity))

    total_short_cap:float = sum(map(lambda ticker: self.data[ticker].get_market_cap(), short_leg))
    for ticker in short_leg:
        symbol:Symobl = self.data[ticker].get_symbol()
        quantity:float = self.get_quantity(ticker, weight, total_short_cap)

        self.MarketOnOpenOrder(symbol, quantity)
        self.open_trades.append((symbol, -quantity))

if self.Time.minute == self.market_open_minute and self.Time.hour == self.market_open_hour:
    # create MarketOnClose order to liquidate opened trades
    for symbol, quantity in self.open_trades:
        if self.Portfolio[symbol].Invested:
            self.MarketOnCloseOrder(symbol, quantity)

    self.open_trades.clear()

def get_quantity(self, ticker:str, weight:float, total_cap:float) -> float:
price:float = self.data[ticker].get_price()
market_cap:float = self.data[ticker].get_market_cap()

quantity:float = np.floor((weight * (market_cap / total_cap)) / price)

return quantity