English Русский 中文 Deutsch 日本語 Português
Cálculo de expresiones matemáticas (Parte 2). Parsers de Pratt y shunting yard

Cálculo de expresiones matemáticas (Parte 2). Parsers de Pratt y shunting yard

MetaTrader 5Integración | 12 octubre 2020, 08:27
949 0
Stanislav Korotky
Stanislav Korotky

En este artículo, continuaremos analizando diferentes métodos de parseo de expresiones matemáticas y su implementación en el lenguaje MQL. En la primera parte, estudiamos los parsers de descenso recursivo. Su principal ventaja reside en que son un dispositivo comprensible a nivel intuitivo, que se relaciona con la gramática concreta de las expresiones. Pero si hablamos de efectividad y nivel tecnológico, existen otros tipos de parsers que merecen nuestra atención.

Parsers que usan la prioridad de los operadores

La siguiente variedad de parsers que analizaremos son los parsers con uso de prioridad (precedence) de los operadores. Se caracterizan por una implementación más compacta, gracias a que los métodos de las clases no se crean usando las reglas de la gramática (como vimos, en este caso, cada regla se transforma en su método), sino un tipo más general que tiene en cuenta solo la prioridad de los operadores.

La prioridad de las operaciones en su forma implícita ya estaba presente en las descripción de EBNF de la gramática: sus reglas se despliegan partiendo de las operaciones con menor prioridad hacia las operaciones con mayor prioridad, hasta las entidades del terminal, es decir, las constantes y variables. Esto sucede así porque la prioridad determina en qué sucesión deberán ejecutarse las operaciones en ausencia de una agrupación explícita con ayuda de paréntesis. Por ejemplo, la prioridad o precedencia de las operaciones de multiplicación es mayor que la prioridad de las de suma. Pero el menos unario tiene una prioridad mayor que la multiplicación. Cuando más abajo se encuentre un elemento del árbol de sintaxis en la raíz(expresión al completo), más tarde será calculado.

Para implementar los parsers, necesitaremos dos recuadros con valores numéricos que se correspondan con la prioridad de cada operación. Cuanto mayor sea el valor, mayor será la prioridad.

Hay dos recuadros, porque las operaciones unarias y binarias se distribuirán por los algoritmos de forma lógica. Siendo más estrictos, hablamos no solo de operaciones, sino también, en un sentido más amplio, de los caracteres que se pueden encontrar en las expresiones en forma de prefijos e infijos (podrá encontrar más información sobre los tipos de operador en la Wikipedia).

Como podemos comprender por el nombre, un prefijo es un carácter que precede al operando (por ejemplo, '!' en la expresión "!var"), mientras que un infijo es un carácter que se encuentra entre dos operandos (por ejemplo, '+' en la expresión "a + b"). También existen los postfijos (por ejemplo, la pareja '+' en el operador de incremento, que también existe en MQL, "i++"), pero no son utilizados en las expresiones, y por eso permanecerán fuera de plano.

Como prefijos, aparte de las operaciones unarias '!', '-', '+', pueden intervenir el paréntesis inicial '(' como signo de comienzo de grupo; una letra o un guión bajo como signo de identificador; y también un número o punto '.' como signo de constante numérica.

Vamos a describir los recuadros en la clase ExpressionPrecedence, de la cual se herederán clases concretas de parsers basadas en las prioridades. Todos estos parsers funcionarán usando como base Promise.

  class ExpressionPrecedence: public AbstractExpressionProcessor<Promise *>
  {
    protected:
      static uchar prefixes[128];
      static uchar infixes[128];
      
      static ExpressionPrecedence epinit;
      
      static void initPrecedence()
      {
        // grouping
        prefixes['('] = 9;
  
        // unary
        prefixes['+'] = 9;
        prefixes['-'] = 9;
        prefixes['!'] = 9;
        
        // identifiers
        prefixes['_'] = 9;
        for(uchar c = 'a'; c <= 'z'; c++)
        {
          prefixes[c] = 9;
        }
        
        // numbers
        prefixes['.'] = 9;
        for(uchar c = '0'; c <= '9'; c++)
        {
          prefixes[c] = 9;
        }
        
        // operators
        // infixes['('] = 9; // parenthesis is not used here as 'function call' operator
        infixes['*'] = 8;
        infixes['/'] = 8;
        infixes['%'] = 8;
        infixes['+'] = 7;
        infixes['-'] = 7;
        infixes['>'] = 6;
        infixes['<'] = 6;
        infixes['='] = 5;
        infixes['!'] = 5;
        infixes['&'] = 4;
        infixes['|'] = 4;
        infixes['?'] = 3;
        infixes[':'] = 2;
        infixes[','] = 1; // arg list delimiter
      }
  
      ExpressionPrecedence(const bool init)
      {
        initPrecedence();
      }
  
    public:
      ExpressionPrecedence(const string vars = NULL): AbstractExpressionProcessor(vars) {}
      ExpressionPrecedence(VariableTable &vt): AbstractExpressionProcessor(vt) {}
  };
  
  static uchar ExpressionPrecedence::prefixes[128] = {0};
  static uchar ExpressionPrecedence::infixes[128] = {0};
  static ExpressionPrecedence ExpressionPrecedence::epinit(true);

Los recuadros de prioridades se han implementado de una forma "ahorrativa", utilizando las matrices permitidas con un tamaño de 128 elementos (esto es suficiente, porque los caracteres con otros códigos de otros intervalos no tienen soporte). En las celdas que se corresponden con los códigos de los símbolos se indican sus prioridades. De esta forma, la prioridad se puede obtener mediante el direccionamiento directo según el código del token.

En las clase herederas también se requieren dos nuevos métodos auxiliares para comprobar los símbolos que siguen en la línea de entrada: _lookAhead simplemente retorna el token siguiente (como mirando un paso por delante), mientras que _matchNext lo calcula si coindice con lo esperado; en caso contrario, dará error.

  class ExpressionPrecedence: public AbstractExpressionProcessor<Promise *>
  {
    protected:
      ...
      ushort _lookAhead()
      {
        int i = 1;
        while(_index + i < _length && isspace(_expression[_index + i])) i++;
        if(_index + i < _length)
        {
          return _expression[_index + i];
        }
        return 0;
      }
      
      void _matchNext(ushort c, string message, string context = NULL)
      {
        if(_lookAhead() == c)
        {
          _nextToken();
        }
        else if(!_failed) // prevent chained errors
        {
          error(message, context);
        }
      }
      ...
  };

El parser de Pratt será el primer parser basado en la prioridad de las operaciones que vamos a analizar.

Parser de Pratt (ExpressionPratt)

El parser de Pratt es descendente, como el parser de descenso recursivo. Esto significa que también contendrá llamadas recursivas a ciertos métodos que analizan construcciones aparte dentro de la expresión, pero estos el número de dichos métodos será mucho menor.

Los constructores y el método público principal evaluate tienen un aspecto familiar.

  class ExpressionPratt: public ExpressionPrecedence
  {
    public:
      ExpressionPratt(const string vars = NULL): ExpressionPrecedence(vars) { helper = new ExpressionHelperPromise(&this); }
      ExpressionPratt(VariableTable &vt): ExpressionPrecedence(vt) { helper = new ExpressionHelperPromise(&this); }
  
      virtual Promise *evaluate(const string expression) override
      {
        Promise::environment(&this);
        AbstractExpressionProcessor<Promise *>::evaluate(expression);
        if(_length > 0)
        {
          return parseExpression();
        }
        return NULL;
      }

El nuevo método parseExpression es el corazón del algoritmo de Pratt. Este comienza estableciendo la prioridad actual igual por defecto a 0, lo cual indica la posibilidad de leer cualquier carácter.

      virtual Promise *parseExpression(const int precedence = 0)
      {
        if(_failed) return NULL; // cut off subexpressions in case of errors
      
        _nextToken();
        if(prefixes[(uchar)_token] == 0)
        {
          this.error("Can't parse " + ShortToString(_token), __FUNCTION__);
          return NULL;
        }
        
        Promise *left = _parsePrefix();
        
        while((precedence < infixes[_token]) && !_failed)
        {
          left = _parseInfix(left, infixes[(uchar)_token]);
        }
        
        return left;
      }

La esencia del método es simple: comenzamos el análisis de la expresión leyendo el próximo carácter obligado a ser prefijo (de lo contrario, tendremos error), y transmitimos el control al método _parsePrefix, que sabe leer por completo cualquier construcción con prefijo. Después de ello, llega el turno de la prioridad del carácter que se encuentra por encima de la prioridad actual; así, transmitimos el control al método _parseInfix, que sabe leer por completo cualquier construcción con infijo. De esta forma, el parser completo consta de solo 3 métodos. En cierto sentido, el parser de Pratt representa la expresión como una jerarquía de construcciones de prefijos e infijos.

Preste atención: si el carácter _token actual no se encuentra en el recuadro de infijos, su prioridad será igual a cero, y el ciclo while dejará de ejecutarse (o no comenzará en absoluto).

El truco del método _parseInfix consiste en que el objeto Promise (left) actual se transmite dentro en el primer parámetro y se convierte en parte de la subexpresión, mientras que la prioridad mínima de las operaciones que se permiten calcular al método se establece en el segundo parámetro, como prioridad del token de infijo actual. El método retornará el nuevo objeto Promise para toda la subexpresión, además, este se guardará en la misma variable (y el anterior enlace a Promise estará de una u otra forma disponible en los campos de enlace del nuevo objeto).

Veamos cómo están construidos los métodos _parsePrefix y _parseInfix.

Resulta lógico que _parsePrefix espere el token actual desde el número de prefijos permitidos y procese estos con la ayuda de switch. El caso del paréntisis inicial '(', se llama el método parseExpression, que ya conocemos, para calcular la expresión incorporada. El parámetro de prioridad ha sido omitido, lo cual implica el parseo con la prioridad más baja (ya que entre paréntesis se cuentra, en esencia, una expresión aparte). En el caso de '!', el objeto helper se usa para obtener la negación lógica del fragmento que viene a continuación. Nuevamente, será el método parseExpression el encargado de leerlo, pero esta vez, en el interior se transmite la prioridad del token actual. Esto significa que el fragmento sometido a negación se terminará antes del primer carácter con una prioridad más baja que '!'. Por ejemplo, si en la expresión tenemos "!a*b", parseExpression se convertirá después de la lectura del nombre en la variable 'a', porque la prioridad de la multipliación '*' es inferior a la negación '!'. Los '+' y '-' unarios se procesan de forma semejante, pero aquí nos las arreglaremos sin el objeto helper. Para '+', bastará con leer la subexpresión en parseExpression; respecto a '-', llamaremos para el resultado obtenido al operador predefinido "menos" (recordemos que los resultados suponen objetos Promise).

Todos los demás caracteres son clasificados por el método _parsePrefix según la pertenencia a la categoría isalpha. Obviamente, la letra será el inicio del indicador, mientras que la cifra o el punto será el inicio del número. En todos los demás casos, el método retornará NULL.

      Promise *_parsePrefix()
      {
        Promise *result = NULL;
        switch(_token)
        {
          case '(':
            result = parseExpression();
            _match(')', ") expected!", __FUNCTION__);
            break;
          case '!':
            result = helper._negate(parseExpression(prefixes[_token]));
            break;
          case '+':
            result = parseExpression(prefixes[_token]);
            break;
          case '-':
            result = -parseExpression(prefixes[_token]);
            break;
          default:
            if(isalpha(_token))
            {
              string variable;
            
              while(isalnum(_token))
              {
                variable += ShortToString(_token);
                _nextToken();
              }
              
              if(_token == '(')
              {
                const string name = variable;
                const int index = _functionTable.index(name);
                if(index == -1)
                {
                  error("Function undefined: " + name, __FUNCTION__);
                  return NULL;
                }
                
                const int arity = _functionTable[index].arity();
                if(arity > 0 && _lookAhead() == ')')
                {
                  error("Missing arguments for " + name + ", " + (string)arity + " required!", __FUNCTION__);
                  return NULL;
                }
                
                Promise *params[];
                ArrayResize(params, arity);
                for(int i = 0; i < arity; i++)
                {
                  params[i] = parseExpression(infixes[',']);
                  if(i < arity - 1)
                  {
                    if(_token != ',')
                    {
                      _match(',', ", expected (param-list)!", __FUNCTION__);
                      break;
                    }
                  }
                }
              
                _match(')', ") expected after " + (string)arity + " arguments!", __FUNCTION__);
                
                result = helper._call(index, params);
              }
              else
              {
                return helper._variable(variable); // get index and if not found - optionally reserve the name with nan
              }
            }
            else // digits are implied, must be a number 
            {
              string number;
              if(_readNumber(number))
              {
                return helper._literal(number);
              }
            }
        }
        return result;
      }

El identificador al que sigue el paréntesis '(', se percibe como la llamada de una función. Para este, se parsea opcionalmente la lista de argumentos (de acuerdo con la aridad de la función) separados por comas. Cada argumento se obtiene gracias a la llamada de parseExpression con una prioridad de coma ','. El objeto Promise para la función se genera con la ayuda de helper._call(). Si no hay paréntesis después del identificador, se creará el objeto Promise para la variable helper._variable().

Cuando el primer token no es una letra, el método _parsePrefix intenta leer un número con la ayuda de _readNumber, y crea para él un objeto Promise llamando a helper._literal().

El método _parseInfix espera que el token actual sea uno de los infijos permitidos, además, en el primer parámetro obtiene el operando izquierdo, ya leído en el objeto Promise *left. En el segundo parámetro, se indica la prioridad mínima de los tokens que van a ser analizados: en cuanto se encuentre cualquier cosa con una prioridad inferior, la subexpresión finalizará. La tarea de _parseInfix consiste en llamar a parseExpression con la prioridad precedence para leer el operando correcto, después de lo cual, podremos crear el objeto Promise para la operación binaria que corresponda al índice.

      Promise *_parseInfix(Promise *left, const int precedence = 0)
      {
        Promise *result = NULL;
        const ushort _previous = _token;
        switch(_previous)
        {
          case '*':
          case '/':
          case '%':
          case '+':
          case '-':
            result = new Promise((uchar)_previous, left, parseExpression(precedence));
            break;
          case '>':
          case '<':
            if(_lookAhead() == '=')
            {
              _nextToken();
              result = new Promise((uchar)(_previous == '<' ? '{' : '}'), left, parseExpression(precedence));
            }
            else
            {
              result = new Promise((uchar)_previous, left, parseExpression(precedence));
            }
            break;
          case '=':
          case '!':
            _matchNext('=', "= expected after " + ShortToString(_previous), __FUNCTION__);
            result = helper._isEqual(left, parseExpression(precedence), _previous == '=');
            break;
          case '&':
          case '|':
            _matchNext(_previous, ShortToString(_previous) + " expected after " + ShortToString(_previous), __FUNCTION__);
            result = new Promise((uchar)_previous, left, parseExpression(precedence));
            break;
          case '?':
            {
              Promise *truly = parseExpression(infixes[':']);
              if(_token != ':')
              {
                _match(':', ": expected", __FUNCTION__);
              }
              else
              {
                Promise *falsy = parseExpression(infixes[':']);
                if(truly != NULL && falsy != NULL)
                {
                  result = helper._ternary(left, truly, falsy);
                }
              }
            }
          case ':':
          case ',': // just skip
            break;
          default:
            error("Can't process infix token " + ShortToString(_previous));
          
        }
        return result;
      }

Es importante que el token de infijo actual al inicio del método se anote en la variable _previous. Esto se ha hace así porque la llamada a parseExpression, en caso de éxito, desplaza la posición en la línea a otro token en un número aleatorio de caracteres a la derecha.

Así, hemos analizado solo 3 métodos con una estructura bastante transparente, pero esto es precisamente el analizador Pratt en su conjunto.

Su uso es similar al del parser ExpressionCompiler: creamos el objeto ExpressionPratt, indicamos el recuadro de variables, iniciamos el método evaluate para la línea con la expresión y obtenemos en la salida el objeto Promise con un árbol de sintaxis que se puede calcular usando resolve().

La utilización de un árbol de sintaxis no es, obviamente, el único método para realizar el cálculo diferido de expresiones. El siguiente tipo de parser que vamos a analizar no necesita el árbol, y escribe el algoritmo de cálculo en lo que conocemos como código de bytes. Por consiguiente, primero debemos familiarizarnos con esta tecnología.

Generando el código de bytes

Un código de bytes es una secuencia de comandos que describen en una representación binaria "rápida" un algoritmo de cálculo completo. La creación de un código de bytes se parece a la compilación real de un programa, pero el resultado no contiene instrucciones de procesador, sino variables o estructuras de lenguaje aplicado que controlan una determinada clase-calculadora. En nuestro caso, la unidad de ejecución será una estructura de ByteCode de este tipo.

  struct ByteCode
  {
      uchar code;
      double value;
      int index;
  
      ByteCode(): code(0), value(0.0), index(-1) {}
      ByteCode(const uchar c): code(c), value(0.0), index(-1) {}
      ByteCode(const double d): code('n'), value(d), index(-1) {}
      ByteCode(const uchar c, const int i): code(c), value(0.0), index(i) {}
      
      string toString() const
      {
        return StringFormat("%s %f %d", CharToString(code), value, index);
      }
  };

Aunque sus campos repiten los campos de los objetos Promise, incluyen solo el conjunto mínimo imprescindible para los cálculos "de flujo". Son de flujo porque, durante el funcionamiento del comando, se calcularán y ejecutarán secuencialmente de izquierda a derecha sin pasar por ninguna estructura jerárquica.

El campo code contiene la esencia del comando (el valor se corresponde con los códigos de Promise), el campo value, un número (constante), el campo index, el número de la variable o función en los recuadros, propiamente, de variables o funciones.

Uno de los métodos para introducir los datos de cálculo es la notación polaca inversa (Reverse Polish Notation), conocida también como notación postfija. Su esencia reside en que los operandos se anotan primero, y luego el código de operación. Por ejemplo, la notación de infijo acostumbrada para nosotros "a + b" se convierte en notación de postfijo "a b +", y el caso "a + b * sqrt(c)", más complejo, se transforma en "a b c 'sqrt' * +".

La RPN es buena para el código de bytes porque con ella resulta sencillo implementar los cálculos con la ayuda de una pila. Cuando un programa "ve" en el flujo de caracteres de entrada un número o un enlace a una variable, coloca dicho valor en la pila. Si en el flujo de entrada hay un operador o una función, el programa extrae de la pila el número de valores requerido, ejecuta con ellos la operación determinada y coloca de nuevo el resultado en la pila. Tras finalizar del proceso, el resultado del cálculo de la expresión quedará como el único número en la pila.

Como la RPN describe de forma alternativa las mismas expresiones para las que construimos los árboles de sintaxis, estas dos representaciones se pueden convertir mutuamente. Vamos a intentar generar un código de bytes basado en el árbol de sintaxis de Promise. Para ello, añadiremos a la clase de Promise el método exportToByteCode.

  class Promise
  {
    ...
    public:
      void exportToByteCode(ByteCode &codes[])
      {
        if(left) left.exportToByteCode(codes);
        const int truly = ArraySize(codes);
        
        if(code == '?')
        {
          ArrayResize(codes, truly + 1);
          codes[truly].code = code;
        }
        
        if(right) right.exportToByteCode(codes);
        const int falsy = ArraySize(codes);
        if(last) last.exportToByteCode(codes);
        const int n = ArraySize(codes);
        
        if(code != '?')
        {
          ArrayResize(codes, n + 1);
          codes[n].code = code;
          codes[n].value = value;
          codes[n].index = index;
        }
        else // (code == '?')
        {
          codes[truly].index = falsy; // jump over true branch
          codes[truly].value = n;     // jump over both branches
        }
      }
      ...
  };

El método obtiene como parámetro la matriz de estructuras ByteCode, en la que debe guardar el contenido del objeto Promise actual. Primero, se analizan todos los nodos subordinados: para ello, se llama al método de forma recursiva para los punteros left, right, last, si no son iguales a cero. Y solo después de ello, cuando todas las partes (operandos) han sido guardadas, las propiedades del objeto Promise se escribirán en el código de bytes.

Dado que la gramática de las expresiones contiene un operador condicional, el método también registra el tamaño de la matriz del código de bytes en los puntos donde empiezan las ramas de las instrucciones verdadera y falsa, así como el final de la expresión condicional. Esto permite anotar en la estructura de código de bytes del operador condicional el desplazamiento en la matriz a donde se debe saltar durante el proceso de cálculo, si la condición es verdadera o falsa. La rama de instrucciones para la condición verdadera empieza justo después del código de bytes '?', y tras su ejecución, debemos pasar al desplazamiento en el campo value. La rama de instrucciones para la condición falsa empieza con el desplazamiento en el campo index, justo después de las instrucciones "verdaderas".

Debemos tener en cuenta que, al calcular una expresión en el modo de interpretación o mediante un árbol de sintaxis, ambas ramas del operador condicional se calculan antes de que se elija uno de sus valores según la condición, es decir, una de las ramas se considera inactiva. En el caso del código de bytes, omitiremos los cálculos de la rama innecesaria.

Para convertir el árbol completo de la expresión en código de bytes, llamaremos a exportToByteCode para el objeto raíz obtenido como resultado de evaluate. Por ejemplo, para el parser de Pratt:

    ExpressionPratt e(vars);
    Promise *p = e.evaluate(expr);
  
    ByteCode codes[];
    p.exportToByteCode(codes);
  
    for(int i = 0; i < ArraySize(codes); i++)
    {
      Print(i, "] ", codes[i].toString());
    }

Ahora, queda escribir una función que ejecute los cálculos basados en el código de bytes. La ubicaremos igualmente en la clase de Promise, porque en los códigos de bytes se usan índices de variables y funciones, mientras que Promise por defecto tiene enlaces a estos recuadros.

  #define STACK_SIZE 100
  
  // stack imitation
  #define push(S,V,N) S[N++] = V
  #define pop(S,N) S[--N]
  #define top(S,N) S[N-1]
  
  class Promise
  {
    ...
    public:
      static double execute(const ByteCode &codes[], VariableTable *vt = NULL, FunctionTable *ft = NULL)
      {
        if(vt) variableTable = vt;
        if(ft) functionTable = ft;
  
        double stack[]; int ssize = 0; ArrayResize(stack, STACK_SIZE);
        int jumps[]; int jsize = 0; ArrayResize(jumps, STACK_SIZE / 2);
        const int n = ArraySize(codes);
        for(int i = 0; i < n; i++)
        {
          if(jsize && top(jumps, jsize) == i)
          {
            --jsize; // fast "pop & drop"
            i = pop(jumps, jsize);
            continue;

          }
          switch(codes[i].code)
          {
            case 'n': push(stack, codes[i].value, ssize); break;
            case 'v': push(stack, variableTable[codes[i].index], ssize); break;
            case 'f':
              {
                IFunctor *ptr = functionTable[codes[i].index];
                double params[]; ArrayResize(params, ptr.arity()); int psize = 0;
                for(int j = 0; j < ptr.arity(); j++)
                {
                  push(params, pop(stack, ssize), psize);
                }
                ArrayReverse(params);
                push(stack, ptr.execute(params), ssize);
              }
              break;
            case '+': push(stack, pop(stack, ssize) + pop(stack, ssize), ssize); break;
            case '-': push(stack, -pop(stack, ssize) + pop(stack, ssize), ssize); break;
            case '*': push(stack, pop(stack, ssize) * pop(stack, ssize), ssize); break;
            case '/': push(stack, Promise::safeDivide(1, pop(stack, ssize)) * pop(stack, ssize), ssize); break;
            case '%':
              {
                const double second = pop(stack, ssize);
                const double first = pop(stack, ssize);
                push(stack, fmod(first, second), ssize);
              }
              break;
            case '!': push(stack, (double)(!pop(stack, ssize)), ssize); break;
            case '~': push(stack, (double)(-pop(stack, ssize)), ssize); break;
            case '<':
              {
                const double second = pop(stack, ssize);
                const double first = pop(stack, ssize);
                push(stack, (double)(first < second), ssize);
              }
              break;
            case '>':
              {
                const double second = pop(stack, ssize);
                const double first = pop(stack, ssize);
                push(stack, (double)(first > second), ssize);
              }
              break;
            case '{':
              {
                const double second = pop(stack, ssize);
                const double first = pop(stack, ssize);
                push(stack, (double)(first <= second), ssize);
              }
              break;
            case '}':
              {
                const double second = pop(stack, ssize);
                const double first = pop(stack, ssize);
                push(stack, (double)(first >= second), ssize);
              }
              break;
            case '&': push(stack, (double)(pop(stack, ssize) && pop(stack, ssize)), ssize); break;
            case '|':
              {
                const double second = pop(stack, ssize);
                const double first = pop(stack, ssize);
                push(stack, (double)(first || second), ssize); // order is important
              }
              break;
            case '`': push(stack, _precision < fabs(pop(stack, ssize) - pop(stack, ssize)), ssize); break;
            case '=': push(stack, _precision > fabs(pop(stack, ssize) - pop(stack, ssize)), ssize); break;
            case '?':
              {
                const double first = pop(stack, ssize);
                if(first) // true
                {
                  push(jumps, (int)codes[i].value, jsize); // to where the entire if ends
                  push(jumps, codes[i].index, jsize);      // we jump from where true ends
                }
                else // false
                {
                  i = codes[i].index - 1; // -1 is needed because of forthcoming ++
                }
              }
              break;
            default:
              Print("Unknown byte code ", CharToString(codes[i].code));
          }
        }
        return pop(stack, ssize);
      }
      ...
  };

El trabajo con la pila se ha organizado con la ayuda de una macro en la matriz stack, en la que previamente se reserva el número predeterminado de elementos STACK_SIZE. Esto se hace así para lograr una mayor velocidad gracias a la exclusión de las llamadas a ArrayResize al ejecutar las operaciones push y pop. Un STACK_SIZE igual a 100 parece suficiente para la mayoría de expresiones unidireccionales reales. En caso contrario, la pila se desbordará.

Para controlar la ejecución de los operadores condicionales que puedan estar anidados, deberemos usar la pila adicional jumps.

Todas las operaciones ya nos son familiares gracias a los códigos de Promise y el parser de Pratt que analizamos anteriormente. La única diferencia reside en el uso generalizado de la pila como fuente de operandos y como lugar para almacenar los resultados intermedios. La ejecución del código de bytes al completo tiene lugar en un ciclo, con una única llamada al método y sin recursividad.

Disponiendo de esta funcionalidad, ya podemos calcular expresiones utilizando el código de bytes exportando árboles de sintaxis desde el parser de Pratt o ExpressionCompiler.

    ExpressionPratt e(vars);
    Promise *p = e.evaluate(expr);
  
    ByteCode codes[];
    p.exportToByteCode(codes);
    double r = Promise::execute(codes);

Un poco más tarde, al poner a prueba todos los parsers, compararemos la velocidad de los cálculos con el árbol y el código de bytes.

Pero el objetivo principal de la introducción del código de bytes es hacer posible la implementación de otro tipo de parser, el parser de "shunting yard".

Parser de shunting yard (ExpressionShuntingYard)

El parser de shunting yard (Shunting Yard) debe su nombre al modo en que separa el flujo de tokens de entrada entre los que se pueden pasar inmediatamente a la salida y los que deben colocarse en una pila especial, desde donde se extraen los tokens según normas especiales para combinar las prioridades de los mismos (lo que está en la pila y lo que sigue en el flujo de entrada). El parser transforma la expresión de entrada en notación polaca inversa (RPN). Para nosotros, esto resulta conveniente porque podemos generar código de bytes de inmediato, sin pasar por el árbol de sintaxis. Como podemos ver por la descripción general, el método de clasificación se basa en la prioridad de los operadores, por consiguiente, este parser se relaciona con el parser de Pratt, y se implementará como la clase heredera ExpressionPrecedence.

Este parser pertenece a la clase ascendente (bottom-up).

En líneas generales, el algoritmo es el siguiente (aquí omitimos los detalles sobre las asociatividad hacia la derecha, dado que no la tenemos; asimismo, omitimos las complicaciones relacionadas con el operador condicional ternario):

  En un ciclo, leemos el siguiente token de la expresión (hasta que no se termine)
    si el token -- es una operación unaria, la guardamos en la pila
    si se trata de una cifra, la anotamos en código de bytes
    si se trata de una variable, anotamos su índice en código de bytes
    si se trata de un identificador de función, guardamos su índice en la pila
    si el token -- es un operador de infijos
      mientras en la cima de la pila no se encuentre '(' y ((la prioridad del operador en la cima de la pila >= la prioridad del operador actual) o en la cima de la función)
        trasladamos la cima de la pila a código de bytes de salida
      guardar operador en la pila
    si el token es '(', lo guardamos en la pila
    si el token es ')'
      mientras en la cima de la pila no se encuentre '('
        trasladamos la cima de la pila a código de bytes de salida
      si en la cima de la pila se encuentra '(', extraemos y descartamos
  si en la pila quedan tokens, los trasladamos secuencialmente al código de bytes de salida

Obviamente, la implementación de este parser requerirá un único método.

Más abajo, la clase ExpressionShuntingYard se muestra al completo. El método público principal convertToByteCode inicia el parseo, que se realiza en exportToByteCode. Como nuestras expresiones dan soporte a operadores condicionales, para analizar su subexpresión, se usa la llamada recursiva a exportToByteCode.

  class ExpressionShuntingYard: public ExpressionPrecedence
  {
    public:
      ExpressionShuntingYard(const string vars = NULL): ExpressionPrecedence(vars) { }
      ExpressionShuntingYard(VariableTable &vt): ExpressionPrecedence(vt) { }
  
      bool convertToByteCode(const string expression, ByteCode &codes[])
      {
        Promise::environment(&this);
        AbstractExpressionProcessor<Promise *>::evaluate(expression);
        if(_length > 0)
        {
          exportToByteCode(codes);
        }
        return !_failed;
      }
  
    protected:
      template<typename T>
      static void _push(T &stack[], T &value)
      {
        const int n = ArraySize(stack);
        ArrayResize(stack, n + 1, STACK_SIZE);
        stack[n] = value;
      }
  
      void exportToByteCode(ByteCode &output[])
      {
        ByteCode stack[];
        int ssize = 0;
        string number;
        uchar c;
        
        ArrayResize(stack, STACK_SIZE);
        
        const int previous = ArraySize(output);
        
        while(_nextToken() && !_failed)
        {
          if(_token == '+' || _token == '-' || _token == '!')
          {
            if(_token == '-')
            {
              _push(output, ByteCode(-1.0));
              push(stack, ByteCode('*'), ssize);
            }
            else if(_token == '!')
            {
              push(stack, ByteCode('!'), ssize);
            }
            continue;
          }
          
          number = "";
          if(_readNumber(number)) // if a number was read, _token has changed
          {
            _push(output, ByteCode(StringToDouble(number)));
          }
          
          if(isalpha(_token))
          {
            string variable;
            while(isalnum(_token))
            {
              variable += ShortToString(_token);
              _nextToken();
            }
            if(_token == '(')
            {
              push(stack, ByteCode('f', _functionTable.index(variable)), ssize);
            }
            else // variable name
            {
              int index = -1;
              if(CheckPointer(_variableTable) != POINTER_INVALID)
              {
                index = _variableTable.index(variable);
                if(index == -1)
                {
                  if(_variableTable.adhocAllocation())
                  {
                    index = _variableTable.add(variable, nan);
                    _push(output, ByteCode('v', index));
                    error("Unknown variable is NaN: " + variable, __FUNCTION__, true);
                  }
                  else
                  {
                    error("Unknown variable : " + variable, __FUNCTION__);
                  }
                }
                else
                {
                  _push(output, ByteCode('v', index));
                }
              }
            }
          }
          
          if(infixes[_token] > 0) // operator, including least significant '?'
          {
            while(ssize > 0 && isTop2Pop(top(stack, ssize).code))
            {
              _push(output, pop(stack, ssize));
            }
            
            if(_token == '?' || _token == ':')
            {
              if(_token == '?')
              {
                const int start = ArraySize(output);
                _push(output, ByteCode((uchar)_token));
                exportToByteCode(output); // subexpression truly, _token has changed
                if(_token != ':')
                {
                  error("Colon expected, given: " + ShortToString(_token), __FUNCTION__);
                  break;
                }
                output[start].index = ArraySize(output);
                exportToByteCode(output); // subexpression falsy, _token has changed
                output[start].value = ArraySize(output);
                if(_token == ':')
                {
                  break;
                }
              }
              else
              {
                break;
              }
            }
            else
            {
              if(_token == '>' || _token == '<')
              {
                if(_lookAhead() == '=')
                {
                  push(stack, ByteCode((uchar)(_token == '<' ? '{' : '}')), ssize);
                  _nextToken();
                }
                else
                {
                  push(stack, ByteCode((uchar)_token), ssize);
                }
              }
              else if(_token == '=' || _token == '!')
              {
                if(_lookAhead() == '=')
                {
                  push(stack, ByteCode((uchar)(_token == '!' ? '`' : '=')), ssize);
                  _nextToken();
                }
              }
              else if(_token == '&' || _token == '|')
              {
                _matchNext(_token, ShortToString(_token) + " expected after " + ShortToString(_token), __FUNCTION__);
                push(stack, ByteCode((uchar)_token), ssize);
              }
              else if(_token != ',')
              {
                push(stack, ByteCode((uchar)_token), ssize);
              }
            }
          }
          
          if(_token == '(')
          {
            push(stack, ByteCode('('), ssize);
          }
          else if(_token == ')')
          {
            while(ssize > 0 && (c = top(stack, ssize).code) != '(')
            {
              _push(output, pop(stack, ssize));
            }
            if(c == '(') // must be true unless it's a subexpression (then 'c' can be 0)
            {
              ByteCode disable_warning = pop(stack, ssize);
            }
            else
            {
              if(previous == 0)
              {
                error("Closing parenthesis is missing", __FUNCTION__);
              }
              return;
            }
          }
        }
        
        while(ssize > 0)
        {
          _push(output, pop(stack, ssize));
        }
      }
      
      bool isTop2Pop(const uchar c)
      {
        return (c == 'f' || infixes[c] >= infixes[_token]) && c != '(' && c != ':';
      }
  };

El uso del parser de clasificación se distingue de los tipos anteriores. Para él, se excluye el paso de la obtención del árbol con la ayuda de la llamada a evaluate. En lugar de esto, el método convertToByteCode retorna de inmediato el código de bytes para la expresión transmitida.

  ExpressionShuntingYard sh;
  sh.variableTable().adhocAllocation(true);
  
  ByteCode codes[];
  bool success = sh.convertToByteCode("x + y", codes);
  if(success)
  {
    sh.variableTable().assign("x=10;y=20");
    double r = Promise::execute(codes);
  }

Con esto, damos por finalizado el análisis de los tipos de parsers. El diagrama de clases tiene el aspecto que sigue.

Diagrama de clases de los parsers

Diagrama de clases de los parsers

Para poner a prueba y comparar diferentes parsers, crearemos un script de prueba más adelante.

Para aproximar la tarea al campo aplicado del trading, añadiremos el toque final: vamos a mostrar cómo la lista de funciones integradas se puede complementar con indicadores técnicos.

Incorporando indicadores en expresiones en calidad de funciones

Al calcular expresiones, el tráder puede necesitar información específica, como el balance concreto, el número de posiciones, los valores de los indicadores, etcétera. Todo esto puede hacerse disponible dentro de las expresiones ampliando la lista de funciones incorporadas. Para mostrar este enfoque, vamos a añadir un indicador de media móvil al conjunto de funciones.

El mecanismo para insertar un indicador en las expresiones se basa en los functores que hemos analizado anteriormente y, por consiguiente, se implementa como una clase derivada de AbstractFunc. Debemos recordar que todas las instancias de las clases de la familia AbstractFunc se registran automáticamente con AbstractFuncStorage, y se encuentran disponibles en el recuadro de funciones.

  class IndicatorFunc: public AbstractFunc
  {
    public:
      IndicatorFunc(const string n, const int a = 1): AbstractFunc(n, a)
      {
        // the single argument is the bar number,
        // two arguments are bar number and buffer index
      }
      static IndicatorFunc *create(const string name);
  };

Una peculiaridad de los indicadores en MetaTrader 5 es que necesitan dos etapas para su aplicación: primero se debe crear el indicador (debemos obtener su descriptor), y solo entonces podremos solicitarle los datos. En el contexto del procesamiento de expresiones, el primer paso debe efectuarse durante el análisis, y el segundo, durante los cálculos. Dado que la creación del indicador requiere que especifiquemos todos sus parámetros, estos deberán codificarse en el nombre, y no transmitirse en los parámetros de la función. Por ejemplo, si creásemos la función "iMA" con parámetros (periodo, método, tipo_de_precio), en la etapa de análisis obtendríamos solo su nombre, y la definición de los parámetros se pospondría hasta el estadio de ejecución, cuando es demasiado tarde para crear un indicador (puesto que ya es hora de leer sus datos).

Debido a ello, hemos decidido reservar un conjunto completo de nombres para el indicador de media móvil, creados según la siguiente norma: método_precio_periodo. Aquí, el método supone una de las palabras significativas de la enumeración ENUM_MA_METHOD (SMA, EMA, SMMA, LWMA), el precio es uno de los tipos de precios de la enumeración ENUM_APPLIED_PRICE (CLOSE, OPEN, HIGH, LOW, MEDIAN, TYPICAL, WEIGHTED), y el periodo es un número entero. Por consiguiente, al utilizar la función "SMA_OPEN_10", se debería crear una media móvil simple basada en los precios de apertura con un periodo de 10.

La aridad de la función del indicador será 1 por defecto. En el único parámetro se transmite el número de barra. Si indicamos una aridad de 2, se supone que el segundo parámetro indicará el número de búfer. No lo vamos a necesitar en absoluto para la media móvil.

La clase MAIndicatorFunc es responsable de crear instancias de indicadores con parámetros que se correspondan con los nombres solicitados.

  class MAIndicatorFunc: public IndicatorFunc
  {
    protected:
      const int handle;
      
    public:
      MAIndicatorFunc(const string n, const int h): IndicatorFunc(n), handle(h) {}
      
      ~MAIndicatorFunc()
      {
        IndicatorRelease(handle);
      }
      
      static MAIndicatorFunc *create(const string name) // SMA_OPEN_10(0)
      {
        string parts[];
        if(StringSplit(name, '_', parts) != 3) return NULL;
        
        ENUM_MA_METHOD m = -1;
        ENUM_APPLIED_PRICE t = -1;
        
        static string methods[] = {"SMA", "EMA", "SMMA", "LWMA"};
        for(int i = 0; i < ArraySize(methods); i++)
        {
          if(parts[0] == methods[i])
          {
            m = (ENUM_MA_METHOD)i;
            break;
          }
        }
  
        static string types[] = {"NULL", "CLOSE", "OPEN", "HIGH", "LOW", "MEDIAN", "TYPICAL", "WEIGHTED"};
        for(int i = 1; i < ArraySize(types); i++)
        {
          if(parts[1] == types[i])
          {
            t = (ENUM_APPLIED_PRICE)i;
            break;
          }
        }
        
        if(m == -1 || t == -1) return NULL;
        
        int h = iMA(_Symbol, _Period, (int)StringToInteger(parts[2]), 0, m, t);
        if(h == INVALID_HANDLE) return NULL;
        
        return new MAIndicatorFunc(name, h);
      }
      
      double execute(const double &params[]) override
      {
        const int bar = (int)params[0];
        double result[1] = {0};
        if(CopyBuffer(handle, 0, bar, 1, result) != 1)
        {
          Print("CopyBuffer error: ", GetLastError());
        }
        return result[0];
      }
  };

El método de fábrica create analiza el nombre que le es transmitido, extrae de él los parámetros y crea un indicador con el descriptor handle. El valor del indicador se obtiene en el método de functor estándar: execute.

Como se pueden añadir otros indicadores a la función en el futuro, la clase IndicatorFunc ofrece un único punto de entrada para las solicitudes de cualquier indicador: el método create. Hasta ahora, este solo contiene una redirección a la llamada MAIndicatorFunc::create().

  static IndicatorFunc *IndicatorFunc::create(const string name)
  {
    // TODO: support more indicator types, dispatch calls based on the name
    return MAIndicatorFunc::create(name);
  }

Este método debe ser obligatoriamente renombrado desde el recuadro de funciones, por eso, vamos a completar la clase FunctionTable.

  class FunctionTable: public Table<IFunctor *>
  {
    public:
      ...
      #ifdef INDICATOR_FUNCTORS
      virtual int index(const string name) override
      {
        int i = _table.getIndex(name);
        if(i == -1)
        {
          i = _table.getSize();
          IFunctor *f = IndicatorFunc::create(name);
          if(f)
          {
            Table<IFunctor *>::add(name, f);
            return i;
          }
          return -1;
        }
        return i;
      }
      #endif
  };

Si no se ha encontrado el nombre transmitido en la lista con las 25 funciones incorporadas, la nueva versión del método index intentará encontrar el indicador adecuado. Para incluir esta funcionalidad adicional, necesitaremos definir la macro INDICATOR_FUNCTORS.

Con la opción activada, podremos calcular, por ejemplo, esta expresión: "EMA_OPEN_10(0)/EMA_OPEN_21(0)".

En la práctica, los parámetros de los indicadores llamados se sacan a los ajustes. Esto significa que estos, de alguna forma, deberán incluirse dinámicamente en la línea con la expresión. Para simplificar esta tarea, la clase AbstractExpressionProcessor soporta una opción especial de preprocesamiento de la expresión. Hemos omitido este detalle para abreviar la explicación. De la activación del preprocesamiento se encarga un segundo parámetro -no obligatorio- del método evaluate (por defecto, es igual a false — el preprocesamiento está desactivado).

El principio de funcionamiento de esta opción es el siguiente. Dentro de la expresión, podemos indicar con corchetes el nombre de la variable a sustituir por el valor de la variable antes del análisis. Por ejemplo, si la expresión es igual a "EMA_TYPICAL_{Period}(0)" y en el recuadro de variables tenemos una variable Period con un valor de 11, se analizará la expresión "EMA_TYPICAL_11(0)".

Para poner a prueba las funciones del indicador, crearemos posteriormente un asesor experto cuyas señales comerciales se formarán usando como base las expresiones calculadas, incluida la media móvil.

Pero antes, debemos asegurarnos de que los parsers funcionen correctamente.

Script de prueba (ExpresSParserS)

El script de prueba ExpresSParserS.mq5 incluye un conjunto de pruebas funcionales, y también la medición de la velocidad de los cálculos para los 4 tipos de parsers. Asimismo, incluye la demostración de los distintos modos, la muestra del árbol de sintaxis y el código de bytes en el log, y el uso de indicadores como funciones incorporadas.

Entre las pruebas funcionales, existen tanto expresiones correctas como funciones que ya sabemos erróneas (variables no declaradas, división por cero en la etapa de cálculo, etcétera). La exactitud de la prueba se definirá según la correspondencia entre los resultados reales y esperados, es decir, los errores también pueden ser "correctos". Por ejemplo, este es el aspecto del log de prueba del parser de Pratt.

  Running 19 tests on ExpressionPratt* …
  1 passed, ok: a > b ? b > c ? 1 : 2 : 3 = 3.0; expected = 3.0
  2 passed, ok: 2 > 3 ? 2 : 3 > 4 ? 3 : 4 = 4.0; expected = 4.0
  3 passed, ok: 4 > 3 ? 2 > 4 ? 2 : 4 : 3 = 4.0; expected = 4.0
  4 passed, ok: (a + b) * sqrt(c) = 8.944271909999159; expected = 8.944271909999159
  5 passed, ok: (b == c) > (a != 1.5) = 0.0; expected = 0.0
  6 passed, ok: (b == c) >= (a != 1.5) = 1.0; expected = 1.0
  7 passed, ok: (a > b) || sqrt(c) = 1.0; expected = 1.0
  8 passed, ok: (!1 != !(b - c/2)) = 1.0; expected = 1.0
  9 passed, ok: -1 * c == -sqrt(-c * -c) = 1.0; expected = 1.0
  10 passed, ok: pow(2, 5) % 5 = 2.0; expected = 2.0
  11 passed, ok: min(max(a,b),c) = 2.5; expected = 2.5
  12 passed, ok: atan(sin(0.5)/cos(0.5)) = 0.5; expected = 0.5
  13 passed, ok: .2 * .3 + .1 = 0.16; expected = 0.16
  14 passed, ok: (a == b) + (b == c) = 0.0; expected = 0.0
  15 passed, ok: -(a + b) * !!sqrt(c) = -4.0; expected = -4.0
  16 passed, ok: sin ( max ( 2 * 1.5, 3 ) / 3 * 3.14159265359 ) = -2.068231111547469e-13; expected = 0.0
  lookUpVariable error: Variable is undefined: _1c @ 7: 1 / _1c^
  17 passed, er: 1 / _1c = nan; expected = nan
  safeDivide error: Error : Division by 0! @ 15: 1 / (2 * b - c)^
  18 passed, er: 1 / (2 * b - c) = inf; expected = inf
  19 passed, ok: sqrt(b-c) = -nan(ind); expected = -nan(ind)
  19 tests passed of 19
  17 for correct expressions, 2 for invalid expressions

Aquí podemos ver que las 19 pruebas se han realizado correctamente, y en este caso, además, los errores esperados se han obtenido dos veces.

La medición de la velocidad se efectúa solo para múltiples cálculos en un ciclo. En el caso del intérprete, esto incluye la etapa de análisis de la expresión, dado que se lleva a cabo con cada cálculo, y para todos los demás tipos de analizadores, la etapa de análisis se encuentra "fuera de los corchetes". El análisis único de una expresión ocupa aproximadamente el mismo tiempo para todos los métodos. Aquí tenemos uno de los resultados de la medición de 10,000 ciclos (microsegundos).

  >>> Performance tests (timing per method)
  Evaluation: 104572
  Compilation: 25011
  Pratt bytecode: 23738
  Pratt: 24967
  ShuntingYard: 23147

Como podíamos esperar, las expresiones previamente "compiladas" se calculan varias veces más rápido que las interpretadas. También podemos deducir que los más rápidos son los cálculos basados ​​en el código de bytes, y que el tipo de parser utilizado para obtener el código de bytes no juega un papel esencial: podemos usar tanto el parser de Pratt, como la clasificación. Podemos seleccionar uno de los parsers basándonos tanto en una evaluación subjetiva de la claridad del algoritmo, como en la facilidad de adaptación a sus tareas, tales como la ampliación de la sintaxis o la integración con desarrollos existentes.

Uso de expresiones para configurar las señales del experto (ExprBot)

Podemos utilizar las expresiones en robots para generar señales comerciales. Esto nos ofrecerá más flexibilidad que la simple modificación de los parámetros. Gracias a la lista de funciones expandible, las capacidades son prácticamente iguales a las permitidas por MQL, pero no es necesario compilar todo ello. Además, resulta sencillo ocultar las operaciones rutinarias dentro de functores preparados. Por consiguiente, el programador ofrecerá al usuario un equilibrio entre la flexibilidad y la complejidad de la personalización del producto.

Ya disponemos de un conjunto de indicadores de media móvil, así que ya podemos construir un sistema comercial basado en los mismos (aunque nada nos impide integrar en las expresiones funciones temporales, gestión de riesgos, precios de ticks, etcétera).

Para mostrar el principio, vamos a crear un Asesor Experto simple, ExprBot. En las variables de entrada SignalBuy y SignalSell, introduciremos las expresiones con las condiciones para realizar compras y ventas, respectivamente. Para una estrategia basada en el cruzamiento de dos MA, podemos ofrecer las siguientes fórmulas.

  #define INDICATOR_FUNCTORS
  #include <ExpresSParserS/ExpressionCompiler.mqh>
  
  input string SignalBuy = "EMA_OPEN_{Fast}(0)/EMA_OPEN_{Slow}(0) > 1 + Threshold";
  input string SignalSell = "EMA_OPEN_{Fast}(0)/EMA_OPEN_{Slow}(0) < 1 - Threshold";
  input string Variables = "Threshold=0.01";
  input int Fast = 10;
  input int Slow = 21;

Estableceremos el umbral como una constante, solo para demostrar la introducción de variables aleatorias. Los parámetros con los periodos promediados de Fast y Slow han sido diseñados para su inserción en las expresiones antes de analizar estas, como un fragmento del nombre del indicador.

Como hay dos señales, instanciaremos dos parsers de descenso recursivo. En principio, podríamos tomar uno, pero, en potencia, los recuadros de la variables pueden diferir en las dos expresiones, y luego sería necesario alternar este contexto antes de cada cálculo.

  ExpressionCompiler ecb(Variables), ecs(Variables);
  Promise *p1, *p2;

En el manejador OnInit, guardamos los parámetros en los recuadros de variables y construimos los árboles de sintaxis.

  int OnInit()
  {
    ecb.variableTable().set("Fast", Fast);
    ecb.variableTable().set("Slow", Slow);
    p1 = ecb.evaluate(SignalBuy, true);
    if(!ecb.success())
    {
      Print("Syntax error in Buy signal:");
      p1.print();
      return INIT_FAILED;
    }
    ecs.variableTable().set("Fast", Fast);
    ecs.variableTable().set("Slow", Slow);
    p2 = ecs.evaluate(SignalSell, true);
    if(!ecs.success())
    {
      Print("Syntax error in Sell signal:");
      p2.print();
      return INIT_FAILED;
    }
    
    return INIT_SUCCEEDED;
  }

La estrategia cabe en el pequeño manejador OnTick (hemos omitido aquí las funciones auxiliares; asimismo, necesitaremos incluir la biblioteca MT4Orders).

  #define _Ask SymbolInfoDouble(_Symbol, SYMBOL_ASK)
  #define _Bid SymbolInfoDouble(_Symbol, SYMBOL_BID)
  
  void OnTick()
  {
    if(!isNewBar()) return;
    
    bool buy = p1.resolve();
    bool sell = p2.resolve();
    
    if(buy && sell)
    {
      buy = false;
      sell = false;
    }
    
    if(buy)
    {
      OrdersCloseAll(_Symbol, OP_SELL);
      if(OrdersTotalByType(_Symbol, OP_BUY) == 0)
      {
        OrderSend(_Symbol, OP_BUY, Lot, _Ask, 100, 0, 0);
      }
    }
    else if(sell)
    {
      OrdersCloseAll(_Symbol, OP_BUY);
      if(OrdersTotalByType(_Symbol, OP_SELL) == 0)
      {
        OrderSend(_Symbol, OP_SELL, Lot, _Bid, 100, 0, 0);
      }
    }
    else
    {
      OrdersCloseAll();
    }
  }

Ambas expresiones se calculan según las "circunstancias" p1 y p2. Como resultado, obtenemos dos banderas, buy y sell, que inician la apertura o el cierre de posiciones en las direcciones correspondientes. El código MQL garantiza que solo puede haber una posición abierta al mismo tiempo, y si las señales se contradicen entre sí (eso es posible si cambiamos las expresiones por algo más complejo que un sistema de viraje, o establecemos un umbral negativo por error), cualquier posición existente se cerrará. Podemos editar las condiciones para las señales como deseemos dentro de lo pautado en las fuciones de los parsers.

Si iniciamos un experto en el simulador, seguramente obtengamos un informe con valores por debajo de la media, pero lo que importa es otra cosa: el comercio se estará realizando, y serán los parsers quienes se encargarán de él. Aunque no ofrecen sistemas comerciales rentables ya preparados, sí que constituyen una herramienta para buscar estos.

Ejemplo de comercio según la señales calculadas con las expresiones

Ejemplo de comercio según la señales calculadas con las expresiones

Conclusión

En el presente artículo (en las dos partes), hemos examinado 4 tipos de parsers, comparando sus capacidades e implementando clases ya listas para integrarse en programas MQL. Todos los parsers usan la misma gramática con las operaciones matemáticas más populares y 25 funciones. Si fuera necesario, la gramática se puede expandir rellenando una lista con los operadores admitidos, y también añadiendo nuevas funciones integradas del plan aplicado (indicadores, precios, valores estadísticos del comercio) y estructuras sintácticas (en particular, matrices y funciones para su procesamiento).

Esta tecnología ofrece un enfoque más flexible sobre la separación de las configuraciones y el código MQL no modificable. La posibilidad de personalizar algoritmos utilizando la edición de expresiones en los parámetros de entrada parece más sencilla para el usuario final que la necesidad de estudiar los conceptos básicos de la programación MQL para analizar el fragmento requerido (incluso si los códigos fuente se encuentran disponibles), editar este según todas las convenciones y aclararse con los posibles errores de compilación. En lo que respecta a los autores de los programas MQL, el soporte del parseo y la evaluación de expresiones ofrece otras ventajas, en particular, puede (si se trabaja adecuadamente) transformarse en un concepto de "script por encima de MQL", lo que permite eliminar las bibliotecas y el control de versiones del compilador MQL.

Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/8028

Archivos adjuntos |
parsers2.zip (40.45 KB)
Instrumental para el comercio manual rápido: Funcionalidad básica Instrumental para el comercio manual rápido: Funcionalidad básica
En la actualidad, cada vez son más los tráders que dan el salto a los sistemas comerciales automáticos. Muchos de ellos, o bien demandan una configuración inicial, o bien (una parte de los mismos) que los sistemas ya estén totalmente automatizados. No obstante, queda una parte significativa de tráders que comercian manualmente, a la antigua. En este artículo, crearemos un conjunto de herramientas para el comercio automático rápido con la ayuda de atajos de teclado y la ejecución de acciones comerciales rápidas en un solo clic.
Cálculo de expresiones matemáticas (Parte 1). Parsers de descenso recursivo Cálculo de expresiones matemáticas (Parte 1). Parsers de descenso recursivo
En el presente artículo, estudiaremos los principios esenciales del análisis y el cálculo de las expresiones matemáticas. Asimismo, implementaremos los parsers de descenso recursivo que funcionan en los modos de intérprete y de cálculos rápidos basados en un árbol de sintaxis previamente construido.
Trabajando con las series temporales en la biblioteca DoEasy (Parte 45): Búferes de indicador de periodo múltiple Trabajando con las series temporales en la biblioteca DoEasy (Parte 45): Búferes de indicador de periodo múltiple
En el artículo, comenzaremos a mejorar los objetos de búfer de indicador y la clase de colección de búferes para trabajar en los modos de periodo y símbolo múltiples. Asimismo, analizaremos el funcionamiento de los objetos de búfer para obtener y mostrar los datos desde cualquier marco temporal en el gráfico actual del símbolo actual.
Conjunto de instrumentos para el marcado manual de gráficos y comercio (Parte I). Preparación - descripción de la estructura y clase de funciones auxiliares Conjunto de instrumentos para el marcado manual de gráficos y comercio (Parte I). Preparación - descripción de la estructura y clase de funciones auxiliares
En este artículo, comenzaremos a describir el conjunto para el marcado gráfico con la ayuda de atajos de teclado. Es un herramienta muy cómoda: con solo pulsar un botón, aparecerá una línea de tendencia, el abanico de Fibonacci con los parámetros necesarios, etcétera. Asimismo, tendremos la posibilidad de alternar marcos temporales, cambiar el orden de las "capas" de los objetos o eliminar todos los objetos de un gráfico.