Backtest di una Strategia di Moving Average Crossover

Backtest di una Strategia di Moving Average

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

Sosteniamo con convinzione i sistemi di trading basati sull’incrocio delle medie mobili perché rappresentano la prima strategia non banale, estremamente utile per testare una nuova implementazione di un motore di backtesting. Su timeframe giornalieri e archi temporali di più anni, otteniamo pochi segnali per singolo titolo, facilitando così la verifica manuale del comportamento del sistema. Per generare questa simulazione con il codice di backtesting basato sugli eventi, creiamo una sottoclasse dell’oggetto 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()
				
			
Poiché la strategia inizia fuori dal mercato, impostiamo il valore iniziale “bought” su “OUT”, per ogni simbolo:
				
					
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:

SMA_performance_trading_algoritmico

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.”

Torna in alto