![Padrão de design MVC e a possibilidade de usá-lo](https://c.mql5.com/i/articles/overlay.png)
Padrão de design MVC e a possibilidade de usá-lo
Introdução
Vou arriscar e supor que a grande maioria dos desenvolvedores passou por um estágio em que, conforme o projeto se desenvolvia, tornava-se mais complexo e ficava sobrecarregado de funcionalidade, o código se transformava numa espécie de espaguete. O trabalho ainda não terminava, e já era muito difícil lembrar onde nessa bagunça estava a chamada deste ou daquele método, porque esta chamada estava localizada exatamente ali e como tudo funcionava.
E o mais triste é que depois de algum tempo era completamente impossível entender o código-fonte, mesmo para o autor do código. E nem vale a pena falar sobre o caso em que escolhem outra pessoa para desmontar essa bagunça. A tarefa fica praticamente sem solução se a essa altura o autor do código não estiver disponível por algum motivo. O código não estruturado é muito difícil de manter e modificar quando o programa é mais complexo do que apenas "Hello, world". Esta é uma das razões para o surgimento de padrões de design. Eles geram uma certa estrutura dentro do projeto, tornam-no mais claro, mais visual.
O padrão MVC e por que precisamos dele
Esse padrão apareceu há muito tempo (em 1978), mas foi descrito pela primeira vez muito depois, em 1988, quando foi publicada sua descrição final. Desde então, o padrão foi mudando, tornando-se mais complexo, dando origem a novas abordagens.
Neste artigo, iremos focar no "MVC clássico", sem complicações e funcionalidades adicionais. Seu propósito é o de "dividir" o código existente em três componentes separados: Modelo (Model), Visualização (View) e Controlador (Controller). A essência do padrão MVC é que esses três componentes podem ser desenvolvidos e mantidos independentemente um do outro. Um grupo separado de desenvolvedores pode trabalhar em cada componente, iniciar novas versões, corrigir bugs. É bastante óbvio que, neste caso, torna-se muito mais fácil trabalhar num projeto comum, sendo que pode ser mais rápido e fácil entender o projeto de outra pessoa.
Vamos dar uma olhada no que cada componente é individualmente.
- Visualização (View). A Visualização é responsável pela parte visual. De forma mais geral, ele envia dados ao usuário. Reparemos que, na realidade, pode haver várias maneiras de apresentar dados ao usuário. Por exemplo, os dados podem ser representados por uma tabela, gráfico ou diagrama ao mesmo tempo. Em outras palavras, pode haver várias Visualizações num único aplicativo construído com base no esquema MVC. As Visualizações recebem dados do modelo sem ter nenhuma ideia do que está acontecendo dentro do Modelo.
- Modelo (Model). O Modelo contém dados. Ele estabelece comunicação com bases de dados, envia consulta para a rede, para outras fontes. Ele modifica os dados, valida-os, armazena-os e exclui-os. O Modelo não sabe nada sobre como funciona a Visualização e quantas estão disponíveis, mas possui as interfaces necessárias através das quais elas podem solicitar dados. As Visualizações não podem fazer mais nada - elas não podem forçar o Modelo a mudar seu estado. Isso é feito pelo Controlador. Internamente, um Modelo pode ser composto por vários outros, organizados numa hierarquia ou funcionando igualmente. Nesse sentido, nenhuma restrição é imposta ao Módulo, exceto as já mencionadas - o Modelo mantém sua estrutura interna secreta da Visualização e do Controlador.
- Controlador (Controller). O Controlador estabelece a comunicação entre o usuário e o Modelo. O Controlador não sabe o que o Modelo está fazendo com os dados, mas pode dizer a este que é hora de atualizar o conteúdo. Em geral, o Controlador, por sua interface, trabalha com o Modelo, sem tentar entender o que está acontecendo dentro dele.
Visualmente, a relação entre os componentes individuais do padrão MVC é algo assim:
Ainda assim, não existem regras e restrições particularmente rígidas sobre o uso de MVC. O desenvolvedor deve ter cuidado para não colocar parte da lógica (ou mesmo toda a lógica) de trabalho com os dados do Modelo no Controlador e não entrar na Visualização. Além disso, o próprio Controlador deve ser mais "fino" sem sobrecarregá-lo. Reparemos também que o esquema MVC é usado para outros padrões de design, por exemplo, "Observador" e "Estratégia".
Agora, vamos tentar entender como usar o padrão MVC em MQL e, o mais importante, saber se é mesmo necessário.
Indicador mais simples do ponto de vista MVC
Vamos criar um indicador primitivo que desenhará uma linha usando o cálculo mais simples. O indicador é muito pequeno e colocaremos seu código num arquivo. É assim que pode ficar:
....... #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); } //+------------------------------------------------------------------+
Os cálculos são reduzidos ao cálculo do valor médio open[i] + close[i]. O código-fonte do indicador está disponível no arquivo anexo MVC_primitive_1.zip.
O indicador está muito mal escrito e um olho experiente reparará imediatamente. Suponhamos que haja necessidade de alterar o método de cálculo e, em vez de open[i] + close[i], usemos apenas close[i]. É fácil ver que mesmo num indicador tão primitivo, há três lugares onde devem ser feitas mudanças. Se essas mudanças continuarem, será que se tornaram mais complexas? A saída óbvia é mover os cálculos para uma função separada. Isso possibilitará, se necessário, alterar a lógica para fazer correções apenas nela.
Agora, o manipulador junto com a nova função fica assim:
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); }
Repare que quase todas as séries de tempo são passadas para a nova função. Por que motivo? Isso não é necessário, afinal, apenas duas séries de tempo são usadas -open e close. Mas olhamos para o futuro e assumiremos que existem muitas melhorias à frente para nosso indicador, o resto virá a calhar, na verdade, estaremos estabelecendo uma plataforma sólida para versões futuras.
Agora, vamos examinar o código resultante do ponto de vista do padrão MVC.
- Visualização. Como já dissemos, ela é um componente que apresenta dados ao usuário, portanto, é óbvio que o código associado aos buffers do indicador deve fazer parte dela. Isso também deve incluir o código a partir de OnInit(), no nosso caso, todo ele.
- Modelo. Nosso indicador possui um Modelo simples de uma linha. Calculamos a média entre open e close. Em seguida, a Visualização é atualizada sem a nossa participação. Portanto, no componente Modelo será incluída apenas a função Prepare, que escrevemos imediatamente levando em consideração o desenvolvimento futuro.
- Controlador. O Componente é responsável pela comunicação entre dois outros componentes e pela interação do usuário. Com base nisso, deixamos a ele com manipuladores de eventos, parâmetros de entrada do indicador. Ele também chama a função Prepare, que serve como uma entrada para o Modelo. Tal chamada forçará o Modelo a mudar seu estado por causa da chegada de novos ticks e quando o histórico de preços do símbolo muda.
Vamos tentar reconstruir nosso indicador com base no descrito acima. Vamos mover o código de componentes individuais não apenas para outros arquivos, mas também para outras pastas. Vale a pena fazer isso, lembrando que podem existir várias Visualizações, o Modelo pode conter outros Modelos e o Controlador pode ser complexo. É nisto que o arquivo principal do indicador se tornará agora:
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
Algumas palavras sobre as propriedades do indicador:
#property indicator_buffers 1 #property indicator_plots 1
Estas duas linhas podem ser removidas para a Visualização (para o arquivo MVC_View.mqh) No entanto, depois disso, receberemos um comentário do compilador:
no indicator plot defined for indicator
Por isso, deixamos essas duas linhas no arquivo principal com o código do Controlador. O código-fonte deste indicador está no arquivo anexado MVC_primitive_2.zip.
Vamos prestar atenção à seguinte situação no que diz respeito à comunicação entre os componentes individuais do modelo. No momento, não há, de fato, nenhuma situação desse tipo. Anexamos dois arquivos de inclusão e tudo funciona. Em particular, a Visualização inclui um buffer de indicador na forma de uma variável global e uma função na qual a inicialização ocorre. Vamos reescrever esta parte de uma forma mais correta e segura. Vamos combinar o buffer, sua inicialização e o acesso a ele num objeto. Isso nos dará imediatamente um código compacto, bem como fácil de usar, de depurar e de manter. Além disso, esta abordagem permitirá ao desenvolvedor ter todos os recursos para realizar melhorias no futuro. Ele conseguirá extrair uma parte do código numa classe base ou interface, fazer uma matriz de Visualizações, por exemplo, esta pode ser a aparência de uma nova Visualização:
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; }
Preste atenção ao último método SetData. Restringimos o acesso não controlado ao buffer de indicador e criamos um método para isso, método esse ao qual podemos adicionar verificações adicionais ou declará-lo virtual na classe base. Também há alterações no arquivo do Controlador, mas aqui elas não serão o foco. Além disso, é bastante óbvio que não há nenhum outro construtor ao qual se possa passar os parâmetros de inicialização do buffer - cor, estilo, etc.
As chamadas feitas no objeto da primeira Visualização ainda são bastante duvidosas:
IndicatorSetString (INDICATOR_SHORTNAME, _Name); IndicatorSetInteger(INDICATOR_DIGITS, _Digits);
É claro que em condições reais elas precisarão ser removidas da classe CView, tendo em mente que:
- Pode haver muitas Visualizações.
- Essas duas linhas de código não estão relacionadas à Visualização! Esta é a inicialização do indicador em geral, por isso, vamos deixá-los no arquivo Controller, no manipulador OnInit.
O código-fonte do indicador está disponível no arquivo anexo MVC_primitive_3.zip.
Assim, o arquivo principal do indicador - o arquivo com o código do Controlador - eventualmente se tornou significativamente menor. Todo o código está agora mais seguro e pronto para futuras atualizações e depuração. Mas será que ele agora é mais legível para um desenvolvedor alheio? É algo questionável! Em vez disso, vale a pena admitir que, neste caso particular, nada do acima deveria ter sido feito, mas era melhor salvar todo o código do indicador num arquivo vinculando o Controlador, o Modelo e a Visualização. Como era no início.
Sim, esse ponto de vista faz sentido, é justificado, mas não vamos argumentar que seja correto. Mas apenas neste e em casos semelhantes. Imagine um indicador composto por uma dezena de arquivos, com um painel gráfico e com consultas de dados a partir da rede. Nesse caso, o modelo MVC é mais do que adequado. Pois, outras pessoas poderão facilmente entender, encontrar e corrigir erros, bem como alterar, excluir e adicionar código. E ser for necessário atrair um especialista para se ocupar de área específica do trabalho? Será muito mais fácil fazer isso. E se quisermos adicionar outro esquema de inicialização? Isso é elementar. Quanto a isso, a conclusão é bastante óbvia, conforme a complexidade do projeto cresce, o uso do padrão MVC se torna mais útil e faz mais sentido.
Mas será que todo esse raciocínio só vale para indicadores? Vejamos a estrutura dos Expert Advisors e se o padrão MVC pode ser aplicado nela.
MVC em Expert Advisors
Vamos criar o pseudo-EA bem simples. Ele terá que abrir uma posição de compra se o candle anterior for de alta e uma posição de venda se for de baixa. Para simplificar, o EA não abrirá posições reais, mas simplesmente simulará a entrada e a saída. Vamos combinar que haverá apenas uma posição no mercado. Neste caso, o código do Expert Advisor (Ea_primitive.mq5 está no arquivo anexado) pode ficar assim:
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;}
Após considerarmos os indicadores, já concluímos que os manipuladores OnInit, OnDeinit e outros têm a ver com o Controlador e que não há motivo para alteração dessa decisão no caso de EAs. Mas o que deve ser atribuído à Visualização? Nem a exibição de gráfico, nem diagramas. Lembremo-nos de que a Visualização é responsável por apresentar os dados ao usuário. Já, no caso dos EA, a visualização dos dados são as posições abertas exibidas. Portanto, é todo o serviço que pode ser associado a elas. Ordens, trailing, stop loss e take profit virtuais, preços médios ponderados, etc.
Em seguida, ao Modelo é atribuída a lógica por trás da abertura de posições, do tamanho do lote, do take profit e do stop loss. O gerenciamento do dinheiro também deve ser atribuída ao Modelo. Neste caso, começam a surgir os contornos de um sistema fechado, constituído por vários submodelos - análise de preços, cálculo de volume, verificação do estado de conta (eventualmente outros submodelos) e, por conseguinte, uma decisão sobre a possibilidade de entrada no mercado.
Vamos mudar a estrutura do pseudo-EA de acordo com as considerações acima. Claro, não temos um cálculo do volume e uso de conta, por isso, seguiremos os passos que pudermos - moveremos as funções relacionadas aos diferentes componentes para suas subpastas e editaremos algumas delas. É assim que o pseudocódigo do manipulador OnTick será alterado:
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()
Mesmo nesta seção insignificante, nota-se que o código ficou mais curto. Mas será que ficou mais claro para um desenvolvedor terceirizado? Aqui as ideias que foram aplicadas para o exemplo com o indicador serão válidas:
Quanto mais complexo e grande o EA, mais útil e justificado será o uso do MVC.
Todo o Expert Advisor está no arquivo anexado MVC_EA_primitive.zip. Agora é hora de aplicar o padrão MVC não ao pseudo-EA, mas, sim, ao código "real".
Para esses fins, selecionaremos um EA simples, e não necessariamente funcional e não necessariamente bem escrito. Pelo contrário, mesmo que este Expert Advisor seja mal escrito, erros e deficiências irão revelar ainda mais os resultados do uso do modelo.
Para experimentação, encontrei um antigo rascunho da edição de 2013 com o esboço do EA $OrdersInTheMorning, cuja estratégia era a seguinte:
- Na segunda-feira, no horário especificado, o EA abriu duas ordens pendentes de compra e venda a uma certa distância. Quando uma ordem for acionado, a segunda será excluída. A ordem foi encerrada na noite de sexta-feira. O trabalho ocorreu com uma lista específica de pares de moedas.
Como o EA foi desenvolvido para o MetaTrader 4, ele teve que ser refeito para funcionar no MetaTrader 5, o que foi feito de forma muito descuidada. Vamos apresentar as principais funções do Expert Advisor em sua forma original:
#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); }
Assim, há um bloco de inicialização, um manipulador OnTick e funções auxiliares. Os manipuladores, como antes, ficarão no Controlador, após corrigir a chamada desatualizada init. Agora vamos prestar atenção a OnTick. Dentro do manipulador, algumas verificações e um loop no qual a função auxiliar é chamadaWeekOrderParam que se foca em si mesma na tomada de decisões de entrada no mercado e de abertura de posições. Essa abordagem está absolutamente errada, e isso pode ser notado à primeira vista nesta função, pois ela é grande, muitas vezes tem condições e loops aninhados. Esta função deverá ser dividida em pelo menos duas partes. A última função mOrderSend não levanta nenhuma questão especial e se relaciona com a Visualização. Além de alterar a estrutura do EA de acordo com o modelo, o próprio código deverá ser corrigido. Faremos breves comentários ao longo do caminho.
Vamos transferir imediatamente a lista de pares de moedas para os parâmetros de entrada. Vamos remover o lixo do manipulador OnInit. Vamos criar o arquivo EA_Init.mqh, para retirarmos todos os detalhes de inicialização e anexá-lo ao principal. Criamos uma classe num novo arquivo e executamos todas as inicializações nele:
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); } }
O código é extremamente simples e não deve gerar dúvidas. No entanto, ressaltaremos alguns pontos:
- Todos os membros da classe são públicos, o que não é muito correto. Isso é feito excepcionalmente para não sobrecarregar o código com vários métodos de acesso a membros privados.
- Existe apenas um método na classe e, portanto, é possível nos livrar de apuros graças a ele, sem usar a classe. Mas, neste caso, todos os dados estariam sendo acessados globalmente, o que gostaríamos de evitar.
- Esta classe, desempenhando a função de interagir com o usuário, faz parte do Controlador.
Criamos um objeto do tipo da classe criada no arquivo principal do EA e chamamos seu método de inicialização no manipulador OnInit.
Agora vamos nos ocupar do Modelo. Remover o conteúdo do manipulador OnTick. Criamos uma pasta Model e um arquivo Model.mqh. Vamos criar uma classe CModel num novo arquivo e nele dois métodos para verificar as condições de entrada e saída do mercado. Além disso, na mesma classe, salvaremos o sinalizador que indica que as posições estão abertas ou fechadas. Repare que, se não fosse pela necessidade de armazenar esse sinalizador, a existência de toda a classe seria questionável. Apenas algumas funções seriam suficientes. Veja que, na vida real, certas verificações adicionais teriam que ser realizadas. Volume, disponibilidade de fundos, etc. Tudo isso deve ser atribuído ao Modelo, conforme observado anteriormente. Por enquanto, o arquivo que contém o Modelo tem a seguinte aparência:
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; }
Como no caso anterior, criamos um objeto do tipo desta classe no arquivo principal do Expert Advisor e adicionamos chamadas aos seus métodos no manipulador OnTick.
Agora vamos nos ocupar da Visualização. Criamos uma pasta View e um arquivo View.mqh nela. Aqui, conforme mencionado acima, serão colocados os fundos para abrir/fechar ordens e posições. Os componentes de gerenciamento de níveis virtuais, rede de arrasto e vários elementos gráficos também estariam localizados aqui. Neste caso, a preferência é dada à máxima disponibilidade e simplicidade do código, portanto, para variar, tentaremos executar o componente Visualização sem utilizar classes. No total, o componente Visualização terá três funções até agora. Uma para entrar no mercado, a segunda para fechar todas as posições e a terceira para fechar ordens. Chama a atenção que cada uma das três funções utiliza um objeto do tipo CTrade, que deve ser criado a cada vez, o que não faz sentido:
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 isso, vamos mudar o código criando uma classe CView. Vamos mover as funções já criadas para uma nova classe e criar mais um método de inicialização de componente para um campo privado do tipo CTrade. Como em outros casos, criamos um objeto do tipo da classe criada no arquivo principal e adicionamos uma chamada ao seu método de inicialização no manipulador OnInit.
Resta implementar a remoção de ordens que não se ativaram. Para fazer isso, adicionamos um manipulador OnTrade ao Controlador. No manipulador, verificaremos a alteração no número de ordens e, se houver, excluiremos a ordem que não se ativou correspondente. Este manipulador é a única parte complicada do EA. Vamos criar um método na classe CView e chamá-lo a partir do manipulador OnTrade do Controlador. Isso é o que a Visualização será:
#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)); } }
Vale a pena reparar que no final o código original foi totalmente reescrito. Melhorou? Sem dúvida! O resumo de todo o trabalho está no arquivo anexo EA_Real.zip. Agora, o arquivo principal do Expert Advisor (Controlador) fica assim:
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(); }
Agora, para alterar, adicionar, consertar algo, basta trabalhar com uma parte do Expert Advisor, com um componente separado. Além disso, fica claro onde esse componente pode estar localizado. O Expert Advisor pode se desenvolver aumentando sua funcionalidade, adicionar Modelos e desenvolver uma Visualização. Podemos até mudar completamente um dos componentes, sem tocar nos outros dois.
E finalmente
Dentro da aplicação do MVC que consideramos, há um aspecto que foi abordado casualmente no início do artigo. Estamos falando sobre a interação dos componentes do modelo. Do ponto de vista do usuário, o problema não existe: existe um Controlador, que pode adicionar uma caixa de diálogo, um painel de negociação. Existem parâmetros de entrada como parte do Controlador. Mas como Modelo e a Visualização devem interagir? Em nosso Expert Advisor, a resposta a esta pergunta em particular é muito simples: nada. Eles não interagem diretamente, mas apenas por meio do Controlador, no manipulador OnTick. Além disso, a Visualização se comunica com o Controlador de maneira semelhante - chamando métodos de um objeto do tipo CInit "diretamente". Nesse caso, a interação dos componentes é estabelecida por meio de seus objetos globais, que se veem mutuamente. Isso é permitido e justificado pela simplicidade do próprio EA e nosso desejo de não sobrecarregar o código.
No entanto, a Visualização tem até onze chamadas para o Controlador, apesar da simplicidade do código. E no caso de um desenvolvimento posterior, o número dessas ligações cruzadas aumentará muitas vezes, anulando todos os benefícios de usar o padrão MVC. A solução para este problema recusar o acesso a objetos globais, permitir referências a componentes e métodos para acessá-los. Um exemplo desse tipo de interação é o MFC e seus componentes Document e View.
Repare que, do ponto de vista do modelo, as formas de interação entre os componentes deste não são regulamentadas. Por isso, não nos aprofundaremos neste tema e nos limitaremos apenas a revelar a própria existência do problema e formularemos duas formas de resolvê-lo.
Fim do artigo
Concluindo, vamos falar sobre como poderíamos desenvolver a estrutura de um indicador ou um Expert Advisor, nos quais inicialmente foi aplicar o padrão MVC. Suponhamos que existam mais dois Modelos e mais uma Visualização. O Controlador se torna muito mais complexo. Qual caminho seguir para permanecer dentro da estrutura MVC? Usando um design com módulos separados! Tudo é muito simples. Existem três componentes. Cada um deles tem uma maneira de ser acessado. Cada um dos componentes consiste em módulos separados. Este método foi falado aqui. Nesse mesmo artigo examinamos maneiras de interagir no nível do módulo e gerenciar.
Programas utilizados no artigo:
# | Nome | Tipo | Descrição |
---|---|---|---|
1 | MVC_primitive_1.zip | Arquivo | Primeira e pior variante do indicador. |
2 | MVC_primitive_2.zip | Arquivo | Segunda variante do indicador com divisão em componentes. |
3 | MVC_primitive_3.zip | Arquivo | Terceira versão do indicador com objetos. |
4 | EA_primitive.zip | Arquivo | Pseudo-EA |
5 | MVC_EA_primitive.zip | Arquivo | Pseudo-EA com base nas regras MVC. |
6 | EA_Real.zip | Arquivo | EA com base nas regras MVC. |
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/9168
![Outras classes na biblioteca DoEasy (Parte 68): classe de objeto-gráfico e classes de objetos-indicadores na janela do gráfico](https://c.mql5.com/2/42/MQL5-avatar-doeasy-library__6.png)
![Outras classes na biblioteca DoEasy (Parte 67): classe de objeto-gráfico](https://c.mql5.com/2/42/MQL5-avatar-doeasy-library__5.png)
![Redes neurais de maneira fácil (Parte 13): normalização em lote](https://c.mql5.com/2/48/Neural_networks_made_easy_013.png)
![Força bruta para encontrar padrões (Parte IV): funcionalidade mínima](https://c.mql5.com/2/41/1560775468.png)
![MQL5 - Linguagem para estratégias de negociação inseridas no terminal do cliente MetaTrader 5](https://c.mql5.com/i/registerlandings/logo-2.png)
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso