English Русский Español Deutsch 日本語 Português
preview
MQL5 酷宝书 — 服务

MQL5 酷宝书 — 服务

MetaTrader 5示例 | 21 四月 2023, 10:39
770 0
Denis Kirichenko
Denis Kirichenko

概述

从最近开始,MetaTrader 5 提供了一种新的程序类型,称为服务。 根据开发人员的说法,服务允许用户为终端创建自定义价格馈送,即实现从外部系统实时递送价格,就像经纪商交易服务器上实现的一样。 而这并非服务的唯一功能。

在本文中,我将研究使用服务的细微差别。 本文主要针对初学者。 有基于此,我试图令代码完全可重现,并且能从一个示例移植到另一个更加复杂的示例。



1. 工作中的守护进程

MQL5 中的服务与 Windows 服务有相似之处。 维基百科(Wikipedia)为服务给出了以下定义:

Windows 服务 是在后台运行的计算机程序从定义中可以清楚地看出,它与 Unix 中的守护进程概念有很多共同之处。

在我们的例子中,服务的外部环境不是操作系统本身,而是 MetaTrader5 终端外包层。

关于守护进程的几句话。

守护进程 是一段计算机程序, 作为后台进程运行,其并非处于用户直接地交互式控制下

这个术语是由麻省理工学院 MAC 项目的程序员创造的。 рус.根据 1963 年参与 MAC 项目的费尔南多·J·科尔巴托(Fernando J. Corbató)的说辞,他的团队是第一个使用守护进程这个词,灵感来自麦克斯韦(Maxwell)恶魔,麦克斯韦恶魔是物理学和热力学中一位构想的代理人,有助于对分子进行分类UNIX 系统继承了这个术语。

麦克斯韦恶魔与希腊神话对于守护进程的解释一致,即守护进程是在后台工作的超自然生物。 正如 Unix 系统管理手册中所述,古希腊人的“个人守护进程”概念类似于现代的“守护天使”概念。

虽然古希腊人还没有计算机,但它们之间的实体关系很清晰。



2. 服务 – 文档信息

在深入研究该主题之前,我建议泛读相关文档材料,看看开发人员如何描述服务的功能。

2.1 应用类型

文档的第一页,即 MQL5 应用程序类型章节,服务被定义为一种 MQL5 程序:

  • 服务 是一个程序,与指标、智能系统和脚本不同,它不需要绑定到图表即可工作。 就像脚本一样,服务不处理除触发器之外的任何事件。 若要启动服务,其代码应包含 OnStart 处理程序函数。 服务不接受除 Start(开始)以外的任何其它事件,但能够利用 EventChartCustom 将自定义事件发送到图表。 服务存储在 <terminal_directory>\MQL5\Services

请注意,此处的服务与脚本非常相似。 根本区别在于它们不与任何图表绑定。


2.2 程序执行

程序运行章节提供了 MQL5 中程序的摘要:

程序
运行 注解
  服务
 在它自己的线程中,执行线程与服务数量一致
 循环服务,不会中断其它程序的运行
  脚本
 在它自己的线程中,执行线程与脚本数量一致
 循环脚本,不会中断其它程序的运行
  智能系统
 在它自己的线程中,执行线程与 EA 数量一致
 循环智能系统,不会中断其它程序的运行
 指标
 一个品种上的所有指标共用一个线程。 线程数等于含有指标的品种数量
 一个指标中的无限循环将阻塞此品种上的所有其它指标

换言之,在激活执行流的方法方面,服务与脚本和 EA 并无区别。 服务也类似于脚本和 EA,循环代码模块的存在不会影响其它 mql5 程序的操作。


2.3 禁止在服务中使用的函数

开发人员列举了不能在服务中使用的函数:

这些都是合理的,因为服务不能停止智能系统,并使用计时器,因为它们只处理单个 Start 事件。 它们也不能使用自定义指标的函数。

 
2.4 加载与卸载服务

相应的文档章节具有几个要点。 我们来逐个研究它们。

如果在终端关闭时服务仍在运行,则在启动终端后立即加载服务。 服务在完成其工作后立即卸载。

这是服务的标志性属性之一。 不应对其进行监视。 一旦启动之后,它会自动执行任务。

服务有单一的 OnStart() 处理程序,您可以在其中实现无休止的数据接收和处理循环,例如使用网络函数创建和更新自定义品种。

我们能够得出一个简单的结论。 如果服务只需执行一套一次性操作,则代码模块无需任何循环。 如果任务涉及服务的常量或常规操作,则需要将代码模块包装在循环之中。 稍后我们将研究此类任务的示例。

与智能系统、指标和脚本不同,服务不与特定图表绑定,因此提供了一套单独的机制来启动它们。

 大概,这是该服务的第二个标志性特征。 它不需要基于任何时间帧即可工作。

从导航器使用“添加服务”命令创建新的服务实例。 可以使用相应的实例菜单启动、停止和删除服务实例。 为了管理所有实例,使用服务菜单。

这是服务的第三个标志性属性。 尽管只有一个程序文件,但您可以同时运行它的多个实例。 通常您需要采用不同的参数(输入变量)来完成。


3. 服务原型

终端帮助可由按下 F1 打开,其中讲述了启动和管理服务的机制。 故此,我们现在不会详述这个问题。

MetaEditor 中,创建服务模板,并将其命名为 dEmpty.mq5

//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   
  }
//+------------------------------------------------------------------+

编译之后,我们就能够在导航器中看到服务名称(图例 1)。


dEmpty 服务

图例 1. 导航器子窗口中的 dEmpty 服务

在导航器子窗口中添加并启动 dEmpty 服务后,我们在日志中获得以下条目:

CS      0       19:54:18.590    Services        service 'dEmpty' started
CS      0       19:54:18.592    Services        service 'dEmpty' stopped

日志显示服务已启动和停止。由于其代码不包含任何命令,因此终端中不会有任何变化。 启动服务后,我们不会注意到任何内容。

我们用一些命令填充服务模板。 创建 dStart.mq5 服务,并编写以下代码行:

//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   string program_name=::MQLInfoString(MQL_PROGRAM_NAME);
   datetime now=::TimeLocal();
   ::PrintFormat("Service \"%s\" starts at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS));   
  }
//+------------------------------------------------------------------+

启动服务后,我们将在 Experts 选项卡上看到以下条目:

CS      0       20:04:28.347    dStart       Service "dStart" starts at: 2022.11.30 20:04:28.

如此,dStart 服务通知我们其已启动,然后又已停止。

我们来扩展以前服务的功能,并将新服务命名为 dStartStop.mq5

//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   string program_name=::MQLInfoString(MQL_PROGRAM_NAME);
   datetime now=::TimeLocal();
   ::PrintFormat("Service \"%s\" starts at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS));
   ::Sleep(1000);
   now=::TimeLocal();
   ::PrintFormat("Service \"%s\" stops at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS));
  }
//+------------------------------------------------------------------+

不仅当前服务已通知其启动,而且还通知其活动也停止。

启动服务后,在日志中我们将看到以下条目:

2022.12.01 22:49:10.324 dStartStop   Service "dStartStop" starts at: 2022.12.01 22:49:10
2022.12.01 22:49:11.336 dStartStop   Service "dStartStop" stops at: 2022.12.01 22:49:11

很容易看出,第一次和第二次相隔一秒。 Sleep() 原生函数已在第一个和最后一个命令之间触发。

现在,我们来扩展当前服务的功能,如此它一直运行到被强制停止为止。我们将新服务命名为 dStandBy.mq5

//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   string program_name=::MQLInfoString(MQL_PROGRAM_NAME);
   datetime now=::TimeLocal();
   ::PrintFormat("Service \"%s\" starts at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS));
   do
     {
      ::Sleep(1);
     }
   while(!::IsStopped());
   now=::TimeLocal();
   ::PrintFormat("Service \"%s\" stops at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS));
//--- final goodbye
   for(ushort cnt=0; cnt<5; cnt++)
     {
      ::PrintFormat("Count: %hu", cnt+1);
      ::Sleep(10000);
     }
  }
//+------------------------------------------------------------------+

由于程序停止,而退出 do while 循环后,服务仍会向日志写入多个计数器值。 在每条记录之后,调用 Sleep() 延迟间隔 10 秒。

日志包含以下记录:

CS      0       23:20:44.478    dStandBy     Service "dStandBy" starts at: 2022.12.01 23:20:44
CS      0       23:20:51.144    dStandBy     Service "dStandBy" stops at: 2022.12.01 23:20:51
CS      0       23:20:51.144    dStandBy     Count: 1
CS      0       23:20:51.159    dStandBy     Count: 2
CS      0       23:20:51.175    dStandBy     Count: 3
CS      0       23:20:51.191    dStandBy     Count: 4
CS      0       23:20:51.207    dStandBy     Count: 5

服务于 23:20:44 开始,并于 23:20:51 被强行停止。 还很容易看出计数器值之间的间隔不超过 0.02 秒。 尽管之前为此该间隔设置了 10 秒的延迟。

根据有关 Sleep() 函数的文档

注解

不能从自定义指标调用 Sleep() 函数,因为指标是在界面线程中执行的,不应该减慢它的速度。 该函数内置每隔 0.1 秒检查一次 EA 停止标志状态

故此,在我们的例子中,Sleep() 函数迅速检测到服务被强制停止,并捕捉到 mql5 程序的执行延迟。

为了完整起见,我们来看一下文档例针对 IsStoped() 状态检查函数的返回值的说辞:

返回值

如果 _StopFlag 系统变量包含非 0 值,则返回 true。 如果已命令 mql5 程序完成其操作,则将非零值写入 _StopFlag。 在这种情况下,您必须立即终止程序,否则该程序将在 3 秒后从外部强行完成终止

故此,在强制停止后,服务在完全停用之前有 3 秒钟的时间执行其它操作。 我们要在现场检查这一刻。 在前一个服务的代码中,我们在循环之后添加矩阵计算。 计算大约需要一分钟。 我们将查看该服务在强制停止后是否有时间计算所有内容。 我们将新服务命名为 srvcStandByMatrixMult.mq5

计算计数器值的循环之后,我们需要在之前的代码中添加以下模块

//--- Matrix mult
//--- matrix A 1000x2000
   int rows_a=1000;
   int cols_a=2000;
//--- matrix B 2000x1000
   int rows_b=cols_a;
   int cols_b=1000;
//--- matrix C 1000x1000
   int rows_c=rows_a;
   int cols_c=cols_b;
//--- matrix A: size=rows_a*cols_a
   int size_a=rows_a*cols_a;
   int size_b=rows_b*cols_b;
   int size_c=rows_c*cols_c;
//--- prepare matrix A
   double matrix_a[];
   ::ArrayResize(matrix_a, rows_a*cols_a);
   for(int i=0; i<rows_a; i++)
      for(int j=0; j<cols_a; j++)
         matrix_a[i*cols_a+j]=(double)(10*::MathRand()/32767);
//--- prepare matrix B
   double matrix_b[];
   ::ArrayResize(matrix_b, rows_b*cols_b);
   for(int i=0; i<rows_b; i++)
      for(int j=0; j<cols_b; j++)
         matrix_b[i*cols_b+j]=(double)(10*::MathRand()/32767);
//--- CPU: calculate matrix product matrix_a*matrix_b
   double matrix_c_cpu[];
   ulong time_cpu=0;
   if(!MatrixMult_CPU(matrix_a, matrix_b, matrix_c_cpu, rows_a, cols_a, cols_b, time_cpu))
     {
      ::PrintFormat("Error in calculation on CPU. Error code=%d", ::GetLastError());
      return;
     }
   ::PrintFormat("time CPU=%d ms", time_cpu);

启动 dStandByMatrixMult 服务,并在几秒钟后强制停止它。 日志中将显示以下行:

CS      0       15:17:23.493    dStandByMatrixMult   Service "dStandByMatrixMult" starts at: 2022.12.02 15:17:23
CS      0       15:18:17.282    dStandByMatrixMult   Service "dStandByMatrixMult" stops at: 2022.12.02 15:18:17
CS      0       15:18:17.282    dStandByMatrixMult   Count: 1
CS      0       15:18:17.297    dStandByMatrixMult   Count: 2
CS      0       15:18:17.313    dStandByMatrixMult   Count: 3
CS      0       15:18:17.328    dStandByMatrixMult   Count: 4
CS      0       15:18:17.344    dStandByMatrixMult   Count: 5
CS      2       15:18:19.771    dStandByMatrixMult   Abnormal termination

正如我们所见,终止 mql5 程序执行的命令到达时间是 15:18:17.282。 服务本身在 15:18:19.771 被强制终止。 确实,从终止到强制停止服务经历了 2.489 秒。 事实上服务已被强行停止,作为紧急终止的结果,以  "Abnormal termination" 条目标示。

由于强制停止服务前剩余时间不超过 3 秒(_StopFlag == true),因此不建议针对中断的循环进行任何正式的计算或交易动作。

下面是一个简单的示例。 假设终端提供一项服务,当终端本身关闭时所有持仓平仓。 终端关闭,服务尝试清算全部有效持仓。 结果就是,终端关闭后,一些持仓仍然未平仓,而我们并不知道这一点。 


4. 使用示例

在继续展示实例之前,我建议讨论一下交易终端服务可以做什么。 一方面,我们几乎可以将任何代码引入服务(禁止的代码除外);另一方面,可能值得在交易终端环境中划分权限,并为服务提供自己的市场定位。

首先,服务不应重复其它活跃的 MQL5 程序的操作:智能交易、指标和脚本。 假设有一个智能系统在交易时段结束时通过信号放置限价订单。 此外,还有一项服务可以放置这些限价订单。 结果就是,EA 本身的系统审计的限价订单可能会遭破坏,或者在魔幻数字不同的情况下,EA 可能会忽略服务放置的订单。

其次,我们需要避免相反的情况 — 即服务与其它 MQL5 程序的冲突。 假设有一个智能系统在交易时段结束时通过信号放置限价订单。 且有一项服务可以控制在交易日结束时将全部持仓平仓,并删除挂单。这样就存在利益冲突:EA 下单,服务却立即把它们删除。 所有这些,都可能导致交易服务器遭受 DDoS 攻击而告终。

一般来说,服务应该和谐地集成到交易终端的操作中,不应干扰 mql5 程序,而是与它们交互,以便更有效地使用交易算法。


4.1 清除日志

假设该服务的任务是在新的交易日开始时把由一个或多个智能系统生成的过去(昨天、前天、等)日志(流水账)文件夹清除。

我们这里需要什么工具? 我们将需要文件操作和新柱线的定义。有关新柱线检测类的详细信息,请参阅“新柱线”事件处理程序一文

现在我们来应对文件操作。原生文件操作在这里不起作用,因为我们会遇到文件沙箱的限制。 根据文档:

出于安全原因,在 MQL5 语言中文件处理严格受控。 MQL5 语言在文件操作中能够接触的文件不能位于文件沙箱之外。

由 MQL5 程序写入磁盘的日志文件位于 %MQL5\Logs 之中。 幸运的是,我们可以借助 WinAPI 的文件操作功能。

WinAPI 可由以下指令包含:

#include <WinAPI\winapi.mqh>

我们将调用 WinAPI 文件里的八个函数:

  1. FindFirstFileW(),
  2. FindNextFileW(),
  3. CopyFileW(),
  4. GetFileAttributesW(),
  5. SetFileAttributesW(),
  6. DeleteFileW(),
  7. FindClose(),
  8. GetLastError().

第一个函数在指定的文件夹中搜索含有给定名称的第一个文件。 可以将通配符作为名称。 故此,若要在文件夹中查找日志文件,将 “.log” 字符串指定为名称就足够了。

第二个函数接续由第一个函数启动的搜索。

第三个函数将现有文件复制到新文件之中。

第四个函数获取指定文件或目录的文件系统属性。

第五个函数设置此类属性。

第六个函数删除含有给定名称的文件。

第七个函数关闭文件搜索句柄。

第八个函数提取最后一个错误代码值。

我们看一下 dClearTradeLogs.mq5 服务代码。

//--- include
#include <WinAPI\winapi.mqh>
#include "Include\CisNewBar.mqh"
//--- defines
#define ERROR_FILE_NOT_FOUND 0x2
#define ERROR_NO_MORE_FILES 0x12
#define INVALID_FILE_ATTRIBUTES 0xFFFFFFFF
#define FILE_ATTRIBUTE_READONLY 0x1
#define FILE_ATTRIBUTE_DIRECTORY 0x10
#define FILE_ATTRIBUTE_ARCHIVE 0x20
//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input string InpDstPath="G:" ; // Destination drive
//+------------------------------------------------------------------+
//| Service program start function                                   |
//+------------------------------------------------------------------+
void OnStart()
  {
   string program_name=::MQLInfoString(MQL_PROGRAM_NAME);
   datetime now=::TimeLocal();
   ::PrintFormat("Service \"%s\" starts at: %s", program_name, ::TimeToString(now, TIME_DATE|TIME_SECONDS));
//--- new bar
   CisNewBar daily_new_bar;
   daily_new_bar.SetPeriod(PERIOD_D1);
   daily_new_bar.SetLastBarTime(1);
//--- logs path
   string logs_path=::TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Logs\\";
   string mask_path=logs_path+"*.log";
//--- destination folder (if to copy files)
   string new_folder_name=NULL;
   uint file_attributes=0;
   if(::StringLen(InpDstPath)>0)
     {
      new_folder_name=InpDstPath+"\\Logs";
      //--- check whether a folder exists
      file_attributes=kernel32::GetFileAttributesW(new_folder_name);
      bool does_folder_exist=(file_attributes != INVALID_FILE_ATTRIBUTES) &&
                             ((file_attributes & FILE_ATTRIBUTE_DIRECTORY) != 0);
      if(!does_folder_exist)
        {
         //--- create a folder
         int create_res=kernel32::CreateDirectoryW(new_folder_name, 0);
         if(create_res<1)
           {
            ::PrintFormat("Failed CreateDirectoryW() with error: %x", kernel32::GetLastError());
            return;
           }
        }
     }
//--- main processing loop
   do
     {
      MqlDateTime sToday;
      ::TimeTradeServer(sToday);
      sToday.hour=sToday.min=sToday.sec=0;
      datetime dtToday=::StructToTime(sToday);
      if(daily_new_bar.isNewBar(dtToday))
        {
         ::PrintFormat("\nToday is: %s", ::TimeToString(dtToday, TIME_DATE));
         string todays_log_file_name=::TimeToString(dtToday, TIME_DATE);
         int replaced=::StringReplace(todays_log_file_name, ".", "");
         if(replaced>0)
           {
            todays_log_file_name+=".log";
            //--- log files
            FIND_DATAW find_file_data;
            ::ZeroMemory(find_file_data);
            HANDLE hFind=kernel32::FindFirstFileW(mask_path, find_file_data);
            if(hFind==INVALID_HANDLE)
              {
               ::PrintFormat("Failed FindFirstFile (hFind) with error: %x", kernel32::GetLastError());
               continue;
              }
            // List all the files in the directory with some info about them
            int result=0;
            uint files_cnt=0;
            do
              {
               string name="";
               for(int i=0; i<MAX_PATH; i++)
                  name+=::ShortToString(find_file_data.cFileName[i]);
               //--- delete any file except today's
               if(::StringCompare(name, todays_log_file_name))
                 {
                  string file_name=logs_path+name;
                  //--- if to copy a file before deletion
                  if(::StringLen(new_folder_name)>0)
                    {                     
                     string new_file_name=new_folder_name+"\\"+name;
                     if(kernel32::CopyFileW(file_name, new_file_name, 0)==0)
                       {
                        ::PrintFormat("Failed CopyFileW() with error: %x", kernel32::GetLastError());
                       }
                     //--- set READONLY attribute
                     file_attributes=kernel32::GetFileAttributesW(new_file_name);
                     if(file_attributes!=INVALID_FILE_ATTRIBUTES)
                        if(!(file_attributes & FILE_ATTRIBUTE_READONLY))
                          {
                           file_attributes=kernel32::SetFileAttributesW(new_file_name, file_attributes|FILE_ATTRIBUTE_READONLY);
                           if(!(file_attributes & FILE_ATTRIBUTE_READONLY))
                              ::PrintFormat("Failed SetFileAttributesW() with error: %x", kernel32::GetLastError());
                          }
                    }
                  int del_ret=kernel32::DeleteFileW(file_name);
                  if(del_ret>0)
                     files_cnt++;
                 }
               //--- next file
               ::ZeroMemory(find_file_data);
               result= kernel32::FindNextFileW(hFind, find_file_data);
              }
            while(result!=0);
            uint kernel32_last_error=kernel32::GetLastError();
            if(kernel32_last_error>0)
               if(kernel32_last_error!=ERROR_NO_MORE_FILES)
                  ::PrintFormat("Failed FindNextFileW (hFind) with error: %x", kernel32_last_error);
            ::PrintFormat("Deleted log files: %I32u", files_cnt);
            int file_close=kernel32::FindClose(hFind);
           }
        }
      ::Sleep(15000);
     }
   while(!::IsStopped());
   now=::TimeLocal();
   ::PrintFormat("Service \"%s\" stops at: %s", program_name, ::TimeToString(now, TIME_DATE|TIME_SECONDS));
  }
//+------------------------------------------------------------------+

如果输入变量指定了文件将复制到的磁盘,则检查此文件夹是否存在,之后创建一个文件夹来存储日志文件

在主处理循环中,检查新的一天是否已到来 。 然后我们在同一循环中搜索并删除日志文件但要掠过当日的文件。 如果我们需要复制文件,检查这种可能性,并在复制后为新文件设置“只读”属性。 

在循环中,设置暂停 15 秒钟。 这可能是确定新一天的相对最优频率。

故此,在启动服务之前,%MQL5\Logs 文件夹在资源管理器中如下所示(图例 2)。

删除文件前的 “%MQL5\Logs” 资源管理器文件夹

图例 2. 删除文件前的 “%MQL5\Logs” 资源管理器文件夹


启动服务后,日志中将显示以下消息:

2022.12.05 23:26:59.960 dClearTradeLogs Service "dClearTradeLogs" starts at: 2022.12.05 23:26:59
2022.12.05 23:26:59.960 dClearTradeLogs 
2022.12.05 23:26:59.960 dClearTradeLogs Today is: 2022.12.05
2022.12.05 23:26:59.985 dClearTradeLogs Deleted log files: 6

很容易看出,该服务没有向日志写入有关其工作结束的任何内容。 原因是服务操作尚未结束。 它只是循环运行,直到被中断。

删除文件后的 "%MQL5\Logs" 资源管理器文件夹

图例 3. 删除文件后的 "%MQL5\Logs" 资源管理器文件夹

因此,删除日志后,指定文件夹中只保留一个文件(图例 3)。 自然地,删除文件的任务可以改进,并变得更加灵活。 例如,在删除文件之前,您可以将它们复制到另一个磁盘,以免永久丢失必要的信息。 通常,实现应该取决于算法的特定需求。 在当前示例中,文件被复制到 G:\Logs 文件夹(图例 4)。

复制文件后的 “G:\Logs” 资源管理器文件夹

图例 4. 复制文件后的 “G:\Logs” 资源管理器文件夹

日志的工作到此结束。 在下面的示例中,我们将给服务分配展示图表的任务。


4.2 管理图表

我们想象一下,我们面临着以下任务。 终端应提供当前交易品种的图表,即持仓品种的图表。

打开图表的规则非常简单。 如果某个品种尚有持仓,则打开该品种的图表。 如果没有持仓,则无需图表。 即使某个品种有多笔持仓,也只会打开一个图表。

另外,我们还要添加一些颜色。 如果持仓处于盈利状态,则图表的背景色将为浅蓝色;如果处于亏损,则为浅粉色。 零利润则为薰衣草色。


因此,为了执行此任务,我们首先需要在服务代码中创建一个循环,在该循环中,我们将监视持仓和图表的状态。 循环体量已经足够了。 那么,我们逐模块分析其代码。

循环分为两个模块。

第一个模块是处理没有持仓的情况:

int positions_num=::PositionsTotal();
//--- if there are no positions
if(positions_num<1)
  {
   // close all the charts
   CChart temp_chart_obj;
   temp_chart_obj.FirstChart();
   long temp_ch_id=temp_chart_obj.ChartId();
   for(int ch_idx=0; ch_idx<MAX_CHARTS && temp_ch_id>-1; ch_idx++)
     {
      long ch_id_to_close=temp_ch_id;
      temp_chart_obj.NextChart();
      temp_ch_id=temp_chart_obj.ChartId();
      ::ChartClose(ch_id_to_close);
     }
  }

在模块中,我们通览所有已打开的图表(如果有的话),并关闭它们。 在此和下面,我将使用 CChart 类来处理价格图表的属性。

第二个模块更复杂:

//--- if there are some positions
else
   {
   //--- collect unique position symbols
   CHashSet<string> pos_symbols_set;
   for(int pos_idx=0; pos_idx<positions_num; pos_idx++)
      {
      string curr_pos_symbol=::PositionGetSymbol(pos_idx);
      if(!pos_symbols_set.Contains(curr_pos_symbol))
         {
         if(!pos_symbols_set.Add(curr_pos_symbol))
            ::PrintFormat("Failed to add a symbol \"%s\" to the positions set!", curr_pos_symbol);
         }
      }
   string pos_symbols_arr[];
   int unique_pos_symbols_num=pos_symbols_set.Count();
   if(pos_symbols_set.CopyTo(pos_symbols_arr)!=unique_pos_symbols_num)
      continue;
   //--- collect unique chart symbols and close duplicates
   CHashMap<string, long> ch_symbols_map;
   CChart map_chart_obj;
   map_chart_obj.FirstChart();
   long map_ch_id=map_chart_obj.ChartId();
   for(int ch_idx=0; ch_idx<MAX_CHARTS && map_ch_id>-1; ch_idx++)
      {
      string curr_ch_symbol=map_chart_obj.Symbol();
      long ch_id_to_close=0;
      if(!ch_symbols_map.ContainsKey(curr_ch_symbol))
         {
         if(!ch_symbols_map.Add(curr_ch_symbol, map_ch_id))
            ::PrintFormat("Failed to add a symbol \"%s\" to the charts map!", curr_ch_symbol);
         }
      else
         {
         //--- if there's a duplicate
         ch_id_to_close=map_chart_obj.ChartId();
         }
      //--- move to the next chart
      map_chart_obj.NextChart();
      map_ch_id=map_chart_obj.ChartId();
      if(ch_id_to_close>0)
         {
         ::ChartClose(ch_id_to_close);
         }
      }
   map_chart_obj.Detach();
   //--- looking for a chart if there's a position
   for(int s_pos_idx=0; s_pos_idx<unique_pos_symbols_num; s_pos_idx++)
      {
      string curr_pos_symbol=pos_symbols_arr[s_pos_idx];
      //--- if there's no chart of the symbol
      if(!ch_symbols_map.ContainsKey(curr_pos_symbol))
         if(::SymbolSelect(curr_pos_symbol, true))
            {
            //--- open a chart of the symbol
            CChart temp_chart_obj;
            long temp_ch_id=temp_chart_obj.Open(curr_pos_symbol, PERIOD_H1);
            if(temp_ch_id<1)
               ::PrintFormat("Failed to open a chart of the symbol \"%s\"!", curr_pos_symbol);
            else
               {
               if(!ch_symbols_map.Add(curr_pos_symbol, temp_ch_id))
                  ::PrintFormat("Failed to add a symbol \"%s\" to the charts map!", curr_pos_symbol);
               temp_chart_obj.Detach();
               }
            }
      }
   string ch_symbols_arr[];
   long ch_ids_arr[];
   int unique_ch_symbols_num=ch_symbols_map.Count();
   if(ch_symbols_map.CopyTo(ch_symbols_arr, ch_ids_arr)!=unique_ch_symbols_num)
      continue;
   //--- looking for a position if there's a chart
   for(int s_ch_idx=0; s_ch_idx<unique_ch_symbols_num; s_ch_idx++)
      {
      string curr_ch_symbol=ch_symbols_arr[s_ch_idx];
      long ch_id_to_close=ch_ids_arr[s_ch_idx];
      CChart temp_chart_obj;
      temp_chart_obj.Attach(ch_id_to_close);
      //--- if there's no position of the symbol
      if(!pos_symbols_set.Contains(curr_ch_symbol))
         {
         temp_chart_obj.Close();
         }
      else
         {
         CPositionInfo curr_pos_info;
         //--- calculate  a position profit
         double curr_pos_profit=0.;
         int pos_num=::PositionsTotal();
         for(int pos_idx=0; pos_idx<pos_num; pos_idx++)
            if(curr_pos_info.SelectByIndex(pos_idx))
               {
               string curr_pos_symbol=curr_pos_info.Symbol();
               if(!::StringCompare(curr_ch_symbol, curr_pos_symbol))
                  curr_pos_profit+=curr_pos_info.Profit()+curr_pos_info.Swap();
               }
         //--- apply a color
         color profit_clr=clrLavender;
         if(curr_pos_profit>0.)
            {
            profit_clr=clrLightSkyBlue;
            }
         else if(curr_pos_profit<0.)
            {
            profit_clr=clrLightPink;
            }
         if(!temp_chart_obj.ColorBackground(profit_clr))
            ::PrintFormat("Failed to apply a profit color for the symbol \"%s\"!", curr_ch_symbol);
         temp_chart_obj.Redraw();
         }
      temp_chart_obj.Detach();
      }
   //--- tile windows (Alt+R)
   uchar vk=VK_MENU;
   uchar scan=0;
   uint flags[]= {0, KEYEVENTF_KEYUP};
   ulong extra_info=0;
   uchar Key='R';
   for(int r_idx=0; r_idx<2; r_idx++)
      {
      user32::keybd_event(vk, scan, flags[r_idx], extra_info);
      ::Sleep(10);
      user32::keybd_event(Key, scan, flags[r_idx], extra_info);
      }
   }

首先,收集唯一的品种值,所有持仓在其名下CHashSet<T> 类的功能适用于该任务。 该类是类型 T 的无序动态数据集的实现,每个值都具有所需的唯一性。 将获得的唯一值复制到字符串数组中,以便以后简化对它们的访问。

在下一阶段,收集已打开图表的独有品种值。 关闭重复的图表(如果有)。 假设我们有两个EURUSD 图表。 这意味着我们需保留一个图表,并关闭另一个图表。 此处应用 CHashMap<TKey,TValue> 类的实例。 该类是动态哈希表的实现,其数据存储为无序的键/值对,且所需的键值唯一性。

只余两个循环。 在第一个中,沿着持仓交易品种数组移动,并检查是否有图表。 如果没有,则打开它。 在第二个循环中,沿着已打开图表的品种数组移动,并检查持仓是否与每个品种相对应。 假设有一个打开的 USDJPY 图表,但它没有持仓。 那么 USDJPY 就会被关闭。 在同一循环中,计算仓位的盈利,并据其设置背景颜色,恰如任务开始时确定的那样。 为了访问持仓属性,并获取其数值,使用了标准库的 CPositionInfo 类。

最后,我们通过将图表窗口放置为磁贴来增加一些视觉吸引力。 为达此目的,使用 WinAPI,即模拟击键的 keybd_event() 函数。

就这样了。 它仍然只是启动 dActivePositionCharts 服务。


4.3 自定义品种,报价

该服务的优点之一是能够在不依靠价格图表的情况下在后台工作。 作为示例,在本节中,我将展示如何使用该服务创建自定义品种,及其即时报价历史记录,以及生成新的跳价。

我将使用美元指数作为自定义品种。

4.3.1 USD 指数,成分

美元指数是一个综合指数,反映了美元相对于一篮子其它六种货币的价值:

  1. EUR (57.6%);
  2. JPY (13.6%);
  3. GBP (11.9%);
  4. CAD (9.1%);
  5. SEK (4.2%);
  6. CHF (3.6%).

指数方程是美元兑换这些货币汇率的几何加权平均值,并带有修正因子:

USDX = 50.14348112 * EURUSD-0.576 * USDJPY0.136 * GBPUSD-0.119 * USDCAD0.091 * USDSEK0.042 * USDCHF0.036

根据该方程,假设当报价中的美元是报价货币时,货币对的报价被提高到负数幂,当报价中的美元是基准货币时,货币对的报价被提高到正数幂

一篮子货币可以示意如下(图例 5)。



USD 指数货币篮子 (DXY)

图例 5. USD 指数 (DXY) 货币篮子


美元指数是在洲际交易所ICE)交易的期货标的资产。 指数期货大约每 15 秒计算一次。 计算价格取指数中包含的货币对市场深度中的最高买入价和最低卖出价计算。


4.3.2 USD 指数,服务

我们已拥有计算所需的一切,因此是时候启动服务代码了。 但首先,我要提请注意,该服务将分阶段工作。 在第一阶段,它将形成合成的跳价,和柱线的历史;在第二阶段,它将处理新的跳价。 显而易见,第一阶段与过去有关,第二阶段与现在有关。

我们来创建一个名为 dDxySymbol.mq5 的 MQL5 程序(服务)模板。

定义以下 输入 变量:

input datetime InpStartDay=D'01.10.2022'; // Start date
input int InpDxyDigits=3;                 // Index digits

第一个定义了报价历史的开始,我们将尝试创建我们自己的品种。换言之,我们将下载自 2022 年 10 月 1 日以来的历史报价记录

第二个设置品种的报价精度。

因此,若要开始引用指数,我们需要创建一个自定义交易品种 — 显示合成报价的基础。 DXY 是指数品种的名称。 该资源在自定义品种上有很多材料。 我将定位到文章 MQL5 酷宝书 – 使用自定义品种的交易策略压力测试 中定义的 CiCustomSymbol 类。

下面是创建 DXY 合成品种的代码模块:

//--- create a custom symbol
string custom_symbol="DXY",
       custom_group="Dollar Index";
CiCustomSymbol custom_symbol_obj;
const uint batch_size = 1e6;
const bool is_selected = true;
int code = custom_symbol_obj.Create(custom_symbol, custom_group, NULL, batch_size, is_selected);
::PrintFormat("Custom symbol \"%s\", code: %d", custom_symbol, code);
if(code < 0)
   return;

如果之前从未创建过 DXY 品种,且其不在自定义终端品种列表之中,则 CiCustomSymbol::Create() 方法返回代码 1。 如果 DXY 品种已在品种列表之中,那么我们得到代码 0。 如果无法创建品种,则我们会收到一个错误代码:-1。 如果在创建自定义品种时出错,服务将停止工作。

创建合成品种之后,我们将为其设置若干个属性。

//--- Integer properties
//--- sector
ENUM_SYMBOL_SECTOR symbol_sector = SECTOR_CURRENCY;
if(!custom_symbol_obj.SetProperty(SYMBOL_SECTOR, symbol_sector))
   {
   ::PrintFormat("Failed to set a sector for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- background color
color symbol_background_clr = clrKhaki;
if(!custom_symbol_obj.SetProperty(SYMBOL_BACKGROUND_COLOR, symbol_background_clr))
   {
   ::PrintFormat("Failed to set a background color for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- chart mode
ENUM_SYMBOL_CHART_MODE symbol_ch_mode=SYMBOL_CHART_MODE_BID;
if(!custom_symbol_obj.SetProperty(SYMBOL_CHART_MODE, symbol_ch_mode))
   {
   ::PrintFormat("Failed to set a chart mode for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- digits
if(!custom_symbol_obj.SetProperty(SYMBOL_DIGITS, InpDxyDigits))
   {
   ::PrintFormat("Failed to set digits for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- trade mode
ENUM_SYMBOL_TRADE_MODE symbol_trade_mode = SYMBOL_TRADE_MODE_DISABLED;
if(!custom_symbol_obj.SetProperty(SYMBOL_TRADE_MODE, symbol_trade_mode))
   {
   ::PrintFormat("Failed to disable trade for the custom symbol \"%s\"", custom_symbol);
   return;
   }

以下属性属于 ENUM_SYMBOL_INFO_INTEGER 类型:

  • SYMBOL_SECTOR,
  • SYMBOL_BACKGROUND_COLOR,
  • SYMBOL_CHART_MODE,
  • SYMBOL_DIGITS,
  • SYMBOL_TRADE_MODE.

最后一个属性负责交易模式。 合成品种不允许交易,故此该属性将设置为 SYMBOL_TRADE_MODE_DISABLED。 如果我们需要在测试器中按品种检查某些策略,则应启用属性(SYMBOL_TRADE_MODE_FULL)。

//--- Double properties
//--- point
double symbol_point = 1./::MathPow(10, InpDxyDigits);
if(!custom_symbol_obj.SetProperty(SYMBOL_POINT, symbol_point))
   {
   ::PrintFormat("Failed to to set a point value for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- tick size
double symbol_tick_size = symbol_point;
if(!custom_symbol_obj.SetProperty(SYMBOL_TRADE_TICK_SIZE, symbol_tick_size))
   {
   ::PrintFormat("Failed to to set a tick size for the custom symbol \"%s\"", custom_symbol);
   return;
   }

以下属性属于 ENUM_SYMBOL_INFO_DOUBLE 类型:

  • SYMBOL_POINT,
  • SYMBOL_TRADE_TICK_SIZE.
由于之前已确定该品种是非交易品种,因此很少有双精度属性。

//--- String properties
//--- category
string symbol_category="Currency indices";
if(!custom_symbol_obj.SetProperty(SYMBOL_CATEGORY, symbol_category))
   {
   ::PrintFormat("Failed to to set a category for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- country
string symbol_country= "US";
if(!custom_symbol_obj.SetProperty(SYMBOL_COUNTRY, symbol_country))
   {
   ::PrintFormat("Failed to to set a country for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- description
string symbol_description= "Synthetic US Dollar Index";
if(!custom_symbol_obj.SetProperty(SYMBOL_DESCRIPTION, symbol_description))
   {
   ::PrintFormat("Failed to to set a description for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- exchange
string symbol_exchange= "ICE";
if(!custom_symbol_obj.SetProperty(SYMBOL_EXCHANGE, symbol_exchange))
   {
   ::PrintFormat("Failed to to set an exchange for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- page
string symbol_page = "https://www.ice.com/forex/usdx";
if(!custom_symbol_obj.SetProperty(SYMBOL_PAGE, symbol_page))
   {
   ::PrintFormat("Failed to to set a page for the custom symbol \"%s\"", custom_symbol);
   return;
   }
//--- path
string symbol_path="Custom\\"+custom_group+"\\"+custom_symbol;
if(!custom_symbol_obj.SetProperty(SYMBOL_PATH, symbol_path))
   {
   ::PrintFormat("Failed to to set a path for the custom symbol \"%s\"", custom_symbol);
   return;
   }

以下属性属于 ENUM_SYMBOL_INFO_STRING 类型:

  • SYMBOL_CATEGORY,
  • SYMBOL_COUNTRY,
  • SYMBOL_DESCRIPTION,
  • SYMBOL_EXCHANGE,
  • SYMBOL_PAGE,
  • SYMBOL_PATH.

最后一个属性负责品种树中的路径。 创建合成品种时,指定了一组特征和一个品种名称。 因此,可以跳过此属性,因为它是相同的。

当然,我可以直接设置合成品种方程,而无需收集跳价。 但如此的话这个示例的意义就会丧失。 此外,指数的价格是定期计算的。 在当前示例中,计数周期为 10 秒。

现在我们进入下一个模块 — 这是对交易历史的检查。 在此,我们将解决两个任务:检查柱线,和加载跳价的历史记录。 我们按以下方式检查柱线:

//--- check quotes history
CBaseSymbol base_symbols[BASE_SYMBOLS_NUM];
const string symbol_names[]=
  {
   "EURUSD", "USDJPY", "GBPUSD", "USDCAD", "USDSEK", "USDCHF"
  };
ENUM_TIMEFRAMES curr_tf=PERIOD_M1;
::Print("\nChecking of quotes history is running...");
for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++)
  {
   CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx];
   string curr_symbol_name=symbol_names[s_idx];
   if(ptr_base_symbol.Init(curr_symbol_name, curr_tf, InpStartDay))
     {
      ::PrintFormat("\n   Symbol #%hu: \"%s\"", s_idx+1, curr_symbol_name);
      ulong start_cnt=::GetTickCount64();
      int check_load_code=ptr_base_symbol.CheckLoadHistory();
      ::PrintFormat("   Checking code: %I32d", check_load_code);
      ulong time_elapsed_ms=::GetTickCount64()-start_cnt;
      ::PrintFormat("   Time elapsed: %0.3f sec", time_elapsed_ms/MS_IN_SEC);
      if(check_load_code<0)
        {
         ::PrintFormat("Failed to load quotes history for the symbol \"%s\"", curr_symbol_name);
         return;
        }
     }
  }

我们需要循环查询 6 个品种,并处理它们的报价。 所创建的 CBaseSymbol 类就是为了方便起见。

//+------------------------------------------------------------------+
//| Class CBaseSymbol                                                |
//+------------------------------------------------------------------+
class CBaseSymbol : public CObject
  {
      //--- === Data members === ---
   private:
      CSymbolInfo    m_symbol;
      ENUM_TIMEFRAMES m_tf;
      matrix         m_ticks_mx;
      datetime       m_start_date;
      ulong          m_last_idx;
      //--- === Methods === ---
   public:
      //--- constructor/destructor
      void           CBaseSymbol(void);
      void          ~CBaseSymbol(void) {};
      //---
      bool           Init(const string _symbol, const ENUM_TIMEFRAMES _tf, datetime start_date);
      int            CheckLoadHistory(void);
      bool           LoadTicks(const datetime _stop_date, const uint _flags);
      matrix         GetTicks(void) const
        {
         return m_ticks_mx;
        };
      bool           SearchTickLessOrEqual(const double _dbl_time, vector &_res_row);
      bool           CopyLastTick(vector &_res_row);
  };

该类处理柱线和跳价的历史,这是一项极其重要的任务,否则就没有数据来创建合成品种。 

我们加载跳价:

//--- try to load ticks
::Print("\nLoading of ticks is running...");
now=::TimeCurrent();
uint flags=COPY_TICKS_INFO | COPY_TICKS_TIME_MS | COPY_TICKS_BID | COPY_TICKS_ASK;
double first_tick_dbl_time=0.;
for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++)
   {
   CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx];
   string curr_symbol_name=symbol_names[s_idx];
   ::PrintFormat("\n   Symbol #%hu: \"%s\"", s_idx+1, curr_symbol_name);
   ulong start_cnt=::GetTickCount64();
   ::ResetLastError();
   if(!ptr_base_symbol.LoadTicks(now, flags))
      {
      ::PrintFormat("Failed to load ticks for the symbol \"%s\" , error: %d", curr_symbol_name, ::GetLastError());
      return;
      }
   ulong time_elapsed_ms=::GetTickCount64()-start_cnt;
   ::PrintFormat("   Time elapsed: %0.3f sec", time_elapsed_ms/MS_IN_SEC);
   //--- looking for the 1st tick
   matrix ticks_mx=ptr_base_symbol.GetTicks();
   double tick_dbl_time=ticks_mx[0][0];
   if(tick_dbl_time>first_tick_dbl_time)
      first_tick_dbl_time=tick_dbl_time;
   }

matrix::CopyTicksRange() 原生函数即用于加载跳价。 方便的是,您只能按由标志定义的跳价结构加载这些列。 当我们请求数百万个跳价时,节省资源的问题尤显重要。

COPY_TICKS_INFO    = 1,       // ticks caused by Bid and/or Ask changes
COPY_TICKS_TRADE   = 2,       // ticks caused by Last and Volume changes
COPY_TICKS_ALL     = 3,       // all ticks that have changes
COPY_TICKS_TIME_MS = 1<<8,    // time in milliseconds
COPY_TICKS_BID     = 1<<9,    // Bid price
COPY_TICKS_ASK     = 1<<10,   // Ask price
COPY_TICKS_LAST    = 1<<11,   // Last price
COPY_TICKS_VOLUME  = 1<<12,   // volume
COPY_TICKS_FLAGS   = 1<<13,   // tick flags

在日志记录里,检查历史记录,以及加载跳价的阶段将依据时间成本说明。

CS      0       12:01:11.802    dDxySymbol      Checking of quotes history is running...
CS      0       12:01:11.802    dDxySymbol      
CS      0       12:01:11.802    dDxySymbol         Symbol #1: "EURUSD"
CS      0       12:01:14.476    dDxySymbol         Checking code: 1
CS      0       12:01:14.476    dDxySymbol         Time elapsed: 2.688 sec
CS      0       12:01:14.476    dDxySymbol      
CS      0       12:01:14.476    dDxySymbol         Symbol #2: "USDJPY"
CS      0       12:01:17.148    dDxySymbol         Checking code: 1
CS      0       12:01:17.148    dDxySymbol         Time elapsed: 2.672 sec
CS      0       12:01:17.148    dDxySymbol      
CS      0       12:01:17.148    dDxySymbol         Symbol #3: "GBPUSD"
CS      0       12:01:19.068    dDxySymbol         Checking code: 1
CS      0       12:01:19.068    dDxySymbol         Time elapsed: 1.922 sec
CS      0       12:01:19.068    dDxySymbol      
CS      0       12:01:19.068    dDxySymbol         Symbol #4: "USDCAD"
CS      0       12:01:21.209    dDxySymbol         Checking code: 1
CS      0       12:01:21.209    dDxySymbol         Time elapsed: 2.140 sec
CS      0       12:01:21.209    dDxySymbol      
CS      0       12:01:21.209    dDxySymbol         Symbol #5: "USDSEK"
CS      0       12:01:22.631    dDxySymbol         Checking code: 1
CS      0       12:01:22.631    dDxySymbol         Time elapsed: 1.422 sec
CS      0       12:01:22.631    dDxySymbol      
CS      0       12:01:22.631    dDxySymbol         Symbol #6: "USDCHF"
CS      0       12:01:24.162    dDxySymbol         Checking code: 1
CS      0       12:01:24.162    dDxySymbol         Time elapsed: 1.531 sec
CS      0       12:01:24.162    dDxySymbol      
CS      0       12:01:24.162    dDxySymbol      Loading of ticks is running...
CS      0       12:01:24.162    dDxySymbol      
CS      0       12:01:24.162    dDxySymbol         Symbol #1: "EURUSD"
CS      0       12:02:27.204    dDxySymbol         Time elapsed: 63.032 sec
CS      0       12:02:27.492    dDxySymbol      
CS      0       12:02:27.492    dDxySymbol         Symbol #2: "USDJPY"
CS      0       12:02:32.587    dDxySymbol         Time elapsed: 5.094 sec
CS      0       12:02:32.938    dDxySymbol      
CS      0       12:02:32.938    dDxySymbol         Symbol #3: "GBPUSD"
CS      0       12:02:37.675    dDxySymbol         Time elapsed: 4.734 sec
CS      0       12:02:38.285    dDxySymbol      
CS      0       12:02:38.285    dDxySymbol         Symbol #4: "USDCAD"
CS      0       12:02:43.223    dDxySymbol         Time elapsed: 4.937 sec
CS      0       12:02:43.624    dDxySymbol      
CS      0       12:02:43.624    dDxySymbol         Symbol #5: "USDSEK"
CS      0       12:03:18.484    dDxySymbol         Time elapsed: 34.860 sec
CS      0       12:03:19.596    dDxySymbol      
CS      0       12:03:19.596    dDxySymbol         Symbol #6: "USDCHF"
CS      0       12:03:24.317    dDxySymbol         Time elapsed: 4.719 sec

收到跳价后,形成 DXY 合成品种的跳价历史记录。 此过程将在以下模块中进行:

//--- create a custom symbol ticks history
::Print("\nCustom symbol ticks history is being formed...");
long first_tick_time_sec=(long)(first_tick_dbl_time/MS_IN_SEC);
long first_tick_time_ms=(long)first_tick_dbl_time%(long)MS_IN_SEC;
::PrintFormat("   First tick time: %s.%d", ::TimeToString((datetime)first_tick_time_sec,
              TIME_DATE|TIME_SECONDS), first_tick_time_ms);
double active_tick_dbl_time=first_tick_dbl_time;
double now_dbl_time=MS_IN_SEC*now;
uint ticks_cnt=0;
uint arr_size=0.5e8;
MqlTick ticks_arr[];
::ArrayResize(ticks_arr, arr_size);
::ZeroMemory(ticks_arr);
matrix base_prices_mx=matrix::Zeros(BASE_SYMBOLS_NUM, 2);
do
   {
   //--- collect base symbols ticks
   bool all_ticks_ok=true;
   for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++)
      {
      CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx];
      vector tick_prices_vc;
      bool to_break_loop=false;
      if(!ptr_base_symbol.SearchTickLessOrEqual(active_tick_dbl_time, tick_prices_vc))
         to_break_loop=true;
      else
         {
         if(!base_prices_mx.Row(tick_prices_vc, s_idx))
            to_break_loop=true;
         }
      if(to_break_loop)
         {
         all_ticks_ok=false;
         break;
         }
      }
   //--- calculate index prices
   if(all_ticks_ok)
      {
      MqlTick last_ind_tick;
      CalcIndexPrices(active_tick_dbl_time, base_prices_mx, last_ind_tick);
      arr_size=ticks_arr.Size();
      if(ticks_cnt>=arr_size)
         {
         uint new_size=(uint)(arr_size+0.1*arr_size);
         if(::ArrayResize(ticks_arr, new_size)!=new_size)
            continue;
         }
      ticks_arr[ticks_cnt]=last_ind_tick;
      ticks_cnt++;
      }
   active_tick_dbl_time+=TICK_PAUSE;
   }
while(active_tick_dbl_time<now_dbl_time);
::ArrayResize(ticks_arr, ticks_cnt);
int ticks_replaced=custom_symbol_obj.TicksReplace(ticks_arr, true);

设置一个临时点(active_tick_dbl_time),并在循环结束时为其添加 10 秒。 这是一种“时间戳”,用于获取构成指数的所有品种的跳价。

故此,在每个品种上搜索所需的跳价是基于过去的特定时间点。 CBaseSymbol::SearchTickLessOrEqual() 方法提供了一个不晚于 active_tick_dbl_time 值到达的跳价。

当收到来自指数每个成分的跳价时,跳价的价格已经在 base_prices_mx 矩阵当中。 

CalcIndexPrices() 函数返回此时已准备就绪的指数跳价值。 

创建跳价时,调用 CiCustomSymbol::TicksReplace() 方法更新跳价数据库。

然后,该服务仅应对下一个出现的模块:

//--- main processing loop
::Print("\nA new tick processing is active...");
do
   {
   ::ZeroMemory(base_prices_mx);
   //--- collect base symbols ticks
   bool all_ticks_ok=true;
   for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++)
      {
      CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx];
      vector tick_prices_vc;
      bool to_break_loop=false;
      if(!ptr_base_symbol.CopyLastTick(tick_prices_vc))
         to_break_loop=true;
      else
         {
         if(!base_prices_mx.Row(tick_prices_vc, s_idx))
            to_break_loop=true;
         }
      if(to_break_loop)
         {
         all_ticks_ok=false;
         break;
         }
      }
   //--- calculate index prices
   if(all_ticks_ok)
      {
      MqlTick last_ind_tick, ticks_to_add[1];
      now=::TimeCurrent();
      now_dbl_time=MS_IN_SEC*now;
      CalcIndexPrices(now_dbl_time, base_prices_mx, last_ind_tick);
      ticks_to_add[0]=last_ind_tick;
      int ticks_added=custom_symbol_obj.TicksAdd(ticks_to_add, true);
      }
   ::Sleep(TICK_PAUSE);
   }
while(!::IsStopped());

模块的任务与前一个模块的任务类似,尽管略微容易一些。 每隔 10 秒钟,我们需要获取品种的跳价数据,并计算指数价格。 在指数中,出价是根据所有品种的出价计算得出,要价分别根据所有品种的要价计算得出。

启动 dDxySymbol 服务后,自定义 DXY 品种图表会在一段时间后打开(图例 6)。 

图例 6. DXY 自定义品种图表,且拥有历史记录

图例 6. DXY 自定义品种图表,且拥有历史记录 


在图表上,星期六以红色垂直段高亮显示。 事实证明,在周六和周日,该服务继续基于历史记录计算跳价,这可能并不完全符合现状。 有必要加入时间限制(周内相对日数)来补充服务代码。 我们将此任务分派给 CheckDayOfWeek() 函数。

现在合成品种图表如下所示(图l例 7)。 看起来该错误已得到修复。

DXY 自定义品种图表,无历史记录

图例 7. DXY 自定义品种图表,无历史记录 

dDxySymbol 服务的工作至此完毕。


结束语

本文重点介绍了称为服务的 MQL5 程序类型的一些功能。 这种类型的 MQL5 程序的不同之处在于它无需绑定图表,即可独立工作。 服务的性质是,它们可能与其它 EA、脚本发生冲突,且在较小程度上有可能与指标发生冲突。 因此,在 MetaTrader 5 环境中定义服务程序的权限和义务的任务落在了开发人员的肩上。

存档包含可放置在 %MQL5\\Services 文件夹中的源代码。

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

附加的文件 |
code.zip (23.7 KB)
神经网络变得轻松(第三十五部分):内在好奇心模块 神经网络变得轻松(第三十五部分):内在好奇心模块
我们继续研究强化学习算法。 到目前为止,我们所研究的所有算法都需要创建一个奖励政策,从而令代理者能够每次从一个系统状态过渡到另一个系统状态的转换中估算其每个动作。 然而,这种方式人为因素相当大。 在实践中,动作和奖励之间存在一些时间滞后。 在本文中,我们将领略一种模型训练算法,该算法可以操控从动作到奖励的各种时间延迟。
种群优化算法:杜鹃优化算法(COA) 种群优化算法:杜鹃优化算法(COA)
我将研究的下一个算法是 Levy 飞行正在使用的杜鹃搜索优化。 这是最新的优化算法之一,也是排行榜的新领导者。
DoEasy. 控件(第 二十九 部分):滚动条(ScrollBar)辅助控件 DoEasy. 控件(第 二十九 部分):滚动条(ScrollBar)辅助控件
在本文中,我起始开发滚动条(ScrollBar)辅助控制元素,及其衍生对象 — 垂直和水平滚动条。 滚动条用于窗体内容(如果窗体超出容器)的滚动显示。 滚动条通常位于窗体的底部和右侧。 底部的水平滚动条可左右滚动内容,而垂直的则上下滚动内容。
DoEasy. 控件 (第 28 部分): 进度条控件中的柱线样式 DoEasy. 控件 (第 28 部分): 进度条控件中的柱线样式
在本文中,我将开发进度条控件的柱线显示样式和说明文本。