diff --git a/src/ml4t/backtest/core/fill_engine.py b/src/ml4t/backtest/core/fill_engine.py index 60a7f64..1b05301 100644 --- a/src/ml4t/backtest/core/fill_engine.py +++ b/src/ml4t/backtest/core/fill_engine.py @@ -40,6 +40,22 @@ def get_max_affordable_quantity(self, order, fill_price: float) -> float: if fill_price <= 0 or order.quantity <= 0: return 0.0 + if self.broker.share_type == ShareType.INTEGER: + high_int = int(order.quantity) + if high_int <= 0: + return 0.0 + if self._can_afford_quantity(order, fill_price, float(high_int)): + return float(high_int) + + low_int = 0 + while low_int < high_int: + mid_int = (low_int + high_int + 1) // 2 + if self._can_afford_quantity(order, fill_price, float(mid_int)): + low_int = mid_int + else: + high_int = mid_int - 1 + return float(low_int) + high = order.quantity if self._can_afford_quantity(order, fill_price, high): return high @@ -51,8 +67,6 @@ def get_max_affordable_quantity(self, order, fill_price: float) -> float: low = mid else: high = mid - if self.broker.share_type == ShareType.INTEGER: - return low return max(0.0, low - CASH_TOLERANCE / fill_price) def try_partial_fill(self, order, fill_price: float) -> bool: diff --git a/tests/test_config_wiring.py b/tests/test_config_wiring.py index 34ab491..041a2b2 100644 --- a/tests/test_config_wiring.py +++ b/tests/test_config_wiring.py @@ -498,6 +498,22 @@ def test_partial_fill_with_integer_shares(self): assert pos.quantity == int(pos.quantity) assert pos.quantity == 52.0 # floor(5250/100) + def test_integer_partial_fill_preserves_exact_affordability_boundary(self): + broker = _make_broker( + initial_cash=5_200.0, + partial_fills_allowed=True, + share_type=ShareType.INTEGER, + ) + _set_prices(broker, {"AAPL": 100.0}) + + broker.submit_order("AAPL", 100, OrderSide.BUY) + broker._process_orders() + + pos = broker.get_position("AAPL") + assert pos is not None + assert pos.quantity == 52.0 + assert broker.cash == 0.0 + def test_partial_fill_accounts_for_commission_minimum(self): broker = _make_broker( initial_cash=5_005.0,