Skip to content
Open
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
12 changes: 8 additions & 4 deletions src/features/position_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
19 changes: 19 additions & 0 deletions tests/test_position_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down