在文章“趋势有多长?”中所进行的分析表明价格在 60% 的时间内维持趋势。 这意味着在趋势伊始即开仓会产生最佳结果。 搜索趋势逆转点会产生大量的逆转形态。 双顶/双底是最著名和最常用的形态之一。
在价格图表上可以频繁返现双顶/双底形态。 它的形成与交易价位理论密切相关。 当价格达到支撑位或阻力位时(取决于之前的走势),该形态在趋势结束时形成。 在重复测试价位过程中进行调整之后,它会再次回滚而非突破。
在这一点位上,逆势交易者开始从该价位回滚时交易,并将价格推向调整。 在调整走势获得动量的同时,顺势交易者开始获利了结离场,或将亏损持仓平仓来避免突破该价位。 这进一步加强了走势,导致了新趋势的出现。
在图表上搜索形态时,不必搜索顶部/底部的精准匹配。 顶部/底部价位的偏差被认为是正常的。 只需确保峰值在相同的支撑/阻力位之内。 形态可靠性取决于它所基于的级别强度。
形态的普及引发了多种涉及它的策略。 在互联网上,交易这种形态至少有三个不同的入场点。
第一个入场点是基于颈线的突破。 止损设置在顶/底线之外。 有不同的方法来定义“颈线突破”。 交易者可以使用在颈线下方收盘的柱线,以及突破颈线一段固定距离的柱线。 两种方法都有其优点和缺点。 在凌厉走势的情况下,蜡烛可以在距颈线足够的距离处收盘,令形态效率很低。
这种方法的缺点是止损价位相对较高,这降低了策略的利润/风险比。
第二个入场点基于镜面等级理论,当颈线从支撑变成阻力时,反之亦然。 此处的入场点是在价格突破后回到颈线时进行的。 在这种情况下,止损设置超出最后一次调整的极值,从而显著降低止损价位。 不幸的是,价格并不总是在突破后回测颈线,从而减少了入场次数。
第三个入场点基于趋势理论。 它的定义是突破从走势起点到颈线极值的趋势线。 与第一种情况一样,止损设置在顶/底线之外。 与第一个入场点相比,早期入场提供较低的止损价位。 与第二种情况相比,它还提供更多信号。 与此同时,这样的入场点会发出更多的假信号,因为在极值线和颈部之间可能形成通道,或者可能存在旗形形态。 两种情况都表示趋势延续。
所有三种策略都指示在等于极值和颈线之间距离的价位上离场。
此外,在判断图表上的形态时,您应该注意到双顶/双底应该从价格走势中清晰地脱颖而出。 在描述形态时,通常会添加限制:两个顶部/底部之间应该至少有六根柱线。
此外,由于形态形成是基于价位理论,形态交易不应该与之相矛盾。 因此,基于预期目的,颈线不应低于初始走势的菲波纳奇等级 50。 此外,为了滤除假信号,我们可以添加第一次调整的最低价位(形成颈线)作为价格等级强度的指标。
我们将从形态搜索模块开始开发 EA。 我们用 MetaTrader 5 标准发行包中的之字折线指标来搜索价格极值。 将指标计算部分移至文章 [1] 中所述的类。 该指标包含两个指标缓冲区,其中包含极值点的价格值。 指标缓冲区包含极值之间的空值。 为了避免创建两个包含多个空值的指标缓冲区,它们由包含有关极值信息的结构数组所取代。 用于存储有关极值信息的结构如下所示。
struct s_Extremum { datetime TimeStartBar; double Price; s_Extremum(void) : TimeStartBar(0), Price(0) { } void Clear(void) { TimeStartBar=0; Price=0; } };
如果您至少使用过一次之字折线指标,您就会知道在搜索最佳参数时必须要做出一些妥协。 参数值太小会将大走势分成小部分,而太大的参数值会掩盖短期走势。 搜索形态图形的算法对于寻找极值的品质要求很高。 在尝试寻找中间点的同时,我决定使用具有小参数值的指标,并创建一个额外的上层结构,将单向走势与短期调整合并到一个走势。
CTrends 类是为解决这个问题而开发的。 类的头部提供如下。 在初始化期间,将指标类对象的引用和趋势延续的最小移动值传递给类。
class CTrends : public CObject { private: CZigZag *C_ZigZag; // 链接到之字折线指标对象 s_Extremum Trends[]; // 极值数组 int i_total; // 保存的极值总数 double d_MinCorrection; // 趋势延续的最小移动值 public: CTrends(); ~CTrends(); //--- 类初始化方法 virtual bool Create(CZigZag *pointer, double min_correction); //--- 获取有关极值的信息 virtual bool IsHigh(s_Extremum &pointer) const; virtual bool Extremum(s_Extremum &pointer, const int position=0); virtual int ExtremumByTime(datetime time); //--- 获取一般信息 virtual int Total(void) { Calculate(); return i_total; } virtual string Symbol(void) const { if(CheckPointer(C_ZigZag)==POINTER_INVALID) return "Not Initilized"; return C_ZigZag.Symbol(); } virtual ENUM_TIMEFRAMES Timeframe(void) const { if(CheckPointer(C_ZigZag)==POINTER_INVALID) return PERIOD_CURRENT; return C_ZigZag.Timeframe(); } protected: virtual bool Calculate(void); virtual bool AddTrendPoint(s_Extremum &pointer); };
要获取极值数据,类中提供了以下方法:
通用信息模块拥有返回已保存的极值总数,所用品名和时间帧的方法。
类的主要逻辑在 Calculate 方法中实现。 我们来仔细查看。
在方法的开头,检查指标类对象的引用的相关性,以及指标已发现的存在极值。
bool CTrends::Calculate(void) { if(CheckPointer(C_ZigZag)==POINTER_INVALID) return false; //--- if(C_ZigZag.Total()==0) return true;
接下来,定义未处理的极值数。 如果所有极值已处理,则使用 true 结果退出方法。
int start=(i_total<=0 ? C_ZigZag.Total() : C_ZigZag.ExtremumByTime(Trends[i_total-1].TimeStartBar)); switch(start) { case 0: return true; break; case -1: start=(i_total<=1 ? C_ZigZag.Total() : C_ZigZag.ExtremumByTime(Trends[i_total-2].TimeStartBar)); if(start<0 || ArrayResize(Trends,i_total-1)<=0) { ArrayFree(Trends); i_total=0; start=C_ZigZag.Total(); } else i_total=ArraySize(Trends); if(start==0) return true; break; }
之后,从指标类中请求必要数量的极值。
s_Extremum base[]; if(!C_ZigZag.Extremums(base,0,start)) return false; int total=ArraySize(base); if(total<=0) return true;
如果到该时刻为止数据库中没有极值,请通过调用 AddTrendPoint 方法将最旧的极值添加到数据库。
if(i_total==0) if(!AddTrendPoint(base[total-1])) return false;
接下来,安排循环迭代所有下载的极值。 将跳过上次保存的前一个极值。
for(int i=total-1;i>=0;i--) { int trends_pos=i_total-1; if(Trends[trends_pos].TimeStartBar>=base[i].TimeStartBar) continue;
在下一步中,检查极值点是否是单向的。 如果新的极值重新绘制前一个极值,则更新数据。
if(IsHigh(Trends[trends_pos])) { if(IsHigh(base[i])) { if(Trends[trends_pos].Price<base[i].Price) { Trends[trends_pos].Price=base[i].Price; Trends[trends_pos].TimeStartBar=base[i].TimeStartBar; } continue; }
对于相反指向的极值点,检查新走势是否是先前趋势的延续。 如果是,则更新极值数据。 如果不是,则通过调用 AddTrendPoint 方法在极值上添加数据;
else { if(trends_pos>1 && Trends[trends_pos-1].Price>base[i].Price && Trends[trends_pos-2].Price>Trends[trends_pos].Price) { double trend=fabs(Trends[trends_pos].Price-Trends[trends_pos-1].Price); double correction=fabs(Trends[trends_pos].Price-base[i].Price); if(fabs(1-correction/trend)>d_MinCorrection) { Trends[trends_pos-1].Price=base[i].Price; Trends[trends_pos-1].TimeStartBar=base[i].TimeStartBar; i_total--; ArrayResize(Trends,i_total); continue; } } AddTrendPoint(base[i]); } }
附件中提供了所有类及其方法的完整代码。
在定义价格极值之后,构建用于搜索入场点的模块。 将这项工作分为两个子步骤:
此功能已分配给 CPttern 类。 它的头部在下面提供。
class CPattern : public CObject { private: s_Extremum s_StartTrend; //趋势起始点 s_Extremum s_StartCorrection; //调整起始点 s_Extremum s_EndCorrection; //调整结束点 s_Extremum s_EndTrend; //趋势完成点 double d_MinCorrection; //最小调整 double d_MaxCorrection; //最大调整 //--- bool b_found; //"已形态检测" 标志 //--- CTrends *C_Trends; public: CPattern(); ~CPattern(); //--- 类初始化 virtual bool Create(CTrends *trends, double min_correction, double max_correction); //--- 搜索形态和入场点的方法 virtual bool Search(datetime start_time); virtual bool CheckSignal(int &signal, double &sl, double &tp1, double &tp2); //--- 比较对象的方法 virtual int Compare(const CPattern *node,const int mode=0) const; //--- 获取形态极值数据的方法 s_Extremum StartTrend(void) const { return s_StartTrend; } s_Extremum StartCorrection(void) const { return s_StartCorrection; } s_Extremum EndCorrection(void) const { return s_EndCorrection; } s_Extremum EndTrend(void) const { return s_EndTrend; } virtual datetime EndTrendTime(void) { return s_EndTrend.TimeStartBar; } };
使用四个相邻的极值来定义形态。 它们上的数据保存在 s_StartTrend,s_StartCorrection,s_EndCorrection 和 s_EndTrend 结构中。 为了识别形态,我们还需要存储在 d_MinCorrection 和 d_MaxCorrection 变量中的最小和最大调整等级。 我们将从先前创建的 CTrends 类的实例中获取极值。
在类初始化期间,我们将指针传递给 CTrends 类对象和边界调整等级。 在方法内部,检查所传递指针的有效性,保存接收到的信息并清除极值的结构。
bool CPattern::Create(CTrends *trends,double min_correction,double max_correction) { if(CheckPointer(trends)==POINTER_INVALID) return false; //--- C_Trends=trends; b_found=false; s_StartTrend.Clear(); s_StartCorrection.Clear(); s_EndCorrection.Clear(); s_EndTrend.Clear(); d_MinCorrection=min_correction; d_MaxCorrection=max_correction; //--- return true; }
搜索潜在形态将在 Search() 方法中执行。 参数中的方法接收搜索开始日期,并返回搜索结果形成的逻辑值。 我们来详细考察方法的算法。
首先,检查指向 CTrends 类对象的指针的相关性,以及是否存在已保存的极值。 如果结果为否定,则使用 false 结果退出方法。
bool CPattern::Search(datetime start_time) { if(CheckPointer(C_Trends)==POINTER_INVALID || C_Trends.Total()<4) return false;
接下来,定义与输入中指定的日期对应的极值点。 如果未找到极值,则使用 false 结果退出方法。
int start=C_Trends.ExtremumByTime(start_time); if(start<0) return false;
接下来,安排循环从指定日期开始迭代直到最后检测到的所有极值。 首先,我们获取四个连续的极值。 如果连至少一个极值都未有,则移至下一个极值。
b_found=false; for(int i=start;i>=0;i--) { if((i+3)>=C_Trends.Total()) continue; if(!C_Trends.Extremum(s_StartTrend,i+3) || !C_Trends.Extremum(s_StartCorrection,i+2) || !C_Trends.Extremum(s_EndCorrection,i+1) || !C_Trends.Extremum(s_EndTrend,i)) continue;
在下一阶段,检查极值是否与必要的形态相对应。 如果它们不对应,则转到下一个极值。 如果检测到形态,将标志设置为 true 并以相同结果退出方法。
double trend=s_StartCorrection.Price-s_StartTrend.Price; double correction=s_StartCorrection.Price-s_EndCorrection.Price; double re_trial=s_EndTrend.Price-s_EndCorrection.Price; double koef=correction/trend; if(koef<d_MinCorrection || koef>d_MaxCorrection || (1-fmin(correction,re_trial)/fmax(correction,re_trial))>=d_MaxCorrection) continue; b_found= true; //--- break; } //--- return b_found; }
下一步是检测入场点。 我们将使用 第二种案例。 为了降低价格不返回颈线的风险,我们将在较低的时间帧内搜索确认信号。
若要实现此功能,我们创建 CheckSignal() 方法。 除了信号本身,该方法还会返回止损和止盈价位。 所以,我们即将使用方法参数中指向变量的指针。
在该方法的开头,检查是否存在先前检测到形态的标志。 如果未找到形态,则使用 "false" 结果退出方法。
bool CPattern::CheckSignal(int &signal, double &sl, double &tp1, double &tp2) { if(!b_found) return false;
然后,判断形态形成蜡烛的收盘时间,并从形态形成的开始加载我们感兴趣的时间帧的数据直到当前时刻。
string symbol=C_Trends.Symbol(); if(symbol=="Not Initilized") return false; datetime start_time=s_EndTrend.TimeStartBar+PeriodSeconds(C_Trends.Timeframe()); int shift=iBarShift(symbol,e_ConfirmationTF,start_time); if(shift<0) return false; MqlRates rates[]; int total=CopyRates(symbol,e_ConfirmationTF,0,shift+1,rates); if(total<=0) return false;
在那之后,安排循环,我们将逐根柱线检查颈线突破,蜡烛调整,和收盘蜡烛在预期走势方向超越颈线。
我在这里添加了一些限制:
signal=0; sl=tp1=tp2=-1; bool up_trend=C_Trends.IsHigh(s_EndTrend); double extremum=(up_trend ? fmax(s_StartCorrection.Price,s_EndTrend.Price) : fmin(s_StartCorrection.Price,s_EndTrend.Price)); double exit_level=2*s_EndCorrection.Price - extremum; bool break_neck=false; for(int i=0;i<total;i++) { if(up_trend) { if(rates[i].low<=exit_level || rates[i].high>extremum) return false; if(!break_neck) { if(rates[i].close>s_EndCorrection.Price) continue; break_neck=true; continue; } if(rates[i].high>s_EndCorrection.Price) { if(sl==-1) sl=rates[i].high; else sl=fmax(sl,rates[i].high); } if(rates[i].close<s_EndCorrection.Price || sl==-1) continue; if((total-i)>2) return false;
在检测到入场信号后,指定信号类型(“-1” - 卖出,“1” - 买入)和交易价位。 止损设定在突破后相对于颈线的最大调整深度。 为止盈设置两个价位:
1. 位于极值线到持仓方向颈线的 90%。
2. 位于之前趋势走势的 90%。
添加限制:第一个止盈价位不能超过第二个。
signal=-1; double top=fmax(s_StartCorrection.Price,s_EndTrend.Price); tp1=s_EndCorrection.Price-(top-s_EndCorrection.Price)*0.9; tp2=top-(top-s_StartTrend.Price)*0.9; tp1=fmax(tp1,tp2); break; }
附件中提供了所有类和方法的完整代码。
准备工作完成后,将所有模块收集到一个 EA 中。 声明外部变量并将它们分成三个模块:
sinput string s1 = "---- ZigZag Settings ----"; //--- input int i_Depth = 12; // 深度 input int i_Deviation = 100; // 偏离 input int i_Backstep = 3; // 后退步数 input int i_MaxHistory = 1000; // 最大历史, 柱线 input ENUM_TIMEFRAMES e_TimeFrame = PERIOD_M30; // 工作时间帧 sinput string s2 = "---- Pattern Settings ----"; //--- input double d_MinCorrection= 0.118; // 最小修正 input double d_MaxCorrection= 0.5; // 最大修正 input ENUM_TIMEFRAMES e_ConfirmationTF= PERIOD_M5; // 确认时间帧 sinput string s3 = "---- Trade Settings ----"; //--- input double d_Lot = 0.1; // 交易手数 input ulong l_Slippage = 10; // 滑点 input uint i_SL = 350; // 止损后退,点数
在全局变量中,声明用于存储指向形态对象的指针数组,交易操作类的实例,形态搜索类的实例,其中存储指向所处理类实例的指针,以及用于存储下一个形态搜索变量的开始时间。
CArrayObj *ar_Objects;
CTrade *Trade;
CPattern *Pattern;
datetime start_search;
若要启用同时设置两个止盈的功能,请使用文章 [2] 中提供的技术。
在 OnInit() 函数中初始化所有必需的对象。 由于我们从未声明过 CZigZag 和 CTrends 类实例,因此我们只需初始化它们,并将指向这些对象的指针添加到数组中。 如果出现初始化错误,则在任何阶段使用 INIT_FAILED 结果退出该函数。
int OnInit() { //--- 初始化对象数组 ar_Objects=new CArrayObj(); if(CheckPointer(ar_Objects)==POINTER_INVALID) return INIT_FAILED; //--- 初始化之字折线指标类 CZigZag *zig_zag=new CZigZag(); if(CheckPointer(zig_zag)==POINTER_INVALID) return INIT_FAILED; if(!ar_Objects.Add(zig_zag)) { delete zig_zag; return INIT_FAILED; } zig_zag.Create(_Symbol,i_Depth,i_Deviation,i_Backstep,e_TimeFrame); zig_zag.MaxHistory(i_MaxHistory); //--- 初始化趋势走势搜索类 CTrends *trends=new CTrends(); if(CheckPointer(trends)==POINTER_INVALID) return INIT_FAILED; if(!ar_Objects.Add(trends)) { delete trends; return INIT_FAILED; } if(!trends.Create(zig_zag,d_MinCorrection)) return INIT_FAILED; //--- 初始化交易操作类 Trade=new CTrade(); if(CheckPointer(Trade)==POINTER_INVALID) return INIT_FAILED; Trade.SetAsyncMode(false); Trade.SetDeviationInPoints(l_Slippage); Trade.SetTypeFillingBySymbol(_Symbol); //--- 初始化其它变量 start_search=0; CLimitTakeProfit::OnlyOneSymbol(true); //--- return(INIT_SUCCEEDED); }
清除 OnDeinit() 函数中应用对象的实例。
void OnDeinit(const int reason) { //--- if(CheckPointer(ar_Objects)!=POINTER_INVALID) { for(int i=ar_Objects.Total()-1;i>=0;i--) delete ar_Objects.At(i); delete ar_Objects; } if(CheckPointer(Trade)!=POINTER_INVALID) delete Trade; if(CheckPointer(Pattern)!=POINTER_INVALID) delete Pattern; }
像往常一样,主要功能在 OnTick 函数中实现。 它可以划分为两个模块:
1. 检查先前检测到形态中的入场信号。 每当在搜索信号确认的较小时间帧内出现新蜡烛时,都会启动它。
2. 搜索新形态。 每当在工作时间帧(指标指定)内出现新蜡烛时,都会启动它。
在函数开始时,检查入场点确认时间帧内是否存在新柱线。 如果柱线未形成,则退出该函数直到下一次逐笔报价。 应该注意,只有在确认入场点的时间帧不超过工作时间帧时,此方法才能正常工作。 否则,您需要转到形态搜索模块,而非退出该函数。
void OnTick() { //--- static datetime Last_CfTF=0; datetime series=(datetime)SeriesInfoInteger(_Symbol,e_ConfirmationTF,SERIES_LASTBAR_DATE); if(Last_CfTF>=series) return; Last_CfTF=series;
如果出现新柱线,则安排循环检查所有先前保存的形态是否存在入场信号。 我们不会检查前两个数组对象的信号,因为我们在这些单元中存储指向极值搜索类实例的指针。 如果存储的指针无效或信号检查函数返回 false,则指针将从数组中删除。 在 CheckPattern() 函数中检查形态信号。 其算法将在下面提供。
int total=ar_Objects.Total(); for(int i=2;i<total;i++) { if(CheckPointer(ar_Objects.At(i))==POINTER_INVALID) if(ar_Objects.Delete(i)) { i--; total--; continue; } //--- if(!CheckPattern(ar_Objects.At(i))) { if(ar_Objects.Delete(i)) { i--; total--; continue; } } }
检查先前检测到的形态后,是时候转到第二个模块 — 搜索新形态。 为此,检查工作时间帧内新柱线的可用性。 如果未形成新柱线,则退出等待新的逐笔报价的函数。
static datetime Last_WT=0; series=(datetime)SeriesInfoInteger(_Symbol,e_TimeFrame,SERIES_LASTBAR_DATE); if(Last_WT>=series) return;
出现新柱线时,定义搜索形态的初始日期(考虑参数中指定的分析历史的深度)。 接下来,检查指向 CPattern 类对象的指针的相关性。 如果指针无效,则创建一个新的类实例。
start_search=iTime(_Symbol,e_TimeFrame,fmin(i_MaxHistory,Bars(_Symbol,e_TimeFrame))); if(CheckPointer(Pattern)==POINTER_INVALID) { Pattern=new CPattern(); if(CheckPointer(Pattern)==POINTER_INVALID) return; if(!Pattern.Create(ar_Objects.At(1),d_MinCorrection,d_MaxCorrection)) { delete Pattern; return; } } Last_WT=series;
之后,在循环中调用搜索潜在形态的方法。 如果搜索成功,将搜索的开始日期移至新形态,并在先前已发现形态数组中检查是否存在检测到的形态。 如果数组中已存在该形态,则移至新搜索。
while(!IsStopped() && Pattern.Search(start_search)) { start_search=fmax(start_search,Pattern.EndTrendTime()+PeriodSeconds(e_TimeFrame)); bool found=false; for(int i=2;i<ar_Objects.Total();i++) if(Pattern.Compare(ar_Objects.At(i),0)==0) { found=true; break; } if(found) continue;
如果找到新形态,则通过调用 CheckPattern() 函数检查入场信号。 之后,如有必要,将形态保存到数组中,并为下一次搜索初始化新的类实例。 循环继续,直到 Search() 方法在某次后续搜索中返回 false。
if(!CheckPattern(Pattern)) continue; if(!ar_Objects.Add(Pattern)) continue; Pattern=new CPattern(); if(CheckPointer(Pattern)==POINTER_INVALID) break; if(!Pattern.Create(ar_Objects.At(1),d_MinCorrection,d_MaxCorrection)) { delete Pattern; break; } } //--- return; }
我们看一下 CheckPattern() 函数算法,完成全景。 该方法在参数中接收指向 CPatern 类实例的指针,并返回操作结果的逻辑值。 如果函数返回 false,则从已保存对象的数组中删除所分析的形态。
在函数开始时,调用 CPattern 类的入场信号搜索方法。 如果检查失败,则以 false 结果退出函数。
bool CheckPattern(CPattern *pattern) { int signal=0; double sl=-1, tp1=-1, tp2=-1; if(!pattern.CheckSignal(signal,sl,tp1,tp2)) return false;
如果入场信号搜索成功,则根据信号设置交易价位,并发送入场订单。
double price=0; double to_close=100; //--- switch(signal) { case 1: price=SymbolInfoDouble(_Symbol,SYMBOL_ASK); CLimitTakeProfit::Clear(); if((tp1-price)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point) if(CLimitTakeProfit::AddTakeProfit((uint)((tp1-price)/_Point),(fabs(tp1-tp2)>=_Point ? 50 : 100))) to_close-=(fabs(tp1-tp2)>=_Point ? 50 : 100); if(to_close>0 && (tp2-price)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point) if(!CLimitTakeProfit::AddTakeProfit((uint)((tp2-price)/_Point),to_close)) return false; if(Trade.Buy(d_Lot,_Symbol,price,sl-i_SL*_Point,0,NULL)) return false; break; case -1: price=SymbolInfoDouble(_Symbol,SYMBOL_BID); CLimitTakeProfit::Clear(); if((price-tp1)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point) if(CLimitTakeProfit::AddTakeProfit((uint)((price-tp1)/_Point),(fabs(tp1-tp2)>=_Point ? 50 : 100))) to_close-=(fabs(tp1-tp2)>=_Point ? 50 : 100); if(to_close>0 && (price-tp2)>SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point) if(!CLimitTakeProfit::AddTakeProfit((uint)((price-tp2)/_Point),to_close)) return false; if(Trade.Sell(d_Lot,_Symbol,price,sl+i_SL*_Point,0,NULL)) return false; break; } //--- return true; }
如果开仓成功,则使用 false 结果退出该函数。 这样做是为了从数组中删除使用过的形态。 这允许我们避免在同一形态上重复开仓。
附件中提供了所有方法和函数的完整代码。
现在 EA 已经开发完毕,是时候依据历史数据检验其操作了。 该测试将在 2018 年的 9 个月期间进行,品种为 EURUSD。 在 M30 上执行形态搜索,而在 М5 上检测开仓入场点。
测试结果显示 EA 有产生利润的能力。 EA 在测试期内进行了 90 笔交易(其中 70 笔是盈利的)。 盈利因子为 2.02,恢复因子为 4.77,表明在实盘账户中使用 EA 的可能性。 完整的测试结果显示如下。
在本文中,我们基于双顶/双底趋势逆转形态开发了 EA。 依据历史数据测试 EA 已经证明了结果可接受,并且 EA 产生利润的能力证实了在搜索入场点时应用双订/双底形态作为有效趋势逆转信号的可能性。
# |
名称 |
类型 |
描述 |
---|---|---|---|
1 | ZigZag.mqh | 类库 | 之字折线指标类 |
2 | Trends.mqh | 类库 | 趋势搜索类 |
3 | Pattern.mqh | 类库 | 处理形态的类 |
4 | LimitTakeProfit.mqh | 类库 | 用限价订单替换止盈的类 |
5 | Header.mqh | 函数库 | EA 头文件 |
6 | DoubleTop.mq5 | 智能交易系统 | 基于双顶/双底策略的 EA |
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...
移动端课程