In questa lezione mostriamo come effettuare il backtest di una strategia di Moving Average Crossover utilizzando il sistema di trading TQTradingSystem, che abbiamo descritto nel corso Trading System Event Driven con Python. 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.
La 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:
import datetime
import numpy as np
import pandas as pd
import statsmodels.api as sm
import datetime
import numpy as np
import pandas as pd
import statsmodels.api as sm
from strategy.strategy import Strategy
from event.event import SignalEvent
from backtest.backtest import Backtest
from data.data import HistoricCSVDataHandler
from execution.execution import SimulatedExecutionHandler
from portfolio.portfolio import Portfolio
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.
L’attributo finale, bought, viene utilizzato per indicare all’oggetto Strategy quando il backtest è effettivamente “a mercato”. I segnali di ingresso vengono generati solo se è “OUT” e i segnali di uscita vengono generati solo se è “LONG” o “SHORT”:
class MovingAverageCrossStrategy(Strategy):
"""
Esegue una strategia base di Moving Average Crossover tra due
medie mobile semplici, una breve e una lunga. Le finestre brevi / lunghe
sono rispettivamente di 100/400 periodi.
"""
def __init__(self, bars, events, short_window=100, long_window=400):
"""
Initializza la strategia di Moving Average Cross.
Parametri:
bars - L'oggetto DataHandler object che fornisce le barre dei prezzi
events - L'oggetto Event Queue.
short_window - Il periodo per la media mobile breve.
long_window - Il periodo per la media mobile lunga.
"""
self.bars = bars
self.symbol_list = self.bars.symbol_list
self.events = events
self.short_window = short_window
self.long_window = long_window
# Impostato a True se la strategia è a mercato
self.bought = self._calculate_initial_bought()
def _calculate_initial_bought(self):
"""
Aggiunge keys per ogni simbolo al dizionario bought e le
imposta a 'OUT'.
"""
bought = {}
for s in self.symbol_list:
bought[s] = 'OUT'
return bought
Il cuore della strategia risiede nel metodo calculate_signals. Questo metodo reagisce a un oggetto MarketEvent 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 degli eventi in ciascuna delle rispettive situazioni e quindi aggiornando l’attributo “bought” (per ogni simbolo) in modo che sia rispettivamente “LONG” o “SHORT”. Poiché questa è una strategia solo long, non prenderemo in considerazione le posizioni “SHORT”:
def calculate_signals(self, event):
"""
Genera un nuovo set di segnali basato sull'incrocio della
SMA di breve periodo con quella a lungo periodo che
significa un'entrata long e viceversa per un'entrata short.
Parametri
event - Un oggetto MarketEvent.
"""
if event.type == 'MARKET':
for s in self.symbol_list:
bars = self.bars.get_latest_bars_values(
s, "adj_close", N=self.long_window
)
bar_date = self.bars.get_latest_bar_datetime(s)
if bars is not None and bars != []:
short_sma = np.mean(bars[-self.short_window:])
long_sma = np.mean(bars[-self.long_window:])
symbol = s
dt = datetime.datetime.utcnow()
sig_dir = ""
if short_sma > long_sma and self.bought[s] == "OUT":
print("LONG: %s" % bar_date)
sig_dir = 'LONG'
signal = SignalEvent(1, symbol, dt, sig_dir, 1.0)
self.events.put(signal)
self.bought[s] = 'LONG'
elif short_sma < long_sma and self.bought[s] == "LONG":
print("SHORT: %s" % bar_date)
sig_dir = 'EXIT'
signal = SignalEvent(1, symbol, dt, sig_dir, 1.0)
self.events.put(signal)
self.bought[s] = 'OUT'
Concludiamo così l’implementazione dell’oggetto MovingAverageCrossStrategy. Per completare il sistema di backtest, implementiamo il metodo __main__ che configura ed esegue il backtest.
Esecuzione del backtest
Innanzitutto, bisogna assicursi di modificare il valore di csv_dir con percorso assoluto della directory dove si trovano i file CSV per i dati finanziari. Scarichiamo i dati storici di AAPL (da Yahoo Finance) dal 1° gennaio 1990 al 1° gennaio 2012 e li salviamo nel percorso indicato. Usiamo yfinance per scaricare e memorizzare facilmente questi dati. E’ quindi necessario inserire questo file nel percorso indicato dalla funzione principale in csv_dir.
La funzione __main__ crea semplicemente un’istanza di un nuovo oggetto di backtest e quindi chiama il metodo simulate_trading per eseguire il backtest:
# mac.py
if __name__ == "__main__":
csv_dir = '/path/to/your/csv/file' # DA MODIFICARE
symbol_list = ['AAPL']
initial_capital = 100000.0
heartbeat = 0.0
start_date = datetime.datetime(1990, 1, 1, 0, 0, 0)
backtest = Backtest(csv_dir, symbol_list, initial_capital, heartbeat, start_date,
HistoricCSVDataHandler, SimulatedExecutionHandler, Portfolio,
MovingAverageCrossStrategy)
backtest.simulate_trading()
Per eseguire il Backtest di una strategia di moving average crossover bisogna avere aver già configurato un ambiente Python e poi navigare nella directory in cui è memorizzato il codice. Lo script restituisce il seguente listato
..
..
3029
3030
Creating summary stats...
Creating equity curve...
AAPL cash commission total returns equity_curve drawdown
datetime
2001-12-18 0 99211 13 99211 0 0.99211 0.025383
2001-12-19 0 99211 13 99211 0 0.99211 0.025383
2001-12-20 0 99211 13 99211 0 0.99211 0.025383
2001-12-21 0 99211 13 99211 0 0.99211 0.025383
2001-12-24 0 99211 13 99211 0 0.99211 0.025383
2001-12-26 0 99211 13 99211 0 0.99211 0.025383
2001-12-27 0 99211 13 99211 0 0.99211 0.025383
2001-12-28 0 99211 13 99211 0 0.99211 0.025383
2001-12-31 0 99211 13 99211 0 0.99211 0.025383
2001-12-31 0 99211 13 99211 0 0.99211 0.025383
[(’Total Return’, ’-0.79%’),
(’Sharpe Ratio’, ’-0.09’),
(’Max Drawdown’, ’2.56%’),
(’Drawdown Duration’, ’2312’)]
Signals: 10
Orders: 10
Fills: 10
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 TQTradingSystem. 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 questa lezione, basato sul sistema di trading event-driven TQTradingSystem, è disponibile nel seguente repository GitHub: https://github.com/tradingquant-it/TQTradingSystem.”
