量化分析师的Python日记【第12天:量化入门进阶之葵花宝典:因子如何产生和回测】
0 预备知识
预备知识包括:数学,计算机,投资学
数学方面至少包括微积分,线性代数,优化理论,概率统计基础,线性回归等知识点。数学出生最佳,一般理工科都基本满足要求,即使有所欠缺,花点时间也就自学补上了
计算机主要有两点:一是会编程;二是会数据分析,相信在量化实验室看到截止今天日记的同学都已经满足了
投资学方面只要通过大学的《投资学》课程就好,像William Sharpe等3人合著的《投资学》,要是能够通过CFA那就最好,知识面更广
1 入门阶段
Barra USE3 handbook
Barra是量化投资技术提供商,是量化投资先驱。其经典的美国股票风险模型第3版(USE3)手册,详细介绍了股票市场多因子模型的理论框架和实证细节。手册共几十页,不太长,描述规范清晰,不陷入无意义的细节,非常适合于入门, 点此下载
2 系统学习阶段
系统学习1:Quantitative Equity Portfolio Management(QEPM), Ludwig Chincarini 偏学术风格。
偏学术界的作者撰写的关于量化股票组合投资的系统教程。尤其是前几章概述部分写得非常精彩、易懂、准确。把该领域的各个方面高屋建瓴地串讲了一遍。后面部分的章节似乎略有些学术了,但也值得一读。由于其较高的可读性,适于初学者学习。
系统学习2:Active Portfolio Management(APM), Grinold & Kahn 偏业界风格。
业界先驱所著,作者均曾任Barra公司的研究总监。本书深度相对较深,描述也偏实践,介绍了许多深刻的真知。并且书中很多论述精彩而透彻。该书被奉为量化组合投资业界圣经。不过该书有些章节撰写得深度不一,初学者容易感到阅读起来有点困难。所以推荐:首次阅读不必纠结看不懂的细节,只要不影响后续阅读就跳过具体细节;有一定基础后,建议经常反复阅读本书。
系统学习3:Quantitative Equity Portfolio Management(QEPM), Qian & Hua & Sorensen APM的补充
业界人士所著。针对性地对APM没有展开讲的一些topic做了很好的深入探讨。建议在APM之后阅读。该书风格比较数学,不过对数学专业背景的人并不太难。撰写文字也比较流畅。
注:修行上述3本葵花宝典是否要割舍些什么?主要是与亲友坐在一起聊天喝茶的时光、一些睡觉的时间以及购书需要上千元钱(建议读英文原著);好消息是,练成之后,不仅钱可以赚回来,空闲时间也会多起来。
3 实践阶段
券商卖方金工研究报告:多因子模型、选股策略、择时策略
系统学习上面的材料之后,你已经有了分辨能力,这是看数量众多的券商卖方金工研究报告,就可以庖丁解牛,分辨真伪,总能筛选出优质信息积累下来了。
值得总结的是数学、计算机、分析框架等工具都只是量化投资的形,优质投资想法才是灵魂。所以在修炼上述量化投资的基本功的同时,请不要忘记向有洞察力、有独立思考的其它派系的投资专家学习,无论他/她是价值投资、成长投资、涨停板敢死队、技术分析、主题投资、逆向投资、各类套利。将你自己想出的或者从别人那里习得的投资想法,用量化框架验证、改进、去伪存真,并最终上实盘创造价值。
最推荐的入行过程:学习上述材料的同时,在通联量化实验室利用海量数据编程实现,理论付诸实践!
4 实战操作示例
在关于pandas
的前两篇介绍中,我们已经接触了不少关于Series
和DataFrame
的操作以及函数。本篇将以实际的例子来介绍pandas
在处理实际金融数据时的应用。
因子选股是股票投资中最常用的一种分析手段,利用量化计算的因子从成百上千的股票中进行快速筛选,帮助投资者从海量的数据中快速确定符合要求的目标,以下我们以量化因子计算过程的实例来展示如何利用pandas处理数据。
首先,我们依然是导入需要的一些外部模块:
import numpy as np
import pandas as pd
import datetime as dt
from pandas import Series, DataFrame, isnull
from datetime import timedelta, datetime
from CAL.PyCAL import *
pd.set_option('display.width', 200)
接着我们定义股票池和计算时所需要的时间区间参数。通常而言,计算某个因子是基于全A股的,这里作为示例,以HS300作为股票池。以计算市净率(PB)为例,我们取近一年的数据:
universe = set_universe('HS300')
today = Date.todaysDate()
start_date = (today - Period('1Y')).toDateTime().strftime('%Y%m%d')
end_date = today.toDateTime().strftime('%Y%m%d')
print 'start_date'
print start_date
print 'end_date'
print end_date
start_date
20150714
end_date
20160714
市净率是每股市价(Price)和每股净资产(Book Value)的比值,计算时通常使用总市值和归属于母公司所有者权益合计之比得到。前者通过访问股票日行情数据可以获得,后者在资产负债表上能够查到。在量化实验室中提供了访问股票日行情和资产负债表的API,可以获得相应数据。需要注意的一点是在获取财务报表数据时,因为只能指定一种类型的财报(季报,半年报,年报),需要做一个循环查询,并将获取到的DataFrame
数据按垂直方向拼接,这里使用了concat
函数:
market_capital = DataAPI.MktEqudGet(secID=universe, field=['secID', 'tradeDate', 'marketValue', 'negMarketValue'], beginDate=start_date, endDate=end_date, pandas='1')
equity = DataFrame()
for rpt_type in ['Q1', 'S1', 'Q3', 'A']:
try:
tmp = DataAPI.FdmtBSGet(secID=universe, field=['secID', 'endDate', 'publishDate', 'TEquityAttrP'], beginDate=start_date, publishDateEnd=end_date, reportType=rpt_type)
except:
tmp = DataFrame()
equity = pd.concat([equity, tmp], axis=0)
print 'Data of TEquityAttrP:'
print equity.head()
print 'Data of marketValue:'
print market_capital.head()
Data of TEquityAttrP:
secID endDate publishDate TEquityAttrP
0 000001.XSHE 2016-03-31 2016-04-21 1.875690e+11
1 000002.XSHE 2016-03-31 2016-04-28 1.006367e+11
2 000009.XSHE 2016-03-31 2016-04-29 4.467273e+09
3 000039.XSHE 2016-03-31 2016-04-29 5.668081e+09
4 000060.XSHE 2016-03-31 2016-04-28 7.340159e+09
Data of marketValue:
secID tradeDate marketValue negMarketValue
0 000001.XSHE 2015-07-14 1.984613e+11 1.637222e+11
1 000001.XSHE 2015-07-15 1.943118e+11 1.602991e+11
2 000001.XSHE 2015-07-16 1.945980e+11 1.605351e+11
3 000001.XSHE 2015-07-17 1.977459e+11 1.631320e+11
4 000001.XSHE 2015-07-20 1.945980e+11 1.605351e+11
对于市值的数据,每个交易日均有提供,实际上我们多取了数据,我们只需要最新的市值数据即可。为此,我们将数据按股票代码和交易日进行排序,并按股票代码丢弃重复数据。以下代码表示按股票代码和交易日进行升序排序,并在丢弃重复值时,保留最后一个(默认是第一个):
market_capital = market_capital.sort(columns=['secID', 'tradeDate'], ascending=[True, True])
market_capital = market_capital.drop_duplicates(subset='secID', take_last=True)
并非所有的数据都是完美的,有时候也会出现数据的缺失。我们在计算时无法处理缺失的数据,需要丢弃。下面这一行代码使用了isnull
函数检查数据中总市值的缺失值,返回的是一个等长的逻辑Series
,若数据缺失则为True
。为尽可能多利用数据,我们考虑在总市值缺失的情况下,若流通市值有数值,则使用流通市值替换总市值,仅在两者皆缺失的情况下丢弃数据(虽然多数情况下是流通市值缺失,有总市值的数据,这一处理方式在其它使用到流通市值计算的情形中可以参考):
market_capital['marketValue'][isnull(market_capital['marketValue'])] = market_capital['negMarketValue'][isnull(market_capital['marketValue'])]
以下代码使用drop
函数舍去了流通市值这一列,使用dropna
函数丢弃缺失值,并使用rename
函数将列marketValue
重命名为numerator
:
market_capital = market_capital.drop('negMarketValue', axis=1)
numerator = market_capital.dropna()
numerator.rename(columns={'marketValue': 'numerator'}, inplace=True)
我们可以看一下处理好的分子:
print numerator
secID tradeDate numerator
244 000001.XSHE 2016-07-13 1.543620e+11
489 000002.XSHE 2016-07-13 2.022373e+11
734 000009.XSHE 2016-07-13 2.257608e+10
49244 000027.XSHE 2016-07-13 2.755322e+10
979 000039.XSHE 2016-07-13 4.434777e+10
49489 000046.XSHE 2016-07-13 5.222182e+10
1224 000060.XSHE 2016-07-13 2.739233e+10
1469 000061.XSHE 2016-07-13 2.161932e+10
1714 000063.XSHE 2016-07-13 6.184518e+10
1959 000069.XSHE 2016-07-13 5.743977e+10
24744 000100.XSHE 2016-07-13 4.335857e+10
24989 000156.XSHE 2016-07-13 2.833737e+10
2204 000157.XSHE 2016-07-13 3.287913e+10
49734 000166.XSHE 2016-07-13 1.291645e+11
25234 000333.XSHE 2016-07-13 1.750697e+11
2449 000338.XSHE 2016-07-13 3.266872e+10
2694 000402.XSHE 2016-07-13 3.000886e+10
49979 000413.XSHE 2016-07-13 3.831165e+10
50224 000415.XSHE 2016-07-13 4.217844e+10
2939 000423.XSHE 2016-07-13 3.700454e+10
3184 000425.XSHE 2016-07-13 2.249481e+10
50469 000503.XSHE 2016-07-13 3.908079e+10
3429 000538.XSHE 2016-07-13 7.392896e+10
50714 000540.XSHE 2016-07-13 3.147026e+10
50959 000559.XSHE 2016-07-13 3.634171e+10
3674 000568.XSHE 2016-07-13 4.571343e+10
25479 000623.XSHE 2016-07-13 2.298707e+10
3919 000625.XSHE 2016-07-13 7.134216e+10
4164 000630.XSHE 2016-07-13 2.753465e+10
4409 000651.XSHE 2016-07-13 1.065987e+11
... ... ... ...
45079 601669.XSHG 2016-07-13 8.307799e+10
45324 601688.XSHG 2016-07-13 1.519940e+11
71425 601718.XSHG 2016-07-13 3.189739e+10
71670 601727.XSHG 2016-07-13 1.034922e+11
45569 601766.XSHG 2016-07-13 2.573330e+11
71915 601788.XSHG 2016-07-13 7.094565e+10
45814 601800.XSHG 2016-07-13 1.788926e+11
23764 601808.XSHG 2016-07-13 6.083780e+10
46059 601818.XSHG 2016-07-13 1.750466e+11
24009 601857.XSHG 2016-07-13 1.356185e+12
46304 601866.XSHG 2016-07-13 4.977011e+10
72160 601872.XSHG 2016-07-13 2.771617e+10
46549 601888.XSHG 2016-07-13 4.459454e+10
46794 601898.XSHG 2016-07-13 7.623731e+10
47039 601899.XSHG 2016-07-13 8.466495e+10
47284 601901.XSHG 2016-07-13 6.478664e+10
72405 601919.XSHG 2016-07-13 5.527004e+10
47529 601928.XSHG 2016-07-13 2.842653e+10
47774 601933.XSHG 2016-07-13 3.644512e+10
48019 601939.XSHG 2016-07-13 1.265056e+12
48264 601958.XSHG 2016-07-13 2.913624e+10
72650 601985.XSHG 2016-07-13 1.092693e+11
24254 601988.XSHG 2016-07-13 9.832552e+11
48509 601989.XSHG 2016-07-13 1.327548e+11
72895 601991.XSHG 2016-07-13 5.430495e+10
48754 601992.XSHG 2016-07-13 4.399242e+10
24499 601998.XSHG 2016-07-13 2.788525e+11
73140 603000.XSHG 2016-07-13 2.080911e+10
73385 603885.XSHG 2016-07-13 3.755758e+10
48999 603993.XSHG 2016-07-13 7.734337e+10
[300 rows x 3 columns]
接下来处理分母数据。同样,为保留最新数据,对权益数据按股票代码升序,报表日期和发布日期按降序排列。随后丢弃缺失数据并按照股票代码去掉重复项,更改列名TEquityAttrP
为denominator
:
equity = equity.sort(columns=['secID', 'endDate', 'publishDate'], ascending=[True, False, False])
equity = equity.dropna()
equity = equity.drop_duplicates(cols='secID')
denominator = equity
denominator.rename(columns={"TEquityAttrP": "denominator"}, inplace=True)
处理好的分母:
print denominator
secID endDate publishDate denominator
0 000001.XSHE 2016-03-31 2016-04-21 1.875690e+11
1 000002.XSHE 2016-03-31 2016-04-28 1.006367e+11
2 000009.XSHE 2016-03-31 2016-04-29 4.467273e+09
203 000027.XSHE 2016-03-31 2016-04-30 2.139399e+10
3 000039.XSHE 2016-03-31 2016-04-29 5.668081e+09
204 000046.XSHE 2016-03-31 2016-04-28 1.632725e+10
4 000060.XSHE 2016-03-31 2016-04-28 7.340159e+09
5 000061.XSHE 2016-03-31 2016-04-30 4.845586e+09
6 000063.XSHE 2016-03-31 2016-04-29 3.902424e+10
7 000069.XSHE 2016-03-31 2016-04-29 3.836175e+10
100 000100.XSHE 2016-03-31 2016-04-21 2.461872e+10
101 000156.XSHE 2016-03-31 2016-04-25 9.396250e+09
8 000157.XSHE 2016-03-31 2016-04-23 3.929794e+10
205 000166.XSHE 2016-03-31 2016-04-30 5.003656e+10
102 000333.XSHE 2016-03-31 2016-04-30 5.385758e+10
9 000338.XSHE 2016-03-31 2016-04-30 3.198088e+10
10 000402.XSHE 2016-03-31 2016-04-30 2.626535e+10
206 000413.XSHE 2016-03-31 2016-04-29 1.466822e+10
207 000415.XSHE 2016-03-31 2016-04-30 2.720240e+10
11 000423.XSHE 2016-03-31 2016-04-20 7.558454e+09
12 000425.XSHE 2016-03-31 2016-04-26 2.057239e+10
208 000503.XSHE 2016-03-31 2016-04-29 1.347180e+09
13 000538.XSHE 2016-03-31 2016-04-27 1.405493e+10
209 000540.XSHE 2016-03-31 2016-04-29 1.271475e+10
210 000559.XSHE 2016-03-31 2016-04-20 4.417438e+09
14 000568.XSHE 2016-03-31 2016-04-28 1.083373e+10
103 000623.XSHE 2016-03-31 2016-04-29 1.743589e+10
15 000625.XSHE 2016-03-31 2016-04-30 3.702576e+10
16 000630.XSHE 2016-03-31 2016-04-30 1.374468e+10
17 000651.XSHE 2016-03-31 2016-04-30 5.069274e+10
.. ... ... ... ...
185 601669.XSHG 2016-03-31 2016-04-30 5.714574e+10
186 601688.XSHG 2016-03-31 2016-04-29 8.060570e+10
295 601718.XSHG 2016-03-31 2016-04-19 1.303635e+10
296 601727.XSHG 2016-03-31 2016-04-30 3.758142e+10
187 601766.XSHG 2016-03-31 2016-04-28 9.868600e+10
297 601788.XSHG 2016-03-31 2016-04-26 3.993967e+10
188 601800.XSHG 2016-03-31 2016-04-27 1.479940e+11
96 601808.XSHG 2016-03-31 2016-04-29 4.578060e+10
189 601818.XSHG 2016-03-31 2016-04-30 2.323020e+11
97 601857.XSHG 2016-03-31 2016-04-29 1.169389e+12
190 601866.XSHG 2016-03-31 2016-04-29 1.801931e+10
298 601872.XSHG 2016-03-31 2016-04-22 1.404631e+10
191 601888.XSHG 2016-03-31 2016-04-23 1.188385e+10
192 601898.XSHG 2016-03-31 2016-04-28 8.306575e+10
193 601899.XSHG 2016-03-31 2016-04-30 2.722604e+10
194 601901.XSHG 2016-03-31 2016-04-29 3.514534e+10
300 601919.XSHG 2016-03-31 2016-04-29 2.253836e+10
195 601928.XSHG 2016-03-31 2016-04-30 1.093871e+10
196 601933.XSHG 2016-03-31 2016-05-06 1.269974e+10
198 601939.XSHG 2016-03-31 2016-04-30 1.499405e+12
199 601958.XSHG 2016-03-31 2016-04-29 1.271524e+10
301 601985.XSHG 2016-03-31 2016-04-29 3.860421e+10
98 601988.XSHG 2016-03-31 2016-04-27 1.348157e+12
200 601989.XSHG 2016-03-31 2016-04-29 5.696661e+10
302 601991.XSHG 2016-03-31 2016-04-28 4.568534e+10
201 601992.XSHG 2016-03-31 2016-04-27 3.824465e+10
99 601998.XSHG 2016-03-31 2016-04-28 3.286200e+11
303 603000.XSHG 2016-03-31 2016-04-29 2.734087e+09
304 603885.XSHG 2016-03-31 2016-04-21 3.820480e+09
202 603993.XSHG 2016-03-31 2016-04-29 1.757461e+10
[300 rows x 4 columns]
分子分母处理好之后,我们将两个DataFrame
使用merge
函数合并,使用参数how='inner'
保留在两者中均存在的股票。
dat_info = numerator.merge(denominator, on='secID', how='inner')
作为比值,分母不可以为零,这里我们通过设置分母绝对值大于一个很小的数来过滤不符合要求的数据。随后直接通过DataFrame['Column_name']
的复制添加一列PB:
dat_info = dat_info[abs(dat_info['denominator']) >= 1e-8]
dat_info['PB'] = dat_info['numerator'] / dat_info['denominator']
pb_signal = dat_info[['secID', 'PB']]
pb_signal = pb_signal.set_index('secID')['PB']
print pb_signal
secID
000001.XSHE 0.822961
000002.XSHE 2.009577
000009.XSHE 5.053661
000027.XSHE 1.287895
000039.XSHE 7.824125
000046.XSHE 3.198446
000060.XSHE 3.731845
000061.XSHE 4.461653
000063.XSHE 1.584789
000069.XSHE 1.497319
000100.XSHE 1.761203
000156.XSHE 3.015817
000157.XSHE 0.836663
000166.XSHE 2.581403
000333.XSHE 3.250605
000338.XSHE 1.021508
000402.XSHE 1.142527
000413.XSHE 2.611881
000415.XSHE 1.550541
000423.XSHE 4.895781
000425.XSHE 1.093446
000503.XSHE 29.009320
000538.XSHE 5.260001
000540.XSHE 2.475100
000559.XSHE 8.226875
000568.XSHE 4.219546
000623.XSHE 1.318376
000625.XSHE 1.926825
000630.XSHE 2.003295
000651.XSHE 2.102841
...
601669.XSHG 1.453791
601688.XSHG 1.885648
601718.XSHG 2.446804
601727.XSHG 2.753812
601766.XSHG 2.607594
601788.XSHG 1.776320
601800.XSHG 1.208783
601808.XSHG 1.328899
601818.XSHG 0.753530
601857.XSHG 1.159739
601866.XSHG 2.762044
601872.XSHG 1.973199
601888.XSHG 3.752535
601898.XSHG 0.917795
601899.XSHG 3.109705
601901.XSHG 1.843392
601919.XSHG 2.452265
601928.XSHG 2.598709
601933.XSHG 2.869754
601939.XSHG 0.843705
601958.XSHG 2.291442
601985.XSHG 2.830503
601988.XSHG 0.729333
601989.XSHG 2.330397
601991.XSHG 1.188674
601992.XSHG 1.150289
601998.XSHG 0.848556
603000.XSHG 7.610988
603885.XSHG 9.830593
603993.XSHG 4.400859
Name: PB, dtype: float64
好了接下来我们把以上PB因子计算过程变成一个函数,使得它可以计算回测开始时间到结束时间的PB值,这样我们可以在通联的多因子信号分析工具RDP中方便的测试
def str2date(date_str):
date_obj = dt.datetime(int(date_str[0:4]), int(date_str[4:6]), int(date_str[6:8]))
return Date.fromDateTime(date_obj)
def signal_pb_calc(universe, current_date):
today = str2date(current_date)
start_date = (today - Period('1Y')).toDateTime().strftime('%Y%m%d')
end_date = today.toDateTime().strftime('%Y%m%d')
# dealing with the numerator
market_capital = DataAPI.MktEqudGet(secID=universe, field=['secID', 'tradeDate', 'marketValue', 'negMarketValue', 'turnoverVol'], beginDate=start_date, endDate=end_date, pandas='1')
market_capital = market_capital[market_capital['turnoverVol'] > 0]
market_capital = market_capital.sort(columns=['secID', 'tradeDate'], ascending=[True, True])
market_capital = market_capital.drop_duplicates(subset='secID', take_last=True)
market_capital['marketValue'][isnull(market_capital['marketValue'])] = market_capital['negMarketValue'][isnull(market_capital['marketValue'])]
market_capital = market_capital.drop('negMarketValue', axis=1)
numerator = market_capital.dropna()
numerator.rename(columns={'marketValue': 'numerator'}, inplace=True)
# dealing with the denominator
equity = DataFrame()
for rpt_type in ['Q1', 'S1', 'Q3', 'A']:
try:
tmp = DataAPI.FdmtBSGet(secID=universe, field=['secID', 'endDate', 'publishDate', 'TEquityAttrP'], beginDate=start_date, publishDateEnd=end_date, reportType=rpt_type)
except:
tmp = DataFrame()
equity = pd.concat([equity, tmp], axis=0)
equity = equity.sort(columns=['secID', 'endDate', 'publishDate'], ascending=[True, False, False])
equity = equity.dropna()
equity = equity.drop_duplicates(cols='secID')
denominator = equity
denominator.rename(columns={"TEquityAttrP": "denominator"}, inplace=True)
# merge two dataframe and calculate price-to- book ratio
dat_info = numerator.merge(denominator, on='secID', how='inner')
dat_info = dat_info[abs(dat_info['denominator']) >= 1e-8]
dat_info['PB'] = dat_info['numerator'] / dat_info['denominator']
pb_signal = dat_info[['secID', 'PB']]
pb_signal["secID"] = pb_signal["secID"].apply(lambda x:x[:6])
return pb_signal
此代码完成的功能是:
- 计算沪深300成分股在一段时间内的PB值作为信号
- 把这些PB数据按照天存储为csv文件
- 把csv文件打包成zip
可以将这些文件下载到本地,解压到一个文件夹(比如PB_for_Mercury_DEMO
),然后上传到RDP(通联策略研究)中当做信号使用。
start = datetime(2015, 1, 1)
end = datetime(2015, 4, 23)
univ = set_universe('HS300')
cal = Calendar('China.SSE')
all_files = []
today = start
while((today - end).days < 0):
today_CAL = Date.fromDateTime(today)
if(cal.isBizDay(today_CAL)):
today_str = today.strftime("%Y%m%d")
print "Calculating PB values on " + today_str
pb_value = signal_pb_calc(univ, today_str)
file_name = today_str + '.csv'
pb_value.to_csv(file_name, index=False, header=False)
all_files.append(file_name)
today = today + timedelta(days=1)
# exporting all *.csv files to PB.zip
zip_files("PB"+ "_" + start.strftime("%Y%m%d") + "_" + end.strftime("%Y%m%d"), all_files)
# delete all *.csv
delete_files(all_files)
第一步:解压点击‘上传新信号’,选择信号文件夹并为信号命名,然后,开始上传;
第二步:选中上传的新信号,点击 ‘开始回测’;
第三步:进行回测的各种配置;
第四步:开始回测,回测完成后,点击报告的链接,查看回测结果;
第五步:查看回测结果。
以上研究过程演示了单个因子的产生和快速回测的过程,后面会介绍多因子的策略框架