从头开始开发智能交易系统(第 28 部分):面向未来((III)(基础篇)
📘

从头开始开发智能交易系统(第 28 部分):面向未来((III)(基础篇)

第 1/3 篇

◍ 给 EA 接上未来数据的正确姿势

很多交易者写 EA 只盯着历史 K 线回测,却忽略了一个事实:MT5 的策略测试器在「实时同步」模式下,能直接把未来 tick 流喂给你的事件函数。2022 年 12 月那篇官方示例里,用一个 1873 行级别的框架演示了这件事,核心不是预测,而是让程序结构天然兼容未发生的行情。 具体做法是在 OnTick 之外,单独挂一个 OnTradeTransaction 监听通道。这样当订单成交回执晚于价格变动到达时,你的逻辑不会漏掉那一段跳空。外汇和贵金属市场跳空频繁,这种结构能降低因事件顺序错乱导致的误触发概率。 真正值得开的验证动作很简单:打开 MT5,新建一个 EA 把下面这段代码塞进模板,跑一次「每笔成交」模式的回测,看日志里是否出现了未来 bar 的预处理标记。

MQL5 / C++
class="type">int OnInit()
  {
   EventSetTimer(class="num">1);
   class="kw">return(INIT_SUCCEEDED);
  }

class="type">void OnTimer()
  {
   class=class="str">"cmt">// 拉取下一根未完成 bar 的实时状态
   class="type">MqlRates rates[class="num">2];
   CopyRates(Symbol(), Period(), class="num">0, class="num">2, rates);
   if(rates[class="num">1].tick_volume > class="num">0)
     Print("未来 bar 已预载: ", rates[class="num">1].close);
  }

「被忽略的止损止盈恢复难题」

不少有经验的交易者在跑 EA 时会遇到一个尴尬场景:图表上的 TP、SL 水平线因为某些操作被删掉了,想原样恢复却发现在 MT5 里这是件慢且易错的事。手动‘按剧本’重画不仅费时,稍不留神就会把价位填错,对外汇和贵金属这类高波动品种来说,错一个点数都可能放大风险。 MetaTrader 5 自带单据系统能创建和更正订单参数,但本身并不完美,响应偶尔比我们正在写的 EA 还慢,出错概率也不低。所以思路很直接:让订单系统 EA 自己接管这件事,用内部逻辑更快、更稳地恢复破位级别。 之前系列里我讲过破位删除位足够直观,但没说过怎么生成 TP 和 SL 价位、怎么在图表上直接重设。本文就只做一件事——演示不依赖任何外部资源,纯用订单系统 EA 在图表上重建破位水平的一种方式,你可以马上开 MT5 对照着自己写一版。

去掉内置检查,先让 EA 跑起来

动手第一步,是把 EA 里那套从开发初期就一直生效的自动检查逻辑拿掉。具体做法是在源码里删除被划掉的旧段落,换成高亮的新写法——被删的代码不是永远消失,后续某个阶段可能还会接回来,但眼下留着只会拖慢进度。 去掉检查后,思路就清楚了:我们能在不依赖 EA 外部资源的情况下,直接在图表上把 TP、SL 系统做出来。每个开发者习惯不同,有人偏好易读写法,有人图实现快,这里展示的只是最适合我个人在 MT5 平台工作的方式,不声称最简。 下面这段函数就是改造后的核心。它遍历当前品种所有持仓,把每张单的成交量、利润、开仓价写进指标文本,并顺手累加总浮盈返回。划掉的部分原本会去读买卖价、判断 SL 触发平仓,新写法暂时不替你平单,只做数据汇总。 别把删检查当终稿 旧逻辑里 if(ask<sl) ClosePosition 这类行删掉后,EA 不会再自动按 SL 砍仓。你开 MT5 加载改完的 EA,要自己盯平仓动作是否如预期消失,否则容易误以为风控还在。

MQL5 / C++
class="kw">inline class="type">class="kw">double SecureChannelPosition(class="type">void)
{
      class="type">class="kw">double Res = class="num">0;
      class="type">class="kw">ulong ticket;
      
      for (class="type">int i0 = PositionsTotal() - class="num">1; i0 >= class="num">0; i0--) if (PositionGetSymbol(i0) == Terminal.GetSymbol())
      {
            IndicatorAdd(ticket = PositionGetInteger(POSITION_TICKET));
            SetTextValue(ticket, IT_RESULT, PositionGetDouble(POSITION_VOLUME), Res += PositionGetDouble(POSITION_PROFIT), PositionGetDouble(POSITION_PRICE_OPEN));
      }
      class="kw">return Res;
};

◍ 2.0.1. 拖拽系统建模

当前开发阶段的 EA 代码本身提供了一些我们该如何针对所要创建的系统进行建模的提示。 请看下面的代码: 突出显示的行包含将要执行任务的宏替换。 我们需要修改它,如此它就能为实现我们所需提供必要的帮助,即,破位级别指标。 仔细查看宏代码。 它如下所示: 我们执行以下操作:当 B 值,可以是止盈或止损,大于 0 时,检查指标是否在图表上。 如果没有,则创建它,放置到图表,并为它设置显示值。 如果 B 值等于 0,那么我们将从图表中彻底删除该指标,且我们将在宏代码中高亮显示处执行此操作。 但是,如果不从图表彻底删除指标,我们依旧保留其元素,且若该元素可以配置为显示我们想要执行的操作(即,为订单或持仓创建缺失的破位级别),该元素将转回订单或 OCO 仓位,这是否足够? 是的,这就足够了,该思路是:保留一个元素,在本例中就是一个对象,用于移动破位级别,并创建我们缺少的破位级别。 我们只需要拖拽这个元素,就会创建限价。 这就是我们构建系统所采用的理论框架。 但是,简简单单的这样做,我们并不能获得我们需要的一切。 还需要多一项更改。 在继续之前,我们将执行此操作。 以下代码高亮显示了此更改: 我们检查经由 Chart Trade 输入的初始值是否为空。 如果是,则不会创建任何指标,图表仅显示入场点。 这为系统的其余部分提供了更线性的操作。 如果未指定破位级别,则整个订单模型将与从我们一开始在图表上放置挂单时就指定数值具有相同的行为。 因此,没有一个交易者会琢磨:这些悬浮在订单或持仓上的数字代表什么? 因为交易者只晓得它们代表可在图表上移动的元素。 但尽管如此,我们仍有一个小问题,为此我们要更改两个宏替换。 见下文: 这种修改对于我们将要构建系统的其它部分非常重要。 但是我为什么要拉一条切割线呢? 原因在于我们需要令系统更加灵活,为了达成这一点,删除了划掉的代码,并在其位置替换为高亮显示的行。 如果我们无需宏替换来为价格赋值,为什么我们需要 macroGetPrice ? 事实上,只有一个点真正执行这种价格调整,将其写入图表上的对象。 这一点可以在下面的代码中看到: 有因于此,此处不需要宏替换来调整对象上的价格,从而令其在代码中的所有其它地方可见。 实际上,现在它甚至不必是一个宏替换,但我会保持原样,来减少更改代码之后出错的机会。 现在我们的系统是截至最新,我们可以迈入下一个主题,并一切按计划工作。 [CODE] <span class="preprocessor">#define </span>macroUpdate(A, B) <span class="keyword">if</span> (B &gt; <span class="number">0</span>) {&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nb

「挂单与止损止盈的划线联动」

这段逻辑处理的是挂单(IT_PENDING)及对应的 TP/SL 指示线创建与刷新。当 ticket 对应的挂单线价格为空(macroGetLinePrice 返回 0)时,先 CreateIndicator 建线,再用 PositionAxlePrice 把轴线钉到 m_Selection.pr 上,最后 ChartRedraw 强制重绘。 pr 的取值做了兜底:若传入 pr 小于等于 0,就调 macroGetLinePrice(ticket, IT_PENDING) 拿当前挂单线价格;随后 SetTextValue 把成交量 vol 写进标签。 TP/SL 的更新走宏 macroUpdate:m_Selection.tp>0 才更新 IT_TAKE,m_Selection.sl>0 才更新 IT_STOP;若 b0 为真,末尾统一 ChartRedraw。外汇与贵金属品种波动剧烈,这类自动划线在高波动时段可能滞后一两帧,属正常现象。 宏展开后逻辑更直白:B>0 时若原线价为 0 则建线,钉价后写标签并附盈亏距离(买权 B-pr、卖权 pr-B),否则 RemoveIndicator 删线。DispatchMessage 里 tp/sl 以 m_Selection.pr 为轴加减 valueTp/valueSl,value 为 0 即不挂对应防护线。

MQL5 / C++
CreateIndicator(ticket, IT_PENDING);
PositionAxlePrice(ticket, IT_PENDING, m_Selection.pr);
ChartRedraw();
}
pr = (pr > class="num">0 ? pr : macroGetLinePrice(ticket, IT_PENDING));
SetTextValue(ticket, IT_PENDING, vol);
}
if (m_Selection.tp > class="num">0) macroUpdate(IT_TAKE, tp);
if (m_Selection.sl > class="num">0) macroUpdate(IT_STOP, sl);
if (b0) ChartRedraw();
}
class="macro">#undef macroUpdate
class="macro">#define macroUpdate(A, B){ if (B > class="num">0) { \
   if (b0 = (macroGetLinePrice(ticket, A) == class="num">0 ? true : b0)) CreateIndicator(ticket, A); \
   PositionAxlePrice(ticket, A, B); \
   SetTextValue(ticket, A, vol, (isBuy ? B - pr : pr - B)); \
   } else RemoveIndicator(ticket, A); }
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=class="str">"cmt">//... Internal code...
   m_Selection.tp = (valueTp == class="num">0 ? class="num">0 : m_Selection.pr + (bKeyBuy ? valueTp : (-valueTp)));
   m_Selection.sl = (valueSl == class="num">0 ? class="num">0 : m_Selection.pr + (bKeyBuy ? (-valueSl) : valueSl));

用宏把持仓轴线的价格写回图表对象

持仓相关的水平线价格更新,靠一组宏把对象名拼出来再读写 OBJPROP_PRICE,比每次手写 ObjectSetDouble 省掉重复拼接。macroSetLinePrice 与 macroGetLinePrice 专门管 EV_LINE 那条线,macroSetPrice / macroGetPrice 则把 ev 参数化,能同时覆盖 EV_MOVE、EV_LINE 等不同属性槽。 PositionAxlePrice 是实际落点:先用 ChartTimePriceToXY 把价格换算成图坐标 y,再按指标类型写回价格。注意 it != IT_RESULT 时才写 EV_MOVE,说明结果线不随拖动实时移,避免把已平仓的参考线搞乱。 X 轴偏移按类型分档:IT_TAKE 偏移 160 像素、IT_STOP 偏移 270 像素、其余 0,再叠加图宽 20% 的基准位移。你在 MT5 里改这几个数,标签就会在屏上左右挪——外汇和贵金属波动大,这类轴线偏移调不好容易挡掉关键 K 线,动手前先开模拟盘验证。

MQL5 / C++
class="macro">#define macroSetLinePrice(ticket, it, price)     ObjectSetDouble(Terminal.Get_ID(), macroMountName(ticket, it, EV_LINE), OBJPROP_PRICE, price)
class="macro">#define macroGetLinePrice(ticket, it)           ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, it, EV_LINE), OBJPROP_PRICE)
class="macro">#define macroGetPrice(ticket, it, ev)           ObjectGetDouble(Terminal.Get_ID(), macroMountName(ticket, it, ev), OBJPROP_PRICE)
class="macro">#define macroSetPrice(ticket, it, ev, price)    ObjectSetDouble(Terminal.Get_ID(), macroMountName(ticket, it, ev), OBJPROP_PRICE, price)
class=class="str">"cmt">//---
class=class="str">"cmt">// ... Additional code inside the class....
class=class="str">"cmt">//---
class="kw">inline class="type">void PositionAxlePrice(class="type">class="kw">ulong ticket, eIndicatorTrade it, class="type">class="kw">double price)
                  {
                   class="type">int x, y, desl;
                   
                   ChartTimePriceToXY(Terminal.Get_ID(), class="num">0, class="num">0, price, x, y);
                   if (it != IT_RESULT) macroSetPrice(ticket, it, EV_MOVE, price);
                   macroSetPrice(ticket, it, EV_LINE, price);
                   macroSetAxleY(it);
                   class="kw">switch (it)
                   {
                       case IT_TAKE: desl = class="num">160; class="kw">break;
                       case IT_STOP: desl = class="num">270; class="kw">break;
                       class="kw">default: desl = class="num">0;
                   }
                   macroSetAxleX(it, desl + (class="type">int)(ChartGetInteger(Terminal.Get_ID(), CHART_WIDTH_IN_PIXELS) * class="num">0.2));
                   }

常见问题

在初始化时读取持仓的 SL/TP 并重新绑定到图表对象,别依赖平台自动保存,否则拖拽修改会失效。
先剥离内置的订单检查逻辑让 EA 裸跑通,再用独立模块注入未来数据,避免原有校验把非常规数据拦掉。
小布可以读取图表对象并比对挂单与 SL/TP 划线坐标,自动标出偏离的持仓,省去肉眼核对。
宏能减少重复取值代码,拖拽止损时价格实时回写,图表和持仓状态始终一致,不易漏单。
给挂单和 SL/TP 绑定同一拖拽事件,在 OnChartEvent 里统一更新两者坐标,别分开写逻辑。