
Trabajando con las series temporales en la biblioteca DoEasy (Parte 39): Indicadores basados en la biblioteca - Preparación de datos y eventos de la series temporales
Contenido
- Concepto
- Mejorando las clases para trabajar con indicadores, creando los eventos de series temporales
- Simulando el funcionamiento de las series temporales y sus eventos en los indicadores
- ¿Qué es lo próximo?
Concepto
Todo lo que hemos hecho hasta el día de hoy, estaba relacionado con asesores y scripts, y no se relacionaba de forma alguna con los indicadores. Sin embargo, las series temporales se pueden utilizar de forma muy activa como fuente de datos para diferentes cálculos en los indicadores, por lo que, en nuestra opinión, ha llegado el momento de que la biblioteca y los indicadores comiencen a interactuar.
A diferencia de los asesores, los indicadores tienen una estructura completamente distinta; cada indicador se ejecuta en un único flujo del símbolo en el que está iniciado. Esto significa que, si iniciamos diferentes indicadores en varios gráficos del mismo instrumento, todos los indicadores se ejecutarán en el mismo flujo del símbolo al que pertenecen todos estos gráficos.
Por consiguiente, si uno de los indicadores tiene una arquitectura mal diseñada y se ralentiza, frenará el flujo completo del símbolo. En este caso, todos los demás indicadores, creados correctamente, pero funcionando en el mismo flujo que el indicador "pesado", se verán obligados a esperar a que se desbloquee el indicador que frena el flujo.
Para evitar retrasos al solicitar los datos históricos mientras trabajamos con los indicadores en el terminal, hemos diseñado un suministro secuencial de los datos solicitados: varias funciones capaces de activar la carga de datos históricos y retornar de inmediato el resultado del funcionamiento de la función, sin esperas.
Al solicitar los datos de cualquier serie temporal de cualquier símbolo con las funciones Copy, en el indicador y el asesor se dará un comportamiento distinto cuando el terminal suministre los datos históricos:
Al solicitar los datos desde el indicador, si las series temporales solicitadas aún no han sido construidas o deben ser cargadas desde el servidor, la función retornará de inmediato -1, pero, en este caso, el propio proceso de carga/construcción será iniciado.
Al solicitar los datos desde un experto o script, se inicializará la carga desde el servidor, si el terminal no dispone de estos datos a nivel local; o bien comenzará la construcción de la serie temporal necesaria, si los datos se pueden construir a partir de la historia local, pero aún no están preparados. La función retornará la cantidad de datos que estén preparados al momento de finalización del timeout, pero la carga de la historia continuará, y con la siguiente solicitud análoga, la función retornará ya más datos.
De esta forma, podemos ver que, al solicitar los datos desde el asesor, el propio terminal comienza la carga de los datos (si los datos solicitados aún no existen a nivel local, o no son suficientes), y al finalizar un cierto tiempo (timeout), la función retorna la cantidad de historia ya existente en el momento en que finaliza la espera de la carga de la historia, es decir, el terminal intenta de inmediato darnos la historia solicitada, y si no resulta suficiente a nivel local, tratará de cargarla en la cantidad necesaria.
En este momento, nuestro programa espera la carga de los datos.
En los indicadores, esperar resulta algo categóricamente inasumible, por eso, el terminal nos entrega lo que hay (o nos comunica que no hay nada en absoluto), y si no hay historia a nivel local, o no es suficiente al solicitar los datos por primera vez, comenzará su carga. En este caso, además, no se espera la carga de los datos que faltan hasta que se dé el timeout: nos informan de que no hay nada, y luego salimos.
En esta situación, nuestro programa deberá salir por sí mismo de su parte computacional hasta el siguiente tick. Al darse el siguiente inicio del manejador OnCalculate() del indicador en el nuevo tick, los datos ya podrán estar parcial o totalmente cargados y disponibles para los cálculos. Aquí, deberemos decidir por nosotros mismos cuántos datos nos bastarán para que el algoritmo del programa funcione sin problemas.
Y además, el indicador no debe intentar cargar sus propios datos, es decir, los datos del símbolo y periodo en los que está iniciado. En caso contrario, dicha solicitud puede provocar un clinch. De la carga de estos datos para los indicadores se encarga el subsistema del terminal, que nos proporciona todos los datos sobre el número y el estado en las variablesrates_total y prev_calculated del manejador OnCalculate().
Partiendo precisamente de estas exigencias mínimas, tenemos que corregir ciertas clases para trabajar con las series temporales y organizar correctamente la carga primaria de los datos necesarios para realizar los cálculos.
En esta ocasión, nos encargaremos de corregir las clases creadas, organizando correctamente la carga primaria de datos de todas las series temporales utilizadas en nuestros programas, y enviando al gráfico del programa de control cualquier evento de todas las series temporales utilizadas durante su actualización online.
Mejorando las clases para trabajar con indicadores, creando los eventos de series temporales
Para comenzar, cargaremos en el archivo Datas.mqh los nuevos mensajes de la biblioteca, es decir, los índices de los mensajes:
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 ... }; //+------------------------------------------------------------------+
y los textos de los mensajes que se corresponden con los índices nuevamente añadidos:
{"Не удалось подготовить массив используемых символов. Ошибка ","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 ..."}, }; //+---------------------------------------------------------------------+
En el constructor de la clase del objeto básico de todos los objetos de la biblioteca CBaseObj, en el archivo \MQL5\Include\DoEasy\Objects\BaseObj.mqh, hemos modificado la inicialización de la variable m_available: justo al crearse, todos los objetos herederos de la clase básica CBaseObj tendrán la propiedad sobre su accesibilidad para trabajar con ellos en el programa en el estado "se utiliza" (true). Antes, el valor se establecía al realizarse la inicialización en el estado "no se utiliza" 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) {} }; //+------------------------------------------------------------------+
También se ha cambiado el nombre del método encargado de establecer la bandera que indica que se ha registrado un evento en el objeto, en la clase del objeto básico expandido de todos los objetos de la biblioteca CBaseObjExt, en el archivo \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; }
Antes, el método tenía el nombre SetEvent(), lo cual provocaba ciertas interferencias al desarrollar nuevos objetos, ya que SetEvent puede indicar la creación, el establecimiento, el envío, etcétera, precisamente de un evento, y no el establecimiento de una bandera de señal sobre la presencia de un evento.
Por consiguiente, en los archivos de las clases en las que se utilizaba este método, también hemos introducido algunos cambios: hemos sustituido la llamada del método SetEvent() por la llamada de SetEventFlag(). No tiene sentido describir aquí este punto, para aclaraciones adicionales, el lector podrá recurrir a los archivos adjuntos al artículo.
Dado que las funciones comerciales están prohibidas en los indicadores, introduciremos los cambios en las clases de los objetos comerciales.
En la clase del objeto comercial multiplataforma, en el archivo \MQL5\Include\DoEasy\Objects\Trade\TradeObj.mqh, añadimos al inicio de todos los métodos comerciales la comprobación del tipo de programa, y si se trata de un indicador o servicio, salimos del 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();
Exactamente de la misma forma, hemos introducido los cambios en todos los métodos comerciales homónimos de la clase comercial principal de la biblioteca
en el archivo \MQL5\Include\DoEasy\Trading.mqh.
Esta salida de los métodos comerciales, en primer lugar, no nos dejará llamar las funciones comerciales en los programas donde han sido finalizadas, y, en segundo lugar, retornará el éxito de la ejecución del método, lo que no nos permitirá iniciar el procesamiento de los errores de la biblioteca.
Ahora, vamos a analizar los cambios relacionados directamente con las clases de los objetos de las series temporales.
En la clase del objeto de barra, hemos modificado ligeramente los textos mostrados desde los constructores de la clase al obtener erróneamente los datos históricos al crear el objeto de barra. Asimismo, hemos añadido al texto mostrado el número del constructor, el símbolo y el marco temporal de la serie temporal para el que se crea el objeto de barra.
En el constructor de la primera forma, la comprobación de errores y el registro de la hora en la estructura de tiempo han sido desplazados a diferentes bloques:
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
Estas acciones nos proporcionan más datos al darse el error de creación del objeto de barra.
Dado que para solicitar los datos sobre el número de barras y sus valores en el símbolo de periodo actual necesitamos usar las matrices de series temporales proporcionadas por el manejador OnCalculate(), deberemos transmitir de alguna manera estas matrices y valores a las clases de la biblioteca.
Para ello, vamos a crear una estructura en el archivo \MQL5\Include\DoEasy\Defines.mqh, en el que se guardarán todas las variables que utilizaremos para transmitir a las series temporales de la biblioteca todos los datos necesarios calculados para la serie temporal actual:
//+------------------------------------------------------------------+ //| 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 podemos ver, la estructura contiene todos los campos necesarios para transmitir los datos a la biblioteca en cualquier implementación del manejador OnCalculate() del indicador.
Para la primera forma del manejador
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 );
se usan las variables de estructura rates_total, prev_calculated, begin y price.
Para la segunda forma del manejador
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 );
se usan las variables de estructura rates_total, prev_calculated y la estructura MqlRates rates para guardar los valores de las matrices.
Esta implementación de la estructura resulta conveniente para transmitir a la biblioteca los valores de solo una barra.
En la clase CSeries, en el archivo \MQL5\Include\DoEasy\Objects\Series\Series.mqh, añadimos a los métodos para establecer el símbolo y el marco temporal la bandera de establecimiento de las fechas del 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);
La bandera está quitada por defecto, lo cual, al llamar al método, impide establecer las fechas del servidor, ya que, para llamar al método que establece las fechas del servidor, primero se comprueba el estado de esta bandera:
//+------------------------------------------------------------------+ //| 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(); } //+------------------------------------------------------------------+
Se ha hecho de esta forma para no establecer varias veces las fechas del servidor al llamar al método de establecimiento simultáneo del símbolo y el marco temporal:
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
Aquí, primero llamamos al método de establecimiento del símbolo (la bandera está quitada); acto seguido, llamamos al método que especifica el marco temporal con la bandera establecida para llamar al método que establece las fechas del servidor desde el método de establecimiento del marco temporal.
Ahora, al método de actualización de los datos de la serie temporal se transmite la nueva estructura de datos del manejador OnCalculate(), en lugar de la lista completa de sus matrices:
//--- (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);
Por consiguiente, en la implementación del método Refresh(), ahora viene, no el recurso a las matrices, sino a los datos de esta estructura:
//+------------------------------------------------------------------+ //| 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 que sea posible realizar búsquedas en la lista de los objetos de series temporales según su marco temporal, ahora hemos implementado un método virtual para comparar dos objetos de series temporales:
//--- 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); }; //+------------------------------------------------------------------+
El método compara la propiedad "marco temporal" en los dos objetos de series temporales comparados (el actual y el transmitido al método), y si son iguales, retorna cero.
Ya hemos analizado muchas veces la lógica de funcionamiento de semejantes métodos para la búsqueda y clasificación de diferentes objetos herederos del objeto básico de la biblioteca estándar CObject. El método es definido como virtual en el objeto básico de la biblioteca estándar, por eso, su implementación deberá ejecutarse en los objetos herederos, y el método deberá retornar cero en caso de igualdad, o bien 1/-1 , si el valor de la propiedad comprobada es mayor/menor que valor de esta propiedad en el objeto comparado.
Dado que la primera invocación de las funciones que retornan los datos históricos activa la carga de datos en caso de que estos no existan (o sean insuficientes) a nivel local, vamos a añadir al propio inicio del método que establece la cantidad de datos requeridos (y este se llama antes de crear el objeto de serie temporal) la invocación de los datos históricos necesarios (simplemente solicitamos la fecha de la barra actual). Esto iniciará la carga de los datos necesarios (en el caso de que no se encuentren a nivel local):
//+------------------------------------------------------------------+ //| 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
Cuando creamos el objeto que guarda las listas de todas las series temporales de un símbolo (la clase CTimeSeries), hicimos que este objeto siempre disponga de la lista en la que está registrado el conjunto completo de todos los marcos temporales posibles en el terminal. Las listas de series temporales ya se van añadiendo de inmediato a esta lista, pero, en este caso, no son creadas, se crean a medida que sean necesarias. La invocación de la lista de serie temporal necesaria la hemos implementado según el índice modificado que se corresponde con la posición del índice del marco temporal de la lista en la enumeración ENUM_TIMEFRAMES con un desplazamiento de 1 (se ha descrito en el artículo).
Lo hemos hecho de esta forma para acelerar el acceso al puntero al objeto de serie temporal necesario en la lista. Pero, como resultó finalmente, junto con el acceso inmediato al puntero, también surgió un problema al trabajar con el simulador: el simulador visual creaba los gráficos de absolutamente todos los marcos temporales, independientemente de si estos se usaban realmente en el programa, y de si se creaban o no sus series temporales.
Asimismo, topamos con otro problema al cambiar el periodo del gráfico mientras funcionaba el programa: las listas creadas anteriormente no se creaban de nuevo, y el programa continuaba monitoreando los eventos de objetos que ya no existían, sustituyéndolos con otros.
En general, para evitar que surjan errores ocultos y no tener que buscar largamente las causas de estos, hemos llegado a la conclusión de que será más conveniente guardar en el objeto de la clase CTimeSeries (que guarda las listas de serie temporal de todos los marcos temporales) los punteros a las series temporales realmente creadas y utilizadas, es decir, los punteros a cada serie temporal de cada periodo del gráfico se guardarán en la lista solo en el caso de que se indique explícitamemnte en el programa que su uso es necesario, y que este objeto de serie temporal se haya creado realmente.
Abrimos el archivo \MQL5\Include\DoEasy\Objects\Series\TimeSeries.mqh e introducimos en el mismo las mejoras pertinentes.
Ahora, la clase de todas las series temporales de un mismo símbolo se heredará de la clase del objeto básico ampliado de todos los objetos de la biblioteca.
Esto se ha implementado así para poder utilizar la funcionalidad de eventos de la clase CBaseObjExt:
//+------------------------------------------------------------------+ //| Symbol timeseries class | //+------------------------------------------------------------------+ class CTimeSeries : public CBaseObjExt {
El método que retorna el índice de una serie temporal en la lista según la denominación del marco temporal ahora se declara simplemente en la sección privada de la clase:
//+------------------------------------------------------------------+ 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:
La implementación de este método ahora se ha ejecutado fuera del cuerpo de la clase:
//+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+
Transmitimos al método el marco temporal con la serie temporal a la que indica el puntero que debemos retornar.
A continuación, creamos un objeto de serie temporal transitorio con el marco temporal buscado.
Asignamos a la lista de objetos de series temporales la bandera de lista clasificada y
obtenemos el índice del objeto de serie temporal cuyo marco temporal es igual al marco temporal del objeto transitorio.
Si este objeto existe en la lista, obtendremos su índice, de lo contrario, obtendremos WRONG_VALUE (-1).
Eliminamos el objeto transitorio y retornamos el índice obtenido.
En lugar de los métodos Create() y CreateAll(), declaramos los métodos para añadir una serie temporal a la lista y el método para crear el objeto de serie temporal indicado.
En cuanto a los objetos para actualizar las listas de series temporales, ahora obtienen la estructura de los valores de los parámetros y matrices OnCalculate(), en lugar de la lista completa de matrices:
//--- (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); }; //+------------------------------------------------------------------+
Eliminamos del constructor de la clase el ciclo para crear las listas de series temporales:
//+------------------------------------------------------------------+ //| 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(); } //+------------------------------------------------------------------+
Ahora, las listas temporales necesarias se crearán una vez creada la matriz de series temporales utilizadas en el manejador OnInit() del programa. Cualquier cambio en el número de periodos de los gráficos utilizados en el programa, causará la desinicialización del asesor o la nueva creación del indicador, lo que provocará que se cree de nuevo y completamente la lista de los objetos de series temporales utilizados, y, en lo sucesivo, que estos se tengan en cuenta correctamente.
En los métodos para establecer la profundidad de la historia de todas las series temporales utilizadas SetRequiredAllUsedData() y para retornar la bandera de sincronización de todas las series temporales utilizadas SyncAllData(), sustituimos el ciclo por el número completo de todos los marcos temporales posibles
//+------------------------------------------------------------------+ //| 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 un ciclo por el número de objetos de series temporales reales en la lista:
int total=this.m_list_series.Total(); for(int i=0;i<total;i++)
Todo está claro: ahora, la lista consta solo de los objetos de series temporales realmente creados, y los ciclos se realizan sobre su cantidad real.
Implementación del método para añadir a la lista el objeto de serie temporal indicado:
//+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+
Transmitimos al método el periodo del gráfico de la serie temporal que debemos añadir a la lista de series temporales del símbolo.
Creamos un objeto de serie temporal con el marco temporal cuyo valor se ha transmitido al método.
Asignamos a la lista de series temporales la bandera de lista clasificada y buscamos en la lista un objeto de serie temporal igual al que acabamos de crear.
Si en la lista aún no existe un objeto así (la búsqueda ha retornado -1), añadimos a la lista el objeto de serie temporal creado.
De lo contrario, eliminamos el objeto creado: dicho objeto de serie temporal ya existe en la lista.
Ya que estamos creando una serie temporal, significa que esta es necesaria, así que colocamos su bandera de uso en el programa y
retornamos el resultado de la adición de la serie temporal a la lista.
Si se ha añadido con éxito, se retornará el valor true, de lo contrario, false.
En la biblioteca, para añadir los eventos que suceden con sus diferentes objetos, se ha creado una funcionalidad de eventos en el objeto expandido de todos los objetos de la biblioteca. Ya analizamos sus principios y lógica de trabajo con los eventos de la biblioteca en el artículo 16, y continuamos su desarrollo en el artículo 17.
En resumen: cada objeto heredado del objeto básico de la biblioteca CBaseObj (y ahora, de CBaseObjExt), tiene una lista en la que se registran todos los eventos que pueden suceder con el objeto durante un ciclo de trabajo del programa, tanto en un tick, como en una iteración del temporizador.
Al identificar cualquier evento en el objeto, se le asigna la bandera de evento sucedido. A continuación, en las clases de colección, se analizan las listas de objetos de colección, y en ellas, a su vez, se comprueban estas banderas. Si se ha encontrado un objeto con la banadera de evento alzada, la clase colección de estos objetos obtendrá la lista con todos los eventos del objeto con la bandera de evento alzada, y enviará todos los eventos de esta lista al gráfico del programa de control.
En el propio programa, se ha creado la funcionalidad para procesar todos los eventos entrantes. En este caso, además, todos los eventos se procesan por ticks, mientras que en el simulador se hace en el manejador OnChartEvent().
En la clase del objeto de todas las series temporales de un símbolo CTimeSeries, que ya hemos analizado, el lugar adecuado para determinar los eventos de todas sus listas de series temporales será el método de actualización de la serie temporal indicada Refresh(), y el método para actualizar todas las series temporales del símbolo RefreshAll().
Vamos a echar un vistazo a la implementación de los métodos de actualización de las listas de series temporales.
//+------------------------------------------------------------------+ //| 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(); } //+------------------------------------------------------------------+
Aquí, hemos comentado cada línea de código de los métodos, por lo que es de esperar que no haya dudas al respecto. En cualquier caso, el lector podrá escribir cualquier duda en los comentarios al artículo.
Bien, ya hemos terminado con la clase del objeto de todas las series temporales de un símbolo CTimeSeries.
La siguiente clase será la clase de colección de objetos de las series temporales de los símbolos CTimeSeriesCollection, que deberá estar igualmente equipada con funcionalidad de eventos, ya que sobre ella recae la "responsabilidad" de obtener las listas con los eventos de todos los objetos que guardan todas las series temporales del símbolo utilizado en el programa.
Abrimos el archivo \MQL5\Include\DoEasy\Collections\TimeSeriesCollection.mqh y lo heredamos de la clase básica expandida de todos los objetos de la biblioteca:
//+------------------------------------------------------------------+ //| Symbol timeseries collection | //+------------------------------------------------------------------+ class CTimeSeriesCollection : public CBaseObjExt {
En la sección pública de la clase, declaramos dos métodos para retornar el objeto de todas las series temporales del símbolo indicado, y para retornar el objeto de serie temporal del símbolo y el periodo indicados:
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);
Los implementamos de inmediato fuera del cuerpo de la clase.
Método para retornar el objeto de series temporales del símbolo indicado:
//+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+
Aquí, obtenemos el índice del objeto de series temporales según la denominación del símbolo con el método IndexTimeSeries(), que ya analizamos antes en la parte 37 de la descripción de la biblioteca. Según el índice conseguido, obtenemos el objeto de series temporales de la lista. Si no se ha logrado obtener un índice u objeto de la lista, retornaremos el valor NULL, de lo contrario, retornaremos el puntero al objeto solicitado de la lista.
Método para retornar el objeto de series temporales del símbolo/periodo indicado:
//+------------------------------------------------------------------+ //| 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; } //+-----------------------------------------------------------------------+
Aquí, obtenemos el objeto de series temporales con la ayuda del método GetTimeseries() (analizado anteriormente) según el símbolo transmitido al método.
Del objeto de series temporales, obtenemos la lista de series temporales según el marco temporal indicado y retornamos el puntero al objeto de serie temporal obtenido.
El método GetSeries() del objeto de series temporales usa para el retorno de la serie temporal obtenida el método IndexTimeframe(), que hemos analizado anteriormente, mientras que el método GetSeries() del objeto de series temporales CTimeSeries tiene el aspecto siguiente:
CSeries *GetSeries(const ENUM_TIMEFRAMES timeframe) { return this.m_list_series.At(this.IndexTimeframe(timeframe)); }
Eliminamos de la sección pública de la clase los tres métodos encargados de crear las series temporales, dejando solo uno para crear la serie temporal indicada del símbolo indicado:
//--- 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
Los tres métodos remotos por ahora resultarán sobrantes, así que en lugar de ellos, declaramos tres nuevos métodos: para crear de nuevo la serie temporal indicada, para retornar una serie temporal vacía y para retornar una serie temporal no rellenada por completo:
//--- (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 qué tenemos que crear una serie temporal de nuevo? Al inicializar la biblioteca y crear todas las series temporales utilizadas de todos los símbolos, invocamos las funciones que inician la descarga de los datos históricos. Pero, como ya hemos dicho más de una vez, si el programa es un indicador, e invoca el símbolo o marco temporal en el que está iniciado, podríamos obtener un clinch. Por eso, estas situaciones se omiten; tras finalizar la inicialización y entrar en el manejador OnCalculate(), en primer lugar, deberemos mirar las series temporales creadas, obtener la vacía (omitida durante la optimización) y crearla de nuevo usando los datos de las variables rates_total en OnCalculate().
Ahora, transmitimos a los métodos de actualización de las series temporales no los datos de las matrices de las series temporales de OnCalculate(), sino la estructura de estos datos; luego, declaramos el método para obtener los eventos del objeto de series temporales y añadir estos a la lista de eventos de todos los objetos de la colección de series temporales:
//--- 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(); }; //+------------------------------------------------------------------+
Implementación de los métodos que retornan una serie temporal vacía y una no rellenada por completo:
//+------------------------------------------------------------------+ //|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; } //+------------------------------------------------------------------+
Hemos comentado cada línea de los métodos: los métodos son comletamente idénticos, salvo por la comprobación de la serie temporal vacía y la no rellenada por completo.
Los métodos retornan la primera serie temporal encontrada que cumpla con las condiciones de búsqueda. Lo hemos implementado así a propósito, para obtener en cada tick (cada entrada en OnCalculate) de forma secuencial todas las series temporales vacías o no rellenadas por completo, siguiendo las recomendaciones de MetaQuotes sobre el correcto procesamiento de la falta de datos en los indicadores, es decir, debemos salir del manejador y comprobar la presencia de datos en el tick siguiente.
Implementación del método para crear la serie temporal indicada del símbolo indicado:
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
El método añade datos al objeto de series temporales de un símbolo, la serie temporal con el periodo indicado del gráfico.
Transmitimos al método el símbolo y el periodo de la serie temporal necesaria.
Obtenemos el objeto de series temporales y añadimos al mismo la nueva serie temoral del periodo indicado del gráfico.
Solicitamos los datos del símbolo/periodo y establecemos el número necesario de datos en la serie temporal.
Si hemos realizado todas las acciones anteriores con éxito, retornamos el resultado de la creación de la nueva serie temoral y la adición de datos a la misma.
Todos estos métodos han sido analizados en artículos anteriores, aquí, simplemente hemos implementado una lógica de creación de la serie temporal del símbolo/periodo distinta a la descrita en el artículo 37.
Implementación del método para crear nuevamente la serie temporal indicada del símbolo indicado:
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
Aquí todo es exactamente igual, salvo por una diferencia: la serie temporal ya ha sido creada, por eso, omitimos el paso de la adición de la nueva serie temporal al objeto de todas las series temporales del símbolo.
Implementación del método que obtiene los eventos de un objeto de serie temporal y los añade a la lista de eventos de la colección de serie temporales:
//+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+
Al método se transmite el puntero al objeto de series temporales del símbolo, y en un ciclo por la lista de eventos de este objeto, todos sus eventos son añadidos a la lista de eventos de la colección de series temporales.
Implementación del método que actualiza la serie temporal indicada del símbolo indicado y añade sus eventos a la lista de eventos de la colección de series temporales:
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
Implementación del método que actualiza todas las series temporales de todos los símbolos y añade sus eventos a la lista de eventos de la colección de series temporales:
//+------------------------------------------------------------------+ //| 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 estos métodos se han comentado ampliamente, por lo que no suscitarán dudas sobre sus principios lógicos.
Ya hemos finalizado la mejora de todas las clases de series temporales en la etapa presente.
Ahora, vamos a mejorar el objeto principal de la biblioteca CEngine (\MQL5\Include\DoEasy\Engine.mqh) para trabajar con la colección de series temporales desde los programas.
En la sección privada de la clase, declaramos el objeto de 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
A la sección pública, entre tanto, le añadimos el método que retorna la bandera de presencia de un evento en la colección de series temporales:
//--- 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(); }
El método retorna el resultado del funcionamiento del método IsEvent() del objeto de colección de series temporales.
Puesto que ahora tenemos que enviar a los métodos de actualización de las series temporales los datos de las matrices desde el manejador OnCalculate() del indicador para procesar los datos de la serie temporal, vamos añadir a los métodos de procesamiento de eventos Timer y Tick la transmisión de la estructura de los datos de las matrices OnCalculate(), y ya de paso, también declararemos el método de procesamiento del 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);
Allí mismo, en la sección pública de la clase, añadimos el método que retorna la lista de eventos de las series temporales:
//--- 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(); }
El método retorna el puntero a la lista de eventos de la colección de series temporales con la ayuda del método de colección de series temporales GetListEvents()
En la sección pública de la clase, tenemos cuatro métodos para crear diferentes series temporales: Eliminamos tres de ellos, que no necesitamos por el momento:
//--- 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); }
y en lugar de ellos, añadimos la declaración del método para crear todas las series temporales de todos los símbolos de la colección utilizados, escribimos el método para crear nuevamente la serie temporal indicada y declaramos el método para solicitar la sincronización de las series temporales con el 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);
En el mismo lugar, tenemos cuatro métodos para actualizar la colección de las series temporales.
Dejamos solo dos de ellos: el método para actualizar la serie temporal indicada, y el método para actualizar todas las series temporales de la colección:
//--- 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); }
Ahora, en lugar de los valores de las matrices OnCalculate(), transmitimos a los métodos la estructura con los datos de las variables y las matrices OnCalculate().
Y añadimos cuatro métodos más: para retornar el puntero al objeto de series temporales del símbolo indicado, y al objeto de serie temporal indicado, y también los métodos que retornan los punteros a las series temporales vacías y no rellenadas por completo:
//--- 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(); }
Los métodos devuelven el resultado del retorno de los métodos homónimos de la colección de series temporales que hemos analizado anteriormente.
El método TradingOnInit(), que transmite a la clase comercial los punteros a todas las colecciones, ha sido renombrado como CollectionOnInit(), dado que el nombre ya no le conviene: en él realizaremos las inicializaciones necesarias de todas las clases de colecciones.
Al final del cuerpo de la clase, añadimos un bloque con los métodos para trabajar con el objeto de 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();
La clase "Pausa" la analizamos en el artículo 30, y ha sido diseñada para organizar las pausas en lugar de la función Sleep(), que no funciona en los indicadores.
Aquí, junto con los métodos de la clase CPause, llamados desde estos métodos (los analizamos anteriormente), hemos añadido otro método, Pause(), que permite directamente iniciar una nueva espera de pausa sin inicializar previamente sus parámetros: todos los parámetros se transmiten al método, y dentro del método se organiza un ciclo para esperar la finalización del número de milisegundos de la pausa; dicho número se transmite al método con un parámetro de entrada. Estos métodos nos serán necesarios en los programas para organizar las pausas en los indicadores.
No debemos olvidar que este objeto de pausa retiene exactamente de la misma forma el flujo principal en el que está iniciado el indicador, al igual que la función Sleep(),
por lo que tendremos que aplicar esta pausa en los indicadores solo cuando esté justificada.
El temporizador de la clase CEngine ha sido reorganizado: antes, para cada manejador de cada colección se comprobaba dónde estábamos trabajando, en el simulador, o fuera del mismo. Esto nos obligaba a realizar dichas comprobaciones para cada manejador en todas las colecciones, lo cual resultaba ya irracional.
Ahora, en primer lugar, se comprueba dónde trabajamos (fuera del simulador o en el mismo), y ya después, dentro de los bloques, ya sea fuera del simulador o en él, se realiza el procesamiento de todas las colecciones:
//+------------------------------------------------------------------+ //| 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); } } //+------------------------------------------------------------------+
El manejador ahora es más compacto. Asimismo, su lógica resulta más comprensible y está libre de comprobaciones repetitivas e innecesarias.
Método que sincroniza los datos de una serie temporal vacía con el servidor, creando después una serie temporal vacía:
//+------------------------------------------------------------------+ //| 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()); } } //+------------------------------------------------------------------+
Bien. Este método representa la piedra angular en cuanto a la carga correcta de los datos históricos de cualquier serie temporal utilizada, así como de cualquier símbolo y periodo de los gráficos.
El método obtiene de la colección de series temporales la primera serie temporal encontrada que no esté rellenada por completo: esto significará que para ella no había datos en el tick anterior, por lo que se realizará de inmediato un intento de sincronizar los datos de esta serie temporal con los datos en el servidor. Si no se ha logrado, salimos del método hasta el próximo tick. Si los datos han sido sincronizados, esta serie temporal se creará nuevamente, siendo rellenada con todas las barras disponibles de la historia (pero nunca más del número solicitado).
Así, en cada tick obtenemos la siguiente serie temporal vacía, que sincronizamos y creamos nuevamente hasta que no queden más series temporales sin rellenar por completo.
Implementación de los manejadores de eventos NewTick y 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; } //+------------------------------------------------------------------+
En ambos métodos, se llama al método para crear nuevamente las series temporales vacías.
Los propios métodos deberán llamarse desde los manejadores homónimos del programa que funciona usando como base esta biblioteca.
Implementación del método para crear todas las series temporales utilizadas de todos los símbolos utilizados:
//+------------------------------------------------------------------+ //| 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; } //+------------------------------------------------------------------+
El método debe llamarse al inicializar el programa tras crear la lista de todos los símbolos utilizados.
Transmitimos al método la matriz creada en la inicialización con los nombres de los periodos utilizados de los gráficos y los parámetros para crear las series temporales: el número de barras de la serie temporal (solo para los indicadores, rates_total) y la profundidad de la historia necesaria para las series temporales creadas (por defecto, 1000, pero no superior al valor Bars() del símbolo, para los indicadores, no superior a rates_total).
Estas son todas las mejoras necesarias por hoy en cuanto al trabajo con las series temporales.
Simulando el funcionamiento de las series temporales y sus eventos en los indicadores
Para poner a prueba el funcionamiento de la clase de colección de la series temporales en los indicadores, vamos a crear en el directorio de los indicadores del terminal la nueva carpeta
\MQL5\Indicators\TestDoEasy\, y en ella, la nueva subcarpeta Part39\, en la que crearemos un nuevo indicador con el nombre TestDoEasyPart39.mq5.
El número y el tipo de los búferes de indicador dibujados no tiene por ahora importancia para nosotros: no vamos a dibujar nada en él. Pero, en el futuro, hemos establecido dos búferes a dibujar con el tipo de dibujado DRAW_LINE.
Los parámetros de entrada necesarios para establecer los símbolos y marcos temporales necesarios, además de algunos otros,
los hemos trasladado desde el asesor de prueba del artículo anterior. Hemos obtenido lo siguiente:
//+------------------------------------------------------------------+ //| TestDoEasyPart39.mq5 | //| Copyright 2020, MetaQuotes Software Corp. | //| https://mql5.com/es/users/artmedia70 | //+------------------------------------------------------------------+ #property copyright "Copyright 2020, MetaQuotes Software Corp." #property link "https://mql5.com/es/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 //+------------------------------------------------------------------+
En el manejador OnInit() del indicador, implementamos el establecimiento de las variables globales del indicador y la llamada de la función de inicialización de la 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); } //+------------------------------------------------------------------+
El manejador OnDeinit() del indicador lo tomaremos del asesor de prueba del artículo anterior:
//+------------------------------------------------------------------+ //| Custom indicator deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Remove indicator graphical objects by an object name prefix ObjectsDeleteAll(0,prefix); Comment(""); } //+------------------------------------------------------------------+
Los manejadores OnTimer() y OnChartEvent() también los tomaremos del asesor:
//+------------------------------------------------------------------+ //| 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 rellenar la estructura de datos de las matrices y variables de la primera y la segunda forma de OnCalculate() del indicador, crearemos dos funciones:
//+------------------------------------------------------------------+ //| 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); } //+------------------------------------------------------------------+
La función de procesamiento de eventos de la biblioteca DoEasy también la trasladaremos desde el asesor de prueba:
//+------------------------------------------------------------------+ //| 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()); } } } //+------------------------------------------------------------------+
La función para trabajar con los eventos de la biblioteca en el simulador, la tomamos igualmente del asesor:
//+------------------------------------------------------------------+ //| 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); } } } } //+------------------------------------------------------------------+
No sería necesario trasladar la función del asesor para trabajar con los botones del panel comercial, pero, en el futuro, para que resulte posible utilizar ciertos botones en el indicador, trasladaremos desde el asesor estas funciones con algunos pequeños cambios (se presuponen dos botones):
//+------------------------------------------------------------------+ //| 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 vemos, la mayoría de las funciones del asesor se utiliza en los indicadores sin ningún problema ni modificación por nuestra parte. Esto nos hace pensar que deberemos trasladar todas las funciones necesarias para trabajar con la biblioteca desde los asesores e indicadores a un archivo de inclusión de la biblioteca, y ya utilizarlas desde allí. Pero eso lo haremos ya más tarde. Ahora, tenemos que crear el manejador OnCalculate() del indicador.
El manejador constará de un bloque de código obligatorio para preparar los datos de la biblioteca y de un bloque de código no obligatorio (hoy) para trabajar con el 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 vemos, todo lo relacionado con el funcionamiento de la biblioteca ha encajado en un pequeño bloque de código en el manejador OnCalculate(), y en esencia, la única diferencia aquí con el asesor reside en que rellenamos la estructura de precios de los datos actuales de las matrices desde OnCalculate() con la función CopyData(). Todo lo demás es absolutamente idéntico al funcionamiento en el asesor: la biblioteca trabaja en el temporizador si el indicador ha sido iniciado en el gráfico del símbolo, y en OnCalculate(), según los ticks, si el indicador ha sido iniciado en el simulador.
Los búferes de indicador en la parte computacional de OnCalculate() simplemente los rellenamos con los datos de las matrices high[] y low[].
En los archivos adjuntos al final del artículo, el lector podrá ver el código completo del indicador.
Compilamos el indicador y lo iniciamos en el gráfico de un símbolo con el que no hemos trabajado durante mucho tiempo, estableciendo previamente en los ajustes el trabajo con el símbolo actual; luego, seleccionamos el trabajo con la lista de marcos temporales establecida. El inicio en símbolos que no hemos utilizado durante mucho tiempo, obligará al indicador a cargar los datos que le faltan y comunicarlo en el diario y en el gráfico:
Aquí, podemos ver cómo una nueva serie temporal vacía se sincronizaba y creaba con cada tick. En el diario, en este caso, se mostraban las siguientes entradas:
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
Aquí vemos que, al inicializar la biblioteca, se crearon todas las series temporales solicitadas, si bien estas no se rellenaron con datos, debido a que no había. Al invocar por primera vez los datos solicitados, se inicializó la carga de datos por parte del terminal. Y con la llegada de cada tick siguiente, obteníamos un nuevo objeto de serie temporal vacío, cuyos datos sincronizábamos con el servidor. Luego rellenábamos el objeto de serie temporal con los datos de las barras en la cantidad solicitada. En MN1, hay verdaderamente disponibles un total de 323 barras, que han sido añadidas a la lista de serie temporal.
Ahora, vamos a iniciar el indicador en el modo visual del simulador con los mismos ajustes:
El simulador carga la historia necesaria de todos los marcos temporales utilizados, después de lo cual, la biblioteca informa de la creación de todas las series temporales excepto la actual. A continuación, en la primera entrada en OnCalculate() se crea nuevamente con éxito la serie temporal para el símbolo y el marco temporal actuales, y después, tras quitar la pausa del simulador, vemos como en este funcionan los eventos de "Nueva barra" de las series temporales utilizadas.
Todo funciona como suponíamos.
¿Qué es lo próximo?
En el siguiente artículo, continuaremos trabajando con las series temporales en los indicadores, y también pondremos a prueba el uso de las series temporales creadas para representar la información en el gráfico.
Más abajo se adjuntan todos los archivos de la versión actual de la biblioteca y los archivos del asesor de prueba. Puede descargarlo todo y ponerlo a prueba por sí mismo.
Si tiene preguntas, observaciones o sugerencias, podrá concretarlas en los comentarios al artículo.
Artículos de esta serie:
Trabajando con las series temporales en la biblioteca DoEasy (Parte 35): El objeto "Barra" y la lista de serie temporal del símbolo
Trabajando con las series temporales en la biblioteca DoEasy (Parte 36): El objeto de series temporales de todos los periodos utilizados del símbolo
Trabajando con las series temporales en la biblioteca DoEasy (Parte 37): Colección de series temporales - Base de datos de series temporales según el símbolo y el periodo
Trabajando con las series temporales en la biblioteca DoEasy (Parte 38): Colección de series temporales - Actualización en tiempo real y acceso a los datos desde el programa
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/7724





- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso