English Русский Español Deutsch 日本語 Português
preview
掌握 MQL5:从入门到精通(第六部分):开发 EA 交易的基础知识

掌握 MQL5:从入门到精通(第六部分):开发 EA 交易的基础知识

MetaTrader 5示例 |
56 3
Oleh Fedorov
Oleh Fedorov

概述

最后,我们到达了创建 EA 交易的阶段。从某种程度上来说,我们已经到了决定性阶段。

为了充分利用本文,您应该已经熟悉以下概念:

  • 变量(局部变量和全局变量), 
  • 函数及其参数(通过引用和值), 
  • 数组(包括对序列数组的基本理解),
  • 核心运算符,包括逻辑、算术、条件(if、switch、三元)和循环运算符(主要是 “for”,但熟悉 “while” 和 “do...while” 也很有用)。

从程序员的角度来看,EA 交易并不比我们在本系列上一篇文章中讨论的指标复杂多少。交易逻辑同样涉及检查某些条件,如果满足这些条件,则执行操作(通常向服务器发送交易订单)。关键是要了解交易订单的结构,了解发送这些订单的函数,并能够访问交易所需的数据。 

重要!本文中介绍的所有 EA 交易仅用于说明编程原理,并不用于实际交易或产生利润。如果你打算在真实账户上使用这段代码,你可能需要改进决策算法。否则,您将面临遭受损失的风险。

事实上,即使入场逻辑得到改进,这里提供的 EA 代码也不适合真实交易。第一个示例不包含任何错误处理:既不包含请求提交,也不包含服务器响应。这样做是为了简化代码,使其更容易理解,但它限制了 EA 在快速原型设计或测试基本策略逻辑中的使用。第二个 EA 包含更多验证,然而,即使这样也不足以在市场上发布或进行可靠的实时交易,因为问题必须在出现时得到处理(如果可以处理的话,而不是简单地报告和关闭)。

下一篇文章将介绍一个在技术上可以在市场上发布的功能齐全的 EA。该 EA 将包括必要的验证和比我们在这里介绍的稍微复杂的逻辑。在本文中,我将重点介绍交易自动化的基础知识。我们将构建两个 EA:一个没有指标,另一个使用内置移动平均指标。第一个将使用挂单进行交易,而第二个将按市场价格执行交易。



EA 交易模板

每个 EA 交易都从创建一个空白模板开始,通常使用 MQL5 向导(图 1)。

MQL 向导 - 第一个屏幕

图 1.MQL 向导 - 第一个屏幕

MQL 向导提供两个主要选项:从模板创建 EA 交易(顶部选项),或生成更高级、结构化的版本。对于初级程序员,我强烈建议选择第一个选项,即模板。

高级的生成版本是面向对象的,并分布在多个文件中。如果没有额外的工具或经验,很难理解,甚至更难为自己的交易逻辑进行定制。这就是为什么我建议只有在您对 OOP(面向对象编程)概念和实践有了深入的了解之后才使用此版本。审查生成的代码可能对“了解可能的情况”有教育意义,但请记住,这只是许多可能的实现之一,针对自动生成进行了优化。当你准备好完全理解其类结构的复杂性时,你可能更喜欢编写自己的代码模板。当然,编辑自己的代码比破译别人的代码容易得多。而且,您自己的模板很可能与向导提供的模板一样好,甚至更好。

向导可以添加的大多数可选功能(图 2 和图 3)并不是严格必需的,但通常非常有用。例如,第三个向导屏幕(图 2)中的功能允许您处理交易操作期间触发的事件,例如服务器收到信号、开仓等。( OnTradeOnTradeTransaction ),以及计时器事件( OnTimer ),图表交互(如按下按钮或创建对象)( OnChartEvent )和订单簿更新( OnBookEvent )。

创建 EA 交易 - 向导的第三个屏幕

图 2.创建 EA 交易 - 向导的第三个屏幕(额外 EA 函数)

还有一些专门在策略测试器中使用,但在正常运行期间不使用的特殊函数(图 3)。这些主要适用于仅在测试器中工作而不在真实账户上工作的演示版本。有时,您可能在测试期间需要更详细的日志或想要从其他来源获取数据。就我个人而言,我很少使用这些函数,但它们在正确的环境下很有价值。

策略测试器的 EA 函数

图 3.创建 EA 交易 - 向导的第四个屏幕(仅用于测试器操作的函数)

图 2 中的某些函数将在以后的文章中更详细地讨论,而图 3 中的函数则留给您自行探索。

使用向导创建 EA 时,生成的文件始终包含至少三个函数(示例 1):

//+------------------------------------------------------------------+
//|                                                  FirstExpert.mq5 |
//|                                       Oleg Fedorov (aka certain) |
//|                                   mailto:coder.fedorov@gmail.com |
//+------------------------------------------------------------------+
#property copyright "Oleg Fedorov (aka certain)"
#property link      "mailto:coder.fedorov@gmail.com"
#property version   "1.00"
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
  }
//+------------------------------------------------------------------+

例 1 .向导创建的 EA 的最小模板

  • OnInit —— 指标开发中的一个熟悉函数,用于初始设置。它在程序启动时运行一次。
  • OnDeinit —— 也可能很熟悉,当 EA 交易停止时调用此函数。它用于清理:删除 EA 创建的图形对象、关闭文件、释放指标资源以及执行其他最终任务。
  • OnTick —— 每次报价时执行(类似于指标中的 OnCalculate)。这是您的 EA 的核心逻辑运行的地方。

其中,只有 OnTick 是 EA 交易的必需项。如果您的 EA 很简单,您可以省略 OnInit 和 OnDeinit。



MetaTrader 5 自动交易中的关键术语

在 MetaTrader 5 中开发自动交易系统时,每个程序员都必须理解某些术语和概念。这些术语与交易逻辑和函数名称相关。让我们来探索一下。

订单 —— 发送到服务器的消息,表明您打算以特定价格买入或卖出特定工具。

交易中的每个变化 —— 无论是下达市价单还是修改止损水平 —— 都是通过交易订单发生的。订单可以立即执行(例如,以市场价格买入/卖出)或待执行,这意味着交易旨在在满足价格条件时在未来执行(例如止损或限价订单)。

挂单包括止损、止盈、买入/卖出止损、买入/卖出限价、买入/卖出止损限价。与订单处理相关的示例函数包括 OrderSend (向服务器发送订单)和 OrderGetInteger (返回订单的整数参数,例如单号或创建时间)。

交易 —— 订单的实际执行。

MetaTrader 5 中的交易本质上与历史相关。这是订单完成的时刻。您不能直接影响交易,因为一旦订单执行,交易就会在服务器上发生。但是,您可以检索有关交易的历史信息,如执行价格和时间。示例函数:HistoryDealGetDouble (获取交易的双精度型参数,如价格)、 HistoryDealsTotal (返回历史记录中的交易总数)。

仓位 —— 在对特定交易品种进行一次或多次交易后,您的投资组合的最终状态。

MetaTrader 5 最初的设计是每个交易品种只能有一个仓位。然而,实际行为取决于账户类型。在单边账户中,所有交易都会更新单一的仓位。在对冲账户中,每笔交易都会创建自己的仓位(除非是止损单)。这可能导致同一交易品种上出现多个仓位,甚至出现相反的方向。在这种情况下,您可能需要手动计算净多头和净空头敞口。可以使用交易订单修改仓位 —— 全部或部分关闭,或根据止损和获利水平进行调整。与仓位相关的函数示例包括:PositionSelectByTicket (通过编号选择仓位)和 PositionGetString (获取字符串参数,如交易品种名称或用户注释)。

每笔交易都源于交易订单的执行,每个仓位都反映了一笔或多笔交易的累积效应。

事件 —— 程序环境中发生的任何重大事情。

提交交易请求是一个事件。服务器接受请求、用户点击图表、图表缩放变化、新的报价 —— 所有这些也都是事件。其中一些是通过以 On 开头的标准处理程序函数来处理的 —— 例如 OnInit(由初始化事件触发)或 OnTick(用于分时报价事件)。

其他事件类型使用常量标识符进行处理。这意味着必须首先触发全局事件处理程序函数之一。例如,任何图表事件都会调用 OnChartEvent。在这个函数中,您可以通过将事件代码变量与已知常量进行比较来确定确切的事件类型。传递给这些函数的参数有助于识别事件详细信息。本文不会详细介绍这些较小事件的细节。


MetaTrader 5 自动交易的基本原理

让我们来深入了解一下 MetaTrader 5 中交易的实际运作方式。我们将从一个重要事实开始。

在您的计算机上运行的终端软件和用您的资金执行交易的服务器软件是两个独立的程序。它们通过网络(通常是互联网)进行通信。

因此,当您点击“买入”或“卖出”时,将发生以下事件序列:

  • 您的终端会生成一个特殊的数据包,填充特殊的 MqlTradeRequest 结构。
  • 此填充的结构使用 OrderSend(同步模式)或 OrderSendAsync (异步模式)发送到服务器,形成交易订单
  • 服务器接收数据包并检查它是否满足所有要求:是否有匹配的价格,您的余额是否充足,等等。
  • 如果一切正常,订单将与其他交易者的订单一起放入订单队列,等待执行。 
  • 确认消息将发送回您的终端。
  • 如果市场达到请求的价格水平,服务器将执行交易并将事件记录在其日志中。
  • 服务器将结果发送回您的终端。
  • 终端以 MqlTradeResult 结构的形式接收此结果,并生成相应的事件,如 TradeTradeTransaction
  • 终端检查服务器端的错误(通过检查 MqlTradeResult 结构的 “retcode” 字段来完成)。
  • 如果一切正常,终端将更新其内部变量、日志条目和图形图表。
  • 因此,您的投资组合中将出现相关工具的新头寸(或更新的头寸)。

整个过程可以可视化为一个简化的图表,如图 4 所示:

交易过程分布在终端和服务器之间

图 4.交易订单处理图


异步数据传输模式

您可能已经注意到,在终端和服务器之间的交互过程中,终端必须通过网络至少通信两次:一次是发送数据,另一次是接收响应。当使用 OrderSend 函数时,该过程本质上是同步的,即 EA 等待响应,占用系统资源(例如互联网带宽,甚至 CPU 时间)。

然而,从处理器的角度来看,网络操作非常缓慢。交易脚本通常需要几百微秒(例如,200 μs = 2e-4 秒)才能执行完毕,但网络数据传输通常以毫秒(例如,20 ms = 2e-2 秒)为单位,这至少要慢 100 倍。增加服务器端处理时间,有时还会因维护或技术问题而出现意外延迟.......在最坏的情况下,发送交易请求和收到响应之间的差距可能会延长到几秒钟甚至几分钟。如果在此期间有多个 EA 空闲等待,则大量 CPU 资源将被浪费。

为了解决这种低效率问题,MetaTrader 提供了一种特殊的异步交易模式。异步这个词意味着EA可以发送交易请求并继续执行其他任务 —— 睡眠、运行计算或其他任何任务 —— 而无需等待回复。当服务器响应最终到达时,终端会生成一个 TradeTransaction 事件(然后是 Trade 事件)。然后 EA 可以“醒来”并处理响应以做出进一步的交易决策。这种方法的优点是显而易见的。

在同步和异步模式下,交易错误都使用 OnTradeTransaction 函数处理。这不会使代码变得更复杂 —— 一些逻辑只是从 OnTick 移动到 OnTradeTransaction。如果你将这段代码移动到单独的函数中,那么调用和传输它根本不会造成任何问题。所以,同步和异步交易模式之间的选择完全取决于您的偏好和手头的任务。两种模式中使用的数据结构保持不变。


开始交易

假设我们想要为外汇市场建立一个寻找内包线的 EA 交易。提醒一下,内包线是指其最高点和最低点完全位于前一根(较大)蜡烛图范围内的蜡烛图。EA 将对每根烛形运行一次,当它检测到内包线形态时,它将同时下达两个挂单:

  • 买入止损点位于较大烛形高点上方几个点(可配置),
  • 卖出止损位于该烛形最低价下方相同距离处,
  • 每个订单的有效期为两根烛形。如果在此期间未触发,它将被删除,
  • 两个订单的止损都将设置在较大烛形的中点, 
  • 止盈将设置为较大烛形范围的 7/8,
  • 交易量将是允许的最小手数。

EA 的初始版本将避免附加条件,以使代码更易于理解。我们将构建一个骨架框架,稍后可以扩展。我们将首先使用向导创建 EA 模板,并在第三个窗口中取消选中所有复选框(因为在此版本中我们不会处理服务器响应)。生成的代码与示例 1 类似。为了使 EA 可配置和可优化,我们将定义四个输入参数:从最高/最低到下达挂单的距离(inp_PipsToExtremum)、到设置止损和止盈的距离(inp_StopCoefficient 和 inp_TakeCoefficient)以及未触发订单将被删除后的柱形数量(inp_BarsForOrderExpired)。此外,我们将为 EA 声明一个幻数 - 这有助于区分“我们的”订单与其他 EA 或手动下达的订单。

//--- declare and initialize input parameters
input int     inp_PipsToExtremum      = 2;
input double  inp_TakeCoeffcient      = 0.875;
input double  inp_StopCoeffcient      = 0.5;
input int     inp_BarsForOrderExpired = 2;

//--- declare and initialize global variables
#define EXPERT_MAGIC 11223344

场景 2 。EA 输入参数和幻数的描述

提醒一下:示例 2 中的代码必须放在 EA 文件的最顶部,紧跟在 #property 指令之后。

本例中的其余代码将放在 OnTick 函数内。目前我们将其他所有函数保留为空。以下是需要放置在 OnTick 主体中的代码:

 /****************************************************************
  *    Please note: this Expert Advisor uses standard functions  *
  * to access price/time data. Therefore, it's convenient to     *
  * work with series as arrays (time and prices).                *
  ****************************************************************/
  string          symbolName  = Symbol();
  ENUM_TIMEFRAMES period      = PERIOD_CURRENT;

//--- Define a new candlestick (Operations only at the start of a new candlestick)
  static datetime timePreviousBar = 0; // Time of the previous candlestick
  datetime timeCurrentBar;             // Time of the current candlestick

  // Get the time of the current candlestick using the standard function
  timeCurrentBar = iTime(
                     symbolName, // Symbol name
                     period,     // Period
                     0           // Candlestick index (remember it's series)
                   );

  if(timeCurrentBar==timePreviousBar)
   {
    // If the time of the current and previous candlesticks match
    return;  // Exit the function and do nothing
   }
  // Otherwise the current candlestick becomes the previous one,
  //   so as not to trade on the next tick
  timePreviousBar = timeCurrentBar;

//--- Prepare data for trading
  double volume=SymbolInfoDouble(symbolName,SYMBOL_VOLUME_MIN); // Volume (lots) - get minimum allowed volume

  // Candlestick extrema
  double high[],low[]; // Declare arrays

  // Declare that arrays are series
  ArraySetAsSeries(high,true);
  ArraySetAsSeries(low,true);

  // Fill arrays with values of first two closed candlesticks
  //   (start copying with index 1 
  //   as we only need closed candlesticks; use 2 values)
  CopyHigh(symbolName,period,1,2,high);
  CopyLow(symbolName,period,1,2,low);


  double lengthPreviousBar; // The range of the "long" bar
  MqlTradeRequest request;  // Request structure
  MqlTradeResult  result;   // Server response structure

  if( // If the first closed bar is inside
    high[0]<high[1]
    && low[0]>low[1]
  )
   {
    // Calculate the range
    lengthPreviousBar=high[1]-low[1];  // Timeseries have right-to-left indexing

  //--- Prepare data for a buy order
    request.action      =TRADE_ACTION_PENDING;                         // order type (pending)
    request.symbol      =symbolName;                                   // symbol name
    request.volume      =volume;                                       // volume deal
    request.type        =ORDER_TYPE_BUY_STOP;                          // order action (buy)
    request.price       =high[1] + inp_PipsToExtremum*Point();         // buy price
    // Optional parameters
    request.deviation   =5;                                            // acceptable deviation from the price
    request.magic       =EXPERT_MAGIC;                                 // EA's magic number
    
    request.type_time   =ORDER_TIME_SPECIFIED;                         // Parameter is required to set the lifetime
    request.expiration  =timeCurrentBar+
                         PeriodSeconds()*inp_BarsForOrderExpired;      // Order lifetime

    request.sl          =high[1]-lengthPreviousBar*inp_StopCoeffcient;  // Stop Loss
    request.tp          =high[1]+lengthPreviousBar*inp_TakeCoeffcient;  // Take Profit


  //--- Send a buy order to the server
    OrderSend(request,result); // For asynchronous mode you need to use OrderSendAsync(request,result);
    
  //--- Clear the request and response structures for reuse
    ZeroMemory(request);
    ZeroMemory(result);
    
  //--- Prepare data for a sell order. Parameers are the same as in the previous function.
    request.action      =TRADE_ACTION_PENDING;                         // order type (pending)
    request.symbol      =symbolName;                                   // symbol name
    request.volume      =volume;                                       // volume
    request.type        =ORDER_TYPE_SELL_STOP;                         // order action (sell)
    request.price       =low[1] - inp_PipsToExtremum*Point();          // sell price
    // Optional parameters
    request.deviation   =5;                                            // acceptable deviation from the price
    request.magic       =EXPERT_MAGIC;                                 // EA's magic number
    
    request.type_time   =ORDER_TIME_SPECIFIED;                         // Parameter is required to set the lifetime
    request.expiration  =timeCurrentBar+
                         PeriodSeconds()*inp_BarsForOrderExpired;      // Order lifetime

    request.sl          =low[1]+lengthPreviousBar*inp_StopCoeffcient;   // Stop Loss
    request.tp          =low[1]-lengthPreviousBar*inp_TakeCoeffcient;   // Take Profit

  //--- Send a sell order to the server
    OrderSend(request,result);
   }
 

例 3.该 EA 的 OnTick 函数包含所有交易逻辑。

标准 Point 函数返回当前图表的点大小。例如,如果经纪商提供五位数报价,那么 EURUSD 的一个点为 0.00001,USDJPY 的一个点为 0.001。函数 iTimeiHighiLow 允许您检索特定烛形的时间、最高价和最低价(按其索引从右到左,其中 0 为当前烛形)。在这个例子中,我们仅使用 iTime 通过检索当前时间来检查新的烛形。为了获取最高值和最低值,我们使用了数组复制函数 CopyHighCopyLow

代码分为两个主要部分:检查新柱和交易(从准备阶段开始)。交易区块进一步分为两个几乎相同的部分:一个用于买入,一个用于卖出。显然,在这两种情况下,结构设置和订单提交逻辑非常相似,因此将它们重构为一个单独的函数是有意义的,该函数将填充公共字段,并仅扩展到订单类型和执行价格(price、tp、sl)等细节。然而,在这个例子中,为了清晰性和可读性,故意牺牲了代码的紧凑性和可重用性。

该过程的每个阶段都使用 //--- 标记注释,而每个阶段内的注释都以简单的样式编写,没有破折号。交易逻辑由两个主要部分组成:填充请求结构和发送请求结构。在填写请求结构时,只有前五个字段是必填的。

需要注意的是,如果您打算使用订单到期时间,如本例所示,则必须同时填写 request.type_time 和 request.expiration。如果未设置第一个字段,默认情况下将忽略第二个字段。

为了测试此 EA 交易如何运行,您可以在模拟账户上以任何时间范围运行它(它甚至可以在分钟图上运行,尽管实际性能取决于所选交易品种的点差)。或者,在 MetaEditor 中按 <Ctrl>+<F5> 键,使用策略测试器中的历史数据启动回溯测试。完整的源代码可以在附件 TrendPendings.mq5 中找到。


使用标准指标

示例 3 中的 EA 交易没有使用任何指标,但情况并非总是如此。对于基于标准指标的策略,主要有两种方法:使用内置指标函数或使用指标类。我们将介绍这两种方法,从内置函数开始。

假设我们要创建一个基于简单移动平均线的 EA 交易。提醒一下,本文的目标不是建立一个盈利策略,而是展示基本的交易逻辑。因此,我将尽可能保持代码的简单性和可读性。在此基础上,我们将定义我们的交易规则如下:

  • 与之前的 EA 类似,只有当新柱形成时才会考虑交易;
  • 要买入,前一根烛形必须收于移动平均线上方; 
  • 要卖出,前一根烛形必须收于移动平均线以下;
  • 作为过滤器,我们将使用移动平均线的斜率:如果平均线从倒数第二根柱线上升到最近一根收盘柱线,我们就买入;如果下降,我们就卖出;
  • 当相反信号出现时,我们退出该仓位;
  • 保护性止损设置在信号烛形的最高点(对于卖出信号)或最低点(对于买入信号);
  • 每个交易品种只允许开立一个仓位,即使在对冲账户中也是如此;如果出现信号但仓位已经开立,我们会跳过它。

图 5 说明了此 EA 中使用的过滤原理。

信号烛形过滤原理

图 5.基于移动平均线的 EA 中使用的烛形过滤原理

在这个 EA 中,我将继续努力保持代码尽可能干净和易于理解。然而,我将开始引入更多的错误检查,使实现更接近现实世界的标准。

所有移动平均参数,以及与请求订单价格的最大允许价格偏差,都将添加到输入参数中。此外,我们将为指标句柄声明一个全局变量(我稍后会解释这一点),并定义 EA 的幻数。

//--- declare and initialize global variables

#define EXPERT_MAGIC 3345677

input int inp_maPeriod = 3;                                 // MA period
input int inp_maShift = 0;                                  // Shift
input ENUM_MA_METHOD inp_maMethod = MODE_SMA;               // Calculation mode
input ENUM_APPLIED_PRICE inp_maAppliedPrice = PRICE_CLOSE;  // Applied price
input int inp_deviation = 5;                                // Max price deviation from the request price in points

//--- MA indicator handle
int g_maHandle;

例 4 .使用移动平均线进行交易的 EA 全局变量

在 EA 或其他指标中使用任何指标之前,我们需要做三件事:

  1. 初始化指标并获取其句柄。这通常是使用内置指标函数(例如用于移动平均线的 iMA )或用于自定义、用户定义指标的 iCustom 来完成的。这个初始化通常在 OnInit 函数内部进行。

    在编程中,句柄就像是代码可以与之交互的资源的引用或指针。可以将其视为一个编号,它可以让您访问指标并在需要时请求其数据。
  2. 在使用指标值之前,您需要获取最新的数据。这通常通过为每个指标缓冲区创建一个数组并使用 CopyBuffer 函数填充这些数组来完成。
  3. 使用已填充数组的数据。
  4. 为了避免内存泄漏或不必要的资源使用,在程序结束时释放指标句柄非常重要。这是在 OnDeinit 函数中使用 IndicatorRelease 完成的。

在这个特定的 EA 中,OnInit 和 OnDeinit 函数相当简单,不包含任何不寻常或复杂的逻辑:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
 {
//--- Before an action that could potentially cause an error, reset
//   built-in _LastError variable to default
//   (assuming there's no error yet)
  ResetLastError();

//--- The standard iMA function returns the indicator handle
  g_maHandle = iMA(
                 _Symbol,           // Symbol
                 PERIOD_CURRENT,    // Chart period
                 inp_maPeriod,      // MA period
                 inp_maShift,       // MA shift
                 inp_maMethod,      // MA calculation method
                 inp_maAppliedPrice // Applied price
               );
// inp_maAppliedPrice in general case can be
// either a price type as in this example,
// (from ENUM_APPLIED_PRICE),
// or a handle of another indicator

//--- if the handle is not created
  if(g_maHandle==INVALID_HANDLE)
   {
    //--- report failure and output error code
    PrintFormat("Failed to crate iMA indicator handle for the pair %s/%s, error code is %d",
                _Symbol,
                EnumToString(_Period),
                GetLastError() // Output error code
               );
    //--- If an error occurs, terminate the EA early
    return(INIT_FAILED);
   }
//---
  return(INIT_SUCCEEDED);
 }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
 {
//--- Release resources occupied by the indicator
  if(g_maHandle!=INVALID_HANDLE)
    IndicatorRelease(g_maHandle);
 }

例 5.EA 交易中指标的初始化和去初始化

我想提请您注意的一个细微差别是在初始化函数中使用 ResetLastErrorGetLastError 函数对。前者将系统变量 _LastError 重置为“无错误”状态,而后者允许您获取最近发生的错误的代码(如果发生)。

除此之外,一切都相当简单。指标(包括 iMA)的初始化函数返回有效的指标句柄,如果无法获取句柄,则返回特殊常量 INVALID_HANDLE。这种机制允许我们检测什么时候出了问题,并相应地处理错误 —— 在我们的例子中,通过显示错误消息。如果 OnInit 返回 INIT_FAILED,则 EA 交易(或指标)将不会启动。事实上,如果我们无法获得移动平均线指标的有效参考,停止执行是唯一正确的做法。

至于 OnTick 函数,我们将逐步分解。第一部分涉及变量的声明和初始化。

//--- Declare and initialize variables
  MqlTradeRequest requestMakePosition;  // Request structure for opening a new position
  MqlTradeRequest requestClosePosition; // Request structure for closing an existing position
  MqlTradeResult  result;               // Structure for receiving the server's response
  MqlTradeCheckResult checkResult;      // Structure for validating the request before sending

  bool positionExists = false;      // Flag indicating if a position exists
  bool tradingNeeds = false;        // Flag indicating whether trading is allowed
  ENUM_POSITION_TYPE positionType;  // Type of currently open position
  ENUM_POSITION_TYPE tradingType;   // Desired position type (used for comparison)
  ENUM_ORDER_TYPE orderType;        // Desired order type
  double requestPrice=0;            // Entry price for the future position

  /* The MqlRates structure contains 
     all candle data: open, close, high, 
     and low prices, tick volume, 
     real volume, spread, and time.
     
     In this example, I decided to demonstrate how to fill the entire structure at once, 
     instead of retrieving each value separately.                              */
  
  MqlRates rates[];   // Array of price data used for evaluating trading conditions
  double maValues[];  // Array of MA values

// Declare data arrays as series
  ArraySetAsSeries(rates,true);
  ArraySetAsSeries(maValues,true);

例 6 .OnTick 函数的局部变量

我们用同样的方法检查是否出现了一个柱形:

//--- Check whether there's a new bar
  static datetime previousTime  = iTime(_Symbol,PERIOD_CURRENT,0);
  datetime currentTime          = iTime(_Symbol,PERIOD_CURRENT,0);
  if(previousTime==currentTime)
   {
    return;
   }
  previousTime=currentTime;

例 7 .检查是否是新柱形

接下来,我们使用特殊函数获取所需的所有数据。这里我们假设可能存在错误,例如,终端没有时间加载必要的数据,因此我们使用分支运算符来处理这些潜在的错误。

//---  Prepare data for processing
// Copy the quotes of two bars, starting from the first one
  if(CopyRates(_Symbol,PERIOD_CURRENT,1,2,rates)<=0)
   {
    PrintFormat("Data error for symbol %s, error code is %d", _Symbol, GetLastError());
    return;
   }

// Copy the values of the moving average indicator buffer
  if(CopyBuffer(g_maHandle,0,1,2,maValues)<=0)
   {
    PrintFormat("Error getting indicator data, error code is %d", GetLastError());
    return;
   }

 例 8 .将当前指标数据和报价复制到本地数组

现在我们使用标准 PositionSelect 函数为当前交易品种选择一个未平仓头寸。我们决定一个交易品种只能有一个头寸,所以应该不会有任何问题,但我们仍然仔细考虑可能出现的问题......至少,我们需要检查我们的 EA 是否已经开仓:

//--- Determine if there is an open position
  if(PositionSelect(_Symbol))
   {
    // Set the open position flag - for further processing
    positionExists = true;
    // Save the type of the open position
    positionType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);

    // Check if the position has been opened by our EA
    requestClosePosition.magic = PositionGetInteger(POSITION_MAGIC); // I didn't create a separate variable for 
                                                                     // the existing position magic number
    if(requestClosePosition.magic!= EXPERT_MAGIC)
     {
      // Some other EA started trading our symbol. Let it do so...
      return;
     } // if(requestClosePosition.magic!= EXPERT_MAGIC)
   } // if(PositionSelect(_Symbol))

例 9.获取当前头寸数据

现在我们可以检查交易条件。在这个例子中,我将把检查结果保存在单独的变量中,然后在做出最终决定时使用这些变量:买入或卖出。在大型复杂的解决方案中,这种方法首先以其灵活性为依据,其次以最终代码变短为依据。在这里,我使用这种技术主要是出于第二个原因:由于最终的算法不是很清楚,我试图通过不同的方式提高清晰度。

//--- Check trading conditions,
  if( // Conditions for BUY
    rates[0].close>maValues[0] // If the first candlestick closed above MA
    && maValues[0]>maValues[1] // and the MA slope is upwards
  )
   {
    // Set the trade flag
    tradingNeeds = true;
    // and inform the EA about the direction (here - BUY)
    tradingType = POSITION_TYPE_BUY; // to check the direction of the open direction
    orderType = ORDER_TYPE_BUY;      // to trade in the right direction
    // calculate the deal price
    requestPrice = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   }

  else
    if( // conditions for SELL
      rates[0].close<maValues[0]
      && maValues[0]<maValues[1]
    )
     {
      tradingNeeds = true;
      tradingType = POSITION_TYPE_SELL;
      orderType = ORDER_TYPE_SELL;
      requestPrice = SymbolInfoDouble(_Symbol,SYMBOL_BID);
     }

例 10.检查交易条件

下面显示的代码决定了当前是否应该执行交易。该决定基于三个关键问题:

  • 有交易设置吗?换句话说,烛形收盘价是否超出了移动平均线?此条件由 tradingNeeds 变量处理。如果答案为“否”(tradingNeeds == false),则不应进行交易。
  • 已经有开立的头寸了吗?这是使用 positionExists 变量检查的。如果没有未平仓头寸 - 那就继续交易。如果有,则继续下一步检查。
  • 现有头寸与新的交易信号一致还是相反?这是通过比较交易类型和持仓类型来确定的。如果它们相等,则该仓位与新信号一致,因此不会开启新的交易。如果不同,则当前仓位处于相反方向,必须先平仓才能开立新仓位。

该决策逻辑在流程图中可视化(图 6)。

关键交易逻辑分支流程图

图 6.表示交易算法主要决策点的流程图

在 MQL5 中,平仓和建仓都涉及发送市价单。该方法与您已经熟悉的方法相同:填写交易请求结构并将其发送到服务器。两种结构的区别在于,平仓时需要指定其编号,并将现有仓位的参数准确地复制到请求中。开新仓的时候没有什么可复制的,所以我们比较自由,而且没有旧仓位编号,所以不需要传递任何东西。

与前面的挂单示例相比,此处的代码有两个字段不同:

  • action 字段先前保存的值是 TRADE_ACTION_PENDING,现在包含 TRADE_ACTION_DEAL。
  • type 字段现在代表直接市场订单(ORDER_TYPE_BUY 或 ORDER_TYPE_SELL)而不是挂单。 

为了更容易地理解图 6 中代码片段和流程图之间的对应关系,示例代码已根据图中所示的分支逻辑进行了颜色编码。

与示例 3 相比,还有两个显著的区别。在发送交易请求之前,使用 OrderCheck 验证结构。这使得程序能够捕获填写错误的字段并提供返回代码(retcode)和文本注释(comment)。发送请求后,我们检查服务器是否接受了它。如果发生错误,程序将会通过相关消息进行报告。

// If the setup is to trade
  if(tradingNeeds)
   {
    // If there is a position
    if(positionExists)
     {
      // And it is opposite to the desired direction of trade
      if(positionType != tradingType)
       {
        //--- Close the position

        //--- Clear all participating structures, otherwise you may get an "invalid request" error
        ZeroMemory(requestClosePosition);
        ZeroMemory(checkResult);
        ZeroMemory(result);
        //--- set operation parameters
        // Get position ticket
        requestClosePosition.position = PositionGetInteger(POSITION_TICKET);
        // Closing a position is just a trade
        requestClosePosition.action = TRADE_ACTION_DEAL;
        // position type is opposite to current trading direction,
        // therefore, for the closing deal, we can use the current order type
        requestClosePosition.type = orderType;
        // Current price
        requestClosePosition.price = requestPrice;
        // Operation volume must match the current position volume
        requestClosePosition.volume = PositionGetDouble(POSITION_VOLUME);
        // Set acceptable deviation from the current price
        requestClosePosition.deviation = inp_deviation;
        // Symbol
        requestClosePosition.symbol = Symbol();
        // Position magic number
        requestClosePosition.magic = EXPERT_MAGIC;


        if(!OrderCheck(requestClosePosition,checkResult))
         {
          // If the structure is filled incorrectly, display a message
          PrintFormat("Error when checking an order to close position: %d - %s",checkResult.retcode, checkResult.comment);
         }
        else
         {
          // Send order
          if(!OrderSend(requestClosePosition,result))
           {
            // If position closing failed, report
            PrintFormat("Error closing position: %d - %s",result.retcode,result.comment);
           } // if(!OrderSend)
         } // else (!OrderCheck)
       } // if(positionType != tradingType)
      else
       {
        // Position opened in the same direction as the trade signal. Do not trade
        return; 
       } // else(positionType != tradingType)
     } // if(positionExists)

    //--- Open a new position

    //--- Clear all participating structures, otherwise you may get an "invalid request" error
    ZeroMemory(result);
    ZeroMemory(checkResult);
    ZeroMemory(requestMakePosition);

    // Fill the request structure
    requestMakePosition.action = TRADE_ACTION_DEAL;
    requestMakePosition.symbol = Symbol();
    requestMakePosition.volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
    requestMakePosition.type = orderType;
    // While waiting for position to close, the price could have changed
    requestMakePosition.price = orderType == ORDER_TYPE_BUY ?
                                SymbolInfoDouble(_Symbol,SYMBOL_ASK) :
                                SymbolInfoDouble(_Symbol,SYMBOL_BID) ;
    requestMakePosition.sl = orderType == ORDER_TYPE_BUY ?
                             rates[0].low :
                             rates[0].high;
    requestMakePosition.deviation = inp_deviation;
    requestMakePosition.magic = EXPERT_MAGIC;



    if(!OrderCheck(requestMakePosition,checkResult))
     {
      // If the structure check fails, report a check error
      PrintFormat("Error when checking a new position order: %d - %s",checkResult.retcode, checkResult.comment);
     }
    else
     {
      if(!OrderSend(requestMakePosition,result))
       {
        // If position opening failed, report an error
        PrintFormat("Error opening position: %d - %s",result.retcode,result.comment);
       } // if(!OrderSend(requestMakePosition

      // Trading completed, reset flag just in case
      tradingNeeds = false;
     } // else (!OrderCheck(requestMakePosition))
   } // if(tradingNeeds)

例 11.主要交易代码(大部分空间用于填充结构和检查错误)

此示例的完整源代码包含在附件中:MADeals.mq5。


使用标准库中的指标类

标准指标的类位于 <Include\Indicators> 文件夹中。您可以通过导入文件 <Include\Indicators\Indicators.mqh> (注意文件名末尾的“s”) 一次性包含所有这些,或按组加载它们 —— 例如 “Trend.mqh”、“Oscillators.mqh”、“Volumes.mqh” 或 “BillWilliams.mqh”。还有单独的文件包含时间序列访问类(“TimeSeries.mqh”)和用于处理自定义指标的类(“Custom.mqh”)。

该文件夹中的其余文件是辅助模块,对于那些不熟悉面向对象编程的人来说可能用处不大。文件夹中的每个“功能”文件通常包含几个相关的类。这些类通常按照一致的约定命名:前缀 C 后跟指标创建函数中使用的相同名称。例如,用于处理移动平均线的类称为 CiMA,可以在 “Trend.mqh” 中找到。

使用这些类与使用原生 MQL5 指标函数非常相似。主要区别包括方法调用及其命名。在第一阶段 —— 创建 —— 我们调用 Create 方法,并为指标传递必要的参数。在第二阶段 —— 获取数据 —— 我们使用 Refresh 方法,通常不带参数。如果需要,您可以指定要更新的时间框架,例如:(OBJ_PERIOD_D1 | OBJ_PERIOD_H1)。在使用过程中,我们使用 GetData 方法,最常见的是两个参数:缓冲区号和烛形索引(注意,索引遵循时间序列模型,从右到左增加)。

在示例 12 中,我提供了一个使用 CiMA 类的最小 EA 交易。该 EA 仅输出第一个关闭的烛形上的移动平均线的值。如果您想了解如何在实际交易策略中使用这种基于类的方法,请将上一节中的“EA 交易”(MADeals.mq5)复制到一个新文件中,并用示例 12 中的相应行替换。

#include <Indicators\Indicators.mqh>
CiMA g_ma;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
 {
//--- Create the indicator
  g_ma.Create(_Symbol,PERIOD_CURRENT,3,0,MODE_SMA,PRICE_CLOSE);

//---
  return(INIT_SUCCEEDED);
 }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
 {
//---
  Comment("");
  
 }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
 {
//--- Get data  
  g_ma.Refresh();
 
//--- Use
  Comment(
    NormalizeDouble(
      g_ma.GetData(0,1),
      _Digits
    )
  );
 }
//+------------------------------------------------------------------+

例 12.使用 CiMA 类(移动平均指标)


结论

阅读完本文后,您现在应该能够编写简单的 EA 交易来快速制作任何直接交易策略的原型 —— 无论是仅基于烛形数据还是结合通过指标缓冲区(而不是图形表示)绘制信号的标准指标。我希望这个话题看起来不太复杂。但如果确实如此,可能值得重新审视早期文章中的材料,以便更清楚地理解。

在下一篇文章中,我计划介绍一款技术上已准备好在市场上发布的 EA 交易。该 EA 将包含比本文中的第二个示例更多的验证检查。这些检查将使 EA 更加稳健和可靠。其结构也会略有不同,OnTick 函数将不再作为业务逻辑的唯一中心。将会出现额外的函数来更好地组织代码。最重要的是,EA 将获得处理订单错误(例如重新报价)的能力。为了实现这一点,我们将重组 OnTick,以便 EA 操作的每个“阶段”(例如,进行交易、等待新柱、计算手数……)都可以直接访问,而不必经过其他阶段。我们还将使用 TradeTransaction 事件来跟踪服务器响应。结果将是一个功能有序、易于修改的模板,您可以使用它来构建任何复杂程度的 EA —— 仍然不需要深入 OOP,但完全可操作和应用于实际环境。

本系列先前文章列表:

本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/15727

附加的文件 |
MADeals.mq5 (20.77 KB)
TrendPendings.mq5 (12.84 KB)
最近评论 | 前往讨论 (3)
Utkir Khayrullaev
Utkir Khayrullaev | 7 1月 2025 在 11:06
非常感谢你们的辛勤工作!很多事情都变得简单明了了。
Roman Shiredchenko
Roman Shiredchenko | 19 2月 2025 在 14:33

这篇文章写得非常清楚,解释了很多东西,非常感谢。特别是最后如何通过类来使用指标!酷!我会考虑在开发简单 TS 时测试原型。

Oleh Fedorov
Oleh Fedorov | 19 3月 2025 在 11:39
干杯很高兴能帮上忙。
交易中的神经网络:多智代自适应模型(终篇) 交易中的神经网络:多智代自适应模型(终篇)
在上一篇文章中,我们讲述了多智代自适应框架 MASA,它结合了强化学习方法和自适应策略,在动荡的市场条件下提供了盈利能力、及风险之间的和谐平衡。我们已在该框架内构建了单个智代的功能。在本文中,我们继续我们已开始的工作,令其得出合乎逻辑的结论。
在MQL5中实现基于经济日历新闻事件的突破型智能交易系统(EA) 在MQL5中实现基于经济日历新闻事件的突破型智能交易系统(EA)
重大经济数据发布前后市场波动率通常显著上升,为突破交易策略提供了理想的环境。在本文中,我们将阐述基于经济日历的突破策略的实现过程。我们将全面覆盖从创建用于解析和存储日历数据的类,到利用这些数据开发符合实际的回测系统,最终实现实盘交易执行代码的完整流程。
使用MQL5和Python集成经纪商API与智能交易系统 使用MQL5和Python集成经纪商API与智能交易系统
在本文中,我们将探讨如何将MQL5与Python相结合,以执行与经纪商相关的操作。想象一下,您有一个持续运行的智能交易系统(EA),它托管在虚拟专用服务器(VPS)上,并代表您执行交易。在某个阶段,EA 管理资金的能力变得至关重要。这包括为您的交易账户入金和发起出金等操作。在本文中,我们将阐明这些功能的优势和具体实现方法,从而确保将资金管理无缝地集成到您的交易策略中。敬请关注!
日志记录精通指南(第三部分):探索日志处理器(Handlers)实现方案 日志记录精通指南(第三部分):探索日志处理器(Handlers)实现方案
在本文中,我们将探索日志库中"处理器"(handlers)的概念,理解其工作原理,并创建三种基础实现:控制台、数据库和文件。我们将覆盖从处理器的基本结构到实际测试,为后续文章中的完整功能实现奠定基础。