1.1 研究概述
在 Force Avatar"HMM 在股票上的简单应用"一文中,利用隐马尔科夫链模型( Hidden Markov Model, HMM)对沪深 300 指数预测建模,取得了较为理想的效果。本文基于该文章中的源代码,对模型所使用的可观测序列的正态性进行分析和修正,并对隐藏状态数目的选择作了简单的探讨。分析结果表明,对于可观测序列正态性的修正,及合理的隐藏状态数目选择,能够显著提升模型的预测能力。
2.2 问题分析
2.1 可观测序列的正态性分析和处理
在 Force Avatar 的文章中,选取了三个可观测序列(每日最高最低价格的对数差值, 每 5 日的指数对数收益差和每 5 日的指数成交量的对数差)进行可观测序列进行建模,并假设这三个指标服从正态分布。
在基于正态分布可观测序列的 HMM 模型中,对于参数的估计(概率转移矩阵,概率发射矩阵)和隐藏状态的标注是通过极大似然估计实现的,其最大似然函数表达式中包含了多元正态分布的概率密度函数。如果所选择的可观测序列偏离正态分布,将导致参数估计有偏,和对于隐藏状态的标注出现较高的错误率。
通过直方图观察所选的三个可观测序列的分布,发现第一个可观测序列(每日最高最低价格的对数差值)较为显著地偏离正态分布。因此对其进行 Box-Cox Transformation 数据转换的方法,使其更接近正态分布。
此外,在经验协方差矩阵计算过程中,具有较大方差的可观测序列将具有较大的权重。如果可观测序列的单位不同,其方差的差距可能达到数个数量级。这里为了保证所选的可观测序列在建模中具有接近的权重,对其分布进行标准化(均值调整为 0 ,标准差调整为 1 个单位)。
2.2 隐藏状态数量的选择
在广发证券的一篇 HMM 模型研究报告中,隐藏状态的数目设定为 3 个。 探寻西蒙斯投资之道:基于 HMM 模型的周择时策略研究
文中并未提及选择三个隐藏状态的原因。在 Force Avatar 所提到的 HMM 模型所关心的三个问题中,隐藏状态均假设为已知。由于目前尚未查到关于隐藏状态数目选择的相关研究文献,目前对于隐藏状态对于建模的影响为个人推测:
以 Force Avatar 一文中用掷骰子作为例子解释 HMM 模型,其中骰子的数目为隐藏状态数目,抛掷结果为可观测序列。如果我们用 3 颗不同的骰子抛掷获得一个观测序列,但在建模中我们假设隐藏状态数目为 5 个,因为 HMM 无法识别隐藏状态的合理数目,因此对于任意给定的隐藏状态数目和初始分布,模型总能通过反复迭代对参数进行估计,和对隐藏状态进行标注。因此我们最终得到的 5 个隐藏状态中,有 3 颗将会对应真骰子,而另外 2 颗则是这 3 颗真骰子线性组合得到的假骰子。
由于这 2 颗假骰子是真骰子的线性组合,因此它们可能和真骰子以大致相等的概率导致同一观测序列,从而导致隐藏状态的标准错误,和概率转移矩阵和概率发射矩阵参数估计的不稳定。
对于证券市场来说,隐藏状态有无穷多个。在建模时,我们希望每个隐藏状态有相互独立的特征,保证我们能对市场状态有清楚的判断,作出投资决策。合理隐藏状态的选取是一个较为复杂的问题, HMM 模型对于隐藏状态标注的准确度取决于可观测序列的数目,数据的数量和质量,迭代次数,数据分布的稳定性等多种因素。
大体上,我们可以认为市场有三种显著不同的特征:快速上升,快速下跌和震荡。直觉上,震荡这种状态最为复杂,可能有不同的亚隐藏状态,例如震荡上升,震荡下跌等等复杂的的市场状态。而在 Force Avatar 得到的结果中,震荡行情中出现多种状态的标注,导致我们无法准确判断隐藏状态对应的含义。因此,在目前的建模中,基于以下理由把隐藏状态设定为 3 个:
1.1 希望模型能够正确标注快速上升,快速下跌和震荡三种市场状态,并不期望目前的简单模型能够对震荡行情中的复杂状态准确标注。
2.2 震荡行情所包含的隐藏状态的标注较为复杂,标注的准确率较低,考虑到交易中实际产生的交易费用,我们希望模型标准的隐藏状态的含义是清晰和准确的,而不希望基于不清晰的隐藏状态含义进行交易。
3.3 结果讨论
基于以上讨论,在以下的源代码中,我们分别对未修正的可观测序列和修正后的可观测序列进行建模。我们先对 3 隐藏状态 HMM 模型和 6 隐藏状态 HMM 模型进行对比。根据 3 个隐藏状态得到的模型进行交易,在 2013 年初左右收益率即达到 100%, 2014 年初达到 150%,根据 6 个隐藏状态得到的模型进行交易,在 2015 年初左右收益率达到 100%, 2015 年中达到 150%;此外,在 2015 年下半年的股灾中,根据 3 隐藏状态模型进行交易,收益率仍然维持在 200%左右,而根据 3 隐藏状态模型进行交易,收益率仍然跌至 170%左右
然后,我们对基于未修正可观测序列的 3 隐藏状态 HMM 模型和基于修正可观测序列的 3 隐藏状态 HMM 模型进行对比。这两个 HMM 模型中均能对快速上升,快速下跌和震荡行情三种市场状态进行较为准确的标准。基于修正可观测序列的 HMM 模型在 2015 年上半年的牛市中收益率达到 300%,远高于基于未修正可观测序列的 HMM 模型的收益率;且在 2015 年下半年的股灾中,基于修正可观测序列的模型表现出色,在稍微回调以后重新上升,在 2015 年年底和其峰值大致持平。
3.3 研究展望
在本文中,我们对于 HMM 模型的隐藏状态数目选择进行了讨论,并对所选的可观测序列正态性进行了分析和处理,调整以后的 HMM 对于沪深 300 的测试结果显著改善。
在下一步的研究中,可以对模型的以下方面作进一步改善:
1.1 对建模时选择的 2000 步迭代合理性进行检验。理想的迭代数目应该使参数估计和隐藏状态标注的准确性收敛。
2.2 选择更多的尽可能正交的,符合正态分布可观测序列序列向量作为指标进行建模。理论上,相互正交的可观测序列向量比存在线性相关的可观测序列向量包含更多的市场信息,且和 HMM 模型中的观测独立性假设一致,同时能够降低经验协方差矩阵非对角元素估计的误差。
3.3 对符合正态分布的可观测序列进行方差分析,判断其可合理使用的时间范围,并通过检验结果进一步了解市场结构的变化。
通过交叉验证( Cross-Validation )进一步确定合理的隐藏状态数目。
最后,在此对于 Force Avatar 分享自己的研究策略提出致谢。
HMM_Model_Modified_3_Hidden_States.ipynb
以下为从“ HMM 在股票上的简单应用”一文中获得的源代码,其中唯一的修改是隐藏状态数从 6 变为 3 。
In [ ]:
from hmmlearn.hmm import GaussianHMM
import numpy as np
from matplotlib import cm, pyplot as plt
import matplotlib.dates as dates
import pandas as pd
import datetime
from scipy import stats # To perfrom box-cox transformation
from sklearn import preprocessing # To center and standardize the data.
# Source Code from previous HMM modeling
# Note that numbers of hidden states are modified to be 3, instead of 6.
beginDate = '2005-01-01'
endDate = '2015-12-31'
n = 3 # Hidden states are set to be 3 instead of 6
data = get_price('CSI300.INDX',start_date=beginDate, end_date=endDate,frequency='1d')
data[0:9]
volume = data['TotalVolumeTraded']
close = data['ClosingPx']
logDel = np.log(np.array(data['HighPx'])) - np.log(np.array(data['LowPx']))
logDel
logRet_1 = np.array(np.diff(np.log(close)))#这个作为后面计算收益使用
logRet_5 = np.log(np.array(close[5:])) - np.log(np.array(close[:-5]))
logRet_5
logVol_5 = np.log(np.array(volume[5:])) - np.log(np.array(volume[:-5]))
logVol_5
logDel = logDel[5:]
logRet_1 = logRet_1[4:]
close = close[5:]
Date = pd.to_datetime(data.index[5:])
通过直方图来观察所选指标(可观测序列)分布的正态性。其中第一个指标(每日最高最低价格的对数差值)明显偏离正态分布。
In [53]:
# the histogram of the raw observation sequences
n, bins, patches = plt.hist(logDel, 50, normed=1, facecolor='green', alpha=0.75)
plt.show()
n, bins, patches = plt.hist(logRet_5, 50, normed=1, facecolor='green', alpha=0.75)
plt.show()
n, bins, patches = plt.hist(logVol_5, 50, normed=1, facecolor='green', alpha=0.75)
plt.show()
In [ ]:
通过 Box-Cox Transformation 来对第一个指标进行调整,使其更接近正态分布。
同时对三个指标的分布进行标准化(调整其均值为 0 ,且标准差调整为 1 ),保证指标在参数估计中具有大致相等的权重。
In [54]:
# Box-Cox Transformation of the observation sequences
boxcox_logDel, _ = stats.boxcox(logDel)
# Standardize the observation sequence distribution
rescaled_boxcox_logDel = preprocessing.scale(boxcox_logDel, axis=0, with_mean=True, with_std=True, copy=False)
rescaled_logRet_5 = preprocessing.scale(logRet_5, axis=0, with_mean=True, with_std=True, copy=False)
rescaled_logVol_5 = preprocessing.scale(logVol_5, axis=0, with_mean=True, with_std=True, copy=False)
# the histogram of the rescaled observation sequences
n, bins, patches = plt.hist(rescaled_boxcox_logDel, 50, normed=1, facecolor='green', alpha=0.75)
plt.show()
n, bins, patches = plt.hist(rescaled_logRet_5, 50, normed=1, facecolor='green', alpha=0.75)
plt.show()
n, bins, patches = plt.hist(rescaled_logVol_5, 50, normed=1, facecolor='green', alpha=0.75)
plt.show()
In [55]:
# Observation sequences matrix
A = np.column_stack([logDel,logRet_5,logVol_5])
# Rescaled observation sequences matrix
rescaled_A = np.column_stack([rescaled_boxcox_logDel, rescaled_logRet_5, rescaled_logVol_5])
对于未修正的指标进行隐马尔科夫链建模。
In [64]:
# HMM modeling based on raw observation sequences
model = GaussianHMM(n_components= 3, covariance_type="full", n_iter=2000).fit([A])
hidden_states = model.predict(A)
hidden_states
plt.figure(figsize=(25, 18))
for i in range(model.n_components):
pos = (hidden_states==i)
plt.plot_date(Date[pos],close[pos],'o',label='hidden state %d'%i,lw=2)
plt.legend(loc="left")
In [67]:
for i in range(3):
pos = (hidden_states==i)
pos = np.append(0,pos[:-1])#第二天进行买入操作
df = res.logRet_1
res['state_ret%s'%i] = df.multiply(pos)
plt.plot_date(Date,np.exp(res['state_ret%s'%i].cumsum()),'-',label='hidden state %d'%i)
plt.legend(loc="left")
In [83]:
long = (hidden_states==0) #做多
short = (hidden_states == 1) #做空
long = np.append(0,long[:-1]) #第二天才能操作
short = np.append(0,short[:-1]) #第二天才能操作
res['ret'] = df.multiply(long) - df.multiply(short)
plt.plot_date(Date,np.exp(res['ret'].cumsum()),'r-')
Out [83]:[<matplotlib.lines.Line2D at 0x7fc541b9d1d0>]
In [ ]:
对修正后的指标进行隐马尔科夫链建模。
In [71]:
# HMM modeling based on processed observation sequences
rescaled_model = GaussianHMM(n_components= 3, covariance_type="full", n_iter=2000).fit([rescaled_A])
rescaled_hidden_states = rescaled_model.predict(rescaled_A)
rescaled_hidden_states
plt.figure(figsize=(25, 18))
for i in range(model.n_components):
pos = (rescaled_hidden_states==i)
plt.plot_date(Date[pos],close[pos],'o',label='hidden state %d'%i,lw=2)
plt.legend(loc="left")
In [73]:
for i in range(3):
pos = (rescaled_hidden_states==i)
pos = np.append(0,pos[:-1])#第二天进行买入操作
df = res.logRet_1
res['state_ret%s'%i] = df.multiply(pos)
plt.plot_date(Date,np.exp(res['state_ret%s'%i].cumsum()),'-',label='hidden state %d'%i)
plt.legend(loc="left")
In [74]:
long = (rescaled_hidden_states==0) #做多
short = (rescaled_hidden_states==1) + (rescaled_hidden_states == 2) #做空
long = np.append(0,long[:-1]) #第二天才能操作
short = np.append(0,short[:-1]) #第二天才能操作
res['ret'] = df.multiply(long) - df.multiply(short)
plt.plot_date(Date,np.exp(res['ret'].cumsum()),'r-')
Out [74]:[<matplotlib.lines.Line2D at 0x7fc541b4fe48>]