From f8b99ecbf21e1417a1eedf91a696a83eb3ac10b3 Mon Sep 17 00:00:00 2001 From: nyxst4ck <289980115+nyxst4ck@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:47:43 -0300 Subject: [PATCH 1/2] fix(broker): split spread across trade lifecycle Co-Authored-By: claude-flow --- backtesting/backtesting.py | 51 ++++++------ backtesting/test/_test.py | 161 ++++++++++++++++++++++++++++++++++++- 2 files changed, 185 insertions(+), 27 deletions(-) diff --git a/backtesting/backtesting.py b/backtesting/backtesting.py index 3f634244..b09f627b 100644 --- a/backtesting/backtesting.py +++ b/backtesting/backtesting.py @@ -349,7 +349,7 @@ def size(self) -> float: @property def pl(self) -> float: """Profit (positive) or loss (negative) of the current position in cash units.""" - return self.__broker._position_unrealized_pl + return sum(trade.pl for trade in self.__broker.trades) @property def pl_pct(self) -> float: @@ -656,7 +656,7 @@ def is_short(self): def pl(self): """ Trade profit (positive) or loss (negative) in cash units. - Commissions are reflected only after the Trade is closed. + Commissions already incurred are reflected. """ price = self.__exit_price or self.__broker.last_price return (self.__size * (price - self.__entry_price)) - self._commissions @@ -821,6 +821,8 @@ def _position_initial_value(self) -> float: @cached_property def _position_unrealized_pl(self) -> float: + # Entry commissions are already reflected in cash. Keep this value gross + # so equity does not count those commissions twice. return (self.last_price * self._position_size - sum(trade.size * trade.entry_price for trade in self.trades)) @@ -836,10 +838,10 @@ def last_price(self) -> float: def _adjusted_price(self, size=None, price=None) -> float: """ - Long/short `price`, adjusted for spread. + Long/short `price`, adjusted for half the bid-ask spread. In long positions, the adjusted price is a fraction higher, and vice versa. """ - return (price or self.last_price) * (1 + copysign(self._spread, size)) + return (price or self.last_price) * (1 + copysign(self._spread / 2, size)) @property def equity(self) -> float: @@ -955,8 +957,7 @@ def _process_orders(self): # Else this is a stand-alone trade - # Adjust price to include commission (or bid-ask spread). - # In long positions, the adjusted price is a fraction higher, and vice versa. + # Adjust the entry price for half the bid-ask spread. adjusted_price = self._adjusted_price(order.size, price) adjusted_price_plus_commission = \ adjusted_price + self._commission(order.size, price) / abs(order.size) @@ -1056,20 +1057,26 @@ def _reduce_trade(self, trade: Trade, price: float, size: float, time_index: int assert abs(trade.size) >= abs(size) self._trades_cache_clear() + original_size = trade.size size_left = trade.size + size assert size_left * trade.size >= 0 if not size_left: close_trade = trade else: + # Allocate already-paid entry commission proportionally before + # reducing the active trade. This is especially important for + # fixed commissions, which must not be recreated per fragment. + close_trade = trade._copy(size=-size, sl_order=None, tp_order=None) + close_trade._commissions = \ + trade._commissions * abs(size) / abs(original_size) + trade._commissions -= close_trade._commissions + # Reduce existing trade ... trade._replace(size=size_left) if trade._sl_order: trade._sl_order._replace(size=-trade.size) if trade._tp_order: trade._tp_order._replace(size=-trade.size) - - # ... by closing a reduced copy of it - close_trade = trade._copy(size=-size, sl_order=None, tp_order=None) self.trades.append(close_trade) self._close_trade(close_trade, price, time_index) @@ -1082,16 +1089,15 @@ def _close_trade(self, trade: Trade, price: float, time_index: int): if trade._tp_order: self.orders.remove(trade._tp_order) + # Apply the remaining half of the bid-ask spread at exit. + price = self._adjusted_price(-trade.size, price) closed_trade = trade._replace(exit_price=price, exit_bar=time_index) self.closed_trades.append(closed_trade) - # Apply commission one more time at trade exit + # Entry commission was already debited and stored when the trade opened. + # Realize gross P/L here so it is not subtracted from cash twice. commission = self._commission(trade.size, price) - self._cash += trade.pl - commission - # Save commissions on Trade instance for stats - trade_open_commission = self._commission(closed_trade.size, closed_trade.entry_price) - # applied here instead of on Trade open because size could have changed - # by way of _reduce_trade() - closed_trade._commissions = commission + trade_open_commission + self._cash += trade.size * (price - trade.entry_price) - commission + closed_trade._commissions += commission def _open_trade(self, price: float, size: int, sl: Optional[float], tp: Optional[float], time_index: int, tag): @@ -1099,7 +1105,9 @@ def _open_trade(self, price: float, size: int, self.trades.append(trade) self._trades_cache_clear() # Apply broker commission at trade open - self._cash -= self._commission(size, price) + commission = self._commission(size, price) + self._cash -= commission + trade._commissions = commission # Create SL/TP (bracket) orders. if tp: trade.tp = tp @@ -1136,6 +1144,7 @@ class Backtest: `cash` is the initial cash to start with. `spread` is the constant bid-ask spread rate (relative to the price). + Half the spread is applied at trade entry and the other half at trade exit. E.g. set it to `0.0002` for commission-less forex trading where the average spread is roughly 0.2‰ of the asking price. @@ -1155,14 +1164,6 @@ class Backtest: Before v0.4.0, the commission was only applied once, like `spread` is now. If you want to keep the old behavior, simply set `spread` instead. - .. note:: - With nonzero `commission`, long and short orders will be placed - at an adjusted price that is slightly higher or lower (respectively) - than the current price. See e.g. - [#153](https://github.com/kernc/backtesting.py/issues/153), - [#538](https://github.com/kernc/backtesting.py/issues/538), - [#633](https://github.com/kernc/backtesting.py/issues/633). - `margin` is the required margin (ratio) of a leveraged account. No difference is made between initial and maintenance margins. To run the backtest using e.g. 50:1 leverage that your broker allows, diff --git a/backtesting/test/_test.py b/backtesting/test/_test.py index 763d81a5..f3874b02 100644 --- a/backtesting/test/_test.py +++ b/backtesting/test/_test.py @@ -249,9 +249,9 @@ def next(self): ORDER_BAR = 2 stats = Backtest(SHORT_DATA, S, cash=CASH, spread=SPREAD, commission=COMMISSION).run() trade_open_price = SHORT_DATA['Open'].iloc[ORDER_BAR] - self.assertEqual(stats['_trades']['EntryPrice'].iloc[0], trade_open_price * (1 + SPREAD)) + self.assertEqual(stats['_trades']['EntryPrice'].iloc[0], trade_open_price * (1 + SPREAD / 2)) self.assertEqual(stats['_equity_curve']['Equity'].iloc[2:4].round(2).tolist(), - [9685.31, 9749.33]) + [9734.52, 9750.10]) stats = Backtest(SHORT_DATA, S, cash=CASH, commission=(100, COMMISSION)).run() self.assertEqual(stats['_equity_curve']['Equity'].iloc[2:4].round(2).tolist(), @@ -262,6 +262,163 @@ def next(self): self.assertEqual(stats['_equity_curve']['Equity'].iloc[2:4].round(2).tolist(), [9781.28, 9846.04]) + def test_spread_is_split_between_entry_and_exit(self): + class Long(Strategy): + def init(self): + pass + + def next(self): + if len(self.data) == 2: + self.buy(size=10) + elif self.position: + self.position.close() + + class Short(Long): + def next(self): + if len(self.data) == 2: + self.sell(size=10) + elif self.position: + self.position.close() + + index = pd.date_range('2020', periods=5) + data = pd.DataFrame({column: 100. for column in ('Open', 'High', 'Low', 'Close')}, + index=index) + + long_trade = Backtest(data, Long, spread=.02).run()._trades.iloc[0] + short_trade = Backtest(data, Short, spread=.02).run()._trades.iloc[0] + + self.assertEqual((long_trade.EntryPrice, long_trade.ExitPrice), (101., 99.)) + self.assertEqual((short_trade.EntryPrice, short_trade.ExitPrice), (99., 101.)) + self.assertEqual((long_trade.PnL, short_trade.PnL), (-20., -20.)) + + def test_open_trade_pl_includes_entry_commission(self): + class S(Strategy): + def init(self): + self.open_pl = self.open_pl_pct = None + self.position_pl = self.position_pl_pct = None + + def next(self): + if len(self.data) == 2: + self.buy(size=10) + elif self.position: + trade = self.trades[0] + self.open_pl = trade.pl + self.open_pl_pct = trade.pl_pct + self.position_pl = self.position.pl + self.position_pl_pct = self.position.pl_pct + self.position.close() + + index = pd.date_range('2020', periods=5) + data = pd.DataFrame({column: 100. for column in ('Open', 'High', 'Low', 'Close')}, + index=index) + stats = Backtest(data, S, cash=10_000, commission=(5, .01)).run() + + self.assertEqual(stats._strategy.open_pl, -15.) + self.assertEqual(stats._strategy.open_pl_pct, -.015) + self.assertEqual(stats._strategy.position_pl, -15.) + self.assertEqual(stats._strategy.position_pl_pct, -1.5) + self.assertEqual(stats._trades.Commission.iloc[0], 30.) + self.assertEqual(stats._trades.PnL.iloc[0], -30.) + + def test_partial_close_allocates_entry_commission(self): + class S(Strategy): + def init(self): + self.remaining_pl = None + self.accounting_delta = None + + def next(self): + if len(self.data) == 2: + self.buy(size=10) + elif len(self.data) == 3: + self.position.close(.4) + elif self.position: + self.remaining_pl = self.trades[0].pl + self.accounting_delta = ( + self.closed_trades[0].pl + self.trades[0].pl, + self.equity - 10_000, + ) + self.position.close() + + index = pd.date_range('2020', periods=6) + data = pd.DataFrame({column: 100. for column in ('Open', 'High', 'Low', 'Close')}, + index=index) + stats = Backtest(data, S, cash=10_000, commission=(5, .01)).run() + + self.assertEqual(stats._strategy.remaining_pl, -9.) + self.assertEqual(stats._strategy.accounting_delta, (-24., -24.)) + self.assertEqual(stats['Commissions [$]'], 35.) + self.assertEqual(stats._trades.Commission.tolist(), [15., 20.]) + self.assertEqual(stats._trades.PnL.tolist(), [-15., -20.]) + + def test_callable_entry_commission_is_not_recomputed_at_close(self): + class Commission: + def __init__(self): + self.calls = [] + + def __call__(self, size, price): + self.calls.append((size, price)) + return len(self.calls) + + class S(Strategy): + def init(self): + pass + + def next(self): + if len(self.data) == 2: + self.buy(size=10) + elif self.position: + self.position.close() + + commission = Commission() + index = pd.date_range('2020', periods=5) + data = pd.DataFrame({column: 100. for column in ('Open', 'High', 'Low', 'Close')}, + index=index) + stats = Backtest(data, S, cash=10_000, commission=commission).run() + + self.assertEqual(len(commission.calls), 3) + self.assertEqual(stats._trades.Commission.iloc[0], 5.) + self.assertEqual(stats._trades.PnL.iloc[0], -5.) + + def test_reversal_applies_half_spread_once_per_fill(self): + class S(Strategy): + def init(self): + pass + + def next(self): + if len(self.data) == 2: + self.buy(size=10) + elif len(self.data) == 3: + self.sell(size=15) + elif self.position: + self.position.close() + + index = pd.date_range('2020', periods=6) + data = pd.DataFrame({column: 100. for column in ('Open', 'High', 'Low', 'Close')}, + index=index) + trades = Backtest(data, S, spread=.02).run()._trades + + self.assertEqual(trades[['Size', 'EntryPrice', 'ExitPrice']].values.tolist(), + [[10., 101., 99.], [-5., 99., 101.]]) + self.assertEqual(trades.PnL.tolist(), [-20., -10.]) + + def test_stop_exit_pays_half_spread(self): + class S(_S): + def next(self): + if len(self.data) == 2: + self.buy(size=10, sl=95) + + data = pd.DataFrame({ + 'Open': [100.] * 5, + 'High': [100.] * 5, + 'Low': [100., 100., 100., 94., 100.], + 'Close': [100.] * 5, + }, index=pd.date_range('2020', periods=5)) + trade = Backtest(data, S, spread=.02).run()._trades.iloc[0] + + self.assertEqual(trade.EntryPrice, 101.) + self.assertEqual(trade.SL, 95.) + self.assertEqual(trade.ExitPrice, 94.05) + def test_commissions(self): class S(_S): def next(self): From 4670a842ebab4ab867d0b24818b6611ba26c5e86 Mon Sep 17 00:00:00 2001 From: nyxst4ck <289980115+nyxst4ck@users.noreply.github.com> Date: Tue, 21 Jul 2026 19:22:57 -0300 Subject: [PATCH 2/2] fix(broker): apply spread on both transactions Rework the proposed behavior to charge the configured spread at trade entry and exit while retaining per-transaction commission calculation. --- backtesting/backtesting.py | 55 +++++++++--------- backtesting/test/_test.py | 112 ++++--------------------------------- 2 files changed, 40 insertions(+), 127 deletions(-) diff --git a/backtesting/backtesting.py b/backtesting/backtesting.py index b09f627b..c0addbd4 100644 --- a/backtesting/backtesting.py +++ b/backtesting/backtesting.py @@ -349,7 +349,7 @@ def size(self) -> float: @property def pl(self) -> float: """Profit (positive) or loss (negative) of the current position in cash units.""" - return sum(trade.pl for trade in self.__broker.trades) + return self.__broker._position_unrealized_pl @property def pl_pct(self) -> float: @@ -656,7 +656,7 @@ def is_short(self): def pl(self): """ Trade profit (positive) or loss (negative) in cash units. - Commissions already incurred are reflected. + Commissions are reflected only after the Trade is closed. """ price = self.__exit_price or self.__broker.last_price return (self.__size * (price - self.__entry_price)) - self._commissions @@ -821,8 +821,6 @@ def _position_initial_value(self) -> float: @cached_property def _position_unrealized_pl(self) -> float: - # Entry commissions are already reflected in cash. Keep this value gross - # so equity does not count those commissions twice. return (self.last_price * self._position_size - sum(trade.size * trade.entry_price for trade in self.trades)) @@ -838,10 +836,10 @@ def last_price(self) -> float: def _adjusted_price(self, size=None, price=None) -> float: """ - Long/short `price`, adjusted for half the bid-ask spread. + Long/short `price`, adjusted for spread. In long positions, the adjusted price is a fraction higher, and vice versa. """ - return (price or self.last_price) * (1 + copysign(self._spread / 2, size)) + return (price or self.last_price) * (1 + copysign(self._spread, size)) @property def equity(self) -> float: @@ -957,7 +955,8 @@ def _process_orders(self): # Else this is a stand-alone trade - # Adjust the entry price for half the bid-ask spread. + # Adjust price to include commission (or bid-ask spread). + # In long positions, the adjusted price is a fraction higher, and vice versa. adjusted_price = self._adjusted_price(order.size, price) adjusted_price_plus_commission = \ adjusted_price + self._commission(order.size, price) / abs(order.size) @@ -1057,26 +1056,20 @@ def _reduce_trade(self, trade: Trade, price: float, size: float, time_index: int assert abs(trade.size) >= abs(size) self._trades_cache_clear() - original_size = trade.size size_left = trade.size + size assert size_left * trade.size >= 0 if not size_left: close_trade = trade else: - # Allocate already-paid entry commission proportionally before - # reducing the active trade. This is especially important for - # fixed commissions, which must not be recreated per fragment. - close_trade = trade._copy(size=-size, sl_order=None, tp_order=None) - close_trade._commissions = \ - trade._commissions * abs(size) / abs(original_size) - trade._commissions -= close_trade._commissions - # Reduce existing trade ... trade._replace(size=size_left) if trade._sl_order: trade._sl_order._replace(size=-trade.size) if trade._tp_order: trade._tp_order._replace(size=-trade.size) + + # ... by closing a reduced copy of it + close_trade = trade._copy(size=-size, sl_order=None, tp_order=None) self.trades.append(close_trade) self._close_trade(close_trade, price, time_index) @@ -1089,15 +1082,18 @@ def _close_trade(self, trade: Trade, price: float, time_index: int): if trade._tp_order: self.orders.remove(trade._tp_order) - # Apply the remaining half of the bid-ask spread at exit. + # Crossing the spread affects both transactions: entry and exit. price = self._adjusted_price(-trade.size, price) closed_trade = trade._replace(exit_price=price, exit_bar=time_index) self.closed_trades.append(closed_trade) - # Entry commission was already debited and stored when the trade opened. - # Realize gross P/L here so it is not subtracted from cash twice. + # Apply commission one more time at trade exit commission = self._commission(trade.size, price) - self._cash += trade.size * (price - trade.entry_price) - commission - closed_trade._commissions += commission + self._cash += trade.pl - commission + # Save commissions on Trade instance for stats + trade_open_commission = self._commission(closed_trade.size, closed_trade.entry_price) + # applied here instead of on Trade open because size could have changed + # by way of _reduce_trade() + closed_trade._commissions = commission + trade_open_commission def _open_trade(self, price: float, size: int, sl: Optional[float], tp: Optional[float], time_index: int, tag): @@ -1105,9 +1101,7 @@ def _open_trade(self, price: float, size: int, self.trades.append(trade) self._trades_cache_clear() # Apply broker commission at trade open - commission = self._commission(size, price) - self._cash -= commission - trade._commissions = commission + self._cash -= self._commission(size, price) # Create SL/TP (bracket) orders. if tp: trade.tp = tp @@ -1144,7 +1138,7 @@ class Backtest: `cash` is the initial cash to start with. `spread` is the constant bid-ask spread rate (relative to the price). - Half the spread is applied at trade entry and the other half at trade exit. + The spread is applied when entering and exiting a trade. E.g. set it to `0.0002` for commission-less forex trading where the average spread is roughly 0.2‰ of the asking price. @@ -1161,8 +1155,15 @@ class Backtest: Negative commission values are interpreted as market-maker's rebates. .. note:: - Before v0.4.0, the commission was only applied once, like `spread` is now. - If you want to keep the old behavior, simply set `spread` instead. + Before v0.4.0, the commission was only applied once per trade. + + .. note:: + With nonzero `commission`, long and short orders will be placed + at an adjusted price that is slightly higher or lower (respectively) + than the current price. See e.g. + [#153](https://github.com/kernc/backtesting.py/issues/153), + [#538](https://github.com/kernc/backtesting.py/issues/538), + [#633](https://github.com/kernc/backtesting.py/issues/633). `margin` is the required margin (ratio) of a leveraged account. No difference is made between initial and maintenance margins. diff --git a/backtesting/test/_test.py b/backtesting/test/_test.py index f3874b02..1f8051e7 100644 --- a/backtesting/test/_test.py +++ b/backtesting/test/_test.py @@ -249,9 +249,9 @@ def next(self): ORDER_BAR = 2 stats = Backtest(SHORT_DATA, S, cash=CASH, spread=SPREAD, commission=COMMISSION).run() trade_open_price = SHORT_DATA['Open'].iloc[ORDER_BAR] - self.assertEqual(stats['_trades']['EntryPrice'].iloc[0], trade_open_price * (1 + SPREAD / 2)) + self.assertEqual(stats['_trades']['EntryPrice'].iloc[0], trade_open_price * (1 + SPREAD)) self.assertEqual(stats['_equity_curve']['Equity'].iloc[2:4].round(2).tolist(), - [9734.52, 9750.10]) + [9685.31, 9652.42]) stats = Backtest(SHORT_DATA, S, cash=CASH, commission=(100, COMMISSION)).run() self.assertEqual(stats['_equity_curve']['Equity'].iloc[2:4].round(2).tolist(), @@ -262,7 +262,7 @@ def next(self): self.assertEqual(stats['_equity_curve']['Equity'].iloc[2:4].round(2).tolist(), [9781.28, 9846.04]) - def test_spread_is_split_between_entry_and_exit(self): + def test_spread_is_applied_at_entry_and_exit(self): class Long(Strategy): def init(self): pass @@ -287,99 +287,11 @@ def next(self): long_trade = Backtest(data, Long, spread=.02).run()._trades.iloc[0] short_trade = Backtest(data, Short, spread=.02).run()._trades.iloc[0] - self.assertEqual((long_trade.EntryPrice, long_trade.ExitPrice), (101., 99.)) - self.assertEqual((short_trade.EntryPrice, short_trade.ExitPrice), (99., 101.)) - self.assertEqual((long_trade.PnL, short_trade.PnL), (-20., -20.)) + self.assertEqual((long_trade.EntryPrice, long_trade.ExitPrice), (102., 98.)) + self.assertEqual((short_trade.EntryPrice, short_trade.ExitPrice), (98., 102.)) + self.assertEqual((long_trade.PnL, short_trade.PnL), (-40., -40.)) - def test_open_trade_pl_includes_entry_commission(self): - class S(Strategy): - def init(self): - self.open_pl = self.open_pl_pct = None - self.position_pl = self.position_pl_pct = None - - def next(self): - if len(self.data) == 2: - self.buy(size=10) - elif self.position: - trade = self.trades[0] - self.open_pl = trade.pl - self.open_pl_pct = trade.pl_pct - self.position_pl = self.position.pl - self.position_pl_pct = self.position.pl_pct - self.position.close() - - index = pd.date_range('2020', periods=5) - data = pd.DataFrame({column: 100. for column in ('Open', 'High', 'Low', 'Close')}, - index=index) - stats = Backtest(data, S, cash=10_000, commission=(5, .01)).run() - - self.assertEqual(stats._strategy.open_pl, -15.) - self.assertEqual(stats._strategy.open_pl_pct, -.015) - self.assertEqual(stats._strategy.position_pl, -15.) - self.assertEqual(stats._strategy.position_pl_pct, -1.5) - self.assertEqual(stats._trades.Commission.iloc[0], 30.) - self.assertEqual(stats._trades.PnL.iloc[0], -30.) - - def test_partial_close_allocates_entry_commission(self): - class S(Strategy): - def init(self): - self.remaining_pl = None - self.accounting_delta = None - - def next(self): - if len(self.data) == 2: - self.buy(size=10) - elif len(self.data) == 3: - self.position.close(.4) - elif self.position: - self.remaining_pl = self.trades[0].pl - self.accounting_delta = ( - self.closed_trades[0].pl + self.trades[0].pl, - self.equity - 10_000, - ) - self.position.close() - - index = pd.date_range('2020', periods=6) - data = pd.DataFrame({column: 100. for column in ('Open', 'High', 'Low', 'Close')}, - index=index) - stats = Backtest(data, S, cash=10_000, commission=(5, .01)).run() - - self.assertEqual(stats._strategy.remaining_pl, -9.) - self.assertEqual(stats._strategy.accounting_delta, (-24., -24.)) - self.assertEqual(stats['Commissions [$]'], 35.) - self.assertEqual(stats._trades.Commission.tolist(), [15., 20.]) - self.assertEqual(stats._trades.PnL.tolist(), [-15., -20.]) - - def test_callable_entry_commission_is_not_recomputed_at_close(self): - class Commission: - def __init__(self): - self.calls = [] - - def __call__(self, size, price): - self.calls.append((size, price)) - return len(self.calls) - - class S(Strategy): - def init(self): - pass - - def next(self): - if len(self.data) == 2: - self.buy(size=10) - elif self.position: - self.position.close() - - commission = Commission() - index = pd.date_range('2020', periods=5) - data = pd.DataFrame({column: 100. for column in ('Open', 'High', 'Low', 'Close')}, - index=index) - stats = Backtest(data, S, cash=10_000, commission=commission).run() - - self.assertEqual(len(commission.calls), 3) - self.assertEqual(stats._trades.Commission.iloc[0], 5.) - self.assertEqual(stats._trades.PnL.iloc[0], -5.) - - def test_reversal_applies_half_spread_once_per_fill(self): + def test_reversal_applies_spread_at_each_transaction(self): class S(Strategy): def init(self): pass @@ -398,10 +310,10 @@ def next(self): trades = Backtest(data, S, spread=.02).run()._trades self.assertEqual(trades[['Size', 'EntryPrice', 'ExitPrice']].values.tolist(), - [[10., 101., 99.], [-5., 99., 101.]]) - self.assertEqual(trades.PnL.tolist(), [-20., -10.]) + [[10., 102., 98.], [-5., 98., 102.]]) + self.assertEqual(trades.PnL.tolist(), [-40., -20.]) - def test_stop_exit_pays_half_spread(self): + def test_stop_exit_pays_spread(self): class S(_S): def next(self): if len(self.data) == 2: @@ -415,9 +327,9 @@ def next(self): }, index=pd.date_range('2020', periods=5)) trade = Backtest(data, S, spread=.02).run()._trades.iloc[0] - self.assertEqual(trade.EntryPrice, 101.) + self.assertEqual(trade.EntryPrice, 102.) self.assertEqual(trade.SL, 95.) - self.assertEqual(trade.ExitPrice, 94.05) + self.assertEqual(trade.ExitPrice, 93.1) def test_commissions(self): class S(_S):