在 MetaTrader 5 里使用 HedgeTerminal (对冲终端) 面板进行双向交易和仓位对冲, 第二部分

Vasiliy Sokolov | 29 十月, 2015

目录


介绍

这篇文章是 "在 MetaTrader 5 里使用 HedgeTerminal (对冲终端) 面板进行双向交易和仓位对冲的第一部分" 的延续。在第二部分里, 我们要讨论 EA 以及其它 MQL5 程序与 HedgeTerminalAPI 程序库的集成。阅读此文来学习如何操作这个程序库。它将帮助您在一个舒适简单的交易环境中创建双向交易 EA。

在程序库描述之外, 本文还涉及了异步交易基础和多线程编程。这些描述将在本文的第三、四节给出。所以, 这些资料对那些双向交易不感兴趣的交易者一样有用, 因为他能找到一些新的关于异步和多线程编程的内容。

以下提供的资料, 目的是为那些了解 MQL5 编程语言, 经验丰富的算法交易者。如果您不了解 MQL5, 请阅读本文的第一部分, 其中包含了简单的图解和绘图, 解释了程序库的一般原理和 HedgeTerminal 面板。


第一章. EA 与 HedgeTerminal 及其面板的交互。

1.1. 安装 HedgeTermianlAPI。首次启动程序库

安装 HedgeTerminalAPI 的过程不同于安装 HT 可视面板, 因为程序库不能在 MetaTrader 5 中独立运行。换言之, 您需要开发专门的 EA 来从程序库中调用 HedgeTerminalInstall() 函数。此函数将设置一个特殊的头文件 Prototypes.mqh, 来描述 HedgeTerminalAPI 里可用的函数。

在电脑上安装程序库有三步:

步骤 1. 下载 HedgeTerminalAPI 程序库到您的电脑。程序库相对于您的终端位置: \MQL5\Experts\Market\HedgeTerminalApi.ex5

步骤 2. 在 MQL5 向导里使用标准模板创建一个新的 EA 。MQL5 向导生成以下源代码:

//+------------------------------------------------------------------+
//|                                   InstallHedgeTerminalExpert.mq5 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
//+------------------------------------------------------------------+
//| EA 初始化函数                                                      |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| EA 逆初函数                                                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   
  }
//+------------------------------------------------------------------+
//| EA 即时报价函数                                                    |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
  }

步骤 3. 您的结果 EA 仅需一个函数 - OnInit(), 并指定从 HedgeTerminalApi 程序库里导出的 HedgeTerminalInstall() 安装器函数。在 OnInit() 函数里正确地运行此函数。以黄色标记的源代码执行这些操作:

//+------------------------------------------------------------------+
//|                                   InstallHedgeTerminalExpert.mq5 |
//|                        Copyright 2015, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2015, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"

#import "HedgeTerminalAPI.ex5"
   void HedgeTerminalInstall(void);
#import

//+------------------------------------------------------------------+
//| EA 初始化函数                                                      |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   HedgeTerminalInstall();
   ExpertRemove();   
//---
   return(INIT_SUCCEEDED);
  }

步骤 4. 您的进一步动作取决于您是否购买了程序库。如果您已经购买了它, 您可以直接在图表上实时运行 EA。这将启动整个 HedgeTerminal 产品线的标准安装器。您可以按照 "在 MetaTrader 5 里使用 HedgeTerminal 面板进行双向交易和仓位对冲, 第一部分" 文章里的 2.1 和 2.2 节描述的指导简单地完成这一步骤。安装向导会在您的电脑上安装所有需要的文件, 包括头文件和测试 EA。

如果您未购买此程序库, 而且只是希望测试它, 则您不能实时操作 EA, 但您可以在策略测试里运行 EA 以便测试 API。安装器在此情况下不会运行。在测试模式里, HedgeTermianalAPI 以单用户模式工作, 所以它不需要正常模式里安装的文件。亦即您无需进行任何配置。

一旦 EA 测试结束, 文件夹 \HedgeTerminal 将在终端的公用文件夹里被创建。MetaTrader 终端的公用目录的正常路径是 c:\Users\<用户名>\AppData\Roaming\MetaQuotes\Terminal\Common\Files\HedgeTerminal\, 此处 <用户名> 是您当前使用的电脑帐户的名字。而 \HedgeTerminal 文件夹已经包含了文件 \MQL5\Include\Prototypes.mqh\MQL5\Experts\Chaos2.mq5。拷贝这些文件至终端的同一目录下: 文件 Prototypes.mqh\MetaTrader5\MQL5\Include, 以及文件 Chaos2.mq5\MetaTrader5\MQL5\Experts

文件 Prototypes.mqh 是一个头文件包含从 HedgeTerminalAPI 程序库导出的函数描述。它们的目的和描述已经包含在它们的注释里。

文件 Chaos2.mq5 包含一个在 "混沌 II EA 例程中的 SendTradeRequest 函数和 HedgeTradeRequest 结构" 一节里描述的 EA 例程。以此方式您可以直观地理解 HedgeTerminalAPI 是如何工作的, 以及如何利用 HedgeTerminal 虚拟化技术开发一款 EA。

拷贝的文件可用于您的EA。所以您只需要在 EA 源代码里包含头文件即可开始使用程序库。此处是一个样例:

#include <Prototypes.mqh>
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   int transTotal = TransactionsTotal();
   printf((string)transTotal);
  }

例如, 以上代码获取持仓总数并在 MetaTrader 5 终端的 "Experts" 栏显示该数字。

重要的是理解 HedgeTerminal 在其任一函数首次调用时会进行初始化。初始化是调用 lazy。所以, 首次调用其任一函数时要花费较长时间。若您在首次调用时希望快速响应, 您必须预先初始化 HT, 例如, 您可以在 OnInit() 模块里调用 TransactionTotal() 函数。

利用 lazy 初始化, 您可以在 EA 里省略显式初始化。这极大简化了 HedgeTerminal 的操作, 且无必要预先配置它。


1.2. EA 与 HedgeTerminal 面板的集成

如果您有 HedgeTerminal 的可视化面板以及全功能的程序库版本, 即可实时运行的, 您能够将您的 EA 与面板集成, 以便通过它们执行的所有交易操作也能出现在面板里。一般情况下, 集成是不可见的。如果您使用 HedgeTermianalAPI 函数, 经机器人执行的动作会自动显示在面板上。不过, 您可以在每笔提交的事务里指明 EA 名称来扩展可视性。您可以通过取消文件 Settings.xml 里以下代码行的注释来达成:

<Column ID="Magic" Name="Magic" Width="100"/>

此标签是在 <Active-Position> 和 <History-Position> 部分里。

现在, 注释已删除, 且标签包含于处理之中。在面板重启之后, 新的一列 "Magic" 将会出现在活跃和历史仓位表格里。此列包含仓位所属 EA 的魔幻数字。

如果您希望出示 EA 名字来替代它的魔幻数字, 在别名文件 ExpertAliases.xml 里添加名字。例如, 一款 EA 的魔幻数字是 123847, 并且您希望显示它的名字, 如 "ExPro 1.1", 在文件里添加以下标签:

<Expert Magic="123847" Name="ExPro 1.1"></Expert>

若它正确完成, 则 EA 名字将会替代它的魔幻数字显示在恰当的列中:

图例. 1.  显示 EA 名字替代魔幻数字

图例. 1. 显示 EA 名字替代魔幻数字

注意面板与 EA 间的通信是实时的。这意味着若您在面板上直接将一笔 EA 的仓位平仓, 则 EA 将在下次调用 TransactionsTotal() 函数时获知此事。反之亦然: 当 EA 将其仓位平仓之后, 它立即从活跃仓位栏消失。


1.3. HedgeTerminalAPI 操作的一般原理

除了双向仓位之外, HedgeTerminal 也可工作于其它交易类型, 诸如挂单, 成交和经纪商的操作。HedgeTerminal 将所有类型作为一个单一 事务组处理。一笔成交, 一笔挂单, 一笔双向仓位 - 所有这些均是事务。不过, 一笔事务不能单独存在。在面向对象编程方面, 一笔事务可引申为一个抽象基准类, 所有交易实例, 诸如成交和双向仓位均自其继承。在这方面 HedgeTerminalAPI 的所有函数可以分成几组:

  1. 事务搜索和选择函数。函数的共同签名以及与 MetaTrader 4 函数 OrderSend()OrderSelect() 几乎一样的工作方式;
  2. 获取已选择事务属性的函数。每笔事务均有一套特殊的属性以及特殊的选择属性函数。函数的共同签名以及它们的工作方式类似 MetaTrader 5 系统函数访问仓位, 成交和订单 (比如 OrderGetDouble()HistoryDealGetInteger());
  3. HedgeTerminalAPI 仅使用一个交易函数: SendTradeRequest()。此函数可将一笔双向仓位平仓或部分平仓。同一函数可用于修改止损, 止盈或离场注释。函数的工作类似 MetaTrader 5 中的OrderSend();
  4. 获取一般错误的函数 GetHedgeError(), 函数用于详尽分析 HedgeTerminal 交易行为: TotalActionsTask()GetActionResult()。同样也用于错误检测。在 MetaTrader 4 或 MetaTrader 5 里没有类似特征。

所有函数的操作类似于使用 MetaTrader 4 和 MetaTrader 5 系统函数。作为一条规则, 函数的输入是一些标识符 (枚举值), 并且函数返回与之相应的数值。

每个函数都有可用的指定枚举。共同的调用签名如下:

<value> = Function(<identifier>);

让我们来研究一个获取仓位独有标识符的例子。这是它在 MetaTrader 5 中看到的样子:

ulong id = PositionGetInteger(POSITION_IDENTIFIER);

在 HedgeTerminal 里, 接收一笔双向仓位的独有标识符如下:

ulong id = HedgePositionGetInteger(HEDGE_POSITION_ENTRY_ORDER_ID)

函数的一般原理相同。仅有一个枚举类型不同。


1.4. 选择事务

遍历事务列表来选择一笔事务, 与在 MetaTrader 4 中搜索一笔订单类似。不过, 在 MetaTrader 4 里仅有订单可用于搜索, 而在 HedgeTerminal 里所有东西都可作为一笔事务来查找 - 诸如一笔挂单或对冲仓位。所以, 每笔事务应首先使用 TransactionSelect() 函数进行选择, 之后它的类型应可经 TransactionType()识别。

两个事务列表可用于日期: 活跃和历史事务。列表的应用被定义为基于 ENUM_MODE_TRADES 修饰符。它类似于 MetaTrader 4 里的 MODE_TRADES 修饰符。

事务搜索和选择算法如下:

1: for(int i=TransactionsTotal(MODE_TRADES)-1; i>=0; i--)
2:     {
3:      if(!TransactionSelect(i,SELECT_BY_POS,MODE_TRADES))continue;
4:      if(TransactionType()!=TRANS_HEDGE_POSITION)continue;
5:      if(HedgePositionGetInteger(HEDGE_POSITION_MAGIC) != Magic)continue;
6:      if(HedgePositionGetString(HEDGE_POSITION_SYMBOL) != Symbol())continue;
7:      if(HedgePositionGetInteger(HEDGE_POSITION_STATE) == POSITION_STATE_FROZEN)continue;
8:      ulong id = HedgePositionGetInteger(HEDGE_POSITION_ENTRY_ORDER_ID)
9:     }

代码在 for (行 1) 周期里循环遍历活跃事务列表。在您处理事务之前, 使用 TransactionSelect() (行 3) 选择它。仅有双向仓位被从这些事务里选择出来 (行 4)。如果仓位的魔幻数字和其品名不能匹配当前正在运行的品名和 EA 的魔幻数字, HT 移至下一仓位 (行 5 和 6)。之后它定义仓位的独有标识符 (行 8)。

特别要留意行 7。已选择仓位应检查能否修改的条件。如果仓位已处于修改进程, 它不能在当前线程里改变, 尽管您可以得到它的属性。如果仓位被锁定, 最好待至它被解锁再访问它的属性或重新尝试修改它。属性 HEDGE_POSITION_STATE 用于查询仓位是否可以修改。

POSITION_STATE_FROZEN 修饰符表示此仓位被 "冻结" 且不能被改变。POSITION_STATE_ACTIVE 修饰符表示仓位处于活跃且可以被改变。这些修饰符列于 ENUM_HEDGE_POSITION_STATE 枚举, 它们已记录在相应的 章节

如果需要遍历历史事务, 在函数 TransactionTotal()TransactionSelect() 里的 MODE_TRADES 修饰符必须替换为 MODE_HISTORY

在 HedgeTerminal 中一笔事务可以嵌套另一笔。这与 MetaTrader 5 中的概念十分不同, 那里没有嵌套。例如, 在 HedgeTerminal 中的历史双向仓位由两笔订单组成, 每笔包含一笔成交。嵌套可以表示如下:

图例. 2. 嵌套事务

图例. 2. 嵌套事务

事务嵌套可以清晰地在 HedgeTerminal 可视面板上看到。

以下截图示意 MagicEx 1.3 仓位的详情:

图例. 3. 在 HedgeTerminal 面板上的嵌套事务

图例. 3. 在 HedgeTerminal 面板上的嵌套事务

可以访问双向仓位内的特定订单或成交的属性。

为此:

  1. 选择一笔历史事务并确认它是一笔双向仓位;
  2. 使用 HedgeOrderSelect()选择此仓位的一笔订单;
  3. 获取选择订单的一个属性: 包含的成交号码;
  4. 通过遍历所有成交选择这笔成交所属的订单;
  5. 获取所需的成交属性。

注意: 事务被选择之后, 它的指定属性变为可用。例如, 如果事务是一笔订单, 则通过 HedgeOrderSelect() 选择之后, 您可以找到它的成交号码 (HedgeOrderGetInter(HEDGE_ORDER_DEALS_TOTAL)) 或平均加权入场价格 (HedgeDealGetDouble(HEDGE_DEAL_PRICE_EXECUTED))。

让我们来查询成交 #1197610 的价格, 其在截图里以红色标记。这笔成交是 MagicEx 1.3 EA 双向仓位的一部分。

通过以下代码, EA 可以访问它的仓位和其成交:

#include <Prototypes.mqh>

ulong Magic=5760655; // MagicEx 1.3.

//+------------------------------------------------------------------+
//| EA 即时报价函数                                                    |
//+------------------------------------------------------------------+ 
void OnTick()
  {
   for(int i=TransactionsTotal(MODE_HISTORY)-1; i>=0; i--)
    {
      if(!TransactionSelect(i,SELECT_BY_POS,MODE_HISTORY))continue;        // 选择事务 #i;
      if(TransactionType()!=TRANS_HEDGE_POSITION)continue;                 // 如果事务不是仓位 - 继续;
      if(HedgePositionGetInteger(HEDGE_POSITION_MAGIC) != Magic)continue;  // 如果仓位不是主要的 - 继续;
      ulong id = HedgePositionGetInteger(HEDGE_POSITION_ENTRY_ORDER_ID);   // 获取平仓订单 id;
      if(id!=5917888)continue;                                             // 如果仓位 id  != 5917888 - 继续;
      printf("1: -> 选择仓位 #"+(string)id);                        // 打印仓位 id;
      if(!HedgeOrderSelect(ORDER_SELECTED_CLOSED))continue;                // 选择平仓订单或继续;    
      ulong order_id = HedgeOrderGetInteger(HEDGE_ORDER_ID);               // 获取平仓订单 id;
      printf("2: ----> 选择订单 #" + (string)order_id);                // 打印订单 id;
      int deals_total = (int)HedgeOrderGetInteger(HEDGE_ORDER_DEALS_TOTAL);// 获取已选订单的成交总数;
      for(int deal_index = deals_total-1; deal_index >= 0; deal_index--)   // 搜索成交 #1197610...
        {
         if(!HedgeDealSelect(deal_index))continue;                         // 以索引选择成交或继续;
         ulong deal_id = HedgeDealGetInteger(HEDGE_DEAL_ID);               // 获取当前成交 id;
         if(deal_id != 1197610)continue;                                   // 选择成交 #1197610;
         double price = HedgeDealGetDouble(HEDGE_DEAL_PRICE_EXECUTED);     // 获取执行价格;
         printf("3: --------> 选择成交 #"+(string)deal_id+              // 打印执行价格;
              ". Executed price = "+DoubleToString(price,0));
        }
     }
  }

在代码执行之后, 以下条目将在 MetaTrader 5 终端的 EA 日志栏出现:

2014.10.21 14:46:37.545 MagicEx1.3 (VTBR-12.14,D1)      3: --------> 选择成交 #1197610. Executed price = 4735
2014.10.21 14:46:37.545 MagicEx1.3 (VTBR-12.14,D1)      2: ----> 选择订单 #6389111
2014.10.21 14:46:37.545 MagicEx1.3 (VTBR-12.14,D1)      1: -> 选择仓位 #5917888

该 EA 首先选择仓位 #5917888, 之后选择仓位内的订单 #6389111。一旦订单被选择, EA 开始搜索成交号码 1197610。当发现成交时, EA 获取它的执行价格并在日志里输出价格。


1.5. 如何使用 GetHedgeError() 获取错误代码

当工作于 HedgeTerminal 环境时, 有可能发生错误和意外情形。错误获取和分析函数用于这些情况。

最简单的情况是当您忘记使用 TransactionSelect() 函数来选择一笔事务时将得到一个错误。函数 TransactionType() 在此情况下将返回修饰符 TRANS_NOT_DEFINED

为了理解问题出于何处, 我们需要得到最后错误的修饰符。修饰符将告诉我们现在事务已被选择。以下代码做这些事:

for(int i=TransactionsTotal(MODE_HISTORY)-1; i>=0; i--)
  {
   //if(!TransactionSelect(i,SELECT_BY_POS,MODE_HISTORY))continue;        // 忘记选择;
   ENUM_TRANS_TYPE type = TransactionType();
   if(type == TRANS_NOT_DEFINED)
   {
      ENUM_HEDGE_ERR error = GetHedgeError();
      printf("错误, 事务类型未定义。原因: " + EnumToString(error));
   }
  }

结果消息是:

错误, 事务类型未定义。原因: HEDGE_ERR_TRANS_NOTSELECTED

错误 ID 提示我们在尝试获取它的类型之前, 忘记选择一笔事务。

所有可能的错误列于 ENUM_HEDGE_ERR 结构。


1.6. 使用 TotalActionsTask() 和 GetActionResult() 详细分析交易并识别错误

除了在 HedgeTerminal 环境中, 工作的过程中发生的错误, 交易错误也可能作为调用 SendTradeRequest() 的结果发生。这些类型的错误更加难以处理。通过 SendTradeRequest() 执行一个任务可能包含多个交易活动(子任务)。例如, 改变一笔处于止损位保护下的仓位离场注释, 您必须进行两个交易动作:

  1. 取消对应此止损位的挂单;
  2. 在之前订单的位置放置新注释的新挂单。

如果新止损订单触发, 则其注释会显示为平仓注释, 此为正确的方式。

不过, 任务可以被部分执行。假设, 挂单成功取消, 但出于什么原因放置新订单失败。在此情况下, 仓位会保留但没有了止损位。为能够处理这种错误, EA 需调用特殊任务的记录并搜索子任务是否失败。

这可使用两个函数完成: TotalActionsTask() 返回此任务的交易动作总数 (子任务); 而 GetActionResult() 接受子任务索引并返回它的类型以及它的执行结果。因为所有的交易操作使用标准的 MetaTrader 5 工具来执行, 它们的执行结果对应于交易服务器的返回代码。

通常, 搜索失败原因的算法如下:

  1. 使用 TotalActionsTask() 得到该任务的所有子任务数量;
  2. for 循环里搜索所有子任务。判断每个子任务的类型及其结果。

假设, 带有新注释的停止订单不能放置是因为执行价格太接近当前价位。。

以下例程代码示意 EA 如何发现失败原因:

#include <Prototypes.mqh> 

ulong Magic=5760655; // MagicEx 1.3.

//+------------------------------------------------------------------+
//| EA 即时报价函数                                                    |
//+------------------------------------------------------------------+ 
void OnTick()
  {
//检测活跃仓位
   for(int i=TransactionsTotal(MODE_HISTORY)-1; i>=0; i--)
     {
      if(!TransactionSelect(i,SELECT_BY_POS,MODE_HISTORY))continue;
      ENUM_TRANS_TYPE type=TransactionType();
      if(type==TRANS_NOT_DEFINED)
        {
         ENUM_HEDGE_ERR error=GetHedgeError();
         printf("错误, 事务未定义。原因: "+EnumToString(error));
        }
      if(TransactionType()!=TRANS_HEDGE_POSITION)continue;
      if(HedgePositionGetInteger(HEDGE_POSITION_MAGIC) != Magic)continue;
      if(HedgePositionGetString(HEDGE_POSITION_SYMBOL) != Symbol())continue;
      HedgeTradeRequest request;
      request.action=REQUEST_MODIFY_COMMENT;
      request.exit_comment="我的新注释";
      if(!SendTradeRequest(request)) // 出错?
        {
         for(uint action=0; action < TotalActionsTask(); action++)
           {
            ENUM_TARGET_TYPE typeAction;
            int retcode=0;
            GetActionResult(action, typeAction, retcode);
            printf("动作#" + (string)action + ": " + EnumToString(type) +(string)retcode);
           }
        }
     }
  }

在代码执行之后以下消息将会出现:

动作 #0 TARGET_DELETE_PENDING_ORDER 10009 (TRADE_RETCODE_PLACED)
动作 #1 TARGET_SET_PENDING_ORDER 10015 (TRADE_RETCODE_INVALID_PRICE)

通过对比交易服务器返回代码的标准修饰符的号码, 我们发现挂单成功移除, 但放置新订单失败。交易服务器返回一个错误代码 10015 (不正确的价格), 这意味着当前价格与停止价太接近。

知道这点, EA 即可控制停止位。为此, EA 仅可使用相同的 SendTradeRequest() 函数来平仓。


1.7. 跟踪交易任务执行状态

每次交易任务可由顺序执行的任意数量的子任务组成。

在异步模式中, 一个任务的执行可以传递若干代码。也有可能是任务 "冻结" 的情况。因此 EA 的控制任务执行是必需的。当采用 HEDGE_POSITION_TASK_STATUS 修饰符调用 HedgePositionGetInteger() 函数时, 它返回 ENUM_TASK_STATUS 类型枚举包括当前仓位任务的状态。

例如, 如果在发送一笔平仓订单之后有什么错误, 由于仓位未平仓, 则您需要得到任务状态。

以下例子显示一段异步 EA 的代码, 可执行用来分析仓位任务的状态:

ENUM_TASK_STATUS status=HedgePositionGetInteger(HEDGE_POSITION_TASK_STATUS);
switch(status)
  {
   case TASK_STATUS_COMPLETE:
      printf("任务完成!");
      break;
   case TASK_STATUS_EXECUTING:
      printf("任务正在执行。等待...");
      Sleep(200);
      break;
   case TASK_STATUS_FAILED:
      printf("记录执行任务。打印记录...");
      for(int i=0; i<TotalActionsTask(); i++)
        {
         ENUM_TARGET_TYPE type;
         uint retcode;
         GetActionResult(i,type,retcode);
         printf("#"+i+" "+EnumToString(type)+" "+retcode);
        }
      break;
   case TASK_STATUS_WAITING:
      printf("任务很快开始。");
      break;
  }

注意, 一些复杂任务需要加倍迭代执行。

在异步模式中, 在交易环境里信号改变事件将开始新迭代。因此, 所有迭代无延迟执行, 一个接一个, 随后接收到来自交易服务器的相应。在同步模式里任务的执行略有不同。

同步方法使用 同步操作模拟器, 由于用户可以在单次传递中执行复合任务。模拟器使用时间滞后。例如, 在子任务开始执行之后, 模拟器不可返回执行线程到 EA。作为替代, 它等待一些时间期待交易环境的变化。之后, 它再次重读交易环境。如果它知道子任务已成功完成, 就开始下一个子任务。

这个过程有些降低了总体性能, 因为它要花费一些时间来等待。但它可将复杂的任务分解成十分简单的执行序列, 按顺序调用单一函数。所以, 您在同步方法里几乎无需分析任务执行记录。


1.8. 如何修改和关闭双向仓位

双向仓位可使用 SendTradeRequest() 函数来修改及平仓。只有三个选项可用于活跃仓位:

  1. 仓位可全部或部分平仓;
  2. 仓位的止损和止盈可被修改;
  3. 仓位离场注释可被修改。

历史仓位不可更改。类似于 MetaTrader 5 里的 OrderSend() 函数, SendTradeRequest() 使用一个预编译队列来形成一个交易结构 HedgeTraderRequest。参阅文档获取 SendTradeRequest() 函数和 HedgeTraderRequest 结构的进一步详情。例程示意混沌 II EA 的仓位修改和平仓片断。


1.9. 如何通过 EA 设置 HedgeTerminal 属性

HedgeTerminal 处理一套属性, 诸如刷新率, 等待来自服务器的响应秒数及其它。

所有这些属性在 Settings.xml 里定义。当一款 EA 实时运行时, 程序库从文件里读取属性并设置适当的内部参数。当 EA 在图表上测试时, 文件 Settings.xml 未使用。然而, 在某些情形当中您也许需要单独修改 EA 属性, 而不管它是否运行在图表或策略测试中。

完成这些可通过特殊的函数集合 HedgePropertySet…当前版本仅有一个来自该集合的原型:

enum ENUM_HEDGE_PROP_INTEGER
{
   HEDGE_PROP_TIMEOUT,
};

bool HedgePropertySetInteger(ENUM_HEDGE_PROP_INTEGER property, int value)

例如, 设置程序库等待服务器响应的超时, 编写如下:

bool res = HedgePropertySetInteger(HEDGE_PROP_TIMEOUT, 30);

如果您在发送异步请求之后的 30 秒内未收到服务器响应, 锁定的仓位将会解锁。


1.10. 同步和异步操作模式

HedgeTerminal 及其 API 的交易活动完全以异步方式执行。

尽管, 这种模式要求 EA 更为复杂。为了隐藏这种复杂性, HedgeTerminalAPI 包含了一个特殊的同步操作模拟器, 允许在传统的同步方式下开发 EA, 并与 HedgeTerminalAPI 的异步算法通信。这种交互表现在通过 SendTradeRequest() 对双向仓位进行修改和平仓的时刻。该函数允许执行交易任务时, 即可以采用同步模式, 或是采用异步模式。省缺时, 所有交易动作经过同步操作模拟器同步执行。然而, 如果一个交易请求 (HedgeTradeRequest 结构) 包含一个显式特殊标志 asynch_mode = true, 则交易任务将以异步模式执行。

在异步模式中, 任务将独立于主线程执行。在异步 EA 与 HedgeTerminal 的异步算法之间进行交互尚未完全实现。

同步模拟器十分简单。它按顺序开始子任务, 并等待一些时间, 直到 MetaTrader 5 的交易环境变化。模拟器分析这些变化并判断当前任务的状态。如果任务执行成功, 模拟器前进到下一步。

同步模拟器将导致交易订单的执行略有延迟。这实际上是由于在 MetaTrader 5 的交易环境里, 交易动作的执行反馈需要一点时间。访问环境的必要性主要在于, 实际上 HedgeTermianlAPI 在同步线程模拟器模式下不能访问进入 OnTradeTransaction() 处理器的事件。

通过模拟器在异步线程之间交互, 以及在异步与同步线程之间交互的问题十分复杂且没有明确的解决方案。


1.11. 通过脚本例程管理双向仓位属性

在以下脚本中, TransactionSelect() 函数在动作事务列表里搜索所有可用事务。

从列表里选择每笔事务。如果事务为仓位, 将会访问它的一些属性并打印。除了仓位的属性之外, 仓位之内的订单和成交属性也一并打印。订单和成交首先使用 HedgeOrderSelect() HedgeDealSelect() 分别选择。

所有仓位及其订单和成交的属性将使用系统函数 printf 合并打印在单一行中。

//+------------------------------------------------------------------+
//|                                           sample_using_htapi.mq5 |
//|         Copyright 2014, Vasiliy Sokolov, Russia, St.-Petersburg. |
//|                              https://login.mql5.com/ru/users/c-4 |
//+------------------------------------------------------------------+
#property copyright "Copyright 2014, Vasiliy Sokolov."
#property link      "https://login.mql5.com/ru/users/c-4"
#property version   "1.00"

// 包含 HedgeTerminalAPI 程序库的函数原型。
#include <Prototypes.mqh> 

//+------------------------------------------------------------------+
//| EA 即时报价函数                                                    |
//+------------------------------------------------------------------+ 
void OnStart()
  {
   // 在事务列表里搜索所有事务...
   for(int i=TransactionsTotal(); i>=0; i--)
     {
      if(!TransactionSelect(i,SELECT_BY_POS,MODE_TRADES))                           // 从活跃事务里选择
        {
         ENUM_HEDGE_ERR error=GetHedgeError();                                      // 若选择失败, 获取原因
         printf("选择事务错误 # "+(string)i+". 原因: "+            // 打印原因
                EnumToString(error));
         ResetHedgeError();                                                         // 清除错误
         continue;                                                                  // 继续下一个事务
        }
      // 仅处理对冲仓位
      if(TransactionType()==TRANS_HEDGE_POSITION) 
        {
         // --- 仓位说明 --- //
         ENUM_TRANS_DIRECTION direction=(ENUM_TRANS_DIRECTION)                      // 获取方向说明
                              HedgePositionGetInteger(HEDGE_POSITION_DIRECTION);
         double price_entry = HedgeOrderGetDouble(HEDGE_ORDER_PRICE_EXECUTED);      // 获取持仓量
         string symbol = HedgePositionGetString(HEDGE_POSITION_SYMBOL);             // 获取持仓品名
         // --- 订单说明 --- //
         if(!HedgeOrderSelect(ORDER_SELECTED_INIT))continue;                        // 在持仓里选择初始订单
         double slippage = HedgeOrderGetDouble(HEDGE_ORDER_SLIPPAGE);               // 获取滑点
         uint deals_total = (uint)HedgeOrderGetInteger(HEDGE_ORDER_DEALS_TOTAL);    // 获取成交总数
         // --- 成交说明 --- //
         double commissions=0.0;
         ulong deal_id=0;
         //在成交列表里搜索所有成交...
         for(uint d_index=0; d_index<deals_total; d_index++)                        
           {
            if(!HedgeDealSelect(d_index))continue;                                  // 按照其索引选择成交
            deal_id = HedgeDealGetInteger(HEDGE_DEAL_ID);                           // 获取成交标识
            commissions += HedgeDealGetDouble(HEDGE_DEAL_COMMISSION);               // 计算佣金
           }
         int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
         printf("仓位 #" + (string)i + ": 方向 " + EnumToString(direction) +     // 打印结果行
         "; 入场价格 " + DoubleToString(price_entry, digits) + 
         "; 初始滑点 " + DoubleToString(slippage, 2) + "; 最后成交标识 " +
         (string)deal_id + "; 佣金总和 " + DoubleToString(commissions, 2));
        }
     }
  }

1.12. 以混沌 II EA 为例的 SendTradeRequest() 函数和 HedgeTradeRequest 结构

作为一个例程, 让我们基于比尔·威廉姆斯在其著作 混沌交易法。第二版 的交易策略开发一个交易机器人。

我们不会完全照搬他的建议, 通过省略 鳄鱼 指标以及一些其它条件来简化方案。这一策略的选择源于几个方面的考虑。最主要的是, 这个战策略包括复合仓位的维护战术。有时您需要部分平仓, 并将止损移动到盈亏平衡。

当行情到达盈亏平衡, 停止位将尾随价格移动。第二个考虑则是对这种战术足够了解, 且为其开发的指标已包括在标准的 MetaTrader 5 发行包内。让我们稍微修改并简化一些规则, 以避免 EA 的复杂逻辑从而阻碍其主要目标: 示意 EA 与 HedgeTerminalAPI 程序库进行交互。该 EA 的逻辑更多利用 HedgeTerminalAPI 的交易函数。这是对程序库的一个很好测试。

让我们从 反转柱线 开始。一根多头反转柱线是收盘价在其高位三分之一, 最低价为 N 根柱线最低的柱线。一根空头反转柱线是收盘价在其低位三分之一, 最高价为 N 根柱线最高的柱线。N 是随机选择参数, 它可以在 EA 启动期间设置。这与经典的 "混沌 2" 策略不同。

在定义了反转柱线之后, 放置两笔挂单。对于多头柱线, 订单放置要高于其最高价, 对于空头柱线 - 就是低于其最低价。如果这两笔订单在 OldPending 柱线期间未能触发, 信号则认为过时了, 订单将被取消。OldPending 和 N 的数值可当用户在图表上启动 EA 之前设置。

订单触发并分为两笔双向仓位。EA 通过在注释里分别添加数字, "# 1" 和 "# 2" 来区分它们。这不是一个很优雅的解决方案, 但对于演示目的就很好了。一旦订单触发, 止损位将置于反转柱线的最高价 (对于空头) 或最低价 (如果多头)。

第一个仓位有严格的目标。设置可触发的止盈, 仓位的盈利应与绝对亏损相等。例如, 如果多头仓位在 1.0000 处开仓, 且其止损设于 0.9000, 则止盈位应设于 1.0000 + (1.0000 - 0.9000) = 1.1000。EA 将在止损或止盈位离场。

第二笔仓位是长线。其止损将尾随行情。当形成新的比尔·威廉姆斯分形时止损会移动。对于多头仓位, 止损依据低位分形移动, 而高位分型用于空头仓位。EA 仅在止损位离场。

以下图是该策略的图解:

图例. 4. 在价格图表上的混沌 2 EA 双向仓位示意

图例. 4. 在价格图表上的混沌 2 EA 双向仓位示意

反转柱线由红色边框标记。在此图表上 N 周期等于 2。对于该策略, 在最适当的时候选择。空头仓位显示为蓝色虚线, 多头仓位用绿色表示。如图所示, 多头和空头仓位可以同时存在, 即使在相对简单的策略里。注意周期是自 2014 年 1 月 5 到 8 日。

这是一个 AUDCAD 下跌趋势的转折点。1 月 4 日收到一个来自多头反转柱线的信号, 并于1 月 5 日开了两笔多头仓位。此时, 依然有三笔空头仓位, 它们的止损会尾随趋势 (红色虚线)。之后, 于 1 月 7 日, 空头仓位止损触发, 市场里仅留有多头仓位。

净持仓的变化很难监控, 因为净交易量不会考虑由 EA 维护的真实仓位数量。HedgeTerminal 允许 EA 监控它们各自的仓位, 而不管当前的净持仓量, 使得获取这些图表并开发类似策略成为可能。

以下代码实现该策略。

我故意不使用面向对象编程, 以便代码适合初学者:

//+------------------------------------------------------------------+
//|                                                       Chaos2.mq5 |
//|     Copyright 2014, Vasiliy Sokolov specially for HedgeTerminal. |
//|                                          St.-Petersburg, Russia. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2014, Vasiliy Sokolov."
#property link      "https://login.mql5.com/ru/users/c-4"
#property version   "1.00"

//+------------------------------------------------------------------+
//| 包含文件                                                          |
//+------------------------------------------------------------------+
#include <Prototypes.mqh>           // 包含 HedgeTerminalAPI 程序库的函数原型

//+------------------------------------------------------------------+
//| 输入参数。                                                         |
//+------------------------------------------------------------------+
input uint N=2;                     // 极大/极小周期
input uint OldPending=3;            // 过时挂单

//+------------------------------------------------------------------+
//| EA 的私有变量。                                                    |
//+------------------------------------------------------------------+
ulong Magic = 2314;                 // EA 魔幻数字
datetime lastTime = 0;              // 函数 DetectNewBar 的最后记忆时间
int hFractals = INVALID_HANDLE;     // 指标 '分形' 的句柄参阅: 'https://www.mql5.com/en/docs/indicators/ifractals'
//+------------------------------------------------------------------+
//| 比尔·威廉姆斯策略的柱线类型                                          |
//+------------------------------------------------------------------+
enum ENUM_BAR_TYPE
  {
   BAR_TYPE_ORDINARY,               // 一般柱线。
   BAR_TYPE_BEARISH,                // 该柱线收盘价位于高点三分之一处且它的最小值是 N 周期的最低
   BAR_TYPE_BULLISH,                // 该柱线收盘价位于低点三分之一处且它的最大值是 N 周期的最高
  };
//+------------------------------------------------------------------+
//| 极值类型。                                                         |
//+------------------------------------------------------------------+
enum ENUM_TYPE_EXTREMUM
  {
   TYPE_EXTREMUM_HIGHEST,           // 最高价极值
   TYPE_EXTREMUM_LOWEST             // 最低价极值
  };
//+------------------------------------------------------------------+
//| 仓位类型。                                                         |
//+------------------------------------------------------------------+
enum ENUM_ENTRY_TYPE
  {
   ENTRY_BUY1,                      // 多头仓位带止损
   ENTRY_BUY2,                      // 多头仓位带止盈
   ENTRY_SELL1,                     // 空头仓位带止盈
   ENTRY_SELL2,                     // 空头仓位带止损
   ENTRY_BAD_COMMENT                // 错误注释仓位
  };
//+------------------------------------------------------------------+
//| EA 初始化函数                                                      |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 创建 '分形' 指标 ---//
   hFractals=iFractals(Symbol(),NULL);
   if(hFractals==INVALID_HANDLE)
      printf("警告!指标 '分形' 未能创建。原因: "+
             (string)GetLastError());
//--- 以时间帧调整魔幻数字 ---//
   int minPeriod=PeriodSeconds()/60;
   string strMagic=(string)Magic+(string)minPeriod;
   Magic=StringToInteger(strMagic);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| EA 逆初函数                                                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- 删除指标 '分形' ---//
   if(hFractals!=INVALID_HANDLE)
      IndicatorRelease(hFractals);
//---
  }
//+------------------------------------------------------------------+
//| EA 即时报价函数                                                    |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- 仅当新柱线开盘时运行逻辑。---//
   int totals=SupportPositions();
   if(NewBarDetect()==true)
     {
      MqlRates rates[];
      CopyRates(Symbol(),NULL,1,1,rates);
      MqlRates prevBar=rates[0];
      //--- 设置新挂单 ---//
      double closeRate=GetCloseRate(prevBar);
      if(closeRate<=30 && BarIsExtremum(1,N,TYPE_EXTREMUM_HIGHEST))
        {
         DeleteOldPendingOrders(0);
         SetNewPendingOrder(1,BAR_TYPE_BEARISH);
        }
      else if(closeRate>=70 && BarIsExtremum(1,N,TYPE_EXTREMUM_LOWEST))
        {
         DeleteOldPendingOrders(0);
         SetNewPendingOrder(1,BAR_TYPE_BULLISH);
        }
      DeleteOldPendingOrders(OldPending);
     }
//---
  }
//+------------------------------------------------------------------+
//| 分析开仓并在需要时修改。                                             |
//+------------------------------------------------------------------+
int SupportPositions()
  {
//---
   int count=0;
   //--- 分析活跃仓位... ---//
   for(int i=0; i<TransactionsTotal(); i++) // 获取仓位总数。
     {
      //--- 选择主要活跃仓位 ---//
      if(!TransactionSelect(i, SELECT_BY_POS, MODE_TRADES))continue;             // 选择活跃事务
      if(TransactionType() != TRANS_HEDGE_POSITION)continue;                     // 仅选择对冲仓位
      if(HedgePositionGetInteger(HEDGE_POSITION_MAGIC) != Magic)                 // 以魔幻数字选择主要仓位
      if(HedgePositionGetInteger(HEDGE_POSITION_STATE) == POSITION_STATE_FROZEN) // 如果仓位为零 - 继续
         continue;                                                               // 稍后尝试访问仓位
      count++;
      //--- 我们要选择哪个仓位?... ---//
      ENUM_ENTRY_TYPE type=IdentifySelectPosition();
      bool modify=false;
      double sl = 0.0;
      double tp = 0.0;
      switch(type)
        {
         case ENTRY_BUY1:
         case ENTRY_SELL1:
           {
            //--- 检查止损, 止盈位并在需要时修改它。---//
            double currentStop=HedgePositionGetDouble(HEDGE_POSITION_SL);
            sl=GetStopLossLevel();
            if(!DoubleEquals(sl,currentStop))
               modify=true;
            tp=GetTakeProfitLevel();
            double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
            double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID);
            //--- 如果价格超越止盈位, 按止盈平仓
            bool isBuyTp=tp<bid && !DoubleEquals(tp,0.0) && type==ENTRY_BUY1;
            bool isSellTp=tp>ask && type==ENTRY_SELL1;
            if(isBuyTp || isSellTp)
              {
               HedgeTradeRequest request;
               request.action=REQUEST_CLOSE_POSITION;
               request.exit_comment="由 EA 止盈平仓";
               request.close_type=CLOSE_AS_TAKE_PROFIT;
               if(!SendTradeRequest(request))
                 {
                  ENUM_HEDGE_ERR error=GetHedgeError();
                  string logs=error==HEDGE_ERR_TASK_FAILED ?". 打印日志..." : "";
                  printf("因止盈失败平仓。原因: "+EnumToString(error)+" "+logs);
                  if(error==HEDGE_ERR_TASK_FAILED)
                     PrintTaskLog();
                  ResetHedgeError();
                 }
               else break;
              }
            double currentTakeProfit=HedgePositionGetDouble(HEDGE_POSITION_TP);
            if(!DoubleEquals(tp,currentTakeProfit))
               modify=true;
            break;
           }
         case ENTRY_BUY2:
           {
            //--- 检查止损位并设置修改标志。---//
            sl=GetStopLossLevel();
            double currentStop=HedgePositionGetDouble(HEDGE_POSITION_SL);
            if(sl>currentStop)
               modify=true;
            break;
           }
         case ENTRY_SELL2:
           {
            //--- 检查止损位并设置修改标志。---//
            sl=GetStopLossLevel();
            double currentStop=HedgePositionGetDouble(HEDGE_POSITION_SL);
            bool usingSL=HedgePositionGetInteger(HEDGE_POSITION_USING_SL);
            if(sl<currentStop || !usingSL)
               modify=true;
            break;
           }
        }
      //--- 如果需要修改止损, 止盈位 - 修改它。---//
      if(modify)
        {
         HedgeTradeRequest request;
         request.action=REQUEST_MODIFY_SLTP;
         request.sl = sl;
         request.tp = tp;
         if(type==ENTRY_BUY1 || type==ENTRY_SELL1)
            request.exit_comment="由止盈位离场";
         else
            request.exit_comment="由尾随止损离场";
         if(!SendTradeRequest(request))
           {
            ENUM_HEDGE_ERR error=GetHedgeError();
            string logs=error==HEDGE_ERR_TASK_FAILED ?". 打印日志..." : "";
            printf("修改止损或止盈失败。原因: "+EnumToString(error)+" "+logs);
            if(error==HEDGE_ERR_TASK_FAILED)
               PrintTaskLog();
            ResetHedgeError();
           }
         else break;
        }
     }
   return count;
//---
  }
//+------------------------------------------------------------------+
//| 返回选择仓位的止损位。                                              |
//| 结果                                                              |
//|   止损位                                                          |
//+------------------------------------------------------------------+
double GetStopLossLevel()
  {
//---
   double point=SymbolInfoDouble(Symbol(),SYMBOL_TRADE_TICK_SIZE)*3;
   double fractals[];
   double sl=0.0;
   MqlRates ReversalBar;

   if(!LoadReversalBar(ReversalBar))
     {
      printf("反转柱线加载失败。");
      return sl;
     }
   //--- 我们要选择哪个仓位?... ---//
   switch(IdentifySelectPosition())
     {
      case ENTRY_SELL2:
        {
         if(HedgePositionGetInteger(HEDGE_POSITION_USING_SL))
           {
            sl=NormalizeDouble(HedgePositionGetDouble(HEDGE_POSITION_SL),Digits());
            CopyBuffer(hFractals,UPPER_LINE,ReversalBar.time,TimeCurrent(),fractals);
            for(int i=ArraySize(fractals)-4; i>=0; i--)
              {
               if(DoubleEquals(fractals[i],DBL_MAX))continue;
               if(DoubleEquals(fractals[i],sl))continue;
               if(fractals[i]<sl)
                 {
                  double price= SymbolInfoDouble(Symbol(),SYMBOL_ASK);
                  int ifreeze =(int)SymbolInfoInteger(Symbol(),SYMBOL_TRADE_FREEZE_LEVEL);
                  double freeze=SymbolInfoDouble(Symbol(),SYMBOL_TRADE_TICK_SIZE)*ifreeze;
                  if(fractals[i]>price+freeze)
                     sl=NormalizeDouble(fractals[i]+point,Digits());
                 }
              }
            break;
           }
        }
      case ENTRY_SELL1:
         sl=ReversalBar.high+point;
         break;
      case ENTRY_BUY2:
         if(HedgePositionGetInteger(HEDGE_POSITION_USING_SL))
           {
            sl=NormalizeDouble(HedgePositionGetDouble(HEDGE_POSITION_SL),Digits());
            CopyBuffer(hFractals,LOWER_LINE,ReversalBar.time,TimeCurrent(),fractals);
            for(int i=ArraySize(fractals)-4; i>=0; i--)
              {
               if(DoubleEquals(fractals[i],DBL_MAX))continue;
               if(DoubleEquals(fractals[i],sl))continue;
               if(fractals[i]>sl)
                 {
                  double price= SymbolInfoDouble(Symbol(),SYMBOL_BID);
                  int ifreeze =(int)SymbolInfoInteger(Symbol(),SYMBOL_TRADE_FREEZE_LEVEL);
                  double freeze=SymbolInfoDouble(Symbol(),SYMBOL_TRADE_TICK_SIZE)*ifreeze;
                  if(fractals[i]<price-freeze)
                     sl=NormalizeDouble(fractals[i]-point,Digits());
                 }
              }
            break;
           }
      case ENTRY_BUY1:
         sl=ReversalBar.low-point;
     }
   sl=NormalizeDouble(sl,Digits());
   return sl;
//---
  }
//+------------------------------------------------------------------+
//| 返回选择仓位的止盈位。                                              |
//| 结果                                                              |
//|   止盈位                                                          |
//+------------------------------------------------------------------+
double GetTakeProfitLevel()
  {
//---
   double point=SymbolInfoDouble(Symbol(),SYMBOL_TRADE_TICK_SIZE)*3;
   ENUM_ENTRY_TYPE type=IdentifySelectPosition();
   double tp=0.0;
   if(type==ENTRY_BUY1 || type==ENTRY_SELL1)
     {
      if(!HedgePositionGetInteger(HEDGE_POSITION_USING_SL))
         return tp;
      double sl=HedgePositionGetDouble(HEDGE_POSITION_SL);
      double openPrice=HedgePositionGetDouble(HEDGE_POSITION_PRICE_OPEN);
      double deltaStopLoss=MathAbs(NormalizeDouble(openPrice-sl,Digits()));
      if(type==ENTRY_BUY1)
         tp=openPrice+deltaStopLoss;
      if(type==ENTRY_SELL1)
         tp=openPrice-deltaStopLoss;
      return tp;
     }
   else
      return 0.0;
//---
  }
//+------------------------------------------------------------------+
//| 识别选择的仓位类型。                                                |
//| 结果                                                              |
//|   返回仓位类型。参阅 ENUM_ENTRY_TYPE                                |
//+------------------------------------------------------------------+
ENUM_ENTRY_TYPE IdentifySelectPosition()
  {
//---   
   string comment=HedgePositionGetString(HEDGE_POSITION_ENTRY_COMMENT);
   int pos=StringLen(comment)-2;
   string subStr=StringSubstr(comment,pos);
   ENUM_TRANS_DIRECTION posDir=(ENUM_TRANS_DIRECTION)HedgePositionGetInteger(HEDGE_POSITION_DIRECTION);
   if(subStr=="#0")
     {
      if(posDir==TRANS_LONG)
         return ENTRY_BUY1;
      if(posDir==TRANS_SHORT)
         return ENTRY_SELL1;
     }
   else if(subStr=="#1")
     {
      if(posDir==TRANS_LONG)
         return ENTRY_BUY2;
      if(posDir==TRANS_SHORT)
         return ENTRY_SELL2;
     }
   return ENTRY_BAD_COMMENT;
//---
  }
//+------------------------------------------------------------------+
//| 依据 index_bar 所在柱线设置或高或低的挂单                            |
//| 输入参数                                                          |
//|   index_bar - 柱线索引。                                          |
//|   barType - 柱线类型。参阅枚举 ENUM_BAR_TYPE。                      |
//| 结果                                                              |
//|   True 如果新订单成功设置, 否则 false。                              | 
//+------------------------------------------------------------------+
bool SetNewPendingOrder(int index_bar,ENUM_BAR_TYPE barType)
  {
//---
   MqlRates rates[1];
   CopyRates(Symbol(),NULL,index_bar,1,rates);
   MqlTradeRequest request={0};
   request.volume=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN);
   double vol=request.volume;
   request.symbol = Symbol();
   request.action = TRADE_ACTION_PENDING;
   request.type_filling=ORDER_FILLING_FOK;
   request.type_time=ORDER_TIME_GTC;
   request.magic=Magic;
   double point=SymbolInfoDouble(Symbol(),SYMBOL_TRADE_TICK_SIZE)*3;
   string comment="";
   if(barType==BAR_TYPE_BEARISH)
     {
      request.price=rates[0].low-point;
      comment="空头柱线空单入场";
      request.type=ORDER_TYPE_SELL_STOP;
     }
   else if(barType==BAR_TYPE_BULLISH)
     {
      request.price=rates[0].high+point;
      comment="多头柱线多单入场";
      request.type=ORDER_TYPE_BUY_STOP;
     }
   MqlTradeResult result={0};
//--- 发送挂单两次...
   for(int i=0; i<2; i++)
     {
      request.comment=comment+" #"+(string)i;       // 按照注释删除订单;
      if(!OrderSend(request,result))
        {
         printf("交易错误 #"+(string)result.retcode+" "+
                result.comment);
         return false;
        }
     }
   return true;
//---
  }
//+------------------------------------------------------------------+
//| 删除旧挂单。如果挂单设置旧于                                         |
//| n_bars 之前, 挂单将被删除。                                         |
//| 输入参数                                                           |
//|   period - 柱线计数。                                              |
//+------------------------------------------------------------------+
void DeleteOldPendingOrders(int n_bars)
  {
//---
   for(int i=0; i<OrdersTotal(); i++)
     {
      ulong ticket = OrderGetTicket(i);            // 按照索引获取订单号。
      if(!OrderSelect(ticket))                     // 如果未选择继续。
         continue;
      if(Magic!=OrderGetInteger(ORDER_MAGIC))      // 如果魔幻数字不是主体。
         continue;
      if(OrderGetString(ORDER_SYMBOL)!=Symbol())   // 如果品名不是主体继续。
         continue;
      //--- 过时计数 ---//
      datetime timeSetup=(datetime)OrderGetInteger(ORDER_TIME_SETUP);
      int secElapsed=(int)(TimeCurrent()-timeSetup);
      //--- 删除旧挂单 ---//
      if(secElapsed>=PeriodSeconds() *n_bars)
        {
         MqlTradeRequest request={0};
         MqlTradeResult result={0};
         request.action= TRADE_ACTION_REMOVE;
         request.order = ticket;
         if(!OrderSend(request,result))
            printf("删除挂单失败。原因 #"+(string)result.retcode+" "+result.comment);
        }
     }
//---
  }
//+------------------------------------------------------------------+
//| 检测新柱线                                                         |
//+------------------------------------------------------------------+
bool NewBarDetect(void)
  {
//---
   datetime timeArray[1];
   CopyTime(Symbol(),NULL,0,1,timeArray);
   if(lastTime!=timeArray[0])
     {
      lastTime=timeArray[0];
      return true;
     }
   return false;
//---
  }
//+------------------------------------------------------------------+
//| 获取收盘价。在混沌交易策略里定义的柱线类型                             |
//| 并等于枚举 'ENUM_TYPE_BAR'.                                        |
//| 输入参数                                                           |
//|   index - 柱线系列索引。例如:                                       |
//|   '0' - 当前柱线。1 - 前一柱线。                                    |
//| 结果                                                              |
//|   ENUM_TYPE_BAR 类型。                                            | 
//+------------------------------------------------------------------+
double GetCloseRate(const MqlRates &bar)
  {
//---
   double highLowDelta = bar.high-bar.low;      // 计算柱线高度。
   double lowCloseDelta = bar.close - bar.low;  // 计算收盘价 - 最低价增量。
   double percentClose=0.0;
   if(!DoubleEquals(lowCloseDelta, 0.0))                    // 除零保护。
      percentClose = lowCloseDelta/highLowDelta*100.0;      // 计算 'lowCloseDelta' 与 'highLowDelta' 的百分比。
   return percentClose;
//---
  }
//+------------------------------------------------------------------+
//| 如果索引处柱线为极值 - 返回 true, 否则                               |
//| 返回 false。                                                      |
//| 输入参数                                                          |
//|   index - 柱线索引。                                               |
//|   period - 极值周期的柱线数量。                                     |
//|   type - 极值类型。参阅 ENUM_TYPE_EXTREMUM TYPE 枚举。              |
//| 结果                                                              |
//|   True - 如果柱线是极值, 否则 false。                               | 
//+------------------------------------------------------------------+
bool BarIsExtremum(const int index,const int period,ENUM_TYPE_EXTREMUM type)
  {
//--- 拷贝汇率 --- //
   MqlRates rates[];
   ArraySetAsSeries(rates,true);
   CopyRates(Symbol(),NULL,index,N+1,rates);
//--- 搜索极值 --- //
   for(int i=1; i<ArraySize(rates); i++)
     {
      //--- 如果您打算包含交易量分析则重置注释。---//
      //if(rates[0].tick_volume<rates[i].tick_volume)
      //   return false;
      if(type==TYPE_EXTREMUM_HIGHEST && 
         rates[0].high<rates[i].high)
         return false;
      if(type==TYPE_EXTREMUM_LOWEST && 
         rates[0].low>rates[i].low)
         return false;
     }
   return true;
//---
  }
//+------------------------------------------------------------------+
//| 打印当前错误和结果。                                                |
//+------------------------------------------------------------------+  
void PrintTaskLog()
  {
//---
   uint totals=(uint)HedgePositionGetInteger(HEDGE_POSITION_ACTIONS_TOTAL);
   for(uint i = 0; i<totals; i++)
     {
      uint retcode=0;
      ENUM_TARGET_TYPE type;
      GetActionResult(i,type,retcode);
      printf("---> 动作 #"+(string)i+"; "+EnumToString(type)+"; 返回码: "+(string)retcode);
     }
//---
  }
//+------------------------------------------------------------------+
//| 加载反转柱线。当前仓位必须已选择。                                    |
//| 输出参数                                                          |
//|   bar - MqlRates 柱线。                                           |
//+------------------------------------------------------------------+  
bool LoadReversalBar(MqlRates &bar)
  {
//---
   datetime time=(datetime)(HedgePositionGetInteger(HEDGE_POSITION_ENTRY_TIME_SETUP_MSC)/1000+1);
   MqlRates rates[];
   ArraySetAsSeries(rates,true);
   CopyRates(Symbol(),NULL,time,2,rates);
   int size=ArraySize(rates);
   if(size==0)return false;
   bar=rates[size-1];
   return true;
//---   
  }
//+------------------------------------------------------------------+
//| 比较两个双精度数字。                                                |
//| 结果                                                              |
//|   True 如果两个双精度数字相等, 否则 false。                          |
//+------------------------------------------------------------------+
bool DoubleEquals(const double a,const double b)
  {
//---
   return(fabs(a-b)<=16*DBL_EPSILON*fmax(fabs(a),fabs(b)));
//---
  }

以下是代码如何工作的简要说明。EA 在每次即使报价来临时被调用。它使用 BarIsExtremum() 函数分析前一根柱线: 如果它是空头或多头, 它防置两笔挂单 (函数 SetNewPendingOrder())。一旦激活, 挂单转换为仓位。之后 EA 为仓位设置止损和止盈。

不幸地是, 这些价位不能与挂单一同放置, 因为它们还不是真实的仓位。价位可以通过 SupportPositions() 函数来设置。为了正确操作, 我们需要知道仓位应该设置止盈, 且应该根据分形尾随移动。仓位定义已由 IdentifySelectPosition() 函数完成。它分析初始仓位的注释, 如果它包含字符串 "#1", 则设置一个较近的目标; 如果它包含 "# 2", 则应用尾随。

为了修改一笔双向仓位, 或是将之平仓, 需要创建一个特殊交易请求, 其后发送 SendTradeRequest() 函数执行:

...
if(modify)
  {
   HedgeTradeRequest request;
   request.action=REQUEST_MODIFY_SLTP;
   request.sl = sl;
   request.tp = tp;
   if(type==ENTRY_BUY1 || type==ENTRY_SELL1)
      request.exit_comment="由止盈位离场";
   else
      request.exit_comment="由尾随止损离场";
   if(!SendTradeRequest(request))
     {
      ENUM_HEDGE_ERR error=GetHedgeError();
      string logs=error==HEDGE_ERR_TASK_FAILED ?". 打印日志..." : "";
      printf("修改止损或止盈失败。原因: "+EnumToString(error)+" "+logs);
      if(error==HEDGE_ERR_TASK_FAILED)
         PrintTaskLog();
      ResetHedgeError();
     }
   else break;
  }
...

要注意错误处理。

如果发送失败且函数返回 false, 我们需要使用 GetHedgeError() 函数获取最后的错误代码。在某些情况, 交易订单的执行根本不会启动。如果仓位没有预先选择, 则查询是不正确的, 且它不可能执行。

如果一笔订单未执行, 分析其实现日志是毫无意义的, 获取一个错误代码就足够了。

不过, 如果查询正确, 但订单出于某些原因未能执行, 错误 HEDGE_ERR_TASK_FAILED 将被返回。在此情况, 有必要搜索日志来分析订单执行记录。这可以通过特殊函数 PrintTaskLog() 来完成:

//+------------------------------------------------------------------+
//| 打印当前错误和结果。                                                |
//+------------------------------------------------------------------+  
void PrintTaskLog()
  {
//---
   uint totals=(uint)HedgePositionGetInteger(HEDGE_POSITION_ACTIONS_TOTAL);
   for(uint i = 0; i<totals; i++)
     {
      uint retcode=0;
      ENUM_TARGET_TYPE type;
      GetActionResult(i,type,retcode);
      printf("---> 动作 #"+(string)i+"; "+EnumToString(type)+"; 返回码: "+(string)retcode);
     }
//---
  }

这些消息可以识别失败原因并修复它。

现在让我们来图解实时情况下的混沌2 EA 及其仓位在 HedgeTerminal 中显示。EA 正运行在图表 M1:

图例. 5. 在 HedgeTerminal 面板中混沌2 EA 的双向仓位示意

图例. 5. 在 HedgeTerminal 面板中混沌2 EA 的双向仓位示意

如图所见, EA 的双向仓位完美并存。


1.13. 经纪商的虚拟 "重复品种"

当 MetaTrader 5 发行时, 一些经纪商开始提供所谓的重复品种。它们的报价等同于原始金融工具, 但它们带一个后缀作为规则, 如 "_m" or "_1"。引入它们是为了让交易者利用虚拟的相同品种持有双向仓位。

然而, 这些品种对于使用机器人的算法交易者来说几乎无用。这里就是为什么。假设我们需要编写 "混沌 II" EA, 不采用 HedgeTerminalAPI 程序库。代之, 我们有一些重复品种。我们如何做呢?设想在单一金融工具上进行所有卖出操作, 诸如 EURUSD, 且在另一品种上进行所有的买入操作, 例如 EURUSD_m1。

但是如果此刻该品种已由其它机器人或交易者操作, 将会发生什么呢?即使该品种一直处于空闲, 此问题对于机器人还是无解, 即在同一方向上的并发仓位。

上述截图示意了三笔仓位, 且它们可以有更多。仓位与保护性止损位不同, 这就是为什么它们不能被合并为一笔单一的净仓位。解决的方案是打开新重复品种的一笔新仓位。但这些品种还不够, 因为一个机器人需要六个重复金融工具 (每个交易方向上三个)。如果两个机器人运行在不同的金融工具上, 就需要 12 个重复品种。

没有一个经纪商会提供如此多的重复品种。但即使这样的品种有无限量, 且它们总是空闲的, 将需要一个复杂的分解算法。机器人应该遍历所有可用品种来搜索重复品种, 及其仓位。这会产生更多问题, 得不偿失。 

甚至采用重复品种会更加麻烦。这是一份用之而产生的额外问题的简表:

重复品种在经纪商那端本质上是虚拟化的。HedgeTerminal 在客户端使用虚拟化。

在两种情况下我们如此使用虚拟化。它改变了交易者义务的真实表现。利用虚拟化, 一笔仓位可以分为两笔仓位。当它在客户端发生时毫无问题, 因为客户端可以呈现他们之所想。但如果虚拟化由经纪商来完成, 监管和授权组织可能会有如何提供相关品种真实信息的问题。第二个难点在于需要气功两套 API: 一套函数和修饰符用于纯净模式, 另一套用于双向模式。

许多算法交易者已经找到他们自己的方式, 可以在单一仓位里绑定多笔交易。这些方法许多工作出色, 并有许多描述这些方法的文章。然而, 仓位虚拟化是一个比表面看上去更复杂的过程。在 HedgeTerminal 中, 仓位虚拟化的辅助算法超过 20,000 行源代码。不过, HedgeTerminal 还仅是实现了基本功能。在您的 EA 里只为双向仓位而创建类似的代码量将消耗大量资源。


第二章. HedgeTerminal API 手册

2.1. 事务选择函数

函数 TransactionsTotal()

函数返回事务列表中的可用事务总数。这是遍历可用事务的基本函数 (参阅本文 1.41.11 节内的例子)。

int TransactionsTotal(ENUM_MODE_TRADES pool = MODE_TRADES);

参数

返回值

函数返回事务列表中的可用事务总数。


函数 TransactionType()

函数返回所选择的事务类型。

ENUM_TRANS_TYPE TransactionType(void);

返回值

返回值。值可为 ENUM_TRANS_TYPE 值之一。

使用例程

参阅本文 1.11 节内的例子: "通过脚本例程管理双向仓位属性"。


函数 TransactionSelect()

函数选择一笔事务用于进一步操纵。函数在事务列表里按照索引或独有的标识符选择一笔事务。

bool TransactionSelect(int index,
     ENUM_MODE_SELECT select = SELECT_BY_POS,
     ENUM_MODE_TRADES pool=MODE_TRADES
     );

参数

返回值

返回 true 如果一笔事务已被成功选择, 否则 false。为了获取错误详情, 调用 GetHedgeError()

使用例程

参阅本文 1.11 节内的例子: "通过脚本例程管理双向仓位属性"。

注释

如果基于其索引的一笔事务已被选择, 操作复杂性对应于 O(1)。如果基于其独有标识符的一笔事务已被选择, 操作复杂性逐渐趋向 O(log2(n))


函数 HedgeOrderSelect()

函数选择包含于双向仓位内的订单之一。双向仓位, 其包含请求订单, 必须预先使用 TransactionSelect () 选择。

bool HedgeOrderSelect(ENUM_HEDGE_ORDER_SELECTED_TYPE type);

参数

返回值

返回 true 如果一笔订单已被成功选择, 否则 false。为了获取错误详情, 调用 GetHedgeError()

使用例程

参阅本文 1.11 节内的例子: "通过脚本例程管理双向仓位属性"。


函数 HedgeDealSelect()

函数选择已执行订单的成交之一。所选成交部分的订单必须使用 HedgeOrderSelect() 函数预选。

bool HedgeDealSelect(int index);

参数

返回值

返回 true 如果一笔成交已被成功选择, 否则 false。为了获取错误详情, 调用 GetHedgeError()

使用例程

参阅本文 1.11 节内的例子: "通过脚本例程管理双向仓位属性"。


2.2. 用于获取已选择事务属性的函数

函数 HedgePositionGetInteger()

函数返回所选择的双向仓位的属性。属性可以是类型 int, long, datetimebool, 依据请求属性的类型。双向仓位必须使用 TransactionSelect() 函数预选。

ulong HedgePositionGetInteger(ENUM_HEDGE_POSITION_PROP_INTEGER property);

参数

返回值

值为 ulong 类型。为进一步使用该值, 它的类型应 显式转换 为所请求的属性类型。

使用例程

参阅本文 1.11 节内的例子: "通过脚本例程管理双向仓位属性"。


函数 HedgePositionGetDouble()

函数返回所选择的双向仓位的属性。返回属性类型应是 double。属性类型已通过 ENUM_HEDGE_POSITION_PROP_DOUBLE 枚举指定。双向仓位必须使用 TransactionSelect()函数预选。

ulong HedgePositionGetDouble(ENUM_HEDGE_POSITION_PROP_DOUBLE property);

参数

返回值

数值类型 double

使用例程

参阅本文 1.11 节内的例子: "通过脚本例程管理双向仓位属性"。


函数 HedgePositionGetString()

函数返回所选择的双向仓位的属性。属性是 string 类型。属性类型已通过 ENUM_HEDGE_POSITION_PROP_STRING 枚举指定。双向仓位必须使用 TransactionSelect()函数预选。

ulong HedgePositionGetString(ENUM_HEDGE_POSITION_PROP_STRING property);

参数

返回值

值为 string 类型。

使用例程

参阅本文 1.11 节内的例子: "通过脚本例程管理双向仓位属性"。


函数 HedgeOrderGetInteger()

函数返回所选择订单的属性, 其是双向仓位的一部分。属性可以是类型 int, long, datetimebool。属性类型已通过 ENUM_HEDGE_ORDER_PROP_INTEGER 枚举指定。订单必须使用 HedgeOrderSelect() 函数预选。

ulong HedgeOrderGetInteger(ENUM_HEDGE_ORDER_PROP_INTEGER property);

参数

返回值

值为 ulong 类型。为进一步使用该值, 它的类型应 显式转换 为所请求的属性类型。

使用例程

参阅本文 1.11 节内的例子: "通过脚本例程管理双向仓位属性"。


函数 HedgeOrderGetDouble()

函数返回所选择订单的属性, 其是双向仓位的一部分。请求属性是 double 类型。属性类型已通过 ENUM_HEDGE_ORDER_PROP_DOUBLE 枚举指定。订单必须使用 HedgeOrderSelect() 函数预选。

double HedgeOrderGetDouble(ENUM_HEDGE_ORDER_PROP_DOUBLE property);

参数

返回值

数值类型 double

使用例程

参阅本文 1.11 节内的例子: "通过脚本例程管理双向仓位属性"。


函数 HedgeDealGetInteger()

函数返回所选择成交的属性, 其是已执行订单的一部分。属性可以是类型 int, long, datetimebool。属性类型已通过 ENUM_HEDGE_DEAL_PROP_INTEGER 枚举指定。成交必须使用 HedgeDealSelect() 函数预选。

ulong HedgeOrderGetInteger(ENUM_HEDGE_DEAL_PROP_INTEGER property);

参数

返回值

值为 ulong 类型。为进一步使用该值, 它的类型应显式转换为所请求的属性类型。

使用例程

参阅本文 1.11 节内的例子: "通过脚本例程管理双向仓位属性"。


函数 HedgeDealGetDouble()

函数返回所选择成交的属性, 其是已执行订单的一部分。属性可以是类型 double。属性类型已通过 ENUM_HEDGE_DEAL_PROP_DOUBLE 枚举指定。成交必须使用 HedgeDealSelect() 函数预选。

ulong HedgeOrderGetDouble(ENUM_HEDGE_DEAL_PROP_DOUBLE property);

参数

返回值

数值类型 double

使用例程

参阅本文 1.11 节内的例子: "通过脚本例程管理双向仓位属性"。


2.3. 从 EA 里设置和获取 HedgeTerminal 属性的函数

函数 HedgePropertySetInteger()

函数设置 HedgeTerminal 属性之一。属性可以是类型 int, long, datetimebool。属性类型已通过 ENUM_HEDGE_PROP_INTEGER 枚举指定。

bool HedgePropertySetInteger(ENUM_HEDGE_PROP_INTEGER property, long value);

参数

返回值

数值类型 bool。如果属性成功设置, 函数返回 true, 否则返回 false。

使用例程

在例程中, 当发送一条异步请求, 函数用于设置仓位锁定时间。如果在您发送一个异步请求之后 30 秒内未能收到服务器响应, 阻塞仓位将被解锁。

void SetTimeOut()
  {
   bool res=HedgePropertySetInteger(HEDGE_PROP_TIMEOUT,30);
   if(res)
      printf("属性设置成功");
   else
      printf("属性未设置");
  }

函数 HedgePropertyGetInteger()

函数获取 HedgeTerminal 属性之一。属性可以是类型 int, long, datetimebool。属性类型已通过 ENUM_HEDGE_PROP_INTEGER 枚举指定。

long HedgePropertyGetInteger(ENUM_HEDGE_PROP_INTEGER property);

参数

返回值

数值类型 long

使用例程

函数在发送一个异步请求之后接收仓位阻塞时间, 并将其显示在终端上。

void GetTimeOut()
  {
   int seconds=HedgePropertyGetInteger(HEDGE_PROP_TIMEOUT);
   printf("超时为 "+(string) 秒);
  }

2.4. 获取并处理错误代码的函数

函数 GetHedgeError()

函数返回错误标识符, 所获来自最后的动作。错误标识符相应于 ENUM_HEDGE_ERR 枚举。

ENUM_HEDGE_ERR GetHedgeError(void);

返回值

Position ID. 值可为 ENUM_HEDGE_ERR 枚举值之一。

注释

调用之后, GetHedgeError() 函数不能重置错误 ID。为重置错误 ID, 使用 ResetHedgeError() 函数。

使用例程

参阅本文 1.11 节内的例子: "通过脚本例程管理双向仓位属性"。


函数 ResetHedgeError()

函数重置最后接收到的错误标识符。调用之后, 由 GetHedgeError() 返回 ENUM_HEDGE_ERR 标识符将等同于 HEDGE_ERR_NOT_ERROR

void ResetHedgeError(void);

使用例程

参阅本文 1.11 节内的例子: "通过脚本例程管理双向仓位属性"。


函数 TotalActionsTask()

一旦使用 HedgePositionSelect() 函数选择了仓位, 它可由 SendTradeRequest() 函数修改。例如, 它可被平仓, 或其离场注释可被修改。该修改由特殊的 交易任务 执行。每个任务可由若干交易动作组成 (子任务)。一个任务可能失败。在此情况下您也许需要分析某个任务内包含的所有子任务的结果来看看子任务因何失败。

函数 TotalActionTask() 返回所选仓位最后被执行交易任务内包含的子任务总数。知道了子任务的总数, 您可以按它们的索引遍历所有子任务, 并使用 GetActionResult() 函数分析它们的执行结果, 从而找出故障情况。

uint TotalActionsTask(void);

返回值

返回任务内的子任务总数。

使用例程

参阅本文 1.6 节内的例子: "使用 TotalActionsTask() 和 GetActionResult() 详细分析交易并识别错误"。


函数 GetActionResult()

函数取得任务内的子任务索引 (参阅 TotalActionTask())。通过引用参数, 返回子任务类型及其执行结果。子任务类型已由 ENUM_TARGET_TYPE 枚举定义。子任务执行结果对应于 MetaTrader 5 交易服务器的返回代码

void GetActionResult(uint index, ENUM_TARGET_TYPE& target_type, uint& retcode);

参数

使用例程

参阅本文 1.6 节内的例子: "使用 TotalActionsTask() 和 GetActionResult() 详细分析交易并识别错误"。


2.5. 交易

函数 SendTradeRequest()

函数发送一个请求来修改 HedgeTerminalAPI 中的已选择双向仓位。函数执行结果是三个动作之一:

  1. 完整或部分平仓;
  2. 修改止损或止盈位;
  3. 修改离场注释。

动作类型和其参数在 HedgeTradeRequest 结构里指定, 即传递引用作为参数。在调用函数之前, 双向仓位必须使用 TransactionSelect() 函数预选。

bool SendTradeRequest(HedgeTradeRequest& request);

参数

[in] request – 修改双向仓位的请求结构。请参阅 HedgeTradeRequest 结构的描述, 其中有结构描述和其字段的解释。

返回值

返回 true, 如果仓位修改请求已经成功执行。否则返回 false。在请求执行失败的情况下, 使用函数 TotalActionsTask()GetActionResult() 来查找失败原因。

注释

以异步模式发送请求, 如果任务已被成功放置或启动, 返回标志包含 true。然而, 我们必须记住即使成功启动了一个任务, 它的执行也是无法保证的。所以这个标志不能在异步模式下用于控制任务完成。在同步模式下, 一个任务在单线程里启动并执行, 所以在同步模式下, 您可以使用该标志控制交易请求执行结果。


交易请求结构 HedgeTradeRequest()

在 HedgeTerminal 之内的双向仓位通过调用 SendTradeRequest() 函数来平仓或修改, 其中交易请求作为一个参数。请求由特殊的预定义结构 HedgeTradeRequest 来表示, 其包括所有平仓或修改所选仓位的必要字段:

struct HedgeTradeRequest
  {
   ENUM_REQUEST_TYPE action;             // 动作类型
   double            volume;             // 持仓量
   ENUM_CLOSE_TYPE   close_type;         // 平仓标记
   double            sl;                 // 止损位
   double            tp;                 // 止盈位
   string            exit_comment;       // 离场注释
   uint              retcode;            // 最后执行操作的返回代码
   bool              asynch_mode;        // true 如果需异步执行, 否则 false
   ulong             deviation;          // 偏离的价格步长
                     HedgeTradeRequest() // 省缺参数
     {
      action=REQUEST_CLOSE_POSITION;
      asynch_mode=false;
      volume=0.0;
      sl = 0.0;
      tp = 0.0;
      retcode=0;
      deviation=3;
     }
  };

字段描述

字段描述
 action 操作仓位所需动作的类型。值可以是任何 ENUM_REQUEST_TYPE 枚举值之一。
 volume 平仓量。可小于当前活跃持仓量。如果交易量为零, 仓位将完全平仓。
 sl 持仓的止损位。
 tp 持仓的止盈位。
 exit_comment  持仓的离场注释。
 retcode 操作执行的返回代码。
 asynch_mode True 如果使用异步模式发送请求, 否则 false。
 deviation 最大价格偏离。


2.6. 与事务选择函数进行操作的枚举

ENUM_TRANS_TYPE

所有事务可用于分析, 包括在活跃以及历史事务列表里的挂单和双向仓位。

枚举 ENUM_TRANS_TYPE 包括选择的事务类型。此枚举由 TransactionType() 函数返回。以下是枚举字段及其描述:

字段描述
 TRANS_NOT_DEFINED 事务未经 TransactionSelect() 函数选择, 或其类型未定义。
 TRANS_HEDGE_POSITION 事务是一笔双向仓位。
 TRANS_BROKERAGE_DEAL  事务是一笔经纪商成交 (账户操作)。例如, 入金或账户调整。
 TRANS_PENDING_ORDER 事务是一笔挂单。
 TRANS_SWAP_POS 事务是一笔净持仓掉期利息。


ENUM_MODE_SELECT

TransactionSelect() 函数里的参数设置 index 的枚举类型定义。

字段描述
 SELECT_BY_POS 在 index 参数里传递的是列表内的事务索引。
 SELECT_BY_TICKET index 参数里传递的是单号。


ENUM_MODE_TRADES

此枚举定义数据源, 来自使用 TransactionSelect() 所选择的事务。

字段描述
 MODE_TRADES 被选择事务来自活跃事务。
 MODE_HISTORY 被选择事务来自历史事务。

2.7. 与获取事务属性函数进行操作的枚举

Enumeration ENUM_TRANS_DIRECTION

每笔事务, 无论是否为成交或双向仓位, 都有市场方向。

此市场方向由 ENUM_TRANS_DIRECTION 枚举定义。以下是其字段和它们的描述:

字段描述
 TRANS_NDEF 事务方向未定义。例如, 经纪商操作账户的事务就没有市场方向, 带此修饰符。
 TRANS_LONG 指示该事务 (订单货双向仓位) 是一笔买入事务。
 TRANS_SHORT  指示该事务 (订单货双向仓位) 是一笔卖出事务。


Enumeration ENUM_HEDGE_POSITION_STATUS

枚举包含双向仓位的状态。

字段描述
 HEDGE_POSITION_ACTIVE  活跃仓位。活跃仓位出现在 HedgeTerminal 面板的活跃栏。
 HEDGE_POSITION_HISTORY  历史仓位。历史仓位出现在 HedgeTerminal 面板的历史栏。


Enumeration ENUM_HEDGE_POSITION_STATE

枚举包含双向仓位的状态。

字段描述
 POSITION_STATE_ACTIVE 所选仓位是活跃的, 且可使用 HedgeTradeRequest 进行修改。
 POSITION_STATE_FROZEN  所选仓位是锁定的, 且不可进行修改。如果收到此修饰符, 则应等待直到仓位解锁。


Enumeration ENUM_HEDGE_POSITION_PROP_INTEGER

HedgePositionGetInteger() 返回的属性类型枚举。

字段描述
 HEDGE_POSITION_ENTRY_TIME_SETUP_MSC 自 1970 年 1 月 1 日以来的毫秒数, 当双向仓位的初始订单被放置时设定。
 HEDGE_POSITION_ENTRY_TIME_EXECUTED_MSC  自 1970 年 1 月 1 日以来的毫秒数, 当双向仓位的初始订单被执行时设定 (开仓时间)。
 HEDGE_POSITION_EXIT_TIME_SETUP_MSC 自 1970 年 1 月 1 日以来的毫秒数, 当双向仓位的订单被平仓时设置。
 HEDGE_POSITION_EXIT_TIME_EXECUTED_MSC 自 1970 年 1 月 1 日以来的毫秒数, 当双向仓位的平仓订单被执行时设定 (平仓时间)。
 HEDGE_POSITION_TYPE 双向仓位的类型。等于初始订单类型。包括系统枚举 ENUM_ORDER_TYPE 的值之一。
 HEDGE_POSITION_DIRECTION 仓位方向。由枚举 ENUM_TRANS_DIRECTION 定义。
 HEDGE_POSITION_MAGIC 选择仓位所属的 EA 魔幻数字。零值表示该仓位是手工开仓的。
 HEDGE_POSITION_CLOSE_TYPE 平仓订单的标记。由 ENUM_CLOSE_TYPE 定义。
 HEDGE_POSITION_ID Position ID. 等于初始订单标识符。
 HEDGE_POSITION_ENTRY_ORDER_ID 初始订单标识符。
 HEDGE_POSITION_EXIT_ORDER_ID 历史仓位的平仓订单标识符。
 HEDGE_POSITION_STATUS 仓位状态。由 ENUM_HEDGE_POSITION_STATUS 定义。
 HEDGE_POSITION_STATE 仓位状态。由 ENUM_HEDGE_POSITION_STATE 定义。 
 HEDGE_POSITION_USING_SL 使用止损的标志。如果使用止损, 则 HedgePositionGetInteger() 函数返回 true, 否则 false。
 HEDGE_POSITION_USING_TP 使用止盈的标志。如果使用止盈, HedgePositionGetInteger() 返回 true, 否则它返回 false。
 HEDGE_POSITION_TASK_STATUS 所选仓位执行的任务状态。仓位可以处于修改。修饰符用于跟踪此仓位的变化。仓位状态由 ENUM_TASK_STATUS 定义。
 HEDGE_POSITION_ACTIONS_TOTAL 返回修改仓位启动的所有子任务总数。

 

Enumeration ENUM_HEDGE_POSITION_PROP_DOUBLE

HedgePositionGetDouble() 函数返回的属性类型枚举。

字段描述
 HEDGE_POSITION_VOLUME 双向仓位的交易量。
 HEDGE_POSITION_PRICE_OPEN 仓位的加权平均开仓价。
 HEDGE_POSITION_PRICE_CLOSED 仓位的加权平均平仓价。
 HEDGE_POSITION_PRICE_CURRENT 一笔活跃仓位的当前价。对于历史仓位, 此修饰符返回仓位平仓价。
 HEDGE_POSITION_SL 止损位。零表示未使用止损。
 HEDGE_POSITION_TP 止盈位。零表示未使用止盈。
 HEDGE_POSITION_COMMISSION 仓位支付的佣金。
 HEDGE_POSITION_SLIPPAGE 滑点。
 HEDGE_POSITION_PROFIT_CURRENCY  仓位盈亏。此数值以入金货币为单位。
 HEDGE_POSITION_PROFIT_POINTS 仓位盈亏。以该仓位相应的金融品种指定的点数为单位。

注释

滑点 HEDGE_POSITION_SLIPPAGE 的计算就是最佳入场价和平均加权入场价的差值。

 

Enumeration ENUM_HEDGE_POSITION_PROP_STRING

HedgePositionGetString() 函数返回的属性类型枚举。

字段描述
 HEDGE_POSITION_SYMBOL 当前仓位的品种。
 HEDGE_POSITION_ENTRY_COMMENT 仓位的入场注释。
 HEDGE_POSITION_EXIT_COMMENT 仓位的离场注释。

 

Enumeration ENUM_HEDGE_ORDER_STATUS

枚举包括订单类型。

字段描述
 HEDGE_ORDER_PENDING  订单为挂单, 且在 MetaTrader 5 的交易栏可见。
 HEDGE_ORDER_HISTORY 订单为历史单, 且在 MetaTrader 5 的历史栏可见。

Enumeration ENUM_HEDGE_ORDER_SELECTED_TYPE

HedgeOrderSelect() 返回的所选订单类型枚举。

字段数值
 ORDER_SELECTED_INIT 订单初始化一笔双向仓位。
 ORDER_SELECTED_CLOSED  订单将一笔双向仓位平仓。
 ORDER_SELECTED_SL 订单作为止损位。


Enumeration ENUM_HEDGE_ORDER_PROP_INTEGER

HedgePositionGetInteger() 函数返回的属性类型枚举。

字段描述
 HEDGE_ORDER_ID 独有的订单标识符。
 HEDGE_ORDER_STATUS 订单状态。它可以是 ENUM_HEDGE_ORDER_STATUS 枚举值之一。
 HEDGE_ORDER_DEALS_TOTAL 已填充订单的成交总数。零用于挂单。
 HEDGE_ORDER_TIME_SETUP_MSC 挂单放置时间, 自 1970 年 1 月 1 日以来的毫秒数。
 HEDGE_ORDER_TIME_EXECUTED_MSC 一笔已执行订单的执行时间, 自 1970 年 1 月 1 日以来的毫秒数。
 HEDGE_ORDER_TIME_CANCELED_MSC 一笔已执行订单的取消时间, 自 1970 年 1 月 1 日以来的毫秒数。

注释

订单执行时间 HEDGE_ORDER_TIME_EXECUTED_MSC 等于它的最后成交时间。 


Enumeration ENUM_HEDGE_ORDER_PROP_DOUBLE

HedgeOrderGetDouble() 函数返回的属性类型枚举。

字段描述
 HEDGE_ORDER_VOLUME_SETUP 指定订单的交易量。
 HEDGE_ORDER_VOLUME_EXECUTED 订单的执行交易量。如果是挂单, 执行交易量为零。
 HEDGE_ORDER_VOLUME_REJECTED 叮当的交易量未能执行。等于初始交易量和执行后交易量的差值。
 HEDGE_ORDER_PRICE_SETUP 订单放置价格。
 HEDGE_ORDER_PRICE_EXECUTED 一笔订单的平均加权执行价。
 HEDGE_ORDER_COMMISSION 订单执行需支付给经纪商的佣金。以入金货币为单位。
 HEDGE_ORDER_SLIPPAGE 订单滑点。

注释

滑点 HEDGE_ORDER_SLIPPAGE 的计算就是最佳执行成交和订单平均加权入场价的差值。


Enumeration ENUM_HEDGE_DEAL_PROP_INTEGER

HedgeDealGetInteger() 返回的属性类型枚举。

字段描述
 HEDGE_DEAL_ID 独有的成交标识符。
 HEDGE_DEAL_TIME_EXECUTED_MSC 成交执行时间, 自 1970 年 1 月 1 日以来的毫秒数。


Enumeration ENUM_HEDGE_DEAL_PROP_DOUBLE

HedgeDealGetDouble() 返回的属性类型枚举。

字段描述
 HEDGE_DEAL_VOLUME_EXECUTED 成交量。
 HEDGE_DEAL_PRICE_EXECUTED 成交执行价格。
 HEDGE_DEAL_COMMISSION 成交执行需支付给经纪商的佣金。以入金货币为单位。

 

2.8. 设置和获取 HedgeTerminal 属性的枚举

Enumeration ENUM_HEDGE_PROP_INTEGER

您打算获取/设置的 HedgeTerminal 属性类型枚举。

字段描述
 HEDGE_PROP_TIMEOUT 超时秒数, HedgeTerminal 解锁被修改仓位之前等待交易服务器响应的时间。


2.9. 与处理错误代码函数进行操作的枚举

Enumeration ENUM_TASK_STATUS

每笔双向仓位均可修改。一笔仓位由交易任务进行修改。

每次执行的交易任务其执行状态由 ENUM_TASK_STATUS 定义。以下是其字段和它们的描述:

字段描述
 TASK_STATUS_WAITING 无当前任务, 或任务正在等待。
 TASK_STATUS_EXECUTING 交易任务当前正在执行。
 TASK_STATUS_COMPLETE 仓位的交易任务已成功完成。
 TASK_STATUS_FAILED 仓位的交易任务失败。


Enumeration ENUM_HEDGE_ERR

枚举包括的错误 ID 可由 GetHedgeError() 返回。

字段描述
 HEDGE_ERR_NOT_ERROR 无错误。
 HEDGE_ERR_TASK_FAILED 所选仓位的任务失败。
 HEDGE_ERR_TRANS_NOTFIND 事务未发现。
 HEDGE_ERR_WRONG_INDEX 不正确的索引。
 HEDGE_ERR_WRONG_VOLUME 不正确的交易量。
 HEDGE_ERR_TRANS_NOTSELECTED  事务不能用 TransactionSelect() 预选。
 HEDGE_ERR_WRONG_PARAMETER 传递的参数之一不正确。
 HEDGE_ERR_POS_FROZEN 双向仓位当前正修改, 且其不能进行新的修改。等待直到仓位释放。
 HEDGE_ERR_POS_NO_CHANGES 交易请求无变化。

 

Enumeration ENUM_TARGET_TYPE

GetActionResult() 函数返回的所选任务类型枚举定义。

字段描述
 TARGET_NDEF 子任务未定义。
 TARGET_CREATE_TASK 子任务正被创建。此类型在 HedgeTerminalAPI 的内部逻辑里使用。
 TARGET_DELETE_PENDING_ORDER 删除一笔挂单。
 TARGET_SET_PENDING_ORDER 放置一笔挂单。
 TARGET_MODIFY_PENDING_ORDER  修改挂单价格。
 TARGET_TRADE_BY_MARKET 制定交易操作。


2.10. 与处理错误代码函数进行操作的枚举

Enumeration ENUM_REQUEST_TYPE

此枚举描述 HedgeTerminal 应用于双向仓位的动作。

字段描述
 REQUEST_CLOSE_POSITION 平仓。如果 HedgeTradeRequest 结构的交易量字段所包含交易量低于当前值, 仅有部分仓位被平仓。在此情况下, 平仓部分与交易量字段数值对应。
 REQUEST_MODIFY_SLTP 设置或修改存在的止损或止盈位。
 REQUEST_MODIFY_COMMENT 修改一笔活跃仓位的离场注释。


Enumeration ENUM_CLOSE_TYPE

次枚举定义双向仓位的平仓订单的特殊标记。标记指示平仓原因。它可是如下原因之一:

字段描述
 CLOSE_AS_MARKET 指示该仓位由市价平仓。止损和止盈位未设置或触及。
 CLOSE_AS_STOP_LOSS 指示该仓位由于触及止损位而平仓。
 CLOSE_AS_TAKE_PROFIT  指示该仓位由于触及止盈位而平仓。

 

第 3 章. 异步交易基础

异步操作的主题十分复杂, 且详情需要单独文章。然而, 由于事实上 HedgeTerminal 的活动使用异步操作, 需要适当的简要介绍一下 EA 使用这类请求提交的组织原则。此外, 几乎没有关于该主题的资料。

3.1. 组织并发送同步交易订单的方案

MetaTrader 5 提供两种函数用于发送交易请求至服务器。

函数 OrderSend() 接受一个请求作为填充的 MqlTradeRequest 结构, 并执行基本结构正确性验证。如果基本验证成功, 它发送请求至服务器, 等待其结果, 之后通过 MqlTradeResult 结构返回结果以及返回标志至自定义的线程。如果基本验证失败, 函数返回一个负数。

请求不能被验证的原因也包括在 MqlTradeResult。

以下方案是一个含 OrderSend() 函数的自定义 MQL5 程序线程的执行特征:

图例. 6. 组织并发送同步交易请求的方案

图例. 6. 组织并发送同步交易请求的方案。

如方案中所见, 发送请求至服务器并在交易所执行交易操作的 MQL5 程序线程无法从系统公共线程里分离出来。

这就是为什么, 当 OrderSend() 完成后, 我们可以分析交易请求的真实结果。自定义线程以红色箭头标记。它几乎是立即执行。绝大多数时间花费在交易所进行的交易操作。由于两个线程连接, 大量时间消耗在 OrderSend() 函数的开始和结束之间。由于实际上交易操作是在单线程内执行, MQL5 程序的逻辑可以连续。


3.2. 组织并发送异步交易订单的方案

函数 OrderSendAsync() 是不同的。像是 OrderSend(), 它接受交易请求 MqlTradeRequest 并返回一个标志指明它的结果。

然而, 不想第一个例子, 它不等待服务器执行交易请求, 但是仅从交易请求结果值的基本验证模块获取返回值 (基本验证位于终端之内)。以下方案示意当使用 OrderSendAsync() 函数时自定义线程执行的过程:

图例. 7. 组织并发送异步交易请求的方案。

图例. 7. 组织并发送异步交易请求的方案。

一旦一条交易请求成功验证, 它将被并行发送到主线程和交易服务器。经网络传递交易请求, 以及在交易所执行它都会花费一些时间, 就像第一种情况。但自定义线程将几乎立即从 OrderSendAsync() 函数获取结果。

以上方案示意 OrderSendAsync() 实际形成了一个由交易服务器执行的新的并行线程, 且它的执行结果由 OnTradeTransaction()OnTrade() 函数获取。这些函数开始一个新的自定义线程。发送交易请求的结果应当在新线程里得到处理。这极大增加了 EA 逻辑的复杂度, 因为采用异步订单发送, 不可能在单一线程里组织好请求发送以及对其检查。例如, 您不能连续在 OnTick() 里连续放置请求发送和检查的代码。

让我们编写一段简单的测试 EA 来描述上述所说:

//+------------------------------------------------------------------+
//|                                                    AsynchExp.mq5 |
//|                           Copyright 2014, Vasiliy Sokolov (C-4). |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2014, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
input bool UsingAsynchMode=true;
bool sendFlag=false;
//+------------------------------------------------------------------+
//| EA 即时报价函数                                                    |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   if(sendFlag)return;
   printf("订单形成并发送到服务器...");
   MqlTradeRequest request={0};
   request.magic=12345;
   request.symbol = Symbol();
   request.volume = 0.1;
   request.type=ORDER_TYPE_BUY;
   request.comment= "异步测试";
   request.action = TRADE_ACTION_DEAL;
   request.type_filling=ORDER_FILLING_FOK;
   MqlTradeResult result;
   uint tiks= GetTickCount();
   bool res = false;
   if(UsingAsynchMode)
      res=OrderSendAsync(request,result);
   else
      res=OrderSend(request,result);
   uint delta=GetTickCount()-tiks;
   if(OrderSendAsync(request,result))
     {
      printf("订单已成功"+
             "发送到服务器。");
     }
  else
     {
     printf("订单无法发货。"+
             " 原因: "+(string)result.retcode);
     }
   printf("发送交易请求时间: "+(string)delta);
   sendFlag=true;
//---
  }

让我们确认 EA 开始工作时 UsingAsynchMode = false。

EA 开一笔 0.1 手多仓。交易请求使用 OrderSend() 函数同步执行。此处是例程日志:

2014.11.06 17:49:28.442 AsynchExp (AUDCAD,H1)   发送交易请求时间: 94
2014.11.06 17:49:28.442 AsynchExp (AUDCAD,H1)   订单已成功发送到服务器。
2014.11.06 17:49:28.345 AsynchExp (AUDCAD,H1)   订单形成并发送到服务器...

订单请求在 94 毫秒内完成。这个时间告诉我们请求通过了基本验证, 被发送到服务器, 并已填充。

现在我们修改 EA 代码改变事务交易量为最大可能值 DBL_MAX:

request.volume = DBL_MAX;

显而易见, 此值超出真实范围。让我们尝试在同步模式执行此请求:

2014.11.06 17:54:15.373 AsynchExp (AUDCAD,H1)   发送交易请求时间: 0
2014.11.06 17:54:15.373 AsynchExp (AUDCAD,H1)   订单无法发货。原因: 10014
2014.11.06 17:54:15.373 AsynchExp (AUDCAD,H1)   订单形成并发送到服务器...

发送请求失败。失败原因是 error 10014 (无效请求交易量)。请求在基本验证期间失败, 甚至没有发送到服务器, 这可从请求执行时间 0 毫秒清晰看出。

再来, 让我们修改请求。这次我们指定一个足够大的交易量, 但没有超过极限值 – 15 手。对于测试 EA 的 $1,000 帐户, 它确实太多了。如此仓位在帐户里不可能开仓。

让我们来看 OrderSend() 返回了什么:

2014.11.06 17:59:22.643 AsynchExp (AUDCAD,H1)   发送交易请求时间: 78
2014.11.06 17:59:22.643 AsynchExp (AUDCAD,H1)   订单无法发货。原因: 10019
2014.11.06 17:59:22.550 AsynchExp (AUDCAD,H1)   订单形成并发送到服务器...

这次出错时间不同: 10019 (资金不足以执行请求, 这当然了)。注意请求执行时间是现在的 79 毫秒。它表示请求已发送到了服务器, 且服务器返回一个错误。

让我们现在使用 OrderSendAsync() 函数发送相同的请求 15 手交易量。如同 OrderSend(), 没有开仓。但让我们来分析日志:

2014.11.06 18:03:58.106 AsynchExp (AUDCAD,H1)   发送交易请求时间: 0
2014.11.06 18:03:58.106 AsynchExp (AUDCAD,H1)   订单已成功发送到服务器。
2014.11.06 18:03:58.104 AsynchExp (AUDCAD,H1)   订单形成并发送到服务器...

这个日志居然告之没错!因为错误 10019 是由交易服务器检测到, 在异步订单发送模式它无法用于当前线程。返回值仅代表请求通过了基本验证。为了得到真实错误 10019, 我们需要在一个新的线程里分析结果, 在 OnTradeTransaction() 系统函数里, 应该加入我们的 EA:

void  OnTradeTransaction(const MqlTradeTransaction    &trans,
                         const MqlTradeRequest        &request,
                         const MqlTradeResult         &result)
  {
   uint delta = GetTickCount() - tiks;
   printf("服务器应答: " + (string)result.retcode + "; 时间: " + (string)delta);
  }

让我们再次运行 EA 并查看日志:

2014.11.06 18:17:00.943 AsynchExp (AUDCAD,H1)   服务器应答: 10019; 时间: 94
2014.11.06 18:17:00.854 AsynchExp (AUDCAD,H1)   发送交易请求时间: 0
2014.11.06 18:17:00.854 AsynchExp (AUDCAD,H1)   订单已成功发送到服务器。
2014.11.06 18:17:00.851 AsynchExp (AUDCAD,H1)   订单形成并发送到服务器...

错误 10019 已收到, 但并非在发送后立即得到。它是运行在 OnTradeTransaction() 内的新自定义线程里被接收到。


3.3. 异步订单执行速度

交易者错误地相信异步请求的执行速度接近于零。

这源于观察 OrderSendAsync(), 其完全规则就是要小于一毫秒。在现实中, 如上所见, 交易事务的真实执行时间应该自函数 OnTradeTransaction()OnTrade() 内部收到服务器响应时测算。这一测量结果显示实际速度, 其等于单一订单的一个同步执行速度。当发送一组事务时, 就能够感觉到执行时间上的优势。至少有三种情况, 您需要发送多个请求:

记住, 要考虑到放置多个订单时发送请求的限制。

在 MetaTrader 5 构建 1010 以及更高版本里, 该限制是 64 笔事务, 其中 4 笔为用户保留, 其它可用于 EA。该限制的目的是为了保护交易新手在其程序里产生严重错误, 以及降低交易服务器的垃圾负载。

这意味着在同一时刻, 例如在循环里, 您可以通过调用 SendOrderAsync() 发送至多 60 笔交易订单的适当请求。在所有 60 笔被发送后, 事务缓存区将会满员。我们需要等待来自服务器的逐笔事务已处理的确认。

经处理之后, 一笔事务在事务缓存区占据的位置将被释放, 且新的交易请求可以占用。一旦缓存区满员, 用于新事务的空间释放缓慢, 因为交易服务器需要时间来处理每笔事务, 且开始处理的 TradeTransaction() 事件提示要经网络来传递, 这都会导致附加延迟。

所以, 相较于请求数量的增长, 发送请求的时间需求将非线性增长。下表示意异步模式下发送订单的评估速率。测试进行了若干次, 所示时间是平均值:

请求数量时间, 毫秒
5050
100180
2002100
5009000
100023000

当请求的数量低于 60 的情况下, 该脚本不等待服务器的响应, 这就是为何时间如此之小。它约等于发送单一请求所需的时间。实际上, 为得到一个近似的实际执行时间, 平均请求执行时间加上表中指定的放置请求时间。

 

第 4 章. 在 MetaTrader 5 集成开发环境中的多线程编程基础

MQL5 程序员知道线程不能直接通过 MQL 程序控制。这个限制对于程序员新手来说很好, 因为使用线程程序算法会极大复杂化。然而, 在一些情形下, 两个或更多的 EA 必须彼此通信, 例如, 它们创建并读取全局数据。

HedgeTerminal 就是这样的一个 EA。为了通知每个使用 HedgeTerminalAPI 程序库的 EA 关于其它 EA 的动作, HT 通过多线程读写 ActivePositions.xml 文件来组织数据交换。这个解决方案非比寻常, 很少有 MQL 的程序员这样使用。所以, 我们创建一个类似 HedgeTerminal 算法的多线程 EA。这有助于更好地理解多线程编程, 当然也可以更好地理解 HedgeTerminal 是如何工作地。


4.1. 以报价收集器 UnitedExchangeQuotes 为例的多线程编程

我们将通过特殊例子来了解多线程编程的基础: 我们将编写一个接收不同提供者 (经济上) 报价的收集器。

此思路是: 假设我们有 6-7 家经济商提供相同金融工具的报价。自然而然, 来自不同经纪商的报价略有不同。分析这些开盘价的差别就是套利策略的方式。此外, 比较报价动态将有助于识别最佳和最垃圾提供者。例如, 如果一名经纪商提供最佳价格, 我们更愿意选择这家经纪商进行交易。我们并非猎取这些结果的实用价值, 代之我们仅描述这些结果可被实现的机制。

此处是一幅 EA 的截图, 我们将在本章末尾来编写它:

图例. 8. 报价收集器 UnitedExhangesQuotes 的外观。

图例. 8. 报价收集器 UnitedExhangesQuotes 的外观。

EA 在一个由四列且无行数限制组成的表格里显示结果。

每行表示一个经纪商提供的品种报价 (在此例中, EURUSD)。Ask 和 Bid 是经纪商的最佳出价以及需求询价。截图所示价格略有不同。当前经纪商与其它家之间的出价差别出现在 D-ASK (增量 Ask) 列。类似地, 需求价之间的差别显示在 D-BID (增量 Bid)。例如, 在摄取截图的同一时刻, 最佳 Ask 由 "Alpari Limited" 提供, 而最昂贵的是 "Bank VTB 24"。

MQL 程序不能访问其它的 MetaTrader 终端环境。换言之, 如果一段程序运行在某一终端上, 它不能从其它那里接收数据。然而, 所有 MQL 程序可以通过 MetaTrader 终端的共享目录里的文件来通信。如果任何程序写入信息, 如, 当前报价至一个文件, 来自其它终端的 MQL 程序可以读取它。除非外部 DLL, 则 MQL 再无其它手段。所以, 我们将使用此方法。

最大的困难是组织此类访问。一方面, EA 从其它提供者那里读取报价, 另一方面 - 将其自身提供者的报价写到相同文件。其它的问题在于, 事实上在读取报价的同时, 其它 EA 可能正在对此文件写入新报价。这种并行工作的结果是不可预知的。最好的情况, 就是随之崩溃且程序中断, 最坏的情况将导致偶尔出现与报价显示相关的莫名其妙的错误。

要消除这些错误, 或至少将它们发生的概率降至最低, 我们将要制定一个明确的计划。

首先, 所有信息将以 XML 格式保存。这种格式取代了笨拙的 ini 文件。XML 允许灵活扩展它的节点构成复杂的数据结构, 比如类。接下来, 让我们来确定读取和写入的一般算法。这里有两个基本操作: 读取数据和写入数据。当任何 MQL 程序在读取或写入时, 没有其它程序可以访问此文件。因此, 我们排除一种情况, 当其中一个程序读取数据时, 第二个试图修改它们。出于这个原因, 对数据的访问并非总是可能的。

让我们创建一个特殊的类 CQuoteList, 它将包含访问 XML, 以及从该文件访问报价数据的所有算法。

这个类的其中一个函数是 TryGetHandle(), 它试图访问该文件, 并在成功的情况下返回它的句柄。此处是函数的实现:

int CQuoteList::TryGetHandle(void)
{
   int attempts = 10;
   int handle = INVALID_HANDLE;
   // 我们尝试打开 'attemps' 次数
   for(att = 0; att < attempts; att++)
   {
      handle = FileOpen("Quotes.xml", FILE_WRITE|FILE_READ|FILE_BIN|FILE_COMMON);
      if(handle == INVALID_HANDLE)
      {
         Sleep(15);
         continue;
      }
      break;
   }
   return handle;
}

它按照读/写复合模式尝试若干次来打开文件。省缺尝试次数是十次。

如果一次尝试不成功, 函数冻结 15 毫秒之后将再次尝试打开文件, 且至多尝试 10 次。

一旦文件被打开, 它的句柄传递至 LoadQuotes() 函数。一份完整的函数列表, 以及 CQuoteList 类能够在文章的附件里得到。所以此处我们仅描述函数的动作循序:

  1. TryGetHandle() 打开文件用于读写;
  2. 使用 XML Parser 程序库将 XML 文档加载到 EA 内存;
  3. 基于加载的 XML 文档, 形成一个新的报价数组保存信息;
  4. 创建的数组包含一个属于当前 EA 的报价。它的数值已得到更新。
  5. 报价数组变换回到 XML 文档。打开的 XML 文件内容会被这份 XML 文档覆盖;
  6. 报价的 XML 文件被关闭。

函数 LoadQuotes() 完成了一个伟大的工作, 但大多数情况下它花费的时间少于 1 毫秒。

数据读取和数据更新以及将来的保存被合并到一个模块。这样特意这样做的, 以便在读写操作之间不丢失所获的文件控制权。

一旦报价加载至类内部, 它们就可以被访问, 就像任何其它在 MetaTrader 5 里的数据一样。这是通过 CQuotesList 类中实现的一个特别的 MetaTrader 5 风格程序接口来完成的。

函数调用和数据渲染在 OnTick() 模块内部执行。这是内容:

//+------------------------------------------------------------------+
//| EA 即时报价函数                                                    |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   if(!AccountInfoInteger(ACCOUNT_TRADE_EXPERT))
      return;
   if(!QuotesList.LoadQuotes())    
      return;   
   PrintQuote quote = {0};
   Panel.DrawAccess(QuotesList.CountAccess());
   double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
   double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID);
   string brokerName = AccountInfoString(ACCOUNT_COMPANY);
   for(int i = 0; i < QuotesList.BrokersTotal(); i++)
   {
      if(!QuotesList.BrokerSelect(i))
         continue;
      if(!QuotesList.SymbolSelect(Symbol()))
         continue;
      quote.ask = QuotesList.QuoteInfoDouble(QUOTE_ASK);
      quote.bid = QuotesList.QuoteInfoDouble(QUOTE_BID);
      quote.delta_ask = ask - quote.ask;
      quote.delta_bid = quote.bid - bid;
      quote.broker_name = QuotesList.BrokerName();
      quote.index = i;
      Panel.DrawBroker(quote);
   }
  }

值得注意的是, 这段示例代码无论是在 MetaTrader 4 或在 MetaTrader 5 上均可工作, 而无须任何额外的修改!

仅需针对不同版本终端上面板显示的差别略微进行一些美容。毫无疑问, 这是一个明显的事实, 有利于平台之间的代码移植。

EA 的操作可动态地进行最佳观察。以下视频示意 EA 在不同帐户内的操作:

 

读写文件有明显优势, 但也有一些缺点。

主要优势是:

  1. 灵活。您可以保存并加载任何数据, 即便是整个类;
  2. 相对高速。读写的整个周期几乎只需不超过 1 毫秒, 相对于每次花费 80 - 150 毫秒甚至更多时间的慢速交易操作来讲, 这么短的时间已经很好;
  3. 基于 MQL5 语言的标准工具而无需调用 DLL。

而这种解决方案的主要缺点是存储系统的严重负荷。当只有一两家经纪商提供报价时, 读写操作次数相对较少, 但若有大量经纪商/品种, 提供海量报价流时, 读写操作的次数将变得十分庞大。在不过一小时的时间里, 演示 EA 产生了超过 90,000 次的 Quotes.xml 文件读写操作。这些统计显示在 EA 面板的顶部: "I/O Rewrite" 示意读写总数, "fps" 指示最后两次读写操作之间的速率, 而 "Avrg" 显示每妙读写的平均速度。

如果您将文件保存在 SSD 或 HDD, 这些操作将对磁盘寿命造成负面影响。所以最好是使用一个虚拟 RAM 磁盘来进行数据交换。

不像以上例子, HedgeTerminal 保守地使用 ActivePositions.xml, 仅将不可通过全局数据访问的明显仓位变化写入。所以它产生的读/写操作比上述例子少很多, 因此并不需要任何特殊的条件, 譬如 RAM 磁盘。


4.2. 使用多线程在 EA 之间进行交互

在独立的 MQL 程序之间实时交互是一个复杂的, 但很有趣的话题。本文只包含一点它的非常简短的描述, 但它值得一篇单独的文章。在大多数情况下, 并不需要在 EA 之间利用多线程交互。不过, 这里有份任务和各种方案的清单, 需要这种交互组织。


附件说明

这是文章附件中文件的简要说明, 以及过程汇编。

Prototypes.mqh 文件是 HedgeTerminalAPI 程序库函数的说明。文件包括 HedgeTerminalAPI 程序库的说明和函数原型。它可以让您的 EA 知道程序库里哪些函数和修饰符可用, 如何调用该函数以及它们的返回值是什么。

将文件保存到 C:\Program Files\MetaTrader 5\MQL5\Include, 此处 "C:\Program Files\MetaTrader 5\" 是您的 MetaTrader 5 终端的安装目录。一旦文件被拷贝到正确的位置, 您即可在您的 MQL 程序里 引用 它。当您需要使用 HedgeTerminalAPI 程序库时就可如此完成。为了引用 Prototypes.mqh 文件, 在您的代码里加入指定文件包含目录:

#include <Prototypes.mqh>
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   //...
   // 此处是您的程序内容。
   //...
  }

在以上例子中, 这条指令以黄色标记, 并调用 "#include <Ptototypes.mqh>"。现在以上脚本可以引用程序库函数并使用它们的功能。

请注意, 在 HedgeTerminalAPI 程序库的发展过程中, 文件原型也许经历了稍许变化。经常是, 随着程序库的版本更新, 您也需要更新原型文件, 该文件将描述变化。若有不适敬请谅解。在所有情况下, 最后版本的原型文件可从程序库里手工安装 (安装过程已在 1.1 节里说明) 或从文章的附件里下载 (期望能定期更新附件)。

Chaos2.mqh 是混沌2 EA 的源代码。它的操作说明在 1.12 节: "混沌 II EA 例程中的 SendTradeRequest 函数和 HedgeTradeRequest 结构。为了成功编译代码, 将函数原型文件保存至相应的目录 \Include 并保存 HedgeTerminalAPI 程序库至: C:\Program Files\MetaTrader 5\MQL5\Market\hedgeterminalapi.ex5。此处 "C:\Program Files\MetaTrader 5\" 是目录名 (终端数据文件夹), 即您的 MetaTrader 5 终端安装所在。

UnitedExchangeQuotes 源代码 是特别的 zip 文档 (unitedexchangequotes.zip) 它包括第 4 章内的项目说明: "在 METATRADER 5 集成开发环境里的多线程编程基础"。此 zip 包括以下文件:

文档内的所有文件包含相对路径。例如, 文件 UnitedExchangeQuotes.mq5 位于文件夹 \MQL5\Experts。这意味着, 它应放置在 MetaTrader 5 终端数据文件夹的相同子目录, 譬如 C:\Program Files\MetaTrader 5\MQL5\Experts\UnitedExchangeQuotes.mq5


结论

我们已经研究了 HedgeTerminal 程序界面的工作细节。

它已经表明, 这个程序库的原理非常类似于 MetaTrader 4 的 API。就像 MetaTrader 4 API, 在您开始一笔事务操作之前 (在 MetaTrader 4 里是一个模拟的 "订单"), 您应首先使用 TransactionSelect() 选择它。在 HedgeTerminal 里, 作为一条规则, 一笔事务就是一笔双向仓位。一旦一笔仓位被选中, 您可以获取它的属性, 或为其应用一个交易行为, 比如, 设定一个止损位或将其平仓。这个动作的顺序几乎与 MetaTrader 4 处理订单的算法相同。

除了有关双向仓位数量及其属性的基本信息, HedgeTerminal 还提供访问 MetaTrader 5 不能直接利用且需复杂分析计算的数值。例如, 您可以通过查询一个属性, 即可查看每笔双向仓位的滑点。您可以检查所选仓位内的成交数量。所有这些计算和成交所需的匹配, 都在 HedgeTerminal 启动时在 "幕后" 执行。这极其便利, 因为负责交易的 EA 并不需要计算任何东西。所有必要的信息已经被计算, 并可通过简单、直观的 API 提供使用。

通过 HedgeTerminal API 使用通用算法, 且面板可以统一数据表现。因此, 您可以从 HedgeTerminal 面板上控制 EA, 而由 EA 所做的更改将在面板上直接显示。