Skip to content

Commit ae3d69f

Browse files
Benouarepr0kiumkernc
authored
ENH: Optionally finalize trades at the end of backtest run (#393)
* ENH: Add the possibility to close trades at end of bt.run (#273 & #343) * Change parameter name, simplify tests * Fix failing test --------- Co-authored-by: Bénouare <[email protected]> Co-authored-by: benoit <[email protected]> Co-authored-by: Kernc <[email protected]>
1 parent 44fcb02 commit ae3d69f

File tree

2 files changed

+26
-17
lines changed

2 files changed

+26
-17
lines changed

backtesting/backtesting.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,7 +1088,8 @@ def __init__(self,
10881088
margin: float = 1.,
10891089
trade_on_close=False,
10901090
hedging=False,
1091-
exclusive_orders=False
1091+
exclusive_orders=False,
1092+
finalize_trades=False,
10921093
):
10931094
"""
10941095
Initialize a backtest. Requires data and a strategy to test.
@@ -1155,8 +1156,13 @@ def __init__(self,
11551156
trade/position, making at most a single trade (long or short) in effect
11561157
at each time.
11571158
1159+
If `finalize_trades` is `True`, the trades that are still
1160+
[active and ongoing] at the end of the backtest will be closed on
1161+
the last bar and will contribute to the computed backtest statistics.
1162+
11581163
[FIFO]: https://www.investopedia.com/terms/n/nfa-compliance-rule-2-43b.asp
1159-
"""
1164+
[active and ongoing]: https://kernc.github.io/backtesting.py/doc/backtesting/backtesting.html#backtesting.backtesting.Strategy.trades
1165+
""" # noqa: E501
11601166

11611167
if not (isinstance(strategy, type) and issubclass(strategy, Strategy)):
11621168
raise TypeError('`strategy` must be a Strategy sub-type')
@@ -1218,6 +1224,7 @@ def __init__(self,
12181224
)
12191225
self._strategy = strategy
12201226
self._results: Optional[pd.Series] = None
1227+
self._finalize_trades = bool(finalize_trades)
12211228

12221229
def run(self, **kwargs) -> pd.Series:
12231230
"""
@@ -1304,14 +1311,15 @@ def run(self, **kwargs) -> pd.Series:
13041311
# Next tick, a moment before bar close
13051312
strategy.next()
13061313
else:
1307-
# Close any remaining open trades so they produce some stats
1308-
for trade in broker.trades:
1309-
trade.close()
1310-
1311-
# Re-run broker one last time to handle orders placed in the last strategy
1312-
# iteration. Use the same OHLC values as in the last broker iteration.
1313-
if start < len(self._data):
1314-
try_(broker.next, exception=_OutOfMoneyError)
1314+
if self._finalize_trades is True:
1315+
# Close any remaining open trades so they produce some stats
1316+
for trade in broker.trades:
1317+
trade.close()
1318+
1319+
# HACK: Re-run broker one last time to handle close orders placed in the last
1320+
# strategy iteration. Use the same OHLC values as in the last broker iteration.
1321+
if start < len(self._data):
1322+
try_(broker.next, exception=_OutOfMoneyError)
13151323

13161324
# Set data back to full length
13171325
# for future `indicator._opts['data'].index` calls to work

backtesting/test/_test.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ def next(self, _FEW_DAYS=pd.Timedelta('3 days')): # noqa: N803
218218
bt = Backtest(GOOG, Assertive)
219219
with self.assertWarns(UserWarning):
220220
stats = bt.run()
221-
self.assertEqual(stats['# Trades'], 145)
221+
self.assertEqual(stats['# Trades'], 144)
222222

223223
def test_broker_params(self):
224224
bt = Backtest(GOOG.iloc[:100], SmaCross,
@@ -282,7 +282,7 @@ def test_compute_drawdown(self):
282282
np.testing.assert_array_equal(peaks, pd.Series([7, 4], index=[3, 5]).reindex(dd.index))
283283

284284
def test_compute_stats(self):
285-
stats = Backtest(GOOG, SmaCross).run()
285+
stats = Backtest(GOOG, SmaCross, finalize_trades=True).run()
286286
expected = pd.Series({
287287
# NOTE: These values are also used on the website!
288288
'# Trades': 66,
@@ -438,7 +438,8 @@ def next(self):
438438
elif len(self.data) == len(SHORT_DATA):
439439
self.position.close()
440440

441-
self.assertFalse(Backtest(SHORT_DATA, S).run()._trades.empty)
441+
self.assertTrue(Backtest(SHORT_DATA, S, finalize_trades=False).run()._trades.empty)
442+
self.assertFalse(Backtest(SHORT_DATA, S, finalize_trades=True).run()._trades.empty)
442443

443444
def test_check_adjusted_price_when_placing_order(self):
444445
class S(Strategy):
@@ -540,7 +541,7 @@ def test_autoclose_trades_on_finish(self):
540541
def coroutine(self):
541542
yield self.buy()
542543

543-
stats = self._Backtest(coroutine).run()
544+
stats = self._Backtest(coroutine, finalize_trades=True).run()
544545
self.assertEqual(len(stats._trades), 1)
545546

546547
def test_order_tag(self):
@@ -587,7 +588,7 @@ def test_optimize(self):
587588
bt.plot(filename=f, open_browser=False)
588589

589590
def test_method_sambo(self):
590-
bt = Backtest(GOOG.iloc[:100], SmaCross)
591+
bt = Backtest(GOOG.iloc[:100], SmaCross, finalize_trades=True)
591592
res, heatmap, sambo_results = bt.optimize(
592593
fast=range(2, 20), slow=np.arange(2, 20, dtype=object),
593594
constraint=lambda p: p.fast < p.slow,
@@ -925,7 +926,7 @@ def init(self):
925926
self.data.Close < sma)
926927

927928
stats = Backtest(GOOG, S).run()
928-
self.assertIn(stats['# Trades'], (1181, 1182)) # varies on different archs?
929+
self.assertIn(stats['# Trades'], (1179, 1180)) # varies on different archs?
929930

930931
def test_TrailingStrategy(self):
931932
class S(TrailingStrategy):
@@ -941,7 +942,7 @@ def next(self):
941942
self.buy()
942943

943944
stats = Backtest(GOOG, S).run()
944-
self.assertEqual(stats['# Trades'], 57)
945+
self.assertEqual(stats['# Trades'], 56)
945946

946947

947948
class TestUtil(TestCase):

0 commit comments

Comments
 (0)