From 39715351fe560c7a2eb0efd81f62bed2a8c48f91 Mon Sep 17 00:00:00 2001 From: benoit Date: Mon, 21 Jun 2021 21:10:18 +0200 Subject: [PATCH 1/3] ENH: Add the possibility to close trades at end of bt.run (#273 & #343) --- CHANGELOG.md | 5 +++-- backtesting/backtesting.py | 25 +++++++++++++++---------- backtesting/test/_test.py | 37 +++++++++++++++++++++++++++++++------ 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27c31c6a..8f06a42b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ What's New These were the major changes contributing to each release: -### 0.x.x +### 0.3.2 +* new param on run to manage close, or not the trades at end (#273) ### 0.3.1 @@ -114,4 +115,4 @@ These were the major changes contributing to each release: ### 0.1.0 (2019-01-15) -* Initial release \ No newline at end of file +* Initial release diff --git a/backtesting/backtesting.py b/backtesting/backtesting.py index edb7be01..db131468 100644 --- a/backtesting/backtesting.py +++ b/backtesting/backtesting.py @@ -986,7 +986,8 @@ def __init__(self, margin: float = 1., trade_on_close=False, hedging=False, - exclusive_orders=False + exclusive_orders=False, + close_all_at_end=False ): """ Initialize a backtest. Requires data and a strategy to test. @@ -1031,6 +1032,9 @@ def __init__(self, trade/position, making at most a single trade (long or short) in effect at each time. + If `close_all_at_end` is `False`, the trade will not be close at end, + and will not apear in _Stats. + [FIFO]: https://www.investopedia.com/terms/n/nfa-compliance-rule-2-43b.asp """ @@ -1080,7 +1084,7 @@ def __init__(self, warnings.warn('Data index is not datetime. Assuming simple periods, ' 'but `pd.DateTimeIndex` is advised.', stacklevel=2) - + self._close_all_at_end = bool(close_all_at_end) self._data: pd.DataFrame = data self._broker = partial( _Broker, cash=cash, commission=commission, margin=margin, @@ -1166,14 +1170,15 @@ def run(self, **kwargs) -> pd.Series: # Next tick, a moment before bar close strategy.next() else: - # Close any remaining open trades so they produce some stats - for trade in broker.trades: - trade.close() - - # Re-run broker one last time to handle orders placed in the last strategy - # iteration. Use the same OHLC values as in the last broker iteration. - if start < len(self._data): - try_(broker.next, exception=_OutOfMoneyError) + if self._close_all_at_end is True: + # Close any remaining open trades so they produce some stats + for trade in broker.trades: + trade.close() + + # Re-run broker one last time to handle orders placed in the last strategy + # iteration. Use the same OHLC values as in the last broker iteration. + if start < len(self._data): + try_(broker.next, exception=_OutOfMoneyError) # Set data back to full length # for future `indicator._opts['data'].index` calls to work diff --git a/backtesting/test/_test.py b/backtesting/test/_test.py index 85ecea6a..61ef517f 100644 --- a/backtesting/test/_test.py +++ b/backtesting/test/_test.py @@ -215,7 +215,7 @@ def next(self, FIVE_DAYS=pd.Timedelta('3 days')): bt = Backtest(GOOG, Assertive) with self.assertWarns(UserWarning): stats = bt.run() - self.assertEqual(stats['# Trades'], 145) + self.assertEqual(stats['# Trades'], 144) def test_broker_params(self): bt = Backtest(GOOG.iloc[:100], SmaCross, @@ -248,7 +248,7 @@ def test_compute_drawdown(self): np.testing.assert_array_equal(peaks, pd.Series([7, 4], index=[3, 5]).reindex(dd.index)) def test_compute_stats(self): - stats = Backtest(GOOG, SmaCross).run() + stats = Backtest(GOOG, SmaCross, close_all_at_end=True).run() expected = pd.Series({ # NOTE: These values are also used on the website! '# Trades': 66, @@ -397,7 +397,32 @@ def next(self): elif len(self.data) == len(SHORT_DATA): self.position.close() - self.assertFalse(Backtest(SHORT_DATA, S).run()._trades.empty) + self.assertTrue(Backtest(SHORT_DATA, S).run()._trades.empty) + + def test_dont_close_orders_from_last_strategy_iteration(self): + class S(Strategy): + def init(self): pass + + def next(self): + if not self.position: + self.buy() + elif len(self.data) == len(SHORT_DATA): + self.position.close() + self.assertEqual(len( + Backtest(SHORT_DATA, S, close_all_at_end=False).run()._strategy.closed_trades), 0) + self.assertEqual(len( + Backtest(SHORT_DATA, S, close_all_at_end=False).run()._strategy.trades), 1) + + def test_dont_close_orders_trades_from_last_strategy_iteration(self): + class S(Strategy): + def init(self): pass + + def next(self): + if not self.position: + self.buy() + + self.assertEqual(len( + Backtest(SHORT_DATA, S, close_all_at_end=False).run()._strategy.trades), 1) def test_check_adjusted_price_when_placing_order(self): class S(Strategy): @@ -499,7 +524,7 @@ def test_autoclose_trades_on_finish(self): def coroutine(self): yield self.buy() - stats = self._Backtest(coroutine).run() + stats = self._Backtest(coroutine, close_all_at_end=True).run() self.assertEqual(len(stats._trades), 1) @@ -844,7 +869,7 @@ def init(self): self.data.Close < sma) stats = Backtest(GOOG, S).run() - self.assertIn(stats['# Trades'], (1181, 1182)) # varies on different archs? + self.assertIn(stats['# Trades'], (1179, 1180)) # varies on different archs? def test_TrailingStrategy(self): class S(TrailingStrategy): @@ -860,7 +885,7 @@ def next(self): self.buy() stats = Backtest(GOOG, S).run() - self.assertEqual(stats['# Trades'], 57) + self.assertEqual(stats['# Trades'], 56) class TestUtil(TestCase): From 34504550929fb7f489312333ad1cc87ac93f8ef4 Mon Sep 17 00:00:00 2001 From: Kernc Date: Thu, 30 Jan 2025 06:30:10 +0100 Subject: [PATCH 2/3] Change parameter name, simplify tests --- CHANGELOG.md | 3 +-- backtesting/backtesting.py | 19 +++++++++++-------- backtesting/test/_test.py | 32 ++++---------------------------- 3 files changed, 16 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6935f20a..57f30bec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,6 @@ These were the major changes contributing to each release: ### 0.x.x - ### 0.5.0 (2025-01-21) @@ -157,4 +156,4 @@ These were the major changes contributing to each release: ### 0.1.0 (2019-01-15) -* Initial release +* Initial release \ No newline at end of file diff --git a/backtesting/backtesting.py b/backtesting/backtesting.py index 71f93bea..7039bf20 100644 --- a/backtesting/backtesting.py +++ b/backtesting/backtesting.py @@ -1089,7 +1089,7 @@ def __init__(self, trade_on_close=False, hedging=False, exclusive_orders=False, - close_all_at_end=False + finalize_trades=False, ): """ Initialize a backtest. Requires data and a strategy to test. @@ -1156,11 +1156,13 @@ def __init__(self, trade/position, making at most a single trade (long or short) in effect at each time. - If `close_all_at_end` is `False`, the trade will not be close at end, - and will not apear in _Stats. + If `finalize_trades` is `True`, the trades that are still + [active and ongoing] at the end of the backtest will be closed on + the last bar and will contribute to the computed backtest statistics. [FIFO]: https://www.investopedia.com/terms/n/nfa-compliance-rule-2-43b.asp - """ + [active and ongoing]: https://kernc.github.io/backtesting.py/doc/backtesting/backtesting.html#backtesting.backtesting.Strategy.trades + """ # noqa: E501 if not (isinstance(strategy, type) and issubclass(strategy, Strategy)): raise TypeError('`strategy` must be a Strategy sub-type') @@ -1213,7 +1215,7 @@ def __init__(self, warnings.warn('Data index is not datetime. Assuming simple periods, ' 'but `pd.DateTimeIndex` is advised.', stacklevel=2) - self._close_all_at_end = bool(close_all_at_end) + self._data: pd.DataFrame = data self._broker = partial( _Broker, cash=cash, spread=spread, commission=commission, margin=margin, @@ -1222,6 +1224,7 @@ def __init__(self, ) self._strategy = strategy self._results: Optional[pd.Series] = None + self._finalize_trades = bool(finalize_trades) def run(self, **kwargs) -> pd.Series: """ @@ -1308,13 +1311,13 @@ def run(self, **kwargs) -> pd.Series: # Next tick, a moment before bar close strategy.next() else: - if self._close_all_at_end is True: + if self._finalize_trades is True: # Close any remaining open trades so they produce some stats for trade in broker.trades: trade.close() - # Re-run broker one last time to handle orders placed in the last strategy - # iteration. Use the same OHLC values as in the last broker iteration. + # HACK: Re-run broker one last time to handle close orders placed in the last + # strategy iteration. Use the same OHLC values as in the last broker iteration. if start < len(self._data): try_(broker.next, exception=_OutOfMoneyError) diff --git a/backtesting/test/_test.py b/backtesting/test/_test.py index 94af42bf..3c9d0f08 100644 --- a/backtesting/test/_test.py +++ b/backtesting/test/_test.py @@ -282,7 +282,7 @@ def test_compute_drawdown(self): np.testing.assert_array_equal(peaks, pd.Series([7, 4], index=[3, 5]).reindex(dd.index)) def test_compute_stats(self): - stats = Backtest(GOOG, SmaCross, close_all_at_end=True).run() + stats = Backtest(GOOG, SmaCross, finalize_trades=True).run() expected = pd.Series({ # NOTE: These values are also used on the website! '# Trades': 66, @@ -438,32 +438,8 @@ def next(self): elif len(self.data) == len(SHORT_DATA): self.position.close() - self.assertTrue(Backtest(SHORT_DATA, S).run()._trades.empty) - - def test_dont_close_orders_from_last_strategy_iteration(self): - class S(Strategy): - def init(self): pass - - def next(self): - if not self.position: - self.buy() - elif len(self.data) == len(SHORT_DATA): - self.position.close() - self.assertEqual(len( - Backtest(SHORT_DATA, S, close_all_at_end=False).run()._strategy.closed_trades), 0) - self.assertEqual(len( - Backtest(SHORT_DATA, S, close_all_at_end=False).run()._strategy.trades), 1) - - def test_dont_close_orders_trades_from_last_strategy_iteration(self): - class S(Strategy): - def init(self): pass - - def next(self): - if not self.position: - self.buy() - - self.assertEqual(len( - Backtest(SHORT_DATA, S, close_all_at_end=False).run()._strategy.trades), 1) + self.assertTrue(Backtest(SHORT_DATA, S, finalize_trades=False).run()._trades.empty) + self.assertFalse(Backtest(SHORT_DATA, S, finalize_trades=True).run()._trades.empty) def test_check_adjusted_price_when_placing_order(self): class S(Strategy): @@ -565,7 +541,7 @@ def test_autoclose_trades_on_finish(self): def coroutine(self): yield self.buy() - stats = self._Backtest(coroutine, close_all_at_end=True).run() + stats = self._Backtest(coroutine, finalize_trades=True).run() self.assertEqual(len(stats._trades), 1) def test_order_tag(self): From bab229db137ccc9cd88c7080195c8b3fbc5044dc Mon Sep 17 00:00:00 2001 From: Kernc Date: Thu, 30 Jan 2025 07:05:15 +0100 Subject: [PATCH 3/3] Fix failing test --- backtesting/test/_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backtesting/test/_test.py b/backtesting/test/_test.py index 3c9d0f08..3f86829e 100644 --- a/backtesting/test/_test.py +++ b/backtesting/test/_test.py @@ -588,7 +588,7 @@ def test_optimize(self): bt.plot(filename=f, open_browser=False) def test_method_sambo(self): - bt = Backtest(GOOG.iloc[:100], SmaCross) + bt = Backtest(GOOG.iloc[:100], SmaCross, finalize_trades=True) res, heatmap, sambo_results = bt.optimize( fast=range(2, 20), slow=np.arange(2, 20, dtype=object), constraint=lambda p: p.fast < p.slow,