掌握 MQL5:从入门到精通(第四部分):关于数组、函数和全局终端变量·进阶篇
(2/3)· 从静态到动态数组、函数参数传递与全局终端变量,补齐 EA 内部数据组织的最后拼图
序列索引反向这坑,先搞清再写EA
MT5 里开盘、收盘、最高、最低、 tick 量、真实量、价差、K线开盘时间以及指标值,都归为时间序列。程序员不能直接摸这些内部序列,只能靠一组预定义函数(CopyClose、CopyTime、iBarShift 等)把数据搬进自己的变量里再用。 搬完之后最反直觉的一点:物理内存里序列和普通动态数组一样,从最旧排到最新;但序列函数的编号从右往左,0 号是最右边那根还没收线的当前 K。普通数组不认这套,所以它眼里的「最后一根」刚好是序列的 0 号,方向完全反着。 图 4 和图 5 直观展示了这点——CopyRates 这类函数把数据拷进常规数组后,序列的第一个元素会变成数组的最后一个。EURUSD H4 上实测:未设序列时打印出 04:00 在前、08:00 在后;ArraySetAsSeries 设成 true 后,同样两个值变成 08:00 在前、04:00 在后。 两种写法可消掉这个别扭。若算法总是从最后一根 K 往前取数,直接对目标动态数组调 ArraySetAsSeries(arr, true),让标准和你自己代码用同一套索引。若是从图表任意位置抠小片段、且每步都明确知道数量,就老实接受反向索引,另写个校验函数兜住取值逻辑,别在表达式里裸写容易看走眼。 外汇与贵金属杠杆高、滑点跳空频繁,序列索引错一位可能让策略在错误 K 线上下单,实盘前务必在策略测试器用历史数据跑一遍索引打印。
class="type">class="kw">datetime lastBarTime; class=class="str">"cmt">// We&class="macro">#x27;ll try to write the last candle&class="macro">#x27;s time into this variable class="type">class="kw">datetime lastTimeValues[]; class=class="str">"cmt">// The array will store the time of the last two candles. class=class="str">"cmt">// It&class="macro">#x27;s dynamic so that it can be made into a time series to test indices class=class="str">"cmt">// Get the start time of the current candlestick using the iTime function lastBarTime = iTime( Symbol(), class=class="str">"cmt">// Use the current symbol PERIOD_CURRENT, class=class="str">"cmt">// For the current timeframe class="num">0 class=class="str">"cmt">// Current candlestick ); Print("Start time of the class="num">0 bar is ", lastBarTime); class=class="str">"cmt">// Get the start time of the last two candlesticks using the CopyTime function CopyTime( Symbol(), class=class="str">"cmt">// Use the current symbol PERIOD_CURRENT, class=class="str">"cmt">// For the current timeframe class="num">0, class=class="str">"cmt">// Start with position class="num">0 class="num">2, class=class="str">"cmt">// Take two values lastTimeValues class=class="str">"cmt">// Write them to array lastTimeValues("regular") array ); Print("No series"); ArrayPrint(lastTimeValues,_Digits,"; "); class=class="str">"cmt">// Print the entire array to log. The separator between elements is a semicolon ArraySetAsSeries(lastTimeValues,true); class=class="str">"cmt">// Convert the array into a time series Print("Series"); ArrayPrint(lastTimeValues,_Digits,"; "); class=class="str">"cmt">// Print the entire array again. Note the order of the data class=class="str">"cmt">/* Script output: class="num">2024.08.class="num">01 class="num">09:class="num">43:class="num">27.000 PrintArraySeries(EURUSD,H4) Start time of the class="num">0 bar is class="num">2024.08.class="num">01 class="num">08:class="num">00:class="num">00 class="num">2024.08.class="num">01 class="num">09:class="num">43:class="num">27.051 PrintArraySeries(EURUSD,H4) No series class="num">2024.08.class="num">01 class="num">09:class="num">43:class="num">27.061 PrintArraySeries(EURUSD,H4) class="num">2024.08.class="num">01 class="num">04:class="num">00:class="num">00; class="num">2024.08.class="num">01 class="num">08:class="num">00:class="num">00 class="num">2024.08.class="num">01 class="num">09:class="num">43:class="num">27.061 PrintArraySeries(EURUSD,H4) Series class="num">2024.08.class="num">01 class="num">09:class="num">43:class="num">27.061 PrintArraySeries(EURUSD,H4) class="num">2024.08.class="num">01 class="num">08:class="num">00:class="num">00; class="num">2024.08.class="num">01 class="num">04:class="num">00:class="num">00 */
◍ 函数定义的两个阶段与形参实参
MQL5 里写函数用的是统一模板:先写返回类型,再写函数名,括号里列参数类型和名字,大括号里放执行逻辑和 return。返回类型或参数列表都能用 void 表示“无”,比如无返回值的函数就不必写 return,也不存在结果变量。 一个硬限制是:函数不能嵌套定义,只能写在全部函数之外;同名函数可以重载,只要参数数量或类型、返回类型不同即可,但你必须自己厘清调用时到底命中哪一个。MT5 编译器最多允许一个函数带 63 个形式参数,日常写指标或 EA 基本碰不到上限。 定义函数只是“形式化算法”,不调用就不会发生任何事。定义时写的参数叫形式参数,代表事先未知、任意传入的数据;调用时填进去的具体变量、常量或字面量叫实际参数,类型和数量必须和形式参数对应,否则编译直接报错。下面这段代码里 diff 用两个整数做形参,return (a-b) 返回差值;若你调用时写 diff(1) 或 diff(1,2,3),编译器会立刻拦截。 重载时函数名能复用,比如把 diff 改成接收 double 参数就能算实数差,但调用方得清楚传的是整型还是双精度,不然结果类型可能偏离预期。
ResultType Function_Name(TypeOfParameter1 nameOfParameter1, TypeOfParameter2 nameOfParameter2 …) { class=class="str">"cmt">// Description of the result variable and other local variables ResultType result; class=class="str">"cmt">// … class=class="str">"cmt">//--- class=class="str">"cmt">// Main actions are performed here class=class="str">"cmt">//--- class="kw">return resut; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Example class="num">1 | class=class="str">"cmt">//| Comments are often used to describe what a function does, | class=class="str">"cmt">//| what date it needs and why. For example, like this: | class=class="str">"cmt">//| | class=class="str">"cmt">//| | class=class="str">"cmt">//| The function returns difference between two integers. | class=class="str">"cmt">//| | class=class="str">"cmt">//| Parameters: | class=class="str">"cmt">//| class="type">int a is a minuend | class=class="str">"cmt">//| class="type">int b is a subtrahend | class=class="str">"cmt">//| Return value: | class=class="str">"cmt">//| difference between a and b | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int diff(class="type">int a, class="type">int b) { class=class="str">"cmt">// The action is very simple, we do not create a variable for the result. class="kw">return (a-b); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Example 1a | class=class="str">"cmt">//| The function returns the difference between two real numbers. | class=class="str">"cmt">//| | class=class="str">"cmt">//| Function name is as in the previous example, but parameter type | class=class="str">"cmt">//| differs | class=class="str">"cmt">//| | class=class="str">"cmt">//| Parameters: | class=class="str">"cmt">//| class="type">class="kw">double a is a minuendе | class=class="str">"cmt">//| class="type">class="kw">double b is a subtrahend | class=class="str">"cmt">//| Return value: | class=class="str">"cmt">//| difference between a and b | class=class="str">"cmt">//+------------------------------------------------------------------+
「void 与无参函数的调用细节」
在 MQL5 里,函数返回类型写成 void 就代表不往外递任何值,调用时只管跑内部逻辑,不需要 return 收尾。上面 test() 里直接拿 Print 把 diff 的结果打出来,省掉了中间变量,适合做快速自检。 diff 本身接收两个 double 参数并返回差值。传整数 3 和 4 时,Print(diff(3,4)) 输出 -1;传双精度 3.0 和 4.0 时输出 -1.0。这个现象说明:实参类型会影响终端显示精度,但函数签名锁死的是形参类型,编译器按声明做隐式转换。 无参函数有两种写法:括号留空,或显式写 void。示例里的 name(void) 返回固定字符串 "Needed Name",在 EA 初始化阶段拿来给外部参数赋默认标签很顺手。外汇与贵金属波动剧烈、杠杆风险高,这类工具函数只解决拼接与调试,不替代仓位判断。 开 MT5 按 F4 建个脚本,把下面代码原样贴进去编译,切回图表看专家日志,能直接验证 -1 与 -1.0 的差异。
class="type">class="kw">double diff(class="type">class="kw">double a, class="type">class="kw">double b) { class="kw">return (a-b); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Example class="num">2 | class=class="str">"cmt">//| Illustrates the use of "class="type">void". | class=class="str">"cmt">//| Calls(uses) the diff function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void test() { class=class="str">"cmt">// You can do whatever you want. class=class="str">"cmt">// For example, use the function from Example class="num">1. Print(diff(class="num">3,class="num">4)); class=class="str">"cmt">// the result is -class="num">1 class=class="str">"cmt">// Since when calling the diff function, integer parameters were class=class="str">"cmt">// passed in parentheses, the result is also class="type">int. class=class="str">"cmt">// Now let&class="macro">#x27;s try to call the same function with class="type">class="kw">double precision parameters Print(diff(class="num">3.0,class="num">4.0)); class=class="str">"cmt">// the result is -class="num">1.0 class=class="str">"cmt">// Since the function is declared as "class="type">void", the "class="kw">return" statement is not needed } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Example class="num">3 | class=class="str">"cmt">//| The function has no parameters to process. We could use | class=class="str">"cmt">//| empty parentheses as in the previous example or explicitly use | class=class="str">"cmt">//| the word "class="type">void" | class=class="str">"cmt">//| Return value: | class=class="str">"cmt">//| class="type">class="kw">string nameForReturn is some name , always the same | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">string name(class="type">void) { class="type">class="kw">string nameForReturn="Needed Name"; class="kw">return nameForReturn; }
局部与全局:变量在哪活、在哪死
在 MT5 写 EA 或脚本时,变量声明位置直接决定谁能读它、它能活多久。函数内部(含形参)声明的就是局部变量,调用时诞生、退出时销毁,别的函數根本看不到。
如果变量写在所有函数之外,它就变成全局变量,整个程序任何函数都能用,生命周期等同于程序运行期。上面的代码里 int globalVariable = 345; 在 OnStart 外,因此任意函数里 Print(globalVariable) 都能输出 345。
花括号会切出更窄的块作用域。例程中 int k=4; 写在函数内的 {} 里,只能在那对括号内用;把 Print(k) 移到括号外编译直接报错。
同名遮蔽是实盘调试时的暗坑。局部变量可以和全局变量重名,此时函数内默认读到的是局部值。代码里全局 globalVariable=5,函数内又声明 int globalVariable=10,Print(globalVariable) 打出 10;想强制读全局,得写 ::globalVariable,输出才是 5。外汇与贵金属波动剧烈、杠杆高风险大,这类作用域误读可能引发下单逻辑偏差,改完建议开 MT5 编译跑一遍验证。
class="type">void OnStart() { class=class="str">"cmt">//--- Local variable inside a function is visible to all blocks of that function, but not beyond it class="type">int myString = "This is local class="type">class="kw">string"; class=class="str">"cmt">// Curly braces describe a block inside a function { class="type">int k=class="num">4; class=class="str">"cmt">// Block local variable - visible only inside curly braces Print(k); class=class="str">"cmt">// It&class="macro">#x27;s ok Print(myString); } class=class="str">"cmt">// Print(k); // Compilation error. The variable k does not exist outside the curly brace block. } class="type">int globalVariable = class="num">345; class="type">void OnStart() { class=class="str">"cmt">//--- Print(globalVariable); class=class="str">"cmt">// It&class="macro">#x27;s ok } class="type">int globalVariable=class="num">5; class="type">void OnStart() { class="type">int globalVariable=class="num">10; class=class="str">"cmt">// The variable is described according to all the rules, including the type. class=class="str">"cmt">// If the type were not declared, this expression would change the global variable class=class="str">"cmt">//--- Print(globalVariable); class=class="str">"cmt">// The result is class="num">10 - that is, the value of the local variable Print(::globalVariable); class=class="str">"cmt">// The result is class="num">5. To print the value of a global variable, not the local one, class=class="str">"cmt">// we use two colons before the name }
◍ 用 static 把计数留在函数里
MT5 里普通局部变量随函数退出就释放,但有些场景你得在函数外面还记着上一次的状态。最典型的就是数函数被调了几次,或者盯盘时判断新 K 线有没有出来——后者要在每次 tick 拿当前时间跟上次存的时间比,全局变量能用但容易让别的模块误改,埋坑。 这时候给局部变量加一个 static 前缀,生命周期就拉到跟全局一样长,作用域却还锁在函数内。下面这段演示了静态计数器:连续三次调用,日志依次打出 1 calls、2 calls、3 calls,说明 counter 没被重置。 [CODE] //+------------------------------------------------------------------+
| // | Script program start function |
|---|
//+------------------------------------------------------------------+ void OnStart() { //--- HowManyCalls(); HowManyCalls(); HowManyCalls(); } //+------------------------------------------------------------------+
| // | The function counts the number of requests |
|---|
//+------------------------------------------------------------------+ void HowManyCalls() { //--- Variable description. The variable is local, but its lifetime is long. static int counter=0; // Since 'static' keyword is used, the variable is initialized only // before he first function call // (more precisely, before the OnInit function call) //--- Main actions counter++; // During program execution, the value will be stored till the end //--- Operation result Print( IntegerToString(counter)+" calls"); } // Script output: // 1 calls // 2 calls // 3 calls [/CODE] 逐行看关键点:static int counter=0 只在第一轮(严格说 OnInit 前)初始化一次;之后每次进函数 counter++ 都基于上次保留的值累加;Print 把整数转字符串拼上 " calls" 输出。 把这套搬去 EA 里,static datetime lastbar 就能低成本判新柱,不必开全局变量。外汇和贵金属行情跳空频繁,这类状态变量要留意重置时机,否则可能误判 bar 边界。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- HowManyCalls(); HowManyCalls(); HowManyCalls(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| The function counts the number of requests | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void HowManyCalls() { class=class="str">"cmt">//--- Variable description. The variable is local, but its lifetime is class="type">long. class="kw">static class="type">int counter=class="num">0; class=class="str">"cmt">// Since &class="macro">#x27;class="kw">static&class="macro">#x27; keyword is used, the variable is initialized only class=class="str">"cmt">// before he first function call class=class="str">"cmt">// (more precisely, before the OnInit function call) class=class="str">"cmt">//--- Main actions counter++; class=class="str">"cmt">// During program execution, the value will be stored till the end class=class="str">"cmt">//--- Operation result Print( IntegerToString(counter)+" calls"); } class=class="str">"cmt">// Script output: class=class="str">"cmt">// class="num">1 calls class=class="str">"cmt">// class="num">2 calls class=class="str">"cmt">// class="num">3 calls
「改不动的还是改得动的:参数传递的引用门槛」
MQL5 里函数默认拿的是数据副本,也就是「按值」传参。你在函数里把形参改了天翻地覆,外面的原变量纹丝不动。想让函数直接动到源数据,形参前面得挂个 &(与号),这叫「按引用」传递。 上面这段脚本把差异摊得很直白:OnStart 里 first=3、second=77,Swap 走引用,调完之后日志显示 first=77、second=3,原值被调换了;CheckLocal 走值传递,里面把 a、b 改成 5 和 10,出来一打印还是 77 和 3,外部毫无波动。 有个硬规矩得刻进肌肉记忆:枚举、结构、对象、任何数组这类复杂类型,编译器只认引用传递。你敢写按值传,MT5 编译阶段直接报错,根本跑不起来。 全局变量活到程序退出,局部变量随声明块消亡;同名时局部覆盖全局。写 EA 或脚本时,若你期望子函数回写计算结果,漏写 & 就会静默失效,这种 bug 在盯盘逻辑里可能让信号判断偏离预期。外汇与贵金属杠杆高,策略代码错一处,实盘风险会被放大。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart(class="type">void) { class=class="str">"cmt">//--- Declare and initialize two local variables class="type">int first = class="num">3; class="type">int second = class="num">77; class=class="str">"cmt">//--- Print their values BEFORE all changes Print("Before swap: first = " + first + " second = " + second); class=class="str">"cmt">//--- Use the Swap function, which takes data by reference Swap(first,second); class=class="str">"cmt">//--- See what happened Print("After swap: first = " + first + " second = " + second); class=class="str">"cmt">//--- class=class="str">"cmt">//--- Apply the CheckLocal function to the received data class=class="str">"cmt">//--- This function takes parameters by value CheckLocal(first,second); class=class="str">"cmt">//--- Print the result again Print("After CheckLocal: first = " + first + " second = " + second); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Swaps the values of two integer variables | class=class="str">"cmt">//| Data is passed by reference, so the originals will be modified | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Swap(class="type">int &a, class="type">int& b) class=class="str">"cmt">// It can be done in any way, both positions are correct { class="type">int temp; class=class="str">"cmt">//--- temp = a; a = b; b = temp; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Takes parameters by value, that is why changes happen | class=class="str">"cmt">//| only locally | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CheckLocal(class="type">int a, class="type">int b) { a = class="num">5; b = class="num">10; } class=class="str">"cmt">// Script output: class=class="str">"cmt">// Before swap: first = class="num">3 second = class="num">77 class=class="str">"cmt">// After swap: first = class="num">77 second = class="num">3 class=class="str">"cmt">// After CheckLocal: first = class="num">77 second = class="num">3