Files
vault-dash/tests/test_storage_costs.py
2026-03-28 23:48:41 +01:00

304 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for storage cost calculations."""
from __future__ import annotations
from decimal import Decimal
from uuid import uuid4
from app.models.position import Position, create_position
from app.services.storage_costs import (
calculate_annual_storage_cost,
calculate_total_storage_cost,
get_default_storage_cost_for_underlying,
)
class TestCalculateAnnualStorageCost:
"""Test calculate_annual_storage_cost function."""
def test_no_storage_cost_returns_zero(self) -> None:
"""Test that position without storage cost returns zero."""
pos = create_position(
underlying="GLD",
quantity=Decimal("100"),
entry_price=Decimal("2150"),
storage_cost_basis=None,
)
current_value = Decimal("215000")
result = calculate_annual_storage_cost(pos, current_value)
assert result == Decimal("0")
def test_percentage_annual_cost(self) -> None:
"""Test percentage-based annual storage cost."""
pos = create_position(
underlying="XAU",
quantity=Decimal("100"),
entry_price=Decimal("2150"),
storage_cost_basis=Decimal("0.12"), # 0.12%
storage_cost_period="annual",
)
current_value = Decimal("215000")
result = calculate_annual_storage_cost(pos, current_value)
# 0.12% of 215000 = 258
expected = Decimal("258")
assert result == expected
def test_percentage_monthly_cost_annualized(self) -> None:
"""Test percentage-based monthly storage cost is annualized."""
pos = create_position(
underlying="XAU",
quantity=Decimal("100"),
entry_price=Decimal("2150"),
storage_cost_basis=Decimal("0.01"), # 0.01% per month
storage_cost_period="monthly",
)
current_value = Decimal("215000")
result = calculate_annual_storage_cost(pos, current_value)
# 0.01% monthly × 12 = 0.12% annual
# 0.12% of 215000 = 258
expected = Decimal("258")
assert result == expected
def test_fixed_annual_cost(self) -> None:
"""Test fixed annual storage cost."""
pos = create_position(
underlying="XAU",
quantity=Decimal("100"),
entry_price=Decimal("2150"),
storage_cost_basis=Decimal("300"), # $300 per year
storage_cost_period="annual",
)
current_value = Decimal("215000")
result = calculate_annual_storage_cost(pos, current_value)
assert result == Decimal("300")
def test_fixed_monthly_cost_annualized(self) -> None:
"""Test fixed monthly storage cost is annualized."""
pos = create_position(
underlying="XAU",
quantity=Decimal("100"),
entry_price=Decimal("2150"),
storage_cost_basis=Decimal("25"), # $25 per month
storage_cost_period="monthly",
)
current_value = Decimal("215000")
result = calculate_annual_storage_cost(pos, current_value)
# $25 × 12 = $300 per year
expected = Decimal("300")
assert result == expected
def test_percentage_boundary_at_one(self) -> None:
"""Test that basis < 1 is treated as percentage, >= 1 as fixed."""
# 0.99 should be treated as percentage
pos_pct = create_position(
underlying="XAU",
quantity=Decimal("100"),
entry_price=Decimal("2150"),
storage_cost_basis=Decimal("0.99"),
storage_cost_period="annual",
)
current_value = Decimal("100000")
result_pct = calculate_annual_storage_cost(pos_pct, current_value)
# 0.99% of 100000 = 990
assert result_pct == Decimal("990")
# 1.00 should be treated as fixed
pos_fixed = create_position(
underlying="XAU",
quantity=Decimal("100"),
entry_price=Decimal("2150"),
storage_cost_basis=Decimal("1"),
storage_cost_period="annual",
)
result_fixed = calculate_annual_storage_cost(pos_fixed, current_value)
assert result_fixed == Decimal("1")
class TestCalculateTotalStorageCost:
"""Test calculate_total_storage_cost function."""
def test_empty_positions_returns_zero(self) -> None:
"""Test that empty position list returns zero."""
result = calculate_total_storage_cost([], {})
assert result == Decimal("0")
def test_multiple_positions_summed(self) -> None:
"""Test that multiple positions have costs summed."""
pos1 = create_position(
underlying="XAU",
quantity=Decimal("50"),
entry_price=Decimal("2150"),
storage_cost_basis=Decimal("0.12"),
storage_cost_period="annual",
)
pos2 = create_position(
underlying="XAU",
quantity=Decimal("50"),
entry_price=Decimal("2150"),
storage_cost_basis=Decimal("0.12"),
storage_cost_period="annual",
)
positions = [pos1, pos2]
current_values = {
str(pos1.id): Decimal("107500"), # 50 × 2150
str(pos2.id): Decimal("107500"),
}
result = calculate_total_storage_cost(positions, current_values)
# Each: 0.12% of 107500 = 129
# Total: 129 + 129 = 258
expected = Decimal("258")
assert result == expected
def test_mixed_positions_with_and_without_costs(self) -> None:
"""Test positions with and without storage costs."""
pos_xau = create_position(
underlying="XAU",
quantity=Decimal("100"),
entry_price=Decimal("2150"),
storage_cost_basis=Decimal("0.12"),
storage_cost_period="annual",
)
pos_gld = create_position(
underlying="GLD",
quantity=Decimal("100"),
entry_price=Decimal("2150"),
storage_cost_basis=None, # GLD has no storage cost
)
positions = [pos_xau, pos_gld]
current_values = {
str(pos_xau.id): Decimal("215000"),
str(pos_gld.id): Decimal("215000"),
}
result = calculate_total_storage_cost(positions, current_values)
# Only XAU position has cost: 0.12% of 215000 = 258
expected = Decimal("258")
assert result == expected
class TestGetDefaultStorageCostForUnderlying:
"""Test get_default_storage_cost_for_underlying function."""
def test_xau_default(self) -> None:
"""Test XAU gets 0.12% annual default."""
basis, period = get_default_storage_cost_for_underlying("XAU")
assert basis == Decimal("0.12")
assert period == "annual"
def test_gld_default(self) -> None:
"""Test GLD gets no storage cost (expense ratio in price)."""
basis, period = get_default_storage_cost_for_underlying("GLD")
assert basis is None
assert period is None
def test_gc_f_default(self) -> None:
"""Test GC=F gets no storage cost (roll costs deferred)."""
basis, period = get_default_storage_cost_for_underlying("GC=F")
assert basis is None
assert period is None
def test_unknown_underlying_default(self) -> None:
"""Test unknown underlying gets no storage cost."""
basis, period = get_default_storage_cost_for_underlying("UNKNOWN")
assert basis is None
assert period is None
class TestPositionStorageCostFields:
"""Test Position model storage cost fields."""
def test_position_with_storage_cost_fields(self) -> None:
"""Test Position can be created with storage cost fields."""
pos = Position(
id=uuid4(),
underlying="XAU",
quantity=Decimal("100"),
unit="oz",
entry_price=Decimal("2150"),
entry_date="2025-01-01",
storage_cost_basis=Decimal("0.12"),
storage_cost_period="annual",
storage_cost_currency="USD",
)
assert pos.storage_cost_basis == Decimal("0.12")
assert pos.storage_cost_period == "annual"
assert pos.storage_cost_currency == "USD"
def test_position_default_storage_cost_fields(self) -> None:
"""Test Position defaults for storage cost fields."""
pos = create_position(
underlying="XAU",
quantity=Decimal("100"),
entry_price=Decimal("2150"),
)
assert pos.storage_cost_basis is None
assert pos.storage_cost_period is None
assert pos.storage_cost_currency == "USD"
def test_position_serialization_with_storage_costs(self) -> None:
"""Test Position serialization includes storage cost fields."""
pos = create_position(
underlying="XAU",
quantity=Decimal("100"),
entry_price=Decimal("2150"),
storage_cost_basis=Decimal("0.12"),
storage_cost_period="annual",
)
data = pos.to_dict()
assert data["storage_cost_basis"] == "0.12"
assert data["storage_cost_period"] == "annual"
assert data["storage_cost_currency"] == "USD"
def test_position_deserialization_with_storage_costs(self) -> None:
"""Test Position deserialization restores storage cost fields."""
pos = create_position(
underlying="XAU",
quantity=Decimal("100"),
entry_price=Decimal("2150"),
storage_cost_basis=Decimal("0.12"),
storage_cost_period="annual",
)
data = pos.to_dict()
restored = Position.from_dict(data)
assert restored.storage_cost_basis == Decimal("0.12")
assert restored.storage_cost_period == "annual"
assert restored.storage_cost_currency == "USD"
def test_position_serialization_with_null_storage_costs(self) -> None:
"""Test Position serialization handles null storage cost fields."""
pos = create_position(
underlying="GLD",
quantity=Decimal("100"),
entry_price=Decimal("2150"),
storage_cost_basis=None,
)
data = pos.to_dict()
assert data["storage_cost_basis"] is None
assert data["storage_cost_period"] is None
assert data["storage_cost_currency"] == "USD"