MQL5中的自动化交易策略(第七部分):构建具备仓位动态调整功能的网格交易EA·进阶篇
(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 里怎么调这几个函数。
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。这些量是后续破位与保本平仓判断的原材料,参数调一处即可改策略脾气。
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 验证。
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 账户观察加仓节奏。
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);