从头开始开发一款智能交易系统·进阶篇
📘

从头开始开发一款智能交易系统·进阶篇

第 2/2 篇

用鼠标按键组合挂出 pending 单的底层逻辑

在 MT5 图表事件里,左键点击配合 Shift 或 Ctrl 能区分买卖意图:代码用位运算把 Key 与 0x01、0x04、0x08 做与操作,分别识别左键、Shift、Ctrl。Shift 对应买入倾向,Ctrl 对应卖出倾向,两者不能同时生效,否则不画线也不下单。 ChartXYToTimePrice 把鼠标坐标 X、Y 转成时间和价格,随后三条 ObjectMove 把水平线移到对应价位:价格线取 AdjustPrice 修正后的值,止盈线在价格基础上加 TakeProfit 乘以方向系数,止损线则减 StopLoss 乘以方向系数。若 bKeyBuy 与 bKeySell 相等,price 直接置 0,三根线全部隐藏。 真正发单只看 bEClick 且买卖键互斥这一条件,调用 CreateOrderPendent 时把计算出的 price、take、stop 传进去。最后三句 ObjectSetInteger 按 take、stop 是否大于 0 切换线的颜色,clrNONE 表示不显示。外汇与贵金属杠杆高,挂单触发前价格可能跳空,实际成交价与画线价可能有偏差,验证时建议先开模拟盘跑这套事件回调。

MQL5 / C++
class="type">class="kw">double take = class="num">0, stop = class="num">0, price;
bEClick  = (Key & 0x01) == 0x01;      class=class="str">"cmt">//Left mouse button click
bKeyBuy  = (Key & 0x04) == 0x04;      class=class="str">"cmt">//Pressed SHIFT
bKeySell = (Key & 0x08) == 0x08;      class=class="str">"cmt">//Pressed CTRL
ChartXYToTimePrice(m_Infos.Id, X, Y, w, dt, price);
ObjectMove(m_Infos.Id, m_Infos.szHLinePrice, class="num">0, class="num">0, price = (bKeyBuy != bKeySell ? AdjustPrice(price) : class="num">0));
ObjectMove(m_Infos.Id, m_Infos.szHLineTake, class="num">0, class="num">0, take = price + (m_Infos.TakeProfit * (bKeyBuy ? class="num">1 : -class="num">1)));
ObjectMove(m_Infos.Id, m_Infos.szHLineStop, class="num">0, class="num">0, stop = price + (m_Infos.StopLoss * (bKeyBuy ? -class="num">1 : class="num">1)));
if((bEClick) && (bKeyBuy != bKeySell))
   CreateOrderPendent(bKeyBuy, m_Infos.Volume, price, take, stop, m_Infos.IsDayTrade);
ObjectSetInteger(m_Infos.Id, m_Infos.szHLinePrice, OBJPROP_COLOR, (bKeyBuy != bKeySell ? m_Infos.cPrice : clrNONE));
ObjectSetInteger(m_Infos.Id, m_Infos.szHLineTake, OBJPROP_COLOR, (take > class="num">0 ? m_Infos.cTake : clrNONE));
ObjectSetInteger(m_Infos.Id, m_Infos.szHLineStop, OBJPROP_COLOR, (stop > class="num">0 ? m_Infos.cStop : clrNONE));

◍ 跳空让 OCO 止损止盈集体失效

OCO 挂单的致命点不在 MT5 系统bug,而在行情本身。理论价格连续波动,但高波动时烛条内部会出现跳空缺口;若缺口正好落在 SL 或 TP 价位,这两单都不会被触发,持仓就这么裸露着。 更麻烦的是,用户手动移动止损止盈后,价格可能已冲出走廊区间,原 OCO 逻辑同样不平仓。外汇和贵金属杠杆高、跳空频繁,这种失控状态无法预测,只能靠程序兜底。 我们在 OnTrade 里调 UpdatePosition 刷新持仓数据,在 OnTick 里跑 CheckPosition 把价格锁回走廊。OnTick 每段报价都执行,代码必须极简,否则拖慢终端。 ClosePosition 能按指定交易量平仓,部分平仓可降低爆仓概率;但它跑在客户端,若与服务器断连就完全无效,实盘前务必测断网表现。

MQL5 / C++
  class="type">void UpdatePosition(class="type">void)
    {
      for(class="type">int i0 = PositionsTotal() - class="num">1; i0 >= class="num">0; i0--)
        if(PositionGetSymbol(i0) == m_szSymbol)
          {
            m_Take      = PositionGetDouble(POSITION_TP);
            m_Stop      = PositionGetDouble(POSITION_SL);
            m_IsBuy     = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY;
            m_Volume    = PositionGetDouble(POSITION_VOLUME);
            m_Ticket    = PositionGetInteger(POSITION_TICKET);
          }
    };
  class="kw">inline class="type">bool CheckPosition(class="kw">const class="type">class="kw">double price = class="num">0, class="kw">const class="type">int factor = class="num">0)
    {
      class="type">class="kw">double last;
      if(m_Ticket == class="num">0)
        class="kw">return false;
      last = SymbolInfoDouble(m_szSymbol, SYMBOL_LAST);
      if(m_IsBuy)
        {
         if((last > m_Take) || (last < m_Stop))
            class="kw">return ClosePosition();
         if((price > class="num">0) && (price >= last))
            class="kw">return ClosePosition(factor);
        }
      else
        {
         if((last < m_Take) || (last > m_Stop))
            class="kw">return ClosePosition();
         if((price > class="num">0) && (price <= last))
            class="kw">return ClosePosition(factor);
        }
      class="kw">return false;
    };
  class="type">bool ClosePosition(class="kw">const class="type">int arg = class="num">0)
    {
      class="type">class="kw">double v1 = arg * m_VolMinimal;
      if(!PositionSelectByTicket(m_Ticket))
        class="kw">return false;
      ZeroMemory(TradeRequest);
      ZeroMemory(TradeResult);
      TradeRequest.action      = TRADE_ACTION_DEAL;
      TradeRequest.type        = (m_IsBuy ? ORDER_TYPE_SELL : ORDER_TYPE_BUY);

「平仓请求里的价格与手数细节」

下面这段 MQL5 代码展示了一个持仓平仓请求的核心字段填充逻辑,直接在 MT5 的 EA 源码里替换 m_szSymbol、m_Ticket 等变量就能跑通。 价格字段按多空方向取即时 BID 或 ASK:买入持仓平掉时吃 BID,卖出持仓平掉时吃 ASK,这是避免开平仓价错位的底线。 手数做了防溢出判断——传入的 v1 若为 0 或大于原持仓 m_Volume,就退回全平;否则按 v1 部分平仓。deviation 写死 1000 点,意味着允许极端滑点,实盘外汇或贵金属高波动时段可能以偏差较大的价格成交,风险显著。 OrderSend 失败会弹窗报错误码并中断,成功则把 m_Ticket 清零表示仓位已不在。

MQL5 / C++
      TradeRequest.price       = SymbolInfoDouble(m_szSymbol, (m_IsBuy ? SYMBOL_BID : SYMBOL_ASK));
      TradeRequest.position  = m_Ticket;
      TradeRequest.symbol    = m_szSymbol;
      TradeRequest.volume    = ((v1 == class="num">0) || (v1 > m_Volume) ? m_Volume : v1);
      TradeRequest.deviation = class="num">1000;
      if(!OrderSend(TradeRequest, TradeResult))
        {
         MessageBox(StringFormat("Error Number: %d", TradeResult.retcode), "Nano EA");
         class="kw">return false;
        }
      else
         m_Ticket = class="num">0;
      class="kw">return true;
   };

部分平仓的坑与务实做法

部分订单在 MT5 里能实现,但通用方案极难写。动态数组在 EA 运行期存状态,一旦日间交易中途关掉 EA,数组清空,逻辑就断了;真要做,得用文件或全局变量这类持久介质,格式看你后续怎么消费。 用开仓反向单做部分平仓,风险比想象中大。举例:3 倍杠杆多单,想留 1 倍、锁 2 倍盈利,若发市价卖单,波动可能在成交前推涨触发止盈,EA 反手开出新空单;挂 Sell Limit/Stop 看似稳,但若触价前另有订单进来,持仓被平掉后稍晚又开仓,损失可能扩大,强波动下和前面市价单的情形一样糟。 作为程序员视角,最稳的模仿方式是发市价单做部分平,但必须卡死不超过原持仓量。本 EA 就是这么处理的,你也可以另写逻辑,但开仓平仓的边界条件建议先在策略测试器里跑极端滑点场景验证。外汇和贵金属杠杆品种波动剧烈,部分平仓失误会带来额外亏损,属高风险操作。

◍ 把下单交给 MT5 自己跑

这套 EA 的核心思路不是替你决策,而是把「挂单动作」封装好之后,立刻交还给 MetaTrader 5 终端去执行。代码里不写平仓、不写加仓,只做一件事:按你给的合约数和品种,自动换算点数把单子挂出去。从作者附的 mq5 文件看,EA_Nano_rvl_1.1.mq5 体积仅 23.44 KB,轻量到几乎不占用运行时资源。 评论区有用户问迷你指数的点数和手数在哪设,作者回复说调整是自动的——你告诉 EA 品种和合约数,它用点数自适应,不用手动填。这印证了「让终端干活」的设计:EA 只下指令,后续由 MT5 原生机制接管,所以稳定性来自平台而非策略逻辑。 外汇和贵金属杠杆高、滑点随机,这种只挂单不控盘的辅助工具能降低操作失误,但无法消除行情反向风险。拿 23.44 KB 的源码去 MT5 回测一次,比读十篇概述更有用。

常见问题

底层逻辑依赖鼠标按键事件与图表坐标换算,若点击位置不在可挂单区域或按键组合被占用,请求会被丢弃。先在模拟盘用简单组合键验证事件响应,再固化自己的快捷键。
跳空价位跳过挂单触发区间,OCO 关联可能因价格断层集体失效。务实做法是跳空高发时段手动复核挂单,或改用市价单加后续止损。
小布可监控品种跳空并提示 OCO 异常,也能把鼠标挂单逻辑简化为语音或快捷键指令,减少手动操作失误。
若请求里手数写满或超出剩余持仓,系统按剩余量处理,但部分平仓需明确小于总仓。务必在代码里用 PositionGetDouble 校验可用手数再发请求。
自动下单走的是当时盘口可得价格,滑点和高频波动会让成交偏离预期。回测之外用模拟盘实跑,记录请求价与成交价差值来调阈值。