Skip to content

Commit e03f55f

Browse files
committed
ENH: Add Backtest(..., hedging=) that makes FIFO trade closing optional
Thanks @qacollective
1 parent e56f758 commit e03f55f

File tree

2 files changed

+53
-32
lines changed

2 files changed

+53
-32
lines changed

backtesting/backtesting.py

+33-26
Original file line numberDiff line numberDiff line change
@@ -356,17 +356,14 @@ class Order:
356356
Place new orders through `Strategy.buy()` and `Strategy.sell()`.
357357
Query existing orders through `Strategy.orders`.
358358
359-
When an order is executed or [filled], it normally results in a `Trade`, except when an
360-
existing opposite-facing trade can be sufficiently
361-
reduced or closed in an [NFA compliant FIFO] manner.
359+
When an order is executed or [filled], it results in a `Trade`.
362360
363361
If you wish to modify aspects of a placed but not yet filled order,
364362
cancel it and place a new one instead.
365363
366364
All placed orders are [Good 'Til Canceled].
367365
368366
[filled]: https://www.investopedia.com/terms/f/fill.asp
369-
[NFA compliant FIFO]: https://www.investopedia.com/terms/n/nfa-compliance-rule-2-43b.asp
370367
[Good 'Til Canceled]: https://www.investopedia.com/terms/g/gtc.asp
371368
"""
372369
def __init__(self, broker: '_Broker',
@@ -646,7 +643,7 @@ def __set_contingent(self, type, price):
646643

647644

648645
class _Broker:
649-
def __init__(self, *, data, cash, commission, margin, trade_on_close, index):
646+
def __init__(self, *, data, cash, commission, margin, trade_on_close, hedging, index):
650647
assert 0 < cash, "cash shosuld be >0, is {}".format(cash)
651648
assert 0 <= commission < .1, "commission should be between 0-10%, is {}".format(commission)
652649
assert 0 < margin <= 1, "margin should be between 0 and 1, is {}".format(margin)
@@ -655,6 +652,7 @@ def __init__(self, *, data, cash, commission, margin, trade_on_close, index):
655652
self._commission = commission
656653
self._leverage = 1 / margin
657654
self._trade_on_close = trade_on_close
655+
self._hedging = hedging
658656

659657
self._equity = np.tile(np.nan, len(index))
660658
self.orders = [] # type: List[Order]
@@ -825,25 +823,26 @@ def _process_orders(self):
825823
assert size == round(size)
826824
need_size = int(size)
827825

828-
# Fill position by FIFO closing/reducing existing opposite-facing trades.
829-
# Existing trades are closed at unadjusted price, because the adjustment
830-
# was already made when buying.
831-
for trade in list(self.trades):
832-
if trade.is_long == order.is_long:
833-
continue
834-
assert np.sign(trade.size) + np.sign(order.size) == 0
835-
836-
# Order size greater than this opposite-directed existing trade,
837-
# so it will be closed completely
838-
if abs(need_size) >= abs(trade.size):
839-
self._close_trade(trade, price, time_index)
840-
need_size += trade.size
841-
else:
842-
# The existing trade is larger than the new order,
843-
# so it will only be closed partially
844-
self._reduce_trade(trade, price, need_size, time_index)
845-
need_size = 0
846-
break
826+
if not self._hedging:
827+
# Fill position by FIFO closing/reducing existing opposite-facing trades.
828+
# Existing trades are closed at unadjusted price, because the adjustment
829+
# was already made when buying.
830+
for trade in list(self.trades):
831+
if trade.is_long == order.is_long:
832+
continue
833+
assert np.sign(trade.size) + np.sign(order.size) == 0
834+
835+
# Order size greater than this opposite-directed existing trade,
836+
# so it will be closed completely
837+
if abs(need_size) >= abs(trade.size):
838+
self._close_trade(trade, price, time_index)
839+
need_size += trade.size
840+
else:
841+
# The existing trade is larger than the new order,
842+
# so it will only be closed partially
843+
self._reduce_trade(trade, price, need_size, time_index)
844+
need_size = 0
845+
break
847846

848847
# If we don't have enough liquidity to cover for the order, cancel it
849848
if abs(need_size) * adjusted_price > self.margin_available * self._leverage:
@@ -918,7 +917,8 @@ def __init__(self,
918917
cash: float = 10000,
919918
commission: float = .0,
920919
margin: float = 1.,
921-
trade_on_close=False
920+
trade_on_close=False,
921+
hedging=False,
922922
):
923923
"""
924924
Initialize a backtest. Requires data and a strategy to test.
@@ -952,6 +952,12 @@ def __init__(self,
952952
If `trade_on_close` is `True`, market orders will be executed
953953
with respect to the current bar's closing price instead of the
954954
next bar's open.
955+
956+
If `hedging` is `True`, allow trades in both directions simultaneously.
957+
If `False`, the opposite-facing orders first close existing trades in
958+
a [FIFO] manner.
959+
960+
[FIFO]: https://www.investopedia.com/terms/n/nfa-compliance-rule-2-43b.asp
955961
"""
956962

957963
if not (isinstance(strategy, type) and issubclass(strategy, Strategy)):
@@ -999,7 +1005,8 @@ def __init__(self,
9991005
self._data = data # type: pd.DataFrame
10001006
self._broker = partial(
10011007
_Broker, cash=cash, commission=commission, margin=margin,
1002-
trade_on_close=trade_on_close, index=data.index
1008+
trade_on_close=trade_on_close, hedging=hedging,
1009+
index=data.index,
10031010
)
10041011
self._strategy = strategy
10051012
self._results = None

backtesting/test/_test.py

+20-6
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,16 @@ def next(self):
329329

330330

331331
class TestStrategy(TestCase):
332+
def _Backtest(self, strategy_coroutine, **kwargs):
333+
class S(Strategy):
334+
def init(self):
335+
self.step = strategy_coroutine(self)
336+
337+
def next(self):
338+
try_(self.step.__next__, None, StopIteration)
339+
340+
return Backtest(SHORT_DATA, S, **kwargs)
341+
332342
def test_position(self):
333343
def coroutine(self):
334344
yield self.buy()
@@ -349,14 +359,18 @@ def coroutine(self):
349359
assert not self.position.pl
350360
assert not self.position.pl_pct
351361

352-
class S(Strategy):
353-
def init(self):
354-
self.step = coroutine(self)
362+
self._Backtest(coroutine).run()
355363

356-
def next(self):
357-
try_(self.step.__next__, None, StopIteration)
364+
def test_broker_hedging(self):
365+
def coroutine(self):
366+
yield self.buy(size=2)
367+
368+
assert len(self.trades) == 1
369+
yield self.sell(size=1)
370+
371+
assert len(self.trades) == 2
358372

359-
Backtest(SHORT_DATA, S).run()
373+
self._Backtest(coroutine, hedging=True).run()
360374

361375

362376
class TestOptimize(TestCase):

0 commit comments

Comments
 (0)