Motore di Backtesting con Python – Parte I (Struttura Base)

Negli ultimi mesi abbiamo descritto su DataTrading come testare le varie strategie di trading utilizzando Python e Pandas. La natura vettoriale di Pandas permette elaborazioni estremamente rapide su set di dati di grandi dimensioni siano. Tuttavia, gli approcci di backtesting vettorializzato che abbiamo studiato finora presentano alcune criticità nelle modalità di simulazione dell’esecuzione dei trade. In questa serie di articoli discuteremo un approccio più realistico alla simulazione della strategia, usando Python per costruire un ambiente di backtesting basato sugli eventi.

Software basati sugli Eventi

Prima di approfondire lo sviluppo di questo ambiente di backtesting, è necessario introdurre i concetti base dei sistemi basati sugli eventi. I videogiochi forniscono un classico caso d’uso di tale tipologia di software e sono un semplice esempio da studiare. Un videogioco ha più componenti che interagiscono tra loro in un ambiente real-time con elevati frame-rate. Questo viene gestito grazie all’esecuzione di una serie di calcoli all’interno di un ciclo “infinito” noto come event-loop o game-loop.

Ad ogni tick del game-loop viene chiamata una funzione che si occupa di acquisire l’ultimo evento, quest’ultimo è stato generato da una corrispondente azione precedente all’interno del loop. A seconda della natura dell’evento, come ad esempio la pressione di un tasto o il clic del mouse, sono eseguite specifiche azioni che interromperanno il ciclo o genereranno alcuni eventi aggiuntivi.

Di seguito un esempio del pseudo-codice dell’event-loop:

            while True:  # Esecuzione infinita del loop f
    new_event = get_new_event()   # ottengo l'ultimo evento

    # A seconda del tipo di evento si esegue una azione
    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()
    # ... e molti altri eventi

    redraw_screen()   # Update dell'output per fornire un'animazione
    tick(50)   # Pausa di 50 millisecondi
        

Il codice verifica continuamente la presenza di nuovi eventi e, in caso affermativo, esegue azioni a seconda del tipo di eventi. In particolare, permette l’illusione di un sistema con risposta in tempo reale dato che il codice viene continuamente ripetuto e quindi si ha una verifica continua degli eventi. Ovviamente, questo è esattamente quello di cui abbiamo bisogno per effettuare simulazioni di trading ad alta frequenza.

Perchè abbiamo bisogno di un Backtesting Event-Driven

I sistemi basati sugli eventi offrono molti vantaggi rispetto a un approccio vettorializzato:

  • Riutilizzo del codice – un backtester basato sugli eventi, in base alla progettazione, può essere utilizzato sia per il backtesting storico sia per il live trading con una minima modifica dei componenti. Questo non è possibile per i backtesting vettorizzati dove tutti i dati devono essere disponibili contemporaneamente per poter effettuare analisi statistiche.
  • Bias di Look-Ahead – con un backtesting basato sugli eventi non vi è alcun bias di previsione perché l’acquisizione dei dati finanziari è gestita come un “evento” su cui si deve effettuare specifiche azioni. In questo modo è possibile un ambiente di backtestering event-driven è alimentato  “instante dopo instante” con i dati di mercato, replicando il comportamento di un sistema di gestione degli ordini e del portafoglio.
  • Realismo – I backtesting basati sugli eventi consentono una significativa personalizzazione del modalità di esecuzione degli ordini e dei costi di transazione sostenuti. È semplice gestire il market-order e il limit-order, oltre al market-on-open (MOO) e al market-on-close (MOC), poiché è possibile costruire un gestore di exchange personalizzato.

 

Sebbene i sistemi basati sugli eventi siano dotati di numerosi vantaggi, essi presentano due importanti svantaggi rispetto ai più semplici sistemi vettorizzati. Innanzitutto sono molto più complessi da implementare e testare. Ci sono più “parti mobili” che causano una maggiore probabilità di introdurre bug. Per mitigare questa criticità è possibile implementare una  metodologia di testing del software, come il test-driven development.

In secondo luogo, hanno tempi di esecuzione più lenti da eseguire rispetto a un sistema vettorializzato. Le operazioni vettoriali ottimizzate non possono essere utilizzate quando si eseguono calcoli matematici. Discuteremo dei modi per superare queste limitazioni negli articoli successivi.

Struttura di un sistema di Backtesting Event-Driven

Per applicare un approccio event-driven a un sistema di backtesting è necessario definire i componenti base (o oggetti) che gestiscono compiti specifici:

  • Event: l’Event è la classe fondamentale di un sistema event-driven. Contiene un attributto “tipo” (ad esempo, “MARKET”, “SIGNAL”, “ORDER” o “FILL”) che determina come viene gestito uno specifico evento all’interno dell’event-loop.
  • Event Queue: la Coda degli Eventi è un oggetto Python Queue che memorizza tutti gli oggetti della sotto-classe Event generati dal resto del software.
  • DataHandler: il DataHandler è una classe base astratta (ABC) che presenta un’interfaccia per la gestione di dati storici o del mercato in tempo reale. Fornisce una significativa flessibilità in quanto i moduli della strategia e del portfolio possono essere riutilizzati da entrambi gli approcci. Il DataHandler genera un nuovo MarketEvent ad loop del sistema (vedi sotto).
  • Strategy: anche la Strategy è una classe ABC e presenta un’interfaccia per elaborare i dati di mercato e generare i corrispondenti SignalEvents, che vengono infine utilizzati dall’oggetto Portfolio. Un SignalEvent contiene un simbolo ticker, una direzione (LONG or SHORT) e un timestamp.
  • Portfolio: si tratta di una classe ABC che implementa la gestione degli ordini associata alle posizioni attuali e future di una strategia. Svolge anche la gestione del rischio in tutto il portafoglio, compresa l’esposizione settoriale e il dimensionamento delle posizioni. In un’implementazione più sofisticata, questo potrebbe essere delegato a una classe RiskManagement. La classe Portfolio prende un SignalEvents dalla coda e genera uno o più OrderEvents che vengono aggiunti alla coda.
  • ExecutionHandler: l’ExecutionHandler simula una connessione a una società di intermediazione o broker. Il suo compito consiste nel prelevare gli OrderEvents dalla coda ed eseguirli, tramite un approccio simulato o una connessione reale verso il broker. Una volta eseguiti gli ordini, il gestore crea i FillEvents, che descrivono ciò che è stato effettivamente scambiato, comprese le commissioni, lo spread e lo slippage (se modellato).
  • Loop – Tutti questi componenti sono racchiusi in un event-loop che gestisce correttamente tutti i tipi di eventi, indirizzandoli al componente appropriato.

Questo è il modello base di un motore di trading. Vi è un significativo margine di espansione, in particolare per quanto riguarda l’utilizzo del portafoglio. Inoltre, i diversi modelli di costo delle transazioni possono essere implementati utilizzando una propria gerarchia di classi. In questa fase però introdurrebbe una complessità inutile all’interno di questa serie di articoli, quindi al momento non viene approfondita ulteriormente. Nei tutorial successivi si potrà pensare di  espandere il sistema per includere ulteriori gradi di realismo.

Di seguito potete trovare il codice Python che mostra  come il backtester funziona in pratica. Ci sono due loop nidificati all’interno del codice. Il loop esterno è usato per dare al backtester un impulso, o ritmo. Nel live trading questa è la frequenza con cui vengono acquisiti i nuovi dati di mercato. Per le strategie di backtesting questo non è strettamente necessario poiché il backtester utilizza i dati di mercato forniti in forma di drip-feed (vedi la riga bars.update_bars ()).

Il ciclo interno gestisce effettivamente gli eventi dall’oggetto Queue. Gli Eventi specifici sono delegati al rispettivo componente e successivamente vengono aggiunti nuovi eventi alla coda. Quando la coda degli eventi è vuota, si riprende il ciclo esterno:

            # Dichiarazione dei componenti e rispettive classi 
bars = DataHandler(..)
strategy = Strategy(..)
port = Portfolio(..)
broker = ExecutionHandler(..)

while True:
    # Update delle barre dei prezzi (codice specifico per il backtesting, opposto al live trading)
    if bars.continue_backtest == True:
        bars.update_bars()
    else:
        break

    # Gestione degli eventi
    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)

    # pausa di 10 minuti
    time.sleep(10 * 60)
        
Questo è lo schema di base di come è progettato un ambiente di backtesting basato sugli eventi. Nel prossimo articolo si descrive la gerarchia della classe Events.
Torna in alto
Scroll to Top