
The investment universe consists of ETFs from Bloomberg database. Data of outstanding ETF shares are also obtained from Bloomberg or can be obtained from ETF Global data. We construct a value-weighted portfolio with a simple rule: At the close of day t, we categorize all ETFs in our sample into two groups based on Flow variable, and then long ETFs with unexpected positive Flow, and short ones with negative.
ASSET CLASS: ETFs | REGION: United States | FREQUENCY:
Intraday | MARKET: bonds, commodities, currencies, equities, REITs | KEYWORD: ETF
I. 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.
II. 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.
III. SOURCE PAPER
Are the Flows of Exchange-Traded Funds Informative? [Click to Open PDF]
Liao Xu, Xiangkang Yin, and Jing Zhao
Zhejiang Gongshang University (ZJGSU); Deakin University; Financial Research Network (FIRN); La Trobe University
<Abstract>
This paper provides novel evidence of information asymmetry in Exchange-Traded Fund (ETF) markets. By decomposing daily ETF flows, we find that the unexpected flow component, orthogonal to the components driven by market-making and arbitraging, wields substantial power in predicting next day’s ETF returns. Informed traders are able to exploit their information advantage to realize an annualized open-to-close return of 19.16% or close-to-close return of 22.42%. The informativeness of the unexpected ETF component is further confirmed by its strong power of predicting next day’s macro news while the demand- and arbitrage-driven components are not closely related to forthcoming news.


IV. BACKTEST PERFORMANCE
| Annualised Return | 9.58% |
| Volatility | 8.48% |
| Beta | 0.292 |
| Sharpe Ratio | 1.13 |
| Sortino Ratio | N/A |
| Maximum Drawdown | N/A |
| Win Rate | 51% |
V. 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