{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Simulazione di Gamma Scalping con Delta Hedging\n", "\n", "**Companion notebook per \"Trading con le Opzioni - Strategie Operative\"** \n", "di Pierpaolo Marturano (Core Matrix S.r.l.)\n", "\n", "Questo notebook simula:\n", "- Long straddle + delta hedging continuo\n", "- Profitto dal gamma vs costo del theta\n", "- Impatto della frequenza di hedging\n", "- Confronto tra diversi regimi di volatilità\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 greche import delta, gamma, theta, bs_price\n", "\n", "plt.style.use('seaborn-v0_8-whitegrid')\n", "plt.rcParams['figure.dpi'] = 100" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Il Principio del Gamma Scalping\n", "\n", "**Idea**: compri un long straddle (long gamma) e fai hedging del delta con il sottostante.\n", "\n", "- Se il sottostante si muove molto → guadagni dal gamma (ribilanciamento)\n", "- Se il sottostante non si muove → perdi dal theta (decadimento)\n", "- **Profitto = Gamma P&L - Theta P&L**\n", "\n", "Il gamma scalping è profittevole quando la **volatilità realizzata > volatilità implicita**." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# === PARAMETRI ===\n", "S0 = 5500 # Prezzo iniziale\n", "K = 5500 # Strike dello straddle (ATM)\n", "T = 30 / 365 # 30 DTE\n", "r = 0.045 # Risk-free\n", "sigma_implied = 0.16 # IV dello straddle (quello che paghiamo)\n", "\n", "# Costo dello straddle\n", "call_price = bs_price(S0, K, T, r, sigma_implied, 'call')\n", "put_price = bs_price(S0, K, T, r, sigma_implied, 'put')\n", "straddle_cost = call_price + put_price\n", "\n", "# Greche iniziali\n", "initial_delta = delta(S0, K, T, r, sigma_implied, 'call') + delta(S0, K, T, r, sigma_implied, 'put')\n", "initial_gamma = 2 * gamma(S0, K, T, r, sigma_implied) # straddle = 2x gamma\n", "initial_theta = theta(S0, K, T, r, sigma_implied, 'call') + theta(S0, K, T, r, sigma_implied, 'put')\n", "\n", "print(\"=\" * 50)\n", "print(\"SETUP GAMMA SCALPING\")\n", "print(\"=\" * 50)\n", "print(f\"\\nLong Straddle {K} Strike, {T*365:.0f} DTE\")\n", "print(f\" Costo: ${straddle_cost:.2f}\")\n", "print(f\" Delta iniziale: {initial_delta:+.4f} (quasi zero → neutrale)\")\n", "print(f\" Gamma: {initial_gamma:.6f}\")\n", "print(f\" Theta: ${initial_theta:.2f}/giorno\")\n", "print(f\"\\nBreakeven giornaliero (movimento necessario per coprire il theta):\")\n", "# Gamma P&L ≈ 0.5 * gamma * dS² → dS = sqrt(2 * |theta| / gamma)\n", "daily_be = np.sqrt(2 * abs(initial_theta) / initial_gamma)\n", "print(f\" ${daily_be:.1f} ({daily_be/S0*100:.2f}% del prezzo)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. Simulazione: un percorso di prezzo" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def simulate_gamma_scalping(S0, K, T, r, sigma_implied, sigma_realized,\n", " hedge_frequency='daily', n_days=None, seed=42):\n", " \"\"\"\n", " Simula gamma scalping su un singolo percorso.\n", " \n", " Parameters\n", " ----------\n", " sigma_implied : IV pagata (costo dello straddle)\n", " sigma_realized : volatilità effettiva del sottostante\n", " hedge_frequency : 'daily', 'twice_daily', 'hourly'\n", " \n", " Returns: dict con risultati dettagliati\n", " \"\"\"\n", " rng = np.random.default_rng(seed)\n", " \n", " if n_days is None:\n", " n_days = int(T * 365)\n", " \n", " # Steps per giorno\n", " if hedge_frequency == 'hourly':\n", " steps_per_day = 7 # ore di mercato\n", " elif hedge_frequency == 'twice_daily':\n", " steps_per_day = 2\n", " else:\n", " steps_per_day = 1\n", " \n", " total_steps = n_days * steps_per_day\n", " dt = 1 / (365 * steps_per_day)\n", " \n", " # Simula il percorso di prezzo\n", " drift = (r - 0.5 * sigma_realized**2) * dt\n", " diffusion = sigma_realized * np.sqrt(dt)\n", " \n", " prices = [S0]\n", " for _ in range(total_steps):\n", " dS = prices[-1] * (drift + diffusion * rng.standard_normal())\n", " prices.append(prices[-1] + dS)\n", " prices = np.array(prices)\n", " \n", " # Gamma scalping: traccia P&L\n", " hedge_position = 0 # shares di sottostante per hedging\n", " hedge_pnl = 0 # P&L dal delta hedging\n", " \n", " hedge_pnl_history = [0]\n", " delta_history = [0]\n", " \n", " for i in range(1, total_steps + 1):\n", " S_curr = prices[i]\n", " S_prev = prices[i-1]\n", " T_remaining = T - i * dt\n", " \n", " if T_remaining <= 0:\n", " T_remaining = 0.0001\n", " \n", " # P&L dalla posizione di hedge\n", " hedge_pnl += hedge_position * (S_curr - S_prev)\n", " \n", " # Calcola nuovo delta dello straddle\n", " straddle_delta = (delta(S_curr, K, T_remaining, r, sigma_implied, 'call') +\n", " delta(S_curr, K, T_remaining, r, sigma_implied, 'put'))\n", " \n", " # Ribilancia: hedge_position = -straddle_delta\n", " hedge_position = -straddle_delta\n", " \n", " hedge_pnl_history.append(hedge_pnl)\n", " delta_history.append(straddle_delta)\n", " \n", " # P&L finale dello straddle (valore intrinseco - costo)\n", " S_final = prices[-1]\n", " straddle_final = max(S_final - K, 0) + max(K - S_final, 0)\n", " straddle_pnl = straddle_final - straddle_cost\n", " \n", " # P&L totale = straddle P&L + hedge P&L\n", " total_pnl = straddle_pnl + hedge_pnl\n", " \n", " return {\n", " 'prices': prices,\n", " 'hedge_pnl_history': np.array(hedge_pnl_history),\n", " 'delta_history': np.array(delta_history),\n", " 'straddle_pnl': straddle_pnl,\n", " 'hedge_pnl': hedge_pnl,\n", " 'total_pnl': total_pnl,\n", " 'straddle_cost': straddle_cost,\n", " 'S_final': S_final,\n", " 'n_days': n_days,\n", " 'steps_per_day': steps_per_day,\n", " }\n", "\n", "# Simulazione con volatilità realizzata > implicita (caso favorevole)\n", "result_high = simulate_gamma_scalping(\n", " S0, K, T, r, sigma_implied=0.16, sigma_realized=0.22, seed=42\n", ")\n", "\n", "# Simulazione con volatilità realizzata < implicita (caso sfavorevole)\n", "result_low = simulate_gamma_scalping(\n", " S0, K, T, r, sigma_implied=0.16, sigma_realized=0.10, seed=42\n", ")\n", "\n", "print(f\"{'Scenario':<30} {'RV=22% (alta)':<18} {'RV=10% (bassa)'}\")\n", "print(f\"{'—'*65}\")\n", "print(f\"{'Straddle P&L:':<30} ${result_high['straddle_pnl']:>+10.2f} ${result_low['straddle_pnl']:>+10.2f}\")\n", "print(f\"{'Hedge P&L:':<30} ${result_high['hedge_pnl']:>+10.2f} ${result_low['hedge_pnl']:>+10.2f}\")\n", "print(f\"{'TOTALE:':<30} ${result_high['total_pnl']:>+10.2f} ${result_low['total_pnl']:>+10.2f}\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Visualizzazione\n", "fig, axes = plt.subplots(2, 2, figsize=(14, 10))\n", "\n", "for col, (result, title) in enumerate([\n", " (result_high, 'RV=22% > IV=16% (profitto)'),\n", " (result_low, 'RV=10% < IV=16% (perdita)')\n", "]):\n", " days = np.linspace(0, result['n_days'], len(result['prices']))\n", " \n", " # Prezzo\n", " ax = axes[0, col]\n", " ax.plot(days, result['prices'], linewidth=1.5, color='#0d5c4d')\n", " ax.axhline(y=K, color='gray', linestyle='--', alpha=0.5, label=f'Strike {K}')\n", " ax.set_title(f'Prezzo sottostante — {title}', fontweight='bold')\n", " ax.set_xlabel('Giorni')\n", " ax.set_ylabel('Prezzo ($)')\n", " ax.legend()\n", " ax.grid(True, alpha=0.3)\n", " \n", " # P&L cumulativo dall'hedging\n", " ax = axes[1, col]\n", " ax.plot(days, result['hedge_pnl_history'], linewidth=1.5, color='#3b82f6',\n", " label='Hedge P&L')\n", " ax.axhline(y=0, color='gray', linewidth=0.8)\n", " \n", " # Linea del theta cumulativo (approssimazione)\n", " theta_cost = np.linspace(0, abs(initial_theta) * result['n_days'], len(days))\n", " ax.plot(days, -theta_cost, linewidth=1.5, color='#ef4444', linestyle='--',\n", " label='Theta cumulativo', alpha=0.7)\n", " \n", " total_final = result['total_pnl']\n", " color = '#10b981' if total_final > 0 else '#ef4444'\n", " ax.set_title(f'P&L del hedging — Totale: ${total_final:+.2f}', \n", " fontweight='bold', color=color)\n", " ax.set_xlabel('Giorni')\n", " ax.set_ylabel('P&L ($)')\n", " ax.legend()\n", " ax.grid(True, alpha=0.3)\n", "\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Impatto della frequenza di hedging" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Confronto: hedging daily vs twice daily vs hourly\n", "sigma_realized = 0.20 # Realizziamo più di quanto pagato\n", "\n", "results_freq = {}\n", "for freq in ['daily', 'twice_daily', 'hourly']:\n", " results_freq[freq] = simulate_gamma_scalping(\n", " S0, K, T, r, sigma_implied=0.16, sigma_realized=sigma_realized,\n", " hedge_frequency=freq, seed=42\n", " )\n", "\n", "print(f\"Impatto della frequenza di hedging (RV={sigma_realized*100:.0f}%, IV=16%):\")\n", "print(f\"{'—'*55}\")\n", "print(f\"{'Frequenza':<20} {'Totale P&L':<15} {'Hedge P&L':<15} {'N. ribilanciamenti'}\")\n", "for freq, res in results_freq.items():\n", " n_rebal = len(res['prices']) - 1\n", " print(f\"{freq:<20} ${res['total_pnl']:>+8.2f} ${res['hedge_pnl']:>+8.2f} {n_rebal}\")\n", "\n", "print(f\"\\nOsservazione: hedging più frequente cattura meglio il gamma,\")\n", "print(f\"ma nella pratica i costi di transazione limitano la frequenza ottimale.\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4. Monte Carlo: distribuzione dei risultati" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Simulazione su molti percorsi\n", "n_simulations = 1000\n", "sigma_realized = 0.20\n", "\n", "total_pnls = []\n", "for seed in range(n_simulations):\n", " res = simulate_gamma_scalping(\n", " S0, K, T, r, sigma_implied=0.16, sigma_realized=sigma_realized,\n", " hedge_frequency='daily', seed=seed\n", " )\n", " total_pnls.append(res['total_pnl'])\n", "\n", "total_pnls = np.array(total_pnls)\n", "\n", "fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n", "\n", "# Istogramma\n", "ax = axes[0]\n", "n, bins, patches = ax.hist(total_pnls, bins=50, density=True, alpha=0.7, edgecolor='white')\n", "for patch, left in zip(patches, bins[:-1]):\n", " patch.set_facecolor('#10b981' if left >= 0 else '#ef4444')\n", "\n", "ax.axvline(x=0, color='black', linewidth=1.5)\n", "ax.axvline(x=np.mean(total_pnls), color='blue', linewidth=1.5, linestyle='--',\n", " label=f'Media: ${np.mean(total_pnls):.2f}')\n", "ax.set_title(f'Distribuzione P&L Gamma Scalping\\n(RV={sigma_realized*100:.0f}%, IV=16%, {n_simulations} sim.)',\n", " fontweight='bold')\n", "ax.set_xlabel('P&L ($)')\n", "ax.set_ylabel('Densità')\n", "ax.legend()\n", "ax.grid(True, alpha=0.3)\n", "\n", "# Statistiche\n", "ax = axes[1]\n", "ax.axis('off')\n", "stats = f\"\"\"\n", " GAMMA SCALPING — Statistiche\n", " {'═' * 40}\n", "\n", " Simulazioni: {n_simulations}\n", " IV pagata: {sigma_implied*100:.0f}%\n", " RV realizzata: {sigma_realized*100:.0f}%\n", " Hedge: Daily\n", "\n", " {'─' * 40}\n", " Win rate: {np.mean(total_pnls > 0)*100:.1f}%\n", " P&L medio: ${np.mean(total_pnls):.2f}\n", " P&L mediano: ${np.median(total_pnls):.2f}\n", " Std dev: ${np.std(total_pnls):.2f}\n", " Max profit: ${np.max(total_pnls):.2f}\n", " Max loss: ${np.min(total_pnls):.2f}\n", " Sharpe: {np.mean(total_pnls)/np.std(total_pnls):.2f}\n", "\"\"\"\n", "ax.text(0.1, 0.95, stats, transform=ax.transAxes,\n", " fontsize=11, verticalalignment='top', fontfamily='monospace',\n", " bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.8))\n", "\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 5. Breakeven: quale RV serve per essere profittevoli?" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Sweep sulla volatilità realizzata\n", "rv_range = np.arange(0.08, 0.35, 0.02)\n", "avg_pnls = []\n", "win_rates = []\n", "\n", "for rv in rv_range:\n", " pnls = []\n", " for seed in range(200):\n", " res = simulate_gamma_scalping(\n", " S0, K, T, r, sigma_implied=0.16, sigma_realized=rv,\n", " hedge_frequency='daily', seed=seed\n", " )\n", " pnls.append(res['total_pnl'])\n", " avg_pnls.append(np.mean(pnls))\n", " win_rates.append(np.mean(np.array(pnls) > 0))\n", "\n", "fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n", "\n", "# P&L medio vs RV\n", "ax = axes[0]\n", "colors = ['#10b981' if p > 0 else '#ef4444' for p in avg_pnls]\n", "ax.bar(rv_range * 100, avg_pnls, width=1.5, color=colors, alpha=0.7)\n", "ax.axhline(y=0, color='black', linewidth=1)\n", "ax.axvline(x=sigma_implied * 100, color='blue', linewidth=2, linestyle='--',\n", " label=f'IV pagata: {sigma_implied*100:.0f}%')\n", "ax.set_title('P&L Medio vs Volatilità Realizzata', fontweight='bold')\n", "ax.set_xlabel('Volatilità Realizzata (%)')\n", "ax.set_ylabel('P&L Medio ($)')\n", "ax.legend()\n", "ax.grid(True, alpha=0.3)\n", "\n", "# Win rate vs RV\n", "ax = axes[1]\n", "ax.plot(rv_range * 100, np.array(win_rates) * 100, 'o-',\n", " linewidth=2, color='#0d5c4d', markersize=6)\n", "ax.axhline(y=50, color='gray', linestyle='--', alpha=0.5)\n", "ax.axvline(x=sigma_implied * 100, color='blue', linewidth=2, linestyle='--',\n", " label=f'IV pagata: {sigma_implied*100:.0f}%')\n", "ax.set_title('Win Rate vs Volatilità Realizzata', fontweight='bold')\n", "ax.set_xlabel('Volatilità Realizzata (%)')\n", "ax.set_ylabel('Win Rate (%)')\n", "ax.set_ylim(0, 100)\n", "ax.legend()\n", "ax.grid(True, alpha=0.3)\n", "\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "# Trova breakeven\n", "for i, p in enumerate(avg_pnls):\n", " if i > 0 and avg_pnls[i-1] < 0 and p >= 0:\n", " be_rv = rv_range[i] * 100\n", " print(f\"\\nBreakeven approssimativo: RV ≈ {be_rv:.0f}%\")\n", " print(f\"Con IV pagata al 16%, serve una RV di almeno ~{be_rv:.0f}% per essere profittevoli.\")\n", " break" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 6. Takeaway Operativi\n", "\n", "**Quando fare gamma scalping:**\n", "- RV attesa > IV pagata (es. prima di eventi con impatto sottostimato)\n", "- IV Rank basso (opzioni \"economiche\")\n", "- Sottostante con alta probabilità di breakout\n", "\n", "**Quando NON farlo:**\n", "- IV Rank alto (straddle troppo caro)\n", "- Mercato in range stretto senza catalizzatori\n", "- Costi di transazione elevati rispetto al gamma disponibile\n", "\n", "**Regola pratica:** il sottostante deve muoversi di almeno `sqrt(2×|theta|/gamma)` al giorno per coprire il costo del theta." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.10.0" } }, "nbformat": 4, "nbformat_minor": 4 }