# \- bt | Complete Documentation -

## index

# bt - Flexible Backtesting for Python

## What is bt?

**bt** is a flexible backtesting framework for Python used to test
quantitative trading strategies. **Backtesting** is the process of testing a
strategy over a given data set. This framework allows you to easily create
strategies that mix and match different `Algos`. It aims to foster the
creation of easily testable, re-usable and flexible blocks of strategy logic
to facilitate the rapid development of complex trading strategies.

The goal: to save **quants** from re-inventing the wheel and let them focus on
the important part of the job - strategy development.

**bt** is coded in **Python** and joins a vibrant and rich ecosystem for data
analysis. Numerous libraries exist for machine learning, signal processing and
statistics and can be leveraged to avoid re-inventing the wheel - something
that happens all too often when using other languages that don’t have the same
wealth of high-quality, open-source projects.

bt is built atop [ffn](https://github.com/pmorissette/ffn) \- a financial
function library for Python. Check it out!

## A Quick Example

Here is a quick taste of bt:

`ipython3 import bt`

`ipython3 %matplotlib inline`

### A Simple Strategy Backtest

Let’s create a simple strategy. We will create a monthly rebalanced, long-only
strategy where we place equal weights on each asset in our universe of assets.

First, we will download some data. By default, `bt.get (alias for ffn.get)`
downloads the Adjusted Close from Yahoo! Finance. We will download some data
starting on January 1, 2010 for the purposes of this demo.

```ipython3

# fetch some data

data = bt.get('spy,agg', start='2010-01-01') print(data.head()) ```

`default spy agg Date 2010-01-04 89.225410 74.942825 2010-01-05 89.461586
75.283791 2010-01-06 89.524574 75.240227 2010-01-07 89.902473 75.153221
2010-01-08 90.201691 75.196724`

Once we have our data, we will create our strategy. The `Strategy` object
contains the strategy logic by combining various `Algos`.

```ipython3

# create the strategy

s = bt.Strategy('s1', [bt.algos.RunMonthly(), bt.algos.SelectAll(),
bt.algos.WeighEqually(), bt.algos.Rebalance()]) ```

Finally, we will create a `Backtest`, which is the logical combination of a
strategy with a data set.

Once this is done, we can run the backtest and analyze the results.

```ipython3

# create a backtest and run it

test = bt.Backtest(s, data) res = bt.run(test) ```

Now we can analyze the results of our backtest. The `Result` object is a thin
wrapper around
[ffn.GroupStats](http://pmorissette.github.io/ffn/ffn.html#ffn.core.GroupStats)
that adds some helper methods.

```ipython3

# first let's see an equity curve

res.plot(); ```

![image](_static/intro_9_0.png) ```ipython3

# ok and what about some stats?

res.display() ```

```default Stat s1

* * *

Start 2010-01-03 End 2022-07-01 Risk-free rate 0.00%

Total Return 150.73% Daily Sharpe 0.90 Daily Sortino 1.35 CAGR 7.64% Max
Drawdown -18.42% Calmar Ratio 0.41

MTD 0.18% 3m -10.33% 6m -14.84% YTD -14.84% 1Y -10.15% 3Y (ann.) 5.12% 5Y
(ann.) 6.44% 10Y (ann.) 7.36% Since Incep. (ann.) 7.64%

Daily Sharpe 0.90 Daily Sortino 1.35 Daily Mean (ann.) 7.74% Daily Vol (ann.)
8.62% Daily Skew -0.98 Daily Kurt 16.56 Best Day 4.77% Worst Day -6.63%

Monthly Sharpe 1.06 Monthly Sortino 1.91 Monthly Mean (ann.) 7.81% Monthly Vol
(ann.) 7.36% Monthly Skew -0.39 Monthly Kurt 1.59 Best Month 7.57% Worst Month
-6.44%

Yearly Sharpe 0.81 Yearly Sortino 1.75 Yearly Mean 7.48% Yearly Vol 9.17%
Yearly Skew -1.34 Yearly Kurt 2.28 Best Year 19.64% Worst Year -14.84%

Avg. Drawdown -0.84% Avg. Drawdown Days 13.23 Avg. Up Month 1.70% Avg. Down
Month -1.80% Win Year % 83.33% Win 12m % 93.57% ```

```ipython3

# ok and how does the return distribution look like?

res.plot_histogram() ```

![image](_static/intro_11_0.png) ```ipython3

# and just to make sure everything went along as planned, let's plot the
security weights over time

res.plot_security_weights() ```

![image](_static/intro_12_0.png)

### Modifying a Strategy

Now what if we ran this strategy weekly and also used some risk parity style
approach by using weights that are proportional to the inverse of each asset’s
volatility? Well, all we have to do is plug in some different algos. See
below:

```ipython3

# create our new strategy

s2 = bt.Strategy('s2', [bt.algos.RunWeekly(), bt.algos.SelectAll(),
bt.algos.WeighInvVol(), bt.algos.Rebalance()])

# now let's test it with the same data set. We will also compare it with our
first backtest.

test2 = bt.Backtest(s2, data)

# we include test here to see the results side-by-side

res2 = bt.run(test, test2)

res2.plot(); ```

![image](_static/intro_14_0.png) `ipython3 res2.display()`

```default Stat s1 s2

* * *

Start 2010-01-03 2010-01-03 End 2022-07-01 2022-07-01 Risk-free rate 0.00%
0.00%

Total Return 150.73% 69.58% Daily Sharpe 0.90 0.96 Daily Sortino 1.35 1.41
CAGR 7.64% 4.32% Max Drawdown -18.42% -14.62% Calmar Ratio 0.41 0.30

MTD 0.18% 0.38% 3m -10.33% -6.88% 6m -14.84% -12.00% YTD -14.84% -12.00% 1Y
-10.15% -10.03% 3Y (ann.) 5.12% 1.84% 5Y (ann.) 6.44% 3.35% 10Y (ann.) 7.36%
3.76% Since Incep. (ann.) 7.64% 4.32%

Daily Sharpe 0.90 0.96 Daily Sortino 1.35 1.41 Daily Mean (ann.) 7.74% 4.33%
Daily Vol (ann.) 8.62% 4.50% Daily Skew -0.98 -2.21 Daily Kurt 16.56 46.12
Best Day 4.77% 2.84% Worst Day -6.63% -4.66%

Monthly Sharpe 1.06 1.13 Monthly Sortino 1.91 1.87 Monthly Mean (ann.) 7.81%
4.40% Monthly Vol (ann.) 7.36% 3.89% Monthly Skew -0.39 -1.06 Monthly Kurt
1.59 3.92 Best Month 7.57% 4.05% Worst Month -6.44% -5.04%

Yearly Sharpe 0.81 0.65 Yearly Sortino 1.75 1.19 Yearly Mean 7.48% 4.13%
Yearly Vol 9.17% 6.31% Yearly Skew -1.34 -1.48 Yearly Kurt 2.28 3.37 Best Year
19.64% 11.71% Worst Year -14.84% -12.00%

Avg. Drawdown -0.84% -0.48% Avg. Drawdown Days 13.23 13.68 Avg. Up Month 1.70%
0.90% Avg. Down Month -1.80% -0.93% Win Year % 83.33% 83.33% Win 12m % 93.57%
91.43% ```

As you can see, the strategy logic is easy to understand and more importantly,
easy to modify. The idea of using simple, composable Algos to create
strategies is one of the core building blocks of bt.

## Features

  * **Tree Structure** : [The tree structure](tree.md) facilitates the construction and composition of complex algorithmic trading strategies that are modular and re-usable. Furthermore, each tree `Node` has its own `price index` that can be used by Algos to determine a Node’s allocation.
  * **Algorithm Stacks** : `Algos` and `AlgoStacks` are another core feature that facilitate the creation of modular and re-usable strategy logic. Due to their modularity, these logic blocks are also easier to test - an important step in building robust financial solutions.
  * **Transaction Cost Modeling** : Through the use of a commission function and instrument-specific, time-varying bid/offer spreads passed to the `Backtest`.
  * **Fixed Income** : Strategies can include coupon-paying instruments such as bonds, unfunded instruments such as swaps, holding costs, and the option for notional weighting. These are extensions of [the tree structure](tree.md).
  * **Charting and Reporting** : bt also provides many useful charting functions that help visualize backtest results. We also plan to add more charts, tables and report formats in the future, such as automatically generated PDF reports.
  * **Detailed Statistics** : Furthermore, bt calculates a bunch of stats relating to a backtest and offers a quick way to compare these various statistics across many different backtests via `Results'` display methods.

## Roadmap

Future development efforts will focus on:

  * **Speed** : Due to the flexible nature of bt, a trade-off had to be made between usability and performance. Usability will always be the priority, but we do wish to enhance the performance as much as possible.
  * **Algos** : We will also be developing more algorithms as time goes on. We also encourage anyone to contribute their own algos as well.
  * **Charting and Reporting** : This is another area we wish to constantly improve on as reporting is an important aspect of the job. Charting and reporting also facilitate finding bugs in strategy logic.

  * [Installation Guide](install.md)

  * [Installing bt](install.md#installing-bt)
  * [Recommended Setup](install.md#recommended-setup)
  * [All About Algos](algos.md)
  * [Overview](algos.md#overview)
  * [Data Passing](algos.md#data-passing)
  * [Implementation](algos.md#implementation)
  * [Best Practices](algos.md#best-practices)
  * [The Tree Structure](tree.md)
  * [Overview](tree.md#overview)
  * [Types](tree.md#types)
  * [Examples](examples.md)
  * [SMA Strategy](examples.md#sma-strategy)
  * [SMA Crossover Strategy](examples.md#sma-crossover-strategy)
  * [Exploring the Tree Structure](examples.md#exploring-the-tree-structure)
  * [Buy and Hold Strategy](examples.md#buy-and-hold-strategy)
  * [Trend Example 1](examples.md#trend-example-1)
  * [Trend Example 2](examples.md#trend-example-2)
  * [Strategy Combination](examples.md#strategy-combination)
  * [Equally Weighted Risk Contributions Portfolio](examples.md#equally-weighted-risk-contributions-portfolio)
  * [Predicted Tracking Error Rebalance Portfolio](examples.md#predicted-tracking-error-rebalance-portfolio)
  * [Fixed Income Examples](examples.md#fixed-income-examples)
  * [API](bt.md)
  * [`bt` Package](bt.md#id1)
  * [`algos` Module](bt.md#module-bt.algos)
  * [`backtest` Module](bt.md#module-bt.backtest)
  * [`core` Module](bt.md#module-bt.core)

* * *

## Buy_and_hold

# Buy and Hold Strategy

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

import ffn import bt

%matplotlib inline ```

## Create Fake Index Data

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

![image](_static/Buy_and_hold_3_0.png)

## Build Strategy

```ipython3

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

```ipython3

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

`ipython3 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  
  
`ipython3 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  
  
`ipython3 res.plot_security_weights()`

![image](_static/Buy_and_hold_10_0.png)

Strategy value over time

```ipython3 performanceStats = res['static']

# performance stats is an ffn object

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

![image](_static/Buy_and_hold_12_0.png)

Strategy Outlays

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

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

![image](_static/Buy_and_hold_14_0.png)

You can get the change in number of shares purchased a

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

* * *

## ERC

# Equally Weighted Risk Contributions Portfolio

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

import ffn import bt

%matplotlib inline ```

## Create Fake Index Data

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

`default array([[1.58730159e-04, 9.92063492e-06], [9.92063492e-06,
9.92063492e-06]])`

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

![image](_static/ERC_4_0.png)

## Build and run ERC Strategy

You can read more about ERC here. <http://thierry-
roncalli.com/download/erc.pdf>

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

`ipython3 res_target.get_security_weights().plot();`

![image](_static/ERC_7_0.png) `ipython3 res_target.prices.plot();`

![image](_static/ERC_8_0.png) ```ipython3 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(); ```

![image](_static/ERC_9_0.png)

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

* * *

## Fixed_Income

# 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**
  3. Rolling series of government bonds
  4. Corporate bonds with spreads driven by a common factor
  5. **Example 1: Basic Strategies**
  6. Weigh all active corporate bond equally
  7. Add hedging of interest rates risk with the on-the-run government bond
  8. **Example 2: Nested Strategies**
  9. One strategy buys the top N bonds, by yield
  10. Another strategy sells the bottom N bonds, by yield
  11. Parent strategy gives 50% weight to each of the above
  12. Add hedges of remaining interest rates risk with the on-the-run government bond

## Setup

`ipython3 import bt import pandas as pd from pandas.tseries.frequencies import
to_offset import numpy as np np.random.seed(1234) %matplotlib inline`

```ipython3

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

```ipython3

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

```ipython3

# 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

```ipython3

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

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

```ipython3

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

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

```ipython3

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

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

```ipython3

# Extract Tear Sheet for base backtest

stats = out['BaseBacktest'] stats.display() ```

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

* * *

```

```ipython3

# Extract Tear Sheet for hedged backtest

stats = out['HedgedBacktest'] stats.display() ```

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

* * *

```

```ipython3

# Total PNL time series values

pd.DataFrame( {'base':base_test.strategy.values,
'hedged':hedge_test.strategy.values} ).plot(); ```

![image](_static/Fixed_Income_13_0.png) ```ipython3

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

![image](_static/Fixed_Income_14_0.png) ```ipython3

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

![image](_static/Fixed_Income_15_0.png)

## Example 2: Nested Strategies

```ipython3

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

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

```ipython3

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

![image](_static/Fixed_Income_18_0.png) ```ipython3

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

![image](_static/Fixed_Income_19_0.png) ```ipython3

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

![image](_static/Fixed_Income_20_0.png) ```ipython3

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

![image](_static/Fixed_Income_21_0.png) ```ipython3

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

## PTE

# Predicted Tracking Error Rebalance Portfolio

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

import ffn import bt

%matplotlib inline ```

## Create Fake Index Data

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

![image](_static/PTE_3_0.png)

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

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

`ipython3 res_target.get_security_weights().plot();`

![image](_static/PTE_6_0.png)

Now use the PTE rebalance algo to trigger a rebalance whenever predicted
tracking error is greater than 1%.

```ipython3

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

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

![image](_static/PTE_9_0.png) ```ipython3 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) ```

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

`ipython3 trans_df = trans_df.fillna(0)`

`ipython3 fig, ax = plt.subplots(nrows=1,ncols=1)
trans_df.cumsum().plot(ax=ax) ax.set_title('Cumulative sum of notional
traded') ax.plot();`

![image](_static/PTE_13_0.png)

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.

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

![image](_static/PTE_15_0.png)

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

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

![image](_static/PTE_17_0.png) ```ipython3 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 ```

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

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

![image](_static/PTE_19_0.png)

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

* * *

## Strategy_Combination

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

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

import ffn import bt

%matplotlib inline ```

## Create fake data

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

![image](_static/Strategy_Combination_3_0.png) ```ipython3 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)
    

```

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

`ipython3 res.prices.plot();`

![image](_static/Strategy_Combination_6_0.png) `ipython3
res.get_security_weights().plot();`

![image](_static/Strategy_Combination_7_0.png)

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.

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

```

`ipython3 fig, ax = plt.subplots(nrows=1,ncols=1) for i,r in
enumerate(results): r.plot(ax=ax)`

![image](_static/Strategy_Combination_10_0.png) ```ipython3 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) ```

`ipython3 res.plot();`

![image](_static/Strategy_Combination_12_0.png) `ipython3
res.get_security_weights().plot();`

![image](_static/Strategy_Combination_13_0.png)

* * *

## Target_Volatility

# Target Volatility

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

import ffn import bt

%matplotlib inline ```

## Create Fake Index Data

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

![image](_static/Target_Volatility_3_0.png)

## Build Strategy

```ipython3

# algo to fire on the beginning of every week and to run on the first date

runWeeklyAlgo = bt.algos.RunWeekly( run_on_first_date=True )

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

weighInvVolAlgo = bt.algos.WeighInvVol( lookback=pd.DateOffset(months=12),
lag=pd.DateOffset(days=1) )

# algo to set overall volatility of the portfolio to an annualized 10%

targetVolAlgo = bt.algos.TargetVol( 0.1, lookback=pd.DateOffset(months=12),
lag=pd.DateOffset(days=1), covar_method='standard', annualization_factor=252 )

# algo to rebalance the current weights to weights set in target.temp

rebalAlgo = bt.algos.Rebalance()

# a strategy that rebalances monthly to specified weights

strat = bt.Strategy('static', [ runWeeklyAlgo, selectTheseAlgo,
weighInvVolAlgo, targetVolAlgo, rebalAlgo ] ) ```

## Run Backtest

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

```ipython3

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

You can see the realized volatility below is close to the targeted 10%
volatility.

`ipython3 fig, ax = plt.subplots(nrows=1,ncols=1)
(res.prices.pct_change().rolling(window=12*20).std()*np.sqrt(252)).plot(ax =
ax) ax.set_title('Rolling Volatility') ax.plot();`

![image](_static/Target_Volatility_9_0.png)

Because we are using a 1/vol allocation bar, the less risky security, has a
much smaller weight.

`ipython3 fig, ax = plt.subplots(nrows=1,ncols=1)
res.get_security_weights().plot(ax = ax) ax.set_title('Weights') ax.plot();`

![image](_static/Target_Volatility_11_0.png)

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

```ipython3 weights = res.get_security_weights() rolling_cov =
pdf.loc[:,weights.columns].pct_change().rolling(window=12 _20).cov()_ 252

trc = pd.DataFrame( np.nan, index = weights.index, columns = weights.columns )
for dt in pdf.index: trc.loc[dt,:] =
weights.loc[dt,:].values*rolling_cov.loc[dt,:].values@weights.loc[dt,:].values/np.sqrt(weights.loc[dt,:].values@rolling_cov.loc[dt,:].values@weights.loc[dt,:].values)

fig, ax = plt.subplots(nrows=1,ncols=1) trc.plot(ax=ax) ax.set_title('% Total
Risk Contribution') ax.plot(); ```

![image](_static/Target_Volatility_13_0.png)

* * *

## Trend_1

# Trend Example 1

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

import ffn import bt

%matplotlib inline ```

## Create fake data

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

![image](_static/Trend_1_3_0.png)

## Create Trend signal over the last 12 months

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

![image](_static/Trend_1_5_0.png) ```ipython3

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

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

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

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

```ipython3

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

`default tsmom_invvol 0% [############################# ] 100% | ETA: 00:00:00tsmom_ew 0% [############################# ] 100% | ETA: 00:00:00`

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

![image](_static/Trend_1_12_0.png) `ipython3 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_2

# Trend Example 2

```ipython3 import numpy as np import pandas as pd

import bt import matplotlib.pyplot as plt

%matplotlib inline ```

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

![image](_static/Trend_2_2_0.png) ```ipython3 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() ```

```ipython3 s = bt.Strategy( 'example1', [ runMonthlyAlgo, signalAlgo,
weighFromSignalAlgo, rebalAlgo ] )

t = bt.Backtest(s, pdf, integer_positions=False, progress_bar=True) res =
bt.run(t) ```

`default example1 0% [############################# ] 100% | ETA: 00:00:00`

`ipython3 res.plot_security_weights();`

![image](_static/Trend_2_5_0.png) `ipython3 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

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

## algos

# Algorithms

## Overview

One of the core building blocks of bt is the `Algo` and the closely related
`AlgoStack`.

![algo stack](_static/stack.png)

### Algos

An Algo is essentially a function that returns True or False. It takes a
single argument that is the `Strategy` being tested. An Algo should ideally
only serve one specific purpose. This purpose can control execution flow, it
can control security selection, security allocation, etc. For example, you can
have an Algo that checks if the month has changed (such as
`bt.algos.RunMonthly`). If it has, this Algo return True, if not, False.

### Algo Stacks

An `AlgoStack` is a class that groups together many Algos and runs them one
after another as long as each Algo returns True. As soon as an Algo returns
False, the AlgoStack stops its execution and returns False (an AlgoStack is an
Algo after all). This allows us to combine different Algos together and
control the flow of execution with the Algo return value. Many AlgoStacks can
they themselves be included into another AlgoStack should the need arise.

By breaking down strategy logic into these small blocks of code, we achieve
testability and reusability - two appealing features when working on software
development.

## Data Passing

In order to pass data between different Algos, the Strategy has two
properties: **temp** and **perm**. They are both dictionaries and are used for
storing data generated by Algos. Temporary data is refreshed on each data
change whereas permanent data is not altered.

Algos usually **set** and/or **require** values in the temp or perm objects.
For example, the `bt.algos.WeighEqually` Algo sets the ‘weights’ key in temp,
and it requires the ‘selected’ key in temp.

For example, let’s take a simple select -> weight -> allocate logic chain. We
would break this strategy up into 3 Algos:

  * **selection** Which securities do I want to allocate capital to out of the entire universe of investable assets? See, i.e. `SelectAll`, `SelectThese`, `SelectWhere`, `SelectN`, etc
  * **weighting** How much weight should each of the selected securities have in the target portfolio? See, i.e. `WeighEqually`, `WeighRandomly` `WeighSpecified`, `WeighTarget`, `WeighInvVol`, `WeighMeanVar`, etc
  * **allocate** Close out positions that are no longer needed and allocate capital to those that were selected and given target weights. See, i.e. `Rebalance`

In this case, the selection Algo could set the ‘selected’ key in the
strategy’s temp dict, and the weighting Algo could read those values and in
turn set the ‘weights’ key in the temp dict. The allocation Algo would then
read the ‘weights’ and act accordingly.

To extend the simple select -> weight -> allocate logic chain to include an
additional risk/exposure calculation step, one would do this by implementing
specific Algos for this purpose. These could be used either before weighting
(for risk-based portfolio construction) or after (for reporting). See, e.g.
`UpdateRisk`.

#### NOTE

To preserve maximal flexibility, there are currently no checks to make sure
the AlgoStack is valid. Therefore, it is up to the user and creator of Algos
to make sure the requirements and side effects are well documented and
properly used (by the way, this may not be a great way to go about this
problem. If you have a better idea, please let me know!).

Developers should add a section in the docstring that outlines the “sets” and
the “requires”. See the doctrings of `bt.algos.WeighEqually` for an example.

## Implementation

In most cases, Algos must preserve some kind of state. In this case, it is
easier to implement them as classes and define the __call__ method, like
below:

```default class MyAlgo(bt.Algo):

    
    
    def __init__(self, arg1, arg2):
        self.arg1 = arg1
        self.arg2 = arg2
    
    def __call__(self, target):
        # my logic goes here
    
        # accessing/storing variables through target.temp['key']
    
        # remember to return a bool - True in most cases
        return True
    

```

Note that the attributes on the class should **not** be specific to any
particular target.

However, for Algos that do not need to preserve any state, you may simply
implement them as a basic function that takes one argument - the Strategy:

```default def MyAlgo2(target): # all the logic

    
    
    return True
    

```

## Best Practices

### Re-usability

Recall that Algos should be re-usable across different backtests (including
backtests on different underlying security universes or different time
ranges), and that a Backtest is the logical combination of the strategy and
the data. However, there are cases when the Algo needs to use some extra data
that **does** depend on the security universe or time range (i.e. a data frame
of signals that has been pre-computed).

The best way to handle this is to construct the Algo with the **name** of the
data, and to instantiate the backtest with this named data:

```default class MyAlgo(bt.Algo):

    
    
    def __init__(self, signal_name ):
        self.signal_name = signal_name
    
    def __call__(self, target):
        # my logic goes here
    
        # accessing data via target.get_data( self.signal_name )
    
        # remember to return a bool - True in most cases
        return True
    

# create the strategy

s = bt.Strategy('s1', [bt.algos.MyAlgo( 'my_signal' )])

# create a backtest and run it

test = bt.Backtest(s, data, additional_data={'my_signal':signal_df}) res =
bt.run(test)

# Run the same strategy on different data without changing MyAlgo

test = bt.Backtest(s, data2, additional_data={'my_signal':signal_df2}) res =
bt.run(test) ```

Note that some additional data keys are used by the framework itself to
support additional functionality (i.e. `bidoffer`, `coupon`, `cost_long` and
`cost_short`). These are documented in the `setup` functions of `Security` and
`CouponPayingSecurity`.

### Debugging

The easiest way to debug algos is by adding leveraging one of the existing
debug algos or by writing your own! Just insert them in the appropriate places
in your algo stack, and add breakpoints to examine the state of the passed
strategy.

>   * `Debug`
>   * `PrintTempData`
>   * `PrintInfo`
>   * `PrintRisk`
>

### Branching and Control Flow

While the Algo setup may seem overly simple (a list of functions which returns
either True or False), this is a powerful construct that allows for complex
branching and conditional structures. In particular, branching is achieved via
the `Or Algo`.

For example, the code below illustrates how printing of strategy performance
can occur on a different timeline from rebalancing the portfolio. Additional
conditions can be added by placing those algos at the head of the relevant
stack.

```python import bt

data = bt.get('spy,agg', start='2010-01-01')

# create two separate algo stacks and combine the branches

logging_stack = bt.AlgoStack( bt.algos.RunWeekly(),
bt.algos.PrintInfo('{name}:{now}. Value:{_value:0.0f}, Price:{_price:0.4f}') )
trading_stack = bt.AlgoStack( bt.algos.RunMonthly(), bt.algos.SelectAll(),
bt.algos.WeighEqually(), bt.algos.Rebalance() ) branch_stack = bt.AlgoStack( #
Upstream algos could go here... bt.algos.Or( [ logging_stack, trading_stack ]
) # Downstream algos could go here... )

s = bt.Strategy('strategy', branch_stack, ['spy', 'agg']) t = bt.Backtest(s,
data) r = bt.run(t) ```

* * *

## bt

# bt Package

## `bt` Package

## `algos` Module

## `backtest` Module

## `core` Module

* * *

## examples-nb

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

`ipython3 import bt`

`ipython3 %matplotlib inline`

```ipython3

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

```ipython3

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

![image](_static/examples-nb_6_0.png)

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.

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

`default signal = data > sma`

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

```ipython3

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

```ipython3

# what does the equity curve look like?

res.plot(); ```

![image](_static/examples-nb_12_0.png) ```ipython3

# and some performance stats

res.display() ```

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

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

```ipython3

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

`ipython3 res2.plot(freq='m');`

![image](_static/examples-nb_18_0.png) `ipython3 res2.display()`

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

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

```ipython3

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

```ipython3

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

![image](_static/examples-nb_26_0.png)

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.

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

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

`ipython3 res.plot();`

![image](_static/examples-nb_29_0.png)

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:

```ipython3

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

`ipython3 res.plot();`

![image](_static/examples-nb_32_0.png) `ipython3 res.plot_weights();`

![image](_static/examples-nb_33_0.png)

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:

```ipython3

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

`ipython3 res.plot();`

![image](_static/examples-nb_36_0.png) `ipython3 res.plot_weights();`

![image](_static/examples-nb_37_0.png)

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

* * *

## examples

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

`ipython3 import bt`

`ipython3 %matplotlib inline`

```ipython3

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

```ipython3

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

![image](_static/examples-nb_6_0.png)

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.

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

`default signal = data > sma`

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

```ipython3

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

```ipython3

# what does the equity curve look like?

res.plot(); ```

![image](_static/examples-nb_12_0.png) ```ipython3

# and some performance stats

res.display() ```

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

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

```ipython3

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

`ipython3 res2.plot(freq='m');`

![image](_static/examples-nb_18_0.png) `ipython3 res2.display()`

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

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

```ipython3

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

```ipython3

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

![image](_static/examples-nb_26_0.png)

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.

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

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

`ipython3 res.plot();`

![image](_static/examples-nb_29_0.png)

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:

```ipython3

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

`ipython3 res.plot();`

![image](_static/examples-nb_32_0.png) `ipython3 res.plot_weights();`

![image](_static/examples-nb_33_0.png)

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:

```ipython3

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

`ipython3 res.plot();`

![image](_static/examples-nb_36_0.png) `ipython3 res.plot_weights();`

![image](_static/examples-nb_37_0.png)

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

## Buy and Hold Strategy

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

import ffn import bt

%matplotlib inline ```

### Create Fake Index Data

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

![image](_static/Buy_and_hold_3_0.png)

### Build Strategy

```ipython3

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

```ipython3

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

`ipython3 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  
  
`ipython3 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  
  
`ipython3 res.plot_security_weights()`

![image](_static/Buy_and_hold_10_0.png)

Strategy value over time

```ipython3 performanceStats = res['static']

# performance stats is an ffn object

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

![image](_static/Buy_and_hold_12_0.png)

Strategy Outlays

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

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

![image](_static/Buy_and_hold_14_0.png)

You can get the change in number of shares purchased a

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

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

import ffn import bt

%matplotlib inline ```

### Create fake data

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

![image](_static/Trend_1_3_0.png)

### Create Trend signal over the last 12 months

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

![image](_static/Trend_1_5_0.png) ```ipython3

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

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

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

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

```ipython3

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

`default tsmom_invvol 0% [############################# ] 100% | ETA: 00:00:00tsmom_ew 0% [############################# ] 100% | ETA: 00:00:00`

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

![image](_static/Trend_1_12_0.png) `ipython3 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

```ipython3 import numpy as np import pandas as pd

import bt import matplotlib.pyplot as plt

%matplotlib inline ```

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

![image](_static/Trend_2_2_0.png) ```ipython3 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() ```

```ipython3 s = bt.Strategy( 'example1', [ runMonthlyAlgo, signalAlgo,
weighFromSignalAlgo, rebalAlgo ] )

t = bt.Backtest(s, pdf, integer_positions=False, progress_bar=True) res =
bt.run(t) ```

`default example1 0% [############################# ] 100% | ETA: 00:00:00`

`ipython3 res.plot_security_weights();`

![image](_static/Trend_2_5_0.png) `ipython3 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

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

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

import ffn import bt

%matplotlib inline ```

### Create fake data

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

![image](_static/Strategy_Combination_3_0.png) ```ipython3 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)
    

```

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

`ipython3 res.prices.plot();`

![image](_static/Strategy_Combination_6_0.png) `ipython3
res.get_security_weights().plot();`

![image](_static/Strategy_Combination_7_0.png)

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.

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

```

`ipython3 fig, ax = plt.subplots(nrows=1,ncols=1) for i,r in
enumerate(results): r.plot(ax=ax)`

![image](_static/Strategy_Combination_10_0.png) ```ipython3 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) ```

`ipython3 res.plot();`

![image](_static/Strategy_Combination_12_0.png) `ipython3
res.get_security_weights().plot();`

![image](_static/Strategy_Combination_13_0.png)

## Equally Weighted Risk Contributions Portfolio

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

import ffn import bt

%matplotlib inline ```

### Create Fake Index Data

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

`default array([[1.58730159e-04, 9.92063492e-06], [9.92063492e-06,
9.92063492e-06]])`

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

![image](_static/ERC_4_0.png)

### Build and run ERC Strategy

You can read more about ERC here. <http://thierry-
roncalli.com/download/erc.pdf>

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

`ipython3 res_target.get_security_weights().plot();`

![image](_static/ERC_7_0.png) `ipython3 res_target.prices.plot();`

![image](_static/ERC_8_0.png) ```ipython3 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(); ```

![image](_static/ERC_9_0.png)

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

## Predicted Tracking Error Rebalance Portfolio

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

import ffn import bt

%matplotlib inline ```

### Create Fake Index Data

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

![image](_static/PTE_3_0.png)

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

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

`ipython3 res_target.get_security_weights().plot();`

![image](_static/PTE_6_0.png)

Now use the PTE rebalance algo to trigger a rebalance whenever predicted
tracking error is greater than 1%.

```ipython3

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

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

![image](_static/PTE_9_0.png) ```ipython3 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) ```

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

`ipython3 trans_df = trans_df.fillna(0)`

`ipython3 fig, ax = plt.subplots(nrows=1,ncols=1)
trans_df.cumsum().plot(ax=ax) ax.set_title('Cumulative sum of notional
traded') ax.plot();`

![image](_static/PTE_13_0.png)

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.

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

![image](_static/PTE_15_0.png)

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

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

![image](_static/PTE_17_0.png) ```ipython3 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 ```

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

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

![image](_static/PTE_19_0.png)

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**
  3. Rolling series of government bonds
  4. Corporate bonds with spreads driven by a common factor
  5. **Example 1: Basic Strategies**
  6. Weigh all active corporate bond equally
  7. Add hedging of interest rates risk with the on-the-run government bond
  8. **Example 2: Nested Strategies**
  9. One strategy buys the top N bonds, by yield
  10. Another strategy sells the bottom N bonds, by yield
  11. Parent strategy gives 50% weight to each of the above
  12. Add hedges of remaining interest rates risk with the on-the-run government bond

### Setup

`ipython3 import bt import pandas as pd from pandas.tseries.frequencies import
to_offset import numpy as np np.random.seed(1234) %matplotlib inline`

```ipython3

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

```ipython3

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

```ipython3

# 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

```ipython3

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

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

```ipython3

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

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

```ipython3

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

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

```ipython3

# Extract Tear Sheet for base backtest

stats = out['BaseBacktest'] stats.display() ```

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

* * *

```

```ipython3

# Extract Tear Sheet for hedged backtest

stats = out['HedgedBacktest'] stats.display() ```

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

* * *

```

```ipython3

# Total PNL time series values

pd.DataFrame( {'base':base_test.strategy.values,
'hedged':hedge_test.strategy.values} ).plot(); ```

![image](_static/Fixed_Income_13_0.png) ```ipython3

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

![image](_static/Fixed_Income_14_0.png) ```ipython3

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

![image](_static/Fixed_Income_15_0.png)

### Example 2: Nested Strategies

```ipython3

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

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

```ipython3

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

![image](_static/Fixed_Income_18_0.png) ```ipython3

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

![image](_static/Fixed_Income_19_0.png) ```ipython3

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

![image](_static/Fixed_Income_20_0.png) ```ipython3

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

![image](_static/Fixed_Income_21_0.png) ```ipython3

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

## install

# Getting Started

## Installing bt

The easiest way to install `bt` is from the [Python Package
Index](https://pypi.python.org/pypi/bt/) using `pip`:

`bash $ pip install bt`

Since `bt` has many dependencies, we strongly recommend installing the
[Anaconda Scientific Python
Distribution](https://store.continuum.io/cshop/anaconda/), especially on
Windows. This distribution comes with many of the required packages pre-
installed, including pip. Once Anaconda is installed, the above command should
complete the installation.

`bt` is also available on [Conda Forge](https://conda-forge.org/), and
installable via:

`bash $ conda install bt --channel conda-forge`

`bt` is compatible with Python >=3.7.

## Recommended Setup

We believe the best environment to develop with bt is the [IPython
Notebook](http://ipython.org/notebook.html). From their homepage, the IPython
Notebook is:

> “[…] a web-based interactive computational environment where you can combine
> code execution, text, mathematics, plots and rich media into a single
> document […]”

This environment allows you to plot your charts in-line and also allows you to
easily add surrounding text with Markdown. You can easily create Notebooks
that you can share with colleagues and you can also save them as PDFs. If you
are not yet convinced, head over to their website.

* * *

## intro

`ipython3 import bt`

`ipython3 %matplotlib inline`

# A Simple Strategy Backtest

Let’s create a simple strategy. We will create a monthly rebalanced, long-only
strategy where we place equal weights on each asset in our universe of assets.

First, we will download some data. By default, `bt.get (alias for ffn.get)`
downloads the Adjusted Close from Yahoo! Finance. We will download some data
starting on January 1, 2010 for the purposes of this demo.

```ipython3

# fetch some data

data = bt.get('spy,agg', start='2010-01-01') print(data.head()) ```

`default spy agg Date 2010-01-04 89.225410 74.942825 2010-01-05 89.461586
75.283791 2010-01-06 89.524574 75.240227 2010-01-07 89.902473 75.153221
2010-01-08 90.201691 75.196724`

Once we have our data, we will create our strategy. The `Strategy` object
contains the strategy logic by combining various `Algos`.

```ipython3

# create the strategy

s = bt.Strategy('s1', [bt.algos.RunMonthly(), bt.algos.SelectAll(),
bt.algos.WeighEqually(), bt.algos.Rebalance()]) ```

Finally, we will create a `Backtest`, which is the logical combination of a
strategy with a data set.

Once this is done, we can run the backtest and analyze the results.

```ipython3

# create a backtest and run it

test = bt.Backtest(s, data) res = bt.run(test) ```

Now we can analyze the results of our backtest. The `Result` object is a thin
wrapper around
[ffn.GroupStats](http://pmorissette.github.io/ffn/ffn.html#ffn.core.GroupStats)
that adds some helper methods.

```ipython3

# first let's see an equity curve

res.plot(); ```

![image](_static/intro_9_0.png) ```ipython3

# ok and what about some stats?

res.display() ```

```default Stat s1

* * *

Start 2010-01-03 End 2022-07-01 Risk-free rate 0.00%

Total Return 150.73% Daily Sharpe 0.90 Daily Sortino 1.35 CAGR 7.64% Max
Drawdown -18.42% Calmar Ratio 0.41

MTD 0.18% 3m -10.33% 6m -14.84% YTD -14.84% 1Y -10.15% 3Y (ann.) 5.12% 5Y
(ann.) 6.44% 10Y (ann.) 7.36% Since Incep. (ann.) 7.64%

Daily Sharpe 0.90 Daily Sortino 1.35 Daily Mean (ann.) 7.74% Daily Vol (ann.)
8.62% Daily Skew -0.98 Daily Kurt 16.56 Best Day 4.77% Worst Day -6.63%

Monthly Sharpe 1.06 Monthly Sortino 1.91 Monthly Mean (ann.) 7.81% Monthly Vol
(ann.) 7.36% Monthly Skew -0.39 Monthly Kurt 1.59 Best Month 7.57% Worst Month
-6.44%

Yearly Sharpe 0.81 Yearly Sortino 1.75 Yearly Mean 7.48% Yearly Vol 9.17%
Yearly Skew -1.34 Yearly Kurt 2.28 Best Year 19.64% Worst Year -14.84%

Avg. Drawdown -0.84% Avg. Drawdown Days 13.23 Avg. Up Month 1.70% Avg. Down
Month -1.80% Win Year % 83.33% Win 12m % 93.57% ```

```ipython3

# ok and how does the return distribution look like?

res.plot_histogram() ```

![image](_static/intro_11_0.png) ```ipython3

# and just to make sure everything went along as planned, let's plot the
security weights over time

res.plot_security_weights() ```

![image](_static/intro_12_0.png)

# Modifying a Strategy

Now what if we ran this strategy weekly and also used some risk parity style
approach by using weights that are proportional to the inverse of each asset’s
volatility? Well, all we have to do is plug in some different algos. See
below:

```ipython3

# create our new strategy

s2 = bt.Strategy('s2', [bt.algos.RunWeekly(), bt.algos.SelectAll(),
bt.algos.WeighInvVol(), bt.algos.Rebalance()])

# now let's test it with the same data set. We will also compare it with our
first backtest.

test2 = bt.Backtest(s2, data)

# we include test here to see the results side-by-side

res2 = bt.run(test, test2)

res2.plot(); ```

![image](_static/intro_14_0.png) `ipython3 res2.display()`

```default Stat s1 s2

* * *

Start 2010-01-03 2010-01-03 End 2022-07-01 2022-07-01 Risk-free rate 0.00%
0.00%

Total Return 150.73% 69.58% Daily Sharpe 0.90 0.96 Daily Sortino 1.35 1.41
CAGR 7.64% 4.32% Max Drawdown -18.42% -14.62% Calmar Ratio 0.41 0.30

MTD 0.18% 0.38% 3m -10.33% -6.88% 6m -14.84% -12.00% YTD -14.84% -12.00% 1Y
-10.15% -10.03% 3Y (ann.) 5.12% 1.84% 5Y (ann.) 6.44% 3.35% 10Y (ann.) 7.36%
3.76% Since Incep. (ann.) 7.64% 4.32%

Daily Sharpe 0.90 0.96 Daily Sortino 1.35 1.41 Daily Mean (ann.) 7.74% 4.33%
Daily Vol (ann.) 8.62% 4.50% Daily Skew -0.98 -2.21 Daily Kurt 16.56 46.12
Best Day 4.77% 2.84% Worst Day -6.63% -4.66%

Monthly Sharpe 1.06 1.13 Monthly Sortino 1.91 1.87 Monthly Mean (ann.) 7.81%
4.40% Monthly Vol (ann.) 7.36% 3.89% Monthly Skew -0.39 -1.06 Monthly Kurt
1.59 3.92 Best Month 7.57% 4.05% Worst Month -6.44% -5.04%

Yearly Sharpe 0.81 0.65 Yearly Sortino 1.75 1.19 Yearly Mean 7.48% 4.13%
Yearly Vol 9.17% 6.31% Yearly Skew -1.34 -1.48 Yearly Kurt 2.28 3.37 Best Year
19.64% 11.71% Worst Year -14.84% -12.00%

Avg. Drawdown -0.84% -0.48% Avg. Drawdown Days 13.23 13.68 Avg. Up Month 1.70%
0.90% Avg. Down Month -1.80% -0.93% Win Year % 83.33% 83.33% Win 12m % 93.57%
91.43% ```

* * *

## modules

# bt

  * [bt Package](bt.md)
  * [`bt` Package](bt.md#id1)
  * [`algos` Module](bt.md#module-bt.algos)
  * [`backtest` Module](bt.md#module-bt.backtest)
  * [`core` Module](bt.md#module-bt.core)

* * *

## tree

# The Tree Structure

## Overview

In addition to the concept of `Algos` and `AlgoStacks`, a tree structure lies
at the heart of the framework. It allows you to mix and match securities and
strategies in order to express your sophisticated trading ideas. Here is a
very simple diagram to help explain this concept:

![simple tree structure](_static/tree1.png)

This diagram represents the strategy we tested in the [overview
example](index.md). A simple `strategy` with two children that happen to be
`securities`. However, children nodes don’t have to be securities. They can
also be strategies. This concept is very powerful as it allows you to combine
strategies together and allocate capital dynamically between different
strategies as time progresses using sophisticated allocation logic. This is
similar to what hedge funds do - they have a portfolio of strategies and
dynamically allocate capital according to a set of rules.

For example, say we didn’t mind having a passive bond allocation (AGG in the
above graph), but we wanted to swap out the equity portion (SPY) for something
a little more sophisticated. In this case, we will swap out the SPY node for
another strategy. This strategy could be a momentum strategy that attempts to
pick the best performing ETF every month (to keep it simple, let’s say it
picks either the SPY or the EEM based on total return over the past 3 months).

Here is the updated graph:

![advanced tree structure](_static/tree2.png)

This approach allows you to build complex systems even though all of the
building blocks may be relatively simple. Hopefully you can see how powerful
this can be when designing and testing quantitative strategies.

Oh and here’s the code for the second example - not much more complex:

```python import bt

# create the momentum strategy - we will specify the children (3rd argument)

# to limit the universe the strategy can choose from

mom_s = bt.Strategy('mom_s', [bt.algos.RunMonthly(), bt.algos.SelectAll(),
bt.algos.SelectMomentum(1), bt.algos.WeighEqually(), bt.algos.Rebalance()],
['spy', 'eem'])

# create the parent strategy - this is the top-most node in the tree

# Once again, we are also specifying the children. In this case, one of the

# children is a Security and the other is a Strategy.

parent = bt.Strategy('parent', [bt.algos.RunMonthly(), bt.algos.SelectAll(),
bt.algos.WeighEqually(), bt.algos.Rebalance()], [mom_s, 'agg'])

# create the backtest and run it

t = bt.Backtest(parent, data) r = bt.run(t) ```

For even more sophisticated strategies, sub-strategies can be dynamically
created by constructing them with the appropriate parent argument. The code
below is equivalent to the example above, but knowledge of the sub-strategy is
not needed at construction time of the parent:

```python

# create the parent strategy first - this is the top-most node in the tree

# To start, there is only one child, which is a Security

parent = bt.Strategy('parent', [bt.algos.RunMonthly(), bt.algos.SelectAll(),
bt.algos.WeighEqually(), bt.algos.Rebalance()], ['agg'])

# Create the momentum strategy dynamically - we will specify the children

# (3rd argument) to limit the universe the strategy can choose from

mom_s = bt.Strategy('mom_s', [bt.algos.RunMonthly(), bt.algos.SelectAll(),
bt.algos.SelectMomentum(1), bt.algos.WeighEqually(), bt.algos.Rebalance()],
['spy', 'eem'], parent = parent)

# create the backtest and run it

t = bt.Backtest(parent, data) r = bt.run(t) ```

While this seems like a trivial example, it enables algos to create sub-
strategies on-the-fly (based on market conditions/triggers) and register them
to the target. Each of these sub-strategies will have its own algos and
performance measurement.

## Types

The base class for nodes in the tree is `Node`, and these can be either of
type `StrategyBase` or `SecurityBase`.

Each node offers an interface to the **current values** of many quantities of
interest (price, value, weight, etc), which is useful for building Algos.
Furthermore, they also offer an interface to the **history** of these
quantities, which is useful for building **path-dependent** algos as well as
for drilling into the strategy behavior _after_ the backtest has run.

For more information, see the APIs for `Node`, `SecurityBase` and
`StrategyBase`.

There are two main sub-types of `StrategyBase`: : * `Strategy`: Market-value
weighted strategy based on `Algos`. * `FixedIncomeStrategy`: Notional weighted
strategy based on `Algos`.

There are also two main sub-types of `SecurityBase`: : * `Security`: Standard
security. If used within a `FixedIncomeStrategy`, its notional weight is equal
to market value. i.e. common stock. * `CouponPayingSecurity`: A security that
pays regular or irregular cashflows, and can have (asymmetric) holding costs.
i.e. a corporate bond with funding and repo costs, or an unfunded swap.

When using `FixedIncomeStrategy`, there are additional security types that are
helpful due to the different treatment of their notional weight in the
portfolio. These have no effect in a standard `Strategy`.

>   * `FixedIncomeSecurity`: A `Security` for which position (rather than
> market value) will be used as the notional weight, i.e. a zero-coupon bond.
>   * `HedgeSecurity`: A `Security` for which the notional weight is zero,
> i.e. an ETF hedge in a bond strategy.
>   * `CouponPayingHedgeSecurity`: A `CouponPayingSecurity` for which the
> notional weight is zero, i.e. a rates swap hedge in a CDS strategy.
>

As in the examples, a list of strings can be passed to the strategy
constructors, which will be automatically converted to instances of `Security`
when needed. For more fine-grained control over which security types are used
(or over other arguments like the `multiplier`), explicitly construct the
security nodes yourself before passing them to the strategy.

# Notebook

* * *

## Buy and Hold Strategy

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

import ffn import bt

%matplotlib inline ```

### Create Fake Index Data

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

```python

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

```python

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

`python res.stats`

`python res.prices.head()`

`python res.plot_security_weights()`

Strategy value over time

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

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

You can get the change in number of shares purchased a

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

```python

```

# Notebook

* * *

## Equally Weighted Risk Contributions Portfolio

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

import ffn import bt

%matplotlib inline ```

### Create Fake Index Data

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

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

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

`python res_target.get_security_weights().plot();`

`python res_target.prices.plot();`

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

```python

```

# Notebook

* * *

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

`python import bt import pandas as pd from pandas.tseries.frequencies import
to_offset import numpy as np np.random.seed(1234) %matplotlib inline`

```python

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

```python

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

```python

# 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

```python

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

```python

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

### Example 1: Basic Strategies

```python

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

```python

# Extract Tear Sheet for base backtest

stats = out['BaseBacktest'] stats.display() ```

```python

# Extract Tear Sheet for hedged backtest

stats = out['HedgedBacktest'] stats.display() ```

```python

# Total PNL time series values

pd.DataFrame( {'base':base_test.strategy.values,
'hedged':hedge_test.strategy.values} ).plot(); ```

```python

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

```python

# 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

```python

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

```python

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

```python

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

```python

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

```python

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

```python

# Show transactions

out.get_transactions('HedgedBacktest').head(20) ```

```python

```

# Notebook

* * *

## Predicted Tracking Error Rebalance Portfolio

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

import ffn import bt

%matplotlib inline ```

### Create Fake Index Data

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

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

`python res_target.get_security_weights().plot();`

Now use the PTE rebalance algo to trigger a rebalance whenever predicted
tracking error is greater than 1%.

```python

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

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

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

```

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

```

`python trans_df = trans_df.fillna(0)`

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

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

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

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

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

```python

```

# Notebook

* * *

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

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

import ffn import bt

%matplotlib inline ```

### Create fake data

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

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

```

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

`python res.prices.plot();`

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

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

```

`python fig, ax = plt.subplots(nrows=1,ncols=1) for i,r in enumerate(results):
r.plot(ax=ax)`

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

`python res.plot();`

`python res.get_security_weights().plot();`

```python

```

# Notebook

* * *

## Target Volatility

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

import ffn import bt

%matplotlib inline ```

### Create Fake Index Data

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

```python

# algo to fire on the beginning of every week and to run on the first date

runWeeklyAlgo = bt.algos.RunWeekly( run_on_first_date=True )

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

weighInvVolAlgo = bt.algos.WeighInvVol( lookback=pd.DateOffset(months=12),
lag=pd.DateOffset(days=1) )

# algo to set overall volatility of the portfolio to an annualized 10%

targetVolAlgo = bt.algos.TargetVol( 0.1, lookback=pd.DateOffset(months=12),
lag=pd.DateOffset(days=1), covar_method='standard', annualization_factor=252 )

# algo to rebalance the current weights to weights set in target.temp

rebalAlgo = bt.algos.Rebalance()

# a strategy that rebalances monthly to specified weights

strat = bt.Strategy('static', [ runWeeklyAlgo, selectTheseAlgo,
weighInvVolAlgo, targetVolAlgo, rebalAlgo ] ) ```

### Run Backtest

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

```python

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

You can see the realized volatility below is close to the targeted 10%
volatility.

`python fig, ax = plt.subplots(nrows=1,ncols=1)
(res.prices.pct_change().rolling(window=12*20).std()*np.sqrt(252)).plot(ax =
ax) ax.set_title('Rolling Volatility') ax.plot();`

Because we are using a 1/vol allocation bar, the less risky security, has a
much smaller weight.

`python fig, ax = plt.subplots(nrows=1,ncols=1)
res.get_security_weights().plot(ax = ax) ax.set_title('Weights') 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 asset's contribute roughly similar
amounts of volatility.

```python weights = res.get_security_weights() rolling_cov =
pdf.loc[:,weights.columns].pct_change().rolling(window=12 _20).cov()_ 252

trc = pd.DataFrame( np.nan, index = weights.index, columns = weights.columns )
for dt in pdf.index: trc.loc[dt,:] =
weights.loc[dt,:].values*rolling_cov.loc[dt,:].values@weights.loc[dt,:].values/np.sqrt(weights.loc[dt,:].values@rolling_cov.loc[dt,:].values@weights.loc[dt,:].values)

fig, ax = plt.subplots(nrows=1,ncols=1) trc.plot(ax=ax) ax.set_title('% Total
Risk Contribution') ax.plot(); ```

```python

```

# Notebook

* * *

## Trend Example 1

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

import ffn import bt

%matplotlib inline ```

### Create fake data

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

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

```python

# sma with 1 day lag

sma.tail() ```

```python

# sma with 0 day lag

pdf.rolling(window=num_days_per_month*12,center=False).median().tail() ```

```python

# target weights

trend = sma.copy() trend[pdf > sma] = True trend[pdf <= sma] = False
trend[sma.isnull()] = False trend.tail() ```

Compare EW and 1/vol

Both strategies rebalance daily using trend with 1 day lag and weights limited
to 40%.

```python

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

```python

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

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

```

`python tsmom_ew_res.stats`

```python

```

# Notebook

* * *

## Trend Example 2

```python import numpy as np import pandas as pd

import bt import matplotlib.pyplot as plt

%matplotlib inline ```

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

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

```python s = bt.Strategy( 'example1', [ runMonthlyAlgo, signalAlgo,
weighFromSignalAlgo, rebalAlgo ] )

t = bt.Backtest(s, pdf, integer_positions=False, progress_bar=True) res =
bt.run(t)

```

`python res.plot_security_weights();`

`python t.positions`

`python res.prices.tail()`

`python res.stats`

```python

```

# Notebook

* * *

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

`python jupyter={"outputs_hidden": false} import bt`

`python jupyter={"outputs_hidden": false} %matplotlib inline`

```python jupyter={"outputs_hidden": false}

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

```python jupyter={"outputs_hidden": false}

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

```python jupyter={"outputs_hidden": false} 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. 
    :class:`Here is the code <bt.algos.SelectWhere>`.
    

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.

```python jupyter={"outputs_hidden": false}

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

```python jupyter={"outputs_hidden": false}

# what does the equity curve look like?

res.plot(); ```

```python jupyter={"outputs_hidden": false}

# and some performance stats

res.display() ```

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:

```python jupyter={"outputs_hidden": false} 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.

```python jupyter={"outputs_hidden": false}

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

`python jupyter={"outputs_hidden": false} res2.plot(freq='m');`

`python jupyter={"outputs_hidden": false} res2.display()`

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

```python jupyter={"outputs_hidden": false} 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.

```python jupyter={"outputs_hidden": false}

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

```python jupyter={"outputs_hidden": false}

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

```python jupyter={"outputs_hidden": false} ma_cross = bt.Strategy('ma_cross',
[WeighTarget(tw), bt.algos.Rebalance()])

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

`python jupyter={"outputs_hidden": false} 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:

```python jupyter={"outputs_hidden": false}

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

`python jupyter={"outputs_hidden": false} res.plot();`

`python jupyter={"outputs_hidden": false} 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:

```python jupyter={"outputs_hidden": false}

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

`python jupyter={"outputs_hidden": false} res.plot();`

`python jupyter={"outputs_hidden": false} res.plot_weights();`

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

# Notebook

* * *

`python import bt`

`python %matplotlib inline`

A Simple Strategy Backtest ~~~~~~~~~~~~~~~~~~~~~~~~~~

Let's create a simple strategy. We will create a monthly rebalanced, long-only
strategy where we place equal weights on each asset in our universe of assets.

First, we will download some data. By default, :func:`bt.get (alias for
ffn.get) <ffn.data.get>` downloads the Adjusted Close from Yahoo! Finance. We
will download some data starting on January 1, 2010 for the purposes of this
demo.

```python jupyter={"outputs_hidden": false}

# fetch some data

data = bt.get('spy,agg', start='2010-01-01') print(data.head()) ```

Once we have our data, we will create our strategy. The :class:`Strategy
<bt.core.Strategy>` object contains the strategy logic by combining various
:class:`Algos <bt.core.Algo>`.

```python jupyter={"outputs_hidden": false}

# create the strategy

s = bt.Strategy('s1', [bt.algos.RunMonthly(), bt.algos.SelectAll(),
bt.algos.WeighEqually(), bt.algos.Rebalance()]) ```

Finally, we will create a :class:`Backtest <bt.backtest.Backtest>`, which is
the logical combination of a strategy with a data set.

Once this is done, we can run the backtest and analyze the results.

```python jupyter={"outputs_hidden": false}

# create a backtest and run it

test = bt.Backtest(s, data) res = bt.run(test) ```

Now we can analyze the results of our backtest. The :class:`Result
<bt.backtest.Result>` object is a thin wrapper around `ffn.GroupStats
<http://pmorissette.github.io/ffn/ffn.html#ffn.core.GroupStats>`__ that adds
some helper methods.

```python jupyter={"outputs_hidden": false}

# first let's see an equity curve

res.plot(); ```

```python jupyter={"outputs_hidden": false}

# ok and what about some stats?

res.display() ```

```python jupyter={"outputs_hidden": false}

# ok and how does the return distribution look like?

res.plot_histogram() ```

```python jupyter={"outputs_hidden": false}

# and just to make sure everything went along as planned, let's plot the
security weights over time

res.plot_security_weights() ```

Modifying a Strategy ~~~~~~~~~~~~~~~~~~~~

Now what if we ran this strategy weekly and also used some risk parity style
approach by using weights that are proportional to the inverse of each asset's
volatility? Well, all we have to do is plug in some different algos. See
below:

```python jupyter={"outputs_hidden": false}

# create our new strategy

s2 = bt.Strategy('s2', [bt.algos.RunWeekly(), bt.algos.SelectAll(),
bt.algos.WeighInvVol(), bt.algos.Rebalance()])

# now let's test it with the same data set. We will also compare it with our
first backtest.

test2 = bt.Backtest(s2, data)

# we include test here to see the results side-by-side

res2 = bt.run(test, test2)

res2.plot(); ```

`python jupyter={"outputs_hidden": false} res2.display()`

