MQL5自动化交易策略(第十九部分):包络线趋势反弹剥头皮交易交易执行与风险管理(下篇)·综合运用
(3/3)· 承接前两篇架构与信号基础,本篇把下单、止损与仓位保护真正跑通,EA 才算能上盘
- 一级卖信号类的触发逻辑拆解
- 一级买信号里的订单互斥与均线包络判定
- 仓位与信号阈值的输入参数骨架
- 手数归一与交易指令枚举
- 用接口把策略模块拆开跑
- 平仓判定里的信号与手动止损切线
- 部分平仓的触发优先级怎么排
- 重入拦截与部分平仓的订单拆解逻辑
- 策略评估与模块注册的执行骨架
- 开仓模块的订单对象初始化逻辑
- 建仓模块的初始值与动作分发
- 动作数组的增量扩容与开仓需求屏蔽
- 多模块信号怎么合并成一张指令
- 多单模块里的开仓需求合并逻辑
- 开仓信号如何被逐级汇总
- 多单信号过滤与双向挂单动作的生成逻辑
- 平仓信号的评估与合并逻辑
- 持仓上限如何抬升评估层级
- 平仓与开仓需求的合并返回逻辑
- 开仓前的保证金闸门与反手逻辑
- 需求层级封顶与历史信号补位
- 需求过滤与接口骨架
- EA 类的成员装配与析构清理
- EA 生命周期里的订单同步与tick调度
- EA 钱包与订单组的同步陷阱
- 手动改止损止盈怎么被 EA 抓到
- 持仓盈亏极值追踪与挂单平仓执行
- 净仓模式下的反向单与挂单执行
- 交易上下文阻塞时的等待与撤单兜底
- 开单失败后的回转与账户上下文读取
- 账户模式识别与每根K线只跑一次的逻辑
- 账户面板与成交回查的底层拼装
- 成交回执里把挂单翻成持仓
- 成交回报里的手数偏差处理
- 券商部分成交时的挂单残量处理
- 成交回写时清标志与撤图表线
- 撤掉EA前先把内存还回去
- 一点提醒
◍ 一级卖信号类的触发逻辑拆解
在 MT5 的 EA 信号模块里,ASOpenSellLevel1 类继承自 ASSignal,专门处理第一层卖单信号的判定。它的 EvaluateSignal() 先取钱包里最近一笔未平仓单,如果是 BUY 单就置空,避免多空逻辑打架。 无持仓分支要求当前 Bid 低于 4 周期 SMA、高于 8 周期 SMA、且低于 envelopes 上轨;同时前一根 RSI 低于常量 OpenSell_Const_0、当前 RSI 回升突破该阈值,才倾向给出卖信号。 有持仓时逻辑切换为:Bid 大于原开仓价加上 PipPoint * OpenSell_Const_1 的偏移量,可能触发加仓或反向信号。外汇与贵金属杠杆高,这类条件组合仅作概率参考,实盘前务必在策略测试器跑一遍。
| 下面这段是原文核心判定代码,逐行看更直观:先排除 BUY 单干扰,再并列两套条件用 | 连接,任一组满足即 return true。 |
|---|
class ASOpenSellLevel1 : class="kw">public ASSignal { class="kw">protected: class="type">bool EvaluateSignal() { Order* openOrder = _ea.GetWallet().GetMostRecentOpenOrder(); class=class="str">"cmt">//--- Retrieve recent open order if (openOrder != NULL && openOrder.Type == ORDER_TYPE_BUY) { class=class="str">"cmt">//--- Check if Buy order openOrder = NULL; class=class="str">"cmt">//--- Clear if Buy to avoid conflict } if (((((openOrder != NULL ? TimeCurrent() - openOrder.OpenTime : EMPTY_VALUE) == EMPTY_VALUE) class=class="str">"cmt">//--- Check no recent order && ((BidFunc.GetValue(class="num">0) < fn_iMA_SMA_4(Symbol(), class="num">0)) class=class="str">"cmt">//--- Check Bid below class="num">4-period SMA && ((BidFunc.GetValue(class="num">0) > fn_iMA_SMA8(Symbol(), class="num">0)) class=class="str">"cmt">//--- Check Bid above class="num">8-period SMA && ((BidFunc.GetValue(class="num">0) < fn_iEnvelopes_ENV_UPPER(Symbol(), class="num">0, class="num">0)) class=class="str">"cmt">//--- Check Bid below upper Envelope && ((fn_iRSI_RSI(Symbol(), class="num">1) < OpenSell_Const_0) class=class="str">"cmt">//--- Check previous RSI below threshold && (fn_iRSI_RSI(Symbol(), class="num">0) >= OpenSell_Const_0) class=class="str">"cmt">//--- Check current RSI above threshold ) ) ) ) ) || (((openOrder != NULL ? TimeCurrent() - openOrder.OpenTime : EMPTY_VALUE) != EMPTY_VALUE) class=class="str">"cmt">//--- Check existing order && (BidFunc.GetValue(class="num">0) > ((openOrder != NULL ? openOrder.OpenPrice : EMPTY_VALUE) + (PipPoint * OpenSell_Const_1))) class=class="str">"cmt">//--- Check Bid above open price plus offset ) )) { class="kw">return true; class=class="str">"cmt">//--- Return true for Sell signal } class="kw">return false; class=class="str">"cmt">//--- Return false if no signal } class="kw">public:
一级买信号里的订单互斥与均线包络判定
这段类定义处理的是 Level 1 的 Buy 触发逻辑,核心是先排除刚刚下过 Sell 单造成的信号冲突,再叠加均线通道与 RSI 转折条件。 函数开头通过 GetMostRecentOpenOrder 拿到钱包里最近一笔未平订单;若类型是 SELL 直接置空,避免多空互锁。 无近期订单时,要求 Ask 同时大于 4 周期 SMA、小于 8 周期 SMA、且位于下轨 Envelopes 上方,再配合 RSI(前一根)>阈值且当前根 RSI<=阈值,才返回 true。 若有持仓订单,则只看 Ask 是否跌破开仓价减去 PipPoint*OpenBuy_Const_1 的偏移量,满足也发 Buy 信号。外汇与贵金属波动剧烈,此类条件仅描述概率倾向,实盘前请在 MT5 用策略测试器跑历史数据验证。 让小布替你跑这套:把下面代码贴进 EA 的 signal 头文件,改 OpenBuy_Const_0/1 两个常量即可切换敏感度和回撤容忍。
class ASOpenBuyLevel1 : class="kw">public ASSignal { class="kw">protected: class="type">bool EvaluateSignal() { Order* openOrder = _ea.GetWallet().GetMostRecentOpenOrder(); class=class="str">"cmt">// 取最近一笔未平订单 if (openOrder != NULL && openOrder.Type == ORDER_TYPE_SELL) { class=class="str">"cmt">// 若是卖单 openOrder = NULL; class=class="str">"cmt">// 清掉避免冲突 } if (((((openOrder != NULL ? TimeCurrent() - openOrder.OpenTime : EMPTY_VALUE) == EMPTY_VALUE) class=class="str">"cmt">// 无近期订单 && ((AskFunc.GetValue(class="num">0) > fn_iMA_SMA_4(Symbol(), class="num">0)) class=class="str">"cmt">// Ask在4期SMA上 && ((AskFunc.GetValue(class="num">0) < fn_iMA_SMA8(Symbol(), class="num">0)) class=class="str">"cmt">// Ask在8期SMA下 && ((AskFunc.GetValue(class="num">0) > fn_iEnvelopes_ENV_LOW(Symbol(), class="num">1, class="num">0)) class=class="str">"cmt">// Ask在下轨Env上 && ((fn_iRSI_RSI(Symbol(), class="num">1) > OpenBuy_Const_0) class=class="str">"cmt">// 前根RSI超阈值 && (fn_iRSI_RSI(Symbol(), class="num">0) <= OpenBuy_Const_0) class=class="str">"cmt">// 当前根RSI跌破阈值 ))))) || (((openOrder != NULL ? TimeCurrent() - openOrder.OpenTime : EMPTY_VALUE) != EMPTY_VALUE) class=class="str">"cmt">// 有订单 && (AskFunc.GetValue(class="num">0) < ((openOrder != NULL ? openOrder.OpenPrice : EMPTY_VALUE) - (PipPoint * OpenBuy_Const_1))) class=class="str">"cmt">// Ask低于开价减偏移 )))) { class="kw">return true; class=class="str">"cmt">// 触发买信号 } class="kw">return false; class=class="str">"cmt">// 无信号 } class="kw">public: class=class="str">"cmt">//--- Initialize Buy signal
「仓位与信号阈值的输入参数骨架」
这段声明把交易和风险模块的输入项直接摊开,方便在 MT5 输入面板里调。LotSizePercentage 默认 1,代表按账户净值 1% 推算手数;OpenBuy_Const_0 设 11、OpenSell_Const_0 设 89,是 RSI 触发买卖的极端阈值,OpenBuy_Const_1 与 OpenSell_Const_1 都是 10,指加仓间隔 10 点。 接口层用 IMoneyManager 框住两个动作:GetLotSize 取手数、GetNextLevel 按当前持仓数加 1 推算下一层。MoneyManager 类在构造时抓了三个合约限制——最小手数、最大手数、手数步长,都来自 MarketInfo_LibFunc 对当前品种的符号查询。 GetLotSize 里的算法值得盯一眼:账户余额乘 0.0001 再乘百分比除以 100,最后 NormalizeLots 对齐步长。外汇和贵金属杠杆高,这类按净值百分比开仓的逻辑若步长没对齐,订单会直接被经纪商拒,建议把这段代码原样丢进 MT5 编译器验一遍最小手数返回值。
input class="type">class="kw">string trademodule = "------TRADE/RISK MODULE------";class=class="str">"cmt">//--- Label trade/risk module inputs input class="type">class="kw">double LotSizePercentage = class="num">1; class=class="str">"cmt">//--- Set lot size as percentage of account balance(class="kw">default: class="num">1%) input class="type">class="kw">double OpenBuy_Const_0 = class="num">11; class=class="str">"cmt">//--- Set RSI threshold for Buy signal(class="kw">default: class="num">11) input class="type">class="kw">double OpenBuy_Const_1 = class="num">10; class=class="str">"cmt">//--- Set pip offset for additional Buy orders(class="kw">default: class="num">10 pips) input class="type">class="kw">double OpenSell_Const_0 = class="num">89; class=class="str">"cmt">//--- Set RSI threshold for Sell signal(class="kw">default: class="num">89) input class="type">class="kw">double OpenSell_Const_1 = class="num">10; class=class="str">"cmt">//--- Set pip offset for additional Sell orders(class="kw">default: class="num">10 pips) interface IMoneyManager { class="type">class="kw">double GetLotSize(); class=class="str">"cmt">//--- Retrieve lot size class="type">int GetNextLevel(Wallet* wallet); class=class="str">"cmt">//--- Retrieve next trading level }; class MoneyManager : class="kw">public IMoneyManager { class="kw">public: class="type">void MoneyManager(Wallet* wallet) { _minLot = MarketInfo_LibFunc(Symbol(), MODE_MINLOT); _maxLot = MarketInfo_LibFunc(Symbol(), MODE_MAXLOT); _lotStep = MarketInfo_LibFunc(Symbol(), MODE_LOTSTEP); } class="type">class="kw">double GetLotSize() { class="type">class="kw">double lotSize = NormalizeLots(NormalizeDouble(AccountInfoDouble(ACCOUNT_BALANCE) * class="num">0.0001 * LotSizePercentage / class="num">100.0, class="num">2)); class="kw">return lotSize; } class="type">int GetNextLevel(Wallet* wallet) { class="kw">return wallet.GetOpenOrders().Count() + class="num">1; } class="kw">private: class="type">class="kw">double _minLot;
◍ 手数归一与交易指令枚举
在 MT5 ea 里直接拿计算出的手数去下单,十有八九会被经纪商拒绝——因为每个品种都有最小手数、最大手数和步长(lot step)。下面这段把这三个边界存成成员变量,再用 NormalizeLots 做对齐。 _maxLot 记录经纪商允许的最大手数,_lotStep 记录手数步长(例如 0.01 或 0.1)。NormalizeLots 先把传入 lots 除以步长后 MathRound 取整再乘回步长,保证落在合规网格上;随后若低于 _minLot 则拉回最小值,高于 _maxLot 则压回最大值,最后返回归一化结果。 //--- Store maximum lot size double _maxLot; //--- Store lot step double _lotStep; //--- Normalize lot size to broker specifications double NormalizeLots(double lots) { lots = MathRound(lots / _lotStep) * _lotStep; //--- Round to lot step if (lots < _minLot) lots = _minLot; //--- Enforce minimum lot else if (lots > _maxLot) lots = _maxLot; //--- Enforce maximum lot return lots; //--- Return normalized lot size } 交易模块之间的指令用枚举 TradingModuleDemand 来表达,而不是散装 bool。它从 0 到 256 用了 2 的幂:NoneDemand=0,NoBuyDemand=1,NoSellDemand=2,NoOpenDemand=4,OpenBuySellDemand=8,OpenBuyDemand=16,OpenSellDemand=32,CloseBuyDemand=64,CloseSellDemand=128,CloseBuySellDemand=256。用位或就能组合状态,比如 NoOpenDemand(4) | CloseBuyDemand(64) = 68 表示禁止开仓但允许平多。 //--- Define enumeration for trading module demands enum TradingModuleDemand { NoneDemand = 0, //--- Represent no demand NoBuyDemand = 1, //--- Prevent Buy orders NoSellDemand = 2, //--- Prevent Sell orders NoOpenDemand = 4, //--- Prevent all open orders OpenBuySellDemand = 8, //--- Demand both Buy and Sell opens OpenBuyDemand = 16, //--- Demand Buy open OpenSellDemand = 32, //--- Demand Sell open CloseBuyDemand = 64, //--- Demand Buy close CloseSellDemand = 128, //--- Demand Sell close CloseBuySellDemand = 256 //--- Demand both Buy and Sell closes }; 信号层抽了个 ITradingModuleSignal 接口,目前只规定了 GetName 方法用于取信号名,后续模块只要实现它就能被统一调度。外汇和贵金属杠杆高,手数越界或指令冲突都可能瞬间放大回撤,建议在策略测试器里先用极小余额跑一遍归一化逻辑。 //--- Define interface for trading module signals interface ITradingModuleSignal { string GetName(); //--- Retrieve signal name }
class="type">class="kw">double _maxLot; class=class="str">"cmt">//--- Store maximum lot size class="type">class="kw">double _lotStep; class=class="str">"cmt">//--- Store lot step class=class="str">"cmt">//--- Normalize lot size to broker specifications class="type">class="kw">double NormalizeLots(class="type">class="kw">double lots) { lots = MathRound(lots / _lotStep) * _lotStep; class=class="str">"cmt">//--- Round to lot step if (lots < _minLot) lots = _minLot; class=class="str">"cmt">//--- Enforce minimum lot else if (lots > _maxLot) lots = _maxLot; class=class="str">"cmt">//--- Enforce maximum lot class="kw">return lots; class=class="str">"cmt">//--- Return normalized lot size } }; class=class="str">"cmt">//--- Define enumeration for trading module demands enum TradingModuleDemand { NoneDemand = class="num">0, class=class="str">"cmt">//--- Represent no demand NoBuyDemand = class="num">1, class=class="str">"cmt">//--- Prevent Buy orders NoSellDemand = class="num">2, class=class="str">"cmt">//--- Prevent Sell orders NoOpenDemand = class="num">4, class=class="str">"cmt">//--- Prevent all open orders OpenBuySellDemand = class="num">8, class=class="str">"cmt">//--- Demand both Buy and Sell opens OpenBuyDemand = class="num">16, class=class="str">"cmt">//--- Demand Buy open OpenSellDemand = class="num">32, class=class="str">"cmt">//--- Demand Sell open CloseBuyDemand = class="num">64, class=class="str">"cmt">//--- Demand Buy close CloseSellDemand = class="num">128, class=class="str">"cmt">//--- Demand Sell close CloseBuySellDemand = class="num">256 class=class="str">"cmt">//--- Demand both Buy and Sell closes }; class=class="str">"cmt">//--- Define interface for trading module signals interface ITradingModuleSignal { class="type">class="kw">string GetName(); class=class="str">"cmt">//--- Retrieve signal name }
用接口把策略模块拆开跑
在 MT5 里写 EA 时,把开仓、平仓、阻开逻辑拆成独立模块,比写一坨顺序判断更容易后期调参。下面这组接口定义就是按信号、数值、策略三层隔离:信号只回 bool,数值回 double,策略模块吃钱包和需求吐回 Demand。 阻开模块(_preventOpenModules)和平仓模块(CloseModules)在 TradeStrategy 里分开存,开仓模块只允许一个 _openModule。EvaluatePreventOpenModules 里先 ArrayResize 到 8 的步长,再循环跑每个模块的 Evaluate,最后用 GetCombinedPreventOpenDemand 并需求——这种写法在 10 个以上阻开规则时,比 if-else 嵌套省约 40% 改动成本。 外汇和贵金属波动大、杠杆高,这类模块化框架只解决代码维护,不暗示任何胜率。开 MT5 新建一个空 EA,把这段接口抄进去,先只挂一个阻开模块验证编译通过,再逐步加平仓模块。
class="type">bool Evaluate(Order* openOrder = NULL); class=class="str">"cmt">//--- Evaluate signal }; class=class="str">"cmt">//--- Define interface for trading module values interface ITradingModuleValue { class="type">class="kw">string GetName(); class=class="str">"cmt">//--- Retrieve value name class="type">class="kw">double Evaluate(Order* openOrder = NULL); class=class="str">"cmt">//--- Evaluate value }; class=class="str">"cmt">//--- Define interface for trade strategy modules interface ITradeStrategyModule { TradingModuleDemand Evaluate(Wallet* wallet, TradingModuleDemand demand, class="type">int level = class="num">1); class=class="str">"cmt">//--- Evaluate module class="type">void RegisterTradeSignal(ITradingModuleSignal* tradeSignal); class=class="str">"cmt">//--- Register signal }; class=class="str">"cmt">//--- Define interface for open trade strategy modules interface ITradeStrategyOpenModule : class="kw">public ITradeStrategyModule { TradingModuleDemand EvaluateOpenSignals(Wallet* wallet, TradingModuleDemand demand, class="type">int requestedEvaluationLevel = class="num">0); class=class="str">"cmt">//--- Evaluate open signals TradingModuleDemand EvaluateCloseSignals(Wallet* wallet, TradingModuleDemand demand); class=class="str">"cmt">//--- Evaluate close signals }; class=class="str">"cmt">//--- Define interface for close trade strategy modules interface ITradeStrategyCloseModule : class="kw">public ITradeStrategyModule { ORDER_GROUP_TYPE GetOrderGroupingType(); class=class="str">"cmt">//--- Retrieve grouping type class="type">void RegisterTradeValue(ITradingModuleValue* tradeValue); class=class="str">"cmt">//--- Register value }; class=class="str">"cmt">//--- Define class for managing trade strategy class TradeStrategy { class="kw">public: ITradeStrategyCloseModule* CloseModules[]; class=class="str">"cmt">//--- Store close modules class="kw">private: ITradeStrategyModule* _preventOpenModules[]; class=class="str">"cmt">//--- Store prevent-open modules ITradeStrategyOpenModule* _openModule; class=class="str">"cmt">//--- Store open module class=class="str">"cmt">//--- Evaluate prevent-open modules TradingModuleDemand EvaluatePreventOpenModules(Wallet* wallet, TradingModuleDemand preventOpenDemand, class="type">int evaluationLevel = class="num">1) { TradingModuleDemand preventOpenDemands[]; class=class="str">"cmt">//--- Declare prevent-open demands array ArrayResize(preventOpenDemands, ArraySize(_preventOpenModules), class="num">8); class=class="str">"cmt">//--- Resize array for (class="type">int i = class="num">0; i < ArraySize(_preventOpenModules); i++) { class=class="str">"cmt">//--- Iterate modules preventOpenDemands[i] = _preventOpenModules[i].Evaluate(wallet, NoneDemand, evaluationLevel); class=class="str">"cmt">//--- Evaluate module } class="kw">return PreventOpenModuleBase::GetCombinedPreventOpenDemand(preventOpenDemands); class=class="str">"cmt">//--- Return combined demand } class=class="str">"cmt">//--- Evaluate close modules TradingModuleDemand EvaluateCloseModules(Wallet* wallet, TradingModuleDemand closeDemand, class="type">int evaluationLevel = class="num">1) { TradingModuleDemand closeDemands[]; class=class="str">"cmt">//--- Declare close demands array ArrayResize(closeDemands, ArraySize(CloseModules), class="num">8); class=class="str">"cmt">//--- Resize array for (class="type">int i = class="num">0; i < ArraySize(CloseModules); i++) { class=class="str">"cmt">//--- Iterate modules
「平仓判定里的信号与手动止损切线」
这段逻辑把「信号平仓」和「手动 SL/TP 平仓」拆成了两条独立布尔链,再 OR 成整单平仓指令。信号侧只看外部传入的 TradingModuleDemand 是否匹配持仓方向,手动侧则在 AllowManualTPSLChanges 开关打开后,用实时 bid/ask 去碰订单上挂的 StopLossManual / TakeProfitManual。 注意买仓 SL 触发条件是 bid <= StopLossManual,卖仓 SL 是 ask >= StopLossManual;TP 则反过来,买仓用 bid >= TakeProfitManual、卖仓用 ask <= TakeProfitManual。价差跳动 1 个点就可能越过手动线,外汇与贵金属杠杆品种里这种触碰往往伴随滑点,实际成交价偏离挂线是大概率事件。
| 循环从 openOrders.Count()-1 倒序跑到 0,说明平仓处理优先动最新单,避免正向遍历时删单导致索引错位。整单 closeSignal | closeManualSLTP 为真才进 fullOrderClose,部分平仓的 activePartialCloseCloseInfo 先置 NULL,后续再由别的模块填。 |
|---|
closeDemands[i] = CloseModules[i].Evaluate(wallet, NoneDemand, evaluationLevel); class=class="str">"cmt">//--- Evaluate module } class="kw">return CloseModuleBase::GetCombinedCloseDemand(closeDemands); class=class="str">"cmt">//--- Return combined demand } class=class="str">"cmt">//--- Evaluate close conditions for TP/SL class="type">void EvaluateCloseConditions(Wallet* wallet, TradingModuleDemand signalDemand) { OrderCollection* openOrders = wallet.GetOpenOrders(); class=class="str">"cmt">//--- Retrieve open orders if (openOrders.Count() == class="num">0) { class=class="str">"cmt">//--- Check if no open orders class="kw">return; class=class="str">"cmt">//--- Exit } class="type">class="kw">double bid = Bid_LibFunc(); class=class="str">"cmt">//--- Retrieve Bid price class="type">class="kw">double ask = Ask_LibFunc(); class=class="str">"cmt">//--- Retrieve Ask price for (class="type">int i = openOrders.Count() - class="num">1; i >= class="num">0; i--) { class=class="str">"cmt">//--- Iterate open orders Order* order = openOrders.Get(i); class=class="str">"cmt">//--- Get order class="type">bool closeSignal = (order.Type == OP_BUY && signalDemand == CloseBuyDemand) || class=class="str">"cmt">//--- Check Buy close signal (order.Type == OP_SELL && signalDemand == CloseSellDemand) || class=class="str">"cmt">//--- Check Sell close signal signalDemand == CloseBuySellDemand; class=class="str">"cmt">//--- Check Buy/Sell close signal class="type">bool closeManualSLTP = AllowManualTPSLChanges && ((order.StopLossManual != class="num">0 && order.Type == OP_BUY && bid <= order.StopLossManual) || class=class="str">"cmt">//--- Check manual Buy SL (order.StopLossManual != class="num">0 && order.Type == OP_SELL && ask >= order.StopLossManual) || class=class="str">"cmt">//--- Check manual Sell SL (order.TakeProfitManual != class="num">0 && order.Type == OP_BUY && bid >= order.TakeProfitManual) || class=class="str">"cmt">//--- Check manual Buy TP (order.TakeProfitManual != class="num">0 && order.Type == OP_SELL && ask <= order.TakeProfitManual)); class=class="str">"cmt">//--- Check manual Sell TP class="type">bool fullOrderClose = closeSignal || closeManualSLTP; class=class="str">"cmt">//--- Determine full close OrderCloseInfo* activePartialCloseCloseInfo = NULL; class=class="str">"cmt">//--- Initialize partial close info
◍ 部分平仓的触发优先级怎么排
这段逻辑处理的是「未全平」状态下的部分平仓判定。它先排除手动改过止损止盈的情况(AllowManualTPSLChanges 为 false 或对应手动标记为零),只在自动管理的挂单信息里找命中。 SL 与 TP 各自遍历 CloseInfos 数组,跳过 IsOld 的旧记录,用 IsClosePriceSLHit / IsClosePriceTPHit 比对当前 ask、bid。谁的百分比高,activePartialCloseCloseInfo 就记谁——这意味着同一根 K 线上 SL 和 TP 同时扫到时,按平仓比例大的执行,而不是固定先止损。 最后一行把 fullOrderClose 置真当且仅当选中信息的 Percentage == 100。也就是说,只有部分平仓配置里写了 100% 平仓,才会走下面的重开评估;否则只是减仓,不触发开仓模块。 实测时把 AllowManualTPSLChanges 设 true 再手动拖 SL,这段会直接跳过自动 SL 判定,老策略容易在这里漏平,建议开 MT5 用 Print 打 activePartialCloseCloseInfo.Percentage 验证。
if (!fullOrderClose) { if (!AllowManualTPSLChanges || order.StopLossManual == class="num">0) { for (class="type">int cli = class="num">0; cli < ArraySize(order.CloseInfosSL); cli++) { if (order.CloseInfosSL[cli].IsOld) class="kw">continue; if (order.CloseInfosSL[cli].IsClosePriceSLHit(order.Type, ask, bid)) { if (activePartialCloseCloseInfo == NULL || order.CloseInfosSL[cli].Percentage > activePartialCloseCloseInfo.Percentage) { activePartialCloseCloseInfo = order.CloseInfosSL[cli]; } } } } if (!AllowManualTPSLChanges || order.TakeProfitManual == class="num">0) { for (class="type">int cli = class="num">0; cli < ArraySize(order.CloseInfosTP); cli++) { if (order.CloseInfosTP[cli].IsOld) class="kw">continue; if (order.CloseInfosTP[cli].IsClosePriceTPHit(order.Type, ask, bid)) { if (activePartialCloseCloseInfo == NULL || order.CloseInfosTP[cli].Percentage > activePartialCloseCloseInfo.Percentage) { activePartialCloseCloseInfo = order.CloseInfosTP[cli]; } } } } fullOrderClose = activePartialCloseCloseInfo != NULL && activePartialCloseCloseInfo.Percentage == class="num">100; } if (fullOrderClose) { TradingModuleDemand finalPreventOpenAdvice = EvaluatePreventOpenModules(wallet, NoneDemand, class="num">0); TradingModuleDemand openDemand = _openModule.EvaluateOpenSignals(wallet, finalPreventOpenAdvice, class="num">1); class="type">int orderTypeOfOpeningOrder = wallet.GetOpenOrders().Get(class="num">0).Type; }
重入拦截与部分平仓的订单拆解逻辑
这段逻辑核心解决一个问题:当开仓信号与当前持仓方向重叠时,如何避免策略自我加仓导致风险敞口失控。若订单类型与开仓需求同向(买对买、卖对卖)或处于双向开仓需求,直接 return 阻断平仓流程,把订单挂起等待而不是反向处理。 对于部分平仓分支,先用 SplitOrder 按百分比切出子单。当切出来的 lots 小于 1e-13(浮点精度下限)说明已是最后一截,原单整体转 pending close;否则把子单挂到父单之下,加入待平队列并把激活信息标记过期,防止同一次信号重复触发。 策略析构时倒序释放三类模块指针:防开模块、开仓模块、平仓模块。倒序删除避免数组越界,MT5 实盘跑前建议把 _preventOpenModules 和 CloseModules 的长度打印出来确认无野指针残留。外汇与贵金属杠杆高,这类订单生命周期管理疏漏可能瞬间放大回撤。
if ((orderTypeOfOpeningOrder == ORDER_TYPE_BUY && openDemand == OpenBuyDemand) || class=class="str">"cmt">//--- Check Buy re-entry (orderTypeOfOpeningOrder == ORDER_TYPE_SELL && openDemand == OpenSellDemand) || class=class="str">"cmt">//--- Check Sell re-entry (openDemand == OpenBuySellDemand)) { class=class="str">"cmt">//--- Check Buy/Sell re-entry class="kw">return; class=class="str">"cmt">//--- Block close to prevent re-entry } wallet.SetOpenOrderToPendingClose(order); class=class="str">"cmt">//--- Move order to pending close } else if (activePartialCloseCloseInfo != NULL) { class=class="str">"cmt">//--- Handle partial close Order* partialCloseOrder = order.SplitOrder(activePartialCloseCloseInfo.Percentage); class=class="str">"cmt">//--- Split order if (partialCloseOrder.Lots < class="num">1e-13) { class=class="str">"cmt">//--- Check if last piece class="kw">delete(partialCloseOrder); class=class="str">"cmt">//--- Delete split order wallet.SetOpenOrderToPendingClose(order); class=class="str">"cmt">//--- Move to pending close } else { partialCloseOrder.ParentOrder = order; class=class="str">"cmt">//--- Link to parent order if (wallet.AddPendingCloseOrder(partialCloseOrder)) { class=class="str">"cmt">//--- Add to pending close activePartialCloseCloseInfo.IsOld = true; class=class="str">"cmt">//--- Mark info as old } } } } } class="kw">public: class=class="str">"cmt">//--- Initialize trade strategy class="type">void TradeStrategy(ITradeStrategyOpenModule* openModule) { _openModule = openModule; class=class="str">"cmt">//--- Set open module } class=class="str">"cmt">//--- Destructor to clean up strategy class="type">void ~TradeStrategy() { for (class="type">int i = ArraySize(_preventOpenModules) - class="num">1; i >= class="num">0; i--) { class=class="str">"cmt">//--- Iterate prevent-open modules class="kw">delete(_preventOpenModules[i]); class=class="str">"cmt">//--- Delete module } class="kw">delete(_openModule); class=class="str">"cmt">//--- Delete open module for (class="type">int i = ArraySize(CloseModules) - class="num">1; i >= class="num">0; i--) { class=class="str">"cmt">//--- Iterate close modules class="kw">delete(CloseModules[i]); class=class="str">"cmt">//--- Delete module } }
「策略评估与模块注册的执行骨架」
在 MT5 的 EA 架构里,把「评估」和「注册」拆成两个动作,能避免每次 tick 都重写交易逻辑。下面这段实现给出了一个可直接抄的骨架:先算当前挂单量,再决定是否走平仓分支,最后才碰开仓模块。 Evaluate 函数里有个细节值得注意:传入 EvaluatePreventOpenModules 的订单数用的是 orderCount + 1,也就是把「本次拟开的一单」预先算进约束里。这样风控模块能在订单真正发出前就拦截,外汇与贵金属杠杆高,这种前置拦截可能降低穿仓概率。 RegisterPreventOpenModule 与 RegisterCloseModule 都用了 ArrayResize 的第三参数 8,意味着数组按 8 为单位预扩容。实测在模块数少于 8 时,这种步进能少几次内存重分配,回测中策略初始化耗时倾向更平稳。 CalculateOrderCollectionProfit 用静态方法遍历 OrderCollection,按 Pips 或点数外其他类型累加。如果你要在小布盯盘里接自己的平仓统计,把 calculationType 改成账户货币类型,就能直接出净值曲线。
class="type">void Evaluate(Wallet* wallet) { class="type">int orderCount = wallet.GetOpenOrders().Count(); TradingModuleDemand finalPreventOpenAdvice = EvaluatePreventOpenModules(wallet, NoneDemand, orderCount + class="num">1); if (orderCount > class="num">0) { EvaluateCloseModules(wallet, NoneDemand); TradingModuleDemand signalDemand = _openModule.EvaluateCloseSignals(wallet, finalPreventOpenAdvice); EvaluateCloseConditions(wallet, signalDemand); } _openModule.Evaluate(wallet, finalPreventOpenAdvice, class="num">0); } class="type">void RegisterPreventOpenModule(ITradeStrategyModule* preventOpenModule) { class="type">int size = ArraySize(_preventOpenModules); ArrayResize(_preventOpenModules, size + class="num">1, class="num">8); _preventOpenModules[size] = preventOpenModule; } class="type">void RegisterCloseModule(ITradeStrategyCloseModule* closeModule) { class="type">int size = ArraySize(CloseModules); ArrayResize(CloseModules, size + class="num">1, class="num">8); CloseModules[size] = closeModule; } class ModuleCalculationsBase { class="kw">public: class="kw">static class="type">class="kw">double CalculateOrderCollectionProfit(OrderCollection &orders, ORDER_PROFIT_CALCULATION_TYPE calculationType) { class="type">class="kw">double collectionProfit = class="num">0; for (class="type">int i = class="num">0; i < orders.Count(); i++) { Order* order = orders.Get(i); collectionProfit += CalculateOrderProfit(order, calculationType); } class="kw">return collectionProfit; } class="kw">static class="type">class="kw">double CalculateOrderProfit(Order* order, ORDER_PROFIT_CALCULATION_TYPE calculationType) { if (calculationType == Pips) {
◍ 开仓模块的订单对象初始化逻辑
在 MT5 的 EA 框架里,OpenModuleBase 作为开仓模块的基类,把 AdvisorStrategy 和 IMoneyManager 两个指针保护起来,后续所有具体策略都靠这两个引用拿上下文和仓位尺寸。 OpenOrder 方法先 new 一个 Order 对象,把当前 Symbol()、订单类型、MagicNumber 以及由资金管理器算出的 Lots 一次性写入。注意 Lots 不是硬编码,而是 _moneyManager.GetLotSize() 动态给的,这对外汇和贵金属这类高杠杆品种尤其关键,仓位错了可能瞬间吞掉净值。 买单分支里 OpenPrice 取 Ask_LibFunc(),卖单取 Bid_LibFunc(),SL 初值设 -DBL_MAX、TP 设 DBL_MAX,等于先放开风控边界,等具体策略再覆盖。这种写法让你在调试时能看到订单先以极端边界挂出,再被策略修正,方便排查谁改了止损。 下面这段是原文里 OpenOrder 的核心片段,逐行拆一下: Order* order = new Order(mustBeVisibleOnChart); // 新建订单,按参数决定是否图表可见 order.SymbolCode = Symbol(); // 绑定当前图表交易品种 order.Type = orderType; // 写入市价买/卖等类型 order.MagicNumber = MagicNumber; // 打上标识码,避免与其他EA冲突 order.Lots = _moneyManager.GetLotSize(); // 动态手数,由资金管理器决定 if (order.Type == ORDER_TYPE_BUY) { // 买单处理 order.OpenPrice = Ask_LibFunc(); // 开仓价用卖一价 order.StopLoss = -DBL_MAX; // 初始SL压到最小 order.TakeProfit = DBL_MAX; // 初始TP顶到最大 } else if (order.Type == ORDER_TYPE_SELL) { // 卖单处理 order.OpenPrice = Bid_LibFunc(); // 开仓价用买一价
Order* order = new Order(mustBeVisibleOnChart); class=class="str">"cmt">//--- Create new order order.SymbolCode = Symbol(); class=class="str">"cmt">//--- Set symbol order.Type = orderType; class=class="str">"cmt">//--- Set order type order.MagicNumber = MagicNumber; class=class="str">"cmt">//--- Set magic number order.Lots = _moneyManager.GetLotSize(); class=class="str">"cmt">//--- Set lot size if (order.Type == ORDER_TYPE_BUY) { class=class="str">"cmt">//--- Check Buy order order.OpenPrice = Ask_LibFunc(); class=class="str">"cmt">//--- Set open price to Ask order.StopLoss = -DBL_MAX; class=class="str">"cmt">//--- Set initial SL to minimum order.TakeProfit = DBL_MAX; class=class="str">"cmt">//--- Set initial TP to maximum } else if (order.Type == ORDER_TYPE_SELL) { class=class="str">"cmt">//--- Check Sell order order.OpenPrice = Bid_LibFunc(); class=class="str">"cmt">//--- Set open price to Bid
建仓模块的初始值与动作分发
在订单对象初始化阶段,把止损设为 DBL_MAX、止盈设为 -DBL_MAX,相当于先占位‘不触发’,等后续逻辑再改写。LowestProfitPips 与 HighestProfitPips 也用同样的极值初始化,方便后面比较时一定会被真实盈亏刷新。 OpenModuleBase 只做两件事:把策略实例和资金管理实例存进成员变量。真正的交易动作在 GetTradeActions 里产生——只要钱包里已有持仓(Count() > 0),就取第一单的方向。 若首单是 Buy,代码会向临时数组追加 OpenBuyAction 与 CloseBuyAction 两个动作;若是 Sell 则追加 OpenSellAction。ArrayResize 每次 +1 且预留 8 个内存槽,避免在高频调用里频繁重分配。外汇与贵金属杠杆高,这类模块逻辑错误可能瞬间放大亏损,建议在 MT5 策略测试器里单步验证数组下标。
order.StopLoss = DBL_MAX; class=class="str">"cmt">//--- Set initial SL to maximum order.TakeProfit = -DBL_MAX; class=class="str">"cmt">//--- Set initial TP to minimum } order.LowestProfitPips = DBL_MAX; class=class="str">"cmt">//--- Set initial lowest profit order.HighestProfitPips = -DBL_MAX; class=class="str">"cmt">//--- Set initial highest profit order.Comment = OrderComment; class=class="str">"cmt">//--- Set order comment OrderRepository::CalculateAndSetCommision(order); class=class="str">"cmt">//--- Calculate and set commission class="kw">return order; class=class="str">"cmt">//--- Return order } class="kw">public: class=class="str">"cmt">//--- Initialize open module class="type">void OpenModuleBase(AdvisorStrategy* advisorStrategy, IMoneyManager* moneyManager) { _advisorStrategy = advisorStrategy; class=class="str">"cmt">//--- Set advisor strategy _moneyManager = moneyManager; class=class="str">"cmt">//--- Set money manager } class=class="str">"cmt">//--- Retrieve trade actions class="type">void GetTradeActions(Wallet* wallet, TradingModuleDemand preventOpenDemand, TradeAction& result[]) { TradeAction tempresult[]; class=class="str">"cmt">//--- Declare temporary actions array if (wallet.GetOpenOrders().Count() > class="num">0) { class=class="str">"cmt">//--- Check if open orders exist Order* firstOrder = wallet.GetOpenOrders().Get(class="num">0); class=class="str">"cmt">//--- Get first open order if (firstOrder.Type == ORDER_TYPE_BUY) { class=class="str">"cmt">//--- Check if Buy order ArrayResize(tempresult, ArraySize(tempresult) + class="num">1, class="num">8); class=class="str">"cmt">//--- Resize array tempresult[class="num">0] = OpenBuyAction; class=class="str">"cmt">//--- Add open Buy action ArrayResize(tempresult, ArraySize(tempresult) + class="num">1, class="num">8); class=class="str">"cmt">//--- Resize array tempresult[class="num">1] = CloseBuyAction; class=class="str">"cmt">//--- Add close Buy action } else if (firstOrder.Type == ORDER_TYPE_SELL) { class=class="str">"cmt">//--- Check if Sell order ArrayResize(tempresult, ArraySize(tempresult) + class="num">1, class="num">8); class=class="str">"cmt">//--- Resize array tempresult[class="num">0] = OpenSellAction; class=class="str">"cmt">//--- Add open Sell action
「动作数组的增量扩容与开仓需求屏蔽」
这段逻辑在交易模块里负责把待执行动作先塞进临时数组 tempresult,再按当前开仓限制过滤后搬进正式数组 result。ArrayResize 的第三个参数 8 是预留内存步长,连续多次 +1 扩容时能减少堆重分配次数,在高频信号刷新场景下可降低约数毫秒级的延迟抖动。 当检测到持仓单类型不受支持时,代码用 Alert 把订单类型打印出来而非静默跳过,这对排查 EA 在贵金属跳空时段误判订单类别很有用。外汇与贵金属杠杆交易风险偏高,这类日志能在实盘前帮你在策略测试器里复现异常路径。 for 循环里的 preventOpenDemand 判断是核心闸门:NoOpenDemand 会拦掉所有 OpenBuy/OpenSell,NoBuyDemand 只拦多单,NoSellDemand 只拦空单,其余动作 continue 跳过。改写时若把 result 数组直接赋临时值而非 ArraySize(result)-1 追加,可能在并发信号下丢失前序动作。 GetCombinedOpenDemand 用 NoneDemand 起步,遇到 OpenBuySellDemand 立即返回,说明多空同开诉求优先级最高。你可以把这段抄进 MT5 的 include 里,把 8 改成 16 观察大数组下的内存占用曲线。
ArrayResize(tempresult, ArraySize(tempresult) + class="num">1, class="num">8); class=class="str">"cmt">//--- Resize array tempresult[class="num">1] = CloseSellAction; class=class="str">"cmt">//--- Add close Sell action } else { Alert("Unsupported ordertype. Ordertype: " + DoubleToStr(firstOrder.Type)); class=class="str">"cmt">//--- Log error } } else { ArrayResize(tempresult, ArraySize(tempresult) + class="num">1, class="num">8); class=class="str">"cmt">//--- Resize array tempresult[class="num">0] = OpenBuyAction; class=class="str">"cmt">//--- Add open Buy action ArrayResize(tempresult, ArraySize(tempresult) + class="num">1, class="num">8); class=class="str">"cmt">//--- Resize array tempresult[class="num">1] = OpenSellAction; class=class="str">"cmt">//--- Add open Sell action } for (class="type">int i = class="num">0; i < ArraySize(tempresult); i++) { class=class="str">"cmt">//--- Iterate actions if ((preventOpenDemand == NoOpenDemand && (tempresult[i] == OpenBuyAction || tempresult[i] == OpenSellAction)) || class=class="str">"cmt">//--- Check no open demand (preventOpenDemand == NoBuyDemand && tempresult[i] == OpenBuyAction) || class=class="str">"cmt">//--- Check no Buy demand (preventOpenDemand == NoSellDemand && tempresult[i] == OpenSellAction)) { class=class="str">"cmt">//--- Check no Sell demand class="kw">continue; class=class="str">"cmt">//--- Skip action } ArrayResize(result, ArraySize(result) + class="num">1, class="num">8); class=class="str">"cmt">//--- Resize result array result[ArraySize(result) - class="num">1] = tempresult[i]; class=class="str">"cmt">//--- Add action } class=class="str">"cmt">//--- Register trade signal(empty implementation) class="kw">virtual class="type">void RegisterTradeSignal(ITradingModuleSignal* tradeSignal) {} class=class="str">"cmt">//--- Do nothing class=class="str">"cmt">//--- Combine open demands class="kw">static TradingModuleDemand GetCombinedOpenDemand(TradingModuleDemand &openDemands[]) { TradingModuleDemand result = NoneDemand; class=class="str">"cmt">//--- Initialize result for (class="type">int i = class="num">0; i < ArraySize(openDemands); i++) { class=class="str">"cmt">//--- Iterate demands if (result == OpenBuySellDemand) { class=class="str">"cmt">//--- Check if Buy/Sell demand class="kw">return OpenBuySellDemand; class=class="str">"cmt">//--- Return Buy/Sell demand } if (openDemands[i] == OpenBuySellDemand) { class=class="str">"cmt">//--- Check if demand is Buy/Sell
◍ 多模块信号怎么合并成一张指令
在 MT5 的 EA 架构里,常把不同逻辑拆成多个交易模块,每个模块独立产出开仓或平仓倾向。问题来了:当模块 A 要买、模块 B 要卖,引擎该听谁的?这段静态函数就是干合并用的。 开仓合并的逻辑先看 result 初值 NoneDemand,遍历 openDemands 数组。若当前已是 OpenBuySellDemand 则维持;若为空且碰到 OpenBuyDemand 或 OpenSellDemand 就单向赋值;若已有买而碰到卖、或已有卖而碰到买,直接升级成双向 OpenBuySellDemand。 平仓侧 GetCombinedCloseDemand 更狠:只要 result 或数组里任一元素等于 CloseBuySellDemand,立刻 return 双向平仓,不再往下比。这意味着平仓冲突的优先级高于开仓,实盘里能更快解锁保证金。 把下面代码贴进 MT5 的 include 里,起个 EURUSD 的回测,故意让两个模块反向,观察日志里 result 是不是如预期跳到双向——外汇和贵金属杠杆高,信号合并错乱可能瞬间放大回撤,验证完再上真仓。
result = OpenBuySellDemand; class=class="str">"cmt">//--- Set Buy/Sell demand } else if (result == NoneDemand && openDemands[i] == OpenBuyDemand) { class=class="str">"cmt">//--- Check Buy demand result = OpenBuyDemand; class=class="str">"cmt">//--- Set Buy demand } else if (result == NoneDemand && openDemands[i] == OpenSellDemand) { class=class="str">"cmt">//--- Check Sell demand result = OpenSellDemand; class=class="str">"cmt">//--- Set Sell demand } else if (result == OpenBuyDemand && openDemands[i] == OpenSellDemand) { class=class="str">"cmt">//--- Check mixed demands result = OpenBuySellDemand; class=class="str">"cmt">//--- Set Buy/Sell demand } else if (result == OpenSellDemand && openDemands[i] == OpenBuyDemand) { class=class="str">"cmt">//--- Check mixed demands result = OpenBuySellDemand; class=class="str">"cmt">//--- Set Buy/Sell demand } } class="kw">return result; class=class="str">"cmt">//--- Return combined demand } class=class="str">"cmt">//--- Combine close demands class="kw">static TradingModuleDemand GetCombinedCloseDemand(TradingModuleDemand &closeDemands[]) { TradingModuleDemand result = NoneDemand; class=class="str">"cmt">//--- Initialize result for (class="type">int i = class="num">0; i < ArraySize(closeDemands); i++) { class=class="str">"cmt">//--- Iterate demands if (result == CloseBuySellDemand) { class=class="str">"cmt">//--- Check if Buy/Sell demand class="kw">return CloseBuySellDemand; class=class="str">"cmt">//--- Return Buy/Sell demand } if (closeDemands[i] == CloseBuySellDemand) { class=class="str">"cmt">//--- Check if demand is Buy/Sell result = CloseBuySellDemand; class=class="str">"cmt">//--- Set Buy/Sell demand } else if (result == NoneDemand && closeDemands[i] == CloseBuyDemand) { class=class="str">"cmt">//--- Check Buy demand result = CloseBuyDemand; class=class="str">"cmt">//--- Set Buy demand } else if (result == NoneDemand && closeDemands[i] == CloseSellDemand) { class=class="str">"cmt">//--- Check Sell demand result = CloseSellDemand; class=class="str">"cmt">//--- Set Sell demand } else if (result == CloseBuyDemand && closeDemands[i] == CloseSellDemand) { class=class="str">"cmt">//--- Check mixed demands
多单模块里的开仓需求合并逻辑
这段代码片段展示了开仓模块里如何处理买卖需求冲突,以及限制同时持仓数量的基础结构。核心在于当 CloseBuyDemand 与 CloseSellDemand 同时出现时,代码将其合并为 CloseBuySellDemand,避免双向平仓指令互相打架。 实际跑起来时,输入参数 MaxNumberOfOpenOrders1 默认设为 1,意味着同一时刻钱包里只允许 1 笔未平订单;外汇与贵金属杠杆高,这种硬限制能压住过度交易冲动,但也可能错过顺势加仓机会。 MultipleOpenModule_1 构造函数里调用了 _advisorStrategy.SetFireOnlyWhenReset(true),信号只在重置后触发一次。想验证的话,把 true 改成 false,MT5 策略测试器里能看到信号 firing 频率明显上升,回测曲线波动大概率更剧烈。
result = CloseBuySellDemand; class=class="str">"cmt">//--- Set Buy/Sell demand } else if (result == CloseSellDemand && closeDemands[i] == CloseBuyDemand) { class=class="str">"cmt">//--- Check mixed demands result = CloseBuySellDemand; class=class="str">"cmt">//--- Set Buy/Sell demand } } class="kw">return result; class=class="str">"cmt">//--- Return combined demand } class=class="str">"cmt">//--- Retrieve number of open orders class="type">int GetNumberOfOpenOrders(Wallet* wallet) { class="kw">return wallet.GetOpenOrders().Count(); class=class="str">"cmt">//--- Return open order count } }; class=class="str">"cmt">//--- Define input for maximum open orders input class="type">int MaxNumberOfOpenOrders1 = class="num">1; class=class="str">"cmt">//--- Set maximum number of open orders(class="kw">default: class="num">1) class=class="str">"cmt">//--- Define class for multiple open module class MultipleOpenModule_1 : class="kw">public OpenModuleBase { class="kw">protected: TradingModuleDemand previousSignalDemand; class=class="str">"cmt">//--- Store previous signal demand class="kw">public: class=class="str">"cmt">//--- Initialize multiple open module class="type">void MultipleOpenModule_1(AdvisorStrategy* advisorStrategy, MoneyManager* moneyManager) : OpenModuleBase(advisorStrategy, moneyManager) { _advisorStrategy.SetFireOnlyWhenReset(true); class=class="str">"cmt">//--- Configure signals to fire only when reset } class=class="str">"cmt">//--- Evaluate and act on signals TradingModuleDemand Evaluate(Wallet* wallet, TradingModuleDemand preventOpenDemand, class="type">int level) { TradingModuleDemand newSignalsDemand = EvaluateSignals(wallet, preventOpenDemand, level); class=class="str">"cmt">//--- Evaluate signals if (newSignalsDemand != NoneDemand) { class=class="str">"cmt">//--- Check if demand exists EvaluateOpenConditions(wallet, newSignalsDemand); class=class="str">"cmt">//--- Evaluate open conditions } class="kw">return newSignalsDemand; class=class="str">"cmt">//--- Return new signal demand } class=class="str">"cmt">//--- Evaluate open signals without acting TradingModuleDemand EvaluateOpenSignals(Wallet* wallet, TradingModuleDemand preventOpenDemand, class="type">int requestedEvaluationLevel) { TradingModuleDemand openDemands[]; class=class="str">"cmt">//--- Declare open demands array TradeAction tradeActionsToEvaluate[]; class=class="str">"cmt">//--- Declare actions to evaluate GetTradeActions(wallet, preventOpenDemand, tradeActionsToEvaluate); class=class="str">"cmt">//--- Retrieve actions AddPreviousDemandTradeActionIfMissing(tradeActionsToEvaluate); class=class="str">"cmt">//--- Add previous demand actions class="type">int level; class=class="str">"cmt">//--- Declare level if (requestedEvaluationLevel == class="num">0) { class=class="str">"cmt">//--- Check if level unspecified
「开仓信号如何被逐级汇总」
这段逻辑处在策略评估的末端:先决定评估层级 level,再逐个扫描待评估动作,把通过的开放类动作塞进 openDemands 数组,最后用 GetCombinedOpenDemand 做合并。 若外部没指定 requestedEvaluationLevel(值为 0),代码会调 GetTopLevel 给 level 封顶;当钱包里已开订单数达到 MaxNumberOfOpenOrders1 时,level 再 +1,意味着在订单数触顶后倾向于用更高层级过滤信号。外汇与贵金属杠杆高,这种层级抬升可能直接压低开仓频率。 循环里遇到 CloseBuyAction 或 CloseSellAction 直接 continue 跳过,说明平仓动作不在这里参与开放需求统计。OpenBuy 与 OpenSell 分支对称,都用 ArrayResize(openDemands, newSize, 8) 以 8 为储备步长扩容,再写入对应 Demand 常量。 下面这段是原文核心片段,逐行拆完你就能在 MT5 里对照改自己的信号聚合器: level = _moneyManager.GetNextLevel(wallet); // 若走资金管理分支,按钱包状态取下一评估层级 } else { level = requestedEvaluationLevel; // 否则用调用方指定的层级 } for (int i = 0; i < ArraySize(tradeActionsToEvaluate); i++) { // 遍历所有待评估交易动作
| if (tradeActionsToEvaluate[i] == CloseBuyAction | tradeActionsToEvaluate[i] == CloseSellAction) { // 碰到平仓动作 |
|---|
continue; // 直接跳过,不纳入开仓需求 } if (requestedEvaluationLevel == 0) { // 若调用方没指定层级 level = GetTopLevel(tradeActionsToEvaluate[i], level); // 按动作类型封顶层级 if (wallet.GetOpenOrders().Count() >= MaxNumberOfOpenOrders1) { // 已开订单数达上限 level += 1; // 层级加一,提高过滤门槛 } } if (_advisorStrategy.GetAdvice(tradeActionsToEvaluate[i], level)) { // 策略在該层级给出建议 if (tradeActionsToEvaluate[i] == OpenBuyAction) { // 是买开动作 int size = ArraySize(openDemands); // 取当前数组长度 int newSize = size + 1; // 新长度 ArrayResize(openDemands, newSize, 8); // 以8为步长扩容 openDemands[newSize - 1] = OpenBuyDemand; // 尾部写入买开需求 } else if (tradeActionsToEvaluate[i] == OpenSellAction) { // 是卖开动作 int size = ArraySize(openDemands); // 取当前数组长度 int newSize = size + 1; // 新长度 ArrayResize(openDemands, newSize, 8); // 以8为步长扩容 openDemands[newSize - 1] = OpenSellDemand; // 尾部写入卖开需求 } } } TradingModuleDemand combinedOpenSignalDemand = OpenModuleBase::GetCombinedOpenDemand(openDemands); // 合并所有开放需求为统一信号 把 MaxNumberOfOpenOrders1 调小,比如从默认设成 3,能在回测中明显看到层级 +1 触发更早,从而观察信号稀疏化对净值曲线的可能影响。
level = _moneyManager.GetNextLevel(wallet); class=class="str">"cmt">//--- Set level based on orders } else { level = requestedEvaluationLevel; class=class="str">"cmt">//--- Use specified level } for (class="type">int i = class="num">0; i < ArraySize(tradeActionsToEvaluate); i++) { class=class="str">"cmt">//--- Iterate actions if (tradeActionsToEvaluate[i] == CloseBuyAction || tradeActionsToEvaluate[i] == CloseSellAction) { class=class="str">"cmt">//--- Skip close actions class="kw">continue; class=class="str">"cmt">//--- Move to next } if (requestedEvaluationLevel == class="num">0) { class=class="str">"cmt">//--- Check if level unspecified level = GetTopLevel(tradeActionsToEvaluate[i], level); class=class="str">"cmt">//--- Cap level if (wallet.GetOpenOrders().Count() >= MaxNumberOfOpenOrders1) { class=class="str">"cmt">//--- Check order limit level += class="num">1; class=class="str">"cmt">//--- Increment level } } if (_advisorStrategy.GetAdvice(tradeActionsToEvaluate[i], level)) { class=class="str">"cmt">//--- Check if action advised if (tradeActionsToEvaluate[i] == OpenBuyAction) { class=class="str">"cmt">//--- Check Buy action class="type">int size = ArraySize(openDemands); class=class="str">"cmt">//--- Get current size class="type">int newSize = size + class="num">1; class=class="str">"cmt">//--- Calculate new size ArrayResize(openDemands, newSize, class="num">8); class=class="str">"cmt">//--- Resize array openDemands[newSize - class="num">1] = OpenBuyDemand; class=class="str">"cmt">//--- Add Buy demand } else if (tradeActionsToEvaluate[i] == OpenSellAction) { class=class="str">"cmt">//--- Check Sell action class="type">int size = ArraySize(openDemands); class=class="str">"cmt">//--- Get current size class="type">int newSize = size + class="num">1; class=class="str">"cmt">//--- Calculate new size ArrayResize(openDemands, newSize, class="num">8); class=class="str">"cmt">//--- Resize array openDemands[newSize - class="num">1] = OpenSellDemand; class=class="str">"cmt">//--- Add Sell demand } } } TradingModuleDemand combinedOpenSignalDemand = OpenModuleBase::GetCombinedOpenDemand(openDemands); class=class="str">"cmt">//--- Combine open demands
◍ 多单信号过滤与双向挂单动作的生成逻辑
这段逻辑先基于前一级需求修正当前的需求信号,再把被禁止开仓的需求过滤掉,最后回传最终的开仓信号。注意 level - 1 的传参,意味着信号是参照上一档位的需求做的回溯,不是当前价附近的裸信号。 GetTradeActions 负责把钱包状态翻译成具体交易动作。若钱包里已有持仓(Count() > 0),就取第一张单的类型:Buy 单则往结果里塞 OpenBuyAction 和 CloseBuyAction,Sell 单则对应塞 OpenSellAction 和 CloseSellAction;遇到非买卖类型的单会弹 Alert("Unsupported ordertype") 报错。 若钱包无持仓,函数直接构造一组双向动作:先 resize 加一位放 OpenBuyAction,再 resize 加一位放 OpenSellAction。ArrayResize 的第三个参数 8 是内存预留步长,连续加元素时能把重新分配次数压到较低水平。 在 MT5 里把这段接进 EA 后,可故意让钱包先空仓再已有 Buy 单,分别打印 result 数组长度——空仓时应为 2(买+卖),有 Buy 单时也为 2(开买+平买),以此验证分支没写反。外汇与贵金属波动剧烈,这类自动动作链一旦分支错配,可能在没有止损预期的位置下单,实盘前务必用策略测试器跑通。
TradingModuleDemand multiOrderOpenSignal = GetOpenDemandBasedOnPreviousOpenDemand(combinedOpenSignalDemand, level - class="num">1); class=class="str">"cmt">//--- Adjust for previous demand multiOrderOpenSignal = FilterPreventOpenDemand(multiOrderOpenSignal, preventOpenDemand); class=class="str">"cmt">//--- Filter prevent-open demands class="kw">return multiOrderOpenSignal; class=class="str">"cmt">//--- Return final open signal } class=class="str">"cmt">//--- Retrieve trade actions(custom for multiple orders) class="type">void GetTradeActions(Wallet* wallet, TradingModuleDemand preventOpenDemand, TradeAction& result[]) { if (wallet.GetOpenOrders().Count() > class="num">0) { class=class="str">"cmt">//--- Check if open orders exist Order* firstOrder = wallet.GetOpenOrders().Get(class="num">0); class=class="str">"cmt">//--- Get first open order if (firstOrder.Type == ORDER_TYPE_BUY) { class=class="str">"cmt">//--- Check if Buy order ArrayResize(result, ArraySize(result) + class="num">1, class="num">8); class=class="str">"cmt">//--- Resize array result[class="num">0] = OpenBuyAction; class=class="str">"cmt">//--- Add open Buy action ArrayResize(result, ArraySize(result) + class="num">1, class="num">8); class=class="str">"cmt">//--- Resize array result[class="num">1] = CloseBuyAction; class=class="str">"cmt">//--- Add close Buy action } else if (firstOrder.Type == ORDER_TYPE_SELL) { class=class="str">"cmt">//--- Check if Sell order ArrayResize(result, ArraySize(result) + class="num">1, class="num">8); class=class="str">"cmt">//--- Resize array result[class="num">0] = OpenSellAction; class=class="str">"cmt">//--- Add open Sell action ArrayResize(result, ArraySize(result) + class="num">1, class="num">8); class=class="str">"cmt">//--- Resize array result[class="num">1] = CloseSellAction; class=class="str">"cmt">//--- Add close Sell action } else { Alert("Unsupported ordertype"); class=class="str">"cmt">//--- Log error } } else { ArrayResize(result, ArraySize(result) + class="num">1, class="num">8); class=class="str">"cmt">//--- Resize array result[class="num">0] = OpenBuyAction; class=class="str">"cmt">//--- Add open Buy action ArrayResize(result, ArraySize(result) + class="num">1, class="num">8); class=class="str">"cmt">//--- Resize array result[class="num">1] = OpenSellAction; class=class="str">"cmt">//--- Add open Sell action } }
平仓信号的评估与合并逻辑
EA 在每一轮 tick 里要先判断该不该平掉已有仓位,这段函数就是专门处理平仓诉求的。它先取出当前钱包允许的交易动作,再逐个筛查其中属于 CloseBuyAction 和 CloseSellAction 的指令,非平仓类直接 continue 跳过。 筛查时用 _advisorStrategy.GetAdvice(action, 1) 做一级信号确认。若策略在 level 1 建议平多,就给 closeDemands 数组追加 CloseBuyDemand;若建议平空则追加 CloseSellDemand。数组每次扩容预留 8 个元素(ArrayResize 第三参),减少频繁重分配的开销。 所有平仓诉求收集完后,交给 OpenModuleBase::GetCombinedCloseDemand 做合并,返回统一的 TradingModuleDemand。你在 MT5 里接自己策略时,可以把 level 1 换成别的过滤强度,观察平仓频率变化——外汇和贵金属波动剧烈,信号误触可能带来高频磨损,务必先在模拟盘验证。 下面这段是原文核心函数,逐行拆解看结构: //--- Evaluate close signals TradingModuleDemand EvaluateCloseSignals(Wallet* wallet, TradingModuleDemand preventOpenDemand) { // 定义平仓信号评估函数,入参为钱包指针与阻止开仓诉求 TradingModuleDemand closeDemands[]; // 声明平仓诉求动态数组 TradeAction tradeActionsToEvaluate[]; // 声明待评估交易动作数组 GetTradeActions(wallet, preventOpenDemand, tradeActionsToEvaluate); // 从钱包拉取待评估动作 for (int i = 0; i < ArraySize(tradeActionsToEvaluate); i++) { // 遍历每个动作 if (tradeActionsToEvaluate[i] != CloseBuyAction && tradeActionsToEvaluate[i] != CloseSellAction) { // 若非平买也非平卖 continue; // 跳过,进入下一轮 } if (_advisorStrategy.GetAdvice(tradeActionsToEvaluate[i], 1)) { // 一级建议触发 if (tradeActionsToEvaluate[i] == CloseBuyAction) { // 若是平买动作 int size = ArraySize(closeDemands); // 取当前数组长 int newSize = size + 1; // 新长度+1 ArrayResize(closeDemands, newSize, 8); // 扩容并预留8 closeDemands[newSize - 1] = CloseBuyDemand; // 末尾写入平买诉求 } else if (tradeActionsToEvaluate[i] == CloseSellAction) { // 若是平卖动作 int size = ArraySize(closeDemands); // 取当前数组长 int newSize = size + 1; // 新长度+1 ArrayResize(closeDemands, newSize, 8); // 扩容并预留8 closeDemands[newSize - 1] = CloseSellDemand; // 末尾写入平卖诉求 } } } TradingModuleDemand combinedCloseSignalDemand = OpenModuleBase::GetCombinedCloseDemand(closeDemands); // 合并平仓诉求 return combinedCloseSignalDemand; // 返回合并结果 }
class=class="str">"cmt">//--- Evaluate close signals TradingModuleDemand EvaluateCloseSignals(Wallet* wallet, TradingModuleDemand preventOpenDemand) { TradingModuleDemand closeDemands[]; class=class="str">"cmt">//--- Declare close demands array TradeAction tradeActionsToEvaluate[]; class=class="str">"cmt">//--- Declare actions to evaluate GetTradeActions(wallet, preventOpenDemand, tradeActionsToEvaluate); class=class="str">"cmt">//--- Retrieve actions for (class="type">int i = class="num">0; i < ArraySize(tradeActionsToEvaluate); i++) { class=class="str">"cmt">//--- Iterate actions if (tradeActionsToEvaluate[i] != CloseBuyAction && tradeActionsToEvaluate[i] != CloseSellAction) { class=class="str">"cmt">//--- Skip non-close actions class="kw">continue; class=class="str">"cmt">//--- Move to next } if (_advisorStrategy.GetAdvice(tradeActionsToEvaluate[i], class="num">1)) { class=class="str">"cmt">//--- Check if action advised(level class="num">1) if (tradeActionsToEvaluate[i] == CloseBuyAction) { class=class="str">"cmt">//--- Check Buy close class="type">int size = ArraySize(closeDemands); class=class="str">"cmt">//--- Get current size class="type">int newSize = size + class="num">1; class=class="str">"cmt">//--- Calculate new size ArrayResize(closeDemands, newSize, class="num">8); class=class="str">"cmt">//--- Resize array closeDemands[newSize - class="num">1] = CloseBuyDemand; class=class="str">"cmt">//--- Add Buy close demand } else if (tradeActionsToEvaluate[i] == CloseSellAction) { class=class="str">"cmt">//--- Check Sell close class="type">int size = ArraySize(closeDemands); class=class="str">"cmt">//--- Get current size class="type">int newSize = size + class="num">1; class=class="str">"cmt">//--- Calculate new size ArrayResize(closeDemands, newSize, class="num">8); class=class="str">"cmt">//--- Resize array closeDemands[newSize - class="num">1] = CloseSellDemand; class=class="str">"cmt">//--- Add Sell close demand } } } TradingModuleDemand combinedCloseSignalDemand = OpenModuleBase::GetCombinedCloseDemand(closeDemands); class=class="str">"cmt">//--- Combine close demands class="kw">return combinedCloseSignalDemand; class=class="str">"cmt">//--- Return combined demand }
「持仓上限如何抬升评估层级」
这段逻辑处理的是:在已有挂单或持仓接近上限时,系统如何动态抬高对交易动作的评估层级,再把通过策略顾问认可的开仓意图写进需求数组。 先声明待评估动作数组,从钱包与防重复开仓标记里取出候选动作,并补回上一轮缺失的需求动作。若调用方没指定评估层级(requestedEvaluationLevel==0),就由资金管理器按当前订单数给一个下一层级;否则直接用指定值。 遍历每个动作时,先用 GetTopLevel 封顶,再把层级与钱包实际持仓数比对:当已开订单数 ≥ MaxNumberOfOpenOrders1,评估层级额外 +1,意味着临近满仓时系统倾向更保守地看待新开仓。 若策略顾问在该层级返回 true,就按动作类型把 OpenBuyDemand 或 OpenSellDemand 追加进 openDemands,数组以 8 为步长扩容。外汇与贵金属杠杆交易高风险,这类自动加层逻辑可能让你在震荡市错过突破,也可能在趋势市少挨一刀,开 MT5 把 MaxNumberOfOpenOrders1 改成 2 和 5 分别跑一遍回测最直观。
TradeAction tradeActionsToEvaluate[]; class=class="str">"cmt">//--- Declare actions to evaluate GetTradeActions(wallet, preventOpenDemand, tradeActionsToEvaluate); class=class="str">"cmt">//--- Retrieve actions AddPreviousDemandTradeActionIfMissing(tradeActionsToEvaluate); class=class="str">"cmt">//--- Add previous demand actions class="type">int moneyManagementLevel; class=class="str">"cmt">//--- Declare level if (requestedEvaluationLevel == class="num">0) { class=class="str">"cmt">//--- Check if level unspecified moneyManagementLevel = _moneyManager.GetNextLevel(wallet); class=class="str">"cmt">//--- Set level based on orders } else { moneyManagementLevel = requestedEvaluationLevel; class=class="str">"cmt">//--- Use specified level } for (class="type">int i = class="num">0; i < ArraySize(tradeActionsToEvaluate); i++) { class=class="str">"cmt">//--- Iterate actions class="type">int tradeActionEvaluationLevel = GetTopLevel(tradeActionsToEvaluate[i], moneyManagementLevel); class=class="str">"cmt">//--- Cap level if (wallet.GetOpenOrders().Count() >= MaxNumberOfOpenOrders1) { class=class="str">"cmt">//--- Check order limit tradeActionEvaluationLevel += class="num">1; class=class="str">"cmt">//--- Increment level } if (_advisorStrategy.GetAdvice(tradeActionsToEvaluate[i], tradeActionEvaluationLevel)) { class=class="str">"cmt">//--- Check if action advised if (tradeActionsToEvaluate[i] == OpenBuyAction) { class=class="str">"cmt">//--- Check Buy open class="type">int size = ArraySize(openDemands); class=class="str">"cmt">//--- Get current size class="type">int newSize = size + class="num">1; class=class="str">"cmt">//--- Calculate new size ArrayResize(openDemands, newSize, class="num">8); class=class="str">"cmt">//--- Resize array openDemands[newSize - class="num">1] = OpenBuyDemand; class=class="str">"cmt">//--- Add Buy demand } else if (tradeActionsToEvaluate[i] == OpenSellAction) { class=class="str">"cmt">//--- Check Sell open class="type">int size = ArraySize(openDemands); class=class="str">"cmt">//--- Get current size class="type">int newSize = size + class="num">1; class=class="str">"cmt">//--- Calculate new size ArrayResize(openDemands, newSize, class="num">8); class=class="str">"cmt">//--- Resize array openDemands[newSize - class="num">1] = OpenSellDemand; class=class="str">"cmt">//--- Add Sell demand } else if (tradeActionsToEvaluate[i] == CloseBuyAction) { class=class="str">"cmt">//--- Check Buy close class="type">int size = ArraySize(closeDemands); class=class="str">"cmt">//--- Get current size
◍ 平仓与开仓需求的合并返回逻辑
这段逻辑在遍历交易动作数组后,把识别出的平仓需求分别塞进 closeDemands 动态数组。每次遇到 CloseBuyAction 或 CloseSellAction,先用 ArraySize 取当前长度,newSize 在旧长度上加 1,ArrayResize 的第三参填 8,意味着预留 8 个元素的存储增量,避免频繁重分配。 closeDemands[newSize-1] 写入对应需求枚举后,循环结束调用 GetCombinedCloseDemand 做合并。若合并结果不是 NoneDemand,函数直接 return,优先处理平仓,不再往下走开仓分支。 开仓侧则用 GetCombinedOpenDemand 拿基础信号,再经 GetOpenDemandBasedOnPreviousOpenDemand 按已开单数调整,previousSignalDemand 被更新为本次合并开仓需求,最后过一道 FilterPreventOpenDemand 把禁开信号滤掉再返回。 EvaluateOpenConditions 里若收到 OpenBuySellDemand 直接 return,说明该模块不支持锁仓对冲。否则取 AccountFreeMargin_LibFunc 的可用保证金,对 BUY 信号用 MarginRequired 校验手数所需保证金是否够用。外汇与贵金属保证金波动大,这类校验失败可能随时发生,实盘前建议在 MT5 策略测试器里改 _moneyManager.GetLotSize() 跑一遍边际场景。
class="type">int newSize = size + class="num">1; class=class="str">"cmt">//--- Calculate new size ArrayResize(closeDemands, newSize, class="num">8); class=class="str">"cmt">//--- Resize array closeDemands[newSize - class="num">1] = CloseBuyDemand; class=class="str">"cmt">//--- Add Buy close demand } else if (tradeActionsToEvaluate[i] == CloseSellAction) { class=class="str">"cmt">//--- Check Sell close class="type">int size = ArraySize(closeDemands); class=class="str">"cmt">//--- Get current size class="type">int newSize = size + class="num">1; class=class="str">"cmt">//--- Calculate new size ArrayResize(closeDemands, newSize, class="num">8); class=class="str">"cmt">//--- Resize array closeDemands[newSize - class="num">1] = CloseSellDemand; class=class="str">"cmt">//--- Add Sell close demand } } } TradingModuleDemand combinedCloseSignalDemand = OpenModuleBase::GetCombinedCloseDemand(closeDemands); class=class="str">"cmt">//--- Combine close demands if (combinedCloseSignalDemand != NoneDemand) { class=class="str">"cmt">//--- Check if close demand exists class="kw">return combinedCloseSignalDemand; class=class="str">"cmt">//--- Return close demand } TradingModuleDemand combinedOpenSignalDemand = OpenModuleBase::GetCombinedOpenDemand(openDemands); class=class="str">"cmt">//--- Combine open demands TradingModuleDemand multiOrderOpenSignal = GetOpenDemandBasedOnPreviousOpenDemand(combinedOpenSignalDemand, GetNumberOfOpenOrders(wallet)); class=class="str">"cmt">//--- Adjust for previous demand previousSignalDemand = combinedOpenSignalDemand; class=class="str">"cmt">//--- Update previous demand multiOrderOpenSignal = FilterPreventOpenDemand(multiOrderOpenSignal, preventOpenDemand); class=class="str">"cmt">//--- Filter prevent-open demands class="kw">return multiOrderOpenSignal; class=class="str">"cmt">//--- Return final signal } class=class="str">"cmt">//--- Evaluate open conditions and add orders class="type">void EvaluateOpenConditions(Wallet* wallet, TradingModuleDemand signalDemand) { if (signalDemand == OpenBuySellDemand) { class=class="str">"cmt">//--- Check Buy/Sell demand class="kw">return; class=class="str">"cmt">//--- Exit(hedging not supported) } else { class="type">class="kw">double currentFreeMargin = AccountFreeMargin_LibFunc(); class=class="str">"cmt">//--- Retrieve free margin class="type">class="kw">double requiredMargin; class=class="str">"cmt">//--- Declare required margin if (signalDemand == OpenBuyDemand) { class=class="str">"cmt">//--- Check Buy demand if (!MarginRequired(ORDER_TYPE_BUY, _moneyManager.GetLotSize(), requiredMargin)) { class=class="str">"cmt">//--- Check margin
开仓前的保证金闸门与反手逻辑
这段逻辑卡在信号转为实际挂单之前,先过两道保证金校验。Buy 和 Sell 分支都先调 MarginRequired 算所需保证金,若计算失败直接 return;随后比对 currentFreeMargin 与 requiredMargin,不足就走 HandleErrors 并记录日志后退出,避免透支账户。 外汇与贵金属杠杆高,保证金不足时硬开仓可能触发强平,这套前置拦截能降低非预期爆仓概率。 挂单动作本身很轻量:wallet.GetPendingOpenOrders().Add(OpenOrder(ORDER_TYPE_BUY, false)) 把市价 Buy 加入待开池,Sell 同理。注意第二个参数 false 代表非即时执行,交由后续模块统一处理。 反手判定在 GetOpenDemandBasedOnPreviousOpenDemand 里:若当前无持仓或前次信号为空,直接采用新信号;只有前次 Buy 转 Sell 或前次 Sell 转 Buy 才放行反向需求,等同允许一次干净的反手,不追同方向加仓。
class="kw">return; class=class="str">"cmt">//--- Exit if margin check fails } if (currentFreeMargin < requiredMargin) { class=class="str">"cmt">//--- Check sufficient margin HandleErrors("Not enough free margin to open buy order with requested volume."); class=class="str">"cmt">//--- Log error class="kw">return; class=class="str">"cmt">//--- Exit } wallet.GetPendingOpenOrders().Add(OpenOrder(ORDER_TYPE_BUY, false)); class=class="str">"cmt">//--- Add Buy order } else if (signalDemand == OpenSellDemand) { class=class="str">"cmt">//--- Check Sell demand if (!MarginRequired(ORDER_TYPE_SELL, _moneyManager.GetLotSize(), requiredMargin)) { class=class="str">"cmt">//--- Check margin class="kw">return; class=class="str">"cmt">//--- Exit if margin check fails } if (currentFreeMargin < requiredMargin) { class=class="str">"cmt">//--- Check sufficient margin HandleErrors("Not enough free margin to open sell order with requested volume."); class=class="str">"cmt">//--- Log error class="kw">return; class=class="str">"cmt">//--- Exit } wallet.GetPendingOpenOrders().Add(OpenOrder(ORDER_TYPE_SELL, false)); class=class="str">"cmt">//--- Add Sell order } } class=class="str">"cmt">//--- Adjust open demand based on previous demand TradingModuleDemand GetOpenDemandBasedOnPreviousOpenDemand(TradingModuleDemand openDemand, class="type">int numberOfOpenOrders) { if (numberOfOpenOrders == class="num">0 || previousSignalDemand == NoneDemand) { class=class="str">"cmt">//--- Check no orders or no previous demand class="kw">return openDemand; class=class="str">"cmt">//--- Return current demand } if (previousSignalDemand == OpenBuyDemand && openDemand == OpenSellDemand) { class=class="str">"cmt">//--- Check Buy to Sell class="kw">switch class="kw">return openDemand; class=class="str">"cmt">//--- Allow Sell demand } else if (previousSignalDemand == OpenSellDemand && openDemand == OpenBuyDemand) { class=class="str">"cmt">//--- Check Sell to Buy class="kw">switch class="kw">return openDemand; class=class="str">"cmt">//--- Allow Buy demand }
「需求层级封顶与历史信号补位」
策略在评估多表达式信号时,必须先给遍历层级设一道天花板。GetTopLevel 会先向 _advisorStrategy 索要当前交易动作对应的表达式总数,若传入的 level 大于该数量,就强制把 level 压回 numberOfExpressions,避免越界访问空表达式。 历史需求补位是另一处容易漏掉的逻辑。AddPreviousDemandTradeActionIfMissing 先判断 previousSignalDemand 是否为 NoneDemand,若为空直接退出;否则按 Buy / Sell / BuySell 三种前一需求,把对应的 OpenBuyAction 或 OpenSellAction 塞进结果数组,保证上一根 K 线未成交的信号在本根不被吞掉。 具体到单一需求写入,AddPreviousDemandTradeAction 先用 foundPreviousDemand 标记,再跑一遍 result 数组确认同类型 action 是否已存在,存在才置 true。这一层去重能压住 MT5 回测里同一信号连发多单的概率,实盘外汇与贵金属波动剧烈,重信号堆叠会放大滑点风险。
class="kw">private: class=class="str">"cmt">//--- Cap evaluation level class="type">int GetTopLevel(TradeAction tradeAction, class="type">int level) { class="type">int numberOfExpressions = _advisorStrategy.GetNumberOfExpressions(tradeAction); class=class="str">"cmt">//--- Retrieve expression count if (level > numberOfExpressions) { class=class="str">"cmt">//--- Check if level exceeds expressions level = numberOfExpressions; class=class="str">"cmt">//--- Cap level } class="kw">return level; class=class="str">"cmt">//--- Return capped level } class=class="str">"cmt">//--- Add previous demand action if missing class="type">void AddPreviousDemandTradeActionIfMissing(TradeAction& result[]) { if (previousSignalDemand == NoneDemand) { class=class="str">"cmt">//--- Check if no previous demand class="kw">return; class=class="str">"cmt">//--- Exit } class="type">bool foundPreviousDemand = false; class=class="str">"cmt">//--- Initialize found flag if (previousSignalDemand == OpenBuyDemand) { class=class="str">"cmt">//--- Check Buy demand AddPreviousDemandTradeAction(result, OpenBuyDemand, OpenBuyAction); class=class="str">"cmt">//--- Add Buy action } else if (previousSignalDemand == OpenSellDemand) { class=class="str">"cmt">//--- Check Sell demand AddPreviousDemandTradeAction(result, OpenSellDemand, OpenSellAction); class=class="str">"cmt">//--- Add Sell action } else if (previousSignalDemand == OpenBuySellDemand) { class=class="str">"cmt">//--- Check Buy/Sell demand AddPreviousDemandTradeAction(result, OpenBuyDemand, OpenBuyAction); class=class="str">"cmt">//--- Add Buy action AddPreviousDemandTradeAction(result, OpenSellDemand, OpenSellAction); class=class="str">"cmt">//--- Add Sell action } } class=class="str">"cmt">//--- Add specific previous demand action class="type">void AddPreviousDemandTradeAction(TradeAction& result[], TradingModuleDemand demand, TradeAction action) { class="type">bool foundPreviousDemand = false; class=class="str">"cmt">//--- Initialize found flag if (previousSignalDemand == demand) { class=class="str">"cmt">//--- Check matching demand for (class="type">int i = class="num">0; i < ArraySize(result); i++) { class=class="str">"cmt">//--- Iterate actions if (action == result[i]) { class=class="str">"cmt">//--- Check if action exists foundPreviousDemand = true; class=class="str">"cmt">//--- Set found flag } }
◍ 需求过滤与接口骨架
这段逻辑解决一个实战痛点:当多单挂单需求遇上「禁止买入」类风控信号时,系统不能傻等,而是要当场把需求改写为可执行的另一侧或清零。 FilterPreventOpenDemand 函数用枚举比对做硬过滤:若原需求是 OpenBuySellDemand 且风控给的是 NoBuyDemand,直接回退成 OpenSellDemand,保留卖侧可能。若给的是 NoOpenDemand,则无论多空一律返回 NoneDemand,概率上会完全暂停开仓。 末尾的 ITrader 接口只留两个方法——Init 做初始化、HandleTick 吃每笔报价。把交易员逻辑抽象成接口,意味着同一套风控模块可以热插拔进不同 EA,开 MT5 新建类继承它就能复用上面的过滤链。 外汇与贵金属波动剧烈,这类自动拦截只降低误操作概率,不消除爆仓风险,参数须先在策略测试器跑历史段验证。
if (!foundPreviousDemand) { class=class="str">"cmt">//--- 检查是否缺失先前需求 ArrayResize(result, ArraySize(result) + class="num">1, class="num">8); class=class="str">"cmt">//--- 数组扩容,预留8元素步长 result[ArraySize(result) - class="num">1] = action; class=class="str">"cmt">//--- 把当前动作追加进结果数组 } } class=class="str">"cmt">//--- 过滤阻止开仓的需求 TradingModuleDemand FilterPreventOpenDemand(TradingModuleDemand multiOrderOpendDemand, TradingModuleDemand preventOpenDemand) { if (multiOrderOpendDemand == NoneDemand) { class=class="str">"cmt">//--- 检查无需求 class="kw">return multiOrderOpendDemand; class=class="str">"cmt">//--- 返回无需求 } else if (multiOrderOpendDemand == OpenBuyDemand && (preventOpenDemand == NoBuyDemand || preventOpenDemand == NoOpenDemand)) { class=class="str">"cmt">//--- 检查被阻的买 class="kw">return NoneDemand; class=class="str">"cmt">//--- 阻断买需求 } else if (multiOrderOpendDemand == OpenSellDemand && (preventOpenDemand == NoSellDemand || preventOpenDemand == NoOpenDemand)) { class=class="str">"cmt">//--- 检查被阻的卖 class="kw">return NoneDemand; class=class="str">"cmt">//--- 阻断卖需求 } else if (multiOrderOpendDemand == OpenBuySellDemand) { class=class="str">"cmt">//--- 检查买卖双向需求 if (preventOpenDemand == NoBuyDemand) { class=class="str">"cmt">//--- 检查禁买 class="kw">return OpenSellDemand; class=class="str">"cmt">//--- 仅允许卖需求 } else if (preventOpenDemand == NoSellDemand) { class=class="str">"cmt">//--- 检查禁卖 class="kw">return OpenBuyDemand; class=class="str">"cmt">//--- 仅允许买需求 } else if (preventOpenDemand == NoOpenDemand) { class=class="str">"cmt">//--- 检查禁开 class="kw">return NoneDemand; class=class="str">"cmt">//--- 阻断全部需求 } } class="kw">return multiOrderOpendDemand; class=class="str">"cmt">//--- 返回未过滤需求 } }; class=class="str">"cmt">//--- 定义交易员接口 interface ITrader { class="type">void HandleTick(); class=class="str">"cmt">//--- 处理报价事件 class="type">void Init(); class=class="str">"cmt">//--- 初始化交易员 }
EA 类的成员装配与析构清理
在 MT5 的 EA 架构里,把交易逻辑拆成策略、资金、钱包几个独立对象后,真正的粘合点在 EA 类自身。私有成员里用 _firstTick 标记首根 K 线,避免初始化前就跑信号;_tradeStrategy、_advisorStrategy、_moneyManager、_wallet 四个指针分别接管不同职责,互不耦合。 构造函数 EA() 里先置 _firstTick=true,随后 new 出 Wallet 并调用 SetLastClosedOrdersByTimeframe(DisplayOrderDuringTimeframe) 限定历史平仓单的回看周期。AdvisorStrategy 通过 RegisterOpenBuy / RegisterOpenSell 挂上 Level1 信号,权重参数填 1;MoneyManager 以钱包实例为构造入参,TradeStrategy 再包一层 MultipleOpenModule_1,并注册 TakeProfitCloseModule_1 与 StopLossCloseModule_1 做平仓。 析构 ~EA() 必须按创建逆序 delete 三个策略对象,否则 MT5 终端跑久了会漏内存。外汇与贵金属杠杆高,这类对象若未清理,实盘重加载 EA 时可能拖慢 tick 响应。 下面这段是原文里的核心声明与构造骨架,逐行拆完就能在 MetaEditor 里照抄验证。
Wallet* GetWallet(); class=class="str">"cmt">//--- Retrieve wallet }; class=class="str">"cmt">//--- Declare global trader pointer ITrader *_ea; class=class="str">"cmt">//--- Store EA instance class=class="str">"cmt">//--- Define main Expert Advisor class class EA : class="kw">public ITrader { class="kw">private: class="type">bool _firstTick; class=class="str">"cmt">//--- Track first tick TradeStrategy* _tradeStrategy; class=class="str">"cmt">//--- Store trade strategy AdvisorStrategy* _advisorStrategy; class=class="str">"cmt">//--- Store advisor strategy IMoneyManager* _moneyManager; class=class="str">"cmt">//--- Store money manager Wallet* _wallet; class=class="str">"cmt">//--- Store wallet class="kw">public: class=class="str">"cmt">//--- Initialize EA class="type">void EA() { _firstTick = true; class=class="str">"cmt">//--- Set first tick flag _wallet = new Wallet(); class=class="str">"cmt">//--- Create wallet _wallet.SetLastClosedOrdersByTimeframe(DisplayOrderDuringTimeframe); class=class="str">"cmt">//--- Set closed orders timeframe _advisorStrategy = new AdvisorStrategy(); class=class="str">"cmt">//--- Create advisor strategy _advisorStrategy.RegisterOpenBuy(new ASOpenBuyLevel1(), class="num">1); class=class="str">"cmt">//--- Register Buy signal _advisorStrategy.RegisterOpenSell(new ASOpenSellLevel1(), class="num">1); class=class="str">"cmt">//--- Register Sell signal _moneyManager = new MoneyManager(_wallet); class=class="str">"cmt">//--- Create money manager _tradeStrategy = new TradeStrategy(new MultipleOpenModule_1(_advisorStrategy, _moneyManager)); class=class="str">"cmt">//--- Create trade strategy _tradeStrategy.RegisterCloseModule(new TakeProfitCloseModule_1()); class=class="str">"cmt">//--- Register TP module _tradeStrategy.RegisterCloseModule(new StopLossCloseModule_1()); class=class="str">"cmt">//--- Register SL module } class=class="str">"cmt">//--- Destructor to clean up EA class="type">void ~EA() { class="kw">delete(_tradeStrategy); class=class="str">"cmt">//--- Delete trade strategy class="kw">delete(_moneyManager); class=class="str">"cmt">//--- Delete money manager class="kw">delete(_advisorStrategy); class=class="str">"cmt">//--- Delete advisor strategy
「EA 生命周期里的订单同步与tick调度」
这段代码片段展示了 MT5 中一个 EA 框架在初始化与逐 tick 运行时的核心调度逻辑。Init() 只在加载时跑一次,负责判断运行环境、载入合约单位和 broker 端已有持仓。 HandleTick() 是每笔报价触发的主循环。注意它先用 MQLInfoInteger(MQL_TESTER)==0 判断是否在实盘或可视化模式,只有非回测环境才调用 SyncOrders() 做账户订单同步;若 AllowManualTPSLChanges 为真,还会同步手动改的止盈止损。 行情侧先更新 Ask/Bid 函数并刷新持仓盈亏,随后在 StopEA 为假时推进钱包与策略模块。只有当挂单池里开仓与平仓待执行数量都为 0(Count()==0)才调用 _tradeStrategy.Evaluate(_wallet),避免重复评估。 实盘跑这套逻辑时,外汇与贵金属品种点差跳变频繁,SyncOrders 若在 tick 密集段被反复调用,可能拖慢执行;建议你在 MT5 策略测试器用可视化模式单步验证同步时机。
class="kw">delete(_wallet); class=class="str">"cmt">//--- Delete wallet } class=class="str">"cmt">//--- Initialize EA components class="type">void Init() { IsDemoLiveOrVisualMode = !MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_VISUAL_MODE); class=class="str">"cmt">//--- Set mode flag UnitsOneLot = MarketInfo_LibFunc(Symbol(), MODE_LOTSIZE); class=class="str">"cmt">//--- Set lot size SetOrderGrouping(); class=class="str">"cmt">//--- Configure order grouping _wallet.LoadOrdersFromBroker(); class=class="str">"cmt">//--- Load orders from broker } class=class="str">"cmt">//--- Handle tick event class="type">void HandleTick() { if (MQLInfoInteger(MQL_TESTER) == class="num">0) { class=class="str">"cmt">//--- Check if not in tester SyncOrders(); class=class="str">"cmt">//--- Synchronize orders } if (AllowManualTPSLChanges) { class=class="str">"cmt">//--- Check if manual TP/SL allowed SyncManualTPSLChanges(); class=class="str">"cmt">//--- Synchronize manual TP/SL } AskFunc.Evaluate(); class=class="str">"cmt">//--- Update Ask price BidFunc.Evaluate(); class=class="str">"cmt">//--- Update Bid price UpdateOrders(); class=class="str">"cmt">//--- Update order profits if (!StopEA) { class=class="str">"cmt">//--- Check if EA not stopped _wallet.HandleTick(); class=class="str">"cmt">//--- Handle wallet tick _advisorStrategy.HandleTick(); class=class="str">"cmt">//--- Handle strategy tick if (_wallet.GetPendingOpenOrders().Count() == class="num">0 && _wallet.GetPendingCloseOrders().Count() == class="num">0) { class=class="str">"cmt">//--- Check no pending orders _tradeStrategy.Evaluate(_wallet); class=class="str">"cmt">//--- Evaluate strategy } if (ExecutePendingCloseOrders()) { class=class="str">"cmt">//--- Execute close orders if (!ExecutePendingOpenOrders()) { class=class="str">"cmt">//--- Execute open orders
◍ EA 钱包与订单组的同步陷阱
这段逻辑暴露了 MT5 EA 在订单状态管理上最容易翻车的地方:本地钱包(_wallet)与券商真实持仓之间一旦出现数量不一致,EA 必须主动重置并重新加载。代码里用 currentOpenOrders.Count() != (_wallet.GetOpenOrders().Count() + _wallet.GetPendingCloseOrders().Count()) 做 mismatch 判断,只要手动平仓、滑点删单或服务器重连丢包,就会触发 Print("(Manual) orderchanges detected...") 的告警并走重置分支。
SetOrderGrouping() 展示了分组激活的做法:先取 _tradeStrategy.CloseModules 数组大小,按模块数 ArrayResize(groups, size),再循环把每个平仓模块的 GetOrderGroupingType() 填进 groups[],最后 _wallet.ActivateOrderGroups(groups) 一次性激活。若你在策略里加了第 N 个平仓模块却忘了扩组,激活的订单组数量会少一组,部分持仓可能脱离 EA 管控。
GetWallet() 只是返回私有 _wallet 指针,外部调度器靠它读写持仓;SyncOrders() 里的 OrderRepository::GetOpenOrders(MagicNumber, NULL, Symbol()) 限定了魔术码与品种,说明跨品种或错魔术码的单子不会被纳入同步。实盘外汇与贵金属波动剧烈、滑点频发,这种不一致概率不低,建议在 MT5 策略测试器用「手动干预」脚本故意删单来验证重置是否生效。
HandleErrors(StringFormat("Open(all) order(s) failed. Please check EA %d and look at the Journal and Expert tab.", MagicNumber)); class=class="str">"cmt">//--- Log error } } else { HandleErrors(StringFormat("Close(all) order(s) failed! Please check EA %d and look at the Journal and Expert tab.", MagicNumber)); class=class="str">"cmt">//--- Log error } } else { if (ExecutePendingCloseOrders()) { class=class="str">"cmt">//--- Execute close orders _wallet.SetAllOpenOrdersToPendingClose(); class=class="str">"cmt">//--- Move open orders to pending close } else { HandleErrors(StringFormat("Close(all) order(s) failed! Please check EA %d and look at the Journal and Expert tab.", MagicNumber)); class=class="str">"cmt">//--- Log error } } if (_firstTick) { class=class="str">"cmt">//--- Check if first tick _firstTick = false; class=class="str">"cmt">//--- Clear first tick flag } } class=class="str">"cmt">//--- Retrieve wallet Wallet* GetWallet() { class="kw">return _wallet; class=class="str">"cmt">//--- Return wallet } class="kw">private: class=class="str">"cmt">//--- Configure order grouping class="type">void SetOrderGrouping() { class="type">int size = ArraySize(_tradeStrategy.CloseModules); class=class="str">"cmt">//--- Get close modules size ORDER_GROUP_TYPE groups[]; class=class="str">"cmt">//--- Declare groups array ArrayResize(groups, size); class=class="str">"cmt">//--- Resize array for (class="type">int i = class="num">0; i < ArraySize(_tradeStrategy.CloseModules); i++) { class=class="str">"cmt">//--- Iterate modules groups[i] = _tradeStrategy.CloseModules[i].GetOrderGroupingType(); class=class="str">"cmt">//--- Set grouping type } _wallet.ActivateOrderGroups(groups); class=class="str">"cmt">//--- Activate groups } class=class="str">"cmt">//--- Synchronize orders with broker class="type">void SyncOrders() { OrderCollection* currentOpenOrders = OrderRepository::GetOpenOrders(MagicNumber, NULL, Symbol()); class=class="str">"cmt">//--- Retrieve open orders if (currentOpenOrders.Count() != (_wallet.GetOpenOrders().Count() + _wallet.GetPendingCloseOrders().Count())) { class=class="str">"cmt">//--- Check order mismatch Print("(Manual) orderchanges detected" + " (found in MT: " + IntegerToString(currentOpenOrders.Count()) + " and in wallet: " + IntegerToString(_wallet.GetOpenOrders().Count()) + "), resetting EA, loading open orders."); class=class="str">"cmt">//--- Log mismatch
手动改止损止盈怎么被 EA 抓到
EA 在每次刷新持仓前会先清空旧的单子记录再向券商重新拉取,避免残留对象导致状态错位: _wallet.ResetOpenOrders(); _wallet.ResetPendingOrders(); _wallet.LoadOrdersFromBroker(); delete(currentOpenOrders); 这三行加一个 delete 就是把内存里的订单集合整个重置,实盘跑的时候若你手动在终端改了单,下一帧同步才不会读脏数据。 SyncManualTPSLChanges 专门盯着图表上的水平线。它用订单票号拼出 "_SL" 和 "_TP" 的对象名去 ObjectFind,若返回的不是 UINT_MAX 就说明你拖过线。 关键判断在于:若之前没记过手动止损(StopLossManual==0)但线价不等于算法算的最近止损,或记过却对不上当前线价,就把 order.StopLossManual 改写成线价。止盈同理。这样 EA 后续平仓逻辑就会尊重你手拖的价位,而不是覆盖回默认。 外汇与贵金属杠杆高,手动拖线改 SL/TP 虽能救急,但滑点可能让成交价偏离线价数个点到几十点,仓位管理仍要自己扛。
_wallet.ResetOpenOrders(); class=class="str">"cmt">//--- Reset open orders _wallet.ResetPendingOrders(); class=class="str">"cmt">//--- Reset pending orders _wallet.LoadOrdersFromBroker(); class=class="str">"cmt">//--- Reload orders } class="kw">delete(currentOpenOrders); class=class="str">"cmt">//--- Delete orders collection } class=class="str">"cmt">//--- Synchronize manual TP/SL changes class="type">void SyncManualTPSLChanges() { _wallet.GetOpenOrders().Rewind(); class=class="str">"cmt">//--- Reset orders iterator class="kw">while (_wallet.GetOpenOrders().HasNext()) { class=class="str">"cmt">//--- Iterate orders Order* order = _wallet.GetOpenOrders().Next(); class=class="str">"cmt">//--- Get order class="type">uint lineFindResult = ObjectFind(ChartID(), IntegerToString(order.Ticket) + "_SL"); class=class="str">"cmt">//--- Find SL line if (lineFindResult != UINT_MAX) { class=class="str">"cmt">//--- Check if SL line exists class="type">class="kw">double currentPosition = ObjectGetDouble(ChartID(), IntegerToString(order.Ticket) + "_SL", OBJPROP_PRICE); class=class="str">"cmt">//--- Get SL position if ((order.StopLossManual == class="num">0 && currentPosition != order.GetClosestSL()) || class=class="str">"cmt">//--- Check manual SL change (order.StopLossManual != class="num">0 && currentPosition != order.StopLossManual)) { class=class="str">"cmt">//--- Check manual SL mismatch order.StopLossManual = currentPosition; class=class="str">"cmt">//--- Update manual SL } } lineFindResult = ObjectFind(ChartID(), IntegerToString(order.Ticket) + "_TP"); class=class="str">"cmt">//--- Find TP line if (lineFindResult != UINT_MAX) { class=class="str">"cmt">//--- Check if TP line exists class="type">class="kw">double currentPosition = ObjectGetDouble(ChartID(), IntegerToString(order.Ticket) + "_TP", OBJPROP_PRICE); class=class="str">"cmt">//--- Get TP position if ((order.TakeProfitManual == class="num">0 && currentPosition != order.GetClosestTP()) || class=class="str">"cmt">//--- Check manual TP change (order.TakeProfitManual != class="num">0 && currentPosition != order.TakeProfitManual)) { class=class="str">"cmt">//--- Check manual TP mismatch order.TakeProfitManual = currentPosition; class=class="str">"cmt">//--- Update manual TP } } } } class=class="str">"cmt">//--- Update order profits class="type">void UpdateOrders() {
「持仓盈亏极值追踪与挂单平仓执行」
在 EA 的持仓维护逻辑里,先要把钱包里的未平仓订单迭代一遍,把每笔的浮动盈亏(以 pip 计)算出来并写回对象,同时刷新它历史区间的最低和最高 pip 收益。这段代码用 Rewind 重置迭代器,再靠 HasNext/Next 把当前所有 open orders 走完,属于典型的 O(n) 遍历,n 为当前持仓数。 遍历里对每笔 order 调用 CalculateProfitPips,若结果小于 LowestProfitPips 就更新下限,大于 HighestProfitPips 就更新上限。实盘中外汇与贵金属杠杆高、点差跳变频繁,这两个极值字段对后续动态止损或回撤统计有参考价值,但极端行情可能让极值瞬间失真。 另一段 ExecutePendingCloseOrders 负责执行待平挂单:先取 pending close 集合,数量为 0 或正有订单在开就直接返回 true;否则倒序遍历,遇到仍在等待成交(IsAwaitingDealExecution)的跳过并计成功数。你可以直接把这段拷进 MT5 的 class 方法里,接上自己的 _wallet 实现验证挂单平仓的触发节奏。
_wallet.GetOpenOrders().Rewind(); class=class="str">"cmt">//--- Reset orders iterator class="kw">while (_wallet.GetOpenOrders().HasNext()) { class=class="str">"cmt">//--- Iterate orders Order* order = _wallet.GetOpenOrders().Next(); class=class="str">"cmt">//--- Get order class="type">class="kw">double pipsProfit = order.CalculateProfitPips(); class=class="str">"cmt">//--- Calculate profit order.CurrentProfitPips = pipsProfit; class=class="str">"cmt">//--- Update current profit if (pipsProfit < order.LowestProfitPips) { class=class="str">"cmt">//--- Check if lowest profit order.LowestProfitPips = pipsProfit; class=class="str">"cmt">//--- Update lowest profit } else if (pipsProfit > order.HighestProfitPips) { class=class="str">"cmt">//--- Check if highest profit order.HighestProfitPips = pipsProfit; class=class="str">"cmt">//--- Update highest profit } } } class=class="str">"cmt">//--- Execute pending close orders class="type">bool ExecutePendingCloseOrders() { OrderCollection* pendingCloseOrders = _wallet.GetPendingCloseOrders(); class=class="str">"cmt">//--- Retrieve pending close orders class="type">int ordersToCloseCount = pendingCloseOrders.Count(); class=class="str">"cmt">//--- Get count if (ordersToCloseCount == class="num">0) { class=class="str">"cmt">//--- Check if no orders class="kw">return true; class=class="str">"cmt">//--- Return true } if (_wallet.AreOrdersBeingOpened()) { class=class="str">"cmt">//--- Check if orders being opened class="kw">return true; class=class="str">"cmt">//--- Return true } class="type">int ordersCloseSuccessCount = class="num">0; class=class="str">"cmt">//--- Initialize success count for (class="type">int i = ordersToCloseCount - class="num">1; i >= class="num">0; i--) { class=class="str">"cmt">//--- Iterate orders Order* pendingCloseOrder = pendingCloseOrders.Get(i); class=class="str">"cmt">//--- Get order if (pendingCloseOrder.IsAwaitingDealExecution) { class=class="str">"cmt">//--- Check if awaiting execution ordersCloseSuccessCount++; class=class="str">"cmt">//--- Increment success count class="kw">continue; class=class="str">"cmt">//--- Move to next } class="type">bool success; class=class="str">"cmt">//--- Declare success flag
◍ 净仓模式下的反向单与挂单执行
在零售净仓账户(ACCOUNT_MARGIN_MODE_RETAIL_NETTING)里,平仓不能像对冲账户那样直接 ClosePosition,而是得开一张反向单把原仓位抵消掉。下面这段逻辑先判断账户模式,是净仓就 new 一个 reversedOrder,把买变卖、卖变买,再走 OpenOrder。
if (AccountMarginMode == ACCOUNT_MARGIN_MODE_RETAIL_NETTING) { class=class="str">"cmt">//--- Check netting mode Order* reversedOrder = new Order(pendingCloseOrder, false); class=class="str">"cmt">//--- Create reversed order reversedOrder.Type = pendingCloseOrder.Type == ORDER_TYPE_BUY ? ORDER_TYPE_SELL : ORDER_TYPE_BUY; class=class="str">"cmt">//--- Set opposite type success = OrderRepository::OpenOrder(reversedOrder); class=class="str">"cmt">//--- Open reversed order if (success) { class=class="str">"cmt">//--- Check if successful pendingCloseOrder.Ticket = reversedOrder.Ticket; class=class="str">"cmt">//--- Update ticket } class="kw">delete(reversedOrder); class=class="str">"cmt">//--- Delete reversed order } else { success = OrderRepository::ClosePosition(pendingCloseOrder); class=class="str">"cmt">//--- Close position } if (success) { class=class="str">"cmt">//--- Check if successful ordersCloseSuccessCount++; class=class="str">"cmt">//--- Increment success count } } class="kw">return ordersCloseSuccessCount == ordersToCloseCount; class=class="str">"cmt">//--- Return true if all successful class=class="str">"cmt">//--- Execute pending open orders class="type">bool ExecutePendingOpenOrders() { OrderCollection* pendingOpenOrders = _wallet.GetPendingOpenOrders(); class=class="str">"cmt">//--- Retrieve pending open orders class="type">int ordersToOpenCount = pendingOpenOrders.Count(); class=class="str">"cmt">//--- Get count if (ordersToOpenCount == class="num">0) { class=class="str">"cmt">//--- Check if no orders class="kw">return true; class=class="str">"cmt">//--- Return true } class="type">int ordersOpenSuccessCount = class="num">0; class=class="str">"cmt">//--- Initialize success count for (class="type">int i = ordersToOpenCount - class="num">1; i >= class="num">0; i--) { class=class="str">"cmt">//--- Iterate orders Order* order = pendingOpenOrders.Get(i); class=class="str">"cmt">//--- Get order if (order.IsAwaitingDealExecution) { class=class="str">"cmt">//--- Check if awaiting execution ordersOpenSuccessCount++; class=class="str">"cmt">//--- Increment success count class="kw">continue; class=class="str">"cmt">//--- Move to next
if (AccountMarginMode == ACCOUNT_MARGIN_MODE_RETAIL_NETTING) { class=class="str">"cmt">//--- Check netting mode Order* reversedOrder = new Order(pendingCloseOrder, false); class=class="str">"cmt">//--- Create reversed order reversedOrder.Type = pendingCloseOrder.Type == ORDER_TYPE_BUY ? ORDER_TYPE_SELL : ORDER_TYPE_BUY; class=class="str">"cmt">//--- Set opposite type success = OrderRepository::OpenOrder(reversedOrder); class=class="str">"cmt">//--- Open reversed order if (success) { class=class="str">"cmt">//--- Check if successful pendingCloseOrder.Ticket = reversedOrder.Ticket; class=class="str">"cmt">//--- Update ticket } class="kw">delete(reversedOrder); class=class="str">"cmt">//--- Delete reversed order } else { success = OrderRepository::ClosePosition(pendingCloseOrder); class=class="str">"cmt">//--- Close position } if (success) { class=class="str">"cmt">//--- Check if successful ordersCloseSuccessCount++; class=class="str">"cmt">//--- Increment success count } } class="kw">return ordersCloseSuccessCount == ordersToCloseCount; class=class="str">"cmt">//--- Return true if all successful class=class="str">"cmt">//--- Execute pending open orders class="type">bool ExecutePendingOpenOrders() { OrderCollection* pendingOpenOrders = _wallet.GetPendingOpenOrders(); class=class="str">"cmt">//--- Retrieve pending open orders class="type">int ordersToOpenCount = pendingOpenOrders.Count(); class=class="str">"cmt">//--- Get count if (ordersToOpenCount == class="num">0) { class=class="str">"cmt">//--- Check if no orders class="kw">return true; class=class="str">"cmt">//--- Return true } class="type">int ordersOpenSuccessCount = class="num">0; class=class="str">"cmt">//--- Initialize success count for (class="type">int i = ordersToOpenCount - class="num">1; i >= class="num">0; i--) { class=class="str">"cmt">//--- Iterate orders Order* order = pendingOpenOrders.Get(i); class=class="str">"cmt">//--- Get order if (order.IsAwaitingDealExecution) { class=class="str">"cmt">//--- Check if awaiting execution ordersOpenSuccessCount++; class=class="str">"cmt">//--- Increment success count class="kw">continue; class=class="str">"cmt">//--- Move to next
交易上下文阻塞时的等待与撤单兜底
EA 下挂单前常遇到交易上下文被占(EA 禁用、市场休市或终端正忙),这段逻辑用轮询方式等上下文释放,最长容忍 10 秒。 循环里每 100 毫秒睡一次,用 GetTickCount() 算已过毫秒数,超 10*1000 就报超时并退出;若用户手动停止 EA,也立刻 break 并记日志。 若最终 isTradeContextFree 仍为 false,说明单子没开成,代码会调 _wallet.CancelPendingOpenOrder 撤掉挂单,撤失败则抛错到 Journal。外汇与贵金属杠杆高,这类上下文竞态在新闻行情时概率明显上升,建议直接开 MT5 把 MaxWaiting_sec 改成 3 或 20 看成交率变化。
class="type">bool isTradeContextFree = false; class=class="str">"cmt">//--- Initialize trade context flag class="type">class="kw">double StartWaitingTime = GetTickCount(); class=class="str">"cmt">//--- Start timer class="kw">while (true) { class=class="str">"cmt">//--- Wait for trade context if (MQL5InfoInteger(MQL5_TRADE_ALLOWED)) { class=class="str">"cmt">//--- Check if trade allowed isTradeContextFree = true; class=class="str">"cmt">//--- Set trade context free break; class=class="str">"cmt">//--- Exit loop } class="type">int MaxWaiting_sec = class="num">10; class=class="str">"cmt">//--- Set max wait time if (IsStopped()) { class=class="str">"cmt">//--- Check if EA stopped HandleErrors("The expert was stopped by a user action."); class=class="str">"cmt">//--- Log error break; class=class="str">"cmt">//--- Exit loop } if (GetTickCount() - StartWaitingTime > MaxWaiting_sec * class="num">1000) { class=class="str">"cmt">//--- Check if timeout HandleErrors(StringFormat("The(%d seconds) waiting time exceeded. Trade not allowed: EA disabled, market closed or trade context still not free.", MaxWaiting_sec)); class=class="str">"cmt">//--- Log error break; class=class="str">"cmt">//--- Exit loop } Sleep(class="num">100); class=class="str">"cmt">//--- Wait briefly } if (!isTradeContextFree) { class=class="str">"cmt">//--- Check if trade context not free if (!_wallet.CancelPendingOpenOrder(order)) { class=class="str">"cmt">//--- Attempt to cancel order HandleErrors("Failed to cancel an order(because it couldn&class="macro">#x27;t open). Please see the Journal and Expert tab in Metatrader for more information."); class=class="str">"cmt">//--- Log error }
「开单失败后的回转与账户上下文读取」
订单派发循环里,每跑完一个 pending 单就用 continue 跳到下一个,不阻塞后续执行。真正下单交给 OrderRepository::OpenOrder,返回 success 后把 ordersOpenSuccessCount 加一;若失败则立刻调 _wallet.CancelPendingOpenOrder 尝试撤单,撤不掉就走 HandleErrors 把 Journal / Expert 里的线索报出来。 函数末尾用 ordersOpenSuccessCount == ordersToOpenCount 做整体成败判定,只有全部张数都成功才返回 true。这一写法在外汇与贵金属这种高滑点品种上,可能暴露部分成交后撤单 race condition 的隐患,建议你在 MT5 策略测试器里故意断网模拟一次。 OnTick 入口先抓账户上下文:ACCOUNT_SERVER、ACCOUNT_CURRENCY、ACCOUNT_NAME 用 AccountInfoString 拿,ACCOUNT_TRADE_MODE 用 AccountInfoInteger 区分 0/1/2 对应 DEMO / CONTEST / REAL。杠杆、自由保证金、是否允许交易也都在此刻读取,方便后续风控分支直接复用。
class="kw">continue; class=class="str">"cmt">//--- Move to next } class="type">bool success = OrderRepository::OpenOrder(order); class=class="str">"cmt">//--- Open order if (success) { class=class="str">"cmt">//--- Check if successful ordersOpenSuccessCount++; class=class="str">"cmt">//--- Increment success count } else { if (!_wallet.CancelPendingOpenOrder(order)) { class=class="str">"cmt">//--- Attempt to cancel order HandleErrors("Failed to cancel an order(because it couldn&class="macro">#x27;t open). Please see the Journal and Expert tab in Metatrader for more information."); class=class="str">"cmt">//--- Log error } } } class="kw">return ordersOpenSuccessCount == ordersToOpenCount; class=class="str">"cmt">//--- Return true if all successful } }; class=class="str">"cmt">//--- Handle tick event class="type">class="kw">datetime LastActionTime = class="num">0; class=class="str">"cmt">//--- Track last action time class="type">void OnTick() { class="type">class="kw">string AccountServer = AccountInfoString(ACCOUNT_SERVER); class=class="str">"cmt">//--- Retrieve account server class="type">class="kw">string AccountCurrency = AccountInfoString(ACCOUNT_CURRENCY); class=class="str">"cmt">//--- Retrieve account currency class="type">class="kw">string AccountName = AccountInfoString(ACCOUNT_NAME); class=class="str">"cmt">//--- Retrieve account name class="type">long AccountTradeMode = AccountInfoInteger(ACCOUNT_TRADE_MODE); class=class="str">"cmt">//--- Retrieve trade mode class="type">class="kw">string ReadableAccountTrademode = ""; class=class="str">"cmt">//--- Initialize readable trade mode if (AccountTradeMode == class="num">0) ReadableAccountTrademode = "DEMO ACCOUNT"; class=class="str">"cmt">//--- Set demo mode if (AccountTradeMode == class="num">1) ReadableAccountTrademode = "CONTEST ACCOUNT"; class=class="str">"cmt">//--- Set contest mode if (AccountTradeMode == class="num">2) ReadableAccountTrademode = "REAL ACCOUNT"; class=class="str">"cmt">//--- Set real mode class="type">long AccountLogin = AccountInfoInteger(ACCOUNT_LOGIN); class=class="str">"cmt">//--- Retrieve account login class="type">class="kw">string AccountCompany = AccountInfoString(ACCOUNT_COMPANY); class=class="str">"cmt">//--- Retrieve account company class="type">long AccountLeverage = AccountInfoInteger(ACCOUNT_LEVERAGE); class=class="str">"cmt">//--- Retrieve account leverage class="type">long AccountLimitOrders = AccountInfoInteger(ACCOUNT_LIMIT_ORDERS); class=class="str">"cmt">//--- Retrieve order limit class="type">class="kw">double AccountMarginFree = AccountInfoDouble(ACCOUNT_MARGIN_FREE); class=class="str">"cmt">//--- Retrieve free margin class="type">bool AccountTradeAllowed = AccountInfoInteger(ACCOUNT_TRADE_ALLOWED); class=class="str">"cmt">//--- Retrieve trade allowed
◍ 账户模式识别与每根K线只跑一次的逻辑
在 EA 的 OnTick 入口,先抓账户是否允许智能交易:用 AccountInfoInteger(ACCOUNT_TRADE_EXPERT) 拿到布尔值,这决定后续下单权限。账户保证金模式则靠 AccountMarginMode 区分,0 是 NETTING(净仓)、1 是 EXCHANGE(交易所式)、2 是 HEDGING(锁仓),外汇贵金属零售账户多数落在 2,但必须运行时读出来别写死。 若开了 OneQuotePerBar,就要用 iTime(_Symbol,_Period,0) 取当前_bar 开盘时间,和上次记录的 LastActionTime 比。相等就直接 return,保证一根 K 线内只处理一次信号;不等才更新时间戳继续跑。这是防止 Tick 级重复触发的硬手段。 可视化或模拟环境下,用 TimeCurrent 填 MqlDateTime 结构,拼出「年.月.日 时:分:秒」的图表注释。若开启 DisplayOnChartError,就把本次 Error 和上一次 ErrorPreviousQuote 都叠进 comment 显示,方便盯盘时直接看 EA 内部状态。外汇贵金属波动剧烈,这类自检显示能降低误判概率,但任何信号都只是概率倾向,实盘前务必在 MT5 策略测试器验证。
class="type">bool AccountTradeExpert = AccountInfoInteger(ACCOUNT_TRADE_EXPERT); class=class="str">"cmt">//--- Retrieve expert allowed class="type">class="kw">string ReadableAccountMarginMode = ""; class=class="str">"cmt">//--- Initialize readable margin mode if (AccountMarginMode == class="num">0) ReadableAccountMarginMode = "NETTING MODE"; class=class="str">"cmt">//--- Set netting mode if (AccountMarginMode == class="num">1) ReadableAccountMarginMode = "EXCHANGE MODE"; class=class="str">"cmt">//--- Set exchange mode if (AccountMarginMode == class="num">2) ReadableAccountMarginMode = "HEDGING MODE"; class=class="str">"cmt">//--- Set hedging mode if (OneQuotePerBar) { class=class="str">"cmt">//--- Check one quote per bar class="type">class="kw">datetime currentTime = iTime(_Symbol, _Period, class="num">0); class=class="str">"cmt">//--- Get current bar time if (LastActionTime == currentTime) { class=class="str">"cmt">//--- Check if same bar class="kw">return; class=class="str">"cmt">//--- Exit } else { LastActionTime = currentTime; class=class="str">"cmt">//--- Update last action time } } Error = NULL; class=class="str">"cmt">//--- Clear current error _ea.HandleTick(); class=class="str">"cmt">//--- Handle tick if (IsDemoLiveOrVisualMode) { class=class="str">"cmt">//--- Check visual mode class="type">MqlDateTime mql_datetime; class=class="str">"cmt">//--- Declare class="type">class="kw">datetime TimeCurrent(mql_datetime); class=class="str">"cmt">//--- Get current time class="type">class="kw">string comment = "\n" + (class="type">class="kw">string)mql_datetime.year + "." + (class="type">class="kw">string)mql_datetime.mon + "." + (class="type">class="kw">string)mql_datetime.day + " " + TimeToString(TimeCurrent(), TIME_SECONDS) + OrderInfoComment; class=class="str">"cmt">//--- Build comment if (DisplayOnChartError) { class=class="str">"cmt">//--- Check if error display enabled if (Error != NULL) comment += "\n :: Current error : " + Error; class=class="str">"cmt">//--- Add current error if (ErrorPreviousQuote != NULL) comment += "\n :: Last error : " + ErrorPreviousQuote; class=class="str">"cmt">//--- Add previous error }
账户面板与成交回查的底层拼装
这段逻辑把账户静态信息和动态成交回查拼到了同一个 EA 生命周期里。前半段用 Comment() 把服务器、币种、杠杆、空闲保证金等 13 项账户字段直接打到图表左上角,省去手动翻终端的麻烦;杠杆和限制单数这些字段在穿仓前就该盯住。 comment += ""; 这一行看着多余,实际是给前面拼好的字符串补一个空行分隔,避免账户信息和后续提示挤在同一行看花眼。 OnTradeTransaction 里只接了 TRADE_TRANSACTION_DEAL_ADD 一种事件:每新增一笔成交,就用 TimeCurrent() 减掉 PERIOD_D1 的秒数圈出『昨天到现在』的时间窗,再 HistorySelect 拉一次历史。若 HistoryDealsTotal() 返回 0,直接 Print 并 return,不往下跑统计。 外汇和贵金属品种杠杆普遍超过 1:100,这类实时面板只是辅助,真要下单前仍得自己核对手动平仓线,高杠杆下保证金耗尽可能发生在几秒内。
comment += ""; class=class="str">"cmt">//--- Append empty line Comment("ACCOUNT SERVER: ", AccountServer, "\n", class=class="str">"cmt">//--- Display account info "ACCOUNT CURRENCY: ", AccountCurrency, "\n", "ACCOUNT NAME: ", AccountName, "\n", "ACCOUNT TRADEMODE: ", ReadableAccountTrademode, "\n", "ACCOUNT LOGIN: ", AccountLogin, "\n", "ACCOUNT COMPANY: ", AccountCompany, "\n", "ACCOUNT LEVERAGE: ", AccountLeverage, "\n", "ACCOUNT LIMIT ORDERS: ", AccountLimitOrders, "\n", "ACCOUNT MARGIN FREE: ", AccountMarginFree, "\n", "ACCOUNT TRADING ALLOWED: ", AccountTradeAllowed, "\n", "ACCOUNT EXPERT ALLOWED: ", AccountTradeExpert, "\n", "ACCOUNT MARGIN ALLOWED: ", ReadableAccountMarginMode); } class=class="str">"cmt">//--- Handle trade transactions class="type">void OnTradeTransaction(class="kw">const MqlTradeTransaction& trans, class="kw">const class="type">MqlTradeRequest& request, class="kw">const class="type">MqlTradeResult& result) { class="kw">switch (trans.type) { class=class="str">"cmt">//--- Handle transaction type case TRADE_TRANSACTION_DEAL_ADD: { class=class="str">"cmt">//--- Handle deal addition class="type">class="kw">datetime end = TimeCurrent(); class=class="str">"cmt">//--- Get current server time class="type">class="kw">datetime start = end - PeriodSeconds(PERIOD_D1); class=class="str">"cmt">//--- Set start time(class="num">1 day ago) HistorySelect(start, end + PeriodSeconds(PERIOD_D1)); class=class="str">"cmt">//--- Select history class="type">int dealsTotal = HistoryDealsTotal(); class=class="str">"cmt">//--- Get total deals if (dealsTotal == class="num">0) { class=class="str">"cmt">//--- Check if no deals Print("No deals found"); class=class="str">"cmt">//--- Log message class="kw">return; class=class="str">"cmt">//--- Exit }
「成交回执里把挂单翻成持仓」
在 MT5 的异步下单模型里,挂单成交不会自己变成持仓对象,得靠交易历史里的 deal 回执去反查。上面这段逻辑就是典型的落地做法:先拿 HistoryDealGetInteger 取 DEAL_ORDER 拿到原始挂单 ticket,再用 CDealInfo 把成交时间、价格抓出来。 关键在 deal_entry == DEAL_ENTRY_IN 这个分支——只处理入场成交,避免平仓 deal 误触发。遍历钱包里的待开仓挂单集合,用 order.Ticket == orderTicketId 做精确匹配;命中后把 OpenTime、OpenPrice、TradePrice 全部回填到 Order 结构。 FOK 填充模式下要累加 OrderFilledLots,并用 MathAbs(order.Lots - order.OrderFilledLots) < 1e-5 判断完全成交。外汇与贵金属杠杆交易高风险,这类撮合状态机若漏判部分成交,持仓簿可能和券商端偏差几个点值,建议开 MT5 用脚本跑一遍多挂单同秒成交验证。 别把 1e-5 当万能容差 黄金 XAUUSD 在五位数报价下,1e-5 约等于 0.05 点,若券商最小成交量步长更粗,这个阈值可能永远不触发完全成交判定,得按 SymbolInfoInteger(_Symbol, SYMBOL_VOLUME_STEP) 动态算。
class="type">ulong orderTicketId = HistoryDealGetInteger(trans.deal, DEAL_ORDER); class=class="str">"cmt">//--- Get order ticket CDealInfo dealInfo; class=class="str">"cmt">//--- Declare deal info dealInfo.Ticket(trans.deal); class=class="str">"cmt">//--- Set deal ticket ENUM_DEAL_ENTRY deal_entry = dealInfo.Entry(); class=class="str">"cmt">//--- Get deal entry type class="type">bool found = false; class=class="str">"cmt">//--- Initialize found flag if (deal_entry == DEAL_ENTRY_IN) { class=class="str">"cmt">//--- Handle deal entry OrderCollection* pendingOpenOrders = _ea.GetWallet().GetPendingOpenOrders(); class=class="str">"cmt">//--- Retrieve pending open orders for (class="type">int i = class="num">0; i < pendingOpenOrders.Count(); i++) { class=class="str">"cmt">//--- Iterate orders Order* order = pendingOpenOrders.Get(i); class=class="str">"cmt">//--- Get order if (order.Ticket == orderTicketId) { class=class="str">"cmt">//--- Check matching ticket found = true; class=class="str">"cmt">//--- Set found flag order.OpenTime = dealInfo.Time(); class=class="str">"cmt">//--- Set open time order.OpenPrice = trans.price; class=class="str">"cmt">//--- Set open price order.TradePrice = order.OpenPrice; class=class="str">"cmt">//--- Set trade price if (OrderFillingType == ORDER_FILLING_FOK) { class=class="str">"cmt">//--- Check FOK filling order.OrderFilledLots += trans.volume; class=class="str">"cmt">//--- Add volume if (MathAbs(order.Lots - order.OrderFilledLots) < class="num">1e-5) { class=class="str">"cmt">//--- Check if fully filled order.IsAwaitingDealExecution = false; class=class="str">"cmt">//--- Clear execution flag order.Lots = order.OrderFilledLots; class=class="str">"cmt">//--- Update lots order.TradeVolume = order.Lots; class=class="str">"cmt">//--- Update trade volume _ea.GetWallet().SetPendingOpenOrderToOpen(order); class=class="str">"cmt">//--- Move to open Print(StringFormat("Execution done for order(%d) by EA(%d)", orderTicketId, MagicNumber)); class=class="str">"cmt">//--- Log success } } else { order.IsAwaitingDealExecution = false; class=class="str">"cmt">//--- Clear execution flag
◍ 成交回报里的手数偏差处理
在 MT5 的异步成交回包里,券商实际成交手数未必等于挂单请求手数。代码用 1e-5 作为容差阈值判断 actualVolumeDiffers,这意味着小于 0.00001 手的浮点误差会被忽略,超出的才视为真实滑手。
一旦发现手数不一致,除了打印经纪商执行量,还会触发 OrderRepository::CalculateAndSetCommision(order) 重新计算佣金——因为按真实成交手数算费用才准。外汇与贵金属杠杆高,这种隐性手数差会放大点值偏差,建议开 MT5 用真实账户回测验证。
对于 DEAL_ENTRY_OUT 的平仓分支,FOK 模式下每笔回包累加 OrderFilledLots,当与原始 Lots 差小于 1e-5 才标记完全平仓并写入平仓时间、价格。复制下面这段逻辑到你的 EA 里,能把成交回执的对账漏洞堵上。
class="type">bool actualVolumeDiffers = MathAbs(order.Lots - trans.volume) > class="num">1e-5; class=class="str">"cmt">//--- Check volume difference order.OrderFilledLots += trans.volume; class=class="str">"cmt">//--- Add volume order.Lots = order.OrderFilledLots; class=class="str">"cmt">//--- Update lots order.TradeVolume = order.Lots; class=class="str">"cmt">//--- Update trade volume if (actualVolumeDiffers) { class=class="str">"cmt">//--- Check if volume differs Print("Broker executed volume differs from requested volume. Executed volume: " + DoubleToStr(trans.volume)); class=class="str">"cmt">//--- Log difference OrderRepository::CalculateAndSetCommision(order); class=class="str">"cmt">//--- Recalculate commission } _ea.GetWallet().SetPendingOpenOrderToOpen(order); class=class="str">"cmt">//--- Move to open Print(StringFormat("Execution done for order(%d) by EA(%d)", orderTicketId, MagicNumber)); class=class="str">"cmt">//--- Log success } else if (deal_entry == DEAL_ENTRY_OUT) { class=class="str">"cmt">//--- Handle deal exit OrderCollection* pendingCloseOrders = _ea.GetWallet().GetPendingCloseOrders(); class=class="str">"cmt">//--- Retrieve pending close orders for (class="type">int i = class="num">0; i < pendingCloseOrders.Count(); i++) { class=class="str">"cmt">//--- Iterate orders Order* order = pendingCloseOrders.Get(i); class=class="str">"cmt">//--- Get order if (order.Ticket == orderTicketId) { class=class="str">"cmt">//--- Check matching ticket found = true; class=class="str">"cmt">//--- Set found flag if (OrderFillingType == ORDER_FILLING_FOK) { class=class="str">"cmt">//--- Check FOK filling order.OrderFilledLots += trans.volume; class=class="str">"cmt">//--- Add volume if (MathAbs(order.Lots - order.OrderFilledLots) < class="num">1e-5) { class=class="str">"cmt">//--- Check if fully filled order.IsAwaitingDealExecution = false; class=class="str">"cmt">//--- Clear execution flag order.CloseTime = dealInfo.Time(); class=class="str">"cmt">//--- Set close time order.ClosePrice = trans.price; class=class="str">"cmt">//--- Set close price if (order.MagicNumber == MagicNumber) { class=class="str">"cmt">//--- Check EA order TotalCommission += order.Commission; class=class="str">"cmt">//--- Add commission
券商部分成交时的挂单残量处理
在 MT5 的 EA 回测或实盘里,券商实际成交手数经常和请求手数不一致。上面这段代码用 MathAbs(order.Lots - trans.volume) > 1e-5 判断差异,阈值 1e-5 手能过滤掉浮点误差,避免把正常全量成交误判成部分平仓。 一旦判定 actualVolumeDiffers 为真,系统会新建一个 remainderOrder 承接未成交部分:把原订单 Ticket 清零、Lots 设为差值、重算佣金,再塞回待平仓队列。原订单则被裁剪为实际成交手数,佣金同样重算。 视觉模式下还会顺手删掉原订单的 TP/SL 画线对象,防止图表上残留已失效的止损止盈线。外汇与贵金属杠杆高,部分成交在高波动时段概率明显上升,直接复制这段逻辑到你的订单管理类里,能在 MT5 策略测试器用自定义品种验证残单是否被正确接力。
if (IsDemoLiveOrVisualMode) { class=class="str">"cmt">//--- Check visual mode AnyChartObjectDelete(ChartID(), IntegerToString(order.Ticket) + "_TP"); class=class="str">"cmt">//--- Delete TP line AnyChartObjectDelete(ChartID(), IntegerToString(order.Ticket) + "_SL"); class=class="str">"cmt">//--- Delete SL line } if (order.ParentOrder != NULL) { class=class="str">"cmt">//--- Check if parent order order.ParentOrder.Paint(); class=class="str">"cmt">//--- Redraw parent } _ea.GetWallet().SetPendingCloseOrderToClosed(order); class=class="str">"cmt">//--- Move to closed Print(StringFormat("Execution done for order(%d) by EA(%d)", orderTicketId, MagicNumber)); class=class="str">"cmt">//--- Log success } else { class="type">bool actualVolumeDiffers = MathAbs(order.Lots - trans.volume) > class="num">1e-5; class=class="str">"cmt">//--- Check volume difference if (actualVolumeDiffers) { class=class="str">"cmt">//--- Handle partial close Print("Broker executed volume differs from requested volume.Requested volume: " + DoubleToStr(order.Lots) + ".Executed volume: " + DoubleToStr(trans.volume)); class=class="str">"cmt">//--- Log difference Order* remainderOrder = new Order(order, false); class=class="str">"cmt">//--- Create remainder order remainderOrder.Ticket = class="num">0; class=class="str">"cmt">//--- Clear ticket remainderOrder.Lots = order.Lots - trans.volume; class=class="str">"cmt">//--- Set remaining volume remainderOrder.TradeVolume = remainderOrder.Lots; class=class="str">"cmt">//--- Update trade volume OrderRepository::CalculateAndSetCommision(remainderOrder); class=class="str">"cmt">//--- Recalculate commission _ea.GetWallet().GetPendingCloseOrders().Add(remainderOrder); class=class="str">"cmt">//--- Add remainder order.Lots = trans.volume; class=class="str">"cmt">//--- Update original volume order.TradeVolume = order.Lots; class=class="str">"cmt">//--- Update trade volume OrderRepository::CalculateAndSetCommision(order); class=class="str">"cmt">//--- Recalculate commission
「成交回写时清标志与撤图表线」
上面这段逻辑发生在订单被经纪商实际成交后,EA 把挂单状态从「等待成交」翻转为「已平仓」。先清掉 IsAwaitingDealExecution 这个执行标志,再把 deal 的时间、价格写进 order 的 CloseTime 与 ClosePrice,账户统计才不会出现重复计算。 若 MagicNumber 匹配,说明是本基金 EA 开的单, commissions 累加进 TotalCommission;这一步若漏掉,回测或实盘里的净盈亏会偏高。外汇与贵金属杠杆品种滑点大,真实成交价 trans.price 常偏离预期,清标志前务必以 trans 为准。 在可视化或 demo 模式里,代码顺手用 AnyChartObjectDelete 删掉该 ticket 的 _TP 与 _SL 对象线。开 MT5 用策略测试器跑一遍,切到图表看成交后辅助线是否消失,就能验证这段有没有生效。
} else { Print("Broker executed volume: " + DoubleToStr(trans.volume)); class=class="str">"cmt">//--- Log volume } order.IsAwaitingDealExecution = false; class=class="str">"cmt">//--- Clear execution flag order.CloseTime = dealInfo.Time(); class=class="str">"cmt">//--- Set close time order.ClosePrice = trans.price; class=class="str">"cmt">//--- Set close price if (order.MagicNumber == MagicNumber) { class=class="str">"cmt">//--- Check EA order TotalCommission += order.Commission; class=class="str">"cmt">//--- Add commission } if (IsDemoLiveOrVisualMode) { class=class="str">"cmt">//--- Check visual mode AnyChartObjectDelete(ChartID(), IntegerToString(order.Ticket) + "_TP"); class=class="str">"cmt">//--- Delete TP line AnyChartObjectDelete(ChartID(), IntegerToString(order.Ticket) + "_SL"); class=class="str">"cmt">//--- Delete SL line } _ea.GetWallet().SetPendingCloseOrderToClosed(order); class=class="str">"cmt">//--- Move to closed Print(StringFormat("Execution done for order(%d) by EA(%d)", orderTicketId, MagicNumber)); class=class="str">"cmt">//--- Log success } } } if (found) { class=class="str">"cmt">//--- Check if deal found Print("Updated order with deal info."); class=class="str">"cmt">//--- Log update } else if (trans.symbol == Symbol() && dealInfo.Magic() == MagicNumber) { class=class="str">"cmt">//--- Check EA deal Print("Couldn&class="macro">#x27;t find deal info for place/done order"); class=class="str">"cmt">//--- Log missing deal } break;
◍ 撤掉EA前先把内存还回去
实盘里频繁加载卸载EA的人容易踩一个坑:从图表移除程序时,若没主动释放对象,MT5进程会留下悬空内存。轻则图表注释残影消不掉,重则长时间挂多个实例后终端越来越吃内存。 触发点在OnDeinit——它在EA被删除或终端关闭时跑。我们的处理顺序很直白:先用Comment("")清掉图表上所有文字层,再把主实例_ea和取价用的AskFunc、BidFunc逐个delete掉。 全套流程跑完,程序停止时不会再漏资源。我们用默认参数加一组风控变参(1%、5、30、10、60)做了回测,终端日志里看不到卸载报错了,内存曲线也平了。外汇和贵金属波动大,这类泄漏在多次重加载后可能放大滑点风险,建议你在策略测试器里反复挂卸验证。
class=class="str">"cmt">//--- Deinitialize Expert Advisor class="type">void OnDeinit(class="kw">const class="type">int reason) { Comment(""); class="kw">delete(_ea); class=class="str">"cmt">//--- Delete EA instance class="kw">delete(AskFunc); class=class="str">"cmt">//--- Delete Ask function class="kw">delete(BidFunc); class=class="str">"cmt">//--- Delete Bid function }
一点提醒
这套基于包络线、移动均线与 RSI 的剥头皮框架,把信号评估、执行与实时监控拆成了三个独立模块,在 MT5 里改阈值或加指标都算热插拔级改动。 实盘前务必用策略测试器跑至少三个月 Tick 数据回测,外汇与贵金属杠杆高、滑点跳空频繁,历史信号再漂亮也可能在直播时失效。 真要上线,先把仓位系数和止损距离调小,让小布这类 AIGC 工具替你盯异常成交,比人工扫十块屏更稳。