- Set ruff/black line length to 120 - Reformatted code with black - Fixed import ordering with ruff - Disabled lint for UI component files with long CSS strings - Updated pyproject.toml with proper tool configuration
108 lines
4.1 KiB
Python
108 lines
4.1 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
import app.core.pricing.black_scholes as black_scholes
|
|
from app.strategies.base import StrategyConfig
|
|
from app.strategies.laddered_put import LadderedPutStrategy, LadderSpec
|
|
from app.strategies.protective_put import ProtectivePutSpec, ProtectivePutStrategy
|
|
|
|
|
|
def _force_analytic_pricing(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Use deterministic analytical pricing for stable expected values."""
|
|
monkeypatch.setattr(black_scholes, "ql", None)
|
|
|
|
|
|
def test_protective_put_costs(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
sample_strategy_config: StrategyConfig,
|
|
) -> None:
|
|
_force_analytic_pricing(monkeypatch)
|
|
strategy = ProtectivePutStrategy(
|
|
sample_strategy_config,
|
|
ProtectivePutSpec(label="ATM", strike_pct=1.0, months=12),
|
|
)
|
|
cost = strategy.calculate_cost()
|
|
|
|
assert cost["strategy"] == "protective_put_atm"
|
|
assert cost["label"] == "ATM"
|
|
assert cost["strike"] == 460.0
|
|
assert cost["premium_per_share"] == pytest.approx(19.6894, abs=1e-4)
|
|
assert cost["total_cost"] == pytest.approx(42803.14, abs=1e-2)
|
|
assert cost["cost_pct_of_portfolio"] == pytest.approx(0.042803, abs=1e-6)
|
|
assert cost["annualized_cost"] == pytest.approx(42803.14, abs=1e-2)
|
|
assert cost["annualized_cost_pct"] == pytest.approx(0.042803, abs=1e-6)
|
|
|
|
|
|
def test_laddered_strategy(sample_strategy_config: StrategyConfig, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
_force_analytic_pricing(monkeypatch)
|
|
strategy = LadderedPutStrategy(
|
|
sample_strategy_config,
|
|
LadderSpec(
|
|
label="50_50_ATM_OTM95",
|
|
weights=(0.5, 0.5),
|
|
strike_pcts=(1.0, 0.95),
|
|
months=12,
|
|
),
|
|
)
|
|
cost = strategy.calculate_cost()
|
|
protection = strategy.calculate_protection()
|
|
|
|
assert cost["strategy"] == "laddered_put_50_50_atm_otm95"
|
|
assert len(cost["legs"]) == 2
|
|
assert cost["legs"][0]["weight"] == 0.5
|
|
assert cost["legs"][0]["strike"] == 460.0
|
|
assert cost["legs"][1]["strike"] == 437.0
|
|
assert cost["blended_cost"] == pytest.approx(34200.72, abs=1e-2)
|
|
assert cost["cost_pct_of_portfolio"] == pytest.approx(0.034201, abs=1e-6)
|
|
|
|
assert protection["portfolio_floor_value"] == pytest.approx(975000.0, rel=1e-12)
|
|
assert protection["payoff_at_threshold"] == pytest.approx(175000.0, abs=1e-2)
|
|
assert protection["hedged_ltv_at_threshold"] == pytest.approx(0.615385, rel=1e-6)
|
|
assert protection["maintains_margin_call_buffer"] is True
|
|
|
|
|
|
def test_scenario_analysis(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
sample_strategy_config: StrategyConfig,
|
|
) -> None:
|
|
_force_analytic_pricing(monkeypatch)
|
|
protective = ProtectivePutStrategy(
|
|
sample_strategy_config,
|
|
ProtectivePutSpec(label="ATM", strike_pct=1.0, months=12),
|
|
)
|
|
ladder = LadderedPutStrategy(
|
|
sample_strategy_config,
|
|
LadderSpec(
|
|
label="50_50_ATM_OTM95",
|
|
weights=(0.5, 0.5),
|
|
strike_pcts=(1.0, 0.95),
|
|
months=12,
|
|
),
|
|
)
|
|
|
|
protective_scenarios = protective.get_scenarios()
|
|
ladder_scenarios = ladder.get_scenarios()
|
|
|
|
assert len(protective_scenarios) == 12
|
|
assert len(ladder_scenarios) == 12
|
|
|
|
first_protective = protective_scenarios[0]
|
|
assert first_protective["price_change_pct"] == -0.6
|
|
assert first_protective["gld_price"] == 184.0
|
|
assert first_protective["option_payoff"] == pytest.approx(600000.0, abs=1e-2)
|
|
assert first_protective["hedge_cost"] == pytest.approx(42803.14, abs=1e-2)
|
|
assert first_protective["hedged_ltv"] == pytest.approx(0.6, rel=1e-12)
|
|
assert first_protective["margin_call_with_hedge"] is False
|
|
|
|
first_ladder = ladder_scenarios[0]
|
|
assert first_ladder["gld_price"] == 184.0
|
|
assert first_ladder["option_payoff"] == pytest.approx(575000.0, abs=1e-2)
|
|
assert first_ladder["hedge_cost"] == pytest.approx(34200.72, abs=1e-2)
|
|
assert first_ladder["hedged_ltv"] == pytest.approx(0.615385, rel=1e-6)
|
|
|
|
worst_ladder = ladder_scenarios[-1]
|
|
assert worst_ladder["gld_price"] == 690.0
|
|
assert worst_ladder["hedged_ltv"] == pytest.approx(0.4, rel=1e-12)
|
|
assert worst_ladder["margin_call_with_hedge"] is False
|