从头开始开发智能交易系统(第 22 部分):新订单系统 (V)·综合运用
(3/3)· 当 EA 在 WDO/WIN 之外资产上失灵,多半是合约定义与移动订单逻辑埋了雷
很多交易者以为图表上拖单只是前端小花招,真到跨品种跑 EA 才发现平仓算量和保证金推算全偏了。根源常在合约规格没吃透,代码里还堆着复制粘贴的幽灵片段。本篇顺着前两步把这套开源订单系统的最后拼图补上。
「成交回执与图表事件的分发落点」
这段逻辑处在订单事务回调的末端,针对挂单成交与持仓变动两类事件,分别把价格、止损止盈、成交量与方向喂给 TradeView.UpdateInfosIndicators。注意挂单分支用 def_IsBuy(trans.order_type) 推断买卖方向,而持仓分支直接用 trans.deal_type == DEAL_TYPE_BUY 判断,两者来源不同,改写指标时别混用。 图表事件入口 OnChartEvent 里,先由 Mouse.DispatchMessage 接鼠标消息,再对 CHARTEVENT_CHART_CHANGE 做三处 resize:Terminal、WallPaper、TimesAndTrade。实测在 1920×1080 屏拖动图表边框时,这三行会在每帧触发一次,若漏掉任一处,浮层会残留旧坐标。 末尾的 Chart.DispatchMessage、VolumeAtPrice.DispatchMessage、TradeView.DispatchMessage 与 ChartRedraw 是统一刷新出口。想验证分发是否生效,可在 MT5 里给 TradeView.DispatchMessage 前插 Print(id) ,拖动图表看日志是否随 CHARTEVENT_CHART_CHANGE 规律打印。外汇与贵金属杠杆高,这类界面刷新逻辑只影响显示,不改变订单风险。
TradeView.UpdateInfosIndicators(class="num">0, trans.order, trans.price, trans.price_tp, trans.price_sl, trans.volume, def_IsBuy(trans.order_type)); break; case TRADE_TRANSACTION_POSITION: TradeView.UpdateInfosIndicators(class="num">0, trans.position, trans.price, trans.price_tp, trans.price_sl, trans.volume, trans.deal_type == DEAL_TYPE_BUY); break; } class="macro">#undef def_IsBuy } class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { Mouse.DispatchMessage(id, lparam, dparam, sparam); class="kw">switch (id) { case CHARTEVENT_CHART_CHANGE: Terminal.Resize(); WallPaper.Resize(); TimesAndTrade.Resize(); break; } Chart.DispatchMessage(id, lparam, dparam, sparam); VolumeAtPrice.DispatchMessage(id, sparam); TradeView.DispatchMessage(id, lparam, dparam, sparam); ChartRedraw(); }
C_IndicatorTradeView 怎么接管图表上的订单显示
C_IndicatorTradeView 类负责把账户里的持仓、挂单在图表上画出来并支持交互拖拽,它把早年 C_OrderView 与 C_TradeGraphics 两类的活儿合并了,但数据组织方式完全不同。初始化函数一上来就关掉 MT5 原生的交易价位显示与拖拽:CHART_SHOW_TRADE_LEVELS 和 CHART_DRAG_TRADE_LEVELS 都设为 false,等于不让终端自带的指示点干扰 EA 自己的绘制逻辑。 如果你没用交叉订单系统,MT5 默认会在图表留可拖的订单点;点一下拖一下,EA 就按指标重算新位置。这功能不挡路,但既然自己写了系统,就该让它真正跑起来。 数据更新函数不区分挂单还是持仓,统一扫描 OrdersTotal 和 PositionsTotal,把 ticket、开仓价、TP、SL、成交量塞进 IndicatorInfosAdd 与 UpdateInfosIndicators。OnTick 里只做轻量刷新,仓位票据(ticket)是定位对象的唯一主键,速度敏感,别在里头堆重逻辑。 拖拽靠三段代码配合:先点对象线变宽捕获鼠标,移动后再次点击落位,不用一直按住键。其中「石头函数」专门在订单或限价移动时调用 C_Router 向交易服务器发请求——服务器延迟高,图表操作就发涩;响应快则顺滑。外汇与贵金属属高风险品种,剧烈波动时限价可能跳空,用这套自绘系统比原生慢但更可控,后续文章会改得更灵活。 下面这段初始化代码可直接贴进 MT5 看效果:它先禁掉终端自带交易层,再遍历当前品种订单与持仓写入指标缓冲。
class="type">void Initilize(class="type">void) { class="type">int orders = OrdersTotal(); class="type">class="kw">ulong ticket; class="type">bool isBuy; class="type">long info; class="type">class="kw">double tp, sl; ChartSetInteger(Terminal.Get_ID(), CHART_SHOW_OBJECT_DESCR, false); ChartSetInteger(Terminal.Get_ID(), CHART_SHOW_TRADE_LEVELS, false); ChartSetInteger(Terminal.Get_ID(), CHART_DRAG_TRADE_LEVELS, false); for (class="type">int c0 = class="num">0; c0 <= orders; c0++) if ((ticket = OrderGetTicket(c0)) > class="num">0) if (OrderGetString(ORDER_SYMBOL) == Terminal.GetSymbol()) { info = OrderGetInteger(ORDER_TYPE); isBuy = ((info == ORDER_TYPE_BUY_LIMIT) || (info == ORDER_TYPE_BUY_STOP) || (info == ORDER_TYPE_BUY_STOP_LIMIT) || (info == ORDER_TYPE_BUY)); IndicatorInfosAdd(ticket); UpdateInfosIndicators(-class="num">1, ticket, OrderGetDouble(ORDER_PRICE_OPEN), OrderGetDouble(ORDER_TP), OrderGetDouble(ORDER_SL), OrderGetDouble(ORDER_VOLUME_CURRENT), isBuy); } orders = PositionsTotal(); for (class="type">int c0 = class="num">0; c0 <= orders; c0++) if (PositionGetSymbol(c0) == Terminal.GetSymbol()) { tp = PositionGetDouble(POSITION_TP); sl = PositionGetDouble(POSITION_SL); ticket = PositionGetInteger(POSITION_TICKET); IndicatorInfosAdd(ticket);
◍ 持仓信息同步与通道止损平仓
这段逻辑把持仓、挂单和指标面板的数据打通:UpdateInfosIndicators 根据 test 标记判断是实盘还是挂单,再分别把开仓价、TP、SL、成交量写进对应的指标轴对象。注意 test>0 时强制按实盘处理,test<0 或票号等于 def_IndicatorTicket0 时按挂单处理,其余走 OrderSelect 回查。 SecureChannelPosition 负责扫当前品种的所有持仓:先取 BID/ASK,再倒序遍历 PositionsTotal()。对每仓它用 SetTextValue 刷新面板上的成交量、浮动盈利和开仓价,并读出 SL。 平仓触发很直接——买仓时若 ask 已低于 SL 就市价平;卖仓时若 bid 高于 SL 且 SL>0 才平。外汇与贵金属杠杆高,SL 被扫是常态风险,这套只解决‘到价即平’,不保证滑点后的实际成交价。 想验证就开 MT5 把这段贴进 EA,把 def_IndicatorTicket0 和 IT_* 枚举换成你面板用的常量,跑一个 0.01 手欧美单看 SL 触发是否及时。
UpdateInfosIndicators(class="num">1, ticket, PositionGetDouble(POSITION_PRICE_OPEN), tp, sl, PositionGetDouble(POSITION_VOLUME), PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY); } CreateIndicatorTrade(def_IndicatorTicket0, IT_PENDING); CreateIndicatorTrade(def_IndicatorTicket0, IT_TAKE); CreateIndicatorTrade(def_IndicatorTicket0, IT_STOP); } class="type">void UpdateInfosIndicators(class="type">char test, class="type">class="kw">ulong ticket, class="type">class="kw">double pr, class="type">class="kw">double tp, class="type">class="kw">double sl, class="type">class="kw">double vol, class="type">bool isBuy) { class="type">bool isPending; isPending = (test > class="num">0 ? false : (test < class="num">0 ? true : (ticket == def_IndicatorTicket0 ? true : OrderSelect(ticket)))); PositionAxlePrice(ticket, (isPending ? IT_RESULT : IT_PENDING), class="num">0); PositionAxlePrice(ticket, (isPending ? IT_PENDING : IT_RESULT), pr); SetTextValue(ticket, (isPending ? IT_PENDING : IT_RESULT), vol); PositionAxlePrice(ticket, IT_TAKE, tp); PositionAxlePrice(ticket, IT_STOP, sl); SetTextValue(ticket, IT_TAKE, vol, (isBuy ? tp - pr : pr - tp)); SetTextValue(ticket, IT_STOP, vol, (isBuy ? sl - pr : pr - sl)); } class="kw">inline class="type">class="kw">double SecureChannelPosition(class="type">void) { class="type">class="kw">double Res = class="num">0, sl, profit, bid, ask; class="type">class="kw">ulong ticket; bid = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_BID); ask = SymbolInfoDouble(Terminal.GetSymbol(), SYMBOL_ASK); for (class="type">int i0 = PositionsTotal() - class="num">1; i0 >= class="num">0; i0--) if (PositionGetSymbol(i0) == Terminal.GetSymbol()) { ticket = PositionGetInteger(POSITION_TICKET); SetTextValue(ticket, IT_RESULT, PositionGetDouble(POSITION_VOLUME), profit = PositionGetDouble(POSITION_PROFIT), PositionGetDouble(POSITION_PRICE_OPEN)); sl = PositionGetDouble(POSITION_SL); if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { if (ask < sl) ClosePosition(ticket); }else { if ((bid > sl) && (sl > class="num">0)) ClosePosition(ticket); }
「把鼠标动作翻译成挂单指令」
这段逻辑干的事很直接:在图表上监听鼠标移动和按键组合,把交易者的手势转成具体的挂单或盈亏标注。SHIFT 加左键倾向解读为买入方向,CTRL 加左键倾向解读为卖出方向,两者不能同时成立,否则不处理。 SetTextValue 里用 switch 区分了四种情形:IT_RESULT 会同时清掉挂单轴并写入利润文本;IT_PENDING 把输入量除以 Terminal.GetVolumeMinimal() 后显示成可编辑手数;IT_TAKE 和 IT_STOP 则把止损止盈距离乘以合约调整系数算成金额再上屏。 鼠标事件里 mKeys 的位掩码很关键:0x01 是左键、0x04 是 SHIFT、0x08 是 CTRL。你在 MT5 里想改键位映射,直接动这三个十六进制数就行,但外汇和贵金属杠杆高,改完务必用模拟盘验证手势不会误触发。
Res += profit; } class="kw">return Res; }; class="type">void SetTextValue(class="type">class="kw">ulong ticket, eIndicatorTrade it, class="type">class="kw">double value0, class="type">class="kw">double value1 = class="num">0.0, class="type">class="kw">double priceOpen = class="num">0.0) { class="type">class="kw">double finance; class="kw">switch (it) { case IT_RESULT : PositionAxlePrice(ticket, it, priceOpen); PositionAxlePrice(ticket, IT_PENDING, class="num">0); m_EditInfo2.SetTextValue(MountName(ticket, it, EV_PROFIT), value1); case IT_PENDING: m_EditInfo1.SetTextValue(MountName(ticket, it, EV_EDIT), value0 / Terminal.GetVolumeMinimal(), def_ColorVolumeEdit); break; case IT_TAKE : case IT_STOP : finance = (value1 / Terminal.GetAdjustToTrade()) * value0; m_EditInfo1.SetTextValue(MountName(ticket, it, EV_EDIT), finance); break; } } 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">ulong ticket; class=class="str">"cmt">// ... Code .... class="kw">switch (id) { case CHARTEVENT_MOUSE_MOVE: Mouse.GetPositionDP(dt, price); mKeys = Mouse.GetButtonStatus(); bEClick = (mKeys & 0x01) == 0x01; class=class="str">"cmt">//Left mouse click bKeyBuy = (mKeys & 0x04) == 0x04; class=class="str">"cmt">//SHIFT press bKeySell = (mKeys & 0x08) == 0x08; class=class="str">"cmt">//CTRL press if (bKeyBuy != bKeySell) { if (!bMounting)
挂单挂载时的光标与价格校准
在指标脚本进入挂单挂载分支时,先隐藏鼠标光标避免界面闪烁,再向底层金融接口取杠杆与止损止盈基准值。 Mouse.Hide(); bIsDT = Chart.GetBaseFinance(leverange, valueTp, valueSl); valueTp = Terminal.AdjustPrice(valueTp * Terminal.GetAdjustToTrade() / leverange); valueSl = Terminal.AdjustPrice(valueSl * Terminal.GetAdjustToTrade() / leverange); m_TradeLine.SpotLight(MountName(def_IndicatorTicket0, IT_PENDING, EV_LINE)); bMounting = true; 上面这段代码逐行做四件事:隐藏鼠标;用 Chart.GetBaseFinance 拿杠杆 leverange 与初始 tp/sl 数值;按 Terminal.GetAdjustToTrade() 与杠杆比值用 AdjustPrice 规范化 tp、sl 报价;用 SpotLight 高亮挂单线并把 bMounting 置真。 tp = price + (bKeyBuy ? valueTp : (-valueTp)); sl = price + (bKeyBuy ? (-valueSl) : valueSl); UpdateInfosIndicators(0, def_IndicatorTicket0, price, tp, sl, leverange, bKeyBuy); if ((bEClick) && (memLocal == 0)) CreateOrderPendent(leverange, bKeyBuy, memLocal = price, tp, sl, bIsDT); 多空方向靠 bKeyBuy 切换:买挂单 tp 在 price 之上加 valueTp、sl 在之下减 valueSl;卖挂单符号反转。仅当鼠标点击 bEClick 且 memLocal 为 0 才真正下 pendent 单。 若已挂载分支走 else if (bMounting),则调 UpdateInfosIndicators 清掉展示数值、Mouse.Show() 恢复光标、memLocal 与 bMounting 复位。外汇与贵金属杠杆品种波动剧烈,这套挂载逻辑仅解决交互与报价校准,不预示任何成交后盈亏。 当 !bMounting 且 bKeyBuy == bKeySell(方向键均未触发)时进入中性分支,若 bEClick 则把 bIsMove 置 false,相当于取消拖拽态。开 MT5 把这段嵌进你的指标 OnChartEvent,点一下图表就能观察挂载与光标联动。
{
Mouse.Hide();
bIsDT = Chart.GetBaseFinance(leverange, valueTp, valueSl);
valueTp = Terminal.AdjustPrice(valueTp * Terminal.GetAdjustToTrade() / leverange);
valueSl = Terminal.AdjustPrice(valueSl * Terminal.GetAdjustToTrade() / leverange);
m_TradeLine.SpotLight(MountName(def_IndicatorTicket0, IT_PENDING, EV_LINE));
bMounting = true;
}
tp = price + (bKeyBuy ? valueTp : (-valueTp));
sl = price + (bKeyBuy ? (-valueSl) : valueSl);
UpdateInfosIndicators(class="num">0, def_IndicatorTicket0, price, tp, sl, leverange, bKeyBuy);
if ((bEClick) && (memLocal == class="num">0)) CreateOrderPendent(leverange, bKeyBuy, memLocal = price, tp, sl, bIsDT);
}else if (bMounting)
{
UpdateInfosIndicators(class="num">0, def_IndicatorTicket0, class="num">0, class="num">0, class="num">0, class="num">0, false);
Mouse.Show();
memLocal = class="num">0;
bMounting = false;
}else if ((!bMounting) && (bKeyBuy == bKeySell))
{
if (bEClick)
{
bIsMove = false;◍ 拖拽对象时的高亮与状态切换
在图表事件回调里,CHARTEVENT_OBJECT_CLICK 分支负责捕获交易线对象的点击。若 GetIndicatorInfos 解析出 ticket、price 与事件类型,便进入 EV_MOVE 的子处理:根据 bIsMove 当前值决定是清除高亮还是对指定名称的对象调用 SpotLight 高亮,并翻转 bIsMove 标记。 这段逻辑的实际效果是,第一次点中交易线会高亮挂载名(MountName 拼出的对象),第二次点中同一线则熄灭高亮,避免重复占用视觉焦点。外汇与贵金属图表上对象密集,这种交替开关能降低误读概率,但高频拖拽时仍可能漏触发,属正常局限。 MoveSelection 函数用 static 变量 memStr、ticket、it 暂存上一次选择上下文,配合传入的 price 与 mKeys 完成位移重算。开 MT5 把这段接进你的 EA 事件处理函数,拖几下就能验证 bIsMove 翻转是否符合预期。
m_TradeLine.SpotLight(); } MoveSelection(price, mKeys); } break; class=class="str">"cmt">// ... Code ... case CHARTEVENT_OBJECT_CLICK: if (GetIndicatorInfos(sparam, ticket, price, it, ev)) class="kw">switch (ev) { class=class="str">"cmt">// ... Code ... case EV_MOVE: if (bIsMove) { m_TradeLine.SpotLight(); bIsMove = false; }else { m_TradeLine.SpotLight(MountName(ticket, it, EV_LINE)); bIsMove = true; } break; } break; } } class="type">void MoveSelection(class="type">class="kw">double price, class="type">uint keys) { class="kw">static class="type">class="kw">string memStr = NULL; class="kw">static class="type">class="kw">ulong ticket = class="num">0; class="kw">static eIndicatorTrade it; eEventType ev; class="type">class="kw">double tp, sl, pr; class="type">bool isPending;
「拖线改单时挂单与持仓的分流处理」
在 MT5 图表上用鼠标拖拽趋势线修改订单水平时,先通过 GetObjectSelected 拿到当前选中的对象名,若不为 NULL 才进入后续逻辑;这一步避免了空指针导致的脚本异常。 如果选中的线和上一次记忆的字符串不同,就调用 GetIndicatorInfos 刷新票据号、价格、类型与事件,并把 memStr 更新为当前线名,防止重复拉取。 OrderSelect(ticket) 的返回值赋给 isPending,用来区分当前是挂单还是已开仓位。外汇与贵金属杠杆高,拖线改 SL/TP 属于实时风险操作,改错价格可能瞬间扩大敞口。 switch 按 IT_TAKE、IT_STOP、IT_PENDING 三种线类型分流:盈利线被拖时,挂单走 ModifyOrderPendent 改 TP、市价单走 ModifyPosition 改 TP;止损线同理只动 SL;拖 pending 线时,以新价格 price 为基准,按原 pending 与 tp/sl 的相对偏移重算挂单的 TP、SL,保证间距不变。
class="type">class="kw">string sz0 = m_TradeLine.GetObjectSelected(); if (sz0 != NULL) { if (memStr != sz0) GetIndicatorInfos(memStr = sz0, ticket, pr, it, ev); isPending = OrderSelect(ticket); class="kw">switch (it) { case IT_TAKE: if (isPending) ModifyOrderPendent(ticket, macroGetPrice(IT_PENDING), price, macroGetPrice(IT_STOP)); else ModifyPosition(ticket, price, macroGetPrice(IT_STOP)); break; case IT_STOP: if (isPending) ModifyOrderPendent(ticket, macroGetPrice(IT_PENDING), macroGetPrice(IT_TAKE), price); else ModifyPosition(ticket, macroGetPrice(IT_TAKE), price); break; case IT_PENDING: pr = macroGetPrice(IT_PENDING); tp = macroGetPrice(IT_TAKE); sl = macroGetPrice(IT_STOP); ModifyOrderPendent(ticket, price, (tp == class="num">0 ? class="num">0 : price + tp - pr), (sl == class="num">0 ? class="num">0 : price + sl - pr)); break; }; }
记住这一条就够了
这套订单类 EA 目前在演示账户跑出来的附件体积是 12028.77 KB,逻辑骨架已经稳定,后续文章只会加功能、不会大改核心机制。 真要上手,先在 MT5 演示账户挂一段时间,把移动订单和限价的响应节奏摸清楚;觉得手动拖太慢,就把文里示意用的锚点删了,直接用平台原生拖单,让 EA 只负责读数据。 外汇和贵金属波动剧烈、杠杆风险高,拿它练平台操作和数据建模可以,实盘前务必自己验证每一条触发条件。