English Русский Español Deutsch 日本語 Português
preview
MetaTrader 中的 Multibot(第二部分):改进的动态模板

MetaTrader 中的 Multibot(第二部分):改进的动态模板

MetaTrader 5示例 | 4 四月 2025, 07:50
419 0
Evgeniy Ilin
Evgeniy Ilin

目录


概述

在上一篇文章中,我受到了市场上一些最受欢迎的解决方案的启发,我能够创建自己版本的模板。但是,考虑到我正在进行的一些项目,事实证明这个解决方案并不是最优的。此外,它仍然存在许多与这种模板的整体架构相关的局限性和不便。这样的模板可能足以用于绝大多数普通解决方案,但不适用于我的解决方案。第二个非常重要的一点是,从潜在 EA 买家和最终用户的角度来看,我个人希望看到最大的简单性和最少的设置。理想情况下,这样的 EA 应该不需要用户进行重新优化和其他操作。毕竟,我花钱不仅是为了一个可行的解决方案,更重要的是,为了一个尽可能友好、每个人都能理解的界面。


动态模板概念

基于之前的 EA,让我们回顾一下创建这样一个模板的原因。主要目标是能够为每个“工具 - 周期”对启动一个具有不同设置的 EA。为了避免在每个图表上单独启动 EA,这是必要的。这简化了这种系统及其配置的管理,因为一个终端中相同 EA 的副本越多,潜在用户出错的可能性就越高。此外,由于例如幻数顺序或基于人为因素的其他原因,这些 EA 之间可能会出现冲突。让我们首先描述第一个模板的优缺点。然后,我将对新模板进行同样的操作,以便清楚地看到新方法的差异和优势。

静态(基本)模板优点:

  1. 每种工具的单独设置 - 周期(虚拟图表上的虚拟 EA)。
  2. 更简单、更快的代码(执行速度会更快)。
  3. 该方法流行(在市场上广泛存在,并已证明其可行性)。

    静态(基本)模板的缺点:

    1. 设置复杂性和高错误概率(设置中的长字符串链)。
    2. 扩展功能的可能性有限。
    3. 在没有手动重新配置的情况下,无法添加或删除虚拟机器人。

      了解这些数据后,我们可以看出,解决方案虽然广泛,但却非常有限。有可能想出一个更有趣的解决方案。此外,将我的 EA 集成到我的解决方案中,该解决方案在 MetaTrader 终端之外执行独立的动态优化,这促使我朝着改进的解决方案迈进。让我们从图示开始,它揭示了新模板的本质,并帮助我们更好地理解它与前一版本相比的优势。

      新模板的用法

      在这里,请注意左下角包含 CLOSING 的元素。该模板用于保存设置,在相应的虚拟图表上打开最后一个仓位。这是一种特殊的保护机制,可以防止频繁更改设置和策略崩溃。如果我们根据某种逻辑打开了头寸,我们有兴趣根据同样的逻辑关闭它,只有在那之后,我们才能将设置更改为较新的设置。否则,我们最终可能会陷入一片混乱。是的,情况并非总是如此,但最好在一般逻辑级别立即阻止此类时刻。

      新模板使用了创建 MetaTrader 4 和 MetaTrader 5 工作目录以及文件系统的功能和结构。换句话说,我们使用文本文件管理我们的模板,这些文件可以包含每个虚拟交易对的必要设置。这确保了在操作过程中 EA 的设置和动态重组的最大便利性,而无需手动重新配置。但这并不是全部内容。这也提供了一些我将在下面列出的好处。

      动态(新)模板优点:

      1. 无论交易终端是什么,每个工具 - 周期对都是使用文本文件进行配置的。
      2. 从文件夹中动态读取设置以及自动打开或关闭虚拟图表及其 EA 同时发生。
      3. 可以自动与 Web API 同步(需要通过端口 443)。
      4. 配置使用终端公共文件夹(*\Common\Files)进行。
      5. 需要引导 EA 来将一台机器上的所有终端与 API 同步。它还可以通过本地网络中的共享文件夹在多台机器上工作。
      6. 它可以与同样可以管理这些文件的外部程序集成,例如创建或删除它们,以及更改设置。
      7. 可以创建一对付费和免费的模板。如果我们切断与 API 的连接,我们可以将其设计为一个演示版本,需要付费版本才能有效运行。

                动态(新)模板的缺点:

                1. 通过文件系统进行操作

                这里的缺点也是有条件的,因为唯一的选择是使用其他方法(例如通过 RAM)进行 web 集成或通信,但在这种情况下,我们的王牌是我们不使用任何额外的库,我们的代码变得跨平台(适用于 MQL4 和 MQL5 EA)。因此,此类 EA 符合市场要求,如果需要,可以很容易地出售。当然,您必须添加一些内容并根据您的需要进行调整,但使用我在此处提供的方法,这将非常容易做到。

                让我们看一下这种 EA 的内部工作原理,并考虑如何以可视化和示意图的方式表示我上面提到的内容。我认为,可以使用下图来实现:

                简化的模板处理结构

                整个方案的工作原理非常简单,借助两个独立的计时器,每个计时器负责自己的任务。第一个计时器通过 API 上传设置,而第二个计时器将相同的设置读入 EA 的内存中。此外,还显示了两个箭头,表示可以手动创建和更改这些设置,或者使用第三方应用程序或其他代码自动执行此过程。理论上,该模板可以从任何其他代码进行控制,例如从另一个 MetaTrader 4 和 MetaTrader 5 EA 或脚本。

                读取设置后,确定这些设置是否是新的,或者文件夹中是否有新的设置,或者我们是否删除了其中的一些设置。如果比较显示文件中没有变化,则模板继续工作,但如果发生了变化,则使用新配置重新启动整个代码并继续工作会更正确。例如,如果我们有一个只与它所操作的图表一起工作的 EA,我们就必须在终端内手动完成所有这些工作。然而,在这种情况下,所有控制都在模板内完全自动发生,不需要手动干预。


                基本动态模板设置

                现在,我们可以继续讨论代码并分析其要点。首先,让我向您展示一组最小的输入参数,在我看来,这些输入参数足以使用逐个柱的逻辑来管理交易。但在这样做之前,有必要提到此模式使用的重要范式,以便在将来更好地理解其代码。

                • 当新柱形打开时,会发生交易操作和输入信号的计算(该点是根据每个虚拟图表的分时报价分别自动计算的)。
                • 每次只能在一个图表上开立一个仓位 (EURUSD M1 和 EURUSD M5 被视为不同的图表)。
                • 在前一个仓位关闭之前,单独图表上不能开立新仓位(我认为这种结构最简单,最正确,因为各种额外交易和平均在一定程度上已经是额外的权重)。

                现在,考虑到这些规则和限制,我们需要明白,我们可以修改此代码以使用平均或马丁格尔,或者例如处理挂单。如果有必要,您可以自行修改其结构。现在让我们看看允许我们管理交易的最小参数集。

                //+------------------------------------------------------------------+
                //|                         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

                在这个例子中,我根据相似的特征将变量分成不同的区块。我们可以看到,变量并不多。第一个区块被设计用来处理不同经纪商的不同风格的命名工具。例如,如果您的经纪商有“EURUSD”,那么该块中的所有变量都应保持现在的样子,但其他选项也是可能的,例如:

                • eurusd
                • EURUSDt
                • tEURUSD
                • _EURUSD
                • eurusd_

                等等。我就不多说了,我想你自己就能找到答案。当然,一个好的模板应该能够处理大多数这些变体。

                第二个子区块告诉我们从哪个文件夹读取文件。此外,我们正在通过 API 在同一个文件夹将数据加载。如果我们指定了正确的文件夹名称,MetaTrader 将创建相应的子目录并使用它。如果我们不指定,所有文件都将位于根目录中。共享终端文件夹提供了一个有趣的功能:如果我们启用此选项,我们不仅可以组合在 MetaTrader 5 中运行的多个 EA,还可以组合所有终端,无论它们是在 MetaTrader 4 还是 MetaTrader 5 上运行。使用这种方法,两个模板将能够使用相同的设置相同地工作,确保最大程度的集成和同步。

                您可能已经猜到了,第三个子区块使用您的 API 启用/禁用同步计时器(如果有的话)。值得注意的是,与 API 的通信仅使用 WebRequest 函数进行,该函数在 MQL4 和 MQL5 中均有效。这里唯一的限制是您的 API 应该在端口 443 上运行。然而,在 MQL5 中这种方法已经得到扩展,并且它具有通过不同端口进行连接的能力。但是,为了确保在其上构建的模板和解决方案是跨平台的,我在我的 API 中放弃了这个想法。

                我构建了 API,以便信号名称也是文件目录。使用这种方法,我就能通过了解信号名称来连接不同的信号。我们可以随时断开旧信号并连接到新信号。当然,你不能通过这种方式下载文件本身,但我的做法略有不同。我获取包含文件内容的 JSON,然后根据模板本身的代码自行创建。这需要额外的方法从 JSON 字符串中提取数据,但就我个人而言,这并没有给我带来任何问题,因为 MQL5 允许我们轻松做到这一点。当然,在使用我们的 API 之前,我们需要域名并将其添加到 MetaTrader 设置中的允许连接列表中。

                第四个区块非常重要,因为它控制风险和入场交易量。当然,我们可以使用相同的文本文件分别为每个工具 - 周期设置交易量,但我决定使用所用交易对的波动性数据进行自动设置。这种方法还包括随着我们的余额的增长而自动按比例增加交易量 - 这称为自动手数。

                第五个区块仅由一个变量组成。默认情况下,模板内工作的所有虚拟 EA 都独立交易,并且不关注其他虚拟图表上的 EA 开设的头寸。此变量确保在一个 EA 中只能建立一个仓位,其他仓位必须等到它关闭后才能建立自己的仓位。在某些情况下,这种交易方式可能非常有用。

                最后一个(第五个)区块仅包含停损设置。如果我们将它们设置为零,那么我们就不需要它们进行交易。在尚不存在的第六个块中,您可以根据需要添加参数。


                使用设置和应用指令命名文件的规则

                我想从从上传文件设置的主要方法开始,以及如何读取它们,以及这些方法如何找到我们需要的文件。为了确保正确读取这些文件,我们首先需要引入简单易懂的命名规则。首先,我们需要在模板中找到这个指令:

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

                此指令设置一个机器人昵称。所有事情都发生在这个昵称下:

                1. 命名新创建的文件,
                2. 读取文件,
                3. 创建终端全局变量,
                4. 读取终端全局变量,
                5. 其他。

                我们的模板只接受“**** dynamictemplate.txt”文件作为其设置。换句话说,我们定义了命名设置文件的第一条规则 - 文件名在扩展名之前应始终以“dynamictemplate”结尾。您可以将此名称更改为您喜欢的任何名称。因此,如果您创建两个具有不同别名的 EA,它们将安全地忽略其“兄弟”的设置,并且只能使用它们自己的文件。

                Nearby 是另一个类似的指令,可以实现同样的功能:

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

                唯一的区别是,该指令确保在订单幻数区分不同的 EA,就像文件一样,这样如果您突然决定在一个交易账户中使用多个这样的 EA,两个或多个 EA 不会关闭其他 EA 的订单。同样的,在创建下一个 EA 时,我们也应该随着更改别名一起更改这个编号。只是不要让它太大,最好每次我们基于此模板创建新的 EA 时,只将这个数字加一。

                接下来我们转到文件名的部分,它将告诉我们的 EA 适用于哪个图表。但首先,让我们看一下这个数组:

                //+------------------------------------------------------------------+
                //|                        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"
                };

                这个数组有一个非常重要的文件过滤功能。换句话说,模板将仅加载此交易品种列表中存在的图表和相应的设置。在这里,我们有义务修复另外几条规则,这些规则既适用于使用设置命名文件,也适用于调整指定的列表。

                1. 所有资产工具名称均转换为大写。
                2. 所有工具后缀和前缀都被删除,只留下真正的工具名称。

                现在我们来看一个例子。假设“EURUSD”交易品种的名称在您的经纪商那里有以下表示:“eurusd_”。这意味着您仍然使用“EURUSD”命名设置文件,但另外在设置中执行以下操作:

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

                换句话说,您向模板发出信号,在 EA 中,通过将名称转换为小写并添加适当的后缀,我们的名称将被转换为原始名称。这种情况不会经常发生,因为经纪商通常会坚持经典的大写命名方案,而不使用任何前缀或后缀,因为它根本没有任何意义。但仍然不排除这种选项。

                目前,我们只弄清楚了如何命名文件,以便模板了解这是必要的“BotNick”设置,以及如何将其与正确的交易工具相匹配。我们仍然需要将设置与特定的图表周期相匹配。为此,我制定了以下规则:

                • 在文件名后面留一个空格,并写上该时间段的等值(以分钟为单位)。
                • 允许的图表周期范围是 M1 到 H4。

                我认为具体列出这个范围内的时间段很重要。对我来说,所有这些时间段都体现在 MetaTrader 4 和 MetaTrader 5 中非常重要,这是选择这些时间段的主要原因。另一个非常重要的原因是,高于“H4”的非常高的周期通常不用于自动柱形交易。无论如何,我还没有见过这样的例子,所以选择落在了以下时间段:

                • M1 — 1 分钟
                • M5 — 5 分钟
                • M15 — 15 分钟
                • M30 — 30 分钟
                • H1 — 60 分钟
                • H4 — 240 分钟

                这些时间段已经足够了。此外,对于每个时间段,都可以很容易地计算出其等值的分钟数,而且这些数字也很容易记住。现在我们可以展示文件名的一般结构,您可以根据该结构手动或使用第三方代码或您的 API 自动创建设置。首先我们来看一下总体轮廓:

                • “工具” + “ “ + “时间段(分钟数)” + “ “ + BotNick + “.txt”

                此外,让我们看一下模板用于复制用于平仓的文件的结构:

                • “CLOSING” + “ “ + “工具” + “ “ + “时间段(分钟数)” + “ “ + BotNick + “.txt”

                如您所见,这两个文件仅在添加了“CLOSING”和随后的分隔空格方面有所不同。当然,名称中的所有信号行都由一个空格分隔,以便模板解释器可以从文件名中识别并提取这些标记。因此,设置是否属于特定图表仅由其名称决定。现在让我们看几个遵循此规则的设置示例:

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

                显然,这对于一个例子来说已经足够了。请注意最后一个名称,此文件将基于“GBPUSD 240 dynamicmplate.txt”进行复制,并放置在 EA 进行复制的确切终端的文件夹中。这样做是为了防止多个不同终端内相同的 EA 多次写入同一文件。如果我们禁用从终端共享文件夹读取的选项,则常规文件也将写入其中。如果我们需要在相应的终端中为每个特定的 EA 配置自己独立数量的设置,这可能是必要的。我将在模板旁边留下几个文件作为示例,以便通过具体示例更清晰,并且您可以尝试将它们移动到不同的文件夹中。这就结束了对使用设置的一般方面的探讨。


                读取文件和创建文件的方法

                为了完全掌握模板功能,建议了解读取和写入文件的工作原理。最终,这将使我们能够开始使用它们,不仅作为我们想要附加虚拟机器人的工具-周期的标记,而且还可以根据需要单独定制它们。为此,我们可以开始考虑以下方法。

                //+------------------------------------------------------------------+
                //|                 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;
                }

                此方法判断我们的工作目录中的文件集是否已更新。如果是,那么我们使用此方法作为信号,重新启动所有图表和 EA,以添加新的或删除不必要的工具-周期。现在让我们看一下 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
                      }
                   }  

                此方法对与我们的 EA 相关的文件的名称进行初步收集,例如“EURUSD 60”。换句话说,它只留下名称中稍后将被解析成工具-周期对的那部分。但是,这些文件的读取并不在这里进行,而是在每个虚拟 EA 中单独进行。但要做到这一点,我们首先需要将字符串本身解析为一个交易品种和一个句点。在此之前还有几点。其中之一如下,

                //+------------------------------------------------------------------+
                //|                        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;
                }

                为了丢弃我们允许的工具列表中不存在的设置(图表),此方法非常重要。重点是最终将所有已通过列表过滤的图表添加到“S”数组中,为在代码中进一步使用以创建虚拟图表对象做好准备。

                还有重要的一点也是设置的保留,它在读取基本设置时不断发生。如果基本设置位置已经开放,那么我们将停止定期设置预留。带有“CLOSING”前缀的备份文件始终保存在当前终端目录中。

                //+------------------------------------------------------------------+
                //|         с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);
                   }

                这里值得澄清的是,备份设置的工作方式是,例如,当重新启动或再次读取模板时,将从中读取数据。这只有在与此设置相对应的未平仓位的情况下才有可能。如果虚拟 EA 的特定实例没有未平仓位,则模板与一般设置同步。


                创建虚拟图表和 EA

                然后我们在下一个方法中从那里获取所有数据,同时创建必要的虚拟图表。

                //+------------------------------------------------------------------+
                //|                      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
                   }

                此方法专注于创建非重复工具-周期的集合,并在此基础上创建相应图表的对象。此外,请注意每个图表的柱形数组的大小设置为刚好超过 1000 个柱形。我相信这对于实现大多数策略来说已经足够了。如果发生某些特殊事情,我们可以将此数量更改为所需的数量。现在让我们用创建虚拟 EA 对象的方法来整合材料。

                //+------------------------------------------------------------------+
                //|              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;
                            } 
                         }
                      }
                   }

                在这里创建虚拟 EA 并附加到相应的图表,将此“j”图表的 ID 存储在 EA 内部,以便将来我们知道从虚拟 EA 中的哪个图表获取数据。至于这两个类的内部结构,我在前面的文章中提到过。在许多方面,除了一些无关紧要的变化外,新代码都与它类似。


                虚拟 EA 的动态读取和重新配置

                显然,这一部分对我们来说极其重要,因为这是新模板整个概念的很大一部分。毕竟,创建虚拟图表和 EA 只是工作的一半。看起来我们已经在最低限度上搞清楚了虚拟图表的重新创建,但这还不够。建议弄清楚如何即时获取新设置并立即重新配置 EA,而不会干扰交易终端。为了解决这个问题,我们使用了一个简单的计时器,如文章开头的图表所示。

                //+------------------------------------------------------------------+
                //|             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;
                   }

                如您所见,计时器每“5”分钟触发一次,这意味着不会立即拾取新文件。但我认为,这个时间已经足以保证动态化。如果这对您来说还不够,您可以将其减少到一秒。您唯一应该明白的是,不鼓励频繁使用文件操作,并且应尽可能避免这种方法。在此代码中,请注意 ReadSettings 方法。它读取所需的文件(针对每个虚拟 EA 单独读取),然后在读取后重新配置 EA。该方法的设计思路是,它可以读取常规设置(如果所选虚拟 EA 中没有未平仓头寸),或者暂停更新设置并等待,直到根据创建该头寸的设置关闭头寸。

                //+------------------------------------------------------------------+
                //|                        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;   
                   }

                首先,我想强调一下,这种方法是专门为读取两种类型的文件而设计的。根据传递的 bClosingFile 标记,读取一般设置或“用于关闭”的设置。每次文件读取都包含几个步骤:

                1. 比较前一次读取的文件与当前文件的内容;
                2. 如果内容不同,我们就读取更新后的设置;
                3. 如果需要,我们将使用新设置重新启动虚拟 EA。

                该方法的构建方式已经考虑了资源清理和其他行动。我们只需要实现下一个方法,它在上一个方法中被调用。我试图以这样的方式做每一件事,让你从这些文件操作的麻烦中解脱出来,这样你就可以尽可能地专注于编写你需要的阅读代码。读取在这里进行。

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

                这里不需要打开或关闭文件。您所要做的就是逐个字符串读取文件字符串,并将读取的内容正确添加到相应的变量中。为此,我们可以使用临时变量,并立即将所有数据写入您将用作策略设置的变量中。但我建议填写此方法中已有的设置,这是为了这些目的。

                //+------------------------------------------------------------------+
                //|                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);
                }

                没有必要触碰最后三行代码,因为它们是强制性的。其中最有趣的是 SmartMagic 方法,其设计目的是自动为每个虚拟 EA 分配幻数。在这个阶段,我们需要知道的是,我们需要编写这个逻辑,将 EA 设置重新分配到更高的位置 — 空块中。如有必要,我们还可以努力重新创建指标和可能仍然存在的其他一切。


                自动生成幻数

                在不偏离前一种方法的情况下,我想立即向您展示一种生成唯一订单 ID 的方法,以确保模板内所有虚拟 EA 的独立交易。为此我使用了以下方法。 

                例如,我在最接近的幻数之间分配一个“10000”的步长。对于每种工具,我首先记录它的初始幻数,例如“10000”或“70000”。但这还不够,因为该工具也有时段。因此,我在这个中间幻数上又加了一个数字。 

                最简单的方法是添加这些时段的分钟量,就像在文件读取结构中一样。这就是完成的方法。

                //+------------------------------------------------------------------+
                //|              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;
                }

                这就是我们对幻数进行额外转变的地方,它似乎使得幻数集变得独一无二,尽管终端内的不同 EA 之间也是如此。

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

                总的来说,一切都很简单。幻数之间的较大步长提供了相当多的转变选项,这足以创建所需数量的 EA。使用此结构的唯一条件是不超过数字“9999”。此外,我们需要确保偏移与我们以分钟为单位的时间范围等值不匹配,因为在这种情况下,两个不同模板的幻数可能会出现巧合。为了不去考虑这样的选择,我们可以简单地进行比“240”稍大的移位,例如“241”、“241*2”、“241*3”、“241*N”。

                总结这种方法,我们可以看到,这种结构完全使我们不再需要设置幻数,这是该解决方案不言而喻的中期目标之一。唯一的缺点是无法连接两个或多个独立的虚拟 EA,因为它们的幻数将会重合,最终导致这些策略互相影响,并最终导致其逻辑崩溃。事实上,我不知道谁可能需要如此奇特的举动。此外,这不属于最初的预期功能。如果有人感兴趣的话,也许我会在下一篇文章中添加它。


                交易量标准化系统

                如果我有一个简单易定制的模板,那么选择正确的方法来设置交易量就非常重要。非常有趣的是,我关于概率论的文章,特别是这篇文章的最终结论,帮助我实现了简单有效的交易量均衡。我决定均衡交易量,假设头寸最终财务结果的平均持续时间和绝对值大小应该相似,这意味着这种均衡的唯一正确解决方案应该基于以下考虑因素。

                最终交易图表净值的平均增加或减少率应由最终组装中包含的每个 EA(独立工具-周期)平等提供。所有必要的值都应在不使用虚拟图表列表中未显示的工具-周期数据的情况下计算。交易量不仅应按照 EA 的数量比例分配,还应按照存款(自动手数)比例分配。

                为此,立即引入以下定义和方程式非常重要。首先,我实施了以下参数来调整风险:

                • DeltaBarPercent - DepositDeltaEquity 的百分比,
                • DepositDeltaEquity - 一个机器人的存款,用于计算其对于一个未平仓头寸的 M1 柱可接受的净值增量。

                这些术语乍一看可能不太清楚,让我澄清一下。为了方便起见,我们指定一个存款,一个单独的虚拟 EA 会使用该存款,然后以百分比的形式指示如果我们开仓并且从顶部点“ M1”柱形移动到底部或反之亦然,该存款的哪一部分应该增加或减少(以我们的净值形式)。

                我们的代码的目标是根据我们的要求自动选择入场交易量。为了做到这一点,我们需要额外的数学量以及基于它们的方程式。我不会得出任何结论,我只会提供给你来解释代码:

                • “Mb” - EA 工作图表上选定历史范围内“bars”大小的平均柱形大小(以点为单位),
                • “Mb1” - EA 工作图表上选定“bars”大小历史范围内的平均柱形大小(以点为单位)重新计算为 M1,
                • “Kb” - 当前图表与其“M1”等价物的平均柱形尺寸之间的连接比率,
                • “T” - 所选图表的周期缩短至一分钟(就像我们在文件中看到的一样),
                • “BasisI” - 所选工具图表上 M1 蜡烛的平均大小所需的存款货币净值线平均增幅或减幅,
                • “Basisb” - 所选工具图表上 M1 蜡烛图的平均大小(交易手数为“1”)对应的存款货币净值线的实际平均增幅或减幅,
                • “Lot”- 选定的手数(交易量)。

                现在我已经列出了计算中用到的所有量,让我们开始分析和理解它。为了计算所需的手数,首先我们应该了解相对于“M1”的较高时间范围内的柱形尺寸之间的关系是如何实现的。下面的公式会有所帮助。

                缩放至 M1 的缩放因子

                这正是允许在不从“M1”加载数据的情况下计算相同特征的表达式,尽管在虚拟图表的呈现时段上,虚拟EA的选定实例正在使用。乘以这个因子后,我们得到的数据几乎与我们在“M1”时期计算的数据相同。这是要做的第一件事。计算该值的方法如下:

                //+------------------------------------------------------------------+
                //|       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;
                   }

                现在,我们当然需要了解我们正在适应“M1”什么值。就是这个。

                换句话说,我们计算虚拟 EA 所用图表上烛形的平均大小(以点为单位)。计算出值后,我们应该像这样使用之前的值进行转换。

                这两个操作均按以下方法发生。

                //+------------------------------------------------------------------+
                //|     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
                   }

                现在我们可以使用简化为 M1 的结果值来计算“Basisb”变量。应该这样计算。

                最小报价单位是当价格变动“1”个点时,交易量为“1”手的未平仓仓位的净值变化量。如果我们将其乘以一分钟烛形的平均大小,我们将得到单个手数仓位的净值变化量,其中考虑到变动的大小已等于一分钟蜡烛的平均大小。接下来我们要计算“BasisI”的剩余值,就是这样的。

                其中的百分比和存款正是我们需要的控制参数,以此作为我们所需要的依据。剩下要做的就是选择一个比例,使得基数相等,这个比例就是我们的最终手数。

                所有描述的操作均按照以下方法执行。

                //+------------------------------------------------------------------+
                //|    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;
                   }

                因此,我们有效地均衡了所有工具的交易量,从而对所有虚拟 EA 的净值做出同等贡献。它实际上有点像固定手数模式,就好像我们为每个 EA 单独设置了它一样,但在这种情况下,我们摆脱了这种需要。这是一个完全不同的风险控制系统,但它的不同之处在于它适应了多样化的交易,如果你想实现利润曲线的最佳稳定性并摆脱这种常规,你无论如何都必须这样做。事实上,平衡多个 EA 是最敏感的时刻。我想,那些做过类似事情的人会欣赏这个决定。无论如何,如果这种平衡方法不适合您,您可以随时将设置写入相应的文件并修改此系统。


                自动手数

                标准化手数可按照存款比例增加。为了理解如何操作,我将引入以下符号:

                • - Lot - 特定虚拟 EA 的标准化(平衡)手数;
                • - AutoLot - 重新计算自动批次模式的“Lot”存款(我们需要在启用自动批次时收到它);
                • - DepositPerOneBot - 当前存款的一部分 (存款的一部分),只能由其中一个虚拟 EA 控制;
                • - DepositDeltaEquity - 我们对其执行标准化的存款(平衡手数);
                • - 存款 - 我们目前的银行;
                • - BotQuantity- 当前在多机器人内部交易的虚拟 EA 数量。

                然后你可以写出我们的“AutoLot”等于什么:

                • AutoLot = Lot * (DepositPerOneBot / DepositDeltaEquity)。

                事实证明,在通常的归一化交易量的情况下,我们忽略了我们的存款,并接受以下存款分配给一个虚拟EA — DepositDeltaEquity。但在自动手数的情况下,这种存款不是真实的,我们应该按比例改变标准化的交易量,以便我们的风险适应真实存款。然而,需要进行调整,考虑到一个虚拟 EA 仅占存款的一部分。

                • DepositPerOneBot = Deposit / BotQuantity.

                这就是我的模板中自动手数的工作方式。我认为这种方法非常方便,并且对曲线指数增长的陡度提供了必要的调整。您可以在附件中找到源代码。我们来看一下适当调整这些值的结果。

                自动手数与标准化交易量

                请注意,如果设置正确且初始信号可以获利,则此模式下的利润曲线将大致如此处所示。您的策略中的利润因子越高,测试区域中的交易就越多,这条曲线就会越平滑,越呈指数增长。这正是多样化所取得的巨大成就。我们的模板包含最大化此效果的所有基础知识。此外,请注意存款的负载:它的平滑度和均匀性间接表明了当前存款负载的正确规范化和随后的缩放。我根据所讨论的模板从我的产品中获取了这个示例。


                与 API 同步

                此功能是可选的,可以很容易地禁用或完全从模板中剪切出来,但我个人发现它对我的产品非常有用。前面提到了,同步也是通过定时器来触发的。

                //+------------------------------------------------------------------+
                //|            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();
                     }
                } 

                现在让我们看一下主要的 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]);
                                }
                            }
                        }
                    }
                }

                我以这样一种方式安排了整个结构:第一步是访问API,以便找出位于我们服务器上指定文件夹中的整个文件列表。它将分发设置。该文件夹名称是 SignalDirectoryE。根据我的想法,这也是信号的名称。收到文件列表后,将分别下载每个文件。在我看来,这个构造逻辑非常方便。这样我们就可以创建许多信号(文件夹),我们可以随时在它们之间切换。如何做和安排这件事由你决定。我的任务是提供现成的功能以便于连接。现在让我们看看从服务器获取文件名列表的方法模板。

                //+------------------------------------------------------------------+
                //|              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;
                   }

                您在这里要做的就是以相同的方式形成您的“URL”字符串。这里最重要的部分是以下字符串:

                • filelist
                • Password_key

                第一个字符串是您的 API 的函数之一。如果您愿意,可以更改其名称。您将拥有几个这样的函数。例如,提供以下操作:

                1. 将设置文件上传到您的 API(从您的个人应用程序或程序)。
                2. 清除 API 上的设置文件(从您的个人应用程序或程序)。
                3. 从现有目录(来自您的 EA)上传文件列表。
                4. 上传文件内容(来自您的 EA)。

                您可能还需要其他函数,但具体来说,EA 中只需要最后两个函数。第二个字符串是“标题”之一。您还需要根据传递给 API 的内容来生成它。一般来说,您只需要一个访问密钥就可以防止任何人闯入。但如果您需要传达一些额外的数据,您可以添加更多内容。应该在这里解析从服务器接收到的字符串,

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

                我们接收 JSON 并将其解析为名称。不幸的是,没有通用的解析代码,每个案例都是独立的。就我个人而言,我不会说编写解析代码太难。当然,有适当的库是好事,但我个人更喜欢自己编写尽可能多的代码。现在让我们看看获取文件内容的类似方法。

                //+------------------------------------------------------------------+
                //|                    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;
                   } 

                所有内容都与前一个完全相同,除了我们得到的不是文件列表,而是文件中的字符串列表。当然,使用文件内容字符串逐个字符串解析 JSON 是通过以下方法中的单独逻辑完成的,该方法与其兄弟 “extractFiles” 具有相同的目的。

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

                当然,你不必完全按照我说的去做,我只是已经有了一个完全这样构建的产品。在我看来,使用一个具体的工作示例来理解这一切要容易得多。收到文件内容后,您可以使用以下方法安全地逐个字符串写入它,该方法实际上已经集成到模板逻辑中。

                //+-----------------------------------------------------------------------+
                //|    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);
                       }
                   }
                
                

                这就是按文件名创建文件的方式。当然,它的内容也以单独的字符串形式写入其中。对于小文件来说,这是一个完全足够的解决方案。我没有注意到它的工作有任何放缓。


                具有交易逻辑的通用方法

                我在上一篇文章中提到了这个问题,但我想再次强调一下,并提醒您,当在每个虚拟 EA 中打开一个新柱时,这种方法是有效的。我们可以将其视为 OnTick 类型的处理函数,但在我们的例子中,它当然是 OnBar。顺便说一句,MQL5 中没有这样的处理函数。它的工作方式与我们所希望的略有不同,但实际上并没有对柱形交易产生重大影响,所以这是我们最不担心的问题。

                //+------------------------------------------------------------------+
                //|      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();
                   }      
                } 

                我在模板中实现了一些基本逻辑,因此您可以使用它作为示例来构建自己的逻辑。我建议在 BotInstance 类中的此方法旁边添加您自己的逻辑和变量,以免混淆。我建议使用您将在主 Trade 方法中使用的方法和变量来构建您的逻辑。


                图形界面

                与之前的版本一样,该模板包含一个简单的用户界面示例,其配色方案和内容可以更改。此界面对于两个模板都是相同的:MetaTrader 4 和 MetaTrader 5,而且看起来更好。

                GUI

                问号表示可以添加一些额外数据或删除不必要块的位置。这很容易做到。有两种使用接口的方法:CreateSimpleInterface 和 UpdateStatus。它们非常简单。我不会在行动中展示它们。您可以通过各自的名称找到它们。

                我向这个界面添加了三个非常有用的字段。如果您查看最后三个字符串,您可以看到“保留的幻数走廊”,这与您当前使用的配置相关。如果我们删除或添加设置文件,那么,相应地,这个走廊将会缩小或扩大。此外,我们需要以某种方式保护不同的 EA 免受冲突,这个字段将帮助我们做到这一点。剩下的两个字段表示上次读取任何设置的时间,以及上次与我们的 API 同步的时间(前提是发生同步)。


                结论

                在本文中,我们得出了一个更方便、功能更强大的模板模型,除其他外,该模型适合进一步扩展和修改。代码仍然远非完美,还有很多需要优化和修复的地方,但即使考虑到所有这些,我对添加什么以及出于什么目的有几个清晰的想法。在下一篇文章中,我想虚拟化头寸,并根据这些数据获得一个独特的跨货币优化器,该优化器将单独优化我们的每个 EA,并生成带有我们策略设置的现成文件。

                跨货币优化器将允许我们同时优化所有虚拟工具-周期。通过合并所有这些设置,我们将获得更好、更安全的利润曲线,同时降低风险。我认为自动多样化和利润提升是进一步改进的绝对优先事项。因此,我希望在任何策略之上获得一些基本的附加组件,以从中获得最大的利润,同时为最终用户保持最大的功能和便利性。这应该有点像我们的交易信号的外骨骼。

                链接

                本文由MetaQuotes Ltd译自俄文
                原文地址: https://www.mql5.com/ru/articles/14251

                附加的文件 |
                DynamicTemplate.zip (45.56 KB)
                在任何市场中获得优势(第三部分):Visa消费指数 在任何市场中获得优势(第三部分):Visa消费指数
                在大数据的世界里,有数以百万计的备选数据集,它们有可能提升我们的交易策略。在这一系列文章中,我们将帮助您识别最有信息量的公开数据集。
                您应当知道的 MQL5 向导技术(第 28 部分):据入门学习率重新审视 GAN 您应当知道的 MQL5 向导技术(第 28 部分):据入门学习率重新审视 GAN
                学习率是许多机器学习算法在训练过程期间,朝向训练目标迈进的步长。我们检验了其众多调度和格式对于生成式对抗网络性能的影响,该神经网络类型我们在早前文章中已检验过。
                您应当知道的 MQL5 向导技术(第 29 部分):继续学习率与 MLP 您应当知道的 MQL5 向导技术(第 29 部分):继续学习率与 MLP
                我们主要验证自适应学习率,圆满考察学习率对智能系统性能的敏感性。这些学习率旨在在训练过程中针对层中的每个参数进行自定义,故我们评估潜在收益相较于预期的性能损失。
                周期与外汇 周期与外汇
                周期在我们的生活中具有极其重要的意义。昼夜交替、四季更迭、一周的七天以及许多其他不同性质的周期都存在于每个人的生活中。在本文中,我们将探究金融市场中的周期。