English Русский Español Deutsch 日本語 Português
preview
掌握 MQL5:从入门到精通(第五部分):基本控制流操作符

掌握 MQL5:从入门到精通(第五部分):基本控制流操作符

MetaTrader 5示例 |
362 2
Oleh Fedorov
Oleh Fedorov

概述

在本系列的这篇文章中,我们将重点介绍如何教程序做出决策。为了实现这一目标,我们将开发一个指标。该指标将显示内含柱(inside bars)或外包柱(outside bars)。

  • 外包柱是指在两个方向上突破前一根烛形范围的烛形。
  • 内含柱是指收盘后仍处于前一根烛形范围内的一根烛形。
  • 我们将标记每个已关闭的烛形。如果烛形仍在形成,我们就不会在其上设置任何标记。
  • 标记仅适用于“长”柱形:对于外包柱,标记将放置在当前柱形上,而对于内含柱,标记将放置在前一个柱形上。这个选择是主观的,所以如果你觉得指标有用,但不同意这个决定,可以随意调整代码中的相应部分。
  • 让我们使我们的指标具有通用性。它将能够显示这两个选项,用户将使用输入参数确定选择哪个选项。

开发此指标将要求我们使用本文中讨论的控制流运算符。由于指标相对简单,我们将主要使用条件语句和循环 — 这项任务所需的一切。但是,为了进行额外的练习,您可以考虑使用以下内容重写指标:

  • 不同类型的循环;
  • 使用 switch-case 运算符代替部分(或全部)条件语句。

到目前为止,我们的程序相当有限,以严格的线性方式执行指令,没有决策能力。阅读本文后,您将能够创建更复杂的程序。

首先,我们将详细介绍布尔(逻辑)表达式,因为它们构成了理解其余内容的基础。


逻辑表达式的详细概述

提醒一下,布尔数据类型(bool)只有两个可能的值: true(真)false(假)

每次使用逻辑运算符时,程序本质上都会询问其当前数据:“(条件)是 true 吗?”(“(prev_calculated == rates_total)是 true 吗?如果是的话,我就应该退出当前函数!否则,我需要继续计算……”。)

在 MQL5 中,数字 0 代表 false,而布尔表达式中的所有其他数字,无论正数还是负数,都被视为 false。

但是,字符串不能转换为布尔值。任何这样做的尝试都会导致编译器错误。但是,字符串是可以比较的。

逻辑数据通常来自比较表达式。最简单的逻辑表达式是比较语句。在 MQL5 中,使用以下比较运算符(类似于标准数学符号):大于(>)、小于(<)、等于(==)、不等于(!=)、大于等于(>=)、小于等于(<=)。例如,只有当两个元素相等时,表达式(a==b)的计算结果才为 true。我认为这一点是清楚的。

比较字符串时,字符表中具有较高 ASCII 值的字符串将被认为较大。例如:("A" > "1") 为 true,而 ("Y" < "A") 为 false。如果字符串包含多个字符,则比较将从左到右逐个字符进行,直到发现差异。空字符串(“”)被认为比任何其他字符串都小。如果两个字符串的起始字符相同,但其中一个较短,则认为较短的一个较小。

还有三种逻辑操作:

  • 逻辑非或反(也称为逻辑“ NOT ”),用感叹号(!)表示;
    将逻辑值转换为其相反值。
  • 逻辑与(逻辑“ AND ”)用两个与符号(&&)表示;
    表达式的两个部分(左部分和右部分)都为 true 时,带有此运算符的表达式才为 true。
  • 逻辑或(逻辑“”)用两条垂直线(||)表示;
    如果表达式中至少有一个部分为 true,则带有此运算符的表达式为 true。
为了更好地理解所有这些逻辑运算符如何相互作用,请参考以下真值表:
表 1.逻辑非(!)
表 2.逻辑与 (&&)
表 3.逻辑或 (||)
非
与
或

为了加强你的理解,试着确定下面每个逻辑表达式的结果。注释中提供了提示,但我建议在不先看的情况下尝试练习。请仅使用注释来验证您的答案。

int a = 3;
int b = 5; 

bool e = ( a*b ) > ( a+b+b );          // true

Print ( (a>b) );                       // false
Print ( (a<b) || ((a/b) > (3/2)) );    // true
Print ( !a  );                         // false
Print ( ! (!e) );                      // true
Print ( (b>a) && ((a<=3) || (b<=3)) ); // true
Print ( "Trust" > "Training" );        // true
Print ( a==b );                        // false
Print ( b=a );                         // 3 (!!!) (therefore, for any logical operator this is always true)
//  In the last example, a very common and hard-to-detect mistake has been made. 
//    Instead of a comparison, an assignment was used, which results in a value of type int! 
//  Fortunately, the compiler will usually warn about this substitution.

例 1.使用逻辑运算符的示例

当表达式中包含多个不带括号的运算符时,一般从左到右进行求值(赋值除外)。执行顺序遵循以下规则:

  1. 所有算术运算符都首先执行。
    • *、/、%
    • +、-
  2. 接下来是比较运算符(==、!=、>、<、>=、<=)。
  3. 接下来是其余的逻辑运算符,它们按照列出的顺序执行:
    • 非(!);
    • 与(&&);
    • 或(||);
  4. 赋值运算符(=)将最后执行。

如果您不确定您的代码中的执行顺序,请使用括号,如示例 1 所示。括号通过强制括号中的运算首先执行来确保清晰性。此外,这也通过适当的缩进提高了代码的可读性。


if 语句

if 语句,也称为条件语句、分支语句或决策结构,可帮助程序做出选择。

它的语法很简单:

if (condition) action_if_condition_is_true;
else action_in_all_other_cases; // The "else" branch is optional

例 2 .条件语句模板。

为什么它被称为“分支”?如果我们将其可视化,结构看起来像一个树枝,分割了程序流:

分支运算符

图 01.分支运算符

如果你运用你的想象力,你可以看到执行的主要“主干”是如何分裂成独立的分支的。在旧的流程图中,整个算法通常类似于灌木丛。例如,假设你想读一本书,但你的台灯坏了。你可以先试着打开灯 — 也许它有效?如果无效,检查灯泡是否在插座中 — 也许有人把它拆了?如果灯泡在那里,但灯仍然不亮,检查它是否烧坏了。如果它烧坏了,试着更换。如果这些步骤都不起作用,请尝试使用另一盏灯。这个逻辑过程可以表示为决策树:

分支图示

图 2.分支图示

在这个流程图中,执行从底部的绿色“开始”点开始。在我看来,它清楚地表明了分支是如何在每个选择点发生的。每个蓝色标记的决策点代表一条分支路径。

注意,MQL5 语言中的“action_if_condition_is_true”和“action_in_all_other_cases”可以是单个语句,但如果需要,该语句也可以是复合语句

我来解释一下。在 MQL5 中,操作可以用花括号括起来(也可以不用)。花括号充当单独的“实体”,并分隔单独的代码。块内声明的任何变量都不能在块外访问。在某些情况下,花括号是强制性的,但通常是可选的。大括号内的内容将被程序的其他部分视为一个整体。例如,块内声明的变量对块外的任何语句都不可见。我所说的复合语句就是指花括号中的“块”。其中可以有任意数量的操作,但 if 类型命令将执行它们,直到它们遇到关闭主块的花括号。如果省略括号,则仅执行 if 之后的第一个语句,而后续语句则无条件运行。

通常,从属于某些语句(在我们的例子中是 if 或 else)的命令被称为“语句主体”。

在示例 2 的模板中,if 语句的主体为 “action_if_condition_is_true”,else 语句的主体为 “action_in_all_other_cases”。

这是一个实际的例子。此代码根据示例 16 检查指标中是否有新的烛形。该逻辑将先前计算的烛形数量(prev_calculated)与烛形总数(rates_total)进行比较:

if (prev_calculated == rates_total) // _If_ they are equal, 
    
  {                                 //    _then_ do nothing, wait for a new candlestick
    Comment("Nothing to do");       // Display a relevant message in a comment
    return(rates_total);            // Since nothing needs to be done, exit the function 
                                    //   and inform the terminal that all bars have been calculated (return rates_total)
  }

// A new candle has arrived. Execute necessary actions
Comment("");                        // Clear comments
Print("I can start to do anything");   // Log a message


// Since the required actions will be executed 
//   only if prev_calculated is not equal to rates_total, 
//   else branch is not needed in this example.
// We just perform the actions.

例 3.使用 prev_calculated 和 rates_total 等待新的烛形

这里的 if 语句包含两个操作:Comment() 和 return。else 语句被省略,因为所有必要的操作都发生在主执行块中。

在花括号内,我们可以根据需要使用任意数量的运算符。如果删除花括号,则只有 Comment("Nothing to do") 语句会有条件地执行。在这种情况下,return 操作符将无条件执行,并且有关工作准备就绪的消息永远不会出现(我建议在分钟图上检查它)。

最佳实践建议:

始终对 if 语句使用花括号 ({}),即使它们只包含一个语句。

坚持这一建议,直到你获得足够的经验,例如通过阅读代码库中其他程序员的代码。括号使应用程序调试更加直观,您将避免花费数小时试图隔离难以发现的错误。此外,如果你对一个语句使用花括号,那么以后添加其他操作会更容易一些。例如,您以后可能希望实现调试信息或提醒的输出。


三元运算符

有时,我们使用条件语句只是将两个可能值中的一个赋给变量。例如:

int a = 9;
int b = 45;
string message;

if( (b%a) == 0 ) // (1)
  {
    message = "b divides by a without remainder"; // (2)
  }
else 
  {
    message = "b is NOT divisible by a"; // (3)
  }

Print (message);

例 4 .转换为三元运算符的条件

在这种情况下,您可以使用三元(三部分)运算符稍微缩短符号:

int a = 9;
int b = 45;
string message;

message = ( (b%a) == 0 ) /* (1) */ ? 
          "b divides by a without remainder" /* (2) */ : 
          "b is NOT divisible by a" /* (3) */ ;
  
Print (message);

例 5.三元运算符

该运算符遵循与 if 语句相同的顺序: condition -> (问号) -> value_if_condition_is_true -> (冒号而不是“else”)-> value_if_condition_is_false 。如果这些三元运算符适合我们的任务,您可以使用它们。

三元运算符和传统 IF 语句之间的关键区别在于,三元运算符必须返回一个与赋值左侧变量类型匹配的值。因此,它只能在表达式中使用。相比之下,传统的 IF 语句在其正文中包含表达式,但不能直接返回值。


switch — case 语句

在某些情况下,我们需要从不止两个选项中进行选择。虽然可以使用多个嵌套的 IF 语句,但它们会显著降低代码的可读性。在这种情况下, switch语句(也称为选择器)是首选解决方案。语法模板:

switch (integer_variable) 
  {
    case value_1:
      operation_list_1;
    case value_2
      operation_list_2;
    // …
    default:
      default_operation_list;
  }

例 6 .switch-case 语句的结构

它的工作方式如下。如果 “integer_variable” 的值与其中一个值(“value_1”,“value_2”......)匹配,则执行从该情况开始并按顺序继续。通常情况下,只需完成一个区块就足够了。因此,在每个操作列表之后,我们通常会添加一个 break 语句,该语句会立即终止 switch 语句。如果没有匹配的 case,则执行 default 部分。default 部分必须始终放在语句的末尾。

在 MQL5 中,switch-case 广泛用于处理 EA 交易中的交易错误以及处理用户输入,例如键盘按键或鼠标移动。以下是处理交易错误的函数的典型示例:

void PrintErrorDescription()
{

  int lastError = GetLastError();

// If (lastError == 0), there are no errors…
  if(lastError == 0)
    {
      return;  // …no need to load cpu with unnecessary computations.
    }

// If there are errors, output an explanatory message to the log.        
  switch(lastError)
    {
      case ERR_INTERNAL_ERROR:         
        Print("Unexpected internal error"); // You can select any appropriate action here
        break;                
      case ERR_WRONG_INTERNAL_PARAMETER:
        Print("Wrong parameter in the inner call of the client terminal function");
        break;
      case ERR_INVALID_PARAMETER:
        Print("Wrong parameter when calling the system function");
        break;
      case ERR_NOT_ENOUGH_MEMORY:
        Print("Not enough memory to perform the system function");
        break;

      default: 
        Print("I don't know anything about this error");
    } 
}

例 7.标准错误处理流程


带前提条件的循环(while)

循环是一种允许重复执行的代码块控制结构。

在编程中,我们经常会遇到某些操作需要执行多次,但使用的数据略有不同的情况。例如,我们可能需要迭代数组的所有元素(例如指标缓冲区),以可视化指标在历史数据上的行为。另一个例子是逐行从文件中读取复杂 EA 的参数。还有许多其他案例。

MQL5 提供了三种类型的循环,每种循环都适用于不同的场景。虽然所有循环都是可互换的,这意味着一个循环总是可以在不损失功能的情况下替换另一个循环,但使用正确类型的循环可以提高代码的可读性和效率。

第一种类型是 while 循环,也称为带前提条件的循环。这个名字来自于事实上,在执行循环体之前要检查条件。从视觉上看,它可以表示如下:

While 循环图

图 3.While 循环图

此运算符的语法模板非常简单:

while (condition)
    action_if_condition_is_true;

例 8.While 循环模板

这里的 “action_if_condition_is_true” 是一个单独的语句,可以是简单的,也可以是复合的。

当程序遇到 while 语句时,它会检查条件,如果条件为 true,它会在循环体中执行操作,然后再次检查条件。这样,循环将一次又一次地执行,直到条件变为 false。由于在执行主体语句之前会检查条件,因此可能会出现循环甚至一次都不会执行的情况

通常这个语句用于无法计算准确重复次数的情况。例如,当您需要读取文本文件时,程序无法知道它有多长。程序将持续读取文件,直到到达文件结束标记。一旦达到,读取就会停止。在编程中,这种情况很常见。

在任何情况下,循环体内必须至少有一个参数发生变化,这会影响括号中的条件。如果没有这样的参数,循环将变得无限,然后只能通过关闭终端窗口来中断。在最坏的情况下,如果处理器被锁定在无限循环中,即使是键盘输入或鼠标移动等基本操作也可能被忽略。停止程序的唯一方法是强制重新启动系统。因此,您应该确保至少有一次机会完成该计划。例如,您可以添加一个条件 (&& !IsStopped () ),如果程序终止,它将停止循环。

让我们使用 while 循环实现一个简单的任务,将 1 到 5 的数字相加。假设我们不知道算术级数。

使用 while 循环,这个问题可以按如下方式解决:

//--- Variables declaration
int a  = 1;  // initialize (!) the variable parameter used in the condition
int sum = 0; // result
string message = "The sum of "+ (string) a; // show message

//--- Perform main operations
while (a <= 5)  // While a is less than 5
  {
    sum += a; // Add the value to the sum
    if ( a != 1) // The first value is already added at the time of initialization
    {
      message += " + " + string (a); // Further operations
    }
    
    a++; // Increase parameter (very important!)
  }

//-- After the loop is completed, output a message
message += " is " + (string) sum;  // message text
Comment (message); // show the comment

例 9.使用 while 循环

尽管迭代次数很少,但这个例子演示了 while 循环的机制。


后置条件循环(do…while)

do…while 循环在处理完主体的所有动作后使用条件检查。从图形上看,这个循环可以表示如下:

Do...while 循环图

图 4.Do-while 循环图

语法模板:

do
  actions_if_condition_is_true;
while (condition);

例 10.do-while 语句模板

与 while 不同,循环体内部的操作在此语句中至少执行一次。其余的是类似的:程序执行一个操作,检查告诉它是否返回开始的条件,如果需要,再次执行循环体中的所有操作。

至于应用领域,这可能是,例如,在市场观察窗口中检查交易工具。算法可能看起来像这样:

  • 从列表中取出第一个交易品种(如果我们绝对确定它确实存在), 
  • 执行所需的操作(例如,检查现有订单) 
  • 然后检查列表中是否还有其他交易品种;
  • 如果找到另一个交易品种,则继续操作,直到列表结束。

这种循环形式的求和看起来几乎与前一种情况相同。只有两行发生了变化:循环本身的描述

//--- Variables declaration
int a  = 1;  // initialize (!) the variable parameter used in the condition
int sum = 0; // result
string message = "The sum of "+ (string) a; // show message

//--- Perform main operations
do // Execute
  {
    sum += a;    // Add value to the sum
    if ( a != 1) // The first value is already added at the time of initialization
    {
      message += " + " + string (a); // Further operations
    }
    
    a++;         // Increase parameter (very important!)
  }
while (a <= 5)  // While a is less than 5

//-- After the loop is completed, output a message
message += " is " + (string) sum;  // message text
Comment (message);                 // show the comment

例 11.使用 do-while 循环


For 循环

for 循环是最常用的一种,因为它用于迭代各种序列,在可以轻松计算要迭代的元素数量的情况下。语法模板:

for (initialize_counter ; conditions ; change_counter)
  action_if_condition_is_true;

例 12.For 循环模板

与以前的循环形式不同,使用 for 循环您可以清楚地看到哪个参数正在变化(在模板中我将此参数称为计数器)以及如何变化。然后,在循环的主体中,您可以专注于解决主要任务(例如,迭代数组的元素)。

本质上这个循环和 while 是一样的,也就是在执行主体操作符之前也进行检查。以下是如何使用 For 循环对数字进行求和:

//--- Variables declaration
int a;   // do NOT initialize the variable parameter, only describe 
         //   (this is optional as you can describe it in the loop header)
int sum = 0;                    // result
string message = "The sum of "; // a shorter message for the user, 
                                //   as the value is not yet known

//--- Perform main operations
for (a=1; a<=5; a++)// For (each `a` from 1 to 5) /loop header/
  {
    sum += a;    // Add `a` to the sum
    if ( a != 1) // the first value does not contain "+" 
    {
      message += " + " // add "+" before all sequence members starting from the second
    } 
   message += string (a); // Add sequence elements to the message
    
            // Changes to the counter are described not here, but in the loop header as the last parameter.
  }

//-- After the loop is completed, output a message
message += " is " + (string) sum;  // message text
Comment (message);                 // show the comment

例 13.使用 for 循环

在这个例子中,我们看到了比以前更多的变化(以黄色突出显示)。再次强调,所有与循环计数器(可修改参数)相关的步骤都移至循环头:初始化(a = 1)、条件检查(a <= 5)(如果条件为 true,则执行主循环体)、增量(a++)和条件重新评估 — 在每次迭代结束时,计数器都会更新,并再次检查条件。


Break 和 continue 语句

有时,没有必要执行循环的所有步骤。

例如,在数组中搜索值时,如果中途找到目标值,则并不总是需要迭代整个数组。在这种情况下,当我们需要完全终止循环时,我们使用 break 语句,立即退出循环并继续执行其后的下一个操作。例如:

string message = "";

for (int i=0; i<=3; i++)
  {
    if ( i == 2 )
      {
        break;
      }
    
    message += " " + IntegerToString( i );
  }

Print(message); // Result: 0 1 (no further execution)

例 14.使用 break 语句

在某些情况下,我们不需要执行循环体,但循环本身不应该终止 — 我们只是想跳过特定的迭代。例如,在示例 14 中,如果我们想排除数字 2 的打印,同时保留所有其他数字,则需要稍微修改代码,将 `break` 替换为 ` continue` 。continue 语句立即跳至下一次迭代:

string message = "";

for (int i=0; i<=3; i++)
  {
    if ( i == 2 )
      {
        continue;
      }
    
    message += " " + IntegerToString( i );
  }

Print(message); // Output: 0 1 3 (the number 2 is skipped)

例 15.使用 continue 语句

break 语句既可以在循环内部使用,也可以在 case 语句中使用;continue 语句只能在循环内部使用。

这就是有关语句的全部内容。现在让我们看看这些运算符如何帮助创建现实世界的程序。


关于 MQL5 向导(指标模式)的几句话

我希望您已经熟悉了使用第一篇文章中的向导创建指标。然而,我将简要回顾向导的步骤,重点介绍与以前不同的方面。

与第一篇文章相比,向导的第一个窗口没有引入任何新内容,因此我将跳过它。第二个窗口虽然也不是新窗口,但现在更有意义了,因为您熟悉通过输入参数管理的全局设置。有时,直接在向导中配置这些参数更直观。提醒:参数名称可以是任意的。但我建议添加前缀 inp_,以便在代码中轻松区分它们。

添加输入参数

图 5.使用向导添加程序参数

下一个重要步骤是选择 OnCalculate 方法的格式(图 6)。OnCalculate 是 MQL5 语言中任何指标的主要方法。每次终端计算该指标时都会调用它。该函数可以接受不同的输入参数集,具体取决于您在向导中的选择。

选择 OnCalculate 方法格式

图 6.选择 OnCalculate 方法格式

选择上选项适用于大多数情况,因为在这种情况下,函数接受自动生成的数组:open、high、low、close、volume、time。用户可以根据需要处理这些数组。

然而,在某些特殊情况下,我们希望让用户能够选择使用哪条曲线来计算给定的指标。例如,移动平均线可以基于高、低或现有指标的输出。在这种情况下,我们应该选择下面的选项,然后包含曲线数据的数组将被传输到 OnCalculate。然后,在输入参数窗口的编译指标中会出现一个用于选择曲线的特殊选项卡。图 7 显示了 Upper 和 Lower 选项的现成指标的启动窗口。

不同 OnCalculate 格式的指标启动窗口比较

图 7.不同 OnCalculate 格式的指标启动窗口比较

我想要提请您注意向导中的最后一点。在对话框的最后一步中,创建指标时,您可以选择指标的显示方式(图 8)。要显示指标,您需要添加一个缓冲区,它是一个包含用于呈现指标的数据的特殊数组。有多种显示样式可供选择,从简单的线条(例如移动平均线)到多色直方图和蜡烛图。通过高级自定义,您甚至可以绘制自定义图形(尽管这超出了向导的功能范围)。

指标渲染参数

图 8.指标渲染参数

无论如何,如果您希望指标能够绘制某些内容,同时其他程序(例如,EA 交易)可以轻松访问其工作结果,则您需要添加至少一个缓冲区。

缓冲区只是一个数字数组,用于存储我们的指标为每个烛形计算的数据。一般来说,这些数据不一定用于绘图。有时缓冲区会存储颜色或其他缓冲区中计算所需的一些中间计算数据。

可以手动添加,但需要额外的(虽然简单)设置。对于初学者,我强烈建议使用向导来创建缓冲区。

通常,在绘制线条时,每条正在计算的曲线都需要至少一个缓冲区。要绘制箭头,您通常需要至少两个缓冲区:每个方向一个。虽然你可以只使用一个,但如果箭头只用于任何烛形的一个计算值,而不显示方向。更复杂的情况可能需要额外的缓冲区(在未来的文章中介绍)。目前,所提供的信息已经足够。

现在,让我们创建一个指标

  • 名称:InsideOutsideBar, 
  • 一个参数:inp_barsTypeSelector,类型 为int,默认值为0, 
  • 向导第三个屏幕上的 OnCalculate 的上层格式(带有数组列表),
  • “Plots” 部分中有两个绘图缓冲区,名称分别为 Up 和 Down,绘图类型为“Arrow”。


向导生成的指标代码

如果上一节中的所有内容都正确完成,您应该得到以下代码:

//+------------------------------------------------------------------+
//|                                             InsideOutsideBar.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"
#property indicator_chart_window
#property indicator_buffers 2
#property indicator_plots   2
//--- plot Up
#property indicator_label1  "Up"
#property indicator_type1   DRAW_ARROW
#property indicator_color1  clrMediumPurple
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1
//--- plot Down
#property indicator_label2  "Down"
#property indicator_type2   DRAW_ARROW
#property indicator_color2  clrMediumPurple
#property indicator_style2  STYLE_SOLID
#property indicator_width2  1
//--- input parameters
input int      inp_barsTypeSelector=0;
//--- indicator buffers
double         UpBuffer[];
double         DownBuffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   SetIndexBuffer(0,UpBuffer,INDICATOR_DATA);
   SetIndexBuffer(1,DownBuffer,INDICATOR_DATA);
//--- setting a code from the Wingdings charset as the property of PLOT_ARROW
   PlotIndexSetInteger(0,PLOT_ARROW,159);
   PlotIndexSetInteger(1,PLOT_ARROW,159);
   
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
   
//--- return value of prev_calculated for the next call
   return(rates_total);
  }
//+------------------------------------------------------------------+

例 16.MQL5 向导生成的指标代码

与往常一样,这里的第一个块是包含参数的标头。然后是描述作者的标准参数。接下来的一行说明指标将位于图表窗口中。此块末尾的两行表示向编译器发送的消息,即我们的指标有两个用于计算的缓冲区和两个用于呈现的缓冲区(如您所知,在这种情况下,这些缓冲区是相同的,尽管在一般情况下缓冲区的数量可能不同):

#property indicator_buffers 2
#property indicator_plots   2

例 17.用于计算和渲染的指标缓冲区的描述

下一个参数块描述了每个缓冲区创建的绘图应该是什么样子:

//--- plot Up
#property indicator_label1  "Up"             // Display name of the buffers
#property indicator_type1   DRAW_ARROW       // Drawing type - arrow
#property indicator_color1  clrMediumPurple  // Arrow color
#property indicator_style1  STYLE_SOLID      // Line style - solid
#property indicator_width1  1                // Line width (arrow size)
//--- plot Down
#property indicator_label2  "Down"
#property indicator_type2   DRAW_ARROW
#property indicator_color2  clrMediumPurple
#property indicator_style2  STYLE_SOLID
#property indicator_width2  1

例 18.渲染参数

下一段代码描述了我们指标的输入参数。在我们的例子中只有一个参数:inp_barsTypeSelector:

//--- input parameters
input int      inp_barsTypeSelector=0;

例 19.指标的输入参数

然后是为渲染缓冲区创建变量的代码。请注意,此缓冲区使用具有 double 类型元素的动态数组。事实上,这是一系列价格水平,指标曲线将基于这些水平构建:

//--- indicator buffers
double         UpBuffer[];
double         DownBuffer[];

例 20.绘图缓冲区的变量

接下来是两个函数的描述:OnInit 和 OnCalculate。

OnInit 与脚本中的函数完全相同。它在指标启动后立即以完全相同的方式启动 - 在执行所有其他函数之前 - 并且只执行一次。在这种情况下,向导已经编写了两次对标准 SetIndexBuffer 函数的调用,用于将我们的数组链接到指标的绘制缓冲区。此外,使用 PlotIndexSetInteger 函数为两个箭头分配了图标。OnInit 函数不接受任何参数并返回初始化成功状态(在这种情况下始终为“成功”)。

正如我之前所写,OnCalculate 是一个在每次报价或其他程序尝试使用我们的指标时调用的函数。现在它是空的。在这个函数中,我们将描述我们的指标的主要动作。

该函数接受多个变量作为参数:

  • 图表上的总柱数(rates_total);
  • 指标先前计算的柱形数量 (prev_calculated)。在第一次启动时,prev_calculated 的值为零,但向导创建的函数的实现会在工作结束时返回给定订单号的总柱数,并且在下次启动开始时,终端会将此作为 prev_calculated 传递给函数。顺便说一下,这是确定新烛形的算法之一的基础:如果 rates_total==prev_calculated,则烛形相同,无需计算任何内容,但如果它们不相等,则我们开始下一步;
  • 多个数组:prices, time, volumes, spreads(价格、时间、交易量、点差)……这是您创建自己的指标时所需要的。请注意,数组是通过引用传递的(因为数组不能通过值传递),但它们有一个 const 修饰符,它告诉编译器这些数组中的数据是不可变的。

现在我们可以开始编程我们的指标了。


编程指标

我想做的第一件事是稍微调整一下指标参数。我们只能有两种类型的形态:内含柱形或外包柱形。让我们在预处理器指令之后立即创建一个全局枚举:

enum BarsTypeSelector
 {
  Inside  = 0, // Inside bar
  Outside = 1  // Outside bar
 };

例 21.计算类型的枚举

让我们更改输入参数的默认类型和值:

//--- input parameters
input BarsTypeSelector      inp_barsTypeSelector=Inside;

例 22.更改输入参数的默认类型和值

现在,让我们扩展定制指标的可能性。让我们添加更改图标外观和图标从栏中可见缩进的功能。为此,让我们手动(所以您记住添加参数非常容易)向输入参数部分添加两个变量:

//--- input parameters
input BarsTypeSelector      inp_barsTypeSelector=Inside;
input int                   inp_arrowCode=159; 
input int                   inp_arrowShift=5;

例 23.外观设置的附加变量

让我提醒你,输入参数本身不会以任何方式影响指示器的操作;它们需要在某个地方使用。为了应用更改,让我们修改 OnInit 函数:

int OnInit()
 {
//--- indicator buffers mapping
  SetIndexBuffer(0,UpBuffer,INDICATOR_DATA);
  SetIndexBuffer(1,DownBuffer,INDICATOR_DATA);
//--- setting a code from the Wingdings charset as the property of PLOT_ARROW
  PlotIndexSetInteger(0,PLOT_ARROW,inp_arrowCode);
  PlotIndexSetInteger(1,PLOT_ARROW,inp_arrowCode);
  
  PlotIndexSetInteger(0, PLOT_ARROW_SHIFT, inp_arrowShift); 
  PlotIndexSetInteger(1, PLOT_ARROW_SHIFT, -inp_arrowShift);

//---
  return(INIT_SUCCEEDED);
 }

例 24.OnInit 函数的修改

现在让我们继续编写 OnCalculate 函数中的主要操作。为了使代码更紧凑,我将跳过此函数的标题,并立即描述执行有用工作的代码。

/********************************************************************************************
 *                                                                                          *
 * Attention! All arrays in the code are NOT series, so we have larger numbers on the right. *
 *                                                                                          *
 ********************************************************************************************/

//--- Description of variables
  int i,     // Loop counter
      start; // Initial bar for historical data

  const int barsInPattern = 2; // For proper preparation we need to know,
                               //   how many bars will be involved in one check
                               //   I've added a constant to avoid the presence of 
                               //   "magic numbers" that come from nowhere,
                               //   we could use the #define directive instead of the constant

//--- Check the boundary conditions and set the origin
  if(rates_total < barsInPattern)      // If there are not enough bars to work
    return(0);                         // Do nothing

  if(prev_calculated < barsInPattern+1)// If nothing has been calculated yet
   {
    start = barsInPattern;     // Set the minimum possible number of bars to start searching 
   }
  else
   {
    start = rates_total — barsInPattern; // If the indicator has been running for some time,
                                         //   Just count the last two bars
   }

//--- To avoid strange artifacts on the last candlestick, initialize the last elements of the arrays 
//      with EMPTY_VALUE
  UpBuffer[rates_total-1] = EMPTY_VALUE;
  DownBuffer[rates_total-1] = EMPTY_VALUE;

//---
  for(i = start; i<rates_total-1; i++) // Start counting from the starting position 
                                       //   and continue until there are no more closed barsя
                                       // (If we needed to include the last - unclosed - bar, 
                                       //   we would set the condition i<=rates_total-1)
   {
    // First, let's clear both indicator buffers (initialize to an empty value) 
    UpBuffer[i] = EMPTY_VALUE;
    DownBuffer[i] = EMPTY_VALUE;
    
    if(inp_barsTypeSelector==Inside) // If the user wants to display inside bars
     {
      // Check if the current bar is inside
      if(high[i] <= high[i-1] && low[i] >= low[i-1])
       {
        // And if yes, we mark the previous (larger) candlestick
        UpBuffer[i-1] = high[i-1];
        DownBuffer[i-1] = low[i-1];
       }
     }
    else // If outside bars are needed
     {
      // Check if the current bar is outside
      if(high[i] >= high[i-1] && low[i] <= low[i-1])
       {
        // Mark the current candlestick if necessary
        UpBuffer[i] = high[i];
        DownBuffer[i] = low[i];
       }
     }
   }

//--- return value of prev_calculated for the next call
  return(rates_total);

例 25.OnCalculate 函数体

在我看来,代码注释已经足够了,进一步分析它没有什么特别的意义。如果我错了,请在评论中提问。此代码应插入到 OnCalculate 函数中,位于左花括号和右花括号之间,删除之前的所有内容(我在示例中包含了 return 运算符 - 在最后一行)。该指标的完整工作代码附于文章中。

图 9 显示了该指标的运行。在左侧,内含柱形之前的柱形以粉红色标记,外包柱形则标记在右侧。

InsideOutsideBar 指标的工作原理

图 9.InsideOutsideBar 指标的工作原理。内侧柱位于左侧,外侧柱位于右侧


结论

掌握了本文中描述的运算符后,您将能够理解用 MQL5 编写的大多数程序,并编写任何复杂程度的算法。本质上,所有编程都可以简化为最简单的操作,例如算术或赋值,从几个选项中进行选择(if 或 switch)并重复所需片段所需的次数(循环)。函数提供了一种方便组织操作的方法,而对象则可以方便地排列函数及其外部数据。

现在你肯定不再是初学者了。从这一点到成为 MQL 5专业人士还有很长的路要走。例如,您需要弄清楚如何在新开发中重用以前创建的指标,以及如何创建可以为您交易的 EA 交易。MQL5 平台还有许多值得研究的功能,以便能够完成您想要的一切:用于您的 EA 交易的图形界面、“heiken”或“renko”等不寻常的指标、便捷的服务和用于自动交易的有利可图的策略......因此,这个系列还会继续下去。在下一篇文章中,我们将尽可能详细地介绍如何创建 EA 交易。然后我将在 MQL5 环境中提出我对 OOP 的看法。这样就结束了“通用”部分,之后我们可能会深入研究一些平台功能以及Include文件夹中终端自带的标准代码库。

本系列先前文章列表:

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

附加的文件 |
最近评论 | 前往讨论 (2)
Juan Luis De Frutos Blanco
Juan Luis De Frutos Blanco | 31 8月 2024 在 09:38

Buenos días Fedorov、

Debo agradecerte el esfuerzo en tus artículos: Me están resultando muy útiles.

致敬、

Oleh Fedorov
Oleh Fedorov | 1 9月 2024 在 12:59
Juan Luis De Frutos Blanco #:

你好,费多罗夫、

Debo agradecerte el esfuerzo en tus artículos: Me están resultando muy útiles.

致敬、

谢谢,先生。
基于MQL5的订单剥头皮交易系统 基于MQL5的订单剥头皮交易系统
这款MetaTrader 5 EA实现了基于订单流的剥头皮交易策略,并配备了高级风险管理功能。它使用多种技术指标,通过订单的不平衡性来识别交易机会。回测结果显示该策略具有潜在的盈利能力,但同时也突显了需要进一步优化的必要性,尤其是在风险管理和交易结果比率方面。该策略适合经验丰富的交易者,但在实际部署之前,需要进行彻底的测试和深入理解。
重构MQL5中的经典策略(第三部分):富时100指数预测 重构MQL5中的经典策略(第三部分):富时100指数预测
在本系列文章中,我们将重新审视一些知名的交易策略,以探究是否可以利用AI来改进这些策略。在今天的文章中,我们将研究富时100指数,并尝试使用构成该指数的部分个股来预测该指数。
HTTP和Connexus(第2部分):理解HTTP架构和库设计 HTTP和Connexus(第2部分):理解HTTP架构和库设计
本文探讨了HTTP协议的基础知识,涵盖了主要方法(GET、POST、PUT、DELETE)、状态码以及URL的结构。此外,还介绍了Conexus库的构建起点,以及CQueryParam和CURL类,这些类用于在HTTP请求中操作URL和查询参数。
交易中的神经网络:统一轨迹生成模型(UniTraj) 交易中的神经网络:统一轨迹生成模型(UniTraj)
理解个体在众多不同领域的行为很重要,但大多数方法只专注其中一项任务(理解、噪声消除、或预测),这会降低它们在现实中的有效性。在本文中,我们将领略一个可以适配解决各种问题的模型。