雙因子權重設置

本文重點概要

  • 文章難度:★★★★☆
  • 等權重法與 IC權重法對本次回測的分組績效並沒有顯著差異。
  • 閱讀建議:在優化多因子權重前,應該先對單因子進行分層回測與橫斷面迴歸分析,來檢驗單因子是否具有獲利性、穩定性與可解釋性,在深入研究多因子時,方能找出最具綜效的因子組合。故本文參考葉怡成【台股研究室一書,選用股東權益報酬率與股價淨值比來分析權重設置對因子投組的影響。
  • 完整程式碼附在 TEJ github

前言

常見的權重設置是等權重與市值比權重,前者將所有因子視為相同,賦予不同因子等權重,並合成新的因子值;後者則是先在個別因子內以市值比加權合成新的因子值,再賦予不同因子等權重,再合成新的因子值,然而兩種方式皆沒有考慮因子的有效性。

本文考慮因子有效性,新增 IC權重法,迴歸個別因子對往後滾動持有報酬率的相關係數,獲得 IC數值,若 IC數值高,則代表該因子當下較具有效性,在每月初動態調整因子權重時,賦予 IC數值較高的因子有較高權重。

編輯環境及模組需求

本文使用 Windows OS 並以 Jupyter Notebook 作為編輯器

#功能與視覺化模組 import pandas as pd from scipy import stats from collections import OrderedDict import matplotlib.pyplot as plt import copy #TEJ import tejapi tejapi.ApiConfig.api_key = 'Your Key' tejapi.ApiConfig.ignoretz = True

資料庫使用

資料處理

Step 1. ROE、財報發布日、股價淨值比與股價資料撈取

# 撈取上市所有普通股證券代碼 code = tejapi.get("TWN/EWNPRCSTD", paginate=True, chinese_column_name=True) code = code["證券碼"][(code["證券種類名稱"] == "普通股") & (code["上市別"].isin(["TSE"]))].to_list() # 匯入 ROE變數 data_ROE = tejapi.get('TWN/AIFIN',                       coid = code,                       mdate= {'gte': '2019-01-01','lte':'2020-12-31'},                       opts={'pivot':True ,'columns':['coid', 'mdate', 'R103']},                       chinese_column_name=True,                       paginate=True) # 匯入 財報發布日 data_annouce = tejapi.get('TWN/AIFINA',                           coid = code,                           mdate= {'gte': '2019-01-01','lte':'2020-12-31'},                           opts={'columns':['coid', 'mdate', 'a0003']},                           chinese_column_name=True,                           paginate=True) # 匯入 調整後開盤價、收盤價與股價淨值比 data_PB_price = tejapi.get('TWN/APRCD1',                            coid = code,                            mdate= {'gte': '2019-03-01','lte':'2020-03-31'},                            opts={'columns':['coid', 'mdate','open_adj','close_adj','pbr_tej']},                            chinese_column_name=True,                            paginate=True)
資料表(一)

先撈取所有上市公司代碼,在使用 TEJ API到不同資料庫中一次撈取全部公司財務資料。

Step 2. 資料前處理

# 整理股價 data_PB_price = data_PB_price.rename({'證券代碼': '公司'}, axis=1) # 改名字 data_PB_price['開盤價(元) t+1'] = data_PB_price.groupby('公司')['開盤價(元)'].shift(-1) # 新增年與月 data_PB_price['年'] = data_PB_price['年月日'].dt.year data_PB_price['月'] = data_PB_price['年月日'].dt.month data_PB_price = data_PB_price.sort_values(by=['年月日']).reset_index(drop=True) # 排序年月日

Step 3. 合併不同資料頻率與篩選月底資料

# 合併季頻率 ROE與財報發布日 data_all = pd.merge(data_ROE ,data_annouce ,how = 'left' , on = ['公司','年/月']) data_all = data_all.rename({'年/月': '財報'}, axis=1) # 改名字 data_all = data_all.sort_values(by=['財報發布日']).reset_index(drop=True) # 排序財報發布日 # 合併季頻率與日頻率資料 data_all = pd.merge_asof(data_PB_price ,data_all ,left_on = '年月日', right_on = '財報發布日', by = ['公司'] ,direction = "backward") # 留下每月底資料 data_all = data_all.drop_duplicates(subset=['公司','年','月'], keep='last') data_all = data_all.sort_values(by=['公司','年月日']).reset_index(drop=True) data_all['月持有報酬率'] = data_all.groupby('公司')['收盤價(元)'].shift(-1).reset_index(drop=True)      / data_all['開盤價(元) t+1'] -1
資料表(二)

pd.mergepd.merge_asof 是合併 dataframe很好用的工具,前者是合併相同頻率資料,而後者是合併不同頻率資料,透過參數 direction = backward可避免資料前視偏誤;pd.drop_duplicates 則是可以篩選年月,保留各公司每月底的資料,方便我們獲得當下準確的因子值,以利因子排序分組。

Step 4. 刪除缺失值

data_all = data_all.dropna().reset_index(drop=True) data_all = data_all.set_index(['年月日','公司']) data_all = data_all[['股價淨值比-TEJ','ROE(A)-稅後','月持有報酬率']]

計算 IC值與建立投組

Step 1. 排序個別因子

def get_rank(data):     factors_name=[i for i in data.columns.tolist() if i not in ['月持有報酬率']] # 得到因子名     rank_df = []     for factor in factors_name:         if factor in ['股價淨值比-TEJ']:             rank_list = data[factor].groupby(level=0).rank(ascending = True) # rank由小到大排序,即值越小,排名越靠前         else:             rank_list = data[factor].groupby(level=0).rank(ascending = False) # rank由大到小排序,即值越大,排名越靠前         rank_df.append(rank_list)     rank_df = pd.concat(rank_df, axis=1)     return rank_df

基於獲利性概念的 ROE,越大越有高期望報酬率,故由大到小排列,即值越大,排名越靠前;基於價值股概念的股價淨值比,越小越有高期望報酬率,故由小到大排列,即值越小,排名越靠前。

Step 2. 獲得 IC數值

def get_ic(data):     factors_name=[i for i in data.columns.tolist() if i not in ['月持有報酬率']] # 得到因子名     ic = data.groupby(level=0).         apply(lambda data: [stats.spearmanr(data[factor],                                             data['月持有報酬率'])[1] for factor in factors_name])     ic = pd.DataFrame(ic.tolist(), index=ic.index, columns=factors_name)     return ic

IC 全名為 Information Coefficient,說明在某橫斷面時間下,因子對往後持有報酬率的相關係數,當下 IC值越高,表示未來持有期望報酬越高, IC 可用來比較不同因子的有效性。

Step 3. 合成新的因子值

# 根據IC計算因子權重 def ic_weight(data):     data_= data.copy()     ic = get_ic(data)       ic0 = ic.abs() # 計算 IC絕對值     rolling_ic = ic0.rolling(12,min_periods=1).mean()  # 滾動 12个月     weight = rolling_ic.div(rolling_ic.sum(axis=1),axis=0)  # 計算 IC權重,按行求和,按列相除     ranks = get_rank(data)   # 得到各因子的排序數據     score_ = OrderedDict()     for date in weight.index.tolist():         rank = ranks.loc[date]         score = rank * weight.loc[date]          score_[date] = score.sum(axis=1).to_frame().rename(columns={0: 'score'})     score_df = pd.concat(score_.values(),keys=score_.keys())     score_df = score_df.reset_index().rename({'level_0': '年月日'}, axis=1)     score_df = score_df.set_index(['年月日','公司'])     data_ = data_.join(score_df)     data_ = data_.reset_index()     return data_ data_ic = ic_weight(data_all) data_ic['因子每年分組'] = data_ic.groupby('年月日')['score'].rank().     transform(lambda x: pd.qcut(x, 10, labels = range(1,11)))
資料表(三)

將排序後因子乘上因子 IC值權重,即各因子平均 IC值在所有因子平均 IC值加總中的佔比,讓 IC值大的因子有高的權重,再合成新的score因子值,由小到大排序分成 10組。

視覺化分組結果

Step 1. 計算投組逐月績效

def arrange_group_return(tempt):     tempt = tempt.groupby(['年月日','因子每年分組'])[['月持有報酬率']].mean().reset_index()     tempt['月持有報酬率'] = tempt['月持有報酬率'] * 100     tempt = pd.pivot_table(tempt, values='月持有報酬率', index=['因子每年分組'] ,columns=['年月日'])     tempt.index = tempt.index.astype(str)     tempt = tempt.T     tempt = tempt.cumsum().dropna()     tempt.index = tempt.index.astype(str).str[:7]     return tempt data_ic = arrange_group_return(data_ic) data_ic.round(2)
資料表(四)

Step 2. 資料前處理

# 整理股價
data_PB_price = data_PB_price.rename({'證券代碼': '公司'}, axis=1) # 改名字
data_PB_price['開盤價(元) t+1'] = data_PB_price.groupby('公司')['開盤價(元)'].shift(-1)# 新增年與月
data_PB_price['年'] = data_PB_price['年月日'].dt.year
data_PB_price['月'] = data_PB_price['年月日'].dt.month
data_PB_price = data_PB_price.sort_values(by=['年月日']).reset_index(drop=True) # 排序年月日

Step 3. 合併不同資料頻率與篩選月底資料

# 合併季頻率 ROE與財報發布日
data_all = pd.merge(data_ROE ,data_annouce ,how = 'left' , on = ['公司','年/月'])
data_all = data_all.rename({'年/月': '財報'}, axis=1) # 改名字
data_all = data_all.sort_values(by=['財報發布日']).reset_index(drop=True) # 排序財報發布日# 合併季頻率與日頻率資料
data_all = pd.merge_asof(data_PB_price ,data_all ,left_on = '年月日', right_on = '財報發布日', by = ['公司'] ,direction = "backward")# 留下每月底資料
data_all = data_all.drop_duplicates(subset=['公司','年','月'], keep='last')
data_all = data_all.sort_values(by=['公司','年月日']).reset_index(drop=True)
data_all['月持有報酬率'] = data_all.groupby('公司')['收盤價(元)'].shift(-1).reset_index(drop=True) \
/ data_all['開盤價(元) t+1'] -1
資料表(五)

pd.merge 與 pd.merge_asof 是合併 dataframe很好用的工具,前者是合併相同頻率資料,而後者是合併不同頻率資料,透過參數 direction = backward可避免資料前視偏誤;pd.drop_duplicates 則是可以篩選年月,保留各公司每月底的資料,方便我們獲得當下準確的因子值,以利因子排序分組。

Step 4. 刪除缺失值

data_all = data_all.dropna().reset_index(drop=True)
data_all = data_all.set_index(['年月日','公司'])
data_all = data_all[['股價淨值比-TEJ','ROE(A)-稅後','月持有報酬率']]

計算 IC值與建立投組

Step 1. 排序個別因子

def get_rank(data):
factors_name=[i for i in data.columns.tolist() if i not in ['月持有報酬率']] # 得到因子名

rank_df = []
for factor in factors_name:
if factor in ['股價淨值比-TEJ']:
rank_list = data[factor].groupby(level=0).rank(ascending = True) # rank由小到大排序,即值越小,排名越靠前
else:
rank_list = data[factor].groupby(level=0).rank(ascending = False) # rank由大到小排序,即值越大,排名越靠前

rank_df.append(rank_list)

rank_df = pd.concat(rank_df, axis=1)
return rank_df

基於獲利性概念的 ROE,越大越有高期望報酬率,故由大到小排列,即值越大,排名越靠前;基於價值股概念的股價淨值比,越小越有高期望報酬率,故由小到大排列,即值越小,排名越靠前。

Step 2. 獲得 IC數值

def get_ic(data):
factors_name=[i for i in data.columns.tolist() if i not in ['月持有報酬率']] # 得到因子名
ic = data.groupby(level=0).\
apply(lambda data: [stats.spearmanr(data[factor],
data['月持有報酬率'])[1] for factor in factors_name])

ic = pd.DataFrame(ic.tolist(), index=ic.index, columns=factors_name)
return ic

IC 全名為 Information Coefficient,說明在某橫斷面時間下,因子對往後持有報酬率的相關係數,當下 IC值越高,表示未來持有期望報酬越高, IC 可用來比較不同因子的有效性。

Step 3. 合成新的因子值

# 根據IC計算因子權重
def ic_weight(data):
data_= data.copy()
ic = get_ic(data)
ic0 = ic.abs() # 計算 IC絕對值
rolling_ic = ic0.rolling(12,min_periods=1).mean() # 滾動 12个月
weight = rolling_ic.div(rolling_ic.sum(axis=1),axis=0) # 計算 IC權重,按行求和,按列相除
ranks = get_rank(data) # 得到各因子的排序數據

score_ = OrderedDict()
for date in weight.index.tolist():
rank = ranks.loc[date]
score = rank * weight.loc[date]
score_[date] = score.sum(axis=1).to_frame().rename(columns={0: 'score'})

score_df = pd.concat(score_.values(),keys=score_.keys())
score_df = score_df.reset_index().rename({'level_0': '年月日'}, axis=1)
score_df = score_df.set_index(['年月日','公司'])
data_ = data_.join(score_df)
data_ = data_.reset_index()
return data_data_ic = ic_weight(data_all)
data_ic['因子每年分組'] = data_ic.groupby('年月日')['score'].rank().\
transform(lambda x: pd.qcut(x, 10, labels = range(1,11)))
資料表(六)

將排序後因子乘上因子 IC值權重,即各因子平均 IC值在所有因子平均 IC值加總中的佔比,讓 IC值大的因子有高的權重,再合成新的score因子值,由小到大排序分成 10組。

視覺化分組結果

Step 1. 計算投組逐月績效

def arrange_group_return(tempt):
tempt = tempt.groupby(['年月日','因子每年分組'])[['月持有報酬率']].mean().reset_index()
tempt['月持有報酬率'] = tempt['月持有報酬率'] * 100
tempt = pd.pivot_table(tempt, values='月持有報酬率', index=['因子每年分組'] ,columns=['年月日'])
tempt.index = tempt.index.astype(str)
tempt = tempt.T
tempt = tempt.cumsum().dropna()
tempt.index = tempt.index.astype(str).str[:7]

return temptdata_ic = arrange_group_return(data_ic)data_ic.round(2)
資料表(七)

Step 2. 視覺化投組累積績效

def draw_group_return(tempt ,weight_method):
fig = plt.figure(figsize = (14,7))
ax = fig.add_subplot()

ax.set_title(weight_method,
fontsize=16,
fontweight='bold')

for i in tempt.columns:
ax.plot(tempt[i], linewidth=2, alpha=0.8 , marker='o')

ax.legend(tempt.columns,loc=2)

plt.grid(axis='y')
plt.xlabel('日期', fontsize=18)
plt.ylabel('報酬率 %',rotation=0, fontsize=18,labelpad=20)
plt.xticks(fontsize=12)
plt.yticks(fontsize=12)
plt.show()draw_group_return(data_ic, 'IC加權法')
圖(一)
圖(二)
# 比較 等權重法與 IC加權法
data_equal_ir = pd.merge(
pd.DataFrame(data_equal['1']).rename({'1':'等權重法'}, axis=1),
pd.DataFrame(data_ic['1']).rename({'1':'IC加權法'}, axis=1),
how='inner',left_index=True, right_index=True)draw_group_return(data_equal_ir, '等權重法 vs IC加權法')

比較兩種權重法的第一組,也就是 ROE最大與股價淨值比最小的分組。

圖(三)

結論

發現 2019年初到 2020年底股價淨值比與 ROE合成的因子分組績效並沒有呈現穩定的單調性與區分度,分組的累積績效線有時甚至會糾結,分析結果可能原因是交易頻率過高,ROE只有季資料,合成因子受股價淨值比影響較大。再者發現等權重法與 IC加權法在不同時段內,並沒有顯著差異。

未來可試著降低交易頻率,以及回測不同的因子組合,挖掘出有穩定單調性與區別度的因子組合,再進一步優化因子權重。如果讀者對於其他因子資料有興趣,歡迎到 TEJ E-Shop 選擇最適的方案,找出具有獲利性、穩定性與可解釋性的因子!

完整程式碼

延伸閱讀

相關連結

返回總覽頁
Procesing