自 2018 年末以来,全球金融基本盘由早期的稳步上升变得起伏不定。由于投资者对市场走向和未来展望的不确定,这样的市场情绪带动着大盘和指数起起伏伏。同时中美贸易战给零售带来的巨大关税压力让市场情绪再度低迷;2020 年初,新冠肺炎疫情在全球肆虐蔓延,造成医疗资源紧张,疫情防控措施使商品和人员流动速度减慢,多地商店停业甚至倒闭,大大影响了人们的经济生活,进一步导致全球各地股票市场出现熔断甚至跌停的状况。面对动荡的金融市场,政府和金融监管机构推行了各式各样的政策以应对疫情造成的影响,稳住经济与股票基本盘。
对于许多金融从业者而言,市场的动荡意味着新的投资机遇,而 2008 年金融危机的教训也时刻提醒着人们,机遇背后往往伴随着巨大的风险。著名的《巴塞尔协定 III》就是在应对全球金融危机、强化金融监管的背景下应运而生的,它也是现阶段被广泛运用于各大金融机构的全球金融监管标准之一。如何识别风险、保证资产安全并保持其相应的流动性一直都是风险管理中重要的组成部分。在股票市场的风险管理中,金融从业者需要先查看价格变化是否在预期范围内,并判断这样的变化是由系统风险还是政策变化或重大事件所产生。如非系统风险,金融从业者则需要在对政策或重大事件充分的了解后,决定是否需要调整当下的策略,买入或出售相应的资产来保障机构金融层面的安全。判断系统风险的方法有多种,其中较为流行的方法是通过历史价格信息来预测未来价格,并比较真实价格与预测结果的差距。在本篇中,我们将使用这样的方法进行股价预测,并与真实价格进行对比。
解决方案结构
为了实现对历史数据进行建模来预测未来价格,本篇采用如下结构的解决方案,同时大部分数据科学的项目也都可以使用类似的结构来完成:
- 确定预测目标
- 数据收集
- 探索性数据分析
- 数据预处理
- 特征工程
- 模型选择和训练
- 模型评估
- 模型预测
确定预测目标
在股票市场中,波动较大的股票不占少数。为了解市场状况,金融从业人员会倾向于研究分析股票指数而非单个股票的价格。因为指数相对于单个股票来说更稳定,也更利于精准地建模。指数通常由多种股票构成,一般反映市场绩效的是广基指数,如道琼斯指数、标普 500 指数、日经指数、恒生指数等。这样的指数既可以反映出股票基本盘状况,又可以反映出投资人对经济现况的敏感度。在本篇中,我们选择的目标预测指数是标普 500 指数,是全球最具标志性的追踪美国高市值公司的股票指数之一。
数据收集
收集金融数据的平台众多,其中像 Bloomberg、Qliq、Quandl 这样的国外市场平台,抑或是如同花顺、万德这样的国内平台都受到大众的喜爱。大部分企业为了保证数据的准确性、安全性和时效性,在生产环境下会使用成熟的收费平台。在本篇中,我们使用的是 Yahoo Finance,一个免费的公开金融数据平台,它不仅包含了大部分公开股票的实时数据,还提供了 Python API,可以通过股票代码和时间区间直接查询历史价格。
import yfinance as yf
df_sp = yf.download('^GSPC',start="2012-11-01",end="2022-11-01")
Pandas 数据集 “df_sp” 包含了从 2012 年 11 月 1 日至 2022 年 10 月 31 日十年以来的标普 500 指数数据,记录了每个日期所对应的开市价格、最高价格、最低价格、闭市价格、闭市调整价格和交易量。
探索性数据分析
在收集完需要的数据后,首先可以查看数据集中可能存在的空缺记录和需要调整的字段。我们使用以下函数来了解数据集中包含多少条记录、是否有空缺值以及每个字段所对应的数据类型。
df_sp.info()
可以看到,数据集中没有需要处理的空缺值,并且每个字段的类型都是统一的。除日期以外(Python Datetime 类型格式),其他字段都是数字类型。价格相关的都为浮点数,交易量相关的为整数。接下来,我们通过以下函数查看数据的统计类信息。
df_sp.describe()
可以看到,指数数据都是正数,大部分以千为单位。唯一在单位上与其他列有所区别的是交易量数据。交易量的绝对数值与价格相比相差巨大,为了更有可比性,我们需要调整交易量的绝对数值大小。在本步骤中,我们选择先将交易量除以 1000000,使其变成以百万为单位,那么在数值上,交易量数值大小缩小至千,与价格相似。
初步分析数据后,我们可以将价格信息的时间序列可视化,更加直观地了解价格趋势。
通过图像,我们发现开市、闭市、最高、最低和闭市调整的价格在趋势上非常相似;交易量和价格之间也有一定的联系。我们需要对交易量和价格做进一步的分析来明确两者之间的关系。这里,我们使用相关性矩阵(correlation matrix)来研究变量之间的关系。
由此可以看出,交易量与价格之间没有非常高的相关性,但这并不能证明交易量不应该在价格预测中被考虑。从另一个角度来说,各价格变量之间的相关性非常高,我们只需挑选其中一个(闭市调整价格)进行预测即可。在下一步中,我们只保留需要用到的信息即可。
df = df_sp[['Adj Close','Volume_in_M']]
另一个值得注意的特征是,相比于 2020 年之前总体上升的趋势,指数价格在 2020 至 2021 年间呈下降趋势。我们需要放大这段时间的数据图像来了解具体情况。
2020 年 3 月,由于疫情的蔓延,全球多个国家都开始实行封控政策。从结果可以看出市场对封控政策的反响巨大,但与此同时多地政府也采取了各式各样的经济政策来稳定市场,人们的生产生活在疫情后开启了新的模式。在这里,我们假设疫情的一系列政策会在短时间内持续下去。根据公共信息的评估,大家可以酌情调整模型和假设。在本项目中,我们的目标是使用自 2020 年 2 月 15 日起的历史数据,预测未来 1 个工作日的调整后闭市指数价格。
至此,我们完成了从原数据集中提取后续建模所需数据的全部步骤。为了发挥提取数据的最大价值,我们需要对现有的两个变量进行一些处理,从而尽可能完整地展现数据的特性。特别是对于时间序列数据而言,季节性和周期性都是非常重要的信息,而这些信息是可以从时间序列本身提取的。必要的数据处理虽然会增加计算量,但可以换来更精准更完善的模型结果。
数据预处理
由于指数数据是时间序列,相比于大多数回归预测,我们不仅要考虑模型需要什么样的变量,也需要考虑时间顺序。在本篇中,回归预测的数学公式如下,对于需要预测的未来时间为 t+1:
即运用 n 条历史记录来预测未来的价格。
为了提取有效信息辅助预测,我们将数据预处理大致分为四步:
- 将时间转换为变量
- 更改价格数据
- 寻找周期和季节性
- 根据周期调整交易量数据
1. 将时间转换为变量
首先,我们需要将时间信息从索引中提取出来。
df_mod_forecast = df_mod.reset_index()
df_mod_forecast.info()
从日期信息中,提取年、月、日和工作日信息,经过处理后的数据如下:
df_mod_forecast.head()
2. 更改价格数据
本篇的预测目标是未来价格,对于指数而言,金融从业者相对看重的是价格变动而非是其数值的大小。我们可以选择预测价格,也可以选择预测价格变动。我们将根据数据本身的特性来决定二者当中谁更适合建模。
首先,我们可以将每日和前一个工作日的价格差百分比计算出来。
df_mod_forecast['AdjPricePctDelta'] = df_mod_forecast['Adj Close'].pct_change()
3. 寻找周期和季节性 — 解构时间序列与自相关分析
指数价格数据是以日期为单位的,且只有在工作日才有数据。我们在尝试周期时可以有多种选择,例如以周、月或者季度为单位。在本篇中,时间序列分解效果最好的是月份。从 2020 年 2 月到 2022 年 11 月大约有 20 个月的时间。合适的周期会帮助我们更好的寻找数据中的季节性。 注意,在之前的时间序列图像中我们注意到,全球疫情蔓延之后,股票市场表现出高于平常的波动。我们需要使用 multiplicative 模型来解决这个问题。我们首先尝试分解价格的时间序列。
通过以上图像可以看出,每 100 天大约有 5 个周期,每个周期大约有 20 天。除此之外,价格数据残差(residual)部分表现并不佳,徘徊在 1 左右。在这个基础上,我们再进行自回归分析,价格的 ACF 和 PACF 图像如下:
ACF 和 PACF 显示近 30 天的历史价格都对当下的价格有影响,而太多的信息会影响模型的效率,甚至带来过拟合的风险。我们再来看看每日价格变动的解构结果。注意,因为价格差有负值,multiplicative 模型并不适用,我们需要使用 additive 模型来进行时间序列分解。
价格百分比差也表现出类似的季节性。残差比之前表现略有进步,徘徊在 0 左右,但是可以看出 2 月份左右的数据波动性远高于平常。价格变动的 ACF 和 PACF 如下:
价格差的 ACF 和 PACF 中,我们可以看到近两天的历史价格变动对当天价格的影响比较大。在本项目中,前两天的历史信息将纳入模型中考虑。根据本节的发现,我们可以在原先的基础上进一步处理数据,加入季节信息。 我们先计算出大致需要多少个周期,以 0-20 的正整数为一周期填充数列,就可以得到代表周期性的变量。
n_seasonal_cycles = math.ceil(df_mod_forecast.shape[0]/20)
df_mod_forecast['seasonality']=(list(range(0,20))*n_seasonal_cycles[:df_mod_forecast.shape[0]]
4. 根据周期调整交易量数据
时间序列较为传统的模型为 ARIMA 模型,这类模型只需要一条时间序列,根据数据周期性等特征调试参数。时间序列也可以使用机器学习进行建模,这样的模型可以输入更多的变量,但需要对数据进行调整。和 ARIMA 类型的模型不同,我们需要使用 t-1 的价格波动和 t-2 的价格波动作为变量的一部分,输入模型进行训练。
在这里我们需要运用 Pandas 的 shift 方程,将价格变动数据分别下移一个和两个时间间隔。Shift 方程需要索引作为时间的依据,因此我们需要将日期设为索引。根据日期,将价格变动数据下移。
df_model['t_1_PricePctDelta']=df_model['AdjPricePctDelta'].shift(periods=1)
df_model['t_2_PricePctDelta']=df_model['AdjPricePctDelta'].shift(periods=2)
以此类推,我们将交易量数据分别下移一个和两个时间间隔,两者求差即可得到每个交易日昨天(t-1)与前天(t-2)之间的交易量差。
df_model['t-1volume'] = df_model['Volume_in_M'].shift(periods=1)
df_model['t-2volume'] = df_model['Volume_in_M'].shift(periods=2)
df_model['t_1_VolumeDelta'] = df_model['t-1volume'] - df_model['t-2volume']
通过交易日前一天的价格变动,我们可以将其转换为涨跌符号来作为模型输入的一部分。
df_model['sign_t_1']=np.where(df_model['t_1_PricePctDelta']>0,1,0*df_model['t_1_PricePctDelta'])
至此,我们完成了标普 500 指数数据从下载到转换的全过程。
在下篇中我们将着手完成模型训练和预测步骤,根据模型来预测未来一个工作日的价格变化,并将预测结果与真实情况进行比对。
参考资料
- Time series into supervised learning problem
- Tuning Ada Boost
- S&P 500 historical data
- Bloomberg News
- 阿布(2021)。《量化交易之路 用 Python 做股票量化分析》。机械工业出版社。