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 Backtest
e 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 Backtest
, SimulatedExecution
, Portfolio
, MovingAverageCrossStrategy
e 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:
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