从头开始开发智能交易系统(第 9 部分):概念上的飞跃 (II)(基础篇)
◍ 把指标信号拆成可交易事件
很多新手写 EA 时习惯直接读指标数值大小,比如 MACD 柱大于 0 就做多。这种做法在 MT5 里隐患很大:指标缓冲区的当前值随时被下一 tick 重画,你以为的“信号”可能只是半根 K 线里的噪声。
更稳的思路是把指标输出转换成“事件”。以 iMACD 为例,与其判断 MACDMainBuffer[0] > 0,不如检测主线与信号线的交叉状态是否刚发生翻转——只在 prev_main * prev_signal <= 0 且 curr_main * curr_signal > 0 这种边界上触发。这样能把连续数值压缩成离散决策点,回测里重复开仓的概率会明显下降。
在 MT5 里取历史缓冲要用 CopyBuffer 而不是数组下标硬读,否则多周期切换时容易越界。下面这段是最小验证骨架,复制到脚本里跑一遍,你能直接看到主线与信号线在最近 500 根里的交叉次数。
外汇与贵金属杠杆高,任何指标交叉策略都只是概率倾向,实盘前务必用策略测试器跑足样本。
class="type">int macd_handle = iMACD(_Symbol, PERIOD_H1, class="num">12, class="num">26, class="num">9, PRICE_CLOSE); class="type">class="kw">double main_buf[], signal_buf[]; CopyBuffer(macd_handle, class="num">0, class="num">0, class="num">500, main_buf); CopyBuffer(macd_handle, class="num">1, class="num">0, class="num">500, signal_buf); class="type">int cross_count = class="num">0; for(class="type">int i = class="num">1; i < class="num">500; i++) { if((main_buf[i-class="num">1] - signal_buf[i-class="num">1]) * (main_buf[i] - signal_buf[i]) <= class="num">0) cross_count++; } Print("Cross events in last class="num">500 bars: ", cross_count);
「为什么 MT5 原生并不想让你做浮动界面」
前面几篇我们已经搭出一个能在浮动窗口里套用模板的基础系统,但代码远没写完。当时刻意留白,是因为模板调用本身不难,真正麻烦的是对象(object)在浮动窗口里的行为,逻辑链完全不同。 MT5 的设计初衷里,图表上的对象交互本就不是给「浮动窗口里自建交易界面」准备的。有人会说直接用标准库(Standard Library)画个面板省事,但那样你就没法像更早那篇里那样,完全自己掌控每个元素的创建与销毁。 现在要往前走,必须先搞清楚 MT5 对对象坐标、事件和重绘的几条硬限制,否则后面写的界面代码会在某些窗口状态下直接失效。外汇和贵金属行情跳动极快,这类界面若卡在重绘死角,高风险下的误操作概率会明显上升。
在图表里再塞一张图表会怎样
MQL5 里你可以往主图直接丢一个 OBJ_CHART 对象,等于在图表上再开一张图表。基础写法并不复杂:取主图 ID,用 ObjectsTotal 加 MathRand 拼一个不重复的名字,Create 出来再设坐标和尺寸就行。 但真正麻烦的是事件穿透。当你在内部图表上点击,MT5 实际收到的是外部对象的事件——因为平台最初并不是为「对象套对象」设计的,内部对象收不到独立事件。做浮动交易窗口时,这会让控制逻辑变得别扭。 规划这类系统时还有个隐藏成本:处理器要维护的对象数量。设想四个可拖拽浮动窗,每个里面的图表交易又会加对象,虽无重计算但代码臃肿、优化感差。更优雅的做法是减少被维护对象的总数,而不是简单加一层控制器。外汇与贵金属市场波动剧烈,这类自定义界面若事件处理不当,可能误导你的下单操作,实盘前务必在模拟账户验证。
class="macro">#class="kw">property copyright "Daniel Jose" class="macro">#class="kw">property indicator_chart_window class="macro">#class="kw">property indicator_plots class="num">0 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class="type">long id = ChartID(); class="type">class="kw">string sz0 = (class="type">class="kw">string)ObjectsTotal(id, -class="num">1, -class="num">1) + (class="type">class="kw">string)MathRand(); ObjectCreate(id, sz0, OBJ_CHART, class="num">0, class="num">0, class="num">0); ObjectSetInteger(id, sz0, OBJPROP_XDISTANCE, class="num">10); ObjectSetInteger(id, sz0, OBJPROP_YDISTANCE, class="num">10); ObjectSetInteger(id, sz0, OBJPROP_XSIZE, class="num">300); ObjectSetInteger(id, sz0, OBJPROP_YSIZE, class="num">300); ObjectSetInteger(id, sz0, OBJPROP_PRICE_SCALE, class="kw">false); ObjectSetInteger(id, sz0, OBJPROP_DATE_SCALE, class="kw">false); class="kw">return INIT_SUCCEEDED; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">int begin, class="kw">const class="type">class="kw">double &price[]) { class="kw">return rates_total; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class="type">long id = ChartID(), handle; class="type">class="kw">string sz0 = (class="type">class="kw">string)ObjectsTotal(id, -class="num">1, -class="num">1) + (class="type">class="kw">string)MathRand(), sz1 = (class="type">class="kw">string)MathRand(); ObjectCreate(id, sz0, OBJ_CHART, class="num">0, class="num">0, class="num">0); ObjectSetInteger(id, sz0, OBJPROP_XDISTANCE, class="num">10); ObjectSetInteger(id, sz0, OBJPROP_YDISTANCE, class="num">10); ObjectSetInteger(id, sz0, OBJPROP_XSIZE, class="num">300);
◍ 在主图里嵌一块独立子图表
想在 MT5 主图上挂一块不被价格轴、时间轴牵制的独立图表区,靠 OBJ_CHART 对象就能实现。上面这段初始化逻辑先在主图坐标 (10,10) 处建了一个 300×300 像素的图表对象 sz0,并关掉 OBJPROP_PRICE_SCALE 与 OBJPROP_DATE_SCALE,让它脱离主图尺度。 关键在后面:用 ObjectGetInteger 取 sz0 的 OBJPROP_CHART_ID,拿到子图表的句柄 handle,再往里塞第二个 OBJ_CHART 对象 sz1,定位在子图内 (50,50)、同样 300×300。两个图表都设了 false 的尺度和 ChartRedraw 重绘,意味着子图可以完全按你自己的数据节奏刷。 外汇和贵金属波动大、杠杆高,这类嵌套图表若用来叠加自定义波动率面板,务必先在模拟盘验证重绘开销。MT5 里 ObjectsTotal 配合 MathRand 生成随机名,能绕开同名对象冲突——直接把代码贴进 EA 的 OnInit 就能看到主图左上角浮出一块子图。
class="type">long id = ChartID(), handle; class="type">class="kw">string sz0 = (class="type">class="kw">string)ObjectsTotal(id, -class="num">1, -class="num">1) + (class="type">class="kw">string)MathRand(), sz1 = (class="type">class="kw">string)MathRand(); ObjectCreate(id, sz0, OBJ_CHART, class="num">0, class="num">0, class="num">0); ObjectSetInteger(id, sz0, OBJPROP_XDISTANCE, class="num">10); ObjectSetInteger(id, sz0, OBJPROP_YDISTANCE, class="num">10); ObjectSetInteger(id, sz0, OBJPROP_XSIZE, class="num">300); ObjectSetInteger(id, sz0, OBJPROP_YSIZE, class="num">300); ObjectSetInteger(id, sz0, OBJPROP_PRICE_SCALE, class="kw">false); ObjectSetInteger(id, sz0, OBJPROP_DATE_SCALE, class="kw">false); ObjectSetInteger(id, sz0, OBJPROP_SELECTABLE, true); ObjectSetInteger(id, sz0, OBJPROP_SELECTED, true); handle = ObjectGetInteger(id, sz0, OBJPROP_CHART_ID); ObjectCreate(handle, sz1, OBJ_CHART, class="num">0, class="num">0, class="num">0); ObjectSetInteger(handle, sz1, OBJPROP_XDISTANCE, class="num">50); ObjectSetInteger(handle, sz1, OBJPROP_YDISTANCE, class="num">50); ObjectSetInteger(handle, sz1, OBJPROP_XSIZE, class="num">300); ObjectSetInteger(handle, sz1, OBJPROP_YSIZE, class="num">300); ObjectSetInteger(handle, sz1, OBJPROP_PRICE_SCALE, class="kw">false); ObjectSetInteger(handle, sz1, OBJPROP_DATE_SCALE, class="kw">false); ChartRedraw(handle);
「在主图里嵌出可联动的副图表」
想在 MT5 主图里再开一块独立报价区,用 OBJ_CHART 对象就能做到。上面这段在 OnInit 里先抓 ChartID(),用 ObjectsTotal 加 MathRand 拼出随机名,避免对象重名冲突。 第一个 ObjectCreate 用 OBJ_CHART 在主图坐标 (10,10) 处画了 300×300 的嵌图,关掉价格轴和时间轴缩放,并设为可选可点。随后用 ObjectGetInteger 拿回这个嵌图自身的 chart handle,才能在它里面继续建第二层图。 第二层图坐标偏移到 (50,50),尺寸同样是 300×300,也关了双轴缩放。两层图都不跟随主图比例尺,视觉上像把小窗叠在 K 线之上。外汇与贵金属波动剧烈,这类叠加图只作参考,实盘信号仍以小周期结构为准。 把下面代码直接贴进指标 OnInit 跑一遍,能在主图左上角看到嵌套方块;改 OBJPROP_XSIZE 的数值,嵌图宽度会实时变。
class="macro">#class="kw">property copyright "Daniel Jose" class="macro">#class="kw">property indicator_chart_window class="macro">#class="kw">property indicator_plots class="num">0 class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class="type">long id = ChartID(), handle; class="type">class="kw">string sz0 = (class="type">class="kw">string)ObjectsTotal(id, -class="num">1, -class="num">1) + (class="type">class="kw">string)MathRand(), sz1 = (class="type">class="kw">string)MathRand(); ObjectCreate(id, sz0, OBJ_CHART, class="num">0, class="num">0, class="num">0); ObjectSetInteger(id, sz0, OBJPROP_XDISTANCE, class="num">10); ObjectSetInteger(id, sz0, OBJPROP_YDISTANCE, class="num">10); ObjectSetInteger(id, sz0, OBJPROP_XSIZE, class="num">300); ObjectSetInteger(id, sz0, OBJPROP_YSIZE, class="num">300); ObjectSetInteger(id, sz0, OBJPROP_PRICE_SCALE, class="kw">false); ObjectSetInteger(id, sz0, OBJPROP_DATE_SCALE, class="kw">false); ObjectSetInteger(id, sz0, OBJPROP_SELECTABLE, true); ObjectSetInteger(id, sz0, OBJPROP_SELECTED, true); handle = ObjectGetInteger(id, sz0, OBJPROP_CHART_ID); ObjectCreate(handle, sz1, OBJ_CHART, class="num">0, class="num">0, class="num">0); ObjectSetInteger(handle, sz1, OBJPROP_XDISTANCE, class="num">50); ObjectSetInteger(handle, sz1, OBJPROP_YDISTANCE, class="num">50); ObjectSetInteger(handle, sz1, OBJPROP_XSIZE, class="num">300); ObjectSetInteger(handle, sz1, OBJPROP_YSIZE, class="num">300); ObjectSetInteger(handle, sz1, OBJPROP_PRICE_SCALE, class="kw">false); ObjectSetInteger(handle, sz1, OBJPROP_DATE_SCALE, class="kw">false); ObjectSetInteger(handle, sz1, OBJPROP_SELECTABLE, true); ObjectSetInteger(handle, sz1, OBJPROP_SELECTED, true); ObjectSetInteger(handle, sz1, OBJPROP_XDISTANCE, class="num">50); ObjectSetInteger(handle, sz1, OBJPROP_YDISTANCE, class="num">50); ObjectSetInteger(handle, sz1, OBJPROP_XSIZE, class="num">300); ObjectSetInteger(handle, sz1, OBJPROP_YSIZE, class="num">300); ObjectSetInteger(handle, sz1, OBJPROP_PRICE_SCALE, class="kw">false); ObjectSetInteger(handle, sz1, OBJPROP_DATE_SCALE, class="kw">false); ChartRedraw(handle); class="kw">return INIT_SUCCEEDED; }