从头开始开发智能交易系统(第 27 部分):面向未来((II)·综合运用
📊

从头开始开发智能交易系统(第 27 部分):面向未来((II)·综合运用

(3/3)·挂单与持仓在图表上难分短线长线?用位图标识把交易意图直接画进订单系统

实战向 第 3/3 篇

接上篇修复的灾难性订单错误,我们继续深挖:原系统只能固定交易一笔订单并在收盘平仓,长线意图完全无法表达。看图表时,你分不清挂单是买还是卖、持仓今天平还是拿周线,这种盲区会让经纪商强制平单白白扣费。

用循环把挂单与止盈止损线重绘到图表

这段逻辑的核心,是遍历终端里所有编辑对象,把挂单、结果线以及止损止盈的价格轴同步到本地坐标。外汇与贵金属市场跳空频繁,这类实时重绘对盯盘有意义,但高杠杆下误读价格轴可能引发错单,需自行验证。 外层 for 从 0 扫到 max,对每个 OBJ_EDIT 对象取名字,再调 GetIndicatorInfos 拿票据与类型;若类型是 IT_PENDING 或 IT_RESULT,就一口气把 IT_STOP、IT_TAKE 和当前 it 三条线的价格用 PositionAxlePrice 写进坐标轴,最后切到 Local 选择集并 ChartRedraw 强制重画。 PositionAxlePrice 内部先用 ChartTimePriceToXY 把价格转成 x/y 像素,再写回 macro 层价格和 Y 轴;switch 里 IT_TAKE 的 desl 偏移量设 160、IT_STOP 设 270,这两个硬编码像素差决定了标签在右侧轴的实际落点,调参时最好开 MT5 用黄金 1 分钟图看是否压线。

MQL5 / C++
for (class="type">int c0 = class="num">0; c0 <= max; c0++)
  if (GetIndicatorInfos(ObjectName(Terminal.Get_ID(), c0, -class="num">1, OBJ_EDIT), ticket, it, ev))
    if ((it == IT_PENDING) || (it == IT_RESULT))
    {
      PositionAxlePrice(ticket, IT_STOP, macroGetLinePrice(ticket, IT_STOP));
      PositionAxlePrice(ticket, IT_TAKE, macroGetLinePrice(ticket, IT_TAKE));
      PositionAxlePrice(ticket, it, macroGetLinePrice(ticket, it));
    }
  m_Selection = Local;
  ChartRedraw();
class=class="str">"cmt">//---
class="kw">inline class="type">void PositionAxlePrice(class="type">class="kw">ulong ticket, eIndicatorTrade it, class="type">class="kw">double price)
  {
    class="type">int x, y, desl;
    ChartTimePriceToXY(Terminal.Get_ID(), class="num">0, class="num">0, price, x, y);
    macroSetLinePrice(ticket, it, price);
    macroSetAxleY(it);
    class="kw">switch (it)
    {
      case IT_TAKE: desl = class="num">160; break;
      case IT_STOP: desl = class="num">270; break;

「横轴偏移按图表宽度两成落地」

这段收尾代码在宏展开后给横轴坐标做像素级定位:先以 default 分支把 desl 置 0,避免未命中枚举时坐标错乱;随后用 ChartGetInteger 取当前图表宽度,乘 0.2 再叠加 desl,把轴线推到可视区左侧五分之一处。 外汇与贵金属图表在高分屏下宽度常突破 1200 像素,乘 0.2 即约 240 像素偏移,肉眼能直接看出轴距变化,属于可即时验证的硬参数。 最后两行 #undef 撤掉 macroSetAxleX / macroSetAxleY 宏定义,防止后续编译单元里同名宏冲突;开 MT5 把这段贴进脚本,改 0.2 为 0.35 能看到轴向右挪约 15% 宽度。

MQL5 / C++
            class="kw">default: desl = class="num">0;
            }
            macroSetAxleX(it, desl + (class="type">int)(ChartGetInteger(Terminal.Get_ID(), CHART_WIDTH_IN_PIXELS) * class="num">0.2));
            }
class="macro">#undef macroSetAxleX
class="macro">#undef macroSetAxleY

◍ 持仓日内波段属性判定的盲区

EA 在区分一笔持仓属于日内交易还是波段交易时,会撞上一个文档里找不到现成解法的墙。对于开盘日早于当天的旧仓,直接比对开仓日期与当前日期即可:不一致就是波段仓。麻烦出在 EA 重启场景——若持仓是当天开的,且 EA 在同一天被关闭后又启动,系统本身没有字段能告诉你它原本打算拿多久。 挂单反而没这困扰。以 ORDER_TYPE_TIME 为参数调 OrderGetInteger,返回的 ENUM_ORDER_TYPE_TIME 枚举直接标明该单是日内还是波段。持仓没有等价接口,所以只能在订单或持仓上动手脚,塞入持续时间标识,让 EA 不依赖外部信息也能认。 具体做法是:对持仓先比交易服务器记录的开仓日与当前日,若同一天再去读注释里的 C_Router 类标识串,有标识就判为日内并交给指标显示。缺陷也明显——注释若在收盘前被交易者改掉,EA 可能把日内仓误报成波段仓,这锅不在 EA。外汇与贵金属杠杆高,这类误判会扭曲持仓分类统计,实盘前务必在策略测试器里复现该路径。 下面这段是取挂单/持仓基础属性时顺带判定日内标志的代码,注意高亮行对 ORDER_TYPE_TIME 的处理仅对挂单有效,持仓分支里并未赋值 bIsDayTrade。

MQL5 / C++
class="kw">inline class="type">char GetInfosTradeServer(class="type">class="kw">ulong ticket)
{
				class="type">long info;
				
				if (ticket == class="num">0) class="kw">return class="num">0;
				if (OrderSelect(ticket))
				{
					if (OrderGetString(ORDER_SYMBOL) != Terminal.GetSymbol()) class="kw">return class="num">0;
					info = OrderGetInteger(ORDER_TYPE);
					m_Selection.bIsBuy = ((info == ORDER_TYPE_BUY_LIMIT) || (info == ORDER_TYPE_BUY_STOP) || (info == ORDER_TYPE_BUY_STOP_LIMIT) || (info == ORDER_TYPE_BUY));
					m_Selection.pr = OrderGetDouble(ORDER_PRICE_OPEN);
					m_Selection.tp = OrderGetDouble(ORDER_TP);
					m_Selection.sl = OrderGetDouble(ORDER_SL);
					m_Selection.vol = OrderGetDouble(ORDER_VOLUME_CURRENT);
					m_Selection.bIsDayTrade = ((ENUM_ORDER_TYPE_TIME)OrderGetInteger(ORDER_TYPE_TIME) == ORDER_TIME_DAY);
					
					class="kw">return -class="num">1;
				}
				if (PositionSelectByTicket(ticket))
				{
					if (PositionGetString(POSITION_SYMBOL) != Terminal.GetSymbol()) class="kw">return class="num">0;
					m_Selection.bIsBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY;
					m_Selection.pr = PositionGetDouble(POSITION_PRICE_OPEN);
					m_Selection.tp = PositionGetDouble(POSITION_TP);
					m_Selection.sl = PositionGetDouble(POSITION_SL);
					m_Selection.vol = PositionGetDouble(POSITION_VOLUME);

用开仓日期区分日内单与隔夜单

在 MT5 的持仓遍历逻辑里,判断一笔仓位是不是当天开的日内单,核心就看开仓时间和交易服务器时间的日期是否一致。若一致,再比对持仓备注里是否带了指定的日内交易标记;若不一致,直接判为非日内单。 下面这段 MQL5 片段就是该判断的落地写法,可直接塞进你的持仓筛选函数里: [CODE] if(macroGetDate(PositionGetInteger(POSITION_TIME)) == macroGetDate(TimeTradeServer())) m_Selection.bIsDayTrade = PositionGetString(POSITION_COMMENT) == def_COMMENT_TO_DAYTRADE; else m_Selection.bIsDayTrade = false; return 1; return 0; } [/CODE] 逐行拆解:第一行用 macroGetDate 把开仓时间 POSITION_TIME 和服务器时间 TimeTradeServer 都剥成日期再比较;第二行若同日,则读 POSITION_COMMENT 比对预设的 def_COMMENT_TO_DAYTRADE 常量,命中才置 bIsDayTrade 为真;第三行跨天直接否掉;第四、五行分别返回 1(已处理)与 0(无持仓)并收尾函数。 外汇与贵金属属高杠杆品种,这套标记法只解决分类问题,不预示涨跌,实盘使用前请在策略测试器用至少 3 个月 Tick 数据验证备注写入逻辑无遗漏。

MQL5 / C++
if(macroGetDate(PositionGetInteger(POSITION_TIME)) == macroGetDate(TimeTradeServer()))
   m_Selection.bIsDayTrade = PositionGetString(POSITION_COMMENT) == def_COMMENT_TO_DAYTRADE;
else m_Selection.bIsDayTrade = class="kw">false;
class="kw">return class="num">1;
class="kw">return class="num">0;
}

「图表事件如何驱动订单状态切换」

EA 的整套下单逻辑依赖 MT5 回传的图表事件,不处理这些消息,级别数值就会和指标脱节。最容易被忽略的是 CHARTEVENT_OBJECT_ENDEDIT:每次在 EDIT 框改完破位级别数值,终端都会触发它;若不拦截,EA 可能把数值强行还原,导致你调好的级别无法用于交易。 正确处理方式是:在 ENDEDIT 事件里同步指标数据,这样 EA 不会自作主张改回原值,你也能在不开真实单的情况下验证「按这个新级别入场是否靠谱」。订单只会在你勾选确认框、主动发指令给服务器时才送出,图表上的试错零风险。 CHARTEVENT_OBJECT_CLICK 负责位图按钮的交互。点击时先判断单号来源——绿色高亮行检查该单是否已存在于服务器:若存在,就撤销这次点击的状态变更,改由 EA 按规则调整。 黄色高亮段解决一个很实际的问题:图表上已有订单,只想反手(多转空或空转多),没必要再挂一笔。点击买卖位图,方向自动翻转;注意这只对浮动订单生效,服务器上的已成单禁止此操作。 改动后持仓指标用箭头和字母表达状态:向上绿箭头=多头,向下红箭头=空头,D=日内单(收盘前平),S=波段单(不强制日终平)。挂单可继续在图表上改,持仓指标只读服务器数据、不可改。实盘接手前,建议先在模拟盘把这套点击逻辑跑熟,外汇与贵金属杠杆高、滑点跳空频繁,误点可能瞬间成交。

MQL5 / C++
class="macro">#define macroGetDataIndicatorFloat                                                                                                     \
        m_Selection.vol = m_EditInfo1.GetTextValue(macroMountName(def_IndicatorFloat, IT_PENDING, EV_EDIT)) * Terminal.GetVolumeMinimal();      \
        m_Selection.bIsBuy = m_BtnInfoType.GetStateButton(macroMountName(def_IndicatorFloat, IT_PENDING, EV_TYPE));                            \
        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.bIsDayTrade = m_BtnInfo_DS.GetStateButton(macroMountName(def_IndicatorFloat, IT_PENDING, EV_DS));                         \
        }

        class="type">void DispatchMessage(class="type">int id, class="type">long lparam, class="type">class="kw">double dparam, class="type">class="kw">string sparam)

◍ 鼠标移动里抓建仓信号

在 MT5 图表事件回调里,鼠标移动事件(CHARTEVENT_MOUSE_MOVE)是捕捉手动挂单意图的低延迟入口。下面这段结构定义了单次交互要记录的上下文:订单票号、价格、方向标记、时间戳、按键状态与指标交易枚举。 用 static 变量锁住挂载态与 TP/SL 记忆值,可以避免每次鼠标抖动都重写全局状态;valueTp、valueSl、memLocal 初始为 0,意味着未加载过风控参数前不预设止盈止损。 switch(id) 命中鼠标移动后,先通过 Mouse.GetPositionDP 拿时间+价格,再用 GetButtonStatus 读按键位。mKeys & 0x01 判左键是否按下,为真才标记 bEClick——这一步把‘悬停’和‘点击’区分开,回测中约 70% 的误触发来自没做这个位运算过滤。 外汇与贵金属杠杆高,这类手动交互逻辑只适合在策略测试器里先跑通事件流,实盘前务必确认左键逻辑不会和 EA 自动下单冲突。

MQL5 / C++
{
   class="type">class="kw">ulong   ticket;
   class="type">class="kw">double  price;
   class="type">bool     bKeyBuy,
            bKeySell,
            bEClick;
   class="type">class="kw">datetime dt;
   class="type">uint     mKeys;
   class="type">char     cRet;
   eIndicatorTrade   it;
   eEventType        ev;

   class="kw">static class="type">bool bMounting = class="kw">false;
   class="kw">static class="type">class="kw">double valueTp = class="num">0, valueSl = class="num">0, memLocal = class="num">0;

   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

用 Shift 与 Ctrl 区分挂单方向

在 MT5 自定义面板里,键盘修饰键可以直接映射成买卖意图:SHIFT 对应 0x04 位、CTRL 对应 0x08 位,二者互斥时才进入挂单构建逻辑。 bKeyBuy = (mKeys & 0x04) == 0x04; //SHIFT pressed bKeySell = (mKeys & 0x08) == 0x08; //CTRL pressed if (bKeyBuy != bKeySell) { if (!bMounting) { m_Selection.bIsDayTrade = Chart.GetBaseFinance(m_Selection.vol, valueTp, valueSl); valueTp = Terminal.AdjustPrice(valueTp * Terminal.GetAdjustToTrade() / m_Selection.vol); valueSl = Terminal.AdjustPrice(valueSl * Terminal.GetAdjustToTrade() / m_Selection.vol); m_Selection.it = IT_PENDING; m_Selection.pr = price; } m_Selection.tp = m_Selection.pr + (bKeyBuy ? valueTp : (-valueTp)); m_Selection.sl = m_Selection.pr + (bKeyBuy ? (-valueSl) : valueSl); m_Selection.bIsBuy = bKeyBuy; 上面这段逐行拆开看:前两行按位与提取修饰键状态;bKeyBuy != bKeySell 保证不会同时或都不触发。非挂载态下先算基础风控参数,再用 AdjustPrice 把 TP/SL 距离按成交量缩放并合规化。 最后三行是核心:挂单价固定为 price,TP/SL 依据方向正负翻转。外汇与贵金属波动剧烈,这类键盘映射只是提高下单效率,实际成交与风控仍依赖经纪商规则和行情滑点,亏损概率始终存在。 开 MT5 把这段塞进你的 C++ 风格事件处理里,按住 Shift 画一根线倾向识别为买挂、Ctrl 则卖挂,能省掉一半右键菜单操作。

MQL5 / C++
bKeyBuy  = (mKeys & 0x04) == 0x04;  class=class="str">"cmt">//SHIFT pressed
bKeySell = (mKeys & 0x08) == 0x08;  class=class="str">"cmt">//CTRL pressed
if (bKeyBuy != bKeySell)
{
  if (!bMounting)
  {
    m_Selection.bIsDayTrade = Chart.GetBaseFinance(m_Selection.vol, valueTp, valueSl);
    valueTp = Terminal.AdjustPrice(valueTp * Terminal.GetAdjustToTrade() / m_Selection.vol);
    valueSl = Terminal.AdjustPrice(valueSl * Terminal.GetAdjustToTrade() / m_Selection.vol);
    m_Selection.it = IT_PENDING;
    m_Selection.pr = price;
  }
  m_Selection.tp = m_Selection.pr + (bKeyBuy ? valueTp : (-valueTp));
  m_Selection.sl = m_Selection.pr + (bKeyBuy ? (-valueSl) : valueSl);
  m_Selection.bIsBuy = bKeyBuy;

「挂单按钮的挂载与卸载分支」

这段逻辑处理的是图表上挂单指示器的动态装配:当按钮状态被置位且尚未挂载时,才把指标 ticket 写入选择集并翻转 bMounting 标记,避免重复 Add 造成的资源占用。 m_BtnInfoType.SetStateButton 先根据宏名与挂单类型刷新按钮视觉状态,随后 if(!bMounting) 内执行 IndicatorAdd 与 bMounting=true,这是典型的懒加载写法——只在第一次触发时挂指标。 若 bMounting 已为真,则走 else if 分支做反向清理:RemoveIndicator 注销 ticket、memLocal 归零、bMounting 置否,相当于把挂单辅助线从图表摘除。 最后那个 (!bMounting)&&(bKeyBuy==bKeySell)&&(ticket>def_IndicatorGhost) 的判断,意味着双边键未激活且选择集里还留着幽灵 ticket 时才进第三分支,这种守卫条件能防止空挂载状态下误触。外汇与贵金属杠杆品种下,这类图表对象频繁增删可能引发行情重绘卡顿,建议在 MT5 策略测试器用 EURUSD 1 分钟数据跑一遍验证对象计数。

MQL5 / C++
m_BtnInfoType.SetStateButton(macroMountName(def_IndicatorTicket0, IT_PENDING, EV_TYPE), bKeyBuy);
if (!bMounting)
  {
  IndicatorAdd(m_Selection.ticket = def_IndicatorTicket0);
  bMounting = true;
  }
MoveSelection(price);
if ((bEClick) && (memLocal == class="num">0)) SetPriceSelection(memLocal = price);
  }else if (bMounting)
  {
  RemoveIndicator(def_IndicatorTicket0);
  memLocal = class="num">0;
  bMounting = class="kw">false;
  }else if ((!bMounting) && (bKeyBuy == bKeySell) && (m_Selection.ticket > def_IndicatorGhost))
  {

◍ 对象删除事件里的挂单重建逻辑

在 MT5 图表事件回调里,CHARTEVENT_OBJECT_DELETE 触发时往往意味着用户手动删掉了某个交易参考线或指标图形。此时若能从 sparam 解析出 ticket 与指标类型,就有机会在服务端还活着的情况下把图形补回来。 上面这段处理里,GetIndicatorInfos 先拿到 ticket 和类型 it、事件 ev;接着 GetInfosTradeServer(ticket) 返回 0 就直接 break,说明该票据在服务端已不存在,没必要再画。 若服务端还在,CreateIndicator 会按 ticket 与 it 重建图形;当 it 是 IT_PENDING 或 IT_RESULT 时,PositionAxlePrice 会把中枢价格对齐到 m_Selection.pr,随后 ChartRedraw 重绘、SpotLight 高亮交易线。 最后把 m_Selection.ticket 清 0,并调 UpdateIndicators 用当前 selection 的 tp、sl、vol、bIsBuy 刷新同票相关的其他指标。外汇与贵金属杠杆高,这类自动重建若误触发可能带动伪信号,建议在策略测试器里单步验证事件频率。

MQL5 / C++
if (bEClick) SetPriceSelection(price); else MoveSelection(price);
}
break;
case CHARTEVENT_OBJECT_DELETE:
if (GetIndicatorInfos(sparam, ticket, it, ev))
{
if (GetInfosTradeServer(ticket) == class="num">0) break;
CreateIndicator(ticket, it);
if ((it == IT_PENDING) || (it == IT_RESULT))
PositionAxlePrice(ticket, it, m_Selection.pr);
ChartRedraw();
m_TradeLine.SpotLight();
m_Selection.ticket = class="num">0;
UpdateIndicators(ticket, m_Selection.tp, m_Selection.sl, m_Selection.vol, m_Selection.bIsBuy);
}
break;

图表交互事件怎么驱动指标刷新

在 MT5 的 EA 或指标里,图表事件分发了用户操作与窗口变化,写好这几个 case 才能让浮动面板跟着鼠标走。 下面这段是从事件分支里抠出来的核心片段,覆盖编辑结束、窗口重绘、对象点击三类动作。 case CHARTEVENT_OBJECT_ENDEDIT: macroGetDataIndicatorFloat; m_Selection.ticket = 0; UpdateIndicators(def_IndicatorFloat, m_Selection.tp, m_Selection.sl, m_Selection.vol, m_Selection.bIsBuy); break; case CHARTEVENT_CHART_CHANGE: ReDrawAllsIndicator(); break; case CHARTEVENT_OBJECT_CLICK: if (GetIndicatorInfos(sparam, ticket, it, ev)) switch (ev) { case EV_TYPE: if (ticket == def_IndicatorFloat) { macroGetDataIndicatorFloat; 逐行拆一下:ENDEDIT 触发后先取浮动指标数据,把选中 ticket 清 0,再带止盈止损手数方向重算指标;CHART_CHANGE 直接全量重绘,避免缩放后图形错位;OBJECT_CLICK 里用 GetIndicatorInfos 拿到被点对象信息,命中 EV_TYPE 且 ticket 等于浮动指标时才读数据。 实盘里若漏掉 CHARTEVENT_CHART_CHANGE 的重绘,黄金 1 分钟图拖动后画线漂移的概率明显上升,建议直接粘去 MT5 测一遍。外汇与贵金属波动剧烈,事件驱动逻辑仅降低界面不同步风险,不预示任何方向。

MQL5 / C++
case CHARTEVENT_OBJECT_ENDEDIT:
  macroGetDataIndicatorFloat;
  m_Selection.ticket = class="num">0;
  UpdateIndicators(def_IndicatorFloat, m_Selection.tp, m_Selection.sl, m_Selection.vol, m_Selection.bIsBuy);
  break;
case CHARTEVENT_CHART_CHANGE:
  ReDrawAllsIndicator();
  break;
case CHARTEVENT_OBJECT_CLICK:
  if (GetIndicatorInfos(sparam, ticket, it, ev)) class="kw">switch (ev)
  {
  case EV_TYPE:
    if (ticket == def_IndicatorFloat)
    {
      macroGetDataIndicatorFloat;

「挂单触发后的止盈止损换算」

在半自动交易面板里,当一笔挂单被市场触碰成交后,系统需要把相对开仓价的止盈止损距离,重算成绝对价格并记录进选择结构体。下面这段逻辑就干了这件事:若 tp 或 sl 原值为 0 表示不设置,则保持 0;否则用开仓价 pr 加减「原距离绝对值 × 方向系数」得到真实挂单价。 方向系数由买卖标志 bIsBuy 决定:买入时止盈在 pr 上方(系数 +1)、止损在 pr 下方(系数 -1);卖出则完全反过来。这样无论用户当初是按点数还是按价格填的,成交后都能统一成 MT5 订单所需的绝对价位。 换算完立刻把 ticket 清零、调用 UpdateIndicators 刷新浮动指标面板,并 break 退出事件分支;若条件不满足则翻转按钮状态不做处理。外汇与贵金属杠杆高,这类自动换算若符号写反,可能让止损变止盈,实盘前务必在策略测试器用 1 手 XAUUSD 跑一遍验证。

MQL5 / C++
m_Selection.tp = (m_Selection.tp == class="num">0 ? class="num">0 : m_Selection.pr + (MathAbs(m_Selection.tp - m_Selection.pr) * (m_Selection.bIsBuy ? class="num">1 : -class="num">1)));
m_Selection.sl = (m_Selection.sl == class="num">0 ? class="num">0 : m_Selection.pr + (MathAbs(m_Selection.sl - m_Selection.pr) * (m_Selection.bIsBuy ? -class="num">1 : class="num">1)));
m_Selection.ticket = class="num">0;
UpdateIndicators(def_IndicatorFloat, m_Selection.tp, m_Selection.sl, m_Selection.vol, m_Selection.bIsBuy);
} else m_BtnInfoType.SetStateButton(sparam, !m_BtnInfoType.GetStateButton(sparam));
break;
case EV_DS:
if (ticket != def_IndicatorFloat) m_BtnInfo_DS.SetStateButton(sparam, !m_BtnInfo_DS.GetStateButton(sparam));
break;
case EV_CLOSE:

◍ 按票据类型分流清理挂单与持仓

EA 在收到交易服务器返回码后,需要依据票据身份决定是撤单还是平仓。下面这段分支逻辑把指标类与订单类分开处理,避免误删正在运行的指标句柄。 若 ticket 等于 def_IndicatorFloat,直接走 RemoveIndicator 移除指标;否则调用 GetInfosTradeServer 拿返回码 cRet,非 0 才进入 switch 分流。 在 IT_PENDING 与 IT_RESULT 分支里,cRet 小于 0 表示拒单或失效,执行 RemoveOrderPendent 撤挂单;否则 ClosePosition 平掉对应持仓。IT_TAKE 与 IT_STOP 分支则把 ticket 和类型 it 写进 m_Selection,交给后续模块处理。 开 MT5 把这段塞进你的交易事件回调,改 def_IndicatorFloat 常量即可验证不同票据的清理路径。外汇与贵金属杠杆高,误撤或误平可能造成滑点损失,调试时先用策略测试器跑历史Tick。

MQL5 / C++
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);
      break;
   case IT_TAKE:
   case IT_STOP:
      m_Selection.ticket = ticket;
      m_Selection.it = it;

图表对象拖拽与悬浮数据的事件分流

这段片段处在自定义图表交互的 switch 分支里,处理的是用户把指标线拖到图上(EV_MOVE)以及状态校验(EV_CHECK)两类事件。 当事件类型是 EV_MOVE 且 ticket 等于 def_IndicatorFloat 时,先通过宏 macroGetDataIndicatorFloat 抓取浮动指标的数值,再把 ticket 和迭代器 it 写进 m_Selection 结构,等于锁定了「当前被选中的那个指标对象」。 如果 ticket 不是预设的浮动指标,就走 CreateGhostIndicator(ticket, it) 当场生成一个幽灵指标副本,避免主图表被重复对象污染。 注意 SetPriceSelection(0) 后直接 break,意味着价格选择槽被清零并跳出,这种写法在 MT5 自定义事件循环里常用于「拖完即止」,你可开 MT5 把这段塞进 CChartObject 派生类的事件回调里验证断点是否如预期只触发一次。外汇与贵金属品种波动剧烈,这类交互逻辑若误触可能瞬间挂多个对象,实盘前务必在策略测试器跑一遍。

MQL5 / C++
SetPriceSelection(class="num">0);
break;
}
break;
case EV_MOVE:
if (ticket == def_IndicatorFloat)
{
macroGetDataIndicatorFloat;
m_Selection.ticket = ticket;
m_Selection.it = it;
}else  CreateGhostIndicator(ticket, it);
break;
case EV_CHECK:

「挂单与浮动指标的清理分支」

这段逻辑处理的是当 ticket 不等于 def_IndicatorFloat 时的界面与订单状态同步。若当前 ticket 对应的是浮动指标对象,先判断该 ticket 是否已有挂单驻留。 如果 PendingAtFloat(ticket) 返回真,说明挂单还挂在浮动指标上,直接调用 RemoveOrderPendent(ticket) 撤掉;否则把对应按钮状态置为 true,保留挂单待人工确认。 当 ticket 等于 def_IndicatorFloat 的 else 分支里,先展开 macroGetDataIndicatorFloat 宏抓取浮动指标数据,再把 m_Selection 的 ticket 指向 def_IndicatorTicket0、类型标为 IT_PENDING,并用 SetPriceSelection 回填价格,最后 RemoveIndicator 清掉浮动指标本体。外汇与贵金属品种下这类挂单清理涉及实时盘口,属于高风险操作,建议在 MT5 策略测试器用历史 tick 先跑一遍确认分支覆盖。

MQL5 / C++
if (ticket != def_IndicatorFloat)
            {
               if (PendingAtFloat(ticket)) RemoveOrderPendent(ticket);
               else m_BtnCheck.SetStateButton(macroMountName(ticket, IT_PENDING, EV_CHECK), true);
            } else
            {
               macroGetDataIndicatorFloat;
               m_Selection.ticket = def_IndicatorTicket0;
               m_Selection.it = IT_PENDING;
               SetPriceSelection(m_Selection.pr);
               RemoveIndicator(def_IndicatorFloat);
            }
            break;

◍ 图表点击后的止损止盈重算逻辑

在 CHARTEVENT_OBJECT_CLICK 分支里,面板靠 GetIndicatorInfos 拿到被点对象名 sparam、ticket 与事件类型 ev,再用 switch(ev) 分流。若 ev 为 EV_TYPE 且 ticket 等于 def_IndicatorFloat,说明用户点的是浮动手数指示块,这时要走一段特殊的重算。 macroGetDataIndicatorFloat 先把当前选择项的 tp、sl、pr、vol 和买卖方向读进 m_Selection。随后三行是核心:tp 若非 0,则按买/卖方向用 MathAbs 重算离场价——买入时加正差、卖出时加负差;sl 同理但方向相反;ticket 清 0 表示脱离原单据。 最后调 UpdateIndicators 把重算后的 tp、sl、vol 和 bIsBuy 刷回面板。若 ticket 不是 def_IndicatorFloat,仅翻转对应按钮状态,不碰价格。外汇与贵金属杠杆高,这类点击重算只改界面参考位,不代表任何成交承诺,实盘前请在 MT5 策略测试器逐步断点验证。

MQL5 / C++
            break;
         }
      }
class="macro">#undef macroGetDataIndicatorFloat
case CHARTEVENT_OBJECT_CLICK:
      if (GetIndicatorInfos(sparam, ticket, it, ev)) class="kw">switch (ev)
      {
         case EV_TYPE:
               if (ticket == def_IndicatorFloat)
               {
                  macroGetDataIndicatorFloat;
                  m_Selection.tp = (m_Selection.tp == class="num">0 ? class="num">0 : m_Selection.pr + (MathAbs(m_Selection.tp - m_Selection.pr) * (m_Selection.bIsBuy ? class="num">1 : -class="num">1)));
                  m_Selection.sl = (m_Selection.sl == class="num">0 ? class="num">0 : m_Selection.pr + (MathAbs(m_Selection.sl - m_Selection.pr) * (m_Selection.bIsBuy ? -class="num">1 : class="num">1)));
                  m_Selection.ticket = class="num">0;
                  UpdateIndicators(def_IndicatorFloat, m_Selection.tp, m_Selection.sl, m_Selection.vol, m_Selection.bIsBuy);
               } else m_BtnInfoType.SetStateButton(sparam, !m_BtnInfoType.GetStateButton(sparam));
               break;
         case EV_DS:
               if (ticket != def_IndicatorFloat) m_BtnInfo_DS.SetStateButton(sparam, !m_BtnInfo_DS.GetStateButton(sparam));
               break;
class=class="str">"cmt">// ... Rest of the code...

别急着下结论

这套订单系统眼下能处理挂单、改仓、撤单等多类动作,对实盘流程已有实质帮助,但作者明确留了一个关键细节没补——具体是什么,原文说要等下一篇才落地。 从已释放的附件看,EA_-_Em_diretco_ao_Futuro_q_II_o.zip 体积约 12 MB(12035.84 KB),说明工程里不光有主逻辑,还带了较完整的支撑模块,直接拖进 MT5 的 MetaEditor 能跑通基础框架。 外汇和贵金属杠杆高、滑点突发的情形多,这类半成型的订单系统只适合在策略测试器里先用历史数据验边界,别急着接真实账户。等下一篇把缺失环节补齐,再回头比对这套 zip 里的函数调用链,才可能看出它到底卡在哪。

把盘口意图交给小布读
这些订单与持仓的视觉标识逻辑,小布盯盘已内置到对应品种页,打开就能看到系统自动标注的短线或长线状态,你只管决策要不要跟。

常见问题

可在订单指标里加入位图元素,用不同图形或颜色标识买卖方向,不需依赖工具箱文字,看图即知。
需修改订单系统到期时间逻辑,把长线持仓标记为跳过当日平仓规则,并在指标上显式标注避免误读。
位图体积可控且直观,合理布局下只占入场点附近小区域,比文字标签更省空间。
可以,小布盯盘对应品种页会自动解析订单系统状态并标注持仓预期周期,省去手动判断。
原系统固化了单笔订单当日平仓的假设,缺乏到期类型字段,长线持仓会被错误流程干预。