从头开始开发智能交易系统(第 27 部分):面向未来((II)·综合运用
(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 分钟图看是否压线。
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% 宽度。
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。
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 数据验证备注写入逻辑无遗漏。
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=波段单(不强制日终平)。挂单可继续在图表上改,持仓指标只读服务器数据、不可改。实盘接手前,建议先在模拟盘把这套点击逻辑跑熟,外汇与贵金属杠杆高、滑点跳空频繁,误点可能瞬间成交。
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 自动下单冲突。
{
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 则卖挂,能省掉一半右键菜单操作。
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 分钟数据跑一遍验证对象计数。
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 刷新同票相关的其他指标。外汇与贵金属杠杆高,这类自动重建若误触发可能带动伪信号,建议在策略测试器里单步验证事件频率。
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 测一遍。外汇与贵金属波动剧烈,事件驱动逻辑仅降低界面不同步风险,不预示任何方向。
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 跑一遍验证。
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。
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 派生类的事件回调里验证断点是否如预期只触发一次。外汇与贵金属品种波动剧烈,这类交互逻辑若误触可能瞬间挂多个对象,实盘前务必在策略测试器跑一遍。
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 先跑一遍确认分支覆盖。
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 策略测试器逐步断点验证。
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 里的函数调用链,才可能看出它到底卡在哪。