在文章的第一部分 当中,我曾描述过一个修订的 ZigZag 指标和一个用于接收该类型指标数据的类。 在此,我将展示如何基于这些工具开发指标,并编写一款根据 ZigZag 指标形成的信号进行交易的 EA 来测试。
作为补充,本文将介绍一套开发图形用户界面的新版 EasyAndFast 函数库。
文章的主要话题:
我们来研究定义价格行为的三个指标。
这些指标中每一个的代码结构与文章第一部分中描述的 ZigZag 指标相同。 所以,我们只关注接收数据和填充指标缓冲区的主函数(FillIndicatorBuffers)。
对于 FrequencyChangeZZ 指标,主要函数的代码与下面的清单相同。 要将柱线索引和时间数组传递给函数。 接下来,从当前柱线时间复制必要数量的 ZigZag 指标和时间数组元素(源数据)。 如果收到源数据,则请求最终数据。 之后,它仍然只是调用一个方法来返回线段集合当中的柱线数量。 结果将保存到指标缓冲区的当前元素。
//+------------------------------------------------------------------+ //| 填充指标缓冲区 | //+------------------------------------------------------------------+ void FillIndicatorBuffers(const int i,const datetime &time[]) { int copy_total=1000; for(int t=0; t<10; t++) { if(::CopyBuffer(zz_handle,2,time[i],copy_total,h_zz_buffer_temp)==copy_total && ::CopyBuffer(zz_handle,3,time[i],copy_total,l_zz_buffer_temp)==copy_total && ::CopyTime(_Symbol,_Period,time[i],copy_total,t_zz_buffer_temp)==copy_total) { //--- 获取 ZZ 数据 zz.GetZigZagData(h_zz_buffer_temp,l_zz_buffer_temp,t_zz_buffer_temp); //--- 将线段集合中的柱线数量保存到指标缓冲区 segments_bars_total_buffer[i]=zz.SegmentsTotalBars(); break; } } }
在指标外部参数中,我们将指定以下内容:
(1) 应根据所有可用数据计算数值,
(2) 形成 ZigZag 指标新线段的最小偏差
(3) 获取最终数据的极值数量。
本文中的所有指标都具有相同的参数。
图例 1. 指标外部参数
FrequencyChangeZZ 指标在子窗口中显示图表,如下所示。 ZigZag 指标会被加载到主图表上以便更直观。 指标清晰显示价格在选择方向时会放缓。
图例 2. FrequencyChangeZZ 指标
在 SumSegmentsZZ 指标中,获取数据的主要函数如下所示。 全部都与前一个示例中的相同。 唯一的区别是这里分别填充了三个指标缓冲区,分别用于向上和向下线段。 多出的一个缓冲区 用来依据当前值计算这些参数的平均值。
//+------------------------------------------------------------------+ //| 填充指标缓冲区 | //+------------------------------------------------------------------+ void FillIndicatorBuffers(const int i,const datetime &time[]) { int copy_total=1000; for(int t=0; t<10; t++) { if(CopyBuffer(zz_handle,2,time[i],copy_total,h_zz_buffer_temp)==copy_total && CopyBuffer(zz_handle,3,time[i],copy_total,l_zz_buffer_temp)==copy_total && CopyTime(_Symbol,_Period,time[i],copy_total,t_zz_buffer_temp)==copy_total) { //--- 获取 ZZ 数据 zz.GetZigZagData(h_zz_buffer_temp,l_zz_buffer_temp,t_zz_buffer_temp); //--- 获取线段数据 segments_up_total_buffer[i] =zz.SumSegmentsUp(); segments_dw_total_buffer[i] =zz.SumSegmentsDown(); segments_average_buffer[i] =(segments_up_total_buffer[i]+segments_dw_total_buffer[i])/2; break; } } }
在图表上加载 SumSegmentsZZ 之后,您将看到如下截图中的结果。 在此我们可以看到,在蓝线超过红线之后,向上线段的总和大于向下线段的总和。 如果红线超过蓝线,情况就会逆转。 只有在策略测试器当中的实验可以告诉我们,这是否能作为未来价格方向的可靠信息来源。 乍一看,单向线段总和超过反向线段总和的时间越长,逆转概率越高。
图例 3. SumSegmentsZZ 指标
现在,我们看一下 PercentageSegmentsZZ 指标。 与前一种情况一样,应在指标的主要函数中填充三个指标缓冲区:用于指向(1)向上(2)向下的线段百分比各占一个缓冲区,以及一个缓冲区(3) 对于这些数值之间的差值。
//+------------------------------------------------------------------+ //| 填充指标缓冲区 | //+------------------------------------------------------------------+ void FillIndicatorBuffers(const int i,const datetime &time[]) { int copy_total=1000; for(int t=0; t<10; t++) { if(CopyBuffer(zz_handle,2,time[i],copy_total,h_zz_buffer_temp)==copy_total && CopyBuffer(zz_handle,3,time[i],copy_total,l_zz_buffer_temp)==copy_total && CopyTime(_Symbol,_Period,time[i],copy_total,t_zz_buffer_temp)==copy_total) { //--- 获取 ZZ 数据 zz.GetZigZagData(h_zz_buffer_temp,l_zz_buffer_temp,t_zz_buffer_temp); //--- 获取线段上的数据 double sum_up =zz.SumSegmentsUp(); double sum_dw =zz.SumSegmentsDown(); double sum =sum_up+sum_dw; //--- 百分比和差值 if(sum>0) { segments_up_total_buffer[i] =zz.PercentSumSegmentsUp(); segments_dw_total_buffer[i] =zz.PercentSumSegmentsDown(); segments_difference_buffer[i] =fabs(segments_up_total_buffer[i]-segments_dw_total_buffer[i]); break; } } } }
结果示意如下。 我们尝试解释一下。 当多向线段之间的百分比差值小于某个阈值时,可视为横盘。 在这种情况下,我们还应该记住,比率应该经常互换,因为价格能够在一个方向上移动很长时间,而差值低于优化器选择的级别。 在这些情况下,我们应该考虑按照一定顺序的形态格式来应用模型。
图例 4. PercentageSegmentsZZ 指标
在前一篇文章中,我们已经演示了 EA 同时分析来自较高和较低时间帧的 ZigZag 指标数据。 因此,可以更详细地从较高时间帧内的线段分析价格行为。 换句话说,我们定义了较高时间帧内的线段如何在较低时间帧上形成。 我们看看这组参数将如何以一个单独指标的形式,基于价格历史显示这些数值。
就像前一篇文章的 EA 一样,我们将收到四个反向线段百分比之间的差值:一个值用于较高的时间帧,三个值用于较低时间帧。 这些数值依据较高时间帧上的最后三个 ZigZag 指标线段计算。 指标缓冲区的颜色与前一部分的 EA 相同。 之后,我们将开发一款 EA 来测试指标,这样我们就可以更容易地理解图表上观察到的时间段与数据如何对应。
//--- 缓冲区数量 #property indicator_buffers 4 #property indicator_plots 4 //--- 缓冲区颜色 #property indicator_color1 clrSilver #property indicator_color2 clrRed #property indicator_color3 clrLimeGreen #property indicator_color4 clrMediumPurple
声明 CZigZagModule 类的四个实例:
#include <Addons\Indicators\ZigZag\ZigZagModule.mqh>
CZigZagModule zz_higher_tf;
CZigZagModule zz_current0;
CZigZagModule zz_current1;
CZigZagModule zz_current2;
我们在外部参数里增添设定 更高指标时间帧 的能力:
input int NumberOfBars =0; // 计算 ZZ 所需的柱线数量 input int MinImpulseSize =0; // 线段的最小点数 input int CopyExtremum =5; // 复制极值 input ENUM_TIMEFRAMES HigherTimeframe =PERIOD_H1; // 较高时间帧
填充指标缓冲区的主要函数实现如下。 首先,从外部参数中指定的较高时间帧获取源数据。 然后获取最终数据并保存参数值。 接着,我们始终从较高的时间帧获取三个指标线段的数据。 之后,填充所有指标缓冲区。 我必须开发两个独立的代码块,以便可以基于历史以及在实时/测试器的最后一根柱线上正确计算指标。
//+------------------------------------------------------------------+ //| 填充指标缓冲区 | //+------------------------------------------------------------------+ void FillIndicatorBuffers(const int i,const int total,const datetime &time[]) { int index=total-i-1; int copy_total=1000; int h_buff=2,l_buff=3; datetime start_time_in =NULL; datetime stop_time_in =NULL; //--- 从更高的时间帧获取源数据 datetime stop_time=time[i]-(PeriodSeconds(HigherTimeframe)*copy_total); CopyBuffer(zz_handle_htf,2,time[i],stop_time,h_zz_buffer_temp); CopyBuffer(zz_handle_htf,3,time[i],stop_time,l_zz_buffer_temp); CopyTime(_Symbol,HigherTimeframe,time[i],stop_time,t_zz_buffer_temp); //--- 从更高的时间帧获取最终数据 zz_higher_tf.GetZigZagData(h_zz_buffer_temp,l_zz_buffer_temp,t_zz_buffer_temp); double htf_value=zz_higher_tf.PercentSumSegmentsDifference(); //--- 第一根线段数据 zz_higher_tf.SegmentTimes(zz_handle_current,h_buff,l_buff,_Symbol,HigherTimeframe,_Period,0,start_time_in,stop_time_in); zz_current0.GetZigZagData(zz_handle_current,_Symbol,_Period,start_time_in,stop_time_in); //--- 第二根线段数据 zz_higher_tf.SegmentTimes(zz_handle_current,h_buff,l_buff,_Symbol,HigherTimeframe,_Period,1,start_time_in,stop_time_in); zz_current1.GetZigZagData(zz_handle_current,_Symbol,_Period,start_time_in,stop_time_in); //--- 第三根线段数据 zz_higher_tf.SegmentTimes(zz_handle_current,h_buff,l_buff,_Symbol,HigherTimeframe,_Period,2,start_time_in,stop_time_in); zz_current2.GetZigZagData(zz_handle_current,_Symbol,_Period,start_time_in,stop_time_in); //--- 基于最后一根柱线 if(i<total-1) { buffer_zz_higher_tf[i] =htf_value; buffer_segment_0[i] =zz_current0.PercentSumSegmentsDifference(); buffer_segment_1[i] =zz_current1.PercentSumSegmentsDifference(); buffer_segment_2[i] =zz_current2.PercentSumSegmentsDifference(); } //--- 基于历史 else { //--- 如果更高时间帧有新的柱线 if(new_bar_time!=t_zz_buffer_temp[0]) { new_bar_time=t_zz_buffer_temp[0]; //--- if(i>2) { int f=1,s=2; buffer_zz_higher_tf[i-f] =buffer_zz_higher_tf[i-s]; buffer_segment_0[i-f] =buffer_segment_0[i-s]; buffer_segment_1[i-f] =buffer_segment_1[i-s]; buffer_segment_2[i-f] =buffer_segment_2[i-s]; } } else { buffer_zz_higher_tf[i] =htf_value; buffer_segment_0[i] =zz_current0.PercentSumSegmentsDifference(); buffer_segment_1[i] =zz_current1.PercentSumSegmentsDifference(); buffer_segment_2[i] =zz_current2.PercentSumSegmentsDifference(); } } }
我们从前一篇文章中复制 EA,并添加几行代码来测试 MultiPercentageSegmentsZZ 指标。 添加外部参数 以便设定更高时间帧。 为了在测试器的可视化模式下进行 EA 测试期间显示指标,获取其句柄就足够了。
//--- 外部参数 input uint CopyExtremum =3; // 复制极值 input int MinImpulseSize =0; // 最小值 impulse size input ENUM_TIMEFRAMES HigherTimeframe =PERIOD_H1; // 较高时间帧 ... //+------------------------------------------------------------------+ //| 智能系统初始化函数 | //+------------------------------------------------------------------+ int OnInit(void) { ... //--- ZZ 指标的路径 string zz_path1="Custom\\ZigZag\\ExactZZ_Plus.ex5"; string zz_path2="Custom\\ZigZag\\MultiPercentageSegmentsZZ.ex5"; //--- 获取指标句柄 zz_handle_current =::iCustom(_Symbol,_Period,zz_path1,0,MinImpulseSize,false,false); zz_handle_higher_tf =::iCustom(_Symbol,HigherTimeframe,zz_path1,0,MinImpulseSize,false,false); zz_handle =::iCustom(_Symbol,_Period,zz_path2,0,MinImpulseSize,CopyExtremum,HigherTimeframe); ... return(INIT_SUCCEEDED); }
这是它在测试器中的样子:
图例 5. MultiPercentageSegmentsZZ 指标
上述所有指标可以在不同的时间帧内以各种组合使用。 现在,我们利用所描述的工具收集一些品种集合的统计数据,以便了解哪些品种更适合在价格通道内进行交易。
作为补充,本文介绍一套用于开发图形用户界面的新版本 EasyAndFast 函数库。 这里我们只列出函数库的新功能:
图例 6. 将元素合并为组
可以在 代码库 中下载新版函数库。
接下来,我们利用新版 EasyAndFast 函数库创建一个测试 EA,来收集一些统计信息。 我们将从开发应用程序的图形用户界面(GUI)开始,然后过渡到收集和显示统计信息的方法。
我们先定义我们需要的 GUI 控件:
如前所提,CWndCreate 类应作为基类包含在自定义类中,以便更快、更便捷地开发 GUI。 完整连接如下所示: CWndContainer -> CWndEvents -> CWndCreate -> CProgram。 CWndCreate 类的存在允许在单行中创建 GUI 元素,而无需在自定义类中创建单独的方法。 该类包含几乎所有库元素的不同模板。 如有必要,您可以自行添加新模板。
若要创建 GUI,请声明上面列表中包含的元素,如下列代码所示。 当前版本的 CWndCreate 类没有快速创建表格的模板,因此我们自行开发此方法。
//+------------------------------------------------------------------+ //| Program.mqh | //| 版权所有 2018, MetaQuotes 软件公司 | //| http://www.mql5.com | //+------------------------------------------------------------------+ #include <EasyAndFastGUI\WndCreate.mqh> //+------------------------------------------------------------------+ //| 用于开发应用程序的类 | //+------------------------------------------------------------------+ class CProgram : public CWndCreate { private: //--- 窗体 CWindow m_window; //--- 状态栏 CStatusBar m_status_bar; //--- 下拉式日历 CDropCalendar m_from_date; CDropCalendar m_to_date; //--- 按钮 CButton m_request; //--- 输入字段 CTextEdit m_filter; CTextEdit m_level; //--- 组合框 CComboBox m_data_type; //--- 表格 CTable m_table; //--- 进度栏 CProgressBar m_progress_bar; //--- public: //--- 创建 GUI bool CreateGUI(void); //--- private: //--- 表格 bool CreateTable(const int x_gap,const int y_gap); };
若要创建包含此种内容的图形界面,只需指定属性的数值,并将其作为参数来调用 CWndCreate 类的必要方法,如下列代码所示。 若要定义与方法参数相关的属性,在其中设置文本光标并单击 Ctrl + Shift + Space:
图例 7. 查看方法参数
如果需要设置其他属性,可采用示例 “货币过滤器输入字段” 中所示的相同方式。 此处示意的是在创建元素后 默认启用复选框。
//+------------------------------------------------------------------+ //| 创建 GUI | //+------------------------------------------------------------------+ bool CProgram::CreateGUI(void) { //--- 创建控制窗体 if(!CWndCreate::CreateWindow(m_window,"ZZ Market Scanner",1,1,640,480,true,true,true,true)) return(false); //--- 状态栏 string text_items[1]; text_items[0]="For Help, press F1"; int width_items[]={0}; if(!CWndCreate::CreateStatusBar(m_status_bar,m_window,1,23,22,text_items,width_items)) return(false); //--- 货币过滤器输入字段 if(!CWndCreate::CreateTextEdit(m_filter,"Symbols filter:",m_window,0,true,7,25,627,535,"USD","Example: EURUSD,GBP,NOK")) return(false); else m_filter.IsPressed(true); //--- 下拉日历 if(!CWndCreate::CreateDropCalendar(m_from_date,"From:",m_window,0,7,50,130,D'2018.01.01')) return(false); if(!CWndCreate::CreateDropCalendar(m_to_date,"To:",m_window,0,150,50,117,::TimeCurrent())) return(false); //--- 用于指定级别的输入字段 if(!CWndCreate::CreateTextEdit(m_level,"Level:",m_window,0,false,280,50,85,50,100,0,1,0,30)) return(false); //--- 按钮 if(!CWndCreate::CreateButton(m_request,"Request",m_window,0,375,50,70)) return(false); //--- 表格 if(!CreateTable(2,75)) return(false); //--- 进度栏 if(!CWndCreate::CreateProgressBar(m_progress_bar,"Processing:",m_status_bar,0,2,3)) return(false); //--- 结束 GUI 开发 CWndEvents::CompletedGUI(); return(true); }
若是表格的情况下,创建一个自定义方法,因为它是一个复杂元素,在创建元素之前应指定大量属性。 它拥有四列。 第一列将显示货币对。 其余的将显示三个时间帧的统计数据: M5, H1 和 H8。
//+------------------------------------------------------------------+ //| 创建表格 | //+------------------------------------------------------------------+ bool CProgram::CreateTable(const int x_gap,const int y_gap) { #define COLUMNS1_TOTAL 4 #define ROWS1_TOTAL 1 //--- 保存指向主元素的指针 m_table.MainPointer(m_window); //--- 列宽数组 int width[COLUMNS1_TOTAL]; ::ArrayInitialize(width,50); width[0]=80; //--- 沿 X 轴的列中文本偏移的数组 int text_x_offset[COLUMNS1_TOTAL]; ::ArrayInitialize(text_x_offset,7); //--- 列中的文本对齐数组 ENUM_ALIGN_MODE align[COLUMNS1_TOTAL]; ::ArrayInitialize(align,ALIGN_CENTER); align[0]=ALIGN_LEFT; //--- 属性 m_table.TableSize(COLUMNS1_TOTAL,ROWS1_TOTAL); m_table.TextAlign(align); m_table.ColumnsWidth(width); m_table.TextXOffset(text_x_offset); m_table.ShowHeaders(true); m_table.IsSortMode(true); m_table.IsZebraFormatRows(clrWhiteSmoke); m_table.AutoXResizeMode(true); m_table.AutoYResizeMode(true); m_table.AutoXResizeRightOffset(2); m_table.AutoYResizeBottomOffset(24); //--- 创建控件 if(!m_table.CreateTable(x_gap,y_gap)) return(false); //--- 标题 string headers[]={"Symbols","M5","H1","H8"}; for(uint i=0; i<m_table.ColumnsTotal(); i++) m_table.SetHeaderText(i,headers[i]); //--- 将对象添加到公共对象组数组 CWndContainer::AddToElementsArray(0,m_table); return(true); }
现在我们考察获取数据的方法。 首先,我们需要获得我们要使用的品种。 在此 EA 版本中,我们将接收来自外汇品种的数据。 同时,我们将排除禁止交易的品种。 在此我们还需要 CheckFilterText() 辅助方法按照过滤器来检查品种。 在输入字段中,用户可以输入理应存在的品种名称,并用逗号分隔文本值。 如果禁用字段复选框或未输入文本,则不执行检查。 如果验证通过,则应能发现匹配项,输入的文本被切分成子串,并按照必要的字符串执行搜索。
class CProgram : public CWndCreate { private: //--- 按照过滤器检查品种 bool CheckFilterText(const string symbol_name); }; //+------------------------------------------------------------------+ //| 按照过滤器检查品种 | //+------------------------------------------------------------------+ bool CProgram::CheckFilterText(const string symbol_name) { bool check=false; //--- 如果启用了品种名称过滤器 if(!m_filter.IsPressed()) return(true); //--- 如果输入了文本 string text=m_filter.GetValue(); if(text=="") return(true); //--- 切分为子串 string elements[]; ushort sep=::StringGetCharacter(",",0); ::StringSplit(text,sep,elements); //--- 匹配检查 int elements_total=::ArraySize(elements); for(int e=0; e<elements_total; e++) { //--- 删除边外空白 ::StringTrimLeft(elements[e]); ::StringTrimRight(elements[e]); //--- 如果检测到匹配 if(::StringFind(symbol_name,elements[e])>-1) { check=true; break; } } //--- 结果 return(check); }
在 CProgram::GetSymbols() 方法里,循环中传递服务器上存在的所有品种,将符合指定条件的品种收集到数组中。 在常规循环中,所有品种都将从 “市场观察” 窗口中删除。 之后,只将数组中包含的数据添加到窗口。
class CProgram : public CWndCreate { private: //--- 品种数组 string m_symbols[]; //--- private: //--- 获取品种 void GetSymbols(void); }; //+------------------------------------------------------------------+ //| 获取品种 | //+------------------------------------------------------------------+ void CProgram::GetSymbols(void) { //--- 进度 m_progress_bar.LabelText("Get symbols..."); m_progress_bar.Update(0,1); //--- 清楚品种数组 ::ArrayFree(m_symbols); //--- 收集外汇品种的数组 int symbols_total=::SymbolsTotal(false); for(int i=0; i<symbols_total; i++) { //--- 获取品种名称 string symbol_name=::SymbolName(i,false); //--- 在市场观察窗口中隐藏它 ::SymbolSelect(symbol_name,false); //--- 如果这不是外汇代码,则转到下一个 if(::SymbolInfoInteger(symbol_name,SYMBOL_TRADE_CALC_MODE)!=SYMBOL_CALC_MODE_FOREX) continue; //--- 如果禁止交易,则转到下一个 if(::SymbolInfoInteger(symbol_name,SYMBOL_TRADE_MODE)==SYMBOL_TRADE_MODE_DISABLED) continue; //--- 按照过滤器检查品种 if(!CheckFilterText(symbol_name)) continue; //--- 将品种保存到数组 int array_size=::ArraySize(m_symbols); ::ArrayResize(m_symbols,array_size+1,1000); m_symbols[array_size]=symbol_name; } //--- 如果数组为空,则将当前品种设置为默认值 int array_size=::ArraySize(m_symbols); if(array_size<1) { ::ArrayResize(m_symbols,array_size+1); m_symbols[array_size]=_Symbol; } //--- 在市场观察窗口中显示 int selected_symbols_total=::ArraySize(m_symbols); for(int i=0; i<selected_symbols_total; i++) ::SymbolSelect(m_symbols[i],true); }
若要获取有关所收集品种的数据,我们应首先获取绑定它们的指标句柄。 每次我们获取指标句柄,我们需要等到计算结束,然后再复制数据以便进一步分析。 接收所有数据完毕后,进行必要的计算。
CProgram::GetSymbolsData() 方法即用于此。 它接受两个参数:品种和时间帧。 收到指标句柄后,在指定时间范围内找出有多少根柱线存在。 可以利用应用程序的 GUI 控件指定日期范围。 接下来,我们尝试获取已计算的指标数据量。 收到句柄后,指标计算可能无法立即完成。 所以,如果 BarsCalculated() 函数返回 -1,我们要再次尝试获取有效值,直到 等于或超过指定时间范围内的总柱线数量。
在计算指标数据后,我们可以尝试将它们放入数组中。 也许在数量 大于或等于柱线总数 之前,需要若干次尝试。
如果指标已成功复制到数组,则剩下的仅仅是进行必要的计算。 在这种情况下,我们计算 数据总量与数量的百分比,其中指标值高于指定级别。 也可以在应用程序的 GUI 中指定此级别。
在方法结束时,移除指标句柄来释放其计算部分。 针对所选的品种列表和若干时间帧,CProgram::GetSymbolsData() 方法会被多次调用。 每次计算它们时只应执行一次,结果值显示在 GUI 表格中,然后句柄不再需要,可以删除。
class CProgram : public CWndCreate { private: //--- 获取品种数据 double GetSymbolsData(const string symbol,const ENUM_TIMEFRAMES period); }; //+------------------------------------------------------------------+ //| 获取品种数据 | //+------------------------------------------------------------------+ double CProgram::GetSymbolsData(const string symbol,const ENUM_TIMEFRAMES period) { double result =0.0; int buffer_index =2; //--- 获取指标句柄 string path ="::Indicators\\Custom\\ZigZag\\PercentageSegmentsZZ.ex5"; int handle =::iCustom(symbol,period,path,0,0,5); if(handle!=INVALID_HANDLE) { //--- 复制指定范围内的数据 double data[]; datetime start_time =m_from_date.SelectedDate(); datetime end_time =m_to_date.SelectedDate(); //--- 指定范围内的柱线数量 int bars_total=::Bars(symbol,period,start_time,end_time); //--- 指定范围内的柱线数量 int bars_calculated=::BarsCalculated(handle); if(bars_calculated<bars_total) { while(true) { ::Sleep(100); bars_calculated=::BarsCalculated(handle); if(bars_calculated>=bars_total) break; } } //--- 获取数据 int copied=::CopyBuffer(handle,buffer_index,start_time,end_time,data); if(copied<1) { while(true) { ::Sleep(100); copied=::CopyBuffer(handle,buffer_index,start_time,end_time,data); if(copied>=bars_total) break; } } //--- 如果没有收到数据则退出 int total=::ArraySize(data); if(total<1) return(result); //--- 计算重复次数 int counter=0; for(int k=0; k<total; k++) { if(data[k]>(double)m_level.GetValue()) counter++; } //--- 百分比 result=((double)counter/(double)total)*100; } //--- 释放指标 ::IndicatorRelease(handle); //--- 返回数值 return(result); }
每次形成新的品种列表时,都需要重建该表。 为此,只需删除表格的所有行,并添加必要的数额即可。
class CProgram : public CWndCreate { private: //--- 重新构建表格 void RebuildingTables(void); }; //+------------------------------------------------------------------+ //| 重新构建表格 | //+------------------------------------------------------------------+ void CProgram::RebuildingTables(void) { //--- 删除所有行 m_table.DeleteAllRows(); //--- 添加数据 int symbols_total=::ArraySize(m_symbols); for(int i=1; i<symbols_total; i++) m_table.AddRow(i); }
CProgram::SetData() 方法用来将数据填充至表格列内。 它需要传递两个参数(列索引和时间帧)。 在此,我们遍历指定列的单元格,在循环中用计算出的数值填充它们。 进度栏 显示品种和时间帧,刚刚收到的数据,以便用户了解当前正在做什么。
class CProgram : public CWndCreate { private: //--- 在指定的列设置数值 void SetData(const int column_index,const ENUM_TIMEFRAMES period); //--- 时间帧转化为字符串 string GetPeriodName(const ENUM_TIMEFRAMES period); }; //+------------------------------------------------------------------+ //| 在指定的列设置数值 | //+------------------------------------------------------------------+ void CProgram::SetData(const int column_index,const ENUM_TIMEFRAMES period) { for(uint r=0; r<(uint)m_table.RowsTotal(); r++) { double value=GetSymbolsData(m_symbols[r],period); m_table.SetValue(column_index,r,string(value),2,true); m_table.Update(); //--- 进度 m_progress_bar.LabelText("Data preparation ["+m_symbols[r]+","+GetPeriodName(period)+"]..."); m_progress_bar.Update(r,m_table.RowsTotal()); } } //+------------------------------------------------------------------+ //| 返回周期字符串对应的值 | //+------------------------------------------------------------------+ string CProgram::GetPeriodName(const ENUM_TIMEFRAMES period) { return(::StringSubstr(::EnumToString(period),7)); }
用数据填充表格的主要方法是 CProgram::SetDataToTable()。 该表格首先在这里重建。 接着,我们需要在其中设置标题和数据类型(TYPE_DOUBLE)。 在第一列内设置所收集的品种。 重新绘制表格以便立即查看变化。
现在我们可以开始接收所有指定品种和时间帧的指标数据。 为此,只需调用 CProgram::SetData() 方法,将列索引和时间帧作为参数传递给它。
class CProgram : public CWndCreate { private: //--- 在表格中填充数据 void SetDataToTable(void); }; //+------------------------------------------------------------------+ //| 在表格中填充数据 | //+------------------------------------------------------------------+ void CProgram::SetDataToTable(void) { //--- 进度 m_progress_bar.LabelText("Data preparation..."); m_progress_bar.Update(0,1); //--- 重新构建表格 RebuildingTable(); //--- 标题 string headers[]={"Symbols","M5","H1","H8"}; for(uint i=0; i<m_table.ColumnsTotal(); i++) m_table.SetHeaderText(i,headers[i]); for(uint i=1; i<m_table.ColumnsTotal(); i++) m_table.DataType(i,TYPE_DOUBLE); //--- 在第一列里设置数值 for(uint r=0; r<(uint)m_table.RowsTotal(); r++) m_table.SetValue(0,r,m_symbols[r],0,true); //--- 显示表格 m_table.Update(true); //--- 用数据填充剩余的列 SetData(1,PERIOD_M5); SetData(2,PERIOD_H1); SetData(3,PERIOD_H8); }
在使用 CProgram::GetData() 方法接收新数据之前,我们应借助 CProgram::StartProgress() 方法令进度条可见。 接收到新数据后,隐藏进度条并从按下的按钮中移除焦点。 为此,调用 CProgram::EndProgress() 方法。
class CProgram : public CWndCreate { private: //--- 获取数据 void GetData(void); //--- 进度(1)开始,(2)结束 void StartProgress(void); void EndProgress(void); }; //+------------------------------------------------------------------+ //| 获取数据 | //+------------------------------------------------------------------+ void CProgram::GetData(void) { //--- 进度开始 StartProgress(); //--- 获取品种列表 GetSymbols(); //--- 在表格中填充数据 SetDataToTable(); //--- 进度结束 EndProgress(); } //+------------------------------------------------------------------+ //| 进度开始 | //+------------------------------------------------------------------+ void CProgram::StartProgress(void) { m_progress_bar.LabelText("Please wait..."); m_progress_bar.Update(0,1); m_progress_bar.Show(); m_chart.Redraw(); } //+------------------------------------------------------------------+ //| 进度结束 | //+------------------------------------------------------------------+ void CProgram::EndProgress(void) { //--- 隐藏进度栏 m_progress_bar.Hide(); //--- 更新按钮 m_request.MouseFocus(false); m_request.Update(true); m_chart.Redraw(); }
当用户单击 请求 时,将生成 ON_CLICK_BUTTON 自定义事件,并且我们可以按元素 ID 定义按下的按钮。 如果是 请求 按钮,启动数据获取进程。
在表格创建方法中,我们包含了通过单击标题对表格进行排序的能力。 ON_SORT_DATA 自定义事件会在每次执行此操作时生成。 收到事件后,应更新表格以便显示变化。
//+------------------------------------------------------------------+ //| 事件处理器 | //+------------------------------------------------------------------+ void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- 按下按钮事件 if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON) { if(lparam==m_request.Id()) { //--- 获取数据 GetData(); return; } //--- return; } //--- 表格排序事件 if(id==CHARTEVENT_CUSTOM+ON_SORT_DATA) { if(lparam==m_table.Id()) { m_table.Update(true); return; } //--- return; } }
现在,我们看看结果。 如果我们编译程序并将其加载到图表上,结果会如下屏幕截图所示。 以下参数按默认情况设置:
图例 8. MQL 应用程序的 GUI
按下 请求 启动数据采集:
图例 9. 接收数据
收到所有数据后,您可以对它们进行排序:
图例 10. 表格数据排序
您可以修改并使用此应用程序来解决您的一些任务。 该表格可以按照任意其他参数来填充。
下面我将提供另一个示例,演示如何进一步提高表格数据的直观性。 正如我在本节开头已经提到的,最新版本的 EasyAndFast 函数库具有设置表格单元格背景颜色的功能。 这可令您按照其它各种表格编辑器中所见到的方式来格式化表格。 下面的屏幕截图显示了 Excel 电子表格中的格式数据。 即使在排序数组时,每个单元格的自身背景颜色也会保持相同数值。
图例 11. Excel 中的色标
这种格式化令快速执行直观数据分析成为可能。
我们对上面研究的 MQL 应用程序略微进行一些修改和补充。 若要为每个表格单元格设置独有颜色,需禁用斑马纹格式。 注释掉这段代码。
// m_table.IsZebraFormatRows(clrWhiteSmoke);
现在,我们为表格的格式化创建 CProgram::SetColorsToTable() 方法。 CColors 类用于处理颜色。 它已经存在于创建 GUI 的函数库当中,因此不需要在项目中包含该文件。 声明两个工作数组:(1)用于获得渐变颜色的数组,和(2)要形成渐变颜色的数组。 我们将创建 三色渐变。 数值越低,颜色越红(clrTomato)。 数值越高,它变得越蓝(clrCornflowerBlue)。 我们添加白色来分隔这两个颜色区域。
定义数值范围的大小,从最小值到最大值。 这将是渐变数组的大小。 CColors::Gradient() 方法用于设置数组大小并填充。 表格单元格的颜色在最终循环中设置。 为了不超出数组范围,索引的计算为单元格值减去范围最小值。 在方法结束时,表格将被更新,以便显示已施加的变化。
class CProgram : public CWndCreate { private: //--- 用单元格的背景颜色填充表格 void SetColorsToTable(void); }; //+------------------------------------------------------------------+ //| 格式化表格 | //+------------------------------------------------------------------+ void CProgram::SetColorsToTable(void) { //--- 用于处理颜色 CColors clr; //--- 用于接收渐变的数组 color out_colors[]; //--- 三色渐变 color colors[3]={clrTomato,clrWhite,clrCornflowerBlue}; //--- 查找最低和最高表格数值 double max =0; double min =100; for(uint c=1; c<(uint)m_table.ColumnsTotal(); c++) { for(uint r=0; r<(uint)m_table.RowsTotal(); r++) { max =::fmax(max,(double)m_table.GetValue(c,r)); min =::fmin(min,(double)m_table.GetValue(c,r)); } } //--- 调整到最接近的较低整数 max =::floor(max); min =::floor(min); //--- 获得范围 int range =int(max-min)+1; //--- 获得颜色渐变数组 clr.Gradient(colors,out_colors,range); //--- 设置单元格的背景颜色 for(uint c=1; c<(uint)m_table.ColumnsTotal(); c++) { for(uint r=0; r<(uint)m_table.RowsTotal(); r++) { int index=(int)m_table.GetValue(c,r)-(int)min; m_table.BackColor(c,r,out_colors[index],true); } } //--- 更新表格 m_table.Update(); }
您可以在下面看到 GUI 中的模样。 在这种情况下,结果表明,数值越小,所考察区域的趋势数量越少。 为了尽可能多的利用数据获取信息,明智的做法是尽可能放宽日期设置。
图例 12. 可视化表格数据的色标
日期范围越宽,可利用的数据越多,因此生成数据和计算参数所需的时间就越多。 如果没有足够的数据,则尝试从服务器下载它们。
现在,我们开发一个程序来计算线段数量。 复制上一节中的 EA 并对其进行必要的修改和添加。 这里会有两张表格。 第一张仅用到一列,它是所分析的品种列表。 第二张用到两个数据列:(1)增加点数的范围,和(2)第一列中指定范围内的线段数量。 下面您可以看到在应用程序加载到图表后 GUI 的外观。
图例 13. 按大小计算线段数量的程序
请求 按钮遵照指定的过滤器请求品种列表。 单击 计算 时,将收集指定时间范围内的数据并将其分发到第二张表格中。
基本上,所有方法都与之前的 EA 保持一致,所以我们只考虑与第二张表格相关的事情。 首先,我们需要接收指标数据。 这是在 CProgram::GetIndicatorData() 方法中完成的。 最初,我们连接到 ZigZag 指标,然后 在指定的时间范围内获取其数据。 品种、时间帧和获取的指标线段数量显示在状态栏中。
class CProgram : public CWndCreate { private: //--- 获取指标数据 void GetIndicatorData(const string symbol,const ENUM_TIMEFRAMES period); }; //+------------------------------------------------------------------+ //| 获取指标数据 | //+------------------------------------------------------------------+ void CProgram::GetIndicatorData(const string symbol,const ENUM_TIMEFRAMES period) { //--- 获取指标句柄 string path ="::Indicators\\Custom\\ZigZag\\ExactZZ_Plus.ex5"; int handle =::iCustom(symbol,period,path,0,0); if(handle!=INVALID_HANDLE) { //--- 复制指定范围内的数据 datetime start_time =m_from_date.SelectedDate(); datetime end_time =m_to_date.SelectedDate(); m_zz.GetZigZagData(handle,2,3,symbol,period,start_time,end_time); //--- 在状态栏中显示数据 string text="["+symbol+","+(string)GetPeriodName(period)+"] - Segments total: "+(string)m_zz.SegmentsTotal(); m_status_bar.SetValue(0,text); m_status_bar.GetItemPointer(0).Update(true); } //--- 释放指标 ::IndicatorRelease(handle); }
按照指定步长计算第一列的价格范围。 CProgram::GetLevels() 方法即用于此。 为了定义范围数量,我们应首先取得所采集数据集合中的最大线段大小。 接下来,在循环中按指定步长填充数组,直至达到最大值。
class CProgram : public CWndCreate { private: //--- 数组范围 int m_levels_array[]; //--- private: //--- 获取级别 void GetLevels(void); }; //+------------------------------------------------------------------+ //| 获取级别 | //+------------------------------------------------------------------+ void CProgram::GetLevels(void) { //--- 释放数组 ::ArrayFree(m_levels_array); //--- 获取最大线段大小 int max_value=int(m_zz.LargestSegment()/m_symbol.Point()); //--- 用级别填充数组 int counter_levels=0; while(true) { int size=::ArraySize(m_levels_array); ::ArrayResize(m_levels_array,size+1); m_levels_array[size]=counter_levels; //--- if(counter_levels>max_value) break; //--- counter_levels+=(int)m_step.GetValue(); } }
CProgram::SetDataToTable2() 方法会用数据填充第二张表格。 在最开始处,将检查品种在第一张表格得列表中是否高亮显示。 如果不是,则程序退出,并发送消息到智能系统日志。 如果出于第一张表格中高亮显示行,则定义品种并获取其数据。 此后,调用上述方法接收指标数据并计算级别。 我们接收的指标数据与启动 EA 的时间帧相同。
当我们知道级别的数量时,我们可以构建一个所需大小的表格并用那些数值填充它。 首先,使用范围数值填充第一列。 之后,填充第二列。 顺序遍历所有范围,增加适合此范围的线段的单元格中的计数器。
class CProgram : public CWndCreate { private: //--- 用数据填写第二张表格 void SetDataToTable2(void); }; //+------------------------------------------------------------------+ //| 用数据填写第二张表格 | //+------------------------------------------------------------------+ void CProgram::SetDataToTable2(void) { //--- 如果不是高亮显示得行,则退出 if(m_table1.SelectedItem()==WRONG_VALUE) { ::Print(__FUNCTION__," > Select a symbol in the table on the left!"); return; } //--- 进度开始 StartProgress(); //--- 隐藏表格 m_table2.Hide(); //--- 从第一张表格中获取品种 string symbol=m_table1.GetValue(0,m_table1.SelectedItem()); m_symbol.Name(symbol); //--- 获取指标数据 GetIndicatorData(symbol,_Period); //--- 获取级别 GetLevels(); //--- 重新构建表格 RebuildingTable2(); //--- 在第一列中设置范围 for(uint r=0; r<(uint)m_table2.RowsTotal(); r++) m_table2.SetValue(0,r,(string)m_levels_array[r],0); //--- 获取第二列的值 int items_total=::ArraySize(m_levels_array); int segments_total=m_zz.SegmentsTotal(); for(int i=0; i<items_total-1; i++) { //--- 进度 m_progress_bar.LabelText("Get data ["+(string)m_levels_array[i]+"]..."); m_progress_bar.Update(i,m_table2.RowsTotal()); //--- for(int s=0; s<segments_total; s++) { int size=int(m_zz.SegmentSize(s)/m_symbol.Point()); if(size>m_levels_array[i] && size<m_levels_array[i+1]) { int value=(int)m_table2.GetValue(1,i)+1; m_table2.SetValue(1,i,(string)value,0); } } } //--- 显示表格 m_table2.Update(true); //--- 结束进度 EndProgress(); }
举例,我们在 M5 图表上接收自 2010 年开始至今的 EURUSD 线段。 设置范围,步长为 100,此值针对五位小数报价系统。 结果显示在下面的屏幕截图中。
线段的总数是 302145。 正如我们所看到的,最大线段数量在 0 到 100 范围内。 进而,线段的数量从一个量级降低到另一个量级。 在指定的时间段内,最大线段大小达到 2400,此值针对五位小数报价系统。
图例 14. 按大小计算线段数量的结果
了解线段的持续时间如何形成分组也很好。 若要查找任何形态,我们需要获得所分析数据的所有统计数据。 我们开发另一个 EA 版本。 只需复制上一节中的程序,然后将另一个表格添加到 GUI。 该表格拥有两列:(1)柱线数量,和(2)该柱线数量内的线段数量。 下面您可以看到在应用程序加载到图表后 GUI 的外观。
图例 15. 按持续时间计算线段数量的程序
在所有表格中接收数据的动作序列如下:
下列提供了用于接收数据和填充第三张表格的 CProgram::SetDataToTable3() 方法的代码。 此处高亮显示的行作为接收范围,其中线段的数量将按其持续时间计算。 表格中的行数由获得的数据集合中的最长(以柱线为单位)线段定义。 当填充表格的第二列时,遍历所有行并计算相应 所选范围和线段数量 的线段。
class CProgram : public CWndCreate { private: //--- 用数据填充表格三 void SetDataToTable3(void); }; //+------------------------------------------------------------------+ //| 用数据填充表格三 | //+------------------------------------------------------------------+ void CProgram::SetDataToTable3(void) { //--- 如果不是高亮显示得行,则退出 if(m_table2.SelectedItem()==WRONG_VALUE) { ::Print(__FUNCTION__," > Select a range in the table on the left!"); return; } //--- 进度开始 StartProgress(); //--- 隐藏表格 m_table3.Hide(); //--- 获取高亮显示的行 int selected_row_index=m_table2.SelectedItem(); //--- 范围 int selected_range=(int)m_table2.GetValue(0,selected_row_index); //--- 重新构建表格 RebuildingTable3(); //--- 在第一列中设置数值 for(uint r=0; r<(uint)m_table3.RowsTotal(); r++) m_table3.SetValue(0,r,(string)(r+1),0); //--- 获取第二列的值 int segments_total=m_zz.SegmentsTotal(); for(uint r=0; r<(uint)m_table3.RowsTotal(); r++) { //--- 进度 m_progress_bar.LabelText("Get data ["+(string)r+"]..."); m_progress_bar.Update(r,m_table3.RowsTotal()); //--- for(int s=0; s<segments_total; s++) { int size =int(m_zz.SegmentSize(s)/m_symbol.Point()); int bars =m_zz.SegmentBars(s); //--- if(size>selected_range && size<selected_range+(int)m_step.GetValue() && bars==r+1) { int value=(int)m_table3.GetValue(1,r)+1; m_table3.SetValue(1,r,(string)value,0); } } } //--- 显示表格 m_table3.Update(true); //--- 进度结束 EndProgress(); }
高亮显示表格和列表行时,将生成 ON_CLICK_LIST_ITEM 自定义事件。 在这种情况下,我们使用 第二张表格的 ID 跟踪事件触发。
//+------------------------------------------------------------------+ //| 事件处理器 | //+------------------------------------------------------------------+ void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { ... //--- 单击行的事件 if(id==CHARTEVENT_CUSTOM+ON_CLICK_LIST_ITEM) { //--- 单击表格行 if(lparam==m_table2.Id()) { //--- 获取第三张表格的数据 SetDataToTable3(); return; } //--- return; } ... }
当第一张表格中接收新的品种列表,或计算新高亮显示的品种数据时,应从表格中清除以前计算的无关数据,以便避免混淆当前显示的数据。
//+------------------------------------------------------------------+ //| 事件处理器 | //+------------------------------------------------------------------+ void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { //--- 单击按钮的事件 if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON) { //--- 点击请求按钮 if(lparam==m_request.Id()) { //--- 获取第一张表格的数据 SetDataToTable1(); //--- 从表格中删除不相关的数据 m_table2.DeleteAllRows(true); m_table3.DeleteAllRows(true); return; } //--- 单击计算按钮 if(lparam==m_calculate.Id()) { //--- 获取第二张表格的数据 SetDataToTable2(); //--- 从表格中删除不相关的数据 m_table3.DeleteAllRows(true); } //--- return; } ... }
在图表上启动 EA 后,我们获得如下所示的结果。 在这种情况下,我们形成了以 USD 为参照的货币对列表。 之后收到了从 2018 年开始的 GBPUSD 数据,并且按 100 的步长形成了范围列表(第二张表格),并逐一计算了它们的线段数量。 例如,在第二张表格中高亮显示范围 200 的行和 1922(从 200 到 300)的线段数量。 第三张表格显示第二张表格中高亮显示的范围内所有线段的持续时间。 例如,我们可以看到,在此期间,在指定范围内只有持续时间为 10 根柱线的 11 个 线段出现在 GBPUSD 上。
图例 16. 按持续时间计算线段数的结果
作为补充,我愿展示 MQL 程序中 GUI 如何正确处理图表品种和时间帧的变化事件。 由于 GUI 可能包含多个不同的控件,因此加载和初始化整个集合可能需要一些时间。 有时,可以节省此时间,这与变更图表品种和时间帧完全相同。 在此,不需要一遍又一遍地持续删除和创建 GUI。
这可以通过以下方式实现:
创建一个字段来保存程序主类中逆始化的最终原因:
class CProgram : public CWndCreate { private: //--- 逆初始化的最终原因 int m_last_deinit_reason; }; //+------------------------------------------------------------------+ //| 构造函数 | //+------------------------------------------------------------------+ CProgram::CProgram(void) : m_last_deinit_reason(WRONG_VALUE) { }
在逆初始化期间,除了原因为 REASON_CHARTCHANGE 的 GUI 之外,其它所有情况下都会删除 GUI。
//+------------------------------------------------------------------+ //| 逆初始化 | //+------------------------------------------------------------------+ void CProgram::OnDeinitEvent(const int reason) { //--- 记住最后的逆初始化原因 m_last_deinit_reason=reason; //--- 如果原因与变更品种和周期无关,则删除 GUI if(reason!=REASON_CHARTCHANGE) { CWndEvents::Destroy(); } }
由于 GUI 是初始化过程中调用 CProgram::CreateGUI() 方法创建的,那么现在检查逆初始化的最后原因就足以了。 如果原因是品种或时间正变更,则无需重新创建 GUI。 代之,只需退出方法,并通知 一切正常。
//+------------------------------------------------------------------+ //| 创建 GUI | //+------------------------------------------------------------------+ bool CProgram::CreateGUI(void) { //--- 如果图表或时间帧已变更,则退出 if(m_last_deinit_reason==REASON_CHARTCHANGE) return(true); ... return(true); }
ZigZag 不适合生成交易信号的观念在交易论坛上广泛传播。 这有很大的误解。 事实上,没有其他指标能提供如此多的信息来判断价格行为的性质。 现在,您可以使用工具轻松获取所有必需的 ZigZag 指标数据,以便进行更详尽的分析。
在下一部分中,我将展示如何利用文章中开发的这些工具来获得其他数据。
文件名称 | 注释 |
---|---|
MQL5\Indicators\Custom\ZigZag\FrequencyChangeZZ.mq5 | 用于计算反向 ZigZag 指标线段形成频率的指标 |
MQL5\Indicators\Custom\ZigZag\SumSegmentsZZ.mq5 | 用于计算所获集合的线段总合及其平均值的指标 |
MQL5\Indicators\Custom\ZigZag\PercentageSegmentsZZ.mq5 | 线段百分比总合及它们之间差值的指标 |
MQL5\Indicators\Custom\ZigZag\MultiPercentageSegmentsZZ.mq5 | 利用较高时间帧的反向线段总合的百分比之间的差值来定义若干已形成线段性质的指标 |
MQL5\Experts\ZigZag\TestZZ_05.mq5 | 用于测试 MultiPercentageSegmentsZZ 指标的 EA |
MQL5\Experts\ZigZag\ZZ_Scanner_01.mq5 | 用于测试 PercentageSegmentsZZ 指标的 EA |
MQL5\Experts\ZigZag\ZZ_Scanner_02.mq5 | 用于计算不同价格范围内线段的 EA |
MQL5\Experts\ZigZag\ZZ_Scanner_03.mq5 | 用于计算位于不同价格范围,且具有不同持续时间线段的 EA |
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...