from __future__ import annotations from datetime import date from decimal import Decimal import pytest from app.domain.backtesting_math import AssetQuantity, PricePerAsset from app.domain.instruments import ( GC_F_OUNCES_PER_CONTRACT, GLD_EXPENSE_DECAY_RATE, GLD_INITIAL_OUNCES_PER_SHARE, GLD_LAUNCH_YEAR, Underlying, asset_quantity_from_weight, gld_ounces_per_share, instrument_metadata, price_per_weight_from_asset_price, supported_underlyings, weight_from_asset_quantity, ) from app.domain.units import BaseCurrency, Weight, WeightUnit def test_gld_ounces_per_share_decay_formula_matches_research() -> None: """Verify decay formula matches research examples from docs/GLD_BASIS_RESEARCH.md.""" # Launch (2004): should be exactly 0.10 oz/share launch_backing = gld_ounces_per_share(date(2004, 1, 1)) assert launch_backing == GLD_INITIAL_OUNCES_PER_SHARE assert launch_backing == Decimal("0.10") # 2026: should be ~0.0916 oz/share (8.4% decay from 22 years) # Formula: 0.10 * e^(-0.004 * 22) = 0.10 * e^(-0.088) ≈ 0.091576 years_2026 = 2026 - GLD_LAUNCH_YEAR # 22 years expected_2026_decay = Decimal("0.10") * Decimal(str(__import__("math").exp(-0.004 * years_2026))) actual_2026 = gld_ounces_per_share(date(2026, 1, 1)) # Check 2026 backing is approximately 0.0916 (within rounding tolerance) assert abs(float(actual_2026) - 0.0916) < 0.0001 assert actual_2026 == expected_2026_decay def test_gld_ounces_per_share_uses_current_year_by_default() -> None: """Verify default behavior uses today's date.""" current_backing = gld_ounces_per_share() current_year = date.today().year expected_backing = gld_ounces_per_share(date(current_year, 1, 1)) # Should match the current year's calculation assert current_backing == expected_backing def test_gld_decay_rate_is_correct() -> None: """Verify the decay rate constant is 0.4% annually.""" assert GLD_EXPENSE_DECAY_RATE == Decimal("0.004") def test_gld_share_quantity_converts_to_troy_ounce_weight() -> None: """GLD shares convert to weight using expense-adjusted backing (~0.0919 oz/share in 2026).""" quantity = AssetQuantity(amount=Decimal("10"), symbol="GLD") current_backing = gld_ounces_per_share() weight = weight_from_asset_quantity(quantity) expected_weight = current_backing * Decimal("10") assert weight == Weight(amount=expected_weight, unit=WeightUnit.OUNCE_TROY) # Verify it's NOT the old 1.0 oz (which would be wrong) assert weight != Weight(amount=Decimal("1.0"), unit=WeightUnit.OUNCE_TROY) def test_gld_troy_ounce_weight_converts_to_share_quantity() -> None: """Convert 1 troy ounce to GLD shares using expense-adjusted backing.""" # 1 oz should require more than 10 shares now (since each share backs <0.1 oz) weight = Weight(amount=Decimal("1"), unit=WeightUnit.OUNCE_TROY) current_backing = gld_ounces_per_share() quantity = asset_quantity_from_weight("GLD", weight) expected_shares = Decimal("1") / current_backing assert quantity == AssetQuantity(amount=expected_shares, symbol="GLD") # Should be more than 10 shares (approximately 10.87 in 2026) assert quantity.amount > Decimal("10") def test_gld_share_quote_converts_to_ounce_equivalent_spot() -> None: """GLD price converts to gold spot using expense-adjusted backing.""" # At ~$423/GLD share with ~0.0919 oz backing, spot should be ~$4600/oz quote = PricePerAsset(amount=Decimal("422.73"), currency=BaseCurrency.USD, symbol="GLD") current_backing = gld_ounces_per_share() spot = price_per_weight_from_asset_price(quote, per_unit=WeightUnit.OUNCE_TROY) expected_spot = quote.amount / current_backing assert spot.amount == expected_spot assert spot.currency is BaseCurrency.USD assert spot.per_unit is WeightUnit.OUNCE_TROY # Spot should be higher than naive 10:1 conversion ($4227.3) assert spot.amount > Decimal("4227.3") def test_gld_metadata_uses_expense_adjusted_backing() -> None: """Verify GLD metadata uses the dynamic expense-adjusted backing.""" gld_meta = instrument_metadata("GLD") expected_backing = gld_ounces_per_share() assert gld_meta.weight_per_share.amount == expected_backing assert gld_meta.weight_per_share.unit is WeightUnit.OUNCE_TROY # Verify it's not the old hardcoded 0.1 assert gld_meta.weight_per_share.amount != Decimal("0.1") def test_instrument_conversions_fail_closed_for_unsupported_symbols() -> None: quote = PricePerAsset(amount=Decimal("28.50"), currency=BaseCurrency.USD, symbol="SLV") with pytest.raises(ValueError, match="Unsupported instrument metadata"): price_per_weight_from_asset_price(quote, per_unit=WeightUnit.OUNCE_TROY) with pytest.raises(ValueError, match="Unsupported instrument metadata"): weight_from_asset_quantity(AssetQuantity(amount=Decimal("10"), symbol="SLV")) with pytest.raises(ValueError, match="Unsupported instrument metadata"): asset_quantity_from_weight("SLV", Weight(amount=Decimal("1"), unit=WeightUnit.OUNCE_TROY)) # DATA-004: Underlying Instrument Selector tests def test_underlying_enum_has_gld_and_gc_f() -> None: """Verify Underlying enum contains GLD and GC=F.""" assert Underlying.GLD.value == "GLD" assert Underlying.GC_F.value == "GC=F" def test_underlying_display_names() -> None: """Verify Underlying display names are descriptive.""" assert Underlying.GLD.display_name() == "SPDR Gold Shares ETF" assert Underlying.GC_F.display_name() == "Gold Futures (COMEX)" def test_underlying_descriptions() -> None: """Verify Underlying descriptions indicate data source status.""" assert Underlying.GLD.description() == "SPDR Gold Shares ETF (live data via yfinance)" assert Underlying.GC_F.description() == "Gold Futures (coming soon)" def test_supported_underlyings_returns_all() -> None: """Verify supported_underlyings() returns all available choices.""" underlyings = supported_underlyings() assert len(underlyings) == 2 assert Underlying.GLD in underlyings assert Underlying.GC_F in underlyings def test_gc_f_metadata() -> None: """Verify GC=F instrument metadata is correct.""" gc_f_meta = instrument_metadata("GC=F") assert gc_f_meta.symbol == "GC=F" assert gc_f_meta.quote_currency is BaseCurrency.USD assert gc_f_meta.weight_per_share == Weight(amount=GC_F_OUNCES_PER_CONTRACT, unit=WeightUnit.OUNCE_TROY) assert gc_f_meta.weight_per_share.amount == Decimal("100") # 100 troy oz per contract def test_gc_f_contract_specs() -> None: """Verify GC=F contract specifications.""" assert GC_F_OUNCES_PER_CONTRACT == Decimal("100") # 1 contract = 100 oz one_contract = AssetQuantity(amount=Decimal("1"), symbol="GC=F") weight = weight_from_asset_quantity(one_contract) assert weight == Weight(amount=Decimal("100"), unit=WeightUnit.OUNCE_TROY) def test_gc_f_price_per_weight_conversion() -> None: """Verify GC=F price converts correctly to price per weight.""" # GC=F quoted at $2700/oz (already per ounce) quote = PricePerAsset(amount=Decimal("270000"), currency=BaseCurrency.USD, symbol="GC=F") # $2700/oz * 100 oz spot = price_per_weight_from_asset_price(quote, per_unit=WeightUnit.OUNCE_TROY) # Should be $2700/oz assert spot.amount == Decimal("2700") assert spot.currency is BaseCurrency.USD assert spot.per_unit is WeightUnit.OUNCE_TROY