从头开始开发智能交易系统(第 22 部分):新订单系统 (V)·综合运用
🧩

从头开始开发智能交易系统(第 22 部分):新订单系统 (V)·综合运用

(3/3)· 当 EA 在 WDO/WIN 之外资产上失灵,多半是合约定义与移动订单逻辑埋了雷

案例拆解新手友好 第 3/3 篇

很多交易者以为图表上拖单只是前端小花招,真到跨品种跑 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 规律打印。外汇与贵金属杠杆高,这类界面刷新逻辑只影响显示,不改变订单风险。

MQL5 / C++
            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 看效果:它先禁掉终端自带交易层,再遍历当前品种订单与持仓写入指标缓冲。

MQL5 / C++
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 触发是否及时。

MQL5 / C++
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 里想改键位映射,直接动这三个十六进制数就行,但外汇和贵金属杠杆高,改完务必用模拟盘验证手势不会误触发。

MQL5 / C++
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,点一下图表就能观察挂载与光标联动。

MQL5 / C++
{
   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 翻转是否符合预期。

MQL5 / C++
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,保证间距不变。

MQL5 / C++
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 只负责读数据。 外汇和贵金属波动剧烈、杠杆风险高,拿它练平台操作和数据建模可以,实盘前务必自己验证每一条触发条件。

让小布替你跑这套
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到合约异动与订单偏移预警,把重复劳动交给小布,你专注决策。

常见问题

通过 SymbolInfoInteger 与 SymbolInfoDouble 读取 SYMBOL_TRADE_CONTRACT_SIZE、SYMBOL_LEVERAGE 等字段,再归一化到点数价值,避免硬编码交易所参数。
小布盯盘目前提供品种页诊断与预警,不托管第三方 EA 执行;但你可用它的 AIGC 笔记比对自建订单系统的偏移日志,快速定位异常。
两类资产的最小价位、合约乘数和保证金模式不同,若算法里用了固定点数或写死手数换算,就会在跨市场时失真,需按 SymbolInfo 动态推导。
它把图表上订单标签、线型和交互事件收拢到单一视图对象,省去在多个控制块里各写一遍绘制与点击响应的冗余代码。
指将原本绑定 B3 期货的假设剥离,改为基于平台合约数据的泛化结构,使同一 EA 可平滑切换到外汇或贵金属,但仍需自行验证高风险品种适配。