|
| 1 | +from decimal import Decimal |
| 2 | +from unittest.mock import MagicMock |
| 3 | + |
1 | 4 | from lumibot.entities import TradingFee, TradingSlippage |
2 | 5 |
|
3 | 6 |
|
4 | 7 | class TestTradingFee: |
5 | 8 | def test_init(self): |
6 | 9 | 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") |
8 | 26 |
|
9 | 27 | def test_slippage_init(self): |
10 | 28 | slippage = TradingSlippage(amount=0.15) |
11 | 29 | 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