生技股如何安全買?逆勢爆賺策略分享

國光生、中天、亞諾法、合一,這些妖怪股最近的漲跌每天都是10%上下,非常的可怕,要如何才能買在低點、賣在高點?本文教你怎麼設計策略,在 2008 ~2020 年翻 6 倍的穩定策略,獲利不錯且風險小,從零開始研究生技股策略?讓 FinLab 做實驗跟你分享!請原諒我標題殺人,這篇還滿學術且實用的,但往往最有用的東西反而沒人看,只好標題殺人吸引你的目光,看完後保證你值回票價。

生技股(國光生、中天、亞諾法、合一)股價波動大 - 如何寫出穩定的策略呢?
生技股(國光生、中天、亞諾法、合一)股價波動大 – 如何寫出穩定的策略呢?

生技股 ,漲停又跌停,如何判斷?

上禮拜問大家最想看什麼股票的回測,很多人都說「國光生」、「合一」,這些生技股。在我的機器學習策略中,上禮拜也出現非常多檔,像是「亞諾法」、「中天」等等,當時打開看盤軟體,跌停好幾天了,看到真的會怕,一邊買手一邊抖。

但對我來講,科學比賺錢還要重要,假如我主觀的干預,就算獲利了,也不會開心,不相信自己的策略,就等於背棄量化交易的信仰,等於無時無刻都要檢查策略是否出錯,檢驗股票,汲汲營營於股市,會犧牲很多時間!還是要交給量化交易,資金才能有系統的放大!

所以無論如何我都不想干預它選出的結果,咬緊牙關低接,事後發現,報酬相當不錯,果然,別人恐慌我貪婪,是股票中的真理。雖然我也很恐慌,但我相信自己做出來的策略。上述經驗,是上禮拜的持股,這禮拜我並沒有建議買生技股呀!

雖然大家可能沒有機器學習策略,但是也想要買生技股,那

要如何判斷對的時機加碼呢?

photo 1512767254318 423c816efbf3
如何加碼生技股呢?

這篇文章就帶大家來設計一個簡單的策略,雖然設計的簡單,但過程不簡單,我們只要由上次「如何買台積電?」這篇文章的範例改寫而來,加入了一個回測上的技巧,來防止過擬合,找到適當的參數,同時避免優化雜訊的風險

生技股策略研發

策略人人都會寫,特別是用 multicharts ,提供簡單好用的功能,10行以內就可以有績效不錯的策略。只要參數夠多,想要績效多好就有多好,只要透過參數優化就可以了!但是重點就在於,參數優化真的是有效的嗎?還是你只是優化了歷史,沒有優化未來呢?

優化歷史不等於優化未來?

為什麼策略上線後,績效表現不好?為什麼無法優化未來?主要可以分成以下幾個原因:

  • 策略參數維度太大,相較之下,樣本太少,有點像是買樂透,只要買夠多張,一定會中獎,但不代表你會知道下一期的開獎號碼!更學術的講法,就是策略誤將歷史雜訊當成規律來優化(overfitting)。
  • 第二種可能是,策略雖然有抓到股價的型態,但是隨著越來越多人用一樣的策略,導致市場效率上升,獲利空間被壓縮
  • 商品因為基本面和消息面、黑天鵝事件,有重大的變化,導致價格的特性跟以往不一樣。
thumbnail 9 1
overfitting 的結果:只是在優化雜訊

要如何避免優化失敗呢?

針對上述的 1 跟 2 點的問題,我們可以用以下的方式,在策略研發時,避免 overfitting:

  1. 限制策略的維度:研發策略時,不要用太多參數,或是參數盡量重複使用,讓可調整的維度盡量縮小
  2. 限制回測的次數:當你手動修改策略時,也是一種最佳化,所以盡量避免盲目的修改程式碼並回測的過程,有些人把「大於」換成「小於」,就測一次,參數隨便改一改,也測一次,條件拿掉一個,再測一次,濾網增加一個,又測一次。在這樣不斷的測試中,其實是「工人」智慧的最佳化,也會導致 overfitting 喔!
  3. 增加樣本數量:我們可以取不同的商品,將所有商品用同一個策略來優化,這樣針對單獨商品的績效會變差,但是整體來說,由於樣本數量變多,商品之間的correlation比較小,找到的 pattern 更 robust(不知道中文怎麼翻:魯棒性?超詭異的)不容易 overfitting。
  4. 使用 hold-out method:將資料分成樣本內(in-sample IS)跟樣本外(out-of-sample OOS)並且在 IS 做最佳化後,再用 OOS 驗證。
  5. 但 hold-out method 會讓 samples 變少,所以也有其他更複雜的方法,可以參考「策略最佳化是有效的嗎?」這篇文章
  6. 驗證近期績效,當你研發出來的策略報酬是凸狀,這種策略可能在近期就不管用了,假如是股票策略,尤其是選股策略,一定要將兩三年的績效,跟大盤做均一化(normalize、rebase)後的比較,才能確保策略在近年是有效的,也有人會用 rolling n years 的方式,來看近期績效是否下滑。

上述這麼多種方法,我們今天先簡單挑幾個,接下來就可以開始實做策略囉!

生技股策略實做

photo 1575503802870 45de6a6217c8

為了避免 overfitting,我們遵守上面「避免優化失敗」的前三個要點:

  • 首先,我們使用最傳統的均線策略即可,不用作任何修改,上次怎麼買台積電,就怎麼買生技股!由於是均線策略,只有兩個維度,就是短週期、長週期的均線參數而已。
  • 增加了停損 -30%,畢竟生技股大漲大跌還是挺可怕的,使用 – 30%最主要是防止策略由於基本面、消息面的因素,產生巨幅下跌。此參數是靠經驗設定,不太需要重新回測,所以不太會造成研發策略時的過擬合。
  • 使用多商品回測,來確保參數的 robustness ,避免 overfitting。

我們跟上次一樣,使用 Colab 進行回測,Colab 是一個免費線上運行 Python 的平台,請大家打開 Colab,跟我們一起運行程式吧!(只要複製貼上就可以囉!)文末附上完整程式碼,假如你連複製貼上都懶,拉到下方,也可以直接運行喔!

安裝 Packages

這個步驟跟上次一樣,是股票回測最基本的起手勢:

# 要一小段時間安裝 Packages
!pip install yfinance > log.txt
!pip install Backtesting==0.2.0 > log.txt
!pip install talib-binary

接下來,使用 yahoo finance 下載各種股票的數據來回測,這次的標的有:

股票名稱Yahoo 股票代碼
國光生4142.TW
中天4124.TWO
亞諾法4133.TW
合一4743.TW
股票代號與代碼

上述股票的 yahoo 代碼,假如是上市,就是「TW」結尾,假如是上櫃,就是「TWO」,我們將上述股票的代碼列出來(stocks),並且使用 get_historical_data 來取得歷史資料,將歷史資料存在 dfs 裡面:

import yfinance as yf
import pandas as pd

def get_historical_data(ticker):
  d = yf.Ticker(ticker)
  df = d.history(period="max")
  df.columns = df.columns.str.lower()
  df.columns = pd.Series(df.columns).str.capitalize().values
  return df.dropna()

stocks = ['4142.TW', '4743.TWO', '4128.TWO', '4133.TW']
dfs = {s:get_historical_data(s) for s in stocks}

接下來就是本篇的重頭戲,要如何一次考慮所有股票進行優化呢?當然我們可以一檔一檔優化,並將結果(如 sharpe ratio)平均起來,但是這樣的方式,要修改不少程式,還要加入 for 迴圈,會讓程式碼不好維護。

所以我用了一個簡單的方法,把它取名為「price fusion」,顧名思義,就是將所有商品的開高低收整合起來,融合成一個商品,這樣就可以用一樣優化方法,但是最後的結果同時考慮了上述 5 檔股票,是不是方便很多呢?

由於程式碼稍微複雜,所以我就不詳細介紹是怎麼寫的,有興趣的人可以自行研究,假如你時間有限,可以直接呼叫下方的「price_fusion」這個 function,就可以了。

import numpy as np
def price_fusion(ohlcv, symbols):
  start_bar = 0
  close_ref = pd.Series(np.concatenate([ohlcv[s].Close.astype(float).pct_change().values[start_bar:] for s in symbols]))
  close_ref[close_ref.abs() > 0.1] = 0
  ret_close = (close_ref + 1).cumprod()

  close = pd.Series(np.concatenate([ohlcv[s].Close.astype(float).values[start_bar:] for s in symbols]))
  high = pd.Series(np.concatenate([ohlcv[s].High.astype(float).values[start_bar:] for s in symbols]))
  low = pd.Series(np.concatenate([ohlcv[s].Low.astype(float).values[start_bar:] for s in symbols]))
  open_ = pd.Series(np.concatenate([ohlcv[s].Open.astype(float).values[start_bar:] for s in symbols]))
  volume = pd.Series(np.concatenate([ohlcv[s].Volume.astype(float).values[start_bar:] for s in symbols]))

  ret_high = ret_close * high / close
  ret_low = ret_close * low / close
  ret_open = ret_close * open_ / close

  return pd.DataFrame({
      'Open': ret_open.values,
      'High': ret_high.values,
      'Low': ret_low.values,
      'Close': ret_close.values,
      'Volume': volume.values,
  }, index=pd.date_range('2000-1-1', periods=len(ret_close), freq='1h')).dropna().astype(float)

df = price_fusion(dfs, stocks)
print('price dataframe shape', df.shape)
df.head()
Screen Shot 2020 08 05 at 4.01.22 PM 1

我們可以發現,原本六檔股票,已經整合成同一個商品了,總共有11861天,大約是47年的績效!你可以想像一下,假如你可以找到一個策略,長達 47年有效,是不是覺得這個策略的可靠性變高很多,對這個策略更有信心了呢?這就是有效增加sample的方法!

回測生技股

融合完以上的商品,我們可以來回測了,回測的策略跟「研發台積電策略」的文章是一模一樣的,所以我們在這邊只列出程式碼,不額外講解,假如對於下方的程式碼有任何不清楚,都可以前往該篇文章詳讀喔!

from backtesting import Backtest, Strategy

class Strategy(Strategy):
    
    n1 = 40
    n2 = 30
    
    def init(self):
        super().init()
        
        # Precompute the two moving averages
        close = pd.Series(self.data.Close)
        sma1 = talib.SMA(close, timeperiod=self.n1)
        sma2 = talib.SMA(close, timeperiod=self.n2)

        # Precompute signal
        signal_long = (sma1 > sma2) & (sma1.shift() < sma2.shift())
        signal_short = ((sma1 < sma2) & (sma1.shift() > sma2.shift()))

        # combine signal
        signal = signal_long
        signal[signal_short] = -1
        
        # plot sma
        self.I(lambda x: sma1, 'sma1')
        self.I(lambda x: sma2, 'sma2')

        # set signal to trade
        self.signal = self.I(lambda x: signal, 'signal')

    def next(self):
        super().next()

        entry_size = self.signal[-1]

        if entry_size > 0:
            self.buy()
        elif entry_size < 0:
          for trade in self.trades:
            trade.close()

        for trade in self.trades:
          if self.data.Close[-1] < trade.entry_price * 0.7:
            trade.close()

bt = Backtest(df, Strategy)
result1 = bt.run()
bt.plot()

下圖就是回測跑完的結果,我偷偷先使用了最佳化參數,但你這個階段理論上,不應該得到如此好的績效,這是優化後的結果,為了省去大家設參數的辛勞!

Screen Shot 2020 08 05 at 10.15.15 PM

策略優化

可以回測後,我們就可以做策略的最佳化,這邊要注意的地方是,為了避免 overfitting,在參數枚舉時(列舉所有參數時),不要間格太密,例如「5」,我覺得就是很剛好的數字,不用到「1」,不然容易 overfitting。

result2 = bt.optimize(n1=range(10, 60, 5),
                      n2=range(10, 60, 5))
print(result2._strategy)

# plot results
result2._equity_curve.Equity.plot(use_index=False, logy=True)

下圖是最後策略的結果,我們可以看到47年總共獲利30000%!由於數字太大了,所以我們用 log 將數值變得比較有可看性,以一個粗糙的策略來說,我覺得很棒了,事實是,有時候均線策略,只要用對的方法來製作策略,就能夠有不錯的績效!你可以說我研發策略返璞歸真,也可以說我懶,但不論如何,有效最重要!

Screen Shot 2020 08 05 at 8.57.37 PM
fusion price 回測結果

上圖中有一行「Strategy(n1=49, n2=30)」可以看到優化後,程式設定最佳參數為(n1=40, n2=30)這兩條均線,代表30日均線跌到40日下方時,我們可以購買這些生技股,停損一律是30%,直到股價回升,均線黃金交叉為止。歷史績效會有不錯的表現。雖然使用融合商品,得到不錯的績效,但在個股單一表現實用上,究竟能不能重現這樣驚人的績效呢?

回到單檔股票回測

上述是我們用融合的商品,回測47年的狀況,但是我們實際交易時,應該是會有很多商品,而商品的價格是同步進行的,怎麼修正呢?最終還是得一檔一檔測測看,使用 for 迴圈測,不過這時候因為策略都研發完了,所以只是做最後的檢驗動作:

eqs = {}
for s in stocks:
  bt = Backtest(dfs[s], Strategy)
  r = bt.run()
  eqs[s] = r._equity_curve.Equity
eqs = pd.DataFrame(eqs)
eqs.plot()

下圖是跑出來的結果,可以看到這個策略在單一商品上,也表現的還算OK,但當然偶爾虧損還是滿大的,例如 4128(綠色)這檔股票 2018 年的跌幅,目測應該三個月虧損了快30%,這樣的策略是不能使用的!太危險!

Screen Shot 2020 08 05 at 9.03.53 PM
生技股回測結果

多商品合併以降低風險

最後,為了將風險降低,我們來寫一個資產分配的回測,模擬將資產平均分配到這 4 個商品上,每個月重新再平衡,會得到如何的報酬率呢?

(eqs.resample('M')
    .last()
    .pct_change()+1
).mean(axis=1).cumprod().plot()
Screen Shot 2020 08 05 at 9.10.56 PM
多商品合併

從2009年到2019年,總共將資產翻了6倍,drawdown 看起來不大,我不會覺得太差,送給大家囉!由於此策略是擇時策略,所以會有很多閒置資金,所以績效應該比 6 倍好很多。另外,由於我們對多商品都用同一個策略,所以過擬合的機率算低,而此策略又是大家最舒服的逆勢加碼,相信除了學術練習外,實用上也會有一定的價值,不過任何策略都有失效和過擬合的可能,我們只能將其機率降低,無法保證 100%有效。假如大家有興趣看那個產業的策略,請在下方留言!我會不定時參考一下,決定之後要出的文章喔!

最後我們可以任意觀看其中一檔標的,是否值得買入,例如法諾亞(4133.TW):

s = '4133.TW'
bt = Backtest(dfs[s], Strategy)
r = bt.run()
bt.plot()

法諾亞最近五筆交易,使用此策略,勝率為80%!雖然大波段沒抱到,但是也賺了4~50%,很不錯了啦(畢竟是逆勢策略)!不過當前的股價,由於均線沒有死亡交叉,所以對此策略來說,價格偏高,不適合買入,大家可以隨時跑程式,來觀察是否有生技股有均線死亡交叉,都會是很好的買入時機喔(根據歷史統計來說)!這就是簡單但又不簡單的均線最佳化策略!

Screen Shot 2020 08 06 at 9.12.55 AM

接下來一系列,我會介紹加密貨幣。其實早在 2020 年 04 月 20 號,我就低調開始撰寫一系列 BTC 的文章,當時價格為 6838 BTC/USDT,現在已經破 12000了!要是你那時候有追蹤 finlab,就算放著不動,可能已經獲利 70%。

我自己也有不少資產放在 BTC,並且用 Python 自動交易,因為 BTC 是新商品,算是非效率市場(個人回測時的感覺),尤其是其他的加密貨幣(alternative coin 簡稱 altcoin),用量化交易操作,就像就是用坦克跟原始人打仗一樣!現在開始,使用量化投資做低風險的BTC交易,為時不晚,下一篇,我們從加密貨幣的基本面開始講起。可以追蹤 finlab 粉絲團,收到最新的文章喔!

FinLab - 韓承佑

嗨大家好,我是韓承佑,FinLab創辦人,畢業於巴黎薩克雷大學資工博士,目前擔任臺灣量化交易協會 學術顧問、台北商業大學 創新育成中心 創業技術顧問與上市科技公司 量化交易顧問。當初,我喜歡寫程式、無意間因為軟體比賽接觸Fintech,從此開始了財經跟程式的學習之路。我們成立 FinLab 量化投資部落格,用自己研發的軟體,對台灣股市做大量快速的實驗。希望可以在量化投資的路上,當大家的「武器製造商」!