"""Tests for position cost calculations.""" from __future__ import annotations from decimal import Decimal from app.models.position import Position, create_position from app.services.position_costs import ( calculate_effective_entry, calculate_effective_exit, calculate_true_pnl, get_default_premium_for_product, ) class TestEffectiveEntry: """Tests for calculate_effective_entry.""" def test_no_premium_returns_entry_price(self) -> None: """Entry price unchanged when no premium.""" result = calculate_effective_entry(Decimal("2000"), None) assert result == Decimal("2000") def test_zero_premium_returns_entry_price(self) -> None: """Entry price unchanged with zero premium.""" result = calculate_effective_entry(Decimal("2000"), Decimal("0")) assert result == Decimal("2000") def test_premium_adds_to_entry(self) -> None: """Premium adds to entry cost.""" # 4% premium on $2000 = $80, total $2080 result = calculate_effective_entry(Decimal("2000"), Decimal("0.04")) assert result == Decimal("2080") def test_large_premium(self) -> None: """Large premium (10%) calculated correctly.""" result = calculate_effective_entry(Decimal("2000"), Decimal("0.10")) assert result == Decimal("2200") class TestEffectiveExit: """Tests for calculate_effective_exit.""" def test_no_spread_returns_spot(self) -> None: """Spot unchanged when no spread.""" result = calculate_effective_exit(Decimal("2000"), None) assert result == Decimal("2000") def test_zero_spread_returns_spot(self) -> None: """Spot unchanged with zero spread.""" result = calculate_effective_exit(Decimal("2000"), Decimal("0")) assert result == Decimal("2000") def test_spread_subtracts_from_exit(self) -> None: """Spread subtracts from exit value.""" # 3% spread on $2000 = $60, result $1940 result = calculate_effective_exit(Decimal("2000"), Decimal("0.03")) assert result == Decimal("1940") def test_large_spread(self) -> None: """Large spread (5%) calculated correctly.""" result = calculate_effective_exit(Decimal("2000"), Decimal("0.05")) assert result == Decimal("1900") class TestTruePnL: """Tests for calculate_true_pnl.""" def test_gld_position_no_premium_spread(self) -> None: """GLD position with no premium/spread shows paper P&L equals true P&L.""" position = create_position( underlying="GLD", quantity=Decimal("100"), entry_price=Decimal("400"), ) result = calculate_true_pnl(position, Decimal("420")) # Paper P&L: (420 - 400) * 100 = 2000 assert result["paper_pnl"] == 2000.0 assert result["true_pnl"] == 2000.0 assert result["effective_entry"] == 400.0 assert result["effective_exit"] == 420.0 def test_xau_position_with_premium(self) -> None: """Physical gold position with premium shows higher effective entry.""" position = create_position( underlying="XAU", quantity=Decimal("10"), # 10 oz entry_price=Decimal("2000"), purchase_premium=Decimal("0.04"), # 4% ) result = calculate_true_pnl(position, Decimal("2200")) # Effective entry: 2000 * 1.04 = 2080 # Paper P&L: (2200 - 2000) * 10 = 2000 # True P&L: (2200 - 2080) * 10 = 1200 assert result["paper_pnl"] == 2000.0 assert result["true_pnl"] == 1200.0 assert result["effective_entry"] == 2080.0 assert result["premium_impact"] == 800.0 # 4% of 2000 * 10 def test_xau_position_with_spread(self) -> None: """Physical gold position with spread shows lower effective exit.""" position = create_position( underlying="XAU", quantity=Decimal("10"), entry_price=Decimal("2000"), bid_ask_spread=Decimal("0.03"), # 3% ) result = calculate_true_pnl(position, Decimal("2200")) # Effective exit: 2200 * 0.97 = 2134 # Paper P&L: (2200 - 2000) * 10 = 2000 # True P&L: (2134 - 2000) * 10 = 1340 assert result["paper_pnl"] == 2000.0 assert result["true_pnl"] == 1340.0 assert result["effective_exit"] == 2134.0 assert result["spread_impact"] == 660.0 # 3% of 2200 * 10 def test_xau_position_with_both_premium_and_spread(self) -> None: """Physical gold with both premium and spread.""" position = create_position( underlying="XAU", quantity=Decimal("10"), entry_price=Decimal("2000"), purchase_premium=Decimal("0.04"), # 4% bid_ask_spread=Decimal("0.03"), # 3% ) result = calculate_true_pnl(position, Decimal("2200")) # Effective entry: 2000 * 1.04 = 2080 # Effective exit: 2200 * 0.97 = 2134 # Paper P&L: (2200 - 2000) * 10 = 2000 # True P&L: (2134 - 2080) * 10 = 540 assert result["paper_pnl"] == 2000.0 assert result["true_pnl"] == 540.0 assert result["effective_entry"] == 2080.0 assert result["effective_exit"] == 2134.0 assert result["premium_impact"] == 800.0 assert result["spread_impact"] == 660.0 class TestDefaultPremiumForProduct: """Tests for get_default_premium_for_product.""" def test_gld_defaults(self) -> None: """GLD has minimal premium/spread.""" premium, spread = get_default_premium_for_product("GLD") assert premium == Decimal("0") assert spread == Decimal("0.001") def test_gldm_defaults(self) -> None: """GLDM has minimal premium/spread.""" premium, spread = get_default_premium_for_product("GLDM") assert premium == Decimal("0") assert spread == Decimal("0.001") def test_gcf_returns_none(self) -> None: """GC=F futures have no premium/spread (roll costs handled separately).""" result = get_default_premium_for_product("GC=F") assert result == (None, None) def test_xau_default(self) -> None: """XAU default uses coin defaults.""" premium, spread = get_default_premium_for_product("XAU") assert premium == Decimal("0.04") assert spread == Decimal("0.03") def test_xau_coin_1oz(self) -> None: """XAU 1oz coins have 4%/3%.""" premium, spread = get_default_premium_for_product("XAU", "coin_1oz") assert premium == Decimal("0.04") assert spread == Decimal("0.03") def test_xau_bar_1kg(self) -> None: """XAU 1kg bars have 1.5%/1.5%.""" premium, spread = get_default_premium_for_product("XAU", "bar_1kg") assert premium == Decimal("0.015") assert spread == Decimal("0.015") def test_xau_allocated(self) -> None: """XAU allocated storage has minimal 0.1%/0.3%.""" premium, spread = get_default_premium_for_product("XAU", "allocated") assert premium == Decimal("0.001") assert spread == Decimal("0.003") def test_unknown_underlying_returns_none(self) -> None: """Unknown underlying returns None.""" result = get_default_premium_for_product("UNKNOWN") assert result == (None, None) class TestPositionWithPremiumSpread: """Tests for Position model with premium/spread fields.""" def test_create_position_with_premium(self) -> None: """Create position with purchase premium.""" position = create_position( underlying="XAU", quantity=Decimal("10"), entry_price=Decimal("2000"), purchase_premium=Decimal("0.04"), ) assert position.purchase_premium == Decimal("0.04") assert position.bid_ask_spread is None def test_create_position_with_spread(self) -> None: """Create position with bid/ask spread.""" position = create_position( underlying="XAU", quantity=Decimal("10"), entry_price=Decimal("2000"), bid_ask_spread=Decimal("0.03"), ) assert position.bid_ask_spread == Decimal("0.03") assert position.purchase_premium is None def test_create_position_with_both(self) -> None: """Create position with both premium and spread.""" position = create_position( underlying="XAU", quantity=Decimal("10"), entry_price=Decimal("2000"), purchase_premium=Decimal("0.04"), bid_ask_spread=Decimal("0.03"), ) assert position.purchase_premium == Decimal("0.04") assert position.bid_ask_spread == Decimal("0.03") def test_position_serialization_with_premium_spread(self) -> None: """Position serializes premium and spread correctly.""" position = create_position( underlying="XAU", quantity=Decimal("10"), entry_price=Decimal("2000"), purchase_premium=Decimal("0.04"), bid_ask_spread=Decimal("0.03"), ) data = position.to_dict() assert data["purchase_premium"] == "0.04" assert data["bid_ask_spread"] == "0.03" def test_position_deserialization_with_premium_spread(self) -> None: """Position deserializes premium and spread correctly.""" data = { "id": "12345678-1234-5678-1234-567812345678", "underlying": "XAU", "quantity": "10", "unit": "oz", "entry_price": "2000", "entry_date": "2024-01-01", "purchase_premium": "0.04", "bid_ask_spread": "0.03", } position = Position.from_dict(data) assert position.purchase_premium == Decimal("0.04") assert position.bid_ask_spread == Decimal("0.03") def test_position_deserialization_without_premium_spread(self) -> None: """Position deserializes without premium and spread (backward compat).""" data = { "id": "12345678-1234-5678-1234-567812345678", "underlying": "GLD", "quantity": "100", "unit": "shares", "entry_price": "400", "entry_date": "2024-01-01", } position = Position.from_dict(data) assert position.purchase_premium is None assert position.bid_ask_spread is None