Nel presente articolo desidero giocare con i parametri di progetto di un trading system genetico allo scopo di mettere in evidenza alcune dinamiche, spesso dirimenti durante la fase di progettazione. L’obiettivo è legare la complessità ed il numero degli elementi del genetic pool alla varietà e alla robustezza dei prodotti finiti.
Per cominciare prendiamo in esame lo storico giornaliero di SPY (ETF su SPDR S&P500) e processiamolo all’interno della versione Python del motore Akira (uno dei codici open source forniti all’interno del percorso “Machine Learning Academy“, sull’applicazione dell’Intelligenza Artificiale all’Analisi Quantitativa).

Assecondando una dinamica di base rialzista, iniziamo ad analizzare lo strumento alla ricerca di inefficienze statistiche da poter sfruttare ripetitivamente su tale asset.
A questo punto isoliamo un’unico elemento nel Genetic Pool: l’Average Price, ossia la media aritmetica tra open, high, low e close. Permettiamo alla macchina di andare indietro nel tempo al massimo 5 barre (MAX_OFFSET = 4), ottenendo così soltanto 20 regole.

A questo punto partiamo con una simulazione 2000-2019 In Sample (la macchina potrà addestrarsi soltanto in questo intervallo temporale) e 2020-2023 Out of Sample (su tale periodo la macchina metterà alla prova i pattern identificati nel periodo In Sample, ma non potrà in alcun modo modellare il proprio comportamento di conseguenza).
Entriamo long a mercato in apertura della barra successiva ad un setup, acquistando 10000 $ di SPY:
CAPITAL = 10000
Usciamo dal trade la sesta apertura successiva all’ingresso:
TIME_EXIT = 5
Come Fitness Function scegliamo il rapporto tra Profit e MaxDrawDown:
FITNESS_FUNCTION = “Profit/MaxDrawDown”
Impostiamo una popolazione di 100 investitori virtuali e 100 generazioni (10000 combinazioni):
POPULATION_SIZE = 100
NU_GENERATIONS = 100
Forziamo un unico cromosoma di 3 regole nel DNA di ciascun investitore:
DNA_SIZE = 3
Salviamo il miglior 30% della popolazione ad ogni generazione:
BEST_DNA_RATIO = 0.3
Applichiamo l’algoritmo di Crossover al 30% della popolazione:
CROSS_DNA_RATIO = 0.3
Applichiamo l’algoritmo di Mutation al 40% della popolazione con probabilità del 20%:
MUTATION_PROB = 0.2
In questa versione del motore Akira permettiamo di aprire trade sovrapposti (non dobbiamo attendere il termine di un trade per aprirne un altro). Questo allo scopo di verificare l’effettiva bontà “grezza” di una condizione di ingresso.
Imponiamo un minimo di 1000 trade in In Sample per organismo (questo per avere un minimo di significatività statistica):
MIN_OPERATIONS = 1000

In generazione 0 troviamo la seguente equity line, relativa al miglior individuo tra i 100 dell’intera popolazione:

generation 0 : 5.1307042175270565 avgprice(2) > avgprice(1) and avgprice(2) > avgprice(0) and avgprice(1) > avgprice(0)
Una semplice regola come quella a tre geni appena intercettata sembra essere in grado di portare vantaggio anche nel periodo successivo al 2020 (a destra della linea rossa tratteggiata troviamo il periodo di Out of Sample).
In generazione 3 (dopo 400 combinazioni) otteniamo un nuovo organismo che migliora impercettibilmente le prestazioni in In Sample, ma muta il comportamento in Out of Sample:

generation 3 : 5.149406673214812 avgprice(3) > avgprice(1) and avgprice(2) > avgprice(0) and avgprice(1) > avgprice(0)
Quello che è cambiato all’interno del pattern è il passaggio da profondità 3 a profondità 4.
In generazione 8 la convergenza tocca il punto di massimo (da quel punto in poi non riusciamo a migliorare la fitness function):

generation 8 : 5.949611461933047 avgprice(3) > avgprice(1) and avgprice(3) > avgprice(0) and avgprice(1) > avgprice(0)
Il rapporto di riferimento (fitness function) è passato da 5.15 a 5.95. La dinamica in Out of Sample è rimasta positiva dall’inizio (sembra che siamo riusciti a trovare un filone interessante).
Diamo uno sguardo, a questo punto, all’insieme delle curve relative ai migliori rappresentanti di ciascuna generazione (visualizzeremo soltanto i rappresentanti che abbiano fatto registrare un miglioramento dalle generazioni precedenti):

In verde il benchmark costituito da una strategia buy & hold. Ben visibile lo sciame di curve ottenute che caratterizza un’unica famiglia genetica (come è possibile anche verificare dalle formule del pattern). Si noti quanto siano limitate le differenze: questo è dovuto alla bassa complessità genetica di progetto (solo 3 geni, pool genetico di sole 20 regole totali, average price come unica metrica di lavoro).
Proviamo a questo punto ad arricchire le componenti genetiche per vedere come ciò si declini sui risultati finali. A tale scopo inseriamo nel pool genetico i seguenti elementi:
- open
- high
- low
- close
- avgprice = (close + open + low + high) / 4
- medprice = (low + high) / 2
- medbodyprice = (close + open) / 2
Le regole totali, a parità di lagging, passano da 20 a 1127.

Rilanciamo la simulazione con i medesimi parametri e analizziamo cosa accade di generazione in generazione:
Il miglior rappresentante della generazione 0 risulta meno appetibile di prima e questo deriva dall’aumento delle combinazioni possibili: è più difficile che ai blocchi di partenza con sole 100 combinazioni si ottenga già qualcosa di utilizzabile (ma non è escluso in assoluto).

generation 0 : 0.9646324017850788 high(4) > low(4) and open(3) > medprice(4) and medprice(1) > avgprice(0)
I geni elementari fanno ricorso ad elementi più variegati, nonostante ciò, appare ben evidente una dinamica positiva dell’equity line che si perpetra anche in Out of Sample.
In generazione 3 (la quarta totale) passiamo ad una auto-soluzione decisamente più appetibile, sia da un punto di vista metrico che grafico. Ben visibile, tuttavia, la fase di sofferenza a marzo 2020 (Covid19).

generation 3 : 7.616502376543235 open(0) > close(0) and medbodyprice(4) > low(4) and open(2) > avgprice(1)
Siamo passati da fitness inferiore ad 1 a 7.61. Tale valore significa che utilizzando tale strategia ci si aspetta un rapporto sette ad 1, ossia per ogni 7.61$ guadagnati ci aspettiamo di perdere un dollaro.
Si noti, inoltre, come la formula del setup sia cambiata, ma abbia mantenuto 5 tratti elementari: open, close, medbodyprice, low, avgprice.
In generazione 11 le cose migliorano ancora. La varianza diminuisce e raggiungiamo addirittura un rapporto di 7.89.

generation 11 : 7.897308288202666 open(0) > close(0) and medbodyprice(4) > low(4) and low(3) > close(0)
Da un punto di vista metrico il Profit Factor ed il Kestner Ratio (metrica legata alla regolarità della curva) ottengono un deciso miglioramento.
L’ultimo miglioramento lo registriamo in generazione 97, quando raggiungiamo una fitness di 14.54.

generation 97 : 14.54777099679234 open(0) > avgprice(0) and high(2) > open(2) and low(3) > close(0)
Visioniamo a questo punto l’intero sciame delle curve relative ai migliori rappresentanti di ciascuna generazione, che abbiano fatto registrare un miglioramento dalle generazioni passate.

Il fatto di partire da una curva di benchmark già fortemente positiva non deve confondere. Il miglioramento è evidente seguendo le curve dal basso verso l’alto (non parliamo ovviamente solo del profitto, ma di quasi tutte le metriche di confronto).
Anche in questo caso si parla della stessa famiglia di curve (da un punto di vista genetico). Quello che colpisce è come lo sciame di curve si sia allargato: ciò dipende dall’arricchimento del pool genetico.
La curva di evoluzione testimonia il fatto che, rispetto al caso precedente, i salti evolutivi sono avvenuti in modo più uniforme e non soltanto all’inizio del processo.

Cosa accade se raddoppiamo il numero di geni all’interno del DNA, ad esempio da 3 a 6?
Facciamo ripartire il simulatore e cristallizziamo la generazione di partenza.

generation 0 : 0.0 avgprice(1) > low(0) and medbodyprice(4) > medprice(2) and close(4) > medbodyprice(2) and medbodyprice(2) > avgprice(4) and high(3) > medbodyprice(0) and high(3) > avgprice(0)
La varietà è aumentata esponenzialmente e non ha permesso di partire con il piede giusto. Si noti inoltre 0 come valore di fitness: ciò è dovuto al fatto che la strategia proposta non riesce a raggiungere i 1000 trade minimi richiesti.
Procediamo dunque nell’evoluzione: dobbiamo attendere la generazione 5 per avere un significativo miglioramento.

generation 5 : 0.2958310395778596 high(1) > low(1) and medbodyprice(4) > low(0) and high(2) > close(1) and low(3) > close(1) and medprice(2) > open(0) and high(4) > open(1)
Notiamo ancora delle aree di forte draw down sia in In Sample che in Out of Sample. La generazione successiva le cose migliorano sensibilmente da questo punto di vista (si noti in particolare modo lo spike negativo in corrispondenza a marzo 2020, decisamente più contenuto).

generation 6 : 2.988439098069271 high(1) > low(1) and medbodyprice(4) > low(0) and high(2) > close(1) and close(4) > medbodyprice(2) and medprice(2) > open(0) and high(4) > open(1)
Ma già in generazione 13 assistiamo ad una prima cosa inaspettata: a fronte di un miglioramento in In Sample (la fitness passa a 2.98), osserviamo un peggioramento in Out of Sample.

generation 13 : 3.830350212378348 low(3) > close(1) and avgprice(2) > medprice(0) and high(2) > close(1) and low(3) > close(1) and medprice(2) > open(0) and medbodyprice(3) > low(3)
La macchina ha probabilmente iniziato a modellare il rumore, trovandosi in quella zona di frontiera a cavallo tra fitting ed overfitting.
Le cose peggiorano ancora (in Out of Sample) in generazione 25.

generation 25 : 5.803365988467282 high(3) > low(3) and avgprice(2) > medprice(0) and high(2) > close(1) and low(3) > open(0) and close(4) > low(0) and medprice(2) > medbodyprice(1)
La fitness function è passata da 3.83 a 5.80.
In generazione 89 abbiamo l’ultimo miglioramento in In Sample (da 5.80 a ben 10.09).

generation 89 : 10.09139869030955 medprice(4) > low(1) and avgprice(2) > medprice(0) and avgprice(2) > medprice(1) and close(4) > close(1) and medprice(3) > medbodyprice(0) and medprice(2) > close(0)
La soluzione finale assomiglia molto a quella della simulazione precedente, tuttavia abbiamo incontrato una tendenza maggiore a modellare il rumore (cosa di per se pericolosa per il futuro andamento della strategia). Ciò è accaduto a causa dell’arricchimento del pool genetico e del numero di combinazioni potenziali legate al numero di geni, che è stato raddoppiato.
Il motore open source Akira e tutto ciò che avete visto in questo articolo potete trovarlo all’interno del percorso “Machine Learning Academy” dedicato all’applicazione del Machine Learning alla Finanza Quantitativa.
Ricordo che potete interagire con noi tramite l’indirizzo info@gandalfproject.com
Oltre che su questo sito potete seguirci anche su Linkedin e si Facebook.
Giovanni Trombetta
Founder – Head of R&D
Gandalf Project