Negli ultimi giorno sono stato impegnato a lavorare sul progetto open-source DTForex. Ho apportato alcuni utili miglioramenti e ho pensato di condividerli con questo nuovo articolo della serie dedicata al trading algoritmico sul mercato forex.
In particolare, ho apportato le seguenti modifiche, che descriveremo a lungo in questo articolo:
- Modifica dell’oggetto
Position
per correggere un errore nella gestione delle aperture e delle chiusure di una posizione - Aggiunta della funzionalità di gestione dei dati storici tramite il download di file di dati tick da DukasCopy
- Implementazione della prima versione di un backtester basato su eventi sulla base dei dati di tick giornalieri
Correzione degli errori di gestione della posizione
La prima modifica che introduciamo è una nuova logica per gestire gli ordini acquisto/vendita nell’oggetto Position
.
Inizialmente l’oggetto Position
è stato progettato in modo molto snello, delegando all’oggetto Portfolio
la maggior parte del lavoro per il calcolo dei prezzi di posizione
Tuttavia, questo ha aumentato inutilmente la complessità della classe Portfolio
, che rende il codice difficile da leggere e capirne la logica. Inoltre diventa particolarmente problematico quando si vuole implementare una gestione personalizzata del portafoglio senza doversi preoccupare della gestione delle posizioni “standard”.
Inoltre abbiamo verificato la presenza di un errore concettuale nella logica implementata: abbiamo mescolato l’acquisto e la vendita di ordini con essere in una posizione long o short. Questo significava il calcolo non corretto del P&L alla chiusura di una posizione il calcolo.
Abbiamo quindi modificato l’oggetto Position
per accettare i prezzi bid e ask, invece di “aggiungere” e “rimuovere” i prezzi, che erano originariamente determinati a monte dell’oggetto Position
tramite il Portfolio
. In questo modo l’oggetto Position
tiene traccia se siamo long o short e utilizza il corretto prezzo di bid/ask come valore di acquisto o di chiusura.
Abbiamo anche modificato gli unit test per riflettere la nuova interfaccia. Nonostante il fatto che queste modifiche richiedano del tempo per essere completate, fornisce una maggiore fiducia nei risultati. Ciò è particolarmente vero se consideriamo strategie più sofisticate.
Di seguito vediamo il codice del nuovo file position.py:
Gestione dei dati storici dei tick
La successiva importante funzionalità da prevedere all’interno di un completo sistema di trading è l’abilità di effettuare un backtesting ad alta frequenza .
Un prerequisito essenziale consiste nella creazione di un archivio per i dati di tick delle coppie di valute. Tali dati possono diventare piuttosto grandi. Ad esempio, i dati di tick di un giorno per una singola coppia di valute da DukasCopy in formato CSV ha una dimensione di circa 3,3 Mb.
Si può quindi facilmente intuire come il backtest intraday di oltre 20 coppie di valute, su più anni, con significative variazioni dei parametri, può portare rapidamente a gigabyte di dati che devono essere elaborati.
Tali dati necessitano di una gestione speciale, compresa la creazione di un database di titoli, al alte prestazioni e completamente automatizzato. Discuteremo di questo sistema in futuro, ma per ora i file CSV saranno sufficienti per i nostri scopi.
Per mettere sullo stesso piano i dati storici di backtest e di live streaming, dobbiamo creare una classe atratta di gestione dei prezzi chiamata PriceHandler
.
PriceHandler
è un esempio di una classe base astratta, dove si prevede che qualsiasi classe ereditata deve sovrascrivere i metodi “puramente virtuali”. L’unico metodo obbligatorio è stream_to_queue
, che viene chiamato tramite il thread dei prezzi quando il sistema viene attivato (live trading o backtest). La funzione stream_to_queue
recuepra i dati sui prezzi da una sorgente che dipende dalla particolare implementazione della classe, quindi utilizza il metodo .put()
della libreria queue per aggiungere un oggetto TickEvent
.
In questo modo tutte le sottoclassi di PriceHandler
possono interfacciarsi con il resto del sistema di trading senza che i componenti rimanenti sappiano (o si preoccupino!) di come vengono generati i dati sui prezzi.
Questo ci offre una notevole flessibilità per collegare file flat, archivi di file come HDF5, database relazionali come PostgreSQL o anche risorse esterne come siti Web, al motore di backtesting o di trading live.
Di seguito il codice dell’oggetto PriceHandler
:
Abbiamo bisogno inoltre di una sottoclasse chiamata HistoricCSVPriceHandler
, che preveda due metodi.
Il primo è chiamato _open_convert_csv_files
e utilizza Pandas per caricare un file CSV in un DataFrame e formare le colonne Bid e Ask. Il secondo metodo, stream_to_queue
scorre attraverso questo DataFrame e ad ogni iterazione aggiunge un oggetto TickEvent
alla coda degli eventi.
Inoltre, i prezzi correnti di bid/ask correnti impostati a livello di classe, e vengono successivamente interrogati tramite l’oggetto Portfolio
.
Di seguito il codice di HistoricCSVPriceHandler
:
Ora che abbiamo una funzionalità per gestire i dati storici di base, siamo in grado di creare un backtester completamente guidato dagli eventi.
Funzionalità di BackTesting Event-Driven
Nel trading algoritmico è fondamentale utilizzare un motore di backtesting che si avvicina il più possibile ad un motore di trading live. Ciò è dovuto al fatto che una sofisticata gestione dei costi di transazione, soprattutto ad alta frequenza, è spesso il fattore determinante per stabilire se una strategia sarà redditizia o meno.
Tale gestione dei costi di transazione ad alta frequenza può essere realmente simulata solo con l’uso di un motore di esecuzione basato su eventi multi-thread. Sebbene un tale sistema sia significativamente più complicato di un basilare backtester vettorializzato di “ricerca” di P&L, potrà simulare più fedelmente il comportamento reale e ci consentirà di prendere decisioni migliori nella scelta delle strategie.
Inoltre, possiamo iterare più rapidamente col passare del tempo, perché non dovremo passare continuamente dalla strategia di “livello di ricerca” alla strategia di “livello di implementazione” poiché sono la stessa cosa. Gli unici due componenti che cambiano sono la classe di streaming dei prezzi e la classe di esecuzione. Tutto il resto sarà identico tra i sistemi di backtesting e live trading.
In effetti, questo significa che il nuovo codice backtest.py
è quasi identico al codice trading.py
che gestisce il trading real o il trading practice con OANDA. Abbiamo solo bisogno di prevedere l’importazione delle classi HistoricPriceCSVHandler
e SimulatedExecution
al posto delle classi StreamingPriceHandler
e OANDAExecutionHandler
. Tutto il resto rimane lo stesso.
Di seguito il codice di backtest.py
:
L’utilizzo di un sistema di esecuzione multi-thread per il backtest ha il principale svantaggio di non essere deterministico. Ciò significa che eseguendo più volte il backtest degli stessi dati si avranno risultati differenti, anche se piccole.
Questo accade perché non è possiamo garantire lo stesso ordine delle istruzioni eseguite dai thread, per esecuzioni differenti della stessa simulazione. Ad esempio, quando si inseriscono elementi nella coda, si potrebbero ottenere nove oggetti TickEvent
inseriti nella coda nel backtest n.1, ma potremmo ottenerne undici nel backtest n.2.
Poiché l’oggetto Strategy
esegue il polling della coda degli oggetti TickEvent
, vedrà prezzi bid/ask diversi nelle due serie e quindi aprirà una posizione a prezzi bid/ask diversi. Ciò porterà a (piccole) differenze nei rendimenti.
Questo è un grosso problema? Non credo proprio. Non solo è così che funzionerà il sistema live, ma ci consente anche di sapere quanto sia sensibile la nostra strategia alla velocità di ricezione dei dati. Ad esempio, se calcoliamo la varianza dei rendimenti in tutte i backtest eseguiti con gli stessi dati, avremo un’idea di quanto la strategia sia sensibile alla latenza dei dati.
Idealmente, vogliamo una strategia che abbia una piccola varianza in ciascuna delle nostre serie. Tuttavia, se si ha una varianza elevata, significa che dovremmo fare molta attenzioni a mettere live questa strategia.
Potremmo persino eliminare completamente il problema del determinismo semplicemente utilizzando un thread singolo nel nostro codice di backtest (come per il backtester event-driven per le azioni di DataTrading). Tuttavia, questo ha lo svantaggio di ridurre il realismo con il sistema live. Questi sono i dilemmi di simulazione di trading ad alta frequenza!
Prossimi Passi
Un altro problema del sistema che bisogna risolvere è la gestione di solo una valuta di base di EUR e una singola coppia di valute, EUR/USD.
Ora che la gestione Position
è stata sostanzialmente modificata, sarà molto più semplice estenderla per gestire più coppie di valute. Questo è il passaggio successivo.
A quel punto saremo in grado di provare strategie multi-coppia di valute ed eventualmente introdurre Matplotlib per rappresentare graficamente i risultati.
Per il codice completo riportato in questo articolo, utilizzando il modulo di backtesting event-driven per il forex (DTForex) si può consultare il seguente repository di github:
https://github.com/datatrading-info/DTForex