本测试旨在重现一套比较简单且完备的量化框架
该框架基于现代投资组合理论,并应用主流的机器学习算法(SVM)进行分析
旨在初步形成一个量化投资的思路,辅助构建科学合理的投资策略
1$ python Init_StockALL_Sp.py2$ python stock_index_pro.py3$ python main_pro.py测试使用的Python版本:3.6.8
测试使用的Anaconda版本:1.9.6
xxxxxxxxxx21$ pip install tushare2$ pip install tushare --upgradexxxxxxxxxx11import tushare as tstushare版本需大于1.2.10
xxxxxxxxxx11ts.set_token('your token')完成调取tushare数据凭证的设置,通常只需要设置一次
xxxxxxxxxx31pro = ts.pro_api()2# 或者在初始化中直接设置token3pro = ts.pro_api('your token')xxxxxxxxxx31pro.daily() # 获取日K数据(未赋权)2pro.index_daily() # 获取指数行情3pro.trade_cal() # 获取交易日历xxxxxxxxxx11import datetimexxxxxxxxxx21import pymysql.cursors2import sqlalchemyxxxxxxxxxx51import numpy as np2import pandas as pd3from sklearn import svm4import pylab as *5import math数据获取
默认获取从预设时间(我们定义为年第个交易日)到最邻近交易日,股票池所有交易日的行情数据
注意更改 Init_StockALL_Sp.py 中的股票池
xxxxxxxxxx31# 设定需要获取数据的股票池, 比如与云计算、软件板块相关的标的2# 中兴通讯, 远光软件, 中国长城, 东方财富, 用友网络, 中科曙光, 中国软件, 浪潮信息, 宝信软件3stock_pool = ['000063.SZ', '002063.SZ', '000066.SZ', '300059.SZ', '600588.SH', '603019.SH', '600536.SH', '000977.SZ', '600845.SH']如果对于股票代码,所属板块,上市状态,上市日期等情况不甚了解,可以优先查询股票的 基本信息
xxxxxxxxxx21# 获取股票基本信息列表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 指数较为合理
xxxxxxxxxx31df = pro.index_daily(ts_code='000300.SH')2# 统一指数标注并删除原复杂指数代码标注3df['stock_code'] = 'HS300' 
存储至MySQL
部分示例

从获取数据的特征,基于 SVM 做分类问题,来对于涨跌判断进行建模
x1from sklearn import svm2import DC3# DC是将原始行情数据划分成SVM训练的各项数据集的预处理类45dc = 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 股市场没有有效的认沽机制,或者说做空条件过于严苛和高贵,所以我们不能将特征向量中的负值在策略中成为空头开仓,而是必须将其舍去(重置为 ),并将特征向量中的正值线性归一化(理论上这一步会极大地降低收益),由于我们需要挖掘期望收益为正的策略,归一化可以增加我们的资金使用效率
现代投资组合理论的主要实现如下
xxxxxxxxxx201# 求协方差矩阵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 策略,回测区间小于一个月意义不大)
股票池为,中国软件,中兴通讯,浪潮信息,用友网络,宝信软件
xxxxxxxxxx11stock_pool = ['600536.SH', '000063.SZ', '000977.SZ', '600588.SH', '600845.SH']无风险利率,设为一年期银行存款,年化
货币资金收益率,预设为年化
其他参数,如前所述,或采取默认值
回测时我们根据收盘价进行交易(假设在资金量不大时一定可以成交到模型中计划的数量)
在已有的训练、建模、预测后,我们进行模拟交易,投资组合变换的频率默认为 个交易日
根据回测区间第一个交易日的行情,进行一次 投资组合建仓(手续费 )
由于最小的交易单位是 股,我们会向下取 的倍数为第 支标的持仓股数
并按照如下公式更新资产信息
xxxxxxxxxx31new_capital = deal_buy.cur_capital - vol * buy_price * 0.00052new_money_lock = deal_buy.cur_money_lock + vol * buy_price3new_money_rest = deal_buy.cur_money_rest - vol * buy_price * 1.0005随后制定平仓(,如果在一个投资组合内,某标的被策略卖出,则会全部卖出,不涉及部分卖出的情况)的择时策略,当持仓非空时,依次进行如下步骤:
推进至回测区间内的下一交易日,对于 2-5 发生的情形,按照如下公式更新相关资产情况
xxxxxxxxxx51new_money_lock = deal.cur_money_lock - sell_price * hold_vol2new_money_rest = deal.cur_money_rest + sell_price * hold_vol * 0.99843new_capital = new_money_lock + new_money_rest4new_profit = (sell_price * 0.9984 - init_price * 1.0005) * hold_vol5new_profit_rate = sell_price * 0.9984 / (init_price * 1.0005)当单一持仓标的收益率超过 时,止盈平仓该标的(GOODSELL)
未有符合条件的执行或者全部执行完后下一步
当单一持仓标的亏损率超过 时,止损平仓该标的(BADSELL)
未有符合条件的执行或者全部执行完后下一步
当建仓后第 个交易日只拥有货币资金,则返回投资组合建仓板块
当建仓后第 个交易日还存在未平仓的标的
未有符合条件的执行则下一步
当单一持仓标的的 SVM 预测下个交易日为 (下跌时),预判平仓该标的(PredictSELL)
未有符合条件的执行或者全部执行完后下一步
执行资金情况更新
返回第一步
回测过程的简要流程图

回测完成后,会对所有的回测情况进行评估,选取了 个经典评价量化策略的指标,分别是
这样既可以对回测效果有很好的刻画,又会在一定程度上杜绝过拟合的情形
收益率
这是我们最关心的,相当于是回测区间内的收益效率衡量
对于第 交易日的账户权益或者指数,收益率计算公式如下
xxxxxxxxxx131def Cal_Return_Rate(seq, yd=250):2 seqn = len(seq)3 Return_Rate = (seq[-1] / seq[0]) - 14 Annual_Rate = math.pow((seq[-1] / seq[0]), yd / seqn) - 15 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最大回撤率
指在回测区间内内任一历史时点往后推,资产(指数)走到最低点时的收益率回撤幅度的最大值
最大回撤用来描述实行投资组合建仓后(指数)可能出现的最糟糕的情况
最大回撤是一个重要的风险指标,对于对冲基金和数量化策略交易,该指标比波动率还重要
为第 天的资产(指数), 则是 后面某一天的资产指数
则该资产(指数)在第 交易日以及整体的最大回撤率计算如下
xxxxxxxxxx91def Cal_Withdrawal_Rate(seq):2 Wdl_Rate_List = []3 max_temp = 04 for i in range(len(seq)):5 max_temp = max(max_temp, seq[i])6 Wdl_Rate = (max_temp - seq[i]) / max_temp7 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夏普率及风险
夏普率代表投资组合(指数)对于波动风险的微分,既单位风险所获得的超额回报率(相当于无风险利率)
该比率越高,策略承担单位风险得到的超额回报率越高,公式为
其中, 为区间收益率, 是区间无风险收益率, 为区间波动率
注意,在主流的策略效果对比时,还是需要统一到年化来比较
xxxxxxxxxx71def 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 / yd5 Risk = float(np.array(seq_return[2]).std())6 Sharp_Rate = (seq_return[0] - norisk_return) / Risk7 return Sharp_Rate, Risk信息比率及跟踪误差
跟踪误差:投资组合与基准指数收益率差值的波动风险
主动投资组合策略可能会拥有较大的跟踪误差
被动复制指数策略通常拥有较小的跟踪误差
信息比率:代表投资组合与基准指数收益率差值对于跟踪误差的微分
既单位跟踪误差所获得的超额回报率(相对于基准指数)
该比率越高,策略承担单位跟踪误差得到的超额回报率越高,公式为
其中, 为区间收益率, 为区间基准收益率(如 HS300 Idx), 为策略与基准每日收益率差值的区间标准差
同样,在主流的策略效果对比时,还是需要统一到年化来比较
xxxxxxxxxx61def 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]) / sigma6 return ir, sigma 
回测完成后,会返部部分前述的评价指标及运行时间(一个自然月的回测约需要 )
我们发现,投资组合的收益曲线背离大盘,在期末的收益率达到 ,明显跑赢市场的
并且策略拥有较小的最大回撤率,以及较大的夏普率,甚至还降低了风险
我们发现跟踪误差略大于策略风险,说明我们的策略还是一个偏主动的策略
资金变动情况
接下来再看这套投资组合的账单详情:

与纯市场方向相比,还是有适量的操作
并且事实上,止盈和止损次数均多于最小风险的投资组合(未展示)
从操作和收益来看也印证了 “高风险高收益” 的道理
仍持仓标的

当然,还会返回回测时间到期时,已经建仓但还没有平仓的标的,在其他分析中不能遗漏
程序还会返回回测过程中的收益率和(取负)回撤率曲线

我们发现策略的收益率曲线整体在沪深 300 指数以上,说明我们的策略整体收益情况超过基准指数
并且整体的回测幅度也相对较小,说明我们的策略较为稳定,在大盘回撤时,没有出现更糟糕的情况(对于在 A 股市场经历过实盘交易的投资者来说,这一点显得尤为重要)
至此,数据采集及建模 → 模型评估与仓位管理 → 构建投资组合与回测验证策略 的流程已经全部结束