fix(pricing): correct relative hedge payoff calculations

This commit is contained in:
Bu5hm4nn
2026-03-25 19:27:26 +01:00
parent 5217304624
commit bfb6c71be3
6 changed files with 241 additions and 50 deletions

View File

@@ -3,7 +3,7 @@ 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 _waterfall_options
from app.pages.hedge import _cost_benefit_options, _waterfall_options
def test_protective_put_atm_minus_20pct_improves_equity() -> None:
@@ -11,15 +11,15 @@ def test_protective_put_atm_minus_20pct_improves_equity() -> None:
assert metrics["scenario_price"] == 172.0
assert metrics["unhedged_equity"] == 27_000.0
assert metrics["hedged_equity"] == 58_750.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", 38_000.0),
("Option payoff", 43_000.0),
("Call cap", 0.0),
("Hedge cost", -6_250.0),
("Net equity", 58_750.0),
("Net equity", 63_750.0),
]
@@ -28,11 +28,39 @@ def test_hedge_waterfall_uses_zero_based_contribution_bars() -> None:
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"] == 38_000.0
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 for display."""
config = PortfolioConfig(entry_price=4400.0, gold_ounces=220.0, entry_basis_mode="weight", loan_amount=145000.0)

View File

@@ -8,7 +8,9 @@ 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
@@ -81,14 +83,14 @@ def test_strategy_metrics_from_snapshot_preserves_minus_20pct_protective_put_exa
assert metrics["scenario_price"] == 172.0
assert metrics["unhedged_equity"] == 27000.0
assert metrics["hedged_equity"] == 58750.0
assert metrics["hedged_equity"] == 63750.0
assert metrics["waterfall_steps"] == [
("Base equity", 70000.0),
("Spot move", -43000.0),
("Option payoff", 38000.0),
("Option payoff", 43000.0),
("Call cap", 0.0),
("Hedge cost", -6250.0),
("Net equity", 58750.0),
("Net equity", 63750.0),
]
@@ -131,6 +133,43 @@ def test_resolve_portfolio_spot_from_quote_fails_closed_to_configured_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