Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed Commission Charging Behavior in FixedFeeModel #1595

Merged
merged 2 commits into from
Apr 16, 2024
Merged
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
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