本文关于为金融数据打标签的方法逐一介绍
由于金融数据的高噪声和序列自相关性很强,很难预测股价的连续值,那么只能为数据打标签,从而预测离散值。
几乎所有机器学习文献都使用了固定时间区间(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 23import pandas as pd4import numpy as np5import seaborn as sns6import matplotlib.pyplot as plt78# load the data910data = pd.read_csv('1Y_Stock_Data.csv', parse_dates=[0], dayfirst=True)1112# get a glimpse of the data13def view(data):14 print('The shape of the data is',data.shape)15 return data.head().append(data.tail())1617# get AAPL data18data = 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 |
xxxxxxxxxx81# compute volatility using EWMA2def 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 sigma67vol = getDailyVol(data=data)8view(vol)xxxxxxxxxx161The shape of the data is (252,)230 NaN41 NaN52 0.00122163 0.00883174 0.0102278247 0.0235609248 0.02333810249 0.02315811250 0.02294812251 0.02271813Name: Return, dtype: float641415#前两个都是 NaN,正常。16#第一个 NaN 是因为 shift(1)。第二个 NaN 是因为不能在 1 个数据上计算 std()。
xxxxxxxxxx71events = data[['Date']].copy(deep=True)2events['VB'] = data['Date'] + pd.Timedelta(days=15)3events['Vol'] = getDailyVol(data)45#第一行初始化 events,将 data 里面的 'Date' 一栏的复制给它。6#第二行用 TimeDelta(days=15) 函数,加在初始日期得到竖直隔栏对应的日期。7#第三行用之前定义好的函数 getDailyVol() 来计算日波动率。xxxxxxxxxx201def 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.nan7 8 if width[1] > 0: events['DB'] = -width[1]*events['Vol']9 else: events['DB'] = np.nan10 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]-114 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 行检查每天的收益是否突破隔栏,突破了则记录第一次突破的时点,并储存起来,'' 代表第一次突破上隔栏日期,'' 代表第一次突破下隔栏日期。
xxxxxxxxxx71def 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 res56result = 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 |
xxxxxxxxxx141def 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-19 outcome['Label'] = np.sign(outcome['Return'].dropna())10 11 return outcome.dropna()1213outcome = 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 |
xxxxxxxxxx171# Visualization23# reformat the data with labels4df_label = pd.merge(data,outcome,on = 'Date')[['Date', 'Adj Close', 'Label']].set_index('Date')56# find the buy/sell signals7pos_idx = df_label[df_label['Label'] == 1].index8pos_val = df_label['Adj Close'][pos_idx]910neg_idx = df_label[df_label['Label'] == -1].index11neg_val = df_label['Adj Close'][neg_idx]1213# visualization14df_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()