選股回測系統豆知識 (2)|持股比例上限設定

  • Post author:
  • Reading time:4 mins read

策略回測最怕測到快樂表,什麼是快樂表?簡而言之就是紙上談兵的表現嚇嚇叫,但實際操作時卻很難貼合回測,通常這類情況多是流動性風險問題,而另一種快樂表就是回測數據的解讀錯誤,像是看到超高年化報酬率就很嗨,卻沒注意到在勝率普通的情況下,高報酬是不是剛好靠幾檔交易重壓而生成?若真是如此,那運氣成分可能佔比很大。
本文會利用一些簡單的技巧去避免統計的陷阱,讓你注意到策略中因持股重壓導致的失真問題。

持股檔數設定

小資族的資金少,選股策略在初步開發完後第一個問題是選到檔數太多,以之前介紹過的創新高延續動能策略為例,可能某幾期選股在大牛市高達百檔,但實務上根本不可能買那麼多。

from finlab.backtest import sim
from finlab import data

with data.universe(market='TSE_OTC'):
    close = data.get("price:收盤價")
    # 近5日內有3日以上的股價創前200日新高
    position = (close == close.rolling(200).max()).sustain(5,3)
    report = sim(position, resample="2W", stop_loss=0.2, name="創新高延續動能策略", upload=False)
    report.display()
newplot 40
創新高延續動能策略原型

此時我們可以針對小資族去客製化務實的條件,每期選股價前10低的標的來當持股組合。回測結果發現報酬率降低一些,但持股檔數上限控制在10檔以內,且低價的優先,讓小資族更能參與整張現股的操作,避免零股交易較高的滑價風險,讓回測更貼近現實交易貼合的可能性。

with data.universe(market='TSE_OTC'):
    close = data.get("price:收盤價")
    # 近5日內有3日以上的股價創前200日新高
    position = (close == close.rolling(200).max()).sustain(5,3)
    position *= close
    position = position[position > 0].is_smallest(10)
    report = sim(position, resample="2W", name="創新高延續動能策略", upload=False)
    report.display()
newplot 42
低價股優先測定持股檔數上限

單檔持股比例上限

以為這樣就沒問題了嗎?你若仔細看上面「低價股優先測定持股檔數上限」的策略,會發現每期的標的數量仍參差不定,牛市有時候10檔,熊市有時候1檔,由於 position 預設分配持股比例是用檔數做平均,因此若當期只選到一隻,而沒在 sim 回測函數 內特別設定 position_limit,那就是100%重壓一檔!如下圖示對整體策略波動會造成很大的風險:

newplot 41

如何解決此問題呢?sim 回測函數 內設定 position_limit 當單檔標的持股比例上限,控制倉位風險。預設為None,也就是不設上限。範例:0.1,代表單檔標的最多持有 10 % 部位,若加上持股檔數上限10檔,當只選到8檔時,總持股上限則為80%,剩下 20% 為現金,在選到檔數較少的時候,自然產生空手避險的效果,而不重壓標地。
position_limit 越高一般來說波動越大 ; position_limit 越低則波動低,報酬率與最大回撤普遍都會縮小,站在風控角度,一般建議都要設 position_limit < 0.2,避免個股非系統性風險影響整體策略。
創新高延續動能策略」的 position_limit 要用多少比較好?我們可以用 ReportCollection 來檢測不同數值的回測效果,程式如下:

def run_strategy(position_limit):
    with data.universe(market='TSE_OTC'):
        close = data.get("price:收盤價")
        # 近5日內有3日以上的股價創前200日新高
        position = (close == close.rolling(200).max()).sustain(5,3)
        report = sim(position, resample="2W", position_limit=position_limit, stop_loss=0.2, name="創新高延續動能策略", upload=False)
        return report


# # 產生回測組合
reports = {ind/10: run_strategy(ind/10) for ind in range(1,11)}
# 放入回測報告組合比較器
collection = ReportCollection(reports)
# 產生回測數據分群柱狀圖
collection.plot_creturns().show()

import plotly.graph_objects as go

def show_index_bar(collection, item = 'max_drawdown'):
    stats = collection.get_stats()
    df = round(stats.loc[item],2).sort_values()
    fig = go.Figure([go.Bar(x=df.index, y=df.values, text=df.values, textposition='auto',)])
    fig.update_layout(title_text=item)
    return fig

# 年化報酬
show_index_bar(collection, item = 'daily_mean').show()
show_index_bar(collection, item = 'max_drawdown').show()
show_index_bar(collection, item = 'daily_sharpe').show()

回測分析

可以發現 position_limit = 0.2 時 的年化報酬率、夏普率、MDD (最大回撤) 表現最好,能有效降低重壓波動風險。如果做出來發現 position_limit = 1 的數據最好,那就要留意快樂表的可能性,可能好績效靠是重壓導致的,這時最好考慮低 position_limit 的情況

newplot 43
newplot 44
newplot 46
newplot 45 1
newplot 48
position_limit = 0.2 的回測情況

結論

colab 範例檔
你的策略有驗證過持股檔數與單檔持股上限的問題嗎?
調整一個 position_limit 參數 就能降低快樂表和大波動的風險,可說是新手必學的參數設定啊!

Ben

Python 軟體工程師與量化策略研究員。 鑽研資料工程、網頁後端、資料視覺化、量化交易策略開發。 投資主力在台股市場,量化策略為主、質化分析為輔,追求人機攜做最佳化。逐步將觸角延伸到總經、美股、加密貨幣,朝更全方位的交易人邁進。