MQL4编程入门:从变量、数组到自定义指标的核心避坑指南

发布时间:2026/10/23 20:50:44
MQL4编程入门:从变量、数组到自定义指标的核心避坑指南

简介:《MT4的MQL4编程入门》是一份面向外汇交易初学者和编程零基础用户的MQL4语言入门教程,基于韬客外汇论坛经典入门系列整理而成。教程系统讲解从自定义变量到函数的完整语法:包括int、double、string、bool四种基本类型,extern附加变量用于设置可调参数,自定义函数支持有返回值类型与void无返回值类型,数组则用于批量处理同类数据;同时结合MT4平台演示如何编写交易策略、自定义指标和EA自动化程序,覆盖市场数据取值等后续进阶内容。资源为DOC文档,共1个文件,压缩包约197KB,内容精炼、结构清晰,适合随时翻阅。目前已有389人浏览学习。通过该教程,零基础读者可快速掌握MQL4的核心编程逻辑,理解变量作用域、函数调用与数组引用规则,并逐步构建个人交易策略的自动执行脚本,提升交易效率与自动化水平。

1. MT4 MQL4 编程入门:先看懂语言,再谈策略落地

很多人拿到 MT4 之后的第一反应是找现成的 EA 和指标源码,复制进去编译,然后面对一堆红色报错只能干瞪眼。这套《MT4 的 MQL4 编程入门》教程不教你预测行情,也不承诺一键生成策略,它做的事情很朴素:把 MQL4 这门语言的骨架拆开——变量与类型、extern 参数、函数与数组、市场数据取值、指标结构与循环判断,每一节都带着能直接编译的小例子。它适合刚入门的交易者,也适合会一点 C 但没碰过 MQL4 的程序员。看完你能读懂别人指标源码的八成逻辑,能改参数、能画第一条指标线,也知道出了问题该去哪里查,而不是把 MT4 当黑匣子瞎猜。

2. 变量、extern 与函数:先立起 MQL4 的语法骨架

2.1 四种基础类型与“先定义后使用”规则

MQL4 不是脚本语言,它和 C 很像:所有要用的数据都必须在使用前声明类型和名称。教程里那句“自定义变量在使用前必须创建”,翻译过来就是编译器要提前知道这个变量占多大空间、用什么方式解释它。拿int mark;举例,int 是类型定义词,mark 是变量名,这条语句告诉编译器“给我一个用来存整数的盒子,名字叫 mark”。

四种基本类型我列了一张表,方便对照:

类型含义典型用途
int整型,包括负整数循环计数、指标周期参数
double双精度小数价格、指标计算值
string字符串指标名、邮件内容、注释
bool逻辑型 true/false开关参数、报警状态标记

声明时可以直接赋初值,比如bool 是否报警 = true;、string 说明 = "测试";。这里有一个 MQL4 的特别之处:变量名可以用中文,这在老牌编程语言里不多见。不过中文变量名参与字符串拼接时要注意编码,教程示例本身没毛病,但如果源码从网页直接复制粘贴,偶尔会出现中文名乱码导致编译失败。我一般遇到这种现象,先把源码另存为 UTF-8 编码再重新编译,大多数情况下就过去了。

类型定义词只在创建变量时用一次。创建之后给变量赋值,直接写变量名:mark = 5;。新手最常见的编译错误就是赋值时顺手又写了一遍int mark = 5;,这在 MQL4 里属于重复定义,直接报错。记住这个原则:int、double 这类关键字只在变量第一次出现的时候写,后面再出现就是错的。

2.2 extern 附加变量:把参数交到用户手里的入口

extern 是 MQL4 里最值得先记住的关键字。一个变量声明前面加上 extern,它就会出现在 MT4 加载指标或 EA 时的“输入参数”面板里,用户不用打开源码就能改数值。MACD(12,26,9) 这种括号参数,本质就是三个 extern 变量的默认值。

extern int 周期 = 9; extern double 止损点数 = 30.0; extern bool 是否开启报警 = true;

第一个 extern 声明了整型变量 周期,默认 9,用户在输入参数面板里把它改成 20,指标内部所有用 周期 的地方就自动换成 20。第二个是双精度型,适合放点数、金额这类带小数参数。第三个是逻辑型,适合做开关。这里要提一个默认值的坑:extern 变量的默认值直接影响用户第一次加载时的行为,很多人策略翻车不是逻辑写错,而是默认参数本身不合理。我一般会把默认值设置成能在当前周期画出正常图形的值,而不是随手写一个整数。

extern 还有两个使用边界:它只能修饰全局变量,不能放在 start() 函数或自定义函数内部,否则编译不通过;命名不要用 MQL4 保留字,比如 int、double、start 这些都不能作为变量名。另外,extern 变量在程序运行中改动后不会立即生效,需要重新加载指标或 EA 才会按新参数计算,调试时要记得这一步。

2.3 自定义函数:参数传递、返回值和局部变量的生命周期

函数的意义是把一段反复用到的运算打包。教程里那个 ft(x,y)=3x+2y 的例子非常典型,写成 MQL4 就是:

int ft(int x, int y) { int temp; temp = 3 * x + 2 * y; return(temp); } void 显示提示(string 文本) { Comment(文本); }

逻辑说明:函数头的 int 是函数类型,表示这个函数返回整数;函数名后面的括号里是参数表,x 和 y 在使用前也必须声明类型,所以写成 int x, int y;大括号里是函数体,temp 是函数内部定义的局部变量;return(temp) 把 temp 的值带回调用处。调用时写 ft(4,5),程序按顺序把 4 传给 x、5 传给 y,函数计算返回 23,调用点拿到这个值继续往下执行。

void 类型是 MQL4 里第二常见的函数类型:它没有 return 值,函数体执行完就结束。像 显示提示 这种只负责在图表左上角输出文本的函数,用 void 最合适。判断什么时候用 int、double 还是 void,核心看调用方是否需要这个函数算出一个结果来参与后续计算。

局部变量的生命周期值得多说一句:ft 函数里的 x、y、temp 都在函数被调用时才创建,函数执行结束立刻注销。这就带来一个实际问题——如果你需要一个变量记录“上一次报警发生在什么时候”,放在函数内部每次调用都会重置。解决方法是把这种状态变量放到函数外面定义成全局变量,或者在声明时加 static 关键字,让它在多次调用之间保留值。这算是 MQL4 新手从函数语法过渡到实际指标编程的一道小坎。

2.4 数组与下标:一次只能操作一个元素

数组在 MQL4 里是一组同类型变量的集合。int aa[4] = {4,3,2,1};声明了一个长度 4 的整型数组,方括号里的数字是下标。数组下标从 0 开始,也就是说 aa[0] 的值是 4,aa[1] 是 3,aa[2] 是 2,aa[3] 是 1。这个“第 0 个元素”的规则和日常计数习惯冲突,新手第一次遍历数组时经常越界,下标写成 4 去访问 aa[4] 就直接报 array out of range。

int aa[4] = {4,3,2,1}; // 长度为 4,aa[0]=4 ... aa[3]=1 double buf[]; // 不定长数组,后面绑定指标缓冲 int cc[8]; // 定长数组,8 个元素,未赋初值

数组最容易被忽略的一点:定义之后,一次只能通过下标访问一个元素,不能像某些语言那样整体赋值或整体比较。要处理整列数据就写循环逐个操作。MQL4 里的动态数组(不定长数组)有一个常见陷阱:int bb[];这种声明在普通场景下直接写 bb[0]=1 会在运行时越界报错,因为编译器没有给这个数组分配实际容量。我常用的做法是先用ArrayResize(bb, 数量)明确容量,再按下标写入。指标缓冲数组是例外,它由 SetIndexBuffer 在初始化函数里绑定,容量由 MT4 内部管理,这个后面会讲到。

3. 市场数据与指标取值:搞懂 Open[] 和 iMA 的参数习惯

3.1 预定义数组:记住 0 是最新一根 K 线

MT4 把每根 K 线的开、高、低、收、成交量、时间分别做成了可以直接使用的数组:Open[]、High[]、Low[]、Close[]、Volume[]、Time[]。这些数组的下标代表 K 线位置,方向跟多数人的直觉相反:0 是当前最新一根,1 是它左边那一根,2 再往左,i 表示第 i+1 根。我之前带人写第一个指标时,十个人里有八个把方向写反,最后画出来的线整体平移了一根 K 线。

double 最新收盘 = Close[0]; double 前一根最高 = High[1]; double 倒数第三根开盘 = Open[2]; int 柱总数 = Bars;

Close[0] 是最新收盘价,High[1] 是前一根最高价,Bars 是当前图表的总柱数。这些预定义数组只属于当前图表——也就是说,它们在哪个货币对、哪个周期加载,取的就是那个图表的数据。想要读别的周期就得用带 i 前缀的函数,见 3.2。除了这些数组,还有几个预定义变量要顺手记住:Ask 和 Bid 是当前实时卖价和买价,double 型;Digits 是当前品种的小数位数,无日元币对一般是 4,有日元的是 2;Point 是点值,和 Digits 对应,写止损止盈计算时离不开它们。

有一个使用上的陷阱:当软件刚连接行情、历史数据还没有完全加载时,Close[0] 这类值可能是 0 或者无效数据。如果在指标初始化阶段就读取这些数组做判断,偶尔会得到错误结论。我的习惯是在 start() 里先检查 Bars,等柱数足够后再开始计算,避免把无效值写进指标线。

3.2 跨周期取数:iOpen、iClose 系列与周期常量

预定义数组只能看当前图表数据,但实际策略经常需要“大周期定方向、小周期找进场”。这时候要用 iOpen(symbol, timeframe, shift) 这一系列函数。它们的参数结构完全一致:第一个参数是品种字符串,第二个是 K 线周期常量,第三个是位置序号。周期常量对应关系如下:

常量周期
PERIOD_M11 分钟
PERIOD_M55 分钟
PERIOD_M1515 分钟
PERIOD_M3030 分钟
PERIOD_H11 小时
PERIOD_H44 小时
PERIOD_D1日线
PERIOD_W1周线
PERIOD_MN1月线
double 指定品种小时最新开盘 = iOpen("USDJPY", PERIOD_H1, 0); double 当前周期最新收盘 = iClose(NULL, 0, 0); double 当前图表第3根收盘 = iClose(NULL, 0, 2);

第一行表示某货币对 1 小时图最新一根开盘价。第二行是“不指定品种也不指定周期”,NULL 表示跟随当前图表品种,0 表示当前图表周期,常用于写通用指标。第三行的 2 是位置序号,注意这里 2 表示从右往左第 3 根。位置序号和预定义数组的下标是同一个坐标系,这样设计的好处是:一个指标里混用 Close[0] 和 iClose(NULL,0,0),结果完全一致,不会出现偏移。

用 NULL 有个副作用要心里有数:它跟随当前图表,图表切到别的品种或周期,取值跟着变。写通用指标是优点,写固定品种策略时反而要小心,最好显式指定品种字符串。iHigh、iLow、iVolume、iTime 系列与 iOpen 完全同构,参数含义一致,区别只在 iTime 返回的是 datetime 类型,其他返回 double。

3.3 iMA 移动平均线:七个参数,错一个就不是你想要的线

均线是 MQL4 里调用频率最高的指标函数,iMA 一共有七个参数,也是新手最容易记混的一组。完整形式是:

double maValue = iMA(symbol, timeframe, period, ma_shift, ma_method, applied_price, shift);

参数依次是品种、周期、均线周期、均线偏移、平均模式、价格种类、位置。举例:

double ma20 = iMA(NULL, 0, 20, 0, MODE_SMA, PRICE_CLOSE, 0); double ema20_3根前 = iMA(NULL, 0, 20, 0, MODE_EMA, PRICE_CLOSE, 2);

第一行取当前图表 20 周期简单移动平均线在最新位置的值。第二行取 20 周期指数移动平均线在第 3 根 K 线位置的值。第五个参数平均模式有四个常量:MODE_SMA 简单移动平均、MODE_EMA 指数移动平均、MODE_SMMA 平滑移动平均、MODE_LWMA 线性加权。第六个参数价格种类最常用 PRICE_CLOSE 收盘价,此外还有 PRICE_OPEN、PRICE_HIGH、PRICE_LOW、PRICE_MEDIAN 等。第七个 shift 是 K 线位置,0 是当前,坐标系和 3.1 一致。

注意:第五、第六个参数位置经常被写反。MODE_EMA 和 PRICE_CLOSE 一个是模式、一个是价格,写反了编译不报错,但计算出来的线完全不是预期中的均线,回测时才发现数值对不上。这种错误在代码里非常隐蔽。

ma_shift 参数大多数时候填 0,它表示均线整体左右平移的柱数。需要提醒的是,如果你在循环里从 i=0 一直取值到 1000,每次 iMA 调用都会重新计算一次均线,性能开销比较大。对于已经算好的数组,可以使用 iMAOnArray 来自定义数据源的移动平均值,它参数更短,适合做二次计算。

3.4 iCustom 读取自定义指标:参数的坑在看不见的地方

当指标是别人写的、没有内置取数函数时,用 iCustom 可以拿到自定义指标的指标线值。标准形式:

double val = iCustom(NULL, 0, "mymacd", 12, 26, 9, 2, 0);

这段的意思是:在当前图表当前周期,加载自定义指标 mymacd,把 12、26、9 三个参数传给它,取它的第 3 条指标线(编号从 0 开始,所以 2 是第 3 条)在最新位置的值。为什么说坑在看不见的地方——iCustom 的参数个数必须和自定义指标源码里的 extern 声明完全一致,多一个少一个通常在编译阶段不报错,运行日志里才有提示;参数顺序也必须和源码声明顺序一致,否则取到的是另一个参数组合算出的结果。

我一般拿到陌生指标,第一步先打开它的源码,数清楚有几个 extern 变量,按顺序把值填进 iCustom。如果指标有多根指标线,编号规则是第一条为 0、第二条为 1,最后一个 shift 参数仍然是 K 线位置。另外 iCustom 每次调用都会让 MT4 去重新计算那个自定义指标,在 for 循环里频繁调用性能很差,合理的做法是把 iCustom 的结果先存进数组再使用,而不是每根 K 线都调好几遍。

4. 自定义指标避坑:四段结构、画线流程与四类报错

4.1 先理解指标的四段结构与 #property 指令

MT4 自定义指标文件由四部分组成:文件头部的 #property 预处理指令、全局变量定义区、init() 初始化函数、start() 主函数,另外还有一个可选的 deinit() 在指标卸载时执行。#property 指令不写分号,它负责告诉编译器“这个指标要在哪个窗口显示、有几条线、什么颜色”。新手第一道坎就在这里,常见组合如下:

#property indicator_chart_window // 显示在主图,如均线类 #property indicator_separate_window // 显示在副图,如 MACD、RSI 类 #property indicator_buffers 2 // 声明 2 条指标缓冲线 #property indicator_color1 Red // 第 1 条线红色 #property indicator_width1 1 // 第 1 条线粗细为 1 #property indicator_level1 0.0 // 在 0 值处画一条水平虚线

第一个和第二个是一组互斥指令,一个指标只能选其一。indicator_buffers 的数量必须不小于你实际绑定的指标线数量,少写会导致高编号的线丢失。indicator_color1、indicator_color2 按编号对应第 1、2 条线;indicator_level1 常用于 MACD 这类在 0 轴上下摆动的指标,固定画一条零值参考线。这些指令全部放在文件最顶部,位置靠后会影响编译阶段的缓冲区分配,所以务必保持在第一条语句之前。

4.2 init() 里绑定缓冲区,start() 里逐根画线:MACD 白线示例

一个指标至少要有三件事:一个存储数值的数组、一次缓冲区绑定、一段给数组赋值的循环。教程里那个只画 MACD 白线的示例很干净,我把它完整贴出来逐段拆:

#property indicator_separate_window #property indicator_color1 White #property indicator_level1 0 extern int FMA = 12; extern int SMA = 26; double buf[]; int init() { SetIndexBuffer(0, buf); SetIndexStyle(0, DRAW_LINE); return(0); } int start() { int limit = Bars - IndicatorCounted(); for(int i = 0; i < limit; i++) { buf[i] = iMA(NULL, 0, FMA, 0, MODE_EMA, PRICE_CLOSE, i) - iMA(NULL, 0, SMA, 0, MODE_EMA, PRICE_CLOSE, i); } return(0); }

逻辑说明:文件头部声明这是一条副图白线,并在 0 位置画水平参考线。FMA 和 SMA 两个 extern 变量是快慢均线周期,默认 12 和 26。double buf[] 是数据缓冲区,它本身没有长度,真正的容量在 SetIndexBuffer 完成时由 MT4 内部分配。init() 在指标加载时只运行一次,SetIndexBuffer(0, buf) 把 buf 绑定为第 1 条指标线,SetIndexStyle(0, DRAW_LINE) 把它画成连续曲线。start() 在有新数据变动时被触发,关键在 limit = Bars - IndicatorCounted():Bars 是当前图表总柱数,IndicatorCounted() 返回已经计算过的柱数,两者相减就是“还没算过的柱数”。循环从 i=0 跑到 limit,逐根取两条 iMA 在相同位置的差值写入 buf[i],已经算过的部分不再重复计算。这就是 MQL4 的增量计算机制,指标加载在几千根 K 线的图表上也不会卡。

循环部分如果习惯用 while 也是一回事:

int i = 0; while(i < limit) { buf[i] = iMA(NULL, 0, FMA, 0, MODE_EMA, PRICE_CLOSE, i) - iMA(NULL, 0, SMA, 0, MODE_EMA, PRICE_CLOSE, i); i++; }

两者执行逻辑完全一样:先判断条件,成立就执行循环体,再回到判断。for 只是把初始化变量、判断条件、递增语句压缩到一行。

4.3 坑一:报警条件永远不触发或每次都触发

写价格穿越报警时,条件判断看起来简单,实际最容易翻车。看这段典型的上下穿 1.0000 报警逻辑:

int mark = 0; if(High[1] < 1.0000 && High[0] >= 1.0000 && mark != 1) { Alert(Symbol(), "价格向上触及 1.0000"); mark = 1; } if(Low[1] > 1.0000 && Low[0] <= 1.0000 && mark != 2) { Alert(Symbol(), "价格向下触及 1.0000"); mark = 2; }

现象:报警条件写好后,要么一根 K 线触发十几次,要么一次都不触发。原因:第一,没有用 mark 标记去重,上穿条件在同一根 K 线持续成立,start() 每来一个报价都会 Alert 一次;第二,判断上穿用的是当前 K 线和前一根 K 线的最高价,如果当前 K 线开盘价已经在 1.0000 上方,High[0] 从一开始就大于等于 1.0000,条件被误判成“刚刚上穿”。解决:用 mark 记录上次报警类型,0 表示从未报警,1 表示已上穿报警,2 表示已下穿报警。上穿后 mark 置 1,只有 mark 不等于 1 时才允许再次触发;同时把穿越判断写成 High[1] < 1.0000 && High[0] >= 1.0000,用前一根 K 线的价格状态和当前 K 线做比较,保证这是真正发生过的一次穿越,而不是状态一直满足。这是我个人的调试经验:先把 Alert 换成 Print 输出 mark 和两个 High 值,看日志确认条件每一步都符合预期,再加回 Alert。

4.4 坑二:数组越界导致指标线中断

现象:指标编译通过,加载后前半段正常,后半段空白,日志窗口反复出现 array out of range。原因:start() 里循环上限写成了 Bars,而不是 Bars - IndicatorCounted()。当 MT4 给指标缓冲数组分配的容量小于你要访问的最大下标时,就会越界。动态数组没分配容量直接写入也是同样的报错。解决:循环上限一律用 Bars - IndicatorCounted(),并且在计算前加一个空值保护:

int limit = Bars - IndicatorCounted(); if(limit <= 0) return(0); for(int i = 0; i < limit; i++) { // 计算逻辑 }

如果你的数据不是指标缓冲区,而是普通动态数组,记得先用 ArrayResize 分配容量:ArrayResize(tmp, Bars);。注意 ArrayResize 的第二个参数是元素个数,不是字节数,这里写成 Bars 就是和当前图表柱数一致。

4.5 坑三:iCustom 参数对不上,取到错误数值却不报错

现象:用 iCustom 引用别人写好的指标,图形加载正常,但取到的指标值跟原指标显示的值对不上。原因:参数顺序和源码 extern 声明顺序不一致,或者指标线编号选错。自定义指标的 extern 参数顺序是硬规则,iCustom 按位置传参,跟参数名无关。解决:打开自定义指标源码,把 extern 声明顺序抄下来,依次填进 iCustom。如果原指标有 3 个 extern 参数,形如 extern int A=12; extern int B=26; extern int C=9;,那调用就是 iCustom(NULL,0,"指标名",12,26,9,线编号,位置)。线编号从 0 开始,第一根线是 0,第二根是 1。这个错误最迷惑人的地方在于:参数个数一致时编译器不报任何警告,只有把取到的值和图上叠加的原指标对比才能发现。

提示:调用 iCustom 前先打开自定义指标源码,数清 extern 个数并按顺序填参数,这是减少排查时间最有效的一步。

5. 把行情提醒和验证变成习惯:SendMail 与数据窗口抽查

5.1 用 SendMail 把行情推到手机

一个很实用的落地小技巧:利用 MT4 的邮件发送函数,每间隔固定分钟数就把最新价格发到你的邮箱,配合邮箱服务商的短信提醒功能,手机就变成了行情接收机。完整指标如下:

#property indicator_chart_window extern int 间隔分钟 = 10; datetime NextTime = 0; int start() { if(TimeLocal() > NextTime) { SendMail(Symbol() + " " + DoubleToStr(Close[0], Digits), ""); NextTime = TimeLocal() + 间隔分钟 * 60; } return(0); }

逻辑说明:这个指标加载到主图后,每次报价到来都会触发 start()。TimeLocal() 返回本机当前时间,NextTime 是预定的下次发送时间,初始为 0,所以第一次触发立即发送。发送完成后,把 NextTime 更新为当前时间加上间隔分钟乘以 60 秒。datetime 类型在 MQL4 里本质是自 1970 年起的秒数,所以分钟要乘 60。SendMail 的第一个参数是邮件主题,第二个参数是正文,这里我把品种和最新价格都拼进主题里,正文留空,手机短信一般直接展示主题内容,一眼就能看到行情。把 间隔分钟 改成 30,就是半小时一封。

这里有一个亲身踩过的坑:MT4 的邮件功能需要在工具选项里预先配置好发件邮箱、收件邮箱和 SMTP 服务器,否则 SendMail 什么也不做。我第一次测试时日志里全是错误,一度以为是代码问题,后来发现是 SMTP 连接没有建立,重启 MT4 再测一次才收到测试邮件。所以写完这个指标,第一件事是先在工具选项里发一封测试邮件,确认通道通了再挂到图表上。

5.2 用数据窗口抽查指标线数值

写完指标不要急着上实盘,验证数值比写代码更要紧。MT4 图表顶部工具栏有数据窗口开关,打开后鼠标移到任意一根 K 线上,就能看到该 K 线位置指标线的具体数值。我每次都在最早的几根 K 线、中间某一根、最新一根分别读数,和手工计算或另一周期的同指标做个交叉对比,数值连续没有跳变,才敢说这条线画对了。

从那以后我每次把指标挂到图表上,都强制走一遍这个流程:先看编译警告,再核对 #property 窗口属性,最后用数据窗口抽查三根 K 线的值。这套流程挡住过不少低级错误,也让我后来写 EA 时少走很多弯路。希望帮到你。

本文还有配套的精品资源,点击获取