针对初学者以 MQL5 编写EA 交易的分步指南·进阶篇
(2/3)· 接上篇概念铺垫,本篇把移动平均线与 ADX 趋势过滤写进 EA 骨架,避开新手最常卡住的向导配置
◍ 用 MA 斜率和 ADX 过滤双向开仓信号
做多逻辑的第一道闸是 MA-8 的阶梯方向:柱0的 MA-8 大于柱1、柱1大于柱2,才算均线向上倾斜,这占买入四条件的一条。紧接着看柱1收盘价是否高于同周期 MA-8,两项同时满足才继续往后验,否则直接跳过剩余判断,省掉无谓计算。 ADX 当前值必须大于输入参数里的 Adx_Min(示例设 22.0),且 plusDI 需高于 minusDI,趋势强度与方向才够格。若 Buy_opened 已是 TRUE,说明持仓中,弹警示并 return,EA 等下一 tick;若为 FALSE,才用 MqlTradeRequest 填 mrequest 准备发单。 发单时 action 填 TRADE_ACTION_DEAL,买价取自 MqlTick 的 latest_price,止损 = 买价 - StopLoss 点、获利 = 买价 + TakeProfit 点,三个价格都过一遍 NormalizeDouble 对齐品种小数位。type_filling 用 ORDER_FILLING_FOK,没足够对手盘就整单不成交;OrderSend 后查 mresult 的返回码,10009 是请求成功、10008 是已挂单,命中任一即确认。 卖出是镜像版:MA-8 柱0<柱1<柱2 为向下倾斜,柱1收低于 MA-8,且 MinusDI 大于 plusDI,Sell_opened 为 FALSE 才发 ORDER_TYPE_SELL。卖价取自 latest_price 的卖价,止损获利反向加减,同样 NormalizeDouble。外汇与贵金属杠杆高,信号过滤只降概率不消风险,参数请先在 MT5 策略测试器跑验证。
class=class="str">"cmt">//--- 输入参数 class="kw">input class="type">int StopLoss=class="num">30; class=class="str">"cmt">// 止损 class="kw">input class="type">int TakeProfit=class="num">100; class=class="str">"cmt">// 获利 class="kw">input class="type">int ADX_Period=class="num">8; class=class="str">"cmt">// ADX 周期 class="kw">input class="type">int MA_Period=class="num">8; class=class="str">"cmt">// 移动平均周期 class="kw">input class="type">int EA_Magic=class="num">12345; class=class="str">"cmt">// EA 幻数 class="kw">input class="type">class="kw">double Adx_Min=class="num">22.0; class=class="str">"cmt">// ADX 最小值 class="kw">input class="type">class="kw">double Lot=class="num">0.1; class=class="str">"cmt">// 交易手数 class=class="str">"cmt">//--- 其他参数 class="type">int adxHandle; class=class="str">"cmt">// 我们 ADX 指标的句柄 class="type">int maHandle; class=class="str">"cmt">// 我们移动平均指标的句柄 class="type">class="kw">double plsDI[],minDI[],adxVal[]; class=class="str">"cmt">// 保存每个柱 +DI, -DI 和 ADX 数值的动态数组 class="type">class="kw">double maVal[]; class=class="str">"cmt">// 保存每个柱移动平均值的动态数组 class="type">class="kw">double p_close; class=class="str">"cmt">// 保存一个柱收盘价的变量 class="type">int STP, TKP; class=class="str">"cmt">// 将用于计算止损和获利值 class="type">int OnInit() { class=class="str">"cmt">//--- 得到ADX指标句柄 adxHandle=iADX(NULL,class="num">0,ADX_Period); class=class="str">"cmt">//--- 得到移动平均指标的句柄 maHandle=iMA(_Symbol,_Period,MA_Period,class="num">0,MODE_EMA,PRICE_CLOSE); class=class="str">"cmt">//--- 如果句柄返回了无效句柄 if(adxHandle<class="num">0 || maHandle<class="num">0) { Alert("创建指标句柄出错 - 错误: ",GetLastError(),"!!"); } class=class="str">"cmt">//--- 让我们用5位或3位小数的价格而不是4位小数的价格处理货币对 STP = StopLoss; TKP = TakeProfit;
「用新柱触发交易前的护栏与结构初始化」
EA 在 5 位或 3 位报价品种上会把止损止盈基准放大 10 倍:当 _Digits 等于 5 或 3 时,STP 与 TKP 各乘 10,避免点值换算错位。这一步必须在订单函数外先做完,否则后续发单的止损距离会偏小一个数量级。 OnTick 里第一道闸是柱数校验:Bars(_Symbol,_Period) 小于 60 就弹 Alert 并 return。少于 60 根柱意味着均线和 ADX 缓冲区不可靠,此时硬跑策略容易在样本不足时误触发。 新柱判定靠静态变量 Old_Time 与 CopyTime 取到的 New_Time[0] 比对。复制成功且时间不等,才把 IsNewBar 置 true 并更新 Old_Time;复制失败则报 GetLastError 并 ResetLastError 后退出。非新柱直接 return,保证策略逻辑每根柱只跑一次。 通过新柱检查后,代码初始化 MqlTick / MqlTradeRequest / MqlTradeResult / MqlRates 四类结构,并用 ZeroMemory 清 mrequest。随后把 mrate、plsDI 等数组用 ArraySetAsSeries(...,true) 设为时间序列排列,确保索引 0 对应最新柱。外汇与贵金属杠杆高,样本不足或数组序列错乱都可能放大实盘风险,建议先在 MT5 策略测试器用 60 根以上历史柱验证该段。
if(_Digits==class="num">5 || _Digits==class="num">3) { STP = STP*class="num">10; TKP = TKP*class="num">10; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| EA订单函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">// 我们是否有足够用于操作的柱 if(Bars(_Symbol,_Period)<class="num">60) class=class="str">"cmt">// 如果总柱数少于60 { Alert("我们只有不到60个柱, EA 将要退出!!"); class="kw">return; } class=class="str">"cmt">// 我们将会使用 Old_Time 静态变量来保存柱时间. class=class="str">"cmt">// 在每一次执行 OnTick 函数的时候我们都将检查当前柱的时间和保存的时间. class=class="str">"cmt">// 如果柱时间不等于保存的时间,说明我们有了一个新柱. class="kw">static class="type">class="kw">datetime Old_Time; class="type">class="kw">datetime New_Time[class="num">1]; class="type">bool IsNewBar=false; class=class="str">"cmt">// 把最新的柱时间复制到 New_Time[class="num">0] class="type">int copied=CopyTime(_Symbol,_Period,class="num">0,class="num">1,New_Time); if(copied>class="num">0) class=class="str">"cmt">// 数据已经被成功复制 { if(Old_Time!=New_Time[class="num">0]) class=class="str">"cmt">// 如果旧的时间不等于新柱的时间 { IsNewBar=true; class=class="str">"cmt">// 如果不是第一次调用,新柱已经出现 if(MQL5InfoInteger(MQL5_DEBUGGING)) Print("我们在此时有了新柱 ",New_Time[class="num">0]," 旧的时间是 ",Old_Time); Old_Time=New_Time[class="num">0]; } } else { Alert("复制历史时间数据出错, 错误 =",GetLastError()); ResetLastError(); class="kw">return; } class=class="str">"cmt">//--- EA 应该在有新柱的时候只检查新的交易 if(IsNewBar==false) { class="kw">return; } class=class="str">"cmt">//--- 我们是否有足够用于处理的柱 class="type">int Mybars=Bars(_Symbol,_Period); if(Mybars<class="num">60) class=class="str">"cmt">// 如果总柱数少于60 { Alert("我们只有不到60个柱, EA 将要退出!!"); class="kw">return; } class=class="str">"cmt">//--- 定义一些我们将用于交易的MQL5结构 class="type">MqlTick latest_price; class=class="str">"cmt">// 将用于取得最近/最新报价 class="type">MqlTradeRequest mrequest; class=class="str">"cmt">// 将用于发送我们的交易请求 class="type">MqlTradeResult mresult; class=class="str">"cmt">// 将用于取得我们的交易结果 class="type">MqlRates mrate[]; class=class="str">"cmt">// 将用于保存每个柱的价格,交易量和差价 ZeroMemory(mrequest); class=class="str">"cmt">// 初始化mrequest 结构 if(MQL5InfoInteger(MQL5_DEBUGGING)) Print("我们在此时有了新柱 ",New_Time[class="num">0]," 旧的时间是 ",Old_Time); class="type">int Mybars=Bars(_Symbol,_Period); class=class="str">"cmt">/* 让我们确认用于保存报价, ADX 值 和 MA 值的数组都以 时间序列数组相同的形式存放 */ class=class="str">"cmt">// 报价数组 ArraySetAsSeries(mrate,true); class=class="str">"cmt">// ADX DI+值数组 ArraySetAsSeries(plsDI,true); class=class="str">"cmt">// ADX DI-值数组
把指标缓冲区和持仓状态读进 EA
这段逻辑干的事很直接:先把 minDI、adxVal、maVal 三个数组用 ArraySetAsSeries(...,true) 设成时间序列,索引 0 就是当前值,往前推是历史。这样后面写 maVal[0]>maVal[1] 时,读到的就是「现在比上一根高」,不用自己倒腾下标。 报价和柱数据分别走 SymbolInfoTick 和 CopyRates。CopyRates(_Symbol,_Period,0,3,mrate) 一次性拉最近 3 根柱的 MqlRates 结构,mrate[1].close 就是前一根收盘价,后面拿来和 MA-8 比。任何一步返回小于 0 就 Alert 报错并 return,避免脏数据继续跑。 指标值用 CopyBuffer 按句柄搬:ADX 句柄的 0/1/2 号缓冲分别是 ADX 主线、+DI、-DI,各取 3 个值;MA 句柄 0 号缓冲是 MA-8 本身。只要其中任意一个复制失败,EA 会直接弹窗报错退出本次 tick。 持仓检查靠 PositionSelect(_Symbol) 配合 PositionGetInteger(POSITION_TYPE)。返回 POSITION_TYPE_BUY 就置 Buy_opened=true,POSITION_TYPE_SELL 则 Sell_opened=true,后面开仓模块靠这两个布尔量判断是否已占边。 买入触发条件拆成三个布尔:Buy_Condition_1 要求 MA-8 连续两根向上(maVal[0]>maVal[1] 且 maVal[1]>maVal[2]);Buy_Condition_2 要求前收 p_close 高于 MA-8(p_close>maVal[1]);Buy_Condition_3 要求当前 ADX 大于阈值 Adx_Min,代码里设的是 22。三者同时为真才倾向满足做多前置过滤。外汇和贵金属波动大,ADX>22 只代表趋势概率偏高,不代表一定延续。
ArraySetAsSeries(minDI,true); class=class="str">"cmt">// ADX 值数组 ArraySetAsSeries(adxVal,true); class=class="str">"cmt">// MA-class="num">8 值数组 ArraySetAsSeries(maVal,true); class=class="str">"cmt">//--- 使用MQL5的MqlTick结构取得最后报价 if(!SymbolInfoTick(_Symbol,latest_price)) { Alert("Error getting the latest price quote - error:",GetLastError(),"!!"); class="kw">return; } class=class="str">"cmt">//--- 取得最新三个柱的详细信息 if(CopyRates(_Symbol,_Period,class="num">0,class="num">3,mrate)<class="num">0) { Alert("复制报价/历史数据出错 - 错误:",GetLastError(),"!!"); class="kw">return; } class=class="str">"cmt">//--- 使用句柄把我们指标的新值复制到缓冲区(数组)中 if(CopyBuffer(adxHandle,class="num">0,class="num">0,class="num">3,adxVal)<class="num">0 || CopyBuffer(adxHandle,class="num">1,class="num">0,class="num">3,plsDI)<class="num">0 || CopyBuffer(adxHandle,class="num">2,class="num">0,class="num">3,minDI)<class="num">0) { Alert("复制 ADX 指标缓冲区出错 - 错误:",GetLastError(),"!!"); class="kw">return; } if(CopyBuffer(maHandle,class="num">0,class="num">0,class="num">3,maVal)<class="num">0) { Alert("复制移动平均指标缓冲区出错 - 错误:",GetLastError()); class="kw">return; } class=class="str">"cmt">//--- 我们没有遇到错误,所以继续 class=class="str">"cmt">//--- 我们是否已经有持仓? class="type">bool Buy_opened=false; class=class="str">"cmt">// 保存建买仓结果的变量 class="type">bool Sell_opened=false; class=class="str">"cmt">// 保存建卖仓结果的变量 if (PositionSelect(_Symbol) ==true) class=class="str">"cmt">// 我们还有未平仓头寸 { if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { Buy_opened = true; class=class="str">"cmt">//是买入 } else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL ) { Sell_opened = true; class=class="str">"cmt">// 是卖出 } } class="type">bool PositionSelect( class="type">class="kw">string symbol class=class="str">"cmt">// 交易品种名称 ); class="type">long PositionGetInteger( ENUM_POSITION_PROPERTY property_id class=class="str">"cmt">// 属性标识号 ); class=class="str">"cmt">// 复制当前柱的前一个柱的收盘价, 也就是柱 class="num">1 p_close=mrate[class="num">1].close; class=class="str">"cmt">// 柱1收盘价 class=class="str">"cmt">/* class="num">1. 检查买入设置 : MA-class="num">8 向上增加, 前一收盘价比它高, ADX > class="num">22, +DI > -DI */ class=class="str">"cmt">//--- 声明bool类型变量用于保存我们的买入条件 class="type">bool Buy_Condition_1 = (maVal[class="num">0]>maVal[class="num">1]) && (maVal[class="num">1]>maVal[class="num">2]); class=class="str">"cmt">// MA-class="num">8 向上增加 class="type">bool Buy_Condition_2 = (p_close > maVal[class="num">1]); class=class="str">"cmt">// 前一收盘价高于 MA-class="num">8 class="type">bool Buy_Condition_3 = (adxVal[class="num">0]>Adx_Min); class=class="str">"cmt">// 当前 ADX 值比最小值大 (class="num">22)
◍ 把四个条件拼成市价买入单
顺势开仓不是看一个指标就冲,这里把均线排列、价格站位、ADX 强度、DI 方向四个布尔量做了两级与判断:Buy_Condition_1 和 2 先过滤趋势结构,Buy_Condition_3 和 4 再确认动量有效。只有四者同真,才可能触发下单逻辑。 代码里最容易被忽略的是持仓去重:如果 Buy_opened 已为真,直接 Alert 并 return,避免同一信号在下一根 K 线重复发单。外汇与贵金属杠杆高,重复开仓会把风险敞口成倍放大,这一道闸门必须留着。 下单部分用 TRADE_ACTION_DEAL 走即时成交,滑点偏移设了 deviation=100 点;止损用 ask - STP*_Point、止盈用 ask + TKP*_Point,手数和幻数由外部变量 Lot、EA_Magic 控制。开 MT5 把 STP、TKP 调成你惯用的点数,就能直接验证这套请求结构是否过经纪商风控。
class="type">bool Buy_Condition_4 = (plsDI[class="num">0]>minDI[class="num">0]); class=class="str">"cmt">// +DI 大于 -DI class=class="str">"cmt">//--- 放到一起 if(Buy_Condition_1 && Buy_Condition_2) { if(Buy_Condition_3 && Buy_Condition_4) { class=class="str">"cmt">// 有未平买入仓位? if (Buy_opened) { Alert("我们已经有了买入仓位!!!"); class="kw">return; class=class="str">"cmt">// 不建新的买入仓位 } mrequest.action = TRADE_ACTION_DEAL; class=class="str">"cmt">// 立即执行订单 mrequest.price = NormalizeDouble(latest_price.ask,_Digits); class=class="str">"cmt">// 最新卖家报价 mrequest.sl = NormalizeDouble(latest_price.ask - STP*_Point,_Digits); class=class="str">"cmt">// 止损 mrequest.tp = NormalizeDouble(latest_price.ask + TKP*_Point,_Digits); class=class="str">"cmt">// 获利 mrequest.symbol = _Symbol; class=class="str">"cmt">// 货币对 mrequest.volume = Lot; class=class="str">"cmt">// 交易手数 mrequest.magic = EA_Magic; class=class="str">"cmt">// 订单幻数 mrequest.type = ORDER_TYPE_BUY; class=class="str">"cmt">// 买入订单 mrequest.type_filling = ORDER_FILLING_FOK; class=class="str">"cmt">// 订单执行类型 mrequest.deviation=class="num">100; class=class="str">"cmt">// 当前价格偏移 class=class="str">"cmt">//--- 发送订单 OrderSend(mrequest,mresult); class="type">bool Buy_Condition_1 = (maVal[class="num">0]>maVal[class="num">1]) && (maVal[class="num">1]>maVal[class="num">2]); class="type">bool Buy_Condition_2 = (p_close > maVal[class="num">1]); class="type">bool Buy_Condition_3 = (adxVal[class="num">0]>Adx_Min); class="type">bool Buy_Condition_4 = (plsDI[class="num">0]>minDI[class="num">0]); class=class="str">"cmt">// 有买入持仓吗? if (Buy_opened) { Alert("我们已经有了买入仓位!!!"); class="kw">return; class=class="str">"cmt">// 不建新的买入仓位 } class="type">bool OrderSend( class="type">MqlTradeRequest& request class=class="str">"cmt">// 查询结构 class="type">MqlTradeResult& result class=class="str">"cmt">// 回复结构 );
「卖出触发与下单回执的硬判断」
买入方向的下单回执要先看 retcode。MT5 里 10009 代表请求已完成、10008 代表订单已下,这两个值任一命中即可认为票已经进场,弹窗报出订单号即可;其余情况直接抓 GetLastError() 报错误码并 ResetLastError() 后 return,避免后续逻辑在脏状态里继续跑。 卖出侧的条件拆成四个 bool 再合判。MA-8 向下要求 maVal[0]<maVal[1] 且 maVal[1]<maVal[2];前一收盘低于该均线;ADX 当前值大于 22;-DI 大于 +DI。前两条满足才进内层,再卡 ADX 与 DI 方向。 内层先查 Sell_opened 标记,已有卖出仓就 Alert 并 return,不叠仓。新建市价卖单走 TRADE_ACTION_DEAL,price 取 NormalizeDouble(latest_price.bid,_Digits),sl 挂在 bid+STP*_Point、tp 挂在 bid-TKP*_Point,symbol 用 _Symbol、volume 用 Lot 变量。外汇与贵金属杠杆高,这类条件单在滑点行情中可能拒单或滑价,参数 STP/TKP/Lot 请在策略测试器按品种点值复核。
class=class="str">"cmt">// 取得返回代码 if(mresult.retcode==class="num">10009 || mresult.retcode==class="num">10008) class=class="str">"cmt">//请求完成或者已下订单 { Alert("买入订单已经成功下单, 订单#:",mresult.order,"!!"); } else { Alert("买入订单请求无法完成 -错误:",GetLastError()); ResetLastError(); class="kw">return; } class=class="str">"cmt">/* class="num">2. 检查卖出设定 : MA-class="num">8 向下降低, 前一收盘价低于它, ADX > class="num">22, -DI > +DI */ class=class="str">"cmt">//--- 声明bool类型变量用于保存我们的卖出条件 class="type">bool Sell_Condition_1 = (maVal[class="num">0]<maVal[class="num">1]) && (maVal[class="num">1]<maVal[class="num">2]); class=class="str">"cmt">// MA-class="num">8 向下降低 class="type">bool Sell_Condition_2 = (p_close <maVal[class="num">1]); class=class="str">"cmt">// 前一收盘价低于 MA-class="num">8 class="type">bool Sell_Condition_3 = (adxVal[class="num">0]>Adx_Min); class=class="str">"cmt">// 当前 ADX 值大于最小值 (class="num">22) class="type">bool Sell_Condition_4 = (plsDI[class="num">0]<minDI[class="num">0]); class=class="str">"cmt">// -DI 大于 +DI class=class="str">"cmt">//--- 放到一起 if(Sell_Condition_1 && Sell_Condition_2) { if(Sell_Condition_3 && Sell_Condition_4) { class=class="str">"cmt">// 还有未平卖出仓位? if (Sell_opened) { Alert("我们已经有了卖出仓位!!!"); class="kw">return; class=class="str">"cmt">// 不建新的卖出仓位 } mrequest.action = TRADE_ACTION_DEAL; class=class="str">"cmt">// 立即执行订单 mrequest.price = NormalizeDouble(latest_price.bid,_Digits); class=class="str">"cmt">// 最新买家报价 mrequest.sl = NormalizeDouble(latest_price.bid + STP*_Point,_Digits); class=class="str">"cmt">// 止损 mrequest.tp = NormalizeDouble(latest_price.bid - TKP*_Point,_Digits); class=class="str">"cmt">// 获利 mrequest.symbol = _Symbol; class=class="str">"cmt">// 货币对 mrequest.volume = Lot; class=class="str">"cmt">// 交易手数