Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: Add the possibility to close trades at end of bt.run (#273 & #343) #393

Merged
merged 7 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions backtesting/backtesting.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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
Expand Down
15 changes: 8 additions & 7 deletions backtesting/test/_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand Down