"""
montecarlo.py - Simulazione Monte Carlo per strategie con opzioni
=================================================================
Companion code per "Trading con le Opzioni - Strategie Operative"
di Pierpaolo Marturano (Core Matrix S.r.l.)

Stima probabilità di profitto, distribuzione dei rendimenti, e VaR
per qualsiasi strategia con opzioni tramite simulazione Monte Carlo.

Requisiti: numpy, scipy, matplotlib
Compatibile con Python 3.10+
"""

import numpy as np
from scipy.stats import norm
import matplotlib.pyplot as plt
from dataclasses import dataclass


@dataclass
class OptionLeg:
    """Gamba di una strategia per la simulazione."""
    option_type: str   # "call" o "put"
    strike: float
    premium: float
    quantity: int      # positivo = long, negativo = short


def simulate_gbm(S0: float, r: float, sigma: float, T: float,
                 n_paths: int, n_steps: int = 252,
                 seed: int | None = None) -> np.ndarray:
    """
    Simula percorsi di prezzo con Geometric Brownian Motion.

    Parameters
    ----------
    S0 : float - Prezzo iniziale
    r : float - Drift (tasso risk-free)
    sigma : float - Volatilità annualizzata
    T : float - Orizzonte temporale (in anni)
    n_paths : int - Numero di simulazioni
    n_steps : int - Passi per percorso
    seed : int - Random seed per riproducibilità

    Returns
    -------
    np.ndarray shape (n_paths, n_steps+1) - Percorsi simulati
    """
    if seed is not None:
        rng = np.random.default_rng(seed)
    else:
        rng = np.random.default_rng()

    dt = T / n_steps
    paths = np.zeros((n_paths, n_steps + 1))
    paths[:, 0] = S0

    # Simulazione log-normale
    drift = (r - 0.5 * sigma**2) * dt
    diffusion = sigma * np.sqrt(dt)

    Z = rng.standard_normal((n_paths, n_steps))
    log_returns = drift + diffusion * Z

    paths[:, 1:] = S0 * np.exp(np.cumsum(log_returns, axis=1))

    return paths


def strategy_payoff_at_expiry(legs: list[OptionLeg],
                              S_final: np.ndarray) -> np.ndarray:
    """
    Calcola il P&L della strategia per un array di prezzi finali.

    Il P&L include il costo/credito iniziale (premium).
    """
    payoff = np.zeros_like(S_final)

    for leg in legs:
        if leg.option_type == "call":
            intrinsic = np.maximum(S_final - leg.strike, 0)
        else:
            intrinsic = np.maximum(leg.strike - S_final, 0)

        # P&L per gamba: quantity * (intrinsic - premium_pagato)
        # Se quantity > 0 (long): pago premium, ricevo intrinsic
        # Se quantity < 0 (short): ricevo premium, pago intrinsic
        payoff += leg.quantity * (intrinsic - leg.premium)

    return payoff


def monte_carlo_strategy(S0: float, r: float, sigma: float, T: float,
                         legs: list[OptionLeg],
                         n_simulations: int = 100_000,
                         seed: int | None = 42) -> dict:
    """
    Esegue simulazione Monte Carlo per una strategia con opzioni.

    Parameters
    ----------
    S0 : float - Prezzo corrente del sottostante
    r : float - Tasso risk-free annualizzato
    sigma : float - Volatilità implicita annualizzata
    T : float - Tempo a scadenza (in anni)
    legs : list[OptionLeg] - Gambe della strategia
    n_simulations : int - Numero di simulazioni
    seed : int - Random seed

    Returns
    -------
    dict con:
        - pnl_array: array dei P&L
        - prob_profit: probabilità di profitto
        - expected_pnl: P&L atteso
        - max_profit: profitto massimo osservato
        - max_loss: perdita massima osservata
        - var_95: Value at Risk al 95%
        - var_99: Value at Risk al 99%
        - median_pnl: P&L mediano
        - std_pnl: deviazione standard del P&L
        - final_prices: prezzi finali simulati
    """
    # Simula solo il prezzo finale (più efficiente per opzioni europee)
    if seed is not None:
        rng = np.random.default_rng(seed)
    else:
        rng = np.random.default_rng()

    # Prezzo finale log-normale
    drift = (r - 0.5 * sigma**2) * T
    diffusion = sigma * np.sqrt(T)
    Z = rng.standard_normal(n_simulations)
    S_final = S0 * np.exp(drift + diffusion * Z)

    # Calcola P&L
    pnl = strategy_payoff_at_expiry(legs, S_final)

    return {
        "pnl_array": pnl,
        "prob_profit": float(np.mean(pnl > 0)),
        "expected_pnl": float(np.mean(pnl)),
        "max_profit": float(np.max(pnl)),
        "max_loss": float(np.min(pnl)),
        "var_95": float(np.percentile(pnl, 5)),
        "var_99": float(np.percentile(pnl, 1)),
        "median_pnl": float(np.median(pnl)),
        "std_pnl": float(np.std(pnl)),
        "final_prices": S_final,
    }


def plot_results(results: dict, strategy_name: str = "Strategia",
                 save_path: str | None = None) -> None:
    """Visualizza i risultati della simulazione Monte Carlo."""
    pnl = results["pnl_array"]

    fig, axes = plt.subplots(1, 2, figsize=(14, 5))

    # Istogramma P&L
    ax1 = axes[0]
    n_bins = 100
    counts, bins, patches = ax1.hist(pnl, bins=n_bins, density=True,
                                      alpha=0.7, edgecolor='none')

    # Colora profitto/perdita
    for patch, left_edge in zip(patches, bins[:-1]):
        if left_edge >= 0:
            patch.set_facecolor('#10b981')
        else:
            patch.set_facecolor('#ef4444')

    ax1.axvline(x=0, color='black', linewidth=1.5, linestyle='-')
    ax1.axvline(x=results["expected_pnl"], color='blue', linewidth=1.5,
               linestyle='--', label=f'E[P&L] = ${results["expected_pnl"]:.2f}')
    ax1.axvline(x=results["var_95"], color='orange', linewidth=1.5,
               linestyle='--', label=f'VaR 95% = ${results["var_95"]:.2f}')

    ax1.set_title(f'{strategy_name} - Distribuzione P&L', fontweight='bold')
    ax1.set_xlabel('Profitto / Perdita ($)')
    ax1.set_ylabel('Densità')
    ax1.legend(fontsize=9)
    ax1.grid(True, alpha=0.3)

    # Statistiche
    ax2 = axes[1]
    ax2.axis('off')
    stats_text = f"""
    {strategy_name}
    {'─' * 40}

    Simulazioni:     {len(pnl):,.0f}

    Prob. Profitto:  {results['prob_profit']*100:.1f}%
    P&L Atteso:      ${results['expected_pnl']:.2f}
    P&L Mediano:     ${results['median_pnl']:.2f}

    Max Profitto:    ${results['max_profit']:.2f}
    Max Perdita:     ${results['max_loss']:.2f}
    Std Dev:         ${results['std_pnl']:.2f}

    VaR 95%:         ${results['var_95']:.2f}
    VaR 99%:         ${results['var_99']:.2f}

    Ratio P/L:       {abs(results['expected_pnl'] / results['var_95']):.2f}x
    """
    ax2.text(0.1, 0.95, stats_text, transform=ax2.transAxes,
            fontsize=11, verticalalignment='top', fontfamily='monospace',
            bbox=dict(boxstyle='round', facecolor='lightyellow', alpha=0.8))

    plt.tight_layout()

    if save_path:
        plt.savefig(save_path, dpi=150, bbox_inches='tight')
        print(f"Grafico salvato in: {save_path}")

    plt.show()


# =============================================================================
# ESEMPIO D'USO
# =============================================================================

if __name__ == "__main__":
    print("=" * 60)
    print("MONTE CARLO - Simulazione Iron Condor SPX 45 DTE")
    print("=" * 60)

    # Parametri di mercato
    S0 = 5500       # SPX attuale
    r = 0.045       # Risk-free 4.5%
    sigma = 0.16    # IV 16%
    T = 45 / 365    # 45 giorni a scadenza

    # Iron Condor: short 5250P/5750C, long 5200P/5800C
    # Strike a circa 16-delta (~4.5% OTM), ali da $50
    # Premi tipici di mercato per IC 50-wide su SPX a 45 DTE:
    net_credit = 3.50
    wing_width = 50.0
    max_loss = wing_width - net_credit

    print(f"\n  Setup: 5200/5250 put — 5750/5800 call (16-delta, ali $50)")
    print(f"  Credito netto: ${net_credit:.2f}")
    print(f"  Max loss: ${max_loss:.2f}")
    print(f"  Risk/Reward: {max_loss/net_credit:.1f}:1")

    legs = [
        OptionLeg("put", 5200, 0, 1),                    # long put
        OptionLeg("put", 5250, net_credit / 2, -1),      # short put
        OptionLeg("call", 5750, net_credit / 2, -1),     # short call
        OptionLeg("call", 5800, 0, 1),                   # long call
    ]

    # Esegui simulazione con IV = sigma di simulazione (risk-neutral)
    results = monte_carlo_strategy(S0, r, sigma, T, legs,
                                   n_simulations=200_000, seed=42)

    print(f"\nScenario 1: simulazione risk-neutral (sigma = IV = {sigma*100:.0f}%)")
    print(f"  Probabilità di profitto: {results['prob_profit']*100:.1f}%")
    print(f"  P&L atteso:             ${results['expected_pnl']:.2f}")
    print(f"  Max profitto:           ${results['max_profit']:.2f}")
    print(f"  Max perdita:            ${results['max_loss']:.2f}")
    print(f"  VaR 95%:                ${results['var_95']:.2f}")

    # Scenario più realistico: RV tipicamente < IV (Variance Risk Premium)
    # In media, la volatilità realizzata di SPX è ~12% quando IV=16%
    sigma_realized = 0.12
    results_vrp = monte_carlo_strategy(S0, r, sigma_realized, T, legs,
                                       n_simulations=200_000, seed=42)

    print(f"\nScenario 2: con Variance Risk Premium (RV={sigma_realized*100:.0f}%, IV={sigma*100:.0f}%)")
    print(f"  Probabilità di profitto: {results_vrp['prob_profit']*100:.1f}%")
    print(f"  P&L atteso:             ${results_vrp['expected_pnl']:.2f}")
    print(f"  P&L mediano:            ${results_vrp['median_pnl']:.2f}")
    print(f"  VaR 95%:                ${results_vrp['var_95']:.2f}")
    print(f"\n  Nota: nella realtà, la IV sovrastima i movimenti effettivi.")
    print(f"  Questo è il Variance Risk Premium (VRP): la differenza tra IV e RV")
    print(f"  è la fonte di rendimento delle strategie di vendita premium.")
    print(f"  Lo scenario 2 è quello più vicino alla realtà operativa.")

    # Visualizza
    plot_results(results, "Iron Condor SPX 5400/5600 (45 DTE)")

    # Esempio 2: Short Strangle
    print("\n" + "=" * 60)
    print("MONTE CARLO - Short Strangle SPX 45 DTE")
    print("=" * 60)

    # Short strangle 16-delta (stessi strike dell'IC, senza ali di protezione)
    strangle_credit = 6.00  # credito totale tipico per strangle 16-delta SPX
    print(f"\n  Setup: short 5250 put / short 5750 call (nudo)")
    print(f"  Credito strangle: ${strangle_credit:.2f}")

    legs_strangle = [
        OptionLeg("put", 5250, strangle_credit / 2, -1),    # short put
        OptionLeg("call", 5750, strangle_credit / 2, -1),   # short call
    ]

    results2 = monte_carlo_strategy(S0, r, sigma, T, legs_strangle,
                                    n_simulations=200_000, seed=42)

    print(f"\nRisultati Short Strangle:")
    print(f"  Probabilità di profitto: {results2['prob_profit']*100:.1f}%")
    print(f"  P&L atteso:             ${results2['expected_pnl']:.2f}")
    print(f"  VaR 95%:                ${results2['var_95']:.2f}")

    plot_results(results2, "Short Strangle SPX 5350/5650 (45 DTE)")
