Table of Contents
價值投資大師班傑明.葛拉漢的防禦型投資策略,班傑明.葛拉漢是華爾街公認的證券分析之父,1923年創立第一個私人基金-葛蘭赫公司,初試啼聲操作績效即非常優異。1925年因合夥人意見不合而清算解散,1926年和友人合資設立葛拉漢聯合投資帳戶(Joint Account),至1929年初資金規模由45萬美元成長至250萬美元(非新投資者)。一夕之間,葛拉漢之名成為華爾街的寵兒,多家上市公司的所有人皆希望葛拉漢為他們負責合夥基金,但皆因葛拉漢認為股市已過度飆漲而婉拒。
1934年葛拉漢和陶德(David L. Dodd)合著「有價證券分析」(Security Analysis)一書,成為證券分析的開山始祖,在葛拉漢之前,證券分析仍不能被視為一門學問。此書至今仍未絕版,是大學證券分析的標準教科書之一。
當代著名的基金經理人如華倫.巴菲特(Warren Buffett)、約翰.奈夫(John Neff)、湯姆.芮普(Tom Knapp)等皆是葛拉漢的學生,目前華爾街只要是標榜價值投資法的基金經理人,也都是葛拉漢的徒子徒孫。
本篇文章屬於實戰策略,因此不論是篇幅還是難度上都有一定的程度,但請大家不要慌張,我們在文末都有提供完整程式碼與聯繫方式,若有看不懂或不會的都歡迎詢問我們~
班傑明.葛拉漢的防禦型投資策略
1. 選擇年銷售額逾一億美元的公司,或年銷售額逾 5000萬美元的公用事業股。
2. 流動比例應為 200%以上,且長期負債不超過淨流動資產。
3. 選擇過去十年,每年皆有盈餘的公司
4. 選擇連續 20年都支付股利的公司
5. 利用 3年平均值,選擇過去 10年每股盈餘至少成長 1/3的公司。
6. 股價÷三年平均每股盈餘小於 15倍。
7. 股價淨值比小於 1.5倍。
8. 投資組合中應保持 10–13種股票。
⬇️ 因應時空背景的轉換,我們對上列條件進行了調整與修正⬇️
調整後投資策略
1. 年營業額大於市場平均值的公司
2. 過去 5年皆有盈餘的公司
3. 連續 2年皆支付現金股利的公司
4. 流動比率 > 200%
5. 流動淨資產 — 長期負債 > 0
6. (近 3年平均稅後淨利-近 5年平均稅後淨利)/近 5年平均稅後淨利的絕對值>0.33
7. PER1(以近 3年平均每股盈餘計算)<= 近三年 PER之平均
8. PER(以近 4季每股盈餘計算)*PBR <= 近三年 PER*PBR之平均
台灣50指數成分股包含了市值排名前5️⃣0️⃣名的公司
建構步驟:
import tejapi
import pandas as pd
import numpy as np
tejapi.ApiConfig.api_key = 'your_key'
import datetime
import matplotlib.pyplot as plt
首先自 TW50.csv 中擷取台灣50指數成分股資料,但成分股的資料格式為(1101 台泥),而 tejapi.get 函數中 coid 是專門用來控制股票代碼的參數,coid 僅接受數字,程式碼第二行的主要功能在將成分股當中的股票代碼抽離。
由於一次抓取多股運行速度較慢,因此建議以迴圈的方式逐一抓取個股基本面數據,再透過 append 的方式擴增。
本次資料庫使用 IFRS以合併為主簡表(累計)-全產業(TWN/AIM1A)、上市(櫃)股價報酬(日)-報酬率 (TWN/APRCD2)和上市(櫃)未調整股價(年) (TWN/APRCY),試用帳號有數據取用上的限制,若想更自由的使用資料的話可以參考 TEJ E Shop🎁
stk_info = pd.read_csv('TW50.csv',engine='python')
stk_nums = stk_info['成份股'].apply(lambda x: str(x).split(' ')[0])
# 撈取財務資料
zz = pd.DataFrame()
for code in stk_nums:
zz = zz.append(tejapi.get('TWN/AIM1A'
,coid=code
,paginate=True,chinese_column_name=True
,opts= {'pivot':True}
)).reset_index(drop=True)
print(code)
開始建構大師策略👷👷
✅ 1. 年營業額大於市場平均值的公司
第一項條件針對營業額進行篩選,其目的在於過濾獲利性不佳的公司,而能被納入台灣50指數之個股,基本上都是頗有名氣的大公司,營收穩定,因此我們直接略過第一項條件 🏄🏄~
✅ 2. 過去5年皆有盈餘的公司
此處先對當期的稅前淨利進行是否大於0的判斷,然後再對判斷的結果進行滾動加總,最後滾動加總值剛好等於5者,視為符合第二項條件。
# 條件2:過去5年皆有盈餘
z1['earning'] = np.where(z1['常續性稅後淨利']>0,1,0)
z1['earning_continue'] = z1['earning'].rolling(5).sum()
z1['condition_2'] = np.where(z1['earning_continue']==5,1,0)
✅ 3. 連續2年皆支付現金股利的公司
此處先對當期的每股現金股利進行是否大於0的判斷,然後再對判斷的結果進行滾動加總,最後滾動加總值剛好等於2者,視為符合第三項條件。
# 條件3:過去2年皆有支付現金股利
z1['cash_dividend'] = np.where(z1['普通股每股現金股利(盈餘及公積)']>0,1,0)
z1['cash_dividends'] = z1['cash_dividend'].rolling(2).sum()
z1['condition_3'] = np.where(z1['cash_dividends']==2,1,0)
✅ 4. 流動比率>200%
流動比率TEJ已經幫我們內建好了,所以我們不必自己再算一遍💪💪~
# 條件4:流動比率>200%
z1['condition_4'] = np.where(z1['流動比率']>200,1,0)
✅ 5. 流動淨資產 — 長期負債 > 0
# 條件5:流動資產-長期負債>0
z1['condition_5'] = np.where((z1['流動資產']-z1['流動負債']-z1['非流動負債'])>0,1,0)
✅ 6. (近3年平均稅後淨利-近5年平均稅後淨利)/近5年平均稅後淨利的絕對值>0.33
– z1[‘歸屬母公司淨利(損)’].rolling(3).mean():近3年平均稅後淨利
– z1[‘歸屬母公司淨利(損)’].rolling(5).mean():近5年平均稅後淨利
# 條件6:abs【(近3年平均稅後淨利-近5年平均稅後淨利)/近5年平均稅後淨利】 >0.33
z1['近3年平均稅後淨利'] = z1['歸屬母公司淨利(損)'].rolling(3).mean()
z1['近5年平均稅後淨利'] = z1['歸屬母公司淨利(損)'].rolling(5).mean()
z1['condition_6'] = np.where(abs((z1['近3年平均稅後淨利']-z1['近5年平均稅後淨利'])/z1['近5年平均稅後淨利'])>0.33,1,0)
✅ 7. PER1(以近3年平均每股盈餘計算)<= 近三年PER之平均
– z1[‘每股盈餘’].rolling(3).mean():近3年平均每股盈餘
– z1[‘當季季底P/E’].rolling(3).mean():近三年PER之平均
– z1[‘close’]/z1[‘近3年平均EPS’]:PER1(以近3年平均每股盈餘計算)
# 條件7:PER (當年年底收盤價/近3年平均每股盈餘) <= 近3年PER之平均
z1['近3年平均EPS'] = z1['每股盈餘'].rolling(3).mean()
z1['近3年平均PER'] = z1['當季季底P/E'].rolling(3).mean()
z1['PER1'] = z1['close']/z1['近3年平均EPS']
z1['condition_7'] = np.where(z1['PER1']<=z1['近3年平均PER'],1,0)
✅ 8. PER(以近4季每股盈餘計算)*PBR <= 近三年PER*PBR之平均
IFRS以合併為主簡表(累計)-全產業中,第四季財報的EPS相當於當年度的累積總合=EPS(Q1+Q2+Q3+Q4)。而TEJ提供的PER事由累積4季的EPS求算出,故可直接使用TEJ內建的PER。
# 條件8:PER(以近4季每股盈餘計算)*PBR <= 近三年PER*PBR平均
z1['PEPB'] = z1['當季季底P/E']*z1['當季季底P/B']
z1['mean_PEPB_3'] = z1['PEPB'].rolling(3).mean()
z1['condition_8'] = np.where(z1['PEPB']<=z1['mean_PEPB_3'],1,0)
✅ 9. 計算總分
# 計算總分
z1['score'] = z1['condition_2']+z1['condition_3']+z1['condition_4']+z1['condition_5']+z1['condition_6']+z1['condition_7']+z1['condition_8']
(接下來我們將進入這本篇文章的核心,可能會有些難理解的部分,因此請大家再閱讀前先集中精神👀 📖 👍)
以總分排序,由高至低分成5個組距,分別回測5個投資組合績效
程式碼解說📚
date : 各期財報年月
buy_date : 設12/31為第 t 日,買進日期為 t+90 日
sell_date : 賣出日期為buy_date+365日(持有一年)
pf_H : 儲存各投資組合的股票代碼
data : 撈取多股(pf_H)年報酬率,日期設定在(buy_date, sell_date)之間
q1_ret : 多股年報酬,日期取最靠近 sell_date 為主
tw0050 : 撈取台灣50指數(TRI50)年報酬率,日期設定在(buy_date, sell_date)之間
假設所有個股權重相等,求算投資組合加權平均報酬:
投資組合股票數量 : len(pf_H)
權重 : w = 1/len(pf_H)
加權平均報酬 : sum(w*q1_ret)
手續費+交易稅 : (0.1425*2*len(pf_H) + 0.3*1)
# -*- coding: utf-8 -*-
"""
Created on Wed Apr 7 14:52:38 2021
@author: 2021011903
"""
return_=pd.DataFrame()
dates = result['財報年月'].astype(str).apply(lambda x: x.split(' ')[0]).unique()
step = 0.2
for date in dates:
# 設定日期
year = int(date.split('-')[0])
month = int(date.split('-')[1])
day = 31
date31 = str(year)+'-'+str(month)+'-'+str(day)
ret = [date31]
pf = result[result['財報年月']==date].sort_values(by='score',ascending=False).reset_index(drop=True)
## 將買進日期設在季底+90日 ##
buy_date = datetime.datetime(year,month,day)+ datetime.timedelta(90)
sell_date = buy_date + datetime.timedelta(365)
for n in np.arange(0,1,step):
# 設立組距 #
first = round(len(pf)*(n))
last = round(len(pf)*(n+step))
# 儲存選出來之公司 #
pf_H = pf.loc[first:last]['公司代碼'].to_list()
## 自 tejapi撈取年報酬資料,日期設定為 buy_date(含)至 sell_date(含) ##
print('getting data')
data = tejapi.get('TWN/APRCD2',coid =pf_H ,paginate = True,mdate={'gte':buy_date,'lte':sell_date},chinese_column_name=True)
q1_ret = data.groupby(by = '證券代碼').last()['年報酬率 %'].values
# 計算報酬率 #
print('calculating return')
w = 1/len(pf_H) # 等權重
q1_wret = (w*q1_ret).tolist() # 加權平均報酬
fee = round((0.1425*2*len(pf_H) + 0.3*1),2)
ret.append(round(sum(q1_wret)-fee,2))
## 撈取台灣 50指數的年報酬率,日期設定為 buy_date(含)至 sell_date(含) ##
tw0050 = tejapi.get('TWN/APRCD2',coid ='TRI50' ,paginate = True,mdate={'gte':buy_date,'lte':sell_date},chinese_column_name=True)
bm_return = tw0050.groupby(by = '證券代碼').last()['年報酬率 %'].values
if bm_return.size!=0:
ret.append(round(bm_return.tolist()[0],2))
else:
ret.append(None)
rets = np.reshape(np.array(ret),(1,7)).tolist()
retss = pd.DataFrame(data=rets,columns=['Date','p1_return','p2_return','p3_return','p4_return','p5_return','twn50_return'])
return_ = return_.append(retss).reset_index(drop=True)
print(return_)
台灣50指數2002年後才上市,因此第一個位置數值為NAN。
由於2020-12-31的買進日期為2021-03-31,並且要持有一年,也就是要取2022-03-31的年報酬率,但2022年是未來的資料我們無法取得,因此剃除2020-12-31計算出來的報酬率。
#計算累積報酬率#
cum_ret = return_[['p1_return','p2_return','p3_return','p4_return','p5_return','twn50_return']].astype(float).apply(lambda x:(x*0.01+1).cumprod(),axis=0).reset_index(drop=True)
cum_ret['Date'] = return_['Date']
#剔除2020-12-31#
cum_ret = cum_ret[:len(cum_ret)-1]
#欄位排序#
cum_ret = cum_ret[['Date','p1_return','p2_return','p3_return','p4_return','p5_return','twn50_return']]
cum_ret
plt.style.use('seaborn')
plt.figure(figsize=(10,5))
plt.xticks(rotation = 90)
plt.title('master invest strategy',fontsize = 20)
date = cum_ret['Date']
plt.plot(date,cum_ret.p1_return,color ='red',label='p1_return')
plt.plot(date,cum_ret.p2_return,color ='orange',label='p2_return')
plt.plot(date,cum_ret.p3_return,color ='blue',label='p3_return')
plt.plot(date,cum_ret.p4_return,color ='purple',label='p4_return')
plt.plot(date,cum_ret.p5_return,color ='green',label='p5_return')
plt.plot(date,cum_ret.twn50_return,color = 'black',label='twn50_return')
plt.legend(fontsize = 15)
結果顯示,排序最高分的組別(P1),累積報酬率超過其他組別,甚至超過台灣50指數的 5倍🚀。
Ratio = pd.DataFrame()
for col in cum_ret.columns[1:]:
##年化報酬率
cagr = (cum_ret[col].values[-1]) ** (1/len(cum_ret)) -1
##年化標準差
std = return_[col][:len(return_)-1].astype(float).std()
##Sharpe Ratio(假設無風險利率為1%)
sharpe_ratio = (cagr-0.01)/(std*0.01)
##最大回撤
roll_max = cum_ret[col].cummax()
monthly_dd =cum_ret[col]/roll_max - 1.0
max_dd = monthly_dd.cummin()
##表格
ratio = np.reshape(np.round(np.array([100*cagr, std, sharpe_ratio, 100*max_dd.values[-1]]),2),(1,4))
Ratio = Ratio.append(pd.DataFrame(index=[col],
columns=['年化報酬率(%)', '年化標準差(%)', '夏普比率', '期間最大回撤(%)'],
data = ratio))
Ratio.T
結果顯示,排序最高分的組別(P1),年化報酬率🚀、夏普值🚀和期間最大回撤(%)🚀表現皆優於其他組別,惟波動度特別大。
2020年第四季的財報算出最新一期的最高分組別 P1
stk_ranking = result[result['財報年月']== '2020-12-01'].sort_values(by='score',ascending=False).reset_index(drop=True)
first_group = round(len(stk_ranking)*(0.2))
stk_ranking = stk_ranking.loc[0:first_group]['公司代碼'].tolist()
# 回台灣 50成分股查詢 P1組合的名稱
stk_info['stk_num'] = stk_info['成份股'].apply(lambda x: str(x).split(' ')[0])
stk_info['stk_cname'] = stk_info['成份股'].apply(lambda x: str(x).split(' ')[1])
stk_info['成份股'][stk_info['stk_num'].isin(stk_ranking)].to_list()
我們透過python實作了大師策略,再對其進行了回測,並透過表格及視覺化的方式呈現了結果。看到結果後是不是相當感動阿 😆😆~
本篇文章內容較多,值得讀者們細細品味與消化,葛拉漢真不愧是證券分析的開山祖師,根據我們回測的結果若自2000年開始投資最高分的組別,其累積報酬超過20倍❗️️️️ ❗️️️️
但要做出一個成功的回測,要考慮的因素還很多像是資料品質、資料長度、程式是否有BUG、交易成本是否忽略過多、若是使用基本面還會有資料時間軸等等的相關問題。上述這些問題只要有一個地方出錯都會造成回測的失真,如果還依據這個結果將資金丟入市場,最嚴重就是造成虧損,所以一定要再三注意❗️️️️ ❗️️️️ ❗️️️️ ❗️️️️ ❗️️️️
我們之後也會再分享更多量化投資相關的文章,如果讀者們有甚麼想要回測策略都歡迎在下方留言,我們會挑選合適的主題來進行撰寫喔~最後,如果喜歡本篇文章的內容請幫我們點擊下方圖示👏 ,給予我們更多支持與鼓勵,有任何的問題都歡迎在下方留言/來信,我們會盡快回覆大家👍👍
想要一個"穩定""品質高""資料長度長"的資料源該怎麼辦呢?TEJ API就是你最好的選擇!!
電子報訂閱