diff --git a/src/features/position_models.py b/src/features/position_models.py index 42f60b5..8c2ed98 100644 --- a/src/features/position_models.py +++ b/src/features/position_models.py @@ -236,11 +236,15 @@ def calculate_max_profit(self) -> int: # For spreads and complex strategies, calculate based on strike differences elif self.strategy_type in [StrategyType.BULL_CALL_SPREAD, StrategyType.BEAR_PUT_SPREAD]: + # Debit spread: max profit = spread width - net debit paid. The old + # max(width - debit, debit) fallback returned the debit (i.e. the + # max loss) whenever the debit exceeded half the width, which is + # never the correct max profit. spread_width = abs(self.strikes[1] - self.strikes[0]) - net_credit = sum( - -qty * price for qty, price in zip(self.quantities, self.entry_prices) - ) - return max(spread_width * abs(self.quantities[0]) - abs(net_credit), abs(net_credit)) + net_debit = abs(sum( + qty * price for qty, price in zip(self.quantities, self.entry_prices) + )) + return spread_width * abs(self.quantities[0]) - net_debit elif self.strategy_type in [StrategyType.BEAR_CALL_SPREAD, StrategyType.BULL_PUT_SPREAD]: net_credit = sum( diff --git a/tests/test_position_state.py b/tests/test_position_state.py index 14613ee..c52b9de 100644 --- a/tests/test_position_state.py +++ b/tests/test_position_state.py @@ -91,6 +91,25 @@ def test_max_profit_calculation(self): lc_max = self.long_call.calculate_max_profit() self.assertEqual(lc_max, 0) + def test_debit_spread_max_profit_when_debit_exceeds_half_width(self): + """Bull call spread max profit must be width - debit, even when the + net debit exceeds half the spread width (regression for issue #16).""" + # Width 1000c, net debit 800c (long 900c - short 100c). Max profit is + # 1000 - 800 = 200c. The old max(width-debit, debit) returned 800c. + bull_call_spread = Position( + strategy_type=StrategyType.BULL_CALL_SPREAD, + entry_date=self.entry_date, + expiration_date=self.expiration_date, + strikes=[10000, 11000], + option_types=[OptionType.CALL, OptionType.CALL], + quantities=[1, -1], + entry_prices=[900, 100], + current_prices=[950, 120], + underlying_price_at_entry=10000, + current_underlying_price=10000 + ) + self.assertEqual(bull_call_spread.calculate_max_profit(), 200) + def test_max_loss_calculation(self): """Test maximum loss calculation.""" # Long call max loss is premium paid