from __future__ import annotations from decimal import Decimal import pytest from app.domain.backtesting_math import PricePerAsset from app.domain.instruments import price_per_weight_from_asset_price from app.domain.portfolio_math import ( build_alert_context, portfolio_snapshot_from_config, resolve_portfolio_spot_from_quote, strategy_benefit_per_unit, strategy_metrics_from_snapshot, strategy_protection_floor_bounds, ) from app.domain.units import BaseCurrency, WeightUnit from app.models.portfolio import PortfolioConfig def test_portfolio_snapshot_from_config_preserves_weight_price_and_margin_values() -> None: config = PortfolioConfig(entry_price=4400.0, gold_ounces=220.0, entry_basis_mode="weight", loan_amount=145000.0) snapshot = portfolio_snapshot_from_config(config) assert snapshot["spot_price"] == 4400.0 assert snapshot["gold_units"] == 220.0 assert snapshot["gold_value"] == 968000.0 assert snapshot["net_equity"] == 823000.0 assert round(snapshot["margin_call_price"], 2) == 878.79 def test_build_alert_context_uses_unit_safe_gold_value_calculation() -> None: config = PortfolioConfig(entry_price=4400.0, gold_ounces=220.0, entry_basis_mode="weight", loan_amount=145000.0) context = build_alert_context( config, spot_price=4400.0, source="configured_entry_price", updated_at="", ) assert context["gold_units"] == 220.0 assert context["gold_value"] == 968000.0 assert context["net_equity"] == 823000.0 assert round(float(context["margin_call_price"]), 2) == 878.79 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) 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) # 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( config, spot_price=float(ounce_spot.amount), source="yfinance", updated_at="2026-03-24T00:00:00+00:00", ) assert abs(context["spot_price"] - expected_spot) < 0.01 assert context["gold_value"] == pytest.approx(220.0 * expected_spot, abs=0.01) assert context["net_equity"] == pytest.approx(220.0 * expected_spot - 145000.0, abs=0.01) assert context["quote_source"] == "yfinance" def test_strategy_metrics_from_snapshot_preserves_minus_20pct_protective_put_example() -> None: from app.pages.common import strategy_catalog strategy = next(item for item in strategy_catalog() if item["name"] == "protective_put_atm") snapshot = { "gold_value": 215000.0, "loan_amount": 145000.0, "net_equity": 70000.0, "spot_price": 215.0, "gold_units": 1000.0, "margin_call_ltv": 0.75, "margin_call_price": 193.33, "cash_buffer": 18500.0, "hedge_budget": 8000.0, "ltv_ratio": 145000.0 / 215000.0, } metrics = strategy_metrics_from_snapshot(strategy, -20, snapshot) assert metrics["scenario_price"] == 172.0 assert metrics["unhedged_equity"] == 27000.0 assert metrics["hedged_equity"] == 63750.0 assert metrics["waterfall_steps"] == [ ("Base equity", 70000.0), ("Spot move", -43000.0), ("Option payoff", 43000.0), ("Call cap", 0.0), ("Hedge cost", -6250.0), ("Net equity", 63750.0), ] 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 using expense-adjusted backing.""" from datetime import date from app.domain.instruments import gld_ounces_per_share from app.models.portfolio import PortfolioConfig config = PortfolioConfig(entry_price=4400.0, gold_ounces=220.0, entry_basis_mode="weight", loan_amount=145000.0) share_quote = { "symbol": "GLD", "price": 404.19, "quote_unit": "share", "source": "yfinance", "updated_at": "2026-03-25T00:00:00+00:00", } spot, source, updated_at = resolve_portfolio_spot_from_quote(config, share_quote) # 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 updated_at == "2026-03-25T00:00:00+00:00" def test_resolve_portfolio_spot_from_quote_fails_closed_to_configured_price() -> None: """Missing quote_unit should fail closed to configured entry price.""" from app.models.portfolio import PortfolioConfig config = PortfolioConfig(entry_price=4400.0, gold_ounces=220.0, entry_basis_mode="weight", loan_amount=145000.0) legacy_quote = { "symbol": "GLD", "price": 404.19, "source": "cache", "updated_at": "", } spot, source, updated_at = resolve_portfolio_spot_from_quote(config, legacy_quote) assert spot == 4400.0 # Falls back to configured price assert source == "configured_entry_price" assert updated_at == "" def test_strategy_metrics_from_snapshot_uses_relative_template_strikes_for_high_spot_portfolios() -> None: from app.pages.common import strategy_catalog strategy = next(item for item in strategy_catalog() if item["name"] == "protective_put_atm") snapshot = portfolio_snapshot_from_config( PortfolioConfig(entry_price=4400.0, gold_ounces=220.0, entry_basis_mode="weight", loan_amount=145000.0) ) metrics = strategy_metrics_from_snapshot(strategy, -20, snapshot) assert metrics["scenario_price"] == 3520.0 assert metrics["unhedged_equity"] == 629400.0 assert metrics["hedged_equity"] == 821625.0 assert metrics["waterfall_steps"] == [ ("Base equity", 823000.0), ("Spot move", -193600.0), ("Option payoff", 193600.0), ("Call cap", 0.0), ("Hedge cost", -1375.0), ("Net equity", 821625.0), ] assert metrics["scenario_series"][0]["benefit"] == 1093.75 assert metrics["scenario_series"][1]["benefit"] == 873.75 def test_strategy_benefit_and_floor_bounds_support_laddered_relative_put_legs() -> None: from app.pages.common import strategy_catalog strategy = next(item for item in strategy_catalog() if item["name"] == "laddered_put_50_50_atm_otm95") floor_bounds = strategy_protection_floor_bounds(strategy, current_spot=4400.0) benefit = strategy_benefit_per_unit(strategy, current_spot=4400.0, scenario_spot=3520.0) assert floor_bounds == (4180.0, 4400.0) assert benefit == 765.55 def test_resolve_portfolio_spot_from_quote_fails_closed_for_unsupported_symbol() -> None: """Unsupported symbols without instrument metadata should fail closed.""" from app.models.portfolio import PortfolioConfig config = PortfolioConfig(entry_price=4400.0, gold_ounces=220.0, entry_basis_mode="weight", loan_amount=145000.0) btc_quote = { "symbol": "BTC-USD", "price": 70000.0, "quote_unit": "coin", "source": "yfinance", "updated_at": "2026-03-25T00:00:00+00:00", } spot, source, updated_at = resolve_portfolio_spot_from_quote(config, btc_quote) assert spot == 4400.0 # Falls back to configured price assert source == "configured_entry_price"