📘

MQL5 里真正撑起 EA 的几个内核

◍ MQL5 里真正撑起 EA 的几个内核

MQL5 跟早期 MQL4 最大的分水岭,是它把订单、持仓、历史分成了三套独立对象:CTrade、CPositionInfo、COrderInfo。写 EA 时如果还用老式 OrderSend 单函数思维,会在 MT5 回测里频繁撞到‘交易上下文忙’的错误。 一套能跑的 MQL5 程序,核心通常包含:OnInit 里初始化指标句柄、OnTick 里取最新价并判断信号、用 CTrade::Buy 或 Sell 发单。2024 年 9 月官方示例库里,标准均线穿越 EA 的平均代码行数约 180 行,其中 60% 在处理仓位状态而非下单本身。 外汇与贵金属杠杆品种波动剧烈,用 MQL5 做自动化前务必在策略测试器开‘逐笔成交’模式验证,否则实盘可能因点差跳空触发非预期止损。

「MQL5 里数组、函数与预处理器的实操骨架」

在 MT5 写 EA 或脚本前,先要把三类基础构件摸透:数组管数据存储、自定义函数管逻辑复用、预处理器管编译前的代码整形。这三者不依赖任何指标或策略,纯语言层,开 MT5 按 F4 进 MetaEditor 就能逐条验证。 数组本质是一个从 0 开始编号的同类型容器。声明固定为 数据类型 数组名[长度];,例如 int closePrices[5]; 即开了 5 个整数格,索引 0~4。赋值可后置也可声明时写死:int p[5]={10,20,30,40,50}; 一步定型。访问时 Print(p[0]); 打出 10,越界会编译报错,这是高频踩坑点。 自定义函数让你把重复计算封成一个黑盒。基础乘法函数结构如下,返回类型、参数、函数体缺一不可: double MyFunction(int param1, double param2) { double result = param1 * param2; return result; } 逐行看:首行定义名MyFunction,收 int 与 double 两参,承诺吐回 double;大括号内先算积存到 result;return 把值交还调用处。在 OnTick 里写 double a=MyFunction(3,1.5); 即得 4.5,比到处写算式清爽。 预处理器跑在编译前,指令全带 # 且不写分号,必须搁全局。最常用 #define 做常量替换: #define MAGIC_NUMBER 10 void OnStart() { int result = MAGIC_NUMBER + 5; Comment(result); } 预处理器先把 MAGIC_NUMBER 文本替换成 10,故 result=15,图表左上角注释输出 15。参数化宏 #define MULTIPLY(x,y) (x*y) 则像内联函数,MULTIPLY(3,5) 直接展开成 15。外汇与贵金属杠杆高、滑点大,任何常量写死前先想好会不会在实盘被跳空击穿。 #undef 可把已定义宏擦掉重来,比如 #define MAGIC_NUMBER 10 后用 #undef MAGIC_NUMBER 便不能再引用,适合在多文件工程里避免命名污染。数组、函数、预处理器组合起来,才接得住后续事件驱动的复杂逻辑。

MQL5 / C++
class="macro">#define MAGIC_NUMBER class="num">10
class="type">void OnStart()
{
   class="type">int result = MAGIC_NUMBER + class="num">5;
   Comment(result);
}

预处理指令与事件钩子怎么切分代码边界

MQL5 预处理器里 #undef 用来注销已定义的宏。比如前面 #define 过 MAGIC_NUMBER,写一行 #undef MAGIC_NUMBER 之后,编译器就忘了这词什么意思,再引用会直接报错。实战里这适合在 EA 某段逻辑里临时屏蔽魔法号,避免和下游模块撞定义。 程序属性用 #property 标元数据,类似书籍版权页。常见写法如 #property version "1.0"、#property copyright "2022",MT5 编译后会在 EA 信息里透出,方便区分多版本回测产物。 #include 把外部 .mqh 合并进主文件。例如主代码写 #include "extraRecipes.mqh",该文件里定义的 MultiplyByTwo() 就能直接调用,不用重写。大型策略把指标计算丢进 include 文件,主 EA 保持瘦身后编译更快。 条件编译靠 #ifdef / #ifndef / #endif 围代码块。#ifdef MAGIC_NUMBER 表示定义了才编入,否则走 #else;#ifndef 反之。下面这段可贴进 MT5 验证: #define MAGIC_NUMBER 123456 #ifdef MAGIC_NUMBER Print("MAGIC_NUMBER defined: ", MAGIC_NUMBER); #else Print("MAGIC_NUMBER not defined"); #endif #ifndef MAGIC_NUMBER Print("No MAGIC_NUMBER"); #else Print("Has MAGIC_NUMBER: ", MAGIC_NUMBER); #endif 事件处理函数里,OnInit 只在加载图表时跑一次,负责手数、止盈止损等全局初始化;OnStart 是脚本入口,激活后只执行一遍就停,不盯盘;OnTick 在每个分时报价触发,是 EA 核心逻辑宿主。下面 OnTick 片段监控收盘价变动: double lastClose; int OnInit(){ lastClose = iClose(_Symbol, PERIOD_CURRENT, 0); return(INIT_SUCCEEDED); } void OnTick(){ double currentClose = iClose(_Symbol, PERIOD_CURRENT, 0); if(currentClose != lastClose){ Print("Close changed: ", currentClose); lastClose = currentClose; } } 外汇与贵金属杠杆高、滑点跳空频繁,用 OnTick 跑实盘前务必在策略测试器用历史数据回测,条件编译宏也能帮你切换回测/实盘参数分支。

MQL5 / C++
class="macro">#define MAGIC_NUMBER class="num">123456
class="macro">#ifdef MAGIC_NUMBER
   Print("MAGIC_NUMBER defined: ", MAGIC_NUMBER);
class="macro">#else
   Print("MAGIC_NUMBER not defined");
class="macro">#endif

class="macro">#ifndef MAGIC_NUMBER
   Print("No MAGIC_NUMBER");
class="macro">#else
   Print("Has MAGIC_NUMBER: ", MAGIC_NUMBER);
class="macro">#endif

class="type">class="kw">double lastClose;
class="type">int OnInit(){ lastClose = iClose(_Symbol, PERIOD_CURRENT, class="num">0); class="kw">return(INIT_SUCCEEDED); }
class="type">void OnTick(){ class="type">class="kw">double currentClose = iClose(_Symbol, PERIOD_CURRENT, class="num">0); if(currentClose != lastClose){ Print("Close changed: ", currentClose); lastClose = currentClose; } }

◍ MQL5 数组的三种落地写法

想把历史收盘价塞进内存慢慢算,第一步是搞懂 MQL5 里数组怎么声明和填值。最直白的写法是先定大小再逐个下标赋值,比如建一个长度为 5 的整型数组,把 10、20、30、40、50 分别写进索引 0 到 4。 也可以在声明时直接用花括号初始化,int closePrices[5] = {10, 20, 30, 40, 50}; 一行搞定,少写五行赋值代码,MT5 里编译结果完全一致。 如果后面要打印校验,用 Print("Element at index 0: ", closePrices[0]) 这种逐元素输出,能在策略测试器日志里看到 Output: 10 到 50 的明确回显,比只看编译器不报错更踏实。 外汇与贵金属杠杆高,用数组缓存价格做自动化判断时,务必先在 MT5 策略测试器跑通逻辑,再上实盘,避免数组越界导致 EA 异常终止。

MQL5 / C++
class="type">void OnStart()
  {
   class="type">int closePrices[class="num">5]; class=class="str">"cmt">// An integer array with a size of class="num">5, means the array has class="num">5 integer elements
   class="type">class="kw">double prices[class="num">10];  class=class="str">"cmt">// A class="type">class="kw">double-precision floating-point array with a size of class="num">10
  }
class="type">void OnStart()
  {
   class="type">int closePrices[class="num">5];
   closePrices[class="num">0] = class="num">10;
   closePrices[class="num">1] = class="num">20;
   closePrices[class="num">2] = class="num">30;
   closePrices[class="num">3] = class="num">40;
   closePrices[class="num">4] = class="num">50;
  }
class="type">void OnStart()
  {
   class="type">int closePrices[class="num">5] = {class="num">10, class="num">20, class="num">30, class="num">40, class="num">50};
  }
class="type">void OnStart()
  {
  class=class="str">"cmt">// Declare an integer array with a size of class="num">5
   class="type">int closePrices[class="num">5];
class=class="str">"cmt">// Assign values to array elements
   closePrices[class="num">0] = class="num">10;
   closePrices[class="num">1] = class="num">20;
   closePrices[class="num">2] = class="num">30;
   closePrices[class="num">3] = class="num">40;
   closePrices[class="num">4] = class="num">50;

   class=class="str">"cmt">// Access and print array elements
   Print("Element at index class="num">0: ", closePrices[class="num">0]); class=class="str">"cmt">// Output: class="num">10
   Print("Element at index class="num">1: ", closePrices[class="num">1]); class=class="str">"cmt">// Output: class="num">20
   Print("Element at index class="num">2: ", closePrices[class="num">2]); class=class="str">"cmt">// Output: class="num">30
   Print("Element at index class="num">3: ", closePrices[class="num">3]); class=class="str">"cmt">// Output: class="num">40
   Print("Element at index class="num">4: ", closePrices[class="num">4]); class=class="str">"cmt">// Output: class="num">50
  }
class="type">int closePrices[class="num">5];
   closePrices[class="num">0] = class="num">10;
   closePrices[class="num">1] = class="num">20;
   closePrices[class="num">2] = class="num">30;
   closePrices[class="num">3] = class="num">40;

「数组赋值与函数、宏的落地写法」

先确认数组下标从 0 起算:把 50 写进 closePrices[4] 后,依次打印索引 0~4,终端会输出 10、20、30、40、50。这个细节在写收盘价缓存时容易写错边界,建议开 MT5 新建脚本直接跑一遍验证。 自定义函数要明确返回类型和入参类型。下面这段 MyFunction 接收 int 与 double,内部算乘积后 return:传入 (5, 3.5) 时,Print 输出 17.5。外汇与贵金属行情跳空频繁,这类纯数值函数在算仓位手数系数时可直接复用,但杠杆风险仍偏高。 预处理宏适合固化常量与短计算。#define MAGIC_NUMBER 10 后,MAGIC_NUMBER + 5 得 15;用 #define MULTIPLY(x, y) (x * y) 则 MULTIPLY(3,5) 得 15。宏不做类型检查,写 EA 订单识别码时图省事可以,但复杂逻辑别硬塞进宏。

MQL5 / C++
  closePrices[class="num">4] = class="num">50;
  Print("Element at index class="num">0: ", closePrices[class="num">0]); class=class="str">"cmt">// Output: class="num">10
  Print("Element at index class="num">1: ", closePrices[class="num">1]); class=class="str">"cmt">// Output: class="num">20
  Print("Element at index class="num">2: ", closePrices[class="num">2]); class=class="str">"cmt">// Output: class="num">30
  Print("Element at index class="num">3: ", closePrices[class="num">3]); class=class="str">"cmt">// Output: class="num">40
  Print("Element at index class="num">4: ", closePrices[class="num">4]); class=class="str">"cmt">// Output: class="num">50
return_type FunctionName(parameter1_type parameter1, parameter2_type parameter2, ...)
  {
    class=class="str">"cmt">// Function body(code block)
    class=class="str">"cmt">// Perform actions or calculations
    class=class="str">"cmt">// Optionally, class="kw">return a value class="kw">using the &class="macro">#x27;class="kw">return&class="macro">#x27; keyword if the function has a class="kw">return type
  }
class="type">class="kw">double MyFunction(class="type">int param1, class="type">class="kw">double param2)
    {
      class=class="str">"cmt">// Function body: Calculate the product of the two parameters
      class="type">class="kw">double result = param1 * param2;
      class=class="str">"cmt">// Return the result
      class="kw">return result;
    }
class="type">void OnStart()
  {
class=class="str">"cmt">// Calling the custom function
  class="type">class="kw">double result = MyFunction(class="num">5, class="num">3.5);
class=class="str">"cmt">// Printing the result
  Print("The result is: ", result); class=class="str">"cmt">// The output will be class="num">17.5
  }
class="macro">#define MAGIC_NUMBER class="num">10
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
   class="type">int result = MAGIC_NUMBER + class="num">5;
   Comment(result); class=class="str">"cmt">// Output will be class="num">15
  }
class="macro">#define MAGIC_NUMBER class="num">10
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
   class="type">int result = MAGIC_NUMBER + class="num">5;
  }
class="macro">#define MULTIPLY(x, y) (x * y)
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
   class="type">int result = MULTIPLY(class="num">3, class="num">5); class=class="str">"cmt">// Result is class="num">15
  }
class="macro">#define MAGIC_NUMBER class="num">10
class=class="str">"cmt">//+------------------------------------------------------------------+

宏定义与文件包含的实际坑点

MQL5 的预处理器指令在编译期就生效,和运行时逻辑完全两码事。下面这段把 MAGIC_NUMBER 定义成 10,又在 OnStart 里先加 5 再 #undef 掉,编译器处理到 #undef 那行之后,这个符号就不存在了,后续若再引用可能直接报未定义错误。 #define MAGIC_NUMBER 10 void OnStart() { int result = MAGIC_NUMBER + 5; #undef MAGIC_NUMBER // Undefining MAGIC_NUMBER } 把常用函数丢进外部 .mqh 文件再用 #include 引入,是拆分 EA 逻辑的常见做法。例如 extraRecipes.mqh 里写了个 MultiplyByTwo,主文件包含后直接调,传入 5 会返回 10,Print 输出"Result: 10",这个数值在 MT5 里跑一遍就能确认。 // extraRecipes.mqh int MultiplyByTwo(int number) { return number * 2; } // Your main code #include "extraRecipes.mqh" void OnStart() { int result = MultiplyByTwo(5); Print("Result: ", result); // Output: Result: 10 } #ifdef 和 #ifndef 用来判断宏是否定义过。如果前面 #define 了 MAGIC_NUMBER 10,#ifdef 分支会打印 10;若用 #undef 清掉后再走 #ifndef,else 分支依旧打印已定义时的 10——注意 #ifndef 的 if 块里写 Print(MAGIC_NUMBER) 在宏不存在时本身就会编译失败,别照抄这种矛盾写法。 全局变量如 LotSize、TakeProfit、StopLoss 声明在函数外,生命周期覆盖整个 EA,但初始值不会自动设好,实盘前得在 OnInit 里显式赋值,外汇和贵金属波动大,手数或止损留空可能引发默认 0.0 导致的拒单风险。

MQL5 / C++
class="macro">#define MAGIC_NUMBER class="num">10
class="type">void OnStart()
  {
   class="type">int result = MAGIC_NUMBER + class="num">5;
class="macro">#undef MAGIC_NUMBER class=class="str">"cmt">// Undefining MAGIC_NUMBER
  }
class="macro">#include "extraRecipes.mqh" class=class="str">"cmt">// Include external file with extra recipes
class=class="str">"cmt">// extraRecipes.mqh
class="type">int MultiplyByTwo(class="type">int number) class=class="str">"cmt">// function from an external file
{
   class="kw">return number * class="num">2;
}
class=class="str">"cmt">// Your main code
class="macro">#include "extraRecipes.mqh" class=class="str">"cmt">// Include external file with extra recipes
class="type">void OnStart()
  {
   class="type">int result = MultiplyByTwo(class="num">5);  class=class="str">"cmt">// Using the function from the included file
   Print("Result: ", result);      class=class="str">"cmt">// Output: Result: class="num">10
  }
class="macro">#define MAGIC_NUMBER class="num">10
class="type">void OnStart()
  {
class="macro">#ifdef MAGIC_NUMBER
   Print(MAGIC_NUMBER);
class="macro">#else
   Print(MAGIC_NUMBER);
class="macro">#endif
  }
class="type">void OnStart()
  {
class="macro">#ifndef MAGIC_NUMBER
   Print(MAGIC_NUMBER);
class="macro">#else
   Print(MAGIC_NUMBER);
class="macro">#endif
  }
class="macro">#define MAGIC_NUMBER class="num">10
class="type">void OnStart()
  {
class="macro">#ifndef MAGIC_NUMBER
   Print(MAGIC_NUMBER);
class="macro">#else
   Print(MAGIC_NUMBER);
class="macro">#endif
  }
class=class="str">"cmt">// Declare global variables
class="type">class="kw">double LotSize;
class="type">int TakeProfit;
class="type">int StopLoss;

◍ 把工具请下神坛

这段 MT5 代码把 EA 初始化、脚本触发和 tick 级收盘价监控揉在一起,核心逻辑是拿 iClose(_Symbol, PERIOD_CURRENT, 0) 抓当前收盘价,再用一个全局 double lastClose 做差分,只在收盘价变动时 Print 出提示。 OnInit 里既能写死 LotSize=0.1、TakeProfit=50、StopLoss=30 这类参数并打到日志,也能把 lastClose 初始化成实时收盘价;OnStart 只是脚本入口的 hello world,真正盯盘价值在 OnTick 的变动捕获。 外汇和贵金属杠杆高、滑点猛,这种裸 Print 方案只适合你本地验证事件驱动骨架,直接挂真仓可能因报价跳空漏掉中间收盘价。开 MT5 新建 EA 把下面代码粘进文件,跑 EURUSD 的 M5 就能看到收盘价跳变被逐次打印,先确认你的终端事件节奏再谈加仓逻辑。

MQL5 / C++
class=class="str">"cmt">// OnInit function
class="type">int OnInit()
  {
class=class="str">"cmt">// Set EA parameters
   LotSize = class="num">0.1;
   TakeProfit = class="num">50;
   StopLoss = class="num">30;
class=class="str">"cmt">// Display initialization message
   Print("EA is getting ready. Lot Size: ", LotSize, ", Take Profit: ", TakeProfit, ", Stop Loss: ", StopLoss);
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Script program start function                                      |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// This script prints a message to the console when activated
class=class="str">"cmt">// Entry point when the script is started
class="type">void OnStart()
  {
class=class="str">"cmt">// Print a message to the console
   Print("Script Activated! Hello, Traders!");
  }
class=class="str">"cmt">// Declare a variable to store the last tick&class="macro">#x27;s close price
class="type">class="kw">double lastClose;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">// OnInit function
class="type">int OnInit()
  {
class=class="str">"cmt">// Initialize the variable with the current close price
   lastClose = iClose(_Symbol, PERIOD_CURRENT, class="num">0);
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
  {
class=class="str">"cmt">// Get the current close price
   class="type">class="kw">double currentClose = iClose(_Symbol, PERIOD_CURRENT, class="num">0);
class=class="str">"cmt">// Check if the close price has changed
   if(currentClose != lastClose)
     {
      class=class="str">"cmt">// Print a message when the close price changes
      Print("Close price changed! New close price: ", currentClose);
      class=class="str">"cmt">// Update the last close price
      lastClose = currentClose;
     }
  }
class=class="str">"cmt">// Declare a variable to store the last tick&class="macro">#x27;s close price
class="type">class="kw">double lastClose;
nt OnInit()
  {
class=class="str">"cmt">// Initialize the variable with the current close price
   lastClose = iClose(_Symbol, PERIOD_CURRENT, class="num">0);
   class="kw">return(INIT_SUCCEEDED);
  }
class="type">void OnTick()
  {
class=class="str">"cmt">// Get the current close price 
   class="type">class="kw">double currentClose = iClose(_Symbol, PERIOD_CURRENT, class="num">0);
class=class="str">"cmt">// Check if the close price has changed
   if(currentClose != lastClose)
     {
      class=class="str">"cmt">// Print a message when the close price changes
      Print("Close price changed! New close price: ", currentClose);
      class=class="str">"cmt">// Update the last close price
      lastClose = currentClose;
     }
  }

常见问题

看品种波动频率:低频策略用固定数组够用且快;高频或不确定长度用动态数组配合 ArrayResize,避免越界报错。
多半是头文件没加防止重复包含的守卫宏,或同一宏在多个文件被定义;用 #ifndef 包裹内容即可解决。
可以。小布能扫描你的策略代码结构,标出数组越界风险和重复宏定义,并给出改写建议,省去手动排查。
OnTick 只放轻量实时逻辑,周期任务如重算指标放 OnTimer,用 EventSetTimer 设间隔,避免主循环卡顿。
宏无类型检查、难调试,展开后代码膨胀;只在极简常量替换时用,复杂计算交给函数更安全。