Lab del libro · L04
L04 — Avversione alle perdite e disposizione, misurate sui propri numeri
Lab 4 — Il costo misurato dei tuoi automatismi
Quaderno del capitolo «Perché il tuo cervello non è fatto per questo» di La matematica di chi perde.
Il capitolo dice che prendere i piccoli utili e tenere le perdite grandi è misurabile e caro. Qui lo misuri sui tuoi parametri, e poi provi l'esperimento che il capitolo racconta: distinguere a occhio un processo con un vantaggio reale da uno senza. Quasi nessuno ci riesce.
Le righe marcate PROVA sono quelle da cambiare: cambiale e riesegui per vedere l'effetto. Il resto — comprese le righe marcate NON TOCCARE — serve a mantenere il risultato confrontabile con quello stampato nel libro.
Espandi lo script di questo passo
import matplotlib.pyplot as plt
import numpy as np
from cvbook import seed_for
from cvbook.dati import carica
from cvbook.metriche import drawdown_massimo, equity, rendimenti
from cvbook.simulazioni import equity_casuali
prezzi = carica("btcusdt").sort("data")["chiusura"].to_numpy()
r = rendimenti(prezzi)1. Il costo del prendere subito l'utile
Due comportamenti, stessa serie di prezzi, stesso capitale, stessi costi. Il primo compra e non tocca più niente. Il secondo fa quello che l'esperienza dei conti reali documenta: chiude appena è in utile di una certa percentuale, e resta dentro finché la perdita non raggiunge una soglia molto più larga.
Nessuna previsione distingue i due. Solo le due soglie.
Due curve di capitale su scala logaritmica lungo 3.240 giorni, con l'asse verticale da 10 alla 0 a 10 alla 1. Quella continua, di chi non tocca niente, arriva a 13,7 volte il capitale iniziale; quella tratteggiata, di chi chiude appena e' in utile del 10% e sopporta perdite fino al 50%, si ferma a 8,5. I cali massimi sono simili, 83% e 81%.
Output
chi non ha toccato niente: 13.66x calo massimo -83.2% chi ha preso i piccoli utili: 9.01x calo massimo -81.2% differenza: -34.1%
Espandi lo script di questo passo
PRENDI_UTILE = 0.10 # ← chiudi quando sei in utile di questa percentuale
# PROVA / TRY: 0,05 · 0,10 · 0,50 (vedi esercizi 1 e 2)
SOPPORTA_PERDITA = 0.50 # ← resti dentro finche' la perdita non arriva a questa
# PROVA / TRY: 0,10 · 0,50 · 0,70 (vedi esercizi 1 e 2)
COSTO = 0.0012 # ← costo di un GIRO COMPLETO — chiudere e riaprire
# PROVA / TRY: 0,0006 · 0,0012 · 0,0025
# NON TOCCARE / DO NOT CHANGE: qui lo 0,12% e' il costo di un giro completo, ed
# e' la convenzione che il libro dichiara fino al capitolo sull'analisi tecnica.
# Addebitarlo due volte per giro — una in uscita e una al rientro — e' la
# convenzione severa, che il libro introduce solo dal cap. 18b in avanti: con
# quella il quaderno chiudeva a 8,46 volte dove il capitolo stampa 9,0.
# Here 0.12% is the cost of a full round trip, the convention the book declares
# up to the technical-analysis chapter. Charging it twice per round trip is the
# stricter convention, introduced only from ch. 18b on.
def con_soglie(p: np.ndarray, su: float, giu: float, costo: float) -> np.ndarray:
"""Chiude quando la posizione tocca una soglia, e rientra il giorno dopo.
Sono tre le cose che questo comportamento paga rispetto al non far nulla,
e vale la pena tenerle distinte perche' pesano in modo molto diverso:
1. il costo del giro completo — uscita piu' rientro — addebitato una volta
sola, in uscita, secondo la convenzione dichiarata dal capitolo;
2. il giorno passato fuori dal mercato ad ogni chiusura — ed e' questa la
voce piu' cara, perche' il capitolo sulla media che mente ha mostrato
che pochissimi giorni contengono quasi tutto il risultato;
3. niente altro: nessuna previsione, nessuna scelta di direzione.
"""
valore = np.empty(len(p))
valore[0] = 1.0
ingresso, quota, liquido, dentro = p[0], 1.0 / p[0], 0.0, True
for i in range(1, len(p)):
if dentro:
corrente = quota * p[i]
variazione = p[i] / ingresso - 1.0
if variazione >= su or variazione <= -giu:
liquido = corrente * (1.0 - costo) # esce: paga il giro intero
dentro, corrente = False, liquido
valore[i] = corrente
else:
ingresso = p[i] # rientra il giorno dopo, senza ripagare:
quota = liquido / p[i] # il giro e' gia' stato addebitato in uscita
dentro = True
valore[i] = quota * p[i]
# Anche l'ingresso del primo giorno e' un'operazione, e va pagato: sotto
# sta il compra-e-tieni che lo paga, e le due colonne devono essere
# confrontabili fino all'ultimo centesimo.
return valore * (1.0 - costo)
# Anche chi non tocca niente paga il proprio ingresso: e' un'operazione, e il
# metro di confronto non puo' viaggiare gratis mentre l'altro comportamento
# paga. Sono dodici centesimi per mille euro, tutti a favore della tesi.
# Whoever never touches anything still pays for the entry: it is one trade, and
# the benchmark cannot travel for free while the other behaviour pays.
fermo = equity(r) * (1 - COSTO)
nervoso = con_soglie(prezzi, PRENDI_UTILE, SOPPORTA_PERDITA, COSTO)
with avvio.figura("schermo"):
fig, ax = plt.subplots()
ax.semilogy(fermo, linewidth=1.8, label="compra e non tocca niente")
ax.semilogy(nervoso, linewidth=1.8, linestyle="--",
label=f"chiude a +{PRENDI_UTILE:.0%}, sopporta -{SOPPORTA_PERDITA:.0%}")
ax.set_ylabel("Capitale (scala log)")
ax.set_xlabel("Giorni")
ax.legend()
plt.show()
print(f"chi non ha toccato niente: {fermo[-1]:6.2f}x calo massimo {drawdown_massimo(fermo):.1%}")
print(f"chi ha preso i piccoli utili: {nervoso[-1]:6.2f}x calo massimo {drawdown_massimo(nervoso):.1%}")
print(f"differenza: {nervoso[-1] / fermo[-1] - 1:+.1%}")Nota le due colonne del calo massimo. Il secondo comportamento ha rinunciato a una parte del risultato senza comprarsi in cambio nemmeno un po' di tranquillità. Ha pagato per l'illusione di controllo.
2. L'asimmetria del dolore, e perché appiattisce
I parametri sperimentali della teoria del prospetto: la perdita pesa circa due volte e mezzo il guadagno di pari entità, e la curva si appiattisce allontanandosi dallo zero.
Una curva a esse centrata sull'origine, con il guadagno o la perdita in euro da meno 10.000 a 10.000 in orizzontale e il valore percepito in unita' arbitrarie in verticale, da meno 6.000 a poco oltre 2.000. Il ramo delle perdite scende molto piu' ripido di quanto salga quello dei guadagni: a 2.000 euro il rapporto fra dolore e piacere e' 2,25, e il tratto da meno 8.000 a meno 10.000 aggiunge meno dolore del primo tratto sotto lo zero.
Output
2,000 euro: piacere 803.4 dolore 1807.5 rapporto 2.25 8,000 euro: piacere 2720.9 dolore 6122.1 rapporto 2.25 dolore nel passare da 0 a -2.000: 1807.5 dolore nel passare da -8.000 a -10.000: 1328.3 È il motivo per cui, dopo una perdita gia' grande, rischiare ancora costa pochissimo in termini di sofferenza attesa.
Espandi lo script di questo passo
CURVATURA = 0.88
AVVERSIONE = 2.25
# NON TOCCARE / DO NOT CHANGE: sono i parametri stimati sperimentalmente dalla
# teoria del prospetto (Kahneman e Tversky), non un valore a piacere — cambiarli
# smetterebbe di rappresentare quella ricerca.
# These are the parameters experimentally estimated by prospect theory
# (Kahneman and Tversky), not an arbitrary value — changing them would stop
# representing that research.
importi = np.linspace(-10_000, 10_000, 400)
# np.where valuta entrambi i rami: si eleva a potenza il valore assoluto e si
# rimette il segno dopo, altrimenti numpy si lamenta delle radici di numeri
# negativi (e ha ragione).
grandezza = np.abs(importi) ** CURVATURA
valore = np.where(importi >= 0, grandezza, -AVVERSIONE * grandezza)
with avvio.figura("schermo"):
fig, ax = plt.subplots()
ax.plot(importi, valore, linewidth=2)
ax.axhline(0, linewidth=0.8, color="#8C8C8C")
ax.axvline(0, linewidth=0.8, color="#8C8C8C")
ax.set_xlabel("Guadagno o perdita (euro)")
ax.set_ylabel("Valore percepito (unità arbitrarie)")
plt.show()
for x in (2_000, 8_000):
su = x**CURVATURA
giu = AVVERSIONE * x**CURVATURA
print(f"{x:6,d} euro: piacere {su:9.1f} dolore {giu:9.1f} rapporto {giu / su:.2f}")
passo_vicino = AVVERSIONE * (2000**CURVATURA)
passo_lontano = AVVERSIONE * (10000**CURVATURA - 8000**CURVATURA)
print(f"\ndolore nel passare da 0 a -2.000: {passo_vicino:8.1f}")
print(f"dolore nel passare da -8.000 a -10.000: {passo_lontano:8.1f}")
print("È il motivo per cui, dopo una perdita gia' grande, rischiare ancora "
"costa pochissimo in termini di sofferenza attesa.")3. Riesci a distinguere il vantaggio dal rumore?
Sei serie. Alcune hanno un vantaggio reale, altre no. Scrivi la tua risposta prima di eseguire la cella successiva.
Sei riquadri titolati da «serie 1» a «serie 6», ciascuno con una curva di capitale a base 100 e una riga punteggiata sul valore di partenza. Le scale verticali sono diverse fra loro: la prima va da 70 a 105, la seconda arriva a 140, la quarta scende sotto 60. Due delle sei hanno dentro un vantaggio reale, le altre quattro no, e l'ordine e' mescolato.
Espandi lo script di questo passo
rng = np.random.default_rng(seed_for("lab-bias-indovina"))
# NON TOCCARE / DO NOT CHANGE: scrivi la tua risposta PRIMA di eseguire la
# cella successiva. Cambiare il seme dopo aver sbagliato per ottenere un
# disegno più facile vanificherebbe l'esercizio, non lo migliorerebbe.
# Write down your answer BEFORE running the next cell. Changing the seed
# after getting it wrong, to get an easier draw, would defeat the exercise,
# not improve it.
VANTAGGI = rng.permutation([0.0, 0.0, 0.0, 0.0005, 0.0005, 0.0])
with avvio.figura("schermo"):
fig, assi = plt.subplots(2, 3, figsize=(11, 5))
curve = []
for k, ax in enumerate(assi.flat):
c = equity_casuali(1, 400, rendimento_atteso=VANTAGGI[k],
volatilita_periodo=0.02, rng=rng)[0]
curve.append(c)
ax.plot(c * 100, linewidth=1.4)
ax.axhline(100, linestyle=":", linewidth=0.8)
ax.set_title(f"serie {k + 1}", fontsize=10)
ax.set_xticks([])
plt.show()Output
vantaggio reale per operazione: serie 1: 0.0000% → capitale finale 0.88x serie 2: 0.0500% → capitale finale 1.08x serie 3: 0.0000% → capitale finale 0.95x serie 4: 0.0000% → capitale finale 0.48x serie 5: 0.0000% → capitale finale 1.19x serie 6: 0.0500% → capitale finale 1.43x Se le due con vantaggio non sono quelle che avevi indicato, non e' un tuo limite: 400 osservazioni non bastano a distinguerle, e il capitolo sulla potenza statistica dice quante ne servirebbero.
Espandi lo script di questo passo
print("vantaggio reale per operazione:")
for k, v in enumerate(VANTAGGI):
print(f" serie {k + 1}: {v:.4%} → capitale finale {curve[k][-1]:.2f}x")
print("\nSe le due con vantaggio non sono quelle che avevi indicato, non e' un "
"tuo limite: 400 osservazioni non bastano a distinguerle, e il capitolo "
"sulla potenza statistica dice quante ne servirebbero.")Esercizi
- Nella prima cella metti
PRENDI_UTILE = 0.05eSOPPORTA_PERDITA = 0.70: è il comportamento estremo, e il costo cresce di conseguenza. - Prova
PRENDI_UTILE = 0.50eSOPPORTA_PERDITA = 0.10— cioè il contrario di quello che fa quasi tutti. Guarda cosa succede al risultato e al calo massimo: non è gratis nemmeno quello. - Rifai l'esperimento della terza cella cambiando
400in4000. Con dieci volte le osservazioni la distinzione diventa possibile. È esattamente il punto del capitolo sul potere statistico.
Riproducibilità e download
Eseguito il 2026-08-27 dal quaderno del repository
Il quaderno
lab_04_bias.ipynb15,8 KB
sha256 ff5ff213ddd9e016fa2d3f454055be78f7adba9fdd3f8ef1914d89fe23d61c70
lab_04_bias.py12,3 KB
sha256 a154812752bac28498310133090ce0cf5e70fc067904e06d2cf7b9984bf6b373
I dati
btcusdt.parquet93,2 KB
sha256 ea75ad84e6e981507054df5c622c6b0ec3c8849c1f4dd007721878d4e4c8a329
Fonte: Binance Data Vision · Periodo: 2017-08-17 → 2026-06-30 · 3240 righe · estratto 2026-08-16