MQL5: Crea il tuo Indicatore

MetaQuotes | 8 dicembre, 2021

Introduzione

Cos'è un indicatore? È un insieme di valori calcolati che vogliamo siano visualizzati sullo schermo in un modo pratico. Gli insiemi di valori sono rappresentati nei programmi, come array. Pertanto, la creazione di un indicatore equivale alla scrittura di un algoritmo che gestisce alcuni array (matrici di prezzo) e registra i risultati della gestione su altri array (valori dell'indicatore).

Nonostante il fatto che esistano molti indicatori pronti, che sono già diventati dei classici, vi sarà sempre la necessità di creare i propri indicatori. Tali indicatori che creiamo utilizzando i nostri algoritmi sono chiamati indicatori personalizzati. In questo articolo, parleremo di come creare un semplice indicatore personalizzato.

Gli indicatori sono diversi

Un indicatore può essere presentato come linee o aree colorate, oppure può essere visualizzato sotto forma di etichette speciali che puntano in momenti favorevoli per l'inserimento della posizione. Anche questi tipi possono essere associati, cosa che ci offre ancora più tipi di indicatori. Prenderemo in considerazione la creazione di un indicatore sull'esempio del noto True Strength Index sviluppato da William Blau.

True Strength Index

L'indicatore TSI si basa su un doppio momentum per identificare le tendenze, nonché le aree di ipervenduto/ipercomprato. La spiegazione matematica di esso può essere trovata su Momentum, Direction, and Divergence di William Blau. Qui, includiamo solamente la sua formula di calcolo.

STI(CLOSE,r,s) =100*EMA(EMA(mtm,r),s) / EMA(EMA(|mtm|,r),s)

dove:

Da questa formula possiamo estrarre tre parametri che condizionano il calcolo dell'indicatore. Questi sono i periodi r e s, nonché il tipo di prezzi utilizzati per i calcoli. Nel nostro caso, usiamo il prezzo CLOSE.

MQL5 Wizard

Mostriamo il TSI come una linea blu: qui, dobbiamo avviare la procedura guidata MQL5. Nella prima fase, dovremmo indicare il tipo di programma che intendiamo creare - indicatore personalizzato. Nella seconda fase, impostiamo il nome del programma, i parametri r e s ed i loro valori.

Procedura guidata MQL5: impostazione nome indicatore e parametro

Successivamente, definiamo che l'indicatore deve essere visualizzato in una finestra separata come una linea blu e impostiamo l'etichetta TSI per questa linea.

 Wizard MQL5: impostazione del tipo di indicatore

Tutti i dati iniziali sono stati inseriti, quindi premiamo sul pulsante Fatto e otteniamo una bozza del nostro indicatore. 

//+------------------------------------------------------------------+
//|                                          True Strength Index.mq5 |
//|                        Copyright 2009, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2009, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_separate_window
#property indicator_buffers 1
#property indicator_plots   1
//---- plot TSI
#property indicator_label1  "TSI"
#property indicator_type1   DRAW_LINE
#property indicator_color1  Blue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- input parameters
input int      r=25;
input int      s=13;
//--- indicator buffers
double         TSIBuffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,TSIBuffer,INDICATOR_DATA);
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime& time[],
                const double& open[],
                const double& high[],
                const double& low[],
                const double& close[],
                const long& tick_volume[],
                const long& volume[],
                const int& spread[])
  {
//---
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

MQL5 Wizard crea l'intestazione dell'indicatore, in cui scrive le proprietà dell'indicatore, ovvero:

Tutti i preparativi sono pronti, ora possiamo perfezionare e migliorare il nostro codice.

OnCalculate()

La funzione OnCalculate() è il gestore dell'evento Calculate che compare quando è necessario ricalcolare i valori dell'indicatore e ridisegnarlo sul grafico. Questo è l'evento della ricezione di un nuovo tick, aggiornamento della cronologia dei simboli, ecc. Ecco perché il codice principale per tutti i calcoli dei valori degli indicatori deve trovarsi esattamente in questa funzione.

Naturalmente, i calcoli ausiliari possono essere implementati in altre funzioni specifiche, ma tali funzioni devono essere utilizzate nell’handler OnCalculate.

Di default, la procedura guidata MQL5 crea la seconda forma di OnCalculate() che fornisce l'accesso a tutti i tipi di time serie:

Tuttavia, nel nostro caso abbiamo bisogno di un solo array di dati, ecco perché cambiamo OnCalculate() la prima forma di chiamata.

int OnCalculate (const int rates_total,      // size of the price[] array
                 const int prev_calculated,  // number of available bars at the previous call
                 const int begin,            // from what index in price[] authentic data start
                 const double& price[])      // array, on which the indicator will be calculated
  {
//---
//--- return value of prev_calculated for next call
   return(rates_total);
  }  

Ciò ci consentirà di applicare ulteriormente l'indicatore non solo ai dati sui prezzi, bensì anche di creare l'indicatore in base ai valori di altri indicatori.

Specificare il tipo di dati per il calcolo dell'indicatore personalizzato

Se selezioniamo Chiudi nella tab dei Parametri (è previsto di default),allora il prezzo[] passato a OnCalculate() conterrà i prezzi di chiusura. Se selezioniamo, ad esempio, Prezzo Tipico, price[] conterrà i prezzi di (Alto+Basso+di Chiusura)/3 per ogni periodo.

Il parametro rate_total indica la dimensione dell'array price[]; sarà utile per organizzare i calcoli in un ciclo. L'indicizzazione degli elementi in price[] parte da zero ed è orientata dal passato al futuro. Ad esempio, l'elemento price[0] contiene il valore più vecchio, mentre price[rates_total-1] contiene l'ultimo elemento dell'array.

Organizzazione dei Buffer degli Indicatori Ausiliari

In un grafico, verrà mostrata solo una riga, ovvero i dati di un array di indicatori. Prima, però, dobbiamo organizzare dei calcoli intermedi. I dati intermedi vengono archiviati in matrici di indicatori contrassegnate dall'attributo INDICATOR_CALCULATIONS. Dalla formula vediamo che abbiamo bisogno di array aggiuntivi:

  1. per valori mtm - array MTMBuffer[];
  2. per valori |mtm| - array AbsMTMBuffer[];
  3. per EMA(mtm,r) - array EMA_MTMBuffer[];
  4. per EMA(EMA(mtm,r),s) - array EMA2_MTMBuffer[];
  5. per EMA(|mtm|,r) - array EMA_AbsMTMBuffer[];
  6. per EMA(EMA(|mtm|,r),s) - array EMA2_AbsMTMBuffer[].

In totale, abbiamo bisogno di aggiungere altri 6 array di doppio tipo a livello globale e associare questi array con i buffer indicatori sulla funzione OnInit(). Non dimenticare di indicare il nuovo numero di buffer indicatori; la proprietà indicator_buffers deve essere uguale a 7 (ce n'era 1 e ne sono stati aggiunti altri 6).

#property indicator_buffers 7

Ora, il codice dell'indicatore è simile a questo:

#property indicator_separate_window
#property indicator_buffers 7
#property indicator_plots   1
//---- plot TSI
#property indicator_label1  "TSI"
#property indicator_type1   DRAW_LINE
#property indicator_color1  Blue
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- input parameters
input int      r=25;
input int      s=13;
//--- indicator buffers
double         TSIBuffer[];
double         MTMBuffer[];
double         AbsMTMBuffer[];
double         EMA_MTMBuffer[];
double         EMA2_MTMBuffer[];
double         EMA_AbsMTMBuffer[];
double         EMA2_AbsMTMBuffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,TSIBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(2,AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(3,EMA_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(4,EMA2_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(5,EMA_AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(6,EMA2_AbsMTMBuffer,INDICATOR_CALCULATIONS);
//---
   return(0);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate (const int rates_total,    // size of the price[] array;
                 const int prev_calculated,// number of available bars;
                                           // during the previous call;
                 const int begin,          // from what index in  
                                           // price[] authentic data start;
                 const double& price[])    // array, on which the indicator will be calculated;
  {
//---
//--- return value of prev_calculated for next call
   return(rates_total);
  }

Calcoli intermedi

È molto facile organizzare il calcolo dei valori per i buffer MTMBuffer[] e AbsMTMBuffer[]. Nel ciclo, uno per uno passa attraverso i valori da price[1] a price[rates_total-1] e scrive la differenza in un array e il valore assoluto della differenza nel secondo.

//--- calculate values of mtm and |mtm|
   for(int i=1;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }

La fase successiva è il calcolo della media esponenziale di questi array. Ci sono due modi per farlo. Nel primo, scriviamo l'intero algoritmo cercando di non commettere errori. Nel secondo caso, utilizziamo funzioni pronte che sono già state sottoposte a debug e destinate esattamente a questi scopi.

Su MQL5 non ci sono funzioni integrate per il calcolo delle medie mobili in base ai valori dell'array, ma esiste una libreria pronta di funzioni MovingAverages.mqh, il cui percorso completo è terminal_directory/MQL5/Include/MovingAverages.mqh, dove il terminal_directory è un catalogo in cui è installato il terminale MetaTrader 5. La libreria è un file di inclusione che contiene funzioni per calcolare le medie mobili su array utilizzando uno dei quattro metodi classici:

Per utilizzare queste funzioni, in qualsiasi programma MQL5 aggiungere quanto segue nell'intestazione del codice:

#include <MovingAverages.mqh>

Abbiamo bisogno della funzione ExponentialMAOnBuffer() che calcola la media mobile esponenziale sull'array di valori e registra i valori della media in un altro array.

La Funzione di Smoothing di un Array

Complessivamente, il file include MovingAverages.mqh contiene otto funzioni che possono essere divise in due gruppi di funzioni dello stesso tipo, ciascuno contenente 4 di esse. Il primo gruppo contiene funzioni che ricevono un array e restituiscono semplicemente un valore di una media mobile in una posizione specificata:

Queste funzioni hanno lo scopo di ottenere il valore di una media una volta per ciascun array e non sono ottimizzate per più chiamate. Se hai bisogno di usare una funzione di questo gruppo in un ciclo (per calcolare i valori di una media e scrivere ulteriormente ogni valore calcolato in un array), dovrai predisporre un algoritmo ottimale.

Il secondo gruppo di funzioni ha lo scopo di riempire l'array del destinatario con i valori di una media mobile basata sull'array dei valori iniziali:

Tutte le funzioni specificate, ad eccezione degli array buffer[], price[] e il periodo di media del periodo, ottengono altri 3 parametri, il cui scopo è analogo ai parametri della funzione OnCalculate() - rate_total, prev_calculated e begin. Le funzioni di questo gruppo elaborano correttamente gli array passati di price[] e buffer[], tenendo conto della direzione dell'indicizzazione (flag AS_SERIES).

Il parametro begin denota l'indice di un array sorgente, da cui partono i dati significativi, cioè i dati che devono essere gestiti. Per l'array MTMBuffer[] i dati reali iniziano con l'indice 1, perché MTMBuffer[1]=price[1]-price[0]. Il valore di MTMBuffer[0] non è definito, ecco perché begin=1.

//--- calculate the first moving
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,  // index, starting from which data for smoothing are available 
                         r,  // period of the exponential average
                         MTMBuffer,       // buffer to calculate average
                         EMA_MTMBuffer);  // into this buffer locate value of the average
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,r,AbsMTMBuffer,EMA_AbsMTMBuffer);

Nel calcolare la media, è necessario prendere in considerazione il valore del periodo, poiché nell'array di output i valori calcolati vengono compilati con un lieve ritardo, che è maggiore con periodi di media più grandi. Ad esempio, se periodo=10, i valori nell'array risultante inizieranno con begin+period-1=begin+10-1. Per ulteriori chiamate di buffer[] dovrebbe essere considerato e la gestione dovrebbe essere avviata con l'indice begin+period-1.

Dunque, possiamo facilmente ottenere la seconda media esponenziale dagli array di MTMBuffer[] e AbsMTMBuffer:

//--- calculate the second moving average on arrays
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_MTMBuffer,EMA2_MTMBuffer);
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_AbsMTMBuffer,EMA2_AbsMTMBuffer);

Il valore di begin è ora uguale a r, perché begin=1+r-1 (r è il periodo della media esponenziale primaria, la gestione inizia con l'indice 1). Negli array di output EMA2_MTMBuffer[] e EMA2_AbsMTMBuffer[], i valori calcolati iniziano con l'indice r+s-1, perché abbiamo iniziato a gestire gli array di input con l'indice r e il periodo per la seconda media esponenziale è uguale a s.

Tutti i calcoli preliminari sono pronti, ora possiamo calcolare i valori del buffer dell'indicatore TSIBuffer[], che verranno riportati nel grafico.

//--- now calculate values of the indicator
   for(int i=r+s-1;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }
Compila il codice premendo il tasto F5 e avvialo nel terminale MetaTrader 5. Funziona!

La prima versione del True Strength Index

Restano ancora delle domande in sospeso.

Ottimizzazione dei Calcoli

In realtà, non è sufficiente scrivere un indicatore funzionante. Se osserviamo attentamente l'attuale implementazione di OnCalculate(), vedremo che non è ottimale.

int OnCalculate (const int rates_total,    // size of the price[] array;
                 const int prev_calculated,// number of available bars;
                 // at the previous call;
                 const int begin,// from what index of the 
                 // price[] array true data start;
                 const double &price[]) // array, at which the indicator will be calculated;
  {
//--- calculate values of mtm and |mtm|
   MTMBuffer[0]=0.0;
   AbsMTMBuffer[0]=0.0;
   for(int i=1;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }
//--- calculate the first moving average on arrays
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,  // index, starting from which data for smoothing are available 
                         r,  // period of the exponential average
                         MTMBuffer,       // buffer to calculate average
                         EMA_MTMBuffer);  // into this buffer locate value of the average
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         1,r,AbsMTMBuffer,EMA_AbsMTMBuffer);

//--- calculate the second moving average on arrays
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_MTMBuffer,EMA2_MTMBuffer);
   ExponentialMAOnBuffer(rates_total,prev_calculated,
                         r,s,EMA_AbsMTMBuffer,EMA2_AbsMTMBuffer);
//--- now calculate values of the indicator
   for(int i=r+s-1;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }
//--- return value of prev_calculated for next call
   return(rates_total);
  }

Ad ogni avvio di funzione, calcoliamo i valori negli array di MTMBuffer[] e AbsMTMBuffer[]. In questo caso, se la dimensione di price[] è uguale a centinaia di migliaia o addirittura milioni, calcoli ripetuti non necessari possono impegnare tutte le risorse della CPU, indipendentemente da quanto sia potente.

Per organizzare calcoli ottimali, utilizziamo il parametro di input prev_calculated, che è uguale al valore restituito da OnCalculate() alla chiamata precedente. Nella prima chiamata della funzione, il valore di prev_calculated è sempre pari a 0. In questo caso, calcoliamo tutti i valori nel buffer dell'indicatore. Durante la chiamata successiva, non dovremo calcolare l'intero buffer, verrà calcolato solo l'ultimo valore. Scriviamolo in questo modo:

//--- if it is the first call 
   if(prev_calculated==0)
     {
      //--- set zero values to zero indexes
      MTMBuffer[0]=0.0;
      AbsMTMBuffer[0]=0.0;
     }
//--- calculate values of mtm and |mtm|
   int start;
   if(prev_calculated==0) start=1;  // start filling out MTMBuffer[] and AbsMTMBuffer[] from the 1st index 
   else start=prev_calculated-1;    // set start equal to the last index in the arrays 
   for(int i=start;i<rates_total;i++)
     {
      MTMBuffer[i]=price[i]-price[i-1];
      AbsMTMBuffer[i]=fabs(MTMBuffer[i]);
     }

I blocchi di calcolo EMA_MTMBuffer[], EMA_AbsMTMBuffer[], EMA2_MTMBuffer[] e EMA2_AbsMTMBuffer[] non richiedono l'ottimizzazione dei calcoli, poiché ExponentialMAOnBuffer() è già scritto in modo ottimale. Dobbiamo ottimizzare solamente il calcolo dei valori per l'array TSIBuffer[]. Utilizziamo lo stesso metodo di quello usato per MTMBuffer[].

//--- now calculate the indicator values
   if(prev_calculated==0) start=r+s-1; // set the starting index for input arrays
   for(int i=start;i<rates_total;i++)
     {
      TSIBuffer[i]=100*EMA2_MTMBuffer[i]/EMA2_AbsMTMBuffer[i];
     }
//--- return value of prev_calculated for next call
   return(rates_total);

L'ultima osservazione per la procedura di ottimizzazione: OnCalculate() restituisce il valore di rate_total. Ciò significa il numero di elementi nell'array di input price[], utilizzato per i calcoli degli indicatori.

Il valore restituito da OnCalculate() viene salvato nella memoria del terminale e alla successiva chiamata di OnCalculate() viene passato alla funzione come valore del parametro di input prev_calculated.

Ciò permette di conoscere sempre la dimensione dell'array di input alla precedente chiamata di OnCalculate() e avviare il calcolo dei buffer degli indicatori da un indice corretto senza inutili ricalcoli.

Controllo dei Dati di Input

C'è un'altra cosa che dobbiamo fare affinché OnCalculate() funzioni alla perfezione. Aggiungiamo il controllo del price[] array su cui vengono calcolati i valori dell'indicatore. Se la dimensione dell'array (rates_total) è troppo piccola, non sono necessari calcoli - dobbiamo attendere fino alla prossima chiamata di OnCalculate(), quando i dati sono sufficienti.

//--- if the size of price[] is too small
  if(rates_total<r+s) return(0); // do not calculate or draw anything
//--- if it's the first call 
   if(prev_calculated==0)
     {
      //--- set zero values for zero indexes
      MTMBuffer[0]=0.0;
      AbsMTMBuffer[0]=0.0;
     }

Poiché l’adattamento esponenziale viene utilizzato due volte in sequenza per calcolare il True Strength Index, la dimensione di price[] deve essere almeno uguale o maggiore della somma dei periodi r e s; altrimenti l'esecuzione viene terminata e OnCalculate() restituisce 0. Il valore zero restituito significa che l'indicatore non verrà tracciato nel grafico, in quanto i suoi valori non vengono calcolati.

Configurare la Rappresentazione

Per quanto riguarda la correttezza dei calcoli, l'indicatore è pronto per l'uso. Tuttavia, se lo chiamiamo da un altro programma mql5, di default verrà creato da prezzi di Chiusura. Possiamo specificare un altro tipo di prezzo predefinito: specificare un valore dall'elenco ENUM_APPLIED_PRICE nella proprietà dell'indicatore Indicator_applied_price.

Ad esempio, per impostare un prezzo tipico ((alto+basso+di chiusura)/3) per un prezzo, scriviamo quanto segue:

#property indicator_applied_price PRICE_TYPICAL


Se prevediamo di usare solo i suoi valori utilizzando le funzioni iCustom() o IndicatorCreate(), non sono necessari ulteriori perfezionamenti. Tuttavia se usato direttamente, cioè tracciato nel grafico, si consigliano alcune impostazioni aggiuntive:

Queste impostazioni possono essere configurate nel gestore OnInit(), utilizzando le funzioni dal gruppo Indicatori Personalizzati. Aggiungi nuove righe e salva l'indicatore come True_Strength_Index_ver2.mq5.

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,TSIBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(2,AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(3,EMA_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(4,EMA2_MTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(5,EMA_AbsMTMBuffer,INDICATOR_CALCULATIONS);
   SetIndexBuffer(6,EMA2_AbsMTMBuffer,INDICATOR_CALCULATIONS);
//--- bar, starting from which the indicator is drawn
   PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,r+s-1);
   string shortname;
   StringConcatenate(shortname,"TSI(",r,",",s,")");
//--- set a label do display in DataWindow
   PlotIndexSetString(0,PLOT_LABEL,shortname);   
//--- set a name to show in a separate sub-window or a pop-up help
   IndicatorSetString(INDICATOR_SHORTNAME,shortname);
//--- set accuracy of displaying the indicator values
   IndicatorSetInteger(INDICATOR_DIGITS,2);
//---
   return(0);
  }

Se avviamo entrambe le versioni dell'indicatore e scorriamo il grafico fino all'inizio, vedremo tutte le differenze.


La seconda versione dell'indicatore True Strength Index ha un aspetto migliore

Conclusione

Sulla base dell'esempio della creazione dell'indicatore True Strength Index, possiamo indicare i momenti fondamentali nel processo di scrittura di qualsiasi indicatore in MQL5: