パンダとPythonで移動平均のクロスオーバーをバックテストする

作者: リン・ハーン優しさ, 作成日:2019-03-27 15:11:40, 更新日:

この記事では,我々が導入した機械を利用して,実際の戦略,すなわちAAPLにおける移動平均のクロスオーバーに関する研究を行います.

移動平均のクロスオーバー戦略

移動平均クロスオーバーテクニックは,非常によく知られている簡略的なモメント戦略である.それはしばしば定量的な取引の"Hello World"例と考えられる.

ストラテジーは,長期に限ったものである.特定の時間系列の異なるバックバック期間の2つの異なる単純な移動平均フィルタが作成される.短いバックバック移動平均が長いバックバック移動平均を超えると資産を購入するシグナルが発生する.長い平均が後に短い平均を超えると,資産は再び販売される. タイムシリーズが強いトレンドの期間に侵入し,徐々にトレンドを逆転すると戦略はうまく機能する.

この例では,短回顧100日,長回顧400日という時間系列としてApple, Inc. (AAPL) を選択しました.これはziplineアルゴリズム取引ライブラリが提供する例です.したがって,自分のバックテストを実行したい場合は,基本的検証手段としてziplineの結果と一致することを確認する必要があります.

実施

バックテスト用の初期オブジェクト階層がどのように構築されているかを説明する前のチュートリアルを必ずフォローしてください.そうでなければ,以下のコードは動作しません.この特定の実装のために,私は以下のライブラリを使用しました:

  • パイソン - 2.7.3
  • NumPy - 1.8.0
  • パンダ 0.12.0
  • マットプロットリブ - 1.1.0

ma_cross.py の実装にはbacktest.py最初のステップは必要なモジュールとオブジェクトをインポートすることです.

# 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

前回のチュートリアルと同様に,AAPLの移動平均値が互いに交差するときに信号を生成する方法の詳細をすべて含むMovingAverageCrossStrategyを生成するために,Strategy抽象的なベースクラスをサブクラスをします.

このオブジェクトには動作するための short_window と long_window が必要です.値はそれぞれ 100 日と 400 日でデフォルトで設定されています.これは,zipline のメイン例で使用されている同じパラメータです.

移動平均値は,AAPL株のバー[Close]の閉値上のパンダスローリング_ミニアン関数を使用して作成されます.個々の移動平均値が構築された後,短移動平均値が長い移動平均値よりも大きい場合,または0.0である場合,列を1.0に等しく設定することによって信号シリーズが生成されます.これからは,取引信号を表すためにポジションオーダーを生成することができます.

# 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

MarketOnCloseポートフォリオは,ポートフォリオからサブクラスを区別し,backtest.pyポートフォリオオブジェクトの定義の詳細については,前回のチュートリアルを参照してください. 完全性のためにコードを残し,このチュートリアルを自立的に保つために:

# 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

移動平均CrossStrategyとMarketOnClosePortfolioのクラスが定義されているので,主要さらに,戦略のパフォーマンスは,株式曲線のグラフを通じて検討されます.

pandas DataReader オブジェクトは,1990年1月1日から2002年1月1日までの期間のAAPL株式のOHLCV価格をダウンロードし,その時点で,データフレームシグナルがロングのみのシグナルを生成するために作成されます.その後,ポートフォリオは10万米ドルの初期資本ベースで生成され,収益は株式曲線で計算されます.

最後のステップは,AAPL価格の両方の2桁のグラフをプロットするためにmatplotlibを使用することです. 移動平均値と購入/販売信号,および同じ購入/販売信号を持つ株式曲線が覆われています.プロットコードは,zipline実装例から取られています (そして修正されています).

# 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()

コードのグラフィカル出力は以下の通りです.私はUbuntu の間 IPython のコンソールに直接これを挿入するために IPython %paste コマンドを使用しました.グラフィカル出力は表示され続けました.ピンク色のアップティックはストックを購入することを表し,黒い色のダウンティックはそれを売り戻すことを表します:imgAAPL移動平均クロスオーバーパフォーマンス 1990-01-01から2002-01-01

戦略は5回の往復取引で,この期間中に損失を被っていることがわかります.これは,AAPLがこの期間中,わずかな下落傾向を示し,1998年から大幅な上昇を遂げていたことを考えると驚くことではありません.移動平均信号の見直し期間はかなり大きく,これが最終取引の利益に影響を与え,そうでなければ戦略が収益的になっていた可能性があります.

次の記事では,パフォーマンスを分析するためのより洗練された手段を作成し,個々の移動平均信号のバックバック期間を最適化する方法を説明します.


もっと