MQL5 酷宝书  服务·进阶篇
⚙️

MQL5 酷宝书 服务·进阶篇

(2/3)· 弄懂服务不绑图表也能跑价的原理,避开把脚本当服务用的常见坑

偏理论进阶 第 2/3 篇
很多交易者以为 MQL5 里只有 EA 和指标能自动跑,其实服务能在不挂图表的前提下持续推价。把后台逻辑塞进脚本,终端一关图表就全停,这种误用很普遍。

「服务程序的起停与收尾打印陷阱」

MT5 的服务类程序(Service)在终端里跑起来后,会在日志打出启动与停止时间。上面这段真实日志里,名为 dStartStop 的服务在 2022.12.01 22:49:10.324 启动,约 1 秒后 22:49:11.336 停止,说明服务主体逻辑退出极快。 看 OnStart 函数:先用 MQLInfoString(MQL_PROGRAM_NAME) 取服务名,TimeLocal() 取本地时间,PrintFormat 打出 starts at。随后是个 do-while 循环,里面只 Sleep(1),只要 IsStopped() 不为真就一直空转——这意味着服务靠终端发停止信号才退出,不是自己算完就走。 [CODE] 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); } } [/CODE] 逐行拆解:第 3 行取当前服务程序名;第 4、5 行取本地时间并格式化输出启动时刻。do 循环第 7~10 行以 1 毫秒休眠轮询,直到 IsStopped 为真。退出后第 12~13 行再取时间打 stops at。 注意收尾的 for 循环:原以为服务停了就完事,实际日志里 dStandBy 在 23:20:51.144 收到停止信号后,仍连续打出 Count: 1 到 Count: 5,且每次 Sleep(10000) 被压缩成微秒级(51.144~51.207 全打完),说明服务停止后终端不再真正挂起线程,Sleep 长延时失效。 别把停止信号当立即销毁 服务被停时,OnStart 里剩余的同步代码仍会跑完,但 Sleep 超时参数会被忽略。若你在收尾段放依赖延时的逻辑(如等价格回踩再平仓),外汇与贵金属高波动下可能瞬间跳过等待,仓位暴露风险可能放大。开 MT5 挂个空服务验证这段,看自己终端是否也吞掉了那 50 秒。

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

千阶矩阵乘在 CPU 上的实测耗时

做大规模数值计算前,先在 MT5 里把矩阵维度定死:A 为 1000×1000,B 为 1000×1000,C 承接 A×B 自然也是 1000×1000。用 MathRand 把两个矩阵填成 0~10 之间的随机浮点,再走自写的 MatrixMult_CPU 跑乘法,time_cpu 以毫秒计。 日志里服务 dStandByMatrixMult 从 15:17:23 启动,到 15:18:17 停止,期间循环计数到 5 之后在 15:18:19.771 打出 Abnormal termination——说明纯 CPU 跑千阶方阵乘,在常规脚本生命周期内可能撑不住,存在异常终止倾向。 开 MT5 把这段初始化和调用原样贴进 EA 的 OnStart,重点看 PrintFormat 打出的 time CPU= 数值;若你的机器也卡在 Count:5 附近崩,就该考虑把计算拆批或挪到 OpenCL。外汇与贵金属相关的策略回测若依赖此类重算,须留意终端卡死带来的滑点高风险。

MQL5 / C++
class="type">int cols_b=class="num">1000;
class=class="str">"cmt">//--- matrix C 1000x1000
class="type">int rows_c=rows_a;
class="type">int cols_c=cols_b;
class=class="str">"cmt">//--- matrix A: size=rows_a*cols_a
class="type">int size_a=rows_a*cols_a;
class="type">int size_b=rows_b*cols_b;
class="type">int size_c=rows_c*cols_c;
class=class="str">"cmt">//--- prepare matrix A
class="type">class="kw">double matrix_a[];
::ArrayResize(matrix_a, rows_a*cols_a);
for(class="type">int i=class="num">0; i<rows_a; i++)
  for(class="type">int j=class="num">0; j<cols_a; j++)
    matrix_a[i*cols_a+j]=(class="type">class="kw">double)(class="num">10*::MathRand()/class="num">32767);
class=class="str">"cmt">//--- prepare matrix B
class="type">class="kw">double matrix_b[];
::ArrayResize(matrix_b, rows_b*cols_b);
for(class="type">int i=class="num">0; i<rows_b; i++)
  for(class="type">int j=class="num">0; j<cols_b; j++)
    matrix_b[i*cols_b+j]=(class="type">class="kw">double)(class="num">10*::MathRand()/class="num">32767);
class=class="str">"cmt">//--- CPU: calculate matrix product matrix_a*matrix_b
class="type">class="kw">double matrix_c_cpu[];
class="type">class="kw">ulong time_cpu=class="num">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());
    class="kw">return;
   }
::PrintFormat("time CPU=%d ms", time_cpu);

◍ 服务别和EA抢活干

在MT5里写Service(服务)程序,第一原则就是别和其它MQL5程序撞车。一方面服务能跑几乎任意代码(违禁项除外),另一方面它该有自己明确的定位,而不是重复EA、指标或脚本的活。 设想一个EA在交易时段结束按信号挂限价单,同时有个服务也在挂同样的单。结果可能是EA自己的审计逻辑被打乱,或者因为魔幻数字不同直接忽略掉服务下的单。反过来更糟:服务若在收盘时强平所有持仓并删挂单,而EA正按信号下单,两者利益冲突,极端情况下还可能把交易服务器拖到类似DDoS的窘境。 合理的做法是让服务和谐嵌进终端,不和现有程序打架,而是互补。比如下面这类活就适合服务接手: 清理旧日志 假设要让服务在每个新交易日开始时,清掉EA们昨天、前天生成的%MQL5\Logs里的旧日志。原生MQL5文件函数受沙箱限制碰不到这个目录,只能借WinAPI。 需要八个WinAPI函数:FindFirstFileW()搜首个匹配文件(用“.log”通配)、FindNextFileW()续搜、CopyFileW()复制、GetFileAttributesW()取属性、SetFileAttributesW()设属性、DeleteFileW()删文件、FindClose()关搜索句柄、GetLastError()取错误码。 服务主循环里检测新的一天到来,然后删旧日志但跳过当日文件;若设了备份盘(示例为G:\Logs)就先复制再设“只读”。循环每次暂停15秒,这频率对判新一天算相对最优。启动前Logs里一堆旧log,跑起来后只留当日一个文件,备份盘收到副本。 按持仓管图表 另一个任务:终端只展示有持仓的品种图表。有持仓就开图,无持仓就关图;同品种多仓也只留一张图。盈利仓图表背景浅蓝,亏损浅粉,零利润薰衣草色。 服务里用CChart类巡已开图表,无持仓时全关。有持仓则用CHashSet收集唯一品种名,用CHashMap清掉重复图表(例如两个EURUSD留一个)。随后按持仓数组开缺的图,按已开图数组关无持仓的图(如USDJPY无仓就关),并用CPositionInfo算盈亏设背景色,最后用WinAPI的keybd_event()把窗口排成磁贴。 合成美元指数 服务还能脱离图表在后台造自定义品种。以DXY为例,成分权重:EUR 57.6%、JPY 13.6%、GBP 11.9%、CAD 9.1%、SEK 4.2%、CHF 3.6%,方程为几何加权:USDX=50.14348112*EURUSD^-0.576*USDJPY^0.136*GBPUSD^-0.119*USDCAD^0.091*USDSEK^0.042*USDCHF^0.036。美元在报价货币端取负幂,基准端取正幂。 服务dDxySymbol分两阶段:先借CiCustomSymbol建DXY(非交易品种,SYMBOL_TRADE_MODE_DISABLED),用matrix::CopyTicksRange()按标志列拉六个成分历史跳价;再以10秒为步,用SearchTickLessOrEqual()抓不晚于时间戳的跳价算指数并TicksReplace()写库。实测图表周六周日仍基于历史算价,需加CheckDayOfWeek()限周内日才正常。外汇与贵金属合成品种测算仅供验证,实盘含高杠杆高风险。

MQL5 / C++
class="macro">#include <WinAPI\winapi.mqh>
class=class="str">"cmt">//--- include
class="macro">#include <WinAPI\winapi.mqh>
class="macro">#include "Include\CisNewBar.mqh"
class=class="str">"cmt">//--- defines
class="macro">#define ERROR_FILE_NOT_FOUND 0x2

「用服务程序把每日日志搬去外接盘」

MT5 服务程序(Service)能在无图表环境下常驻运行,靠 OnStart() 启动后循环检测日线新柱。上面这段把终端 Data 路径下的 MQL5\Logs\*.log 作为源,按输入参数 InpDstPath(默认 "G:")拼出目标文件夹 G:\Logs,实现日志外迁。 判定目标文件夹是否存在,走的是 Windows API 的 GetFileAttributesW。返回 0xFFFFFFFF 即 INVALID_FILE_ATTRIBUTES,说明路径无效;再与 0x10(FILE_ATTRIBUTE_DIRECTORY)做位与,非 0 才认作目录。不存在就调 CreateDirectoryW 建目录,失败则打印 GetLastError() 的十六进制码并直接 return。 实际部署时把 InpDstPath 改成你的移动盘盘符,日线新柱触发后日志有可能自动归档到外接存储,降低 C 盘日志堆积风险。外汇与贵金属交易终端常开,这类文件操作须先在模拟环境验证,误写路径可能清掉既有目录。

MQL5 / C++
class="macro">#define ERROR_NO_MORE_FILES 0x12
class="macro">#define INVALID_FILE_ATTRIBUTES 0xFFFFFFFF
class="macro">#define FILE_ATTRIBUTE_READONLY 0x1
class="macro">#define FILE_ATTRIBUTE_DIRECTORY 0x10
class="macro">#define FILE_ATTRIBUTE_ARCHIVE 0x20
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Inputs                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="kw">input class="type">class="kw">string InpDstPath="G:" ; class=class="str">"cmt">// Destination drive
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Service program start function                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
   class="type">class="kw">string program_name=::MQLInfoString(MQL_PROGRAM_NAME);
   class="type">class="kw">datetime now=::TimeLocal();
   ::PrintFormat("Service \"%s\" starts at: %s", program_name, ::TimeToString(now, TIME_DATE|TIME_SECONDS));
class=class="str">"cmt">//--- new bar
   CisNewBar daily_new_bar;
   daily_new_bar.SetPeriod(PERIOD_D1);
   daily_new_bar.SetLastBarTime(class="num">1);
class=class="str">"cmt">//--- logs path
   class="type">class="kw">string logs_path=::TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Logs\\";
   class="type">class="kw">string mask_path=logs_path+"*.log";
class=class="str">"cmt">//--- destination folder(if to copy files)
   class="type">class="kw">string new_folder_name=NULL;
   class="type">uint file_attributes=class="num">0;
   if(::StringLen(InpDstPath)>class="num">0)
     {
     new_folder_name=InpDstPath+"\\Logs";
     class=class="str">"cmt">//--- check whether a folder exists
     file_attributes=kernel32::GetFileAttributesW(new_folder_name);
     class="type">bool does_folder_exist=(file_attributes != INVALID_FILE_ATTRIBUTES) &&
                            ((file_attributes & FILE_ATTRIBUTE_DIRECTORY) != class="num">0);
     if(!does_folder_exist)
       {
        class=class="str">"cmt">//--- create a folder
        class="type">int create_res=kernel32::CreateDirectoryW(new_folder_name, class="num">0);
        if(create_res<class="num">1)
          {
           ::PrintFormat("Failed CreateDirectoryW() with error: %x", kernel32::GetLastError());
           class="kw">return;
          }
       }
     }
class=class="str">"cmt">//--- main processing loop
   do
     {
     class="type">MqlDateTime sToday;
     ::TimeTradeServer(sToday);

按日切分日志并清理历史文件

在 MT5 的 EA 或脚本里,按自然日切分日志是避免单文件膨胀的实用做法。先把 MqlDateTime 的时、分、秒清零,再用 StructToTime 拿到当日 0 点的 datetime,交给 isNewBar 判定是否跨日。 跨日触发后,把日期字符串里的点号去掉并拼上 .log,就得到当日日志名。接着用 kernel32 的 FindFirstFileW 枚举目录,若句柄等于 INVALID_HANDLE 就打印错误码并 continue,说明路径或权限有问题。 遍历文件时,用 ShortToString 把宽字符文件名拼成 string,再和当日日志名比较。不相等就视为旧文件,若设置了 new_folder_name 则先 CopyFileW 拷贝到新目录,拷贝失败返回 0 时需注意文件可能被占用。 这套逻辑跑在实盘前务必在策略测试器用历史数据验证,外汇和贵金属波动剧烈、滑点不可控,自动化文件操作失误可能丢失诊断记录。

MQL5 / C++
  sToday.hour=sToday.min=sToday.sec=class="num">0;
  class="type">class="kw">datetime dtToday=::StructToTime(sToday);
  if(daily_new_bar.isNewBar(dtToday))
    {
     ::PrintFormat("\nToday is: %s", ::TimeToString(dtToday, TIME_DATE));
     class="type">class="kw">string todays_log_file_name=::TimeToString(dtToday, TIME_DATE);
     class="type">int replaced=::StringReplace(todays_log_file_name, ".", "");
     if(replaced>class="num">0)
       {
        todays_log_file_name+=".log";
class=class="str">"cmt">//--- 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());
           class="kw">continue;
          }
        class=class="str">"cmt">// List all the files in the directory with some info about them
        class="type">int result=class="num">0;
        class="type">uint files_cnt=class="num">0;
        do
          {
           class="type">class="kw">string name="";
           for(class="type">int i=class="num">0; i<MAX_PATH; i++)
              name+=::ShortToString(find_file_data.cFileName[i]);
           class=class="str">"cmt">//--- class="kw">delete any file except today&class="macro">#x27;s
           if(::StringCompare(name, todays_log_file_name))
             {
              class="type">class="kw">string file_name=logs_path+name;
class=class="str">"cmt">//--- if to copy a file before deletion
              if(::StringLen(new_folder_name)>class="num">0)
                {
                 class="type">class="kw">string new_file_name=new_folder_name+"\\"+name;
                 if(kernel32::CopyFileW(file_name, new_file_name, class="num">0)==class="num">0)
                   {

◍ 用 WinAPI 收尾日志清理服务

这段代码片段承接前面的文件遍历逻辑,负责把临时日志改名、设只读、再删除,并统计实际清掉的数量。 复制后设只读那一步先取原属性,若不含 READONLY 位就补上;SetFileAttributesW 返回后再次校验,失败则打印十六进制错误码,方便在 MT5 Experts 日志里直接对照系统错误。 主循环末尾用 FindNextFileW 推进,直到返回 0;循环退出后单独判断 ERROR_NO_MORE_FILES 以外的错误,避免把正常结束误报成异常。 最后 PrintFormat 输出 Deleted log files 的计数,FindClose 释放句柄,外层 Sleep(15000) 让服务每 15 秒轮一次,直到 IsStopped 为真才退出并打印本地停止时间。外汇与贵金属相关的 EA 跑这类系统级服务时仍属高风险环境,误删路径可能引发策略中断。

MQL5 / C++
::PrintFormat("Failed CopyFileW() with error: %x", kernel32::GetLastError());
}
class=class="str">"cmt">//--- 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());
    }
}
class="type">int del_ret=kernel32::DeleteFileW(file_name);
if(del_ret>class="num">0)
  files_cnt++;
}
class=class="str">"cmt">//--- next file
::ZeroMemory(find_file_data);
result= kernel32::FindNextFileW(hFind, find_file_data);
}
while(result!=class="num">0);
class="type">uint kernel32_last_error=kernel32::GetLastError();
if(kernel32_last_error>class="num">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);
class="type">int file_close=kernel32::FindClose(hFind);
}
}
::Sleep(class="num">15000);
}
while(!::IsStopped());
now=::TimeLocal();
::PrintFormat("Service \"%s\" stops at: %s", program_name, ::TimeToString(now, TIME_DATE|TIME_SECONDS));
}
让小布替你分辨程序类型
这些后台程序的区别小布盯盘已内置对照说明,打开对应品种页就能看到服务与脚本的适用边界,省去翻文档的时间。

常见问题

服务运行在 MetaTrader 5 终端外包层而非操作系统层,不绑图表但依赖终端进程存活,类似守护进程概念。
服务可用 EventChartCustom 向图表推事件,本身只收 Start 触发,借此桥接后台逻辑与前端显示。
服务须存于 terminal_directory\MQL5\Services,且必须含 OnStart 处理函数,否则终端无法识别启动。
目前小布盯盘提供品种页的程序类型对照与风险标注,服务实例的实时启停仍需在终端服务列表确认。
源自麦克斯韦恶魔隐喻,强调后台非交互执行;理解这点能少走把服务当交互 EA 设计的弯路。