From 47d322b8d0221ba57ed2ee4af6a028f4bd7a65d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nathan=20Ramos=2C=20CFA=C2=AE?= <86393277+nathanramoscfa@users.noreply.github.com> Date: Wed, 13 Sep 2023 14:24:47 -0500 Subject: [PATCH 01/11] Update backtest.py Add progress bars to backtest.py --- bt/backtest.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bt/backtest.py b/bt/backtest.py index e9e919cd..36686be0 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() From b4febd1a0dd0f47c1d846cf8a50f21b1a75b6ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nathan=20Ramos=2C=20CFA=C2=AE?= <86393277+nathanramoscfa@users.noreply.github.com> Date: Wed, 13 Sep 2023 17:22:11 -0500 Subject: [PATCH 02/11] Update setup.py --- setup.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f991fb62..70cf3b49 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,11 @@ def local_file(filename): keywords="python finance quant backtesting strategies", url="https://github.com/pmorissette/bt", license="MIT", - install_requires=["ffn>=0.3.7", "pyprind>=2.11"], + install_requires=[ + "ffn>=0.3.7", + "pyprind>=2.11", + "tqdm==4.65.0" # <-- Added this line + ], extras_require={ "dev": [ "black>=20.8b1", From a1a21b0126f38400e2f5c75a802cb2063f7973c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nathan=20Ramos=2C=20CFA=C2=AE?= <86393277+nathanramoscfa@users.noreply.github.com> Date: Thu, 14 Sep 2023 09:51:51 -0500 Subject: [PATCH 03/11] Update setup.py Modified the pinned version of tqdm as requested. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 70cf3b49..481a1d5d 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ def local_file(filename): install_requires=[ "ffn>=0.3.7", "pyprind>=2.11", - "tqdm==4.65.0" # <-- Added this line + "tqdm>=4.65.0" ], extras_require={ "dev": [ From e949e063367c0d944d02e70c5a9c38008122925c Mon Sep 17 00:00:00 2001 From: 0xEljh Date: Wed, 29 Nov 2023 22:28:41 +0800 Subject: [PATCH 04/11] add relative tolerance to allocate outlay check --- bt/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bt/core.py b/bt/core.py index a995f73e..3a560c43 100644 --- a/bt/core.py +++ b/bt/core.py @@ -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 From efb3e882b48673de83add7d78d0eda002e435df5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 14:40:35 +0000 Subject: [PATCH 05/11] Bump actions/setup-python from 4 to 5 Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- .github/workflows/deploy.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2426aad8..e3d9818a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: 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' diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5ec99659..b41520d1 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -18,7 +18,7 @@ jobs: 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 }} From 2ba403033a967d0640494b31f5e6fa1b8a6f32fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 14:38:34 +0000 Subject: [PATCH 06/11] Bump codecov/codecov-action from 3 to 4 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v3...v4) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e3d9818a..57e59478 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 From aeb0f1c526c84ef54199c3240ea5a88f0fe355fc Mon Sep 17 00:00:00 2001 From: zer0e <63488636+zero-element@users.noreply.github.com> Date: Thu, 4 Apr 2024 05:21:32 +0000 Subject: [PATCH 07/11] fixed bug which positions access did not trigger updating --- bt/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bt/core.py b/bt/core.py index 3a560c43..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) From 03bc2043824c2ec5592ecb50af4ac9b027afd093 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Sat, 22 Jun 2024 21:42:07 -0500 Subject: [PATCH 08/11] add numpy/pandas version tests --- .github/workflows/regression.yml | 48 ++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/workflows/regression.yml 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 From 6b8144be217bf5aa13c3a213dfc4218aa793885e Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Sat, 22 Jun 2024 21:45:35 -0500 Subject: [PATCH 09/11] Fix numpy 2 issues --- bt/algos.py | 4 +- docs/source/Fixed_Income.rst | 170 +++++++++++++++++------------------ examples/fixed_income.ipynb | 2 +- 3 files changed, 88 insertions(+), 88 deletions(-) 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/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) " ] }, From bb6975017737f20400f6393268368c1b5cbfcafe Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Sat, 22 Jun 2024 21:53:35 -0500 Subject: [PATCH 10/11] Move regular ci to 3.9,3.12, add 3.12 to deploy --- .github/workflows/build.yml | 2 +- .github/workflows/deploy.yml | 2 +- tests/test_core.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 57e59478..b50170db 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ 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 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b41520d1..c909173b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -13,7 +13,7 @@ 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 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 From 0a1969c0b3aca8e4e65605bca9d7d2ebd5c55812 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Sat, 22 Jun 2024 22:05:36 -0500 Subject: [PATCH 11/11] Bump to 1.0.1 --- bt/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/setup.py b/setup.py index 6d27a4c4..170fcbdc 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ 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",