In questa lezione descriviamo come per condurre ricerche su una strategia reale, ovvero una Strategia di Moving Average Crossover con Python e Pandas. Vediamo come applicare i concetti e il codice introdotti nel corso sulle basi del trading algoritmico con Python, dove abbiamo creato un ambiente di backtesting orientato agli oggetti.
Strategia di Moving Average Crossover
La strategia di Moving Average Crossover (ovvero incrocio della media mobile) rappresenta una semplice strategia di momentum molto diffusa. Molti la considerano come l’esempio di “Hello World” nel trading quantitativo.
Questa strategia opera solo in long e utilizza due simple moving average calcolate su periodi differenti della stessa serie storica. Il segnale di acquisto si verifica quando la media mobile semplice più breve incrocia dal basso, cioè supera, quella più lunga. Quando invece la media lunga supera quella breve, scatta la vendita dell’asset. Questa strategia produce buoni risultati quando una serie temporale segue un forte trend e poi rallenta gradualmente.
Per questo esempio, abbiamo scelto Apple, Inc. (AAPL) come serie temporale, con una media mobile breve a 100 giorni e una lunga a 400 giorni. Troviamo questo esempio nella libreria di trading algoritmico “zipline“. Per creare un backtester coerente, dobbiamo ottenere risultati simili a quelli di zipline, così da usarli come riferimento per validare il nostro test.
Implementazione
Prima di procedere, consigliamo di leggere la precedente lezione, dove abbiamo costruito la gerarchia dell’oggetto iniziale del nostro backtester. In caso contrario, il codice riportato qui non funzionerà. Per questa implementazione, abbiamo usato le seguenti librerie:
- Python – 3.7
- NumPy – 1.16.2
- Pandas – 0.24.2
- Matplotlib – 3.0.3
Per eseguire ma_cross.py
, servono i moduli definiti nel file backtest.py
del tutorial precedente. Iniziamo importando i moduli 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
Generazione dei Segnali
Come nel precedente tutorial, questa strategia eredita la classe astratta <code>Strategy</code> e definisce la classe <code>MovingAverageCrossStrategy</code>, che include tutti i dettagli per generare segnali quando le medie mobili di AAPL si incrociano.
L’oggetto utilizza una <code>short_window</code> e una <code>long_window</code> per l’elaborazione. I valori predefiniti sono pari rispettivamente a 100 giorni e 400 giorni, gli stessi parametri impiegati nell’esempio con zipline.
La funzione <code>rolling_mean</code>, applicata a <code>bars[‘Close’]</code> (i prezzi di chiusura di AAPL), calcola le medie mobili. Dopo aver ottenuto le singole medie mobili, il codice genera la serie di segnali assegnando alla colonna il valore 1,0 quando la media mobile breve supera quella lunga, oppure 0,0 negli altri casi. Con queste informazioni, si costruiscono gli ordini <code>positions</code> 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
Gestione delle Posizioni
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.
Esecuzione del Backtest
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: 
Conclusione
Come si può vedere, la strategia genera perdite durante questo periodo, con cinque operazioni di apertura e chiusura della posizione. Questo risultato non sorprende, considerando l’andamento dell’AAPL nel periodo analizzato, caratterizzato da un leggero calo iniziale seguito da una crescita significativa a partire dal 1998. L’intervallo di take profit associato ai segnali della media mobile risulta piuttosto ampio e ha ridotto il profitto finale dell’operazione, che altrimenti avrebbe potuto rendere la strategia redditizia.
Nelle prossime lezioni svilupperemo uno strumento più avanzato per analizzare le performance e illustreremo come ottimizzare i take profit dei singoli segnali basati sulla media mobile.
Il codice completo presentato in questa lezione, parte del pacchetto python per il backtest TQBacktest, è disponibile nel seguente repository GitHub: https://github.com/tradingquant-it/TQBacktest.”