Il Modello di Trading MQL5: Struttura Fondamentale
Prima di scrivere una riga di codice sugli ordini, devi capire che MQL5 usa un modello completamente diverso da MQL4. In MQL4, tutto era un “ordine” — posizioni aperte e ordini pendenti convivevano nello stesso pool. In MQL5 esistono tre livelli distinti: Orders (ordini pendenti non ancora eseguiti), Positions (posizioni aperte sul mercato), e History (ordini e posizioni chiusi). Confondere questi livelli genera bug silenziosi difficilissimi da diagnosticare.
Per gestire ordini in MQL5 usi sempre due strutture: MqlTradeRequest (i parametri della richiesta) e MqlTradeResult (il risultato dell’esecuzione). La funzione OrderSend(request, result) ritorna true/false, ma il risultato vero si legge da result.retcode. Un EA che controlla solo il valore di ritorno booleano di OrderSend sta ignorando il 90% delle informazioni disponibili.
Le Cinque Azioni di Trading
MQL5 prevede cinque tipi di azione nel campo request.action:
| TRADE_ACTION | Descrizione | Uso Oracle |
| TRADE_ACTION_DEAL | Ordine di mercato immediato | Entry principale XAUUSD |
| TRADE_ACTION_PENDING | Ordine pendente (limit/stop) | Setup multi-livello |
| TRADE_ACTION_SLTP | Modifica SL/TP su posizione esistente | Breakeven + Trailing ATR |
| TRADE_ACTION_MODIFY | Modifica ordine pendente | Aggiornamento livelli |
| TRADE_ACTION_CLOSE_BY | Chiusura posizione con opposta (hedging) | Hedge M25 |
OrderSend Completo: Codice Oracle per XAUUSD H4
La funzione di invio ordine di Oracle prevede calcolo del lot size basato su rischio percentuale, normalizzazione con il volume step del broker, verifica margine disponibile, e un ciclo retry per gestire requote e congestioni temporanee del server. Ecco il codice completo con commenti Oracle.
bool Oracle_SendMarketOrder(
string symbol,
int direction,
double sl_price,
double tp_price,
double risk_pct,
string comment
) {
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double risk_eur = balance * risk_pct;
double entry = (direction == ORDER_TYPE_BUY)
? SymbolInfoDouble(symbol, SYMBOL_ASK)
: SymbolInfoDouble(symbol, SYMBOL_BID);
double sl_dist = MathAbs(entry - sl_price);
double tick_val = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
double tick_sz = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
double sl_ticks = sl_dist / tick_sz;
double lot_raw = risk_eur / (sl_ticks * tick_val);
double vol_step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
double vol_min = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
double vol_max = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
double lot_norm = MathFloor(lot_raw / vol_step) * vol_step;
lot_norm = MathMax(vol_min, MathMin(vol_max, lot_norm));
lot_norm = NormalizeDouble(lot_norm, 2);
double margin_req;
if(!OrderCalcMargin(
(ENUM_ORDER_TYPE)direction, symbol, lot_norm,
entry, margin_req)) {
PrintFormat("[Oracle] ERRORE OrderCalcMargin: %d", GetLastError());
return false;
}
if(margin_req > AccountInfoDouble(ACCOUNT_FREEMARGIN) * 0.9) {
PrintFormat("[Oracle] SKIP — margine insufficiente. Req:%.2f Free:%.2f",
margin_req, AccountInfoDouble(ACCOUNT_FREEMARGIN));
return false;
}
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_DEAL;
req.symbol = symbol;
req.volume = lot_norm;
req.type = (ENUM_ORDER_TYPE)direction;
req.price = entry;
req.sl = sl_price;
req.tp = tp_price;
req.deviation = 30;
req.magic = 77000;
req.comment = comment;
req.type_filling = ORDER_FILLING_IOC;
for(int attempt = 0; attempt < 3; attempt++) {
if(OrderSend(req, res)) {
PrintFormat("[Oracle] ORDINE OK — Ticket:%d Lot:%.2f Entry:%.5f SL:%.5f TP:%.5f",
res.order, lot_norm, entry, sl_price, tp_price);
return true;
}
if(res.retcode == TRADE_RETCODE_REQUOTE) {
PrintFormat("[Oracle] Requote tentativo %d/3 — attesa 200ms", attempt+1);
req.price = (direction == ORDER_TYPE_BUY)
? SymbolInfoDouble(symbol, SYMBOL_ASK)
: SymbolInfoDouble(symbol, SYMBOL_BID);
Sleep(200);
continue;
}
if(res.retcode == TRADE_RETCODE_NO_MONEY) {
Print("[Oracle] ERRORE: Margine insufficiente (retcode 10019)");
return false;
}
if(res.retcode == TRADE_RETCODE_INVALID_STOPS) {
PrintFormat("[Oracle] ERRORE: Stop non validi. SL=%.5f TP=%.5f Entry=%.5f",
sl_price, tp_price, entry);
return false;
}
PrintFormat("[Oracle] ERRORE OrderSend retcode:%d — %s",
res.retcode, TradeRetcodeDescription(res.retcode));
return false;
}
Print("[Oracle] SKIP dopo 3 requote — setup perso");
return false;
}
I Codici Errore MQL5 che Devi Conoscere
In MQL5 il codice errore corretto si legge sempre da result.retcode, non da GetLastError(). I due sistemi sono distinti e confonderli causa diagnosi errate. Oracle UlcerZenith categorizza gli errori in tre classi: recuperabili con retry, recuperabili con attesa, non recuperabili (skip del trade).
| Retcode | Nome | Causa | Risposta Oracle |
| 10004 | REQUOTE | Prezzo cambiato nel ms | Retry ×3 con nuovo prezzo |
| 10006 | REQUEST_REJECTED | Server occupato | Sleep(500) + retry |
| 10014 | INVALID_VOLUME | Lot non normalizzato | Fix calcolo, no retry |
| 10016 | INVALID_STOPS | SL/TP troppo vicini | Log + skip trade |
| 10019 | NO_MONEY | Margine insufficiente | Alert + skip |
| 10026 | MARKET_CLOSED | Mercato chiuso | Attesa riapertura |
| 10027 | TRADE_DISABLED | Trading disabilitato | Alert + stop EA |
| 10030 | ORDER_LOCKED | Ordine in elaborazione | Sleep(200) + retry |
Errore Comune 01
Usare GetLastError() invece di result.retcode
In MQL5, GetLastError() ritorna errori del runtime MQL, non del server di trading. Dopo un OrderSend fallito, GetLastError() potrebbe ritornare 0 (nessun errore) mentre result.retcode mostra 10019 (no money). Questo bug causa EA che non loggano mai gli errori reali e continuano a tentare operazioni impossibili. Regola Oracle: leggere sempre result.retcode come fonte primaria di diagnosi.
Errore Comune 02
Inviare SL/TP con ordine di mercato senza verificare distanza minima
Molti broker ECN non accettano SL/TP inclusi nell’ordine di mercato se sono troppo vicini al prezzo corrente. Il risultato è retcode 10016 INVALID_STOPS anche se la logica è corretta. Oracle separa l’invio dell’ordine di mercato dalla modifica SL/TP: prima apre la posizione (senza SL/TP), poi usa TRADE_ACTION_SLTP per settare gli stop. Due chiamate, zero reiezioni.
Errore Comune 03
Non verificare PositionSelect() prima di ogni operazione sulla posizione
Se un ordine viene chiuso manualmente o dal TP/SL mentre l’EA è in elaborazione, PositionGetDouble(POSITION_SL) ritorna il valore della posizione precedentemente selezionata nel cache — non 0 o errore. Il risultato è una modifica di SL che viene ignorata senza errore. Regola Oracle: chiamare PositionSelect(Symbol()) prima di ogni PositionGet* — se ritorna false, la posizione non esiste, uscire immediatamente.
Modifica Posizione: Breakeven e Trailing ATR
Oracle UlcerZenith implementa due livelli di gestione della posizione dopo l’apertura: il breakeven (SL portato al prezzo di apertura + buffer) quando il trade raggiunge +1R, e il trailing ATR×1.5 che segue il prezzo proteggendo i profitti. Entrambe le operazioni usano TRADE_ACTION_SLTP, non TRADE_ACTION_DEAL.
bool Oracle_SetBreakeven(string symbol, double be_buffer_pts) {
if(!PositionSelect(symbol)) return false;
double open_price = PositionGetDouble(POSITION_PRICE_OPEN);
double cur_sl = PositionGetDouble(POSITION_SL);
long pos_type = PositionGetInteger(POSITION_TYPE);
double tick_sz = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
double buffer = be_buffer_pts * tick_sz;
double new_sl;
if(pos_type == POSITION_TYPE_BUY) {
new_sl = open_price + buffer;
if(cur_sl >= new_sl) return false;
} else {
new_sl = open_price - buffer;
if(cur_sl <= new_sl && cur_sl != 0) return false;
}
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_SLTP;
req.symbol = symbol;
req.sl = NormalizeDouble(new_sl, (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS));
req.tp = PositionGetDouble(POSITION_TP);
req.position = PositionGetInteger(POSITION_TICKET);
if(OrderSend(req, res) && res.retcode == TRADE_RETCODE_DONE) {
PrintFormat("[Oracle] BREAKEVEN impostato: SL=%.5f (era %.5f)", new_sl, cur_sl);
return true;
}
PrintFormat("[Oracle] ERRORE BE retcode:%d", res.retcode);
return false;
}
void Oracle_TrailingATR(string symbol, int atr_period, double atr_mult) {
if(!PositionSelect(symbol)) return;
long pos_type = PositionGetInteger(POSITION_TYPE);
double cur_sl = PositionGetDouble(POSITION_SL);
double cur_tp = PositionGetDouble(POSITION_TP);
long ticket = PositionGetInteger(POSITION_TICKET);
double atr_val = iATR(symbol, PERIOD_H4, atr_period, 1);
double trail = atr_val * atr_mult;
double cur_bid = SymbolInfoDouble(symbol, SYMBOL_BID);
double cur_ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
double new_sl, min_dist;
int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
if(pos_type == POSITION_TYPE_BUY) {
new_sl = NormalizeDouble(cur_bid - trail, digits);
if(new_sl <= cur_sl) return;
} else {
new_sl = NormalizeDouble(cur_ask + trail, digits);
if(new_sl >= cur_sl && cur_sl != 0) return;
}
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_SLTP;
req.symbol = symbol;
req.sl = new_sl;
req.tp = cur_tp;
req.position = ticket;
if(OrderSend(req, res))
PrintFormat("[Oracle] TRAILING ATR: SL %.5f → %.5f", cur_sl, new_sl);
}
Chiusura Posizione: Oracle_ClosePosition()
In MQL5 non esiste una funzione OrderClose() come in MQL4. Per chiudere una posizione aperta si usa OrderSend() con TRADE_ACTION_DEAL e un ordine opposto pari al volume della posizione. Il campo critico è request.position: deve contenere il ticket della posizione da chiudere, altrimenti apri una nuova posizione invece di chiudere quella esistente.
bool Oracle_ClosePosition(string symbol, string reason) {
if(!PositionSelect(symbol)) {
PrintFormat("[Oracle] ClosePosition: nessuna posizione su %s", symbol);
return false;
}
double vol = PositionGetDouble(POSITION_VOLUME);
long p_type = PositionGetInteger(POSITION_TYPE);
long ticket = PositionGetInteger(POSITION_TICKET);
ENUM_ORDER_TYPE close_type = (p_type == POSITION_TYPE_BUY)
? ORDER_TYPE_SELL
: ORDER_TYPE_BUY;
double close_price = (close_type == ORDER_TYPE_SELL)
? SymbolInfoDouble(symbol, SYMBOL_BID)
: SymbolInfoDouble(symbol, SYMBOL_ASK);
MqlTradeRequest req = {};
MqlTradeResult res = {};
req.action = TRADE_ACTION_DEAL;
req.symbol = symbol;
req.volume = vol;
req.type = close_type;
req.price = close_price;
req.deviation = 30;
req.position = ticket;
req.magic = 77000;
req.comment = reason;
req.type_filling = ORDER_FILLING_IOC;
for(int i = 0; i < 3; i++) {
if(OrderSend(req, res) && res.retcode == TRADE_RETCODE_DONE) {
PrintFormat("[Oracle] CHIUSURA OK — Ticket:%d Motivo:%s", ticket, reason);
return true;
}
if(res.retcode == TRADE_RETCODE_REQUOTE) {
req.price = (close_type == ORDER_TYPE_SELL)
? SymbolInfoDouble(symbol, SYMBOL_BID)
: SymbolInfoDouble(symbol, SYMBOL_ASK);
Sleep(200);
continue;
}
PrintFormat("[Oracle] ERRORE chiusura retcode:%d", res.retcode);
break;
}
return false;
}
◆ Oracle UlcerZenith v78 — Dati Verificati
Architettura di Esecuzione su 65.587 Trade Reali
Ogni trade Oracle esegue questa sequenza: 1) Lettura GlobalVariable UZ_XAUUSD_16384_SCORE (Oracle score) e UZ_XAUUSD_16384_DIR (direzione) scritte da UlcerZenith LITE ogni candela H4. 2) Se score ≥ 65 e nessuna posizione aperta su XAUUSD, calcolo SL con ATR×1.5 e TP con SL×3 (RR 1:3). 3) Oracle_SendMarketOrder() con risk 1%. 4) Check ogni tick: se profitto ≥ 1R, chiama Oracle_SetBreakeven() una sola volta. 5) Oracle_TrailingATR() attivo da quel momento.
La robustezza dell’esecuzione (retry, normalizzazione lot, verifica margine) contribuisce all’eliminazione degli errori di esecuzione. Su 19 anni di forward test: 0 margin call, 0 ordini duplicati, 0 posizioni aperte senza SL.
Implementazione Oracle Completa: OnTick() in Produzione
L’integrazione di tutte le funzioni nell’EA Oracle prevede un flusso preciso in OnTick(). La sequenza è: lettura GlobalVariables da UlcerZenith → verifica condizioni entry → apertura se nessuna posizione → gestione posizione esistente (BE + trailing) → check segnale exit anticipata. Ogni blocco deve essere idempotente: chiamato mille volte deve produrre lo stesso risultato di una chiamata sola.
void OnTick() {
string sym = Symbol();
double score = GlobalVariableGet("UZ_" + sym + "_" +
(string)Period() + "_SCORE");
double dir = GlobalVariableGet("UZ_" + sym + "_" +
(string)Period() + "_DIR");
double gv_sl = GlobalVariableGet("UZ_" + sym + "_" +
(string)Period() + "_SL");
double gv_tp = GlobalVariableGet("UZ_" + sym + "_" +
(string)Period() + "_TP");
if(PositionSelect(sym)) {
double open_p = PositionGetDouble(POSITION_PRICE_OPEN);
double cur_sl = PositionGetDouble(POSITION_SL);
double sl_dist = MathAbs(open_p - cur_sl);
long p_type = PositionGetInteger(POSITION_TYPE);
double cur_bid = SymbolInfoDouble(sym, SYMBOL_BID);
bool profit_1r = (p_type == POSITION_TYPE_BUY)
? (cur_bid - open_p) >= sl_dist
: (open_p - cur_bid) >= sl_dist;
if(profit_1r && !g_BESet) {
if(Oracle_SetBreakeven(sym, 10)) g_BESet = true;
}
if(g_BESet) Oracle_TrailingATR(sym, 14, 1.5);
if(dir != 0 &&
((p_type == POSITION_TYPE_BUY && dir < 0) ||
(p_type == POSITION_TYPE_SELL && dir > 0))) {
Oracle_ClosePosition(sym, "Oracle_ExitSignal_ScoreInv");
g_BESet = false;
}
return;
}
static datetime last_bar = 0;
datetime cur_bar = (datetime)SeriesInfoInteger(sym, PERIOD_H4,
SERIES_LASTBAR_DATE);
if(cur_bar == last_bar) return;
last_bar = cur_bar;
if(score < 65 || gv_sl == 0 || gv_tp == 0) return;
int order_dir = (dir > 0) ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
if(Oracle_SendMarketOrder(sym, order_dir, gv_sl, gv_tp,
0.01, "Oracle_H4_SCORE" + (string)(int)score)) {
g_BESet = false;
}
}
Best Practice per EA in Produzione su VPS
Il codice corretto è condizione necessaria ma non sufficiente. Un EA che funziona perfettamente in tester può collassare in produzione per cause ambientali: disconnessioni, restart del terminale, cambio di ora del broker. Oracle UlcerZenith è progettato per sopravvivere a qualsiasi interruzione.
Best Practice 01
OnInit() deve ricostruire lo stato senza assumere sessione continua
All’avvio, controllare se esiste già una posizione aperta su Symbol(). Se sì, recuperare il ticket, il prezzo di apertura, e impostare g_BESet correttamente in base al profitto attuale. Non assumere che l’EA stia partendo da zero. Un restart del VPS alle 03:00 deve trovare la posizione aperta e continuare la gestione seamlessly.
Best Practice 02
Magic number univoco per EA e simbolo
Se hai più istanze di Zenith Executor su simboli diversi, usa magic number distinti: 77001 per EURUSD, 77002 per XAUUSD, 77003 per USDJPY. Il filtro PositionGetInteger(POSITION_MAGIC) == magic in ogni funzione di gestione garantisce che ogni EA gestisca solo le sue posizioni.
Best Practice 03
Loggare ogni operazione con timestamp e motivo
Ogni chiamata OrderSend deve loggare: timestamp, simbolo, tipo, volume, prezzo, SL, TP, retcode, commento. Questo log è l’unica fonte di diagnosi dopo un problema in produzione. Su Oracle: ogni log include il score Oracle al momento dell’entry, il valore ATR, e il motivo della decisione. Senza questo, diagnosticare un bug su 500 trade è impossibile.
Best Practice 04
Evitare operazioni di rete in OnTick()
OnTick() viene chiamato migliaia di volte al giorno su XAUUSD. Operazioni lente (file I/O, GlobalVariableSet inutili, Print() excessivo) degradano le prestazioni. Oracle usa un flag g_NewBar per eseguire il codice Oracle-heavy solo all’apertura di ogni candela H4, riducendo l’overhead del 99%.
Best Practice 05
Usare ORDER_FILLING_IOC su broker ECN
Molti broker ECN non supportano ORDER_FILLING_FOK (Fill or Kill) per ordini di mercato su forex/oro. Il retcode 10030 (ORDER_FILLING_NOT_ALLOWED) sparisce quasi sempre settando req.type_filling = ORDER_FILLING_IOC. Verificare il tipo di filling supportato con SymbolInfoInteger(SYMBOL, SYMBOL_FILLING_MODE) nell’OnInit().
Confronto: EA Amatoriale vs EA Oracle
| Aspetto | EA Amatoriale Tipico | Oracle UlcerZenith v78 |
| Gestione requote | Nessuna — ordine fallisce silenziosamente | Retry ×3 con prezzo aggiornato |
| Lot size | Fisso o non normalizzato | Risk% + normalizzazione vol_step |
| SL/TP su entry | Inviati con ordine (10016 su ECN) | Separati con TRADE_ACTION_SLTP |
| Restart VPS | Perde stato, apre posizione doppia | OnInit() ricostruisce stato |
| Log errori | GetLastError() fuori contesto | result.retcode con descrizione |
| Breakeven | Settato infinite volte (spam server) | Flag g_BESet: una sola volta |
| Verifica margine | OrderSend() fallisce con 10019 | OrderCalcMargin() preventivo |
| Multi-simbolo | Magic 0: gestisce tutte le posizioni | Magic univoco per simbolo |
Un EA con logica mediocre ma esecuzione robusta batte un EA con logica brillante ma gestione fragile. Ho visto sistemi con WR 55% collassare in produzione per un lot size non normalizzato che generava errore 10014 su ogni entry.
— Manuel Trotta, 19 anni di sviluppo Oracle su MQL4/MQL5
FAQ — Gestione Ordini MQL5
Qual è la differenza tra OrderSend in MQL4 e MQL5?►
In MQL4, OrderSend() gestisce sia ordini di mercato che pendenti con un’unica funzione. In MQL5 il modello di trading è completamente diverso: si usa una struttura MqlTradeRequest per definire i parametri e MqlTradeResult per ricevere il risultato. La funzione OrderSend() ritorna true/false, ma il risultato reale si legge da result.retcode. Le posizioni sono gestite con PositionSelect/PositionGetDouble, non con OrderSelect/OrderGetDouble. Questo rende MQL5 più robusto ma richiede più codice boilerplate.
Come si gestisce il requote in MQL5?►
Il requote (TRADE_RETCODE_REQUOTE = 10004) si gestisce con un ciclo retry. Il pattern standard Oracle prevede massimo 3 tentativi con Sleep(200) tra un tentativo e l’altro, aggiornando req.price con il prezzo corrente a ogni tentativo. Se il requote persiste dopo 3 tentativi, si logga e si skippa il trade — meglio nessun trade che un trade a prezzo deteriorato.
Come si calcola il lot size corretto per Oracle su XAUUSD H4?►
Formula Oracle: lot = (balance × risk%) ÷ (SL_ticks × tick_value). Con SL ATR×1.5 tipicamente 220–350 punti su XAUUSD H4, rischio 1% su €10.000 = €100. Se SL = 250 punti (0.01 tick size → 25.000 ticks), tick_value XAUUSD ≈ €0.01/0.01lot → lot = 100/(25000×0.01) = 0.40 lotti. Normalizzare sempre con MathFloor(raw/vol_step)*vol_step e verificare con OrderCalcMargin() prima di inviare.
Cosa fare quando OrderSend ritorna false in MQL5?►
Leggere result.retcode per il codice errore specifico. I più comuni: 10004 (requote → retry), 10014 (invalid volume → correggere lot size), 10016 (invalid stops → SL/TP troppo vicini), 10019 (no money → verificare margine), 10027 (trade disabled → alert). Non usare mai GetLastError() come sostituto — in MQL5 il retcode nella struttura result è il riferimento corretto.
Posso aprire più ordini contemporaneamente con Oracle?►
Oracle UlcerZenith LITE gestisce una posizione per strumento per volta. Se PositionSelect(Symbol()) ritorna true, la posizione esiste già e non si apre un nuovo ordine. L’architettura multi-istanza (più EA su simboli diversi) usa GlobalVariables separate per simbolo garantendo isolamento completo.
Come funziona il trailing stop ATR in MQL5?►
Il trailing stop ATR si implementa calcolando il valore ATR corrente con iATR(), moltiplicandolo per il fattore Oracle (1.5), e confrontando il nuovo SL con quello attuale. Si modifica solo se il nuovo SL è migliore di quello esistente. Su Oracle XAUUSD H4, il trailing ATR×1.5 si attiva solo dopo che il prezzo ha raggiunto 1R di profitto e il breakeven è già impostato.
Perché usare una VPS per EA MQL5 con Oracle?►
Una VPS garantisce esecuzione continua 24/5. Per Oracle H4 su XAUUSD, i segnali arrivano all’apertura della candela H4 — un PC spento o connessione caduta in quel momento fa perdere il setup. Latenza VPS Equinix NY4: 1–3ms vs MetaQuotes server. PC domestico: 30–150ms. Su 65.587 trade, la differenza in slippage tra VPS e PC domestico vale circa 0.8–1.2% di PF annuo.
Qual è il modo corretto di modificare SL in breakeven in MQL5?►
Usare TRADE_ACTION_SLTP con il ticket della posizione. Prima verificare con PositionSelect() che la posizione esiste, leggere il prezzo di apertura, calcolare il nuovo SL con buffer (10 punti su XAUUSD), poi inviare. Fondamentale: usare un flag g_BESet per evitare di inviare la stessa modifica mille volte per tick. Il breakeven si setta una volta sola.