fix(pricing): correct relative hedge payoff calculations
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user