English Русский Türkçe
preview
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.

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.

MetaTrader 5Esempi |
19 4
MetaQuotes
MetaQuotes

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.

Flusso di dati personalizzato

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:

  1. Lettura dei tick dallo storico o ricezione online tramite le funzioni CopyTicksRange()/OnTick(), rispettivamente;
  2. Accumulo di dati nel buffer: tracciamento di Apertura, Massimo, Minimo, Chiusura, Volume;
  3. Verifica delle condizioni per la formazione di una nuova barra (punti/intervallo/volume);
  4. Salvataggio di una barra nel database dei simboli personalizzati tramite CustomRatesUpdate();
  5. 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.

Renko

Fig. 2. Barre Renko

Range

Fig. 3. Range bars

Equal Volume

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:

  1. 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.
  2. 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).
  3. 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.
  4. 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:

  1. L'EA rileva un segnale sul grafico con un simbolo personalizzato (ad esempio, l'intersezione della media mobile sul Renko),
  2. Invia una richiesta di acquisto o di vendita,
  3. La classe CustomOrder sostituisce un simbolo con EURUSD,
  4. 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:


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

    Ultimi commenti | Vai alla discussione (4)
    Roman Shiredchenko
    Roman Shiredchenko | 19 mag 2026 a 16:13

    Un articolo davvero interessante: lo sto rileggendo...

    Aleksandr Slavskii
    Aleksandr Slavskii | 21 mag 2026 a 03:44

    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.


    Dmitriy Skub
    Dmitriy Skub | 21 mag 2026 a 05:16
    Aleksandr Slavskii #:

    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.

    Aleksandr Slavskii
    Aleksandr Slavskii | 21 mag 2026 a 06:28
    Dmitriy Skub #:

    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.

    Arriva il Nuovo MetaTrader 5 e MQL5 Arriva il Nuovo MetaTrader 5 e MQL5
    Questa è solo una panoramica di MetaTrader 5. Non posso descrivere tutte le nuove funzionalità del sistema per un periodo di tempo così breve: i test sono iniziati il 09.09.2009. Questa è una data simbolica e sono sicuro che sarà un numero fortunato. Sono passati alcuni giorni da quando ho ricevuto la versione beta del terminale MetaTrader 5 e MQL5. Non sono riuscito a provare tutte le sue funzionalità, ma sono già sorpreso.
    Architettura basata sugli eventi in MQL5: Come convertire un Expert Advisor in un sistema di trading a tutti gli effetti Architettura basata sugli eventi in MQL5: Come convertire un Expert Advisor in un sistema di trading a tutti gli effetti
    L'articolo è dedicato all'architettura basata sugli eventi in MQL5 e descrive il passaggio dal modello monolitico OnTick all'elaborazione distribuita. Prenderemo in considerazione eventi predefiniti e personalizzati, servizi e messaggistica tra programmi, nonché errori architetturali comuni. Un esempio pratico dimostra come organizzare le interazioni tra indicatori e un Expert Advisor (EA) per ridurre il carico, migliorare la leggibilità e semplificare la manutenzione.
    Utilizza i canali MQL5.community e le chat di gruppo Utilizza i canali MQL5.community e le chat di gruppo
    Il sito web MQL5.com riunisce trader di tutto il mondo. Gli utenti pubblicano articoli, condividono codici gratuiti, vendono prodotti nel Market, offrono servizi da freelance e copiano segnali di trading. Puoi comunicare con loro sul Forum, nelle chat dei trader e nei canali MetaTrader.
    Dalla CPU alla GPU in MQL5: Un framework OpenCL pratico per accelerare la ricerca, le ottimizzazioni e il riconoscimento dei pattern. Dalla CPU alla GPU in MQL5: Un framework OpenCL pratico per accelerare la ricerca, le ottimizzazioni e il riconoscimento dei pattern.
    Scopri come creare un percorso di migrazione pratico da CPU a GPU in MQL5 utilizzando OpenCL. Ci concentreremo sull'inizializzazione del contesto, sull'organizzazione del buffer, sui batch di grandi dimensioni, sull'avvio del kernel e sulla minimizzazione degli scambi di dati. Verranno inoltre esaminati gli errori più comuni e i modi per eliminarli. Un esempio con i pattern di candele illustra il vantaggio pratico di questo approccio.