量化投资学习笔记05——检验计算回测指标程序

因为对前面计算回测指标的程序的准确性还有疑问,我决定再验证一次。验证的方法是找一个带数据的完整的程序,先实现其程序,再用它的数据和我的程序计算,对比一下二者的结果。
在知乎上找到一篇,https://zhuanlan.zhihu.com/p/55425806 是用贵州茅台,工商银行和中国平安三只股票做回测。我照着其程序写了,计算结果与文章中的一致。

接下来就用pyalgotrade框架和我自己的封装来写了。因为pyalgotrade_tushare只能按整年进行数据抓取,把文章中的日期改为到2018年12月31日(而不是原文中的2019年1月18日)。
但是用我的程序算那些指标还是与文章里算的不对,尤其β值居然是0。我还是找现成的库吧。
找了一圈,发现一个我能用的:empyrical。但是尝试以后,发现还是有问题,如图:

用pyalgotrade文档里的例子做测试,前面是pyalgotrade里的回测结果,然后用其计算的收益率作为输入,用empyrical库计算其它指标,最大回撤是一致的,αβ值不知道是否正确,夏普比率相差太多啦。
于是决定回到最基础的办法:自己编个数据,按照这些指标的定义手算,再用程序验证吧。
本文以下参考《量化投资:以python为工具》一书相关章节。
假设有股票test,初始价格为1,每天涨1元,一共10天。基准指数base初始价格也是1,每天涨0.5元,一共10天。第一天各买入1股,计算每天的收益率。为了保持一致,假定初始投入2.5元,再少pyalgotrade会报现金不足。
test.csv

1
2
3
4
5
6
7
8
9
10
11
12
Date,Open,Close,High,Low,Volume
2019-01-01,2.369,1.0,9,0,1877797.0
2019-01-02,2.358,2.0,9,0,4404445.0
2019-01-03,2.335,3.0,9,0,4834089.0
2019-01-04,2.27,4.0,9,0,1525888.0
2019-01-05,2.287,5.0,9,0,2050543.0
2019-01-06,2.28,6.0,9,0,3371288.0
2019-01-07,2.269,7.0,9,0,3701781.0
2019-01-08,2.255,8.0,9,0,4884821.0
2019-01-09,2.239,9.0,19,0,2509259.0
2019-01-10,2.253,10.0,19,0,3339884.0
2019-01-10,2.253,10.0,19,0,3339884.0

base.csv

1
2
3
4
5
6
7
8
9
10
11
Date,Open,Close,High,Low,Volume
2019-01-01,2.369,1.0,9,0,1877797.0
2019-01-02,2.358,1.5,9,0,4404445.0
2019-01-03,2.335,2.0,9,0,4834089.0
2019-01-04,2.27,2.5,9,0,1525888.0
2019-01-05,2.287,3.0,9,0,2050543.0
2019-01-06,2.28,3.5,9,0,3371288.0
2019-01-07,2.269,4.0,9,0,3701781.0
2019-01-08,2.255,4.5,9,0,4884821.0
2019-01-09,2.239,5.0,9,0,2509259.0
2019-01-10,2.253,5.5,9,0,3339884.0

计算中只用Close一列,最低最高价也修改了,其它的保持原状,目的是使pyalgotrade框架能够使用数据。
接下来从收益和收益率开始计算。
资产的收益率是指投入某资产所能产生的收益与当初投资成本的比例。
收益率=投资收益/投资成本
期间投资收益=期末价格-期初价格+其它收益
期间收益率=期间收益/期初价格
每天为一期,为了跟pyalgotrade框架一致,第一天决策,次日才交易。
test的收益率
第一天: 2.5-2.5/2.5 = 0.0
第二天: 2.5-2.5/2.5 = 0.0
第三天: 3.5-2.5/2.5 = 0.4
第四天: 4.5-3.5/3.5 = 0.2857142857142857
以此类推。
base的收益率:
第一天: 2.5-2.5/2.5 = 0.0
第二天: 2.5-2.5/2.5 = 0.0
第三天: 3.0-2.5/2.5 = 0.2
第四天: 3.5-3.0/3.0 = 0.1666666666666667
以此类推。
现在用python算一下。
先读取数据

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
# 从文件中读取数据
test_df = pd.read_csv("test.csv", index_col = "Date")
print(test_df)
base_df = pd.read_csv("base.csv", index_col = "Date")
print(base_df)
# 提取收盘价信息
test_close = test_df["Close"]
base_close = base_df[["Close"]]
# test_close.name = "Close"
# base_close.name = "Close"
print(test_close, base_close)
# 计算每日收益率
# 初始投资
cash_test = 2.5
cash_base = 2.5
# 每期市值
position_test = []
position_base = []
print(test_close.values[0], base_close.values[0])
for i in range(len(test_close)):
if i == 0:
position_test.append(cash_test)
position_base.append(cash_base)
continue
elif i == 1:
cash_test = cash_test - test_close[0]
cash_base = cash_base - base_close.values[0][0]
if cash_test <= 0 or cash_base <= 0:
print("现金不足,退出")
position_test.append(cash_test + test_close[i-1])
position_base.append(cash_base + base_close.values[i-1][0])

print(position_test, position_base)
test_return = []
base_return = []
test_return.append(0.0)
base_return.append(0.0)
for i in range(1, len(position_test)):
print(i, position_test[i], position_test[i-1])
test_return.append((position_test[i] - position_test[i-1])/position_test[i-1])
base_return.append((position_base[i] - position_base[i-1])/position_base[i-1])

计算结果

对了。接下来计算年化收益率
年化收益率是把当前收益率(日、周、月收益率等)换算成年收益率,方便投资人比较不同期限的投资。这只是理论上的收益率,并不是投资人真正能获得的收益率。
持有T期收益为Rt, 一年有m个单期,则年化收益率为(Rt/T)×m
如果考虑复利,年化收益率=(1+Rt)^(1/(T/m))-1

1
2
3
4
5
6
7
8
9
10
# 计算年化收益率
np_test_return = np.array(test_return)
np_base_return = np.array(base_return)
annret_test = (1+np_test_return).cumprod()[-1]**(245/311) - 1
annret_base = (1+np_base_return).cumprod()[-1]**(245/311) - 1
print(annret_test, annret_base)
print("empyrical")
annret_test_ep = ep.annual_return(np_test_return)
annret_base_ep = ep.annual_return(np_base_return)
print(annret_test_ep, annret_base_ep)

结果
2.097306384013633 1.1227960188906434
empyrical里有计算年化收益率的函数,试试。
5080215298974669.0 28663443858.08141
差别好大,感觉年化收益率蛮坑的,理财,保险等机构都喜欢用这个概念,先略过吧。后面用不到。
接下来看风险指标
首先可以用收益率的标准差来衡量。

1
2
3
#衡量风险
# 标准差
print(np_test_return.std(), np_base_return.std())

结果
0.11597007874921565 0.06111509424879022
前者的风险更大。
最大回撤,因为我的数据就没有回撤,而且几个库计算的最大回撤值几乎一样,就不自己写了。用empyrical库试试。

1
2
# 最大回撤
print(ep.max_drawdown(np_test_return), ep.max_drawdown(np_base_return))

结果都是0.0
现在来计算策略的α和β值,其来自资本资产定价模型(CAPM),Rq为资产组合的收益,Rf为无风险资产收益,Rm为市场资产组合收益(一般以大盘指数代表),有如下关系:
E(Rq) - Rf = βqm(E(Rm) - Rf)
即,β值为策略收益与无风险收益之差与和市场平均收益与无风险收益之差的比值。
βqm又等于σ(Rq,Rm)/σ²(Rm),前者为资产组合收益率与市场投资组合收益率之间的协方差,后者为市场投资组合的方差,β值反映出投资组合的系统性风险。若β=1,则策略与市场的波动性是一致的,若β绝对值小于1,则策略的波动性小于市场,若β绝对值大于1,则策略的波动性大于市场。单只股票的期望收益是无风险收益加上系统性风险溢酬。非系统风险可以通过分散投资消除。
将模型写成不含期望值的形式:
Rit - Rft = α + β(Rmt - Rft) + ε
Rit,Rft,Rmt分别为个股收益率,无风险收益率和市场收益率,对这些资料进行线性回归,可以得到α和β值的估计值。β值可以解释个股过去收益率与风险的关系,根据这个模型,所有资产α值都应为0,若显著异于0,则个股有异常收益。Alpha值代表收益率胜过大盘的部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 计算αβ值
# 先将两个收益率合并到一起
Ret = pd.merge(pd.DataFrame(base_return), pd.DataFrame(test_return), left_index = True, right_index = True, how = "inner")
print(Ret)
# 计算无风险收益
rf = 1.036**(1/360) - 1.0
print(rf)
# 计算股票超额收益率和市场风险溢酬
Eret = Ret - rf
print(Eret)
# 接下来进行拟合
model = sm.OLS(np_test_return, sm.add_constant(np_base_return))
result = model.fit()
print(result.summary())


计算结果,α为-0.0279,β为1.8462,再用empyrical算一遍。

1
2
3
print("empyrical")
alpha, beta = ep.alpha_beta(np_test_return, np_base_return, 0.036)
print(alpha, beta)

计算结果
0.8273048650075308 1.8426052351694182
还是相差很多。β值很不确定,同一股票不同时期的β值相差很大。用历史数据计算β值,对投资的指导意义不大。所以,也许可以不用再纠结了。
最后算夏普值。
夏普比率就是一个可以同时对收益与风险加以综合考虑的三大经典指标之一。 投资中有一个常规的特 点,即投资标的的预期报酬越高,投资人所能忍受的波动风险越高;反之,预期报酬越低,波动风险也越低。所以理性的投资人选择投资标的与投资组合的主要目的为:在固定所能承受的风险下,追求最大的报酬;或在固定的预期报酬下,追求最低的风险。
。理性的投资者将选择并持有有效的投资组合,即那些在给定的风险水平下使期望回报最大化的投资组合,或那些在给定期望回报率的水平上使风险最小化的投资组合。解释起来非常简单,他认为投资者在建立有风险的投资组合时,至少应该要求投资回报达到无风险投资的回报,或者更多。
夏普比率目的是计算投资组合每承受一单位总风险,会产生多少的超额报酬。夏普指数代表投资人每多承担一分风险,可以拿到几分超额报酬;若为正值,代表基金报酬率高过波动风险;若为负值,代表基金操作风险大过于报酬率。这样一来,每个投资组合都可以计算Sharpe Ratio, 即投资回报与多冒风险的比例,这个比例越高,投资组合越佳。夏普比率没有基准点,因此其大小本身没有意义,只有在与其他组合的比较中才有价值。

1
2
3
# 计算夏普比率
sharpe = (np_test_return.mean() - 0.03)/np_test_return.std()*np.sqrt(252)
print(sharpe)

结果
17.79285680812303
再用empyrical算一次
16.879786078470694
两个结果是基本一致的。
接下来,就再用pyalgotrade回测一下吧。
代码我就不往上放了,只放结果。


还是有差异。尤其是α和β值。看了一下代码,在pyalgotrade的回测代码里增加了输出,发现成交价是2.36元,不是我想的2.0元。是因为这个原因吗?唉,不纠结了,就这样吧。用pyalgotrade回测算收益率、投资收益、夏普值,最大回撤等,用empyrical算α和β值。
本文代码:
https://github.com/zwdnet/MyQuant/tree/master/05
自己算指标在index.py里,用pyalgotrade回测在pyat_index.py里。

补充:

终于发现哪里错了!是数据,pyalgotrade默认用开盘价交易,不是我以为的收盘价。把开盘价改了跟收盘价一样,结果就和我手算的一样了。β值也完全一样了,但是α值和夏普值还是有差别。貌似用框架算的要可靠一些。

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

欢迎打赏!感谢支持!