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()
%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:
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