Skip to content
Open
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
6 changes: 4 additions & 2 deletions backtesting/backtesting.py
Original file line number Diff line number Diff line change
Expand Up @@ -1082,6 +1082,8 @@ def _close_trade(self, trade: Trade, price: float, time_index: int):
if trade._tp_order:
self.orders.remove(trade._tp_order)

# 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)
# Apply commission one more time at trade exit
Expand Down Expand Up @@ -1136,6 +1138,7 @@ class Backtest:
`cash` is the initial cash to start with.

`spread` is the constant bid-ask spread rate (relative to the price).
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.

Expand All @@ -1152,8 +1155,7 @@ 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
Expand Down
71 changes: 70 additions & 1 deletion backtesting/test/_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ def next(self):
trade_open_price = SHORT_DATA['Open'].iloc[ORDER_BAR]
self.assertEqual(stats['_trades']['EntryPrice'].iloc[0], trade_open_price * (1 + SPREAD))
self.assertEqual(stats['_equity_curve']['Equity'].iloc[2:4].round(2).tolist(),
[9685.31, 9749.33])
[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(),
Expand All @@ -262,6 +262,75 @@ def next(self):
self.assertEqual(stats['_equity_curve']['Equity'].iloc[2:4].round(2).tolist(),
[9781.28, 9846.04])

def test_spread_is_applied_at_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), (102., 98.))
self.assertEqual((short_trade.EntryPrice, short_trade.ExitPrice), (98., 102.))
self.assertEqual((long_trade.PnL, short_trade.PnL), (-40., -40.))

def test_reversal_applies_spread_at_each_transaction(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., 102., 98.], [-5., 98., 102.]])
self.assertEqual(trades.PnL.tolist(), [-40., -20.])

def test_stop_exit_pays_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, 102.)
self.assertEqual(trade.SL, 95.)
self.assertEqual(trade.ExitPrice, 93.1)

def test_commissions(self):
class S(_S):
def next(self):
Expand Down