MQL5 酷宝书  服务·综合运用
⚙️

MQL5 酷宝书 服务·综合运用

(3/3)· 前两级铺垫完概念与单点用法,这里把 11 个环节串成可跑通的完整服务工程

案例拆解 第 3/3 篇
很多人以为服务只是不绑图表的脚本,结果真要接外部行情时就卡在事件发送和目录权限上。本篇把分散的坑点按执行顺序排好,照着拼就能少绕弯路。

「日志清理后按持仓去重图表」

一段真实运行日志显示,dClearTradeLogs 服务在 2022.12.05 23:26:59.960 启动,当天删除了 6 个旧日志文件,随后进入图表与持仓的整理逻辑。这类脚本的价值在于:MT5 开久了图表越堆越多,手动关既慢又容易误关有持仓的品种。 核心分支先看有没有持仓。用 PositionsTotal() 取持仓数,若小于 1 说明空仓,就遍历所有图表直接 ChartClose 关掉,MAX_CHARTS 是终端图表上限常量,循环里靠 FirstChart/NextChart 游标推进。 有持仓时则先建一个字符串哈希集,把每个持仓的 PositionGetSymbol 去重存好;再建图表符号到图表 ID 的哈希映射,遍历图表时若某符号已存在映射中,当前这张就标记为待关闭。这样只保留每个品种的第一张图,重复开的图被清掉,持仓对应的图全留着。外汇和贵金属波动大、持仓过夜常见,这种自动收敛图表的方式能降低终端卡顿概率,但脚本误关风险仍在,上线前建议在策略测试器或模拟盘先跑一遍。

MQL5 / C++
class="type">int positions_num=::PositionsTotal();
class=class="str">"cmt">//--- if there are no positions
if(positions_num<class="num">1)
  {
  class=class="str">"cmt">// close all the charts
  CChart temp_chart_obj;
  temp_chart_obj.FirstChart();
  class="type">long temp_ch_id=temp_chart_obj.ChartId();
  for(class="type">int ch_idx=class="num">0; ch_idx<MAX_CHARTS && temp_ch_id>-class="num">1; ch_idx++)
    {
      class="type">long ch_id_to_close=temp_ch_id;
      temp_chart_obj.NextChart();
      temp_ch_id=temp_chart_obj.ChartId();
      ::ChartClose(ch_id_to_close);
    }
  }
class=class="str">"cmt">//--- if there are some positions
else
  {
  class=class="str">"cmt">//--- collect unique position symbols
  CHashSet<class="type">class="kw">string> pos_symbols_set;
  for(class="type">int pos_idx=class="num">0; pos_idx<positions_num; pos_idx++)
    {
      class="type">class="kw">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);
        }
    }
  class="type">class="kw">string pos_symbols_arr[];
  class="type">int unique_pos_symbols_num=pos_symbols_set.Count();
  if(pos_symbols_set.CopyTo(pos_symbols_arr)!=unique_pos_symbols_num)
    class="kw">continue;
  class=class="str">"cmt">//--- collect unique chart symbols and close duplicates
  CHashMap<class="type">class="kw">string, class="type">long> ch_symbols_map;
  CChart map_chart_obj;
  map_chart_obj.FirstChart();
  class="type">long map_ch_id=map_chart_obj.ChartId();
  for(class="type">int ch_idx=class="num">0; ch_idx<MAX_CHARTS && map_ch_id>-class="num">1; ch_idx++)
    {
      class="type">class="kw">string curr_ch_symbol=map_chart_obj.Symbol();
      class="type">long ch_id_to_close=class="num">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
        {
          class=class="str">"cmt">//--- if there&class="macro">#x27;s a duplicate
          ch_id_to_close=map_chart_obj.ChartId();
        }
      class=class="str">"cmt">//--- move to the next chart
      map_chart_obj.NextChart();
      map_ch_id=map_chart_obj.ChartId();

◍ 持仓与图表窗口的双向对齐逻辑

这段逻辑解决一个实盘常见麻烦:手动或 EA 开了很多图表,但有些品种已经没有持仓,有些持仓品种却没图表可看。代码用两张映射表做双向扫描,把图表状态和持仓状态对齐。 先扫持仓:遍历去重后的持仓品种数组,若某品种不在已开图表映射里,就先 SymbolSelect 把它加进市场报价窗,再用 CChart.Open 以 H1 周期开图,开图成功才写进 ch_symbols_map。temp_ch_id 小于 1 代表开图失败,会打印具体品种名方便排查。 再扫图表:把映射 CopyTo 成数组后反向遍历,若图表对应品种不在持仓集合 pos_symbols_set 中,直接 temp_chart_obj.Close() 关掉。若仍有持仓,则进入盈利计算分支,用 PositionsTotal 循环累加同品种仓位浮盈。 外汇与贵金属杠杆高、跳空频繁,这类自动关图逻辑能减少 MT5 终端资源占用,但关图不等于平仓,仓位风险依旧由你自己承担。

MQL5 / C++
if(ch_id_to_close>class="num">0)
  {
  ::ChartClose(ch_id_to_close);
  }
 }
 map_chart_obj.Detach();
 class=class="str">"cmt">//--- looking for a chart if there&class="macro">#x27;s a position
 for(class="type">int s_pos_idx=class="num">0; s_pos_idx<unique_pos_symbols_num; s_pos_idx++)
  {
  class="type">class="kw">string curr_pos_symbol=pos_symbols_arr[s_pos_idx];
  class=class="str">"cmt">//--- if there&class="macro">#x27;s no chart of the symbol
  if(!ch_symbols_map.ContainsKey(curr_pos_symbol))
    if(::SymbolSelect(curr_pos_symbol, true))
      {
      class=class="str">"cmt">//--- open a chart of the symbol
      CChart temp_chart_obj;
      class="type">long temp_ch_id=temp_chart_obj.Open(curr_pos_symbol, PERIOD_H1);
      if(temp_ch_id<class="num">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();
        }
      }
  }
 class="type">class="kw">string ch_symbols_arr[];
 class="type">long ch_ids_arr[];
 class="type">int unique_ch_symbols_num=ch_symbols_map.Count();
 if(ch_symbols_map.CopyTo(ch_symbols_arr, ch_ids_arr)!=unique_ch_symbols_num)
   class="kw">continue;
 class=class="str">"cmt">//--- looking for a position if there&class="macro">#x27;s a chart
 for(class="type">int s_ch_idx=class="num">0; s_ch_idx<unique_ch_symbols_num; s_ch_idx++)
  {
  class="type">class="kw">string curr_ch_symbol=ch_symbols_arr[s_ch_idx];
  class="type">long ch_id_to_close=ch_ids_arr[s_ch_idx];
  CChart temp_chart_obj;
  temp_chart_obj.Attach(ch_id_to_close);
  class=class="str">"cmt">//--- if there&class="macro">#x27;s no position of the symbol
  if(!pos_symbols_set.Contains(curr_ch_symbol))
    {
    temp_chart_obj.Close();
    }
  else
    {
    CPositionInfo curr_pos_info;
    class=class="str">"cmt">//--- calculate  a position profit
    class="type">class="kw">double curr_pos_profit=class="num">0.;
    class="type">int pos_num=::PositionsTotal();
    for(class="type">int pos_idx=class="num">0; pos_idx<pos_num; pos_idx++)
      if(curr_pos_info.SelectByIndex(pos_idx))
        {
        class="type">class="kw">string curr_pos_symbol=curr_pos_info.Symbol();
        if(!::StringCompare(curr_ch_symbol, curr_pos_symbol))

用后台图色和快捷键拼出多品种监控墙

这段逻辑把当前品种的所有持仓盈亏累加后,按正负给后台图表刷不同底色:盈利用 LightSkyBlue,亏损用 LightPink,持平用 Lavender。调用 ColorBackground 失败会在日志打印具体品种名,方便你定位是哪张子图权限或对象句柄出了问题。 刷完颜色立刻 Redraw,再 Detach 把临时图从前台脱离,避免堆积几十个窗口拖垮终端。最后用 keybd_event 模拟 Alt+R(菜单键 VK_MENU 配 'R'),循环两次发按下/抬起,等于自动平铺所有图表——Sleep(10) 是给系统留的事件间隔,太短可能吞键。 下半段在造一个自定义品种 DXY 做美元指数载体:InpStartDay 默认 2022.10.01,InpDxyDigits=3 控制报价小数位;Create 时 batch_size 设 1e6 根柱、is_selected=true 直接选中。建完先写 SECTOR_CURRENCY、背景色 clrKhaki、图表模式 BID,任何一步 SetProperty 返回 false 就 PrintFormat 并报退。外汇与贵金属自定义指数叠加实盘前,务必清楚这类合成品种存在报价断层与重算偏差的高风险,仅建议用历史数据回测验证。

MQL5 / C++
curr_pos_profit+=curr_pos_info.Profit()+curr_pos_info.Swap();
    }
    class=class="str">"cmt">//--- apply a class="type">class="kw">color
    class="type">class="kw">color profit_clr=clrLavender;
    if(curr_pos_profit>class="num">0.)
      {
       profit_clr=clrLightSkyBlue;
      }
    else if(curr_pos_profit<class="num">0.)
      {
       profit_clr=clrLightPink;
      }
    if(!temp_chart_obj.ColorBackground(profit_clr))
       ::PrintFormat("Failed to apply a profit class="type">class="kw">color for the symbol \"%s\"!", curr_ch_symbol);
    temp_chart_obj.Redraw();
    }
   temp_chart_obj.Detach();
   }
  class=class="str">"cmt">//--- tile windows(Alt+R)
  class="type">uchar vk=VK_MENU;
  class="type">uchar scan=class="num">0;
  class="type">uint flags[]= {class="num">0, KEYEVENTF_KEYUP};
  class="type">class="kw">ulong extra_info=class="num">0;
  class="type">uchar Key=&class="macro">#x27;R&class="macro">#x27;;
  for(class="type">int r_idx=class="num">0; r_idx<class="num">2; r_idx++)
    {
     user32::keybd_event(vk, scan, flags[r_idx], extra_info);
     ::Sleep(class="num">10);
     user32::keybd_event(Key, scan, flags[r_idx], extra_info);
     }
   }
class="kw">input class="type">class="kw">datetime InpStartDay=D&class="macro">#x27;class="num">01.10.class="num">2022&class="macro">#x27;; class=class="str">"cmt">// Start date
class="kw">input class="type">int InpDxyDigits=class="num">3;                 class=class="str">"cmt">// Index digits
class=class="str">"cmt">//--- create a custom symbol
class="type">class="kw">string custom_symbol="DXY",
       custom_group="Dollar Index";
CiCustomSymbol custom_symbol_obj;
class="kw">const class="type">uint batch_size = class="num">1e6;
class="kw">const class="type">bool is_selected = true;
class="type">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 < class="num">0)
   class="kw">return;
class=class="str">"cmt">//--- Integer properties
class=class="str">"cmt">//--- 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);
    class="kw">return;
   }
class=class="str">"cmt">//--- background class="type">class="kw">color
class="type">class="kw">color symbol_background_clr = clrKhaki;
if(!custom_symbol_obj.SetProperty(SYMBOL_BACKGROUND_COLOR, symbol_background_clr))
   {
    ::PrintFormat("Failed to set a background class="type">class="kw">color for the custom symbol \"%s\"", custom_symbol);
    class="kw">return;
   }
class=class="str">"cmt">//--- chart mode
ENUM_SYMBOL_CHART_MODE symbol_ch_mode=SYMBOL_CHART_MODE_BID;
if(!custom_symbol_obj.SetProperty(SYMBOL_CHART_MODE, symbol_ch_mode))
   {

「自定义美元指数符号的属性落盘」

在 MT5 里用代码造一个合成美元指数(如 DXY 类自定义品种)时,光建出名字不够,必须逐项把属性写进 CCustomSymbol 对象,否则图表上要么不显示报价,要么点值错得离谱。下面这段逻辑把小数位、报价点、交易开关和文本类字段一次性设死。 小数位由外部输入 InpDxyDigits 决定,点值直接用 1/10^digits 算,例如 digits=3 时点值为 0.001;tick size 跟 point 取同一值,避免最小跳动和单位不一致。交易模式被强制设为 SYMBOL_TRADE_MODE_DISABLED,因为这个合成品只用来盯盘和回测,不能真下单。 文本属性里 category 填 "Currency indices"、country 填 "US"、description 写 "Synthetic US Dollar Index"、exchange 标 "ICE",page 给到 ice.com 的 USDX 页面链接;path 拼成 Custom\\组名\\品种名,决定它在市场报价树里的位置。任何一步 SetProperty 返回 false 就 PrintFormat 报错并 return,防止半成品符号污染后续计算。 最后代码会预载六个基础货币对——EURUSD、USDJPY、GBPUSD、USDCAD、USDSEK、USDCHF——并检查它们的 M1 历史,作为合成指数的权重原料。外汇与贵金属合成品波动受杠杆和流动性影响,高风险,参数设错可能让回测曲线失真。

MQL5 / C++
  ::PrintFormat("Failed to set a chart mode for the custom symbol \"%s\"", custom_symbol);
  class="kw">return;
  }
class=class="str">"cmt">//--- digits
if(!custom_symbol_obj.SetProperty(SYMBOL_DIGITS, InpDxyDigits))
  {
  ::PrintFormat("Failed to set digits for the custom symbol \"%s\"", custom_symbol);
  class="kw">return;
  }
class=class="str">"cmt">//--- 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);
  class="kw">return;
  }
class=class="str">"cmt">//--- Double properties
class=class="str">"cmt">//--- point
class="type">class="kw">double symbol_point = class="num">1./::MathPow(class="num">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);
  class="kw">return;
  }
class=class="str">"cmt">//--- tick size
class="type">class="kw">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);
  class="kw">return;
  }
class=class="str">"cmt">//--- String properties
class=class="str">"cmt">//--- category
class="type">class="kw">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);
  class="kw">return;
  }
class=class="str">"cmt">//--- country
class="type">class="kw">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);
  class="kw">return;
  }
class=class="str">"cmt">//--- description
class="type">class="kw">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);
  class="kw">return;
  }
class=class="str">"cmt">//--- exchange
class="type">class="kw">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);
  class="kw">return;
  }
class=class="str">"cmt">//--- page
class="type">class="kw">string symbol_page = "https:class=class="str">"cmt">//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);
  class="kw">return;
  }
class=class="str">"cmt">//--- path
class="type">class="kw">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);
  class="kw">return;
  }
class=class="str">"cmt">//--- check quotes history
CBaseSymbol base_symbols[BASE_SYMBOLS_NUM];
class="kw">const class="type">class="kw">string symbol_names[]=
  {
  "EURUSD", "USDJPY", "GBPUSD", "USDCAD", "USDSEK", "USDCHF"
  };
ENUM_TIMEFRAMES curr_tf=PERIOD_M1;

◍ 逐品种校验报价历史加载耗时

多品种监控前,必须先确认每个品种的报价历史能正常拉取,否则后续矩阵运算全是无源之水。下面这段循环会遍历 BASE_SYMBOLS_NUM 个品种,逐个 Init 后调用 CheckLoadHistory,并用 GetTickCount64 掐表。 代码里用 PrintFormat 输出三项关键数据:品种序号与名称、检查返回码 check_load_code、耗时 time_elapsed_ms 换算成的秒数(除以 MS_IN_SEC)。若返回码小于 0,直接打印失败品种名并 return,整个初始化中断。 CBaseSymbol 类把单品种的状态封装干净:m_symbol 存 CSymbolInfo,m_tf 存周期,m_ticks_mx 是 tick 数据矩阵,m_start_date 和 m_last_idx 管时间窗口与游标。对外暴露 Init、CheckLoadHistory、LoadTicks、GetTicks 等接口,GetTicks 直接返回内部矩阵副本。 实盘里把 MS_IN_SEC 设成 1000,在 EURUSD/XAUUSD 这类品种上,一次历史加载可能从几毫秒到数百毫秒不等,取决于本地缓存与经纪商服务器响应。外汇与贵金属杠杆高、滑点风险大,加载失败往往意味着行情断层,不能忽视。

MQL5 / C++
::Print("\nChecking of quotes history is running...");
for(class="type">class="kw">ushort s_idx=class="num">0; s_idx<BASE_SYMBOLS_NUM; s_idx++)
  {
   CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx];
   class="type">class="kw">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+class="num">1, curr_symbol_name);
       class="type">class="kw">ulong start_cnt=::GetTickCount64();
       class="type">int check_load_code=ptr_base_symbol.CheckLoadHistory();
       ::PrintFormat("   Checking code: %I32d", check_load_code);
       class="type">class="kw">ulong time_elapsed_ms=::GetTickCount64()-start_cnt;
       ::PrintFormat("   Time elapsed: %class="num">0.3f sec", time_elapsed_ms/MS_IN_SEC);
       if(check_load_code<class="num">0)
         {
          ::PrintFormat("Failed to load quotes history for the symbol \"%s\"", curr_symbol_name);
          class="kw">return;
         }
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Class CBaseSymbol                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class CBaseSymbol : class="kw">public CObject
  {
     class=class="str">"cmt">//--- === Data members === ---
   class="kw">private:
      CSymbolInfo     m_symbol;
      ENUM_TIMEFRAMES m_tf;
      matrix          m_ticks_mx;
      class="type">class="kw">datetime        m_start_date;
      class="type">class="kw">ulong           m_last_idx;
     class=class="str">"cmt">//--- === Methods === ---
   class="kw">public:
      class=class="str">"cmt">//--- constructor/destructor
      class="type">void            CBaseSymbol(class="type">void);
      class="type">void           ~CBaseSymbol(class="type">void) {};
      class=class="str">"cmt">//---
      class="type">bool            Init(class="kw">const class="type">class="kw">string _symbol, class="kw">const ENUM_TIMEFRAMES _tf, class="type">class="kw">datetime start_date);
      class="type">int             CheckLoadHistory(class="type">void);
      class="type">bool            LoadTicks(class="kw">const class="type">class="kw">datetime _stop_date, class="kw">const class="type">uint _flags);
      matrix          GetTicks(class="type">void) class="kw">const
        {
         class="kw">return m_ticks_mx;
        };
      class="type">bool            SearchTickLessOrEqual(class="kw">const class="type">class="kw">double _dbl_time, vector &_res_row);
      class="type">bool            CopyLastTick(vector &_res_row);
  };
class=class="str">"cmt">//--- try to load ticks
::Print("\nLoading of ticks is running...");
now=::TimeCurrent();

批量拉取多品种 tick 的耗时实测

做跨品种联动分析时,先要把一组基础品种的 tick 一次性读进内存。下面这段逻辑用 COPY_TICKS_INFO 叠加 TIME_MS、BID、ASK 三个标志位,循环遍历 BASE_SYMBOLS_NUM 个品种并各自计时,方便你直接抄去测自己broker的报价延迟。

uint flags=COPY_TICKS_INFOCOPY_TICKS_TIME_MSCOPY_TICKS_BIDCOPY_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; } 代码逐行拆解: 第1行:flags 组合了信息位与毫秒时间、买卖价字段,决定 CopyTicks 返回哪些列。 第2行:first_tick_dbl_time 初始化为0,后面用来记录所有品种里最早的那条 tick 时间。 第3行:用 ushort 下标 s_idx 从0循环到 BASE_SYMBOLS_NUM-1,覆盖配置好的品种数组。 第5行:取当前下标对应的 CBaseSymbol 对象指针,封装了该品种的 tick 容器。 第6行:从 symbol_names 取品种名,如 EURUSD,用于日志输出。 第7行:PrintFormat 打印序号和品种名,序号从1开始更直观。 第8行:GetTickCount64 取循环前时间戳,作为耗时起点。 第9行:ResetLastError 清掉旧错误码,避免干扰本次 LoadTicks 的判断。 第10-14行:LoadTicks 失败就打印错误码并 return,实盘里可能倾向改成 continue 以免中断整批。 第15行:再次取时间戳相减,得到本次加载的毫秒耗时。 第16行:把毫秒除以 MS_IN_SEC(1000)打印成秒,保留三位小数。 第18行:GetTicks 返回 matrix,行是 tick、列是字段。 第19行:取第0行第0列即首 tick 的时间(double 型 MQL5 时间)。 第20-21行:若当前品种首 tick 更晚则更新全局最早时间,后续可做时间对齐。 标志位本身是按位移定义的:COPY_TICKS_INFO=1 是买卖价变动触发的 tick;COPY_TICKS_TIME_MS=1<<8 用毫秒精度;COPY_TICKS_BID=1<<9、ASK=1<<10 分别取买卖价;LAST=1<<11、VOLUME=1<<12、FLAGS=1<<13 对应成交价、量、旗标。如果你只关心价差,可省掉 LAST 和 VOLUME 来降内存。 实测日志里 EURUSD 在 12:01:11.802 启动检查,到 12:01:14.476 完成 LoadTicks,耗时 2.688 秒。外汇与贵金属报价受 broker 端推送频率影响,该数值可能在不同账户间波动,属正常高风险市场现象,建议在 MT5 策略测试器外用脚本实测自己的环境。

MQL5 / C++
class="type">uint flags=COPY_TICKS_INFO | COPY_TICKS_TIME_MS | COPY_TICKS_BID | COPY_TICKS_ASK;
class="type">class="kw">double first_tick_dbl_time=class="num">0.;
for(class="type">class="kw">ushort s_idx=class="num">0; s_idx<BASE_SYMBOLS_NUM; s_idx++)
  {
  CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx];
  class="type">class="kw">string curr_symbol_name=symbol_names[s_idx];
  ::PrintFormat("\n   Symbol #%hu: \"%s\"", s_idx+class="num">1, curr_symbol_name);
  class="type">class="kw">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());
    class="kw">return;
    }
  class="type">class="kw">ulong time_elapsed_ms=::GetTickCount64()-start_cnt;
  ::PrintFormat("   Time elapsed: %class="num">0.3f sec", time_elapsed_ms/MS_IN_SEC);
  class=class="str">"cmt">//--- looking for the 1st tick
  matrix ticks_mx=ptr_base_symbol.GetTicks();
  class="type">class="kw">double tick_dbl_time=ticks_mx[class="num">0][class="num">0];
  if(tick_dbl_time>first_tick_dbl_time)
    first_tick_dbl_time=tick_dbl_time;
  }

「多品种遍历的耗时实测」

在 MT5 专家日志里跑 dDxySymbol 这类扫描器,逐个品种吐出 Symbol #N 与 Checking code 后,会附带 Time elapsed。上面这段真实日志覆盖了 USDJPY、GBPUSD、USDCAD、USDSEK 四个直盘,单次校验耗时分别是 2.672 秒、1.922 秒、2.140 秒、1.422 秒。 耗时波动和品种流动性、点差刷新频率有关,USDJPY 因为报价更新最密,反而比 USDSEK 慢了近一倍。外汇与贵金属杠杆高、滑点随机,这类扫描结果只反映当时环境,不能当方向依据。 如果你也在写跨品种相关性脚本,直接把 Print 埋在 Symbol 循环里,就能复刻这份耗时表,顺便揪出哪一对货币对拖慢了整体轮询。

◍ 逐品种加载耗时的真实落差

在 MT5 专家日志里跑 dDxySymbol 这类美元指数合成脚本,不同品种的 tick 加载耗时差异极大。上面这段日志里,USDCHF 作为第 6 个符号只花了 1.531 秒就完成校验,而 EURUSD 作为第 1 个符号却耗了 63.032 秒,中间还穿插了 Loading of ticks is running 的提示。 USDJPY、GBPUSD、USDCAD 三个品种紧随其后,分别用了 5.094 秒、4.734 秒、4.937 秒。可见首个品种往往触发了底层历史 tick 的冷加载,后续品种命中缓存后速度才稳定下来。 如果你在写多品种美元指数监控 EA,别按平均耗时估算总运行时间。直接开 MT5 终端把这段日志模式打开,先单独跑一遍 EURUSD 预热本地 tick 缓存,再批量加载其余品种,整体耗时可能从一分钟级降到十秒级。外汇与贵金属交易杠杆高,脚本耗时只影响分析效率,不等于任何方向判断。

美元指数合成时的逐券扫描耗时

日志里能直接看到 dDxySymbol 遍历基础货币对的真实节奏:USDSEK 在 12:02:43.624 被抓出,到 12:03:18.484 打完收工,耗时 34.860 秒;紧接着 USDCHF 在 12:03:19.596 出现,仅用 4.719 秒就完成。两组数据摆在一起,说明不同品种的 tick 密度和检索路径差异极大,外汇与贵金属合成指数属高风险操作,耗时波动可能直接影响回测窗口。 代码段在做的事,是把自定义品种的 tick 历史从基础符号里拼出来。先打印提示,再把首 tick 的毫秒时间拆成秒和毫秒两部分输出,随后开一个 0.5e8 大小的 MqlTick 数组准备灌数据。 [CODE] //--- 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; [/CODE] 逐行拆解:Print 输出“自定义 symbol 的 tick 历史正在生成”;第一_tick_time_sec 把双精度毫秒时间整除 MS_IN_SEC 得到秒;first_tick_time_ms 取余得到毫秒尾数;PrintFormat 把秒级时间转字符串并附毫秒打印。active_tick_dbl_time 承接首 tick 时间,now_dbl_time 用当前秒乘 MS_IN_SEC 得当前毫秒;ticks_cnt 计数清零,arr_size 设 5 千万预留空间;ArrayResize 扩数组、ZeroMemory 清内存;base_prices_mx 建 BASE_SYMBOLS_NUM×2 零矩阵存基价。do 循环里先置 all_ticks_ok,遍历各基础符号,调 SearchTickLessOrEqual 找不晚于当前时间的 tick,失败就断循环,成功则写入矩阵行;全部 OK 才进指数价计算分支。 开 MT5 把这段塞进 EA 跑一遍,盯一眼日志里每个 Symbol 的 Time elapsed,就能知道你的合成逻辑在哪个品种上最容易卡。

MQL5 / C++
class=class="str">"cmt">//--- create a custom symbol ticks history
::Print("\nCustom symbol ticks history is being formed...");
class="type">long first_tick_time_sec=(class="type">long)(first_tick_dbl_time/MS_IN_SEC);
class="type">long first_tick_time_ms=(class="type">long)first_tick_dbl_time%(class="type">long)MS_IN_SEC;
::PrintFormat("  First tick time: %s.%d", ::TimeToString((class="type">class="kw">datetime)first_tick_time_sec,
               TIME_DATE|TIME_SECONDS), first_tick_time_ms);
class="type">class="kw">double active_tick_dbl_time=first_tick_dbl_time;
class="type">class="kw">double now_dbl_time=MS_IN_SEC*now;
class="type">uint ticks_cnt=class="num">0;
class="type">uint arr_size=class="num">0.5e8;
class="type">MqlTick ticks_arr[];
::ArrayResize(ticks_arr, arr_size);
::ZeroMemory(ticks_arr);
matrix base_prices_mx=matrix::Zeros(BASE_SYMBOLS_NUM, class="num">2);
do
  {
  class=class="str">"cmt">//--- collect base symbols ticks
  class="type">bool all_ticks_ok=true;
  for(class="type">class="kw">ushort s_idx=class="num">0; s_idx<BASE_SYMBOLS_NUM; s_idx++)
    {
    CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx];
    vector tick_prices_vc;
    class="type">bool to_break_loop=class="kw">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=class="kw">false;
      break;
      }
    }
  class=class="str">"cmt">//--- calculate index prices
  if(all_ticks_ok)
    {
    class="type">MqlTick last_ind_tick;

「合成品种tick的批量回填与主循环」

这段逻辑干了两件事:先把离线算出的指标 tick 批量写回自定义品种,再进入常驻循环持续合成新 tick。外汇与贵金属自定义品种合成属于高复杂度操作,实盘前务必在策略测试器跑通,品种报价异常可能导致回测与真实环境偏差。 回填阶段先按 10% 步长扩容 ticks_arr,避免频繁重分配:当 ticks_cnt 达到当前数组长度 arr_size 时,new_size 取 arr_size + 0.1*arr_size,若 ArrayResize 返回值不等于 new_size 则 continue 跳过本次写入。最后用 ArrayResize(ticks_arr, ticks_cnt) 截断多余容量,再调用 TicksReplace 一次性替换,返回值 ticks_replaced 可直接打印核对。 主循环里每次先 ZeroMemory 清空 base_prices_mx,再逐个基础品种 CopyLastTick 取最新价填入矩阵行;任一品种失败就置 all_ticks_ok=false 并 break。只有全部成功后,才用 CalcIndexPrices 算指数 tick,并通过 TicksAdd 写入自定义品种,循环尾部 Sleep(TICK_PAUSE) 控频,直到 IsStopped 才退出。 把 TICK_PAUSE 从默认毫秒值调小,合成 tick 密度会上升,但 CPU 占用和报价队列压力也同步增加;建议在 MT5 用自定义品种可见性开关观察 ticks_added 数量变化来验证频率。

MQL5 / C++
CalcIndexPrices(active_tick_dbl_time, base_prices_mx, last_ind_tick);
arr_size=ticks_arr.Size();
if(ticks_cnt>=arr_size)
  {
  class="type">uint new_size=(class="type">uint)(arr_size+class="num">0.1*arr_size);
  if(::ArrayResize(ticks_arr, new_size)!=new_size)
    class="kw">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);
class="type">int ticks_replaced=custom_symbol_obj.TicksReplace(ticks_arr, true);
class=class="str">"cmt">//--- main processing loop
::Print("\nA new tick processing is active...");
do
  {
  ::ZeroMemory(base_prices_mx);
  class=class="str">"cmt">//--- collect base symbols ticks
  class="type">bool all_ticks_ok=true;
  for(class="type">class="kw">ushort s_idx=class="num">0; s_idx<BASE_SYMBOLS_NUM; s_idx++)
    {
    CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx];
    vector tick_prices_vc;
    class="type">bool to_break_loop=class="kw">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=class="kw">false;
      break;
      }
    }
  class=class="str">"cmt">//--- calculate index prices
  if(all_ticks_ok)
    {
    class="type">MqlTick last_ind_tick, ticks_to_add[class="num">1];
    now=::TimeCurrent();
    now_dbl_time=MS_IN_SEC*now;
    CalcIndexPrices(now_dbl_time, base_prices_mx, last_ind_tick);
    ticks_to_add[class="num">0]=last_ind_tick;
    class="type">int ticks_added=custom_symbol_obj.TicksAdd(ticks_to_add, true);
    }
  ::Sleep(TICK_PAUSE);
  }
while(!::IsStopped());

◍ 画得少,看得清

MQL5 里的服务程序不挂靠任何图表,靠 %MQL5\Services 目录下的源码独立运行,这点让它和 EA、脚本天然存在资源争用风险,和指标冲突概率相对小一些。权限边界没人替你划,什么能碰、什么该让,得开发者自己定。 把服务当后台守护进程用,最直观的回报是图表区干净了。有用户拿它每天只触发一次,把交易历史落盘成 CSV,不占图表窗口。 若你正纠结多策略并行的数据同步,不妨开 MT5 建个空 Service 试水:写个定时器把虚拟持仓写进独立 SQLite 表,观察终端负载变化再决定是否上生产。外汇与贵金属杠杆高,这类后台程序出错可能放大滑点亏损。

让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到服务状态与异常推送,把重复劳动交给小布,你专注决策。

常见问题

脚本绑图表可直接读写,服务不绑图表只能通过 EventChartCustom 向指定图表发自定义事件,接收端要在图表程序里写 OnChartEvent 分支处理。
大概率 OnStart 里用了阻塞循环却没处理退出的 ServiceStop 信号,或者文件写在错误目录;服务必须放 MQL5\Services 且响应 Stop 事件。
可以,小布盯盘的品种页能展示服务是否存活、最近一次自定义事件时间,异常退出会推送到你的看盘界面,省去手动开日志。
需要外部数据源的协议解析、本地中转或直连、以及把价格包成 MarketBookAdd 或自定义事件发图表,还要处理断线重连与时钟对齐。
它提醒你把服务当长期后台进程设计:不依赖用户交互、做好自恢复、别在 OnStart 里写一次性逻辑,完整讨论见《MQL5 酷宝书 服务·基础篇》。