English Русский 中文 Español Deutsch 日本語 Português 한국어 Français Türkçe
Pattern con Esempi (Parte I): Massimi Multipli

Pattern con Esempi (Parte I): Massimi Multipli

MetaTrader 5Sistemi di trading | 7 ottobre 2024, 12:34
334 0
Evgeniy Ilin
Evgeniy Ilin

Contenuto


Estratto

I pattern sono spesso discussi su internet, perché sono utilizzati da molti trader. I pattern possono essere definiti come criteri di analisi visiva per determinare la direzione del prezzo che segue. L'algo trading è differente da questo. Non possono esistere criteri visivi per il trading algoritmico. Gli Expert Advisor e gli indicatori hanno metodi individuali per lavorare con le serie di prezzi. Ci sono vantaggi e svantaggi a entrambe le estremità. Il codice non ha l'ampiezza del pensiero umano e la qualità dell'analisi umana, ma ha altri preziosi vantaggi: una velocità incomparabile e una quantità incomparabile di dati numerici o logici elaborati per unità di tempo. Non è facile istruire la macchina su cosa fare. Questo richiede un po' di pratica. Col tempo, il programmatore inizia a capire la macchina e la macchina inizia a capire il programmatore. Questa serie di articoli sarà utile per i principianti, che impareranno a strutturare i loro pensieri e a suddividere compiti complessi in fasi più semplici.


Pattern di inversione

Personalmente, i pattern di inversione hanno una definizione troppo vaga. Inoltre, non hanno alcuna base matematica. A dire il vero, qualsiasi pattern non ha una base matematica e quindi l'unica matematica che può essere considerata qui è la statistica. Le statistiche sono l'unico criterio di verità, ma le statistiche sono compilate sulla base del trading reale. Ovviamente non esistono fonti in grado di fornire statistiche molto precise. Non ha nemmeno senso fornire tali dati per un problema di ricerca specifico. L'unica soluzione è il backtesting e la visualizzazione nel tester di strategie. Sebbene questo approccio offra una qualità dei dati inferiore, presenta un innegabile vantaggio, la velocità e la quantità di dati. 

Naturalmente, i pattern di inversione non sono uno strumento sufficiente per determinare le inversioni di tendenza, ma in combinazione con altri metodi di analisi, come i livelli o l'analisi delle candele, possono produrre il risultato desiderato. All'interno di questa serie di articoli, i pattern non sono considerati un metodo di analisi specificamente interessante, ma possono essere utilizzati per esercitarsi nel trading algoritmico. Oltre a fare pratica, otterrete uno strumento ausiliario interessante e utile, se non per l'algo-trading, per l'occhio del trader. Gli opportuni indicatori sono molto apprezzati.


Perché Massimi Multipli — le sue caratteristiche specifiche

Questo schema è diventato molto popolare su internet grazie alla sua semplicità. Il pattern è abbastanza comune su diversi strumenti di trading e su vari timeframe grafici, semplicemente perché non c'è nulla di complicato in esso. Inoltre, se si osserva più da vicino il pattern, si può notare che il concetto di metodo può essere ampliato utilizzando le funzionalità dell'algo-trading e del linguaggio MQL5. Possiamo provare a creare un codice generale che non sia limitato solo da un doppio massimo. Un prototipo sapientemente creato può essere utilizzato per esplorare tutti i pattern ibridi e i discendenti.

Il classico discendente del massimo multiplo è il popolarissimo pattern "Testa e Spalle". Purtroppo non esistono informazioni strutturate su come fare trading con questo pattern. Questo problema è comune a molte strategie popolari, perché ci sono molte belle parole ma nessuna statistica. In questo articolo proverò a comprendere se è possibile utilizzarli nell’ambito del trading algoritmico. L'unico metodo per raccogliere statistiche senza operare su un conto demo o reale è quello di utilizzare le funzionalità dello strategy tester. Senza questo strumento, non sarete in grado di trarre conclusioni complesse su una particolare strategia.


Il concetto di Doppio Massimo può essere esteso?

Per quanto riguarda l'argomento dell'articolo, cercherò di disegnare un diagramma come un albero di pattern che parte da un doppio massimo. Questo aiuterà a capire quanto siano ampie le possibilità di questo concetto:

Albero

Ho deciso di combinare il concetto di diversi pattern con l'ipotesi che si basino più o meno sulla stessa idea. L'idea ha un principio semplice - trovare un buon movimento in qualsiasi direzione e determinare correttamente il punto in cui dovrebbe invertirsi. Dopo il contatto visivo con il pattern proposto, il trader deve tracciare correttamente alcune linee ausiliarie, che lo aiuteranno a valutare se il pattern soddisfa determinati criteri e a determinare il punto di ingresso nel mercato insieme al livello di target e stop loss. Qui è possibile utilizzare il take profit invece del target.

I pattern possono avere alcuni principi costruttivi comuni, in base ai quali è possibile combinare i concetti di questi modelli. Questa chiara definizione è ciò che differenzia i trader algoritmici da quelli manuali. L'incertezza e l'interpretazione multipla degli stessi principi possono portare a conseguenze deludenti.

Gli schemi di base sono i seguenti:

  1. Doppio Massimo
  2. Triplo Massimo
  3. Testa e Spalle

Questi pattern hanno strutture e principi di utilizzo simili. Tutti mirano a individuare le inversioni di tendenza. Tutti e tre i pattern hanno una logica simile per quanto riguarda le linee ausiliarie. Considerate un esempio di Doppio Massimo:

Doppio estremo

Nella figura precedente, tutte le linee richieste sono numerate e significano quanto segue:

  1. Resistenza al trend
  2. Linea ausiliaria per definire un picco pessimistico (qualcuno pensa che sia un collo)
  3. Linea del collo
  4. Target ottimistico (è anche un livello di take profit per il trading)
  5. Il livello di stop-loss massimo consentito (è impostato all'estremo superiore)
  6. Linea di previsione ottimistica (pari al movimento del trend precedente)

Il target pessimistico viene determinato rispetto al punto di intersezione della linea del collo dal bordo più vicino al mercato - prendiamo la distanza tra "1" e "2", indicata come "t", e misuriamo la stessa distanza nella direzione dell'inversione proposta. Il minimo dell'obiettivo ottimistico è determinato in modo analogo, ma la distanza è misurata tra "5" e "3", indicata come "s".


Scrittura del codice per il rendering di Massimi Multipli

Iniziamo a definire la logica di ragionamento per definire questi pattern. Per trovare un pattern, dobbiamo attenerci alla logica "bar-by-bar", cioè non lavoreremo per tick, ma per barre. In questo caso, si ridurrà notevolmente il carico sul terminale, in quanto si eviteranno calcoli inutili. In primo luogo, determiniamo una classe che simboleggia un osservatore indipendente che cercherà il pattern. Tutte le operazioni necessarie per un corretto rilevamento del pattern faranno parte dell'istanza, così la ricerca verrà eseguita al suo interno. Ho scelto questa soluzione per consentire ulteriori modifiche al codice, ad esempio quando è necessario ampliare le funzionalità o modificare quelle esistenti.

Mappa della classe

Cominciamo con il considerare i contenuti della classe:

class ExtremumsPatternFamilySearcher// class simulating an independent pattern search
   {
   private:
   int BarsM;// how many bars on chart to use
   int MinimumSeriesBarsM;// the minimum number of bars in a row to detect a top
   int TopsM;// number of tops in the pattern
   int PointsPessimistM;// minimum distance in points to the nearest target
   double RelativeUnstabilityM;// maximum excess of the head size relative to the minimum shoulder
   double RelativeUnstabilityMinM;// minimum excess of the head size relative to the minimum shoulder
   double RelativeUnstabilityTimeM;// maximum excess of head and shoulders sizes
   bool bAbsolutelyHeadM;// whether a pronounced head is required
   bool bRandomExtremumsM;// random selection of extrema
     


   struct Top// top data
      {
      datetime Datetime0;// time of the candlestick closest to the market
      datetime Datetime1;// time of the next candlestick
      int Index0;// index of the candlestick closest to the market
      int Index1;// index of the next candlestick
      datetime DatetimeExtremum;// time of the top
      int IndexExtremum;// index of the top
      double Price;// price of the top
      bool bActive;// if the top is active (if not, then it does not exist)
      };
   
   struct Line// line
      {
      double Price0;// price of the candlestick closest to the market, to which the line is bound
      datetime Time0;// time of the candlestick closest to the market, to which the line is bound
      double Price1;// price of the farthest candlestick to which the line is bound
      datetime Time1;// time of the farthest candlestick to which the line is bound
      datetime TimeX;// time of the X point
      int Index1;// index of the left edge
      bool DirectionOfFormation;// direction
      double C;// free coefficient in the equation
      double K;// aspect ratio
   
      void CalculateKC()// find unknowns in the equation
         {
         if ( Time0 != Time1 ) K=double(Price0-Price1)/double(Time0-Time1);
         else K=0.0;
         C=double(Price1)-K*double(Time1);
         }
      
      double Price(datetime T)// function of line depending on time
         {
         return K*T+C;
         }
      };
   
   public:   
   
   ExtremumsPatternFamilySearcher(int BarsI,int MinimumSeriesBarsI,int TopsI,int PointsPessimistI, double RelativeUnstabilityI,
   double RelativeUnstabilityMinI,double RelativeUnstabilityTimeI,bool bAbsolutelyHeadI,bool bRandomExtremumsI)// parametric constructor
      {
      BarsM=BarsI;
      MinimumSeriesBarsM=MinimumSeriesBarsI;
      TopsM=TopsI;
      PointsPessimistM=PointsPessimistI;
      RelativeUnstabilityM=RelativeUnstabilityI;
      RelativeUnstabilityMinM=RelativeUnstabilityMinI;
      RelativeUnstabilityTimeM=RelativeUnstabilityTimeI;
      bAbsolutelyHeadM=bAbsolutelyHeadI;
      bRandomExtremumsM=bRandomExtremumsI;
      bPatternFinded=bFindPattern();
      }
      
   int FormationDirection;// direction of the formation (multiple top or bottom, or none at all) ( -1,1,0 )      
   bool bPatternFinded;// if the pattern was found during formation
   Top TopsUp[];// required upper extrema
   Top TopsDown[];// required lower extrema
   Top TopsUpAll[];// all upper extrema
   Top TopsDownAll[];// all lower extrema
   int RandomIndexUp[];// array for the random selection of the tops index
   int RandomIndexDown[];// array for the random selection of the bottoms index
   Top StartTop;// where the formation starts (top farthest from the market)
   Top EndTop;// where the formation ends (top closest to the market)
   Line Neck;// neck
   Top FarestTop;// top farthest from the neck (will be used to determine the head or the formation size) or the same as the head
   Line OptimistLine;// line of optimistic forecast
   Line PessimistLine;// line of pessimistic forecast
   Line BorderLine;// line at the edge of the pattern
   Line ParallelLine;// line parallel to the trend resistance
   
      
   private:
   void SetTopsSize();// setting sizes for arrays with tops
   bool SearchFirstUps();// search for tops
   bool SearchFirstDowns();// search for bottoms
   void CalculateMaximum(Top &T,int Index0,int Index1);// calculate the maximum price between two bars
   void CalculateMinimum(Top &T,int Index0,int Index1);// calculate the minimum price between two bars
   bool PrepareExtremums();// prepare extrema
   bool IsExtremumsAbsolutely();// control the priority of tops
   void DirectionOfFormation();// determine the direction of the formation
   void FindNeckUp(Top &TStart,Top &TEnd);// find neck for the bullish pattern
   void FindNeckDown(Top &TStart,Top &TEnd);// find neck for the bearish pattern
   void SearchFarestTop();// find top farthest from the neck
   bool bBalancedExtremums();// initial balancing of extrema (so that they do not differ much)
   bool bBalancedExtremumsHead();// if a pattern has more than 2 tops, we can check for a pronounced head
   bool bBalancedExtremumsTime();// require that the extrema be not very far in time relative to the minimum distance
   bool bBalancedHead();// balance the head (in other words, require that it be neither the first nor the last one on the list of tops, if there are more than three of them)
   bool CorrectNeckUpLeft();// adjust the neck so as to find the intersection of price and neck (this creates prerequisites for the previous trend) 
   bool CorrectNeckDownLeft();// similarly for the bottom
   int CorrectNeckUpRight();// adjust the neck so as to find the intersection of price and neck on the right or at the current price position, which is the same (to determine the entry point)
   int CorrectNeckDownRight();// similarly for the bottom
   void SearchLineOptimist();// calculate the optimistic forecast line
   bool bWasTrend();// determine whether a trend preceded the pattern definition (in this case the optimistic target line is considered as the trend beginning)
   void SearchLineBorder();// determine trend resistance or support (usually a sloping line)
   void CalculateParallel();// determine a line parallel to support or resistance (crosses the neck at the pattern low or high)
   bool bCalculatePessimistic();// calculate the line of the pessimistic target
   bool bFindPattern();// perform all the above actions
   int iFindEnter();// find intersection with the neck
   public:
   void CleanAll();// clean up objects
   void DrawPoints();// draw points
   void DrawNeck();// draw the neck
   void DrawLineBorder();// line at the border
   void DrawParallel();// line parallel to the border
   void DrawOptimist();// line of optimistic forecast
   void DrawPessimist();// line of pessimistic forecast
   };

Una classe rappresenta operazioni sequenziali che una persona eseguirebbe se fosse al posto di una macchina. In ogni caso, il rilevamento di qualsiasi formazione può essere suddiviso in una serie di semplici operazioni che si susseguono. C’è una regola in matematica: se non sapete come risolvere un'equazione, semplificatela. Questa regola si applica non solo alla matematica, ma anche a qualsiasi algoritmo. La logica di rilevamento in primo luogo non è chiara. Ma se si sa da dove iniziare il rilevamento, il compito diventa molto più semplice. In questo caso, per poter trovare l'intero pattern, cerchiamo entrambi i massimi o i minimi, o addirittura tutt'e due.

Determinazione dei massimi e dei minimi

Senza massimi e minimi, l'intero pattern non ha senso, poiché la presenza dei massimi e minimi è una condizione necessaria per il pattern, anche se questa condizione da sola non è sufficiente. Esistono diversi modi per determinare i massimi. La condizione più importante è la presenza di una semionda pronunciata, mentre, la semionda è determinata da due movimenti opposti pronunciati, che nel nostro caso dovrebbero essere diverse barre di fila, nella stessa direzione. A tal fine, è necessario determinare il numero minimo di barre in una direzione, che indicano la presenza del movimento. Per questo, forniamo una variabile di input. 

bool ExtremumsPatternFamilySearcher::SearchFirstUps()// find tops
   {
   int NumUp=0;// the number of found tops
   int NumDown=0;// the number of found bottoms
   bool bDown=false;// an auxiliary boolean which shows if a segment of bearish candlesticks has been found
   bool bUp=false;// an auxiliary boolean which shows if a segment of bullish candlesticks has been found
   bool bNextUp=true;// can we move on to searching for the next top
   bool bNextDown=true;// can we move on to searching for the next bottom
   
   for(int i=0;i<ArraySize(TopsUp);i++)// before search, set all necessary tops to an inactive state
      {
      TopsUp[i].bActive=false;
      }
   for(int i=0;i<ArraySize(TopsUpAll);i++)// before search, set all tops to an inactive state
      {
      if (!TopsUpAll[i].bActive) break;
      TopsUpAll[i].bActive=false;
      }
               
   
   for(int i=0;i<BarsM;i++)
      {
      if ( i+MinimumSeriesBarsM-1 < BarsM )// if remaining bars are enough to determine the extremum and we can start searching for the next top
         {
         if ( bNextUp )// if it is allowed to search for the next top
            {
            bDown=true;
            for(int j=i;j<i+MinimumSeriesBarsM;j++)// determine the first extrema for upper tops
               {
               if ( Open[j]-Close[j] < 0 )// if at least one of the selected candlesticks was upward
                  {
                  bDown=false;
                  break;
                  }
               }
            if ( bDown )
               {
               TopsUpAll[NumUp].Datetime0=Time[i+MinimumSeriesBarsM-1];
               TopsUpAll[NumUp].Index0=i+MinimumSeriesBarsM-1;
               bNextUp=false;
               }
            }        
         }

      if ( MinimumSeriesBarsM+i < BarsM && bDown )// if the remaining bars are enough to determine the second half of the extremum and the previous half has been found
         {
         bUp=true;                  
         for(int j=i;j<MinimumSeriesBarsM+i;j++)//determine further candlesticks in the opposite direction
            {
            if ( Open[j]-Close[j] > 0 )//if at least one of the selected candlesticks was downward
               {
               bUp=false;
               break;
               }
            }
         if ( bUp )
            {
            TopsUpAll[NumUp].Datetime1=Time[i];
            TopsUpAll[NumUp].Index1=i;
            TopsUpAll[NumUp].bActive=true;
            bNextUp=false;
            }   
         } 
      // after that, register the found formation as a top, if it is a top
      if ( bDown && bUp )
         {
         CalculateMaximum(TopsUpAll[NumUp],TopsUpAll[NumUp].Index0,TopsUpAll[NumUp].Index1);// calculate extremum between two bars
         bNextUp=true;
         bDown=false;
         bUp=false;
         NumUp++;
         }
      }
   if ( NumUp >= TopsM ) return true;// if the required number of tops have been found
   else return false;
   }

I minimi sono definiti in modo opposto:

bool ExtremumsPatternFamilySearcher::SearchFirstDowns()// find bottoms
   {
   int NumUp=0;
   int NumDown=0;
   bool bDown=false;// an auxiliary boolean which shows if a segment of bearish candlesticks has been found
   bool bUp=false;// an auxiliary boolean which shows if a segment of bullish candlesticks has been found
   bool bNextUp=true;// can we move on to searching for the next top
   bool bNextDown=true;// can we move on to searching for the next bottom

   for(int i=0;i<ArraySize(TopsDown);i++)// before search, set all necessary bottoms to an inactive state
      {
      TopsDown[i].bActive=false;
      }
   for(int i=0;i<ArraySize(TopsDownAll);i++)// before search, set all bottoms to an inactive state
      {
      if (!TopsDownAll[i].bActive) break;
      TopsDownAll[i].bActive=false;
      }

   for(int i=0;i<BarsM;i++)
      {
      if ( i+MinimumSeriesBarsM-1 < BarsM )// if remaining bars are enough to determine the extremum and we can start searching for the next top
         {
         if ( bNextDown )// if it is allowed to search for the next bottom
            {
            bUp=true;               
            for(int j=i;j<i+MinimumSeriesBarsM;j++)// determine the first extrema for upper tops
               {
               if ( Open[j]-Close[j] > 0 )//if at least one of the selected candlesticks was downward
                  {
                  bUp=false;
                  break;
                  }
               }
            if ( bUp )
               {
               TopsDownAll[NumDown].Datetime0=Time[i+MinimumSeriesBarsM-1];
               TopsDownAll[NumDown].Index0=i+MinimumSeriesBarsM-1;
               bNextDown=false;
               }
            }        
         }

      if ( MinimumSeriesBarsM+i < BarsM && bUp )// if the remaining bars are enough to determine the second half of the extremum and the previous half has been found
         {   
         bDown=true;                              
         for(int j=i;j<MinimumSeriesBarsM+i;j++)//determine further candlesticks in the opposite direction
            {
            if ( Open[j]-Close[j] < 0 )// if at least one of the selected candlesticks was upward
               {
               bDown=false;
               break;
               }
            }
         if ( bDown )
            {
            TopsDownAll[NumDown].Datetime1=Time[i];
            TopsDownAll[NumDown].Index1=i;
            TopsDownAll[NumDown].bActive=true;
            bNextDown=false;              
            }
         } 
      // after that, register the found formation as a bottom, if it is a bottom
      if ( bDown && bUp )
         {
         CalculateMinimum(TopsDownAll[NumDown],TopsDownAll[NumDown].Index0,TopsDownAll[NumDown].Index1);// calculate extremum between two bars
         bNextDown=true;
         bDown=false;
         bUp=false;            
         NumDown++;
         }
      }
      
   if ( NumDown == TopsM ) return true;//if the required number of bottoms have been found
   else return false;
   }

In questo caso non ho usato la logica dei frattali. Ho invece creato una mia logica per determinare i massimi e i minimi. Non credo che sia migliore o peggiore dei frattali, ma almeno non è necessario utilizzare alcuna funzionalità esterna. Inoltre, non c’è bisogno di utilizzare inutili funzioni integrate del linguaggio, che a volte non sono necessarie. Queste funzioni potrebbero essere buone, ma in questo caso sono ridondanti. La funzione determina tutti i massimi e i minimi con cui lavoreremo in futuro. L'immagine seguente fornisce una rappresentazione visiva di ciò che accade in questa funzione:

Ricerca di massimi e minimi

In primo luogo, cerca il movimento 1; poi cerca il movimento 2 e infine il 3 che implica la determinazione del massimo o del minimo. La logica per il 3 è implementata in due funzioni separate, che saranno così:

void ExtremumsPatternFamilySearcher::CalculateMaximum(Top &T,int Index0,int Index1)// if 2 intermediate points are found, find High between them
   {
   double MaxValue=High[Index0];
   datetime MaxTime=Time[Index0];
   int MaxIndex=Index0;
   for(int i=Index0;i<=Index1;i++)
      {
      if ( High[i] >  MaxValue )
         {
         MaxValue=High[i];
         MaxTime=Time[i];
         MaxIndex=i;
         }
      }
   T.DatetimeExtremum=MaxTime;
   T.IndexExtremum=MaxIndex;
   T.Price=MaxValue;
   }
   
void ExtremumsPatternFamilySearcher::CalculateMinimum(Top &T,int Index0,int Index1)//if 2 intermediate points are found, find Low between them
   {
   double MinValue=Low[Index0];
   datetime MinTime=Time[Index0];
   int MinIndex=Index0;
   for(int i=Index0;i<=Index1;i++)
      {
      if ( Low[i] <  MinValue ) 
         {
         MinValue=Low[i];
         MinTime=Time[i];
         MinIndex=i;
         }
      } 
   T.DatetimeExtremum=MinTime;
   T.IndexExtremum=MinIndex;
   T.Price=MinValue;      
   }

Quindi, mettere il tutto in un contenitore già pronto. La logica è la seguente: tutte le strutture utilizzate all'interno della classe richiedono l'aggiunta graduale di dati. Dopo aver superato tutti i passaggi e le fasi, vengono emessi i dati richiesti. Utilizzando questi dati, il modello può essere visualizzato graficamente sul grafico. Naturalmente, la logica determinante del massimo e del minimo può essere diversa. Il mio scopo è solo quello di mostrare una semplice logica di rilevamento per cose complesse.

Selezione dei massimi con cui lavorare

I massimi e i minimi che abbiamo trovato sono solo intermedi. Dopo averli individuati, dobbiamo selezionare i massimi che riteniamo più adatti a fungere da spalle. Non possiamo stabilirlo con certezza, perché il codice non prevede la visione artificiale (in generale, è improbabile che l'uso di tecniche così complesse possa favorire le prestazioni). Per ora, selezioniamo i massimi più vicini al mercato:

bool ExtremumsPatternFamilySearcher::PrepareExtremums()// assign the tops with which we will work
   {
   int Quantity;// an auxiliary counter for random tops
   int PrevIndex;// an auxiliary index for maintaining the order of indexes (increment only)
   
   for(int i=0;i<TopsM;i++)// simply select the tops that are closest to the market
      {
      TopsUp[i]=TopsUpAll[i];
      TopsDown[i]=TopsDownAll[i];
      }
   return true;   
   }

Visivamente, sul grafico del simbolo, la logica sarà equivalente alla variante nel riquadro viola. Disegnerò altre varianti per la selezione:

Scegliere i massimi e i minimi

In questo caso, la logica di selezione è molto semplice. Le varianti selezionate sono 0 e 1 perché sono le più vicine al mercato. Qui tutto si applica a un doppio massimo. Ma la stessa logica verrà utilizzata per i tripli o superiori massimi multipli, con l'unica differenza del numero di massimi selezionati.

Questa funzione sarà ampliata in futuro per consentire la selezione casuale dei massimi, come mostrato in blu nell'immagine precedente. In questo modo si simuleranno più istanze di cercatori di pattern. Ciò consente di trovare in modo più efficiente e frequente tutti i pattern in modo automatico.

Determinazione della direzione del pattern

Una volta identificati i massimi e i minimi, dobbiamo determinare la direzione della formazione, se tale formazione esiste in un determinato momento del mercato. In questa fase, considero di assegnare una maggiore priorità alla direzione il cui tipo di estremo è più vicino al mercato. In base a questa logica, utilizziamo la variante 0 della figura, perché la più vicina al mercato è la parte inferiore, non quella superiore (a condizione che la situazione del mercato sia esattamente la stessa della figura). Questa parte è semplice nel codice:

void ExtremumsPatternFamilySearcher::DirectionOfFormation()// determine whether it is a double top (1) or double bottom (-1) (only if all tops and bottoms are found - if not found, then 0)
   {
   if ( TopsDown[0].DatetimeExtremum > TopsUp[0].DatetimeExtremum && TopsDown[ArraySize(TopsDown)-1].bActive )
      {
      StartTop=TopsDown[ArraySize(TopsDown)-1];
      EndTop=TopsDown[0];    
      FormationDirection=-1;
      }
   else if ( TopsDown[0].DatetimeExtremum < TopsUp[0].DatetimeExtremum && TopsUp[ArraySize(TopsUp)-1].bActive )
      {
      StartTop=TopsUp[ArraySize(TopsUp)-1];
      EndTop=TopsUp[0]; 
      FormationDirection=1;  
      }
   else FormationDirection=0;   
   }

Ulteriori azioni richiedono una direzione chiaramente determinata. La direzione è equivalente al tipo di pattern:

  1. Massimi multipli
  2. Minimi multipli

Queste regole si applicano anche al pattern Testa e Spalle e a tutte le altre formazioni ibride. La classe doveva essere comune a tutti i pattern di questa famiglia — questa generalità sta già funzionando in parte.

Filtri per scartare i pattern non corretti:

Continuiamo. Sapendo che abbiamo una direzione e uno dei modi per selezionare i massimi e i minimi, dobbiamo prevedere quanto segue per un massimo multiplo: i massimi che si trovano tra quelli selezionati devono essere inferiori al più basso dei massimi selezionati. Per un minimo multiplo, tale minimo deve essere maggiore del più alto tra quelli selezionati. In questo caso, se i massimi vengono selezionati in modo casuale, tutti i massimi selezionati si distinguono chiaramente. Altrimenti, questo controllo non è necessario:

bool ExtremumsPatternFamilySearcher::IsExtremumsAbsolutely()// require the selected extrema to be the most extreme ones
   {
   if ( bRandomExtremumsM )// check only if we have a random selection of tops (in other case the check should be considered completed)
      {
      if ( FormationDirection == 1 )
         {
         int StartIndex=RandomIndexUp[0];
         int EndIndex=RandomIndexUp[ArraySize(RandomIndexUp)-1];
         for(int i=StartIndex+1;i<EndIndex;i++)// check all tops between the selected ones
            {
            for(int j=0;j<ArraySize(TopsUp);j++)
               {
               if ( TopsUpAll[i].Price >= TopsUp[j].Price )
                  {
                  for(int k=0;k<ArraySize(RandomIndexUp);k++)
                     {
                     if ( i != RandomIndexUp[k] ) return false;
                     }
                  }
               }
            }
         return true;
         }
      else if ( FormationDirection == -1 )
         {
         int StartIndex=RandomIndexDown[0];
         int EndIndex=RandomIndexDown[ArraySize(RandomIndexDown)-1];
         for(int i=StartIndex+1;i<EndIndex;i++)// check all tops between the selected ones
            {
            for(int j=0;j<ArraySize(TopsDown);j++)
               {
               if ( TopsDownAll[i].Price <= TopsDown[j].Price )
                  {
                  for(int k=0;k<ArraySize(RandomIndexDown);k++)
                     {
                     if ( i != RandomIndexDown[k] ) return false;
                     }
                  }
               }
            }
         return true;      
         }
      else return false;      
      }
   else
      {
      return true;
      }
   }

Se visualizziamo la variante corretta e quella errata della selezione casuale del massimo, eseguita dall'ultima funzione predicata, l'aspetto sarà il seguente:

Controllo dei massimi non contabilizzati


Questi criteri si rispecchiano per i pattern rialzisti e ribassisti. La figura mostra un pattern rialzista come esempio. Il secondo caso può essere facilmente immaginato.

Dopo aver completato tutte le procedure preparatorie, possiamo procedere alla ricerca del collo. Diversi trader tracciano il collo in modi vari. Ho determinato in modo condizionato diversi tipi di costruzione:

  1. Inclinato visivamente (non dalle ombre)
  2. Visivamente, orizzontale (non dalle ombre)
  3. Punto più alto o più basso, inclinato (dalle ombre)
  4. Punto più alto o più basso, orizzontale (dalle ombre)

Per motivi di sicurezza e per aumentare le possibilità di guadagno, ritengo che la variante ottimale sia la 4. Ho scelto questa soluzione per i seguenti motivi:

  • L'inizio di un movimento di inversione si trova in modo più chiaro
  • Questo approccio è più facile da implementare nel codice
  • La pendenza è determinata in modo univoco (in orizzontale)

Forse non è del tutto corretto dal punto di vista costruttivo, ma non ho trovato regole chiare. Questo non è critico dal punto di vista dell'algo-trading. Se troviamo qualcosa di razionale in questo schema, il tester o la visualizzazione ci mostreranno sicuramente qualcosa. Un ulteriore compito implica il rafforzamento dei risultati del trading, che però è un compito assolutamente differente.

Ho creato due funzioni specchio per i pattern rialzisti e ribassisti che definiscono tutti i parametri necessari del collo:

void ExtremumsPatternFamilySearcher::FindNeckUp(Top &TStart,Top &TEnd)// find the neck line based on the two extreme tops (for the classic multiple top)
   {
   double PriceMin=Low[TStart.IndexExtremum];
   datetime TimeMin=Time[TStart.IndexExtremum];
   for(int i=TStart.IndexExtremum;i>=TEnd.IndexExtremum;i--)// define the lowest point
      {
      if ( Low[i] < PriceMin )
         {
         PriceMin=Low[i];
         TimeMin=Time[i];
         }
      }
   // define the parameters of the anchor point and all parameters of the line equation
   Neck.Price0=PriceMin;
   Neck.TimeX=TimeMin;
   Neck.Time0=Time[0];
   Neck.Price1=PriceMin;
   Neck.Time1=TStart.DatetimeExtremum;
   Neck.DirectionOfFormation=true;
   Neck.CalculateKC();
   }
   
void ExtremumsPatternFamilySearcher::FindNeckDown(Top &TStart,Top &TEnd)// find the neck line based on two extreme bottoms (for the classic multiple bottom)
   {
   double PriceMax=High[TStart.IndexExtremum];
   datetime TimeMax=Time[TStart.IndexExtremum];
   for(int i=TStart.IndexExtremum;i>=TEnd.IndexExtremum;i--)// define the lowest point
      {
      if ( High[i] > PriceMax )
         {
         PriceMax=High[i];
         TimeMax=Time[i];         
         }
      }
   // define the parameters of the anchor point and all parameters of the line equation
   Neck.Price0=PriceMax;
   Neck.TimeX=TimeMax;
   Neck.Time0=Time[0];
   Neck.Price1=PriceMax;
   Neck.Time1=TStart.DatetimeExtremum;
   Neck.DirectionOfFormation=false;
   Neck.CalculateKC();
   }

Per una corretta e semplice tracciatura del collo, è meglio utilizzare le stesse regole di costruzione del collo per tutti i pattern della famiglia selezionata. Da un lato, questo elimina i dettagli superflui, che nel nostro caso non daranno nulla. Per costruire un collo per un massimo multiplo di qualsiasi complessità, è meglio utilizzare due massimi estremi del pattern. Gli indici di questi picchi saranno gli indici tra i quali cercheremo il prezzo più basso o più alto nel segmento di mercato selezionato. Il collo sarà una linea orizzontale regolare. I primi punti di ancoraggio dovrebbero trovarsi esattamente a questo livello, mentre il tempo di ancoraggio dovrebbe essere esattamente uguale al tempo dei massimi o dei minimi estremi (a seconda del pattern che stiamo considerando). Ecco come apparirà nell'immagine:

Collo

La finestra per la ricerca del minimo o del massimo è esattamente tra il primo e l'ultimo massimo. Questa regola è valida per qualsiasi pattern di questa famiglia, per qualsiasi numero di massimi e minimi.

Per determinare un target ottimistico, occorre innanzitutto definire le dimensioni del pattern. La dimensione del pattern è la distanza verticale dalla testa al collo in punti. Per determinare la distanza, occorre innanzitutto individuare il massimo più lontano dal collo. Questa massimo sarà il bordo del pattern:

void ExtremumsPatternFamilySearcher::SearchFarestTop()// define the farthest top
   {
   double MaxTranslation;// temporary variable to determine the highest top
   if ( FormationDirection == 1 )// if we deal with a multiple top
      {
      MaxTranslation=TopsUp[0].Price-Neck.Price0;// temporary variable to determine the highest top
      FarestTop=TopsUp[0];
      for(int i=1;i<ArraySize(TopsUp);i++)
         {
         if ( TopsUp[i].Price-Neck.Price0 > MaxTranslation ) 
            {
            MaxTranslation=TopsUp[i].Price-Neck.Price0;
            FarestTop=TopsUp[i];
            }
         }      
      }
   if ( FormationDirection == -1 )// if we deal with a multiple bottom
      {
      MaxTranslation=Neck.Price0-TopsDown[0].Price;// temporary variable to determine the lowest bottom
      FarestTop=TopsDown[0];      
      for(int i=1;i<ArraySize(TopsDown);i++)
         {
         if ( Neck.Price0-TopsDown[i].Price > MaxTranslation ) 
            {
            MaxTranslation=Neck.Price0-TopsDown[0].Price;
            FarestTop=TopsDown[i];
            }
         }      
      }
   }

È necessario un ulteriore controllo per verificare che i massimi non differiscano troppo. Solo se il controllo ha esito positivo si può procedere con le fasi successive. Più precisamente, dovrebbero esserci due controlli: uno per la dimensione verticale degli estremi, l'altro per quella orizzontale (tempo). Se i massimi sono troppo distanti nel tempo, questa variante non è adatta. Ecco un controllo delle dimensioni verticali:

bool ExtremumsPatternFamilySearcher::bBalancedExtremums()// balance the tops
   {
   double Lowest;// the lowest top for the multiple top
   double Highest;// the highest bottom for the multiple bottom
   double AbsMin;// distance from the neck to the nearest top
   if ( FormationDirection == 1 )// for the multiple top
      {
      Lowest=TopsUp[0].Price;
      for(int i=1;i<ArraySize(TopsUp);i++)// find the lowest top
         {
         if ( TopsUp[i].Price < Lowest ) Lowest=TopsUp[i].Price;
         }
      AbsMin=Lowest-Neck.Price0;// determine distance from the lowest top to the neck
      if ( AbsMin == 0.0 ) return false;
      if ( ((FarestTop.Price - Neck.Price0)-AbsMin)/AbsMin >= RelativeUnstabilityM ) return false;// if the head is too much bigger than the lowest leverage
      }
   else if ( FormationDirection == -1 )// for the multiple bottom
      {
      Highest=TopsDown[0].Price;
      for(int i=1;i<ArraySize(TopsDown);i++)// find the highest top
         {
         if ( TopsDown[i].Price > Highest ) Highest=TopsDown[i].Price;
         }
      AbsMin=Neck.Price0-Highest;// determine distance from the highest top to the neck
      if ( AbsMin == 0.0 ) return false;
      if ( ((Neck.Price0-FarestTop.Price)-AbsMin)/AbsMin >= RelativeUnstabilityM ) return false;// if the head is too much bigger than the lowest leverage
      }
   else return false;
   return true;   
   }

Per determinare la dimensione verticale corretta dei massimi, abbiamo bisogno di due massimi. Il primo è il più lontano dal collo, mentre il secondo è il più vicino. Se queste dimensioni differiscono notevolmente, la formazione potrebbe risultare non valida ed è meglio non rischiare e contrassegnarla come non valida. Analogamente al predicato precedente, tutto questo può essere accompagnato da una grafica appropriata di ciò che è giusto e ciò che è sbagliato:

Controllo della dimensione verticale

Sono facili da determinare visivamente, ma il codice ha bisogno di una metrica quantitativa. In questo caso, è semplice come segue:

  • K = (Max - Min)/Min
  • K <= RelativeUnstabilityM

Questa metrica è abbastanza efficiente per filtrare un gran numero di falsi pattern. Ebbene, anche il codice più sofisticato non può essere più efficiente del nostro occhio. L'unica cosa che possiamo fare è rendere la logica il più possibile vicina alla realtà, ma qui dobbiamo sapere dove fermarci.

Il controllo orizzontale avrà un aspetto simile. L'unica differenza è che noi usiamo gli indici delle barre come dimensioni (potete usare il tempo, non c'è alcuna differenza fondamentale):

bool ExtremumsPatternFamilySearcher::bBalancedExtremumsTime()// balance the sizes of shoulders and head along the horizontal axis
   {
   double Lowest;// minimum distance between the tops
   double Highest;// maximum distance between the tops
   if ( FormationDirection == 1 )// for the multiple top
      {
      Lowest=TopsUp[1].IndexExtremum-TopsUp[0].IndexExtremum;
      Highest=TopsUp[1].IndexExtremum-TopsUp[0].IndexExtremum;
      for(int i=1;i<ArraySize(TopsUp)-1;i++)// find the lowest top
         {
         if ( TopsUp[i+1].IndexExtremum-TopsUp[i].IndexExtremum < Lowest ) Lowest=TopsUp[i+1].IndexExtremum-TopsUp[i].IndexExtremum;
         if ( TopsUp[i+1].IndexExtremum-TopsUp[i].IndexExtremum > Highest ) Highest=TopsUp[i+1].IndexExtremum-TopsUp[i].IndexExtremum;
         }
      if ( double(Highest-Lowest)/double(Lowest) > RelativeUnstabilityTimeM ) return false;// if the width of one of the waves differs much
      }
   else if ( FormationDirection == -1 )// for the multiple bottom
      {   
      Lowest=TopsDown[1].IndexExtremum-TopsDown[0].IndexExtremum;
      Highest=TopsDown[1].IndexExtremum-TopsDown[0].IndexExtremum;
      for(int i=1;i<ArraySize(TopsDown)-1;i++)// find the lowest top
         {
         if ( TopsDown[i+1].IndexExtremum-TopsDown[i].IndexExtremum < Lowest ) Lowest=TopsDown[i+1].IndexExtremum-TopsDown[i].IndexExtremum;
         if ( TopsDown[i+1].IndexExtremum-TopsDown[i].IndexExtremum > Highest ) Highest=TopsDown[i+1].IndexExtremum-TopsDown[i].IndexExtremum;
         }
      if ( double(Highest-Lowest)/double(Lowest) > RelativeUnstabilityTimeM ) return false;// if the width of one of the waves differs much 
      }
   else return false;
   return true;
   }

Per questa verifica, possiamo utilizzare una metrica simile. Visivamente, può essere espresso come segue:

Controllo delle dimensioni orizzontali

In questo caso, i criteri quantitativi saranno gli stessi. Tuttavia, questa volta utilizziamo indici o tempi invece dei punti. Sarebbe meglio implementare il numero con cui ci si confronta, separatamente, in modo da lasciare spazio ad aggiustamenti flessibili:

  • K = (Max - Min)/Min
  • K <= RelativeUnstabilityTimeM

La linea del collo deve attraversare il prezzo a sinistra — ciò significa che il pattern è stato preceduto da un trend:

bool ExtremumsPatternFamilySearcher::CorrectNeckUpLeft()// next the neck line must be corrected so that it finds an intersection with the price on the left
   {
   bool bCrossNeck=false;// indicates if the neck was crossed
   if ( Neck.DirectionOfFormation )// if the neck is found for a double top
      {
      for(int i=StartTop.Index1;i<BarsM;i++)// define the intersection point
         {
         if ( High[i] >= FarestTop.Price )// if the movement goes beyond the formation, then the formation is fake
            {
            return false;
            }         
         if ( Close[i] < Neck.Price0 && Open[i] < Neck.Price0 && High[i] < Neck.Price0 && Low[i] < Neck.Price0   )
            {
            Neck.Time1=Time[i];
            Neck.Index1=i;
            return true;
            }
         }
      }
   return false;
   }
   
bool ExtremumsPatternFamilySearcher::CorrectNeckDownLeft()// next the neck line must be corrected so that it finds an intersection with the price on the left
   {
   bool bCrossNeck=false;// indicates if the neck was crossed
   if ( !Neck.DirectionOfFormation )// if the neck is found for a double bottom
      {
      for(int i=StartTop.Index1;i<BarsM;i++)// define the intersection point
         {
         if ( Low[i] <= FarestTop.Price )//  if the movement goes beyond the formation, then the formation is fake
            {
            return false;
            }         
         if ( Close[i] > Neck.Price0 && Open[i] > Neck.Price0 && High[i] > Neck.Price0 && Low[i] > Neck.Price0 )
            {
            Neck.Time1=Time[i];
            Neck.Index1=i;
            return true;
            }
         }
      }
   return false;
   }

Anche in questo caso, esistono due funzioni speculari per i pattern rialzisti e ribassisti. Di seguito è riportata un'illustrazione grafica di questo predicato e di quello successivo:

Controllo dell'intersezione a destra e a sinistra

Le caselle blu indicano i segmenti di mercato in cui controlliamo l'intersezione. Entrambi i segmenti si trovano dietro il pattern, a sinistra e a destra dei massimi estremi. 

Sono rimasti solo due controlli:

  1. Abbiamo bisogno di un pattern che attraversi la linea del collo nel momento attuale (alla candela zero).
  2. Il pattern deve essere preceduto da un movimento maggiore o uguale al pattern stesso.

Il primo punto è necessario per il trading algoritmico. Non credo che valga la pena di rilevare le formazioni solo per visualizzarle, sebbene sia prevista anche questa funzione. Abbiamo bisogno sia di rilevare che di trovare esattamente il punto da cui possiamo operare — dove possiamo aprire immediatamente una posizione, sapendo di essere al punto di ingresso. Il secondo punto è una delle condizioni necessarie, perché il pattern in sé è inutile senza un buon movimento precedente.

L'incrocio della candela zero (controllando l'intersezione a destra) si determina come segue:

int ExtremumsPatternFamilySearcher::CorrectNeckUpRight()// next the neck line must be corrected so that it finds an intersection with the price on the right
   bool bCrossNeck=false;// indicates if the neck was crossed
   if ( Neck.DirectionOfFormation )// if the neck is found for a double top
      {
      for(int i=EndTop.IndexExtremum;i>1;i--)// define the intersection point
         {
         if ( High[i] > FarestTop.Price || Low[i] < Neck.Price0 )// if the movement goes beyond the formation, then the formation is fake
            {
            return -1;
            }         
         }
      }
      
   if ( Close[0] <= Neck.Price0 )
      {
      Neck.Time0=Time[0];
      return 1;
      }      
   return 0;
   }

int ExtremumsPatternFamilySearcher::CorrectNeckDownRight()// next the neck line must be corrected so that it finds an intersection with the price on the right
   {
   bool bCrossNeck=false;// indicates if the neck was crossed
   if ( !Neck.DirectionOfFormation )// if the neck is found for a double bottom
      {
      for(int i=EndTop.IndexExtremum;i>1;i--)// define the intersection point
         {
         if ( Low[i] < FarestTop.Price || High[i] > Neck.Price0  )// if the movement goes beyond the formation, then the formation is fake
            {
            return -1;
            }         
         }
      }
      
   if ( Close[0] >= Neck.Price0 )
      {
      Neck.Time0=Time[0];
      return 1;
      }   
      
   return 0;
   }
<

Anche in questo caso, abbiamo due funzioni speculari. Notare che l'intersezione sulla destra non è considerata valida se il prezzo si è spostato oltre il pattern e poi è tornato indietro - questo comportamento è trattato qui ed è mostrato nella figura precedente.

Determiniamo ora come trovare la tendenza precedente. Finora sto usando la linea di previsione ottimistica per questo scopo. Se esiste un segmento di mercato tra il collo e la linea della previsione ottimistica, allora questo è il movimento desiderato. Questo movimento non deve essere troppo esteso nel tempo, altrimenti non è ovviamente un movimento:

bool ExtremumsPatternFamilySearcher::bWasTrend()// did we find the movement preceding the formation (also move here the anchor point to the intersection)
   {
   bool bCrossOptimist=false;// denotes if the neck is crossed
   if ( FormationDirection == 1 )// if the optimistic forecast is at the double top
      {
      for(int i=Neck.Index1;i<BarsM;i++)// define the intersection point
         {
         if ( High[i] > Neck.Price0 )// if the movement goes beyond the neck, then the formation is fake
            {
            return false;
            }         
         if ( Low[i] < OptimistLine.Price0 )
            {
            OptimistLine.Time1=Time[i];
            return true;
            }
         }
      }
   else if ( FormationDirection == -1 )// if the optimistic forecast is at the double bottom
      {
      for(int i=Neck.Index1;i<BarsM;i++)// define the intersection point
         {
         if ( Low[i] < Neck.Price0 )//  if the movement goes beyond the neck, then the formation is fake
            {
            return false;
            }         
         if ( High[i] > OptimistLine.Price0 )
            {
            OptimistLine.Time1=Time[i];
            return true;
            }
         }      
      }
   return false;
   }

L'ultimo predicato può essere rappresentato anche graficamente come segue:

Movimento precedente

Terminiamo qui la revisione del codice e passiamo alle valutazioni visive. Credo che le idee principali del metodo siano state sufficientemente descritte in questo articolo. Altre idee saranno prese in considerazione nel prossimo articolo di questa serie.

Verifichiamo il risultato nel tester visivo di MetaTrader 5:

Uso sempre il disegno a linee sul grafico, perché è veloce, semplice e chiaro. La Guida di MQL5 fornisce esempi di utilizzo di qualsiasi oggetto grafico, comprese le linee. Non fornirò qui il codice del disegno, ma potete vedere il risultato della sua esecuzione. Naturalmente tutto potrebbe essere fatto meglio, ma abbiamo solo un prototipo. Quindi, credo che in questo caso possiamo usare il principio di "necessità e sufficienza":

Triplo massimo nel visualizzatore del tester di strategia MetaTrader 5

Ecco un esempio con un triplo massimo. Questo esempio mi è sembrato più interessante. I doppi massimi vengono rilevati in modo analogo — è sufficiente impostare il numero di massimi desiderati nei parametri. Il codice non trova spesso formazioni di questo tipo, ma è solo una dimostrazione. Il codice può essere ulteriormente perfezionato (cosa che ho intenzione di fare in seguito).


Ulteriori idee di sviluppo

In seguito, prenderemo in considerazione ciò che non è stato detto in questo articolo e miglioreremo la qualità della ricerca per tutte le formazioni. Inoltre, perfezioneremo la classe per consentirle di rilevare le formazioni Testa e Spalle. Cercheremo anche di trovare possibili funzioni ibride di queste formazioni; una di queste potrebbe essere "N multipli e spalle multiple". La serie non è dedicata solo a questa famiglia di pattern e includerà nuovo materiale interessante e utile. Esistono diversi approcci alla ricerca di pattern e l'idea di questa serie è quella di mostrare il maggior numero possibile di pattern utilizzando differenti esempi, in modo da coprire i vari modi possibili di scomporre un compito complesso in un insieme di compiti più semplici. La serie comprenderà:

  1. Altri pattern interessanti
  2. Altri metodi per rilevare i diversi tipi di formazioni
  3. Fare trading utilizzando i dati storici e raccogliendo statistiche per diversi strumenti e periodi.
  4. Ci sono molti pattern e non li conosco tutti (quindi posso potenzialmente prendere in considerazione il vostro pattern).
  5. Prenderemo in considerazione anche i livelli (poiché i livelli sono spesso utilizzati per individuare le inversioni)


    Conclusioni

    Ho cercato di rendere il materiale semplice e comprensibile per tutti. Spero che qualcuno possa trovare qualcosa di utile qui. La conclusione di questo particolare articolo è che, come si può vedere dal tester di strategia visuale, un codice semplice è in grado di trovare formazioni complesse. Non è quindi necessario utilizzare reti neurali o scrivere/utilizzare algoritmi complessi di visione artificiale. Il linguaggio MQL5 è ricco di funzionalità per implementare anche gli algoritmi più complessi. Le possibilità sono limitate solo dalla vostra immaginazione e diligenza. 

    Tradotto dal russo da MetaQuotes Ltd.
    Articolo originale: https://www.mql5.com/ru/articles/9394

    File allegati |
    Prototype.zip (309.42 KB)
    Impara come progettare un sistema di trading tramite Fibonacci Impara come progettare un sistema di trading tramite Fibonacci
    In questo articolo continueremo la nostra serie sulla creazione di un sistema di trading basato sugli indicatori tecnici più popolari. Ecco un nuovo strumento tecnico, il Fibonacci, e impareremo a progettare un sistema di trading basato su questo indicatore tecnico.
    Gli Swap (Parte I): Posizioni di Blocco e Sintetiche Gli Swap (Parte I): Posizioni di Blocco e Sintetiche
    In questo articolo cercherò di ampliare il concetto classico dei metodi di swap trading. Spiegherò perché sono giunto alla conclusione che questo concetto merita un'attenzione particolare ed è assolutamente da studiare.
    Come utilizzare MQL5 per individuare i pattern di candele Come utilizzare MQL5 per individuare i pattern di candele
    Un nuovo articolo per imparare a rilevare automaticamente i pattern di candele sui prezzi con MQL5.
    Multibot in MetaTrader: Avvio di più robot da un singolo grafico Multibot in MetaTrader: Avvio di più robot da un singolo grafico
    In questo articolo, prenderò in considerazione un semplice modello per la creazione di un robot MetaTrader universale che può essere utilizzato su più grafici, pur essendo allegato a un solo grafico, senza la necessità di configurare ogni istanza del robot su ogni singolo grafico.