量化投资学习笔记105——股价预测3:机器学习方法

主要采用支持向量机算法,参考两篇文章:
1.傅航聪,张伟.机器学习算法在股票走势预测中的应用.软件导刊,2017年10日第16卷第10期:31-34,46.
2.文成.基于机器学习方法的股票数据研究.重庆理工大学硕士学位论文(2011年).
主要参考第二篇。传统技术分析过多依靠主观判断。
文章使用了开盘价,收盘价,最高价,最低价,成交量,成交额这六个指标,先对数据进行归一化到[1,2]。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 准备数据
def readData(code="sh000300", start="2018-01-01", end="2018-12-31", refresh = False):
# 加载数据
tools.initOutput()
data = tools.loadData(code=code, start=start, end=end, refresh=refresh)
# 筛选特征
feature_cols = ["open", "close", "high", "low", "volume", "amount"]
target_cols = ["close"]
features_data = data.loc[:, feature_cols]
target_data = data.loc[:, target_cols].shift(-1)
target_data.fillna(method = "ffill", inplace = True)
print(features_data.info())
print(target_data.head())
scaler = MinMaxScaler(feature_range=(1, 2))
features = scaler.fit_transform(features_data.values)
target = scaler.fit_transform(target_data.values)
return (features, target)

将收盘价上移一行作为目标值,即预测第二天的收盘价。
在对数据没有先验知识的情况下,选择高斯径向基核函数。
先用默认参数让模型跑起来。
用2018年的数据进行训练,80%的训练集,20%测试集。
模型评分: 0.9551444471554572
再用2019年的数据进行验证:
验证数据模型评分: 0.7667456601663809
还不错。
改进一下,用今天以前的五天的数据来预测今天的收盘价。
遇到一个问题是如何准备训练特征,直接用每五天的数据组成新的特征,用第六天的收盘价作为目标值,进行训练,结果sklearn报错,称只接受小于等于二维的数据。于是直接用reshape函数把特征数据拉成二维,再进行训练。
测试数据的评分为:-1.3774486958961827
验证数据的评分:0.6959191032444751
验证数据评分降了很多,测试数据的模型评分居然成了负值。有没有办法改进呢?更换核函数看看,试了一圈,线性核函数效果最好,测试集评分0.35,验证集评分0.91。画图看看。

看着还行。但是再来比较一下模型预测涨跌的准确率吧。
模型预测涨跌准确率:0.495798,跟瞎猜一样的……可能是将五天数据合并成一天有问题吧,我是直接合并成一个列表。应该组合成一个五行六列的矩阵作为一个特征值,但是sklearn的fit只接受n×m的二维矩阵。要不试试用5天的平均数来作为特征值。尝试结果,是完全一样的。再做个试验:用前五天收盘价的均值作为第六天的预测值,看看结果如何。
结果是预测准确率0.516807,一样的。
现在进行调参,主要是惩罚参数C(默认值1.0)和核函数参数ε(取值范围[0.0001, 0.1], 默认值0.1)两个。
调参方法有穷举法,网格搜索法,交叉验证法,粒子群算法,遗传算法等。
先用网格搜索法。最佳参数:{‘C’: 1000, ‘epsilon’: 0.1, ‘kernel’: ‘linear’}
模型预测涨跌准确率:0.520833
是比前面提升了一点,但是提升太少。尝试缩小搜索的间隔,结果……
模型参数: {‘C’: 11, ‘epsilon’: 0.0901, ‘kernel’: ‘linear’}
最佳模型准确率评分 -1.023333747321061
模型预测涨跌准确率:0.479167
更差了!
试试其它方法。
粒子群算法(Particle Swarm Optimization,PSO),先看基础知识
是受鸟群觅食规律启发想出来的。其优点是收敛速度快、参数少、算法简单易实现。其基本思想是粒子之间共享信息使群体找到最优解。粒子有位置和速度两个属性,速度表示粒子下一步迭代的移动方向和距离,位置是所求问题的一个解。算法的六个参数:粒子速度、位置、个体最优解、群体最优解、个体历史最优适应值(个体搜索到最优解时的优化目标的函数值)、群体历史最优适应值。
算法流程(我就直接搬运上文的图啦,图源见水印,致谢!)

迭代时粒子速度更新的公式有三项,分别是惯性部分(对之前运动状态的继承),认知部分(粒子自己的经验),社会部分(其它粒子共享的经验)。
算法的参数有:粒子群规模N(推荐[20, 1000]),粒子维度D(自变量个数),迭代次数K(推荐[50, 100]),惯性权重w(不宜固定,越大,全局寻优能力越强,局部寻优能力越弱,推荐[0.4,2]),学习因子c1c2(c1粒子自己经验所占比重,c2群体经验所占比重)。
大致了解了一下原理,接下来就实操吧。找到一篇现成的用粒子群算法进行SVR调参的文章
照搬就行了。
具体到我们的SVR模型调参的问题,粒子群规模为每个参数取值范围大小,粒子群维度为待寻优参数的个数,粒子群位置为参数的具体数值,粒子群方向为参数取值变化方向,适应度为粒子对应的模型评价指标。gbest为全局历史最优解,pbest为粒子个体历史最优解。
粒子速度更新公式:
vi =w×vi + c1×rand()×(pbesti − x i) + c2×rand()×(gbest − xi)
位置更新公式:
xi =xi + vi
下面看具体代码:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# 粒子群算法进行SVR调参
class PSO:
# max_value和min_value分别为参数的最大/最小值
def __init__(self, particle_num, particle_dim, iter_num, c1, c2, w, max_value, min_value):
self.particle_num = particle_num
self.particle_dim = particle_dim
self.iter_num = iter_num
self.c1 = c1
self.c2 = c2
self.w = w
self.max_value = max_value
self.min_value = min_value
# 读取数据
features, target = readData()
self.X_train, self.X_test, self.y_train, self.y_test = splitTimeSeries(features, target)

# 粒子群初始化
def swarm_origin(self):
# 初始化随机数种子
random.seed(time.time())
particle_loc = []
particle_dir = []
for i in range(self.particle_num):
tmp1 = []
tmp2 = []
for j in range(self.particle_dim):
a = random.random()
b = random.random()
tmp1.append(a * (self.max_value[j] - self.min_value[j]) + self.min_value[j])
tmp2.append(b)
particle_loc.append(tmp1)
particle_dir.append(tmp2)

return particle_loc, particle_dir

# 计算适应度列表,更新pbest,gbest
def fitness(self, particle_loc):
fitness_value = []
# 适应度函数为模型预测正确率
for i in range(self.particle_num):
clf = svm.SVR(kernel = "linear", C = particle_loc[i][0], epsilon = particle_loc[i][1])
clf.fit(self.X_train, self.y_train)
y_pred = clf.predict(self.X_test)
fitness_value.append(testHighLow(self.y_test, y_pred))
# 当前粒子群最优适应度函数值和对应参数
current_fitness = 0.0
current_parameter = []
for i in range(self.particle_num):
if current_fitness < fitness_value[i]:
current_fitness = fitness_value[i]
current_parameter = particle_loc[i]

return fitness_value, current_fitness, current_parameter

# 粒子位置更新
def update(self, particle_loc, particle_dir, gbest_parameter, pbest_parameters):
# 计算新的粒子群方向和位置
for i in range(self.particle_num):
a1 = [x*self.w for x in particle_dir[i]]
a2 = [y*self.c1*random.random() for y in list(np.array(pbest_parameters[i]) - np.array(particle_loc[i]))]
a3 = [z*self.c2*random.random() for z in list(np.array(gbest_parameter) - np.array(particle_dir[i]))]
particle_dir[i] = list(np.array(a1) + np.array(a2) + np.array(a3))
particle_loc[i] = list(np.array(particle_loc[i]) + np.array(particle_dir[i]))
# 将更新后的粒子位置参数固定
parameter_list = []
for i in range(self.particle_dim):
tmp1 = []
for j in range(self.particle_num):
tmp1.append(particle_loc[j][i])
parameter_list.append(tmp1)
value = []
for i in range(self.particle_dim):
tmp2 = []
tmp2.append(max(parameter_list[i]))
tmp2.append(min(parameter_list[i]))
value.append(tmp2)

for i in range(self.particle_num):
for j in range(self.particle_dim):
particle_loc[i][j] = (particle_loc[i][j] - value[j][1])/(value[j][0] - value[j][1]) * (self.max_value[j] - self.min_value[j]) + self.min_value[j]

return particle_loc, particle_dir

# 画出适应度函数值变化图
@run.change_dir
def plot(self, results):
x = []
y = []
for i in range(self.iter_num):
x.append(i + 1)
y.append(results[i])
plt.figure()
plt.plot(x, y)
plt.xlabel("Number of iteration")
plt.ylabel("Value of fitness")
plt.title("PSO_SVM")
plt.savefig("./output/PSO_svm.png")
plt.close()

# 主函数
def main(self):
results = []
best_fitness = 0.0
# 粒子群初始化
particle_loc, particle_dir = self.swarm_origin()
# 初始化参数
gbest_parameter = []
for i in range(self.particle_dim):
gbest_parameter.append(0.0)
pbest_parameters = []
for i in range(self.particle_num):
tmp1 = []
for j in range(self.particle_dim):
tmp1.append(0.0)
pbest_parameters.append(tmp1)
fitness_value = []
for i in range(self.particle_num):
fitness_value.append(0.0)

# 迭代
for i in range(self.iter_num):
# 计算当前适应度函数值列表
current_fitness_value, current_best_fitness, current_best_parameter = self.fitness(particle_loc)
# 求当前最佳参数
for j in range(self.particle_num):
if current_fitness_value[j] > fitness_value[j]:
pbest_parameters[j] = particle_loc[j]
if current_best_fitness > best_fitness:
best_fitness = current_best_fitness
gbest_parameter = current_best_parameter

print("迭代次数:", i+1, " 最佳参数:", gbest_parameter, " 最佳适应度:", best_fitness)
results.append(best_fitness)
# 更新适应度值
fitness_value = current_fitness_value
# 更新粒子群
particle_loc, particle_dir = self.update(particle_loc, particle_dir, gbest_parameter, pbest_parameters)

# 结果展示
results.sort()
self.plot(results)
print("最终参数:", gbest_parameter)
return gbest_parameter


# 粒子群算法SVM调参
@run.timethis
def PSO_SVM():
particle_num = 100
particle_dim = 2
iter_num = 500
c1 = 0.5
c2 = 0.5
w = 0.8
max_value = [100, 1.0]
min_value = [1, 0.0001]
pso = PSO(particle_num,particle_dim,iter_num,c1,c2,w,max_value,min_value)
best_params = pso.main()
# 用新数据验证模型
model = svm.SVR(kernel = "linear", C = best_params[0], epsilon = best_params[1])
features, target = readData()
X_train, X_test, y_train, y_test = splitTimeSeries(features, target)
model.fit(X_train, y_train)
testModel(model)

运行结果

迭代次数: 500 最佳参数: [1.4696656179315561, 0.0001] 最佳适应度: 0.6041666666666666
最终参数: [1.4696656179315561, 0.0001] 验证数据模型评分: 0.7564837943259253 模型预测涨跌准确率: 0.495798
用预测涨跌的准确率作为适应度计算函数,最终结果还是等于瞎猜。

时间越长,预测结果与真实值相差越大。
改改粒子群算法参数看看。改变不大,准确率还是50%左右。
最后再试试遗传算法。它是借鉴了自然界的遗传,进化机制,通过遗传、交叉、变异、选择等,保留较优个体,淘汰较差个体,最终找到最优个体。选择、交叉、变异是遗传算法的基本操作。
还是找一篇现成的
尝试了半天,还是有bug没法顺利运行,于是再找找,果然有现成的库:scikit-opt。
用pip install scikit-opt安装。
文档地址
先测试一下。

1
2
3
4
5
6
7
8
9
10
11
12
from sko.GA import GA


# 测试scikit-opt
def testOpt():
def demo_func(x):
x1, x2, x3 = x
return x1**2 + (x2-0.05)**2 + x3**2

ga = GA(func = demo_func, size_pop = 500, n_dim = 3, lb = [-1, -10, -5], ub = [2, 10, 2], max_iter = 100)
best_x, best_y = ga.run()
print(best_x, best_y)

然后就用遗传算法进行调参,具体代码太长,上github去看吧。运行了半天,最后结果:

准确率51.6%,回测年化收益率29.4%,好像不行……

可以看到开始还行,到后面误差越来越大。

看资产净值图也能看出来,到后半段直接就没交易了。
尝试失败,不过主要的收获在于学会了用粒子群算法和遗传算法来调参的方法,还有个体会:在python里很多常规的问题都有相应的库,别上来就自己造轮子。聚焦自己的主要问题吧。
本文代码

我发文章的三个地方,欢迎大家在朋友圈等地方分享,欢迎点“在看”。
我的个人博客地址:https://zwdnet.github.io
我的知乎文章地址: https://www.zhihu.com/people/zhao-you-min/posts
我的微信个人订阅号:赵瑜敏的口腔医学学习园地

欢迎打赏!感谢支持!