79 lines
3.1 KiB
Python
79 lines
3.1 KiB
Python
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)
|