MetaTrader 5: Costruire un mercato su misura per la propria strategia — grafici Renko, Range ed Equal-Volume, simboli sintetici e stress test con simboli personalizzati.
Introduzione
Il grafico standard di MetaTrader 5 è uno strumento affidabile, ma è strettamente legato ai timeframe, al feed delle quotazioni del broker e alla griglia del calendario. Nel trading algoritmico moderno, questo, spesso non è sufficiente: il cosiddetto rumore di mercato, i buchi nei dati storici e gli intervalli di tempo fissi distorcono le dinamiche reali. E se il terminale vi permettesse non solo di leggere il mercato, ma anche di progettarne la rappresentazione in modo da adattarla alle esigenze di una specifica strategia?
Con lo sviluppo dell'API per i simboli personalizzati, MetaTrader 5 ha modificato la sua architettura: il trader non è più solo un fruitore passivo di quotazioni, ma diventa un architetto dei dati. Un simbolo personalizzato non è un grafico offline per la visualizzazione, ma un oggetto terminale a tutti gli effetti, con la propria cronologia dei tick, la specifica del contratto e il supporto nativo nello Strategy Tester.
Ora è possibile generare grafici non basati sul tempo (barre Renko, barre Range, barre equal volume), assemblare strumenti e panieri sintetici ed eseguire stress test modificando arbitrariamente spread, livelli di stop e requisiti di margine direttamente nel codice.
In questo articolo, esploreremo gli aspetti pratici dell'utilizzo di strumenti personalizzati: la struttura di archiviazione dei dati, l'API MQL5 di base, gli algoritmi di aggregazione dei tick e la modifica dello storico. Vediamo come adattare gli EA standard per instradare gli ordini di trading da un simbolo personalizzato ad un simbolo reale di trading.
Fig. 1. Costruite il vostro mercato con strumenti standard
Che cos'è un simbolo personalizzato: Architettura, archiviazione e API
In MetaTrader 5, un simbolo personalizzato non rappresenta un grafico offline per l'osservazione visiva, come avveniva nelle precedenti versioni della piattaforma. Ora è un oggetto terminale a tutti gli effetti, con un proprio storico dei tick, specifica del contratto e supporto nativo nel tester di strategia.
A differenza degli strumenti standard forniti da un broker e dipendenti dalla qualità del suo feed di quotazioni (fornitore di quotazioni), un simbolo personalizzato consente al trader di diventare l'architetto dei dati. Sarete voi a decidere quali quotazioni, in quale forma e con quale livello di accuratezza saranno incluse nello storico. Questo apre la strada all'analisi non solo di mercati non gestiti dal broker (criptovalute, spread interbancari), ma anche alla trasformazione di dati esistenti (Renko, equal volume, indici sintetici).
I simboli personalizzati sono isolati dai server di trading del broker. I file vengono memorizzati localmente, ad esempio: “AppData\Roaming\MetaQuotes\Terminal\[InstanceID]\bases\Custom”.
Questa soluzione presenta un vantaggio importante: l'indipendenza. Potete cambiare broker, aggiornare il terminale, ma i vostri simboli personalizzati e lo storico accumulato rimarranno invariati. Nella finestra Market Watch, si trovano sempre in una cartella separata chiamata Custom, per distinguere visivamente gli strumenti reali dai propri.
Core dell'API MQL5
La gestione di simboli personalizzati nel codice richiede l'accesso a funzioni API specializzate. L'elenco completo è disponibile nella documentazione, ma per i casi d'uso principali ci concentreremo su tre gruppi di funzioni.
Gestione del ciclo di vita dei simboli:
- CustomSymbolCreate() — crea un simbolo personalizzato con un nome specificato in un gruppo specificato.
- CustomSymbolDelete() — rimuove un simbolo dell’utente con un nome specificato.
- SymbolSelect() — aggiunge al Market Watch (senza questo, il simbolo non sarà visibile nell'interfaccia grafica).
Gestione del download dei dati:
- CustomTicksAdd() — aggiunge dati dall'array di tipo MqlTick[] allo storico dei prezzi di un simbolo personalizzato. Nella finestra Market Watch è necessario selezionare un simbolo personalizzato.
- CustomTicksReplace() — sostituisce completamente lo storico dei prezzi di un simbolo personalizzato in un intervallo di tempo specificato con i dati dell'array di tipo MqlTick[].
- CustomRatesUpdate() — aggiunge le barre mancanti allo storico del simbolo personalizzato e/o sostituisce quelle esistenti con i dati dall'array di tipo MqlRates[].
Gestione delle proprietà:
- CustomSymbolSetInteger()/CustomSymbolSetDouble()/CustomSymbolSetString() — imposta le proprietà del contratto (spread, precisione, valuta del margine, ecc.).
La chiamata diretta alle funzioni API richiede la gestione di vari errori e il controllo dello stato. Per velocizzare lo sviluppo, utilizziamo la classe wrapper CiCustomSymbol. Incapsula la logica per la creazione, la clonazione delle proprietà dei simboli reali e il caricamento in batch dei tick. Il codice completo della classe CiCustomSymbol si trova nel file CiCustomSymbol.mqh allegato qui sotto.
Di seguito è riportato un esempio di inizializzazione di un simbolo personalizzato - creiamo un simbolo, cloniamo le proprietà dal simbolo EURUSD e controlliamo i codici di ritorno:
#include <CiCustomSymbol.mqh> //+------------------------------------------------------------------+ //| inputs | //+------------------------------------------------------------------+ input string CustomSymbolName = "EURUSD_Custom"; // new symbol name input string OriginSymbol = "EURUSD"; // source symbol //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { CiCustomSymbol symb; //--- attempt to create a symbol //--- return codes: -1 (error), 0 (already exists), 1 (successfully created) int res = symb.Create(CustomSymbolName, "", OriginSymbol, 1000000, true); if(res == -1) { Print("Error creating symbol: ", GetLastError()); return; } if(res == 0) { Print("Symbol ", CustomSymbolName, " already exists."); } else { Print("Symbol ", CustomSymbolName, " successfully created."); //--- set specific properties (for example, fixed spread of 20 points) //--- properties are changed BEFORE loading history if(!symb.SetProperty(SYMBOL_SPREAD, 20)) Print("Failed to set spread. Error: ", GetLastError()); if(!symb.SetProperty(SYMBOL_SPREAD_FLOAT, false)) Print("Failed to set spread type (fixed). Error: ", GetLastError()); } //--- symbol added to Market Watch ('true' parameter to Create) Print("Symbol created: ", CustomSymbolName); }
Il codice sorgente completo dello script è disponibile nel file CreateCustomSymbol.mq5 allegato di seguito.
Durante la gestione dei simboli personalizzati, si prega di tenere presente alcune limitazioni descritte nella documentazione:
Disabilitazione della MQL5 Cloud Network:
L'ottimizzazione delle strategie su simboli personalizzati tramite cloud network è vietata. Il motivo è semplice: i computer di differenti trader possono contenere simboli con gli stessi nomi (ad esempio, EURUSD_Custom), ma con storici completamente diversi. L'utilizzo della Cloud Network porterebbe a un caos di sincronizzazione e alla generazione di traffico eccessivo. I test possono essere eseguiti solo in locale o tramite agenti locali.
Logica di calcolo del margine:
Lo strategy tester utilizza tassi incrociati per calcolare il margine e il profitto. Se avete creato e state testando il simbolo AUDCAD.custom (basato su AUDUSD/USDCAD) e il vostro conto è in USD, il tester deve:
- Trovare AUDUSD (quanto vale AUD in USD) - per calcolare il margine
- Trovare USDCAD (come convertire CAD in USD) - per calcolare il profitto
È importante assicurarsi che tutte le coppie di valute richieste siano presenti nel Market Watch e che lo storico sia caricato, altrimenti il tester non sarà in grado di calcolare gli indicatori finanziari.
Le proprietà del simbolo non cambiano durante il test:
Le proprietà personalizzate del simbolo (ad esempio, SYMBOL_TRADE_STOPS_LEVEL è la distanza minima per gli stop) vengono impostate prima di avviare il test. Nella versione attuale del tester non è possibile modificarli dinamicamente mentre l'EA è in esecuzione (ad esempio, per ampliare il livello di stop prima delle notizie). Il tester fissa i parametri del simbolo durante l'inizializzazione.
Digressione analitica: Il significato del backtesting
Perché è necessaria un'architettura così complessa? L'utilizzo di simboli personalizzati modifica l'approccio ai test delle idee di trading. Anziché adattare la vostra strategia ai dati potenzialmente imperfetti di un broker specifico (con buchi nello storico o spread anomali), potete creare il vostro ambiente di laboratorio ideale.
È possibile verificare il comportamento della strategia con uno spread di 50 pip (per escludere lo scalping) o come fa trading su grafici in cui il tempo non è un fattore determinante (Range/Renko). Questo aiuta a isolare la logica centrale di una strategia dalle distorsioni dovute al rumore di mercato.
Grafici indipendenti dal tempo: Renko, Range Bars, e Equal-Volume
I timeframe standard sono comodi, ma impongono al mercato una griglia temporale artificiale. Ad esempio, la candela M1 si chiude ogni 60 secondi, indipendentemente dal numero di tick. In condizioni di bassa liquidità, una barra di questo tipo trasmette poche informazioni, e nei momenti di elevata volatilità, nasconde movimenti importanti in un singolo rettangolo.
I grafici non basati sul tempo risolvono questo problema formando una nuova barra non dopo che è trascorso del tempo, ma quando il prezzo o il volume raggiungono una soglia specificata. Ciò consente di:
- Eliminare il rumore del mercato e concentrarsi sui movimenti significativi;
- Adattare automaticamente la fase di analisi alla volatilità attuale;
- Identificare le zone di accumulazione/distribuzione in base al volume reale (o tick), non in base a un timer.
Analizziamo tre tipologie comuni di questi grafici — Renko, Range Bars e Equal-Volume, e mostriamo come generarli in MetaTrader 5 utilizzando simboli personalizzati. Per cominciare, confrontiamo e descriviamo questi tre tipi di barre in una tabella:
| Tipo di grafico | Nuovi criteri di formazione di una nuova barra | Migliori casi d'uso |
|---|---|---|
| Renko | Variazione di prezzo di un dato numero di punti | Strategie di tendenza, filtraggio dei movimenti laterali |
| Range Bars | L’escursione High-Low della barra raggiunge il valore specificato | Scalping e trading intraday in condizioni di volatilità |
| Equal-Volume | Accumulo di un numero specificato di tick o di volume reale | Analisi della liquidità, ricerca di aree di interesse dei principali operatori |
Nota:
Tutti i simboli personalizzati in MetaTrader 5 sono collegati alla griglia del timeframe M1. Si tratta di una limitazione architetturale della piattaforma: anche se una barra si forma in 2 secondi, riceverà comunque un timestamp nella cronologia con una precisione fino a un minuto. In presenza di elevata volatilità, diverse barre possono ricadere nello stesso minuto (avere la stessa ora) - l'EA deve tenerne conto.
Implementazione tecnica: aggregazione dei tick in barre
In generale, l'algoritmo per generare barre non basate sul tempo è lo stesso per tutti e tre i tipi:
- Lettura dei tick dallo storico o ricezione online tramite le funzioni CopyTicksRange()/OnTick(), rispettivamente;
- Accumulo di dati nel buffer: tracciamento di Apertura, Massimo, Minimo, Chiusura, Volume;
- Verifica delle condizioni per la formazione di una nuova barra (punti/intervallo/volume);
- Salvataggio di una barra nel database dei simboli personalizzati tramite CustomRatesUpdate();
- Emulazione dei tick tramite CustomTicksAdd() per attivare indicatori ed Expert Advisor sui grafici di MetaTrader 5.
Classe base per lavorare con simboli personalizzati: algoritmo per tutti i tipi di barre
//+------------------------------------------------------------------+ //| class-aggregator of ticks into custom bars | //+------------------------------------------------------------------+ class CBarAggregator { private: string symbol_name; double threshold; int bar_type; ENUM_VOLUME_MODE volume_mode; MqlRates rates_buffer[]; int buffer_limit; int buffer_idx; MqlRates current_bar; double last_close; int trend; bool is_initialized; datetime last_bar_time; int symbol_digits; public: CBarAggregator(void); bool Init(const string _symbol_name, double _threshold, int _bar_type, ENUM_VOLUME_MODE _vol_mode = VOLUME_MODE_TICK); bool ProcessTick(const MqlTick &tick); void FlushBuffer(void); void Reset(void); private: void CreateBar(const MqlTick &tick, double open_price = 0.0); void CloseAndSaveBar(const MqlTick &tick, double forced_close = 0.0); void WriteBatch(void); }; //+------------------------------------------------------------------+ //| handle an incoming tick | //+------------------------------------------------------------------+ bool CBarAggregator::ProcessTick(const MqlTick &tick) { if(tick.bid <= 0 || tick.ask <= 0) return false; if(tick.time <= 0) return false; // protection from invalid ticks if(!is_initialized) { CreateBar(tick); last_close = tick.bid; last_bar_time = tick.time; // initialize with the first tick time is_initialized = true; return false; } bool bar_closed = false; if(bar_type == 0) // renko { double diff = tick.bid - last_close; double size = threshold * _Point; int req_move = (trend != 0) ? 2 : 1; if(diff >= size * req_move) { int bricks = (int)(diff / size); for(int i = 0; i < bricks; i++) { double target_close = NormalizeDouble(last_close + size, symbol_digits); CloseAndSaveBar(tick, target_close); last_close = target_close; CreateBar(tick, last_close); trend = 1; } bar_closed = true; } else if(diff <= -size * req_move) { int bricks = (int)(MathAbs(diff) / size); for(int i = 0; i < bricks; i++) { double target_close = NormalizeDouble(last_close - size, symbol_digits); CloseAndSaveBar(tick, target_close); last_close = target_close; CreateBar(tick, last_close); trend = -1; } bar_closed = true; } } else if(bar_type == 1) // range bars { double bid = NormalizeDouble(tick.bid, symbol_digits); if(bid > current_bar.high) current_bar.high = bid; if(bid < current_bar.low) current_bar.low = bid; if((current_bar.high - current_bar.low) >= threshold * _Point) { CloseAndSaveBar(tick); CreateBar(tick); bar_closed = true; } } else if(bar_type == 2) // equal volume bars { if(volume_mode == VOLUME_MODE_REAL) current_bar.real_volume += (long)tick.volume_real; else current_bar.tick_volume++; long current_vol = (volume_mode == VOLUME_MODE_REAL) ? current_bar.real_volume : current_bar.tick_volume; if(current_vol >= (long)threshold) { CloseAndSaveBar(tick); CreateBar(tick); bar_closed = true; } } return bar_closed; } //+------------------------------------------------------------------+ //| close the bar and save to the buffer | //+------------------------------------------------------------------+ void CBarAggregator::CloseAndSaveBar(const MqlTick &tick, double forced_close) { current_bar.close = (forced_close != 0.0) ? NormalizeDouble(forced_close, symbol_digits) : NormalizeDouble(tick.bid, symbol_digits); current_bar.high = NormalizeDouble(MathMax(current_bar.high, MathMax(current_bar.open, current_bar.close)), symbol_digits); current_bar.low = NormalizeDouble(MathMin(current_bar.low, MathMin(current_bar.open, current_bar.close)), symbol_digits); current_bar.spread = MathMax(0, current_bar.spread); // make sure last_bar_time > 0 (initialized) if(last_bar_time > 0 && current_bar.time <= last_bar_time) { // shift by 1 second instead of 60 to minimize distortion current_bar.time = last_bar_time + 1; } last_bar_time = current_bar.time; // final validation before the buffer if(current_bar.high < current_bar.low || current_bar.high < current_bar.open || current_bar.high < current_bar.close || current_bar.low > current_bar.open || current_bar.low > current_bar.close) { Print("🟡 Invalid bar skipped: O=", current_bar.open, " H=", current_bar.high, " L=", current_bar.low, " C=", current_bar.close, " Time=", TimeToString(current_bar.time)); return; } if(buffer_idx < buffer_limit) { rates_buffer[buffer_idx] = current_bar; buffer_idx++; } if(buffer_idx >= buffer_limit) WriteBatch(); } //+------------------------------------------------------------------+ //| save the buffer to the symbol history | //+------------------------------------------------------------------+ void CBarAggregator::WriteBatch(void) { if(buffer_idx > 0) { datetime t_from = rates_buffer[0].time; datetime t_to = rates_buffer[buffer_idx - 1].time; if(CustomRatesUpdate(symbol_name, rates_buffer) < 1) Print("CustomRatesUpdate error: ", GetLastError()); buffer_idx = 0; } } //+------------------------------------------------------------------+
Il codice completo della classe CBarAggregator si trova nel file CBarAggregator.mqh.
Indicatore universale per barre non basate sul tempo
Creare un indicatore universale per l'applicazione pratica della classe CBarAggregator. Consente di passare dalla modalità Renko, Range e Equal Volume Bars negli input e generare un simbolo personalizzato in un dato intervallo di storico.
Input:
input ENUM_CUSTOM_CHART_MODE InputMode = CMT_RENKO; // generation mode input double InputBoxSize = 10; // bar size (points) for renko/range input long InputVolLimit = 1000; // volume limit for equal-volume input string InputSuffix = ""; // symbol name suffix input datetime InputStartTime = 0; // history start (0 - last 7 days)
Enumerazione della modalità di generazione delle barre:
enum ENUM_CUSTOM_CHART_MODE { CMT_RENKO = 0, // renko CMT_RANGE = 1, // range bars CMT_EQVOL_TICK = 2, // equal tick volumes CMT_EQVOL_REAL = 3 // equal real volumes };
Funzionamento in tempo reale — dopo aver caricato lo storico, l'indicatore continua a funzionare online, elaborando i nuovi tick:
//+------------------------------------------------------------------+ //| handle a new tick | //+------------------------------------------------------------------+ void ProcessNewTicks() { if(last_tick_time_msc==0) return; MqlTick ticks[]; int copied=CopyTicksRange(_Symbol,ticks,COPY_TICKS_ALL,last_tick_time_msc,0); if(copied>0) { for(int i=0;i<copied;i++) { aggregator.ProcessTick(ticks[i]); last_tick_time_msc=ticks[i].time_msc; } aggregator.FlushBuffer(); } }
Questo indicatore non disegna grafici autonomamente. Il suo scopo è quello di creare e popolare un simbolo personalizzato con i dati. Per iniziare, trascinate l'indicatore su un qualsiasi grafico (ad esempio, EURUSD) - si aprirà la finestra delle proprietà. Selezionate una modalità (ad esempio MODE_RENKO), le dimensioni del box della barra (InputBoxSize) e l'inizio dello storico.
Sequenza di applicazione dell'indicatore:
- L'indicatore inizierà a generare barre (i messaggi relativi all'inizio e al completamento della generazione appariranno nei log),
- Una volta visualizzato il messaggio relativo al completamento della generazione, aprite il Market Watch (Ctrl+M),
- Trovate e selezionate lo strumento creato nell'elenco dei simboli (ad esempio, EURUSD_Renko_10),
- Aprite un nuovo grafico (Ctrl+N),
- Impostate il timeframe su M1,
I risultati del funzionamento dell'indicatore nelle diverse modalità sono presentati nelle figure da 2 a 4.
Fig. 2. Barre Renko
Fig. 3. Range bars

Fig. 4. Barre Equal Volume
Analizziamo brevemente le caratteristiche e gli esempi di utilizzo delle barre non basate sul tempo.
Renko — una nuova barra viene visualizzata solo quando il prezzo supera la dimensione specificata della barra, espressa in punti, rispetto al prezzo di chiusura della barra precedente. La direzione della barra (rialzista/ribassista) è determinata dalla direzione della rottura. Le ombre non vengono visualizzate di default, ma possono essere utilizzate se è necessario analizzare la volatilità intra-barra. Con un movimento deciso, si possono formare diverse barre in un minuto. Il prezzo di chiusura della barra è il prezzo di breakout, non il prezzo di chiusura del minuto.
Esempio di strategia basata su Renko:
- Ingresso - intersezione della MA veloce(9) e della MA lenta(21) alla chiusura della barra,
- Filtro - se il valore ATR(14) è maggiore della soglia specificata, filtra le false rotture nel movimento laterale,
- Uscita - incrocio inverso delle medie mobili o stop fisso in punti,
- La particolarità è che le operazioni di trading vengono effettuate al prezzo PRICE_CLOSE, poiché la barra Renko con indice 0 (l'ultima) è sempre incompleta.
Range bars — si apre una nuova barra quando l'oscillazione tra il valore massimo e quello minimo raggiunge il valore specificato in punti. A differenza del Renko, prende in considerazione tutti i movimenti all'interno della barra, non solo la chiusura. Invece di un valore di dimensione fisso, possiamo utilizzare una dimensione della barra dinamica (Range) basata sull'ATR.
Che cosa si intende per dimensione del Range? La dimensione del Range definisce il valore di soglia della variazione di prezzo, in corrispondenza del quale inizia a formarsi una nuova barra. Una Range pari a 1 corrisponde alla minima variazione di prezzo. Questo valore può essere rappresentato dalla seguente equazione: 1 Range = Tick Size.
Vantaggi dell'utilizzo nello scalping:
- Durante i periodi di bassa volatilità, le barre si formano lentamente - meno falsi segnali,
- Quando la volatilità aumenta, le barre diventano più frequenti, permettendoci di cogliere i movimenti rapidi,
- Livelli di supporto/resistenza ben definiti ai limiti della barra.
Barre equal volume — una nuova barra viene creata dopo l'accumulo di un numero specificato di tick (per il Forex) o di volume reale (per gli strumenti di borsa). Le barre equal volume ci permettono di confrontare i movimenti su una base equa:
- Le zone con rapida formazione di barre indicano un'elevata attività e, pertanto, è possibile una rottura dell'attuale intervallo.
- Le aree vuote del grafico indicano una mancanza di interesse e, pertanto, è possibile un'inversione di tendenza.
Osservazioni:
Lo Strategy Tester genera tick per i simboli personalizzati secondo regole standard: in base alla configurazione delle barre (OHLC). Tuttavia, per le barre non basate sul tempo (soprattutto le barre Renko), il prezzo di chiusura della barra è per definizione un indicatore del movimento successivo. Il tester può creare l'illusione di un ingresso ideale osservando la configurazione della barra zero (non completata).
Soluzione:
- Prendere sempre i segnali dalla prima barra (completata), non da zero;
- Impostare il tester in modo che funzioni sulle barre completate;
- Utilizzare l’instradamento dell'ordine del simbolo reale (vedere di seguito) — ciò eliminerà gli artefatti di generazione dei tick nel tester.
Strumenti sintetici: spread, panieri e collegamenti intramercato
I mercati raramente si muovono in isolamento — gli strumenti finanziari sono spesso interconnessi. La coppia EUR/USD tende a trascinare con sé la coppia GBP/USD, il prezzo del petrolio influenza il sentiment di CAD, AUD e NOK, mentre l'S&P 500 e il DAX spesso si muovono in sincronia. Ma cosa succederebbe se, invece di osservare passivamente le correlazioni, creassimo un unico strumento in grado di combinare matematicamente questi asset? I simboli personalizzati in MetaTrader 5 vi permettono di andare oltre gli strumenti standard del broker e di costruire il proprio sistema analitico: spread, panieri, coppie di arbitraggio e indici di anticorrelazione.
Matematica dei sintetici:
La base di qualsiasi strumento sintetico è una combinazione lineare:
Synth = k₁·Asset_A ± k₂·Asset_B ± ... ± kₙ·Asset_N.
In pratica, si utilizza più spesso un'equazione semplificata per calcolare lo spread tra due asset finanziari:
Spread = Price_A - Ratio · Price_B,
dove Ratio è il coefficiente di normalizzazione che uniforma la volatilità o la scala dei prezzi degli asset. Senza normalizzazione, lo spread sarà distorto a favore dello strumento più costoso, il che distorcerà i segnali dell'oscillatore e i calcoli dei requisiti di margine.
Il compito tecnico principale è sincronizzare i tick. La ricezione asincrona delle quotazioni da diverse fonti di liquidità genera spread irregolari e falsi segnali di breakout. In MetaTrader 5, risolviamo questo problema tramite una finestra temporale di buffering e fissando l'ultimo prezzo noto per ogni gamba del sintetico.
Generatore di tick sintetici:
//+------------------------------------------------------------------+ //| synthetic tick generator based on two assets | //+------------------------------------------------------------------+ class CSyntheticTickGenerator { private: string symbol_a; string symbol_b; string synth_name; double ratio; double last_price_a; double last_price_b; int symbol_digits; double point; public: //+------------------------------------------------------------------+ //| initializer | //+------------------------------------------------------------------+ bool Init(const string _sym_a, const string _sym_b, const string _synth, double _ratio) { symbol_a=_sym_a; symbol_b=_sym_b; synth_name=_synth; ratio=_ratio; symbol_digits=(int)SymbolInfoInteger(_sym_a, SYMBOL_DIGITS); point=SymbolInfoDouble(_sym_a, SYMBOL_POINT); last_price_a=0.0; last_price_b=0.0; return true; } //+------------------------------------------------------------------+ //| handle an incoming tick | //+------------------------------------------------------------------+ void ProcessTick(const MqlTick &tick, const string source_symbol) { if(tick.bid<=0 || tick.ask<=0) return; //--- update the last known price for the corresponding leg if(source_symbol==symbol_a) { last_price_a=tick.bid; } else if(source_symbol==symbol_b) { last_price_b=tick.bid; } //--- waiting for prices to appear for both assets if(last_price_a<=0 || last_price_b<=0) return; //--- calculate synthetic price with normalization double synth_bid=NormalizeDouble(last_price_a-ratio*last_price_b, symbol_digits); double base_spread=tick.ask-tick.bid; double synth_ask=NormalizeDouble(synth_bid+base_spread, symbol_digits); //--- tick structure formation MqlTick synth_tick={0}; synth_tick.time=tick.time; synth_tick.time_msc=tick.time_msc; synth_tick.bid=synth_bid; synth_tick.ask=synth_ask; synth_tick.flags=TICK_FLAG_BID | TICK_FLAG_ASK; //--- writing a custom symbol to the database MqlTick batch[1]; batch[0]=synth_tick; CustomTicksAdd(synth_name, batch); } }; //+------------------------------------------------------------------+
Esempio di strategia — gli spread sintetici sono ideali per l'arbitraggio statistico e le strategie di regressione verso la media. L'approccio tradizionale consiste nel monitorare la deviazione dello spread dalla media mobile in unità di deviazione standard (z-score).
Logica dei segnali:
- Ingresso — se |z-score| > 2,0, la deviazione dello spread è statisticamente anomala;
- Uscita — |z-score| < 0,5, si è verificata una regressione verso la media, la posizione viene chiusa;
- Gestione del rischio - lo stop loss è collegato alla volatilità del paniere (ad esempio, 2,5 × ATR(20)).
Di seguito vengono presentate le funzioni per il calcolo del z-score e la generazione dei segnali:
//+------------------------------------------------------------------+ //| z-score calculation for synthetic spread | //+------------------------------------------------------------------+ double CalculateSpreadZScore(const string synth_symbol, int ma_period, int std_dev_period) { static int ma_handle=INVALID_HANDLE; static int std_handle=INVALID_HANDLE; //--- initialize handles on first call if(ma_handle == INVALID_HANDLE) { ma_handle=iMA(synth_symbol, PERIOD_M1, ma_period, 0, MODE_SMA, PRICE_CLOSE); if(ma_handle==INVALID_HANDLE) { Print("Error creating iMA handle: ", GetLastError()); return 0.0; } } if(std_handle == INVALID_HANDLE) { std_handle = iStdDev(synth_symbol, PERIOD_M1, std_dev_period, 0, MODE_SMA, PRICE_CLOSE); if(std_handle==INVALID_HANDLE) { Print("Error creating iStdDev handle: ", GetLastError()); return 0.0; } } double ma_buf[], std_buf[], close_buf[]; ArraySetAsSeries(ma_buf, true); ArraySetAsSeries(std_buf, true); ArraySetAsSeries(close_buf, true); //--- copy data from the 1st completed bar (index 1, quantity 1) if(CopyBuffer(ma_handle, 0, 1, 1, ma_buf) != 1) return 0.0; if(CopyBuffer(std_handle, 0, 1, 1, std_buf) != 1) return 0.0; if(CopyClose(synth_symbol, PERIOD_M1, 1, 1, close_buf) != 1) return 0.0; double spread_ma = ma_buf[0]; double spread_std = std_buf[0]; double current_spread = close_buf[0]; if(spread_std == 0.0) return 0.0; return (current_spread - spread_ma) / spread_std; } //+------------------------------------------------------------------+ //| trading signal generator | //+------------------------------------------------------------------+ int CheckSpreadSignal(const string synth_symbol) { double z = CalculateSpreadZScore(synth_symbol, 20, 20); if(z>2.0) return -1; // spread overbought -> spread short if(z<-2.0) return 1; // spread oversold -> spread long if(MathAbs(z)<0.5) return 0; // close signal return 0; }
La direzione della copertura dipende dal segno dello z-score: se la deviazione è positiva, vendiamo la gamba A in ipercomprato e acquistiamo la gamba B in ipervenduto; se è negativa, avviene l'opposto. I requisiti di margine vengono calcolati automaticamente dal tester se i tassi incrociati corrispondenti sono disponibili nel MarketWatch.
Indici del paniere e ribilanciamento dinamico
Se uno spread è uno strumento a due gambe, allora un paniere è un portafoglio di N simboli. I fattori di ponderazione possono essere:
- Statico - ponderato in modo uguale (k = 1/N) o fisso in base alla capitalizzazione/liquidità del simbolo;
- Dinamico - ricalcolato in base alla volatilità inversa o al movimento dei volumi.
Il ribilanciamento dinamico richiede il ricalcolo periodico dei coefficienti k e la regolazione delle proprietà del simbolo tramite CustomSymbolSetDouble(). Nella versione attuale dello strategy tester, le proprietà del contratto sono fisse all'inizio dell'ottimizzazione, quindi le modifiche dinamiche dei pesi devono essere emulate modificando la cronologia dei tick o utilizzando indicatori personalizzati per calcolare i segnali senza modificare le specifiche del contratto.
Digressione analitica: Le insidie degli strumenti sintetici
La correlazione non implica causalità - il legame storico tra gli asset può interrompersi nel momento degli shock macroeconomici. Lo spread sintetico EUR/USD/GBP/USD, stabile da anni, può creare un gap di 50-80 punti in seguito a notizie sui tassi di interesse delle banche centrali o a sviluppi geopolitici. La strategia di regressione alla media non tiene conto dei cambiamenti strutturali degli asset (simboli).
Calcolo dei margini e dei profitti nel tester - lo strategy tester cerca automaticamente i tassi incrociati per convertire il margine e ottenere un risultato finanziario. Se si esegue il test SYNTH_EURGBP.custom su un conto in USD, il terminale cerca le coppie nel seguente ordine:
- EURUSD.custom / GBPUSD.custom (custom)
- EURUSD.b / GBPUSD.b (con il suffisso del broker)
- EURUSD / GBPUSD (coppie base)
Se non sono presenti nel Market Watch, il tester restituirà un errore di calcolo del margine o imposterà il profitto a zero.
Disabilitazione della MQL5 Cloud Network: l'ottimizzazione sui simboli sintetici tramite agenti cloud è disabilitata. Poiché macchine diverse possono memorizzare simboli definiti dall'utente con gli stessi nomi ma con storico o fattori di normalizzazione differenti, ciò comporterebbe una desincronizzazione dei risultati e un traffico eccessivo. Pertanto, i test sono possibili solo in locale o in una rete locale.
Il filtraggio delle notizie – l'arbitraggio statistico dello spread è altamente vulnerabile agli eventi macroeconomici asimmetrici (quando l'evento interessa un solo paese emittente). Si consiglia di aggiungere il calendario economico o il filtro di volatilità che disattiva la strategia 30-60 minuti prima della pubblicazione di dati ad alto impatto.
Pipeline di integrazione del flusso di lavoro
Nello sviluppo di sistemi di trading MQL5, una "pipeline di integrazione" è una catena di elaborazione di dati ed eventi che collega una sorgente esterna all'interfaccia di trading del terminale.
In altre parole, si tratta di un flusso di dati attraverso il quale i dati grezzi (tick) passano attraverso diverse fasi di elaborazione fino a essere trasformati in uno strumento di trading o di analisi pronto all'uso. In pratica, la struttura della pipeline (dai dati al trade) si compone di quattro fasi sequenziali:
- Quando riceviamo i tick grezzi dal broker, utilizziamo le funzioni SymbolInfoTick() e/o CopyTicksRange() per la cronologia dei tick. Ad esempio, i tick per EURUSD e GBPUSD provengono da server diversi.
- L'aggregatore di classi (CBarAggregator o CSyntheticTickGenerator) riceve questi tick, li sincronizza nel tempo e applica un'equazione matematica. Ad esempio, calcola lo spread tra il Prezzo(EUR) e il Prezzo(GBP).
- Il risultato viene scritto nel database del terminale tramite le funzioni CustomRatesUpdate() o CustomTicksAdd(). Ad esempio, creando il simbolo personalizzato SPREAD_EURGBP.custom. Ora il terminale gestisce questo strumento esattamente come una normale coppia di valute.
- Utilizzo degli strumenti standard della piattaforma per il simbolo creato - avvio dell'EA/indicatore nello Strategy Tester.
Test di stress tramite storico modificato
I test retrospettivi standard spesso creano l'illusione della sicurezza. Eseguendo un Expert Advisor su uno storico perfetto, con un'esecuzione impeccabile, si ottiene una bella immagine della curva dell’equity. Ma il mercato reale è un ambiente pieno di tick tossici, spread che si allargano in concomitanza con eventi di mercato legati alle notizie e problemi di liquidità.
MetaTrader 5 offre un'opportunità unica per diventare il vostro broker. Non solo è possibile analizzare i dati storici, ma anche modificarli, peggiorando deliberatamente le condizioni di trading per testare la validità della strategia. Se un robot guadagna con dati ideali, ma perde il suo deposito quando lo spread si allarga di 5 pip, allora un sistema del genere non è sostenibile.
Vediamo come utilizzare i simboli personalizzati per creare scenari di stress: dall'aumento artificiale dei costi di transazione all'emulazione delle restrizioni imposte dai broker durante periodi di elevata volatilità.
La filosofia alla base degli stress test è piuttosto semplice: cerchiamo di mettere alla prova il sistema prima di immetterlo sul mercato reale.
Partiamo da uno storico di base di un simbolo e lo modifichiamo per simulare condizioni di mercato estreme.
Scenari di modifica:
- L'allargamento dello spread simula gli aumenti dei costi, fondamentali per lo scalping e il day trading.
- Aumento del livello di stop/freeze - test della resistenza alle restrizioni dei broker sull'impostazione di stop loss e take profit.
- Modifica dei requisiti di margine - test di resilienza alle richieste di margine in caso di variazione della leva finanziaria;
- Iniezione di tick tossici - aggiunta di gap o interruzioni di liquidità.
Il principale vantaggio dell'approccio basato su simboli personalizzati è la riproducibilità. È possibile eseguire lo stesso test con parametri di stress differenti e confrontare le metriche (fattore di profitto, drawdown massimo, fattore di recupero) in formato tabellare.
Scenario 1: Allargamento artificiale dello spread
Lo spread è il principale nemico delle strategie a breve termine. In condizioni reali, lo spread non è fisso. Oscilla tra zero e di fatto, infinito, e può ampliarsi notevolmente in concomitanza con la pubblicazione di importanti notizie macroeconomiche. Per testare questa ipotesi, possiamo prendere lo storico dei tick, clonare il simbolo e riscrivere i tick, aggiungendo un numero fisso di pip o una percentuale dello spread corrente a ogni Ask.
Di seguito è riportato uno script di esempio che crea una versione stressata di un simbolo con uno spread fisso elevato:
//+------------------------------------------------------------------+ //| inputs | //+------------------------------------------------------------------+ input string SourceSymbol = "EURUSD"; // Source symbol input string TargetSymbol = "EURUSD_Stress"; // New symbol name input int StressSpreadPoints = 50; // Fixed spread (in points) input datetime HistoryFromDate = D'2023.01.01'; // Start loading history input bool DeleteOldData = true; // Clear the target symbol's history before writing CiCustomSymbol stressSymb; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { double point; int digits; //--- get the properties of the original symbol for calculations point = SymbolInfoDouble(SourceSymbol, SYMBOL_POINT); digits = (int)SymbolInfoInteger(SourceSymbol, SYMBOL_DIGITS); if(point == 0) { Print("Error: Failed to get point size for ", SourceSymbol); return; } Print("--- Start generating stress symbol ---"); PrintFormat("Source: %s | Target: %s | Spread: %d pp", SourceSymbol, TargetSymbol, StressSpreadPoints); //--- create a custom symbol via a class method //--- return codes: -1 (error), 0 (already exists), 1 (created) int createRes = stressSymb.Create(TargetSymbol, "", SourceSymbol, 1000000, true); if(createRes == -1) { Print("Error creating symbol via CiCustomSymbol.Create()"); return; } stressSymb.Select(true); //--- clone the contract properties (Digits, Point, Mode etc.) if(!stressSymb.Clone(SourceSymbol)) { Print("Error cloning symbol properties"); return; } //--- clear old history if(DeleteOldData) { Print("Clear history using CiCustomSymbol..."); if(stressSymb.TicksDelete(0, LONG_MAX) < 0) Print("Failed to clear ticks: ", GetLastError()); } //--- cyclic loading and modification of ticks (in 1-day batches) datetime current_date = HistoryFromDate; datetime stop_date = TimeCurrent(); if(current_date >= stop_date) { Print("Error: Invalid history start date."); return; } int total_ticks_added = 0; MqlTick ticks[]; //--- calculate the stress spread value double stress_spread_value = StressSpreadPoints * point; Print("Starting batch history processing..."); while(current_date < stop_date && !IsStopped()) { //--- define the boundaries of the day datetime day_start = current_date; datetime day_end = current_date + PeriodSeconds(PERIOD_D1); ulong t1 = (ulong)day_start * 1000; ulong t2 = (ulong)day_end * 1000; //--- read the original symbol ticks int copied = CopyTicksRange(SourceSymbol, ticks, COPY_TICKS_ALL, t1, t2); if(copied > 0) { //--- data stress modification for(int i = 0; i < copied; i++) { //--- set Ask = Bid + Fixed Spread ticks[i].ask = ticks[i].bid + stress_spread_value; //--- mark that both prices have changed ticks[i].flags = TICK_FLAG_BID | TICK_FLAG_ASK; } //--- write to the database via a class int added = stressSymb.TicksReplace(t1, t2, ticks); if(added != copied) { PrintFormat("Error recording ticks for %s via class. Registered: %d of %d", TimeToString(day_start, TIME_DATE), added, copied); } else { total_ticks_added += added; } } //--- move on to the next day current_date = day_end; Sleep(10); } Print("--- Generation complete ---"); PrintFormat("Total ticks processed: %d", total_ticks_added); } //+------------------------------------------------------------------+
Lo script utilizza la classe wrapper CiCustomSymbol per gestire i simboli personalizzati. Il codice sorgente completo dello script è contenuto nel file StressTest_SpreadModifier.mq5 allegato all'articolo.
Scenario 2: Livello di Stop e livello di Freeze
Durante i periodi di elevata volatilità (ad esempio, prima della pubblicazione dei dati NFP negli Stati Uniti), molti broker aumentano la distanza minima per l'impostazione degli stop loss (Stop Level) e il livello di freeze degli ordini (Freeze Level). Se la vostra strategia imposta uno stop loss a 5 pip e il vostro broker richiede un minimo di 20 pip all'ingresso, l'ordine verrà rifiutato dal server oppure la posizione rimarrà aperta senza stop.
Come si verifica questo scenario in MetaTrader 5? Le proprietà del contratto sono specificate dalla funzione CustomSymbolSetInteger(). È possibile creare un simbolo personalizzato in cui SYMBOL_TRADE_STOPS_LEVEL è impostato su un valore compreso tra 5 e 10 volte il valore normale.
//--- set an extreme stop level (for example, 500 points) long huge_stop_level = 500; if(!CustomSymbolSetInteger(stress_symbol, SYMBOL_TRADE_STOPS_LEVEL, huge_stop_level)) Print("Error setting stop level"); //--- set the freeze level (so that orders cannot be modified close to the market) long huge_freeze_level = 500; if(!CustomSymbolSetInteger(stress_symbol, SYMBOL_TRADE_FREEZE_LEVEL, huge_freeze_level)) Print("Error setting freeze level");
Dopo aver eseguito il tester su tale simbolo, prestare attenzione al registro. Se la strategia tenta di posizionare uno stop troppo vicino, il tester restituirà "OrderSend error 130" (invalid stops). Questo vi permetterà di valutare quanto la strategia dipenda dalla capacità di impostare stop loss stretti.
Scenario 3: Requisiti di margine e leva finanziaria
La modifica dei requisiti di margine consente di testare la resilienza della propria strategia alle richieste di margin call quando la leva finanziaria disponibile diminuisce. Ciò è particolarmente vero per le strategie a griglia e le martingala, che tendono a perdere i depositi quando il margine libero è insufficiente. È possibile aumentare artificialmente SYMBOL_MARGIN_INITIAL (margine iniziale).
//--- increase the margin requirement 2 times double normal_margin = SymbolInfoDouble(source_symbol, SYMBOL_MARGIN_INITIAL); double stress_margin = normal_margin * 2.0; if(!CustomSymbolSetDouble(stress_symbol, SYMBOL_MARGIN_INITIAL, stress_margin)) Print("Error setting margin");
Digressione analitica: Come distinguere un rischio da un artefatto
Nell'interpretazione dei risultati dei test di stress, è importante non cadere negli eccessi:
- Una degradazione graduale va bene. Se la curva dell’equity diminuisce gradualmente al peggiorare delle condizioni (aumento dello spread, aumento degli stop loss), allora la strategia ha un margine di sicurezza.
- Una rottura improvvisa non va bene. Se una strategia mostra un risultato positivo su uno spread di 20, ma una perdita totale su uno spread di 21, questo è il Santo Graal adattato (sovra-ottimizzato) a condizioni ristrette. Nel trading reale, un robot del genere non sopravvivrebbe a lungo.
Fonti di dati
Per un test di stress corretto, la qualità dello storico dei tick è importante. Le barre di 1 minuto integrate (M1) non sono adatte per il test dello spread, poiché lo spread è incorporato nel prezzo di chiusura o non è presente come entità separata. Utilizzare CopyTicks() e simboli personalizzati basati sui tick.
Fasi di test della strategia:
- Preparazione dei dati - scaricare tick di alta qualità (ad esempio, da Dukascopy) o assicurarsi che il terminale abbia uno storico completo dei tick caricato,
- Creazione di un banco di prova - utilizzare uno script che clona il simbolo base (ad esempio EURUSD → EURUSD_Stress_50) e applica i modificatori (spread, stop, margine),
- Esecuzione in batch - utilizzare l'ottimizzatore dello strategy tester.
Trucchetto:
Anziché modificare il codice EA, modificate il simbolo su cui viene testato. Eseguite l'ottimizzazione per un set di simboli personalizzati: EURUSD_Normale, EURUSD_Spread_30, EURUSD_Spread_50. Raggruppate i risultati (utile netto, drawdown) in una tabella o in un database. Una strategia che si dimostra redditizia in tutti gli scenari può essere considerata adatta all'utilizzo su un conto reale.
Integrazione del flusso di lavoro: Dal tester al grafico live
Supponiamo di aver creato simboli personalizzati, generato grafici non basati sul tempo (Renko, Range) e testato la strategia nel tester. Ma ecco il problema: il server di trading del vostro broker non sa nulla dell'esistenza di EURUSD_Renko_10.
Se si tenta di inviare direttamente un ordine per un simbolo personalizzato, il terminale restituirà l'errore 4756 (Simbolo sconosciuto). I simboli personalizzati esistono solo nel terminale client. Come possiamo quindi creare un Expert Advisor (EA) che prenda decisioni basate sulle barre Renko e operi sul mercato reale di EUR/USD?
Per rispondere a questa domanda, analizzeremo la struttura di trading con instradamento degli ordini. Creeremo un meccanismo che sostituirà in modo trasparente i simboli nelle query di trading, permettendoci di analizzare uno strumento e fare trading su un altro.
Problema di instradamento e realtà virtuale
Quando un Expert Advisor (EA) viene posizionato su un grafico con un simbolo personalizzato, la variabile di sistema _Symbol restituisce il nome di questo strumento personalizzato (ad esempio XAGUSD_Range_10), il che non sorprende. Allo stesso tempo, l'EA utilizza solitamente il campo _Symbol o (che è lo stesso) la funzione di sistema Symbol() per le seguenti operazioni:
- Invio degli ordini;
- Richiesta delle quotazioni (SymbolInfoTick());
- Verifica delle posizioni aperte (PositionSelect ecc.).
Per farlo operare sul mercato reale, dobbiamo intercettare tutte queste chiamate e sostituire _Symbol con uno strumento reale (ad esempio, XAGUSD). Riscrivere manualmente il codice per ogni Expert Advisor non è il nostro approccio. La soluzione è semplice - la classe wrapper CustomOrder. Creiamo una classe che replichi le funzioni principali dell'API MQL5. All'interno di queste funzioni viene eseguito un controllo: se viene richiesto il simbolo grafico corrente (personalizzato), lo si sostituisce con quello reale.
Per evitare di modificare il codice sorgente dell'EA, utilizzeremo la direttiva #define, che sostituirà le chiamate standard con le nostre nella fase di pre-elaborazione.
Di seguito viene fornita l'implementazione della classe per la gestione dei simboli personalizzati:
//+------------------------------------------------------------------+ //| class for routing orders | //| purpose: replacing a custom symbol with a real one | //+------------------------------------------------------------------+ class CustomOrder { private: static string workSymbol; // real symbol name public: //--- set a replacement symbol static void setReplacementSymbol(const string replacement) { workSymbol = replacement; } //--- send a trade request static bool OrderSend(MqlTradeRequest &request, MqlTradeResult &result) { //--- replace a request symbol if(request.symbol == _Symbol && workSymbol != "") { request.symbol = workSymbol; //--- price adjustment if it is taken from a custom symbol if(request.type == ORDER_TYPE_BUY) request.price = SymbolInfoDouble(workSymbol, SYMBOL_ASK); else if(request.type == ORDER_TYPE_SELL) request.price = SymbolInfoDouble(workSymbol, SYMBOL_BID); } //--- call the original function return ::OrderSend(request, result); } //--- calculate profit static bool OrderCalcProfit(ENUM_ORDER_TYPE action, string symbol, double volume, double price_open, double price_close, double &profit) { if(symbol == _Symbol && workSymbol != "") symbol = workSymbol; return ::OrderCalcProfit(action, symbol, volume, price_open, price_close, profit); } //--- get the position string property static string PositionGetString(ENUM_POSITION_PROPERTY_STRING property_id) { string res = ::PositionGetString(property_id); //--- if a position symbol is requested, return the chart name, not the actual one if(property_id == POSITION_SYMBOL && res == workSymbol) return _Symbol; return res; } //--- get the order string property static string OrderGetString(ENUM_ORDER_PROPERTY_STRING property_id) { string res = ::OrderGetString(property_id); if(property_id == ORDER_SYMBOL && res == workSymbol) return _Symbol; return res; } //--- select position by symbol static bool PositionSelect(string symbol) { if(symbol == _Symbol && workSymbol != "") return ::PositionSelect(workSymbol); return ::PositionSelect(symbol); } }; //+------------------------------------------------------------------+ //| static initialization | //+------------------------------------------------------------------+ string CustomOrder::workSymbol = ""; //+------------------------------------------------------------------+ //| macros for transparent integration | //+------------------------------------------------------------------+ #define OrderSend(request, result) CustomOrder::OrderSend(request, result) #define OrderCalcProfit(action, symbol, volume, open, close, profit) CustomOrder::OrderCalcProfit(action, symbol, volume, open, close, profit) #define PositionGetString(prop) CustomOrder::PositionGetString(prop) #define OrderGetString(prop) CustomOrder::OrderGetString(prop) #define PositionSelect(symbol) CustomOrder::PositionSelect(symbol) //+------------------------------------------------------------------+
Una delle caratteristiche di implementazione è che le macro sostituiscono le funzioni di trading standard del terminale MetaTrader 5 con gli stessi parametri. Il codice completo è contenuto nel file CustomOrder.mqh allegato a questo articolo.
Esempio di integrazione nell'EA
Immaginiamo di avere un Expert Advisor TrendFollower standard generato dalla procedura guidata MQL5. Per poter fare trading su un asset reale su un grafico Renko, dobbiamo seguire tre passaggi:
1. Inclusione nell'intestazione - la direttiva #include deve essere inserita per prima, prima dell'inclusione delle librerie standard. Ciò garantisce che le macro sostituiscano le chiamate prima che il codice della libreria venga compilato.
//+------------------------------------------------------------------+ //| TrendFollower.mq5 | //+------------------------------------------------------------------+ #include <CustomOrder.mqh> #include <Expert\Expert.mqh> #include <Expert\Signal\MySignals\SignalMACD.mqh> //--- other declarations...
2. Inserimento di un parametro - aggiungiamo un parametro per indicare all'EA a quale simbolo reale inviare gli ordini.
input string InpWorkSymbol = "XAGUSD"; // Actual symbol to execute
3. Inizializzazione in OnInit() — nella funzione di inizializzazione passiamo il simbolo effettivo alla nostra classe. Utilizziamo anche un trucco ingegnoso per il tester visivo: per evitare che il grafico si blocchi, dobbiamo forzare una richiesta di quotazioni per il simbolo attuale:
int OnInit() { //--- configure the router if(InpWorkSymbol != "") { CustomOrder::setReplacementSymbol(InpWorkSymbol); //--- we force loading the history of the real symbol, so that the visualization goes smoothly MqlRates rates[1]; CopyRates(InpWorkSymbol, PERIOD_M1, 0, 1, rates); } //--- standard EA initialization... // ... return(INIT_SUCCEEDED); }
Dopo queste modifiche, l'EA analizzerà i grafici XAGUSD_Renko_10, ma tutti gli ordini vengono elaborati su XAGUSD e il simbolo viene visualizzato correttamente nel registro dei trade.
Digressione analitica: Eliminare il look-ahead bias nel tester
Perché abbiamo bisogno di tutto questo se possiamo semplicemente effettuare i test su dati reali? Ricordiamo la sezione relativa a Renko. Abbiamo scoperto che lo strategy tester genera tick in base alla configurazione delle barre (OHLC) quando si esegue il test su un simbolo personalizzato. Nel caso della barra Renko, questo crea l'illusione di un ingresso ideale: l'EA vede la chiusura della barra ed entra immediatamente al prezzo di chiusura. Tuttavia, in realtà, la formazione del mattoncino della barra richiede del tempo. Il prezzo può variare all'interno dell'intervallo di formazione della barra.
Quando si abilita l’instradamento (utilizzando la classe CustomOrder), accade quanto segue:
- L'EA rileva un segnale sul grafico con un simbolo personalizzato (ad esempio, l'intersezione della media mobile sul Renko),
- Invia una richiesta di acquisto o di vendita,
- La classe CustomOrder sostituisce un simbolo con EURUSD,
- Il trade viene eseguito al prezzo di mercato corrente del tasso di cambio reale EURUSD.
Di conseguenza, si ottengono risultati realistici. Il prezzo di ingresso potrebbe differire dal prezzo visualizzato sul grafico Renko a causa dello slippage o dell'asincronia dei dati. La differenza tra un test di un simbolo personalizzato e un test di instradamento sta nel costo della vostra ipotesi di trading.
Prima di iniziare a fare trading reale con simboli personalizzati, assicuratevi quanto segue:
- Sincronizzazione storico - assicurarsi che il simbolo attuale abbia uno storico completo nel terminale. In assenza di dati, CustomOrder potrebbe calcolare in modo errato il margine o il prezzo di ingresso;
- Nessun errore con il codice 4756 - controllare il log degli expert. Se visualizzate Unknown Symbol, significa che le macro non hanno funzionato (forse avete incluso CustomOrder.mqh dopo altre librerie);
- Slippage - quando si fa trading tramite un wrapper, si ottiene il prezzo di mercato. Assicuratevi che lo slippage consentito nelle impostazioni dell'EA sia impostato su un valore sufficientemente elevato da garantire l'esecuzione degli ordini durante i movimenti rapidi, quando i segnali Renko vengono generati con una frequenza maggiore rispetto all'aggiornamento del tick del simbolo reale.
Se si utilizzano simboli personalizzati per l'analisi, l'ottimizzazione sulla MQL5 Cloud Network non è disponibile. Utilizzare una rete locale e/o un computer locale.
Conclusioni
MetaTrader 5 ha subito un'evoluzione silenziosa ma fondamentale: il terminale ha cessato di essere un lettore passivo di quotazioni ed è diventato un laboratorio di ingegneria. Ora i trader non devono più adattarsi a ciò che offre il broker, ma possono piuttosto costruire il proprio ambiente analitico, ottimale per una specifica idea di investimento.
Abbiamo imparato a svincolarci dalla griglia del calendario attraverso i grafici Renko, Range e Equal-Volume, a sintetizzare le relazioni intramercato tramite spread e panieri, a modificare intenzionalmente i dati storici per i test di stress e infine, a collegare il trading virtuale all'esecuzione reale tramite un instradamento degli ordini trasparente. Ciascuna di queste fasi affronta un problema specifico per lo sviluppatore del sistema: il rumore temporale, la mancanza degli strumenti necessari, l'illusione di un backtest perfetto e il divario tra il grafico e il server.
Il principale vantaggio dei simboli personalizzati risiede nel controllo sui dati. Non si testano solo i parametri dell'EA, ma anche la robustezza della logica stessa rispetto ai costi di transazione, all'asincronia dei tick e alle restrizioni del broker. Se una strategia mantiene un valore atteso positivo con uno spread fisso di 50 pip, gap artificiali e un livello di stop dinamico, è pronta per il mercato reale. Altrimenti, salverete il vostro deposito scoprendo un punto debole nella fase di simulazione.
Iniziate con calma: prendete un Expert Advisor (EA) per lo scalping o per il trend, create un grafico con barre equal-volume, simulate uno scenario di stress con uno spread maggiore e collegate l’instradamento tramite CustomOrder. Confrontate le metriche.
Potreste rimanere sorpresi da come cambia la curva dell’equity quando si filtra il rumore temporale e si calcolano in anticipo i rischi di transazione. La documentazione MQL5, il CodeBase e il forum della community sono i vostri principali alleati nella creazione della vostra realtà di mercato.
Risorse consigliate per imparare a lavorare con i simboli personalizzati in MetaTrader 5:
- Documentazione sui simboli personalizzati,
- Simboli personalizzati: Nozioni di base pratiche ,
- Creazione e test di simboli personalizzati in MetaTrader 5,
- Test di stress della strategia di trading utilizzando simboli personalizzati,
- Simboli personalizzati MQL5: Creazione di un simbolo con barre 3D .
Elenco dei file allegati all'articolo:
| Nome del File | Descrizione |
|---|---|
| CiCustomSymbol.mqh | File contenente il codice della classe CiCustomSymbol |
| CreateCustomSymbol.mq5 | Codice di esempio per uno script che permette di creare un simbolo personalizzato. |
| CBarAggregator.mqh | codice della classe CBarAggregator |
| CustomChartGenerator.mq5 | Un file contenente il codice di un indicatore che genera barre storiche e in tempo reale di tre tipi: Renko, Range e equal-volume |
| CSyntheticTickGenerator.mqh | File contenente il codice della classe CSyntheticTickGenerator |
| StressTest_SpreadModifier.mq5 | Un file contenente il codice per uno script di esempio per creare un simbolo personalizzato per test di stress con un ampio spread |
| CustomOrder.mqh | File contenente il codice della classe CustomOrder |
Tradotto dal russo da MetaQuotes Ltd.
Articolo originale: https://www.mql5.com/ru/articles/22391
Avvertimento: Tutti i diritti su questi materiali sono riservati a MetaQuotes Ltd. La copia o la ristampa di questi materiali in tutto o in parte sono proibite.
Arriva il Nuovo MetaTrader 5 e MQL5
Architettura basata sugli eventi in MQL5: Come convertire un Expert Advisor in un sistema di trading a tutti gli effetti
Utilizza i canali MQL5.community e le chat di gruppo
Dalla CPU alla GPU in MQL5: Un framework OpenCL pratico per accelerare la ricerca, le ottimizzazioni e il riconoscimento dei pattern.
- App di trading gratuite
- Oltre 8.000 segnali per il copy trading
- Notizie economiche per esplorare i mercati finanziari
Accetti la politica del sito e le condizioni d’uso
Un articolo davvero interessante: lo sto rileggendo...
Qualcuno è riuscito a creare un grafico Renko utilizzando l'indicatore descritto in questo articolo?
Per qualche motivo, questo indicatore non vuole proprio funzionare da me.
Qualcuno è riuscito a creare un grafico Renko utilizzando l'indicatore descritto in questo articolo?
Per qualche motivo, questo indicatore non vuole proprio funzionare.
Hanno inserito un "blocco" davvero enorme.
Hanno installato un "mattone" davvero enorme.
L'ho impostato così com'era di default nell'indicatore.
Grazie davvero; in effetti, se si imposta un «blocco» più piccolo, il grafico viene visualizzato.