證券分析之開山始祖一班傑明.葛拉漢的投資心法

photo by Unsplash

前言 

班傑明.葛拉漢是誰?

價值投資大師班傑明.葛拉漢的防禦型投資策略班傑明.葛拉漢是華爾街公認的證券分析之父,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指數成分股為例

台灣50指數成分股包含了市值排名前5️⃣0️⃣名的公司

建構步驟:

  • 第一步:匯入套件
  • 第二步:資料撈取
  • 第三步:建構大師策略
  • 第四步:策略績效回測
  • 第五步:大師策略 vs 台灣50指數

第一步:匯入套件

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年皆有盈餘的公司

  • np.where功能類似 if else 條件句,其好處在於提升程式碼的易讀性。
     np.where(z1[‘常續性稅後淨利’]>0,1,0):符合條件z1[‘稅前淨利’]>0的話輸出1,不符合則輸出0。
  • rolling 為 pandas 內建的功能,括弧內的數字可調整移動窗格的大小。
     rolling(5).sum()目的在計算近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個投資組合績效

  • 由於該策略是會每年更換投資組合的,當拿到當年度的年報資料時,會計算出當期的score,並根據 “score”由高至低排序,依五分位距分成5個投資組合。ex:前20%的股票為第一組、21%到40%的股票為第二組……

程式碼解說📚

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指數各期報酬率

第五步:大師策略 vs 台灣50指數

  • 💻計算大師策略和台灣50指數(benchmark)的累積報酬率💻

台灣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
大師策略+台灣50指數之累積報酬率
  • 📈資料視覺化📈
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)
大師策略 vs 台灣50指數
結果顯示,排序最高分的組別(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()
最新一期P1投資組合

結語

我們透過python實作了大師策略,再對其進行了回測,並透過表格及視覺化的方式呈現了結果。看到結果後是不是相當感動阿 😆😆~

本篇文章內容較多,值得讀者們細細品味與消化,葛拉漢真不愧是證券分析的開山祖師,根據我們回測的結果若自2000年開始投資最高分的組別,其累積報酬超過20倍❗️️️️ ❗️️️️

但要做出一個成功的回測要考慮的因素還很多像是資料品質、資料長度、程式是否有BUG、交易成本是否忽略過多、若是使用基本面還會有資料時間軸等等的相關問題。上述這些問題只要有一個地方出錯都會造成回測的失真,如果還依據這個結果將資金丟入市場,最嚴重就是造成虧損,所以一定要再三注意❗️️️️ ❗️️️️ ❗️️️️ ❗️️️️ ❗️️️️

我們之後也會再分享更多量化投資相關的文章,如果讀者們有甚麼想要回測策略都歡迎在下方留言,我們會挑選合適的主題來進行撰寫喔~最後,如果喜歡本篇文章的內容請幫我們點擊下方圖示👏 ,給予我們更多支持與鼓勵,有任何的問題都歡迎在下方留言/來信,我們會盡快回覆大家👍👍

想要一個"穩定""品質高""資料長度長"的資料源該怎麼辦呢?TEJ API就是你最好的選擇!!

完整程式碼

延伸閱讀

相關網站連結 

🌟有任何使用上的問題都歡迎與我們聯繫:聯絡資訊🌟

 
返回总览页