# Examples¶

Here are a few examples to give you a better idea of what bt is all about.

## SMA Strategy¶

Let’s start off with a Simple Moving Average (SMA) strategy. We will start with a simple version of the strategy, namely:

• Select the securities that are currently above their 50 day moving average

• Weigh each selected security equally

• Rebalance the portfolio to reflect the target weights

This should be pretty simple to build. The only thing missing above is the calculation of the simple moving average. When should this take place?

Given the flexibility of bt, there is no strict rule. The average calculation could be performed in an Algo, but that would be pretty inefficient. A better way would be to calculate the moving average at the beginning - before starting the backtest. After all, all the data is known in advance.

Now that we know what we have to do, let’s get started. First we will download some data and calculate the simple moving average.

```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()
```

It’s always a good idea to plot your data to make sure it looks ok. So let’s see how the data + sma plot looks like.

```# 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))
```

Looks legit.

Now that we have our data, we will need to create our security selection logic. Let’s create a basic Algo that will select the securities that are above their moving average.

Before we do that, let’s think about how we will code it. We could pass the SMA data and then extract the row (from the sma DataFrame) on the current date, compare the values to the current prices, and then keep a list of those securities where the price is above the SMA. This is the most straightforward approach. However, this is not very re-usable because the logic within the Algo will be quite specific to the task at hand and if we wish to change the logic, we will have to write a new algo.

For example, what if we wanted to select securities that were below their sma? Or what if we only wanted securities that were 5% above their sma?

What we could do instead is pre-calculate the selection logic DataFrame (a fast, vectorized operation) and write a generic Algo that takes in this boolean DataFrame and returns the securities where the value is True on a given date. This will be must faster and much more reusable. Let’s see how the implementation looks like.

```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
```

So there we have it. Our selection Algo.

Note

By the way, this Algo already exists - I just wanted to show you how you would code it from scratch. `Here is the code`.

All we have to do now is pass in a signal matrix. In our case, it’s quite easy:

```signal = data > sma
```

Simple, concise and more importantly, fast! Let’s move on and test the strategy.

```# 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)
```

So just to recap, we created the strategy, created the backtest by joining Strategy+Data, and ran the backtest. Let’s see the results.

```# 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%
```

Nothing stellar but at least you learnt something along the way (I hope).

Oh, and one more thing. If you were to write your own “library” of backtests, you might want to write yourself a helper function that would allow you to test different parameters and securities. That function might look something like this:

```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)
```

This function allows us to easily generate backtests. We could easily compare a few different SMA periods. Also, let’s see if we can beat a long-only allocation to the 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%
```

And there you have it. Beating the market ain’t that easy!

## SMA Crossover Strategy¶

Let’s build on the last section to test a moving average crossover strategy. The easiest way to achieve this is to build an Algo similar to SelectWhere, but for the purpose of setting target weights. Let’s call this algo WeighTarget. This algo will take a DataFrame of target weights that we will pre-calculate.

Basically, when the 50 day moving average will be above the 200-day moving average, we will be long (+1 target weight). Conversely, when the 50 is below the 200, we will be short (-1 target weight).

Here’s the WeighTarget implementation (this Algo also already exists in the algos module):

```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
```

So let’s start with a simple 50-200 day sma crossover for a single security.

```## 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
```

Ok so we downloaded our data, calculated the simple moving averages, and then we setup our target weight (tw) DataFrame. Let’s take a look at our target weights to see if they make any sense.

```# 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'])
```

As mentioned earlier, it’s always a good idea to plot your strategy data. It is usually easier to spot logic/programming errors this way, especially when dealing with lots of data.

Now let’s move on with the Strategy & Backtest.

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

t = bt.Backtest(ma_cross, data)
res = bt.run(t)
```
```res.plot();
```

Ok great so there we have our basic moving average crossover strategy.

## Exploring the Tree Structure¶

So far, we have explored strategies that allocate capital to securities. But what if we wanted to test a strategy that allocated capital to sub-strategies?

The most straightforward way would be to test the different sub-strategies, extract their equity curves and create “synthetic securities” that would basically just represent the returns achieved from allocating capital to the different sub-strategies.

Let’s see how this looks:

```# 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();
```

As we can see above, the process is a bit more involved, but it works. It is not very elegant though, and obtaining security-level allocation information is problematic.

Luckily, bt has built-in functionality for dealing with strategies of strategies. It uses the same general principal as demonstrated above but does it seamlessly. Basically, when a strategy is a child of another strategy, it will create a “paper trade” version of itself internally. As we run our strategy, it will run its internal “paper version” and use the returns from that strategy to populate the price property.

This means that the parent strategy can use the price information (which reflects the returns of the strategy had it been employed) to determine the appropriate allocation. Again, this is basically the same process as above, just packed into 1 step.

Perhaps some code will help:

```# 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();
```

So there you have it. Simpler, and more complete.

## Buy and Hold Strategy¶

```import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

import ffn
import bt

%matplotlib inline
```

### Create Fake Index Data¶

```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();
```

### Build Strategy¶

```# 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
]
)
```

### Run Backtest¶

Note: The logic of the strategy is seperate from the data used in the backtest.

```# 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
```
static
start 2017-01-01 00:00:00
end 2017-12-29 00:00:00
rf 0.0
total_return 0.229372
cagr 0.231653
max_drawdown -0.069257
calmar 3.344851
mtd -0.000906
three_month 0.005975
six_month 0.142562
ytd 0.229372
one_year NaN
three_year NaN
five_year NaN
ten_year NaN
incep 0.231653
daily_sharpe 1.804549
daily_sortino 3.306154
daily_mean 0.206762
daily_vol 0.114578
daily_skew 0.012208
daily_kurt -0.04456
best_day 0.020402
worst_day -0.0201
monthly_sharpe 2.806444
monthly_sortino 15.352486
monthly_mean 0.257101
monthly_vol 0.091611
monthly_skew 0.753881
monthly_kurt 0.456278
best_month 0.073657
worst_month -0.014592
yearly_sharpe NaN
yearly_sortino NaN
yearly_mean NaN
yearly_vol NaN
yearly_skew NaN
yearly_kurt NaN
best_year NaN
worst_year NaN
avg_drawdown -0.016052
avg_drawdown_days 12.695652
avg_up_month 0.03246
avg_down_month -0.008001
win_year_perc NaN
twelve_month_win_perc NaN
```res.prices.head()
```
static
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()
```

Strategy value over time

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

Strategy Outlays

Outlays are the total dollar amount spent(gained) by a purchase(sale) of securities.

```res.backtest_list[0].strategy.outlays.plot();
```

You can get the change in number of shares purchased a

```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 rows × 2 columns

## Trend Example 1¶

```import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

import ffn
import bt

%matplotlib inline
```

### Create fake data¶

```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();
```

### Create Trend signal over the last 12 months¶

```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 False True True True True
2017-08-24 False True True True True
2017-08-25 False True True True True
2017-08-28 False True True True True
2017-08-29 False True True True True

Compare EW and 1/vol

Both strategies rebalance daily using trend with 1 day lag and weights limited to 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
start 2008-01-01 00:00:00
end 2017-08-29 00:00:00
rf 0.0
total_return 1.982933
cagr 0.119797
max_drawdown -0.103421
calmar 1.158343
mtd 0.017544
three_month 0.040722
six_month 0.079362
ytd 0.08107
one_year 0.100432
three_year 0.159895
five_year 0.172284
ten_year 0.119797
incep 0.119797
daily_sharpe 1.356727
daily_sortino 2.332895
daily_mean 0.112765
daily_vol 0.083116
daily_skew 0.029851
daily_kurt 0.96973
best_day 0.02107
worst_day -0.021109
monthly_sharpe 1.373241
monthly_sortino 2.966223
monthly_mean 0.118231
monthly_vol 0.086096
monthly_skew -0.059867
monthly_kurt 0.571064
best_month 0.070108
worst_month -0.064743
yearly_sharpe 1.741129
yearly_sortino inf
yearly_mean 0.129033
yearly_vol 0.074109
yearly_skew 0.990397
yearly_kurt 1.973883
best_year 0.285249
worst_year 0.024152
avg_drawdown -0.015516
avg_drawdown_days 25.223214
avg_up_month 0.024988
avg_down_month -0.012046
win_year_perc 1.0
twelve_month_win_perc 0.971429

## Trend Example 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 rows × 1 columns

```res.prices.tail()
```
example1
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
```
example1
start 2008-01-30 00:00:00
end 2017-12-31 00:00:00
rf 0.0
total_return 1.810698
cagr 0.109805
max_drawdown -0.267046
calmar 0.411186
mtd 0.05991
three_month 0.102033
six_month 0.22079
ytd 0.879847
one_year 0.879847
three_year 0.406395
five_year 0.227148
ten_year 0.109805
incep 0.109805
daily_sharpe 3.299555
daily_sortino 6.352869
daily_mean 2.448589
daily_vol 0.742097
daily_skew 0.307861
daily_kurt 1.414455
best_day 0.137711
worst_day -0.14073
monthly_sharpe 0.723148
monthly_sortino 1.392893
monthly_mean 0.117579
monthly_vol 0.162594
monthly_skew 0.301545
monthly_kurt 1.379006
best_month 0.137711
worst_month -0.14073
yearly_sharpe 0.503939
yearly_sortino 5.019272
yearly_mean 0.14814
yearly_vol 0.293964
yearly_skew 2.317496
yearly_kurt 5.894955
best_year 0.879847
worst_year -0.088543
avg_drawdown -0.091255
avg_drawdown_days 369.714286
avg_up_month 0.064341
avg_down_month -0.012928
win_year_perc 0.555556
twelve_month_win_perc 0.46789

## Strategy Combination¶

This notebook creates a parent strategy(combined) with 2 child strategies(Equal Weight, Inv Vol).

Alternatively, it creates the 2 child strategies, runs the backtest, combines the results, and creates a parent strategy using both of the backtests.

```import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

import ffn
import bt

%matplotlib inline
```

### Create fake data¶

```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();
```

In order to get the weights of each strategy, you can run each strategy, get the prices for each strategy, combine them into one price dataframe, run the combined strategy on the new data set.

```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();
```

## Equally Weighted Risk Contributions Portfolio¶

```import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

import ffn
import bt

%matplotlib inline
```

### Create Fake Index Data¶

```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();
```

### Build and run ERC Strategy¶

You can read more about ERC here. 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();
```

You can see the Total Risk Contribution is roughly equal from both assets.

## Predicted Tracking Error Rebalance Portfolio¶

```import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

import ffn
import bt

%matplotlib inline
```

### Create Fake Index Data¶

```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();
```

### Build and run Target Strategy¶

I will first run a strategy that rebalances everyday.

Then I will use those weights as target to rebalance to whenever the PTE is too high.

```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();
```

Now use the PTE rebalance algo to trigger a rebalance whenever predicted tracking error is greater than 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();
```

If we plot the total risk contribution of each asset class and divide by the total volatility, then we can see that both strategy’s contribute roughly similar amounts of volatility from both of the securities.

```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();
```

Looking at the Target strategy’s and PTE strategy’s Total Risk they are very similar.

```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();
```

We can see the Predicted Tracking Error of the PTE Strategy with each transaction marked.

## Fixed Income Examples¶

This example notebook illustrates some of the more sophisticated functionality of the package, especially related to fixed income securities and strategies. For fixed income strategies:

• capital allocations are not necessary, and initial capital is not used

• bankruptcy is disabled (as money can always be borrowed at some rate, potentially represented as another asset)

• weights are based off notional_value rather than value. For fixed income securities, notional value is just the position. For non-fixed income securities (i.e. equities), it is the market value of the position.

• strategy notional_value is always positive, equal to the sum of the magnitudes of the notional values of all its children

• strategy price is computed from additive PNL returns per unit of notional_value, with a reference price of PAR

• “rebalancing” the portfolio adjusts notionals rather than capital allocations based on weights

Further to the above characteristics of fixed income strategies, we also demonstrate the usage of the following features which arise in these types of use case:

• Coupon paying securities (i.e. bonds)

• Handing of security lifecycle such as new issues and maturity

• Usage of “On-The-Run” instruments, and rolling of positions into the “new” on-the-run security at pre-defined times

• Risk tracking/aggregation and hedging from pre-computed risk per unit notional

The notebook contains the following parts:

1. Setup

2. Market data generation

1. Rolling series of government bonds

2. Corporate bonds with spreads driven by a common factor

3. Example 1: Basic Strategies

1. Weigh all active corporate bond equally

2. Add hedging of interest rates risk with the on-the-run government bond

4. Example 2: Nested Strategies

1. One strategy buys the top N bonds, by yield

2. Another strategy sells the bottom N bonds, by yield

3. Parent strategy gives 50% weight to each of the above

4. Add hedges of remaining interest rates risk with the on-the-run government bond

### Setup¶

```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')
```

### Market Data Generation¶

```# 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)
```

### Example 1: Basic Strategies¶

```# 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();
```

### Example 2: Nested Strategies¶

```# 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)
```
price quantity
Date Security
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