In questo articolo mostriamo come effettuare il backtest di una strategia di Moving Average Crossover utilizzando il sistema di trading basato sugli eventi DataTrader, che abbiamo descritto negli articoli precedenti. In particolare, creiamo le curve equity utilizzando gli importi nozionali di portafoglio, simulando così concetti di margine e leva finanziaria. Adottiamo un approccio più realistico rispetto a quello vettoriale o basato sui rendimenti.
Applichiamo questa prima strategia con dati disponibili gratuitamente da Yahoo Finance, Google Finance o Quandl. Si tratta di una strategia adatta ai trader algoritmici di lungo periodo che desiderano analizzare sia la generazione del segnale di trade, sia l’intero sistema end-to-end. Queste strategie offrono spesso Sharpe Ratio più contenuti, ma risultano molto semplici da implementare ed eseguire.
Backtest di una Strategia di Moving Average Crossover
AbstractStrategy
per definire MovingAverageCrossStrategy
. All’interno inseriamo la logica di calcolo delle medie mobili semplici e la generazione dei segnali di trading. Implementiamo inoltre la funzione run
, che carica l’oggetto TradingSession
e ne gestisce l’esecuzione completa.
Come primo passo, importiamo correttamente i componenti necessari. Inseriamo quasi tutti gli oggetti che compongono il motore di backtesting event-driven:
from collections import deque
import datetime
import numpy as np
from datatrader import settings
from datatrader.strategy.base import AbstractStrategy
from datatrader.event import SignalEvent, EventType
from datatrader.compat import queue
from datatrader.trading_session import TradingSession
Logica della strategia
Procediamo ora con la creazione della classe MovingAverageCrossStrategy
. Per la strategia utilizziamo le barre generate da DataHandler
, gli eventi gestiti da Event Queue e i periodi di ricerca delle medie mobili semplici. Per questa implementazione, scegliamo 100 e 400 come periodi di ricerca brevi e lunghi.
Usiamo l’attributo invested
per indicare all’oggetto AbstractStrategy
quando il backtest si trova effettivamente “a mercato”. Generiamo segnali di ingresso solo se siamo “OUT” e segnali di uscita solo se siamo “LONG” o “SHORT”.
..
class MovingAverageCrossStrategy(AbstractStrategy):
"""
Richiede:
ticker - Il simbolo ticker utilizzato per le medie mobili
events_queue - Un gestore per la coda degli eventi di sistema
short_window - Periodo per media mobile breve
long_window - Periodo per media mobile lunga
"""
def __init__(
self, ticker,
events_queue,
short_window=100,
long_window=300,
base_quantity=100
):
self.ticker = ticker
self.events_queue = events_queue
self.short_window = short_window
self.long_window = long_window
self.base_quantity = base_quantity
self.bars = 0
self.invested = False
self.sw_bars = deque(maxlen=self.short_window)
self.lw_bars = deque(maxlen=self.long_window)
Il cuore della strategia risiede nel metodo calculate_signals
. Questo metodo reagisce a un oggetto BarEvent
e recupera i prezzi di chiusura delle ultime N barre, dove N equivale al periodo di ricerca più lungo. Calcoliamo quindi le medie mobili semplici di breve e lungo periodo. Entriamo a mercato (long) quando la media breve supera quella lunga; usciamo dal mercato quando la media lunga supera quella breve.
Gestiamo questa logica inserendo un oggetto SignalEvent
nella coda degli eventi in ciascuna delle rispettive condizioni, aggiornando quindi l’attributo “invested” su “BUY” o “SELL” di conseguenza.
..
def calculate_signals(self, event):
if (
event.type == EventType.BAR and
event.ticker == self.ticker
):
# Aggiunge l'ultimo prezzo di chiusura aggiustato alle barre
# delle finestre dei periodi brevi e lunghi
self.lw_bars.append(event.adj_close_price)
if self.bars > self.long_window - self.short_window:
self.sw_bars.append(event.adj_close_price)
# Sono presenti sufficienti barre per il trading
if self.bars > self.long_window:
# Calcola le medie mobili semplici
short_sma = np.mean(self.sw_bars)
long_sma = np.mean(self.lw_bars)
# Segnali di trading baasati sull'incrocio delle medie mobili
if short_sma > long_sma and not self.invested:
print("LONG %s: %s" % (self.ticker, event.time))
signal = SignalEvent(
self.ticker, "BOT",
suggested_quantity=self.base_quantity
)
self.events_queue.put(signal)
self.invested = True
elif short_sma < long_sma and self.invested:
print("SHORT %s: %s" % (self.ticker, event.time))
signal = SignalEvent(
self.ticker, "SLD",
suggested_quantity=self.base_quantity
)
self.events_queue.put(signal)
self.invested = False
self.bars += 1
Concludiamo così l’implementazione dell’oggetto MovingAverageCrossStrategy
. Per completare il sistema di backtest, implementiamo il metodo run
che configura ed esegue il backtest.
Esecuzione del backtest
Nel file di configurazione specifichiamo il valore di CSV_DATA_DIR
con il percorso relativo della directory contenente i file CSV dei dati finanziari che vogliamo testare. Scarichiamo i dati storici di AAPL (da Yahoo Finance) dal 1° gennaio 2000 al 1° gennaio 2014 e li salviamo nel percorso indicato. Usiamo yfinance
per scaricare e memorizzare facilmente questi dati.
La funzione __main__
carica i file di configurazione e avvia run
per eseguire l’intero backtest:
..
def run(config, testing, tickers, filename):
# Informazioni sul Backtest
title = ['Moving Average Crossover Example on AAPL: 100x300']
initial_equity = 10000.0
start_date = datetime.datetime(2000, 1, 1)
end_date = datetime.datetime(2014, 1, 1)
# Uso della strategia MAC
events_queue = queue.Queue()
strategy = MovingAverageCrossStrategy(
tickers[0], events_queue,
short_window=100,
long_window=300
)
# Setup del backtest
backtest = TradingSession(
config, strategy, tickers,
initial_equity, start_date, end_date,
events_queue, title=title,
benchmark=tickers[1],
)
results = backtest.start_trading(testing=testing)
return results
if __name__ == "__main__":
# Dati di configurazione
testing = False
config = settings.from_file(
settings.DEFAULT_CONFIG_FILENAME, testing
)
tickers = ["AAPL", "SPY"]
filename = None
run(config, testing, tickers, filename)
print("")
Running Backtest...
LONG AAPL: 2002-02-08 00:00:00
SHORT AAPL: 2002-08-14 00:00:00
LONG AAPL: 2003-07-15 00:00:00
SHORT AAPL: 2008-10-17 00:00:00
LONG AAPL: 2009-07-28 00:00:00
SHORT AAPL: 2013-02-19 00:00:00
LONG AAPL: 2013-11-29 00:00:00
---------------------------------
Backtest complete.
Sharpe Ratio: 0.64
Max Drawdown: 40.94%
Il backtest e le performance di questa strategia sono visualizzati nella seguente figura:

Conclusioni
Abbiamo visto come effettuare il backtest di una strategia di Moving Average Crossover in modo semplice con il framework DataTrader. E’ evidente che i rendimenti e lo Sharpe Ratio non sono stellari per le azioni AAPL su questo particolare set di indicatori tecnici!
Chiaramente abbiamo molto lavoro da fare nella prossima serie di strategie per trovare un sistema in grado di generare performance positive.
Il codice completo presentato in questo articolo, basato sul framework di trading quantitativo event-driven DataTrader, è disponibile nel seguente repository GitHub: https://github.com/tradingquant-it/DataTrader.”