量化投资学习笔记80——实现量化交易经典策略:计算回测指标

使用backtrader计算α,β,信息比例等回测指标。
参考:
https://www.r-bloggers.com/stock-trading-analytics-and-optimization-in-python-with-pyfolio-rs-performanceanalytics-and-backtrader/
https://teddykoker.com/2019/05/improving-cross-sectional-mean-reversion-strategy-in-python/
对于框架没有的指标,可以自建分析类,继承自backtrader.Analyzer,实现next()和stop()函数来计算指标,get_analysis()来获取分析结果。然后用addanalyzer()方法将分析类加载到cerebro对象里,这是一种轻量的做法,不需要在分析类里提供数据。
上面文章的作者是调用用R的PerformanceAnalytics库计算指标,我这没成功,搜了一下,用另一个python库,empyreal吧。
使用empyreal需要两个收益率序列,分别是策略的收益率和基准的收益率,这里的收益率是当前交易日相对前一交易日的收益率。对于策略收益率,可以向cerebro里添加TimeReturn分析器获得。

1
self.__cerebro.addanalyzer(btay.TimeReturn, _name = "TR")

在运行了回测以后获得策略收益率序列:

1
self.__returns = pd.Series(self.__results[0].analyzers.TR.get_analysis())

基准策略的收益率序列呢?再建一个策略类吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 基准策略类,用于计算α,β等回测指标
# 采用第一天全仓买入并持有的策略
class Benchmark(bt.Strategy):
def __init__(self):
self.order = None
self.bBuy = False
self.dataclose = self.datas[0].close

def next(self):
if self.bBuy == True:
return
else:
cash = self.broker.get_cash()
stock = math.ceil(cash/self.dataclose/100)*100 - 100
self.order = self.buy(size = stock, price = self.datas[0].close)
self.bBuy = True

def stop(self):
self.order = self.close()

再用前述方法计算收益率序列。
接着自己定义一个risk分析类。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import empyrical as ey
# 用empyrical库计算风险指标
class riskAnalyzer:
def __init__(self, returns, benchReturns, riskFreeRate = 0.02):
self.__returns = returns
self.__benchReturns = benchReturns
self.__risk_free = riskFreeRate
self.__alpha = 0.0
self.__beta = 0.0
self.__info = 0.0
self.__vola = 0.0
self.__omega = 0.0
self.__sharpe = 0.0
self.__sortino = 0.0
self.__calmar = 0.0

def run(self):
# 计算各指标
self._alpha()
self._beta()
self._info()
self._vola()
self._omega()
self._sharpe()
self._sortino()
result = pd.Series()
result["阿尔法"] = self.__alpha
result["贝塔"] = self.__beta
result["信息比例"] = self.__info
result["策略波动率"] = self.__vola
result["欧米伽"] = self.__omega
result["夏普值"] = self.__sharpe
result["sortino"] = self.__sortino
result["calmar"] = self.__calmar
return result

def _alpha(self):
self.__alpha = ey.alpha(returns = self.__returns, factor_returns = self.__benchReturns, risk_free = self.__risk_free)

def _beta(self):
self.__beta = ey.beta(returns = self.__returns, factor_returns = self.__benchReturns, risk_free = self.__risk_free)

def _info(self):
self.__info = ey.excess_sharpe(returns = self.__returns, factor_returns = self.__benchReturns)

def _vola(self):
self.__vola = ey.annual_volatility(self.__returns, period='daily')

def _omega(self):
self.__omega = ey.omega_ratio(returns = self.__returns, risk_free = self.__risk_free)

def _sharpe(self):
self.__sharpe = ey.sharpe_ratio(returns = self.__returns)

def _sortino(self):
self.__sortino = ey.sortino_ratio(returns = self.__returns)

def _calmar(self):
self.__calmar = ey.calmar_ratio(returns = self.__returns)
在BackTest里增加一个风险分析函数:
# 分析策略的风险指标
def _riskAnaly(self):
risk = riskAnalyzer(self.__returns, self.__benchReturns)
result = risk.run()
self.__backtestResult["阿尔法"] = result["阿尔法"]
self.__backtestResult["贝塔"] = result["贝塔"]
self.__backtestResult["信息比例"] = result["信息比例"]
self.__backtestResult["策略波动率"] = result["策略波动率"]
self.__backtestResult["欧米伽"] = result["欧米伽"]
self.__backtestResult["夏普值"] = result["夏普值"]
self.__backtestResult["sortino"] = result["sortino"]
self.__backtestResult["calmar"] = result["calmar"]

运行

OK,有点问题,夏普值Backtrader和empyrical的计算结果有差异,可能参数设置有差异吧。调试一下看看。
用这篇文章 https://www.cnblogs.com/bitquant/p/8432891.html 的数据和例子,用我的程序计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if __name__ == "__main__":
# 构造测试数据
returns = pd.Series(
index = pd.date_range("2017-03-10", "2017-03-19"),
data = (-0.012143, 0.045350, 0.030957, 0.004902, 0.002341, -0.02103, 0.00148, 0.004820, -0.00023, 0.01201))
print(returns)
benchmark_returns = pd.Series(
index = pd.date_range("2017-03-10", "2017-03-19"),
data = ( -0.031940, 0.025350, -0.020957, -0.000902, 0.007341, -0.01103, 0.00248, 0.008820, -0.00123, 0.01091))
print(benchmark_returns)
# 计算累积收益率
creturns = ey.cum_returns(returns)
print("累积收益率\n", creturns)
risk = riskAnalyzer(returns, benchmark_returns, riskFreeRate = 0.01)
results = risk.run()
print(results)


大部分结果跟文章里是一致的,但是阿尔法值文章里是0.7781,Calmar比率为207.1054,这两个值不对。
直接调用empyreal算一下看看。

1
2
3
4
5
    # 直接调用empyrical试试
alpha = ey.alpha(returns = returns, factor_returns = benchmark_returns, risk_free = 0.01)
calmar = ey.calmar_ratio(returns)
print(alpha, calmar)
1.1749273413863706 207.10543798664153

阿尔法值还是不对,calmar值对了。先改计算calmar的程序。原来是我忘了调用我的类里面计算calmar的程序,加上去就对了。那么阿尔法值呢?
自己算一下:

1
2
3
4
5
6
7
8
    # 自己计算阿尔法值
annual_return = ey.annual_return(returns)
annual_bench = ey.annual_return(benchmark_returns)
print(annual_return, annual_bench)
alpha2 = (annual_return - 0.01) - results["贝塔"]*(annual_bench - 0.01)
print(alpha2)
4.355427360859065 -0.26851705257114356
4.501836011461441

算出来4.5,差别更大了。换篇文章看看。
http://www.imooc.com/article/293203

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
# 自己计算阿尔法贝塔
def get_return(code, startdate, endate):
df = ts.get_k_data(code, ktype = "D", autype = "qfq", start = startdate, end = endate)
p1 = np.array(df.close[1:])
p0 = np.array(df.close[:-1])
logret = np.log(p1/p0)
rate = pd.DataFrame()
rate[code] = logret
rate.index = df["date"][1:]
return rate
def alpha_beta(code, startdate, endate):
mkt_ret = get_return("sh", startdate, endate)
stock_ret = get_return(code, startdate, endate)
df = pd.merge(mkt_ret, stock_ret, left_index = True, right_index = True)
x = df.iloc[:, 0]
y = df.iloc[:, 1]
beta, alpha, r_value, p_value, std_err = stats.linregress(x, y)
return (alpha, beta)
def stocks_alpha_beta(stocks, startdate, endate):
df = pd.DataFrame()
alpha = []
beta = []
for code in stocks.values():
a, b = alpha_beta(code, startdate, endate)
alpha.append(float("%.4f"%a))
beta.append(float("%.4f"%b))
df["alpha"] = alpha
df["beta"] = beta
df.index = stocks.keys()
return df

startdate = "2017-01-01"
endate = "2018-11-09"
stocks={'中国平安':'601318','格力电器':'000651','招商银行':'600036','恒生电子':'600570','中信证券':'600030','贵州茅台':'600519'}
results = stocks_alpha_beta(stocks, startdate, endate)
print(results)
1
2
3
4
5
6
7
8
计算结果
alpha beta
中国平安 0.0020 1.2701
格力电器 0.0016 1.2261
招商银行 0.0016 1.0667
恒生电子 0.0007 1.4698
中信证券 0.0008 1.3857
贵州茅台 0.0017 1.0937

跟原文的结果一致。下面用empyreal算一下看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 用empyrical计算
def stocks_alpha_beta2(stocks, startdate, endate):
df = pd.DataFrame()
alpha = []
beta = []
for code in stocks.values():
a, b = empyrical_alpha_beta(code, startdate, endate)
alpha.append(float("%.4f"%a))
beta.append(float("%.4f"%b))
df["alpha"] = alpha
df["beta"] = beta
df.index = stocks.keys()
return df
def empyrical_alpha_beta(code, startdate, endate):
mkt_ret = get_return("sh", startdate, endate)
stock_ret = get_return(code, startdate, endate)
alpha, beta = ey.alpha_beta(returns = stock_ret, factor_returns = mkt_ret)
return (alpha, beta)

results2 = stocks_alpha_beta2(stocks, startdate, endate)
print("empyrical计算结果")
print(results2)


贝塔值是一样的,阿尔法值差别就大了。
除一下两组阿尔法值:

1
2
3
4
5
6
7
8
print(results2["alpha"]/results["alpha"])

中国平安 317.750000
格力电器 306.375000
招商银行 315.125000
恒生电子 279.428571
中信证券 269.750000
贵州茅台 307.529412

并不一致。
经过不断试验,发现是参数的问题:
annualization = 1就完全一样啦。这个参数是设置收益率是多长时间间隔的收益率,1代表每天的收益率。

1
alpha, beta = ey.alpha_beta(returns = stock_ret, factor_returns = mkt_ret, annualization = 1)


再回测一下双均线策略

现在的问题就剩夏普值的计算了,这又是一个比较重要的数据。还是用一样的方法调试:自己实现一下计算夏普值,再跟empyrical计算的结果对比。
照这篇文章: https://zhuanlan.zhihu.com/p/61949367 的方法来。

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
# 测试夏普值的计算
def testSharpe():
# 读取数据
stock_data = pd.read_csv("stock_data.csv", parse_dates = ["Date"], index_col = ["Date"]).dropna()
benchmark_data = pd.read_csv("benchmark_data.csv", parse_dates = ["Date"], index_col = ["Date"]).dropna()
# 了解数据
print("Stocks\n")
print(stock_data.info())
print(stock_data.head())
print("\nBenchmarks\n")
print(benchmark_data.info())
print(benchmark_data.head())
# 输出统计量
print(stock_data.describe())
print(benchmark_data.describe())
# 计算每日回报率
stock_returns = stock_data.pct_change()
print(stock_returns.describe())
sp_returns = benchmark_data.pct_change()
print(sp_returns.describe())
# 每日超额回报
excess_returns = pd.DataFrame()
excess_returns["Amazon"] = stock_returns["Amazon"] - sp_returns["S&P 500"]
excess_returns["Facebook"] = stock_returns["Facebook"] - sp_returns["S&P 500"]
print(excess_returns.describe())
# 超额回报的均值
avg_excess_return = excess_returns.mean()
print(avg_excess_return)
# 超额回报的标准差
std_excess_return = excess_returns.std()
print(std_excess_return)
# 计算夏普比率
# 日夏普比率
daily_sharpe_ratio = avg_excess_return.div(std_excess_return)
# 年化夏普比率
annual_factor = np.sqrt(252)
annual_sharpe_ratio = daily_sharpe_ratio.mul(annual_factor)
print("年化夏普比率\n", annual_sharpe_ratio)


接下来再用empyrical算。

1
2
3
4
5
6
7
# 用empyrical算
sharpe = pd.DataFrame()
a = ey.sharpe_ratio(stock_returns["Amazon"])
b = ey.sharpe_ratio(stock_returns["Facebook"])
print("empyrical计算结果")
print(a, b)
print(a/annual_sharpe_ratio["Amazon"], b/annual_sharpe_ratio["Facebook"])


结果还是不对。还是参数问题?改改试试。
把上面程序作为基准的标普500换成固定的年化4%,设定

1
risk_free = 0.04/252.0

empyrical计算程序设定risk_free参数

1
2
3
4
5
6
7
# 用empyrical算
sharpe = pd.DataFrame()
a = ey.sharpe_ratio(stock_returns["Amazon"], risk_free = risk_free)#, annualization = 252)
b = ey.sharpe_ratio(stock_returns["Facebook"], risk_free = risk_free)
print("empyrical计算结果")
print(a, b)
print(a/annual_sharpe_ratio["Amazon"], b/annual_sharpe_ratio["Facebook"])


这下对了!说明empyrical算的没问题(废话!),现在来解决我回测中的计算夏普值差异的问题。
搜了一下backtrader的文档,例子里就有计算年化夏普值的程序,照着来吧:
https://www.backtrader.com/docu/analyzers/analyzers/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 计算年化夏普值,参考backtrader的文档
class SharpeRatio(Analyzer):
params = (("timeframe", TimeFrame.Years), ("riskfreerate", 0.02))

def __init__(self):
super(SharpeRatio, self).__init__()
self.anret = AnnualReturn()

def start(self):
pass

def next(self):
pass

def stop(self):
retfree = [self.p.riskfreerate] * len(self.anret.rets)
retavg = average(list(map(operator.sub, self.anret.rets, retfree)))
retdev = standarddev(self.anret.rets)

self.ratio = retavg/retdev

def get_analysis(self):
return dict(sharperatio = self.ratio)

输出结果

还是一个不同于一个啊?再试试SharpeRatio_A分析器吧。

四个不同的结果……先用backtrader框架的数据吧。反正不同的策略对比用的是同一个计算方法。
代码地址还是: https://github.com/zwdnet/MyQuant/tree/master/47
主要修改了backtest.py文件。

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

欢迎打赏!感谢支持!