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 便不能再引用,适合在多文件工程里避免命名污染。数组、函数、预处理器组合起来,才接得住后续事件驱动的复杂逻辑。
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 跑实盘前务必在策略测试器用历史数据回测,条件编译宏也能帮你切换回测/实盘参数分支。
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 异常终止。
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 订单识别码时图省事可以,但复杂逻辑别硬塞进宏。
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 导致的拒单风险。
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 就能看到收盘价跳变被逐次打印,先确认你的终端事件节奏再谈加仓逻辑。
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; } }