DTForex #7 – Nuova Interfaccia di Backtesting

forex-python-trading-algoritmico-007

In questo articolo descriviamo come semplificare l’interfaccia per costruire un nuovo backtest, incapsulando molto del codice “boilerplate” in una nuova classe Backtest. Inoltre vediamo come modificare il sistema per poter gestire più coppie di valute.

Infine vediamo come testare la nuova interfaccia tramite la solita strategia di esempio di Moving Average Crossover, sia su GBP/USD che su EUR/USD.

Nuova Interfaccia di Backtest

Abbiamo modificato l’interfaccia di backtest in modo tale da creare semplicemente un’istanza di Backteste popolarla con i componenti di trading, invece di dover creare un file  backtest.py file personalizzato come in precedenza.

Il modo migliore per iniziare con il nuovo approccio è dare un’occhiata alla directory  examples/ e aprire mac.py:

from backtest import Backtest
from execution import SimulatedExecution
from portfolio import Portfolio
from settings import settings
from strategy import MovingAverageCrossStrategy
from data.price import HistoricCSVPriceHandler

if __name__ == "__main__":
    # Trading su GBP/USD e EUR/USD
    pairs = ["GBPUSD", "EURUSD"]

    # Crea i parametri della strategia per MovingAverageCrossStrategy
    strategy_params = {
        "short_window": 500,
        "long_window": 2000
    }

    # Crea ed esegue il backtest
    backtest = Backtest(
        pairs, HistoricCSVPriceHandler,
        MovingAverageCrossStrategy, strategy_params,
        Portfolio, SimulatedExecution,
        equity=settings.EQUITY
    )
    backtest.simulate_trading()

Il codice è relativamente semplice. In primo luogo il codice importa i componenti necessari, ovvero il BacktestSimulatedExecutionPortfolioMovingAverageCrossStrategy HistoricCSVPriceHandler.

In secondo luogo, definiamo le coppie di valute da negoziare e quindi creiamo un dizionario noto come strategy_params. Questo contiene essenzialmente qualsiasi argomento delle key words che potremmo voler passare alla strategia. Nel caso di un Moving Average Crossover dobbiamo impostare le lunghezze dei periodi delle medie mobili. Questi valori sono in termini di “tick”.

Infine creiamo un’istanza Backtest e passiamo tutti gli oggetti come parametri. Quindi, eseguiamo il backtest stesso.

All’interno del nuovo backtest.py chiamiamo questo metodo:

# backtest.py
..
..
    def simulate_trading(self):
        """
        Simula il backtest e calcola le performance del portfolio
        """
        self._run_backtest()
        self._output_performance()
        print("Backtest complete.")

Esegue il calcolo del backtest (ovvero l’aggiornamento del portafoglio all’arrivo dei tick), nonché il calcolo e l’output delle prestazioni in equity.csv.

Come descritto negli articoli precedenti, possiamo ancora produrre un grafico dell’output con lo script backtest/output.py. Useremo questo script di seguito quando discuteremo dell’implementazione di più coppie di valute.

Gestione di più Coppie di Valute

Siamo finalmente in grado di  testare la prima strategia di trading (non banale) su dati di tick ad alta frequenza per più coppie di valute!

A tale scopo è necessario modificare le modalità di gestione all’interno di MovingAverageCrossStrategy.

Di seguito il codice completo:

class MovingAverageCrossStrategy(object):
    """
    Una strategia base di Moving Average Crossover che genera
    due medie mobili semplici (SMA), con finestre predefinite
    di 500 tick per la SMA  breve e 2.000 tick per la SMA
    lunga.

    La strategia è "solo long" nel senso che aprirà solo una
    posizione long una volta che la SMA breve supera la SMA
    lunga. Chiuderà la posizione (prendendo un corrispondente
    ordine di vendita) quando la SMA lunga incrocia nuovamente
    la SMA breve.

    La strategia utilizza un calcolo SMA a rotazione per
    aumentare l'efficienza eliminando la necessità di chiamare due
    calcoli della media mobile completa su ogni tick.
    """
    def __init__(
            self, pairs, events,
            short_window=500, long_window=2000
    ):
        self.pairs = pairs
        self.pairs_dict = self.create_pairs_dict()
        self.events = events
        self.short_window = short_window
        self.long_window = long_window

    def create_pairs_dict(self):
        attr_dict = {
            "ticks": 0,
            "invested": False,
            "short_sma": None,
            "long_sma": None
        }
        pairs_dict = {}
        for p in self.pairs:
            pairs_dict[p] = copy.deepcopy(attr_dict)
        return pairs_dict

    def calc_rolling_sma(self, sma_m_1, window, price):
        return ((sma_m_1 * (window - 1)) + price) / window

    def calculate_signals(self, event):
        if event.type == 'TICK':
            pair = event.instrument
            price = event.bid
            pd = self.pairs_dict[pair]
            if pd["ticks"] == 0:
                pd["short_sma"] = price
                pd["long_sma"] = price
            else:
                pd["short_sma"] = self.calc_rolling_sma(
                    pd["short_sma"], self.short_window, price
                )
                pd["long_sma"] = self.calc_rolling_sma(
                    pd["long_sma"], self.long_window, price
                )
            # Si avvia la strategia solamente dopo aver creato una 
            # accurata finestra di breve periodo
            if pd["ticks"] > self.short_window:
                if pd["short_sma"] > pd["long_sma"] and not pd["invested"]:
                    signal = SignalEvent(pair, "market", "buy", event.time)
                    self.events.put(signal)
                    pd["invested"] = True
                if pd["short_sma"] < pd["long_sma"] and pd["invested"]:
                    signal = SignalEvent(pair, "market", "sell", event.time)
                    self.events.put(signal)
                    pd["invested"] = False
            pd["ticks"] += 1
                

Essenzialmente creiamo un dizionario degli attributi attr_dict che memorizza il numero di tick trascorsi e se la strategia è “a mercato” per quella particolare coppia.

In calculate_signals aspettiamo di ricevere un TickEvent e quindi calcoliamo le medie mobili semplici per il breve e lungo periodo. Una volta che la SMA breve incrocia al rialzo la SMA lunga per una particolare coppia, la strategia va long ed esce nel modo visto nei precedenti articoli, sebbene lo faccia separatamente per ciascuna coppia.

Abbiamo utilizzato 2 mesi di dati sia per GBP/USD che per EUR/USD e il backtest richiede un po ‘di tempo per essere eseguito. Tuttavia, una volta completato il backtest, siamo in grado di utilizzare backtest/output.py per produrre il seguente grafico delle prestazioni:

trading-algoritmico-forex-7-mac-results

Chiaramente le prestazioni non sono eccezionali in quanto la strategia rimane quasi interamente “sott’acqua” col passare del tempo. Detto questo, non dovremmo aspettarci molto da una strategia di base sui dati tick ad alta frequenza. In futuro esamineremo approcci molto più sofisticati al trading su questa scala temporale.

Si spera che questo sistema possa fornire un utile punto di partenza per lo sviluppo di strategie più sofisticate. Non vedo l’ora di scoprire cosa inventeranno gli altri nel prossimo futuro!

Conclusioni

In questa serie di articoli abbiamo visto le basi di un sistema di trading automatico sul mercato del Forex , implementato in Python. Nonostante il sistema sia completo di funzionalità per il backtest e il paper/live trading, ci sono ancora molti aspetti su cui lavorare.

In particolare si può rendere il sistema molto più veloce, in modo da permettere di effettuare ricerche di parametri in tempi ragionevoli. Sebbene Python sia un ottimo strumento, uno svantaggio è che è relativamente lento rispetto a C / C ++. Quindi si può lavorare sul cercare di migliorare la velocità di esecuzione sia del backtest che dei calcoli delle prestazioni.

Inoltre, un altro aspetto che merita di essere implementato è la gestione di altri tipi di ordine rispetto al semplice ordine di mercato. Per attuare adeguate strategie HFT sul broker OANDA dovremo utilizzare gli ordini limite. Ciò richiederà probabilmente una rielaborazione del modo in cui il sistema esegue attualmente le operazioni, ma consentirà di realizzare un universo molto più ampio di strategie di trading.

 

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

Se si desidera leggere gli altri articoli di questa serie, sono disponibili ai seguenti link:

DTForex #6 – Backtesting su più giorni e Visualizzazione dei Risultati

forex-python-trading-algoritmico-006

In questo articolo descriviamo le ultime modifiche che abbiamo inserito nel sistema di trading sul mercato forex. In particolare abbiamo aggiunto alcune nuove funzionalità tra cui:

  • Documentazione : ora ho creato una sezione DTForex sul sito, che include tutti gli articoli della serie sul  trading algoritmo per il Forex e la documentazione per DTForex. In particolare, include istruzioni dettagliate per l’installazione e una guida all’uso sia per il backtesting che per il trading dal vivo.
  • Generazione di dati tick simulati – Dal momento che è difficile scaricare in blocco molti dati tick sul forex (o almeno per alcuni dei data provider che uso!) ho deciso che sarebbe stato più semplice generare semplicemente alcuni dati tick casuali per testare il sistema.
  • Backtest di più giorni – Una funzionalità essenziale per il sistema DTForex è la capacità di eseguire il backtest su più giorni di dati tick. L’ultima versione di DTForex supporta sia il backtesting di più giorni che quello di più coppie di valute, rendendolo sostanzialmente più utile.
  • Tracciare i risultati del backtesting – Sebbene l’output della console sia utile, niente batte la possibilità di visualizzare una curva di equity e un drawdown storico. Ho utilizzato la libreria Seaborn per tracciare i vari grafici delle prestazioni.

Script per Simulare i Dati di Tick

Una caratteristica estremamente importante per un sistema di trading è la capacità di eseguire un backtest su dati di tick storici che coprono un periodo di tempo di più giorni . In precedenza il sistema prevedeva solo il backtest tramite un singolo file. Questa non era una soluzione scalabile in quanto tale file deve essere caricato in memoria e poi strutturato in DataFrame di pandas . Sebbene i file di dati dei tick prodotti non siano enormi (circa 3,5 MB ciascuno), si sommano rapidamente se consideriamo più coppie di valute per periodi di mesi o anche più.

Per iniziare a creare una funzionalità per più giorni / più file, si inizia a scaricare più file dal feed tick storico di DukasCopy . Purtroppo ho avuto qualche problema e non sono riuscito a scaricare i file necessari per testare il sistema.

Dal momento che non è essenziale avere serie temporali storiche per testare il sistema, può essere più semplice scrivere uno script per generare automaticamente dei dati di tick simulati. Questo script è stato inserito nel file scripts/generate_simulated_pair.py. Il codice può essere visionato qui .

L’idea di base dello script è generare un elenco di timestamp distribuiti in modo casuale, ognuno dei quali possiede sia valori bid/ask che valori di volume. Lo spread tra l’offerta e la domanda è costante, mentre i valori bid / ask stessi sono generati come un random walk.

Dal momento che non si testerà mai alcuna strategia reale con questi dati, non c’è bisogno di preoccuparsi  delle sue proprietà statistiche o dei suoi valori assoluti in relazione alle coppie di valute forex reali. Finché si garantisce il corretto formato e una lunghezza approssimativa, si può usare  per testare il sistema di backtesting di più giorni.

Lo script è attualmente codificato per generare dati forex per l’intero mese di gennaio 2017. Utilizza la libreria Calendar di Python per considerare i giorni lavorativi (anche se non abbiamo ancora escluso le festività) e quindi genera una serie di file nel formato BBBQQQ_YYYYMMDD.csv, dove BBBQQQsarà una specifica coppia di valute specificata (es. EURUSD) ed YYYYMMDD è la data specificata (es 20170112.).

Questi file vengono inseriti nella directory  CSV_DATA_DIR, che è specificata file  settings.py  dell’applicazione.

Per generare i dati è necessario eseguire il seguente comando, dove BBBQQQdeve essere sostituito con lo specifico della valuta di interesse, es EURUSD:

python scripts/generate_simulated_pair.py BBBQQQ

Il file richiederà una modifica per generare dati per più mesi o anni. Ogni file di tick giornalieri ha una dimensione dell’ordine di 3,2 MB.

In futuro si modificherà questo script per generare dati per un periodo di più mesi o anni in base a uno specifico elenco di coppie di valute, anziché i valori codificati. Tuttavia, per il momento questo è sufficiente per iniziare.

Si tenga presente che il formato corrisponde esattamente a quello dei dati storici dei tick forniti da DukasCopy, che è il set di dati che stiamo attualmente utilizzando.

Implementazione di un Backtesting per più giorni

Successivamente alla generazione di dati tick simulati, si passa all’implementazione del backtesting per più giorni. Sebbene il piano a lungo termine prevede di utilizzare un sistema di archiviazione di dati storico più robusto come PyTables con HDF5 , per il momento si utilizza un set di file CSV, un file per ogni giorno per ogni coppia di valute.

Questa è una soluzione scalabile all’aumentare del numero di giorni. La natura event-driven del sistema richiede che siano presenti solo \(N\) file in memoria contemporaneamente, dove \(N\) è il numero di coppie di valute scambiate in un particolare giorno.

L’idea di base del sistema prevede che la classe HistoricCSVPriceHandler continui a utilizzare il metodo stream_next_tick, ma con una modifica per tenere conto di dati per più giorni caricando ogni giorno di dati in modo sequenziale.

L’implementazione prevede di terminare il backtest quando si riceve l’eccezione StopIteration generata dal metodo next(..) per 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 viene generato un  StopIteration, il codice verifica il risultato di self._update_csv_for_day(). Se il risultato è True il backtest continua (il self.cur_date_pairs, che potrebbe essere stato modificato nei dati dei giorni successivi). Se il risultato è False, il backtest termina.

Questo approccio è molto efficiente in termini di memoria poiché solo un limitato numero di giorni di dati è caricato in un punto qualsiasi. Significa che possiamo potenzialmente eseguire mesi di backtesting e siamo limitati solo dalla velocità di elaborazione della CPU e dalla quantità di dati che possiamo generare o acquisire.

Abbiamo quindi aggiornato la documentazione per riflettere il fatto che il sistema ora si aspetta più giorni di dati in un formato particolare, in una directory particolare che deve essere specificata.

Rappresentazione Grafica dei Risultati del Backtest tramite la libreria Seaborn

Un backtest è relativamente inutile se non siamo in grado di visualizzare le prestazioni della strategia nel tempo. Sebbene il sistema sia stato per lo più basato su console fino ad oggi, in questo articolo iniziamo ad introdurre le basi per un’interfaccia utente grafica (GUI).

In particolare, iniziamo con creare i soliti “tre pannelli” di grafici che spesso accompagnano le metriche di performance per i sistemi di trading quantitativo, vale a dire la curva equity, il profilo dei rendimenti e la curva dei drawdown. Tutti e tre vengono calcolati per ogni tick e vengono emessi in un file chiamato equity.csv nella directory specificata in  OUTPUT_RESULTS_DIR presente in settings.py.

Per visualizzare i dati utilizziamo una libreria chiamata Seaborn , che produce grafica di qualità elevata che ha un aspetto sostanzialmente migliore rispetto ai grafici predefiniti prodotti da Matplotlib. La grafica è molto simile a quella prodotta dal pacchetto ggplot2 di R. Inoltre Seaborn si basa effettivamente  su Matplotlib, quindi si puo ancora utilizzare l’API Matplotlib.

Per consentire la visualizzazione dei risultati abbiamo creato lo script output.py che risiede nella directory backtest/. Il codice dello 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 puoi vedere, lo script importa Seaborn e apre il file equity.csv in un DataFrame pandas, quindi crea semplicemente tre grafici, rispettivamente per la curva di equity, i rendimenti e il drawdown.

Nota che il grafico di drawdown stesso è effettivamente calcolato da una funzione di supporto che risiede performance/performance.py, che viene chiamata dalla classe Portfolio alla fine di un backtest.

Un esempio dell’output per la strategia MovingAverageCrossStrategy, per un set di dati di EURUSD generato casualmente per il mese di gennaio 2017, è il seguente: 

trading-algoritmico-forex-6-output

In particolare, è possibile vedere le sezioni piatte della curva azionaria nei fine settimana in cui non sono presenti dati (almeno, per questo set di dati simulato). Inoltre, la strategia perde denaro in modo piuttosto prevedibile su questo set di dati simulato in modo casuale.

Questo è un buon test del sistema. Stiamo semplicemente tentando di seguire una tendenza su una serie temporale generata casualmente. Le perdite si verificano a causa dello spread fisso introdotto nel processo di simulazione.

Ciò rende palese che se vogliamo realizzare un profitto consistente nel trading forex con frequenze più alte avremo bisogno di uno specifico vantaggio quantificabile che generi rendimenti positivi oltre i costi di transazione come spread e slippage.

Avremo molto altro da dire su questo punto estremamente importante nei prossimi articoli di questa serie sul trading algoritmico del Forex.

Prossimi Passi

Calcoli della posizione di fissaggio

Abbiamo notato che i calcoli effettuati dalla classe Position non rispecchiano esattamente il modo in cui OANDA (il broker utilizzato per il sistema trading.py) calcola i trade di cross valutari.

Quindi, uno dei passaggi successivi più importanti è eseguire e testare effettivamente le nuove modifiche al file position.py e aggiornare anche gli unit test implementati in position_test.py. Questo avrà un effetto a catena sui file portfolio.py portfolio_test.py.

Valutazione della prestazione

Sebbene ora disponiamo di un set di base di grafici delle prestazioni tramite la curva di equity, il profilo dei rendimenti e le serie dei drawdown, abbiamo bisogno di misure di performance più quantificate.

In particolare, avremo bisogno di metriche a livello di strategia, inclusi i comuni rapporti di rischio/rendimento come lo Sharpe Ratio, Information Ratio e Sortino Ratio. Avremo anche bisogno di statistiche sul drawdown inclusa la distribuzione dei drawdown, oltre a statistiche descrittive come il massimo drawdown. Altre metriche utili includono il tasso di crescita annuale composto (CAGR) e il rendimento totale.

A livello di trade/posizione vogliamo vedere metriche come il profitto/perdita medio, il profitto/perdita massimo, rapporto di profitto e rapporto di vincita / perdita. Dal momento che abbiamo costruito fin dall’inizio la classe Position come parte fondamentale del software, non dovrebbe essere troppo problematico generare queste metriche tramite alcuni metodi aggiuntivi.

 

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

Se si desidera leggere gli altri articoli di questa serie, sono disponibili ai seguenti link:

DTForex #5 – Trading su diverse Coppie di Valute

forex-python-trading-algoritmico-005

Nel precedente articolo della serie sul trading algoritmico per il forex abbiamo descritto  alcune importanti modifiche al software DTForex. Questi aggiornamento hanno aumentato in modo signicativo le funzionalità del sistema, al punto che è quasi pronto per il backtesting con dati storici di tick su una gamma di coppie di valute.

In questo articolo descriviamo le seguenti modifiche apportate al sistema:

  • Ulteriori modifiche agli oggetti Position Portfolio per consentire lo scambio di più coppie di valute e valute non denominate nella valuta del conto, cioè con un conto nominato in EUR si può ora negoziare anche GBP/USD, ad esempio.
  • Revisione completa delle modalità con cui Position Portfolio calcolano di apertura, chiusura, aggiunta e rimozione di unità. L’oggetto Position esegue ora la maggior parte della logica, lasciando all’oggetto Portfolio la gestione ad alto livello.
  • Aggiunta della prima strategia non banale, ovvero la ben nota strategia Moving Average Crossover con una coppia di medie mobili semplici (SMA).
  • Modifica di backtest.py per renderlo single-threaded e deterministico. Nonostante il mio ottimismo sul fatto che un approccio multi-thread non sarebbe troppo dannoso per l’accuratezza della simulazione, ho trovato difficile ottenere risultati soddisfacenti di backtest con un approccio multi-thread.
  • Introduzione di uno script molto semplice di output basato su Matplotlib per visualizzare la curva di equity.

Gestione di coppie di valute multiple

Una caratteristica del sistema di trading che abbiamo discusso molte volte negli articoli di questa serie è la possibilità capacità di gestire più coppie di valute.

In questa articolo vediamo come modificare il software per consentire la gestione di conti nominati in valute diverse da EUR, che era la sola valuta codificata in precedenza. Descriviamo anche come poter negoziare altre coppie di valute, ad eccezione di quelle che consistono in una base o quotazione in Yen giapponese (JPY). La limitazione sullo Yen è dovuta alle modalità di calcolo delle dimensioni dei tick nelle coppie di valute con JPY.

Per ottenere questo è necessario modificare la logica di calcolo del profitto quando le unità vengono rimosse o la posizione viene chiusa. Di seguito vediamo il nuovo codice per calcolo dei pips, nel file position.py:

def calculate_pips(self):
    mult = Decimal("1")
    if self.position_type == "long":
        mult = Decimal("1")
    elif self.position_type == "short":
        mult = Decimal("-1")
    pips = (mult * (self.cur_price - self.avg_price)).quantize(
        Decimal("0.00001"), ROUND_HALF_DOWN
    )
    return pips

Se chiudiamo la posizione per realizzare un guadagno o una perdita, dobbiamo utilizzare il seguente codice per close_position, da inserire nel file position.py:

def close_position(self):
    ticker_cp = self.ticker.prices[self.currency_pair]
    ticker_qh = self.ticker.prices[self.quote_home_currency_pair]
    if self.position_type == "long":
        remove_price = ticker_cp["ask"]
        qh_close = ticker_qh["bid"]
    else:
        remove_price = ticker_cp["bid"]
        qh_close = ticker_qh["ask"]
    self.update_position_price()
    # Calcolo dele PnL
    pnl = self.calculate_pips() * qh_close * self.units
    return pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN))

In primo luogo otteniamo i prezzi denaro e lettera sia per la coppia di valute negoziata che per la coppia di valute di base (“quote/home”). Ad esempio, per un conto denominato in EUR, dove stiamo negoziando GBP/USD, dobbiamo ottenere i prezzi per “USD/EUR”, poiché GBP è la valuta di base e USD è la quotazione.

In questa fase controlliamo se la posizione stessa è una posizione long o short e quindi calcoliamo il “prezzo di rimozione” per la coppia negoziata e il “prezzo di rimozione” per la coppia quote/home, che calcolati rispettivamente da remove_priceqh_close.

Quindi aggiorniamo i prezzi correnti e medi all’interno della posizione e infine calcoliamo il P&L moltiplicando i pip, il prezzo di rimozione per quote/home e quindi il numero di unità che stiamo chiudendo.

Abbiamo completamente eliminato la necessità di valutare la “esposizione”, che era una variabile ridondante. Questa formula fornisce quindi correttamente il P&L rispetto a qualsiasi scambio di coppie di valute (non denominate in JPY).

Revisione della posizione e gestione del portafoglio

Oltre alla possibilità di negoziare più coppie di valute, vediamo come perfezionare la logica in cui Position Portfolio “condividono” la responsabilità di aprire e chiudere le posizioni, nonché di aggiungere e sottrarre unità. In particolare, dobbiamo spostare molto del codice di gestione della posizione da portfolio.py a  position.py

Questo è più naturale poiché la posizione dovrebbe prendersi cura di se stessa e non delegarla al portafoglio!

In particolare, dobbiamo creare o migrare i metodi add_unitsremove_units close_position:


    def add_units(self, units):
        cp = self.ticker.prices[self.currency_pair]
        if self.position_type == "long":
            add_price = cp["ask"]
        else:
            add_price = cp["bid"]
        new_total_units = self.units + units
        new_total_cost = self.avg_price * self.units + add_price * units
        self.avg_price = new_total_cost / new_total_units
        self.units = new_total_units
        self.update_position_price()

    def remove_units(self, units):
        dec_units = Decimal(str(units))
        ticker_cp = self.ticker.prices[self.currency_pair]
        ticker_qh = self.ticker.prices[self.quote_home_currency_pair]
        if self.position_type == "long":
            remove_price = ticker_cp["ask"]
            qh_close = ticker_qh["bid"]
        else:
            remove_price = ticker_cp["bid"]
            qh_close = ticker_qh["ask"]
        self.units -= dec_units
        self.update_position_price()
        # Calcolo dele PnL
        pnl = self.calculate_pips() * qh_close * dec_units
        return pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN))

    def close_position(self):
        ticker_cp = self.ticker.prices[self.currency_pair]
        ticker_qh = self.ticker.prices[self.quote_home_currency_pair]
        if self.position_type == "long":
            remove_price = ticker_cp["ask"]
            qh_close = ticker_qh["bid"]
        else:
            remove_price = ticker_cp["bid"]
            qh_close = ticker_qh["ask"]
        self.update_position_price()
        # Calcolo dele PnL
        pnl = self.calculate_pips() * qh_close * self.units
        return pnl.quantize(Decimal("0.01", ROUND_HALF_DOWN))
Negli ultimi due metodi si può vedere come è implementata la nuova formula per il calcolo del profitto. Di conseguenza, molte delle funzionalità della classe Portfolio sono state ridotte. In particolare, i metodi add_new_positionadd_position_unitsremove_position_unitsclose_position sono stati modificati a seguito dello spostamento del calcolo all’interno dell’oggetto Position:
def add_new_position(self, position_type, currency_pair, units, ticker):
    ps = Position(
        self.home_currency, position_type, 
        currency_pair, units, ticker
    )
    self.positions[currency_pair] = ps

def add_position_units(self, currency_pair, units):
    if currency_pair not in self.positions:
        return False
    else:
        ps = self.positions[currency_pair]
        ps.add_units(units)
        return True

def remove_position_units(self, currency_pair, units):
    if currency_pair not in self.positions:
        return False
    else:
        ps = self.positions[currency_pair]
        pnl = ps.remove_units(units)
        self.balance += pnl
        return True

def close_position(self, currency_pair):
    if currency_pair not in self.positions:
        return False
    else:
        ps = self.positions[currency_pair]
        pnl = ps.close_position()
        self.balance += pnl
        del[self.positions[currency_pair]]
        return True

In sostanza, tutti  i metodi (a parte add_new_position) controllano semplicemente se la posizione esiste per quella coppia di valute e quindi chiamano il corrispondente metodo in Position, tenendo conto del profitto se necessario.

Strategia di crossover della media mobile

In DataTrading abbiamo già descritto una strategia Moving Average Crossover, nel contesto del mercato azionario. È una strategia utile come banco di prova del sistema perché i calcoli sono facile da replicare, anche a mano (almeno a frequenze più basse!), al fine di verificare che il backtester si stia comportando come dovrebbe.

L’idea di base della strategia è la seguente:

  • Vengono creati due filtri separati di media mobile semplici, con periodi variabili della finestra, di una particolare serie temporale.
  • I segnali di acquisto dell’asset si verificano quando la media mobile più breve supera la media mobile più lunga.
  • Se la media più lunga successivamente supera la media più breve, l’asset viene venduto.

La strategia funziona bene quando una serie temporale entra in un periodo di forte tendenza e poi lentamente inverte la tendenza.

L’implementazione è semplice. In primo luogo, implementiamo un metodo calc_rolling_sma che ci consente di utilizzare in modo più efficiente il calcolo SMA del periodo di tempo precedente per generare quello nuovo, senza dover ricalcolare completamente l’SMA in ogni fase.

In secondo luogo, generiamo segnali in due casi. Nel primo caso generiamo un segnale se la SMA breve supera la SMA lunga e non siamo long nella coppia di valute. Nel secondo caso generiamo un segnale se la SMA lunga supera la SMA breve e siamo già long nello strumento.

In questo esempio abbiamo impostato il periodo della finestra a 500 tick per la SMA breve e 2.000 tick per la SMA lunga. Ovviamente in un ambiente di produzione questi parametri devono essere ottimizzati, ma funzionano bene per i nostri scopi di test.

class MovingAverageCrossStrategy(object):
    """
    Una strategia base di Moving Average Crossover che genera
    due medie mobili semplici (SMA), con finestre predefinite
    di 500 tick per la SMA  breve e 2.000 tick per la SMA
    lunga.

    La strategia è "solo long" nel senso che aprirà solo una
    posizione long una volta che la SMA breve supera la SMA
    lunga. Chiuderà la posizione (prendendo un corrispondente
    ordine di vendita) quando la SMA lunga incrocia nuovamente 
    la SMA breve.

    La strategia utilizza un calcolo SMA a rotazione per
    aumentare l'efficienza eliminando la necessità di chiamare due
    calcoli della media mobile completa su ogni tick.
    """

    def __init__(
            self, pairs, events,
            short_window=500, long_window=2000
    ):
        self.pairs = pairs
        self.events = events
        self.ticks = 0
        self.invested = False

        self.short_window = short_window
        self.long_window = long_window
        self.short_sma = None
        self.long_sma = None

    def calc_rolling_sma(self, sma_m_1, window, price):
        return ((sma_m_1 * (window - 1)) + price) / window

    def calculate_signals(self, event):
        if event.type == 'TICK':
            price = event.bid
            if self.ticks == 0:
                self.short_sma = price
                self.long_sma = price
            else:
                self.short_sma = self.calc_rolling_sma(
                    self.short_sma, self.short_window, price
                )
                self.long_sma = self.calc_rolling_sma(
                    self.long_sma, self.long_window, price
                )
            # Si avvia la strategia solamente dopo aver creato una accurata 
            # finestra di breve periodo
            if self.ticks > self.short_window:
                if self.short_sma > self.long_sma and not self.invested:
                    signal = SignalEvent(self.pairs[0], "market", "buy", event.time)
                    self.events.put(signal)
                    self.invested = True
                if self.short_sma < self.long_sma and self.invested:
                    signal = SignalEvent(self.pairs[0], "market", "sell", event.time)
                    self.events.put(signal)
                    self.invested = False
            self.ticks += 1

Backtester a thread singolo

Un altro cambiamento importante è modificare il componente del backtest in modo da essere a singolo thread, anziché multi-thread.

Dobbiamo prevede questa modifica perché è molto complesso sincronizzare i thread da eseguire in un modo simile a quello che si avrebbe nel trading live senza introdurre errori e bias che comprometto i risultati del backtest. In particolare, con un backtester multi-thread si hanno i prezzi di entrata e di uscita molto irrealistici, perchè si verificano tipicamente dopo alcune ore (virtuali) l’effettiva ricezione del tick.

Per evitare questa criticità è sufficiente incorporare lo streaming dell’oggetto TickEventnel ciclo di backtest, come implementato nel seguente frammento di backtest.py:

def backtest(events, ticker, strategy, portfolio,
        execution, heartbeat, max_iters=200000
    ):
    """
    Esegue un ciclo while infinito che esegue il polling
    della coda degli eventi e indirizza ogni evento al
    componente della strategia del gestore di esecuzione.
    Il ciclo si fermerà quindi per "heartbeat" secondi
    e continuerà fino a quando si supera il numero massimo 
    di iterazioni.
    """
    iters = 0
    while True and iters < max_iters:
        ticker.stream_next_tick()
        try:
            event = events.get(False)
        except queue.Empty:
            pass
        else:
            if event is not None:
                if event.type == 'TICK':
                    strategy.calculate_signals(event)
                elif event.type == 'SIGNAL':
                    portfolio.execute_signal(event)
                elif event.type == 'ORDER':
                    execution.execute_order(event)
        time.sleep(heartbeat)
        iters += 1
    portfolio.output_results()

Da notare la linea ticker.stream_next_tick(). Questo metodo viene chiamato prima di un polling della coda degli eventi e quindi ci assicuriamo che un nuovo evento tick venga elaborato prima che la coda venga nuovamente interrogata.

In questo modo un segnale è eseguito all’arrivo di nuovi dati di mercato, anche se c’è un certo ritardo nel processo di esecuzione degli ordini a causa dello slippage.

Abbiamo anche impostato un valore max_iters che controlla per quanto tempo continua il ciclo di backtest. In pratica questo dovrà essere abbastanza grande quando si tratta di più valute in più giorni. In questo caso è stato impostato su un valore predefinito che consente di elaborare i dati di un singolo giorno di una coppia di valute.

Il metodo stream_next_tick della classe del price handler è simile a, stream_to_queue tranne per il fatto che chiama manualmente il metodo iterativo next(), invece di eseguire il tick streaming in un ciclo for:

    def stream_next_tick(self):
        """
        Il Backtester è ora passato ad un modello a un thread singolo
        in modo da riprodurre completamente i risultati su ogni esecuzione.
        Ciò significa che il metodo stream_to_queue non può essere usato 
        ed è sostituito dal metodo stream_next_tick.
        
        Questo metodo viene chiamato dalla funzione di backtesting, esterna
        a questa classe e inserisce un solo tick nella coda, ed inoltre
        aggiornare l'attuale bid / ask e l'inverso bid / ask.
        """
        try:
            index, row = self.all_pairs.next()
        except StopIteration:
            return
        else:
            self.prices[row["Pair"]]["bid"] = Decimal(str(row["Bid"])).quantize(
                Decimal("0.00001", ROUND_HALF_DOWN)
            )
            self.prices[row["Pair"]]["ask"] = Decimal(str(row["Ask"])).quantize(
                Decimal("0.00001", ROUND_HALF_DOWN)
            )
            self.prices[row["Pair"]]["time"] = index
            inv_pair, inv_bid, inv_ask = self.invert_prices(row)
            self.prices[inv_pair]["bid"] = inv_bid
            self.prices[inv_pair]["ask"] = inv_ask
            self.prices[inv_pair]["time"] = index
            tev = TickEvent(row["Pair"], index, row["Bid"], row["Ask"])
            self.events_queue.put(tev)

Da notare che si interrompe al ricevimento di un’eccezione StopIteration. Ciò consente al codice di riprendere l’esecuzione anziché bloccarsi.

Visualizzazione risultati con Matplotlib

Dobbiamo anche creare uno script di output, utilizzando Matplotlib in modo molto semplice per visualizzare la curva di equity. Il file output.py è inserito all’interno della directory backtest di DTForex ed il codice è riportato di seguito:.

import os, os.path

import pandas as pd
import matplotlib.pyplot as plt

from settings import OUTPUT_RESULTS_DIR


if __name__ == "__main__":
    """
    Un semplice script per visualizzare il grafico del bilancio del portfolio, o
    "curva di equity", in funzione del tempo.

    Richiede l'impostazione di OUTPUT_RESULTS_DIR nel settings del progetto.
    """
    equity_file = os.path.join(OUTPUT_RESULTS_DIR, "equity.csv")
    equity = pd.io.parsers.read_csv(
        equity_file, header=True, 
        names=["time", "balance"], 
        parse_dates=True, index_col=0
    )
    equity["balance"].plot()
    plt.show()

Da notare che  settings.py deve ora prevedere la nuova variabile OUTPUT_RESULTS_DIR, che deve essere presente e valorizzata nelle impostazioni. In questo esempio abbiamo impostato a una directory temporanea fuori dalla struttura del progetto in modo da non aggiungere accidentalmente nessun risultato di backtest al codice base del progetto!

La curva di equity è costruita  aggiungendo un valore di portafoglio (“balance”) a una lista di dizionari, con un dizionario corrispondente a una marca temporale.

Una volta completato il backtest, l’elenco dei dizionari viene convertito in un DataFrame di pandas e il metodo to_csv viene utilizzato per l’output equity.csv.

Questo script di output legge semplicemente il file e visualizza il grafico della colonna balance del DataFrame.

Di seguito il codice per i metodi append_equity_row output_results della classe Portfolio:

    def append_equity_row(self, time, balance):
        d = {"time": time, "balance": balance}
        self.equity.append(d)

    def output_results(self):
        filename = "equity.csv"
        out_file = os.path.join(OUTPUT_RESULTS_DIR, filename)
        df_equity = pd.DataFrame.from_records(self.equity, index='time')
        df_equity.to_csv(out_file)
        print
        "Simulation complete and results exported to %s" % filename

Ogni volta che viene chiamato execute_signal, si richiamata il metodo precedente e si aggiunge il valore di timestamp / saldo al membro equity.

Alla fine del backtest viene chiamato output_results che semplicemente converte l’elenco dei dizionari in un DataFrame e quindi l’output nella directory specificata in OUTPUT_RESULTS_DIR.

Sfortunatamente, questo non è un modo particolarmente appropriato per creare una curva di equity poiché si verifica solo quando viene generato un segnale. Ciò significa che non tiene conto del P&L non realizzato .

Anche se questo è il modo in cui avviene realmente il trading (non si fa effettivamente un profitto/perdita  fino a quando non si chiude una posizione!), Significa che la curva dell’equità rimarrà completamente piatta tra gli aggiornamenti del saldo del portafoglio. Peggio ancora, Matplotlib per impostazione predefinita esegue l’interpolazione lineare tra questi punti, fornendo così la falsa impressione del P&L non realizzato.

La soluzione a questo problema è creare un tracker P&L non realizzato per la classe Position che si aggiorna correttamente ad ogni tick. Questo è un po ‘più costoso dal punto di vista computazionale, ma consente una curva di equity più utile e realistica. Descriveremo questa funzione in un prossimo articolo!

Prossimi Passi

La prossima funzionalità da prevedere per DTForex è la possibilità di effettuare il backtesting con dati relativi a molti giorni. Attualmente l’oggetto HistoricCSVPriceHandler carica solo il valore di un singolo giorno di dati tick DukasCopy per qualsiasi coppia di valute specificata.

Per consentire backtest per periodi che coprono più giorni, sarà necessario caricare e trasmettere sequenzialmente un singolo giorno in modo da evitare di riempire la RAM con l’intera cronologia dei dati dei tick. Ciò richiederà una modifica al funzionamento del metodo stream_next_tick. Una volta completato, consentirà il backtesting della strategia a lungo termine su più coppie.

Un altro compito è migliorare l’output della curva di equity Per calcolare una qualsiasi delle normali metriche di performance (come lo Sharpe Ratio ), avremo bisogno di calcolare i rendimenti percentuali in un determinato periodo di tempo. Tuttavia, ciò richiede di raggruppare i dati del tick in barre per calcolare un rendimento di periodo di tempo.

Tale binning deve avvenire su una frequenza di campionamento che è simile alla frequenza di negoziazione o lo Sharpe Ratio non rifletterà il vero rischio / rendimento della strategia. Questo raggruppamento non è un esercizio banale in quanto ci sono molti presupposti che contribuiscono a generare un “prezzo” per ogni campione.

Una volta completate queste due attività e acquisiti dati sufficienti, saremo in grado di eseguire il backtest di un’ampia gamma di strategie forex basate sui dati tick e di produrre curve azionarie al netto della maggior parte dei costi di transazione. Inoltre, sarà estremamente semplice testare queste strategie sul conto di paper trading fornito da OANDA.

Ciò dovrebbe consentire di prendere decisioni più precise sull’opportunità di eseguire una strategia, rispetto ai test effettuati con un sistema di backtesting più “orientato alla ricerca”.

 

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

Se si desidera leggere gli altri articoli di questa serie, sono disponibili ai seguenti link:

DTForex #4 – Aggiunta del motore di Backtesting

forex-python-trading-algoritmico-004

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 PortfolioIn 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:

from decimal import Decimal, getcontext, ROUND_HALF_DOWN


class Position(object):
    def __init__(
        self, position_type, market, 
        units, exposure, bid, ask
    ):
        self.position_type = position_type  # Long or short
        self.market = market
        self.units = units
        self.exposure = Decimal(str(exposure))

        # Long or short
        if self.position_type == "long":
            self.avg_price = Decimal(str(ask))
            self.cur_price = Decimal(str(bid))
        else:
            self.avg_price = Decimal(str(bid))
            self.cur_price = Decimal(str(ask))

        self.profit_base = self.calculate_profit_base(self.exposure)
        self.profit_perc = self.calculate_profit_perc(self.exposure)

    def calculate_pips(self):
        getcontext.prec = 6
        mult = Decimal("1")
        if self.position_type == "long":
            mult = Decimal("1")
        elif self.position_type == "short":
            mult = Decimal("-1")
        return (mult * (self.cur_price - self.avg_price)).quantize(
            Decimal("0.00001"), ROUND_HALF_DOWN
        )

    def calculate_profit_base(self, exposure):
        pips = self.calculate_pips()        
        return (pips * exposure / self.cur_price).quantize(
            Decimal("0.00001"), ROUND_HALF_DOWN
        )

    def calculate_profit_perc(self, exposure):
        return (self.profit_base / exposure * Decimal("100.00")).quantize(
            Decimal("0.00001"), ROUND_HALF_DOWN
        )

    def update_position_price(self, bid, ask, exposure):
        if self.position_type == "long":
            self.cur_price = Decimal(str(bid))
        else:
            self.cur_price = Decimal(str(ask))
        self.profit_base = self.calculate_profit_base(exposure)
        self.profit_perc = self.calculate_profit_perc(exposure)

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:

from abc import ABCMeta, abstractmethod

..
..

class PriceHandler(object):
    """
    PriceHandler è una classe base astratta che fornisce un'interfaccia per
    tutti i successivi gestori di dati (ereditati) (sia live che storici).

    L'obiettivo di un oggetto PriceHandler (derivato) è produrre un insieme di
    bid / ask / timestamp "tick" per ogni coppia di valute e inserirli
    una coda di eventi.

    Questo replicherà il modo in cui una strategia live funzionerebbe con i dati
    dei tick che sarebbero trasmessi in streaming tramite un broker.
    """

    __metaclass__ = ABCMeta

    @abstractmethod
    def stream_to_queue(self):
        """
        Trasmette una sequenza di eventi di dati tick (timestamp, bid, ask)
        come tuple alla coda degli eventi.
        """
        raise NotImplementedError("Should implement stream_to_queue()")

Abbiamo bisogno inoltre di  una sottoclasse chiamata HistoricCSVPriceHandler, che preveda due metodi.

Il primo è chiamato _open_convert_csv_filese 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:

class HistoricCSVPriceHandler(PriceHandler):
    """
    HistoricCSVPriceHandler è progettato per leggere un file CSV di
    dati tick per ciascuna coppia di valute richiesta e trasmetterli in streaming
    alla coda degli eventi.
    """

    def __init__(self, pairs, events_queue, csv_dir):
        """
        Inizializza il gestore dati storici richiedendo
        la posizione dei file CSV e un elenco di simboli.

        Si presume che tutti i file siano nella forma
        'pair.csv', dove " pair " è la coppia di valute. Per
        EUR/USD il nome del file è EURUSD.csv.

        Parametri:
        pairs - L'elenco delle coppie di valute da ottenere.
        events_queue - La coda degli eventi a cui inviare i tick.
        csv_dir: percorso di directory assoluto per i file CSV.
        """
        self.pairs = pairs
        self.events_queue = events_queue
        self.csv_dir = csv_dir
        self.cur_bid = None
        self.cur_ask = None

    def _open_convert_csv_files(self):
        """
        Apre i file CSV dalla directory su disco, converte i dati
        in un DataFrame di pandas con un dizionario di coppie.
        """
        pair_path = os.path.join(self.csv_dir, '%s.csv' % self.pairs[0])
        self.pair = pd.io.parsers.read_csv(
            pair_path, header=True, index_col=0, parse_dates=True,
            names=("Time", "Ask", "Bid", "AskVolume", "BidVolume")
        ).iterrows()

    def stream_to_queue(self):
        self._open_convert_csv_files()
        for index, row in self.pair:
            self.cur_bid = Decimal(str(row["Bid"])).quantize(
                Decimal("0.00001", ROUND_HALF_DOWN)
            )
            self.cur_ask = Decimal(str(row["Ask"])).quantize(
                Decimal("0.00001", ROUND_HALF_DOWN)
            )
            tev = TickEvent(self.pairs[0], index, row["Bid"], row["Ask"])
            self.events_queue.put(tev)

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 HistoricPriceCSVHandlerSimulatedExecution al posto delle classi StreamingPriceHandlerOANDAExecutionHandler. Tutto il resto rimane lo stesso.

Di seguito il codice di backtest.py:

import copy, sys
import queue
import threading
import time
from decimal import Decimal, getcontext

from execution import SimulatedExecution
from portfolio import Portfolio
from settings import settings
from strategy import TestStrategy
from data.price import HistoricCSVPriceHandler


def trade(events, strategy, portfolio, execution, heartbeat):
    """
    Esegue un ciclo while infinito che esegue il polling 
    della coda degli eventi e indirizza ogni evento al 
    componente della strategia del gestore di esecuzione. 
    Il ciclo si fermerà quindi per "heartbeat" secondi 
    e continuerà.
    """
    while True:
        try:
            event = events.get(False)
        except queue.Empty:
            pass
        else:
            if event is not None:
                if event.type == 'TICK':
                    strategy.calculate_signals(event)
                elif event.type == 'SIGNAL':
                    portfolio.execute_signal(event)
                elif event.type == 'ORDER':
                    execution.execute_order(event)
        time.sleep(heartbeat)


if __name__ == "__main__":
    # Imposta il numero di decimali a 2
    getcontext().prec = 2

    heartbeat = 0.0  # mezzo secondo tra ogni polling
    events = queue.Queue()
    equity = settings.EQUITY

    # Carica il file CSV dei dati storici
    pairs = ["EURUSD"]
    csv_dir = settings.CSV_DATA_DIR
    if csv_dir is None:
        print("No historic data directory provided - backtest terminating.")
        sys.exit()

    # Crea la classe di streaming dei dati storici di tick
    prices = HistoricCSVPriceHandler(pairs, events, csv_dir)

    # Crea il generatore della strategia/signale, passando lo 
    # strumento e la coda degli eventi
    strategy = TestStrategy(pairs[0], events)

    # Crea l'oggetto portfolio per tracciare i trade
    portfolio = Portfolio(prices, events, equity=equity)

    # Crea il gestore di esecuzione simulato
    execution = SimulatedExecution()

    # Crea due thread separati: uno per il ciclo di trading
    # e un'altro per la classe di streaming dei prezzi di mercato
    trade_thread = threading.Thread(
        target=trade, args=(
            events, strategy, portfolio, execution, heartbeat
        )
    )
    price_thread = threading.Thread(target=prices.stream_to_queue, args=[])

    # Avvia entrambi i thread
    trade_thread.start()
    price_thread.start()

L’utilizzo di un sistema di esecuzione multi-thread per il backtest ha il principale svantaggio di non essere deterministicoCiò 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

Se si desidera leggere gli altri articoli di questa serie, sono disponibili ai seguenti link:

DTForex #3 – Open Sourcing del Sistema di Trading sul Forex

forex-python-trading-algoritmico-003

In questo articolo della serie sul trading Forex descriviamo il piano a lungo termine per il sistema di trading forex. Inoltre, approfondiamo l’uso del tipo di dati Decimal di Python per rendere i calcoli più accurati.

Ad oggi, abbiamo sperimentato l’ API Rest di OANDA per verificarne il confronto con l’API fornita da Interactive Brokers. Abbiamo anche visto come aggiungere un elemento base di replica del portafoglio come primo passo verso un adeguato sistema di backtesting basato sugli eventi. Ho anche ricevuto alcune email con suggerimenti relativi agli articoli precedenti ( n. 1 e n. 2 ), il che suggerisce che molti di voi desiderano modificare ed estendere il codice da soli.

Codice Open Source per il Sistema di Trading sul Forex

Per i motivi sopra esposti ho deciso di rendere open source il sistema di trading sul forex. Cosa significa questo? Significa che tutto il codice attuale e futuro sarà disponibile gratuitamente, con una licenza open source MIT , sul sito Web di Github al seguente repository: datatrading-info/DTForex.

Per chi ha familiarità con git e Github, è sicuramente in grado di eseguire il git clone del repository ed iniziare a modificarlo per i propri scopi.

Il sistema di trading automatico sul Forex di DataTrading è ora open source con una licenza MIT. Puoi trovare il codice più recente su Github nel repository DTForex su datatrading-info/DTForex.

Per chi non conosce il controllo di versione del codice sorgente, è sicuramente utile leggere come funziona git (e il controllo della versione in generale) con questo ebook gratuito Pro Git . Vale la pena dedicare un po’ di tempo per capire il controllo del codice sorgente in quanto farà risparmiare un’enorme quantità di problemi futuri se si trascorre tempo a programmare e ad aggiornare i progetti!

In Ubuntu si può installare git in modo rapido con il seguente comando:

sudo apt-get install git-core

Si crea una directory per il progetto DTForex e “clonare” il repository dal sito Github, come segue:

mkdir -p ~/progetti/ 
cd ~/progetti/ 
git clone 
https://github.com/datatrading-info/DTForex.git

A questo punto si deve creare un ambiente virtuale in cui eseguire il codice:

mkdir -p ~/venv/dtforex
cd ~/venv/dtforex
virtualenv .
source ~/venv/dtforex/bin/activate

Successivamente è necessario installare le librerie python richieste dal progetto (ci vorrà qualche minuto!):

pip install -r ~/progetti/dtforex/requirements.txt

Come accennato negli articoli precedenti, si deve inoltre creare le variabili di ambiente necessarie per le credenziali di autenticazione OANDA. Si prega di consultare l’articolo DTForex #2 per le istruzioni su come farlo.

Si prega di prestare attenzione al README associato al repository, poiché contiene le istruzioni di installazione, un disclaimer e una garanzia sull’utilizzo del codice.

Poiché il software è in modalità “alpha”, queste istruzioni diventeranno più semplici con il passare del tempo. In particolare cercherò di includere il progetto in un pacchetto Python in modo che possa essere facilmente installato tramite pip.

In caso di domande sulla procedura di installazione, non esitare a scrivermi a [email protected] .

Piano a lungo termine

La “filosofia” del sistema di trading per il forex, come per tutti i progetti del sito DataTrading, è costruire un motore di backtesting che possa imitare il più possibile il trading reale. Ciò significa includere dettagli che sono spesso esclusi da scenari di backtesting più “orientate alla ricerca”. Latenza, interruzioni del server, automazione, monitoraggio e costi di transazione realistici saranno tutti inclusi nei modelli di DataTrading per darci una buona idea della profittabilità di una strategia.

Dato che abbiamo ai dati del tick (timestamp bid/ask) possiamo incorporare lo spread nei costi di transazione. Possiamo anche modellare lo slippage. È invece meno immediato modellare l’impatto del mercato, sebbene ciò sia meno preoccupante per trade con piccoli importi (come per i trader retail) o quando si opera su strumenti molti liquidi.

Oltre ai costi di transazione, vogliamo modellare una solida gestione del portafoglio utilizzando la gestione del rischio e dimensionamento della posizione.

Quindi, cosa è attualmente incluso nel nostro Forex Trading System fino ad oggi?

  • Architettura guidata dagli eventi – Il sistema di trading forex è stato progettato da zero come un sistema guidato dagli eventi, poiché è così che un sistema di trading intraday verrà implementato in un ambiente live.
  • Streaming dei prezzi : abbiamo un oggetto base di streaming dei prezzi di mercato. Questo attualmente gestisce lo streaming di una sola coppia, ma possiamo facilmente modificarlo per gestire più coppie di valute.
  • Generazione del segnale – Possiamo incorporare strategie di trading (basate direttamente sui prezzi tick passati e attuali) utilizzando l’oggetto Strategy, che crea oggetto SignalEvent.
  • Esecuzione degli ordini : disponiamo di un ingenuo sistema di esecuzione degli ordini che invia gli ordini alla cieca da Portfolio averso OANDA. Con “alla cieca” si intende che non viene effettuata alcuna gestione del rischio o dimensionamento della posizione, né alcuna esecuzione algoritmica che potrebbe portare a costi di transazione ridotti.
  • Valuta di base EUR – Per semplificare le cose, abbiamo implementato il sistema solo per la valuta di base EUR. Questo è forse l’aspetto più importante da modificare visto che il mercato forex offre strumenti  denominati in USD, GBP, CAD, JPY, AUD e NZD!
  • Trading EUR / USD – Abbiamo scelto EUR/USD come coppia di valute iniziale con cui testare gli oggetti Position Portfolio. La gestione di più coppie di valute è un importante passaggio successivo. Ciò comporterà modifiche alla posizione e ai calcoli del portafoglio.
  • Gestione dei decimali : qualsiasi sistema di trading live deve gestire correttamente i calcoli di valuta. In particolare, i valori di valuta non dovrebbero essere memorizzati come tipi di dati in virgola mobile, poiché gli errori di arrotondamento si accumulano. Per maggiori dettagli si può consultare questo fantastico articolo sulle rappresentazioni in virgola mobile.
  • Trading Long / Short – Tra l’articolo #2 della serie e l’articolo #3 abbiamo aggiunto la possibilità di shortare una coppia di valute (invece di poter solo andare long). Fondamentalmente, anche questo è oggetto di unit test.
  • Gestione del portafoglio locale – A mio parere, eseguire un backtest che gonfia le prestazioni della strategia a causa di ipotesi non realistiche è inutile nella migliore delle ipotesi ed estremamente poco redditizio nel peggiore dei casi! L’introduzione di un oggetto di portafoglio locale che replica i calcoli di OANDA significa che possiamo controllare i nostri calcoli interni mentre eseguiamo il paper trading, il che ci dà maggiore fiducia quando in seguito utilizzeremo lo stesso oggetto di portafoglio per il backtest sui dati storici.
  • Unit test per Position/Portfolio – Anche se è stato direttamente menzionato direttamente negli articoli #1 e #2, ho effettivamente scritto alcuni unit test per gli oggetti Portfolio Position. Poiché questi sono così cruciali per i calcoli della strategia, bisogna essere estremamente sicuri che funzionino come previsto. Un ulteriore vantaggio di tali test è la possibilità di modificare il calcolo sottostante, in modo tale che se tutti i test continuano ad essere superati, possiamo essere certi che il sistema continuerà a comportarsi come previsto.

In questa fase il Forex Trading System manca delle seguenti funzionalità:

  • Gestione dello slippage – Il sistema sta attualmente generando molti slippage a causa della natura ad alta frequenza dei dati tick forniti da OANDA. Ciò significa che il saldo del portafoglio calcolato localmente non riflette il saldo calcolato da OANDA. Fino a quando non viene eseguita la corretta gestione degli eventi e la regolazione dello slippage, il backtest non rifletterà correttamente la realtà.
  • Valute di base multiple – Attualmente siamo limitati a EUR. Per lo meno dobbiamo includere le principali denominazioni di valuta: USD, GBP, CAD, AUD, JPY e NZD.
  • Coppie di valute multiple – Allo stesso modo dobbiamo supportare le principali coppie di valute oltre EUR / USD). Ci sono due aspetti da considerare. Il primo è gestire correttamente i calcoli quando né la base né la quotazione di una coppia di valute è uguale alla valuta di denominazione del conto. Il secondo aspetto è supportare più posizioni in modo da poter negoziare un portafoglio di coppie di valute.
  • Gestione del rischio – Molti backtest di “ricerca” ignorano completamente la gestione del rischio. Purtroppo questo è generalmente necessario per brevità nel descrivere le regole di una strategia. In realtà dobbiamo prevedere una gestione del rischio durante il trading, altrimenti è estremamente probabile che prima o poi subiremo una pesante perdita. Questo non vuol dire che la gestione del rischio possa prevenirla del tutto, ma certamente la rende meno probabile!
  • Ottimizzazione del portafoglio – In un contesto istituzionale avremo un mandato di investimento, che determinerà un solido sistema di gestione del portafoglio con varie regole di allocazione. In un contesto di trading retail / personale potremmo voler utilizzare un approccio di dimensionamento della posizione come il criterio di Kelly per massimizzare il nostro tasso di crescita composto nel lungo termine.
  • Strategie robuste – Finora abbiamo dimostrato solo alcune semplici strategie “giocattolo” che generano segnali casuali. Ora che stiamo iniziando a creare un sistema di trading forex intraday affidabile, dovremmo iniziare a mettere in atto alcune strategie più interessanti. I prossimi articoli di questa serie si concentreranno su strategie tratte da una combinazione di indicatori / filtri “tecnici”, modelli di serie temporali e tecniche di apprendimento automatico.
  • Distribuzione remota : Dato che siamo potenzialmente interessati al trading 24 ore su 24 (almeno durante la settimana!), Abbiamo bisogno di una configurazione più sofisticata rispetto all’esecuzione del backtest su un computer desktop / laptop locale a casa. È fondamentale creare una solida distribuzione del nostro sistema in un server remoto, con specifici tool di ridondanza e monitoraggio.
  • Backtest storico : Abbiamo costruito l’oggetto Portfolio per poter eseguire un backtest realistico. In questa fase ci manca un sistema di archiviazione dei dati storici dei tick. Negli articoli successivi esamineremo come ottenere i dati storici dei tick e archiviarli in un database appropriato, come HDF5 .
  • Database dei Trade – Inoltre è opportuno memorizzare i nostri trade in tempo reale in un database. Ciò ci consentirà di eseguire le nostre analisi sui dati di live trading. Una buona raccomandazione per un database relazionale è PostgreSQL o MySQL .
  • Monitoraggio e alta disponibilità : Dato che stiamo costruendo un sistema intraday ad alta frequenza, dobbiamo mettere in atto un monitoraggio completo e una ridondanza ad alta disponibilità. Ciò significa generare rapporti sull’utilizzo della CPU, utilizzo del disco, I/O di rete, latenza e verificare che gli script periodici siano impostati per continuare a funzionare. Inoltre abbiamo bisogno di una strategia di backup e ripristino. Chiediti quali piani di backup avresti in atto se avessi grandi posizioni aperte, in un mercato volatile e il tuo server morisse improvvisamente. Credimi, succede!
  • Integrazione Multiple Broker / FIX – Al momento siamo fortemente legati al broker OANDA. Come ho detto in precedenza, perché semplicemente mi sono imbattuto nella loro API e ho trovato che fosse un’ottima infrastruttura per i test. Ci sono molti altri broker là fuori, molti dei quali supportano il protocollo FIX. L’aggiunta di una funzionalità FIX aumenterebbe il numero di broker che potrebbero essere utilizzati nel sistema.
  • Controllo e reportistica GUI – In questo momento il sistema è completamente basato su console / riga di comando. Per lo meno avremo bisogno di alcuni grafici di base per visualizzare i risultati del backtest. Un sistema più sofisticato incorporerà statistiche riassuntive delle operazioni, metriche delle prestazioni a livello di strategia e prestazioni complessive del portafoglio. Questa GUI potrebbe essere implementata utilizzando un sistema di finestre multipiattaforma come Qt o Tkinter . Potrebbe anche essere implementato un front-end basato sul web, utilizzando un framework web come Django .

Come si può vedere, sono rimaste molte funzionalità sulla roadmap! Detto questo, ogni nuovo articolo della serie farà avanzare il progetto.

Tipi di dati decimali

Dopo aver discusso il piano a lungo termine, descriviamo alcune delle modifiche apportate al codice presentato nell’articolo #2 di questa serie. In particolare, descriviamo le modifiche al codice necessarie per gestire il tipo di dati Decimal invece di utilizzare le variabili a virgola mobile. Si tratta di un cambiamento estremamente importante poiché le rappresentazioni in virgola mobile sono una primaria fonte di errori a lungo termine nei sistemi di gestione del portafoglio e degli ordini.

Python supporta nativamente le rappresentazioni decimali con una precisione arbitraria. La funzionalità è contenuta nella libreria decimal.

In particolare abbiamo bisogno di modificare -ogni- valore che appare nei calcoli implementati in Position in un tipo di dato Decimal. Ciò include le unità, l’esposizione, i pip, il profitto e il profitto percentuale. Ciò garantisce che abbiamo il pieno controllo di come vengono gestiti i problemi di arrotondamento quando si tratta di rappresentazioni di valute con una precisione a due cifre decimali. In particolare dobbiamo scegliere il metodo di arrotondamento. Python supporta alcuni tipi diversi, in questo caso usiamo ROUND_HALF_DOWN, che arrotonda all’intero più vicino con legami che vanno verso lo zero.

Ecco un esempio delle modifiche al codice per gestire i tipi di dati Decimal rispetto alle loro precedenti rappresentazioni in virgola mobile. Di seguito è riportato il codice di position.py:

from decimal import Decimal, getcontext, ROUND_HALF_DOWN

class Position(object):
    def __init__(
        self, side, market, units, 
        exposure, avg_price, cur_price
    ):
        self.side = side
        self.market = market
        self.units = units
        self.exposure = Decimal(str(exposure))
        self.avg_price = Decimal(str(avg_price))
        self.cur_price = Decimal(str(cur_price))
        self.profit_base = self.calculate_profit_base(self.exposure)
        self.profit_perc = self.calculate_profit_perc(self.exposure)

    def calculate_pips(self):
        getcontext.prec = 6
        mult = Decimal("1")
        if self.side == "SHORT":
            mult = Decimal("-1")
        return (mult * (self.cur_price - self.avg_price)).quantize(
            Decimal("0.00001"), ROUND_HALF_DOWN
        )

    def calculate_profit_base(self, exposure):
        pips = self.calculate_pips()        
        return (pips * exposure / self.cur_price).quantize(
            Decimal("0.00001"), ROUND_HALF_DOWN
        )

    def calculate_profit_perc(self, exposure):
        return (self.profit_base / exposure * Decimal("100.00")).quantize(
            Decimal("0.00001"), ROUND_HALF_DOWN
        )

    def update_position_price(self, cur_price, exposure):
        self.cur_price = cur_price
        self.profit_base = self.calculate_profit_base(exposure)
        self.profit_perc = self.calculate_profit_perc(exposure)

Da notare come dobbiamo l’argomento di Decimal è una stringa, piuttosto che un argomento in virgola mobile. Questo perché una stringa specifica con esattezza la precisione del valore, mentre non è possibile con un argomento a virgola mobile.

Inoltre bisogna tenere presente che la memorizzazione delle operazioni in un database relazionale (come descritto nella roadmap al paragrafo precedente) prevede di utilizzare il corretto tipo di dati. PostgreSQL e MySQL supportano una rappresentazione decimale. È fondamentale utilizzare questi tipi di dati quando si crea lo schema del database, altrimenti si avranno errori di arrotondamento estremamente difficili da diagnosticare!

Per coloro che sono interessati a una discussione più approfondita di questi problemi, in matematica e informatica, il tema dell’analisi numerica copre i problemi di archiviazione in virgola mobile, tra molti altri argomenti interessanti.

Nei prossimi articoli del diario descriveremo come applicare gli unit test al codice e come estendere il sistema per gestire più coppie di valute, modificando i calcoli di posizione.

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

Se si desidera leggere gli altri articoli di questa serie, sono disponibili ai seguenti link:

DTForex #2 – Aggiunta di un Portafoglio al Sistema di Trading Automatico sul Forex

forex-python-trading-algoritmico-002

Nel primo articolo della serie sul trading algoritmico sul Forex (link) abbiamo descritto come creare un sistema di trading automatico che si collega all’API del broker OANDA. Abbiamo anche menzionato che i passaggi successivi includevano la costruzione di un portafoglio e una copertura per la gestione del rischio da applicare per tutti i segnali suggeriti e generati dalla componente Strategy. In questo articolo descriviamo come costruire una componente di Portfolio completa e funzionante.

Questa componente è necessaria se vogliamo costruire un motore di backtest per le strategie forex in modo analogo a quanto descritto in precedenza con il mercato azionario tramite il backtester guidato dagli eventi. In particole si vuole un ambiente che presenti una differenza minima tra il trading live e il sistema di backtest. Per questo motivo dobbiamo realizzare una componente di portafoglio che riflettesse (per quanto possibile) lo stato attuale del conto di trading fornito da OANDA.

La logica base prevede che il conto di trading “practice” e le componenti del portafoglio locale dovrebbero avere valori simili, se non uguali, per attributi come il saldo del conto, il profitto e la perdita (P&L) non realizzati, il conto economico realizzato e qualsiasi posizione aperta . Se raggiungiamo questo obiettivo ed eseguiamo alcune strategie di test tramite questa componente di portafoglio, e se gli attributi risultino con valori uguali sia nel portfolio locale che in OANDA, allora potremmo essere fiduciosi nella capacità del backtester di produrre risultati più realistici, molto simili a quelli che si le strategie avrebbero avuto se fossero state in “live”.

Ho passato gli ultimi due giorni a tentare di implementare un tale oggetto Portfolio e credo di esserci quasi riuscito. Vedo ancora alcune differenze tra il saldo del portafoglio locale e il saldo del conto OANDA dopo che sono state effettuate diverse operazioni.

Quali sono i limiti attuali di questa implementazione?

  • La valuta di base, e quindi l’esposizione, è codificata per essere EUR. Deve essere cambiato per consentire la scelta di qualsiasi valuta di base.
  • Attualmente l’ho testato solo per EUR / USD, poiché la mia valuta di base è EUR Successivamente modificherò i calcoli dell’esposizione per consentire qualsiasi coppia di valute.
  • Sebbene alcuni unit test abbiano suggerito che l’aggiunta e la rimozione di posizioni e unità sta funzionando come previsto, non è stato ancora testato.
  • Finora l’ho provato solo con l’apertura e la chiusura di posizioni long, non ho testato posizioni short. Avrò bisogno di scrivere alcuni unit test per gestire le posizioni short.

Ci si potrebbe ragionevolmente chiedere perché sto descrivendo questa componente se presenta tutte queste limitazioni? In questo modo vorrei che qualsiasi lettore di questo articolo possa essere consapevole di come la creazione di sistemi di trading algoritmico è un lavoro duro e richiede molta attenzione ai dettagli! C’è un notevole margine di manovra per introdurre bug e comportamenti scorretti. Voglio delineare come vengono costruiti i sistemi del “mondo reale” e mostrarvi come testare questi errori e correggerli.

Inizieremo descrivendo come ho costruito l’attuale configurazione del portafoglio e poi l’ho integrato nel sistema di trading demo che abbiamo esaminato nel precedente articolo.  Successivamente vedremo i punti in cui penso ci possano essere differenze.

Il seguente codice “così com’è” sotto il disclaimer che ho indicato nel precedente articolo.

Creazione del Portfolio

Per generare un oggetto Portfolio è necessario descrivere come vengono eseguite le negoziazioni in valuta, poiché differiscono in modo sostanziale dalle azioni.

Calcolo di Pips e Unità

In altre classi di attività, il più piccolo incremento di una variazione del prezzo dell’asset è noto come “tick”. Nel trading in Forex è noto come “pip” (Price Interest Point). È l’incremento più piccolo in qualsiasi coppia di valute ed è (di solito) 1/100 di centesimo, noto anche come punto base. Dato che la maggior parte delle principali coppie di valute ha un prezzo di quattro cifre decimali, la variazione più piccola si verifica sull’ultimo punto decimale.

In EUR / USD, ad esempio, un movimento da 1.1184 a 1.1185 è un pip (4 cifre decimali) e quindi un pip è uguale a 0.0001. Qualsiasi valuta basata sullo yen giapponese utilizza due punti decimali, quindi un pip sarebbe uguale a 0,01. 

Una domanda che possiamo ora porci è: A quanto equivale in euro (EUR) un movimento di 20 pips (20 x 0,0001 = 0,002) per una quantità fissa di unità di EUR/USD? Se prendiamo 2.000 unità della valuta di base (ad esempio 2.000 euro), possiamo calcolare il P&L in euro come segue: 

Profitto (EUR) = Pip x Esposizione / EURUSD = 0,002 x 2.000 / 1,1185 = 3,57

Con OANDA siamo liberi di scegliere il numero di quote negoziate (e quindi la generica esposizione). Dal momento che ho un conto in euro (EUR) e sto negoziando EUR/USD (in questo esempio) l’esposizione sarà sempre uguale al numero di unità. Questo è attualmente “codificato” nel sistema sottostante. Nel caso si vuole gestire più coppie di valute, è indispensabile modificare il calcolo dell’esposizione per tenere conto delle diverse valute di base.

Poiché il valore del profitto sopra descritto è piuttosto piccolo e le valute non oscillano molto (tranne quando lo fanno!), di solito è necessario introdurre la leva finanziaria nel conteggio. Discuteremo di questo negli articoli successivi. Per ora, non  dobbiamo preoccuparcene.

Panoramica del sistema di backtesting / trading

Il sistema attuale è costituito dai seguenti componenti:

  • Event – I componenti Evento trasportano i “messaggi” (come tick, segnali e ordini) tra gli oggetti Strategy, Portfolio ed Esecution.
  • Position – La componente Position rappresenta il concetto di una “posizione” Forex, ovvero un “long” o uno “short” in una coppia di valute con associata una quantità di unità.
  • Portfolio: il componente Portfolio contiene più oggetti Position, uno per ciascuna coppia di valute negoziata. Tiene traccia dell’attuale P&L di ciascuna posizione, anche dopo successivi incrementi e riduzioni di unità.
  • Strategy – L’oggetto Strategy prende le informazioni delle serie temporali (tick delle coppie di valute) e quindi calcola e invia gli eventi di segnale al portafoglio, che decide come agire su di essi.
  • Streaming Forex Price: questo componente si collega a OANDA tramite un web-socket in streaming e riceve dati tick-by-tick in tempo reale (ovvero bid / ask) da qualsiasi coppia di valute sottoscritta.
  • Esecution: si prende gli eventi  di tipo “Ordine” e li invia a OANDA per essere eseguiti.
  • Trading Loop – Il trading loop avvolge insieme tutti i componenti descritti sopra ed esegue due thread: uno per i prezzi di streaming e uno per il gestore di eventi.

Per ottenere maggiori informazioni su come il sistema è collegato insieme, vale la pena leggere il precedente articolo di questa serie.

Implementazione in Python

Discuteremo ora come implementare in Python il sistema appena descritto.

Position

Il primo componente è l’oggetto Position. È progettato per replicare il comportamento di una posizione aperta nel sistema fxTrade Practice di OANDA. La scheda Position nel software fxTrade contiene 8 colonne:

  • Type: indica se la posizione è “long” o “short”
  • Market: quale coppia di valute negoziare, ad es. “EUR/USD”
  • Unit: il numero di unità della valuta (vedi sopra)
  • Exposure (BASE) – L’esposizione nella valuta base della posizione
  • Avg. Price: il prezzo medio raggiunto per più acquisti. Se ci sono \(P\) acquisti, il prezzo medio viene calcolato come \(\frac{\sum_{p=1} ^ Pc_pu_p} {\sum_{p=1}^{P}u_p}\), dove \(c_p\) è il costo di acquisto \(p\) e \(u_p\) sono le unità acquisite per l’acquisto \(p\).
  • Current: il prezzo di vendita corrente.
  • Profit (BASE) – L’attuale P&L nella valuta base della posizione.
  • Profit (%): l’attuale percentuale di P&L della posizione.

Come è evidente nel codice seguente, questi attributi sono stati riflessi come membri della classe Position, ad eccezione di “Type”, che ho rinominato “side”, poiché type è una parola riservata in Python!

La classe ha quattro metodi (esclusa l’inizializzazione): calculate_pips, calculate_profit_base, calculate_profit_perc e update_position_price.

Il primo metodo, calculate_pips, determina il numero di pips che sono stati generati dalla posizione da quando è stata aperta (tenendo conto di eventuali nuove unità aggiunte alla posizione). Il secondo metodo, calculate_profit_base, calcola il profitto (o la perdita!) corrente sulla posizione. Il terzo metodo, calculate_profit_perc, determina la percentuale di profitto sulla posizione. Infine, update_position_price aggiorna i due valori precedenti in base ai dati di mercato correnti.

class Position(object):
    def __init__(
        self, side, market, units,
        exposure, avg_price, cur_price
    ):
        self.side = side
        self.market = market
        self.units = units
        self.exposure = exposure
        self.avg_price = avg_price
        self.cur_price = cur_price
        self.profit_base = self.calculate_profit_base()
        self.profit_perc = self.calculate_profit_perc()

    def calculate_pips(self):
        mult = 1.0
        if self.side == "SHORT":
            mult = -1.0
        return mult * (self.cur_price - self.avg_price)

    def calculate_profit_base(self):
        pips = self.calculate_pips()
        return pips * self.exposure / self.cur_price

    def calculate_profit_perc(self):
        return self.profit_base / self.exposure * 100.0

    def update_position_price(self, cur_price):
        self.cur_price = cur_price
        self.profit_base = self.calculate_profit_base()
        self.profit_perc = self.calculate_profit_perc()

Poiché un portafoglio può contenere più posizioni, ci sarà un’istanza di classe per ogni mercato che viene negoziato. Come accennato in precedenza, al momento il Portfoilo gestisce solamente EUR come valuta di base e EUR/USD come strumento di trading. Negli articoli futuri vedremo In articoli futuri estenderò l’oggetto Portfolio per gestire più valute di base e più coppie di valute. Parliamo ora di come configurare un ambiente virtuale di base per Python e quindi di come funziona il Portfolio.

Symlink per l'ambiente virtuale

Nel seguente modulo dell’oggetto Portfolio ho modificato il modo in cui vengono gestite le importazioni. Ho creato un ambiente virtuale, per cui ho aggiunto un collegamento simbolico alla mia directory DTForex. Ciò mi consente di fare riferimento a una gerarchia annidata di file di progetto all’interno di ogni modulo Python. Il codice per realizzare questo in Ubuntu è simile al seguente:

cd /PATH/TO/YOUR/VIRTUALENV/DIRECTORY/lib/python3/site-packages/
ln -s /PATH/TO/YOUR/DTFOREX/DIRECTORY/ROOT/ DTForex

Ovviamente si deve sostituire le posizioni del tuo ambiente virtuale e la posizione del codice sorgente. Normalmente memorizzo i miei ambienti virtuali nella directory home in ~/venv/. Memorizzo i miei progetti nella directory home in ~/sites/. Questo mi consente di fare riferimento, ad esempio, a dtforex.event.event import OrderEvent da qualsiasi file all’interno del progetto.

Portfolio

Il costruttore __init__ del Portfolio richiede i seguenti argomenti:

  • ticker – il gestore del ticker dei prezzi forex in streaming. Viene utilizzato per ottenere gli ultimi prezzi bid / ask.
  • event: la coda degli eventi, in cui il portfolio deve inserire gli eventi.
  • base – la valuta di base, nel mio caso è EUR.
  • leverage – il fattore di leva. Attualmente è 1:20.
  • equity – la quantità di patrimonio netto effettivo nel conto, che ho impostato per default a 100.000.
  • risk_per_trade – la percentuale del patrimonio netto del conto da poter rischiare per ogni operazione, che ho impostato di default al 2%. Ciò significa che le unità di scambio saranno pari a 2.000 per una dimensione del conto iniziale di 100.000.

All’inizializzazione la classe calcola le trade_units, che sono la quantità massima di unità consentite per posizione, oltre a dichiarare il dizionario delle positions (ogni mercato è una chiave) che contiene tutte le posizioni aperte all’interno del portafoglio:

from copy import deepcopy

from event import OrderEvent
from portfolio import Position


class Portfolio(object):
    def __init__(
        self, ticker, events, base="EUR", leverage=20, 
        equity=100000.0, risk_per_trade=0.02
    ):
        self.ticker = ticker
        self.events = events
        self.base = base
        self.leverage = leverage
        self.equity = equity
        self.balance = deepcopy(self.equity)
        self.risk_per_trade = risk_per_trade
        self.trade_units = self.calc_risk_position_size()
        self.positions = {}

In questa fase la “gestione del rischio” è piuttosto semplice! Nel seguente metodo calc_risk_position_size ci assicuriamo solamente che l’esposizione di ciascuna posizione non superi il risk_per_trade% del capitale del conto. Il valore predefinito del risk_per_trade è 2% come argomento della parola chiave, sebbene questo possa ovviamente essere modificato. Quindi per un conto di 100.000 euro, il rischio per operazione non supererà 2.000 euro per posizione.

Nota che questa cifra non si ridimensionerà dinamicamente con la dimensione del saldo del conto, utilizzerà solo il saldo del conto iniziale. Le implementazioni successive incorporeranno logiche più sofisticate di gestione del rischio e dimensionamento della posizione.

 def calc_risk_position_size(self):
        return self.equity * self.risk_per_trade

Il successivo metodo, add_new_position, richiede i parametri necessari per aggiungere una nuova posizione al Portfolio. In particolare, richiede add_price e remove_price. Non si utilizza direttamente i prezzi bid/ask perché i prezzi dipenderanno dal fatto che il lato sia “long” o “short”. Quindi dobbiamo specificare correttamente quale prezzo considerare in modo da ottenere un backtest realistico:

    def add_new_position(
        self, side, market, units, exposure,
        add_price, remove_price
    ):
        ps = Position(side, market, units, exposure,
                      add_price, remove_price
                     )
        
        self.positions[market] = ps

Abbiamo anche bisogno di un metodo, add_position_units, che consente di aggiungere unità ad una posizione, solamente dopo aver precedentemente creato la posizione. Per fare ciò dobbiamo calcolare il nuovo prezzo medio delle unità acquistate. Ricorda che questo viene calcolato dalla seguente espressione:

\(\begin{eqnarray}\frac{\sum_{p=1}^{P} c_p u_p} {\sum_{p = 1} ^ {P} u_p} \end{eqnarray}\)

Dove \(P\) è il numero di acquisti, \(c_p\) è il costo di acquisto \(p\) e \(u_p\) sono le unità acquistate con l’acquisto \(p\).

Una volta calcolato il nuovo prezzo medio, le unità vengono aggiornate nella posizione e quindi viene ricalcolato il P&L associato alla posizione:

    def add_position_units(
        self, market, units, exposure, 
        add_price, remove_price
    ):
        if market not in self.positions:
            return False
        else:
            ps = self.positions[market]
            new_total_units = ps.units + units
            new_total_cost = ps.avg_price*ps.units + add_price*units
            ps.exposure += exposure
            ps.avg_price = new_total_cost/new_total_units
            ps.units = new_total_units
            ps.update_position_price(remove_price)
            return True

Allo stesso modo, abbiamo bisogno di un metodo per rimuovere le unità da una posizione (ma non per chiuderla completamente). Questo è implementato da remove_position_units. Una volta che le unità e l’esposizione sono state ridotte, il conto economico viene calcolato per le unità rimosse e quindi aggiunto (o sottratto!) dal saldo del portafoglio:

    def remove_position_units(
        self, market, units, remove_price
    ):
        if market not in self.positions:
            return False
        else:
            ps = self.positions[market]
            ps.units -= units
            exposure = float(units)
            ps.exposure -= exposure
            ps.update_position_price(remove_price)
            pnl = ps.calculate_pips() * exposure / remove_price 
            self.balance += pnl
            return True

Abbiamo anche bisogno di un modo per chiudere completamente una posizione. Questo è implementato in close_position. È simile a remove_position_units tranne per il fatto che la posizione viene eliminata dal dizionario positions:

    def close_position(
            self, market, remove_price
    ):
        if market not in self.positions:
            return False
        else:
            ps = self.positions[market]
            ps.update_position_price(remove_price)
            pnl = ps.calculate_pips() * ps.exposure / remove_price
            self.balance += pnl
            del [self.positions[market]]
            return True

La maggior parte del lavoro di questa classe viene eseguita dal metodo execute_signal. Il metodo rende gli oggetti SignalEvent creati dagli oggetti Strategy e li utilizza per generare oggetti OrderEvent da reinserire nella coda degli eventi.

La logica di base è la seguente:

  • Se non esiste una posizione corrente per questa coppia di valute, creane una.
  • Se una posizione esiste già, controlla se sta aggiungendo o sottraendo unità.
  • Se sta aggiungendo unità, aggiungi semplicemente la quantità corretta di unità.
  • Se non sta aggiungendo unità, controlla se la nuova riduzione di unità avversaria chiude lo scambio, in tal caso fallo.
  • Se le unità di riduzione sono inferiori alle unità di posizione, rimuovere semplicemente quella quantità dalla posizione.
  • Tuttavia, se le unità riducenti superano la posizione corrente, è necessario chiudere la posizione corrente dalle unità riducenti e quindi creare una nuova posizione opposta con le unità rimanenti. Non l’ho ancora testato ampiamente, quindi potrebbero esserci ancora dei bug!

Il codice per execute_signal segue:

    def execute_signal(self, signal_event):
        side = signal_event.side
        market = signal_event.instrument
        units = int(self.trade_units)

        # Controlla il lato per il corretto prezzo bid/ask
        # TODO: Supporta solo i long
        add_price = self.ticker.cur_ask
        remove_price = self.ticker.cur_bid
        exposure = float(units)

        # Se non c'è una posizione, si crea una nuova
        if market not in self.positions:
            self.add_new_position(
                side, market, units, exposure,
                add_price, remove_price
            )
            order = OrderEvent(market, units, "market", "buy")
            self.events.put(order)
        # Se la posizione esiste, si aggiunge o rimuove unità
        else:
            ps = self.positions[market]
            # controlla se il lato è coerente con il lato della posizione
            if side == ps.side:
                # aggiunge unità alla posizione
                self.add_position_units(market, units, exposure,
                                        add_price, remove_price
                                        )
            else:
                # Controlla se ci sono unità nella posizione
                if units == ps.units:
                    # Chiude la posizione
                    self.close_position(market, remove_price)
                    order = OrderEvent(market, units, "market", "sell")
                    self.events.put(order)
                elif units < ps.units:
                    # Rimuove unità dalla posizione
                    self.remove_position_units(
                        market, units, remove_price
                    )
                else:  # units > ps.units
                    # Chiude la posizione e crea una nuova posizione
                    # nel lato opposto con le unità rimanenti
                    new_units = units - ps.units
                    self.close_position(market, remove_price)

                    if side == "buy":
                        new_side = "sell"
                    else:
                        new_side = "sell"
                    new_exposure = float(units)
                    self.add_new_position(
                        new_side, market, new_units,
                        new_exposure, add_price, remove_price
                    )
        print
        "Balance: %0.2f" % self.balance

Questo conclude il codice per la classe Portfolio. Ora discutiamo della gestione degli eventi.

Event

Affinché questo Portfolio funzioni con le nuove logiche di generazione di segnali e ordini è necessario modificare event.py. In particolare abbiamo aggiunto la componente SignalEvent, che ora è generato dall’oggetto Strategy, invece di un OrderEvent. Indica semplicemente se andare long o short su un particolare “strumento”, cioè una coppia di valute. L’order_type si riferisce al fatto che l’ordine sia un ordine di mercato o un ordine limite. Non ho abbiamo ancora implementato quest’ultimo, quindi per ora sarà sempre valorizzato come “market”:

class Event(object):
    pass


class TickEvent(Event):
    def __init__(self, instrument, time, bid, ask):
        self.type = 'TICK'
        self.instrument = instrument
        self.time = time
        self.bid = bid
        self.ask = ask


class SignalEvent(Event):
    def __init__(self, instrument, order_type, side):
        self.type = 'SIGNAL'
        self.instrument = instrument
        self.order_type = order_type
        self.side = side        


class OrderEvent(Event):
    def __init__(self, instrument, units, order_type, side):
        self.type = 'ORDER'
        self.instrument = instrument
        self.units = units
        self.order_type = order_type
        self.side = side        

Strategy

Dopo aver definito l’oggetto SignalEvent, dobbiamo modificare la logica di funzionamento della classe Strategy. In particolare, ora deve generare eventi SignalEvent invece di OrderEvents.

Dobbiamo effettivamente cambiare la logica base della “strategia”. Invece di creare segnali casuali di acquisto o vendita, ora genera un ordine di acquisto ogni 5 tick e quindi il sistema diventa “investito”. Al 5° tick successivo, se è investito, si effettua una vendita e diventa “non investito”. Questo processo si ripete in un ciclo infinito:

from event import SignalEvent

class TestStrategy(object):
    def __init__(self, instrument, events):
        self.instrument = instrument
        self.events = events
        self.ticks = 0
        self.invested = False

    def calculate_signals(self, event):
        if event.type == 'TICK':
            self.ticks += 1
            if self.ticks % 5 == 0:
                if self.invested == False:
                    signal = SignalEvent(self.instrument, "market", "buy")
                    self.events.put(signal)
                    self.invested = True
                else:
                    signal = SignalEvent(self.instrument, "market", "sell")
                    self.events.put(signal)
                    self.invested = False

StreamingForexPrices

L’oggetto Portfolio richiede un oggetto ticker che contienea i prezzi ask/bid più recenti. Abbiamo semplicemente modificato StreamingForexPrices nel file streaming.py per contenere due attributi extra:

..
..
        self.cur_bid = None
        self.cur_ask = None
..
..

Questi attributo sono valorizzati nel metodo stream_to_queue:

..
..
                if msg.has_key("instrument") or msg.has_key("tick"):
                    print msg
                    instrument = msg["tick"]["instrument"]
                    time = msg["tick"]["time"]
                    bid = msg["tick"]["bid"]
                    ask = msg["tick"]["ask"]
                    self.cur_bid = bid
                    self.cur_ask = ask
                    tev = TickEvent(instrument, time, bid, ask)
                    self.events_queue.put(tev)

Come per ogni oggetto di questo articolo, il codice completo può essere trovato nel seguente repository di github: github.com/datatrading-info/DTForex

Trading

L’ultima parte delle modifiche è relativo al file trading.py. Per prima cosa si modificano le importazioni per tenere conto della struttura della directory e del fatto che ora stiamo importando un oggetto Portfolio:

from execution import Execution
from portfolio import Portfolio
from settings import STREAM_DOMAIN, API_DOMAIN, ACCESS_TOKEN, ACCOUNT_ID
from strategy import TestStrategy
from data import StreamingForexPrices

Quindi modifichiamo il gestore della coda degli eventi per indirizzare SignalEvents all’istanza di Portfolio:

..
..
    while True:
        try:
            event = events.get(False)
        except Queue.Empty:
            pass
        else:
            if event is not None:
                if event.type == 'TICK':
                    strategy.calculate_signals(event)
                elif event.type == 'SIGNAL':
                    portfolio.execute_signal(event)
                elif event.type == 'ORDER':
                    execution.execute_order(event)
        time.sleep(heartbeat)
..
..

Infine modifichiamo la funzione __main__ per creare il Portfolio e aggiustiamo trade_thread per prendere il Portfolio come argomento:

    ..
    ..
    # Crea un oggetto Portfolio che sarà usato per 
    # confrontare le posizioni OANDA con quelle locali
    # in modo da verificare l'integrità del backtesting.
    portfolio = Portfolio(prices, events, equity=100000.0)

    # Crea due threads separati: Uno per il ciclo di trading
    # e l'altro per lo streaming dei prezzi di mercato
    trade_thread = threading.Thread(
        target=trade, args=(
            events, strategy, portfolio, execution
        )
    )
    ..
    ..

Variabili d'ambiente nelle impostazioni

Nell’articolo precedente abbiamo menzionato che non è una buona idea memorizzare le password o altre informazioni di autenticazione, inclusi il token API, all’interno del codice sorgente. Quindi si può modificare il file delle impostazioni in questo modo:
import os

ENVIRONMENTS = { 
    "streaming": {
        "real": "stream-fxtrade.oanda.com",
        "practice": "stream-fxpractice.oanda.com",
        "sandbox": "stream-sandbox.oanda.com"
    },
    "api": {
        "real": "api-fxtrade.oanda.com",
        "practice": "api-fxpractice.oanda.com",
        "sandbox": "api-sandbox.oanda.com"
    }
}

DOMAIN = "practice"
STREAM_DOMAIN = ENVIRONMENTS["streaming"][DOMAIN]
API_DOMAIN = ENVIRONMENTS["api"][DOMAIN]
ACCESS_TOKEN = os.environ.get('OANDA_API_ACCESS_TOKEN', None)
ACCOUNT_ID = os.environ.get('OANDA_API_ACCOUNT_ID', None)
Nello specifico, le seguenti due righe:
ACCESS_TOKEN = os.environ.get('OANDA_API_ACCESS_TOKEN', None)
ACCOUNT_ID = os.environ.get('OANDA_API_ACCOUNT_ID', None)

Abbiamo utilizzato la libreria os per recuperare due variabili di ambiente (ENVVARS). Il primo è il token di accesso API e il secondo è l’ID account OANDA. Questi possono essere memorizzati in un file di ambiente che viene caricato all’avvio del sistema. In Ubuntu, puoi usare il file .bash_profile nascosto nella tua directory home. Ad esempio, usando l’editor di testo preferito, si può digitare:

emacs ~/.bash_profile
Si aggiungono le seguenti righe, assicurandosi di sostituire le variabili con i dettagli di un account practice:
export OANDA_API_ACCESS_TOKEN='1234567890abcdef1234567890abcdef1234567890abcdef'
export OANDA_API_ACCOUNT_ID='12345678'
Potrebbe essere necessario assicurarsi che il terminale abbia accesso a queste variabili eseguendo quanto segue da riga di comando:
source ~/.bash_profile

Esecuzione del Codice

Per eseguire il codice è necessario assicurarsi che l’ambiente virtuale sia correttamente impostato. Lo si può verificare eseguendo il seguente comando (attenzione e a specificare la directory corretta):
source ~/venv/qsforex/bin/activate

Si dovrà anche installare la libreria requests, se non è statp fatto durante l’articolo precedente:

pip install requests
Infine, si può eseguire il codice (assicurandosi di adattare il percorso al codice sorgente del progetto):
python dtforex/trading/trading.py

A questo punto, stiamo effettuando il nostro primo sismtea di trading! Come affermato nell’articolo precedente, è molto facile perdere denaro con un sistema di questo tipo collegato a un conto di trading live! Assicurati di visualizzare il disclaimer nel post e di essere estremamente attento con gli oggetti Strategy. Consiglio vivamente di provarlo sulla sandbox o sugli account di esercitazione prima di un’implementazione live.

Tuttavia, prima di procedere con l’implementazione di strategie personalizzate, vorrei discutere da dove credo derivino alcune delle differenze tra il saldo del conto OANDA e il saldo calcolato.

Possibili fonti di errore

Man mano che l’implementazione dei sistemi diventa più complessa, aumenta il rischio che siano stati introdotti bug. Si possono utilizzare alcuni unit test per verificare se gli oggetti Position e Portfolio si comportano come previsto, ma ci sono ancora discrepanze tra il portafoglio locale e il saldo del conto OANDA. Le possibili ragioni includono:

  • Bug – Ovviamente i bug possono insinuarsi ovunque. Il modo migliore per eliminarli è definire in anticipo delle solide specifiche su ciò che il programma dovrebbe fare e creare precisi unit test. È necessario prevedere ulteriore lavoro per effettuare gli unit test  di tutte le classi
  • Errori di arrotondamento: poiché si utilizza variabili a virgola mobile per memorizzare tutti i dati finanziari, si verificheranno errori nell’arrotondamento. Il modo per aggirare questo è usare il tipo Decimal di Python. Le implementazioni successive utilizzeranno il valore Decimal.
  • Slippage – Lo slippage è la differenza tra il prezzo che l’oggetto Strategy ha definito quando ha deciso di acquistare o vendere e il prezzo effettivo raggiunto quando il broker esegue un ordine. Data la natura multi-threaded del programma, è estremamente probabile che lo slippage sia una delle cause delle differenze tra il saldo locale e il saldo del conto OANDA.

Studierò questi problemi mentre continuo a lavorare sul sistema forex. Nella prossimo articolo della serie vedremo i miei progressi.

Prossimi Passi

Negli articoli successivi discuteremo i seguenti miglioramenti:

  • Saldi contabili differenti – Il primo compito è determinare perché i saldi contabili differiscono tra OANDA e questa implementazione locale. 
  • Strategie reali: di recente ho letto alcuni articoli su come applicare l’apprendimento automatico ai mercati forex. Convertire alcune di questi teorie in strategie effettive di cui possiamo il bakctest sarebbe interessante (e divertente!).
  • Valute multiple – Aggiunta di più coppie di valute e valute di base alternative.
  • Costi di transazione – Gestione realistica dei costi di transazione, oltre che lo spread denaro-lettera. Ciò includerà una migliore modellazione dello slippage e un impatto sul mercato.

Ci sono anche molti altri miglioramenti da apportare. Questo progetto migliorerà continuamente e spero che possa aiutarti nel tuo trading automatico

 

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

Se si desidera leggere gli altri articoli di questa serie, sono disponibili ai seguenti link:

DTForex #1 – Trading Automatico sul Forex tramite le API di Oanda

forex-python-trading-algoritmico-001

Nonostante io stesso svolgo la mia attività di ricerca sui mercati azionari e futures, ho pensato che sarebbe stato divertente (ed educativo!) scrivere delle mie esperienze di studio del mercato forex tramite una nuova serie di articoli. Ogni articolo sarà costruito sulla base dei precedenti articoli, ma dovrebbe essere anche relativamente autosufficiente.

In questo primo articolo  descriviamo come creare un nuovo account DEMO con il broker OANDA e come creare un motore di trading basato su eventi multithreading in grado di eseguire automaticamente le operazioni sia in modalità demo che live.

Negli articoli precedenti abbiamo dedicato molto tempo a esaminare il backtester guidato dagli eventi, principalmente per le azioni e gli ETF. In questo articolo introduciamo un motore di backtest orientato al forex, che può essere utilizzato sia per il paper-trading che per il live-trading.

Le istruzioni operative di questo articolo sono in ambiente Ubuntu 18.04, ma possono essere facilmente tradotte in Windows o Mac OS X, utilizzando una distribuzione Python come Anaconda. L’unica libreria aggiuntiva utilizzata per il motore di trading in Python è la libreria requests, necessaria per la comunicazione HTTP con l’API di OANDA.

Poiché questo è il primo post diretto al trading sul forex e il codice presentato di seguito può essere facilmente adattato a un ambiente di trading dal vivo, è necessario presentare l’opportuna dichiarazione di non responsabilità:

Disclaimer: il trading sul forex a margine comporta un alto livello di rischio e potrebbe non essere adatto a tutti gli investitori. I rendimenti passati non sono indicativi di risultati futuri. L’elevato grado di leva finanziaria può funzionare sia contro di te che per te. Prima di decidere di investire in valuta estera, dovresti considerare attentamente i tuoi obiettivi di investimento, il livello di esperienza e la propensione al rischio. Esiste la possibilità che tu possa sostenere una perdita parziale o totale del tuo investimento iniziale e quindi non dovresti investire denaro che non puoi permetterti di perdere. È necessario essere consapevoli di tutti i rischi associati al trading in valuta estera e, in caso di dubbi, chiedere consiglio a un consulente finanziario indipendente.

Questo software viene fornito “così com’è” e qualsiasi garanzia espressa o implicita, incluse, ma non limitate a, le garanzie implicite di commerciabilità e idoneità per uno scopi particolari sono escluse. In nessun caso i realizzatori o i contributori saranno responsabili per danni diretti, indiretti, incidentali, speciali, esemplari o consequenziali (inclusi, ma non limitati a, l’approvvigionamento di beni o servizi sostitutivi; perdita di usabilità, dati o profitti; o interruzione dell’attività) comunque causati e in base a qualsiasi teoria di responsabilità, sia contrattuale, oggettiva o illecita (inclusa negligenza o altro) derivante dall’uso di questo software, anche se informati della possibilità di tale danno.

Creazione di un account con OANDA

La prima domanda che mi viene in mente è “Perché scegliere OANDA?”. In poche parole, dopo aver cercato su Google i broker forex che disponevano di API, ho visto che OANDA aveva recentemente rilasciato una REST API adeguata con cui era possibile comunicare facilmente con quasi tutti i linguaggi in modo estremamente facile. Dopo aver letto la loro documentazione API per sviluppatori, ho deciso di provarlo, almeno con un account di prova.

Per essere chiari, non ho alcun rapporto precedente o esistente con OANDA e sto fornendo questa raccomandazione solo in base alla mia limitata esperienza limitata nel testare nella pratica la loro API e un breve utilizzo (per il download dei dati di mercato). Se qualcuno si è imbattuto in altri broker forex che hanno anche un’API altrettanto moderna, sarei felice di dare un’occhiata anche a questi broker.

Prima di utilizzare l’API è necessario registrarsi per un account di pratica. Per fare ciò, utilizza il link di registrazione. Si avrà una schermata simile alla seguente:

qs-oanda-forex-sign-up-trading-algoritmico
Si potrà quindi loggarsi con le proprie credenziali di accesso. Assicurarsi di selezionare la scheda “fxTradePractice” dalla schermata di accesso:
qs-oanda-forex-sign-in-trading-algoritmico
Una volta entrati, si dovrà prendere nota del proprio ID account. È elencato sotto l’intestazione nera “My Funds” accanto a “Primary”. Il mio è un numero di 7 cifre. Inoltre si dovrà generare anche un token API personale. A tale scopo, si deve far clic su “Manage API Access” nella scheda “Other Actions”:
qs-oanda-forex-manage-api-trading-algoritmico

A questo punto saremo in grado di generare un token API. Si avrà bisogno di questo token per usarla successivamente, quindi bisogna assicurarsi di scriverla correttamente.Ora si può lanciare l’applicazione FXTrade Practice, che permetterà di consultare gli ordini eseguiti e i profitti e perdite (in paper!).

Se si sta utilizzando un sistema Ubuntu, si dovrà installare una versione leggermente diversa di Java. In particolare, la versione Oracle di Java 8. Se non lo si fa, il simulatore di pratica non verrà caricato dal browser.

In Ubuntu si eseguono questi comandi:

sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update
sudo apt-get install oracle-java8-installer
Ora siamo in grado di avviare l’ambiente di trading demo. Tornando alla dashboard di OANDA e si fa clic sul link evidenziato in verde “Launch FXTrade Practice”. Verrà visualizzata una finestra di dialogo Java che chiede se si desidera eseguirlo. Cliccando su “Run” si carica l’applicazione fxTrade Practice. Ad esempio si può visualizzare un grafico a candele di 15 minuti di EUR / USD con il pannello delle quotazioni a sinistra:
qs-oanda-forex-manage-api-trading-algoritmico
A questo punto siamo pronti per iniziare a progettare e codificare il nostro sistema di forex trading automatizzato contro l’API di OANDA.

Panoramica dell'Architettura di Trading

Nella serie di articoli relativi alla progettazione di un backtester basato sugli eventi per azioni ed ETF, scritti in precedenza, abbiamo descritto il funzionamento di un sistema di trading basato sugli eventi. Se si desira capire questo motore basato sugli eventi, suggerisco caldamente di leggere questi articoli per avere un’idea di come funziona.

In sostanza, l’intero software viene eseguito in un ciclo continuo che termina solo quando il sistema di trading viene spento. Il meccanismo di comunicazione centrale del sistema è dato tramite una coda di eventi.

La coda viene costantemente interrogata per verificare la presenza di nuovi eventi. Una volta che un evento è stato rimosso dalla parte superiore della coda, deve essere gestito da un componente appropriato del sistema. Quindi un feed di dati di mercato potrebbe creare TickEvents che vengono inseriti in coda quando arriva un nuovo prezzo di mercato. Un oggetto Strategy di generazione di segnali potrebbe creare OrderEvents che devono essere inviati a un broker o società di intermediazione.

L’utilità di un tale sistema consiste nell’essere indipendente dal tipo di ordine o dal tipo di eventi vengono posti in coda, in quanto saranno sempre gestiti correttamente dal giusto componente all’interno del programma.

Inoltre, diverse parti del programma possono essere eseguite in thread separati, quindi non c’è bisogno di attendere la conclusione di particolare componente prima di elaborarne un altro. Ciò è estremamente utile negli scenari di trading algoritmico in cui i gestori di feed dei dati di mercato e i generatori di segnali strategici hanno caratteristiche di performance molto diverse.

Il ciclo di trading principale è dato dal seguente pseudo-codice Python:

while True:
    try:
        event = events_queue.get(False)
    except Queue.Empty:
        pass
    else:
        if event is not None:
            if event.type == 'TICK':
                strategy.calculate_signals(event)
            elif event.type == 'ORDER':
                execution.execute_order(event)
    time.sleep(heartbeat)
Come detto in precedenza, il codice viene eseguito in un ciclo infinito. In primo luogo, viene eseguito il polling della coda per recuperare un nuovo evento. Se la coda è vuota, il ciclo si riavvia semplicemente dopo un breve periodo di sospensione noto come “battito cardiaco”. Se viene trovato un evento, viene valutato il suo tipo e quindi viene chiamato il relativo modulo (il gestore strategy o execution) per gestire l’evento ed eventualmente generarne di nuovi che saranno inseriti in coda. I componenti di base del nostro motore di trading includono quanto segue:
  • Streaming Price Handler – Mantiene  una connessione di lunga durata aperta verso i server di OANDA e invierà dati tick (ad es. bid/ask) attraverso la connessione per tutti gli strumenti a cui siamo interessati.
  • Strategy Signal Generator – Utilizza una sequenza di eventi tick e li utilizzerà per generare ordini di trading che verranno eseguiti dal gestore dell’esecuzione.
  • Execution Handler – Il gestore dell’esecuzione: prende un insieme di eventi Ordine e li esegue alla cieca con OANDA.
  • Events: questi oggetti costituiscono i “messaggi” che vengono passati nella coda degli eventi. Ne occorrono solo due per questa implementazione, vale a dire TickEvent e OrderEvent.
  • Main Entry Point – Il punto di ingresso principale include anche il ciclo “trading” che esegue continuamente il polling della coda dei messaggi e invia i messaggi al corretto componente. Questo è spesso noto come “ciclo di eventi” o “gestore di eventi”.
Discuteremo ora in dettaglio l’implementazione del codice. In fondo all’articolo c’è l’elenco completo di tutti i file del codice sorgente. Inserendoli nella stessa directory ed eseguendo python trading.py inizierai a generare ordini, supponendo che tu abbia inserito l’account ID e il token di autenticazione da OANDA.

Implementazione Python

È una cattiva pratica memorizzare le password o le chiavi di autenticazione all’interno del codice poiché non è mai possibile prevedere a chi verrà consentito l’accesso a un progetto. In un sistema in produzione si memorizzano queste credenziali come variabili di ambiente del sistema e quindi si interroga questi “envvars” ogni volta che il codice viene ridistribuito. Ciò garantisce che le password e i token di autenticazione non siano mai archiviati in un sistema di controllo della versione.

Tuttavia, poiché siamo interessati esclusivamente a costruire un sistema di trading “paper” e in questo articolo  non ci occupiamo dei dettagli di deploy, memorizziamo questi token di autenticazione in un file di impostazioni.

Nel seguente file di configurazione settings.py abbiamo un dizionario chiamato ENVIRONMENTS che memorizza gli API endpoint sia per l’API di streaming dei prezzi di OANDA che per l’API di trading. Ogni dizionario secondario contiene tre separati API endpoint: real, practice e sandbox.

L’API sandbox serve esclusivamente per testare il codice e per verificare che non ci siano errori o bug. Non ha le garanzie di uptime delle API real o practice. L’API practice, in sostanza, fornisce la funzionalità di paper trading. Cioè, fornisce tutte le funzionalità dell’API real su un account DEMO simulato. L’API real è proprio questo: è il live trading! Se si usa questo endpoint nel codice, verrà effettuato il trading in un conto con denaro reale. STATE ESTREMAMENTE ATTENTI!

IMPORTANTE: quando si fa trading con l’API practice, bisogna ricordare che un costo di transazione importante, cioè l’impatto sul mercato, non viene considerato. Dal momento che nessuna negoziazione viene effettivamente collocata nell’ambiente, questo costo deve essere contabilizzato in un altro modo utilizzando un modello di impatto sul mercato se si desidera una valutazione realistica delle prestazioni.

Di seguito utilizziamo l’account practice fornito dall’impostazione DOMAIN. Abbiamo bisogno di due dizionari separati per i domini, uno per i componenti di streaming e e uno per il trading. Infine abbiamo ACCESS_TOKEN e ACCOUNT_ID. Ho valorizzato i due ID con valori fittizi, quindi si dovrà utilizzare i propri ID, che possono essere ricavati dalla pagina dell’account OANDA:

ENVIRONMENTS = {
    "streaming": {
        "real": "stream-fxtrade.oanda.com",
        "practice": "stream-fxpractice.oanda.com",
        "sandbox": "stream-sandbox.oanda.com"
    },
    "api": {
        "real": "api-fxtrade.oanda.com",
        "practice": "api-fxpractice.oanda.com",
        "sandbox": "api-sandbox.oanda.com"
    }
}

DOMAIN = "practice"
STREAM_DOMAIN = ENVIRONMENTS["streaming"][DOMAIN]
API_DOMAIN = ENVIRONMENTS["api"][DOMAIN]
ACCESS_TOKEN = 'abcdef0123456abcdef0123456-abcdef0123456abcdef0123456'
ACCOUNT_ID = '12345678'

Il passaggio successivo consiste nel definire gli events che la coda utilizzerà per aiutare la comunicazione tra tutti i singoli componenti del sistema. Abbiamo bisogno di due eventi: TickEvent e OrderEvent. Il primo memorizza le informazioni sui dati di mercato dello strumento come la (migliore) bid/ask e il tempo di negoziazione. Il secondo è utilizzato per trasmettere gli ordini all’executive handler e quindi contiene lo strumento, il numero di unità da negoziare, il tipo di ordine (“mercato” o “limite”) e il “lato” (cioè “long” e “short” ).

Per rendere flessibile il codice si crea una classe base chiamata Event e tutti gli eventi ereditano da questa classe. Di seguito il codice della classe events.py:

class Event(object):
    pass


class TickEvent(Event):
    def __init__(self, instrument, time, bid, ask):
        self.type = 'TICK'
        self.instrument = instrument
        self.time = time
        self.bid = bid
        self.ask = ask


class OrderEvent(Event):
    def __init__(self, instrument, units, order_type, side):
        self.type = 'ORDER'
        self.instrument = instrument
        self.units = units
        self.order_type = order_type
        self.side = side

La prossima classe è quella per gestire la strategia di trading. In questa demo creiamo una strategia piuttosto priva di senso che riceve semplicemente tutti i tick di mercato e ogni 5 tick acquista o vende a caso 10.000 unità di EUR / USD.Chiaramente questa è una “strategia” ridicola! Tuttavia, è utile per scopi di didattici perché è semplice da codificare e da capire. Negli articoli successivi vedremo come sostituire questa logica con qualcosa di più significativo che (si spera) genererà un profitto!

Il file strategy.py è riportato di seguito. Analizziamolo e vediamo cosa implementa. Per prima cosa importiamo la libreria random e l’oggetto OrderEvent da events.py. Abbiamo bisogno della libreria random per selezionare casualmente un ordine di acquisto o vendita casuale. Abbiamo bisogno di OrderEvent poiché questo è l’evento con cui l’oggetto strategy invierà gli ordini alla coda degli eventi, che verrà successivamente eseguita dal gestore di esecuzione.

La classe TestRandomStrategy prende semplicemente lo strumento (in questo caso EUR/USD), il numero di unità e gli eventi in coda come un insieme di parametri. Quindi crea un contatore ticks che viene utilizzato per dire quante istanze di TickEvent sono state viste.

La maggior parte del lavoro avviene nel metodo calcolate_signals, che prende semplicemente un evento, determina se si tratta di un TickEvent (altrimenti lo ignora) e incrementa il contatore ticks. Quindi controlla se il conteggio è divisibile per 5 e poi acquista o vende in modo casuale, con un ordine di mercato, il numero di unità specificato. Non è certamente la migliore strategia di trading al mondo, ma sarà più che adatta ai nostri scopi di test delle API di intermediazione di OANDA!

import random

from event import OrderEvent


class TestRandomStrategy(object):
    def __init__(self, instrument, units, events):
        self.instrument = instrument
        self.units = units
        self.events = events
        self.ticks = 0

    def calculate_signals(self, event):
        if event.type == 'TICK':
            self.ticks += 1
            if self.ticks % 5 == 0:
                side = random.choice(["buy", "sell"])
                order = OrderEvent(
                    self.instrument, self.units, "market", side
                )
                self.events.put(order)

Il componente successivo è l’execution handler. Questa classe ha il compito di agire sulle istanze OrderEvent e di effettuare richieste al broker (in questo caso OANDA) in modo “stupido”. Cioè, non vi è alcuna gestione del rischio o sovrapposizione della costruzione del portafoglio. Il gestore dell’esecuzione eseguirà semplicemente qualsiasi ordine che gli è stato inviato.

Dobbiamo passare tutte le informazioni di autenticazione alla classe Execution, incluso il “dominio” (practice, real o sandbox), il token di accesso e l’ID account. Quindi creiamo una connessione sicura con http.client, una delle librerie incorporate in Pythons.

La maggior parte del lavoro avviene in execute_order. Il metodo richiede un evento come parametro. Quindi costruisce due dizionari: le headers e i params. Questi dizionari sono quindi correttamente codificati (tramite urllib, un’altra libreria Python) per essere inviati come richiesta HTTP POST all’API di OANDA.

Passiamo i parametri di intestazione Content-Type e Authorization, che includono le nostre informazioni di autenticazione. Inoltre codifichiamo i parametri, che includono lo strumento (EUR / USD), le unità, il tipo di ordine e il lato (acquisto / vendita). Infine, effettuiamo la richiesta e salviamo la risposta:

import http.client
import urllib


class Execution(object):
    def __init__(self, domain, access_token, account_id):
        self.domain = domain
        self.access_token = access_token
        self.account_id = account_id
        self.conn = self.obtain_connection()

    def obtain_connection(self):
        return http.client.HTTPSConnection(self.domain)

    def execute_order(self, event):
        headers = {
            "Content-Type": "application/x-www-form-urlencoded",
            "Authorization": "Bearer " + self.access_token
        }
        params = urllib.urlencode({
            "instrument" : event.instrument,
            "units" : event.units,
            "type" : event.order_type,
            "side" : event.side
        })
        self.conn.request(
            "POST",
            "/v1/accounts/%s/orders" % str(self.account_id),
            params, headers
        )
        response = self.conn.getresponse().read()
        print(response)
Il componente più complesso del sistema di trading è l’oggetto StreamingForexPrices, che gestisce gli aggiornamenti dei prezzi di mercato da OANDA. Esistono due metodi: connect_to_stream e stream_to_queue. Il primo metodo utilizza la libreria requests di Python per connettersi a un socket di streaming con le intestazioni e i parametri appropriati. I parametri includono l’ID account e l’elenco degli strumenti  che devono essere monitorati per ricevere gli aggiornamenti (in questo caso è solo EUR/USD). Da notare la seguente riga:
resp = s.send(pre, stream=True, verify=False)

Questo indica che la connessione deve essere trasmessa in streaming e quindi mantenuta costantemente aperta per un periodo molto prolungato.

Il secondo metodo, stream_to_queue, tenta effettivamente di connettersi allo stream. Se la risposta non ha esito positivo (ovvero il codice di risposta non è HTTP 200), si esce e cede il controllo alla chiamante. Se ha successo si prova a caricare il pacchetto JSON ricevuto in un dizionario Python. Infine, converte il dizionario Python con lo strumento, bid / ask e timestamp in un TickEvent che viene inviato alla coda degli eventi:

import requests
import json

from event import TickEvent


class StreamingForexPrices(object):
    def __init__(
        self, domain, access_token,
        account_id, instruments, events_queue
    ):
        self.domain = domain
        self.access_token = access_token
        self.account_id = account_id
        self.instruments = instruments
        self.events_queue = events_queue

    def connect_to_stream(self):
        try:
            s = requests.Session()
            url = "https://" + self.domain + "/v1/prices"
            headers = {'Authorization' : 'Bearer ' + self.access_token}
            params = {'instruments' : self.instruments, 'accountId' : self.account_id}
            req = requests.Request('GET', url, headers=headers, params=params)
            pre = req.prepare()
            resp = s.send(pre, stream=True, verify=False)
            return resp
        except Exception as e:
            s.close()
            print("Caught exception when connecting to stream\n" + str(e))

    def stream_to_queue(self):
        response = self.connect_to_stream()
        if response.status_code != 200:
            return
        for line in response.iter_lines(1):
            if line:
                try:
                    msg = json.loads(line)
                except Exception as e:
                    print("Caught exception when converting message into json\n" + str(e))
                    return
                if msg.has_key("instrument") or msg.has_key("tick"):
                    print(msg)
                    instrument = msg["tick"]["instrument"]
                    time = msg["tick"]["time"]
                    bid = msg["tick"]["bid"]
                    ask = msg["tick"]["ask"]
                    tev = TickEvent(instrument, time, bid, ask)
                    self.events_queue.put(tev)

Disponiamo ora di tutti i componenti principali. Il passaggio finale è racchiudere tutto ciò che abbiamo scritto finora in un programma “principale”. L’obiettivo di questo file, noto come trading.py, è creare due thread separati, uno che esegue il gestore dei prezzi e l’altro che esegue il gestore del trading.

Perché abbiamo bisogno di due thread separati? In parole povere, stiamo eseguendo due pezzi di codice “separati”, entrambi in esecuzione continua. Se dovessimo creare un programma senza thread, il socket di streaming utilizzato per gli aggiornamenti dei prezzi non verrebbe mai “rilasciato”, cioè non si tornerebbe mai al codice principale e quindi non effettueremmo mai alcun trading. Allo stesso modo, se eseguissimo il ciclo trade(vedi sotto), non restituiremmo mai il controllo del flusso di esecuzione del codice al socket di streaming dei prezzi. Quindi abbiamo bisogno di più thread, uno per ogni componente, in modo che possano essere eseguiti in modo indipendente. Entrambi comunicheranno tra loro tramite la coda degli eventi.

Esaminiamo un po’ nel dettaglio. Creiamo due thread separati con le seguenti righe:

trade_thread = threading.Thread(target=trade, args=(events, strategy, execution))
price_thread = threading.Thread(target=prices.stream_to_queue, args=[])

Passiamo la funzione o il nome del metodo all’argomento della parola chiave target e quindi passiamo un iterabile (un elenco o una tupla) come argomento della parola chiave args, che corrispondo ai parametri d’ingresso del metodo / funzione specificato in “target”.

Infine iniziamo entrambi i thread con le seguenti righe:

trade_thread.start()
price_thread.start()

In questo modo siamo in grado di eseguire in modo indipendente e separato due segmenti di codice a ciclo infinito, che comunicano entrambi attraverso la coda degli eventi. Si noti che la libreria threading di Python non produce un vero ambiente multi-core multithread a causa dell’implementazione CPython di Python e del Global Interpreter Lock (GIL).

Esaminiamo nel dettaglio il resto del codice. Per prima cosa importiamo tutte le librerie necessarie tra cui queue, threading e time. Quindi importiamo tutti i file di codice descritti in precedenza. Personalmente preferisco capitalizzare qualsiasi impostazione di configurazione, è un’abitudine che ho acquisito lavorando con Django!

Dopodiché definiamo la funzione trade, che è stata descritta con il precedente pseudo-codice in python. Viene eseguito un ciclo “while” infinito (while True:) che esegue il polling continuo dalla coda degli eventi e salta il ciclo solo se viene trovato vuoto (nessun evento). Se viene rilevato un evento, che può essere un TickEvent o un OrderEvent e quindi viene chiamato il componente appropriato per eseguirlo. In questo caso si tratta di una strategia o di un gestore di esecuzione. Il ciclo quindi si ferma semplicemente per “heartbeat” di secondi (in questo caso 0,5 secondi) e si rincomincia il ciclo.

Infine, definiamo l’entrypoint principale del codice nella funzione __main__. In particolare, istanziamo la coda degli eventi e definiamo gli strumenti / unità. Creiamo quindi la classe di streaming dei prezzi StreamingForexPrices e successivamente il gestore di esecuzione Execution. Entrambi ricevono i necessari parametri di autenticazione forniti da OANDA durante la creazione di un account.

Quindi creiamo l’istanza TestRandomStrategy. Infine definiamo i due thread e poi li avviamo:

import queue
import threading
import time

from execution import Execution
from settings import STREAM_DOMAIN, API_DOMAIN, ACCESS_TOKEN, ACCOUNT_ID
from strategy import TestRandomStrategy
from data import StreamingForexPrices


def trade(events, strategy, execution):
    """
    Esegue un ciclo while infinito che effettua il polling 
    della coda degli eventi e indirizza ogni evento al 
    componente strategia del gestore di esecuzione. 
    Il ciclo si fermerà per "heartbeat" di alcuni secondi 
    e continuerà.
    """
    while True:
        try:
            event = events.get(False)
        except queue.Empty:
            pass
        else:
            if event is not None:
                if event.type == 'TICK':
                    strategy.calculate_signals(event)
                elif event.type == 'ORDER':
                    print("Executing order!")
                    execution.execute_order(event)
        time.sleep(heartbeat)


if __name__ == "__main__":
    heartbeat = 0.5  # Pausa di mezzo secondo
    events = queue.Queue()

    # Trading di 10000 unità di EUR/USD
    instrument = "EUR_USD"
    units = 10000

    # Creazione di una classe di streaming di prezzi da 
    # OANDA, assicurandosi di fornire le credenziali 
    # di autenticazione
    prices = StreamingForexPrices(
        STREAM_DOMAIN, ACCESS_TOKEN, ACCOUNT_ID,
        instrument, events
    )

    # Creazione di un gestore di esecuzione con parametri
    # di autenticazioni di OANDA
    execution = Execution(API_DOMAIN, ACCESS_TOKEN, ACCOUNT_ID)

    # Creazione del generatore di strategia/segnali, utilizzando
    # lo strumento, la quantità di unità e la coda di eventi come
    # parametri
    strategy = TestRandomStrategy(instrument, units, events)

    # Creazione di due thread separati: uno per il ciclo di trading
    # e uno per lo streaming dei prezzi di mercato
    trade_thread = threading.Thread(target=trade, args=(events, strategy, execution))
    price_thread = threading.Thread(target=prices.stream_to_queue, args=[])

    # Avvio di entrambi i thread
    trade_thread.start()
    price_thread.start()
Per eseguire il codice è sufficiente posizionare tutti i file nella stessa directory e eseguire il seguente codice sul terminale:
python trading.py
Si noti che per fermare il codice in questa fase è necessario un hard kill del processo Python, tramite “Ctrl-Z” o equivalente! Non ho inserito un thread aggiuntivo per gestire la ricerca di sys.exit() che sarebbe necessario per interrompere il codice in modo sicuro. Un potenziale modo per interrompere il codice su una macchina Ubuntu / Linux è digitare:
pgrep python
E quindi passare l’output di questo (un numero di processo) nel seguente comando:
kill -9 PROCESS_ID

Dove PROCESS_ID deve essere sostituito con l’output di pgrep. Da notare che questa NON è una pratica particolarmente corretta!

Negli articoli successivi creeremo un meccanismo di avvio / arresto più sofisticato che utilizza la supervisione del processo di Ubuntu per far funzionare il sistema di trading 24 ore su 24, 7 giorni su 7.

L’output dopo circa 30 secondi, a seconda dell’ora del giorno relativa ai principali orari di negoziazione per EUR / USD, per il codice descritto in precedenza, è il seguente:

{u'tick': {u'ask': 1.16283, u'instrument': u'EUR_USD', u'bid': 1.1627, u'time': u'2018-01-19T15:28:19.563256Z'}}
{u'tick': {u'ask': 1.16287, u'instrument': u'EUR_USD', u'bid': 1.16274, u'time': u'2018-01-19T15:28:28.021045Z'}}
{u'tick': {u'ask': 1.16287, u'instrument': u'EUR_USD', u'bid': 1.16273, u'time': u'2018-01-19T15:28:30.982725Z'}}
{u'tick': {u'ask': 1.16285, u'instrument': u'EUR_USD', u'bid': 1.16272, u'time': u'2018-01-19T15:28:52.493297Z'}}
{u'tick': {u'ask': 1.16283, u'instrument': u'EUR_USD', u'bid': 1.16272, u'time': u'2018-01-19T15:29:12.854066Z'}}
Executing order!
{
    "instrument" : "EUR_USD",
    "time" : "2018-01-19T15:29:14.000000Z",
    "price" : 1.16283,
    "tradeOpened" : {
        "id" : 821102691,
        "units" : 10000,
        "side" : "buy",
        "takeProfit" : 0,
        "stopLoss" : 0,
        "trailingStop" : 0
    },
    "tradesClosed" : [],
    "tradeReduced" : {}
}
{u'tick': {u'ask': 1.16284, u'instrument': u'EUR_USD', u'bid': 1.1627, u'time': u'2018-01-19T15:29:17.817401Z'}}
{u'tick': {u'ask': 1.16283, u'instrument': u'EUR_USD', u'bid': 1.1627, u'time': u'2018-01-19T15:29:17.920900Z'}}

Le prime cinque righe mostrano i dati del tick JSON restituiti da OANDA con prezzi bid / ask. Successivamente puoi vedere l’ordine di esecuzione! così come la risposta JSON restituita da OANDA confermando l’apertura di un’operazione di acquisto per 10.000 unità di EUR / USD e il prezzo a cui è stata raggiunta.

Questo continuerà a funzionare all’infinito fino a quando non si ucciderà il programma con un comando “Ctrl-Z” o simile.

Prossimi Passi

Negli articoli successivi apporteremo alcuni miglioramenti fondamentali, tra cui:

  • Strategie reali – Strategie forex corrette che generano segnali redditizi.
  • Infrastruttura per il live trading – Implementazione del server remoto e sistema di trading monitorato 24 ore su 24, 7 giorni su 7, con funzionalità di avvio / arresto.
  • Portafoglio e gestione del rischio – Portafoglio e sovrapposizioni di rischio per tutti gli ordini suggeriti dalla strategia.
  • Strategie multiple – costruzione di un portafoglio di strategie che si integrano nella copertura della gestione del rischio

Come con il backtester basato sugli eventi per il mercato azionario, dobbiamo anche creare un modulo di backtesting forex. Ciò ci consentirà di effettuare ricerche rapide e semplificare l’implementazione delle strategie.

 

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