理解编程范式(第 2 部分):面向对象方式开发价格行为智能系统·综合运用
📘

理解编程范式(第 2 部分):面向对象方式开发价格行为智能系统·综合运用

第 3/3 篇

◍ 按魔术码与品种过滤平仓

在 EA 的持仓遍历逻辑里,先通过 PositionGetInteger 和 PositionGetString 把当前持仓的魔术码、品种、类型抓出来,再用 SymbolInfoInteger 拿该品种的报价小数位,决定后续 PrintFormat 里价格显示几位。 过滤条件写死为 positionMagic == magicNumber && positionSymbol == _Symbol,意味着只处理本 EA 自己下的、且品种与当前图表一致的单;其余来源的仓位一律不动,避免误平手动单或其他 EA 的仓。 平仓动作调用 myTrade.PositionClose(positionTicket, SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * 3),第二个参数是允许滑点,取当前点差乘以 3。外汇与贵金属点差随流动性跳动,实盘里这个 3 倍系数可能在某些时段偏紧导致平仓失败,建议你在 MT5 策略测试器用 2023 年 XAUUSD 数据跑一遍看成交率。 成功后若 enableAlerts 为 true 会弹 Alert 提示,并 PrintFormat 输出 ticket 与类型;失败则进入 else 分支处理。下面这段是原文核心片段,逐行对应上面逻辑。

MQL5 / C++
positionMagic = PositionGetInteger(POSITION_MAGIC);
positionSymbol = PositionGetString(POSITION_SYMBOL);
positionType = PositionGetInteger(POSITION_TYPE);
class="type">int positionDigits= (class="type">int)SymbolInfoInteger(positionSymbol, SYMBOL_DIGITS);
class="type">class="kw">double positionVolume = PositionGetDouble(POSITION_VOLUME);
class="type">ENUM_POSITION_TYPE positionType = (class="type">ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
if(positionMagic == magicNumber && positionSymbol == _Symbol) class=class="str">"cmt">//close the position
  {
   class=class="str">"cmt">//print the position details
   Print("*********************************************************************");
   PrintFormat(
     "#%I64u %s  %s  %.2f  %s [%I64d]",
     positionTicket, positionSymbol, EnumToString(positionType), positionVolume,
     DoubleToString(PositionGetDouble(POSITION_PRICE_OPEN), positionDigits), positionMagic
   );
   
   class=class="str">"cmt">//print the position close details
   PrintFormat("Close #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType));
   class=class="str">"cmt">//send the tradeRequest
   if(myTrade.PositionClose(positionTicket, SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * class="num">3)) class=class="str">"cmt">//success, position has been closed
     {
      if(enableAlerts)
       {
        Alert(
          _Symbol + " PROFIT LIQUIDATION: Just successfully closed POSITION(#" +
          IntegerToString(positionTicket) + "). Check the EA journal for more details."
        );
       }
      PrintFormat("Just successfully closed position: #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType));
      myTrade.PrintResult();
     }
   else  class=class="str">"cmt">//trade tradeRequest failed

「平仓报错与手数推演的实盘代码片段」

EA 在利润平仓失败时,会先判断 enableAlerts 开关,若开启则弹窗报警,把品种名、持仓 ticket 号拼进提示语,方便你立刻在终端定位是哪一笔单子没平掉。 随后用 PrintFormat 把平仓失败的单号、符号、多空类型以及 GetLastError 的错误码打到日志,这一步是排查 MT5 交易类故障的最低成本手段,外汇与贵金属品种在高波动时滑点拒单概率明显上升,属正常现象但需留痕。 BuySellPosition 里手数不是写死的:用权益除以 10000 再乘最小交易量,归一化到 2 位小数;若已有空单体积大于该值且保证金水平 >200,就补仓式地把空单体积加上去再归一。 手数还有上下限兜底——低于 0.01 取 SYMBOL_VOLUME_MIN,高于 SYMBOL_VOLUME_MAX 就截断为最大值,避免品种限制导致 OrderSend 直接报错。 TP/SL 以点数乘 _Point 偏移计算,买单价加 TP、减 SL,再用 _Digits 对齐报价精度,复制下面代码到 MT5 能直接验证你的品种小数位逻辑。

MQL5 / C++
  {
      class=class="str">"cmt">//print the information about the operation
      if(enableAlerts)
        {
         Alert(
            _Symbol + " ERROR ** PROFIT LIQUIDATION: closing POSITION(#" +
            IntegerToString(positionTicket) + "). Check the EA journal for more details."
         );
        }
      PrintFormat("Position clossing failed: #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType));
      PrintFormat("OrderSend error %d", GetLastError());class=class="str">"cmt">//print the error code
     }
   }
  }
 }
}
class="type">bool CEmaExpertAdvisor::BuySellPosition(class="type">int positionType, class="type">class="kw">string positionComment)
  {
   class="type">class="kw">double volumeLot = NormalizeDouble(((SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN) * AccountInfoDouble(ACCOUNT_EQUITY)) / class="num">10000), class="num">2);
   class="type">class="kw">double tpPrice = class="num">0.0, slPrice = class="num">0.0, symbolPrice;

   if(positionType == POSITION_TYPE_BUY)
    {
     if(sellPositionsVol > volumeLot && AccountInfoDouble(ACCOUNT_MARGIN_LEVEL) > class="num">200)
      {
       volumeLot = NormalizeDouble((sellPositionsVol + volumeLot), class="num">2);
      }
     if(volumeLot < class="num">0.01)
      {
       volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
      }
     if(volumeLot > SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX))
      {
       volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
      }

     volumeLot = NormalizeDouble(volumeLot, class="num">2);
     symbolPrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK);

     if(TP > class="num">0)
      {
       tpPrice = NormalizeDouble(symbolPrice + (TP * _Point), _Digits);
      }
     if(SL > class="num">0)
      {
       slPrice = NormalizeDouble(symbolPrice - (SL * _Point), _Digits);
      }

开仓函数里的手数校准与报错分支

这段逻辑处理的是卖单分支下的手数重算与下单回执。当持仓类型为 POSITION_TYPE_SELL 时,若已有买仓体积 buyPositionsVol 大于本次计划 volumeLot,且账户保证金水平 AccountInfoDouble(ACCOUNT_MARGIN_LEVEL) 超过 200,就把 volumeLot 重算为两者之和并保留两位小数。 手数不会裸奔出边界:低于 0.01 时回退到 SYMBOL_VOLUME_MIN,高于 SYMBOL_VOLUME_MAX 时截断到上限,最后再 NormalizeDouble 一次到 2 位精度。外汇与贵金属杠杆高,保证金水平阈值设 200 只是例子,实盘可能触发强平,需按经纪商规则复核。 下单价以 SYMBOL_BID 为基准,TP/SL 为点数时分别向下减、向上加 _Point 后按 _Digits 规范化。myTrade.Sell 返回真就弹 Alert 并打印结果,假则报 SymbolInfoDouble(SYMBOL_ASK) 处的错误并打出 GetLastError 代码,方便你直接在 MT5 Experts 日志里定位。

MQL5 / C++
if(positionType == POSITION_TYPE_SELL)
  {
    if(buyPositionsVol > volumeLot && AccountInfoDouble(ACCOUNT_MARGIN_LEVEL) > class="num">200)
     {
      volumeLot = NormalizeDouble((buyPositionsVol + volumeLot), class="num">2);
     }
    if(volumeLot < class="num">0.01)
     {
      volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
     }
    if(volumeLot > SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX))
     {
      volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
     }
    volumeLot = NormalizeDouble(volumeLot, class="num">2);
    symbolPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID);
    if(TP > class="num">0)
     {
      tpPrice = NormalizeDouble(symbolPrice - (TP * _Point), _Digits);
     }
    if(SL > class="num">0)
     {
      slPrice = NormalizeDouble(symbolPrice + (SL * _Point), _Digits);
     }
    if(myTrade.Sell(volumeLot, NULL, class="num">0.0, slPrice, tpPrice, positionComment)) class=class="str">"cmt">//successfully openend position
     {
      if(enableAlerts)
       {
        Alert(_Symbol, " Successfully openend SELL POSITION!");
       }
        myTrade.PrintResult();
      class="kw">return(true);
     }

◍ EA 参数注入与卖单报错回退

卖单下单失败时,代码先判断 enableAlerts 开关:开着就弹 Alert 报出品种名和当前 SYMBOL_ASK 价格,方便你立刻在 MT5 终端看到是哪一根tick卡住。 随后用 PrintFormat 把 GetLastError() 的错误码打到日志,return(false) 直接终止本次交易函数,避免重复发单放大滑点风险。外汇与贵金属波动剧烈,这类回退逻辑能防止错单连环触发。 下半段是 EA 的 input 区:magicNumber 默认 101,tradingTimeframe 用 H1,emaPeriod 15、emaShift 0;账户盈利目标 6.0%、亏损停手 10.0%,maxPositions 限 3 单同向,TP 5000 点、SL 500 点。 这些变量通过 CEmaExpertAdvisor ea(...) 构造函数注入,OnInit 里若 ea.GetInit()<=0 就返回 INIT_FAILED,EA 不会启动;OnDeinit 只调 ea.GetDeinit() 做清理。把 SL 从 500 改成 200 能在 MT5 里直观看到止损收紧后的持仓时长变化。

MQL5 / C++
   else
      {
       if(enableAlerts)
         {
          Alert(_Symbol, " ERROR opening a SELL POSITION at: ", SymbolInfoDouble(_Symbol, SYMBOL_ASK));
         }
       PrintFormat("ERROR: Opening a SELL POSITION: ErrorCode = %d",GetLastError());class=class="str">"cmt">//OrderSend failed, output the error code
       class="kw">return(class="kw">false);
      }
   }
 class="kw">return(class="kw">false);
}
class=class="str">"cmt">// Include the CEmaExpertAdvisor file so that it&class="macro">#x27;s code is available in this EA
class="macro">#include "EmaExpertAdvisor.mqh"
class=class="str">"cmt">//--User input variables
input class="type">long magicNumber = class="num">101;class=class="str">"cmt">//Magic Number(Set class="num">0 [Zero] to disable
input group ""
input ENUM_TIMEFRAMES tradingTimeframe = PERIOD_H1;class=class="str">"cmt">//Trading Timeframe
input class="type">int emaPeriod = class="num">15;class=class="str">"cmt">//Moving Average Period
input class="type">int emaShift = class="num">0;class=class="str">"cmt">//Moving Average Shift
input group ""
input class="type">bool enableTrading = true;class=class="str">"cmt">//Enable Trading
input class="type">bool enableAlerts = class="kw">false;class=class="str">"cmt">//Enable Alerts
input group ""
input class="type">class="kw">double accountPercentageProfitTarget = class="num">6.0;class=class="str">"cmt">//Account Percentage(%) Profit Target
input class="type">class="kw">double accountPercentageLossTarget = class="num">10.0;class=class="str">"cmt">//Account Percentage(%) Loss Target
input group ""
input class="type">int maxPositions = class="num">3;class=class="str">"cmt">//Max Positions(Max open positions in one direction)
input class="type">int TP = class="num">5000;class=class="str">"cmt">//TP(Take Profit Points/Pips [Zero(class="num">0) to diasable])
input class="type">int SL = class="num">500;class=class="str">"cmt">//SL(Stop Loss Points/Pips [Zero(class="num">0) to diasable])
class=class="str">"cmt">//Create an instance/object of the included CEmaExpertAdvisor class
class=class="str">"cmt">//with the user inputed data as the specified constructor parameters
CEmaExpertAdvisor ea(
      magicNumber, tradingTimeframe, emaPeriod, emaShift,
      enableTrading, enableAlerts, accountPercentageProfitTarget,
      accountPercentageLossTarget, maxPositions, TP, SL
   );
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
class=class="str">"cmt">//---
   if(ea.GetInit() <= class="num">0)
     {
      class="kw">return(INIT_FAILED);
     }
class=class="str">"cmt">//---
   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)
  {
class=class="str">"cmt">//---
   ea.GetDeinit();
  }

「把交易逻辑塞进 OnTick 主循环」

EA 的所有实时动作都从 OnTick() 开始跑,每来一个报价就触发一次。上面这段是典型骨架:先抓 EMA 数值和持仓数据,再判断环境是否允许交易,允许才下单并管损益,最后把状态画到图上。 具体顺序不能乱:ea.GetEma() 拿快慢线,ea.GetPositionsData() 刷新当前仓位,这两步是后面判断的原料。若 TradingIsAllowed() 返回 false,后面整段交易代码直接跳过,只留 PrintOnChart() 刷界面,避免乱单。 在 MT5 里新建 EA 时,把这段直接贴进模板的 OnTick 函数体,就能跑通“指标→判断→交易→展示”的最小闭环。外汇与贵金属杠杆高,实盘前务必用策略测试器以 2023 年 XAUUSD 的 M15 数据回测,观察成交密度与回撤。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">//---
   ea.GetEma();
   ea.GetPositionsData();
   if(ea.TradingIsAllowed())
     {
       ea.TradeNow();
       ea.ManageProfitAndLoss();
     }
   ea.PrintOnChart();
  }

把这条线请下神坛

随文附带的 ZIP 里压了 8 个文件:PhoneClass.mqh 3.25 KB、SmartPhoneClass.mqh 1.66 KB,以及把过程化 EMA 策略重写后的 OOP_PriceActionEMA.mq5 仅 3 KB,对照 Procedural_PriceActionEMA.mq5 的 23.33 KB,体量差出近 7 倍,模块拆开之后单文件确实轻了。 开 MT5 把 OOP_Article_-_All_Source_Code_Files.zip 解到 MQL5/Include 与 Experts 下,直接编译 OOP_PriceActionEMA.mq5,你能直观看到类封装把原来缠在一起的均线逻辑归到了 EmaExpertAdvisor.mqh 里。 外汇与贵金属杠杆高、回撤可能超预期,跑任何 EA 前先在策略测试器用历史数据验证,别把封装漂亮当成胜率保证。 OOP 只是把现实实体映射成代码结构的工具,不是圣杯;哪天你写 EA 卡在状态管理乱成一团,回来翻这几个 mqh 怎么分层的,比追新框架实在。

常见问题

检查平仓循环里是否同时比对 OrderMagicNumber() 与 OrderSymbol(),漏掉品种判断会误平其他周期开的同魔术码单。
在手数计算后加最小手数下限校验,若低于 MarketInfo(symbol,MODE_MINLOT) 则回退到最小手数或跳过下单。
可以,小布盯盘的 AIGC 已内置常见 EA 报错与手数推演诊断,打开对应品种页即可看到逻辑校验结果。
在 OrderSend 返回无效 ticket 时记录错误码并跳过本次循环,不要直接改全局变量,避免下一 tick 重复发单。
会,建议用静态变量做 tick 节流或只在新柱触发核心逻辑,否则高频品种上 EA 占用偏多可能漏单。