Performance di una Strategia – Trading System sul Forex Parte 6

performance di una strategia

In questa lezione del corso per il trading automatico sul  forex descriviamo effettuare il backtest e visualizzare le performance di una strategia multiday sul mercato Forex. 

A tale scopo dobbiamo introdurre alcune nuove funzionalità al sistema di trading automatico sul forex che stiamo costruendo in questo corso:

  • Generazione di dati tick simulati: poiché scaricare molti dati tick sul Forex risulta complesso (almeno per alcuni dei provider che utilizziamo!), abbiamo preferito generare dati tick casuali per testare il sistema.
  • Backtest multiday: per rendere il sistema TQforex più utile, abbiamo aggiunto la funzionalità di backtest su più giorni di dati tick e su più coppie di valute.
  • Tracciamento dei risultati del backtesting: oltre all’output della console, ora visualizziamo una curva di equity e il drawdown storico utilizzando la libreria Seaborn per creare i grafici delle performance.

Script per Simulare i Dati di Tick

Una funzionalità estremamente importante per un sistema di trading consiste nel poter eseguire un backtest su dati tick storici che coprano più giorni. In passato il sistema gestiva il backtest solo tramite un singolo file. Questa soluzione non risultava scalabile, dato che caricava tutto il file in memoria per convertirlo in un DataFrame di pandas. Sebbene i file dei dati tick non siano enormi (circa 3,5 MB ciascuno), gestire più coppie di valute su mesi o anni genera rapidamente grandi quantità di dati.

Per costruire una funzionalità che supporti più giorni e file, abbiamo iniziato a scaricare dati dal feed tick storico di DukasCopy. Tuttavia, abbiamo incontrato problemi durante il download dei file necessari per i test.

Dato che non riteniamo essenziale disporre di serie storiche reali per testare il sistema, abbiamo preferito scrivere uno script che generasse automaticamente dati di tick simulati. Abbiamo inserito questo script nel file scripts/generate_simulated_pair.py, che puoi trovare qui.

Lo script crea un elenco di timestamp distribuiti casualmente, associando a ciascuno valori bid/ask e volumi. Manteniamo costante lo spread tra bid e ask, mentre i prezzi bid e ask seguono un random walk.

Poiché non intendiamo testare strategie reali con questi dati, non ci preoccupiamo delle loro proprietà statistiche o dei valori assoluti rispetto ai dati reali del Forex. Ci interessa garantire il formato corretto e una lunghezza approssimativa per testare efficacemente il sistema di backtesting su più giorni.

Creazione dei file

Attualmente lo script genera dati Forex per tutto il mese di gennaio 2017. Utilizziamo la libreria Calendar di Python per considerare i giorni lavorativi (anche se non abbiamo ancora escluso le festività). A tale scopo creiamo file nel formato BBBQQQ_YYYYMMDD.csv, dove BBBQQQ indica la coppia di valute (es. EURUSD) e YYYYMMDD rappresenta la data (es. 20170112).

Inseriamo questi file nella directory CSV_DATA_DIR, definita nel file settings.py dell’applicazione.

Per generare i dati, basta eseguire il seguente comando, sostituendo BBBQQQ con la coppia di valute desiderata, ad esempio EURUSD:

				
					python scripts/generate_simulated_pair.py BBBQQQ
				
			

Il file richiede una modifica per generare dati su più mesi o anni. Ogni file di tick giornalieri occupa circa 3,2 MB di spazio.

In futuro modificheremo questo script per generare dati su un periodo più ampio. Ci baseremo su uno specifico elenco di coppie di valute invece di utilizzare valori codificati. Per ora, però, questa versione ci basta per iniziare.

Ricordiamo che il formato corrisponde esattamente a quello dei dati storici dei tick forniti da DukasCopy. Il set di dati che stiamo attualmente utilizzando.

Implementazione di un Backtest Multiday

Dopo aver generato i dati tick simulati, per calcolare le performance di una strategia multiday passiamo all’implementazione del backtesting su più giorni. Per il momento preferiamo lavorare con un set di file CSV, uno per ogni giorno e per ogni coppia di valute. In futuro prevediamo di utilizzare un sistema di archiviazione dati più robusto, come PyTables con HDF5,

Questa soluzione scala bene con l’aumento del numero di giorni. La natura event-driven del nostro sistema ci permette di mantenere in memoria solo N file contemporaneamente, dove N rappresenta il numero di coppie di valute scambiate in un dato giorno.

Alla base del sistema abbiamo pensato che la classe HistoricCSVPriceHandler continuerà a usare il metodo stream_next_tick. Dobbiamo però prevedere una modifica: dovrà gestire i dati di più giorni caricandoli in modo sequenziale.

Termineremo il backtest quando riceveremo l’eccezione StopIteration generata dal metodo next(..) su self.all_pairs, come mostrato in questo frammento di pseudocodice:

				
					# price.py

..
..

def stream_next_tick(self):
	  ..
	  ..
    try:
        index, row = next(self.all_pairs)
    except StopIteration:
        return
    else:
    	..
    	..
      # Aggiungere un tick alla coda
				
			

Nella nuova implementazione, questo snippet viene modificato come segue:

				
					# price.py

..
..

def stream_next_tick(self):
    ..
    ..
    try:
        index, row = next(self.cur_date_pairs)
    except StopIteration:
        # Fine dei dati per l'attuale giorno
        if self._update_csv_for_day():
            index, row = next(self.cur_date_pairs)
        else: # Fine dei dati
            self.continue_backtest = False
            return

    ..
    ..

    # Aggiunta del tick nella coda
				
			

In questo frammento, quando si genera un StopIteration, verifichiamo il risultato di self._update_csv_for_day(). Se il risultato è True, continuiamo il backtest (dato che il self.cur_date_pairs potrebbe essere cambiato nei dati dei giorni successivi). Se invece il risultato è False, interrompiamo il backtest.

Questo approccio risulta molto efficiente in termini di memoria, perché carichiamo solo un numero limitato di giorni di dati alla volta. Così possiamo eseguire mesi di backtesting e siamo limitati soltanto dalla velocità di elaborazione della CPU e dalla quantità di dati che riusciamo a generare o acquisire.

Aggiorniamo quindi la documentazione per indicare che ora il sistema si aspetta più giorni di dati in un formato specifico, collocati in una directory particolare che dobbiamo specificare.

Performance di una strategia multiday

Un backtest serve a poco se non riusciamo a visualizzare le performance di una strategia multiday nel tempo. Anche se il sistema si è basato finora sulla console, in questa lezione iniziamo a introdurre le basi per un’interfaccia utente grafica (GUI).

In particolare, creiamo i classici “tre pannelli” di grafici che spesso accompagnano le metriche di performance dei sistemi di trading quantitativo: la curva equity, il profilo dei rendimenti e la curva dei drawdown. Calcoliamo tutti e tre per ogni tick e li salviamo in un file chiamato equity.csv nella directory specificata in OUTPUT_RESULTS_DIR di settings.py.

Per visualizzare i dati utilizziamo una libreria chiamata Seaborn, che genera grafici di alta qualità con un aspetto nettamente migliore rispetto ai grafici predefiniti di Matplotlib. La grafica somiglia molto a quella prodotta dal pacchetto ggplot2 di R. Inoltre, Seaborn si basa su Matplotlib, quindi possiamo ancora utilizzare l’API di Matplotlib.

Per permettere la visualizzazione delle performance di una strategia multiday, abbiamo creato lo script output.py che si trova nella directory backtest/. Il codice di questo script è il seguente:

				
					# output.py

import os, os.path

import pandas as pd
import matplotlib
try:
    matplotlib.use('TkAgg')
except:
    pass
import matplotlib.pyplot as plt
import seaborn as sns

from qsforex.settings import OUTPUT_RESULTS_DIR


if __name__ == "__main__":
    """
    A simple script to plot the balance of the portfolio, or
    "equity curve", as a function of time.

    It requires OUTPUT_RESULTS_DIR to be set in the project
    settings.
    """
    sns.set_palette("deep", desat=.6)
    sns.set_context(rc={"figure.figsize": (8, 4)})

    equity_file = os.path.join(OUTPUT_RESULTS_DIR, "equity.csv")
    equity = pd.io.parsers.read_csv(
        equity_file, parse_dates=True, header=0, index_col=0
    )

    # Plot three charts: Equity curve, period returns, drawdowns
    fig = plt.figure()
    fig.patch.set_facecolor('white')     # Set the outer colour to white

    # Plot the equity curve
    ax1 = fig.add_subplot(311, ylabel='Portfolio value')
    equity["Equity"].plot(ax=ax1, color=sns.color_palette()[0])

    # Plot the returns
    ax2 = fig.add_subplot(312, ylabel='Period returns')
    equity['Returns'].plot(ax=ax2, color=sns.color_palette()[1])

    # Plot the returns
    ax3 = fig.add_subplot(313, ylabel='Drawdowns')
    equity['Drawdown'].plot(ax=ax3, color=sns.color_palette()[2])

    # Plot the figure
    plt.show()
				
			

Come possiamo vedere, importiamo Seaborn nello script, apriamo il file equity.csv in un DataFrame pandas e creiamo tre grafici: la curva di equity, i rendimenti e il drawdown.

Il grafico del drawdown viene calcolato da una funzione di supporto che si trova in performance/performance.py e che richiamiamo dalla classe Portfolio alla fine di ogni backtest.

Risultati

Ecco un esempio di performance di una strategia multiday MovingAverageCrossStrategy, utilizzando un set di dati EURUSD generato casualmente per il mese di gennaio 2017:


trading-algoritmico-forex-6-output

In particolare, possiamo notare le sezioni piatte della curva azionaria durante i fine settimana, quando non abbiamo dati disponibili (almeno per questo set di dati simulato). Inoltre, osserviamo che la strategia perde denaro in modo piuttosto prevedibile su questo set di dati simulato in modo casuale.

Questo rappresenta un buon test per il nostro sistema. Stiamo semplicemente tentando di seguire una tendenza su una serie temporale casuale. Le perdite derivano dallo spread fisso introdotto durante la simulazione.

Questo ci rende evidente che, per ottenere profitti consistenti nel trading algoritmico del Forex ad alta frequenza, dobbiamo possedere uno specifico vantaggio quantificabile che generi rendimenti positivi superiori ai costi di transazione come spread e slippage.

Tratteremo in modo più approfondito questo tema fondamentale nelle prossime lezioni di questo corso dedicato al trading algoritmico del Forex.

Conclusioni

Calcoli della posizione di fissaggio

Abbiamo notato che i calcoli della classe Position non riflettono esattamente il metodo con cui OANDA (il broker che utilizziamo nel sistema trading.py) calcola i trade sui cross valutari.

Per questo motivo, uno dei prossimi passi fondamentali sarà eseguire e testare le modifiche al file position.py e aggiornare anche gli unit test presenti in position_test.py. Queste modifiche avranno un effetto a catena sui file portfolio.py e portfolio_test.py.

Valutazione della prestazione

Ora disponiamo di un set base di grafici delle performance di una strategia multiday, che comprende la curva di equity, il profilo dei rendimenti e la serie dei drawdown, ma dobbiamo integrare misure di performance più avanzate.

In particolare, vogliamo calcolare metriche a livello di strategia, come lo Sharpe Ratio, l’Information Ratio e il Sortino Ratio. Inoltre, ci servono statistiche sui drawdown, inclusa la distribuzione dei drawdown e il massimo drawdown. Altri indicatori utili saranno il tasso di crescita annuale composto (CAGR) e il rendimento totale.

A livello di trade o posizione, desideriamo analizzare metriche come profitto/perdita medio, massimo profitto/perdita, rapporto di profitto e rapporto di vincita/perdita. Poiché abbiamo costruito la classe Position come parte centrale del software, potremo generare facilmente queste metriche aggiungendo alcuni metodi dedicati.

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

Torna in alto