MQL5 简介:如何编写简单的EA 交易和自定义指标·进阶篇
(2/3)· 接基础概念,本篇手把手把向导生成的空壳填成能加载的自动交易与图表指标
用 ATR 偏移画通道的算价循环
这段自定义指标的核心在 OnCalculate:先通过 CopyBuffer 把 ATR 指标的 0 号缓冲前 2 根取到本地 atr 数组,并用 ArraySetAsSeries(atr,true) 把序列倒序,使 atr[1] 代表上一根 K 线的 ATR 值。 循环从 prev_calculated 起步,只对新增柱线算量。day_t 由 time[i] 除以 PeriodSeconds(ATRtimeframe) 得到,本质是按所选周期(如日线)给每根报价柱打“第几个周期”的整型标签。 当 day_n 小于 day_t,说明跨入了新周期,重置 h_day / l_day 为当前柱高低;否则就在原周期内持续追踪最高与最低。bu[i] 和 bd[i] 分别等于周期内最低价加 atr[1]、最高价减 atr[1],形成动态通道。 外汇与贵金属波动受杠杆放大,通道仅描述历史波动边界,实盘突破倾向不代表趋势确认,需结合其他信号。开 MT5 把 ATRtimeframe 设成 PERIOD_D1、ATRper 设 14,即可看日级通道对外汇品种的反应。
SetIndexBuffer(class="num">1,bd,INDICATOR_DATA); hATR=iATR(NULL,ATRtimeframe,ATRper); } class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">class="kw">datetime& time[], const class="type">class="kw">double& open[], const class="type">class="kw">double& high[], const class="type">class="kw">double& low[], const class="type">class="kw">double& close[], const class="type">long& tick_volume[], const class="type">long& volume[], const class="type">int& spread[]) { class="type">int i,day_n,day_t; class="type">class="kw">double atr[],h_day,l_day; CopyBuffer(hATR,class="num">0,class="num">0,class="num">2,atr); ArraySetAsSeries(atr,true); for(i=prev_calculated;i<rates_total;i++) { day_t=time[i]/PeriodSeconds(ATRtimeframe); if(day_n<day_t) { day_n=day_t; h_day=high[i]; l_day=low[i]; } else { if(high[i]>h_day) h_day=high[i]; if(low[i]<l_day) l_day=low[i]; } bu[i]=l_day+atr[class="num">1]; bd[i]=h_day-atr[class="num">1]; } class="kw">return(rates_total); } class="type">void OnDeinit(const class="type">int reason) { IndicatorRelease(hATR); } day_t=time[i]/PeriodSeconds(ATRtimeframe);
「把交易逻辑落进 EA 的骨头里」
这一节直接动手写只跑单一品种的 EA。多 EA 同品种叠加要先算清总持仓冲击,不在本篇范围内,所以默认你只挂这一个实例。 输入参数先定死几个能现场调的:StartHour 和 EndHour 框住挂单时段;MAper 给简单移动平均的周期,专门用来做追踪止损的基准;Lots 就是每单手数。 OnInit 里用 iMA 抓 MA 句柄、用 iCustom 抓上节写的自定义指标——iCustom 第三个参数填文件名(不含扩展名),路径相对 MQL5\Indicators\。 OnTick 是报价驱动的入口。先开 t[]、h[]、l[] 三个数组收当日每根 K 的时间、最高、最低,复制量用 i=(dt.hour+1)*60 估算,CopyTime/CopyHigh/CopyLow 任一个返回 -1 就 Print('Can't copy timeseries!') 并 return。数组一律 ArraySetAsSeries 设成时序索引,从新到旧。 挂单价这样算:买停损在当日最高 lev_h 上加 Spread+_Point;卖停损从当日最低 lev_l 减一个 _Point。自定义指标上轨 atr_h[] 和下轨 atr_l[] 用 CopyBuffer 取,atr_l[0] 要补回点差才是图表卖价可用的获利线。 持仓修改看 PositionsTotal 循环:买单止损取 max(MA值, lev_l),卖单对称;新值必须离开当前买/卖价至少 STOP_LEVEL*_Point,否则 OrderSend 会被拒。挂单这边用 OrdersTotal 扫,买停损若超 atr_h[] 或不在时段内直接删,价格不够高就 TRADE_ACTION_MODIFY 推上去。 没单的时候在时段内补 TRADE_ACTION_PENDING:bord/sord 标志防重挂,价格卡在 atr_l[]~atr_h[] 区间内才放单。OnDeinit 里 IndicatorRelease 释放指标资源,编译过就能拖图表上跑。外汇和贵金属杠杆高,挂单被滑点拒单是常态,上 MT5 先用策略测试器跑一周再上实盘。
class="macro">#class="kw">property copyright "class="num">2010, MetaQuotes Software Corp." class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property description "这个EA交易程序在每天的StartHour和EndHour" class="macro">#class="kw">property description "之间挂单.每个订单的止损价格是价格区间的
◍ 日内高低与均线夹击的开仓触发
这段逻辑把交易窗口锁在每天 7 点到 19 点(StartHour=7、EndHour=19),用 240 周期 SMA(MAper=240)和自定义指标 indicator_TP 共同决定挂单位置。订单成交后,获利单的止损会被推到 SMA 数值上,止盈则取 indicator_TP 算出的水平——只针对已盈利持仓,亏损单不动。 代码先在 OnInit 里拿到两个句柄:hMA 是收盘价 SMA,hCI 是外部 TP 指标。OnTick 开头先拉时间、高低、均线三类序列,若 CopyTime/CopyHigh/CopyLow 任意一项返回条数不足就直接 Print 报错退出,避免拿半截数据瞎算。 日内极值不是只看最新一根 K。循环从 i=1 扫到当天零点边界,只要还在同一自然日(用 MathFloor(t[i]/86400) 判日),就持续刷新 lev_h 和 lev_l。最后 lev_h 加 Spread 和点差缓冲,lev_l 减一个点,作为突破挂单的触发线。外汇与贵金属杠杆高,这类突破单在消息时段可能滑点放大,实盘前先用策略测试器跑一轮。 别把 240 周期 SMA 当万能轴线 不同品种波动结构差很多,XAUUSD 在 240 SMA 上的回撤深度常比 EURUSD 深 30%~50%,直接套同参数可能频繁被扫。开 MT5 把 MAper 调到 120 对比一下成交密度再说。
class="macro">#class="kw">property description "相反方向的值."当订单被执行后, 获利值" class="macro">#class="kw">property description "被设定为 &class="macro">#x27;indicator_TP&class="macro">#x27; 计算的水平. 止损移动到" class="macro">#class="kw">property description "SMA 的值,只针对获利订单." input class="type">int StartHour = class="num">7; input class="type">int EndHour = class="num">19; input class="type">int MAper = class="num">240; input class="type">class="kw">double Lots = class="num">0.1; class="type">int hMA,hCI; class="type">void OnInit() { hMA=iMA(NULL,class="num">0,MAper,class="num">0,MODE_SMA,PRICE_CLOSE); hCI=iCustom(NULL,class="num">0,"indicator_TP"); } class="type">void OnTick() { class="type">MqlTradeRequest request; class="type">MqlTradeResult result; class="type">MqlDateTime dt; class="type">bool bord=false, sord=false; class="type">int i; class="type">ulong ticket; class="type">class="kw">datetime t[]; class="type">class="kw">double h[], l[], ma[], atr_h[], atr_l[], lev_h, lev_l, StopLoss, StopLevel=_Point*SymbolInfoInteger(Symbol(),SYMBOL_TRADE_STOPS_LEVEL), Spread =NormalizeDouble(SymbolInfoDouble(Symbol(),SYMBOL_ASK) - SymbolInfoDouble(Symbol(),SYMBOL_BID),_Digits); request.symbol =Symbol(); request.volume =Lots; request.tp =class="num">0; request.deviation =class="num">0; request.type_filling=ORDER_FILLING_FOK; TimeCurrent(dt); i=(dt.hour+class="num">1)*class="num">60; if(CopyTime(Symbol(),class="num">0,class="num">0,i,t)<i || CopyHigh(Symbol(),class="num">0,class="num">0,i,h)<i || CopyLow(Symbol(),class="num">0,class="num">0,i,l)<i) { Print("Can&class="macro">#x27;t copy timeseries!"); class="kw">return; } ArraySetAsSeries(t,true); ArraySetAsSeries(h,true); ArraySetAsSeries(l,true); lev_h=h[class="num">0]; lev_l=l[class="num">0]; for(i=class="num">1;i<ArraySize(t) && MathFloor(t[i]/class="num">86400)==MathFloor(t[class="num">0]/class="num">86400);i++) { if(h[i]>lev_h) lev_h=h[i]; if(l[i]<lev_l) lev_l=l[i]; } lev_h+=Spread+_Point; lev_l-=_Point; if(CopyBuffer(hMA,class="num">0,class="num">0,class="num">2,ma)<class="num">2 || CopyBuffer(hCI,class="num">0,class="num">0,class="num">1,atr_h)<class="num">1 || CopyBuffer(hCI,class="num">1,class="num">0,class="num">1,atr_l)<class="num">1) { Print("Can&class="macro">#x27;t copy indicator buffer!"); class="kw">return; } ArraySetAsSeries(ma,true);
遍历持仓改止损止盈的实战写法
这段逻辑跑在 EA 的 OnTick 里,先把点差加进 atr_l[0],再用 for 循环扫一遍 PositionsTotal() 拿到的全部持仓。只挑 Symbol() 与当前图表品种一致的订单动手,避免误改其他品种的仓。 买入仓的处理核心是:若前一根均线 ma[1] 高于开仓价,止损锚在 ma[1],否则退回 lev_l 轨道值。紧接着一层复合判断——原 SL 为 0 或新止损更高、原 TP 为 0 或新止盈更低,且新 SL/TP 距实时 BID 都超过 StopLevel 止损距离,才发 TRADE_ACTION_SLTP 请求。 卖出仓对称处理:ma[1]+Spread 低于开仓价时止损取该值,否则用 lev_h。外汇与贵金属杠杆高、滑点跳空频繁,这种按轨修改法在剧烈行情中可能来不及成交,开 MT5 用策略测试器跑 EURUSD 15 分钟周期能直接看回测里改单失败的次数。 别把止损距离当摆设 StopLevel 是经纪商硬限制,NormalizeDouble 算出来的新 SL 若贴着 BID 减 StopLevel 还是小于等于 0,OrderSend 必被拒。写 EA 时把 StopLevel 打印到日志,比事后猜改单为啥没生效管用。
atr_l[class="num">0]+=Spread; class=class="str">"cmt">// 在这个循环中,我们检查所有的持仓 for(i=class="num">0;i<PositionsTotal();i++) { class=class="str">"cmt">// 只处理我们自己的交易品种的订单 if(Symbol()==PositionGetSymbol(i)) { class=class="str">"cmt">// 我们将修改止损和获利值 request.action=TRADE_ACTION_SLTP; class=class="str">"cmt">// 处理买入订单 if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY) { class=class="str">"cmt">// 让我们确定止损 if(ma[class="num">1]>PositionGetDouble(POSITION_PRICE_OPEN)) StopLoss=ma[class="num">1]; else StopLoss=lev_l; class=class="str">"cmt">// 如果止损没有被定义或者比所需低 if((PositionGetDouble(POSITION_SL)==class="num">0 || NormalizeDouble(StopLoss-PositionGetDouble(POSITION_SL),_Digits)>class="num">0 class=class="str">"cmt">// 如果获利没有被定义或者比所需高 || PositionGetDouble(POSITION_TP)==class="num">0 || NormalizeDouble(PositionGetDouble(POSITION_TP)-atr_h[class="num">0],_Digits)>class="num">0) class=class="str">"cmt">// 新的止损接近当前价格吗? && NormalizeDouble(SymbolInfoDouble(Symbol(),SYMBOL_BID)-StopLoss-StopLevel,_Digits)>class="num">0 class=class="str">"cmt">// 新的获利接近当前价格吗? && NormalizeDouble(atr_h[class="num">0]-SymbolInfoDouble(Symbol(),SYMBOL_BID)-StopLevel,_Digits)>class="num">0) { class=class="str">"cmt">// 设置结构中止损的新值 request.sl=NormalizeDouble(StopLoss,_Digits); class=class="str">"cmt">// 设置结构中获利的新值 request.tp=NormalizeDouble(atr_h[class="num">0],_Digits); class=class="str">"cmt">// 发请求到交易服务器 OrderSend(request,result); } } class=class="str">"cmt">// 处理卖出订单 if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL) { class=class="str">"cmt">// 让我们确定止损的值 if(ma[class="num">1]+Spread<PositionGetDouble(POSITION_PRICE_OPEN)) StopLoss=ma[class="num">1]+Spread; else StopLoss=lev_h; class=class="str">"cmt">// 如果止损没有被定义或者比所需低
「挂单与持仓的止损移动逻辑」
这段逻辑跑在 EA 的 OnTick 里,先扫持仓再扫挂单。对已有仓位,只有当 SL 未设、或当前 SL 距算出的 StopLoss 还有偏差,且 TP 缺失或偏离 atr_l[0] 时,才重新挂单改 SL/TP;同时用 NormalizeDouble 比对 SYMBOL_ASK 与 StopLevel,确保新价不在禁止修改距离内。 持仓修改的核心判断可浓缩成下面这段:SL 或 TP 任一不满足,且新止损距卖价大于 StopLevel、新止盈距卖价也大于 StopLevel,才发 OrderSend。外汇与贵金属点差跳动快,StopLevel 常由经纪商动态给出,实盘里这段若不打日志很容易静默失败。 扫挂单时只处理当前品种。对 BUY_STOP,限定在 StartHour 到 EndHour 交易时段内,且 lev_h < atr_h[0] 才允许移动;开仓价偏低或 SL 未对齐 lev_l 时触发修正。把时间窗和 ATR 阈值写死前,建议先用历史数据看一眼自己品种的波动分布,黄金亚盘常出现 lev_h 假突破。 下面这段是持仓分支的判定与发单代码,逐行看就是条件嵌套加 NormalizeDouble 防浮点误差:
if((PositionGetDouble(POSITION_SL)==class="num">0 || NormalizeDouble(PositionGetDouble(POSITION_SL)-StopLoss,_Digits)>class="num">0 class=class="str">"cmt">// 如果获利没有被定义或者比所需低 || PositionGetDouble(POSITION_TP)==class="num">0 || NormalizeDouble(atr_l[class="num">0]-PositionGetDouble(POSITION_TP),_Digits)>class="num">0) class=class="str">"cmt">// 新的止损接近当前价格吗? && NormalizeDouble(StopLoss-SymbolInfoDouble(Symbol(),SYMBOL_ASK)-StopLevel,_Digits)>class="num">0 class=class="str">"cmt">// 新的获利接近当前价格吗? && NormalizeDouble(SymbolInfoDouble(Symbol(),SYMBOL_ASK)-atr_l[class="num">0]-StopLevel,_Digits)>class="num">0) { class=class="str">"cmt">// 设置结构中止损的新值 request.sl=NormalizeDouble(StopLoss,_Digits); class=class="str">"cmt">// 设置结构中获利的新值 request.tp=NormalizeDouble(atr_l[class="num">0],_Digits); class=class="str">"cmt">// 发请求到交易服务器 OrderSend(request,result); } } class=class="str">"cmt">// 如果有持仓,从这里退出... class="kw">return; } } class=class="str">"cmt">// 在这个循环中我们将检查所有的挂单 for(i=class="num">0;i<OrdersTotal();i++) { class=class="str">"cmt">// 选择每一个订单,取得订单号 ticket=OrderGetTicket(i); class=class="str">"cmt">// 只处理我们交易品种的订单 if(OrderGetString(ORDER_SYMBOL)==Symbol()) { class=class="str">"cmt">// 处理止损买单 if(OrderGetInteger(ORDER_TYPE)==ORDER_TYPE_BUY_STOP) { class=class="str">"cmt">// 检查是否是交易时间,价格能否移动 if(dt.hour>=StartHour && dt.hour<EndHour && lev_h<atr_h[class="num">0]) { class=class="str">"cmt">// 如果开仓价位比所需低 if((NormalizeDouble(lev_h-OrderGetDouble(ORDER_PRICE_OPEN),_Digits)>class="num">0 class=class="str">"cmt">// 如果止损价位没有定义或者比所需高 || OrderGetDouble(ORDER_SL)==class="num">0 || NormalizeDouble(OrderGetDouble(ORDER_SL)-lev_l,_Digits)!=class="num">0)