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

@@ -1,6 +1,6 @@
from __future__ import annotations
from decimal import Decimal
from decimal import Decimal, InvalidOperation
from typing import Any, Mapping
from app.domain.backtesting_math import PricePerAsset
@@ -54,6 +54,111 @@ def _safe_quote_price(value: object) -> float:
return parsed
def _strategy_decimal(value: object) -> Decimal | None:
if value is None or isinstance(value, bool):
return None
if isinstance(value, Decimal):
return value if value.is_finite() else None
if isinstance(value, int):
return Decimal(value)
if isinstance(value, float):
return decimal_from_float(value)
if isinstance(value, str):
stripped = value.strip()
if not stripped:
return None
try:
parsed = Decimal(stripped)
except InvalidOperation:
return None
return parsed if parsed.is_finite() else None
return None
def _strategy_downside_put_legs(strategy: Mapping[str, Any], current_spot: Decimal) -> list[tuple[Decimal, Decimal]]:
raw_legs = strategy.get("downside_put_legs")
if isinstance(raw_legs, (list, tuple)):
parsed_legs: list[tuple[Decimal, Decimal]] = []
for leg in raw_legs:
if not isinstance(leg, Mapping):
continue
weight = _strategy_decimal(leg.get("allocation_weight", leg.get("weight")))
strike_pct = _strategy_decimal(leg.get("strike_pct"))
if weight is None or strike_pct is None or weight <= 0 or strike_pct <= 0:
continue
parsed_legs.append((weight, current_spot * strike_pct))
if parsed_legs:
return parsed_legs
protection_floor_pct = _strategy_decimal(strategy.get("protection_floor_pct"))
if protection_floor_pct is not None and protection_floor_pct > 0:
return [(_DECIMAL_ONE, current_spot * protection_floor_pct)]
absolute_floor = _strategy_decimal(strategy.get("max_drawdown_floor"))
if absolute_floor is not None and absolute_floor > 0:
return [(_DECIMAL_ONE, absolute_floor)]
return [(_DECIMAL_ONE, current_spot * Decimal("0.95"))]
def _strategy_upside_cap_decimal(strategy: Mapping[str, Any], current_spot: Decimal) -> Decimal | None:
upside_cap_pct = _strategy_decimal(strategy.get("upside_cap_pct"))
if upside_cap_pct is not None and upside_cap_pct > 0:
return current_spot * upside_cap_pct
absolute_cap = _strategy_decimal(strategy.get("upside_cap"))
if absolute_cap is not None and absolute_cap > 0:
return absolute_cap
return None
def _strategy_option_payoff_per_unit(
strategy: Mapping[str, Any], current_spot: Decimal, scenario_spot: Decimal
) -> Decimal:
return sum(
weight * max(strike_price - scenario_spot, _DECIMAL_ZERO)
for weight, strike_price in _strategy_downside_put_legs(strategy, current_spot)
)
def _strategy_upside_cap_effect_per_unit(
strategy: Mapping[str, Any], current_spot: Decimal, scenario_spot: Decimal
) -> Decimal:
cap = _strategy_upside_cap_decimal(strategy, current_spot)
if cap is None or scenario_spot <= cap:
return _DECIMAL_ZERO
return -(scenario_spot - cap)
def strategy_protection_floor_bounds(strategy: Mapping[str, Any], *, current_spot: float) -> tuple[float, float] | None:
current_spot_decimal = decimal_from_float(current_spot)
legs = _strategy_downside_put_legs(strategy, current_spot_decimal)
if not legs:
return None
floor_prices = [strike_price for _, strike_price in legs]
return _decimal_to_float(min(floor_prices)), _decimal_to_float(max(floor_prices))
def strategy_upside_cap_price(strategy: Mapping[str, Any], *, current_spot: float) -> float | None:
cap = _strategy_upside_cap_decimal(strategy, decimal_from_float(current_spot))
if cap is None:
return None
return _decimal_to_float(cap)
def strategy_benefit_per_unit(strategy: Mapping[str, Any], *, current_spot: float, scenario_spot: float) -> float:
current_spot_decimal = decimal_from_float(current_spot)
scenario_spot_decimal = decimal_from_float(scenario_spot)
cost = _strategy_decimal(strategy.get("estimated_cost")) or _DECIMAL_ZERO
benefit = (
_strategy_option_payoff_per_unit(strategy, current_spot_decimal, scenario_spot_decimal)
+ _strategy_upside_cap_effect_per_unit(strategy, current_spot_decimal, scenario_spot_decimal)
- cost
)
return round(float(benefit), 2)
def resolve_portfolio_spot_from_quote(
config: PortfolioConfig,
quote: Mapping[str, object],
@@ -158,24 +263,17 @@ def strategy_metrics_from_snapshot(
current_spot = PricePerWeight(amount=spot, currency=BaseCurrency.USD, per_unit=WeightUnit.OUNCE_TROY)
loan_amount = Money(amount=decimal_from_float(float(snapshot["loan_amount"])), currency=BaseCurrency.USD)
base_equity = Money(amount=decimal_from_float(float(snapshot["net_equity"])), currency=BaseCurrency.USD)
default_floor = spot * Decimal("0.95")
floor = (
decimal_from_float(float(strategy["max_drawdown_floor"])) if "max_drawdown_floor" in strategy else default_floor
)
cap_decimal = (
decimal_from_float(float(strategy["upside_cap"]))
if isinstance(strategy.get("upside_cap"), (int, float))
else None
)
cost = decimal_from_float(float(strategy["estimated_cost"]))
cost = _strategy_decimal(strategy.get("estimated_cost")) or _DECIMAL_ZERO
scenario_prices = [spot * _pct_factor(pct) for pct in range(-25, 30, 5)]
benefits: list[float] = []
for price in scenario_prices:
payoff = max(floor - price, _DECIMAL_ZERO)
if cap_decimal is not None and price > cap_decimal:
payoff -= price - cap_decimal
benefits.append(round(float(payoff - cost), 2))
benefits = [
strategy_benefit_per_unit(
strategy,
current_spot=_decimal_to_float(spot),
scenario_spot=_decimal_to_float(price),
)
for price in scenario_prices
]
scenario_price = spot * _pct_factor(scenario_pct)
scenario_gold_value = gold_weight * PricePerWeight(
@@ -185,10 +283,8 @@ def strategy_metrics_from_snapshot(
)
current_gold_value = gold_weight * current_spot
unhedged_equity = scenario_gold_value - loan_amount
scenario_payoff_per_unit = max(floor - scenario_price, _DECIMAL_ZERO)
capped_upside_per_unit = _DECIMAL_ZERO
if cap_decimal is not None and scenario_price > cap_decimal:
capped_upside_per_unit = -(scenario_price - cap_decimal)
scenario_payoff_per_unit = _strategy_option_payoff_per_unit(strategy, spot, scenario_price)
capped_upside_per_unit = _strategy_upside_cap_effect_per_unit(strategy, spot, scenario_price)
option_payoff_cash = Money(amount=gold_weight.amount * scenario_payoff_per_unit, currency=BaseCurrency.USD)
capped_upside_cash = Money(amount=gold_weight.amount * capped_upside_per_unit, currency=BaseCurrency.USD)