from __future__ import annotations import pytest from app.models.portfolio import PortfolioConfig def test_ltv_calculation(sample_portfolio) -> None: assert sample_portfolio.current_ltv == pytest.approx(0.60, rel=1e-12) assert sample_portfolio.ltv_at_price(460.0) == pytest.approx(0.60, rel=1e-12) assert sample_portfolio.ltv_at_price(368.0) == pytest.approx(0.75, rel=1e-12) def test_net_equity_calculation(sample_portfolio) -> None: assert sample_portfolio.net_equity == pytest.approx(400_000.0, rel=1e-12) assert sample_portfolio.net_equity_at_price(420.0) == pytest.approx( sample_portfolio.gold_ounces * 420.0 - 600_000.0, rel=1e-12, ) def test_margin_call_threshold(sample_portfolio) -> None: assert sample_portfolio.margin_call_price() == pytest.approx(368.0, rel=1e-12) assert sample_portfolio.ltv_at_price(sample_portfolio.margin_call_price()) == pytest.approx(0.75, rel=1e-12) def test_portfolio_config_derives_gold_weight_from_start_value_and_entry_price() -> None: config = PortfolioConfig( gold_value=215_000.0, entry_price=215.0, entry_basis_mode="value_price", ) assert config.gold_ounces == pytest.approx(1_000.0, rel=1e-12) assert config.gold_value == pytest.approx(215_000.0, rel=1e-12) assert config.entry_basis_mode == "value_price" def test_portfolio_config_derives_start_value_from_gold_weight_and_entry_price() -> None: config = PortfolioConfig( gold_ounces=1_000.0, entry_price=215.0, entry_basis_mode="weight", ) assert config.gold_value == pytest.approx(215_000.0, rel=1e-12) assert config.gold_ounces == pytest.approx(1_000.0, rel=1e-12) assert config.entry_basis_mode == "weight" def test_portfolio_config_serializes_canonical_entry_basis_fields() -> None: config = PortfolioConfig(gold_value=215_000.0, entry_price=215.0) data = config.to_dict() assert data["gold_value"] == pytest.approx(215_000.0, rel=1e-12) assert data["gold_ounces"] == pytest.approx(1_000.0, rel=1e-12) assert data["entry_price"] == pytest.approx(215.0, rel=1e-12) assert PortfolioConfig.from_dict(data).gold_ounces == pytest.approx(1_000.0, rel=1e-12) def test_portfolio_config_keeps_legacy_gold_value_payloads_compatible() -> None: config = PortfolioConfig.from_dict({"gold_value": 215_000.0, "loan_amount": 145_000.0}) assert config.gold_value == pytest.approx(215_000.0, rel=1e-12) assert config.entry_price == pytest.approx(215.0, rel=1e-12) assert config.gold_ounces == pytest.approx(1_000.0, rel=1e-12) def test_portfolio_config_rejects_invalid_entry_basis_values() -> None: with pytest.raises(ValueError, match="Entry price must be positive"): PortfolioConfig(entry_price=0.0) with pytest.raises(ValueError, match="Gold weight must be positive"): PortfolioConfig(gold_ounces=-1.0, entry_price=215.0, entry_basis_mode="weight") with pytest.raises(ValueError, match="Gold value and weight contradict each other"): PortfolioConfig(gold_value=215_000.0, gold_ounces=900.0, entry_price=215.0)