从头开始开发智能交易系统(第 30 部分):CHART TRADE 当作指标?(基础篇)
「把图表成交痕迹当指标用」
在 MT5 里,CHART TRADE 指的并非独立指标,而是图表上由真实成交产生的箭头、线宽与价位标注。很多手动交易者靠这些痕迹判断主力介入,但程序化系统默认不会读取它们。 MQL5 提供了 ChartGetInteger 与 OBJ_TRADE 系列对象接口,可以从图表直接抓取最近一笔成交的对象属性。下面这段代码演示如何取得图表上最后一个成交对象的 ticket 与开仓价,跑通后你就能在 EA 里把‘肉眼看到的成交痕迹’变成可计算变量。 外汇与贵金属杠杆高、滑点大,用成交痕迹做信号只是概率倾向,不能直接当作方向确认。建议先在策略测试器用 2022 年黄金 1 分钟数据回放,观察痕迹出现后 3 根 K 线内的反向刺穿比例,再决定是否接入实盘逻辑。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 获取图表上最后一个成交对象的信息 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">long ticket = ChartGetInteger(class="num">0, CHART_LAST_TRADE_ID); if(ticket != class="num">0) { class="type">class="kw">double open_price = ObjectGetDouble(class="num">0, "Trade_"+(class="type">class="kw">string)ticket, OBJPROP_PRICE); Print("Last trade ticket: ", ticket, " open price: ", open_price); } else Print("No trade objects on chart");
◍ 把 Chart Trade 从 EA 里剥离出来
在前面第 29 部分里,我们已经把 Chart Trade 从 EA 中移除,之前 Volume At Price、Times & Trade 也做过同样处理,目的是给 EA 减重、提升执行可靠性。去掉之后 EA 只剩最基础的订单系统:挂单、市价单、仓位管理都能跑,但一部分交易者习惯等价格触价再手动市价进出,不喜欢提前挂单。 MT5 左上角自带“快速交易”按钮可以下市价单,逻辑和早期 Chart Trading 差不多;但它在交叉订单系统(本系列第 11 部分讨论过)下根本不显示,那种场景就只能回头用 Chart Trade。区别在于:现在它不再是 EA 代码的一部分,而是一个独立指标。 这么切分的原因很直接——EA 只该管交易系统本身,凡是订单系统以外的东西都应从 EA 代码里踢出去。短期看像多此一举,后续续篇会讲清动机。 若把 Chart Trade 做成脚本有个坑:一切换图表周期脚本就退出,得手动重跑;做成指标则常驻,且不占用 EA 执行线程,EA 可专注订单与仓位。指标版虽拿不到原版全部控制信息和底层数据,但足够轻、照样能用。外汇/贵金属杠杆高、滑点跳空频繁,触价手动单和指标辅助都只是工具,实际成交概率和时机仍受市场流动性制约。
把 Chart Trade 拆成独立指标的底裤逻辑
把原本寄生在 EA 里的 Chart Trade 抽出来做指标,核心不是重写,而是断连。EA 里那套子窗口和订单显示系统,指标根本用不上,硬编进去只会在编译期炸出一大堆错误。 第一步是在 C_Chart_IDE.mqh 里用 def_INTEGRATION_CHART_TRADER 这个宏做隔离:宏只在 EA 编译链里定义,指标构建时走 #else 分支,直接继承 C_ChartFloating 而不是 C_SubWindow,子窗口系统连同 EA 订单显示彻底不进指标。 GetIdSubWinEA 这种取子窗口编号的调用散落在多处,逐处改条件编译会把代码搞成迷宫。更干净的做法是在系统头里加一段模拟代码,让指标环境下这个函数‘假装’返回主图窗口号,调用点一行不用动。 鼠标事件才是真麻烦。EA 里的 C_Mouse 类若直接塞进指标,会和 EA 抢鼠标控制权。暂时的折中是把 C_Mouse 的部分能力挪进指标的 C_TemplateChart::DispatchMessage 和 C_ChartFloating,加几行高亮代码就能骗过编译,功能表现如同鼠标类还在。 下面这段是能直接丢进 MT5 指标文件编译的最小骨架:indicator_chart_window 表明画在主图,OnInit 里只挂一个 170x240 的浮动 IDE 面板,OnCalculate 空跑返回 rates_total。编译通过后图表上就能看到原始 Chart Trade 的雏形。外汇与贵金属市场杠杆高、滑点随机,这类界面改造只解决交互,不暗示任何交易胜率。
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="macro">#include <NanoEA-SIMD\SubWindow\C_TemplateChart.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ C_TemplateChart Chart; C_Terminal Terminal; class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { Chart.AddThese("IDE(,,class="num">170, class="num">240)"); class="kw">return INIT_SUCCEEDED; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">int begin, const class="type">class="kw">double &price[]) { class="kw">return rates_total; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#ifdef def_INTEGRATION_CHART_TRADER class="macro">#include <NanoEA-SIMD\SubWindow\C_SubWindow.mqh> class="macro">#include <NanoEA-SIMD\Trade\Control\C_IndicatorTradeView.mqh> class="macro">#else class="macro">#include <NanoEA-SIMD\SubWindow\C_ChartFloating.mqh> class="macro">#include <NanoEA-SIMD\Auxiliar\C_Terminal.mqh> class="macro">#endif class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#ifdef def_INTEGRATION_CHART_TRADER class C_Chart_IDE : class="kw">public C_SubWindow class="macro">#else class C_Chart_IDE : class="kw">public C_ChartFloating class="macro">#endif { class="type">bool Create(class="type">bool bFloat) { m_CountObject = class="num">0; if ((m_fp = FileOpen("Chart Trade\\IDE.tpl", FILE_BIN | FILE_READ)) == INVALID_HANDLE) class="kw">return false;
「模板加载与子窗口归属的判定分支」
在 EA 初始化阶段,从 Chart Trade\IDE.tpl 读取二进制模板是常见做法。代码先以 FileOpen 尝试打开,若返回 INVALID_HANDLE 直接退出,说明模板缺失时不应继续构造图形对象。
读取首部 FileReadInteger(m_fp, SHORT_VALUE) 之后,用 for 循环把 eRESULT 到 eEDIT_STOP 区间内的对象名清空,等于重置上一轮挂单/止盈止损的命名状态,避免旧标签串到新图表上。
是否浮动窗口决定了 m_SubWindow 的取值:宏 def_INTEGRATION_CHART_TRADER 打开时,按 bFloat 决定用 0 还是 GetIdSubWinEA();未定义时强制为 0。想验证自己环境走哪条分支,在 MT5 里 #define def_INTEGRATION_CHART_TRADER 前后各编译一次,看子窗口 ID 是否变化即可。
鼠标移动事件里调用 Mouse.GetPositionDP(dt, p) 取价格坐标,仅在集成图表交易宏生效时编译。外汇与贵金属杠杆高,这类鼠标锚点若用于自动挂单,需先在策略测试器跑历史数据确认坐标映射无误。
class="type">bool Create(class="type">bool bFloat) { m_CountObject = class="num">0; if ((m_fp = FileOpen("Chart Trade\\IDE.tpl", FILE_BIN | FILE_READ)) == INVALID_HANDLE) class="kw">return false; FileReadInteger(m_fp, SHORT_VALUE); for (m_CountObject = eRESULT; m_CountObject <= eEDIT_STOP; m_CountObject++) m_ArrObject[m_CountObject].szName = ""; class="macro">#ifdef def_INTEGRATION_CHART_TRADER m_SubWindow = ((m_IsFloating = bFloat) ? class="num">0 : GetIdSubWinEA()); class="macro">#else m_SubWindow = class="num">0; class="macro">#endif } class="macro">#ifndef def_INTEGRATION_CHART_TRADER class="macro">#define GetIdSubWinEA() class="num">0 class="macro">#define ExistSubWin() false class="macro">#endif class="type">void DispatchMessage(class="type">int id, class="type">long lparam, class="type">class="kw">double dparam, class="type">class="kw">string sparam) { class="type">class="kw">datetime dt; class="type">class="kw">double p; C_Chart_IDE::DispatchMessage(id, lparam, dparam, sparam); class="kw">switch (id) { case CHARTEVENT_MOUSE_MOVE: class="macro">#ifdef def_INTEGRATION_CHART_TRADER Mouse.GetPositionDP(dt, p);
◍ 鼠标拖拽下的坐标与时间映射
在 MT5 图表事件里,鼠标移动消息 CHARTEVENT_MOUSE_MOVE 的 lparam、dparam 直接就是客户端坐标的 x、y 像素值,sparam 的低位比特标记左键是否按下(0x01 为按下)。若未集成 ChartTrader 类,就得用这几个参数手动换算,而不能依赖 Mouse.GetPositionXY。 #ifdef def_INTEGRATION_CHART_TRADER 分支走的是封装好的鼠标对象;#else 分支里把 lparam 强转 int 赋给 mx、dparam 强转给 my,再用 (uint)sparam & 0x01 判断按键,逻辑等价但更底层。 拿到 mx、my 后,ChartXYToTimePrice 能把像素坐标反向映射成具体时间和价格——这是画动态垂直线、挂单辅助线的关键一步。外汇与贵金属波动剧烈,这类坐标换算仅用于界面交互辅助,任何信号都只是概率倾向,实盘前请在策略测试器里跑一遍验证。
class="macro">#else { class="type">int w; ChartXYToTimePrice(Terminal.Get_ID(), (class="type">int)lparam, (class="type">int)dparam, w, dt, p); } class="macro">#endif for (class="type">int c0 = class="num">0; c0 < m_Counter; c0++) if (m_Info[c0].szVLine != "") class=class="str">"cmt">// ... Rest of the code... class="type">void DispatchMessage(class="type">int id, class="type">long lparam, class="type">class="kw">double dparam, class="type">class="kw">string sparam) { class="type">int mx, my; class="type">class="kw">datetime dt; class="type">class="kw">double p; class="kw">static class="type">int six = -class="num">1, siy = -class="num">1, sic = -class="num">1; class="kw">switch (id) { case CHARTEVENT_MOUSE_MOVE: class="macro">#ifdef def_INTEGRATION_CHART_TRADER Mouse.GetPositionXY(mx, my); if ((Mouse.GetButtonStatus() & 0x01) == class="num">1) class="macro">#else mx = (class="type">int)lparam; my = (class="type">int)dparam; if (((class="type">uint)sparam & 0x01) == class="num">1) class="macro">#endif { if (sic == -class="num">1) for (class="type">int c0 = m_MaxCounter - class="num">1; (sic < class="num">0) && (c0 >= class="num">0); c0--) class=class="str">"cmt">// ... Rest of the code...