摘要
随着全球经济与股市的快速发展,股票投资成为了民众们常用的理财方式之一。近年来,量化投资凭借其优良的纪律性、准确性、时效性和系统性等优势受到越来越多的关注。与西方成熟市场相比,我国量化投资还处于起步阶段,存在一些不足的同时具有很广阔的发展前景。同时随着人工智能技术飞速发展,机器学习与量化投资研究间擦起火花,因此本文针对量化选股问题,将机器学习与技术分析结合,构建了决策树模型,获得排名前十的股票投资组合,并通过可视化界面进行实证检验,分别获得投资组合中每个股票的收益和投资组合的总收益。
关键词:决策树; 量化选股; 可视化;夏普比率
一、问题重述
1.1 问题背景
随着软件技术和人工智能技术的发展,大量繁琐的数据分析和处理任务已逐渐由人工执行转变为电脑自动运行。这一变化在追求精准、高效的金融界也悄然发生着,曾被认为是一门艺术的主观证券投资已经逐步被依附于计算机的量化投资策略所取代。量化投资根据人的投资思想和投资经验来构建数学模型,并利用计算机来处理大量历史数据,在较短时间内验证模型的有效性,只有当模型在历史数据上表现满足要求时,才会被进一步应用到实盘交易中。因此对于股票投资来说,量化选股是基础,没有良好的选股技术,量化投资效果将大打折扣。
1.2 需要解决的问题
设计一个量化选股系统,可任意选择申银万国28个一级行业,基于总体规模与投资效率方法选择排名前10的股票构建投资组合,并通过可视化界面进行实证检验,分别获得投资组合中每个股票的收益和投资组合的总收益。
二、问题分析
问题的研究对象是任给的多支股票,研究内容为量化选股策略。该问题实质上是采用机器学习中的决策树模型,用历年经过预处理的数据作为训练集,选择合适的标签将其逐步切分成不同子集,直至所有的训练样本被分类正确,并对下一年度数据进行分类预测,随后根据分类结果构建投资组合。模型的核心在于选择适当特征即特征提取。
三、符号说明及名词定义
四、模型建立与求解
4.1 数据预处理
数据预处理包含数据清洗和数据可视化与分析。
共有2个附件,附件中的数据给出了申银万国28个一级行业股票的相关信息:
附件1:申万一级行业股票代码;
附件2:申万一级行业股票行情数据(2021-01-01至2021-06-30);
附件1中存在无效值,故附件1需对数据进行清洗整理,使用Excel和Python软件对数据做如下预处理。利用以上数据,对问题进行求解分析。
4.1.1 数据清洗
对附件1中的,有部分股票退市但是其代码仍在,具体代码如下:688509
688688、688385、688670、688148、688071、688778、688779故须删除。
4.1.2 数据可视化与分析
附件1申银万国各一级行业成分股数量如图1所示。
申银万国各一级行业的涨跌幅、收益指数、成交量的数据如图2、图3,图4所示。
4.2 特征工程
衡量一个股票一般有很多指标,这里我们选取五个指标:收益率、alpha、夏普比率、最大回撤、beta。
收益率是股票收益(股票增值+分红)与初始投资之间的比例。数值越高代表不考虑风险的情况下股票的表现越好。
夏普比率是描述股票或组合相比于无风险收益在单位风险下的所能获得收益的程度。它将股票或组合的风险归一化,便于更好的比较组合之间的有效性。数值越高代表考虑风险的情况下股票或组合表现越好。
回撤是描述了投资者可能面临的的最大亏损,是衡量投资组合的抗风险能力的重要风险指标。最大回撤率 (Maximum Drawdown, MDD) 定义为,在选定周期内任一历史时点往后推,收益率回撤幅度的最大值。回撤值越低越好。
Alpha衡量了股票或组合相对于市场的主动收益 (超额收益)。alpha=0说明表现与大盘一致。alpha<0说明相比大盘收益要差。alpha>0说明股票或组合表现优于大盘。alpha=1%相当于高于同期市场收益1%。
Beta衡量了股票或组合与市场的走势的相关程度,解释了股票或组合来源于市场的收益 (市场收益)。大盘的beta=1。beta>1表示股票或大盘与市场走势更相关,并且震荡比较剧烈。Beta<1表示要么与市场走势相关性小,要么震荡相比于市场要小。
4.3 模型的建立与求解
4.3.1 模型的建立
此处采用机器学习策略,使用支持向量机模型,具体算法描述如下:
表1 NIUBI具体算法描述
用上述算法得出的结果支持接下来的计算
附件2中股票数为4408个,首先对每个股票计算他的收益率,具体算法描述如表2所示:
表2 RETURN具体算法描述
同理,根据上述算法流程可以计算出股票的最大回撤率,alpha,beta值:
4.3.2 模型的求解
分别对附件二中的股票进行计算,最后得出的结果如下(排名只取到前15名,计算结果保留小数点后一位数字):
表3 股票收益率排名
表4 股票最大回撤率排名
表5 股票最大回撤率排名
把计算得出的数据通过下列公式计算得到最后总分:
评分结果如下:
根据下面公式得到,该组合夏普比率为1.43,说明该组合综合收益和风险之后,属于较好的投资组合。
4.3.3 模型的性能评价
采用分类准确率、精度,召回率、F1分数和AUC值作为评价支持向量机的指标。准确率为分类正确的样板占总数的比例,精度为预测正类预测正确的样本数占预测是正类的样本数的比例,召回率为预测正类预测正确的样本数,占实际是正类的样本数的比例,F1分数为精度和召回率的调和平均,AUC为ROC曲线下面积,越大越好。模型性能如表6所示。
表6 模型评价
五、模型的评价与改进
5.1 模型的评价
5.1.1 模型的优点
(1) 速度快: 计算量相对较小, 且容易转化成分类规则. 只要沿着树根向下一直走到叶, 沿途的分裂条件就能够唯一确定一条分类的谓词;
(2) 准确性高: 挖掘出来的分类规则准确性高, 便于理解, 决策树可以清晰的显示哪些字段比较重要, 即可以生成可以理解的规则;
(3) 可以处理连续和种类字段;
(4) 不需要任何领域知识和参数假设;
(5) 适合高维数据。
5.1.2 模型的缺点
(1) 对于各类别样本数量不一致的数据, 信息增益偏向于那些更多数值的特征;
(2) 容易过拟合;
(3) 忽略属性之间的相关性。
5.2 模型的改进
过度拟合对于决策树学习和其他很多学习算法是一个重要的实践困难。有几种途径用来避免决策树学习中的过度拟合。它们可被分为两类:
(1) 及早停止增长树法,在ID3算法完美分类训练数据之前停止增长树;
(2) 后修剪法,即允许树过度拟合数据,然后对这个树后修剪。
尽管第一种方法可能看起来更直接,但是对过度拟合的树进行后修剪的第二种方法被证明在实践中更成功。这是因为在第一种方法中精确地估计何时停止增长树很困难。无论是通过及早停止还是后修剪来得到正确大小的树,一个关键的问题是使用什么样的准则来确定最终正确树的大小。解决这个问题的方法包括:
(1) 使用与训练样例截然不同的一套分离的样例,来评估通过后修剪方法从树上修剪结点的效用;
(2) 使用所有可用数据进行训练,但进行统计测试来估计扩展(或修剪)一个特定的结点是否有可能改善在训练集合外的实例上的性能;
(3) 使用一个明确的标准来衡量训练样例和决策树编码的复杂度,当这个编码的长度最小时停止增长树。
六、模型的应用
金融行业可以用决策树做贷款风险评估,保险行业可以用决策树做险种推广预测,医疗行业可以用决策树生成辅助诊断处置模型等等。
参考文献
[1]李斌,邵新月,李珥阳.机器学习驱动的基本面量化投资研究[J].中国工业经济,2019(08):61-79.
[2]黄宏远,王梅.基于多元回归分析的多因子选股模型[J].同化示范学院学报,2016.
[3]丁鹏.量化投资——策略与技术[M]、北京:电子工业出版社,2014,24-29.
[4]周志华.机器学习[M]﹒北京:清华大学出版社,2016,121-137.
[5]王远帆,施勇,薛质.基于决策树的端口扫描恶意流量检测研究[J].通信技术,2020,53(08):2002-2005.
[6]李恺,谭海波,王海元,解玉满,黄红桥,卜文彬,谈丛,彭潇,郭光,刘谋海,陈浩.一种基于决策树的主网线路状态检测方法、系统及介质[P].湖南省:CN111612149A,2020-09-01.
[7]周渐.基于SVM算法的多因子选股模型实证研究[D].浙江工商大学,2017.
[8]曹正凤,纪宏,谢邦昌.使用随机森林算法实现优质股票的选择[J].首都经济.
附录1
1、数据清洗
1. import pandas as pd
2. from jqdatasdk import *
3. from jqdatasdk.api import get_price, get_query_count
4. from numpy import nan
5.
6. auth('13259391862', '2001720Lmt')
7. get_query_count()
8.
9. industry_code = pd.read_csv("Industries.csv", index_col=0)
10. stock_code = pd.read_csv("total.csv",index_col=0,dtype=str)
11. stock_code.columns = [x for x in industry_code.index]
12.
13. for j in stock_code.index:
14. for i in stock_code.columns:
15. try:
16. if stock_code[i][j] is not nan:
17. prices = get_price(
18. stock_code[i][j],
19. start_date='2021-01-01',
20. end_date='2021-06-30',
21. frequency='1d',
22. fields=['open', 'close', 'low', 'high', 'avg'])
23. prices.to_csv(stock_code[i][j] + '.csv')
24. print("数据录入" + stock_code[i][j] + "已完成...")
25. else:
26. continue
27. except:
28. print(stock_code[i][j] + "无法录入...")
2、策略
1. from datetime import timedelta
2. import jqdata
3. import scipy.optimize as optimize
4. import statsmodels.api as sm
5. from jqdatasdk import valuation, balance, income, indicator, get_fundamentals
6.
7.
8. # 初始化函数,设定基准等等
9. def initialize(context):
10. set_benchmark('000300.XSHG')
11. # 开启动态复权模式(真实价格)
12. set_option('use_real_price', True)
13. # 输出内容到日志 log.info()
14. log.info('初始函数开始运行且全局只运行一次')
15. # 股票类每笔交易时的手续费是:买入时佣金万分之三,卖出时佣金万分之三加千分之一印花税, 每笔交易佣金最低扣5块钱
16. set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5),
17. type='stock')
18. # 开盘前运行
19. run_daily(before_market_open,time='before_open',
20. reference_security='000300.XSHG')
21. # 收盘后运行
22. run_daily(after_market_close,time='after_close',
23. reference_security='000300.XSHG')
24. set_parameters()
25.
26.
27. ## 开盘前运行函数
28. def before_market_open(context):
29. # 输出运行时间
30. log.info('函数运行时间(before_market_open):' +
31. str(context.current_dt.time()))
32.
33. factors = ['CMC', 'MC', 'CMC/C', 'TOE/MC',
34. 'PB', 'NP/MC', 'TP/MC', 'TA/MC',
35. 'OP/MC', 'CRF/MC', 'PS', 'OR/MC',
36. 'RP/MC', 'TL/TA', 'TCA/TCL', 'PE',
37. 'OR*ROA/NP', 'GPM', 'IRYOY', 'IRA',
38. 'INPYOY', 'INPA', 'NPM', 'OPTTR',
39. 'C', 'CC', 'PR', 'PRL', 'ROE',
40. 'ROA', 'EPS', 'ROIC', 'ZYZY']
41. # 因子获得因子参数
42. theta, mu, sigma = getThetaByFeatures(context, factors)
43. # 因子选股
44. if sum(theta) == 0:
45. return
46. stock_list = selectStocks(context, factors, theta, mu, sigma)
47. stock_list = unStartWith300(stock_list)
48. stock_list = filter_paused_and_st_stock(stock_list)
49. stock_list = filter_limitup_stock(context, stock_list)
50. stock_list = filter_limitup_stock(context, stock_list)
51. g.stock_to_buy = stock_list
52.
53.
54. ## 收盘后运行函数
55. def after_market_close(context):
56. log.info(str('函数运行时间(after_market_close):' +
57. str(context.current_dt.time())))
58. # 得到当天所有成交记录
59. trades = get_trades()
60. for _trade in trades.values():
61. log.info('成交记录:' + str(_trade))
62. log.info('一天结束')
63. log.info('#######################################################')
64.
65.
66. # 设置参数
67. def set_parameters():
68. g.period = 10
69. g.buyStockCount = 50
70. g.stock_to_buy = []
71. g.days = 0
72.
73.
74. # 交易
75. def trades(context, data, stock_list):
76. # 卖出不在列表的股票
77. for stock in context.portfolio.positions.keys():
78. if stock not in stock_list:
79. order_target_value(stock, 0)
80.
81. # 计算仍然需要购买的数量
82. num_to_buy = len(stock_list) - len(context.portfolio.positions.keys())
83. if num_to_buy == 0:
84. return
85. # 现金分配
86. cash = context.portfolio.available_cash
87. cash = 1.0 * cash / num_to_buy
88. for stock in stock_list:
89. if stock not in context.portfolio.positions.keys():
90. order_target_value(stock, cash)
91.
92.
93. # 每日运行
94. def dailyRunning(context, data):
95. # 过滤涨跌停股票
96. stock_list = g.stock_to_buy
97. stock_list = filter_limitup_stock(context, stock_list)
98. stock_list = filter_limitup_stock(context, stock_list)
99. stock_list = stock_list[:g.buyStockCount]
100. # 交易
101. if g.days % g.period == 0:
102. trades(context, data, stock_list)
103. g.days += 1
104. pass
105.
106.
107. ## 开盘时运行函数
108. def handle_data(context, data):
109. # 获得当前时间
110. hour = context.current_dt.hour
111. minute = context.current_dt.minute
112.
113. # 每天下午14:50调仓
114. if hour == 14 and minute == 50:
115. # 每日运行
116. dailyRunning(context, data)
117.
118.
119. # 去除创业板
120. def unStartWith300(stockspool):
121. return [stock for stock in stockspool if stock[0:3] != '300']
122.
123.
124. # 过滤停牌、ST类股票及其他具有退市标签的股票
125. def filter_paused_and_st_stock(stock_list):
126. current_data = get_current_data()
127. return [stock for stock in stock_list if not current_data[stock].paused and not current_data[stock].is_st
128. and 'ST' not in current_data[stock].name and '*' not in current_data[stock].name and '退' not in
129. current_data[stock].name]
130.
131.
132. # 过滤涨停的股票
133. def filter_limitup_stock(context, stock_list):
134. last_prices = history(1, unit='1m', field='close', security_list=stock_list)
135. current_data = get_current_data()
136.
137. # 已存在于持仓的股票即使涨停也不过滤,避免此股票再次可买,但因被过滤而导致选择别的股票
138. return [stock for stock in stock_list if stock in context.portfolio.positions.keys()
139. or last_prices[stock][-1] < current_data[stock].high_limit]
140.
141.
142. # 过滤跌停的股票
143. def filter_limitdown_stock(context, stock_list):
144. last_prices = history(1, unit='1m', field='close',
145. security_list=stock_list)
146. current_data = get_current_data()
147. return [stock for stock in stock_list if stock in context.portfolio.positions.keys()
148. or last_prices[stock][-1] > current_data[stock].low_limit]
149.
150.
151. # 代价函数
152. def costFunction(theta, X, y):
153. m = len(y)
154. tmp_theta = theta.reshape(len(theta), 1).copy()
155. temp = X.dot(tmp_theta)
156. J = sum(np.square(temp - y)) / 2.0 / m
157. return J
158.
159.
160. # 梯度
161. def gradient(theta, X, y):
162. # 样本数量
163. m = y.size
164. # 参数的拷贝
165. tmp_theta = theta.reshape(len(theta), 1).copy()
166. # 预测函数
167. h = dot(X, tmp_theta)
168. # 梯度计算
169. grad = 1.0 / m * X.T.dot(h - y)
170. grad = grad.flatten()
171. return grad
172.
173.
174. # 代价函数(正则化)
175. def costFunctionReg(theta, X, y, mylambda):
176. m = len(y)
177. tmp_theta = theta.reshape(len(theta), 1).copy()
178. temp = X.dot(tmp_theta)
179. J = sum(np.square(temp - y)) / 2.0 / m + 1.0 * mylambda / 2 / m * sum(tmp_theta ** 2)
180. return J
181.
182.
183. # 梯度
184. def gradientReg(theta, X, y, mylambda):
185. # 样本数量
186. m = y.size
187. # 参数的拷贝
188. tmp_theta = theta.reshape(len(theta), 1).copy()
189. # 预测函数
190. h = dot(X, tmp_theta)
191. # 梯度计算
192. grad = 1.0 / m * X.T.dot(h - y) + 1.0 * mylambda / m * tmp_theta
193. grad = grad.flatten()
194. return grad
195.
196.
197. # 均值归一
198. def featureNormalize(X):
199. mu = mean(X)
200. sigma = std(X)
201. X_norm = 1.0 * (X - mu) / sigma
202. return X_norm, mu, sigma
203.
204.
205. # 得到拟合参数
206. def getThetaByFeatures(context, factors):
207. period = g.period
208. period = 1
209. # 上一个调仓日期
210. yesterday = context.previous_date
211. daysBefore = yesterday - timedelta(days=period * 2) # period * 2
212. trade_days = jqdata.get_trade_days(start_date=daysBefore, end_date=yesterday)
213. log.info(trade_days)
214. log.info(trade_days[-period - 1:])
215. # 周期
216. trade_days = trade_days[-period - 1:]
217. # 起止日期
218. start_date = trade_days[0]
219. end_date = trade_days[-1]
220. # 得到上一个调仓日股票的因子数据,构建特征组合
221. x_df = get_factors(start_date, factors)
222. # 特征缩放
223. # 得到上一个调仓日股票的涨幅,构建结果组合
224. stock_list = x_df.index.tolist()
225. df = get_price(stock_list, start_date=start_date, end_date=end_date, frequency='daily', fields=['close'])['close']
226. y_se = df.ix[-1] / df.ix[0] - 1
227. y = y_se[~ np.isnan(y_se)]
228. x = x_df.ix[y.index.tolist()]
229. n = len(x_df.columns)
230. m = len(y)
231. X_norm, mu, sigma = featureNormalize(x)
232. X = sm.add_constant(X_norm)
233. for i in X.columns:
234. X[i] = np.nan_to_num(X[i])
235. X = np.c_[X]
236. y = np.c_[y]
237. # 初始化参数
238. initial_theta = np.zeros(n + 1)
239. # 正则化参数
240. mylambda = 1
241. opts = {'disp': False,
242. 'xtol': 1e-05,
243. 'eps': 1.4901161193847656e-08,
244. 'return_all': False,
245. 'maxiter': None}
246. result = optimize.minimize(costFunctionReg, initial_theta, args=(X, y, mylambda), method='Newton-CG',
247. jac=gradientReg, hess=None, hessp=None, tol=None, callback=None, options=opts)
248. theta = result.x
249. return theta, mu, sigma
250.
251.
252. # 得到因子数据
253. def get_factors(fdate, factors):
254. # stock_set = get_index_stocks('000300.XSHG',fdate)
255. q = query(
256. valuation.code, # 股票代码
257. valuation.circulating_market_cap, # CMC 流通市值
258. valuation.market_cap, # MC 总市值
259. valuation.circulating_market_cap / valuation.capitalization * 10000, # CMC/C 流通市值(亿)/总股本(万) (收盘价)
260. balance.total_owner_equities / valuation.market_cap / 100000000, # TOE/MC 每元所有者权益
261. valuation.pb_ratio, # PB 市净率
262. income.net_profit / valuation.market_cap / 100000000, # NP/MC 每元所有者净利润
263. income.total_profit / valuation.market_cap / 100000000, # TP/MC 每元利润总额
264. balance.total_assets / valuation.market_cap / 100000000, # TA/MC 每元资产总额
265. income.operating_profit / valuation.market_cap / 100000000, # OP/MC 每元营业利润
266. balance.capital_reserve_fund / valuation.market_cap / 100000000, # CRF/MC 每元资本公积
267. valuation.ps_ratio, # PS 市销率
268. income.operating_revenue / valuation.market_cap / 100000000, # OR/MC 每元营业收入
269. balance.retained_profit / valuation.market_cap / 100000000, # RP/MC 每元未分配利润
270. balance.total_liability / balance.total_sheet_owner_equities, # TL/TA 资产负债率
271. balance.total_current_assets / balance.total_current_liability, # TCA/TCL 流动比率
272. valuation.pe_ratio, # PE 市盈率
273. income.operating_revenue * indicator.roa / income.net_profit, # OR*ROA/NP 总资产周转率
274. indicator.gross_profit_margin, # GPM 销售毛利率
275. indicator.inc_revenue_year_on_year, # IRYOY 营业收入同比增长率(%)
276. indicator.inc_revenue_annual, # IRA 营业收入环比增长率(%)
277. indicator.inc_net_profit_year_on_year, # INPYOY 净利润同比增长率(%)
278. indicator.inc_net_profit_annual, # INPA 净利润环比增长率(%)
279. indicator.net_profit_margin, # NPM 销售净利率(%)
280. indicator.operation_profit_to_total_revenue, # OPTTR 营业利润/营业总收入(%)
281. valuation.capitalization, # C 总股本
282. valuation.circulating_cap, # CC 流通股本(万股)
283. valuation.pcf_ratio, # PR 市现率
284. valuation.pe_ratio_lyr, # PRL 市盈率LYR
285. indicator.roe, # ROE 净资产收益率ROE(%)
286. indicator.roa, # ROA 总资产净利率ROA(%)
287. indicator.eps, # EPS 每股盈余
288. # ROIC
289. # EBIT = 净利润 + 利息 + 税
290. # ROIC
291. (income.net_profit + income.financial_expense + income.income_tax_expense) / (
292. balance.total_owner_equities + balance.shortterm_loan + balance.non_current_liability_in_one_year + balance.longterm_loan + balance.bonds_payable + balance.longterm_account_payable),
293. (
294. balance.accounts_payable + balance.advance_peceipts + balance.other_payable - balance.account_receivable - balance.advance_payment - balance.other_receivable) / (
295. balance.total_owner_equities + balance.shortterm_loan + balance.non_current_liability_in_one_year + balance.longterm_loan + balance.bonds_payable + balance.longterm_account_payable)
296. ).filter(
297. # valuation.code.in_(stock_set),
298. valuation.circulating_market_cap
299. )
300. fdf = get_fundamentals(q, date=fdate)
301. fdf.index = fdf['code']
302. fdf.columns = ['code'] + factors
303. # 行:选择全部,列,返回除了股票代码所有因子
304. return fdf.iloc[:, 1:]
305.
306. # 选股方法
307. def selectStocks(context, factors, theta, mu, sigma):
308. x_df = get_factors(context.current_dt, factors)
309. X_norm = (x_df - mu) / sigma
310. X = sm.add_constant(X_norm)
311. for i in X.columns:
312. X[i] = np.nan_to_num(X[i])
313. # 参数的拷贝
314. tmp_theta = theta.reshape(len(theta), 1).copy()
315. # 预测函数
316. h = dot(X, tmp_theta)
317. # 结果赋值,预测的涨幅
318. X['predict'] = h
319. X = X.sort(columns=['predict'], ascending=[False])
320. return X.index.tolist()
附录2
附件清单:
附件 1: 附件1:申万一级行业股票代码.csv
附件 2: 附件2:申万一级行业股票行情数据(2021-01-01至2021-06-30)
附件 3: 附件3:图片
附件 1,附件2,附件3需要的可以联系我
欢迎大家加我微信学习讨论