Lab 4 實作:動量策略 Momentum Trading 的 Python 實戰|Quant Theory 第 21 講筆記

第 21 課・共 34 課

⚠️ 本站內容僅作教育用途,不構成投資建議。投資涉及風險,作出任何投資決定前請諮詢持牌專業人士。
目錄

這是 Liu Peng 大學級量化交易課程第四個實作 lab 的筆記,主題是以 Python 完整實作動量策略 Momentum Trading。策略的理論基礎——橫截面動量、多空組合的構建邏輯——第 12 講已有講解(詳見動量策略|第 12 課);本 lab 在 Colab 環境把理論落地:由抓取道指 30 隻成分股開始,經過月回報換算、六個月滾動累積回報、五分位分組選股,到最後計算策略回報並與買入持有基準對照,走完整個量化策略的最小流程。

學習重點

詳細筆記

策略回顧與環境準備

講者開場先扼要重溫動量策略的定義:對一組資產,買入過去表現最強的一群(做多),同時沽空過去表現最弱的一群(做空)。背後假設是強者延續強勢、弱者延續弱勢——表現的「慣性」會在未來一段時間內持續。這正是第 12 講介紹的橫截面動量 Cross-Sectional Momentum:比較的是資產之間的相對強弱,而非單一資產自身的走勢。

實作環境是 Google Colab,講者先安裝並匯入所需套件:numpy 與 pandas 負責數值與表格運算,yfinance 負責下載股價,另有 requests 與 BeautifulSoup 負責網頁抓取。整個 lab 不使用任何付費數據源,全部數據來自公開網頁與 Yahoo Finance。

第一步:從 Wikipedia 爬取道指成分股名單

策略需要一個明確的資產池。講者選用道瓊斯工業平均指數的 30 隻成分股,但名單並非手動輸入,而是寫一個函數即場從 Wikipedia 的道指條目頁面爬取:

# 摘錄自原片教學
url = "https://en.wikipedia.org/wiki/Dow_Jones_Industrial_Average"
response = requests.get(url, headers=headers)
soup = BeautifulSoup(response.content, "html.parser")
tables = soup.find_all("table")
df = pd.read_html(str(tables))[1]

流程分三層:先用 requests 向該網址發出 HTTP 請求取回整頁內容(附帶 User-Agent 等標頭模擬瀏覽器);再用 BeautifulSoup 解析 HTML,以 find_all("table") 定位頁面中的表格元素——該頁內描述成分股的表格只有一個,容易鎖定;最後交給 pandas 的 read_html 把 HTML 表格直接讀成 DataFrame,略作清理(去掉 Notes 一欄)即成。講者提醒:30 隻成分股並非一成不變,指數公司會不時調整名單,所以「即場爬取」比「硬編碼名單」更符合實務——程式每次運行都取到當時的最新成分。

名單到手後,只取 Symbol 一欄轉成 Python list,便可直接餵給 yfinance。

以 requests 與 BeautifulSoup 從 Wikipedia 爬取道指成分股表格的程式碼(截圖出自原片(Liu Peng))

第二步:下載價格並換算月回報

把 30 個股票代碼以 list 形式傳入 yfinance 的 download 函數,一次過下載全部股票的歷史數據,實驗取用的時段橫跨 2021 年初至 2022 年 9 月。回傳的 DataFrame 以日期為索引、每欄對應一隻股票;講者只保留調整後收市價 Adjusted Close,因為它已剔除拆股與股息造成的跳空,是回報計算的標準輸入。

接着是頻率轉換:策略以月為決策單位——月度是較能代表整體表現的聚合尺度——要把日線數據聚合為月回報。做法是先以 pct_change 把價格轉成日簡單回報,再用 resample('M') 按月分組,最後以 agg 指定聚合方式:

# 摘錄自原片教學
mth_return_df = df.pct_change().resample('M').agg(lambda x: (x+1).prod()-1)

這一行對應的數學是把月內每日的 $1+r$ 連乘再減一:

$$R^{(m)} = \prod_{d=1}^{D}(1+r_d) - 1$$

其中 $r_d$ 是第 $d$ 個交易日的簡單回報,$D$ 是該月交易日數目。連乘體現複利效應——這是累積回報唯一正確的算法,把日回報直接相加會得出錯誤結果。輸出的月回報 DataFrame 以每月月末日期為索引,每格是該股該月的簡單回報。

為免學生對 resample 一知半解,講者特別插入一個玩具例子:建立一條 0 至 8、以一分鐘為間隔的時間序列,再以 resample('3T').sum() 按三分鐘一組求和——首三個值 0、1、2 相加得 3,次三個值 3、4、5 相加得 12,一目了然。resample 的本質就是「按時間段分箱,再對每箱做聚合」,放在月回報場景,箱子是月份,聚合方式是連乘。

第三步:滾動六個月累積回報(形成期)

單看一個月回報太短,排名易受單月噪音主導。動量策略的標準做法是看形成期 Formation Period 的累積表現——本 lab 用六個月:站在某個月末,回望過去六個月(包括當月),計算累積回報作為強弱排名的依據。數學上同樣是 $1+R$ 連乘:

$$R^{(6)}_t = \prod_{k=0}^{5}\left(1+R^{(m)}_{t-k}\right) - 1$$

程式上用 rollingapply 一行完成:

# 摘錄自原片教學
past_cum_return_df = (mth_return_df + 1).rolling(6).apply(np.prod) - 1

rolling(6) 把窗口沿時間軸逐月滾動,每個窗口含六個月的 $1+R$ 值;`apply(np.prod)` 對窗口內六個數連乘,得出 \$1+R$ 形式的累積回報;最後減一轉回簡單回報。輸出的 DataFrame 首五行全是 NaN——前五個月沒有足夠歷史數據填滿六期窗口,累積回報在定義上不存在,與第 19 講 SMA 期初空值同理;由第六個月起,每格才是貨真價實的「過去六個月累積回報」。

以 rolling(6).apply(np.prod) 計算過去六個月累積回報:首五個月為 NaN,第六個月起有值(截圖出自原片(Liu Peng))

第四步:劃分三個時期,鎖定選股空間

進入訊號生成前,講者先清楚劃分三個時期,這是理解整個流程的骨架:

三步對應「看歷史、下決定、驗成果」,時間上嚴格先後排列,不存在用未來數據做決定的問題。

選股空間是量度期末(2022 年 6 月末)那一行的數據:30 隻股票各自的六個月累積回報。講者先用 idxmax 找出回報最高的股票——理論上屬於最強的一群,將會做多;再用 idxmin 找出回報最低者——將會做空。這兩步只是熱身,真正的分組交給 qcut

第五步:qcut 五分位分組,多空建倉

qcut 是 pandas 的分位數分組函數:把序列按數值大小排序後切成若干個元素數目相等的組。講者先以一條十元素的序列示範 pd.qcut(series, 5, labels=False):切五組、每組兩個元素,輸出的組別標籤由 0(最小)到 4(最大)。

套用到真實數據:對量度期末那 30 個六個月累積回報做 qcut,分成五組、每組六隻,新增一欄 rank 記錄每隻股票所屬組別。rank 4 是回報最高的五分之一——做多;rank 0 是回報最低的五分之一——做空:

# 摘錄自原片教學
long_stocks = df.loc[df["rank"] == 4, "index"].values
short_stocks = df.loc[df["rank"] == 0, "index"].values

loc 篩出對應組別的索引(即股票代碼),兩張名單各六隻,多空組合就此建成。整個建倉邏輯完全由歷史回報排名驅動,不涉及任何主觀判斷。

量度期末的六個月累積回報表,配合 idxmax、idxmin 與 qcut 分組(截圖出自原片(Liu Peng))

第六步:評估期績效與買入持有基準

最後檢驗成果。用 dateutil 的 relativedelta(months=1) 把形成期推後一個月,定位評估期(2022 年 8 月)的月回報行,再以 columns.isin 分別抽出做多名單與做空名單的回報。兩組各取平均,做空組前面加負號(做空者從下跌中獲利),相加即得動量組合的月回報:

$$\pi_{\text{mom}} = \bar{R}_{\text{long}} - \bar{R}_{\text{short}}$$

實驗結果約為 $+1.5\%$。講者再與買入持有 Buy and Hold 基準對照:下載道指本身(^DJI)同期數據,同樣經 pct_change 轉日回報、resample 聚合成月回報,取評估期的值——約為 $-4\%$。講者解釋背景:當時正值新冠疫情後遺與利率急升,大市普遍向下,買入持有全數承受跌幅;動量組合在同一月份錄得正回報,因為它做空了最弱的一組,從下跌的一端賺取了收益。至此,由取數、清洗、訊號、建倉到績效評估的完整流程走完,理論與程式一一對應。

動量組合回報約 1.5%,與買入持有基準比較(截圖出自原片(Liu Peng))

🙏 覺得內容有用?本站所有內容免費提供,使用推薦碼開戶就是支持我們繼續營運的最大鼓勵:

ZA Bank 邀請碼 P20923 —— 你享 HKD 2,000 迎新獎賞,本站獲得營運支持。 立即開戶 → (聯盟連結|投資涉及風險,受條款及細則約束,此資料僅可於香港境內分發)

欣利克的看法

名詞解釋

常見問題 FAQ

為什麼月回報不能直接把每日回報相加?

因為回報有複利效應。今日的回報是在昨日已變動的本金上計算的,正確做法是把每日的 $1+r$ 連乘再減一,即 $R = \prod(1+r_d)-1$。直接相加在數值小時看似接近,但期數一多或波動一大,偏差便會浮現。

qcut 與手動排名有什麼分別?

qcut 按分位數分組,保證每組元素數目大致相等——30 隻股票切五組,每組剛好六隻,多空兩端規模對稱。手動排名再取頭尾若干隻也能達到類似效果,但 qcut 一行完成排序、分箱、貼標籤三個步驟,程式更簡潔,也不易出錯。

為什麼累積回報 DataFrame 的開頭幾個月是 NaN?

因為滾動窗口尚未填滿。以六個月窗口計算時,首五個月之前沒有足夠歷史數據,累積回報在定義上不存在,pandas 以 NaN 標示;由第六個月起窗口首次填滿,才有有效數值。這與移動平均線期初出現空值是同一道理。

實驗中動量策略跑贏買入持有,是否證明策略有效?

不能這樣推論。實驗只涵蓋 2022 年 8 月一個評估期,且當時大市因加息普遍下跌,做空端自然佔優,單期結果隨機成分極高。嚴謹驗證需要在多年數據上重複整個流程數百次,檢驗平均回報的統計顯著性,並計入交易成本後再下結論。

原影片

本文整理自以下影片,版權屬原創作者所有,建議配合原片觀看:

來源:https://www.youtube.com/watch?v=WcKAqhCglWQ

🏦 ZA Bank 開戶優惠

本站所有內容免費提供。多使用網站的推薦碼,就是對我們繼續製作優質內容的最大鼓勵和支持 🙏

去 ZA Bank 開戶,享價值 HKD 2,000 迎新獎賞,仲有機會享 USD 30 指定美股兌換券!

  • 💛 全線上開戶,無需前往分行
  • 💛 無最低存款要求,零賬戶管理費
  • 💛 24x7 銀行服務

專屬邀請碼:P20923

立即開戶 → (聯盟連結)