Test en arrière d'un croisement de moyenne mobile en Python avec des pandas

Auteur:La bonté, Créé: 2019-03-27 15:11:40, Mis à jour:

Dans cet article, nous utiliserons le mécanisme que nous avons introduit pour effectuer des recherches sur une stratégie réelle, à savoir le croisement de la moyenne mobile sur AAPL.

Stratégie de croisement des moyennes mobiles

La technique de croisement des moyennes mobiles est une stratégie de dynamique simpliste extrêmement bien connue.

La stratégie décrite ici est longue seulement. Deux filtres simples mobiles distincts sont créés, avec des périodes de rétrospective variables, d'une série temporelle particulière. Les signaux d'achat de l'actif se produisent lorsque la moyenne mobile de rétrospective plus courte dépasse la moyenne mobile de rétrospective plus longue. Si la moyenne plus longue dépasse ensuite la moyenne plus courte, l'actif est revendu.

Pour cet exemple, j'ai choisi Apple, Inc. (AAPL) comme série chronologique, avec un court lookback de 100 jours et un long lookback de 400 jours. C'est l'exemple fourni par la bibliothèque de trading algorithmique zipline.

Mise en œuvre

Assurez-vous de suivre le tutoriel précédent ici, qui décrit comment la hiérarchie d'objets initiale pour le backtester est construite, sinon le code ci-dessous ne fonctionnera pas.

  • Python - 2.7.3
  • NumPy - 1.8.0
  • Les pandas - 0.12.0
  • Le projet de loi est en cours de révision.

La mise en œuvre de ma_cross.py nécessitebacktest.pyLa première étape consiste à importer les modules et objets nécessaires:

# ma_cross.py

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

from pandas.io.data import DataReader
from backtest import Strategy, Portfolio

Comme dans le tutoriel précédent, nous allons sous-classer la classe de base abstraite Stratégie pour produire MovingAverageCrossStrategy, qui contient tous les détails sur la façon de générer les signaux lorsque les moyennes mobiles d'AAPL se croisent.

L'objet nécessite une short_window et une long_window sur lesquelles opérer. Les valeurs ont été définies par défaut à 100 jours et 400 jours respectivement, qui sont les mêmes paramètres utilisés dans l'exemple principal de zipline.

Les moyennes mobiles sont créées en utilisant la fonction panda rolling_mean sur les barres [Close] prix de clôture des actions AAPL. Une fois les moyennes mobiles individuelles construites, la série de signaux est générée en définissant la colonne égale à 1,0 lorsque la moyenne mobile courte est supérieure à la moyenne mobile longue, ou à 0,0 autrement.

# ma_cross.py

class MovingAverageCrossStrategy(Strategy):
    """    
    Requires:
    symbol - A stock symbol on which to form a strategy on.
    bars - A DataFrame of bars for the above symbol.
    short_window - Lookback period for short moving average.
    long_window - Lookback period for long moving average."""

    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):
        """Returns the DataFrame of symbols containing the signals
        to go long, short or hold (1, -1 or 0)."""
        signals = pd.DataFrame(index=self.bars.index)
        signals['signal'] = 0.0

        # Create the set of short and long simple moving averages over the 
        # respective periods
        signals['short_mavg'] = pd.rolling_mean(bars['Close'], self.short_window, min_periods=1)
        signals['long_mavg'] = pd.rolling_mean(bars['Close'], self.long_window, min_periods=1)

        # Create a 'signal' (invested or not invested) when the short moving average crosses the long
        # moving average, but only for the period greater than the shortest moving average window
        signals['signal'][self.short_window:] = np.where(signals['short_mavg'][self.short_window:] 
            > signals['long_mavg'][self.short_window:], 1.0, 0.0)   

        # Take the difference of the signals in order to generate actual trading orders
        signals['positions'] = signals['signal'].diff()   

        return signals

La sous-classe MarketOnClosePortfolio est dérivée de la sous-classe Portfolio, qui se trouve dansbacktest.py. Il est presque identique à la mise en œuvre décrite dans le tutoriel précédent, à l'exception que les transactions sont maintenant effectuées sur une base Close-to-Close, plutôt que sur une base Open-to-Open. Pour plus de détails sur la définition de l'objet Portfolio, voir le tutoriel précédent.

# ma_cross.py

class MarketOnClosePortfolio(Portfolio):
    """Encapsulates the notion of a portfolio of positions based
    on a set of signals as provided by a Strategy.

    Requires:
    symbol - A stock symbol which forms the basis of the portfolio.
    bars - A DataFrame of bars for a symbol set.
    signals - A pandas DataFrame of signals (1, 0, -1) for each symbol.
    initial_capital - The amount in cash at the start of the portfolio."""

    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=signals.index).fillna(0.0)
        positions[self.symbol] = 100*signals['signal']   # This strategy buys 100 shares
        return positions
                    
    def backtest_portfolio(self):
        portfolio = self.positions*self.bars['Close']
        pos_diff = self.positions.diff()

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

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

Maintenant que les classes MovingAverageCrossStrategy et MarketOnClosePortfolio ont été définies, unele principalLa fonctionnalité sera appelée pour lier toutes les fonctionnalités entre elles.

L'objet panda DataReader télécharge les prix OHLCV des actions AAPL pour la période du 1er janvier 1990 au 1er janvier 2002, auquel moment les signaux DataFrame sont créés pour générer les signaux longs.

L'étape finale consiste à utiliser matplotlib pour tracer un graphique à deux chiffres des deux prix AAPL, superposés aux moyennes mobiles et aux signaux d'achat / vente, ainsi qu'à la courbe des actions avec les mêmes signaux d'achat / vente.

# ma_cross.py

if __name__ == "__main__":
    # Obtain daily bars of AAPL from Yahoo Finance for the period
    # 1st Jan 1990 to 1st Jan 2002 - This is an example from ZipLine
    symbol = 'AAPL'
    bars = DataReader(symbol, "yahoo", datetime.datetime(1990,1,1), datetime.datetime(2002,1,1))

    # Create a Moving Average Cross Strategy instance with a short moving
    # average window of 100 days and a long window of 400 days
    mac = MovingAverageCrossStrategy(symbol, bars, short_window=100, long_window=400)
    signals = mac.generate_signals()

    # Create a portfolio of AAPL, with $100,000 initial capital
    portfolio = MarketOnClosePortfolio(symbol, bars, signals, initial_capital=100000.0)
    returns = portfolio.backtest_portfolio()

    # Plot two charts to assess trades and equity curve
    fig = plt.figure()
    fig.patch.set_facecolor('white')     # Set the outer colour to white
    ax1 = fig.add_subplot(211,  ylabel='Price in $')
    
    # Plot the AAPL closing price overlaid with the moving averages
    bars['Close'].plot(ax=ax1, color='r', lw=2.)
    signals[['short_mavg', 'long_mavg']].plot(ax=ax1, lw=2.)

    # Plot the "buy" trades against AAPL
    ax1.plot(signals.ix[signals.positions == 1.0].index, 
             signals.short_mavg[signals.positions == 1.0],
             '^', markersize=10, color='m')

    # Plot the "sell" trades against AAPL
    ax1.plot(signals.ix[signals.positions == -1.0].index, 
             signals.short_mavg[signals.positions == -1.0],
             'v', markersize=10, color='k')

    # Plot the equity curve in dollars
    ax2 = fig.add_subplot(212, ylabel='Portfolio value in $')
    returns['total'].plot(ax=ax2, lw=2.)

    # Plot the "buy" and "sell" trades against the equity curve
    ax2.plot(returns.ix[signals.positions == 1.0].index, 
             returns.total[signals.positions == 1.0],
             '^', markersize=10, color='m')
    ax2.plot(returns.ix[signals.positions == -1.0].index, 
             returns.total[signals.positions == -1.0],
             'v', markersize=10, color='k')

    # Plot the figure
    fig.show()

La sortie graphique du code est la suivante. J'ai utilisé la commande IPython %paste pour le mettre directement dans la console IPython alors que j'étais dans Ubuntu, de sorte que la sortie graphique reste visible. Les pics en rose représentent l'achat de l'action, tandis que les pics en noir représentent sa revente:imgPerformance croisée moyenne mobile de l'AAPL de 1990 à 2002

Comme on peut le constater, la stratégie perd de l'argent au cours de la période, avec cinq transactions aller-retour. Cela n'est pas surprenant étant donné le comportement d'AAPL au cours de la période, qui a connu une légère tendance à la baisse, suivie d'une forte hausse à partir de 1998.

Dans les articles suivants, nous allons créer un moyen plus sophistiqué d'analyser les performances, ainsi que de décrire comment optimiser les périodes de rétrospective des signaux de moyenne mobile individuels.


Plus de