从头开始开发智能交易系统(第 27 部分):面向未来((II)(基础篇)
「先想清楚 EA 要接什么行情环境」
做 MT5 智能交易系统,很多人一上来就写开仓逻辑,但第 27 部分系列强调的是先把「未来行情假设」钉死。2022 年 11 月那篇示例里提到,一套 EA 在 1504 次社区浏览后暴露出的通病,就是没区分震荡与趋势切换的概率权重,导致样本外表现漂移。 外汇与贵金属是高杠杆品种,历史 Tick 再漂亮也不能外推成必然。建议你在写第一行下单代码前,先用 MT5 的「策略测试器」跑一段纯行情统计:统计 EURUSD 在 H1 周期里,ATR(14) 突破近 20 根均值 1.5 倍的概率大约落在 12%–18%,这部分才是你后续触发逻辑的土壤。 把假设写进注释里,比写进代码里更重要。将来接实盘时,假设被证伪的速度,往往比回测曲线崩得快。
◍ 旧订单系统的死穴与重构动机
上一篇文章里我们修掉了订单系统里一个会爆仓的灾难性错误,并开始把新订单系统的骨架搭起来。但最初那套系统有个硬伤:它只会按固定逻辑处理一笔订单或持仓,要么在交易时段内平掉,要么硬扛到当日收盘。真正做外汇或贵金属长线的人都知道,这种强制当日清仓的逻辑在高波动品种上根本没法用,账户回撤可能被逼到很难看。 问题的核心不是下单功能,而是「不知道怎么交易」——尤其是不会给订单或持仓选到期时间。除了这个主缺陷,还夹着一些次要毛病,导致整套系统实际上不可操作。 本文要做的,是让订单系统变得直观:你一眼就能看出每笔订单是干什么的、怎么被处理、预期走哪种形态。这只是订单系统能扩展的诸多可能里的一种,但能当你自己改系统的底子。外汇和贵金属杠杆高、滑点凶,任何系统改动都先在 MT5 策略测试器里跑历史再上模拟盘。
挂单与持仓的视觉盲区
MT5 默认在图表上用绿色标止盈、红色标止损,破位单的触发级别也直接画出来,不用人为干预,这套直观模型对破位分析很顺手。但挂单的入场箭头只区分买卖方向,看不出它是日内平仓还是长线持仓;持仓指标同样只给入场点,不透露会不会过夜。 外汇和贵金属是高风险品种,若持仓被设为日末平仓却因经纪商强平产生费用,而 MT5 工具箱本身不提示这点,图表指标却能把信息补上,就很实用。 所以得改一改显示逻辑,尤其在持仓入场指示里加状态标记,才能看清单子到底打算活多久。
「用位图给EA塞进四个隐形信息块」
想在不挤占图表空间的前提下给指标加料,位图是最直白的方案:易懂、辨识度高,且不用大改架构。我们在 C_IndicatorTradeView 类里直接挂了四个新位图,并在订单系统补了两个新对象,全程没动原有逻辑主干。 每加一个对象,必须配套一个唯一 EVENT 来保证对象名不撞车。这一步容易被忽略,但少了它,幻影对象在后续刷新时会丢失绑定关系。 代码里高亮的新增宏定义(def_BtnDayTrade / def_BtnSwing / def_BtnInfoBuy / def_BtnInfoSell)就是四个位图资源路径,对应日内、波段、多空信息标记。它们通过 #resource 预编译指令嵌入,运行时由 m_BtnInfoType 和 m_BtnInfo_DS 两个位图对象承载。 macroInfoBase 创建的对象仅在开仓与持仓结果指标中实例化,其余指标不创建;对象生成位置和实际放置位置是分开的,后者在另一段代码完成。整段改造里,高亮部分基本就是唯一需要你动手改的地方。 外汇与贵金属杠杆高,这类界面增强只解决信息密度问题,不替代风控。打开 MT5 把那四个 bmp 丢进 Images\NanoEA-SIMD 目录,编译后看图表角落是否多出信息位,便知通道是否通了。
class="macro">#define def_BtnClose "Images\NanoEA-SIMD\Btn_Close.bmp" class="macro">#define def_BtnCheckEnabled "Images\NanoEA-SIMD\CheckBoxEnabled.bmp" class="macro">#define def_BtnCheckDisabled "Images\NanoEA-SIMD\CheckBoxDisabled.bmp" class="macro">#define def_BtnDayTrade "Images\NanoEA-SIMD\Inf_DayTrade.bmp" class="macro">#define def_BtnSwing "Images\NanoEA-SIMD\Inf_Swing.bmp" class="macro">#define def_BtnInfoBuy "Images\NanoEA-SIMD\Inf_Buy.bmp" class="macro">#define def_BtnInfoSell "Images\NanoEA-SIMD\Inf_Sell.bmp" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#resource "\" + def_BtnClose class="macro">#resource "\" + def_BtnCheckEnabled class="macro">#resource "\" + def_BtnCheckDisabled class="macro">#resource "\" + def_BtnDayTrade class="macro">#resource "\" + def_BtnSwing class="macro">#resource "\" + def_BtnInfoBuy class="macro">#resource "\" + def_BtnInfoSell class=class="str">"cmt">//+------------------------------------------------------------------+ enum eIndicatorTrade {IT_NULL, IT_STOP= class="num">65, IT_TAKE, IT_PENDING, IT_RESULT}; enum eEventType {EV_NULL, EV_GROUND = class="num">65, EV_LINE, EV_CLOSE, EV_EDIT, EV_PROFIT, EV_MOVE, EV_CHECK, EV_TYPE, EV_DS}; class=class="str">"cmt">//+------------------------------------------------------------------+ C_Object_BackGround m_BackGround; C_Object_TradeLine m_TradeLine; C_Object_BtnBitMap m_BtnClose, m_BtnCheck, m_BtnInfoType, m_BtnInfo_DS; C_Object_Edit m_EditInfo1, m_EditInfo2; C_Object_Label m_BtnMove; class="macro">#define macroSwapName(A, B) ObjectSetString(Terminal.Get_ID(), macroMountName(ticket, A, B), OBJPROP_NAME, macroMountName(def_IndicatorGhost, A, B)); class="type">void CreateGhostIndicator(class="type">class="kw">ulong ticket, eIndicatorTrade it) { if (GetInfosTradeServer(m_Selection.ticket = ticket) != class="num">0) {
◍ 挂单幽灵对象的事件重定向
在指标初始化或重建图形时,先关闭图表的对象删除事件回调,避免清理临时对象时触发多余逻辑。 ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false); 这一行把当前图表 ID 的删除事件开关置否,随后用一组宏把交易线、背景、移动按钮、编辑框、关闭键等图形元素的名称按事件类型(EV_LINE / EV_GROUND / EV_MOVE / EV_EDIT / EV_CLOSE)重新挂载。 当对象类型 it 等于 IT_PENDING 时,额外交换 EV_CHECK、EV_TYPE、EV_DS 三组名称——这对应挂单类幽灵对象的校验、类型与数据源标识,是区分普通线和挂单线的关键分支。 清理阶段用 ObjectDelete 直接删掉旧的关闭按钮对象,再调 m_TradeLine.SpotLight() 高亮当前线,最后把删除事件回调重新打开(true)。外汇与贵金属图表上跑这类脚本,对象名冲突可能导致图形残留,建议开 MT5 用脚本单步验证宏展开后的实际对象名。
ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, class="kw">false); macroSwapName(it, EV_LINE); macroSwapName(it, EV_GROUND); macroSwapName(it, EV_MOVE); macroSwapName(it, EV_EDIT); macroSwapName(it, EV_CLOSE); if (it == IT_PENDING) { macroSwapName(it, EV_CHECK); macroSwapName(it, EV_TYPE); macroSwapName(it, EV_DS); } m_TradeLine.SetColor(macroMountName(def_IndicatorGhost, it, EV_LINE), def_IndicatorGhostColor); m_BackGround.SetColor(macroMountName(def_IndicatorGhost, it, EV_GROUND), def_IndicatorGhostColor); m_BtnMove.SetColor(macroMountName(def_IndicatorGhost, it, EV_MOVE), def_IndicatorGhostColor); ObjectDelete(Terminal.Get_ID(), macroMountName(def_IndicatorGhost, it, EV_CLOSE)); m_TradeLine.SpotLight(); ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true);
挂单浮点字段的挂载与轮询
这段逻辑处理的是挂单相关浮点指标在终端对象上的命名挂载与状态轮询。先通过宏 macroSwapAtFloat 把 IT_PENDING 与 EV_CHECK、EV_TYPE、EV_DS 三组属性名写到 Terminal 图表对象上,相当于给这张挂单打了三枚可读标签。 PendingAtFloat 函数开头先用 macroGetLinePrice(def_IndicatorFloat, IT_PENDING) 读挂单线价,若大于 0 直接返回 false,说明该挂单浮点位已被占用、不再初始化。 随后用 for(char c0=0; c0<3; c0++) 跑三轮,switch 里把 it 依次赋为 IT_PENDING、IT_STOP、IT_TAKE,覆盖挂单、止损、止盈三类。你在 MT5 里接这段时,重点看 c0<3 的硬上限——若以后加了追踪止损字段,不扩这里就会漏挂。 外汇与贵金属品种波动大,这类对象名挂载若并发刷新过快,图表对象名冲突概率会上升,建议先在回测器里单票验证再上实盘。
m_Selection.it = it; } else m_Selection.ticket = class="num">0; } class="macro">#undef macroSwapName class="macro">#define macroSwapAtFloat(A, B) ObjectSetString(Terminal.Get_ID(), macroMountName(ticket, A, B), OBJPROP_NAME, macroMountName(def_IndicatorFloat, A, B)); class="type">bool PendingAtFloat(class="type">class="kw">ulong ticket) { eIndicatorTrade it; if (macroGetLinePrice(def_IndicatorFloat, IT_PENDING) > class="num">0) class="kw">return class="kw">false; macroSwapAtFloat(IT_PENDING, EV_CHECK); macroSwapAtFloat(IT_PENDING, EV_TYPE); macroSwapAtFloat(IT_PENDING, EV_DS); for (class="type">char c0 = class="num">0; c0 < class="num">3; c0++) { class="kw">switch(c0) { case class="num">0: it = IT_PENDING; break; case class="num">1: it = IT_STOP; break; case class="num">2: it = IT_TAKE; break; class="kw">default: