{ "cells": [ { "cell_type": "markdown", "id": "92d740bc", "metadata": {}, "source": [ "# Calcolatore 1 — Quanto serve per recuperare una perdita\n", "\n", "*Quaderno del capitolo «L'aritmetica che nessuno ti mostra» di\n", "**La matematica di chi perde**.*\n", "\n", "La perdita e il recupero non sono simmetrici, e la differenza cresce in fretta.\n", "Questo calcolatore mette i tuoi numeri dentro quell'asimmetria: quanto serve\n", "per tornare in pari, quanto costa la volatilità al capitale composto, e cosa\n", "succede aggiungendo la leva.\n", "\n", "---\n", "\n", "> **EN** — *Calculator 1 — How much it takes to recover from a loss.*\n", "> Notebook for the chapter \"The arithmetic nobody shows you\" of **The math of\n", "> those who lose**. Loss and recovery are not symmetric, and the gap grows\n", "> fast. This calculator puts your own numbers inside that asymmetry: how much\n", "> it takes to break even, what volatility costs compounded capital, and what\n", "> happens once you add leverage." ] }, { "cell_type": "markdown", "id": "c4070946", "metadata": {}, "source": [ "Le righe marcate **PROVA** sono quelle da cambiare: cambiale e riesegui per\n", "vedere l'effetto. Il resto — comprese le righe marcate **NON TOCCARE** —\n", "serve a mantenere il risultato confrontabile con quello stampato nel libro.\n", "\n", "The lines marked **TRY** are the ones to change: edit them and rerun to see\n", "the effect. Everything else — including lines marked **DO NOT CHANGE** —\n", "exists to keep the result comparable with the one printed in the book." ] }, { "cell_type": "code", "execution_count": null, "id": "dc9348c3", "metadata": {}, "outputs": [], "source": [ "# Setup — esegui questa cella per prima.\n", "%pip install -q \"polars>=1.0\"\n", "try:\n", " import avvio\n", "except ModuleNotFoundError:\n", " import urllib.request\n", "\n", " urllib.request.urlretrieve(\n", " \"https://raw.githubusercontent.com/cryptoverso-lab/quaderni/main/matematica/avvio.py\",\n", " \"avvio.py\",\n", " )\n", " import avvio\n", "\n", "avvio.prepara([\"btcusdt\"])" ] }, { "cell_type": "code", "execution_count": null, "id": "2ea9765d", "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "import numpy as np\n", "\n", "from cvbook.dati import carica\n", "from cvbook.lingua import t\n", "from cvbook.metriche import drawdown_massimo, equity, recupero_necessario, rendimenti" ] }, { "cell_type": "markdown", "id": "957d86ef", "metadata": {}, "source": [ "## 1. Il tuo numero\n", "\n", "Cambia `PERDITA` e riesegui. È l'unica cella che devi toccare.\n", "\n", "---\n", "\n", "> **EN** — *1. Your number.* Change `PERDITA` (loss) and rerun. It's the\n", "> only cell you need to touch." ] }, { "cell_type": "code", "execution_count": null, "id": "1293ccd7", "metadata": {}, "outputs": [], "source": [ "PERDITA = 0.50 # ← la perdita subita, in frazione: 0,50 vuol dire meno 50%\n", " # PROVA / TRY: qualunque valore fra 0,01 e 0,99 · guarda cosa\n", " # succede vicino a 0,90 (dove la curva diventa quasi verticale)\n", "\n", "recupero = recupero_necessario(PERDITA)\n", "print(t(\"perdita subita: \", \"loss taken: \") + f\"{PERDITA:6.1%}\")\n", "print(t(\"ti resta: \", \"you have left: \") + f\"{1 - PERDITA:6.1%}\"\n", " + t(\" del capitale\", \" of capital\"))\n", "print(t(\"serve un guadagno del: \", \"you need a gain of: \") + f\"{recupero:6.1%}\"\n", " + t(\" per tornare al punto di partenza\", \" to get back to break even\"))" ] }, { "cell_type": "markdown", "id": "9b16011a", "metadata": {}, "source": [ "## 2. La curva intera\n", "\n", "La forma è la cosa da guardare: fino al 30% è una salita, dal 70% in poi è un\n", "muro. Non c'è nessuna soglia dichiarata da qualche parte — è aritmetica.\n", "\n", "---\n", "\n", "> **EN** — *2. The full curve.* The shape is what matters: up to 30% it's a\n", "> climb, past 70% it's a wall. No threshold is declared anywhere — it's\n", "> arithmetic." ] }, { "cell_type": "code", "execution_count": null, "id": "858135dd", "metadata": {}, "outputs": [], "source": [ "perdite = np.linspace(0.01, 0.95, 200)\n", "recuperi = np.array([recupero_necessario(p) for p in perdite])\n", "\n", "with avvio.figura(\"schermo\"):\n", " fig, ax = plt.subplots()\n", " ax.plot(perdite * 100, recuperi * 100, linewidth=2)\n", " for p in (0.2, 0.5, 0.8, 0.9):\n", " r = recupero_necessario(p)\n", " ax.plot([p * 100], [r * 100], marker=\"o\")\n", " ax.annotate(\n", " f\"-{p:.0%} → +{r:.0%}\",\n", " xy=(p * 100, r * 100),\n", " xytext=(-6, 8),\n", " textcoords=\"offset points\",\n", " ha=\"right\",\n", " )\n", " ax.set_xlabel(t(\"Perdita subita (%)\", \"Loss taken (%)\"))\n", " ax.set_ylabel(t(\"Guadagno necessario per tornare in pari (%)\", \"Gain needed to break even (%)\"))\n", " ax.set_ylim(0, 1000)\n", " plt.show()" ] }, { "cell_type": "markdown", "id": "b6472f8f", "metadata": {}, "source": [ "## 3. Il freno della volatilità, sui tuoi parametri\n", "\n", "Due strategie con lo **stesso rendimento medio** non lasciano gli stessi soldi:\n", "la più volatile ne lascia meno. Il freno vale circa metà della varianza, e il\n", "calcolo qui sotto lo verifica invece di affermarlo.\n", "\n", "---\n", "\n", "> **EN** — *3. The volatility drag, on your own parameters.* Two strategies\n", "> with the **same average return** don't leave you with the same money: the\n", "> more volatile one leaves less. The drag is worth about half the variance,\n", "> and the calculation below verifies it instead of just asserting it." ] }, { "cell_type": "code", "execution_count": null, "id": "10a385f3", "metadata": {}, "outputs": [], "source": [ "MEDIA_GIORNALIERA = 0.0010 # ← rendimento medio per giorno\n", " # PROVA / TRY: 0,0005 · 0,0010 · 0,0020 — il freno\n", " # dipende dalla volatilità, non da questo valore\n", "GIORNI = 1000 # PROVA / TRY: 250 (un anno) · 1000 · 3000\n", "\n", "rng = np.random.default_rng(20260816)\n", "# NON TOCCARE / DO NOT CHANGE: il seme è fisso perché la tabella qui sotto è\n", "# commentata nel testo con questi numeri esatti; cambiarlo dopo aver visto il\n", "# risultato è il p-hacking che il libro smonta altrove.\n", "# The seed is fixed because the table below is discussed in the text with\n", "# these exact numbers; changing it after seeing the result is the p-hacking\n", "# the book takes apart elsewhere.\n", "\n", "print(f\"{t('volatilita/giorno', 'volatility/day'):>18s} \"\n", " f\"{t('media aritm.', 'arith. mean'):>14s} \"\n", " f\"{t('composto', 'compounded'):>12s} \"\n", " f\"{t('capitale finale', 'final capital'):>16s}\")\n", "for vol in (0.005, 0.01, 0.02, 0.035, 0.05):\n", " r = rng.normal(MEDIA_GIORNALIERA, vol, GIORNI)\n", " curva = equity(r)\n", " composto = curva[-1] ** (1 / GIORNI) - 1\n", " print(f\"{vol:18.3%} {r.mean():14.4%} {composto:12.4%} {curva[-1]:16.2f}x\")" ] }, { "cell_type": "markdown", "id": "bd7a3bf7", "metadata": {}, "source": [ "La colonna della media aritmetica resta ferma; quella del composto scende. È\n", "lo stesso rendimento medio che produce risultati diversi, e la differenza è\n", "**solo** la volatilità.\n", "\n", "---\n", "\n", "> **EN** — The arithmetic-mean column stays put; the compounded one drops.\n", "> It's the same average return producing different outcomes, and the\n", "> difference is **only** volatility." ] }, { "cell_type": "markdown", "id": "5b718fa9", "metadata": {}, "source": [ "## 4. La leva, che moltiplica il freno per il suo quadrato\n", "\n", "Con leva `k` il rendimento atteso si moltiplica per `k`, ma il freno per `k²`.\n", "Ecco perché esiste un punto oltre il quale aumentare la leva peggiora tutto —\n", "e su un asset già volatile quel punto arriva prestissimo.\n", "\n", "---\n", "\n", "> **EN** — *4. Leverage, which multiplies the drag by its square.* With\n", "> leverage `k` the expected return is multiplied by `k`, but the drag by\n", "> `k²`. That's why there's a point past which more leverage makes everything\n", "> worse — and on an already-volatile asset that point arrives very soon." ] }, { "cell_type": "code", "execution_count": null, "id": "8678ccc6", "metadata": {}, "outputs": [], "source": [ "df = carica(\"btcusdt\").sort(\"data\")\n", "# PROVA / TRY: per usare un'altra serie aggiungila anche alla cella di setup\n", "# (avvio.prepara([...])) — le 11 disponibili sono in codice/dati/registro.json:\n", "# btcusdt · ethusdt · solusdt · lunausdt · fttusdt · ftsemib · eni · enel ·\n", "# intesa · generali · eurusd\n", "r_reali = rendimenti(df[\"chiusura\"].to_numpy())\n", "\n", "\n", "def curva_con_leva(rend: np.ndarray, k: float) -> np.ndarray:\n", " \"\"\"Capitale con leva `k`, con l'azzeramento che e' definitivo.\n", "\n", " Un giorno in cui la posizione perde piu' del capitale non produce un numero\n", " negativo: produce la fine. Senza questo taglio il calcolo restituirebbe cali\n", " superiori al 100%, che non significano niente.\n", "\n", " Capital with `k`× leverage, where wipeout is permanent: a day in which the\n", " position loses more than the capital does not produce a negative number —\n", " it produces the end. Without this floor the calculation would return\n", " drawdowns above 100%, which mean nothing.\n", " \"\"\"\n", " passi = np.maximum(1.0 + k * rend, 0.0)\n", " return np.concatenate([[1.0], np.cumprod(passi)])\n", "\n", "\n", "print(f\"{t('leva', 'leverage'):>5s} {t('capitale finale', 'final capital'):>16s} \"\n", " f\"{t('calo massimo', 'max drawdown'):>14s} {t('azzerato il', 'wiped out on'):>14s}\")\n", "for k in (1, 2, 3, 5, 10): # PROVA / TRY: aggiungi 4 o 6, come suggerito negli esercizi\n", " curva = curva_con_leva(r_reali, k)\n", " azzerata = np.argmax(curva <= 0.0) if np.any(curva <= 0.0) else None\n", " quando = str(df[\"data\"][int(azzerata)]) if azzerata is not None else \"—\"\n", " print(f\"{k:5d} {curva[-1]:16.4f}x {drawdown_massimo(curva):14.1%} {quando:>14s}\")" ] }, { "cell_type": "markdown", "id": "1ee59ecd", "metadata": {}, "source": [ "### Esercizi\n", "\n", "1. Metti `PERDITA = 0.83`, che è il calo massimo realmente attraversato da\n", " questo asset nel periodo. Il numero che esce è il motivo per cui il capitolo\n", " sul rischio parla di **sopravvivenza** e non di rendimento.\n", "2. Nella cella della leva, prova `k = 4` e `k = 6`. Trova il punto in cui il\n", " capitale finale smette di crescere. Nessuna previsione è cambiata: solo la\n", " dimensione.\n", "3. Nella cella del freno, tieni fissa `MEDIA_GIORNALIERA` e chiediti quale\n", " volatilità azzera il composto. La risposta è la radice quadrata del doppio\n", " della media — provala.\n", "\n", "---\n", "\n", "> **EN** — *Exercises.*\n", "> 1. Set `PERDITA = 0.83`, the actual maximum drawdown this asset went\n", "> through in the period. The resulting number is why the risk chapter\n", "> talks about **survival**, not return.\n", "> 2. In the leverage cell, try `k = 4` and `k = 6`. Find the point where\n", "> final capital stops growing. No forecast changed — only the size.\n", "> 3. In the drag cell, keep `MEDIA_GIORNALIERA` fixed and ask yourself which\n", "> volatility wipes out the compounded return. The answer is the square\n", "> root of twice the mean — try it." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" } }, "nbformat": 4, "nbformat_minor": 5 }