a13e3fe045
Common (mcp_common): - indicators.py: vol_cone, hurst_exponent, half_life_mean_reversion, garch11_forecast, autocorrelation, rolling_sharpe, var_cvar - options.py (nuovo): oi_weighted_skew, smile_asymmetry, atm_vs_wings_vol, dealer_gamma_profile, vanna_charm_aggregate - microstructure.py (nuovo): orderbook_imbalance (ratio + microprice + slope) - stats.py (nuovo): cointegration_test Engle-Granger + ADF helper Deribit (+6 tool MCP): - get_dealer_gamma_profile (net dealer gamma + flip level) - get_vanna_charm (vanna/charm aggregati pesati OI) - get_oi_weighted_skew, get_smile_asymmetry, get_atm_vs_wings_vol - get_orderbook_imbalance Bybit (+2 tool MCP): - get_orderbook_imbalance, get_basis_term_structure (futures dated curve) Macro (+2 tool MCP): - get_yield_curve_slope (2y10y/5y30y + butterfly + regime) - get_breakeven_inflation (FRED T5YIE/T10YIE/T5YIFR) Sentiment (+3 tool MCP): - get_funding_arb_spread (opportunità arb compatte annualizzate) - get_liquidation_heatmap (heuristic da OI delta + funding extreme, no feed paid Coinglass) - get_cointegration_pairs (Engle-Granger su coppie crypto Binance hourly) Tutto in TDD pure-Python (no numpy/scipy in mcp_common). README aggiornato con elenco completo. 442 test totali verdi. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
147 lines
4.9 KiB
Python
147 lines
4.9 KiB
Python
"""Test puri per mcp_common.options (logiche option-flow indipendenti
|
|
dall'exchange).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from 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<wings → richness positiva
|
|
assert out["wing_richness"] > 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
|