Il Motore di Backtest – Creare un Trading System Parte 8

Il Motore di Backtest di un trading system

In questa lezione del corso per creare un trading system event-driven con Python, mostriamo come implementare il motore di backtest. Creiamo la classe gerarchica Backtest, che incapsula la logica di gestione degli eventi e collega tutte le altre classi descritte nelle lezioni precedenti.

L’oggetto Backtest gestisce un sistema guidato da eventi, racchiuso in un ciclo while che elabora gli eventi presenti nell’EventQueue. Il ciclo esterno, noto come “heartbeat loop”, determina la risoluzione temporale del sistema. In un ambiente live, impostiamo questo valore a un numero positivo, come 600 secondi (ogni dieci minuti), così aggiorniamo dati di mercato e posizioni solo in quel lasso di tempo.

Il Motore di Backtest

Il “heartbeat” rappresenta un elemento chiave del motore di backtest. Per il backtest, possiamo impostarlo a zero, indipendentemente dalla frequenza della strategia, poiché i dati storici sono già disponibili!

Eseguiamo il backtest alla velocità desiderata, perché il sistema event-driven è indipendente dalla disponibilità temporale dei dati, purché abbiano un timestamp. Includiamo questo comportamento per mostrare come funziona un motore di trading in tempo reale. Il ciclo esterno termina quando il DataHandler segnala al Backtest di fermarsi, impostando l’attributo booleano continue_backtest su False.

Il ciclo while interno elabora i segnali e li indirizza al componente corretto, in base al tipo di evento. La coda degli eventi viene costantemente popolata e svuotata, definendo così il funzionamento di un sistema guidato dagli eventi.

Come primo passo, importiamo le librerie necessarie. Includiamo pprint (“pretty-print”) per visualizzare in modo chiaro le statistiche finali nell’output:

				
					# backtest.py

import datetime
import pprint
import queue
import time
				
			

L’inizializzazione dell’oggetto per il motore di backtest richiede la directory CSV, l’elenco completo dei simboli da analizzare, il capitale iniziale, il periodo di “heartbreat” in millisecondi, la data e ora di inizio del backtest nonché degli oggetti DataHandler, ExecutionHandler, Portfolio e Strategy. 

Usiamo una coda per gestire gli eventi. Inoltre prevediamo delle variabili per conteggiare i segnali, gli ordini e le esecuzioni:

				
					# backtest.py

class Backtest(object):
    """
    Racchiude le impostazioni e i componenti per l'esecuzione
    un backtest basato sugli eventi.
    """
    def __init__(self, csv_dir, symbol_list, initial_capital,
                 heartbeat, start_date, data_handler,
                 execution_handler, portfolio, strategy ):
        """
        Inizializza il backtest.

        Parametri:
        csv_dir - Il percorso della directory dei dati CSV.
        symbol_list - L'elenco dei simboli.
        intial_capital - Il capitale iniziale del portafoglio.
        heartbeat - il "battito cardiaco" del backtest in secondi
        data_inizio - La data e ora di inizio della strategia.
        data_handler - (Classe) Gestisce il feed di dati di mercato.
        execution_handler - (Classe) Gestisce gli ordini / esecuzioni per i trade.
        portfolio - (Classe) Tiene traccia del portafoglio attuale e delle posizioni precedenti.
        strategy - (Classe) Genera segnali basati sui dati di mercato.
        """

        self.csv_dir = csv_dir
        self.symbol_list = symbol_list
        self.initial_capital = initial_capital
        self.heartbeat = heartbeat
        self.start_date = start_date
        self.data_handler_cls = data_handler
        self.execution_handler_cls = execution_handler
        self.portfolio_cls = portfolio
        self.strategy_cls = strategy
        self.events = queue.Queue()
        self.signals = 0
        self.orders = 0
        self.fills = 0
        self.num_strats = 1
        self._generate_trading_instances()
				
			
Il primo metodo, _generate_trading_instances, collega tutti gli oggetti di trading (Data- Handler, Strategy, Portfolio and ExecutionHandler) a vari componenti interni:
				
					# backtest.py

def _generate_trading_instances(self):
    """
    Genera le istanze degli componenti del backtest a partire dalle loro classi.
    """

    print("Creating DataHandler, Strategy, Portfolio and ExecutionHandler")
    self.data_handler = self.data_handler_cls(self.events,
                                              self.csv_dir,
                                              self.symbol_list)
    self.strategy = self.strategy_cls(self.data_handler,
                                      self.events)
    self.portfolio = self.portfolio_cls(self.data_handler,
                                        self.events,
                                        self.start_date,
                                        self.initial_capital)
    self.execution_handler = self.execution_handler_cls(self.events)
				
			

Il metodo _run_backtest gestisce direttamente i segnali all’interno del motore di backtest.

Esecuzione del backtest

Come spiegato nelle lezioni precedenti del corso sul trading system event-driven con Python, il motore di backtest utilizza due cicli while annidati. Il ciclo esterno regola il ritmo operativo del sistema, mentre il ciclo interno controlla gli eventi nella coda e richiama il metodo appropriato sull’oggetto corrispondente.

Quando il sistema rileva un MarketEvent, l’oggetto Strategy elabora nuovi segnali e l’oggetto Portfolio aggiorna l’orario di riferimento. Se l’evento è un SignalEvent, il Portfolio gestisce il segnale e lo trasforma, se necessario, in uno o più OrderEvents.

Nel caso in cui arrivi un OrderEvent, il sistema invia l’ordine all’ExecutionHandler, che lo trasmette al broker se è attivo il live trading. Infine, quando si riceve un FillEvent, il Portfolio aggiorna le posizioni per mantenerle coerenti con gli eseguiti più recenti.

				
					# backtest.py

def _run_backtest(self):
    """
    Esecuzione del backtest.
    """
    i = 0
    while True:
        i += 1
        print(i)
        # Aggiornamento dei dati di mercato
        if self.data_handler.continue_backtest == True:
            self.data_handler.update_bars()
        else:
           break
        # Gestione degli eventi
        while True:
            try:
                event = self.events.get(False)
            except queue.Empty:
                break
            else:
                if event is not None:
                    if event.type == 'MARKET':
                        self.strategy.calculate_signals(event)
                        self.portfolio.update_timeindex(event)
                    elif event.type == 'SIGNAL':
                        self.signals += 1
                        self.portfolio.update_signal(event)
                    elif event.type == 'ORDER':
                        self.orders += 1
                        self.execution_handler.execute_order(event)
                    elif event.type == 'FILL':
                        self.fills += 1
                        self.portfolio.update_fill(event)
        time.sleep(self.heartbeat)

				
			

Dopo aver completato la simulazione del backtest, possiamo visualizzare le prestazioni della strategia nel terminale / console python.
Il motore di backtest crea la curva di equity dal Dataframe pandas e visualizza le statistiche di riepilogo, così come il conteggio di Segnali, Ordini ed Eseguiti:

				
					# backtest.py

def _output_performance(self):
    """
    Stampa delle performance della strategia dai risultati del backtest.
    """
    self.portfolio.create_equity_curve_dataframe()
    print("Creating summary stats...")
    stats = self.portfolio.output_summary_stats()
    print("Creating equity curve...")
    print(self.portfolio.equity_curve.tail(10))
    pprint.pprint(stats)
    print("Signals: %s" % self.signals)
    print("Orders: %s" % self.orders)
    print("Fills: %s" % self.fills)

				
			
L’ultimo metodo da implementare è il simulate_trading. Esso richiama semplicemente in ordine i 2 metodi descritti precedentemente:
				
					# backtest.py

def simulate_trading(self):
    """
    Simula il backtest e stampa le performance del portafoglio.
    """
    self._run_backtest()
    self._output_performance()
				
			

Conclusione

Il motore di backtest appena descritto rappresenta una soluzione potente per eseguire simulazioni di trading basate su eventi. Questo sistema è altamente flessibile e può essere facilmente adattato per eseguire strategie di trading live o per il backtesting con dati storici. Grazie alla sua architettura modulare, è possibile personalizzare ogni componente, migliorando così l’efficienza e la precisione del backtest.

Il codice completo presentato in questa lezione, basato sul sistema di trading event-driven TQTradingSystem, è disponibile nel seguente repository GitHub: https://github.com/tradingquant-it/TQTradingSystem.”

Torna in alto