Skip to content

Commit 3971535

Browse files
committed
ENH: Add the possibility to close trades at end of bt.run (#273 & #343)
1 parent 0a76e96 commit 3971535

File tree

3 files changed

+49
-18
lines changed

3 files changed

+49
-18
lines changed

CHANGELOG.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ What's New
44
These were the major changes contributing to each release:
55

66

7-
### 0.x.x
7+
### 0.3.2
8+
* new param on run to manage close, or not the trades at end (#273)
89

910

1011
### 0.3.1
@@ -114,4 +115,4 @@ These were the major changes contributing to each release:
114115
### 0.1.0
115116
(2019-01-15)
116117

117-
* Initial release
118+
* Initial release

backtesting/backtesting.py

+15-10
Original file line numberDiff line numberDiff line change
@@ -986,7 +986,8 @@ def __init__(self,
986986
margin: float = 1.,
987987
trade_on_close=False,
988988
hedging=False,
989-
exclusive_orders=False
989+
exclusive_orders=False,
990+
close_all_at_end=False
990991
):
991992
"""
992993
Initialize a backtest. Requires data and a strategy to test.
@@ -1031,6 +1032,9 @@ def __init__(self,
10311032
trade/position, making at most a single trade (long or short) in effect
10321033
at each time.
10331034
1035+
If `close_all_at_end` is `False`, the trade will not be close at end,
1036+
and will not apear in _Stats.
1037+
10341038
[FIFO]: https://www.investopedia.com/terms/n/nfa-compliance-rule-2-43b.asp
10351039
"""
10361040

@@ -1080,7 +1084,7 @@ def __init__(self,
10801084
warnings.warn('Data index is not datetime. Assuming simple periods, '
10811085
'but `pd.DateTimeIndex` is advised.',
10821086
stacklevel=2)
1083-
1087+
self._close_all_at_end = bool(close_all_at_end)
10841088
self._data: pd.DataFrame = data
10851089
self._broker = partial(
10861090
_Broker, cash=cash, commission=commission, margin=margin,
@@ -1166,14 +1170,15 @@ def run(self, **kwargs) -> pd.Series:
11661170
# Next tick, a moment before bar close
11671171
strategy.next()
11681172
else:
1169-
# Close any remaining open trades so they produce some stats
1170-
for trade in broker.trades:
1171-
trade.close()
1172-
1173-
# Re-run broker one last time to handle orders placed in the last strategy
1174-
# iteration. Use the same OHLC values as in the last broker iteration.
1175-
if start < len(self._data):
1176-
try_(broker.next, exception=_OutOfMoneyError)
1173+
if self._close_all_at_end is True:
1174+
# Close any remaining open trades so they produce some stats
1175+
for trade in broker.trades:
1176+
trade.close()
1177+
1178+
# Re-run broker one last time to handle orders placed in the last strategy
1179+
# iteration. Use the same OHLC values as in the last broker iteration.
1180+
if start < len(self._data):
1181+
try_(broker.next, exception=_OutOfMoneyError)
11771182

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

backtesting/test/_test.py

+31-6
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ def next(self, FIVE_DAYS=pd.Timedelta('3 days')):
215215
bt = Backtest(GOOG, Assertive)
216216
with self.assertWarns(UserWarning):
217217
stats = bt.run()
218-
self.assertEqual(stats['# Trades'], 145)
218+
self.assertEqual(stats['# Trades'], 144)
219219

220220
def test_broker_params(self):
221221
bt = Backtest(GOOG.iloc[:100], SmaCross,
@@ -248,7 +248,7 @@ def test_compute_drawdown(self):
248248
np.testing.assert_array_equal(peaks, pd.Series([7, 4], index=[3, 5]).reindex(dd.index))
249249

250250
def test_compute_stats(self):
251-
stats = Backtest(GOOG, SmaCross).run()
251+
stats = Backtest(GOOG, SmaCross, close_all_at_end=True).run()
252252
expected = pd.Series({
253253
# NOTE: These values are also used on the website!
254254
'# Trades': 66,
@@ -397,7 +397,32 @@ def next(self):
397397
elif len(self.data) == len(SHORT_DATA):
398398
self.position.close()
399399

400-
self.assertFalse(Backtest(SHORT_DATA, S).run()._trades.empty)
400+
self.assertTrue(Backtest(SHORT_DATA, S).run()._trades.empty)
401+
402+
def test_dont_close_orders_from_last_strategy_iteration(self):
403+
class S(Strategy):
404+
def init(self): pass
405+
406+
def next(self):
407+
if not self.position:
408+
self.buy()
409+
elif len(self.data) == len(SHORT_DATA):
410+
self.position.close()
411+
self.assertEqual(len(
412+
Backtest(SHORT_DATA, S, close_all_at_end=False).run()._strategy.closed_trades), 0)
413+
self.assertEqual(len(
414+
Backtest(SHORT_DATA, S, close_all_at_end=False).run()._strategy.trades), 1)
415+
416+
def test_dont_close_orders_trades_from_last_strategy_iteration(self):
417+
class S(Strategy):
418+
def init(self): pass
419+
420+
def next(self):
421+
if not self.position:
422+
self.buy()
423+
424+
self.assertEqual(len(
425+
Backtest(SHORT_DATA, S, close_all_at_end=False).run()._strategy.trades), 1)
401426

402427
def test_check_adjusted_price_when_placing_order(self):
403428
class S(Strategy):
@@ -499,7 +524,7 @@ def test_autoclose_trades_on_finish(self):
499524
def coroutine(self):
500525
yield self.buy()
501526

502-
stats = self._Backtest(coroutine).run()
527+
stats = self._Backtest(coroutine, close_all_at_end=True).run()
503528
self.assertEqual(len(stats._trades), 1)
504529

505530

@@ -844,7 +869,7 @@ def init(self):
844869
self.data.Close < sma)
845870

846871
stats = Backtest(GOOG, S).run()
847-
self.assertIn(stats['# Trades'], (1181, 1182)) # varies on different archs?
872+
self.assertIn(stats['# Trades'], (1179, 1180)) # varies on different archs?
848873

849874
def test_TrailingStrategy(self):
850875
class S(TrailingStrategy):
@@ -860,7 +885,7 @@ def next(self):
860885
self.buy()
861886

862887
stats = Backtest(GOOG, S).run()
863-
self.assertEqual(stats['# Trades'], 57)
888+
self.assertEqual(stats['# Trades'], 56)
864889

865890

866891
class TestUtil(TestCase):

0 commit comments

Comments
 (0)