MQL5交易策略自动化(第二十二部分):构建基于包络线趋势交易的区间补仓系统·综合运用
◍ 从风险金额反推开仓手数的代码骨架
这段 MQL5 片段承接了前面按风险算手数的逻辑,核心在 lotValue 的算法:用风险点数乘 _Point 得到每点波动金额基准,再除以 tickSize 乘 tickValue,得出每手对应的风险价值。若 lotValue 为 0 直接返回 -1,避免后续除零崩单。 开仓函数 openOrder 里先按 ORDER_TYPE_BUY / ORDER_TYPE_SELL 分流取 ask / bid 价,并用 NormalizeDouble(..., Digits()) 对齐品种报价精度;非法订单类型打印日志并返回 -1。 手数变量 lotSize 初始化为 0,准备接前面 calculateLotSize 之类函数的返回值。外汇与贵金属杠杆高,这套按风险反推手数的逻辑在 MT5 里实测能压住单笔回撤,但滑点可能让实际亏损偏大,需自行校验 tickValue 取值。
class="kw">return -class="num">1; class=class="str">"cmt">//--- Return invalid lot } class="type">class="kw">double lotValue = (riskPips * _Point) / tickSize * tickValue; class=class="str">"cmt">//--- Calculate lot value if (lotValue == class="num">0) { class=class="str">"cmt">//--- Validate lot value Print("Invalid lot value"); class=class="str">"cmt">//--- Log invalid lot class="kw">return -class="num">1; class=class="str">"cmt">//--- Return invalid lot } class="kw">return NormalizeDouble(riskMoney / lotValue, class="num">2); class=class="str">"cmt">//--- Return normalized lot size class=class="str">"cmt">//--- Lot Size Calculation End } class=class="str">"cmt">//--- Order Execution class="type">int openOrder(ENUM_ORDER_TYPE orderType, class="type">class="kw">double stopLoss, class="type">class="kw">double takeProfit) { class=class="str">"cmt">//--- Order Opening Start class="type">int ticket; class=class="str">"cmt">//--- Initialize ticket class="type">class="kw">double openPrice; class=class="str">"cmt">//--- Initialize open price if (orderType == ORDER_TYPE_BUY) { class=class="str">"cmt">//--- Check buy order openPrice = NormalizeDouble(getMarketAsk(), Digits()); class=class="str">"cmt">//--- Set buy price } else if (orderType == ORDER_TYPE_SELL) { class=class="str">"cmt">//--- Check sell order openPrice = NormalizeDouble(getMarketBid(), Digits()); class=class="str">"cmt">//--- Set sell price } else { Print("Invalid order type"); class=class="str">"cmt">//--- Log invalid type class="kw">return -class="num">1; class=class="str">"cmt">//--- Return invalid ticket } class="type">class="kw">double lotSize = class="num">0; class=class="str">"cmt">//--- Initialize lot size
开仓前的手数分支与失效拦截
EA 在下单前先判定手数模式:固定手数直接取 m_initialLotSize,动态手数则交给 calculateLotSize(),按风险百分比与止损点数反推。若算出的 lotSize ≤ 0,立刻 Print 报错并返回 -1,避免把废单丢给 broker。 真正发单走 m_tradeExecutor.PositionOpen(),传入品种、订单类型、手数、开仓价,滑点与止损止盈先填 0。成功就取 ResultOrder() 转成 ticket 并打印类型与成交量;失败则 ticket 置 -1 并留日志,调用方靠这个值判断有没有单。 构造函数里把状态锁在 INACTIVE、活跃 ticket 数组清空,再用 targetPts * _Point 算出以点数表达的盈利跨度。外汇与贵金属杠杆高,动态手数若 riskPct 设大,回撤可能瞬间吃掉保证金,MT5 里把这两参数调小一档更稳。
if (m_lotOption == FIXED_LOTSIZE) { class=class="str">"cmt">//--- Check fixed lot lotSize = m_initialLotSize; class=class="str">"cmt">//--- Use fixed lot size } else if (m_lotOption == UNFIXED_LOTSIZE) { class=class="str">"cmt">//--- Check dynamic lot lotSize = calculateLotSize(m_riskPercentage, m_riskPoints); class=class="str">"cmt">//--- Calculate risk-based lot } if (lotSize <= class="num">0) { class=class="str">"cmt">//--- Validate lot size Print("Invalid lot size: ", lotSize); class=class="str">"cmt">//--- Log invalid lot class="kw">return -class="num">1; class=class="str">"cmt">//--- Return invalid ticket } if (m_tradeExecutor.PositionOpen(m_tradeConfig.marketSymbol, orderType, lotSize, openPrice, class="num">0, class="num">0, __FILE__)) { class=class="str">"cmt">//--- Open position ticket = (class="type">int)m_tradeExecutor.ResultOrder(); class=class="str">"cmt">//--- Get ticket Print("New trade opened: Ticket=", ticket, ", Type=", EnumToString(orderType), ", Volume=", lotSize); class=class="str">"cmt">//--- Log success } else { ticket = -class="num">1; class=class="str">"cmt">//--- Set invalid ticket Print("Failed to open order: Type=", EnumToString(orderType), ", Volume=", lotSize); class=class="str">"cmt">//--- Log failure } class="kw">return ticket; class=class="str">"cmt">//--- Return ticket class=class="str">"cmt">//--- Order Opening End } class="kw">public: class=class="str">"cmt">//--- Constructor MarketZoneTrader(TradingLotSizeOptions lotOpt, class="type">class="kw">double initLot, class="type">class="kw">double riskPct, class="type">int riskPts, class="type">int maxOrds, class="type">bool restrictOrds, class="type">class="kw">double targetPts, class="type">class="kw">double sizePts) { class=class="str">"cmt">//--- Constructor Start m_tradeConfig.currentState = INACTIVE; class=class="str">"cmt">//--- Set initial state ArrayResize(m_tradeConfig.activeTickets, class="num">0); class=class="str">"cmt">//--- Initialize ticket array m_tradeConfig.zoneProfitSpan = targetPts * _Point; class=class="str">"cmt">//--- Set profit target
「构造函数里的参数落地与析构清理」
在 MarketZoneTrader 的构造函数里,一堆成员变量的初始化被集中完成,这决定了后续 zone 恢复交易的边界与风控基线。zoneRecoverySpan 直接用 sizePts 乘以 _Point 换算成真实价格跨度,意味着你在 EURUSD 上设 sizePts=50,恢复区就是 5 个点(50×0.0001),XAUUSD 上则是 0.5 美元——同一参数跨品种量级完全不同。 lossTracker 初始化为 0.0,表示新实例不继承任何历史浮亏;lotOption、initialLotSize、riskPercentage、riskPoints 分别接管手数模式与风险锚点。maxOrders 与 restrictMaxOrders 控制网格层数上限与是否硬限制,zoneTargetPoints 和 zoneSizePoints 框定盈利目标和区间宽度。 析构函数只做一件事:调 cleanup() 释放指标句柄。RSI 与上下轨 Envelopes 的句柄若不清,EA 重载时会漏句柄导致内存占用累积,MT5 终端跑几天可能报 'indicator cannot be created'。 开 MT5 把这段粘进类定义,改 sizePts 从 20 到 200 各挂一次回测,能直接看到恢复区跨度对成交频率的挤压效果。外汇与贵金属杠杆高,参数误设可能瞬间放大回撤,验证时先用模拟盘。
m_tradeConfig.zoneRecoverySpan = sizePts * _Point; class=class="str">"cmt">//--- Set recovery zone m_lossTracker.tradeLossTracker = class="num">0.0; class=class="str">"cmt">//--- Initialize loss tracker m_lotOption = lotOpt; class=class="str">"cmt">//--- Set lot size option m_initialLotSize = initLot; class=class="str">"cmt">//--- Set initial lot m_riskPercentage = riskPct; class=class="str">"cmt">//--- Set risk percentage m_riskPoints = riskPts; class=class="str">"cmt">//--- Set risk points m_maxOrders = maxOrds; class=class="str">"cmt">//--- Set max positions m_restrictMaxOrders = restrictOrds; class=class="str">"cmt">//--- Set restriction flag m_zoneTargetPoints = targetPts; class=class="str">"cmt">//--- Set target points m_zoneSizePoints = sizePts; class=class="str">"cmt">//--- Set zone points m_tradeConfig.marketSymbol = _Symbol; class=class="str">"cmt">//--- Set symbol m_tradeConfig.tradeIdentifier = magicNumber; class=class="str">"cmt">//--- Set magic number class=class="str">"cmt">//--- Constructor End } class=class="str">"cmt">//--- Destructor ~MarketZoneTrader() { class=class="str">"cmt">//--- Destructor Start cleanup(); class=class="str">"cmt">//--- Release resources class=class="str">"cmt">//--- Destructor End } class=class="str">"cmt">//--- Cleanup class="type">void cleanup() { class=class="str">"cmt">//--- Cleanup Start IndicatorRelease(m_handleRsi); class=class="str">"cmt">//--- Release RSI handle ArrayFree(m_rsiBuffer); class=class="str">"cmt">//--- Free RSI buffer IndicatorRelease(m_handleEnvUpper); class=class="str">"cmt">//--- Release upper Envelopes handle
◍ 清理句柄与状态读取的落地写法
在 EA 销毁阶段,先释放 Envelopes 指标句柄再清空关联数组缓冲,能避免 MT5 终端在重载脚本时残留隐藏引用。下面这段释放逻辑直接对应上轨与下轨两套缓冲:ArrayFree 清掉 m_envUpperBandBuffer、m_envLowerBandBuffer,IndicatorRelease 关掉 m_handleEnvLower,顺序错了可能报 handle 占用。 状态与区间读取全部走 getter 返回成员变量,getCurrentState 吐出 m_tradeConfig.currentState,getZoneTargetHigh / Low 给盈利目标边界,getZoneHigh / Low 给回收区边界。这样外部调度层只管取值,不碰内部结构体。 initialize 里第一件事是 m_tradeExecutor.SetExpertMagicNumber(m_tradeConfig.tradeIdentifier),把魔数钉死;随后 PositionsTotal 拿总持仓数,按 ticket 逐仓 PositionSelectByTicket,再比对 POSITION_SYMBOL 与 POSITION_MAGIC 是否等于本 EA 的配置。实战中若魔数漏设,历史持仓不会被本 EA 识别,可能在外汇或贵金属上重复下单,这类品种杠杆高、滑点跳空频繁,务必在初始化就卡死标识。 开 MT5 把这段塞进你的类析构前和 OnInit 后,断点看 m_handleEnvLower 是否为 0,能立刻验证释放是否生效。
ArrayFree(m_envUpperBandBuffer); class=class="str">"cmt">//--- Free upper Envelopes buffer IndicatorRelease(m_handleEnvLower); class=class="str">"cmt">//--- Release lower Envelopes handle ArrayFree(m_envLowerBandBuffer); class=class="str">"cmt">//--- Free lower Envelopes buffer class=class="str">"cmt">//--- Cleanup End } class=class="str">"cmt">//--- Getters TradeState getCurrentState() { class=class="str">"cmt">//--- Get Current State Start class="kw">return m_tradeConfig.currentState; class=class="str">"cmt">//--- Return trade state class=class="str">"cmt">//--- Get Current State End } class="type">class="kw">double getZoneTargetHigh() { class=class="str">"cmt">//--- Get Target High Start class="kw">return m_zoneBounds.zoneTargetHigh; class=class="str">"cmt">//--- Return profit target high class=class="str">"cmt">//--- Get Target High End } class="type">class="kw">double getZoneTargetLow() { class=class="str">"cmt">//--- Get Target Low Start class="kw">return m_zoneBounds.zoneTargetLow; class=class="str">"cmt">//--- Return profit target low class=class="str">"cmt">//--- Get Target Low End } class="type">class="kw">double getZoneHigh() { class=class="str">"cmt">//--- Get Zone High Start class="kw">return m_zoneBounds.zoneHigh; class=class="str">"cmt">//--- Return recovery zone high class=class="str">"cmt">//--- Get Zone High End } class="type">class="kw">double getZoneLow() { class=class="str">"cmt">//--- Get Zone Low Start class="kw">return m_zoneBounds.zoneLow; class=class="str">"cmt">//--- Return recovery zone low class=class="str">"cmt">//--- Get Zone Low End } class=class="str">"cmt">//--- Initialization class="type">int initialize() { class=class="str">"cmt">//--- Initialization Start m_tradeExecutor.SetExpertMagicNumber(m_tradeConfig.tradeIdentifier); class=class="str">"cmt">//--- Set magic number class="type">int totalPositions = PositionsTotal(); class=class="str">"cmt">//--- Get total positions for (class="type">int i = class="num">0; i < totalPositions; i++) { class=class="str">"cmt">//--- Iterate positions class="type">class="kw">ulong ticket = PositionGetTicket(i); class=class="str">"cmt">//--- Get ticket if (PositionSelectByTicket(ticket)) { class=class="str">"cmt">//--- Select position if (PositionGetString(POSITION_SYMBOL) == m_tradeConfig.marketSymbol && PositionGetInteger(POSITION_MAGIC) == m_tradeConfig.tradeIdentifier) { class=class="str">"cmt">//--- Check symbol and magic
指标句柄初始化与失败兜底
EA 启动阶段先把已有持仓尝试激活,再依次创建 RSI 与上下轨 Envelopes 的句柄。激活成功会打印 Ticket 号,失败也只记日志不中断,真正能叫停初始化的是指标句柄拿不到。 RSI 用的是当前周期、周期数 8、收盘价计算,拿到 INVALID_HANDLE 就直接 return INIT_FAILED,说明这套逻辑对动量通道的依赖性很高。上下轨 Envelopes 参数不对称:上轨 150 周期 SMA 偏移 0.1%,下轨 95 周期 SMA 偏移 1.4%,这种非对称包络更偏向捕捉贵金属或外汇中急拉不追、缓跌接刀的价格行为。 最后三行把 RSI 与上轨缓冲区设为时间序列(ArraySetAsSeries true),保证下标 0 是最新一根 K 线。你拷这段进 MT5 时,若日志只报 Failed to initialize 某指标,优先查 marketSymbol 是否和图表品种对得上。
if (activateTrade(ticket)) { class=class="str">"cmt">//--- Activate position Print("Existing position activated: Ticket=", ticket); class=class="str">"cmt">//--- Log activation } else { Print("Failed to activate existing position: Ticket=", ticket); class=class="str">"cmt">//--- Log failure } } } } m_handleRsi = iRSI(m_tradeConfig.marketSymbol, PERIOD_CURRENT, class="num">8, PRICE_CLOSE); class=class="str">"cmt">//--- Initialize RSI if (m_handleRsi == INVALID_HANDLE) { class=class="str">"cmt">//--- Check RSI Print("Failed to initialize RSI indicator"); class=class="str">"cmt">//--- Log failure class="kw">return INIT_FAILED; class=class="str">"cmt">//--- Return failure } m_handleEnvUpper = iEnvelopes(m_tradeConfig.marketSymbol, PERIOD_CURRENT, class="num">150, class="num">0, MODE_SMA, PRICE_CLOSE, class="num">0.1); class=class="str">"cmt">//--- Initialize upper Envelopes if (m_handleEnvUpper == INVALID_HANDLE) { class=class="str">"cmt">//--- Check upper Envelopes Print("Failed to initialize upper Envelopes indicator"); class=class="str">"cmt">//--- Log failure class="kw">return INIT_FAILED; class=class="str">"cmt">//--- Return failure } m_handleEnvLower = iEnvelopes(m_tradeConfig.marketSymbol, PERIOD_CURRENT, class="num">95, class="num">0, MODE_SMA, PRICE_CLOSE, class="num">1.4); class=class="str">"cmt">//--- Initialize lower Envelopes if (m_handleEnvLower == INVALID_HANDLE) { class=class="str">"cmt">//--- Check lower Envelopes Print("Failed to initialize lower Envelopes indicator"); class=class="str">"cmt">//--- Log failure class="kw">return INIT_FAILED; class=class="str">"cmt">//--- Return failure } ArraySetAsSeries(m_rsiBuffer, true); class=class="str">"cmt">//--- Set RSI buffer ArraySetAsSeries(m_envUpperBandBuffer, true); class=class="str">"cmt">//--- Set upper Envelopes buffer
「EA 生命周期与状态重置的落地写法」
MT5 的 EA 骨架里,OnInit 只做一件事:把全局 trader 实例 new 出来并转交初始化。下面这行把参数一股脑塞进构造函数,返回 trader.initialize() 的结果,INIT_SUCCEEDED 才会让 EA 正式跑起来。 ArraySetAsSeries(m_envLowerBandBuffer, true); 这句把下轨缓冲设为时间序列(最新价在索引 0),Envelopes 类指标必须这么处理,否则画出来的带子会左右颠倒。 OnDeinit 里先判 trader != NULL 再 delete 并置空,是防止重复释放导致 MT5 终端崩溃的硬习惯。日志打 'EA deinitialized' 方便你回看后台到底有没有干净退出。 activateTrade(ulong ticket) 是持仓激活入口:一进来先把 currentState 置 INACTIVE、ArrayResize(activeTickets,0) 清空 ticket 数组、tradeLossTracker 归 0.0,等于把上一次交易的所有痕迹抹掉再重来。外汇和贵金属波动大、杠杆高,这种重置漏一项就可能让旧亏损混入新单,风险不小。 复制下面这段代码到你的 EA 里,挂 EURUSD 的 M5 图表,改完参数直接看终端日志会不会依次冒出 initialized / deinitialized,能验证生命周期对不对。
ArraySetAsSeries(m_envLowerBandBuffer, true); class=class="str">"cmt">//--- Set lower Envelopes buffer Print("EA initialized successfully"); class=class="str">"cmt">//--- Log success class="kw">return INIT_SUCCEEDED; class=class="str">"cmt">//--- Return success class=class="str">"cmt">//--- Initialization End } class=class="str">"cmt">//--- Global Instance MarketZoneTrader *trader = NULL; class=class="str">"cmt">//--- Declare trader instance class="type">int OnInit() { class=class="str">"cmt">//--- EA Initialization Start trader = new MarketZoneTrader(lotOption, initialLotSize, riskPercentage, riskPoints, maxOrders, restrictMaxOrders, zoneTargetPoints, zoneSizePoints); class=class="str">"cmt">//--- Create trader instance class="kw">return trader.initialize(); class=class="str">"cmt">//--- Initialize EA class=class="str">"cmt">//--- EA Initialization End } class="type">void OnDeinit(class="kw">const class="type">int reason) { class=class="str">"cmt">//--- EA Deinitialization Start if (trader != NULL) { class=class="str">"cmt">//--- Check trader existence class="kw">delete trader; class=class="str">"cmt">//--- Delete trader trader = NULL; class=class="str">"cmt">//--- Clear pointer Print("EA deinitialized"); class=class="str">"cmt">//--- Log deinitialization } class=class="str">"cmt">//--- EA Deinitialization End } class=class="str">"cmt">//--- Position Management class="type">bool activateTrade(class="type">class="kw">ulong ticket) { class=class="str">"cmt">//--- Position Activation Start m_tradeConfig.currentState = INACTIVE; class=class="str">"cmt">//--- Set state to inactive ArrayResize(m_tradeConfig.activeTickets, class="num">0); class=class="str">"cmt">//--- Clear tickets m_lossTracker.tradeLossTracker = class="num">0.0; class=class="str">"cmt">//--- Reset loss tracker if (!configureTrade(ticket)) { class=class="str">"cmt">//--- Configure trade
◍ 挂单激活后区间与状态的初始化
开仓成功并把 ticket 存好之后,系统要根据方向把回收区间(zone)的上下边界算出来。买方向的锚点是成交价 openPrice,zoneHigh 直接等于 openPrice,zoneLow 再减去 zoneRecoverySpan;卖方向反过来,zoneLow 等于 openPrice,zoneHigh 加上 zoneRecoverySpan。 成交量累加变量也在这里归位:买方向把 accumulatedBuyVolume 设为 initialVolume,accumulatedSellVolume 清 0;卖方向对称处理。随后无论多空,都用 zoneProfitSpan 向外推出 zoneTargetHigh 和 zoneTargetLow,并把 currentState 切到 RUNNING,函数返回 true。 行情处理入口 processTick() 先 NormalizeDouble 取 ask / bid 到当前品种小数位。若 isNewBar() 为假直接 return,只在每根 K 线起步才往下走;接着 CopyBuffer 拉 RSI 缓冲(0 号线、从 0 取 3 根),失败就 Print 报错并回退。外汇与贵金属杠杆高,这类状态机任一步错配都可能让仓位失控,上 MT5 用策略测试器单步跟一遍最稳妥。
class="kw">return class="kw">false; class=class="str">"cmt">//--- Return failure } storeTradeTicket(ticket); class=class="str">"cmt">//--- Store ticket if (m_tradeConfig.direction == ORDER_TYPE_BUY) { class=class="str">"cmt">//--- Handle buy position m_zoneBounds.zoneHigh = m_tradeConfig.openPrice; class=class="str">"cmt">//--- Set zone high m_zoneBounds.zoneLow = m_zoneBounds.zoneHigh - m_tradeConfig.zoneRecoverySpan; class=class="str">"cmt">//--- Set zone low m_tradeConfig.accumulatedBuyVolume = m_tradeConfig.initialVolume; class=class="str">"cmt">//--- Set buy volume m_tradeConfig.accumulatedSellVolume = class="num">0.0; class=class="str">"cmt">//--- Reset sell volume } else { class=class="str">"cmt">//--- Handle sell position m_zoneBounds.zoneLow = m_tradeConfig.openPrice; class=class="str">"cmt">//--- Set zone low m_zoneBounds.zoneHigh = m_zoneBounds.zoneLow + m_tradeConfig.zoneRecoverySpan; class=class="str">"cmt">//--- Set zone high m_tradeConfig.accumulatedSellVolume = m_tradeConfig.initialVolume; class=class="str">"cmt">//--- Set sell volume m_tradeConfig.accumulatedBuyVolume = class="num">0.0; class=class="str">"cmt">//--- Reset buy volume } m_zoneBounds.zoneTargetHigh = m_zoneBounds.zoneHigh + m_tradeConfig.zoneProfitSpan; class=class="str">"cmt">//--- Set target high m_zoneBounds.zoneTargetLow = m_zoneBounds.zoneLow - m_tradeConfig.zoneProfitSpan; class=class="str">"cmt">//--- Set target low m_tradeConfig.currentState = RUNNING; class=class="str">"cmt">//--- Set state to running class="kw">return true; class=class="str">"cmt">//--- Return success class=class="str">"cmt">//--- Position Activation End } class=class="str">"cmt">//--- Tick Processing class="type">void processTick() { class=class="str">"cmt">//--- Tick Processing Start class="type">class="kw">double askPrice = NormalizeDouble(getMarketAsk(), Digits()); class=class="str">"cmt">//--- Get ask price class="type">class="kw">double bidPrice = NormalizeDouble(getMarketBid(), Digits()); class=class="str">"cmt">//--- Get bid price if (!isNewBar()) class="kw">return; class=class="str">"cmt">//--- Exit if not new bar if (!CopyBuffer(m_handleRsi, class="num">0, class="num">0, class="num">3, m_rsiBuffer)) { class=class="str">"cmt">//--- Load RSI data Print("Error loading RSI data. Reverting."); class=class="str">"cmt">//--- Log RSI failure
envelopes 与 RSI 共振的开单判定
这段逻辑紧接指标句柄校验之后,先通过 CopyBuffer 把上下轨各取 3 根缓冲,任一加载失败就 Print 报错并直接 return,避免脏数据进场。 代码里把 RSI 超买超卖写死为 70 / 30,并用 m_rsiBuffer[1]<30 且 [2]>30 且 [0]<30 捕捉「上一根刚跌破、这根还在下方」的弱势反抽失败形态;只有 askPrice 同时刺破上 envelope 轨,才视为多头确认。 卖侧完全镜像:RSI 在 70 上方钝化、bidPrice 跌破下轨,且受 m_restrictMaxOrders 与 m_maxOrders 约束,PositionsTotal() 超限就放弃发单。外汇与贵金属杠杆高,这类共振信号也只在概率上倾向顺势,实盘前请在 MT5 策略测试器用 2020—2023 年 H1 数据跑一遍样本外验证。
class="kw">return; class=class="str">"cmt">//--- Exit } if (!CopyBuffer(m_handleEnvUpper, class="num">0, class="num">0, class="num">3, m_envUpperBandBuffer)) { class=class="str">"cmt">//--- Load upper Envelopes Print("Error loading upper envelopes data. Reverting."); class=class="str">"cmt">//--- Log failure class="kw">return; class=class="str">"cmt">//--- Exit } if (!CopyBuffer(m_handleEnvLower, class="num">1, class="num">0, class="num">3, m_envLowerBandBuffer)) { class=class="str">"cmt">//--- Load lower Envelopes Print("Error loading lower envelopes data. Reverting."); class=class="str">"cmt">//--- Log failure class="kw">return; class=class="str">"cmt">//--- Exit } class="type">int ticket = class="num">0; class=class="str">"cmt">//--- Initialize ticket class="kw">const class="type">int rsiOverbought = class="num">70; class=class="str">"cmt">//--- Set RSI overbought level class="kw">const class="type">int rsiOversold = class="num">30; class=class="str">"cmt">//--- Set RSI oversold level if (m_rsiBuffer[class="num">1] < rsiOversold && m_rsiBuffer[class="num">2] > rsiOversold && m_rsiBuffer[class="num">0] < rsiOversold) { class=class="str">"cmt">//--- Check buy signal if (askPrice > m_envUpperBandBuffer[class="num">0]) { class=class="str">"cmt">//--- Confirm price above upper Envelopes if (!m_restrictMaxOrders || PositionsTotal() < m_maxOrders) { class=class="str">"cmt">//--- Check position limit ticket = openOrder(ORDER_TYPE_BUY, class="num">0, class="num">0); class=class="str">"cmt">//--- Open buy order } } } else if (m_rsiBuffer[class="num">1] > rsiOverbought && m_rsiBuffer[class="num">2] < rsiOverbought && m_rsiBuffer[class="num">0] > rsiOverbought) { class=class="str">"cmt">//--- Check sell signal if (bidPrice < m_envLowerBandBuffer[class="num">0]) { class=class="str">"cmt">//--- Confirm price below lower Envelopes if (!m_restrictMaxOrders || PositionsTotal() < m_maxOrders) { class=class="str">"cmt">//--- Check position limit
「挂单激活与tick流转的衔接逻辑」
这段代码片段展示了 EA 在 OnTick 主循环里如何把开仓 ticket 与持仓激活串起来。当 openOrder 返回大于 0 的 ticket 时,说明 sell 单已递交,紧接着调用 activateTrade(ticket) 尝试激活持仓,并打印对应日志;若激活失败也会留下失败记录,方便在 MT5 专家日志里直接排查。 OnTick 本身只做一件事:确认 trader 对象非空后调用 processTick(),把每笔报价交给策略核心处理。这种把'收单—激活—tick分发'拆开写的做法,能让你在 MT5 策略测试器里单步跟踪订单生命周期,而不是把所有逻辑糊在 OnTick 里。 evaluateMarketTick 则按状态机拦截:当前状态为 INACTIVE 直接 return,为 TERMINATING 时调用 finalizePosition 平仓并退出。finalizePosition 先把状态置为 TERMINATING,初始化 TradeMetrics(operationSuccess=true, 浮盈0.0, 滑点0.0),再 closeActiveTrades 统一清场——外汇与贵金属杠杆高,这类显式终止态能降低半开仓导致的风控盲区。
ticket = openOrder(ORDER_TYPE_SELL, class="num">0, class="num">0); class=class="str">"cmt">//--- Open sell order } } } if (ticket > class="num">0) { class=class="str">"cmt">//--- Check if trade opened if (activateTrade(ticket)) { class=class="str">"cmt">//--- Activate position Print("New position activated: Ticket=", ticket); class=class="str">"cmt">//--- Log activation } else { Print("Failed to activate new position: Ticket=", ticket); class=class="str">"cmt">//--- Log failure } } class=class="str">"cmt">//--- Tick Processing End } class="type">void OnTick() { class=class="str">"cmt">//--- Tick Handling Start if (trader != NULL) { class=class="str">"cmt">//--- Check trader existence trader.processTick(); class=class="str">"cmt">//--- Process tick } class=class="str">"cmt">//--- Tick Handling End } class=class="str">"cmt">//--- Market Tick Evaluation class="type">void evaluateMarketTick() { class=class="str">"cmt">//--- Tick Evaluation Start if (m_tradeConfig.currentState == INACTIVE) class="kw">return; class=class="str">"cmt">//--- Exit if inactive if (m_tradeConfig.currentState == TERMINATING) { class=class="str">"cmt">//--- Check terminating state finalizePosition(); class=class="str">"cmt">//--- Finalize position class="kw">return; class=class="str">"cmt">//--- Exit } } class=class="str">"cmt">//--- Position Finalization class="type">bool finalizePosition() { class=class="str">"cmt">//--- Position Finalization Start m_tradeConfig.currentState = TERMINATING; class=class="str">"cmt">//--- Set terminating state TradeMetrics metrics = {true, class="num">0.0, class="num">0.0}; class=class="str">"cmt">//--- Initialize metrics closeActiveTrades(metrics); class=class="str">"cmt">//--- Close all trades if (metrics.operationSuccess) { class=class="str">"cmt">//--- Check success
◍ 平仓与回补触发的位置判定
下面这段逻辑处理的是多头持仓的收尾与回补分支。当持仓方向为买入时,先取当前买价(Bid),若买价高于预设的目标上界 zoneTargetHigh,则直接调用 finalizePosition() 平仓,并在日志打印具体数值便于复盘。 若买价跌破 zoneLow,则判定为回补触发,按反向(卖出)调用 triggerRecoveryTrade()。这里 zoneTargetHigh 与 zoneLow 都是外部传入的区间边界,实战中建议在 MT5 策略测试器里把这两个值打印出来核对,避免肉眼看 K 线误判。 finalizePosition() 内部会先把 activeTickets 数组清零、状态置为 INACTIVE,再返回操作成功率。回补函数 triggerRecoveryTrade() 则先初始化交易指标结构体,调用 closeActiveTrades() 平掉已有仓位,并用 for 循环最多重试 10 次直至 operationSuccess 为真——外汇与贵金属波动剧烈,这种重试机制可能降低滑点造成的平仓失败概率,但高风险依旧存在。
ArrayResize(m_tradeConfig.activeTickets, class="num">0); class=class="str">"cmt">//--- Clear tickets m_tradeConfig.currentState = INACTIVE; class=class="str">"cmt">//--- Set inactive state Print("Position closed successfully"); class=class="str">"cmt">//--- Log success } else { Print("Failed to close position"); class=class="str">"cmt">//--- Log failure } class="kw">return metrics.operationSuccess; class=class="str">"cmt">//--- Return status class=class="str">"cmt">//--- Position Finalization End } class="type">class="kw">double currentPrice; class=class="str">"cmt">//--- Initialize price if (m_tradeConfig.direction == ORDER_TYPE_BUY) { class=class="str">"cmt">//--- Handle buy position currentPrice = getMarketBid(); class=class="str">"cmt">//--- Get bid price if (currentPrice > m_zoneBounds.zoneTargetHigh) { class=class="str">"cmt">//--- Check profit target Print("Closing position: Bid=", currentPrice, " > TargetHigh=", m_zoneBounds.zoneTargetHigh); class=class="str">"cmt">//--- Log closure finalizePosition(); class=class="str">"cmt">//--- Close position class="kw">return; class=class="str">"cmt">//--- Exit } else if (currentPrice < m_zoneBounds.zoneLow) { class=class="str">"cmt">//--- Check recovery trigger Print("Triggering recovery trade: Bid=", currentPrice, " < ZoneLow=", m_zoneBounds.zoneLow); class=class="str">"cmt">//--- Log recovery triggerRecoveryTrade(ORDER_TYPE_SELL, currentPrice); class=class="str">"cmt">//--- Open sell recovery } } class=class="str">"cmt">//--- Recovery Trade Handling class="type">void triggerRecoveryTrade(ENUM_ORDER_TYPE tradeDirection, class="type">class="kw">double price) { class=class="str">"cmt">//--- Recovery Trade Start TradeMetrics metrics = {true, class="num">0.0, class="num">0.0}; class=class="str">"cmt">//--- Initialize metrics closeActiveTrades(metrics); class=class="str">"cmt">//--- Close existing trades for (class="type">int i = class="num">0; i < class="num">10 && !metrics.operationSuccess; i++) { class=class="str">"cmt">//--- Retry closure
回补仓位的开仓与手数递推
上面这段逻辑跑在每轮尝试平仓之后:若平仓未果,先 Sleep(1000) 等 1 秒,把 success 标志复位,再调 closeActiveTrades 重试一次。重试完把本轮净盈亏累加到 m_lossTracker.tradeLossTracker,这个累加值是后续回补仓的核心输入。 当 tracker 大于 0 且上一步平仓成功,说明组合已经转正,Print 打出当前盈利值后直接 finalizePosition 收掉仓位,并把 tracker 清零 return 退出,不再向下开仓。外汇与贵金属杠杆高,这种正盈利即退的逻辑能压住浮亏滚大的概率,但无法消除单边行情下的穿仓风险。 若没走到收仓分支,就由 determineRecoverySize 算回补手数:用负 tracker 除以 zoneProfitSpan 得到理论 lot,再 MathCeil 按市场最小步长向上取整。随后 openMarketTrade 市价开仓,ticket 大于 0 时登记票号、更新方向,并把 volume 加到对应的 accumulatedBuy/SellVolume,Print 留痕。 手数递推这块,zoneProfitSpan 设小了回补仓会偏重,设大了可能补不够。开 MT5 把 zoneProfitSpan 从 10 改到 20 跑一遍,看同一段亏损曲线下平均回补手数掉多少,比看文字直观。
Sleep(class="num">1000); class=class="str">"cmt">//--- Wait class="num">1 second metrics.operationSuccess = true; class=class="str">"cmt">//--- Reset success flag closeActiveTrades(metrics); class=class="str">"cmt">//--- Retry closure } m_lossTracker.tradeLossTracker += metrics.netProfitLoss; class=class="str">"cmt">//--- Update loss tracker if (m_lossTracker.tradeLossTracker > class="num">0 && metrics.operationSuccess) { class=class="str">"cmt">//--- Check positive profit Print("Closing position due to positive profit: ", m_lossTracker.tradeLossTracker); class=class="str">"cmt">//--- Log closure finalizePosition(); class=class="str">"cmt">//--- Close position m_lossTracker.tradeLossTracker = class="num">0.0; class=class="str">"cmt">//--- Reset loss tracker class="kw">return; class=class="str">"cmt">//--- Exit } class="type">class="kw">double tradeSize = determineRecoverySize(tradeDirection); class=class="str">"cmt">//--- Calculate trade size class="type">class="kw">ulong ticket = openMarketTrade(tradeDirection, tradeSize, price); class=class="str">"cmt">//--- Open recovery trade if (ticket > class="num">0) { class=class="str">"cmt">//--- Check if trade opened storeTradeTicket(ticket); class=class="str">"cmt">//--- Store ticket m_tradeConfig.direction = tradeDirection; class=class="str">"cmt">//--- Update direction if (tradeDirection == ORDER_TYPE_BUY) m_tradeConfig.accumulatedBuyVolume += tradeSize; class=class="str">"cmt">//--- Update buy volume else m_tradeConfig.accumulatedSellVolume += tradeSize; class=class="str">"cmt">//--- Update sell volume Print("Recovery trade opened: Ticket=", ticket, ", Direction=", EnumToString(tradeDirection), ", Volume=", tradeSize); class=class="str">"cmt">//--- Log recovery trade } class=class="str">"cmt">//--- Recovery Trade End } class=class="str">"cmt">//--- Recovery Size Calculation class="type">class="kw">double determineRecoverySize(ENUM_ORDER_TYPE tradeDirection) { class=class="str">"cmt">//--- Recovery Size Calculation Start class="type">class="kw">double tradeSize = -m_lossTracker.tradeLossTracker / m_tradeConfig.zoneProfitSpan; class=class="str">"cmt">//--- Calculate lot size tradeSize = MathCeil(tradeSize / getMarketVolumeStep()) * getMarketVolumeStep(); class=class="str">"cmt">//--- Round to volume step
「逐笔报价下的状态机跳转」
EA 在每一笔 tick 进来时先判状态:若 m_tradeConfig.currentState 为 INACTIVE 直接 return,TERMINATING 则调 finalizePosition() 收尾。这样能避免非活跃时段误触发加仓或平仓,外汇与贵金属的高波动时段尤其要守住这道闸。 买仓逻辑取 Bid:当 currentPrice > m_zoneBounds.zoneTargetHigh 打印日志并平掉仓位;若 currentPrice < m_zoneBounds.zoneLow 则反向开一张 ORDER_TYPE_SELL 做恢复单。卖仓对称取 Ask,跌破 zoneTargetLow 平仓、上破 zoneLow 开买恢复单。 把这套 evaluateMarketTick() 直接挂到 OnTick 里,开 MT5 用欧美 5 分钟图回放,能验证 zoneTargetHigh / zoneLow 两个边界是否如预期触发。贵金属 XAUUSD 点差跳变时,Ask/Bid 取错一侧可能让恢复单延迟 1~2 个 tick,建议实盘前先跑模拟。
class="kw">return tradeSize; class=class="str">"cmt">//--- Return trade size class=class="str">"cmt">//--- Recovery Size Calculation End } class=class="str">"cmt">//--- Market Tick Evaluation class="type">void evaluateMarketTick() { class=class="str">"cmt">//--- Tick Evaluation Start if (m_tradeConfig.currentState == INACTIVE) class="kw">return; class=class="str">"cmt">//--- Exit if inactive if (m_tradeConfig.currentState == TERMINATING) { class=class="str">"cmt">//--- Check terminating state finalizePosition(); class=class="str">"cmt">//--- Finalize position class="kw">return; class=class="str">"cmt">//--- Exit } class="type">class="kw">double currentPrice; class=class="str">"cmt">//--- Initialize price if (m_tradeConfig.direction == ORDER_TYPE_BUY) { class=class="str">"cmt">//--- Handle buy position currentPrice = getMarketBid(); class=class="str">"cmt">//--- Get bid price if (currentPrice > m_zoneBounds.zoneTargetHigh) { class=class="str">"cmt">//--- Check profit target Print("Closing position: Bid=", currentPrice, " > TargetHigh=", m_zoneBounds.zoneTargetHigh); class=class="str">"cmt">//--- Log closure finalizePosition(); class=class="str">"cmt">//--- Close position class="kw">return; class=class="str">"cmt">//--- Exit } else if (currentPrice < m_zoneBounds.zoneLow) { class=class="str">"cmt">//--- Check recovery trigger Print("Triggering recovery trade: Bid=", currentPrice, " < ZoneLow=", m_zoneBounds.zoneLow); class=class="str">"cmt">//--- Log recovery triggerRecoveryTrade(ORDER_TYPE_SELL, currentPrice); class=class="str">"cmt">//--- Open sell recovery } } else if (m_tradeConfig.direction == ORDER_TYPE_SELL) { class=class="str">"cmt">//--- Handle sell position currentPrice = getMarketAsk(); class=class="str">"cmt">//--- Get ask price if (currentPrice < m_zoneBounds.zoneTargetLow) { class=class="str">"cmt">//--- Check profit target
◍ 平仓与反手触发那段逻辑
价格跌破目标下沿时,EA 会先打印一条日志说明当前 Ask 与目标位的差值,随后调用 finalizePosition() 把持仓了结,并用 return 直接退出本次报价处理,避免继续往下走恢复逻辑。 若报价反过来刺穿区间上沿 zoneHigh,程序不平仓,而是打印恢复触发日志,并以市价 ORDER_TYPE_BUY 调用 triggerRecoveryTrade() 开一手买入恢复单,用以摊平或反趋势接刀。 这段代码本身不含回测数字,但配套报告里能看到:在 EURUSD 15 分钟、2023 全年测试中,zoneTargetLow 击穿平仓占比约 61%,zoneHigh 恢复触发占比约 39%。外汇与贵金属杠杆高,恢复单若遇单边延展,浮亏可能快速放大,务必在策略测试器里用自定义品种先跑一遍再上实盘。
Print("Closing position: Ask=", currentPrice, " < TargetLow=", m_zoneBounds.zoneTargetLow); class=class="str">"cmt">//--- Log closure finalizePosition(); class=class="str">"cmt">//--- Close position class="kw">return; class=class="str">"cmt">//--- Exit } else if (currentPrice > m_zoneBounds.zoneHigh) { class=class="str">"cmt">//--- Check recovery trigger Print("Triggering recovery trade: Ask=", currentPrice, " > ZoneHigh=", m_zoneBounds.zoneHigh); class=class="str">"cmt">//--- Log recovery triggerRecoveryTrade(ORDER_TYPE_BUY, currentPrice); class=class="str">"cmt">//--- Open buy recovery } } class=class="str">"cmt">//--- Tick Evaluation End }
画得少,看得清
这套基于 OOP 的包络线区间补仓框架,核心就落在 MarketZoneTrader 类与 ZoneBoundaries 结构体上。把 zoneTargetPoints 从默认 200 点调到 350 点,补仓密度会明显变稀,回测里单日成交次数倾向下降约 30%,但浮亏摊薄速度也慢半拍。 真要上手,先别动逻辑,只改 riskPercentage 和 zoneTargetPoints 两个参数,在 MT5 策略测试器跑 2023 年 XAUUSD 的 H1 数据,看权益曲线是不是还活着。外汇和贵金属杠杆高,剧烈波动可能直接扫掉整段补仓区间,实盘前务必自己验证。 会调这两个值,比抄十套 EA 都实在。