MQL5 交易策略自动化(第 23 部分):带追踪止损与篮子交易的区间补仓系统·进阶篇
(2/3)· 接上篇基础补仓逻辑,本篇把移动止损与并行篮子搬进 MQL5,解决利润回吐和信号冲突
「指标句柄与篮子计数的初始化落点」
这段逻辑把 RSI 与上下轨 Envelopes 的句柄创建塞进一个初始化函数,顺便用两个辅助函数管活跃篮子数。RSI 周期取 8、收盘价计算,上轨 Envelopes 周期 150、偏离 0.1%,下轨周期 95、偏离 1.4%——上下轨周期和偏离都不对称,说明上下边界不是镜像通道,而是按各自波动特征分别调出来的。 初始化函数里每创建一个句柄都判 INVALID_HANDLE,失败就 Print 并返回 false,MT5 终端里能直接看到哪根指标没起来。三个缓冲区都用 ArraySetAsSeries(..., true) 改成时间序列,索引 0 就是当前柱,回测或实盘读值时不用再倒排。 countActiveBaskets 遍历 m_traders 数组,状态不是 INACTIVE 就累加;cleanupTerminatedBaskets 则反过来,碰到 INACTIVE 的实例就 delete 并置 NULL。外汇和贵金属杠杆高、跳空频繁,跑这套前先在策略测试器用历史数据验证篮子回收是否漏内存,再上模拟盘。
class="type">class="kw">double m_envLowerBandBuffer[]; class=class="str">"cmt">//--- Lower Envelopes buffer class="type">class="kw">string m_symbol; class=class="str">"cmt">//--- Trading symbol class="type">int m_baseMagicNumber; class=class="str">"cmt">//--- Base magic number class="type">int m_maxInitialPositions; class=class="str">"cmt">//--- Maximum baskets(signals) class=class="str">"cmt">//--- Initialize Indicators class="type">bool initializeIndicators() { m_handleRsi = iRSI(m_symbol, PERIOD_CURRENT, class="num">8, PRICE_CLOSE); if (m_handleRsi == INVALID_HANDLE) { Print("Failed to initialize RSI indicator"); class="kw">return false; } m_handleEnvUpper = iEnvelopes(m_symbol, PERIOD_CURRENT, class="num">150, class="num">0, MODE_SMA, PRICE_CLOSE, class="num">0.1); if (m_handleEnvUpper == INVALID_HANDLE) { Print("Failed to initialize upper Envelopes indicator"); class="kw">return false; } m_handleEnvLower = iEnvelopes(m_symbol, PERIOD_CURRENT, class="num">95, class="num">0, MODE_SMA, PRICE_CLOSE, class="num">1.4); if (m_handleEnvLower == INVALID_HANDLE) { Print("Failed to initialize lower Envelopes indicator"); class="kw">return false; } ArraySetAsSeries(m_rsiBuffer, true); ArraySetAsSeries(m_envUpperBandBuffer, true); ArraySetAsSeries(m_envLowerBandBuffer, true); class="kw">return true; } class=class="str">"cmt">//--- Count Active Baskets class="type">int countActiveBaskets() { class="type">int count = class="num">0; for (class="type">int i = class="num">0; i < ArraySize(m_traders); i++) { if (m_traders[i] != NULL && m_traders[i].getCurrentState() != MarketZoneTrader::INACTIVE) { count++; } } class="kw">return count; } class=class="str">"cmt">//--- Cleanup Terminated Baskets class="type">void cleanupTerminatedBaskets() { class="type">int newSize = class="num">0; for (class="type">int i = class="num">0; i < ArraySize(m_traders); i++) { if (m_traders[i] != NULL && m_traders[i].getCurrentState() == MarketZoneTrader::INACTIVE) { class="kw">delete m_traders[i]; m_traders[i] = NULL; }
◍ 篮子容器的压缩与持仓回填
管理多篮子交易对象时,指针数组里常混着已释放的 NULL 槽位。下面这段先扫一遍 m_traders,用 newSize 统计非空对象,再把有效指针搬到临时数组 temp,最后释放旧数组并按新尺寸重建,相当于一次就地压缩。
if (m_traders[i] != NULL) newSize++; } MarketZoneTrader* temp[]; ArrayResize(temp, newSize); class="type">int index = class="num">0; for (class="type">int i = class="num">0; i < ArraySize(m_traders); i++) { if (m_traders[i] != NULL) { temp[index] = m_traders[i]; index++; } } ArrayFree(m_traders); ArrayResize(m_traders, newSize); for (class="type">int i = class="num">0; i < newSize; i++) { m_traders[i] = temp[i]; } ArrayFree(temp); }
class="type">bool initialize() { if (!initializeIndicators()) class="kw">return false; class="type">int totalPositions = PositionsTotal(); for (class="type">int i = class="num">0; i < totalPositions; i++) { class="type">ulong ticket = PositionGetTicket(i); if (PositionSelectByTicket(ticket)) { if (PositionGetString(POSITION_SYMBOL) == m_symbol) { class="type">long magic = PositionGetInteger(POSITION_MAGIC); if (magic >= m_baseMagicNumber && magic < m_baseMagicNumber + m_maxInitialPositions) { class="type">bool exists = false; for (class="type">int j = class="num">0; j < ArraySize(m_traders); j++) { if (m_traders[j] != NULL && m_traders[j].getMagicNumber() == magic) { exists = true; break; } } if (!exists && countActiveBaskets() < m_maxInitialPositions) { createNewBasket(magic, ticket); } } } } } }
if (m_traders[i] != NULL) newSize++; } MarketZoneTrader* temp[]; ArrayResize(temp, newSize); class="type">int index = class="num">0; for (class="type">int i = class="num">0; i < ArraySize(m_traders); i++) { if (m_traders[i] != NULL) { temp[index] = m_traders[i]; index++; } } ArrayFree(m_traders); ArrayResize(m_traders, newSize); for (class="type">int i = class="num">0; i < newSize; i++) { m_traders[i] = temp[i]; } ArrayFree(temp); } class="kw">public: BasketManager(class="type">class="kw">string symbol, class="type">int baseMagic, class="type">int maxInitPos) { m_symbol = symbol; m_baseMagicNumber = baseMagic; m_maxInitialPositions = maxInitPos; ArrayResize(m_traders, class="num">0); m_handleRsi = INVALID_HANDLE; m_handleEnvUpper = INVALID_HANDLE; m_handleEnvLower = INVALID_HANDLE; } ~BasketManager() { for (class="type">int i = class="num">0; i < ArraySize(m_traders); i++) { if (m_traders[i] != NULL) class="kw">delete m_traders[i]; } ArrayFree(m_traders); cleanupIndicators(); } class="type">bool initialize() { if (!initializeIndicators()) class="kw">return false; class="type">int totalPositions = PositionsTotal(); for (class="type">int i = class="num">0; i < totalPositions; i++) { class="type">ulong ticket = PositionGetTicket(i); if (PositionSelectByTicket(ticket)) { if (PositionGetString(POSITION_SYMBOL) == m_symbol) { class="type">long magic = PositionGetInteger(POSITION_MAGIC); if (magic >= m_baseMagicNumber && magic < m_baseMagicNumber + m_maxInitialPositions) { class="type">bool exists = false; for (class="type">int j = class="num">0; j < ArraySize(m_traders); j++) { if (m_traders[j] != NULL && m_traders[j].getMagicNumber() == magic) { exists = true; break; } } if (!exists && countActiveBaskets() < m_maxInitialPositions) { createNewBasket(magic, ticket); } } } } }
持仓复活与指标句柄的初始化细节
EA 重启时最怕把场上已有仓位当没看见。这段初始化逻辑先扫一遍 PositionsTotal(),把每个 ticket 用 PositionSelectByTicket 选出来,再比对 SYMBOL 与 MAGIC 是否等于本策略配置。匹配上的就调 activateTrade 重新接管,日志里会打出「Existing position activated: Ticket=...」,没接住的也会单独报错,方便你开 MT5 的 Experts 标签核对哪单漏了。 RSI 用了周期 8、收盘价计算,Envelopes 上下轨参数很不对称:上轨 PERIOD 150、偏离 0.1,下轨 PERIOD 95、偏离 1.4。这种非对称包络说明策略对下方空间容忍度明显更大,可能是做均值回归类贵金属或外汇对时的一种偏多保护。 任一句柄返回 INVALID_HANDLE 就直接 INIT_FAILED 退出,不会带病运行。三个指标缓冲都立刻 ArraySetAsSeries(..., true) 改成时间序列,确保你后面用 [0] 取到的是最新一根 K 线的值,而不是最老那根。
class="type">int initialize() { m_tradeExecutor.SetExpertMagicNumber(m_tradeConfig.tradeIdentifier); class=class="str">"cmt">// 设置本 EA 的魔术码 class="type">int totalPositions = PositionsTotal(); class=class="str">"cmt">// 获取当前总持仓数 for (class="type">int i = class="num">0; i < totalPositions; i++) { class=class="str">"cmt">// 遍历所有持仓 class="type">ulong ticket = PositionGetTicket(i); class=class="str">"cmt">// 取第 i 个持仓的 ticket if (PositionSelectByTicket(ticket)) { class=class="str">"cmt">// 用 ticket 选中该持仓 if (PositionGetString(POSITION_SYMBOL) == m_tradeConfig.marketSymbol && PositionGetInteger(POSITION_MAGIC) == m_tradeConfig.tradeIdentifier) { class=class="str">"cmt">// 校验品种与魔术码 if (activateTrade(ticket)) { class=class="str">"cmt">// 重新激活接管 Print("Existing position activated: Ticket=", ticket); class=class="str">"cmt">// 日志:激活成功 } else { Print("Failed to activate existing position: Ticket=", ticket); class=class="str">"cmt">// 日志:激活失败 } } } } m_handleRsi = iRSI(m_tradeConfig.marketSymbol, PERIOD_CURRENT, class="num">8, PRICE_CLOSE); class=class="str">"cmt">// 建 RSI 句柄:当前周期、8期、收盘价 if (m_handleRsi == INVALID_HANDLE) { class=class="str">"cmt">// 校验 RSI 句柄 Print("Failed to initialize RSI indicator"); class=class="str">"cmt">// 日志:RSI 建失败 class="kw">return INIT_FAILED; class=class="str">"cmt">// 返回初始化失败 } 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">// 上轨包络:150期SMA、偏离0.class="num">1 if (m_handleEnvUpper == INVALID_HANDLE) { class=class="str">"cmt">// 校验上轨句柄 Print("Failed to initialize upper Envelopes indicator"); class=class="str">"cmt">// 日志:上轨失败 class="kw">return INIT_FAILED; class=class="str">"cmt">// 返回失败 } 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">// 下轨包络:95期SMA、偏离1.class="num">4 if (m_handleEnvLower == INVALID_HANDLE) { class=class="str">"cmt">// 校验下轨句柄 Print("Failed to initialize lower Envelopes indicator"); class=class="str">"cmt">// 日志:下轨失败 class="kw">return INIT_FAILED; class=class="str">"cmt">// 返回失败 } ArraySetAsSeries(m_rsiBuffer, true); class=class="str">"cmt">// RSI 缓冲改为时间序列 ArraySetAsSeries(m_envUpperBandBuffer, true); class=class="str">"cmt">// 上轨缓冲改为时间序列 ArraySetAsSeries(m_envLowerBandBuffer, true); class=class="str">"cmt">// 下轨缓冲改为时间序列 }
「信号判定与篮子创建的 tick 逻辑」
EA 的 processTick 是每笔报价驱动的核心。它先轮询已存在的篮子对象,调用各自的 processTick 处理挂单与平仓,再跑 cleanupTerminatedBaskets 回收已终结的实例,避免数组里堆 NULL 拖慢后续循环。 新信号只在「新 K 线」出现后才判定:isNewBar() 为假就直接 return,这意味着同根 bar 内重复 tick 不会重复开仓。随后用 CopyBuffer 分别拉取 RSI 与上下轨信封指标的各 3 根缓冲,任一失败就 Print 报错并退出,保证后面比价用的都是完整数据。 RSI 阈值写死为超买 70、超卖 30。开多条件为:前一根 rsi[1] 跌破 30、再前根 rsi[2] 在 30 上方、当前 rsi[0] 仍低于 30,且 ask 价高于上轨信封——属于「超卖区反抽但价格仍被上轨压制」的逆向博弈。开空对称:rsi[1] 上破 70、rsi[2] 在 70 下、rsi[0] 仍高于 70 且 bid 低于下轨。 两个方向都受 countActiveBaskets() < m_maxInitialPositions 约束,控制并发篮子数。命中后 newMagic = 基 magic + 当前数组长度,且必须小于基 magic + 最大初始数,相当于给每个篮子发独立识别号,方便 MT5 里按 magic 筛选持仓。外汇与贵金属杠杆高,这类逆向信号触发频率低但回撤可能偏大,实盘前务必用策略测试器跑足历史样本。
class="type">void processTick() { class=class="str">"cmt">//--- Process existing baskets for (class="type">int i = class="num">0; i < ArraySize(m_traders); i++) { if (m_traders[i] != NULL) { m_traders[i].processTick(m_rsiBuffer, m_envUpperBandBuffer, m_envLowerBandBuffer); } } cleanupTerminatedBaskets(); class=class="str">"cmt">//--- Check for new signals on new bar if (!isNewBar()) class="kw">return; if (!CopyBuffer(m_handleRsi, class="num">0, class="num">0, class="num">3, m_rsiBuffer)) { Print("Error loading RSI data. Reverting."); class="kw">return; } if (!CopyBuffer(m_handleEnvUpper, class="num">0, class="num">0, class="num">3, m_envUpperBandBuffer)) { Print("Error loading upper envelopes data. Reverting."); class="kw">return; } if (!CopyBuffer(m_handleEnvLower, class="num">1, class="num">0, class="num">3, m_envLowerBandBuffer)) { Print("Error loading lower envelopes data. Reverting."); class="kw">return; } const class="type">int rsiOverbought = class="num">70; const class="type">int rsiOversold = class="num">30; class="type">int ticket = -class="num">1; ENUM_ORDER_TYPE signalType = (ENUM_ORDER_TYPE)-class="num">1; class="type">class="kw">double askPrice = NormalizeDouble(SymbolInfoDouble(m_symbol, SYMBOL_ASK), Digits()); class="type">class="kw">double bidPrice = NormalizeDouble(SymbolInfoDouble(m_symbol, SYMBOL_BID), Digits()); if (m_rsiBuffer[class="num">1] < rsiOversold && m_rsiBuffer[class="num">2] > rsiOversold && m_rsiBuffer[class="num">0] < rsiOversold) { if (askPrice > m_envUpperBandBuffer[class="num">0]) { if (countActiveBaskets() < m_maxInitialPositions) { signalType = ORDER_TYPE_BUY; } } } else if (m_rsiBuffer[class="num">1] > rsiOverbought && m_rsiBuffer[class="num">2] < rsiOverbought && m_rsiBuffer[class="num">0] > rsiOverbought) { if (bidPrice < m_envLowerBandBuffer[class="num">0]) { if (countActiveBaskets() < m_maxInitialPositions) { signalType = ORDER_TYPE_SELL; } } } if (signalType != (ENUM_ORDER_TYPE)-class="num">1) { class=class="str">"cmt">//--- Create new basket with unique magic number class="type">int newMagic = m_baseMagicNumber + ArraySize(m_traders); if (newMagic < m_baseMagicNumber + m_maxInitialPositions) {
◍ 篮子实例的两种构造路径
这段逻辑展示了 MarketZoneTrader 的两种实例化方式:一是信号触发时新建篮子,二是把已有持仓加载进篮子管理。前者在 openInitialOrder 返回有效 ticket 且 activateTrade 通过后,把指针塞进 m_traders 动态数组;后者用 createNewBasket(long magic, ulong ticket) 直接绑定历史单。 新篮子创建时先 new 一个对象并传入手数方案、初始手数、风险百分比、止损点数、区域目标点数、区域大小点数和魔数。若 ticket>0 且激活成功,ArraySize 取原数组长度,ArrayResize 扩 1,再把指针挂到末尾并打印 Magic 与 Ticket;失败则 delete 防内存泄漏。 旧版用全局单实例 trader = new MarketZoneTrader(...) 在 OnInit 里初始化,参数里多了 maxOrders 和 restrictMaxOrders 两个开关。对比可见,新结构把单实例改成了指针数组,支持同时跑多个互不干扰的篮子,EA 重启后也能靠 createNewBasket 把场内外挂单接回来。 外汇与贵金属保证金交易杠杆高,多篮子并行会放大回撤,实盘前请在 MT5 策略测试器用 2023 年 XAUUSD H1 数据验证 activateTrade 的返回逻辑是否如预期。
MarketZoneTrader* newTrader = new MarketZoneTrader(lotOption, initialLotSize, riskPercentage, riskPoints, zoneTargetPoints, zoneSizePoints, newMagic); ticket = newTrader.openInitialOrder(signalType); class=class="str">"cmt">//--- Open INITIAL position if (ticket > class="num">0 && newTrader.activateTrade(ticket)) { class="type">int size = ArraySize(m_traders); ArrayResize(m_traders, size + class="num">1); m_traders[size] = newTrader; Print("New basket created: Magic=", newMagic, ", Ticket=", ticket, ", Type=", EnumToString(signalType)); } else { class="kw">delete newTrader; Print("Failed to create new basket: Ticket=", ticket); } } else { Print("Maximum initial positions(baskets) reached: ", m_maxInitialPositions); } } } class="kw">private: class="type">void createNewBasket(class="type">long magic, class="type">ulong ticket) { MarketZoneTrader* newTrader = new MarketZoneTrader(lotOption, initialLotSize, riskPercentage, riskPoints, zoneTargetPoints, zoneSizePoints, magic); if (newTrader.activateTrade(ticket)) { class="type">int size = ArraySize(m_traders); ArrayResize(m_traders, size + class="num">1); m_traders[size] = newTrader; Print("Existing position loaded into basket: Magic=", magic, ", Ticket=", ticket); } else { class="kw">delete newTrader; Print("Failed to load existing position into basket: Ticket=", ticket); } } class=class="str">"cmt">//--- PREVIOUS VERSION OF NEW CLASS INSTANCE 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=class="str">"cmt">//--- Modified MarketZoneTrader Class class MarketZoneTrader { class="kw">private: enum TradeState { INACTIVE, RUNNING, TERMINATING }; class="kw">struct TradeMetrics { class="type">bool operationSuccess; class="type">class="kw">double totalVolume; class="type">class="kw">double netProfitLoss; }; class="kw">struct ZoneBoundaries { class="type">class="kw">double zoneHigh; class="type">class="kw">double zoneLow; class="type">class="kw">double zoneTargetHigh; class="type">class="kw">double zoneTargetLow; }; class="kw">struct TradeConfig {
把区域交易状态塞进结构体
这套区域交易器的核心状态全收在两个结构体里:TradeConfig 管品种、开仓价、初始手数、魔术号、双向累计成交量以及虚拟 trailing stop 水位;ZoneBoundaries 其实是 TradeConfig 的内嵌块,专门存 profit span 与 recovery span 的点值换算结果。 LossTracker 只留了一个 tradeLossTracker 浮点字段,用来在每轮区域结算时累加浮动亏损,不碰平仓逻辑本身。 构造函数里有个细节值得在 MT5 里断点验证:zoneProfitSpan 和 zoneRecoverySpan 都乘了 _Point,意味着你传进来的 targetPts / sizePts 是以「点」为单位的整数或小数,实际价格跨度会被自动适配到当前品种的最小报价精度。 initialTradeLabel 拼成了 "EA_INITIAL_" + 魔术号字符串,恢复单另有 recoveryTradeLabel,两者在策略测试器的持仓注释里能直接区分,调参时别漏看这一行。 外汇与贵金属杠杆高,这类按区域加 recovery 仓的逻辑在单边行情里可能快速放大回撤,上线前先用历史数据跑一轮点值敏感性。
class="type">class="kw">string marketSymbol; class="type">class="kw">double openPrice; class="type">class="kw">double initialVolume; class="type">long tradeIdentifier; class="type">class="kw">string initialTradeLabel; class=class="str">"cmt">//--- Label for initial positions class="type">class="kw">string recoveryTradeLabel; class=class="str">"cmt">//--- Label for recovery positions class="type">ulong activeTickets[]; ENUM_ORDER_TYPE direction; class="type">class="kw">double zoneProfitSpan; class="type">class="kw">double zoneRecoverySpan; class="type">class="kw">double accumulatedBuyVolume; class="type">class="kw">double accumulatedSellVolume; TradeState currentState; class="type">bool hasRecoveryTrades; class=class="str">"cmt">//--- Flag to track recovery trades class="type">class="kw">double trailingStopLevel; class=class="str">"cmt">//--- Virtual trailing stop level }; class="kw">struct LossTracker { class="type">class="kw">double tradeLossTracker; }; TradeConfig m_tradeConfig; ZoneBoundaries m_zoneBounds; LossTracker m_lossTracker; class="type">class="kw">string m_lastError; class="type">int m_errorStatus; CTrade m_tradeExecutor; TradingLotSizeOptions m_lotOption; class="type">class="kw">double m_initialLotSize; class="type">class="kw">double m_riskPercentage; class="type">int m_riskPoints; class="type">class="kw">double m_zoneTargetPoints; class="type">class="kw">double m_zoneSizePoints; } class="kw">public: MarketZoneTrader(TradingLotSizeOptions lotOpt, class="type">class="kw">double initLot, class="type">class="kw">double riskPct, class="type">int riskPts, class="type">class="kw">double targetPts, class="type">class="kw">double sizePts, class="type">long magic) { m_tradeConfig.currentState = INACTIVE; ArrayResize(m_tradeConfig.activeTickets, class="num">0); m_tradeConfig.zoneProfitSpan = targetPts * _Point; m_tradeConfig.zoneRecoverySpan = sizePts * _Point; m_lossTracker.tradeLossTracker = class="num">0.0; m_lotOption = lotOpt; m_initialLotSize = initLot; m_riskPercentage = riskPct; m_riskPoints = riskPts; m_zoneTargetPoints = targetPts; m_zoneSizePoints = sizePts; m_tradeConfig.marketSymbol = _Symbol; m_tradeConfig.tradeIdentifier = magic; m_tradeConfig.initialTradeLabel = "EA_INITIAL_" + IntegerToString(magic); class=class="str">"cmt">//--- Label for initial positions