为莫斯科交易所开发一个交易机器人从哪里开始呢?

MetaQuotes | 8 七月, 2016

很多莫斯科交易所的交易者想把他们的交易算法自动化,但是他们不知道从何做起,MQL5语言提供了很大范围的交易功能,它还额外提供了方便使用的类,以使用户在算法交易中方便地起步。在本文中,我们将讨论MQL5交易策略语言所提供的本地化工具,而在莫斯科交易所(MOEX)交易的人们可以从中获益。

莫斯科交易所(MOEX)的两种类型的交易订单

  莫斯科交易所支持两种类型的交易订单: 市价与限价。

市价订单能够保证交易的执行(只要有足够的流动性就可以),但是它不能保证价格,这就是说所执行的订单价格与当时所要求的可能相差很大。而限价订单能够保证交易执行时的价格,但是它不能保证交易能够被执行,所以您的订单可能会无法执行。

莫斯科交易所经济商提供的所有其他类型的订单都是软件系统的一部分,通过它,交易者就能与交易所互动,换句话说,所有其他的订单都是通过计算所得的结果,这些订单在莫斯科交易所之外保存和处理,然后再经过内部处理后再以市价订单或者限价订单的方式发送给交易所。

MetaTrader 5 平台提供了以下类型的交易订单,可以在莫斯科交易所使用:

ID 描述 保存与执行

ORDER_TYPE_BUY

市价买入订单

订单以市价买入订单的方式,以可用的最佳卖出价格发送到交易所

ORDER_TYPE_SELL

市价卖出订单

订单以市价卖出订单的方式,以可用的最佳买入价格发送到交易所

ORDER_TYPE_BUY_LIMIT

限价买入挂单

订单以限价买入订单的方式发送到交易所。一旦市场上出现指定价格或者更好价格的卖出请求时,该订单就会被执行。

ORDER_TYPE_SELL_LIMIT

限价卖出挂单

订单以限价卖出订单的方式发送到交易所。一旦市场上出现指定价格或者更好价格的买入请求时,该订单就会被执行。

ORDER_TYPE_BUY_STOP

止损买入挂单

订单保存在 MetaTrader 5 服务器上。一旦订单被触发,就会发送到交易所:
  • 对于外汇和证券市场,它会以市价买入订单的形式发送,
  • 而对于期货产品,订单则是以限价买入订单的形式,以最差通道阈值价格发送。

ORDER_TYPE_SELL_STOP

止损卖出挂单

订单保存在 MetaTrader 5 服务器上。一旦订单被触发,就会发送到交易所:
  • 对于外汇和证券市场,它是以市价卖出订单的形式发送的,
  • 对于期货产品,订单则是以限价卖出订单的形式,以最差通道阈值价格发送。

ORDER_TYPE_BUY_STOP_LIMIT

止损买入挂单

订单保存在 MetaTrader 5 服务器上。一旦订单被触发,它就会向交易所以限价买入的形式发送订单

ORDER_TYPE_SELL_STOP_LIMIT

止损卖出挂单

订单保存在 MetaTrader 5 服务器上。一旦订单被触发,它就会向交易所以限价卖出的形式发送订单

MetaTrader 5 平台允许对所持仓位设置止损和获利水平,这些水平保存在 MetaTrader 5 交易服务器上并能自动触发,哪怕交易账户没有被连接上:

另外,MetaTrader 5 平台允许交易者设置挂单的止损/获利水平,以及修改所有挂单的触发价格水平。

MetaTrader 5 中的交易操作

MetaTrader 5 提供了交易操作的基本类型,您可能想在您的交易机器人中使用它们:

      MQL5 中所有的交易操作都是使用OrderSend()函数来实现的,当交易订单成功发送到莫斯科交易所之后,就会把程序的控制权返回给用户,订单状态(order state)就会改为 ORDER_STATE_PLACED, 但它不一定表示您的订单将被成功执行(那样的话,订单状态会变成 ORDER_STATE_FILLED 或者 ORDER_STATE_PARTIAL),您交易订单的执行结果是依赖于当前市场的,而且可能由于不同原因被莫斯科交易所拒绝 (订单状态为 ORDER_STATE_REJECTED)。

      还有一个异步版本的函数 — OrderSendAsync(), 它运行得比 OrderSend() 快得多, 因为它不会等待订单发送到交易所的交易系统中,这个函数在MetaTrader 5终端把订单发送到外面之后会立即返回,这就是说您的交易订单通过了终端的基本验证,现在已经发到MetaTrader 5交易服务器上进一步处理了,把您的订单加到交易所队列所需的时间,以及订单被执行或者拒绝的时间都依赖于交易所是否忙碌以及您网络连接的速度。

      MqlTradeRequest结构中描述了多种交易操作,其中包含了交易请求的描述,所以这里的主要任务就是正确填充 MqlTradeRequest 结构以及处理请求执行的结果。

      根据您的交易系统的规则,您可以以市场价格买入或者卖出(BUY 或者 SELL), 或者在距离当前价格一定距离设置买入/卖出的挂单:

      下图显示了使用止损买入(BUY STOP), 止损卖出(SELL STOP) 以及限价买入(BUY LIMIT), 限价卖出(SELL LIMIT) 的原则, 并指出从从市场深度(Market Depth)中如何下单。

      另外,您也许需要修改甚至删除挂单,这可以通过使用 OrderSend()/OrderSendAsync() 函数来做到。管理持有的仓位也是简单的,因为管理是通过交易操作进行的。

      在本文中,我们将向您展示如何使用 MQL5 编程进行买入和卖出, 并且我们还将演示如何操作交易账户和交易品种的属性,这可以通过使用标准库(Standard Library)中的交易类来完成。

      在交易所中使用机器人交易是很简单的

      MQL5 语言完全支持了 MetaTrader 5 平台的全部交易功能: 它包含了许多交易函数用于处理订单,仓位和交易请求。不论您交易何种资产,操作都是相同的: 期货,证券,期权,债券,等等.

      使用 MQL5 语言,您可以创建交易订单并将它发送到服务器上,使用的是OrderSend() 或者 OrderSendAsync() 函数,您也可以接收订单执行结果, 查看交易历史,检查资产的合约规格, 处理交易时间 以及访问其他有用的信息。

      对交易机器人开发者的重要提醒: 任何交易操作,包括建立仓位,设置止损或者获利,或者通过反向仓位平仓,都包含了在 MetaTrader 5 服务器上和莫斯科交易所的一系列处理。您可以通过在您的账户中运行TradeTransactionListener.mql5来查看这些过程。该 EA 侦听TradeTransaction事件并显示与此事件相关的简要信息:

      //+------------------------------------------------------------------+
      //|                                     TradeTransactionListener.mq5 |
      //|                        Copyright 2016, MetaQuotes Software Corp. |
      //|                                             https://www.mql5.com |
      //+------------------------------------------------------------------+
      #property copyright "Copyright 2016, MetaQuotes Software Corp."
      #property link      "https://www.mql5.com"
      #property version   "1.00"
      //+------------------------------------------------------------------+
      //| Expert initialization function                                   |
      //+------------------------------------------------------------------+
      int OnInit()
        {
      //---
         PrintFormat("最后一次 PING=%.f ms",
                     TerminalInfoInteger(TERMINAL_PING_LAST)/1000.);
      //---
         return(INIT_SUCCEEDED);
        }
      //+------------------------------------------------------------------+
      //| Expert tick function                                             |
      //+------------------------------------------------------------------+
      void OnTick()
        {
      //---
      
        }
      //+------------------------------------------------------------------+
      //| TradeTransaction function                                        |
      //+------------------------------------------------------------------+
      void OnTradeTransaction(const MqlTradeTransaction &trans,
                              const MqlTradeRequest &request,
                              const MqlTradeResult &result)
        {
      //---
         static int counter=0;   // OnTradeTransaction() 调用计数器
         static uint lasttime=0; // 最后调用 OnTradeTransaction() 的时间
      //---
         uint time=GetTickCount();
      //--- 如果最后一次事务是在1秒钟之前进行的,
         if(time-lasttime>1000)
           {
            counter=0; // 那么这就是一个新的交易操作,计数器要被重置
            if(IS_DEBUG_MODE)
               Print(" 新的交易操作");
           }
         lasttime=time;
         counter++;
         Print(counter,". ",__FUNCTION__);
      //--- 交易请求的执行结果
         ulong            lastOrderID   =trans.order;
         ENUM_ORDER_TYPE  lastOrderType =trans.order_type;
         ENUM_ORDER_STATE lastOrderState=trans.order_state;
      //--- 进行交易的交易品种名称
         string trans_symbol=trans.symbol;
      //--- 交易的类型
         ENUM_TRADE_TRANSACTION_TYPE  trans_type=trans.type;
         switch(trans.type)
           {
            case  TRADE_TRANSACTION_POSITION:   // 修改仓位
              {
               ulong pos_ID=trans.position;
               PrintFormat("MqlTradeTransaction: 修改了仓位 #%I64u %s: SL=%.5f TP=%.5f",
                           pos_ID,trans_symbol,trans.price_sl,trans.price_tp);
              }
            break;
            case TRADE_TRANSACTION_REQUEST:     // 发送交易请求
               PrintFormat("MqlTradeTransaction: TRADE_TRANSACTION_REQUEST");
               break;
            case TRADE_TRANSACTION_DEAL_ADD:    // 增加一项交易
              {
               ulong          lastDealID   =trans.deal;
               ENUM_DEAL_TYPE lastDealType =trans.deal_type;
               double        lastDealVolume=trans.volume;
               //--- 内部系统的交易 ID  - 莫斯科交易所赋予的编号
               string Exchange_ticket="";
               if(HistoryDealSelect(lastDealID))
                  Exchange_ticket=HistoryDealGetString(lastDealID,DEAL_EXTERNAL_ID);
               if(Exchange_ticket!="")
                  Exchange_ticket=StringFormat("(MOEX 交易=%s)",Exchange_ticket);
      
               PrintFormat("MqlTradeTransaction: %s 交易 #%I64u %s %s %.2f lot   %s",EnumToString(trans_type),
                           lastDealID,EnumToString(lastDealType),trans_symbol,lastDealVolume,Exchange_ticket);
              }
            break;
            case TRADE_TRANSACTION_HISTORY_ADD: // 向历史中增加一个订单
              {
               //--- 内部系统的订单 ID - 莫斯科交易所赋予的一个编号
               string Exchange_ticket="";
               if(lastOrderState==ORDER_STATE_FILLED)
                 {
                  if(HistoryOrderSelect(lastOrderID))
                     Exchange_ticket=HistoryOrderGetString(lastOrderID,ORDER_EXTERNAL_ID);
                  if(Exchange_ticket!="")
                     Exchange_ticket=StringFormat("(MOEX 订单编号=%s)",Exchange_ticket);
                 }
               PrintFormat("MqlTradeTransaction: %s 订单 #%I64u %s %s %s   %s",EnumToString(trans_type),
                           lastOrderID,EnumToString(lastOrderType),trans_symbol,EnumToString(lastOrderState),Exchange_ticket);
              }
            break;
            default: // 其它事务
              {
               //--- 内部系统的订单 ID - 莫斯科交易所赋予的一个编号
               string Exchange_ticket="";
               if(lastOrderState==ORDER_STATE_PLACED)
                 {
                  if(OrderSelect(lastOrderID))
                     Exchange_ticket=OrderGetString(ORDER_EXTERNAL_ID);
                  if(Exchange_ticket!="")
                     Exchange_ticket=StringFormat("MOEX 订单编号=%s",Exchange_ticket);
                 }
               PrintFormat("MqlTradeTransaction: %s 订单 #%I64u %s %s   %s",EnumToString(trans_type),
                           lastOrderID,EnumToString(lastOrderType),EnumToString(lastOrderState),Exchange_ticket);
              }
            break;
           }
      //--- 订单编号    
         ulong orderID_result=result.order;
         string retcode_result=GetRetcodeID(result.retcode);
         if(orderID_result!=0)
            PrintFormat("MqlTradeResult: 订单 #%d 返回代码=%s ",orderID_result,retcode_result);
      //---   
        }
      //+------------------------------------------------------------------+
      //| 把返回的数字代码转为字符串型的描述                                    |
      //+------------------------------------------------------------------+
      string GetRetcodeID(int retcode)
        {
         switch(retcode)
           {
            case 10004: return("TRADE_RETCODE_REQUOTE");             break;
            case 10006: return("TRADE_RETCODE_REJECT");              break;
            case 10007: return("TRADE_RETCODE_CANCEL");              break;
            case 10008: return("TRADE_RETCODE_PLACED");              break;
            case 10009: return("TRADE_RETCODE_DONE");                break;
            case 10010: return("TRADE_RETCODE_DONE_PARTIAL");        break;
            case 10011: return("TRADE_RETCODE_ERROR");               break;
            case 10012: return("TRADE_RETCODE_TIMEOUT");             break;
            case 10013: return("TRADE_RETCODE_INVALID");             break;
            case 10014: return("TRADE_RETCODE_INVALID_VOLUME");      break;
            case 10015: return("TRADE_RETCODE_INVALID_PRICE");       break;
            case 10016: return("TRADE_RETCODE_INVALID_STOPS");       break;
            case 10017: return("TRADE_RETCODE_TRADE_DISABLED");      break;
            case 10018: return("TRADE_RETCODE_MARKET_CLOSED");       break;
            case 10019: return("TRADE_RETCODE_NO_MONEY");            break;
            case 10020: return("TRADE_RETCODE_PRICE_CHANGED");       break;
            case 10021: return("TRADE_RETCODE_PRICE_OFF");           break;
            case 10022: return("TRADE_RETCODE_INVALID_EXPIRATION");  break;
            case 10023: return("TRADE_RETCODE_ORDER_CHANGED");       break;
            case 10024: return("TRADE_RETCODE_TOO_MANY_REQUESTS");   break;
            case 10025: return("TRADE_RETCODE_NO_CHANGES");          break;
            case 10026: return("TRADE_RETCODE_SERVER_DISABLES_AT");  break;
            case 10027: return("TRADE_RETCODE_CLIENT_DISABLES_AT");  break;
            case 10028: return("TRADE_RETCODE_LOCKED");              break;
            case 10029: return("TRADE_RETCODE_FROZEN");              break;
            case 10030: return("TRADE_RETCODE_INVALID_FILL");        break;
            case 10031: return("TRADE_RETCODE_CONNECTION");          break;
            case 10032: return("TRADE_RETCODE_ONLY_REAL");           break;
            case 10033: return("TRADE_RETCODE_LIMIT_ORDERS");        break;
            case 10034: return("TRADE_RETCODE_LIMIT_VOLUME");        break;
            case 10035: return("TRADE_RETCODE_INVALID_ORDER");       break;
            case 10036: return("TRADE_RETCODE_POSITION_CLOSED");     break;
            default:
               return("TRADE_RETCODE_UNKNOWN="+IntegerToString(retcode));
               break;
           }
      //---
        }
      //+------------------------------------------------------------------+
      
      

      侦听操作实例:

      2016.06.09 14:51:19.763 TradeTransactionListener (Si-6.16,M15)  最后一次 PING=14 ms
      买入
      2016.06.09 14:51:24.856 TradeTransactionListener (Si-6.16,M15)  1. OnTradeTransaction
      2016.06.09 14:51:24.856 TradeTransactionListener (Si-6.16,M15)  MqlTradeTransaction: TRADE_TRANSACTION_ORDER_ADD order #49118594 ORDER_TYPE_BUY ORDER_STATE_STARTED   
      2016.06.09 14:51:24.859 TradeTransactionListener (Si-6.16,M15)  2. OnTradeTransaction
      2016.06.09 14:51:24.859 TradeTransactionListener (Si-6.16,M15)  MqlTradeTransaction: TRADE_TRANSACTION_REQUEST
      2016.06.09 14:51:24.859 TradeTransactionListener (Si-6.16,M15)  MqlTradeResult: 订单 #49118594 返回代码=TRADE_RETCODE_PLACED 
      2016.06.09 14:51:24.859 TradeTransactionListener (Si-6.16,M15)  3. OnTradeTransaction
      2016.06.09 14:51:24.859 TradeTransactionListener (Si-6.16,M15)  MqlTradeTransaction: TRADE_TRANSACTION_ORDER_UPDATE 订单 #49118594 ORDER_TYPE_BUY ORDER_STATE_REQUEST_ADD   
      2016.06.09 14:51:24.881 TradeTransactionListener (Si-6.16,M15)  4. OnTradeTransaction
      2016.06.09 14:51:24.881 TradeTransactionListener (Si-6.16,M15)  MqlTradeTransaction: TRADE_TRANSACTION_ORDER_UPDATE 订单 #49118594 ORDER_TYPE_BUY ORDER_STATE_PLACED   
      2016.06.09 14:51:24.881 TradeTransactionListener (Si-6.16,M15)  5. OnTradeTransaction
      2016.06.09 14:51:24.881 TradeTransactionListener (Si-6.16,M15)  MqlTradeTransaction: TRADE_TRANSACTION_ORDER_DELETE 订单 #49118594 ORDER_TYPE_BUY ORDER_STATE_PLACED   
      2016.06.09 14:51:24.884 TradeTransactionListener (Si-6.16,M15)  6. OnTradeTransaction
      2016.06.09 14:51:24.884 TradeTransactionListener (Si-6.16,M15)  MqlTradeTransaction: TRADE_TRANSACTION_HISTORY_ADD order #49118594 ORDER_TYPE_BUY Si-6.16 ORDER_STATE_FILLED   (MOEX 订单编号=3377179723)
      2016.06.09 14:51:24.884 TradeTransactionListener (Si-6.16,M15)  7. OnTradeTransaction
      2016.06.09 14:51:24.885 TradeTransactionListener (Si-6.16,M15)  MqlTradeTransaction: TRADE_TRANSACTION_DEAL_ADD 交易 #6945344 DEAL_TYPE_BUY Si-6.16 1.00 lot   (MOEX 交易=185290434)
      Setting SL/TP
      2016.06.09 14:51:50.872 TradeTransactionListener (Si-6.16,M15)  1. OnTradeTransaction
      2016.06.09 14:51:50.872 TradeTransactionListener (Si-6.16,M15)  MqlTradeTransaction: TRADE_TRANSACTION_REQUEST
      2016.06.09 14:51:50.872 TradeTransactionListener (Si-6.16,M15)  2. OnTradeTransaction
      2016.06.09 14:51:50.872 TradeTransactionListener (Si-6.16,M15)  MqlTradeTransaction: 仓位  #0 Si-6.16 modified: SL=62000.00000 TP=67000.00000
      Closing the position (selling)
      2016.06.09 14:52:24.063 TradeTransactionListener (Si-6.16,M15)  1. OnTradeTransaction
      2016.06.09 14:52:24.063 TradeTransactionListener (Si-6.16,M15)  MqlTradeTransaction: TRADE_TRANSACTION_ORDER_ADD 订单 #49118750 ORDER_TYPE_SELL ORDER_STATE_STARTED   
      2016.06.09 14:52:24.067 TradeTransactionListener (Si-6.16,M15)  2. OnTradeTransaction
      2016.06.09 14:52:24.067 TradeTransactionListener (Si-6.16,M15)  MqlTradeTransaction: TRADE_TRANSACTION_REQUEST
      2016.06.09 14:52:24.067 TradeTransactionListener (Si-6.16,M15)  MqlTradeResult: 订单 #49118750 retcode=TRADE_RETCODE_PLACED 
      2016.06.09 14:52:24.067 TradeTransactionListener (Si-6.16,M15)  3. OnTradeTransaction
      2016.06.09 14:52:24.067 TradeTransactionListener (Si-6.16,M15)  MqlTradeTransaction: TRADE_TRANSACTION_ORDER_UPDATE 订单 #49118750 ORDER_TYPE_SELL ORDER_STATE_REQUEST_ADD   
      2016.06.09 14:52:24.071 TradeTransactionListener (Si-6.16,M15)  4. OnTradeTransaction
      2016.06.09 14:52:24.071 TradeTransactionListener (Si-6.16,M15)  MqlTradeTransaction: TRADE_TRANSACTION_ORDER_UPDATE 订单 #49118750 ORDER_TYPE_SELL ORDER_STATE_PLACED   
      2016.06.09 14:52:24.073 TradeTransactionListener (Si-6.16,M15)  5. OnTradeTransaction
      2016.06.09 14:52:24.073 TradeTransactionListener (Si-6.16,M15)  MqlTradeTransaction: TRADE_TRANSACTION_DEAL_ADD 交易 #6945378 DEAL_TYPE_SELL Si-6.16 1.00 lot   (MOEX deal=185290646)
      2016.06.09 14:52:24.075 TradeTransactionListener (Si-6.16,M15)  6. OnTradeTransaction
      2016.06.09 14:52:24.075 TradeTransactionListener (Si-6.16,M15)  MqlTradeTransaction: TRADE_TRANSACTION_ORDER_DELETE 订单 #49118750 ORDER_TYPE_SELL ORDER_STATE_PLACED   
      2016.06.09 14:52:24.077 TradeTransactionListener (Si-6.16,M15)  7. OnTradeTransaction
      2016.06.09 14:52:24.077 TradeTransactionListener (Si-6.16,M15)  MqlTradeTransaction: TRADE_TRANSACTION_HISTORY_ADD 订单 #49118750 ORDER_TYPE_SELL Si-6.16 ORDER_STATE_FILLED   (MOEX ticket=3377182821)

      现在是时候回顾源代码示例了。


      操作交易账户

      在交易机器人的最开始,需要取得交易机器人所要交易的账户的信息,

      账户的详细信息可以使用特定的CAccountInfo类,让我们在我们的代码中包含 AccountInfo.mqh 文件,并声明这个类的 account 变量:

      #include <Trade\AccountInfo.mqh>
      //+------------------------------------------------------------------+
      //| Script program start function                                    |
      //+------------------------------------------------------------------+
      void OnStart()
        {
      //--- 用于操作账户的对象
      CAccountInfo account;
      //--- 取得EA所运行的账户的编号
         long login=account.Login();
         Print("登录=",login);
      //--- 打印账户币别    
         Print("账户币别: ",account.Currency());   
      //--- 打印账户的余额和当前的利润
         Print("余额=",account.Balance(),"  利润=",account.Profit(),"   净值=",account.Equity());
      //--- 打印账户类型    
         Print("账户类型: ",account.TradeModeDescription());
      //--- 看帐户是否允许交易
         if(account.TradeAllowed())
            Print("允许在此账户交易。");
         else
            Print("不允许在此账户上交易: 也许是使用投资者密码连接中。");
      //--- 保证金计算模式
         Print("保证金计算模式: ",account.MarginModeDescription());
      //--- 检查是否允许在此账户上运行EA交易
         if(account.TradeExpert())
            Print("允许在此账户上自动交易。");
         else
            Print("不允许在此账户上使用EA交易或者脚本程序。");
      //--- 是否指定了允许的最大订单数量
         int orders_limit=account.LimitOrders();
         if(orders_limit!=0)Print("允许的最大挂单数量: ",orders_limit);
      //--- 打印公司名称和服务器的名称
         Print(account.Company(),": 服务器 ",account.Server());
         Print(__FUNCTION__,"  完毕。"); //---   
        }
      

      从以上的代码可以看出,通过使用在OnInit()函数中使用account变量,您能够获得许多有用的信息,您可以把这些代码加到您的EA交易中,在分析EA的操作时就更加容易分析记录了。

      下图显示了运行脚本的结果。



      取得金融资产的属性

      我们已经取得了账户的信息,然而,为了能够进行交易操作,我们还需要知道我们将要交易的金融资产的属性。为此,我们将使用另外一个方便的类CSymbolInfo,它含有很多方法,以下是其中一些特性的示例。

      #include<Trade\SymbolInfo.mqh>
      //+------------------------------------------------------------------+
      //| Script program start function                                    |
      //+------------------------------------------------------------------+
      void OnStart()
        {
      //--- 用于接收交易品种属性的对象
      CSymbolInfo symbol_info;
      //--- 设置我们需要取得信息的交易品种名称
         symbol_info.Name(_Symbol);
      //--- 取得当前报价并打印
         symbol_info.RefreshRates();
         Print(symbol_info.Name()," (",symbol_info.Description(),")",
               "  卖出价=",symbol_info.Bid(),"   买入价=",symbol_info.Ask());
      //--- 取得小数点位数以及点数大小
         Print("小数位数=",symbol_info.Digits(),
               ", 1点=",DoubleToString(symbol_info.Point(),symbol_info.Digits()));
      //--- 请求订单执行类型,检查限制
         Print("交易操作的限制: ",EnumToString(symbol_info.TradeMode()),
               " (",symbol_info.TradeModeDescription(),")");
      //--- 检查交易模式
         Print("交易执行模式: ",EnumToString(symbol_info.TradeExecution()),
               " (",symbol_info.TradeExecutionDescription(),")");
      //--- 检查合约计算方式
         Print("计算合约值: ",EnumToString(symbol_info.TradeCalcMode()),
               " (",symbol_info.TradeCalcModeDescription(),")");
      //--- 合约大小
         Print("标准合约大小: ",symbol_info.ContractSize());
      //--- 每一手合约的初始保证金
         Print("1标准合约的初始保证金: ",symbol_info.MarginInitial()," ",symbol_info.CurrencyBase());
      //--- 交易操作中的最小和最大交易量
         Print("交易量信息: 最小手数=",symbol_info.LotsMin(),"  最大手数=",symbol_info.LotsMax(),
               "  手数步长=",symbol_info.LotsStep());
      //--- 
         Print(__FUNCTION__,"  完毕。");   
        }
      

      下图显示了在莫斯科FORTS市场上交易品种 Si-6.16 的属性。现在您可以准备转到交易了。



      交易操作的编程

      交易订单是通过使用以下两个 MQL5 函数来发送的: OrderSend() 以及 OrderSendAsync()。事实上,它们是相同功能的两种实现方式,OrderSend() 发送交易订单并等待执行结果,而异步的 OrderSendAsync() 函数只是发送请求,并允许程序不用等待交易服务器的回应就继续运行。所以使用 MQL5 交易确实简单, 您只要使用一个函数就能进行所有的交易操作。

      这两个函数都使用MqlTradeRequest结构作为第一个参数,其中包含了多个栏位,所必需的栏位依赖于交易操作类型(trading operation type), 所以不是所有的栏位都需要填充。如果数值不正确或者所需栏位没有填充,请求在终端的验证就会失败,而不会发送到服务器上。其中有五个栏位,您需要从预先定义好的枚举中指定。

      交易请求包含了很多栏位,是因为您需要描述订单的很多属性,而且它们会根据执行原则,过期设置和其他参数而有所变化,不需要记住所有这些细节,只需要使用准备好的CTrade类,这里就是交易机器人中如何使用这个类的示例:

      #include<Trade\Trade.mqh>
      //--- 进行交易操作的对象
      CTrade  trade;
      //+------------------------------------------------------------------+
      //| Expert initialization function                                   |
      //+------------------------------------------------------------------+
      int OnInit()
        {
      //--- 设置用于标识EA订单的幻数
         int MagicNumber=123456;
         trade.SetExpertMagicNumber(MagicNumber);
      //--- 设置在买入/卖出时所允许的滑点点数
         int deviation=10;
         trade.SetDeviationInPoints(deviation);
      //--- 订单执行模式, 使用服务器允许的模式
         trade.SetTypeFilling(ORDER_FILLING_RETURN);
      //--- 记录模式: 建议不用调用此方法,因为类会自己设置合适的模式
         trade.LogLevel(1); 
      //--- 交易中使用的函数: true - OrderSendAsync(), false - OrderSend()
         trade.SetAsyncMode(true);
      //---
         return(0);
        }
      

      作为一项规则,莫斯科交易所使用的是ORDER_FILLING_RETURN模式,语言参考中的描述:

      这种模式只是用于市价交易和交易所交易模式: 用于市价订单 (ORDER_TYPE_BUY 以及 ORDER_TYPE_SELL), 限价及止损限价订单 (ORDER_TYPE_BUY_LIMIT, ORDER_TYPE_SELL_LIMIT, ORDER_TYPE_BUY_STOP_LIMIT 和 ORDER_TYPE_SELL_STOP_LIMIT。如果有部分执行,市价单或者还有剩余部分的限价订单不会被取消,还会放到未来进行处理。

      在 ORDER_TYPE_BUY_STOP_LIMIT 和 ORDER_TYPE_SELL_STOP_LIMIT 类型的订单被激活时,会产生 ORDER_FILLING_RETURN 类型的对应的 ORDER_TYPE_BUY_LIMIT/ORDER_TYPE_SELL_LIMIT 订单。

      现在,是时候讨论 CTrade 是如何在交易操作中运用的了。

      以当前价格买入/卖出

      在交易策略中,我们经常需要以当前价格立即买入或者卖出某项资产,CTrade 类熟悉这种状况,它只需要交易操作所需的交易量是多少,其他的所有参数,包括建仓价格,交易品种名称,止损和获利水平,以及订单的注视都可以忽略。

      //--- 1. 一个买入当前交易品种的例子
         if(!trade.Buy(1))
           {
            //--- 报告失败
            Print("Buy() 方法失败. 返回代码=",trade.ResultRetcode(),
                  ". 代码描述: ",trade.ResultRetcodeDescription());
           }
         else
           {
            Print("The Buy() method has been successfully performed. 返回代码=",trade.ResultRetcode(),
                  " (",trade.ResultRetcodeDescription(),")");
           }
      

      默认情况下,如果不指定交易品种名称,CTrade 就将使用所运行图表的交易品种名称,这对简单的策略很好,如果EA交易同时交易多个交易品种,交易操作的交易品种名称应该在每次操作中指定。

      //--- 2. 一个买入指定交易品种的示例
         if(!trade.Buy(1,"Si-6.16"))
           {
            //--- 错误报告
            Print("Buy() 方法失败. 返回代码=",trade.ResultRetcode(),
                  ". 代码描述: ",trade.ResultRetcodeDescription());
           }
         else
           {
            Print("The Buy() method has been successfully performed. 返回代码=",trade.ResultRetcode(),
                  " (",trade.ResultRetcodeDescription(),")");
           }
      

      您可以指定订单中的全部参数: 止损/获利,建仓价格和注释.

      //--- 3. 一个预先指定止损和获利的某交易品种的买入示例
         double volume=1;           // 指定交易操作的交易量
         string symbol="Si-6.16";   // 指定交易操作的交易品种名称
         int    digits=(int)SymbolInfoInteger(symbol,SYMBOL_DIGITS); // 小数点位数
         double point=SymbolInfoDouble(symbol,SYMBOL_POINT);         // 点数
         double bid=SymbolInfoDouble(symbol,SYMBOL_BID);             // 当前的买入平仓价格
         double SL=bid-100*point;                                    // 未规范化的止损值
         SL=NormalizeDouble(SL,digits);                              // 规范化止损
         double TP=bid+100*point;                                    // 未规范化的获利值
         TP=NormalizeDouble(TP,digits);                              // 规范化获利
      //--- 取得当前的买入建仓价格
         double open_price=SymbolInfoDouble(symbol,SYMBOL_ASK);
         string comment=StringFormat("买入 %s %G 手, 价位 %s, 止损=%s 获利=%s",
                                     symbol,volume,
                                     DoubleToString(open_price,digits),
                                     DoubleToString(SL,digits),
                                     DoubleToString(TP,digits));
         if(!trade.Buy(volume,symbol,open_price,SL,TP,comment))
           {
            //--- 错误报告
            Print("Buy() 方法失败. 返回代码=",trade.ResultRetcode(),
                  ". 代码描述: ",trade.ResultRetcodeDescription());
           }
         else
           {
            Print("The Buy() method has been successfully performed. 返回代码=",trade.ResultRetcode(),
                  " (",trade.ResultRetcodeDescription(),")");
           }
      

      幻数和允许的滑点值是在 CTrade 实例的初始化过程中指定的,所以我们不需要现在设置它们,尽管有必要时也可以在每个交易操作前直接设置它们。

      设置一个限价单

      限价订单可以使用对应的类方法来发送: BuyLimit() 或者 SellLimit()。对于大多数情况,使用简要的版本,只指定建仓价格和交易量,就足够了。BuyLimit 的建仓价格必须低于当前价格,而对于 SellLimit,建仓价格必须高于当前价格,这些订单是用于以更好价格进入市场的,例如,在策略中期待价格从支撑位回滚。在这个列子中,使用的是EA所运行的交易品种:

      //--- 1. 设置 BuyLimit 挂单的实例
         string symbol="Si-6.16";                                    // 指定订单的交易品种
         int    digits=(int)SymbolInfoInteger(symbol,SYMBOL_DIGITS); // 设置的小数点位数
         double point=SymbolInfoDouble(symbol,SYMBOL_POINT);         // 点数
         double ask=SymbolInfoDouble(symbol,SYMBOL_ASK);             // 当前买入价格
         double price=ask-100*point;                                 // 未规范化的建仓价格
         price=NormalizeDouble(price,digits);                        // 规范化建仓价格
      //--- 一切准备工作就绪,向服务器发出限价买入挂单
         if(!trade.BuyLimit(1,price))
           {
            //--- 错误报告
            Print("BuyLimit() 方法失败. 返回代码=",trade.ResultRetcode(),
                  ". 代码描述: ",trade.ResultRetcodeDescription());
           }
         else
           {
            Print("The BuyLimit() method has been successfully performed. 返回代码=",trade.ResultRetcode(),
                  " (",trade.ResultRetcodeDescription(),")");
           }
      

      您也可以使用一个更加详细的版本,其中您需要所有的参数:SL/TP, 过期,交易品种的名称以及订单的注释。

      //--- 2. 使用全部参数设置 BuyLimit 挂单的示例
         double volume=1;
         string symbol="Si-6.16";                                    // 指定订单的交易品种
         int    digits=(int)SymbolInfoInteger(symbol,SYMBOL_DIGITS); // 小数点位数
         double point=SymbolInfoDouble(symbol,SYMBOL_POINT);         // 点数
         double ask=SymbolInfoDouble(symbol,SYMBOL_ASK);             // 当前买入价格
         double price=ask-100*point;                                 // 未规范化的建仓价格
         price=NormalizeDouble(price,digits);                        // 规范化建仓价格
         int SL_pips=100;                                            // 止损点数
         int TP_pips=100;                                            // 获利点数
         double SL=price-SL_pips*point;                              // 未规范化的止损值
         SL=NormalizeDouble(SL,digits);                              // 规范化止损
         double TP=price+TP_pips*point;                              // 未规范化的获利值
         TP=NormalizeDouble(TP,digits);                              // 规范化获利
         datetime expiration=TimeTradeServer()+PeriodSeconds(PERIOD_D1);
         string comment=StringFormat("限价买入 %s %G 手,价位 %s, 止损=%s 获利=%s",
                                     symbol,volume,
                                     DoubleToString(price,digits),
                                     DoubleToString(SL,digits),
                                     DoubleToString(TP,digits));
      //--- 一切准备工作就绪,向服务器发送限价买入挂单
         if(!trade.BuyLimit(volume,price,symbol,SL,TP,ORDER_TIME_DAY,expiration,comment))
           {
            //--- 错误报告
            Print("BuyLimit() 方法失败. 返回代码=",trade.ResultRetcode(),
                  ". 代码描述: ",trade.ResultRetcodeDescription());
           }
         else
           {
            Print("BuyLimit() 方法运行成功. 返回代码=",trade.ResultRetcode(),
                  " (",trade.ResultRetcodeDescription(),")");
           }
      

      在第二种方法中,需要正确设置止损和获利水平,请注意,当您买入时,获利值必须高于建仓价格,而止损值必须低于建仓价格。对于SellLimit 订单则相反。如果水平没有正确设置,您就会在您机器人的回归测试发现,因为 CTrade 类会在这种情况下自动地打印记录 (如果您没有调用 LogLevel 函数)。

      设置止损挂单

      止损挂单可以使用 BuyStop() 以及 SellStop() 方法来发送。止损买入挂单的建仓价格必须高于当前价格,而对于止损卖出,则需要低于当前价格。止损单应用的策略是用于当市价突破阻力水平时进入市场,另外它们也有助于限制亏损。简单版本:

      //--- 1. 设置止损买入挂单的示例
         string symbol="RTS-6.16";    // 指定订单的交易品种
         int    digits=(int)SymbolInfoInteger(symbol,SYMBOL_DIGITS); // 小数位数
         double point=SymbolInfoDouble(symbol,SYMBOL_POINT);         // 点数
         double ask=SymbolInfoDouble(symbol,SYMBOL_ASK);             // 当前买入价格
         double price=ask+100*point;                                 // 未规范化的建仓价格
         price=NormalizeDouble(price,digits);                        // 规范化建仓价格
      //--- 一切准备工作就绪,把止损买入挂单发送给服务器 
         if(!trade.BuyStop(1,price))
           {
            //--- 错误报告
            Print("BuyStop() 方法失败. 返回代码=",trade.ResultRetcode(),
                  ". 代码描述: ",trade.ResultRetcodeDescription());
           }
         else
           {
            Print("BuyStop() 方法运行成功. 返回代码=",trade.ResultRetcode(),
                  " (",trade.ResultRetcodeDescription(),")");
           }
      

      当您需要为 BuyStop 挂单设置更多的参数时,这里有一个更加详细的版本:

      //--- 2. 一个使用全部参数设置止损买入挂单的示例
         double volume=1;
         string symbol="RTS-6.16";    // 指定订单的交易品种
         int    digits=(int)SymbolInfoInteger(symbol,SYMBOL_DIGITS); // 小数位数
         double point=SymbolInfoDouble(symbol,SYMBOL_POINT);         // 点数
         double ask=SymbolInfoDouble(symbol,SYMBOL_ASK);             // 当前的买入价格
         double price=ask+100*point;                                 // 未规范化的建仓价格
         price=NormalizeDouble(price,digits);                        // 规范化建仓价格
         int SL_pips=100;                                            // 止损点数
         int TP_pips=100;                                            // 获利点数
         double SL=price-SL_pips*point;                              // 未规范化的止损
         SL=NormalizeDouble(SL,digits);                              // 规范化止损
         double TP=price+TP_pips*point;                              // 未规范化的获利
         TP=NormalizeDouble(TP,digits);                              // 规范化获利
         datetime expiration=TimeTradeServer()+PeriodSeconds(PERIOD_D1);
         string comment=StringFormat("止损买入 %s %G 手,价位 %s, 止损=%s 获利=%s",
                                     symbol,volume,
                                     DoubleToString(price,digits),
                                     DoubleToString(SL,digits),
                                     DoubleToString(TP,digits));
      //--- 一切准备工作就绪,把止损买入订单发向服务器 
         if(!trade.BuyStop(volume,price,symbol,SL,TP,ORDER_TIME_DAY,expiration,comment))
           {
            //--- 错误报告
            Print("BuyStop() 方法失败. 返回代码=",trade.ResultRetcode(),
                  ". 代码描述: ",trade.ResultRetcodeDescription());
           }
         else
           {
            Print("BuyStop() 方法运行成功。返回代码=",trade.ResultRetcode(),
                  " (",trade.ResultRetcodeDescription(),")");
           }
      

      止损卖出挂单也可以通过 CTrade 类的对应方法来发送,在这种情况下很重要的就是正确指定价格。

      操作仓位

      除了使用 Buy() 和 Sell() 方法,您还可以使用其他方法来建立仓位,但是,这样的话您就要指定更加详细的信息:

      //--- 小数位数
         int    digits=(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
      //--- 点数值
         double point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
      //--- 取得买入价格
         double price=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
      //--- 计算和规范化止损和获利水平
         double SL=NormalizeDouble(price-100*point,digits);
         double TP=NormalizeDouble(price+100*point,digits);
      //--- 加上注释
         string comment="Buy "+_Symbol+" 1 at "+DoubleToString(price,digits);
      //--- 一切准备工作就绪,尝试建立买入仓位
         if(!trade.PositionOpen(_Symbol,ORDER_TYPE_BUY,1,price,SL,TP,comment))
           {
            //--- 错误报告
            Print("PositionOpen() 方法失败. 返回代码=",trade.ResultRetcode(),
                  ". 代码描述: ",trade.ResultRetcodeDescription());
           }
         else
           {
            Print("PositionOpen() 方法运行成功. 返回代码=",trade.ResultRetcode(),
                  " (",trade.ResultRetcodeDescription(),")");
           }
      

      为了平掉一个仓位,您只需要指定交易品种的名称,而 CTrade 类会去做剩下的事情。

      //--- 在当前交易品种下平仓
         if(!trade.PositionClose(_Symbol))
           {
            //--- 错误报告
            Print("PositionClose() 方法失败. 返回代码=",trade.ResultRetcode(),
                  ". 代码描述: ",trade.ResultRetcodeDescription());
           }
         else
           {
            Print("PositionClose() 方法运行成功. 返回代码=",trade.ResultRetcode(),
                  " (",trade.ResultRetcodeDescription(),")");
           }
      

      也可以修改已建仓为的止损和获利水平,这可以通过使用 ModifyPosition() 方法来做到。

      //--- 小数位数
         int    digits=(int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS);
      //--- 点数值
         double point=SymbolInfoDouble(_Symbol,SYMBOL_POINT);
      //--- 取得当前买入价格
         double price=SymbolInfoDouble(_Symbol,SYMBOL_BID);
      //--- 计算和规范化止损和获利水平
         double SL=NormalizeDouble(price-100*point,digits);
         double TP=NormalizeDouble(price+100*point,digits);
      //--- 一切准备工作就绪,尝试修改买入仓位
         if(!trade.PositionModify(_Symbol,SL,TP))
           {
            //--- 错误报告
            Print("PositionModify() 方法失败. 返回代码=",trade.ResultRetcode(),
                  ". 代码描述: ",trade.ResultRetcodeDescription());
           }
         else
           {
            Print("PositionModify() 方法运行成功. 返回代码=",trade.ResultRetcode(),
                  " (",trade.ResultRetcodeDescription(),")");
           }
      

      修改和删除订单

      为了修改挂单的参数,CTrade 类提供了 OrderModify() 方法, 可以传给它所需的参数。

      //--- 检查订单是否存在  
         if(!OrderSelect(ticket))
           {
            Print("订单 #",ticket," 没有找到。");
            return;
      
           }
      //--- 交易品种 
         string symbol=OrderGetString(ORDER_SYMBOL);
      //--- 小数位数
         int    digits=(int)SymbolInfoInteger(symbol,SYMBOL_DIGITS);
      //--- 点数值
         double point=SymbolInfoDouble(symbol,SYMBOL_POINT);
      //--- 取得建仓价格
         double price=OrderGetDouble(ORDER_PRICE_OPEN);
      //--- 计算并规范化止损和获利水平
         double SL=NormalizeDouble(price-200*point,digits);
         double TP=NormalizeDouble(price+200*point,digits);
      //--- 一切准备工作就绪,准备修改订单 
         if(!trade.OrderModify(ticket,price,SL,TP,ORDER_TIME_DAY,0))
           {
            //--- 错误报告
            Print("OrderModify() 方法失败. 返回代码=",trade.ResultRetcode(),
                  ". 代码描述: ",trade.ResultRetcodeDescription());
           }
         else
           {
            Print("OrderModify() 方法运行成功. 返回代码=",trade.ResultRetcode(),
                  " (",trade.ResultRetcodeDescription(),")");
           }
      

      您需要取得想要修改的订单编号,并根据订单类型设置正确的止损和获利水平,另外,新的建仓价格相对当前价格必须是正确的,

      如果您想要删除一个订单,您只需知道它的编号:

      //--- 检查订单是否存在  
         if(!OrderSelect(ticket))
           {
            Print("订单 #",ticket," 没有找到。");
            return;
           }
      //--- 一切准备就绪,尝试删除订单
         if(!trade.OrderDelete(ticket))
           {
            //--- 错误报告
            Print("OrderDelete() 方法失败. 返回代码=",trade.ResultRetcode(),
                  ". 代码描述: ",trade.ResultRetcodeDescription());
           }
         else
           {
            Print("OrderDelete() 方法运行成功. 返回代码=",trade.ResultRetcode(),
                  " (",trade.ResultRetcodeDescription(),")");
           }
      

      此类还包含了通用的OrderOpen()方法,它可以设置任意类型的挂单。与特定的BuyLimit, BuyStop, SellLimitSellStop 方法不同, OrderOpen() 包含了更多必需的参数。所以您可以选择想要的方法。


      交易类中更多的内容

      在本文中,我们已经分析了编程进行买入和卖出操作的简单技术,以及编程操作挂单的方法,然而,交易类中还包含了更多对MQL5交易程序开发人员方便使用的内容:

      使用这些类,将有助您把精力集中在您的交易策略上,而最大程度地解决技术问题,另外,CTrade 可以用于学习交易请求,例如使用调试器。这样一段时间之后您就能基于CTrade创建您自己的类了,而在这个类中实现处理交易请求执行结果的逻辑。

      使用简单的脚本程序开始您的算法交易旅程

      本文中所描述的 MQL5 机器人开发主要是针对初学者,而有经验的开发人员在这里也可以获取一些新的有用信息,试试执行本文描述的简单脚本程序,您就会明白创建一个交易机器人比你想像的要简单。

      如果您想学习更多内容,这里有两篇相关文章:

      1. 以莫斯科交易所衍生产品市场为例的交易定价原则
      2. 在莫斯科交易所交易时如何保护您和您的EA交易