"""Test puri per mcp_common.options (logiche option-flow indipendenti dall'exchange). """ from __future__ import annotations import pytest from cerbero_mcp.common.options import ( atm_vs_wings_vol, dealer_gamma_profile, oi_weighted_skew, smile_asymmetry, vanna_charm_aggregate, ) # ---------- oi_weighted_skew ---------- def test_oi_weighted_skew_balanced(): """OI distribuito 50/50 calls/puts → skew vicino a 0.""" legs = [ {"iv": 0.5, "delta": 0.5, "oi": 100, "option_type": "call"}, {"iv": 0.5, "delta": -0.5, "oi": 100, "option_type": "put"}, ] out = oi_weighted_skew(legs) assert abs(out["skew"]) < 0.01 def test_oi_weighted_skew_put_heavy(): """Put heavy → IV media puts > IV media calls → skew positivo (put > call).""" legs = [ {"iv": 0.4, "delta": 0.5, "oi": 50, "option_type": "call"}, {"iv": 0.7, "delta": -0.5, "oi": 500, "option_type": "put"}, ] out = oi_weighted_skew(legs) assert out["skew"] > 0 assert out["call_iv_weighted"] > 0 assert out["put_iv_weighted"] > out["call_iv_weighted"] def test_oi_weighted_skew_empty(): out = oi_weighted_skew([]) assert out == {"skew": None, "call_iv_weighted": None, "put_iv_weighted": None, "total_oi": 0} # ---------- smile_asymmetry ---------- def test_smile_asymmetry_symmetric(): """Smile simmetrico ATM → asymmetry ≈ 0.""" legs = [ {"strike": 80, "iv": 0.55, "option_type": "put"}, {"strike": 90, "iv": 0.50, "option_type": "put"}, {"strike": 100, "iv": 0.45, "option_type": "call"}, {"strike": 110, "iv": 0.50, "option_type": "call"}, {"strike": 120, "iv": 0.55, "option_type": "call"}, ] out = smile_asymmetry(legs, spot=100.0) assert out["atm_iv"] is not None assert abs(out["asymmetry"]) < 0.05 def test_smile_asymmetry_put_skew(): """OTM puts (low strike) IV >> OTM calls (high strike) IV → asymmetry > 0.""" legs = [ {"strike": 80, "iv": 0.80, "option_type": "put"}, {"strike": 100, "iv": 0.50, "option_type": "call"}, {"strike": 120, "iv": 0.45, "option_type": "call"}, ] out = smile_asymmetry(legs, spot=100.0) assert out["asymmetry"] > 0.1 def test_smile_asymmetry_no_atm(): legs = [{"strike": 200, "iv": 0.5, "option_type": "call"}] out = smile_asymmetry(legs, spot=100.0) assert out["atm_iv"] is None # ---------- atm_vs_wings_vol ---------- def test_atm_vs_wings_vol_basic(): legs = [ {"strike": 90, "iv": 0.55, "delta": -0.25, "option_type": "put"}, {"strike": 100, "iv": 0.45, "delta": 0.5, "option_type": "call"}, {"strike": 110, "iv": 0.50, "delta": 0.25, "option_type": "call"}, ] out = atm_vs_wings_vol(legs, spot=100.0) assert out["atm_iv"] == pytest.approx(0.45, rel=1e-3) assert out["wing_25d_call_iv"] == pytest.approx(0.50, rel=1e-3) assert out["wing_25d_put_iv"] == pytest.approx(0.55, rel=1e-3) # ATM 0 def test_atm_vs_wings_vol_no_data(): out = atm_vs_wings_vol([], spot=100.0) assert out["atm_iv"] is None # ---------- dealer_gamma_profile ---------- def test_dealer_gamma_profile_assumes_dealer_short_calls(): """Convention: dealer SHORT calls (sells calls to retail), LONG puts. Calls oi → negative dealer gamma, puts oi → positive dealer gamma. """ legs = [ {"strike": 100, "gamma": 0.01, "oi": 1000, "option_type": "call"}, {"strike": 100, "gamma": 0.01, "oi": 500, "option_type": "put"}, ] out = dealer_gamma_profile(legs, spot=100.0) # call gamma greater than put gamma at same strike → net dealer short gamma assert len(out["by_strike"]) == 1 row = out["by_strike"][0] assert row["call_dealer_gamma"] < 0 assert row["put_dealer_gamma"] > 0 assert row["net_dealer_gamma"] < 0 # calls dominate assert out["total_net_dealer_gamma"] < 0 def test_dealer_gamma_profile_empty(): out = dealer_gamma_profile([], spot=100.0) assert out["by_strike"] == [] assert out["total_net_dealer_gamma"] == 0.0 # ---------- vanna_charm_aggregate ---------- def test_vanna_charm_aggregate_basic(): legs = [ {"strike": 100, "vanna": 0.05, "charm": -0.001, "oi": 1000, "option_type": "call"}, {"strike": 100, "vanna": -0.05, "charm": 0.001, "oi": 500, "option_type": "put"}, ] out = vanna_charm_aggregate(legs, spot=100.0) assert out["total_vanna"] != 0 # some net exposure assert "total_charm" in out assert out["legs_analyzed"] == 2 def test_vanna_charm_aggregate_skip_missing_greeks(): legs = [ {"strike": 100, "vanna": None, "charm": -0.001, "oi": 1000, "option_type": "call"}, {"strike": 100, "vanna": 0.05, "charm": None, "oi": 500, "option_type": "put"}, ] out = vanna_charm_aggregate(legs, spot=100.0) # entrambe le legs hanno almeno una greca None → skippate assert out["legs_analyzed"] == 0