3000+ ETFs and years of data… looks like we got ourselves an ETF Search Party!!!
Age and Dollar Traded
there are lots of ETFs out there… let’s see the average “age” and dollar-traded
Code
import plotly.express as pximport plotly.io as pioimport pandas as pdimport numpy as nppio.renderers.default ="notebook"# plot Agefig_age = px.histogram(df_screen, x ="age", title =f"ETFs' Age")fig_age.add_vline(x= df_screen['age'].mean(), annotation_text=f"Average Age ({df_screen['age'].mean():.2f})", annotation_position="bottom right")age_bins = [0,3,5,10,float('inf')]age_labels = ['0 to 3', '3 to 5', '5 to 10', '>10']df_screen['age_cat'] = pd.cut(df_screen['age'], bins = age_bins, labels = age_labels, right =False)age_cat_vc = df_screen['age_cat'].value_counts()pie_age = px.pie(age_cat_vc, values ='count', names = age_cat_vc.index, title =f"ETFs' Age by Categories")# plot dollar tradeddollar_bins = [0,1e5, 1e5*5, 1e6, 1e6*100, 1e6*1000, float('inf')]dollar_labels = ['less than 100k', '100k to 500k', '500k to 1mm', '1mm to 100mm', '100mm to 1B', 'above 1B']df_screen['dollar_traded_1d_cat'] = pd.cut(df_screen['dollar_traded_1d'], bins = dollar_bins, labels = dollar_labels, right =False)dollar_cat_vc = df_screen['dollar_traded_1d_cat'].value_counts()fig_dollar = px.pie(dollar_cat_vc, values ="count", names = dollar_cat_vc.index, title =f"ETFs' Average 10-d Dollar Traded")display(fig_age)display(pie_age)display(fig_dollar)
(a) ETF’s Age Distribution
(b) ETF’s Age by Categories
(c) Average 10d Dollar Traded
Figure 1: ETFs’ Age and Dollar Traded
Filter by volume and age
we are going to separate these ETFs by Age and filter out those with less than 500k dollar trade.
re-using the visualization function here we are going to plot all these ETFs against SPY
Figure 2 (a) shows that it’s hard to beat the market in returns and have a higher Sortino Ratio. FTEC has the highest Sortino ratio in the top-right quadrant but that’s only 0.18 higher than the SPY.
when thinking in terms of Gain-to-Pain, it seems that all funds converges at about 1.17 in Figure 2 (b). But there are a few reliable funds that can preserve capital close to the risk-free rate! Those are the typical money market funds like BOXX, SHV, and BIL
Code
import plotly.graph_objects as godef visualize_metrics(df, x: str, y: str, size: str=None, color: str=None, text:str=None, textposition ="top center", color_continuous_scale: str='rdbu', color_continuous_midpoint: float=None, ref_ticker: str=None, ref_ticker_marker_symbol: str="circle-open-dot", ref_ticker_marker_color: str='green', hl_tickers: list= [], hl_tickers_color: str='yellow', hl_marker_size: int=15 ):''' return a plotly figure object Args: ref_ticker_marker_symbol: get help on marker styling here https://plotly.com/python/marker-style/#color-opacity color_continuous_scale: any in https://plotly.com/python/colorscales/#color-scales-in-plotly-express ''' fig = px.scatter(df, x = x, y = y, size = size, color = color, text = text, hover_data = {'ticker': df.index}, title =f"{y} vs {x}", color_continuous_midpoint= color_continuous_midpoint, color_continuous_scale= color_continuous_scale, ) fig.update_traces(textposition = textposition) other_trace_order =-1if ref_ticker: fig.add_trace(go.Scatter( x= [df.at[ref_ticker, x]], y = [df.at[ref_ticker, y]], mode ="markers", marker_symbol = ref_ticker_marker_symbol, marker_size =10, marker = {'color': ref_ticker_marker_color}, hoverinfo ="skip", zorder = other_trace_order # order this trace behind the original scatter )) fig.add_hline(y = df.at[ref_ticker, y], line_dash ="dot", opacity =0.5) fig.add_vline(x = df.at[ref_ticker, x], line_dash ="dot", opacity =0.5)for t in hl_tickers: other_trace_order -=1 fig.add_trace(go.Scatter( x= [df.at[t, x]], y = [df.at[t, y]], mode ="markers", marker_symbol = ref_ticker_marker_symbol, marker_size = hl_marker_size, marker = {'color': hl_tickers_color}, hoverinfo ="skip", zorder = other_trace_order # order this trace behind the original scatter )) fig.update_layout(showlegend=False) fig.data = fig.data[::-1] # reorder tracereturn figdf_10y_metrics = df_10y_metrics.set_index('ticker', drop =True) if'ticker'in df_10y_metrics.columns else df_10y_metricsdisplay( visualize_metrics(df_10y_metrics, x ="Sortino Ratio", y="Annualized Return", color ="Max Drawdown", text=None, color_continuous_scale ='reds_r', color_continuous_midpoint =0, ref_ticker='SPY', hl_tickers=['BRK-B', 'GLD', "BOXX"], hl_tickers_color='blue', hl_marker_size=13 ))display( visualize_metrics(df_10y_metrics, x ="Gain-to-Pain Ratio", y="Annualized Return", color ="Max Drawdown", text=None, color_continuous_scale ='reds_r', color_continuous_midpoint =0, ref_ticker='SPY', hl_tickers=['BRK-B', 'GLD', "BOXX"], hl_tickers_color='blue', hl_marker_size=13 ))
(a) Sortino Ratio vs Returns
(b) GPR vs Returns
Figure 2: 10-year club: Annualized Return vs GPR or Sortino Ratio
Drawdown vs Returns
Figure 3 shows that it’s pretty hard to beat the market and also have less draw-down. The GOAT BRK-B definitely did it on that front. And so did CLSE but with both lower sortino and Gain-to-Pain ratio1.
Figure 3: it’s pretty hard to beat the market and have a smaller drawn-down
Leaderboard
we are going to use pandas’ ranking function to help create a leaderboard for the ETF with the highest return, highest Gain-to-Pain Ratio, highest Sortino Ratio, and lowest Max Draw-down.
Here are the top 10 and as you can see, only 6 investments out of 832 ranks above SPY
Code
def df_to_leaderboard(data, rank_dict, debug:bool=False, out_col_name ='ranking'): df = data.copy(deep =True) s_ranks = [] f_rank =Nonefor k,v in rank_dict.items(): s_rank = df[k].rank(**v) s_ranks.append(s_rank) f_rank = f_rank + s_rank ifisinstance(f_rank, pd.Series) else s_rankif debug: df[f'{k}:rank'] = s_rankif debug: df[out_col_name +":points"] = f_rank df[out_col_name] = f_rank.rank(ascending=True)return df.sort_values(out_col_name, ascending =True)df_10y_leaderboard = df_to_leaderboard(df_10y_metrics, rank_dict = {'Gain-to-Pain Ratio': {"ascending": False},'Annualized Return': {"ascending": False},'Sortino Ratio': {'ascending': False},'Max Drawdown': {'ascending': False} })# adding details for each ETFdf_10y_leaderboard['ETF Desc'] = [ df_screen[df_screen['name']==x]['description'].tolist()for x in df_10y_leaderboard.index.tolist()]df_10y_leaderboard['ETF Desc']= [x[0] if x else""for x in df_10y_leaderboard['ETF Desc'].tolist()]df_10y_leaderboard['launch_date'] = [ df_screen[df_screen['name']==x]['launch_date'].tolist()for x in df_10y_leaderboard.index.tolist()]df_10y_leaderboard['launch_date']= [x[0] if x else""for x in df_10y_leaderboard['launch_date'].tolist()]df_10y_leaderboard['url'] = [ df_screen[df_screen['name']==x]['ticker'].tolist()for x in df_10y_leaderboard.index.tolist()]df_10y_leaderboard['url']= [f'https://www.tradingview.com/symbols/{x[0].replace(":","-")}/'if x else""for x in df_10y_leaderboard['url'].tolist()]for t in bm_tickers:print(f'{t} is ranked {df_10y_leaderboard.at[t, "ranking"]}')df_10y_leaderboard.drop(columns = ['ETF Desc', 'launch_date', 'url']).head(10)
BOXX is ranked 21.0
SPY is ranked 7.0
BRK-B is ranked 6.0
GLD is ranked 33.5
Annualized Return
Sortino Ratio
Max Drawdown
Gain-to-Pain Ratio
bar_count
ranking
ticker
CLSE
0.119273
0.872539
-0.170736
1.151976
787
1.0
IWY
0.157936
1.036899
-0.321345
1.178058
2517
2.0
IWF
0.144320
0.954093
-0.317081
1.167209
2517
3.0
OEF
0.128231
0.932230
-0.314355
1.173177
2517
4.0
XLG
0.136418
0.865103
-0.304963
1.157646
2517
5.0
BRK-B
0.136595
0.869941
-0.295699
1.149261
2517
6.0
SPY
0.117075
0.903195
-0.307322
1.170085
2517
7.0
SPHQ
0.121310
0.830261
-0.289488
1.156425
2517
8.0
IVW
0.133561
0.896730
-0.318637
1.160998
2517
9.0
RTH
0.123469
0.818123
-0.248585
1.146301
2517
10.0
Code
n =10r =1print('::: {.callout-caution title = "Expand to see Top 10 by Name:" collapse="true"}')for i, row in df_10y_leaderboard.head(n).iterrows():if i in df_screen['name'].tolist(): md_str =f'{r}. [{i}](https://www.tradingview.com/symbols/{df_screen[df_screen["name"]==i]["ticker"].tolist()[0].replace(":","-")}/)' md_str +=f'(est. {datetime.strftime(df_screen[df_screen["name"]==i]["launch_date"].tolist()[0].date(), "%b %Y")}' md_str +=f", Returns: {df_10y_leaderboard.at[i,'Annualized Return']*100:.2f}%)" md_str +=f' - {row["ETF Desc"]}'print(md_str)else:print(f'{r}. {i}') r+=1print(f':::')
Expand to see Top 10 by Name:
CLSE(est. Dec 2009, Returns: 11.93%) - Trust for Professional Managers Convergence Long/Short Equity ETF
IWY(est. Sep 2009, Returns: 15.79%) - iShares Russell Top 200 Growth ETF
IWF(est. May 2000, Returns: 14.43%) - iShares Russell 1000 Growth ETF
OEF(est. Oct 2000, Returns: 12.82%) - iShares S&P 100 ETF
XLG(est. May 2005, Returns: 13.64%) - Invesco S&P 500 Top 50 ETF
RTH(est. May 2001, Returns: 12.35%) - VanEck Retail ETF
5-year Club
5-year is not a long time but as Figure 1 (b) shows, just under 20% of all actively traded ETFs are between 5 to 10 years old. In this past 5-year, we saw the rally back from the pandemic crash of early 2020 and a bad year for the SPY in 2022. For this period, we will use a risk-free rate of 2.69%2.
Along with the 832 ETFs form the 10-year Club, we now have a universe of 1,331 ETFs to look at.
Figure 5: seems that beating the market and having less draw-down is easier in the last 5-y than over 10
Leaderboard
looking at the top 20 (~top 1%) on our leaderboard, we found that 9 were ETFs from the 10-y club but interestingly their 10-year performance ranking wise were quite low.
However, our reference BRK-B performance during both period were equally impressive:
5-year rank: 42th (top 3%)
10-year rank: 6th (top 1%)
Code
df_5y_leaderboard = df_to_leaderboard(df_5y_metrics, rank_dict = {'Gain-to-Pain Ratio': {"ascending": False},'Annualized Return': {"ascending": False},'Sortino Ratio': {'ascending': False},'Max Drawdown': {'ascending': False} })df_5y_leaderboard['10y_club_ranking'] = [df_10y_leaderboard.at[x,"ranking"] if x in df_10y_leaderboard.index elseNonefor x in df_5y_leaderboard.index.tolist()]df_5y_leaderboard['ETF Desc'] = [ df_screen[df_screen['name']==x]['description'].tolist()for x in df_5y_leaderboard.index.tolist()]df_5y_leaderboard['ETF Desc']= [x[0] if x else""for x in df_5y_leaderboard['ETF Desc'].tolist()]df_5y_leaderboard['launch_date'] = [ df_screen[df_screen['name']==x]['launch_date'].tolist()for x in df_5y_leaderboard.index.tolist()]df_5y_leaderboard['launch_date']= [x[0] if x else""for x in df_5y_leaderboard['launch_date'].tolist()]df_5y_leaderboard['url'] = [ df_screen[df_screen['name']==x]['ticker'].tolist()for x in df_5y_leaderboard.index.tolist()]df_5y_leaderboard['url']= [f'https://www.tradingview.com/symbols/{x[0].replace(":","-")}/'if x else""for x in df_5y_leaderboard['url'].tolist()]for t in bm_tickers:print(f'{t} is ranked {df_5y_leaderboard.at[t, "ranking"]}')df_5y_leaderboard.drop(columns=["ETF Desc", "launch_date", "url"]).head(20)
BOXX is ranked 83.0
SPY is ranked 89.0
BRK-B is ranked 42.0
GLD is ranked 244.0
Annualized Return
Sortino Ratio
Max Drawdown
Gain-to-Pain Ratio
bar_count
ranking
10y_club_ranking
ticker
AUSF
0.193447
1.251066
-0.146535
1.196563
1257
1.0
NaN
TPYP
0.233657
1.562684
-0.170997
1.226821
1257
2.0
NaN
IAK
0.220112
1.438219
-0.169390
1.213091
2517
3.0
122.0
SDCI
0.222137
1.442262
-0.188271
1.230980
1257
4.0
NaN
USCI
0.208252
1.368932
-0.188408
1.227467
2517
5.0
634.5
IYK
0.152852
1.282406
-0.131536
1.202332
2517
6.0
61.0
USAI
0.323535
1.713700
-0.214130
1.245183
1257
7.0
NaN
DBA
0.158424
1.291786
-0.159408
1.196004
2517
8.0
695.0
CEFS
0.156491
1.321315
-0.161861
1.215522
1257
9.0
NaN
FLIN
0.183966
1.448071
-0.196180
1.218308
1257
10.0
NaN
EPI
0.225338
1.548842
-0.212186
1.237671
2517
11.0
395.0
FDVV
0.168454
1.436817
-0.189884
1.224328
1257
12.0
NaN
CDL
0.156263
1.254956
-0.163621
1.194549
1257
13.0
NaN
FCPI
0.164801
1.214822
-0.178916
1.194403
1257
14.0
NaN
FNDX
0.158695
1.227043
-0.172099
1.191250
2517
15.0
110.0
NFTY
0.199864
1.288201
-0.209061
1.189343
2517
16.0
537.0
VUSE
0.190301
1.247887
-0.203583
1.183790
2517
17.0
289.0
DIVO
0.143004
1.482278
-0.168663
1.235777
1257
18.0
NaN
DXJ
0.221022
1.267594
-0.221939
1.198037
2517
19.0
216.0
PTLC
0.135140
1.195819
-0.134277
1.214713
1257
20.0
NaN
Code
n =20r =1print('::: {.callout-caution title = "Expand to see Top 20 by Name:" collapse="true"}')for i, row in df_5y_leaderboard.head(n).iterrows():if i in df_screen['name'].tolist(): md_str =f'{r}. [{i}](https://www.tradingview.com/symbols/{df_screen[df_screen["name"]==i]["ticker"].tolist()[0].replace(":","-")}/)' md_str +=f'(est. {datetime.strftime(df_screen[df_screen["name"]==i]["launch_date"].tolist()[0].date(), "%b %Y")}' md_str +=f", Returns: {df_5y_leaderboard.at[i,'Annualized Return']*100:.2f}%)" md_str +=f' - {row["ETF Desc"]}'print(md_str)else:print(f'{r}. {i}') r+=1print(f':::')
Expand to see Top 20 by Name:
AUSF(est. Aug 2018, Returns: 19.34%) - Global X Funds Global X Adaptive U.S. Factor ETF
TPYP(est. Jun 2015, Returns: 23.37%) - Tortoise North American Pipeline ETF
IAK(est. Apr 2006, Returns: 22.01%) - iShares U.S. Insurance ETF
SDCI(est. May 2018, Returns: 22.21%) - USCF SummerHaven Dynamic Commodity Strategy No K-1 Fund
USCI(est. Aug 2010, Returns: 20.83%) - United States Commodity Index Fund ETV
IYK(est. Jun 2000, Returns: 15.29%) - iShares U.S. Consumer Staples ETF
USAI(est. Dec 2017, Returns: 32.35%) - Pacer American Energy Independence ETF
DBA(est. Jan 2007, Returns: 15.84%) - Invesco DB Agriculture Fund
CEFS(est. Mar 2017, Returns: 15.65%) - Saba Closed-End Funds ETF
FLIN(est. Feb 2018, Returns: 18.40%) - Franklin FTSE India ETF
EPI(est. Feb 2008, Returns: 22.53%) - WisdomTree India Earnings Fund
FDVV(est. Sep 2016, Returns: 16.85%) - Fidelity High Dividend ETF
CDL(est. Jul 2015, Returns: 15.63%) - VictoryShares US Large Cap High Div Volatility Wtd ETF
FCPI(est. Nov 2019, Returns: 16.48%) - Fidelity Stocks for Inflation ETF
FNDX(est. Aug 2013, Returns: 15.87%) - Schwab Fundamental U.S. Large Company ETF
NFTY(est. Feb 2012, Returns: 19.99%) - First Trust India Nifty 50 Equal Weight ETF
VUSE(est. Jan 2014, Returns: 19.03%) - Vident Core US Equity ETF
DIVO(est. Dec 2016, Returns: 14.30%) - Amplify CWP Enhanced Dividend Income ETF
DXJ(est. Jun 2006, Returns: 22.10%) - WisdomTree Japan Hedged Equity Fund
PTLC(est. Jun 2015, Returns: 13.51%) - Pacer Fund Trust Pacer Trendpilot US Large Cap ETF
3-year Club
in the 3-5 year category we have additional of 571 ETFs which brings our universe of investment to evaluate over the last 3-year to 1,902. For the 3-year period, we saw the market rallied in 2023 and 2024 led by tech stocks. But we also experienced some pretty big negative days in the tarrifs sell off in Apr 2025.
Figure 7: the top-right quadrant reveals a few more actively managed funds
Leaderboard
Code
df_3y_leaderboard = df_to_leaderboard(df_3y_metrics, rank_dict = {'Gain-to-Pain Ratio': {"ascending": False},'Annualized Return': {"ascending": False},'Sortino Ratio': {'ascending': False},'Max Drawdown': {'ascending': False} })df_3y_leaderboard['10y_club_ranking'] = [df_10y_leaderboard.at[x,"ranking"] if x in df_10y_leaderboard.index elseNonefor x in df_3y_leaderboard.index.tolist()]df_3y_leaderboard['5y_club_ranking'] = [df_5y_leaderboard.at[x,"ranking"] if x in df_5y_leaderboard.index elseNonefor x in df_3y_leaderboard.index.tolist()]df_3y_leaderboard['ETF Desc'] = [ df_screen[df_screen['name']==x]['description'].tolist()for x in df_3y_leaderboard.index.tolist()]df_3y_leaderboard['ETF Desc']= [x[0] if x else""for x in df_3y_leaderboard['ETF Desc'].tolist()]df_3y_leaderboard['launch_date'] = [ df_screen[df_screen['name']==x]['launch_date'].tolist()for x in df_3y_leaderboard.index.tolist()]df_3y_leaderboard['launch_date']= [x[0] if x else""for x in df_3y_leaderboard['launch_date'].tolist()]df_3y_leaderboard['url'] = [ df_screen[df_screen['name']==x]['ticker'].tolist()for x in df_3y_leaderboard.index.tolist()]df_3y_leaderboard['url']= [f'https://www.tradingview.com/symbols/{x[0].replace(":","-")}/'if x else""for x in df_3y_leaderboard['url'].tolist()]for t in bm_tickers:print(f'{t} is ranked {df_3y_leaderboard.at[t, "ranking"]}')df_3y_leaderboard.head(30).drop(columns = ["ETF Desc", 'launch_date', 'url'])
BOXX is ranked 11.5
SPY is ranked 232.0
BRK-B is ranked 215.0
GLD is ranked 56.5
Annualized Return
Sortino Ratio
Max Drawdown
Gain-to-Pain Ratio
bar_count
ranking
10y_club_ranking
5y_club_ranking
ticker
FLRT
0.069053
1.332329
-0.071796
1.627367
2517
1.0
279.0
82.0
IGHG
0.068666
0.856732
-0.051349
1.237450
2517
2.0
333.0
411.0
OCTW
0.076108
0.789919
-0.083793
1.252515
753
3.0
NaN
NaN
SIXH
0.081064
0.932971
-0.100578
1.208382
753
4.0
NaN
NaN
JANW
0.076447
0.760075
-0.086614
1.219663
753
5.0
NaN
NaN
BGLD
0.145539
1.456350
-0.146925
1.261416
753
6.0
NaN
NaN
BALT
0.061212
0.900420
-0.048918
1.365610
753
7.0
NaN
NaN
VRIG
0.056020
1.064912
-0.020842
2.103360
1257
8.0
NaN
90.0
PULS
0.053072
2.071176
-0.007899
3.314056
1257
9.0
NaN
113.0
IAK
0.139727
1.135163
-0.143834
1.186429
2517
10.0
122.0
3.0
BOXX
0.050582
5.734312
-0.001159
9.519356
573
11.5
21.0
83.0
SIXA
0.100613
0.974649
-0.124871
1.185202
753
11.5
NaN
NaN
HEQT
0.084612
0.753923
-0.101625
1.175581
753
13.0
NaN
NaN
BFIX
0.057763
1.158105
-0.050203
1.400234
753
14.0
NaN
NaN
PTNQ
0.103609
0.868129
-0.125163
1.166248
1257
15.0
NaN
33.0
NEAR
0.052572
1.239426
-0.008235
1.954934
2517
16.5
299.0
186.0
RISR
0.130929
1.100260
-0.149944
1.196075
753
16.5
NaN
NaN
GLDI
0.108806
0.990762
-0.138939
1.204813
2517
18.0
95.0
362.5
GSST
0.049828
2.207438
-0.005089
3.367343
1257
19.0
NaN
138.0
BSJP
0.062637
0.717038
-0.064020
1.259180
1257
20.0
NaN
97.0
JAAA
0.053291
0.993421
-0.021841
1.721282
753
21.0
NaN
NaN
GSY
0.048956
2.128984
-0.004421
3.247476
2517
22.0
300.5
174.0
TFLO
0.046979
2.511089
-0.001387
7.369236
2517
23.5
348.0
535.0
PJUL
0.083790
0.691950
-0.106948
1.169280
1257
23.5
NaN
143.0
SGOV
0.046049
3.791378
-0.000300
36.355921
753
25.0
NaN
NaN
POCT
0.078019
0.659162
-0.102248
1.194244
1257
26.5
NaN
57.0
JULW
0.073419
0.695999
-0.094951
1.193857
753
26.5
NaN
NaN
ICSH
0.047515
1.625441
-0.004186
3.843889
2517
28.0
327.5
457.0
JPST
0.046886
1.773243
-0.002970
3.411023
1257
29.0
NaN
198.0
MINT
0.047845
1.687724
-0.008405
3.442114
2517
30.0
327.5
427.0
Code
n =20r =1print('::: {.callout-caution title = "Expand to see Top 20 by Name:" collapse="true"}')for i, row in df_3y_leaderboard.head(n).iterrows():if i in df_screen['name'].tolist(): md_str =f'{r}. [{i}](https://www.tradingview.com/symbols/{df_screen[df_screen["name"]==i]["ticker"].tolist()[0].replace(":","-")}/)' md_str +=f'(est. {datetime.strftime(df_screen[df_screen["name"]==i]["launch_date"].tolist()[0].date(), "%b %Y")}' md_str +=f", Returns: {df_3y_leaderboard.at[i,'Annualized Return']*100:.2f}%)" md_str +=f' - {row["ETF Desc"]}'print(md_str)else:print(f'{r}. {i}') r+=1print(f':::')
Expand to see Top 20 by Name:
FLRT(est. Feb 2015, Returns: 6.91%) - Pacer Funds Pacer Pacific Asset Floating Rate High Income ETF
PULS(est. Apr 2018, Returns: 5.31%) - PGIM Ultra Short Bond ETF
IAK(est. Apr 2006, Returns: 13.97%) - iShares U.S. Insurance ETF
BOXX(est. Dec 2022, Returns: 5.06%) - EA Series Trust Alpha Architect 1-3 Month Box ETF
SIXA(est. May 2020, Returns: 10.06%) - ETC 6 Meridian Mega Cap Equity ETF
HEQT(est. Oct 2021, Returns: 8.46%) - Simplify Hedged Equity ETF
BFIX(est. Feb 2022, Returns: 5.78%) - Build Funds Trust Build Bond Innovation ETF
PTNQ(est. Jun 2015, Returns: 10.36%) - Pacer Trendpilot 100 ETF
NEAR(est. Sep 2013, Returns: 5.26%) - iShares Short Duration Bond Active ETF
RISR(est. Sep 2021, Returns: 13.09%) - FolioBeyond Alternative Income and Interest Rate Hedge ETF
GLDI(est. Jan 2013, Returns: 10.88%) - ETRACS Gold Shares Covered Call ETNs due February 2, 2033
GSST(est. Apr 2019, Returns: 4.98%) - Goldman Sachs Ultra Short Bond ETF
BSJP(est. Sep 2017, Returns: 6.26%) - Invesco BulletShares 2025 High Yield Corporate Bond ETF
multi-period leaderboard
our search found a lot of candidates for different time frame!
And so there’s one more way to evaluate performance and that’s to look at multi-period result. Investing might be a game of average and consistency… the fastest sprinter won’t be the fastest Marathon runner and vice versa. But can we find a decently good runner who excel at both distance? Think of this as the search for the hybrid athlete.
remember when Jason Khalipa won BOTH the 2k and half-marathon at the 2013 Crossfit Game’s Row Event4
10-year: 3 periods
Code
rank_data = []for t in df_10y_leaderboard.index.tolist():if t notin ['BOXX']: rank_data.append( {'ticker': t,'rank_10y': df_10y_leaderboard.at[t,"ranking"],'rank_5y': df_5y_leaderboard.at[t,"ranking"],'rank_3y': df_3y_leaderboard.at[t,"ranking"],'ETF Desc': df_10y_leaderboard.at[t, 'ETF Desc'],'url': df_10y_leaderboard.at[t, 'url'],'launch_date': df_10y_leaderboard.at[t, 'launch_date'] } )rank_data = pd.DataFrame(rank_data)rank_data['points'] = rank_data['rank_10y'] + rank_data['rank_5y'] + rank_data['rank_3y']rank_data['rank'] = rank_data['points'].rank(ascending =True)rank_data = rank_data.set_index('ticker', drop =True)rank_data = rank_data.sort_values(by = ['rank'], ascending=True)for t in bm_tickers:if t notin ['BOXX']:print(f'{t} is ranked {rank_data.at[t, "rank"]}')rank_data.drop(columns = ['ETF Desc','url', 'launch_date']).head(20)
SPY is ranked 14.0
BRK-B is ranked 5.0
GLD is ranked 16.0
rank_10y
rank_5y
rank_3y
points
rank
ticker
IAK
122.0
3.0
10.0
135.0
1.0
OEF
4.0
73.5
133.0
210.5
2.0
IWL
11.0
64.0
144.0
219.0
3.0
IVV
19.5
68.0
173.0
260.5
4.0
BRK-B
6.0
42.0
215.0
263.0
5.0
SGOL
23.0
196.5
47.5
267.0
6.0
DXJS
207.0
49.0
32.0
288.0
7.0
OUNZ
28.0
214.0
54.0
296.0
8.0
IAU
25.0
220.0
53.0
298.0
9.0
KBWP
65.0
53.0
182.5
300.5
10.0
LGLV
24.0
47.5
233.0
304.5
11.0
IYF
128.0
92.0
86.0
306.0
12.0
IOO
42.0
66.0
214.0
322.0
13.0
SPY
7.0
89.0
232.0
328.0
14.0
IAI
68.0
75.0
186.0
329.0
15.0
GLD
33.5
244.0
56.5
334.0
16.0
QDEF
49.0
112.0
182.5
343.5
17.0
IWB
39.0
96.0
210.0
345.0
18.0
PPA
93.0
110.5
143.0
346.5
19.0
FIDU
116.5
47.5
184.0
348.0
20.0
Figure 8: top 1% of all ETFs looking at 10y, 5y, and 3y period performances
Code
n =20r =1print('::: {.callout-caution title = "Expand to see Top 20 by Name:" collapse="true"}')for i, row in rank_data.head(n).iterrows():if i in df_screen['name'].tolist(): md_str =f'{r}. [{i}](https://www.tradingview.com/symbols/{df_screen[df_screen["name"]==i]["ticker"].tolist()[0].replace(":","-")}/)' md_str +=f'(est. {datetime.strftime(df_screen[df_screen["name"]==i]["launch_date"].tolist()[0].date(), "%b %Y")}' md_str +=f", Return: {df_10y_leaderboard.at[i,'Annualized Return']*100:.2f}%)" md_str +=f' - {row["ETF Desc"]}'print(md_str)else:print(f'{r}. {i}') r+=1print(":::")
Expand to see Top 20 by Name:
IAK(est. Apr 2006, Return: 12.28%) - iShares U.S. Insurance ETF
OEF(est. Oct 2000, Return: 12.82%) - iShares S&P 100 ETF
IWL(est. Sep 2009, Return: 12.52%) - iShares Russell Top 200 ETF
FIDU(est. Oct 2013, Return: 10.12%) - Fidelity MSCI Industrials Index ETF
5-year: 2 periods
Code
rank_data = []for t in df_5y_leaderboard.index.tolist():if t notin ['BOXX']: rank_data.append( {'ticker': t,'rank_5y': df_5y_leaderboard.at[t,"ranking"],'rank_3y': df_3y_leaderboard.at[t,"ranking"],'ETF Desc': df_5y_leaderboard.at[t, 'ETF Desc'],'url': df_5y_leaderboard.at[t, 'url'],'launch_date': df_5y_leaderboard.at[t, 'launch_date'] } )rank_data = pd.DataFrame(rank_data)rank_data['points'] = rank_data['rank_5y'] + rank_data['rank_3y']rank_data['rank'] = rank_data['points'].rank(ascending =True)rank_data = rank_data.set_index('ticker', drop =True)rank_data = rank_data.sort_values(by = ['rank'], ascending=True)for t in bm_tickers:if t notin ['BOXX']:print(f'{t} is ranked {rank_data.at[t, "rank"]}')rank_data.drop(columns = ['ETF Desc', 'url', 'launch_date']).head(20)
SPY is ranked 95.0
BRK-B is ranked 63.5
GLD is ranked 89.0
rank_5y
rank_3y
points
rank
ticker
IAK
3.0
10.0
13.0
1.0
CEFS
9.0
38.0
47.0
2.0
PTNQ
33.0
15.0
48.0
3.0
PTLC
20.0
34.0
54.0
4.0
TPYP
2.0
73.0
75.0
5.0
DXJS
49.0
32.0
81.0
6.0
FLRT
82.0
1.0
83.0
7.0
POCT
57.0
26.5
83.5
8.0
LVHI
22.0
65.5
87.5
9.0
VRIG
90.0
8.0
98.0
10.0
USAI
7.0
92.5
99.5
11.0
EMLP
27.0
75.0
102.0
12.0
AUSF
1.0
102.0
103.0
13.0
BSJP
97.0
20.0
117.0
14.0
PULS
113.0
9.0
122.0
15.0
FLJH
30.0
98.0
128.0
16.0
EINC
38.0
104.0
142.0
17.0
DYNF
72.0
72.0
144.0
18.0
DXJ
19.0
134.0
153.0
19.0
GSST
138.0
19.0
157.0
20.0
Figure 9: top 1% of all ETFs looking at both 5y and 3y period performances
Code
n =20r =1print('::: {.callout-caution title = "Expand to see Top 20 by Name:" collapse="true"}')for i, row in rank_data.head(n).iterrows():if i in df_screen['name'].tolist(): md_str =f'{r}. [{i}](https://www.tradingview.com/symbols/{df_screen[df_screen["name"]==i]["ticker"].tolist()[0].replace(":","-")}/)' md_str +=f'(est. {datetime.strftime(df_screen[df_screen["name"]==i]["launch_date"].tolist()[0].date(), "%b %Y")}' md_str +=f", Return: {df_5y_leaderboard.at[i,'Annualized Return']*100:.2f}%)" md_str +=f' - {row["ETF Desc"]}'print(md_str)else:print(f'{r}. {i}') r+=1print(":::")
Expand to see Top 20 by Name:
IAK(est. Apr 2006, Return: 22.01%) - iShares U.S. Insurance ETF
CEFS(est. Mar 2017, Return: 15.65%) - Saba Closed-End Funds ETF
PTNQ(est. Jun 2015, Return: 14.36%) - Pacer Trendpilot 100 ETF
PTLC(est. Jun 2015, Return: 13.51%) - Pacer Fund Trust Pacer Trendpilot US Large Cap ETF
TPYP(est. Jun 2015, Return: 23.37%) - Tortoise North American Pipeline ETF
DXJS(est. Jun 2013, Return: 17.73%) - WisdomTree Japan Hedged SmallCap Equity Fund
FLRT(est. Feb 2015, Return: 6.75%) - Pacer Funds Pacer Pacific Asset Floating Rate High Income ETF
POCT(est. Sep 2018, Return: 9.80%) - Innovator U.S. Equity Power Buffer ETF - October
LVHI(est. Jul 2016, Return: 13.36%) - Franklin International Low Volatility High Dividend Index ETF
USAI(est. Dec 2017, Return: 32.35%) - Pacer American Energy Independence ETF
EMLP(est. Jun 2012, Return: 16.80%) - First Trust North American Energy Infrastructure Fund
AUSF(est. Aug 2018, Return: 19.34%) - Global X Funds Global X Adaptive U.S. Factor ETF
BSJP(est. Sep 2017, Return: 6.97%) - Invesco BulletShares 2025 High Yield Corporate Bond ETF
PULS(est. Apr 2018, Return: 3.78%) - PGIM Ultra Short Bond ETF
FLJH(est. Nov 2017, Return: 16.45%) - Franklin FTSE Japan Hedged ETF
EINC(est. Mar 2012, Return: 29.01%) - VanEck Energy Income ETF
DYNF(est. Mar 2019, Return: 16.91%) - iShares U.S. Equity Factor Rotation Active ETF
DXJ(est. Jun 2006, Return: 22.10%) - WisdomTree Japan Hedged Equity Fund
GSST(est. Apr 2019, Return: 3.49%) - Goldman Sachs Ultra Short Bond ETF
Conclusion
This is a lot of data to look at and we found a lot of interseting ETFs along the way. We’ll do a deep dive on a different post but for now, some key observations are:
Beating the market
beating the market and doing so with less downside risk is possible! It’s definitely harder to do it in the long run and consistently when looking at multiple periods.
One such funds that have done that is Mr Buffett’s BRK-B, and as this leaderboard revealed, there are >10 investments over the last 10-year (BRK-B included) that have “beaten” the market. So the index funds that’s most likely “Warren approved” would be IAK, OEF, IWL, and IVV.
Innovation or Luck
Buying NVDA in the early 2010s to become a millionaire like this guy is luck. Making >300% return in a year like this other guy5 is skill. Putting that skill to work in creating an investment product is innovation.
Looking at Figure 9, you’d see a lot of these innovation that have came out in the recent years6. Tickers like PTNQ, PTLC, POCT, LVHI, AUSF, and DYNF all follow some pretty complex strategies from using FLEX options, incorporating technical factors, to factor rotation.
Looking at the top-right quadrant of Figure 7 we can find a few more interesting ones:
RISR7 stands out in Figure 7 because it bet the SPY with 13% annualized returns while only seeing a Max Drawdown of 15%. But it was able to do that because it’s a hedge for rising interest rates, which was the trend for the last 3 years. So the outperformance vs SPY is somewhat “luck”, the “innovation” part is that it’s an actively managed fund that primarily invests in interest-only MBSs and US Treasury (definitely not something that’s accessible to the retail investors). It’s unlikely thatRISR will continue to out perform the market but it has proven it’s own marketing as a rising interest rate hedge.
BGLD is invested in Gold (like GLD) but with downside protection using FLEX options. Over the 3-year period, it achieved a GPR of 1.26 vs GLD’s 1.21
BALT is like BGLD but reference the S&P 500. The result is impressive, GPR of 1.36 vs SPY’s 1.10
the innovations in the ETF space also got me thinking about ways to potentially just capture the relative performance8 without the underlying risk.
For example, it’s well known that Cathie Wood’s ARKK consistently underperforms. So how would I capitalize on that without being exposed to swings in the tech sector?
Perhaps by buying a fairly consistent tech ETF from Figure 11, like IYW, and staying “market neutral” by shorting ARKK?
Or similarly, buying SMH and shorting XSD in the semi-conductor space.
if you look close at the bottom left quadrant of Figure 3, Figure 5, and Figure 7, you will consistently see VIXY which is the VIX short-term futures ETF. It’s definitely NOT meant as an investment product but more of a hedge for black swan events9.
But as an investment, it looks like it has consistently capture the negative roll yield of the VIX futures market, which is beyond the scope of this post10. But this is an example to show that while there are more innovative ETFs, it’s always worth looking at some data to see if they confirm what the strategies claim to do.
Figure 13: Volatility Futures ETFs are meant more for hedging than for buy-and-hold investing
Dividends
one other consideration for the non-Americans is withholding tax on dividend. Our analysis here is focused on capital gains only. But just as a disclaimer, here’s the number of ETFs paying dividends on a regular basis.
Code
div_summary = [ {"label": "dividend paying", 'count': sum(df_3y_metrics['div_count']>0)}, {"label": "no dividend", 'count': sum(df_3y_metrics['div_count']==0)},]pie_div = px.pie(pd.DataFrame(div_summary), values ="count", names ='label', title ="Dividend Paying vs Non-Paying ETFs")display(pie_div)
Figure 14: Percentage of ETFs paying dividends in the 3-year Club
Narrowing the Search
If I’m just interested in finding something to invest in a specific country or say I strongly believe in a given theme. We could just re-run the same analysis on a reduced universe of ETFs.
Here are some ideas:
Country Specific (e.g. Hong Kong or Canadian ETFs)
actually this article makes the case for when this might not even be a good hedge at all during periods of high market volatility but some (like this guy) might still recommend it.↩︎
this and this post goes into details of how these VIX futures ETFs need to roll futures contracts on a regular basis and how value has consistently be lost to negative roll yield since the VIX futures curve is in contango 82% of the time (since VXX’s inception)↩︎