![Plantilla para proyectar el MVC y posibilidades de uso](https://c.mql5.com/i/articles/overlay.png)
Plantilla para proyectar el MVC y posibilidades de uso
Introducción
En base a la experiencia, podemos aventurarnos a sugerir que la gran mayoría de los desarrolladores ha pasado por una etapa en la que, a medida que el proyecto se desarrolla, se hace más complejo y se llena de funcionalidad sin remedio, volviéndose su código una especie de espagueti enmarañado. El trabajo aún no ha terminado, y ya resulta muy complicado recordar en qué parte de este revoltijo está la llamada de este o aquel método, por qué esta llamada se encuentra exactamente aquí y cómo funciona todo esto.
Y lo más triste es que después de un tiempo ni siquiera el autor del código podrá comprender el código fuente. ¿Y qué podemos decir cuando encargamos descifrar ese lío a otra persona? Si en ese momento el autor del código no está disponible por alguna razón, la tarea se vuelve prácticamente imposible de resolver. El código no estructurado es muy difícil de mantener y modificar cuando el programa resulta más complejo que simplemente "Hola, mundo". Este es uno de los motivos de la aparición de los patrones de diseño. Estos aportan cierta estructura al proyecto, haciéndolo más claro y visual.
El patrón de MVC y por qué es necesario
Este patrón apareció hace bastante tiempo (en 1978), pero se describió por primera vez mucho más tarde, en 1988, al publicarse su descripción final. Desde entonces, la plantilla ha ido cambiando, haciéndose más compleja y dando lugar a nuevos enfoques.
En este artículo, nos interesará el "MVC clásico", sin complicaciones y con funcionalidad adicional. Su esencia consiste en "dividir" el código existente en tres componentes separados: Modelo (Model), Vista (View) y Controlador (Controller). La esencia de la plantilla MVC consiste en que estos tres componentes pueden desarrollarse y acompañarse entre sí de forma independiente. Un grupo aparte de desarrolladores puede trabajar en cada componente, para lanzar así nuevas versiones y eliminar errores. Está claro que, en este caso, se hace mucho más sencillo trabajar en un proyecto común, a la vez que podemos estudiar la información de otros de forma más rápida y más fácil.
Vamos a analizar lo que supone cada componente por separado.
- Vista (View). La vista es responsable de la representación visual. En un caso más general, se encarga de enviar datos al usuario. Debemos tener en cuenta que en realidad los métodos de representación de datos pueden ser varios. Por ejemplo, los datos pueden visualizarse como un recuadro, un gráfico o diagrama al mismo tiempo. En otras palabras, dentro de una sola aplicación construida según el esquema MVC, puede haber varias Vistas. Las Vistas reciben datos del Modelo sin tener idea de lo está sucediendo dentro del mismo.
- Modelo (Model). El modelo contiene los datos. Asimismo, establece la comunicación con las bases de datos, envía las solicitudes a la red y a otras fuentes. Además, modifica los datos, los registra, los guarda y los elimina. El modelo no sabe nada sobre el funcionamiento de la Vista ni cuántas Vistas están disponibles, pero dispone de las interfaces necesarias sobre las que las Vistas pueden solicitar datos. Las Vistas no pueden hacer nada más: no pueden obligar al Modelo a cambiar su estado. De esto se encraga el Controlador. De forma interna, el Modelo puede constar de varios otros modelos alineados en una jerarquía o funcionando por igual. En este sentido, el Modelo no impone restricciones, excepto la ya mencionada: el Modelo mantiene su dispositivo interno en secreto respecto a la Vista y el Controlador.
- Controlador (Controller). El Controlador hace posible la conexión entre el usuario y el Modelo. El Controlador no sabe lo que el modelo hace con los datos, pero puede decirle al Modelo que ha llegado el momento de actualizar el contenido. En general, el Controlador, según su interfaz, trabaja con el Modelo sin intentar entender lo que está sucediendo en su interior.
Visualmente, la conexión entre los componentes individuales de la plantilla de MVC se ve así:
No obstante, no existen reglas y limitaciones particularmente estrictas para el uso de MVC. El desarrollador debe estar atento para no ubicar parte de la lógica de trabajo (o incluso la lógica completa) con los datos del Modelo en el Controlador y no meterse en la Vista. Además, deberemos hacer el Controlador en sí más "fino", sin sobrecargarlo. Asimismo, deberemos tener en cuenta que el esquema de MVC se usa para otras plantillas de diseño, como el "Observador" y la "Estrategia", por ejemplo.
Ahora, vamos a intentar entender cómo utilizar la plantilla de MVC en MQL, y lo más importante, comprender si esta resulta necesaria.
El indicador más simple en términos de MVC.
Crearemos un indicador primitivo que dibuje una línea utilizando el cálculo más simple. El indicador será muy pequeño y su código se ubicará en un archivo. Este podría ser su aspecto:
....... #property indicator_chart_window #property indicator_buffers 1 #property indicator_plots 1 //--- plot Label1 #property indicator_label1 "Label1" #property indicator_type1 DRAW_LINE #property indicator_color1 clrDarkSlateBlue #property indicator_style1 STYLE_SOLID #property indicator_width1 2 //--- indicator buffers double lb[]; //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { SetIndexBuffer(0, lb, INDICATOR_DATA); ArraySetAsSeries(lb, true); IndicatorSetString(INDICATOR_SHORTNAME, "Primitive1"); IndicatorSetInteger(INDICATOR_DIGITS, _Digits); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { if(rates_total <= 4) return 0; ArraySetAsSeries(close, true); ArraySetAsSeries(open, true); int limit = rates_total - prev_calculated; if(limit == 0) { } else if(limit == 1) { lb[1] = (open[1] + close[1]) / 2; return(rates_total); } else if(limit > 1) { ArrayInitialize(lb, EMPTY_VALUE); limit = rates_total - 4; for(int i = limit; i >= 1 && !IsStopped(); i--) { lb[i] = (open[i] + close[i]) / 2; } return(rates_total); } lb[0] = (open[0] + close[0]) / 2; return(rates_total); } //+------------------------------------------------------------------+
Los cálculos se reducen al cálculo del promedio open[i] + close[i]. El código fuente del indicador se muestra en el archivo adjunto MVC_primitive_1.zip.
El indicador se ha escrito muy mal y una mirada experimentada se dará cuenta de inmediato. Supongamos que necesitamos cambiar el método de cálculo y usar close[i] en lugar de open[i] + close[i]. Podemos ver fácilmente que incluso en un indicador tan primitivo hay tres lugares donde se deben realizar cambios. ¿Y si continuamos desarrollando esos cambios, e incluso complicándolos? La respuesta resulta obvia: lo haremos así para sacar los cálculos a una función aparte. Esto nos permitirá modificar la lógica si fuera necesario, para realizar las correcciones solo en ella.
Ahora, el manejador junto con la nueva característica tiene el siguiente aspecto:
double Prepare(const datetime &t[], const double &o[], const double &h[], const double &l[], const double &c[], int shift) { ArraySetAsSeries(c, true); ArraySetAsSeries(o, true); return (o[shift] + c[shift]) / 2; } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { if(rates_total <= 4) return 0; int limit = rates_total - prev_calculated; if (limit == 0) { } else if (limit == 1) { lb[1] = Prepare(time, open, high, low, close, 1); return(rates_total); } else if (limit > 1) { ArrayInitialize(lb, EMPTY_VALUE); limit = rates_total - 4; for(int i = limit; i >= 1 && !IsStopped(); i--) { lb[i] = Prepare(time, open, high, low, close, i); } return(rates_total); } lb[0] = Prepare(time, open, high, low, close, 0); return(rates_total); }
Observe que casi todas las series temporales se transmiten a la nueva función. ¿Por qué? Esto no es obligatorio, ya que luego se usan solo dos series temporales: open y close. Pero estamos investigando el futuro y asumimos que hay muchas mejoras por delante en nuestro indicador; aquí nos serán útiles las demás series. En esencia, estamos constryendo unos cimientos sólidos para futuras versiones.
Ahora, vamos a analizar el código obtenido desde el punto de vista de la plantilla de MVC.
- Vista. Como ya hemos dicho, este es el componente que proporciona los datos al usuario, así que resulta obvio que deberá incluir un código relacionado con los búferes de indicador. Aquí debemos considerar también el código OnInit(), en nuestro caso, el código completo.
- Modelo. En nuestro indicador hay un modelo muy sencillo que consta de una línea. Calculamos la media entre open y close. A continuación, la Vista se acutaliza ya sin nuestra participación. Por consiguiente, en el componente Modelo entrará solo la función Prepare, que ya hemos escrito directamente considerando el desarrollo futuro.
- Controlador. Este componente es responsable de la comunicación entre los otros dos y la interacción con el usuario. Partiendo de esto, le dejaremos los manejadores de eventos, los parámetros de entrada del indicador, y eso será todo. También se encarga de llamar a la función Prepare, que sirve como entrada para el Modelo. Esta llamada obligará al Modelo a cambiar su estado debido a la llegada de nuevos ticks y al cambio de la historia de precios del símbolo.
Vamos a intentar reconstruir nuestro indicador basándonos en lo anterior. Para ello, sacaremos el código de algunos componentes individuales no solo a otros archivos, sino también a otras carpetas. Merece la pena hacer esto, recordando que puede haber muchas Vistas y que, al mismo tiempo, el Modelo puede contener otros modelos, sin olvidar que el Controlador puede ser arbitrariamente complejo. Esto es en lo que se convertirá ahora el archivo principal del indicador:
//+------------------------------------------------------------------+ //| MVC_primitive_2.mq5 | //| Copyright 2021, Andrei Novichkov. | //| https://www.mql5.com/en/users/andreifx60/news | //+------------------------------------------------------------------+ #property copyright "Copyright 2021, Andrei Novichkov." #property link "https://www.mql5.com/en/users/andreifx60/news" #property version "1.00" #property indicator_chart_window #property indicator_buffers 1 #property indicator_plots 1 #include "View\MVC_View.mqh" #include "Model\MVC_Model.mqh" //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { return Initialize(); } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { if(rates_total <= 4) return 0; int limit = rates_total - prev_calculated; if (limit == 0) { } else if (limit == 1) { lb[1] = Prepare(time, open, high, low, close, 1); return(rates_total); } else if (limit > 1) { ArrayInitialize(lb, EMPTY_VALUE); limit = rates_total - 4; for(int i = limit; i >= 1 && !IsStopped(); i--) { lb[i] = Prepare(time, open, high, low, close, i); } return(rates_total); } lb[0] = Prepare(time, open, high, low, close, 0); return(rates_total); } //+------------------------------------------------------------------+
Unas cuantas palabras sobre las propiedades del indicador:
#property indicator_buffers 1 #property indicator_plots 1
Estas dos líneas las podemos sacar a la Vista (al archivo MVC_View.mqh). No obstante, después de ello, recibiremos una observación de parte del compilador:
no indicator plot defined for indicator
Por consiguiente, dejaremos estas dos líneas en el archivo principal con el código del controlador. El código fuente de este indicador se ubica en el archivo adjunto MVC_primitive_2.zip.
Vamos a considerar la siguiente circunstancia: la comunicación entre los componentes individuales de la plantilla. En estos momentos, como tales, no hay ninguno. Nosotros incluimos dos archivos de inclusión y todo funciona. En particular, la Vista incluye un búfer de indicador en forma de variable global y una función en la que se realiza la inicialización. Vamos a reescribir esta parte de una forma más correcta y segura. Para ello, combinaremos en un mismo objeto el búfer, su inicialización y el acceso al mismo. Esto nos proporcionará de inmediato un código adecuado y compacto, fácil de depurar y mantener. Además, este enfoque ofrecerá al desarrollador todas las oportunidades para su mejora posterior. Permitirá extraer una parte del código a una clase básica o una interfaz, o crear una matriz de Vistas, por ejemplo. Este sería el aspceto de una nueva Vista:
class CView { public: void CView(); void ResetBuffers(); int Initialize(); void SetData(double value, int shift = 0); private: double _lb[]; int _Width; string _Name; string _Label; };// class CView void CView::CView() { _Width = 2; _Name = "Primitive" ; _Label = "Label1"; }// void CView::CView() void CView::ResetBuffers() { ArrayInitialize(_lb, EMPTY_VALUE); } int CView::Initialize() { SetIndexBuffer (0, _lb, INDICATOR_DATA); ArraySetAsSeries (_lb, true); IndicatorSetString (INDICATOR_SHORTNAME, _Name); IndicatorSetInteger(INDICATOR_DIGITS, _Digits); PlotIndexSetString (0, PLOT_LABEL, _Label); PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_LINE); PlotIndexSetInteger(0, PLOT_LINE_COLOR, clrDarkSlateBlue); PlotIndexSetInteger(0, PLOT_LINE_STYLE, STYLE_SOLID); PlotIndexSetInteger(0, PLOT_LINE_WIDTH, _Width); return(INIT_SUCCEEDED); } void CView::SetData(double value,int shift) { _lb[shift] = value; }
Fijémonos en el último método, SetData. Vamos a prohibir el acceso incontrolado al búfer de indicador y a proporcionar para ello un método al que podamos añadir comprobaciones adicionales o que podamos declarar virtual en la clase básica. También tendremos cambios en el archivo del controlador, pero no los presentaremos aquí, dado que son de importancia menor. Además, resulta bastante obvio que no existe ningún otro constructor en el que podamos transmitir los parámetros de inicialización del búfer: color, estilo, etcétera.
También nos parecen bastante dudosas las llamadas realizadas en el objeto de la primera Vista:
IndicatorSetString (INDICATOR_SHORTNAME, _Name); IndicatorSetInteger(INDICATOR_DIGITS, _Digits);
Está claro que en condiciones reales deberemos eliminarlas de la clase CView, considerando que:
- Puede haber muchas vistas,
- ¡y estas dos líneas de código no están relacionadas con la Vista! Hablamos de la inicialización del indicador en general, así que las dejaremos en el archivo del Controlador, en el manejador OnInit.
El código fuente del indicador se muestra en el directorio adjunto MVC_primitive_3.zip.
Por consiguiente, el archivo principal del indicador (el archivo con el código del controlador) es ahora significativamente más corto. El código completo ahora es más seguro, y está listo para futuras actualizaciones y la depuración. Pero, ¿resulta más claro por ello para un desarrollador externo? Es bastante dudoso. Más bien, debemos suponer que en este caso particular, no deberíamos haber hecho nada de lo anterior, pero resultaba mejor guardar el código completo del indicador en un archivo que conectara el Controlador, el Modelo y la Vista. Tal y como era al principio.
Sí, este punto de vista resulta lógico y está justificado, pero no vamos a defender que sea el correcto. Esto es solo aplicable para este indicador específico. Imaginemos un indicador que consta de una docena de archivos, con un panel gráfico y solicitud de los datos de la red. En este caso, el modelo MVC resultará más que adecuado. Dicha herramienta será fácil de comprender por parte de terceros. Además, en ella resultará cómodo encontrar y corregir errores, así como modificar, eliminar y añadir lógica. ¿Necesitamos implicar a un especialista que se encargue de un área de trabajo específica? Sería mucho más fácil hacer esto. ¿Y añadir otro esquema de inicialización? Obviamente. De lo anterior, podemos extraer una conclusión bastante obvia: a medida que aumenta la complejidad del proyecto, el uso de la plantilla de MVC se hace cada vez más útil y justificado.
Pero, ¿tal vez todo este razonamiento resulte cierto solo para los indicadores? Vamos a analizar la estructura de los asesores expertos y a comprobar si la plantilla de MVC se puede aplicar a estos.
MVC en asesores expertos
Vamos a contruir el pseudo-asesor más simple. Tendrá que abrir una posición de compra si la vela anterior era alcista y una posición de venta si era bajista. Para mayor simplicidad, el asesor no abrirá posiciones reales, sino que simplemente simulará la entrada y la salida. Asimismo, acordaremos que solo habrá una posición en el mercado. En este caso, el código del asesor (Ea_primitive.mq5, se encuentra en el archivo adjunto) tendrá el aspecto que sigue:
datetime dtNow; int iBuy, iSell; int OnInit() { iBuy = iSell = 0; return(INIT_SUCCEEDED); } void OnDeinit(const int reason) { } void OnTick() { if (IsNewCandle() ) { double o = iOpen(NULL,PERIOD_CURRENT,1); double c = iClose(NULL,PERIOD_CURRENT,1); if (c < o) { // Enter Sell if (GetSell() == 1) return; if (GetBuy() == 1) CloseBuy(); EnterSell(); } else { // Enter Buy if (GetBuy() == 1) return; if (GetSell() == 1) CloseSell(); EnterBuy(); } }// if (IsNewCandle() ) }// void OnTick() bool IsNewCandle() { datetime d = iTime(NULL, PERIOD_CURRENT, 0); if (dtNow == -1 || dtNow != d) { dtNow = d; return true; } return false; }// bool IsNewCandle() void CloseBuy() {iBuy = 0;} void CloseSell() {iSell = 0;} void EnterBuy() {iBuy = 1;} void EnterSell() {iSell = 1;} int GetBuy() {return iBuy;} int GetSell() {return iSell;}
Tras analizar los indicadores, hemos concluido que los manejadores OnInit, OnDeinit y otros se relacionan con el Controlador, y que no hay razón para cambiar esta decisión en el caso de los asesores. Pero, ¿qué debemos atribuir a la Vista? No hay muestra de gráficos, ni diagramas. No olvidemos que la Vista se encarga de presentar los datos al usuario. Y en el caso de los asesores expertos, la presentación de datos supone la representación de las posiciones abiertas, y, por tanto, de todo el servicio que se pueda relacionar con ellas: órdenes, trailing, stop loss y take profit virtuales, precios medios ponderados, etc.
Entonces, deberemos atribuir al Modelo la lógica de toma de decisiones para abrir posiciones, la elección del tamaño del lote, el take profit y el stop loss. La gestión de capital también debe atribuirse al Modelo. En este caso, empiezan a dibujarse los contornos de un sistema cerrado, que constará de varios submodelos: análisis de precios, cálculo de volumen, verificación del estado de la cuenta (con otros submodelos, posiblemente) y, como resultado, la decisión sobre la oportunidad de entrar en el mercado.
Vamos a modificar la estructura del pseudo-asesor según las consideraciones anteriores. Obviamente, no dispondremos del cálculo del volumen y el trabajo con la cuenta, por lo que ejecutaremos los pasos que podamos: moveremos las funciones relacionadas con los diferentes componentes a sus subcarpetas y editaremos algunas de ellas. Así es como cambiará el pseudocódigo del controlador OnTick:
void OnTick() { if (IsNewCandle() ) { double o = iOpen(NULL,PERIOD_CURRENT,1); double c = iClose(NULL,PERIOD_CURRENT,1); if (MaySell(o, c) ) EnterSell(); if (MayBuy(o, c) ) EnterBuy(); }// if (IsNewCandle() ) }// void OnTick()
Incluso en esta pequeña área, se nota que el código se ha vuelto, cuanto menos, más corto. Pero, ¿resulta ahora más claro para un desarrollador externo? Aquí también serán válidas las consideraciones planteadas para el ejemplo con el indicador:
- Cuanto más complejo y voluminoso resulte el asesor, más útil y justificado será el uso de MVC.
El asesor completo se encuentra en el archivo adjunto MVC_EA_primitive.zip. Ahora, ha llegado el momento de aplicar la plantilla de MVC no al pseudo-asesor, sino al código "real".
Para ello, elegiremos un asesor simple: no necesariamente uno que funcione y no necesariamente uno bien escrito. Por el contrario, incluso si este asesor experto está mal escrito, los errores y deficiencias revelarán aún mejor los resultados de la utilización de la plantilla, mostrando las posibles formas de corregirlos.
Para el experimento, hemos encontrado un borrador antiguo del asesor $OrdersInTheMorning, creado en 2013. Su estrategia era la siguiente:
- El lunes, a una hora determinada, el asesor abría dos órdenes pendientes de compra y venta a cierta distancia del mercado. Al activarse una orden, se eliminaba la segunda. La orden se cerraba el viernes por la noche. El trabajo se realizaba con una lista específica de parejas de divisas.
Como el asesor se desarrolló para MetaTrader 4, tuvo que rehacerse para operar en MetaTrader 5. Dicho proceso se realizó con mucha negligencia. Vamos a ver las principales funciones del asesor experto en su forma inicial:
#property copyright "Copyright 2013, MetaQuotes Software Corp." #property link "http://www.metaquotes.net" #include <Trade\Trade.mqh> //+------------------------------------------------------------------+ //| script program start function | //+------------------------------------------------------------------+ input double delta = 200; input double volumes = 0.03; input double sTopLossKoeff = 1; input double tAkeProfitKoeff = 2; input int iTHour = 0; input bool bHLprocess = true; input bool oNlyMondeyOrders = false; input string sTimeToCloseOrders = "22:00"; input string sTimeToOpenOrders = "05:05"; input double iTimeIntervalForWork = 0.5; input int iSlippage = 15; input int iTradeCount = 3; input int iTimeOut = 2000; int dg; bool bflag; string smb[] = {"AUDJPY","CADJPY","EURJPY","NZDJPY","GBPJPY","CHFJPY"}; int init () { if ( (iTimeIntervalForWork < 0) || (iTimeIntervalForWork > 24) ) { Alert ("... ",iTimeIntervalForWork); } return (0); } void OnTick() { if ((oNlyMondeyOrders == true) && (DayOfWeek() != 1) ) { } else { int count=ArraySize(smb); bool br = true; for (int i=0; i<count;i++) { if (!WeekOrderParam(smb[i], PERIOD_H4, delta*SymbolInfoDouble(smb[i],SYMBOL_POINT) ) ) br = false; } if (!br) Alert("..."); bflag = true; }//end if if ((oNlyMondeyOrders == true) && (DayOfWeek() != 1) ) else... if ((oNlyMondeyOrders == true) && (DayOfWeek() != 5) ) { } else { if (OrdersTotal() != 0) Alert ("..."); }//end if ((oNlyMondeyOrders == true) && (DayOfWeek() != 5) ) else... } bool WeekOrderParam(string symbol,int tf, double dlt) { int j = -1; datetime mtime = 0; int k = 3; Alert(symbol); if (iTHour >= 0) { if (oNlyMondeyOrders == true) { for (int i = 0; i < k; i++) { mtime = iTime(symbol,0,i); if (TimeDayOfWeek(mtime) == 1) { if (TimeHour(mtime) == iTHour) { j = i; break; } } } } else { for (int i = 0; i < k; i++) { mtime = iTime(symbol,0,i); if (TimeHour(mtime) == iTHour) { j = i; break; } } } if (j == -1) { Print("tf?"); return (false); } }//end if (iTHour >= 0) else j = 0; Alert(j); double bsp,ssp; if (bHLprocess) { bsp = NormalizeDouble(iHigh(symbol,0,j) + dlt, dg); ssp = NormalizeDouble(iLow(symbol,0,j) - dlt, dg); } else { bsp = NormalizeDouble(MathMax(iOpen(symbol,0,j),iClose(symbol,0,j)) + dlt, dg); ssp = NormalizeDouble(MathMin(iOpen(symbol,0,j),iClose(symbol,0,j)) - dlt, dg); } double slsize = NormalizeDouble(sTopLossKoeff * (bsp - ssp), dg); double tpb = NormalizeDouble(bsp + tAkeProfitKoeff*slsize, dg); double tps = NormalizeDouble(ssp - tAkeProfitKoeff*slsize, dg); datetime expr = 0; return (mOrderSend(symbol,ORDER_TYPE_BUY_STOP,volumes,bsp,iSlippage,ssp,tpb,NULL,0,expr,CLR_NONE) && mOrderSend(symbol,ORDER_TYPE_SELL_STOP,volumes,ssp,iSlippage,bsp,tps,NULL,0,expr,CLR_NONE) ); } int mOrderSend( string symbol, int cmd, double volume, double price, int slippage, double stoploss, double takeprofit, string comment = "", int magic=0, datetime expiration=0, color arrow_color=CLR_NONE) { int ticket = -1; for (int i = 0; i < iTradeCount; i++) { // ticket=OrderSend(symbol,cmd,volume,price,slippage,stoploss,takeprofit,comment,magic,expiration,arrow_color); if(ticket<0) Print(symbol,": ",GetNameOP(cmd), GetLastError() ,iTimeOut); else break; } return (ticket); }
Bien, entonces, tenemos un bloque de inicialización, un manejador OnTick y funciones auxiliares. Como antes, dejaremos los manejadores en el Controlador, corrigiendo la llamada obsoleta init. Ahora, centraremos nuestra atención en OnTick. Dentro del manejador, existen algunas comprobaciones y un ciclo en el que se llama a la función auxiliar WeekOrderParam, que concentra las decisiones en entrada en el mercado y apertura de posiciones. Este enfoque es absolutamente incorrecto, como podemos ver a primera vista por esta función, demasiado larga, con muchas condiciones y ciclos anidados. Deberíamos dividir esta función en al menos dos partes. La última función mOrderSend no plantea ninguna pregunta especial y se relaciona con la Vista, en base a las consideraciones expresadas anteriormente. Además de modificar la estructura del asesor según la plantilla, deberemos corregir el código en sí. Haremos breves comentarios a medida que surja oportunidad.
Comenzaremos moviendo la lista de parejas de divisas a los parámetros de entrada. Después, eliminaremos la basura del controlador OnInit. Luego, crearemos el archivo EA_Init.mqh, a donde sacaremos todos los detalles referentes a la inicialización, y lo conectaremos al archivo principal. Acto seguido, crearemos una clase en el nuevo archivo y efectuaremos toda la inicialización ahí:
class CInit { public: void CInit(){} void Initialize(string pair); string names[]; double points[]; int iCount; }; void CInit::Initialize(string pair) { iCount = StringSplit(pair, StringGetCharacter(",", 0), names); ArrayResize(points, iCount); for (int i = 0; i < iCount; i++) { points[i] = SymbolInfoDouble(names[i], SYMBOL_POINT); } }
El código es muy sencillo y no debería plantear preguntas. no obstante, destacaremos algunos puntos:
- Todos los miembros de la clase son públicos, lo cual no resulta muy correcto. Esto se ha hecho de forma excepcional para no saturar el código con múltiples métodos para acceder a miembros privados.
- Solo hay un método en la clase y, por consiguiente, podemos usar solo este, sin utilizar la clase. Pero, en dicho caso, todos los datos estarían disponibles para el acceso global, lo cual nos gustaría evitar.
- Esta clase, que ejecuta la función de interacción con el usuario, forma parte del Controlador.
Vamos a crear un objeto del tipo de clase creado en el archivo principal del asesor, llamando luego a su método de inicialización en el controlador OnInit.
Ahora, vamos a ocuparnos del modelo. Eliminamos todo el contenido del manejador OnTick. Después, creamos la carpeta Model, y en ella, el archivo Model.mqh. A continuación, creamos en el nuevo archivo la clase CModel; esta contiene dos métodos encargados de verificar las condiciones para entrar en el mercado y salir del mismo. Además, en la propia clase, guardamos la bandera que indica que las posiciones están abiertas o cerradas. Debemos tener en cuenta que, si no fuera por la necesidad de almacenar esta bandera, la existencia de toda la clase resultaría cuestionable. Sería suficiente con un par de funciones. Además, en la vida real, deberían realizarse ciertas comprobaciones adicionales. El volumen, la disponibilidad de fondos, etcétera. Como hemos señalado anteriormente, todo esto debe atribuirse al Modelo. Por ahora, el archivo que contiene el modelo se verá así:
class CModel { public: void CModel(): bFlag(false) {} bool TimeToOpen(); bool TimeToClose(); private: bool bFlag; }; bool CModel::TimeToOpen() { if (bFlag) return false; MqlDateTime tm; TimeCurrent(tm); if (tm.day_of_week != 1) return false; if (tm.hour < iHourOpen) return false; bFlag = true; return true; } bool CModel::TimeToClose() { if (!bFlag) return false; MqlDateTime tm; TimeCurrent(tm); if (tm.day_of_week != 5) return false; if (tm.hour < iHourClose) return false; bFlag = false; return true; }
Al igual que en el caso anterior, creamos un objeto del tipo de esta clase en el archivo principal del asesor y añadimos las llamadas a sus métodos en el manejador OnTick.
Ahora, vamos a ocuparnos de la Vista. Después, creamos la carpeta View, y en ella, el archivo View.mqh. Aquí, como ya hemos mencionado antes, se colocarán los fondos para abrir/cerrar las órdenes y posiciones. También se hallarían aquí los componentes para gestionar los niveles virtuales, el trailing y diversos elementos gráficos. En este caso, daremos preferencia a la máxima accesibilidad y simplicidad del código, por lo que, para variar, trataremos de ejecutar el componente View sin usar clases. En total, el componente Vista tendrá tres funciones por ahora. Una para entrar al mercado, otra para cerrar todas las posiciones, y una tercera para cerrar las órdenes. Llama la atención que cada una de las tres funciones use un objeto del tipo CTrade, que deberá crearse cada vez, lo cual resulta irracional:
void Enter() { CTrade trade; trade.SetExpertMagicNumber(Magic); trade.SetMarginMode(); trade.SetDeviationInPoints(iSlippage); double dEnterBuy, dEnterSell; double dTpBuy, dTpSell; double dSlBuy, dSlSell; double dSlSize; for (int i = 0; i < init.iCount; i++) { dEnterBuy = NormalizeDouble(iHigh(init.names[i],0,1) + delta * init.points[i], _Digits); dEnterSell = NormalizeDouble(iLow(init.names[i],0,1) - delta * init.points[i], _Digits); dSlSell = dEnterBuy; dSlBuy = dEnterSell; dSlSize = (dEnterBuy - dEnterSell) * tAkeProfitKoeff; dTpBuy = NormalizeDouble(dEnterBuy + dSlSize, _Digits); dTpSell = NormalizeDouble(dEnterSell - dSlSize, _Digits); trade.SetTypeFillingBySymbol(init.names[i]); trade.BuyStop(volumes, dEnterBuy, init.names[i], dSlBuy, dTpBuy); trade.SellStop(volumes, dEnterSell, init.names[i], dSlSell, dTpSell); } } void ClosePositions() { CTrade trade; for (int i = PositionsTotal() - 1; i >= 0; i--) { trade.PositionClose(PositionGetTicket(i) ); } } void CloseOrder(string pair) { CTrade trade; ulong ticket; for (int i = OrdersTotal() - 1; i >= 0; i--) { ticket = OrderGetTicket(i); if (StringCompare(OrderGetString(ORDER_SYMBOL), pair) == 0) { trade.OrderDelete(ticket); break; } } }
Por eso, vamos a modificar el código, creando la clase CView. Después, trasladaremos las funciones ya creadas a una nueva clase y crearemos un método adicional de inicialización de componentes para un campo privado del tipo CTrade. Como en los otros casos, crearemos un objeto del tipo de clase creado en el archivo principal y añadiremos una llamada a su método de inicialización en el manejador OnInit.
Queda por implementar la eliminación de órdenes no activadas. Para ello, añadiremos al Controlador el manejador OnTrade. En el controlador, comprobaremos el cambio en el número de órdenes y, si ha cambiado, eliminaremos la orden no activada correspondiente. Este manejador es la única parte complicada del asesor. Vamos a crear un método en la clase CView y a llamarlo desde el manejador OnTrade del Controlador. Este será el aspecto final de la Vista:
#include <Trade\Trade.mqh> class CView { public: void CView() {} void Initialize(); void Enter(); void ClosePositions(); void CloseAllOrder(); void OnTrade(); private: void InitTicketArray() { ArrayInitialize(bTicket, 0); ArrayInitialize(sTicket, 0); iOrders = 0; } CTrade trade; int iOrders; ulong bTicket[], sTicket[]; }; void CView::OnTrade() { if (OrdersTotal() == iOrders) return; for (int i = 0; i < init.iCount; i++) { if (bTicket[i] != 0 && !OrderSelect(bTicket[i]) ) { bTicket[i] = 0; iOrders--; if (sTicket[i] != 0) { trade.OrderDelete(sTicket[i]); sTicket[i] = 0; iOrders--; } continue; } if (sTicket[i] != 0 && !OrderSelect(sTicket[i]) ) { sTicket[i] = 0; iOrders--; if (bTicket[i] != 0) { trade.OrderDelete(bTicket[i]); bTicket[i] = 0; iOrders--; } } } } void CView::Initialize() { trade.SetExpertMagicNumber(Magic); trade.SetMarginMode(); trade.SetDeviationInPoints(iSlippage); ArrayResize(bTicket, init.iCount); ArrayResize(sTicket, init.iCount); InitTicketArray(); } void CView::Enter() { double dEnterBuy, dEnterSell; double dTpBuy, dTpSell; double dSlBuy, dSlSell; double dSlSize; for (int i = 0; i < init.iCount; i++) { dEnterBuy = NormalizeDouble(iHigh(init.names[i],0,1) + delta * init.points[i], _Digits); dEnterSell = NormalizeDouble(iLow(init.names[i],0,1) - delta * init.points[i], _Digits); dSlSell = dEnterBuy; dSlBuy = dEnterSell; dSlSize = (dEnterBuy - dEnterSell) * tAkeProfitKoeff; dTpBuy = NormalizeDouble(dEnterBuy + dSlSize, _Digits); dTpSell = NormalizeDouble(dEnterSell - dSlSize, _Digits); trade.SetTypeFillingBySymbol(init.names[i]); trade.BuyStop(volumes, dEnterBuy, init.names[i], dSlBuy, dTpBuy); bTicket[i] = trade.ResultOrder(); trade.SellStop(volumes, dEnterSell, init.names[i], dSlSell, dTpSell); sTicket[i] = trade.ResultOrder(); iOrders +=2; } } void CView::ClosePositions() { for (int i = PositionsTotal() - 1; i >= 0; i--) { trade.PositionClose(PositionGetTicket(i) ); } InitTicketArray(); } void CView::CloseAllOrder() { for (int i = OrdersTotal() - 1; i >= 0; i--) { trade.OrderDelete(OrderGetTicket(i)); } }
Llama la atención que al final se ha reescrito por completo el código original. ¿Ha mejorado? ¡Indudablemente! El resultado de todo el trabajo se encuentra en el archivo adjunto EA_Real.zip. Ahora, el archivo principal del asesor (Controlador) tiene el aspecto que sigue:
input string smb = "AUDJPY, CADJPY, EURJPY, NZDJPY, GBPJPY, CHFJPY"; input double delta = 200; input double volumes = 0.03; input double tAkeProfitKoeff = 2; input int iHourOpen = 5; input int iHourClose = 22; input int iSlippage = 15; input int Magic = 12345; #include "EA_Init.mqh" #include "View\View.mqh" #include "Model\Model.mqh" CInit init; CModel model; CView view; int OnInit() { init.Initialize(smb); view.Initialize(); return INIT_SUCCEEDED; } void OnTick() { if (model.TimeToOpen() ) { view.Enter(); return; } if (model.TimeToClose() ) { view.CloseAllOrder(); view.ClosePositions(); } } void OnTrade() { view.OnTrade(); }
Ahora, para modificar, añadir o corregir algo, bastará con trabajar con una parte del asesor, con un componente aparte. Además, queda inmediatamente claro dónde podemos ubicar este componente. El asesor puede desarrollarse aumentando su funcionalidad, añadiendo Modelos y desarrollando la Vista. Incluso podemos modificar por completo uno de los componentes sin tocar apenas los otros dos.
Y por último,
en la aplicación de MVC que hemos analizado, hay un aspecto que mencionamos casualmente al principio del artículo. Se trata de la interacción de los componentes de la plantilla entre sí. Desde el punto de vista del usuario, no existe ningún problema: tenemos un Controlador, podemos añadirle una ventana de diálogo, o un panel comercial. También tenemos los parámetros de entrada, como parte del Controlador. Pero, ¿cómo deberían interactuar el Modelo y la Vista? En nuestro asesor, la respuesta a esta pregunta concreta es muy simple: de ninguna forma. No interactúan directamente, sino solo a través del Controlador, en el manejador OnTick. Además, la Vista se comunica con el Controlador de forma semejante: llamando "directamente" a los métodos de un objeto de tipo CInit. En este caso, la interacción de los componentes se organiza mediante sus objetos globales, que se ven mutuamente. Esto está permitido, y se ve justificado por la simplicidad del propio asesor y nuestro deseo de no sobrecargar el código.
No obstante, a pesar de la simplicidad del código, la Vista tiene hasta once llamadas al Controlador. Si desarrollamos más el asesor, el número de interacciones podrá aumentar sustancialmente, reduciendo a la nada el efecto positivo de la plantilla de MVC. Podemos resolver este problema renunciando a los objetos globales y accediendo a los componentes y métodos mediante referencias. Un ejemplo de este tipo de interacción es la MFC y sus componentes Document y View.
Destacaremos que, desde el punto de vista de la plantilla, las formas de interacción entre los componentes de la misma no están reguladas de modo alguno. Por consiguiente, no vamos a profundizar en este tema; nos limitaremos únicamente a indicar la existencia del problema en sí, esbozando, asimismo, dos formas de solucionarlo.
Conclusión
Como conclusión, hableremos sobre cómo podríamos desarrollar la estructura de un indicador o un asesor experto al que se ha aplicado inicialmente el patrón MVC. Supongamos que hay dos Modelos más, y además, otra Vista. El Controlador es ahora mucho más complejo. ¿Qué deberíamos hacer entonces para permanecer dentro del marco de MVC? ¡Utilizar módulos separados en el proyecto! Todo es muy sencillo: hay tres componentes. Cada uno de ellos ofrece una forma de acceder a sí mismo. Y cada uno de los componentes consta de módulos separados. Ya hemos analizado este método aquí. En ese mismo artículo, también analizamos las formas de interacción a nivel de módulos y la gestión de los mismos.
Programas utilizados en el artículo:
# | Nombre | Tipo | Descripción |
---|---|---|---|
1 | MVC_primitive_1.zip | Fichero | Primera (y peor) versión del indicador. |
2 | MVC_primitive_2.zip | Fichero | Segunda versión del indicador con separación por componentes. |
3 | MVC_primitive_3.zip | Fichero | Tercera versión del indicador con objetos. |
4 | EA_primitive.zip | Fichero | Pseudo-asesor |
5 | MVC_EA_primitive.zip | Fichero | Pseudo-asesor según las reglas de MVC. |
6 | EA_Real.zip | Fichero | Asesor según las reglas de MVC. |
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/9168
![Otras clases en la biblioteca DoEasy (Parte 67): Clase de objeto de gráfico](https://c.mql5.com/2/42/MQL5-avatar-doeasy-library__5.png)
![Redes neuronales: así de sencillo (Parte 13): Normalización por lotes (Batch Normalization)](https://c.mql5.com/2/48/Neural_networks_made_easy_013.png)
![Otras clases en la biblioteca DoEasy (Parte 68): Clase de objeto de ventana de gráfico y clases de objetos de indicador en la ventana del gráfico](https://c.mql5.com/2/42/MQL5-avatar-doeasy-library__6.png)
![Aproximación por fuerza bruta a la búsqueda de patrones (Parte IV): Funcionalidad mínima](https://c.mql5.com/2/41/1560775468.png)
![MQL5 - Lenguaje de estrategias comerciales para el terminal de cliente MetaTrader 5](https://c.mql5.com/i/registerlandings/logo-2.png)
- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso