
逆转形态:测试双顶/双底形态
内容
概述
在文章“趋势有多长?”中所进行的分析表明价格在 60% 的时间内维持趋势。 这意味着在趋势伊始即开仓会产生最佳结果。 搜索趋势逆转点会产生大量的逆转形态。 双顶/双底是最著名和最常用的形态之一。
1. 形态形成的理论观点
在价格图表上可以频繁返现双顶/双底形态。 它的形成与交易价位理论密切相关。 当价格达到支撑位或阻力位时(取决于之前的走势),该形态在趋势结束时形成。 在重复测试价位过程中进行调整之后,它会再次回滚而非突破。
在这一点位上,逆势交易者开始从该价位回滚时交易,并将价格推向调整。 在调整走势获得动量的同时,顺势交易者开始获利了结离场,或将亏损持仓平仓来避免突破该价位。 这进一步加强了走势,导致了新趋势的出现。
在图表上搜索形态时,不必搜索顶部/底部的精准匹配。 顶部/底部价位的偏差被认为是正常的。 只需确保峰值在相同的支撑/阻力位之内。 形态可靠性取决于它所基于的级别强度。
2. 形态交易策略
形态的普及引发了多种涉及它的策略。 在互联网上,交易这种形态至少有三个不同的入场点。
2.1. 案例 1
第一个入场点是基于颈线的突破。 止损设置在顶/底线之外。 有不同的方法来定义“颈线突破”。 交易者可以使用在颈线下方收盘的柱线,以及突破颈线一段固定距离的柱线。 两种方法都有其优点和缺点。 在凌厉走势的情况下,蜡烛可以在距颈线足够的距离处收盘,令形态效率很低。
这种方法的缺点是止损价位相对较高,这降低了策略的利润/风险比。
2.2. 案例 2
第二个入场点基于镜面等级理论,当颈线从支撑变成阻力时,反之亦然。 此处的入场点是在价格突破后回到颈线时进行的。 在这种情况下,止损设置超出最后一次调整的极值,从而显著降低止损价位。 不幸的是,价格并不总是在突破后回测颈线,从而减少了入场次数。
2.3. 案例 3
第三个入场点基于趋势理论。 它的定义是突破从走势起点到颈线极值的趋势线。 与第一种情况一样,止损设置在顶/底线之外。 与第一个入场点相比,早期入场提供较低的止损价位。 与第二种情况相比,它还提供更多信号。 与此同时,这样的入场点会发出更多的假信号,因为在极值线和颈部之间可能形成通道,或者可能存在旗形形态。 两种情况都表示趋势延续。
所有三种策略都指示在等于极值和颈线之间距离的价位上离场。
此外,在判断图表上的形态时,您应该注意到双顶/双底应该从价格走势中清晰地脱颖而出。 在描述形态时,通常会添加限制:两个顶部/底部之间应该至少有六根柱线。
此外,由于形态形成是基于价位理论,形态交易不应该与之相矛盾。 因此,基于预期目的,颈线不应低于初始走势的菲波纳奇等级 50。 此外,为了滤除假信号,我们可以添加第一次调整的最低价位(形成颈线)作为价格等级强度的指标。
3. 创建 EA
3.1. 搜索极值
我们将从形态搜索模块开始开发 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); };
要获取极值数据,类中提供了以下方法:
- ExtremumByTime — 获取数据库中指定时间的极值数字,
- Extremum — 返回数据库中指定位置的极值,
- IsHigh — 如果指定的极值是顶部,则返回 true; 如果是底部,则返回 false。
通用信息模块拥有返回已保存的极值总数,所用品名和时间帧的方法。
类的主要逻辑在 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]); } }
附件中提供了所有类及其方法的完整代码。
3.2. 搜索形态
在定义价格极值之后,构建用于搜索入场点的模块。 将这项工作分为两个子步骤:
- 搜索潜在的入场形态。
- 入场点。
此功能已分配给 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; }
附件中提供了所有类和方法的完整代码。
3.3. 开发 EA
准备工作完成后,将所有模块收集到一个 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 结果退出该函数。 这样做是为了从数组中删除使用过的形态。 这允许我们避免在同一形态上重复开仓。
附件中提供了所有方法和函数的完整代码。
4. 策略测试
现在 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 |
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/5319
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.


您在哪个历史时期进行测试?
从今年年初到今天。
时间框架和工具?
时间框架和工具?
你好,请出示日志。
你好,德米特里
我想测试您的 EA,但出现了一些错误。TP 设置不正确,而且出现了这些错误: