diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2426aad8..b50170db 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,12 +19,12 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [3.8, 3.9, '3.10', 3.11] + python-version: [3.9, 3.12] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} cache: 'pip' @@ -42,7 +42,7 @@ jobs: run: make test - name: Coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 - name: Package and check run: make dist diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5ec99659..c909173b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -13,12 +13,12 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [3.8, 3.9, '3.10', 3.11] + python-version: [3.8, 3.9, '3.10', 3.11, 3.12] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} diff --git a/.github/workflows/regression.yml b/.github/workflows/regression.yml new file mode 100644 index 00000000..3da47f8d --- /dev/null +++ b/.github/workflows/regression.yml @@ -0,0 +1,48 @@ +name: Regression and Version Tests + +on: + push: + branches: + - master + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + build: + runs-on: ${{ matrix.os }} + environment: dev + + strategy: + matrix: + python-version: [3.9] + os: [ubuntu-latest] + pandas_version: + - '>=2.2' + - '<2' + numpy_version: + - '<2' + - '>=2' + exclude: + - numpy_version: '>=2' + pandas_version: '<2' + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: 'setup.py' + + - name: Install dependencies + run: | + make develop + python -m pip install -U wheel twine setuptools "numpy${{ matrix.numpy_version }}" "pandas${{ matrix.pandas_version}}" + + - name: Test + run: make test diff --git a/bt/__init__.py b/bt/__init__.py index 3d3e5933..238068b6 100644 --- a/bt/__init__.py +++ b/bt/__init__.py @@ -10,4 +10,4 @@ import ffn from ffn import utils, data, get, merge -__version__ = "1.0.0" +__version__ = "1.0.1" diff --git a/bt/algos.py b/bt/algos.py index 596bcd7a..8b6e7875 100644 --- a/bt/algos.py +++ b/bt/algos.py @@ -2260,9 +2260,9 @@ def _setup_risk(self, target, set_history): def _setup_measure(self, target, set_history): """Setup a risk measure within the risk attributes on the node in question""" - target.risk[self.measure] = np.NaN + target.risk[self.measure] = np.nan if set_history: - target.risks[self.measure] = np.NaN + target.risks[self.measure] = np.nan def _set_risk_recursive(self, target, depth, unit_risk_frame): set_history = depth < self.history diff --git a/bt/backtest.py b/bt/backtest.py index 2a768416..68481031 100644 --- a/bt/backtest.py +++ b/bt/backtest.py @@ -9,6 +9,7 @@ import numpy as np from matplotlib import pyplot as plt import pyprind +from tqdm import tqdm def run(*backtests): @@ -24,7 +25,7 @@ def run(*backtests): """ # run each backtest - for bkt in backtests: + for bkt in tqdm(backtests): bkt.run() return Result(*backtests) @@ -66,7 +67,7 @@ def benchmark_random(backtest, random_strategy, nsim=100): data = backtest.data.dropna() # create and run random backtests - for i in range(nsim): + for i in tqdm(range(nsim)): random_strategy.name = "random_%s" % i rbt = bt.Backtest(random_strategy, data) rbt.run() diff --git a/bt/core.py b/bt/core.py index a995f73e..901d54c9 100644 --- a/bt/core.py +++ b/bt/core.py @@ -1249,7 +1249,7 @@ def positions(self): TimeSeries of positions. """ # if accessing and stale - update first - if self._needupdate: + if self._needupdate or self.now != self.parent.now: self.update(self.root.now) if self.root.stale: self.root.update(self.root.now, None) @@ -1540,7 +1540,7 @@ def allocate(self, amount, update=True): i = 0 last_q = q last_amount_short = full_outlay - amount - while not np.isclose(full_outlay, amount, rtol=0.0) and q != 0: + while not np.isclose(full_outlay, amount, rtol=TOL) and q != 0: dq_wout_considering_tx_costs = (full_outlay - amount) / (self._price * self.multiplier) q = q - dq_wout_considering_tx_costs diff --git a/docs/source/Fixed_Income.rst b/docs/source/Fixed_Income.rst index 7801d23c..6ef11de3 100644 --- a/docs/source/Fixed_Income.rst +++ b/docs/source/Fixed_Income.rst @@ -79,10 +79,10 @@ Setup .. code:: 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 ): + 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) + 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) .. code:: ipython3 @@ -97,10 +97,10 @@ Market Data Generation .. code:: ipython3 # Government Bonds: Create synthetic data for a single series of rolling government bonds - + # Reference Data - roll_freq = 'Q' - maturity = 10 + 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 @@ -108,27 +108,27 @@ Market Data Generation 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 ], + 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_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') @@ -136,10 +136,10 @@ Market Data Generation 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_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) @@ -155,7 +155,7 @@ Market Data Generation .. code:: 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 @@ -166,7 +166,7 @@ Market Data Generation 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 @@ -179,7 +179,7 @@ Market Data Generation 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') @@ -187,18 +187,18 @@ Market Data Generation 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_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) @@ -217,85 +217,85 @@ Example 1: Basic Strategies .. code:: 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' ), + 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.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), + bt.algos.UpdateRisk( 'beta', history=1), ) hedging_stack = bt.AlgoStack( # Specify how frequently to hedge risk - bt.algos.RunMonthly(), + 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' ), + 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), + 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.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(), + bt.algos.RunMonthly(), # Select instruments for rebalancing. Start with everything - bt.algos.SelectAll(), + bt.algos.SelectAll(), # Prevent matured/rolled instruments from coming back into the mix - bt.algos.SelectActive(), + 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' ), + 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., + 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"}), + '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', + base_test = bt.Backtest( base_strategy, data, 'BaseBacktest', initial_capital = 0, additional_data = additional_data ) - hedge_test = bt.Backtest( hedged_strategy, data, 'HedgedBacktest', + hedge_test = bt.Backtest( hedged_strategy, data, 'HedgedBacktest', initial_capital = 0, additional_data = additional_data) out = bt.run( base_test, hedge_test ) @@ -418,12 +418,12 @@ Example 1: Basic Strategies 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 ------ ------- --------- -------- @@ -434,12 +434,12 @@ Example 1: Basic Strategies 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% @@ -465,12 +465,12 @@ Example 1: Basic Strategies 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 ------ ------- --------- -------- @@ -481,12 +481,12 @@ Example 1: Basic Strategies 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% @@ -512,7 +512,7 @@ Example 1: Basic Strategies .. code:: ipython3 # Total risk time series values - pd.DataFrame( {'base_pvbp':base_test.strategy.risks['pvbp'], + pd.DataFrame( {'base_pvbp':base_test.strategy.risks['pvbp'], 'hedged_pvbp':hedge_test.strategy.risks['pvbp'], 'beta':hedge_test.strategy.risks['beta']} ).dropna().plot(); @@ -527,7 +527,7 @@ Example 1: Basic Strategies .. code:: ipython3 # Total bid/offer paid (same for both strategies) - pd.DataFrame( {'base_pvbp':base_test.strategy.bidoffers_paid, + pd.DataFrame( {'base_pvbp':base_test.strategy.bidoffers_paid, 'hedged_pvbp':hedge_test.strategy.bidoffers_paid }).cumsum().dropna().plot(); @@ -544,27 +544,27 @@ Example 2: Nested Strategies .. code:: 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(), + # Specify how frequenty to rebalance + bt.algos.RunMonthly(), # Select instruments for rebalancing. Start with everything - bt.algos.SelectAll(), + bt.algos.SelectAll(), # Prevent matured/rolled instruments from coming back into the mix - bt.algos.SelectActive(), + bt.algos.SelectActive(), # Set the stat to be used for selection bt.algos.SetStat( 'corp_yield' ), # Select the top N yielding bonds @@ -573,21 +573,21 @@ Example 2: Nested Strategies 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' ), + bt.algos.SetNotional( 'notional_value' ), # Rebalance the portfolio - bt.algos.Rebalance(), + 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.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), + bt.algos.UpdateRisk( 'beta', history=2), ) hedging_stack = bt.AlgoStack( # Close any matured hedge positions (including hedges) @@ -595,38 +595,38 @@ Example 2: Nested Strategies # Roll government bond positions into the On The Run bt.algos.RollPositionsAfterDates( 'govt_roll_map' ), # Specify how frequently to hedge risk - bt.algos.RunMonthly(), + 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' ), + 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), + 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.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( + trading_stack =bt.core.AlgoStack( # Specify how frequently to rebalance the portfolio of sub-strategies - bt.algos.RunOnce(), + 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 + 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"}), @@ -636,10 +636,10 @@ Example 2: Nested Strategies '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', + base_test = bt.Backtest( base_strategy, data, 'BaseBacktest', initial_capital = 0, additional_data = additional_data) - hedge_test = bt.Backtest( hedged_strategy, data, 'HedgedBacktest', + hedge_test = bt.Backtest( hedged_strategy, data, 'HedgedBacktest', initial_capital = 0, additional_data = additional_data) out = bt.run( base_test, hedge_test ) @@ -749,7 +749,7 @@ Example 2: Nested Strategies .. code:: ipython3 # Total PNL time series values - pd.DataFrame( {'base':base_test.strategy.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} @@ -766,10 +766,10 @@ Example 2: Nested Strategies .. code:: ipython3 # Total pvbp time series values - pd.DataFrame( {'base':base_test.strategy.risks['pvbp'], + 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']} + 'bottom':base_test.strategy['BottomStrategy'].risks['pvbp']} ).dropna().plot(); @@ -783,10 +783,10 @@ Example 2: Nested Strategies .. code:: ipython3 # Total beta time series values - pd.DataFrame( {'base':base_test.strategy.risks['beta'], + 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']} + 'bottom':base_test.strategy['BottomStrategy'].risks['beta']} ).dropna().plot(); @@ -800,7 +800,7 @@ Example 2: Nested Strategies .. code:: ipython3 # "Price" time series values - pd.DataFrame( {'base':base_test.strategy.prices, + pd.DataFrame( {'base':base_test.strategy.prices, 'hedged':hedge_test.strategy.prices, 'top':base_test.strategy['TopStrategy'].prices, 'bottom':base_test.strategy['BottomStrategy'].prices} @@ -829,11 +829,11 @@ Example 2: Nested Strategies .dataframe tbody tr th:only-of-type { vertical-align: middle; } - + .dataframe tbody tr th { vertical-align: top; } - + .dataframe thead th { text-align: right; } diff --git a/examples/fixed_income.ipynb b/examples/fixed_income.ipynb index 832950f0..4dc9ab9b 100644 --- a/examples/fixed_income.ipynb +++ b/examples/fixed_income.ipynb @@ -88,7 +88,7 @@ "# Utility function to set data frame values to nan before the security has been issued or after it has matured\n", "def censor( data, ref_data ): \n", " for bond in data:\n", - " data.loc[ (data.index > ref_data['mat_date'][bond]) | (data.index < ref_data['issue_date'][bond]), bond] = np.NaN\n", + " data.loc[ (data.index > ref_data['mat_date'][bond]) | (data.index < ref_data['issue_date'][bond]), bond] = np.nan\n", " return data.ffill(limit=1,axis=0) # Because bonds might mature during a gap in the index (i.e. on the weekend) " ] }, diff --git a/setup.py b/setup.py index a91c00d0..170fcbdc 100644 --- a/setup.py +++ b/setup.py @@ -25,14 +25,18 @@ def local_file(filename): setup( name="bt", - version="1.0.0", + version="1.0.1", author="Philippe Morissette", author_email="morissette.philippe@gmail.com", description="A flexible backtesting framework for Python", keywords="python finance quant backtesting strategies algotrading algorithmic trading", url="https://github.com/pmorissette/bt", license="MIT", - install_requires=["ffn>=1.0.0", "pyprind>=2.11"], + install_requires=[ + "ffn>=1.0.0", + "pyprind>=2.11", + "tqdm>=4" + ], extras_require={ "dev": [ "cython>=0.25", diff --git a/tests/test_core.py b/tests/test_core.py index 0a6940b9..827b6dfb 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3786,7 +3786,7 @@ def test_fi_strategy_precision(): assert s.value == pytest.approx(0, 14) assert not is_zero(s.value) # Notional value not quite equal to N * 0.1 - assert s.notional_value == sum(0.1 for _ in range(N)) + assert s.notional_value == pytest.approx(sum(0.1 for _ in range(N))) assert s.notional_value != N * 0.1 assert s.price == 100.0