1. 程式人生 > >Python股市資料分析教程——學會它,或可以實現半“智慧”炒股 (Part 2)

Python股市資料分析教程——學會它,或可以實現半“智慧”炒股 (Part 2)

以下為譯文

本篇文章是"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%,這樣的表現並不差。

請注意,這個交易策略並不會觸發我們的止損指令。難道這意味著我們不需要止損指令嗎?要回答這個問題並不簡單。畢竟,如果我們選擇了另一個不同的股價來判斷是否丟擲股票,止損指令可能真的會被觸發。

止損指令會被自動觸發,且不會詢問指令被觸發的原因。這意味著股價的真實變化與短暫波動都有可能觸發止損指令,而後者我們更為關心,因為你不僅要為訂單支付費用,而且還無法保證以指定的價格丟擲股票,這可能會使你的損失更大。與此同時,你交易股票的走勢仍在繼續,如果止損指令不被觸發,你甚至可以從中獲利。也就是說,止損指令能夠幫助你保持自己的情緒,繼續持有股票,即使它已經失去了自己的價值。如果你無法監控或快速訪問自己的投資專案,例如在度假,它們也能發揮作用。

我曾介紹過一些關於贊成不贊成止損指令的觀點,但從現在起,我不會要求我們的回溯檢驗系統考慮止損指令。雖然不太現實(我確實相信在工業中實際應用的系統能夠考慮止損規則),但這簡化了回溯檢驗任務。

更為真實的投資