MQL5 è un linguaggio C-like completo, con OOP, e un ambiente — MetaTrader 5 — pieno di comportamenti specifici che nessun manuale generalista copre. Questo non è un corso: è la lista ragionata dei punti dove ho visto morire più soldi, nel mio codice e in quello che revisiono. Se usate l'AI per generare EA, questa lista è anche la vostra checklist di revisione: sono esattamente i punti dove il codice generato sbaglia più spesso.
1 · Nuova barra vs ogni tick
OnTick scatta a ogni aggiornamento di prezzo; la maggior parte delle logiche su barre deve invece agire una volta per barra. Il controllo canonico — confrontare il timestamp della barra corrente con l'ultimo visto — deve stare all'inizio, e la logica deve leggere la barra chiusa (indice 1), non quella in formazione (indice 0). Leggere l'indice 0 in un EA che decide "a fine candela" è look-ahead in versione live: produce backtest e realtà divergenti.
2 · Denaro e volumi non si confrontano con ==
Ogni size va normalizzata a SYMBOL_VOLUME_STEP e vincolata tra SYMBOL_VOLUME_MIN e SYMBOL_VOLUME_MAX; ogni prezzo va normalizzato ai tick. Il classico conto prop bruciato dal lotto 0,014999 arrotondato male non è una leggenda. (È anche il problema che FastAutolot esiste per eliminare: il sizing calcolato dal sistema, non dalle dita.)
3 · Gli ordini falliscono, ed è normale
OrderSend restituisce esiti che vanno letti: requote, off quotes, no money, trade context busy, mercato chiuso. Un EA di produzione distingue gli errori transitori (ritenta con backoff, poche volte, con log) da quelli definitivi (si ferma e notifica). Il codice generato dall'AI, di default, o ignora il retcode o ritenta all'infinito: entrambe le versioni, su un conto vero, sono già costate care a qualcuno.
4 · Lo stato vero è sul server
Dopo un ordine, la verità è nelle posizioni e negli ordini risultanti, non nella variabile locale g_hoPosizione. Servono: riconciliazione periodica stato-locale/stato-server, e ricostruzione dello stato in OnInit — l'EA può riavviarsi con posizioni aperte (crash, riavvio VPS, ricompilazione). È ciò che separa i sistemi che sopravvivono a un blackout da quelli che al riavvio aprono la posizione doppia.
5 · OnTradeTransaction per gli eventi, non il polling
Esecuzioni, chiusure da SL/TP, modifiche: arrivano come transazioni asincrone. Gestirle in OnTradeTransaction — filtrando per magic number — è più affidabile che interrogare la cronologia a ogni tick. Ma la funzione va scritta sapendo che le transazioni possono arrivare in ordini non ovvi e duplicarsi nei casi limite: idempotenza — ogni handler deve poter processare lo stesso evento due volte senza danni.
6 · Il tester mente sui dettagli
"Every tick based on real ticks" è il minimo sindacale; anche così, lo spread storico può essere ricostruito, lo slippage non esiste, gli swap tripli del mercoledì sui metalli e le interruzioni di liquidità vanno verificati a mano. Un backtest MT5 è un limite superiore della realtà: trattatelo come tale nel dimensionare le attese.
7 · Magic number e coesistenza
Ogni sistema marca i propri ordini con un magic univoco e ignora religiosamente tutto il resto. Sembra banale; la metà dei disastri "l'EA mi ha chiuso i trade manuali" nasce qui. Su un conto misto, i magic number sono anche ciò che rende possibile l'analisi separata delle componenti a posteriori.
Lo scheletro che riuso
La struttura di ogni mio EA, ridotta all'osso — ogni riga ha dietro una cicatrice:
int OnInit()
{
if(!ValidateInputs()) return INIT_PARAMETERS_INCORRECT; // niente default silenziosi
if(!ReconcileState()) return INIT_FAILED; // riparto con posizioni aperte?
g_dayStartEquity = AccountInfoDouble(ACCOUNT_EQUITY);
g_peakEquity = MathMax(g_peakEquity, g_dayStartEquity); // il picco sopravvive ai riavvii
return INIT_SUCCEEDED;
}
void OnTick()
{
if(!CheckRiskGuardian()) return; // il guardiano comanda su tutto
if(!IsNewBar()) return; // logica su barra chiusa
if(!FiltersPass()) return; // veti: direzione, ore, giorno, spread, cooldown
Signal s = DetectSignal(); // solo geometria, nessun ordine
if(s.valid) Execute(s); // sizing + invio con gestione errori
}
void OnTradeTransaction(const MqlTradeTransaction &t,
const MqlTradeRequest &req,
const MqlTradeResult &res)
{
if(!IsMine(t)) return; // filtro magic: il resto del conto non esiste
UpdateStateIdempotent(t); // due volte lo stesso evento = zero danni
}
E il guardiano — il quinto modulo, trasversale e non negoziabile — con il dettaglio che fa la differenza tra un limite e un suggerimento:
// Circuit breaker con latch: una volta scattato, resta scattato.
bool CheckRiskGuardian()
{
if(g_latched) return false; // già fermo: resta fermo
double eq = AccountInfoDouble(ACCOUNT_EQUITY); // EQUITY, mai balance
if(InpUseDailyLossLimit && eq <= g_dayStartEquity*(1.0-InpDailyLossPct/100.0))
{ g_latched=true; CloseAllPositions(); return false; }
if(InpUseMaxDDLimit && eq <= g_peakEquity*(1.0-InpMaxDDPct/100.0))
{ g_latched=true; CloseAllPositions(); return false; }
if(eq > g_peakEquity) g_peakEquity = eq;
return true;
}
Tre dettagli da produzione, imparati a caro prezzo: il latch (g_latched) è la differenza tra un limite e un suggerimento; il controllo va fatto sull'equity, non sul balance (il rischio vivo è nelle posizioni aperte); e il guardiano chiude tutto, non blocca solo i nuovi ingressi — un limite di drawdown che lascia correre le posizioni aperte non è un limite.
Come uso l'AI per scrivere e revisionare questo codice senza farmi male: il metodo completo con i prompt.