量化投资学习笔记86——实现量化交易经典策略:多因子选股(改进1)

下面对上次实现的多因子选股模型进行一些改进。
首先来看评分标准,增加几个系数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 计算评分指标
def scale(factors, a1=1.0, a2 = 1.0, a3 = 1.0, a4 = 1.0, a5 = 1.0):
pe = -1.0*a1*factors.pe/factors.pe.mean()
esp = a2*factors.esp/factors.esp.mean()
bvps = a3*factors.bvps/factors.bvps.mean()
pb = a4*factors.pb/factors.pb.mean()
npr = a5*factors.npr/factors.npr.mean()
score = pe+esp+bvps+pb+npr
# print(score)
# 排序并画图
score = score.sort_values()
# print(score)
# score.plot(kind = "hist", bins = 1000, range = (-25.0, 30.0))
# plt.savefig("fsctorScore.png")
return score

也就是为每个因子赋予不同的权重,然后找到最佳的因子权重组合。
专门写一个函数。

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
# 对不同的因子权重组合进行优化
def optStrategy(factors, strategy, cash = 1000000, bDraw = False):
start = "2018-01-01"
end = "2020-07-05"

res = []
maxRes = 0.0
maxParams = [0, 0, 0, 0, 0]
x = 200
step = 100
for a1 in range(1, x, step):
for a2 in range(1, x, step):
for a3 in range(1, x, step):
for a4 in range(1, x, step):
for a5 in range(1, x, step):
score = scale(factors, a1, a2, a3, a4, a5)
codes = score[-10:].index
# 准备数据
name = factors.loc[codes, "name"].values
# 将汉字转换为拼音
p = Pinyin()
name = [p.get_pinyin(s) for s in name]
code = [str(x) for x in codes]
opttest = backtest.BackTest(strategy, start, end, code, name, cash)
result = opttest.run()
print("a1 = {}, a2 = {}, a3 = {}, a4 = {}, a5 = {}, 年化收益率: {}\n".format(a1, a2, a3, a4, a5, result.年化收益率))
res.append(result.年化收益率)
if result.年化收益率 > maxRes:
maxRes = result.年化收益率
maxParams = [a1, a2, a3, a4, a5]
print("最佳权重:", maxParams, "最大年化收益率:", maxRes)
return res

用的最简单的穷举法。

尝试的情况太大时,手机跑了一个多小时,最后termux死了。只能用最少的情况来测试。看来还是得搞个服务器。

尝试用比穷举好的算法,先试一下随机算法,即在解空间内随机生成系数,再比较年化收益率。
代码如下:

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
# 采用随机算法进行优化
def randOpt(factors, strategy, times = 100, cash = 1000000, bDraw = False):
start = "2018-01-01"
end = "2020-07-05"

res = []
maxRes = 0.0
maxParams = [0, 0, 0, 0, 0]
random.seed()
for i in range(times):
a1 = random.randint(1, 200)
a2 = random.randint(1, 200)
a3 = random.randint(1, 200)
a4 = random.randint(1, 200)
a5 = random.randint(1, 200)
score = scale(factors, a1, a2, a3, a4, a5)
codes = score[-10:].index
# 准备数据
name = factors.loc[codes, "name"].values
# 将汉字转换为拼音
p = Pinyin()
name = [p.get_pinyin(s) for s in name]
code = [str(x) for x in codes]
opttest = backtest.BackTest(strategy, start, end, code, name, cash)
result = opttest.run()
print("第{}次尝试:a1 = {}, a2 = {}, a3 = {}, a4 = {}, a5 = {}, 年化收益率: {}\n".format(i+1, a1, a2, a3, a4, a5, result.年化收益率))
res.append(result.年化收益率)
if result.年化收益率 > maxRes:
maxRes = result.年化收益率
maxParams = [a1, a2, a3, a4, a5]
print("最佳权重:", maxParams, "最大年化收益率:", maxRes)
return res

解的空间为[1,200),穷举的话,约200^5次尝试。结果为:

看来最重要的指标是市净率。

现在的问题是有没有办法能扩大搜索范围而效率又更高?
先试试多元线性回归吧。为了获取数据,再把上面的随机算法运行一次,记录每次的5个参数,以及相应的回测年化收益率。
运行完结果是这样

画图看看

看图,市盈率的权重与年化收益率无明显相关关系,每股收益,每股净资产,每股利润的权重与年化收益率都是呈负相关,而市净率的权重与年化收率是呈正相关。但在年化收益率0.1-0.2之间有个区域,权重的改变对其无影响。要不要考虑把这些数据给剔除掉?

剔除以后的情况,先用多元线性回归试试。
之前在手机的termux里运行包含sklearn库的程序时,总是提示:
“This platform lacks a functioning sem_open implementation, therefore, the required synchronization primitives needed will not function, see issue 3770.”
没有找到解决方法,只能传到服务器或者docker里运行。刚又搜了一下,找到一个解决方案:把sklearn版本退回到0.19.2,
如下:

1
pip install scikit-learn==0.19.2

搞定!现在程序在termux里运行正常了。只是只能单线程执行。
参考了 https://www.jianshu.com/p/dc53be46d172 特此感谢!

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
# 回归分析
def regress(data):
print(data)
draw(data, "factor_analysis.png")
# 剔除年化收益率在0.1-0.2之间的数据
data = data[(data.result < 0.1) | (data.result > 0.2)]
draw(data, "factor_after_clean.png")
# 进行多元线性回归
# 划分数据
print(data.describe())
X = data.loc[:, ["a1", "a2", "a3", "a4", "a5"]]
Y = data.loc[:, ["result"]]
# print(X.count())
# n = len(X)
# X_train = X.iloc[:int(n*0.75), :].reset_index(drop = True)
# X_test = X.iloc[int(n*0.75):, :].reset_index(drop = True)
# Y_train = Y.iloc[:int(n*0.75), :].reset_index(drop = True)
# Y_test = Y.iloc[int(n*0.75):, :].reset_index(drop = True)
# print(X_train.count(), Y_test)
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size = 0.25, random_state = 1)
print(X_test, Y_test)
# 建模
model = LinearRegression()
model.fit(X_train, Y_train)
predictions = model.predict(X_test)
for i, prediction in enumerate(predictions):
print(i)
print("预测值:%s, 目标值:%s" % (prediction, Y_test.iloc[i, :]))
print("R平方值:%.2f" % model.score(X_test, Y_test))
MSE = metrics.mean_squared_error(Y_test, predictions)
RMSE = np.sqrt(MSE)
print("MSE:", MSE)
print("RMSE:", RMSE)


结果很差,画图看看。

看图倒还不错。
画散点图看看。

看来还要生成更大的数据集来训练。
用回归的模型选择系数权重再回测一下看看。

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
# 用线性回归所得模型选择因子权重
def regressChoose(factors, strategy, model, times = 200, cash = 1000000, bDraw = False):
start = "2018-01-01"
end = "2020-07-05"

random.seed()
best = 0.0
bestWeight = [0, 0, 0, 0, 0]
data = pd.DataFrame()
for i in range(times):
a1 = random.randint(1, 200)
a2 = random.randint(1, 200)
a3 = random.randint(1, 200)
a4 = random.randint(1, 200)
a5 = random.randint(1, 200)
data = data.append(pd.DataFrame({"a1":[a1], "a2":[a2], "a3":[a3], "a4":[a4], "a5":[a5]}), ignore_index = True)
# print(data)
pred = model.predict(data)
print(type(pred), pred.max(), np.argmax(pred))
best = pred.max()
bestPos = np.argmax(pred)
bestWeight = [data.iloc[bestPos, 0], data.iloc[bestPos, 1], data.iloc[bestPos, 2], data.iloc[bestPos, 3], data.iloc[bestPos, 4]]
score = scale(factors, bestWeight[0], bestWeight[1], bestWeight[2], bestWeight[3], bestWeight[4])
codes = score[-10:].index
# 准备数据
name = factors.loc[codes, "name"].values
# 将汉字转换为拼音
p = Pinyin()
name = [p.get_pinyin(s) for s in name]
code = [str(x) for x in codes]
opttest = backtest.BackTest(strategy, start, end, code, name, cash)
result = opttest.run()
print("模型预测年化收益率{}, 实际回测年化收益率: {}\n".format(best, result.年化收益率))
return bestWeight

结果

差别很小的,跟一开始的随机算法相比,这么做优点是不用每组值都进行回测,那个很耗时间的。这样可以增加尝试次数。试试增加到10000次吧。

用了大概10秒钟。
还有个问题,就是是不是每次运行都需要训练模型,能不能保存下来,下次直接用?
用sklearn的joblib即可。

1
2
3
4
# 保存模型
joblib.dump(model, "Regress.m")
# 加载模型
model = joblib.load("Regress.m")

现在就可以把回归模型那里注释掉啦。另外再扩大因子权重的取值范围,到1000看看。

现在差距就比较大了,而且实测下来的年化收益率并没有增加多少,因此还是改回原来的200以内的范围。程序有好多重复的地方,重构一下。然后再运行随机算法,模拟次数多一点,多生成一些数据。
在我的手机上试了两次,基本上模拟超过200次就比较悬了,不知道什么时候就死了。传到诊室电脑上试试。
在电脑上大概每秒钟能回测一次,我用随机权重系数回测了4000次,生成4000组数据。再把数据传回手机进行回归和选股。

数据多了果然好点。
接下来,就用筛选出来的股票池再进行回测,换一个时间范围看看。

1
2
3
4
5
6
# 根据输入的股票池进行回测检验
def checkResult(strategy, codes, names, start, end, cash = 1000000):
opttest = backtest.BackTest(strategy, start, end, codes, names, cash)
result = opttest.run()
print("回测结果")
print(result)

回测10年的数据。

回测十年,结果蛮不错的。
接下来,如果是正儿八经的量化策略研究,就该上实盘了吧?
嘿嘿,再研究下有没有其它方法吧。我在知乎上提问了:https://www.zhihu.com/question/417584064
没人回我……

下次了。
代码地址还是: https://github.com/zwdnet/MyQuant/tree/master/47
策略文件为facts.py。

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

欢迎打赏!感谢支持!