MQL5 最佳实践(2026 Q3 更新)
📊

MQL5 最佳实践(2026 Q3 更新)

代码质量、性能调优与上线前清单

MQL5 编程 难度 · 进阶 2026-07-24 6 分钟阅读
#最佳实践#上线检查

为什么要在脚本里模拟键盘事件

很多中文交易者习惯用指标或专家顾问拦截图表上的按键消息,因为这两类程序天然支持 OnChartEvent 处理机制,可以轻松拿到用户按下了哪个键。但脚本在设计上并不支持事件处理程序,也就是说你没法在脚本里直接写 OnChartEvent 来捕获按键。可是实际交易中,我们有时只是想写个一次性工具:用方向键在图表上移动一条参考线,看看某根 K 线的收盘价位置,用完就删掉对象。这种需求如果非得写成指标,反而把简单事情搞复杂了。

所以本文要解决的真实问题是:如何只依靠标准函数,在脚本运行期间“轮询”键盘状态,从而模拟出类似按键事件的效果。注意,这里说的是模拟,不是真正拦截系统消息。脚本内部永远拿不到原始的按键消息流,只能通过终端提供的信息函数去查询某些键当前是否被按住。理解这一点,才能避免后续写出自以为能抓事件、实际根本不触发的错误代码。

⚠ 对象残留风险
当用户切换周期时,终端会重建图表并移除脚本,但如果你的代码没在退出前删除对象,那些水平线、垂直线会留在图上。下次加载又叠加新的,图表越来越乱。所以 Deinit 逻辑不是可选项,是上线必须检查的环节。

基础实现:轮询按键与对象控制

最直白的思路是:在 OnStart 里先创建两条线,然后进入一个循环,不断检查上、下键的状态。如果发现上键被按住,就把索引减一;下键被按住,索引加一。索引指向 Bars 函数返回范围内的某根 K 线,再用 CopyRates 取出时间和收盘价,移动对象到对应位置。循环里一定要放 Sleep,否则空转会吃掉大量 CPU,导致终端卡顿甚至被系统限流。

退出条件用 IsStopped 判断,当用户移除脚本或终端关闭时,循环自然结束,接着执行删除对象与重绘。下面这段示范代码展示了核心结构:定义按键常量、初始化对象、循环里查键、移动对象、退出清理。它不依赖任何外部库,纯标准环境可编译。

#define def_KEY_UP   38
#define def_KEY_DOWN 40
string gl_Objs[2];
void OnStart(void)
{
   ObjectCreate(0, gl_Objs[0] = "Demo" + (string)ObjectsTotal(0), OBJ_VLINE, 0, 0, 0);
   ObjectSetInteger(0, gl_Objs[0], OBJPROP_COLOR, clrRoyalBlue);
   ObjectCreate(0, gl_Objs[1] = "Demo" + (string)ObjectsTotal(0), OBJ_HLINE, 0, 0, 0);
   ObjectSetInteger(0, gl_Objs[1], OBJPROP_COLOR, clrPurple);
   int p = 0;
   MqlRates rate[1];
   while(!IsStopped())
   {
      if(TerminalInfoInteger(TERMINAL_KEYSTATE_UP)) p = (p > 0 ? p - 1 : p);
      if(TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN)) p = (p < Bars(_Symbol, _Period) ? p + 1 : p);
      CopyRates(_Symbol, _Period, p, rate.Size(), rate);
      ObjectMove(0, gl_Objs[0], 0, rate[0].time, rate[0].close);
      ObjectMove(0, gl_Objs[1], 0, rate[0].time, rate[0].close);
      Sleep(100);
   }
   for(uint c = 0; c < gl_Objs.Size(); c++) ObjectDelete(0, gl_Objs[c]);
   ChartRedraw();
}
✦ Sleep 间隔取舍
100 毫秒是兼顾响应与消耗的折中。若做高频盯盘工具可降到 50,但别低于 10;若只是偶尔查看,200 也可。外汇与贵金属波动随机,任何键盘辅助工具都不能替代风控。

函数指针是什么:从变量视角理解

很多初学者听到“指针”就头大,以为那是底层语言才有的危险东西。其实在本平台的脚本语言里,官方出于安全考虑没有开放普通内存指针,但允许一种受限形式:函数指针。你可以把它理解成一种特殊变量,里面存的不是数字或字符串,而是某个函数所在的入口地址。当我们调用这个变量时,程序就去那个地址执行对应的函数体。

为什么需要它?假设我们有好几个逻辑相似的函数,比如处理上键、下键、无键。如果写一堆 if else 去调用,代码会又长又难改。用函数指针数组,就可以把函数当数据一样放进数组,用下标决定执行哪一个。这样扩展新按键行为时,只改数组赋值,不动主循环。对中级交易者写可维护工具很有价值。

定义函数指针要用 typedef 配合签名模板。签名必须和真实函数完全一致:返回类型、参数类型和顺序都得匹配,否则编译报错。下面例子定义了一个指向“接收 const int 返回 string”的函数的指针类型 FnPtr,然后把两个同名结构函数装进数组,通过循环调用。

typedef string (*FnPtr)(const int);
string Msg_01(const int value) { return "Msg_01: " + (string)value; }
string Msg_02(const int value) { return "Msg_02: " + (string)value; }
void OnStart(void)
{
   FnPtr FnMsg[2];
   FnMsg[0] = Msg_01;
   FnMsg[1] = Msg_02;
   for(uchar c = 0; c < FnMsg.Size(); c++)
      Print(FnMsg[c](c * 10 + 5));
}

用函数指针重构键盘脚本

回到前面的脚本,我们把初始化、主循环、清理三个步骤定义为三个无参过程,用过程指针数组按顺序执行。主循环内部再把“无键、上一根、下一根”三种行为定义为带引用参数的过程,用按键状态算出的下标来调用。这样主逻辑非常干净:OnStart 只管按顺序跑数组,Loop 只管查键选过程。

这种写法初期看起来绕,但后期加功能轻松。比如你想加个 Home 键跳到最新根,只需写个 Bar_Home 过程,在数组里加一项,改一下三元运算的映射,不必碰循环体结构。对于要长期维护的自定义工具,这种概率上的可扩展性比一时方便更重要。

typedef void (*ProcPtr)(void);
typedef void (*KeyEvent)(int &);
string gl_Objs[2];
void OnStart(void)
{
   ProcPtr proc[3];
   proc[0] = Init; proc[1] = Loop; proc[2] = Deinit;
   for(uint c = 0; c < proc.Size(); c++) proc[c]();
}
void Init(void) { /* 创建对象 */ }
void Loop(void)
{
   KeyEvent key[3]; int pos = 0;
   key[0] = Bar_NONE; key[1] = Bar_Next; key[2] = Bar_Prev;
   while(!IsStopped())
   {
      key[TerminalInfoInteger(TERMINAL_KEYSTATE_UP) ? 1 : (TerminalInfoInteger(TERMINAL_KEYSTATE_DOWN) ? 2 : 0)](pos);
      Sleep(100);
   }
}
void Bar_Next(int &arg) { arg = (arg > 0 ? arg - 1 : arg); Event(arg); }
void Bar_Prev(int &arg) { arg = (arg < Bars(_Symbol, _Period) ? arg + 1 : arg); Event(arg); }
void Bar_NONE(int &arg) {}
void Event(const int arg) { /* 移动对象 */ }
void Deinit(void) { /* 删除对象 */ }
⚠ 引用参数别误用
上面 Bar_Next 等过程用了 int & 引用参数,才能在内部改外部 pos 值。如果漏掉 & 写成值传递,循环里 pos 永远不变,按键像失灵。这是新手常踩的坑。

生产环境部署检查清单

把你写的键盘辅助脚本真正用到盘前盘后,不能只在本机随手跑。以下几点建议来自常见翻车案例,概率上能避开大部分低级失误。外汇、贵金属属高风险品种,任何脚本仅作辅助参考,不构成方向判断。

✦ 对比指标方案
若需长期常驻且要稳定收事件,指标比脚本更合适,因为它天生有 OnChartEvent。脚本方案胜在轻量、即用即弃。按使用频率选,别盲目统一。

常见认知误区与排错

误区一:以为脚本能收到 CHARTEVENT_KEYDOWN。实际上那是指标和专家顾问的事件,脚本里写 OnChartEvent 不会被调用。误区二:以为函数指针很神秘,其实它只是把函数当变量存。误区三:以为 Sleep(0) 不要紧,结果轮询空转让终端变卡,反而漏键。

排错时先打印 IsStopped 退出前的最后状态,再看对象数组大小。若发现线没动,优先查 TerminalInfoInteger 的两个键常量是否和平台定义一致;不同版本终端键值偶尔有出入,用实际捕获值替换宏更安全。中文交易者常因复制旧代码忽略版本差异而出错。

小结与进阶方向

本文从脚本模拟键盘的由头,讲清了轮询机制、对象清理、函数指针定义与数组化调度。你可以把这些拼装成自己的看图小工具。下一步可研究鼠标事件在指标里的处理,或者把函数指针思想用在多策略切换的专家顾问上。记住:工具降低重复劳动,但外汇贵金属高风险,策略失效概率始终存在,落地前务必模拟验证。

常见问题

脚本不支持事件处理程序,OnChartEvent 不会被终端调用。正确做法是在 OnStart 里用 TerminalInfoInteger(TERMINAL_KEYSTATE_UP/DOWN) 轮询按键状态,配合 while(!IsStopped()) 与 Sleep(100) 移动对象。
用 typedef 返回类型 (*类型名)(参数列表) 形式,例如 typedef void (*ProcPtr)(void); 目标函数的返回类型、参数类型和顺序必须完全匹配。然后把函数名赋值给数组元素,用 数组[下标](实参) 调用。
你可以把本文的函数指针模板和轮询框架发给小布盯盘,让它按你的品种和键值习惯生成可直接编译的脚本雏形,并提醒检查 Deinit 对象删除与 Sleep 间隔,降低手写漏掉清理逻辑的概率。
终端重建图表会移除脚本但不自动删对象。必须在退出前(如 Deinit 或循环结束处)用 ObjectDelete 遍历 gl_Objs 删除,并 ChartRedraw。建议上线前切周期测一次残留。
因为要在被调过程内部修改外部循环的索引值(如 pos)。若写成值传递 int,改动只在过程内有效,外部 pos 不变,导致按键看似失灵。引用参数才能回写。
实时快讯
实时
加载中…
查看全部 →