您现在的位置是:首页 >技术交流 >Python量化实战(二):回测-银行低估值选股策略网站首页技术交流

Python量化实战(二):回测-银行低估值选股策略

Reginald0207 2024-08-22 00:01:04
简介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)的数据支持,喜欢的欢迎留言+关注!后续也会更一些其他策略啦!

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。