Pruebas de retroceso basadas en eventos con Python - Parte I

El autor:La bondad, Creado: 2019-03-22 11:53:50, Actualizado:

Hemos pasado los últimos dos meses en QuantStart backtesting varias estrategias comerciales utilizando Python y pandas. La naturaleza vectorizada de los pandas asegura que ciertas operaciones en grandes conjuntos de datos son extremadamente rápidas. Sin embargo, las formas de backtesting vectorizado que hemos estudiado hasta la fecha sufren algunos inconvenientes en la forma en que se simula la ejecución de operaciones. En esta serie de artículos vamos a discutir un enfoque más realista para la simulación de estrategias históricas mediante la construcción de un entorno de backtesting basado en eventos utilizando Python.

Software basado en eventos

Antes de profundizar en el desarrollo de tal backtester, necesitamos entender el concepto de sistemas impulsados por eventos. Los videojuegos proporcionan un caso de uso natural para el software impulsado por eventos y proporcionan un ejemplo sencillo para explorar. Un videojuego tiene múltiples componentes que interactúan entre sí en un entorno en tiempo real a altas velocidades de fotogramas. Esto se maneja ejecutando todo el conjunto de cálculos dentro de un bucle infinito conocido como el bucle de eventos o bucle de juegos.

En cada clic del bucle de juego se llama a una función para recibir el evento más reciente, que habrá sido generado por alguna acción anterior correspondiente dentro del juego. Dependiendo de la naturaleza del evento, que podría incluir una tecla o un clic del ratón, se toma alguna acción posterior, que terminará el bucle o generará algunos eventos adicionales.

while True:  # Run the loop forever
    new_event = get_new_event()   # Get the latest event

    # Based on the event type, perform an action
    if new_event.type == "LEFT_MOUSE_CLICK":
        open_menu()
    elif new_event.type == "ESCAPE_KEY_PRESS":
        quit_game()
    elif new_event.type == "UP_KEY_PRESS":
        move_player_north()
    # ... and many more events

    redraw_screen()   # Update the screen to provide animation
    tick(50)   # Wait 50 milliseconds

El código está continuamente comprobando nuevos eventos y luego ejecutando acciones basadas en estos eventos. En particular, permite la ilusión de manejo de respuesta en tiempo real porque el código está continuamente en bucle y se verifican los eventos. Como quedará claro, esto es precisamente lo que necesitamos para llevar a cabo una simulación de comercio de alta frecuencia.

¿Por qué una prueba de retroceso impulsada por eventos?

Los sistemas basados en eventos ofrecen muchas ventajas sobre un enfoque vectorizado:

  • Reutilización de código - Un backtester basado en eventos, por diseño, puede utilizarse tanto para backtesting histórico como para operaciones en vivo con un mínimo de interrupción de componentes.
  • Buscar prejuicios - Con un backtester basado en eventos no hay prejuicios de lookahead ya que la recepción de datos de mercado se trata como un "evento" sobre el que se debe actuar.
  • Realismo - Los backtesters basados en eventos permiten una personalización significativa de la forma en que se ejecutan las órdenes y se incurren en los costos de transacción.

Aunque los sistemas impulsados por eventos vienen con muchos beneficios, sufren dos desventajas principales en comparación con los sistemas vectorizados más simples. En primer lugar, son significativamente más complejos de implementar y probar.

En segundo lugar, son más lentos de ejecutar en comparación con un sistema vectorizado. Las operaciones vectorizadas óptimas no pueden ser utilizadas al realizar cálculos matemáticos.

Resumen general de las pruebas de retroceso basadas en eventos

Para aplicar un enfoque basado en eventos a un sistema de backtesting es necesario definir nuestros componentes (u objetos) que manejarán tareas específicas:

  • Evento - El Evento es la unidad de clase fundamental del sistema impulsado por eventos. Contiene un tipo (como MARKET, SIGNAL, ORDER o FILL) que determina cómo se manejará dentro del bucle de eventos.
  • Coda de eventos - La cola de eventos es un objeto de coda de Python en memoria que almacena todos los objetos de la subclase Event que son generados por el resto del software.
  • DataHandler - DataHandler es una clase base abstracta (ABC) que presenta una interfaz para manejar tanto datos históricos como de mercado en vivo. Esto proporciona una flexibilidad significativa ya que los módulos de Estrategia y Cartera pueden reutilizarse entre ambos enfoques.
  • Estrategia - La Estrategia es también un ABC que presenta una interfaz para tomar datos de mercado y generar Eventos de señal correspondientes, que finalmente son utilizados por el objeto de cartera.
  • Portfolio - Este es un ABC que maneja la gestión de órdenes asociadas con posiciones actuales y posteriores para una estrategia. También lleva a cabo la gestión de riesgos en toda la cartera, incluida la exposición del sector y el tamaño de la posición. En una implementación más sofisticada, esto podría delegarse en una clase de RiskManagement.
  • ExecutionHandler - El ExecutionHandler simula una conexión a una correduría. El trabajo del manipulador es tomar OrderEvents de la cola y ejecutarlos, ya sea a través de un enfoque simulado o una conexión real a una correduría hepática. Una vez que se ejecutan las órdenes, el manipulador crea FillEvents, que describen lo que realmente se transacciona, incluidas las tarifas, la comisión y el deslizamiento (si se modelan).
  • El bucle - Todos estos componentes están envueltos en un bucle de eventos que maneja correctamente todos los tipos de eventos, encaminándolos al componente apropiado.

Este es un modelo bastante básico de un motor de negociación. Hay un margen significativo de expansión, particularmente en lo que respecta a cómo se utiliza la cartera. Además, diferentes modelos de costos de transacción también podrían ser abstractos en su propia jerarquía de clases. En esta etapa, introduce complejidad innecesaria dentro de esta serie de artículos, por lo que actualmente no lo discutiremos más. En tutoriales posteriores, probablemente expandiremos el sistema para incluir realismo adicional.

Aquí hay un fragmento de código de Python que demuestra cómo funciona el backtester en la práctica. Hay dos bucles que ocurren en el código. El bucle externo se utiliza para dar al backtester un latido del corazón. Para el comercio en vivo esta es la frecuencia con la que se encuestan los nuevos datos del mercado. Para las estrategias de backtesting esto no es estrictamente necesario ya que el backtester utiliza los datos del mercado proporcionados en forma de goteo (ver la línea bars.update_bars)).

El bucle interno realmente maneja los eventos desde el objeto de la cola de eventos. Los eventos específicos se delegan en el componente respectivo y posteriormente se agregan nuevos eventos a la cola. Cuando la cola de eventos está vacía, el bucle de latidos continúa:

# Declare the components with respective parameters
bars = DataHandler(..)
strategy = Strategy(..)
port = Portfolio(..)
broker = ExecutionHandler(..)

while True:
    # Update the bars (specific backtest code, as opposed to live trading)
    if bars.continue_backtest == True:
        bars.update_bars()
    else:
        break
    
    # Handle the events
    while True:
        try:
            event = events.get(False)
        except Queue.Empty:
            break
        else:
            if event is not None:
                if event.type == 'MARKET':
                    strategy.calculate_signals(event)
                    port.update_timeindex(event)

                elif event.type == 'SIGNAL':
                    port.update_signal(event)

                elif event.type == 'ORDER':
                    broker.execute_order(event)

                elif event.type == 'FILL':
                    port.update_fill(event)

    # 10-Minute heartbeat
    time.sleep(10*60)

Este es el esquema básico de cómo se diseña un backtester impulsado por eventos.


Más.