TQuant Lab 股價動能因子策略,強者恆強的市場天擇

本文重點概要

  • 文章難度:★★★☆☆
  • 探討股價動能與股票期望報酬率之間的關係。
  • 以 TQuant Lab 回測平台撰寫動能交易策略並回測風險與績效。
  • 改編自 股價動能與股票期望報酬率的關係

前言

過去有關股價動能的研究,研究發現在美國股票市場上,26 周的股價乖離率與股票的未來報酬率呈現正相關;Jegadeesh and Titman 發現美國股票市場存在動能效應-利用買進過去 6-12 個月報酬率最佳的股票,並同時賣出過去 6-12 個月報酬率最差的股票所建立的投資策略,可以獲得經濟與統計上顯著的超額報酬率。後續許多學者研究也發現,動能效應普遍存在國際股票市場與不同類別的資產當中。

而近年來主管機關對於股票市場的資訊揭露的不遺於力,以及對股票市場的漲跌幅逐步放寬,使台股市場的效率性迅速提高。因此,台股市場的股價動能與股票期望報酬率的關係值得進一步去檢驗。為了探討股價動能與股票期望報酬率之間的關係,學界與業界也發展許多衡量股票動能的指標,除了有利用過去累積報酬率所計算的動能的指標、也有應用波動度調整後股票累積報酬率、長短均線的乖離率、CAPM 殘差的累計值…等,本文將運用 TEJ 過去的研究結論:

  • 單變量分析顯示,形成期3個月的 JTMOM3、長短均線乖離率的 MAD 動能變量對股票的期望報酬率的預測能力最具經濟與統計上的顯著性。
  • 多變量分析顯示,控制一年期 beta 值、股價淨值比、市值、12 個月動能的特徵變量後,JTMOM3、MAD的動能變量與股票期望報酬率的關係仍顯著為正,代表其對橫截面股票期望報酬率獨具預測力與解釋能力。
動能因子策略

為此本文將主要運用 JTMOM3 與 MAD 動能變量進行回測分析。

更多詳細內容請至:股價動能與股票期望報酬率的關係

動能因子策略

  • 計算過去3個月的股票平均回報率。
  • 計算長短均線 MAD 作為黃金交叉出場條件。
  • 以四個月為週期,將交易分為紀錄月(前三個月)與交易月(第四個月):
    • 在紀錄月紀錄三個月來的每檔股票動能,等到交易月進行下單
    • 在交易月「最後一天」買入股票池中動能前 5% 的股票,同時做空股票池中動能表現最差的 5% 的股票。(與動能形成期間隔一個月,避免買賣價差與短期反轉效應的影響)
  • 清除紀錄月股票資料,重新開始新週期
  • 在紀錄月的出場條件(統一在紀錄月出場):
    • 短均線 MAD > 長均線 MAD 時出場長部位
    • 短均線 MAD < 長均線 MAD 時出場短部位

編輯環境與模組需求

本文使用 Mac 作業系統以及 Jupyter Notebook 作為編輯器。

資料導入

import tejapi
import os
os.environ['TEJAPI_KEY'] = "your key"
os.environ['TEJAPI_BASE'] = "https://api.tej.com.tw"
start = '2018-02-01'
end = '2023-12-31'
os.environ['ticker'] ='2308 2311 2317 2324 2325 2327 2330 2347 2353 2354 2615 2618 2633 2801 2823 2880 3034 3037 3045 3231 3474 3481 3673 3697 3711 4904 4938 5854 5871 5876 5880 6239 6415 6505 6669 6770 8046 8454 9904 9910 IX0001'
os.environ['mdate'] = start+' '+end
!zipline ingest -b tquant

資料期間從 2018-02-01 至 2023–12–31,隨機挑選 40 檔股票,並加入台股加權指數 IX0001,作為大盤比較。

建立 Pipeline 函式

建立 Custom Factor 函數

Custom Factor 可以讓使用者自行設計所需的客製化因子,於本次案例我們用以處理:

  • 形成期 3 個月之動能因子(Momentum,詳情請見 Custom Factors
from zipline.pipeline.filters import StaticAssets

class Momentum(CustomFactor):
    inputs = [TWEquityPricing.close]
    window_length = 63    # finding past n days' return
    
    def compute(self, today, assets, out, close):
        
        monthly_return_1 = ((close[21-1]-close[0])/close[0])*100
        monthly_return_2 = ((close[42-1]-close[20])/close[0])*100
        monthly_return_3 = ((close[63-1]-close[41])/close[0])*100

        formation_return = (((monthly_return_1)+(monthly_return_2)+(monthly_return_3))/3).round(5)   # 3 months average return

        out[:] = formation_return

from numpy import average, abs as np_abs
from zipline.pipeline.factors.basic import _ExponentialWeightedFactor, exponential_weights

class WeightedMovingAbsDev(_ExponentialWeightedFactor):

    def compute(self, today, assets, out, data, decay_rate):
        weights = exponential_weights(len(data), decay_rate = 1)

        mean = average(data, axis=0, weights=weights)
        abs_deviation = average(np_abs(data - mean), axis=0, weights=weights)

        out[:] = abs_deviation

Pipeline() 提供使用者快速處理多檔標的的量化指標與價量資料的功能,於本次案例我們用以處理:

  • MAD 指標計算,作為出場條件(內建因子:WeightedMovingAbsDev,詳情請見 Pipeline built-in factors
  • 其中短均線 MAD 的 window_length 設置為 21 天,長均線 MAD 的 window_length 設置為 120 天。
  • HighTrue 表示報酬率最佳的 8 檔股票,反之 LowTrue 表示報酬率最差的 8 檔股票。(使用者可只抓取 40 * 5% = 各 2 支股票交易,不過建議擴大股票池,否則交易股數太少。在這邊先以各 8 支股票做計算。)
start_dt, end_dt = pd.Timestamp(start, tz='utc'), pd.Timestamp(end, tz='utc')
bundle = bundles.load('tquant')
benchmark_asset = bundle.asset_finder.lookup_symbol('IX0001',as_of_date = None)
def make_pipeline():
    mom = Momentum()
    mad_short = WeightedMovingAbsDev(inputs = [TWEquityPricing.close], window_length = 21, decay_rate = 1)
    mad_long = WeightedMovingAbsDev(inputs = [TWEquityPricing.close], window_length = 120, decay_rate = 1)
    curr_price = TWEquityPricing.close.latest
    
    return Pipeline(
        columns = {
        'curr_price': curr_price,
        'Momentum': mom,
        'High': mom.top(8),
        'Low': mom.bottom(8),
        'MAD_short': mad_short,
        'MAD_long': mad_long,
    },
        screen = ~StaticAssets([benchmark_asset])
    )
my_pipeline = run_pipeline(make_pipeline(), start_dt, end_dt)
my_pipeline.tail(20)
動能因子策略
2023/12/29的 Pipeline 部分資訊

建立 initialize 函式

inintialize() 函式用於定義交易開始前的每日交易環境,與此例中我們設置:

  • 滑價成本
  • 交易手續費
  • 報酬指數 ( IX0001 ) 作為大盤指數
  • 將 Pipeline 設計的動能因子策略導入交易流程中
from zipline.finance import slippage, commission
from zipline.api import *
from zipline.api import set_slippage, set_commission, set_benchmark, attach_pipeline, order, order_target, symbol, pipeline_output, record

def initialize(context):
    set_slippage(slippage.VolumeShareSlippage())
    set_commission(commission.PerShare(cost = 0.001425 + 0.003 / 2))
    attach_pipeline(make_pipeline(), 'mystrats')
    set_benchmark(symbol('IX0001'))
    context.day_count = 0
    context.high_list = []
    context.low_list = []
    context.history_high = []
    context.history_low = []

建立 handle_data 函式

handle_data() 為構建股價動能因子策略的重要函式,會在回測開始後每天被呼叫,主要任務為設定交易策略、下單與紀錄交易資訊。

關於本策略的交易詳細規則請至:動能因子.ipynb

# record month -> record 3 months' momentum
        if (context.day_count == 1) and (High == True) and (len(context.high_list) < 45):
            context.high_list.append(i)
        if (context.day_count == 1) and (Low == True) and (len(context.low_list) < 45):
            context.low_list.append(i)
        
# trade month (Long, Short) -> first day in trade month, context.high_list and context.low_list have values
        if (context.day_count == 0) and (i in context.high_list) and (cash_position > curr_price * 2000):
            order(i, 2000)
            buy = True
            record(buy = buy)
            context.high_list = [x for x in context.high_list if x != i]
        elif (context.day_count == 0) and (i in context.low_list) and (cash_position >= 0):
            order(i, -2000)
            buy = True
            record(buy = buy)
            context.low_list = [x for x in context.low_list if x != i]
            
# Exiting the position (clearing the position from the previous trading month on the 21st day of the recording month) -> MAD_short > MAD_long or MAD_short > MAD_long        
        if (21 <= context.day_count <= 63) and (MAD_short > MAD_long) and (stock_position > 0) and (i in context.history_high):
            order_target(i, 0)
            sell = True
            record(sell = sell)
            context.history_high = [x for x in context.history_high if x != i]
        elif (21 <= context.day_count <= 63) and (MAD_short < MAD_long) and (i in context.history_low):
            order_target(i, 0)
            sell = True
            record(sell = sell)
            context.history_low = [x for x in context.history_low if x != i]

執行交易策略

使用 run_algorithm() 執行上述設定的動能因子策略,設置交易期間為 start_dt(2018-02-01) 到 end_dt(2023-12-31),使用資料集 tquant,初始資金為一千萬元。其中輸出的 results 就是每日績效與交易的明細表。

from zipline import run_algorithm
results = run_algorithm(
    start = start_dt,
    end = end_dt,
    initialize=initialize,
    bundle='tquant',
    analyze=analyze,
    capital_base=1e7,
    handle_data = handle_data
)
results
動能因子策略
交易明細表

利用 Pyfolio 進行績效評估

import pyfolio as pf
from pyfolio.utils import extract_rets_pos_txn_from_zipline

returns, positions, transactions = pf.utils.extract_rets_pos_txn_from_zipline(results)
benchmark_rets = results.benchmark_return

# Creating a Full Tear Sheet
pf.create_full_tear_sheet(returns, positions = positions, transactions = transactions,
                          benchmark_rets = benchmark_rets,
                          round_trips=False)
動能因子策略
回測表現與大盤比較圖

可以看到股價動能因子策略在這 68 個月中我們獲得了約 16.56% 的年化報酬,累積報酬 140.25%,獲利表現可說相當不俗;不過相對來說因為策略本身之特性,會有入場滯後性的可能,舉例來說,這個月表現較好的股票我們無法立刻跟進,而是要等一段時間才買進,因此錯過入場時機,這可能是導致 20 年 Q1 到 Q2 表現與大盤相悖的原因,不過相反來說,這種滯後性也可能為策略帶來更多超額報酬,例如在某板塊行情回溫時,由於之前的動能好,策略就已經買了,相當於提前佈局,所以推測出現如 21 年和 23 年後半大幅超越大盤的表現。不過關於滯後性的問題,我們可以搭配如 RSI (TQuant Lab RSI 均線策略,找出低檔反向操作)或是波動率相關的指標來彌補,可留待日後再進行更深入的探討。

動能因子策略
月報酬率變化圖
動能因子策略
長短部位曝露圖與持有檔數

長部位總金額較短部位多,但是短部位有更多的交易個股數。

結論

本次策略應用股價動能與股票期望報酬率的關係之結論,並運用動能因子「之前表現好的股票,未來也會表現好,反之亦然」的觀念模擬回測,驗證「強者恆強、弱者恆弱」在台股之可獲利性,歡迎投資朋友參考。之後也會持續介紹使用 TEJ 資料庫來建構各式指標,並回測指標績效,所以歡迎對各種交易回測有興趣的讀者,選購 TQuant Lab 的相關方案,用高品質的資料庫,建構出適合自己的交易策略。

溫馨提醒,本次策略僅供參考,不代表任何商品或投資上的建議

【TQuant Lab回測系統】解決你的量化金融痛點

全方位提供交易回測所需工具

Github 原始碼

點此前往 Github

延伸閱讀

相關連結

返回總覽頁
Procesing