尋找Alpha

透過Fama&French三因子模型所得到的alpha,來建構long-short strategy,並回測與大盤的績效比較。

Photo by Ishant Mishra on Unsplash

本文重點概要

文章難度:★★☆☆☆

使用Fama&French三因子模型來計算台灣上市櫃股票的alpha,並取得alpha最高的前20%股票以及alpha最低的20%股票,做一個多空策略的投資組合,並評斷績效。

閱讀建議:本文會從三因子模型的建立開始,計算出SMB以及HML,因此需要對投資學裡的資本資產定價模型(CAPM)有初步認識,並且了解alpha以及beta的概念,對閱讀文章會更有幫助。

前言

資本資產定價模型(CAPM),在現代投資組合理論中佔據了相當重要的地位,也是現代金融市場價格理論的基礎,而後續許多學者不斷在此基礎上延伸,開創了各式各樣的因子模型,甚至有因子動物園(factor zoo)之說,如本文使用的Fama&French的三因子模型,是將原本CAPM僅考慮市場風險因子的部分,而外加入了規模溢酬以及B/M ratio溢酬,以期待投資組合或股票的報酬率,能被這三個因子所解釋。

編輯環境及模組需求

本文使用Mac OS並以jupyter作為編輯器

import pandas as pd
import numpy as np
import tejapi
import statsmodels.api as sm
import matplotlib.pyplot as plt
import matplotlib.transforms as transforms
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS'] # 解決MAC電腦 plot中文問題
plt.rcParams['axes.unicode_minus'] = False
tejapi.ApiConfig.api_key ="Your Key"
tejapi.ApiConfig.ignoretz = True

資料庫使用

調整股價(日)-除權息調整(TWN/APRCD1)
證券屬性資料(TWN/ANPRCSTD)

資料導入

資料期間取自2014年到2021年6月,分別取得了上市櫃股票代碼、收盤價、報酬率、市值以及股價淨值比資料。

data=tejapi.get('TWN/ANPRCSTD' ,chinese_column_name=True )
select=data["上市別"].unique()
select=select[1:3]
condition =(data["上市別"].isin(select)) & ( data["證券種類名稱"]=="普通股" )
data=data[condition]
twid=data["證券碼"].to_list() #取得上市櫃股票證券碼

df = pd.DataFrame()
for i in twid: #資料筆數超過100萬筆,透過迴圈方式抓取
df = pd.concat([df, tejapi.get('TWN/APRCD1', #從TEJ api撈取所需要的資料
chinese_column_name = True,
paginate = True,
mdate = {'gt':'2013-12-31', 'lt':'2022-07-01'},
coid=i,
opts={'columns':['coid','mdate', 'close_adj' ,'roi' ,'mv', "pbr_tej"]})])

先將股價淨值比取倒數得到帳面市值比,再取得每日市值的中位數,並將大於中位數的股票標記為B,小於中位數的標記為S,形成兩個投資組合。

df['帳面市值比'] = 1/df['股價淨值比-TEJ']

ME = df.groupby('年月日')['市值(百萬元)'].apply(lambda x: x.median())
ME.name = '市值_中位數'
df = df.merge(ME, on='年月日')
df['市值matrix'] = np.where(df['市值(百萬元)']>df['市值_中位數'], 'B', 'S')

將大型股投資組合與小型股投資組合,用市值加權的方式,得到投資組合的權重,並確認兩個投資組合的權重總合是否都等於1。

df1 = (df.groupby(['年月日','市值matrix'])['市值(百萬元)'].sum()).reset_index()
df = df.merge(df1, on=['年月日','市值matrix'])
df['weight'] = df['市值(百萬元)_x']/df['市值(百萬元)_y']
df.groupby(['年月日','市值matrix'])['weight'].sum()

df.groupby(['年月日','市值matrix'])['weight'].sum()
市值說明圖

計算投資組合的報酬率,並將小型股投組報酬率減掉大型股投組報酬率,組合成一個long-short portfolio,完成SMB因子。

df['return1'] = df['報酬率%']* df['weight']
SMB = df.groupby(['年月日','市值matrix'])['return1'].sum()
SMB.reset_index(inplace=True)
SMB.set_index('年月日',drop=True, inplace=True)
SMB = SMB[SMB['市值matrix']=='S']['return1'] - SMB[SMB['市值matrix']=='B']['return1']
SMB.name = 'SMB'
SMB

Fama&French將 BM ratio分成30%, 40%, 30%,因此我取得30百分位數以及70百分位數的BM_ratio,並將大於70百分位數的標記為V(value),小於30%的標記為G(growth),其餘的則標記為N(Neutral),形成三個投資組合。

a = df.groupby('年月日')['帳面市值比'].quantile(0.7)
a.name = 'BM_0.7'
b = df.groupby('年月日')['帳面市值比'].quantile(0.3)
b.name = 'BM_0.3'
df = df.merge(a, on='年月日')
df = df.merge(b, on='年月日')
df['BM_matrix'] = np.where(df['帳面市值比']>df['BM_0.7'], 'V', (np.where(df['帳面市值比']<df['BM_0.3'],'G', 'N')))

一樣使用市值加權的方式來計算三個投資組合的權重,並確認權重是否總和為一。

df2 = (df.groupby(['年月日','BM_matrix'])['市值(百萬元)_x'].sum()).reset_index()
df = df.merge(df2, on=['年月日','BM_matrix'])
df['weight2'] = df['市值(百萬元)_x_x']/df['市值(百萬元)_x_y']
df.groupby(['年月日','BM_matrix'])['weight2'].sum()
市值說明圖

計算投資組合的報酬率,並將價值股投組報酬率減掉成長股投組報酬率,組合成一個long-short portfolio,完成HML因子。

df['return2'] = df['報酬率%']* df['weight2']
HML = df.groupby(['年月日','BM_matrix'])['return2'].sum()
HML.reset_index(inplace=True)
HML.set_index('年月日',drop=True, inplace=True)
HML = HML[HML['BM_matrix']=='V']['return2'] - HML[HML['BM_matrix']=='G']['return2']
HML.name = 'HML'
HML

將計算好的SMB和HML因子合併

大盤報酬率

抓取大盤的報酬率,作為三因子模型中第一個的市場因子

Y9999 = tejapi.get('TWN/APRCD1',  #從TEJ api撈取所需要的資料
chinese_column_name = True,
paginate = True,
mdate = {'gt':'2013-12-31', 'lt':'2022-07-01'},
coid='Y9999',
opts={'columns':['coid','mdate', 'roi']})

把三個因子合併成一張表

fama = fama.merge(Y9999[['年月日','報酬率%']], on='年月日')
fama.rename(columns = {'報酬率%':'rm'}, inplace=True)
fama.set_index('年月日',drop=True,inplace=True)
fama
三個因子合併

將個股的報酬率篩選出來

stock = df[['證券代碼', '年月日','報酬率%']]
stock.set_index('年月日', drop=True, inplace=True)
stock = stock.loc[:'2022-06-30']
stock
個股的報酬率

我們目標的投資組合是將所有股票前20%的alpha扣掉後20%的alpha,組合出一個多空策略,權重部分採用等權重法,再平衡的方式為每半年調整一次,將使用前半年資料篩選出的投組,用在下半年並進行回測。

m = pd.date_range('2013-12-31', '2022-07-31', freq='6M').to_list()
X = sm.add_constant(fama)
stock_list = stock['證券代碼'].unique()

b = pd.DataFrame()
for j in stock_list:
a=[]
for i in range(len(m)-1):
try:
Y = (stock[stock['證券代碼']== j]).loc[m[i]:m[i+1]]
result = sm.OLS(Y['報酬率%'], X.loc[m[i]:m[i+1]]).fit()
a.append(result.params[0])
except:
pass
j = str(j)
c = pd.DataFrame({'證券代碼':([j]*len(a)), 'alpha':a}, index=m[1:len(a)+1])
b = pd.concat([b,c])
b.index.name = '年月日'

計算出第80百分位數以及第20百分位數的alpha數值,並將大於80百分位數的個股篩選出來形成做多投組,小於20%的形成做空投組。

alpha1 = b.groupby('年月日')['alpha'].apply(lambda x : x.quantile(0.8))
alpha1.name = 'alpha0.8'
alpha2 = b.groupby('年月日')['alpha'].apply(lambda x : x.quantile(0.2))
alpha2.name = 'alpha0.2'
b = b.merge(alpha1, on='年月日')
b = b.merge(alpha2, on='年月日')
long = (b.where(b['alpha'] > b['alpha0.8'])).dropna()
short = (b.where(b['alpha'] < b['alpha0.2'])).dropna()

在計算回測報酬率前先做一些資料預處理

stock1 = df[['證券代碼','年月日','收盤價(元)']]
stock1.set_index('年月日',drop=True, inplace=True)
stock1 = stock1.loc[:"2022-06-30"]
stock1['證券代碼'] = stock1['證券代碼'].astype('str')

計算出做多投組的報酬率以及做空投組的報酬率,並計算此alpha策略的報酬率。

ret = []
for i in range(1, len(m)-1):
qq = (stock1.loc[m[i]:m[i+1]])['證券代碼'].isin((long.loc[m[i]])['證券代碼'].tolist())
a = ((stock1.loc[m[i]:m[i+1]])[qq]).groupby('證券代碼')['收盤價(元)'].tail(1).sum()
b = ((stock1.loc[m[i]:m[i+1]])[qq]).groupby('證券代碼')['收盤價(元)'].head(1).sum()
c = len((long.loc[m[i]])['證券代碼'].tolist())
long_ret = ((a/b)-1)/c
qq1 = (stock1.loc[m[i]:m[i+1]])['證券代碼'].isin((short.loc[m[i]])['證券代碼'].tolist())
a1 = ((stock1.loc[m[i]:m[i+1]])[qq1]).groupby('證券代碼')['收盤價(元)'].tail(1).sum()
b1 = ((stock1.loc[m[i]:m[i+1]])[qq1]).groupby('證券代碼')['收盤價(元)'].head(1).sum()
c1 = len((short.loc[m[i]])['證券代碼'].tolist())
short_ret = ((a1/b1)-1)/c1
ret.append(long_ret - short_ret)

理論上alpha代表了超額報酬率,也就是可以超過大盤所賺到的報酬率,但從結果發現,利用這個策略形成的投資組合,無論大盤大漲大跌,報酬率皆介於0上下,可見這幾個因子已經有失效的疑慮。

y9999  = tejapi.get('TWN/APRCD1',  #從TEJ api撈取所需要的資料
chinese_column_name = True,
paginate = True,
mdate = {'gt':'2013-12-31', 'lt':'2022-07-01'},
coid='Y9999',
opts={'columns':['coid','mdate', 'close_adj']})

y9999.set_index('年月日' ,drop=True, inplace=True)

a = []
for i in range(1 , len(m)-1):
b = (((y9999.loc[m[i]:m[i+1]]).tail(1)['收盤價(元)'].values / (y9999.loc[m[i]:m[i+1]]).head(1)['收盤價(元)'].values) -1)[0]
a.append(b)

ret['大盤'] = a
ret[['ret', '大盤']].apply(lambda x :x*100)
收盤價

結論

從結果可以看到,我們尋找alpha的投資組合報酬率幾乎在0上下,可能原因為Fama&French 的三因子模型發現甚早,存在的異常報酬機會早已被投資人發現,因此失效,讀者可以進一步去了解其他因子模型,像是Fama&French 在2015年提出的五因子模型,去實際驗證績效,儘管三因子模型的報酬率看來沒有特別好,但仍然是資產定價中非常重要的一部分,在學術與業界中也很多人使用。

之後也會介紹使用TEJ資料庫來建構各式指標,並回測指標績效,所以歡迎對各種交易回測有興趣的讀者,選購TEJ E-Shop的相關方案,用高品質的資料庫,建構出適合自己的交易策略。

完整程式碼

延伸閱讀

相關連結

返回總覽頁
Procesing