交易者生活窍门: 利用 defines (#define) 融合 ForEach·综合运用
📘

交易者生活窍门: 利用 defines (#define) 融合 ForEach·综合运用

第 3/3 篇

把 MQL4 保证金算法映射到 MQL5 的坑

在把老 MQL4 指标移植到 MT5 时,保证金计算模式常需要手动对齐。上面这段 switch 把外汇、期货、CFD 三种模式分别映射到 0、2、1 三个 double 值,default 分支则弹窗报警并返回 NaN,避免静默出错。 遇到未覆盖的 profit_calc_mode,代码会拼出 'MQL4 MODE_MARGINCALCMODE returned MQL5 …' 的提示并同时 Alert 和 Print。你在 MT5 里跑这段,若终端突然弹警告,说明品种用了非标准计价模式,得回代码补 case。 同文件里还用宏封装了运行环境检查:IsConnected 取 TERMINAL_CONNECTED,IsDemo 比对 ACCOUNT_TRADE_MODE_DEMO,IsDllsAllowed 看 TERMINAL_DLLS_ALLOWED。外汇和贵金属杠杆高、波动猛,EA 上线前用这些宏先卡一道环境,可能省掉一半莫名不成交的工单。

MQL5 / C++
case SYMBOL_CALC_MODE_FOREX:
     class="kw">return((class="type">class="kw">double)class="num">0);
case SYMBOL_CALC_MODE_FUTURES:
     class="kw">return((class="type">class="kw">double)class="num">2);
case SYMBOL_CALC_MODE_CFD:
     class="kw">return((class="type">class="kw">double)class="num">1);
class="kw">default :
     {
      class="type">class="kw">string text="MQL4 MODE_MARGINCALCMODE returned MQL5 "+EnumToString(profit_calc_mode);
      Alert(text);
      Print(text);
      class="kw">return(class="type">class="kw">double("nan"));
     }
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 Check.mqh |
class=class="str">"cmt">//|            Copyright class="num">2018, MetaQuotes Software Corp. |
class=class="str">"cmt">//|                                     http://wmua.ru/slesar/ |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2018, MetaQuotes Software Corp."
class="macro">#class="kw">property link      "http:class=class="str">"cmt">//wmua.ru/slesar/"
class="macro">#class="kw">property version   "class="num">1.003"
class=class="str">"cmt">//--- check connection between client and server
class="macro">#define IsConnected(class="type">bool)TerminalInfoInteger(TERMINAL_CONNECTED)
class=class="str">"cmt">//--- check if Expert Advisor runs on demo account
class="macro">#define IsDemo(class="type">bool)(AccountInfoInteger(ACCOUNT_TRADE_MODE)==(ENUM_ACCOUNT_TRADE_MODE)ACCOUNT_TRADE_MODE_DEMO)
class=class="str">"cmt">//--- check if DLL function calls are allowed for Expert Advisors
class="macro">#define IsDllsAllowed(class="type">bool)TerminalInfoInteger(TERMINAL_DLLS_ALLOWED)
class=class="str">"cmt">//--- check if Expert Advisors are allowed to run
class="macro">#define IsExpertEnabled(class="type">bool)TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)
class=class="str">"cmt">//--- check if library function calls are allowed for Expert Advisors
class="macro">#define IsLibrariesAllowed(class="type">bool)MQLInfoInteger(MQL_DLLS_ALLOWED)
class=class="str">"cmt">//--- check if Expert Advisor runs in Strategy Tester optimization mode
class="macro">#define IsOptimization(class="type">bool)MQLInfoInteger(MQL_OPTIMIZATION)
class=class="str">"cmt">//--- check if Expert Advisor runs in test mode

◍ 把环境宏和报价函数揉进EA骨架

MT5里有一批预定义宏,本质是给 MQLInfoInteger / TerminalInfoString / SymbolInfoTick 等底层调用套了层短名。比如 IsTesting 展开就是 (bool)MQLInfoInteger(MQL_TESTER),用来判断当前是否在策略测试器里跑;IsTradeAllowed 对应 MQL_TRADE_ALLOWED,确认终端是否放行自动交易。把这些写成 #define,EA里就能直接用 if(IsTradeAllowed) 而不必每次拼长函数。 _Digits、_Point、_Symbol、_Period 这类下划线变量也同理:_Digits 等价于 Digits(),返回当前品种小数位(EURUSD 通常是 5,XAUUSD 常见 2);_Point 是 Point(),即 1 点的绝对值。注意原文里 IsTradeContextBusy 被直接 #define 成 false——这是旧版兼容写法,实盘里终端已不再用忙标志阻塞交易上下文,但复制老代码时要知道它永远返回假。 Ask 和 Bid 的定义值得细看。原文先给出用 SymbolInfoTick 取 tick 的 GetAsk/GetBid 函数,再被 #define Ask SymbolInfoDouble(__Symbol,SYMBOL_ASK) 覆盖。两者都能拿到卖价买价,但后者少一次结构体构造,tick 方式能顺带拿 last/volume。 [CODE] #define IsTesting (bool)MQLInfoInteger(MQL_TESTER) //--- 检查智能交易系统是否允许交易, 且交易前后关联是否繁忙 #define IsTradeAllowed (bool)MQLInfoInteger(MQL_TRADE_ALLOWED) //--- 返回有关交易前后关联的信息 #define IsTradeContextBusy false //--- 检查智能交易系统是否在可视模式下进行测试 #define IsVisualMode (bool)MQLInfoInteger(MQL_VISUAL_MODE) //--- 返回拥有客户终端的公司名称 #define TerminalCompany TerminalInfoString(TERMINAL_COMPANY) //--- 返回客户端名称 #define TerminalName TerminalInfoString(TERMINAL_NAME) //--- 返回客户端启动的目录 #define TerminalPath TerminalInfoString(TERMINAL_PATH) //+------------------------------------------------------------------+ //--- _Digits 变量存储小数点后的位数, #define _Digits Digits() //--- _Point 变量包含报价货币中当前品种的点数大小 #define _Point Point() //--- _LastError 变量包含最后一个错误的代码 #define _LastError GetLastError() //--- _Period 变量包含当前图表的时间帧数值 #define _Period Period() //--- _StopFlag 变量包含程序停止的标志 #define _StopFlag IsStopped() //--- _Symbol 变量包含当前图表的品种名称 #define _Symbol Symbol() //--- _UninitReason 变量包含程序未初始化原因的代码 #define _UninitReason UninitializeReason() //--- 当前品种最后知道的卖方价格 (ask 价格) #define Ask GetAsk() //--- 当前品种最后知道的买方价格 (报价, bid 价格) #define Bid GetBid() double GetAsk() { MqlTick tick; SymbolInfoTick(Symbol(),tick); return(tick.ask); } double GetBid() { MqlTick tick; SymbolInfoTick(Symbol(),tick); return(tick.bid); } #define Ask SymbolInfoDouble(__Symbol,SYMBOL_ASK) [/CODE] 逐行拆一下关键行:第1行 IsTesting 用 MQLInfoInteger 查 MQL_TESTER 状态,强转 bool;IsTradeContextBusy 直接宏成 false,是历史包袱。_Digits 映射到 Digits() 拿小数位,_Point 映射到 Point() 拿点值。GetAsk 函数里先声明 MqlTick tick,再 SymbolInfoTick 填充最新行情,返回 tick.ask;GetBid 对称返回 tick.bid。最后一行 #define Ask 又被重定义为 SymbolInfoDouble(__Symbol,SYMBOL_ASK),编译时后面的定义覆盖前面的函数宏。 外汇和贵金属杠杆高、滑点跳空频繁,用 SymbolInfoTick 还是 SymbolInfoDouble 取价,在流动性断裂时可能拿到不同时间戳的报价。开 MT5 新建一个 EA,把上面宏整段贴进头部,在 OnTick 里 Print(_Symbol, " ", _Digits, " ", Ask, " ", Bid),跑EURUSD和XAUUSD各一分钟,就能验证小数位和点值差异。

MQL5 / C++
class="macro">#define IsTesting(class="type">bool)MQLInfoInteger(MQL_TESTER)
class=class="str">"cmt">//--- 检查智能交易系统是否允许交易, 且交易前后关联是否繁忙 
class="macro">#define IsTradeAllowed(class="type">bool)MQLInfoInteger(MQL_TRADE_ALLOWED)
class=class="str">"cmt">//--- 返回有关交易前后关联的信息 
class="macro">#define IsTradeContextBusy       class="kw">false
class=class="str">"cmt">//--- 检查智能交易系统是否在可视模式下进行测试 
class="macro">#define IsVisualMode(class="type">bool)MQLInfoInteger(MQL_VISUAL_MODE)
class=class="str">"cmt">//--- 返回拥有客户终端的公司名称 
class="macro">#define TerminalCompany          TerminalInfoString(TERMINAL_COMPANY)
class=class="str">"cmt">//--- 返回客户端名称
class="macro">#define TerminalName             TerminalInfoString(TERMINAL_NAME)
class=class="str">"cmt">//--- 返回客户端启动的目录
class="macro">#define TerminalPath             TerminalInfoString(TERMINAL_PATH)
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- _Digits 变量存储小数点后的位数,
class="macro">#define _Digits                  Digits()
class=class="str">"cmt">//--- _Point 变量包含报价货币中当前品种的点数大小
class="macro">#define _Point                   Point()
class=class="str">"cmt">//--- _LastError 变量包含最后一个错误的代码
class="macro">#define _LastError               GetLastError()
class=class="str">"cmt">//--- _Period 变量包含当前图表的时间帧数值
class="macro">#define _Period                  Period()
class=class="str">"cmt">//--- _StopFlag 变量包含程序停止的标志
class="macro">#define _StopFlag                IsStopped()
class=class="str">"cmt">//--- _Symbol 变量包含当前图表的品种名称
class="macro">#define _Symbol                  Symbol()
class=class="str">"cmt">//--- _UninitReason 变量包含程序未初始化原因的代码
class="macro">#define _UninitReason            UninitializeReason()
class=class="str">"cmt">//--- 当前品种最后知道的卖方价格 (ask 价格)
class="macro">#define Ask                      GetAsk()
class=class="str">"cmt">//--- 当前品种最后知道的买方价格 (报价, bid 价格)
class="macro">#define Bid                      GetBid()
class="type">class="kw">double GetAsk()
  {
   class="type">MqlTick tick;
   SymbolInfoTick(Symbol(),tick);
   class="kw">return(tick.ask);
  }
class="type">class="kw">double GetBid()
  {
   class="type">MqlTick tick;
   SymbolInfoTick(Symbol(),tick);
   class="kw">return(tick.bid);
  }
class="macro">#define Ask SymbolInfoDouble(__Symbol,SYMBOL_ASK)

「宏定义能塞进整个类」

MQL5 的 #define 不只是替换常量,参数形式 #define identifier(par1,... par8) expression 允许把一段带参表达式整体替换,甚至能把一个完整类塞进宏体里。 上面例子里 SeriesVolume(Volume,T) 第一次被定义为字面量 159,在 OnStartlong a=SeriesVolume(Volume,long); 展开后就是 long a=159;,编译期直接固化。 同一宏名后面被重定义为 class CVolume { ... },用反斜杠续行包裹了一个只读下标运算符:用 CopyTickVolume 取第 i 根 K 线的tick成交量,成功返回 long 值,失败返回 -1。这种写法在 MT5 里可能让序列访问语法更像数组,但宏重定义会覆盖前面的值,调试时容易看错展开结果。 外汇与贵金属杠杆高,这类底层封装若返回 -1 没处理,策略可能误判成交量缺失为真实零值,实盘概率上会放大滑点风险。开 MT5 新建脚本粘下面代码,能直接看宏展开差异。

MQL5 / C++
class="macro">#define identifier(par1,... par8) expression    class=class="str">"cmt">// 参数形式
class="macro">#define SeriesVolume(Volume,T) class="num">159
class=class="str">"cmt">//+-----------------------------------------------------------------+
class=class="str">"cmt">//| 脚本程序 start 函数                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
   class="type">long a=SeriesVolume(Volume,class="type">long);
  }
   class="type">long a=class="num">159;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                Test_en.mq5 |
class=class="str">"cmt">//|                                                                版权所有 class="num">2012, 公司名称 |
class=class="str">"cmt">//|                                                                http://www.companyname.net |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#define SeriesVolume(Volume,T) class CVolume      \
  {                                               \
  class="kw">public:                                         \
    T class="kw">operator[](class="kw">const class="type">int i) class="kw">const               \
    {                                              \
    class="type">long val[class="num">1];                                  \
    if(CopyTickVolume(Symbol(),Period(),i,class="num">1,val)==class="num">1)\
      class="kw">return(val[class="num">0]);                             \
    else                                          \
      class="kw">return(-class="num">1);                                \
    }                                              \

用类封装把 Tick 成交量拉进脚本

在 MT5 脚本里直接读 Volume[i] 往往拿不到实时 tick 量,更稳的做法是把取数逻辑包进一个类,重载 operator[] 让调用端像数组一样用。下面这段把 CVolume 类挂上全局对象 Volume,OnStart 里 Print(Volume[4]) 就能打出倒数第 5 根 K 线的 tick 成交量。 类内部用 CopyTickVolume(Symbol(),Period(),i,1,val) 按索引拷 1 根的量到局部数组;返回 1 表示成功,直接返回 val[0],失败则返回 -1。这样写比每次手动调 API 更干净,也方便在多个脚本里复用。 实盘里外汇和贵金属波动大、滑点频繁,tick 成交量只是市场活跃度的概率性参考,不能单独当成方向信号;开 MT5 新建脚本粘下面代码,改 Print 里的下标就能验证不同柱子的量。

MQL5 / C++
class CVolume
  {
  class="kw">public:
    class="type">long class="kw">operator[](class="kw">const class="type">int i) class="kw">const
    {
      class="type">long val[class="num">1];
      if(CopyTickVolume(Symbol(),Period(),i,class="num">1,val)==class="num">1)
        class="kw">return(val[class="num">0]);
      else
        class="kw">return(-class="num">1);
    };
  };
CVolume Volume;

class=class="str">"cmt">//---
SeriesVolume(Volume,class="type">long)

class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 脚本程序 start 函数                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
   Print(Volume[class="num">4]);
  }

◍ 用 iClose 封装跨周期取价与指标模式常量

在 MT5 脚本里直接写 Volume[4] 只能拿到当前图表第 5 根柱(索引从 0 计)的成交量,换品种或时间帧就抓瞎。下面这段把取收盘价逻辑包成了一个 iClose 函数,指定 symbol、timeframe 和 shift 就能拉到任意品种的闭合价。 代码里的 CopyClose 只拷 1 根柱到 val 数组,copied>0 才赋值,否则用 GetLastError 把错误码打进日志。实盘跑外汇或贵金属前,建议先在非交易时段用 Print(iClose("XAUUSD",PERIOD_M15,0)) 验证返回是不是正常双精度数,这类品种点差跳变时可能返回 0.0 或 NaN。 后面那一串 #define 是把旧版 MQL4 的 MODE_ 宏在 MQL5 里重新映射:比如 MODE_MAIN 是 0、MODE_SIGNAL 是 1,一目均衡里 MODE_TENKANSEN=1、MODE_KIJUNSEN=2 直到 MODE_CHIKOUSPAN=5。自己写指标缓冲区索引时直接套这些宏,比裸写数字可读性强,也方便以后改参数。 别把 NaN 当正常价用 代码末尾用 double NaN=double("nan") 造了个非数,若 CopyClose 失败返回 0.0 而不是 NaN,你在 EA 里拿它算均线不会报错但会污染序列。更稳的做法是失败分支直接返回 NaN,调用层用 MathIsValidNumber 拦掉。

MQL5 / C++
class="type">void OnStart()
  {
   Print(Volume[class="num">4]);
  }
class=class="str">"cmt">//--- 返回指定品种、时间帧和偏移柱线的收盘价
class="type">class="kw">double   iClose(
                         class="type">class="kw">string            symbol,       class=class="str">"cmt">// 品种
                         ENUM_TIMEFRAMES   timeframe,    class=class="str">"cmt">// 时间帧
                         class="type">int               shift         class=class="str">"cmt">// 偏移
                         )
  {
   class="type">class="kw">double result=class="num">0.0;
class=class="str">"cmt">//---
   class="type">class="kw">double val[class="num">1];
   ResetLastError();
   class="type">int copied=CopyClose(symbol,timeframe,shift,class="num">1,val);
   if(copied>class="num">0)
      result=val[class="num">0];
   else
      Print(__FUNCTION__,": CopyClose error=",GetLastError());
class=class="str">"cmt">//---
   class="kw">return(result);
  }
class="type">class="kw">double NaN=class="type">class="kw">double("nan");
class="macro">#define MODE_MAIN          class="num">0  
class="macro">#define MODE_SIGNAL        class="num">1
class="macro">#define MODE_PLUSDI        class="num">1
class="macro">#define MODE_MINUSDI       class="num">2  
class="macro">#define MODE_GATORJAW      class="num">1
class="macro">#define MODE_GATORTEETH    class="num">2
class="macro">#define MODE_GATORLIPS     class="num">3
class="macro">#define MODE_UPPER         class="num">1
class="macro">#define MODE_LOWER         class="num">2  
class="macro">#define MODE_TENKANSEN     class="num">1    
class="macro">#define MODE_KIJUNSEN      class="num">2          
class="macro">#define MODE_SENKOUSPANA   class="num">3          
class="macro">#define MODE_SENKOUSPANB   class="num">4          
class="macro">#define MODE_CHIKOUSPAN    class="num">5  
class="macro">#define MODE_OPEN  class="num">0

「重定义价格模式常量避开系统冲突」

在 MT5 的自定义头文件里直接沿用 MQL5 内置的 MODE_LOW、MODE_HIGH 等宏,容易和终端预定义常量撞车。上面这段头文件把低频用的取值改成 10001、10002、10005,等于在用户层重新划了一块编号空间。 注释里保留的 #define MODE_CLOSE 3#define MODE_VOLUME 4 仍是原值,说明只有低/高/时间三类被抬到万级区间,收盘与成交量维持原生映射。开 MT5 新建一个 EA 头文件,把这段贴进去编译,若之前报过 'MODE_LOW redefinition' 类错误,现在应能过。 外汇与贵金属杠杆高,这类底层常量改错会导致取价返回空值,实盘前务必在策略测试器用历史数据跑一遍 iOpen/iHigh 调用验证。

MQL5 / C++
class=class="str">"cmt">//class="macro">#define MODE_LOW    class="num">1
class=class="str">"cmt">//class="macro">#define MODE_HIGH    class="num">2
class="macro">#define MODE_CLOSE   class="num">3
class="macro">#define MODE_VOLUME  class="num">4
class=class="str">"cmt">//class="macro">#define MODE_TIME    class="num">5
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                Header.mqh |
class=class="str">"cmt">//|          版权所有 class="num">2018, MetaQuotes 软件公司 |
class=class="str">"cmt">//|              http://wmua.ru/slesar/ |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "版权所有 class="num">2018, MetaQuotes 软件公司"
class="macro">#class="kw">property link      "http:class=class="str">"cmt">//wmua.ru/slesar/"
class=class="str">"cmt">//---
class="macro">#define MODE_LOW    class="num">10001
class="macro">#define MODE_HIGH   class="num">10002
class="macro">#define MODE_TIME   class="num">10005

宏迁移方案的取舍与余味

把 MQL4 风格 EA 搬进 MetaTrader 5,核心就是挂上重载函数与预定义变量的头文件,再用 #define 做字符串替换。实测附带的 ForEachOrder 宏在回测中每多挂一个指标,测试速度会出现可感知的下降,这是宏方案绕不开的代价。 缺点里最致命的是 #define 宏替代无法调试,变量藏在宏后面还可能冲突;优点则是代码量骤减、宏以红色高亮便于辨识。下方代码里 ForEachOrder 混用了 HistorySelect 与 OrdersTotal,社区已有指出:若只处理未结订单本不需要 HistorySelect,这类坑正好印证了不可调试的隐患。 #define ForEachSymbol(s,i) string s=SymbolName(0,true); int os_total=SymbolsTotal(true); for(int i=1;i<os_total;i++,s=SymbolName(i,true)) #define ForEachOrder(ticket,i) HistorySelect(0,TimeCurrent()); ulong ticket=OrderGetTicket(0); int or_total=OrdersTotal(); for(int i=1;i<or_total;i++,ticket=OrderGetTicket(i)) 本人仍倾向传统 MQL5 写法,宏只是过渡期的生活窍门。习惯 MQL4 的人借这套头文件大概率能跨过平台切换的心理门槛,但外汇与贵金属交易杠杆高、风险大,实盘前务必在 MT5 策略测试器里先跑通再谈信任。

MQL5 / C++
class="macro">#define ForEachSymbol(s,i)  class="type">class="kw">string s=SymbolName(class="num">0,true); class="type">int os_total=SymbolsTotal(true); for(class="type">int i=class="num">1;i<os_total;i++,s=SymbolName(i,true))
class="macro">#define ForEachOrder(ticket,i)    HistorySelect(class="num">0,TimeCurrent());  class="type">class="kw">ulong ticket=OrderGetTicket(class="num">0); class="type">int or_total=OrdersTotal();  for(class="type">int i=class="num">1;i<or_total;i++,ticket=OrderGetTicket(i))
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| 脚本程序启动功能                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
class=class="str">"cmt">//---
   ForEachOrder(orderticket,index)
     {
       Print(index,": #",orderticket," ",OrderGetString(ORDER_SYMBOL)," ",
             EnumToString((ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE)));
     }
class=class="str">"cmt">/* 输出示例
class="num">1: class="num">13965457 CADJPY ORDER_TYPE_SELL_LIMIT
class="num">2: class="num">14246567 AUDNZD ORDER_TYPE_SELL_LIMIT
*/
  }

常见问题

老平台保证金按合约大小和杠杆直接除,新平台还要乘当前品种保证金比率;先在回测里打印每单保证金差值再上线。
用环境宏区分运行场景,报价函数只负责取数,两者通过中间变量传参,别在取价处写条件分支。
可以。把源码贴给小布,它会扫一遍全局符号并重命名撞车的常量,省去手动排雷。
不用。用类封装包一层 tick 回调,把 Volume 字段暴露成公开属性,主逻辑直接读就行。
用 iClose 封装取价,并把模式常量本地重定义加前缀,避开系统保留字即可编译通过。