In [1]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

import ffn
import bt

%matplotlib inline

Create Fake Index Data


In [2]:
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()


Out[2]:
<matplotlib.axes._subplots.AxesSubplot at 0x195c31eb160>

Build Strategy


In [3]:
# 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.


In [4]:
# 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)


static
0% [############################# ] 100% | ETA: 00:00:00
C:\ProgramData\Anaconda3\lib\site-packages\ffn\core.py:2054: RuntimeWarning: invalid value encountered in minimum
  negative_returns = np.minimum(returns, 0.)

In [5]:
res.stats


Out[5]:
static
start 2017-01-01 00:00:00
end 2017-12-29 00:00:00
rf 0
total_return 0.229372
cagr 0.231653
max_drawdown -0.0692566
calmar 3.34485
mtd -0.000905607
three_month 0.00597538
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.80455
daily_sortino 3.30615
daily_mean 0.206762
daily_vol 0.114578
daily_skew 0.0122082
daily_kurt -0.0445598
best_day 0.0204024
worst_day -0.0201002
monthly_sharpe 2.80644
monthly_sortino 15.3525
monthly_mean 0.257101
monthly_vol 0.0916111
monthly_skew 0.753881
monthly_kurt 0.456278
best_month 0.0736565
worst_month -0.0145918
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.0160522
avg_drawdown_days 12.6957
avg_up_month 0.0324599
avg_down_month -0.00800092
win_year_perc NaN
twelve_month_win_perc NaN

In [6]:
res.prices.head()


Out[6]:
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

In [7]:
res.plot_security_weights()


Strategy value over time


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


Out[26]:
<matplotlib.axes._subplots.AxesSubplot at 0x195c6693710>

Strategy Outlays

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


In [28]:
res.backtest_list[0].strategy.outlays.plot()


Out[28]:
<matplotlib.axes._subplots.AxesSubplot at 0x195c66d6b38>

You can get the change in number of shares purchased a


In [34]:
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


Out[34]:
bar foo
2017-01-01 0.000000 0.000000
2017-01-02 3998.068018 5879.285683
2017-01-03 3998.068018 5879.285683
2017-01-04 3998.068018 5879.285683
2017-01-05 3998.068018 5879.285683
2017-01-06 3998.068018 5879.285683
2017-01-09 3998.068018 5879.285683
2017-01-10 3998.068018 5879.285683
2017-01-11 3998.068018 5879.285683
2017-01-12 3998.068018 5879.285683
2017-01-13 3998.068018 5879.285683
2017-01-16 3998.068018 5879.285683
2017-01-17 3998.068018 5879.285683
2017-01-18 3998.068018 5879.285683
2017-01-19 3998.068018 5879.285683
2017-01-20 3998.068018 5879.285683
2017-01-23 3998.068018 5879.285683
2017-01-24 3998.068018 5879.285683
2017-01-25 3998.068018 5879.285683
2017-01-26 3998.068018 5879.285683
2017-01-27 3998.068018 5879.285683
2017-01-30 3998.068018 5879.285683
2017-01-31 3998.068018 5879.285683
2017-02-01 3890.785319 5989.384683
2017-02-02 3890.785319 5989.384683
2017-02-03 3890.785319 5989.384683
2017-02-06 3890.785319 5989.384683
2017-02-07 3890.785319 5989.384683
2017-02-08 3890.785319 5989.384683
2017-02-09 3890.785319 5989.384683
... ... ...
2017-11-20 4529.959868 5433.422211
2017-11-21 4529.959868 5433.422211
2017-11-22 4529.959868 5433.422211
2017-11-23 4529.959868 5433.422211
2017-11-24 4529.959868 5433.422211
2017-11-27 4529.959868 5433.422211
2017-11-28 4529.959868 5433.422211
2017-11-29 4529.959868 5433.422211
2017-11-30 4529.959868 5433.422211
2017-12-01 4673.239436 5324.589093
2017-12-04 4673.239436 5324.589093
2017-12-05 4673.239436 5324.589093
2017-12-06 4673.239436 5324.589093
2017-12-07 4673.239436 5324.589093
2017-12-08 4673.239436 5324.589093
2017-12-11 4673.239436 5324.589093
2017-12-12 4673.239436 5324.589093
2017-12-13 4673.239436 5324.589093
2017-12-14 4673.239436 5324.589093
2017-12-15 4673.239436 5324.589093
2017-12-18 4673.239436 5324.589093
2017-12-19 4673.239436 5324.589093
2017-12-20 4673.239436 5324.589093
2017-12-21 4673.239436 5324.589093
2017-12-22 4673.239436 5324.589093
2017-12-25 4673.239436 5324.589093
2017-12-26 4673.239436 5324.589093
2017-12-27 4673.239436 5324.589093
2017-12-28 4673.239436 5324.589093
2017-12-29 4673.239436 5324.589093

261 rows × 2 columns