均值回归策略(二)—代码实现
在上一篇均值回归策略(一)中我们简单的介绍了均值回归策略的一些基础知识,这里我们将通过代码的形式来实现均值回归策略的指标计算及买卖点的识别,以及最后收益率和持有天数的计算。
一、数据提取
本文用到的数据主要是从优矿平台下载的沪深300从2013年1月1日至2024年5月31日的前复权的所有股票的价格信息。删除了2013年之后上市的股票,确保股价数据的完整性。优矿平台数据调取代码如下(需在优矿平台运行该代码):
这里就不对代码进行详细解释了,有兴趣的同学,可以自己研究一下哈
import re
import pandas as pd
hushen300 = DataAPI.IdxConsGet(secID=u"",ticker=u"000300",isNew=u"",intoDate=u"20240531",field=u"",pandas="1")
hushen300_name = hushen300["consShortName"]
hushen300_id_org = hushen300["consID"]
hushen300_id = hushen300["consID"].str.extract(r'(\d+)', expand=False)
hushen300_price = pd.DataFrame()
for m in range(0,300):
object_time = datetime(2013, 1, 1, 0, 0, 0)
ttm = DataAPI.EquGet(secID=hushen300_id_org[m],ticker=u"",equTypeCD=u"A",listStatusCD=u"",exchangeCD="",ListSectorCD=u"",field=u"secID,listDate,secShortName",pandas="1")
ttm['listDate'] = pd.to_datetime(ttm['listDate'])
mubiaoshijian=datetime(2013, 1, 1, 0, 0, 0)
if ttm["listDate"][0] < mubiaoshijian:#从沪深300成分股里面筛选出2013年之前上市的股票
stock_price = DataAPI.MktEqudAdjGet(secID=u"",ticker=hushen300_id[m],tradeDate=u"",beginDate=u"20130101",endDate=u"",isOpen="",field=u"tradeDate,closePrice",pandas="1")#股价是前复权股价
stock_price = stock_price.set_index("tradeDate")
stock_price = stock_price.rename(columns={"closePrice":hushen300_name[m]})
if m==0:
hushen300_price=stock_price
else:
hushen300_price = hushen300_price.join(stock_price)
hushen300_price = hushen300_price.dropna() #删除带有空值的行
hushen300_price.to_csv('hushen300_price.csv')
二、数据读取
数据下载到本地,使用python读取数据,以便后续数据的分析使用。具体代码如下:
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from sklearn.linear_model import LinearRegression
#一、数据提取
hushen300_price = pd.read_csv('hushen300_price.csv',parse_dates=['tradeDate'])
#读取数据,parse_dates可以将时间格式的信息转化为时间
hushen300_price.set_index('tradeDate', inplace=True)
#设置数据index为交易时间(tradeDate)
hushen300_price_train = hushen300_price.loc[pd.Timestamp("2013-01-04"):pd.Timestamp("2023-01-04")]
#23年之前的数据,用于规律查找
hushen300_price_test = hushen300_price.loc[pd.Timestamp("2023-01-05"):]
#23年之后的数据,用于结果的测试
虽然区分了实验数据和测试数据,但文章最后并没有使用测试数据,仅仅统计了实验数据的一些收益表现。
三、指标计算
1、均值、标准差和变异系数
变异系数( C V ) = 标准差( σ ) 均值 ( μ ) 变异系数(CV)=\frac{标准差(\sigma)}{均值(\mu)} 变异系数(CV)=均值(μ)标准差(σ)
查看数据的波动性,可以选择出波动性大的股票。波动性越大,均值回归策略表现有可能越好。
代码实现如下:
std_train = hushen300_price_train.std()#标准差
mean_train = hushen300_price_train.mean()#均值
cv_train = std_train/mean_train#变异系数
2、计算线性回归后的斜率
y = a + k x y=a+kx y=a+kx 字母 k k k就代表斜率,斜率越接近0,说明股价趋势越平缓,越符合均值回归策略的前提。
代码实现如下:
k_train={}
for col in hushen300_price_train.columns:
X_train = pd.DataFrame(range(1,2433), columns=['Number'])#变成df格式
# X_train = list(range(1,2433))#生成一个1至2432的连续的整数,用于当做线性拟合的x
Y_train = hushen300_price_train[col]
model = LinearRegression()
model.fit(X_train, Y_train)
k_train[col] = model.coef_#斜率
indicator_df = pd.DataFrame({'std_train': std_train, 'mean_train': mean_train, 'cv_train': cv_train,'k_train': k_train}) #将均值、标准差和斜率多个指标合并成一个df中,便于后续分析
3、新指标创建
变异系数越大越好,斜率越小越好,并且斜率要为正值(说明股价是增长的),因此可以构造一个变异系数除以斜率的指标,该指标越大,该股票越符合均值回归策略的前提。
代码实现如下:
indicator_df['cv_train/k_train'] = indicator_df['cv_train'] / (indicator_df['k_train']*100) #k_train*100避免分母过小
四、股票池筛选
前面我们创建了一个新指标,可以对新指标进行降序排列,选出前三的股票,作为均值分析的目标股票。
代码实现如下:
target_df = indicator_df[indicator_df['cv_train/k_train'] >= 0]
target_df = target_df.sort_values(by='cv_train/k_train', ascending=False)#进行降序排列
first_indices = target_df.head(3).index#提取前3行的股票名称
目标股票筛选出来后,我们也可以看一下后3的股票,以及中间股票的走势,以此来辅助判断前3个股票选取的可行性。
代码实现如下:
last_indices = target_df.tail(3).index#提取后3行的股票名称
# 下面提取中间三行的股票信息
num_rows = len(target_df)
# 确定中间三行的起始和结束索引
# 注意:这里我们假设如果行数不是3的倍数,我们取靠近中间的三行
start_index = num_rows // 2 - 1 # 向上取整后减1,确保在中间偏前的位置开始
end_index = start_index + 3 # 从起始索引开始取三行
middel_indices = target_df.index[start_index:end_index]#提取中间3行的股票名称
# 画趋势图,查看前复权收盘价走势,对比前面几个股票和后面几个股票趋势的差异
plt.figure(figsize=(20, 20)) # 设置画布大小
# 遍历2行3列的子图
for i in range(3):
for j in range(3):
# 使用subplot创建子图,指定行数、列数、以及当前子图的索引(从1开始)
plt.subplot(3, 3, i * 3 + j + 1) # i*3 + j + 1 是计算当前子图的索引
# 在子图上绘制图形
if i==0:
stock_name=first_indices[j]
plt.ylabel('First')
elif i==2:
stock_name=last_indices[j]
plt.ylabel('Last')
else:
stock_name=middel_indices[j]
plt.ylabel('Middle')
plt.plot(hushen300_price_train[stock_name])
# 设置子图的标题、x/y轴标签等(可选)
# plt.title(f'Subplot {i*3 + j + 1}')
plt.xlabel('Date')
# plt.ylabel('Price')
# 调整子图之间的间距和画布边缘的间距
plt.tight_layout()
# 显示图表
plt.show()
股价走势
股价走势图如上,第一行为前3的股价走势,中间一行为中间的股价走势,最后一行为后3的股价走势。从上图可以看出,前3的股价确实是波动比较大,并且增长趋势不显著的股票。
五、股票买卖点的判断
这里我们主要是通过布林带的方式,来判断股价的买卖点,当股价低于布林带底线时买入股票,当股价高于布林带上线时卖出股票。
代码实现如下:
T = 30#移动平均的天数
mu = 1#卖出点的标准差倍数
d = 1#买入点的标准差倍数
stock_name = "方正证券"#以该股票为例进行
stock_info = hushen300_price_train[stock_name].to_frame()#转化成df格式
stock_info['moving_avg'] = stock_info[stock_name].rolling(window=T).mean()
stock_info['moving_std'] = stock_info[stock_name].rolling(window=T).std()#这里计算的是样本标准差
stock_info=stock_info.dropna() #删除带有空值的行
stock_info["label"] = np.where(stock_info[stock_name]<(stock_info['moving_avg']-d*stock_info['moving_std']), 1,
np.where(stock_info[stock_name]>(stock_info['moving_avg']+mu*stock_info['moving_std']),-1,0))
#1代表可以买入,-1代表可以卖出,0代表持有
上述代码,我们以方正证券为例,并使用30天的股价平均值作为均值,1倍标准偏差作为布林带的上下线。同时新建一个“label”列,用于买卖点识别的标记。当label为1时,代表当前股价低于均值减去1倍标准差,股价有可能会回升,需要买入。当label为-1时,代表当前股价高于均值加上1倍标准差,股价可能会下降,需要卖出。
当我们查看label的标签值时,我们会发现有许多连续的1或相差不远的行会被重新标记为1。-1的标签也是同样情况。例如标签为[0,0,1,0,1,1,0,0,-1],这个例子就会出现多个买入点,第3、5、6,都是一个买入点。实际中,我们的买入点为第3个元素,其他都是非买入点。为了避免这个情况,我们需要重新标记一下,首先1要先出现,并且是最早出现的那个值,此后再查找-1,并且也要是最早出现的那个值。这样便寻找到了一组买卖的交易点。后面的数据,则依次进行,直至取出全部买卖点。
代码实现如下:
#依次查找1及在1后面的-1,并且循环进行
stock_info["marks"] = 0
idx_first_one = stock_info[stock_info['label'] == 1].index.min()#获取第一个1的index
if idx_first_one is not pd.NaT:#判断是否存在1,如果存在则标记
stock_info.at[idx_first_one, 'marks'] = 1 # 标记第一个1
next_index_actual = stock_info.index[stock_info.index.searchsorted(idx_first_one + pd.Timedelta(days=1), side='left')]
for myindex in stock_info.loc[next_index_actual:].index:#next_index_actual=idx_first_one+1
prev_index_actual = stock_info.index[stock_info.index.searchsorted(myindex - pd.Timedelta(days=1), side='right') - 1]
if stock_info["marks"].loc[:prev_index_actual].sum() ==1 : #prev_index_actual=myindex-1
id_minone = stock_info.loc[myindex:][stock_info['label'] == -1].index.min()#获取第一个-1的index
if id_minone is not pd.NaT:
stock_info.at[id_minone, 'marks'] = -1 # 标记-1
elif stock_info["marks"].loc[:prev_index_actual].sum() ==0 :
id_one = stock_info.loc[myindex:][stock_info['label'] == 1].index.min()#获取1的index
if id_one is not pd.NaT:
stock_info.at[id_one, 'marks'] = 1 # 标记1
经过上述代码处理后,我们便得到了一列,1和-1交替出现的买卖点标签。
六、计算股票统计量
有了买卖点标签后,我们需要计算该策略的平均收益率以及平均持有天数,从而判断该策略的可行性。
代码实现如下:
stock_rets = []
stock_days = []
minone = stock_info[stock_info['marks'] == -1].index.min()#查找第一个-1
while minone is not pd.NaT:
preone = stock_info.loc[:minone][stock_info['marks'] == 1].index.max()#获取-1之前的1
if minone is not pd.NaT and preone is not pd.NaT:
ret_tep = stock_info[stock_name][stock_info.index==minone][0]/stock_info[stock_name][stock_info.index==preone][0]-1
day_tep = minone-preone
day_tep = day_tep.days
stock_rets.append(ret_tep)
stock_days.append(day_tep)
next_index_actual = stock_info.index[stock_info.index.searchsorted(minone + pd.Timedelta(days=1), side='left')]
minone = stock_info.loc[next_index_actual:][stock_info['marks'] == -1].index.min()#获取后面的-1
stock_trade = pd.DataFrame({'rets': stock_rets, 'hold_days': stock_days})
print("平均收益率为:"+str(stock_trade["rets"].mean())," 平均持有天数为:"+str(stock_trade["hold_days"].mean()))
输出结果如下:
平均收益率为:0.01795124738903331 平均持有天数为:89.57142857142857
从上述结果来看,该策略并不是很理想,收益率偏低。但该策略也有一些超参数,比如移动日平均的天数T,买卖点的标准差偏离倍数mu,d,甚至是股票的选择方式,该方法或许没有选出合适的股票等等。通过上述参数的调节,或许可以得到一个比较优秀的策略。
—End—
参考资料