这一节分为上下两个部分,内容较多,喜欢看的仔细阅读一下,干货!
这一篇看完你会知道:
- 机器学习在金融领域为啥 fail
- 什么是金融数据的Stationarity并如何判断
由于套利对于市场的强大推进,金融序列展现出了异常低的信噪比,那么什么是信噪比呢?
这还是要回归到机器学习到底能不能在金融领域有效应用的问题(当然,整本书都在讨论这个问题!)
为什么机器学习在很多领域,比如图像识别、语音识别、自然语言处理等等方面,机器学习或者深度学习算法都能有很好的效果,但是这么多年了,机器学习在金融领域的有效应用还没有完全挖掘出来。
机器学习在很多领域似乎很强大,可以识别猫狗、驾驶汽车,甚至在大赛上完虐人类玩家,那么在选股等金融方面的任务中,似乎也应大展拳脚才对啊!但这并没有得到研究的支持,至少现在无。。。。
到底有何不同?
资产管理的一个核心问题,回报预测,就是典型的一个小数据问题,都说金融大数据,比如 tick 数据几个 T,bar 也能达到几个 G(不明白tick/bar的看前面的文章),但是这跟工业界数据没法比,况且,就从一个简单的回归问题的公式来看:
左边假设就是要预测的回报,右边就是参数加变量的组合,对于金融数据,我们不是最在乎 N 的大小,而往往更在乎有效的 t 的数量,也就是 T 一定要大一些,数据应该又肥又长才行。不然模型复杂度一上去,样本外测试会让你亏得妈都不认识。
信噪比,它总结了系统中存在多少是可以预测的,那么就以识别猫狗为例,用个不要太复杂的 ResNet 训练个 10 个 epoch 成功率基本达到了 100%,高成功率就意味着这是个高信噪比环境。信号(猫的图像)控制着照片中的噪声源(模糊程度和背景等)。机器学习在这方面就很牛逼。
但对于回报预测,信噪比就不能说是弱了,是弱的一批,这就说名这个一系统本身就充满着随机不确定性,那么具体为啥?
金融市场嘈杂,哪怕最好的投资组合,在一个月甚至一天的波动也许会因为意想不到的信息而疯狂波动。
还有就是我们本身就预测金融市场的信噪比就很低,并一直保持在较低的水平上,随着某些交易者掌握了一些可靠的预测未来价格上涨信息,他们定会交易,知道这种信息不断被市场消磨,把价格推上去,使得价格达到信息所预测的水平,那么其他投资者只被留下极小的可预测性,所以预测性已被定价了,唯一能推动市场的是未预料到的消息或冲击。
还是回到回报预测(return prediction)这个问题,回报的计算方式就有待考究,传统的收益率计算:
整数阶差分移除了信号(return)的记忆性(memory),价格序列有记忆性,因为每个值都依赖于之前很长的历史序列。然而,整数阶差分会抹掉记忆,在一个有限的window不断沿着时间轴往下过滤,之前的信息就被遗忘了,所以我们要在数据转换上下点功夫,因为我们不想舍弃整数阶差分的稳定性特征(stationary transformation),但又不想丢失太多之前的记忆(memory)。
So, we are trapped in the stationary vs. memory dilemma.
简单来说,stationarity 就是随着时间推移,序列的均值方差不随时间改变的性质。所以对于研究者来说,他们希望序列能有个稳定的变化,那么这种变化能够通过历史数据以及模型来去学习所谓的 alpha,并且他们认为这种变化能够随着时间一直研究下去,从而挣钱。But, that is a wishful assumption to expect they will persist over time!
但是 alpha 会反噬!alpha约牛逼,越有可能被其他人copy,所以世上便不存在alpha了,因此,每个预测的变化趋势或者说是 pattern 都是短暂的。
还是回到 stationary 这里来,这个性质对于经济学家来说相当重要,所以过去几十年来,似乎在进行建模之前,都先要把数据做一些平稳性处理。
但是单单从一个有限的样本路径来看,是不可能检验序列是否平稳的!
单看上面这个图,很明显不满足 stationary 的情况,因为序列的均值方差都随着时间在变化,可以用 ADF test 去检验一下。
得到:
1ADF Statistic: 4.264155
2p-Value: 1.000000
3Critical Values:
41%: -3.4370
55%: -2.8645
610%: -2.5683
ADF 检验 p 值过大,明显不能拒绝原假设,也就是说明序列是不平稳的。
但是,如果我现在告诉你上面的图是来自于一个高斯分布(mean 为 100)且自协方差方程满足:
那么肯定认为这一定是个平稳性序列,因为均值和自协方差都是不随时间变化的。
这是把时间维度扩大所得图像。
再用 ADF test 检验一下得到:
xxxxxxxxxx
61ADF Statistic: -4.2702
2p-Value: 0.0005
3Critical Values:
41%: -3.4440
55%: -2.8676
610%: -2.5700
很明显拒绝了原假设,所以说明序列是平稳的,并且置信度很高,p值达到了 0.05%。
可以看出,第一个图中包含了足够多的样本数量,也就是样本容量虽然够大,但是时间跨度却不够,也间接说明金融数据的 small data 问题,所以很明显,没有其他的假设的情况下,不可能从一个有限的时间跨度来判定序列是否稳定。
这一篇看完你会知道:
- 什么是平稳性(stationary)和差分(differencing)
- 分数阶差分的方法具体是什么(emmm理论推导)
说到整数差分,或者与 differentiation 相关的,你肯定首先想到时间序列里面学到的 ARIMA,那么为啥要差分之前那篇文章说的很清楚了,就是要让序列变得 Stationarity,但是过度的差分又会使模型丢失 memory,从而丧失一大部分预测能力,所以我们才想办法看看能不能做一个权衡,搞个分数差分出来。
ok, just a little review.
不过,那么一个序列是完全平稳的话,就完全没有预测的必要了,因为模型的参数不会随着时间改变,现在是啥以后还是啥,那还预测个啥,图像来说就基本是平的。
那么,你是否有信心能够分辨出下面这几个图哪些是 stationary 的呢?
季节性(seasonality)排除了系列 (d)、(h) 和 (i)。趋势性(trend)排除了系列 (a)、(c)、(e)、(f) 和 (i),只剩下 (b) 和 (g) 是平稳序列。
可以发现,(b)就是(a)的差分,从而序列变得平稳了,那么差分就是连续观测值的差值,就这么简单。
Differencing can help stabilise the mean of a time series by removing changes in the level of a time series, and therefore eliminating (or reducing) trend and seasonality.
学过 R 的朋友,应该知道 ACF 那个图哈,就是 auto-correlation function,它是关于不同的延滞值而画出的序列自相关系数,自相关就是自己跟自己的相关系数,那有人说了,那不等于1么?是的,但是在时间序列中,不同时间下的同一序列的自相关系数是不一样的,这里不再展开了。
那么既然了解了差分的基本原理和基础,我们来说一下 backshift operator,可以叫做延滞算子?我也不清楚,我们就命名为 吧,这个 ,它满足对于任意的自然数 ,,其中 是特征所形成的向量。
举个例子,,其中 ,所以
根据二项式展开公式,我们有
那么,对于一个实数 ,我们有
同理可推导
我们经过展开之后,特征向量 的权重可以用 来表示
其中这个权重 就是上面含 的单项式的系数
并且 为
可以看到 (1) 中,如果 d 是一个正整数,那么第三项之后就全变成 0 了,因此隐藏在第三项之后的memory就被清除了。
可以发现,随着 的增大, 的变化是由规律的
可以发现,
那么再来看一下 的情况:
变得有一些不一样了是吧,但是这些曲线随着 的增加,最后都收敛到 0 了!
下面是上面曲线的代码,有兴趣可以看一下,有详细的备注!
x1import matplotlib.pyplot as plt
2import pandas as pd
3
4# 计算权重 omega
5def getWeights(d,size):
6# thres>0 drops insignificant weights
7 w=[1.]
8 for k in range(1,size):
9 w_=-w[-1]/k*(d-k+1)
10 w.append(w_)
11 w=np.array(w[::-1]).reshape(-1,1)
12 # 倒序排序并且reshape成一列
13 return w
14
15# 画图
16def plotWeights(dRange,nPlots,size):
17 # 思路:
18 # 对每个d求其对应size的weight
19 # 然后根据index来outer join到一起
20 w=pd.DataFrame()
21 for d in np.linspace(dRange[0],dRange[1],nPlots):
22 d = round(d,1)
23 w_=getWeights(d,size=size)
24 w_=pd.DataFrame(w_,index=range(w_.shape[0])[::-1],columns=[d])
25 w=w.join(w_,how='outer')
26 ax=w.plot(figsize = [12,8])
27 ax.legend(loc='upper left')
28 return
29
30plotWeights(dRange=[0,1],nPlots=11,size=6)
31plotWeights(dRange=[1,2],nPlots=11,size=6)
32
33# have a try!
那么,如果 不断增大,之后的曲线还会随着 增大继续收敛么?
没错!
通过这个式子,我们可以发现,通过不断地迭代,当 时,,当然这里的 。从而,权重就会逐渐地趋近 0。
总结起来就是:
那么,这一期就讲到这里,你应该了解下面几点:
前两周参加公司合唱比赛,丢了半条命,最近身体欠佳所以更新迟了,不过会继续努力的!BTW,我准备开通一个 video channel!但是还在准备中,重点放在本科数学狗留美攻略,以及中英文的数统知识以及机器学习等方面的对照讲解,emmm,反正还在准备中,到时候请大家多多支持啦!
OK,希望这期内容对大家有帮助,下期再见!
这一篇看完你会知道:
- 分数阶差分的两个替代方法(Expanding window和FFD)
- 分数阶差分的适用场景以及代码实战!
现在,要介绍两个分数阶差分的替代方法
那么首先来说一下,什么是 Expanding Window 呢?接触过 ARIMA 处理过时间序列的人应该了解 Rolling Window 是什么,那么我们就简单对他们做一下解释并对比他们的不同。
just a window
窗户,大家都很熟悉了,上面这个图就是个普通到不能再普通的窗户了,简而言之,窗户的大小决定了我们能透过窗户看到的视野范围的大小。所以对于数据来说也一样,一个移动的窗户可以帮助我们研究数据的一部分。
假设,我们想知道每一天的之前5天的股票平均价格,那么我们应该使用 rolling windows,随着窗口的移动,我们在每次移动过程中都可以计算出窗口数据的均值,最终形成了一条曲线,如下图。
xxxxxxxxxx
111plt.figure(figsize = [12,8])
2data = [100,101,99,105,102,103,104,101,105,102,99,98,105,109,105,120,115,109,105,108]
3#Create pandas DataFrame from list
4df = pd.DataFrame(data,columns=['close'])
5#Calculate a 5 period simple moving average
6sma5 = df['close'].rolling(window=5).mean()
7#Plot
8plt.plot(df['close'],label='HS300')
9plt.plot(sma5,label='SMA',color='red')
10plt.legend(prop = {'size':20})
11plt.show()
随着 rolling window 不断前进,形成的动图如下:
Expanding Windows 则是固定了一个 starting point,然后随着数据的加入,窗口的 size 越来越大。
就像下面这样!
xxxxxxxxxx
151plt.figure(figsize = [12,8])
2#Random stock prices
3data = [100,101,99,105,102,103,104,101,105,102,99,98,105,109,105,120,115,109,105,108]
4#Create pandas DataFrame from list
5df = pd.DataFrame(data,columns=['close'])
6#Calculate expanding window mean
7expanding_mean = df.expanding(min_periods=1).mean()
8#Calculate full sample mean for reference
9full_sample_mean = df['close'].mean()
10#Plot
11plt.plot(df['close'],label='HS300')
12plt.plot(expanding_mean,label='Expanding Mean',color='red')
13plt.axhline(full_sample_mean,label='Full Sample Mean',linestyle='--',color='red')
14plt.legend(prop = {'size':20})
15plt.show()
随着 window size不断增大,形成的动图如下:
总结一下:
“在这一点,之前的 n 个值的平均值是多少”
用 rolling window
“在这一点,之前所有可获取的数据的平均值是多少”
用 expanding window
下面两个图分别表示沪深300(HS300)的收盘价的分数差分情况,第一个的参数为 ,第二个为 。
可看到当 不变时,确实我们把 tolerance 降低之后,前边的数据被 drop 掉更多了!而且序列也在保持跟蓝色曲线走势的同时,兼顾了平稳性!(代码我放在下面)
681import pandas as pd
2import numpy as np
3import matplotlib.pyplot as plt
4import seaborn as sns
5%matplotlib inline
6
7# (1) 定义 fractional differencing function
8def fracDiff(series,d,thres=.01):
9 '''
10 Increasing width window, with treatment of NaNs
11 Note 1: For thres=1, nothing is skipped.
12 Note 2: d can be any positive fractional, not necessarily bounded [0,1].
13 '''
14 #1) Compute weights for the longest series
15 w=getWeights(d,series.shape[0])
16 #2) Determine initial calcs to be skipped based on weight-loss threshold
17 w_=np.cumsum(abs(w))
18 w_/=w_[-1]
19 skip=w_[w_>thres].shape[0]
20 #3) Apply weights to values
21 df={}
22 for name in series.columns:
23 seriesF,df_=series[[name]].fillna(method='ffill').dropna(),pd.Series(dtype = 'float64', index = np.arange(series.shape[0]))
24 #print(seriesF)
25 for iloc in range(skip,seriesF.shape[0]):
26 #print(iloc)
27 loc=seriesF.index[iloc]
28 #print(loc)
29 #if not np.isfinite(series.loc[loc,name]):continue # exclude NAs
30 #print(np.dot(w[-(iloc+1):,:].T,seriesF.loc[:loc])[0][0])
31 df_[loc]=np.dot(w[-(iloc+1):,:].T,seriesF.loc[:loc])[0,0]
32 #print(df_)
33 df[name]=df_.copy(deep=True)
34 df=pd.concat(df,axis=1)
35 return df
36
37# 获取沪深300数据
38import tushare as ts
39df = ts.get_k_data('hs300',start = '2016-01-01')
40raw_df = df[['open','close','high','low']]
41new_df = fracDiff(series = raw_df, d = 0.4, thres = 0.01)
42
43# (2) 画图
44def plotExpandingWindow(raw_df,new_df,column = 'close'):
45 plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签
46 x = np.arange(raw_df.shape[0])
47 y1 = raw_df[column]
48 y2 = new_df[column]
49
50 fig = plt.figure(figsize = [40,20])
51
52 ax1 = fig.add_subplot(111)
53 ax1.plot(x, y1,linewidth = 6)
54 ax1.set_ylabel(column+" price for HS300 BEFORE expanding window differencing",size = 35, color = 'b')
55 ax1.set_title("HS300使用Expanding Window前后对比图"+"",size = 40)
56 ax1.set_title("{0} {1}".format("HS300使用Expanding Window前后对比图 ==>", column.upper()),size = 60)
57
58
59 ax2 = ax1.twinx() # this is the important function
60 ax2.plot(x, y2, '#C70039',alpha=0.6)
61 ax2.set_ylabel(column+" price for HS300 AFTER expanding window differencing",size = 35, color = 'r')
62
63 plt.show()
64 return
65
66# 画出 open close high low 四种对比图
67for col in raw_df.columns:
68 plotExpandingWindow(raw_df,new_df,column = col)
那么之前介绍了 Expanding Window 的方法,现在再介绍一个基于 Rolling Window 的方法,也就是固定 window size,同理我们认为如果 落到了一个给定区间,也就是小于一个阈值 。
这种方法的好处在于可以避免 expanding window 所带来的累积驱动效应而导致的 negative drift,也就是由于累积动量产生的定向偏移。
对上面的代码加以改进,我们可以得到:
xxxxxxxxxx
601def get_weight_ffd(d, thres, lim = len(series)):
2w, k = [1.], 1
3ctr = 0
4while True:
5w_ = -w[-1] / k * (d - k + 1)
6if abs(w_) < thres:
7break
8w.append(w_)
9k += 1
10ctr += 1
11if ctr == lim - 1:
12break
13w = np.array(w[::-1]).reshape(-1, 1)
14return w
15
16def fracDiff_FFD(series,d,thres=1e-5):
17'''
18Constant width window (new solution)
19Note 1: thres determines the cut-off weight for the window
20Note 2: d can be any positive fractional, not necessarily bounded [0,1].
21'''
22#1) Compute weights for the longest series
23w=get_weight_ffd(d,thres)
24width=len(w)-1
25#2) Apply weights to values
26df={}
27for name in series.columns:
28seriesF,df_=series[[name]].fillna(method='ffill').dropna(),pd.Series()
29for iloc1 in range(width,seriesF.shape[0]):
30loc0,loc1=seriesF.index[iloc1-width],seriesF.index[iloc1]
31if not np.isfinite(series.loc[loc1,name]):continue # exclude NAs
32df_[loc1]=np.dot(w.T,seriesF.loc[loc0:loc1])[0,0]
33df[name]=df_.copy(deep=True)
34df=pd.concat(df,axis=1)
35return df
36
37def plotExpandingWindow_FFD(raw_df,new_df,column = 'close'):
38plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签
39x = np.arange(raw_df.shape[0])
40y1 = raw_df[column]
41y2 = new_df[column]
42
43fig = plt.figure(figsize = [40,20])
44
45ax1 = fig.add_subplot(111)
46ax1.plot(x, y1,linewidth = 6)
47ax1.set_ylabel(column+" price for HS300 BEFORE FFD differencing",size = 35, color = 'b')
48ax1.set_title("HS300使用FFD前后对比图"+"",size = 40)
49ax1.set_title("{0} {1}".format("HS300使用FFD前后对比图 ==>", column.upper()),size = 60)
50
51
52ax2 = ax1.twinx() # this is the important function
53ax2.plot(x, y2, '#C70039',alpha=0.6)
54ax2.set_ylabel(column+" price for HS300 AFTER FFD differencing",size = 35, color = 'r')
55
56plt.show()
57return
58
59for col in raw_df.columns:
60plotExpandingWindow_FFD(raw_df,new_df,column = col)
可以明显看出来,平稳化后的序列比 Expanding Window 的序列要短一些,但是更加平稳了!**
那么这时候,我们自然想着是否能在最大化去保存 memory 的同时,能否也尽量保证序列的平稳性。
FFD 中的主要影响参数是 ,那么我们可以根据不同的 来测试序列的平稳度,并画出曲线。这个 可以说是我们需要移除多少 memory 才能保证序列的 stationary 的变量,当原始序列是很不平稳的时候,我们初步应该选择 。
上图的右侧 y 轴表示基于 close price 取对数计算的 ADF 统计量,横坐标是 。
所以,实际情况当中,我们可以按照以下 4 步来进行:
计算时间序列的累计和,确认序列的平稳程度,以及是否需要差分
使用不同的 计算 FFD(d)
序列,如果序列极其不平稳,那么 应该首选趋于 0 的小数
确定一个能使得 FFD(d)
的 ADF test 统计量的 p 值低于 5% 的 d(一般小于0.6)
使用 FFD(d)
序列作为新的预测特征
小结
OK,那么关于第五章的分数阶差分特征的内容就到这里啦,希望对大家关于 “金融序列在保证平稳性的同时能使得记忆最大化” 这一问题上有新的理解,希望大家多多写写代码,试试不同参数,说不定会有意想不到的效果!下期见~