交易员生存技巧: 由指标制作的快餐·综合运用
(3/3)· 从指标句柄到 EA 移植,手把手把 12 年前的 MQL4 快餐喂给 MQL5 引擎
EA 初始化时的报价与小数位处理
在 MT5 写 EA 时,OnInit 里第一件事往往是把当前品种名塞进 CSymbolInfo 包装对象,并立刻调一次报价刷新。若 m_symbol.Name(Symbol()) 失败直接返回 INIT_FAILED,避免后续用到空品种。 RefreshRates 这个函数不是系统自带的必须写法,而是自己封的一层:先调 m_symbol.RefreshRates(),失败就 Print 报错并返回 false;再防一手 Ask 或 Bid 为 0 的情况,否则后面算止损止盈会拿到脏值。 外汇和贵金属点差跳动快,这类品种常见 3 位或 5 位小数报价(如 XAUUSD 常是 5 位)。代码里用 Digits() 判断,若是 3 或 5 就把调整系数设成 10,m_adjusted_point = Point()*10,后续挂单距离、TP 计算都该用这个调整后点值,否则在 5 位平台可能把 50 点误算成 5 点。 开 MT5 新建 EA 把下面片段贴进 OnInit 验证:切到 XAUUSD 的 5 位账户,Print(m_adjusted_point) 应看到原 Point 的 10 倍;切 EURUSD 4 位则维持原值。外汇保证金交易杠杆高,参数误用会放大滑点与穿仓风险。
CAccountInfo m_account; class=class="str">"cmt">// 账户信息包装 class=class="str">"cmt">//--- class="kw">input class="type">class="kw">double TakeProfit =class="num">50; class="kw">input class="type">class="kw">double MACDCloseLevel =class="num">2; class="kw">input class="type">int MATrendPeriod =class="num">26; class=class="str">"cmt">//--- class="type">class="kw">double m_adjusted_point; class=class="str">"cmt">// 用于小数位为 class="num">3 或 class="num">5 的调整值 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- if(!m_symbol.Name(Symbol())) class=class="str">"cmt">// 设置品名 class="kw">return(INIT_FAILED); RefreshRates(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 更新品种报价数据 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">bool RefreshRates(class="type">void) { class=class="str">"cmt">//--- 更新汇率 if(!m_symbol.RefreshRates()) { Print("RefreshRates 错误"); class="kw">return(class="kw">false); } class=class="str">"cmt">//--- 防止返回 "零" 值 if(m_symbol.Ask()==class="num">0 || m_symbol.Bid()==class="num">0) class="kw">return(class="kw">false); class=class="str">"cmt">//--- class="kw">return(true); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- if(!m_symbol.Name(Symbol())) class=class="str">"cmt">// 设置品名 class="kw">return(INIT_FAILED); RefreshRates(); class=class="str">"cmt">//--- 调整 class="num">3 或 class="num">5 位小数 class="type">int digits_adjust=class="num">1; if(m_symbol.Digits()==class="num">3 || m_symbol.Digits()==class="num">5) digits_adjust=class="num">10; m_adjusted_point=m_symbol.Point()*digits_adjust; class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); }
◍ MACD 与 EMA 共振下的开仓判定
这段逻辑先把 MACD 主线、信号线以及一条自定义周期的 EMA 在当前柱和前一柱的值全部抓出来,作为后续多空过滤的基础。参数固定为 12/26/9 的快慢线组合,价格源用收盘价,和 MT5 自带的 MACD 默认值一致,可直接在指标窗口核对。 当账户无持仓(total<1)时,多头触发条件是:MACD 主线在零轴下方,但已上穿信号线(当前 MacdCurrent>SignalCurrent 且前一柱 MacdPrevious<SignalPrevious),同时主线绝对值要超过 MACDOpenLevel 与调试点值的乘积,且 EMA 当前值大于前一值。 满足后直接用 m_trade.Buy 以 Ask 市价入场,止盈挂在该价位加上 TakeProfit 个调试点处,注释标为 "macd sample"。若 ResultDeal 不为 0 说明成交,否则打印错误码描述——开 MT5 把 EA 日志打开就能看到真实返回。 持仓遍历部分用 PositionsTotal()-1 倒序循环,SelectByIndex 按索引选中仓位再比对 Symbol,只对当前品种的多头判断平仓:当 MACD 主线回到零轴上方且下穿信号线、绝对值大于 MACDCloseLevel 阈值时,倾向平掉买入仓。外汇与贵金属杠杆高,这类信号只是概率倾向,实盘前务必在策略测试器跑历史数据验证阈值。
MacdCurrent=iMACD(NULL,class="num">0,class="num">12,class="num">26,class="num">9,PRICE_CLOSE,MAIN_LINE,class="num">0); MacdPrevious=iMACD(NULL,class="num">0,class="num">12,class="num">26,class="num">9,PRICE_CLOSE,MAIN_LINE,class="num">1); SignalCurrent=iMACD(NULL,class="num">0,class="num">12,class="num">26,class="num">9,PRICE_CLOSE,SIGNAL_LINE,class="num">0); SignalPrevious=iMACD(NULL,class="num">0,class="num">12,class="num">26,class="num">9,PRICE_CLOSE,SIGNAL_LINE,class="num">1); MaCurrent=iMA(NULL,class="num">0,MATrendPeriod,class="num">0,MODE_EMA,PRICE_CLOSE,class="num">0); MaPrevious=iMA(NULL,class="num">0,MATrendPeriod,class="num">0,MODE_EMA,PRICE_CLOSE,class="num">1); total=PositionsTotal(); if(total<class="num">1) { class=class="str">"cmt">//--- 检查多头 (买入) 的可能性 if(MacdCurrent<class="num">0 && MacdCurrent>SignalCurrent && MacdPrevious<SignalPrevious && MathAbs(MacdCurrent)>(MACDOpenLevel*m_adjusted_point) && MaCurrent>MaPrevious) { m_trade.Buy(Lots,m_symbol.Name(),m_symbol.Ask(), class="num">0.0, m_symbol.NormalizePrice(m_symbol.Ask()+TakeProfit*m_adjusted_point), "macd sample"); if(m_trade.ResultDeal()!=class="num">0) Print("买入仓位开仓 : ",m_trade.ResultPrice()); else Print("开买入仓位错误 : ",m_trade.ResultRetcodeDescription()); class="kw">return; } for(class="type">int i=PositionsTotal()-class="num">1;i>=class="num">0;i--) if(m_position.SelectByIndex(i)) class=class="str">"cmt">// 通过索引选择仓位以便进一步访问其属性 class=class="str">"cmt">//--- 正确入场非常重要, 但正确离场更重要... for(class="type">int i=PositionsTotal()-class="num">1;i>=class="num">0;i--) if(m_position.SelectByIndex(i)) class=class="str">"cmt">// 通过索引选择仓位以便进一步访问其属性 if(m_position.Symbol()==m_symbol.Name()) { class=class="str">"cmt">//--- 开多头仓位 if(m_position.PositionType()==POSITION_TYPE_BUY) { class=class="str">"cmt">//--- 应当平仓吗? if(MacdCurrent>class="num">0 && MacdCurrent<SignalCurrent && MacdPrevious>SignalPrevious && MacdCurrent>(MACDCloseLevel*m_adjusted_point))
「空单的离场与尾随停止逻辑」
| 当持仓类型为 POSITION_TYPE_SELL 时,系统先判断是否满足平仓条件:MACD 柱线位于零轴下方(MacdCurrent<0),且当前柱线高于信号线、前一根柱线低于前一根信号线,同时 | MacdCurrent | 大于 MACDCloseLevel 与微调点数的乘积。三者同时成立,才触发市价平仓。 |
|---|
平仓动作通过 m_trade.PositionClose(m_position.Ticket()) 执行,若返回失败则打印错误码描述并直接 return,不再继续本帧处理。这一道判断把“动量反转”和“阈值过滤”绑死,避免零轴附近杂讯频繁平错单。 若 TrailingStop 参数大于 0,则进入空单尾随逻辑:当开仓价与当前价差超过 TrailingStop 个微调点时,才允许把止损上移。外汇与贵金属杠杆高,尾随停止能在趋势延续时锁利,但滑点可能让修改失败,需自行在 MT5 策略测试器里验证不同 TrailingStop 值下的回撤表现。
{
class=class="str">"cmt">//--- 平仓并退出
if(!m_trade.PositionClose(m_position.Ticket()))
Print("PositionClose 错误 ",m_trade.ResultRetcodeDescription());
class="kw">return;
}
class=class="str">"cmt">//--- 检查尾随停止
if(TrailingStop>class="num">0)
{
if(m_position.PriceCurrent()-m_position.PriceOpen()>m_adjusted_point*TrailingStop)
{
if(m_position.StopLoss()<m_symbol.Bid()-m_adjusted_point*TrailingStop)
{
class=class="str">"cmt">//--- 修改仓位并退出
if(!m_trade.PositionModify(m_position.Ticket(),
m_symbol.NormalizePrice(m_position.PriceCurrent()-m_adjusted_point*TrailingStop),
m_position.TakeProfit()))
Print("PositionModify 错误 ",m_trade.ResultRetcodeDescription());
class="kw">return;
}
}
}
}
if(m_position.PositionType()==POSITION_TYPE_SELL)
{
class=class="str">"cmt">//--- 应当平仓吗?
if(MacdCurrent<class="num">0 && MacdCurrent>SignalCurrent &&
MacdPrevious<SignalPrevious && MathAbs(MacdCurrent)>(MACDCloseLevel*m_adjusted_point))
{
class=class="str">"cmt">//--- 平仓并退出
if(!m_trade.PositionClose(m_position.Ticket()))
Print("PositionClose 错误 ",m_trade.ResultRetcodeDescription());
class="kw">return;
}
class=class="str">"cmt">//--- 检查尾随停止
if(TrailingStop>class="num">0)
{空单 trailing stop 的回拉触发逻辑
这段代码片段只处理持仓为空头(Short)时的移动止损回拉:当开仓价高于当前价的浮动盈利超过 TrailingStop 与修正点值乘积时,才进入修改判断。 外层条件用了 m_position.PriceOpen() - m_position.PriceCurrent() > m_adjusted_point*TrailingStop,意味着浮盈必须以「点」为单位越过阈值,不是随便波动就改 SL。 内层先判断原止损要么等于 0(未设),要么比「卖价 Ask + TrailingStop 点距」更靠后(即旧止损太松),满足其一才调用 PositionModify 把止损钉到 Ask+TrailingStop 处,止盈不动。 修改若失败,Print 打出 ResultRetcodeDescription 并直接 return,避免后续重复提交。 测试机是一台 i3-3120M @2.50GHz、RAM 约 8G 的 Win10 老本,说明这类 EA 对硬件几乎零压力,本地跑回测足够。外汇与贵金属杠杆高,移动止损只降风险不保盈利,触发概率随点值参数敏感变化。
if((m_position.PriceOpen()-m_position.PriceCurrent())>(m_adjusted_point*TrailingStop)) { if((m_position.StopLoss()>(m_symbol.Ask()+m_adjusted_point*TrailingStop)) || (m_position.StopLoss()==class="num">0.0)) { class=class="str">"cmt">//--- 修改仓位并退出 if(!m_trade.PositionModify(m_position.Ticket(), m_symbol.NormalizePrice(m_symbol.Ask()+m_adjusted_point*TrailingStop), m_position.TakeProfit())) Print("PositionModify 错误 ",m_trade.ResultRetcodeDescription()); class="kw">return; } } Windows class="num">10 (build class="num">16299) x64, IE class="num">11, UAC, Intel Core i3-3120M @ class="num">2.50GHz, 内存: class="num">4217 / class="num">8077 Mb, 硬盘: class="num">335 / class="num">464 Gb, GMT+class="num">2
◍ MQL4 风格指标调用在均线 EA 里的实测代价
把旧版 Moving Average.mq4 迁到 MQL5,最省事的做法不是重写逻辑,而是让改过的 Moving Average.mq5 包含 IndicatorsMQL5.mqh,用 iMA 直接取数替代 CopyBuffer。检查动作只剩一件:把拿到的值和 0.0 比,等于零就视为没取到。 我们在 MetaQuotes-Demo 上跑了对拍:EURUSD M15,区间 2017.02.01–2018.01.16,终端每次测试后重置。原版 Moving Average.mq5 在「每次逐笔报价」模式下测试耗时 0:00:33.359、成交 2270;MQL4 风格版耗时 0:00:34.984、成交同样是 2270。时间差约 1.6 秒,倾向是内核在句柄间搜索带来的微小开销。 内存这边用 14 个图表压测,Terminal memory used.mq5 钉在首图每 10 秒打印 TERMINAL_MEMORY_USED。原版任务管理器占用 295.6 MB、终端自报 771 MB;MQL4 风格版 283.6 MB、自报 760 MB。两者几乎持平,几 MB 浮动更像是新闻更新这类终端后台噪声。 唯一指标时内核不花时间搜索蜡烛,这点与 MACD Sample 不同——后者要搜两个句柄才拖慢。外汇与贵金属品种波动剧烈、杠杆风险高,任何迁移都请在策略测试器里用自己的样本重跑一遍再上模拟盘。
class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <SimpleCall\IndicatorsMQL5.mqh> class="macro">#include <Trade\Trade.mqh> class=class="str">"cmt">//--- 获得当前移动平均线 class="type">class="kw">double ma[class="num">1]; if(CopyBuffer(ExtHandle,class="num">0,class="num">0,class="num">1,ma)!=class="num">1) { Print("CopyBuffer 自 iMA 失败, 无数据"); class="kw">return; } class=class="str">"cmt">//--- 得到移动均线 ma=iMA(NULL,class="num">0,MovingPeriod,MovingShift,MODE_SMA,PRICE_CLOSE,class="num">0); class=class="str">"cmt">//--- 获得当前移动平均线 class="type">class="kw">double ma[class="num">1]; if(CopyBuffer(ExtHandle,class="num">0,class="num">0,class="num">1,ma)!=class="num">1) { Print("CopyBuffer 自 iMA 失败, 无数据"); class="kw">return; } class=class="str">"cmt">//--- 获得当前移动平均线 class="type">class="kw">double ma[class="num">1]; ma[class="num">0]=iMA(_Symbol,_Period,MovingPeriod,MovingShift,MODE_SMA,PRICE_CLOSE,class="num">0); class=class="str">"cmt">//if(CopyBuffer(ExtHandle,class="num">0,class="num">0,class="num">1,ma)!=class="num">1) if(ma[class="num">0]==class="num">0.0) { class=class="str">"cmt">//Print("CopyBuffer 自 iMA 失败, 无数据"); Print("获取 MQL4 风格的 iMA 失败, 无数据"); class="kw">return; } Windows class="num">10 (build class="num">16299) x64, IE class="num">11, UAC, Intel Core i3-3120M @ class="num">2.50GHz, 内存: class="num">4217 / class="num">8077 Mb, 硬盘: class="num">335 / class="num">464 Gb, GMT+class="num">2
「用 MQL4 习惯读取 MT5 时间序列」
在 MT5 里用 MQL4 风格取指标值,最务实的做法是把常用时间序列访问函数重写一遍。Series.mqh 里集中实现了 Bars、iClose、iHigh、iLowest 等 11 个函数,让老 EA 代码几乎不用改就能在 MQL5 编译跑起来。 iHighest 和 iLowest 还兼容了 MODE_OPEN、MODE_LOW、MODE_HIGH、MODE_CLOSE、MODE_VOLUME、MODE_TIME 这几组预定义 ID,回测时切周期不用重写逻辑。 下面这段 iClose 是典型写法:用 CopyClose 按 shift 偏移取 1 根 K 线的收盘价,失败就打印错误码。注意它只复制 1 个元素到 val[1],所以 val[0] 就是目标值。 [CODE] //+------------------------------------------------------------------+
| // | iClose 函数的 MQL4 表述形式 |
|---|
//+------------------------------------------------------------------+ double iClose( string symbol, // 品种 ENUM_TIMEFRAMES timeframe, // 时间帧 int shift // 偏移 ) { double result=0.0; //--- double val[1]; ResetLastError(); int copied=CopyClose(symbol,timeframe,shift,1,val); if(copied>0) result=val[0]; else Print(__FUNCTION__,": CopyClose 错误=",GetLastError()); //--- return(result); } int CopyClose( string symbol_name, // 品名 ENUM_TIMEFRAMES timeframe, // 周期 int start_pos, // 初始位置 int count, // 复制数量 double close_array[] // 复制收盘价的数组 ); [/CODE] 中文逐行拆解:函数声明部分三个入参分别是品种名、周期枚举、偏移量,对应 MT4 里 iClose 的调用习惯。内部先建一个长度为 1 的 double 数组 val,ResetLastError 清掉之前的错误码避免误判。CopyClose 从 shift 位置取 1 根收盘价塞进 val,返回值 copied 大于 0 才把 val[0] 赋给 result,否则用 GetLastError 把具体错误打到日志。 外汇和贵金属杠杆高、滑点大,这类取数函数在实盘里可能因报价中断返回 0.0,调用方必须自行判断 result 是否异常,不能默认一定拿到有效价。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| iClose 函数的 MQL4 表述形式 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double iClose( class="type">class="kw">string symbol, class=class="str">"cmt">// 品种 ENUM_TIMEFRAMES timeframe, class=class="str">"cmt">// 时间帧 class="type">int shift class=class="str">"cmt">// 偏移 ) { class="type">class="kw">double result=class="num">0.0; class=class="str">"cmt">//--- class="type">class="kw">double val[class="num">1]; ResetLastError(); class="type">int copied=CopyClose(symbol,timeframe,shift,class="num">1,val); if(copied>class="num">0) result=val[class="num">0]; else Print(__FUNCTION__,": CopyClose 错误=",GetLastError()); class=class="str">"cmt">//--- class="kw">return(result); } class="type">int CopyClose( class="type">class="kw">string symbol_name, class=class="str">"cmt">// 品名 ENUM_TIMEFRAMES timeframe, class=class="str">"cmt">// 周期 class="type">int start_pos, class=class="str">"cmt">// 初始位置 class="type">int count, class=class="str">"cmt">// 复制数量 class="type">class="kw">double close_array[] class=class="str">"cmt">// 复制收盘价的数组 );
画得少,看得清
这套兼容层把 MQL4 风格的取值写法搬进 MT5,最直接的收益是代码行数压缩:原本要四五行的 iMA、iRSI 调用,现在一条字符串就能拿到值,可读性随冗余减少而上升。社区里有人实测,同机跑 iVolume 循环,MQL4 平稳如热熔黄油,MQL5 的 CopyBuffer 高频调用却像对系统发起 DDOS,会触发跳闸——多线程下反复拷缓冲区确实是性能暗坑。 代价也摆在那:指标返回错误能拦的不多,多指标并行取数会拖慢测试器,且连 IndicatorsMQL5.mqh 或 IndicatorsMQL4.mqh 后得手动高亮指标线。作者本人仍留着经典 MQL5 写法当主刀,兼容层只算备案。 外汇与贵金属杠杆高、滑点跳空频繁,任何取值简化都不改品种风险本质。你若常切 MT4/MT5 写 EA,不妨开 MT5 挂上那 25.33 KB 的 MQL5.zip 跑一节样例,亲眼看短写法在回测里的速度折损再决定要不要用。