Prueba de un cruce de promedio móvil en Python con pandas

El autor:La bondad, Creado: 2019-03-27 15:11:40, Actualizado:

En este artículo utilizaremos el mecanismo que hemos introducido para realizar investigaciones sobre una estrategia real, a saber, el cruce de la media móvil en AAPL.

Estrategia de cruce de la media móvil

La técnica de cruce de promedio móvil es una estrategia de impulso simplista extremadamente conocida.

La estrategia descrita aquí es de duración limitada. Se crean dos filtros de promedio móvil simples separados, con períodos de retroceso variables, de una serie de tiempo particular. Las señales para comprar el activo ocurren cuando el promedio móvil de retroceso más corto excede el promedio móvil de retroceso más largo. Si el promedio más largo posteriormente excede el promedio más corto, el activo se vuelve a vender. La estrategia funciona bien cuando una serie de tiempo entra en un período de fuerte tendencia y luego invierte lentamente la tendencia.

Para este ejemplo, he elegido Apple, Inc. (AAPL) como la serie temporal, con una retroalimentación corta de 100 días y una retroalimentación larga de 400 días. Este es el ejemplo proporcionado por la biblioteca de comercio algorítmico zipline. Por lo tanto, si deseamos implementar nuestro propio backtester, debemos asegurarnos de que coincida con los resultados en zipline, como un medio básico de validación.

Aplicación

Asegúrese de seguir el tutorial anterior aquí, que describe cómo se construye la jerarquía de objetos inicial para el backtester, de lo contrario el código a continuación no funcionará.

  • Python - 2.7.3
  • NumPy - 1.8.0
  • Los pandas - 0.12.0
  • - el número de personas que han sido objeto de una investigación

La implementación de ma_cross.py requierebacktest.pyEl primer paso es importar los módulos y objetos necesarios:

# 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

Como en el tutorial anterior vamos a subclase la estrategia abstracta clase base para producir MovingAverageCrossStrategy, que contiene todos los detalles sobre cómo generar las señales cuando las medias móviles de AAPL cruzan entre sí.

El objeto requiere un short_window y un long_window para operar. Los valores se han establecido en valores predeterminados de 100 días y 400 días respectivamente, que son los mismos parámetros utilizados en el ejemplo principal de zipline.

Los promedios móviles se crean utilizando la función panda rolling_mean en las barras [Close] precio de cierre de la acción AAPL. Una vez que se han construido los promedios móviles individuales, la serie de señales se genera estableciendo la columna igual a 1.0 cuando el promedio móvil corto es mayor que el promedio móvil largo, o 0.0 de lo contrario.

# 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

El MarketOnClosePortfolio se subclasifica de Portfolio, que se encuentra enbacktest.py. Es casi idéntico a la implementación descrita en el tutorial anterior, con la excepción de que las operaciones ahora se llevan a cabo en una base Close-to-Close, en lugar de una base Open-to-Open. Para obtener detalles sobre cómo se define el objeto Portfolio, consulte el tutorial anterior.

# 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

Ahora que se han definido las clases MovingAverageCrossStrategy y MarketOnClosePortfolio, unael principalEn el caso de la estrategia, el valor de la función se utilizará para vincular todas las funcionalidades.

El objeto Panda DataReader descarga los precios OHLCV de las acciones de AAPL para el período del 1 de enero de 1990 al 1 de enero de 2002, momento en el que se crean las señales DataFrame para generar las señales de largo plazo.

El paso final es usar matplotlib para trazar un gráfico de dos cifras de los precios de AAPL, superpuestos con las medias móviles y las señales de compra / venta, así como la curva de renta variable con las mismas señales de compra / venta.

# 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 salida gráfica del código es la siguiente. Utilicé el comando IPython %paste para poner esto directamente en la consola IPython mientras estaba en Ubuntu, de modo que la salida gráfica permaneciera a la vista. Los picos rosados representan la compra de la acción, mientras que los picos negros representan su venta:imgAAPL Promedio móvil de rendimiento cruzado desde 1990-01-01 hasta 2002-01-01

Como se puede ver, la estrategia pierde dinero durante el período, con cinco operaciones de ida y vuelta. Esto no es sorprendente dado el comportamiento de AAPL durante el período, que tuvo una ligera tendencia a la baja, seguida de un aumento significativo a partir de 1998.

En artículos posteriores crearemos un medio más sofisticado para analizar el rendimiento, así como describiremos cómo optimizar los períodos de retroceso de las señales de promedio móvil individuales.


Más.