量化投资学习笔记85——实现量化交易经典策略:多因子选股

为了应付一个考试,停了一个多月。现在学习继续。这次要实现的是多因子选股。即通过多个因子来在股票池中选择一个或数个股票,以期获得高于市场平均水平的收益。属于量化选股的一种。
主要有打分法和回归法两种。先实现最简单的打分法吧。
由于我使用的是tushare获取数据,先看看能获取哪些因子。
用ts.get_stock_basics(),结果是这样的:

pe是市盈率,outstanding是流通股本,totals是总股本,totalAssets是总资产,liquidAssets是流动资产,fixedAssets是固定资产,reversved是公积金,esp每股收益,bvps每股净资,pb市净率,timeToMarket上市日期,undp未分利润,rev收入同比,profit利润同比,gpr毛利润,npr净利润,holders,股东人数。
还有获取其它数据的函数,第一次弄,先用这个吧。
首先排除一些股票:
①上市不满2年的股票。
②ST的股票
③亏损的股票。

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
# coding:utf-8
# 多因子选股实现


import backtrader as bt
import backtrader.indicators as bi
import backtest
import pandas as pd
import tushare as ts


# 获取股票数据,进行初步筛选,返回供因子分析的股票数据。
def getFactors():
# data = ts.get_stock_basics()
# print(data.head())
# print(len(data))
# data.to_csv("stocks.csv")
data = pd.read_csv("stocks.csv", index_col = "code")
# 排除亏损的股票
data = data[data.npr > 0.0]
# 排除上市不满2年的
data = data[data.timeToMarket <= 20180801]
# 排除ST股票
data = data[~ data.name.str.contains("ST")]
# print(data)
return data

if __name__ == "__main__":
factors = getFactors()
大概剔除了1/3的股票。接下来分析一下剩下的数据。
# 分析数据
def analysis(factors):
print("平均市盈率:%.2f" % (factors.pe.mean()))
print("每股收益:%.2f" % (factors.esp.mean()))
print("每股净资产:%.2f" % (factors.bvps.mean()))
print("平均市净率:%.2f" % (factors.pb.mean()))
print("平均每股净利润:%.2f" % (factors.npr.mean()))
print("平均股东人数:%.2f" % (factors.holders.mean()))
1
2
3
4
5
6
平均市盈率:138.04
每股收益:0.18
每股净资产:5.24
平均市净率:4.20
平均每股净利润:13.38
平均股东人数:54578.72

画图看看。

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
# 分析数据
def analysis(factors):
print("平均市盈率:%.2f" % (factors.pe.mean()))
print("每股收益:%.2f" % (factors.esp.mean()))
print("每股净资产:%.2f" % (factors.bvps.mean()))
print("平均市净率:%.2f" % (factors.pb.mean()))
print("平均每股净利润:%.2f" % (factors.npr.mean()))
print("平均股东人数:%.2f" % (factors.holders.mean()))
# 绘图
print(factors.pe)
plt.figure()
factors.pe.hist(bins = 100, range = (0, 2.0), align = "left")
plt.savefig("PE.png")
plt.figure()
factors.esp.hist(bins = 100, range = (0, 2.0), align = "left")
plt.savefig("ESP.png")
plt.figure()
factors.pb.hist(bins = 100, range = (0, 50.0), align = "left")
plt.savefig("PB.png")
plt.figure()
factors.npr.hist(bins = 100, range = (0, 50.0), align = "left")
plt.savefig("NPR.png")
plt.figure()
factors.holders.hist(bins = 100, range = (0, 50.0), align = "left")
plt.savefig("HOLDERS.png")



图形大多是这样的,长尾形的。值越高的股票越少。
现在来找因子了,我就主观定义吧:
选取市盈率、每股收益、每股净资产、市净率、净利润这几个指标,其中市盈率是越小越好,其它都是越大越好。因此设计的评分公式为:
评分 = -1×市盈率/平均市盈率 + 每股收益/平均每股收益 + 每股净资产/平均每股净资产 + 市净率/平均每股市净率 + 净利润/平均每股净利润
即把每个值都与所有值的平均值相除最后再相加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 计算评分指标
def scale(factors):
pe = -1.0*factors.pe/factors.pe.mean()
esp = factors.esp/factors.esp.mean()
bvps = factors.bvps/factors.bvps.mean()
pb = factors.pb/factors.pb.mean()
npr = 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")


提取评分最高的10个股票,结果如下:

1
2
3
Int64Index([661, 600695, 603301, 2582, 600620, 603444, 600061, 617, 2069,
600519],
dtype='int64', name='code')

发现一个问题:661,2582这些是啥?唉,在一开始的筛选的代码里再加一条,排除代码小于100000的。

1
2
3
Int64Index([600895, 600621, 600674, 600685, 600695, 603301, 600620, 603444,
600061, 600519],
dtype='int64', name='code')

现在就对了!
接下来就用这十只股票来回测,策略就是买入持有。

1
['张江高科' '华鑫股份' '川投能源' '中船防务' '绿庭投 资' '振德医疗' '天宸股份' '吉比特' '国投资本' '贵州茅台'] ['600895', '600621', '600674', '600685', '600695', '603301', '600620', '603444', '600061', '600519']

就这十只股票。
下面是回测代码:

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
# 交易策略类,一开始买入然后持有。
class FactorStrategy(bt.Strategy):
def __init__(self):
self.p_value = self.broker.getvalue()*0.9/10.0

def next(self):
# 买入
for data in self.datas:
# 获取仓位
pos = self.getposition(data).size
if pos == 0:
size = int(self.p_value/100/data.close[0])*100
self.buy(data = data, size = size)
# 最后卖出
date = self.datas[0].datetime.date(0)
closeDate = datetime.datetime(2020, 7, 2)
if date.year == closeDate.year and date.month == closeDate.month and date.day == closeDate.day:
for data in self.datas:
pos = self.getposition(data).size
if pos != 0:
self.sell(data = data, size = pos )

# 输出
def log(self, txt):
print(txt)

# 输出交易过程
def __displayOrder(self, buy, order):
if buy:
self.log(
'执行买入, 价格: %.2f, 成本: %.2f, 手续费 %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
else:
self.log(
'执行卖出, 价格: %.2f, 成本: %.2f, 手续费 %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))

# 交易情况
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
return
if order.status in [order.Completed]:
if order.isbuy():
self.__displayOrder(True, order)
elif order.issell():
self.__displayOrder(False, order)
self.order = None


if __name__ == "__main__":
factors = getFactors()
analysis(factors)
score = scale(factors)
codes = score[-10:].index
# 进行回测
start = "2018-01-01"
end = "2020-07-05"
name = factors.loc[codes, "name"].values
# 将汉字转换为拼音
p = Pinyin()
name = [p.get_pinyin(s) for s in name]
code = [str(x) for x in codes]
print(len(name), code)
backtest = backtest.BackTest(FactorStrategy, start, end, code, name, 1000000, bDraw = True)
result = backtest.run()
backtest.output()
print(result)

结果

结果还不错,年化收益率15.8%,最大回撤达41.6%。夏普值0.44。有个问题,我用的因子数据应该是最近的,而回测时间是2018-2020年的,也就是用了未来数据。原因是我不知道怎么获取历史的因子数据,先掌握方法吧。还有画的图很烂,我再研究下。
代码地址还是: https://github.com/zwdnet/MyQuant/tree/master/47
策略文件为facts.py。

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

欢迎打赏!感谢支持!