从头开始开发智能交易系统(第 18 部分):新订单系统 (I)·综合运用
「挂单与持仓的价格边界判定」
这段逻辑处理的是已含持仓状态下,对挂单开仓价与止损止盈边界的校验。先通过 OrderGetDouble(ORDER_PRICE_OPEN) 取出挂单原始开仓价,若持仓对象 m_bContainsPosition 为真,则进入边界测试。 当止损与止盈均为 -1 时,bTest 直接置 false,意味着无边界限制就不做修正;否则按多空方向判断:空单要求 StopLoss 大于当前 price,多单要求 StopLoss 小于 price,若都不满足再检查 TP 与 SL 是否跨骑 price(最大值大于 price 且最小值小于 price)。 bTest 为真时调用 ModifyOrderPendent 把挂单价格修正为 price,并顺手 OrderSelect 刷新订单上下文。外汇与贵金属杠杆高,这类边界重算若写错方向,可能把止损放到不利于成交的一侧,建议在 MT5 策略测试器里用 EURUSD 5 分钟数据跑一遍验证。 最后三行 SetLineOrder 不管校验结果如何都会执行,分别把挂单价、TP、SL 画到图表上,便于肉眼核对水平线是否落在预期位置。
price = OrderGetDouble(ORDER_PRICE_OPEN); if (m_bContainsPosition) { if ((m_Limits.StopLoss == -class="num">1) && (m_Limits.TakeProfit == -class="num">1)) bTest = false; else { bTest = ((!m_Limits.IsBuy) && (m_Limits.StopLoss > price)); bTest = (bTest ? bTest : (m_Limits.IsBuy) && (m_Limits.StopLoss < price)); bTest = (bTest ? bTest : ((macro_MAX(m_Limits.TakeProfit, m_Limits.StopLoss) > price) && (macro_MIN(m_Limits.TakeProfit, m_Limits.StopLoss) < price))); } if (bTest) { ModifyOrderPendent(ul, price, class="num">0, class="num">0); (OrderSelect(ul) ? class="num">0 : class="num">0); } } SetLineOrder(ul, price, HL_PRICE, true); SetLineOrder(ul, OrderGetDouble(ORDER_TP), HL_TAKE, true); SetLineOrder(ul, OrderGetDouble(ORDER_SL), HL_STOP, true);
鼠标拖拽挂单时的限价保留逻辑
C_OrderView 里真正要改的只有一处:创建挂单时,是否沿用持仓已有的止盈止损限价。每次都自动带进去很诱人,但实际操作里得看你是想重画一条线还是延续旧仓位参数,这个选择直接影响后续路由类的刷新行为。 剩下的活儿全在 C_Router。订单池或挂单列表一变,MT5 自己的 OnTrade 就被触发,进而调 C_Router 的更新例程做调整。有一段让人抓狂的代码原本埋在更新函数里,后来被抽出来换成调用。 下面这段就是抽出来的 MoveTo 与 CheckLimits。它用位掩码判鼠标:0x01 左键、0x04 _SHIFT、0x08 _CTRL;当 SHIFT 与 CTRL 状态不同(即只按了一个)才隐藏鼠标并校验限价,否则显示鼠标。 [CODE] inline void MoveTo(uint Key) { static double local = 0; datetime dt; bool bEClick, bKeyBuy, bKeySell, bCheck; double take = 0, stop = 0, price; bEClick = (Key & 0x01) == 0x01; //Left click bKeyBuy = (Key & 0x04) == 0x04; //SHIFT pressed bKeySell = (Key & 0x08) == 0x08; //CTRL pressed Mouse.GetPositionDP(dt, price); if (bKeyBuy != bKeySell) { Mouse.Hide(); bCheck = CheckLimits(price); } else Mouse.Show(); ObjectMove(Terminal.Get_ID(), m_Infos.szHLinePrice, 0, 0, price = (bKeyBuy != bKeySell ? price : 0)); ObjectMove(Terminal.Get_ID(), m_Infos.szHLineTake, 0, 0, take = (bCheck ? 0 : price + (m_Infos.TakeProfit * (bKeyBuy ? 1 : -1)))); ObjectMove(Terminal.Get_ID(), m_Infos.szHLineStop, 0, 0, stop = (bCheck ? 0 : price + (m_Infos.StopLoss * (bKeyBuy ? -1 : 1)))); if((bEClick) && (bKeyBuy != bKeySell) && (local == 0)) CreateOrderPendent(bKeyBuy, m_Infos.Volume, local = price, take, stop, m_Infos.IsDayTrade); local = (local != price ? 0 : local); ObjectSetInteger(Terminal.Get_ID(), m_Infos.szHLinePrice, OBJPROP_COLOR, (bKeyBuy != bKeySell ? m_Infos.cPrice : clrNONE)); ObjectSetInteger(Terminal.Get_ID(), m_Infos.szHLineTake, OBJPROP_COLOR, (take > 0 ? m_Infos.cTake : clrNONE)); ObjectSetInteger(Terminal.Get_ID(), m_Infos.szHLineStop, OBJPROP_COLOR, (stop > 0 ? m_Infos.cStop : clrNONE)); }; #define macro_MAX(A, B) (A > B ? A : B) #define macro_MIN(A, B) (A < B ? A : B) inline bool CheckLimits(const double price) { bool bTest = false;
class="kw">inline class="type">void MoveTo(class="type">uint Key) { class="kw">static class="type">class="kw">double local = class="num">0; class="type">class="kw">datetime dt; class="type">bool bEClick, bKeyBuy, bKeySell, bCheck; class="type">class="kw">double take = class="num">0, stop = class="num">0, price; bEClick = (Key & 0x01) == 0x01; class=class="str">"cmt">//Left click bKeyBuy = (Key & 0x04) == 0x04; class=class="str">"cmt">//SHIFT pressed bKeySell = (Key & 0x08) == 0x08; class=class="str">"cmt">//CTRL pressed Mouse.GetPositionDP(dt, price); if (bKeyBuy != bKeySell) { Mouse.Hide(); bCheck = CheckLimits(price); } else Mouse.Show(); ObjectMove(Terminal.Get_ID(), m_Infos.szHLinePrice, class="num">0, class="num">0, price = (bKeyBuy != bKeySell ? price : class="num">0)); ObjectMove(Terminal.Get_ID(), m_Infos.szHLineTake, class="num">0, class="num">0, take = (bCheck ? class="num">0 : price + (m_Infos.TakeProfit * (bKeyBuy ? class="num">1 : -class="num">1)))); ObjectMove(Terminal.Get_ID(), m_Infos.szHLineStop, class="num">0, class="num">0, stop = (bCheck ? class="num">0 : price + (m_Infos.StopLoss * (bKeyBuy ? -class="num">1 : class="num">1)))); if((bEClick) && (bKeyBuy != bKeySell) && (local == class="num">0)) CreateOrderPendent(bKeyBuy, m_Infos.Volume, local = price, take, stop, m_Infos.IsDayTrade); local = (local != price ? class="num">0 : local); ObjectSetInteger(Terminal.Get_ID(), m_Infos.szHLinePrice, OBJPROP_COLOR, (bKeyBuy != bKeySell ? m_Infos.cPrice : clrNONE)); ObjectSetInteger(Terminal.Get_ID(), m_Infos.szHLineTake, OBJPROP_COLOR, (take > class="num">0 ? m_Infos.cTake : clrNONE)); ObjectSetInteger(Terminal.Get_ID(), m_Infos.szHLineStop, OBJPROP_COLOR, (stop > class="num">0 ? m_Infos.cStop : clrNONE)); }; class="macro">#define macro_MAX(A, B) (A > B ? A : B) class="macro">#define macro_MIN(A, B) (A < B ? A : B) class="kw">inline class="type">bool CheckLimits(const class="type">class="kw">double price) { class="type">bool bTest = false;
◍ 持仓缺失与零止盈下的触发判定分支
这段逻辑出现在限价单触发检测的收口处,先过滤两类无效场景:当前账户没有对应持仓,或止损、止盈同时被设为 -1(代表不限制),此时直接返回既有的 bTest 布尔值,不再做价格区间比对。 若通过了上面的过滤,就用 macro_MAX 与 macro_MIN 取出止损、止盈中的较大值与较小值,只有当实时 price 落在二者之间时,bTest 才置真——这意味着价格必须穿透限价区间才认定为可触发。 当 TakeProfit 显式等于 0 时,走特殊分支:若前面 bTest 已真则保持;否则按多空方向补判——空单要求 StopLoss 大于 price,多单要求 StopLoss 小于 price,相当于用止损位反向确认突破有效性。外汇与贵金属波动剧烈,这类边界值(0 与 -1)处理不当可能在 MT5 回测中漏掉关键信号,建议直接挂模拟盘核对三种组合。 结尾用 #undef 注销 macro_MAX / macro_MIN 两个宏,避免在同编译单元后续代码里发生命名污染,这是 MQL5 头文件式封装的常见收尾动作。
if ((!m_bContainsPosition) || ((m_Limits.StopLoss == -class="num">1) && (m_Limits.TakeProfit == -class="num">1))) class="kw">return bTest; bTest = ((macro_MAX(m_Limits.TakeProfit, m_Limits.StopLoss) > price) && (macro_MIN(m_Limits.TakeProfit, m_Limits.StopLoss) < price)); if (m_Limits.TakeProfit == class="num">0) { bTest = (bTest ? bTest : (!m_Limits.IsBuy) && (m_Limits.StopLoss > price)); bTest = (bTest ? bTest : (m_Limits.IsBuy) && (m_Limits.StopLoss < price)); } class="kw">return bTest; class="macro">#undef macro_MAX class="macro">#undef macro_MIN
「OCO 挂单转持仓后限价丢失的兜底逻辑」
在 MT5 里跑过 OCO 挂单的人大概率撞过这个坑:挂单触发成持仓后,原本绑定的限价会直接掉没,而且这是必然发生的,不是偶发 bug。若此时持仓被手动改过限价,或原 OCO 已退化成简单订单并越过了原限价区间,它就一直以松散简单单存在,高波动时段可能在你反应过来前就吃掉一大段净值——外汇和贵金属这种高杠杆品种尤其凶险。 EA 面对这种情况只有两条路:要么弹警告让交易者自己处理,要么自动按图表里设的财务值补限价、把简单单重新锁成 OCO。我在路由类里加了 user27 这个开关(默认 true),由交易者决定 EA 是否介入兜底。 代码里 OnTrade 会调 UpdatePosition(user27),高亮行就是判断「订单是否自由单 + EA 是否允许干预」。若介入,就按 user21(止盈财务值 100)、user22(止损财务值 75)和挂单交易量算位移,把简单挂单补成 OCO。GetDisplacementTake / GetDisplacementStop 两个内联函数用 FinanceTake*Vol/Contract*调整系数 算价格偏移,买单调正向、卖单调反向。 别把自动补限价当万能 user27 开 true 时 EA 会自行写限价并留日志,但财务值 100/75 是示例参数,实盘得按品种点值和你的风险额度重设;杠杆因子 user20 也会放大偏移,贵金属跳空时补单可能偏离预期,建议先在策略测试器用历史数据验一遍再上真仓。
class=class="str">"cmt">// ... Code ... class="kw">input group "Chart Trader" class="kw">input class="type">int user20 = class="num">1; class=class="str">"cmt">//Leverage factor class="kw">input class="type">int user21 = class="num">100; class=class="str">"cmt">//Take Profit(financial) class="kw">input class="type">int user22 = class="num">75; class=class="str">"cmt">//Stop Loss(financial) class="kw">input class="type">class="kw">color user23 = clrBlue; class=class="str">"cmt">//Price line class="type">class="kw">color class="kw">input class="type">class="kw">color user24 = clrForestGreen; class=class="str">"cmt">//Take Profit line class="type">class="kw">color class="kw">input class="type">class="kw">color user25 = clrFireBrick; class=class="str">"cmt">//Stop Loss line class="type">class="kw">color class="kw">input class="type">bool user26 = true; class=class="str">"cmt">//Day Trade? class="kw">input class="type">bool user27 = true; class=class="str">"cmt">//Always set loose order limits class=class="str">"cmt">// ... Rest of the code... class="type">void OnTrade() { Chart.DispatchMessage(CHARTEVENT_CHART_CHANGE, class="num">0, OrderView.UpdateRoof(), C_Chart_IDE::szMsgIDE[C_Chart_IDE::eROOF_DIARY]); OrderView.UpdatePosition(user27); } class=class="str">"cmt">// ... Rest of the code... class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void SetFinance(const class="type">int Contracts, const class="type">int Take, const class="type">int Stop) { m_Limits.Contract = Contracts; m_Limits.FinanceTake = Take; m_Limits.FinanceStop = Stop; } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">class="kw">double GetDisplacementTake(const class="type">bool IsBuy, const class="type">class="kw">double Vol) const { class="kw">return (Terminal.AdjustPrice(m_Limits.FinanceTake * (Vol / m_Limits.Contract) * Terminal.GetAdjustToTrade() / Vol) * (IsBuy ? class="num">1 : -class="num">1)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">inline class="type">class="kw">double GetDisplacementStop(const class="type">bool IsBuy, const class="type">class="kw">double Vol) const { class="kw">return (Terminal.AdjustPrice(m_Limits.FinanceStop * (Vol / m_Limits.Contract) * Terminal.GetAdjustToTrade() / Vol) * (IsBuy ? -class="num">1 : class="num">1)); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void UpdatePosition(class="type">bool bAdjust) {
持仓变动时重画挂单与止损线
EA 在每帧 tick 里先比对内存中的持仓数 memPositions 与挂单数 memOrder 和实时 PositionsTotal()、OrdersTotal() 的差异。一旦检测到数量变化,就临时关闭图表的对象删除事件、清掉所有画线再恢复,避免旧持仓的横线残留干扰判断。 清场后会重置几个关键状态:m_bContainsPosition 置 false,m_Limits.StopLoss 与 TakeProfit 都设回 -1,方向 IsBuy 也清空。这样下一轮循环重新统计时,极限止损止盈值一定是从当前真实持仓算出来的。 随后用 for 从 p 往下扫,只挑出 Symbol 匹配的持仓。每张仓读出 ticket、开仓价、TP、SL,分别用 SetLineOrder 画 HL_PRICE / HL_TAKE / HL_STOP 三类线;同时按多空分别取 TP 的最大值、SL 的最小值更新进 m_Limits,供后续逻辑参考。 外汇和贵金属杠杆高,持仓线只是辅助可视化,重画逻辑再严谨也不能替代仓位风控,实际触发仍以经纪商成交为准。
class="kw">static class="type">int memPositions = class="num">0, memOrder = class="num">0; class="type">class="kw">ulong ul; class="type">int p, o; class="type">long info; class="type">class="kw">double price, stop, take, vol; class="type">bool bIsBuy, bTest; p = PositionsTotal(); o = OrdersTotal(); if ((memPositions != p) || (memOrder != o)) { ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false); RemoveAllsLines(); ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true); memOrder = o; memPositions = p; m_bContainsPosition = false; m_Limits.StopLoss = -class="num">1; m_Limits.TakeProfit = -class="num">1; m_Limits.IsBuy = false; }; for(class="type">int i0 = p; i0 >= class="num">0; i0--) if(PositionGetSymbol(i0) == Terminal.GetSymbol()) { ul = PositionGetInteger(POSITION_TICKET); m_bContainsPosition = true; SetLineOrder(ul, PositionGetDouble(POSITION_PRICE_OPEN), HL_PRICE, false); SetLineOrder(ul, take = PositionGetDouble(POSITION_TP), HL_TAKE, true); SetLineOrder(ul, stop = PositionGetDouble(POSITION_SL), HL_STOP, true); m_Limits.IsBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY; m_Limits.TakeProfit = (m_Limits.TakeProfit < class="num">0 ? take : (m_Limits.IsBuy ? (m_Limits.TakeProfit > take ? m_Limits.TakeProfit : take) : (take > m_Limits.TakeProfit ? m_Limits.TakeProfit : take))); m_Limits.StopLoss = (m_Limits.StopLoss < class="num">0 ? stop : (m_Limits.IsBuy ? (m_Limits.StopLoss < stop ? m_Limits.StopLoss : stop) : (stop < m_Limits.StopLoss ? m_Limits.StopLoss : stop))); } if ((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_TRADE_MODE) == ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
◍ 挂单的止损止盈自动校准逻辑
这段循环在 MT5 的 EA 里负责遍历当前品种的全部挂单与市价单,把没有设 TP/SL 且触发调整条件的单子自动补上位移止损止盈。先通过 OrderGetTicket 倒序取单,再用 OrderGetString(ORDER_SYMBOL) 比对 Terminal.GetSymbol(),只处理本图表品种,避免误改其他品种仓位。 核心判断在 bTest = CheckLimits(price) 之后:若原单 take 和 stop 都为 0、允许调整且价格不满足限制,就按买卖方向调用 GetDisplacementTake / GetDisplacementStop 算出偏移,再用 ModifyOrderPendent 改写挂单。反过来,若已有双线但 bTest 为真(价格越界),则把 TP、SL 清零只留价格线。 最后三行 SetLineOrder 不管改没改,都会重绘价格线、止盈线、止损线到图表上。外汇与贵金属杠杆高,自动改单可能在快速波动中失效,建议在策略测试器用 2023 年 XAUUSD 的 M1 数据跑一遍,观察 ModifyOrderPendent 的调用频次再决定偏移参数。
m_bContainsPosition = false; for (class="type">int i0 = o; i0 >= class="num">0; i0--) if ((ul = OrderGetTicket(i0)) > class="num">0) if (OrderGetString(ORDER_SYMBOL) == Terminal.GetSymbol()) { price = OrderGetDouble(ORDER_PRICE_OPEN); take = OrderGetDouble(ORDER_TP); stop = OrderGetDouble(ORDER_SL); bTest = CheckLimits(price); if ((take == class="num">0) && (stop == class="num">0) && (bAdjust) && (!bTest)) { info = OrderGetInteger(ORDER_TYPE); vol = OrderGetDouble(ORDER_VOLUME_CURRENT); bIsBuy = ((info == ORDER_TYPE_BUY_LIMIT) || (info == ORDER_TYPE_BUY_STOP) || (info == ORDER_TYPE_BUY_STOP_LIMIT) || (info == ORDER_TYPE_BUY)); take = price + GetDisplacementTake(bIsBuy, vol); stop = price + GetDisplacementStop(bIsBuy, vol); ModifyOrderPendent(ul, price, take, stop); } if ((take != class="num">0) && (stop != class="num">0) && (bTest)) ModifyOrderPendent(ul, price, take = class="num">0, stop = class="num">0); SetLineOrder(ul, price, HL_PRICE, true); SetLineOrder(ul, take, HL_TAKE, true); SetLineOrder(ul, stop, HL_STOP, true); }
「对冲账户上的挂单识别坑」
实测下来,这套 EA 在识别账户结算模式时并不可靠。作者在 MT5 终端明确看到账户为对冲模式,但 EA 内部始终回报为净持结算(netting),导致挂单调整逻辑按净持账户处理,即便底层是对冲账户也一样。 视频记录里能直接看到这个偏差:EA 照常运行、下单动作看起来没问题,但订单管理层的判定从根上就错了。外汇与贵金属交易本身杠杆高、滑点凶,这种底层模式误判可能让对冲仓位被错误平掉或重复调整,实盘前务必在策略测试器里手动核对 AccountInfoInteger(ACCOUNT_MARGIN_MODE) 与终端显示是否一致。 如果你打算沿用这套源码,别急着挂真仓。先开一个 MT5 模拟对冲账户,把 EA 日志里打印的账户模式字段和平台信息并排比对,确认不再报 netting 再谈下一步。