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

Vladimir Karputov | 12 四月, 2018


 — 兄弟, 什么能令你变酷?                           
— 兄弟, defines 预处理能令你变酷                    

                                  (с)  fxsaber                  

您仍然使用 MQL4 编程并希望切换到 MQL5? 我们会告诉您从哪里开始! 现在, 您可以在 MQL5 MetaEditor 中舒适地工作, 并可同时使用 MQL4 表示法 (实际上, 这样的机会早就出现了, 尽管在本文中, 我是想提供关于如何将 MQL4 函数迁移到 MQL5 的更完整细节)。


一名好的程序员是一位懒惰的程序员

创建智能交易系统几乎总是需要大量的循环工作。 循环随处环绕着我们: 搜索订单, 历史交易, 图表对象, 市场观察品种, 指标缓冲区中的柱线。 为了让程序员的生活更轻松一点, MetaEditor 以 代码片段 (snippets) 为特色, 意即当您输入第一个字符时, 按 Tab 键后它们会自动变成一小段代码。 这就是 'for' 循环片段的工作原理:


不错, 但它尚未覆盖我们的所有需求。 考虑最简单的例子: 假设我们需要搜索市场观察内的所有品种。

   int total=SymbolsTotal(true);
   for(int i=0;i<total;i++)
     {
      string symbol=SymbolName(i,true);
      PrintFormat("%d. %s",i+1,symbol);
     }

开发一段以 fes (for_each_symbol) 开头的 MetaEditor 代码片段并在以下部分展开感觉会很棒:


MetaEditor 中没有自定义代码片段, 因此我们将利用 "defines"。 #define 宏定义替代是由追求多个目标的懒惰又聪明的程序员发明的。 其便利性包括便于阅读和编写复用代码。

许多编程语言, 除了标准 for 循环外, 还有其变体, 例如: for(<typename> element:Collection)for each (type identifier in expression)。 如果我们可以编写如下代码

for(ulong order_id in History)
  {
   参照 order_id 工作  
  }

, 程序员的生活会更轻松一些。 您可以在互联网上找到这种方法的发对者和支持者。 在此我会告诉您如何用 #define 宏定义做类似的事情。

我们从一个简单的任务开始 - 获取所有市场观察内品种的名称。 我们向前直行, 编写如下的宏定义:

#define ForEachSymbol(s,i)  string s; int total=SymbolsTotal(true); for(int i=0;i<total;i++,s=SymbolName(i,true))
//+------------------------------------------------------------------+
//| 脚本程序 start 函数                                               |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   ForEachSymbol(symbol,index)
     {
      PrintFormat("%d. %s",index,symbol);
     }
  }

编译器完全理解这条语句没有返回错误。 我们按 F5 开始调试, 发现有问题:

1. (null)
2. GBPUSD
3. USDCHF
4. USDJPY
...

问题出于 'for' 循环中 s = SymbolName (i, true) 表达式要在迭代之后计算, 且我们的 's' 变量在 i=0 的首次迭代时并未初始化。 'for' 语句:

'for' 语句由三个表达式和一条可执行语句组成:

for(expression1; expression2; expression3)
   statement;

Expression1 表示循环初始化。 Expression2 — 检查循环完成情况。 如果它为 'true', 则 for 循环实体操作符会被执行。 重复一切, 直到 expression2 变成 'false'。 如果它为 'false', 则循环终止并略过之下的语句。 ExpressionЗ 则在 每次迭代之后 进行计算。

这个过程很简单。 我们来做几个编辑:

#define ForEachSymbol(s,i)  string s=SymbolName(0,true); int total=SymbolsTotal(true); for(int i=1;i<total;i++,s=SymbolName(i,true))
//+------------------------------------------------------------------+
//| 脚本程序 start 函数                                               |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   ForEachSymbol(symbol,index)
     {
      PrintFormat("%d. %s",index,symbol); // index+1 由 index 替代
     }
  }

并获得必要的结果。 我们已经开发了带有 symbolindex 参数的 ForEachSymbol 宏定义, 就好像这是一个正常的 for 循环, 并且用这些变量处理伪循环体就好像它们已经使用必要的值进行了声明和初始化。 因此, 我们可以使用 SymbolInfoXXX() 函数获得市场观察内品种的所需属性。 例如:

//+------------------------------------------------------------------+
//| 脚本程序 start 函数                                               |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   ForEachSymbol(symbol,index)
     {
      //--- 准备数据
      double spread=SymbolInfoDouble(symbol,SYMBOL_ASK)-SymbolInfoDouble(symbol,SYMBOL_BID);
      double point=SymbolInfoDouble(symbol,SYMBOL_POINT);
      long digits=SymbolInfoInteger(symbol,SYMBOL_DIGITS);
      string str_spread=DoubleToString(spread/point,0);
      string str_point=DoubleToString(point,digits);
      //--- 显示数据
      Print(index,". ",symbol," spread=",str_spread," points (",
            digits," digits",", point=",str_point,")");
     }
/* 简单输出
        1. EURUSD spread=3 points (5 digits, point=0.00001)
        2. USDCHF spread=8 points (5 digits, point=0.00001)
        3. USDJPY spread=5 points (3 digits, point=0.001)
        4. USDCAD spread=9 points (5 digits, point=0.00001)
        5. AUDUSD spread=5 points (5 digits, point=0.00001)
        6. NZDUSD spread=10 points (5 digits, point=0.00001)
        7. USDSEK spread=150 points (5 digits, point=0.00001)
*/
  }

现在我们可以编写一个类似的宏定义来搜索图表上的图形对象:

#define ForEachObject(name,i)   string name=ObjectName(0,0); int total=ObjectsTotal(0); for(int i=1;i<=total;i++,name=ObjectName(0,i-1))
//+------------------------------------------------------------------+
//| 脚本程序 start 函数                                               |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   ForEachObject(objectname,index)
     {
      Print(index,": objectname=\"",objectname,"\", objecttype=",
            EnumToString((ENUM_OBJECT)ObjectGetInteger(0,objectname,OBJPROP_TYPE)));
     }
/* 简单输出
        1: objectname="H1 Arrow 61067", objecttype=OBJ_ARROW_UP
        2: objectname="H1 Rectangle 31152", objecttype=OBJ_RECTANGLE
        3: objectname="H1 StdDev Channel 56931", objecttype=OBJ_STDDEVCHANNEL
        4: objectname="H1 Trendline 6605", objecttype=OBJ_TREND
*/     
  }

ForEachObject 宏定义字符串变得有点长了。 此外, 理解单一字符串替换的代码更加困难。 但事实证明, 这个问题已经得到解决: 现在可以使用反斜杠 '\' 将宏定义切分为多个字符串。 结果如下:

#define ForEachObject(name,i)   string name=ObjectName(0,0);   \
   int ob_total=ObjectsTotal(0);                               \
   for(int i=1;i<=ob_total;i++,name=ObjectName(0,i-1))

对于编译器来说, 所有这三个字符串看起来像一个单一的长字符串, 同时变得更容易理解。 现在, 我们需要创建类似的宏定义来处理交易实例 — 订单, 仓位和交易。

我们从搜索订单开始。 在此仅添加 HistorySelect() 历史选择函数:

#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))
//+------------------------------------------------------------------+
//| 脚本程序 start 函数                                               |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   ForEachOrder(orderticket,index)
     {
      Print(index,": #",orderticket," ",OrderGetString(ORDER_SYMBOL)," ",
            EnumToString((ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE)));
     }
/* 简单输出     
   1: #13965457 CADJPY ORDER_TYPE_SELL_LIMIT
   2: #14246567 AUDNZD ORDER_TYPE_SELL_LIMIT
*/ 
  }

您可以注意到这个宏定义中没有错误处理 (以及前两个)。 例如, 如果 HistorySelect() 返回 false 会怎样? 在这种情况下, 我们将无法在循环中遍历所有的订单。 另外, 是谁在真正分析 HistorySelect() 的执行结果? 因此, 这个宏定义没有包含任何可能被禁止用于开发程序的常用方式。

对于 #define 最强烈的批评是 代码调试 中不允许使用宏替换。 我同意这一点, 但正如 fxsaber 所说: "已康复患者不需要麻醉 已调试宏定义不需要再进行调试"。

接下来, 我们以类似的方式开发用于搜索仓位的宏定义:

#define ForEachPosition(ticket,i) HistorySelect(0,TimeCurrent());    \
   ulong ticket=PositionGetTicket(0);                                \
   int po_total=PositionsTotal();                                    \
   for(int i=1;i<=po_total;i++,ticket=PositionGetTicket(i-1))        
//+------------------------------------------------------------------+
//| 脚本程序 start 函数                                               |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   ForEachPosition(positionid,index)
     {
      Print(index,": ",PositionGetString(POSITION_SYMBOL)," postionID #",positionid);
     }
/* 简单输出 
   1: AUDCAD postionID #13234934
   2: EURNZD postionID #13443909
   3: AUDUSD postionID #14956799
   4: EURUSD postionID #14878673
*/ 

搜索历史中的交易:

#define ForEachDeal(ticket,i) HistorySelect(0,TimeCurrent());        \
   ulong ticket=HistoryDealGetTicket(0);                             \
   int total=HistoryDealsTotal();                                    \
   for(int i=1;i<=total;i++,ticket=HistoryDealGetTicket(i-1))
//+------------------------------------------------------------------+
//| 脚本程序 start 函数                                               |
//+------------------------------------------------------------------+
void OnStart()
  {
   //---
   ForEachDeal(dealticket,index)
     {
      Print(index,": deal #",dealticket,",  order ticket=",
            HistoryDealGetInteger(dealticket,DEAL_ORDER));
     }
  }

搜索历史中的订单:

#define ForEachHistoryOrder(ticket,i) HistorySelect(0,TimeCurrent());\
   ulong ticket=HistoryOrderGetTicket(0);                            \
   int total=HistoryOrdersTotal();                                   \
   for(int i=1;i<=total;i++,ticket=HistoryOrderGetTicket(i-1))
//+------------------------------------------------------------------+
//| 脚本程序 start 函数                                               |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   ForEachHistoryOrder(historyorderticket,index)
     {
      Print(index,": #",historyorderticket);
     }
  }

在一个 ForEach.mqh4 文件中收集所有宏替换:

//+------------------------------------------------------------------+
//|                                                      ForEach.mqh |
//|                                版权所有 2018, MetaQuotes 软件公司 |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "版权所有 2018, MetaQuotes 软件公司"
#property link      "https://www.mql5.com"
//+------------------------------------------------------------------+
//| 市场观察搜索循环                                                  |
//+------------------------------------------------------------------+
#define ForEachSymbol(symbol,i)  string symbol=SymbolName(0,true);   \
   int os_total=SymbolsTotal(true);                                  \
   for(int i=1;i<os_total;i++,symbol=SymbolName(i,true))
//+------------------------------------------------------------------+
//| 搜索图表主窗口对象循环                                            |
//+------------------------------------------------------------------+   
#define ForEachObject(name,i)   string name=ObjectName(0,0);         \
   int ob_total=ObjectsTotal(0);                                     \
   for(int i=1;i<=ob_total;i++,name=ObjectName(0,i-1))      
//+------------------------------------------------------------------+
//| 搜索活跃订单循环                                                  |
//+------------------------------------------------------------------+
#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))   
//+------------------------------------------------------------------+
//| 搜索持仓循环                                                      |
//+------------------------------------------------------------------+
#define ForEachPosition(ticket,i) HistorySelect(0,TimeCurrent());    \
   ulong ticket=PositionGetTicket(0);                                \
   int po_total=PositionsTotal();                                    \
   for(int i=1;i<=po_total;i++,ticket=PositionGetTicket(i-1))      
//+------------------------------------------------------------------+
//| 搜索历史交易循环                                                  |
//+------------------------------------------------------------------+
#define ForEachDeal(ticket,i) HistorySelect(0,TimeCurrent());        \
   ulong ticket=HistoryDealGetTicket(0);                             \
   int dh_total=HistoryDealsTotal();                                 \
   for(int i=1;i<=dh_total;i++,ticket=HistoryDealGetTicket(i-1))        
//+------------------------------------------------------------------+
//| 搜索历史订单循环                                       |
//+------------------------------------------------------------------+
#define ForEachHistoryOrder(ticket,i) HistorySelect(0,TimeCurrent());\
   ulong ticket=HistoryOrderGetTicket(0);                            \
   int oh_total=HistoryOrdersTotal();                                \
   for(int i=1;i<=oh_total;i++,ticket=HistoryOrderGetTicket(i-1))   
//+------------------------------------------------------------------+

注意: 我们必须为每个宏定义的 'total' 变量添加一个 前缀, 这样的话如果我们决定在代码中使用多个宏定义, 就不会有冲突。 这是宏定义的最大缺点: 我们将变量声明隐藏其内, 而变量从外部可见。 编译时会导致难以检测的错误。

另外, 当使用参数宏时, 编译器不会像函数那样给出任何提示。 如果您想使用它们, 您必须用心学习这 6 个宏定义。 虽然这并不太难, 但是因为 第一个参数 总是一个循环的实例, 而第二个参数总是从 1 开始的循环索引。

最后, 我们添加更多反向搜索的宏定义。 在这种情况下, 我们需要从列表的末尾开始。 将 Back 后缀添加到宏定义名称并进行一些微小的修改。 以下是搜索市场观察内品种的宏定义。

#define ForEachSymbolBack(symbol,i) int s_start=SymbolsTotal(true)-1;\
   string symbol=SymbolName(s_start,true);                           \
   for(int i=s_start;i>=0;i--,symbol=SymbolName(i,true))
//+------------------------------------------------------------------+
//| 脚本程序 start 函数                                               |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   ForEachSymbolBack(symbol,index)
     {
      //--- 准备数据
      double spread=SymbolInfoDouble(symbol,SYMBOL_ASK)-SymbolInfoDouble(symbol,SYMBOL_BID);
      double point=SymbolInfoDouble(symbol,SYMBOL_POINT);
      long digits=SymbolInfoInteger(symbol,SYMBOL_DIGITS);
      string str_spread=DoubleToString(spread/point,0);
      string str_point=DoubleToString(point,digits);
      //--- 输出数据
      Print(index,". ",symbol," spread=",str_spread," points (",
            digits," digits",", point=",str_point,")");
     }
/* 简单输出
   3. USDJPY spread=5 points (3 digits, point=0.001)
   2. USDCHF spread=8 points (5 digits, point=0.00001)
   1. GBPUSD spread=9 points (5 digits, point=0.00001)
   0. EURUSD spread=2 points (5 digits, point=0.00001)
*/
  }

如您所见, index 变量的值在此处从 size-1 变化为 0。 我们已经完成了对 #define 的了解。 现在, 是时候开发用于简单 MQL4 风格访问的函数了。


1. 本文中描述了哪些 MQL4 函数组

注意: 用 Point(), Digits() 和 Bar(Symbol(),Period()) 替换 Point, Digits 和 Bar 等变量。

MQL4 AccountXXXX, MQL4 MarketInfo, MQL4 状态检查和 MQL4 预定义变量组将被转换为 MQL5。 所以, 四个文件: AccountInfo.mqhMarketInfo.mqhCheck.mqh 和 Predefined.mqh 要添加到 [数据文件夹]\MQL5\Include\SimpleCall\。 文件夹中有七个文件要从 MQL4 函数转换为 MQL5: AccountInfo.mqh, Check.mqh, IndicatorsMQL4.mqh, IndicatorsMQL5.mqh, MarketInfo.mqh, Predefined.mqhSeries.mqh

您应该包含所有这些文件。 此外, 请注意限制: IndicatorsMQL4.mqhIndicatorsMQL5.mqh 文件不能一同包含 — 您只能从中选择其一。 因此, 该文件夹有两个文件: SimpleCallMQL4.mqh 包含所有文件加上 IndicatorsMQL4.mqh 以及 SimpleCallMQL5.mqh 包含所有文件加上 IndicatorsMQL5.mqh

包含基于 MACD Sample.mq4 的示例

与 EA 一同将 MACD Sample.mq4 复制到 MQL5 文件夹 — 例如, [数据文件夹]\MQL5\Experts\, 并将文件扩展名更改为 mq5。 所以, 我们得到 MACD Sample.mq5 文件。 编译并得到 59 个错误和一个警告。

现在, 连接 SimpleCallMQL4.mqh:

//+------------------------------------------------------------------+
//|                                                  MACD Sample.mq4 |
//|                           版权所有 2005-2014, MetaQuotes 软件公司 |
//|                                              http://www.mql4.com |
//+------------------------------------------------------------------+
#property copyright   "2005-2014, MetaQuotes 软件公司"
#property link        "http://www.mql4.com"
//---
#include <SimpleCall\SimpleCallMQL4.mqh>
//---
input double TakeProfit    =50;

再次编译并得到 39 个错误和一个警告。 现在, 手工 (Ctrl+H) 用 Bars(Symbol(),Period()) 替换 Bars, 以及用 Point() 替换 Point。 编译。 还剩 35 个错误。 它们都与交易函数有关。 但我们不会在本文中讨论交易函数。

1.1. MQL4 AccountXXXX

MQL4 AccountXXXX 函数已在文件中转换 [数据文件夹]\MQL5\Include\SimpleCall\AccountInfo.mqh

与 MQL4 函数匹配的 MQL5 函数的表格如下所示:

MQL4 MQL5 AccountInfoXXXX MQL5 CAccountInfo  注意 
 AccountInfoDouble  AccountInfoDouble  CAccountInfo::InfoDouble 或 CAccountInfo::double 方法  
 AccountInfoInteger  AccountInfoInteger  CAccountInfo::InfoInteger 或 CAccountInfo::integer 方法  
 AccountInfoString  AccountInfoString  CAccountInfo::InfoString 或 CAccountInfo::text 方法  
 AccountBalance  AccountInfoDouble(ACCOUNT_BALANCE) 或   CAccountInfo::Balance  
 AccountCredit  AccountInfoDouble(ACCOUNT_CREDIT) 或   CAccountInfo::Credit  
 AccountCompany  AccountInfoString(ACCOUNT_COMPANY) 或   CAccountInfo::Company  
 AccountCurrency  AccountInfoString(ACCOUNT_CURRENCY) 或   CAccountInfo::Currency  
 AccountEquity  AccountInfoDouble(ACCOUNT_EQUITY) 或   CAccountInfo::Equity  
 AccountFreeMargin  AccountInfoDouble(ACCOUNT_FREEMARGIN) 或   CAccountInfo::FreeMargin  
 AccountFreeMarginCheck  --- /---  CAccountInfo::FreeMarginCheck   
 AccountFreeMarginMode  无等价品  无等价品  
 AccountLeverage  AccountInfoInteger(ACCOUNT_LEVERAGE)   CAccountInfo::Leverage  在 MQL4 中, 它是 int 类型, 而在 MQL5 中, 它是 long
 AccountMargin  AccountInfoDouble(ACCOUNT_MARGIN)  CAccountInfo::Margin  
 AccountName  AccountInfoString(ACCOUNT_NAME)  CAccountInfo::Name  
 AccountNumber  AccountInfoInteger(ACCOUNT_LOGIN)  CAccountInfo::Login  在 MQL4 中, 它是 int 类型, 而在 MQL5 中, 它是 long
 AccountProfit  AccountInfoDouble(ACCOUNT_PROFIT)  CAccountInfo::Profit   
 AccountServer  AccountInfoString(ACCOUNT_SERVER)  CAccountInfo::Server  
 AccountStopoutLevel  AccountInfoDouble(ACCOUNT_MARGIN_SO_SO)  CAccountInfo::MarginStopOut  在 MQL4 中, 它是 int 类型, 而在 MQL5 中, 它是 double
 AccountStopoutMode  AccountInfoInteger(ACCOUNT_MARGIN_SO_MODE)  CAccountInfo::StopoutMode  

请注意, MQL5 没有匹配 MQL4 AccountFreeMarginMode 的等价品。 您使用 MQL4 AccountFreeMarginMode 则风险自负。 当检测到 MQL4 AccountFreeMarginMode 时, 会向日志发送警告, 并返回 NaN ("非数字")。

对于其它 MQL4 AccountXXXX 函数, 有两个版本的等价品: 通过 AccountInfoXXXX 或通过 CAccountInfo 交易类。 对于 AccountInfoDouble, AccountInfoInteger 和 AccountInfoString, MQL4 和 MQL5 之间没有区别。

文件头有以下代码。 如何工作?

//---
#define  OP_BUY                     ORDER_TYPE_BUY
#define  OP_SELL                    ORDER_TYPE_SELL
//--- 返回当前帐户的余额值

首先, 我们来看看 #define help:

#define 指令可用于将常用名称分配给常量。 有两种形式:

#define identifier expression                   // 无参数形式 
#define identifier(par1,... par8) expression    // 参数形式

#define 指令在源文本中将所有发现的 "identifier" 替换为 "expression"。

在我们的代码中, 这些描述看起来如下 (我们使用了无参数形式): #define 指令以 ORDER_TYPE_BUY 替换所有在源文件中发现的 OP_BUY。 #define 指令以 ORDER_TYPE_SELL 替换所有在源文件中发现的 OP_SELL。 换言之, 将文件 [数据文件夹]\MQL5\Include\SimpleCall\AccountInfo.mqh 包含到您的 MQL5 EA 之后, 您能够按照类似的方式使用 OP_BUY 和 OP_SELL MQL4 类型。 编译器不会返回错误。

因此, 我们为 MQL5 类型带来了第一组函数: AccountBalance(), AccountCredit(), AccountCompany(), AccountCurrency(), AccountEquity() 和 AccountFreeMargin():

//--- 返回当前帐户的余额值
#define  AccountBalance(void)       AccountInfoDouble(ACCOUNT_BALANCE)
//--- 返回当前帐户的信用值
#define  AccountCredit(void)        AccountInfoDouble(ACCOUNT_CREDIT)
//--- 返回当前帐户注册的经纪公司名称
#define  AccountCompany(void)       AccountInfoString(ACCOUNT_COMPANY)
//--- 返回当前帐户的货币名称 
#define  AccountCurrency(void)      AccountInfoString(ACCOUNT_CURRENCY)
//--- 返回当前账户的净值
#define  AccountEquity(void)        AccountInfoDouble(ACCOUNT_EQUITY)
//--- 返回当前帐户的可用保证金
#define  AccountFreeMargin(void)    AccountInfoDouble(ACCOUNT_MARGIN_FREE)
此处, 在 MQL4 XXXX(void) 函数中, 使用参数化的 #define 形式, 其中 "void" 充当参数。 这可以很容易地检查: 如果我们删除 "void", 编译期间出现 "unexpected in macro format parameter list" 错误:


unexpected in macro format parameter list

在 MQL4 AccountFreeMarginCheck 的情况下, 我们将采取不同的行动 — 将 AccountFreeMarginCheck 设置为正常函数, 仅使用其中的 MQL5 代码:

//--- 返回指定订单打开后剩余的可用保证金 
//---    按当前账户的当前价格计算
double   AccountFreeMarginCheck(string symbol,int cmd,double volume)
  {
   double price=0.0;
   ENUM_ORDER_TYPE trade_operation=(ENUM_ORDER_TYPE)cmd;
   if(trade_operation==ORDER_TYPE_BUY)
      price=SymbolInfoDouble(symbol,SYMBOL_ASK);
   if(trade_operation==ORDER_TYPE_SELL)
      price=SymbolInfoDouble(symbol,SYMBOL_BID);
//---
   double margin_check=EMPTY_VALUE;
   double margin=EMPTY_VALUE;
   margin_check=(!OrderCalcMargin(trade_operation,symbol,volume,price,margin))?EMPTY_VALUE:margin;
//---
   return(AccountInfoDouble(ACCOUNT_FREEMARGIN)-margin_check);
  }

正如我们上面所说的, AccountFreeMarginMode 在 MQL5 中没有等价品, 因此, 在代码中检测到 AccountFreeMarginMode 时, 会发出 "非数字 (not a number)" 和警告:

//--- 返回当前账户上开单的可用保证金计算模式
double AccountFreeMarginMode(void)
  {
   string text="MQL4 functions \"AccountFreeMarginMode()\" has no analogs in MQL5. Returned \"NAN - not a number\"";
   Alert(text);
   Print(text);
   return(double("nan"));
  }

对于其它 MQL4 函数, 我们以类似的方式进行:

//--- 返回当前账户的杠杆
#define  AccountLeverage(void)      (int)AccountInfoInteger(ACCOUNT_LEVERAGE)
//--- 返回当前帐户的保证金
#define  AccountMargin(void)        AccountInfoDouble(ACCOUNT_MARGIN)
//--- 返回当前帐户名称
#define  AccountName(void)          AccountInfoString(ACCOUNT_NAME)
//--- 返回当前帐号
#define  AccountNumber(void)        (int)AccountInfoInteger(ACCOUNT_LOGIN)
//--- 返回当前帐户的利润值
#define  AccountProfit(void)        AccountInfoDouble(ACCOUNT_PROFIT)
//--- 返回连接的服务器名称
#define  AccountServer(void)        AccountInfoString(ACCOUNT_SERVER)
//--- 返回强制平仓级别的数值
#define  AccountStopoutLevel(void)  (int)AccountInfoDouble(ACCOUNT_MARGIN_SO_SO)
//--- 返回强制平仓级别的计算模式
int      AccountStopoutMode(void)
  {
   ENUM_ACCOUNT_STOPOUT_MODE stopout_mode=(ENUM_ACCOUNT_STOPOUT_MODE)AccountInfoInteger(ACCOUNT_MARGIN_SO_MODE);
   if(stopout_mode==ACCOUNT_STOPOUT_MODE_PERCENT)
      return(0);
   return(1);
  }

1.2. MQL4 MarketInfo

MQL4 MarketInfotXXXX 函数已在文件 [数据文件夹]\MQL5\Include\SimpleCall\MarketInfo.mqh 转换

MQL4 MarketInfo 是 double 类型, 但在 MQL5 中, MarketInfo 的等价品 SymbolInfoInteger() (得到 'long' 类型) 以及 SymbolInfoDouble() (得到 'double' 类型)。 此处, 我们可以清楚地感受到使用 MarketInfo (类型转换) 废弃函数的不便。 使用 MQL5 SYMBOL_DIGITS 作为示例:

MarketInfo(Symbol(),MODE_DIGITS) <= (double)SymbolInfoInteger(symbol,SYMBOL_DIGITS)

1.2.1 MQL4 MODE_TRADEALLOWED 的歧义 

在 MQL4 中, 这是一个简单的 'bool' 标志, 而在 MQL5 中, 一个品种可能有几种权限/禁止:

ENUM_SYMBOL_TRADE_MODE

ID 描述
 SYMBOL_TRADE_MODE_DISABLED  品种禁止交易
 SYMBOL_TRADE_MODE_LONGONLY  仅允许买入
 SYMBOL_TRADE_MODE_SHORTONLY  仅允许卖出
 SYMBOL_TRADE_MODE_CLOSEONLY  仅允许平仓
 YMBOL_TRADE_MODE_FULL  无交易限制

我只在 SYMBOL_TRADE_MODE_DISABLED 的情况下提出 'false', 而对于部分限制或完全访问 (伴随部分限制的警告和打印警告), 我建议 'true'。

1.2.2. MQL4 MODE_SWAPTYPE 的歧义

在MQL4 中, MODE_SWAPTYPE 只返回四个值 (隔夜利息计算方法。 0 — 按照点数; 1 — 按照货币的基准货币; 2 — 按照 %; 3 — 按照保证金基准货币), 在 MQL5 中, ENUM_SYMBOL_SWAP_MODE 枚举包含九个值, 与 MQL4 类似:

MQL4 MODE_SWAPTYPE MQL5 ENUM_SYMBOL_SWAP_MODE
 无等价品  SYMBOL_SWAP_MODE_DISABLED
 0 - 按照点数  SYMBOL_SWAP_MODE_POINTS
 1 - 按照品种的基准货币  SYMBOL_SWAP_MODE_CURRENCY_SYMBOL
 3 - 按照保证金基准货币  SYMBOL_SWAP_MODE_CURRENCY_MARGIN
 无等价品  SYMBOL_SWAP_MODE_CURRENCY_DEPOSIT
 2 - 按照 %  SYMBOL_SWAP_MODE_INTEREST_CURRENT
 2 - 按照 %  SYMBOL_SWAP_MODE_INTEREST_OPEN
 无等价品  SYMBOL_SWAP_MODE_REOPEN_CURRENT
 无等价品  SYMBOL_SWAP_MODE_REOPEN_BID

在 MQL5 中, 按照 % 计算隔夜利息时有两种选项:

/*
SYMBOL_SWAP_MODE_INTEREST_CURRENT
隔夜利息计算按计算时的金融产品价格年度 % 计算 (银行模式为每年 360 天)
SYMBOL_SWAP_MODE_INTEREST_OPEN
隔夜利息按品种开仓价格的年度 % 计算 (银行模式为每年 360 天)
*/

我们将它们视为一种类型。 对于在 MQL4 中没有等价品的其它 MQL5 选项, 将发出警告并返回 "非数字 (not a number)"。

1.2.3. MQL4 MODE_PROFITCALCMODE 和 MODE_MARGINCALCMODE 的歧义

在 MQL4 中, MODE_PROFITCALCMODE (盈利计算方法) 返回仅三个数值: 0 — Forex (外汇); 1 — CFD (差价合约); 2 — Futures (期货), 而 MODE_MARGINCALCMODE (保证金计算方法) 返回四个: 0 — Forex (外汇); 1 — CFD (差价合约); 2 — Futures (期货); 3 — 指数型 CFD。 在 MQL5 中, 可以为某个金融产品定义保证金计算方法 (ENUM_SYMBOL_CALC_MODE 枚举), 但无法计算盈利。 我假设在 MQL5 中, 计算保证金的方法等于盈利计算方法, 因此, 对于 MQL4 MODE_PROFITCALCMODE 和 MODE_MARGINCALCMODE, 会返回与 MQL5 ENUM_SYMBOL_CALC_MODE 相似的值。

MQL5 的 ENUM_SYMBOL_CALC_MODE 枚举包括十个方法。 我们来比较 MQL4 的 MODE_PROFITCALCMODE 和 MQL5 的 ENUM_SYMBOL_CALC_MODE:

MQL4 MODE_PROFITCALCMODE "Forex" <==> MQL5 SYMBOL_CALC_MODE_FOREX

MQL4 MODE_PROFITCALCMODE "CFD" <==> MQL5 SYMBOL_CALC_MODE_CFD

MQL4 MODE_PROFITCALCMODE "Futures" <==> MQL5 SYMBOL_CALC_MODE_FUTURES

对于在 MQL4 中没有等价物的其它 MQL5 选项, 会发出警告并返回 "非数字 (not a number)":

...
#define MODE_PROFITCALCMODE   1000//SYMBOL_TRADE_CALC_MODE
#define MODE_MARGINCALCMODE   1001//SYMBOL_TRADE_CALC_MODE
...
      case MODE_PROFITCALCMODE:
        {
         ENUM_SYMBOL_CALC_MODE profit_calc_mode=(ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(symbol,SYMBOL_TRADE_CALC_MODE);
         switch(profit_calc_mode)
           {
            case  SYMBOL_CALC_MODE_FOREX:
               return((double)0);
            case  SYMBOL_CALC_MODE_FUTURES:
               return((double)2);
            case  SYMBOL_CALC_MODE_CFD:
               return((double)1);
            default :
              {
               string text="MQL4 MODE_PROFITCALCMODE returned MQL5 "+EnumToString(profit_calc_mode);
               Alert(text);
               Print(text);
               return(double("nan"));
              }
           }
        }
      case MODE_MARGINCALCMODE:
        {
         ENUM_SYMBOL_CALC_MODE profit_calc_mode=(ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(symbol,SYMBOL_TRADE_CALC_MODE);
         switch(profit_calc_mode)
           {
            case  SYMBOL_CALC_MODE_FOREX:
               return((double)0);
            case  SYMBOL_CALC_MODE_FUTURES:
               return((double)2);
            case  SYMBOL_CALC_MODE_CFD:
               return((double)1);
            default :
              {
               string text="MQL4 MODE_MARGINCALCMODE returned MQL5 "+EnumToString(profit_calc_mode);
               Alert(text);
               Print(text);
               return(double("nan"));
              }
           }
        }

1.3. MQL4 状态检查

状态检查 MQL4 函数已在文件 [数据文件夹]\MQL5\Include\SimpleCall\Check.mqh 中被转换

它包括以下元素:

  • Digits
  • Point
  • IsConnected
  • IsDemo
  • IsDllsAllowed
  • IsExpertEnabled
  • IsLibrariesAllowed
  • IsOptimization
  • IsTesting
  • IsTradeAllowed
  • IsTradeContextBusy
  • IsVisualMode
  • TerminalCompany
  • TerminalName
  • TerminalPath
MQL4 MQL5 MQL5 类 注意
 Digits

 MQL4 允许同时使用 Digits 和 Digits()
 Point

 MQL4 允许同时使用 Point 和 Point()
 IsConnected  TerminalInfoInteger(TERMINAL_CONNECTED)  CTerminalInfo::IsConnected 
 IsDemo  AccountInfoInteger(ACCOUNT_TRADE_MODE)  CAccountInfo::TradeMode  从 ENUM_ACCOUNT_TRADE_MODE 枚举中获取其中一个值
 IsDllsAllowed  TerminalInfoInteger(TERMINAL_DLLS_ALLOWED) and MQLInfoInteger(MQL_DLLS_ALLOWED)  CTerminalInfo::IsDLLsAllowed   TERMINAL_DLLS_ALLOWED 的状态最高, 而 MQL_DLLS_ALLOWED 可以忽略
 IsExpertEnabled  TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)   CTerminalInfo::IsTradeAllowed  终端的 AutoTrading 按钮的状态
 IsLibrariesAllowed  MQLInfoInteger(MQL_DLLS_ALLOWED)  -/-  该检查毫无意义: 如果程序使用 DLL, 但您已禁用它们 (程序的依赖性选项卡), 那么您将无法运行该程序。
 IsOptimization  MQLInfoInteger(MQL_OPTIMIZATION)  -/-  MQL5 程序有四种模式: 调试, 代码分析, 测试和优化
 IsTesting  MQLInfoInteger(MQL_TESTER)  -/-  MQL5 程序有四种模式: 调试, 代码分析, 测试和优化
 IsTradeAllowed  MQLInfoInteger(MQL_TRADE_ALLOWED)  -/-  程序属性中的 "允许自动交易" 复选框状态
 IsTradeContextBusy  -/-  -/-  返回 "false"
 IsVisualMode  MQLInfoInteger(MQL_VISUAL_MODE)
 MQL5 程序有四种模式: 调试, 代码分析, 测试和优化
 TerminalCompany  TerminalInfoString(TERMINAL_COMPANY)  CTerminalInfo::Company  
 TerminalName  TerminalInfoString(TERMINAL_NAME)  CTerminalInfo::Name  
 TerminalPath  TerminalInfoString(TERMINAL_PATH)  CTerminalInfo::Path  

前两点 (Digits 和 Point) 无法实现, 因为 MQL4 和 MQL5 在这里完全混淆。 特别是, MQL4 可能同时面对 Digits 和 Digits() 以及 Point 和 Point()。 例如, 我不知道如何通过 'define' 将 Digits 和 Digits() 转换为 Digits()。 剩余的 XXXX(), 可以用 MQL5 等价函数替代。

具体实现如下:

//+------------------------------------------------------------------+
//|                                                        Check.mqh |
//|                                版权所有 2018, MetaQuotes 软件公司 |
//|                                           http://wmua.ru/slesar/ |
//+------------------------------------------------------------------+
#property copyright "版权所有 2018, MetaQuotes 软件公司"
#property link      "http://wmua.ru/slesar/"
#property version   "1.003" 
//--- 检查客户端和服务器之间的连接
#define IsConnected        (bool)TerminalInfoInteger(TERMINAL_CONNECTED)
//--- 检查智能交易系统是否在模拟账户上运行
#define IsDemo             (bool)(AccountInfoInteger(ACCOUNT_TRADE_MODE)==(ENUM_ACCOUNT_TRADE_MODE)ACCOUNT_TRADE_MODE_DEMO)
//--- 检查是否允许智能交易系统的 DLL 函数调用
#define IsDllsAllowed      (bool)TerminalInfoInteger(TERMINAL_DLLS_ALLOWED) 
//--- 检查是否允许智能交易系统运行
#define IsExpertEnabled    (bool)TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) 
//--- 检查智能交易系统是否可以调用库函数
#define IsLibrariesAllowed (bool)MQLInfoInteger(MQL_DLLS_ALLOWED)
//--- 检查智能交易系统是否在策略测试器优化模式下运行
#define IsOptimization     (bool)MQLInfoInteger(MQL_OPTIMIZATION)
//--- 检查智能交易系统是否在测试模式下运行
#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)
//+------------------------------------------------------------------+

1.4. MQL4 预定义变量

在文件 [数据文件夹]\MQL5\Include\SimpleCall\Predefined.mqh 中实现

MQL4 预定义变量:

  • _Digits
  • _Point
  • _LastError
  • _Period
  • _RandomSeed
  • _StopFlag
  • _Symbol
  • _UninitReason
  • Ask
  • Bars
  • Bid
  • Close
  • Digits
  • High
  • Low
  • Open
  • Point
  • Time
  • Volume

使用 #define 非参数形式将 _XXXX 预定义变量转换为 MQL5 函数:

//--- _Digits 变量存储小数点后的位数,
#define _Digits         Digits()
//--- _Point 变量包含报价货币中当前品种的点数大小
#define _Point          Point()
//--- _LastError 变量包含最后一个错误的代码
#define _LastError      GetLastError()
//--- _Period 变量包含当前图表的时间帧数值
#define _Period         Period()
//#define _RandomSeed
//--- _StopFlag 变量包含程序停止的标志
#define _StopFlag       IsStopped()
//--- _Symbol 变量包含当前图表的品种名称
#define _Symbol         Symbol()
//--- _UninitReason 变量包含程序未初始化原因的代码
#define _UninitReason   UninitializeReason()
//#define Bars            Bars(Symbol(),Period());
//#define Digits
//#define Point

唯一的例外是 "_RandomSeed" — 这个变量存储了伪随机整数发生器的当前状态。 Bars 不能被转换为 MQL5 (更确切地说, Bars 留待手工替换)。 Digits 和 Point 没有解决方案。 如上所述, Digits 和 Digits() 以及 Point 和 Point() 可以同时在文本中找到。

MQL4 的 Ask 和 Bid 被替换为自定义 (自行编写) 的 GetAsk() 和 GetBid() 函数:

//--- 当前品种最后知道的卖方价格 (ask 价格)
#define Ask             GetAsk()
//--- 当前品种最后知道的买方价格 (报价, bid 价格)
#define Bid             GetBid()
...
//--- 当前品种最后知道的卖方价格 (ask 价格)                  
double GetAsk()
  {
   MqlTick tick;
   SymbolInfoTick(Symbol(),tick);
   return(tick.ask);
  }
//--- 当前品种最后知道的买方价格 (报价, bid 价格)             
double GetBid()
  {
   MqlTick tick;
   SymbolInfoTick(Symbol(),tick);
   return(tick.bid);
  }

当然, 我们可以用更简单的方式为 Ask 编写一个宏定义:

#define Ask SymbolInfoDouble(__Symbol,SYMBOL_ASK)

SymbolInfoDouble 的注释说:

如果函数用来获取最后一次逐笔报价的数据, 最好采用 SymbolInfoTick()有可能在品种中没有报价, 因为 终端已连接到交易账户。 在这种情况下, 请求的值将是未定义的。

在大多数情况下, 使用 SymbolInfoTick() 函数就足够了, 该函数允许您获取每次调用的竞卖价, 竞买价, 每次调用的最后价格和交易量, 以及最后一次逐笔报价到达时间。

现在, 我们介绍一些新内容 — 使用 "operator (运算符)"

'operator' 关键字将用于重载 (重新分配) [] 索引操作符。 这是将 MQL4 时间序列数组 (Open[], High[], Low[], Close[], Time[], Volume[]) 翻译为 MQL5 形式所必需的。

以下帮助告诉我们关于 "operator (运算符)" 的内容:

操作重载允许使用复杂对象 (结构和类) 的操作符号 (以简单表达式的形式编写)。

因此, 我们可以假设我们需要创建一个类来重载 [] 索引操作符。

记住 #define:

#define 指令可用于将常用名称分配给常量。 有两种形式:

#define identifier expression                   // 无参数形式 
#define identifier(par1,... par8) expression    // 参数形式

#define 指令在源文本中将所有发现的 "identifier" 替换为 "expression"。

以下代码可读取为: #define 指令替代源文本中 所有找到的 159 条目

#define SeriesVolume(Volume,T) 159
//+-----------------------------------------------------------------+
//| 脚本程序 start 函数                                               |
//+------------------------------------------------------------------+
void OnStart()
  {
   long a=SeriesVolume(Volume,long);
  }

换言之, OnStart 中的代码被转换为:

   long a=159;

步骤 2

整个类 已放置在 #define 中, 

//+------------------------------------------------------------------+
//|                                                      Test_en.mq5 |
//|                                           版权所有 2012, 公司名称 |
//|                                       http://www.companyname.net |
//+------------------------------------------------------------------+
#define SeriesVolume(Volume,T) class CVolume       \
  {                                                \
  public:                                          \
    T operator[](const int i) const                \
    {                                              \
    long val[1];                                   \
    if(CopyTickVolume(Symbol(),Period(),i,1,val)==1)\
      return(val[0]);                              \
    else                                           \
      return(-1);                                  \
    }                                              \
  };                                               \
CVolume Volume;
//---
SeriesVolume(Volume,long)
//+------------------------------------------------------------------+
//| 脚本程序 start 函数                                               |
//+------------------------------------------------------------------+
void OnStart()
  {
   Print(Volume[4]);
  }
//+------------------------------------------------------------------+

这种替换可以表示如下:

//+------------------------------------------------------------------+
//|                                                      Test_en.mq5 |
//|                                           版权所有 2012, 公司名称 |
//|                                       http://www.companyname.net |
//+------------------------------------------------------------------+
class CVolume      
  {                                                
  public:                                          
    long operator[](const int i) const                
    {                                              
    long val[1];                                   
    if(CopyTickVolume(Symbol(),Period(),i,1,val)==1)
      return(val[0]);                              
    else                                           
      return(-1);                                  
    }                                              
  };                                               
CVolume Volume;
//+------------------------------------------------------------------+
//| 脚本程序 start 函数                                               |
//+------------------------------------------------------------------+
void OnStart()
  {
   Print(Volume[4]);
  }

换言之, 在 OnStart 中, 我们实际上为传递 i 索引的 [] 方法引用 CVolume 类的 Volume 对象。 同样的原则适用于 Open, High, Low, Close 和 Time 等 MQL4 序列。

最后, 我们有 MQL4 iXXX 函数: iOpen, iHigh, iLow, iClose, iTime 和 iVolume。 我们应该为它们应用自定义函数声明方法。

iClose 的示例:

//--- 返回指定品种、时间帧和偏移柱线的收盘价
double   iClose(
                string                    symbol,              // 品种
                ENUM_TIMEFRAMES           timeframe,           // 时间帧
                int                       shift                // 偏移
                )
  {
   double result=0.0;
//---
   double val[1];
   ResetLastError();
   int copied=CopyClose(symbol,timeframe,shift,1,val);
   if(copied>0)
      result=val[0];
   else
      Print(__FUNCTION__,": CopyClose error=",GetLastError());
//---
   return(result);
  }

2. 其它文件中的修改

在 [数据文件夹]\MQL5\Include\SimpleCall\IndicatorsMQL4.mqh 中, 现在所有 MQL4 指标线名称都设置在头文件中:

double NaN=double("nan");
#define MODE_MAIN          0   
#define MODE_SIGNAL        1
#define MODE_PLUSDI        1
#define MODE_MINUSDI       2  
#define MODE_GATORJAW      1
#define MODE_GATORTEETH    2
#define MODE_GATORLIPS     3 
#define MODE_UPPER         1
#define MODE_LOWER         2  
#define MODE_TENKANSEN     1     
#define MODE_KIJUNSEN      2                                   
#define MODE_SENKOUSPANA   3                                 
#define MODE_SENKOUSPANB   4                                 
#define MODE_CHIKOUSPAN    5   

在 [数据文件夹]\MQL5\Include\SimpleCall\Series.mqh 中, 我们已删除 

#define MODE_OPEN    0
//#define MODE_LOW     1
//#define MODE_HIGH    2
#define MODE_CLOSE   3
#define MODE_VOLUME  4
//#define MODE_TIME    5

现在, 它们在 [数据文件夹]\MQL5\Include\SimpleCall\Header.mqh 中编写:

//+------------------------------------------------------------------+
//|                                                       Header.mqh |
//|                                版权所有 2018, MetaQuotes 软件公司 |
//|                                           http://wmua.ru/slesar/ |
//+------------------------------------------------------------------+
#property copyright "版权所有 2018, MetaQuotes 软件公司"
#property link      "http://wmua.ru/slesar/"
//---
#define MODE_LOW     10001
#define MODE_HIGH    10002
#define MODE_TIME    10005

以及 iClose, iHigh, iLow, iOpen, iTime 和 iVolume — 现在它们被设置在 [数据文件夹]\MQL5\Include\SimpleCall\Predefined.mqh 中


结束语

之前的文章 中, 我们讨论了如何使用 MQL4 风格编写指标调用, 并阐述了后果。 事实证明, 简单的编写会导致智能交易系统的运行放缓, 且未能对创建的指标进行内置控制。 在本文中, 我们继续寻找简化代码并查看 #define 宏替换的方法。

结果就是, 您可以使用本文附带的代码令任任意 MQL4 EA 在 MetaTrader 5 中工作。 您只需包含重载或添加必要函数和预定义变量的必要头文件。

为了完全兼容, 只有 MQL4 的简化交易功能缺失。 但是这个问题也可以解决。 我们再次重申这种方法的优缺点作为结论:

缺点:

  • 访问指标时返回错误的处理限制;
  • 在同时访问多于一个指标时降低测试速度;
  • 根据连接的是 IndicatorsMQL5.mqh 或 IndicatorsMQL4.mqh, 需要正确高亮指示线;
  • 无法调试 #define 宏替代;
  • 没有参数化的 #define 参数工具提示;
  • 隐藏在宏定义后面的变量会有潜在冲突。
优点
  • 代码编写简单 — 一个字符串替换多个字符串;
  • 可视性和简洁性 — 代码量越少, 理解起来就越容易;
  • 宏替换以红色突出显示, 因此可以更轻松地查看用户 ID 和函数;
  • 能够开发自定义代码片段等价品。

我自己仍然是传统 MQL5 方法的支持者, 并将本文中描述的方法视为一种生活窍门。 也许, 这些文章将使那些习惯用 MQL4 风格编写代码的人能够克服他们过渡到 MetaTrader 5 平台时的心理障碍, 对于所有方面都更加方便。