English Русский 中文 Deutsch 日本語 Português
preview
Multibot en MetaTrader (Parte II): Plantilla dinámica mejorada

Multibot en MetaTrader (Parte II): Plantilla dinámica mejorada

MetaTrader 5Ejemplos | 14 agosto 2024, 15:34
405 0
Evgeniy Ilin
Evgeniy Ilin

Contenido


Introducción

En el último artículo me inspiré en algunas de las soluciones más populares del Mercado, y conseguí crear un análogo propio de una plantilla de este tipo. Pero, considerando algunos de los proyectos en los que participo, resulta que esta solución no es óptima. Además, sigue presentando una serie de limitaciones e inconvenientes relacionados con la arquitectura general de dicha plantilla. Sí, ese patrón puede ser suficiente para un número aplastante de decisiones medias, pero no para la mía. Un segundo punto muy importante es el hecho de que desde el punto de vista de un potencial comprador y usuario final de este asesor, personalmente querría ver un desarrollo con la máxima simplicidad y el mínimo número de ajustes. Y, en el mejor de los casos, una ausencia total de necesidad de sobreoptimización y otras manipulaciones por nuestra parte. Al fin y al cabo, pagamos dinero no solo por una solución que funcione, sino (y lo que es mucho más importante) por una interfaz más amigable y comprensible incluso para el usuario más sencillo.


Idea de plantilla dinámica

Basándonos en el asesor anterior, vamos ahora a recordar para qué creamos dicha plantilla. El objetivo principal era poder ejecutar un asesor, pero con diferentes configuraciones para cada par "instrumento - periodo". Esto era necesario para no colgar cada asesor por separado en su propio gráfico. Así logramos simplificar el control de dicho sistema y su configuración, ya que cuantas más copias del mismo asesor haya en un terminal, mayor será la probabilidad de un posible error del usuario. Además, podían surgir conflictos entre dichos asesores debido a los números mágicos de las órdenes, por ejemplo, o por otros motivos necesariamente precedidos de errores humanos. Ahora, para estructurarlo un poco más, primero describiremos los pros y los contras de la primera plantilla, y luego haremos lo mismo con la nueva, tras lo cual se verán claramente las diferencias y las ventajas del nuevo enfoque.

Ventajas de la plantilla estática (básica):

  1. Ajuste individual de cada instrumento - periodo (asesor virtual en un gráfico virtual).
  2. Código más sencillo y rápido (se ejecutará más rápido).
  3. Amplitud del enfoque (extensamente comercializado y de viabilidad demostrada).

    Desventajas de la plantilla estática (básica):

    1. Difícil de configurar y alta probabilidad de error (largas cadenas de caracteres en la configuración).
    2. Oportunidades limitadas para ampliar la funcionalidad.
    3. Imposibilidad de añadir o eliminar robots virtuales sin realizar una reconfiguración manual.

      Considerando estos datos, podemos entender que la solución es muy limitada, aunque común. Podemos pensar una solución mucho más interesante. Además, el reto de integrar nuestro asesor con nuestra solución que realiza una optimización dinámica independiente fuera del terminal MetaTrader, me ha impulsado a crear una solución mejorada. Podemos empezar con un diagrama que revele la esencia de la nueva plantilla y nos ayude a comprender mejor sus ventajas y a compararla con la versión anterior.

      esquema de la nueva plantilla

      Eche un vistazo al elemento inferior izquierdo que contiene "CLOSING". La plantilla permite guardar la configuración con la que se abrió la última posición en el gráfico virtual correspondiente. Se trata de un mecanismo de defensa especial contra los cambios de configuración frecuentes y los fallos de estrategia. Si abrimos una posición según alguna lógica, nos interesará cerrarla usando la misma lógica, y solo entonces podremos cambiar la configuración a una más reciente, de lo contrario, podemos acabar hechos papilla. Sí, no siempre es así, pero resulta mejor bloquear esos momentos de una vez en el plano de la lógica general.

      La nueva plantilla usará las características y la estructura de construcción de los catálogos de MetaTrader 4 y MetaTrader 5 de forma conjunta con el sistema de archivos. En otras palabras, gestionaremos nuestra plantilla con archivos de texto que puedan contener los ajustes necesarios para cada par comercial virtual. Esto garantizará que podamos configurar y ajustar de forma dinámica el asesor de la forma más fácil posible durante el funcionamiento, sin necesidad de realizar esto manualmente. Pero eso no es todo. Dicha forma de gestión proporcionará una serie de utilidades, que enumeraremos a continuación.

      Ventajas de la plantilla dinámica (nueva):

      1. Cada instrumento-periodo se configura independientemente del terminal comercial mediante archivos de texto.
      2. La lectura dinámica de configuraciones desde carpetas y la apertura o cierre automáticos de gráficos virtuales se produce junto con nuestros asesores.
      3. Existe la posibilidad de realizar la sincronización automática con Web API (necesariamente a través del puerto 443).
      4. La configuración se realiza a través de la carpeta compartida del terminal (*\Common/Files).
      5. Se necesita un asesor maestro para sincronizar todos los terminales de una máquina con la API. También podría funcionar potencialmente en múltiples máquinas a través de una carpeta compartida dentro de una red local.
      6. Puede integrarse con programas externos que también pueden gestionar estos archivos, por ejemplo: crearlos, borrarlos o cambiar la configuración.
      7. Hay una situación posible en la que se crea un par de plantillas gratuitas. Si eliminamos la conexión API, podremos enmarcar esto como una demo que requiere una plantilla de pago para funcionar eficazmente.

                Desventajas de la plantilla dinámica (nueva):

                1. Trabajo con el sistema de archivos

                La desventaja aquí también es muy condicional, porque la alternativa solo puede ser la integración web o la comunicación mediante otros métodos (por ejemplo, vía RAM), pero en este caso nuestra baza será que no utilizamos ninguna biblioteca adicional, y nuestro código se convierte así en multiplataforma (apto tanto para asesores de MQL4 como para asesores de MQL5). Por ello, estos asesores están sujetos a los requisitos del mercado y pueden ponerse a la venta fácilmente si lo deseamos. Sí, obviamente, tendremos que añadir algunas cosas y ajustarlas a nuestro gusto, pero será extremadamente fácil hacerlo utilizando mis desarrollos.

                Creo que no estará de más comprender las bases del funcionamiento de un asesor de este tipo desde dentro, para visualizar y representar esquemáticamente lo que hemos escrito un poco más arriba. Podremos conseguirlo utilizando el siguiente esquema:

                esquema de plantilla simplificado

                Todo el esquema funcionará de forma bastante sencilla, con dos temporizadores independientes responsables cada uno de sus propias tareas. El primer temporizador cargará los ajustes desde la API, mientras que el segundo temporizador leerá los mismos ajustes en la memoria del asesor. Además, tendremos dos flechas que simbolizarán la posibilidad de crear y cambiar manualmente estos ajustes, o de automatizar este proceso mediante una aplicación de terceros u otro código. En teoría, esta plantilla puede controlarse desde cualquier otro código, así como desde cualquier otro asesor o script para MetaTrader 4 y MetaTrader 5.

                Después de leer los ajustes, se determinará si los ajustes son nuevos, o si hay nuevos ajustes en la carpeta de ajustes, o si hemos podido borrar algunos de ellos. Si la comparación muestra que no hay cambios en los archivos, la plantilla seguirá funcionando, pero si algo ha cambiado, será mucho más correcto por nuestra parte reiniciar todo el código con la nueva configuración y seguir trabajando. Por ejemplo, si tuviéramos un asesor que funcionara solo con el gráfico en el que se coloca, tendríamos que hacer todo esto manualmente dentro del terminal, pero en este caso, toda la gestión resulta totalmente automática dentro de la plantilla, sin necesidad de intervención manual.


                Configuración básica de la plantilla dinámica

                Ahora, creo que podemos pasar poco a poco al código y analizar sus puntos esenciales. Permítame mostrarles primero el conjunto mínimo de parámetros de entrada: creo que bastarán para gestionar el trading utilizando la lógica "por barras". Pero antes, debemos mencionar los paradigmas importantes que usa esta plantilla para comprender mejor su código más adelante.

                • Las operaciones comerciales y los cálculos de las señales de entrada se producirán cuando se abra una nueva barra (este punto se calculará automáticamente en función de los ticks, para cada gráfico virtual por separado).
                • Solo se podrá abrir una posición a la vez en un gráfico (EURUSD M1 y EURUSD M5 se consideran gráficos diferentes).
                • No se podrá abrir una nueva posición en otro gráfico hasta que no se cierre la anterior (a mi parecer, este esquema es el más sencillo y correcto, porque cualquier tipo de añadidos y promedios ya supone un engaño, hasta cierto punto).

                Ahora, considerando estas reglas y restricciones, deberemos entender que podemos modificar este código para trabajar con promedios o martingales, o, por ejemplo, para trabajar con órdenes pendientes. Podrá modificar su estructura usted mismo, si lo necesita. Veamos ahora el conjunto mínimo de parámetros que nos permitirá gestionar la negociación.

                //+------------------------------------------------------------------+
                //|                         main variables                           |
                //+------------------------------------------------------------------+
                input bool bToLowerE=false;//To Lower Symbol
                input string SymbolPrefixE="";//Symbol Prefix
                input string SymbolPostfixE="";//Symbol Postfix
                
                input string SubfolderE="folder1";//Subfolder In Files Folder
                input bool bCommonReadE = true;//Read From Common Directory
                
                input bool bWebSyncE = false;//Sync with API
                input string SignalDirectoryE = "folder1";//Signal Name(Folder)
                input string ApiDomen = "https://yourdomen.us";//API DOMEN (add in terminal settings!)
                
                input bool bInitLotControl=true;//Auto Lot
                input double DeltaBarPercent=1.5;//Middle % of Delta Equity Per M1 Bar (For ONE! Bot)
                input double DepositDeltaEquityE=100.0;//Deposit For ONE! Bot
                
                input bool bParallelTradingE=true;//Parallel Trading
                
                input int SLE=0;//Stop Loss Points
                input int TPE=0;//Take Profit Points

                En este ejemplo, hemos dividido las variables en diferentes bloques según las similitudes. Como podemos ver, no hay muchas variables. El primer bloque está pensado para procesar distintos estilos de denominación de instrumentos en distintos brókeres. Por ejemplo, si en su bróker tiene "EURUSD", entonces todas las variables de este bloque deberán quedarse como están ahora, pero también serán posibles otras opciones, por ejemplo:

                • eurusd
                • EURUSDt
                • tEURUSD
                • _EURUSD
                • eurusd_

                Y así sucesivamente. No vamos a continuar, creo que lo descubrirá usted mismo. Ni que decir tiene que una buena plantilla debería procesar la mayoría de estas variaciones.

                El segundo sub-bloque nos dice de qué carpeta leeremos los archivos; los datos de la API se cargarán de esta misma carpeta. Si especificamos un nombre de carpeta correcto, MetaTrader creará el subdirectorio correspondiente y trabajará con él. Si no lo especificamos, todos los archivos estarán en la carpeta raíz. Una característica interesante es la carpeta de terminales compartidos: si activamos esta opción, podremos combinar no solo varios asesores que se ejecuten dentro de MetaTrader 5, sino todos los terminales, independientemente de si se están ejecutando en MetaTrader 4 o MetaTrader 5. De este modo, ambas plantillas podrán funcionar de la misma manera con la misma configuración, garantizando la máxima integración y sincronización.

                El tercer sub-bloque, como supongo que todo el mundo habrá deducido, activa y desactiva el temporizador de sincronización con nuestra API, si existe. Debemos destacar que la comunicación con la API se realiza gracias a una única función "WebRequest", que funciona tanto en MQL4 como en MQL5. La única limitación aquí es que nuestra API deberá ejecutarse necesariamente en el puerto 443. Sin embargo, en MQL5 este método ha sido ampliado, y tiene la capacidad de conectarse a través de otro puerto. Sin embargo, hemos abandonado esta idea en nuestra API para garantizar la coherencia multiplataforma de nuestras plantillas y las soluciones construidas sobre ella.

                La API se ha construido de tal manera que el nombre de la señal sea también un directorio con archivos. De esa forma, podremos conectarnos a diferentes señales conociendo sus nombres. En cualquier momento podremos desconectarnos de la señal antigua y conectarnos a la nueva. Sí, claro que así no se descargarán los archivos, pero lo hemos hecho de otra manera. Yo obtengo JSON con el contenido de este archivo, y luego lo creo yo mismo a partir del código de la propia plantilla. Sí, esto requiere métodos adicionales para extraer estos datos de nuestra cadena JSON, pero personalmente no he tenido ningún problema con ello, porque MQL5 nos permite hacerlo fácilmente sin muchos problemas. Y, por supuesto, antes de utilizar nuestra API, necesitaremos un dominio cualquiera y añadir este a la lista de conexiones permitidas en la configuración de MetaTrader.

                El cuarto bloque es muy importante, porque controla los riesgos y los volúmenes de nuestros datos de entrada. Según los clásicos, obviamente, somos libres de ajustar los volúmenes para cada instrumento-periodo por separado, utilizando los mismos archivos de texto, pero hemos decidido implementar el ajuste automático, usando los datos de volatilidad del par comercial utilizado. Este planteamiento también implicará aumentar automáticamente los volúmenes de forma proporcional al crecimiento de nuestro balance, lo que se denomina lote automático.

                El quinto bloque constará de una sola variable. El asunto es que por defecto todos los asesores virtuales que operan dentro de la plantilla funcionan independientemente y no prestan atención a las posiciones abiertas por los asesores que estén en otros gráficos virtuales. Esta variable incluye un control en el que solo se podrá abrir una posición en un único asesor; los demás esperarán a que se cierre y solo entonces podrán abrir las suyas, funcionando, por así decirlo, por orden de llegada. En algunos casos, este estilo de negociación podría resultar extremadamente útil.

                El último bloque, el quinto, solo contendrá los ajustes de los stops. Si se ponen a cero, se comerciará sin ellos. En el sexto bloque, que aún no existe, podremos añadir nuestros propios parámetros si fuera necesario.


                Reglas para nombrar los archivos con los ajustes y directivas de ayuda

                Querría empezar con los métodos básicos que cargan las configuraciones desde nuestros archivos y cómo se leen estos, así como la forma en que estos métodos encuentran exactamente los archivos necesarios. Para que estos archivos se puedan leer correctamente, primero deberemos introducir reglas de denominación que sean sencillas y directas. En primer lugar, tendremos que encontrar esta directiva en nuestra plantilla:

                //+------------------------------------------------------------------+
                //|                     your bot unique name                         |
                //+------------------------------------------------------------------+
                #define BotNick "dynamictemplate" //bot

                Esta directiva establecerá una especie de pseudónimo para nuestro bot. Absolutamente todo pasará por este seudónimo:

                1. La denominación de los archivos recién creados,
                2. La lectura de los archivos,
                3. La creación de las variables globales de terminal,
                4. La lectura de las variables globales del terminal,
                5. Otros.

                Nuestra plantilla solo admitirá como ajustes propios los archivos "**** dynamictemplate.txt". En otras palabras, hemos definido la primera regla para nombrar los archivos de configuración: el nombre del archivo antes de su extensión deberá terminar necesariamente con "dynamictemplate". Es usted libre de cambiar ese nombre por el que desee. Por lo tanto, si crea dos asesores con diferentes pseudónimos, estos ignorarán a buen seguro la configuración de su "hermano" y trabajarán solo con sus propios archivos.

                Cerca hay otra directiva similar que posibilitará lo mismo:

                //+------------------------------------------------------------------+
                //|               unique shift for difference of EA                  |
                //+------------------------------------------------------------------+
                #define MagicHelp 0 //bot magic shift

                La única diferencia es que esta directiva se asegurará de que los diferentes asesores se diferencien al nivel del número mágico de las órdenes, al igual que sucede con los archivos, para que dos o más asesores no cierren las órdenes de otros asesores, si de repente decidimos utilizar varios asesores de este tipo dentro de una misma cuenta comercial. Del mismo modo, al crear un nuevo asesor, deberemos cambiar adicionalmente este número junto con el cambio del pseudónimo, pero no lo haremos demasiado grande: será mejor añadir solo una unidad a este número cada vez que creemos un nuevo asesor basado en esta plantilla.

                A continuación pasaremos a la parte del nombre del archivo que indicará a nuestro asesor a qué gráfico va destinado. Pero primero, será mejor que veamos este array:

                //+------------------------------------------------------------------+
                //|                        applied symbols                           |
                //+------------------------------------------------------------------+
                string III[] = { 
                   "EURUSD",
                   "GBPUSD",
                   "USDJPY",
                   "USDCHF",
                   "USDCAD",
                   "AUDUSD",
                   "NZDUSD",
                   "EURGBP",
                   "EURJPY",
                   "EURCHF",
                   "EURCAD",
                   "EURAUD",
                   "EURNZD",
                   "GBPJPY",
                   "GBPCHF",
                   "GBPCAD",
                   "GBPAUD",
                   "GBPNZD",
                   "CHFJPY",
                   "CADJPY",
                   "AUDJPY",
                   "NZDJPY",
                   "CADCHF",
                   "AUDCHF",
                   "NZDCHF",
                   "AUDCAD",
                   "NZDCAD",
                   "AUDNZD",
                   "USDPLN",
                   "EURPLN",
                   "USDMXN",
                   "USDZAR",
                   "USDCNH",
                   "XAUUSD",
                   "XAGUSD",
                   "XAUEUR"
                };

                Este array tiene una función muy importante de filtrado de archivos. En otras palabras, la plantilla solo cargará los gráficos y los ajustes correspondientes de estos que se encuentren en esta lista de símbolos. Aquí estamos obligados a fijar un par de reglas más, que funcionarán de la misma manera tanto al denominar archivos con configuraciones como al ajustar la lista especificada.

                1. Todos los nombres de instrumentos se convertirán a mayúsculas.
                2. Asimismo, se eliminarán todos los prefijos y postfijos de los instrumentos y solo quedarán los nombres verdaderos.

                Veamos ahora un ejemplo. Por ejemplo, el nombre del instrumento "EURUSD" en su bróker se escribe de la forma siguiente: "eurusd_". Esto significa que usted cambiará de todas formas el nombre del archivo de configuración utilizando "EURUSD", pero, además, en la configuración deberá hacer lo siguiente:

                //+------------------------------------------------------------------+
                //|                        symbol correction                         |
                //+------------------------------------------------------------------+
                input bool bToLowerE=true;//To Lower Symbol
                input string SymbolPrefixE="";//Symbol Prefix
                input string SymbolPostfixE="_";//Symbol Postfix

                En otras palabras, estará indicando a la plantilla que dentro del asesor, nuestros nombres se convertirán a su original convirtiendo los nombres a minúsculas y añadiendo el postfijo apropiado. Esto no ocurrirá muy a menudo, ya que los brókeres suelen ceñirse al clásico esquema de nomenclatura en mayúsculas y sin utilizar prefijos ni postfijos, sencillamente porque no tiene ningún sentido. Pero algunos pueden fantasear, así que esta opción no queda descartada.

                No obstante, solo hemos descubierto cómo nombrar el archivo para que la plantilla entienda que este es precisamente su ajuste "BotNick" y cómo asignarlo a un instrumento comercial correcto, pero todavía debemos asignar el ajuste a un periodo de gráfico específico. Para ello, hemos introducido la siguiente regla:

                • Después del nombre del archivo, colocaremos un espacio y escribiremos el equivalente de este periodo en minutos.
                • Se permiten periodos gráficos en el rango de M1..... H4.

                Creo que no resulta descabellado enumerar específicamente los periodos que entran dentro de esta horquilla. Para mí era muy importante que todos estos periodos estuvieran disponibles tanto en MetaTrader 4 como en MetaTrader 5: esta fue la razón principal para elegir los periodos. Otra razón muy importante fue que los periodos muy altos por encima de "H4" no suelen utilizarse en el trading automatizado de barras. En cualquier caso, no he visto ejemplos semejantes, por lo que la elección ha recaído en los siguientes periodos:

                • M1 - 1 minuto
                • M5 - 5 minutos
                • M15 - 15 minutos
                • M30 - 30 minutos
                • H1 - 60 minutos
                • H4 - 240 minutos

                Creo que a nadie le parecerá que esto es insuficiente y, además, para cada uno de estos periodos resulta muy sencillo calcular su equivalente en minutos: los números se recuerdan muy fácilmente. Ahora podremos mostrar la estructura general de la construcción del nombre de archivo, que usaremos para crear ajustes manuales o automáticos utilizando código de terceros o nuestra API. En primer lugar, echemos un vistazo al esquema general:

                • "INSTRUMENT" + " " + "PERIOD IN MINUTES" + " " + BotNick + ".txt"

                Además, quiero indicarles qué esquema utilizará la plantilla para duplicar estos archivos que están diseñados para cerrar posiciones:

                • "CLOSING" + " " + "INSTRUMENT" + " " + "PERIOD IN MINUTES" + " " + BotNick + ".txt"

                Como podemos ver, los dos archivos solo se diferenciarán por el prefijo "CLOSING" seguido de un espacio de separación. Obviamente, todas las líneas de señal del nombre estarán separadas por un espacio para que el intérprete de patrones pueda reconocerlas y extraerlas del nombre del archivo. Así, la pertenencia de un ajuste a cualquier gráfico vendrá determinada únicamente por su nombre. Veamos ahora algunos ejemplos de configuraciones que se construyen usando esta regla:

                • EURUSD 15 dynamictemplate.txt
                • GBPUSD 240 dynamictemplate.txt
                • EURCHF 60 dynamictemplate.txt
                • CLOSING GBPUSD 240 dynamictemplate.txt

                Claramente, eso es suficiente para un ejemplo. Le pido que preste especial atención al último nombre. Este archivo se copiará basándose en "GBPUSD 240 dynamictemplate.txt" y se colocará en la carpeta del terminal en la que el asesor ha realizado el copiado. Esto se hace para prevenir múltiples escrituras en los mismos archivos por asesores diferentes pero idénticos dentro de múltiples terminales. Si desactivamos la opción de leer desde la carpeta compartida del terminal, los archivos normales también se escribirán allí. Esto puede ser necesario si deseamos configurar cada copia específica del asesor en el terminal correspondiente con su propio conjunto independiente de ajustes. Dejaremos algunos archivos junto a las plantillas a modo de ejemplo, para que quede más claro en un ejemplo concreto, y para que usted pueda experimentar con ellos moviéndolos a diferentes carpetas. Con esto damos por concluidas las consideraciones generales sobre el uso de los ajustes.


                Métodos de lectura y creación de archivos

                Para dominar completamente la funcionalidad de la plantilla, es aconsejable entender cómo funcionan la lectura y escritura de archivos. Con el tiempo, esto nos permitirá empezar a utilizarlos no solo como marcadores de las herramientas-periodo a las que deseamos adjuntar robots virtuales, sino también para personalizar cada uno de ellos individualmente si lo deseamos. Para ello, podemos empezar a considerar el siguiente método.

                //+------------------------------------------------------------------+
                //|                 used for configuration settings                  |
                //+------------------------------------------------------------------+
                bool QuantityConfiguration()
                {
                    FilesGrab(); // Determine the names of valid files
                    
                    // Check if there are changes in the configuration settings (either add or delete)
                    if (bNewConfiguration())
                    {
                        return true;
                    }     
                    return false;
                }

                Este método solo determinará si se ha actualizado el conjunto de archivos de nuestro directorio de trabajo. Si es así, utilizaremos este método como señal para reiniciar todos los gráficos y asesores para añadir nuevas herramientas de periodo o eliminar las redundantes. Veamos ahora el método FilesGrab.

                //+------------------------------------------------------------------+
                //|   reads all files and forms a list of instruments and periods    |
                //+------------------------------------------------------------------+
                void FilesGrab()
                   {
                   string file;
                   string tempsubfolder= SubfolderE == "" ? ""  : SubfolderE + "\\"; // SubfolderE is the path to the specific subfolder
                   // Returns the handle of the first found file with the specified characteristics, based on whether CommonReadE is True or False
                   long total_files = !bCommonReadE? FileFindFirst(tempsubfolder+"*"+BotNick+".txt", file) :FileFindFirst(tempsubfolder+"*"+BotNick+".txt", file,FILE_COMMON);
                   if(total_files > 0)
                      {
                         ArrayResize(SettingsFileNames,0); // Clear the array from previous values if there are files to be read
                         do
                         {
                            int second_space = StringFind(file, " ", StringFind(file, " ") + 1); // Searches for the index of the second space in the file's name
                            if(second_space > 0) 
                            {
                                string filename = StringSubstr(file, 0, second_space); // Extracts the string/characters from the filename up to the second space
                                ArrayResize(SettingsFileNames, ArraySize(SettingsFileNames) + 1); // Increases the size of the array by one
                                SettingsFileNames[ArraySize(SettingsFileNames) - 1] = filename; // Adds the new filename into the existing array
                            }
                         }
                         while(FileFindNext(total_files, file)); // Repeat for all the files        
                         FileFindClose(total_files); // Close the file handle to free resources
                      }
                   }  

                Este método precisamente preensamblará los nombres de esos archivos que se refieren a nuestro asesor en algo como "EURUSD 60". En otras palabras, solo dejará la parte del nombre que más tarde se convertirá en un par herramienta-periodo. La lectura de estos archivos, sin embargo, no tendrá lugar aquí, sino dentro de cada asesor virtual por separado. Pero para ello, primero deberemos analizar la propia cadena en cuanto al símbolo y el periodo. Esto irá precedido de algunos momentos a considerar. Una de ellos es el siguiente.

                //+------------------------------------------------------------------+
                //|                        symbol validator                          |
                //+------------------------------------------------------------------+
                bool AdaptDynamicArrays()
                {
                    bool RR=QuantityConfiguration();
                    // If a new configuration of files is detected (new files, changed order, etc.)
                    if (RR)
                    {
                        // Read the settings (returns the count)
                        int Readed = ArraySize(SettingsFileNames); 
                        int Valid =0;
                
                        // Only valid symbol name needs to be populated (filenames are taken from already prepared array)
                        ArrayResize(S, Readed);
                     
                        for ( int j = 0; j < Readed; j++ )
                        {
                            for ( int i = 0; i < ArraySize(III); i++ )
                            {
                                // check the symbol to valid
                                if ( III[i] == BasicNameToSymbol(SettingsFileNames[j]) )
                                {
                                    S[Valid++]=SettingsFileNames[j];
                                    break; // stop the loop
                                }
                            }
                        } 
                        //resize S with the actual valid quantity
                        ArrayResize(S, Valid);
                        return true;
                    }
                    return false;
                }

                Este método es muy importante para descartar aquellas configuraciones (gráficos) que no estén presentes en nuestra lista de herramientas permitidas. La idea es sumar finalmente todos los gráficos que se han filtrado a través de la lista en un array "S", preparándolo para su posterior uso en código y crear objetos gráficos virtuales.

                Un punto importante es también la redundancia de ajustes, que se producirá continuamente a medida que se leen los ajustes principales. Si tenemos una posición abierta basada en la configuración básica, dejaremos de reservar periódicamente la configuración. Siempre se guardará un archivo de copia de seguridad denominado "CLOSING" en el directorio del terminal actual.

                //+------------------------------------------------------------------+
                //|         сopy settings from the main file to a CLOSING file       |
                //+------------------------------------------------------------------+
                void SaveCloseSettings()
                   {
                   string FileNameString=Charts[chartindex].BasicName;
                   bool bCopied;
                   string filenametemp;
                   string filename="";
                   long handlestart;   
                   
                   //Checking if SubfolderE doesn't exist, if yes, assign tempsubfolder to be an empty string
                   string tempsubfolder= SubfolderE == "" ? ""  : SubfolderE + "\\"; 
                
                   //Find the first file in the subfolder according to bCommonReadE and assign the result to handlestart 
                   if (bCommonReadE) handlestart=FileFindFirst(tempsubfolder+"*",filenametemp,FILE_COMMON);
                   else handlestart=FileFindFirst(tempsubfolder+"*",filenametemp);
                   
                   //Check if the start of our found file name matches FileNameString 
                   if ( StringSubstr(filenametemp,0,StringLen(FileNameString)) == FileNameString )
                      {
                      //if yes, complete the file's path 
                      filename=tempsubfolder+filenametemp;
                      }
                     //keep finding the next file while conditions are aligned  
                   while ( FileFindNext(handlestart,filenametemp) )
                      {
                      //if found file's name matches FileNameString then add found file's name to the path
                      if ( StringSubstr(filenametemp,0,StringLen(FileNameString)) == FileNameString )
                         {
                         filename=tempsubfolder+filenametemp;
                         break;
                         }
                      }   
                   //if handlestart is not INVALID_HANDLE then close the handle to release the resources after the search
                   if (handlestart != INVALID_HANDLE) FileFindClose(handlestart); 
                
                   //Perform file copy operation and notice if it was successful 
                   if ( bCommonReadE ) bCopied=FileCopy(filename,FILE_COMMON,tempsubfolder+"CLOSING "+FileNameString+".txt",FILE_REWRITE|FILE_TXT|FILE_ANSI);
                   else bCopied=FileCopy(filename,0,tempsubfolder+"CLOSING "+FileNameString+".txt",FILE_REWRITE|FILE_TXT|FILE_ANSI);
                   }

                Aquí vale la pena especificar que la configuración de la copia de seguridad funciona de tal forma que, por ejemplo, cuando la plantilla se reinicie o se vuelva a leer, los datos se leerán de ella. Esto solo será posible con una posición abierta correspondiente a este ajuste. Si un ejemplar concreto del asesor virtual no tiene posiciones abiertas, la plantilla se sincronizará con la configuración general.


                Creación de gráficos y asesores virtuales

                Acto seguido, trabajaremos con él y obtendremos todos los datos de ahí en el siguiente método, creando los gráficos virtuales necesarios en paralelo.

                //+------------------------------------------------------------------+
                //|                      creates chart objects                       |
                //+------------------------------------------------------------------+
                void CreateCharts()
                   {
                   bool bAlready;
                   int num=0;
                   string TempSymbols[];
                   string Symbols[];
                   ArrayResize(TempSymbols,ArraySize(S)); // Resize TempSymbols array to the size of S array
                   for (int i = 0; i < ArraySize(S); i++) // Populate TempSymbols array with empty strings
                      {
                      TempSymbols[i]="";
                      }
                   for (int i = 0; i < ArraySize(S); i++) // Count the required number of unique trading instruments
                      {
                      bAlready=false;
                      for (int j = 0; j < ArraySize(TempSymbols); j++)
                         {
                         if ( S[i] == TempSymbols[j] ) // If any symbol is already present in TempSymbols from S, then it's not unique
                            {
                            bAlready=true;
                            break;
                            }
                         }
                      if ( !bAlready ) // If the symbol is not found in TempSymbols i.e., it is unique, add it to TempSymbols
                         {
                         for (int j = 0; j < ArraySize(TempSymbols); j++)
                            {
                            if ( TempSymbols[j] == "" )
                               {
                               TempSymbols[j] = S[i];
                               break;
                               }
                            }
                         num++; // Increments num if a unique element is added          
                         }
                      }      
                   ArrayResize(Symbols,num); // Resize the Symbols array to the size of the num
                
                   for (int j = 0; j < ArraySize(Symbols); j++) // Now that the Symbols array has the appropriate size, populate it
                      {
                      Symbols[j]=TempSymbols[j];
                      } 
                   ArrayResize(Charts,num); // Resize Charts array to the size of num
                
                   int tempcnum=0;
                   tempcnum=1000; // Sets all charts to a default of 1000 bars
                   Chart::TCN=tempcnum; 
                   for (int j = 0; j < ArraySize(Charts); j++)
                      {
                      Charts[j] = new Chart();
                      Charts[j].lastcopied=0; // Initializes the array position where the last copy of the chart was stored
                      Charts[j].BasicName=Symbols[j]; 
                      ArrayResize(Charts[j].CloseI,tempcnum+2); // Resizes the CloseI array to store closing price of each bar
                      ArrayResize(Charts[j].OpenI,tempcnum+2); // Resizes the OpenI array for opening prices
                      ArrayResize(Charts[j].HighI,tempcnum+2); // HighI array for high price points in each bar
                      ArrayResize(Charts[j].LowI,tempcnum+2); // LowI array for low price points of each bar
                      ArrayResize(Charts[j].TimeI,tempcnum+2); // TimeI array is resized to store time of each bar
                      string vv = BasicNameToSymbol(Charts[j].BasicName); 
                      StringToLower(vv);
                      // Append prefix and postfix to the basic symbol name to get the specific symbol of the financial instrument 
                      Charts[j].CurrentSymbol = SymbolPrefixE +  (!bToLowerE ? BasicNameToSymbol(Charts[j].BasicName) : vv) + SymbolPostfixE;
                      Charts[j].Timeframe = BasicNameToTimeframe(Charts[j].BasicName); // Extracts the timeframe from the basic name string
                      }
                   ArrayResize(Bots,ArraySize(S)); // Resize Bots array to the size of S array
                   }

                Este método se centrará en la creación de una colección de periodos-herramienta no repetitivos, mediante los cuales se crearán los objetos de los gráficos correspondientes. Además, tenga en cuenta que el tamaño del array de barras para cada gráfico está fijado en algo más de 1000 barras. Creo que es más que suficiente para aplicar la mayoría de las estrategias. De ser necesario, esta cantidad puede cambiarse por la cantidad deseada. Ahora vamos a consolidar el material con un método que cree objetos de asesor virtuales.

                //+------------------------------------------------------------------+
                //|              attaching all virtual robots to charts              |
                //+------------------------------------------------------------------+
                void CreateInstances()
                   {
                   // iterating over the S array
                   for (int i = 0; i < ArraySize(S); i++)
                      {
                      // iterating over the Charts array
                      for (int j = 0; j < ArraySize(Charts); j++)
                         {
                         // checking if the BasicName of current Chart matches with the current item in S array
                         if ( Charts[j].BasicName == S[i] )
                            {
                            // creating a new Bot instance with indices i, j and assigning it to respective position in Bots array
                            Bots[i] = new BotInstance(i,j);
                            break;
                            } 
                         }
                      }
                   }

                Aquí se crearán los asesores virtuales y se colocarán sobre los gráficos correspondientes, guardando el identificador de este gráfico "j" dentro del asesor, para que en el futuro sepamos de qué gráfico tomar los datos dentro del asesor virtual. En cuanto a la estructura interna de estas dos clases, ya hemos hablado de ella en un artículo anterior. En muchos aspectos, el nuevo código será similar a éste, con algunos cambios que no son tan significativos como para ocupar un lugar destacado en el artículo.


                Lectura dinámica y reconfiguración de asesores virtuales

                En mi opinión, resulta obvio que esta sección es extremadamente importante para nosotros, ya que supone, como poco, la mitad de todo el concepto de la nueva plantilla. Después de todo, crear gráficos virtuales y asesores supone solo la mitad de la batalla. En líneas generales, ya nos hemos ocupado de recrear gráficos virtuales a un nivel mínimo, pero esto no es suficiente. Sería deseable entender cómo captar los nuevos ajustes sobre la marcha y reconfigurar al instante el asesor sin interferir con el terminal comercial. Para resolver este problema se usará un simple temporizador, como hemos dibujado en el esquema al principio de este artículo.

                //+------------------------------------------------------------------+
                //|             we will read the settings every 5 minutes +          |
                //+------------------------------------------------------------------+
                bool bReadTimer()
                   {
                   if (  TimeCurrent() - LastTime > 5*60 + int((double(MathRand())/32767.0) * 60) )
                      {
                      LastTime=TimeCurrent();
                      int orders=OrdersG();
                      bool bReaded=false;
                      if (orders == 0)  bReaded = ReadSettings(false,Charts[chartindex].BasicName);//reading a regular file
                      else bReaded = ReadSettings(true,Charts[chartindex].BasicName);//reading file to close position
                      if (orders == 0 && bReaded) SaveCloseSettings();//save settings for closing position
                      return bReaded;
                      }
                   return false;
                   }

                Como podemos ver, el temporizador se activará cada "5" minutos, lo cual significa que los archivos nuevos no se capturarán al instante. Pero creo que este momento resultará suficiente para aportar dinamismo. Si esto no le basta, podrá reducirlo a un segundo. Lo único que deberá tener en cuenta es que el uso frecuente de operaciones de archivo resulta desaconsejable y deberá evitarse siempre que sea posible. En este código, le pido que preste atención al método "ReadSettings". Este leerá el archivo requerido (para cada asesor virtual por separado) y luego reconfigurará el asesor después de la lectura. El método está construido para que pueda leer la configuración general (en caso de que no haya posiciones abiertas en el asesor virtual seleccionado) o pausar la actualización de la configuración y esperar a que la posición se cierre con los ajustes con los que se creó la posición.

                //+------------------------------------------------------------------+
                //|                        reading settings                          |
                //+------------------------------------------------------------------+
                bool BotInstance::ReadSettings(bool bClosingFile,string Path)
                   {
                   string FileNameString=Path;
                   int Handle0x;
                   string filenametemp;
                   string filename="";
                   long handlestart;
                            
                   string tempsubfolder= SubfolderE == "" ? ""  : SubfolderE + "\\"; 
                        
                   if (!bClosingFile)//reading a regular file
                      {
                      if (!bCommonReadE)
                         {
                         handlestart=FileFindFirst(tempsubfolder+"*",filenametemp);         
                         int SearchStart=0;
                         
                         if ( StringSubstr(filenametemp,SearchStart,StringLen(FileNameString)) == FileNameString )
                            {
                            filename=tempsubfolder+filenametemp;
                            }
                         if (filename != filenametemp || filename == "")
                            {
                            while ( FileFindNext(handlestart,filenametemp) )
                               {
                               if ( StringSubstr(filenametemp,SearchStart,StringLen(FileNameString)) == FileNameString )
                                  {
                                  filename=tempsubfolder+filenametemp;
                                  break;
                                  }
                               }         
                            }
                         if (handlestart != INVALID_HANDLE) FileFindClose(handlestart);// Release resources after search
                         
                         if (filename != "")
                            {
                            Handle0x=FileOpen(filename,FILE_READ|FILE_SHARE_READ|FILE_TXT|FILE_ANSI);
                            
                            if ( Handle0x != INVALID_HANDLE )//if the file exists
                               {
                               FileSeek(Handle0x,0,SEEK_SET);
                               ulong size = FileSize(Handle0x);
                               string str = "";
                               for(ulong i = 0; i < size; i++)
                                  {
                                     str += FileReadString(Handle0x);
                                  }
                               if (str != "" && str != PrevReaded)
                                  {
                                  FileSeek(Handle0x,0,SEEK_SET);
                                  //read the required parameters
                                  ReadFileStrings(Handle0x);
                                  //                     
                                  FileClose(Handle0x);
                                  LastRead = TimeCurrent();
                                  RestartParams();
                                  }
                               else
                                  {
                                  FileClose(Handle0x);
                                  }
                               return true;
                               }
                            else
                               {
                               return false;
                               }         
                            }         
                         }
                      else
                         {
                         handlestart=FileFindFirst(tempsubfolder+"*",filenametemp,FILE_COMMON);   
                         int SearchStart=0;
                         
                         if ( StringSubstr(filenametemp,SearchStart,StringLen(FileNameString)) == FileNameString )
                            {
                            filename=tempsubfolder+filenametemp;
                            }
                         if (filename != filenametemp || filename == "")
                            {
                            while ( FileFindNext(handlestart,filenametemp) )
                               {
                               if ( StringSubstr(filenametemp,SearchStart,StringLen(FileNameString)) == FileNameString )
                                  {
                                  filename=tempsubfolder+filenametemp;
                                  break;
                                  }
                               }         
                            }
                         if (handlestart != INVALID_HANDLE) FileFindClose(handlestart);// Release resources after search
                         
                         if (filename != "")
                            {
                            Handle0x=FileOpen(filename,FILE_READ|FILE_SHARE_READ|FILE_TXT|FILE_ANSI|FILE_COMMON);
                            
                            if ( Handle0x != INVALID_HANDLE )//if the file exists
                               {
                               FileSeek(Handle0x,0,SEEK_SET);
                               ulong size = FileSize(Handle0x);
                               string str = "";
                               for(ulong i = 0; i < size; i++)
                                  {
                                     str += FileReadString(Handle0x);
                                  }
                               if (str != "" && str != PrevReaded)
                                  {
                                  FileSeek(Handle0x,0,SEEK_SET);
                                  //read the required parameters
                                  ReadFileStrings(Handle0x);
                                  //      
                                  FileClose(Handle0x);
                                  LastRead = TimeCurrent();
                                  RestartParams();
                                  }
                               else
                                  {
                                  FileClose(Handle0x);
                                  }
                               return true;
                               }
                            else
                               {
                               return false;
                               }         
                            }         
                         }
                      }         
                   else//reading a file to close a position
                      {
                      handlestart=FileFindFirst(tempsubfolder+"*",filenametemp);   
                      int SearchStart=8;//when the line starts with "CLOSING "
                      
                      if ( StringLen(filenametemp) >= (8 + StringLen(FileNameString)) && StringSubstr(filenametemp,0,8) == "CLOSING " 
                      && StringSubstr(filenametemp,SearchStart,StringLen(FileNameString)) == FileNameString )
                         {
                         filename=tempsubfolder+filenametemp;
                         }
                      if (filename != filenametemp || filename == "")
                         {
                         while ( FileFindNext(handlestart,filenametemp) )
                            {
                            if ( StringLen(filenametemp) >= (8 + StringLen(FileNameString)) && StringSubstr(filenametemp,0,8) == "CLOSING " 
                            && StringSubstr(filenametemp,SearchStart,StringLen(FileNameString)) == FileNameString )
                               {
                               filename=tempsubfolder+filenametemp;
                               break;
                               }
                            }         
                         }
                      if (handlestart != INVALID_HANDLE) FileFindClose(handlestart);// Release resources after search
                      
                      if (filename != "")
                         {
                         Handle0x=FileOpen(filename,FILE_READ|FILE_SHARE_READ|FILE_TXT|FILE_ANSI);
                         
                         if ( Handle0x != INVALID_HANDLE )//if the file exists
                            {
                            FileSeek(Handle0x,0,SEEK_SET);
                            ulong size = FileSize(Handle0x);
                            string str = "";
                            for(ulong i = 0; i < size; i++)
                               {
                                  str += FileReadString(Handle0x);
                               }
                            if (str != "" && str != PrevReaded)
                               {
                               PrevReaded=str;
                               FileSeek(Handle0x,0,SEEK_SET);
                               //read the required parameters
                               ReadFileStrings(Handle0x);
                               //
                               FileClose(Handle0x);
                               LastRead = TimeCurrent();
                               RestartParams();                  
                               }
                            else
                               {
                               FileClose(Handle0x);
                               }
                            return true;
                            }
                         else
                            {
                            return false;
                            }         
                         }         
                      }         
                   return false;   
                   }

                En primer lugar, quiero destacar que este método solo sirve para leer dos tipos de archivos. Dependiendo del token "bClosingFile" transmitido, se leerá la configuración general o la configuración "para el cierre". Cada lectura de archivos constará de varios pasos:

                1. Comparación del contenido del archivo leído anteriormente y el actual;
                2. Si el contenido es diferente, leeremos nuestra configuración ya actualizada;
                3. Reinicio de nuestro asesor virtual con la nueva configuración, si es necesario.

                El método está construido de tal forma que la limpieza de recursos y otras acciones ya han sido pensadas, solo deberemos implementar el siguiente método, que simplemente se llamará en el anterior. He hecho todo lo posible por ahorrarle las molestias de estas operaciones con archivos para que pueda concentrarse al máximo en escribir exactamente el código de lectura que necesite. Efectúe la lectura aquí.

                //+------------------------------------------------------------------+
                //|               read settings from file line by line               |
                //+------------------------------------------------------------------+
                void BotInstance::ReadFileStrings(int handle)
                   {
                   //FileReadString(Handle,0);
                   
                   }

                Aquí no será necesario abrir el archivo ni cerrarlo. Todo lo que deberá hacer es leer el archivo línea por línea y poner correctamente lo que lea en las variables correspondientes. Para ello, podrá utilizar variables temporales o registrar inmediatamente todos los datos en las variables que utilizará como configuración de su estrategia. Pero yo le recomendaría rellenar los ajustes ya en este método, que está diseñado para dicho propósito.

                //+------------------------------------------------------------------+
                //|                function to prepare new parameters                |
                //+------------------------------------------------------------------+
                void BotInstance::RestartParams() 
                {
                   //additional code
                
                   //
                   MagicF=SmartMagic(BasicNameToSymbol(Charts[chartindex].BasicName), Charts[chartindex].Timeframe);
                   CurrentSymbol=Charts[chartindex].CurrentSymbol;
                   m_trade.SetExpertMagicNumber(MagicF);
                }

                Las tres últimas líneas no hay que tocarlas, su presencia es obligatoria. Lo más interesante es el método "SmartMagic", diseñado para asignar automáticamente números mágicos a cada uno de los asesores virtuales. Todo lo que necesita saber en este momento es que deberá escribir esta lógica de reasignación de configuraciones del asesor en el bloque vacío de arriba. Si lo necesita, también podrá ocuparse de recrear los indicadores y cualquier otra cosa que pueda haber ahí.


                Generación automática de números mágicos

                Sin apartarnos del método anterior, quiero revelarle de inmediato una forma de generar IDs de órdenes únicas que hacen posible el trading independiente para todos los asesores virtuales dentro de la plantilla. Para ello, hemos utilizado la siguiente técnica. 

                Asignaremos un paso de "10000", por ejemplo, entre los números mágicos más cercanos. Para cada instrumento, primero fijaremos su número mágico preliminar, digamos "10000" o "70000". Sin embargo, eso no será suficiente, porque no solo tenemos la herramienta, sino que también hay un periodo. Por ello, añadiremos otro número a este número mágico intermedio. 

                La forma más sencilla será añadir el equivalente en minutos de estos periodos, igual que en la estructura de lectura de archivos. Se hace así.

                //+------------------------------------------------------------------+
                //|              Smart generation of magical numbers                 |
                //|    (each instrument-period has its own fixed magic number)       |
                //+------------------------------------------------------------------+
                int BotInstance::SmartMagic(string InstrumentSymbol,ENUM_TIMEFRAMES InstrumentTimeframe)
                {
                   // initialization
                   int magicbuild=0;
                   
                   // loop through the array
                   for ( int i=0; i<ArraySize(III); i++ )
                   {
                      // check the symbol to assign a magic number
                      if ( III[i] == InstrumentSymbol )
                      {
                          magicbuild=MagicHelp+(i+1)*10000;
                          break; // stop the loop
                      }
                   }  
                   
                   // add identifier for time frame    
                   magicbuild+=InstrumentTimeframe;      
                   return magicbuild;
                }

                Aquí es donde aparecerá nuestro desplazamiento adicional de números mágicos para conjuntos únicos de números mágicos, pero entre diferentes asesores dentro del terminal.

                //+------------------------------------------------------------------+
                //|               unique shift for difference of EA                  |
                //+------------------------------------------------------------------+
                #define MagicHelp 0 //bot magic shift [0...9999]

                En general, es bastante sencillo. El gran paso entre números mágicos proporcionará bastantes opciones de cambio, lo cual será más que suficiente para crear el número adecuado de asesor. La única condición para usar este sistema será no superar el número "9999". Además, tenemos que controlar que el desplazamiento no coincida con nuestros equivalentes en minutos, ya que en este caso podrían coincidir los números mágicos en dos plantillas diferentes. Para no tener que pensar en estas opciones, bastará con hacer un desplazamiento ligeramente mayor que el número "240", por ejemplo, "241", "241*2", "241*3", "241*N".

                Para resumir este enfoque, podemos ver que este esquema se deshace completamente del ajuste de números mágicos, que era uno de los objetivos intermedios tácitos de esta solución. El único inconveniente de este esquema es la imposibilidad de conectar dos o más asesores virtuales independientes, ya que sus números mágicos coincidirán, lo cual terminará provocando la interacción de estas estrategias y, como consecuencia, la ruptura de su lógica. Pero en realidad, no sé quién querría algo tan exótico. Y en general, francamente, no entra dentro de la funcionalidad que pretendíamos originalmente. Tal vez añadamos esto en el próximo artículo si hay interesados.


                Sistema de normalización de volúmenes

                Si ya hemos concebido una plantilla sencilla y fácil de configurar, entonces será crucial elegir el método adecuado para establecer los volúmenes comerciales. Es muy curioso que, a la hora de poner en práctica una igualación sencilla y eficaz de los volúmenes, me hayan ayudado las conclusiones finales de mis artículos sobre teoría de probabilidades, concretamente de este artículo. Hemos decidido igualar los volúmenes asumiendo que la duración media y el tamaño de la magnitud absoluta del resultado financiero final de la posición deberían ser similares, y por lo tanto las siguientes consideraciones deberían ser la única solución verdadera para dicha igualación.

                La tasa media de aumento o disminución de la equidad del gráfico comercial final deberá ser ofrecida por igual por cada uno de los asesores (herramienta-periodo independiente) incluidos en el conjunto final. Todas las magnitudes necesarias deberán calcularse sin utilizar los datos de aquellos periodos-instrumento que no aparezcan en la lista de nuestros gráficos virtuales. Los volúmenes deberán distribuirse no solo en proporción al número de asesores, sino también en proporción al depósito (lote automático).

                Para ello, será importante introducir las siguientes definiciones y fórmulas. Para empezar, hemos introducido estos parámetros para el ajuste del riesgo:

                • DeltaBarPercent - porcentaje de DepositDeltaEquity,
                • DepositDeltaEquity - depósito de un bot para calcular su delta de equidad permitida para una barra M1 en una posición abierta.

                Son términos bastante difusos, pero ahora quedarán más claros. Para mayor comodidad, estableceremos el depósito con el que trabajará un asesor virtual independiente, y luego especificaremos en tanto por ciento, qué parte de este depósito deberá aumentar o disminuir (ya como equidad nuestra), si abrimos una posición y se ha dado un movimiento desde el punto superior de la barra "M1" al inferior o viceversa.

                La tarea de nuestro código consistirá en elegir automáticamente los volúmenes de entrada dados nuestros requisitos. Para lograrlo, necesitaremos magnitudes matemáticas adicionales y fórmulas construidas sobre ellas. No vamos a extraer conclusiones, solo le ofrecemos las magnitudes para que se aclare con el código:

                • "Mb" - tamaño medio de las barras (en puntos) en el rango histórico seleccionado con el tamaño "bars" en el gráfico de trabajo del asesor,
                • "Mb1" - tamaño medio de las barras (en puntos) en el rango histórico seleccionado con el tamaño "bars" en el gráfico de trabajo del asesor, recalculado a M1,
                • "Kb" - coeficiente de conexión entre el tamaño medio de la barra del gráfico actual y su equivalente "M1",
                • "T" - periodo del gráfico seleccionado, reducido al equivalente en minutos (al igual que teníamos en nuestros archivos),
                • "BasisI" - aumento o disminución medio requerido de la línea de equidad en la divisa de depósito para el tamaño medio de la vela M1 del gráfico del instrumento seleccionado,
                • "Basisb" - aumento o disminución medio real de la línea de equidad en la divisa de depósito para el tamaño medio de la vela M1 del gráfico del instrumento seleccionado, para un tamaño de operación de "1" lote,
                • "Lote" - lote (volumen) seleccionado.

                Ahora que ya hemos enumerado todas las magnitudes que se utilizan en el cálculo, vamos a desglosarlas y a darles sentido. Comenzaremos por orden. Para calcular el lote necesario, primero deberemos comprender cómo se relacionan los tamaños de las barras en los marcos temporales superiores respecto a "M1". La siguiente fórmula nos servirá de ayuda.

                Factor de escalado para pasar a M1

                Esta es exactamente la expresión que permite, sin cargar datos de "M1", calcular la misma característica, pero sobre el periodo presentado del gráfico virtual con el que trabaja el ejemplar seleccionado del asesor virtual. Tras multiplicar por este coeficiente, obtendremos casi los mismos datos que si los hubiéramos calculado sobre el periodo "M1". Eso es lo primero que deberemos hacer. El método para calcular esta magnitud será el siguiente:

                //+------------------------------------------------------------------+
                //|       timeframe to average movement adjustment coefficient       |
                //+------------------------------------------------------------------+
                double PeriodK(ENUM_TIMEFRAMES tf)
                   {
                   double ktemp;
                   switch(tf)
                      {
                      case  PERIOD_H1:
                          ktemp = MathSqrt(1.0/60.0);
                          break;
                      case  PERIOD_H4:
                          ktemp = MathSqrt(1.0/240.0);
                          break;
                      case PERIOD_M1:
                          ktemp = 1.0;
                          break;
                      case PERIOD_M5:
                          ktemp = MathSqrt(1.0/5.0);
                          break;
                      case PERIOD_M15:
                          ktemp = MathSqrt(1.0/15.0);
                          break;
                      case PERIOD_M30:
                          ktemp = MathSqrt(1.0/30.0);
                          break;
                      default: ktemp = 0;
                      }
                   return ktemp;
                   }

                Ahora, obviamente, tendremos que averiguar qué magnitud estamos adaptando a "M1". Ahí la tenemos.

                En otras palabras, calcularemos el tamaño medio de las velas en pips en el gráfico con el que trabaja el asesor virtual. Una vez calculada esta magnitud, deberemos convertirla utilizando la magnitud anterior de la siguiente manera.

                Ambas acciones se producirán en el siguiente método.

                //+------------------------------------------------------------------+
                //|     average candle size in points for M1 for the current chart   |
                //+------------------------------------------------------------------+
                double CalculateAverageBarPoints(Chart &Ch)
                   {
                   double SummPointsSize=0.0;
                   double MaxPointSize=0.0;
                   for (int j = 0; j < ArraySize(Ch.HighI); j++)
                      {
                      if (Ch.HighI[j]-Ch.LowI[j] > MaxPointSize) MaxPointSize= Ch.HighI[j]-Ch.LowI[j];
                      }
                        
                   for (int j = 0; j < ArraySize(Ch.HighI); j++)
                      {
                      if (Ch.HighI[j]-Ch.LowI[j] > 0) SummPointsSize+=(Ch.HighI[j]-Ch.LowI[j]);
                      else SummPointsSize+=MaxPointSize;
                      }  
                   SummPointsSize=(SummPointsSize/ArraySize(Ch.HighI))/Ch.ChartPoint;
                   return PeriodK(Ch.Timeframe)*SummPointsSize;//return the average size of candles reduced to a minute using the PeriodK() adjustment function
                   }

                Ahora podremos utilizar la magnitud obtenida, reducida a M1, para realizar el cálculo de la variable "Basisb". Deberá calcularse del siguiente modo.

                Por si alguien no lo sabe, el tamaño del tick será la magnitud de cambio en la equidad de una posición abierta con un volumen de "1" lote, cuando el precio se mueve en "1" punto. Si lo multiplicamos por el tamaño medio de una vela de un minuto, obtendremos cuánto cambiará la equidad de una posición con un solo lote, pero considerando que el tamaño del movimiento se ha igualado al tamaño medio de una barra de un minuto. A continuación, deberemos calcular el magnitud restante "BasisI", y lo haremos de esta manera.

                El interés y el depósito en ella serán exactamente los parámetros de control según los cuales requeriremos la base que necesitamos. Lo único que deberemos hacer es elegir un coeficiente de proporcionalidad para que las bases sean iguales, y ese factor será nuestro lote final.

                Todas las operaciones descritas se realizarán según el método siguiente.

                //+------------------------------------------------------------------+
                //|    calculate the optimal balanced lot for the selected chart     |
                //+------------------------------------------------------------------+
                double OptimalLot(Chart &Ch)
                   {
                   double BasisDX0 =  (DeltaBarPercent/100.0) * DepositDeltaEquityE;
                   double DY0=CalculateAverageBarPoints(Ch)*SymbolInfoDouble(Ch.CurrentSymbol,SYMBOL_TRADE_TICK_VALUE);
                   return BasisDX0/DY0;
                   }

                De este modo, igualaremos de forma efectiva los volúmenes de todos los instrumentos para contribuir por igual a la equidad de todos los asesores virtuales. Tendremos una especie de modo de lote fijo, como si lo estableciéramos por separado para cada asesor, pero en este caso hemos evitado esta necesidad. Sí, se trata de un sistema de control del riesgo completamente diferente, adaptado a la negociación diversificada. Usándolo, tendrá que hacerlo de todos modos si quiere conseguir una estabilidad óptima de la curva de beneficios y librarse de esta rutina. De hecho, el equilibrio entre varios asesores será el punto más delicado. Los que se han dedicado a este tipo de cosas creo que apreciarán la decisión. En cualquier caso, si este método de equilibrado no le conviene, siempre podrá escribir su propia configuración en los archivos correspondientes y rehacer este sistema o modificarlo.


                Lote automático

                El lote normalizado puede aumentar en proporción al depósito. Para entender cómo, introduciremos las siguientes notaciones:

                • - Lot - lote normalizado (equilibrado) para un asesor virtual específico;
                • - AutoLot - "Lot" recalculado respecto al depósito para el modo AutoLot (necesitaremos obtenerlo cuando AutoLot está activado);
                • - DepositPerOneBot - parte del depósito actual (parte de Deposit), que solo puede ser gestionada por uno de los asesores virtuales;
                • - DepositDeltaEquity - depósito con respecto al cual realizaremos la normalización (lotes equilibrados);
                • - Deposit - nuestro banco actual;
                • - BotQuantity - número actual de asesores virtuales que funcionan dentro del multibot.

                Después podemos escribir a qué es igual nuestro AutoLot:

                • AutoLot = Lot * (DepositPerOneBot / DepositDeltaEquity).

                Resulta que en el caso del volumen normalizado habitual, ignoraremos nuestro depósito y, por así decirlo, aceptaremos que a un asesor virtual se le asigne este depósito - "DepositDeltaEquity". Pero en el caso de un lote de automóviles, este depósito no será un depósito real y deberemos modificar proporcionalmente los volúmenes normalizados para que nuestros riesgos se adapten al depósito real. Sin embargo, deberemos adaptarnos considerando que un asesor virtual representará solo una parte del depósito.

                • DepositPerOneBot = Deposit / BotQuantity.

                Así es como está dispuesto el lote automático en mi plantilla. Creo que este enfoque resulta bastante cómodo y ofrece el ajuste necesario para la inclinación de la curva de crecimiento exponencial. No vamos a mostrar el código; si le interesa, podrá encontrarlo usted mismo en el código fuente. No obstante, podemos mostrarle el resultado del ajuste correcto de estas magnitudes.

                lote automático con volúmenes normalizados

                Tenga en cuenta que la curva de beneficios en este modo, siempre que los ajustes sean correctos y la señal inicial sea rentable, será aproximadamente la siguiente. Esta curva será más suave y de tipo exponencial cuanto mayor sea el factor de beneficio de nuestra estrategia y cuantas más operaciones de la misma caigan en su sección de pruebas. Esto se consigue muy bien usando la diversificación. Nuestra plantilla tiene lo básico para maximizar este efecto. Además, preste atención a la carga del depósito: su suavidad y uniformidad indican indirectamente que la normalización y el posterior escalado de la carga al depósito actual son correctos. Este ejemplo lo tomé de mi producto, que está construido sobre la plantilla en cuestión.


                Sincronización con API

                Esta función no es obligatoria y puede desactivarse o eliminarse fácilmente de la plantilla, pero a mí personalmente me ha resultado muy útil para mi producto. Como ya hemos dicho, la sincronización también se activa mediante un temporizador.

                //+------------------------------------------------------------------+
                //|            used to read the settings every 5 minutes +           |
                //+------------------------------------------------------------------+
                void DownloadTimer()
                {
                    // Check if the passed time from the last download time is more than 5 minutes
                    if (  TimeCurrent() - LastDownloadTime > 5*60 + int((double(MathRand())/32767.0) * 60) )
                    {
                        // Set the last download time to the current time
                        LastDownloadTime=TimeCurrent();
                        // Download files again
                        DownloadFiles();
                     }
                } 

                Vamos a echar ahora un vistazo al método principal "DownloadFiles".

                //+------------------------------------------------------------------+
                //|       used to download control files if they isn't present       |
                //+------------------------------------------------------------------+
                void DownloadFiles()
                {
                    string Files[];
                    // Initialize the response code by getting files from the signal directory
                    int res_code=GetFiles(SignalDirectoryE,Files); 
                
                    // Check if the list of files is successfully got
                    if (res_code == 200)
                    {
                        // Proceed if there is at least one file in the server
                        if (ArraySize(Files) > 0)
                        {
                            // Download each file individually
                            for (int i = 0; i < ArraySize(Files); i++)
                            {
                                string FileContent[];
                                // Get the content of the file
                                int resfile =  GetFileContent(SignalDirectoryE,Files[i],FileContent);
                
                                // Check if the file content is successfully got
                                if (resfile == 200)
                                {
                                    // Write the file content in our local file
                                    WriteData(FileContent,Files[i]);
                                }
                            }
                        }
                    }
                }

                Hemos organizado toda la estructura de tal manera que lo primero que hay que hacer es llamar a la API para averiguar la lista completa de archivos que se encuentran en la carpeta especificada de nuestro servidor. Esta asignará los ajustes. El nombre de la carpeta es "SignalDirectoryE". También será el nombre de la señal en mi diseño. Una vez obtenida la lista de archivos, procederemos a descargar cada uno de ellos por separado. En mi opinión, esta lógica de construcción resulta bastante cómoda. De este modo, tendremos la posibilidad de crear muchas señales (carpetas) entre las que podremos alternar en cualquier momento. Cómo hacer y organizar esto dependerá de usted: mi tarea consiste en ofrecer una funcionalidad operativa para lograr una conexión cómoda. Ahora echaremos un vistazo a la plantilla del método que obtiene una lista con los nombres de archivos de nuestro servidor.

                //+------------------------------------------------------------------+
                //|              getting the list of files into an array             |
                //+------------------------------------------------------------------+
                int GetFiles(string directory,string &fileArray[])
                   {
                   //string for getting a list of files in the form of JSON via GET to API 
                   string urlList = ApiDomen+"/filelist/"+directory;//URL
                   char message[];//Body of the request
                   string headers = "Password_key: " + key;// We form the headers of the request
                   string resultheaders = "";//returning headers          
                   string cookie = "";//cookies
                   int timeout = 1500;//waiting for a response when requesting a file or json
                   char result[];
                   
                   // We send a GET request to the server to receive JSON with a list of files
                   int res_code =  WebRequest("GET", urlList, headers, timeout, message, result, resultheaders);
                   bool rez = extractFiles(CharArrayToString(result),fileArray);
                   if (rez) return res_code;
                   else return 400;
                   }

                Todo lo que deberemos hacer aquí es formar nuestra cadena "URL" de forma similar. Lo más importante aquí serán estas partes (líneas):

                • filelist
                • Password_key

                La primera línea será una de nuestra funciones "API", que podremos nombrar de otra manera; también podrá dejar el nombre que le he sugerido. Tendrá varias funciones de este tipo. Por ejemplo, para realizar las siguientes operaciones:

                1. Cargar archivos de configuración en su API (desde su aplicación o programa autónomo).
                2. Borrar archivos de configuración en su API (desde su aplicación o programa autónomo).
                3. Carga listas de archivos desde un directorio existente (de su asesor).
                4. Cargar el contenido de un archivo (de su asesor).

                Puede haber otras funciones que necesite, pero solo las dos últimas serán necesarias en el asesor. La segunda línea será uno de los encabezados "headers". También tendremos que generarlo, basándonos en lo que transmitamos a la API. Por regla general, bastará con una clave de acceso para evitar que todo el mundo pueda entrar, pero también podremos añadir más si necesitamos transmitir alguna información adicional. El análisis de la cadena obtenida del servidor deberá hacerse aquí.

                //+------------------------------------------------------------------+
                //|                  get file list from JSON string                  |
                //+------------------------------------------------------------------+
                bool extractFiles(string json, string &Files[])
                   {
                
                   return false;
                   }

                Obtendremos "JSON" y lo parsearemos en nombres. Por desgracia, no existe un código de análisis sintáctico universal, ya que se individualiza en cada caso. Personalmente, he escrito un código propio para el análisis y no diría que resulta demasiado complicado, ni tampoco que es demasiado largo. Obviamente, sería positivo que usted tuviera algunas bibliotecas que hagan esto, pero personalmente prefiero escribir la mayor parte del código por mí mismo. Veamos ahora un método similar que recupera el contenido de un archivo.

                //+------------------------------------------------------------------+
                //|                    getting the file content                      |
                //+------------------------------------------------------------------+
                int GetFileContent(string directory,string filename,string &OutContent[])
                   {
                   //string for getting a file content in the form of JSON via GET to API 
                   string urlList = ApiDomen+"/file_content/"+directory+"/"+filename;//
                   char message[];// Body of the request
                   string headers = "Password_key: " + key;// We form the headers of the request
                   string resultheaders = "";//returning headers             
                   string cookie = "";//cookies
                   int timeout = 1500;//waiting for a response when requesting a file or json
                   char result[];
                   
                   // We send a GET request to the server to receive JSON with a file content
                   int res_code =  WebRequest("GET", urlList, headers, timeout, message, result, resultheaders);
                   bool rez = extractContent(CharArrayToString(result),OutContent);
                   if (rez) return res_code;
                   else return 400;
                   } 

                Todo será exactamente igual que en el anterior, salvo que no obtendremos una lista de archivos, sino una lista de cadenas que estarán dentro del archivo. Y por supuesto, analizar el "JSON" con el contenido del archivo línea por línea debería suponer una lógica aparte en el siguiente método, que tendrá un propósito idéntico al de su compañero "extractFiles".

                //+------------------------------------------------------------------+
                //|   read the contents of the file from JSON each line separately   |
                //+------------------------------------------------------------------+ 
                bool extractContent(string json, string &FileLines[])
                   {
                   
                   return false;
                   }

                Sí, claro que no es necesario hacerlo todo exactamente como yo digo, la cuestión es que ya tengo un producto que está construido exactamente así y con un ejemplo concreto de funcionamiento, me resulta mucho más fácil entenderlo todo. Después de obtener el contenido del archivo, podremos escribir de forma segura línea por línea utilizando el siguiente método, que en realidad ya está integrado en la lógica de la plantilla.

                //+-----------------------------------------------------------------------+
                //|    fill the file with its lines, which are all contained in data      |
                //|  with a new line separator, and save in the corresponding directory   |
                //+-----------------------------------------------------------------------+
                void WriteData(string &data[],string FileName)
                   {
                   int fileHandle;
                   string tempsubfolder= SubfolderE == "" ? ""  : SubfolderE + "\\"; 
                   
                   if (!bCommonReadE)
                      {
                      fileHandle = FileOpen(tempsubfolder+FileName,FILE_REWRITE|FILE_WRITE|FILE_TXT|FILE_ANSI);
                      }
                   else
                      {
                      fileHandle = FileOpen(tempsubfolder+FileName,FILE_REWRITE|FILE_WRITE|FILE_TXT|FILE_ANSI|FILE_COMMON);
                      } 
                      
                   if(fileHandle != INVALID_HANDLE) 
                       {
                       FileSeek(fileHandle,0,SEEK_SET);
                       for (int i=0; i < ArraySize(data) ; i++)
                          {
                          FileWriteString(fileHandle,data[i]+"\r\n");
                          }
                       FileClose(fileHandle);
                       }
                   }
                
                

                Así se creará un archivo según su nombre. Y, por supuesto, su contenido también se escribirá en él como cadenas independientes. Para archivos pequeños, esta será una solución perfectamente adecuada. No he notado ninguna molestia ni ralentización.


                Método general con la lógica comercial

                Ya hemos tratado este tema en el artículo anterior, pero me gustaría destacarlo una vez más y recordarle que este método se activará al abrirse una nueva barra en cada asesor virtual. Podemos pensar en ello como un manejador del tipo "OnTick", pero en nuestro caso será por supuesto "OnBar". Por cierto, no existe tal manejador en MQL5. Su rendimiento resulta un poco bajo, pero en realidad no tiene un impacto significativo en el comercio de barras, por lo que será la menor de nuestras preocupaciones.

                //+------------------------------------------------------------------+
                //|      the main trading function of individual robot instance      |
                //+------------------------------------------------------------------+
                void BotInstance::Trade() 
                {
                   //data access
                   
                   //Charts[chartindex].CloseI[0]//current bar (zero bar is current like in mql4)
                   //Charts[chartindex].OpenI[0]
                   //Charts[chartindex].HighI[0]
                   //Charts[chartindex].LowI[0]
                   //Charts[chartindex]. ???
                   
                   //close & open
                   
                   //CloseBuyF();
                   //CloseSellF();
                   //BuyF();
                   //SellF();   
                
                   // Here we can include operations such as closing the buying position, closing selling position and opening new positions.
                   // Other information from the chart can be used for making our buying/selling decisions.
                   
                   // Here is a simple trading logic example
                   if ( Charts[chartindex].CloseI[1] > Charts[chartindex].OpenI[1] )
                   {
                      CloseBuyF();
                      SellF();
                   }
                   if ( Charts[chartindex].CloseI[1] < Charts[chartindex].OpenI[1] )
                   {
                      CloseSellF();
                      BuyF();
                   }      
                } 

                Dentro de la plantilla, hemos implementado la lógica más simple para que pueda construir la suya propia usándola como ejemplo. En la clase "BotInstance", sería aconsejable que añadiese su propia lógica y variables junto a este método para no confundirse. Le recomiendo que construya su lógica usando los métodos y variables que ya utilizará en el método principal "Trade".


                Interfaz gráfica

                En la plantilla, al igual que sucede en la versión anterior, hay un ejemplo de interfaz de usuario sencilla, cuyo esquema de colores y contenido podrá modificar, si así lo desea. Esta interfaz será idéntica para las plantillas de MetaTrader 4 y MetaTrader 5, y tendrá mejor aspecto.

                interfaz gráfica

                Donde vea signos de interrogación, podrá añadir información adicional o eliminar bloques innecesarios. Es muy fácil de hacer. Existen dos métodos para trabajar con la interfaz: "CreateSimpleInterface" y "UpdateStatus". Ambos son muy simples, así que no los mostraremos en acción. Podrá encontrarlos por sus respectivos títulos.

                Hemos añadido tres campos muy útiles a esta interfaz. Si se fija en las tres últimas líneas, estas muestran su "corredor de números mágicos reservados", que es relevante para la configuración actual que está utilizando. Si elimina o añade archivos de configuración, este corredor se estrechará o ensanchará en consecuencia. Además, necesitaremos proteger de algún modo los distintos asesores contra los conflictos, y este campo nos ayudará a ello. Los dos campos restantes indicarán cuándo se ha leído por última vez cualquiera de los ajustes y la hora de la última sincronización con nuestra API, si es que se produce una sincronización.


                Conclusión

                En este artículo hemos obtenido un modelo de plantilla más cómodo y funcional que, entre otras cosas, resultará apto para futuras ampliaciones y modificaciones. Sí, el código aún está lejos de ser perfecto, y todavía hay mucho que optimizar y arreglar, pero incluso con todo eso en mente, ya tenemos algunas ideas bien formadas sobre qué añadir y con qué propósito hacerlo. Para el próximo artículo, queremos virtualizar las posiciones y, basándonos en estos datos, obtener un optimizador único de divisas cruzadas que optimizará cada uno de nuestros asesores individualmente y generará archivos de configuración listos para nuestras estrategias.

                El optimizador de divisas cruzadas nos permitirá realizar una optimización simultánea de todos los instrumentos-periodos virtuales. Fusionando todos estos ajustes, obtendremos una curva de beneficios mejor y más segura con un riesgo reducido. A mi juicio, la diversificación automática y el aumento de los beneficios son una prioridad absoluta para seguir mejorando. En resumen, querríamos tener algún tipo de complemento básico para cualquier estrategia, que nos permita maximizar el beneficio de la misma, manteniendo la máxima funcionalidad y comodidad para el usuario final. Digamos que sería una especie de exoesqueleto para nuestra señal comercial.

                Enlaces

                Traducción del ruso hecha por MetaQuotes Ltd.
                Artículo original: https://www.mql5.com/ru/articles/14251

                Archivos adjuntos |
                DynamicTemplate.zip (45.56 KB)
                Características del Wizard MQL5 que debe conocer (Parte 15): Máquinas de vectores de soporte utilizando el polinomio de Newton Características del Wizard MQL5 que debe conocer (Parte 15): Máquinas de vectores de soporte utilizando el polinomio de Newton
                Las máquinas de vectores de soporte clasifican los datos en función de clases predefinidas explorando los efectos de aumentar su dimensionalidad. Se trata de un método de aprendizaje supervisado bastante complejo dado su potencial para tratar datos multidimensionales. Para este artículo consideramos cómo su implementación muy básica de datos bidimensionales puede hacerse más eficientemente con el polinomio de Newton al clasificar precio-acción.
                Desarrollo de asesores expertos autooptimizantes en MQL5 Desarrollo de asesores expertos autooptimizantes en MQL5
                Construya asesores expertos que miren hacia delante y se ajusten a cualquier mercado.
                Desarrollando un cliente MQTT para MetaTrader 5: un enfoque TDD - Final Desarrollando un cliente MQTT para MetaTrader 5: un enfoque TDD - Final
                Este artículo es la última parte de una serie que describe nuestros pasos de desarrollo de un cliente MQL5 nativo para el protocolo MQTT 5.0. Aunque la biblioteca aún no está lista para la producción, en esta parte utilizaremos nuestro cliente para actualizar un símbolo personalizado con ticks (o precios) procedentes de otro broker. Por favor, consulte la parte inferior de este artículo para obtener más información sobre el estado actual de la biblioteca, lo que falta para que sea totalmente compatible con el protocolo MQTT 5.0, una posible hoja de ruta, y cómo seguir y contribuir a su desarrollo.
                Operar con noticias de manera sencilla (Parte 1): Creando una base de datos Operar con noticias de manera sencilla (Parte 1): Creando una base de datos
                Operar con noticias puede ser complicado y abrumador, en este artículo repasaremos los pasos para obtener datos de noticias. Además, conoceremos el calendario económico de MQL5 y lo que ofrece.