Lab from the book · L21
L21 — Two implementations of the same strategy: find the lookahead
Lab 21 — Find the lookahead
Code language
The code, its comments and its outputs are in Italian: they are the book’s code, kept identical to what the reader runs.
Notebook for the chapter "AI as an accelerator". Below are two implementations of the same strategy, both written the way an automated assistant would write them on request. One is correct, the other has a subtle lookahead. Your task is to find which one, before running them. Then you run them and see the difference. It's the exercise I recommend doing before letting a machine write anything you'll later use with real money.
The lines marked TRY are the ones to change: edit them and rerun to see the effect. Everything else — including lines marked DO NOT CHANGE — exists to keep the result comparable with the one printed in the book.
Show the script for this step
import matplotlib.pyplot as plt
import numpy as np
from cvbook import seed_for
from cvbook.dati import carica
from cvbook.metriche import rendimenti
from cvbook.regole import esegui
prezzi = carica("btcusdt").sort("data")["chiusura"].to_numpy()
COSTO = 0.0012The two versions
The stated strategy is the same for both: stay invested when closing price crosses above the upper band, computed as a twenty-day average plus a twenty-day standard deviation. Exit when it drops below the average. Read them carefully. Don't run the next cell yet.
Show the script for this step
# NON TOCCARE / DO NOT CHANGE: le due versioni sono la coppia corretta/errata
# su cui si basa l'esercizio — scrivi prima la tua risposta, poi esegui.
# The two versions are the correct/wrong pair the exercise is built on — write
# down your answer first, then run them.
def versione_a(p: np.ndarray, finestra: int = 20) -> np.ndarray:
"""Versione A."""
posizione = np.zeros(len(p))
stato = 0.0
for t in range(finestra, len(p)):
blocco = p[t - finestra:t]
media = blocco.mean()
banda = media + blocco.std(ddof=1)
if p[t - 1] > banda:
stato = 1.0
elif p[t - 1] < media:
stato = 0.0
posizione[t] = stato
return posizione
def versione_b(p: np.ndarray, finestra: int = 20) -> np.ndarray:
"""Versione B."""
posizione = np.zeros(len(p))
stato = 0.0
for t in range(finestra, len(p)):
blocco = p[t - finestra + 1:t + 1]
media = blocco.mean()
banda = media + blocco.std(ddof=1)
if p[t] > banda:
stato = 1.0
elif p[t] < media:
stato = 0.0
posizione[t] = stato
return posizioneBefore continuing
Write down your answer here. Which of the two uses information that, at the moment of deciding, didn't exist yet? (The difference is two characters.)
Two capital curves on a logarithmic scale over 3,240 days, with the vertical axis running from ten to the zero to ten to the fifth. The two versions of the same strategy make the same number of trades, 190, but version B ends at 107,029 times the starting capital and version A, dashed, at 32. The two are 3,348 times apart, and in the code the difference is two characters.
Output
versione A: 31.97x operazioni 190 versione B: 107029.36x operazioni 190 rapporto: 3347.73
Show the script for this step
a = esegui(prezzi, versione_a(prezzi), costo=COSTO)
b = esegui(prezzi, versione_b(prezzi), costo=COSTO)
with avvio.figura("schermo"):
fig, ax = plt.subplots()
ax.semilogy(b["curva"], linewidth=2, label=f"versione B — {b['finale']:,.1f}x")
ax.semilogy(a["curva"], linewidth=2, linestyle="--", label=f"versione A — {a['finale']:,.1f}x")
ax.set_ylabel("Capitale (scala log)")
ax.set_xlabel("Giorni")
ax.legend()
plt.show()
print(f"versione A: {a['finale']:10.2f}x operazioni {a['operazioni']:.0f}")
print(f"versione B: {b['finale']:10.2f}x operazioni {b['operazioni']:.0f}")
print(f"rapporto: {b['finale'] / a['finale']:10.2f}")The two tests, and why you need both
The first is the prefix invariance test: the value computed for a given day must not change when later data arrives. It catches errors where a statistic is computed over the entire period. Run it and see what happens.
Output
versione A: troncando a 600: identico troncando a 1600: identico troncando a 2600: identico → PASSA versione B: troncando a 600: identico troncando a 1600: identico troncando a 2600: identico → PASSA
Show the script for this step
def test_invarianza(funzione, p: np.ndarray, tagli=(600, 1600, 2600)) -> bool:
"""Il passato non deve cambiare quando arriva il futuro."""
completa = funzione(p)
passa = True
for taglio in tagli:
parziale = funzione(p[:taglio])
uguali = np.allclose(parziale[:taglio - 1], completa[:taglio - 1])
print(f" troncando a {taglio}: {'identico' if uguali else 'DIVERSO'}")
passa = passa and uguali
return passa
for nome, funzione in (("versione A", versione_a), ("versione B", versione_b)):
print(f"{nome}:")
esito = test_invarianza(funzione, prezzi)
print(f" → {'PASSA' if esito else 'FALLISCE'}\n")Both pass. And this is the most useful lesson of the notebook. The invariance test doesn't catch this type of error, because version B doesn't use the future: it uses the present — data not yet available at decision time, but still belonging to the past of any truncation. A second, different kind of test is needed. The second test is more direct: change one day's price and see whether that same day's position changes. If it does, the decision was using data that didn't exist yet at the time of deciding.
Output
versione A: alterando il giorno 700: la posizione di quel giorno resta uguale alterando il giorno 1500: la posizione di quel giorno resta uguale alterando il giorno 2400: la posizione di quel giorno resta uguale → PASSA versione B: alterando il giorno 700: la posizione di quel giorno CAMBIA alterando il giorno 1500: la posizione di quel giorno CAMBIA alterando il giorno 2400: la posizione di quel giorno resta uguale → FALLISCE: decide con il prezzo di oggi
Show the script for this step
def test_esecuzione_sfasata(funzione, p: np.ndarray, giorni=(700, 1500, 2400)) -> bool:
"""La posizione di oggi non deve dipendere dal prezzo di oggi."""
base = funzione(p)
passa = True
for t in giorni:
alterata = p.copy()
alterata[t] *= 1.5 # un movimento enorme, quel giorno
nuova = funzione(alterata)
cambia = not np.isclose(nuova[t], base[t])
print(f" alterando il giorno {t}: la posizione di quel giorno "
f"{'CAMBIA' if cambia else 'resta uguale'}")
passa = passa and not cambia
return passa
for nome, funzione in (("versione A", versione_a), ("versione B", versione_b)):
print(f"{nome}:")
esito = test_esecuzione_sfasata(funzione, prezzi)
print(f" → {'PASSA' if esito else 'FALLISCE: decide con il prezzo di oggi'}\n")The solution
Version B uses p[t] to decide day t's position, and computes mean and deviation on a window that includes day t. At the moment that decision would need to be made, today's close doesn't exist yet. The code difference is t - finestra + 1:t + 1 versus t - finestra:t, and p[t] versus p[t - 1]. Two characters. The result changes by orders of magnitude. It's exactly the error these tools produce most often, and not out of carelessness: using the current index is the most natural phrasing in human language. "I buy when price crosses the band" becomes, without a second thought, if p[t] > banda.
The four prompts, written out
The chapter lists the phrasings I actually use. I reproduce them here so they can be copied. They stay in Italian: they are prompts meant to be used as written, not labels to translate — adapt them to your own working language when you copy them.
Output
--- sul codice --- Elenca tutti i punti in cui questo calcolo potrebbe usare un'informazione non disponibile al momento della decisione, anche i piu' improbabili, e per ciascuno indica quale riga. --- sul risultato --- Elenca dieci ragioni per cui questo risultato potrebbe essere un artefatto e non un fenomeno, ordinate dalla piu' probabile alla meno probabile. --- sul metodo --- Assumi che io mi stia ingannando. Descrivi il modo piu' plausibile in cui questo procedimento produce un risultato positivo anche in assenza di qualunque vantaggio reale. --- sui dati --- Scrivi i controlli che rivelerebbero valori riempiti, giunzioni fra fonti diverse, giorni mancanti trattati come zero e incoerenze fra massimo, minimo e chiusura; poi eseguili e riporta i conteggi. Lo schema comune: chiedere un ELENCO invece di un giudizio. Un giudizio puo' essere assecondante; un elenco e' verificabile voce per voce. E se l'elenco e' vuoto o generico, quella e' a sua volta un'informazione.
Show the script for this step
RICHIESTE = {
"sul codice":
"Elenca tutti i punti in cui questo calcolo potrebbe usare "
"un'informazione non disponibile al momento della decisione, anche i "
"piu' improbabili, e per ciascuno indica quale riga.",
"sul risultato":
"Elenca dieci ragioni per cui questo risultato potrebbe essere un "
"artefatto e non un fenomeno, ordinate dalla piu' probabile alla meno "
"probabile.",
"sul metodo":
"Assumi che io mi stia ingannando. Descrivi il modo piu' plausibile in "
"cui questo procedimento produce un risultato positivo anche in assenza "
"di qualunque vantaggio reale.",
"sui dati":
"Scrivi i controlli che rivelerebbero valori riempiti, giunzioni fra "
"fonti diverse, giorni mancanti trattati come zero e incoerenze fra "
"massimo, minimo e chiusura; poi eseguili e riporta i conteggi.",
}
for ambito, testo in RICHIESTE.items():
print(f"\n--- {ambito} ---\n{testo}")
print("\n\nLo schema comune: chiedere un ELENCO invece di un giudizio. Un giudizio "
"puo' essere assecondante; un elenco e' verificabile voce per voce. E se "
"l'elenco e' vuoto o generico, quella e' a sua volta un'informazione.")The calculation that closes the chapter
How many ideas can you try in one afternoon with a tool that writes the code for you? And what happens to the meaning of your best result?
Output
idee provate migliore dentro la stessa fuori
10 5.11x 0.69x
50 18.86x 1.77x
200 35.83x 1.04x
1000 44.75x 1.29x
Piu' cerchi, piu' trovi. E quello che trovi in piu' NON sopravvive al contatto con dati che non hai usato per cercare.
L'intelligenza artificiale non accelera la scoperta: accelera la RICERCA. Se hai il metodo, accelera anche la scoperta. Se non ce l'hai, accelera solo la produzione di illusioni — a un ritmo che prima era fisicamente impossibile.Show the script for this step
rng = np.random.default_rng(seed_for("lab-ai"))
r = rendimenti(prezzi)
meta = len(r) // 2
print(f"{'idee provate':>13s} {'migliore dentro':>17s} {'la stessa fuori':>17s}")
for tentativi in (10, 50, 200, 1000): # PROVA / TRY: aggiungi 5000
migliore, fuori = -np.inf, np.nan
for _ in range(tentativi):
rumore = rng.normal(size=len(r))
posizione = (rumore > 0).astype(float)
dentro = float(np.prod(1 + posizione[:meta] * r[:meta]))
if dentro > migliore:
migliore = dentro
fuori = float(np.prod(1 + posizione[meta:] * r[meta:]))
print(f"{tentativi:13d} {migliore:16.2f}x {fuori:16.2f}x")
print("\nPiu' cerchi, piu' trovi. E quello che trovi in piu' NON sopravvive al "
"contatto con dati che non hai usato per cercare.")
print("\nL'intelligenza artificiale non accelera la scoperta: accelera la "
"RICERCA. Se hai il metodo, accelera anche la scoperta. Se non ce l'hai, "
"accelera solo la produzione di illusioni — a un ritmo che prima era "
"fisicamente impossibile.")Reproducibility & downloads
Run on 2026-08-27 from the repository notebook
The notebook
lab_21_ai.ipynb17.8 KB
sha256 7627452b62b977924cf77af6e292904eaca67bcc1b6e3ca6657242dbbabd6c18
lab_21_ai.py13.4 KB
sha256 a6994f69c515fc9a65e79888558d14dc7c0c7d1c8f9a2f430bd6e08205856d26
The data
btcusdt.parquet93.2 KB
sha256 ea75ad84e6e981507054df5c622c6b0ec3c8849c1f4dd007721878d4e4c8a329
Source: Binance Data Vision · Period: 2017-08-17 → 2026-06-30 · 3,240 rows · extracted 2026-08-16