Se conoce como Backtesting al proceso o metodología retrospectiva por la cual comprobaremos una serie de hipótesis o suposiciones en el mercado. Simulamos condiciones en el pasado a fin de proyectar a futuro unos posibles resultados que hayan sido contrastados. Nos permite validar una estrategia concreta, fabricar o modificar una en base a unas conclusiones.
Sin embargo esta metodología no está exenta de problemática y dificultades: Tamaño y calidad de la muestra, comisiones, gestión de capital, liquidez, sesgo de supervivencia, overfitting...
En este artículo vamos a detallar una posible estructura de Backtest automatizada en Python para un indicador como el de las medias móviles para todas las compañías del SP500.
Vamos con el código:
import yahoo_fin.stock_info as si
import pandas as pd
import numpy as np
import yfinance as yf
tickers = si.tickers_sp500()
resumen=pd.DataFrame(columns=['Ticker','Mean 25',' %', 'Mean 50',' %','Mean 100', ' %','Mean 200',' %' ])
Creamos una variable tickers
con los activos del sp500 y un DataFrame
con las columnas especificadas, mediremos la % de cada condición para cuatro medias: 25,50,100 y 200.
capi=float(100000)
cf=0.5
cv=0.0003 # 0.3 por mil
ct=float(0) # Total comisiones por ticker y estrategia (cf+cv)
tit=int()
resultado=float()
Declaramos una serie de variables que serán necesarias para el Backtest: Un capital, unas comisiones fijas y variables, una cantidad de stocks y un resultado con la rentabilidad.
for i in range(len(tickers)):
df =yf.download(tickers[i],'2021-01-01', progress=False)['Close']
resumen.loc[i,'Ticker']= tickers[i]
""" Calculamos distintas medias """
mean25 = df.rolling(25).mean()
mean50 = df.rolling(50).mean()
mean100 = df.rolling(100).mean()
mean200 = df.rolling(200).mean()
x=1 # controlamos columna importe y % para cada estrategia a modo de contador
p=0
Empezamos definiendo un bucle for
que englobará todo el programa, para el rango de tickers, en el que descargamos los precios de cierre de cada activo en cada iteración, calculando sus medias móviles e introduciendo cada ticker en el DataFrame
de resumen.
Creamos variables x
,
p
a modo de contador. En x
controlaremos la columna del importe y el % de cada estrategia, y en p
el control de la estrategia usando las mismas variables (de forma iterativa se irán comprobando las condiciones media > precio de cierre)
while p<4:
cash=capi
if p==0:
data = pd.DataFrame(np.where((mean25 < 0.98*df),1,0), index=df.index)
elif p==1:
data = pd.DataFrame(np.where((mean50 < 0.97*df),1,0), index=df.index)
elif p==2:
data = pd.DataFrame(np.where((mean100 < 0.96* df),1,0), index=df.index)
else:
data = pd.DataFrame(np.where((mean200 < 0.95*df),1,0), index=df.index)
Utilizamos numpy
para determinar con 1's o 0's si los precios de cierre están por encima o por debajo (con un poco de margen: 0.9x). En cada iteración p
irá incrementando (lo veremos más adelante).
signal = data.diff()
signal['Titulos']=0
signal['Coste']=0
signal['Venta']=0
signal['Comision']=0
Con diff()
tenemos los momentos de cruce exactos (1,-1) y creamos cuatro columnas en signal
para realizar nuestro backtest.
for z in range(len(df)):
""" Dada la condición de compra calculamos qué cantidad podemos comprar"""
if signal.iloc[z,0]>0: # señal de compra
signal.iloc[z,1]= cash//df[z] # numero de acciones
tit+=signal.iloc[z,1] # acumulamos títulos pq no sabemos cuándo se venderán y vamos a borrar registros a cero
signal.iloc[z,2]=round(signal.iloc[z,1]*df[z],2) # coste de la compra
signal.iloc[z,4 ]= round(cf+signal.iloc[z,2]*cv,2)
ct=ct+cf+signal.iloc[z,2]*cv
cash+=-signal.iloc[z,2]-signal.iloc[z,4] # mantenemos acumulado cash neto
Trabajamos con otro bucle for
dentro del anterior while
para, partiendo de señales de compra, delimitar el número de acciones compradas (// para evitar fraccionamientos) ponderadas por las comisiones, mantener el efectivo acumulado.
if signal.iloc[z,0]<0 : # señal de venta
signal.iloc[z,3]=round(tit *df[z],2) # importe venta
signal.iloc[z,4]= round(cf+signal.iloc[z,3]*cv,2)
ct=ct+cf+signal.iloc[z,2]*cv
signal.iloc[z,1]=-tit # cuenta títulos
tit=0
cash+=signal.iloc[z,3]-signal.iloc[z,4]
A continuación trabajamos con señales de venta (<0): Calculamos el importe multiplicando el stock disponible con su correspondiente precio de cierre, calculamos las comisiones totales ct
, el incremento del cash y reescribimos la variable de los títulos a 0.
signal=signal.fillna(0)
signal=signal.loc[(signal!=0).any(axis=1)]
""" Pantallazo de la estrategia"""
print('Ticker: ',tickers[i], 'Estgia: ',p)
print(signal)
Borramos los registros sin señal de compra o de venta (de ahí haber guardado tit
y cash
fuera del DataFrame
)
ct=0
resultado=round(float(tit*df[-1:] + cash),2)
porcentaje=round((resultado/capi-1)*100,2)
print('Resultado: ',resultado, ' ', porcentaje,' %' )
Reiniciamos la variable de comisiones y calculamos el resultado total (suma de efectivo más acciones a precio de mercado y calculamos el % de rentabilidad para cada iteración (con las cuatro medias móviles distintas por separado).
"""Guardamos en resumen para lista objetivo , manipulamos x e y para lograr encolumnarlos correctamente"""
resumen.iloc[i,(x)]=resultado
resumen.iloc[i,(x+1)]=porcentaje
tit=0
x+=2
p+=1
Encolumnamos resultado y porcentaje con iloc[i,(x)]
e iloc[i,(x+1)]
haciendo referencia al primer bucle for
.
print(resumen) # Pantallazo del avance título a título.
Finalmente obtenemos un DataFrame
505x9 con los resultados de las cuatro estrategias para todo el SP500.
Aquí os dejo el código entero:
import yahoo_fin.stock_info as si
import pandas as pd
import numpy as np
import yfinance as yf
tickers = si.tickers_sp500()
resumen=pd.DataFrame(columns=['Ticker','Mean 25',' %', 'Mean 50',' %','Mean 100', ' %','Mean 200',' %' ])
capi=float(100000)
cf=0.5
cv=0.0003 #0.3 por mil
ct=float(0) # Total comisiones por ticker y estrategia (cf+cv)
tit=int()
resultado=float()
for i in range(len(tickers)):
df =yf.download(tickers[i],'2021-01-01', progress=False)['Close']
resumen.loc[i,'Ticker']= tickers[i]
""" Calculamos distintas media """
mean25 = df.rolling(25).mean()
mean50 = df.rolling(50).mean()
mean100 = df.rolling(100).mean()
mean200 = df.rolling(200).mean()
x=1 # controlamos columna importe y % a modo de contador
p=0
while p<4:
cash=capi
if p==0:
data = pd.DataFrame(np.where((mean25 < 0.98*df),1,0), index=df.index)
elif p==1:
data = pd.DataFrame(np.where((mean50 < 0.97*df),1,0), index=df.index)
elif p==2:
data = pd.DataFrame(np.where((mean100 < 0.96* df),1,0), index=df.index)
else:
data = pd.DataFrame(np.where((mean200 < 0.95*df),1,0), index=df.index)
""" Usamos un solo objeto para las 4 estrategias, se controla mediante el valor de p"""
signal = data.diff()
signal['Titulos']=0
signal['Coste']=0
signal['Venta']=0
signal['Comision']=0
for z in range(len(df)):
""" Dada la condición de compra calculamos qué cantidad podemos comprar"""
if signal.iloc[z,0]>0: # señal de compra
signal.iloc[z,1]= cash//df[z] # numero de acciones
tit+=signal.iloc[z,1] # acumulamos títulospq no sabemos cuando se venderán y vamos a borrar registros a cero
signal.iloc[z,2]=round(signal.iloc[z,1]*df[z],2) # coste de la compra
signal.iloc[z,4 ]= round(cf+signal.iloc[z,2]*cv,2)
ct=ct+cf+signal.iloc[z,2]*cv
cash+=-signal.iloc[z,2]-signal.iloc[z,4] # mantenemos acumulado cash neto
if signal.iloc[z,0]<0 : # señal de venta
signal.iloc[z,3]= round(tit *df[z],2) # importe venta
signal.iloc[z,4]= round(cf+signal.iloc[z,3]*cv,2)
ct=ct+cf+signal.iloc[z,2]*cv
signal.iloc[z,1]=-tit # cuenta títulos
tit=0
cash+=signal.iloc[z,3]-signal.iloc[z,4]
""" Borramos registros sin señal de compra o venta de ahí haber guardado titulos y cash fuera del DataFrame"""
signal=signal.fillna(0)
signal=signal.loc[(signal!=0).any(axis=1)]
""" Pantallazo de la estrategia, se podrá anular."""
print('Ticker:',tickers[i],'Estrategia:',p)
print(signal)
ct=0
resultado=round(float(tit*df[-1:] + cash),2)
porcentaje=round((resultado/capi-1)*100,2)
print('Resultado: ',resultado, ' ', porcentaje,' %' )
"""Guardamos en resumen para lista objetivo , manipulamos x e y para lograr encolumnarlos correctamente"""
resumen.iloc[i,(x)]=resultado
resumen.iloc[i,(x+1)]=porcentaje
tit=0
x+=2
p+=1
print(resumen) # Pantallazo del avance título a título.