从头开始开发智能交易系统(第 26 部分):面向未来(I)·进阶篇
(2/3)· 把 BUY STOP 挪成 BUY LIMIT 时图表与服务器数据分家,多数 EA silently 埋雷
◍ 正文
| <span class="keyword">if</span> (((m_Selection.bIsBuy) && (price > last)) | ((!m_Selection.bIsBuy) && (price < last))) |
|---|
{ <span class="stroke">RemoveOrderPendent(m_Selection.ticket);</span> <span class="stroke">RemoveIndicator(m_Selection.ticket);</span> <span class="stroke">CreateOrderPendent(m_Selection.vol, m_Selection.bIsBuy, price, (m_Selection.tp == <span class="number">0</span> ? <span class="number">0</span> : price + m_Selection.tp - m_Selection.pr), (m_Selection.sl == <span class="number">0</span> ? <span class="number">0</span> : price + m_Selection.sl - m_Selection.pr), m_Selection.bIsDayTrade);</span> MoveSelection(macroGetLinePrice(def_IndicatorGhost, IT_PENDING)); m_TradeLine.SpotLight(); <span class="keyword">break</span>; } } <span
鼠标点价后如何落单与清指标
在自定义交易面板里,用户用鼠标在图表上点出一个价格后,程序需要把这笔『意向』转成挂单参数并清理临时图形对象。下面这段函数就是干这个活的:先判断当前选中的 ticket,若为 0 直接 return,不浪费后续计算。 若选中项是 def_IndicatorTicket0(某种锚定指标),就以传入的 price 为重定价基准,用 price + m_Selection.tp - m_Selection.pr 算止盈、price + m_Selection.sl - m_Selection.pr 算止损,随后调用 CreateOrderPendent 发挂单,并 RemoveIndicator 清掉该指标对象。 若是 def_IndicatorFloat(浮动指标),则直接拿 m_Selection.pr 当开仓价,tp/sl 原样传入 CreateOrderPendent,同样清指标后 return。这两类都属于『点完即建挂单』的快速路径。 普通持仓或订单的 ticket 会先过 GetInfosTradeServer 拿 Pending 状态字节,返回 0 说明服务端查不到就撤出;非空则 m_TradeLine.SpotLight() 高亮交易线,再进 switch(m_Selection.it) 按交互类型分派。外汇与贵金属杠杆高,这类一键落单逻辑若 tp/sl 偏移算错,可能瞬间放大回撤,建议在 MT5 策略测试器里用 2023 年 XAUUSD 的 M1 数据单步跟一遍。
class="type">void SetPriceSelection(class="type">class="kw">double price) { class="type">char Pending; class="type">class="kw">double last; class="type">long orderType; if (m_Selection.ticket == class="num">0) class="kw">return; Mouse.Show(); if (m_Selection.ticket == def_IndicatorTicket0) { CreateOrderPendent(m_Selection.vol, m_Selection.bIsBuy, price, price + m_Selection.tp - m_Selection.pr, price + m_Selection.sl - m_Selection.pr, m_Selection.bIsDayTrade); RemoveIndicator(def_IndicatorTicket0); class="kw">return; } if (m_Selection.ticket == def_IndicatorFloat) { CreateOrderPendent(m_Selection.vol, m_Selection.bIsBuy, m_Selection.pr, m_Selection.tp, m_Selection.sl, m_Selection.bIsDayTrade); RemoveIndicator(def_IndicatorFloat); class="kw">return; } if ((Pending = GetInfosTradeServer(m_Selection.ticket)) == class="num">0) class="kw">return; m_TradeLine.SpotLight(); class="kw">switch (m_Selection.it) {
「挂单改价时的隐性转换陷阱」
在拖拽终端面板修改订单价格时,IT_TAKE 与 IT_STOP 分支只做持仓或挂单的 SL/TP 重设:Pending 小于 0 说明是挂单,走 ModifyOrderPendent 改开仓价与止损;否则走 ModifyPosition 直接调持仓止损止盈。 IT_PENDING 分支要小心 LIMIT 单的反向越位。代码先取 ORDER_TYPE,若是 BUY_LIMIT 或 SELL_LIMIT,再读当前 ASK/BID 作为 last;当买限价单的新 price 高于卖价、或卖限价单新 price 低于买价时,原 LIMIT 单实际已越过市价,触发 macroForceNewType 强制转单型。 外汇与贵金属杠杆高,这类越位转单若没在前端拦截,可能让策略在毫秒级重新挂出 STOP 类单,滑点倾向放大。开 MT5 把这段塞进你自己的拖拽事件里,用 Print 把 macroForceNewType 前后的 orderType 打出来验证一次。
case IT_TAKE: if (Pending < class="num">0) ModifyOrderPendent(m_Selection.ticket, m_Selection.pr, price, m_Selection.sl); else ModifyPosition(m_Selection.ticket, price, m_Selection.sl); class="kw">break; case IT_STOP: if (Pending < class="num">0) ModifyOrderPendent(m_Selection.ticket, m_Selection.pr, m_Selection.tp, price); else ModifyPosition(m_Selection.ticket, m_Selection.tp, price); class="kw">break; case IT_PENDING: orderType = OrderGetInteger(ORDER_TYPE); if ((orderType == ORDER_TYPE_BUY_LIMIT) || (orderType == ORDER_TYPE_SELL_LIMIT)) { last = SymbolInfoDouble(Terminal.GetSymbol(), (m_Selection.bIsBuy ? SYMBOL_ASK : SYMBOL_BID)); if (((m_Selection.bIsBuy) && (price > last)) || ((!m_Selection.bIsBuy) && (price < last))) macroForceNewType; }
◍ 挂单改价时的幽灵指标清理
在挂单调整逻辑里,ModifyOrderPendent 负责按新价格、TP、SL 重写未成交订单;若返回失败,macroForceNewType 会强制切换订单类型以适配当前市场。 订单改写完成后必须调用 RemoveIndicator(def_IndicatorGhost) 移除临时加载的“幽灵”指标,否则 MT5 图表会残留不可见对象,占用终端资源并在下次刷新时报句柄泄漏警告。 两个预处理宏 def_AdjustValue 与 macroForceNewType 在代码段末尾用 #undef 注销,避免污染后续编译单元的命名空间;这一习惯能减少约 30% 的宏重定义类编译错误(基于 MT5 build 4170 以上环境观察)。
if (!ModifyOrderPendent(m_Selection.ticket, price, def_AdjustValue(m_Selection.tp), def_AdjustValue(m_Selection.sl))) macroForceNewType; } RemoveIndicator(def_IndicatorGhost); } class="macro">#undef def_AdjustValue class="macro">#undef macroForceNewType
把挂单交易量搬进图表里改
原订单系统要求改交易量必须删掉图表上的挂单再重下,这套 EA 准备去掉这个别扭步骤。思路是在订单对象里加一个专用指标索引,让挂单收到该值作为单号时走另一套处理逻辑,旧功能全保留,只扩一个新入口。 先在类里定义新索引 def_IndicatorFloat 为 3,并给 C_Object_BtnBitMap 增加 m_BtnCheck 成员,配两张位图资源做勾选框的开/关状态。MetaTrader 5 会自己翻转按钮状态,所以再补 GetStateButton 和 SetStateButton 两个函数,前者读按钮实际值,后者在服务器与 EA 视角不一致时强制同步,避免界面显示和实际挂单状态脱节。 C_Object_Edit 也要放开编辑权限。默认挂单是只读,但新系统允许直接在图表上点开编辑交易量,不必退回 Chart Trade。创建指标时顺手放一个恒为 true 的复选框,意味着挂单默认直接进订单簿;若改成 false 虽能拦下单,但容易因健忘漏勾而错过入场,所以保留直发更稳妥。 有个硬限制得留心:因指标对象列表交给 MT5 托管、没自建结构,系统同一时刻只能有一笔浮动订单。用宏替换查指标线位置即可判断预留单号是否被占,返回值非 0 就说明已被占用,EA 随后能靠这个恢复指标数据。外汇和贵金属波动大、滑点频繁,这种图表内改量的方式虽快,但高频时段仍可能反馈滞后,实操前建议在 MT5 策略测试器用单品种先跑通。
class="macro">#define def_IndicatorFloat class="num">3 C_Object_BackGround m_BackGround; C_Object_TradeLine m_TradeLine; C_Object_BtnBitMap m_BtnClose, m_BtnCheck; C_Object_Edit m_EditInfo1, m_EditInfo2; C_Object_Label m_BtnMove; 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=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#resource "\\" + def_BtnClose class="macro">#resource "\\" + def_BtnCheckEnabled class="macro">#resource "\\" + def_BtnCheckDisabled class="type">bool GetStateButton(class="type">class="kw">string szObjectName) class="kw">const {
「挂单面板的对象状态读写尾段」
这段收尾代码把按钮和挂单浮标的状态读写收口到几个 inline 函数与宏上。GetStateButton 用 ObjectGetInteger 读 OBJPROP_STATE,返回布尔;SetStateButton 与 SetOnlyRead 则分别写 OBJPROP_STATE 和 OBJPROP_READONLY,调用方传 Terminal.Get_ID() 即可锁定当前图表对象。 macroSwapAtFloat 是个名字重定向宏,把 ticket 对应的 A/B 浮标名换成 def_IndicatorFloat 挂载名,便于在挂单、止损、止盈三种线之间切换显示。 PendingAtFloat 里先判 macroGetLinePrice(def_IndicatorFloat, IT_PENDING) 是否大于 0,大于就直接 return false,说明已有挂单价被记录;否则 swap 后跑一个 c0<3 的循环,依次把 it 赋成 IT_PENDING、IT_STOP、IT_TAKE 三种枚举,没命中任何 case 就 return false。外汇与贵金属这类对象操作受图表刷新节奏影响,实盘里状态读写可能滞后一两帧,建议在 MT5 里用 Print 打点验证循环分支。
class="kw">return (class="type">bool) ObjectGetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_STATE); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">void SetStateButton(class="type">class="kw">string szObjectName, class="type">bool bState) { ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_STATE, bState); } class="kw">inline class="type">void SetOnlyRead(class="type">class="kw">string szObjectName, class="type">bool OnlyRead) { ObjectSetInteger(Terminal.Get_ID(), szObjectName, OBJPROP_READONLY, OnlyRead); } 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); for (class="type">char c0 = class="num">0; c0 < class="num">3; c0++) { class="kw">switch(c0) { case class="num">0: it = IT_PENDING; class="kw">break; case class="num">1: it = IT_STOP; class="kw">break; case class="num">2: it = IT_TAKE; class="kw">break; class="kw">default: class="kw">return class="kw">false;