L'errore ambientale del trader algoritmico retail è vivere dentro la piattaforma: idee testate direttamente nel tester di MetaTrader, sui suoi dati, con i suoi tempi. Il salto di qualità è dotarsi di un laboratorio esterno — nel mio caso Python — dove un'ipotesi si interroga in minuti, con la statistica che serve, prima di meritarsi un'implementazione.
Il flusso maturo è un imbuto: ipotesi → laboratorio (ore) → solo se sopravvive: EA e tester (giorni) → solo se sopravvive: shadow/demo (settimane) → produzione. Il laboratorio è il piano dell'imbuto dove morire costa meno; per questo deve essere il più affollato. Questa è la pipeline che ha prodotto la mia ricerca su 210.240 barre — e i suoi pezzi sono riutilizzabili per qualsiasi ipotesi vostra.
1 · I dati: le API di Binance
Binance espone gratuitamente tutto quello che serve alla ricerca su crypto. Tre canali, tre usi diversi:
- REST
/api/v3/klines— storico OHLCV per il backtest e la ricerca. Fino a 1000 barre per chiamata, si pagina all'indietro. - Websocket
@kline_1m/@bookTicker— il feed live. IlbookTicker(best bid/ask in tempo reale) è quello che usa il mio bot di latency arbitrage come "prezzo veloce". - Campi di order flow — le klines includono
taker_buy_volume: da lì si costruisce il taker buy ratio, un proxy (grezzo) dell'aggressività di acquisto.
# Download storico klines — paginato, senza dipendenze esotiche
import requests, pandas as pd, time
def klines(symbol="BTCUSDT", interval="5m", start_ms=None, limit=1000):
r = requests.get("https://api.binance.com/api/v3/klines",
params=dict(symbol=symbol, interval=interval,
startTime=start_ms, limit=limit), timeout=10)
r.raise_for_status()
cols = ["open_time","open","high","low","close","volume","close_time",
"quote_vol","n_trades","taker_buy_vol","taker_buy_quote","_"]
df = pd.DataFrame(r.json(), columns=cols).astype(float, errors="ignore")
return df
frames, t0 = [], int(pd.Timestamp("2024-06-01").timestamp()*1000)
while True:
df = klines(start_ms=t0)
if df.empty: break
frames.append(df); t0 = int(df.close_time.iloc[-1]) + 1
time.sleep(0.15) # rispetta i rate limit
data = pd.concat(frames, ignore_index=True)
2 · L'audit: prima dell'ipotesi, sempre
Regola non negoziabile: l'audit dei dati viene prima dell'ipotesi. Un risultato — positivo o negativo — su dati non auditati non è un risultato. Nella mia ricerca, l'audit ha trovato 148.608 timestamp forniti in microsecondi anziché millisecondi: il genere di difetto silenzioso che, non corretto, disallinea le feature e fabbrica edge fantasma.
# Audit minimo: duplicati, gap, coerenza OHLC
assert data.open_time.duplicated().sum() == 0, "duplicati!"
step = data.open_time.diff().dropna()
assert (step == step.mode()[0]).all(), "gap nel feed!"
ok = (data.high >= data[["open","close"]].max(axis=1)) & \
(data.low <= data[["open","close"]].min(axis=1))
assert ok.all(), "violazioni OHLC!"
3 · Feature senza look-ahead
Il look-ahead — usare informazione che al momento della decisione non esisteva — è l'errore più subdolo del settore, perché produce backtest meravigliosi e sistemi live che muoiono al primo giorno. La difesa è strutturale: ogni feature è vincolata per costruzione alla barra chiusa. In pandas la parola magica è shift(1):
f = pd.DataFrame(index=data.index)
rng = (data.high - data.low).replace(0, 1e-12)
f["body_ratio"] = ((data.close - data.open).abs() / rng).shift(1)
f["lower_shadow"] = ((data[["open","close"]].min(axis=1) - data.low) / rng).shift(1)
f["ret_1"] = data.close.pct_change().shift(1)
f["taker_ratio"] = (data.taker_buy_vol / data.volume).shift(1)
f["atr_norm"] = (rng.rolling(96).mean() / data.close).shift(1)
# target: rendimento FORWARD, mai shiftato
horizon = 12 # 1 ora su barre 5m
y = data.close.pct_change(horizon).shift(-horizon)
Se usate l'AI per generare questi script, mettete il vincolo nel prompt, esplicitamente — l'AI introduce look-ahead silenziosi con una frequenza impressionante. Il mio prompt tipo è nell'articolo sul metodo.
4 · Il test: contro il baseline, con i costi, IS/OOS
Tre regole che separano la ricerca dall'autoinganno:
- Ogni pattern deve battere il baseline ("sempre long su tutte le barre"), non lo zero: su un asset con drift, anche il nulla sembra un segnale.
- I costi dentro dal primo minuto: nel mio caso 6,5 bps di costo round-trip. Un edge lordo di 1,3 bps contro 6,5 di pedaggio è un rapporto segnale/costo di 1:30 — cioè non è un edge.
- In-sample / out-of-sample con voto unico: si sviluppa solo sulla prima parte dei dati; gli ultimi 6 mesi restano sigillati e si usano una volta, alla fine, come verdetto. Usarli dieci volte "per controllare" li trasforma in in-sample con cerimonia. Criterio di lettura: non pretendo che l'OOS eguagli l'IS; pretendo che non cambi verdetto. Segnali con +1,5 bps IS che diventano −6,8 OOS sono la firma del rumore.
5 · Triple barrier: etichette che rispettano la volatilità
Etichettare gli esiti con orizzonti fissi ("dov'è il prezzo dopo N barre?") ignora come un trade vive davvero: con uno stop, un target e un tempo massimo. La triple barrier etichetta ogni segnale con barriere dinamiche in unità di ATR — stop a k×ATR, target a m×ATR, timeout a T barre — e misura profit factor e perdita media per configurazione. Con drift nullo le barriere danno PF≈1 al lordo, quindi <1 dopo i costi: la gestione dinamica non crea un edge che non c'è — lo stesso teorema che vale per i vostri trailing stop.
6 · Dal laboratorio alla piattaforma
Solo ciò che sopravvive a tutto questo merita un'implementazione: prototipo Python → specifica formale → MQL5 (con la checklist degli errori) → tester "every tick based on real ticks" → shadow/demo con criterio di promozione scritto prima. Nel mio standard: almeno 30 trade con aspettativa media positiva prima di un euro vero.