Skip to content
SIAT PAPER 2026Open the research page
Cryptoverso

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
lab_21_ai.py
python
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.0012

The 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
lab_21_ai.py
python
# 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 posizione

Before 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.

Two implementations of the same strategy, and the difference that is worth three thousand times.Source: Binance Data Vision · Period: 2017-08-17 … 2026-06-30 · Method: Same series, same rule and same number of trades; only which close the position uses to decide changes.

Output

versione A:      31.97x   operazioni 190
versione B:  107029.36x   operazioni 190
rapporto:      3347.73
Show the script for this step
lab_21_ai.py
python
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
lab_21_ai.py
python
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
lab_21_ai.py
python
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
lab_21_ai.py
python
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
lab_21_ai.py
python
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

Back to the lab index