Skip to content

Commit

Permalink
Fixed commission charging behavior for FixedFeeModel (#1595)
Browse files Browse the repository at this point in the history
  • Loading branch information
rsmb7z authored Apr 16, 2024
1 parent cf730bd commit f43613e
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 30 deletions.
2 changes: 2 additions & 0 deletions nautilus_trader/backtest/models.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,5 @@ cdef class MakerTakerFeeModel(FeeModel):

cdef class FixedFeeModel(FeeModel):
cdef Money _commission
cdef Money _zero_commission
cdef bint _charge_commission_once
15 changes: 13 additions & 2 deletions nautilus_trader/backtest/models.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ cdef class FixedFeeModel(FeeModel):
----------
commission : Money
The fixed commission amount for trades.
charge_commission_once : bool, default True
Whether to charge the commission once per order or per fill.
Raises
------
Expand All @@ -252,10 +254,16 @@ cdef class FixedFeeModel(FeeModel):
"""

def __init__(self, Money commission not None):
def __init__(
self,
Money commission not None,
bint charge_commission_once: bool = True,
):
Condition.positive(commission, "commission")

self._commission = commission
self._zero_commission = Money(0, commission.currency)
self._charge_commission_once = charge_commission_once

cpdef Money get_commission(
self,
Expand All @@ -264,4 +272,7 @@ cdef class FixedFeeModel(FeeModel):
Price fill_px,
Instrument instrument,
):
return self._commission
if not self._charge_commission_once or order.filled_qty == 0:
return self._commission
else:
return self._zero_commission
100 changes: 72 additions & 28 deletions tests/unit_tests/backtest/test_commission_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
from nautilus_trader.model.instruments.base import Instrument
from nautilus_trader.model.objects import Money
from nautilus_trader.model.objects import Price
from nautilus_trader.model.orders import Order
from nautilus_trader.test_kit.providers import TestInstrumentProvider
from nautilus_trader.test_kit.stubs.events import TestEventStubs
from nautilus_trader.test_kit.stubs.execution import TestExecStubs


Expand All @@ -36,49 +36,89 @@ def instrument() -> Instrument:
return TestInstrumentProvider.default_fx_ccy("EUR/USD")


@pytest.fixture()
def buy_order(instrument: Instrument) -> Order:
return TestExecStubs.make_filled_order(
@pytest.mark.parametrize("order_side", [OrderSide.BUY, OrderSide.SELL])
def test_fixed_commission_single_fill(instrument, order_side):
# Arrange
expected = Money(1, USD)
fee_model = FixedFeeModel(expected)
order = TestExecStubs.make_accepted_order(
instrument=instrument,
order_side=OrderSide.BUY,
order_side=order_side,
)


@pytest.fixture()
def sell_order(instrument: Instrument) -> Order:
return TestExecStubs.make_filled_order(
instrument=instrument,
order_side=OrderSide.SELL,
# Act
commission = fee_model.get_commission(
order,
instrument.make_qty(10),
Price.from_str("1.1234"),
instrument,
)

# Assert
assert commission == expected

def test_fixed_commission(buy_order, instrument):

@pytest.mark.parametrize(
"order_side, charge_commission_once, expected_first_fill, expected_next_fill",
[
[OrderSide.BUY, True, Money(1, USD), Money(0, USD)],
[OrderSide.SELL, True, Money(1, USD), Money(0, USD)],
[OrderSide.BUY, False, Money(1, USD), Money(1, USD)],
[OrderSide.SELL, False, Money(1, USD), Money(1, USD)],
],
)
def test_fixed_commission_multiple_fills(
instrument,
order_side,
charge_commission_once,
expected_first_fill,
expected_next_fill,
):
# Arrange
expected = Money(1, USD)
fee_model = FixedFeeModel(expected)
fee_model = FixedFeeModel(
commission=expected_first_fill,
charge_commission_once=charge_commission_once,
)
order = TestExecStubs.make_accepted_order(
instrument=instrument,
order_side=order_side,
)

# Act
commission = fee_model.get_commission(
buy_order,
buy_order.quantity,
commission_first_fill = fee_model.get_commission(
order,
instrument.make_qty(10),
Price.from_str("1.1234"),
instrument,
)
fill = TestEventStubs.order_filled(order=order, instrument=instrument)
order.apply(fill)
commission_next_fill = fee_model.get_commission(
order,
instrument.make_qty(10),
Price.from_str("1.1234"),
instrument,
)

# Assert
assert commission == expected
assert commission_first_fill == expected_first_fill
assert commission_next_fill == expected_next_fill


def test_instrument_percent_commission_maker(instrument, buy_order):
def test_instrument_percent_commission_maker(instrument):
# Arrange
fee_model = MakerTakerFeeModel()
expected = buy_order.quantity * buy_order.price * instrument.maker_fee
order = TestExecStubs.make_filled_order(
instrument=instrument,
order_side=OrderSide.SELL,
)
expected = order.quantity * order.price * instrument.maker_fee

# Act
commission = fee_model.get_commission(
buy_order,
buy_order.quantity,
buy_order.price,
order,
order.quantity,
order.price,
instrument,
)

Expand All @@ -87,16 +127,20 @@ def test_instrument_percent_commission_maker(instrument, buy_order):
assert commission.as_decimal() == expected


def test_instrument_percent_commission_taker(instrument, sell_order):
def test_instrument_percent_commission_taker(instrument):
# Arrange
fee_model = MakerTakerFeeModel()
expected = sell_order.quantity * sell_order.price * instrument.taker_fee
order = TestExecStubs.make_filled_order(
instrument=instrument,
order_side=OrderSide.SELL,
)
expected = order.quantity * order.price * instrument.taker_fee

# Act
commission = fee_model.get_commission(
sell_order,
sell_order.quantity,
sell_order.price,
order,
order.quantity,
order.price,
instrument,
)

Expand Down

0 comments on commit f43613e

Please sign in to comment.