Construimos un asesor usando módulos individuales

Andrei Novichkov | 13 noviembre, 2019

Introducción

Durante el desarrollo de indicadores, asesores y scripts, el desarrollor se ve obligado a crear constantemente fragmentos de código terminados, que no tienen relación directa con la estrategia de trading. Por ejemplo, puede tratarse del horario de trabajo del asesor, de un día, una semana, un mes. Como resultado del desarrollo, proyectaremos un objeto independiente que interactuará con la principal lógica comercial y otros componentes, utilizando su sencilla interfaz. Si el desarrollador emplea un mínimo de esfuerzo, este horario se podrá usar en posteriores trabajos con los asesores, indicadores, etc, sin realizar casi modificaciones. En el presente artículo, hemos tratado de sistematizar la proyección de asesores "por bloques", y también hemos analizado la interesante capacidad que obtendremos como resultado. El artículo se ha escrito pensando en desarrolladores principiantes.

Cómo sucede ahora

Comenzaremos por intentar comprender qué aspecto puede tener un asesor así, proyectado "sobre la marcha" y de qué partes / componentes / módulos puede constar. ¿De dónde pueden proceder estos componentes? La respuesta es clara y comprensible: durante el desarrollo constante del trabajo, el desarrollador se ve obligado una y otra vez a proyectar componentes aparte con funcionalidad semejante e incluso coincidente.

Salta rápidamente a la vista que preparar cada vez un nuevo trailing (por ejemplo) supone un gasto inútil de tiempo. En general, el traling de cualquier asesor tendrá siempre las mismas tareas y aproximadamente los mismos parámetros de entrada. Por eso, el desarrollador debe proyectar el trailing una sola vez, y luego colocarlo en todos los asesores que tienen esta necesidad, invirtiendo en ello el mínimo esfuerzo. Lomismo podemos decir de multitud de otros componentes: el ya mencionado componente de horario, diferentes filtros y de noticias, módulos que combinan diversas funciones comerciales y otros.

Como resultado, obtendremos un asesor bastante caótico, preparado a partir de una especie de puzle conformado por diferntes módulos / bloques programáticos. Los módulos intercambian información entre sí y con la "parte central" del asesor, la "estrategia" que toma las decisiones. Vamos a representar una posible variante de interacción de varios módulos:



Nos ha salido un esquema bastante confuso. Y eso que solo hemos usado tres módulos y dos manejadores del asesor: OnStart y OnTick. Si complicamos al asesor, como suele suceder, las conexiones internas se harán todavía más complejas. Resulta complicado corregir y mejorar un asesor así, y si surge la necesidad de quitar / incluir otro módulo, esto causará no pocos problemas. Además, la depuración primaria y la búsqueda de errores tampoco serán sencillas. Uno de las causas de estas complicaciones se encuentra en las conexiones proyectadas sin sistematización alguna. Debemos prohibir a los módulos relacionarse entre ellos y con los manejadores del asesor en cuanto surja cierta necesidad, y aparecer en cierto orden de inmediato:

Esta acción tan sencilla puede dar bastante rápido un efecto positivo. Los módulos individuales podrán incluirse / quitarse, depurarse, modificarse con mucha mayor facilidad. Y la lógica de OnTick será más susceptible a correcciones y mejoras si las conexiones con los módulos se encuentran en un solo manejador, y no están dispersas por todo el código del asesor:


Un cambio casi insignificante en el diseño conferirá a la estructura del asesor un aspecto más visual y comprensible a nivel intuitivo. La nueva estructura se asemeja ahora un poco al resultado del uso del patrón "Observer", aunque las diferencias de esta estructura con respecto a la mencionada no nos de pie a sacar tales conclusiones. Vamos a analizar a continuación cómo podemos mejorar el diseño y los cambios que esto acarreará.

Asesor para los experimentos

Para procesar nuestras ideas, necesitaremos un asesor sencillo. No vamos a hacerlo muy complejo, ya que lo necesitamos con fines exclusivamente didácticos. El asesor abrirá una orden de venta, si la vela anterior ha sido bajista. Proyectaremos el asesor usando la estructura de módulos. El primer módulo será el comercial:

class CTradeMod {
public:
   double dBaseLot;
   double dProfit;
   double dStop;
   long   lMagic;
   void   Sell();
   void   Buy();
};

void CTradeMod::Sell()
{
  CTrade Trade;
  Trade.SetExpertMagicNumber(lMagic);
  double ask = SymbolInfoDouble(Symbol(),SYMBOL_ASK);
  Trade.Sell(dBaseLot,NULL,0,ask + dStop,ask - dProfit);
} 

void CTradeMod::Buy()
{
  CTrade Trade;
  Trade.SetExpertMagicNumber(lMagic);
  double bid = SymbolInfoDouble(Symbol(),SYMBOL_BID);
  Trade.Buy(dBaseLot,NULL,0,bid - dStop,bid + dProfit);
} 

El módulo se ejecutará en forma de clase con campos y métodos abiertos. Por el momento, no necesitamos que el método Buy() esté implementado, pero nos hará falta más adelante. El valor de los campos aparte es obvio: volumen, niveles comerciales, número mágico. También resulta comprensible cómo usar el módulo: lo creamos y luego llamamos al método Sell() cuando aparezca la señal de entrada.

En el asesor se incluirá un módulo más:

class CParam {
public:
   double dStopLevel;      
   double dFreezeLevel;   
    CParam() {
      new_day();      
    }//EA_PARAM()
    void new_day() {
      dStopLevel   = SymbolInfoInteger(Symbol(),SYMBOL_TRADE_STOPS_LEVEL) * Point();
      dFreezeLevel = SymbolInfoInteger(Symbol(),SYMBOL_TRADE_FREEZE_LEVEL) * Point();
    }//void new_day()
};
Este módulo merece especial atención. Se trata de un módulo auxiliar en el que se reúnen diferentes parámetros usados por los demás módulos y los manejadores del asesor. Desafortunadamente, se dan casos en los que podemos ver más o menos un código así:
...
input int MaxSpread = 100;
...
OnTick()
 {
   if(ask - bid > MaxSpread * Point() ) return;
....
 }

Está claro que este fragmento es totalmente ineficaz. Pero si colocamos todos los parámetros de entrada (y no solo de entrada) que requieren actualización y transformación en un módulo aparte (aquí se trataría de MaxSpread * Point() ), así no llenaremos de código innecesario el espacio global, logrando de esta forma controlar y gestionar su estado con facilidad, como se hace en el módulo CParam con los valores stops_level y freeze_level.

Es posible (y esto sería más correcto) no hacer en este y otros módulos los campos abiertos, sino prever getters para ellos. Aquí, esto se hace para simplifcar el código al máximo, pero si implementamos un proyecto real, la presencia de un getter sería más que deseable.

Además, es totalmente posible que, concretamente para el módulo CParam, merezca la pena hacer una excepción y apartarnos de las normas, permitiendo recurrir a este módulo no solo al manejador OnTick(), sino a todos los demas módulos y manejadores.

Y al fin, el bloque de parámetros de entrada y los manejadores del asesor:

input double dlot   = 0.01;
input int    profit = 50;
input int    stop   = 50;
input long   Magic  = 123456;

CParam par;
CTradeMod trade;

int OnInit()
  {  
   trade.dBaseLot = dlot;
   trade.dProfit  = profit * _Point;
   trade.dStop    = stop   * _Point;
   trade.lMagic   = Magic;
   
   return (INIT_SUCCEEDED);
  }
  
void OnDeinit(const int reason)
  {
  }

void OnTick()
  {
   int total = PositionsTotal(); 
   ulong ticket, tsell = 0;
   ENUM_POSITION_TYPE type;
   double l, p;
   for (int i = total - 1; i >= 0; i--) {
      if ( (ticket = PositionGetTicket(i)) != 0) {
         if ( PositionGetString(POSITION_SYMBOL) == _Symbol) {
            if (PositionGetInteger(POSITION_MAGIC) == Magic) {
               type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
               l    = PositionGetDouble(POSITION_VOLUME);
               p    = PositionGetDouble(POSITION_PRICE_OPEN);
               switch(type) {
                  case POSITION_TYPE_SELL:
                     tsell = ticket;    
                     break;
               }//switch(type)
            }//if (PositionGetInteger(POSITION_MAGIC) == lmagic)
         }//if (PositionGetString(POSITION_SYMBOL) == symbol)
      }//if ( (ticket = PositionGetTicket(i)) != 0)
   }//for (int i = total - 1; i >= 0; i--)
   if (tsell == 0) 
      {
        double o = iOpen(NULL,PERIOD_CURRENT,1); 
        double c = iClose(NULL,PERIOD_CURRENT,1); 
        if (c < o) 
          {
            trade.Sell();
          }   
      }                         
  }

El asesor inicializa los módulos en el manejador OnInit(), y después recurre a ellos solo desde el manejador OnTick(). En OnTick(), el asesor ejecuta un ciclo por las posiciones abiertas, con objeto de comprobar si la posición necesaria está ya abierta. Si la posición aún no está abierta, el asesor la abre al darse la señal.

Merece la pena notar que el manejador OnDeinit(const int reason) aún está vacío. Los módulos se crean de tal forma que no requieren eliminación explícita. Además, todavía no hemos involucrado el módulo CParam, porque las comprobaciones al abrir una posición aún no se realizan. Pero si dichas comprobaciones se efecturan, el módulo CTradeMod podría requerir acceso al módulo CParam, y el módulo se vería obligado a responder a la pregunta mencionada anteriormente: ¿debemos usar el módulo CParam como excepción a las normas ya aplicadas? No obstante, si observamos con mayor detenimiento, esto no resulta necesario, por lo menos en el caso que nos ocupa.

Vamos a analizar este punto con más detalle. El módulo CTradeMod podría necesitar los datos del módulo CParam para comprobar los niveles de Stop Loss y Take Profit, así como el volumen de la posición abierta. Pero esta comprobación también se puede realizar en el punto de la toma de decisiones sobre la apertura de una posición: si los niveles y el volumen no superan la comprobación, tampoco necesitaremos abrir nada. Esto conlleva el traslado de la comprobación al manejador OnTick(). En este caso, dado que los valores de los niveles comerciales y el volumen se han establecido en los parámetros de entrada, la comprobación se puede realizar una vez en el manejador OnInit(), y si finiliza sin éxito, la inicialización de todo el asesor se deberá finalizar con error. Bien, como podemos ver, los módulos CTradeMod y CParam pueden actuar de forma independiente. De la misma forma sucederá en la mayoría de los asesores: los módulos independientes actúan a través del manejador OnTick() y no saben nada el uno sobre el otro. Pero, ¿en qué casos se hace imposible respetar esta condición? Lo veremos más tarde.

Comencemos las mejoras

Salta a la vista de inmediato el amplio ciclo de iteración de posiciones en el manejador OnTick(). Este segmento de código es necesario si el manejador no quiere que el asesor:

  1. continúe abriendo posiciones si la señal de entrada aún no ha tenido tiempo de desvancerse,
  2. detecte órdenes ya colocadas después de una interrupción en el funcionamiento,
  3. posiblemente recopile algunas estadísticas actuales, como el volumen total de las posiciones abiertas o la reducción máxima.

El valor de este ciclo aumentará todavía más en el caso de que el asesor use la promediación o una cuadrícula. ¿Y cómo arreglárnoslas sin él cuando se usan niveles comerciales virtuales? Como cierre a lo dicho, podemos sacar la conclusión de que debemos crear un módulo aparte usando como base este fragmento de código. En el caso más sencillo, se trataría de un módulo que simplemente detectaría las posiciones con un determinado número mágico y comunicaría este hecho de cierta forma. En el caso complejo, este módulo podría constituir por sí mismo un "centro" que incluyese módulos mas sencillos, por ejemplo, los módulos de registro de logs y los módulos de estadística. En este caso, el asesor comenzará a adquirir la apreciada estructura "de árbol" con los manejadores OnInit() y OnTick() en la base. Veamos el aspecto que podría tener un módulo así:

class CSnap {
public:
           void CSnap()  {
               m_lmagic = -1; 
               m_symbol = Symbol();               
           }
   virtual void  ~CSnap() {}   
           bool   CreateSnap();
           long   m_lmagic;
           string m_symbol;
};
Dejamos todos los campos abiertos, como ya hemos hecho anteriormente. Son dos: el número mágico y el nombre del símbolo en el que se ejeucta el asesor. En caso necesario, podemos asignar los valores a estos campos en OnInit(). El trabajo principal es ejecutado por el método CreateSnap():
bool CSnap::CreateSnap() {
   int total = PositionsTotal(); 
   ulong ticket;
   ENUM_POSITION_TYPE type;
   double l, p;
   for (int i = total - 1; i >= 0; i--) {
      if ( (ticket = PositionGetTicket(i)) != 0) {
         if (StringLen(m_symbol) == 0 || PositionGetString(POSITION_SYMBOL) == m_symbol) {
            if (m_lmagic < 0 || PositionGetInteger(POSITION_MAGIC) == m_lmagic) {
               type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
               l    = PositionGetDouble(POSITION_VOLUME);
               p    = PositionGetDouble(POSITION_PRICE_OPEN);
               switch(type) {
                  case POSITION_TYPE_BUY:
// ???????????????????????????????????????
                     break;
                  case POSITION_TYPE_SELL:
// ???????????????????????????????????????
                     break;
               }//switch(type)
            }//if (lmagic < 0 || PositionGetInteger(POSITION_MAGIC) == lmagic)
         }//if (StringLen(symbol) == 0 || PositionGetString(POSITION_SYMBOL) == symbol)
      }//if ( (ticket = PositionGetTicket(i)) != 0)
   }//for (int i = total - 1; i >= 0; i--)
   return true;
}

El código no es complicado, pero topamos con un problema. ¿De qué forma y a quién precisamente debe transmitir el módulo la información obtenida? ¿Qué debemos registrar en las líneas que contienen los símbolos en el último fragmento de código? Podría parecer que todo es sencillo. Llamamos en el manejador OnTick() el método CreateSnap(), que ejecuta el trabajo guardando los resultados en los campos de la clase CSnap. A continuación, el manejador comprueba los campos y saca ciertas conclusiones.

Sí, así puede hacerse en el caso más simple. Pero, ¿qué hacemos cuando debamos procesar los parámetros de cada posición por separado, por ejemplo, para calcular el valor medio ponderado? En este caso, necesitaremos un enfoque más universal, capaz de transmitar los datos al siguiente objeto para el procesamiento. Y para ello, deberemos prever en la clase CSnap un puntero a este objeto:

CStrategy* m_strategy;
Y un método para asignar valores a este campo:
     bool SetStrategy(CStrategy* strategy) {
              if(CheckPointer(strategy) == POINTER_INVALID) return false;
                 m_strategy = strategy;
                 return true;
              }//bool SetStrategy(CStrategy* strategy)   

Hemos elegido este nombre para el objeto CStrategy, de forma que en él se puedan tomar decisiones de entrada, y posiblemente otras decisiones. Por consiguiente, este objeto puede determinar la estrategia de todo el asesor.

Ahora, el conmutador switch en el método CreateSnap() tendrá el aspecto que sigue:

               switch(type) {
                  case POSITION_TYPE_BUY:
                     m_strategy.OnBuyFind(ticket, p, l);
                     break;
                  case POSITION_TYPE_SELL:
                     m_strategy.OnSellFind(ticket, p, l);
                     break;
               }//switch(type

Obviamente, en caso necesario, el código se puede completar fácilmente con conmutadores para las órdenes pendientes y las llamadas de los métodos correspondientes. Además, se puede modificar con mucha facilidad el método para recopilar una gran cantidad de información. Posiblemente merezca la pena hacer el método CreateSnap() virtual, teniendo en cuenta con ello las potenciales posibilidades de herencia de la clase CSnap. Pero esto no resulta elemental para nosotros en el caso actual, por lo que nos limitaremos a implementar un código más sencillo para el módulo actual.

Notemos que es muy probable que el puntero a dicho objeto (en nuestro caso, se trata del puntero CStrategy*) resulte útil no solo para el módulo analizado. La potencial necesidad de la conexión del módulo con la lógica de funcionamiento del asesor puede surgir en cualquier módulo que ejecute cálculos activos. Por eso, vamos a sacar el campo correspondiente y el método de inicialización a la clase básica:

class CModule {
   public:
      CModule() {m_strategy = NULL;}
     ~CModule() {}
     virtual bool SetStrategy(CStrategy* strategy) {
                     if(CheckPointer(strategy) == POINTER_INVALID) return false;
                     m_strategy = strategy;
                     return true;
                  }//bool SetStrategy(CStrategy* strategy)   
   protected:   
      CStrategy* m_strategy;  
};

Y ahora, vamos a crear los módulos heredando de la clase CModule. Sí, en varios casos va a sobrar bastante código, pero esto se verá de sobra compensado por aquellos módulos donde este puntero será necesario de verdad. En el caso del resto, donde no existe esta necesidad, bastará con no llamar el método SetStrategy(...). La clase básica para los módulos puede ser también útil para ubicar distintos campos y métodos sobre los que no sabemos nada aún. Por ejemplo, podría resultar totalmente útil (aunque en este caso no se haya implementado) el método:
public:
   const string GetName() const {return m_sModName;}
protected:
         string m_sModName;

El método retornaría el nombre del módulo que podría usarse para realizar el diagnóstico, la depuración, o en algún panel informativo.

Ahora, vamos a ver cómo podríamos implementar la clase CStrategy, tan importante para nosotros:

La estrategia del asesor

Anteriormente, hemos escrito que debe tratarse del objeto que tome las decisiones sobre las entradas. Pero entonces, significa que también puede tomar decisiones acerca de las salidas, las modificaciones, los cierres parciales, etcétera. Por eso, le hemos dado el nombre correspondiente. Además, resulta obvio que no puede de ninguna forma implementarse como módulo, ya que es muy importante que el objeto que tome la decisión se diferencie en cada asesor. En caso contrario, el desarrollador se arriesga a obtener simplemente un asesor idéntico al ya desarrollado. Por eso, no lograremos desarrollar e incluir una vez un objeto así en los demás asesores, como hacíamos con los módulos. Pero, por otra parte, no nos hace falta: iremos por otro camino. Comenzaremos el desarrollo por la clase básica, usando lo que ya conocemos:

class CStrategy  {
public:
   virtual  void    OnBuyFind  (ulong ticket, double price, double lot) = 0;
   virtual  void    OnSellFind (ulong ticket, double price, double lot) = 0;       
};// class CStrategy

Todo es muy sencillo. Necesitábamos dos métodos para la llamada en el caso de que el método CreateSnap() detectara las posiciones necesarias; ahora han sido añadidos. La clase CStrategy se ha ejecutado como abstracta, mientras que los métodos han sido declarados como virtuales. Esto es totalmente lógico y está completamente justificado, puesto que ya hemos analizado que este objeto debe variar de un asesor a otro. Por consiguiente, la clase básica puede ser utilizada solo para la herencia, mientras que sus métodos serán redefinidos.

Ahora queda añadir el archivo CStrategy.mqh al archivo CModule.mqh:

#include "CStrategy.mqh"

...y podremos considerar que la carcasa del asesor ha sido construida "a grandes rasgos", por lo que podremos ocuparnos de su posterior mejora.

Mejorando la estrategia del asesor

Con la ayuda de los métodos virtuales mostrados, el objeto CSnap recurre al objeto CStrategy. No obstante, en el objeto CStrategy tendrán que existir otros métodos. Recordemos que el objeto de la estrategia toma las decisiones. Por consiguiente, deberá dar recomendaciones sobre la entrada si detecta la señal correspondiente, y también efectuar dicha entrada. Por ello, tendrán que existir métodos que obliguen al objeto CSnap a llamar a su método CreateSnap(), etcétera. Vamos a añadir algunos de estos métodos a la clase CStrategy:

   virtual  string  Name() const     = 0;
   virtual  void    CreateSnap()     = 0;  
   virtual  bool    MayAndEnter()    = 0;  
   virtual  bool    MayAndContinue() = 0;       
   virtual  void    MayAndClose()    = 0;

Por supuesto, debemos recordar que esta lista es sumamente condicional y puede ser modificada y corregida de un asesor a otro. Describamos estos métodos brevemente:

Recordemos de nuevo que esta lista es una lista bastante simplificada y lejanamente aproximada. Además, aquí falta un método muy importante: el método de inicialización de la estrategia. Entre nuestros planes consideramos concentrar en el objeto CStrategy los punteros al resto de los módulos, de forma que los manejadores del asesor OnTick() y los demás recurran exclusivamente al objeto CStrategy, sin saber nada sobre ningún otro módulo. Por eso, debemos añadir de alguna forma los punteros de los módulos al objeto CStrategy. Prever los campos abiertos correspondientes e inicializarlos en el manejador OnInit() es algo inasumible. El motivo lo conoceremos más tarde.

Para la inicialización, añadimos el método:

virtual  bool    Initialize(CInitializeStruct* pInit) = 0;
  con el objeto de inicialización CInitializeStruct, que contendrá todos los punteros necesarios. Por ahora, vamos a describir este objeto en el archivo CStrategy.mqh de esta forma:
class CInitializeStruct {};

Como podemos ver, se trata de una clase vacía que ha sido pensada como la clase CStrategy, para la herencia. Ahora, ha llegado el momento de pasar al asesor real, cuando todos los trabajos preparatorios han sido finalizados.

Aplicación práctica

Vamos a crear una asesor de demostración con un funcionamiento muy simple: Si la vela anterior ha sido bajista, abrimos una posición de venta con Take Profit y Stop Loss fijos. La siguiente posición no se abrirá hasta que no se cierre la anterior.

Los módulos están prácticamente preparados, por eso vamos a concentrarnos en la clase derivada de CStrategy:

class CRealStrat1 : public CStrategy   {
public:
   static   string  m_name;
                     CRealStrat1(){};
                    ~CRealStrat1(){};
   virtual  string  Name() const {return m_name;}
   virtual  bool    Initialize(CInitializeStruct* pInit) {
                        m_pparam = ((CInit1* )pInit).m_pparam;
                        m_psnap = ((CInit1* )pInit).m_psnap;
                        m_ptrade = ((CInit1* )pInit).m_ptrade;
                        m_psnap.SetStrategy(GetPointer(this));
                        return true;
                    }//Initialize(EA_InitializeStruct* pInit)
   virtual  void    CreateSnap() {
                        m_tb = 0;
                        m_psnap.CreateSnap();
                    }  
   virtual  bool    MayAndEnter();
   virtual  bool    MayAndContinue() {return false;}       
   virtual  void    MayAndClose()   {}
   virtual  bool    Stop()            {return false;}   
   virtual  void    OnBuyFind  (ulong ticket, double price, double lot) {}
   virtual  void    OnBuySFind (ulong ticket, double price, double lot) {}   
   virtual  void    OnBuyLFind (ulong ticket, double price, double lot) {}
   virtual  void    OnSellFind (ulong ticket, double price, double lot) {tb = ticket;}  
   virtual  void    OnSellSFind(ulong ticket, double price, double lot) {}   
   virtual  void    OnSellLFind(ulong ticket, double price, double lot) {}      
private:
   CParam*          m_pparam;
   CSnap*           m_psnap;  
   CTradeMod*       m_ptrade;   
   ulong            m_tb;            
};
static string CRealStrat1::m_name = "Real Strategy 1";

bool CRealStrat1::MayAndEnter() {
   if (tb != 0) return false;  
   double o = iOpen(NULL,PERIOD_CURRENT,1); 
   double c = iClose(NULL,PERIOD_CURRENT,1); 
   if (c < o) {
      m_ptrade.Sell();
      return true;
   }   
   return false;
} 

El código del asesor es sencillo y no requiere aclaraciones. Vamos a detenernos solo en algunos aspectos. El método CreateSnap() de la clase CRealStrat1 resetea el campo en el que se encuentra el valor del ticket de una posición de venta ya abierta y abre el método CreateSnap() del módulo CSnap. El módulo CSnap comprueba las posiciones abiertas, y si encuentra una posición de venta abierta por este asesor, llama al método OnSellFind(...) de la clase CStrategy cuyo puntero está en el módulo CSnap. Como resultado, se llama el método OnSellFind(...) de la clase CRealStrat1, que cambia de nuevo el valor del campo m_tb. El método MayAndEnter(), al ver que ya hay una posición abierta, no abre una nueva. No vamos a usar ningún otro método de la clase básica CStrategy, por eso su implementación se muestra vacía.

Otro aspecto interesante implica al método Initialize(...). Este método añade a la clase CRealStrat1 punteros a otros módulos que podrían ser necesarios al tomar decisiones aparte. La clase CStrategy desconoce qué módulos podría necesitar la clase CRealStrat1, y por eso utiliza la clase vacía CInitializeStruct. En nuestro caso, añadimos en el archivo que contiene la clase CRealStrat1 (aunque esto no es obligatorio) la clase CInit1, heredándola de CInitializeStruct:

class CInit1: public CInitializeStruct {
public:
   bool Initialize(CParam* pparam, CSnap* psnap, CTradeMod* ptrade) {
   if (CheckPointer(pparam) == POINTER_INVALID || 
       CheckPointer(psnap)  == POINTER_INVALID) return false;   
      m_pparam = pparam; 
      m_psnap  = psnap;
      m_ptrade = ptrade;
       return true;
   }
   CParam* m_pparam;
   CSnap*  m_psnap;
   CTradeMod* m_ptrade;
};

Un objeto de esta clase puede crearse e inicializarse en el manejador OnInit y transmitirse al método correspondiente en el objeto de clase CRealStrat1. De esta forma, obtendremos una estructura relativamente compleja de varios objetos, con la que podremos trabajar a través de una interfaz bastante sencilla en el manejador OnTick().

Los manejadores OnInit() y OnTick()

Este es el aspecto que pueden tener el manejador OnInit() y la lista de objetos globales:

CParam     par;
CSnap      snap;
CTradeMod  trade;

CStrategy* pS1;

int OnInit()
  {  
   ...
   pS1 = new CRealStrat1();
   CInit1 ci;
   if (!ci.Initialize(GetPointer(par), GetPointer(snap), GetPointer(trade)) ) return (INIT_FAILED);
   pS1.Initialize(GetPointer(ci));      
   return (INIT_SUCCEEDED);
  }
  

  Creamos en el manejador OnInit() solo un objeto: un ejemplar de la clase CRealStrat1. A continuación, lo inicializamos mediante el objeto de clase CInit1. Este mismo objeto se elimina en el manejador OnDeinit():
void OnDeinit(const int reason)
  {
      if (CheckPointer(pS1) != POINTER_INVALID) delete pS1;      
  }

Gracias a nuestras acciones, el manejador OnTick() ha resultado bastante sencillo:
void OnTick()
  {
      if (IsNewCandle() ){
         pS1.CreateSnap();
         pS1.MayAndEnter();
         
      }    
  }

En la apertura de una nueva barra, comprobamos las posiciones abiertas, y luego comprobamos si hay señal de entrada. Si hay una señal y el asesor no ha entrado anteriormente según esta, abrimos una posición. El manejador es tan sencillo, que, si así lo queremos, podemos ubicar en él un cierto código "global", por ejemplo, no comenzar a comerciar de inmediato al colocar el asesor en el gráfico, sino esperar la confirmación por parte del ususario como pulsación de un botón, u otras opciones.

Las demás funciones del asesor no nos interesan, de hecho, no las mostramos aquí, se encuentran en el fichero adjunto.

Bien, ya hemos proyectado un asesor que consta de "piezas" individuales: los módulos. Pero esto no todo, ni mucho menos. Vamos a ver otra interesante posibilidad que nos descubre este método de implementación de proyectos.

¿Qué es lo próximo?

Lo primero que viene a la cabeza es el cambio dinámico de módulos. No hay ningún motivo especial para no sustituir un horario sencillo por otro más avanzado, y con ello nos referimos a sustituir el horario como objeto, no solo añadir propiedades y métodos al ya existente, complicando así la depuración, corrección y mejora del código. Podemos prever las versiones "Debug" y "Release" para módulos aparte, o crear algún controlador para la gestión de módulos.  

Pero eso no es lo más importante. El método de creación de proyectos que hemos analizado posibilita la sustitución dinámica de la estrategia del asesor, en este caso, del objeto de clase CRealStrat1. Como resultado, obtenemos un asesor en el que se encuentran dos "núcleos" distintos que implementan dos estrategias distintas, por ejemplo, una para trabajar según la tendencia, y otra para el flat. Podemos añadir una tercera para el comercio con scalpers en la sesión asiática. En otras palabras: podemos empaquetar varios asesores en uno solo, conectándolos dinámicamente. Cómo hacerlo:

  1. Desarrollamos una clase con la lógica de la toma de decisiones, derivada de la clase CStrategy (como CRealStrat1)
  2. Desarrollamos una clase que inicialice esta nueva estrategia derivada de CInitializeStruct (CInit1)
  3. Incluimos en el proyecto el archivo con el nuevo desarrollo.
  4. Introducimos en los parámetros de entrada una nueva variable, que se encargará de decidir qué estrategia se activará en el inicio.
  5. Desarrollamos recursos que nos permitan pasar de una estrategia a otra: un panel comercial.

Como ejemplo, vamos a añadir otra estrategia a nuestro asesor tutorial. Para no complicar el código, haremos esta estrategia igual de simple que la original. En aquella, el asesor abría una posición de venta, aquí será de esta forma: Si la vela anterior ha sido alcista, abrimos una posición de compra con Take Profit y Stop Loss fijos. La siguiente posición no se abrirá hasta que no se cierre la anterior. El código de la segunda estrategia prácticamente repite el código de la primera desarrollada, por lo que no vamos a mostrarlo aquí (se encuentra en el fichero adjunto). Lo mismo podemos decir de la clase necesaria para inicializar la nueva estrategia.

Vamos a ver los cambios que deberemos introducir en el archivo del asesor en el que se ubican los parámetros de entrada y los manejadores:

enum CSTRAT {
   strategy_1 = 1,
   strategy_2 = 2
};

input CSTRAT strt  = strategy_1;

CSTRAT str_curr = -1;

int OnInit()
  {  
   ...
   if (!SwitchStrategy(strt) ) return (INIT_FAILED);
   ...       
   return (INIT_SUCCEEDED);
  }
  
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
      if (id == CHARTEVENT_OBJECT_CLICK && StringCompare(...) == 0 ) {
         SwitchStrategy((CSTRAT)EDlg.GetStratID() );
      } 
  }  

bool SwitchStrategy(CSTRAT sr) {
   if (str_curr == sr) return true;
   CStrategy* s = NULL;
   switch(sr) {
      case strategy_1:
         {
            CInit1 ci;
            if (!ci.Initialize(GetPointer(par), GetPointer(snap), GetPointer(trade)) ) return false;
            s = new CRealStrat1();
            s.Initialize(GetPointer(ci));  
         }
         break;
      case strategy_2:
         {
            CInit2 ci;
            if (!ci.Initialize(GetPointer(par), GetPointer(snap), GetPointer(trade)) ) return false;
            s = new CRealStrat2();
            s.Initialize(GetPointer(ci));              
         }   
         break;
   }
   if (CheckPointer(pS1) != POINTER_INVALID) delete pS1;
   pS1 = s;    
   str_curr = sr;
   return true;
}

La función para alternar las estrategias SwitchStrategy(...) y el manejador OnChartEvent(...) están conectados por un panel comercial cuyo código no se muestra aquí: podrá encontrarlo en el fichero adjunuto. Tampoco se ha percibido nada complicado en cuanto a la gestión diámica de las estrategias. Creamos un nuevo objeto con la estrategia, eliminamos el antiguo y registramos el nuevo puntero a la variable:

CStrategy* pS1;

Desde este momento, el asesor en el manejador OnTick() recurrirá a la nueva estrageia, y, por consiguiente, podría cambiar por completo la lógica de funcionamiento. La jerarquía de los objetos y las dependencias básicas tendrán más o menos este aspecto:

Aquí no mostramos el panel comercial y las conexiones surgidas durante la inicialización, por su carácter secundario. En esta etapa, podemos considerar finalizada nuestra tarea, pues el asesor está preparado para trabajar y ser eventualmente modernizado. Y, gracias a este enfoque, esa modernización puede ser totalmente radical, y también realizarse en un plazo de tiempo realmente ajustado.

    Conclusión

    Hemos proyectado un asesor usando los elementos de los patrones estándar de creación de proyectos "Observer" y "Facade". La descripción completa de estos patrones (y no solo de ellos) se puede encontrar aquí: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides "Design Patterns. Elements of Reusable Object-Oriented Software", un libro cuya lectura recomendamos encarecidamente.

    Programas usados en el artículo:

     # Nombre
    Tipo
     Descripción
    1
    Ea&Modules.zip Directorio Fichero con los archivos del asesor.