移动平均线EA是MetaTrader 5客户端终端标准包中自带的一个示例,它利用移动平均线指标进行交易。
该EA文件“Moving Average.mq5”存放在“terminal_data_folder\MQL5\Experts\Examples\Moving Average\”目录下。这款EA展示了如何使用技术指标、交易历史功能和标准库的交易类。此外,EA还包含一个基于交易结果的资金管理系统。
接下来,我们来看看这款EA的结构及其工作原理。
1. EA属性
//+------------------------------------------------------------------+ //| 移动平均线.mq5 | //| 版权所有 2009-2013, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "版权所有 2009-2013, MetaQuotes Software Corp." #property link "https://www.mql5.com" #property version "1.00"
前五行是注释,接下来的三行使用预处理指令#property设置了MQL5程序的属性(版权、链接、版本)。
运行EA时,这些信息会显示在“常规”选项卡中:

图1. 移动平均线EA的常规参数
1.2. 包含文件
接下来,#include指令告诉编译器包含“Trade.mqh”文件。
该文件是标准库的一部分,包含了CTrade类,便于访问交易功能。
#include <Trade\Trade.mqh>包含文件的名称用尖括号“<>”显示,因此路径是相对于目录“terminal_data_folder\Include\”设置的。
1.3 输入参数
接下来是类型、名称、默认值和注释。其作用如图2所示。
input double MaximumRisk = 0.02; // 最大风险百分比 input double DecreaseFactor = 3; // 减少因子 input int MovingPeriod = 12; // 移动平均线周期 input int MovingShift = 6; // 移动平均线偏移
MaximumRisk和DecreaseFactor参数将用于资金管理,而MovingPeriod和MovingShift则设置将用于检查交易条件的移动平均线技术指标的周期和偏移量。
输入参数行中的注释文本和默认值会显示在“选项”选项卡中,而不是输入参数的名称:

图2. 移动平均线EA的输入参数
1.4. 全局变量
随后声明了全局变量ExtHandle,用于存储移动平均线指标的句柄。
//--- int ExtHandle=0;
接下来是6个函数,每个函数的目的在函数体前的注释中进行了说明:
- TradeSizeOptimized() - 计算最佳手数;
- CheckForOpen() - 检查开仓条件;
- CheckForClose() - 检查平仓条件;
- OnInit() - EA初始化函数;
- OnTick() - EA逐笔函数;
- OnDeinit() - EA去初始化函数;
最后三个函数是事件处理函数;前三个服务函数在它们的代码中被调用。
2. 事件处理函数
2.1. OnInit()初始化函数
OnInit()函数在EA首次启动时调用一次。通常在OnInit()事件处理器中,EA会为操作做好准备:检查输入参数、初始化指标和参数等。如果遇到严重错误,继续操作没有意义,则函数会以返回代码INIT_FAILED退出。
//+------------------------------------------------------------------+ //| EA初始化函数 | //+------------------------------------------------------------------+ int OnInit(void) { //--- ExtHandle=iMA(_Symbol,_Period,MovingPeriod,MovingShift,MODE_SMA,PRICE_CLOSE); if(ExtHandle==INVALID_HANDLE) { printf("创建MA指标时出错"); return(INIT_FAILED); } //--- return(INIT_SUCCEEDED); }
由于EA交易是基于移动平均线指标的,通过调用iMA()创建移动平均线指标,并将其句柄保存在全局变量ExtHandle中。
如果发生错误,OnInit()的返回代码为INIT_FAILED,这是在初始化失败时正确完成EA/指标操作的方法。
2.2. OnTick()函数
OnTick()函数在每次收到图表符号的新报价时调用。
//+------------------------------------------------------------------+ //| EA逐笔函数 | //+------------------------------------------------------------------+ void OnTick(void) { //--- if(PositionSelect(_Symbol)) CheckForClose(); else CheckForOpen(); //--- }
PositionSelect()函数用于确定当前符号是否有未平仓的持仓。
如果存在未平仓的持仓,则调用CheckForClose()函数,该函数分析市场当前状态并关闭持仓;如果没有,则调用CheckForOpen()函数,检查市场入场条件并在条件满足时开仓。
2.3. OnDeInit()去初始化函数
OnDeInit()在EA从图表中移除时调用。如果程序在运行期间放置了图形对象,则可以将其从图表中移除。
//+------------------------------------------------------------------+ //| EA去初始化函数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { } //+------------------------------------------------------------------+
在EA去初始化时没有执行任何操作。
3. 服务函数
3.1. TradeSizeOptimized()函数
该函数计算并返回指定风险水平和交易结果的最佳手数值。
//+------------------------------------------------------------------+ //| 计算最佳手数 | //+------------------------------------------------------------------+ double TradeSizeOptimized(void) { double price=0.0; double margin=0.0; //--- 计算手数 if(!SymbolInfoDouble(_Symbol,SYMBOL_ASK,price)) return(0.0); if(!OrderCalcMargin(ORDER_TYPE_BUY,_Symbol,1.0,price,margin)) return(0.0); if(margin<=0.0) return(0.0); double lot=NormalizeDouble(AccountInfoDouble(ACCOUNT_FREEMARGIN)*MaximumRisk/margin,2); //--- 计算连续亏损交易的系列长度 if(DecreaseFactor>0) { //--- 请求整个交易历史 HistorySelect(0,TimeCurrent()); //-- int orders=HistoryDealsTotal(); // 交易总数 int losses=0 // 连续亏损交易的数量 for(int i=orders-1;i>=0;i--) { ulong ticket=HistoryDealGetTicket(i); if(ticket==0) { Print("HistoryDealGetTicket失败,没有交易历史"); break; } //--- 检查交易符号 if(HistoryDealGetString(ticket,DEAL_SYMBOL)!=_Symbol) continue; //--- 检查利润 double profit=HistoryDealGetDouble(ticket,DEAL_PROFIT); if(profit>0.0) break; if(profit<0.0) losses++; } //--- if(losses>1) lot=NormalizeDouble(lot-lot*losses/DecreaseFactor,1); } //--- 规范化并检查交易量的允许值 double stepvol=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP); lot=stepvol*NormalizeDouble(lot/stepvol,0); double minvol=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN); if(lot<minvol) lot=minvol; double maxvol=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX); if(lot>maxvol) lot=maxvol; //--- 返回交易量的值 return(lot); }
SymbolInfoDouble()函数用于检查当前符号的价格是否可用,然后使用OrderCalcMargin()函数请求下单所需的保证金(在这种情况下是买单)。初始手数由下单所需的保证金、账户的可用保证金(AccountInfoDouble(ACCOUNT_FREEMARGIN))和输入参数MaximumRisk指定的最大风险值确定。
如果输入参数DecreaseFactor为正,则分析历史交易,并根据连续亏损交易的最大系列信息调整手数:初始手数乘以(1-损失/DecreaseFactor)。
然后交易量被“规范化”为当前符号的最小允许交易量(stepvol)的倍数。同时请求最小(minvol)和最大可能值(maxvol)的交易量,如果手数超出允许范围,则进行调整。因此,该函数返回计算出的交易量值。
3.2. CheckForOpen()函数
CheckForOpen()用于检查开仓条件,并在交易条件满足时开仓(在本例中为价格穿越移动平均线时)。
//+------------------------------------------------------------------+ //| 检查开仓条件 | //+------------------------------------------------------------------+ void CheckForOpen(void) { MqlRates rt[2]; //--- 复制价格值 if(CopyRates(_Symbol,_Period,0,2,rt)!=2) { Print("复制",_Symbol,"的CopyRates失败,没有历史"); return; } //--- 仅在新柱的第一个tick交易 if(rt[1].tick_volume>1) return; //--- 获取移动平均线指标的当前值 double ma[1]; if(CopyBuffer(ExtHandle,0,0,1,ma)!=1) { Print("CopyBuffer从iMA失败,没有数据"); return; } //--- 检查信号 ENUM_ORDER_TYPE signal=WRONG_VALUE; if(rt[0].open>ma[0] && rt[0].close<ma[0]) signal=ORDER_TYPE_SELL // 卖出条件 else { if(rt[0].open<ma[0] && rt[0].close>ma[0]) signal=ORDER_TYPE_BUY // 买入条件 } //--- 额外检查 if(signal!=WRONG_VALUE) if(TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) if(Bars(_Symbol,_Period)>100) { CTrade trade; trade.PositionOpen(_Symbol,signal,TradeSizeOptimized(), SymbolInfoDouble(_Symbol,signal==ORDER_TYPE_SELL ? SYMBOL_BID:SYMBOL_ASK), 0,0); } //--- }
在使用移动平均线进行交易时,需要检查价格是否穿越了移动平均线。通过使用CopyRates()函数,将当前价格的两个值复制到结构数组rt[]中,rt[1]对应当前柱,rt[0]对应已完成的柱。
通过检查当前柱的tick交易量是否为1,判断是否开始了新柱,需要注意的是,这种检测新柱的方法在某些情况下可能会失败(当报价成组到达时),因此新柱形成的事实应通过保存和比较当前报价的时间来完成(请参见IsNewBar)。
通过CopyBuffer()函数请求移动平均线指标的当前值,并保存在仅包含一个值的ma[]数组中。然后程序检查价格是否穿越了移动平均线,并进行额外检查(如果可以使用EA进行交易以及历史中是否有柱)。如果成功,则通过调用交易对象(CTrade的实例)的PositionOpen()方法,为该符号开仓。
开仓价格是通过调用SymbolInfoDouble()函数获取的,该函数根据信号变量的值返回买入或卖出价格。持仓量通过调用上述的TradeSizeOptimized()确定。
3.3. CheckForClose()函数
CheckForClose()检查平仓条件,并在满足条件时平仓。
//+------------------------------------------------------------------+ //| 检查平仓条件 | //+------------------------------------------------------------------+ void CheckForClose(void) { MqlRates rt[2]; //--- 复制价格值 if(CopyRates(_Symbol,_Period,0,2,rt)!=2) { Print("复制",_Symbol,"的CopyRates失败,没有历史"); return; } //--- 仅在新柱的第一个tick交易 if(rt[1].tick_volume>1) return; //--- 获取移动平均线指标的当前值 double ma[1]; if(CopyBuffer(ExtHandle,0,0,1,ma)!=1) { Print("CopyBuffer从iMA失败,没有数据"); return; } //--- 获取先前使用PositionSelect()选择的持仓类型 bool signal=false; long type=PositionGetInteger(POSITION_TYPE); if(type==(long)POSITION_TYPE_BUY && rt[0].open>ma[0] && rt[0].close<ma[0]) signal=true; if(type==(long)POSITION_TYPE_SELL && rt[0].open<ma[0] && rt[0].close>ma[0]) signal=true; //--- 额外检查 if(signal) if(TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) if(Bars(_Symbol,_Period)>100) { CTrade trade; trade.PositionClose(_Symbol,3); } //--- }
CheckForClose()函数的算法与CheckForOpen()函数的算法相似。根据当前未平仓持仓的方向,检查其平仓条件(价格向下穿越买入或向上穿越卖出)。通过调用交易对象(CTrade的实例)的PositionClose()方法来平仓。
4. 回测
可以通过策略测试器来寻找最佳参数值。
例如,在2012年01月01日至2013年08月01日的时间范围内优化移动平均线周期参数时,最佳结果为MovingPeriod=45:

移动平均线EA的回测结果
结论:
包含在MetaTrader 5终端标准包中的移动平均线EA是使用技术指标、交易历史功能和标准库的交易类的示例。此外,该EA还包括一个基于交易结果的资金管理系统。