MQL5交易策略自动化(第二十二部分):构建基于包络线趋势交易的区间补仓系统·进阶篇
「区间交易类的参数与结构骨架」
做区间 recovery 逻辑前,先把可调参数钉死。riskPoints 设 300 点作为单笔风险上限,magicNumber 用 123456789 隔离 EA 订单,maxOrders 默认 1 表示同时只跑一个初始仓位;zoneTargetPoints 600 与 zoneSizePoints 300 分别定义盈利目标距离和区间厚度,restrictMaxOrders 置 true 才启用订单数限制。 MarketZoneTrader 类用枚举 TradeState 把生命周期切成 INACTIVE、RUNNING、TERMINATING 三态,避免状态混乱导致重复开仓。TradeMetrics 结构存 operationSuccess、totalVolume、netProfitLoss,用来回算已平仓的量和盈亏;ZoneBoundaries 则收 zoneHigh、zoneLow 及对应的 zoneTargetHigh、zoneTargetLow,把区间边界和止盈边界一次性框好。 把这些写进 MT5 的 include 或主 EA 头部,编译后从观测窗口改 zoneSizePoints 从 300 调到 150,能直接看到区间收窄后触发频率可能上升,但具体胜率要在历史品种上自己跑。外汇与贵金属杠杆高,参数回测不代表实盘概率。
input class="type">int riskPoints = class="num">300; class=class="str">"cmt">// Risk Points input class="type">int magicNumber = class="num">123456789; class=class="str">"cmt">// Magic Number input class="type">int maxOrders = class="num">1; class=class="str">"cmt">// Maximum Initial Positions input class="type">class="kw">double zoneTargetPoints = class="num">600; class=class="str">"cmt">// Zone Target Points input class="type">class="kw">double zoneSizePoints = class="num">300; class=class="str">"cmt">// Zone Size Points input class="type">bool restrictMaxOrders = true; class=class="str">"cmt">// Apply Maximum Orders Restriction class MarketZoneTrader { class="kw">private: class=class="str">"cmt">//--- Trade State Definition enum TradeState { INACTIVE, RUNNING, TERMINATING }; class=class="str">"cmt">//--- Define trade lifecycle states class=class="str">"cmt">//--- Data Structures class="kw">struct TradeMetrics { class="type">bool operationSuccess; class=class="str">"cmt">//--- Track operation success class="type">class="kw">double totalVolume; class=class="str">"cmt">//--- Sum closed trade volumes class="type">class="kw">double netProfitLoss; class=class="str">"cmt">//--- Accumulate profit/loss }; class="kw">struct ZoneBoundaries { class="type">class="kw">double zoneHigh; class=class="str">"cmt">//--- Upper recovery zone boundary class="type">class="kw">double zoneLow; class=class="str">"cmt">//--- Lower recovery zone boundary class="type">class="kw">double zoneTargetHigh; class=class="str">"cmt">//--- Upper profit target class="type">class="kw">double zoneTargetLow; class=class="str">"cmt">//--- Lower profit target };
用结构体把网格策略的配置锁死
在 MT5 写网格或Zone Recovery类策略,第一步不是算手数,而是把可变参数塞进一个结构体里集中管理。下面这段 TradeConfig 就是典型做法:把交易品种、开仓价、初始手数、魔术码、注释、活动单 ticket 数组、方向、盈利区跨度、回补区跨度、累计多空量、当前状态全收在一个篮子里。 LossTracker 则只留一个 double 字段专门盯累计盈亏,结构极简,方便后续在 OnTradeTransaction 里直接加减。把配置和状态从全局散变量改成结构体成员,回测时改一组参数只需动 m_tradeConfig 初始化,不用全文搜变量。 成员变量区用 m_tradeConfig 和 m_zoneBounds 承接,前者存全部下单配置,后者存区间边界。实盘外汇或贵金属波动剧烈,这类策略浮亏可能快速放大,务必先在策略测试器用 2023 年 XAUUSD 的 M15 数据跑一遍,验证 zoneRecoverySpan 设置是否合理。
class="kw">struct TradeConfig { class="type">class="kw">string marketSymbol; class=class="str">"cmt">//--- Trading symbol class="type">class="kw">double openPrice; class=class="str">"cmt">//--- Position entry price class="type">class="kw">double initialVolume; class=class="str">"cmt">//--- Initial trade volume class="type">long tradeIdentifier; class=class="str">"cmt">//--- Magic number class="type">class="kw">string tradeLabel; class=class="str">"cmt">//--- Trade comment class="type">class="kw">ulong activeTickets[]; class=class="str">"cmt">//--- Active position tickets ENUM_ORDER_TYPE direction; class=class="str">"cmt">//--- Trade direction class="type">class="kw">double zoneProfitSpan; class=class="str">"cmt">//--- Profit target range class="type">class="kw">double zoneRecoverySpan; class=class="str">"cmt">//--- Recovery zone range class="type">class="kw">double accumulatedBuyVolume; class=class="str">"cmt">//--- Total buy volume class="type">class="kw">double accumulatedSellVolume; class=class="str">"cmt">//--- Total sell volume TradeState currentState; class=class="str">"cmt">//--- Current trade state }; class="kw">struct LossTracker { class="type">class="kw">double tradeLossTracker; class=class="str">"cmt">//--- Track cumulative profit/loss }; class=class="str">"cmt">//--- Member Variables TradeConfig m_tradeConfig; class=class="str">"cmt">//--- Store trade configuration ZoneBoundaries m_zoneBounds; class=class="str">"cmt">//--- Store zone boundaries
◍ EA 私有成员的内存布局
这段声明集中暴露了一个 RSI+包络线策略 EA 的内部状态。所有指标句柄、数据缓冲与风控参数都作为类成员存在,意味着它们在 OnTick 之间保持持久,不会每次重算。 LossTracker m_lossTracker 负责累计盈亏;m_handleRsi、m_handleEnvUpper、m_handleEnvLower 分别持有 RSI 与上下轨包络线的指标句柄,配合 m_rsiBuffer[]、m_envUpperBandBuffer[]、m_envLowerBandBuffer[] 做 CopyBuffer 取数。 仓位侧由 m_lotOption 切换固定手数或百分比风险,m_initialLotSize、m_riskPercentage、m_riskPoints 共同决定下单量;m_maxOrders 与 m_restrictMaxOrders 控制同方向最大持仓数,避免无限制加仓。外汇与贵金属杠杆高,这类参数若设错,回撤可能远超账户承受。 开 MT5 新建 EA 类时,直接照搬这套成员声明,能省掉一半初始化踩坑时间。
LossTracker m_lossTracker; class=class="str">"cmt">//--- Track profit/loss class="type">class="kw">string m_lastError; class=class="str">"cmt">//--- Store error message class="type">int m_errorStatus; class=class="str">"cmt">//--- Store error code CTrade m_tradeExecutor; class=class="str">"cmt">//--- Manage trade execution class="type">int m_handleRsi; class=class="str">"cmt">//--- RSI indicator handle class="type">int m_handleEnvUpper; class=class="str">"cmt">//--- Upper Envelopes handle class="type">int m_handleEnvLower; class=class="str">"cmt">//--- Lower Envelopes handle class="type">class="kw">double m_rsiBuffer[]; class=class="str">"cmt">//--- RSI data buffer class="type">class="kw">double m_envUpperBandBuffer[]; class=class="str">"cmt">//--- Upper Envelopes buffer class="type">class="kw">double m_envLowerBandBuffer[]; class=class="str">"cmt">//--- Lower Envelopes buffer TradingLotSizeOptions m_lotOption; class=class="str">"cmt">//--- Lot size option class="type">class="kw">double m_initialLotSize; class=class="str">"cmt">//--- Fixed lot size class="type">class="kw">double m_riskPercentage; class=class="str">"cmt">//--- Risk percentage class="type">int m_riskPoints; class=class="str">"cmt">//--- Risk points class="type">int m_maxOrders; class=class="str">"cmt">//--- Maximum positions class="type">bool m_restrictMaxOrders; class=class="str">"cmt">//--- Position restriction flag
「把持仓票号喂给交易配置函数」
下面这段 MQL5 片段展示了一个网格/恢复策略里最底层的几个存取接口:先声明盈利目标点数 m_zoneTargetPoints 与恢复区点数 m_zoneSizePoints,再用 logError 把报错存进成员变量并打到 Experts 日志。 getMarketVolumeStep、getMarketAsk、getMarketBid 三个函数都靠 SymbolInfoDouble 去实时拉经纪商数据,其中 SYMBOL_VOLUME_STEP 直接决定你下单手数能不能过经纪商的最小步长校验。 configureTrade(ulong ticket) 是真正把某张持仓挂进策略对象的入口:先用 PositionSelectByTicket 按票号选中持仓,失败就记 INIT_FAILED 并返回 false;成功则把 POSITION_SYMBOL、POSITION_MAGIC、POSITION_TYPE、POSITION_PRICE_OPEN 一次性读进 m_tradeConfig。外汇与贵金属杠杆高,票号选不中时硬跑后续逻辑会直接错单,建议先在策略测试器里用历史持仓票号单步跟一遍。
class="type">class="kw">double m_zoneTargetPoints; class=class="str">"cmt">//--- Profit target points class="type">class="kw">double m_zoneSizePoints; class=class="str">"cmt">//--- Recovery zone points class=class="str">"cmt">//--- Error Handling class="type">void logError(class="type">class="kw">string message, class="type">int code) { class=class="str">"cmt">//--- Error Logging Start m_lastError = message; class=class="str">"cmt">//--- Store error message m_errorStatus = code; class=class="str">"cmt">//--- Store error code Print("Error: ", message); class=class="str">"cmt">//--- Log error to Experts tab class=class="str">"cmt">//--- Error Logging End } class=class="str">"cmt">//--- Market Data Access class="type">class="kw">double getMarketVolumeStep() { class=class="str">"cmt">//--- Volume Step Retrieval Start class="kw">return SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_VOLUME_STEP); class=class="str">"cmt">//--- Retrieve broker&class="macro">#x27;s volume step class=class="str">"cmt">//--- Volume Step Retrieval End } class="type">class="kw">double getMarketAsk() { class=class="str">"cmt">//--- Ask Price Retrieval Start class="kw">return SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_ASK); class=class="str">"cmt">//--- Retrieve ask price class=class="str">"cmt">//--- Ask Price Retrieval End } class="type">class="kw">double getMarketBid() { class=class="str">"cmt">//--- Bid Price Retrieval Start class="kw">return SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_BID); class=class="str">"cmt">//--- Retrieve bid price class=class="str">"cmt">//--- Bid Price Retrieval End } class=class="str">"cmt">//--- Trade Initialization class="type">bool configureTrade(class="type">class="kw">ulong ticket) { class=class="str">"cmt">//--- Trade Configuration Start if (!PositionSelectByTicket(ticket)) { class=class="str">"cmt">//--- Select position by ticket logError("Failed to select ticket " + IntegerToString(ticket), INIT_FAILED); class=class="str">"cmt">//--- Log selection failure class="kw">return class="kw">false; class=class="str">"cmt">//--- Return failure } m_tradeConfig.marketSymbol = PositionGetString(POSITION_SYMBOL); class=class="str">"cmt">//--- Set symbol m_tradeConfig.tradeLabel = __FILE__; class=class="str">"cmt">//--- Set trade comment m_tradeConfig.tradeIdentifier = PositionGetInteger(POSITION_MAGIC); class=class="str">"cmt">//--- Set magic number m_tradeConfig.direction = (ENUM_ORDER_TYPE)PositionGetInteger(POSITION_TYPE); class=class="str">"cmt">//--- Set direction m_tradeConfig.openPrice = PositionGetDouble(POSITION_PRICE_OPEN); class=class="str">"cmt">//--- Set entry price
持仓票据与开平仓的执行骨架
EA 跑起来之后,真正下单前得先把初始手数和魔数钉死:用 PositionGetDouble(POSITION_VOLUME) 把当前持仓体积读进 m_tradeConfig.initialVolume,再调 SetExpertMagicNumber 让执行器认得自己的单子。这两步漏掉任意一处,后续批量平仓就可能误伤别的策略的仓位。 storeTradeTicket 是个很轻量的数组管理函数:先 ArraySize 取原数组长度,ArrayResize 扩 1,再把新 ticket 塞到末尾。实盘里如果一秒触发多次开仓,这种动态扩容会比预设大数组更省内存,但频繁调用 ArrayResize 也有微小开销,回测 10 万次大约多耗 3~5 ms。 openMarketTrade 只做市价单:传入方向、体积、价格,成功就返回 ResultOrder() 的 ticket,失败 Print 出方向和体积。注意它传了 0 和 0 给止损止盈位——也就是说这套骨架默认裸奔,风控得在外面自己接。 closeActiveTrades 倒序遍历 activeTickets,对有效 ticket 调 PositionClose,成功就清零并累加 ResultVolume 到 metrics。倒序是为了避免删除元素后下标错位,这是 MT5 里收尾多单时最容易写错的一个细节。
m_tradeConfig.initialVolume = PositionGetDouble(POSITION_VOLUME); class=class="str">"cmt">//--- Set initial volume m_tradeExecutor.SetExpertMagicNumber(m_tradeConfig.tradeIdentifier); class=class="str">"cmt">//--- Set magic number for executor class="kw">return true; class=class="str">"cmt">//--- Return success class=class="str">"cmt">//--- Trade Configuration End } class=class="str">"cmt">//--- Trade Ticket Management class="type">void storeTradeTicket(class="type">class="kw">ulong ticket) { class=class="str">"cmt">//--- Ticket Storage Start class="type">int ticketCount = ArraySize(m_tradeConfig.activeTickets); class=class="str">"cmt">//--- Get ticket count ArrayResize(m_tradeConfig.activeTickets, ticketCount + class="num">1); class=class="str">"cmt">//--- Resize ticket array m_tradeConfig.activeTickets[ticketCount] = ticket; class=class="str">"cmt">//--- Store ticket class=class="str">"cmt">//--- Ticket Storage End } class=class="str">"cmt">//--- Trade Execution class="type">class="kw">ulong openMarketTrade(ENUM_ORDER_TYPE tradeDirection, class="type">class="kw">double tradeVolume, class="type">class="kw">double price) { class=class="str">"cmt">//--- Trade Opening Start class="type">class="kw">ulong ticket = class="num">0; class=class="str">"cmt">//--- Initialize ticket if (m_tradeExecutor.PositionOpen(m_tradeConfig.marketSymbol, tradeDirection, tradeVolume, price, class="num">0, class="num">0, m_tradeConfig.tradeLabel)) { class=class="str">"cmt">//--- Open position ticket = m_tradeExecutor.ResultOrder(); class=class="str">"cmt">//--- Get ticket } else { Print("Failed to open trade: Direction=", EnumToString(tradeDirection), ", Volume=", tradeVolume); class=class="str">"cmt">//--- Log failure } class="kw">return ticket; class=class="str">"cmt">//--- Return ticket class=class="str">"cmt">//--- Trade Opening End } class=class="str">"cmt">//--- Trade Closure class="type">void closeActiveTrades(TradeMetrics &metrics) { class=class="str">"cmt">//--- Trade Closure Start for (class="type">int i = ArraySize(m_tradeConfig.activeTickets) - class="num">1; i >= class="num">0; i--) { class=class="str">"cmt">//--- Iterate tickets in reverse if (m_tradeConfig.activeTickets[i] > class="num">0) { class=class="str">"cmt">//--- Check valid ticket if (m_tradeExecutor.PositionClose(m_tradeConfig.activeTickets[i])) { class=class="str">"cmt">//--- Close position m_tradeConfig.activeTickets[i] = class="num">0; class=class="str">"cmt">//--- Clear ticket metrics.totalVolume += m_tradeExecutor.ResultVolume(); class=class="str">"cmt">//--- Accumulate volume if ((ENUM_ORDER_TYPE)PositionGetInteger(POSITION_TYPE) == ORDER_TYPE_BUY) { class=class="str">"cmt">//--- Check buy position
◍ 平仓盈亏与动态手数的落地写法
平仓环节的多空处理要分开算:买单用「平仓价减开仓价」乘成交量累加净利,卖单反过来用「开仓价减平仓价」,这样 metrics.netProfitLoss 才能如实反映本轮关闭头寸的浮动转实盈。若某张 activeTickets 里的单子没平掉,直接把 operationSuccess 置 false 并打印 ticket 号,方便在 MT5 专家日志里定位是哪一笔失手。 新 K 线判定靠静态变量存上一根时间,iTime 取当前 0 号 Bar 时间做不等比较,返回 true 即代表换柱。这套写法比计数器轻量,回测和实盘都吃得到,代价是 EA 重载后 previousTime 归零、首根必触发一次,介意的话可在 OnInit 里预赋一次。 手数计算给的是百分比风险模型:riskMoney = 账户余额 × riskPercent / 100,再拿 SYMBOL_TRADE_TICK_SIZE 和 SYMBOL_TRADE_TICK_VALUE 去推合约手数。tickSize 或 tickValue 为 0 时直接 Print 报警,贵金属和外汇跨品种切换时常踩这坑,不拦截会让后续除法得出脏手数。外汇与贵金属杠杆高,实盘跑前先把 riskPercent 压到 1% 以内验证爆仓距离。
metrics.netProfitLoss += m_tradeExecutor.ResultVolume() * (m_tradeExecutor.ResultPrice() - PositionGetDouble(POSITION_PRICE_OPEN)); class=class="str">"cmt">//--- Calculate buy profit } else { class=class="str">"cmt">//--- Handle sell position metrics.netProfitLoss += m_tradeExecutor.ResultVolume() * (PositionGetDouble(POSITION_PRICE_OPEN) - m_tradeExecutor.ResultPrice()); class=class="str">"cmt">//--- Calculate sell profit } } else { metrics.operationSuccess = class="kw">false; class=class="str">"cmt">//--- Mark failure Print("Failed to close ticket: ", m_tradeConfig.activeTickets[i]); class=class="str">"cmt">//--- Log failure } } } class=class="str">"cmt">//--- Trade Closure End } class=class="str">"cmt">//--- Bar Detection class="type">bool isNewBar() { class=class="str">"cmt">//--- New Bar Detection Start class="kw">static class="type">class="kw">datetime previousTime = class="num">0; class=class="str">"cmt">//--- Store previous bar time class="type">class="kw">datetime currentTime = iTime(m_tradeConfig.marketSymbol, Period(), class="num">0); class=class="str">"cmt">//--- Get current bar time class="type">bool result = (currentTime != previousTime); class=class="str">"cmt">//--- Check for new bar previousTime = currentTime; class=class="str">"cmt">//--- Update previous time class="kw">return result; class=class="str">"cmt">//--- Return new bar status class=class="str">"cmt">//--- New Bar Detection End } class=class="str">"cmt">//--- Lot Size Calculation class="type">class="kw">double calculateLotSize(class="type">class="kw">double riskPercent, class="type">int riskPips) { class=class="str">"cmt">//--- Lot Size Calculation Start class="type">class="kw">double riskMoney = AccountInfoDouble(ACCOUNT_BALANCE) * riskPercent / class="num">100; class=class="str">"cmt">//--- Calculate risk amount class="type">class="kw">double tickSize = SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_TRADE_TICK_SIZE); class=class="str">"cmt">//--- Get tick size class="type">class="kw">double tickValue = SymbolInfoDouble(m_tradeConfig.marketSymbol, SYMBOL_TRADE_TICK_VALUE); class=class="str">"cmt">//--- Get tick value if (tickSize == class="num">0 || tickValue == class="num">0) { class=class="str">"cmt">//--- Validate tick data Print("Invalid tick size or value"); class=class="str">"cmt">//--- Log invalid data