Implementación de Indicadores como Classes por Ejemplos de Zigzag y ATR

Aleksandr Chugunov | 21 marzo, 2014


¿Para qué lo necesitamos?

MetaQuotes Software Corp. revisó el concepto de trabajar con indicadores personalizados en la 5ª versión del terminal de cliente MetaTrader. Ahora se ejecutan mucho más rápido; solo hay un ejemplo de cada indicador con parámetros de entrada únicos, de modo que se calcula una vez independientemente de si usa sus copias en hasta tres gráficos de un símbolo, etc.

Pero la operación de un algoritmo permanece inalterable. Cuando perdemos la conexón a un servidor o una sincronización de historial significactiva, el valor prev_calculated (o IndicatorCounted() en MetaTrader 4) vuelve a cero, lo que lleva a un recálculo completo del indicador para el historial entero (los desarrolladores lo hicieron así a propósito para garantizar la corrección de los valores de los indicadores en cualquier circunstancia). Hay varias cosas que pueden afectar la velocidad de cálculos de los indicadores:

Cuantas más cualidades se puedan aplicar a su situación, más acuciante será para usted el problema del recálculo del indicadorpara el historial entero. Además, la situación empeora con un mal canal para la transmisión de información.

Por supuesto, puede limitar la profundidad del cálculo del indicador usando un parámetro de entrada adicional, pero ya hay un matiz al usar indicadores iCustom. El número máximo de barras que se pueden usar en un gráfico o un indicador personalizado se configura en el alcance global para el terminal entero. La memoria se distribuye para cada buffer o indicador personalizado, y se limita solo por TERMINAL_MAXBARS.

No obstante, hay una adición significativa: si limita el número máximo de barras calculadas dentro del algoritmo del indicador (por ejemplo, usando un parámetro de entrada o directamente en el código), la memoria se distribuirá dinámicamente a la llegada de cada nueva barra (aumentará gradualmente al límite de TERMINAL_MAXBARS especificado (o un poco más, este algoritmo depende totalmente de los desarrolladores, lo pueden cambiar en las próximas versiones)).


Formas de Evitar el Recálculo del Indicador del Historial Entero

Por ahora, conozco las siguiente formas de resolver este problema:
  1. Pedir a MetaQuotes que revise este problema a nivel de plataforma
  2. Crear una clase separada para la implementación de un análogo de prev_calculated

Había otra variante como presuposición de que usted puede construir en el indicador un algoritmo de cálculo de prev_calculated, pero parece ser que MetaTrader 5, a diferencia de MetaTrader 4, "limpia" todos los buffers del indicador al poner prev_calculated a cero (es decir, pone a cero todo el array del indicador de forma forzada; no puede controlarlo, puesto que este comportamiento se implementa a nivel de plataforma).

Analicemos cada variante por separado.


Ventajas y Desventajas de la Segunda Variante para Resolver el Problema

Ventajas:
Desventajas:


Creación de la clase CCustPrevCalculated para la Implementación de un Análogo de prev_calculated

La implementación de la clase en sí misma no contiene nada interesante para describir. El algoritmo considera tanto expandir el historial a amgos lados como su posible "corte" en el lado izquierdo. Asimismo, este algoritmo puede procesar la inserción de historial dentro de los datos calculados (es así para MetaTrader 4, en MetaTrader 5 aún no lo he comprobado). El código fuente de la clase se encuentra en la clase CustPrevCalculated.mqh.

Déjeme hablarse sobre las claves.


Crear un Acceso de Anillo para Elementos de Array

Para crear esta clase, vamos a usar un método poco convencional: acceso de anillo a los elementos del array para la distribución de una vez de la memoria para el array y para evitar procedimientos de copia de arrays excesivos. Consideremos el ejemplo de cinco elementos:


Acceso de anillo a los elementos del array


 
Inicialmente, trabajamos con el array cuya numeración empieza de 0. ¿Pero qué deberíamos hacer si necesitamos añadir el siguiente valor manteniendo el tamaño del array (añadir una barra nueva)? Hay dos formas:

Para implementar la segunda variante necesitaremos una variable, que llamaremos DataStartInd. Guardará la posición del índice cero del array. Por motivos de conveniencia para futuros cálculos, su numeración se corresponderá con el índice usual de un array (es decir, empezará de cero). En la variable BarsLimit almacenaremos el número de elementos del array. Por tanto, la dirección del elemento del array para el índice virtual "I" se calculará usando la siguiente fórmula simple:

La variable DataBarsCount guarda el número de celdas de memoria usadas realmente (solo podemos usar 3 de 5 celdas, por ejemplo).


Algoritmos de Sincronización de Historial

Para mí mismo seleccioné e implementé tres modos de trabajo con el algoritmo de sincronización de una copia del historial (historial local) con el historial del terminal de cliente:

El mecanismo de sincronización mismo se basa en otro parámetro configurado por un programador: HSMinute (guardado como HistorySynchSecond). Suponemos que un Dealer Center solo puede corregir los últimos minutos HSMinute del historial. Si no se encontraron diferencias durante la sincronización de ese período, el historial se considera idéntico, y se detiene la comparación. Si se encuentra una diferencia, el historial entero se revisa y corrige.

Además, el algoritmo comprueba solo precios/diferenciales/volúmenes de la estructura MqlRates especificados durante la inicialización. Por ejemplo, para dibujar ZigZag solo necesitamos precios High (alto) y Low (bajo).


Uso Práctico de la Clase CCustPrevCalculated

Para inicializar la clase CCustPrevCalculated debemos llamar a la función InitData(), que devolverá 'true' en caso de tener éxito:
CCustPrevCalculated CustPrevCalculated;
CustPrevCalculated.InitData(_Symbol, _Period, 150, CPCHSM_Normal, CPCH_high|CPCH_low, 15);
Para sincronizar el historial necesitamos llamar a la función PrepareData():
CPCPrepareDataResultCode resData;
resData = CustPrevCalculated.PrepareData();

Variantes de valores que puede devolver la función PrepareData():

enum CPCPrepareDataResultCode
  {
   CPCPDRC_NoData,                     // Returned when there is no data for calculation (not prepared by the server)
   CPCPDRC_FullInitialization,         // Full initialization of the array has been performed
   CPCPDRC_Synch,                      // Synchronization with adding new bars has been performed
   CPCPDRC_SynchOnlyLastBar,           // Synchronization of only the last bar has been performed (possible cutting of the history)
   CPCPDRC_NoRecountNotRequired        // Recalculation has not been performed, since the data was not changed
  };


Funciones de la clase CCustPrevCalculated para Acceso a Datos

Nota: para acelerar los cálculos, se excluyen las comprobaciones de desbordamiento del array. Para ser más precisos, los valores erróneos se devolverán si el índice es incorrecto.

Nombre
Propósito
 uint GetDataBarsCount()
 Devuelve el número de barras disponibles
 uint GetDataBarsCalculated()
 Devuelve el número de barras que no han cambiado
 uint GetDataStartInd()
 Devuelve el índice para accesos de envoltura (para indicadores personalizados)
 bool GetDataBarsCuttingLeft()
 Devuleve el resultado de cortar las barras desde la izquierda
 double GetDataOpen(int shift, bool AsSeries)
 Devuelve 'Open' (apertura) para la barra de cambio
 double GetDataHigh(int shift, bool AsSeries)
 Devuelve 'High' (alto) para la barra de cambio
 double GetDataLow(int shift, bool AsSeries)
 Devuelve 'Low' (bajo) para la barra de cambio
 double GetDataClose(int shift, bool AsSeries)
 Devuelve 'Close' (cierre) para la barra de cambio
 datetime GetDataTime(int shift, bool AsSeries)
 Devuelve 'Time' (tiempo) para la barra de cambio
 long GetDataTick_volume(int shift, bool AsSeries)
 Devuelve 'Tick_volume' (volumen de ticks) para la barra de cambio
 long GetDataReal_volume(int shift, bool AsSeries)
 Devuelve 'Real_volume' (volumen real) para la barra de cambio
 int GetDataSpread(int shift, bool AsSeries)
 Devuelve 'Spread' (diferencial) para la barra de cambio


Examples of Further Optimization of the Class CCustPrevCalculated


Crear CCustZigZagPPC para el Cálculo del Indicador Personalizado ZigZag en Base a los Datos de la Clase CCustPrevCalculated

Este algoritmo se basa en el indicador personalizado Professional ZigZag. El código fuente de la clase está en el archivo ZigZags.mqh; además, la biblioteca OutsideBar.mqh se usa para trabajar con barras externas.

Creemos una estructura separada para la descripción de una barra de nuestro indicador:

struct ZZBar
  {
   double UP, DN;                      // Buffers of the ZigZag indicator
   OrderFormationBarHighLow OB;       // Buffer for caching of an external bar
  };

Asimismo, determinemos el resultado de la devolución de los cálculos de la clase:

enum CPCZZResultCode
  {
   CPCZZRC_NotInitialized,             // Class is no initialized
   CPCZZRC_NoData,                     // Faield to receive data (including the external bar)
   CPCZZRC_NotChanged,                 // No changes of ZZ rays
   CPCZZRC_Changed                     // ZZ rays changed
  };

Para inicializar la clase CCustZigZagPPC debemos llamar la función Init() una vez; si tiene éxito, devolverá 'true':

CCustZigZagPPC ZZ1;
ZZ1.Init(CustPrevCalculated, _Symbol, _Period, 150, CPCHSM_Normal, CPCH_high|CPCH_low, 15, 0, true, 12, 10);

Para los cálculos del indicador debemos comenzar la actualización de los datos basados en los datos anteriormente calculados de la clase CCustPrevCalculated:

CPCPrepareDataResultCode resZZ1;
resZZ1 = ZZ1.PrepareData(resData);

Y después llamaremos al procedimiento Calculate():

if ( (resZZ1 != CPCPDRC_NoData) && (resZZ1 != CPCPDRC_NoRecountNotRequired) )
   ZZ1.Calculate();

El ejemplo completo de uso de una clase CCustPrevCalculated junto con varias clases CCustZigZagPPC se da en el archivo ScriptSample_CustZigZagPPC.mq5.


Función de Acceso a DAtos de la Clase CCustZigZagPPC

Nombre
Propósito
 uint GetBarsCount()
 Devuelve el número de barras disponibles
 uint GetBarsCalculated()  Devuelve el número de barras calculadas
 double GetUP(uint shift, bool AsSeries)
 Devuelve el valor del pico de ZigZag para una barra
 double GetDN(uint shift, bool AsSeries)
 Devuelve el valor low de ZigZag para una barra
 OrderFormationBarHighLow GetOB(uint shift, bool AsSeries)  Devuelve el valor 'Outside' (fuera) para una barra


Revisión Visual y de Programa

Para la revisión visual, adjuntemos el indicador original a un gráfico, y sobre él adjuntemos el indicador de prueba especialmente escrito Indicator_CustZigZag.mq5 con parámetros de entrada idénticos (pero debería seleccionar colores diferentes para ver ambos indicadores). Aquí puede ver el resultado de su funcionamiento:

Rojo - original, azul - nuestro indicador propio, calculado en las últimas 100 barras.

Podemos compararlos igualmente en un Expert Advisor. ¿Habrá alguna diferencia? Los resultados obtenidos de iCustom("AlexSTAL_ZigZagProf") y la clase CCustZigZagPPC se comparan en cada tick en el Expert Advisor de prueba Expert_CustZigZagPPC_test.mq5. La información sobre el cálculo se muestra en el diario (puede que no hayan cálculos en las primeras barras por falta de historial para el algoritmo):

(EURUSD,M1)                1.35797; 1.35644; 1.35844; 1.35761; 1.35901; 1.35760; 1.35959; 1.35791; 1.36038; 1.35806; 1.36042; 1.35976; 1.36116; 1.35971; // it is normal
(EURUSD,M1) Tick processed: 1.35797; 1.35644; 1.35844; 1.35761; 1.35901; 1.35760; 1.35959; 1.35791; 1.36038; 1.35806; 1.36042; 1.35976; 1.36116; 
(EURUSD,M1) Divergence on the bar: 7 

Consideremos este Expert Advisor con más detalle. Determine las variables globales para su funcionamiento:

#include <ZigZags.mqh>

CCustPrevCalculated CustPrevCalculated;
CCustZigZagPPC ZZ1;
int HandleZZ;

Initialize the variables:

int OnInit()
  {
   // Creating new class and initializing it
   CustPrevCalculated.InitData(_Symbol, _Period, 150, CPCHSM_Normal, CPCH_high|CPCH_low, 15);
   
   // Initializing the class ZZ
   ZZ1.Init(GetPointer(CustPrevCalculated), _Symbol, _Period, 150, CPCHSM_Normal, CPCH_high|CPCH_low, 15, 0, true, 12, 10);
   
   // Receiving handle for the custom indicator
   HandleZZ = iCustom(_Symbol, _Period, "AlexSTAL_ZigZagProf", 12, 10, 0 , true);
   Print("ZZ_handle = ", HandleZZ, "  error = ", GetLastError());

   return(0);
  }
Procesar ticks en el Expert Advisor:
void OnTick()
  {
   // Calculation of data
   CPCPrepareDataResultCode resData, resZZ1;
   resData = CustPrevCalculated.PrepareData();
   
   // Start recalculation for each indicator! PrepareData obligatory!
   resZZ1 = ZZ1.PrepareData(resData);
   
   // Расчет данных ZZ1
   if ( !((resZZ1 != CPCPDRC_NoData) && (resZZ1 != CPCPDRC_NoRecountNotRequired)) )
      return;

   // Получим результаты расчета
   ZZ1.Calculate();

Ahora tenemos barras ZZ1.GetBarsCalculated() calculadas por el CCustZigZagPPC. Añadamos el código de datos comparados de iCustom("AlexSTAL_ZigZagProf") y la clase CCustZigZagPPC:

   int tmpBars = (int)ZZ1.GetBarsCalculated();
   double zzUP[], zzDN[];
   CopyBuffer(HandleZZ, 0, 0, tmpBars, zzUP);
   CopyBuffer(HandleZZ, 1, 0, tmpBars, zzDN);
   
   // Perform comparison
   string tmpSt1 = "", tmpSt2 = "";
   for (int i = (tmpBars-1); i >= 0; i--)
     {
      double tmpUP = ZZ1.GetUP(i, false);
      double tmpDN = ZZ1.GetDN(i, false);
      if (tmpUP != zzUP[i])
         Print("Divergence on the bar: ", i);
      if (tmpDN != zzDN[i])
         Print("Divergence on the bar: ", i);
      if (tmpUP != EMPTY_VALUE)
         tmpSt1 = tmpSt1 + DoubleToString(tmpUP, _Digits) + "; ";
      if (tmpDN != EMPTY_VALUE)
         tmpSt1 = tmpSt1 + DoubleToString(tmpDN, _Digits) + "; ";

      if (zzUP[i] != EMPTY_VALUE)
         tmpSt2 = tmpSt2 + DoubleToString(zzUP[i], _Digits) + "; ";
      if (zzDN[i] != EMPTY_VALUE)
         tmpSt2 = tmpSt2 + DoubleToString(zzDN[i], _Digits) + "; ";
     }
  Print("Tick processed: ", tmpSt1);
  Print("                              ", tmpSt2);
  }

Aquí está el sencillo uso práctico de la clase CCustZigZagPPC en un Expert Advisor o script. Las funciones de acceso directo GetUP(), GetDN(), GetOB() en lugar de CopyBuffer().


Mover Nuestro Indicador a una Clase Separada (por el ejemplo de iATR)

En la base del archivo ZigZags.mqh, yo hice la plantilla MyIndicator.mqh para un desarrollo rápido de indicadores personalizados de acuerdo con los principios descritos arriba.

Plan general:

1. Fase Preparatoria

2. Elija parámetros externos que se tomarán del indicador inicial (original) a la clase, declárelos e inicialícelos.

En mi ejemplo, el indicador ATR tiene un parámetro externo:
input int InpAtrPeriod=14;  // ATR period
class CCustATR
  {
protected:
   ...
   uchar iAtrPeriod;
   ...
public:
   ...
   bool Init(CCustPrevCalculated *CPC, string Instr, ENUM_TIMEFRAMES TF, int Limit, CPCHistorySynchMode HSM, uchar HS, uint HSMinute, uchar AtrPeriod);
bool CCustATR::Init(CCustPrevCalculated *CPC, string Instr, ENUM_TIMEFRAMES TF, int Limit, CPCHistorySynchMode HSM, uchar HS, uint HSMinute, uchar AtrPeriod)
{
      ...
      BarsLimit = Limit;
      iAtrPeriod = AtrPeriod;
      ...

3. Determine el número requerido de buffers en el indicador inicial, y declárelos en nuestra clase. Asimismo, declare las funciones de devolución de los buffers INDICATOR_DATA.

struct ATRBar
  {
   double Val;                          // Indicator buffers
  };

a nuestra estructura propia:

struct ATRBar
  {
   double ATR;
   double TR;
  };
CPCPrepareDataResultCode CCustATR::PrepareData(CPCPrepareDataResultCode resData)
{
   ...
   for (uint i = (DataBarsCalculated == 0)?0:(DataBarsCalculated+1); i < DataBarsCount; i++)
     {
      Buf[PInd(i, false)].ATR = EMPTY_VALUE;
      Buf[PInd(i, false)].TR = EMPTY_VALUE;
     }
   ...

cambie (si solo hay un buffer, puede saltarse el cambio)

clase CCustATR
  {
   ...
   double GetVal(uint shift, bool AsSeries);                      // returns the Val value of the buffer for a bar
   ...

a

class CCustATR
  {
   ...
   double GetATR(uint shift, bool AsSeries);                      // Возвращает значение буфера ATR для бара
   ...

y cambie el código de la función correspondiente:

double CCustATR::GetATR(uint shift, bool AsSeries)
{
   if ( shift > (DataBarsCount-1) )
      return(EMPTY_VALUE);
   return(Buf[PInd(shift, AsSeries)].ATR);
}
Nota: en lugar de varias funciones de valores de devolución de buffer, puede usar solo uno que tiene un parámetro adicional - número o nombre del buffer.


4. Copìe la lógia de la función OnCalculate() del indicador inicial a la función correspondiente de la clase

CPCATRResultCode CCustATR::Calculate()
{
   ...
   // Check if there are enough bars for the calculation
   if (DataBarsCount <= iAtrPeriod)
      return(CPCATRRC_NoData);
   ...
   if ( DataBarsCalculated != 0 )
      BarsForRecalculation = DataBarsCount - ATRDataBarsCalculated - 1;
   else
     {
      Buf[PInd(0, false)].TR = 0.0;
      Buf[PInd(0, false)].ATR = 0.0;
      //--- filling out the array of True Range values for each period
      for (uint i = 1; i < DataBarsCount; i++)
         Buf[PInd(i, false)].TR = MathMax(CustPrevCalculated.GetDataHigh(i, false), CustPrevCalculated.GetDataClose(i-1, false)) - 
                                  MathMin(CustPrevCalculated.GetDataLow(i, false), CustPrevCalculated.GetDataClose(i-1, false));
      //--- first AtrPeriod values of the indicator are not calculated
      double firstValue = 0.0;
      for (uint i = 1; i <= iAtrPeriod; i++)
        {
         Buf[PInd(i, false)].ATR = 0;
         firstValue += Buf[PInd(i, false)].TR;
        }
      //--- calculating the first value of the indicator
      firstValue /= iAtrPeriod;
      Buf[PInd(iAtrPeriod, false)].ATR = firstValue;
      
      BarsForRecalculation = DataBarsCount - iAtrPeriod - 2;
     }
   for (uint i = (DataBarsCount - BarsForRecalculation - 1); i < DataBarsCount; i++)
     {
      Buf[PInd(i, false)].TR = MathMax(CustPrevCalculated.GetDataHigh(i, false), CustPrevCalculated.GetDataClose(i-1, false)) - 
                               MathMin(CustPrevCalculated.GetDataLow(i, false), CustPrevCalculated.GetDataClose(i-1, false));
      Buf[PInd(i, false)].ATR = Buf[PInd(i-1, false)].ATR + (Buf[PInd(i, false)].TR-Buf[PInd(i-iAtrPeriod, false)].TR) / iAtrPeriod;
      ...

Eso es todo. Hemos creado nuestra clase. Para la revisión visual, puede crear un indicador de prueba (en mi ejemplo es Indicator_ATRsample.mq5):



Se me ocurrió una idea al corregir el artículo: cuando usa la clase CCustPrevCalculated junto con solo un indicador personalizado, puede integrar la creación, inicialización y sincronización de esta clase en el indicador personalizado (en mi ejemplo, eran CCustZigZagPPC y CCustATR). Al llamar a la función de inicialización de indicadores personalizados para este propósito, debe usar el señalizador cero en el objeto:

   ATR.Init(NULL, _Symbol, _Period, iBars, CPCHSM_Normal, 0, 30, InpAtrPeriod);

Ahora, la estructura general

#include <CustPrevCalculated.mqh>
#include <ATRsample.mqh>
CCustPrevCalculated CustPrevCalculated;
CCustATR ATR;

int OnInit()
  {
   CustPrevCalculated.InitData(_Symbol, _Period, iBars, CPCHSM_Normal, 0, 30);
   ATR.Init(GetPointer(CustPrevCalculated), _Symbol, _Period, iBars, CPCHSM_Normal, 0, 30, InpAtrPeriod);
  }

int OnCalculate(...)
  {
   CPCPrepareDataResultCode resData = CustPrevCalculated.PrepareData();
   CPCPrepareDataResultCode resATR = ATR.PrepareData(resData);
   if ( (resATR != CPCPDRC_NoData) && (resATR != CPCPDRC_NoRecountNotRequired) )
      ATR.Calculate();
  }

se simplificará a:

#include <ATRsample.mqh>
CCustATR ATR;

int OnInit()
  {
   ATR.Init(NULL, _Symbol, _Period, iBars, CPCHSM_Normal, 0, 30, InpAtrPeriod);
  }

int OnCalculate(...)
  {
   ATR.Calculate();
  }
Puede encontrar un ejemplo práctico en el archivo Indicator_ATRsample2.mq5.

Influencia de la Tecnología Descrita sobre Rendimiento en el Probador de Estrategias

Para la revisión, creé un Expert Advisor de prueba (TestSpeed_IndPrevCalculated.mq5) que recibe el valor del indicador de la barra cero en cada tick según una de tres variantes:

enum eTestVariant
  {
   BuiltIn,    // Built-in indicator iATR
   Custom,     // Custom indicator iCustom("ATR")
   IndClass    // Calculation in the class
  };

Este Expert Advisor se ejecutó 10 veces en 1 agente con los siguientes parámetros de optimización:

Medí el tiempo de optimización al usar cada una de las tres variantes del indicador. El resultado de la revisión se muestra como un histograma lineal.

El tiempo de optimización para tres tipos de implementación del indicador ATR

El código fuente del Expert Advisor usado para medir el tiempo de optimización:

//+------------------------------------------------------------------+
//|                                  TestSpeed_IndPrevCalculated.mq5 |
//|                                         Copyright 2011, AlexSTAL |
//|                                           http://www.alexstal.ru |
//+------------------------------------------------------------------+
#property copyright "Copyright 2011, AlexSTAL"
#property link      "http://www.alexstal.ru"
#property version   "1.00"
//--- connect the include file with the CustATR class
#include <ATRsample.mqh>
//--- set the selection of the parameter as an enumeration
enum eTestVariant
  {
   BuiltIn,    // Built-in indicator iATR
   Custom,     // Custom indicator iCustom("ATR")
   IndClass    // Calculation withing the class
  };
//--- input variables
input eTestVariant TestVariant;
input int          FalseParameter = 0;
//--- period of the ATR indicator
const uchar        InpAtrPeriod = 14;
//--- handle of the built-in or custom indicator
int                Handle;
//--- indicator based on the class 
CCustATR           *ATR;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //---
   switch(TestVariant)
     {
      case BuiltIn:
         Handle = iATR(_Symbol, _Period, InpAtrPeriod);
         break;
      case Custom:
         Handle = iCustom(_Symbol, _Period, "Examples\ATR", InpAtrPeriod);
         break;
      case IndClass:
         ATR = new CCustATR;
         ATR.Init(NULL, _Symbol, _Period, 100, CPCHSM_Normal, 0, 30, InpAtrPeriod);
         break;
     };
   //---
   return(0);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   switch(TestVariant)
     {
      case IndClass:
         delete ATR;
         break;
     };
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   double tmpValue[1];
   switch(TestVariant)
     {
      case BuiltIn:
         CopyBuffer(Handle, 0, 0, 1, tmpValue);
         break;
      case Custom:
         CopyBuffer(Handle, 0, 0, 1, tmpValue);
         break;
      case IndClass:
         ATR.Calculate();
         tmpValue[0] = ATR.GetATR(0, true);
         break;
     };
  }
//+------------------------------------------------------------------+

Como podemos observar, esta tecnología no disminuye el rendimiento en el Probador de Estrategias de forma significativa en compraración con el uso de un indicador personalizado ordinario.


Notas para el Uso Práctico de esta Tecnología


Conclusión

En cada situación, un programador debería considerar todas las ventajas y desventajas de diferentes variantes de implementación de la tarea. La implementación sugerida en este artículo es justamente una forma con sus propias ventajas y desventajas.

P.S. ¡Los errores son humanos! Si encuentra errores, por favor, no dude en informarme.