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 = 0. Por 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()