一个用于交易系统的合适指标通常是通过在图表上使用各种参数监视各种指标后选出来的,如果您通过导航窗口来拖拽测试每个指标,并且每次都通过指标的属性窗口来改变它的参数,这个过程将花费很多时间,有一种方法可以加速这个过程。
它包含了创建一个图形界面来直接从图表上访问,使得用户快速修改指标参数并马上看到新的结果。可以通过在一个带有图形界面的通用指标中组合各种指标来实现指标的快速切换。
分析问题
创建一个通用指标的任务并不是很难,它需要一点面向对象的编程: 一个基类和一些相同类型的子类。
每个特定指标的参数将通过子类的构造函数来传递。在这种情况下,当创建一个对象时,MetaEditor 会有一个参数列表的提示,它将非常有助于开发过程(图1)
图 1. 当创建一个对象时构造函数参数的提示
主要的困难将在实际使用这样的指标时出现,不同的震荡指标有不同的外部参数设置,如果我们为每个震荡指标独立出参数并且为它们使用不同的前缀, 我们将能够人工使用这些指标,但是它将不适合用在iCustom()或者IndicatorCreate()函数中,因为参数太多了。对于 IndicatorCreate(),传入参数的数量限制是256,而对于 iCustom() 是64。这个值还包含了通用的参数,例如交易品种和指标名称,所以实际可用的参数数量甚至比它还少。我们可以也使用少一些的通用参数集,但是指标在这种情况下使用将会不方便: 我们将需要检查引用来指导对于特定的指标来说使用哪些参数,
图形界面可以解决这个问题: 它的对话框可以使用特定的控件来用于选定的指标。我们也应该提供功能来使用 iCustom() 或者 IndicatorCreate() 来调用指标, 这样指标属性窗口将有少量的通用外部参数。
参数集
让我们定义所需的最小外部参数集合,在终端中查看震荡指标的列表: 主菜单 - 插入 - 指标 - 震荡指标, 把它们加到表格中。
表格 1. 终端中所有的震荡指标
函数 | 名称 | 缓冲区 | 参数 |
---|---|---|---|
iATR | 平均真实范围 | 1. 线形 | 1. int ma_period — 平均周期数 |
iBearsPower | 空头力度 | 1. 柱形 | 1. int ma_period — 平均周期数 |
iBullsPower | 多头力度 | 1. 线形 | 1. int ma_period — 平均周期数 |
iCCI | 商品通道指数 | 1. 线形 | 1. int ma_period — 平均周期数 2. ENUM_APPLIED_PRICE applied_price — 价格类型 |
iChaikin | 蔡金(Chaikin)震荡指标 | 1. 线形 | 1. int fast_ma_period — 快速平均周期数 2. int slow_ma_period — 慢速平均周期数 3. ENUM_MA_METHOD ma_method — 平滑类型 4. ENUM_APPLIED_VOLUME applied_volume — 使用的交易量 |
iDeMarker | DeM指标 | 1. 线形 | 1. int ma_period — 平均周期数 |
iForce | 强力指数 | 1. 线形 | 1. int ma_period — 平均周期数 2. ENUM_MA_METHOD ma_method — 平滑类型 3. ENUM_APPLIED_VOLUME applied_volume — 用于计算的交易量类型 |
iMomentum | 动量 | 1. 线形 | 1. int mom_period — 平均周期数 2. ENUM_APPLIED_PRICE applied_price — 价格类型 |
iMACD | 移动平均汇总/分离指标(MACD) | 1. 柱形 2. 线形 | 1. int fast_ema_period — 快速移动平均周期数 2. int slow_ema_period — 慢速移动平均周期数 3. int signal_period — 差别平均周期数 4. ENUM_APPLIED_PRICE applied_price — 价格类型 |
iOsMA | 移动平均震荡指标(OsMA, MACD 柱形图) | 1. 柱形 | 1. int fast_ema_period — 快速移动平均周期数 2. int slow_ema_period — 慢速移动平均周期数 3. int signal_period — 差别平均周期数 4. ENUM_APPLIED_PRICE applied_price — 价格类型 |
iRSI | 相对强度指数 | 1. 线形 | 1. int ma_period — 平均周期数 2. ENUM_APPLIED_PRICE applied_price — 价格类型 |
iRVI | 相对动量指数 | 1. 线形 2. 线形 | 1. int ma_period — 平均周期数 |
iStochastic | 随机震荡指标 | 1. 线形 2. 线形 | 1. int Kperiod — 用于计算的柱数 2. int Dperiod — 首要平滑周期数 3. int slowing — 最终平滑周期数 4. ENUM_MA_METHOD ma_method — 平滑类型 5. ENUM_STO_PRICE price_field — 随机震荡计算方法 |
iTriX | 三重指数平均线(TRIX) | 1. 线形 | 1. int ma_period — 平均周期数 2. ENUM_APPLIED_PRICE applied_price — 价格类型 |
iWPR | 威廉姆斯百分比范围 | 1. 线形 | 1. int calc_period — 平均周期数 |
根据参数列,我们创建一个含有所有参数类型的列表,然后确定它们的最大数量。
表格 2. 参数的类型和数量
类型 | 数量 |
---|---|
int | 3 |
ENUM_APPLIED_PRICE | 1 |
ENUM_MA_METHOD | 1 |
ENUM_APPLIED_VOLUME | 1 |
ENUM_STO_PRICE | 1 |
从缓冲区列,您可以看到可以使用两个指标缓冲区,并且不同的指标可以有不同的绘图方式。当然,我们可以把它们都画成线形,但是它们其中的一些通常显示为柱形图,并且终端也允许这样,所以我们会试着提供功能在更换指标时修改对应的绘图类型选项。另外,我们需要提供绘制水平线的功能,因为有些指标需要加上它们 (RSI, CCI, 等等.)
计划
一个大的通用任务能够分成小的独立任务的数量越多,它的实现就会越简单方便,所以,我们的工作将包括三个阶段:
- 为通用震荡指标创建类,并创建不包含GUI(图形用户界面)的震荡指标。
- 创建用于GUI的类。
- 把通用震荡指标和图形界面整合到一起。
其中重要的一点是,您应当注意包括默认设置,我们应该提供功能来同时使用图形界面或者属性窗口来配置指标的参数(为了使得通用指标有最大的灵活性),当在属性窗口中配置参数时,它包含了一小组通用参数,我们需要保证,所有的默认设置的参数能够提供指标的自然外观。
考虑不同震荡指标的默认值。例如, 随机震荡指标的周期数为 5, 3, 3 (第一个参数比第二个大), 而 MACD 使用的是 12, 26, 9 (第一个参数比第二个小),MACD 的第一个参数意思是快速移动平均的周期数, 而第二个参数是慢速移动平均的周期数,所以第一个参数必须小于第二个。对于蔡金震荡指标,第一个参数和第二个的比例很重要 (也使用了快速和慢速移动平均的周期数),而对于随机震荡指标,这个比例就没有那么重要,并且它对应了价格变化,可以是任何数值。如果我们把MACD的第一个参数设置得大于第二个,指标的方向就会和价格变化相反 (在设置默认参数时我们应当记住这一点)。
当使用图形界面时,指标应该使用常用的默认参数集来开始运行: MACD 使用周期数为 12, 26, 9, 随机震荡使用的周期数为 5, 3, 3, 等等。另外,最好可以使新选择的指标可以选择从默认参数开始运行或者以与之前指标相同的参数开始运行。例如,我们分析 RSI 和 CCI, 并且我们想看到不同的指标在使用相同参数时线形的变化,所以我们在实现类的时候要提供这种功能。
创建指标的基类
让我们在 Include 文件夹下创建一个新的文件夹 'UniOsc' ,所有增加的指标文件都位于这个新文件夹之中。使用的震荡指标集合在表格1种已经定义过,让我们创建一个对应的枚举来选择震荡指标类型,除了指标文件,我们可能在其他地方也要使用这个枚举,所以我们把它加到独立的文件 UniOscDefines.mqh 中(在文件夹 'UniOsc'下):
enum EOscUnyType{
OscUni_ATR,
OscUni_BearsPower,
OscUni_BullsPower,
OscUni_CCI,
OscUni_Chaikin,
OscUni_DeMarker,
OscUni_Force,
OscUni_Momentum,
OscUni_MACD,
OscUni_OsMA,
OscUni_RSI,
OscUni_RVI,
OscUni_Stochastic,
OscUni_TriX,
OscUni_WPR
};
这个文件将不再加入其他内容。
让我们为指标文件创建 "CUniOsc.mqh",并在其中写上 COscUni 类的模板:
class COscUni{
protected:
public:
};
'protected' 部分是由模板确定的,因为有些类成员需要被保护,但是还是应当可以在子类中访问 ( 'private' 部分的成员是被保护的,不能在子类中找到 )。
基类的主要方法是对应着指标的 OnCalculate() 函数的,让我们称它为 Calculate(),这个方法的前两个参数对应着 OnCalculate() 的相应参数: rates_total (柱的总数) 和 prew_calculate (已经计算过的柱数),不需要把数组传给 Calculate() 方法了, 因为使用了另外指标的数据。但是我们需要传入两个指标缓冲区,它们会填入数据。即使在使用只含一个缓冲区的指标时,我们将需要控制第二个缓冲区,所以在任何情况下都会给 Calculate() 方法传入两个缓冲区。Calculate() 的代码将依赖于使用的震荡指标类型: 它可以是含有一个或者两个缓冲区。所以,Calculate() 方法将会是虚函数:
virtual int Calculate( const int rates_total,
const int prev_calculated,
double & buffer0[],
double & buffer1[]
){
return(rates_total);
}
当载入不同类型的指标时,我们将需要一个变量来保存指标的句柄。我们在 protected 部分声明它,
另外,我们还需要更多的变量来用于不同的缓冲区显示属性。这些属性将在载入每个指标时确定,也就是这些变量将在子类中设置:
int m_handle; // 指标句柄
int m_bufferscnt; // 使用的缓冲区数量
string m_name; // 指标名称
string m_label1; // 缓冲区1的名称
string m_label2; // 缓冲区2的名称
int m_drawtype1; // 缓冲区1的绘图类型
int m_drawtype2; // 缓冲区2的绘图类型
string m_help; // 指标参数的提示
int m_digits; // 指标值中小数点位数
int m_levels_total; // 水平总数
double m_level_value[]; // 水平数值的数组
我们需要检查指标是否被成功载入,所以我们需要对应的方法来检查指标的句柄:
bool CheckHandle(){
return(m_handle!=INVALID_HANDLE);
}
如果我们通过图形界面修改震荡指标,我们需要确定指标是否已经完成了计算,这可以通过使用 BarsCalculated() 函数来做到, 调用需要指标的句柄,所以,我们加上一个方法来取得句柄:
int Handle(){
return(m_handle);
}
在类的构造函数中我们需要初始化句柄,在析构函数中要检查句柄,如有必要则调用 IndicatorRelease() :
void COscUni(){
m_handle=INVALID_HANDLE;
}
void ~COscUni(){
if(m_handle!=INVALID_HANDLE){
IndicatorRelease(m_handle);
}
}
让我们提供对决定各种指标显示的变量的访问,并创建方法来取得它们的数值:
string Name(){ // 震荡指标的名称
return(m_name);
}
int BuffersCount(){ // 震荡指标缓冲区的数量
return(m_bufferscnt);
}
string Label1(){ // 第一个缓冲区的名称
return(m_label1);
}
string Label2(){ // 第二个缓冲区的名称
return(m_label2);
}
int DrawType1(){ // 第一个缓冲区的绘图类型
return(m_drawtype1);
}
int DrawType2(){ // 第二个缓冲区的绘图类型
return(m_drawtype2);
}
string Help(){ // 使用参数的提示
return(m_help);
}
int Digits(){ // 指标值的小数点位数
return(m_digits);
}
int LevelsTotal(){ // 指标水平的数量
return(m_levels_total);
}
double LevelValue(int index){ // 根据指定的索引取得水平的数值
return(m_level_value[index]);
}
所有这些方法都返回对应变量的值,并且变量的值应当在震荡指标的子类中赋值。
'Calculate(计算)'的子类
让我们创建两个子类: 用于一个缓冲区的指标和两个缓冲区的指标。对于一个缓冲区的指标:
class COscUni_Calculate1:public COscUni{
public:
void COscUni_Calculate1(){
m_bufferscnt=1;
}
virtual int Calculate( const int rates_total,
const int prev_calculated,
double & buffer0[],
double & buffer1[]
){
int cnt,start;
if(prev_calculated==0){
cnt=rates_total;
start=0;
}
else{
cnt=rates_total-prev_calculated+1;
start=prev_calculated-1;
}
if(CopyBuffer(m_handle,0,0,cnt,buffer0)<=0){
return(0);
}
for(int i=start;i<rates_total;i++){
buffer1[i]=EMPTY_VALUE;
}
return(rates_total);
}
};
让我们讨论这个类,这个类的构造函数是 COscUni_Calculate1, 缓冲区的数量 (在本例中是 1) 是在构造函数中设置的。需要复制的缓冲区元件数量('cnt'变量)和我们需要清空的第二个缓冲区的起始柱的索引('start'变量)就在 Calculate() 方法中计算, 它依赖于 rates_total 和 prev_calculate 变量的值。如果数据的复制失败(当调用 CopyBuffer()),方法会返回0,这样可以在下一个订单时刻的最开始进行全部计算,在方法的最后返回 rates_total。
用于含有两个缓冲区的指标的类:
class COscUni_Calculate2:public COscUni{
public:
void COscUni_Calculate2(){
m_bufferscnt=2;
}
virtual int Calculate( const int rates_total,
const int prev_calculated,
double & buffer0[],
double & buffer1[]
){
int cnt;
if(prev_calculated==0){
cnt=rates_total;
}
else{
cnt=rates_total-prev_calculated+1;
}
if(CopyBuffer(m_handle,0,0,cnt,buffer0)<=0){
return(0);
}
if(CopyBuffer(m_handle,1,0,cnt,buffer1)<=0){
return(0);
}
return(rates_total);
}
};
这个类比单个缓冲区指标的类还要更简单,在 Calculate() 方法的最开始计算需要复制的元件数量('cnt'变量),然后复制缓冲区。
指标的子类
现在我们创建用于震荡指标的子类。这些将使 COscUni_Calculate1 或者 COscUni_Calculate2 的子类,所有这些类都将只有一个构造函数。对应着震荡指标的参数和一些额外参数都将会传给每个类的构造函数,额外的参数将会决定如何使用传入构造函数的参数或者用于设置默认值 ('use_default' 变量)。第二个参数 keep_previous 决定了是设置所有指标参数的默认值还是只设置那些还没有被使用的参数。
列表中第一个指标是 ATR, 让我们先为它开始写一个子类。首先,我们使用类的模板:
class COscUni_ATR:public COscUni_Calculate1{
public:
void COscUni_ATR(bool use_default,bool keep_previous,int & ma_period){
}
};
请注意,ma_period 参数是通过引用传入的,是为了当设置指标的默认参数时可以在通用震荡指标中访问这些参数值。
在构造函数中写下代码:
if(use_default){
if(keep_previous){
if(ma_period==-1)ma_period=14;
}
else{
ma_period=14;
}
}
如果 use_default=true, 在这部分代码中会设置默认值。如果 keep_previous=true, 则只会在参数等于 -1 时,也就是之前没有被使用过时,才设置默认值。所以,在通用震荡指标的初始化过程中,我们需要把所有参数的值设为 -1 。
现在让我们分析在子类的构造函数中最重要的代码行,它包含了指标的载入:
m_handle=iATR(Symbol(),Period(),ma_period);
加上几行代码来设置显示参数:
m_name=StringFormat("ATR(%i)",ma_period); // 指标名称
m_label1="ATR"; // 缓冲区名称
m_drawtype1=DRAW_LINE; // 绘制类型
m_help=StringFormat("ma_period - Period1(%i)",ma_period); // 提示
m_digits=_Digits+1; // 指标值的小数点位数
m_levels_total=0; // 水平的数量
让我们分析在一个更加复杂的指标 MACD 中子类的一些创建步骤,创建原则是一样的,尽管在这种情况下需要更多的代码。所以,让我们考虑分段,设置默认参数:
if(use_default){
if(keep_previous){
if(fast_ema_period==-1)fast_ema_period=12;
if(slow_ema_period==-1)slow_ema_period=26;
if(signal_period==-1)signal_period=9;
if(applied_price==-1)applied_price=PRICE_CLOSE;
}
else{
fast_ema_period=12;
slow_ema_period=26;
signal_period=9;
applied_price=PRICE_CLOSE;
}
}
设置显示参数:
m_handle=iMACD(Symbol(),
Period(),
fast_ema_period,
slow_ema_period,
signal_period,
(ENUM_APPLIED_PRICE)applied_price);
m_name=StringFormat( "iMACD(%i,%i,%i,%s)",
fast_ema_period,
slow_ema_period,
signal_period,
EnumToString((ENUM_APPLIED_PRICE)applied_price));
m_label1="Main";
m_label2="Signal";
m_drawtype1=DRAW_HISTOGRAM;
m_drawtype2=DRAW_LINE;
m_help=StringFormat( "fast_ema_period - Period1(%i), "+
"slow_ema_period - Period2(%i), "+
"signal_period - Period3(%i), "+
"applied_price - Price(%s)",
fast_ema_period,
slow_ema_period,
signal_period,
EnumToString((ENUM_APPLIED_PRICE)applied_price));
m_digits=_Digits+1;
构造函数的参数:
void COscUni_MACD(bool use_default,
bool keep_previous,
int & fast_ema_period,
int & slow_ema_period,
int & signal_period,
long & applied_price
){
请注意,所使用的 applied_price 在标准的 ENUM_APPLIED_PRICE 枚举中声明为长整形(long),这使得可以把这个变量设为 -1 以指示该参数还没有被使用。
让我们看一下用于RSI指标的类的另一端代码,它包含了设置水平的代码部分:
m_levels_total=3;
ArrayResize(m_level_value,3);
m_level_value[0]=30;
m_level_value[1]=50;
m_level_value[2]=70;
它设置水平的数量,修改数组的大小并填充水平的数值。
我不会在这里描述其他的震荡指标类是如何创建的,文章的附件中包含了完整可用的震荡指标集(CUniOsc.mqh 文件)。
创建一个通用的震荡指标(开始)
震荡指标类已经准备好了,我们就可以创建一个通用的震荡指标,尽管现在还没有震荡指标的图形界面。
创建一个新的指标,也就是 "iUniOsc",然后,在指标创建向导中选择函数类型 OnCalculate(...open,high,low,close), 创建一个外部变量 (这样可以更容易发现在哪里放置外部变量) 和两个线类型的缓冲区。
在外部变量之前,我们需要包含含有枚举和震荡指标类的文件:
#include <UniOsc/UniOscDefines.mqh>
#include <UniOsc/CUniOsc.mqh>
创建一个外部变量用于选择震荡指标类型:
input EOscUnyType Type = OscUni_ATR;
UseDefault 和 KeepPrevious 变量:
input bool UseDefault = true;
input bool KeepPrev = true;
用于震荡指标参数的通用变量:
input int Period1 = 14;
input int Period2 = 14;
input int Period3 = 14;
input ENUM_MA_METHOD MaMethod = MODE_EMA;
input ENUM_APPLIED_PRICE Price = PRICE_CLOSE;
input ENUM_APPLIED_VOLUME Volume = VOLUME_TICK;
input ENUM_STO_PRICE StPrice = STO_LOWHIGH;
有些指标画一条线,其他的画两条线。第一个缓冲区有时候显示为线,有时候画成柱形图。我们将画明亮的线形,而柱形图是灰色的,所以我们创建三个用于颜色的变量:
input color ColorLine1 = clrLightSeaGreen;
input color ColorLine2 = clrRed;
input color ColorHisto = clrGray;
因为我们将要创建一个GUI,它将可以使得不重新启动指标就能改变震荡指标的类型和参数,所以让我们创建 Type 变量的副本和用于指标参数的变量:
int _Period1;
int _Period2;
int _Period3;
long _MaMethod;
long _Price;
long _Volume;
long _StPrice;
EOscUnyType _Type;
让我们声明一个指针变量用于通用震荡指标对象:
COscUni * osc;
还有更多一些变量要声明:
string ProgName;
string ShortName;
这些变量将用于生成指标的名称来显示在子窗口的左上角。
现在我们将在 OnInit() 函数的末尾加上代码, 但是首先我们需要做些准备。我们还要根据 UseDefault 和 KeepPrevious 的值来准备震荡指标的参数 (并给 _Type 变量赋值), 把它写成一个函数,这样使代码结构更好。
void PrepareParameters(){
_Type=Type;
if(UseDefault && KeepPrev){
_Period1=-1;
_Period2=-1;
_Period3=-1;
_MaMethod=-1;
_Volume=-1;
_Price=-1;
_StPrice=-1;
}
else{
_Period1=Period1;
_Period2=Period2;
_Period3=Period3;
_MaMethod=MaMethod;
_Volume=Volume;
_Price=Price;
_StPrice=StPrice;
}
}
如果使用了 UseDefault 和 KeepPrevious, 所有的变量都赋值为 -1, 这样我们在类的构造函数中就可以看到我们没有使用过的变量,然后只把它们设为默认值。来自属性窗口的数值将会在其他条件下赋值,这些数值将会根据设置赋值,或者它们可以在对象创建的时候使用默认值替代,
在准备完参数以后,再载入选中的震荡指标。载入代码也写成一个函数:
void LoadOscillator(){
switch(_Type){
case OscUni_ATR:
osc=new COscUni_ATR(UseDefault,KeepPrev,_Period1);
break;
case OscUni_BearsPower:
osc=new COscUni_BearsPower(UseDefault,KeepPrev,_Period1);
break;
case OscUni_BullsPower:
osc=new COscUni_BullsPower(UseDefault,KeepPrev,_Period1);
break;
...
}
}
在载入震荡指标之后,我们需要检查句柄:
if(!osc.CheckHandle()){
Alert("指标载入错误 "+osc.Name());
return(INIT_FAILED);
}
如果成功载入,再通过对应的对象方法来接收绘制风格,这部分代码也实现为一个函数:
void SetStyles(){
// 设置风格
if(osc.BuffersCount()==2){
PlotIndexSetInteger(0,PLOT_DRAW_TYPE,osc.DrawType1());
PlotIndexSetInteger(1,PLOT_DRAW_TYPE,osc.DrawType2());
PlotIndexSetInteger(0,PLOT_SHOW_DATA,true);
PlotIndexSetInteger(1,PLOT_SHOW_DATA,true);
PlotIndexSetString(0,PLOT_LABEL,osc.Label1());
PlotIndexSetString(1,PLOT_LABEL,osc.Label2());
if(osc.DrawType1()==DRAW_HISTOGRAM){
PlotIndexSetInteger(0,PLOT_LINE_COLOR,ColorHisto);
}
else{
PlotIndexSetInteger(0,PLOT_LINE_COLOR,ColorLine1);
}
PlotIndexSetInteger(1,PLOT_LINE_COLOR,ColorLine2);
}
else{
PlotIndexSetInteger(0,PLOT_DRAW_TYPE,osc.DrawType1());
PlotIndexSetInteger(1,PLOT_DRAW_TYPE,DRAW_NONE);
PlotIndexSetInteger(0,PLOT_SHOW_DATA,true);
PlotIndexSetInteger(1,PLOT_SHOW_DATA,false);
PlotIndexSetString(0,PLOT_LABEL,osc.Label1());
PlotIndexSetString(1,PLOT_LABEL,"");
if(osc.DrawType1()==DRAW_HISTOGRAM){
PlotIndexSetInteger(0,PLOT_LINE_COLOR,ColorHisto);
}
else{
PlotIndexSetInteger(0,PLOT_LINE_COLOR,ColorLine1);
}
}
// 设置小数位数
IndicatorSetInteger(INDICATOR_DIGITS,osc.Digits());
// 设置水平
int levels=osc.LevelsTotal();
IndicatorSetInteger(INDICATOR_LEVELS,levels);
for(int i=0;i<levels;i++){
IndicatorSetDouble(INDICATOR_LEVELVALUE,i,osc.LevelValue(i));
}
}
首先,根据震荡指标的缓冲区数量,实现从两种可用的风格设置选项中选择一个,如果第一个缓冲区是一个柱形图,就设置对应的缓冲区类型,然后设置指标数值的小数点位数,最后设置水平。
这里是 OnInit() 的完整代码, 它包含了对所有新创建的函数的调用:
int OnInit(){
SetIndexBuffer(0,Label1Buffer,INDICATOR_DATA);
SetIndexBuffer(1,Label2Buffer,INDICATOR_DATA);
PrepareParameters();
LoadOscillator();
if(!osc.CheckHandle()){
Alert("载入指标时出错 "+osc.Name());
return(INIT_FAILED);
}
SetStyles();
Print("参数对应: "+osc.Help());
ShortName=ProgName+": "+osc.Name();
IndicatorSetString(INDICATOR_SHORTNAME,ShortName);
return(INIT_SUCCEEDED);
}
请注意,函数的末尾调用了 Print 函数, 它包含了属性窗口中使用的参数的提示,并且设置了短的指标名称。
现在我们创建通用震荡指标项目的第一步就完成了,也就是说,我们已经准备好了使用之前创建类的指标。下一步我们创建一个GUI类。
本文的附件中有一个准备好的指标叫做 iUniOsc (晚些时候将稍微修改一些指标代码,这样它就与当前阶段的指标有少许不同)。
图形界面的创建计划
为了创建一个图形界面,我们可以使用图形对象,包括"输入栏位(entry field)"来用于输入数字值,以及几个按钮(buttons)来用于枚举类型参数(下拉列表)。然而,这将是一种不同的方法。您可以找到各种不同的MQL5开发库来创建图形界面,这些开发库可以创建标准控件,例如对话框,带有调节按钮的输入栏位,下拉列表,等等。终端中也包含了一系列用于创建面板和对话框的标准类,"文章"部分有很多系列文章,与创建图形界面有关。
有一系列三篇文章(文章 1, 文章 2, 文章 3)描述了创建图形界面的简单而快速的方法。除了理论知识之外,这些文章创建了一个开发库,这个库可以操作图形对象并创建图形界面。所有以上这些选择都有它们的优点和缺点,当写这篇文章时都已经考虑了它们,最终,我选择了上面的最后一个选项 (incGUI 开发库)。
MetaTrader 5 一直在活跃开发和提高,所以这个库的一些控件有可能会变得过时(例如,滚动条), 但是它们还是可以使用的。为了开始使用这个开发库,下载位于 "自定义图形化控件. 第三部分. 用于 MetaTrader 5 的表单"的附件, 解压缩,并把 incGUI_v3.mqh 文件复制到终端数据文件目录的 Include 文件夹下。
表单类
图形界面的创建将在一个单独文件 "UniOscGUI.mqh" 中实现。首先我们需要包含开发库:
#include <IncGUI_v3.mqh>
编译它。现在,在编译的时候将会出现一些警告信息。增强的编译器发现了这些代码中的问题,并且允许改正它们,改正过后的 "inc_GUI_v4" 文件在文章的附件中。我们包含 IncGUI_v4.mqh 而不是 IncGUI_v4.mqh 以及 UniOscDefines.mqh。
#include <IncGUI_v4.mqh>
#include <UniOsc/UniOscDefines.mqh>
让我们把 iUniOsc 复制一份命名为 iUniOscGUI。随后,iUniOsc 指标就可以编辑,隐藏 UseDefault 和 KeepPrev 参数。它们在没有GUI的指标中是没有意义的,但是我们需要把它们设为 false:
bool UseDefault = false;
bool KeepPrev = false;
随后 iUniOsc 指标就全部完成了。
让我们继续操作 iUniOscGUI 指标,在其中包含 UniOscGUI.mqh 文件。我们需要一共包含三个文件:
#include <UniOsc/UniOscDefines.mqh>
#include <UniOsc/CUniOsc.mqh>
#include <UniOsc/UniOscGUI.mqh>
在编译了指标之后,您可以检查代码并马上在图表上看到 GUI 了。直到现在,所有的工作都是在 UniOscGUI.mqh 文件中进行的,
GUI 将以对话框的形式展现; 在上面的部分有震荡指标的下拉列表, 下面是每个震荡指标对应的一些控件。所以,在文件中我们将有一个类用于创建表单,还有一组类 (父类和几个子类)用于创建表单上的控件。
让我们从表单开始,详细的按步骤描述创建表单的过程在文章 "自定义图形控件. 第三部分. MetaTrader 5 的表单". 这里我们针对我们特定的任务进行这个过程。
1. 首先,我们需要把 CFormTemplate 类从 IncGUI_v4.mqh 文件复制到 UniOscGUI.mqh, 并且把它重命名为 CUniOscForm。
2. 设置属性。这是通过 CUniOscForm 类中的 MainProperties() 方法来实现的。让我们设置如下的属性:
void MainProperties(){
m_Name = "UniOscForm";
m_Width = FORM_WIDTH;
m_Height = 150;
m_Type = 0;
m_Caption = "UniOsc";
m_Movable = true;
m_Resizable = true;
m_CloseButton = true;
}
请注意,m_Heigh 变量设为 FORM_WIDTH。在最后一步,我们将需要找到控件正确的大小和形状,所以,让我们在文件的开头加上以下常数:
#define FORM_WIDTH 210 // 表单宽度
#define SPIN_BOX_WIDTH 110 // 调节按钮的宽度
#define COMBO_BOX_WIDTH 110 // 下拉列表的宽度
随后,表单就可以在指标中使用了。然后,我们在指标中声明一个外部变量 UseGUI,默认值为 'true' (在属性窗口的开始):
input bool UseGUI = true;
在外部变量之后,我们需要声明一个表单类的指针:
CUniOscForm * frm;
如果 UseGUI = true, 我们在指标的 OnInit() 中创建一个对象,并且通过调用设置属性的方法来进行准备:
frm=new CUniOscForm(); // 创建一个对象
frm.Init(); // 初始化
frm.SetSubWindow(0); // 创建一个显示表单的子窗口
frm.SetPos(10,30); // 设置表单的初始位置
frm.Show(); // 使表单可见
在 OnDeinit() 函数中, 我们隐藏表单并删除对象:
if(CheckPointer(frm)==POINTER_DYNAMIC){
frm.Hide();
delete(frm);
}
从 OnChartEvent() 函数中调用 Event() 方法:
void OnChartEvent(const int id,
const long &lparam,
const double &dparam,
const string &sparam)
{
frm.Event(id,lparam,dparam,sparam);
}
现在,如果您在图表上附加指标,您就会看到表单 (图 2).
图 2. 在图表上运行 iUniOscGUI 指标后创建一个表单
表单上的所有按钮都是有效的: 表单可以通过左上角的按钮移动 (点击按钮,然后指向一个新的位置再点击), 它也可以被最小化(右上角有一个长方形按钮)。点击有交叉的按钮可以关闭表单,在这种情况下指标应该从图表上删除。指标可以通过使用 ChartIndicatorDelete() 函数来删除。为了使用这个功能,您需要知道指标子窗口的索引,您可以使用 ChartWindowFind() 函数来得到, 它需要指标的短名称。
当点击了表单的关闭按钮后,Event() 方法返回1。检查返回值并且如有必要从表单上删除指标:
int win=ChartWindowFind(0,ShortName); // 寻找子窗口
ChartIndicatorDelete(0,win,ShortName); // 删除指标
ChartRedraw(); // 加快图表的重绘
现在,点击关闭表单的按钮就会从图表上删除指标了。
让我们在图表上加上主控件: 用于选择振荡指标类型的下拉列表。它可以使用 CComBox 类来创建,我们在 CUniOscForm 类中加入一些代码。声明用于对象的变量:
CComBox m_cmb_main;
然后在 OnInitEvent() 方法中调用类的 Init() 方法:
m_cmb_main.Init("cb_main",100," 选择震荡指标");
要向方法中传入控件的名称 (用于图形对象名称的前缀), 控件的宽度以及一个标签,
在 OnShowEvent() 方法中调用 Show() 方法:
m_cmb_main.Show(aLeft+10,aTop+10);
这里要指定表单中控件的位置坐标 (在表单空间的左上角有10个像素的缩进),
在 OnHideEvent() 中调用 Hide() 方法:
m_cmb_main.Hide();
在主列表中选择有变化后应该载入另一个指标,这可以通过指标文件中方便地做到, 所以震荡指标列表的 Event() 方法应该在指标的 OnChartEvent() 函数中调用,而不是在表单的 EventsHandler() 方法中调用。并且应该处理这个事件:
int me=frm.m_cmb_main.Event(id,lparam,dparam,sparam);
if(me==1){
Alert(frm.m_cmb_main.SelectedText());
}
图表事件的标准参数会传给这个方法,然后当方法返回1时,会打开一个消息框。
列表中应该使用选项填充,有几种实现方法:
- 一切都可以在表单的 OnInitEvent() 方法中完成;
- 表单中可以额外加一个方法,然后它可以从指标的 Init() 方法后调用;
- 列表的方法可以直接从指标中访问。
让我们使用第三个选项,它只需要较少的方法。首先,我们要在指标中创建一个震荡指标类型的数组: