肯尼斯.費雪超級強勢選股法則

Photo by Wance Paleri on Unsplash

 

本文重點概要

文章難度:★★☆☆☆

使用負債比率、平均營收成長率、平均稅後淨利率、股價營收比和股價研發比等五項財務指標篩選強勢成長股

閱讀建議:本文資料來源皆來自TEJ API的公司財務資料庫,如果對Python的Dataframe操作不是很熟悉的讀者,可以先行閱讀【新手上路(六)】- 財務數據撈取中的程式碼,能更好理解本文中的操作流程。

前言

肯尼斯.費雪( kenneth L. Fisher)是費雪投資公司(Fisher Investments)的創辦人兼總裁,他的父親菲利普.費雪(Philip A. Fisher)是美國體質投資(Qualitative Investment)的代表;華倫.巴菲特(Warren Buffett)特別指出菲利普.費雪是他兩個主要投資靈感的啟發者之一【另一個當然是班傑明.葛拉漢(Benjamin Graham)】,由此可見肯尼斯.費雪的投資家學非常淵源。

肯尼斯.費雪認為一支完美的超級強勢股應有下列特性一,利用自有資金創造未來,長期平均成長率約為15%-20%。二,未來長期平均稅後獲利率高於5%。三,股價/營收比為0.75或更低。在選股方面,肯尼斯.費雪考慮兩個項因素:一,是股價/營收比,二,是股價/研發費用比,且有明確的準則。

1. 股價/營收比 (PSR)

a.避開PSR超過1.5的股票,任何PSR大於3的公司決不要碰。
b.積極尋找PSR低於0.75的超級公司。
c.任何超級公司的PSR漲到3至6之間時,要賣出持股。

2. 股價/研發費用比(PRR)

別買PRR高於15的公司,PRR低的超級公司多的是。
尋找PRR介於5至10的超級公司,PRR低於5倍的公司不多見。

因應時空背景的轉換,我們對上列條件進行了總結與調整

1. 負債比率低於35%
2. 最近5年平均營收成長率≧15%
3. 最近5年平均稅後淨利率≧5%
4. 股價營收比(PSR)≦1.5
5. 股價/研發費用比(PRR) ≦15

此外,由於該策略沒有明確的出場時機點,但由於台灣的公司月營收都於每個月初時公布,因此我們以一個月為周期進行篩選,同時對投資組合進行再平衡調整。

編輯環境及模組需求

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

import tejapi as tej
import pandas as pd
import numpy as np
import datetime
import matplotlib.pyplot as plt

tej.ApiConfig.api_key = 'Your Key'
plt.rcParams['font.sans-serif'] = ['Taipei Sans TC Beta']

資料庫使用

IFRS財務會計科目說明檔(TWN/AIACC)
調整股價(日)-除權息調整(TWN/APRCD1)
證券屬性資料表(TWN/ANPRCSTD)
IFRS以合併為主財務(單季)-全部產業Ⅳ(TWN/AIFINQ)
上市(櫃)月營收盈餘(TWN/ASALE)

資料導入

首先我們從「IFRS財務會計科目說明檔」搜尋所需的會計科目代號,再從「IFRS以合併為主財務(單季)-全部產業Ⅳ」資料庫下載所有上市櫃公司的負債比率、稅後淨利率和研發費用,至於營收成長率及股價營收比我們另外從「上市(櫃)月營收盈餘」資料庫抓取。

financial_name = tej.get('TWN/AIACC', chinese_column_name=True, paginate=True) #找尋所需的會計科目
financial_dict = ['負債比率', '稅後淨利率', '研發費用']
a = []
for i in financial_dict:
    a.append(financial_name[financial_name['中文全稱'].str.contains(i)]['會計科目'].mode().to_list())
a

使用pd.Series.str.contains再取眾數的方式得知R505為負債比率、R108為稅後淨利率、3368為研發費用。

會計科目代號
會計科目代號

使用公司代號及特定的會計科目代號下載所需的財務和月營收資料。

#上市櫃公司代號
code = tej.get('TWN/ANPRCSTD', mdate={'lt':'2022-10-14'}, chinese_column_name=True, paginate=True)
all_code = code[(code['證券種類名稱'] == '普通股') & (code['上市別'].isin(['TSE', 'OTC']))]['證券碼'].to_list()
#下載近十年的財務數據
financial_data = tej.get('TWN/AIFINQ', 
                            coid=all_code, 
                            mdate={'gt': '2012-01-01', 'lt':'2022-10-14'}, 
                            acc_code=a,
                            chinese_column_name=True, 
                            paginate=True)
financial_data = financial_data.reset_index(drop=True)
#下載近十年的月營收數據
revenue_data = tej.get('TWN/ASALE', 
                            coid=all_code, 
                            mdate={'gt': '2011-12-31', 'lt':'2022-10-14'}, 
                            opts={'columns': ['coid', 'mdate', 'd0001']},
                            chinese_column_name=True, 
                            paginate=True)
revenue_data.rename(columns={'年月': '年/月'}, inplace=True)
revenue_data['公司'] = revenue_data['公司'].astype(int)
revenue_data = revenue_data.astype({'年/月':'datetime64[ns]'})

股價由於資料量極大,因此每隔一個月下載一批資料再用pd.concat合併,最後串接成一筆三百萬的資料集。

m = pd.date_range('2011-12-31', '2022-11-01', freq='1M', inclusive='both').to_list()
price = pd.DataFrame()
for i in range(1, len(m)):
    price = pd.concat([price, tej.get('TWN/APRCD1', 
                                coid=all_code, 
                                mdate={'gt': m[i-1], 'lt':m[i]}, 
                                opts={'columns': ['coid', 'mdate',    'close_adj', 'mv']},
                                chinese_column_name=True, 
                                paginate=True)])
price = price.reset_index(drop=True)
price = price.rename(columns={'證券代碼':'公司', '年月日':'年/月'})
price['公司'] = price['公司'].astype(int)
price = price.astype({'年/月':'datetime64[ns]'})
2012~2022年上市櫃股價資料
2012~2022年上市櫃股價資料

進一步整理財務數據,將R505、R108、3368分別轉為中文的會計名稱,並使用pd.pivot_table進行格式轉置以方便之後進一步的合併。

financial_data1 = financial_data.copy()
financial_data1['數值'] = financial_data1['數值'].astype(float)
financial_data1['會計科目'] = financial_data1['會計科目'].map({'R505':'負債比率',    #會計科目改名
         'R108':'稅後淨利率',
         '3368':'研發費用'})
                                                    
financial_data1 = financial_data1.pivot_table(index=['公司', '年/月'], columns='會計科目', values='數值').reset_index()  #格式轉置
financial_data1['公司'] = financial_data1['公司'].astype(int)
financial_data1 = financial_data1.astype({'年/月':'datetime64[ns]'})
financial_data1 = financial_data1.fillna(0)
financial_data1
財報資料轉置
財報資料轉置

使用pd.merge_asof將財務數據、月營收和股價合併成每月為單位的資料集。

financial_data1 = pd.merge_asof(revenue_data.sort_values('年/月'), financial_data1.sort_values('年/月'), on='年/月', by='公司', direction='backward').sort_values(['公司', '年/月'])
financial_data1 = pd.merge_asof(financial_data1.sort_values('年/月'), price.sort_values('年/月'), on='年/月', by='公司', direction='nearest').sort_values(['公司', '年/月'])
financial_data1
月營收、股價資料集
月營收、股價資料集

計算策略所需的五年平均營收年成長率、股價營收比和股價研發比。

financial_data1['營收年成長率'] = financial_data1['單月營收(千元)'] / financial_data1['單月營收(千元)'].shift(12)
financial_data1['平均5年營收成長率'] = financial_data1.groupby('公司')['營收年成長率'].transform(lambda x: x.rolling(60).mean())
financial_data1['平均5年稅後淨利率'] = financial_data1.groupby('公司')['稅後淨利率'].transform(lambda x: x.rolling(60).mean())
financial_data1['近一年營收總合'] = financial_data1.groupby('公司')['單月營收(千元)'].transform(lambda x: x.rolling(12).sum()/1000)
financial_data1['股價營收比'] = financial_data1['市值(百萬元)'] / financial_data1['近一年營收總合']
financial_data1['近一年研發總合'] = financial_data1.groupby('公司')['研發費用'].transform(lambda x: x.rolling(12).sum()/1000)
financial_data1['股價研發比'] = financial_data1['市值(百萬元)'] / financial_data1['近一年研發總合']
financial_data2 = financial_data1[['公司', '年/月', '負債比率', '收盤價(元)', '平均5年營收成長率', '平均5年稅後淨利率', '股價營收比', '股價研發比']].reset_index(drop=True)
financial_data2
每月所需指標
每月所需指標

我們將篩選器組成一個function方便之後進一步的參數優化比較,並簡單計算年化報酬率、年化標準差和年化夏普指標。

def condition(df, n1, n2, n3, n4, n5):
    price_T = price.pivot_table(index='年/月', columns='公司', values='收盤價(元)')
target_stock = df[(df['負債比率'] < n1) & (df['平均5年營收成長率'] >= n2) & (df['平均5年稅後淨利率'] >= n3) & (df['股價營收比'] <= n4) & (df['股價研發比'] <= n5)]
target_Q = list(target_stock.groupby('年/月')[['公司', '年/月', '收盤價(元)']])
ret, daily_ret = [], []
    for i in range(len(target_Q)):
        daily_ret.append(price_T.loc[target_Q[i][1].iloc[0]['年/月']:target_Q[i][1].iloc[0]['年/月']+pd.DateOffset(months=1), target_Q[i][1]['公司'].to_list()])
        ret.append(price_T.loc[target_Q[i][1].iloc[0]['年/月']:target_Q[i][1].iloc[0]['年/月']+pd.DateOffset(months=1), target_Q[i][1]['公司'].to_list()].pct_change().mean(axis=1))
ret_table = pd.concat(ret)
ret_table = pd.DataFrame(columns=['投組日報酬'], data=ret_table).dropna()
ret_table['投組累積報酬'] = (ret_table['投組日報酬'] +1).cumprod()
ret_table.reset_index(inplace=True)
print('年化報酬率:',"%6.3f" % (ret_table['投組日報酬'].mean()*252))
    print('年化標準差:',"%6.3f" % (ret_table['投組日報酬'].std()*np.sqrt(252)))
    print('年化夏普:',"%6.4f" % (ret_table['投組日報酬'].mean()*252 / (ret_table['投組日報酬'].std()*np.sqrt(252))))
return ret_table, target_stock[['公司', '年/月', '收盤價(元)']], daily_ret

設定(n1)負債比率35%、(n2)近5年平均營收成長率≧15%、(n3)近5年平均稅後淨利率≧5%、(n4)股價營收比(PSR)≦1.5、(n5)股價/研發費用比(PRR) ≦15,可以看出在三年的投資期間最終有75.7%的累計報酬率,年化報酬率達36%(投組實際持股天數少於三年時間,因此年化後數值較高),夏普比率也有1.3869的水準。註:無風險利率設為0%,無計入手續費與交易稅。

ret_table, target_stock, daily_ret = condition(financial_data1, 35, 15, 5, 1.5, 15)
ret_table
初始參數績效
初始參數績效

為了更直觀的審視策略績效,我們另外下載大盤報酬指數Y9997作為基準,來比較看看該策略是否能贏過單純投資大盤。

y9997 = tej.get('TWN/APRCD1', 
            coid='y9997', 
            mdate={'gt': '2017-02-01', 'lt':'2022-10-14'}, 
            opts={'columns': ['coid', 'mdate', 'close_adj']},
            chinese_column_name=True, 
            paginate=True)
y9997['基準日報酬'] = y9997['收盤價(元)'].pct_change()
y9997['基準累積報酬'] = (y9997['基準日報酬']+1).cumprod()
y9997.rename(columns={'年月日':'年/月'}, inplace=True)
y9997 = y9997.astype({'年/月':'datetime64[ns]'})

從圖中我們可以看出,初期找到不錯的標的可以快速超越大盤,但在2020年末股市V型反彈後就找不太優秀的成長股,主要原因可能是資金大量的湧入容易在短時間內將股價與營收表現脫節,導致股價營收比這項比率的設定,限縮了股市暴漲行情中能找到的標的;此外近一年無研發費用的公司將導致股價研發比呈現無限大,使其被股價研發比<15的條件所淘汰,最終在2020年末以後不再有適合的標的。

初始績效vs.大盤報酬指數
初始績效vs.大盤報酬指數

為了測試該策略能否更顯著贏過大盤,我們對之前的參數進一步放寬,特別是股價營收比這項限制,從1.5倍放寬到2倍的區間。可以看到放寬後雖然標準差有所增加,但隨之而來的報酬率更加顯著,讓整體的夏普比率從1.3869提高了47%上升到2.0448的水平。

ret_table2, target_stock2, daily_ret2 = condition(financial_data1, 35, 15, 5, 2, 15)
ret_table2
參數微調後績效
參數微調後績效

從下表能看出由於該策略所找尋的是成長型股票,對比同期間的Y9997基準有著至少兩倍的差距,標的特點在於爆發力高且注重未來展望,因此整體來說BETA值也比較大,所以在面對新冠肺炎的股市暴跌時,可以看出投組的回撤也比同期間的大盤來的嚴重。

參數微調後績效
參數微調後績效

而我們也能發現主要報酬皆來自2019年中至年末這段時間。從程式碼裡面去找這段期間,發現只有6538倉和這支個股,若拆開來單算它的報酬率就高達83%,對投組整體的貢獻十分驚人。

(pd.concat(daily_ret2)['2019-08-01': '2019-12-01'].pct_change()+1).cumprod()
2019年末投組單一標的累積報酬
2019年末投組單一標的累積報酬

結論

總體而言,肯尼斯.費雪這支策略核心在於以股價營收比(PSR)找尋被低估的成長型股票,而公司自身則需要有持續投入研發費用,並在低負債比率的情況下,提高營收成長率並維持一定水平的稅後淨利率。
不過原策略的PSR 0.75倍~1.5倍對台股而言過於嚴格,導致在股市大反彈期間PSR太低的限制會無法找到標的,而我們在放寬至PSR 2倍後也成功找到倉和的起漲點,將投組整個報酬率拉升不少。要注意的是該策略只有提供選股的邏輯,並沒有出場或停損的相關設定,若使用者要參照該策略組建投組,建議要多參考其他指標作為出場點和停損的依據。

此外本策略無計算手續費以及交易稅等成本,提醒使用者在參考本策略時須多加注意。

最後,還是要再次提醒本文所提及之標的僅供說明使用,不代表任何金融商品之推薦或建議。因此,若讀者對於建置策略、績效回測、研究實證等相關議題有興趣,歡迎選購 TEJ E Shop中的方案,具有齊全的資料庫,就能輕易的完成各種檢定。

完整程式碼

延伸閱讀

相關連結

返回總覽頁
Procesing