轻松快捷开发 MetaTrader 程序的函数库(第四部分):交易事件

28 五月 2019, 08:58
Artyom Trishkin
0
1 697

内容

在第一篇文章中,我们已着手创建一个大型跨平台函数库,简化了 MetaTrader 5 和 MetaTrader 4 程序的开发。 在后续的文章中,我们继续开发函数库,并完成了引擎(Engine)函数库的基础对象,以及在场订单和仓位的集合。 在本文中,我们将继续开发基础对象,并教导它识别帐户上的交易事件。

将交易事件传递给程序

如果我们回到第三篇文章最后创建的测试 EA,我们可以看到该函数库能够定义帐户上发生的交易事件。 不过,我们应该按照类型准确地划分所有发生的事件,以便利用函数库进行操控。
为此,我们应编写定义事件的方法,和定义事件类型的方法。

我们思考一下我们需要识别的交易事件有哪些:

  • 放置挂单,
  • 删除挂单,
  • 挂单激活从而生成持仓,
  • 挂单部分激活从而生成持仓,
  • 开仓,
  • 平仓,
  • 部分开仓,
  • 部分平仓,
  • 逆向平仓,
  • 部分逆向平仓,
  • 帐户补仓,
  • 从账户中出金,
  • 账户里的余额操作
    尚未跟踪的事件:
  • 修改挂单(更改激活价格,添加/删除/更改止损和止盈价位)
  • 修改持仓(添加/删除/更改止损和止盈价位)

基于以上所述,您需要决定如何无歧义地识别事件。 最好根据帐户类型立即划分解决方案:

对冲:

  1. 挂单数量增加表示添加一笔挂单(在场环境中的事件)
  2. 挂单数量减少:
    1. 仓位数量增加表示挂单激活(在场和历史环境中的事件)
    2. 仓位数量未增加表示删除挂单(在场环境中的事件)
  3. 挂单数量未减少:
    1. 仓位数量增加意味着开新仓(在场和历史环境中的事件)
    2. 仓位数量减少意味着平仓(在场和历史环境中的事件)
    3. 仓位保持不变但持仓量持续减少意味着部分平仓(历史环境中的事件)

净持:

  1. 挂单数量增加意味着添加一笔挂单
  2. 挂单数量减少:
    1. 仓位数量增加表示挂单激活
    2. 持仓数量保持不变,但伴随持仓时间修改和未变化的持仓量表示挂单激活,且持仓量增加
    3. 仓位数量减少表示平仓
  3. 挂单数量未减少:
    1. 仓位数量增加意味着开新仓
    2. 仓位数量减少表示平仓
    3. 持仓数量保持不变,但伴随持仓时间修改和持仓量增加意味着持仓量增加
    4. 持仓数量保持不变,但伴随持仓时间修改和持仓量减少意味着部分平仓

为了识别交易事件,我们需要掌握该程序的操作机制。 在 CEngine 类的私有部分添加对冲帐户类型的标志在类构造函数中定义帐户类型,并将结果写入此标志变量

//+------------------------------------------------------------------+
//| 函数库基类                                                          |
//+------------------------------------------------------------------+
class CEngine : public CObject
  {
private:
   CHistoryCollection   m_history;                       // 历史订单和成交集合
   CMarketCollection    m_market;                        // 在场订单和成交集合
   CArrayObj            m_list_counters;                 // 计时器列表计数器
   bool                 m_first_start;                   // 首次启动标志
   bool                 m_is_hedge;                      // 对冲账户标志
//--- 按 id 返回计数器索引
   int                  CounterIndex(const int id) const;
//--- 返回首次启动标志
   bool                 IsFirstStart(void);
public:
//--- 创建计时器计数器
   void                 CreateCounter(const int id,const ulong frequency,const ulong pause);
//--- 计时器
   void                 OnTimer(void);
                        CEngine();
                       ~CEngine();
  };
//+------------------------------------------------------------------+
//| CEngine 构造函数                                                   |
//+------------------------------------------------------------------+
CEngine::CEngine() : m_first_start(true)
  {
   ::EventSetMillisecondTimer(TIMER_FREQUENCY);
   this.m_list_counters.Sort();
   this.CreateCounter(COLLECTION_COUNTER_ID,COLLECTION_COUNTER_STEP,COLLECTION_PAUSE);
   this.m_is_hedge=bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
  }
//+------------------------------------------------------------------+

在首次启动期间,在构造类对象时,在其构造函数中定义启动程序时的帐户类型。 相应地,定义交易事件的方法立即确定对冲或净持结算账户的归属。

在定义传入的交易事件后,我们需要存储其代码。 它将保持不变直到下一个事件。 因此,程序始终能够定义帐户上的最后一个事件。 事件代码将由一组标志构成。 每个标志将描述特定事件。 例如,可将平仓事件划分为更准确地表征它的某些子集:

  1. 全部平仓
  2. 部分平仓
  3. 由逆向平仓
  4. 由止损平仓
  5. 由止盈平仓
  6. 诸如此类

所有这些属性都是一个“平仓”事件所固有的,这意味着事件代码应包含所有这些数据。 为了使用这些标志构造一个事件,我们在函数库根文件夹(交易事件标志可能的交易事件)的 Defines.mqh 文件中为我们要跟踪的帐户创建两个新的枚举:

//+------------------------------------------------------------------+
//| 帐户上的交易事件标志列表                                                 |
//+------------------------------------------------------------------+
enum ENUM_TRADE_EVENT_FLAGS
  {
   TRADE_EVENT_FLAG_NO_EVENT        =  0,                   // 无事件
   TRADE_EVENT_FLAG_ORDER_PLASED    =  1,                   // 挂单已放置
   TRADE_EVENT_FLAG_ORDER_REMOVED   =  2,                   // 挂单已删除
   TRADE_EVENT_FLAG_ORDER_ACTIVATED =  4,                   // 挂单已激活
   TRADE_EVENT_FLAG_POSITION_OPENED =  8,                   // 已开仓
   TRADE_EVENT_FLAG_POSITION_CLOSED =  16,                  // 已平仓
   TRADE_EVENT_FLAG_ACCOUNT_BALANCE =  32,                  // 余额操作 (按交易类型声明)
   TRADE_EVENT_FLAG_PARTIAL         =  64,                  // 部分执行
   TRADE_EVENT_FLAG_BY_POS          =  128,                 // 由逆向仓位执行
   TRADE_EVENT_FLAG_SL              =  256,                 // 由止损执行
   TRADE_EVENT_FLAG_TP              =  512                  // 由止盈执行
  };
//+------------------------------------------------------------------+
//| 帐户上的仓位交易事件列表                                                 |
//+------------------------------------------------------------------+
enum ENUM_TRADE_EVENT
  {
   TRADE_EVENT_NO_EVENT,                                    // 无交易事件
   TRADE_EVENT_PENDING_ORDER_PLASED,                        // 挂单已放置
   TRADE_EVENT_PENDING_ORDER_REMOVED,                       // 挂单已删除
//--- 与 ENUM_DEAL_TYPE 枚举匹配的枚举成员
   TRADE_EVENT_ACCOUNT_CREDIT,                              // 信贷充值
   TRADE_EVENT_ACCOUNT_CHARGE,                              // 充值
   TRADE_EVENT_ACCOUNT_CORRECTION,                          // 调整
   TRADE_EVENT_ACCOUNT_BONUS,                               // 奖金
   TRADE_EVENT_ACCOUNT_COMISSION,                           // 佣金
   TRADE_EVENT_ACCOUNT_COMISSION_DAILY,                     // 日结佣金
   TRADE_EVENT_ACCOUNT_COMISSION_MONTHLY,                   // 月结佣金
   TRADE_EVENT_ACCOUNT_COMISSION_AGENT_DAILY,               // 日结代理佣金
   TRADE_EVENT_ACCOUNT_COMISSION_AGENT_MONTHLY,             // 月结代理佣金
   TRADE_EVENT_ACCOUNT_INTEREST,                            // 可用资金的应计利息
   TRADE_EVENT_BUY_CANCELLED,                               // 取消的买入成交
   TRADE_EVENT_SELL_CANCELLED,                              // 取消的卖出成交
   TRADE_EVENT_DIVIDENT,                                    // 应计除权
   TRADE_EVENT_DIVIDENT_FRANKED,                            // 应计派发除权
   TRADE_EVENT_TAX,                                         // 应计税款
//--- 与 ENUM_DEAL_TYPE 枚举中的 DEAL_TYPE_BALANCE 交易类型相关的枚举成员
   TRADE_EVENT_ACCOUNT_BALANCE_REFILL,                      // 补充账户余额
   TRADE_EVENT_ACCOUNT_BALANCE_WITHDRAWAL,                  // 从账户中出金
//---
   TRADE_EVENT_PENDING_ORDER_ACTIVATED,                     // 挂单由价格激活
   TRADE_EVENT_PENDING_ORDER_ACTIVATED_PARTIAL,             // 挂单由价格部分激活
   TRADE_EVENT_POSITION_OPENED,                             // 已开仓
   TRADE_EVENT_POSITION_OPENED_PARTIAL,                     // 部分开仓
   TRADE_EVENT_POSITION_CLOSED,                             // 已平仓
   TRADE_EVENT_POSITION_CLOSED_PARTIAL,                     // 部分平仓
   TRADE_EVENT_POSITION_CLOSED_BY_POS,                      // 由逆向平仓
   TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_POS,              // 由逆向部分平仓
   TRADE_EVENT_POSITION_CLOSED_BY_SL,                       // 由止损平仓
   TRADE_EVENT_POSITION_CLOSED_BY_TP,                       // 由止盈平仓
   TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_SL,               // 由止损部分平仓
   TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_TP,               // 由止盈部分平仓
   TRADE_EVENT_POSITION_REVERSED,                           // 持仓逆转 (净持)
   TRADE_EVENT_POSITION_VOLUME_ADD                          // 加仓 (净持)
  };
//+------------------------------------------------------------------+

在此我们应针对 ENUM_TRADE_EVENT 枚举进行一些澄清。
由于某些交易事件不需要程序或人工干预(收取佣金、收杂费、奖金等),我们将从 MQL5 中的交易类型(ENUM_DEAL_TYPE 枚举)中获取该数据。 为了简化以后的事件跟踪,我们在设置我们的事件时,应与 ENUM_DEAL_TYPE 枚举的值匹配。
我们将余额操作分为两个事件:补充帐户余额出金。 除了与余额操作无关的买入卖出(DEAL_TYPE_BUY 和 DEAL_TYPE_SELL),来自 DEAL_TYPE_CREDIT 交易类型枚举中的其他事件应与 ENUM_DEAL_TYPE 枚举具有相同的值。

我们来改进在场订单和仓位集合类。
我们将在 CMarketCollection 类的私有部分添加标本订单,以便按指定的订单属性执行搜索,而类的公有部分将接收获取订单和仓位完整列表的方法,该订单和仓位列表按指定的时间范围选择,以及按订单和仓位的整数、实数和字符串属性作为指定标准选择并返回的订单和仓位列表。 这将令我们能够从集合中获得必要的在场订单和仓位清单(就像在第 2 部分第 3 部分函数库中所描述的历史订单和成交集合一样)。

//+------------------------------------------------------------------+
//| 在场订单和仓位集合                                                  |
//+------------------------------------------------------------------+
class CMarketCollection
  {
private:
   struct MqlDataCollection
     {
      long           hash_sum_acc;           // 账户上所有订单和仓位的哈希值
      int            total_pending;          // 账户上的挂单数量
      int            total_positions;        // 账户上的仓位数量
      double         total_volumes;          // 帐户上订单和仓位的总交易量
     };
   MqlDataCollection m_struct_curr_market;   // 账户内在场订单和仓位的当前数据
   MqlDataCollection m_struct_prev_market;   // 账户内在场订单和仓位的之前数据
   CArrayObj         m_list_all_orders;      // 账户上的挂单和仓位列表
   COrder            m_order_instance;       // 按属性搜索的订单对象
   bool              m_is_trade_event;       // 交易事件标志
   bool              m_is_change_volume;     // 总交易量变化标志
   double            m_change_volume_value;  // 总交易量变化值
   int               m_new_positions;        // 新仓位数量
   int               m_new_pendings;         // 新挂单的数量
   //--- 保存帐户数据状态的当前值作为前值
   void              SavePrevValues(void)                                                                { this.m_struct_prev_market=this.m_struct_curr_market;                  }
public:
   //--- 返回所有挂单和持仓的清单
   CArrayObj*        GetList(void)                                                                       { return &m_list_all_orders;                                            }
   //--- 返回开单时间在 begin_time 到 end_time 时间段内的订单和仓位列表
   CArrayObj*        GetListByTime(const datetime begin_time=0,const datetime end_time=0);
   //--- 返回按(1)实数型,(2)整数型,和(3)字符串型属性选择,并匹配比较条件的订单和仓位列表
   CArrayObj*        GetList(ENUM_ORDER_PROP_DOUBLE property,double value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByOrderProperty(this.GetList(),property,value,mode);  }
   CArrayObj*        GetList(ENUM_ORDER_PROP_INTEGER property,long value,ENUM_COMPARER_TYPE mode=EQUAL)  { return CSelect::ByOrderProperty(this.GetList(),property,value,mode);  }
   CArrayObj*        GetList(ENUM_ORDER_PROP_STRING property,string value,ENUM_COMPARER_TYPE mode=EQUAL) { return CSelect::ByOrderProperty(this.GetList(),property,value,mode);  }
   //--- 返回以下数量(1)新挂单,(2)新仓位,(3)发生交易事件标志,(4)交易量变化
   int               NewOrders(void)                                                            const    { return this.m_new_pendings;                                           }
   int               NewPosition(void)                                                          const    { return this.m_new_positions;                                          }
   bool              IsTradeEvent(void)                                                         const    { return this.m_is_trade_event;                                         }
   double            ChangedVolumeValue(void)                                                   const    { return this.m_change_volume_value;                                    }
   //--- 构造函数
                     CMarketCollection(void);
   //--- 更新挂单和仓位列表
   void              Refresh(void);
  };
//+------------------------------------------------------------------+

在类的实体之外实现按时间选择订单和仓位的方法:

//+------------------------------------------------------------------------+
//| 根据时间从集合中选择在场订单或仓位                                            |
//| 范围自 begin_time 至 end_time                                            |
//+------------------------------------------------------------------------+
CArrayObj* CMarketCollection::GetListByTime(const datetime begin_time=0,const datetime end_time=0)
  {
   CArrayObj* list=new CArrayObj();
   if(list==NULL)
     {
      ::Print(DFUN,TextByLanguage("Ошибка создания временного списка","Error creating temporary list"));
      return NULL;
     }
   datetime begin=begin_time,end=(end_time==0 ? END_TIME : end_time);
   list.FreeMode(false);
   ListStorage.Add(list);
   m_order_instance.SetProperty(ORDER_PROP_TIME_OPEN,begin);
   int index_begin=m_list_all_orders.SearchGreatOrEqual(&m_order_instance);
   if(index_begin==WRONG_VALUE)
      return list;
   m_order_instance.SetProperty(ORDER_PROP_TIME_OPEN,end);
   int index_end=m_list_all_orders.SearchLessOrEqual(&m_order_instance);
   if(index_end==WRONG_VALUE)
      return list;
   for(int i=index_begin; i<=index_end; i++)
      list.Add(m_list_all_orders.At(i));
   return list;
  }
//+------------------------------------------------------------------+

该方法几乎与我们在第 3 部分中所述的按时间选择历史订单和成交的方法相同。 如有必要,请重新阅读第三篇文章当中相应部分的说明。 此方法与历史集合类之一的区别在于已选择的列表我们无需再选择时间订单。 在场订单和仓位只有开单时间。

此外,修改历史订单和成交集合的类构造函数。 MQL5 订单系统没有平仓时间概念 — 所有订单和成交都根据其放置时间(或根据 MQL4 订单系统的开单时间)排列在列表中。 为此,修改 CHistoryCollection 类构造函数中历史订单和成交集合列表中定义排序方向的字符串:

//+------------------------------------------------------------------+
//| 构造函数                                                           |
//+------------------------------------------------------------------+
CHistoryCollection::CHistoryCollection(void) : m_index_deal(0),m_delta_deal(0),m_index_order(0),m_delta_order(0),m_is_trade_event(false)
  {
   this.m_list_all_orders.Sort(#ifdef __MQL5__ SORT_BY_ORDER_TIME_OPEN #else SORT_BY_ORDER_TIME_CLOSE #endif );
   this.m_list_all_orders.Clear();
  }
//+------------------------------------------------------------------+

目前在 MQL5 中,历史订单和成交集合中的所有订单和成交默认按其放置时间排序,而在 MQL4 中,它们将按订单属性中定义的平仓时间排序。

现在我们继续讨论 MQL5 订单系统的另一个特征。 当由逆向仓位平仓时,会放置 ORDER_TYPE_CLOSE_BY 类型的特殊平仓订单,而当由停止单平仓时,会用平仓的在场单替代。
考虑到在场订单,我们必须在接收和返回基本订单的整数型属性的方法中添加另一个属性(以接收和返回订单魔幻数字作为示例):

//+------------------------------------------------------------------+
//| 返回魔幻数字                                                         |
//+------------------------------------------------------------------+
long COrder::OrderMagicNumber() const
  {
#ifdef __MQL4__
   return ::OrderMagicNumber();
#else
   long res=0;
   switch((ENUM_ORDER_STATUS)this.GetProperty(ORDER_PROP_STATUS))
     {
      case ORDER_STATUS_MARKET_POSITION   : res=::PositionGetInteger(POSITION_MAGIC);           break;
      case ORDER_STATUS_MARKET_ORDER      :
      case ORDER_STATUS_MARKET_PENDING    : res=::OrderGetInteger(ORDER_MAGIC);                 break;
      case ORDER_STATUS_DEAL              : res=::HistoryDealGetInteger(m_ticket,DEAL_MAGIC);   break;
      case ORDER_STATUS_HISTORY_PENDING   :
      case ORDER_STATUS_HISTORY_ORDER     : res=::HistoryOrderGetInteger(m_ticket,ORDER_MAGIC); break;
      default                             : res=0;                                              break;
     }
   return res;
#endif
  }
//+------------------------------------------------------------------+

我在所有需要考虑此状态的接收和返回基本订单整数型属性的方法里(或逻辑上与方法对应)中完成了这些修改。 由于存在这样的状态,因此在函数库的 Objects 文件夹中创建一个新的 CMarketOrder 在场订单类来存储此类订单类型。 该类与之前创建的在场和历史订单和成交对象完全相同,所以在此我只提供清单:

//+------------------------------------------------------------------+
//|                                                  MarketOrder.mqh |
//|                                   版权所有 2018, MetaQuotes 软件公司 |
//|                             https://mql5.com/zh/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "版权所有 2018, MetaQuotes 软件公司"
#property link      "https://mql5.com/zh/users/artmedia70"
#property version   "1.00"
//+------------------------------------------------------------------+
//| 包含文件                                                           |
//+------------------------------------------------------------------+
#include "Order.mqh"
//+------------------------------------------------------------------+
//| 在场订单                                                           |
//+------------------------------------------------------------------+
class CMarketOrder : public COrder
  {
public:
   //--- 构造函数
                     CMarketOrder(const ulong ticket=0) : COrder(ORDER_STATUS_MARKET_ORDER,ticket) {}
   //--- 支持的订单属性(1)实数型,(2)整数型
   virtual bool      SupportProperty(ENUM_ORDER_PROP_DOUBLE property);
   virtual bool      SupportProperty(ENUM_ORDER_PROP_INTEGER property);
  };
//+------------------------------------------------------------------+
//| 返回 'true',如果订单支持传递的                                      |
//| 整数型属性,否则,返回 'false'                                        |
//+------------------------------------------------------------------+
bool CMarketOrder::SupportProperty(ENUM_ORDER_PROP_INTEGER property)
  {
   if(property==ORDER_PROP_TIME_EXP          || 
      property==ORDER_PROP_DEAL_ENTRY        || 
      property==ORDER_PROP_TIME_UPDATE       || 
      property==ORDER_PROP_TIME_UPDATE_MSC   ||
      property==ORDER_PROP_PROFIT_PT         ||
      property==ORDER_PROP_TIME_CLOSE        ||
      property==ORDER_PROP_TIME_CLOSE_MSC    ||
      property==ORDER_PROP_TICKET_FROM       ||
      property==ORDER_PROP_TICKET_TO
     ) return false;
   return true;
  }
//+------------------------------------------------------------------+
//| 返回 'true',如果订单支持传递的                                      |
//| 实数型属性,否则,返回 'false'                                        |
//+------------------------------------------------------------------+
bool CMarketOrder::SupportProperty(ENUM_ORDER_PROP_DOUBLE property)
  {
   if(property==ORDER_PROP_PROFIT            || 
      property==ORDER_PROP_PROFIT_FULL       || 
      property==ORDER_PROP_SWAP              || 
      property==ORDER_PROP_COMMISSION        ||
      property==ORDER_PROP_PRICE_CLOSE       ||
      property==ORDER_PROP_SL                ||
      property==ORDER_PROP_TP                ||
      property==ORDER_PROP_PRICE_STOP_LIMIT
     ) return false;
   return true;
  }
//+------------------------------------------------------------------+

在函数库的 Defines.mqh 文件中,编写新的 状态 — 在场订单

//+------------------------------------------------------------------+
//| 抽象订单类型 (状态)                                                  |
//+------------------------------------------------------------------+
enum ENUM_ORDER_STATUS
  {
   ORDER_STATUS_MARKET_PENDING,                             // 在场挂单
   ORDER_STATUS_MARKET_ORDER,                               // 在场订单
   ORDER_STATUS_MARKET_POSITION,                            // 在场仓位
   ORDER_STATUS_HISTORY_ORDER,                              // 历史订单
   ORDER_STATUS_HISTORY_PENDING,                            // 已删除挂单
   ORDER_STATUS_BALANCE,                                    // 余额操作
   ORDER_STATUS_CREDIT,                                     // 信贷操作
   ORDER_STATUS_DEAL,                                       // 成交
   ORDER_STATUS_UNKNOWN                                     // 未知状态
  };
//+------------------------------------------------------------------+

现在,在用来向列表添加订单的模块中(CMarketCollection 类 'Refresh() 方法用于刷新在场订单和仓位列表),实现订单类型检查。 取决于类型,将在场订单对象挂单对象添加到集合列表中:

//+------------------------------------------------------------------+
//| 更新订单列表                                                         |
//+------------------------------------------------------------------+
void CMarketCollection::Refresh(void)
  {
   ::ZeroMemory(this.m_struct_curr_market);
   this.m_is_trade_event=false;
   this.m_is_change_volume=false;
   this.m_new_pendings=0;
   this.m_new_positions=0;
   this.m_change_volume_value=0;
   m_list_all_orders.Clear();
#ifdef __MQL4__
   int total=::OrdersTotal();
   for(int i=0; i<total; i++)
     {
      if(!::OrderSelect(i,SELECT_BY_POS)) continue;
      long ticket=::OrderTicket();
      ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)::OrderType();
      if(type==ORDER_TYPE_BUY || type==ORDER_TYPE_SELL)
        {
         CMarketPosition *position=new CMarketPosition(ticket);
         if(position==NULL) continue;
         if(this.m_list_all_orders.InsertSort(position))
           {
            this.m_struct_market.hash_sum_acc+=ticket;
            this.m_struct_market.total_volumes+=::OrderLots();
            this.m_struct_market.total_positions++;
           }
         else
           {
            ::Print(DFUN,TextByLanguage("Не удалось добавить позицию в список","Failed to add position to list"));
            delete position;
           }
        }
      else
        {
         CMarketPending *order=new CMarketPending(ticket);
         if(order==NULL) continue;
         if(this.m_list_all_orders.InsertSort(order))
           {
            this.m_struct_market.hash_sum_acc+=ticket;
            this.m_struct_market.total_volumes+=::OrderLots();
            this.m_struct_market.total_pending++;
           }
         else
           {
            ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Failed to add order to list"));
            delete order;
           }
        }
     }
//--- MQ5
#else 
//--- 仓位
   int total_positions=::PositionsTotal();
   for(int i=0; i<total_positions; i++)
     {
      ulong ticket=::PositionGetTicket(i);
      if(ticket==0) continue;
      CMarketPosition *position=new CMarketPosition(ticket);
      if(position==NULL) continue;
      if(this.m_list_all_orders.InsertSort(position))
        {
         this.m_struct_curr_market.hash_sum_acc+=(long)::PositionGetInteger(POSITION_TIME_UPDATE_MSC);
         this.m_struct_curr_market.total_volumes+=::PositionGetDouble(POSITION_VOLUME);
         this.m_struct_curr_market.total_positions++;
        }
      else
        {
         ::Print(DFUN,TextByLanguage("Не удалось добавить позицию в список","Failed to add position to list"));
         delete position;
        }
     }
//--- 订单
   int total_orders=::OrdersTotal();
   for(int i=0; i<total_orders; i++)
     {
      ulong ticket=::OrderGetTicket(i);
      if(ticket==0) continue;
      ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)::OrderGetInteger(ORDER_TYPE);
      if(type==ORDER_TYPE_BUY || type==ORDER_TYPE_SELL)
        {
         CMarketOrder *order=new CMarketOrder(ticket);
         if(order==NULL) continue;
         if(this.m_list_all_orders.InsertSort(order))
           {
            this.m_struct_curr_market.hash_sum_acc+=(long)ticket;
            this.m_struct_curr_market.total_market++;
           }
         else
           {
            ::Print(DFUN,TextByLanguage("Не удалось добавить маркет-ордер в список","Failed to add market order to list"));
            delete order;
           }
        }
      else
        {
         CMarketPending *order=new CMarketPending(ticket);
         if(order==NULL) continue;
         if(this.m_list_all_orders.InsertSort(order))
           {
            this.m_struct_curr_market.hash_sum_acc+=(long)ticket;
            this.m_struct_curr_market.total_volumes+=::OrderGetDouble(ORDER_VOLUME_INITIAL);
            this.m_struct_curr_market.total_pending++;
           }
         else
           {
            ::Print(DFUN,TextByLanguage("Не удалось добавить отложенный ордер в список","Failed to add pending order to list"));
            delete order;
           }
        }
     }
#endif 
//--- 首次启动
   if(this.m_struct_prev_market.hash_sum_acc==WRONG_VALUE)
     {
      this.SavePrevValues();
     }
//--- 如果所有订单和仓位的哈希值发生变化
   if(this.m_struct_curr_market.hash_sum_acc!=this.m_struct_prev_market.hash_sum_acc)
     {
      this.m_new_market=this.m_struct_curr_market.total_market-this.m_struct_prev_market.total_market;
      this.m_new_pendings=this.m_struct_curr_market.total_pending-this.m_struct_prev_market.total_pending;
      this.m_new_positions=this.m_struct_curr_market.total_positions-this.m_struct_prev_market.total_positions;
      this.m_change_volume_value=::NormalizeDouble(this.m_struct_curr_market.total_volumes-this.m_struct_prev_market.total_volumes,4);
      this.m_is_change_volume=(this.m_change_volume_value!=0 ? true : false);
      this.m_is_trade_event=true;
      this.SavePrevValues();
     }
  }
//+------------------------------------------------------------------+

为了能够考虑平仓 ORDER_TYPE_CLOSE_BY 类型的订单,将此订单类型添加到 CHistoryCollection 类中 Refresh() 历史订单和成交列表更新方法里的订单类型定义模块,以便将此类订单包含在集合中。 没有它,CEngine 函数库的基础对象无法定义一笔仓位被逆向仓位关闭:

//+------------------------------------------------------------------+
//| 更新订单和成交列表                                                    |
//+------------------------------------------------------------------+
void CHistoryCollection::Refresh(void)
  {
#ifdef __MQL4__
   int total=::OrdersHistoryTotal(),i=m_index_order;
   for(; i<total; i++)
     {
      if(!::OrderSelect(i,SELECT_BY_POS,MODE_HISTORY)) continue;
      ENUM_ORDER_TYPE order_type=(ENUM_ORDER_TYPE)::OrderType();
      //--- 平仓和余额/信贷操作
      if(order_type<ORDER_TYPE_BUY_LIMIT || order_type>ORDER_TYPE_SELL_STOP)
        {
         CHistoryOrder *order=new CHistoryOrder(::OrderTicket());
         if(order==NULL) continue;
         if(!this.m_list_all_orders.InsertSort(order))
           {
            ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Failed to add order to list"));
            delete order;
           }
        }
      else
        {
         //--- 删除挂单
         CHistoryPending *order=new CHistoryPending(::OrderTicket());
         if(order==NULL) continue;
         if(!this.m_list_all_orders.InsertSort(order))
           {
            ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Failed to add order to list"));
            delete order;
           }
        }
     }
//---
   int delta_order=i-m_index_order;
   this.m_index_order=i;
   this.m_delta_order=delta_order;
   this.m_is_trade_event=(this.m_delta_order!=0 ? true : false);
//--- __MQL5__
#else 
   if(!::HistorySelect(0,END_TIME)) return;
//--- 订单
   int total_orders=::HistoryOrdersTotal(),i=m_index_order;
   for(; i<total_orders; i++)
     {
      ulong order_ticket=::HistoryOrderGetTicket(i);
      if(order_ticket==0) continue;
      ENUM_ORDER_TYPE type=(ENUM_ORDER_TYPE)::HistoryOrderGetInteger(order_ticket,ORDER_TYPE);
      if(type==ORDER_TYPE_BUY || type==ORDER_TYPE_SELL || type==ORDER_TYPE_CLOSE_BY)
        {
         CHistoryOrder *order=new CHistoryOrder(order_ticket);
         if(order==NULL) continue;
         if(!this.m_list_all_orders.InsertSort(order))
           {
            ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Failed to add order to list"));
            delete order;
           }
        }
      else
        {
         CHistoryPending *order=new CHistoryPending(order_ticket);
         if(order==NULL) continue;
         if(!this.m_list_all_orders.InsertSort(order))
           {
            ::Print(DFUN,TextByLanguage("Не удалось добавить ордер в список","Failed to add order to list"));
            delete order;
           }
        }
     }
//--- 保存最后添加的订单索引,以及与前一次检查相比的差值
   int delta_order=i-this.m_index_order;
   this.m_index_order=i;
   this.m_delta_order=delta_order;

//--- 成交
   int total_deals=::HistoryDealsTotal(),j=m_index_deal;
   for(; j<total_deals; j++)
     {
      ulong deal_ticket=::HistoryDealGetTicket(j);
      if(deal_ticket==0) continue;
      CHistoryDeal *deal=new CHistoryDeal(deal_ticket);
      if(deal==NULL) continue;
      this.m_list_all_orders.InsertSort(deal);
     }
//--- 保存最后添加的成交索引,以及与前一次检查相比的差值
   int delta_deal=j-this.m_index_deal;
   this.m_index_deal=j;
   this.m_delta_deal=delta_deal;
//--- 在历史记录中设置新事件标志
   this.m_is_trade_event=(this.m_delta_order+this.m_delta_deal);
#endif 
  }
//+------------------------------------------------------------------+

在测试 CEngine 类来定义发生的帐户事件时,我检测并修复了服务方法中的一些小缺陷。 在此论述它们毫无意义,因为它们不会影响性能,而过多描述会转移人们对重要函数库功能开发的注意力。 所有类列表修改已经完成了。 您可自行查看附带于下的函数库文件。


我们继续有关事件定义的工作。

调试交易事件定义之后,所有发生的事件将被封包存储到单个类成员变量中,用作一组标志。 方法从变量里读取数据,将其值分解为元件,然后创建表征特定事件。

添加类成员变量来存储交易事件代码验证方法区分对冲和净持结算帐户的交易事件,方法返回必要的订单对象 至 CEngine 类的私有部分。
在公有部分,声明方法返回在场仓位和挂单列表历史订单和交易,方法返回 m_trade_event_code 变量中的交易事件代码,以及方法返回对冲帐户标志

//+------------------------------------------------------------------+
//| 函数库基类                                                          |
//+------------------------------------------------------------------+
class CEngine : public CObject
  {
private:
   CHistoryCollection   m_history;                       // 历史订单和成交集合
   CMarketCollection    m_market;                        // 在场订单和成交集合
   CArrayObj            m_list_counters;                 // 计时器列表计数器
   bool                 m_first_start;                   // 首次启动标志
   bool                 m_is_hedge;                      // 对冲账户标志
   bool                 m_is_market_trade_event;         // 帐户交易事件标志
   bool                 m_is_history_trade_event;        // 帐户历史交易事件标志
   int                  m_trade_event_code;              // 帐户交易事件状态代码
//--- 按 id 返回计数器索引
   int                  CounterIndex(const int id) const;
//--- 返回首次启动标志
   bool                 IsFirstStart(void);
//--- 集合处理(1)对冲,和(2)净持
   void                 WorkWithHedgeCollections(void);
   void                 WorkWithNettoCollections(void);
//--- 返回最后一笔(1)在场挂单,(2)在价订单,(3)最后一笔仓位,(4)按凭单的仓位
   COrder*              GetLastMarketPending(void);                    
   COrder*              GetLastMarketOrder(void);                      
   COrder*              GetLastPosition(void);                         
   COrder*              GetPosition(const ulong ticket);               
//--- 返回最后一笔(1)删除的挂单,(2)历史订单,(3)按凭单的历史订单
   COrder*              GetLastHistoryPending(void);                   
   COrder*              GetLastHistoryOrder(void);                     
   COrder*              GetHistoryOrder(const ulong ticket);           
//--- 从所有仓位订单列表中返回(1)第一笔,和(2)最后的历史订单,(3)最后一笔成交
   COrder*              GetFirstOrderPosition(const ulong position_id);
   COrder*              GetLastOrderPosition(const ulong position_id); 
   COrder*              GetLastDeal(void);                             
public:
   //--- 返回在场(1)仓位,(2)挂单,和(3)在场订单清单
   CArrayObj*           GetListMarketPosition(void);                     
   CArrayObj*           GetListMarketPendings(void);                     
   CArrayObj*           GetListMarketOrders(void);                       
   //--- 返回历史清单(1)订单,(2)删除挂单,(3)成交,(4)所有仓位的订单 ID
   CArrayObj*           GetListHistoryOrders(void);                      
   CArrayObj*           GetListHistoryPendings(void);                    
   CArrayObj*           GetListHistoryDeals(void);                       
   CArrayObj*           GetListAllOrdersByPosID(const ulong position_id);
//--- 返回(1)交易事件代码,和(2)对冲账户标志
   int                  TradeEventCode(void)             const { return this.m_trade_event_code;   }
   bool                 IsHedge(void)                    const { return this.m_is_hedge;           }
//--- 创建计时器
   void                 CreateCounter(const int id,const ulong frequency,const ulong pause);
//--- 计时器
   void                 OnTimer(void);
//--- 构造函数/析构函数
                        CEngine();
                       ~CEngine();
  };
//+------------------------------------------------------------------+

在类构造函数的初始化列表中初始化交易事件代码

//+------------------------------------------------------------------+
//| CEngine 构造函数                                                   |
//+------------------------------------------------------------------+
CEngine::CEngine() : m_first_start(true),m_trade_event_code(TRADE_EVENT_FLAG_NO_EVENT)
  {
   ::EventSetMillisecondTimer(TIMER_FREQUENCY);
   this.m_list_counters.Sort();
   this.CreateCounter(COLLECTION_COUNTER_ID,COLLECTION_COUNTER_STEP,COLLECTION_PAUSE);
   this.m_is_hedge=bool(::AccountInfoInteger(ACCOUNT_MARGIN_MODE)==ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
  }
//+------------------------------------------------------------------+

在类的实体外部实现声明的方法:

//+------------------------------------------------------------------+
//| 返回在场仓位列表                                                     |
//+------------------------------------------------------------------+
CArrayObj* CEngine::GetListMarketPosition(void)
  {
   CArrayObj* list=this.m_market.GetList();
   list=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_MARKET_POSITION,EQUAL);
   return list;
  }
//+------------------------------------------------------------------+
//| 返回在场挂单列表                                                     |
//+------------------------------------------------------------------+
CArrayObj* CEngine::GetListMarketPendings(void)
  {
   CArrayObj* list=this.m_market.GetList();
   list=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_MARKET_PENDING,EQUAL);
   return list;
  }
//+------------------------------------------------------------------+
//| 返回在场订单列表                                                     |
//+------------------------------------------------------------------+
CArrayObj* CEngine::GetListMarketOrders(void)
  {
   CArrayObj* list=this.m_market.GetList();
   list=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_MARKET_ORDER,EQUAL);
   return list;
  }
//+------------------------------------------------------------------+
//| 返回历史订单列表                                                     |
//+------------------------------------------------------------------+
CArrayObj* CEngine::GetListHistoryOrders(void)
  {
   CArrayObj* list=this.m_history.GetList();
   list=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_HISTORY_ORDER,EQUAL);
   return list;
  }
//+------------------------------------------------------------------+
//| 返回已删除挂单列表                                                  |
//+------------------------------------------------------------------+
CArrayObj* CEngine::GetListHistoryPendings(void)
  {
   CArrayObj* list=this.m_history.GetList();
   list=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_HISTORY_PENDING,EQUAL);
   return list;
  }
//+------------------------------------------------------------------+
//| 返回成交列表                                                       |
//+------------------------------------------------------------------+
CArrayObj* CEngine::GetListHistoryDeals(void)
  {
   CArrayObj* list=this.m_history.GetList();
   list=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_DEAL,EQUAL);
   return list;
  }
//+------------------------------------------------------------------+
//|  返回所有仓位的订单清单                                               |
//+------------------------------------------------------------------+
CArrayObj* CEngine::GetListAllOrdersByPosID(const ulong position_id)
  {
   CArrayObj* list=this.GetListHistoryOrders();
   list=CSelect::ByOrderProperty(list,ORDER_PROP_POSITION_ID,position_id,EQUAL);
   return list;
  }
//+------------------------------------------------------------------+
//| 返回最后一笔仓位                                                     |
//+------------------------------------------------------------------+
COrder* CEngine::GetLastPosition(void)
  {
   CArrayObj* list=this.GetListMarketPosition();
   if(list==NULL) return NULL;
   list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC);
   COrder* order=list.At(list.Total()-1);
   return(order!=NULL ? order : NULL);
  }
//+------------------------------------------------------------------+
//| 返回凭单代表的仓位                                                    |
//+------------------------------------------------------------------+
COrder* CEngine::GetPosition(const ulong ticket)
  {
   CArrayObj* list=this.GetListMarketPosition();
   list=CSelect::ByOrderProperty(list,ORDER_PROP_TICKET,ticket,EQUAL);
   if(list==NULL) return NULL;
   list.Sort(SORT_BY_ORDER_TICKET);
   COrder* order=list.At(list.Total()-1);
   return(order!=NULL ? order : NULL);
  }
//+------------------------------------------------------------------+
//| 返回最后一笔成交                                                     |
//+------------------------------------------------------------------+
COrder* CEngine::GetLastDeal(void)
  {
   CArrayObj* list=this.GetListHistoryDeals();
   if(list==NULL) return NULL;
   list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC);
   COrder* order=list.At(list.Total()-1);
   return(order!=NULL ? order : NULL);
  }
//+------------------------------------------------------------------+
//| 返回最后一笔在场挂单                                                 |
//+------------------------------------------------------------------+
COrder* CEngine::GetLastMarketPending(void)
  {
   CArrayObj* list=this.GetListMarketPendings();
   if(list==NULL) return NULL;
   list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC);
   COrder* order=list.At(list.Total()-1);
   return(order!=NULL ? order : NULL);
  }
//+------------------------------------------------------------------+
//| 返回最后一笔历史挂单                                                 |
//+------------------------------------------------------------------+
COrder* CEngine::GetLastHistoryPending(void)
  {
   CArrayObj* list=this.GetListHistoryPendings();
   if(list==NULL) return NULL;
   list.Sort(#ifdef __MQL5__ SORT_BY_ORDER_TIME_OPEN_MSC #else SORT_BY_ORDER_TIME_CLOSE_MSC #endif);
   COrder* order=list.At(list.Total()-1);
   return(order!=NULL ? order : NULL);
  }
//+------------------------------------------------------------------+
//| 返回最后一笔在场订单                                                 |
//+------------------------------------------------------------------+
COrder* CEngine::GetLastMarketOrder(void)
  {
   CArrayObj* list=this.GetListMarketOrders();
   if(list==NULL) return NULL;
   list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC);
   COrder* order=list.At(list.Total()-1);
   return(order!=NULL ? order : NULL);
  }
//+------------------------------------------------------------------+
//| 返回最后一笔历史订单                                                 |
//+------------------------------------------------------------------+
COrder* CEngine::GetLastHistoryOrder(void)
  {
   CArrayObj* list=this.GetListHistoryOrders();
   if(list==NULL) return NULL;
   list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC);
   COrder* order=list.At(list.Total()-1);
   return(order!=NULL ? order : NULL);
  }
//+------------------------------------------------------------------+
//| 返回凭单代表的历史订单                                                |
//+------------------------------------------------------------------+
COrder* CEngine::GetHistoryOrder(const ulong ticket)
  {
   CArrayObj* list=this.GetListHistoryOrders();
   list=CSelect::ByOrderProperty(list,ORDER_PROP_TICKET,(long)ticket,EQUAL);
   if(list==NULL) return NULL;
   list.Sort(SORT_BY_ORDER_TICKET);
   COrder* order=list.At(list.Total()-1);
   return(order!=NULL ? order : NULL);
  }
//+------------------------------------------------------------------+
//| 返回手臂历史订单                                                     |
//| 从所有仓位订单列表                                                    |
//+------------------------------------------------------------------+
COrder* CEngine::GetFirstOrderPosition(const ulong position_id)
  {
   CArrayObj* list=this.GetListAllOrdersByPosID(position_id);
   if(list==NULL) return NULL;
   list.Sort(SORT_BY_ORDER_TIME_OPEN);
   COrder* order=list.At(0);
   return(order!=NULL ? order : NULL);
  }
//+------------------------------------------------------------------+
//| 返回最后一笔历史订单                                                 |
//| 从所有仓位订单列表                                                    |
//+------------------------------------------------------------------+
COrder* CEngine::GetLastOrderPosition(const ulong position_id)
  {
   CArrayObj* list=this.GetListAllOrdersByPosID(position_id);
   if(list==NULL) return NULL;
   list.Sort(SORT_BY_ORDER_TIME_OPEN);
   COrder* order=list.At(list.Total()-1);
   return(order!=NULL ? order : NULL);
  }
//+------------------------------------------------------------------+

我们看看如何使用以下示例获取列表:

//+------------------------------------------------------------------+
//| 返回在场仓位列表                                                     |
//+------------------------------------------------------------------+
CArrayObj* CEngine::GetListMarketPosition(void)
  {
   CArrayObj* list=this.m_market.GetList();
   list=CSelect::ByOrderProperty(list,ORDER_PROP_STATUS,ORDER_STATUS_MARKET_POSITION,EQUAL);
   return list;
  }
//+------------------------------------------------------------------+

一切都非常简单方便:使用 GetList() 集合方法从在场订单和仓位的集合中获得完整的仓位列表。 之后,使用来自 CSelect 类的按指定属性选择订单的方法,选择一笔具有 “position” 状态的订单。 该方法在第三篇的函数库说明中有所阐述。 返回获得的列表。
该列表可能为空(NULL),因此应在调用程序中检查此方法返回的结果。

我们看看如何使用以下示例接收必要的订单:

//+------------------------------------------------------------------+
//| 返回最后一笔仓位                                                     |
//+------------------------------------------------------------------+
COrder* CEngine::GetLastPosition(void)
  {
   CArrayObj* list=this.GetListMarketPosition();
   if(list==NULL) return NULL;
   list.Sort(SORT_BY_ORDER_TIME_OPEN_MSC);
   COrder* order=list.At(list.Total()-1);
   return(order!=NULL ? order : NULL);
  }
//+------------------------------------------------------------------+

首先,使用上述 GetListMarketPosition() 方法接收仓位列表如果列表为空,则返回 NULL。 接着,按开单时间(以毫秒为单位)对列表进行排序(因为我们将得到最后一笔开仓,列表应按时间排序),且选择其中的最后一笔订单。 结果则为,返回从列表中获得的订单至调用程序
方法返回搜索订单结果可能为空(未找到订单),且等于 NULL。 因此,在访问之前检查获得的结果是否为 NULL。

如您所见,一切都快速而简单。 我们可以构造任何方法来接收来自任何现有集合列表的任何数据,以及我们将来创建的数据。 这为我们提供了使用此类列表的更大灵活性。
接收列表的方法放在类的公有部分,允许我们从自定义程序中的列表中接收任何数据,就像上面论及的方法一样。
接收必要订单的方法隐藏在私有部分中。 它们仅在 CEngine 类中用于内部需求 — 特别是用于获取最后订单、成交和仓位的数据。 在自定义程序中,我们可以通过指定的属性创建自定义函数来接收特定的订单(示例如上所示)。
为了从集合中轻松获取任何数据,我们为最终用户提供了极其广泛的函数
这会在处理订单集合的最终文章中完成。

现在我们来实现检查交易事件的方法
目前,它仅适用于 MQL5对冲帐户。 今后,我将开发用于检查 MQL5 净持结算账户和 MQL4 交易事件的方法。 若要测试方法操作,它会显示检查和事件结果。 此能力将在以后删除,因为调试检查帐户内交易事件的方法过后,会创建另一个方法来涵盖此函数。

//+------------------------------------------------------------------+
//| 检查交易事件(对冲)                                                 |
//+------------------------------------------------------------------+
void CEngine::WorkWithHedgeCollections(void)
  {
//--- 初始化交易事件代码和标志
   this.m_trade_event_code=TRADE_EVENT_FLAG_NO_EVENT;
   this.m_is_market_trade_event=false;
   this.m_is_history_trade_event=false;
//--- 更新列表 
   this.m_market.Refresh();
   this.m_history.Refresh();
//--- 首次启动时的行动
   if(this.IsFirstStart())
     {
      this.m_acc_trade_event=TRADE_EVENT_NO_EVENT;
      return;
     }
//--- 检查市场状态动态和帐户历史
   this.m_is_market_trade_event=this.m_market.IsTradeEvent();
   this.m_is_history_trade_event=this.m_history.IsTradeEvent();
//---
#ifdef __MQL4__

#else // MQL5
//--- 如果事件仅在在场订单和仓位中
   if(this.m_is_market_trade_event && !this.m_is_history_trade_event)
     {
      Print(DFUN,TextByLanguage("Новое торговое событие на счёте","New trading event on account"));
      //--- 如果挂单数量增加
      if(this.m_market.NewPendingOrders()>0)
        {
         //--- 添加安置挂单的标志
         this.m_trade_event_code+=TRADE_EVENT_FLAG_ORDER_PLASED;
         string text=TextByLanguage("Установлен отложенный ордер: ","Pending order placed: ");
         //--- 取最后一笔在场挂单
         COrder* order=this.GetLastMarketPending();
         if(order!=NULL)
           {
            //--- 将订单凭单添加到消息中
            text+=order.TypeDescription()+" #"+(string)order.Ticket();
           }
         //--- 将消息添加到流水日志中
         Print(DFUN,text);
        }
      //--- 如果在场订单数量增加
      if(this.m_market.NewMarketOrders()>0)
        {
         //--- 不要添加事件标志
         //--- ...
         string text=TextByLanguage("Выставлен маркет-ордер: ","Market order placed: ");
         //--- 取最后一笔在场订单
         COrder* order=this.GetLastMarketOrder();
         if(order!=NULL)
           {
            //--- 将订单凭单添加到消息中
            text+=order.TypeDescription()+" #"+(string)order.Ticket();
           }
         //--- 将消息添加到流水日志中
         Print(DFUN,text);
        }
     }
   
//--- 如果事件仅在历史订单和成交中
   else if(this.m_is_history_trade_event && !this.m_is_market_trade_event)
     {
      Print(DFUN,TextByLanguage("Новое торговое событие в истории счёта","New trading event in account history"));
      //--- 如果出现一笔新成交
      if(this.m_history.NewDeals()>0)
        {
         //--- 添加帐户余额事件标志
         this.m_trade_event_code+=TRADE_EVENT_FLAG_ACCOUNT_BALANCE;
         string text=TextByLanguage("Новая сделка: ","New deal: ");
         //--- 取最后一笔成交
         COrder* deal=this.GetLastDeal();
         if(deal!=NULL)
           {
            //--- 将其描述添加到文本中
            text+=deal.TypeDescription();
            //--- 如果成交是余额操作
            if((ENUM_DEAL_TYPE)deal.GetProperty(ORDER_PROP_TYPE)==DEAL_TYPE_BALANCE)
              {
              //--- 检查成交利润并在消息中添加事件(入金或出金)
               text+=(deal.Profit()>0 ? TextByLanguage(": Пополнение счёта: ",": Account Recharge: ") : TextByLanguage(": Вывод средств: ",": Withdrawal: "))+::DoubleToString(deal.Profit(),(int)::AccountInfoInteger(ACCOUNT_CURRENCY_DIGITS));
              }
           }
         //--- 在流水帐中显示消息
         Print(DFUN,text);
        }
     }
   
//--- 如果事件是在场和历史订单和仓位
   else if(this.m_is_market_trade_event && this.m_is_history_trade_event)
     {
      Print(DFUN,TextByLanguage("Новые торговые события на счёте и в истории счёта","New trading events on account and in account history"));
      
      //--- 如果挂单数量减少,且没有新的成交出现
      if(this.m_market.NewPendingOrders()<0 && this.m_history.NewDeals()==0)
        {
         //--- 添加删除挂单的标志
         this.m_trade_event_code+=TRADE_EVENT_FLAG_ORDER_REMOVED;
         string text=TextByLanguage("Удалён отложенный ордер: ","Removed pending order: ");
         //--- 取最后一笔历史挂单
         COrder* order=this.GetLastHistoryPending();
         if(order!=NULL)
           {
            //--- 将凭单添加到相应的消息中
            text+=order.TypeDescription()+" #"+(string)order.Ticket();
           }
         //--- 在流水帐中显示消息
         Print(DFUN,text);
        }
      
      //--- 如果有新的成交和新的历史订单
      if(this.m_history.NewDeals()>0 && this.m_history.NewOrders()>0)
        {
         //--- 取最后一笔成交
         COrder* deal=this.GetLastDeal();
         if(deal!=NULL)
           {
            //--- 如果是入场成交
            if(deal.GetProperty(ORDER_PROP_DEAL_ENTRY)==DEAL_ENTRY_IN)
              {
               //--- 添加开仓标志
               this.m_trade_event_code+=TRADE_EVENT_FLAG_POSITION_OPENED;
               string text=TextByLanguage("Открыта позиция: ","Position opened: ");
               //--- 如果挂单数量减少
               if(this.m_market.NewPendingOrders()<0)
                 {
                  //--- 添加挂单激活标记
                  this.m_trade_event_code+=TRADE_EVENT_FLAG_ORDER_ACTIVATED;
                  text=TextByLanguage("Сработал отложенный ордер: ","Pending order activated: ");
                 }
               //--- 从订单列表里取订单的凭证
               ulong order_ticket=deal.GetProperty(ORDER_PROP_DEAL_ORDER);
               COrder* order=this.GetHistoryOrder(order_ticket);
               if(order!=NULL)
                 {
                  //--- 如果当前订单交易量超过零
                  if(order.VolumeCurrent()>0)
                    {
                     //--- 添加部分执行标志
                     this.m_trade_event_code+=TRADE_EVENT_FLAG_PARTIAL;
                     text=TextByLanguage("Частично открыта позиция: ","Position partially open: ");
                    }
                  //--- 将订单方向添加到消息中
                  text+=order.DirectionDescription();
                 }
               //--- 将来自成交的仓位凭证添加到消息中,并在流水帐中显示消息
               text+=" #"+(string)deal.PositionID();
               Print(DFUN,text);
              }
            
            //--- 如果是离场成交
            if(deal.GetProperty(ORDER_PROP_DEAL_ENTRY)==DEAL_ENTRY_OUT)
              {
               //--- 添加平仓标志
               this.m_trade_event_code+=TRADE_EVENT_FLAG_POSITION_CLOSED;
               string text=TextByLanguage("Закрыта позиция: ","Position closed: ");
               //--- 从成交里取成交的凭证
               ulong order_ticket=deal.GetProperty(ORDER_PROP_DEAL_ORDER);
               COrder* order=this.GetHistoryOrder(order_ticket);
               if(order!=NULL)
                 {
                  //--- 如果成交的仓位仍然在场
                  COrder* pos=this.GetPosition(deal.PositionID());
                  if(pos!=NULL)
                    {
                     //--- 添加部分执行标志
                     this.m_trade_event_code+=TRADE_EVENT_FLAG_PARTIAL;
                     text=TextByLanguage("Частично закрыта позиция: ","Partially closed position: ");
                    }
                  //--- 否则,如果仓位已完全平仓
                  else
                    {
                     //--- 如果订单带由止损平仓的标志
                     if(order.IsCloseByStopLoss())
                       {
                        //--- 添加由止损平仓标志
                        this.m_trade_event_code+=TRADE_EVENT_FLAG_SL;
                        text=TextByLanguage("Позиция закрыта по StopLoss: ","Position closed by StopLoss: ");
                       }
                     //--- 如果订单带由止盈平仓的标志
                     if(order.IsCloseByTakeProfit())
                       {
                        //--- 添加由止盈平仓标志
                        this.m_trade_event_code+=TRADE_EVENT_FLAG_TP;
                        text=TextByLanguage("Позиция закрыта по TakeProfit: ","Position closed by TakeProfit: ");
                       }
                    }
                  //--- 将逆转方向添加到消息里:
                  //--- 空头仓位由买入订单平仓,多头仓位由卖出订单平仓
                  //--- 因此,逆转订单方向用于正确描述平仓
                  text+=(order.DirectionDescription()=="Sell" ? "Buy " : "Sell ");
                 }
               //--- 添加成交的仓位凭证,并在流水帐中显示消息
               text+="#"+(string)deal.PositionID();
               Print(DFUN,text);
              }
            
            //--- 当由逆向仓位平仓时
            if(deal.GetProperty(ORDER_PROP_DEAL_ENTRY)==DEAL_ENTRY_OUT_BY)
              {
               //--- 添加平仓标志
               this.m_trade_event_code+=TRADE_EVENT_FLAG_POSITION_CLOSED;
               //--- 添加由逆向仓位平仓的标志
               this.m_trade_event_code+=TRADE_EVENT_FLAG_BY_POS;
               string text=TextByLanguage("Позиция закрыта встречной: ","Position closed by opposite position: ");
               //--- 取成交的订单
               ulong ticket_from=deal.GetProperty(ORDER_PROP_DEAL_ORDER);
               COrder* order=this.GetHistoryOrder(ticket_from);
               if(order!=NULL)
                 {
                  //--- 将逆转方向添加到消息里:
                  //--- 空头仓位由买入订单平仓,多头仓位由卖出订单平仓
                  //--- 因此,逆转订单方向用于正确描述平仓
                  text+=(order.DirectionDescription()=="Sell" ? "Buy" : "Sell");
                  text+=" #"+(string)order.PositionID()+TextByLanguage(" закрыта позицией #"," closed by position #")+(string)order.PositionByID();
                  //--- 如果订单仓位仍然在场
                  COrder* pos=this.GetPosition(order.PositionID());
                  if(pos!=NULL)
                    {
                     //--- 添加部分执行标志
                     this.m_trade_event_code+=TRADE_EVENT_FLAG_PARTIAL;
                     text+=TextByLanguage(" частично"," partially");
                    }
                 }
               //--- 在流水帐中显示消息
               Print(DFUN,text);
              }
           //--- 最后一笔成交处理模块结束
           }
        }
     }
#endif 
  }
//+------------------------------------------------------------------+

该方法很简单但很庞大。 因此,代码提供所有检查和相应的操作,令我们能够更方便地查看方法内部发生的情况。 目前,该方法实现了 MQL5 对冲账户的检查。 事实上,一切都归结为检查帐户历史记录,或场中新出现的订单和成交的数量。 为了在流水帐中显示,数据取自最后一笔仓位,最后一笔成交,最后一笔订单,或最后一笔成交的订单 — 这些所为,均是为了在流水帐中显示数据,以便检查代码的执行。 稍后将从代码中删除此功能,并程序中用调用函数库的单一方法来替代。

测试交易事件的处理

我们来创建一个测试 EA,用于检查定义帐户交易事件的方法。
我们实现一组按钮来管控新事件。
必要的操作和相应按钮的集合如下:

  • 开多仓
  • 放置 BuyLimit 挂单
  • 放置 BuyStop 挂单
  • 放置 BuyStopLimit 挂单
  • 平多仓
  • 平部分多仓
  • 由空仓平多仓
  • 开空仓
  • 放置 SellLimit 挂单
  • 放置 SellStop 挂单
  • 放置 SellStopLimit 挂单
  • 平空仓
  • 平部分空仓
  • 由多仓平空仓
  • 平所有持仓
  • 从账户中出金

输入集如下:

  • Magic number - 魔幻数字
  • Lots - 开仓量
  • StopLoss in points
  • TakeProfit in points
  • Pending orders distance (points)
  • StopLimit orders distance (points)
    StopLimit 挂单置于 Pending orders distance 值设定距离价位,作为停止订单。
    一旦价格达到挂单指定位置并激活它,该价位用于在 StopLimit orders distance 值指定距离的价位处设置限价挂单。
  • Slippage in points
  • Withdrawal funds (in tester) - 在测试器里从帐户中出金

我们需要一些函数,计算相对于 StopLevel 的调整值来定义下单价位,停止单和开仓量。 在此阶段,因为我们还没有交易类和品种类,我们可以将这些函数添加到 DELib.mqh 文件中的服务函数库中:

//+------------------------------------------------------------------+
//| 返回品种的最小手数                                                    |
//+------------------------------------------------------------------+
double MinimumLots(const string symbol_name) 
  { 
   return SymbolInfoDouble(symbol_name,SYMBOL_VOLUME_MIN);
  }
//+------------------------------------------------------------------+
//| 返回品种的最大手数                                                    |
//+------------------------------------------------------------------+
double MaximumLots(const string symbol_name) 
  { 
   return SymbolInfoDouble(symbol_name,SYMBOL_VOLUME_MAX);
  }
//+------------------------------------------------------------------+
//| 返回品种的手数变化增量                                                  |
//+------------------------------------------------------------------+
double StepLots(const string symbol_name) 
  { 
   return SymbolInfoDouble(symbol_name,SYMBOL_VOLUME_STEP);
  }
//+------------------------------------------------------------------+
//| 返回常规化手数                                                      |
//+------------------------------------------------------------------+
double NormalizeLot(const string symbol_name, double order_lots) 
  {
   double ml=SymbolInfoDouble(symbol_name,SYMBOL_VOLUME_MIN);
   double mx=SymbolInfoDouble(symbol_name,SYMBOL_VOLUME_MAX);
   double ln=NormalizeDouble(order_lots,int(ceil(fabs(log(ml)/log(10)))));
   return(ln<ml ? ml : ln>mx ? mx : ln);
  }
//+------------------------------------------------------------------+
//| 返回相对于 StopLevel 的正确止损位                                     |
//+------------------------------------------------------------------+
double CorrectStopLoss(const string symbol_name,const ENUM_ORDER_TYPE order_type,const double price_set,const double stop_loss,const int spread_multiplier=2)
  {
   if(stop_loss==0) return 0;
   int lv=StopLevel(symbol_name,spread_multiplier), dg=(int)SymbolInfoInteger(symbol_name,SYMBOL_DIGITS);
   double pt=SymbolInfoDouble(symbol_name,SYMBOL_POINT);
   double price=(order_type==ORDER_TYPE_BUY ? SymbolInfoDouble(symbol_name,SYMBOL_BID) : order_type==ORDER_TYPE_SELL ? SymbolInfoDouble(symbol_name,SYMBOL_ASK) : price_set);
   return
     (order_type==ORDER_TYPE_BUY       || 
      order_type==ORDER_TYPE_BUY_LIMIT || 
      order_type==ORDER_TYPE_BUY_STOP
      #ifdef __MQL5__                  ||
      order_type==ORDER_TYPE_BUY_STOP_LIMIT
      #endif ? 
      NormalizeDouble(fmin(price-lv*pt,stop_loss),dg) :
      NormalizeDouble(fmax(price+lv*pt,stop_loss),dg)
     );
  }
//+------------------------------------------------------------------+
//| 返回相对于 StopLevel 的正确止损位                                     |
//+------------------------------------------------------------------+
double CorrectStopLoss(const string symbol_name,const ENUM_ORDER_TYPE order_type,const double price_set,const int stop_loss,const int spread_multiplier=2)
  {
   if(stop_loss==0) return 0;
   int lv=StopLevel(symbol_name,spread_multiplier), dg=(int)SymbolInfoInteger(symbol_name,SYMBOL_DIGITS);
   double pt=SymbolInfoDouble(symbol_name,SYMBOL_POINT);
   double price=(order_type==ORDER_TYPE_BUY ? SymbolInfoDouble(symbol_name,SYMBOL_BID) : order_type==ORDER_TYPE_SELL ? SymbolInfoDouble(symbol_name,SYMBOL_ASK) : price_set);
   return
     (order_type==ORDER_TYPE_BUY       || 
      order_type==ORDER_TYPE_BUY_LIMIT || 
      order_type==ORDER_TYPE_BUY_STOP
      #ifdef __MQL5__                  ||
      order_type==ORDER_TYPE_BUY_STOP_LIMIT
      #endif ?
      NormalizeDouble(fmin(price-lv*pt,price-stop_loss*pt),dg) :
      NormalizeDouble(fmax(price+lv*pt,price+stop_loss*pt),dg)
     );
  }
//+------------------------------------------------------------------+
//| 返回相对于 StopLevel 的正确止盈位                                     |
//+------------------------------------------------------------------+
double CorrectTakeProfit(const string symbol_name,const ENUM_ORDER_TYPE order_type,const double price_set,const double take_profit,const int spread_multiplier=2)
  {
   if(take_profit==0) return 0;
   int lv=StopLevel(symbol_name,spread_multiplier), dg=(int)SymbolInfoInteger(symbol_name,SYMBOL_DIGITS);
   double pt=SymbolInfoDouble(symbol_name,SYMBOL_POINT);
   double price=(order_type==ORDER_TYPE_BUY ? SymbolInfoDouble(symbol_name,SYMBOL_BID) : order_type==ORDER_TYPE_SELL ? SymbolInfoDouble(symbol_name,SYMBOL_ASK) : price_set);
   return
     (order_type==ORDER_TYPE_BUY       || 
      order_type==ORDER_TYPE_BUY_LIMIT || 
      order_type==ORDER_TYPE_BUY_STOP
      #ifdef __MQL5__                  ||
      order_type==ORDER_TYPE_BUY_STOP_LIMIT
      #endif ?
      NormalizeDouble(fmax(price+lv*pt,take_profit),dg) :
      NormalizeDouble(fmin(price-lv*pt,take_profit),dg)
     );
  }
//+------------------------------------------------------------------+
//| 返回相对于 StopLevel 的正确止盈位                                     |
//+------------------------------------------------------------------+
double CorrectTakeProfit(const string symbol_name,const ENUM_ORDER_TYPE order_type,const double price_set,const int take_profit,const int spread_multiplier=2)
  {
   if(take_profit==0) return 0;
   int lv=StopLevel(symbol_name,spread_multiplier), dg=(int)SymbolInfoInteger(symbol_name,SYMBOL_DIGITS);
   double pt=SymbolInfoDouble(symbol_name,SYMBOL_POINT);
   double price=(order_type==ORDER_TYPE_BUY ? SymbolInfoDouble(symbol_name,SYMBOL_BID) : order_type==ORDER_TYPE_SELL ? SymbolInfoDouble(symbol_name,SYMBOL_ASK) : price_set);
   return
     (order_type==ORDER_TYPE_BUY       || 
      order_type==ORDER_TYPE_BUY_LIMIT || 
      order_type==ORDER_TYPE_BUY_STOP
      #ifdef __MQL5__                  ||
      order_type==ORDER_TYPE_BUY_STOP_LIMIT
      #endif ?
      ::NormalizeDouble(::fmax(price+lv*pt,price+take_profit*pt),dg) :
      ::NormalizeDouble(::fmin(price-lv*pt,price-take_profit*pt),dg)
     );
  }
//+------------------------------------------------------------------+
//| 返回正确的下单价位                                                  |
//| 相对于 StopLevel                                                  |
//+------------------------------------------------------------------+
double CorrectPricePending(const string symbol_name,const ENUM_ORDER_TYPE order_type,const double price_set,const double price=0,const int spread_multiplier=2)
  {
   double pt=SymbolInfoDouble(symbol_name,SYMBOL_POINT),pp=0;
   int lv=StopLevel(symbol_name,spread_multiplier), dg=(int)SymbolInfoInteger(symbol_name,SYMBOL_DIGITS);
   switch(order_type)
     {
      case ORDER_TYPE_BUY_LIMIT        :  pp=(price==0 ? SymbolInfoDouble(symbol_name,SYMBOL_ASK) : price); return NormalizeDouble(fmin(pp-lv*pt,price_set),dg);
      case ORDER_TYPE_BUY_STOP         :  
      case ORDER_TYPE_BUY_STOP_LIMIT   :  pp=(price==0 ? SymbolInfoDouble(symbol_name,SYMBOL_ASK) : price); return NormalizeDouble(fmax(pp+lv*pt,price_set),dg);
      case ORDER_TYPE_SELL_LIMIT       :  pp=(price==0 ? SymbolInfoDouble(symbol_name,SYMBOL_BID) : price); return NormalizeDouble(fmax(pp+lv*pt,price_set),dg);
      case ORDER_TYPE_SELL_STOP        :  
      case ORDER_TYPE_SELL_STOP_LIMIT  :  pp=(price==0 ? SymbolInfoDouble(symbol_name,SYMBOL_BID) : price); return NormalizeDouble(fmin(pp-lv*pt,price_set),dg);
      default                          :  Print(DFUN,TextByLanguage("Не правильный тип ордера: ","Invalid order type: "),EnumToString(order_type)); return 0;
     }
  }
//+------------------------------------------------------------------+
//| 返回正确的下单价位                                                  |
//| 相对于 StopLevel                                                  |
//+------------------------------------------------------------------+
double CorrectPricePending(const string symbol_name,const ENUM_ORDER_TYPE order_type,const int distance_set,const double price=0,const int spread_multiplier=2)
  {
   double pt=SymbolInfoDouble(symbol_name,SYMBOL_POINT),pp=0;
   int lv=StopLevel(symbol_name,spread_multiplier), dg=(int)SymbolInfoInteger(symbol_name,SYMBOL_DIGITS);
   switch(order_type)
     {
      case ORDER_TYPE_BUY_LIMIT        :  pp=(price==0 ? SymbolInfoDouble(symbol_name,SYMBOL_ASK) : price); return NormalizeDouble(fmin(pp-lv*pt,pp-distance_set*pt),dg);
      case ORDER_TYPE_BUY_STOP         :  
      case ORDER_TYPE_BUY_STOP_LIMIT   :  pp=(price==0 ? SymbolInfoDouble(symbol_name,SYMBOL_ASK) : price); return NormalizeDouble(fmax(pp+lv*pt,pp+distance_set*pt),dg);
      case ORDER_TYPE_SELL_LIMIT       :  pp=(price==0 ? SymbolInfoDouble(symbol_name,SYMBOL_BID) : price); return NormalizeDouble(fmax(pp+lv*pt,pp+distance_set*pt),dg);
      case ORDER_TYPE_SELL_STOP        :  
      case ORDER_TYPE_SELL_STOP_LIMIT  :  pp=(price==0 ? SymbolInfoDouble(symbol_name,SYMBOL_BID) : price); return NormalizeDouble(fmin(pp-lv*pt,pp-distance_set*pt),dg);
      default                          :  Print(DFUN,TextByLanguage("Не правильный тип ордера: ","Invalid order type: "),EnumToString(order_type)); return 0;
     }
  }
//+------------------------------------------------------------------+
//| 检查相对于 StopLevel 的停止价位点数                                    |
//+------------------------------------------------------------------+
bool CheckStopLevel(const string symbol_name,const int stop_in_points,const int spread_multiplier)
  {
   return(stop_in_points>=StopLevel(symbol_name,spread_multiplier));
  }
//+------------------------------------------------------------------+
//| 返回 StopLevel 的点数                                               |
//+------------------------------------------------------------------+
int StopLevel(const string symbol_name,const int spread_multiplier)
  {
   int spread=(int)SymbolInfoInteger(symbol_name,SYMBOL_SPREAD);
   int stop_level=(int)SymbolInfoInteger(symbol_name,SYMBOL_TRADE_STOPS_LEVEL);
   return(stop_level==0 ? spread*spread_multiplier : stop_level);
  }
//+------------------------------------------------------------------+ 

这些函数简单地计算调整值,以便它们不违反所在服务器设置的限制。 除品种外,StopLevel 计算函数还接收点差乘数。 这样做是出于如果服务器上的 StopLevel 设置为零,这意味着此为浮动参数值,并且为了计算 StopLevel,我们应该使用点差值乘以某个数字(通常为 2,但也可以是 3)。 该乘数传递给函数,允许我们在 EA 设置中对其进行硬编码或计算。

为了节省时间,我们不会编写自定义交易函数。 代之,我们将利用标准函数库内现成的交易类,即 CTrade 类来执行交易操作。
为了创建工作图表按钮,我们打算实现枚举,为其成员设置的按钮名称、标签和值用来检查按下某个按钮。

在 MQL5\Experts\TestDoEasy\Part04\,创建一个名为 TestDoEasy04.mqh 的新 EA(在创建 EA 时检查 MQL 向导中的 OnTimer 和 OnChartEvent 事件处理程序):


通过 MQL 向导创建 EA 模板后,将自定义函数库 和标准库的交易类包含在其中。 另外,添加输入参数:

//+------------------------------------------------------------------+
//|                                             TestDoEasyPart04.mq5 |
//|                                   版权所有 2018, MetaQuotes 软件公司 |
//|                             https://mql5.com/zh/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "版权所有 2018, MetaQuotes 软件公司"
#property link      "https://mql5.com/zh/users/artmedia70"
#property version   "1.00"
//--- 包含
#include <DoEasy\Engine.mqh>
#include <Trade\Trade.mqh>  
//--- 枚举
enum ENUM_BUTTONS
  {
   BUTT_BUY,
   BUTT_BUY_LIMIT,
   BUTT_BUY_STOP,
   BUTT_BUY_STOP_LIMIT,
   BUTT_CLOSE_BUY,
   BUTT_CLOSE_BUY2,
   BUTT_CLOSE_BUY_BY_SELL,
   BUTT_SELL,
   BUTT_SELL_LIMIT,
   BUTT_SELL_STOP,
   BUTT_SELL_STOP_LIMIT,
   BUTT_CLOSE_SELL,
   BUTT_CLOSE_SELL2,
   BUTT_CLOSE_SELL_BY_BUY,
   BUTT_CLOSE_ALL,
   BUTT_PROFIT_WITHDRAWAL
  };
#define TOTAL_BUTT   (16)
//--- 结构
struct SDataButt
  {
   string      name;
   string      text;
  };
//--- 输入参数
input ulong    InpMagic       =  123;  // 魔幻数字
input double   InpLots        =  0.1;  // 手数
input uint     InpStopLoss    =  50;   // 止损点数
input uint     InpTakeProfit  =  50;   // 止盈点数
input uint     InpDistance    =  50;   // 挂单距离 (点数)
input uint     InpDistanceSL  =  50;   // StopLimit 挂单距离 (点数)
input uint     InpSlippage    =  0;    // 滑点点数
input double   InpWithdrawal  =  10;   // 出金 (在测试器内)
//--- 全局变量
CEngine        engine;
CTrade         trade;
SDataButt      butt_data[TOTAL_BUTT];
string         prefix;
double         lot;
double         withdrawal=(InpWithdrawal<0.1 ? 0.1 : InpWithdrawal);
ulong          magic_number;
uint           stoploss;
uint           takeprofit;
uint           distance_pending;
uint           distance_stoplimit;
uint           slippage;
//+------------------------------------------------------------------+

此处我们包括 CEngine 函数库的主要对象CTrade 交易类
接下来,创建枚举指定所有必需按钮。
枚举方法的顺序很重要,因为它设置按钮创建的顺序,及其在图表上的位置。

之后,声明结构,用于存储按钮图形对象的名称,和要在按钮上铭刻的文本。
输入模块中,设置上面列出的所有 EA 参数变量。 在 EA 的全局变量块中,声明函数库对象交易类对象按钮结构数组,并在 OnInit() 处理程序中为输入变量赋值:

//+------------------------------------------------------------------+
//| 智能系统初始化函数                                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 检查帐户类型
   if(!engine.IsHedge())
     {
      Alert(TextByLanguage("Ошибка. Счёт должен быть хеджевым","Error. Account must be hedge"));
      return INIT_FAILED;
     }
//--- 设置全局变量
   prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_";
   for(int i=0;i<TOTAL_BUTT;i++)
     {
      butt_data[i].name=prefix+EnumToString((ENUM_BUTTONS)i);
      butt_data[i].text=EnumToButtText((ENUM_BUTTONS)i);
     }
   lot=NormalizeLot(Symbol(),fmax(InpLots,MinimumLots(Symbol())*2.0));
   magic_number=InpMagic;
   stoploss=InpStopLoss;
   takeprofit=InpTakeProfit;
   distance_pending=InpDistance;
   distance_stoplimit=InpDistanceSL;
   slippage=InpSlippage;
//--- 创建按钮
   if(!CreateButtons())  
      return INIT_FAILED;
//---
   trade.SetDeviationInPoints(slippage);
   trade.SetExpertMagicNumber(magic_number);
   trade.SetTypeFillingBySymbol(Symbol());
   trade.SetMarginMode();
   trade.LogLevel(LOG_LEVEL_ERRORS);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+

在OnInit() 处理程序中,检查帐户类型,如果不是对冲,则退出程序并显示错误通告。
接下来,设置对象名称的前缀(以便 EA 能够识别其对象),并以按钮数字在循环中用按钮数据填充结构数组
按钮对象名称的设置是由前缀+与索引对应的 ENUM_BUTTONS 枚举字符串表述值,而按钮文本则利用 EnumToButtText() 函数将对应于循环索引的枚举字符串表述值转换而来。

未来会计算开仓和下单的手数。 由于一半仓位会被平仓,已开仓的手数应该至少是最小仓量的两倍。 因此,最大手数取自两处:
1) 输入字段的参数, 2) 最小手数乘二 fmax(InpLots,MinimumLots(Symbol())*2.0),将获得的手数值常规化并赋值给 lot 全局变量。 结果为,若用户在输入字段中输入的手数小于最小手数的两倍,则使用双倍的最小手数。 否则,按用户输入的手数。

其余输入分配给相应的全局变量,调用 CreateButtons() 函数,依据上一步中已填充按钮数据的结构数组来创建按钮。 如果使用 ButtonCreate() 函数创建按钮失败,则会显示错误消息,并且程序操作因初始化错误结束

最后,初始化 CTrade 类:

//---
   trade.SetDeviationInPoints(slippage);
   trade.SetExpertMagicNumber(magic_number);
   trade.SetTypeFillingBySymbol(Symbol());
   trade.SetMarginMode();
   trade.LogLevel(LOG_LEVEL_ERRORS);
//---
  • 设置滑点点数
  • 设置魔幻数字,
  • 根据当前品种的设定配置订单执行类型,
  • 根据当前帐户设定配置保证金计算模式,以及 
  • 配置 消息记录级别 为仅在流水帐里显示错误消息
    (在测试器中自动启用完整的记录模式)。

OnDeinit() 处理程序中,按对象名称前缀实现删除所有按钮

//+------------------------------------------------------------------+
//| 智能系统逆初始化函数                                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- 删除对象
   ObjectsDeleteAll(0,prefix);
  }
//+------------------------------------------------------------------+

现在我们来决定函数库计时器和 EA 事件处理程序的启动顺序。

  • 如果 EA 并非在测试器中启动,则函数库计时器将从 EA 计时器启动,而事件处理程序则以正常模式运行。
  • 如果 EA 在测试器中启动,则函数库计时器将从 EA 的OnTick() 处理程序里启动。 按下按钮事件也在 OnTick() 中跟踪。

EA 的 OnTick(),OnTimer() 和 OnChartEvent() 处理程序:

//+------------------------------------------------------------------+
//| 智能系统即时报价函数                                                 |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   if(MQLInfoInteger(MQL_TESTER))
      engine.OnTimer();
   int total=ObjectsTotal(0);
   for(int i=0;i<total;i++)
     {
      string obj_name=ObjectName(0,i);
      if(StringFind(obj_name,prefix+"BUTT_")<0)
         continue;
      PressButtonEvents(obj_name);
     }
  }
//+------------------------------------------------------------------+
//| 计时器函数                                                          |
//+------------------------------------------------------------------+
void OnTimer()
  {
   if(!MQLInfoInteger(MQL_TESTER))
      engine.OnTimer();
  }
//+------------------------------------------------------------------+
//| ChartEvent 函数                                                    |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
   if(MQLInfoInteger(MQL_TESTER))
      return;
   if(id==CHARTEVENT_OBJECT_CLICK && StringFind(sparam,"BUTT_")>0)
     {
      PressButtonEvents(sparam);
     }  
  }
//+------------------------------------------------------------------+

在 OnTick() 里

  • 检查 EA 自何处启动。 如果它在测试器中启动,调用函数库的 OnTimer() 处理程序
  • 接下来,在循环中检查所有当前图表对象的对象名称。 如果它与按钮的名称匹配,则调用按下按钮处理程序

在 OnTimer() 里

  • 检查 EA 自何处启动。 如果并非在测试器中启动则调用函数库的 OnTimer() 处理程序

在 OnChartEvent() 里

  • 检查 EA 自何处启动。 如果它在测试器中启动,则退出处理程序
  • 接下来,检查事件 ID,如果这是单击图形对象的事件,并且对象名称包含属于按钮的文本,则调用相应的按钮处理程序

CreateButtons() 函数:

//+------------------------------------------------------------------+
//| 创建按钮面板                                                       |
//+------------------------------------------------------------------+
bool CreateButtons(void)
  {
   int h=18,w=84,offset=10;
   int cx=offset,cy=offset+(h+1)*(TOTAL_BUTT/2)+h+1;
   int x=cx,y=cy;
   int shift=0;
   for(int i=0;i<TOTAL_BUTT;i++)
     {
      x=x+(i==7 ? w+2 : 0);
      if(i==TOTAL_BUTT-2) x=cx;
      y=(cy-(i-(i>6 ? 7 : 0))*(h+1));
      if(!ButtonCreate(butt_data[i].name,x,y,(i<TOTAL_BUTT-2 ? w : w*2+2),h,butt_data[i].text,(i<4 ? clrGreen : i>6 && i<11 ? clrRed : clrBlue)))
        {
         Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_data[i].text);
         return false;
        }
     }
   ChartRedraw(0);
   return true;
  }
//+------------------------------------------------------------------+

在此函数中,一切都归结为在循环中按照 ENUM_BUTTONS 枚举成员的数字计算按钮的坐标和颜色。 基于循环索引指示的 ENUM_BUTTONS 枚举成员数字计算坐标和颜色。 计算 x 和 y 坐标之后,在循环中以计算出的坐标和颜色值调用按钮创建函数。

EnumToButtText() 函数:

//+------------------------------------------------------------------+
//| 将枚举转换为按钮文本                                                 |
//+------------------------------------------------------------------+
string EnumToButtText(const ENUM_BUTTONS member)
  {
   string txt=StringSubstr(EnumToString(member),5);
   StringToLower(txt);
   StringReplace(txt,"buy","Buy");
   StringReplace(txt,"sell","Sell");
   StringReplace(txt,"_limit"," Limit");
   StringReplace(txt,"_stop"," Stop");
   StringReplace(txt,"close_","Close ");
   StringReplace(txt,"2"," 1/2");
   StringReplace(txt,"_by_"," by ");
   StringReplace(txt,"profit_","Profit ");
   return txt;
  }
//+------------------------------------------------------------------+

在此一切都很简单:函数接收枚举成员将其转换为字符串,并删除不必要的文字。 接下来,将所获字符串的所有字符转换为小写,且将所有不恰当的条目进一步替换为必要的条目。
最后将输入的枚举字符串转换为文本返回

创建按钮,放置并接收其状态的函数:

//+------------------------------------------------------------------+
//| 创建按钮                                                           |
//+------------------------------------------------------------------+
bool ButtonCreate(const string name,const int x,const int y,const int w,const int h,const string text,const color clr,const string font="Calibri",const int font_size=8)
  {
   if(ObjectFind(0,name)<0)
     {
      if(!ObjectCreate(0,name,OBJ_BUTTON,0,0,0)) 
        { 
         Print(DFUN,TextByLanguage("не удалось создать кнопку! Код ошибки=","Could not create button! Error code="),GetLastError()); 
         return false; 
        } 
      ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
      ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
      ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
      ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
      ObjectSetInteger(0,name,OBJPROP_XSIZE,w);
      ObjectSetInteger(0,name,OBJPROP_YSIZE,h);
      ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_LOWER);
      ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_LEFT_LOWER);
      ObjectSetInteger(0,name,OBJPROP_FONTSIZE,font_size);
      ObjectSetString(0,name,OBJPROP_FONT,font);
      ObjectSetString(0,name,OBJPROP_TEXT,text);
      ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
      ObjectSetString(0,name,OBJPROP_TOOLTIP,"\n");
      ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,clrGray);
      return true;
     }
   return false;
  }
//+------------------------------------------------------------------+
//| 返回按钮状态                                                       |
//+------------------------------------------------------------------+
bool ButtonState(const string name)
  {
   return (bool)ObjectGetInteger(0,name,OBJPROP_STATE);
  }
//+------------------------------------------------------------------+
//| 设置按钮状态                                                       |
//+------------------------------------------------------------------+
void ButtonState(const string name,const bool state)
  {
   ObjectSetInteger(0,name,OBJPROP_STATE,state);
  }
//+------------------------------------------------------------------+

一切都很简单明了,所以无需解释。

按下按钮的函数:

//+------------------------------------------------------------------+
//| 按下按钮的处理程序                                                  |
//+------------------------------------------------------------------+
void PressButtonEvents(const string button_name)
  {
   //--- 将按钮名称转换为其字符串 ID
   string button=StringSubstr(button_name,StringLen(prefix));
   //--- 如果按钮被按下
   if(ButtonState(button_name))
     {
      //--- 如果 BUTT_BUY 按钮被按下:开多仓
      if(button==EnumToString(BUTT_BUY))
        {
         //--- 获取相对于 StopLevel 的正确止损和止盈价位
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY,0,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY,0,takeprofit);
         //--- 开多仓
         trade.Buy(NormalizeLot(Symbol(),lot),Symbol(),0,sl,tp);
        }
      //--- 如果 BUTT_BUY_LIMIT 按钮被按下: 放置 BuyLimit
      else if(button==EnumToString(BUTT_BUY_LIMIT))
        {
         //--- 获取相对于 StopLevel 的正确挂单放置价位
         double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_LIMIT,distance_pending);
         //--- 参考 StopLevel,获取相对于订单放置价位的正确止损和止盈价位
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY_LIMIT,price_set,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY_LIMIT,price_set,takeprofit);
         //--- 设置 BuyLimit 挂单
         trade.BuyLimit(lot,price_set,Symbol(),sl,tp);
        }
      //--- 如果 BUTT_BUY_STOP 按钮被按下: 设置 BuyStop
      else if(button==EnumToString(BUTT_BUY_STOP))
        {
         //--- 获取相对于 StopLevel 的正确下单价位
         double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_STOP,distance_pending);
         //--- 参考 StopLevel,获取相对于订单放置价位的正确止损和止盈价位
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY_STOP,price_set,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY_STOP,price_set,takeprofit);
         //--- 设置 BuyStop 挂单
         trade.BuyStop(lot,price_set,Symbol(),sl,tp);
        }
      //--- 如果 BUTT_BUY_STOP_LIMIT 按钮被按下: 设置 BuyStopLimit
      else if(button==EnumToString(BUTT_BUY_STOP_LIMIT))
        {
         //--- 获取相对于 StopLevel 的正确放置 BuyStop 挂单价位
         double price_set_stop=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_STOP,distance_pending);
         //--- 参考 StopLevel,计算相对于 BuyStop 价位的 BuyLimit 挂单价位
         double price_set_limit=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_LIMIT,distance_stoplimit,price_set_stop);
         //--- 参考 StopLevel,获取相对于订单放置价位的正确止损和止盈价位
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY_STOP,price_set_limit,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY_STOP,price_set_limit,takeprofit);
         //--- 设置 BuyStopLimit 挂单
         trade.OrderOpen(Symbol(),ORDER_TYPE_BUY_STOP_LIMIT,lot,price_set_limit,price_set_stop,sl,tp);
        }
      //--- 如果 BUTT_SELL 按钮被按下: 开空仓
      else if(button==EnumToString(BUTT_SELL))
        {
         //--- 获取相对于 StopLevel 的正确止损和止盈价位
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL,0,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL,0,takeprofit);
         //--- 开空仓
         trade.Sell(lot,Symbol(),0,sl,tp);
        }
      //--- 如果 BUTT_SELL_LIMIT 按钮被按下: 设置 SellLimit
      else if(button==EnumToString(BUTT_SELL_LIMIT))
        {
         //--- 获取相对于 StopLevel 的正确下单价位
         double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_LIMIT,distance_pending);
         //--- 参考 StopLevel,获取相对于订单放置价位的正确止损和止盈价位
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL_LIMIT,price_set,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL_LIMIT,price_set,takeprofit);
         //--- 设置 SellLimit 挂单
         trade.SellLimit(lot,price_set,Symbol(),sl,tp);
        }
      //--- 如果 BUTT_SELL_STOP 按钮被按下: 设置 SellStop
      else if(button==EnumToString(BUTT_SELL_STOP))
        {
         //--- 获取相对于 StopLevel 的正确下单价位
         double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_STOP,distance_pending);
         //--- 参考 StopLevel,获取相对于订单放置价位的正确止损和止盈价位
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL_STOP,price_set,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL_STOP,price_set,takeprofit);
         //--- 设置 SellStop 挂单
         trade.SellStop(lot,price_set,Symbol(),sl,tp);
        }
      //--- 如果 BUTT_SELL_STOP_LIMIT 按钮被按下: 设置 SellStopLimit
      else if(button==EnumToString(BUTT_SELL_STOP_LIMIT))
        {
         //--- 获取相对于 StopLevel 的正确放置 SellStop 挂单价位
         double price_set_stop=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_STOP,distance_pending);
         //--- 参考 StopLevel,计算相对于 SellStop 价位的 SellLimit 挂单价位
         double price_set_limit=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_LIMIT,distance_stoplimit,price_set_stop);
         //--- 参考 StopLevel,获取相对于订单放置价位的正确止损和止盈价位
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL_STOP,price_set_limit,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL_STOP,price_set_limit,takeprofit);
         //--- 设置 SellStopLimit 挂单
         trade.OrderOpen(Symbol(),ORDER_TYPE_SELL_STOP_LIMIT,lot,price_set_limit,price_set_stop,sl,tp);
        }
      //--- 如果 BUTT_CLOSE_BUY 按钮被按下:平盈利最大的多仓
      else if(button==EnumToString(BUTT_CLOSE_BUY))
        {
         //--- 获取所有持仓的清单
         CArrayObj* list=engine.GetListMarketPosition();
         //--- 仅从列表中选择多仓
         list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL);
         //--- 考虑佣金和掉期,按利润对列表进行排序
         list.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- 获取最大利润的多仓索引
         int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL);
         if(index>WRONG_VALUE)
           {
            COrder* position=list.At(index);
            if(position!=NULL)
              {
               //--- 获取多仓凭证,并按凭证平仓
               trade.PositionClose(position.Ticket());
              }
           }
        }
      //--- 如果 BUTT_CLOSE_BUY2 按钮被按下: 盈利最大的多仓平一半仓位
      else if(button==EnumToString(BUTT_CLOSE_BUY2))
        {
         //--- 获取所有持仓的清单
         CArrayObj* list=engine.GetListMarketPosition();
         //--- 仅从列表中选择多仓
         list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL);
         //--- 考虑佣金和掉期,按利润对列表进行排序
         list.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- 获取最大利润的多仓索引
         int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL);
         if(index>WRONG_VALUE)
           {
            COrder* position=list.At(index);
            if(position!=NULL)
              {
               //--- 计算平仓交易量,并通过凭证平一半多仓
               trade.PositionClosePartial(position.Ticket(),NormalizeLot(position.Symbol(),position.VolumeCurrent()/2.0));
              }
           }
        }
      //--- 如果 BUTT_CLOSE_BUY_BY_SELL 按钮被按下:平盈利最大的空仓
      else if(button==EnumToString(BUTT_CLOSE_BUY_BY_SELL))
        {
         //--- 获取所有持仓的清单
         CArrayObj* list_buy=engine.GetListMarketPosition();
         //--- 仅从列表中选择多仓
         list_buy=CSelect::ByOrderProperty(list_buy,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL);
         //--- 考虑佣金和掉期,按利润对列表进行排序
         list_buy.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- 获取最大利润的多仓索引
         int index_buy=CSelect::FindOrderMax(list_buy,ORDER_PROP_PROFIT_FULL);
         //--- 获取所有持仓的清单
         CArrayObj* list_sell=engine.GetListMarketPosition();
         //--- 仅从列表中选择空仓
         list_sell=CSelect::ByOrderProperty(list_sell,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL);
         //--- 考虑佣金和掉期,按利润对列表进行排序
         list_sell.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- 获取最大利润的空仓索引
         int index_sell=CSelect::FindOrderMax(list_sell,ORDER_PROP_PROFIT_FULL);
         if(index_buy>WRONG_VALUE && index_sell>WRONG_VALUE)
           {
            //--- 选择最大利润的多仓
            COrder* position_buy=list_buy.At(index_buy);
            //--- 选择最大利润的空仓
            COrder* position_sell=list_sell.At(index_sell);
            if(position_buy!=NULL && position_sell!=NULL)
              {
               //--- 由逆向空仓平多仓
               trade.PositionCloseBy(position_buy.Ticket(),position_sell.Ticket());
              }
           }
        }
      //--- 如果 BUTT_CLOSE_SELL 按钮被按下:平盈利最大的空仓
      else if(button==EnumToString(BUTT_CLOSE_SELL))
        {
         //--- 获取所有持仓的清单
         CArrayObj* list=engine.GetListMarketPosition();
         //--- 仅从列表中选择空头仓位
         list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL);
         //--- 考虑佣金和掉期,按利润对列表进行排序
         list.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- 获取最大利润的空仓索引
         int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL);
         if(index>WRONG_VALUE)
           {
            COrder* position=list.At(index);
            if(position!=NULL)
              {
               //--- 获取空头仓位凭证,并按凭证平仓
               trade.PositionClose(position.Ticket());
              }
           }
        }
      //--- 如果 BUTT_CLOSE_SELL2 按钮被按下: 盈利最大的空头仓位平一半仓位
      else if(button==EnumToString(BUTT_CLOSE_SELL2))
        {
         //--- 获取所有持仓的清单
         CArrayObj* list=engine.GetListMarketPosition();
         //--- 仅从列表中选择空头仓位
         list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL);
         //--- 考虑佣金和掉期,按利润对列表进行排序
         list.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- 获取最大利润的空仓索引
         int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL);
         if(index>WRONG_VALUE)
           {
            COrder* position=list.At(index);
            if(position!=NULL)
              {
               //--- 计算平仓交易量,并通过凭证平一半空头仓位
               trade.PositionClosePartial(position.Ticket(),NormalizeLot(position.Symbol(),position.VolumeCurrent()/2.0));
              }
           }
        }
      //--- 如果 BUTT_CLOSE_SELL_BY_BUY 按钮被按下:由最大盈利的多仓盈利平最大盈利的空仓  
      else if(button==EnumToString(BUTT_CLOSE_SELL_BY_BUY))
        {
         //--- 获取所有持仓的清单
         CArrayObj* list_sell=engine.GetListMarketPosition();
         //--- 仅从列表中选择空头仓位
         list_sell=CSelect::ByOrderProperty(list_sell,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL);
         //--- 考虑佣金和掉期,按利润对列表进行排序
         list_sell.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- 获取最大利润的空仓索引
         int index_sell=CSelect::FindOrderMax(list_sell,ORDER_PROP_PROFIT_FULL);
         //--- 获取所有持仓的清单
         CArrayObj* list_buy=engine.GetListMarketPosition();
         //--- 仅从列表中选择多仓
         list_buy=CSelect::ByOrderProperty(list_buy,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL);
         //--- 考虑佣金和掉期,按利润对列表进行排序
         list_buy.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- 获取最大利润的多仓索引
         int index_buy=CSelect::FindOrderMax(list_buy,ORDER_PROP_PROFIT_FULL);
         if(index_sell>WRONG_VALUE && index_buy>WRONG_VALUE)
           {
            //--- 选择最大利润的空仓
            COrder* position_sell=list_sell.At(index_sell);
            //--- 选择最大利润的多仓
            COrder* position_buy=list_buy.At(index_buy);
            if(position_sell!=NULL && position_buy!=NULL)
              {
               //--- 由逆向多仓平空仓
               trade.PositionCloseBy(position_sell.Ticket(),position_buy.Ticket());
              }
           }
        }
      //--- 如果 BUTT_CLOSE_ALL 按钮被按下: 从利润最少的那个开始平所有持仓
      else if(button==EnumToString(BUTT_CLOSE_ALL))
        {
         //--- 接收所有持仓的清单
         CArrayObj* list=engine.GetListMarketPosition();
         if(list!=NULL)
           {
            //--- 考虑佣金和掉期,按利润对列表进行排序
            list.Sort(SORT_BY_ORDER_PROFIT_FULL);
            int total=list.Total();
            //--- 自利润最少的仓位循环
            for(int i=0;i<total;i++)
              {
               COrder* position=list.At(i);
               if(position==NULL)
                  continue;
               //--- 按其凭证平仓
               trade.PositionClose(position.Ticket());
              }
           }
        }
      //--- 如果 BUTT_PROFIT_WITHDRAWAL 按钮被按下: 从账户里出金
      if(button==EnumToString(BUTT_PROFIT_WITHDRAWAL))
        {
         //--- 如果程序在测试器中启动
         if(MQLInfoInteger(MQL_TESTER))
           {
            //--- 模拟出金
            TesterWithdrawal(withdrawal);
           }
        }
      //--- 等待 1/10 秒
      Sleep(100);
      //--- 按钮 “取消按下”并重绘图表
      ButtonState(button_name,false);
      ChartRedraw();
     }
  }
//+------------------------------------------------------------------+

该函数非常巨大但很简单:它接收转换为字符串 ID 的按钮对象的名称。 接下来检查按钮状态,如果按下,则检查字符串 ID。 执行相应的 if-else 分支,计算所有价位,并进行必要的调整,以免违反 StopLevel 限制。 执行交易类的相应方法。
所有解释都直接写在代码的字符串注释中。

对于测试 EA,我们进行了最少的必要检查,跳过了对于真实账户很重要的所有其他检查。 目前,对我们来说最重要的是验证函数库操作,而非开发在真实帐户上运行的 EA。

测试 EA 的完整清单:

//+------------------------------------------------------------------+
//|                                             TestDoEasyPart04.mq5 |
//|                                   版权所有 2018, MetaQuotes 软件公司 |
//|                             https://mql5.com/zh/users/artmedia70 |
//+------------------------------------------------------------------+
#property copyright "版权所有 2018, MetaQuotes 软件公司"
#property link      "https://mql5.com/zh/users/artmedia70"
#property version   "1.00"
//--- 包含
#include <DoEasy\Engine.mqh>
#include <Trade\Trade.mqh>
//--- 枚举
enum ENUM_BUTTONS
  {
   BUTT_BUY,
   BUTT_BUY_LIMIT,
   BUTT_BUY_STOP,
   BUTT_BUY_STOP_LIMIT,
   BUTT_CLOSE_BUY,
   BUTT_CLOSE_BUY2,
   BUTT_CLOSE_BUY_BY_SELL,
   BUTT_SELL,
   BUTT_SELL_LIMIT,
   BUTT_SELL_STOP,
   BUTT_SELL_STOP_LIMIT,
   BUTT_CLOSE_SELL,
   BUTT_CLOSE_SELL2,
   BUTT_CLOSE_SELL_BY_BUY,
   BUTT_CLOSE_ALL,
   BUTT_PROFIT_WITHDRAWAL
  };
#define TOTAL_BUTT   (16)
//--- 结构
struct SDataButt
  {
   string      name;
   string      text;
  };
//--- 输入参数
input ulong    InpMagic       =  123;  // 魔幻数字
input double   InpLots        =  0.1;  // 手数
input uint     InpStopLoss    =  50;   // 止损点数
input uint     InpTakeProfit  =  50;   // 止盈点数
input uint     InpDistance    =  50;   // 挂单距离 (点数)
input uint     InpDistanceSL  =  50;   // StopLimit 挂单距离 (点数)
input uint     InpSlippage    =  0;    // 滑点点数
input double   InpWithdrawal  =  10;   // 出金 (在测试器内)
//--- 全局变量
CEngine        engine;
CTrade         trade;
SDataButt      butt_data[TOTAL_BUTT];
string         prefix;
double         lot;
double         withdrawal=(InpWithdrawal<0.1 ? 0.1 : InpWithdrawal);
ulong          magic_number;
uint           stoploss;
uint           takeprofit;
uint           distance_pending;
uint           distance_stoplimit;
uint           slippage;
//+------------------------------------------------------------------+
//| 智能系统初始化函数                                                  |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- 检查帐户类型
   if(!engine.IsHedge())
     {
      Alert(TextByLanguage("Ошибка. Счёт должен быть хеджевым","Error. Account must be hedge"));
      return INIT_FAILED;
     }
//--- 设置全局变量
   prefix=MQLInfoString(MQL_PROGRAM_NAME)+"_";
   for(int i=0;i<TOTAL_BUTT;i++)
     {
      butt_data[i].name=prefix+EnumToString((ENUM_BUTTONS)i);
      butt_data[i].text=EnumToButtText((ENUM_BUTTONS)i);
     }
   lot=NormalizeLot(Symbol(),fmax(InpLots,MinimumLots(Symbol())*2.0));
   magic_number=InpMagic;
   stoploss=InpStopLoss;
   takeprofit=InpTakeProfit;
   distance_pending=InpDistance;
   distance_stoplimit=InpDistanceSL;
   slippage=InpSlippage;
//--- 创建按钮
   if(!CreateButtons())
      return INIT_FAILED;
//--- 设置交易参数
   trade.SetDeviationInPoints(slippage);
   trade.SetExpertMagicNumber(magic_number);
   trade.SetTypeFillingBySymbol(Symbol());
   trade.SetMarginMode();
   trade.LogLevel(LOG_LEVEL_NO);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| 智能系统逆初始化函数                                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- 删除对象
   ObjectsDeleteAll(0,prefix);
  }
//+------------------------------------------------------------------+
//| 智能系统即时报价函数                                                 |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   if(MQLInfoInteger(MQL_TESTER))
      engine.OnTimer();
   int total=ObjectsTotal(0);
   for(int i=0;i<total;i++)
     {
      string obj_name=ObjectName(0,i);
      if(StringFind(obj_name,prefix+"BUTT_")<0)
         continue;
      PressButtonEvents(obj_name);
     }
   if(engine.TradeEventCode()!=TRADE_EVENT_FLAG_NO_EVENT)
     {
      
      Print(DFUN,EnumToString((ENUM_TRADE_EVENT_FLAGS)engine.TradeEventCode()));
     }
   engine.TradeEventCode();
  }
//+------------------------------------------------------------------+
//| 计时器函数                                                          |
//+------------------------------------------------------------------+
void OnTimer()
  {
   if(!MQLInfoInteger(MQL_TESTER))
      engine.OnTimer();
  }
//+------------------------------------------------------------------+
//| ChartEvent 函数                                                    |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
   if(MQLInfoInteger(MQL_TESTER))
      return;
   if(id==CHARTEVENT_OBJECT_CLICK && StringFind(sparam,"BUTT_")>0)
     {
      PressButtonEvents(sparam);
     }  
  }
//+------------------------------------------------------------------+
//| 创建按钮面板                                                         |
//+------------------------------------------------------------------+
bool CreateButtons(void)
  {
   int h=18,w=84,offset=10;
   int cx=offset,cy=offset+(h+1)*(TOTAL_BUTT/2)+h+1;
   int x=cx,y=cy;
   int shift=0;
   for(int i=0;i<TOTAL_BUTT;i++)
     {
      x=x+(i==7 ? w+2 : 0);
      if(i==TOTAL_BUTT-2) x=cx;
      y=(cy-(i-(i>6 ? 7 : 0))*(h+1));
      if(!ButtonCreate(butt_data[i].name,x,y,(i<TOTAL_BUTT-2 ? w : w*2+2),h,butt_data[i].text,(i<4 ? clrGreen : i>6 && i<11 ? clrRed : clrBlue)))
        {
         Alert(TextByLanguage("Не удалось создать кнопку \"","Could not create button \""),butt_data[i].text);
         return false;
        }
     }
   ChartRedraw(0);
   return true;
  }
//+------------------------------------------------------------------+
//| 创建按钮                                                           |
//+------------------------------------------------------------------+
bool ButtonCreate(const string name,const int x,const int y,const int w,const int h,const string text,const color clr,const string font="Calibri",const int font_size=8)
  {
   if(ObjectFind(0,name)<0)
     {
      if(!ObjectCreate(0,name,OBJ_BUTTON,0,0,0)) 
        { 
         Print(DFUN,TextByLanguage("не удалось создать кнопку! Код ошибки=","Could not create button! Error code="),GetLastError()); 
         return false; 
        } 
      ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
      ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
      ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
      ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
      ObjectSetInteger(0,name,OBJPROP_XSIZE,w);
      ObjectSetInteger(0,name,OBJPROP_YSIZE,h);
      ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_LOWER);
      ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_LEFT_LOWER);
      ObjectSetInteger(0,name,OBJPROP_FONTSIZE,font_size);
      ObjectSetString(0,name,OBJPROP_FONT,font);
      ObjectSetString(0,name,OBJPROP_TEXT,text);
      ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
      ObjectSetString(0,name,OBJPROP_TOOLTIP,"\n");
      ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,clrGray);
      return true;
     }
   return false;
  }
//+------------------------------------------------------------------+
//| 返回按钮状态                                                         |
//+------------------------------------------------------------------+
bool ButtonState(const string name)
  {
   return (bool)ObjectGetInteger(0,name,OBJPROP_STATE);
  }
//+------------------------------------------------------------------+
//| 设置按钮状态                                                         |
//+------------------------------------------------------------------+
void ButtonState(const string name,const bool state)
  {
   ObjectSetInteger(0,name,OBJPROP_STATE,state);
  }
//+------------------------------------------------------------------+
//| 将枚举转换为按钮文本                                                 |
//+------------------------------------------------------------------+
string EnumToButtText(const ENUM_BUTTONS member)
  {
   string txt=StringSubstr(EnumToString(member),5);
   StringToLower(txt);
   StringReplace(txt,"buy","Buy");
   StringReplace(txt,"sell","Sell");
   StringReplace(txt,"_limit"," Limit");
   StringReplace(txt,"_stop"," Stop");
   StringReplace(txt,"close_","Close ");
   StringReplace(txt,"2"," 1/2");
   StringReplace(txt,"_by_"," by ");
   StringReplace(txt,"profit_","Profit ");
   return txt;
  }
//+------------------------------------------------------------------+
//| 处理按钮被按下                                                      |
//+------------------------------------------------------------------+
void PressButtonEvents(const string button_name)
  {
   //--- 将按钮名称转换为其字符串 ID
   string button=StringSubstr(button_name,StringLen(prefix));
   //--- 如果按钮被按下
   if(ButtonState(button_name))
     {
      //--- 如果 BUTT_BUY 按钮被按下:开多仓
      if(button==EnumToString(BUTT_BUY))
        {
         //--- 获取相对于 StopLevel 的正确止损和止盈价位
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY,0,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY,0,takeprofit);
         //--- 开多仓
         trade.Buy(NormalizeLot(Symbol(),lot),Symbol(),0,sl,tp);
        }
      //--- 如果 BUTT_BUY_LIMIT 按钮被按下: 设置 BuyLimit
      else if(button==EnumToString(BUTT_BUY_LIMIT))
        {
         //--- 获取相对于 StopLevel 的正确下单价位
         double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_LIMIT,distance_pending);
         //--- 参考 StopLevel,获取相对于订单放置价位的正确止损和止盈价位
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY_LIMIT,price_set,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY_LIMIT,price_set,takeprofit);
         //--- 设置 BuyLimit 挂单
         trade.BuyLimit(lot,price_set,Symbol(),sl,tp);
        }
      //--- 如果 BUTT_BUY_STOP 按钮被按下: 设置 BuyStop
      else if(button==EnumToString(BUTT_BUY_STOP))
        {
         //--- 获取相对于 StopLevel 的正确下单价位
         double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_STOP,distance_pending);
         //--- 参考 StopLevel,获取相对于订单放置价位的正确止损和止盈价位
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY_STOP,price_set,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY_STOP,price_set,takeprofit);
         //--- 设置 BuyStop 挂单
         trade.BuyStop(lot,price_set,Symbol(),sl,tp);
        }
      //--- 如果 BUTT_BUY_STOP_LIMIT 按钮被按下: 设置 BuyStopLimit
      else if(button==EnumToString(BUTT_BUY_STOP_LIMIT))
        {
         //--- 获取相对于 StopLevel 的正确 BuyStop 下单价位
         double price_set_stop=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_STOP,distance_pending);
         //--- 参考 StopLevel,计算相对于 BuyStop 价位的 BuyLimit 挂单价位
         double price_set_limit=CorrectPricePending(Symbol(),ORDER_TYPE_BUY_LIMIT,distance_stoplimit,price_set_stop);
         //--- 参考 StopLevel,获取相对于订单放置价位的正确止损和止盈价位
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_BUY_STOP,price_set_limit,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_BUY_STOP,price_set_limit,takeprofit);
         //--- 设置 BuyStopLimit 挂单
         trade.OrderOpen(Symbol(),ORDER_TYPE_BUY_STOP_LIMIT,lot,price_set_limit,price_set_stop,sl,tp);
        }
      //--- 如果 BUTT_SELL 按钮被按下: 开空仓
      else if(button==EnumToString(BUTT_SELL))
        {
         //--- 获取相对于 StopLevel 的正确止损和止盈价位
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL,0,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL,0,takeprofit);
         //--- 开空仓
         trade.Sell(lot,Symbol(),0,sl,tp);
        }
      //--- 如果 BUTT_SELL_LIMIT 按钮被按下: 设置 SellLimit
      else if(button==EnumToString(BUTT_SELL_LIMIT))
        {
         //--- 获取相对于 StopLevel 的正确下单价位
         double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_LIMIT,distance_pending);
         //--- 参考 StopLevel,获取相对于订单放置价位的正确止损和止盈价位
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL_LIMIT,price_set,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL_LIMIT,price_set,takeprofit);
         //--- 设置 SellLimit 挂单
         trade.SellLimit(lot,price_set,Symbol(),sl,tp);
        }
      //--- 如果 BUTT_SELL_STOP 按钮被按下: 设置 SellStop
      else if(button==EnumToString(BUTT_SELL_STOP))
        {
         //--- 获取相对于 StopLevel 的正确下单价位
         double price_set=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_STOP,distance_pending);
         //--- 参考 StopLevel,获取相对于订单放置价位的正确止损和止盈价位
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL_STOP,price_set,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL_STOP,price_set,takeprofit);
         //--- 设置 SellStop 挂单
         trade.SellStop(lot,price_set,Symbol(),sl,tp);
        }
      //--- 如果 BUTT_SELL_STOP_LIMIT 按钮被按下: 设置 SellStopLimit
      else if(button==EnumToString(BUTT_SELL_STOP_LIMIT))
        {
         //--- 获取相对于 StopLevel 的正确放置 SellStop 挂单价位
         double price_set_stop=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_STOP,distance_pending);
         //--- 参考 StopLevel,计算相对于 SellStop 价位的 SellLimit 挂单价位
         double price_set_limit=CorrectPricePending(Symbol(),ORDER_TYPE_SELL_LIMIT,distance_stoplimit,price_set_stop);
         //--- 参考 StopLevel,获取相对于订单放置价位的正确止损和止盈价位
         double sl=CorrectStopLoss(Symbol(),ORDER_TYPE_SELL_STOP,price_set_limit,stoploss);
         double tp=CorrectTakeProfit(Symbol(),ORDER_TYPE_SELL_STOP,price_set_limit,takeprofit);
         //--- 设置 SellStopLimit 挂单
         trade.OrderOpen(Symbol(),ORDER_TYPE_SELL_STOP_LIMIT,lot,price_set_limit,price_set_stop,sl,tp);
        }
      //--- 如果 BUTT_CLOSE_BUY 按钮被按下:平盈利最大的多仓
      else if(button==EnumToString(BUTT_CLOSE_BUY))
        {
         //--- 获取所有持仓的清单
         CArrayObj* list=engine.GetListMarketPosition();
         //--- 仅从列表中选择多仓
         list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL);
         //--- 考虑佣金和掉期,按利润对列表进行排序
         list.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- 获取最大利润的多仓索引
         int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL);
         if(index>WRONG_VALUE)
           {
            COrder* position=list.At(index);
            if(position!=NULL)
              {
               //--- 获取多仓凭证,并按凭证平仓
               trade.PositionClose(position.Ticket());
              }
           }
        }
      //--- 如果 BUTT_CLOSE_BUY2 按钮被按下: 盈利最大的多仓平一半仓位
      else if(button==EnumToString(BUTT_CLOSE_BUY2))
        {
         //--- 获取所有持仓的清单
         CArrayObj* list=engine.GetListMarketPosition();
         //--- 仅从列表中选择多仓
         list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL);
         //--- 考虑佣金和掉期,按利润对列表进行排序
         list.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- 获取最大利润的多仓索引
         int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL);
         if(index>WRONG_VALUE)
           {
            COrder* position=list.At(index);
            if(position!=NULL)
              {
               //--- 计算平仓交易量,并通过凭证平一半多仓
               trade.PositionClosePartial(position.Ticket(),NormalizeLot(position.Symbol(),position.VolumeCurrent()/2.0));
              }
           }
        }
      //--- 如果 BUTT_CLOSE_BUY_BY_SELL 按钮被按下:平盈利最大的空仓
      else if(button==EnumToString(BUTT_CLOSE_BUY_BY_SELL))
        {
         //--- 获取所有持仓的清单
         CArrayObj* list_buy=engine.GetListMarketPosition();
         //--- 仅从列表中选择多仓
         list_buy=CSelect::ByOrderProperty(list_buy,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL);
         //--- 考虑佣金和掉期,按利润对列表进行排序
         list_buy.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- 获取最大利润的多仓索引
         int index_buy=CSelect::FindOrderMax(list_buy,ORDER_PROP_PROFIT_FULL);
         //--- 获取所有持仓的清单
         CArrayObj* list_sell=engine.GetListMarketPosition();
         //--- 仅从列表中选择空头仓位
         list_sell=CSelect::ByOrderProperty(list_sell,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL);
         //--- 考虑佣金和掉期,按利润对列表进行排序
         list_sell.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- 获取最大利润的空仓索引
         int index_sell=CSelect::FindOrderMax(list_sell,ORDER_PROP_PROFIT_FULL);
         if(index_buy>WRONG_VALUE && index_sell>WRONG_VALUE)
           {
            //--- 选择最大利润的多仓
            COrder* position_buy=list_buy.At(index_buy);
            //--- 选择最大利润的空仓
            COrder* position_sell=list_sell.At(index_sell);
            if(position_buy!=NULL && position_sell!=NULL)
              {
               //--- 由逆向空仓平多仓
               trade.PositionCloseBy(position_buy.Ticket(),position_sell.Ticket());
              }
           }
        }
      //--- 如果 BUTT_CLOSE_SELL 按钮被按下:平盈利最大的空仓
      else if(button==EnumToString(BUTT_CLOSE_SELL))
        {
         //--- 获取所有持仓的清单
         CArrayObj* list=engine.GetListMarketPosition();
         //--- 仅从列表中选择空头仓位
         list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL);
         //--- 考虑佣金和掉期,按利润对列表进行排序
         list.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- 获取最大利润的空仓索引
         int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL);
         if(index>WRONG_VALUE)
           {
            COrder* position=list.At(index);
            if(position!=NULL)
              {
               //--- 获取空头仓位凭证,并按凭证平仓
               trade.PositionClose(position.Ticket());
              }
           }
        }
      //--- 如果 BUTT_CLOSE_SELL2 按钮被按下: 盈利最大的空头仓位平一半仓位
      else if(button==EnumToString(BUTT_CLOSE_SELL2))
        {
         //--- 获取所有持仓的清单
         CArrayObj* list=engine.GetListMarketPosition();
         //--- 仅从列表中选择空头仓位
         list=CSelect::ByOrderProperty(list,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL);
         //--- 考虑佣金和掉期,按利润对列表进行排序
         list.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- 获取最大利润的空仓索引
         int index=CSelect::FindOrderMax(list,ORDER_PROP_PROFIT_FULL);
         if(index>WRONG_VALUE)
           {
            COrder* position=list.At(index);
            if(position!=NULL)
              {
               //--- 计算平仓交易量,并通过凭证平一半空头仓位
               trade.PositionClosePartial(position.Ticket(),NormalizeLot(position.Symbol(),position.VolumeCurrent()/2.0));
              }
           }
        }
      //--- 如果 BUTT_CLOSE_SELL_BY_BUY 按钮被按下:由最大盈利的多仓盈利平最大盈利的空仓
      else if(button==EnumToString(BUTT_CLOSE_SELL_BY_BUY))
        {
         //--- 获取所有持仓的清单
         CArrayObj* list_sell=engine.GetListMarketPosition();
         //--- 仅从列表中选择空头仓位
         list_sell=CSelect::ByOrderProperty(list_sell,ORDER_PROP_TYPE,POSITION_TYPE_SELL,EQUAL);
         //--- 考虑佣金和掉期,按利润对列表进行排序
         list_sell.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- 获取最大利润的空仓索引
         int index_sell=CSelect::FindOrderMax(list_sell,ORDER_PROP_PROFIT_FULL);
         //--- 获取所有持仓的清单
         CArrayObj* list_buy=engine.GetListMarketPosition();
         //--- 仅从列表中选择多仓
         list_buy=CSelect::ByOrderProperty(list_buy,ORDER_PROP_TYPE,POSITION_TYPE_BUY,EQUAL);
         //--- 考虑佣金和掉期,按利润对列表进行排序
         list_buy.Sort(SORT_BY_ORDER_PROFIT_FULL);
         //--- 获取最大利润的多仓索引
         int index_buy=CSelect::FindOrderMax(list_buy,ORDER_PROP_PROFIT_FULL);
         if(index_sell>WRONG_VALUE && index_buy>WRONG_VALUE)
           {
            //--- 选择最大利润的空仓
            COrder* position_sell=list_sell.At(index_sell);
            //--- 选择最大利润的多仓
            COrder* position_buy=list_buy.At(index_buy);
            if(position_sell!=NULL && position_buy!=NULL)
              {
               //--- 由逆向多仓平空仓
               trade.PositionCloseBy(position_sell.Ticket(),position_buy.Ticket());
              }
           }
        }
      //--- 如果 BUTT_CLOSE_ALL 被按下: 从利润最少的那个开始平所有持仓
      else if(button==EnumToString(BUTT_CLOSE_ALL))
        {
         //--- 获取所有持仓的清单
         CArrayObj* list=engine.GetListMarketPosition();
         if(list!=NULL)
           {
            //--- 考虑佣金和掉期,按利润对列表进行排序
            list.Sort(SORT_BY_ORDER_PROFIT_FULL);
            int total=list.Total();
            //--- 自利润最少的仓位循环
            for(int i=0;i<total;i++)
              {
               COrder* position=list.At(i);
               if(position==NULL)
                  continue;
               //--- 按其凭证平仓
               trade.PositionClose(position.Ticket());
              }
           }
        }
      //--- 如果 BUTT_PROFIT_WITHDRAWAL 按钮被按下: 从账户里出金
      if(button==EnumToString(BUTT_PROFIT_WITHDRAWAL))
        {
         //--- 如果程序在测试器中启动
         if(MQLInfoInteger(MQL_TESTER))
           {
            //--- 模拟出金
            TesterWithdrawal(withdrawal);
           }
        }
      //--- 等待 1/10 秒
      Sleep(100);
      //--- 按钮 “取消按下”并重绘图表
      ButtonState(button_name,false);
      ChartRedraw();
     }
  }
//+------------------------------------------------------------------+

代码看起来很冗长... 不过,对于最终用户,这仅仅是一个开始,因为一切均需要简化。 我们在这里涉及的大多数动作都会隐藏在函数库代码中,同时引入更多与函数库交互的用户友好机制。

我们在测试器中启动 EA 并尝试按钮:

全部正确激活,日志接收有关发生事件的消息。

目前,最后一个事件总是固定的。 换句话说,如果我们将多个仓位同时平仓,那么只有其中的最后一笔仓位才会在事件里发现自身。 可以通过历史当中的新成交或订单的数量来跟踪大规模平仓。 然后可以通过它们的编号获得所有新平仓的清单,并定义它们的完整集合。 我们为此开发一个单独的事件集合类。 它能令我们持续访问程序中所有发生的事件。

目前,测试器日志中的所有事件消息都显示在函数库基础对象的 CEngine::WorkWithHedgeCollections() 方法中,我们需要自定义程序知道事件代码以便“理解”帐户上发生的事情。 这将允许我们根据事件形成程序的响应逻辑。 为了测试实现这一目标的能力,我们将在函数库的基础对象中创建两个方法。 一个方法是存储最后一个事件的代码,而另一个方法是解码由一组事件标志组成的代码。
在下一篇文章中,我们将创建一个完整的类来处理帐户事件。

在 CEngine 类的实体中,定义事件代码的解码和设置帐户交易事件代码的方法检查事件代码中是否存在事件标志的方法,从调用程序里接收最后交易事件的方法,以及重置最后交易事件值的方法

//+------------------------------------------------------------------+
//| 函数库基类                                                          |
//+------------------------------------------------------------------+
class CEngine : public CObject
  {
private:
   CHistoryCollection   m_history;                       // 历史订单和成交集合
   CMarketCollection    m_market;                        // 在场订单和成交集合
   CArrayObj            m_list_counters;                 // 计时器列表计数器
   bool                 m_first_start;                   // 首次启动标志
   bool                 m_is_hedge;                      // 对冲账户标志
   bool                 m_is_market_trade_event;         // 帐户交易事件标志
   bool                 m_is_history_trade_event;        // 帐户历史交易事件标志
   int                  m_trade_event_code;              // 帐户交易事件状态代码
   ENUM_TRADE_EVENT     m_acc_trade_event;               // 帐户交易事件
//--- 解码事件代码,并在帐户上设置交易事件
   void                 SetTradeEvent(void);
//--- 按 id 返回计数器索引
   int                  CounterIndex(const int id) const;
//--- 返回(1)首次启动标志,(2)交易事件中存在标志
   bool                 IsFirstStart(void);
   bool                 IsTradeEventFlag(const int event_code)    const { return (this.m_trade_event_code&event_code)==event_code;  }
//--- 使用(1)对冲,(2)净持集合
   void                 WorkWithHedgeCollections(void);
   void                 WorkWithNettoCollections(void);
//--- 返回最后一笔(1)在场挂单,(2)在价订单,(3)最后一笔仓位,(4)按凭单的仓位
   COrder*              GetLastMarketPending(void);
   COrder*              GetLastMarketOrder(void);
   COrder*              GetLastPosition(void);
   COrder*              GetPosition(const ulong ticket);
//--- 返回最后一笔(1)删除的挂单,(2)历史订单,(3)按凭单的历史订单
   COrder*              GetLastHistoryPending(void);
   COrder*              GetLastHistoryOrder(void);
   COrder*              GetHistoryOrder(const ulong ticket);
//--- 从所有仓位订单列表中返回(1)第一笔,和(2)最后的历史订单,(3)最后一笔成交
   COrder*              GetFirstOrderPosition(const ulong position_id);
   COrder*              GetLastOrderPosition(const ulong position_id);
   COrder*              GetLastDeal(void);
public:
   //--- 返回在场(1)仓位,(2)挂单,和(3)在场订单清单
   CArrayObj*           GetListMarketPosition(void);
   CArrayObj*           GetListMarketPendings(void);
   CArrayObj*           GetListMarketOrders(void);
   //--- 返回历史清单(1)订单,(2)删除挂单,(3)成交,(4)所有仓位的订单 ID
   CArrayObj*           GetListHistoryOrders(void);
   CArrayObj*           GetListHistoryPendings(void);
   CArrayObj*           GetListHistoryDeals(void);
   CArrayObj*           GetListAllOrdersByPosID(const ulong position_id);
//--- 重置最后交易事件
   void                 ResetLastTradeEvent(void)                       { this.m_acc_trade_event=TRADE_EVENT_NO_EVENT;  }
//--- 返回(1)最后交易事件,(2)交易事件代码,(3)对冲账户标志
   ENUM_TRADE_EVENT     LastTradeEvent(void)                      const { return this.m_acc_trade_event;                }
   int                  TradeEventCode(void)                      const { return this.m_trade_event_code;               }
   bool                 IsHedge(void)                             const { return this.m_is_hedge;                       }
//--- 创建计时器计数器
   void                 CreateCounter(const int id,const ulong frequency,const ulong pause);
//--- 计时器
   void                 OnTimer(void);
//--- 构造函数/析构函数
                        CEngine();
                       ~CEngine();
  };
//+------------------------------------------------------------------+

在类的实体之外,编写解码交易事件的方法(将所有说明直接写入代码中):

//+------------------------------------------------------------------+
//| 解码事件代码,并设置交易事件                                           |
//+------------------------------------------------------------------+
void CEngine::SetTradeEvent(void)
  {
//--- 无交易事件 Exit
   if(this.m_trade_event_code==TRADE_EVENT_FLAG_NO_EVENT)
      return;
//--- 设置挂单(检查事件代码是否匹配,因为此处只能有一个标志)
   if(this.m_trade_event_code==TRADE_EVENT_FLAG_ORDER_PLASED)
     {
      this.m_acc_trade_event=TRADE_EVENT_PENDING_ORDER_PLASED;
      Print(DFUN,"Code=",this.m_trade_event_code,", m_acc_trade_event=",EnumToString(m_acc_trade_event));
      return;
     }
//--- 删除挂单(检查事件代码是否匹配,因为此处只能有一个标志)
   if(this.m_trade_event_code==TRADE_EVENT_FLAG_ORDER_REMOVED)
     {
      this.m_acc_trade_event=TRADE_EVENT_PENDING_ORDER_REMOVED;
      Print(DFUN,"Code=",this.m_trade_event_code,", m_acc_trade_event=",EnumToString(m_acc_trade_event));
      return;
     }
//--- 开仓(检查事件代码中的多个标志)
   if(this.IsTradeEventFlag(TRADE_EVENT_FLAG_POSITION_OPENED))
     {
      //--- 如果此为由价格激活挂单
      if(this.IsTradeEventFlag(TRADE_EVENT_FLAG_ORDER_ACTIVATED))
        {
         this.m_acc_trade_event=(!this.IsTradeEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_PENDING_ORDER_ACTIVATED : TRADE_EVENT_PENDING_ORDER_ACTIVATED_PARTIAL);
         Print(DFUN,"Code=",this.m_trade_event_code,", m_acc_trade_event=",EnumToString(m_acc_trade_event));
         return;
        }
      this.m_acc_trade_event=(!this.IsTradeEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_OPENED : TRADE_EVENT_POSITION_OPENED_PARTIAL);
      Print(DFUN,"Code=",this.m_trade_event_code,", m_acc_trade_event=",EnumToString(m_acc_trade_event));
      return;
     }
//--- 平仓(检查事件代码中的多个标志)
   if(this.IsTradeEventFlag(TRADE_EVENT_FLAG_POSITION_CLOSED))
     {
      //--- 如果由止损平仓
      if(this.IsTradeEventFlag(TRADE_EVENT_FLAG_SL))
        {
         //--- 检查部分结束标志,并设置 “由止损平仓” 或 “由止损部分平仓” 交易事件
         this.m_acc_trade_event=(!this.IsTradeEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED_BY_SL : TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_SL);
         Print(DFUN,"Code=",this.m_trade_event_code,", m_acc_trade_event=",EnumToString(m_acc_trade_event));
         return;
        }
      //--- 如果由止盈平仓
      else if(this.IsTradeEventFlag(TRADE_EVENT_FLAG_TP))
        {
         //--- 检查部分结束标志,并设置 “由止盈平仓” 或 “由止盈部分平仓” 交易事件
         this.m_acc_trade_event=(!this.IsTradeEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED_BY_TP : TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_TP);
         Print(DFUN,"Code=",this.m_trade_event_code,", m_acc_trade_event=",EnumToString(m_acc_trade_event));
         return;
        }
      //--- 如果持仓由逆向仓位平仓
      else if(this.IsTradeEventFlag(TRADE_EVENT_FLAG_BY_POS))
        {
         //--- 检查部分结束标志,并设置 “由逆向仓位平仓” 或 “由逆向仓位部分平仓” 事件
         this.m_acc_trade_event=(!this.IsTradeEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED_BY_POS : TRADE_EVENT_POSITION_CLOSED_PARTIAL_BY_POS);
         Print(DFUN,"Code=",this.m_trade_event_code,", m_acc_trade_event=",EnumToString(m_acc_trade_event));
         return;
        }
      //--- 如果已平位
      else
        {
         //--- 检查部分结束标志,并设置 “平仓” 或 “部分平仓” 事件
         this.m_acc_trade_event=(!this.IsTradeEventFlag(TRADE_EVENT_FLAG_PARTIAL) ? TRADE_EVENT_POSITION_CLOSED : TRADE_EVENT_POSITION_CLOSED_PARTIAL);
         Print(DFUN,"Code=",this.m_trade_event_code,", m_acc_trade_event=",EnumToString(m_acc_trade_event));
         return;
        }
     }
//--- 账户余额操作(按成交类型声明事件)
   if(this.m_trade_event_code==TRADE_EVENT_FLAG_ACCOUNT_BALANCE)
     {
      //--- 初始化交易事件
      this.m_acc_trade_event=TRADE_EVENT_NO_EVENT;
      //--- 取最后一笔成交
      COrder* deal=this.GetLastDeal();
      if(deal!=NULL)
        {
         ENUM_DEAL_TYPE deal_type=(ENUM_DEAL_TYPE)deal.GetProperty(ORDER_PROP_TYPE);
         //--- 如果成交是余额操作
         if(deal_type==DEAL_TYPE_BALANCE)
           {
           //--- 检查成交利润,并设置事件(入金或出金)
            this.m_acc_trade_event=(deal.Profit()>0 ? TRADE_EVENT_ACCOUNT_BALANCE_REFILL : TRADE_EVENT_ACCOUNT_BALANCE_WITHDRAWAL);
           }
         //--- 其余的余额操作类型与从 DEAL_TYPE_CREDIT 开始的 ENUM_DEAL_TYPE 枚举匹配
         else if(deal_type>DEAL_TYPE_BALANCE)
           {
           //--- 设置事件
            this.m_acc_trade_event=(ENUM_TRADE_EVENT)deal_type;
           }
        }
      Print(DFUN,"Code=",this.m_trade_event_code,", m_acc_trade_event=",EnumToString(m_acc_trade_event));
      return;
     }
  }
//+------------------------------------------------------------------+

添加交易事件加密方法的调用,并在 WorkWithHedgeCollections() 方法的最末端删除日志中的事件描述显示,检查并创建交易事件代码:

//+------------------------------------------------------------------+
//| 检查交易事件(对冲)                                                 |
//+------------------------------------------------------------------+
void CEngine::WorkWithHedgeCollections(void)
  {
//--- 初始化交易事件代码和标志
   this.m_trade_event_code=TRADE_EVENT_FLAG_NO_EVENT;
   this.m_is_market_trade_event=false;
   this.m_is_history_trade_event=false;
//--- 更新列表 
   this.m_market.Refresh();
   this.m_history.Refresh();
//--- 首次启动时的动作
   if(this.IsFirstStart())
     {
      this.m_acc_trade_event=TRADE_EVENT_NO_EVENT;
      return;
     }
//--- 检查市场状态和帐户历史记录变化
   this.m_is_market_trade_event=this.m_market.IsTradeEvent();
   this.m_is_history_trade_event=this.m_history.IsTradeEvent();
//---
#ifdef __MQL4__

#else // MQL5
//--- 如果事件仅涉及在场订单和仓位
   if(this.m_is_market_trade_event && !this.m_is_history_trade_event)
     {
      //--- 如果挂单数量增加
      if(this.m_market.NewPendingOrders()>0)
        {
         //--- 添加挂单放置标记
         this.m_trade_event_code+=TRADE_EVENT_FLAG_ORDER_PLASED;
        }
      //--- 如果在场订单数量增加
      if(this.m_market.NewMarketOrders()>0)
        {
         //--- 不要添加事件标志
         //--- ...
        }
     }
   
//--- 如果事件仅涉及历史订单和成交
   else if(this.m_is_history_trade_event && !this.m_is_market_trade_event)
     {
      //--- 如果出现新的成交
      if(this.m_history.NewDeals()>0)
        {
         //--- 添加帐户余额事件的标志
         this.m_trade_event_code+=TRADE_EVENT_FLAG_ACCOUNT_BALANCE;
        }
     }
   
//--- 如果事件与在场和历史订单和仓位相关
   else if(this.m_is_market_trade_event && this.m_is_history_trade_event)
     {
      //--- 如果挂单数量减少,且没有新的成交出现
      if(this.m_market.NewPendingOrders()<0 && this.m_history.NewDeals()==0)
        {
         //--- 添加删除挂单的标志
         this.m_trade_event_code+=TRADE_EVENT_FLAG_ORDER_REMOVED;
        }
      
      //--- 如果有新的成交和新的历史订单
      if(this.m_history.NewDeals()>0 && this.m_history.NewOrders()>0)
        {
         //--- 取最后一笔成交
         COrder* deal=this.GetLastDeal();
         if(deal!=NULL)
           {
            //--- 如果是入场成交
            if(deal.GetProperty(ORDER_PROP_DEAL_ENTRY)==DEAL_ENTRY_IN)
              {
               //--- 添加开仓标志
               this.m_trade_event_code+=TRADE_EVENT_FLAG_POSITION_OPENED;
               //--- 如果挂单数量减少
               if(this.m_market.NewPendingOrders()<0)
                 {
                  //--- 添加挂单激活的标志
                  this.m_trade_event_code+=TRADE_EVENT_FLAG_ORDER_ACTIVATED;
                 }
               //--- 取订单的单据
               ulong order_ticket=deal.GetProperty(ORDER_PROP_DEAL_ORDER);
               COrder* order=this.GetHistoryOrder(order_ticket);
               if(order!=NULL)
                 {
                  //--- 如果当前订单交易量超过零
                  if(order.VolumeCurrent()>0)
                    {
                     //--- 添加部分执行标志
                     this.m_trade_event_code+=TRADE_EVENT_FLAG_PARTIAL;
                    }
                 }
              }
            
            //--- 如果是离场成交
            if(deal.GetProperty(ORDER_PROP_DEAL_ENTRY)==DEAL_ENTRY_OUT)
              {
               //--- 添加平仓标志
               this.m_trade_event_code+=TRADE_EVENT_FLAG_POSITION_CLOSED;
               //--- 取成交的单据
               ulong order_ticket=deal.GetProperty(ORDER_PROP_DEAL_ORDER);
               COrder* order=this.GetHistoryOrder(order_ticket);
               if(order!=NULL)
                 {
                  //--- 如果成交仓位仍然在场
                  COrder* pos=this.GetPosition(deal.PositionID());
                  if(pos!=NULL)
                    {
                     //--- 添加部分执行标志
                     this.m_trade_event_code+=TRADE_EVENT_FLAG_PARTIAL;
                    }
                  //--- 否则,如果仓位已完全平仓
                  else
                    {
                     //--- 如果订单含由止损平仓的标志
                     if(order.IsCloseByStopLoss())
                       {
                        //--- 添加由止损平仓的标志
                        this.m_trade_event_code+=TRADE_EVENT_FLAG_SL;
                       }
                     //--- 如果订单带由止盈平仓的标志
                     if(order.IsCloseByTakeProfit())
                       {
                        //--- 添加由止盈平仓标志
                        this.m_trade_event_code+=TRADE_EVENT_FLAG_TP;
                       }
                    }
                 }
              }
            
            //--- 如果由逆向平仓
            if(deal.GetProperty(ORDER_PROP_DEAL_ENTRY)==DEAL_ENTRY_OUT_BY)
              {
               //--- 添加平仓标志
               this.m_trade_event_code+=TRADE_EVENT_FLAG_POSITION_CLOSED;
               //--- 添加由逆向平仓的标志
               this.m_trade_event_code+=TRADE_EVENT_FLAG_BY_POS;
               //--- 取成交的订单
               ulong ticket_from=deal.GetProperty(ORDER_PROP_DEAL_ORDER);
               COrder* order=this.GetHistoryOrder(ticket_from);
               if(order!=NULL)
                 {
                  //--- 如果仓位仍然在场
                  COrder* pos=this.GetPosition(order.PositionID());
                  if(pos!=NULL)
                    {
                     //--- 添加部分执行标志
                     this.m_trade_event_code+=TRADE_EVENT_FLAG_PARTIAL;
                    }
                 }
              }
           //--- 最后一笔成交处理块模块结束
           }
        }
     }
#endif 
   this.SetTradeEvent();
  }
//+------------------------------------------------------------------+

因此,在 WorkWithHedgeCollections() 方法中创建事件代码之后,我们调用事件解码方法。 是什么妨碍我们立即将其解码? 关键点在于我们目前的解码方法是临时的。 需要检查解码过程。 在即将发表的文章中将创建一个完整的交易事件类。
SetTradeEvent() 方法定义交易事件,将其值写入 m_acc_trade_event 类成员变量,而 LastTradeEvent()ResetLastTradeEvent() 方法允许读取变量的值作为帐户上的最后一个交易事件,并在调用程序中重置它(类似于GetLastError())。

TestDoEasyPart04.mqh 测试 EA 的 OnTick() 处理程序中,添加字符串以便读取上一个交易事件,并将其显示在图表注释中:

//+------------------------------------------------------------------+
//| 智能系统即时报价函数                                                 |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   static ENUM_TRADE_EVENT last_event=WRONG_VALUE;
   if(MQLInfoInteger(MQL_TESTER))
      engine.OnTimer();
   int total=ObjectsTotal(0);
   for(int i=0;i<total;i++)
     {
      string obj_name=ObjectName(0,i);
      if(StringFind(obj_name,prefix+"BUTT_")<0)
         continue;
      PressButtonEvents(obj_name);
     }
   if(engine.LastTradeEvent()!=last_event)                                  
     {                                                                      
      Comment("\nLast trade event: ",EnumToString(engine.LastTradeEvent()));
      last_event=engine.LastTradeEvent();                                   
     }                                                                      
  }
//+------------------------------------------------------------------+

现在,如果您在测试器中运行此 EA,并单击按钮,那么来自 CEngine::SetTradeEvent() 方法的正在进行的交易事件将显示在日记中,而图表注释将显示 EA 调用来自函数库的 engine.LastTradeEvent() 方法收到的帐户上发生的最后一个事件的描述:


下一步是什么?

在下一篇文章中,我们将开发事件对象类和类似于订单和仓位集合的事件对象集合,并教导基本函数库对象,将事件发送给程序。

下面附有当前版本函数库的所有文件,以及测试 EA 文件,供您测试和下载。
在评论中留下您的问题、意见和建议。

返回目录

系列文章的前几篇:

第一部分
第二部分
第三部分

本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/5724

附加的文件 |
MQL5.zip (44.67 KB)
如何基于HTML和CSV报表可视化多币种交易历史 如何基于HTML和CSV报表可视化多币种交易历史

自推出以来,MetaTrader 5提供了多货币测试选项,也许交易者经常使用这个功能。然而,这种功能并不是万能的。本文介绍了几种基于HTML和CSV交易历史报告的图表图形绘制程序,多货币交易可以在多个子窗口以及使用动态切换命令的一个窗口中并行分析。

研究烛条分析技术(第四部分):形态分析器的更新和补充 研究烛条分析技术(第四部分):形态分析器的更新和补充

本文论述了形态分析器(Pattern Analyzer)应用程序的新版本。 此版本修复了已发现错误并提供了一些新功能,还改进了用户界面。 在新版本的开发过程中参考了上一篇文章中的意见和建议。 最终的应用程序会在本文中进行说明。

利用 MQL5 和 MQL4 实现的选择和导航工具: 把数据添加到图表中 利用 MQL5 和 MQL4 实现的选择和导航工具: 把数据添加到图表中

在本文中,我们将继续扩展实用程序的功能。这一次,我们将增加显示简化交易的数据的能力,特别是前一天的最高、最低价位,全年的最高、最低价位,开盘时间等。

MQL5.community - 用户手册 MQL5.community - 用户手册

如果你已经在本社区成功注册,那么你很可能会问:怎样在我发送的消息中插入图片?怎样格式化MQL5源代码?我的私信保存在哪?诸如此类的很多问题。本文我们为您准备了一些实用技巧,帮助你熟悉MQL5.community,并充分利用其提供的功能。