目录
三一投资管理公司(Trinity Investment Management) 创立于1974年,原本只提供投资机构客户投资研究建议,自1980年开始为客户管理投资组合,1999年成为美国最大的共同基金及投资管理集团之一的欧本海默基金公司(Oppenheimer Funds, Inc.)的一员。
至2000年第一季底为止,三一投资管理公司为客户管理的资产总额达42亿美元,其投资哲学以价值导向为主,并认为建立成功的价值型投资组合,只需要以单纯而简化的概念及系统性的设计即可,只是大部份的基金经理人不承认而已。
根据三一公司多年的研究,综合本益比、股价帐面价值比及股利收益率三项要素建立投资组合,在1980至1994年的15年间,年平均投资报酬率达20.1%,每年战胜S&P500指数达6.8%;三一投资管理公司的总裁史丹佛凯德伍德(Stanford Calderwood)特别强调股利收益对价值型投资者的重要性,并认为成长型基金经理人所使用的预测性资料,对投资组合的绩效表现没有价值。
本篇文章属于实战策略,因此不论是篇幅还是难度上都有一定的程度,但请大家不要慌张,我们在文末都有提供完整程式码与联系方式,若有看不懂或不会的都欢迎询问我们~
1. 1980至1994年间S&P500指数中,以本益比最低的30%股票为投资组合(每季更新替换),年平均报酬率为17.5%,而同期S&P500指数的报酬率13.3%
2. 1980至1994年间,以股价帐面价值比最低的30%的股票为投资组合(每季更新替换),年平均报酬率为18.1%,高出S&P500指数报酬率4.8%。
3. 以股利收益率最高的30%股票为投资组合,年平均报酬率达18.3%,高出S&P500指数报酬率5%。
1. 将各公司当天的的本益比由低到高进行排序,取最低的前30%公司
2. 将各公司当天的股价帐面价值比由低到高进行排序,取最低的前30%公司
3. 将各公司当天的股利收益率由高到低进行排序,取最高的前30%公司
4. 最后我们取这三个条件的联集,也就是任一个标准符合即可,而之所以不取交集的原因,是因为这三者同时都要满足,就是一间本益比偏低、PB比偏低但股利率却偏高的公司,很有可能有长期经营成长的问题,且高股利率可能是在掩饰股价长期可能走低得市场预期。
那讲解完之后,我们就开始进入实做过程吧!!
Let’s Coding!!
import tejapi import pandas as pd import numpy as np tejapi.ApiConfig.api_key = "your key" tejapi.ApiConfig.ignoretz = True import datetime import matplotlib.pyplot as plt from datetime import datetime, timedelta from functools import reduce
首先自 TW50.csv 中撷取台湾50指数成分股资料,但成分股的资料格式为(1101 台泥),而 tejapi.get 函数中 coid 是专门用来控制股票代码的参数,coid 仅接受数字,程式码第二行的主要功能在将成分股当中的股票代码抽离。
接著就是透过TWN/AIM1A以及TWN/APRCM两个资料库分别拿到财务资料以及股价资料,并透过pandas里面的resample函数将日资料更换为季资料。
stk_info = pd.read_csv('TW50.csv',engine='python') stk_nums = stk_info['成份股'].apply(lambda x: str(x).split(' ')[0])
strategy_cols = ['公司代码','财报年月','当季季底P/E', '当季季底P/B', '股利殖利率']
## 2008年到2020年 # 捞取财务资料及股价资料 df_foundamental = pd.DataFrame() q_df_stock = pd.DataFrame() df_stock_Qrt = pd.DataFrame()
for stk in stk_nums:
df_foundamental = df_foundamental.append(tejapi.get('TWN/AIM1A' ,coid=stk ,mdate={'gte':'2008-01-01', 'lte':'2020-12-31'} ,paginate=True,chinese_column_name=True ,opts={'pivot':True} )).reset_index(drop=True) df_stock = tejapi.get('TWN/APRCM' ,coid=stk ,mdate={'gte':'2008-01-01', 'lte':'2020-12-31'} ,paginate=True,chinese_column_name=True)
q_df_stock = q_df_stock.append(df_stock.resample('Q', on='年月').last().reset_index(drop=True))
这边我们除了用0050csv之外,我们提供另外一种方法给大家一样地可以将0050成份股给挑选出来,也就是靠我们神通广大甚么都有的TEJ API资料库啦!我们透过TWN/AIDXS资料库,可以直接找到0050在当下时间点的成分股有哪些,然后资料经过整理后可以一样地将ticks找出来。
df_indexcomp = tejapi.get('TWN/AIDXS', coid= 'TWN50', opts={'columns':['coid', 'mdate', 'key3']}, mdate='gte':'2021/02/25','lte':'2021/02/25'},paginate=True)
df_indexcomp['stk'] = [df_indexcomp.iloc[i,2].split(' ')[0] for i in range(df_indexcomp.shape[0])]
df_indexcomp['name'] = [df_indexcomp.iloc[i,2].split(' ')[1] for i in range(df_indexcomp.shape[0])]
ticks = df_indexcomp.stk.unique()
TWN/AIDXS资料库内容
接下来因为我们要让两个资料表进行整并的动作,因此我们要将两个资料里面的栏位重新命名以及选择我们要的资料后透过pandas里面的merge函数进行合并。
##财务table整理 df_foundamental.rename(columns={'公司代码':'coid','财报年月':'日期'},inplace=True) df_foundamental = df_foundamental[['coid','日期','当季季底P/E', '当季季底P/B', '股利殖利率']]
##股价table整理 q_df_stock.rename(columns={'证券代码':'coid','年月':'日期'},inplace=True) q_df_stock = q_df_stock[['coid','日期','收盘价(元)_月']]
##股价并财报 new_df = pd.merge(q_df_stock, df_foundamental, on = ['日期', 'coid']) new_df.sort_values(by='日期').reset_index(drop=True)
整理完之后,就可以看到以下的资料表,0050成分股从2008年开始到2020年的季资料以及每季的PE、PB以及股利殖利率。
由于我们这边会需要三个主要的条件,分别是由低到高前30%PE、PB以及由高到低前30%的股利殖利率,因此我们透过先将日期从原先的2008–03–01更换为200803的方式,再使用pandas当中的groupby函数将同为200803的通通放在一块进行比较,将我们的筛选条件先呈现出来。
##时间资料格式更换 new_df['日期'] = pd.to_datetime(new_df['日期']) new_df['Month'] = new_df['日期'].apply(lambda x:datetime.strftime(x,'%Y%m'))
##将相同组别的资料透过groupby进行条件设置 df_pe_quantile = new_df.groupby('Month')['当季季底P/E'].quantile(0.3).reset_index().rename(columns={'当季季底P/E':'PE月quantile'}) df_pb_quantile = new_df.groupby('Month')['当季季底P/B'].quantile(0.3).reset_index().rename(columns={'当季季底P/B':'PB月quantile'}) df_div_quantile = new_df.groupby('Month')['股利殖利率'].quantile(0.7).reset_index().rename(columns={'股利殖利率':'Div月quantile'})
##多表资料合并 df_merged = reduce(lambda left,right: pd.merge(left,right,on=['Month'],how='outer'), [new_df,df_pe_quantile,df_pb_quantile,df_div_quantile])
整理完之后,可以看到以下资料除了原先的栏位之外,多了我们的筛选条件值。
接下来就可以透过pandas当中的栏位条件选择方法将符合我们上述三个条件的成分股选出来啰!
##条件筛选 df_filter = df_merged[ (df_merged['当季季底P/E'] < df_merged['PE月quantile'])| (df_merged['当季季底P/B'] < df_merged['PB月quantile'])| (df_merged['股利殖利率'] > df_merged['Div月quantile'])] .reset_index(drop =True)
选完后可以明显看到我们的资料从2419个变成1386个,接近一半的资料量不见了,那我们这边用两种方式呈现给大家,一种是按照coid排序,另一种是按照日期排序的表格。
(程式码以及参数解说部份可以主要参阅【实战应用(一)】里面的第四步噢,除了时间轴以及分组之外,剩余的参数以及逻辑概念皆相似)
由于该策略是会每季更换投资组合以及我们可以透过TEJ里面TWN/APRCD2资料库直接找到该公司之当季报酬率,所以我们透过投资组合每一季度一天的报酬率跟0050的季度报酬率进行比较。程式码如下:
*本次回测忽略交易成本
接著就可以看我们的return图表拉~
在这一步我们将简单的将季报酬以及累积报酬率分别以图形的方式呈现给大家。
##计算单季报酬以及累积报酬 cum_ret = ((return_[['port_return', 'twn50_return']].astype(float)*0.01)+1).cumprod() cum_ret['Date'] = return_['Date'] cum_ret = cum_ret[:len(cum_ret)-1] cum_ret = cum_ret[['Date','port_return','twn50_return']]
quarter_ret = return_[['port_return', 'twn50_return']].astype(float) quarter_ret['Date'] = return_['Date'] quarter_ret = quarter_ret[:len(quarter_ret)-1] quarter_ret = quarter_ret[['Date','port_return','twn50_return']]
每季报酬:
plt.style.use('seaborn') plt.figure(figsize=(10,5)) plt.xticks(rotation = 90) plt.title('master invest strategy - quarter return',fontsize = 20) date = quarter_ret['Date'] plt.plot(date,quarter_ret.port_return,color ='red',label='port_return') plt.plot(date,quarter_ret.twn50_return,color = 'black',label='twn50_return') plt.legend(fontsize = 15)
累积报酬:
plt.style.use('seaborn') plt.figure(figsize=(10,5)) plt.xticks(rotation = 90) plt.title('master invest strategy - cumulative return',fontsize = 20) date = cum_ret['Date'] plt.plot(date,cum_ret.port_return,color ='red',label='port_return') plt.plot(date,cum_ret.twn50_return,color = 'black',label='twn50_return') plt.legend(fontsize = 15)
结果可以看到走势相近,但表现可以说是非常的突出!!
绩效/统计指标:
Ratio = pd.DataFrame() for col in cum_ret.columns[1:]: ##年化报酬率 cagr = (cum_ret[col].values[-1]) ** (4/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
可以看到透过我们三一的价值型选股方式,在每一个指标项目上皆优于0050!!
2020–12月之成分股:
pf = df_filter[df_filter['日期']== '2020-12-01'].reset_index(drop=True)
# 回台湾 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(pf['coid'].tolist())].to_list()
我们透过python分别实作了数个大师策略,除了资料整理之外也对其进行了回测,并透过表格及视觉化的方式呈现了结果。希望大家能满意我们的产出 ~
但要做出一个成功的回测,要考虑的因素还很多像是资料品质、资料长度、程式是否有BUG、交易成本是否忽略过多、若是使用基本面还会有资料时间轴等等的相关问题。上述这些问题只要有一个地方出错都会造成回测的失真,如果还依据这个结果将资金丢入市场,最严重就是造成亏损,所以一定要再三注意❗️️️️ ❗️️️️
我们之后也会再分享更多量化投资相关的文章,如果读者们有甚么想要回测策略都欢迎在下方留言,我们会挑选合适的主题来进行撰写喔~最后,如果喜欢本篇文章的内容请帮我们点击下方图示 ,给予我们更多支持与鼓励,有任何的问题都欢迎在下方留言/来信,我们会尽快回复大家
有任何使用上的问题都欢迎与我们联系:联络资讯