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.
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
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.
Los sistemas basados en eventos ofrecen muchas ventajas sobre un enfoque vectorizado:
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.
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:
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.