Estrategia de Trading Algorítmico con VWAP en Python

0
66
indicador rsi compuesto python

En las siguientes secciones, nos adentraremos en una comparación detallada de los indicadores VWAP (Precio Promedio Ponderado por Volumen) y la Media Móvil (MA) para el trading algorítmico. Explicaremos cómo se pueden utilizar estos indicadores para construir y hacer pruebas retrospectivas (backtesting) de un bot de trading utilizando datos de Bitcoin en Python.

El VWAP y las Medias Móviles son herramientas populares entre los traders, y cada una ofrece perspectivas únicas. Mientras que la media móvil proporciona un promedio simple de los precios, el indicador VWAP ofrece una perspectiva más matizada al incorporar el volumen de negociación. Esta distinción puede ser crucial para determinar si un valor está sobrevaluado o subvaluado. Discutiremos por qué el VWAP es a menudo preferido por los traders y cómo puede servir como un nivel dinámico de soporte y resistencia.

Para comenzar vamos a explicar el cálculo del VWAP, enfatizando su importancia sobre la media móvil debido a su naturaleza ponderada por volumen. También incluimos un código en Python para hacer pruebas retrospectivas de estas estrategias.

¿Qué es el indicador?

El precio promedio ponderado por volumen (VWAP) es un indicador de análisis técnico utilizado en gráficos intradía que se reinicia al comienzo de cada nueva sesión de trading. Es el precio promedio al que un valor ha sido negociado a lo largo del día, basado tanto en el volumen como en el precio.

El VWAP es importante porque ofrece a los traders una visión tanto de la tendencia del precio como del valor de un valor.

Se calcula mediante la siguiente fórmula:

VWAP = (Precio Típico x Volumen)/Volumen Acumulado

donde:

Precio Típico = (Precio Máximo + Precio Mínimo + Precio de Cierre)/3

Volumen acumulado = Total de transacciones desde que se abrió la sesión de trading

Paso 1: Importación y Preprocesamiento de Datos

En el primer paso, importamos y preprocesamos los datos de trading de Bitcoin para prepararlos para el análisis. El proceso incluye las siguientes acciones:

import pandas as pd
import yfinance as yf

# Descargar los datos de precios de Bitcoin desde yfinance
df = yf.download("BTC-USD", interval="15m", start="2019-08-05", end="2024-04-29")

# Limpiar los datos eliminando filas donde los precios alto y bajo son iguales
df = df[df['High'] != df['Low']]

# El índice del DataFrame ya es la columna de fechas, así que no es necesario convertir ni establecerlo nuevamente

Este código descarga los datos de precios de Bitcoin con intervalos de 15 minutos desde el 5 de agosto de 2019 hasta el 29 de abril de 2024 y elimina las filas donde los precios alto y bajo son iguales.

Paso 2: Cálculo de los Indicadores

En el segundo paso, calculamos los indicadores VWAP (Precio Promedio Ponderado por Volumen) y EMA (Media Móvil Exponencial) utilizando la biblioteca pandas_ta, diseñada precisamente para calcular los indicadores técnicos más conocidos. Estos indicadores son esenciales para construir nuestras estrategias de trading.

import pandas_ta as ta
# Calcular el VWAP (Precio Promedio Ponderado por Volumen) y agregarlo al DataFrame
df["VWAP"] = ta.vwap(df.High, df.Low, df.Close, df.Volume)

# Calcular la EMA (Media Móvil Exponencial) con una longitud de 100 períodos y agregarla al DataFrame
df["EMA"] = ta.ema(df.Close, length=100)
  • Importación de la Biblioteca: Importamos la biblioteca pandas_ta, que proporciona varios indicadores de análisis técnico.
  • Cálculo del VWAP: El indicador VWAP se calcula utilizando los precios altos, bajos, de cierre y los datos de volumen. El VWAP se agrega como una nueva columna llamada “VWAP” en el DataFrame.
  • Cálculo de la EMA: El indicador EMA se calcula para los precios de cierre con una longitud especificada de 100 períodos. La EMA se agrega como una nueva columna llamada “EMA” en el DataFrame.

Paso 3: Generación de Señales la EMA

En el tercer paso, generamos señales de trading basadas en la Media Móvil Exponencial (EMA). Estas señales nos ayudarán a determinar posibles oportunidades de compra y venta. Identificamos señales de trading potenciales basadas en la relación entre los valores alto, bajo y la EMA durante un período de retroceso especificado.

Esta señal verifica si todas las velas dentro del período de retroceso están completamente por encima o completamente por debajo de la EMA. Si todas están por encima, indica una tendencia alcista; si todas están por debajo, indica una tendencia bajista dentro de la ventana de retroceso.

# Inicializar la lista EMASignal con ceros
emasignal = [0] * len(df)

# Definir el período de retroceso
backcandles = 6

# Recorrer el DataFrame desde el inicio del período de retroceso hasta el final
for row in range(backcandles, len(df)):
    upt = 1
    dnt = 1
    # Verificar las condiciones para los períodos de retroceso pasados
    for i in range(row - backcandles, row + 1):
        if df.High[i] >= df.EMA[i]:
            dnt = 0
        if df.Low[i] <= df.EMA[i]:
            upt = 0
    # Establecer la EMASignal según las condiciones
    if upt == 1 and dnt == 1:
        emasignal[row] = 3
    elif upt == 1:
        emasignal[row] = 2
    elif dnt == 1:
        emasignal[row] = 1

# Agregar la EMASignal al DataFrame
df['EMASignal'] = emasignal

Inicialización de la Lista de Señales: Creamos una lista llamada emasignal llena de ceros, correspondiente a la longitud del DataFrame.

Configuración del Período de Retroceso: Definimos una variable backcandles para especificar el número de períodos que retrocedemos para verificar las condiciones de nuestras señales.

Recorrido a Través de los Datos: Recorremos el DataFrame comenzando desde el índice backcandles hasta el final del DataFrame.

Verificación de Condiciones: Para cada fila, inicializamos dos variables, upt y dnt, en 1, que representan las tendencias alcista y bajista. Luego, recorremos los períodos anteriores de backcandles para verificar las siguientes condiciones:

  • Si el precio alto de la vela actual es mayor o igual a la EMA, establecer dnt en 0.
  • Si el precio bajo de la vela actual es menor o igual a la EMA, establecer upt en 0.

Configuración de las Señales:

  • Si tanto upt como dnt permanecen en 1, establecer la señal en 3 (indicando que no hay una tendencia clara).
  • Si solo upt es 1, establecer la señal en 2 (indicando una posible tendencia alcista).
  • Si solo dnt es 1, establecer la señal en 1 (indicando una posible tendencia bajista).

Adición de Señales al DataFrame: Finalmente, agregamos las señales generadas a una nueva columna llamada EMASignal en el DataFrame.

El código en Python también se puede escribir utilizando “slicing”, que es una forma más característica de programar en Python:

# Inicializar la lista EMASignal con ceros
emasignal = [0] * len(df)

# Definir el período de retroceso
backcandles = 6

# Recorrer el DataFrame desde el período de retroceso hasta el final
for row in range(backcandles, len(df)):
    # Hacer un slicing de la ventana relevante de valores altos, bajos y EMA
    high_slice = df.High[row - backcandles: row + 1]
    low_slice = df.Low[row - backcandles: row + 1]
    ema_slice = df.EMA[row - backcandles: row + 1]
    
    # Verificar las condiciones para la tendencia alcista y bajista
    upt = all(high_slice >= ema_slice)
    dnt = all(low_slice <= ema_slice)
    
    # Establecer la EMASignal según las condiciones
    if upt and dnt:
        emasignal[row] = 3
    elif upt:
        emasignal[row] = 2
    elif dnt:
        emasignal[row] = 1

# Agregar la EMASignal al DataFrame
df['EMASignal'] = emasignal

Paso 4: Generación de Señales del Indicador VWAP

En este paso, generamos señales de trading basadas en el Precio Promedio Ponderado por Volumen (VWAP). Estas señales, al igual que las señales de la EMA, nos ayudan a determinar posibles oportunidades de compra y venta, verificando si las velas del período de retroceso están completamente por encima o por debajo de la curva del VWAP.

# Inicializar la lista VWAPsignal con ceros
VWAPsignal = [0] * len(df)

# Definir el período de retroceso
backcandles = 6

# Recorrer el DataFrame desde el período de retroceso hasta el final
for row in range(backcandles, len(df)):
    # Hacer un slicing de la ventana relevante de valores altos, bajos y VWAP
    high_slice = df.High[row - backcandles: row + 1]
    low_slice = df.Low[row - backcandles: row + 1]
    vwap_slice = df.VWAP[row - backcandles: row + 1]
    
    # Verificar las condiciones para la tendencia alcista y bajista
    upt = all(high_slice >= vwap_slice)
    dnt = all(low_slice <= vwap_slice)
    
    # Establecer la VWAPSignal según las condiciones
    if upt and dnt:
        VWAPsignal[row] = 3
    elif upt:
        VWAPsignal[row] = 2
    elif dnt:
        VWAPsignal[row] = 1

# Agregar la VWAPSignal al DataFrame
df['VWAPSignal'] = VWAPsignal

Inicialización de la Lista de Señales: Creamos una lista llamada VWAPsignal llena de ceros, correspondiente a la longitud del DataFrame.

Configuración del Período de Retroceso: Definimos una variable backcandles para especificar el número de períodos que retrocederemos para verificar las condiciones de nuestras señales.

Recorrido a Través de los Datos: Recorremos el DataFrame comenzando desde el índice backcandles hasta el final del DataFrame.

Verificación de Condiciones: Para cada fila, verificamos si todas las velas dentro del período de retroceso están completamente por encima o por debajo del VWAP. Si todas están por encima, indica una tendencia alcista; si todas están por debajo, indica una tendencia bajista dentro de la ventana de retroceso.

Configuración de las Señales:

  • Si todos los precios altos dentro del período de retroceso están por encima del VWAP, establecer la señal en 2 (indicando una posible tendencia alcista).
  • Si todos los precios bajos dentro del período de retroceso están por debajo del VWAP, establecer la señal en 1 (indicando una posible tendencia bajista).
  • Si se cumplen ambas condiciones, establecer la señal en 3 (indicando que no hay una señal clara).

Adición de Señales al DataFrame: Finalmente, agregamos las señales generadas a una nueva columna llamada VWAPSignal en el DataFrame.

Paso 5: Combinación de Señales con Verificación de Proximidad

Este paso tiene como objetivo refinar nuestras señales de trading mediante la incorporación de una verificación de proximidad. Específicamente, queremos asegurarnos de que las señales generadas (ya sea por EMA o VWAP) no solo indiquen una tendencia, sino que también el precio actual esté lo suficientemente cerca de los valores del indicador. Esta verificación adicional ayuda a filtrar las señales que pueden no ser fiables debido a que el precio está demasiado alejado del indicador.

En otras palabras, para una tendencia alcista esperamos que una vela se acerque a la curva del indicador (VWAP o EMA) y, dado que estamos en una tendencia alcista, esperamos que el precio rebote en cualquier momento. La configuración simétrica se aplica a una tendencia bajista, donde verificamos si una vela se acerca a alguna de las curvas desde abajo. La razón por la que esperamos que el precio rebote es que estamos tratando de identificar retrocesos seguidos de un nivel de rechazo del precio alrededor de las curvas de los indicadores.

Objetivo:

  • TotalEMASignal: Refina las señales de la EMA basándose en la proximidad del precio al valor de la EMA.
  • TotalVWAPSignal: Refina las señales del VWAP basándose en la proximidad del precio al valor del VWAP.
  • Combinación de Señales: Aplica la verificación de proximidad a las señales de EMA o VWAP y almacena las señales finales en una nueva columna llamada TotalSignal.

Código y Explicación:

# Definir una función para refinar las señales EMA basándose en la proximidad a los valores de EMA
def TotalEMASignal(l):
    myclosedistance = 100
    if (df.EMASignal[l] == 2 and min(abs(df.EMA[l] - df.High[l]), abs(df.EMA[l] - df.Low[l])) <= myclosedistance):
        return 2
    if (df.EMASignal[l] == 1 and min(abs(df.EMA[l] - df.High[l]), abs(df.EMA[l] - df.Low[l])) <= myclosedistance):
        return 1

# Definir una función para refinar las señales VWAP basándose en la proximidad a los valores de VWAP
def TotalVWAPSignal(l):
    myclosedistance = 100
    if (df.VWAPSignal[l] == 2 and min(abs(df.VWAP[l] - df.High[l]), abs(df.VWAP[l] - df.Low[l])) <= myclosedistance):
        return 2
    if (df.VWAPSignal[l] == 1 and min(abs(df.VWAP[l] - df.High[l]), abs(df.VWAP[l] - df.Low[l])) <= myclosedistance):
        return 1

# Inicializar la lista TotSignal con ceros
TotSignal = [0] * len(df)

# Recorrer el DataFrame y aplicar la verificación de proximidad para las señales VWAP
for row in range(0, len(df)):  # ten cuidado con el período de retroceso utilizado en las partes anteriores
    TotSignal[row] = TotalVWAPSignal(row)

# Agregar las señales refinadas al DataFrame
df['TotalSignal'] = TotSignal

Definición de Funciones de Verificación de Proximidad:

  • TotalEMASignal(l): Esta función verifica si la señal EMA en el índice l es válida según un criterio de proximidad. Si la señal indica una tendencia alcista (2) o bajista (1) y el precio está dentro de una cierta distancia (myclosedistance) de la EMA, la señal se considera válida.
  • TotalVWAPSignal(l): Esta función realiza una verificación similar para las señales VWAP. Asegura que la señal sea válida si el precio está lo suficientemente cerca del valor del VWAP.

Criterio de Proximidad:

Se establece una distancia de 100 unidades (myclosedistance) como umbral para determinar si el precio actual está lo suficientemente cerca de los valores de EMA o VWAP. Se calcula la distancia mínima entre los precios altos/bajos y la EMA/VWAP, y si es menor o igual a 100, la señal se retiene.

Este ejemplo utiliza datos de bitcoins, por lo que las 100 unidades pueden cambiarse para diferentes activos. Un enfoque mejor es definir la proximidad como un porcentaje del precio de cierre reciente, de esta manera es más flexible para diferentes activos.

Generación de Señales Refinadas:

  • Lista TotSignal: Inicializa una lista para almacenar las señales refinadas para cada fila en el DataFrame.
  • Recorrido del DataFrame: Itera a través de cada fila y aplica la función TotalVWAPSignal para generar las señales refinadas basadas en la proximidad del VWAP.
  • Adición de Señales al DataFrame: Las señales refinadas se almacenan en una nueva columna llamada TotalSignal en el DataFrame.

Paso 6: Visualización de Señales

En este paso, visualizamos las señales de trading en un gráfico de velas japonesas para comprender mejor cómo las señales se alinean con los movimientos de precios y los indicadores. La visualización nos ayudará a ver dónde se generan las señales de compra y venta en relación con las líneas de EMA y VWAP.

Visualización estrategia de VWAP en Python

import numpy as np
import plotly.graph_objects as go
from datetime import datetime

# Definir una función para determinar la posición vertical de las señales
def point_pos_break(row):
    if row['TotalSignal'] == 1:
        return row['High'] + 0.001
    elif row['TotalSignal'] == 2:
        return row['Low'] - 0.001
    return np.nan

# Aplicar la función para crear la columna 'pointposbreak' en el DataFrame
df['pointposbreak'] = df.apply(point_pos_break, axis=1)

# Seleccionar un subconjunto del DataFrame para la visualización
dfpl = df.iloc[150:400].reset_index()

# Crear el gráfico de velas
fig = go.Figure(data=[
    go.Candlestick(
        x=dfpl.index,
        open=dfpl['Open'],
        high=dfpl['High'],
        low=dfpl['Low'],
        close=dfpl['Close']
    ),
    go.Scatter(
        x=dfpl.index,
        y=dfpl.EMA,
        line=dict(color='orange', width=1),
        name="EMA"
    ),
    go.Scatter(
        x=dfpl.index,
        y=dfpl.VWAP,
        line=dict(color='blue', width=1),
        name="VWAP"
    )
])

# Agregar marcadores de señales al gráfico
fig.add_scatter(
    x=dfpl.index,
    y=dfpl['pointposbreak'],
    mode="markers",
    marker=dict(size=5, color="MediumPurple"),
    name="Señal"
)

# Mostrar el gráfico
fig.show()
  • pointposbreak(x): Esta función asigna una posición vertical a las señales para su visualización. Si la señal indica una tendencia bajista (1), el marcador se coloca ligeramente por encima del precio alto de la vela. Si la señal indica una tendencia alcista (2), el marcador se coloca ligeramente por debajo del precio bajo de la vela. Si no hay señal, devuelve NaN.
  • df[‘pointposbreak’]: Se crea una nueva columna en el DataFrame para almacenar las posiciones de las señales. Se utiliza el método apply para aplicar la función pointposbreak a cada fila del DataFrame.

Paso 7: Pruebas de Backtesting de Estrategia de Trading
En este paso, realizamos pruebas de nuestra estrategia de trading utilizando la biblioteca de backtesting. El objetivo es evaluar el rendimiento de la estrategia basado en las señales generadas.

import pandas_ta as ta
from backtesting import Strategy, Backtest

# Preparar los datos para las pruebas
dfpl = df.copy()
dfpl['ATR'] = ta.atr(dfpl.High, dfpl.Low, dfpl.Close, length=5)

# Definir una función para devolver la columna TotalSignal
def SIGNAL():
    return dfpl.TotalSignal

# Definir la clase de estrategia personalizada
class MyStrat(Strategy):
    initsize = 0.5
    mysize = initsize

    def init(self):
        self.signal1 = self.I(SIGNAL)

    def next(self):
        slatr = 0.8 * self.data.ATR[-1]
        TPSLRatio = 2.5

        if self.signal1[-1] == 2 and not self.trades:
            sl1 = self.data.Close[-1] - slatr
            tp1 = self.data.Close[-1] + slatr * TPSLRatio
            self.buy(sl=sl1, tp=tp1, size=self.mysize)

        elif self.signal1[-1] == 1 and not self.trades:
            sl1 = self.data.Close[-1] + slatr
            tp1 = self.data.Close[-1] - slatr * TPSLRatio
            self.sell(sl=sl1, tp=tp1, size=self.mysize)

# Ejecutar la prueba
bt = Backtest(dfpl, MyStrat, cash=100000, margin=1/5, commission=0.0000)
stat = bt.run()
print(stat)
  • Creamos una copia del DataFrame dfpl para trabajar con los datos de backtesting.
  • La función ta.atr se utiliza para calcular el Average True Range (ATR) y agregarlo como una nueva columna en el DataFrame.
  • La función SIGNAL devuelve la columna TotalSignal del DataFrame. Esta columna contiene las señales de trading generadas en los pasos anteriores.
  • La clase MyStrat define la estrategia de trading. Se inicializa con un tamaño inicial de 50% para las operaciones (initsize = 0.5).
  • El método init inicializa el indicador de señales usando el método I de la clase Strategy.
  • El método next define la lógica de trading. Calcula los niveles de stop-loss (SL) y take-profit (TP) basados en el ATR y coloca órdenes de compra/venta si se cumplen las condiciones.
  • La distancia de stop loss es 0.8 veces el valor del ATR y la distancia de take profit está relacionada con el stop loss usando la variable TPSLRatio, en este caso es 2.5 veces la distancia del SL.
  • La clase Backtest se utiliza para crear una instancia de backtest con los datos preparados, la estrategia personalizada, el efectivo inicial, el margen y los ajustes de comisión.
  • El método run ejecuta el backtest e imprime las estadísticas.

Resultados de la estrategia con VWAP

Los resultados se resumen en el siguiente informe:

resultados backtesting1

Los resultados muestran que la estrategia es de alto riesgo, aunque generó altos beneficios. El drawdown es del -77%, lo cual es enorme. Podemos disminuir el riesgo reduciendo el apalancamiento y el tamaño de las operaciones, lo que también afectará nuestros rendimientos.

Resultados de la estrategia con EMA

Recuerda que para realizar la prueba utilizando EMA, necesitamos modificar esta parte del código:

# Cambiar señal aquí VWA por  EMA
TotSignal = [0]*len(df)
for row in range(0, len(df)): # cuidado con los backcandles utilizados en la celda anterior
TotSignal[row] = TotalEMASignal(row)
df['TotalSignal'] = TotSignal

Y los resultados obtenidos son los siguientes:

resultados backtesting2Conclusión

Aunque ambos indicadores tienen similitudes al usar esta estrategia específica, el VWAP muestra mayores retornos y también un mayor riesgo, lo cual se refleja en el valor máximo de drawdown.

El ratio de Sortino también es mucho más alto para el VWAP. La estrategia en sí no es segura para el trading, el tamaño de la operación y la gestión no son adecuados. Sin embargo, la estrategia nos permitió una comparación directa entre ambos indicadores, y si tuviéramos que elegir, creo que el VWAP muestra más potencial que la media móvil.

Espero que tenga sentido al final, y gracias por quedarte tanto tiempo. ¡Hasta nuestra próxima historia, opera con seguridad y disfruta codificando!


 

TagsPython
Raul Canessa

Leave a reply