炒股的人通常都喜欢各种技术指标,来指定买卖策略,然而我们却经常看到股民们亏钱,难道是这些指标有问题吗?还是专家在忽悠股民朋友们?将以非常简单的均线指标为例,来挖掘其在一只股票上的绩效潜力。
实现流程的简单梳理
1.读取数据,了解数据的结构
2.对读取的数据进行清洗,使数据符合进一步分析
3.计算相应的股票日涨跌幅度
4.通过画图来展示:
a.基于回测资金曲线
b.股票k线图
c.参数优化的热力图
读取数据
注意事项:
pd.read_excel()有两个设置参数,分别是:
1.parse_dates:是指将指定列转化为时间类型格式,注意是列表形式指定列[“交易日期”]
2.index_cel:是指将指定列变为行索引index
# 导包
import pandas as pd
df = pd.read_excel("sz002115",parse_dates=["加油日期"],index_cel="交易日期")
为什么要设置"交易日期"列为datetime数据类型?
主要是后面的作图,直接传入时间数据为y轴,用str数据类型,有可能不会被识别.
最后我们查看一下数据的基本构造,看数据是否有空值,是否需要填充
df.info()
股票涨跌幅/涨跌停/复权计算
概念解析
1.股票涨跌幅:股票每天相对的昨日收盘价的涨跌幅
2.股票涨跌停价格:股票每一天的涨停价,跌停价
3.复权:把复权简单理解为就是排除送股,配股,分红等影响因素。
计算股票涨跌幅
df["涨跌幅"] = df["收盘价"]/df["前收盘价"]-1
注意事项
使用前收盘价,而非用开盘价,是为了避免因为送股,配股,分红等影响因素的股价不合理变化.
计算股票涨跌停
df["涨停价"] = df["前收盘价"]*1.1
df["跌停价"] = df["前收盘价"]*0.8
计算股票后复权价格
概念解析
前复权处理:股票最近一天价格不变,之前数据按照真实日涨跌幅做调整;
后复权处理:股票上市第一天价格不变,之后数据按照真实日涨跌幅做调整;(后复权类似于基金净值的统计)
计算股票资金曲线
所谓资金曲线,即把股票上市第一天的净值设为1,之后每天净值随真实日涨跌幅波动的曲线
1+涨跌幅 我们的到的是涨跌比例,我们把每一天的涨跌比例累程起来.我们的到一个1+涨跌幅的累乘数据,在做归一化
这个是比较难理解的,看图你应该就明白了!!!
df["资金曲线"] = (1+df["涨跌幅"].cumprod()
df.loc[:,"资金曲线"] = df["资金曲线"]/df["资金曲线"].iat[o] #进行数据归一化
iat只能访问单个元素
df[“xxx”].iat[x]
当前"xxx"这一列的第x值
loc与iloc
loc是标签名,不带i的是名字
iloc是位置号,带i的就是index位置
取数据都遵循:左行右列loc[左行,有列] iloc[左行,右列]
计算后复权价格
格式:
收盘后复权 = 第一天的当日收盘价(固定值) * 资金曲线的值 (一直在变动反应股票涨跌变化情况的值)
其他后复权,求出对于收盘价的涨跌幅后乘上收盘价后复权
涨跌幅 * 收盘价后复权
df["收盘价_后复权"] = df["资金曲线"] * df["收盘价"].iat[0] # 股票第一天的价格不变作为基准价
df["开盘价_后复权"] = df["开盘价"]/df["收盘价"]*df["收盘价_后复权"]
df["最高价_后复权"] = df["最高价"]/df["收盘价"]*df["收盘_后复权"]
df["最低价_后复权"] = df["最低价"]/df["收盘价"]*df["收盘_后复权"]
# 生成新的4个字段
df[["开盘价_后复权","最高价_后复权","最低价_后复权","收盘价_后复权"]]
绘制股票历史价格变动图、k线图
绘制股票历史价格变动图
df["资金曲线"].plot(figsize=(20,8),
grid=True,
alpha=0.8,
rot=45,
title="股票历史价格变动图")
绘制不复权k线图
# 导入pyecharts工具包,用pe作为简写
import pyecharts as pe
# 提取x轴数据,即交易日期,同时转换为字符串形式
x = df.index.astype('str')
# 提取y轴数据,即绘制股票k线所必须的4个价格字段
y = df[['开盘价','收盘价','最低价','最高价']].values
# 创建k线图对象,并设置大标题
kline = pe.Kline('不复权K线图_三维通信')
# 绘制图表
kline.add('日K', x, y, #图例名称,x轴数据,y轴数据
is_datazoom_show=True, #使用区域缩放组件
datazoom_range=[97,100], #设置区域缩放的范围,这里设置为末尾3%的k线数据
mark_point=['max','min'], #标记可视范围内开盘价最大值,最小值
is_xaxislabel_align = True, #设置x轴刻度与标签对齐
tooltip_trigger ='axis', #坐标轴触发弹窗提示
tooltip_axispointer_type ='shadow') #设置tooltip指示器
标记双均线策略信号
计算均线
df["MA50"] = df["收盘价_后复权"].rolling(50).mean()
df["MA200"] = df["收盘价_后复权"].rolling(200).mean()
注意事项
pandasSeries.rolling(n)是表示对一列的n个数据做滚动处理,rolling(3).mean()表示
对滚动的数据做均值计算,并且记录在最后一行
双均线策略信号标记
总结一句话就是:
对于短周期均线前天小后天大是上传买入信号,前天大后天小是下穿卖出信号
## 1位买入信号
df.loc[(df["MA50"]<df["MA200")&
(df["MA50"].shift(1)>df["MA200"].shift(1)),
"交易信号"]=1
# 0卖出信号
df.loc[(df["MA50"]>df["MA200"])&
(df["MA50"].shift(1)<df["MA200"].shift(1)),
"交易信号"]=0
# 去除掉空值
df["交易信号"].dropna()
注意事项:
df.loc[(条件1)&(条件2), ‘交易信号’] = 1找到了同时满足2个条件的所有行,并且索引到交易信号这一列,当这一列最开始不存在的时候,pandas会帮我们自动新建一列空值的交易信号,然后对这一列满足2个条件的所有行赋值为1,这就完成了买入信号的标记,卖出信号标记则同理
涨跌停信号屏蔽
当股票涨停是无法买入的,跌停也是无法卖出的,如果我们策略中有这样的情况,则需要把交易信号屏幕掉(即设置为空值)
df.loc[(df["收盘价"]>=df["涨停价"])&
(df["交易信号"]==1),
"交易信号"]=None
df.loc[(df["收盘价"]<=df["跌停价"])&
(df["交易信号"]==0),
"交易信号"]=None
df["交易信号"].dropna()
持仓信号标记
我们需要根据 交易信号 来标记出,在股票历史中,我们买入持仓的期间,以及卖出空仓状态的期间。这是为了之后能回测双均线策略的优劣所必须的步骤
df["持仓信息"] = df["持仓状态"].fillna(method="ffill").shift().fillna(0)
df[‘交易信号’].fillna(method=‘ffill’) 函数会填充 “交易信号” 字段的空值,这里用到的参数 method=‘ffill’ 代表向前填充。假设交易信号只有4行 [NaN , 1 , NaN , NaN],每一行空值都会按之前最近的一个非空值来填充自己,ffill填充结果为 [NaN , 1 , 1 , 1],首行找不到之前的非空数据就不会被填充
pandas里的方法是可以连续调用的,在填充空值后,连续调用 .shift() 移位,沿用上面例子的假设,[NaN , 1 , 1 , 1] 将变为 [NaN , NaN , 1 , 1, 1],最后调用 .fillna(0) 将剩余空值填充为 0 [0 , 0 , 1 , 1]
这样子做的目的是为了实现,当日建仓因为是T+1的,当日是不可以买卖的持仓数量应为0,次日才应该有持仓
构建函数分析
持仓的计算,都是基于50日均线,200日均线来标记生成的。如果我想测试其他的均线组合呢?我要一行行去改代码中出现MA50,MA200的位置嘛?–这里我们总结上述信息封装一个函数
def Ma_mark(dfr,n,m):
""
dfr股票原始数据
n短周期
m长周期
""
assert n<m,"短周期数值不能大于长周期数组"
assert m<dfr.shape[0],"长周期数值不能大数提供的股票数据的长度"
# 制作数据备份
df = dfr.copy()
# 计算双均线
df["{}日均线".format(n)] = df["收盘价_后复权"].rolling(n).mean()
df["{}日均线".format(m)] = df["收盘价_后复权"].rolling(n).mean()
# 计算交易信号
#买入
df.loc[df["{}日均线".format(n)]<df["{}日均线".format(m)]&
df["{}日均线".format(n)].shift()>df["{}日均线".format(m)].shift(),
"交易信号"]=1
# 卖出
df.loc[df["{}日均线".format(n)]>df["{}日均线".format(m)]&
df["{}日均线".format(n)].shift()<df["{}日均线".format(m)].shift(),
"交易信号"]=0
# 涨停无法买入标记
df.loc[df["收盘价"]>=df["涨停价"]&
df["交易信号"]==1,
"交易信号"]=None
# 跌停无法卖出标记
df.loc[df["收盘价"<=df["跌停价"]]&
df["交易信号"]==0,
"交易信号"]=None
# 计算持仓状态信号
# ffill向上填充
df["持仓状态"] = df["交易信号"].fillna(method="ffill").shift().fillna(0)
# 调用函数
df_ma= Ma_mark(df,50,20)
# 画图展示
df_ma[["收盘价_后复权","50日均线","200日均线"]]
注意事项
1.DataFrame.shape可以拿到数据的(行数,列数),索引取0即取数据的行数
2.assert语句包含两部分,assert 条件,不满足条件时报错并弹出对应提示 ,用来限制大家对函数参数的传入
持仓分组标记及涨跌幅分析
我们持仓状态要么是1,要么是0,类似[0 0 0 1 1 1 1 1 1 0 0 …]这样循环往复,我们希望对这些持仓状态都标记为不同的组,比如[0 0 1 1 0 0] 我们希望标记为 [组1 组1 组2 组2 组3 组3]
我么怎么实现这种持仓分组标记?
1.直接赋值index(交易日期)为持仓分组
2.我们看当日持仓状态是否等于前一日持仓状态.如果等于我们就把相等的数据对应的持仓分组行赋值为空值(None)
3.我们对持仓分组进行向上填充,就可以达到 [组1 组1 组2 组2 组3 组3]
method=‘ffill’ 代表向前填充。假设交易信号只有4行 [NaN , 1 , NaN , NaN],每一行空值都会按之前最近的一个非空值来填充自己,ffill填充结果为 [NaN , 1 , 1 , 1]
策略涨跌幅及手续费计算
策略持仓股票时,策略的涨跌和股票同步,策略空仓时,策略不涨不跌
- 开仓买入手续费计算
在持仓状态为1的每一组,首行即开仓买入,持仓状态刚刚从0到1,此时策略就要开始产生涨跌幅了。假设我们开仓支付了0.1%的手续费,那么实际持仓的价值就只有99.9%,也就是只有99.9%的仓位发生了涨跌, 首行策略实际涨跌幅 = (1 + 股票涨跌幅)[1是股票本身(基准)+涨幅]*(1 - 开仓手续费)[股票买入成本是100%-手续费0.1%]-1 - 平仓卖出手续费计算
在持仓状态为1的每一组,最后一行收盘时平仓卖出,再下一行持仓状态就要从1变为0了,此时策略涨跌幅就要被屏蔽掉了。在此之前,假设平仓也是0.1%手续费,那么最终变现的价值也就只有99.9%,公式同样是 最后一行策略实际涨跌幅 = (1 + 股票涨跌幅)*(1 - 平仓手续费)- 1
# 如果持仓状态为0,乘以涨跌幅,就是0.而如果为1,乘以涨跌幅,就涨跌幅本身
df["涨跌幅_策略"] = df_ma["涨跌幅"] * df_ma["持仓状态"]
# 找到第一次开仓点(持仓状态为1,上一行是0),扣除手续费.上一行=本行的shift()平移
df_ma.loc[df["涨跌幅_策略"]!=0 &
df["涨跌幅_策略"]!=df["涨跌幅_策略"].shift()
,"涨跌幅_策略"]=(1+"涨跌幅_策略")*(1-0.0008)-1
# 找出开仓组的最后一行(最后一行持仓状态为1,下一行是0),扣除手续费
df_ma.loc[df["涨跌幅_策略"]!=0 &
df["涨跌幅_策略"]!=df["涨跌幅_策略"].shift(-1)
,"涨跌幅_策略"]=(1+"涨跌幅_策略")*(1-0.0008)-1
注意事项:
当出现df[“涨跌幅_策略”]!=df[“涨跌幅_策略”].shift(-1)
df[“涨跌幅_策略”]!=df[“涨跌幅_策略”].shift()说明到了组与组的分界点了
绩效指标计算及调参方法
策略资金曲线,绩效指标计算
# 计算并更新策略资金曲线
df_ma.loc[:,"资金曲线_策略"] = (1+ df_ma["涨跌幅_策略"]).cumprod()
# 归一化
df_ma.loc[:,"资金曲线_策略"] = df_ma["资金曲线_策略"]/df_ma["资金曲线_策略"].iat[0]
# 计算最大回撤(策略在每个交易日,相对之前最高点跌幅是多大)
df_ma["最大回撤"]= df_ma["资金曲线_策略"].cummax()
df_ma.loc[:,"最大回撤"]= 1-df_ma["资金曲线_策略"]/df_ma["最大回撤"]
# 新建一个字典来存储各绩效指标
# 股票资金曲线终值:即股票资金曲线在最近一个交易日的数值,展示了开盘以来累计涨跌的收益情况
# 策略资金曲线终值:即策略资金曲线在最近一个交易日的数值,展示了开盘以来累计涨跌的收益情况
# 相对收益:最近一个交易日,策略资金曲线与股票资金曲线的比值
# 总交易次数:所有的买入卖出次数总计
# 平均持仓时间:持仓一次平均的时间长度
# 历史最大回撤:策略历史上,从最高点到最低点的跌幅
# 股票夏普率:股票平均涨跌幅/涨跌幅的波动,课程里我们简化夏普率计算,假设无风险收益率为0
# 策略夏普率:策略平均涨跌幅/涨跌幅的波动
dic={"股票资金曲线终值":df_ma["资金曲线"].iat[-1],
"策略资金曲线终值":df_ma["资金曲线_策略"].iat[-1],
"相对收益":df_ma["资金曲线_策略"].iat[-1]/df_ma["资金曲线"].iat[-1],
"总交易次数":df_ma["持仓分组"].unique().shape[0],
"平均持仓时间":(df_ma.index[-1]-df_ma.index[0])/(df_ma["持仓分组"].unique().shape[0]),
"历史最大回撤":df_ma["最大回撤"].max(),
"股票夏普率":df_ma["涨跌幅"].mean()/df_ma["涨跌幅"].std(),
"策略夏普率":df_ma["涨跌幅_策略"].mean()/df_ma["涨跌幅_策略"].std()
}
print(dic)
df_ma[["资金曲线","资金曲线_策略"]].plot(figsize=(20,8))
注意事项:
1.对策略资金曲线运用cummax函数,它能帮助我们计算出,资金曲线每一行,累计的最大值。比如我对一个列表 [1,3,5,4,2] 计算 cummax 结果为 [1,3,5,5,5]
2.df.unqiue() 去重获取到唯一值
3.shape():读取矩阵长度,如shape[0]是读取矩阵第一维的长度。
上面做了一堆很是凌乱,不方便我们后期复用,所以我们在这里封装一个函数,在实现上面的内容,方便后的的策略优化调用
# 重置一下df_ma,看看其他周期的效果
df_ma = MA_mark(df,100,300)
def fund_curve(dfr,tr=.0008):
'''
【回测:资金曲线及绩效指标计算】
dfr:标记交易信号及持仓状态后的股票数据
tr:单次交易综合手续费,默认万分之8
'''
# 拷贝一份数据
df = dfr.copy()
# 标记多空持仓组别
df['持仓分组'] = df.index
df.loc[df['持仓状态']==df['持仓状态'].shift(),'持仓分组'] = None
df['持仓分组'].fillna(method='ffill',inplace=True)
# 计算策略基本涨跌幅,持仓为0则当日策略基本收益率也为0
df['涨跌幅_策略'] = df['涨跌幅'] * df['持仓状态']
# 筛选所有非空持仓并扣除开平仓手续费
df.loc[(df['持仓状态'] != 0) &
(df['持仓状态'] != df['持仓状态'].shift()),
'涨跌幅_策略'] = (1 + df['涨跌幅_策略']) * (1 - tr) - 1
df.loc[(df['持仓状态'] != 0) &
(df['持仓状态'] != df['持仓状态'].shift(-1)),
'涨跌幅_策略'] = (1 + df['涨跌幅_策略']) * (1 - tr) - 1
# 计算并更新策略资金曲线
df.loc[:,'资金曲线_策略'] = (1 + df['涨跌幅_策略']).cumprod()
df.loc[:,'资金曲线_策略'] = df['资金曲线_策略'] / df['资金曲线_策略'].iat[0]
# 计算最大回撤
df['最大回撤'] = df['资金曲线_策略'].cummax()
df.loc[:,'最大回撤'] = 1 - df['资金曲线_策略'] / df['最大回撤']
return df,{'股票资金曲线终值':df['资金曲线'].iat[-1],
'策略资金曲线终值':df['资金曲线_策略'].iat[-1],
'相对收益':df['资金曲线_策略'].iat[-1]/df['资金曲线'].iat[-1],
'总交易次数':df['持仓分组'].unique().shape[0],
'平均持仓时间':(df.index[-1]-df.index[0])/df['持仓分组'].unique().shape[0],
'历史最大回撤':df['最大回撤'].max(),
'股票夏普率':df['涨跌幅'].mean()/df['涨跌幅'].std(ddof=0),
'策略夏普率':df['涨跌幅_策略'].mean()/df['涨跌幅_策略'].std(ddof=0)}
# 调用函数,把原始的df_ma数据传入资金曲线计算函数
result = fund_curve(df_ma)
# 查看绩效指标,并绘制资金曲线对比可视化图表
print(result[1])
result[0][['资金曲线','资金曲线_策略']].plot(figsize=(20,8),grid=True)