Skip to content

Commit d305d4c

Browse files
committed
BUG: Fix partial closing of position with portion=
Fixes #129
1 parent 0334007 commit d305d4c

File tree

2 files changed

+23
-5
lines changed

2 files changed

+23
-5
lines changed

backtesting/backtesting.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from copy import copy
2020
from functools import partial
2121
from itertools import repeat, product, chain
22+
from math import copysign
2223
from numbers import Number
2324
from typing import Callable, Dict, List, Optional, Sequence, Tuple, Type, Union
2425

@@ -379,6 +380,7 @@ def __init__(self, broker: '_Broker',
379380
tp_price: float = None,
380381
parent_trade: 'Trade' = None):
381382
self.__broker = broker
383+
assert size != 0
382384
self.__size = size
383385
self.__limit_price = limit_price
384386
self.__stop_price = stop_price
@@ -530,9 +532,9 @@ def _copy(self, **kwargs):
530532
def close(self, portion: float = 1.):
531533
"""Place new `Order` to close `portion` of the trade at next market price."""
532534
assert 0 < portion <= 1, "portion must be a fraction between 0 and 1"
533-
# TODO: check this insert
534-
self.__broker.orders.insert(0, Order(self.__broker, -round(self.size * portion),
535-
parent_trade=self))
535+
size = copysign(max(1, round(abs(self.__size) * portion)), -self.__size)
536+
order = Order(self.__broker, size, parent_trade=self)
537+
self.__broker.orders.insert(0, order)
536538

537539
# Fields getters
538540

@@ -819,17 +821,18 @@ def _process_orders(self):
819821
# If order is a SL/TP order, it should close an existing trade it was contingent upon
820822
if order.parent_trade:
821823
trade = order.parent_trade
824+
_prev_size = trade.size
822825
# If this trade isn't already closed (e.g. on multiple `trade.close(.5)` calls)
823826
if trade in self.trades:
824827
self._reduce_trade(trade, price, order.size, time_index)
825-
assert order.size != -trade.size or trade not in self.trades
828+
assert order.size != -_prev_size or trade not in self.trades
826829
if order in (trade._sl_order,
827830
trade._tp_order):
828831
assert order.size == -trade.size
829832
assert order not in self.orders # Removed when trade was closed
830833
else:
831834
# It's a trade.close() order, now done
832-
assert abs(trade.size) >= abs(order.size) >= 1
835+
assert abs(_prev_size) >= abs(order.size) >= 1
833836
self.orders.remove(order)
834837
continue
835838

backtesting/test/_test.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,21 @@ def next(self):
361361
with self.assertWarns(UserWarning):
362362
self.assertEqual(Backtest(GOOG, S).run()._trades.iloc[0].EntryPrice, 104)
363363

364+
def test_position_close_portion(self):
365+
class SmaCross(Strategy):
366+
def init(self):
367+
self.sma1 = self.I(SMA, self.data.Close, 10)
368+
self.sma2 = self.I(SMA, self.data.Close, 20)
369+
370+
def next(self):
371+
if not self.position and crossover(self.sma1, self.sma2):
372+
self.buy(size=10)
373+
if self.position and crossover(self.sma2, self.sma1):
374+
self.position.close(portion=.5)
375+
376+
bt = Backtest(GOOG, SmaCross, commission=.002)
377+
bt.run()
378+
364379

365380
class TestStrategy(TestCase):
366381
def _Backtest(self, strategy_coroutine, **kwargs):

0 commit comments

Comments
 (0)