从头开始开发智能交易系统(第 9 部分):概念上的飞跃  (II)(基础篇)
📘

从头开始开发智能交易系统(第 9 部分):概念上的飞跃 (II)(基础篇)

第 1/3 篇

◍ 把指标信号拆成可交易事件

很多新手写 EA 时习惯直接读指标数值大小,比如 MACD 柱大于 0 就做多。这种做法在 MT5 里隐患很大:指标缓冲区的当前值随时被下一 tick 重画,你以为的“信号”可能只是半根 K 线里的噪声。 更稳的思路是把指标输出转换成“事件”。以 iMACD 为例,与其判断 MACDMainBuffer[0] > 0,不如检测主线与信号线的交叉状态是否刚发生翻转——只在 prev_main * prev_signal <= 0curr_main * curr_signal > 0 这种边界上触发。这样能把连续数值压缩成离散决策点,回测里重复开仓的概率会明显下降。 在 MT5 里取历史缓冲要用 CopyBuffer 而不是数组下标硬读,否则多周期切换时容易越界。下面这段是最小验证骨架,复制到脚本里跑一遍,你能直接看到主线与信号线在最近 500 根里的交叉次数。 外汇与贵金属杠杆高,任何指标交叉策略都只是概率倾向,实盘前务必用策略测试器跑足样本。

MQL5 / C++
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 实际收到的是外部对象的事件——因为平台最初并不是为「对象套对象」设计的,内部对象收不到独立事件。做浮动交易窗口时,这会让控制逻辑变得别扭。 规划这类系统时还有个隐藏成本:处理器要维护的对象数量。设想四个可拖拽浮动窗,每个里面的图表交易又会加对象,虽无重计算但代码臃肿、优化感差。更优雅的做法是减少被维护对象的总数,而不是简单加一层控制器。外汇与贵金属市场波动剧烈,这类自定义界面若事件处理不当,可能误导你的下单操作,实盘前务必在模拟账户验证。

MQL5 / C++
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 就能看到主图左上角浮出一块子图。

MQL5 / C++
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 的数值,嵌图宽度会实时变。

MQL5 / C++
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;
}

常见问题

指标交叉只是状态变化,不是事件。应先把交叉拆成「首次触发」「确认回踩」等可交易事件,再过滤假信号,胜率才可能提高。
原生图表系统默认主图优先绘制,浮动界面需走独立窗口或子图通道,否则会被吞掉。用副图容器承载交互控件更稳。
可以。小布盯盘的 AIGC 能读取品种页指标状态,把交叉类信号转成事件标记,并提示过滤条件,你只需复核决策。
会,若不做透明度与缩放隔离,主图噪声翻倍。正确做法是开独立子图区域,仅联动缩放不共享画笔。
需在副图容器里拦截滚轮事件单独处理,同时订阅主图光标坐标广播。这样既能各看各的,又能对照价位。