Desarrollamos un asesor experto multidivisa (Parte 15): Preparamos el asesor experto para el trading real
Introducción
En los artículos anteriores ya hemos logrado ciertos resultados, pero en general el trabajo no puede considerarse completo. El resultado final que querríamos ver es un asesor experto multidivisa que se pueda poner a trabajar en una cuenta real o en varias cuentas reales con diferentes brókeres. Hasta ahora, nuestros esfuerzos se han centrado en lograr buenos resultados comerciales en las pruebas, ya que sin esto resultará imposible negociar bien con el EA desarrollado en una cuenta real. Ahora que tenemos resultados más o menos decentes de las pruebas, podemos concentrarnos en asegurar el funcionamiento correcto en una cuenta real.
Ya hemos tratado en parte este aspecto del desarrollo del EA. En particular, el desarrollo del gestor de riesgos supuso un paso para garantizar el cumplimiento de los requisitos que puedan surgir ya en el proceso real de trading. Un gestor de riesgos no es necesario para probar ideas comerciales, ya que es una herramienta importante pero de carácter auxiliar.
En el marco de este artículo trataremos de ofrecer otros mecanismos importantes, sin los cuales no es deseable comenzar a negociar en cuentas reales. Como estas serán cosas que deben gestionar situaciones que no ocurren al ejecutar el EA en el simulador de estrategias, lo más probable es que tengamos que desarrollar EAs adicionales para depurarlas y comprobar su corrección.
Trazando el camino
Al negociar con cuentas reales, existen bastantes matices que requieren consideración y atención. Por ahora nos centraremos en algunas de ellas, que se enumeran a continuación:
- Sustitución de símbolos. Ya hemos realizado una optimización y formado cadenas de inicialización de EA utilizando nombres bastante específicos de instrumentos (símbolos) comerciales. Pero puede ocurrir que en una cuenta real los nombres de los instrumentos comerciales difieran de los que hemos usado. Las diferencias pueden ser, por ejemplo, la presencia de sufijos o prefijos en los nombres (EURGBP.x o xEURGBP en lugar de EURGBP), o una entrada en un tamaño de letra diferente (eurgbp en lugar de EURGBP). En el futuro, podremos ampliar la lista de instrumentos comerciales con diferencias aún más significativas en sus nombres. Por lo tanto, deberemos poder establecer las reglas de sustitución de los nombres de los instrumentos comerciales para que el asesor experto pueda trabajar con los símbolos utilizados por un bróker concreto.
- Modo de finalización de transacciones. Como planeamos actualizar periódicamente la composición y los ajustes de las instancias de las estrategias comerciales que trabajan simultáneamente dentro del asesor experto, resultará deseable posibilitar la transferencia de un EA ya en ejecución a un modo especial, en el que trabajará "solo para el cierre", es decir, se esforzará por finalizar la negociación mediante el cierre (preferiblemente con beneficio total) de todas las posiciones abiertas. Este proceso puede llevar algún tiempo si decidimos finalizar la negociación con este EA en algún momento de pérdida en las posiciones abiertas.
- Recuperación tras el reinicio. Se refiere a la capacidad del EA para proseguir su trabajo después de un reinicio del terminal, que puede deberse a varias razones. No hay forma de asegurarse contra algunos de estos motivos. Sin embargo, el EA no solo debe seguir funcionando, sino que además deberá hacerlo exactamente como lo habría hecho si no se hubiera producido el reinicio. Por ello, deberemos asegurarnos de que, durante su funcionamiento, el asesor experto guarde toda la información necesaria para restaurar su estado después del reinicio.
Bien, pues manos a la obra.
Sustitución de símbolos
Empezaremos por lo más sencillo: vamos a añadir la posibilidad de establecer las reglas de sustitución de los nombres de los instrumentos comerciales en la configuración del asesor experto. Generalmente, las diferencias serán la presencia de sufijos y/o prefijos adicionales. Así que a primera vista podemos añadir dos nuevos a los parámetros de entrada, donde indicaremos los sufijos y prefijos.
No obstante, este método tiene menos flexibilidad porque implica que solo se puede usar un algoritmo fijo para obtener el nombre de símbolo correcto a partir de los nombres iniciales tomados de la cadena de inicialización. Sí, y la conversión a minúsculas demandaría otro parámetro. Por ello, implementaremos otro método.
Vamos a añadir al EA un parámetro que contendrá una cadena de la siguiente forma:
<Symbol1>=<TargetSymbol1>;<Symbol2>=<TargetSymbol2>;...<SymbolN>=<TargetSymbolN>
Aquí <Symbol[i]> serán los nombres iniciales de los instrumentos comerciales utilizados en la cadena de inicialización, mientras que <TargetSymbol[i]> serán los nombres objetivo de los instrumentos comerciales que se utilizarán para la negociación real. Por ejemplo:
El valor de este parámetro lo transmitiremos a un método especial del objeto de asesor experto (de la clase CVirtualAdvisor), que realizará todas las acciones posteriores necesarias. Si transmitimos una cadena vacía a este método, no será necesario modificar los nombres de los instrumentos comerciales.
Llamaremos a este método SymbolsReplace, y añadiremos su llamada al código de la función de inicialización del EA:
//+------------------------------------------------------------------+ //| Входные параметры | //+------------------------------------------------------------------+ ... input string symbolsReplace_ = ""; // - Правила замены символов datetime fromDate = TimeCurrent(); CVirtualAdvisor *expert; // Объект эксперта //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { ... // Создаем эксперта, работающего с виртуальными позициями expert = NEW(expertParams); // Если эксперт не создан, то возвращаем ошибку if(!expert) return INIT_FAILED; // Если при замене символов возникла ошибка, то возвращаем ошибку if(!expert.SymbolsReplace(symbolsReplace_)) return INIT_FAILED; // Успешная инициализация return(INIT_SUCCEEDED); }
Luego guardaremos los cambios realizados en el archivo SimpleVolumesExpert.mq5 en la carpeta actual.
Después agregaremos a la clase EA y su implementación una descripción del método de EA que sustituye los nombres de los símbolos. Dentro de este método analizaremos la cadena de sustituciones transmitida en sus partes componentes, separándola primero por símbolos de punto y coma ';' y luego por símbolos de signo igual '='. Partiendo de las partes resultantes, formaremos un diccionario que relacionará los nombres del símbolo de origen con el nombre del símbolo de destino. A continuación, transmitiremos este diccionario a cada instancia de estrategia comercial, para que puedan realizar la sustitución necesaria si sus símbolos están presentes como claves en este diccionario.
En cada paso en el que pueda producirse un error, actualizaremos la variable de resultado para que la función de nivel superior pueda conocerlos posibles fallos al sustituir los nombres de los símbolos. En este caso, el EA informará de un fallo de inicialización.
//+------------------------------------------------------------------+ //| Класс эксперта, работающего с виртуальными позициями (ордерами) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: ... public: ... bool SymbolsReplace(const string p_symbolsReplace); // Замена названий символов }; ... //+------------------------------------------------------------------+ //| Замена названий символов | //+------------------------------------------------------------------+ bool CVirtualAdvisor::SymbolsReplace(string p_symbolsReplace) { // Если строка замен пустая, то ничего не делаем if(p_symbolsReplace == "") { return true; } // Переменная для результата bool res = true; string symbolKeyValuePairs[]; // Массив для отдельных замен string symbolPair[]; // Массив для двух имён в одной замене // Делим строку замен на части, представляющие одну отдельную замену StringSplit(p_symbolsReplace, ';', symbolKeyValuePairs); // Словарь для соответствия целевого символа исходному символу CHashMap<string, string> symbolsMap; // Для всех отдельных замен FOREACH(symbolKeyValuePairs, { // Получаем исходный и целевой символы как два элемента массива StringSplit(symbolKeyValuePairs[i], '=', symbolPair); // Проверяем наличие целевого символа в списке доступных символов (не кастомных) bool custom = false; res &= SymbolExist(symbolPair[1], custom); // Если целевой символ не найден, то сообщаем об ошибке и выходим if(!res) { PrintFormat(__FUNCTION__" | ERROR: Target symbol %s for mapping %s not found", symbolPair[1], symbolKeyValuePairs[i]); return res; } // Добавляем в словарь новый элемент: ключ - исходный символ, значение - целевой символ res &= symbolsMap.Add(symbolPair[0], symbolPair[1]); }); // Если ошибок не возникло, то для всех стратегий вызываем соответствующий метод замены if(res) { FOREACH(m_strategies, res &= ((CVirtualStrategy*) m_strategies[i]).SymbolsReplace(symbolsMap)); } return res; } //+------------------------------------------------------------------+
Luego guardaremos los cambios realizados en el archivo VirtualAdvisor.mqh en la carpeta actual.
Después añadiremos un método homónimo a la clase de estrategia comercial, pero aceptaremos como argumento no una cadena con sustituciones, sino un diccionario de sustituciones. En la clase CVirtualStrategy, por desgracia, no podemos escribir su implementación, porque al nivel de esta clase todavía no sabemos nada acerca de los instrumentos comerciales utilizados. Así que la haremos virtual trasladando la responsabilidad de la implementación al nivel inferior , a las clases hijo.
//+------------------------------------------------------------------+ //| Класс торговой стратегии с виртуальными позициями | //+------------------------------------------------------------------+ class CVirtualStrategy : public CStrategy { ... public: ... // Замена названий символов virtual bool SymbolsReplace(CHashMap<string, string> &p_symbolsMap) { return true; } };
Luego guardaremos los cambios realizados en el archivo VirtualStrategy.mqh en la carpeta actual.
Hasta ahora solo tenemos una clase hijo; esta posee una propiedad m_symbol, que almacena el nombre del instrumento comercial. Le añadiremos el método SymbolsReplace(), que simplemente comprobará si hay una clave en el diccionario transmitida que coincida con el nombre del instrumento comercial actual y cambiará el instrumento comercial si es necesario:
//+------------------------------------------------------------------+ //| Торговая стратегия с использованием тиковых объемов | //+------------------------------------------------------------------+ class CSimpleVolumesStrategy : public CVirtualStrategy { protected: string m_symbol; // Символ (торговый инструмент) ... public: ... // Замена названий символов virtual bool SymbolsReplace(CHashMap<string, string> &p_symbolsMap); }; ... //+------------------------------------------------------------------+ //| Замена названий символов | //+------------------------------------------------------------------+ bool CSimpleVolumesStrategy::SymbolsReplace(CHashMap<string, string> &p_symbolsMap) { // Если в словаре есть ключ, совпадающий с текущим символом if(p_symbolsMap.ContainsKey(m_symbol)) { string targetSymbol; // Целевой символ // Если целевой символ для текущего успешно получен из словаря if(p_symbolsMap.TryGetValue(m_symbol, targetSymbol)) { // Обновляем текущий символ m_symbol = targetSymbol; } } return true; }
Guardaremos los cambios en el archivo SimpleVoumesStrategy.mqh en la carpeta actual.
Con esto completaremos las ediciones relacionadas con esta subtarea. La comprobación en el simulador ha mostrado que el asesor experto comienza a negociar con éxito en los nuevos símbolos, según las reglas de sustitución. Cabe señalar que, como utilizamos el método CHashMap::Add() para rellenar el diccionario de sustituciones, si intentamos añadir un nuevo elemento (símbolo de destino) con una clave ya existente (símbolo de origen) se producirá un error.
Esto significa que si especificamos la regla de sustitución para el mismo símbolo dos veces en la cadena de sustitución, el EA no superará la inicialización. La cadena de sustitución deberá corregirse para excluir la repetición de normas de sustitución para instrumentos comerciales idénticos.
Modo de finalización de la negociación
El siguiente punto que planeamos añadir la capacidad de establecer un modo especial del asesor experto: la finalización de la negociación. Primero deberemos ponernos de acuerdo sobre lo que queremos decir con eso. Como planeamos activar este modo solo cuando queramos iniciar un nuevo asesor experto con parámetros diferentes en lugar del que ya funciona, por un lado, nos interesará «enterrar» todas las posiciones abiertas por el antiguo asesor experto lo antes posible. Por otra parte, no querríamos cerrar posiciones si el beneficio flotante de las mismas es actualmente negativo. En este caso, podría ser mejor esperar un tiempo hasta que el EA salga de la reducción.
Por ello, formularemos la tarea de la siguiente manera: cuando el modo de finalización de la negociación esté activado, el asesor experto deberá cerrar todas las posiciones y no abrir otras nuevas tan pronto como el beneficio flotante sea no negativo. Si el beneficio no es negativo en el momento de activar este modo, no tendremos que esperar en absoluto, el asesor experto cerrará todas las posiciones inmediatamente. Si no, tendrá que esperar.
Y la siguiente pregunta sería: ¿cuánto tiempo debemos esperar? Si observamos los resultados de las pruebas en la historia, observaremos descensos cuya duración alcanza varios meses. Así que si nos limitamos a esperar, la espera puede alargarse bastante si el momento de inicio del modo de finalización de la negociación resulta desafortunado. Tal vez resultaría más rentable cerrar todas las posiciones de la versión antigua sin esperar a entrar en positivo, es decir, aceptar las pérdidas actuales. Esto nos permitiría poner en marcha más rápidamente la nueva versión, que posiblemente, durante el tiempo en que la versión antigua estuviera esperando a entrar en positivo, podría producir beneficios para cubrir las pérdidas sufridas al detener la versión antigua.
Sin embargo, no es posible conocer de antemano ni el momento de la recuperación de la reducción de la versión antigua, ni el beneficio potencial de la nueva versión en este periodo, porque en el momento de la toma de decisiones estos resultados se sitúan en el futuro.
Un posible compromiso en esta situación podría ser la introducción de algún límite de tiempo de espera, tras el cual todas las posiciones de la versión antigua se cerrarán forzosamente con cualquier reducción actual. Podemos inventarnos opciones más complicadas. Por ejemplo, usar este tiempo límite como parámetro de una función lineal o no lineal del tiempo que retorna el valor de los fondos al que acordamos cerrar todas las posiciones en este momento. En el caso más sencillo, se trataría de una función de umbral que retorna 0 antes de este tiempo límite, y después retornará un valor inferior a los fondos actuales en la cuenta. De este modo, garantizaremos el cierre de todas las posiciones una vez transcurrido el periodo asignado.
Manos a la obra. La primera variante consistía en añadir dos parámetros de entrada (activación del modo de cierre y el límite de tiempo en días) al archivo del EA y utilizarlos después en la función de inicialización y más adelante:
//+------------------------------------------------------------------+ //| Входные параметры | //+------------------------------------------------------------------+ ... input bool useOnlyCloseMode_ = false; // - Включить режим закрытия input double onlyCloseDays_ = 0; // - Предельное время режима закрытия (дней) ... //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { ... // Подготавливаем строку инициализации для эксперта с группой из нескольких стратегий string expertParams = StringFormat( "class CVirtualAdvisor(\n" " class CVirtualStrategyGroup(\n" " [\n" " %s\n" " ],%f\n" " ),\n" " class CVirtualRiskManager(\n" " %d,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%.2f" " )\n" " ,%d,%s,%d\n" " ,%d,%.2f\n" ")", strategiesParams, scale_, rmIsActive_, rmStartBaseBalance_, rmCalcDailyLossLimit_, rmMaxDailyLossLimit_, rmCloseDailyPart_, rmCalcOverallLossLimit_, rmMaxOverallLossLimit_, rmCloseOverallPart_, rmCalcOverallProfitLimit_, rmMaxOverallProfitLimit_, rmMaxRestoreTime_, rmLastVirtualProfitFactor_, magic_, "SimpleVolumes", useOnlyNewBars_, useOnlyCloseMode_, onlyCloseDays_ ); // Создаем эксперта, работающего с виртуальными позициями expert = NEW(expertParams); ... // Успешная инициализация return(INIT_SUCCEEDED); }
Sin embargo, a medida que avanzábamos, se hizo evidente que íbamos a tener que escribir un código muy similar al que habíamos creado recientemente. Resultó que el comportamiento requerido en el modo de cierre es muy similar al comportamiento de un asesor experto que tiene un valor de beneficio objetivo establecido en el gestor de riesgos igual a la diferencia entre el balance actual al momento de iniciar el modo de cierre y el balance básico. Entonces, ¿por qué no perfeccionar un poco el gestor de riesgos para que el modo de cierre pueda aplicarse simplemente estableciendo los parámetros correspondientes en el gestor de riesgos?
Vamos a pensar en lo que nos falta en el gestor de riesgos para que el modo de cierre funcione. En el caso más sencillo, si no nos metemos con el límite de tiempo, el gestor de riesgos no necesitará mejorarse. Todo lo que deberemos hacer en la versión antigua es establecer el parámetro de beneficio objetivo en un valor igual a la diferencia entre el balance de la cuenta corriente y el balance básico y esperar a que alcance este valor. Incluso podemos ir más allá y cambiarlo periódicamente a lo largo del tiempo. Sin embargo, se espera que el uso de este mecanismo resulte bastante infrecuente. No obstante, sería preferible el cierre automático una vez transcurrido el tiempo asignado. Así que añadiremos al gestor de riesgos la capacidad de establecer no solo el objetivo de beneficio, sino también el tiempo máximo permitido para esperar por él. Esto tiempo desempeñará el papel de tiempo límite para el cierre de posiciones.
Nos resultará más cómodo transmitir este tiempo en forma de fecha y hora concretas, evitando así la necesidad de memorizar una fecha de inicio a partir de la cual contar un intervalo determinado. Añadiremos este parámetro al conjunto de parámetros de entrada relacionados con el gestor de riesgos. También añadiremos la sustitución de su valor en la cadena de inicialización:
//+------------------------------------------------------------------+ //| Входные параметры | //+------------------------------------------------------------------+ ... input group "::: Риск-менеджер" ... input ENUM_RM_CALC_OVERALL_PROFIT rmCalcOverallProfitLimit_ = RM_CALC_OVERALL_PROFIT_MONEY_BB; // - Способ расчёта общей прибыли input double rmMaxOverallProfitLimit_ = 1000000; // - Значение общей прибыли input datetime rmMaxOverallProfitDate_ = 0; // - Предельное время ожидания общей прибыли (дней) ... //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { ... // Подготавливаем строку инициализации для эксперта с группой из нескольких стратегий string expertParams = StringFormat( "class CVirtualAdvisor(\n" " class CVirtualStrategyGroup(\n" " [\n" " %s\n" " ],%f\n" " ),\n" " class CVirtualRiskManager(\n" " %d,%.2f,%d,%.2f,%.2f,%d,%.2f,%.2f,%d,%.2f,%d,%.2f,%.2f" " )\n" " ,%d,%s,%d\n" ")", strategiesParams, scale_, rmIsActive_, rmStartBaseBalance_, rmCalcDailyLossLimit_, rmMaxDailyLossLimit_, rmCloseDailyPart_, rmCalcOverallLossLimit_, rmMaxOverallLossLimit_, rmCloseOverallPart_, rmCalcOverallProfitLimit_, rmMaxOverallProfitLimit_,rmMaxOverallProfitDate_, rmMaxRestoreTime_, rmLastVirtualProfitFactor_, magic_, "SimpleVolumes", useOnlyNewBars_ ); // Создаем эксперта, работающего с виртуальными позициями expert = NEW(expertParams); ... // Успешная инициализация return(INIT_SUCCEEDED); }
Luego guardaremos los cambios realizados en el archivo SimpleVolumesExpert.mq5 en la carpeta actual.
En la clase gestor de riesgos, en primer lugar añadiremos una nueva propiedad para el tiempo marginal de espera para un beneficio determinado y estableceremos su valor a partir de la cadena de inicialización en el constructor. También añadiremos el nuevo método OverallProfit(), que retornará el valor del beneficio deseado al cierre:
//+------------------------------------------------------------------+ //| Класс управления риском (риск-менеждер) | //+------------------------------------------------------------------+ class CVirtualRiskManager : public CFactorable { protected: // Основные параметры конструктора ... ENUM_RM_CALC_OVERALL_PROFIT m_calcOverallProfitLimit; // Способ расчёта максимальной общей прибыли double m_maxOverallProfitLimit; // Параметр расчёта максимальной общей прибыли datetime m_maxOverallProfitDate; // Предельное время для достижения общей прибыли ... // Защищённые методы double DailyLoss(); // Максимальный дневной убыток double OverallLoss(); // Максимальный общий убыток double OverallProfit(); // Максимальная прибыль ... }; //+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+ CVirtualRiskManager::CVirtualRiskManager(string p_params) { // Запоминаем строку инициализации m_params = p_params; // Читаем строку инициализации и устанавливаем значения свойств m_isActive = (bool) ReadLong(p_params); m_baseBalance = ReadDouble(p_params); m_calcDailyLossLimit = (ENUM_RM_CALC_DAILY_LOSS) ReadLong(p_params); m_maxDailyLossLimit = ReadDouble(p_params); m_closeDailyPart = ReadDouble(p_params); m_calcOverallLossLimit = (ENUM_RM_CALC_OVERALL_LOSS) ReadLong(p_params); m_maxOverallLossLimit = ReadDouble(p_params); m_closeOverallPart = ReadDouble(p_params); m_calcOverallProfitLimit = (ENUM_RM_CALC_OVERALL_PROFIT) ReadLong(p_params); m_maxOverallProfitLimit = ReadDouble(p_params); m_maxOverallProfitDate = (datetime) ReadLong(p_params); m_maxRestoreTime = ReadDouble(p_params); m_lastVirtualProfitFactor = ReadDouble(p_params); ... }
El método OverallProfit() comprobará primero si se ha establecido el tiempo para alcanzar el beneficio deseado. Si se da una hora y la hora actual ya ha superado la hora dada, el método retornará el valor del beneficio actual, puesto que el valor que hay ahora ya es el valor deseado. Esto llevará finalmente a cerrar todas las posiciones y dejar de negociar. Si aún no se ha alcanzado el tiempo, el método retornará el valor del beneficio deseado calculado a partir de los parámetros de entrada:
//+------------------------------------------------------------------+ //| Максимальный общая прибыль | //+------------------------------------------------------------------+ double CVirtualRiskManager::OverallProfit() { // Текущее время datetime tc = TimeCurrent(); // Если текущее время больше заданного максимально допустимого, то if(m_maxOverallProfitDate && tc > m_maxOverallProfitDate) { // Возвращаем значение, гарантирующее закрытие позиций return m_overallProfit; } else if(m_calcOverallProfitLimit == RM_CALC_OVERALL_PROFIT_PERCENT_BB) { // Для заданного процента от базового баланса вычисляем его return m_baseBalance * m_maxOverallProfitLimit / 100; } else { // Для фиксированного значения просто возвращаем его // RM_CALC_OVERALL_PROFIT_MONEY_BB return m_maxOverallProfitLimit; } }
Usaremos este método al comprobar si necesitamos cerrar dentro del método CheckOverallProfitLimit():
//+------------------------------------------------------------------+ //| Проверка достижения заданной прибыли | //+------------------------------------------------------------------+ bool CVirtualRiskManager::CheckOverallProfitLimit() { // Если достигнут общий убыток и позиции ещё открыты if(m_overallProfit >= OverallProfit() && CMoney::DepoPart() > 0) { // Уменьшаем множитель используемой части общего баланса по общему убытку m_overallDepoPart = 0; // Устанавливаем риск-менеджер в состояние достигнутой общей прибыли m_state = RM_STATE_OVERALL_PROFIT; // Устанавливаем значение используемой части общего баланса SetDepoPart(); ... return true; } return false; }
Luego guardaremos los cambios realizados en el archivo VirtualRiskManager.mqh en la carpeta actual.
Las modificaciones relativas al modo de cierre se han finalizado en su mayor parte. Añadiremos el resto más adelante, cuando realicemos los trabajos necesarios para garantizar que podamos restablecer el funcionamiento tras el reinicio.
Recuperación tras el reinicio
La necesidad de ofrecer esta oportunidad se ha previsto desde las primeras partes del ciclo. Muchas de las clases que hemos creado ya poseen los métodos Save() y Load(), que están diseñados para guardar y cargar el estado de un objeto. Ya tenemos código funcional escrito en algunos de estos métodos, pero luego hemos estado haciendo otras cosas y no nos hemos asegurado de mantener estos métodos funcionando correctamente por falta de necesidad. Es hora de volver a ellos y ponerlos de nuevo en funcionamiento.
Quizás los principales cambios que debamos introducir se den nuevamente en la clase gestora de riesgos, ya que estos métodos siguen estando completamente vacíos en ella. También tendremos que asegurarnos de que los métodos de guardo y carga del gestor de riesgos se llamen al cargar/guardar el asesor experto, ya que el gestor de riesgos ha aparecido más tarde y no se ha añadido a la información que estábamos guardando.
Empezaremos añadiendo un parámetro de entrada del EA que determinará si el estado transmitido debe ser restaurado. Por defecto será True. Si queremos iniciar el EA desde cero, podremos establecerlo en False, reiniciar el EA (en este caso, toda la información guardada previamente se sobrescribirá con la nueva información), y luego devolver este parámetro a True. En la función de inicialización de EA, comprobaremos si el estado anterior debe ser cargado, y de ser así, lo cargaremos:
//+------------------------------------------------------------------+ //| Входные параметры | //+------------------------------------------------------------------+ ... input bool usePrevState_ = true; // - Загружать предыдущее состояние ... //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { ... // Создаем эксперта, работающего с виртуальными позициями expert = NEW(expertParams); // Если эксперт не создан, то возвращаем ошибку if(!expert) return INIT_FAILED; // Если при замене символов возникла ошибка, то возвращаем ошибку if(!expert.SymbolsReplace(symbolsReplace_)) return INIT_FAILED; // Если требуется восстанавливать состояние, то if(usePrevState_) { // Загружаем прошлое состояние при наличии expert.Load(); expert.Tick(); } // Успешная инициализация return(INIT_SUCCEEDED); }
Luego guardaremos los cambios realizados en el archivo SimpleVolumesExpert.mq5 en la carpeta actual.
Antes de pasar a los métodos de guardado/carga del estado, señalaremos un aspecto a considerar. En la versión anterior, formábamos el nombre del archivo a guardar a partir del nombre del asesor experto, su número mágico y la palabra ".test" al iniciarlo en el modo de prueba visual. El nombre del asesor experto es un valor constante que está incorporado en el código fuente y no cambia a través de los parámetros de entrada del asesor experto. Podemos cambiar el número mágico a través de los parámetros de entrada. Esto significa que si cambiamos el número mágico, el asesor experto ya no cargará el archivo generado con el anterior número mágico usado. Pero también significa que si cambiamos la composición de instancias individuales de estrategias comerciales pero dejamos el mismo número mágico, el EA intentará utilizar el archivo anterior para cargar el estado.
Es probable que esto dé lugar a errores, por lo que deberemos protegernos para que no se produzca esta situación. Una posible forma sería incluir alguna parte en el nombre del archivo que dependa de las instancias de estrategias comerciales utilizadas. Entonces, si su composición cambia, esta parte del nombre del archivo también cambiará, lo cual significa que el asesor experto no utilizará el archivo antiguo después de actualizar la composición de las estrategias.
Dicha parte cambiante del nombre del archivo puede formarse calculando alguna función hash a partir de la cadena de inicialización del EA o de una parte suya. De hecho, ya hemos hablado de la necesidad de utilizar un archivo diferente solo cuando se cambie la composición de las estrategias comerciales. Si cambiamos, por ejemplo, la configuración del gestor de riesgos, esto modificará la cadena de inicialización, pero no debería cambiar el nombre del archivo utilizado para guardar el estado. Por consiguiente, calcularemos la función hash solo a partir de la parte de la cadena de inicialización que contenga la información sobre las instancias únicas de estrategias comerciales.
Para ello, añadiremos un nuevo método HashParams() y realizaremos cambios en el constructor del asesor experto:
//+------------------------------------------------------------------+ //| Класс эксперта, работающего с виртуальными позициями (ордерами) | //+------------------------------------------------------------------+ class CVirtualAdvisor : public CAdvisor { protected: ... virtual string HashParams(string p_name); // Хеш-значение параметров эксперта public: ... }; ... //+------------------------------------------------------------------+ //| Хеш-значение параметров эксперта | //+------------------------------------------------------------------+ string CVirtualAdvisor::HashParams(string p_params) { uchar hash[], key[], data[]; // Вычисляем хеш от строки инициализации StringToCharArray(p_params, data); CryptEncode(CRYPT_HASH_MD5, data, key, hash); // Переводим его из массива чисел в строку с шестнадцатеричной записью string res = ""; FOREACH(hash, res += StringFormat("%X", hash[i]); if(i % 4 == 3 && i < 15) res += "-"); return res; } //+------------------------------------------------------------------+ //| Конструктор | //+------------------------------------------------------------------+ CVirtualAdvisor::CVirtualAdvisor(string p_params) { ... // Если нет ошибок чтения, то if(IsValid()) { ... // Формируем из имени эксперта и параметров имя файла для сохранения состояния m_name = StringFormat("%s-%d-%s%s.csv", (p_name != "" ? p_name : "Expert"), p_magic, HashParams(groupParams), (MQLInfoInteger(MQL_TESTER) ? ".test" : "") );; ... } }
Luego añadiremos la función de guardado/carga del gestor de riesgos a los métodos de EA correspondientes:
//+------------------------------------------------------------------+ //| Сохранение состояния | //+------------------------------------------------------------------+ bool CVirtualAdvisor::Save() { bool res = true; // Сохраняем состояние, если: if(true // появились более поздние изменения && m_lastSaveTime < CVirtualReceiver::s_lastChangeTime // и сейчас не оптимизация && !MQLInfoInteger(MQL_OPTIMIZATION) // и сейчас не тестирование либо сейчас визуальное тестирование && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE)) ) { int f = FileOpen(m_name, FILE_CSV | FILE_WRITE, '\t'); if(f != INVALID_HANDLE) { // Если файл открыт, то сохраняем FileWrite(f, CVirtualReceiver::s_lastChangeTime); // Время последних изменений // Все стратегии FOREACH(m_strategies, ((CVirtualStrategy*) m_strategies[i]).Save(f)); m_riskManager.Save(f); FileClose(f); // Обновляем время последнего сохранения m_lastSaveTime = CVirtualReceiver::s_lastChangeTime; PrintFormat(__FUNCTION__" | OK at %s to %s", TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS), m_name); } else { PrintFormat(__FUNCTION__" | ERROR: Operation FileOpen for %s failed, LastError=%d", m_name, GetLastError()); res = false; } } return res; } //+------------------------------------------------------------------+ //| Загрузка состояния | //+------------------------------------------------------------------+ bool CVirtualAdvisor::Load() { bool res = true; // Загружаем состояние, если: if(true // файл существует && FileIsExist(m_name) // и сейчас не оптимизация && !MQLInfoInteger(MQL_OPTIMIZATION) // и сейчас не тестирование либо сейчас визуальное тестирование && (!MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE)) ) { int f = FileOpen(m_name, FILE_CSV | FILE_READ, '\t'); if(f != INVALID_HANDLE) { // Если файл открыт, то загружаем m_lastSaveTime = FileReadDatetime(f); // Время последнего сохранения PrintFormat(__FUNCTION__" | LAST SAVE at %s", TimeToString(m_lastSaveTime, TIME_DATE | TIME_MINUTES | TIME_SECONDS)); // Загружаем все стратегии FOREACH(m_strategies, { res &= ((CVirtualStrategy*) m_strategies[i]).Load(f); if(!res) break; }); if(!res) { PrintFormat(__FUNCTION__" | ERROR loading strategies from file %s", m_name); } res &= m_riskManager.Load(f); if(!res) { PrintFormat(__FUNCTION__" | ERROR loading risk manager from file %s", m_name); } FileClose(f); } else { PrintFormat(__FUNCTION__" | ERROR: Operation FileOpen for %s failed, LastError=%d", m_name, GetLastError()); res = false; } } return res; }
Después guardaremos los cambios realizados en el archivo VirtualAdvisor.mq5 en la carpeta actual.
Ahora todo lo que deberemos hacer es escribir la implementación de los métodos de guardado/carga del gestor de riesgos. A continuación, echaremos un vistazo a lo que debemos conservar para el gestor de riesgos. No tendremos que guardar los parámetros de entrada del gestor de riesgos, siempre se tomarán de los parámetros de entrada del asesor experto, donde se pueden cambiar en el siguiente inicio. Tampoco deberemos guardar los valores actualizados por el propio gestor de riesgos: valores de balance, fondos, beneficio diario y otros. Lo único que merecerá la pena conservar de estos valores es el valor del nivel básico diario, ya que su cálculo solo se realizará una vez al día.
Por otro lado, todas las propiedades atribuidas al estado actual y a la gestión del tamaño de las posiciones abiertas (excepto la parte utilizada del balance total) deberán mantenerse necesariamente.
// Текущее состояние ENUM_RM_STATE m_state; // Состояние double m_lastVirtualProfit; // Прибыль открытых виртуальных позиций на момент лимита убытка datetime m_startRestoreTime; // Время начала восстановления размеров открытых позиций datetime m_startTime; // Обновляемые значения ... // Управление размером открытых позиций double m_baseDepoPart; // Используемая часть общего баланса (исходная) double m_dailyDepoPart; // Множитель используемой части общего баланса по дневному убытку double m_overallDepoPart; // Множитель используемой части общего баланса по общему убытку
Considerando todo esto, la implementación de estos métodos podría tener el aspecto siguiente:
//+------------------------------------------------------------------+ //| Сохранение состояния | //+------------------------------------------------------------------+ bool CVirtualRiskManager::Save(const int f) { FileWrite(f, m_state, m_lastVirtualProfit, m_startRestoreTime, m_startTime, m_dailyDepoPart, m_overallDepoPart); return true; } //+------------------------------------------------------------------+ //| Загрузка состояния | //+------------------------------------------------------------------+ bool CVirtualRiskManager::Load(const int f) { m_state = (ENUM_RM_STATE) FileReadNumber(f); m_lastVirtualProfit = FileReadNumber(f); m_startRestoreTime = FileReadDatetime(f); m_startTime = FileReadDatetime(f); m_dailyDepoPart = FileReadNumber(f); m_overallDepoPart = FileReadNumber(f); return true; }
Los cambios realizados los guardaremos en el archivo VirtualRiskManager.mq5 en la carpeta actual.
Simulación
Para probar la funcionalidad añadida, seguiremos dos caminos. En primer lugar, pondremos el EA compilado en el gráfico y comprobaremos que se crea el archivo de datos de estado. Para realizar más pruebas, deberemos esperar a que el EA abra posiciones. Pero para ello podemos esperar bastante tiempo, y aún más tendremos que esperar a que se active el gestor de riesgos para poder comprobar si el EA reanuda su trabajo correctamente tras su intervención. En segundo lugar, utilizaremos el simulador de estrategias y simularemos una situación en la que se reanudará el funcionamiento del asesor experto después de una parada.
Para ello, crearemos un nuevo asesor experto basado en el existente, al que añadiremos dos nuevos parámetros de entrada: el tiempo de parada antes del reinicio y la hora de inicio del reinicio. Se procesarán de la siguiente manera:
- Si el tiempo de parada antes del reinicio no se especifica (es igual a cero o 1970-01-01 00:00:00) o no cae dentro del intervalo de prueba, el asesor experto funcionará como el asesor experto original;
- si se especifica un tiempo de parada concreto que cae dentro del intervalo de prueba, entonces al alcanzarse este tiempo, el asesor experto dejará de ejecutar el manejador del tick del objeto de EA hasta que se alcance el tiempo especificado en el segundo parámetro.
En código, estos dos parámetros tendrán el aspecto siguiente:
input datetime restartStopTime_ = 0; // - Время остановки перед перезапуском input datetime restartStartTime_ = 0; // - Время начала перезапуска
Primero realizaremos cambios en la función de procesamiento de ticks en el EA. Para recordar que se ha producido una interrupción, añadiremos la variable booleana global isRestarting. Si es True, el EA se encuentra ahora en pausa. Tan pronto como el tiempo actual supere el tiempo de reanudación, cargaremos el estado transmitido del EA y reiniciaremos la bandera isRestarting:
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { // Если время остановки указано, то if(restartStopTime_ != 0) { // Определяем текущее время datetime tc = TimeCurrent(); // Если мы находимся в промежутке между остановкой и возобновлением, то if(tc >= restartStopTime_ && tc <= restartStartTime_) { // Запоминаем это состояние и выходим isRestarting = true; return; } // Если мы находились в состоянии между остановкой и возобновлением // и пришло время возобновления работы, то if(isRestarting && tc > restartStartTime_) { // Загружем состояние эксперта expert.Load(); // Сбрасываем флаг состояния между остановкой и возобновлением isRestarting = false; } } // Выполняем обработку тика expert.Tick(); }
Luego guardaremos los cambios realizados en el archivo SimpleVolumesTestRestartExpert.mq5 en la carpeta actual.
Veamos los resultados sin interrupción en el intervalo 2021-2022.
Fig. 1. Resultados de las pruebas sin interrupción de la actividad comercial
Ahora realizaremos una breve pausa en el EA en algún momento. Tras la prueba, los resultados han sido idénticos a los resultados sin la interrupción. Esto indica que con una breve pausa, el EA recupera con éxito su estado y continúa trabajando.
Para ver la diferencia, realizaremos una pausa más larga, por ejemplo de 4 meses. Obtendremos los siguientes resultados:
Fig. 2. Resultados de las pruebas con una pausa comercial del 27.07.2021 al 29.11.2021
En el gráfico, la posición aproximada de la interrupción se muestra mediante un rectángulo con el borde amarillo. En ese momento, las posiciones abiertas por el asesor experto han sido abandonadas a su suerte. Pero luego el EA ha vuelto a ponerse en marcha, retomando sus posiciones abiertas y logrando buenos resultados en general. Podemos considerar que ahora también está implementada la capacidad de guardar y cargar el estado del EA.
Conclusión
Veamos de nuevo los resultados obtenidos. Ya hemos empezado a preparar seriamente nuestro EA para que negocie en una cuenta real. Con este fin, hemos analizado varios escenarios que no se encuentran en el simulador, pero al trabajar en una cuenta real, probablemente el asesor experto se los encuentre.
Asimismo, hemos analizado el funcionamiento del EA en cuentas donde los nombres de los instrumentos comerciales difieren de los utilizados durante su optimización. Para ello, hemos implementado la posibilidad de sustituir los nombres de los símbolos. También hemos implementado la posibilidad de finalizar gradualmente la negociación si resulta necesario para poner en funcionamiento el asesor experto con otros parámetros de entrada. Otra ampliación importante ha sido la adición de la posibilidad de guardar el estado del asesor experto para garantizar la correcta reanudación del trabajo tras reiniciar el terminal.
Sin embargo, estos no son todos los preparativos que deberemos hacer al instalar un asesor experto para trabajar en una cuenta comercial real. Sería útil organizar mejor la salida de la información de depuración en el registro para permitir la visualización de diferentes tipos de información auxiliar. También sería interesante proporcionar una visión consolidada del estado actual del EA directamente en el gráfico. Pero más importante aún sería una mejora que permita que el asesor experto funcione cuando no haya una base de datos con resultados de optimización en la carpeta de trabajo del terminal. De momento no podemos prescindir de ella, porque es de esta base de datos de donde tomaremos los componentes para formar la cadena de inicialización del asesor experto. En futuros artículos nos ocuparemos de estas mejoras.
Gracias por leer hasta el final, ¡hasta pronto!
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/15294
- 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