Strategia di Moving Average Crossover in Python con Pandas

Nel precedente articolo relativo allo Sviluppo di un BackTesting Vettoriale con Python e Pandas abbiamo creato un ambiente di backtesting orientato agli oggetti e testato su una strategia di previsione casuale. In questo articolo utilizzeremo gli strumenti che abbiamo introdotto per condurre ricerche su una strategia reale, ovvero il Moving Average Crossover su AAPL.

Strategia di Moving Average Crossover

La strategia di Moving Average Crossover (ovvero incrocio della media mobile) è una semplice strategia di momentum estremamente nota. È spesso considerata come l’esempio di “Hello World” per il trading quantitativo.

La strategia qui descritta è solo long. Si considerano due simple moving average separate, costruite su diversi periodi, di una particolare serie storica. I segnali per l’acquisto dell’asset si verificano quando la media mobile semplice più breve incrocia dal basso, cioè supera, la media mobile semplice più lunga. Se successivamente la media più lunga supera la media più breve, l’asset viene venduto. La strategia funziona bene quando una serie temporale entra in un periodo di forte trend e poi rallenta lentamente.

Per questo esempio, ho scelto Apple, Inc. (AAPL) come serie temporali, con una media mobile breve di 100 giorni e una media mobile lunga di 400 giorni. Questo è l’esempio presente nella libreria di trading algoritmico “zipline“. Quindi, se vogliamo implementare il nostro backtester, dobbiamo assicurarci che corrisponda ai risultati ottenuti da zipline, come metodo base per la convalidare il test.

Implementazione

Assicurati di aver letto il precedente tutorial, che descrive come viene costruita la gerarchia dell’oggetto iniziale del nostro backtester, altrimenti il codice sottostante non potrà funzionerà. Per questa particolare implementazione ho usato le seguenti librerie:

  • Python – 3.7
  • NumPy – 1.16.2
  • Pandas – 0.24.2
  • Matplotlib – 3.0.3

L’implementazione di ma_cross.py richiede il file backtest.py del precedente tutorial. Il primo passaggio consiste nell’importare i moduli e gli oggetti necessari:

            # ma_cross.py

import datetime
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

from pandas_datareader.data import DataReader
from backtest import Strategy, Portfolio
        

Come nel precedente tutorial, si eredità la classe astratta della strategia (Strategy) per produrre la classe MovingAverageCrossStrategy, che contiene tutti i dettagli per generare  segnali quando le medie mobili di AAPL si incrociano l’una sull’altra.

L’oggetto richiede una short_window e una long_window su cui operare. I valori sono stati impostati com valori predefiniti, rispettivamente pari a 100 giorni e 400 giorni, che sono gli stessi parametri utilizzati nell’esempio di zipline.

Le medie mobili vengono create tramite la funzione rolling_mean applicata a bars['Close'], cioè i prezzi di chiusura del titolo AAPL. Una volta che le singole medie mobili sono state calcolate, la serie di segnali viene generata impostando la colonna uguale a 1,0 quando la media mobile breve è maggiore della media mobile lunga o 0,0 in caso contrario. Da queste informazioni si può generare gli ordini positions per rappresentare i segnali di trading.

            # ma_cross.py

class MovingAverageCrossStrategy(Strategy):
    """
    Richiede:
    symbol - Un simbolo di un titolo azionario su cui formare una strategia.
    bars - Un DataFrame di barre per il simbolo.
    short_window - Periodo di ricerca per media mobile breve.
    long_window - Periodo di ricerca per media mobile lunga.
    """

    def __init__(self, symbol, bars, short_window=100, long_window=400):
        self.symbol = symbol
        self.bars = bars

        self.short_window = short_window
        self.long_window = long_window

    def generate_signals(self):
        """
        Restituisce il DataFrame dei simboli che contiene i segnali
        per andare long, short o flat (1, -1 o 0).
        """
        signals = pd.DataFrame(index=self.bars.index)
        signals['signal'] = 0.0

        # Crea l'insieme di medie mobili semplici di breve e di
        # lungo periodo
        signals['short_mavg'] = pd.rolling_mean(self.bars['Close'], self.short_window, min_periods=1)
        signals['long_mavg'] = pd.rolling_mean(self.bars['Close'], self.long_window, min_periods=1)

        # Crea un "segnale" (investito o non investito) quando la media mobile corta incrocia la media
        # mobile lunga, ma solo per il periodo maggiore della finestra della media mobile più breve
        signals['signal'][self.short_window:] = np.where(signals['short_mavg'][self.short_window:]
            > signals['long_mavg'][self.short_window:], 1.0, 0.0)

        # Si calcola la differenza dei segnali per generare gli effettivi ordini di trading
        signals['positions'] = signals['signal'].diff()

        return signals
        

La classe MarketOnClosePortfolio è una classe derivata dalla classe astratta Portfolio, presente in backtest.py. È quasi identico all’implementazione descritta nel tutorial precedente, con l’eccezione che le operazioni vengono ora eseguite su base Close-to-Close, piuttosto che Open-to-Open. Ho trascritto tutto il codice completo per rendere autonomo questo tutorial:

            # ma_cross.py

class MarketOnClosePortfolio(Portfolio):
    """
    Incapsula la nozione di un portafoglio di posizioni basato
    su una serie di segnali forniti da una strategia.

    Richiede:
    symbol - Un simbolo di un titolo azionario che costituisce la base del portafoglio.
    bars - Un DataFrame di barre per un set di simboli.
    signals - Un DataFrame panda di segnali (1, 0, -1) per ogni simbolo.
    initial_capital - L'importo in contanti all'inizio del portafoglio.
    """

    def __init__(self, symbol, bars, signals, initial_capital=100000.0):
        self.symbol = symbol
        self.bars = bars
        self.signals = signals
        self.initial_capital = float(initial_capital)
        self.positions = self.generate_positions()

    def generate_positions(self):
        positions = pd.DataFrame(index=self.signals.index).fillna(0.0)
        # Questa strategia compra 100 azioni
        positions[self.symbol] = 100 * self.signals['signal']  
        return positions

    def backtest_portfolio(self):
        portfolio = pd.DataFrame(index=self.signals.index).fillna(0.0)
        pos_diff = self.positions[self.symbol].diff()

        portfolio['holdings'] = (self.positions[self.symbol] * self.bars['Close'])
        portfolio['cash'] = self.initial_capital - (pos_diff * self.bars['Close']).cumsum()

        portfolio['total'] = portfolio['cash'] + portfolio['holdings']
        portfolio['returns'] = portfolio['total'].pct_change()
        return portfolio
        

Ora che sono state definite le classi MovingAverageCrossStrategy e MarketOnClosePortfolio, verrà chiamata una funzione __main__ per collegare insieme le funzionalità delle due classi. Inoltre, la performance della strategia sarà esaminato attraverso un grafico della curva equity.

L’oggetto DataReader di pandas scarica i prezzi OHLCV del titolo AAPL per il periodo che va dal 1 gennaio 1990 al 1 gennaio 2002, in seguito di crea il DataFrame signals per generare i segnali long-only. Successivamente il portafoglio è generato con una base di capitale iniziale di 100.000 USD e i rendimenti sono calcolati sulla curva equity.

Il passaggio finale consiste nell’utilizzare matplotlib per tracciare un grafico a due figure con i prezzi di AAPL, sovrapposti con le medie mobili e i segnali di acquisto / vendita, nonché la curva equity con gli stessi segnali di acquisto / vendita. Il codice di plotting è stato preso (e modificato) dall‘esempio di zipline.

            # ma_cross.py

if __name__ == "__main__":
    # Download delle barre giornaliere di AAPL da Yahoo Finance per il periodo
    # Dal 1 ° gennaio 1990 al 1 ° gennaio 2002 - Questo è un esempio tratto da ZipLine
    symbol = 'AAPL'
    bars = DataReader(symbol, "yahoo", datetime.datetime(1990, 1, 1), datetime.datetime(2002, 1, 1))

    # Crea un'istanza della classe MovingAverageCrossStrategy con un periodo della media
    # mobile breve pari a 100 giorni e un periodo per la media lunga pari a 400 giorni
    mac = MovingAverageCrossStrategy(symbol, bars, short_window=100, long_window=400)
    signals = mac.generate_signals()

    # Crea un portofoglio per AAPL, con $100,000 di capitale iniziale
    portfolio = MarketOnClosePortfolio(symbol, bars, signals, initial_capital=100000.0)
    returns = portfolio.backtest_portfolio()

    # Visualizza 2 grafici per i trade e la curva di equity
    fig = plt.figure()
    fig.patch.set_facecolor('white')  # Imposta il colore di fondo a bianco
    ax1 = fig.add_subplot(211, ylabel='Price in $')

    # Visualizza il grafico dei prezzi di chiusura di AAPL con la media mobile
    bars['Close'].plot(ax=ax1, color='r', lw=2.)
    signals[['short_mavg', 'long_mavg']].plot(ax=ax1, lw=2.)

    # Visualizza i trade "buy" su AAPL
    ax1.plot(signals.loc[signals.positions == 1.0].index,
             signals.short_mavg[signals.positions == 1.0],
             '^', markersize=10, color='m')

    # Visualizza i trade "sell" su AAPL
    ax1.plot(signals.loc[signals.positions == -1.0].index,
             signals.short_mavg[signals.positions == -1.0],
             'v', markersize=10, color='k')

    # Visualizza la curva di equity in dollari
    ax2 = fig.add_subplot(212, ylabel='Portfolio value in $')
    returns['total'].plot(ax=ax2, lw=2.)

    # Visualizza i trade "buy" e "sell" su la curva di equity
    ax2.plot(returns.loc[signals.positions == 1.0].index,
             returns.total[signals.positions == 1.0],
             '^', markersize=10, color='m')
    ax2.plot(returns.loc[signals.positions == -1.0].index,
             returns.total[signals.positions == -1.0],
             'v', markersize=10, color='k')

    # Stampa il grafico
    fig.show()
        
Il grafico output del codice è il seguente. Ho fatto uso del comando IPython %paste per inserirlo direttamente nella console IPython di Ubuntu, in modo che l’output grafico rimanesse visibile. Gli uptick rosa rappresentano l’acquisto del titolo, mentre i downtick neri rappresentano la vendita:
Performance di un Moving Average Crossover su AAPL tra il 01/01/1990 e il 01/01/2002
Come si può vedere la strategia perde denaro lungo questo periodo, con cinque trade di apertura e chiusra della posizione. Ciò non sorprende, tenuto conto del comportamento dell’AAPL nel periodo in esame, che era leggermente in calo, seguito da un significativo aumento a partire dal 1998. Il periodo di take profit dei segnali della media mobile è piuttosto ampio e ha influito sul profitto finale dell’operazione. , che altrimenti potrebbe l’avrebbe resa una strategia redditizia. Negli articoli successivi creeremo uno strumento più sofisticato per analizzare le prestazioni, oltre a descrivere come ottimizzare i take profit dei singoli segnali della media mobile.

 

Per il codice completo riportato in questo articolo utilizzando il modulo di backtesting vettoriale VectorBacktest si può consultare il seguente repository di github:
https://github.com/datatrading-info/VectorBacktest

Scroll to Top