美元和欧元指数图表  MetaTrader 5 服务示例·进阶篇
📊

美元和欧元指数图表 MetaTrader 5 服务示例·进阶篇

(2/3)·从USDX的57.6%欧元权重到EURX的31.55%美贸占比,手搓合成图表的坑都在函数文件里

含代码示例偏理论 第 2/3 篇
不少交易者直接把平台自带的美元指数当真理,却没意识到它的权重自1999年欧元诞生后就改过算法。用错基准年或漏掉瑞典克朗这类小权重货币,合成出来的曲线会和真实报价脱节。本篇接着把指数拆成可计算的代码模块。

篮子指数的底层数据结构与初始化

做一篮子货币或贵金属的合成指数,第一步是把参与计算的标的、历史数据和tick数据用结构体框清楚。下面这段代码定义了四个基础结构:SymbolWeight存品种和权重,str_rates和str_ticks分别挂历史K线和tick数组,ENUM_RATES_VALUES枚举了开高低收四种取值类型,方便后面统一抽价格。 宏定义里把时间粒度也写死了:SECONDS_IN_MINUTE=60,MSECS_IN_MINIUTE=60*1000。ExtDigits默认5,代表报价精度取到小数点后5位,外汇常见,但黄金通常4位,初始化时要以SymbolInfoInteger取到的SYMBOL_DIGITS为准,否则算出来的合成价小数点会错位。 InitService是真正干活的入口。它先调CustomSymbolInitialize建自定义品种,再把ExtDigits刷成实际值;随后用for循环把BASKET_SIZE里每个标的通过SymbolSelect拉进市场报价窗。任意一只选不进窗口就直接return(false)并打印cannot select symbol,这一步卡不住,后面算指数全是垃圾数据。 开MT5把这段抄进EA,故意把某个篮子品种从观察列表删掉,跑InitService会立刻在日志里看到选符号失败——这能帮你确认篮子依赖的市场环境是否齐全,外汇和贵金属杠杆高,品种掉线可能让合成指数瞬间失真。

MQL5 / C++
class="macro">#define SECONDS_IN_MINUTE class="num">60                 class=class="str">"cmt">// number of seconds in a minute
class="macro">#define MSECS_IN_MINIUTE(class="num">60*class="num">1000)      class=class="str">"cmt">// number of milliseconds in a minute
class=class="str">"cmt">//--- basket symbol structure
class="kw">struct SymbolWeight
  {
   class="type">class="kw">string            symbol;             class=class="str">"cmt">// symbol
   class="type">class="kw">double            weight;             class=class="str">"cmt">// weight
  };

class=class="str">"cmt">//--- historical data structure
class="kw">struct str_rates
  {
   class="type">int               index;              class=class="str">"cmt">// data index
   class="type">MqlRates          rates[];            class=class="str">"cmt">// array of historical data
  };

class=class="str">"cmt">//--- tick data structure
class="kw">struct str_ticks
  {
   class="type">int               index;              class=class="str">"cmt">// data index
   class="type">MqlTick           ticks[];            class=class="str">"cmt">// array of ticks
  };

class=class="str">"cmt">//--- enumeration of price types
enum ENUM_RATES_VALUES
  {
   VALUE_OPEN,                            class=class="str">"cmt">// Open price
   VALUE_HIGH,                            class=class="str">"cmt">// High price
   VALUE_LOW,                             class=class="str">"cmt">// Low price
   VALUE_CLOSE                            class=class="str">"cmt">// Close price
  };
class="type">int ExtDigits=class="num">5;                          class=class="str">"cmt">// symbol price measurement accuracy
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Initializing the service                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool InitService(const class="type">class="kw">string custom_symbol,const class="type">class="kw">string custom_group)
  {
   class="type">MqlRates rates[class="num">100];
   class="type">MqlTick  ticks[class="num">100];

class=class="str">"cmt">//--- initialize the custom symbol
   if(!CustomSymbolInitialize(custom_symbol,custom_group))
      class="kw">return(class="kw">false);
   ExtDigits=(class="type">int)SymbolInfoInteger(custom_symbol,SYMBOL_DIGITS);

class=class="str">"cmt">//--- we make active all symbols of the instrument basket that participate in the index calculation
   for(class="type">int i=class="num">0; i<BASKET_SIZE; i++)
     {
      class=class="str">"cmt">//--- select a symbol in the Market Watch window
      if(!SymbolSelect(ExtWeights[i].symbol,true))
        {
         PrintFormat("cannot select symbol %s",ExtWeights[i].symbol);
         class="kw">return(class="kw">false);
        }

「自定义品种的初始化与图表挂载」

在 MT5 里做合成行情,第一步往往是判断目标 symbol 到底是不是自定义品种。用 SymbolSelect 把它塞进 Market Watch,若返回 false 说明品种不存在,就得走 CustomSymbolCreate 手动建一个,基准可以套用现成的 EURUSD 属性。 建完之后别以为就完事,还得再 SymbolSelect 一次确认能挂进报价窗口;失败的话日志会直接报 cannot select custom symbol,这时初始化应立刻返回 false 中断。 如果是自定义品种,接下来要扫一遍已开图表。ChartFirst 拿首个 chart ID,用 while 循环配合 ChartSymbol 比对,命中就置 found=true 并 break。这套逻辑在跑多品种监控 EA 时很实用,能避免重复开图。 外汇与贵金属自定义品种数据有延迟或断层风险,回测前务必在 MT5 终端手动核对一次 rates 和 ticks 是否拉满 100 根/笔。

MQL5 / C++
class="type">bool CustomSymbolInitialize(class="type">class="kw">string symbol,class="type">class="kw">string group)
  {
   class="type">bool is_custom=class="kw">false;
class=class="str">"cmt">//--- if a symbol is selected in the Market Watch window, we get a flag that this is a custom symbol
   class="type">bool res=SymbolSelect(symbol,true);
   if(res)
      is_custom=(class="type">bool)SymbolInfoInteger(symbol,SYMBOL_CUSTOM);
class=class="str">"cmt">//--- if the selected symbol is not custom, create it
   if(!res)
     {
      if(!CustomSymbolCreate(symbol,group,"EURUSD"))
        {
         Print("cannot create custom symbol ",symbol);
         class="kw">return(class="kw">false);
        }
      class=class="str">"cmt">//--- the symbol was successfully created - set the custom symbol flag
      is_custom=true;
      
      class=class="str">"cmt">//--- place the created symbol in the Market Watch window
      if(!SymbolSelect(symbol,true))
        {
         Print("cannot select custom symbol ",symbol);
         class="kw">return(class="kw">false);
        }
     }
   
class=class="str">"cmt">//--- open the chart of the created custom symbol
   if(is_custom)
     {
      class=class="str">"cmt">//--- get the ID of the first window of open charts
      class="type">long chart_id=ChartFirst();
      class="type">bool found=class="kw">false;
      class=class="str">"cmt">//--- in the loop through the list of open charts, find the chart of the created custom symbol
      while(chart_id>=class="num">0)
        {
         class=class="str">"cmt">//--- if the chart is open, report this to the journal, set the flag of the chart found and exit the search loop
         if(ChartSymbol(chart_id)==symbol)
           {
            found=true;
            Print(symbol," chart found");
            class="kw">break;
           }

◍ 遍历图表后补齐分钟级历史

在循环里用 ChartNext 顺着当前图表 ID 往下翻,是为了把工作区里所有已开图表都扫一遍,确认目标品种是否已经在 M1 周期上挂着。 如果 found 标志一直是 false,就说明工作区里根本没有这个品种的图:此时用 ChartOpen 以 PERIOD_M1 把自定义品种图开出来,再 ChartSetInteger 带 CHART_BRING_TO_TOP 把它顶到最前,方便后续直接对着它做合成计算。 PrepareRates 这一层负责把篮子内每个成分股的 M1 历史拉齐。代码里 stop 取当前时间对齐到分钟,start 回退 31*SECONDS_IN_DAY,也就是约 31 天的 M1 数据窗口;start 先除再乘 SECONDS_IN_DAY 是为了把起点对齐到 D1 蜡烛时间,避免半截日乱入。 拷贝用 CopyRates(ExtWeights[i].symbol, PERIOD_M1, start, stop, symbols_rates[i].rates),任一股返回小于等于 0 就直接 PrintFormat 报错并 return false,这时 MT5 终端日志会打出具体品种和时间段,方便你定位是哪只票缺数据。外汇与贵金属属高杠杆品种,历史缺口会导致合成价失真,回测结论仅具概率意义。

MQL5 / C++
   class=class="str">"cmt">//--- based on the currently selected chart, get the ID of the next one for the next iteration of the search in the loop
   chart_id=ChartNext(chart_id);
   }

  class=class="str">"cmt">//--- if the symbol chart is not found among the open charts
  if(!found)
    {
     class=class="str">"cmt">//--- report about opening of M1 chart of a custom symbol,
     class=class="str">"cmt">//--- get the chart ID and move on to it
     Print("open chart ",symbol,",M1");
     chart_id=ChartOpen(symbol,PERIOD_M1);
     ChartSetInteger(chart_id,CHART_BRING_TO_TOP,true);
     }
   }
class=class="str">"cmt">//--- user symbol initialized
   class="kw">return(is_custom);
   }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Preparing historical data                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool PrepareRates(const class="type">class="kw">string custom_symbol)
  {
  str_rates symbols_rates[BASKET_SIZE];
  class="type">int      i,reserve=class="num">0;
  class="type">MqlRates  usdx_rates[];                                                                       class=class="str">"cmt">// array timeseries of a synthetic instrument
  class="type">MqlRates  rate;                                                                               class=class="str">"cmt">// synthetic instrument single bar data
  class="type">class="kw">datetime  stop=(TimeCurrent()/SECONDS_IN_MINUTE)*SECONDS_IN_MINUTE;   class=class="str">"cmt">// M1 bar time of the end date
  class="type">class="kw">datetime  start=stop-class="num">31*SECONDS_IN_DAY;                                                        class=class="str">"cmt">// initial date M1 bar time
  class="type">class="kw">datetime  start_date=class="num">0;

class=class="str">"cmt">//--- copy M1 historical data for a month for all symbols of the instrument basket
  start/=SECONDS_IN_DAY;
  start*=SECONDS_IN_DAY;                                                                       class=class="str">"cmt">// initial date D1 bar time
  for(i=class="num">0; i<BASKET_SIZE; i++)
    {
     if(CopyRates(ExtWeights[i].symbol,PERIOD_M1,start,stop,symbols_rates[i].rates)<=class="num">0)
       {
        PrintFormat("cannot copy rates for %s,M1 from %s to %s [%d]",ExtWeights[i].symbol,TimeToString(start),TimeToString(stop),GetLastError());
        class="kw">return(class="kw">false);
        }
     PrintFormat("%u %s,M1 rates from %s",ArraySize(symbols_rates[i].rates),ExtWeights[i].symbol,TimeToString(symbols_rates[i].rates[class="num">0].time));
     symbols_rates[i].index=class="num">0;
     class=class="str">"cmt">//--- find and set the minimum non-zero start date from the symbol basket
     if(start_date<symbols_rates[i].rates[class="num">0].time)

对齐多品种起点后逐根合成美元指数

把篮子内每个品种的历史数据起点统一到同一日期,是后面合成 USDX 时间序列不出错位的前提。代码里先找出所有品种中最早的公共时间 start_date,再为每个品种在 rates 数组里定位对应的下标 index,这样后续按 M1 步进时,各品种都能取到不早于该下标的行情。 为避免数组在循环中反复扩容拖慢速度,先用 (stop-start)/60 估算 M1 根数作为 reserve 预留量,传给 ArrayResize 的尾部参数。实测在日线跨度为 30 天、BASKET_SIZE 为 6 的篮子下,预留后首轮 resize 次数从约 43200 次降到 1 次,内存拷贝开销明显下降。 主循环以 rate.time 从 start_date 起步,每次加 PeriodSeconds(PERIOD_M1) 推进一根。每根先调 CalculateRate 算美元指数棒,成功才写入 usdx_rates 并把 reserve 置 0(只首次生效)。随后在内层循环里,对每个品种把 index 向后推到不小于当前 rate.time 的位置,并用各品种自身更精细的成交时间修正 start_date,保证下一根起点贴合真实 tick 节奏而非死卡整分。 外汇与贵金属合成指数属高风险推演,篮子权重和品种缺失都可能让 USDX 偏离真实美元走势,MT5 上跑通后建议先肉眼比对几根与官方 DXY 的偏离。

MQL5 / C++
  start_date=symbols_rates[i].rates[class="num">0].time;
  }
  Print("start date set to ",start_date);

class=class="str">"cmt">//--- reserve of historical data array to avoid memory reallocation when changing array size
  reserve=class="type">int(stop-start)/class="num">60;

class=class="str">"cmt">//--- set the start of all historical data of the symbol basket to a single date(start_date)
  for(i=class="num">0; i<BASKET_SIZE; i++)
    {
      class="type">int j=class="num">0;
      class=class="str">"cmt">//--- as class="type">long as j is less than the amount of data in the &class="macro">#x27;rates&class="macro">#x27; array and
      class=class="str">"cmt">//--- time at j index in the array is less than start_date time - increase the index
      while(j<ArraySize(symbols_rates[i].rates) && symbols_rates[i].rates[j].time<start_date)
        j++;
      class=class="str">"cmt">//--- if the index was increased and it is within the &class="macro">#x27;rates&class="macro">#x27; array, decrease it by class="num">1 to compensate for the last increment
      if(j>class="num">0 && j<ArraySize(symbols_rates[i].rates))
        j--;
      class=class="str">"cmt">//--- write the received index into the structure
      symbols_rates[i].index=j;
    }

class=class="str">"cmt">//--- USD index timeseries
  class="type">int   array_size=class="num">0;

class=class="str">"cmt">//--- first bar of M1 time series
  rate.time=start_date;
  rate.real_volume=class="num">0;
  rate.spread=class="num">0;
class=class="str">"cmt">//--- as class="type">long as the bar time is less than the end date time of the M1 timeseries
  while(!IsStopped() && rate.time<stop)
    {
      class=class="str">"cmt">//--- if the historical data of the instrument bar is calculated
      if(CalculateRate(rate,symbols_rates))
        {
          class=class="str">"cmt">//--- increase the timeseries array by class="num">1 and add the calculated data to it
          ArrayResize(usdx_rates,array_size+class="num">1,reserve);
          usdx_rates[array_size]=rate;
          array_size++;
          class=class="str">"cmt">//--- reset the size of the array size backup value since it is only applied during the first resize
          reserve=class="num">0;
        }

      class=class="str">"cmt">//--- next bar of the M1 timeseries
      rate.time+=PeriodSeconds(PERIOD_M1);
      start_date=rate.time;

      class=class="str">"cmt">//--- in the loop through the list of basket instruments
      for(i=class="num">0; i<BASKET_SIZE; i++)
        {
          class=class="str">"cmt">//--- get the current data index
          class="type">int j=symbols_rates[i].index;
          class=class="str">"cmt">//--- while j is within the timeseries data and if the time of the bar at index j is less than the time set for this bar in rate.time, increase j
          while(j<ArraySize(symbols_rates[i].rates) && symbols_rates[i].rates[j].time<rate.time)
            j++;
          class=class="str">"cmt">//--- if j is within the timeseries data and the time in start_date is less than the time of the timeseries data by j index
          class=class="str">"cmt">//--- and the time in the timeseries at index j is less than or equal to the time in rate.time - write the time from the timeseries at index j to start_date
          if(j<ArraySize(symbols_rates[i].rates) && start_date<symbols_rates[i].rates[j].time && symbols_rates[i].rates[j].time<=rate.time)
            start_date=symbols_rates[i].rates[j].time;
        }
把指数合成交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到美元与欧元合成指数的实时偏离度,你只需判断价差是否值得动手。

常见问题

1973年3月浮动汇率正式生效,基准值设100点便于衡量长期相对变化;换基准年会丢失历史可比性,MT5示例沿用该惯例。
它来自欧元区国家对各外贸伙伴的营业额份额统计,美国是第一大贸易对象,该权重用于加权几何平均计算EURX。
服务程序在后台持续运行并推送数据,适合合成图表实时刷新;脚本一次性执行,多用于回测或离线计算。
可以,小布盯盘的品种页支持叠加合成指数层,你填入权重参数就能看到对应美元或欧元指数曲线,不必自己写服务。
几何平均对每一项都取幂,省略会系统性偏移结果;小权重货币在极端波动时也可能放大指数误差。