118 lines
4.2 KiB
Python
118 lines
4.2 KiB
Python
from __future__ import annotations
|
|
|
|
from app.domain.portfolio_math import resolve_portfolio_spot_from_quote
|
|
from app.models.portfolio import PortfolioConfig
|
|
from app.pages.common import strategy_metrics
|
|
from app.pages.hedge import _cost_benefit_options, _waterfall_options
|
|
|
|
|
|
def test_protective_put_atm_minus_20pct_improves_equity() -> None:
|
|
metrics = strategy_metrics("protective_put_atm", -20)
|
|
|
|
assert metrics["scenario_price"] == 172.0
|
|
assert metrics["unhedged_equity"] == 27_000.0
|
|
assert metrics["hedged_equity"] == 63_750.0
|
|
assert metrics["hedged_equity"] > metrics["unhedged_equity"]
|
|
assert metrics["waterfall_steps"] == [
|
|
("Base equity", 70_000.0),
|
|
("Spot move", -43_000.0),
|
|
("Option payoff", 43_000.0),
|
|
("Call cap", 0.0),
|
|
("Hedge cost", -6_250.0),
|
|
("Net equity", 63_750.0),
|
|
]
|
|
|
|
|
|
def test_hedge_waterfall_uses_zero_based_contribution_bars() -> None:
|
|
options = _waterfall_options(strategy_metrics("protective_put_atm", -20))
|
|
|
|
assert len(options["series"]) == 1
|
|
assert options["series"][0]["type"] == "bar"
|
|
assert options["series"][0]["label"]["show"] is True
|
|
values = options["series"][0]["data"]
|
|
assert values[2]["value"] == 43_000.0
|
|
assert values[2]["itemStyle"]["color"] == "#22c55e"
|
|
|
|
|
|
def test_cost_benefit_chart_shows_positive_downside_benefit_when_puts_are_in_the_money() -> None:
|
|
metrics = strategy_metrics(
|
|
"protective_put_atm",
|
|
-20,
|
|
portfolio={
|
|
**{
|
|
"gold_value": 968000.0,
|
|
"loan_amount": 145000.0,
|
|
"ltv_ratio": 145000.0 / 968000.0,
|
|
"net_equity": 823000.0,
|
|
"spot_price": 4400.0,
|
|
"gold_units": 220.0,
|
|
"margin_call_ltv": 0.75,
|
|
"margin_call_price": 878.79,
|
|
"cash_buffer": 18500.0,
|
|
"hedge_budget": 8000.0,
|
|
}
|
|
},
|
|
)
|
|
options = _cost_benefit_options(metrics)
|
|
|
|
assert options["xAxis"]["name"] == "Collateral spot"
|
|
assert options["yAxis"]["name"] == "Net hedge benefit / oz"
|
|
assert options["series"][0]["data"][0] > 0
|
|
assert options["series"][0]["data"][1] > 0
|
|
|
|
|
|
def test_hedge_quote_resolution_converts_gld_share_price_to_ozt_spot() -> None:
|
|
"""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)
|
|
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"
|
|
|
|
|
|
def test_hedge_quote_resolution_fails_closed_when_quote_unit_missing() -> None:
|
|
"""Hedge page should fall back to configured price when quote_unit is missing."""
|
|
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",
|
|
}
|
|
|
|
spot, source, _ = resolve_portfolio_spot_from_quote(config, legacy_quote)
|
|
|
|
assert spot == 4400.0
|
|
assert source == "configured_entry_price"
|
|
|
|
|
|
def test_hedge_quote_resolution_fails_closed_for_unsupported_instrument() -> None:
|
|
"""Hedge page should fall back when instrument metadata is unavailable."""
|
|
config = PortfolioConfig(entry_price=4400.0, gold_ounces=220.0, entry_basis_mode="weight", loan_amount=145000.0)
|
|
slv_quote = {
|
|
"symbol": "SLV",
|
|
"price": 28.50,
|
|
"quote_unit": "share",
|
|
"source": "yfinance",
|
|
"updated_at": "2026-03-25T00:00:00+00:00",
|
|
}
|
|
|
|
spot, source, _ = resolve_portfolio_spot_from_quote(config, slv_quote)
|
|
|
|
assert spot == 4400.0
|
|
assert source == "configured_entry_price"
|