MQL5 中的定量分析:实现有前途的算法·进阶篇
📘

MQL5 中的定量分析:实现有前途的算法·进阶篇

第 2/3 篇

◍ EA 初始化与持仓统计的底子

把 ZigZag 接进 EA 的第一步,是在自定义初始化函数里用 iCustom 拿指标句柄。下面这段把品种、周期和指标参数(深度、偏离、回退、枢轴点)一次性传进去,拿不到句柄就直接 INIT_FAILED,避免在后续行情里对着空指针算折返。 global 变量 zigzagHandle 只存一个 descriptor,真正跨周期复用靠的是 OnIniti(symb) 里把 symb 显式传入,这样同一套逻辑能挂不同品种而不用重写。 持仓统计函数 AllProfit 给外部提供了按魔术码和订单方向过滤的能力:type 传 -1 表示不区分买卖、只认本 EA 的 Magic。实盘里若同时跑多个策略,不隔离 Magic 会把这些单子的浮动盈亏全混进一个数字,回测和盯盘都会失真。 外汇与贵金属杠杆高,这类统计若漏掉滑点或未平盈亏波动,可能让平仓逻辑误判总权益,开 MT5 把 Magic 和 InpDepth 调成与你图表一致再验证。

MQL5 / C++
class=class="str">"cmt">// Global variable for storing the indicator descriptor
class="type">int zigzagHandle;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
   trade.SetExpertMagicNumber(Magic);
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function custom                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnIniti(class="type">class="kw">string symb)
  {class=class="str">"cmt">// Loading the ZigZag indicator
   zigzagHandle = iCustom(symb, _Period, "ZigZag", InpDepth, InpDeviation, InpBackstep, InpPivotPoint);
   if (zigzagHandle == INVALID_HANDLE)
     {
      Print("Error loading the ZigZag indicator: ", GetLastError());
      class="kw">return(INIT_FAILED);
     }
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert deinitialization function                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(class="kw">const class="type">int reason)
  {
   Comment("");
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  Position Profit                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double AllProfit(class="type">int type=-class="num">1)
  {
   class="type">class="kw">double p=class="num">0;
    for(class="type">int i=PositionsTotal()-class="num">1; i>=class="num">0; i--)
     {
      if(PositionSelectByTicket(PositionGetTicket(i)))
        {
         if(PositionGetInteger(POSITION_MAGIC)==Magic)
           {
            if(PositionGetInteger(POSITION_TYPE)==type || type==-class="num">1)
              p+=PositionGetDouble(POSITION_PROFIT);
           }
        }
     }
   class="kw">return(p);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|  CloseAll                                                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CloseAll(class="type">int type=-class="num">1)
  {
   for(class="type">int i=PositionsTotal()-class="num">1; i>=class="num">0; i--)
     {

按魔术码和类型清仓的底层循环

这段逻辑跑在 EA 的持仓遍历里:先拿 PositionGetTicket(i) 取出第 i 个持仓的 ticket,再用 PositionSelectByTicket 把它激活到当前上下文。只有选中成功才继续判断,避免对无效持仓调用属性函数导致返零。 内层先比 POSITION_MAGIC 是否等于脚本里的 Magic 变量,不是自己开的单一律跳过。接着看 POSITION_TYPE:若传入的 type 与持仓方向一致,或 type 写成了 -1(代表不限方向),就直接 trade.PositionClose 平掉这一张。外汇与贵金属杠杆高,这种按魔术码隔离平仓能防止误伤手动单,但平仓失败不会抛异常,实盘前应在 MT5 策略测试器里跑一遍观察成交回执。 CountTrades 给网格加仓踩刹车用:倒序扫 PositionsTotal(),只统计 POSITION_SYMBOL 等于传入 symb 的持仓数。返回的是当前符号下真实挂着的单量,配合上限判断能控住马丁类策略的暴露。 Lot 函数把风险百分比落成手数:Risk>0 时手数 = 账户净值 × Risk ÷ 100000,再 NormalizeDouble 到两位小数。以 1 万美金、Risk=1 为例,算出来是 0.1 手,注意这是按标准手 10 万合约估的,黄金(XAUUSD)点值不同,需另测。 FindLastBuyPrice 专找某符号下最近一张买持仓的开仓价:遇到 POSITION_TYPE==0 且符号匹配就记下 POSITION_PRICE_OPEN 并 break。返回 0 代表没买单,上层可据此决定是否补仓或反向。

MQL5 / C++
if(PositionSelectByTicket(PositionGetTicket(i)))
  {
   if(PositionGetInteger(POSITION_MAGIC)==Magic)
     {
      if(PositionGetInteger(POSITION_TYPE)==type || type==-class="num">1)
         trade.PositionClose(PositionGetTicket(i));
     }
  }
 }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|    CountTrades                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CountTrades(class="type">class="kw">string symb)
  {
   class="type">int count=class="num">0;
   for(class="type">int i=PositionsTotal()-class="num">1; i>=class="num">0; i--)
     {
      if(PositionSelectByTicket(PositionGetTicket(i)))
        {
         if(PositionGetString(POSITION_SYMBOL)==symb)
           {
            count++;
           }
        }
     }
   class="kw">return(count);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|    Lot                                                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+  
class="type">class="kw">double Lot()
  {
   class="type">class="kw">double lot=Lots;
   if(Risk>class="num">0)
      lot=AccountInfoDouble(ACCOUNT_BALANCE)*Risk/class="num">100000;
   class="kw">return(NormalizeDouble(lot,class="num">2));
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double FindLastBuyPrice(class="type">class="kw">string symb)
  {
   class="type">class="kw">double pr=class="num">0;
   for(class="type">int i=PositionsTotal()-class="num">1; i>=class="num">0; i--)
     {
      if(PositionSelectByTicket(PositionGetTicket(i)) && PositionGetInteger(POSITION_TYPE)==class="num">0)
        {
         if(PositionGetString(POSITION_SYMBOL)==symb)
           {
            pr=PositionGetDouble(POSITION_PRICE_OPEN);
            break;
           }
        }
     }
   class="kw">return(pr);

「持仓回溯与波幅统计的函数骨架」

要在 MT5 里做价格行为复盘,先得把当前持仓的卖单开仓价和持仓类型捞出来。下面这段函数从持仓池倒序遍历,遇到指定品种且类型为 1(SELL)的仓位就返回其开仓价,没找到则返回 0。 FindLastSellPrice 只解决「上一张卖单在哪开的」;PositionType 则更泛化,返回指定品种持仓的类型编号,初始值设 8 表示无持仓匹配。注意 POSITION_TYPE 在 MQL5 里 0 是买、1 是卖,这两个函数都靠 PositionSelectByTicket 定位再读字段。 波幅统计交给 CalculateAverageMovement:写死回看 500 根 K 线,用 CopyBuffer 把 ZigZag 指标值塞进长度 500 的数组。若复制失败直接 Print 报错。外汇与贵金属杠杆高,这类统计仅作概率参考,实盘须自担风险。 别把初始值 8 当常态 PositionType 里 type=8 只是「没逮到仓位」的占位,不是真实持仓枚举。写 EA 时若拿 8 去比大小会出逻辑漏洞,建议调用方显式判断返回值是否小于 2。

MQL5 / C++
class="type">class="kw">double FindLastSellPrice(class="type">class="kw">string symb)
  {
   class="type">class="kw">double pr=class="num">0;
   for(class="type">int i=PositionsTotal()-class="num">1; i>=class="num">0; i--)
     {
      if(PositionSelectByTicket(PositionGetTicket(i)) && PositionGetInteger(POSITION_TYPE)==class="num">1)
        {
         if(PositionGetString(POSITION_SYMBOL)==symb)
           {
            pr=PositionGetDouble(POSITION_PRICE_OPEN);
            break;
           }
        }
     }
   class="kw">return(pr);
  }
class="type">int PositionType(class="type">class="kw">string symb)
  {
   class="type">int type=class="num">8;
   for(class="type">int i=PositionsTotal()-class="num">1; i>=class="num">0; i--)
     {
      if(PositionSelectByTicket(PositionGetTicket(i)))
        {
         if(PositionGetString(POSITION_SYMBOL)==symb)
           {
            type=(class="type">int)PositionGetInteger(POSITION_TYPE);
            break;
           }
        }
     }
   class="kw">return(type);
  }
class="type">void CalculateAverageMovement(class="type">class="kw">string symb, class="type">class="kw">double &averageMovement, class="type">class="kw">double &currentMovement) {
    class="kw">const class="type">int lookback = class="num">500; class=class="str">"cmt">// Number of bars for analysis
    class="type">class="kw">double sumMovements = class="num">0.0;
    class="type">int countMovements = class="num">0;
    class="type">class="kw">double lastExtremePrice = class="num">0.0;
    class="type">class="kw">double zigzagArray[class="num">500]; class=class="str">"cmt">// Array to store ZigZag values
    OnIniti(symb);
    class=class="str">"cmt">// Copy ZigZag values to array
    if (CopyBuffer(zigzagHandle, class="num">0, class="num">0, lookback, zigzagArray) <= class="num">0) {
        Print("Error copying indicator data");

◍ 把 ZigZag 均值位移接进交易判定

这段逻辑紧接前文的 CalculateAverageMovement,把算出的 averageMovement 与 currentMovement 直接喂给下单函数 Trade()。先通过 SymbolInfoDouble 取 Ask、Bid,再用 SymbolInfoInteger 拿小数位 dg、SymbolInfoDouble 取点值 pp,这些都是后续挂单距离计算的底料。 ATR 周期写死为 3:double atr = iATR(symb, PERIOD_CURRENT, 3); 用极短周期 ATR 去衡量当下波动,对外汇与贵金属这类高杠杆品种只是粗筛,真实滑点可能吞掉窄止损,务必在 MT5 里把周期改成 14 对比一下触发频率。 买卖信号是反直觉写法:sell 条件是 currentMovement > -averageMovement,buy 条件是 -currentMovement > averageMovement。意思是价格从最近极值回拉幅度若超过历史平均位移的反向值,就倾向反向开仓,属于均值回归思路,并非趋势跟随。 下面给出本小节涉及的 MQL5 片段,注意 CopyBuffer 失败会直接把两个位移置 -1 并 return,调用方 Trade() 里若拿到 -1 应当跳过下单,否则会用脏数据计算。

MQL5 / C++
   averageMovement = -class="num">1;
   currentMovement = -class="num">1;
   class="kw">return;
   }
   class=class="str">"cmt">// Copy ZigZag values to array
   if (CopyBuffer(zigzagHandle, class="num">0, class="num">0, lookback, zigzagArray) <= class="num">0) {
      Print("Error copying indicator data");
      averageMovement = -class="num">1;
      currentMovement = -class="num">1;
      class="kw">return;
   }
   for (class="type">int i = class="num">0; i < lookback; i++) {
      if (zigzagArray[i] != class="num">0 && zigzagArray[i] != lastExtremePrice) {
         if (lastExtremePrice != class="num">0) {
            class=class="str">"cmt">// Determine the movement direction
            class="type">class="kw">double movement = zigzagArray[i] - lastExtremePrice;
            sumMovements += movement;
            countMovements++;
         }
         lastExtremePrice = zigzagArray[i];
      }
   }
   class=class="str">"cmt">// Calculate the current movement
   class="type">class="kw">double lastMovement = iClose(symb, _Period, class="num">0) - lastExtremePrice;
   currentMovement = lastMovement;
   class=class="str">"cmt">// Calculate the average movement
   averageMovement = countMovements > class="num">0 ? sumMovements / countMovements : class="num">0.0;
   class=class="str">"cmt">// Print the result
   Print("Average movement: ", averageMovement);
   Print("Current movement: ", currentMovement);
   class=class="str">"cmt">// Release resources
   IndicatorRelease(zigzagHandle);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert Trade unction                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Trade(class="type">class="kw">string symb)
  {
   class="type">class="kw">double averageMovement = class="num">0;
   class="type">class="kw">double currentMovement = class="num">0;
   class="type">class="kw">double pr=class="num">0,sl=class="num">0,tp=class="num">0,hi=class="num">0,lo=class="num">0;
class=class="str">"cmt">// Call function for calculation
   CalculateAverageMovement(symb, averageMovement, currentMovement);
class=class="str">"cmt">// Use results
   class="type">class="kw">double Ask = SymbolInfoDouble(symb, SYMBOL_ASK);
   class="type">class="kw">double Bid = SymbolInfoDouble(symb, SYMBOL_BID);
   class="type">int dg=(class="type">int)SymbolInfoInteger(symb,SYMBOL_DIGITS);
   class="type">class="kw">double pp=SymbolInfoDouble(symb,SYMBOL_POINT);

   class="type">class="kw">double atr = iATR(symb, PERIOD_CURRENT, class="num">3);

class=class="str">"cmt">// Here define your logic for buying and selling
   class="type">bool sell  = currentMovement > -averageMovement; class=class="str">"cmt">// Buy condition
   class="type">bool buy = -currentMovement > averageMovement; class=class="str">"cmt">// Sell condition

   if(AllProfit()>Profit && Profit>class="num">0)

常见问题

在 OnInit 里用循环遍历 PositionsTotal,比对 position魔术码 与目标值并计数即可,注意用 PositionGetInteger 取字段。
遍历持仓后判断 PositionGetInteger(POSITION_TYPE) 与魔术码匹配且利润>0,再调 OrderSend 或 PositionClose 逐个平仓。
可以,小布能按你设定的魔术码和类型做持仓回溯与批量平仓提醒,把重复劳动交给它,你专注决策。
需记录每个 ZigZag 峰谷价、计算相邻拐点价差均值,再用标准差衡量位移离散度,作为触发阈值。
多为初始化未刷新缓存或循环索引用反,确保每次统计前重算总数并用整数索引正向遍历。