fix: correct hedge equity math at downside scenarios
This commit is contained in:
@@ -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),
|
(item for item in strategy_catalog() if item["name"] == strategy_name),
|
||||||
strategy_catalog()[0],
|
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))
|
floor = float(strategy.get("max_drawdown_floor", spot * 0.95))
|
||||||
cap = strategy.get("upside_cap")
|
cap = strategy.get("upside_cap")
|
||||||
cost = float(strategy["estimated_cost"])
|
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))
|
benefits.append(round(payoff - cost, 2))
|
||||||
|
|
||||||
scenario_price = round(spot * (1 + scenario_pct / 100), 2)
|
scenario_price = round(spot * (1 + scenario_pct / 100), 2)
|
||||||
unhedged_equity = scenario_price * 1_000 - 145_000.0
|
unhedged_equity = scenario_price * underlying_units - loan_amount
|
||||||
scenario_payoff = max(floor - scenario_price, 0.0)
|
scenario_payoff_per_unit = max(floor - scenario_price, 0.0)
|
||||||
capped_upside = 0.0
|
capped_upside_per_unit = 0.0
|
||||||
if isinstance(cap, (int, float)) and scenario_price > float(cap):
|
if isinstance(cap, (int, float)) and scenario_price > float(cap):
|
||||||
capped_upside = -(scenario_price - float(cap))
|
capped_upside_per_unit = -(scenario_price - float(cap))
|
||||||
hedged_equity = unhedged_equity + scenario_payoff + capped_upside - cost * 1_000
|
|
||||||
|
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 = [
|
waterfall_steps = [
|
||||||
("Base equity", round(70_000.0, 2)),
|
("Base equity", round(base_equity, 2)),
|
||||||
("Spot move", round((scenario_price - spot) * 1_000, 2)),
|
("Spot move", round((scenario_price - spot) * underlying_units, 2)),
|
||||||
("Option payoff", round(scenario_payoff * 1_000, 2)),
|
("Option payoff", round(option_payoff_cash, 2)),
|
||||||
("Call cap", round(capped_upside * 1_000, 2)),
|
("Call cap", round(capped_upside_cash, 2)),
|
||||||
("Hedge cost", round(-cost * 1_000, 2)),
|
("Hedge cost", round(-hedge_cost_cash, 2)),
|
||||||
("Net equity", round(hedged_equity, 2)),
|
("Net equity", round(hedged_equity, 2)),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -35,19 +35,20 @@ def _waterfall_options(metrics: dict) -> dict:
|
|||||||
steps = metrics["waterfall_steps"]
|
steps = metrics["waterfall_steps"]
|
||||||
running = 0.0
|
running = 0.0
|
||||||
base: list[float] = []
|
base: list[float] = []
|
||||||
values: list[float] = []
|
values: list[dict[str, object]] = []
|
||||||
for index, (_, amount) in enumerate(steps):
|
for index, (label, amount) in enumerate(steps):
|
||||||
if index == 0:
|
if index == 0:
|
||||||
base.append(0)
|
base.append(0)
|
||||||
values.append(amount)
|
|
||||||
running = amount
|
running = amount
|
||||||
elif index == len(steps) - 1:
|
elif index == len(steps) - 1:
|
||||||
base.append(0)
|
base.append(0)
|
||||||
values.append(amount)
|
|
||||||
else:
|
else:
|
||||||
base.append(running)
|
base.append(running)
|
||||||
values.append(amount)
|
|
||||||
running += amount
|
running += amount
|
||||||
|
|
||||||
|
color = "#0ea5e9" if label == "Net equity" else ("#22c55e" if amount >= 0 else "#ef4444")
|
||||||
|
values.append({"value": amount, "itemStyle": {"color": color}})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"tooltip": {"trigger": "axis", "axisPointer": {"type": "shadow"}},
|
"tooltip": {"trigger": "axis", "axisPointer": {"type": "shadow"}},
|
||||||
"xAxis": {"type": "category", "data": [label for label, _ in steps]},
|
"xAxis": {"type": "category", "data": [label for label, _ in steps]},
|
||||||
@@ -58,14 +59,12 @@ def _waterfall_options(metrics: dict) -> dict:
|
|||||||
"stack": "total",
|
"stack": "total",
|
||||||
"data": base,
|
"data": base,
|
||||||
"itemStyle": {"color": "rgba(0,0,0,0)"},
|
"itemStyle": {"color": "rgba(0,0,0,0)"},
|
||||||
|
"tooltip": {"show": False},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "bar",
|
"type": "bar",
|
||||||
"stack": "total",
|
"stack": "total",
|
||||||
"data": values,
|
"data": values,
|
||||||
"itemStyle": {
|
|
||||||
"color": "#22c55e",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,17 @@ def test_homepage_and_options_page_render() -> None:
|
|||||||
assert "Scenario Summary" in hedge_text
|
assert "Scenario Summary" in hedge_text
|
||||||
assert "RuntimeError" not in hedge_text
|
assert "RuntimeError" not in hedge_text
|
||||||
assert "Server error" 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)
|
page.screenshot(path=str(ARTIFACTS / "hedge.png"), full_page=True)
|
||||||
|
|
||||||
browser.close()
|
browser.close()
|
||||||
|
|||||||
20
tests/test_hedge_metrics.py
Normal file
20
tests/test_hedge_metrics.py
Normal file
@@ -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),
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user