使用 Pandas 在 Python 中对移动平均线交叉进行回测

使用 Pandas 在 Python 中对移动平均线交叉进行回测

移动平均线交叉策略

移动平均线交叉技术是一种非常著名的简单动量策略。它通常被认为是量化交易的“Hello World”示例。

此处概述的策略仅适用于多头。创建两个单独的简单移动平均线过滤器,具有特定时间序列的不同回溯期。当较短的回溯移动平均线超过较长的回溯移动平均线时,就会出现购买资产的信号。如果较长的平均值随后超过较短的平均值,则资产将被卖回。当时间序列进入强劲趋势期然后缓慢逆转趋势时,该策略非常有效。

对于这个例子,我选择了苹果公司(AAPL)作为时间序列,短回溯期为 100 天,长回溯期为 400 天。这是zipline 算法交易库提供的示例。因此,如果我们希望实现自己的回测器,我们需要确保它与 zipline 中的结果相匹配,作为验证的基本手段。

执行

请务必遵循此处的上一个教程,其中描述了如何构建回测器的初始对象层次结构,否则下面的代码将不起作用。对于这个特定的实现,我使用了以下库:

  • Python——2.7.3
  • NumPy-1.8.0
  • 熊猫-0.12.0
  • matplotlib-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, ortfolio

与上一教程一样,我们将对Strategy抽象基类进行子类化以生成MovingAverageCrossStrategy,它包含有关如何在 AAPL 的移动平均线相互交叉时生成信号的所有细节。

该对象需要对short_window和进行操作。这两个值分别设置为默认值 100 天和 400 天,与ziplinelong_window主示例中使用的参数相同。

移动平均线是使用 pandasrolling_mean函数对bars['Close']AAPL 股票的收盘价创建的。构建单个移动平均线后,signal通过将列设置为 1.0(当短期移动平均线大于长期移动平均线时)或 0.0(否则)来生成系列。由此positions可以生成订单来表示交易信号。

# 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

MarketOnClosePortfolio是从 子类化而来的Portfolio,可以在 中找到。它backtest.py与前一个教程中描述的实现几乎相同,不同之处在于交易现在是以“收盘价到收盘价”为基础进行的,而不是“开盘价到开盘价”为基础。有关如何Portfolio定义对象的详细信息,请参阅前一个教程。我保留了代码以保持完整性并使本教程独立:

# 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

既然MovingAverageCrossStrategyMarketOnClosePortfolio类已经定义,__main__将调用一个函数将所有功能绑定在一起。此外,还将通过权益曲线图检查策略的表现。

pandasDataReader对象下载 1990 年 1 月 1 日至 2002 年 1 月 1 日期间 AAPL 股票的 OHLCV 价格,此时signals创建 DataFrame 以生成多头信号。随后以 100,000 美元的初始资本基础生成投资组合,并根据股权曲线计算收益。

最后一步是使用 matplotlib 绘制 AAPL 价格的双图,叠加移动平均线和买入/卖出信号,以及具有相同买入/卖出信号的权益曲线。绘图代码取自(并经过修改)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()

代码的图形输出如下。我%paste在 Ubuntu 中使用 IPython 命令将其直接放入 IPython 控制台,以便图形输出仍然可见。粉红色的上升线代表购买股票,而黑色的下降线代表卖回股票:

1990 年 1 月 1 日至 2002 年 1 月 1 日 AAPL 移动平均线交叉表现
1990 年 1 月 1 日至 2002 年 1 月 1 日 AAPL 移动平均线交叉表现
可以看出,该策略在此期间亏损,有五次往返交易。考虑到 AAPL 在此期间的表现,这并不奇怪,当时 AAPL 处于略微下降的趋势,随后从 1998 年开始出现大幅上涨。移动平均线信号的回溯期相当长,这影响了最后一笔交易的利润,否则该策略可能会盈利。

猜你喜欢

转载自blog.csdn.net/m0_74840398/article/details/143052147