我们经常会在交易中使用一些技术指标,但很多时候只是知道这个指标的核心思路,并不太确定它们的实际效果。可能它们的效果和我们的想法有偏差。可能它们的效果是因股票而异的。也可能我们连一个具体的思路都不清楚,只是天马行空地想到,“咦?这个也许能行?”
我们把这些指标写到策略里,有时候回测看着像这么回事,可有时候又不像那么回事,懵懵懂懂的我们也不知道是怎么回事了。
“那…如果,”你说,“嗯,不对,让我再想想…”
时过半晌,“如果,”你又打断了沉默,“有一个系统性的统计方法,能让我们直观地看到这个指标的效果,那就好了。”
诶哟你说得太对了,我就是来找你谈这个的。
```
作者:肖睿
编辑:宏观经济算命师
本文由JoinQuant量化课堂推出,本文的难度属于进阶(上),深度为 level-1。
```
$ $
#### **统计的对象**
现代金融理论认为,证券价格的时间序列在大部分时间服从随机游走,但是在一些特定的时刻(比如基本面发生变化,或者供需关系发生变化),价格序列会偏离随机游走,并且选择向上或者向下的方向。当价格在脱离随机游走时,通常都会产生一些现象或征兆,就是我们常说的“信号”,如果我们捕捉到这些信号,就可以赚取收益。
我们使用利用股票的历史数据计算各种各样的指标(比如MACD,RSI),就是为了通过指标的数值来判断是否出现信号。但很多时候指标的使用是粗糙并且模糊的,比如“当RSI低于20时买入”是一个发出信号的标准,但为什么是20呢?18或者22会不会效果更好?我们想通过历史统计来分析这个问题,那么进行统计的对象就有两个:指标出现的数值,以及出现该数值之后的涨跌结果。
$ $
**指标**
一般而言,一支股票的指标是一个函数Ind(T),它输入的是时间T(确切地说还有时间T之前的所有股票数据),返回的是在那个时间点的指标数值。比如说,我们要考量的指标是过去5天的收益率,那么指标的输出就是今日收盘价除以五天前收盘价的商再减1。
$ $
**结果**
结果指的就是,我们认为指标所预测的事件到底有没有发生,由此把结果分为“赢”、“平”和“输”。举例来说,假设我们认为过去5天的收益率越高,未来两天的收益率就越大;那我们要观测的结果就是未来两天交易量的情况,如果未来两天的平均交易量大于今天的110%,就记作“赢”,小于今天的100%就记作“输”,其余情况记作“平”。这里“赢平输”的计算标准有一定拍脑袋的成分,这么做的缺点就是统计的数据不全面,优点是结果更直观更方便应用,并且如果统计结果不理想,我们可以更改输赢的决定方法再重新来过。
$ $
#### **指标和结果的统计**
**数据统计**
一、提取历史所有交易日的股票数据,并去掉停牌日的信息;
二、记录每一天的指标和输赢结果。对于每一个不停牌的交易日T,做:
$\qquad$ a. 计算当日的指标Ind(T),方便统计需要,四舍五入到合适的小数点位;
$\qquad$ b. 计算从T日起观测的输赢结果;
$\qquad$ c. 记录事件组(Ind(T),赢或平或输)。
三、统计指标的某个值出现后赢和平和输分别发生过多少次。
举例个简单的例子。假设我们认为过去5天涨得越多则未来五天越可能跌。那么把五日收益率设为指标;未来第五天收盘价小于今日收盘价则记为赢,大于记输,等于则记平。
首先收集数据,注意要把停牌时的数据去掉,这里是从06年2月2日到16年2月2日。
```
prices = get_price('002200.XSHE', start_date="2006-02-02", end_date="2016-02-02", frequency='daily', fields=['close'], skip_paused=True)['close']
然后进行统计,第二步第三步可以一气呵成
# 要用到pandas
import pandas as pd
def get_stats(prices):
# 设置一个计数器
i = 5
# 设置一个空list
data = []
# 从第一个到倒数第二个价格
while i < len(prices)-4:
# 计算五日收益率,留小数点后2位
ratio = 0.01 * ((prices[i]/prices[i-5]) //0.01)
# 如果第五天收盘价更低
if prices[i+4] < prices[i]:
# 那结果记赢
result = 'win'
# 如果第五天收盘价更高
if prices[i+4] > prices[i]:
# 结果记输
result = 'lose'
# 收盘价不变的话
if prices[i] == prices[i+4]:
# 记平
result = 'even'
# 看看该比例有无记载过
ratio_recorded = False
# 翻看data
for data_dict in data:
# 如果比例被记载过
if data_dict['value'] == ratio:
# 那就好,更新输赢
data_dict[result] += 1
# 记载过为是
ratio_recorded = True
# 如果翻完了发现没记载过,
if ratio_recorded == False:
# 那么就记载下来
data_dict = {'value':ratio, 'win':0, 'lose':0, 'even':0}
data_dict[result] += 1
data.append(data_dict)
# 别忘更新i
i += 1
# 转换成DataFrame
df = pd.DataFrame(data, columns=['value', 'win','even', 'lose'])
# 按照value列从小到大排序
df = df.sort(['value'], ascending = True)
return(df)
```
然后就可以看到结果啦
```
stats = get_stats(prices)
stats.head(10)
```
![图1.png][1]
$ $
#### **制图**
把统计好的数据画出来就可以看出效果了。基于以上数据的特性,我们要画一个叠加条状图。该图的x坐标轴是指标的值,y轴则是该指标值出现的次数,并按颜色划分成“赢平输”叠加的条状图。代码如下:
```
# 要用到pyplot包
Import matplotlib.pyplot as plt
# 设置图的大小
fig_size = plt.rcParams["figure.figsize"]
fig_size[0] = 15
fig_size[1] = 10
# 把统计的DataFrame中竖列抽出来
values = stats['value']
wins = stats['win']
evens = stats['even']
loses = stats['lose']
# 画输的数目,颜色为红
p1 = plt.bar(values, loses, width = 0.008, color='r')
# 画平的数目,颜色为黄,底部在输之上
p2 = plt.bar(values, evens, width= 0.008, bottom = loses, color='y')
# 画赢的数目,颜色为绿,底部在输+平之上
p3 = plt.bar(values, wins, width=0.008, bottom = evens+loses, color='g')
# 标注坐标轴和注释
plt.ylabel('输赢结果',size = 15)
plt.xlabel('收盘价除以22',size = 15)
plt.legend((p1[0], p2[0],p3[0]), ('输', '平','赢'))
```
得到下图
![图2.png][2]
为了方便看出赢和输的差距,可以把赢减输画出来。
代码
```
# 画赢数减去输数
pdifference = plt.bar(values, wins - loses, width = 0.008, color='b')
# 标注坐标轴和注释
plt.ylabel('赢减去输',size = 15)
plt.xlabel('5日收益率',size = 15)
```
得到
![图3.png][3]
这样就可以清晰地看到一些规律。
$ $
#### **合理地使用统计结果**
**统计结果不可以乱用**
我们单单把图画了出来,那是不是就可以在策略中用了?当然不是。拿之前的统计举例,可以看见两个比较明显的问题
![图4.png][4]
先看紫色标注的位置,那里统计的胜率很高。如果们选择在5日收益率1.17时买入,会怎么样?首先5日收益率正好为1.17的概率很小,可能若干年都不出现信号。其次,可以看见它右边的1.18的胜率并不高,那么实际上当指标计算出为1.175时就无法合理判断。
再看棕色框起的区域,这里只有赢没有输,但是这个区域中样本量太小了,完全无法保证统计结果的准确性。
$ $
**选择胜率最高的区间**
一个可行的解决办法,是设定一个指标区间的宽度w,并且设定最低数据比θ。如果一个宽度为w的x轴区间里的数据量占总数据量的θ以上,我们就计算并记录该区间里的输赢比;如果区间内数据比例少于θ,我们则抛弃该区间。最后选出输赢比最高的区间作为产生信号的指标值。
比如我们设w=4,那么在1.08到1.11的区间里,赢输比为66:58。
![图5.png][5]
我们要选的是依照该计算方法得到的赢输比最高的区间。
下面函数的4个输入分别为:
statistics – 之前算出的DataFrame统计数据
tick_width – 指标值的最小间隔(跳动值,比如上面的示例里就是0.01)
least_percentage – 要求区间数据量至少有总数据量的多少
band_width – 区间的宽度,整数,可理解为进行多少次tick_width的跳动。
代码如下
```
# 返回一Series,内容 low=区间低点,high=区间高点,ratio=区间赢输比
def find_best_region(statistics, tick_width, least_percentage, band_width):
# 取出统计df的列
values = statistics['value']
loses = statistics['lose']
wins = statistics['win']
evens = statistics['even']
# 算总数据量
num_data = sum(wins) + sum(loses) + sum(evens)
# 起始一list
mydata = []
# 计算指标统计出的最低值除以间距,取整数,方便后面移动计算。
low_bound = int(statistics['value'].iloc[0]/tick_width)
# 计算指标统计出的最高值除以间距,减去区间宽度
high_bound = int(statistics['value'].iloc[-1]/tick_width - band_width + 1)
# 对于上限和下限之间的所有整数
for n in range(low_bound, high_bound ):
# 选取统计中所对应的区间
statistics1 = statistics[values >= float(n)*tick_width]
stat_in_range = statistics1[values <= float(n + band_width - 1) * tick_width]
# 计算区间中的赢输比。输数加一,避免除以零。
ratio = float(sum(stat_in_range['win'])) / float(sum(stat_in_range['lose'])+1)
# 计算区间中数据量
range_data = float(sum(stat_in_range['win']) + sum(stat_in_range['lose']) + sum(stat_in_range['even']))
# 如果区间数据量除以总数据量大于最低数据比
if range_data / num_data >= least_percentage:
# 记录区间的最低值,最高值,和区间内的赢输比
mydata.append({'low': float(n) * tick_width, 'high': float(n+band_width) * tick_width, 'ratio': ratio})
# 制作DataFrame
data_table = pd.DataFrame(mydata)
# 按照赢输比排序
sorted_table = data_table.sort('ratio', ascending = False)
# 返回第一行
return(sorted_table.iloc[0])
```
对于上面的例子,tick_width = 0.01,再设 least_percentage = 0.03,以及 band_width = 4,计算
```
find_best_region(stats,0.01, 0.03, 4)
```
得到区间[1.12,1.16],区间内输赢比1.43。
$ $
**还不过瘾?**
不过瘾好啊,那么与其固定区间的宽度,我们不如把所有的宽度都计算一遍。做法就是,先固定一个最短的宽度w_0,对于所有宽度为w_0的区间进行上面的计算,然后将宽度换为w_0+1再重复一遍,然后是w_0+2,以此类推,直到达到了设定的最大宽度。最后取这个过程中算出的最大的输赢比,并取相应的区间。
具体的计算已经由上面的函数完成,现在只要再包装一个函数来迭代地以不同的宽度呼出上面的函数,得到每个长度的最佳区间,再从这里面选出胜率最好的。
这里输入的statistics,tick_width,least_percentage和之前相同,另外的两个输入是
least_width – 最短的区间宽度
most_width – 最大的区间宽度
代码
```
# 返回一Series,内容 low=区间低点,high=区间高点,ratio=区间赢输比
def find_absolute_best_region(statistics, tick_width, least_percentage, least_width, most_width):
# 创建标注列的空DF
columns = ['low', 'high', 'ratio']
df = pd.DataFrame(columns = columns)
# 对于所有在最短和最长标准之间的宽度
for band_width in range(least_width, most_width + 1):
# 运行上面函数,得到该宽度的最佳区间
best_width_region = find_best_region(statistics, tick_width, least_percentage, band_width)
# 将结果加入DF
df = df.append(best_width_region, ignore_index = True)
# 将赢输比从大到小排列
sorted_table = df.sort('ratio', ascending = False)
# 返回第一行
return(sorted_table.iloc[0])
```
还是同样的例子,这次least_percentage选0.05,最小宽度least_width=2,最大宽度most_width=30。然后
```
find_absolute_best_region(stats, 0.01, 0.05, 2, 30)
```
得到宽度为19的区间[1.12,1.31],这之间的赢输比为1.72,这也是使用该指标时保证5%数据量的情况下的最大赢输比!
$ $
#### **对于在策略中运用的补充**
如果想把本篇统计算法的输出嵌入到交易策略中,应该注意以下几点:
$ $
**不同股票要分开统计**
因为每支股票的“个性”不同,所以对于不同指标的关联程度是不一样的,一支股票的最高胜率区间用在另一支股票上也许效果会截然不同,所以一定要把不同的股票分开统计。
$ $
**每日更新统计数据**
使用本方法的话最好把整个算法嵌入到策略中去,并且每日更新统计数据和最佳区间。如果不这么做的话,一则指标的最佳区间可能会过时,二则由于现在的最佳区间对于历史交易日来说是未来数据,所以以其进行回测会产生虚高的效果。
$ $
**运用凯利公式**
本篇的统计方法会计算出依照指标进行判断的成功率,利用该信息我们可以使用凯莉公式来优化投资收益。
凯莉公式[点我](https://www.joinquant.com/post/1311?f=study&m=math),
具体的使用方法就留给大家当作业啦!
$ $
```
本文由JoinQuant量化课堂推出,版权归JoinQuant所有,商业转载请联系我们获得授权,非商业转载请注明出处。
文章更迭记录:
v1.0,2016-08-11,文章上线
```
[1]: https://image.joinquant.com/e00aa283971a13a435ec7543c1eaf306
[2]: https://image.joinquant.com/6783dc1040d219c1d9602c22f31c9a5f
[3]: https://image.joinquant.com/5643a11627019e96cab78465cb9b87ad
[4]: https://image.joinquant.com/1c3d0a9714cdd11529f657c716b585f4
[5]: https://image.joinquant.com/2f5cf3b122c89348522eb56df82afd9f
复制的代码,但获得的数据似乎不一样
![QQ图片20160812232034.png][1]
[1]: https://joinquant-image.b0.upaiyun.com/5abb01d5bae636225fbb4daed04fd472
2016-08-12
这篇蛮好的 有参考价值 要是能更进一步 给个结果就更好了
2016-08-18
请问这个代码直接在我的策略里面跑吗,怎么样子图形才能出来
2016-10-05
@cepvoggg 在研究模块里可以把图画出来,在策略里同样可以计算,但画不了图。看[均值回归进阶](https://www.joinquant.com/post/1899?f=study&m=algorithm)。
2016-10-13
未来第五天收盘价小于今日收盘价则记为赢,大于记输,等于则记平。
我脑子有点转不过来.....
2017-03-24
@JoinQuant量化课堂
# 如果第五天收盘价更低
if prices[i+4] < prices[i]:
# 那结果记赢
result = 'win'
# 如果第五天收盘价更高
if prices[i+4] > prices[i]:
# 结果记输
result = 'lose'
# 收盘价不变的话
if prices[i] == prices[i+4]:
# 记平
result = 'even'
这里是否弄反了,之后第五天大于当天才赢
2017-06-23
学习了,很实用。能不能再多给点其他的分析思路。非常感谢
2017-09-21
@weapon 楼主的假设是涨了5天会跌,所以第五天大意味假设失败了。
2017-10-11
@cepvoggg回复一下,说不定有人看到,可以在 我的策略-投资研究-新建一个测试文本,按shift+enter就可以跑脚本。
2018-03-22
很好的思路,之前在通达信里实现了类似的算法,量化里用处更大,感谢小编分享。
2018-04-07
```python
# 如果区间数据量除以总数据量大于最低数据比
if range_data / num_data >= least_percentage:
# 记录区间的最低值,最高值,和区间内的赢输比
mydata.append({'low': float(n) * tick_width, 'high': float(n+band_width) * tick_width, 'ratio': ratio})
```
最后一行代码,float(n+band_width) * tick_width是否应该为float(n+band_width-1) * tick_width ?
2018-04-21
@moo 对,是float(n+band_width-1)
2019-03-03