En esta entrega vamos a tratar de manera simple la idea de explotar la estacionalidad en Python mediante portfolios, como de costumbre.Esta vez, tratamos de batir al indice de referencia, en la anterior entrega, basada en las ideas de Ray Dalio,no se consiguió, no obstante esta nueva entrega, eliminará ese mal sabor de boca que nos produjo la estrategia(para todos los publicos) que ofrecia el CEO de Bridgewater Associates.

La premisa principal es muy básica. Por razones que desconozco, y que considero que enumerarlas, es caer en un sesgo narrativo fehaciente, no voy a tunelar sobre las mismas.

Realizando estudios estacionales sobre la renta variable americana, y el activo refugio por excelencia, el oro ,se puede comprobar que los meses, que historicamente hacen retroceder a la renta variable americana, son los mismos que hacen avanzar al oro. Por dicho motivos buscamos encontrar la estacionalidad via python para dichos activos.

En la siguiente imagen, de un gráfico normalizado de la estacionalidad del indice SPX de los últimos 20 años, podemos observar que hay unas zonas criticas (bajo mi sesgo), que podríamos delimitar entre mitades de diciembre(El fin del rally de navidad) hasta mitades de marzo, y agosto, el mes donde todos los grandes gestores están de vacaciones, y la liquidez se desvanece.

S&P 500 Index Seasonal Chart
Grafico estacional del SPX (20 años)
$SPX Monthly Averages
Retornos estacionales de SPX de los ultimos 20 años.

En el caso del oro es algo distinto, podemos ver que desde mitades de diciembre hasta finales de febrero, es alcista, y ademas el verano le sienta muy bien, desde mitades de julio hasta mitades de septiembre, tiene un avance importante. Y es un gran logro encontrado gracias a la estacionalida en python

estacionalidad en python del oro
Gold Futures (GC) Seasonal Chart
Grafico estacional del futuro GC(20 años)
FUTURE_GC1 Monthly Averages
Retornos estacionales del oro de los ultimos 20 años.

Una vez encontrada la fuente de valor, puede ser una casualidad espuria, un sesgo del autor de estas lineas, o una ventaja real. Pero hay algo, que se debe analizar en profundidad, y a por ello vamos.

Vamos a hacer la ventaja mas solida, mediante la anomalia de la baja volatilidad, que se puede apreciar en la siguiente imagen.

Fuente: OptionMetrics

Estacionalidad en Python, creando el portfolio

Como se puede apreciar, los ETFs que replican los indices de baja volatilidad, están batiendo sistematicamente desde su creación a el indice convencional, tanto en rentabilidad, como en volatilidad anualizada, y drawdowns. Por consecuencia, vamos a utilizar un etf que replique un indice de baja volatilidad, en lugar de utilizar un etf que replique directamente

Unas vez explicadas, las dos ventajas fundamentales que vamos a explotar en este portfolio, vamos a definir las reglas que trabajaremos:

  • Universo de activos : $SPLV & $GLD
  • Fechas Clave $GLD : 20-12 hasta el 20-2 y del 1-8 al 31-8
  • Fechas Clave $SPLV : desde el 21-2 hasta el 30-7, y desde el 1-9 hasta el 20-12
  • Tamaño de la posicion : 100%
  • TP & SL : No aplica

Una vez justificadas las ventajas y establecidas las reglas, a crear Portfolios estacionales en Python

Cargamos las librerias basicas

In [3]:
#Basicos para todo
import pandas as pd
import numpy as np 
import matplotlib.pyplot as plt

#Herramientas Financieras
import yfinance as yf
import quantstats as qs
import ffn 
import pyfolio as pf

#Limpieza de avisos
import warnings
warnings.filterwarnings(action='ignore')

#Configuraciones Basicas
%matplotlib inline
plt.style.use('ggplot')
plt.rcParams["figure.figsize"] = [20,9]

Descargamos los tickers y los añadimos en un nuevo DataFrame

In [14]:
splv = yf.download("SPLV")
gld = yf.download("GLD")
spy = yf.download("SPY")
dataframe =pd.DataFrame()
dataframe['SPLV'] = splv['Adj Close']
dataframe['GLD'] = gld['Adj Close']
dataframe['SPY'] = spy['Adj Close']
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed
[*********************100%***********************]  1 of 1 completed

Creamos las señales que nos indicaran en que activo debemos estar invertidos

Vamos a crear dos señales, 1 para el oro, y 0 para el indice de renta variable,
y mediante .loc vamos a acceder a las fechas del indice con condiciones, para asignarlo a una colunmna.

In [15]:
dataframe.loc[(dataframe.index.month==12) & (dataframe.index.day>=20), 'signal'] = 1
dataframe.loc[(dataframe.index.month==1),'signal'] = 1
dataframe.loc[(dataframe.index.month==2) & (dataframe.index.day<=20), 'signal'] = 1
dataframe.loc[(dataframe.index.month==8) ,'signal'] = 1
dataframe['signal'] = np.where(dataframe['signal']==1,1,0)

Calculamos los retornos porcentuales de los activos

In [16]:
dataframe['gold_pct'] = dataframe['GLD'].pct_change()
dataframe['splv_pct'] = dataframe['SPLV'].pct_change()
dataframe['spy_pct']  = dataframe['SPY'].pct_change()

Creamos el retorno del portfolio

Le indicamos que cuando signal == 1, utilice los retornos del oro, y cuando no, que utilice los de la renta variable

In [25]:
dataframe['returns'] = np.where(dataframe['signal']==1,dataframe['gold_pct'],dataframe['splv_pct'])

Ploteamos los retornos de los tickers utilizados

In [26]:
plt.figure(figsize=(14,7))
plt.plot(dataframe['spy_pct'].cumsum(), color='r', label='$SPY')
plt.plot(dataframe['splv_pct'].cumsum(),color='b', label='$SPLV')
plt.plot(dataframe['gold_pct'].cumsum(),color='g', label='$GLD')
plt.legend()
plt.show()

Observavamos como SPY y SPLV van siempre en una pelea constante entre ellos, y el oro esta en otro universo paralelo, comprobamos que no es muy buena idea almacenar posiciones en oro durante largos peridos de tiempo, pues conlleva un coste de oportunidad muy alto

Comparacion de la curva de resultados

Creamos unos cuantos resultados aleatorios (siempre hay que compararse con el random walk, o analisis tecnico como lo llaman ahora 😉 )

In [41]:
dataframe['random1'] = np.random.randint(2, size=len(dataframe))
dataframe['random1'] = np.where(dataframe['random1'] == True, dataframe['spy_pct'], -0)
dataframe['random2'] = np.random.randint(2, size=len(dataframe))
dataframe['random2'] = np.where(dataframe['random2'] == True, dataframe['spy_pct'], -0)
In [50]:
plt.figure(figsize=(14,7))
plt.plot(dataframe['returns'].cumsum(), color='r', label='Portfolio')
plt.plot(dataframe['splv_pct'].cumsum(),color='b', label='$SPLV')
plt.plot(dataframe['spy_pct'].cumsum(),color='g', label='$SPY')
plt.plot(dataframe['random1'].cumsum(),color='y', label='Random1')
plt.plot(dataframe['random2'].cumsum(),color='grey', label='Random2')
plt.legend()
plt.show()

a simple vista, batimos al indice, batimos al random walk y batimos a nuestro activo de renta variable. Parece que hemos conseguido el objetivo de batir al indice, de una forma sencilla y aburrida, pero rentable.

Ahora damos paso a las metricas de la cartera para tener algo mas de informacion, y poder evaluar un poco mejor.

In [52]:
qs.reports.full(dataframe['returns'],'SPY')

Performance Metrics

                           Strategy    Benchmark
-------------------------  ----------  -----------
Start Period               2011-05-05  2011-05-05
End Period                 2020-01-16  2020-01-16
Risk-Free Rate             0.0%        0.0%
Time in Market             99.0%       100.0%

Cumulative Return          445.75%     191.84%
CAGR%                      21.52%      13.09%
Sharpe                     1.71        0.93
Sortino                    2.61        1.3
Max Drawdown               -11.41%     -19.35%
Longest DD Days            266         272
Volatility (ann.)          11.84%      14.38%
R^2                        0.18        0.18
Calmar                     1.89        0.68
Skew                       -0.02       -0.48
Kurtosis                   2.37        4.82

Expected Daily %           0.08%       0.05%
Expected Monthly %         1.63%       1.03%
Expected Yearly %          18.49%      11.3%
Kelly Criterion            11.15%      8.32%
Risk of Ruin               0.0%        0.0%
Daily Value-at-Risk        -1.15%      -1.44%
Expected Shortfall (cVaR)  -1.65%      -1.65%

Payoff Ratio               1.01        0.95
Profit Factor              1.34        1.19
Common Sense Ratio         1.59        1.12
CPC Index                  0.75        0.62
Tail Ratio                 1.18        0.95
Outlier Win Ratio          3.91        3.61
Outlier Loss Ratio         4.27        3.57

MTD                        2.13%       2.5%
3M                         6.39%       10.93%
6M                         14.68%      11.11%
YTD                        2.13%       2.5%
1Y                         31.95%      29.12%
3Y (ann.)                  19.66%      15.45%
5Y (ann.)                  19.13%      12.83%
10Y (ann.)                 21.52%      13.09%
All-time (ann.)            21.52%      13.09%

Best Day                   4.02%       5.05%
Worst Day                  -3.75%      -6.51%
Best Month                 12.27%      10.92%
Worst Month                -7.01%      -8.8%
Best Year                  29.93%      32.31%
Worst Year                 2.13%       -5.38%

Avg. Drawdown              -1.4%       -1.48%
Avg. Drawdown Days         16          16
Recovery Factor            39.05       9.92
Ulcer Index                1.0         1.02

Avg. Up Month              3.01%       2.94%
Avg. Down Month            -1.86%      -3.65%
Win Days %                 55.46%      55.38%
Win Month %                68.57%      72.38%
Win Quarter %              86.11%      80.56%
Win Year %                 100.0%      80.0%

Beta                       0.35        -
Alpha                      0.16        -
None

5 Worst Drawdowns

StartValleyEndDaysMax Drawdown99% Max Drawdown
12015-01-232015-06-292015-10-16266-11.413961-11.148054
22016-08-032016-11-042017-01-10160-9.222568-8.810021
32012-10-192013-02-252013-04-19182-8.435467-8.028487
42011-08-232011-10-032011-10-2462-7.772288-7.249426
52013-05-202013-06-202013-07-1859-7.130540-6.421871

Strategy Visualization

findfont: Font family ['Arial'] not found. Falling back to DejaVu Sans.
In [56]:
pf.create_interesting_times_tear_sheet(dataframe['returns'],dataframe['SPLV'].pct_change())
Stress Eventsmeanminmax
US downgrade/European Debt Crisis0.39%-3.75%3.54%
EZB IR Event0.04%-0.70%1.18%
Apr140.08%-1.26%0.85%
Oct140.21%-1.53%1.84%
Fall20150.05%-2.52%2.23%
Recovery0.09%-3.75%3.54%
New Normal0.08%-3.03%4.02%