您现在的位置是:首页 >技术交流 >Python量化实战(二):回测-银行低估值选股策略网站首页技术交流
Python量化实战(二):回测-银行低估值选股策略
Step1:写独立模块utils
首先,写一个独立的python文件,汇总一些常用的函数,实现数据分组、仓位/权重计算等功能。
废话不说,上代码。
# 导入必要的模块
import pandas as pd
import numpy as np
from scipy.stats import rankdata
然后,写一个函数,根据某条件(如:市盈率)对传入的数据进行分组。
# 定义一个函数,根据市盈率PE Ratio对原始数据进行分组
def group_1d(arr, nog):
"""
根据某条件对一维数据进行分组
:param arr: 分组前的原始数据
:param nog: 组数,要把原始数据分成几组
:return: 以Numpy数组形式返回分组的结果
"""
# 创建一个全零数组,用于存放最终的分组结果
results = np.zeros_like(arr, dtype=int)
# 接受一个数组,返回该数组中元素对应的顺序编号,如果元素相同,则返回该元素对应顺序的平均值
ranks = rankdata(arr)
total_num = len(arr) # 数据总量
nan_num = np.isnan(arr).sum() # nan数据量
no_nan_num = total_num - nan_num # 非空数据量
# 下面计算每个分组中有多少个元素,考虑到实际意义,在此四舍五入
d = round(no_nan_num / nog)
# 下面开始分组
# 除了最后一组,其余分组中包含的全部元素个数是相同的
for i in range(1, nog):
rank1 = 1 + (i - 1) * d # 当前分组开始元素对应的顺序编号值
rank2 = rank1 + d # 当前分组结束元素对应的顺序编号值
results[(ranks >= rank1) & (ranks < rank2)] = i # 写入对应元素的分组结果
# 最后一组的分组情况
rank1 = 1 + (nog - 1) * d
rank2 = no_nan_num
results[(ranks >= rank1) & (ranks < rank2)] = nog # 剩下的全部是最后一组
return results # 返回分组结果
再写一个函数,对二维数据分组。
def grouping(arr2d, nog):
"""
对二维数据进行分组
:arr2d:传入需要分组的二维数据集
:nog:组数
:return:返回分组的结果
"""
results = np.zeros_like(arr2d, dtype=int) # 创建一个全零数组,用于保存分组的结果
# 分组
for i in range(len(results)): # 对第i行数据进行分组
results[i] = group_1d(arr2d[i], nog) # 调用函数,对某一行数据进行分组
return results # 返回二维数据的分组结果
再写一个函数,根据分组结果计算权重。
def getting_weight(group, group_id):
"""
根据分组结果和组号生成该组股票对应的仓位权重,核心思路是等权重持有pe最低的股票
:param group:传入分组之后的二维数组
:param group_id:需要创建仓位的分组组号,在此仅仅对pe最低的一组创建仓位
:return:返回仓位权重
"""
w = (group == group_id).astype(int) # 得到仅含有0和1的二维数组w
w_sum = w.sum(axis=1) # 1代表按行求和
w_sum[w_sum == 0] = 1 # 为避免除零错误,在此人为将全零行权重和设置为1
w = w / w_sum.reshape((-1, 1)) # 重构权重矩阵
return w
好了,这个模块就写好了,下面开始写回测部分。
Step 2:策略回测
首先,导入回测需要的各种模块.
import utils
import config
import pandas as pd
import numpy as np
from datetime import datetime
import matplotlib.pyplot as plt
import pyquantfin.nav_calc as cnav
plt.rcParams['font.sans-serif'] = ['SimHei'] # 解决中文显示问题
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
一、导入并整理数据
bank_stock_daily = pd.read_csv(config.input_data_path + 'stock_data\bank_stock_daily.csv', encoding='gbk')
bank_stock_daily_basic = pd.read_csv(config.input_data_path + 'stock_data\bank_stock_daily_basic_monthly.csv', encoding='gbk')
bank_stock_basic = pd.read_csv(config.input_data_path + 'stock_data\bank_stock_basic_infos.csv', encoding='gbk')
bank_index_daily = pd.read_csv(config.input_data_path + 'index_data\bank_index_daily.csv', encoding='gbk')
接下来,先查看一下刚刚导入的日线数据。
一是银行股日线行情数据
bank_stock_daily.head()
二是银行股基本面指标数据
bank_stock_daily_basic.head()
三是银行股基本信息
bank_stock_basic.head()
四是指数收盘价数据
bank_index_daily.head()
然后把基本面数据交易日期列的数据类型转换一下,和其他数据表保持一致,便于后续操作。
bank_stock_daily_basic['trade_date'] = bank_stock_daily_basic['trade_date'].apply(lambda x: datetime.strptime(x, '%Y/%m/%d').strftime('%Y%m%d'))
下面,转换银行股日线行情数据、基本面数据和指数收盘价数据交易日期数据类型。
bank_stock_daily.trade_date = bank_stock_daily.trade_date.astype('str')
bank_stock_daily_basic.trade_date = bank_stock_daily_basic.trade_date.astype('str')
bank_index_daily.trade_date = bank_index_daily.trade_date.astype('str')
二、构建股票池,计算投资权重
构建股票池,拿到一个一维的Numpy数组
bank_stock_pool = bank_stock_daily_basic.ts_code.unique()
获取唯一的交易日期数据
trade_dates = np.sort(bank_stock_daily.trade_date.unique())
然后,创建一个空的数据集,保存收盘价数据
df_prz = pd.DataFrame(index=trade_dates, columns=bank_stock_pool)
获取每个月最后一天的交易日期,作为再平衡日
trade_dates_pe = np.sort(bank_stock_daily_basic.trade_date.unique())
再创造一个空的数据集,存放股票每个月最后一个交易日的pe值
df_pe = pd.DataFrame(index=trade_dates_pe, columns=bank_stock_pool)
向这两个df中填充数据
for stock_code in bank_stock_pool:
df1 = bank_stock_daily[bank_stock_daily['ts_code'] == stock_code] # 获取对应个股日线行情数据
df_prz[stock_code] = pd.Series(df1['closeAdj'].values, index=df1['trade_date'].values) # 将个股后复权收盘价填入对应位置
df2 = bank_stock_daily_basic[bank_stock_daily_basic['ts_code'] == stock_code] # 获取对应个股月底的数据
df_pe[stock_code] = pd.Series(df2['pe_ttm'].values, index=df2['trade_date'].values) # 将pe值填入对应位置
现在查看一下这两个数据集
df_prz
df_pe
将行情开始日设置为pe值产生的首个交易日
df_prz = df_prz[df_pe.index[0]:]
对pe数据集向前填充,对于未上市的情况,仍nan;对于停盘的情况,使用前一个pe。
df_pe = df_pe.fillna(method='ffill')
然后根据pe进行分组
pe_group = utils.grouping(df_pe.values, 3) # 分成三组
pe_group
三、策略回验
bk_rst = {} # 创建一个空字典,用于存放策略回测后的结果
for i, j in zip(['low_pe', 'mid_pe', 'high_pe'], [1, 2, 3]):
# zip函数用于产生两个可迭代对象的一对一元组
print(f"Backtesting the {i} group...", end='') # 打印回测结果提示信息
weight = utils.geting_weight(group=pe_group, group_id=j) # 根据分组的结果和对应的分组序号,产生该分组对应的权重,得到一个二维数组
weight_df = pd.DataFrame(weight, index=df_pe.index, columns=df_pe.columns) # 将权重二维数组转换成DataFrame形式
bk_rst[i] = cnav.multi_assets(df_prz, weight_df, weight_df.index.values, fee=2.5/10000) # 根据历史行情和投资组合权重对策略进行回测
"""
multi_assets:
参数一:传入行情数据df;
参数二:传入投资组织标的权重df;
参数三:再平衡日(在该项目中指定为每月的最后一天);
参数四:交易费率
"""
print("done.")
回测结果展示:
nav = {} # 创建一个空字典,用于存放每个分组回测的净值结果
for i, df in bk_rst.items():
nav[i] = df.nav
# 将nav回测结果转换为df
nav_df = pd.DataFrame(nav)
nav_df
接下来把指数加入到nav数据中作为业绩比较基准,同时作数据归一化处理
bank_index_daily
bank_index_daily.set_index('trade_date', inplace=True)
nav_df['Index_Bank'] = bank_index_daily['399986.CSI'] # 将中证银行指数和沪深300指数存入结果df,作为业绩比较基准
nav_df['Index_HS300'] = bank_index_daily['000300.SH']
nav_df = nav_df / nav_df.iloc[0] # 数据归一化处理
下面以图片呈现回测结果
nav_df.plot()
plt.xticks(rotation=45) # 横轴旋转45度,解决数据重叠显示问题
plt.savefig(config.output_data_path + '银行板块低估值选股策略回测结果.png')
plt.show()
四、策略超额收益的稳健性分析
nav_excess = nav_df.low_pe / nav_df.high_pe
nav_excess.plot()
plt.xticks(rotation=45) # 横轴旋转45度,解决数据重叠显示问题
# 保存策略回测结果
plt.savefig(config.output_data_path + '策略超额收益稳健性分析.png')
plt.show()
由上图可知,该策略在2017年遭遇了大幅回调,有必要单独拿2017年的数据看一下,找一找究竟是哪只股票引起的
nav_2017 = nav_df['20170101': ][['low_pe', 'high_pe']]
nav_2017 = nav_2017 / nav_2017.iloc[0]
nav_2017.plot()
由上图不难看出,2017年high_pe组净值先大幅增长,使得比值下降,出现剧烈波动。因此,接下来,拿出high_pe组数据看一下是由哪只股票引起的?
weight = utils.geting_weight(pe_group, 3)
weight_df = pd.DataFrame(weight, index=df_pe.index, columns=df_pe.columns)
weight_df = weight_df['20170101': '20170630']
weight_df # 获得high_pe组2017上半年个股投资权重
拿到high_pe组2017上半年的投资权重
下面找一下2017上半年的持仓个股
weight_sum = weight_df.sum()
stock_list_2017H1 = list(weight_sum[weight_sum > 0].index)
stock_list_2017H1 # 获得2017上半年持仓个股
找到这几只股票
回测这几只股票的表现,看一看究竟是由哪只股票引起的波动?
df_prz['20170101': '20170630'][stock_list_2017H1].plot()
由上图可知,波动是由于2017.01.24张家港行(002839.SZ)引起的,受我国特殊的股票发行定价机制,新股上市发行一般会出现连续涨停,张家港行在上市后连续6个涨停板,带动high_pe组净值异动,从而使得low_pe / high_pe比值下降。
Summary:
(1)在实际策略回测过程中,应该考虑到新股上市影响,一般新股上市后1个月不作为投资标的;
(2)本项目并未考虑到交易滑点、涨跌停板等交易限制,在实际回测过程中,这些都是要考虑到的,必须要模拟最真实的实盘环境;
(3)从最终的结果来看,根据pe选取低估值的银行股进行长线投资是可行的,但就量化投资而言,low_pe能否作为一个有效因子有待商榷,可能长期来看是有效的,但在短期也许会有不一样的表现,值得进一步研究。
好了,这个策略的回测就先写到这儿吧,最后再次感谢Tushare(570231)的数据支持,喜欢的欢迎留言+关注!后续也会更一些其他策略啦!