本文由 伯樂在線 - 小米雲豆粥 翻譯。未經許可,禁止轉載!
英文出處:Curtis Miller。歡迎加入翻譯組。
這篇博文是用Python分析股市數據系列兩部中的第二部,內容基於我在猶他大學 數學3900 (數據科學)的課程 (閱讀第一部分)。在這兩篇博文中,我會討論一些基礎知識,包括比如如何用pandas從雅虎財經獲得數據, 可視化股市數據,平均數指標的定義,設計移動平均交匯點分析移動平均線的方法,回溯測試和 基准分析法。這篇文章會討論如何設計用移動平均交匯點分析移動平均線的系統,如何做回溯測試和基准分析,最后留有一些練習題以饗讀者。
注意:本文僅代表作者本人的觀點。文中的內容不應該被當做經濟建議。我不對文中代碼負責,取用者自己負責
交易策略
在特定的預期條件達成時一個開放頭寸會被關閉。多頭頭寸表示交易中需要金融商品價格上升才能產生盈利,空頭頭寸表示交易中需要金融商品價格下降才能產生盈利。在股票交易中,多頭頭寸是牛市,空頭頭寸是熊市,反之則不成立。(股票期權交易中這個非常典型)
例如你在預計股價上漲的情況下購入股票,並計划在股票價格上漲高於購入價時拋出,這就是多頭頭寸。就是說你持有一定的金融產品,如果它們價格上漲,你將會獲利,並且沒有上限;如果它們價格下降,你會虧損。由於股票價格不會為負,虧損是有限度的。相反的,如果你預計股價會下跌,就從交易公司借貸股票然后賣出,同時期待未來股票價格下降后再低價買入還貸來賺取差額,這就是空頭股票。如果股價下跌你會獲利。空頭頭寸的獲利額度受股價所限(最佳情況就是股票變得一文不值,你不用花錢就能將它們買回來),而損失卻沒有下限,因為你有可能需要花很多錢才能買回股票。所以交換所只會在確定投資者有很好的經濟基礎的情況下才會讓他們空頭借貸股票。
所有股民都應該決定他在每一股上可以冒多大的風險。比如有人決定無論什么情況他都不會在某一次交易中投入總額的10%去冒險。同時在交易中,股民要有一個撤出策略,這是讓股民退出頭寸的各種條件。股民也可以設置一個目標,這是導致股民退出頭寸的最小盈利額。同樣的,股民也需要有一個他能承受的最大損失額度。當預計損失大於可承受額度時,股民應該退出頭寸以避免更大損失(這可以通過設置停止損失委托來避免未來的損失)。
我們要設計一個交易策略,它包含用於快速交易的交易激發信號、決定交易額度的規則和完整的退出策略。我們的目標是設計並評估該交易策略。
假設每次交易金額占總額的比例是固定的(10%)。同時設定在每一次交易中,如果損失超過了20%的交易值,我們就退出頭寸。現在我們要決定什么時候進入頭寸,什么時候退出以保證盈利。
這里我要演示移動平均交匯點分析移動平均線的方法。我會使用兩條移動平均線,一條快速的,另一條是慢速的。我們的策略是:
- 當快速移動平均線和慢速移動線交匯時開始交易
- 當快速移動平均線和慢速移動線再次交匯時停止交易
做多是指在快速平均線上升到慢速平均線之上時開始交易,當快速平均線下降到慢速平均線之下時停止交易。賣空正好相反,它是指在快速平均線下降到慢速平均線之下時開始交易,快速平均線上升到慢速平均線之上時停止交易。
現在我們有一整套策略了。在使用它之前我們需要先做一下測試。回溯測試是一個常用的測試方法,它使用歷史數據來看策略是否會盈利。例如這張蘋果公司的股票價值圖,如果20天的移動平均是快速線,50天的移動平均是慢速線,那么我們這個策略不是很掙錢,至少在你一直做多頭頭寸的時候。
下面讓我們來自動化回溯測試的過程。首先我們要識別什么時候20天平均線在50天之下,以及之上。
apple['20d-50d'] =apple['20d'] -apple['50d'] apple.tail()
| Open | High | Low | Close | Volume | Adj Close | 20d | 50d | 200d | 20d-50d | |
|---|---|---|---|---|---|---|---|---|---|---|
| Date | ||||||||||
| 2016-08-26 | 107.410004 | 107.949997 | 106.309998 | 106.940002 | 27766300 | 106.940002 | 107.87 | 101.51 | 102.73 | 6.36 |
| 2016-08-29 | 106.620003 | 107.440002 | 106.290001 | 106.820000 | 24970300 | 106.820000 | 107.91 | 101.74 | 102.68 | 6.17 |
| 2016-08-30 | 105.800003 | 106.500000 | 105.500000 | 106.000000 | 24863900 | 106.000000 | 107.98 | 101.96 | 102.63 | 6.02 |
| 2016-08-31 | 105.660004 | 106.570000 | 105.639999 | 106.099998 | 29662400 | 106.099998 | 108.00 | 102.16 | 102.60 | 5.84 |
| 2016-09-01 | 106.139999 | 106.800003 | 105.620003 | 106.730003 | 26643600 | 106.730003 | 108.04 | 102.39 | 102.56 | 5.65 |
我們將差異的符號稱為狀態轉換。快速移動平均線在慢速移動平均線之上代表牛市狀態;相反則為熊市。以下的代碼用於識別狀態轉換。
# np.where() is a vectorized if-else function, where a condition is checked for each component of a vector, and the first argument passed is used when the condition holds, and the other passed if it does not apple["Regime"] = np.where(apple['20d-50d'] > 0, 1, 0) # We have 1's for bullish regimes and 0's for everything else. Below I replace bearish regimes's values with -1, and to maintain the rest of the vector, the second argument is apple["Regime"] 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)

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

apple["Regime"].value_counts()
1 966 -1 663 0 50 Name: Regime, dtype: int64
從上面的曲線可以看到有966天蘋果公司的股票是牛市,663天是熊市,有54天沒有傾向性。(原文中牛市和熊市說反了,譯文中更正;原文數字跟代碼結果對不上,譯文按照代碼結果更正)
交易信號出現在狀態轉換之時。牛市出現時,買入信號被激活;牛市完結時,賣出信號被激活。同樣的,熊市出現時賣出信號被激活,熊市結束時,買入信號被激活。(只有在你空頭股票,或者使用一些其他的方法例如用股票期權賭市場的時候這種情況才對你有利)

# To ensure that all trades close out, I temporarily change the regime of the last row to 0 regime_orig = apple.ix[-1, "Regime"] apple.ix[-1, "Regime"] = 0 apple["Signal"] = np.sign(apple["Regime"] - apple["Regime"].shift(1)) # Restore original regime data apple.ix[-1, "Regime"] = regime_orig apple.tail()
| Open | High | Low | Close | Volume | Adj Close | 20d | 50d | 200d | 20d-50d | Regime | Signal | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Date | ||||||||||||
| 2016-08-26 | 107.410004 | 107.949997 | 106.309998 | 106.940002 | 27766300 | 106.940002 | 107.87 | 101.51 | 102.73 | 6.36 | 1.0 | 0.0 |
| 2016-08-29 | 106.620003 | 107.440002 | 106.290001 | 106.820000 | 24970300 | 106.820000 | 107.91 | 101.74 | 102.68 | 6.17 | 1.0 | 0.0 |
| 2016-08-30 | 105.800003 | 106.500000 | 105.500000 | 106.000000 | 24863900 | 106.000000 | 107.98 | 101.96 | 102.63 | 6.02 | 1.0 | 0.0 |
| 2016-08-31 | 105.660004 | 106.570000 | 105.639999 | 106.099998 | 29662400 | 106.099998 | 108.00 | 102.16 | 102.60 | 5.84 | 1.0 | 0.0 |
| 2016-09-01 | 106.139999 | 106.800003 | 105.620003 | 106.730003 | 26643600 | 106.730003 | 108.04 | 102.39 | 102.56 | 5.65 | 1.0 | -1.0 |
apple["Signal"].plot(ylim =(-2, 2))

apple["Signal"].value_counts()
0.0 1637
-1.0 21
1.0 20
Name: Signal, dtype: int64
我們會買入蘋果公司的股票20次,拋出21次 (原文數字跟代碼結果不符,譯文根據代碼結果更正)。如果我們只選了蘋果公司的股票,六年內只有21次交易發生。如果每次多頭轉空頭的時候我們都采取行動,我們將會參與21次交易。(請記住交易次數不是越多越好,畢竟交易不是免費的)
你也許注意到了這個系統不是很穩定。快速平均線在慢速平均線之上就激發交易,即使這個狀態只是短短一瞬,這樣會導致交易馬上終止(這樣並不好因為現實中每次交易都要付費,這個費用會很快消耗掉收益)。同時所有的牛市瞬間轉為熊市,如果你允許同時押熊市和牛市,那就會出現每次交易結束就自動激發另一場押相反方向交易的詭異情況。更好的系統會要求有更多的證據來證明市場的發展方向,但是這里我們不去追究那個細節。
下面我們來看看每次買入賣出時候的股票價格。
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
# Create a DataFrame with trades, including the price at the trade and the regime under which the trade is made. 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
| Price |
Regime |
Signal |
|
| Date |
|
|
|
| 2010-03-16 |
224.449997 |
1.0 |
Buy |
| 2010-06-11 |
253.509995 |
-1.0 |
Sell |
| 2010-06-18 |
274.070011 |
1.0 |
Buy |
| 2010-07-22 |
259.020000 |
-1.0 |
Sell |
| 2010-09-20 |
283.230007 |
1.0 |
Buy |
| 2011-03-30 |
348.630009 |
0.0 |
Sell |
| 2011-03-31 |
348.510006 |
-1.0 |
Sell |
| 2011-05-12 |
346.569988 |
1.0 |
Buy |
| 2011-05-27 |
337.409992 |
-1.0 |
Sell |
| 2011-07-14 |
357.770004 |
1.0 |
Buy |
| 2011-11-17 |
377.410000 |
-1.0 |
Sell |
| 2011-12-28 |
402.640003 |
1.0 |
Buy |
| 2012-05-09 |
569.180023 |
-1.0 |
Sell |
| 2012-06-25 |
570.770020 |
1.0 |
Buy |
| 2012-10-17 |
644.610001 |
-1.0 |
Sell |
| 2013-05-17 |
433.260010 |
1.0 |
Buy |
| 2013-06-26 |
398.069992 |
-1.0 |
Sell |
| 2013-07-31 |
452.529984 |
1.0 |
Buy |
| 2013-10-03 |
483.409996 |
-1.0 |
Sell |
| 2013-10-16 |
501.110001 |
1.0 |
Buy |
| 2014-01-28 |
506.499977 |
-1.0 |
Sell |
| 2014-03-26 |
539.779991 |
1.0 |
Buy |
| 2014-04-22 |
531.700020 |
-1.0 |
Sell |
| 2014-04-25 |
571.939980 |
1.0 |
Buy |
| 2014-06-11 |
93.860001 |
-1.0 |
Sell |
| 2014-08-18 |
99.160004 |
1.0 |
Buy |
| 2014-10-17 |
97.669998 |
-1.0 |
Sell |
| 2014-10-28 |
106.739998 |
1.0 |
Buy |
| 2015-01-05 |
106.250000 |
-1.0 |
Sell |
| 2015-02-05 |
119.940002 |
1.0 |
Buy |
| 2015-04-16 |
126.169998 |
-1.0 |
Sell |
| 2015-04-28 |
130.559998 |
1.0 |
Buy |
| 2015-06-25 |
127.500000 |
-1.0 |
Sell |
| 2015-10-27 |
114.550003 |
1.0 |
Buy |
| 2015-12-18 |
106.029999 |
-1.0 |
Sell |
| 2016-03-11 |
102.260002 |
1.0 |
Buy |
| 2016-05-05 |
93.239998 |
-1.0 |
Sell |
| 2016-07-01 |
95.889999 |
1.0 |
Buy |
| 2016-07-08 |
96.680000 |
-1.0 |
Sell |
| 2016-07-25 |
97.339996 |
1.0 |
Buy |
| 2016-09-01 |
106.730003 |
1.0 |
Sell |
# Let's see the profitability of long trades 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
| End Date | Price | Profit | |
|---|---|---|---|
| Date | |||
| 2010-03-16 | 2010-06-11 | 224.449997 | 29.059998 |
| 2010-06-18 | 2010-07-22 | 274.070011 | -15.050011 |
| 2010-09-20 | 2011-03-30 | 283.230007 | 65.400002 |
| 2011-05-12 | 2011-05-27 | 346.569988 | -9.159996 |
| 2011-07-14 | 2011-11-17 | 357.770004 | 19.639996 |
| 2011-12-28 | 2012-05-09 | 402.640003 | 166.540020 |
| 2012-06-25 | 2012-10-17 | 570.770020 | 73.839981 |
| 2013-05-17 | 2013-06-26 | 433.260010 | -35.190018 |
| 2013-07-31 | 2013-10-03 | 452.529984 | 30.880012 |
| 2013-10-16 | 2014-01-28 | 501.110001 | 5.389976 |
| 2014-03-26 | 2014-04-22 | 539.779991 | -8.079971 |
| 2014-04-25 | 2014-06-11 | 571.939980 | -478.079979 |
| 2014-08-18 | 2014-10-17 | 99.160004 | -1.490006 |
| 2014-10-28 | 2015-01-05 | 106.739998 | -0.489998 |
| 2015-02-05 | 2015-04-16 | 119.940002 | 6.229996 |
| 2015-04-28 | 2015-06-25 | 130.559998 | -3.059998 |
| 2015-10-27 | 2015-12-18 | 114.550003 | -8.520004 |
| 2016-03-11 | 2016-05-05 | 102.260002 | -9.020004 |
| 2016-07-01 | 2016-07-08 | 95.889999 | 0.790001 |
| 2016-07-25 | 2016-09-01 | 97.339996 | 9.390007 |
從上表可以看出2013年5月17日那天蘋果公司股票價格大跌,我們的系統會表現很差。但是那個價格下降不是因為蘋果遇到了什么大危機,而僅僅是一次分股。由於分紅不如分股那么顯著,這也許會影響系統行為。
# Let's see the result over the whole period for which we have Apple data pandas_candlestick_ohlc(apple, stick = 45, otherseries = ["20d", "50d", "200d"])

我們不希望我們的交易系統的表現受到分紅和分股的影響。一個解決方案是利用歷史的分紅分股數據來設計交易系統,這些數據可以真實地反映股市的行為從而幫助我們找到最佳解決方案,但是這個方法要更復雜一些。另一個方案就是根據分紅和分股來調整股票的價格。
雅虎財經只提供調整之后的股票閉市價格,不過這些對於我們調整開市,高價和低價已經足夠了。調整閉市股價是這樣實現的:

讓我們回到開始,先調整股票價格,然后再來評價我們的交易系統。
def ohlc_adj(dat): """ :param dat: pandas DataFrame with stock data, including "Open", "High", "Low", "Close", and "Adj Close", with "Adj Close" containing adjusted closing prices :return: pandas DataFrame with adjusted stock data This function adjusts stock data for splits, dividends, etc., returning a data frame with "Open", "High", "Low" and "Close" columns. The input DataFrame is similar to that returned by pandas Yahoo! Finance API. """ 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) # This next code repeats all the earlier analysis we did on the adjusted data 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'] # np.where() is a vectorized if-else function, where a condition is checked for each component of a vector, and the first argument passed is used when the condition holds, and the other passed if it does not apple_adj["Regime"] = np.where(apple_adj['20d-50d'] > 0, 1, 0) # We have 1's for bullish regimes and 0's for everything else. Below I replace bearish regimes's values with -1, and to maintain the rest of the vector, the second argument is apple["Regime"] apple_adj["Regime"] = np.where(apple_adj['20d-50d'] < 0, -1, apple_adj["Regime"]) # To ensure that all trades close out, I temporarily change the regime of the last row to 0 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)) # Restore original regime data apple_adj.ix[-1, "Regime"] = regime_orig # Create a DataFrame with trades, including the price at the trade and the regime under which the trade is made. 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"])

apple_adj_long_profits
| End Date | Price | Profit | |
|---|---|---|---|
| Date | |||
| 2010-03-16 | 2010-06-10 | 29.355667 | 3.408371 |
| 2010-06-18 | 2010-07-22 | 35.845436 | -1.968381 |
| 2010-09-20 | 2011-03-30 | 37.043466 | 8.553623 |
| 2011-05-12 | 2011-05-27 | 45.327660 | -1.198030 |
| 2011-07-14 | 2011-11-17 | 46.792503 | 2.568702 |
| 2011-12-28 | 2012-05-09 | 52.661020 | 21.781659 |
| 2012-06-25 | 2012-10-17 | 74.650634 | 10.019459 |
| 2013-05-17 | 2013-06-26 | 57.882798 | -4.701326 |
| 2013-07-31 | 2013-10-04 | 60.457234 | 4.500835 |
| 2013-10-16 | 2014-01-28 | 67.389473 | 1.122523 |
| 2014-03-11 | 2014-03-17 | 72.948554 | -1.272298 |
| 2014-03-24 | 2014-04-22 | 73.370393 | -1.019203 |
| 2014-04-25 | 2014-10-17 | 77.826851 | 16.191371 |
| 2014-10-28 | 2015-01-05 | 102.749105 | -0.028185 |
| 2015-02-05 | 2015-04-16 | 116.413846 | 6.046838 |
| 2015-04-28 | 2015-06-26 | 126.721620 | -3.184117 |
| 2015-10-27 | 2015-12-18 | 112.152083 | -7.897288 |
| 2016-03-10 | 2016-05-05 | 100.015950 | -7.278331 |
| 2016-06-23 | 2016-06-27 | 95.582210 | -4.038123 |
| 2016-06-30 | 2016-07-11 | 95.084904 | 1.372569 |
| 2016-07-25 | 2016-09-01 | 96.815526 | 9.914477 |
可以看到根據分紅和分股調整之后的價格圖變得很不一樣了。之后的分析我們都會用到這個調整之后的數據。
假設我們在股市有一百萬,讓我們來看看根據下面的條件,我們的系統會如何反應:
- 每次用總額的10%來進行交易
- 退出頭寸如果虧損達到了交易額的20%
模擬的時候要記住:
- 每次交易有100支股票
- 我們的避損規則是當股票價格下降到一定數值時就拋出。我們需要檢查這段時間內的低價是否低到可以出發避損規則。現實中除非我們買入看空期權,我們無法保證我們能以設定低值價格賣出股票。這里為了簡潔我們將設定值作為賣出值。
- 每次交易都會付給中介一定的佣金。這里我們沒有考慮這個。
下面的代碼演示了如何實現回溯測試:
# We need to get the low of the price during each trade. tradeperiods =pd.DataFrame({"Start": apple_adj_long_profits.index, "End": apple_adj_long_profits["End Date"]}) apple_adj_long_profits["Low"] =tradeperiods.apply(lambdax: min(apple_adj.loc[x["Start"]:x["End"], "Low"]), axis =1) apple_adj_long_profits
| End Date |
Price |
Profit |
Low |
|
| Date |
|
|
|
|
| 2010-03-16 |
2010-06-10 |
29.355667 |
3.408371 |
26.059775 |
| 2010-06-18 |
2010-07-22 |
35.845436 |
-1.968381 |
31.337127 |
| 2010-09-20 |
2011-03-30 |
37.043466 |
8.553623 |
35.967068 |
| 2011-05-12 |
2011-05-27 |
45.327660 |
-1.198030 |
43.084626 |
| 2011-07-14 |
2011-11-17 |
46.792503 |
2.568702 |
46.171251 |
| 2011-12-28 |
2012-05-09 |
52.661020 |
21.781659 |
52.382438 |
| 2012-06-25 |
2012-10-17 |
74.650634 |
10.019459 |
73.975759 |
| 2013-05-17 |
2013-06-26 |
57.882798 |
-4.701326 |
52.859502 |
| 2013-07-31 |
2013-10-04 |
60.457234 |
4.500835 |
60.043080 |
| 2013-10-16 |
2014-01-28 |
67.389473 |
1.122523 |
67.136651 |
| 2014-03-11 |
2014-03-17 |
72.948554 |
-1.272298 |
71.167335 |
| 2014-03-24 |
2014-04-22 |
73.370393 |
-1.019203 |
69.579335 |
| 2014-04-25 |
2014-10-17 |
77.826851 |
16.191371 |
76.740971 |
| 2014-10-28 |
2015-01-05 |
102.749105 |
-0.028185 |
101.411076 |
| 2015-02-05 |
2015-04-16 |
116.413846 |
6.046838 |
114.948237 |
| 2015-04-28 |
2015-06-26 |
126.721620 |
-3.184117 |
119.733299 |
| 2015-10-27 |
2015-12-18 |
112.152083 |
-7.897288 |
104.038477 |
| 2016-03-10 |
2016-05-05 |
100.015950 |
-7.278331 |
91.345994 |
| 2016-06-23 |
2016-06-27 |
95.582210 |
-4.038123 |
91.006996 |
| 2016-06-30 |
2016-07-11 |
95.084904 |
1.372569 |
93.791913 |
| 2016-07-25 |
2016-09-01 |
96.815526 |
9.914477 |
95.900485 |
# Now we have all the information needed to simulate this strategy in apple_adj_long_profits 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# Max proportion of portfolio bet on any trade batch =100# Number of shares bought per batch stoploss =.2# % of trade loss that would trigger a stoploss forindex, row inapple_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"] # How much money is put on the line with each trade ifrow["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 # Compute profits # Add a row to the backtest data frame containing the results of the trade 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
| End Date |
End Port. Value |
Profit per Share |
Share Price |
Shares |
Start Port. Value |
Stop-Loss Triggered |
Total Profit |
Trade Value |
|
| 2010-03-16 |
2010-06-10 |
1.011588e+06 |
3.408371 |
29.355667 |
3400.0 |
1.000000e+06 |
0.0 |
11588.4614 |
99809.2678 |
| 2010-06-18 |
2010-07-22 |
1.006077e+06 |
-1.968381 |
35.845436 |
2800.0 |
1.011588e+06 |
0.0 |
-5511.4668 |
100367.2208 |
| 2010-09-20 |
2011-03-30 |
1.029172e+06 |
8.553623 |
37.043466 |
2700.0 |
1.006077e+06 |
0.0 |
23094.7821 |
100017.3582 |
| 2011-05-12 |
2011-05-27 |
1.026536e+06 |
-1.198030 |
45.327660 |
2200.0 |
1.029172e+06 |
0.0 |
-2635.6660 |
99720.8520 |
| 2011-07-14 |
2011-11-17 |
1.031930e+06 |
2.568702 |
46.792503 |
2100.0 |
1.026536e+06 |
0.0 |
5394.2742 |
98264.2563 |
| 2011-12-28 |
2012-05-09 |
1.073316e+06 |
21.781659 |
52.661020 |
1900.0 |
1.031930e+06 |
0.0 |
41385.1521 |
100055.9380 |
| 2012-06-25 |
2012-10-17 |
1.087343e+06 |
10.019459 |
74.650634 |
1400.0 |
1.073316e+06 |
0.0 |
14027.2426 |
104510.8876 |
| 2013-05-17 |
2013-06-26 |
1.078880e+06 |
-4.701326 |
57.882798 |
1800.0 |
1.087343e+06 |
0.0 |
-8462.3868 |
104189.0364 |
| 2013-07-31 |
2013-10-04 |
1.086532e+06 |
4.500835 |
60.457234 |
1700.0 |
1.078880e+06 |
0.0 |
7651.4195 |
102777.2978 |
| 2013-10-16 |
2014-01-28 |
1.088328e+06 |
1.122523 |
67.389473 |
1600.0 |
1.086532e+06 |
0.0 |
1796.0368 |
107823.1568 |
| 2014-03-11 |
2014-03-17 |
1.086547e+06 |
-1.272298 |
72.948554 |
1400.0 |
1.088328e+06 |
0.0 |
-1781.2172 |
102127.9756 |
| 2014-03-24 |
2014-04-22 |
1.085120e+06 |
-1.019203 |
73.370393 |
1400.0 |
1.086547e+06 |
0.0 |
-1426.8842 |
102718.5502 |
| 2014-04-25 |
2014-10-17 |
1.106169e+06 |
16.191371 |
77.826851 |
1300.0 |
1.085120e+06 |
0.0 |
21048.7823 |
101174.9063 |
| 2014-10-28 |
2015-01-05 |
1.106140e+06 |
-0.028185 |
102.749105 |
1000.0 |
1.106169e+06 |
0.0 |
-28.1850 |
102749.1050 |
| 2015-02-05 |
2015-04-16 |
1.111582e+06 |
6.046838 |
116.413846 |
900.0 |
1.106140e+06 |
0.0 |
5442.1542 |
104772.4614 |
| 2015-04-28 |
2015-06-26 |
1.109035e+06 |
-3.184117 |
126.721620 |
800.0 |
1.111582e+06 |
0.0 |
-2547.2936 |
101377.2960 |
| 2015-10-27 |
2015-12-18 |
1.101928e+06 |
-7.897288 |
112.152083 |
900.0 |
1.109035e+06 |
0.0 |
-7107.5592 |
100936.8747 |
| 2016-03-10 |
2016-05-05 |
1.093921e+06 |
-7.278331 |
100.015950 |
1100.0 |
1.101928e+06 |
0.0 |
-8006.1641 |
110017.5450 |
| 2016-06-23 |
2016-06-27 |
1.089480e+06 |
-4.038123 |
95.582210 |
1100.0 |
1.093921e+06 |
0.0 |
-4441.9353 |
105140.4310 |
| 2016-06-30 |
2016-07-11 |
1.090989e+06 |
1.372569 |
95.084904 |
1100.0 |
1.089480e+06 |
0.0 |
1509.8259 |
104593.3944 |
| 2016-07-25 |
2016-09-01 |
1.101895e+06 |
9.914477 |
96.815526 |
1100.0 |
1.090989e+06 |
0.0 |
10905.9247 |
106497.0786 |
apple_backtest["End Port. Value"].plot()

我們的財產總額六年增加了10%。考慮到每次交易額只有總額的10%,這個成績不算差。
同時我們也注意到這個策略並沒有引發停止損失委托。這意味着我們可以不需要它么?這個難說。畢竟這個激發事件完全取決於我們的設定值。
停止損失委托是被自動激活的,它並不會考慮股市整體走勢。也就是說不論是股市真正的走低還是暫時的波動都會激發停止損失委托。而后者是我們需要注意的因為在現實中,由價格波動激發停止損失委托不僅讓你支出一筆交易費用,同時還無法保證最終的賣出價格是你設定的價格。
下面的鏈接分別支持和反對使用停止損失委托,但是之后的內容我不會要求我們的回溯測試系統使用它。這樣可以簡化系統,但不是很符合實際(我相信工業系統應該有停止損失委托)。
現實中我們不會只用總額的10%去押一支股票而是投資多種股票。在給定的時間可以跟不同公司同時交易,而且大部分財產應該在股票上,而不是現金。現在我們開始投資多支股票 (原文是stops,感覺是typo,譯文按照stocks翻譯),並且在兩條移動平均線交叉的時候退市(不使用止損)。我們需要改變回溯測試的代碼。我們會用一個pandas的DataFrame來存儲所有股票的買賣,上一層的循環也需要記錄更多的信息。
下面的函數用於產生買賣訂單,以及另一回溯測試函數。
def ma_crossover_orders(stocks, fast, slow): """ :param stocks: A list of tuples, the first argument in each tuple being a string containing the ticker symbol of each stock (or however you want the stock represented, so long as it's unique), and the second being a pandas DataFrame containing the stocks, with a "Close" column and indexing by date (like the data frames returned by the Yahoo! Finance API) :param fast: Integer for the number of days used in the fast moving average :param slow: Integer for the number of days used in the slow moving average :return: pandas DataFrame containing stock orders This function takes a list of stocks and determines when each stock would be bought or sold depending on a moving average crossover strategy, returning a data frame with information about when the stocks in the portfolio are bought or sold according to the strategy """ fast_str =str(fast) +'d' slow_str =str(slow) +'d' ma_diff_str =fast_str +'-'+slow_str trades =pd.DataFrame({"Price": [], "Regime": [], "Signal": []}) fors instocks: # Get the moving averages, both fast and slow, along with the difference in the moving averages 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] # np.where() is a vectorized if-else function, where a condition is checked for each component of a vector, and the first argument passed is used when the condition holds, and the other passed if it does not s[1]["Regime"] =np.where(s[1][ma_diff_str] > 0, 1, 0) # We have 1's for bullish regimes and 0's for everything else. Below I replace bearish regimes's values with -1, and to maintain the rest of the vector, the second argument is apple["Regime"] s[1]["Regime"] =np.where(s[1][ma_diff_str] < 0, -1, s[1]["Regime"]) # To ensure that all trades close out, I temporarily change the regime of the last row to 0 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)) # Restore original regime data s[1].ix[-1, "Regime"] =regime_orig # Get signals 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"]) returntrades defbacktest(signals, cash, port_value =.1, batch =100): """ :param signals: pandas DataFrame containing buy and sell signals with stock prices and symbols, like that returned by ma_crossover_orders :param cash: integer for starting cash value :param port_value: maximum proportion of portfolio to risk on any single trade :param batch: Trading batch sizes :return: pandas DataFrame with backtesting results This function backtests strategies, with the signals generated by the strategies being passed in the signals DataFrame. A fictitious portfolio is simulated and the returns generated by this portfolio are reported. """ SYMBOL =1# Constant for which element in index represents symbol portfolio =dict() # Will contain how many stocks are in the portfolio for a given symbol port_prices =dict() # Tracks old trade prices for determining profits # Dataframe that will contain backtesting report results =pd.DataFrame({"Start Cash": [], "End Cash": [], "Portfolio Value": [], "Type": [], "Shares": [], "Share Price": [], "Trade Value": [], "Profit per Share": [], "Total Profit": []}) forindex, row insignals.iterrows(): # These first few lines are done for any trade shares =portfolio.setdefault(index[SYMBOL], 0) trade_val =0 batches =0 cash_change =row["Price"] *shares # Shares could potentially be a positive or negative number (cash_change will be added in the end; negative shares indicate a short) portfolio[index[SYMBOL]] =0# For a given symbol, a position is effectively cleared old_price =port_prices.setdefault(index[SYMBOL], row["Price"]) portfolio_val =0 forkey, val inportfolio.items(): portfolio_val +=val *port_prices[key] ifrow["Signal"] =="Buy"androw["Regime"] ==1: # Entering a long position batches =np.floor((portfolio_val +cash) *port_value) //np.ceil(batch *row["Price"]) # Maximum number of batches of stocks invested in trade_val =batches *batch *row["Price"] # How much money is put on the line with each trade cash_change -=trade_val # We are buying shares so cash will go down portfolio[index[SYMBOL]] =batches *batch # Recording how many shares are currently invested in the stock port_prices[index[SYMBOL]] =row["Price"] # Record price old_price =row["Price"] elifrow["Signal"] =="Sell"androw["Regime"] ==-1: # Entering a short pass # Do nothing; can we provide a method for shorting the market? #else: #raise ValueError("I don't know what to do with signal " + row["Signal"]) pprofit =row["Price"] -old_price # Compute profit per share; old_price is set in such a way that entering a position results in a profit of zero # Update report 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 # Final change to cash balance results.sort_index(inplace =True) results.index =pd.MultiIndex.from_tuples(results.index, names =["Date", "Symbol"]) returnresults # Get more stocks 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
| Price |
Regime |
Signal |
|
|
| Date |
Symbol |
|
|
|
| 2010-03-16 |
AAPL |
29.355667 |
1.0 |
Buy |
|
|
AMZN |
131.789993 |
1.0 |
Buy |
|
|
GOOG |
282.318173 |
-1.0 |
Sell |
|
|
HPQ |
20.722316 |
1.0 |
Buy |
|
|
IBM |
110.563240 |
1.0 |
Buy |
|
|
MSFT |
24.677580 |
-1.0 |
Sell |
|
|
NFLX |
10.090000 |
1.0 |
Buy |
|
|
NTDOY |
37.099998 |
1.0 |
Buy |
|
|
SNY |
16.360001 |
-1.0 |
Sell |
|
|
YHOO |
16.360001 |
-1.0 |
Sell |
| 2010-03-17 |
SNY |
16.500000 |
1.0 |
Buy |
|
|
YHOO |
16.500000 |
1.0 |
Buy |
| 2010-03-22 |
GOOG |
278.472004 |
1.0 |
Buy |
| 2010-03-23 |
MSFT |
25.106096 |
1.0 |
Buy |
| 2010-05-03 |
GOOG |
265.035411 |
-1.0 |
Sell |
| 2010-05-10 |
HPQ |
19.435830 |
-1.0 |
Sell |
| 2010-05-14 |
NTDOY |
35.799999 |
-1.0 |
Sell |
| 2010-05-17 |
SNY |
16.270000 |
-1.0 |
Sell |
|
|
YHOO |
16.270000 |
-1.0 |
Sell |
| 2010-05-19 |
AMZN |
124.589996 |
-1.0 |
Sell |
|
|
MSFT |
23.835187 |
-1.0 |
Sell |
| 2010-05-21 |
IBM |
108.322991 |
-1.0 |
Sell |
| 2010-06-10 |
AAPL |
32.764038 |
0.0 |
Sell |
| 2010-06-11 |
AAPL |
33.156405 |
-1.0 |
Sell |
| 2010-06-18 |
AAPL |
35.845436 |
1.0 |
Buy |
| 2010-06-28 |
IBM |
111.397697 |
1.0 |
Buy |
| 2010-07-01 |
IBM |
105.861499 |
-1.0 |
Sell |
| 2010-07-06 |
IBM |
106.630175 |
1.0 |
Buy |
| 2010-07-09 |
NTDOY |
36.950001 |
1.0 |
Buy |
| 2010-07-20 |
IBM |
109.298956 |
-1.0 |
Sell |
| … |
… |
… |
… |
… |
| 2016-06-23 |
AAPL |
95.582210 |
1.0 |
Buy |
|
|
TWTR |
17.040001 |
1.0 |
Buy |
| 2016-06-27 |
AAPL |
91.544087 |
-1.0 |
Sell |
|
|
FB |
108.970001 |
-1.0 |
Sell |
| 2016-06-28 |
SNY |
36.040001 |
-1.0 |
Sell |
|
|
YHOO |
36.040001 |
-1.0 |
Sell |
| 2016-06-30 |
AAPL |
95.084904 |
1.0 |
Buy |
|
|
NFLX |
91.480003 |
0.0 |
Sell |
| 2016-07-01 |
NFLX |
96.669998 |
-1.0 |
Sell |
|
|
SNY |
37.990002 |
1.0 |
Buy |
|
|
YHOO |
37.990002 |
1.0 |
Buy |
| 2016-07-11 |
AAPL |
96.457473 |
-1.0 |
Sell |
|
|
NTDOY |
27.700001 |
1.0 |
Buy |
| 2016-07-14 |
MSFT |
53.407133 |
1.0 |
Buy |
| 2016-07-25 |
AAPL |
96.815526 |
1.0 |
Buy |
|
|
FB |
121.629997 |
1.0 |
Buy |
| 2016-07-26 |
GOOG |
738.419983 |
1.0 |
Buy |
| 2016-08-18 |
NFLX |
96.160004 |
1.0 |
Buy |
| 2016-09-01 |
AAPL |
106.730003 |
1.0 |
Sell |
| 2016-09-02 |
AMZN |
772.440002 |
1.0 |
Sell |
|
|
FB |
126.510002 |
1.0 |
Sell |
|
|
GOOG |
771.460022 |
1.0 |
Sell |
|
|
HPQ |
14.490000 |
1.0 |
Sell |
|
|
IBM |
159.550003 |
1.0 |
Sell |
|
|
MSFT |
57.669998 |
1.0 |
Sell |
|
|
NFLX |
97.379997 |
1.0 |
Sell |
|
|
NTDOY |
28.840000 |
1.0 |
Sell |
|
|
SNY |
43.279999 |
1.0 |
Sell |
|
|
TWTR |
19.549999 |
1.0 |
Sell |
|
|
YHOO |
43.279999 |
1.0 |
Sell |
475 rows × 3 columns
bk =backtest(signals, 1000000)
bk
| End Cash |
Portfolio Value |
Profit per Share |
Share Price |
Shares |
Start Cash |
Total Profit |
Trade Value |
Type |
|
|
| Date |
Symbol |
|
|
|
|
|
|
|
|
|
| 2010-03-16 |
AAPL |
9.001907e+05 |
1.000000e+06 |
0.000000 |
29.355667 |
3400.0 |
1.000000e+06 |
0.0 |
99809.2678 |
Buy |
|
|
AMZN |
8.079377e+05 |
1.000000e+06 |
0.000000 |
131.789993 |
700.0 |
9.001907e+05 |
0.0 |
92252.9951 |
Buy |
|
|
GOOG |
8.079377e+05 |
1.000000e+06 |
0.000000 |
282.318173 |
0.0 |
8.079377e+05 |
0.0 |
0.0000 |
Sell |
|
|
HPQ |
7.084706e+05 |
1.000000e+06 |
0.000000 |
20.722316 |
4800.0 |
8.079377e+05 |
0.0 |
99467.1168 |
Buy |
|
|
IBM |
6.089637e+05 |
1.000000e+06 |
0.000000 |
110.563240 |
900.0 |
7.084706e+05 |
0.0 |
99506.9160 |
Buy |
|
|
MSFT |
6.089637e+05 |
1.000000e+06 |
0.000000 |
24.677580 |
0.0 |
6.089637e+05 |
0.0 |
0.0000 |
Sell |
|
|
NFLX |
5.090727e+05 |
1.000000e+06 |
0.000000 |
10.090000 |
9900.0 |
6.089637e+05 |
0.0 |
99891.0000 |
Buy |
|
|
NTDOY |
4.126127e+05 |
1.000000e+06 |
0.000000 |
37.099998 |
2600.0 |
5.090727e+05 |
0.0 |
96459.9948 |
Buy |
|
|
SNY |
4.126127e+05 |
1.000000e+06 |
0.000000 |
16.360001 |
0.0 |
4.126127e+05 |
0.0 |
0.0000 |
Sell |
|
|
YHOO |
4.126127e+05 |
1.000000e+06 |
0.000000 |
16.360001 |
0.0 |
4.126127e+05 |
0.0 |
0.0000 |
Sell |
| 2010-03-17 |
SNY |
3.136127e+05 |
1.000000e+06 |
0.000000 |
16.500000 |
6000.0 |
4.126127e+05 |
0.0 |
99000.0000 |
Buy |
|
|
YHOO |
2.146127e+05 |
1.000000e+06 |
0.000000 |
16.500000 |
6000.0 |
3.136127e+05 |
0.0 |
99000.0000 |
Buy |
| 2010-03-22 |
GOOG |
1.310711e+05 |
1.000000e+06 |
0.000000 |
278.472004 |
300.0 |
2.146127e+05 |
0.0 |
83541.6012 |
Buy |
| 2010-03-23 |
MSFT |
3.315733e+04 |
1.000000e+06 |
0.000000 |
25.106096 |
3900.0 |
1.310711e+05 |
0.0 |
97913.7744 |
Buy |
| 2010-05-03 |
GOOG |
1.126680e+05 |
9.959690e+05 |
-13.436593 |
265.035411 |
0.0 |
3.315733e+04 |
-0.0 |
79510.6233 |
Sell |
| 2010-05-10 |
HPQ |
2.059599e+05 |
9.897939e+05 |
-1.286486 |
19.435830 |
0.0 |
1.126680e+05 |
-0.0 |
93291.9840 |
Sell |
| 2010-05-14 |
NTDOY |
2.990399e+05 |
9.864139e+05 |
-1.299999 |
35.799999 |
0.0 |
2.059599e+05 |
-0.0 |
93079.9974 |
Sell |
| 2010-05-17 |
SNY |
3.966599e+05 |
9.850339e+05 |
-0.230000 |
16.270000 |
0.0 |
2.990399e+05 |
-0.0 |
97620.0000 |
Sell |
|
|
YHOO |
4.942799e+05 |
9.836539e+05 |
-0.230000 |
16.270000 |
0.0 |
3.966599e+05 |
-0.0 |
97620.0000 |
Sell |
| 2010-05-19 |
AMZN |
5.814929e+05 |
9.786139e+05 |
-7.199997 |
124.589996 |
0.0 |
4.942799e+05 |
-0.0 |
87212.9972 |
Sell |
|
|
MSFT |
6.744502e+05 |
9.736573e+05 |
-1.270909 |
23.835187 |
0.0 |
5.814929e+05 |
-0.0 |
92957.2293 |
Sell |
| 2010-05-21 |
IBM |
7.719409e+05 |
9.716411e+05 |
-2.240249 |
108.322991 |
0.0 |
6.744502e+05 |
-0.0 |
97490.6919 |
Sell |
| 2010-06-10 |
AAPL |
8.833386e+05 |
9.832296e+05 |
3.408371 |
32.764038 |
0.0 |
7.719409e+05 |
0.0 |
111397.7292 |
Sell |
| 2010-06-11 |
AAPL |
8.833386e+05 |
9.832296e+05 |
3.800738 |
33.156405 |
0.0 |
8.833386e+05 |
0.0 |
0.0000 |
Sell |
| 2010-06-18 |
AAPL |
7.865559e+05 |
9.832296e+05 |
0.000000 |
35.845436 |
2700.0 |
8.833386e+05 |
0.0 |
96782.6772 |
Buy |
| 2010-06-28 |
IBM |
6.974378e+05 |
9.832296e+05 |
0.000000 |
111.397697 |
800.0 |
7.865559e+05 |
0.0 |
89118.1576 |
Buy |
| 2010-07-01 |
IBM |
7.821270e+05 |
9.788006e+05 |
-5.536198 |
105.861499 |
0.0 |
6.974378e+05 |
-0.0 |
84689.1992 |
Sell |
| 2010-07-06 |
IBM |
6.861598e+05 |
9.788006e+05 |
0.000000 |
106.630175 |
900.0 |
7.821270e+05 |
0.0 |
95967.1575 |
Buy |
| 2010-07-09 |
NTDOY |
5.900898e+05 |
9.788006e+05 |
0.000000 |
36.950001 |
2600.0 |
6.861598e+05 |
0.0 |
96070.0026 |
Buy |
| 2010-07-20 |
IBM |
6.884589e+05 |
9.812025e+05 |
2.668781 |
109.298956 |
0.0 |
5.900898e+05 |
0.0 |
98369.0604 |
Sell |
| … |
… |
… |
… |
… |
… |
… |
… |
… |
… |
… |
| 2016-06-23 |
AAPL |
3.951693e+05 |
1.863808e+06 |
0.000000 |
95.582210 |
1900.0 |
5.767755e+05 |
0.0 |
181606.1990 |
Buy |
|
|
TWTR |
2.094333e+05 |
1.863808e+06 |
0.000000 |
17.040001 |
10900.0 |
3.951693e+05 |
0.0 |
185736.0109 |
Buy |
| 2016-06-27 |
AAPL |
3.833670e+05 |
1.856135e+06 |
-4.038123 |
91.544087 |
0.0 |
2.094333e+05 |
-0.0 |
173933.7653 |
Sell |
|
|
FB |
5.795130e+05 |
1.862921e+06 |
3.770004 |
108.970001 |
0.0 |
3.833670e+05 |
0.0 |
196146.0018 |
Sell |
| 2016-06-28 |
SNY |
7.885450e+05 |
1.880959e+06 |
3.110001 |
36.040001 |
0.0 |
5.795130e+05 |
0.0 |
209032.0058 |
Sell |
|
|
YHOO |
9.975770e+05 |
1.898997e+06 |
3.110001 |
36.040001 |
0.0 |
7.885450e+05 |
0.0 |
209032.0058 |
Sell |
| 2016-06-30 |
AAPL |
8.169157e+05 |
1.898997e+06 |
0.000000 |
95.084904 |
1900.0 |
9.975770e+05 |
0.0 |
180661.3176 |
Buy |
|
|
NFLX |
9.907277e+05 |
1.893981e+06 |
-2.640000 |
91.480003 |
0.0 |
8.169157e+05 |
-0.0 |
173812.0057 |
Sell |
| 2016-07-01 |
NFLX |
9.907277e+05 |
1.893981e+06 |
2.549995 |
96.669998 |
0.0 |
9.907277e+05 |
0.0 |
0.0000 |
Sell |
|
|
SNY |
8.045767e+05 |
1.893981e+06 |
0.000000 |
37.990002 |
4900.0 |
9.907277e+05 |
0.0 |
186151.0098 |
Buy |
|
|
YHOO |
6.184257e+05 |
1.893981e+06 |
0.000000 |
37.990002 |
4900.0 |
8.045767e+05 |
0.0 |
186151.0098 |
Buy |
| 2016-07-11 |
AAPL |
8.016949e+05 |
1.896589e+06 |
1.372569 |
96.457473 |
0.0 |
6.184257e+05 |
0.0 |
183269.1987 |
Sell |
|
|
NTDOY |
6.133349e+05 |
1.896589e+06 |
0.000000 |
27.700001 |
6800.0 |
8.016949e+05 |
0.0 |
188360.0068 |
Buy |
| 2016-07-14 |
MSFT |
4.264099e+05 |
1.896589e+06 |
0.000000 |
53.407133 |
3500.0 |
6.133349e+05 |
0.0 |
186924.9655 |
Buy |
| 2016-07-25 |
AAPL |
2.424604e+05 |
1.896589e+06 |
0.000000 |
96.815526 |
1900.0 |
4.264099e+05 |
0.0 |
183949.4994 |
Buy |
|
|
FB |
6.001543e+04 |
1.896589e+06 |
0.000000 |
121.629997 |
1500.0 |
2.424604e+05 |
0.0 |
182444.9955 |
Buy |
| 2016-07-26 |
GOOG |
-8.766857e+04 |
1.896589e+06 |
0.000000 |
738.419983 |
200.0 |
6.001543e+04 |
0.0 |
147683.9966 |
Buy |
| 2016-08-18 |
NFLX |
-2.703726e+05 |
1.896589e+06 |
0.000000 |
96.160004 |
1900.0 |
-8.766857e+04 |
0.0 |
182704.0076 |
Buy |
| 2016-09-01 |
AAPL |
-6.758557e+04 |
1.915427e+06 |
9.914477 |
106.730003 |
0.0 |
-2.703726e+05 |
0.0 |
202787.0057 |
Sell |
| 2016-09-02 |
AMZN |
1.641464e+05 |
1.979327e+06 |
213.000000 |
772.440002 |
0.0 |
-6.758557e+04 |
0.0 |
231732.0006 |
Sell |
|
|
FB |
3.539114e+05 |
1.986647e+06 |
4.880005 |
126.510002 |
0.0 |
1.641464e+05 |
0.0 |
189765.0030 |
Sell |
|
|
GOOG |
5.082034e+05 |
1.993255e+06 |
33.040039 |
771.460022 |
0.0 |
3.539114e+05 |
0.0 |
154292.0044 |
Sell |
|
|
HPQ |
7.081654e+05 |
2.006030e+06 |
0.925746 |
14.490000 |
0.0 |
5.082034e+05 |
0.0 |
199962.0000 |
Sell |
|
|
IBM |
8.996254e+05 |
2.015652e+06 |
8.018727 |
159.550003 |
0.0 |
7.081654e+05 |
0.0 |
191460.0036 |
Sell |
|
|
MSFT |
1.101470e+06 |
2.030572e+06 |
4.262865 |
57.669998 |
0.0 |
8.996254e+05 |
0.0 |
201844.9930 |
Sell |
|
|
NFLX |
1.286492e+06 |
2.032890e+06 |
1.219993 |
97.379997 |
0.0 |
1.101470e+06 |
0.0 |
185021.9943 |
Sell |
|
|
NTDOY |
1.482604e+06 |
2.040642e+06 |
1.139999 |
28.840000 |
0.0 |
1.286492e+06 |
0.0 |
196112.0000 |
Sell |
|
|
SNY |
1.694676e+06 |
2.066563e+06 |
5.289997 |
43.279999 |
0.0 |
1.482604e+06 |
0.0 |
212071.9951 |
Sell |
|
|
TWTR |
1.907771e+06 |
2.093922e+06 |
2.509998 |
19.549999 |
0.0 |
1.694676e+06 |
0.0 |
213094.9891 |
Sell |
|
|
YHOO |
2.119843e+06 |
2.119843e+06 |
5.289997 |
43.279999 |
0.0 |
1.907771e+06 |
0.0 |
212071.9951 |
Sell |
475 rows × 9 columns
bk["Portfolio Value"].groupby(level =0).apply(lambdax: x[-1]).plot()

更為現實的投資組合可以投資任何12支股票而達到100%的收益。這個看上去不錯,但是我們可以做得更好。
基准分析法
基准分析法可以分析交易策略效率的好壞。所謂基准分析,就是將策略和其他(著名)策略進行比較從而評價該策略的表現好壞。
每次你評價交易系統的時候,都要跟買入持有策略(SPY)進行比較。除了一些信托基金和少數投資經理沒有使用它,該策略在大多時候都是無敵的。有效市場假說強調沒有人能戰勝股票市場,所以每個人都應該購入指數基金,因為它能反應整個市場的構成。SPY是一個交易型開放式指數基金(一種可以像股票一樣交易的信托基金),它的價格有效反映了S&P 500中的股票價格。買入並持有SPY,說明你可以有效地匹配市場回報率而不是戰勝它。
下面是SPY的數據,讓我們看看簡單買入持有SPY能得到的回報:
spyder =web.DataReader("SPY", "yahoo", start, end) spyder.iloc[[0,-1],:]
| Open |
High |
Low |
Close |
Volume |
Adj Close |
|
| Date |
|
|
|
|
|
|
| 2010-01-04 |
112.370003 |
113.389999 |
111.510002 |
113.330002 |
118944600 |
99.292299 |
| 2016-09-01 |
217.369995 |
217.729996 |
216.029999 |
217.389999 |
93859000 |
217.389999 |
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
# We see that the buy-and-hold strategy beats the strategy we developed earlier. I would also like to see a plot. ax_bench =(spyder["Adj Close"] /spyder.ix[0, "Adj Close"]).plot(label ="SPY") ax_bench =(bk["Portfolio Value"].groupby(level =0).apply(lambdax: x[-1]) /1000000).plot(ax =ax_bench, label ="Portfolio") ax_bench.legend(ax_bench.get_lines(), [l.get_label() forl inax_bench.get_lines()], loc ='best') ax_bench

買入持有SPY比我們當前的交易系統好——我們的系統還沒有考慮不菲的交易費用。考慮到機會成本和該策略的消耗,我們不應該用它。
怎樣才能改進我們的系統呢?對於初學者來盡量多樣化是一個選擇。目前我們所有的股票都來自技術公司,這意味着技術型公司的不景氣會反映在我們的投資組合上。我們應該設計一個可以利用空頭頭寸和熊市的系統,這樣不管市場如何變動,我們都可以盈利。我們也可以尋求更好的方法預測股票的最高期望價格。但是不論如何我們都需要做得比SPY更好,不然由於我們的系統會自帶機會成本,是沒用的。
其他的基准策略也是存在的。如果我們的系統比“買入持有SPY”更好,我們可以進一步跟其他的系統比較,例如:

(我最初在這里接觸到這些策略)基本准則仍然是:不要使用一個復雜的,包含大量交易的系統如果它贏不了一個簡單的交易不頻繁的指數基金模型。(事實上這個標准挺難實現的)
最后要強調的是,假設你的交易系統在回溯測試中打敗了所有的基准系統,也不意味着它能夠准確地預測未來。因為回溯測試容易過擬合,它不能用於預測未來。
結論
雖然講座最后的結論不是那么樂觀,但記住有效市場理論也有缺陷。我個人的觀點是當交易更多依賴於算法的時候,就更難戰勝市場。有一個說法是:信托基金都不太可能戰勝市場,你的系統能戰勝市場僅僅是一個可能性而已。(當然信托基金表現很差的原因是收費太高,而指數基金不存在這個問題。)
本講座只簡單地說明了一種基於移動平均的交易策略。還有許多別的交易策略這里並沒有提到。而且我們也沒有深入探討空頭股票和貨幣交易。特別是股票期權有很多東西可以講,它也提供了不同的方法來預測股票的走向。你可以在Derivatives Analytics with Python: Data Analysis, Models, Simulation, Calibration and Hedging書中讀到更多的相關內容。(猶他大學的圖書館有這本書)
另一個資源是O’Reilly出的Python for Finance,猶他大學的圖書館里也有。
記住在股票里面虧錢是很正常的,同樣股市也能提供其他方法無法提供的高回報,每一個投資策略都應該是經過深思熟慮的。這個講座旨在拋磚引玉,希望同學們自己進一步探討這個話題。
作業
問題1
建立一個基於移動平均的交易系統(不需要止損條件)。選擇15支2010年1月1日之前上市的股票,利用回溯測試檢驗你的 系統,並且SPY基准作比較,你的系統能戰勝市場嗎?
問題2
在現實中每一筆交易都要支付一筆佣金。弄明白如何計算佣金,然后修改你的backtes()函數,使之能夠計算不同的佣金模式(固定費用,按比例收費等等)。
我們現在的移動平均交匯點分析系統在兩條平均線交叉的時候觸發交易。修改系統令其更准確:


當你完成修改之后,重復問題1,使用一個真實的佣金策略(從交易所查)來模擬你的系統,同時要求移動平均差異達到一定的移動標准差再激發交易。
問題3
我們的交易系統無法處理空頭股票。空頭買賣的復雜性在於損失是沒有下限的(多頭頭寸的最大損失等於購入股票的總價格)。學習如何處理空頭頭寸,然后修改backtest()使其能夠處理空頭交易。思考要如何實現空頭交易,包括允許多少空頭交易?在進行其他交易的時候如何處理空頭交易?提示:空頭交易的量在函數中可以用一個負數來表示。
完成之后重復問題1,也可以同時考慮問題2中提到的因素。
