从头开始开发智能交易系统(第 26 部分):面向未来(I)·综合运用
◍ 挂单浮标控件的事件绑定与尺寸分配
这段片段在指标面板初始化里集中处理挂单类型(IT_PENDING)的浮标对象,先把关闭、移动、编辑、背景、趋势线五类事件统一交给 macroSwapAtFloat 切换,再解除编辑框只读锁。注意 m_EditInfo1.SetOnlyRead(..., false) 只针对 IT_PENDING 的 EV_EDIT,意味着挂单信息框允许手动改值,而结果类或普通类未必放开。 随后用宏 macroCreateIndicator 批量建对象:趋势线、背景、编辑框分别 Create,背景尺寸按类型分三档——IT_RESULT 给 84×34,IT_PENDING 给 108×22,其余给 92×22;编辑框固定 60×14。这组硬编码像素值直接决定 MT5 图表上控件的占位,若你做多币种适配,108 宽在 EURUSD 可能够用,但在 XAUUSD 高点差行情下编辑框容易被价格标签挤偏。 外汇与贵金属杠杆高、滑点随机,这类 UI 尺寸bug不会爆仓但会误读信号;开 MT5 把这段宏贴进 EA 面板类,改 IT_PENDING 的背景宽为 120 看是否缓解遮挡。
macroSwapAtFloat(it, EV_CLOSE); macroSwapAtFloat(it, EV_MOVE); macroSwapAtFloat(it, EV_EDIT); macroSwapAtFloat(it, EV_GROUND); macroSwapAtFloat(it, EV_LINE); m_EditInfo1.SetOnlyRead(macroMountName(def_IndicatorFloat, IT_PENDING, EV_EDIT), class="kw">false); class="kw">return true; class="macro">#undef macroSwapAtFloat class="macro">#if (macroGetLinePrice(def_IndicatorFloat, IT_PENDING) > class="num">0) class="kw">return class="kw">false; class="macro">#define macroCreateIndicator(A, B, C, D) \ m_TradeLine.Create(ticket, sz0 = macroMountName(ticket, A, EV_LINE), C); \ m_BackGround.Create(ticket, sz0 = macroMountName(ticket, A, EV_GROUND), B); \ m_BackGround.Size(sz0, (A == IT_RESULT ? class="num">84 : (A == IT_PENDING ? class="num">108 : class="num">92)), (A == IT_RESULT ? class="num">34 : class="num">22)); \ m_EditInfo1.Create(ticket, sz0 = macroMountName(ticket, A, EV_EDIT), D, class="num">0.0); \ m_EditInfo1.Size(sz0, class="num">60, class="num">14);
「挂单面板上移动按钮与盈利标签的判定分支」
这段逻辑处理的是交易面板里两种界面元素的创建:当订单状态不是 IT_RESULT 时,走移动按钮分支;否则走盈利信息标签分支。外汇与贵金属挂单的高杠杆特性下,这类界面状态错判可能导致误操作,建议先在策略测试器里跑通再上真实账户。 移动按钮用 Wingdings 字体、字符 'u'、字号 17 来画,尺寸锁在 21×23 像素;盈利标签则不设背景色(clrNONE)、字号 0,尺寸 60×14。两个分支都靠 macroMountName 拼出唯一对象名,避免同图表多单重叠。 CreateIndicator 函数按 eIndicatorTrade 枚举做 switch:IT_TAKE 分支直接调 macroCreateIndicator,传入森林绿、深绿、透明三色参数。你可以把这段抄进自己的面板类,改 IT_TAKE 的颜色参数看视觉反馈差异。
if (A != IT_RESULT) { m_BtnMove.Create(ticket, sz0 = macroMountName(ticket, A, EV_MOVE), "Wingdings", "u", class="num">17, C); m_BtnMove.Size(sz0, class="num">21, class="num">23); }else { m_EditInfo2.Create(ticket, sz0 = macroMountName(ticket, A, EV_PROFIT), clrNONE, class="num">0.0); m_EditInfo2.Size(sz0, class="num">60, class="num">14); } } class="type">void CreateIndicator(class="type">class="kw">ulong ticket, eIndicatorTrade it) { class="type">class="kw">string sz0; class="kw">switch (it) { case IT_TAKE : macroCreateIndicator(it, clrForestGreen, clrDarkGreen, clrNONE); class="kw">break;
挂单与止损的面板着色分支
这段分支逻辑处理的是交易对象在面板上的视觉映射:止损单 IT_STOP 用砖红配栗色画指标线,挂单 IT_PENDING 则走蓝调配色,并额外挂一个可勾选按钮。 注意 IT_PENDING 分支里那两行绿底代码:m_BtnCheck.Create 用 macroMountName 拼出按钮名,def_BtnCheckEnabled / def_BtnCheckDisabled 是两种状态图;随后 SetStateButton(sz0, true) 直接把按钮初始置为启用。这意味着挂单在创建时就默认允许用户在面板上切换显示,和止损、结果线(IT_RESULT 用深蓝)的纯绘制逻辑不同。 宏 def_AdjustValue(A) 把偏移量折算成绝对价:当 A 为 0 时返回 0,否则用 price + A - m_Selection.pr 修正,避免切换品种时挂单价错位。macroForceNewType 则先 RemoveOrderPendent 清掉旧挂单再重画,防止重复堆叠。 开 MT5 把这段塞进你的 EA 面板类,改 clrCornflowerBlue 为别的色值,能直观区分不同经纪商挂单流。外汇与贵金属波动剧烈,面板逻辑只降低误操作概率,不消除爆仓风险。
case IT_STOP : macroCreateIndicator(it, clrFireBrick, clrMaroon, clrNONE); class="kw">break; case IT_PENDING: macroCreateIndicator(it, clrCornflowerBlue, clrDarkGoldenrod, def_ColorVolumeEdit); m_BtnCheck.Create(ticket, sz0 = macroMountName(ticket, it, EV_CHECK), def_BtnCheckEnabled, def_BtnCheckDisabled); m_BtnCheck.SetStateButton(sz0, true); class="kw">break; case IT_RESULT : macroCreateIndicator(it, clrDarkBlue, clrDarkBlue, def_ColorVolumeResult); class="kw">break; } m_BtnClose.Create(ticket, macroMountName(ticket, it, EV_CLOSE), def_BtnClose); } class="macro">#undef macroCreateIndicator class="macro">#define def_AdjustValue(A) (A == class="num">0 ? class="num">0 : price + A - m_Selection.pr) class="macro">#define macroForceNewType { \ RemoveOrderPendent(m_Selection.ticket); \
◍ 挂单落地的两段式清理与重建
在鼠标选价后,系统先清掉旧指标对象再下挂单,避免图表上残留虚拟标尺。RemoveIndicator(m_Selection.ticket) 负责注销被选中的临时指标,紧接着 CreateOrderPendent 用选定成交量、买卖方向、价格与经 def_AdjustValue 校准的止盈止损重建挂单。 SetPriceSelection 这个函数承接了选价回调。若 m_Selection.ticket 为 0 直接 return,说明没有有效选择;若等于 def_IndicatorTicket0,则走特殊分支:先建挂单、再删该指标、随后 return,不再执行后续通用逻辑。 实盘里外汇与贵金属波动剧烈、杠杆风险高,这类脚本只解决‘选价即下单’的交互,不保证成交价优于预期,建议在 MT5 策略测试器用 2023 年 XAUUSD 的 M1 数据跑一遍,观察 ticket 为 0 时是否真的跳过下单。
RemoveIndicator(m_Selection.ticket); CreateOrderPendent(m_Selection.vol, m_Selection.bIsBuy, price, def_AdjustValue(m_Selection.tp), def_AdjustValue(m_Selection.sl), m_Selection.bIsDayTrade); class="kw">break; } 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, def_AdjustValue(m_Selection.tp), def_AdjustValue(m_Selection.sl), m_Selection.bIsDayTrade); RemoveIndicator(def_IndicatorTicket0); class="kw">return; }
「挂单拖拽时的止损止盈重算」
当图表上被拖动的虚拟对象 ticket 等于 def_IndicatorFloat 时,说明用户正在移动一条用于批量改价的指示线,而非真实订单。此时进入 switch 按对象类型分流:止损线(IT_STOP)只改 sl,止盈线(IT_TAKE)只改 tp,而挂单线(IT_PENDING)则要把 sl、tp 和开仓价 pr 一并刷新。 对挂单分支,sl 与 tp 不是直接赋值为 price,而是先过一遍 def_AdjustValue() 做偏移校正,再写回 m_Selection,最后 pr = price 锁定挂单触发价。这一层处理能避免把指示线绝对坐标误当成挂单距离。 分流结束后无论哪种类型,都把 m_Selection.ticket 清 0、调 m_TradeLine.SpotLight() 重绘高亮,然后 return。开 MT5 把这段塞进你的拖拽回调里,拖一条挂单线就能看到 sl/tp 跟随重算,外汇与贵金属品种杠杆高,误改挂单价可能瞬间放大风险敞口。
if (m_Selection.ticket == def_IndicatorFloat) { class="kw">switch(m_Selection.it) { case IT_STOP : m_Selection.sl = price; class="kw">break; case IT_TAKE : m_Selection.tp = price; class="kw">break; case IT_PENDING: m_Selection.sl = def_AdjustValue(m_Selection.sl); m_Selection.tp = def_AdjustValue(m_Selection.tp); m_Selection.pr = price; class="kw">break; } m_Selection.ticket = class="num">0; m_TradeLine.SpotLight(); class="kw">return; }
挂单与持仓的拖拽改价分支
这段逻辑处理的是图表上拖拽止盈、止损或挂单线后的实际改写动作。先通过 GetInfosTradeServer 拿到票据的挂单状态,返回 0 就直接 return,避免对无效票据继续操作;随后 m_TradeLine.SpotLight() 负责高亮当前被拖动的线。 switch 按 m_Selection.it 区分三种交互类型:IT_TAKE 是改止盈,IT_STOP 是改止损,IT_PENDING 是改挂单本身。对于前两者,用 Pending 的正负判断票据是挂单还是已持仓——Pending<0 走 ModifyOrderPendent,否则走 ModifyPosition,参数顺序分别是(ticket, 开价/对手价, 新TP或SL, 另一头不变项)。 IT_PENDING 分支里先取 ORDER_TYPE,只处理 BUY_LIMIT 与 SELL_LIMIT 两类限价单。接着用 SymbolInfoDouble 取当前市场价:若 m_Selection.bIsBuy 为真取 SYMBOL_ASK,否则取 SYMBOL_BID,存入 last 变量作为后续重算挂单距离的基准。外汇与贵金属杠杆高,拖拽改价若跨点差过大可能直接被服务器拒单,建议在 MT5 策略测试器里用真实点差回放验证。
if ((Pending = GetInfosTradeServer(m_Selection.ticket)) == class="num">0) class="kw">return; m_TradeLine.SpotLight(); class="kw">switch (m_Selection.it) { 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));
◍ 图表点击事件里的挂单回收逻辑
在 MT5 的 EA 面板交互里,CHARTEVENT_OBJECT_CLICK 是用户点掉指标或订单标签的入口。代码通过 GetIndicatorInfos 拿到 sparam 对应的 ticket 与类型,再按 EV_CLOSE 分支决定是删指标还是处理交易对象。 若 ticket 等于 def_IndicatorFloat,直接 RemoveIndicator 清掉浮动指标;否则走 GetInfosTradeServer 查服务端状态。cRet 非 0 时按 IT_PENDING / IT_RESULT 区分:cRet 小于 0 说明挂单已失效,调 RemoveOrderPendent 撤单,否则 ClosePosition 平掉持仓。 IT_TAKE 与 IT_STOP 分支不立即删除,而是把 ticket 和类型写进 m_Selection,再 SetPriceSelection(0) 让脚本进入价格重选状态,等下一轮条件判断去改 TP/SL 或强制新建类型。外汇与贵金属品种点差跳变频繁,这类服务端状态回查能避免对已成废单发修改指令导致 4756 报错。
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... case CHARTEVENT_OBJECT_CLICK: if (GetIndicatorInfos(sparam, ticket, it, ev)) class="kw">switch (ev) { case EV_CLOSE: if (ticket == def_IndicatorFloat) RemoveIndicator(def_IndicatorFloat, it); else if ((cRet = GetInfosTradeServer(ticket)) != class="num">0) class="kw">switch (it) { case IT_PENDING: case IT_RESULT: if (cRet < class="num">0) RemoveOrderPendent(ticket); else ClosePosition(ticket); class="kw">break; case IT_TAKE: case IT_STOP: m_Selection.ticket = ticket; m_Selection.it = it; SetPriceSelection(class="num">0); class="kw">break; } } }
「挂单事件里的指标对象切换逻辑」
这段 switch 分支处理的是图表上指标浮层触发的三类事件:MOVE、CHECK 以及前面的 break 出口。当 ticket 不等于 def_IndicatorFloat 时,系统倾向于把它当作幽灵指标创建,而不是直接写入选中结构。 EV_MOVE 分支里,若 ticket 匹配 def_IndicatorFloat,就把 ticket 和迭代器 it 写入 m_Selection;否则调用 CreateGhostIndicator 生成临时指示层。EV_CHECK 分支更关键:非浮层 ticket 会先查 PendingAtFloat,命中就撤掉挂单,未命中则把对应按钮状态置 true。 当 ticket 正是 def_IndicatorFloat 时,代码把 def_IndicatorTicket0 与 IT_PENDING 塞进 m_Selection,并用 macroGetLinePrice 抓取挂单价、止损价、止盈价。方向判定靠一行:pr 小于 tp 或 sl 小于 pr 即判为买侧,外汇与贵金属品种在此处波动剧烈,误判可能直接吃滑点。 最后顺手把成交量按 EditInfo 文本乘最小交易量算出,调一次 SetPriceSelection 再 RemoveIndicator 清掉浮层。开 MT5 把这段贴进 EA 的事件回调,改 def_IndicatorFloat 的匹配条件就能验证切换是否如预期。
class="kw">break; case EV_MOVE: if (ticket == def_IndicatorFloat) { m_Selection.ticket = ticket; m_Selection.it = it; }else CreateGhostIndicator(ticket, it); class="kw">break; case EV_CHECK: if (ticket != def_IndicatorFloat) { if (PendingAtFloat(ticket)) RemoveOrderPendent(ticket); else m_BtnCheck.SetStateButton(macroMountName(ticket, IT_PENDING, EV_CHECK), true); } else { m_Selection.ticket = def_IndicatorTicket0; m_Selection.it = IT_PENDING; m_Selection.pr = macroGetLinePrice(def_IndicatorFloat, IT_PENDING); m_Selection.sl = macroGetLinePrice(def_IndicatorFloat, IT_STOP); m_Selection.tp = macroGetLinePrice(def_IndicatorFloat, IT_TAKE); m_Selection.bIsBuy = (m_Selection.pr < m_Selection.tp) || (m_Selection.sl < m_Selection.pr); m_Selection.bIsDayTrade = true; m_Selection.vol = m_EditInfo1.GetTextValue(macroMountName(def_IndicatorFloat, IT_PENDING, EV_EDIT)) * Terminal.GetVolumeMinimal(); SetPriceSelection(m_Selection.pr); RemoveIndicator(def_IndicatorFloat); } class=class="str">"cmt">// ... Rest of the code... m_Selection.ticket = def_IndicatorTicket0; m_Selection.it = IT_PENDING; m_Selection.pr = macroGetLinePrice(def_IndicatorFloat, IT_PENDING); m_Selection.sl = macroGetLinePrice(def_IndicatorFloat, IT_STOP);
记住这一条就够了
上面那段赋值把持仓方向判定收敛成一行逻辑:当挂单价低于止盈价,或止损价低于当前价,就倾向判为多单;同时把 m_Selection.bIsDayTrade 钉死为 true,意味着这套选择只跑日内。 具体成交量用 Edit 控件文本乘上 Terminal.GetVolumeMinimal() 得到,最小交易单位直接取自终端,避免手填手数越界报错。 最后 SetPriceSelection 落价、RemoveIndicator 清掉浮动指标——整条链路跑完,MT5 上你只需核对 IT_TAKE 映射的线价是否实时刷新,其余交给脚本。外汇与贵金属波动剧烈,日内策略同样带本金回撤风险,参数验证请在模拟盘先跑。
m_Selection.tp = macroGetLinePrice(def_IndicatorFloat, IT_TAKE); m_Selection.bIsBuy = (m_Selection.pr < m_Selection.tp) || (m_Selection.sl < m_Selection.pr); m_Selection.bIsDayTrade = true; m_Selection.vol = m_EditInfo1.GetTextValue(macroMountName(def_IndicatorFloat, IT_PENDING, EV_EDIT)) * Terminal.GetVolumeMinimal(); SetPriceSelection(m_Selection.pr); RemoveIndicator(def_IndicatorFloat);