MQL5 编程基础: 终端的全局变量·综合运用
(3/3)·从函数到类再到实战,把终端全局变量的生存期与隔离性一次讲透
不少人在多终端并行时误以为全局变量是跨端共享的,结果程序间互相踩内存。其实每个终端实例都有独立的全局变量空间,关终端也不清数据,理解错这点调试会绕大弯。
「用全局变量卡住 EA 的执行阶段」
把交易想法固化成 MQL5 程序时,最怕机器人跑到一半失联却看不出卡在哪个环节。给每个功能模块配一对全局变量当「开始 / 结束」开关,就能在 EA 被强删或断线后反推中断点。 一个模块化 EA 通常拆成开仓、平仓、追随止损三块。每块两个控制点,意味着一共要 6 个全局变量,例如 gvarOpen_start、gvarOpen_finish 分别对应开仓模块的开始与结束。主函数在 OnInit 之后于 Main() 里先把所有标志置 false,模块跑完再清自己的那一对。 实测里把示例 EA 挂上图表正常跑,再从图表移除,日志会留下「中断位于 Open() 模块」这类记录;按 F3 打开全局变量列表,只见 gvarOpen_start 被归零,其余还在。说明开仓指令执行失败能被立刻定位。同图表重启机器人,日志补发阶段故障报警——外汇与贵金属杠杆高,这类中断往往意味着滑点或无服务器应答,重跑前先确认 TERMINAL_CONNECTED。 下面这段是主模块骨架:OnInit 只回 INIT_SUCCEEDED;OnDeinit 清图表注释;OnTick 直接调 Main()。Main() 先循环把 6 个标志写 false,再三重判断交易权限与连接,通过后依次跑 Open()、Close()、Trail()。全局变量名数组写死 6 个字符串,和前面说的控制点一一对应。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- class="kw">return INIT_SUCCEEDED; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { Comment(""); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { Main(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Main module | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Main(class="type">void) { class=class="str">"cmt">//--- set flags for all modules for(class="type">int idx=class="num">0;idx<GVARS_LIST_SIZE;idx++) SetFlag(idx,false); class=class="str">"cmt">//--- Check the trade possibility and connectivity class=class="str">"cmt">//--- permission to trade if(TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) class=class="str">"cmt">//--- connection to the trading server if(TerminalInfoInteger(TERMINAL_CONNECTED)) class=class="str">"cmt">//--- permission to trade for the launched EA if(MQLInfoInteger(MQL_TRADE_ALLOWED)) { class=class="str">"cmt">//--- class="num">1) opening module Open(); class=class="str">"cmt">//--- class="num">2) closing module Close(); class=class="str">"cmt">//--- class="num">3) Trailing Stop module Trail(); } } class=class="str">"cmt">//--- global variables: names class="type">class="kw">string gVar_names[class="num">6]= { "gvarOpen_start","gvarOpen_finish", "gvarClose_start","gvarClose_finish", "gvarTrail_start","gvarTrail_finish" };
开仓模块的阻塞式生命周期与全局标记
EA 里把开仓逻辑单独封进 Open() 函数,入口先打一行 Comment 显示当前模块名加函数名,方便在图表上实时看跑到哪一步。 函数体内先用 IsStopped() 拦一道,避免 EA 被手动停止后还硬跑。通过才清掉模块起始标记 SetFlag(0,true),随即 Sleep(1250) 原地停约 1.25 秒——这段时间主线程被占住,其他事件不处理,属于典型的阻塞式模块调度。 睡醒后置 SetFlag(1,true) 清掉结束标记,逻辑上假定本模块耗时就是 1.25 秒左右。下面是这段 Open 函数的原文: [CODE]
| // | Open module |
|---|
//+------------------------------------------------------------------+ void Open(void) { Comment(curr_module+__FUNCTION__); //--- if(!IsStopped()) { //--- clear the module start flag SetFlag(0,true); //--- assume that the module operates for approximately 1.25 s { Sleep(1250); } //--- clear the module finish flag SetFlag(1,true); } } [/CODE] 日志里能看出端倪:2014.10.22 20:14:29 曾出现 Program forced to terminate before execution: <<Open_finish>>,说明那次 EA 在 Sleep 没跑完就被强停,结束标记没写上。 到了 20:28:25.135 的终端记录里,Open_finish、Close_start、Close_finish、Trail_start、Trail_finish 全部 Non-zero value,意味着各模块全局标记都已置位,多模块衔接在那时是完整的。外汇与贵金属品种波动大、点差跳变频繁,这种靠 Sleep 硬等的模块在实盘可能错过入场窗口,建议在 MT5 用 EURUSD.e H1 复刻这段,观察强制停止后标记残留情况。
class=class="str">"cmt">//| Open module | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void Open(class="type">void) { Comment(curr_module+__FUNCTION__); class=class="str">"cmt">//--- if(!IsStopped()) { class=class="str">"cmt">//--- clear the module start flag SetFlag(class="num">0,true); class=class="str">"cmt">//--- assume that the module operates for approximately class="num">1.25 s { Sleep(class="num">1250); } class=class="str">"cmt">//--- clear the module finish flag SetFlag(class="num">1,true); } }
◍ 全局变量落盘的两个致命写法
把终端全局变量当成跨程序控制点,思路本身没问题:用 CGlobalVarList 的 Save/Load 把变量序列到 txt,就能在多个 MT 实例间共享状态。但代码里两处写法会直接让文件写出 0 KB 并抛「创建指针错误」。 第一处是空指针判断写反了: CGlobalVarList *list = new CGlobalVarList; if(list!=NULL){ Print("Pointer Create Error"); } new 成功时 list 不是 NULL,此时反而打了错误日志,而真正分配失败时没拦住,后面 Save 直接崩。 第二处是 Save 的句柄用法:FileOpen 返回的是 int 句柄,而 CGlobalVarList::Save 期望的是 FILE 句柄对象(CFile 之类),把 int 硬塞进去,list.Save(hFile) 必然返回 false,于是文件建了却是 0 字节。 实测在 MT4 公共目录 C:\Users\<名>\AppData\Roaming\MetaQuotes\Terminal\Common\Files 下,Write.txt 生成大小确为 0 KB,Read.txt 同理因 Load 失败报「指针创建错误」。外汇与贵金属交易本身高风险,这类全局变量共享方案只建议用于状态同步,切勿拿来直接下单控仓。 想验证的话,把指针判断改成 if(list==NULL) 并在 Save 前用 CFile 封装句柄,重新挂脚本到图表,文件大小应从 0 KB 变成实际字节数。
class=class="str">"cmt">// WriteGlobalParameters.mq4 核心片段 CGlobalVarList *list = new CGlobalVarList; class=class="str">"cmt">// 新建全局变量列表对象 if(list!=NULL){ Print("Pointer Create Error"); } class=class="str">"cmt">// 判错写反:成功才打错误 class="type">int hFile; class=class="str">"cmt">// 声明 class="type">int 文件句柄 hFile=FileOpen(file,FILE_WRITE|FILE_COMMON); class=class="str">"cmt">// 打开公共目录文件,返回 class="type">int if(hFile>=class="num">0){ if(!list.Save(hFile)){ Print("File Save Error"); } class=class="str">"cmt">// Save 需 CFile 句柄而非 class="type">int,必败 FileClose(hFile); } class="kw">delete list;
「把这条线请下神坛」
实测里,即便把 ReadGlobalParameters.mq4 挂到图表上,终端弹出的报错和全局变量列表依旧是空的,和前面第①类现象并无二致。 我们额外用 FileOpen() 默认的 FILE_CSV、FILE_UNICODE、FILE_BIN、FILE_ANSI 四种标志位重复了同一套实验,结果没有任何变化,说明问题不在文件访问模式,而在全局变量本身的数据形态。
- 年有用户指出 MT 全局变量只能是 double 类型,想要 key:value 的 CMap 或更灵活的内存指针方案并未落地。开 MT4/MT5 自己跑一遍这段空列表现象,就能确认:别把全局变量当成万能跨 EA 存储,它只是个双精度数的窄通道。