本测试旨在重现一套比较简单且完备的量化框架
该框架基于现代投资组合理论,并应用主流的机器学习算法(SVM)进行分析
旨在初步形成一个量化投资的思路,辅助构建科学合理的投资策略
1$ python Init_StockALL_Sp.py
2$ python stock_index_pro.py
3$ python main_pro.py
测试使用的Python版本:3.6.8
测试使用的Anaconda版本:1.9.6
xxxxxxxxxx
21$ pip install tushare
2$ pip install tushare --upgrade
xxxxxxxxxx
11import tushare as ts
tushare版本需大于1.2.10
xxxxxxxxxx
11ts.set_token('your token')
完成调取tushare数据凭证的设置,通常只需要设置一次
xxxxxxxxxx
31pro = ts.pro_api()
2# 或者在初始化中直接设置token
3pro = ts.pro_api('your token')
xxxxxxxxxx
31pro.daily() # 获取日K数据(未赋权)
2pro.index_daily() # 获取指数行情
3pro.trade_cal() # 获取交易日历
xxxxxxxxxx
11import datetime
xxxxxxxxxx
21import pymysql.cursors
2import sqlalchemy
xxxxxxxxxx
51import numpy as np
2import pandas as pd
3from sklearn import svm
4import pylab as *
5import math
数据获取
默认获取从预设时间(我们定义为年第个交易日)到最邻近交易日,股票池所有交易日的行情数据
注意更改 Init_StockALL_Sp.py 中的股票池
xxxxxxxxxx
31# 设定需要获取数据的股票池, 比如与云计算、软件板块相关的标的
2# 中兴通讯, 远光软件, 中国长城, 东方财富, 用友网络, 中科曙光, 中国软件, 浪潮信息, 宝信软件
3stock_pool = ['000063.SZ', '002063.SZ', '000066.SZ', '300059.SZ', '600588.SH', '603019.SH', '600536.SH', '000977.SZ', '600845.SH']
如果对于股票代码,所属板块,上市状态,上市日期等情况不甚了解,可以优先查询股票的 基本信息
xxxxxxxxxx
21# 获取股票基本信息列表
2data = pro.stock_basic()
存储至MySQL
部分示例
注意
对于使用 main_pro.py 正式回测时,我们选择的股票池必须是前述 Init_StockALL_Sp.py
中股票的子集,且为了后续构建资产组合中 不至于过少,规定股票池的度不小于
由于我们在分析不同问题时,从 Init_StockALL_Sp.py
中获取标的的数量较为庞大(比如超过 只股票),但在回测中标的股票数不可能过于庞大(算力限制,一段时间内主成分也不会过多),比如我们在 main_pro.py
中的股票池标的数就选为最小的 ,在 stock_info 表中会优先精简上述的 stock_all 表,既相当于从自己获取的数据库中,抓取回测所需股票池中的标的行情数据,这样会在一定程度上提高查询速度,示例不再赘述
数据获取
默认获取从预设时间(我们定义为 年第 个交易日)到最邻近交易日,参考指数所有交易日的行情数据
注意更改 stock_index_pro.py 中的基准指数
指数名称 | 赋予简称 | 交易所/Tushare编码 |
---|---|---|
上证指数 | SH | 000001.SH |
深圳成指 | SZ | 399001.SZ |
上证50 | SH50 | 000016.SH |
沪深300 | HS300 | 000300.SH or 399300.SZ |
中证500 | ZZ500 | 000905.SH or 399905.SZ |
中小板指 | ZX | 399005.SZ |
创业板 | CY | 399006.SZ |
因为股票池中后续进行回测的股票两市均有,且市值相对较重,所以选择沪深 300 指数较为合理
xxxxxxxxxx
31df = pro.index_daily(ts_code='000300.SH')
2# 统一指数标注并删除原复杂指数代码标注
3df['stock_code'] = 'HS300'
存储至MySQL
部分示例
从获取数据的特征,基于 SVM 做分类问题,来对于涨跌判断进行建模
x1from sklearn import svm
2import DC
3# DC是将原始行情数据划分成SVM训练的各项数据集的预处理类
4
5dc = DC.data_collect(stock, start_date, end_date)
6train = dc.data_train # 训练集
7target = dc.data_target # 目标集
8test_case = [dc.test_case] # 测试集
9model = svm.SVC() # 建模
10model.fit(train, target) # 训练
11ans2 = model.predict(test_case) # 预测
运行结果
有了单个 SVM 结果后,就可以通过遍历股票池中的标的,并对比 SVM 训练时,测试区间中的真实情况给予评价
机器学习常用评价指标公式如下
部分效果如下
再遍历所有回测区间内的交易日,来给出全部的预测情况及评价指标
这里需要注意,在一些其他应用场景(比如医疗,身份识别)中,需要 分值足够接近 ,否则模型毫无意义
并且在其他条件一定时, 增大肯定不是坏事
但在投资领域,胜率并不能完全反应到收益,比如 的胜率也存在每次都赚了小钱,但在错误预判时却造成巨额亏损的情况,相反低胜率也存在每次收益较大而使得总收益期望大于 的情形
当然对于每一小段时间,我们还是需要从指标层面选择较强的标的来构建投资组合,这样在相同的收益率下,我们将承担更小的风险
区分于其他市场(美股等),A 股市场没有有效的认沽机制,或者说做空条件过于严苛和高贵,所以我们不能将特征向量中的负值在策略中成为空头开仓,而是必须将其舍去(重置为 ),并将特征向量中的正值线性归一化(理论上这一步会极大地降低收益),由于我们需要挖掘期望收益为正的策略,归一化可以增加我们的资金使用效率
现代投资组合理论的主要实现如下
xxxxxxxxxx
201# 求协方差矩阵
2cov = np.cov(np.array(list_return).T)
3# 求特征值和其对应的特征向量
4ans = np.linalg.eig(cov)
5# 排序,特征向量中负数置0,非负数线性归一
6ans_index = copy.copy(ans[0])
7ans_index.sort()
8resu = []
9for k in range(len(ans_index)):
10 con_temp = []
11 con_temp.append(ans_index[k])
12 content_temp1 = ans[1][np.argwhere(ans[0] == ans_index[k])[0][0]]
13 content_temp2 = []
14 content_sum = np.array([x for x in content_temp1 if x >= 0.00]).sum()
15 for m in range(len(content_temp1)):
16 if content_temp1[m] >= 0 and content_sum > 0:
17 content_temp2.append(content_temp1[m] / content_sum)
18 else:
19 content_temp2.append(0.00)
20 con_temp.append(content_temp2)
对于某一交易日的仓位管理,可以将较长一段时间(回测时选取了 ,所以记得在第一步尽量剔除次新股)的 return list 传入上述代码来得到
在 Portfolio.py 中,可以返回最小和次小两套特征值和特征向量,分别对应在投资可行域中最小风险组合,以及最佳收益组合(风险稍稍提高,收益明显提高),如下图所示
在正式的回测中,我们选取最佳收益组合来作为投资的仓位管理依据
回测区间, (对于日 K 策略,回测区间小于一个月意义不大)
股票池为,中国软件,中兴通讯,浪潮信息,用友网络,宝信软件
xxxxxxxxxx
11stock_pool = ['600536.SH', '000063.SZ', '000977.SZ', '600588.SH', '600845.SH']
无风险利率,设为一年期银行存款,年化
货币资金收益率,预设为年化
其他参数,如前所述,或采取默认值
回测时我们根据收盘价进行交易(假设在资金量不大时一定可以成交到模型中计划的数量)
在已有的训练、建模、预测后,我们进行模拟交易,投资组合变换的频率默认为 个交易日
根据回测区间第一个交易日的行情,进行一次 投资组合建仓(手续费 )
由于最小的交易单位是 股,我们会向下取 的倍数为第 支标的持仓股数
并按照如下公式更新资产信息
xxxxxxxxxx
31new_capital = deal_buy.cur_capital - vol * buy_price * 0.0005
2new_money_lock = deal_buy.cur_money_lock + vol * buy_price
3new_money_rest = deal_buy.cur_money_rest - vol * buy_price * 1.0005
随后制定平仓(,如果在一个投资组合内,某标的被策略卖出,则会全部卖出,不涉及部分卖出的情况)的择时策略,当持仓非空时,依次进行如下步骤:
推进至回测区间内的下一交易日,对于 2-5 发生的情形,按照如下公式更新相关资产情况
xxxxxxxxxx
51new_money_lock = deal.cur_money_lock - sell_price * hold_vol
2new_money_rest = deal.cur_money_rest + sell_price * hold_vol * 0.9984
3new_capital = new_money_lock + new_money_rest
4new_profit = (sell_price * 0.9984 - init_price * 1.0005) * hold_vol
5new_profit_rate = sell_price * 0.9984 / (init_price * 1.0005)
当单一持仓标的收益率超过 时,止盈平仓该标的(GOODSELL)
未有符合条件的执行或者全部执行完后下一步
当单一持仓标的亏损率超过 时,止损平仓该标的(BADSELL)
未有符合条件的执行或者全部执行完后下一步
当建仓后第 个交易日只拥有货币资金,则返回投资组合建仓板块
当建仓后第 个交易日还存在未平仓的标的
未有符合条件的执行则下一步
当单一持仓标的的 SVM 预测下个交易日为 (下跌时),预判平仓该标的(PredictSELL)
未有符合条件的执行或者全部执行完后下一步
执行资金情况更新
返回第一步
回测过程的简要流程图
回测完成后,会对所有的回测情况进行评估,选取了 个经典评价量化策略的指标,分别是
这样既可以对回测效果有很好的刻画,又会在一定程度上杜绝过拟合的情形
收益率
这是我们最关心的,相当于是回测区间内的收益效率衡量
对于第 交易日的账户权益或者指数,收益率计算公式如下
xxxxxxxxxx
131def Cal_Return_Rate(seq, yd=250):
2 seqn = len(seq)
3 Return_Rate = (seq[-1] / seq[0]) - 1
4 Annual_Rate = math.pow((seq[-1] / seq[0]), yd / seqn) - 1
5 Return_List = []
6 Base_V = seq[0]
7 for i in range(seqn):
8 if i == 0:
9 Return_List.append(float(0.00))
10 else:
11 ri = (float(seq[i]) - float(Base_V))/float(Base_V)
12 Return_List.append(ri)
13 return Return_Rate, Annual_Rate, Return_List
最大回撤率
指在回测区间内内任一历史时点往后推,资产(指数)走到最低点时的收益率回撤幅度的最大值
最大回撤用来描述实行投资组合建仓后(指数)可能出现的最糟糕的情况
最大回撤是一个重要的风险指标,对于对冲基金和数量化策略交易,该指标比波动率还重要
为第 天的资产(指数), 则是 后面某一天的资产指数
则该资产(指数)在第 交易日以及整体的最大回撤率计算如下
xxxxxxxxxx
91def Cal_Withdrawal_Rate(seq):
2 Wdl_Rate_List = []
3 max_temp = 0
4 for i in range(len(seq)):
5 max_temp = max(max_temp, seq[i])
6 Wdl_Rate = (max_temp - seq[i]) / max_temp
7 Wdl_Rate_List.append(round(Wdl_Rate, 4))
8 Max_Index = Wdl_Rate_List.index(max(Wdl_Rate_List))
9 return max(Wdl_Rate_List), Max_Index, Wdl_Rate_List
夏普率及风险
夏普率代表投资组合(指数)对于波动风险的微分,既单位风险所获得的超额回报率(相当于无风险利率)
该比率越高,策略承担单位风险得到的超额回报率越高,公式为
其中, 为区间收益率, 是区间无风险收益率, 为区间波动率
注意,在主流的策略效果对比时,还是需要统一到年化来比较
xxxxxxxxxx
71def Cal_Sharp_Rate(seq, Rf=0.015, yd=250):
2 seqn = len(seq)
3 seq_return = Cal_Return_Rate(seq)
4 norisk_return = Rf * seqn / yd
5 Risk = float(np.array(seq_return[2]).std())
6 Sharp_Rate = (seq_return[0] - norisk_return) / Risk
7 return Sharp_Rate, Risk
信息比率及跟踪误差
跟踪误差:投资组合与基准指数收益率差值的波动风险
主动投资组合策略可能会拥有较大的跟踪误差
被动复制指数策略通常拥有较小的跟踪误差
信息比率:代表投资组合与基准指数收益率差值对于跟踪误差的微分
既单位跟踪误差所获得的超额回报率(相对于基准指数)
该比率越高,策略承担单位跟踪误差得到的超额回报率越高,公式为
其中, 为区间收益率, 为区间基准收益率(如 HS300 Idx), 为策略与基准每日收益率差值的区间标准差
同样,在主流的策略效果对比时,还是需要统一到年化来比较
xxxxxxxxxx
61def Cal_Info_Ratio(seq, seq_base):
2 seq_return = Cal_Return_Rate(seq)
3 seq_base_return = Cal_Return_Rate(seq_base)
4 sigma = float((np.array(seq_return[2]) - np.array(seq_base_return[2])).std())
5 ir = (seq_return[0] - seq_base_return[0]) / sigma
6 return ir, sigma
回测完成后,会返部部分前述的评价指标及运行时间(一个自然月的回测约需要 )
我们发现,投资组合的收益曲线背离大盘,在期末的收益率达到 ,明显跑赢市场的
并且策略拥有较小的最大回撤率,以及较大的夏普率,甚至还降低了风险
我们发现跟踪误差略大于策略风险,说明我们的策略还是一个偏主动的策略
资金变动情况
接下来再看这套投资组合的账单详情:
与纯市场方向相比,还是有适量的操作
并且事实上,止盈和止损次数均多于最小风险的投资组合(未展示)
从操作和收益来看也印证了 “高风险高收益” 的道理
仍持仓标的
当然,还会返回回测时间到期时,已经建仓但还没有平仓的标的,在其他分析中不能遗漏
程序还会返回回测过程中的收益率和(取负)回撤率曲线
我们发现策略的收益率曲线整体在沪深 300 指数以上,说明我们的策略整体收益情况超过基准指数
并且整体的回测幅度也相对较小,说明我们的策略较为稳定,在大盘回撤时,没有出现更糟糕的情况(对于在 A 股市场经历过实盘交易的投资者来说,这一点显得尤为重要)
至此,数据采集及建模 → 模型评估与仓位管理 → 构建投资组合与回测验证策略 的流程已经全部结束