Trabalhando com séries temporais na biblioteca DoEasy (Parte 39): indicadores com base na biblioteca - preparação de dados e eventos das séries temporais
Sumário
- Ideia
- Modificando classes para trabalhar com indicadores, criando eventos de séries temporais
- Testando o funcionamento de séries temporais e seus eventos em indicadores
- O que vem agora?
Ideia
Tudo o que fizemos até hoje esteve relacionado a EA e scripts, não tendo a ver nada com indicadores. No entanto, as séries temporais podem ser usadas ativamente como fonte de dados para vários cálculos em indicadores. E, na minha opinião, é hora de consolidar a amizade entre a biblioteca e os indicadores.
Ao contrário dos EAs, os indicadores têm uma arquitetura completamente diferente, e cada um deles é executado num único fluxo de um símbolo. Isso significa que, se iniciarmos indicadores diferentes em vários gráficos do mesmo símbolo, todos eles serão executados no mesmo fluxo do símbolo ao qual todos esses gráficos pertencem.
Assim, se um dos indicadores tiver uma arquitetura mal concebida, congelará todo o fluxo do símbolo. Nesse caso, todos os outros indicadores feitos corretamente, mas trabalhando no mesmo fluxo que o indicador "pesado", serão forçados a travar, à espera de o fluxo "descongelar".
Para evitar atrasos ao solicitar dados históricos quando trabalhamos com indicadores, o terminal fornece o retorno sequencial dos dados solicitados, isto é, funções que ativam o carregamento de dados históricos retornam imediatamente o resultado da função.
Ao solicitar dados de qualquer série temporal de qualquer símbolo com funções Copy, o indicador e o EA terão um comportamento diferente quando o terminal retornar dados históricos:
Ao solicitar dados do indicador, se as séries temporais solicitadas ainda não estiverem construídas ou precisarem ser carregadas do servidor, a função retornará imediatamente -1, sendo que será iniciado o próprio processo de carregamento/construção.
Ao solicitar dados desde um EA ou script, o carregamento será inicializando a partir do servidor, se o terminal não tiver esses dados localmente, começará a construção das séries temporais necessárias, se os dados puderem ser construídos a partir do histórico local, mas ainda não estiverem prontos. A função retornará a quantidade de dados que estará pronta quando o tempo limite expirar, sendo que o carregamento do histórico continuará e, se houver uma próxima solicitação semelhante, a função retornará mais dados.
Assim, vemos que, ao solicitar dados a partir de um EA, o próprio terminal começará a carregar dados (se ainda não houver dados solicitados localmente ou ainda não forem suficientes) e, após algum tempo (tempo limite), a função retornará a quantidade de histórico existente para no momento em que é concluído o carregamento, ou seja, o terminal tentará fornecer imediatamente o histórico solicitado e, se localmente não for suficiente, tentará carregá-lo na quantidade certa.
Nosso programa, nesse momento, estará aguardando o carregamento de dados.
Nos indicadores é inaceitável aguardar, portanto, o terminal nos fornece o que há (ou informa que não há nada) e, se não houver histórico localmente ou não houver dados suficientes na primeira solicitação, será iniciado seu carregamento. Além disso, não há expectativa de carregar dados ausentes antes do tempo limite.
Nosso próprio programa deve sair de sua parte calculada antes do próximo tick nesta situação. No próximo início do manipulador OnCalculate() do indicadores num novo tique, os dados já podem estar parcial ou totalmente carregados e disponíveis para cálculos. Aqui precisamos decidir quantos dados serão suficientes para o algoritmo do programa funcionar sem problemas.
E mais: o indicador não deve tentar carregar seus próprios dados, os dados com base no símbolo e o período em que está em execução. Caso contrário, essa solicitação pode causar um erro. O subsistema do terminal está envolvido no carregamento desses dados para indicadores e fornece todos os dados sobre sua quantidade e estado nas variáveis rates_total e prev_calculated do manipulador OnCalculate().
Com base nesses requisitos mínimos, precisamos corrigir algumas classes para trabalhar com séries temporais e realizar o carregamento inicial correto dos dados necessários para os cálculos em nossos indicadores.
Hoje, corrigiremos as classes que já foram criadas, realizaremos o carregamento inicial correto de dados de todas as séries temporais usadas em nossos programas e enviaremos ao gráfico do programa de controle eventos de todas as séries temporais usadas durante a atualização em tempo real.
Modificando classes para trabalhar com indicadores, criando eventos de séries temporais
Primeiro, ao arquivo Datas.mqh adicionamos novas mensagens da biblioteca, isto é, os índices das mensagens:
MSG_LIB_SYS_FAILED_PREPARING_SYMBOLS_ARRAY, // Failed to prepare array of used symbols. Error MSG_LIB_SYS_FAILED_GET_SYMBOLS_ARRAY, // Failed to get array of used symbols. MSG_LIB_SYS_ERROR_EMPTY_PERIODS_STRING, // Error. The string of predefined periods is empty and is to be used
...
//--- CBar MSG_LIB_TEXT_BAR_FAILED_GET_BAR_DATA, // Failed to receive bar data MSG_LIB_TEXT_BAR_FAILED_DT_STRUCT_WRITE, // Failed to write time to time structure MSG_LIB_TEXT_BAR_FAILED_GET_SERIES_DATA, // Failed to receive timeseries data
...
MSG_LIB_TEXT_TS_TEXT_SYMBOL_TERMINAL_FIRSTDATE, // The very first date in history by a symbol in the client terminal MSG_LIB_TEXT_TS_TEXT_CREATED_OK, // successfully created MSG_LIB_TEXT_TS_TEXT_NOT_CREATED, // not created MSG_LIB_TEXT_TS_TEXT_IS_SYNC, // synchronized MSG_LIB_TEXT_TS_TEXT_ATTEMPT, // Attempt: MSG_LIB_TEXT_TS_TEXT_WAIT_FOR_SYNC, // Waiting for data synchronization ... }; //+------------------------------------------------------------------+
bem como os textos das mensagens correspondentes aos índices adicionados recentemente:
{"Не удалось подготовить массив используемых символов. Ошибка ","Failed to create an array of used symbols. Error "}, {"Не удалось получить массив используемых символов","Failed to get array of used symbols"}, {"Ошибка. Строка предопределённых периодов пустая, будет использоваться ","Error. String of predefined periods is empty, the Period will be used: "},
...
{"Не удалось получить данные бара","Failed to get bar data"}, {"Не удалось записать время в структуру времени","Failed to write time to datetime structure"}, {"Не удалось получить данные таймсерии","Failed to get timeseries data"},
...
{"Самая первая дата в истории по символу в клиентском терминале","Very first date in history of symbol in client terminal"}, {"создана успешно","created successfully"}, {"не создана","not created"}, {"синхронизирована","synchronized"}, {"Попытка: ","Attempt: "}, {"Ожидание синхронизации данных ...","Waiting for data synchronization ..."}, }; //+---------------------------------------------------------------------+
No construtor da classe de objeto base de todos os objetos da biblioteca CBaseObj, no arquivo \MQL5\Include\DoEasy\Objects\BaseObj.mqh é alterada a inicialização da variável m_available — imediatamente após a criação, todos os objetos-herdeiros da classe base CBaseObj terão o estado "usado" (true) que é uma propriedade indicando que podemos trabalhar com eles no programa. Anteriormente, durante a inicialização o valor era definido como "não usado" false:
//--- Constructor CBaseObj() : m_program((ENUM_PROGRAM_TYPE)::MQLInfoInteger(MQL_PROGRAM_TYPE)), m_global_error(ERR_SUCCESS), m_log_level(LOG_LEVEL_ERROR_MSG), m_chart_id_main(::ChartID()), m_chart_id(::ChartID()), m_folder_name(DIRECTORY), m_sound_name(""), m_name(__FUNCTION__), m_type(0), m_use_sound(false), m_available(true), m_first_start(true) {} }; //+------------------------------------------------------------------+
Também foi alterado o nome do método que define o sinalizador que indica que no objeto foi registrado um evento, na classe do objeto base estendido de todos os objetos da biblioteca CBaseObjExt no arquivo \MQL5\Include\DoEasy\Objects\BaseObj.mqh:
//--- Set/return the occurred event flag to the object data void SetEventFlag(const bool flag) { this.m_is_event=flag; }
Anteriormente, o método era chamado SetEvent(), e isso gerava alguma confusão ao desenvolver novos objetos, pois SetEvent pode significar criar, instalar, enviar etc. de algum evento e não definir o sinalizador que indica presença de evento.
Assim, também foram feitas alterações nos arquivos de classe nos quais foi usado esse método, substituímos a chamada para o método SetEvent() pela chamada para SetEventFlag(), que está explicada nos arquivos anexados ao artigo.
Como nos indicadores são proibidas as funções de negociação, faremos alterações nas classes dos objetos de negociação.
Na classe do objeto de negociação multiplataforma no arquivo \MQL5\Include\DoEasy\Objects\Trade\TradeObj.mqh no início de todos os métodos de negociação, inserimos uma verificação do tipo de programa e, se for um indicador ou um serviço, abandonaremos o método retornando true:
//+------------------------------------------------------------------+ //| Open a position | //+------------------------------------------------------------------+ bool CTradeObj::OpenPosition(const ENUM_POSITION_TYPE type, const double volume, const double sl=0, const double tp=0, const ulong magic=ULONG_MAX, const string comment=NULL, const ulong deviation=ULONG_MAX, const ENUM_ORDER_TYPE_FILLING type_filling=WRONG_VALUE) { if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE) return true; ::ResetLastError();
...
//+------------------------------------------------------------------+ //| Close a position | //+------------------------------------------------------------------+ bool CTradeObj::ClosePosition(const ulong ticket, const string comment=NULL, const ulong deviation=ULONG_MAX) { if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE) return true; ::ResetLastError();
...
//+------------------------------------------------------------------+ //| Close a position partially | //+------------------------------------------------------------------+ bool CTradeObj::ClosePositionPartially(const ulong ticket, const double volume, const string comment=NULL, const ulong deviation=ULONG_MAX) { if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE) return true; ::ResetLastError();
...
//+------------------------------------------------------------------+ //| Close a position by an opposite one | //+------------------------------------------------------------------+ bool CTradeObj::ClosePositionBy(const ulong ticket,const ulong ticket_by) { if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE) return true; ::ResetLastError();
...
//+------------------------------------------------------------------+ //| Modify a position | //+------------------------------------------------------------------+ bool CTradeObj::ModifyPosition(const ulong ticket,const double sl=WRONG_VALUE,const double tp=WRONG_VALUE) { if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE) return true; ::ResetLastError();
...
//+------------------------------------------------------------------+ //| Set an order | //+------------------------------------------------------------------+ bool CTradeObj::SetOrder(const ENUM_ORDER_TYPE type, const double volume, const double price, const double sl=0, const double tp=0, const double price_stoplimit=0, const ulong magic=ULONG_MAX, const string comment=NULL, const datetime expiration=0, const ENUM_ORDER_TYPE_TIME type_time=WRONG_VALUE, const ENUM_ORDER_TYPE_FILLING type_filling=WRONG_VALUE) { if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE) return true; ::ResetLastError();
...
//+------------------------------------------------------------------+ //| Remove an order | //+------------------------------------------------------------------+ bool CTradeObj::DeleteOrder(const ulong ticket) { if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE) return true; ::ResetLastError();
...
//+------------------------------------------------------------------+ //| Modify an order | //+------------------------------------------------------------------+ bool CTradeObj::ModifyOrder(const ulong ticket, const double price=WRONG_VALUE, const double sl=WRONG_VALUE, const double tp=WRONG_VALUE, const double price_stoplimit=WRONG_VALUE, const datetime expiration=WRONG_VALUE, const ENUM_ORDER_TYPE_TIME type_time=WRONG_VALUE, const ENUM_ORDER_TYPE_FILLING type_filling=WRONG_VALUE) { if(this.m_program==PROGRAM_INDICATOR || this.m_program==PROGRAM_SERVICE) return true; ::ResetLastError();
Exatamente as mesmas alterações foram feitas em todos os métodos de negociação com o mesmo nome da classe de negociação principal da biblioteca
no arquivo \MQL5\Include\DoEasy\Trading.mqh.
Essa saída dos métodos de negociação, em primeiro lugar, não permitirá chamar funções de negociação em programas onde são proibidas e, em segundo lugar, retornará a execução bem-sucedida do método, o que impedirá de processar erros da biblioteca.
Agora consideraremos as alterações que afetam diretamente as classes dos objetos das séries temporais.
Na classe do objeto-barra, foram ligeiramente alterados os textos derivados dos construtores da classe no caso de erro na obtenção de dados históricos ao criar o objeto-barra. Ao texto exibido foram adicionados o número do construtor, símbolo e período gráfico da série temporal para a qual o objeto-barra está sendo criado.
No construtor da primeira forma a verificação de erros de obtenção de dados e o registro de tempo numa estrutura de tempo foram colocados em blocos separados:
//+------------------------------------------------------------------+ //| Constructor 1 | //+------------------------------------------------------------------+ CBar::CBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index) { this.m_type=COLLECTION_SERIES_ID; MqlRates rates_array[1]; this.SetSymbolPeriod(symbol,timeframe,index); ::ResetLastError(); //--- If ailed to get the requested data by index and write bar data to the MqlRates array, //--- display an error message, create and fill the structure with zeros, and write it to the rates_array array if(::CopyRates(symbol,timeframe,index,1,rates_array)<1) { int err_code=::GetLastError(); ::Print ( DFUN,"(1) ",symbol," ",TimeframeDescription(timeframe)," ", CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_GET_BAR_DATA),". ", CMessage::Text(MSG_LIB_SYS_ERROR)," ",CMessage::Text(err_code)," ", CMessage::Retcode(err_code) ); MqlRates err={0}; rates_array[0]=err; } ::ResetLastError(); //--- If failed to set time to the time structure, display the error message if(!::TimeToStruct(rates_array[0].time,this.m_dt_struct)) { int err_code=::GetLastError(); ::Print ( DFUN,"(1) ",symbol," ",TimeframeDescription(timeframe)," ", CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_DT_STRUCT_WRITE),". ", CMessage::Text(MSG_LIB_SYS_ERROR)," ",CMessage::Text(err_code)," ", CMessage::Retcode(err_code) ); } //--- Set the bar properties this.SetProperties(rates_array[0]); } //+------------------------------------------------------------------+ //| Constructor 2 | //+------------------------------------------------------------------+ CBar::CBar(const string symbol,const ENUM_TIMEFRAMES timeframe,const int index,const MqlRates &rates) { this.m_type=COLLECTION_SERIES_ID; this.SetSymbolPeriod(symbol,timeframe,index); ::ResetLastError(); //--- If failed to set time to the time structure, display the error message, //--- create and fill the structure with zeros, set the bar properties from this structure and exit if(!::TimeToStruct(rates.time,this.m_dt_struct)) { int err_code=::GetLastError(); ::Print ( DFUN,"(2) ",symbol," ",TimeframeDescription(timeframe)," ", CMessage::Text(MSG_LIB_TEXT_BAR_FAILED_DT_STRUCT_WRITE),". ", CMessage::Text(MSG_LIB_SYS_ERROR)," ",CMessage::Text(err_code)," ", CMessage::Retcode(err_code) ); MqlRates err={0}; this.SetProperties(err); return; } //--- Set the bar properties this.SetProperties(rates); } //+------------------------------------------------------------------+
Essas ações nos fornecem mais dados ao surgir o erro de criar um objeto-barra.
Como, para solicitar dados sobre o número de barras e seus valores no símbolo-período atual, precisamos usar as matrizes de séries temporais fornecidas pelo manipulador OnCalculate(), devemos, de alguma forma, passar essas matrizes e valores para as classes da biblioteca.
Para fazer isso, criamos uma estrutura no arquivo \MQL5\Include\DoEasy\Defines.mqh, na qual serão armazenadas as variáveis, através das quais transferiremos todos os dados necessários, calculados para a série temporal atual, na série temporal da biblioteca:
//+------------------------------------------------------------------+ //| Structures | //+------------------------------------------------------------------+ struct SDataCalculate { int rates_total; // size of input time series int prev_calculated; // number of handled bars at the previous call int begin; // where significant data start double price; // current array value for calculation MqlRates rates; // Price structure } rates_data; //+------------------------------------------------------------------+ //| Enumerations | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Search and sorting data | //+------------------------------------------------------------------+
Como você pode ver, a estrutura contém todos os campos necessários para transferir dados para a biblioteca durante qualquer implementação do manipulador OnCalculate() do indicador.
Para a primeira forma de manipulador
int OnCalculate( const int rates_total, // price[] array size const int prev_calculated, // number of handled bars at the previous call const int begin, // index number in the price[] array meaningful data starts from const double& price[] // array of values for calculation );
são usadas as variáveis de estrutura rates_total, prev_calculated, begin e price.
Para a segunda forma do manipulador
int OnCalculate( const int rates_total, // size of input time series const int prev_calculated, // number of handled bars at the previous call const datetime& time{}, // Time array const double& open[], // Open array const double& high[], // High array const double& low[], // Low array const double& close[], // Close array const long& tick_volume[], // Tick Volume array const long& volume[], // Real Volume array const int& spread[] // Spread array );
são usadas as variáveis das estruturas rates_total, prev_calculated e a estrutura MqlRates rates para armazenar os valores das matrizes.
Essa implementação da estrutura é adequada para transferir apenas um valor de barra para a biblioteca.
Na classe CSeries, no arquivo \MQL5\Include\DoEasy\Objects\Series\Series.mqh, aos métodos para definir o símbolo e período gráfico adicionamos o sinalizador de definição de datas do servidor:
//--- Set (1) symbol, (2) timeframe, (3) symbol and timeframe, (4) amount of applied timeseries data void SetSymbol(const string symbol,const bool set_server_date=false); void SetTimeframe(const ENUM_TIMEFRAMES timeframe,const bool set_server_date=false);
Por padrão, o sinalizador está desmarcado, o que não permite definir as datas do servidor ao chamar o método, pois para chamar o método de configuração da datas do servidor primeiro é verificado o estado do sinalizador em questão:
//+------------------------------------------------------------------+ //| Set a symbol | //+------------------------------------------------------------------+ void CSeries::SetSymbol(const string symbol,const bool set_server_date=false) { if(this.m_symbol==symbol) return; this.m_symbol=(symbol==NULL || symbol=="" ? ::Symbol() : symbol); this.m_new_bar_obj.SetSymbol(this.m_symbol); if(set_server_date) this.SetServerDate(); } //+------------------------------------------------------------------+ //| Set a timeframe | //+------------------------------------------------------------------+ void CSeries::SetTimeframe(const ENUM_TIMEFRAMES timeframe,const bool set_server_date=false) { if(this.m_timeframe==timeframe) return; this.m_timeframe=(timeframe==PERIOD_CURRENT ? (ENUM_TIMEFRAMES)::Period() : timeframe); this.m_new_bar_obj.SetPeriod(this.m_timeframe); this.m_period_description=TimeframeDescription(this.m_timeframe); if(set_server_date) this.SetServerDate(); } //+------------------------------------------------------------------+
Isso é feito assim para não definir datas do servidor várias vezes ao chamar o método que permite definir simultaneamente o símbolo e o período gráfico:
//+------------------------------------------------------------------+ //| Set a symbol and timeframe | //+------------------------------------------------------------------+ void CSeries::SetSymbolPeriod(const string symbol,const ENUM_TIMEFRAMES timeframe) { if(this.m_symbol==symbol && this.m_timeframe==timeframe) return; this.SetSymbol(symbol); this.SetTimeframe(timeframe,true); } //+------------------------------------------------------------------+
Aqui: primeiro é chamado o método de definição do símbolo (sinalizador desmarcado), em seguida, é chamado o método de configuração do período gráfico com o sinalizador definido para chamar o método de configuração de datas do servidor a partir do método de configuração do período gráfico.
Ao método de atualização de dados da série temporal agora é transferida uma nova estrutura de dados do manipulador OnCalculate() em vez de sua lista completa de matrizes:
//--- (1) Create and (2) update the timeseries list int Create(const uint required=0); void Refresh(SDataCalculate &data_calculate); //--- Create and send the "New bar" event to the control program chart void SendEvent(void);
Assim, na implementação do método Refresh() agora, em vez de usar as matrizes, são acessados os dados desta estrutura:
//+------------------------------------------------------------------+ //| Update timeseries list and data | //+------------------------------------------------------------------+ void CSeries::Refresh(SDataCalculate &data_calculate) { //--- If the timeseries is not used, exit if(!this.m_available) return; MqlRates rates[1]; //--- Set the flag of sorting the list of bars by index this.m_list_series.Sort(SORT_BY_BAR_INDEX); //--- If a new bar is present on a symbol and period, if(this.IsNewBarManual(data_calculate.rates.time)) { //--- create a new bar object and add it to the end of the list CBar *new_bar=new CBar(this.m_symbol,this.m_timeframe,0); if(new_bar==NULL) return; if(!this.m_list_series.InsertSort(new_bar)) { delete new_bar; return; } //--- Write the very first date by a period symbol at the moment and the new time of opening the last bar by a period symbol this.SetServerDate(); //--- if the timeseries exceeds the requested number of bars, remove the earliest bar if(this.m_list_series.Total()>(int)this.m_required) this.m_list_series.Delete(0); //--- save the new bar time as the previous one for the subsequent new bar check this.SaveNewBarTime(data_calculate.rates.time); } //--- Get the bar object from the list by the terminal timeseries index (zero bar) CBar *bar=this.GetBarBySeriesIndex(0); //--- if the work is performed in an indicator and the timeseries belongs to the current symbol and timeframe, //--- copy price parameters (passed to the method from the outside) to the bar price structure int copied=1; if(this.m_program==PROGRAM_INDICATOR && this.m_symbol==::Symbol() && this.m_timeframe==(ENUM_TIMEFRAMES)::Period()) { rates[0].time=data_calculate.rates.time; rates[0].open=data_calculate.rates.open; rates[0].high=data_calculate.rates.high; rates[0].low=data_calculate.rates.low; rates[0].close=data_calculate.rates.close; rates[0].tick_volume=data_calculate.rates.tick_volume; rates[0].real_volume=data_calculate.rates.real_volume; rates[0].spread=data_calculate.rates.spread; } //--- otherwise, get data to the bar price structure from the environment else copied=::CopyRates(this.m_symbol,this.m_timeframe,0,1,rates); //--- If the prices are obtained, set the new properties from the price structure for the bar object if(copied==1) bar.SetProperties(rates[0]); } //+------------------------------------------------------------------+
Para poder pesquisar na lista de objetos-séries temporais por seus períodos gráficos, agora está implementado o método virtual para comparar dois objetos-séries temporais:
//--- Comparison method to search for identical timeseries objects by timeframe virtual int Compare(const CObject *node,const int mode=0) const { const CSeries *compared_obj=node; return(this.Timeframe()>compared_obj.Timeframe() ? 1 : this.Timeframe()<compared_obj.Timeframe() ? -1 : 0); } //--- Constructors CSeries(void); CSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const uint required=0); }; //+------------------------------------------------------------------+
O método compara a propriedade "timeframe" de dois objetos-séries temporais (atual e passado para o método) e, se for igual, retornará zero.
Já examinamos muitas vezes a lógica de métodos semelhantes para pesquisar e classificar vários objetos-herdeiros do objeto base da biblioteca padrão CObject. O método é definido no objeto base da biblioteca padrão como virtual, por isso, sua implementação deve ser executada nos objetos-herdeiros e o método deve retornar zero se for igual ou 1/-1 se o valor da propriedade comparada do objeto atual for maior/menor que o valor dessa propriedade do objeto comparado.
Como o primeiro uso das funções que retornam dados históricos ativa o carregamento de dados se eles estiverem ausentes ou não forem suficientes localmente, ao início do método de configuração da quantidade necessária de dados necessários (ele é chamado antes de criar o objeto-série temporal) adicionaremos a consulta de dados históricos necessários (basta solicitarmos a data da barra atual). Isso iniciará o carregamento de dados necessários (se não houver localmente):
//+------------------------------------------------------------------+ //| Set the number of required data | //+------------------------------------------------------------------+ bool CSeries::SetRequiredUsedData(const uint required,const uint rates_total) { this.m_required=(required<1 ? SERIES_DEFAULT_BARS_COUNT : required); //--- Launch downloading historical data if(this.m_program!=PROGRAM_INDICATOR || (this.m_program==PROGRAM_INDICATOR && (this.m_symbol!=::Symbol() || this.m_timeframe!=::Period()))) { datetime array[1]; ::CopyTime(this.m_symbol,this.m_timeframe,0,1,array); } //--- Set the number of available timeseries bars
Quando criamos o objeto que armazenava listas de todas as séries temporais de um símbolo (classe CTimeSeries), fizemos com que este objeto sempre tivesse uma lista na qual fosse gravado o conjunto completo de todos os possíveis períodos gráficos no terminal. Além disso, as listas-séries temporais eram imediatamente adicionadas a essa lista, mas não eram criadas. Eles eram geradas conforme necessário. Quanto ao acesso aos ponteiros para uma lista-série temporal necessária, nós o criamos segundo um índice desconhecido que correspondia à posição do índice do período gráfico da lista na enumeração ENUM_TIMEFRAMES com deslocamento de 1 (descrito no artigo).
Isso foi feito para acelerar o acesso ao ponteiro para o objeto-série temporal desejado na lista. Mas, como se viu, junto com o acesso instantâneo ao índice, tivemos problemas ao trabalhar no testador, pois o testador visual criava gráficos de todos os períodos gráficos, independentemente de se fossem realmente usados no programa e de suas listas-séries temporais fossem na verdade criadas.
Também tivemos mais um problema ao alterar o período de agendamento durante a operação do programa - as listas criadas anteriormente não foram recriadas e o programa continuou a rastrear eventos de objetos já inexistentes, substituindo-os por outros.
Bem, para evitar a futura acumulação de erros e o trabalho de procurar causas, decidi que seria melhor armazenar no objeto da classe CTimeSeries (que armazena as listas-séries temporais de todos os períodos gráficos usados) os ponteiros apenas para as listas-séries temporais realmente criadas e usadas, em outras palavras, à lista adicionaremos os ponteiros para cada série temporal de cada período gráfico apenas se no programa estiver especificado que ela deve ser usada e que tal objeto-série temporal está criado fisicamente.
Abrimos o arquivo \MQL5\Include\DoEasy\Objects\Series\TimeSeries.mqh e fazemos as alterações necessárias.
Agora a classe de todas as séries temporais de um símbolo será herdada da classe de objeto base estendida de todos os objetos de biblioteca.
Isso é feito para poder usar a funcionalidade de evento da classe CBaseObjExt:
//+------------------------------------------------------------------+ //| Symbol timeseries class | //+------------------------------------------------------------------+ class CTimeSeries : public CBaseObjExt {
O método que retorna o índice de série temporal na lista pelo nome do período gráfico agora apenas é declarado na seção privada da classe:
//+------------------------------------------------------------------+ class CTimeSeries : public CBaseObjExt { private: string m_symbol; // Timeseries symbol CNewTickObj m_new_tick; // "New tick" object CArrayObj m_list_series; // List of timeseries by timeframes datetime m_server_firstdate; // The very first date in history by a server symbol datetime m_terminal_firstdate; // The very first date in history by a symbol in the client terminal //--- Return (1) the timeframe index in the list and (2) the timeframe by the list index int IndexTimeframe(const ENUM_TIMEFRAMES timeframe); ENUM_TIMEFRAMES TimeframeByIndex(const uchar index) const { return TimeframeByEnumIndex(uchar(index+1)); } //--- Set the very first date in history by symbol on the server and in the client terminal void SetTerminalServerDate(void) { this.m_server_firstdate=(datetime)::SeriesInfoInteger(this.m_symbol,::Period(),SERIES_SERVER_FIRSTDATE); this.m_terminal_firstdate=(datetime)::SeriesInfoInteger(this.m_symbol,::Period(),SERIES_TERMINAL_FIRSTDATE); } public:
A implementação deste método agora é realizada fora do corpo da classe:
//+------------------------------------------------------------------+ //| Return the timeframe index in the list | //+------------------------------------------------------------------+ int CTimeSeries::IndexTimeframe(const ENUM_TIMEFRAMES timeframe) { const CSeries *obj=new CSeries(this.m_symbol,timeframe); if(obj==NULL) return WRONG_VALUE; this.m_list_series.Sort(); int index=this.m_list_series.Search(obj); delete obj; return index; } //+------------------------------------------------------------------+
O período é passado para o método cujo ponteiro é necessário retornar para a série temporal.
Em seguida, criamos um objeto-série temporal temporário com o período gráfico desejado.
Para a lista de objetos-séries temporais definimos o sinalizador de lista classificada e
obtemos o índice do objeto-série temporal na lista para a qual o período é igual ao período do objeto temporário.
Se tal objeto existir na lista, será obtido seu índice, caso contrário, WRONG_VALUE (-1).
Removemos o objeto temporário e retornamos o índice obtido.
Em vez dos métodos Create() e CreateAll() declaramos os métodos para adicionar a série temporal especificada à lista e o método para criar o objeto-série temporal especificado,
enquanto os métodos de atualização de listas-séries temporais agora obtêm a estrutura dos valores dos parâmetros e matrizes OnCalculate() em vez da lista completa de matrizes:
//--- (1) Add the specified timeseries list to the list and create (2) the specified timeseries list bool AddSeries(const ENUM_TIMEFRAMES timeframe,const uint required=0); bool CreateSeries(const ENUM_TIMEFRAMES timeframe,const uint required=0); //--- Update (1) the specified timeseries list and (2) all timeseries lists void Refresh(const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate); void RefreshAll(SDataCalculate &data_calculate); //--- Compare CTimeSeries objects (by symbol) virtual int Compare(const CObject *node,const int mode=0) const; //--- Display (1) description and (2) short symbol timeseries description in the journal void Print(const bool created=true); void PrintShort(const bool created=true); //--- Constructors CTimeSeries(void){;} CTimeSeries(const string symbol); }; //+------------------------------------------------------------------+
Do construtor de classe excluímos o ciclo de criação da listas de séries temporais:
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CTimeSeries::CTimeSeries(const string symbol) : m_symbol(symbol) { this.m_list_series.Clear(); this.m_list_series.Sort(); for(int i=0;i<21;i++) { ENUM_TIMEFRAMES timeframe=this.TimeframeByIndex((uchar)i); CSeries *series_obj=new CSeries(this.m_symbol,timeframe); this.m_list_series.Add(series_obj); } this.SetTerminalServerDate(); this.m_new_tick.SetSymbol(this.m_symbol); this.m_new_tick.Refresh(); } //+------------------------------------------------------------------+
Agora, as séries temporais necessárias serão criadas após a criação de uma matriz de séries temporais usadas no manipulador OnInit() do programa. Quaisquer alterações no número de períodos gráficos usados no programa farão com que o EA seja reinicializado ou o indicador seja recriado, causando uma recriação completa da lista de objetos-séries temporais usadas e, mais tarde, seu recálculo.
Nos métodos para definir a profundidade do histórico de todas as séries temporais usadas SetRequiredAllUsedData() e para retornar o sinalizador de sincronização de todas as séries temporais usadas SyncAllData(), substituímos o ciclo percorrendo o número total de períodos gráficos possíveis
//+------------------------------------------------------------------+ //| Set the history depth of all applied symbol timeseries | //+------------------------------------------------------------------+ bool CTimeSeries::SetRequiredAllUsedData(const uint required=0,const int rates_total=0) { if(this.m_symbol==NULL) { ::Print(DFUN,CMessage::Text(MSG_LIB_TEXT_TS_TEXT_FIRST_SET_SYMBOL)); return false; } bool res=true; for(int i=0;i<21;i++) { CSeries *series_obj=this.m_list_series.At(i); if(series_obj==NULL) continue; res &=series_obj.SetRequiredUsedData(required,rates_total); } return res; } //+------------------------------------------------------------------+
por um ciclo pelo número de objetos-séries temporais reais na lista:
int total=this.m_list_series.Total(); for(int i=0;i<total;i++)
Isso é compreensível, pois agora nossa lista consiste apenas em objetos-séries temporais realmente criados e realizamos ciclos de acordo com seu número real.
Implementação do método de adição do objeto-série temporaç especificado à lista:
//+------------------------------------------------------------------+ //| Add the specified timeseries list to the list | //+------------------------------------------------------------------+ bool CTimeSeries::AddSeries(const ENUM_TIMEFRAMES timeframe,const uint required=0) { bool res=false; CSeries *series=new CSeries(this.m_symbol,timeframe,required); if(series==NULL) return res; this.m_list_series.Sort(); if(this.m_list_series.Search(series)==WRONG_VALUE) res=this.m_list_series.Add(series); if(!res) delete series; series.SetAvailable(true); return res; } //+------------------------------------------------------------------+
Ao método é transferido o período gráfico da série temporal que é necessário adicionar à lista das séries temporais do símbolo.
Criamos o objeto-série temporal com o período gráfico cujo valor é transferido ao método.
Para o lista de séries temporais definimos o sinalizador indicando lista classificada e procuramos na lista um objeto-série temporal igual ao recém-criado.
Se ainda não houver tal objeto na lista (a pesquisa retornar -1), adicionaremos o objeto-série temporal criado à lista.
Caso contrário, excluímos o objeto criado, pois tal objeto-série temporal já está na lista.
Como estamos criando uma série temporal, é necessário definirmos um sinalizador indicando seu uso no programa e
retornarmos o resultado da adição de série temporal à lista.
Se for bem sucedido, será retornado o valor true, caso contrário, false.
Na biblioteca, para enviar eventos que ocorrem com seus diversos objetos, no objeto estendido de todos os objetos da biblioteca foi criada uma funcionalidade de evento. Examinamos os princípios e a lógica de trabalho com eventos da biblioteca no artigo 16 e continuamos a desenvolver esse tópico no artigo 17.
Em resumo, cada objeto herdado do objeto base da biblioteca CBaseObj (agora já de CBaseObjExt) possui uma lista na qual são registrados todos os eventos que podem ocorrer com o objeto durante uma execução do programa, num tick ou numa iteração do temporizador.
Ao identificar qualquer evento de um objeto, para ele é definido o sinalizador que indica evento ocorrido. Em seguida, nas classes-coleções, são exibidas as listas de objetos de coleções e, por sua vez, são verificados esses sinalizadores. Se for encontrado um objeto com o sinalizador de evento marcado, a classe-coleção desses objetos receberá uma lista de todos os eventos do objeto com o sinalizador de evento marcado e enviará todos os eventos dessa lista para o temporizado do programa de controle.
No próprio programa, foi criada uma funcionalidade para processar todos os eventos recebidos. Além disso, no testador, todos os eventos são processados por ticks, enquanto, fora do testador, no manipulador OnChartEvent().
Na classe do objeto de todas as séries temporais do mesmo símbolo CTimeSeries, que estamos considerando, é o local adequando para determinar os eventos de todas as suas listas-séries temporais, pois este é o método de atualização para as séries temporizadas especificadas Refresh() e o método de atualização para todas as séries temporais do símbolo RefreshAll().
Vejamos a implementação de métodos para atualizar listas-séries temporais:
//+------------------------------------------------------------------+ //| Update a specified timeseries list | //+------------------------------------------------------------------+ void CTimeSeries::Refresh(const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate) { //--- Reset the timeseries event flag and clear the list of all timeseries events this.m_is_event=false; this.m_list_events.Clear(); //--- Get the timeseries from the list by its timeframe CSeries *series_obj=this.m_list_series.At(this.IndexTimeframe(timeframe)); if(series_obj==NULL || series_obj.DataTotal()==0 || !series_obj.IsAvailable()) return; //--- Update the timeseries list series_obj.Refresh(data_calculate); //--- If the timeseries object features the New bar event if(series_obj.IsNewBar(data_calculate.rates.time)) { //--- send the "New bar" event to the control program chart series_obj.SendEvent(); //--- set the values of the first date in history on the server and in the terminal this.SetTerminalServerDate(); //--- add the "New bar" event to the list of timeseries events //--- in case of successful addition, set the event flag for the timeseries if(this.EventAdd(SERIES_EVENTS_NEW_BAR,series_obj.Time(0),series_obj.Timeframe(),series_obj.Symbol())) this.m_is_event=true; } } //+------------------------------------------------------------------+ //| Update all timeseries lists | //+------------------------------------------------------------------+ void CTimeSeries::RefreshAll(SDataCalculate &data_calculate) { //--- Reset the flags indicating the necessity to set the first date in history on the server and in the terminal //--- and the timeseries event flag, and clear the list of all timeseries events bool upd=false; this.m_is_event=false; this.m_list_events.Clear(); //--- In the loop by the list of all used timeseries, int total=this.m_list_series.Total(); for(int i=0;i<total;i++) { //--- get the next timeseries object by the loop index CSeries *series_obj=this.m_list_series.At(i); if(series_obj==NULL || !series_obj.IsAvailable() || series_obj.DataTotal()==0) continue; //--- update the timeseries list series_obj.Refresh(data_calculate); //--- If the timeseries object features the New bar event if(series_obj.IsNewBar(data_calculate.rates.time)) { //--- send the "New bar" event to the control program chart, series_obj.SendEvent(); //--- set the flag indicating the necessity to set the first date in history on the server and in the terminal upd=true; //--- add the "New bar" event to the list of timeseries events //--- in case of successful addition, set the event flag for the timeseries if(this.EventAdd(SERIES_EVENTS_NEW_BAR,series_obj.Time(0),series_obj.Timeframe(),series_obj.Symbol())) this.m_is_event=true; } } //--- if the flag indicating the necessity to set the first date in history on the server and in the terminal is enabled, //--- set the values of the first date in history on the server and in the terminal if(upd) this.SetTerminalServerDate(); } //+------------------------------------------------------------------+
Aqui, comentei cada linha do código do método, e espero que tenham ficado claras. De qualquer forma, na discussão do artigo podem ser feitas todas as perguntas que surgirem.
Assim concluímos a classe de objeto de todas as séries temporais de um mesmo símbolo CTimeSeries.
A próxima classe é uma classe-coleção de objetos de séries temporais de símbolos CTimeSeriesCollection que, da mesma forma, deve ser dotada de funcionalidade orientada a eventos, pois é responsável por receber listas de eventos de todos os objetos que armazenam todas as séries temporais de cada símbolo usado no programa.
Abrimos o arquivo \MQL5\Include\DoEasy\Collections\TimeSeriesCollection.mqh e o herdamos da classe base estendida de todos os objetos de biblioteca:
//+------------------------------------------------------------------+ //| Symbol timeseries collection | //+------------------------------------------------------------------+ class CTimeSeriesCollection : public CBaseObjExt {
Na seção pública da classe, declaramos um método para retornar o objeto de todas as séries temporais do símbolo especificado e outro para retornar um objeto-série temporal do símbolo e período especificados:
public: //--- Return (1) oneself and (2) the timeseries list CTimeSeriesCollection *GetObject(void) { return &this; } CArrayObj *GetList(void) { return &this.m_list; } //--- Return (1) the timeseries object of the specified symbol and (2) the timeseries object of the specified symbol/period CTimeSeries *GetTimeseries(const string symbol); CSeries *GetSeries(const string symbol,const ENUM_TIMEFRAMES timeframe);
Fora do corpo da classe, escrevemos imediatamente sua implementação.
Método que retorna o objeto de séries temporais do símbolo especificado:
//+------------------------------------------------------------------+ //| Return the timeseries object of the specified symbol | //+------------------------------------------------------------------+ CTimeSeries *CTimeSeriesCollection::GetTimeseries(const string symbol) { int index=this.IndexTimeSeries(symbol); if(index==WRONG_VALUE) return NULL; CTimeSeries *timeseries=this.m_list.At(index); return timeseries; } //+------------------------------------------------------------------+
Aqui, obtemos o índice do objeto de séries temporais pelo nome do símbolo através do método IndexTimeSeries(), que revisamos na parte 37, que descrevia a criação de uma biblioteca. De acordo com o índice recebido obtemos o objeto de séries temporais a partir da lista. Se um índice ou objeto da lista não for obtido com sucesso, será retornado o valor NULL, caso contrário, um ponteiro para o objeto solicitado na lista.
Método que retorna um objeto-série temporal do símbolo/período especificado:
//+------------------------------------------------------------------+ //| Return the timeseries object of the specified symbol/period | //+------------------------------------------------------------------+ CSeries *CTimeSeriesCollection::GetSeries(const string symbol,const ENUM_TIMEFRAMES timeframe) { CTimeSeries *timeseries=this.GetTimeseries(symbol); if(timeseries==NULL) return NULL; CSeries *series=timeseries.GetSeries(timeframe); return series; } //+-----------------------------------------------------------------------+
Aqui, obtemos o objeto de séries temporais usando o método GetTimeseries() discutido acima de acordo com o símbolo passado para o método.
Do objeto de séries temporais recebido, obtemos a lista-série temporal pelo período especificado e retornamos um ponteiro para o objeto-série temporal recebido.
Para retornar a série temporal necessária, o método GetSeries() do objeto de séries temporais usa o método IndexTimeframe() discutido acima. O método GetSeries() do objeto de séries temporais CTimeSeries fica assim:
CSeries *GetSeries(const ENUM_TIMEFRAMES timeframe) { return this.m_list_series.At(this.IndexTimeframe(timeframe)); }
Da seção pública da classe removemos os três métodos para criar séries temporais, deixando apenas um para criar as séries temporais especificadas do símbolo especificado:
//--- Create (1) the specified timeseries of the specified symbol, (2) the specified timeseries of all symbols, //--- (3) all timeseries of the specified symbol and (4) all timeseries of all symbols bool CreateSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const uint required=0); bool CreateSeries(const ENUM_TIMEFRAMES timeframe,const uint required=0); bool CreateSeries(const string symbol,const uint required=0); bool CreateSeries(const uint required=0); //--- Update (1) the specified timeseries of the specified symbol, (2) the specified timeseries of all symbols, //--- (3) all timeseries of the specified symbol and (4) all timeseries of all symbols and (5) all timeseries except for the current symbol
Até agora, três métodos remotos parecem redundantes e, em vez deles, declararemos três novos métodos: para recriar as séries temporais especificadas, para retornar uma série temporal vazia e para retornar as séries temporais incompletas:
//--- (1) Create and (2) re-create a specified timeseries of a specified symbol bool CreateSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0); bool ReCreateSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0); //--- Return (1) an empty, (2) partially filled timeseries CSeries *GetSeriesEmpty(void); CSeries *GetSeriesIncompleted(void);
Por que é necessário recriar uma série temporal? Ao inicializar a biblioteca e criar todas as séries temporais de todos os símbolos usados, passamos às funções que iniciam o carregamento de dados históricos. Mas, como dissemos mais de uma vez, se o programa for um indicador e tentar usar o símbolo e período gráfico em que estará sendo executado, poderemos obter um erro. Portanto, essas situações são ignoradas e, após concluir a inicialização e entrar no manipulador OnCalculate(), a primeira coisa que devemos fazer é examinar a série temporal criada, obter uma vazia (ignorada durante a inicialização) e recriá-la usando os dados da variável rates_total em OnCalculate().
Agora aos métodos de atualização de séries temporais não transferiremos dados das matrizes de séries temporais desde OnCalculate(), em vez disso, passaremos a estrutura desses dados, e declararemos o método para obter eventos desde o objeto de séries temporais e para adicioná-los na lista de eventos todos os objetos da coleção de séries temporais dos símbolos:
//--- Update (1) the specified timeseries of the specified symbol, (2) all timeseries of all symbols void Refresh(const string symbol,const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate); void Refresh(SDataCalculate &data_calculate); //--- Get events from the timeseries object and add them to the list bool SetEvents(CTimeSeries *timeseries); //--- Display (1) the complete and (2) short collection description in the journal void Print(const bool created=true); void PrintShort(const bool created=true); //--- Constructor CTimeSeriesCollection(); }; //+------------------------------------------------------------------+
Implementação de métodos que retornam uma série temporal vazia e incompleta:
//+------------------------------------------------------------------+ //|Return the empty (created but not filled with data) timeseries | //+------------------------------------------------------------------+ CSeries *CTimeSeriesCollection::GetSeriesEmpty(void) { //--- In the loop by the timeseries object list int total_timeseries=this.m_list.Total(); for(int i=0;i<total_timeseries;i++) { //--- get the next object of all symbol timeseries by the loop index CTimeSeries *timeseries=this.m_list.At(i); if(timeseries==NULL || !timeseries.IsAvailable()) continue; //--- get the list of timeseries objects from the object of all symbol timeseries CArrayObj *list_series=timeseries.GetListSeries(); if(list_series==NULL) continue; //--- in the loop by the symbol timeseries list int total_series=list_series.Total(); for(int j=0;j<total_series;j++) { //--- get the next timeseries CSeries *series=list_series.At(j); if(series==NULL || !series.IsAvailable()) continue; //--- if the timeseries has no bar objects, //--- return the pointer to the timeseries if(series.DataTotal()==0) return series; } } return NULL; } //+------------------------------------------------------------------+ //| Return partially filled timeseries | //+------------------------------------------------------------------+ CSeries *CTimeSeriesCollection::GetSeriesIncompleted(void) { //--- In the loop by the timeseries object list int total_timeseries=this.m_list.Total(); for(int i=0;i<total_timeseries;i++) { //--- get the next object of all symbol timeseries by the loop index CTimeSeries *timeseries=this.m_list.At(i); if(timeseries==NULL || !timeseries.IsAvailable()) continue; //--- get the list of timeseries objects from the object of all symbol timeseries CArrayObj *list_series=timeseries.GetListSeries(); if(list_series==NULL) continue; //--- in the loop by the symbol timeseries list int total_series=list_series.Total(); for(int j=0;j<total_series;j++) { //--- get the next timeseries CSeries *series=list_series.At(j); if(series==NULL || !series.IsAvailable()) continue; //--- if the timeseries has bar objects, //--- but their number is not equal to the requested and available one for the symbol, //--- return the pointer to the timeseries if(series.DataTotal()>0 && series.AvailableUsedData()!=series.DataTotal()) return series; } } return NULL; } //+------------------------------------------------------------------+
Cada linha dos métodos é comentada, os métodos são exatamente os mesmos, exceto aquele que verifica se a série está vazia e incompleta.
Métodos que retornam as primeiras séries temporais que atendem às condições de pesquisa. Isso foi feito de propósito para em cada tick receber todas as séries temporais vazias ou incompletas possíveis (entrada em OnCalculate), o que corresponde às recomendações da MetaQuotes para o processamento correto da falta de dados nos indicadores (sair do manipulador e verificar os dados no próxima tick).
Implementação do método para criar as séries temporais especificadas do símbolo especificado:
//+------------------------------------------------------------------+ //| Create the specified timeseries of the specified symbol | //+------------------------------------------------------------------+ bool CTimeSeriesCollection::CreateSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0) { CTimeSeries *timeseries=this.GetTimeseries(symbol); if(timeseries==NULL) return false; if(!timeseries.AddSeries(timeframe,required)) return false; if(!timeseries.SyncData(timeframe,required,rates_total)) return false; return timeseries.CreateSeries(timeframe,required); } //+------------------------------------------------------------------+
O método adiciona ao objeto das séries temporais do símbolo uma nova série temporal com o período gráfico especificado.
Ao método são transferidos o símbolo e o período da série temporal necessária.
Obtemos o objeto de séries temporais e adicionamos a ele uma nova série temporal do período gráfico especificado.
Solicitamos dados do símbolo/período e definimos a quantidade de dados necessária na série temporal.
Se todas as etapas anteriores forem bem-sucedidas, retornaremos o resultado da criação de uma nova série temporal e da adição de dados a ela.
Todos esses métodos foram considerados por nós em artigos anteriores, no entanto, ao contrário do lógica descrita no artigo 37, aqui apenas criamos uma lógica para criar a série temporal necessária do símbolo/período em questão.
Implementação do método de reconstrução das séries temporais especificadas do símbolo especificado:
//+------------------------------------------------------------------+ //| Re-create a specified timeseries of a specified symbol | //+------------------------------------------------------------------+ bool CTimeSeriesCollection::ReCreateSeries(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0) { CTimeSeries *timeseries=this.GetTimeseries(symbol); if(timeseries==NULL) return false; if(!timeseries.SyncData(timeframe,rates_total,required)) return false; return timeseries.CreateSeries(timeframe,required); } //+------------------------------------------------------------------+
Aqui tudo é exatamente o mesmo, com apenas uma diferença, isto é, a série temporal já está criada, portanto, a etapa de adicionar novas séries temporais ao objeto de todas as séries temporais do símbolo é ignorada.
Implementação de um método que recebe eventos a partir de um objeto de séries temporais e sua adição à lista de eventos da coleção de séries temporais:
//+------------------------------------------------------------------+ //| Get events from the timeseries object and add them to the list | //+------------------------------------------------------------------+ bool CTimeSeriesCollection::SetEvents(CTimeSeries *timeseries) { //--- Set the flag of successfully adding an event to the list and //--- get the list of symbol timeseries object events bool res=true; CArrayObj *list=timeseries.GetListEvents(); if(list==NULL) return false; //--- In the loop by the obtained list of events, int total=list.Total(); for(int i=0;i<total;i++) { //--- get the next event by the loop index and CEventBaseObj *event=timeseries.GetEvent(i); if(event==NULL) continue; //--- add the result of adding the obtained event to the flag value //--- from the symbol timeseries list to the timeseries collection list res &=this.EventAdd(event.ID(),event.LParam(),event.DParam(),event.SParam()); } //--- Return the result of adding events to the list return res; } //+------------------------------------------------------------------+
Ao método é transferido o ponteiro para um objeto de séries temporais do símbolo, e, num ciclo pela lista de eventos desse objeto, todos os seus eventos são adicionados à lista de eventos da coleção de séries temporais.
Implementação do método para atualizar a série temporal especificada do símbolo especificado e adição de seus eventos à lista de eventos da coleção de séries temporais:
//+------------------------------------------------------------------+ //| Update the specified timeseries of the specified symbol | //+------------------------------------------------------------------+ void CTimeSeriesCollection::Refresh(const string symbol,const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate) { //--- Reset the flag of an event in the timeseries collection and clear the event list this.m_is_event=false; this.m_list_events.Clear(); //--- Get the object of all symbol timeseries by a symbol name CTimeSeries *timeseries=this.GetTimeseries(symbol); if(timeseries==NULL) return; //--- If there is no new tick on the timeseries object symbol, exit if(!timeseries.IsNewTick()) return; //--- Update the required object timeseries of all symbol timeseries timeseries.Refresh(timeframe,data_calculate); //--- If the timeseries has the enabled event flag, //--- get events from symbol timeseries, write them to the collection event list //--- and set the event flag in the collection if(timeseries.IsEvent()) this.m_is_event=this.SetEvents(timeseries); } //+------------------------------------------------------------------+
Implementação do método de atualização de todas as séries temporais de todos os símbolos e inclusão de seus eventos na lista de eventos da coleção de séries temporais:
//+------------------------------------------------------------------+ //| Update all timeseries of all symbols | //+------------------------------------------------------------------+ void CTimeSeriesCollection::Refresh(SDataCalculate &data_calculate) { //--- Reset the flag of an event in the timeseries collection and clear the event list this.m_is_event=false; this.m_list_events.Clear(); //--- In the loop by all symbol timeseries objects in the collection, int total=this.m_list.Total(); for(int i=0;i<total;i++) { //--- get the next symbol timeseries object CTimeSeries *timeseries=this.m_list.At(i); if(timeseries==NULL) continue; //--- if there is no new tick on a timeseries symbol, move to the next object in the list if(!timeseries.IsNewTick()) continue; //--- Update all symbol timeseries timeseries.RefreshAll(data_calculate); //--- If the event flag enabled for the symbol timeseries object, //--- get events from symbol timeseries, write them to the collection event list //--- and set the event flag in the collection if(timeseries.IsEvent()) this.m_is_event=this.SetEvents(timeseries); } } //+------------------------------------------------------------------+
Todos esses métodos são comentados em detalhes, portanto não acho que sua lógica crie confusões.
Assim concluímos a modificação de todas as classes de séries temporais nesta fase.
Agora vamos modificar o objeto principal da biblioteca CEngine (\MQL5\Include\DoEasy\Engine.mqh) para trabalhar com uma coleção de séries temporais desde programas.
Na seção privada da classe declaramos um objeto-pausa:
class CEngine { private: CHistoryCollection m_history; // Collection of historical orders and deals CMarketCollection m_market; // Collection of market orders and deals CEventsCollection m_events; // Event collection CAccountsCollection m_accounts; // Account collection CSymbolsCollection m_symbols; // Symbol collection CTimeSeriesCollection m_time_series; // Timeseries collection CResourceCollection m_resource; // Resource list CTradingControl m_trading; // Trading management object CPause m_pause; // Pause object
À seção pública da classe adicionamos um método que retorne um sinalizador indicando presença de evento na coleção de séries temporais:
//--- Return the (1) hedge account, (2) working in the tester, (3) account event, (4) symbol event and (5) trading event flag bool IsHedge(void) const { return this.m_is_hedge; } bool IsTester(void) const { return this.m_is_tester; } bool IsAccountsEvent(void) const { return this.m_accounts.IsEvent(); } bool IsSymbolsEvent(void) const { return this.m_symbols.IsEvent(); } bool IsTradeEvent(void) const { return this.m_events.IsEvent(); } bool IsSeriesEvent(void) const { return this.m_time_series.IsEvent(); }
O método retorna o resultado de funcionamento do método IsEvent () do objeto-coleção de séries temporais.
Agora como para os métodos de atualização de séries temporais precisamos enviar dados de matrizes a partir do manipulador OnCalculate() do indicador para processar os dados da série temporal atual, aos métodos de processamento de eventos Timer e Tick adicionaremos a transferência de estruturas de dados das matrizes OnCalculate() e, ao mesmo tempo, declararemos um método para processar o evento Calculate:
//--- (1) Timer, (2) NewTick event handler and (3) Calculate event handler void OnTimer(SDataCalculate &data_calculate); void OnTick(SDataCalculate &data_calculate,const uint required=0); int OnCalculate(SDataCalculate &data_calculate,const uint required=0);
Na mesma seção pública da classe escrevemos um método que retorne uma lista de eventos de séries temporais:
//--- Return (1) the timeseries collection and (2) the list of timeseries from the timeseries collection and (3) the list of timeseries events CTimeSeriesCollection *GetTimeSeriesCollection(void) { return &this.m_time_series; } CArrayObj *GetListTimeSeries(void) { return this.m_time_series.GetList(); } CArrayObj *GetListSeriesEvents(void) { return this.m_time_series.GetListEvents(); }
O método retorna um ponteiro para a lista de eventos da coleção de séries temporais com ajuda do método de coleção de séries temporais GetListEvents()
Na seção pública da classe, temos quatro métodos para criar diferentes séries temporais. Vamos remover três métodos que ainda não são necessários:
//--- Create (1) the specified timeseries of the specified symbol, (2) the specified timeseries of all symbols, //--- (3) all timeseries of the specified symbol and (4) all timeseries of all symbols bool SeriesCreate(const string symbol,const ENUM_TIMEFRAMES timeframe,const uint required=0) { return this.m_series.CreateSeries(symbol,timeframe,required); } bool SeriesCreate(const ENUM_TIMEFRAMES timeframe,const uint required=0) { return this.m_series.CreateSeries(timeframe,required); } bool SeriesCreate(const string symbol,const uint required=0) { return this.m_series.CreateSeries(symbol,required); } bool SeriesCreate(const uint required=0) { return this.m_series.CreateSeries(required); }
e em vez deles escrevemos a declaração do método para criar todas as séries temporais de todos os símbolos usados da coleção, escrevemos um método para recriar a série temporal especificada e declaramos um método para solicitar que as séries temporais sejam sincronizadas com o servidor:
//--- Create (1) the specified timeseries of the specified symbol and (2) all used timeseries of all used symbols bool SeriesCreate(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0) { return this.m_time_series.CreateSeries(symbol,timeframe,rates_total,required); } bool SeriesCreateAll(const string &array_periods[],const int rates_total=0,const uint required=0); //--- Re-create a specified timeseries of a specified symbol bool SeriesReCreate(const string symbol,const ENUM_TIMEFRAMES timeframe,const int rates_total=0,const uint required=0) { return this.m_time_series.ReCreateSeries(symbol,timeframe,rates_total,required); } //--- Synchronize timeseries data with the server void SeriesSync(SDataCalculate &data_calculate,const uint required=0);
Agora, temos quatro métodos para atualizar a coleção de séries temporais.
Vamos deixar apenas dois métodos, o usado para atualizar a série temporal especificada e aquele para atualizar todas as séries temporais da coleção:
//--- Update (1) the specified timeseries of the specified symbol, (2) all timeseries of all symbols void SeriesRefresh(const string symbol,const ENUM_TIMEFRAMES timeframe,SDataCalculate &data_calculate) { this.m_time_series.Refresh(symbol,timeframe,data_calculate); } void SeriesRefresh(SDataCalculate &data_calculate) { this.m_time_series.Refresh(data_calculate); }
Agora, em vez de valores de matriz OnCalculate(), para os métodos é passada uma estrutura com dados de variáveis e de matrizes OnCalculate().
E adicionamos mais quatro métodos, um para retornar um ponteiro para um objeto de séries temporais do símbolo especificado, outro para o objeto-série temporal especificado, bem como métodos para retornar ponteiros para séries temporais vazias e incompletas:
//--- Return (1) the timeseries object of the specified symbol and (2) the timeseries object of the specified symbol/period CTimeSeries *SeriesGetTimeseries(const string symbol) { return this.m_time_series.GetTimeseries(symbol); } CSeries *SeriesGetSeries(const string symbol,const ENUM_TIMEFRAMES timeframe) { return this.m_time_series.GetSeries(symbol,timeframe); } //--- Return (1) an empty, (2) partially filled timeseries CSeries *SeriesGetSeriesEmpty(void) { return this.m_time_series.GetSeriesEmpty(); } CSeries *SeriesGetSeriesIncompleted(void) { return this.m_time_series.GetSeriesIncompleted(); }
Métodos retornam o resultado do retorno dos métodos da coleção de séries temporais com o mesmo nome, que examinamos acima.
O método TradingOnInit() que transfere à classe de negociação os ponteiros para todas as coleções necessárias foi renomeado para CollectionOnInit(), nele produziremos as inicializações necessárias de todas as classes-coleções.
No final do código do corpo da classe vamos inserir um bloco com métodos para trabalhar com o objeto-pausa:
//--- Set the new (1) pause countdown start time and (2) pause in milliseconds void PauseSetTimeBegin(const ulong time) { this.m_pause.SetTimeBegin(time); } void PauseSetWaitingMSC(const ulong pause) { this.m_pause.SetWaitingMSC(pause); } //--- Return (1) the time passed from the pause countdown start in milliseconds, (2) waiting completion flag //--- (3) pause countdown start time, (4) pause in milliseconds ulong PausePassed(void) const { return this.m_pause.Passed(); } bool PauseIsCompleted(void) const { return this.m_pause.IsCompleted(); } ulong PauseTimeBegin(void) const { return this.m_pause.TimeBegin(); } ulong PauseTimeWait(void) const { return this.m_pause.TimeWait(); } //--- Return the description (1) of the time passed till the countdown starts in milliseconds, //--- (2) pause countdown start time, (3) pause in milliseconds string PausePassedDescription(void) const { return this.m_pause.PassedDescription(); } string PauseTimeBeginDescription(void) const { return this.m_pause.TimeBeginDescription(); } string PauseWaitingMSCDescription(void) const { return this.m_pause.WaitingMSCDescription(); } string PauseWaitingSECDescription(void) const { return this.m_pause.WaitingSECDescription(); } //--- Launch the new pause countdown void Pause(const ulong pause_msc,const datetime time_start=0) { this.PauseSetWaitingMSC(pause_msc); this.PauseSetTimeBegin(time_start*1000); while(!this.PauseIsCompleted() && !IsStopped()){} } //--- Constructor/destructor CEngine(); ~CEngine();
A classe "Pausa" foi revisada por nós no artigo 30, e destina-se a organizar pausas em vez da função Sleep(), que não funciona em indicadores.
Aqui, juntamente com os métodos já descritos pela classe CPause que são chamados a partir desses métodos, adicionamos outro método Pause(), que imediatamente permite iniciar uma nova pausa pendente sem primeiro inicializar seus parâmetros, pois todos os parâmetros são passados para o método, e dentro do método é realizado um ciclo que espera que o número de milissegundos de pausa seja concluído, milissegundos esses que são passados para o método como um parâmetro de entrada. Esses métodos podem ser úteis em programas para realizar pausas em indicadores.
Você não deve esquecer que esse objeto-pausa também atrasa o fluxo principal no qual o indicador está sendo executado, como a função Sleep(),
e necessário aplicar essa pausa nos indicadores onde for justificado.
O temporizador da classe CEngine foi modificado. Anteriormente para cada manipulador de cada coleção, era verificado onde estávamos trabalhando (no testador ou não). Isso obrigava cada manipulador de todas as coleções a realizar essas verificações, o que não faz muito sentido.
Agora, primeiro é verificado se estamos trabalhando não no testador ou no testador, e depois dentro dos blocos, para não testador e testador, é realizado o processamento de todas as coleções:
//+------------------------------------------------------------------+ //| CEngine timer | //+------------------------------------------------------------------+ void CEngine::OnTimer(SDataCalculate &data_calculate) { //--- If this is not a tester, work with collection events by timer if(!this.IsTester()) { //--- Timer of the collections of historical orders and deals, as well as of market orders and positions int index=this.CounterIndex(COLLECTION_ORD_COUNTER_ID); CTimerCounter* cnt1=this.m_list_counters.At(index); if(cnt1!=NULL) { //--- If unpaused, work with the order, deal and position collections events if(cnt1.IsTimeDone()) this.TradeEventsControl(); } //--- Account collection timer index=this.CounterIndex(COLLECTION_ACC_COUNTER_ID); CTimerCounter* cnt2=this.m_list_counters.At(index); if(cnt2!=NULL) { //--- If unpaused, work with the account collection events if(cnt2.IsTimeDone()) this.AccountEventsControl(); } //--- Timer 1 of the symbol collection (updating symbol quote data in the collection) index=this.CounterIndex(COLLECTION_SYM_COUNTER_ID1); CTimerCounter* cnt3=this.m_list_counters.At(index); if(cnt3!=NULL) { //--- If the pause is over, update quote data of all symbols in the collection if(cnt3.IsTimeDone()) this.m_symbols.RefreshRates(); } //--- Timer 2 of the symbol collection (updating all data of all symbols in the collection and tracking symbl and symbol search events in the market watch window) index=this.CounterIndex(COLLECTION_SYM_COUNTER_ID2); CTimerCounter* cnt4=this.m_list_counters.At(index); if(cnt4!=NULL) { //--- If the pause is over if(cnt4.IsTimeDone()) { //--- update data and work with events of all symbols in the collection this.SymbolEventsControl(); //--- When working with the market watch list, check the market watch window events if(this.m_symbols.ModeSymbolsList()==SYMBOLS_MODE_MARKET_WATCH) this.MarketWatchEventsControl(); } } //--- Trading class timer index=this.CounterIndex(COLLECTION_REQ_COUNTER_ID); CTimerCounter* cnt5=this.m_list_counters.At(index); if(cnt5!=NULL) { //--- If unpaused, work with the list of pending requests if(cnt5.IsTimeDone()) this.m_trading.OnTimer(); } //--- Timeseries collection timer index=this.CounterIndex(COLLECTION_TS_COUNTER_ID); CTimerCounter* cnt6=this.m_list_counters.At(index); if(cnt6!=NULL) { //--- If unpaused, work with the timeseries list if(cnt6.IsTimeDone()) this.SeriesRefresh(data_calculate); } } //--- If this is a tester, work with collection events by tick else { //--- work with events of collections of orders, deals and positions by tick this.TradeEventsControl(); //--- work with events of collections of accounts by tick this.AccountEventsControl(); //--- update quote data of all collection symbols by tick this.m_symbols.RefreshRates(); //--- work with events of all symbols in the collection by tick this.SymbolEventsControl(); //--- work with the list of pending orders by tick this.m_trading.OnTimer(); //--- work with the timeseries list by tick this.SeriesRefresh(data_calculate); } } //+------------------------------------------------------------------+
O manipulador agora se tornou mais compacto e com uma lógica mais compreensível, e é dispensado de verificações repetidas desnecessárias.
Método que sincroniza os dados vazios das séries temporais com o servidor e recria uma série temporal vazia:
//+------------------------------------------------------------------+ //| Synchronize timeseries data with the server | //+------------------------------------------------------------------+ void CEngine::SeriesSync(SDataCalculate &data_calculate,const uint required=0) { //--- If the timeseries data is not calculated, try re-creating the timeseries //--- Get the pointer to the empty timeseries CSeries *series=this.SeriesGetSeriesEmpty(); if(series!=NULL) { //--- Display the empty timeseries data as a chart comment and try synchronizing the timeseries with the server data ::Comment(series.Header(),": ",CMessage::Text(MSG_LIB_TEXT_TS_TEXT_WAIT_FOR_SYNC)); ::ChartRedraw(::ChartID()); //--- if the data has been synchronized if(series.SyncData(0,data_calculate.rates_total)) { //--- if managed to re-create the timeseries if(this.m_time_series.ReCreateSeries(series.Symbol(),series.Timeframe(),data_calculate.rates_total)) { //--- display the chart comment and the journal entry with the re-created timeseries data ::Comment(series.Header(),": OK"); ::ChartRedraw(::ChartID()); Print(series.Header()," ",CMessage::Text(MSG_LIB_TEXT_TS_TEXT_CREATED_OK),":"); series.PrintShort(); } } } //--- Delete all comments else { ::Comment(""); ::ChartRedraw(::ChartID()); } } //+------------------------------------------------------------------+
Este método é a pedra angular para o carregamento correto dos dados históricos de qualquer série temporal usada, para quaisquer símbolos e períodos gráficos.
O método recupera a partir da coleção de séries temporais a primeira série temporal não preenchida, o que significa que não havia dados disponíveis anteriormente e imediatamente é feita uma tentativa de sincronizar os dados dessas séries temporais com os dados no servidor. Se não der certo, sairemos do método antes do próximo tick. Se os dados forem sincronizados, essas séries temporais serão recriadas, pois elas serão preenchidas com todas as barras disponíveis (mas não mais que a quantidade solicitada) do histórico.
E assim, a cada tick, obtemos as próximas séries temporais vazias, sincronizamos e recriamos, até que não existam séries temporais vazias.
Implementação de manipuladores de eventos NewTick e Calculate:
//+------------------------------------------------------------------+ //| NewTick event handler | //+------------------------------------------------------------------+ void CEngine::OnTick(SDataCalculate &data_calculate,const uint required=0) { //--- If this is not a EA, exit if(this.m_program!=PROGRAM_EXPERT) return; //--- Re-create empty timeseries this.SeriesSync(data_calculate,required); //--- end } //+------------------------------------------------------------------+ //| Calculate event handler | //+------------------------------------------------------------------+ int CEngine::OnCalculate(SDataCalculate &data_calculate,const uint required=0) { //--- If this is not an indicator, exit if(this.m_program!=PROGRAM_INDICATOR) return data_calculate.rates_total; //--- Re-create empty timeseries this.SeriesSync(data_calculate,required); //--- return rates_total return data_calculate.rates_total; } //+------------------------------------------------------------------+
Nos dois métodos é chamado um método para recriar séries temporais vazias.
Mas os métodos em si devem ser chamados a partir dos manipuladores de programas com o mesmo nome, trabalhando com base nessa biblioteca.
Implementação do método para criar todas as séries temporais usadas de todos os símbolos usados:
//+------------------------------------------------------------------+ //| Create all applied timeseries of all used symbols | //+------------------------------------------------------------------+ bool CEngine::SeriesCreateAll(const string &array_periods[],const int rates_total=0,const uint required=0) { //--- Set the flag of successful creation of all timeseries of all symbols bool res=true; //--- Get the list of all used symbols CArrayObj* list_symbols=this.GetListAllUsedSymbols(); if(list_symbols==NULL) { ::Print(DFUN,CMessage::Text(MSG_LIB_SYS_FAILED_GET_SYMBOLS_ARRAY)); return false; } //--- In the loop by the total number of symbols for(int i=0;i<list_symbols.Total();i++) { //--- get the next symbol object CSymbol *symbol=list_symbols.At(i); if(symbol==NULL) { ::Print(DFUN,"index ",i,": ",CMessage::Text(MSG_LIB_SYS_ERROR_FAILED_GET_SYM_OBJ)); continue; } //--- In the loop by the total number of used timeframes, int total_periods=::ArraySize(array_periods); for(int j=0;j<total_periods;j++) { //--- create the timeseries object of the next symbol. //--- Add the timeseries creation result to the res variable ENUM_TIMEFRAMES timeframe=TimeframeByDescription(array_periods[j]); res &=this.SeriesCreate(symbol.Name(),timeframe,rates_total,required); } } //--- Return the result of creating all timeseries for all symbols return res; } //+------------------------------------------------------------------+
O método deve ser chamado quando o programa for inicializado após a criação de uma lista de todos os símbolos usados.
Ao método é transferida a matriz criada durante a inicialização com os nomes dos períodos do gráfico usado, bem como parâmetros para criar séries temporais, isto é, o número de barras das séries temporais atuais (apenas para indicadores - rates_total) e a profundidade necessária do histórico para séries temporais criadas (por padrão, 1000, mas não mais que o valor de Bars() do símbolo, para indicadores, não mais que rates_total).
Essas são todas as modificações necessárias para trabalhar com séries temporais hoje.
Testando o funcionamento de séries temporais e seus eventos em indicadores
Para testar o funcionamento da classe-coleção de séries temporais nos indicadores, no diretório de indicadores do terminal criaremos uma nova pasta
\MQL5\Indicators\TestDoEasy\ e, nela, uma subpasta Part39\, nela geraremos um novo indicador chamado de TestDoEasyPart39.mq5.
O número e o tipo de buffers de indicador plotados até agora não importam para nós, pois não vamos desenhar nada nele. Mas para o futuro, defino dois buffers desenháveis com um tipo de plotagem DRAW_LINE.
Os parâmetros de entrada necessários do indicador para definir os símbolos e períodos desejados, e alguns outros,
foram transferidos desde o EA de teste do artigo anterior. Este foi o resultado:
//+------------------------------------------------------------------+ //| TestDoEasyPart39.mq5 | //| Copyright 2020, MetaQuotes Software Corp. | //| https://mql5.com/en/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2020, MetaQuotes Software Corp." #property link "https://mql5.com/en/users/artmedia70" #property version "1.00" //--- includes #include <DoEasy\Engine.mqh> //--- enums //--- defines //--- structures //--- properties #property indicator_chart_window #property indicator_buffers 2 #property indicator_plots 2 //--- plot Label1 #property indicator_label1 "Label1" #property indicator_type1 DRAW_LINE #property indicator_color1 clrRed #property indicator_style1 STYLE_SOLID #property indicator_width1 1 //--- plot Label2 #property indicator_label2 "Label2" #property indicator_type2 DRAW_LINE #property indicator_color2 clrGreen #property indicator_style2 STYLE_SOLID #property indicator_width2 1 //--- indicator buffers double Buffer1[]; double Buffer2[]; //--- input variables sinput ENUM_SYMBOLS_MODE InpModeUsedSymbols = SYMBOLS_MODE_CURRENT; // Mode of used symbols list sinput string InpUsedSymbols = "EURUSD,AUDUSD,EURAUD,EURCAD,EURGBP,EURJPY,EURUSD,GBPUSD,NZDUSD,USDCAD,USDJPY"; // List of used symbols (comma - separator) sinput ENUM_TIMEFRAMES_MODE InpModeUsedTFs = TIMEFRAMES_MODE_LIST; // Mode of used timeframes list sinput string InpUsedTFs = "M1,M5,M15,M30,H1,H4,D1,W1,MN1"; // List of used timeframes (comma - separator) sinput bool InpUseSounds = true; // Use sounds //--- global variables CEngine engine; // CEngine library main object string prefix; // Prefix of graphical object names bool testing; // Flag of working in the tester int used_symbols_mode; // Mode of working with symbols string array_used_symbols[]; // Array of used symbols string array_used_periods[]; // Array of used timeframes //+------------------------------------------------------------------+
No manipulador OnInit() do indicador, escrevemos a definição das variáveis globais do indicador e uma chamada para a função de inicialização da biblioteca:
//+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- indicator buffers mapping SetIndexBuffer(0,Buffer1,INDICATOR_DATA); SetIndexBuffer(1,Buffer2,INDICATOR_DATA); //--- Set indicator global variables prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_"; testing=engine.IsTester(); ZeroMemory(rates_data); //--- Initialize DoEasy library OnInitDoEasy(); //--- Check and remove remaining indicator graphical objects if(IsPresentObectByPrefix(prefix)) ObjectsDeleteAll(0,prefix); //--- Check playing a standard sound using macro substitutions engine.PlaySoundByDescription(SND_OK); //--- Wait for 600 milliseconds engine.Pause(600); engine.PlaySoundByDescription(SND_NEWS); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+
Tomamos o manipulador OnDeinit() do indicador a partir do EA de teste do artigo anterior:
//+------------------------------------------------------------------+ //| Custom indicator deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Remove indicator graphical objects by an object name prefix ObjectsDeleteAll(0,prefix); Comment(""); } //+------------------------------------------------------------------+
Também tomamos os manipuladores OnTimer() e OnChartEvent() a partir do EA:
//+------------------------------------------------------------------+ //| Timer function | //+------------------------------------------------------------------+ void OnTimer() { //--- Launch the library timer (only not in the tester) if(!MQLInfoInteger(MQL_TESTER)) engine.OnTimer(rates_data); } //+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- If working in the tester, exit if(MQLInfoInteger(MQL_TESTER)) return; //--- Handling mouse events if(id==CHARTEVENT_OBJECT_CLICK) { //--- Handling pressing the buttons in the panel if(StringFind(sparam,"BUTT_")>0) PressButtonEvents(sparam); } //--- Handling DoEasy library events if(id>CHARTEVENT_CUSTOM-1) { OnDoEasyEvent(id,lparam,dparam,sparam); } } //+------------------------------------------------------------------+
Para preencher as estruturas de dados de matrizes e variáveis do primeiro e segundo formulário OnCalculate() do indicador, criamos duas funções:
//+------------------------------------------------------------------+ //| Copy data from the first OnCalculate() form to the structure | //+------------------------------------------------------------------+ void CopyData(SDataCalculate &data_calculate, const int rates_total, const int prev_calculated, const int begin, const double &price[]) { //--- Get the array indexing flag as in the timeseries. If failed, //--- set the indexing direction for the array as in the timeseries bool as_series_price=ArrayGetAsSeries(price); if(!as_series_price) ArraySetAsSeries(price,true); //--- Copy the array zero bar to the OnCalculate() SDataCalculate data structure data_calculate.rates_total=rates_total; data_calculate.prev_calculated=prev_calculated; data_calculate.begin=begin; data_calculate.price=price[0]; //--- Return the array's initial indexing direction if(!as_series_price) ArraySetAsSeries(price,false); } //+------------------------------------------------------------------+ //| Copy data from the second OnCalculate() form to the structure | //+------------------------------------------------------------------+ void CopyData(SDataCalculate &data_calculate, 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[]) { //--- Get the array indexing flags as in the timeseries. If failed, //--- set the indexing direction or the arrays as in the timeseries bool as_series_time=ArrayGetAsSeries(time); if(!as_series_time) ArraySetAsSeries(time,true); bool as_series_open=ArrayGetAsSeries(open); if(!as_series_open) ArraySetAsSeries(open,true); bool as_series_high=ArrayGetAsSeries(high); if(!as_series_high) ArraySetAsSeries(high,true); bool as_series_low=ArrayGetAsSeries(low); if(!as_series_low) ArraySetAsSeries(low,true); bool as_series_close=ArrayGetAsSeries(close); if(!as_series_close) ArraySetAsSeries(close,true); bool as_series_tick_volume=ArrayGetAsSeries(tick_volume); if(!as_series_tick_volume) ArraySetAsSeries(tick_volume,true); bool as_series_volume=ArrayGetAsSeries(volume); if(!as_series_volume) ArraySetAsSeries(volume,true); bool as_series_spread=ArrayGetAsSeries(spread); if(!as_series_spread) ArraySetAsSeries(spread,true); //--- Copy the arrays' zero bar to the OnCalculate() SDataCalculate data structure data_calculate.rates_total=rates_total; data_calculate.prev_calculated=prev_calculated; data_calculate.rates.time=time[0]; data_calculate.rates.open=open[0]; data_calculate.rates.high=high[0]; data_calculate.rates.low=low[0]; data_calculate.rates.close=close[0]; data_calculate.rates.tick_volume=tick_volume[0]; data_calculate.rates.real_volume=(#ifdef __MQL5__ volume[0] #else 0 #endif); data_calculate.rates.spread=(#ifdef __MQL5__ spread[0] #else 0 #endif); //--- Return the arrays' initial indexing direction if(!as_series_time) ArraySetAsSeries(time,false); if(!as_series_open) ArraySetAsSeries(open,false); if(!as_series_high) ArraySetAsSeries(high,false); if(!as_series_low) ArraySetAsSeries(low,false); if(!as_series_close) ArraySetAsSeries(close,false); if(!as_series_tick_volume) ArraySetAsSeries(tick_volume,false); if(!as_series_volume) ArraySetAsSeries(volume,false); if(!as_series_spread) ArraySetAsSeries(spread,false); } //+------------------------------------------------------------------+
Também transferiremos desde o EA de teste a função de processamento de eventos da biblioteca DoEasy:
//+------------------------------------------------------------------+ //| Handling DoEasy library events | //+------------------------------------------------------------------+ void OnDoEasyEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { int idx=id-CHARTEVENT_CUSTOM; //--- Retrieve (1) event time milliseconds, (2) reason and (3) source from lparam, as well as (4) set the exact event time ushort msc=engine.EventMSC(lparam); ushort reason=engine.EventReason(lparam); ushort source=engine.EventSource(lparam); long time=TimeCurrent()*1000+msc; //--- Handling symbol events if(source==COLLECTION_SYMBOLS_ID) { CSymbol *symbol=engine.GetSymbolObjByName(sparam); if(symbol==NULL) return; //--- Number of decimal places in the event value - in case of a 'long' event, it is 0, otherwise - Digits() of a symbol int digits=(idx<SYMBOL_PROP_INTEGER_TOTAL ? 0 : symbol.Digits()); //--- Event text description string id_descr=(idx<SYMBOL_PROP_INTEGER_TOTAL ? symbol.GetPropertyDescription((ENUM_SYMBOL_PROP_INTEGER)idx) : symbol.GetPropertyDescription((ENUM_SYMBOL_PROP_DOUBLE)idx)); //--- Property change text value string value=DoubleToString(dparam,digits); //--- Check event reasons and display its description in the journal if(reason==BASE_EVENT_REASON_INC) { Print(symbol.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits)); } if(reason==BASE_EVENT_REASON_DEC) { Print(symbol.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits)); } if(reason==BASE_EVENT_REASON_MORE_THEN) { Print(symbol.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits)); } if(reason==BASE_EVENT_REASON_LESS_THEN) { Print(symbol.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits)); } if(reason==BASE_EVENT_REASON_EQUALS) { Print(symbol.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits)); } } //--- Handling account events else if(source==COLLECTION_ACCOUNT_ID) { CAccount *account=engine.GetAccountCurrent(); if(account==NULL) return; //--- Number of decimal places in the event value - in case of a 'long' event, it is 0, otherwise - Digits() of a symbol int digits=int(idx<ACCOUNT_PROP_INTEGER_TOTAL ? 0 : account.CurrencyDigits()); //--- Event text description string id_descr=(idx<ACCOUNT_PROP_INTEGER_TOTAL ? account.GetPropertyDescription((ENUM_ACCOUNT_PROP_INTEGER)idx) : account.GetPropertyDescription((ENUM_ACCOUNT_PROP_DOUBLE)idx)); //--- Property change text value string value=DoubleToString(dparam,digits); //--- Checking event reasons and handling the increase of funds by a specified value, //--- In case of a property value increase if(reason==BASE_EVENT_REASON_INC) { //--- Display an event in the journal Print(account.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits)); //--- if this is an equity increase if(idx==ACCOUNT_PROP_EQUITY) { //--- Get the list of all open positions for the current symbol CArrayObj* list_positions=engine.GetListMarketPosition(); list_positions=CSelect::ByOrderProperty(list_positions,ORDER_PROP_SYMBOL,Symbol(),EQUAL); //--- Select positions with the profit exceeding zero list_positions=CSelect::ByOrderProperty(list_positions,ORDER_PROP_PROFIT_FULL,0,MORE); if(list_positions!=NULL) { //--- Sort the list by profit considering commission and swap list_positions.Sort(SORT_BY_ORDER_PROFIT_FULL); //--- Get the position index with the highest profit int index=CSelect::FindOrderMax(list_positions,ORDER_PROP_PROFIT_FULL); if(index>WRONG_VALUE) { COrder* position=list_positions.At(index); if(position!=NULL) { //--- Get a ticket of a position with the highest profit and close the position by a ticket engine.ClosePosition(position.Ticket()); } } } } } //--- Other events are simply displayed in the journal if(reason==BASE_EVENT_REASON_DEC) { Print(account.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits)); } if(reason==BASE_EVENT_REASON_MORE_THEN) { Print(account.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits)); } if(reason==BASE_EVENT_REASON_LESS_THEN) { Print(account.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits)); } if(reason==BASE_EVENT_REASON_EQUALS) { Print(account.EventDescription(idx,(ENUM_BASE_EVENT_REASON)reason,source,value,id_descr,digits)); } } //--- Handling market watch window events else if(idx>MARKET_WATCH_EVENT_NO_EVENT && idx<SYMBOL_EVENTS_NEXT_CODE) { //--- Market Watch window event string descr=engine.GetMWEventDescription((ENUM_MW_EVENT)idx); string name=(idx==MARKET_WATCH_EVENT_SYMBOL_SORT ? "" : ": "+sparam); Print(TimeMSCtoString(lparam)," ",descr,name); } //--- Handling timeseries events else if(idx>SERIES_EVENTS_NO_EVENT && idx<SERIES_EVENTS_NEXT_CODE) { //--- "New bar" event if(idx==SERIES_EVENTS_NEW_BAR) { Print(TextByLanguage("Новый бар на ","New Bar on "),sparam," ",TimeframeDescription((ENUM_TIMEFRAMES)dparam),": ",TimeToString(lparam)); } } //--- Handling trading events else if(idx>TRADE_EVENT_NO_EVENT && idx<TRADE_EVENTS_NEXT_CODE) { //--- Get the list of trading events CArrayObj *list=engine.GetListAllOrdersEvents(); if(list==NULL) return; //--- get the event index shift relative to the end of the list //--- in the tester, the shift is passed by the lparam parameter to the event handler //--- outside the tester, events are sent one by one and handled in OnChartEvent() int shift=(testing ? (int)lparam : 0); CEvent *event=list.At(list.Total()-1-shift); if(event==NULL) return; //--- Accrue the credit if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_CREDIT) { Print(DFUN,event.TypeEventDescription()); } //--- Additional charges if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_CHARGE) { Print(DFUN,event.TypeEventDescription()); } //--- Correction if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_CORRECTION) { Print(DFUN,event.TypeEventDescription()); } //--- Enumerate bonuses if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_BONUS) { Print(DFUN,event.TypeEventDescription()); } //--- Additional commissions if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_COMISSION) { Print(DFUN,event.TypeEventDescription()); } //--- Daily commission if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_COMISSION_DAILY) { Print(DFUN,event.TypeEventDescription()); } //--- Monthly commission if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_COMISSION_MONTHLY) { Print(DFUN,event.TypeEventDescription()); } //--- Daily agent commission if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_COMISSION_AGENT_DAILY) { Print(DFUN,event.TypeEventDescription()); } //--- Monthly agent commission if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_COMISSION_AGENT_MONTHLY) { Print(DFUN,event.TypeEventDescription()); } //--- Interest rate if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_INTEREST) { Print(DFUN,event.TypeEventDescription()); } //--- Canceled buy deal if(event.TypeEvent()==TRADE_EVENT_BUY_CANCELLED) { Print(DFUN,event.TypeEventDescription()); } //--- Canceled sell deal if(event.TypeEvent()==TRADE_EVENT_SELL_CANCELLED) { Print(DFUN,event.TypeEventDescription()); } //--- Dividend operations if(event.TypeEvent()==TRADE_EVENT_DIVIDENT) { Print(DFUN,event.TypeEventDescription()); } //--- Accrual of franked dividend if(event.TypeEvent()==TRADE_EVENT_DIVIDENT_FRANKED) { Print(DFUN,event.TypeEventDescription()); } //--- Tax charges if(event.TypeEvent()==TRADE_EVENT_TAX) { Print(DFUN,event.TypeEventDescription()); } //--- Replenishing account balance if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_BALANCE_REFILL) { Print(DFUN,event.TypeEventDescription()); } //--- Withdrawing funds from balance if(event.TypeEvent()==TRADE_EVENT_ACCOUNT_BALANCE_WITHDRAWAL) { Print(DFUN,event.TypeEventDescription()); } //--- Pending order placed if(event.TypeEvent()==TRADE_EVENT_PENDING_ORDER_PLASED) { Print(DFUN,event.TypeEventDescription()); } //--- Pending order removed if(event.TypeEvent()==TRADE_EVENT_PENDING_ORDER_REMOVED) { Print(DFUN,event.TypeEventDescription()); } //--- Pending order activated by price if(event.TypeEvent()==TRADE_EVENT_PENDING_ORDER_ACTIVATED) { Print(DFUN,event.TypeEventDescription()); } //--- Pending order partially activated by price if(event.TypeEvent()==TRADE_EVENT_PENDING_ORDER_ACTIVATED_PARTIAL) { Print(DFUN,event.TypeEventDescription()); } //--- Position opened if(event.TypeEvent()==TRADE_EVENT_POSITION_OPENED) { Print(DFUN,event.TypeEventDescription()); } //--- Position opened partially if(event.TypeEvent()==TRADE_EVENT_POSITION_OPENED_PARTIAL) { Print(DFUN,event.TypeEventDescription()); } //--- Position closed if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED) { Print(DFUN,event.TypeEventDescription()); } //--- Position closed by an opposite one if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_BY_POS) { Print(DFUN,event.TypeEventDescription()); } //--- Position closed by StopLoss if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_BY_SL) { Print(DFUN,event.TypeEventDescription()); } //--- Position closed by TakeProfit if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_BY_TP) { Print(DFUN,event.TypeEventDescription()); } //--- Position reversal by a new deal (netting) if(event.TypeEvent()==TRADE_EVENT_POSITION_REVERSED_BY_MARKET) { Print(DFUN,event.TypeEventDescription()); } //--- Position reversal by activating a pending order (netting) if(event.TypeEvent()==TRADE_EVENT_POSITION_REVERSED_BY_PENDING) { Print(DFUN,event.TypeEventDescription()); } //--- Position reversal by partial market order execution (netting) if(event.TypeEvent()==TRADE_EVENT_POSITION_REVERSED_BY_MARKET_PARTIAL) { Print(DFUN,event.TypeEventDescription()); } //--- Position reversal by activating a pending order (netting) if(event.TypeEvent()==TRADE_EVENT_POSITION_REVERSED_BY_PENDING_PARTIAL) { Print(DFUN,event.TypeEventDescription()); } //--- Added volume to a position by a new deal (netting) if(event.TypeEvent()==TRADE_EVENT_POSITION_VOLUME_ADD_BY_MARKET) { Print(DFUN,event.TypeEventDescription()); } //--- Added volume to a position by partial execution of a market order (netting) if(event.TypeEvent()==TRADE_EVENT_POSITION_VOLUME_ADD_BY_MARKET_PARTIAL) { Print(DFUN,event.TypeEventDescription()); } //--- Added volume to a position by activating a pending order (netting) if(event.TypeEvent()==TRADE_EVENT_POSITION_VOLUME_ADD_BY_PENDING) { Print(DFUN,event.TypeEventDescription()); } //--- Added volume to a position by partial activation of a pending order (netting) if(event.TypeEvent()==TRADE_EVENT_POSITION_VOLUME_ADD_BY_PENDING_PARTIAL) { Print(DFUN,event.TypeEventDescription()); } //--- Position closed partially if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_PARTIAL) { Print(DFUN,event.TypeEventDescription()); } //--- Position partially closed by an opposite one if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_POS) { Print(DFUN,event.TypeEventDescription()); } //--- Position closed partially by StopLoss if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_SL) { Print(DFUN,event.TypeEventDescription()); } //--- Position closed partially by TakeProfit if(event.TypeEvent()==TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_TP) { Print(DFUN,event.TypeEventDescription()); } //--- StopLimit order activation if(event.TypeEvent()==TRADE_EVENT_TRIGGERED_STOP_LIMIT_ORDER) { Print(DFUN,event.TypeEventDescription()); } //--- Changing order price if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_PRICE) { Print(DFUN,event.TypeEventDescription()); } //--- Changing order and StopLoss price if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_PRICE_SL) { Print(DFUN,event.TypeEventDescription()); } //--- Changing order and TakeProfit price if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_PRICE_TP) { Print(DFUN,event.TypeEventDescription()); } //--- Changing order, StopLoss and TakeProfit price if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_PRICE_SL_TP) { Print(DFUN,event.TypeEventDescription()); } //--- Changing order's StopLoss and TakeProfit price if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_SL_TP) { Print(DFUN,event.TypeEventDescription()); } //--- Changing order's StopLoss if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_SL) { Print(DFUN,event.TypeEventDescription()); } //--- Changing order's TakeProfit if(event.TypeEvent()==TRADE_EVENT_MODIFY_ORDER_TP) { Print(DFUN,event.TypeEventDescription()); } //--- Changing position's StopLoss and TakeProfit if(event.TypeEvent()==TRADE_EVENT_MODIFY_POSITION_SL_TP) { Print(DFUN,event.TypeEventDescription()); } //--- Changing position StopLoss if(event.TypeEvent()==TRADE_EVENT_MODIFY_POSITION_SL) { Print(DFUN,event.TypeEventDescription()); } //--- Changing position TakeProfit if(event.TypeEvent()==TRADE_EVENT_MODIFY_POSITION_TP) { Print(DFUN,event.TypeEventDescription()); } } } //+------------------------------------------------------------------+
Também transferiremos desde o EA a função para trabalhar com eventos da biblioteca no testador:
//+------------------------------------------------------------------+ //| Working with events in the tester | //+------------------------------------------------------------------+ void EventsHandling(void) { //--- If a trading event is present if(engine.IsTradeEvent()) { //--- Number of trading events occurred simultaneously int total=engine.GetTradeEventsTotal(); for(int i=0;i<total;i++) { //--- Get the next event from the list of simultaneously occurred events by index CEventBaseObj *event=engine.GetTradeEventByIndex(i); if(event==NULL) continue; long lparam=i; double dparam=event.DParam(); string sparam=event.SParam(); OnDoEasyEvent(CHARTEVENT_CUSTOM+event.ID(),lparam,dparam,sparam); } } //--- If there is an account event if(engine.IsAccountsEvent()) { //--- Get the list of all account events occurred simultaneously CArrayObj* list=engine.GetListAccountEvents(); if(list!=NULL) { //--- Get the next event in a loop int total=list.Total(); for(int i=0;i<total;i++) { //--- take an event from the list CEventBaseObj *event=list.At(i); if(event==NULL) continue; //--- Send an event to the event handler long lparam=event.LParam(); double dparam=event.DParam(); string sparam=event.SParam(); OnDoEasyEvent(CHARTEVENT_CUSTOM+event.ID(),lparam,dparam,sparam); } } } //--- If there is a symbol collection event if(engine.IsSymbolsEvent()) { //--- Get the list of all symbol events occurred simultaneously CArrayObj* list=engine.GetListSymbolsEvents(); if(list!=NULL) { //--- Get the next event in a loop int total=list.Total(); for(int i=0;i<total;i++) { //--- take an event from the list CEventBaseObj *event=list.At(i); if(event==NULL) continue; //--- Send an event to the event handler long lparam=event.LParam(); double dparam=event.DParam(); string sparam=event.SParam(); OnDoEasyEvent(CHARTEVENT_CUSTOM+event.ID(),lparam,dparam,sparam); } } } //--- If there is a timeseries collection event if(engine.IsSeriesEvent()) { //--- Get the list of all timeseries events occurred simultaneously CArrayObj* list=engine.GetListSeriesEvents(); if(list!=NULL) { //--- Get the next event in a loop int total=list.Total(); for(int i=0;i<total;i++) { //--- take an event from the list CEventBaseObj *event=list.At(i); if(event==NULL) continue; //--- Send an event to the event handler long lparam=event.LParam(); double dparam=event.DParam(); string sparam=event.SParam(); OnDoEasyEvent(CHARTEVENT_CUSTOM+event.ID(),lparam,dparam,sparam); } } } } //+------------------------------------------------------------------+
Por enquanto, é possível não transferir as funções para trabalhar com botões do painel de negociação, porém, para o futuro, para poder usar alguns botões no indicador, também transferiremos desde o EA essas funções com pequenas alterações (dois botões):
//+------------------------------------------------------------------+ //| Return the button status | //+------------------------------------------------------------------+ bool ButtonState(const string name) { return (bool)ObjectGetInteger(0,name,OBJPROP_STATE); } //+------------------------------------------------------------------+ //| Set the button status | //+------------------------------------------------------------------+ void ButtonState(const string name,const bool state) { ObjectSetInteger(0,name,OBJPROP_STATE,state); //--- Button 1 if(name=="BUTT_1") { if(state) ObjectSetInteger(0,name,OBJPROP_BGCOLOR,C'220,255,240'); else ObjectSetInteger(0,name,OBJPROP_BGCOLOR,C'240,240,240'); } //--- Button 2 if(name=="BUTT_2") { if(state) ObjectSetInteger(0,name,OBJPROP_BGCOLOR,C'255,220,90'); else ObjectSetInteger(0,name,OBJPROP_BGCOLOR,C'240,240,240'); } } //+------------------------------------------------------------------+ //| Track the buttons' status | //+------------------------------------------------------------------+ void PressButtonsControl(void) { int total=ObjectsTotal(0,0); for(int i=0;i<total;i++) { string obj_name=ObjectName(0,i); if(StringFind(obj_name,prefix+"BUTT_")<0) continue; PressButtonEvents(obj_name); } } //+------------------------------------------------------------------+ //| Handle pressing the buttons | //+------------------------------------------------------------------+ void PressButtonEvents(const string button_name) { //--- Convert button name into its string ID string button=StringSubstr(button_name,StringLen(prefix)); //--- If the button is pressed if(ButtonState(button_name)) { //--- If button 1 is pressed if(button=="BUTT_1") { } //--- If button 2 is pressed else if(button=="BUTT_2") { } //--- Wait for 1/10 of a second engine.Pause(100); //--- "Unpress" the button (if this is neither a trailing button, nor the buttons enabling pending requests) ButtonState(button_name,false); //--- re-draw the chart ChartRedraw(); } //--- Not pressed else { //--- button 1 if(button=="BUTT_1") { ButtonState(button_name,false); } //--- button 2 if(button=="BUTT_2") { ButtonState(button_name,false); } //--- re-draw the chart ChartRedraw(); } } //+------------------------------------------------------------------+
Como se pode ver, a maioria das funções do EA pode ser usada em indicadores, sem problemas e alterações. Isso faz pensar que todas as funções realmente necessárias para trabalhar com a biblioteca de EAs e indicadores precisarão ser transferidas para o arquivo incluído da biblioteca, e, a partir daí, começar a usar. Mas, isso será depois. Agora precisamos criar o manipulador OnCalculate() do indicador.
O manipulador consistirá no bloco de código necessário para preparar dados da biblioteca e num bloco de código opcional (hoje) para trabalhar com o indicador:
//+------------------------------------------------------------------+ //| 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[]) { //+------------------------------------------------------------------+ //| OnCalculate code block for working with the library: | //+------------------------------------------------------------------+ //--- Pass the current symbol data from OnCalculate() to the price structure CopyData(rates_data,rates_total,prev_calculated,time,open,high,low,close,tick_volume,volume,spread); //--- Handle the Calculate event in the library engine.OnCalculate(rates_data); //--- If working in the tester if(MQLInfoInteger(MQL_TESTER)) { engine.OnTimer(rates_data); // Working in the timer PressButtonsControl(); // Button pressing control EventsHandling(); // Working with events } //+------------------------------------------------------------------+ //| OnCalculate code block for working with the indicator: | //+------------------------------------------------------------------+ //--- Arrange resource-saving indicator calculations //--- Set OnCalculate arrays as timeseries ArraySetAsSeries(open,true); ArraySetAsSeries(high,true); ArraySetAsSeries(low,true); ArraySetAsSeries(close,true); ArraySetAsSeries(tick_volume,true); ArraySetAsSeries(volume,true); ArraySetAsSeries(spread,true); //--- Setting buffer arrays as timeseries ArraySetAsSeries(Buffer1,true); ArraySetAsSeries(Buffer2,true); //--- Check for the minimum number of bars for calculation if(rates_total<2 || Point()==0) return 0; //--- Check and calculate the number of calculated bars int limit=rates_total-prev_calculated; if(limit>1) { limit=rates_total-1; ArrayInitialize(Buffer1,EMPTY_VALUE); ArrayInitialize(Buffer2,EMPTY_VALUE); } //--- Prepare data for(int i=limit; i>=0 && !IsStopped(); i--) { // the code for preparing indicator calculation buffers } //--- Calculate the indicator for(int i=limit; i>=0 && !IsStopped(); i--) { Buffer1[i]=high[i]; Buffer2[i]=low[i]; } //--- return value of prev_calculated for next call return(rates_total); } //+------------------------------------------------------------------+
Como podemos ver, tudo relacionado ao trabalho da biblioteca se encaixa num pequeno bloco de código no manipulador OnCalculate() e, de fato, a única diferença com o EA é que preenchemos previamente a estrutura de preços dos dados atuais das matrizes desde OnCalculate() com a função CopyData(), e tudo o restante é absolutamente idêntico ao trabalho no EA - a biblioteca funciona no temporizador se o indicador estiver sendo executado no gráfico de símbolos e no OnCalculate() por ticks, se o indicador estiver sendo executado no testador.
Os buffers do indicador na parte calculada do OnCalculate() são simplesmente preenchidos com os dados das matrizes high[] e low[].
O código completo do indicador pode ser visualizado nos arquivos anexados no final do artigo.
Compilaremos o indicador e o iniciaremos no gráfico do símbolo, com o qual não trabalhamos há muito tempo, depois definir o símbolo atual nas configurações e selecionar a lista de períodos gráficos desejada. A execução de símbolos longos não utilizados fará com que o indicador carregue os dados ausentes e relate isso no log e no gráfico:
Aqui vemos que, a cada novo tick, as próximas séries temporais vazias foram sincronizadas e criadas. Nesse caso, as seguintes entradas foram gravadas no log:
Account 8550475: Artyom Trishkin (MetaQuotes Software Corp.) 10425.23 USD, 1:100, Hedge, MetaTrader 5 demo --- Initializing "DoEasy" library --- Working with the current symbol only: "USDCAD" Working with the specified timeframe list: "M1" "M5" "M15" "M30" "H1" "H4" "D1" "W1" "MN1" USDCAD symbol timeseries: - Timeseries "USDCAD" M1: Requested: 1000, Actual: 0, Created: 0, On the server: 0 - Timeseries "USDCAD" M5: Requested: 1000, Actual: 0, Created: 0, On the server: 0 - Timeseries "USDCAD" M15: Requested: 1000, Actual: 0, Created: 0, On the server: 0 - Timeseries "USDCAD" M30: Requested: 1000, Actual: 0, Created: 0, On the server: 0 - Timeseries "USDCAD" H1: Requested: 1000, Actual: 0, Created: 0, On the server: 0 - Timeseries "USDCAD" H4: Requested: 1000, Actual: 0, Created: 0, On the server: 0 - Timeseries "USDCAD" D1: Requested: 1000, Actual: 0, Created: 0, On the server: 0 - Timeseries "USDCAD" W1: Requested: 1000, Actual: 0, Created: 0, On the server: 0 - Timeseries "USDCAD" MN1: Requested: 1000, Actual: 0, Created: 0, On the server: 0 Library initialization time: 00:00:01.406 "USDCAD" M1 timeseries created successfully: - Timeseries "USDCAD" M1: Requested: 1000, Actual: 1000, Created: 1000, On the server: 5001 "USDCAD" M5 timeseries created successfully: - Timeseries "USDCAD" M5: Requested: 1000, Actual: 1000, Created: 1000, On the server: 5741 "USDCAD" M15 timeseries created successfully: - Timeseries "USDCAD" M15: Requested: 1000, Actual: 1000, Created: 1000, On the server: 5247 "USDCAD" M30 timeseries created successfully: - Timeseries "USDCAD" M30: Requested: 1000, Actual: 1000, Created: 1000, On the server: 5123 "USDCAD" H1 timeseries created successfully: - Timeseries "USDCAD" H1: Requested: 1000, Actual: 1000, Created: 1000, On the server: 6257 "USDCAD" H4 timeseries created successfully: - Timeseries "USDCAD" H4: Requested: 1000, Actual: 1000, Created: 1000, On the server: 6232 "USDCAD" D1 timeseries created successfully: - Timeseries "USDCAD" D1: Requested: 1000, Actual: 1000, Created: 1000, On the server: 5003 "USDCAD" W1 timeseries created successfully: - Timeseries "USDCAD" W1: Requested: 1000, Actual: 1000, Created: 1000, On the server: 1403 "USDCAD" MN1 timeseries created successfully: - Timeseries "USDCAD" MN1: Requested: 1000, Actual: 323, Created: 323, On the server: 323 New bar on USDCAD M1: 2020.03.19 12:18 New bar on USDCAD M1: 2020.03.19 12:19 New bar on USDCAD M1: 2020.03.19 12:20 New bar on USDCAD M5: 2020.03.19 12:20
Aqui vemos que ao inicializar a biblioteca todas as séries temporais solicitadas foram criadas, mas não foram preenchidas com dados devido à sua ausência. No primeiro acesso aos dados solicitados, o carregamento de dados foi iniciado pelo terminal. E após a chegada de cada tick, obtivemos o seguinte objeto-série temporal vazio, sincronizamos seus dados com o servidor e preenchemos o objeto-série temporal com os dados das barras na quantidade solicitada. Somente 323 barras estão realmente disponíveis em MN1, uma vez que todas foram adicionadas à lista-série temporal.
Agora execute o indicador no modo visual do testador com as mesmas configurações:
O testador carrega o histórico necessário para todos os períodos usados, a biblioteca relata a criação de todas as séries temporais, exceto a atual, e, na primeira entrada em OnCalculate(), as séries temporais para o símbolo e o período atuais são recriadas com êxito e, após limpar o testador, vemos como no testador são processados os eventos "Nova Barra" das séries temporais usadas.
Tudo funciona como esperado.
O que vem agora?
No próximo artigo, continuaremos trabalhando com séries temporais em indicadores, e testaremos o uso de séries temporais criadas para exibir informações no gráfico.
Abaixo estão anexados todos os arquivos da versão atual da biblioteca e os arquivos do EA de teste. Você pode baixá-los e testar tudo sozinho.
Se você tiver perguntas, comentários e sugestões, poderá expressá-los nos comentários do artigo.
Artigos desta série:
Trabalhando com séries temporais na biblioteca DoEasy (Parte 35): Objeto "Barra" e lista-série temporal do símbolo
Trabalhando com séries temporais na biblioteca DoEasy (Parte 36): objeto das séries temporais de todos os períodos usados do símbolo
Trabalhando com séries temporais na biblioteca DoEasy (Parte 37): coleção de séries temporais - banco de dados de séries temporais para símbolos e períodos
Trabalhando com séries temporais na biblioteca DoEasy (Parte 38): coleção de séries temporais - atualização em tempo real e acesso aos dados do programa
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/7724
- 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