{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Volatility Term Structure e VIX\n", "\n", "**Companion notebook per \"Trading con le Opzioni - Strategie Operative\"** \n", "di Pierpaolo Marturano (Core Matrix S.r.l.)\n", "\n", "Questo notebook esplora:\n", "- Term structure della volatilità implicita\n", "- Contango e backwardation nel VIX\n", "- IV Rank e IV Percentile\n", "- Superficie di volatilità e skew\n", "\n", "Requisiti: `numpy`, `scipy`, `matplotlib`" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "from scipy.stats import norm\n", "import matplotlib.pyplot as plt\n", "\n", "from vol_surface import (\n", " generate_synthetic_surface, plot_smile, plot_term_structure,\n", " plot_surface_3d, plot_skew_analysis, iv_rank, iv_percentile\n", ")\n", "\n", "plt.style.use('seaborn-v0_8-whitegrid')\n", "plt.rcParams['figure.dpi'] = 100" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Term Structure del VIX\n", "\n", "Il VIX futures curve mostra le aspettative di volatilità a diverse scadenze.\n", "- **Contango** (curva crescente): mercato tranquillo, aspettativa di normalità\n", "- **Backwardation** (curva decrescente): stress di mercato, paura nel breve" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Simuliamo 3 scenari di term structure\n", "months = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9])\n", "\n", "# Scenario 1: Contango normale (mercato calmo)\n", "vix_contango = 14 + 1.2 * np.sqrt(months)\n", "\n", "# Scenario 2: Flat (transizione)\n", "vix_flat = np.ones_like(months, dtype=float) * 18 + 0.2 * np.random.randn(len(months))\n", "\n", "# Scenario 3: Backwardation (stress)\n", "vix_backwardation = 32 - 2.5 * np.sqrt(months)\n", "\n", "fig, ax = plt.subplots(figsize=(10, 6))\n", "\n", "ax.plot(months, vix_contango, 'o-', linewidth=2, markersize=8,\n", " color='#10b981', label='Contango (mercato calmo)')\n", "ax.plot(months, vix_flat, 's-', linewidth=2, markersize=8,\n", " color='#f59e0b', label='Flat (transizione)')\n", "ax.plot(months, vix_backwardation, '^-', linewidth=2, markersize=8,\n", " color='#ef4444', label='Backwardation (stress)')\n", "\n", "ax.set_title('Term Structure della Volatilità (VIX Futures)', fontweight='bold', fontsize=13)\n", "ax.set_xlabel('Mesi a Scadenza')\n", "ax.set_ylabel('Volatilità Implicita (%)')\n", "ax.legend(fontsize=11)\n", "ax.grid(True, alpha=0.3)\n", "ax.set_ylim(10, 35)\n", "\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "print(\"Implicazioni per il trading:\")\n", "print(\" Contango → favorevole per vendita di volatilità (calendar spread, short VIX)\")\n", "print(\" Backwardation → cautela, rischio elevato, favorevole per long volatility\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. IV Rank e IV Percentile\n", "\n", "Due metriche fondamentali per decidere se vendere o comprare volatilità:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Simuliamo 1 anno di storia della IV\n", "np.random.seed(42)\n", "days = 252\n", "t = np.arange(days)\n", "\n", "# IV con pattern realistico: mean-reverting con spike\n", "base_iv = 0.16\n", "iv_history = base_iv + 0.03 * np.sin(2 * np.pi * t / 60) # cicli\n", "iv_history += 0.01 * np.cumsum(np.random.randn(days)) / np.sqrt(days) # random walk\n", "iv_history = np.maximum(iv_history, 0.09) # floor\n", "\n", "# Aggiungi uno spike (es. earnings o evento macro)\n", "iv_history[120:130] += 0.08\n", "iv_history[200:205] += 0.05\n", "\n", "# IV corrente\n", "current_iv = 0.19\n", "\n", "rank = iv_rank(current_iv, iv_history)\n", "pctile = iv_percentile(current_iv, iv_history)\n", "\n", "fig, axes = plt.subplots(2, 1, figsize=(12, 8))\n", "\n", "# Grafico storia IV\n", "ax = axes[0]\n", "ax.plot(t, iv_history * 100, linewidth=1.5, color='#0d5c4d')\n", "ax.axhline(y=current_iv * 100, color='#ef4444', linewidth=2, linestyle='--',\n", " label=f'IV corrente: {current_iv*100:.1f}%')\n", "ax.axhline(y=np.min(iv_history) * 100, color='green', linewidth=1, linestyle=':',\n", " label=f'Min 1Y: {np.min(iv_history)*100:.1f}%')\n", "ax.axhline(y=np.max(iv_history) * 100, color='red', linewidth=1, linestyle=':',\n", " label=f'Max 1Y: {np.max(iv_history)*100:.1f}%')\n", "ax.fill_between(t, np.min(iv_history)*100, np.max(iv_history)*100,\n", " alpha=0.05, color='gray')\n", "ax.set_title('Storia della Volatilità Implicita (1 anno)', fontweight='bold')\n", "ax.set_xlabel('Giorni')\n", "ax.set_ylabel('IV (%)')\n", "ax.legend(loc='upper right')\n", "ax.grid(True, alpha=0.3)\n", "\n", "# Gauge IV Rank\n", "ax = axes[1]\n", "ax.barh(['IV Rank', 'IV Percentile'], [rank * 100, pctile * 100],\n", " color=['#3b82f6', '#8b5cf6'], alpha=0.8, height=0.5)\n", "ax.set_xlim(0, 100)\n", "ax.axvline(x=50, color='gray', linestyle='--', alpha=0.5)\n", "ax.set_title('IV Rank e Percentile', fontweight='bold')\n", "ax.set_xlabel('Valore (%)')\n", "\n", "for i, (val, label) in enumerate([(rank*100, 'Rank'), (pctile*100, 'Pctile')]):\n", " ax.text(val + 1, i, f'{val:.1f}%', va='center', fontweight='bold')\n", "\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "print(f\"\\nIV Rank: {rank*100:.1f}%\")\n", "print(f\" Formula: (IV_corrente - IV_min) / (IV_max - IV_min)\")\n", "print(f\" = ({current_iv*100:.1f} - {np.min(iv_history)*100:.1f}) / ({np.max(iv_history)*100:.1f} - {np.min(iv_history)*100:.1f})\")\n", "print(f\"\\nIV Percentile: {pctile*100:.1f}%\")\n", "print(f\" = % di giorni nell'ultimo anno con IV inferiore alla corrente\")\n", "print(f\"\\nDecisione:\")\n", "if rank > 0.5:\n", " print(f\" → IV ELEVATA: favorevole per strategie di VENDITA (iron condor, strangle, credit spread)\")\n", "else:\n", " print(f\" → IV BASSA: favorevole per strategie di ACQUISTO (long straddle, calendar, debit spread)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Superficie di Volatilità Completa" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Genera superficie sintetica realistica\n", "surface = generate_synthetic_surface(\n", " S=5500, atm_vol=0.16, skew_slope=-0.12,\n", " term_slope=0.02, smile_curvature=0.05\n", ")\n", "\n", "print(\"Superficie di volatilità generata:\")\n", "print(f\" Spot: ${surface['spot']}\")\n", "print(f\" Strike: ${surface['strikes'][0]:.0f} — ${surface['strikes'][-1]:.0f}\")\n", "print(f\" Scadenze: {surface['expirations_days'].tolist()} DTE\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Volatility Smile\n", "plot_smile(surface, expiry_idx=[0, 2, 4, 6, 9])" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Term Structure\n", "plot_term_structure(surface)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Superficie 3D\n", "plot_surface_3d(surface)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Analisi dello Skew\n", "plot_skew_analysis(surface)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4. Implicazioni operative dello Skew\n", "\n", "Lo skew negativo (tipico di equity) significa che:\n", "- Le put OTM costano di più delle call OTM\n", "- I bull put spread incassano più credito dei bear call spread\n", "- Gli iron condor NON sono simmetrici in termini di premio" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from greche import bs_price\n", "\n", "# Confronto: premio put 5% OTM vs call 5% OTM\n", "S = 5500\n", "T = 45 / 365\n", "r = 0.045\n", "\n", "# Con skew (realistico)\n", "iv_put_5otm = 0.19 # IV della put 5% OTM (più alta per skew)\n", "iv_call_5otm = 0.14 # IV della call 5% OTM (più bassa)\n", "iv_atm = 0.16\n", "\n", "put_strike = S * 0.95 # 5% OTM\n", "call_strike = S * 1.05 # 5% OTM\n", "\n", "put_price = bs_price(S, put_strike, T, r, iv_put_5otm, 'put')\n", "call_price = bs_price(S, call_strike, T, r, iv_call_5otm, 'call')\n", "\n", "# Senza skew (IV flat)\n", "put_price_flat = bs_price(S, put_strike, T, r, iv_atm, 'put')\n", "call_price_flat = bs_price(S, call_strike, T, r, iv_atm, 'call')\n", "\n", "print(\"Impatto dello Skew sul pricing (5% OTM, 45 DTE):\")\n", "print(f\"{'':>25} {'Con Skew':>12} {'IV Flat':>12} {'Differenza':>12}\")\n", "print(f\"{'—'*60}\")\n", "print(f\"{'Put 5% OTM (5225):':>25} ${put_price:>9.2f} ${put_price_flat:>9.2f} ${put_price-put_price_flat:>+9.2f}\")\n", "print(f\"{'Call 5% OTM (5775):':>25} ${call_price:>9.2f} ${call_price_flat:>9.2f} ${call_price-call_price_flat:>+9.2f}\")\n", "print(f\"{'':>25} {'—'*36}\")\n", "print(f\"{'Ratio Put/Call:':>25} {put_price/call_price:>9.2f}x {put_price_flat/call_price_flat:>9.2f}x\")\n", "print(f\"\\nLa put OTM costa {put_price/call_price:.1f}x la call OTM → lo skew è reale e significativo!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 5. Regime di volatilità: quando vendere vs comprare\n", "\n", "Framework decisionale basato su IV Rank e Term Structure:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Matrice decisionale\n", "fig, ax = plt.subplots(figsize=(10, 7))\n", "ax.axis('off')\n", "\n", "decision_matrix = \"\"\"\n", "┌─────────────────────────────────────────────────────────────────────┐\n", "│ MATRICE DECISIONALE: IV Rank × Term Structure │\n", "├──────────────────┬──────────────────────┬──────────────────────────┤\n", "│ │ Contango (normale) │ Backwardation (stress) │\n", "├──────────────────┼──────────────────────┼──────────────────────────┤\n", "│ │ │ │\n", "│ IV Rank > 50% │ VENDERE VOLATILITÀ │ CAUTELA / HEDGE │\n", "│ (IV elevata) │ Iron Condor, Strangle│ Calendar, ridurre size │\n", "│ │ Credit Spread │ Protezione portafoglio │\n", "│ │ │ │\n", "├──────────────────┼──────────────────────┼──────────────────────────┤\n", "│ │ │ │\n", "│ IV Rank < 50% │ NEUTRALE / ATTESA │ COMPRARE VOLATILITÀ │\n", "│ (IV bassa) │ Butterfly, Debit │ Long Straddle/Strangle │\n", "│ │ Calendar Spread │ Backspread, VIX calls │\n", "│ │ │ │\n", "└──────────────────┴──────────────────────┴──────────────────────────┘\n", "\"\"\"\n", "\n", "ax.text(0.05, 0.5, decision_matrix, transform=ax.transAxes,\n", " fontsize=10, verticalalignment='center', fontfamily='monospace',\n", " bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.8))\n", "\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "print(\"Regole pratiche:\")\n", "print(\" • IV Rank > 50% + Contango = condizioni IDEALI per vendita premium\")\n", "print(\" • IV Rank < 30% = non vendere volatilità, il premio non giustifica il rischio\")\n", "print(\" • Backwardation = il mercato sta prezzando rischio imminente, cautela!\")" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.10.0" } }, "nbformat": 4, "nbformat_minor": 4 }