当前位置:首页>python>使用pandas在Python中对移动平均线交叉策略进行回测

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

  • 2026-10-21 13:26:23
使用pandas在Python中对移动平均线交叉策略进行回测

在上一篇关于《使用pandas在Python中研究回测环境》的文章中,我们创建了一个面向对象的研究型回测环境,并在随机预测策略上进行了测试。

在本文中,我们将利用已搭建的框架,对一个实际的交易策略进行研究,即针对 AAPL(苹果公司)的移动平均线交叉策略(Moving Average Crossover)。

📝移动平均线交叉策略

移动平均线交叉技术是一种极为经典的简易动量策略,它通常被视为量化交易领域的入门示例。
本文所述策略为纯多头策略。具体逻辑如下:

创建均线:针对特定的时间序列(如股价),创建两个具有不同回溯周期的简单移动平均线(SMA)。

买入信号:当短期均线上穿长期均线时,产生买入信号。

卖出信号:如果长期均线随后下穿短期平均线,则卖出资产。

该策略在时间序列进入强趋势阶段并缓慢反转时表现最佳。

本例参数设定:

标的物:苹果公司(AAPL)

短期均线周期:100日

长期均线周期:400日

本例选用苹果公司(AAPL)股价数据,设置短期回溯周期为100日,长期回溯周期为400日——这与zipline量化交易库提供的示例参数一致。通过对比zipline的回测结果,可以验证我们自建回测系统的准确性。

实现环境

请确保您已阅读并跟随了之前的教程,了解回测系统的初始对象层次结构是如何构建的,否则下面的代码将无法运行。

本次实现使用了以下工具:

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)的生成逻辑如下:

  1. 买入信号:当短期移动平均线大于长期移动平均线时,将该列的值设为1.0。
  2. 卖出/空仓信号:其他情况(即长期均线上穿或等于短期均线),则设为0.0。
基于该信号序列即可生成代表positions交易指令的持仓头寸变化。
# 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 = 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类继承自backtest.py中的Portfolio类。除交易执行时间是从开盘价对开盘价(Open-to-Open)调整为收盘价对收盘价(Close-to-Close)之外,其实现方式与上一篇教程中几乎完全相同。

有关Portfolio对象具体定义的细节,请参阅上一篇教程。为了保证内容的完整性和本教程的独立性,我保留了相关代码:

# ma_cross.pyclass 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

在完成MovingAverageCrossStrategy和MarketOnClosePortfolio类的定义后,我们将通过调用__main__函数来将所有功能模块整合在一起,并借助绘制净值曲线(equity curve)来对策略表现进行可视化分析。

数据与执行流程

  1. 数据获取:利用 pandas 的DataReader对象获取AAPL股票从1990年1月1日到2002年1月1日期间的OHLCV(开盘价-最高价-最低价-收盘价-成交量)价格数据。
  2. 信号生成:在此基础上创建signalsDataFrame,以生成纯多头交易信号。
  3. 回测执行:随后生成投资组合,初始资金设定为100,000 美元,并基于净值曲线计算收益率。

可视化结果

最后一步是使用 matplotlib 绘制一个包含两个子图的图表:

  • 子图一:AAPL 的股价走势,叠加绘制了移动平均线以及买入/卖出信号标记。
  • 子图二:策略的净值曲线,同样叠加了买入/卖出信号。

(注:绘图代码源自 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 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的%paste命令,直接将代码粘贴到IPython控制台中执行,以确保图形化输出保持可见。

在图表中:

  • 粉色上指箭头:代表买入股票(建仓)。
  • 黑色下指箭头:代表卖出股票(平仓)。

AAPL 移动平均线交叉策略表现(1990-01-01 至 2002-01-01)

如图所示,在这段时间内,该策略共完成了五次完整的买卖交易,但最终是亏损的。鉴于AAPL在该时期内的走势表现——初期呈小幅下跌趋势,随后于1998年开始显著上涨,这样的结果并不令人意外。而移动平均线信号的回溯周期(lookback period)设置得相当长,这对最后一笔交易的盈利产生了影响,若优化此参数,策略或许能实现盈利。

在后续的文章中,我们将建立更完善的绩效分析体系,并阐述如何优化各个移动平均线信号的周期参数。

最新文章

随机文章