feat(PRICING-001): add GLD expense ratio decay correction
This commit is contained in:
@@ -1,11 +1,49 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from datetime import date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from app.domain.backtesting_math import AssetQuantity, PricePerAsset
|
from app.domain.backtesting_math import AssetQuantity, PricePerAsset
|
||||||
from app.domain.units import BaseCurrency, PricePerWeight, Weight, WeightUnit
|
from app.domain.units import BaseCurrency, PricePerWeight, Weight, WeightUnit
|
||||||
|
|
||||||
|
# GLD expense ratio decay parameters (from docs/GLD_BASIS_RESEARCH.md)
|
||||||
|
# Formula: ounces_per_share = 0.10 * e^(-0.004 * years_since_2004)
|
||||||
|
GLD_INITIAL_OUNCES_PER_SHARE = Decimal("0.10")
|
||||||
|
GLD_EXPENSE_DECAY_RATE = Decimal("0.004") # 0.4% annual decay
|
||||||
|
GLD_LAUNCH_YEAR = 2004
|
||||||
|
|
||||||
|
|
||||||
|
def gld_ounces_per_share(reference_date: date | None = None) -> Decimal:
|
||||||
|
"""
|
||||||
|
Calculate GLD's current gold backing per share based on expense ratio decay.
|
||||||
|
|
||||||
|
GLD's expense ratio (0.40% annually) causes the gold backing per share to
|
||||||
|
decay exponentially from the initial 0.10 oz/share at launch (2004).
|
||||||
|
|
||||||
|
Formula: ounces_per_share = 0.10 * e^(-0.004 * years_since_2004)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reference_date: Date to calculate backing for. Defaults to today.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Decimal representing troy ounces of gold backing per GLD share.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
>>> # 2026 backing should be ~0.0919 oz/share (8.1% decay)
|
||||||
|
>>> from datetime import date
|
||||||
|
>>> result = gld_ounces_per_share(date(2026, 1, 1))
|
||||||
|
>>> float(result) # doctest: +SKIP
|
||||||
|
0.0919...
|
||||||
|
"""
|
||||||
|
if reference_date is None:
|
||||||
|
reference_date = date.today()
|
||||||
|
|
||||||
|
years_since_launch = Decimal(reference_date.year - GLD_LAUNCH_YEAR)
|
||||||
|
decay_factor = Decimal(str(math.exp(-float(GLD_EXPENSE_DECAY_RATE * years_since_launch))))
|
||||||
|
return GLD_INITIAL_OUNCES_PER_SHARE * decay_factor
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class InstrumentMetadata:
|
class InstrumentMetadata:
|
||||||
@@ -64,7 +102,7 @@ class InstrumentMetadata:
|
|||||||
_GLD = InstrumentMetadata(
|
_GLD = InstrumentMetadata(
|
||||||
symbol="GLD",
|
symbol="GLD",
|
||||||
quote_currency=BaseCurrency.USD,
|
quote_currency=BaseCurrency.USD,
|
||||||
weight_per_share=Weight(amount=Decimal("0.1"), unit=WeightUnit.OUNCE_TROY),
|
weight_per_share=Weight(amount=gld_ounces_per_share(), unit=WeightUnit.OUNCE_TROY),
|
||||||
)
|
)
|
||||||
|
|
||||||
_INSTRUMENTS: dict[str, InstrumentMetadata] = {
|
_INSTRUMENTS: dict[str, InstrumentMetadata] = {
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ def test_asset_quantity_from_floats_matches_workspace_backtest_conversion() -> N
|
|||||||
|
|
||||||
|
|
||||||
def test_asset_quantity_from_workspace_config_uses_instrument_weight_conversion_for_gld() -> None:
|
def test_asset_quantity_from_workspace_config_uses_instrument_weight_conversion_for_gld() -> None:
|
||||||
|
"""GLD shares are calculated using expense-adjusted backing (~0.0916 oz/share in 2026)."""
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from app.domain.instruments import gld_ounces_per_share
|
||||||
|
|
||||||
config = PortfolioConfig(
|
config = PortfolioConfig(
|
||||||
gold_ounces=220.0,
|
gold_ounces=220.0,
|
||||||
entry_price=4400.0,
|
entry_price=4400.0,
|
||||||
@@ -60,7 +65,12 @@ def test_asset_quantity_from_workspace_config_uses_instrument_weight_conversion_
|
|||||||
|
|
||||||
quantity = asset_quantity_from_workspace_config(config, entry_spot=100.0, symbol="GLD")
|
quantity = asset_quantity_from_workspace_config(config, entry_spot=100.0, symbol="GLD")
|
||||||
|
|
||||||
assert quantity == 2200.0
|
# 220 oz / 0.091576... oz/share ≈ 2402.37 shares (NOT 2200 with old 0.1 ratio)
|
||||||
|
current_backing = float(gld_ounces_per_share(date.today()))
|
||||||
|
expected_shares = 220.0 / current_backing
|
||||||
|
assert abs(quantity - expected_shares) < 0.0001
|
||||||
|
# Verify it's more than the old 2200 shares
|
||||||
|
assert quantity > 2200.0
|
||||||
|
|
||||||
|
|
||||||
def test_materialize_backtest_portfolio_state_uses_typed_asset_boundary() -> None:
|
def test_materialize_backtest_portfolio_state_uses_typed_asset_boundary() -> None:
|
||||||
|
|||||||
@@ -62,7 +62,11 @@ def test_cost_benefit_chart_shows_positive_downside_benefit_when_puts_are_in_the
|
|||||||
|
|
||||||
|
|
||||||
def test_hedge_quote_resolution_converts_gld_share_price_to_ozt_spot() -> None:
|
def test_hedge_quote_resolution_converts_gld_share_price_to_ozt_spot() -> None:
|
||||||
"""Hedge page should convert GLD share quotes to USD/ozt for display."""
|
"""Hedge page should convert GLD share quotes to USD/ozt using expense-adjusted backing."""
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from app.domain.instruments import gld_ounces_per_share
|
||||||
|
|
||||||
config = PortfolioConfig(entry_price=4400.0, gold_ounces=220.0, entry_basis_mode="weight", loan_amount=145000.0)
|
config = PortfolioConfig(entry_price=4400.0, gold_ounces=220.0, entry_basis_mode="weight", loan_amount=145000.0)
|
||||||
share_quote = {
|
share_quote = {
|
||||||
"symbol": "GLD",
|
"symbol": "GLD",
|
||||||
@@ -74,7 +78,10 @@ def test_hedge_quote_resolution_converts_gld_share_price_to_ozt_spot() -> None:
|
|||||||
|
|
||||||
spot, source, updated_at = resolve_portfolio_spot_from_quote(config, share_quote)
|
spot, source, updated_at = resolve_portfolio_spot_from_quote(config, share_quote)
|
||||||
|
|
||||||
assert spot == 4041.9
|
# With expense-adjusted backing (~0.0916 oz/share), spot = 404.19 / 0.091576... ≈ 4413.71
|
||||||
|
current_backing = float(gld_ounces_per_share(date.today()))
|
||||||
|
expected_spot = 404.19 / current_backing
|
||||||
|
assert abs(spot - expected_spot) < 0.01
|
||||||
assert source == "yfinance"
|
assert source == "yfinance"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,109 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from app.domain.backtesting_math import AssetQuantity, PricePerAsset
|
from app.domain.backtesting_math import AssetQuantity, PricePerAsset
|
||||||
from app.domain.instruments import (
|
from app.domain.instruments import (
|
||||||
|
GLD_EXPENSE_DECAY_RATE,
|
||||||
|
GLD_INITIAL_OUNCES_PER_SHARE,
|
||||||
|
GLD_LAUNCH_YEAR,
|
||||||
asset_quantity_from_weight,
|
asset_quantity_from_weight,
|
||||||
|
gld_ounces_per_share,
|
||||||
|
instrument_metadata,
|
||||||
price_per_weight_from_asset_price,
|
price_per_weight_from_asset_price,
|
||||||
weight_from_asset_quantity,
|
weight_from_asset_quantity,
|
||||||
)
|
)
|
||||||
from app.domain.units import BaseCurrency, Weight, WeightUnit
|
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:
|
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")
|
quantity = AssetQuantity(amount=Decimal("10"), symbol="GLD")
|
||||||
|
current_backing = gld_ounces_per_share()
|
||||||
|
|
||||||
weight = weight_from_asset_quantity(quantity)
|
weight = weight_from_asset_quantity(quantity)
|
||||||
|
|
||||||
assert weight == Weight(amount=Decimal("1.0"), unit=WeightUnit.OUNCE_TROY)
|
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:
|
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)
|
weight = Weight(amount=Decimal("1"), unit=WeightUnit.OUNCE_TROY)
|
||||||
|
current_backing = gld_ounces_per_share()
|
||||||
|
|
||||||
quantity = asset_quantity_from_weight("GLD", weight)
|
quantity = asset_quantity_from_weight("GLD", weight)
|
||||||
|
|
||||||
assert quantity == AssetQuantity(amount=Decimal("10"), symbol="GLD")
|
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:
|
def test_gld_share_quote_converts_to_ounce_equivalent_spot() -> None:
|
||||||
quote = PricePerAsset(amount=Decimal("404.19"), currency=BaseCurrency.USD, symbol="GLD")
|
"""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)
|
spot = price_per_weight_from_asset_price(quote, per_unit=WeightUnit.OUNCE_TROY)
|
||||||
|
|
||||||
assert spot.amount == Decimal("4041.9")
|
expected_spot = quote.amount / current_backing
|
||||||
|
assert spot.amount == expected_spot
|
||||||
assert spot.currency is BaseCurrency.USD
|
assert spot.currency is BaseCurrency.USD
|
||||||
assert spot.per_unit is WeightUnit.OUNCE_TROY
|
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:
|
def test_instrument_conversions_fail_closed_for_unsupported_symbols() -> None:
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from app.domain.portfolio_math import resolve_portfolio_spot_from_quote
|
from app.domain.portfolio_math import resolve_portfolio_spot_from_quote
|
||||||
from app.models.portfolio import PortfolioConfig
|
from app.models.portfolio import PortfolioConfig
|
||||||
from app.services.alerts import build_portfolio_alert_context
|
from app.services.alerts import build_portfolio_alert_context
|
||||||
|
|
||||||
|
|
||||||
def test_overview_converts_gld_share_quote_to_ounce_equivalent_spot() -> None:
|
def test_overview_converts_gld_share_quote_to_ounce_equivalent_spot() -> None:
|
||||||
|
"""Overview page converts GLD share quotes using expense-adjusted backing."""
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from app.domain.instruments import gld_ounces_per_share
|
||||||
|
|
||||||
config = PortfolioConfig(entry_price=4400.0, gold_ounces=220.0, entry_basis_mode="weight", loan_amount=145000.0)
|
config = PortfolioConfig(entry_price=4400.0, gold_ounces=220.0, entry_basis_mode="weight", loan_amount=145000.0)
|
||||||
|
|
||||||
spot_price, source, updated_at = resolve_portfolio_spot_from_quote(
|
spot_price, source, updated_at = resolve_portfolio_spot_from_quote(
|
||||||
@@ -25,11 +32,16 @@ def test_overview_converts_gld_share_quote_to_ounce_equivalent_spot() -> None:
|
|||||||
updated_at=updated_at,
|
updated_at=updated_at,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert spot_price == 4041.9
|
# With expense-adjusted backing (~0.0916 oz/share), spot = 404.19 / 0.091576... ≈ 4413.71
|
||||||
|
current_backing = float(gld_ounces_per_share(date.today()))
|
||||||
|
expected_spot = 404.19 / current_backing
|
||||||
|
assert abs(spot_price - expected_spot) < 0.01
|
||||||
assert source == "yfinance"
|
assert source == "yfinance"
|
||||||
assert updated_at == "2026-03-24T00:00:00+00:00"
|
assert updated_at == "2026-03-24T00:00:00+00:00"
|
||||||
assert portfolio["gold_value"] == 889218.0
|
# Gold value = 220 oz * spot_price
|
||||||
assert portfolio["net_equity"] == 744218.0
|
expected_gold_value = 220.0 * spot_price
|
||||||
|
assert abs(portfolio["gold_value"] - expected_gold_value) < 0.01
|
||||||
|
assert portfolio["net_equity"] == pytest.approx(expected_gold_value - 145000.0, abs=0.01)
|
||||||
assert round(float(portfolio["margin_call_price"]), 2) == 878.79
|
assert round(float(portfolio["margin_call_price"]), 2) == 878.79
|
||||||
|
|
||||||
|
|
||||||
@@ -53,6 +65,11 @@ def test_overview_fails_closed_to_configured_entry_price_for_unsupported_quote_s
|
|||||||
|
|
||||||
|
|
||||||
def test_overview_uses_fallback_symbol_when_quote_payload_omits_symbol() -> None:
|
def test_overview_uses_fallback_symbol_when_quote_payload_omits_symbol() -> None:
|
||||||
|
"""GLD quote with fallback symbol uses expense-adjusted backing for conversion."""
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from app.domain.instruments import gld_ounces_per_share
|
||||||
|
|
||||||
config = PortfolioConfig(entry_price=4400.0, gold_ounces=220.0, entry_basis_mode="weight", loan_amount=145000.0)
|
config = PortfolioConfig(entry_price=4400.0, gold_ounces=220.0, entry_basis_mode="weight", loan_amount=145000.0)
|
||||||
|
|
||||||
spot_price, source, updated_at = resolve_portfolio_spot_from_quote(
|
spot_price, source, updated_at = resolve_portfolio_spot_from_quote(
|
||||||
@@ -61,7 +78,10 @@ def test_overview_uses_fallback_symbol_when_quote_payload_omits_symbol() -> None
|
|||||||
fallback_symbol="GLD",
|
fallback_symbol="GLD",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert spot_price == 4041.9
|
# With expense-adjusted backing (~0.0916 oz/share), spot = 404.19 / 0.091576... ≈ 4413.71
|
||||||
|
current_backing = float(gld_ounces_per_share(date.today()))
|
||||||
|
expected_spot = 404.19 / current_backing
|
||||||
|
assert abs(spot_price - expected_spot) < 0.01
|
||||||
assert source == "yfinance"
|
assert source == "yfinance"
|
||||||
assert updated_at == "2026-03-24T00:00:00+00:00"
|
assert updated_at == "2026-03-24T00:00:00+00:00"
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from app.domain.backtesting_math import PricePerAsset
|
from app.domain.backtesting_math import PricePerAsset
|
||||||
from app.domain.instruments import price_per_weight_from_asset_price
|
from app.domain.instruments import price_per_weight_from_asset_price
|
||||||
from app.domain.portfolio_math import (
|
from app.domain.portfolio_math import (
|
||||||
@@ -45,10 +47,20 @@ def test_build_alert_context_uses_unit_safe_gold_value_calculation() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_build_alert_context_accepts_explicit_gld_share_quote_conversion() -> None:
|
def test_build_alert_context_accepts_explicit_gld_share_quote_conversion() -> None:
|
||||||
|
"""GLD share quotes convert to spot using expense-adjusted backing."""
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from app.domain.instruments import gld_ounces_per_share
|
||||||
|
|
||||||
config = PortfolioConfig(entry_price=4400.0, gold_ounces=220.0, entry_basis_mode="weight", loan_amount=145000.0)
|
config = PortfolioConfig(entry_price=4400.0, gold_ounces=220.0, entry_basis_mode="weight", loan_amount=145000.0)
|
||||||
share_quote = PricePerAsset(amount=Decimal("404.19"), currency=BaseCurrency.USD, symbol="GLD")
|
share_quote = PricePerAsset(amount=Decimal("404.19"), currency=BaseCurrency.USD, symbol="GLD")
|
||||||
ounce_spot = price_per_weight_from_asset_price(share_quote, per_unit=WeightUnit.OUNCE_TROY)
|
ounce_spot = price_per_weight_from_asset_price(share_quote, per_unit=WeightUnit.OUNCE_TROY)
|
||||||
|
|
||||||
|
# With expense-adjusted backing (~0.0916 oz/share), spot = 404.19 / 0.091576... ≈ 4413.71
|
||||||
|
current_backing = float(gld_ounces_per_share(date.today()))
|
||||||
|
expected_spot = 404.19 / current_backing
|
||||||
|
assert abs(float(ounce_spot.amount) - expected_spot) < 0.01
|
||||||
|
|
||||||
context = build_alert_context(
|
context = build_alert_context(
|
||||||
config,
|
config,
|
||||||
spot_price=float(ounce_spot.amount),
|
spot_price=float(ounce_spot.amount),
|
||||||
@@ -56,9 +68,9 @@ def test_build_alert_context_accepts_explicit_gld_share_quote_conversion() -> No
|
|||||||
updated_at="2026-03-24T00:00:00+00:00",
|
updated_at="2026-03-24T00:00:00+00:00",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert context["spot_price"] == 4041.9
|
assert abs(context["spot_price"] - expected_spot) < 0.01
|
||||||
assert context["gold_value"] == 889218.0
|
assert context["gold_value"] == pytest.approx(220.0 * expected_spot, abs=0.01)
|
||||||
assert context["net_equity"] == 744218.0
|
assert context["net_equity"] == pytest.approx(220.0 * expected_spot - 145000.0, abs=0.01)
|
||||||
assert context["quote_source"] == "yfinance"
|
assert context["quote_source"] == "yfinance"
|
||||||
|
|
||||||
|
|
||||||
@@ -95,7 +107,10 @@ def test_strategy_metrics_from_snapshot_preserves_minus_20pct_protective_put_exa
|
|||||||
|
|
||||||
|
|
||||||
def test_resolve_portfolio_spot_from_quote_converts_gld_share_to_ozt() -> None:
|
def test_resolve_portfolio_spot_from_quote_converts_gld_share_to_ozt() -> None:
|
||||||
"""Hedge/runtime quote resolution should convert GLD share quotes to USD/ozt."""
|
"""Hedge/runtime quote resolution should convert GLD share quotes to USD/ozt using expense-adjusted backing."""
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from app.domain.instruments import gld_ounces_per_share
|
||||||
from app.models.portfolio import PortfolioConfig
|
from app.models.portfolio import PortfolioConfig
|
||||||
|
|
||||||
config = PortfolioConfig(entry_price=4400.0, gold_ounces=220.0, entry_basis_mode="weight", loan_amount=145000.0)
|
config = PortfolioConfig(entry_price=4400.0, gold_ounces=220.0, entry_basis_mode="weight", loan_amount=145000.0)
|
||||||
@@ -109,7 +124,10 @@ def test_resolve_portfolio_spot_from_quote_converts_gld_share_to_ozt() -> None:
|
|||||||
|
|
||||||
spot, source, updated_at = resolve_portfolio_spot_from_quote(config, share_quote)
|
spot, source, updated_at = resolve_portfolio_spot_from_quote(config, share_quote)
|
||||||
|
|
||||||
assert spot == 4041.9 # 404.19 / 0.1 = 4041.9 USD/ozt
|
# With expense-adjusted backing (~0.0916 oz/share), spot = 404.19 / 0.091576... ≈ 4413.71
|
||||||
|
current_backing = float(gld_ounces_per_share(date.today()))
|
||||||
|
expected_spot = 404.19 / current_backing
|
||||||
|
assert abs(spot - expected_spot) < 0.01
|
||||||
assert source == "yfinance"
|
assert source == "yfinance"
|
||||||
assert updated_at == "2026-03-25T00:00:00+00:00"
|
assert updated_at == "2026-03-25T00:00:00+00:00"
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import json
|
|||||||
import re
|
import re
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from app.main import app
|
from app.main import app
|
||||||
@@ -109,9 +110,17 @@ def test_bootstrap_endpoint_requires_turnstile_and_creates_workspace_cookie_and_
|
|||||||
assert response.cookies.get("workspace_id") == workspace_id
|
assert response.cookies.get("workspace_id") == workspace_id
|
||||||
|
|
||||||
created = repo.load_portfolio_config(workspace_id)
|
created = repo.load_portfolio_config(workspace_id)
|
||||||
assert created.entry_price == 4041.9
|
# GLD quote at $404.19/share converts to spot using expense-adjusted backing
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from app.domain.instruments import gld_ounces_per_share
|
||||||
|
|
||||||
|
current_backing = float(gld_ounces_per_share(date.today()))
|
||||||
|
expected_entry_price = 404.19 / current_backing # ~4413.71 with 2026 backing
|
||||||
|
expected_gold_value = expected_entry_price * 100.0
|
||||||
|
assert created.entry_price == pytest.approx(expected_entry_price, rel=1e-4)
|
||||||
assert created.gold_ounces == 100.0
|
assert created.gold_ounces == 100.0
|
||||||
assert created.gold_value == 404190.0
|
assert created.gold_value == pytest.approx(expected_gold_value, rel=1e-4)
|
||||||
|
|
||||||
|
|
||||||
def test_root_with_valid_workspace_cookie_redirects_to_workspace(tmp_path, monkeypatch) -> None:
|
def test_root_with_valid_workspace_cookie_redirects_to_workspace(tmp_path, monkeypatch) -> None:
|
||||||
@@ -238,9 +247,11 @@ def test_workspace_routes_seed_page_defaults_from_workspace_portfolio_config(tmp
|
|||||||
|
|
||||||
|
|
||||||
def test_hedge_page_upgrades_cached_gld_quote_and_uses_converted_spot(tmp_path, monkeypatch) -> None:
|
def test_hedge_page_upgrades_cached_gld_quote_and_uses_converted_spot(tmp_path, monkeypatch) -> None:
|
||||||
"""Hedge page should reuse DataService cache normalization for legacy GLD quotes."""
|
"""Hedge page should reuse DataService cache normalization for legacy GLD quotes with expense-adjusted backing."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from app.domain.instruments import gld_ounces_per_share
|
||||||
from app.pages import hedge as hedge_module
|
from app.pages import hedge as hedge_module
|
||||||
from app.services import runtime as runtime_module
|
from app.services import runtime as runtime_module
|
||||||
|
|
||||||
@@ -269,10 +280,13 @@ def test_hedge_page_upgrades_cached_gld_quote_and_uses_converted_spot(tmp_path,
|
|||||||
|
|
||||||
portfolio, source, _ = asyncio.run(hedge_module._resolve_hedge_spot(workspace_id))
|
portfolio, source, _ = asyncio.run(hedge_module._resolve_hedge_spot(workspace_id))
|
||||||
|
|
||||||
|
# With expense-adjusted backing (~0.0916 oz/share), spot = 404.19 / 0.091576... ≈ 4413.71
|
||||||
|
current_backing = float(gld_ounces_per_share(date.today()))
|
||||||
|
expected_spot = 404.19 / current_backing
|
||||||
assert source == "cache"
|
assert source == "cache"
|
||||||
assert portfolio["spot_price"] == 4041.9
|
assert portfolio["spot_price"] == pytest.approx(expected_spot, abs=0.01)
|
||||||
assert portfolio["gold_value"] == 889218.0
|
assert portfolio["gold_value"] == pytest.approx(220.0 * expected_spot, abs=0.01)
|
||||||
assert portfolio["net_equity"] == 667218.0
|
assert portfolio["net_equity"] == pytest.approx(220.0 * expected_spot - 222_000.0, abs=0.01)
|
||||||
|
|
||||||
|
|
||||||
def test_hedge_page_upgrades_legacy_default_workspace_footprint(tmp_path, monkeypatch) -> None:
|
def test_hedge_page_upgrades_legacy_default_workspace_footprint(tmp_path, monkeypatch) -> None:
|
||||||
@@ -323,8 +337,16 @@ def test_hedge_page_upgrades_legacy_default_workspace_footprint(tmp_path, monkey
|
|||||||
|
|
||||||
portfolio, source, _ = asyncio.run(hedge_module._resolve_hedge_spot(workspace_id))
|
portfolio, source, _ = asyncio.run(hedge_module._resolve_hedge_spot(workspace_id))
|
||||||
|
|
||||||
|
# With expense-adjusted backing (~0.0916 oz/share), spot = 404.19 / 0.091576... ≈ 4413.71
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from app.domain.instruments import gld_ounces_per_share
|
||||||
|
|
||||||
|
current_backing = float(gld_ounces_per_share(date.today()))
|
||||||
|
expected_spot = 404.19 / current_backing
|
||||||
|
|
||||||
assert source == "cache"
|
assert source == "cache"
|
||||||
assert portfolio["gold_units"] == 100.0
|
assert portfolio["gold_units"] == 100.0
|
||||||
assert portfolio["margin_call_price"] == 1933.3333333333333
|
assert portfolio["margin_call_price"] == pytest.approx(1933.3333333333333, abs=0.01)
|
||||||
assert portfolio["gold_value"] == 404190.0
|
assert portfolio["gold_value"] == pytest.approx(100.0 * expected_spot, abs=0.01)
|
||||||
assert portfolio["net_equity"] == 259190.0
|
assert portfolio["net_equity"] == pytest.approx(100.0 * expected_spot - 145000.0, abs=0.01)
|
||||||
|
|||||||
Reference in New Issue
Block a user