diff --git a/app/pages/common.py b/app/pages/common.py index f32623d..49cc79d 100644 --- a/app/pages/common.py +++ b/app/pages/common.py @@ -114,7 +114,11 @@ def strategy_metrics(strategy_name: str, scenario_pct: int) -> dict[str, Any]: (item for item in strategy_catalog() if item["name"] == strategy_name), strategy_catalog()[0], ) - spot = demo_spot_price() + portfolio = portfolio_snapshot() + spot = float(portfolio["spot_price"]) + underlying_units = portfolio["gold_value"] / spot + loan_amount = float(portfolio["loan_amount"]) + base_equity = float(portfolio["net_equity"]) floor = float(strategy.get("max_drawdown_floor", spot * 0.95)) cap = strategy.get("upside_cap") cost = float(strategy["estimated_cost"]) @@ -128,19 +132,23 @@ def strategy_metrics(strategy_name: str, scenario_pct: int) -> dict[str, Any]: benefits.append(round(payoff - cost, 2)) scenario_price = round(spot * (1 + scenario_pct / 100), 2) - unhedged_equity = scenario_price * 1_000 - 145_000.0 - scenario_payoff = max(floor - scenario_price, 0.0) - capped_upside = 0.0 + unhedged_equity = scenario_price * underlying_units - loan_amount + scenario_payoff_per_unit = max(floor - scenario_price, 0.0) + capped_upside_per_unit = 0.0 if isinstance(cap, (int, float)) and scenario_price > float(cap): - capped_upside = -(scenario_price - float(cap)) - hedged_equity = unhedged_equity + scenario_payoff + capped_upside - cost * 1_000 + capped_upside_per_unit = -(scenario_price - float(cap)) + + option_payoff_cash = scenario_payoff_per_unit * underlying_units + capped_upside_cash = capped_upside_per_unit * underlying_units + hedge_cost_cash = cost * underlying_units + hedged_equity = unhedged_equity + option_payoff_cash + capped_upside_cash - hedge_cost_cash waterfall_steps = [ - ("Base equity", round(70_000.0, 2)), - ("Spot move", round((scenario_price - spot) * 1_000, 2)), - ("Option payoff", round(scenario_payoff * 1_000, 2)), - ("Call cap", round(capped_upside * 1_000, 2)), - ("Hedge cost", round(-cost * 1_000, 2)), + ("Base equity", round(base_equity, 2)), + ("Spot move", round((scenario_price - spot) * underlying_units, 2)), + ("Option payoff", round(option_payoff_cash, 2)), + ("Call cap", round(capped_upside_cash, 2)), + ("Hedge cost", round(-hedge_cost_cash, 2)), ("Net equity", round(hedged_equity, 2)), ] diff --git a/app/pages/hedge.py b/app/pages/hedge.py index e2dd8c1..42abfa2 100644 --- a/app/pages/hedge.py +++ b/app/pages/hedge.py @@ -35,19 +35,20 @@ def _waterfall_options(metrics: dict) -> dict: steps = metrics["waterfall_steps"] running = 0.0 base: list[float] = [] - values: list[float] = [] - for index, (_, amount) in enumerate(steps): + values: list[dict[str, object]] = [] + for index, (label, amount) in enumerate(steps): if index == 0: base.append(0) - values.append(amount) running = amount elif index == len(steps) - 1: base.append(0) - values.append(amount) else: base.append(running) - values.append(amount) running += amount + + color = "#0ea5e9" if label == "Net equity" else ("#22c55e" if amount >= 0 else "#ef4444") + values.append({"value": amount, "itemStyle": {"color": color}}) + return { "tooltip": {"trigger": "axis", "axisPointer": {"type": "shadow"}}, "xAxis": {"type": "category", "data": [label for label, _ in steps]}, @@ -58,14 +59,12 @@ def _waterfall_options(metrics: dict) -> dict: "stack": "total", "data": base, "itemStyle": {"color": "rgba(0,0,0,0)"}, + "tooltip": {"show": False}, }, { "type": "bar", "stack": "total", "data": values, - "itemStyle": { - "color": "#22c55e", - }, }, ], } diff --git a/tests/test_e2e_playwright.py b/tests/test_e2e_playwright.py index a4e168e..34d50db 100644 --- a/tests/test_e2e_playwright.py +++ b/tests/test_e2e_playwright.py @@ -96,6 +96,17 @@ def test_homepage_and_options_page_render() -> None: assert "Scenario Summary" in hedge_text assert "RuntimeError" not in hedge_text assert "Server error" not in hedge_text + + slider = page.locator(".q-slider").first + slider_box = slider.bounding_box() + assert slider_box is not None + page.mouse.click(slider_box["x"] + slider_box["width"] * 0.1, slider_box["y"] + slider_box["height"] / 2) + expect(page.locator("text=Scenario move: -20%").first).to_be_visible(timeout=15000) + hedge_text = page.locator("body").inner_text(timeout=15000) + assert "Unhedged equity" in hedge_text + assert "Hedged equity" in hedge_text + assert "$27,000" in hedge_text + assert "$58,750" in hedge_text page.screenshot(path=str(ARTIFACTS / "hedge.png"), full_page=True) browser.close() diff --git a/tests/test_hedge_metrics.py b/tests/test_hedge_metrics.py new file mode 100644 index 0000000..983d8da --- /dev/null +++ b/tests/test_hedge_metrics.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +from app.pages.common import strategy_metrics + + +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"] == 58_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), + ("Call cap", 0.0), + ("Hedge cost", -6_250.0), + ("Net equity", 58_750.0), + ]