Estrategia: RSI + Backtest

Gerard Sánchez - 13 de Agosto de 2021 a las 16:38 - Estrategias




En esta ocasión, vamos a tratar de programar una estrategia de trading algorítmico para un único activo, basándonos en el indicador RSI. A diferencia de lo que hemos hecho hasta ahora, el RSI es un indicador estocástico tipo oscilador que puede proporcionarnos múltiples entradas seguidas lo que complica la programación de una estrategia como habíamos hecho hasta ahora, en la que para cada señal de compra consiguientemente encontrábamos una de venta.

Primeramente debemos construir el indicador RSI como ya hemos hecho anteriormente utilizando las librerías correspondientes. Utilizaremos datos de AMZN para el ejemplo.

import pandas_datareader.data as wb
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import seaborn as sns

ticker = 'AMZN'
df = wb.DataReader(ticker, 'yahoo','2020-1-1')['Adj Close']

returns = df.pct_change()[1:]
long_window = 14
up = returns.clip(lower=0)
down = -1*returns.clip(upper=0)

ema_up = up.ewm(span=long_window).mean()
ema_down = down.ewm(span=long_window).mean()

rs = ema_up/ema_down
rsi = 100-(100/(1+rs))

A continuación os adjunto el enlace al artículo sobre indicadores para que entendáis los fundamentos de este oscilador, así como también su respectivo vídeo:


Una vez tenemos el indicador RSI, vamos a crear un DataFrame de 5 columnas llamado signal que contenga el propio RSI, más las dos condiciones a partir de las cuales generaremos las señales de compra/venta. Las columnas signalbuy y signalsell contendrán el valor numérico 1 en caso de que se cumplan las condiciones especificadas con np.where() RSI < 30 y RSI > 70 respectivamente.

Con diff() obtenemos el momento exacto del cruce para el día concreto y los 1 nos los transformará en -1 para ambas columnas. Esas son nuestras señales de compra/venta definitivas, concretamente cuando se cruza el valor 30 del RSI al alza y se cruza el 70 a la baja.

signal = pd.DataFrame(index = df.index)
signal['RSI'] = rsi

signal['signalbuy'] = np.where(signal['RSI'] < 30, 1,0)
signal['positionbuy'] = signal['signalbuy'].diff()

signal['signalsell'] = np.where(signal['RSI'] > 70, 1,0)
signal['positionsell'] = signal['signalsell'].diff()

Con iloc vamos a especificarle al DataFrame que queremos que empiece a partir del número de periodos que le concretemos en la variable long_window

df = df.iloc[long_window:]
signal = signal.iloc[long_window:]

Hemos programado la estrategia y ahora nos toca "la parte difícil" que es realizar un backtest o comprobación de resultados mínimamente realista. 

La idea que tengo en mente es poder realizar compras múltiples con un tamaño de lote determinado que únicamente se realicen cuando tenga suficiente efectivo (sin deuda) y la posibilidad de vender porcentajes de la cartera (infraponderar), también crear un sistema de comisiones (fija + variable) para ofrecer algo más de realismo y vigilar que nuestro efectivo no pueda quedar negativo.

Partimos de un capital determinado que escogemos discrecionalmente. Fijamos un lote para compra y unas comisiones con parte fija y variable, tanto para compra como para venta.

Compraremos siempre que tengamos efectivo y podamos comprar un lote mínimo que también fijaremos previamente. Para las ventas usaremos un porcentaje que fijaremos de 0 a 100, en tanto por uno, para la cartera. Esa parte de la cartera será la que vendamos con cada señal de venta, siempre que tengamos Stock.

Para ello crearemos una tabla o DataFrame que nos permita arrastrar la situación de la cartera día a día a modo contable. Cada columna queda numerada y esa numeración será la que usaremos para programar nuestras condiciones, de forma más rápida que escribiendo el nombre de las columnas.

data = pd.DataFrame()
data['Precio'] = df                               #1 "Precio del activo"
data['signal C'] = signal['positionbuy']          #2 "Señal de compra"
data['signal V'] = signal['positionsell']         #3 "Señal de venta"
data['Compra'] = 0                                #4 "Número de acciones compradas"
data['Venta'] = 0                                 #5 "Número de acciones vendidas"
data['I.Compra'] = 0                              #6 "Importe de la compra"
data['I.Venta'] = 0                               #7 "Importe de la venta"
data['Stock'] = 0                                 #8 "Número de acciones en cartera acumulado"
data['Portfolio'] = 0                             #9 "Valor del portafolio"
data['Cash'] = 0                                  #10 "Efectivo de la cartera"
data['PyG'] = 0                                   #11 "Pérdidas y ganancias"
data['Comisiones'] = 0                            #12 "Valor de las comisiones"
data = data.reset_index()                         #0 (columna 'Date')

Con data.columns y len(data.columns) podemos observar el DataFrame generado.

Verticalmente podemos referirnos a cada columna con un número, del 0 al 12 en este caso, siendo 0 Date, 1 Precio...

Vamos a crear nuestras varables de partida de efectivo (capital), órden de compra (orden), lote mínimo (lotemin), comisión fija (cf) y variable (cv), y una variable parte en tanto por uno, que será la ponderación realizada en caso de haber una venta.

A continuación se muestra un ejemplo numérico para cada una:

capital = 100000    # 100.000€ de Capital Inicial
orden = int(2000)   # 2000 acciones
lotemin = int(10)   # 10 acciones/unidades mínimas
cf = int(5)         # 5€ de Comisión fija 
cv = 0.001          # Comisión variable en tanto por uno
parte = 0.5         # Porcentaje en tanto por uno

Con la función loc en registro 0 especificamos la primera fila para la columna del efectivo "Cash" y la del total "Total" con la variable capital como inicio.

data.loc[0,'Cash'] = capital 
data.loc[0,'Total'] = capital

Ahora lo que necesitamos es un sistema de gestión de capital al uso que nos funcione para nuestro DataFrame a medida que avancen los días. Para ello, utilizaremos un bucle for para recorrer nuestras columnas e ir configurando posiciones.

for x in range(len(data)):
    if x == 0:
        x=1
    data.loc[x, 'Cash'] = data.iloc[(x-1), 10]
    data.loc[x, 'Stock'] = data.iloc[(x-1),8]
    data.loc[x, 'Portfolio'] = data.iloc[x, 8] * data.iloc[x, 1] 
    data.loc[x,'PyG']= data.iloc[x,10] + data.iloc[x,9]-capital

Debemos poner el contador x=1 dentro del if para poder avanzar con nuestro bucle y evitar registros negativos. Portfolio es la multiplicación de Stock y Precio del día. PyG será el saldo virtual del Cash + Portfolio capital 

    if (data.iloc[x, 2] == -1 and data.iloc[(x-1),10]>0):
        lote = orden 
        comfix = cf
        if (data.iloc[(x-1),10]) < (lote*data.iloc[x,1]+(cf+cv*data.iloc[x,1]*lote)):  
            lote = (data.iloc[(x-1),10]-(cf+cv*data.iloc[x,1]*orden))//data.iloc[x,1]
            if lote < lotemin:
                lote=0
                cf=0

Añadimos otra condición en la que se comprueba señal de compra [x,2] == -1 y existencia de efectivo ( >0 ) y restituimos la variable de tamaño de lote (orden) por si hubiera cambiado ya que contemplamos compras menores si no hay cash suficiente.

Cambiamos el tamaño del lote y comprobamos una compra mínima, que aparte nos garantiza no quedarnos con cash negativo en caso de compra por total cash sin saldo para la comisión.

Para la comisión y tener recursividad en el cálculo exacto, ya que depende del importe y éste de la comisión, usaremos estrategia de lote mínimo que podemos fijar discrecionalmente (evitando así órdenes de compra de por ejemplo 1 título). 

Si no hay suficiente Cash para una órden (<) calculamos cuánto podemos comprar (en valores enteros "//") y fijamos el tamaño del lote, que será siempre superior al lote mínimo. Cuando ese nuevo lote es inferior al lote mínimo evitamos compra poniéndolo a 0 y en este caso también la parte fija de la comisión cf = 0Por eso en el bucle nos aseguramos de restituir los valores lote y cf a los originales.

        if  data.iloc[(x-1),10] > (lote * data.iloc[x, 1]):
            data.loc[x, 'I.Compra'] =  lote * data.iloc[x, 1]
            data.loc[x, 'Compra'] = lote 
            data.loc[x, 'Stock'] = lote + data.iloc[(x-1),8]
            data.loc[x, 'Portfolio'] = data.iloc[x, 8] * data.iloc[x, 1]
            data.loc[x, 'Comisiones']= cf + cv*data.iloc[x,6]
            data.loc[x, 'Cash'] = data.iloc[(x-1),10] - data.iloc[x,6]-data.iloc[x,12]

Si hay suficiente cash ( > ) ejecutamos lote (que será órden o menor) y calculamos I.Compra, Comisiones , descontamos de Cash el Precio + Comisiones, aumentamos Stock, etc...

    elif (data.iloc[x, 3] == -1 and data.iloc[(x-1),8]>0) :
        paquete = int(data.iloc[(x-1),8] * parte)
        if paquete < lotemin:
            paquete=0
            cf=0
        data.loc[x, 'I.Venta'] =  paquete * data.iloc[x, 1]
        data.loc[x, 'Venta'] = paquete 
        data.loc[x, 'Stock'] = data.iloc[(x-1),8] - paquete 
        data.loc[x, 'Portfolio'] = data.iloc[x, 8] * data.iloc[x, 1]
        data.loc[x, 'Comisiones']= cf + cv*data.iloc[x,7]
        data.loc[x, 'Cash'] = data.iloc[(x-1),10]+ data.iloc[x,7]-data.iloc[x,12]

Si hay señal de venta [x,3] == -1, comprobamos que tenemos algo a vender Stock, en caso afirmativo procedemos a calcular I.Venta, Comisiones, aumentar Cash restando Comisiones, nuevo stock , etc., teniendo en cuenta qué parte. Fijamos, igual que para compras, un paquete mínimo.

Comprobaremos que el tamaño del paquete resultante sea mayor que el lote mínimo, si no no vendemos y anulamos comision, cf=0.

    else:       
        data.loc[x, 'Portfolio'] = data.iloc[x, 8] * data.iloc[x, 1] 
        data.loc[x,'PyG']= data.iloc[x,10]+data.iloc[x,9]-capital

Acabamos con un else para el resto de casos.

data.set_index('Date', inplace=True) 

data['Total'] = (data['Portfolio'] + data['Cash'])
data['Returns'] = data['Total'].pct_change()[1:]
data['Returns'] = data['Returns'][data['Returns'] != 0]

print('\n Valor total neto cash + cartera al final del periodo', round(data['Total'][-1],2))

Ponemos la columna de las fechas como índice, calculamos los importes totales de la cartera y los retornos de la misma con pct_change() y eliminamos los 0 (!= 0) cuando está parada la estrategia después de una venta del 100% de la cartera.

fig = plt.figure(figsize=(16,8))
fig.suptitle(ticker)

ax1 = fig.add_subplot(221, ylabel="Precio")
ax2 = fig.add_subplot(223, ylabel="RSI")
ax3 = fig.add_subplot(222, ylabel="Valor de la cartera")
ax4 = fig.add_subplot(224, ylabel="Frecuencia")

ax1.set_title("Estrategia RSI: " + str(ticker))
ax1.get_xaxis().set_visible(False)

df.plot(ax=ax1, color='b', lw=1.1)
ax1.plot(df[signal['positionbuy'] == -1], '^', markersize=8, color='g')
ax1.plot(df[signal['positionsell'] == -1], 'v', markersize=8, color='r')

signal.RSI.plot(ax=ax2, color='b')
ax2.set_ylim(0,100)
ax2.axhline(70, color='r', linestyle='--')
ax2.axhline(30, color='r', linestyle='--')

data.Total.plot(ax=ax3, color='b', lw=1.1)
ax3.set_title("Capital: " +  str(capital) + "\nLote: " + str(orden) + "\nVentas del: " + str(parte*100) +"%")
ax3.plot(data['Total'][signal['positionbuy'] == -1], '^', markersize=8, color='g')
ax3.plot(data['Total'][signal['positionsell'] == -1], 'v', markersize=8, color='r')
sns.histplot(data['Returns'], kde=True, ax=ax4)
plt.show()

Repetimos el código de ejemplo para graficar la estrategia + backtest que ya hicimos con otras anteriores utilizando cuatro subplots, mostrando el histograma de retornos, el gráfico del precio con las señales de compra/venta, el valor de la cartera y el indicador RSI.

Adicionalmente, podríamos escoger un grupo de activos o cesta de valores, ponderados a nuestra elección, sustituyendo estas líneas de código:

ticker = ['AMZN','FB']
weights = np.array([0.2,0.8])
df = (df*weights).sum(axis=1)

En este ejemplo estaremos comprando un 20% y un 80% de acciones de AMZN y FB respectivamente. Tendríamos que vigilar que la suma de los pesos sea 1 y para no fraccionar las acciones vigilar con los múltiplos de los lotes.

Otra posible modificación sería la de no necesitar un lote mínimo si no un efectivo mínimo, evitando así problemáticas con acciones con un valor nominal muy alto.

Crearíamos una variable cashmin en relación a esa cantidad de efectivo mínimo deseado y borraríamos lotemin. 

A continuación modificaríamos las siguientes líneas para el nuevo cálculo del lote en relación a las compras.

cashmin = int(100)
if (data.iloc[(x-1),10]) < (lote*data.iloc[x,1]+(cf+cv*data.iloc[x,1]*lote)+cashmin):  
    lote = (data.iloc[(x-1),10]-(cf+cv*data.iloc[x,1]*orden)-cashmin)//data.iloc[x,1]

Aquí os dejo el código entero con un ejemplo numérico:

import pandas_datareader.data as wb
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import seaborn as sns

ticker = 'AMZN'
#ticker = ['AMZN','FB']

df = wb.DataReader(ticker, 'yahoo','2020-1-1')['Adj Close']

#weights = np.array([0.2,0.8])
#df = (df*weights).sum(axis=1)

returns = df.pct_change()[1:]
long_window = 14
up = returns.clip(lower=0)
down = -1*returns.clip(upper=0)

ema_up = up.ewm(span=long_window).mean()
ema_down = down.ewm(span=long_window).mean()

rs = ema_up/ema_down
rsi = 100-(100/(1+rs))

signal = pd.DataFrame(index = df.index)
signal['RSI'] = rsi

signal['signalbuy'] = np.where(signal['RSI'] < 30, 1,0)
signal['positionbuy'] = signal['signalbuy'].diff()

signal['signalsell'] = np.where(signal['RSI'] > 70, 1,0)
signal['positionsell'] = signal['signalsell'].diff()

df = df.iloc[long_window:]
signal = signal.iloc[long_window:]

####################################################################################################

data = pd.DataFrame()
data['Precio'] = df
data['signal C'] = signal['positionbuy']
data['signal V'] = signal['positionsell']
data['Compra'] = 0
data['Venta'] = 0
data['I.Compra'] = 0
data['I.Venta'] = 0
data['Stock'] = 0
data['Portfolio'] = 0
data['Cash'] = 0
data['PyG'] = 0
data['Comisiones'] = 0
data = data.reset_index()

capital = 100000
orden = int(2000)
lotemin = int(10)
#cashmin = int(100)
cf = int(5)
cv = 0.001
parte = 0.5

data.loc[0,'Cash']= capital 
data.loc[0,'Total']= capital

for x in range(len(data)):
    if x == 0:
        x=1
    data.loc[x, 'Cash'] = data.iloc[(x-1), 10]
    data.loc[x, 'Stock'] = data.iloc[(x-1),8]
    data.loc[x, 'Portfolio'] = data.iloc[x, 8] * data.iloc[x, 1] 
    data.loc[x,'PyG'] = data.iloc[x,10] + data.iloc[x,9]-capital  
    if (data.iloc[x, 2] == -1 and data.iloc[(x-1),10]>0):
        lote = orden 
        comfix = cf
        #if (data.iloc[(x-1),10]) < (lote*data.iloc[x,1]+(cf+cv*data.iloc[x,1]*lote)+cashmin):  
            #lote = (data.iloc[(x-1),10]-(cf+cv*data.iloc[x,1]*orden)-cashmin)//data.iloc[x,1]
        if (data.iloc[(x-1),10]) < (lote*data.iloc[x,1]+(cf+cv*data.iloc[x,1]*lote)):  
            lote = (data.iloc[(x-1),10]-(cf+cv*data.iloc[x,1]*orden))//data.iloc[x,1]
            if lote < lotemin:
                lote=0
                cf=0
        if data.iloc[(x-1),10] > (lote * data.iloc[x, 1]):
            data.loc[x, 'I.Compra'] =  lote * data.iloc[x, 1]
            data.loc[x, 'Compra'] = lote 
            data.loc[x, 'Stock'] = lote + data.iloc[(x-1),8]
            data.loc[x, 'Portfolio'] = data.iloc[x, 8] * data.iloc[x, 1]
            data.loc[x, 'Comisiones']= cf + cv*data.iloc[x,6]
            data.loc[x, 'Cash'] = data.iloc[(x-1),10] - data.iloc[x,6]-data.iloc[x,12]
    elif (data.iloc[x, 3] == -1 and data.iloc[(x-1),8]>0) :
        paquete = int(data.iloc[(x-1),8] * parte)
        if paquete < lotemin:
            paquete=0
            cf=0
        data.loc[x, 'I.Venta'] =  paquete * data.iloc[x, 1]
        data.loc[x, 'Venta'] = paquete 
        data.loc[x, 'Stock'] = data.iloc[(x-1),8] - paquete 
        data.loc[x, 'Portfolio'] = data.iloc[x, 8] * data.iloc[x, 1]
        data.loc[x, 'Comisiones']= cf + cv*data.iloc[x,7]
        data.loc[x, 'Cash'] = data.iloc[(x-1),10]+ data.iloc[x,7]-data.iloc[x,12]
    else:       
        data.loc[x, 'Portfolio'] = data.iloc[x, 8] * data.iloc[x, 1] 
        data.loc[x,'PyG']= data.iloc[x,10]+data.iloc[x,9]-capital     

data.set_index('Date', inplace=True) 

data['Total'] = (data['Portfolio'] + data['Cash'])
data['Returns'] = data['Total'].pct_change()[1:]
data['Returns'] = data['Returns'][data['Returns'] != 0]

print('\n Valor total neto cash + cartera al final del periodo ', round(data['Total'][-1],2))

####################################################################################################

fig = plt.figure(figsize=(16,8))
fig.suptitle(ticker)

ax1 = fig.add_subplot(221, ylabel="Precio")
ax2 = fig.add_subplot(223, ylabel="RSI")
ax3 = fig.add_subplot(222, ylabel="Valor de la cartera")
ax4 = fig.add_subplot(224, ylabel="Frecuencia")

ax1.set_title("Estrategia RSI: " + str(ticker))
ax1.get_xaxis().set_visible(False)

df.plot(ax=ax1, color='b', lw=1.1)
ax1.plot(df[signal['positionbuy'] == -1], '^', markersize=8, color='g')
ax1.plot(df[signal['positionsell'] == -1], 'v', markersize=8, color='r')

signal.RSI.plot(ax=ax2, color='b')
ax2.set_ylim(0,100)
ax2.axhline(70, color='r', linestyle='--')
ax2.axhline(30, color='r', linestyle='--')

data.Total.plot(ax=ax3, color='b', lw=1.1)
ax3.set_title("Capital: " +  str(capital) + "\nLote: " + str(orden) + "\nVentas del: " + str(parte*100) +"%")
ax3.plot(data['Total'][signal['positionbuy'] == -1], '^', markersize=8, color='g')
ax3.plot(data['Total'][signal['positionsell'] == -1], 'v', markersize=8, color='r')
sns.histplot(data['Returns'], kde=True, ax=ax4)
plt.show()

 


1673 visitas
10    Login to like
Categorías:
 Estrategias   Estadísticas   Random   Gestión pasiva   Análisis técnico   Modelos   CEO   Mapas mentales   Liberalismo   Python   Growth   Niusleta   Ahorro   Recursos humanos   Inmobiliario   Fiscalidad   Value investing   Dividendos   Contabilidad   Marketing   Riesgo   IF   Cursos   Opciones   Bolsa