示例

这里有一些例子,让你更好地了解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))
_images/examples-nb_6_0.png

看起来合法。

现在我们有了数据,我们需要创建我们的证券选择逻辑。让我们创建一个基本的算法,该算法将选择那些高于其移动平均线的证券。

在我们这样做之前,让我们考虑一下我们将如何编写代码。我们可以传递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();
_images/examples-nb_12_0.png
# 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');
_images/examples-nb_18_0.png
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'])
_images/examples-nb_26_0.png

如前所述,绘制策略数据总是一个好主意。通过这种方式通常更容易发现逻辑/编程错误,尤其是在处理大量数据时。

现在让我们继续讨论策略与回测。

ma_cross = bt.Strategy('ma_cross', [WeighTarget(tw),
                                    bt.algos.Rebalance()])

t = bt.Backtest(ma_cross, data)
res = bt.run(t)
res.plot();
_images/examples-nb_29_0.png

好的,太好了,所以我们有了我们的基本移动平均线交叉策略。

探索树结构

到目前为止,我们已经探讨了将资本分配给证券的策略。但如果我们想测试一个将资本分配给子策略的策略呢?

最直接的方法是测试不同的子策略,提取它们的权益曲线,并创建“合成证券”,这些证券基本上只代表通过将资本分配给不同子策略所实现的回报。

让我们看看这个看起来如何:

# 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();
_images/examples-nb_32_0.png
res.plot_weights();
_images/examples-nb_33_0.png

正如我们在上面所看到的,这个过程稍微复杂一些,但它是有效的。 不过,这并不是很优雅,而且获取安全级别的分配信息是有问题的。

幸运的是,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();
_images/examples-nb_36_0.png
res.plot_weights();
_images/examples-nb_37_0.png

所以,这就是了。更简单,更完整。

买入并持有策略

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();
_images/Buy_and_hold_3_0.png

构建策略

# 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()
_images/Buy_and_hold_10_0.png

策略值随时间变化

performanceStats = res['static']
#performance stats is an ffn object
res.backtest_list[0].strategy.values.plot();
_images/Buy_and_hold_12_0.png

战略支出

支出是购买(出售)证券所花费(获得)的总金额。

res.backtest_list[0].strategy.outlays.plot();
_images/Buy_and_hold_14_0.png

您可以获取购买的股票数量的变化

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();
_images/Trend_1_3_0.png

创建过去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()
_images/Trend_1_5_0.png
#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()
_images/Trend_1_12_0.png
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();
_images/Trend_2_2_0.png
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();
_images/Trend_2_5_0.png
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();
_images/Strategy_Combination_3_0.png
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();
_images/Strategy_Combination_6_0.png
res.get_security_weights().plot();
_images/Strategy_Combination_7_0.png

为了获取每个策略的权重,你可以运行每个策略,获取每个策略的价格,将它们合并成一个价格数据框,然后在新数据集上运行组合策略。

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)
_images/Strategy_Combination_10_0.png
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();
_images/Strategy_Combination_12_0.png
res.get_security_weights().plot();
_images/Strategy_Combination_13_0.png

等权重风险贡献投资组合

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();
_images/ERC_4_0.png

构建并运行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();
_images/ERC_7_0.png
res_target.prices.plot();
_images/ERC_8_0.png
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();
_images/ERC_9_0.png

你可以看到总风险贡献大致来自两个资产。

预测跟踪误差再平衡投资组合

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();
_images/PTE_3_0.png

构建并运行目标策略

我将首先运行一个每天重新平衡的策略。

然后,我将使用这些权重作为目标,每当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();
_images/PTE_6_0.png

现在使用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();
_images/PTE_9_0.png
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();
_images/PTE_13_0.png

如果我们绘制每个资产类别的总风险贡献并除以总波动率,那么我们可以看到两种策略从两种证券中贡献的波动率大致相似。

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();
_images/PTE_15_0.png

观察目标策略和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();
_images/PTE_17_0.png
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();
_images/PTE_19_0.png

我们可以看到PTE策略的预测跟踪误差,每次交易都有标记。

固定收益示例

此示例笔记本展示了该包的一些更复杂的功能,特别是与固定收益证券和策略相关的部分。对于固定收益策略:

  • 资本分配不是必要的,初始资本未被使用

  • 破产被禁用(因为钱总是可以以某种利率借到,可能表示为另一种资产)

  • 权重是基于名义价值而非实际价值。对于固定收益证券,名义价值就是头寸。对于非固定收益证券(即股票),它是头寸的市场价值。

  • 策略名义价值始终为正,等于其所有子项名义价值的绝对值之和

  • 策略价格是根据每单位名义价值的累加PNL回报计算的,参考价格为PAR

  • “重新平衡”投资组合是基于权重调整名义金额而非资本分配

除了上述固定收益策略的特点外,我们还展示了在这些类型的用例中出现的以下功能的使用:

  • 付息证券(即债券)

  • 处理安全生命周期,例如新问题和到期

  • 使用“On-The-Run”工具,并在预定时间将头寸滚动到“新”的On-The-Run证券中

  • 从预计算的每单位名义风险中进行风险跟踪/汇总和对冲

笔记本包含以下部分:

  1. 设置

  2. 市场数据生成

    1. 政府债券的滚动系列

    2. 由共同因素驱动的公司债券利差

  3. 示例1:基本策略

    1. 对所有活跃的公司债券进行等权重

    2. 使用最新的政府债券进行利率风险的对冲

  4. 示例 2:嵌套策略

    1. 一种策略是按收益率购买前N个债券

    2. 另一种策略是按收益率卖出底部N个债券

    3. 父策略为上述每个策略分配50%的权重

    4. 使用最新的政府债券对冲剩余的利率风险

设置

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();
_images/Fixed_Income_13_0.png
# 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();
_images/Fixed_Income_14_0.png
# 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();
_images/Fixed_Income_15_0.png

示例 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();
_images/Fixed_Income_18_0.png
# 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();
_images/Fixed_Income_19_0.png
# 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();
_images/Fixed_Income_20_0.png
# "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();
_images/Fixed_Income_21_0.png
# 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