本文关于为金融数据打标签的方法逐一介绍
由于金融数据的高噪声和序列自相关性很强,很难预测股价的连续值,那么只能为数据打标签,从而预测离散值。
几乎所有机器学习文献都使用了固定时间区间(Fixed-time Horizon, FH)方法对金融数据打标签。
这种方法简单直观,判断规则十分简单。在固定时间内对于某个股票,如果其收益
用公式对上述规则进行表述。
其中
举个实际例子,从 2019 年 1 月 27 日开盘时点(,0)开始计算苹果股票10 个 bar 后(h = 10)的收益,得到 r = 0.5%,如果阈值是 0.1%(c = 0.1%),那么打上「涨」的标签。
该方法很常用,但也存在以下两个问题:
在〖从 Tick 到 Bar〗一帖可知等时抽样的 Time Bar 的统计特征不好
阈值 c 一直不变,但价格波动率却随时间变化,这就造成了
对于上面二个问题,也有两个解决方法:
那么,再计算完波动率之后,可以设定上下阈值 和
其中, 和 是缩放因子
三隔栏方法(Triple-Barrier,TB)方法是一种路径依赖的标注方法,能够有效地解决上节所提到的止损止盈问题。
为什么要设定三隔栏?
TB 和 FH 方法相似,我们需要三种情况来为数据打上 +1, -1, 0 三个标签,而打哪个标签看价格函数先碰到三隔栏的哪一个。
如何设定三隔栏?
设立两个价格上水平(horizontal)的隔栏和一个时间上垂直(vertical)的隔栏,其中
如何用三隔栏打标签?
如果
这显然是一个路径依赖的方法,因为我们需要确定在整个时间区间内三个隔栏是否在某一刻被触及。
我们定义
通常我们有 关系
此外,我们还可以用 来代表隔栏有效状态,其中
这三个状态只能去 0 和 1,0 代表此隔栏无效,1 代表此隔栏有效。三个状态那么可能会有 8 种情况,它们分别是:
三种实际的情况(上图绿 √):
三种不太实际的情况(上图蓝 ?):
两种不合逻辑的*情况*(上图红 ×):
下面三图分别展示了 [1, 1, 1] 标配的三种退出方式。
一. 先碰到「下水平隔栏」而止损退出。
二. 先碰到「上水平隔栏」而止盈退出。
三. 先碰到「竖直隔栏」而超过持有期限退出。
数据下载:Stock Price
x1# import
2
3import pandas as pd
4import numpy as np
5import seaborn as sns
6import matplotlib.pyplot as plt
7
8# load the data
9
10data = pd.read_csv('1Y_Stock_Data.csv', parse_dates=[0], dayfirst=True)
11
12# get a glimpse of the data
13def view(data):
14 print('The shape of the data is',data.shape)
15 return data.head().append(data.tail())
16
17# get AAPL data
18data = data[data.Symbol == 'AAPL']
19view(data)
Date | Symbol | Open | High | Low | Close | Adj Close | Volume | |
---|---|---|---|---|---|---|---|---|
0 | 2018-02-26 | AAPL | 176.350006 | 179.389999 | 176.210007 | 178.970001 | 176.285675 | 38162200 |
1 | 2018-02-27 | AAPL | 179.100006 | 180.479996 | 178.160004 | 178.389999 | 175.714386 | 38928100 |
2 | 2018-02-28 | AAPL | 179.259995 | 180.619995 | 178.050003 | 178.119995 | 175.448410 | 37782100 |
3 | 2018-03-01 | AAPL | 178.539993 | 179.779999 | 172.660004 | 175.000000 | 172.375214 | 48802000 |
4 | 2018-03-02 | AAPL | 172.800003 | 176.300003 | 172.449997 | 176.210007 | 173.567078 | 38454000 |
247 | 2019-02-20 | AAPL | 171.190002 | 173.320007 | 170.990005 | 172.029999 | 172.029999 | 26114400 |
248 | 2019-02-21 | AAPL | 171.800003 | 172.369995 | 170.300003 | 171.059998 | 171.059998 | 17249700 |
249 | 2019-02-22 | AAPL | 171.580002 | 173.000000 | 171.380005 | 172.970001 | 172.970001 | 18913200 |
250 | 2019-02-25 | AAPL | 174.160004 | 175.869995 | 173.949997 | 174.229996 | 174.229996 | 21873400 |
251 | 2019-02-26 | AAPL | 173.710007 | 175.300003 | 173.169998 | 174.330002 | 174.330002 | 17006000 |
xxxxxxxxxx
81# compute volatility using EWMA
2def getDailyVol(data, span=100):
3 df = data.assign(Return = lambda x: data['Adj Close'] / data['Adj Close'].shift(1)-1)
4 sigma = df['Return'].ewm(span=span).std()
5 return sigma
6
7vol = getDailyVol(data=data)
8view(vol)
xxxxxxxxxx
161The shape of the data is (252,)
2
30 NaN
41 NaN
52 0.001221
63 0.008831
74 0.010227
8247 0.023560
9248 0.023338
10249 0.023158
11250 0.022948
12251 0.022718
13Name: Return, dtype: float64
14
15#前两个都是 NaN,正常。
16#第一个 NaN 是因为 shift(1)。第二个 NaN 是因为不能在 1 个数据上计算 std()。
xxxxxxxxxx
71events = data[['Date']].copy(deep=True)
2events['VB'] = data['Date'] + pd.Timedelta(days=15)
3events['Vol'] = getDailyVol(data)
4
5#第一行初始化 events,将 data 里面的 'Date' 一栏的复制给它。
6#第二行用 TimeDelta(days=15) 函数,加在初始日期得到竖直隔栏对应的日期。
7#第三行用之前定义好的函数 getDailyVol() 来计算日波动率。
xxxxxxxxxx
201def TBL(df, events, width):
2
3 res = events[['Date', 'VB']].copy(deep=True)
4
5 if width[0] > 0: events['UB'] = width[0]*events['Vol']
6 else: events['UB'] = np.nan
7
8 if width[1] > 0: events['DB'] = -width[1]*events['Vol']
9 else: events['DB'] = np.nan
10
11 for col,date,vb in res.itertuples():
12 df0 = df[(df['Date'] > date) & (df['Date'] < vb)].copy(deep=True)
13 df0['Return'] = df0['Adj Close'] / df.loc[df['Date'] == date, 'Adj Close'].iloc[0]-1
14
15 idx = (res['Date'] == date)
16
17 res.loc[idx, 'ut'] = df0.loc[df0['Return'] > events.loc[idx,'UB'].iloc[0], 'Date'].min()
18 res.loc[idx, 'dt'] = df0.loc[df0['Return'] < events.loc[idx,'DB'].iloc[0], 'Date'].min()
19
20 return res
该函数为了计算上下水平隔栏对应的日期,用 result 来储存。
第 5 - 9 行计算上下水平隔栏的点位(level),用上述公式
其中 σ 是日波动率。而 width = [, ],它们都大于等于 0
当大于 0 时,乘上 σ 得到水平隔栏的点位,存储在 'UB' 和 'DB' 栏下。
当等于 0 时,表明不设定隔栏,那么隔栏的点位就设定为 NaN
第 12 - 13 行代码在每一个窗口都运行,即每一个起始日到它 15 天之后的竖直隔栏对应的日期,计算每天的收益率。
第 16 - 17 行检查每天的收益是否突破隔栏,突破了则记录第一次突破的时点,并储存起来,'' 代表第一次突破上隔栏日期,'' 代表第一次突破下隔栏日期。
xxxxxxxxxx
71def get_first_touch(df, events, width):
2 res = TBL(df, events, width)
3 res['First'] = res[['VB', 'ut', 'dt']].dropna(how='all').min(axis=1)
4 return res
5
6result = get_first_touch(data,events,width = [1,1])
7view(result)
Date | VB | ut | dt | First | |
---|---|---|---|---|---|
0 | 2018-02-26 | 2018-03-13 | NaT | NaT | 2018-03-13 |
1 | 2018-02-27 | 2018-03-14 | NaT | NaT | 2018-03-14 |
2 | 2018-02-28 | 2018-03-15 | 2018-03-09 | 2018-03-01 | 2018-03-01 |
3 | 2018-03-01 | 2018-03-16 | 2018-03-05 | NaT | 2018-03-05 |
4 | 2018-03-02 | 2018-03-17 | 2018-03-09 | NaT | 2018-03-09 |
247 | 2019-02-20 | 2019-03-07 | NaT | NaT | 2019-03-07 |
248 | 2019-02-21 | 2019-03-08 | NaT | NaT | 2019-03-08 |
249 | 2019-02-22 | 2019-03-09 | NaT | NaT | 2019-03-09 |
250 | 2019-02-25 | 2019-03-12 | NaT | NaT | 2019-03-12 |
251 | 2019-02-26 | 2019-03-13 | NaT | NaT | 2019-03-13 |
xxxxxxxxxx
141def get_label(df,result):
2 result = result.dropna(subset=['First'])
3 outcome = result[['Date']].copy(deep=True)
4
5 price_t0 = pd.merge(result,df,on=['Date'],how='left')['Adj Close']
6 price_t1 = pd.merge(result,df,left_on=['First'], right_on=['Date'], how = 'left')['Adj Close']
7
8 outcome['Return'] = price_t1/price_t0-1
9 outcome['Label'] = np.sign(outcome['Return'].dropna())
10
11 return outcome.dropna()
12
13outcome = get_label(data,result)
14view(outcome)
Date | Return | Label | |
---|---|---|---|
0 | 2018-02-26 | 0.005587 | 1.0 |
1 | 2018-02-27 | 0.000280 | 1.0 |
2 | 2018-02-28 | -0.017516 | -1.0 |
3 | 2018-03-01 | 0.010400 | 1.0 |
4 | 2018-03-02 | 0.021395 | 1.0 |
236 | 2019-02-04 | 0.002412 | 1.0 |
237 | 2019-02-05 | -0.008108 | -1.0 |
238 | 2019-02-06 | -0.014040 | -1.0 |
239 | 2019-02-07 | 0.016215 | 1.0 |
241 | 2019-02-11 | 0.028330 | 1.0 |
xxxxxxxxxx
171# Visualization
2
3# reformat the data with labels
4df_label = pd.merge(data,outcome,on = 'Date')[['Date', 'Adj Close', 'Label']].set_index('Date')
5
6# find the buy/sell signals
7pos_idx = df_label[df_label['Label'] == 1].index
8pos_val = df_label['Adj Close'][pos_idx]
9
10neg_idx = df_label[df_label['Label'] == -1].index
11neg_val = df_label['Adj Close'][neg_idx]
12
13# visualization
14df_label['Adj Close'].plot(figsize = [22,8])
15plt.scatter(pos_idx,pos_val,marker = '^', alpha = 0.8, edgecolors='orange', c = 'red', s = 50, label = 'buy')
16plt.scatter(neg_idx,neg_val,marker = 'v', alpha = 0.8, c = 'green', s = 50, label = 'sell')
17plt.legend()