从头开始开发一款智能交易系统·进阶篇
用鼠标按键组合挂出 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 表示不显示。外汇与贵金属杠杆高,挂单触发前价格可能跳空,实际成交价与画线价可能有偏差,验证时建议先开模拟盘跑这套事件回调。
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 能按指定交易量平仓,部分平仓可降低爆仓概率;但它跑在客户端,若与服务器断连就完全无效,实盘前务必测断网表现。
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 清零表示仓位已不在。
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 回测一次,比读十篇概述更有用。