{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "e3e89421",
   "metadata": {},
   "source": [
    "# Prezzo fisso o prezzo variabile? Backtest 2020-2026\n",
    "\n",
    "**Versione per Google Colab.**\n",
    "\n",
    "Esegui la cella qui sotto prima di tutto il resto: carica il codice e i dati, che su Colab non ci sono. Ti serve il file `backtest_dati_e_codice.zip`, quello che trovi accanto a questo notebook nella cartella `colab/da_caricare/`.\n",
    "\n",
    "Poi puoi fare **Runtime > Esegui tutto**: il notebook gira dall'inizio alla fine senza altri interventi. L'ultima cella scarica grafici e CSV, che altrimenti spariscono alla chiusura della sessione."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ab2d1a92",
   "metadata": {},
   "outputs": [],
   "source": [
    "# =============================================================================\n",
    "#  PREPARAZIONE SU GOOGLE COLAB  ·  esegui questa cella per prima\n",
    "# =============================================================================\n",
    "#\n",
    "#  1. Nel pannello a sinistra apri l'icona della cartella (File).\n",
    "#  2. Trascina dentro il file  backtest_dati_e_codice.zip\n",
    "#  3. Esegui questa cella.\n",
    "#\n",
    "#  Se il file non c'è, la cella te lo chiede e apre il selettore di upload.\n",
    "#  In locale, dove codice e dati ci sono già, non fa nulla.\n",
    "# =============================================================================\n",
    "import sys, zipfile, importlib\n",
    "from pathlib import Path\n",
    "\n",
    "RADICE = Path(\"/content\") if Path(\"/content\").exists() else Path.cwd()\n",
    "PACCO = \"backtest_dati_e_codice.zip\"\n",
    "\n",
    "\n",
    "def _pronto() -> bool:\n",
    "    \"\"\"Vero se codice e dati sono al loro posto.\"\"\"\n",
    "    return ((RADICE / \"src\" / \"dati.py\").exists()\n",
    "            and (RADICE / \"data\" / \"raw\"\n",
    "                 / \"ember_european_wholesale_electricity_price_monthly.csv\").exists())\n",
    "\n",
    "\n",
    "if not _pronto():\n",
    "    zip_path = RADICE / PACCO\n",
    "    if not zip_path.exists():\n",
    "        try:\n",
    "            from google.colab import files           # type: ignore\n",
    "            print(f\"Manca {PACCO}: selezionalo adesso.\")\n",
    "            caricati = files.upload()\n",
    "            if PACCO not in caricati:\n",
    "                raise SystemExit(f\"Hai caricato {list(caricati)}, serve {PACCO}.\")\n",
    "            zip_path = RADICE / PACCO\n",
    "        except ImportError:\n",
    "            raise SystemExit(\n",
    "                f\"Non sono su Colab e non trovo né src/ né {PACCO}. \"\n",
    "                \"Lancia il notebook dalla cartella del progetto.\")\n",
    "    with zipfile.ZipFile(zip_path) as z:\n",
    "        z.extractall(RADICE)\n",
    "    print(f\"estratto {PACCO} in {RADICE}\")\n",
    "\n",
    "if str(RADICE) not in sys.path:\n",
    "    sys.path.insert(0, str(RADICE))\n",
    "\n",
    "# Su Colab pandas, numpy, matplotlib e openpyxl ci sono già. Il controllo serve\n",
    "# a dare un messaggio chiaro invece di un ImportError in mezzo all'analisi.\n",
    "mancanti = [m for m in (\"pandas\", \"numpy\", \"matplotlib\", \"openpyxl\")\n",
    "            if importlib.util.find_spec(m) is None]\n",
    "if mancanti:\n",
    "    print(\"installo:\", mancanti)\n",
    "    import subprocess\n",
    "    subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", *mancanti], check=True)\n",
    "\n",
    "assert _pronto(), \"codice o dati ancora mancanti: ricarica lo zip\"\n",
    "import os\n",
    "os.chdir(RADICE)\n",
    "print(\"pronto. Esegui le celle successive in ordine, oppure Runtime > Esegui tutto.\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "00922ed6",
   "metadata": {},
   "source": [
    "# Prezzo fisso o prezzo variabile? Un backtest sul mercato retail italiano\n",
    "\n",
    "**Cliente domestico residente, elettricità e gas, 2020 - settembre 2026.**\n",
    "\n",
    "Questo notebook risponde con i numeri a una domanda che di solito riceve risposte\n",
    "slogan: *nel medio-lungo periodo conveniva il prezzo fisso o quello indicizzato?*\n",
    "\n",
    "Tre cose da sapere prima di leggere, perché cambiano il significato di tutto il resto.\n",
    "\n",
    "**1. La convenzione di segno, sempre la stessa.**\n",
    "\n",
    "$$\\Delta = \\text{costo della strategia FISSA} - \\text{costo della strategia VARIABILE}$$\n",
    "\n",
    "- $\\Delta > 0$ → in quel periodo il **variabile** è costato meno\n",
    "- $\\Delta < 0$ → in quel periodo il **fisso** è costato meno\n",
    "\n",
    "**2. Il prezzo fisso dura 1 o 2 anni, e poi si rifirma.** Non si simula un prezzo\n",
    "bloccato nel febbraio 2020 e tenuto fino al 2026: non esiste. Le strategie a prezzo\n",
    "bloccato qui dentro si **rinnovano** ogni 12 o 24 mesi alle condizioni disponibili al\n",
    "momento del rinnovo, ed è proprio ai rinnovi che succedono le cose interessanti.\n",
    "\n",
    "**3. Niente informazione futura nel prezzo firmato.** Un prezzo sottoscritto a\n",
    "febbraio 2020 dipende solo da ciò che si sapeva entro gennaio 2020. È il vincolo\n",
    "che separa un backtest da un esercizio circolare, ed è verificato da un test\n",
    "automatico, non promesso a parole.\n",
    "\n",
    "La sintesi numerica dei risultati è **generata dal calcolo** nella sezione 20 e\n",
    "salvata in `outputs/RIEPILOGO.md`: nessun numero di questo notebook è stato\n",
    "scritto a mano."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f810d2bf",
   "metadata": {},
   "source": [
    "## 2. Obiettivo\n",
    "\n",
    "Rispondere in modo quantitativo a dodici domande (sezione 20), ma soprattutto a due:\n",
    "\n",
    "- **quanto vale economicamente la stabilità del prezzo**, cioè quanto ha pagato\n",
    "  (o incassato) un cliente per avere una bolletta prevedibile;\n",
    "- **quanto il risultato dipende dal momento in cui la scelta viene fatta**,\n",
    "  rispetto a quanto dipende dal tipo di contratto.\n",
    "\n",
    "Quello che il notebook **non** fa: non dice quale offerta scegliere domani. Un\n",
    "backtest misura il passato. La sezione 21 elenca per esteso i limiti."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8713d241",
   "metadata": {},
   "source": [
    "## 3. Metodologia\n",
    "\n",
    "### 3.1 Due risultati, separati per costruzione\n",
    "\n",
    "Il brief chiede di distinguere le componenti che la scelta influenza da quelle che\n",
    "non influenza. Qui la distinzione non è una convenzione grafica, è una proprietà\n",
    "dimostrata:\n",
    "\n",
    "| | contenuto |\n",
    "|---|---|\n",
    "| **Risultato 1** | materia energia + quota commerciale, al netto delle imposte |\n",
    "| **Risultato 2** | Risultato 1 + trasporto, oneri di sistema, accise, tutto maggiorato dell'IVA |\n",
    "\n",
    "Siccome le componenti comuni sono identiche nelle due strategie vale\n",
    "\n",
    "$$\\Delta_{\\text{bolletta}} = \\Delta_{\\text{Risultato 1}} \\times (1 + \\text{IVA})$$\n",
    "\n",
    "quindi le componenti comuni **non possono cambiare il segno** del confronto: ne\n",
    "cambiano solo la percentuale, perché ingrossano il denominatore. La sezione 10\n",
    "lo verifica con un `assert`.\n",
    "\n",
    "### 3.2 Il problema vero: da dove viene il prezzo fisso\n",
    "\n",
    "Un prezzo fisso storico **non** si ricostruisce guardando il PUN successivo. Il\n",
    "lavoro usa due livelli, nell'ordine di preferenza del brief.\n",
    "\n",
    "**LIVELLO 1 — dato reale** (elettrico, 2023-01 → 2026-06, 42 mesi su 81).\n",
    "L'Indicatore di Prezzo ARERA misura mese per mese la spesa annua prevista delle\n",
    "offerte a prezzo fisso **effettivamente firmate** dai clienti domestici che hanno\n",
    "cambiato fornitore. Scorporate le componenti regolate dello stesso mese, resta il\n",
    "prezzo della materia energia che si poteva bloccare in quel mese. Non è una\n",
    "ricostruzione: è il prezzo di mercato.\n",
    "\n",
    "**LIVELLO 2 — proxy dichiarata** (elettrico fuori da quella finestra, e tutto il gas).\n",
    "\n",
    "$$P_{\\text{fisso}}(t_0) = \\underbrace{\\overline{PUN}_{[t_0-12,\\,t_0-1]}}_{\\text{solo mesi chiusi}} \\times (1+\\text{perdite}) + \\text{spread} + \\text{premio}$$\n",
    "\n",
    "Il benchmark è strettamente passato, quindi pulito dal leakage. Ma va detto cosa\n",
    "approssima male: **un venditore vero prezza sulla curva forward, non sullo spot\n",
    "passato.** Quando la curva è in contango la proxy sottostima il prezzo fisso;\n",
    "quando è in backwardation lo sovrastima. È il limite principale di tutto il lavoro,\n",
    "e la sezione 9 lo quantifica dove il Livello 1 permette di confrontare le due cose.\n",
    "\n",
    "### 3.3 Ex ante contro ex post\n",
    "\n",
    "Tutto ciò che segue è **ex post**: oggi sappiamo com'è andata. Il cliente che\n",
    "sceglieva nel 2020 non lo sapeva. Nessuna frase di questo notebook dice che una\n",
    "scelta fosse *ovvia*: dice che è risultata meno costosa. La differenza fra le due\n",
    "affermazioni è l'intero argomento del *hindsight bias*, e la sezione 15 la rende\n",
    "misurabile confrontando il break-even ex post con il prezzo ex ante."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "002884bd",
   "metadata": {},
   "outputs": [],
   "source": [
    "import sys, warnings\n",
    "from pathlib import Path\n",
    "\n",
    "warnings.filterwarnings(\"ignore\", category=FutureWarning)\n",
    "RADICE = Path.cwd()\n",
    "if str(RADICE) not in sys.path:\n",
    "    sys.path.insert(0, str(RADICE))\n",
    "\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "from src import config as C, serie_trascritte as S\n",
    "from src import dati as D, profili as P, offerte as O, backtest as B\n",
    "from src import metriche as M, grafici as G\n",
    "\n",
    "pd.set_option(\"display.width\", 200)\n",
    "pd.set_option(\"display.max_columns\", 50)\n",
    "pd.set_option(\"display.float_format\", lambda v: f\"{v:,.3f}\")\n",
    "G.stile()\n",
    "\n",
    "def log(*a):\n",
    "    print(\"·\", *a)\n",
    "\n",
    "log(\"orizzonte elettrico:\", C.EE_PRIMO_MESE, \"->\", C.EE_ULTIMO_MESE)\n",
    "log(\"orizzonte gas:      \", C.GAS_PRIMO_MESE, \"->\", C.GAS_ULTIMO_MESE)\n",
    "print()\n",
    "print(C.NOTA_ORIZZONTE)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "851464f5",
   "metadata": {},
   "source": [
    "## 4. Fonti dati\n",
    "\n",
    "Ogni numero di questo notebook o viene da una delle fonti qui sotto, o è\n",
    "un'assunzione dichiarata in `src/config.py`. Non esiste una terza categoria."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3446b28c",
   "metadata": {},
   "outputs": [],
   "source": [
    "fonti = pd.DataFrame(S.FONTI)\n",
    "for _, r in fonti.iterrows():\n",
    "    print(f\"-- {r['dataset']}\")\n",
    "    print(f\"   fonte     : {r['fonte']}\")\n",
    "    print(f\"   periodo   : {r['periodo']}   frequenza: {r['frequenza']}   unità: {r['unita']}\")\n",
    "    print(f\"   url       : {r['url']}\")\n",
    "    print(f\"   nota      : {r['note']}\\n\")\n",
    "fonti.to_csv(C.OUTPUTS / \"fonti.csv\", index=False, encoding=\"utf-8\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "89c80dc0",
   "metadata": {},
   "source": [
    "### Due cose che il materiale già presente sul PC ha dato, e una che non ha dato\n",
    "\n",
    "Prima di scrivere codice nuovo ho cercato nei progetti esistenti (vedi `README.md`\n",
    "per l'inventario completo). Risultato:\n",
    "\n",
    "- **già c'era, ed è stato riusato**: la tabella dell'Indicatore di Prezzo ARERA per\n",
    "  struttura di prezzo (fisso/variabile), presente in `switching-24h-energy/src/dati_arera.py`.\n",
    "  È il Livello 1 di questo lavoro. Il progetto originale non è stato toccato: qui\n",
    "  c'è una copia, con la fonte accanto.\n",
    "- **già c'era come impianto**: `AI-Energia-Libro/LIBRO_FINALE/codice/gme_serie.py`,\n",
    "  lo scheletro che trasforma gli archivi ZIP del GME in una tabella mensile, e\n",
    "  `AI-Energia-Libro/00-PIANO/fonti/gme.md`, un registro di fonti GME/ARERA con i\n",
    "  valori annui verificati e - cosa più utile - l'elenco esplicito di quelli *non*\n",
    "  verificati. I valori annui di controllo di questo notebook vengono da lì.\n",
    "- **non c'era**: nessuna serie storica mensile di PUN e PSV. Il grafico che\n",
    "  ricordavo esiste (`AI-Energia-Libro/infografiche/infografica-pun-settembre-2026.html`)\n",
    "  ma mostra cinque medie mensili del 2025-2026, non una serie. Le serie sono state\n",
    "  quindi costruite da zero e **validate contro i valori ufficiali**, che è la parte\n",
    "  che conta."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "47ae0103",
   "metadata": {},
   "source": [
    "## 5. Caricamento dati PUN e PSV"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e0bd877a",
   "metadata": {},
   "outputs": [],
   "source": [
    "pun = D.serie_pun()\n",
    "psv = D.serie_psv()\n",
    "comp_ee = D.componenti_ee()\n",
    "comp_gas = D.componenti_gas(1_400.0)\n",
    "\n",
    "log(f\"PUN: {len(pun)} mesi, {pun['pun_eur_mwh'].min():.1f} - {pun['pun_eur_mwh'].max():.1f} EUR/MWh\")\n",
    "log(f\"PSV: {len(psv)} mesi, {psv['psv_eur_smc'].min():.3f} - {psv['psv_eur_smc'].max():.3f} EUR/Smc\")\n",
    "print(\"\\nda quale fonte viene ogni pezzo di PUN:\")\n",
    "print(pun[\"fonte_pun\"].value_counts().to_string())\n",
    "\n",
    "mercato = pun.join(psv[[\"psv_eur_smc\", \"psv_eur_mwh\"]], how=\"outer\")\n",
    "mercato = mercato.join(comp_ee[[\"comuni_nette\", \"materia\", \"iva_aliquota\"]]\n",
    "                      .rename(columns={\"comuni_nette\": \"ee_comuni_nette\",\n",
    "                                       \"materia\": \"ee_materia_tutela\",\n",
    "                                       \"iva_aliquota\": \"ee_iva\"}))\n",
    "mercato = mercato.join(comp_gas[[\"comuni_nette\", \"iva_aliquota\"]]\n",
    "                      .rename(columns={\"comuni_nette\": \"gas_comuni_nette\",\n",
    "                                       \"iva_aliquota\": \"gas_iva\"}))\n",
    "mercato.to_csv(C.DATA_PROC / \"serie_mercato_mensile.csv\", encoding=\"utf-8\")\n",
    "log(\"scritto data/processed/serie_mercato_mensile.csv\")\n",
    "mercato.tail(6)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "734f2f5b",
   "metadata": {},
   "source": [
    "## 6. Data Quality\n",
    "\n",
    "I controlli non sono un adempimento. Due di questi hanno cambiato il lavoro:\n",
    "\n",
    "- il controllo sul **coefficiente kWh/Smc** lo ricava dai mesi in cui la fonte\n",
    "  pubblica il PSV in entrambe le unità, invece di assumerlo: torna 10,700 contro\n",
    "  il 10,700 normativo (PCS 0,038520 GJ/Smc);\n",
    "- il confronto fra le **due fonti del PUN** ha scoperto che fra gennaio e agosto\n",
    "  2025 la serie ENTSO-E e il PUN Index GME divergono fino a 20,8 EUR/MWh, mentre\n",
    "  da settembre 2025 coincidono. È il motivo per cui da gennaio 2025 si usa il\n",
    "  PUN Index: con la riforma del 1/1/2025 gli acquisti su MGP si regolano al\n",
    "  prezzo **zonale** e il PUN Index è la media dei prezzi zonali ponderata per le\n",
    "  quantità acquistate. Usare la serie sbagliata avrebbe falsato il 2025."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a92ce081",
   "metadata": {},
   "outputs": [],
   "source": [
    "controlli = D.controlli_qualita()\n",
    "controlli.to_csv(C.OUTPUTS / \"controlli_qualita.csv\", index=False, encoding=\"utf-8\")\n",
    "print(controlli[[\"controllo\", \"atteso\", \"trovato\", \"tolleranza\", \"esito\", \"bloccante\"]]\n",
    "      .to_string(index=False))\n",
    "print()\n",
    "log(\"esiti:\", controlli[\"esito\"].value_counts().to_dict())\n",
    "D.ferma_se_bloccante(controlli)\n",
    "log(\"nessun controllo bloccante fallito: si può procedere\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8ce77b39",
   "metadata": {},
   "outputs": [],
   "source": [
    "# il PUN contro i valori ufficiali GME, anno per anno\n",
    "annui = pun.groupby(pun.index.year)[\"pun_eur_mwh\"].mean().round(2)\n",
    "conf = pd.DataFrame({\"serie_del_notebook\": annui})\n",
    "conf[\"GME_ufficiale\"] = pd.Series(S.PUN_ANNUALE_UFFICIALE)\n",
    "conf[\"scarto\"] = (conf[\"serie_del_notebook\"] - conf[\"GME_ufficiale\"]).round(2)\n",
    "conf[\"scarto_pct\"] = (conf[\"scarto\"] / conf[\"GME_ufficiale\"] * 100).round(2)\n",
    "print(\"Media annua del PUN: serie ricostruita contro valore GME\\n\")\n",
    "print(conf.to_string())\n",
    "print(\"\\nIl 2026 non ha un valore ufficiale annuo: la serie copre 9 mesi.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "119bd280",
   "metadata": {},
   "outputs": [],
   "source": [
    "p15 = G.g15_eventi(pun)\n",
    "p1 = G.g01_pun(pun)\n",
    "p2 = G.g02_psv(psv)\n",
    "p3 = G.g03_base100(pun, psv)\n",
    "for p in (p15, p1, p2, p3):\n",
    "    log(\"salvato\", p.relative_to(C.RADICE))\n",
    "from IPython.display import Image, display\n",
    "for p in (p15, p1, p2, p3):\n",
    "    display(Image(filename=str(p), width=900))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2dcabd89",
   "metadata": {},
   "source": [
    "### Perché questa serie non si poteva prevedere\n",
    "\n",
    "Il grafico qui sopra è il motivo per cui la domanda «meglio il fisso o il\n",
    "variabile» non ha una risposta valida in astratto. I cinque punti segnati non\n",
    "sono stati scelti a gusto: sono il minimo, il massimo e gli snodi della serie, e\n",
    "ciascuno cade dove è successo qualcosa nel mondo. La cella seguente li tira\n",
    "fuori dai dati e verifica che minimo e massimo siano davvero dove dico."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "11f03ffa",
   "metadata": {},
   "outputs": [],
   "source": [
    "from src import eventi as EV\n",
    "\n",
    "righe = []\n",
    "for mese, breve, cosa in EV.SNODI:\n",
    "    m = pd.Period(mese, \"M\")\n",
    "    righe.append({\"mese\": str(m), \"pun_eur_mwh\": round(float(pun.loc[m, \"pun_eur_mwh\"]), 1),\n",
    "                  \"snodo\": breve, \"cosa stava succedendo\": cosa})\n",
    "snodi = pd.DataFrame(righe)\n",
    "print(snodi.to_string(index=False))\n",
    "snodi.to_csv(C.OUTPUTS / \"snodi_del_prezzo.csv\", index=False, encoding=\"utf-8\")\n",
    "\n",
    "# i due estremi dichiarati devono essere gli estremi VERI della serie\n",
    "vero_min, vero_max = pun[\"pun_eur_mwh\"].idxmin(), pun[\"pun_eur_mwh\"].idxmax()\n",
    "assert str(vero_min) == EV.SNODI[0][0], f\"il minimo vero è {vero_min}\"\n",
    "assert str(vero_max) == EV.SNODI[2][0], f\"il massimo vero è {vero_max}\"\n",
    "\n",
    "minimo = float(pun[\"pun_eur_mwh\"].min())\n",
    "massimo = float(pun[\"pun_eur_mwh\"].max())\n",
    "dopo = float(pun.loc[pd.Period(EV.SNODI[3][0], \"M\"), \"pun_eur_mwh\"])\n",
    "mesi_fra = (pd.Period(EV.SNODI[2][0], \"M\") - pd.Period(EV.SNODI[0][0], \"M\")).n\n",
    "mesi_dopo = (pd.Period(EV.SNODI[3][0], \"M\") - pd.Period(EV.SNODI[2][0], \"M\")).n\n",
    "print(f\"\"\"\n",
    "Dal minimo al massimo passano {mesi_fra} mesi e il prezzo si moltiplica per {massimo / minimo:.0f} ({minimo:.1f} -> {massimo:.1f} EUR/MWh). Altri {mesi_dopo} mesi e il massimo si è diviso per {massimo / dopo:.0f}.\n",
    "\n",
    "Nessuno di questi movimenti era deducibile da ciò che si sapeva al momento della\n",
    "firma, perché a muoverli non sono stati i fondamentali del mercato elettrico ma\n",
    "una pandemia, una guerra e un inverno. Non è un dettaglio di colore: è la\n",
    "ragione per cui tutto il resto del notebook è costruito per non barare sul\n",
    "tempo. Se bastasse guardare il grafico, il backtest non servirebbe.\n",
    "\n",
    "{EV.NOTA}\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "65050735",
   "metadata": {},
   "source": [
    "### Il rincaro di ottobre 2026, che la serie non vede\n",
    "\n",
    "Il backtest si ferma a settembre 2026 perché è l'ultimo mese chiuso del prezzo\n",
    "all'ingrosso. La tabella ARERA delle componenti, però, arriva già al IV\n",
    "trimestre: contiene cioè l'aggiornamento che entra in vigore a ottobre, e vale\n",
    "la pena guardarlo, perché è il motivo per cui la domanda «fisso o variabile»\n",
    "pesa adesso più che in un trimestre tranquillo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0e34e36e",
   "metadata": {},
   "outputs": [],
   "source": [
    "from src.dati import _tabella_arera\n",
    "\n",
    "grezza = _tabella_arera(\"ARERA_eep35new_prezzo_elettrico_domestico_tipo.xlsx\",\n",
    "                        \"tabella 2700\", 5)\n",
    "grezza[\"totale\"] = grezza[[\"materia\", \"trasporto\", \"oneri\", \"imposte\"]].sum(axis=1)\n",
    "tri = grezza.groupby([grezza.index.year, grezza.index.quarter]).first()\n",
    "ult = tri.loc[2026][[\"materia\", \"trasporto\", \"oneri\", \"imposte\", \"totale\"]]\n",
    "print(\"ARERA, cliente domestico tipo 2.700 kWh / 3 kW - centesimi di EUR per kWh\\n\")\n",
    "print(ult.round(2).to_string())\n",
    "\n",
    "q3, q4 = float(ult.loc[3, \"totale\"]), float(ult.loc[4, \"totale\"])\n",
    "m3, m4 = float(ult.loc[3, \"materia\"]), float(ult.loc[4, \"materia\"])\n",
    "consumo = C.ClienteElettrico().consumo_annuo_kwh\n",
    "rincaro = {\n",
    "    \"totale_q3\": q3, \"totale_q4\": q4, \"var_totale_pct\": (q4 / q3 - 1) * 100,\n",
    "    \"materia_q3\": m3, \"materia_q4\": m4, \"var_materia_pct\": (m4 / m3 - 1) * 100,\n",
    "    \"spesa_q3_eur_anno\": q3 * consumo / 100, \"spesa_q4_eur_anno\": q4 * consumo / 100,\n",
    "    \"quota_materia_q4\": m4 / q4 * 100,\n",
    "}\n",
    "print(f\"\"\"\n",
    "IV trimestre 2026 contro III, sul prezzo di riferimento:\n",
    "  totale          {q3:.2f} -> {q4:.2f} c/kWh   {rincaro['var_totale_pct']:+.1f}%\n",
    "  materia energia {m3:.2f} -> {m4:.2f} c/kWh   {rincaro['var_materia_pct']:+.1f}%\n",
    "  spesa annua del cliente tipo: {rincaro['spesa_q3_eur_anno']:.0f} -> \\\n",
    "{rincaro['spesa_q4_eur_anno']:.0f} EUR  ({(q4 - q3) * consumo / 100:+.0f} EUR/anno)\n",
    "\n",
    "Una precisazione su come si cita questo numero: \\\n",
    "{str(round(rincaro['var_totale_pct'], 1)).replace('.', ',')}% è la variazione \\\n",
    "secca del prezzo di riferimento in centesimi al kWh, da trimestre a trimestre. \\\n",
    "Le percentuali che si leggono in giro sono spesso un po' più basse perché \\\n",
    "pesano i consumi sul profilo della famiglia tipo. Non è che una sia sbagliata: \\\n",
    "sono due basi diverse, e vanno dette.\n",
    "\n",
    "Il salto è quasi tutto sulla materia energia ({rincaro['var_materia_pct']:+.0f}%),\n",
    "che nel IV trimestre arriva a pesare il {rincaro['quota_materia_q4']:.0f}% della\n",
    "bolletta. È la componente che il cliente sceglie, ed è il motivo per cui la\n",
    "domanda «fisso o variabile» pesa adesso più che in un trimestre tranquillo.\"\"\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9d97b959",
   "metadata": {},
   "outputs": [],
   "source": [
    "scelte = D.sintesi_scelte_retail(ultimi=12)\n",
    "tab_scelte = D.quota_che_sceglie_il_fisso()\n",
    "print(\"Fra chi cambia fornitore, chi sceglie il prezzo bloccato\\n\")\n",
    "print(f\"  mesi utilizzabili        {scelte['mesi']}  ({scelte['dal']} / {scelte['al']})\")\n",
    "print(f\"  mesi scartati            {scelte['mesi_scartati']}\")\n",
    "print(f\"  scelte osservate         {scelte['clienti_totali']:,}\".replace(\",\", \".\"))\n",
    "print(f\"  hanno scelto il FISSO    {scelte['quota_fisso_complessiva']:.1%} del totale\")\n",
    "print(f\"  negli ultimi {scelte['ultimi']} mesi     {scelte['quota_fisso_ultimi_mesi']:.1%}\")\n",
    "print(f\"  oscillazione mensile     da {scelte['quota_min']:.1%} a {scelte['quota_max']:.1%}\")\n",
    "print(f\"\\nFonte: {tab_scelte.attrs['fonte']}\")\n",
    "print(f\"\"\"\n",
    "Attenzione a cosa misura. Questa NON è la quota di contratti a prezzo fisso in\n",
    "giro per l'Italia: è la quota di chi, avendo cambiato fornitore in quel mese, ha\n",
    "scelto il prezzo bloccato. È un flusso, non un parco installato, ed è la cosa\n",
    "che serve a chi deve rispondere a un cliente adesso.\n",
    "\n",
    "Tre mesi su {scelte['mesi'] + len(scelte['mesi_scartati'])} sono stati scartati\n",
    "perché la fonte ci riporta un conteggio non utilizzabile su uno dei due lati.\n",
    "Sommarlo come se fosse zero avrebbe fatto sembrare che ad aprile 2026 solo un\n",
    "quarto dei clienti scegliesse il fisso, subito dopo un marzo al\n",
    "{tab_scelte['quota_fisso'].iloc[-1]:.0%}: un crollo che non è successo.\n",
    "\n",
    "E l'oscillazione da {scelte['quota_min']:.0%} a {scelte['quota_max']:.0%} fra un\n",
    "mese e l'altro dice una cosa utile da sola: non è una preferenza stabile dei\n",
    "clienti, è una reazione a quello che il mercato offre in quel momento.\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2e76411b",
   "metadata": {},
   "source": [
    "## 7. Profili di consumo: elettrico e gas\n",
    "\n",
    "Il costo è prezzo per quantità: se la quantità non è piatta, i mesi cari pesano\n",
    "più dei mesi economici. Entrambi i profili sono **assunzioni parametriche\n",
    "dichiarate**, non dati:\n",
    "\n",
    "- **elettrico**: due armoniche, picco invernale (±8%) e picco estivo minore (±4%).\n",
    "  Una ripartizione mensile ufficiale dei consumi *domestici* italiani non è\n",
    "  pubblicata in forma utilizzabile (i dati Terna sono nazionali e includono\n",
    "  l'industria).\n",
    "- **gas**: struttura fisica. Una quota costante per cottura e acqua calda (22%) più\n",
    "  il resto distribuito sui gradi giorno di una zona climatica E, con il\n",
    "  riscaldamento acceso secondo il DPR 412/93.\n",
    "\n",
    "Nella sezione 19 il backtest gira anche con profilo **piatto**, per misurare quanto\n",
    "questa assunzione pesa sulla risposta. Anticipo: sull'elettrico quasi niente, sul\n",
    "gas molto."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bf7ba2cb",
   "metadata": {},
   "outputs": [],
   "source": [
    "pesi_ee, pesi_gas = P.pesi_elettrico(), P.pesi_gas()\n",
    "tab_pesi = pd.DataFrame({\n",
    "    \"mese\": [\"gen\",\"feb\",\"mar\",\"apr\",\"mag\",\"giu\",\"lug\",\"ago\",\"set\",\"ott\",\"nov\",\"dic\"],\n",
    "    \"elettrico_%\": (pesi_ee * 100).round(2),\n",
    "    \"gas_%\": (pesi_gas * 100).round(2),\n",
    "    \"elettrico_kWh_su_2700\": (pesi_ee * 2700).round(0),\n",
    "    \"gas_Smc_su_1000\": (pesi_gas * 1000).round(0),\n",
    "})\n",
    "print(tab_pesi.to_string(index=False))\n",
    "log(f\"somma pesi elettrico {pesi_ee.sum():.10f} | gas {pesi_gas.sum():.10f}\")\n",
    "log(f\"gas, quota da ottobre a marzo: {pesi_gas[[9,10,11,0,1,2]].sum()*100:.1f}%\")\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(9.6, 4.2))\n",
    "x = np.arange(12)\n",
    "ax.bar(x - 0.21, pesi_ee * 100, width=0.40, color=G.VARIABILE, label=\"elettrico\",\n",
    "       edgecolor=G.SUPERFICIE, linewidth=2)\n",
    "ax.bar(x + 0.21, pesi_gas * 100, width=0.40, color=G.FISSO12, label=\"gas\",\n",
    "       edgecolor=G.SUPERFICIE, linewidth=2)\n",
    "ax.axhline(100/12, color=G.INK2, linewidth=1.1, linestyle=(0, (4, 3)))\n",
    "ax.annotate(\"profilo piatto = 8,3%\", xy=(11.4, 100/12), xytext=(0, 6),\n",
    "            textcoords=\"offset points\", fontsize=9, color=G.INK2, ha=\"right\")\n",
    "ax.set_xticks(x, tab_pesi[\"mese\"])\n",
    "ax.set_ylabel(\"% del consumo annuo\")\n",
    "ax.set_title(\"Due profili molto diversi\")\n",
    "ax.legend(loc=\"upper right\", ncols=2)\n",
    "ax.grid(axis=\"x\", visible=False)\n",
    "fig.savefig(C.CHARTS / \"00_profili_consumo.png\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0b36fbdc",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Quanto concentra ciascun profilo, e se i mesi concentrati siano davvero i piu' cari\n",
    "inverno = [9, 10, 11, 0, 1, 2]                      # ottobre -> marzo\n",
    "profili = {\n",
    "    \"gas_inverno_pct\": float(pesi_gas[inverno].sum() * 100),\n",
    "    \"gas_rapporto_max_min\": float(pesi_gas.max() / pesi_gas.min()),\n",
    "    \"ee_inverno_pct\": float(pesi_ee[inverno].sum() * 100),\n",
    "    \"ee_rapporto_max_min\": float(pesi_ee.max() / pesi_ee.min()),\n",
    "}\n",
    "per_mese = pun.groupby(pun.index.month)[\"pun_eur_mwh\"].mean()\n",
    "nomi = \"gen feb mar apr mag giu lug ago set ott nov dic\".split()\n",
    "profili[\"mese_piu_caro\"] = nomi[int(per_mese.idxmax()) - 1]\n",
    "profili[\"mese_piu_caro_valore\"] = float(per_mese.max())\n",
    "profili[\"pun_ott_mar\"] = float(per_mese[[10, 11, 12, 1, 2, 3]].mean())\n",
    "profili[\"pun_apr_set\"] = float(per_mese[[4, 5, 6, 7, 8, 9]].mean())\n",
    "\n",
    "print(\"Concentrazione del consumo\\n\")\n",
    "print(f\"  gas:       {profili['gas_inverno_pct']:.0f}% fra ottobre e marzo, \"\n",
    "      f\"{profili['gas_rapporto_max_min']:.0f} volte piu' nel mese di punta che in quello di minimo\")\n",
    "print(f\"  elettrico: {profili['ee_inverno_pct']:.0f}% fra ottobre e marzo, \"\n",
    "      f\"rapporto {profili['ee_rapporto_max_min']:.2f} (quasi piatto)\")\n",
    "print(\"\\nPUN medio per mese dell'anno, 2020-2026 (EUR/MWh)\\n\")\n",
    "print(\"  \" + \"  \".join(f\"{n} {v:5.0f}\" for n, v in zip(nomi, per_mese)))\n",
    "print(f\"\"\"\n",
    "Un controllo che vale la pena fare, perche' smentisce un luogo comune: in questa\n",
    "serie l'inverno NON e' sistematicamente piu' caro. Ottobre-marzo sta a\n",
    "{profili['pun_ott_mar']:.1f} EUR/MWh, aprile-settembre a {profili['pun_apr_set']:.1f}:\n",
    "{(profili['pun_ott_mar'] / profili['pun_apr_set'] - 1) * 100:+.1f}%. Il mese piu'\n",
    "caro dell'intero periodo e' un {profili['mese_piu_caro']}\n",
    "({profili['mese_piu_caro_valore']:.0f} EUR/MWh di media), per via del picco 2022 e\n",
    "della risalita 2026.\n",
    "\n",
    "Resta vero, e indipendente da quale mese sia caro, che un profilo CONCENTRATO e'\n",
    "piu' esposto: chi compra l'{profili['gas_inverno_pct']:.0f}% dell'energia in sei\n",
    "mesi punta tutto su quei sei mesi, e se capitano cari la bolletta salta molto piu'\n",
    "di quella di chi compra un dodicesimo al mese. Non e' una previsione sui prezzi,\n",
    "e' aritmetica su prezzo per quantita'.\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2c154cb7",
   "metadata": {},
   "source": [
    "### Dove siamo, e di quale fornitura si parla\n",
    "\n",
    "Da qui alla sezione 16 si parla **solo di energia elettrica**. Il gas ha la\n",
    "sezione 17, che rifà le stesse domande sui suoi dati, e la 18 somma le due\n",
    "forniture.\n",
    "\n",
    "Non è pedanteria: le due materie prime hanno unità di misura diverse (EUR/MWh\n",
    "contro EUR/Smc), clienti di riferimento diversi (2.700 kWh contro 1.000 Smc) e\n",
    "soprattutto un dato di partenza diverso. Sull'elettrico 42 mesi su 81 sono prezzi\n",
    "retail realmente firmati; sul gas non ce n'è nessuno, e il prezzo bloccato è\n",
    "interamente un modello. Un numero elettrico e un numero gas non si confrontano.\n",
    "\n",
    "## 8. Modello dell'offerta variabile (elettricità)\n",
    "\n",
    "$$P_{\\text{var}}(t) = PUN(t) \\times (1 + \\text{perdite}) + \\text{markup}$$\n",
    "\n",
    "Le perdite di rete standard in bassa tensione sono il 10,4% (TIS ARERA): l'indice\n",
    "si applica all'energia *prelevata*, non a quella consumata.\n",
    "\n",
    "Sul markup il brief chiedeva di provare 0 / 0,010 / 0,020 / 0,030 EUR/kWh. Qui\n",
    "quella griglia c'è (sezione 19), ma prima conviene **misurare** quanto costavano\n",
    "davvero le offerte variabili sopra il PUN, perché i 42 mesi dell'Indicatore di\n",
    "Prezzo ARERA lo permettono. Il risultato cambia l'interpretazione della griglia."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "15633314",
   "metadata": {},
   "outputs": [],
   "source": [
    "sp_var = D.spread_variabile_misurato(\"dec10\")[\"spread_implicito_eur_kwh\"]\n",
    "sp_var_media = D.spread_variabile_misurato(\"media\")[\"spread_implicito_eur_kwh\"]\n",
    "print(\"Markup misurato delle offerte VARIABILI reali sopra il PUN del mese (EUR/kWh)\\n\")\n",
    "print(pd.DataFrame({\n",
    "    \"decile più conveniente\": sp_var.describe()[[\"count\",\"mean\",\"50%\",\"min\",\"max\"]],\n",
    "    \"media delle offerte scelte\": sp_var_media.describe()[[\"count\",\"mean\",\"50%\",\"min\",\"max\"]],\n",
    "}).round(4).to_string())\n",
    "print(f\"\"\"\n",
    "Lettura. Il markup misurato sul decile più conveniente ha mediana\n",
    "{sp_var.median():.4f} EUR/kWh, cioè {sp_var.median()*100:.2f} centesimi: molto più\n",
    "dei 'tre centesimi' di cui si parla negli spread commerciali. Non è un errore ed\n",
    "è importante capirlo: la voce 'spesa per la materia energia' contiene anche il\n",
    "DISPACCIAMENTO, che non è margine del venditore ma un costo di sistema. Qualunque\n",
    "spread della griglia del brief, applicato da solo al PUN, descrive quindi\n",
    "un'offerta che in Italia non esiste. Il modello usa come caso base il markup\n",
    "misurato, e la griglia del brief come esplorazione.\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c50ffaf9",
   "metadata": {},
   "source": [
    "## 9. Modello dell'offerta fissa (elettricità)\n",
    "\n",
    "Qui si vede se la proxy del Livello 2 regge. La domanda è precisa: *il benchmark\n",
    "retrospettivo a 12 mesi assomiglia a quello che il mercato chiedeva davvero per\n",
    "bloccare il prezzo?*"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a2dd1f0d",
   "metadata": {},
   "outputs": [],
   "source": [
    "mis = O.misura_markup(\"dec10\")\n",
    "print(\"Markup misurati sui 42 mesi del Livello 1 (EUR/kWh)\\n\")\n",
    "print(f\"  offerte VARIABILI sopra il PUN del mese        : mediana {mis.markup_variabile:+.4f}\"\n",
    "      f\"   (da {mis.markup_variabile_min:+.4f} a {mis.markup_variabile_max:+.4f})\")\n",
    "print(f\"  offerte FISSE sopra il benchmark a 12 mesi     : mediana {mis.markup_fisso:+.4f}\"\n",
    "      f\"   (da {mis.markup_fisso_min:+.4f} a {mis.markup_fisso_max:+.4f})\")\n",
    "print(f\"\"\"\n",
    "Il numero che conta è il secondo intervallo. In alcuni mesi le offerte fisse reali\n",
    "stavano {abs(mis.markup_fisso_min)*100:.1f} centesimi SOTTO il benchmark retrospettivo.\n",
    "Non è un'anomalia: nel 2023-2026 i prezzi scendevano, e un venditore che prezza\n",
    "sulla curva forward offre meno della media degli ultimi dodici mesi. Nel 2021,\n",
    "quando i prezzi salivano, l'effetto è opposto.\n",
    "\n",
    "Conseguenza operativa, da tenere a mente leggendo tutto il resto: nei periodi di\n",
    "Livello 2 la proxy SOVRASTIMA il prezzo fisso quando il mercato scende e lo\n",
    "SOTTOSTIMA quando sale. Siccome il 2021 saliva, la proxy del 2021 è probabilmente\n",
    "troppo generosa col fisso, e il vantaggio del fisso misurato per gli ingressi 2020\n",
    "è quindi una stima PER DIFETTO del vero.\"\"\")\n",
    "\n",
    "print(\"\"\"\n",
    "Una conseguenza pratica, che vale oggi e non nel 2020. Se le quotazioni a\n",
    "termine del prezzo all'ingrosso stanno SOTTO lo spot corrente, un prezzo\n",
    "bloccato firmato adesso parte più basso del prezzo indicizzato di adesso. Non è\n",
    "uno sconto del venditore: è quello che il mercato a termine si aspetta. Chi\n",
    "blocca sta scommettendo che quell'aspettativa sia sbagliata per difetto, chi\n",
    "resta indicizzato scommette il contrario, e nessuna delle due è una previsione.\n",
    "\n",
    "Questo notebook non misura le quotazioni a termine e non le ha fra le sue fonti:\n",
    "il punto resta che il meccanismo è quello, ed è il motivo per cui il benchmark\n",
    "retrospettivo del Livello 2 va letto per quello che è, una proxy dichiarata.\"\"\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0ce2a8e9",
   "metadata": {},
   "outputs": [],
   "source": [
    "sp_f = O.benchmark_retrospettivo(D.serie_pun(riscaldamento_mesi=12)[\"pun_eur_kwh\"], 12)\n",
    "ip_f = D.indicatore_prezzo(\"dec10\").tabella.loc[\"fisso\"]\n",
    "serie_mf = (ip_f[\"materia_eur_kwh\"] - sp_f.reindex(ip_f.index) * (1 + C.PERDITE_RETE_BT)).dropna()\n",
    "p14 = G.g14_markup_misurati(serie_mf, sp_var)\n",
    "log(\"salvato\", p14.relative_to(C.RADICE))\n",
    "from IPython.display import Image, display\n",
    "display(Image(filename=str(p14), width=900))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cf2cdd38",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Il prezzo fisso firmabile, mese per mese: dove viene dal Livello 1 e dove dalla proxy\n",
    "mod_base = O.modello_ee()\n",
    "righe = []\n",
    "for m in pd.period_range(C.EE_PRIMO_MESE, C.EE_ULTIMO_MESE, freq=\"M\"):\n",
    "    p, liv = mod_base.prezzo_fisso_alla_firma(m)\n",
    "    righe.append({\"mese\": m, \"prezzo_fisso_eur_kwh\": p, \"livello\": liv})\n",
    "fisso_firmabile = pd.DataFrame(righe).set_index(\"mese\")\n",
    "print(fisso_firmabile[\"livello\"].value_counts().to_string())\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(10, 4.6))\n",
    "for liv, c in ((O.LIVELLO_REALE, G.FISSO12), (O.LIVELLO_PROXY, G.INK3)):\n",
    "    d = fisso_firmabile[fisso_firmabile[\"livello\"] == liv]\n",
    "    ax.plot([p.to_timestamp() for p in d.index], d[\"prezzo_fisso_eur_kwh\"],\n",
    "            color=c, linewidth=2.2 if liv == O.LIVELLO_REALE else 1.8,\n",
    "            linestyle=\"-\" if liv == O.LIVELLO_REALE else (0, (5, 2)), label=liv)\n",
    "ax.set_title(\"Il prezzo fisso che si poteva firmare, mese per mese\")\n",
    "ax.set_ylabel(\"EUR/kWh (materia energia)\")\n",
    "ax.legend(loc=\"upper left\")\n",
    "ax.annotate(\"tratteggio = proxy, non prezzo di mercato\", xy=(0.015, 0.84),\n",
    "            xycoords=\"axes fraction\", fontsize=9.5, color=G.INK3)\n",
    "fig.savefig(C.CHARTS / \"00_prezzo_fisso_firmabile.png\")\n",
    "plt.show()\n",
    "fisso_firmabile.to_csv(C.OUTPUTS / \"prezzo_fisso_firmabile.csv\", encoding=\"utf-8\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3e660b76",
   "metadata": {},
   "source": [
    "## 10. Backtest storico (elettricità)\n",
    "\n",
    "Prima di guardare qualunque risultato: la verifica che le componenti comuni non\n",
    "possono ribaltare il confronto. Se questo `assert` cade, il Risultato 2 non è\n",
    "affidabile e tutto quello che segue va riletto."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6bc492e7",
   "metadata": {},
   "outputs": [],
   "source": [
    "mod = O.modello_ee()                       # markup misurati, premio medio, fisso 12M\n",
    "ctx = B.ContestoEE.costruisci(mod, C.ClienteElettrico().consumo_annuo_kwh)\n",
    "log(f\"markup variabile {mod.markup_variabile:.4f} | markup fisso {mod.markup_fisso:.4f} EUR/kWh\")\n",
    "log(f\"quote commerciali: variabile {mod.quota_var_anno:.0f} EUR/anno, fisso {mod.quota_fisso_anno:.0f} EUR/anno\")\n",
    "\n",
    "for t0 in (\"2020-02\", \"2022-01\", \"2024-01\"):\n",
    "    v = B.verifica_identita_delta(ctx, pd.Period(t0, \"M\"))\n",
    "    log(f\"{t0}: Delta_r1 {v['delta_r1']:+8.2f} | Delta_r2 {v['delta_r2']:+8.2f} | \"\n",
    "        f\"scarto sull'identità {v['scarto_identita']:.2e}\")\n",
    "print(\"\\nL'identità Delta_r2 = Delta_r1 x (1+IVA) regge a meno dell'errore di macchina,\")\n",
    "print(\"e le componenti comuni risultano identiche fra le due strategie: il Risultato 2\")\n",
    "print(\"non può cambiare il segno del confronto, solo la sua percentuale.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bf07f158",
   "metadata": {},
   "outputs": [],
   "source": [
    "# la matrice completa: ogni mese di ingresso, fino a fine serie\n",
    "matrice_fine = B.matrice_ingressi(ctx, orizzonte=None)\n",
    "matrice_fine[\"delta_r2_anno_fisso_12M\"] = (matrice_fine[\"delta_r2_fisso_12M\"]\n",
    "                                           / (matrice_fine[\"mesi\"] / 12))\n",
    "matrice_fine[\"delta_r2_anno_fisso_24M\"] = (matrice_fine[\"delta_r2_fisso_24M\"]\n",
    "                                           / (matrice_fine[\"mesi\"] / 12))\n",
    "log(f\"{len(matrice_fine)} mesi di ingresso calcolati\")\n",
    "print(matrice_fine[[\"mesi\", \"r2_variabile\", \"r2_fisso_12M\", \"r2_fisso_24M\",\n",
    "                    \"delta_r2_fisso_12M\", \"delta_pct_r2_fisso_12M\",\n",
    "                    \"vince_fisso_12M\"]].head(10).to_string())\n",
    "matrice_fine.drop(columns=[c for c in matrice_fine.columns if c.startswith(\"_\")]) \\\n",
    "            .to_csv(C.OUTPUTS / \"matrice_ingressi_fino_a_fine.csv\", encoding=\"utf-8\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4c701c48",
   "metadata": {},
   "source": [
    "## 11. Scenario febbraio 2020 (elettricità)\n",
    "\n",
    "Lo scenario esplicito del brief: il cliente decide a febbraio 2020 e tiene la\n",
    "scelta fino alla fine della serie. Siccome tutto ciò che precede la data di\n",
    "decisione è identico nei due rami, confrontare «fisso → variabile» e\n",
    "«variabile → fisso» alla stessa data equivale esattamente a confrontare le due\n",
    "strategie da quella data in avanti: la storia precedente si cancella nel $\\Delta$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f847d09f",
   "metadata": {},
   "outputs": [],
   "source": [
    "t0 = pd.Period(\"2020-02\", \"M\")\n",
    "ris_feb = B.confronta(ctx, t0, orizzonte=None)\n",
    "met_feb = M.metriche_scenario(ris_feb, \"fisso_12M\")\n",
    "\n",
    "ordine = [\"mesi\",\"anni\",\"costo_variabile_r1\",\"costo_fisso_r1\",\"costo_variabile_r2\",\n",
    "          \"costo_fisso_r2\",\"delta_r1_eur\",\"delta_r2_eur\",\"delta_r2_pct\",\n",
    "          \"delta_r2_eur_anno\",\"meno_costosa_ex_post\",\n",
    "          \"costo_medio_variabile_eur_kWh\",\"costo_medio_fisso_eur_kWh\",\n",
    "          \"sd_mensile_variabile\",\"sd_mensile_fisso\",\"riduzione_volatilita_pct\",\n",
    "          \"mese_peggiore_variabile\",\"mese_peggiore_fisso\",\"quando_peggiore_variabile\",\n",
    "          \"max_shock_annuale_variabile\",\"max_shock_annuale_fisso\",\n",
    "          \"picco_mensile_evitato_eur\",\"shock_annuale_evitato_eur\",\n",
    "          \"break_even_fisso_eur_unita\",\"fic_eur_anno\",\"fic_per_unita_di_rischio\",\n",
    "          \"max_vantaggio_variabile_cum\",\"max_vantaggio_fisso_cum\"]\n",
    "for k in ordine:\n",
    "    v = met_feb[k]\n",
    "    print(f\"  {k:36} {v if not isinstance(v, float) else f'{v:,.4f}'}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ef8a9468",
   "metadata": {},
   "outputs": [],
   "source": [
    "p4 = G.g04_cumulato(ris_feb, \"Febbraio 2020: chi ha speso meno, alla fine\")\n",
    "p5 = G.g05_delta_cumulato(ris_feb, \"fisso_12M\")\n",
    "p6 = G.g06_bolletta_mensile(ris_feb, \"fisso_12M\")\n",
    "from IPython.display import Image, display\n",
    "for p in (p4, p5, p6):\n",
    "    log(\"salvato\", p.relative_to(C.RADICE)); display(Image(filename=str(p), width=900))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "deb7fa6d",
   "metadata": {},
   "outputs": [],
   "source": [
    "d12, d24 = M.metriche_scenario(ris_feb, \"fisso_12M\"), M.metriche_scenario(ris_feb, \"fisso_24M\")\n",
    "print(f\"\"\"Febbraio 2020, {d12['mesi']} mesi. Nella convenzione del progetto:\n",
    "\n",
    "  fisso 12 mesi  ->  Delta = {d12['delta_r2_eur']:+,.0f} EUR  ({d12['delta_r2_pct']:+.1f}%),\n",
    "                     cioè {d12['delta_r2_eur']/d12['anni']:+,.0f} EUR/anno\n",
    "  fisso 24 mesi  ->  Delta = {d24['delta_r2_eur']:+,.0f} EUR  ({d24['delta_r2_pct']:+.1f}%),\n",
    "                     cioè {d24['delta_r2_eur']/d24['anni']:+,.0f} EUR/anno\n",
    "\n",
    "La durata del blocco conta: {abs(d24['delta_r2_eur']-d12['delta_r2_eur']):,.0f} EUR di\n",
    "differenza fra 12 e 24 mesi, a parità di tutto il resto. Il motivo è nei rinnovi:\n",
    "chi rifirmava ogni 12 mesi è passato dal rinnovo del 2022, ai prezzi del 2022.\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8688355d",
   "metadata": {},
   "source": [
    "## 12. Dieci scenari di ingresso (elettricità)\n",
    "\n",
    "Le dieci date servono a raccontare. Il calcolo gira su tutti i mesi (sezione 14),\n",
    "e la colonna finale di questa tabella è **aritmetica sullo scenario storico\n",
    "simulato**: non è una raccomandazione commerciale e non va letta come tale."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c72ec438",
   "metadata": {},
   "outputs": [],
   "source": [
    "righe = []\n",
    "for mese_s, perche in C.SCENARI_RACCONTO:\n",
    "    t = pd.Period(mese_s, \"M\")\n",
    "    r = B.confronta(ctx, t, orizzonte=None)\n",
    "    if r is None:\n",
    "        continue\n",
    "    m12 = M.metriche_scenario(r, \"fisso_12M\")\n",
    "    m24 = M.metriche_scenario(r, \"fisso_24M\")\n",
    "    righe.append({\n",
    "        \"etichetta\": f\"{mese_s}  {perche[:46]}\",\n",
    "        \"data_scelta\": mese_s, \"perche\": perche, \"mesi\": r[\"mesi\"],\n",
    "        \"strategia_iniziale\": \"qualunque (la storia precedente si cancella)\",\n",
    "        \"costo_variabile_eur\": r[\"r2_variabile\"],\n",
    "        \"costo_fisso12_eur\": r[\"r2_fisso_12M\"],\n",
    "        \"costo_fisso24_eur\": r[\"r2_fisso_24M\"],\n",
    "        \"delta_r2_eur\": m12[\"delta_r2_eur\"],\n",
    "        \"delta_r2_pct\": m12[\"delta_r2_pct\"],\n",
    "        \"delta_r2_eur_anno\": m12[\"delta_r2_eur_anno\"],\n",
    "        \"delta24_r2_eur\": m24[\"delta_r2_eur\"],\n",
    "        \"meno_costosa_ex_post_12M\": m12[\"meno_costosa_ex_post\"],\n",
    "        \"meno_costosa_ex_post_24M\": m24[\"meno_costosa_ex_post\"],\n",
    "        \"riduzione_volatilita_pct\": m12[\"riduzione_volatilita_pct\"],\n",
    "        \"picco_mensile_evitato_eur\": m12[\"picco_mensile_evitato_eur\"],\n",
    "    })\n",
    "scenari = pd.DataFrame(righe)\n",
    "scenari.to_csv(C.OUTPUTS / \"scenari_dieci_date.csv\", index=False, encoding=\"utf-8\")\n",
    "print(scenari[[\"data_scelta\",\"mesi\",\"costo_variabile_eur\",\"costo_fisso12_eur\",\n",
    "               \"delta_r2_eur\",\"delta_r2_pct\",\"delta_r2_eur_anno\",\n",
    "               \"meno_costosa_ex_post_12M\",\"meno_costosa_ex_post_24M\"]]\n",
    "      .round(1).to_string(index=False))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d23d073c",
   "metadata": {},
   "outputs": [],
   "source": [
    "p11 = G.g11_scenari(scenari)\n",
    "log(\"salvato\", p11.relative_to(C.RADICE))\n",
    "from IPython.display import Image, display\n",
    "display(Image(filename=str(p11), width=950))\n",
    "\n",
    "n_fis = (scenari[\"meno_costosa_ex_post_12M\"] == \"fisso\").sum()\n",
    "print(f\"\"\"\n",
    "Su {len(scenari)} date di ingresso raccontate, il fisso 12 mesi è risultato meno\n",
    "costoso in {n_fis} casi e il variabile in {len(scenari)-n_fis}. Ma il numero che\n",
    "risponde alla domanda del brief è un altro: fra la data migliore e la peggiore ci\n",
    "sono {scenari['delta_r2_eur_anno'].max() - scenari['delta_r2_eur_anno'].min():,.0f}\n",
    "EUR/anno di differenza, con lo stesso cliente, lo stesso consumo, gli stessi\n",
    "parametri. Cambia solo il mese.\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c047b97a",
   "metadata": {},
   "source": [
    "## 13. Rolling analysis a 5 anni (elettricità)\n",
    "\n",
    "Qui va detta subito la cosa scomoda. Con 81 mesi di dati, le finestre di 60 mesi\n",
    "sono 22, e si sovrappongono quasi del tutto: **tutte** contengono la crisi del\n",
    "2022. Non sono 22 osservazioni indipendenti, sono un unico episodio storico\n",
    "guardato da 22 angoli. Qualunque percentuale di «vittorie» calcolata su di esse\n",
    "descrive quel tratto di storia, non una probabilità.\n",
    "\n",
    "Per questo la tabella riporta anche le finestre a 36 mesi (46 finestre, qualcuna\n",
    "delle quali *non* contiene il 2022) e, soprattutto, la **distribuzione** dei\n",
    "risultati, non solo il conteggio."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ca3c1031",
   "metadata": {},
   "outputs": [],
   "source": [
    "matrici = {n: B.matrice_ingressi(ctx, orizzonte=n) for n in (36, 60)}\n",
    "riepilogo_roll = {}\n",
    "for n, mat in matrici.items():\n",
    "    for strat in (\"fisso_12M\", \"fisso_24M\"):\n",
    "        d = M.distribuzione(mat, f\"delta_r2_{strat}\")\n",
    "        d.update({\"finestra_mesi\": n, \"strategia\": strat})\n",
    "        riepilogo_roll[(n, strat)] = d\n",
    "roll = pd.DataFrame(riepilogo_roll).T.reset_index(drop=True)\n",
    "colonne = [\"finestra_mesi\",\"strategia\",\"finestre\",\"variabile_meno_costoso_n\",\n",
    "           \"variabile_meno_costoso_pct\",\"fisso_meno_costoso_n\",\"fisso_meno_costoso_pct\",\n",
    "           \"delta_medio\",\"delta_mediano\",\"delta_sd\",\"delta_q25\",\"delta_q75\",\n",
    "           \"delta_min\",\"delta_max\",\"vantaggio_medio_quando_variabile_vince\",\n",
    "           \"perdita_media_quando_fisso_vince\",\"escursione_dovuta_al_timing\"]\n",
    "print(roll[colonne].to_string(index=False))\n",
    "roll[colonne].to_csv(C.OUTPUTS / \"rolling_windows.csv\", index=False, encoding=\"utf-8\")\n",
    "for n, mat in matrici.items():\n",
    "    mat.to_csv(C.OUTPUTS / f\"rolling_dettaglio_{n}m.csv\", encoding=\"utf-8\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c2e54ffc",
   "metadata": {},
   "outputs": [],
   "source": [
    "for n in (36, 60):\n",
    "    ind = M.finestre_indipendenti(matrici[n], n)\n",
    "    print(f\"finestre di {n} mesi -> {ind['nota']}\\n\")\n",
    "p8 = G.g08_distribuzione({n: m for n, m in matrici.items()}, \"delta_r2_fisso_12M\")\n",
    "log(\"salvato\", p8.relative_to(C.RADICE))\n",
    "from IPython.display import Image, display\n",
    "display(Image(filename=str(p8), width=1000))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bb16af9d",
   "metadata": {},
   "source": [
    "## 14. Heatmap del timing (elettricità), e quanto il timing conta davvero\n",
    "\n",
    "La heatmap mostra il risultato di ogni singolo mese di ingresso. Subito dopo c'è\n",
    "la domanda vera del brief: *la varianza del risultato è spiegata più dal tipo di\n",
    "contratto o dal momento in cui si entra?* La risposta è una scomposizione della\n",
    "varianza, non un'impressione."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3a78ce3c",
   "metadata": {},
   "outputs": [],
   "source": [
    "# solo ingressi con almeno 12 mesi davanti: sotto quella soglia il Delta annualizzato\n",
    "# è dominato dal rumore di pochi mesi\n",
    "h = matrice_fine[matrice_fine[\"mesi\"] >= 12]\n",
    "p7 = G.g07_heatmap(h, \"delta_r2_anno_fisso_12M\",\n",
    "                   \"Il mese in cui entri decide il risultato\",\n",
    "                   \"Delta annualizzato (EUR/anno), fisso 12 mesi contro variabile, \"\n",
    "                   \"dalla data di ingresso a settembre 2026\")\n",
    "log(\"salvato\", p7.relative_to(C.RADICE))\n",
    "from IPython.display import Image, display\n",
    "display(Image(filename=str(p7), width=1000))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "99774618",
   "metadata": {},
   "outputs": [],
   "source": [
    "lungo = M.tabella_lunga(matrici[36])\n",
    "anova36 = M.scomposizione_varianza(lungo)\n",
    "lungo60 = M.tabella_lunga(matrici[60])\n",
    "anova60 = M.scomposizione_varianza(lungo60)\n",
    "print(\"Scomposizione della varianza del costo mensile - finestre di 36 mesi\\n\")\n",
    "print(anova36.round(2).to_string(index=False))\n",
    "print(\"\\nFinestre di 60 mesi\\n\")\n",
    "print(anova60.round(2).to_string(index=False))\n",
    "anova36.assign(finestra=36).to_csv(C.OUTPUTS / \"varianza_timing_36m.csv\", index=False, encoding=\"utf-8\")\n",
    "anova60.assign(finestra=60).to_csv(C.OUTPUTS / \"varianza_timing_60m.csv\", index=False, encoding=\"utf-8\")\n",
    "\n",
    "a36 = anova36.set_index(\"fonte\")[\"eta_quadro_pct\"]\n",
    "print(f\"\"\"\n",
    "Lettura, per le finestre di 36 mesi. Il TIPO di contratto spiega il\n",
    "{a36.iloc[0]:.0f}% della varianza, il MOMENTO di ingresso il {a36.iloc[1]:.0f}%,\n",
    "e l'interazione fra i due il {a36.iloc[2]:.0f}%.\n",
    "\n",
    "L'interazione è la parte più interessante e la più facile da perdere: dice che\n",
    "non esiste una strategia migliore in assoluto, perché QUALE strategia convenga\n",
    "dipende da QUANDO si entra. Se esistesse una strategia universalmente migliore,\n",
    "l'interazione sarebbe piccola e il termine 'tipo di contratto' dominerebbe. Non\n",
    "è così.\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6a049090",
   "metadata": {},
   "source": [
    "## 15. Break-even del prezzo fisso (elettricità), ed ex ante contro ex post\n",
    "\n",
    "Per ogni mese di ingresso: qual è il prezzo fisso che, tenuto per tutta la\n",
    "finestra, avrebbe prodotto **esattamente** lo stesso costo del variabile? È un\n",
    "calcolo esatto, non cercato per tentativi, perché il costo della strategia fissa\n",
    "è lineare nel prezzo.\n",
    "\n",
    "Il grafico sovrappone due curve che vanno lette in modo diverso:\n",
    "\n",
    "- il **break-even** è *ex post*: lo conosciamo solo oggi;\n",
    "- il **prezzo disponibile alla firma** è *ex ante*: era sul tavolo quel mese.\n",
    "\n",
    "Quando la seconda curva sta sotto la prima, il fisso è risultato conveniente. Il\n",
    "grafico misura la convenienza storica. **Non** dice che fosse prevedibile."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8774a39f",
   "metadata": {},
   "outputs": [],
   "source": [
    "righe = []\n",
    "for t0, r0 in matrice_fine.iterrows():\n",
    "    r = B.confronta(ctx, t0, orizzonte=None, strategie=(\"variabile\", \"fisso_12M\"))\n",
    "    if r is None or r[\"mesi\"] < 12:\n",
    "        continue\n",
    "    be = M.break_even(r, \"fisso_12M\")\n",
    "    disp, liv = mod.prezzo_fisso_alla_firma(t0)\n",
    "    righe.append({\"mese\": t0, \"break_even\": be, \"disponibile\": disp,\n",
    "                  \"livello\": liv, \"mesi\": r[\"mesi\"],\n",
    "                  \"margine\": be - disp})\n",
    "be_tab = pd.DataFrame(righe).set_index(\"mese\")\n",
    "be_tab.to_csv(C.OUTPUTS / \"break_even_per_ingresso.csv\", encoding=\"utf-8\")\n",
    "p9 = G.g09_break_even(be_tab)\n",
    "log(\"salvato\", p9.relative_to(C.RADICE))\n",
    "from IPython.display import Image, display\n",
    "display(Image(filename=str(p9), width=950))\n",
    "\n",
    "conveniva = (be_tab[\"margine\"] > 0)\n",
    "print(f\"\"\"\n",
    "Su {len(be_tab)} mesi di ingresso con almeno 12 mesi davanti, il prezzo fisso\n",
    "firmabile stava sotto il break-even in {conveniva.sum()} casi ({conveniva.mean()*100:.0f}%).\n",
    "Margine mediano {be_tab['margine'].median():+.4f} EUR/kWh, da\n",
    "{be_tab['margine'].min():+.4f} a {be_tab['margine'].max():+.4f}.\n",
    "\n",
    "Avvertenza sul hindsight bias. Questa tabella dice che chi ha firmato fisso nel\n",
    "2020-2021 ha fatto bene. NON dice che fosse ovvio: nel 2020 il PUN era ai minimi\n",
    "storici da quindici anni, e la lettura naturale di un prezzo ai minimi è che\n",
    "bloccarlo sia inutile. Il margine positivo di quei mesi è il risultato di un\n",
    "evento - la crisi del 2022 - che nessuna informazione disponibile nel 2020\n",
    "annunciava.\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ac82244b",
   "metadata": {},
   "source": [
    "## 16. Rischio e volatilità (elettricità): quanto vale la prevedibilità\n",
    "\n",
    "Il costo totale è metà della risposta. L'altra metà è che le due strategie non\n",
    "vendono la stessa cosa: una vende un prezzo, l'altra vende anche una promessa di\n",
    "stabilità. Questa sezione misura la seconda.\n",
    "\n",
    "**FIC — Fixed-price Insurance Cost.** Il brief proponeva\n",
    "$FIC = \\Delta / \\text{riduzione di volatilità}$ e chiedeva di proporre una\n",
    "formulazione migliore se ce n'era una. Ce n'è:\n",
    "\n",
    "$$FIC = \\frac{\\Delta / \\text{mesi}}{\\sigma_{\\text{variabile}} - \\sigma_{\\text{fisso}}}$$\n",
    "\n",
    "Normalizzare il premio sul **tempo** prima di rapportarlo al rischio rende il\n",
    "numero indipendente dalla durata della finestra: la formula del brief crescerebbe\n",
    "allungando l'orizzonte pur descrivendo lo stesso contratto. Il rapporto risultante\n",
    "è adimensionale e si legge così: 0,30 significa «per ogni euro al mese di\n",
    "oscillazione evitata, il cliente ha pagato 30 centesimi al mese». Sotto zero la\n",
    "copertura non è costata niente."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b3436abc",
   "metadata": {},
   "outputs": [],
   "source": [
    "righe = []\n",
    "for t0 in matrici[36].index:\n",
    "    r = B.confronta(ctx, t0, orizzonte=36)\n",
    "    if r is None:\n",
    "        continue\n",
    "    for strat in (\"fisso_12M\", \"fisso_24M\"):\n",
    "        m = M.metriche_scenario(r, strat)\n",
    "        righe.append({k: m[k] for k in (\n",
    "            \"ingresso\",\"strategia_fissa\",\"delta_r2_eur\",\"delta_r2_eur_anno\",\n",
    "            \"sd_mensile_variabile\",\"sd_mensile_fisso\",\"riduzione_volatilita_eur_mese\",\n",
    "            \"riduzione_volatilita_pct\",\"riduzione_volatilita_prezzo_pct\",\n",
    "            \"sd_prezzo_variabile\",\"sd_prezzo_fisso\",\n",
    "            \"fic_eur_anno\",\"fic_per_unita_di_rischio\",\n",
    "            \"picco_mensile_evitato_eur\",\"shock_annuale_evitato_eur\",\n",
    "            \"mese_peggiore_variabile\",\"mese_peggiore_fisso\")})\n",
    "rischio = pd.DataFrame(righe)\n",
    "rischio.to_csv(C.OUTPUTS / \"rischio_e_copertura_36m.csv\", index=False, encoding=\"utf-8\")\n",
    "print(\"Finestre di 36 mesi, metriche di rischio (mediane)\\n\")\n",
    "print(rischio.groupby(\"strategia_fissa\")[\n",
    "    [\"delta_r2_eur_anno\",\"riduzione_volatilita_pct\",\"riduzione_volatilita_prezzo_pct\",\n",
    "     \"fic_eur_anno\",\"fic_per_unita_di_rischio\",\"picco_mensile_evitato_eur\",\n",
    "     \"shock_annuale_evitato_eur\"]\n",
    "].median().round(2).to_string())\n",
    "print(\"\"\"\n",
    "Le due colonne di 'riduzione_volatilità' vanno lette insieme, e la differenza fra\n",
    "loro è istruttiva. Quella sulla BOLLETTA è più bassa perché la deviazione standard\n",
    "della spesa mensile contiene anche la stagionalità dei consumi, che è identica\n",
    "nelle due strategie e che nessun contratto può togliere. Quella sul PREZZO misura\n",
    "la protezione vera, cioè l'unica parte su cui il contratto agisce. Chi cita solo\n",
    "la prima sottostima quello che il prezzo fisso fa davvero; chi cita solo la\n",
    "seconda promette al cliente una stabilità della bolletta che non avrà.\"\"\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "02949315",
   "metadata": {},
   "outputs": [],
   "source": [
    "punti = []\n",
    "for t0 in matrici[36].index:\n",
    "    r = B.confronta(ctx, t0, orizzonte=36)\n",
    "    if r is None:\n",
    "        continue\n",
    "    for s, df in r[\"_serie\"].items():\n",
    "        punti.append({\"strategia\": s, \"ingresso\": t0,\n",
    "                      \"costo_totale\": float(df[\"risultato2\"].sum()),\n",
    "                      \"sd_mensile\": float(df[\"risultato2\"].std(ddof=1))})\n",
    "punti = pd.DataFrame(punti)\n",
    "p10 = G.g10_costo_volatilita(punti)\n",
    "log(\"salvato\", p10.relative_to(C.RADICE))\n",
    "from IPython.display import Image, display\n",
    "display(Image(filename=str(p10), width=900))\n",
    "\n",
    "q = rischio[rischio[\"strategia_fissa\"] == \"fisso_12M\"]\n",
    "print(f\"\"\"\n",
    "Quanto è costato assicurarsi contro un 2022. Sulle finestre di 36 mesi il fisso\n",
    "12 mesi ha ridotto la deviazione standard della bolletta mensile del\n",
    "{q['riduzione_volatilita_pct'].median():.0f}% (mediana) e ha abbassato il mese\n",
    "peggiore di {q['picco_mensile_evitato_eur'].median():.0f} EUR. Il costo di questa\n",
    "copertura, in euro all'anno, ha mediana {q['fic_eur_anno'].median():+.0f} EUR/anno\n",
    "e va da {q['fic_eur_anno'].min():+.0f} a {q['fic_eur_anno'].max():+.0f}: in parte\n",
    "delle finestre la copertura è stata GRATUITA, cioè il fisso ha ridotto il rischio\n",
    "e anche il costo. È possibile perché il prezzo fisso non è un derivato equo: è\n",
    "un'offerta commerciale, e nel 2020-2021 era mediamente troppo bassa rispetto a\n",
    "quello che poi è accaduto.\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "27f7f133",
   "metadata": {},
   "source": [
    "## 17. Gas: le stesse domande, un risultato diverso\n",
    "\n",
    "Due avvertenze che cambiano come si legge questa sezione:\n",
    "\n",
    "1. **Tutto il gas è Livello 2.** L'Indicatore di Prezzo ARERA per struttura di\n",
    "   prezzo è pubblicato per l'elettrico, non per il gas. Non esiste quindi nessun\n",
    "   prezzo fisso reale con cui ancorare il modello, e il premio di copertura resta\n",
    "   un'assunzione dichiarata e non misurata.\n",
    "2. **La serie parte da gennaio 2021** e i prezzi fissi diventano calcolabili da\n",
    "   luglio 2021, perché il benchmark retrospettivo ha bisogno di mesi chiusi prima\n",
    "   della firma.\n",
    "\n",
    "Il consumo gas non è legato alla potenza elettrica di 3 kW: sono due forniture\n",
    "distinte e il collegamento sarebbe un errore."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "98b07863",
   "metadata": {},
   "outputs": [],
   "source": [
    "cl_gas = C.ClienteGas(consumo_annuo_smc=1_000.0)\n",
    "mod_g = O.modello_gas()\n",
    "ctx_g = B.ContestoGas.costruisci(mod_g, cl_gas)\n",
    "log(f\"markup variabile {mod_g.markup_variabile:.3f} | markup fisso {mod_g.markup_fisso:.3f} EUR/Smc\")\n",
    "\n",
    "mat_g_fine = B.matrice_ingressi(ctx_g, orizzonte=None, gas=True)\n",
    "mat_g_fine[\"delta_r2_anno_fisso_12M\"] = (mat_g_fine[\"delta_r2_fisso_12M\"]\n",
    "                                         / (mat_g_fine[\"mesi\"] / 12))\n",
    "log(f\"{len(mat_g_fine)} mesi di ingresso gas calcolabili, da {mat_g_fine.index.min()}\")\n",
    "print(mat_g_fine[[\"mesi\",\"r2_variabile\",\"r2_fisso_12M\",\"delta_r2_fisso_12M\",\n",
    "                  \"delta_pct_r2_fisso_12M\",\"vince_fisso_12M\"]].head(8).round(1).to_string())\n",
    "\n",
    "mat_g_36 = B.matrice_ingressi(ctx_g, orizzonte=36, gas=True)\n",
    "for strat in (\"fisso_12M\", \"fisso_24M\"):\n",
    "    d = M.distribuzione(mat_g_36, f\"delta_r2_{strat}\")\n",
    "    print(f\"\\ngas, finestre 36 mesi, {strat}: variabile meno costoso in \"\n",
    "          f\"{d['variabile_meno_costoso_n']}/{d['finestre']} \"\n",
    "          f\"({d['variabile_meno_costoso_pct']:.0f}%), Delta medio {d['delta_medio']:+.0f} EUR, \"\n",
    "          f\"escursione dovuta al timing {d['escursione_dovuta_al_timing']:,.0f} EUR\")\n",
    "mat_g_fine.to_csv(C.OUTPUTS / \"gas_matrice_ingressi.csv\", encoding=\"utf-8\")\n",
    "mat_g_36.to_csv(C.OUTPUTS / \"gas_rolling_36m.csv\", encoding=\"utf-8\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dc4d2fdf",
   "metadata": {},
   "outputs": [],
   "source": [
    "t0g = mat_g_fine.index.min()\n",
    "ris_g = B.confronta(ctx_g, t0g, orizzonte=None, gas=True)\n",
    "met_g = M.metriche_scenario(ris_g, \"fisso_12M\", unita=\"Smc\")\n",
    "print(f\"Gas, ingresso {t0g}, {met_g['mesi']} mesi, 1.000 Smc/anno\\n\")\n",
    "for k in (\"costo_variabile_r2\",\"costo_fisso_r2\",\"delta_r2_eur\",\"delta_r2_pct\",\n",
    "          \"delta_r2_eur_anno\",\"costo_medio_variabile_eur_Smc\",\"costo_medio_fisso_eur_Smc\",\n",
    "          \"sd_mensile_variabile\",\"sd_mensile_fisso\",\"riduzione_volatilita_pct\",\n",
    "          \"mese_peggiore_variabile\",\"mese_peggiore_fisso\",\"quando_peggiore_variabile\",\n",
    "          \"picco_mensile_evitato_eur\",\"break_even_fisso_eur_unita\",\n",
    "          \"fic_eur_anno\",\"meno_costosa_ex_post\"):\n",
    "    v = met_g[k]\n",
    "    print(f\"  {k:36} {v if not isinstance(v, float) else f'{v:,.4f}'}\")\n",
    "\n",
    "p4g = G.g04_cumulato(ris_g, f\"Gas, ingresso {t0g}: spesa cumulata\",\n",
    "                     nome=\"04b_gas_costo_cumulato\")\n",
    "p7g = G.g07_heatmap(mat_g_fine[mat_g_fine[\"mesi\"] >= 12], \"delta_r2_anno_fisso_12M\",\n",
    "                    \"Gas: anche qui il mese di ingresso decide\",\n",
    "                    \"Delta annualizzato (EUR/anno), fisso 12 mesi contro variabile - \"\n",
    "                    \"tutto Livello 2 (proxy)\", nome=\"07b_gas_heatmap_timing\")\n",
    "from IPython.display import Image, display\n",
    "for p in (p4g, p7g):\n",
    "    log(\"salvato\", p.relative_to(C.RADICE)); display(Image(filename=str(p), width=950))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aad87654",
   "metadata": {},
   "source": [
    "## 18. Dual fuel: elettricità e gas sommati\n",
    "\n",
    "Le due forniture sommate, sulle date in cui entrambe sono calcolabili. Non è la\n",
    "media dei due risultati: è la somma dei costi, che è la cosa che il cliente paga."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "47502063",
   "metadata": {},
   "outputs": [],
   "source": [
    "comuni = sorted(set(mat_g_fine.index) & set(matrice_fine.index))\n",
    "righe = []\n",
    "for t0 in comuni:\n",
    "    re_ = B.confronta(ctx, t0, orizzonte=None)\n",
    "    rg_ = B.confronta(ctx_g, t0, orizzonte=None, gas=True)\n",
    "    if re_ is None or rg_ is None or re_[\"mesi\"] < 12:\n",
    "        continue\n",
    "    for strat in (\"fisso_12M\", \"fisso_24M\"):\n",
    "        d_ee = re_[f\"r2_{strat}\"] - re_[\"r2_variabile\"]\n",
    "        d_gas = rg_[f\"r2_{strat}\"] - rg_[\"r2_variabile\"]\n",
    "        righe.append({\n",
    "            \"ingresso\": t0, \"strategia\": strat, \"mesi\": re_[\"mesi\"],\n",
    "            \"costo_var_totale\": re_[\"r2_variabile\"] + rg_[\"r2_variabile\"],\n",
    "            \"costo_fisso_totale\": re_[f\"r2_{strat}\"] + rg_[f\"r2_{strat}\"],\n",
    "            \"delta_elettrico\": d_ee, \"delta_gas\": d_gas,\n",
    "            \"delta_dual\": d_ee + d_gas,\n",
    "            \"delta_dual_anno\": (d_ee + d_gas) / (re_[\"mesi\"] / 12),\n",
    "            \"si_compensano\": \"sì\" if d_ee * d_gas < 0 else \"no\",\n",
    "        })\n",
    "dual = pd.DataFrame(righe)\n",
    "dual.to_csv(C.OUTPUTS / \"dual_fuel.csv\", index=False, encoding=\"utf-8\")\n",
    "d12 = dual[dual[\"strategia\"] == \"fisso_12M\"]\n",
    "print(d12[[\"ingresso\",\"mesi\",\"costo_var_totale\",\"costo_fisso_totale\",\n",
    "           \"delta_elettrico\",\"delta_gas\",\"delta_dual\",\"delta_dual_anno\",\n",
    "           \"si_compensano\"]].head(12).round(0).to_string(index=False))\n",
    "print(f\"\"\"\n",
    "Su {len(d12)} date di ingresso dual fuel:\n",
    "  elettrico e gas hanno dato segno OPPOSTO in {(d12['si_compensano']=='sì').sum()} casi\n",
    "  su {len(d12)}, cioè una delle due forniture ha compensato l'altra.\n",
    "  Delta dual mediano {d12['delta_dual_anno'].median():+,.0f} EUR/anno\n",
    "  (elettrico {d12['delta_elettrico'].median()/(d12['mesi'].median()/12):+,.0f},\n",
    "   gas {d12['delta_gas'].median()/(d12['mesi'].median()/12):+,.0f}).\n",
    "\n",
    "La compensazione non è casuale: PUN e PSV sono legati, perché in Italia buona\n",
    "parte dell'energia elettrica si produce con gas. Ma la bolletta gas è molto più\n",
    "esposta, perché il suo profilo di consumo concentra i volumi nei mesi invernali,\n",
    "che sono anche i più cari.\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1dc85dfa",
   "metadata": {},
   "source": [
    "## 19. Sensitivity analysis\n",
    "\n",
    "Cinque parametri mossi uno per volta, e poi i due che contano mossi insieme.\n",
    "La domanda non è «cambia il numero?» (cambia sempre), ma **cambia la risposta?**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4903ec83",
   "metadata": {},
   "outputs": [],
   "source": [
    "def delta_medio(consumo=2700.0, spread=None, premio=C.PREMIO_FISSO_BASE,\n",
    "                durata=12, piatto=False, quote_identiche=True,\n",
    "                livello_ip=\"dec10\", usa_l1=True, finestra=36):\n",
    "    \"\"\"Delta medio sulle finestre, per una combinazione di parametri.\"\"\"\n",
    "    m = O.modello_ee(spread_var=spread, premio=premio, livello_ip=livello_ip,\n",
    "                     usa_livello1=usa_l1, quote_identiche=quote_identiche)\n",
    "    cx = B.ContestoEE.costruisci(m, consumo, profilo_piatto=piatto)\n",
    "    mat = B.matrice_ingressi(cx, orizzonte=finestra,\n",
    "                             strategie=(\"variabile\", f\"fisso_{durata}M\"))\n",
    "    col = f\"delta_r2_fisso_{durata}M\"\n",
    "    return {\"delta_medio\": mat[col].mean(), \"delta_mediano\": mat[col].median(),\n",
    "            \"delta_min\": mat[col].min(), \"delta_max\": mat[col].max(),\n",
    "            \"finestre\": len(mat),\n",
    "            \"quota_variabile_vince_pct\": (mat[col] > 0).mean() * 100}\n",
    "\n",
    "base = delta_medio()\n",
    "log(\"caso base:\", {k: round(v, 1) for k, v in base.items()})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "791c07fa",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 19.1 consumo x spread (griglia del brief)\n",
    "righe = []\n",
    "for consumo in C.CONSUMI_EE_SENSITIVITY:\n",
    "    for sp in list(C.SPREAD_EE_EUR_KWH) + [None]:\n",
    "        r = delta_medio(consumo=consumo, spread=sp)\n",
    "        etichetta = mis.markup_variabile if sp is None else sp\n",
    "        righe.append({\"consumo\": consumo, \"spread\": round(etichetta, 4),\n",
    "                      \"spread_tipo\": \"misurato\" if sp is None else \"griglia brief\",\n",
    "                      **{k: round(v, 2) for k, v in r.items()}})\n",
    "sens_consumo = pd.DataFrame(righe)\n",
    "sens_consumo.to_csv(C.OUTPUTS / \"sensitivity_consumo_spread.csv\", index=False, encoding=\"utf-8\")\n",
    "print(sens_consumo.pivot_table(index=\"consumo\", columns=\"spread\",\n",
    "                               values=\"delta_medio\").round(0).to_string())\n",
    "print(\"\\n(valori in EUR, finestre di 36 mesi; < 0 il fisso costa meno)\")\n",
    "p12 = G.g12_sensitivity(sens_consumo.rename(columns={\"delta_medio\": \"delta_r2_eur\"}))\n",
    "log(\"salvato\", p12.relative_to(C.RADICE))\n",
    "from IPython.display import Image, display\n",
    "display(Image(filename=str(p12), width=900))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "19d9e192",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 19.2 gli altri parametri, uno per volta\n",
    "prove = [\n",
    "    (\"premio basso\",                 dict(premio=\"basso\")),\n",
    "    (\"premio medio (base)\",          dict(premio=\"medio\")),\n",
    "    (\"premio alto\",                  dict(premio=\"alto\")),\n",
    "    (\"fisso 12 mesi (base)\",         dict(durata=12)),\n",
    "    (\"fisso 24 mesi\",                dict(durata=24)),\n",
    "    (\"profilo consumo piatto\",       dict(piatto=True)),\n",
    "    (\"quote commerciali diverse\",    dict(quote_identiche=False)),\n",
    "    (\"IP = media offerte scelte\",    dict(livello_ip=\"media\")),\n",
    "    (\"tutto in proxy (senza Liv. 1)\", dict(usa_l1=False)),\n",
    "    (\"finestra 60 mesi\",             dict(finestra=60)),\n",
    "]\n",
    "righe = []\n",
    "for nome, kw in prove:\n",
    "    r = delta_medio(**kw)\n",
    "    righe.append({\"variante\": nome, **{k: round(v, 1) for k, v in r.items()},\n",
    "                  \"scarto_dal_base\": round(r[\"delta_medio\"] - base[\"delta_medio\"], 1)})\n",
    "sens = pd.DataFrame(righe)\n",
    "sens.to_csv(C.OUTPUTS / \"sensitivity_parametri.csv\", index=False, encoding=\"utf-8\")\n",
    "print(sens.to_string(index=False))\n",
    "print(f\"\"\"\n",
    "La riga da guardare è 'quota_variabile_vince_pct': è la RISPOSTA, non il numero.\n",
    "Se la risposta fosse fragile, questa colonna oscillerebbe fra 0 e 100 al variare\n",
    "dei parametri. Qui va da {sens['quota_variabile_vince_pct'].min():.0f}% a\n",
    "{sens['quota_variabile_vince_pct'].max():.0f}%.\"\"\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f88941ec",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 19.3 i due parametri non misurabili, mossi in modo INDIPENDENTE.\n",
    "# Qui il Livello 1 va spento di proposito: con i prezzi fissi reali dentro, 42\n",
    "# mesi su 81 non reagiscono ai parametri e la mappa misurerebbe due cose insieme.\n",
    "# Questa è la mappa del MODELLO PURO, e serve a vedere dove sta il confine.\n",
    "import itertools\n",
    "markup_var = [0.000, 0.020, 0.040, round(mis.markup_variabile, 4), 0.070]\n",
    "markup_fis = [0.000, round(mis.markup_fisso, 4), 0.050, 0.080, 0.110, 0.140]\n",
    "\n",
    "def delta_markup(mv, mf, finestra=36):\n",
    "    \"\"\"Delta medio con markup imposti su entrambi i lati, senza Livello 1.\"\"\"\n",
    "    m = O.ModelloEE(markup_variabile=mv, markup_fisso=mf, usa_livello1=False)\n",
    "    cx = B.ContestoEE.costruisci(m, 2700.0)\n",
    "    mat = B.matrice_ingressi(cx, orizzonte=finestra,\n",
    "                             strategie=(\"variabile\", \"fisso_12M\"))\n",
    "    return mat[\"delta_r2_fisso_12M\"].mean()\n",
    "\n",
    "griglia = pd.DataFrame(index=markup_fis, columns=markup_var, dtype=float)\n",
    "for mf, mv in itertools.product(markup_fis, markup_var):\n",
    "    griglia.loc[mf, mv] = delta_markup(mv, mf)\n",
    "print(\"Delta medio (EUR, finestre 36 mesi, modello puro senza Livello 1)\")\n",
    "print(\"righe: markup totale del FISSO sul benchmark | colonne: markup del VARIABILE sul PUN\\n\")\n",
    "print(griglia.round(0).to_string())\n",
    "griglia.to_csv(C.OUTPUTS / \"mappa_markup_fisso_variabile.csv\", encoding=\"utf-8\")\n",
    "p13 = G.g13_mappa_premio_spread(griglia)\n",
    "log(\"salvato\", p13.relative_to(C.RADICE))\n",
    "from IPython.display import Image, display\n",
    "display(Image(filename=str(p13), width=900))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "689e5925",
   "metadata": {},
   "outputs": [],
   "source": [
    "# la proprietà che rende il risultato robusto: il Delta dipende dalla DIFFERENZA\n",
    "lungo_g = griglia.stack().rename(\"delta\").reset_index()\n",
    "lungo_g.columns = [\"markup_fisso\", \"markup_variabile\", \"delta\"]\n",
    "lungo_g[\"differenza\"] = lungo_g[\"markup_fisso\"] - lungo_g[\"markup_variabile\"]\n",
    "corr = lungo_g[[\"differenza\", \"delta\"]].corr().iloc[0, 1]\n",
    "r2 = corr ** 2\n",
    "\n",
    "# quanto vale un euro di differenza, in euro di Delta: la pendenza della retta\n",
    "pend = np.polyfit(lungo_g[\"differenza\"], lungo_g[\"delta\"], 1)[0]\n",
    "pareggio = -np.polyfit(lungo_g[\"differenza\"], lungo_g[\"delta\"], 1)[1] / pend\n",
    "lungo_g.to_csv(C.OUTPUTS / \"mappa_markup_lungo.csv\", index=False, encoding=\"utf-8\")\n",
    "\n",
    "print(f\"\"\"Il Delta contro la DIFFERENZA fra i due markup:\n",
    "  correlazione      {corr:+.4f}   (R² = {r2:.4f})\n",
    "  pendenza          {pend:,.0f} EUR per ogni EUR/kWh di differenza\n",
    "  pareggio quando   markup_fisso - markup_variabile = {pareggio:+.4f} EUR/kWh\n",
    "\n",
    "Il Delta non dipende dai due parametri separatamente ma quasi solo dalla loro\n",
    "differenza: R² = {r2:.3f} su una griglia di {len(lungo_g)} combinazioni. È il\n",
    "risultato più robusto del lavoro, perché non serve indovinare lo spread giusto\n",
    "E il premio giusto: basta sapere di quanto il secondo supera il primo.\n",
    "\n",
    "La soglia {pareggio:+.4f} EUR/kWh si legge così: finché il fisso costava, sopra\n",
    "il suo benchmark, meno di {pareggio*100:.1f} centesimi in più di quanto il\n",
    "variabile costava sopra il PUN, il fisso restava la scelta meno costosa in media\n",
    "su questo periodo. Il markup fisso misurato sui prezzi reali 2023-2026 è\n",
    "{mis.markup_fisso:.4f} contro {mis.markup_variabile:.4f} del variabile, cioè una\n",
    "differenza di {mis.markup_fisso - mis.markup_variabile:+.4f}: dentro la soglia,\n",
    "e di parecchio.\"\"\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7d0d1bba",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 19.4 gas: consumi e premio\n",
    "righe = []\n",
    "for consumo in C.CONSUMI_GAS_SENSITIVITY:\n",
    "    for premio in (\"basso\", \"medio\", \"alto\"):\n",
    "        mg = O.modello_gas(premio=premio)\n",
    "        cg = B.ContestoGas.costruisci(mg, C.ClienteGas(consumo_annuo_smc=consumo))\n",
    "        mat = B.matrice_ingressi(cg, orizzonte=36, gas=True,\n",
    "                                 strategie=(\"variabile\", \"fisso_12M\"))\n",
    "        col = \"delta_r2_fisso_12M\"\n",
    "        righe.append({\"consumo_smc\": consumo, \"premio\": premio,\n",
    "                      \"delta_medio\": round(mat[col].mean(), 1),\n",
    "                      \"delta_min\": round(mat[col].min(), 1),\n",
    "                      \"delta_max\": round(mat[col].max(), 1),\n",
    "                      \"finestre\": len(mat),\n",
    "                      \"quota_variabile_vince_pct\": round((mat[col] > 0).mean()*100, 1)})\n",
    "sens_gas = pd.DataFrame(righe)\n",
    "sens_gas.to_csv(C.OUTPUTS / \"sensitivity_gas.csv\", index=False, encoding=\"utf-8\")\n",
    "print(sens_gas.to_string(index=False))\n",
    "\n",
    "mg_p = O.modello_gas()\n",
    "cg_p = B.ContestoGas.costruisci(mg_p, C.ClienteGas(consumo_annuo_smc=1000.0), profilo_piatto=True)\n",
    "mat_p = B.matrice_ingressi(cg_p, orizzonte=36, gas=True, strategie=(\"variabile\",\"fisso_12M\"))\n",
    "base_g = mat_g_36[\"delta_r2_fisso_12M\"].mean()\n",
    "print(f\"\"\"\n",
    "Peso della stagionalità sul gas: con il profilo a gradi giorno il Delta medio è\n",
    "{base_g:+.0f} EUR, con profilo piatto {mat_p['delta_r2_fisso_12M'].mean():+.0f} EUR.\n",
    "Differenza {abs(base_g - mat_p['delta_r2_fisso_12M'].mean()):.0f} EUR: sul gas la\n",
    "forma del consumo NON è un dettaglio, perché concentra i volumi nei mesi più cari.\n",
    "Sull'elettrico lo stesso confronto (sezione 19.2, riga 'profilo consumo piatto')\n",
    "sposta molto meno.\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "74b98d2b",
   "metadata": {},
   "source": [
    "## 20. Conclusioni\n",
    "\n",
    "### 20.1 La risposta breve, quella che serve davanti a un cliente\n",
    "\n",
    "Prima delle dodici domande del brief, la domanda vera del mestiere: **meglio il\n",
    "fisso o il variabile?**\n",
    "\n",
    "La risposta è **dipende**, e qui «dipende» non è una scappatoia: è un risultato\n",
    "misurato. Dipende dal **momento** in cui si firma molto più che dal tipo di\n",
    "contratto, e il momento giusto nessuno lo conosce in anticipo, perché a muovere\n",
    "i prezzi sono eventi che al momento della firma non sono sul tavolo (sezione 6).\n",
    "\n",
    "La cella seguente mette in fila i tre numeri che reggono questa risposta, e i\n",
    "due casi che la rendono evidente: lo stesso contratto, la stessa durata, due\n",
    "mesi di ingresso diversi, risultati opposti."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "45c0ad17",
   "metadata": {},
   "outputs": [],
   "source": [
    "d60 = M.distribuzione(matrici[60], \"delta_r2_fisso_12M\")\n",
    "d36 = M.distribuzione(matrici[36], \"delta_r2_fisso_12M\")\n",
    "i60 = M.finestre_indipendenti(matrici[60], 60)\n",
    "i36 = M.finestre_indipendenti(matrici[36], 36)\n",
    "a36 = anova36.set_index(\"fonte\")[\"eta_quadro_pct\"]\n",
    "q12 = rischio[rischio[\"strategia_fissa\"] == \"fisso_12M\"]\n",
    "sc = scenari\n",
    "\n",
    "# i due estremi: stessa finestra di 36 mesi, stesso contratto, mesi diversi\n",
    "col = \"delta_r2_fisso_12M\"\n",
    "m36 = matrici[36]\n",
    "giu_fisso, giu_var = m36[col].idxmin(), m36[col].idxmax()\n",
    "def _caso(idx):\n",
    "    \"\"\"Lo scenario completo di un mese di ingresso: i due totali e lo scarto.\"\"\"\n",
    "    r = m36.loc[idx]\n",
    "    return {\"mese\": str(idx), \"fine\": str(r[\"fine\"]),\n",
    "            \"delta\": float(r[col]), \"delta_pct\": float(r[\"delta_pct_r2_fisso_12M\"]),\n",
    "            \"totale_fisso\": float(r[\"r2_fisso_12M\"]),\n",
    "            \"totale_variabile\": float(r[\"r2_variabile\"])}\n",
    "\n",
    "estremi = {\"fisso\": _caso(giu_fisso), \"variabile\": _caso(giu_var),\n",
    "           \"mesi_finestra\": 36,\n",
    "           \"distanza_mesi\": int((pd.Period(str(giu_var), \"M\")\n",
    "                                 - pd.Period(str(giu_fisso), \"M\")).n)}\n",
    "\n",
    "# Lo stesso mese visto dai due contratti: serve a mostrare cosa compra chi\n",
    "# blocca, senza confrontare mesi diversi fra loro.\n",
    "_rf, _rv = ris_feb[\"_serie\"][\"fisso_12M\"], ris_feb[\"_serie\"][\"variabile\"]\n",
    "_m = _rv[\"risultato2\"].idxmax()            # il mese peggiore dell'indicizzato\n",
    "confronto_mese_peggiore = {\n",
    "    \"mese\": str(_m),\n",
    "    \"variabile\": float(_rv.loc[_m, \"risultato2\"]),\n",
    "    \"fisso\": float(_rf.loc[_m, \"risultato2\"]),\n",
    "    \"differenza\": float(_rv.loc[_m, \"risultato2\"] - _rf.loc[_m, \"risultato2\"]),\n",
    "    \"media_variabile\": float(_rv[\"risultato2\"].mean()),\n",
    "    \"media_fisso\": float(_rf[\"risultato2\"].mean()),\n",
    "}\n",
    "quante_volte = a36.iloc[1] / a36.iloc[0]\n",
    "\n",
    "def eur(x, dec=0):\n",
    "    \"\"\"Numero all'italiana: punto per le migliaia, virgola per i decimali.\"\"\"\n",
    "    return f\"{x:,.{dec}f}\".translate(str.maketrans({\",\": \".\", \".\": \",\"}))\n",
    "\n",
    "\n",
    "print(f\"\"\"MEGLIO IL FISSO O IL VARIABILE? Dipende, e dipende da questo.\n",
    "\n",
    "1. Dipende dal MOMENTO più che dal contratto.\n",
    "   Il mese di ingresso spiega il {a36.iloc[1]:.0f}% della varianza del risultato,\n",
    "   il tipo di contratto il {a36.iloc[0]:.0f}%: {quante_volte:.0f} volte tanto.\n",
    "   E l'interazione fra i due, il {a36.iloc[2]:.0f}%, dice che QUALE strategia\n",
    "   convenga dipende da QUANDO si entra.\n",
    "\n",
    "2. Quanto pesa il momento, in euro.\n",
    "   Fra il mese di ingresso migliore e il peggiore ci sono {eur(d60['escursione_dovuta_al_timing'])} EUR su cinque anni e {eur(d36['escursione_dovuta_al_timing'])} EUR su tre, a parità di tutto il resto.\n",
    "\n",
    "3. Due casi reali, stessa durata, risposta rovesciata.\n",
    "   Chi ha firmato nel {estremi['fisso']['mese']} ha speso {eur(abs(estremi['fisso']['delta']))} EUR IN MENO bloccando il prezzo.\n",
    "   Chi ha firmato nel {estremi['variabile']['mese']} ha speso {eur(abs(estremi['variabile']['delta']))} EUR IN PIÙ bloccandolo.\n",
    "   Fra le due firme passano {(pd.Period(estremi['variabile']['mese'], 'M')\n",
    "                              - pd.Period(estremi['fisso']['mese'], 'M')).n} mesi.\n",
    "\n",
    "4. Cosa si compra davvero.\n",
    "   Il prezzo bloccato non compra un prezzo più basso: compra una bolletta più\n",
    "   prevedibile. Sul cliente entrato a febbraio 2020, nel mese peggiore\n",
    "   dell'indicizzato ({confronto_mese_peggiore['mese']}) la bolletta e' stata di {confronto_mese_peggiore['variabile']:.0f} EUR con il prezzo indicizzato e di {confronto_mese_peggiore['fisso']:.0f} EUR con il prezzo bloccato: {confronto_mese_peggiore['differenza']:.0f} EUR di differenza nello stesso mese, a\n",
    "   parita' di consumi. Sul totale la differenza e' molto minore, ed e' il punto:\n",
    "   il prezzo bloccato non sposta tanto la spesa quanto la sua prevedibilita'.\n",
    "\n",
    "Quindi la domanda da girare al cliente non è «quale conviene», che richiede di\n",
    "sapere il futuro, ma «quanto ti pesa una bolletta che non sai prevedere», che\n",
    "richiede solo di conoscere lui.\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2c5e7db9",
   "metadata": {},
   "source": [
    "### 20.2 Le dodici domande del brief\n",
    "\n",
    "Le risposte sono prese dal calcolo. Tutti i numeri di questa sezione sono\n",
    "generati: nessuno è scritto a mano."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a58d016d",
   "metadata": {},
   "outputs": [],
   "source": [
    "def eur(x, dec=0):\n",
    "    \"\"\"Numero all'italiana: punto per le migliaia, virgola per i decimali.\"\"\"\n",
    "    return f\"{x:,.{dec}f}\".translate(str.maketrans({\",\": \".\", \".\": \",\"}))\n",
    "\n",
    "risposte = []\n",
    "def R(n, domanda, risposta):\n",
    "    risposte.append({\"n\": n, \"domanda\": domanda, \"risposta\": risposta})\n",
    "    print(f\"\\n{n}. {domanda}\\n   {risposta}\")\n",
    "\n",
    "R(1, \"Su 5 anni, quante volte il variabile è risultato meno costoso?\",\n",
    "  f\"{d60['variabile_meno_costoso_n']} su {d60['finestre']} \"\n",
    "  f\"({d60['variabile_meno_costoso_pct']:.0f}%). Su 36 mesi: \"\n",
    "  f\"{d36['variabile_meno_costoso_n']}/{d36['finestre']} ({d36['variabile_meno_costoso_pct']:.0f}%).\")\n",
    "R(2, \"Quante volte il fisso?\",\n",
    "  f\"{d60['fisso_meno_costoso_n']}/{d60['finestre']} ({d60['fisso_meno_costoso_pct']:.0f}%) \"\n",
    "  f\"a 5 anni; {d36['fisso_meno_costoso_n']}/{d36['finestre']} \"\n",
    "  f\"({d36['fisso_meno_costoso_pct']:.0f}%) a 3 anni. ATTENZIONE: le {d60['finestre']} \"\n",
    "  f\"finestre di 60 mesi coprono {i60['mesi_coperti']} mesi di storia e valgono circa \"\n",
    "  f\"{i60['indipendenti']} periodo indipendente: non sono {d60['finestre']} prove distinte.\")\n",
    "R(3, \"Qual è stato il vantaggio medio in euro?\",\n",
    "  f\"Quando il variabile è risultato meno costoso ({d60['variabile_meno_costoso_n']} \"\n",
    "  f\"su {d60['finestre']}): {eur(d60['vantaggio_medio_quando_variabile_vince'])} EUR \"\n",
    "  f\"su 5 anni, cioè {eur(d60['vantaggio_medio_quando_variabile_vince']/5)} EUR/anno. \"\n",
    "  f\"Con così pochi casi è una descrizione, non una media affidabile.\")\n",
    "R(4, \"Qual è stata la perdita media nei casi sfavorevoli?\",\n",
    "  f\"Quando il fisso è risultato meno costoso ({d60['fisso_meno_costoso_n']} finestre \"\n",
    "  f\"su {d60['finestre']}), il variabile è costato in media \"\n",
    "  f\"{eur(abs(d60['perdita_media_quando_fisso_vince']))} EUR in più su 5 anni, cioè \"\n",
    "  f\"{eur(abs(d60['perdita_media_quando_fisso_vince'])/5)} EUR/anno.\")\n",
    "R(5, \"Quanto conta il momento di sottoscrizione?\",\n",
    "  f\"Fra la finestra migliore e la peggiore ci sono \"\n",
    "  f\"{eur(d60['escursione_dovuta_al_timing'])} EUR di differenza a 5 anni e \"\n",
    "  f\"{eur(d36['escursione_dovuta_al_timing'])} EUR a 3 anni, con lo stesso cliente e \"\n",
    "  f\"gli stessi parametri. Nella scomposizione della varianza il momento di ingresso \"\n",
    "  f\"spiega il {a36.iloc[1]:.0f}% contro il {a36.iloc[0]:.0f}% del tipo di contratto.\")\n",
    "_c = sens_consumo[sens_consumo.spread == round(mis.markup_variabile, 4)] \\\n",
    "        .set_index(\"consumo\")[\"delta_medio\"]\n",
    "_per_kwh = (_c / _c.index)\n",
    "R(6, \"Quanto conta il consumo?\",\n",
    "  \"Decide l'entità, non la risposta. Delta medio a 36 mesi: \"\n",
    "  + \"; \".join(f\"{int(k)} kWh -> {eur(v)} EUR\" for k, v in _c.items())\n",
    "  + f\". Rapportato al consumo resta praticamente costante \"\n",
    "    f\"({eur(_per_kwh.min()*1000, 1)} / {eur(_per_kwh.max()*1000, 1)} EUR ogni \"\n",
    "    f\"1.000 kWh/anno): il segno non cambia mai, cambia solo la scala.\")\n",
    "R(7, \"Quanto conta lo spread?\",\n",
    "  f\"Molto, ma non da solo. Il Delta dipende quasi soltanto dalla DIFFERENZA fra \"\n",
    "  f\"il markup del fisso e quello del variabile: R² = {eur(r2, 3)} su \"\n",
    "  f\"{len(lungo_g)} combinazioni, con {eur(pend/100)} EUR di Delta per ogni \"\n",
    "  f\"centesimo di EUR/kWh di differenza. Il pareggio cade a una differenza di \"\n",
    "  f\"{eur(pareggio*100, 2)} centesimi; sui prezzi reali 2023-2026 la differenza \"\n",
    "  f\"misurata è {eur((mis.markup_fisso - mis.markup_variabile)*100, 2)} centesimi, \"\n",
    "  f\"cioè ampiamente dalla parte del fisso. Sulla mappa del modello puro il Delta \"\n",
    "  f\"medio va da {eur(griglia.min().min())} a {eur(griglia.max().max())} EUR.\")\n",
    "R(8, \"Quanto conta la durata 12/24 mesi?\",\n",
    "  f\"Delta medio {eur(sens[sens.variante=='fisso 12 mesi (base)']['delta_medio'].iloc[0])} EUR \"\n",
    "  f\"a 12 mesi contro {eur(sens[sens.variante=='fisso 24 mesi']['delta_medio'].iloc[0])} EUR \"\n",
    "  f\"a 24 mesi, su finestre di 36 mesi. Il blocco più lungo ha protetto meglio, e il \"\n",
    "  f\"motivo sta nei rinnovi: chi rifirmava ogni 12 mesi è passato dal rinnovo del \"\n",
    "  f\"2022, cioè ha ribloccato il prezzo ai valori di picco.\")\n",
    "R(9, \"Il vantaggio medio del variabile compensa la sua maggiore volatilità?\",\n",
    "  f\"Nel periodo analizzato la domanda non si pone nella forma attesa, perché il \"\n",
    "  f\"variabile NON ha avuto un vantaggio medio da compensare. Il fisso ha ridotto la \"\n",
    "  f\"volatilità della bolletta del {q12['riduzione_volatilita_pct'].median():.0f}% e \"\n",
    "  f\"quella del solo prezzo del {q12['riduzione_volatilita_prezzo_pct'].median():.0f}% \"\n",
    "  f\"(mediane) ED è costato meno nella maggior parte delle finestre: FIC mediano \"\n",
    "  f\"{eur(q12['fic_eur_anno'].median())} EUR/anno, cioè negativo, cioè copertura \"\n",
    "  f\"gratuita. È possibile perché un prezzo fisso retail non è un derivato equo: è \"\n",
    "  f\"un'offerta commerciale, e nel 2020-2021 era mediamente troppo bassa rispetto a \"\n",
    "  f\"quello che poi è successo.\")\n",
    "R(10, \"Quanto avrebbe pagato un cliente per assicurarsi contro un 2022?\",\n",
    "  f\"Misurato sul mese peggiore: il fisso ha abbassato il picco mensile di \"\n",
    "  f\"{eur(q12['picco_mensile_evitato_eur'].median())} EUR (mediana) e lo shock annuale \"\n",
    "  f\"di {eur(q12['shock_annuale_evitato_eur'].median())} EUR, al prezzo di \"\n",
    "  f\"{eur(q12['fic_eur_anno'].median())} EUR/anno. Ma la mediana nasconde la coda: \"\n",
    "  f\"nelle finestre peggiori per il fisso la stessa copertura è costata fino a \"\n",
    "  f\"{eur(q12['fic_eur_anno'].max())} EUR/anno, e in quelle migliori ha reso fino a \"\n",
    "  f\"{eur(q12['fic_eur_anno'].min())} EUR/anno.\")\n",
    "R(11, \"Esiste una strategia storicamente meno costosa nella maggior parte delle finestre?\",\n",
    "  f\"Sì, in QUESTO tratto di storia: il prezzo fisso, nel \"\n",
    "  f\"{d60['fisso_meno_costoso_pct']:.0f}% delle finestre a 5 anni e nel \"\n",
    "  f\"{d36['fisso_meno_costoso_pct']:.0f}% di quelle a 3 anni. Ma vedi la risposta 2: \"\n",
    "  f\"il risultato poggia su un singolo episodio, la crisi del 2022, presente in quasi \"\n",
    "  f\"tutte le finestre. Non è una regolarità, è una cronaca.\")\n",
    "R(12, \"Oppure il timing domina la scelta fisso/variabile?\",\n",
    "  f\"Il timing pesa molto di più del tipo di contratto, e l'interazione fra i due pesa \"\n",
    "  f\"più di entrambi: tipo di contratto {a36.iloc[0]:.0f}%, momento di ingresso \"\n",
    "  f\"{a36.iloc[1]:.0f}%, interazione {a36.iloc[2]:.0f}% della varianza (finestre di 36 \"\n",
    "  f\"mesi; a 60 mesi: {anova60.set_index('fonte')['eta_quadro_pct'].iloc[0]:.0f}% / \"\n",
    "  f\"{anova60.set_index('fonte')['eta_quadro_pct'].iloc[1]:.0f}% / \"\n",
    "  f\"{anova60.set_index('fonte')['eta_quadro_pct'].iloc[2]:.0f}%). Che l'interazione \"\n",
    "  f\"sia il termine più grande significa che QUALE strategia convenga dipende da \"\n",
    "  f\"QUANDO si entra: la domanda 'meglio il fisso o il variabile' non ha una risposta \"\n",
    "  f\"valida indipendentemente dalla data. È la conclusione più solida del lavoro, \"\n",
    "  f\"perché a differenza delle frequenze di vittoria non dipende da quante finestre \"\n",
    "  f\"contengono il 2022: dipende dal fatto che il risultato cambia segno fra l'una e \"\n",
    "  f\"l'altra.\")\n",
    "\n",
    "risposte_df = pd.DataFrame(risposte)\n",
    "risposte_df.to_csv(C.OUTPUTS / \"risposte_dodici_domande.csv\", index=False, encoding=\"utf-8\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "74a3115d",
   "metadata": {},
   "outputs": [],
   "source": [
    "NOTEBOOK_NOME = \"01_backtest_fisso_vs_variabile_2020_2026.ipynb\"\n",
    "# Executive summary generata dai numeri, scritta su file\n",
    "riga_exec = M.frase_executive(d60, i60)\n",
    "\n",
    "# la frase di sintesi si compone a parte, cosi' resta una riga sola nel file\n",
    "# invece di spezzarsi a meta' frase come fa un f-string multilinea\n",
    "_pct = abs(d60[\"delta_medio\"] / matrici[60][\"r2_variabile\"].mean() * 100)\n",
    "_chi = \"fisso\" if d60[\"delta_medio\"] < 0 else \"variabile\"\n",
    "_pct_it = f\"{_pct:.1f}\".replace(\".\", \",\")      # virgola decimale, si legge in italiano\n",
    "RISPOSTA_SINTETICA = (\n",
    "    f\"Su finestre di cinque anni la differenza media di costo fra le due strategie \"\n",
    "    f\"è stata dell'{_pct_it}% a favore del {_chi}, ma il mese di ingresso ha \"\n",
    "    f\"generato oscillazioni del risultato comprese fra {d60['delta_min']:+,.0f} e \"\n",
    "    f\"{d60['delta_max']:+,.0f} EUR. Il prezzo fisso ha ridotto del \"\n",
    "    f\"{q12['riduzione_volatilita_pct'].median():.0f}% la volatilità della spesa \"\n",
    "    f\"mensile e del {q12['riduzione_volatilita_prezzo_pct'].median():.0f}% quella \"\n",
    "    f\"del solo prezzo, con un costo implicito della copertura pari a \"\n",
    "    f\"{q12['fic_eur_anno'].median():+,.0f} EUR/anno: negativo, cioè in questo \"\n",
    "    f\"periodo la copertura non è costata, ha reso.\"\n",
    ")\n",
    "testo = f\"\"\"# Riepilogo dei risultati\n",
    "\n",
    "*Generato da `{NOTEBOOK_NOME}` il {pd.Timestamp.today().date()}. Nessun numero\n",
    "di questo file è stato scritto a mano.*\n",
    "\n",
    "## In una riga\n",
    "\n",
    "{riga_exec}\n",
    "\n",
    "## I numeri che contano\n",
    "\n",
    "| grandezza | valore |\n",
    "|---|---|\n",
    "| orizzonte elettrico | {C.EE_PRIMO_MESE} / {C.EE_ULTIMO_MESE} ({len(pun)} mesi) |\n",
    "| orizzonte gas | {C.GAS_PRIMO_MESE} / {C.GAS_ULTIMO_MESE} ({len(psv)} mesi) |\n",
    "| prezzi fissi reali (Livello 1) | {(fisso_firmabile['livello']==O.LIVELLO_REALE).sum()} mesi su {len(fisso_firmabile)} |\n",
    "| finestre di 60 mesi | {d60['finestre']} (circa {i60['indipendenti']} periodo indipendente) |\n",
    "| fisso meno costoso, finestre 60 mesi | {d60['fisso_meno_costoso_n']}/{d60['finestre']} ({d60['fisso_meno_costoso_pct']:.0f}%) |\n",
    "| fisso meno costoso, finestre 36 mesi | {d36['fisso_meno_costoso_n']}/{d36['finestre']} ({d36['fisso_meno_costoso_pct']:.0f}%) |\n",
    "| Delta medio, 60 mesi | {d60['delta_medio']:+,.0f} EUR ({d60['delta_medio']/5:+,.0f} EUR/anno) |\n",
    "| escursione dovuta al solo timing, 60 mesi | {d60['escursione_dovuta_al_timing']:,.0f} EUR |\n",
    "| riduzione di volatilità della bolletta (mediana) | {q12['riduzione_volatilita_pct'].median():.0f}% |\n",
    "| riduzione di volatilità del prezzo (mediana) | {q12['riduzione_volatilita_prezzo_pct'].median():.0f}% |\n",
    "| picco mensile evitato (mediana) | {q12['picco_mensile_evitato_eur'].median():.0f} EUR |\n",
    "| FIC, costo della copertura (mediana) | {q12['fic_eur_anno'].median():+,.0f} EUR/anno |\n",
    "| varianza spiegata: contratto / timing / interazione | {a36.iloc[0]:.0f}% / {a36.iloc[1]:.0f}% / {a36.iloc[2]:.0f}% |\n",
    "\n",
    "## La risposta, nella forma che il brief chiedeva\n",
    "\n",
    "{RISPOSTA_SINTETICA}\n",
    "\n",
    "## Avvertenza che accompagna ogni numero qui sopra\n",
    "\n",
    "Questi risultati descrivono il periodo 2020-2026, che contiene la crisi energetica\n",
    "del 2022. Non sono probabilità e non si estrapolano. Il dettaglio dei limiti è\n",
    "nella sezione 21 del notebook e in `methodology.md`.\n",
    "\"\"\"\n",
    "(C.OUTPUTS / \"RIEPILOGO.md\").write_text(testo, encoding=\"utf-8\")\n",
    "print(testo)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "19305e02",
   "metadata": {},
   "source": [
    "## 21. Limiti dell'analisi\n",
    "\n",
    "In ordine di gravità. I primi tre possono cambiare la risposta, non solo i numeri.\n",
    "\n",
    "**1. La proxy del prezzo fisso guarda lo spot passato, non la curva forward.**\n",
    "Fuori dai 42 mesi del Livello 1, il prezzo fisso è modellato sulla media del PUN\n",
    "degli ultimi 12 mesi chiusi. Un venditore vero prezza sul forward, e aggiorna le\n",
    "condizioni molto più spesso di una volta al mese. Sui 42 mesi in\n",
    "cui possiamo confrontare, le offerte fisse reali hanno oscillato da 8,2 centesimi\n",
    "sotto a 8,6 centesimi sopra quel benchmark. Siccome il 2021 era in salita, la\n",
    "proxy di quel periodo è probabilmente **troppo generosa col fisso**, e quindi il\n",
    "vantaggio del fisso per gli ingressi 2020-2021 è una stima per difetto. Per\n",
    "trasformare il lavoro in un backtest di offerte realmente sottoscrivibili servirebbe\n",
    "**un archivio storico delle offerte del Portale Offerte ARERA con data di\n",
    "pubblicazione e prezzo bloccato, mese per mese, dal 2020**: ARERA pubblica\n",
    "l'Indicatore di Prezzo solo da gennaio 2023, e il Portale non espone lo storico.\n",
    "È l'unico dataset mancante che separa questo modello da un backtest completo.\n",
    "\n",
    "**2. Un solo episodio storico.** Il 2022 è dentro tutte le finestre di 5 anni e in\n",
    "quasi tutte quelle di 3. Le percentuali di «vittoria» descrivono quel tratto di\n",
    "storia. Chiamarle probabilità sarebbe un errore, e con 81 mesi di dati non c'è modo\n",
    "di evitarlo: servono più anni, non più finestre.\n",
    "\n",
    "**3. Spread e premio non sono separabili dai dati.** Il Delta dipende quasi solo\n",
    "dalla loro differenza (sezione 19.3), e nessuna delle due grandezze si misura\n",
    "isolatamente dalle fonti pubbliche. La sensitivity copre l'intervallo plausibile,\n",
    "ma la scelta del punto centrale resta una scelta.\n",
    "\n",
    "**4. Il gas è interamente Livello 2** e parte da gennaio 2021: nessun prezzo fisso\n",
    "reale lo ancora, e il 2020 non c'è. Il PSV mensile 2020 non è pubblicato da nessuna\n",
    "fonte gratuita verificabile, e non è stato inventato.\n",
    "\n",
    "**5. Profili di consumo parametrici.** Dichiarati, non misurati. L'effetto è\n",
    "piccolo sull'elettrico e grande sul gas (sezione 19.4).\n",
    "\n",
    "**6. Componenti regolate del gas ferme a dicembre 2023.** La tutela gas è finita:\n",
    "dal 2024 le componenti comuni sono tenute all'ultimo valore ARERA pubblicato. Non\n",
    "può cambiare il segno del confronto (sezione 10) ma sposta le percentuali.\n",
    "\n",
    "**7. Il cliente del modello non cambia comportamento.** Consumo annuo costante,\n",
    "nessuna reazione al prezzo, nessun abbandono anticipato, nessuna penale di recesso,\n",
    "nessun bonus di benvenuto. Le offerte retail reali hanno tutte queste cose.\n",
    "\n",
    "**8. Una sola fonte secondaria per i tratti più delicati.** PUN 2025-2026 e PSV\n",
    "2021-2024 arrivano da pubblicatori che citano GME ma non sono GME. Sono stati\n",
    "verificati con due pubblicatori indipendenti e contro i valori annui ufficiali, e\n",
    "i controlli sono nella sezione 6, ma l'accesso diretto all'API GME (che richiede\n",
    "credenziali su registrazione) renderebbe la catena più corta.\n",
    "\n",
    "**9. Niente fasce orarie.** Tutto è monorario. Un cliente con contratto a fasce e\n",
    "consumi spostati di notte avrebbe numeri diversi.\n",
    "\n",
    "**10. Nessuna attualizzazione.** 100 euro del 2020 e 100 del 2026 sono sommati\n",
    "come se fossero la stessa cosa. Con l'inflazione del periodo non lo sono. È una\n",
    "scelta di leggibilità e va saputa."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a3d6e346",
   "metadata": {},
   "source": [
    "### Nota editoriale: sì ai prezzi, no ai nomi\n",
    "\n",
    "Questo notebook è **pubblicabile**, e i prezzi di mercato ci stanno: l'Indicatore\n",
    "di Prezzo ARERA è un aggregato anonimo di offerte realmente firmate, ed è l'oggetto\n",
    "dell'analisi.\n",
    "\n",
    "Vale invece un vincolo stretto sui **nomi**: nessuna azienda energetica è citata in\n",
    "nessun punto del progetto, né nei grafici, né nella presentazione. Dove serve un\n",
    "fornitore per fare un esempio si usa un nome inventato. L'unico posto dove\n",
    "compaiono indirizzi di siti appartenenti a venditori è\n",
    "`data/raw/PROVENIENZA.md`, il registro di provenienza delle due serie trascritte a\n",
    "mano (PUN Index 2025-2026 e PSV): senza quegli indirizzi la verifica non sarebbe\n",
    "rifacibile, e di quei siti si usa solo la tabella dell'indice ufficiale GME che\n",
    "sono tenuti a pubblicare, mai una loro offerta."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "374d9651",
   "metadata": {},
   "outputs": [],
   "source": [
    "# I numeri per la presentazione, scritti su file perché il carosello non ne\n",
    "# contenga di propri: `python build_carosello.py` legge solo da qui.\n",
    "import json\n",
    "\n",
    "numeri = {\n",
    "    \"generato\": str(pd.Timestamp.today().date()),\n",
    "    \"orizzonte\": {\n",
    "        \"ee_dal\": C.EE_PRIMO_MESE, \"ee_al\": C.EE_ULTIMO_MESE, \"ee_mesi\": len(pun),\n",
    "        \"gas_dal\": C.GAS_PRIMO_MESE, \"gas_al\": C.GAS_ULTIMO_MESE, \"gas_mesi\": len(psv),\n",
    "    },\n",
    "    \"controlli\": int(len(controlli)),\n",
    "    \"test_bloccanti_falliti\": int(((controlli[\"esito\"] == \"FALLITO\")\n",
    "                                   & controlli[\"bloccante\"]).sum()),\n",
    "    \"dati\": {\n",
    "        \"mesi_livello1\": int((fisso_firmabile[\"livello\"] == O.LIVELLO_REALE).sum()),\n",
    "        \"mesi_totali\": len(fisso_firmabile),\n",
    "        \"pun_min\": float(pun[\"pun_eur_mwh\"].min()),\n",
    "        \"pun_max\": float(pun[\"pun_eur_mwh\"].max()),\n",
    "        \"pun_mese_max\": str(pun[\"pun_eur_mwh\"].idxmax()),\n",
    "        \"pun_mese_min\": str(pun[\"pun_eur_mwh\"].idxmin()),\n",
    "        \"psv_max\": float(psv[\"psv_eur_smc\"].max()),\n",
    "        \"psv_mese_max\": str(psv[\"psv_eur_smc\"].idxmax()),\n",
    "        \"divergenza_fonti_2025\": 20.78,\n",
    "    },\n",
    "    \"markup\": {\n",
    "        \"variabile\": mis.markup_variabile, \"fisso\": mis.markup_fisso,\n",
    "        \"fisso_min\": mis.markup_fisso_min, \"fisso_max\": mis.markup_fisso_max,\n",
    "        \"variabile_media_offerte\": float(sp_var_media.median()),\n",
    "        \"mesi\": mis.mesi,\n",
    "    },\n",
    "    \"rolling\": {str(n): M.distribuzione(mat, \"delta_r2_fisso_12M\")\n",
    "                for n, mat in matrici.items()},\n",
    "    \"indipendenza\": {str(n): M.finestre_indipendenti(matrici[n], n)\n",
    "                     for n in matrici},\n",
    "    \"varianza\": {\n",
    "        \"36\": {\"contratto\": float(a36.iloc[0]), \"timing\": float(a36.iloc[1]),\n",
    "               \"interazione\": float(a36.iloc[2])},\n",
    "        \"60\": {k: float(v) for k, v in zip(\n",
    "            (\"contratto\", \"timing\", \"interazione\"),\n",
    "            anova60.set_index(\"fonte\")[\"eta_quadro_pct\"].iloc[:3])},\n",
    "    },\n",
    "    \"rischio\": {\n",
    "        \"riduzione_bolletta_pct\": float(q12[\"riduzione_volatilita_pct\"].median()),\n",
    "        \"riduzione_prezzo_pct\": float(q12[\"riduzione_volatilita_prezzo_pct\"].median()),\n",
    "        \"picco_evitato\": float(q12[\"picco_mensile_evitato_eur\"].median()),\n",
    "        \"shock_evitato\": float(q12[\"shock_annuale_evitato_eur\"].median()),\n",
    "        \"fic_mediano\": float(q12[\"fic_eur_anno\"].median()),\n",
    "        \"fic_min\": float(q12[\"fic_eur_anno\"].min()),\n",
    "        \"fic_max\": float(q12[\"fic_eur_anno\"].max()),\n",
    "    },\n",
    "    \"break_even\": {\n",
    "        \"mesi\": int(len(be_tab)),\n",
    "        \"quota_conveniva_pct\": float(conveniva.mean() * 100),\n",
    "        \"margine_mediano\": float(be_tab[\"margine\"].median()),\n",
    "        \"margine_min\": float(be_tab[\"margine\"].min()),\n",
    "        \"margine_max\": float(be_tab[\"margine\"].max()),\n",
    "    },\n",
    "    \"markup_differenza\": {\n",
    "        \"r2\": float(r2), \"pendenza\": float(pend), \"pareggio\": float(pareggio),\n",
    "        \"misurata\": float(mis.markup_fisso - mis.markup_variabile),\n",
    "    },\n",
    "    \"estremi_36m\": estremi,\n",
    "    \"confronto_mese_peggiore\": confronto_mese_peggiore,\n",
    "    \"profili\": profili,\n",
    "    \"rincaro_q4_2026\": rincaro,\n",
    "    \"scelte_retail\": scelte,\n",
    "    \"durata\": {\n",
    "        \"delta_12m\": float(sens[sens.variante == \"fisso 12 mesi (base)\"][\"delta_medio\"].iloc[0]),\n",
    "        \"delta_24m\": float(sens[sens.variante == \"fisso 24 mesi\"][\"delta_medio\"].iloc[0]),\n",
    "    },\n",
    "    \"scenari\": scenari[[\"data_scelta\", \"perche\", \"mesi\", \"delta_r2_eur\",\n",
    "                        \"delta_r2_eur_anno\", \"meno_costosa_ex_post_12M\"]]\n",
    "                .to_dict(orient=\"records\"),\n",
    "    \"feb2020\": {k: (float(v) if isinstance(v, (int, float)) else str(v))\n",
    "                for k, v in met_feb.items()},\n",
    "    \"gas\": {\n",
    "        \"consumo_smc\": cl_gas.consumo_annuo_smc,\n",
    "        \"finestre\": int(len(mat_g_36)),\n",
    "        \"mesi_finestra\": 36,\n",
    "        \"variabile_vince_pct\": float((mat_g_36[\"delta_r2_fisso_12M\"] > 0).mean() * 100),\n",
    "        \"delta_medio\": float(mat_g_36[\"delta_r2_fisso_12M\"].mean()),\n",
    "        \"delta_min\": float(mat_g_36[\"delta_r2_fisso_12M\"].min()),\n",
    "        \"delta_max\": float(mat_g_36[\"delta_r2_fisso_12M\"].max()),\n",
    "        \"primo_ingresso\": str(mat_g_36.index.min()),\n",
    "        \"ultimo_ingresso\": str(mat_g_36.index.max()),\n",
    "        # il PSV comincia nel 2021: nessuna finestra entra PRIMA della salita,\n",
    "        # ed è questo che spiega il risultato, non una proprietà del gas\n",
    "        \"psv_dal\": C.GAS_PRIMO_MESE, \"psv_al\": C.GAS_ULTIMO_MESE,\n",
    "        \"psv_min\": float(psv[\"psv_eur_smc\"].min()),\n",
    "        \"psv_max\": float(psv[\"psv_eur_smc\"].max()),\n",
    "        \"psv_mese_max\": str(psv[\"psv_eur_smc\"].idxmax()),\n",
    "        \"psv_rapporto\": float(psv[\"psv_eur_smc\"].max() / psv[\"psv_eur_smc\"].min()),\n",
    "        \"pun_rapporto\": float(pun[\"pun_eur_mwh\"].max() / pun[\"pun_eur_mwh\"].min()),\n",
    "    },\n",
    "    \"consumi\": {str(int(k)): float(v) for k, v in _c.items()},\n",
    "    \"frase\": riga_exec,\n",
    "    \"risposta_sintetica\": RISPOSTA_SINTETICA,\n",
    "}\n",
    "(C.OUTPUTS / \"numeri_carosello.json\").write_text(\n",
    "    json.dumps(numeri, indent=2, ensure_ascii=False), encoding=\"utf-8\")\n",
    "log(\"scritto outputs/numeri_carosello.json \"\n",
    "    f\"({len(json.dumps(numeri))} caratteri): è l'unica fonte di numeri del carosello\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1e72c644",
   "metadata": {},
   "source": [
    "## Scaricare i risultati"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "462d4d2d",
   "metadata": {},
   "outputs": [],
   "source": [
    "# =============================================================================\n",
    "#  SCARICARE I RISULTATI  ·  esegui questa cella alla fine\n",
    "# =============================================================================\n",
    "#  Mette grafici e CSV in un unico zip e lo scarica sul tuo computer.\n",
    "#  Su Colab il contenuto della sessione sparisce quando la chiudi: senza questo\n",
    "#  passaggio il lavoro resta solo a schermo.\n",
    "# =============================================================================\n",
    "import zipfile\n",
    "from pathlib import Path\n",
    "\n",
    "RISULTATI = Path(\"risultati_backtest.zip\")\n",
    "cartelle = [Path(\"charts\"), Path(\"outputs\"), Path(\"data\") / \"processed\"]\n",
    "\n",
    "with zipfile.ZipFile(RISULTATI, \"w\", zipfile.ZIP_DEFLATED) as z:\n",
    "    n = 0\n",
    "    for cartella in cartelle:\n",
    "        for f in sorted(cartella.rglob(\"*\")):\n",
    "            if f.is_file():\n",
    "                z.write(f, f.as_posix())\n",
    "                n += 1\n",
    "print(f\"{n} file in {RISULTATI} ({RISULTATI.stat().st_size / 1024:.0f} KB)\")\n",
    "\n",
    "try:\n",
    "    from google.colab import files                   # type: ignore\n",
    "    files.download(str(RISULTATI))\n",
    "except ImportError:\n",
    "    print(\"non sono su Colab: lo zip resta nella cartella del progetto.\")\n"
   ]
  }
 ],
 "metadata": {
  "colab": {
   "provenance": []
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "title": "Backtest prezzo fisso contro prezzo variabile, Italia 2020-2026"
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
