Skip to content

Commit 99fcfa6

Browse files
authored
Merge pull request #974 from Lumiwealth/version/4.4.54
v4.4.54 - Per-contract trading fee accounting
2 parents 593b5bd + 721fab1 commit 99fcfa6

File tree

4 files changed

+110
-6
lines changed

4 files changed

+110
-6
lines changed

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
# Changelog
22

3-
## 4.4.54 - Unreleased
3+
## 4.4.54 - 2026-03-08
4+
5+
### Added
6+
- `TradingFee` now supports `per_contract_fee` for broker-style option commissions charged per contract.
7+
- Regression tests for `per_contract_fee` initialization and trade-cost calculations in backtesting.
8+
9+
### Changed
10+
- `TradingFee` fee fields now coerce through `Decimal(str(...))` for stable decimal handling across float inputs.
11+
12+
### Fixed
13+
- Backtesting trade-cost calculations now apply `per_contract_fee * quantity` for taker and maker fee paths (`market`, `stop`, `limit`, `stop_limit`, `smart_limit`).
414

515
## 4.4.53 - 2026-03-06
616

lumibot/backtesting/backtesting_broker.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1869,9 +1869,11 @@ def calculate_trade_cost(self, order: Order, strategy, price: float):
18691869
if trading_fee.taker is True and order_type_value in {"market", "stop"}:
18701870
trade_cost += trading_fee.flat_fee
18711871
trade_cost += Decimal(str(price)) * Decimal(str(order.quantity)) * trading_fee.percent_fee
1872+
trade_cost += Decimal(str(order.quantity)) * trading_fee.per_contract_fee
18721873
elif trading_fee.maker is True and order_type_value in {"limit", "stop_limit", "smart_limit"}:
18731874
trade_cost += trading_fee.flat_fee
18741875
trade_cost += Decimal(str(price)) * Decimal(str(order.quantity)) * trading_fee.percent_fee
1876+
trade_cost += Decimal(str(order.quantity)) * trading_fee.per_contract_fee
18751877

18761878
return trade_cost
18771879

lumibot/entities/trading_fee.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,18 @@
44
class TradingFee:
55
"""TradingFee class. Used to define the trading fees for a broker in a strategy/backtesting."""
66

7-
def __init__(self, flat_fee=0.0, percent_fee=0.0, maker=True, taker=True):
7+
def __init__(self, flat_fee=0.0, percent_fee=0.0, per_contract_fee=0.0, maker=True, taker=True):
88
"""
99
Parameters
1010
----------
1111
flat_fee : Decimal, float, or None
1212
Flat fee to pay for each order. This is a fixed fee that is paid for each order in the quote currency.
1313
percent_fee : Decimal, float, or None
1414
Percentage fee to pay for each order. This is a percentage of the order value that is paid for each order in the quote currency.
15+
per_contract_fee : Decimal, float, or None
16+
Fee charged per contract (multiplied by order quantity). Useful for options commissions
17+
where brokers charge per contract (e.g., $0.65/contract). For a 40-contract order with
18+
per_contract_fee=0.65, the total fee would be $26.00.
1519
maker : bool
1620
Whether this fee is a maker fee (applies to limit orders).
1721
Default is True, which means that this fee will be used on limit orders.
@@ -28,8 +32,9 @@ def __init__(self, flat_fee=0.0, percent_fee=0.0, maker=True, taker=True):
2832
>>> class MyStrategy(Strategy):
2933
>>> pass
3034
>>>
31-
>>> trading_fee_1 = TradingFee(flat_fee=5.2) # $5.20 flat fee
35+
>>> trading_fee_1 = TradingFee(flat_fee=5.2) # $5.20 flat fee per order
3236
>>> trading_fee_2 = TradingFee(percent_fee=0.01) # 1% fee
37+
>>> trading_fee_3 = TradingFee(per_contract_fee=0.65) # $0.65 per contract
3338
>>> backtesting_start = datetime(2022, 1, 1)
3439
>>> backtesting_end = datetime(2022, 6, 1)
3540
>>> result = MyStrategy.backtest(
@@ -39,7 +44,8 @@ def __init__(self, flat_fee=0.0, percent_fee=0.0, maker=True, taker=True):
3944
>>> buy_trading_fees=[trading_fee_1, trading_fee_2],
4045
>>> )
4146
"""
42-
self.flat_fee = Decimal(flat_fee)
43-
self.percent_fee = Decimal(percent_fee)
47+
self.flat_fee = Decimal(str(flat_fee))
48+
self.percent_fee = Decimal(str(percent_fee))
49+
self.per_contract_fee = Decimal(str(per_contract_fee))
4450
self.maker = maker
4551
self.taker = taker

tests/test_tradingfee.py

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,97 @@
1+
from decimal import Decimal
2+
from unittest.mock import MagicMock
3+
14
from lumibot.entities import TradingFee, TradingSlippage
25

36

47
class TestTradingFee:
58
def test_init(self):
69
fee = TradingFee(flat_fee=5.2)
7-
assert fee.flat_fee == 5.2
10+
assert fee.flat_fee == Decimal("5.2")
11+
12+
def test_per_contract_fee_init(self):
13+
fee = TradingFee(per_contract_fee=0.65)
14+
assert fee.per_contract_fee == Decimal("0.65")
15+
assert fee.flat_fee == Decimal("0")
16+
assert fee.percent_fee == Decimal("0")
17+
18+
def test_per_contract_fee_default_zero(self):
19+
fee = TradingFee(flat_fee=1.0)
20+
assert fee.per_contract_fee == Decimal("0")
21+
22+
def test_per_contract_fee_with_flat_fee(self):
23+
fee = TradingFee(flat_fee=5.0, per_contract_fee=0.65)
24+
assert fee.flat_fee == Decimal("5.0")
25+
assert fee.per_contract_fee == Decimal("0.65")
826

927
def test_slippage_init(self):
1028
slippage = TradingSlippage(amount=0.15)
1129
assert slippage.amount == 0.15
30+
31+
32+
class TestPerContractFeeCalculation:
33+
"""Test that per_contract_fee is correctly multiplied by order quantity in calculate_trade_cost."""
34+
35+
def _make_order(self, side="sell_to_open", order_type="market", quantity=40):
36+
order = MagicMock()
37+
order.side = side
38+
order.order_type = MagicMock()
39+
order.order_type.value = order_type
40+
order.quantity = quantity
41+
return order
42+
43+
def _make_strategy(self, buy_fees=None, sell_fees=None):
44+
strategy = MagicMock()
45+
strategy.buy_trading_fees = buy_fees or []
46+
strategy.sell_trading_fees = sell_fees or []
47+
return strategy
48+
49+
def test_per_contract_fee_multiplied_by_quantity(self):
50+
"""$0.65/contract on a 40-contract order should cost $26.00."""
51+
from lumibot.backtesting.backtesting_broker import BacktestingBroker
52+
53+
broker = BacktestingBroker.__new__(BacktestingBroker)
54+
fee = TradingFee(per_contract_fee=0.65)
55+
order = self._make_order(side="sell_to_open", order_type="market", quantity=40)
56+
strategy = self._make_strategy(sell_fees=[fee])
57+
58+
cost = broker.calculate_trade_cost(order, strategy, price=1.50)
59+
assert cost == Decimal("26.00")
60+
61+
def test_per_contract_fee_with_flat_fee(self):
62+
"""Both flat_fee and per_contract_fee should apply."""
63+
from lumibot.backtesting.backtesting_broker import BacktestingBroker
64+
65+
broker = BacktestingBroker.__new__(BacktestingBroker)
66+
fee = TradingFee(flat_fee=5.0, per_contract_fee=0.65)
67+
order = self._make_order(side="buy_to_open", order_type="market", quantity=10)
68+
strategy = self._make_strategy(buy_fees=[fee])
69+
70+
cost = broker.calculate_trade_cost(order, strategy, price=2.00)
71+
# flat_fee=5.0 + per_contract=10*0.65=6.50 + percent=0 = 11.50
72+
assert cost == Decimal("11.50")
73+
74+
def test_per_contract_fee_on_limit_order(self):
75+
"""Per-contract fee should work with limit/smart_limit orders too."""
76+
from lumibot.backtesting.backtesting_broker import BacktestingBroker
77+
78+
broker = BacktestingBroker.__new__(BacktestingBroker)
79+
fee = TradingFee(per_contract_fee=0.65)
80+
order = self._make_order(side="sell_to_open", order_type="smart_limit", quantity=20)
81+
strategy = self._make_strategy(sell_fees=[fee])
82+
83+
cost = broker.calculate_trade_cost(order, strategy, price=3.00)
84+
assert cost == Decimal("13.00") # 20 * 0.65
85+
86+
def test_old_flat_fee_behavior_unchanged(self):
87+
"""Existing flat_fee behavior should NOT change (still per-order, not per-contract)."""
88+
from lumibot.backtesting.backtesting_broker import BacktestingBroker
89+
90+
broker = BacktestingBroker.__new__(BacktestingBroker)
91+
fee = TradingFee(flat_fee=0.65)
92+
order = self._make_order(side="sell_to_open", order_type="market", quantity=40)
93+
strategy = self._make_strategy(sell_fees=[fee])
94+
95+
cost = broker.calculate_trade_cost(order, strategy, price=1.50)
96+
# flat_fee is per-order, so $0.65 regardless of quantity
97+
assert cost == Decimal("0.65")

0 commit comments

Comments
 (0)