qsq加密货币量化系统: 隐马尔可夫模型

前言

在前几周的业余时间,我了解了隐马尔可夫模型,并阅读了一些前人在这个模型上的实践。这些实践都很有启发意义,但是对于我个人而已在以下几点上没有满足我的预期。

  1. 很多代码没有开源亦或者依赖特定的平台
  2. 部分专讲理论,部分专门讲实践,两者都照顾到的似乎很少
所以这里我准备先介绍隐马尔科夫模型,接下来在qsq中实现这个模型,并希望能作为一个基础模块来让使用者轻松调用。

项目地址

https://github.com/qiushui777/qsq

隐马尔科夫模型

模型实例

刘建平Pinard使用了《统计学习方法》中的一个例子来进行解释,我觉得简单易懂。hmm(隐马尔科夫模型)中较为重要的几个部分包括初始状态分布、状态转移概率分布矩阵、观测状态概率矩阵。假设我们有3个盒子,每个盒子里都有红色和白色两种球,这三个盒子里球的数量分别是:
盒子 1 2 3
红球数 5 4 7
白球数 5 6 3

按照下面的方法从盒子里抽球,开始的时候,从第一个盒子抽球的概率是0.2,从第二个盒子抽球的概率是0.4,从第三个盒子抽球的概率是0.4。以这个概率抽一次球后,将球放回。然后从当前盒子转移到下一个盒子进行抽球。规则是:如果当前抽球的盒子是第一个盒子,则以0.5的概率仍然留在第一个盒子继续抽球,以0.2的概率去第二个盒子抽球,以0.3的概率去第三个盒子抽球。如果当前抽球的盒子是第二个盒子,则以0.5的概率仍然留在第二个盒子继续抽球,以0.3的概率去第一个盒子抽球,以0.2的概率去第三个盒子抽球。如果当前抽球的盒子是第三个盒子,则以0.5的概率仍然留在第三个盒子继续抽球,以0.2的概率去第一个盒子抽球,以0.3的概率去第二个盒子抽球。如此下去,直到重复三次,得到一个球的颜色的观测序列:

O = {红,白,红}

这个过程中,观察者只能看到球的颜色序列,却不能看到球是从哪个盒子里取出的。(这里我补充一句,在我们量化的过程中,所想要知道或者猜测的就是当前这个盒子是哪个,也就是当前市场的状态是什么,从而做出相应的策略。)

这个模型中,我们的观察集合是

V = {红,白}, M=2

我们的状态集合是

Q = {盒子1,盒子2,盒子3},N=3

初始状态分布为

Π = (0.2,0.4,0.4)T

状态转移概率分布矩阵为

$$
\begin{matrix}
0.5 & 0.2 & 0.3 \
0.3 & 0.5 & 0.2 \
0.2 & 0.3 & 0.5
\end{matrix}
$$

观测状态概率矩阵为

$$
\begin{matrix}
0.5 & 0.5\
0.4 & 0.6\
0.7 & 0.3
\end{matrix}
$$

总结下这个模型我们需要理解的关键点就是,我们有初始状态分布、状态转移概率分布矩阵、观测状态概率矩阵,在模型实践的过程中,也就是取球的过程中,有一个观测序列O(取出的球的颜色),以及一个隐藏的状态序列(球所在的盒子)。如果我们能够知道现在球所在的盒子,那我们是不是就可以知道下一个是哪个盒子的概率以及取出球的颜色的概率?对应到我们金融市场的情况,大家也就一目了然了吧。

hmm模型的三个基本问题

1.评估观察序列概率。给定模型和观测序列,计算该模型下观测序列出现的概率。 2.模型参数学习问题。给定观测序列,估计模型参数,使得该模型下观测序列的条件概率最大。 3.预测问题,给定模型和观测序列,求给定观测序列下最可能出现的状态序列。 我们想利用量化模型来预测股市状态就要解决问题1和3。

简短总结

知道我们要解决的问题后,就是如何解决了。hmm模型的解法具体不细讲,因为我自己也不大清楚。有兴趣的朋友可以看下刘建平Pinard博客上的文章介绍,我后面的参考文献中有列出。这里我们需要知道的其实就是hmm怎么用在市场预测中。简单说,应用hmm的目的如下如果只是观测市场,我们只能知道当天的价格、成交量等信息,但是并不知道当前市场处于什么样的状态(牛市、熊市、震荡、反弹等等),在这种情况下我们有两个状态集合,一个可以观察到的状态集合(市场价格成交量状态等)和一个隐藏的状态集合(市场状况)。我们希望能找到一个算法可以根据股市价格成交量状况和马尔科夫假设来预测股市的状况。明白了这一点后,我们就可以化身“调包侠”,愉快地写代码了。

策略实现

增加hmm类

为了在qsq中加入一个隐马尔可夫策略,我在TacticsQs中加入了QsHmm类。具体看下这个类,主要是实现了以下几个接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class QsHmm(object):
"""
隐马尔可夫策略
"""
def __init__(self,crypto_df):
self.components = 6
self.model = None
self.iter = 1000
self.covariance_type = "diag" #convariance_type 包括有"full","spherical","diag","tied"
self.hidden_states = None
self.cryptodata = crypto_df #行情数据

def fit(self, fit_data):
"""
训练隐马尔可夫模型的接口
"""

def plot_hidden_states(self,cut):
"""
将隐藏状态和市场状态绘制在一起以显示每个隐藏状态的预测效果
此处的cut指的是训练时截取数据的偏移,如5day_logreturn,cut为5
"""

def plot_states_effect(self,cut):
"""
在每个隐藏状态为True的第二天买入,观察资产增值情况
此处的cut指的是训练时截取数据的偏移,如5day_logreturn,cut为5
"""

具体代码实现,大家可以自行阅读代码。

训练模型

获取数据

1
2
3
4
5
6
7
8
9
10
11
from __future__ import print_function
from __future__ import division

import warnings
warnings.filterwarnings('ignore')
warnings.simplefilter('ignore')

import os
import sys
# 使用insert 0即只使用github,避免交叉使用了pip安装的abupy,导致的版本不一致问题
sys.path.insert(0, os.path.abspath('../'))
1
2
3
4
5
6
import numpy as np

from locale import *
from qsq import QsCrypto, QsHmm, QsSignal, QsPickSignal, QsAccount

setlocale(LC_NUMERIC, 'English_US')
'English_United States.1252'

在实际编写策略的过程中我发现,不同的时间维度上,得出的隐藏状态可能差别很大,因为加密货币市场从最初的低价经历了17年的攀升,所以我这里没有分析17年前的数据。实际操练的过程中,大家可以试试看在全部数据上训练的结果,这种情况下会造成隐藏状态的效果很差。在近几年几乎没有任何作用。

1
2
3
bitcoin = QsCrypto('bitcoin')
bitcoin.crypto_df = bitcoin.crypto_df[-670:]
bitcoin.crypto_df

open close high low volume date MarketCap pre_close date_week p_change
2017-12-05 11685.70 11916.70 12032.00 11604.60 6,895,260,160 20171205 199,278,350,420 11657.20 1 2.226
2017-12-06 11923.40 14291.50 14369.10 11923.40 12,656,300,032 20171206 239,024,437,136 11916.70 2 19.928
2017-12-07 14266.10 17899.70 17899.70 14057.30 17,950,699,520 20171207 299,405,364,249 14291.50 3 25.247
2017-12-08 17802.90 16569.40 18353.40 14336.90 21,135,998,976 20171208 277,185,764,485 17899.70 4 -7.432
2017-12-09 16523.30 15178.20 16783.00 13674.90 13,911,300,096 20171209 253,946,084,745 16569.40 5 -8.396
... ... ... ... ... ... ... ... ... ... ...
2019-09-06 10578.20 10353.30 10898.76 10292.30 19,536,574,783 20190906 185,530,405,363 10575.53 4 -2.101
2019-09-07 10353.93 10517.25 10558.67 10348.92 15,307,366,476 20190907 188,488,525,926 10353.30 5 1.584
2019-09-08 10518.11 10441.28 10595.64 10409.09 13,670,567,493 20190908 187,150,078,007 10517.25 6 -0.722
2019-09-09 10443.23 10334.97 10450.31 10144.93 17,595,943,368 20190909 185,263,578,990 10441.28 0 -1.018
2019-09-10 10336.41 10115.98 10394.35 10020.57 14,906,809,639 20190910 181,360,731,383 10334.97 1 -2.119
2019-09-11 10123.03 10178.37 10215.95 9980.78 15,428,063,426 20190911 182,502,788,242 10115.98 2 0.617
2019-09-12 10176.82 10410.13 10442.25 10099.24 15,323,563,925 20190912 186,678,943,059 10178.37 3 2.277
2019-09-13 10415.36 10360.55 10441.49 10226.60 14,109,864,675 20190913 185,809,163,035 10410.13 4 -0.476
2019-09-14 10345.40 10358.05 10422.13 10291.69 13,468,713,124 20190914 185,784,300,332 10360.55 5 -0.024
2019-09-15 10356.47 10347.71 10387.03 10313.09 12,043,433,567 20190915 185,618,174,384 10358.05 6 -0.100
2019-10-01 8299.72 8343.28 8497.69 8232.68 15,305,343,413 20191001 149,913,972,768 8293.87 1 0.596
2019-10-02 8344.21 8393.04 8393.04 8227.70 13,125,712,443 20191002 150,823,797,662 8343.28 2 0.596
2019-10-03 8390.77 8259.99 8414.23 8146.44 13,668,823,409 20191003 148,448,162,840 8393.04 3 -1.585
2019-10-04 8259.49 8205.94 8260.06 8151.24 13,139,456,229 20191004 147,491,804,056 8259.99 4 -0.654
2019-10-05 8210.15 8151.50 8215.53 8071.12 12,200,497,197 20191005 146,529,229,668 8205.94 5 -0.663

670 rows × 10 columns

挑选特征

为了训练我们的隐马尔可夫模型,我们必须提供特征向量。这里我选取的是一日对数收益差、五日对数收益差、当日对数高低价差、当日的市值。这部分的选择我主要还是参考了优矿上的那篇文章,在我后面的研究中,我会对这些参数进行分析以提高我们的模型。
1
2
3
4
5
6
7
8
9
10
bitcoin.add_nday_logreturn(1)
bitcoin.add_nday_logreturn(5)
bitcoin.add_high_low_log()

high_low_log = bitcoin.crypto_df['high_low_log'].values[5:]
one_day_logreturn = bitcoin.crypto_df['1day_logreturn'].values[5:]
five_day_logreturn = bitcoin.crypto_df['5day_logreturn'].values[5:]
cap = bitcoin.crypto_df['MarketCap'].values[5:]
cap = [atof(num) for num in cap] #这里获取的市值是例如1,233,233这样的str,此步骤可以将其化为float
train_data = np.column_stack([high_low_log, one_day_logreturn, five_day_logreturn, cap])
1
train_data
array([[ 1.80977401e-01,  1.80982690e-02,  2.60017679e-01,
         2.58615394e+11],
       [ 1.28315702e-01,  9.15303125e-02,  1.69823814e-01,
         2.83439889e+11],
       [ 7.04850755e-02,  2.78661029e-02, -2.74290807e-02,
         2.91483936e+11],
       ...,
       [ 3.23432982e-02, -1.59794141e-02,  1.70484431e-03,
         1.48448163e+11],
       [ 1.32617885e-02, -6.56509461e-03,  1.24770704e-02,
         1.47491804e+11],
       [ 1.77340073e-02, -6.65632287e-03, -1.73147283e-02,
         1.46529230e+11]])

开始训练

1
2
hmm = QsHmm(bitcoin.crypto_df)
hmm.fit(train_data)

先调用第一个接口plot_hidden_states。这个接口的目的是为了展示我们的6个隐藏状态,同时汇出比特币的价格曲线,因为每天的隐藏状态只有一个,所以我们可以比较直观地看到价格趋势和隐藏状态地关系。这里函数需要传入地那个cut实际上是我们先前获取五日对数收益差时截取掉了最初地5天,所以我们这里输入了5。实际上,如果选择10天对数收益差,那么cut就是10。

1
hmm.plot_hidden_states(5)

接下来,我们调用plot_states_effect()接口。这个接口就是我们在每个隐藏状态出现后买入,并观察收益率。这个接口可以帮助我们快速挑选出显示当前处于上升亦或是下降趋势地隐藏状态。

1
hmm.plot_states_effect(5)

这里我们其实可以悲催地看到,这几个隐藏状态似乎都不怎么样,没有明确地表明上升和下降地趋势。单从实验角度,这里我选择状态1出现做多,状态0和状态2出现做空进行回测。

进行回测

要进行回测,利用到的自然是先前的择时策略中使用的QsSignal买卖信号和signal_backtest_percent回测函数了。为了方便地利用这个函数,我们将先前获得的隐藏状态写入Dataframe中。
1
2
3
bitcoin.crypto_df['hidden_states'] = None
for i in range(5,len(bitcoin.crypto_df)-1):
bitcoin.crypto_df.ix[i,'hidden_states'] = hmm.hidden_states[i-5]
1
bitcoin.crypto_df

open close high low volume date MarketCap pre_close date_week p_change 1day_logreturn 5day_logreturn high_low_log hidden_states
2017-12-05 11685.70 11916.70 12032.00 11604.60 6,895,260,160 20171205 199,278,350,420 11657.20 1 2.226 NaN NaN 0.036168 None
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2019-10-02 8344.21 8393.04 8393.04 8227.70 13,125,712,443 20191002 150,823,797,662 8343.28 2 0.596 0.005946 0.016965 0.019896 4
2019-10-03 8390.77 8259.99 8414.23 8146.44 13,668,823,409 20191003 148,448,162,840 8393.04 3 -1.585 -0.015979 0.001705 0.032343 4
2019-10-04 8259.49 8205.94 8260.06 8151.24 13,139,456,229 20191004 147,491,804,056 8259.99 4 -0.654 -0.006565 0.012477 0.013262 4
2019-10-05 8210.15 8151.50 8215.53 8071.12 12,200,497,197 20191005 146,529,229,668 8205.94 5 -0.663 -0.006656 -0.017315 0.017734 None

670 rows × 14 columns

1
2
3
4
5
6
7
8
9
class hmmsignal(QsSignal):
def produce_signal(self,param):
if self.crypto.crypto_df.loc[param]['hidden_states'] == 1:
return {'mode':1, 'symbol':self.crypto.symbol,'percent':1}
elif self.crypto.crypto_df.loc[param]['hidden_states'] == 0 or self.crypto.crypto_df.loc[param]['hidden_states'] == 2:
return {'mode':2, 'symbol':self.crypto.symbol,'percent':1}
else:
return {'mode':0, 'symbol':self.crypto.symbol,'percent':1}
mysignal = hmmsignal(bitcoin)
1
2
myaccount = QsAccount()
QsPickSignal.signal_backtest_percent(myaccount,mysignal,mysignal)
2019-10-06 21:16:39,846 - INFO - start back test...
2019-10-06 21:16:40,746 - INFO - 2018-12-11 buy bitcoin 291.85982843933505 price: 3424.59
2019-10-06 21:16:40,747 - INFO - The asset is 999500.2498750625
2019-10-06 21:16:40,759 - INFO - 2018-12-13 sell bitcoin 291.85982843933505 price: 3313.68
2019-10-06 21:16:40,759 - INFO - The asset is 966646.5112647044
2019-10-06 21:16:40,769 - INFO - 2018-12-15 buy bitcoin 298.4970864537159 price: 3236.76
2019-10-06 21:16:40,771 - INFO - The asset is 966163.4295499295
2019-10-06 21:16:40,784 - INFO - 2018-12-17 sell bitcoin 298.4970864537159 price: 3545.86
2019-10-06 21:16:40,785 - INFO - The asset is 1057899.6645332864
2019-10-06 21:16:40,808 - INFO - 2018-12-23 buy bitcoin 264.4101693541264 price: 3998.98
2019-10-06 21:16:40,809 - INFO - The asset is 1057370.9790437645
2019-10-06 21:16:40,818 - INFO - 2018-12-24 sell bitcoin 264.4101693541264 price: 4078.6
2019-10-06 21:16:40,819 - INFO - The asset is 1077884.1050693763
...
2019-10-06 21:16:41,181 - INFO - The asset is 1229539.6924601505
2019-10-06 21:16:41,199 - INFO - 2019-04-07 buy bitcoin 236.38177880806094 price: 5198.9
2019-10-06 21:16:41,200 - INFO - The asset is 1228925.229845228
2019-10-06 21:16:41,216 - INFO - 2019-04-11 sell bitcoin 236.38177880806094 price: 5064.49
2019-10-06 21:16:41,217 - INFO - The asset is 1196554.5783781586
2019-10-06 21:16:41,224 - INFO - 2019-04-12 buy bitcoin 234.98324015099982 price: 5089.54
2019-10-06 21:16:41,225 - INFO - The asset is 1195956.6000781197
2019-10-06 21:16:41,257 - INFO - 2019-04-23 sell bitcoin 234.98324015099982 price: 5572.36
2019-10-06 21:16:41,258 - INFO - The asset is 1308756.5024837814
2019-10-06 21:16:41,270 - INFO - 2019-04-26 buy bitcoin 247.77717924709526 price: 5279.35
2019-10-06 21:16:41,271 - INFO - The asset is 1308102.4512581525
2019-10-06 21:16:41,294 - INFO - 2019-05-03 sell bitcoin 247.77717924709526 price: 5768.29
2019-10-06 21:16:41,295 - INFO - The asset is 1428535.9999665874

可以看到,这两年多的时间内我们获得了1.5倍左右的收益,然而,我们知道,隐马尔可夫模型是一个机器学习模型,总是基于过去的数据进行回测的,所以在过去的数据上跑出1.5倍的收益其实没什么好惊奇的。如果不跑出收益才是奇怪。

总结和展望

到了这里,其实我们学习隐马尔可夫模型才刚刚开始,因为我们这个小研究有以下的不足。
  1. 状态选择过于粗糙
  2. 隐藏状态数目选择值得推敲
  3. 这里获得的隐藏状态效果并不明显
所以接下来,针对隐马尔可夫模型,还需要做以下的改进。
  1. 更改用于训练模型的状态
  2. 选择合适的隐藏状态数目

交流方式

email: xudong_shao#hotmail.com
qq群: 742593185

参考资料

1. 量价特征因子:基于HMM的多空策略 https://mp.weixin.qq.com/s/67A3swMZLaHc0r4ybG7O6g
2. 隐马尔科夫模型HMM(一)HMM模型 https://www.cnblogs.com/pinard/p/6945257.html
3. 单特征因子的隐马尔可夫模型在商品期货中的应用 https://zhuanlan.zhihu.com/p/34533310>
4. https://uqer.io/v3/community/share/56ec30bf228e5b887be50b35
5. https://www.cnblogs.com/pinard/p/7001397.html