Temporización de los mercados con flujos de fondos de ETF

Debido al mal momento de la inversión, el inversionista bursátil promedio va a la zaga del desempeño del mercado. Las fuerzas psicológicas de perderse (FOMO) son fuertes pero conducen a rendimientos de inversión débiles. Un inversor inteligente puede utilizar los flujos de fondos de ETF para ayudar a cronometrar el mercado.

He estado ocupado últimamente y es difícil creer que han pasado casi dos meses desde mi última publicación. Decidí publicar algo hoy y pensé que sería bueno explicar algunas de las grandes manipulaciones de datos de pandas que se ven en Our Own Worst Enemy de Chad Gray. No quiero quitarle nada a sus lectores; Solo desarrollaré el código y las matemáticas para ayudar a los operadores algorítmicos menos experimentados y espero que lea este artículo para llenar los vacíos donde sea necesario.

Con esto fuera del camino, ¡comencemos!

Acerca de los flujos de fondos

El entendimiento crítico es que los flujos de fondos de ETF crean o destruyen acciones en circulación al final de cada día, dependiendo de las entradas y salidas del ETF. Podemos determinar la brecha de comportamiento al rastrear los cambios diarios en las acciones en circulación en comparación con el precio.

Usaré los flujos de fondos ETF publicados por ETF mundial. Estos datos incluyen divisiones y dividendos, por lo que no necesitaremos realizar ningún ajuste.

Obtener los datos de precios de ETF

Primero, veamos las importaciones. Tomaré los datos ETF de mi base de datos PSQL local. Estas son las importaciones que comienzan con app. Si está interesado en hacer lo mismo, puede crear su propio Base de datos PSQL.

import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import pandas as pd
from app.db.psql import db, session
from app.models.etf import EtfFundFlow, \
                           get_etf_fund_flows, \
                           get_sector_tickers

A continuación, cree una función que agregue los retornos diarios simples (R) y los retornos de registro diarios (r) al marco de datos get_etf_fund_flows.

def create_flow_data(tickers, start, end):
    ## Using convention for return identification:
    ## Simple returns denoted with capital R
    ## log returns identified by lowercase r
    etf_fund_flows = get_etf_fund_flows(tickers, start, end)
    etf_fund_flows['daily_R'] = etf_fund_flows['nav'].groupby(level='ticker').pct_change()
    etf_fund_flows['daily_r'] = np.log(etf_fund_flows['nav']).groupby(level=0).diff()
    etf_fund_flows['flow'] = np.log(etf_fund_flows['shares_outstanding']).groupby(level=0).diff()
    etf_fund_flows['mktcap'] = etf_fund_flows['nav'] * etf_fund_flows['shares_outstanding']
    etf_fund_flows.dropna(inplace=True)
    return etf_fund_flows

Con los datos en el formato que necesitamos, visualicemos las acciones en circulación vs. precio.

ticker = 'XLE'
xle = etf_fund_flows.loc[ticker]
xle['shares_outstanding'] \
       .plot(figsize=(16,10), legend=True)
xle['nav'].rename('price') \
       .plot(title=f"{ticker}: Shares Outstanding vs. Price",
             legend=True,
             secondary_y=True)
plt.show()

Observe cómo el nav, o el precio, está separado de las acciones en circulación.

Definir las fórmulas de devolución

Comenzaremos creando una función que devuelva el rendimiento anual simple de un marco de datos de flujos de fondos ETF. La función toma la media de la devolución del registro diario, la multiplica por 252, que es el número promedio de trading días por año, “invierte” la operación de registro utilizando np.exp y lo convierte a un porcentaje anual.

Los convertimos en retornos de registro y luego los transformamos nuevamente en retornos simples. Imagínese si compramos una acción por 1,00 que sube un 100 % y luego baja un 50 %, nuestro rendimiento geométrico total sería 0 %: 1,00 * (1+1) * (1+ -0,5) = $1,00.

¿Observa cómo el retorno anterior afectó al siguiente? La media aritmética no considera esta relación y sería engañosa, mostrando un rendimiento del 25 %: (1 + -0,5) / 2 = 25 %. Las devoluciones de logs no tienen esta limitación permitiéndonos sumarlas o multiplicarlas, resolviendo este problema.

def calc_etf_return(df):
    avg_daily_r = df['daily_r'].mean()
    annual_ret_log = avg_daily_r * 252
    annual_ret_simple = np.exp(annual_ret_log) - 1
    return annual_ret_simple * 100

A continuación, necesitamos calcular el rendimiento del inversor.

Podemos determinar cuánto está invirtiendo el inversionista promedio en cualquier ETF al observar los cambios diarios en las acciones en circulación multiplicados por los cambios diarios en la capitalización de mercado. Esto supone que estamos comprando todas nuestras acciones al nuevo valor de capitalización de mercado, pero nos acerca lo suficiente. Veamos esto con un ejemplo.

Imagina que el día cero tenemos 1,00 invertidos. Si duplicamos nuestras acciones en circulación, pero la capitalización de mercado cae un 50 %, nuestra nueva cantidad invertida es 1 (día cero) + 1 * 1/2 (día uno) = 1,50. Luego, en el día dos, reducimos nuestras acciones a la mitad con la capitalización de mercado igual: 1,50 * 1/2 = 0,75. Ahora, para cada día, sabemos nuestra cantidad invertida. Tiene sentido, ¿verdad?

Ahora podemos tomar los rendimientos diarios multiplicados por la cantidad diaria que hemos invertido dividido por la cantidad promedio invertida. Esto nos da un rendimiento para cada día. Como se muestra a continuación, podemos tomar el promedio de todos esos rendimientos y volver a convertirlo en un rendimiento simple.

def calc_investor_return(df):
    flows = df['flow'] * (df['mktcap'] / df['mktcap'].iloc[0])
    flows.iloc[0] = 1
    basis = flows.cumsum()

    avg_daily_r = (df['daily_r'] * basis / basis.mean()).mean()
    annual_ret_log = avg_daily_r * 252
    annual_ret_simple = np.exp(annual_ret_log) - 1
    return annual_ret_simple * 100

Con las funciones de rendimiento del ETF y del inversor definidas, creemos una función para comparar ambos rendimientos anuales con fines gráficos futuros.

Primero, tomamos los tickers obteniendo el primer nivel del índice múltiple, pasándolo a único y luego convirtiéndolo en una lista.

tickers = df.index.get_level_values(0).unique().tolist()

A continuación, recorremos cada ticker, calculando el inversor y el retorno de la inversión. Usamos .resample(‘A’) para agrupar nuestro marco de datos en años (anuales) y luego aplicamos nuestras funciones de devolución. Luego concatenamos los datos junto con el índice y los devolvemos. Predeterminado a la concatenación antes de fusionar funciones, ya que es más eficaz.

Si el remuestreo y la manipulación de datos de tiempo son nuevos para usted, lea Análisis de series de tiempo con Python.

out = pd.DataFrame()
    for ticker in tickers:
        twr = df.loc[ticker].resample('A').apply(calc_etf_return)
        mwr = df.loc[ticker].resample('A').apply(calc_investor_return)
        twr.name = 'twr'
	mwr.name = 'mwr'
        both = pd.concat([twr, mwr], axis=1).reset_index()
        both['ticker'] = ticker
        both['timing_impact'] = both['mwr'] - both['twr']
        both.set_index(['date', 'ticker'], inplace=True)
        out = pd.concat([out, both], axis=0)
    return out

Con las fórmulas definidas, ahora es fácil analizar los resultados. Vamos a crear una lista de tickers para explorar la brecha de comportamiento. Crearemos una lista de tickers y los pasaremos a las funciones que creamos anteriormente.

tickers = ['SPY', 'IWM', 'QQQ', 'VT']
flows = create_flow_data(tickers, start, end)

results = pd.DataFrame(columns=['investment', 'investor'])
for ticker in tickers:
   tmp = flows.xs(ticker, level='ticker', drop_level=True)
   print(tmp)
   results.loc[ticker, 'investment'] = calc_etf_return(tmp)
   results.loc[ticker, 'investor'] = calc_investor_return(tmp)
   results['behavioral_gap'] = results['investor'] - results['investment']
   print(results)
investment          investor      behavioral_gap
SPY    12.134     11.72          -0.414001
IWM    7.47855   7.15249     -0.326054
QQQ   19.0069   18.2117      -0.795162
VT       8.38496   7.82806      -0.556897

Curiosamente, con datos actualizados, los inversores tienen un rendimiento inferior al índice de referencia.

Usemos un poco de magia de pandas para graficar esto.

by_year = compare_annual(flows)['timing_impact']
print(by_year)
date        ticker
2017-12-31  IWM       0.256712
2018-12-31  IWM       -1.601035
2019-12-31  IWM       1.562052
2017-12-31  QQQ      -0.132723
2018-12-31  QQQ      -1.543145
2019-12-31  QQQ      -0.639015
2017-12-31  SPY       -0.033773
2018-12-31  SPY       -0.310273
2019-12-31  SPY        0.025464
2017-12-31  VT          -0.120763
2018-12-31  VT          -0.811108
2019-12-31  VT          1.075920

Nuestro multiíndice contiene tanto la fecha como el teletipo. Podemos desapilar este índice múltiple para convertir el índice más interno, que es el ticker, en columnas. También redondearemos a dos decimales. Poniendolo todo junto:

by_year = compare_annual(flows)['timing_impact'].unstack().round(3)
print(by_year)
ticker        IWM    QQQ    SPY     VT
date
2017-12-31  0.257 -0.133 -0.034 -0.121
2018-12-31 -1.601 -1.543 -0.310 -0.811
2019-12-31  1.562 -0.639  0.025  1.076

Entonces podemos usar nacido en el mar para crear un mapa de calor para ver la brecha de comportamiento visualmente. Convertimos el índice solo al año con fines gráficos.

by_year.index = by_year.index.year
sns.heatmap(by_year,
            center =0.00,
            cmap = sns.diverging_palette(10, 220, sep=1, n=21),
            annot=True)
plt.title('Behavioral Gap Heatmap')
plt.show()

Con suerte, esta publicación ayuda a aquellos en su camino para volverse competentes con los pandas y brinda a algunos de mis lectores una nueva señal potencial contraria para explotar.

Deja un comentario