feat(PORTFOLIO-003): add premium and spread for physical gold positions

This commit is contained in:
Bu5hm4nn
2026-03-28 23:53:46 +01:00
parent 0e972e9dd6
commit bb06fa7e80
4 changed files with 429 additions and 0 deletions

View File

@@ -0,0 +1,271 @@
"""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