在上一篇关于《使用pandas在Python中研究回测环境》的文章中,我们创建了一个面向对象的研究型回测环境,并在随机预测策略上进行了测试。
在本文中,我们将利用已搭建的框架,对一个实际的交易策略进行研究,即针对 AAPL(苹果公司)的移动平均线交叉策略(Moving Average Crossover)。
创建均线:针对特定的时间序列(如股价),创建两个具有不同回溯周期的简单移动平均线(SMA)。
买入信号:当短期均线上穿长期均线时,产生买入信号。
卖出信号:如果长期均线随后下穿短期平均线,则卖出资产。
该策略在时间序列进入强趋势阶段并缓慢反转时表现最佳。
本例参数设定:
标的物:苹果公司(AAPL)
短期均线周期:100日
长期均线周期:400日
实现环境
请确保您已阅读并跟随了之前的教程,了解回测系统的初始对象层次结构是如何构建的,否则下面的代码将无法运行。
Python、NumPy、pandas 、matplotlib
实现ma_cross.py需要用到上一篇教程中的backtest.py文件。首先是导入所需的模块和对象:
# ma_cross.pyimport datetimeimport matplotlib.pyplot as pltimport numpy as npimport pandas as pdfrom pandas.io.data import DataReaderfrom backtest import Strategy, Portfolio
与上一篇教程一样,我们将通过继承Strategy抽象基类,以创建MovingAverageCrossStrategy子类。该子类完整定义了苹果股票移动平均线交叉时生成交易信号的逻辑规则。
该策略对象需要设置一个short_window(短期窗口)和一个 long_window(长期窗口)参数来运行。这两个值的默认设置分别为100日和400日,这与zipline官方示例采用的参数设置保持一致。
移动平均线是通过在AAPL股票的bars['Close'](收盘价)上使用 pandas 的rolling_mean(滚动均值)函数创建的。
一旦构建了单独的移动平均线,信号序列(signalSeries)的生成逻辑如下:
# ma_cross.pyclass 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 = symbolself.bars = barsself.short_window = short_windowself.long_window = long_windowdef generate_signals(self):"""Returns the DataFrame of symbols containing the signalsto 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 periodssignals['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 windowsignals['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 orderssignals['positions'] = signals['signal'].diff()return signals
MarketOnClosePortfolio类继承自backtest.py中的Portfolio类。除交易执行时间是从开盘价对开盘价(Open-to-Open)调整为收盘价对收盘价(Close-to-Close)之外,其实现方式与上一篇教程中几乎完全相同。
有关Portfolio对象具体定义的细节,请参阅上一篇教程。为了保证内容的完整性和本教程的独立性,我保留了相关代码:
# ma_cross.pyclass MarketOnClosePortfolio(Portfolio):"""Encapsulates the notion of a portfolio of positions basedon 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 = symbolself.bars = barsself.signals = signalsself.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 sharesreturn positionsdef 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
在完成MovingAverageCrossStrategy和MarketOnClosePortfolio类的定义后,我们将通过调用__main__函数来将所有功能模块整合在一起,并借助绘制净值曲线(equity curve)来对策略表现进行可视化分析。
最后一步是使用 matplotlib 绘制一个包含两个子图的图表:
(注:绘图代码源自 zipline 的官方示例,并进行了相应的修改。)
# ma_cross.pyif __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 ZipLinesymbol = '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 daysmac = MovingAverageCrossStrategy(symbol, bars, short_window=100, long_window=400)signals = mac.generate_signals()# Create a portfolio of AAPL, with $100,000 initial capitalportfolio = MarketOnClosePortfolio(symbol, bars, signals, initial_capital=100000.0)returns = portfolio.backtest_portfolio()# Plot two charts to assess trades and equity curvefig = plt.figure()fig.patch.set_facecolor('white') # Set the outer colour to whiteax1 = fig.add_subplot(211, ylabel='Price in $')# Plot the AAPL closing price overlaid with the moving averagesbars['Close'].plot(ax=ax1, color='r', lw=2.)signals[['short_mavg', 'long_mavg']].plot(ax=ax1, lw=2.)# Plot the "buy" trades against AAPLax1.plot(signals.ix[signals.positions == 1.0].index,signals.short_mavg[signals.positions == 1.0],'^', markersize=10, color='m')# Plot the "sell" trades against AAPLax1.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 dollarsax2 = fig.add_subplot(212, ylabel='Portfolio value in $')returns['total'].plot(ax=ax2, lw=2.)# Plot the "buy" and "sell" trades against the equity curveax2.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 figurefig.show()
代码生成的图形输出如下所示。我在Ubuntu系统中使用了IPython的%paste命令,直接将代码粘贴到IPython控制台中执行,以确保图形化输出保持可见。
在图表中:

AAPL 移动平均线交叉策略表现(1990-01-01 至 2002-01-01)
在后续的文章中,我们将建立更完善的绩效分析体系,并阐述如何优化各个移动平均线信号的周期参数。