示例¶
这里有一些例子,让你更好地了解bt是什么。
SMA策略¶
让我们从一个简单的移动平均线(SMA)策略开始。我们将从这个策略的一个简单版本开始,即:
选择当前高于其50天移动平均线的证券
权重 每个选定的证券均等
重新平衡 投资组合以反映目标权重
这应该相当简单构建。上面唯一缺少的是简单移动平均的计算。这应该在什么时候进行?
鉴于bt的灵活性,没有严格的规则。平均计算可以在Algo中执行,但这将非常低效。更好的方法是在开始回测之前计算移动平均。毕竟,所有数据都是预先知道的。
既然我们知道要做什么,那就开始吧。首先我们将下载一些数据并计算简单移动平均线。
import bt
%matplotlib inline
# download data
data = bt.get('aapl,msft,c,gs,ge', start='2010-01-01')
# calculate moving average DataFrame using pandas' rolling_mean
import pandas as pd
# a rolling mean is a moving average, right?
sma = data.rolling(50).mean()
绘制数据以确保其看起来正常总是一个好主意。所以让我们看看数据 + 简单移动平均线(SMA)图是什么样子的。
# let's see what the data looks like - this is by no means a pretty chart, but it does the job
plot = bt.merge(data, sma).plot(figsize=(15, 5))

看起来合法。
现在我们有了数据,我们需要创建我们的证券选择逻辑。让我们创建一个基本的算法,该算法将选择那些高于其移动平均线的证券。
在我们这样做之前,让我们考虑一下我们将如何编写代码。我们可以传递SMA数据,然后从sma DataFrame中提取当前日期的行,将值与当前价格进行比较,然后保留价格高于SMA的那些证券的列表。这是最直接的方法。然而,这并不是非常可重用的,因为Algo中的逻辑将非常特定于手头的任务,如果我们希望更改逻辑,我们将不得不编写一个新的algo。
例如,如果我们想要选择低于其简单移动平均线(sma)的证券怎么办?或者如果我们只想要高于其简单移动平均线(sma)5%的证券怎么办?
我们可以做的是预先计算选择逻辑的DataFrame (一个快速的向量化操作),并编写一个通用的Algo,它接收 这个布尔DataFrame并返回在给定日期值为 True的证券。这将更快且更具可重用性。 让我们看看实现是什么样子的。
class SelectWhere(bt.Algo):
"""
Selects securities based on an indicator DataFrame.
Selects securities where the value is True on the current date (target.now).
Args:
* signal (DataFrame): DataFrame containing the signal (boolean DataFrame)
Sets:
* selected
"""
def __init__(self, signal):
self.signal = signal
def __call__(self, target):
# get signal on target.now
if target.now in self.signal.index:
sig = self.signal.loc[target.now]
# get indices where true as list
selected = list(sig.index[sig])
# save in temp - this will be used by the weighing algo
target.temp['selected'] = selected
# return True because we want to keep on moving down the stack
return True
所以,我们有了它。我们的选择算法。
注意
顺便说一下,这个算法已经存在了——我只是想向你展示如何从头开始编写它。
Here is the code
.
我们现在要做的就是传入一个信号矩阵。在我们的情况下,这非常简单:
signal = data > sma
简单、简洁,更重要的是,快速!让我们继续并测试策略。
# first we create the Strategy
s = bt.Strategy('above50sma', [SelectWhere(data > sma),
bt.algos.WeighEqually(),
bt.algos.Rebalance()])
# now we create the Backtest
t = bt.Backtest(s, data)
# and let's run it!
res = bt.run(t)
所以回顾一下,我们创建了策略,通过将策略与数据结合创建了回测,并运行了回测。让我们看看结果。
# what does the equity curve look like?
res.plot();

# and some performance stats
res.display()
Stat above50sma
------------------- ------------
Start 2010-01-03
End 2022-07-01
Risk-free rate 0.00%
Total Return 116.08%
Daily Sharpe 0.42
Daily Sortino 0.63
CAGR 6.36%
Max Drawdown -39.43%
Calmar Ratio 0.16
MTD 0.00%
3m -19.50%
6m -26.03%
YTD -26.03%
1Y -22.10%
3Y (ann.) 10.34%
5Y (ann.) 1.89%
10Y (ann.) 8.70%
Since Incep. (ann.) 6.36%
Daily Sharpe 0.42
Daily Sortino 0.63
Daily Mean (ann.) 8.07%
Daily Vol (ann.) 19.45%
Daily Skew -0.65
Daily Kurt 4.74
Best Day 5.78%
Worst Day -8.26%
Monthly Sharpe 0.39
Monthly Sortino 0.65
Monthly Mean (ann.) 8.59%
Monthly Vol (ann.) 21.86%
Monthly Skew -0.37
Monthly Kurt 0.73
Best Month 21.65%
Worst Month -17.26%
Yearly Sharpe 0.41
Yearly Sortino 0.83
Yearly Mean 9.78%
Yearly Vol 23.65%
Yearly Skew -0.88
Yearly Kurt -0.67
Best Year 34.85%
Worst Year -34.38%
Avg. Drawdown -3.56%
Avg. Drawdown Days 47.27
Avg. Up Month 4.76%
Avg. Down Month -5.35%
Win Year % 66.67%
Win 12m % 67.14%
虽然没有什么特别出色的地方,但至少你在过程中学到了一些东西(我希望如此)。
哦,还有一件事。如果你要编写自己的“库”来进行回测,你可能想编写一个辅助函数,以便测试不同的参数和证券。这个函数可能看起来像这样:
def above_sma(tickers, sma_per=50, start='2010-01-01', name='above_sma'):
"""
Long securities that are above their n period
Simple Moving Averages with equal weights.
"""
# download data
data = bt.get(tickers, start=start)
# calc sma
sma = data.rolling(sma_per).mean()
# create strategy
s = bt.Strategy(name, [SelectWhere(data > sma),
bt.algos.WeighEqually(),
bt.algos.Rebalance()])
# now we create the backtest
return bt.Backtest(s, data)
此函数使我们能够轻松生成回测。我们可以轻松比较几个不同的SMA周期。此外,让我们看看我们是否能超越仅做多SPY的分配。
# simple backtest to test long-only allocation
def long_only_ew(tickers, start='2010-01-01', name='long_only_ew'):
s = bt.Strategy(name, [bt.algos.RunOnce(),
bt.algos.SelectAll(),
bt.algos.WeighEqually(),
bt.algos.Rebalance()])
data = bt.get(tickers, start=start)
return bt.Backtest(s, data)
# create the backtests
tickers = 'aapl,msft,c,gs,ge'
sma10 = above_sma(tickers, sma_per=10, name='sma10')
sma20 = above_sma(tickers, sma_per=20, name='sma20')
sma40 = above_sma(tickers, sma_per=40, name='sma40')
benchmark = long_only_ew('spy', name='spy')
# run all the backtests!
res2 = bt.run(sma10, sma20, sma40, benchmark)
res2.plot(freq='m');

res2.display()
Stat sma10 sma20 sma40 spy
------------------- ---------- ---------- ---------- ----------
Start 2010-01-03 2010-01-03 2010-01-03 2010-01-03
End 2022-07-01 2022-07-01 2022-07-01 2022-07-01
Risk-free rate 0.00% 0.00% 0.00% 0.00%
Total Return 284.16% 229.80% 145.62% 321.22%
Daily Sharpe 0.63 0.58 0.47 0.75
Daily Sortino 0.99 0.91 0.73 1.15
CAGR 11.38% 10.03% 7.46% 12.20%
Max Drawdown -31.77% -40.72% -34.93% -33.72%
Calmar Ratio 0.36 0.25 0.21 0.36
MTD -0.76% 0.00% 0.00% -0.37%
3m -10.58% -22.25% -18.82% -16.66%
6m -10.71% -32.14% -30.31% -20.28%
YTD -10.71% -32.14% -30.31% -20.28%
1Y -13.63% -24.65% -27.20% -11.44%
3Y (ann.) 28.10% 14.77% 3.73% 10.10%
5Y (ann.) 15.80% 8.37% 1.96% 11.11%
10Y (ann.) 13.76% 10.96% 9.67% 12.78%
Since Incep. (ann.) 11.38% 10.03% 7.46% 12.20%
Daily Sharpe 0.63 0.58 0.47 0.75
Daily Sortino 0.99 0.91 0.73 1.15
Daily Mean (ann.) 12.88% 11.52% 9.01% 13.03%
Daily Vol (ann.) 20.48% 19.79% 18.97% 17.34%
Daily Skew -0.11 -0.29 -0.45 -0.59
Daily Kurt 6.61 6.23 4.32 11.75
Best Day 10.47% 10.47% 6.20% 9.06%
Worst Day -8.26% -8.26% -8.26% -10.94%
Monthly Sharpe 0.65 0.54 0.43 0.92
Monthly Sortino 1.18 1.02 0.75 1.62
Monthly Mean (ann.) 13.56% 11.95% 9.71% 13.00%
Monthly Vol (ann.) 20.96% 21.94% 22.42% 14.20%
Monthly Skew -0.02 0.22 -0.10 -0.40
Monthly Kurt 1.01 1.11 0.67 0.89
Best Month 22.75% 24.73% 21.97% 12.70%
Worst Month -16.94% -14.34% -15.86% -12.49%
Yearly Sharpe 0.54 0.43 0.40 0.80
Yearly Sortino 2.01 1.03 0.77 2.15
Yearly Mean 13.38% 13.94% 9.76% 12.67%
Yearly Vol 24.64% 32.80% 24.22% 15.79%
Yearly Skew 0.41 -0.15 -0.87 -0.68
Yearly Kurt -0.43 -0.96 -0.59 0.12
Best Year 62.47% 66.99% 39.35% 32.31%
Worst Year -18.59% -37.01% -32.06% -20.28%
Avg. Drawdown -3.95% -3.49% -3.68% -1.69%
Avg. Drawdown Days 40.43 35.12 48.79 15.92
Avg. Up Month 4.68% 5.00% 4.69% 3.20%
Avg. Down Month -5.00% -4.85% -5.70% -3.56%
Win Year % 58.33% 66.67% 75.00% 83.33%
Win 12m % 68.57% 66.43% 69.29% 91.43%
就这样。打败市场并不那么容易!
SMA交叉策略¶
让我们在上一节的基础上测试一个移动平均线交叉策略。实现这一目标的最简单方法是构建一个类似于SelectWhere的算法,但目的是设置目标权重。我们称这个算法为WeighTarget。这个算法将接受一个我们预先计算的目标权重的DataFrame。
基本上,当50天移动平均线高于200天移动平均线时,我们将做多(+1目标权重)。相反,当50天移动平均线低于200天移动平均线时,我们将做空(-1目标权重)。
这是WeighTarget的实现(这个算法也已经存在于algos模块中):
class WeighTarget(bt.Algo):
"""
Sets target weights based on a target weight DataFrame.
Args:
* target_weights (DataFrame): DataFrame containing the target weights
Sets:
* weights
"""
def __init__(self, target_weights):
self.tw = target_weights
def __call__(self, target):
# get target weights on date target.now
if target.now in self.tw.index:
w = self.tw.loc[target.now]
# save in temp - this will be used by the weighing algo
# also dropping any na's just in case they pop up
target.temp['weights'] = w.dropna()
# return True because we want to keep on moving down the stack
return True
所以让我们从一个简单的50-200日简单移动平均线交叉开始,针对单一证券。
## download some data & calc SMAs
data = bt.get('spy', start='2010-01-01')
sma50 = data.rolling(50).mean()
sma200 = data.rolling(200).mean()
## now we need to calculate our target weight DataFrame
# first we will copy the sma200 DataFrame since our weights will have the same strucutre
tw = sma200.copy()
# set appropriate target weights
tw[sma50 > sma200] = 1.0
tw[sma50 <= sma200] = -1.0
# here we will set the weight to 0 - this is because the sma200 needs 200 data points before
# calculating its first point. Therefore, it will start with a bunch of nulls (NaNs).
tw[sma200.isnull()] = 0.0
好的,我们已经下载了数据,计算了简单移动平均线,然后设置了我们的目标权重(tw)数据框。让我们看一下我们的目标权重,看看它们是否有意义。
# plot the target weights + chart of price & SMAs
tmp = bt.merge(tw, data, sma50, sma200)
tmp.columns = ['tw', 'price', 'sma50', 'sma200']
ax = tmp.plot(figsize=(15,5), secondary_y=['tw'])

如前所述,绘制策略数据总是一个好主意。通过这种方式通常更容易发现逻辑/编程错误,尤其是在处理大量数据时。
现在让我们继续讨论策略与回测。
ma_cross = bt.Strategy('ma_cross', [WeighTarget(tw),
bt.algos.Rebalance()])
t = bt.Backtest(ma_cross, data)
res = bt.run(t)
res.plot();

好的,太好了,所以我们有了我们的基本移动平均线交叉策略。
探索树结构¶
到目前为止,我们已经探讨了将资本分配给证券的策略。但如果我们想测试一个将资本分配给子策略的策略呢?
最直接的方法是测试不同的子策略,提取它们的权益曲线,并创建“合成证券”,这些证券基本上只代表通过将资本分配给不同子策略所实现的回报。
让我们看看这个看起来如何:
# first let's create a helper function to create a ma cross backtest
def ma_cross(ticker, start='2010-01-01',
short_ma=50, long_ma=200, name='ma_cross'):
# these are all the same steps as above
data = bt.get(ticker, start=start)
short_sma = data.rolling(short_ma).mean()
long_sma = data.rolling(long_ma).mean()
# target weights
tw = long_sma.copy()
tw[short_sma > long_sma] = 1.0
tw[short_sma <= long_sma] = -1.0
tw[long_sma.isnull()] = 0.0
# here we specify the children (3rd) arguemnt to make sure the strategy
# has the proper universe. This is necessary in strategies of strategies
s = bt.Strategy(name, [WeighTarget(tw), bt.algos.Rebalance()], [ticker])
return bt.Backtest(s, data)
# ok now let's create a few backtests and gather the results.
# these will later become our "synthetic securities"
t1 = ma_cross('aapl', name='aapl_ma_cross')
t2 = ma_cross('msft', name='msft_ma_cross')
# let's run these strategies now
res = bt.run(t1, t2)
# now that we have run the strategies, let's extract
# the data to create "synthetic securities"
data = bt.merge(res['aapl_ma_cross'].prices, res['msft_ma_cross'].prices)
# now we have our new data. This data is basically the equity
# curves of both backtested strategies. Now we can just use this
# to test any old strategy, just like before.
s = bt.Strategy('s', [bt.algos.SelectAll(),
bt.algos.WeighInvVol(),
bt.algos.Rebalance()])
# create and run
t = bt.Backtest(s, data)
res = bt.run(t)
res.plot();

res.plot_weights();

正如我们在上面所看到的,这个过程稍微复杂一些,但它是有效的。 不过,这并不是很优雅,而且获取安全级别的分配信息是有问题的。
幸运的是,bt内置了处理策略的策略功能。它使用了与上述相同的通用原则,但无缝地实现了这一点。基本上,当一个策略是另一个策略的子策略时,它会在内部创建一个“模拟交易”版本。当我们运行我们的策略时,它将运行其内部的“模拟版本”,并使用该策略的回报来填充price属性。
这意味着父策略可以使用价格信息(反映了如果使用该策略的回报)来确定适当的分配。再次强调,这基本上与上述过程相同,只是打包成1个步骤。
也许一些代码会有帮助:
# once again, we will create a few backtests
# these will be the child strategies
t1 = ma_cross('aapl', name='aapl_ma_cross')
t2 = ma_cross('msft', name='msft_ma_cross')
# let's extract the data object
data = bt.merge(t1.data, t2.data)
# now we create the parent strategy
# we specify the children to be the two
# strategies created above
s = bt.Strategy('s', [bt.algos.SelectAll(),
bt.algos.WeighInvVol(),
bt.algos.Rebalance()],
[t1.strategy, t2.strategy])
# create and run
t = bt.Backtest(s, data)
res = bt.run(t)
res.plot();

res.plot_weights();

所以,这就是了。更简单,更完整。
买入并持有策略¶
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import ffn
import bt
%matplotlib inline
创建虚假索引数据¶
names = ['foo','bar','rf']
dates = pd.date_range(start='2017-01-01',end='2017-12-31', freq=pd.tseries.offsets.BDay())
n = len(dates)
rdf = pd.DataFrame(
np.zeros((n, len(names))),
index = dates,
columns = names
)
np.random.seed(1)
rdf['foo'] = np.random.normal(loc = 0.1/n,scale=0.2/np.sqrt(n),size=n)
rdf['bar'] = np.random.normal(loc = 0.04/n,scale=0.05/np.sqrt(n),size=n)
rdf['rf'] = 0.
pdf = 100*np.cumprod(1+rdf)
pdf.plot();

构建策略¶
# algo to fire on the beginning of every month and to run on the first date
runMonthlyAlgo = bt.algos.RunMonthly(
run_on_first_date=True
)
# algo to set the weights
# it will only run when runMonthlyAlgo returns true
# which only happens on the first of every month
weights = pd.Series([0.6,0.4,0.],index = rdf.columns)
weighSpecifiedAlgo = bt.algos.WeighSpecified(**weights)
# algo to rebalance the current weights to weights set by weighSpecified
# will only run when weighSpecifiedAlgo returns true
# which happens every time it runs
rebalAlgo = bt.algos.Rebalance()
# a strategy that rebalances monthly to specified weights
strat = bt.Strategy('static',
[
runMonthlyAlgo,
weighSpecifiedAlgo,
rebalAlgo
]
)
运行回测¶
注意:策略的逻辑与回测中使用的数据是分开的。
# set integer_positions=False when positions are not required to be integers(round numbers)
backtest = bt.Backtest(
strat,
pdf,
integer_positions=False
)
res = bt.run(backtest)
res.stats
静态 | |
---|---|
开始 | 2017-01-01 00:00:00 |
结束 | 2017-12-29 00:00:00 |
rf | 0.0 |
总回报 | 0.229372 |
年复合增长率 | 0.231653 |
最大回撤 | -0.069257 |
卡尔玛比率 | 3.344851 |
mtd | -0.000906 |
三个月 | 0.005975 |
six_month | 0.142562 |
年初至今 | 0.229372 |
一年 | NaN |
三年 | NaN |
five_year | NaN |
十年 | NaN |
incep | 0.231653 |
每日夏普比率 | 1.804549 |
每日索提诺比率 | 3.306154 |
每日平均值 | 0.206762 |
每日波动率 | 0.114578 |
每日偏斜 | 0.012208 |
每日峰度 | -0.04456 |
最佳日 | 0.020402 |
最差的一天 | -0.0201 |
月度夏普比率 | 2.806444 |
月度索提诺比率 | 15.352486 |
月平均值 | 0.257101 |
月度波动率 | 0.091611 |
月度偏斜 | 0.753881 |
月度峰度 | 0.456278 |
最佳月份 | 0.073657 |
最差月份 | -0.014592 |
年度夏普比率 | NaN |
年度索提诺比率 | NaN |
年平均 | NaN |
yearly_vol | NaN |
年度偏斜 | NaN |
年度峰度 | NaN |
最佳年份 | NaN |
最差年份 | NaN |
平均回撤 | -0.016052 |
平均回撤天数 | 12.695652 |
平均上涨月份 | 0.03246 |
avg_down_month | -0.008001 |
win_year_perc | NaN |
十二个月胜率 | NaN |
res.prices.head()
静态 | |
---|---|
2017-01-01 | 100.000000 |
2017-01-02 | 100.000000 |
2017-01-03 | 99.384719 |
2017-01-04 | 99.121677 |
2017-01-05 | 98.316364 |
res.plot_security_weights()

策略值随时间变化
performanceStats = res['static']
#performance stats is an ffn object
res.backtest_list[0].strategy.values.plot();

战略支出
支出是购买(出售)证券所花费(获得)的总金额。
res.backtest_list[0].strategy.outlays.plot();

您可以获取购买的股票数量的变化
security_names = res.backtest_list[0].strategy.outlays.columns
res.backtest_list[0].strategy.outlays/pdf.loc[:,security_names]
res.backtest_list[0].positions.diff(1)
res.backtest_list[0].positions
foo | bar | |
---|---|---|
2017-01-01 | 0.000000 | 0.000000 |
2017-01-02 | 5879.285683 | 3998.068018 |
2017-01-03 | 5879.285683 | 3998.068018 |
2017-01-04 | 5879.285683 | 3998.068018 |
2017-01-05 | 5879.285683 | 3998.068018 |
... | ... | ... |
2017-12-25 | 5324.589093 | 4673.239436 |
2017-12-26 | 5324.589093 | 4673.239436 |
2017-12-27 | 5324.589093 | 4673.239436 |
2017-12-28 | 5324.589093 | 4673.239436 |
2017-12-29 | 5324.589093 | 4673.239436 |
261 行 × 2 列
趋势示例 1¶
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import ffn
import bt
%matplotlib inline
创建假数据¶
rf = 0.04
np.random.seed(1)
mus = np.random.normal(loc=0.05,scale=0.02,size=5) + rf
sigmas = (mus - rf)/0.3 + np.random.normal(loc=0.,scale=0.01,size=5)
num_years = 10
num_months_per_year = 12
num_days_per_month = 21
num_days_per_year = num_months_per_year*num_days_per_month
rdf = pd.DataFrame(
index = pd.date_range(
start="2008-01-02",
periods=num_years*num_months_per_year*num_days_per_month,
freq="B"
),
columns=['foo','bar','baz','fake1','fake2']
)
for i,mu in enumerate(mus):
sigma = sigmas[i]
rdf.iloc[:,i] = np.random.normal(
loc=mu/num_days_per_year,
scale=sigma/np.sqrt(num_days_per_year),
size=rdf.shape[0]
)
pdf = np.cumprod(1+rdf)*100
pdf.plot();

创建过去12个月的趋势信号¶
sma = pdf.rolling(window=num_days_per_month*12,center=False).median().shift(1)
plt.plot(pdf.index,pdf['foo'])
plt.plot(sma.index,sma['foo'])
plt.show()

#sma with 1 day lag
sma.tail()
foo | bar | baz | fake1 | fake2 | |
---|---|---|---|---|---|
2017-08-23 | 623.241267 | 340.774506 | 99.764885 | 263.491447 | 619.963986 |
2017-08-24 | 623.167989 | 341.096742 | 99.764885 | 263.502145 | 620.979948 |
2017-08-25 | 622.749149 | 341.316672 | 99.764885 | 263.502145 | 622.421401 |
2017-08-28 | 622.353039 | 341.494307 | 99.807732 | 263.517071 | 622.962579 |
2017-08-29 | 622.153294 | 341.662442 | 99.807732 | 263.517071 | 622.992416 |
#sma with 0 day lag
pdf.rolling(window=num_days_per_month*12,center=False).median().tail()
foo | bar | baz | fake1 | fake2 | |
---|---|---|---|---|---|
2017-08-23 | 623.167989 | 341.096742 | 99.764885 | 263.502145 | 620.979948 |
2017-08-24 | 622.749149 | 341.316672 | 99.764885 | 263.502145 | 622.421401 |
2017-08-25 | 622.353039 | 341.494307 | 99.807732 | 263.517071 | 622.962579 |
2017-08-28 | 622.153294 | 341.662442 | 99.807732 | 263.517071 | 622.992416 |
2017-08-29 | 621.907867 | 341.948212 | 99.807732 | 263.634283 | 624.310473 |
# target weights
trend = sma.copy()
trend[pdf > sma] = True
trend[pdf <= sma] = False
trend[sma.isnull()] = False
trend.tail()
foo | bar | baz | fake1 | fake2 | |
---|---|---|---|---|---|
2017-08-23 | 假 | 真 | 真 | 真 | 真 |
2017-08-24 | 假 | 真 | 真 | 真 | 真 |
2017-08-25 | 假 | 真 | 真 | 真 | 真 |
2017-08-28 | 假 | 真 | 真 | 真 | 真 |
2017-08-29 | 假 | 真 | 真 | 真 | 真 |
比较EW和1/vol
两种策略每天使用趋势进行重新平衡,滞后1天,权重限制为40%。
tsmom_invvol_strat = bt.Strategy(
'tsmom_invvol',
[
bt.algos.RunDaily(),
bt.algos.SelectWhere(trend),
bt.algos.WeighInvVol(),
bt.algos.LimitWeights(limit=0.4),
bt.algos.Rebalance()
]
)
tsmom_ew_strat = bt.Strategy(
'tsmom_ew',
[
bt.algos.RunDaily(),
bt.algos.SelectWhere(trend),
bt.algos.WeighEqually(),
bt.algos.LimitWeights(limit=0.4),
bt.algos.Rebalance()
]
)
# create and run
tsmom_invvol_bt = bt.Backtest(
tsmom_invvol_strat,
pdf,
initial_capital=50000000.0,
commissions=lambda q, p: max(100, abs(q) * 0.0021),
integer_positions=False,
progress_bar=True
)
tsmom_invvol_res = bt.run(tsmom_invvol_bt)
tsmom_ew_bt = bt.Backtest(
tsmom_ew_strat,
pdf,
initial_capital=50000000.0,
commissions=lambda q, p: max(100, abs(q) * 0.0021),
integer_positions=False,
progress_bar=True
)
tsmom_ew_res = bt.run(tsmom_ew_bt)
tsmom_invvol
0% [############################# ] 100% | ETA: 00:00:00tsmom_ew
0% [############################# ] 100% | ETA: 00:00:00
ax = plt.subplot()
ax.plot(tsmom_ew_res.prices.index,tsmom_ew_res.prices,label='EW')
pdf.plot(ax=ax)
ax.legend()
plt.legend()
plt.show()

tsmom_ew_res.stats
tsmom_ew | |
---|---|
开始 | 2008-01-01 00:00:00 |
结束 | 2017-08-29 00:00:00 |
rf | 0.0 |
总回报 | 1.982933 |
年复合增长率 | 0.119797 |
最大回撤 | -0.103421 |
卡尔玛比率 | 1.158343 |
mtd | 0.017544 |
三个月 | 0.040722 |
六个月 | 0.079362 |
年初至今 | 0.08107 |
一年 | 0.100432 |
三年 | 0.159895 |
五年 | 0.172284 |
十年 | 0.119797 |
incep | 0.119797 |
每日夏普比率 | 1.356727 |
每日索提诺比率 | 2.332895 |
每日平均值 | 0.112765 |
daily_vol | 0.083116 |
每日偏斜 | 0.029851 |
每日峰度 | 0.96973 |
最佳日期 | 0.02107 |
最差的一天 | -0.021109 |
月度夏普比率 | 1.373241 |
月度索提诺比率 | 2.966223 |
月平均值 | 0.118231 |
月度波动率 | 0.086096 |
月度偏斜 | -0.059867 |
月度峰度 | 0.571064 |
最佳月份 | 0.070108 |
最差月份 | -0.064743 |
年度夏普比率 | 1.741129 |
年度索提诺比率 | inf |
年平均 | 0.129033 |
年波动率 | 0.074109 |
年度偏度 | 0.990397 |
年度峰度 | 1.973883 |
最佳年份 | 0.285249 |
最差年份 | 0.024152 |
平均回撤 | -0.015516 |
平均回撤天数 | 25.223214 |
平均上涨月份 | 0.024988 |
avg_down_month | -0.012046 |
win_year_perc | 1.0 |
十二个月胜率 | 0.971429 |
趋势示例 2¶
import numpy as np
import pandas as pd
import bt
import matplotlib.pyplot as plt
%matplotlib inline
np.random.seed(0)
returns = np.random.normal(0.08/12,0.2/np.sqrt(12),12*10)
pdf = pd.DataFrame(
np.cumprod(1+returns),
index = pd.date_range(start="2008-01-01",periods=12*10,freq="m"),
columns=['foo']
)
pdf.plot();

runMonthlyAlgo = bt.algos.RunMonthly()
rebalAlgo = bt.algos.Rebalance()
class Signal(bt.Algo):
"""
Mostly copied from StatTotalReturn
Sets temp['Signal'] with total returns over a given period.
Sets the 'Signal' based on the total return of each
over a given lookback period.
Args:
* lookback (DateOffset): lookback period.
* lag (DateOffset): Lag interval. Total return is calculated in
the inteval [now - lookback - lag, now - lag]
Sets:
* stat
Requires:
* selected
"""
def __init__(self, lookback=pd.DateOffset(months=3),
lag=pd.DateOffset(days=0)):
super(Signal, self).__init__()
self.lookback = lookback
self.lag = lag
def __call__(self, target):
selected = 'foo'
t0 = target.now - self.lag
if target.universe[selected].index[0] > t0:
return False
prc = target.universe[selected].loc[t0 - self.lookback:t0]
trend = prc.iloc[-1]/prc.iloc[0] - 1
signal = trend > 0.
if signal:
target.temp['Signal'] = 1.
else:
target.temp['Signal'] = 0.
return True
signalAlgo = Signal(pd.DateOffset(months=12),pd.DateOffset(months=1))
class WeighFromSignal(bt.Algo):
"""
Sets temp['weights'] from the signal.
Sets:
* weights
Requires:
* selected
"""
def __init__(self):
super(WeighFromSignal, self).__init__()
def __call__(self, target):
selected = 'foo'
if target.temp['Signal'] is None:
raise(Exception('No Signal!'))
target.temp['weights'] = {selected : target.temp['Signal']}
return True
weighFromSignalAlgo = WeighFromSignal()
s = bt.Strategy(
'example1',
[
runMonthlyAlgo,
signalAlgo,
weighFromSignalAlgo,
rebalAlgo
]
)
t = bt.Backtest(s, pdf, integer_positions=False, progress_bar=True)
res = bt.run(t)
example1
0% [############################# ] 100% | ETA: 00:00:00
res.plot_security_weights();

t.positions
foo | |
---|---|
2008-01-30 | 0.000000 |
2008-01-31 | 0.000000 |
2008-02-29 | 0.000000 |
2008-03-31 | 0.000000 |
2008-04-30 | 0.000000 |
... | ... |
2017-08-31 | 631321.251898 |
2017-09-30 | 631321.251898 |
2017-10-31 | 631321.251898 |
2017-11-30 | 631321.251898 |
2017-12-31 | 631321.251898 |
121 行 × 1 列
res.prices.tail()
示例1 | |
---|---|
2017-08-31 | 240.302579 |
2017-09-30 | 255.046653 |
2017-10-31 | 254.464421 |
2017-11-30 | 265.182603 |
2017-12-31 | 281.069771 |
res.stats
示例1 | |
---|---|
开始 | 2008-01-30 00:00:00 |
结束 | 2017-12-31 00:00:00 |
rf | 0.0 |
总回报 | 1.810698 |
年复合增长率 | 0.109805 |
最大回撤 | -0.267046 |
卡尔玛比率 | 0.411186 |
mtd | 0.05991 |
三个月 | 0.102033 |
六个月 | 0.22079 |
年初至今 | 0.879847 |
一年 | 0.879847 |
三年 | 0.406395 |
五年 | 0.227148 |
十年 | 0.109805 |
incep | 0.109805 |
每日夏普比率 | 3.299555 |
每日索提诺比率 | 6.352869 |
每日平均值 | 2.448589 |
daily_vol | 0.742097 |
每日偏斜 | 0.307861 |
每日峰度 | 1.414455 |
最佳天数 | 0.137711 |
最差的一天 | -0.14073 |
月度夏普比率 | 0.723148 |
月度索提诺比率 | 1.392893 |
月平均值 | 0.117579 |
月度波动率 | 0.162594 |
月度偏斜 | 0.301545 |
月度峰度 | 1.379006 |
最佳月份 | 0.137711 |
最差月份 | -0.14073 |
年化夏普比率 | 0.503939 |
年度索提诺比率 | 5.019272 |
年平均 | 0.14814 |
年波动率 | 0.293964 |
年度偏度 | 2.317496 |
年度峰度 | 5.894955 |
最佳年份 | 0.879847 |
最差年份 | -0.088543 |
平均回撤 | -0.091255 |
平均回撤天数 | 369.714286 |
avg_up_month | 0.064341 |
月均下降 | -0.012928 |
win_year_perc | 0.555556 |
十二个月胜率 | 0.46789 |
策略组合¶
本笔记本创建了一个包含2个子策略(等权重、逆波动率)的父策略(组合)。
或者,它创建了2个子策略,运行回测,合并结果,并使用这两个回测创建一个父策略。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import ffn
import bt
%matplotlib inline
创建假数据¶
rf = 0.04
np.random.seed(1)
mus = np.random.normal(loc=0.05,scale=0.02,size=5) + rf
sigmas = (mus - rf)/0.3 + np.random.normal(loc=0.,scale=0.01,size=5)
num_years = 10
num_months_per_year = 12
num_days_per_month = 21
num_days_per_year = num_months_per_year*num_days_per_month
rdf = pd.DataFrame(
index = pd.date_range(
start="2008-01-02",
periods=num_years*num_months_per_year*num_days_per_month,
freq="B"
),
columns=['foo','bar','baz','fake1','fake2']
)
for i,mu in enumerate(mus):
sigma = sigmas[i]
rdf.iloc[:,i] = np.random.normal(
loc=mu/num_days_per_year,
scale=sigma/np.sqrt(num_days_per_year),
size=rdf.shape[0]
)
pdf = np.cumprod(1+rdf)*100
pdf.iloc[0,:] = 100
pdf.plot();

strategy_names = np.array(
[
'Equal Weight',
'Inv Vol'
]
)
runMonthlyAlgo = bt.algos.RunMonthly(
run_on_first_date=True,
run_on_end_of_period=True
)
selectAllAlgo = bt.algos.SelectAll()
rebalanceAlgo = bt.algos.Rebalance()
strats = []
tests = []
for i,s in enumerate(strategy_names):
if s == "Equal Weight":
wAlgo = bt.algos.WeighEqually()
elif s == "Inv Vol":
wAlgo = bt.algos.WeighInvVol()
strat = bt.Strategy(
str(s),
[
runMonthlyAlgo,
selectAllAlgo,
wAlgo,
rebalanceAlgo
]
)
strats.append(strat)
t = bt.Backtest(
strat,
pdf,
integer_positions = False,
progress_bar=False
)
tests.append(t)
combined_strategy = bt.Strategy(
'Combined',
algos = [
runMonthlyAlgo,
selectAllAlgo,
bt.algos.WeighEqually(),
rebalanceAlgo
],
children = [x.strategy for x in tests]
)
combined_test = bt.Backtest(
combined_strategy,
pdf,
integer_positions = False,
progress_bar = False
)
res = bt.run(combined_test)
res.prices.plot();

res.get_security_weights().plot();

为了获取每个策略的权重,你可以运行每个策略,获取每个策略的价格,将它们合并成一个价格数据框,然后在新数据集上运行组合策略。
strategy_names = np.array(
[
'Equal Weight',
'Inv Vol'
]
)
runMonthlyAlgo = bt.algos.RunMonthly(
run_on_first_date=True,
run_on_end_of_period=True
)
selectAllAlgo = bt.algos.SelectAll()
rebalanceAlgo = bt.algos.Rebalance()
strats = []
tests = []
results = []
for i,s in enumerate(strategy_names):
if s == "Equal Weight":
wAlgo = bt.algos.WeighEqually()
elif s == "Inv Vol":
wAlgo = bt.algos.WeighInvVol()
strat = bt.Strategy(
s,
[
runMonthlyAlgo,
selectAllAlgo,
wAlgo,
rebalanceAlgo
]
)
strats.append(strat)
t = bt.Backtest(
strat,
pdf,
integer_positions = False,
progress_bar=False
)
tests.append(t)
res = bt.run(t)
results.append(res)
fig, ax = plt.subplots(nrows=1,ncols=1)
for i,r in enumerate(results):
r.plot(ax=ax)

merged_prices_df = bt.merge(results[0].prices,results[1].prices)
combined_strategy = bt.Strategy(
'Combined',
algos = [
runMonthlyAlgo,
selectAllAlgo,
bt.algos.WeighEqually(),
rebalanceAlgo
]
)
combined_test = bt.Backtest(
combined_strategy,
merged_prices_df,
integer_positions = False,
progress_bar = False
)
res = bt.run(combined_test)
res.plot();

res.get_security_weights().plot();

等权重风险贡献投资组合¶
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import ffn
import bt
%matplotlib inline
创建虚假索引数据¶
mean = np.array([0.05/252 + 0.02/252, 0.03/252 + 0.02/252])
volatility = np.array([0.2/np.sqrt(252), 0.05/np.sqrt(252)])
variance = np.power(volatility,2)
correlation = np.array(
[
[1, 0.25],
[0.25,1]
]
)
covariance = np.zeros((2,2))
for i in range(len(variance)):
for j in range(len(variance)):
covariance[i,j] = correlation[i,j]*volatility[i]*volatility[j]
covariance
array([[1.58730159e-04, 9.92063492e-06],
[9.92063492e-06, 9.92063492e-06]])
names = ['foo','bar','rf']
dates = pd.date_range(start='2015-01-01',end='2018-12-31', freq=pd.tseries.offsets.BDay())
n = len(dates)
rdf = pd.DataFrame(
np.zeros((n, len(names))),
index = dates,
columns = names
)
np.random.seed(1)
rdf.loc[:,['foo','bar']] = np.random.multivariate_normal(mean,covariance,size=n)
rdf['rf'] = 0.02/252
pdf = 100*np.cumprod(1+rdf)
pdf.plot();

构建并运行ERC策略¶
您可以在这里阅读更多关于ERC的信息。 http://thierry-roncalli.com/download/erc.pdf
runAfterDaysAlgo = bt.algos.RunAfterDays(
20*6 + 1
)
selectTheseAlgo = bt.algos.SelectThese(['foo','bar'])
# algo to set the weights so each asset contributes the same amount of risk
# with data over the last 6 months excluding yesterday
weighERCAlgo = bt.algos.WeighERC(
lookback=pd.DateOffset(days=20*6),
covar_method='standard',
risk_parity_method='slsqp',
maximum_iterations=1000,
tolerance=1e-9,
lag=pd.DateOffset(days=1)
)
rebalAlgo = bt.algos.Rebalance()
strat = bt.Strategy(
'ERC',
[
runAfterDaysAlgo,
selectTheseAlgo,
weighERCAlgo,
rebalAlgo
]
)
backtest = bt.Backtest(
strat,
pdf,
integer_positions=False
)
res_target = bt.run(backtest)
res_target.get_security_weights().plot();

res_target.prices.plot();

weights_target = res_target.get_security_weights().copy()
rolling_cov_target = pdf.loc[:,weights_target.columns].pct_change().rolling(window=252).cov()*252
trc_target = pd.DataFrame(
np.nan,
index = weights_target.index,
columns = weights_target.columns
)
for dt in pdf.index:
trc_target.loc[dt,:] = weights_target.loc[dt,:].values*(rolling_cov_target.loc[dt,:].values@weights_target.loc[dt,:].values)/np.sqrt(weights_target.loc[dt,:].values@rolling_cov_target.loc[dt,:].values@weights_target.loc[dt,:].values)
fig, ax = plt.subplots(nrows=1,ncols=1)
trc_target.plot(ax=ax)
ax.set_title('Total Risk Contribution')
ax.plot();

你可以看到总风险贡献大致来自两个资产。
预测跟踪误差再平衡投资组合¶
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import ffn
import bt
%matplotlib inline
创建虚假索引数据¶
names = ['foo','bar','rf']
dates = pd.date_range(start='2015-01-01',end='2018-12-31', freq=pd.tseries.offsets.BDay())
n = len(dates)
rdf = pd.DataFrame(
np.zeros((n, len(names))),
index = dates,
columns = names
)
np.random.seed(1)
rdf['foo'] = np.random.normal(loc = 0.1/252,scale=0.2/np.sqrt(252),size=n)
rdf['bar'] = np.random.normal(loc = 0.04/252,scale=0.05/np.sqrt(252),size=n)
rdf['rf'] = 0.
pdf = 100*np.cumprod(1+rdf)
pdf.plot();

构建并运行目标策略¶
我将首先运行一个每天重新平衡的策略。
然后,我将使用这些权重作为目标,每当PTE过高时进行重新平衡。
selectTheseAlgo = bt.algos.SelectThese(['foo','bar'])
# algo to set the weights to 1/vol contributions from each asset
# with data over the last 3 months excluding yesterday
weighInvVolAlgo = bt.algos.WeighInvVol(
lookback=pd.DateOffset(months=3),
lag=pd.DateOffset(days=1)
)
# algo to rebalance the current weights to weights set in target.temp
rebalAlgo = bt.algos.Rebalance()
# a strategy that rebalances daily to 1/vol weights
strat = bt.Strategy(
'Target',
[
selectTheseAlgo,
weighInvVolAlgo,
rebalAlgo
]
)
# set integer_positions=False when positions are not required to be integers(round numbers)
backtest = bt.Backtest(
strat,
pdf,
integer_positions=False
)
res_target = bt.run(backtest)
res_target.get_security_weights().plot();

现在使用PTE重新平衡算法,在预测跟踪误差大于1%时触发重新平衡。
# algo to fire whenever predicted tracking error is greater than 1%
wdf = res_target.get_security_weights()
PTE_rebalance_Algo = bt.algos.PTE_Rebalance(
0.01,
wdf,
lookback=pd.DateOffset(months=3),
lag=pd.DateOffset(days=1),
covar_method='standard',
annualization_factor=252
)
selectTheseAlgo = bt.algos.SelectThese(['foo','bar'])
# algo to set the weights to 1/vol contributions from each asset
# with data over the last 12 months excluding yesterday
weighTargetAlgo = bt.algos.WeighTarget(
wdf
)
rebalAlgo = bt.algos.Rebalance()
# a strategy that rebalances monthly to specified weights
strat = bt.Strategy(
'PTE',
[
PTE_rebalance_Algo,
selectTheseAlgo,
weighTargetAlgo,
rebalAlgo
]
)
# set integer_positions=False when positions are not required to be integers(round numbers)
backtest = bt.Backtest(
strat,
pdf,
integer_positions=False
)
res_PTE = bt.run(backtest)
fig, ax = plt.subplots(nrows=1,ncols=1)
res_target.get_security_weights().plot(ax=ax)
realized_weights_df = res_PTE.get_security_weights()
realized_weights_df['PTE foo'] = realized_weights_df['foo']
realized_weights_df['PTE bar'] = realized_weights_df['bar']
realized_weights_df = realized_weights_df.loc[:,['PTE foo', 'PTE bar']]
realized_weights_df.plot(ax=ax)
ax.set_title('Target Weights vs PTE Weights')
ax.plot();

trans_df = pd.DataFrame(
index=res_target.prices.index,
columns=['Target','PTE']
)
transactions = res_target.get_transactions()
transactions = (transactions['quantity'] * transactions['price']).reset_index()
bar_mask = transactions.loc[:,'Security'] == 'bar'
foo_mask = transactions.loc[:,'Security'] == 'foo'
trans_df.loc[trans_df.index[4:],'Target'] = np.abs(transactions[bar_mask].iloc[:,2].values) + np.abs(transactions[foo_mask].iloc[:,2].values)
transactions = res_PTE.get_transactions()
transactions = (transactions['quantity'] * transactions['price']).reset_index()
bar_mask = transactions.loc[:,'Security'] == 'bar'
foo_mask = transactions.loc[:,'Security'] == 'foo'
trans_df.loc[transactions[bar_mask].iloc[:,0],'PTE'] = np.abs(transactions[bar_mask].iloc[:,2].values)
trans_df.loc[transactions[foo_mask].iloc[:,0],'PTE'] += np.abs(transactions[foo_mask].iloc[:,2].values)
trans_df = trans_df.fillna(0)
fig, ax = plt.subplots(nrows=1,ncols=1)
trans_df.cumsum().plot(ax=ax)
ax.set_title('Cumulative sum of notional traded')
ax.plot();

如果我们绘制每个资产类别的总风险贡献并除以总波动率,那么我们可以看到两种策略从两种证券中贡献的波动率大致相似。
weights_target = res_target.get_security_weights()
rolling_cov_target = pdf.loc[:,weights_target.columns].pct_change().rolling(window=3*20).cov()*252
weights_PTE = res_PTE.get_security_weights().loc[:,weights_target.columns]
rolling_cov_PTE = pdf.loc[:,weights_target.columns].pct_change().rolling(window=3*20).cov()*252
trc_target = pd.DataFrame(
np.nan,
index = weights_target.index,
columns = weights_target.columns
)
trc_PTE = pd.DataFrame(
np.nan,
index = weights_PTE.index,
columns = [x + " PTE" for x in weights_PTE.columns]
)
for dt in pdf.index:
trc_target.loc[dt,:] = weights_target.loc[dt,:].values*(rolling_cov_target.loc[dt,:].values@weights_target.loc[dt,:].values)/np.sqrt(weights_target.loc[dt,:].values@rolling_cov_target.loc[dt,:].values@weights_target.loc[dt,:].values)
trc_PTE.loc[dt,:] = weights_PTE.loc[dt,:].values*(rolling_cov_PTE.loc[dt,:].values@weights_PTE.loc[dt,:].values)/np.sqrt(weights_PTE.loc[dt,:].values@rolling_cov_PTE.loc[dt,:].values@weights_PTE.loc[dt,:].values)
fig, ax = plt.subplots(nrows=1,ncols=1)
trc_target.plot(ax=ax)
trc_PTE.plot(ax=ax)
ax.set_title('Total Risk Contribution')
ax.plot();

观察目标策略和PTE策略的总风险,它们非常相似。
fig, ax = plt.subplots(nrows=1,ncols=1)
trc_target.sum(axis=1).plot(ax=ax,label='Target')
trc_PTE.sum(axis=1).plot(ax=ax,label='PTE')
ax.legend()
ax.set_title('Total Risk')
ax.plot();

transactions = res_PTE.get_transactions()
transactions = (transactions['quantity'] * transactions['price']).reset_index()
bar_mask = transactions.loc[:,'Security'] == 'bar'
dates_of_PTE_transactions = transactions[bar_mask].iloc[:,0]
dates_of_PTE_transactions
0 2015-01-06
2 2015-01-07
4 2015-01-08
6 2015-01-09
8 2015-01-12
10 2015-02-20
12 2015-04-07
14 2015-09-01
16 2017-03-23
18 2017-06-23
20 2017-10-24
Name: Date, dtype: datetime64[ns]
fig, ax = plt.subplots(nrows=1,ncols=1)
np.sum(np.abs(trc_target.values - trc_PTE.values))
#.abs().sum(axis=1).plot()
ax.set_title('Total Risk')
ax.plot(
trc_target.index,
np.sum(np.abs(trc_target.values - trc_PTE.values),axis=1),
label='PTE'
)
for i,dt in enumerate(dates_of_PTE_transactions):
if i == 0:
ax.axvline(x=dt,color='red',label='PTE Transaction')
else:
ax.axvline(x=dt,color='red')
ax.legend();

我们可以看到PTE策略的预测跟踪误差,每次交易都有标记。
固定收益示例¶
此示例笔记本展示了该包的一些更复杂的功能,特别是与固定收益证券和策略相关的部分。对于固定收益策略:
资本分配不是必要的,初始资本未被使用
破产被禁用(因为钱总是可以以某种利率借到,可能表示为另一种资产)
权重是基于名义价值而非实际价值。对于固定收益证券,名义价值就是头寸。对于非固定收益证券(即股票),它是头寸的市场价值。
策略名义价值始终为正,等于其所有子项名义价值的绝对值之和
策略价格是根据每单位名义价值的累加PNL回报计算的,参考价格为PAR
“重新平衡”投资组合是基于权重调整名义金额而非资本分配
除了上述固定收益策略的特点外,我们还展示了在这些类型的用例中出现的以下功能的使用:
付息证券(即债券)
处理安全生命周期,例如新问题和到期
使用“On-The-Run”工具,并在预定时间将头寸滚动到“新”的On-The-Run证券中
从预计算的每单位名义风险中进行风险跟踪/汇总和对冲
笔记本包含以下部分:
设置
市场数据生成
政府债券的滚动系列
由共同因素驱动的公司债券利差
示例1:基本策略
对所有活跃的公司债券进行等权重
使用最新的政府债券进行利率风险的对冲
示例 2:嵌套策略
一种策略是按收益率购买前N个债券
另一种策略是按收益率卖出底部N个债券
父策略为上述每个策略分配50%的权重
使用最新的政府债券对冲剩余的利率风险
设置¶
import bt
import pandas as pd
from pandas.tseries.frequencies import to_offset
import numpy as np
np.random.seed(1234)
%matplotlib inline
# (Approximate) Price to yield calcs, and pvbp, for later use. Note we use clean price here.
def price_to_yield( p, ttm, coupon ):
return ( coupon + (100. - p)/ttm ) / ( ( 100. + p)/2. ) * 100
def yield_to_price( y, ttm, coupon ):
return (coupon + 100/ttm - 0.5 * y) / ( y/200 + 1/ttm)
def pvbp( y, ttm, coupon ):
return (yield_to_price( y + 0.01, ttm, coupon ) - yield_to_price( y, ttm, coupon ))
# Utility function to set data frame values to nan before the security has been issued or after it has matured
def censor( data, ref_data ):
for bond in data:
data.loc[ (data.index > ref_data['mat_date'][bond]) | (data.index < ref_data['issue_date'][bond]), bond] = np.NaN
return data.ffill(limit=1,axis=0) # Because bonds might mature during a gap in the index (i.e. on the weekend)
# Backtesting timeline setup
start_date = pd.Timestamp('2020-01-01')
end_date = pd.Timestamp('2022-01-01')
timeline = pd.date_range( start_date, end_date, freq='B')
市场数据生成¶
# Government Bonds: Create synthetic data for a single series of rolling government bonds
# Reference Data
roll_freq = 'Q'
maturity = 10
coupon = 2.0
roll_dates = pd.date_range( start_date, end_date+to_offset(roll_freq), freq=roll_freq) # Go one period beyond the end date to be safe
issue_dates = roll_dates - roll_dates.freq
mat_dates = issue_dates + pd.offsets.DateOffset(years=maturity)
series_name = 'govt_10Y'
names = pd.Series(mat_dates).apply( lambda x : 'govt_%s' % x.strftime('%Y_%m'))
# Build a time series of OTR
govt_otr = pd.DataFrame( [ [ name for name, roll_date in zip(names, roll_dates) if roll_date >=d ][0] for d in timeline ],
index=timeline,
columns=[series_name])
# Create a data frame of reference data
govt_data = pd.DataFrame( {'mat_date':mat_dates, 'issue_date': issue_dates, 'roll_date':roll_dates}, index = names)
govt_data['coupon'] = coupon
# Create the "roll map"
govt_roll_map = govt_otr.copy()
govt_roll_map['target'] = govt_otr[series_name].shift(-1)
govt_roll_map = govt_roll_map[ govt_roll_map[series_name] != govt_roll_map['target']]
govt_roll_map['factor'] = 1.
govt_roll_map = govt_roll_map.reset_index().set_index(series_name).rename(columns={'index':'date'}).dropna()
# Market Data and Risk
govt_yield_initial = 2.0
govt_yield_vol = 1.
govt_yield = pd.DataFrame( columns = govt_data.index, index=timeline )
govt_yield_ts = (govt_yield_initial + np.cumsum( np.random.normal( 0., govt_yield_vol/np.sqrt(252), len(timeline)))).reshape(-1,1)
govt_yield.loc[:,:] = govt_yield_ts
govt_mat = pd.DataFrame( columns = govt_data.index, index=timeline, data=pd.NA ).astype('datetime64')
govt_mat.loc[:,:] = govt_data['mat_date'].values.T
govt_ttm = (govt_mat - timeline.values.reshape(-1,1))/pd.Timedelta('1Y')
govt_coupon = pd.DataFrame( columns = govt_data.index, index=timeline )
govt_coupon.loc[:,:] = govt_data['coupon'].values.T
govt_accrued = govt_coupon.multiply( timeline.to_series().diff()/pd.Timedelta('1Y'), axis=0 )
govt_accrued.iloc[0] = 0
govt_price = yield_to_price( govt_yield, govt_ttm, govt_coupon )
govt_price[ govt_ttm <= 0 ] = 100.
govt_price = censor(govt_price, govt_data)
govt_pvbp = pvbp( govt_yield, govt_ttm, govt_coupon)
govt_pvbp[ govt_ttm <= 0 ] = 0.
govt_pvbp = censor(govt_pvbp, govt_data)
/opt/homebrew/lib/python3.9/site-packages/IPython/core/interactiveshell.py:3397: FutureWarning: Units 'M', 'Y' and 'y' do not represent unambiguous timedelta values and will be removed in a future version
exec(code_obj, self.user_global_ns, self.user_ns)
# Corporate Bonds: Create synthetic data for a universe of corporate bonds
# Reference Data
n_corp = 50 # Number of corporate bonds to generate
avg_ttm = 10 # Average time to maturity, in years
coupon_mean = 5
coupon_std = 1.5
mat_dates = start_date + np.random.exponential(avg_ttm*365, n_corp).astype(int) * pd.offsets.Day()
issue_dates = np.minimum( mat_dates, end_date ) - np.random.exponential(avg_ttm*365, n_corp).astype(int) * pd.offsets.Day()
names = pd.Series( [ 'corp{:04d}'.format(i) for i in range(n_corp)])
coupons = np.random.normal( coupon_mean, coupon_std, n_corp ).round(3)
corp_data = pd.DataFrame( {'mat_date':mat_dates, 'issue_date': issue_dates, 'coupon':coupons}, index=names)
# Market Data and Risk
# Model: corporate yield = government yield + credit spread
# Model: credit spread changes = beta * common factor changes + idiosyncratic changes
corp_spread_initial = np.random.normal( 2, 1, len(corp_data) )
corp_betas_raw = np.random.normal( 1, 0.5, len(corp_data) )
corp_factor_vol = 0.5
corp_idio_vol = 0.5
corp_factor_ts = np.cumsum( np.random.normal( 0, corp_factor_vol/np.sqrt(252), len(timeline))).reshape(-1,1)
corp_idio_ts = np.cumsum( np.random.normal( 0, corp_idio_vol/np.sqrt(252), len(timeline))).reshape(-1,1)
corp_spread = corp_spread_initial + np.multiply( corp_factor_ts, corp_betas_raw ) + corp_idio_ts
corp_yield = govt_yield_ts + corp_spread
corp_yield = pd.DataFrame( columns = corp_data.index, index=timeline, data = corp_yield )
corp_mat = pd.DataFrame( columns = corp_data.index, index=timeline, data=start_date )
corp_mat.loc[:,:] = corp_data['mat_date'].values.T
corp_ttm = (corp_mat - timeline.values.reshape(-1,1))/pd.Timedelta('1Y')
corp_coupon = pd.DataFrame( columns = corp_data.index, index=timeline )
corp_coupon.loc[:,:] = corp_data['coupon'].values.T
corp_accrued = corp_coupon.multiply( timeline.to_series().diff()/pd.Timedelta('1Y'), axis=0 )
corp_accrued.iloc[0] = 0
corp_price = yield_to_price( corp_yield, corp_ttm, corp_coupon )
corp_price[ corp_ttm <= 0 ] = 100.
corp_price = censor(corp_price, corp_data)
corp_pvbp = pvbp( corp_yield, corp_ttm, corp_coupon)
corp_pvbp[ corp_ttm <= 0 ] = 0.
corp_pvbp = censor(corp_pvbp, corp_data)
bidoffer_bps = 5.
corp_bidoffer = -bidoffer_bps * corp_pvbp
corp_betas = pd.DataFrame( columns = corp_data.index, index=timeline )
corp_betas.loc[:,:] = corp_betas_raw
corp_betas = censor(corp_betas, corp_data)
/opt/homebrew/lib/python3.9/site-packages/IPython/core/interactiveshell.py:3397: FutureWarning: Units 'M', 'Y' and 'y' do not represent unambiguous timedelta values and will be removed in a future version
exec(code_obj, self.user_global_ns, self.user_ns)
示例 1: 基本策略¶
# Set up a strategy and a backtest
# The goal here is to define an equal weighted portfolio of corporate bonds,
# and to hedge the rates risk with the rolling series of government bonds
# Define Algo Stacks as the various building blocks
# Note that the order in which we execute these is extremely important
lifecycle_stack = bt.core.AlgoStack(
# Close any matured bond positions (including hedges)
bt.algos.ClosePositionsAfterDates( 'maturity' ),
# Roll government bond positions into the On The Run
bt.algos.RollPositionsAfterDates( 'govt_roll_map' ),
)
risk_stack = bt.AlgoStack(
# Specify how frequently to calculate risk
bt.algos.Or( [bt.algos.RunWeekly(),
bt.algos.RunMonthly()] ),
# Update the risk given any positions that have been put on so far in the current step
bt.algos.UpdateRisk( 'pvbp', history=1),
bt.algos.UpdateRisk( 'beta', history=1),
)
hedging_stack = bt.AlgoStack(
# Specify how frequently to hedge risk
bt.algos.RunMonthly(),
# Select the "alias" for the on-the-run government bond...
bt.algos.SelectThese( [series_name], include_no_data = True ),
# ... and then resolve it to the underlying security for the given date
bt.algos.ResolveOnTheRun( 'govt_otr' ),
# Hedge out the pvbp risk using the selected government bond
bt.algos.HedgeRisks( ['pvbp']),
# Need to update risk again after hedging so that it gets recorded correctly (post-hedges)
bt.algos.UpdateRisk( 'pvbp', history=True),
)
debug_stack = bt.core.AlgoStack(
# Specify how frequently to display debug info
bt.algos.RunMonthly(),
bt.algos.PrintInfo('Strategy {name} : {now}.\tNotional: {_notl_value:0.0f},\t Value: {_value:0.0f},\t Price: {_price:0.4f}'),
bt.algos.PrintRisk('Risk: \tPVBP: {pvbp:0.0f},\t Beta: {beta:0.0f}'),
)
trading_stack =bt.core.AlgoStack(
# Specify how frequently to rebalance the portfolio
bt.algos.RunMonthly(),
# Select instruments for rebalancing. Start with everything
bt.algos.SelectAll(),
# Prevent matured/rolled instruments from coming back into the mix
bt.algos.SelectActive(),
# Select only corp instruments
bt.algos.SelectRegex( 'corp' ),
# Specify how to weigh the securities
bt.algos.WeighEqually(),
# Set the target portfolio size
bt.algos.SetNotional( 'notional_value' ),
# Rebalance the portfolio
bt.algos.Rebalance()
)
govt_securities = [ bt.CouponPayingHedgeSecurity( name ) for name in govt_data.index]
corp_securities = [ bt.CouponPayingSecurity( name ) for name in corp_data.index ]
securities = govt_securities + corp_securities
base_strategy = bt.FixedIncomeStrategy('BaseStrategy', [ lifecycle_stack, bt.algos.Or( [trading_stack, risk_stack, debug_stack ] ) ], children = securities)
hedged_strategy = bt.FixedIncomeStrategy('HedgedStrategy', [ lifecycle_stack, bt.algos.Or( [trading_stack, risk_stack, hedging_stack, debug_stack ] ) ], children = securities)
#Collect all the data for the strategies
# Here we use clean prices as the data and accrued as the coupon. Could alternatively use dirty prices and cashflows.
data = pd.concat( [ govt_price, corp_price ], axis=1) / 100. # Because we need prices per unit notional
additional_data = { 'coupons' : pd.concat([govt_accrued, corp_accrued], axis=1) / 100.,
'bidoffer' : corp_bidoffer/100.,
'notional_value' : pd.Series( data=1e6, index=data.index ),
'maturity' : pd.concat([govt_data, corp_data], axis=0).rename(columns={"mat_date": "date"}),
'govt_roll_map' : govt_roll_map,
'govt_otr' : govt_otr,
'unit_risk' : {'pvbp' : pd.concat( [ govt_pvbp, corp_pvbp] ,axis=1)/100.,
'beta' : corp_betas * corp_pvbp / 100.},
}
base_test = bt.Backtest( base_strategy, data, 'BaseBacktest',
initial_capital = 0,
additional_data = additional_data )
hedge_test = bt.Backtest( hedged_strategy, data, 'HedgedBacktest',
initial_capital = 0,
additional_data = additional_data)
out = bt.run( base_test, hedge_test )
Strategy BaseStrategy : 2020-01-01 00:00:00. Notional: 1000000, Value: -1644, Price: 99.8356
Risk: PVBP: -658, Beta: -659
Strategy BaseStrategy : 2020-02-03 00:00:00. Notional: 1000000, Value: -6454, Price: 99.3546
Risk: PVBP: -642, Beta: -643
Strategy BaseStrategy : 2020-03-02 00:00:00. Notional: 1000000, Value: -26488, Price: 97.3512
Risk: PVBP: -611, Beta: -613
Strategy BaseStrategy : 2020-04-01 00:00:00. Notional: 1000000, Value: -20295, Price: 97.9705
Risk: PVBP: -607, Beta: -608
Strategy BaseStrategy : 2020-05-01 00:00:00. Notional: 1000000, Value: -43692, Price: 95.6308
Risk: PVBP: -573, Beta: -574
Strategy BaseStrategy : 2020-06-01 00:00:00. Notional: 1000000, Value: -41095, Price: 95.8905
Risk: PVBP: -566, Beta: -566
Strategy BaseStrategy : 2020-07-01 00:00:00. Notional: 1000000, Value: -15724, Price: 98.4985
Risk: PVBP: -609, Beta: -608
Strategy BaseStrategy : 2020-08-03 00:00:00. Notional: 1000000, Value: -22308, Price: 97.8400
Risk: PVBP: -587, Beta: -594
Strategy BaseStrategy : 2020-09-01 00:00:00. Notional: 1000000, Value: 12832, Price: 101.4263
Risk: PVBP: -644, Beta: -650
Strategy BaseStrategy : 2020-10-01 00:00:00. Notional: 1000000, Value: 35263, Price: 103.6965
Risk: PVBP: -683, Beta: -680
Strategy BaseStrategy : 2020-11-02 00:00:00. Notional: 1000000, Value: 3702, Price: 100.5404
Risk: PVBP: -638, Beta: -646
Strategy BaseStrategy : 2020-12-01 00:00:00. Notional: 1000000, Value: -18534, Price: 98.3168
Risk: PVBP: -606, Beta: -613
Strategy BaseStrategy : 2021-01-01 00:00:00. Notional: 1000000, Value: -11054, Price: 99.0648
Risk: PVBP: -603, Beta: -609
Strategy BaseStrategy : 2021-02-01 00:00:00. Notional: 1000000, Value: -16424, Price: 98.5537
Risk: PVBP: -602, Beta: -609
Strategy BaseStrategy : 2021-03-01 00:00:00. Notional: 1000000, Value: -34462, Price: 96.6943
Risk: PVBP: -603, Beta: -586
Strategy BaseStrategy : 2021-04-01 00:00:00. Notional: 1000000, Value: -23533, Price: 97.7872
Risk: PVBP: -603, Beta: -586
Strategy BaseStrategy : 2021-05-03 00:00:00. Notional: 1000000, Value: -27024, Price: 97.4381
Risk: PVBP: -590, Beta: -574
Strategy BaseStrategy : 2021-06-01 00:00:00. Notional: 1000000, Value: -50723, Price: 95.0682
Risk: PVBP: -558, Beta: -541
Strategy BaseStrategy : 2021-07-01 00:00:00. Notional: 1000000, Value: -52714, Price: 94.8690
Risk: PVBP: -547, Beta: -528
Strategy BaseStrategy : 2021-08-02 00:00:00. Notional: 1000000, Value: -53039, Price: 94.8067
Risk: PVBP: -550, Beta: -531
Strategy BaseStrategy : 2021-09-01 00:00:00. Notional: 1000000, Value: -39027, Price: 96.2079
Risk: PVBP: -550, Beta: -524
Strategy BaseStrategy : 2021-10-01 00:00:00. Notional: 1000000, Value: -2051, Price: 99.9002
Risk: PVBP: -588, Beta: -561
Strategy BaseStrategy : 2021-11-01 00:00:00. Notional: 1000000, Value: -8616, Price: 99.2438
Risk: PVBP: -573, Beta: -544
Strategy BaseStrategy : 2021-12-01 00:00:00. Notional: 1000000, Value: 53520, Price: 105.6538
Risk: PVBP: -656, Beta: -623
Strategy HedgedStrategy : 2020-01-01 00:00:00. Notional: 1000000, Value: -1644, Price: 99.8356
Risk: PVBP: 0, Beta: -659
Strategy HedgedStrategy : 2020-02-03 00:00:00. Notional: 1000000, Value: -10996, Price: 98.9004
Risk: PVBP: 0, Beta: -643
Strategy HedgedStrategy : 2020-03-02 00:00:00. Notional: 1000000, Value: -16765, Price: 98.3235
Risk: PVBP: 0, Beta: -613
Strategy HedgedStrategy : 2020-04-01 00:00:00. Notional: 1000000, Value: -21649, Price: 97.8351
Risk: PVBP: -0, Beta: -608
Strategy HedgedStrategy : 2020-05-01 00:00:00. Notional: 1000000, Value: -33399, Price: 96.6601
Risk: PVBP: 0, Beta: -574
Strategy HedgedStrategy : 2020-06-01 00:00:00. Notional: 1000000, Value: -22927, Price: 97.7073
Risk: PVBP: -0, Beta: -566
Strategy HedgedStrategy : 2020-07-01 00:00:00. Notional: 1000000, Value: -14965, Price: 98.5366
Risk: PVBP: -0, Beta: -608
Strategy HedgedStrategy : 2020-08-03 00:00:00. Notional: 1000000, Value: 5092, Price: 100.5423
Risk: PVBP: -0, Beta: -594
Strategy HedgedStrategy : 2020-09-01 00:00:00. Notional: 1000000, Value: 22278, Price: 102.2828
Risk: PVBP: 0, Beta: -650
Strategy HedgedStrategy : 2020-10-01 00:00:00. Notional: 1000000, Value: 13903, Price: 101.4286
Risk: PVBP: -0, Beta: -680
Strategy HedgedStrategy : 2020-11-02 00:00:00. Notional: 1000000, Value: 12081, Price: 101.2464
Risk: PVBP: -0, Beta: -646
Strategy HedgedStrategy : 2020-12-01 00:00:00. Notional: 1000000, Value: 10531, Price: 101.0914
Risk: PVBP: -0, Beta: -613
Strategy HedgedStrategy : 2021-01-01 00:00:00. Notional: 1000000, Value: 12144, Price: 101.2528
Risk: PVBP: 0, Beta: -609
Strategy HedgedStrategy : 2021-02-01 00:00:00. Notional: 1000000, Value: 15903, Price: 101.6469
Risk: PVBP: -0, Beta: -609
Strategy HedgedStrategy : 2021-03-01 00:00:00. Notional: 1000000, Value: 11958, Price: 101.2204
Risk: PVBP: 0, Beta: -586
Strategy HedgedStrategy : 2021-04-01 00:00:00. Notional: 1000000, Value: 28170, Price: 102.8417
Risk: PVBP: -0, Beta: -586
Strategy HedgedStrategy : 2021-05-03 00:00:00. Notional: 1000000, Value: 34561, Price: 103.4807
Risk: PVBP: 0, Beta: -574
Strategy HedgedStrategy : 2021-06-01 00:00:00. Notional: 1000000, Value: 29233, Price: 102.9479
Risk: PVBP: -0, Beta: -541
Strategy HedgedStrategy : 2021-07-01 00:00:00. Notional: 1000000, Value: 10323, Price: 101.0569
Risk: PVBP: 0, Beta: -528
Strategy HedgedStrategy : 2021-08-02 00:00:00. Notional: 1000000, Value: 14539, Price: 101.4646
Risk: PVBP: 0, Beta: -531
Strategy HedgedStrategy : 2021-09-01 00:00:00. Notional: 1000000, Value: 10754, Price: 101.0860
Risk: PVBP: 0, Beta: -524
Strategy HedgedStrategy : 2021-10-01 00:00:00. Notional: 1000000, Value: 32502, Price: 103.2515
Risk: PVBP: -0, Beta: -561
Strategy HedgedStrategy : 2021-11-01 00:00:00. Notional: 1000000, Value: 24506, Price: 102.4519
Risk: PVBP: -0, Beta: -544
Strategy HedgedStrategy : 2021-12-01 00:00:00. Notional: 1000000, Value: 42093, Price: 104.2905
Risk: PVBP: -0, Beta: -623
# Extract Tear Sheet for base backtest
stats = out['BaseBacktest']
stats.display()
Stats for BaseBacktest from 2019-12-31 00:00:00 - 2021-12-31 00:00:00
Annual risk-free rate considered: 0.00%
Summary:
Total Return Sharpe CAGR Max Drawdown
-------------- -------- ------ --------------
2.34% 0.19 1.16% -10.64%
Annualized Returns:
mtd 3m 6m ytd 1y 3y 5y 10y incep.
------ ----- ----- ----- ----- ----- ---- ----- --------
-3.06% 1.45% 8.12% 3.43% 3.43% 1.16% - - 1.16%
Periodic:
daily monthly yearly
------ ------- --------- --------
sharpe 0.19 0.18 0.38
mean 1.38% 1.49% 1.19%
vol 7.26% 8.35% 3.17%
skew 0.16 0.75 -
kurt 0.52 0.70 -
best 1.59% 6.32% 3.43%
worst -1.44% -3.29% -1.05%
Drawdowns:
max avg # days
------- ------ --------
-10.64% -2.59% 79.22
Misc:
--------------- ------
avg. up month 1.88%
avg. down month -1.63%
up year % 50.00%
12m up % 57.14%
--------------- ------
# Extract Tear Sheet for hedged backtest
stats = out['HedgedBacktest']
stats.display()
Stats for HedgedBacktest from 2019-12-31 00:00:00 - 2021-12-31 00:00:00
Annual risk-free rate considered: 0.00%
Summary:
Total Return Sharpe CAGR Max Drawdown
-------------- -------- ------ --------------
3.51% 0.41 1.74% -3.87%
Annualized Returns:
mtd 3m 6m ytd 1y 3y 5y 10y incep.
------ ------ ----- ----- ----- ----- ---- ----- --------
-0.47% -0.30% 2.29% 2.46% 2.46% 1.74% - - 1.74%
Periodic:
daily monthly yearly
------ ------- --------- --------
sharpe 0.41 0.43 1.71
mean 1.75% 1.81% 1.74%
vol 4.26% 4.22% 1.02%
skew -0.17 0.67 -
kurt 0.21 -0.46 -
best 0.69% 2.82% 2.46%
worst -1.07% -1.62% 1.02%
Drawdowns:
max avg # days
------ ------ --------
-3.87% -1.02% 49.57
Misc:
--------------- -------
avg. up month 1.25%
avg. down month -0.78%
up year % 100.00%
12m up % 85.71%
--------------- -------
# Total PNL time series values
pd.DataFrame( {'base':base_test.strategy.values, 'hedged':hedge_test.strategy.values} ).plot();

# Total risk time series values
pd.DataFrame( {'base_pvbp':base_test.strategy.risks['pvbp'],
'hedged_pvbp':hedge_test.strategy.risks['pvbp'],
'beta':hedge_test.strategy.risks['beta']} ).dropna().plot();

# Total bid/offer paid (same for both strategies)
pd.DataFrame( {'base_pvbp':base_test.strategy.bidoffers_paid,
'hedged_pvbp':hedge_test.strategy.bidoffers_paid }).cumsum().dropna().plot();

示例 2: 嵌套策略¶
# Set up a more complex strategy and a backtest
# The goal of the more complex strategy is to define two sub-strategies of corporate bonds
# - Highest yield bonds
# - Lowest yield bonds
# Then we will go long the high yield bonds, short the low yield bonds in equal weight
# Lastly we will hedge the rates risk with the government bond
govt_securities = [ bt.CouponPayingHedgeSecurity( name ) for name in govt_data.index]
corp_securities = [ bt.CouponPayingSecurity( name ) for name in corp_data.index ]
def get_algos( n, sort_descending ):
''' Helper function to return the algos for long or short portfolio, based on top n yields'''
return [
# Close any matured bond positions
bt.algos.ClosePositionsAfterDates( 'corp_maturity' ),
# Specify how frequenty to rebalance
bt.algos.RunMonthly(),
# Select instruments for rebalancing. Start with everything
bt.algos.SelectAll(),
# Prevent matured/rolled instruments from coming back into the mix
bt.algos.SelectActive(),
# Set the stat to be used for selection
bt.algos.SetStat( 'corp_yield' ),
# Select the top N yielding bonds
bt.algos.SelectN( n, sort_descending, filter_selected=True ),
# Specify how to weigh the securities
bt.algos.WeighEqually(),
bt.algos.ScaleWeights(1. if sort_descending else -1.), # Determine long/short
# Set the target portfolio size
bt.algos.SetNotional( 'notional_value' ),
# Rebalance the portfolio
bt.algos.Rebalance(),
]
bottom_algos = []
top_strategy = bt.FixedIncomeStrategy('TopStrategy', get_algos( 10, True ), children = corp_securities)
bottom_strategy = bt.FixedIncomeStrategy('BottomStrategy',get_algos( 10, False ), children = corp_securities)
risk_stack = bt.AlgoStack(
# Specify how frequently to calculate risk
bt.algos.Or( [bt.algos.RunWeekly(),
bt.algos.RunMonthly()] ),
# Update the risk given any positions that have been put on so far in the current step
bt.algos.UpdateRisk( 'pvbp', history=2),
bt.algos.UpdateRisk( 'beta', history=2),
)
hedging_stack = bt.AlgoStack(
# Close any matured hedge positions (including hedges)
bt.algos.ClosePositionsAfterDates( 'govt_maturity' ),
# Roll government bond positions into the On The Run
bt.algos.RollPositionsAfterDates( 'govt_roll_map' ),
# Specify how frequently to hedge risk
bt.algos.RunMonthly(),
# Select the "alias" for the on-the-run government bond...
bt.algos.SelectThese( [series_name], include_no_data = True ),
# ... and then resolve it to the underlying security for the given date
bt.algos.ResolveOnTheRun( 'govt_otr' ),
# Hedge out the pvbp risk using the selected government bond
bt.algos.HedgeRisks( ['pvbp']),
# Need to update risk again after hedging so that it gets recorded correctly (post-hedges)
bt.algos.UpdateRisk( 'pvbp', history=2),
)
debug_stack = bt.core.AlgoStack(
# Specify how frequently to display debug info
bt.algos.RunMonthly(),
bt.algos.PrintInfo('{now}: End {name}\tNotional: {_notl_value:0.0f},\t Value: {_value:0.0f},\t Price: {_price:0.4f}'),
bt.algos.PrintRisk('Risk: \tPVBP: {pvbp:0.0f},\t Beta: {beta:0.0f}'),
)
trading_stack =bt.core.AlgoStack(
# Specify how frequently to rebalance the portfolio of sub-strategies
bt.algos.RunOnce(),
# Specify how to weigh the sub-strategies
bt.algos.WeighSpecified( TopStrategy=0.5, BottomStrategy=-0.5),
# Rebalance the portfolio
bt.algos.Rebalance()
)
children = [ top_strategy, bottom_strategy ] + govt_securities
base_strategy = bt.FixedIncomeStrategy('BaseStrategy', [ bt.algos.Or( [trading_stack, risk_stack, debug_stack ] ) ], children = children)
hedged_strategy = bt.FixedIncomeStrategy('HedgedStrategy', [ bt.algos.Or( [trading_stack, risk_stack, hedging_stack, debug_stack ] ) ], children = children)
# Here we use clean prices as the data and accrued as the coupon. Could alternatively use dirty prices and cashflows.
data = pd.concat( [ govt_price, corp_price ], axis=1) / 100. # Because we need prices per unit notional
additional_data = { 'coupons' : pd.concat([govt_accrued, corp_accrued], axis=1) / 100., # Because we need coupons per unit notional
'notional_value' : pd.Series( data=1e6, index=data.index ),
'govt_maturity' : govt_data.rename(columns={"mat_date": "date"}),
'corp_maturity' : corp_data.rename(columns={"mat_date": "date"}),
'govt_roll_map' : govt_roll_map,
'govt_otr' : govt_otr,
'corp_yield' : corp_yield,
'unit_risk' : {'pvbp' : pd.concat( [ govt_pvbp, corp_pvbp] ,axis=1)/100.,
'beta' : corp_betas * corp_pvbp / 100.},
}
base_test = bt.Backtest( base_strategy, data, 'BaseBacktest',
initial_capital = 0,
additional_data = additional_data)
hedge_test = bt.Backtest( hedged_strategy, data, 'HedgedBacktest',
initial_capital = 0,
additional_data = additional_data)
out = bt.run( base_test, hedge_test )
2020-01-01 00:00:00: End BaseStrategy Notional: 0, Value: 0, Price: 100.0000
Risk: PVBP: 0, Beta: 0
2020-02-03 00:00:00: End BaseStrategy Notional: 2000000, Value: 3277, Price: 100.1639
Risk: PVBP: 51, Beta: 41
2020-03-02 00:00:00: End BaseStrategy Notional: 2000000, Value: 7297, Price: 100.3649
Risk: PVBP: 45, Beta: 34
2020-04-01 00:00:00: End BaseStrategy Notional: 2000000, Value: 9336, Price: 100.4668
Risk: PVBP: 44, Beta: 34
2020-05-01 00:00:00: End BaseStrategy Notional: 2000000, Value: 13453, Price: 100.6727
Risk: PVBP: 38, Beta: 28
2020-06-01 00:00:00: End BaseStrategy Notional: 2000000, Value: 15887, Price: 100.7943
Risk: PVBP: 37, Beta: 26
2020-07-01 00:00:00: End BaseStrategy Notional: 1800000, Value: 16024, Price: 100.8010
Risk: PVBP: 39, Beta: 28
2020-08-03 00:00:00: End BaseStrategy Notional: 2000000, Value: 14785, Price: 100.7391
Risk: PVBP: -152, Beta: -124
2020-09-01 00:00:00: End BaseStrategy Notional: 1800000, Value: 30310, Price: 101.5550
Risk: PVBP: -263, Beta: -204
2020-10-01 00:00:00: End BaseStrategy Notional: 1900000, Value: 35915, Price: 101.8430
Risk: PVBP: -109, Beta: -53
2020-11-02 00:00:00: End BaseStrategy Notional: 2000000, Value: 37649, Price: 101.9297
Risk: PVBP: -12, Beta: 36
2020-12-01 00:00:00: End BaseStrategy Notional: 2000000, Value: 39045, Price: 101.9995
Risk: PVBP: -13, Beta: 34
2021-01-01 00:00:00: End BaseStrategy Notional: 2000000, Value: 40569, Price: 102.0758
Risk: PVBP: -14, Beta: 31
2021-02-01 00:00:00: End BaseStrategy Notional: 1900000, Value: 41228, Price: 102.1094
Risk: PVBP: -16, Beta: 27
2021-03-01 00:00:00: End BaseStrategy Notional: 1900000, Value: 38916, Price: 101.9868
Risk: PVBP: -101, Beta: -47
2021-04-01 00:00:00: End BaseStrategy Notional: 2000000, Value: 40755, Price: 102.0788
Risk: PVBP: 9, Beta: -31
2021-05-03 00:00:00: End BaseStrategy Notional: 2000000, Value: 43290, Price: 102.2055
Risk: PVBP: -6, Beta: -43
2021-06-01 00:00:00: End BaseStrategy Notional: 2000000, Value: 35947, Price: 101.8384
Risk: PVBP: -235, Beta: -91
2021-07-01 00:00:00: End BaseStrategy Notional: 2000000, Value: 35671, Price: 101.8246
Risk: PVBP: -123, Beta: -129
2021-08-02 00:00:00: End BaseStrategy Notional: 2000000, Value: 37756, Price: 101.9288
Risk: PVBP: 3, Beta: -29
2021-09-01 00:00:00: End BaseStrategy Notional: 2000000, Value: 38434, Price: 101.9627
Risk: PVBP: -7, Beta: -43
2021-10-01 00:00:00: End BaseStrategy Notional: 1900000, Value: 37082, Price: 101.8966
Risk: PVBP: 73, Beta: 19
2021-11-01 00:00:00: End BaseStrategy Notional: 2000000, Value: 39526, Price: 102.0187
Risk: PVBP: 51, Beta: 53
2021-12-01 00:00:00: End BaseStrategy Notional: 1900000, Value: 29228, Price: 101.4826
Risk: PVBP: 125, Beta: 97
2020-01-01 00:00:00: End HedgedStrategy Notional: 0, Value: 0, Price: 100.0000
Risk: PVBP: 0, Beta: 0
2020-02-03 00:00:00: End HedgedStrategy Notional: 2000000, Value: 3277, Price: 100.1639
Risk: PVBP: 0, Beta: 41
2020-03-02 00:00:00: End HedgedStrategy Notional: 2000000, Value: 6159, Price: 100.3079
Risk: PVBP: 0, Beta: 34
2020-04-01 00:00:00: End HedgedStrategy Notional: 2000000, Value: 9008, Price: 100.4504
Risk: PVBP: 0, Beta: 34
2020-05-01 00:00:00: End HedgedStrategy Notional: 2000000, Value: 12274, Price: 100.6137
Risk: PVBP: 0, Beta: 28
2020-06-01 00:00:00: End HedgedStrategy Notional: 2000000, Value: 14189, Price: 100.7094
Risk: PVBP: 0, Beta: 26
2020-07-01 00:00:00: End HedgedStrategy Notional: 1800000, Value: 15451, Price: 100.7752
Risk: PVBP: 0, Beta: 28
2020-08-03 00:00:00: End HedgedStrategy Notional: 2000000, Value: 12494, Price: 100.6273
Risk: PVBP: 0, Beta: -124
2020-09-01 00:00:00: End HedgedStrategy Notional: 1800000, Value: 23384, Price: 101.1967
Risk: PVBP: 0, Beta: -204
2020-10-01 00:00:00: End HedgedStrategy Notional: 1900000, Value: 16414, Price: 100.8372
Risk: PVBP: -0, Beta: -53
2020-11-02 00:00:00: End HedgedStrategy Notional: 2000000, Value: 22887, Price: 101.1609
Risk: PVBP: 0, Beta: 36
2020-12-01 00:00:00: End HedgedStrategy Notional: 2000000, Value: 24681, Price: 101.2506
Risk: PVBP: 0, Beta: 34
2021-01-01 00:00:00: End HedgedStrategy Notional: 2000000, Value: 26080, Price: 101.3205
Risk: PVBP: -0, Beta: 31
2021-02-01 00:00:00: End HedgedStrategy Notional: 1900000, Value: 26954, Price: 101.3647
Risk: PVBP: 0, Beta: 27
2021-03-01 00:00:00: End HedgedStrategy Notional: 1900000, Value: 25008, Price: 101.2611
Risk: PVBP: 0, Beta: -47
2021-04-01 00:00:00: End HedgedStrategy Notional: 2000000, Value: 27730, Price: 101.3972
Risk: PVBP: 0, Beta: -31
2021-05-03 00:00:00: End HedgedStrategy Notional: 2000000, Value: 30112, Price: 101.5163
Risk: PVBP: 0, Beta: -43
2021-06-01 00:00:00: End HedgedStrategy Notional: 2000000, Value: 22951, Price: 101.1582
Risk: PVBP: -0, Beta: -91
2021-07-01 00:00:00: End HedgedStrategy Notional: 2000000, Value: 15553, Price: 100.7884
Risk: PVBP: 0, Beta: -129
2021-08-02 00:00:00: End HedgedStrategy Notional: 2000000, Value: 18657, Price: 100.9436
Risk: PVBP: 0, Beta: -29
2021-09-01 00:00:00: End HedgedStrategy Notional: 2000000, Value: 19441, Price: 100.9827
Risk: PVBP: 0, Beta: -43
2021-10-01 00:00:00: End HedgedStrategy Notional: 1900000, Value: 17903, Price: 100.9072
Risk: PVBP: 0, Beta: 19
2021-11-01 00:00:00: End HedgedStrategy Notional: 2000000, Value: 20524, Price: 101.0383
Risk: PVBP: 0, Beta: 53
2021-12-01 00:00:00: End HedgedStrategy Notional: 1900000, Value: 14170, Price: 100.7071
Risk: PVBP: 0, Beta: 97
# Total PNL time series values
pd.DataFrame( {'base':base_test.strategy.values,
'hedged':hedge_test.strategy.values,
'top':base_test.strategy['TopStrategy'].values,
'bottom':base_test.strategy['BottomStrategy'].values}
).plot();

# Total pvbp time series values
pd.DataFrame( {'base':base_test.strategy.risks['pvbp'],
'hedged':hedge_test.strategy.risks['pvbp'],
'top':base_test.strategy['TopStrategy'].risks['pvbp'],
'bottom':base_test.strategy['BottomStrategy'].risks['pvbp']}
).dropna().plot();

# Total beta time series values
pd.DataFrame( {'base':base_test.strategy.risks['beta'],
'hedged':hedge_test.strategy.risks['beta'],
'top':base_test.strategy['TopStrategy'].risks['beta'],
'bottom':base_test.strategy['BottomStrategy'].risks['beta']}
).dropna().plot();

# "Price" time series values
pd.DataFrame( {'base':base_test.strategy.prices,
'hedged':hedge_test.strategy.prices,
'top':base_test.strategy['TopStrategy'].prices,
'bottom':base_test.strategy['BottomStrategy'].prices}
).plot();

# Show transactions
out.get_transactions('HedgedBacktest').head(20)
价格 | 数量 | ||
---|---|---|---|
日期 | 安全 | ||
2020-01-01 | corp0000 | 1.009697 | -100000.0 |
corp0001 | 0.991417 | 100000.0 | |
corp0002 | 1.016553 | -100000.0 | |
corp0005 | 1.035779 | -100000.0 | |
corp0009 | 1.014195 | 100000.0 | |
corp0015 | 0.849097 | 100000.0 | |
corp0017 | 1.018107 | -100000.0 | |
corp0018 | 1.009549 | 100000.0 | |
corp0019 | 0.908531 | 100000.0 | |
corp0023 | 1.216847 | 100000.0 | |
corp0024 | 1.094375 | -100000.0 | |
corp0025 | 1.054762 | -100000.0 | |
corp0030 | 0.888091 | 100000.0 | |
corp0032 | 1.086487 | -100000.0 | |
corp0035 | 0.996676 | 100000.0 | |
corp0036 | 1.070212 | -100000.0 | |
corp0037 | 0.992530 | 100000.0 | |
corp0044 | 0.959150 | 100000.0 | |
corp0048 | 0.987408 | -100000.0 | |
corp0049 | 1.016879 | -100000.0 |