Files
vault-dash/tests/test_instruments.py
Bu5hm4nn dc4ee1f261 feat(CONV-001): add GLD launch date validation, feat(DATA-DB-003): add cache CLI
CONV-001:
- Add GLD_LAUNCH_DATE constant (November 18, 2004)
- Validate reference_date in gld_ounces_per_share()
- Raise ValueError for dates before GLD launch
- Update docstring with valid date range
- Add comprehensive test coverage for edge cases

DATA-DB-003:
- Create scripts/cache_cli.py with three commands:
  - vault-dash cache stats: Show cache statistics
  - vault-dash cache list: List cached entries
  - vault-dash cache clear: Clear all cache files
- Add Makefile targets: cache-stats, cache-list, cache-clear
- Integrate with DatabentoHistoricalPriceSource methods
2026-03-29 12:00:30 +02:00

211 lines
8.5 KiB
Python

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_DATE,
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_launch_date_constant() -> None:
"""Verify GLD launch date constant is November 18, 2004."""
assert GLD_LAUNCH_DATE == date(2004, 11, 18)
def test_gld_ounces_per_share_launch_date_returns_initial_backing() -> None:
"""Verify launch date (2004-11-18) returns exactly 0.10 oz/share."""
launch_backing = gld_ounces_per_share(GLD_LAUNCH_DATE)
assert launch_backing == GLD_INITIAL_OUNCES_PER_SHARE
assert launch_backing == Decimal("0.10")
def test_gld_ounces_per_share_rejects_pre_launch_date() -> None:
"""Verify dates before GLD launch raise ValueError."""
with pytest.raises(ValueError, match="GLD backing data unavailable before"):
gld_ounces_per_share(date(2004, 11, 17)) # Day before launch
with pytest.raises(ValueError, match="GLD backing data unavailable before"):
gld_ounces_per_share(date(2004, 1, 1)) # Early 2004
with pytest.raises(ValueError, match="GLD backing data unavailable before"):
gld_ounces_per_share(date(2003, 12, 31)) # Prior year
def test_gld_ounces_per_share_early_2004_within_year_raises() -> None:
"""Verify dates in 2004 but before November 18 also raise ValueError."""
with pytest.raises(ValueError, match="GLD backing data unavailable"):
gld_ounces_per_share(date(2004, 6, 1)) # June 2004, before launch
def test_gld_ounces_per_share_decay_formula_matches_research() -> None:
"""Verify decay formula matches research examples from docs/GLD_BASIS_RESEARCH.md."""
# 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