MQL5 fatto bene: gli errori che costano soldi veri

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.

Il principio dietro tutto: il codice di un EA maturo è la trascrizione della sua dottrina di rischio. Se nel codice non ritrovate expectancy, costi, drawdown e sizing come moduli espliciti, non avete un sistema di trading: avete un generatore di ordini con l'ottimismo hard-coded.

Come uso l'AI per scrivere e revisionare questo codice senza farmi male: il metodo completo con i prompt.