MQL5中的自动化交易策略(第七部分):构建具备仓位动态调整功能的网格交易EA·进阶篇
🕸️

MQL5中的自动化交易策略(第七部分):构建具备仓位动态调整功能的网格交易EA·进阶篇

(2/3)·固定间隔下单只是起点,让仓位随波动与余额自适应才是风控分水岭

偏理论 第 2/3 篇
不少交易者把网格当成无脑挂单机器,间距写死、手数不变,一波单边就爆仓。动态仓位缩放不是可选项,而是网格策略在外汇贵金属高波动下的生存底线。

◍ 网格EA的全局变量与平仓模式定义

这段声明块决定了一个网格型EA的基础骨架:用 CTrade 实例统一下单接口,并用枚举把平仓逻辑拆成「按总盈利金额」和「按保本点差」两种模式。默认 closureMode 设为 CLOSE_BY_POINTS,意味着仓位达到保本偏移 50 点后才有可能触发离场,而不是死盯账户浮盈。 全局变量里几个数字直接决定风险形状:initialLotsize 0.1 手是首单仓位,takeProfitPts 200 点为首单止盈距,gridSize_Spacing 500 点控制网格加仓间隔,profitTotal_inCurrency 100 是另一种平仓模式下的金额阈值。外汇与贵金属杠杆高,网格在单边行情里会快速放大暴露,这些参数上 MT5 前建议先按品种波动率重算。 函数原型已经把执行流画清楚:IsNewBar 控节奏、ExecuteInitialTrade 开首单、ManageGridPositions 补网格、UpdateMovingAverage 供过滤、CalculateWeightedBreakevenPrice 算加权成本。CheckAndCloseProfitTargets 则按前面选的模式扫一遍持仓。 下方代码逐行拆完,你复制进 MetaEditor 就能看到变量树,下一步该填的是 OnTick 里怎么调这几个函数。

MQL5 / C++
class="macro">#include <Trade/Trade.mqh>              class=class="str">"cmt">//--- Include trading library
CTrade obj_Trade;                        class=class="str">"cmt">//--- Trading object instance
class=class="str">"cmt">//--- Closure Mode Enumeration and Inputs
enum ClosureMode {
   CLOSE_BY_PROFIT,       class=class="str">"cmt">//--- Use total profit(in currency) to close positions
   CLOSE_BY_POINTS        class=class="str">"cmt">//--- Use a points threshold from breakeven to close positions
};
input group "General EA Inputs"
input ClosureMode closureMode = CLOSE_BY_POINTS;     class=class="str">"cmt">//Select closure mode
class="type">class="kw">double breakevenPoints = class="num">50 * _Point;                class=class="str">"cmt">//--- Points offset to add/subtract to/from breakeven
class=class="str">"cmt">//--- Global Variables
class="type">class="kw">double TakeProfit;                                  class=class="str">"cmt">//--- Current take profit level
class="type">class="kw">double initialLotsize    = class="num">0.1;                     class=class="str">"cmt">//--- Initial lot size for the first trade
class="type">class="kw">double takeProfitPts     = class="num">200 * _Point;            class=class="str">"cmt">//--- Take profit distance in points
class="type">class="kw">double profitTotal_inCurrency = class="num">100;                class=class="str">"cmt">//--- Profit target(in currency) to close positions
class="type">class="kw">double gridSize;                                    class=class="str">"cmt">//--- Price level at which grid orders are triggered
class="type">class="kw">double gridSize_Spacing  = class="num">500 * _Point;            class=class="str">"cmt">//--- Grid spacing in points
class="type">class="kw">double LotSize;                                     class=class="str">"cmt">//--- Current lot size(increased with grid orders)
class="type">bool isTradeAllowed      = true;                    class=class="str">"cmt">//--- Flag to allow trade on a new bar
class="type">int totalBars            = class="num">0;                       class=class="str">"cmt">//--- Count of bars seen so far
class="type">int handle;                                         class=class="str">"cmt">//--- Handle for the Moving Average indicator
class="type">class="kw">double maData[];                                    class=class="str">"cmt">//--- Array for Moving Average data
class=class="str">"cmt">//--- Function Prototypes
class="type">void   CheckAndCloseProfitTargets();                class=class="str">"cmt">//--- Closes all positions if total profit meets target
class="type">void   ExecuteInitialTrade(class="type">class="kw">double ask, class="type">class="kw">double bid); class=class="str">"cmt">//--- Executes the initial BUY/SELL trade(initial positions)
class="type">void   ManageGridPositions(class="type">class="kw">double ask, class="type">class="kw">double bid); class=class="str">"cmt">//--- Adds grid orders when market moves to grid level(grid positions)
class="type">void   UpdateMovingAverage();                       class=class="str">"cmt">//--- Updates MA indicator data from its buffer
class="type">bool   IsNewBar();                                  class=class="str">"cmt">//--- Checks if a new bar has formed
class="type">class="kw">double CalculateWeightedBreakevenPrice();           class=class="str">"cmt">//--- Calculates the weighted average entry price for positions

均线句柄与K线切换的信号骨架

这段 EA 骨架把交易触发锁在「新 K 线」节拍上:OnTick 里先用 IsNewBar() 放开 isTradeAllowed,再刷均线数据,避免在单根 K 线内反复发信号造成过度交易。外汇与贵金属波动跳空频繁,这种按根判定的节奏能降低滑点踩雷概率。 初始化阶段用 iMA(_Symbol, _Period, 21, 0, MODE_SMA, PRICE_CLOSE) 建了 21 周期 SMA 句柄,并把 maData 数组设为时间序列(ArraySetAsSeries(maData, true))。句柄若返回 INVALID_HANDLE 直接 INIT_FAILED,实盘里这步能拦掉九成以上的指标加载失败事故。 UpdateMovingAverage() 只从缓冲区拷贝 3 根:CopyBuffer(handle, 0, 1, 3, maData) 取的是 1~3 号偏移的均线值,跳过 0 号当前未闭合 K 线。IsNewBar() 靠 iBars 计数对比 totalBars,新棒一到就置 true,读者可在 MT5 策略测试器里打印 bars 值验证切换点。 信号预备段还顺手归零了仓位:PositionsTotal()==0 时 LotSize 回退到 initialLotsize,并取了前两根 K 线的 iLow(1/2)、iHigh(1/2) 及归一化 ask。这些量是后续破位与保本平仓判断的原材料,参数调一处即可改策略脾气。

MQL5 / C++
class="type">void  CheckBreakevenClose(class="type">class="kw">double ask, class="type">class="kw">double bid); class=class="str">"cmt">//--- Closes positions if price meets breakeven+/- threshold
class="type">void  CloseAllPositions();                      class=class="str">"cmt">//--- Closes all open positions
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Expert initialization function
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit(){
   class=class="str">"cmt">//--- Initialize the Moving Average indicator(Period: class="num">21, SMA, Price: Close)
   handle = iMA(_Symbol, _Period, class="num">21, class="num">0, MODE_SMA, PRICE_CLOSE);
   if (handle == INVALID_HANDLE){
      Print("ERROR: UNABLE TO INITIALIZE THE INDICATOR. REVERTING NOW!");
      class="kw">return (INIT_FAILED);
   }
   ArraySetAsSeries(maData, true);              class=class="str">"cmt">//--- Ensure MA data array is in series order
   class="kw">return(INIT_SUCCEEDED);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//--- Expert tick function
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick(){

   class=class="str">"cmt">//--- Allow new trade signals on a new bar
   if(IsNewBar())
      isTradeAllowed = true;

   class=class="str">"cmt">//--- Update the Moving Average data
   UpdateMovingAverage();

}
class=class="str">"cmt">//+-------------------------------------------------------------------+
class=class="str">"cmt">//--- Function: UpdateMovingAverage
class=class="str">"cmt">//--- Description: Copies the latest data from the MA indicator buffer.
class=class="str">"cmt">//+-------------------------------------------------------------------+
class="type">void UpdateMovingAverage(){
   if(CopyBuffer(handle, class="num">0, class="num">1, class="num">3, maData) < class="num">0)
      Print("Error: Unable to update Moving Average data.");
}

class=class="str">"cmt">//+-------------------------------------------------------------------+
class=class="str">"cmt">//--- Function: IsNewBar
class=class="str">"cmt">//--- Description: Checks if a new bar has been formed.
class=class="str">"cmt">//+-------------------------------------------------------------------+
class="type">bool IsNewBar(){
   class="type">int bars = iBars(_Symbol, _Period);
   if(bars > totalBars){
      totalBars = bars;
      class="kw">return true;
   }
   class="kw">return false;
}
class=class="str">"cmt">//--- Reset lot size if no positions are open
if(PositionsTotal() == class="num">0)
   LotSize = initialLotsize;
class=class="str">"cmt">//--- Retrieve recent bar prices for trade signal logic
class="type">class="kw">double low1  = iLow(_Symbol, _Period, class="num">1);
class="type">class="kw">double low2  = iLow(_Symbol, _Period, class="num">2);
class="type">class="kw">double high1 = iHigh(_Symbol, _Period, class="num">1);
class="type">class="kw">double high2 = iHigh(_Symbol, _Period, class="num">2);
class=class="str">"cmt">//--- Get current Ask and Bid prices(normalized)
class="type">class="kw">double ask = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits);

「首单触发与网格补仓的判定骨架」

无持仓且交易开关放开时,EA 会先抓取实时买价并归一化到品种小数位,再调用首单函数。这里用 NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits) 拿到的 bid,精度直接对齐经纪商报价,避免后续挂单触发价出现多余尾数。 首单逻辑只看两根已完成 K 线相对于均线的穿越:前一根低点站上 MA、再前一根低点跌破 MA,视为偏多初始信号;反过来前一根高点跌破 MA、再前一根高点站上 MA,视为偏空。外汇与贵金属杠杆高,这种穿越信号在震荡市可能频繁假突破,实盘前建议在 MT5 策略测试器用至少 3 个月 tick 数据回测。 网格管理在持仓数大于 0 后才接管。初始单成交后 isTradeAllowed 置 false,防止同根 K 线重复开仓;之后价格走到 gridSize 设定的间距价位,ManageGridPositions 会按倍投手数补网格单。手数逐层翻倍意味着浮亏扩张速度非线性,黄金 1 手初始单若连补 4 层即到 8 手,回撤可能吞噬账户。 下面这段是首单与调度的核心代码片段,逐行拆开能直接拷进 EA 的 OnTick 验证。

MQL5 / C++
class="type">class="kw">double bid = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits);
class=class="str">"cmt">//--- If no positions are open and trading is allowed, check for an initial trade signal
if(PositionsTotal() == class="num">0 && isTradeAllowed){
   ExecuteInitialTrade(ask, bid);
}

class=class="str">"cmt">//+---------------------------------------------------------------------------+
class=class="str">"cmt">//--- Function: ExecuteInitialTrade
class=class="str">"cmt">//--- Description: Executes the initial BUY or SELL trade based on MA criteria.
class=class="str">"cmt">//---              (These are considered "initial positions.")
class=class="str">"cmt">//+---------------------------------------------------------------------------+
class="type">void ExecuteInitialTrade(class="type">class="kw">double ask, class="type">class="kw">double bid){
   class=class="str">"cmt">//--- BUY Signal: previous bar&class="macro">#x27;s low above MA and bar before that below MA
   if(iLow(_Symbol, _Period, class="num">1) > maData[class="num">1] && iLow(_Symbol, _Period, class="num">2) < maData[class="num">1]){
      gridSize = ask - gridSize_Spacing;      class=class="str">"cmt">//--- Set grid trigger below current ask
      TakeProfit = ask + takeProfitPts;       class=class="str">"cmt">//--- Set TP for BUY
      if(obj_Trade.Buy(LotSize, _Symbol, ask, class="num">0, TakeProfit,"Initial Buy"))
         Print("Initial BUY order executed at ", ask, " with LotSize: ", LotSize);
      else
         Print("Initial BUY order failed at ", ask);
      isTradeAllowed = false;
   }
   class=class="str">"cmt">//--- SELL Signal: previous bar&class="macro">#x27;s high below MA and bar before that above MA
   else if(iHigh(_Symbol, _Period, class="num">1) < maData[class="num">1] && iHigh(_Symbol, _Period, class="num">2) > maData[class="num">1]){
      gridSize = bid + gridSize_Spacing;      class=class="str">"cmt">//--- Set grid trigger above current bid
      TakeProfit = bid - takeProfitPts;       class=class="str">"cmt">//--- Set TP for SELL
      if(obj_Trade.Sell(LotSize, _Symbol, bid, class="num">0, TakeProfit,"Initial Sell"))
         Print("Initial SELL order executed at ", bid, " with LotSize: ", LotSize);
      else
         Print("Initial SELL order failed at ", bid);
      isTradeAllowed = false;
   }
}
class=class="str">"cmt">//--- If positions exist, manage grid orders
if(PositionsTotal() > class="num">0){
   ManageGridPositions(ask, bid);
}
class=class="str">"cmt">//+------------------------------------------------------------------------+
class=class="str">"cmt">//--- Function: ManageGridPositions
class=class="str">"cmt">//--- Description: When an initial position exists, grid orders are added 
class=class="str">"cmt">//---              if the market moves to the grid level. (These orders are 
class=class="str">"cmt">//---              considered "grid positions.") The lot size is doubled 
class=class="str">"cmt">//---              with each grid order.
class=class="str">"cmt">//+------------------------------------------------------------------------+
class="type">void ManageGridPositions(class="type">class="kw">double ask, class="type">class="kw">double bid){
   for(class="type">int i = PositionsTotal()-class="num">1; i >= class="num">0; i--){

◍ 网格加仓与整体利润平仓的实现骨架

这段逻辑跑在持仓遍历的循环里,先按 ticket 取单并选中,再读 POSITION_TYPE 判断多空方向。BUY 网格的触发条件是 ask 小于等于 gridSize,SELL 网格则是 bid 大于等于 gridSize,触发后手数直接翻倍,属于典型的马丁式网格加仓。 加仓成交后,BUY 侧的 gridSize 会减去 gridSize_Spacing 下移触发线,SELL 侧则加上间距上移,保证下一格在固定间隔后重新等待触发。外汇与贵金属杠杆高,这种翻倍加仓在单边行情里可能快速放大回撤,实盘前务必在 MT5 策略测试器用历史数据压一遍。 如果 closureMode 设为 CLOSE_BY_PROFIT,循环外会调用 CheckAndCloseProfitTargets。该函数要求持仓数大于 1 才生效,遍历所有持仓把 POSITION_PROFIT 累加进 totalProfit,达到预设目标再统一平仓,避免单格盈利过早离场。 把下面代码直接贴进 EA 的 OnTick 后半段,改一下 gridSize_Spacing 和 TakeProfit 就能在 demo 账户观察加仓节奏。

MQL5 / C++
class="type">ulong ticket = PositionGetTicket(i);
if(PositionSelectByTicket(ticket)){
   class="type">int positionType = (class="type">int)PositionGetInteger(POSITION_TYPE);
   class=class="str">"cmt">//--- Grid management for BUY positions
   if(positionType == POSITION_TYPE_BUY){
      if(ask <= gridSize){
         LotSize *= class="num">2;                 class=class="str">"cmt">//--- Increase lot size for grid order
         if(obj_Trade.Buy(LotSize, _Symbol, ask, class="num">0, TakeProfit,"Grid Position BUY"))
            Print("Grid BUY order executed at ", ask, " with LotSize: ", LotSize);
         else
            Print("Grid BUY order failed at ", ask);
         gridSize = ask - gridSize_Spacing; class=class="str">"cmt">//--- Update grid trigger
      }
   }
   class=class="str">"cmt">//--- Grid management for SELL positions
   else if(positionType == POSITION_TYPE_SELL){
      if(bid >= gridSize){
         LotSize *= class="num">2;                 class=class="str">"cmt">//--- Increase lot size for grid order
         if(obj_Trade.Sell(LotSize, _Symbol, bid, class="num">0, TakeProfit,"Grid Position SELL"))
            Print("Grid SELL order executed at ", bid, " with LotSize: ", LotSize);
         else
            Print("Grid SELL order failed at ", bid);
         gridSize = bid + gridSize_Spacing; class=class="str">"cmt">//--- Update grid trigger
      }
   }
}
class=class="str">"cmt">//--- Check if total profit meets the target(only used if closureMode == CLOSE_BY_PROFIT)
if(closureMode == CLOSE_BY_PROFIT)
   CheckAndCloseProfitTargets();
class=class="str">"cmt">//+----------------------------------------------------------------------------+
class=class="str">"cmt">//--- Function: CheckAndCloseProfitTargets
class=class="str">"cmt">//--- Description: Closes all positions if the combined profit meets or exceeds
class=class="str">"cmt">//---             the user-defined profit target.
class=class="str">"cmt">//+----------------------------------------------------------------------------+
class="type">void CheckAndCloseProfitTargets(){
   if(PositionsTotal() > class="num">1){
      class="type">class="kw">double totalProfit = class="num">0;
      for(class="type">int i = PositionsTotal()-class="num">1; i >= class="num">0; i--){
         class="type">ulong tkt = PositionGetTicket(i);
         if(PositionSelectByTicket(tkt))
            totalProfit += PositionGetDouble(POSITION_PROFIT);
交给小布盯盘看波动
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时波动率与余额曲线,帮你判断缩放阈值是否该重设。

常见问题

没有万能值,需结合品种波动率和交易时段;贵金属夜盘跳空大,间距过窄易密集成交,倾向用 ATR 倍数动态推算。
常见触发有账户余额比例、近期胜率、波动率突破;MQL5 中可用枚举切换模式,按预设风险参数重算手数。
目前小布提供波动与风险诊断,参数仍需你在 MetaEditor 内写逻辑;把重复劳动交给小布,你专注决策。
多点差与滑点未被建模是主因,外汇贵金属高风险,回测应加入真实点差和延迟,否则盈利概率被高估。