diff --git a/backtesting/backtesting.py b/backtesting/backtesting.py index d2e72c5e..7039bf20 100644 --- a/backtesting/backtesting.py +++ b/backtesting/backtesting.py @@ -1088,7 +1088,8 @@ def __init__(self, margin: float = 1., trade_on_close=False, hedging=False, - exclusive_orders=False + exclusive_orders=False, + finalize_trades=False, ): """ Initialize a backtest. Requires data and a strategy to test. @@ -1155,8 +1156,13 @@ def __init__(self, trade/position, making at most a single trade (long or short) in effect at each time. + 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') @@ -1218,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: """ @@ -1304,14 +1311,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._finalize_trades is True: + # Close any remaining open trades so they produce some stats + for trade in broker.trades: + trade.close() + + # 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) # 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 30fdb8a6..3f86829e 100644 --- a/backtesting/test/_test.py +++ b/backtesting/test/_test.py @@ -218,7 +218,7 @@ def next(self, _FEW_DAYS=pd.Timedelta('3 days')): # noqa: N803 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, @@ -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).run() + stats = Backtest(GOOG, SmaCross, finalize_trades=True).run() expected = pd.Series({ # NOTE: These values are also used on the website! '# Trades': 66, @@ -438,7 +438,8 @@ 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, 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): @@ -540,7 +541,7 @@ def test_autoclose_trades_on_finish(self): def coroutine(self): yield self.buy() - stats = self._Backtest(coroutine).run() + stats = self._Backtest(coroutine, finalize_trades=True).run() self.assertEqual(len(stats._trades), 1) def test_order_tag(self): @@ -587,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, @@ -925,7 +926,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): @@ -941,7 +942,7 @@ def next(self): self.buy() stats = Backtest(GOOG, S).run() - self.assertEqual(stats['# Trades'], 57) + self.assertEqual(stats['# Trades'], 56) class TestUtil(TestCase):