MQL5 最佳实践(2026 Q3 更新)
代码质量、性能调优与上线前清单
为什么要在脚本里模拟键盘事件
很多中文交易者习惯用指标或专家顾问拦截图表上的按键消息,因为这两类程序天然支持 OnChartEvent 处理机制,可以轻松拿到用户按下了哪个键。但脚本在设计上并不支持事件处理程序,也就是说你没法在脚本里直接写 OnChartEvent 来捕获按键。可是实际交易中,我们有时只是想写个一次性工具:用方向键在图表上移动一条参考线,看看某根 K 线的收盘价位置,用完就删掉对象。这种需求如果非得写成指标,反而把简单事情搞复杂了。
所以本文要解决的真实问题是:如何只依靠标准函数,在脚本运行期间“轮询”键盘状态,从而模拟出类似按键事件的效果。注意,这里说的是模拟,不是真正拦截系统消息。脚本内部永远拿不到原始的按键消息流,只能通过终端提供的信息函数去查询某些键当前是否被按住。理解这一点,才能避免后续写出自以为能抓事件、实际根本不触发的错误代码。
- 脚本不支持 OnChartEvent,不能像指标那样被动等消息
- 可以用终端信息函数主动查询上下方向键的状态
- 必须在循环中轮询,并配合休眠降低 CPU 占用
- 脚本结束或被图表刷新移除时,要主动删除自己创建的对象
基础实现:轮询按键与对象控制
最直白的思路是:在 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();
}
函数指针是什么:从变量视角理解
很多初学者听到“指针”就头大,以为那是底层语言才有的危险东西。其实在本平台的脚本语言里,官方出于安全考虑没有开放普通内存指针,但允许一种受限形式:函数指针。你可以把它理解成一种特殊变量,里面存的不是数字或字符串,而是某个函数所在的入口地址。当我们调用这个变量时,程序就去那个地址执行对应的函数体。
为什么需要它?假设我们有好几个逻辑相似的函数,比如处理上键、下键、无键。如果写一堆 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));
}
- 函数指针类型定义依赖 typedef 和 (*名称) 形式
- 目标函数签名必须与指针类型严格一致
- 可以把函数赋值给数组元素,实现动态调度
- 调用时写 数组名[下标](参数) 即可,像普通函数调用
用函数指针重构键盘脚本
回到前面的脚本,我们把初始化、主循环、清理三个步骤定义为三个无参过程,用过程指针数组按顺序执行。主循环内部再把“无键、上一根、下一根”三种行为定义为带引用参数的过程,用按键状态算出的下标来调用。这样主逻辑非常干净: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) { /* 删除对象 */ }
生产环境部署检查清单
把你写的键盘辅助脚本真正用到盘前盘后,不能只在本机随手跑。以下几点建议来自常见翻车案例,概率上能避开大部分低级失误。外汇、贵金属属高风险品种,任何脚本仅作辅助参考,不构成方向判断。
- 确认脚本退出路径必走 Deinit,对象无残留
- 循环内含 IsStopped 与 Sleep,避免 CPU 占满
- 函数指针签名与实际函数逐字核对,防止编译期漏网
- 引用参数检查是否真的改到了外部变量
- 在模拟盘或历史回放先跑一天,观察切换周期后的行为
- 不在脚本里硬编码账户、品种,用 _Symbol _Period 自适应
常见认知误区与排错
误区一:以为脚本能收到 CHARTEVENT_KEYDOWN。实际上那是指标和专家顾问的事件,脚本里写 OnChartEvent 不会被调用。误区二:以为函数指针很神秘,其实它只是把函数当变量存。误区三:以为 Sleep(0) 不要紧,结果轮询空转让终端变卡,反而漏键。
排错时先打印 IsStopped 退出前的最后状态,再看对象数组大小。若发现线没动,优先查 TerminalInfoInteger 的两个键常量是否和平台定义一致;不同版本终端键值偶尔有出入,用实际捕获值替换宏更安全。中文交易者常因复制旧代码忽略版本差异而出错。
小结与进阶方向
本文从脚本模拟键盘的由头,讲清了轮询机制、对象清理、函数指针定义与数组化调度。你可以把这些拼装成自己的看图小工具。下一步可研究鼠标事件在指标里的处理,或者把函数指针思想用在多策略切换的专家顾问上。记住:工具降低重复劳动,但外汇贵金属高风险,策略失效概率始终存在,落地前务必模拟验证。