Python股市數據分析教程——可以實現半“智能”炒股 (Part 2)

https://ntguardian.wordpress.com/2016/09/26/introduction-stock-market-data-python-2/?spm=a2c4e.10696291.0.0.6d7319a4KndFTq

本篇文章是"Python股市數據分析"兩部曲中的第二部分,內容基於我在猶他州立大學MATH 3900 (Data Mining)課程上的一次講座,第一部分在這裡。在這些文章中,我將介紹一些關於金融數據分析的基礎知識,例如,使用pandas獲取雅虎財經上的數據,股票數據可視化,移動均線,開發一種均線交叉策略,回溯檢驗以及基準測試。而本篇文章中,我討論的話題包括均線交叉策略的設計、回溯檢驗、基準測試以及實踐中可能出現的若干問題,以供讀者思考。

注意:本篇文章所涉及的看法、意見等一般性信息僅為作者個人觀點。本文的任何內容都不應被視為金融投資方面的建議。此外,在此給出的所有代碼均無法提供任何保證。選擇使用這些代碼的個人需自行承擔風險。

交易策略

我們把在未來條件滿足時將被終止的交易稱為未平倉交易。多頭倉位是指在交易過程中通過金融商品增值來獲取利潤,而空頭倉位是指在交易過程中通過金融資產價值下跌來獲取利潤。在直接交易股票時,所有的多頭倉位看漲,所有的空頭倉位看跌。這也就是說,持看漲態度並不需要伴隨著一個多頭倉位,而持看跌態度同樣也不需要伴隨著一個空頭倉位(在交易股票期權時,更是如此)。

這裡有一個例子。打算你買入了一隻股票,計劃在股價上漲時以更高的價格將股票拋出。這就是多頭倉位:你持有一種金融資產,如果資產價值增長,你將從中獲利。你的潛在利潤是無限的,而你的潛在損失受到股價的限制,因為股價永遠不會低於0。另一方面,如果你預計一隻股票的價格會下跌,你可以向經紀公司籌借股票並出售,以期在後續以較低的價格回購股票,從而獲取利潤。這種做法稱為做空股票,屬於空頭倉位,即通過股價下跌賺取收益。做空股票的潛在利潤受到股價的限制(最好的做法是,使股票變得一文不值,這樣你可以免費回購這些股票),而損失卻是無限的,因為你可能需要花費任意多的錢來買回籌借的股票。因此,在允許投資者做空股票前,經紀人需要確保投資者保持良好的財務狀況。

任何交易員都必須有一套規則,決定她願意在任何一筆交易上投入多少錢。例如,一名交易員可能認為在任何情況下,她在一筆交易中承受的風險都不能超過所有投資的10%。另外,在任何交易中,交易員必須制定一個由一組條件構成的退出策略,決定她何時退出倉位,從而獲利或止損。交易員可以設置一個目標,即促使她清空倉位的最少利潤。同樣地,交易員也要明確自身能夠承受的最大損失;如果潛在損失超過了這個金額,交易員將退出倉位,以避免任何進一步的損失(通常通過設置止損指令來實現,觸發該指令以避免進一步損失)。

如果一個方案包括促成交易的交易信號、一套能在任何特定策略情況下明確承受多少投資風險的規則、以及一個適用於任何交易的退出策略,那麼我們稱這個方案為一個完整的交易策略。目前,我們關注的是如何設計和評價交易策略。

我們假設任何一筆交易的金額都是投資總資產的一個固定比例;10%看起來是一個不錯的數字。我們決定,對於任何一筆交易,如果損失超過交易金額的20%,我們將結束交易。現在,我們需要一種方法來判斷何時進入倉位以及何時退出倉位,進而獲取利潤。

在這裡,我將介紹一種均線交叉策略。我們將使用兩條移動均線:一條表示長期均線,另一條表示短期均線。採用的策略如下:

  • 當短期均線越過長期均線時,交易金融資產。
  • 當短期均線再一次越過長期均線時,結束交易。

當短期均線高於長期均線時,我們應進行多頭交易,當短期均線再次越過(低於)長期均線時,結束此類交易。當短期均線低於長期均線時,我們應進行空頭交易,當短期均線再次越過(高於)長期均線時,結束此類交易。

現在,我們有了一個完整的策略。但在我們決定使用它之前,我們首先應該儘可能地評估這個策略的效果。回溯檢驗是一種常用的方法,該方法基於歷史數據對交易策略所能帶來的利潤多少進行評估。例如,看看上方圖表中Apple股票的表現,如果20天均線表示短期均線,50天均線表示長期均線,這個交易策略似乎並不能產生多少利潤,至少不如你一直持有多頭倉位更有利可圖。

讓我們看看我們是否可以自動進行回溯檢驗任務。我們首先確定20天均線什麼時候低於50天均線,以及相反的情況。

<code>apple['20d-50d'] = apple['20d'] - apple['50d']
apple.tail()/<code>


Python股市數據分析教程——可以實現半“智能”炒股 (Part 2)

我們把這種差異的標誌稱為行情。也就是說,如果短期均線高於長期均線,那麼這是一個牛市行情(牛市規則),如果短期均線低於長期均線,則目前為熊市行情(熊市規則)。我使用以下代碼判斷當前的股市行情。

<code>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)/<code>


Python股市數據分析教程——可以實現半“智能”炒股 (Part 2)

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


Python股市數據分析教程——可以實現半“智能”炒股 (Part 2)

<code>apple["Regime"].value_counts()/<code>
<code>1       966
-1 663
0 50
Name: Regime, dtype: int64/<code>

上面的最後一行表明,Apple股票在股市中的行情,有1005天為熊市,有600天為牛市,而有54天股市行情較為平穩。

行情變化時會出現交易信號。當牛市開始時,買入信號會被觸發,而當牛市結束時,拋出信號會被觸發。同樣地,當熊市開始時,拋出信號會被觸發,而當熊市結束時,買入信號會被觸發(只有當你要做空股票,或使用一些股票期權等衍生品做空市場時,才會對這些感興趣)。

我們很容易就可以獲取交易信號。令rt表示t時刻的股市行情,st表示t時刻的交易信號,則有:

st = sing(rt - rt-1)

st ∈ {-1, 0, 1},其中-1表示"拋出",1表示"買入",0表示不採取任何措施,我們可以這樣獲取信號:

<code>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()/<code>


Python股市數據分析教程——可以實現半“智能”炒股 (Part 2)

<code>apple["Signal"].plot(ylim = (-2, 2))/<code>


Python股市數據分析教程——可以實現半“智能”炒股 (Part 2)

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

我們會買入Apple股票23次,並拋出Apple股票23次。如果我們僅持有多頭倉位,在6年期間只會進行23筆交易,然而,如果我們在每次多頭倉位終止後,由多頭倉位轉為空頭倉位,我們一共會進行23筆交易。(請記住,更加頻繁的交易並不一定就是好的,交易從來不是免費的。)

你可能會注意到,目前的系統並不是很健全,即使是一個短期均線超過長期均線的短暫瞬間,交易也會被觸發,並導致交易立即結束(這樣並不好,不僅僅是因為每一筆實際交易都伴隨著一筆費用,已獲得的收益會因此被迅速稀釋)。此外,每個牛市行情都會立即轉換到熊市行情,如果你在構建一個允許看漲押注和看跌押注的交易系統,這會導致在一筆交易結束時,立即觸發另一筆在股市中反向押注的交易,這看起來又有些挑剔了。一個更好的系統應該根據更多的證據來判斷股市正朝著發展的特定方向,但我們現在不會關心這些細節。

現在,讓我們嘗試著確定每次買入和拋出股票時的價格。

<code>apple.loc[apple["Signal"] == 1, "Close"]/<code>
<code>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/<code>
<code>apple.loc[apple["Signal"] == -1, "Close"]/<code>
<code>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/<code>
<code>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/<code>


Python股市數據分析教程——可以實現半“智能”炒股 (Part 2)

<code>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/<code>


Python股市數據分析教程——可以實現半“智能”炒股 (Part 2)

從上面我們可以看到,在2013年5月17日,Apple股票的價格大幅下跌,我們的交易系統似乎不能很好地處理這種狀況。但是,這次股價下跌並不是因為Apple公司受到了巨大的衝擊,而是由於股票拆分。儘管派付股息不如股票拆分那樣明顯,但是這些因素仍可能影響到我們交易系統的效果。

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


Python股市數據分析教程——可以實現半“智能”炒股 (Part 2)

我們不希望我們的交易系統因為股票拆分和派付股息而表現得很糟糕。我們應該如何處理這種情況?一種方法是獲取股票拆分和派付股息的歷史數據,並設計一個處理這類數據的交易系統。這或許是最好的解決方案,能夠最為真實地反映股票的行為,但是它過於複雜。另一種解決方案是根據股票拆分和派付股息的情況調整股票價格。

雅虎財經只提供調整後的股票收盤價,但是對於我們來說,要得到調整後的開盤價、最高價、最低價,這樣就足夠了。已調整收盤價計算方式如下:

pricetadj = mt x pricet

其中,mt是用來調整股價的係數。只需進行一次除法就可以求出mt的值,因此,我們可以使用收盤價和已調整收盤價來調整股票的其他所有價格。

讓我們回到前面,調整Apple的股價,並用這些調整後的數據重新評估我們的交易系統。

<code>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"])/<code>


Python股市數據分析教程——可以實現半“智能”炒股 (Part 2)

<code>apple_adj_long_profits/<code>


Python股市數據分析教程——可以實現半“智能”炒股 (Part 2)

你可以看到,根據股票拆分和派付股息情況調整後的股票價格有明顯的不同。從現在開始,我們將使用這些數據。

現在,讓我們創建一個價值100萬美元的虛擬投資項目,根據我們建立的規則,看看它會如何表現。規則包括:

  • 在任何交易中,僅投資所有投資總額的10%。
  • 如果損失超過交易金額的20%,則退出倉位。

在模擬的過程中,牢記以下幾點:

  • 股票交易以100股為單位。
  • 我們的止損規則包含在股價下跌至一定程度時將股票拋出的指令。因此,我們需要檢查這一期間的低價是否已經足夠得低,以至於觸發止損指令。實際上,除非我們買入了看跌期權,否則我們無法保證以設置的止損價格拋出股票,但為簡單起見,我們將這個價格作為拋出價。
  • 每一筆交易都需要向經紀人支付一筆佣金,這部分費用應該計算在內。但在這裡我們不這樣做。

回溯檢驗按如下方式進行:

<code>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/<code>


Python股市數據分析教程——可以實現半“智能”炒股 (Part 2)

<code>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/<code>


Python股市數據分析教程——可以實現半“智能”炒股 (Part 2)

<code>apple_backtest["End Port. Value"].plot()/<code>


Python股市數據分析教程——可以實現半“智能”炒股 (Part 2)

我們的投資項目總值在六年間增長了10%。考慮到任何一筆交易僅涉及所有投資總額的10%,這樣的表現並不差。

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

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

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

更為真實的投資項目不會將投資總額的10%押注在一隻股票上。更現實的做法是考慮在多隻股票上分散投資。涉及多家公司的多筆交易可能會在任何時刻進行,並且大多數投資項目會選擇股票交易,而不是現金。既然我們將在多隻股票上投資,只有當移動均線交叉(不是因為止損)時才退出倉位,那麼我們需要改變進行回溯檢驗的方式。例如,我們將使用pandas中的DataFrame來記錄所有考察股票的買入、拋出訂單,前面的循環代碼也需要記錄更多的信息。

我實現了為多隻股票創建訂單數據的代碼,以及一個執行回溯檢驗的函數。

<code>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)/<code>
<code>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/<code>
<code>bk = backtest(signals, 1000000)
bk/<code>
<code>bk["Portfolio Value"].groupby(level = 0).apply(lambda x: x[-1]).plot()/<code>


Python股市數據分析教程——可以實現半“智能”炒股 (Part 2)

這個虛擬的投資項目投資了十二隻(科技)股票,最終的資產增長達到了100%左右。這很好嗎?雖然表面上看起來不錯,但我們會看到我們可以做得更好。

基準測試

回溯檢驗只是評估交易策略有效性過程的一部分。我們會對策略進行基準測試,或者與其他的可行(通常是眾所周知的)策略進行比較,以確定我們所能達到的效果。

當你評估一個交易系統時,有一個策略你一定要與它比較,除了少數互惠基金與投資管理人,這個策略的效果是最好的:買入並持有SPY指數基金。有效市場假說聲稱,任何人都無法擊敗市場。因此,投資者應該一直購買反映市場結構的指數基金。SPY指數基金是一種交易所買賣基金(一種在市場上交易的類似股票的互惠基金),其價值實際上地代表著標準普爾500指數中股票的價值。通過買入並持有SPY指數基金,我們實際上可以嘗試將回報與市場匹配,而不是試著去擊敗市場。

我通過以下方式獲取關於SPY的數據,並根據收益簡單地買入和持有SPY指數基金。

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


Python股市數據分析教程——可以實現半“智能”炒股 (Part 2)

<code>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/<code>
<code>2180977.0/<code>
<code>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/<code>


Python股市數據分析教程——可以實現半“智能”炒股 (Part 2)

買入和持有SPY指數基金的效果優於我們的交易系統,至少優於我們現在初期的系統,而且,我們甚至沒有說明,考慮到費用我們這個更加複雜的策略有多麼的昂貴。考慮到機會成本和與主動投資策略相關的費用,我們不應該採用這樣的策略。

我們怎樣才能提高我們系統的效果呢?對於初學者而言,我們可以嘗試投資多樣化。我們之前考慮的所有股票都屬於科技公司,這意味著如果科技行業表現不佳,我們的投資項目也會反映出這種低迷的狀況。我們可以開發一個能夠做空股票或看跌押注的系統,這樣,我們就可以利用市場上各個行業領域的走向。我們還可以尋找一種能夠預測股價變化的方法。但是,無論我們做什麼,都必須擊敗這個基準;否則,我們的交易系統中始終會存在著機會成本。

也存在著其他的基準測試策略,如果我們的交易系統擊敗了"買入和持有SPY基金"這個策略,那麼我們可以與這些策略進行比較。這類的交易策略包括:

  • 當每月收盤價高於十月均線時,買入SPY基金。
  • 當十月均線的動量為正時,買入SPY基金。(動量是移動平均過程中的第一個差值,即MOtq = MAtq - MAt-1q。)

(我最早在這裡知道了這些策略。)普遍的經驗仍然成立:對於一個包含大量活躍交易的複雜交易系統,如果一個涉及指數基金且不進行頻繁交易的簡單策略擊敗了它,那麼不要使用這個複雜系統。實際上,這個要求很難滿足。

最後一點,假設你的交易系統確實在回溯檢驗中擊敗了所有的基準策略。回溯檢驗就能夠預測系統在未來的表現了嗎?不太可能。回溯檢驗存在著過擬合的可能,所以,僅僅是回溯檢驗預測的上漲並不意味著在未來會保持上漲的勢頭。

結論

雖然這篇教程以一個令人沮喪的觀點收尾,但是請記住,有效市場假說有很多的反對者。我自己的看法是,隨著交易變得越來越算法化,擊敗市場也將變得更加困難。也就是說,擊敗市場仍然是有可能的,儘管互惠基金似乎還做不到(但是請記住,互惠基金表現得如此糟糕的部分原因,是因為交易所伴隨的費用)。

這篇教程非常簡短,只涉及一種策略類型:基於移動平均線的交易策略。在實踐中也會應用許多其他的交易信號。此外,我們並沒有深入討論有關做空股票、貨幣交易或者股票期權的細節。特別地,股票期權的形式非常豐富,能夠提供許多不同的方式來押注股票的走勢。你可以在《Python衍生分析:數據分析,模型,仿真,校準與對沖》一書中瞭解更多關於衍生品(包括股票期權和其他衍生品)的信息,(對於猶他州立大學的學生)這本書可以在猶他州立大學圖書館中找到。

另一個資源(也是我準備這篇教程時參考的文獻)是O'Reilly出版的圖書《Python金融分析》,也可在猶他州立大學圖書館中找到。

請記住,我們可能(甚至很常見)在股市中虧損。同樣,我們也很難在其他領域獲得像股市那樣的回報,任何投資策略都應該認真對待投資。這篇教程旨在向大家介紹評估股票交易與投資的入門知識,我希望大家能夠繼續研究這些觀點。

問題

問題1

基於均線交叉,設計一個本篇教程中描述的交易策略(你不需要考慮止損指令)。選擇至少15只自2010年1月1日就存在的股票。根據所挑選出的股票回溯檢驗你的策略,並模擬一個投資項目,根據SPY指數基金的效果對策略進行基準測試。你能夠擊敗市場嗎?

問題2

實際上,每一筆交易都需要繳納佣金。瞭解佣金的收取機制,並修改本文中的backtest()函數,使其能夠模擬多種佣金制度(固定費用,投資金額的百分比等)。

此外,我們的均線交叉策略在兩條均線交叉時會導致交易信號被觸發。我們希望通過以下兩種方式之一,確保信號觸發機制更加健壯:

  1. 當移動均線相差固定金額時,觸發交易
  2. 當移動均線相差一定數值的(滾動)標準差時,觸發交易,標準差根據如下公式定義:


Python股市數據分析教程——可以實現半“智能”炒股 (Part 2)

(pandas確實提供了計算滾動標準差的方法。)對於後者,如果移動均線相差p x SDtn,交易信號將被釋放。

為了實現這些約束,可以修改ma_crossover_orders()函數。具體來說,你應該能夠設置滾動標準差中時間窗口對應的天數(並不需要與短期均線和長期均線的時間窗口保持一致),以及移動均線的標準差至少相差多少才能釋放信號。(目前這些函數的實現也應保留下來,事實上,它們應作為函數的默認行為。)

一旦進行了這些修改,那麼回到問題1,此外,還包括一個在模擬投資項目效果時切實可行的佣金方案(可參考某個經紀公司的做法),以及移動均線間相差的能夠觸發交易信號的固定金額或標準差數值。

問題3

我們並沒有建立一個能夠做空股票的交易系統。做空交易的手段很巧妙,因為做空所帶來的損失是無限大的(另一方面,多頭倉位限制了損失最多為購入資產的總值)。在這裡可以瞭解更多關於做空交易的信息。接下來,可以修改backtest()使其支持做空交易。這個函數會怎麼決定如何進行做空銷售,包括做空多少股份以及在進行其他交易時如何評估做空的股票?我們將這個問題留給你來決定。提示一點,做空股票的份額在函數內部可通過負數表示。

一旦完成了這些,回到問題1,或許還需要使用到問題2實現的功能。


分享到:


相關文章: