Python股市数据分析教程——学会它,或可以实现半“智能”炒股 (Part 2)

简介: 本篇文章是"Python股市数据分析"两部曲中的第二部分。在本篇文章中,我们讨论了均线交叉策略的设计、回溯检验、基准测试以及实践中可能出现的若干问题,并结合Python代码实现了一个基于均线交叉的交易策略系统。

本文由北邮@爱可可-爱生活 老师推荐,阿里云云栖社区组织翻译。

以下为译文

本篇文章是"Python股市数据分析"两部曲中的第二部分(第一部分的文章在这里),内容基于我在犹他州立大学MATH 3900 (Data Mining)课程上的一次讲座,第一部分在这里。在这些文章中,我将介绍一些关于金融数据分析的基础知识,例如,使用pandas获取雅虎财经上的数据,股票数据可视化,移动均线,开发一种均线交叉策略,回溯检验以及基准测试。而本篇文章中,我讨论的话题包括均线交叉策略的设计、回溯检验、基准测试以及实践中可能出现的若干问题,以供读者思考。

注意:本篇文章所涉及的看法、意见等一般性信息仅为作者个人观点。本文的任何内容都不应被视为金融投资方面的建议。此外,在此给出的所有代码均无法提供任何保证。选择使用这些代码的个人需自行承担风险。

交易策略

我们把在未来条件满足时将被终止的交易称为未平仓交易。多头仓位是指在交易过程中通过金融商品增值来获取利润,而空头仓位是指在交易过程中通过金融资产价值下跌来获取利润。在直接交易股票时,所有的多头仓位看涨,所有的空头仓位看跌。这也就是说,持看涨态度并不需要伴随着一个多头仓位,而持看跌态度同样也不需要伴随着一个空头仓位(在交易股票期权时,更是如此)。

这里有一个例子。打算你买入了一只股票,计划在股价上涨时以更高的价格将股票抛出。这就是多头仓位:你持有一种金融资产,如果资产价值增长,你将从中获利。你的潜在利润是无限的,而你的潜在损失受到股价的限制,因为股价永远不会低于0。另一方面,如果你预计一只股票的价格会下跌,你可以向经纪公司筹借股票并出售,以期在后续以较低的价格回购股票,从而获取利润。这种做法称为做空股票,属于空头仓位,即通过股价下跌赚取收益。做空股票的潜在利润受到股价的限制(最好的做法是,使股票变得一文不值,这样你可以免费回购这些股票),而损失却是无限的,因为你可能需要花费任意多的钱来买回筹借的股票。因此,在允许投资者做空股票前,经纪人需要确保投资者保持良好的财务状况。

任何交易员都必须有一套规则,决定她愿意在任何一笔交易上投入多少钱。例如,一名交易员可能认为在任何情况下,她在一笔交易中承受的风险都不能超过所有投资的10%。另外,在任何交易中,交易员必须制定一个由一组条件构成的退出策略,决定她何时退出仓位,从而获利或止损。交易员可以设置一个目标,即促使她清空仓位的最少利润。同样地,交易员也要明确自身能够承受的最大损失;如果潜在损失超过了这个金额,交易员将退出仓位,以避免任何进一步的损失(通常通过设置止损指令来实现,触发该指令以避免进一步损失)。

如果一个方案包括促成交易的交易信号、一套能在任何特定策略情况下明确承受多少投资风险的规则、以及一个适用于任何交易的退出策略,那么我们称这个方案为一个完整的交易策略。目前,我们关注的是如何设计和评价交易策略。

我们假设任何一笔交易的金额都是投资总资产的一个固定比例;10%看起来是一个不错的数字。我们决定,对于任何一笔交易,如果损失超过交易金额的20%,我们将结束交易。现在,我们需要一种方法来判断何时进入仓位以及何时退出仓位,进而获取利润。

在这里,我将介绍一种均线交叉策略。我们将使用两条移动均线:一条表示长期均线,另一条表示短期均线。采用的策略如下:

  • 当短期均线越过长期均线时,交易金融资产。
  • 当短期均线再一次越过长期均线时,结束交易。

当短期均线高于长期均线时,我们应进行多头交易,当短期均线再次越过(低于)长期均线时,结束此类交易。当短期均线低于长期均线时,我们应进行空头交易,当短期均线再次越过(高于)长期均线时,结束此类交易。

现在,我们有了一个完整的策略。但在我们决定使用它之前,我们首先应该尽可能地评估这个策略的效果。回溯检验是一种常用的方法,该方法基于历史数据对交易策略所能带来的利润多少进行评估。例如,看看上方图表中Apple股票的表现,如果20天均线表示短期均线,50天均线表示长期均线,这个交易策略似乎并不能产生多少利润,至少不如你一直持有多头仓位更有利可图。

让我们看看我们是否可以自动进行回溯检验任务。我们首先确定20天均线什么时候低于50天均线,以及相反的情况。

apple['20d-50d'] = apple['20d'] - apple['50d']
apple.tail()


cb6d54f06390d50e4702b6c2e17c274facd17f9f

我们把这种差异的标志称为行情。也就是说,如果短期均线高于长期均线,那么这是一个牛市行情(牛市规则),如果短期均线低于长期均线,则目前为熊市行情(熊市规则)。我使用以下代码判断当前的股市行情。

apple["Regime"] = np.where(apple['20d-50d'] > 0, 1, 0)
apple["Regime"] = np.where(apple['20d-50d'] < 0, -1, apple["Regime"])
apple.loc['2016-01-01':'2016-08-07',"Regime"].plot(ylim = (-2,2)).axhline(y = 0, color = "black", lw = 2)


cbd175b9deaf1a8f56702537ccbcc64c1ff5c1b9
apple["Regime"].plot(ylim = (-2,2)).axhline(y = 0, color = "black", lw = 2)


05cdc32736a0e3e63312bdc846434d0a2f8acb01
apple["Regime"].value_counts()
1       966
-1      663
0       50
Name: Regime, dtype: int64

上面的最后一行表明,Apple股票在股市中的行情,有1005天为熊市,有600天为牛市,而有54天股市行情较为平稳。

行情变化时会出现交易信号。当牛市开始时,买入信号会被触发,而当牛市结束时,抛出信号会被触发。同样地,当熊市开始时,抛出信号会被触发,而当熊市结束时,买入信号会被触发(只有当你要做空股票,或使用一些股票期权等衍生品做空市场时,才会对这些感兴趣)。

我们很容易就可以获取交易信号。令rt表示t时刻的股市行情,st表示t时刻的交易信号,则有:

st = sing(rt - rt-1)

st ∈ {-1, 0, 1},其中-1表示"抛出",1表示"买入",0表示不采取任何措施,我们可以这样获取信号:

regime_orig = apple.ix[-1, "Regime"]
apple.ix[-1, "Regime"] = 0
apple["Signal"] = np.sign(apple["Regime"] - apple["Regime"].shift(1))
apple.ix[-1, "Regime"] = regime_orig
apple.tail()


8fb082dc8ee6a34f494fbcc107815f266e5e936e
apple["Signal"].plot(ylim = (-2, 2))


bc364709244d43400fb168ea26d695a381644b11
apple["Signal"].value_counts()
0.0     1637
-1.0    21
1.0     20
Name: Signal, dtype: int64

我们会买入Apple股票23次,并抛出Apple股票23次。如果我们仅持有多头仓位,在6年期间只会进行23笔交易,然而,如果我们在每次多头仓位终止后,由多头仓位转为空头仓位,我们一共会进行23笔交易。(请记住,更加频繁的交易并不一定就是好的,交易从来不是免费的。)

你可能会注意到,目前的系统并不是很健全,即使是一个短期均线超过长期均线的短暂瞬间,交易也会被触发,并导致交易立即结束(这样并不好,不仅仅是因为每一笔实际交易都伴随着一笔费用,已获得的收益会因此被迅速稀释)。此外,每个牛市行情都会立即转换到熊市行情,如果你在构建一个允许看涨押注和看跌押注的交易系统,这会导致在一笔交易结束时,立即触发另一笔在股市中反向押注的交易,这看起来又有些挑剔了。一个更好的系统应该根据更多的证据来判断股市正朝着发展的特定方向,但我们现在不会关心这些细节。

现在,让我们尝试着确定每次买入和抛出股票时的价格。

apple.loc[apple["Signal"] == 1, "Close"]
Date
2010-03-16    224.449997
2010-06-18    274.070011
2010-09-20    283.230007
2011-05-12    346.569988
2011-07-14    357.770004
2011-12-28    402.640003
2012-06-25    570.770020
2013-05-17    433.260010
2013-07-31    452.529984
2013-10-16    501.110001
2014-03-26    539.779991
2014-04-25    571.939980
2014-08-18     99.160004
2014-10-28    106.739998
2015-02-05    119.940002
2015-04-28    130.559998
2015-10-27    114.550003
2016-03-11    102.260002
2016-07-01     95.889999
2016-07-25     97.339996
Name: Close, dtype: float64
apple.loc[apple["Signal"] == -1, "Close"]
Date
2010-06-11    253.509995
2010-07-22    259.020000
2011-03-30    348.630009
2011-03-31    348.510006
2011-05-27    337.409992
2011-11-17    377.410000
2012-05-09    569.180023
2012-10-17    644.610001
2013-06-26    398.069992
2013-10-03    483.409996
2014-01-28    506.499977
2014-04-22    531.700020
2014-06-11     93.860001
2014-10-17     97.669998
2015-01-05    106.250000
2015-04-16    126.169998
2015-06-25    127.500000
2015-12-18    106.029999
2016-05-05     93.239998
2016-07-08     96.680000
2016-09-01    106.730003
Name: Close, dtype: float64
apple_signals = pd.concat([
        pd.DataFrame({"Price": apple.loc[apple["Signal"] == 1, "Close"],
                     "Regime": apple.loc[apple["Signal"] == 1, "Regime"],
                     "Signal": "Buy"}),
        pd.DataFrame({"Price": apple.loc[apple["Signal"] == -1, "Close"],
                     "Regime": apple.loc[apple["Signal"] == -1, "Regime"],
                     "Signal": "Sell"}),
    ])
apple_signals.sort_index(inplace = True)
apple_signals


e8c079f5978b8dbef63fb2770a0b51f9ac4511e5
apple_long_profits = pd.DataFrame({
        "Price": apple_signals.loc[(apple_signals["Signal"] == "Buy") &
                                  apple_signals["Regime"] == 1, "Price"],
        "Profit": pd.Series(apple_signals["Price"] - apple_signals["Price"].shift(1)).loc[
            apple_signals.loc[(apple_signals["Signal"].shift(1) == "Buy") & (apple_signals["Regime"].shift(1) == 1)].index
        ].tolist(),
        "End Date": apple_signals["Price"].loc[
            apple_signals.loc[(apple_signals["Signal"].shift(1) == "Buy") & (apple_signals["Regime"].shift(1) == 1)].index
        ].index
    })
apple_long_profits


b577764f9980388e2bbded4ca250ce6c45601f86

从上面我们可以看到,在2013年5月17日,Apple股票的价格大幅下跌,我们的交易系统似乎不能很好地处理这种状况。但是,这次股价下跌并不是因为Apple公司受到了巨大的冲击,而是由于股票拆分。尽管派付股息不如股票拆分那样明显,但是这些因素仍可能影响到我们交易系统的效果。

pandas_candlestick_ohlc(apple, stick = 45, otherseries = ["20d", "50d", "200d"])


367c68a2d481209c24773297a3f3355a143864c7

我们不希望我们的交易系统因为股票拆分和派付股息而表现得很糟糕。我们应该如何处理这种情况?一种方法是获取股票拆分和派付股息的历史数据,并设计一个处理这类数据的交易系统。这或许是最好的解决方案,能够最为真实地反映股票的行为,但是它过于复杂。另一种解决方案是根据股票拆分和派付股息的情况调整股票价格。

雅虎财经只提供调整后的股票收盘价,但是对于我们来说,要得到调整后的开盘价、最高价、最低价,这样就足够了。已调整收盘价计算方式如下:

pricetadj = mt x pricet

其中,mt是用来调整股价的系数。只需进行一次除法就可以求出mt的值,因此,我们可以使用收盘价和已调整收盘价来调整股票的其他所有价格。

让我们回到前面,调整Apple的股价,并用这些调整后的数据重新评估我们的交易系统。

def ohlc_adj(dat):
    return pd.DataFrame({"Open": dat["Open"] * dat["Adj Close"] / dat["Close"],
                       "High": dat["High"] * dat["Adj Close"] / dat["Close"],
                       "Low": dat["Low"] * dat["Adj Close"] / dat["Close"],
                       "Close": dat["Adj Close"]})
 
apple_adj = ohlc_adj(apple)
 
apple_adj["20d"] = np.round(apple_adj["Close"].rolling(window = 20, center = False).mean(), 2)
apple_adj["50d"] = np.round(apple_adj["Close"].rolling(window = 50, center = False).mean(), 2)
apple_adj["200d"] = np.round(apple_adj["Close"].rolling(window = 200, center = False).mean(), 2)
 
apple_adj['20d-50d'] = apple_adj['20d'] - apple_adj['50d']

apple_adj["Regime"] = np.where(apple_adj['20d-50d'] > 0, 1, 0)

apple_adj["Regime"] = np.where(apple_adj['20d-50d'] < 0, -1, apple_adj["Regime"])

regime_orig = apple_adj.ix[-1, "Regime"]
apple_adj.ix[-1, "Regime"] = 0
apple_adj["Signal"] = np.sign(apple_adj["Regime"] - apple_adj["Regime"].shift(1))

apple_adj.ix[-1, "Regime"] = regime_orig
 
apple_adj_signals = pd.concat([
        pd.DataFrame({"Price": apple_adj.loc[apple_adj["Signal"] == 1, "Close"],
                     "Regime": apple_adj.loc[apple_adj["Signal"] == 1, "Regime"],
                     "Signal": "Buy"}),
        pd.DataFrame({"Price": apple_adj.loc[apple_adj["Signal"] == -1, "Close"],
                     "Regime": apple_adj.loc[apple_adj["Signal"] == -1, "Regime"],
                     "Signal": "Sell"}),
    ])
apple_adj_signals.sort_index(inplace = True)
apple_adj_long_profits = pd.DataFrame({
        "Price": apple_adj_signals.loc[(apple_adj_signals["Signal"] == "Buy") &
                                  apple_adj_signals["Regime"] == 1, "Price"],
        "Profit": pd.Series(apple_adj_signals["Price"] - apple_adj_signals["Price"].shift(1)).loc[
            apple_adj_signals.loc[(apple_adj_signals["Signal"].shift(1) == "Buy") & (apple_adj_signals["Regime"].shift(1) == 1)].index
        ].tolist(),
        "End Date": apple_adj_signals["Price"].loc[
            apple_adj_signals.loc[(apple_adj_signals["Signal"].shift(1) == "Buy") & (apple_adj_signals["Regime"].shift(1) == 1)].index
        ].index
    })
 
pandas_candlestick_ohlc(apple_adj, stick = 45, otherseries = ["20d", "50d", "200d"])


de0a4e05045888a06207787acce44fc103b8636a
apple_adj_long_profits


aa1b657669d48462998f7fffcef41becff6dfa30

你可以看到,根据股票拆分和派付股息情况调整后的股票价格有明显的不同。从现在开始,我们将使用这些数据。

现在,让我们创建一个价值100万美元的虚拟投资项目,根据我们建立的规则,看看它会如何表现。规则包括:

  • 在任何交易中,仅投资所有投资总额的10%。
  • 如果损失超过交易金额的20%,则退出仓位。

在模拟的过程中,牢记以下几点:

  • 股票交易以100股为单位。
  • 我们的止损规则包含在股价下跌至一定程度时将股票抛出的指令。因此,我们需要检查这一期间的低价是否已经足够得低,以至于触发止损指令。实际上,除非我们买入了看跌期权,否则我们无法保证以设置的止损价格抛出股票,但为简单起见,我们将这个价格作为抛出价。
  • 每一笔交易都需要向经纪人支付一笔佣金,这部分费用应该计算在内。但在这里我们不这样做。

回溯检验按如下方式进行:

tradeperiods = pd.DataFrame({"Start": apple_adj_long_profits.index,
                            "End": apple_adj_long_profits["End Date"]})
apple_adj_long_profits["Low"] = tradeperiods.apply(lambda x: min(apple_adj.loc[x["Start"]:x["End"], "Low"]), axis = 1)
apple_adj_long_profits


adad53c5338e65f0347e20ace4fb6779b43fdc22
cash = 1000000
apple_backtest = pd.DataFrame({"Start Port. Value": [],
                         "End Port. Value": [],
                         "End Date": [],
                         "Shares": [],
                         "Share Price": [],
                         "Trade Value": [],
                         "Profit per Share": [],
                         "Total Profit": [],
                         "Stop-Loss Triggered": []})
port_value = .1 
batch = 100    
stoploss = .2 
for index, row in apple_adj_long_profits.iterrows():
    batches = np.floor(cash * port_value) // np.ceil(batch * row["Price"]) # Maximum number of batches of stocks invested in
    trade_val = batches * batch * row["Price"] 
    if row["Low"] < (1 - stoploss) * row["Price"]:   # Account for the stop-loss
        share_profit = np.round((1 - stoploss) * row["Price"], 2)
        stop_trig = True
    else:
        share_profit = row["Profit"]
        stop_trig = False
    profit = share_profit * batches * batch 
   
    apple_backtest = apple_backtest.append(pd.DataFrame({
                "Start Port. Value": cash,
                "End Port. Value": cash + profit,
                "End Date": row["End Date"],
                "Shares": batch * batches,
                "Share Price": row["Price"],
                "Trade Value": trade_val,
                "Profit per Share": share_profit,
                "Total Profit": profit,
                "Stop-Loss Triggered": stop_trig
            }, index = [index]))
    cash = max(0, cash + profit)
 
apple_backtest


a255da176feb4560fd88d46fd91439243aacdac0
apple_backtest["End Port. Value"].plot()


ca12b4a75291ca1fb5319f8fe4a080f6b05c99a5

我们的投资项目总值在六年间增长了10%。考虑到任何一笔交易仅涉及所有投资总额的10%,这样的表现并不差。

请注意,这个交易策略并不会触发我们的止损指令。难道这意味着我们不需要止损指令吗?要回答这个问题并不简单。毕竟,如果我们选择了另一个不同的股价来判断是否抛出股票,止损指令可能真的会被触发。

止损指令会被自动触发,且不会询问指令被触发的原因。这意味着股价的真实变化与短暂波动都有可能触发止损指令,而后者我们更为关心,因为你不仅要为订单支付费用,而且还无法保证以指定的价格抛出股票,这可能会使你的损失更大。与此同时,你交易股票的走势仍在继续,如果止损指令不被触发,你甚至可以从中获利。也就是说,止损指令能够帮助你保持自己的情绪,继续持有股票,即使它已经失去了自己的价值。如果你无法监控或快速访问自己的投资项目,例如在度假,它们也能发挥作用。

我曾介绍过一些关于赞成不赞成止损指令的观点,但从现在起,我不会要求我们的回溯检验系统考虑止损指令。虽然不太现实(我确实相信在工业中实际应用的系统能够考虑止损规则),但这简化了回溯检验任务。

更为真实的投资项目不会将投资总额的10%押注在一只股票上。更现实的做法是考虑在多只股票上分散投资。涉及多家公司的多笔交易可能会在任何时刻进行,并且大多数投资项目会选择股票交易,而不是现金。既然我们将在多只股票上投资,只有当移动均线交叉(不是因为止损)时才退出仓位,那么我们需要改变进行回溯检验的方式。例如,我们将使用pandas中的DataFrame来记录所有考察股票的买入、抛出订单,前面的循环代码也需要记录更多的信息。

我实现了为多只股票创建订单数据的代码,以及一个执行回溯检验的函数。

def ma_crossover_orders(stocks, fast, slow):
    fast_str = str(fast) + 'd'
    slow_str = str(slow) + 'd'
    ma_diff_str = fast_str + '-' + slow_str
 
    trades = pd.DataFrame({"Price": [], "Regime": [], "Signal": []})
    for s in stocks:
        s[1][fast_str] = np.round(s[1]["Close"].rolling(window = fast, center = False).mean(), 2)
        s[1][slow_str] = np.round(s[1]["Close"].rolling(window = slow, center = False).mean(), 2)
        s[1][ma_diff_str] = s[1][fast_str] - s[1][slow_str]
 
        s[1]["Regime"] = np.where(s[1][ma_diff_str] > 0, 1, 0)
      
        s[1]["Regime"] = np.where(s[1][ma_diff_str] < 0, -1, s[1]["Regime"])
        
        regime_orig = s[1].ix[-1, "Regime"]
        s[1].ix[-1, "Regime"] = 0
        s[1]["Signal"] = np.sign(s[1]["Regime"] - s[1]["Regime"].shift(1))
        
        s[1].ix[-1, "Regime"] = regime_orig
 
        signals = pd.concat([
            pd.DataFrame({"Price": s[1].loc[s[1]["Signal"] == 1, "Close"],
                         "Regime": s[1].loc[s[1]["Signal"] == 1, "Regime"],
                         "Signal": "Buy"}),
            pd.DataFrame({"Price": s[1].loc[s[1]["Signal"] == -1, "Close"],
                         "Regime": s[1].loc[s[1]["Signal"] == -1, "Regime"],
                         "Signal": "Sell"}),
        ])
        signals.index = pd.MultiIndex.from_product([signals.index, [s[0]]], names = ["Date", "Symbol"])
        trades = trades.append(signals)
 
    trades.sort_index(inplace = True)
    trades.index = pd.MultiIndex.from_tuples(trades.index, names = ["Date", "Symbol"])
 
    return trades
 
 
def backtest(signals, cash, port_value = .1, batch = 100):
    SYMBOL = 1 
    portfolio = dict()   
    port_prices = dict()  
    
    results = pd.DataFrame({"Start Cash": [],
                            "End Cash": [],
                            "Portfolio Value": [],
                            "Type": [],
                            "Shares": [],
                            "Share Price": [],
                            "Trade Value": [],
                            "Profit per Share": [],
                            "Total Profit": []})
 
    for index, row in signals.iterrows():
        shares = portfolio.setdefault(index[SYMBOL], 0)
        trade_val = 0
        batches = 0
        cash_change = row["Price"] * shares   
        portfolio[index[SYMBOL]] = 0  
 
        old_price = port_prices.setdefault(index[SYMBOL], row["Price"])
        portfolio_val = 0
        for key, val in portfolio.items():
            portfolio_val += val * port_prices[key]
 
        if row["Signal"] == "Buy" and row["Regime"] == 1: 
            batches = np.floor((portfolio_val + cash) * port_value) // np.ceil(batch * row["Price"]) 
            trade_val = batches * batch * row["Price"] 
            cash_change -= trade_val 
            portfolio[index[SYMBOL]] = batches * batch  
            port_prices[index[SYMBOL]] = row["Price"]   
            old_price = row["Price"]
        elif row["Signal"] == "Sell" and row["Regime"] == -1: 
            pass
 
        pprofit = row["Price"] - old_price  
        
        results = results.append(pd.DataFrame({
                "Start Cash": cash,
                "End Cash": cash + cash_change,
                "Portfolio Value": cash + cash_change + portfolio_val + trade_val,
                "Type": row["Signal"],
                "Shares": batch * batches,
                "Share Price": row["Price"],
                "Trade Value": abs(cash_change),
                "Profit per Share": pprofit,
                "Total Profit": batches * batch * pprofit
            }, index = [index]))
        cash += cash_change 
 
    results.sort_index(inplace = True)
    results.index = pd.MultiIndex.from_tuples(results.index, names = ["Date", "Symbol"])
 
    return results
 
microsoft = web.DataReader("MSFT", "yahoo", start, end)
google = web.DataReader("GOOG", "yahoo", start, end)
facebook = web.DataReader("FB", "yahoo", start, end)
twitter = web.DataReader("TWTR", "yahoo", start, end)
netflix = web.DataReader("NFLX", "yahoo", start, end)
amazon = web.DataReader("AMZN", "yahoo", start, end)
yahoo = web.DataReader("YHOO", "yahoo", start, end)
sony = web.DataReader("SNY", "yahoo", start, end)
nintendo = web.DataReader("NTDOY", "yahoo", start, end)
ibm = web.DataReader("IBM", "yahoo", start, end)
hp = web.DataReader("HPQ", "yahoo", start, end)
signals = ma_crossover_orders([("AAPL", ohlc_adj(apple)),
                              ("MSFT",  ohlc_adj(microsoft)),
                              ("GOOG",  ohlc_adj(google)),
                              ("FB",    ohlc_adj(facebook)),
                              ("TWTR",  ohlc_adj(twitter)),
                              ("NFLX",  ohlc_adj(netflix)),
                              ("AMZN",  ohlc_adj(amazon)),
                              ("YHOO",  ohlc_adj(yahoo)),
                              ("SNY",   ohlc_adj(yahoo)),
                              ("NTDOY", ohlc_adj(nintendo)),
                              ("IBM",   ohlc_adj(ibm)),
                              ("HPQ",   ohlc_adj(hp))],
                            fast = 20, slow = 50)
signals
bk = backtest(signals, 1000000)
bk
bk["Portfolio Value"].groupby(level = 0).apply(lambda x: x[-1]).plot()


8c6692fd940961ee0ea20bf84a55dc4d95d200be

这个虚拟的投资项目投资了十二只(科技)股票,最终的资产增长达到了100%左右。这很好吗?虽然表面上看起来不错,但我们会看到我们可以做得更好。

基准测试

回溯检验只是评估交易策略有效性过程的一部分。我们会对策略进行基准测试,或者与其他的可行(通常是众所周知的)策略进行比较,以确定我们所能达到的效果。

当你评估一个交易系统时,有一个策略你一定要与它比较,除了少数互惠基金与投资管理人,这个策略的效果是最好的:买入并持有SPY指数基金。有效市场假说声称,任何人都无法击败市场。因此,投资者应该一直购买反映市场结构的指数基金。SPY指数基金是一种交易所买卖基金(一种在市场上交易的类似股票的互惠基金),其价值实际上地代表着标准普尔500指数中股票的价值。通过买入并持有SPY指数基金,我们实际上可以尝试将回报与市场匹配,而不是试着去击败市场。

我通过以下方式获取关于SPY的数据,并根据收益简单地买入和持有SPY指数基金。

spyder = web.DataReader("SPY", "yahoo", start, end)
spyder.iloc[[0,-1],:]


734c7f5c9b7b5e24969ef5b8b6fb71f1d07e27db
batches = 1000000 // np.ceil(100 * spyder.ix[0,"Adj Close"]) # Maximum number of batches of stocks invested in
trade_val = batches * batch * spyder.ix[0,"Adj Close"] # How much money is used to buy SPY
final_val = batches * batch * spyder.ix[-1,"Adj Close"] + (1000000 - trade_val) # Final value of the portfolio
final_val
2180977.0
ax_bench = (spyder["Adj Close"] / spyder.ix[0, "Adj Close"]).plot(label = "SPY")
ax_bench = (bk["Portfolio Value"].groupby(level = 0).apply(lambda x: x[-1]) / 1000000).plot(ax = ax_bench, label = "Portfolio")
ax_bench.legend(ax_bench.get_lines(), [l.get_label() for l in ax_bench.get_lines()], loc = 'best')
ax_bench


164bf72465b29899d3052db799340a40a389a517

买入和持有SPY指数基金的效果优于我们的交易系统,至少优于我们现在初期的系统,而且,我们甚至没有说明,考虑到费用我们这个更加复杂的策略有多么的昂贵。考虑到机会成本和与主动投资策略相关的费用,我们不应该采用这样的策略。

我们怎样才能提高我们系统的效果呢?对于初学者而言,我们可以尝试投资多样化。我们之前考虑的所有股票都属于科技公司,这意味着如果科技行业表现不佳,我们的投资项目也会反映出这种低迷的状况。我们可以开发一个能够做空股票或看跌押注的系统,这样,我们就可以利用市场上各个行业领域的走向。我们还可以寻找一种能够预测股价变化的方法。但是,无论我们做什么,都必须击败这个基准;否则,我们的交易系统中始终会存在着机会成本。

也存在着其他的基准测试策略,如果我们的交易系统击败了"买入和持有SPY基金"这个策略,那么我们可以与这些策略进行比较。这类的交易策略包括:

  • 当每月收盘价高于十月均线时,买入SPY基金。
  • 当十月均线的动量为正时,买入SPY基金。(动量是移动平均过程中的第一个差值,即MOtq = MAtq - MAt-1q。)

(我最早在这里知道了这些策略。)普遍的经验仍然成立:对于一个包含大量活跃交易的复杂交易系统,如果一个涉及指数基金且不进行频繁交易的简单策略击败了它,那么不要使用这个复杂系统。实际上,这个要求很难满足。

最后一点,假设你的交易系统确实在回溯检验中击败了所有的基准策略。回溯检验就能够预测系统在未来的表现了吗?不太可能。回溯检验存在着过拟合的可能,所以,仅仅是回溯检验预测的上涨并不意味着在未来会保持上涨的势头。

结论

虽然这篇教程以一个令人沮丧的观点收尾,但是请记住,有效市场假说有很多的反对者。我自己的看法是,随着交易变得越来越算法化,击败市场也将变得更加困难。也就是说,击败市场仍然是有可能的,尽管互惠基金似乎还做不到(但是请记住,互惠基金表现得如此糟糕的部分原因,是因为交易所伴随的费用)。

这篇教程非常简短,只涉及一种策略类型:基于移动平均线的交易策略。在实践中也会应用许多其他的交易信号。此外,我们并没有深入讨论有关做空股票、货币交易或者股票期权的细节。特别地,股票期权的形式非常丰富,能够提供许多不同的方式来押注股票的走势。你可以在《Python衍生分析:数据分析,模型,仿真,校准与对冲》一书中了解更多关于衍生品(包括股票期权和其他衍生品)的信息,(对于犹他州立大学的学生)这本书可以在犹他州立大学图书馆中找到

另一个资源(也是我准备这篇教程时参考的文献)是O'Reilly出版的图书《Python金融分析》,也可在犹他州立大学图书馆中找到

请记住,我们可能(甚至很常见)在股市中亏损。同样,我们也很难在其他领域获得像股市那样的回报,任何投资策略都应该认真对待投资。这篇教程旨在向大家介绍评估股票交易与投资的入门知识,我希望大家能够继续研究这些观点。

问题

问题1

基于均线交叉,设计一个本篇教程中描述的交易策略(你不需要考虑止损指令)。选择至少15只自2010年1月1日就存在的股票。根据所挑选出的股票回溯检验你的策略,并模拟一个投资项目,根据SPY指数基金的效果对策略进行基准测试。
你能够击败市场吗?

问题2

实际上,每一笔交易都需要缴纳佣金。了解佣金的收取机制,并修改本文中的backtest()函数,使其能够模拟多种佣金制度(固定费用,投资金额的百分比等)。

此外,我们的均线交叉策略在两条均线交叉时会导致交易信号被触发。我们希望通过以下两种方式之一,确保信号触发机制更加健壮:

  1. 当移动均线相差固定金额时,触发交易
  2. 当移动均线相差一定数值的(滚动)标准差时,触发交易,标准差根据如下公式定义:


e1cddb5424196de91d40f0bca2073b58948dc550

(pandas确实提供了计算滚动标准差的方法。)对于后者,如果移动均线相差p x SDtn,交易信号将被释放。

为了实现这些约束,可以修改ma_crossover_orders()函数。具体来说,你应该能够设置滚动标准差中时间窗口对应的天数(并不需要与短期均线和长期均线的时间窗口保持一致),以及移动均线的标准差至少相差多少才能释放信号。(目前这些函数的实现也应保留下来,事实上,它们应作为函数的默认行为。)

一旦进行了这些修改,那么回到问题1,此外,还包括一个在模拟投资项目效果时切实可行的佣金方案(可参考某个经纪公司的做法),以及移动均线间相差的能够触发交易信号的固定金额或标准差数值。

问题3

我们并没有建立一个能够做空股票的交易系统。做空交易的手段很巧妙,因为做空所带来的损失是无限大的(另一方面,多头仓位限制了损失最多为购入资产的总值)。在这里可以了解更多关于做空交易的信息。接下来,可以修改backtest()使其支持做空交易。这个函数会怎么决定如何进行做空销售,包括做空多少股份以及在进行其他交易时如何评估做空的股票?我们将这个问题留给你来决定。提示一点,做空股票的份额在函数内部可通过负数表示。

一旦完成了这些,回到问题1,或许还需要使用到问题2实现的功能。
数十款阿里云产品限时折扣中,赶紧点击领劵开始云上实践吧!
文章原标题《An Introduction to Stock Market Data Analysis with Python (Part 2)》,作者:Curtis Miller,译者:6816816151

文章为简译,更为详细的内容,请查看原文

相关文章
|
3天前
|
机器学习/深度学习 数据可视化 数据挖掘
Python跳水:探索数据分析的深渊
Python跳水:探索数据分析的深渊
15 0
|
19天前
|
人工智能 数据可视化 数据挖掘
【python】Python航空公司客户价值数据分析(代码+论文)【独一无二】
【python】Python航空公司客户价值数据分析(代码+论文)【独一无二】
|
19天前
|
人工智能 监控 数据可视化
【Python】Python商业公司贸易业务数据分析可视化(数据+源码)【独一无二】
【Python】Python商业公司贸易业务数据分析可视化(数据+源码)【独一无二】
|
14天前
|
机器学习/深度学习 数据可视化 数据挖掘
Python在体育分析中的应用:从数据到洞察
【4月更文挑战第12天】Python在体育数据分析中扮演重要角色,利用其强大的数据处理(如Pandas, NumPy)和可视化工具(Matplotlib, Seaborn),以及机器学习库(Scikit-learn, TensorFlow),可提升球队表现和训练效率。基本流程包括数据获取、预处理、探索、模型选择与训练、评估优化及结果可视化。通过球员表现、球队战术分析和赛事预测等案例,展示了Python在体育领域的广泛应用。要精通Python体育数据分析,需持续学习和实践。
|
18天前
|
人工智能 数据可视化 数据挖掘
【python】Python国内GDP经济总量数据分析可视化(源码+报告)【独一无二】
【python】Python国内GDP经济总量数据分析可视化(源码+报告)【独一无二】
|
24天前
|
人工智能 供应链 数据可视化
【python】python国内社会消费品零售总额数据分析(代码+数据+报告)【独一无二】
【python】python国内社会消费品零售总额数据分析(代码+数据+报告)【独一无二】
【python】python国内社会消费品零售总额数据分析(代码+数据+报告)【独一无二】
|
4月前
|
数据可视化 数据挖掘 Python
Python数据挖掘实用案例——自动售货机销售数据分析与应用(二)
Python数据挖掘实用案例——自动售货机销售数据分析与应用(二)
376 0
|
3月前
|
数据采集 存储 数据可视化
Python爬取哈尔滨旅游爆火视频数据并进行可视化分析
Python爬取哈尔滨旅游爆火视频数据并进行可视化分析
|
4月前
|
Linux Python Windows
python疫情分析,全唐诗分析,广告收入分析
python疫情分析,全唐诗分析,广告收入分析
38 1
|
7月前
|
数据采集 数据可视化 数据挖掘
Python实战项目——物流行业数据分析(二)
Python实战项目——物流行业数据分析(二)
209 0