![利用 MQL5 面向对象编程法编写"EA 交易"](https://c.mql5.com/i/articles/overlay.png)
利用 MQL5 面向对象编程法编写"EA 交易"
简介
通过第一篇文章,我们从整体上了解了 MQL5 中某“EA 交易”创建、调试及测试的基本步骤。
我们做过的每一件事都非常简单且有趣;但是,新的 MQL5 语言的功能还远远不止于此。 而在本文中,我们则会探讨面向对象法,将我们曾在第一篇文章中谈到的内容落到实处。 很多人都觉得难,但是,我向您保证,看完本文之后,您就能够编写自己的面向对象 “EA 交易”了。
我们不会重复第一篇文章中学过的某些内容,所以,如果您没读过,则建议您首先通读该文。
1. 面向对象范式
新型 MQL5 之所以比 MQL4 强大许多,其 OOP (面向对象编程)法即为原因之一。
建议 OOP 中不呈现对象的任何实现细节。 如此一来,无需变动使用该对象的代码,即可更改其实现。 也就是说,类允许程序员隐藏(亦可防止变更)其所编写的类的实现方式。
为了说得更清楚一些,我们再简单讲讲刚才提到的术语“类”和“对象”。
- 类。 类更像是数据结构的一种延伸概念,但它不再是单纯地保存数据,而是同时保存数据和函数。一个类可以包含多个变量和函数,也就是所谓的类成员。 它是数据成员与处理数据的函数的一种封装。 类则要强大得多,您可以将自己所有的“EA 交易”函数打包到一个类中。 只要您的 EA 代码中有需要,您可以随时参照这些函数。 顺便说一下,这也是本文的主旨所在。
- 对象。 对象是指某个类的某个实例。 某个类被创建之后,想要使用,必须要声明一个此类的实例。 此即谓对象。 换句话说,欲创建对象,您需要一个类。
1.1. 声明一个类
一个类,基本上都会包含您欲于其创建的某个对象的成员描述(属性与函数/方法)。 我们来看一个例子…</span
如果我们想要创建一个拥有门、座、轮胎、重量等等的对象,也可以是有启动、变速齿轮、停止和喇叭的对象,则我们需要为其编写一个类。 而门、座、轮胎、重量、启动、变速齿轮、停止和喇叭则是该类的成员。
当然,您也会注意到,这些成员已被分类:其中一些只是对象将会具备的(属性),还有一些则是对象将会执行的(操作 - 函数/方法)。 要声明类,我们需要为其构思一个非常好的描述性名称。 本例中,我们将自己的类命名为 CAR。 而我们的 CAR 类,将以上述的属性和函数作为其成员。
欲声明一个类,我们先要键入关键词 class ,后接类的名称, 再然后是一对大括号(内含类的成员)。
所以,类的基本格式即如下所示:class class_name
{
access_keyword_1:
members1;
access_keyword_2:
members2;
...
};
其中,class_name 是我们想要编写的类的一个有效标识符,而members1 和 members2 则都是该类的数据成员。
access_keyword 会指定类的各成员的访问权限。 access_keyword 可有 private(私人)、protected(受保护) 或 public(公开) 只种形式。 记住:我们正在尝试编写一个可供自己和他人使用、而且实际上不会暴露实施细节的类。 为此,访问权限是必需的。
可能会有一些我们不想从类外访问的类成员。 它们全都在私有访问部分中,利用 private 或 protected 关键词声明。 而我们想从类外访问的其它成员,则随后在公共访问部分内利用 public 关键词声明。 现在,我们新的 CAR 类如下所示:
class CAR { private: int doors; int sits; int tyres; double weight; public: bool start(); void changegear(); void stop(); bool horn(); ... };
我们的 CAR 类利用关键词 class 声明。 该类包含 8 个成员,其中 4 个为私有访问,另 4 个为公共访问。 私有部分中的 4 个成员为数据成员。 其中 3 个为 integer (整数)数据类型,1 个为双精度数据类型。 上述成员不能通过此类外部声明的任何其它函数进行访问。
公共部分中的 4 个成员为函数成员。 两个返回 bool(布尔) 数据类型,另两个返回 void(空) 类型。 该类的任何对象,不管任何人利用该类于何时创建,都可以访问上述成员。 我们类的某个对象一旦被创建之后,这些成员都可供使用。
您也知道,访问关键词 (private, public, protected) 后面总要跟着一个冒号。 类声明则以一个分号结束。 各成员均利用其正确的数据类型声明。
要注意的是:除非如上明确指定,一旦您声明一个类后,该类的所有成员都会被赋予私有访问权限。 比如下面的类声明:
class CAR { int doors; int sits; int tyres; double weight; public: bool start(); void changegear(); void stop(); bool horn(); ... };
公共访问关键词之上声明的所有四个成员,都会自动拥有私有权限。
至于我们待用的类,则必须首先创建该类的一个对象。 现在,我们来创建一个属于本类某个类型的对象。 为此,我们会采用本类的名称,再以想要赋予该对象的名称为后缀。
CAR Honda;
或者,我们可以创建另一个对象
CAR Toyota;
Honda 或 Toyota 现在是 CAR 的一个类型,可以访问 CAR 类中所有的成员函数,前提是成员函数已于公共访问部分内声明。 稍后我们再回过头来看这个。
您会看到,我们可以创建的对象,想要多少就有多少。 这也是面向对象编程众多优势中的一项。
现在,我们来研究一下细节 - MQL5 中类的格式。
class class_name { private: members1; members2; members3; public: class_name() //构造函数 ~class_name() //析构函数 Members4(); Members5(); protected: members6; members7; };
此为某类的声明,其中的 class_name 是该类的名称。 该类有九个成员,其中有两个为特殊成员。
构造函数:
构造函数(表示为 class_name())是一种特殊函数,会在创建类的新对象类型时自动被调用。 所以本例中,当您创建一个本类的对象类型时
class_name 对象;
构造函数 class_name() 即被自动调用。 构造函数的名称必须匹配类的名称,所以,我们将该构造函数命名为 class_name()。 MQL5 中的构造函数不带有任何输入参数,且无返回类型。 一般来讲,内存分配和类成员的初始化在构造函数被调用时即已完成。 构造函数不能像常规成员函数那样被显式调用。 它们只在创建该类的新对象时被执行。 MQL5 中的类只能有一个构造函数。
析构函数:
第二个特殊成员被表示为 ~class_name()。 这就是类析构函数,在类名称前加一个波浪号 (~)。 它会在某个类对象被损坏时被自动调用。 该类所有需要去初始化的成员,均于此阶段去初始化,至于您是否显式声明了该析构函数,则无关紧要了。
数据成员:
类成员可以是任何合法的 data 类型、class 类型或 struct 类型。 换句话说,声明类的成员变量时,您可以采用任何合法的数据类型(int、double、string 等)、另一个类的某个对象,或是某个结构的某个类型(比如 MQL5 MqlTradeRequest 等)
函数成员:
类中的这些成员,都是用于数据成员的修改,以及类中主要函数/方法的执行。 函数成员的返回类型,可以是任何合法返回类型(bool、void、double、string 等)。
私有:
在此部分中声明的成员,只能通过类的函数成员访问。 类以外的任何其它函数均不可访问。
受保护:
在此部分中声明的成员,类的函数成员可以访问,而且由此类衍生的其它类的成员函数亦可访问。 也就是说,我们也可以从这个类创建一个新的类。 本例中,衍生自此类(现已变成基类)的新类,将能够访问该基类的受保护成员。 此即 OOP 中继承性的概念。 稍后我们再讨论它,放松一下……
公共:
在此部分中声明的成员,可供此类中某对象在此类以外使用。 那些需要在其它程序中使用该类的函数,其中一些也是在此声明。
现在,我们看完了类的基本格式。我希望您还没感觉到厌烦,因为在最终开始为我们的“EA 交易”创建一个包装类之前,我们还需要了解一下类的一些其它有趣的方面。
1.2. 继承性
比方说,我们想要由这个初始类 base_class 生成另一个类。 由一个初始类衍生为一个新类的格式如下:
基类:
class base_class { private: members1; members2; members3; public: class_name() //构造函数 ~class_name() //析构函数 Members4(); Members5(); protected: members6; members7; };
衍生类:
class new_class : access_keyword base_class { private: members8; public: new_class() //构造函数 ~new_class() //析构函数 Members9(); };
继续阐释详情之前,这里还要多说几句。 类 new_class 是利用冒号和一个如上所述的 access_keyword,由 base_class 衍生得来。 现在,由 base_class 衍生(或生成)得来的 new_class 可以同时访问(或继承) base_class 的公共与受保护成员,但不能访问(或继承) base_class 的私有成员。 new_class 还可以实施与 base_class 不同的新成员方法/函数。 换句话说,除了从 base_class 继承的之外,new_class 还可以拥有其自己的数据和函数成员。
如果使用 public 关键词创建衍生类,则意味着基类的公共与受保护成员,会被作为衍生类的公共与受保护成员继承。 如果使用 Protected 关键词,则基类的公共与受保护成员,会被作为衍生类的受保护成员继承。 如果使用 private 关键词,则基类的公共与受保护成员,会被作为衍生类的私有成员继承。
重要的是注意:当一个新的 new_class (衍生类)对象被创建时,base_class 的构建函数会在 new_class 的构造函数之前先被调用;但如有对象被损坏时,则 new_class (衍生类)的析构函数会在 base_class 的析构函数之前被调用。
为了更好地理解这个继承概念,我们再回到初始类 CAR。
class CAR { protected: int doors; int sits; double weight; public: bool start(); void changegear(); void stop(); bool horn(); private: int tyres; };
我们可以由此类再衍生另一个类 SALOON。 注意:我已经将 CAR 类数据成员中的三个声明为受保护。 如此则会让我们的新类 SALOON 继承上述成员。
我也想让您明白,您将访问关键词如何排序,都没有关系。 重要的是,在某个访问关键词下声明的所有成员,都隶属于该关键词。
class SALOON : public CAR { private: int maxspeed; public: void runathighspeed(); };
我们的衍生类 SALOON 有两个成员,同时还由基类 CAR 继承了七个成员(受保护与公共成员)。 这意味着一旦创建一个 SALOON 对象,它就能够访问 CAR 的公共成员函数,即 start()、changegear()、stop() 和 horn(),以及其自有的公共成员函数 runathighspeed()。 此即继承性的概念。
就像是我们的父亲/父母(基类)在我们 - 他们的孩子(衍生类) - 身上体现出来的某些特征/行为(方法)一样,因为我们或者通过基因、或者通过其它方式,继承了他们的行为(方法/函数)。 抱歉,我不是医疗专业人士,但我相信,您一定能领会我所说的意思。 顺便提一下,MQL5 并不支持多重继承,所以不必讨论这点。
好了! 我希望那个被黑色幕布遮盖、名为 OOP 或“类”的神秘事物正在被一点点地拨开迷雾……可别不耐烦,如果到这一步您还觉得自己搞不太清楚我们讲过的内容,可能就需要休息一下,来杯咖啡,然后再回来,从头开始。 它还没有您想像得那么神秘……
如果您回到这一步,我就会认为您跟上了我的节奏。 我就会要求您告诉我,您还可以从我们的基类 CAR 衍生出多少个类? 拜托,我需要您的答案。 我可是认真的。 请为它们命名、编写相应的声明,再把它们发送给我。如果您能完成全部命名,我就请您吃午餐……(天啊,我在开玩笑吗?)
您一定会有更大的收获,我们继续吧……
没错,我的书法很像我的父亲。 他的笔迹非常工整,而且和我写的一样漂亮。 我觉得这是我从他身上继承下来的一些东西,但是,您知道吗?他是个左撇子,而我是用右手写字,但是当您看到这些字时,您可能却很难分辨出来,因为实在是太像了。 这里有什么问题呢? 我从父亲那里继承了优秀的书法,但却不像他那样用左手写字。 也就是说,尽管我是从他那里继承得来、而且看起来也差不多,但是,我的方式却与父亲有所不同。 这么说对您是否有所启发? 这就是 OOP 中所谓“多态性”的理念之一。
一个衍生类(上例中的我本人)从一个基类(我父亲)继承了一个成员函数(writefine() – 我的笔迹),但是它(我)却以一种与基类(我父亲)不同的方式实现了函数 (writefine() ) 。
再回到我们的 CAR 类,以及衍生类 SALOON:
class CAR { protected: int doors; int sits; double weight; public: bool start(); virtual void changegear(){return(0);} void stop(); bool horn(); private: int tyres; }; class SALOON : public CAR { private: int maxspeed; public: void runathighspeed(); virtual void changegear(){gear1=reverse; gear2=low; gear3=high;} }; class WAGON : public CAR { private: bool hasliftback; public: virtual void changegear(){gear1=low; gear2=high; gear3=reverse;} };
一起来看看我们在这里做出的几项变更。 首先,我们于 CAR 声明了一个新的衍生类,并命名为 WAGON,其中包含两个成员。 我们还对成员函数 changegear() 进行了修改,使之成为基类中的一个虚函数。 我们为什么要让 changegear() 成为一个虚函数呢? 很简单,因为我们想要从基类继承函数的任何类,都能以其自己的方式来实现继承。
换而言之,类的虚成员函数都是可被覆盖、或是在由其声明所在类衍生的任何类中以不同方式实现的成员函数。 之后,成员函数主体可被替换为衍生类中一组新的实现。 我们可能不会于衍生类中再次使用“虚”这个词,尽管始终在衍生类中使用它是一个很好的编程习惯。
通过上例我们可以看出,类 SALOON 与 WAGON 会以其自己的方式实现函数 changegear()。
1.3. 定义类方法(成员函数)
我们已在某种程度上掌握了如何声明类;现在我们更进一步,讨论一下如何定义类的成员函数。 声明类之后,接下来就是要定义类的成员函数。 我们再来看看这个 CAR 类
class CAR { protected: int doors; int sits; double weight; public: void CAR() // 构造函数 bool start(); void changegear(); void stop(); bool horn(){press horn;} private: int tyres; }; void CAR::CAR() { // 在此初始化成员变量 } bool CAR::start() { // 此处是汽车启动程序 } void CAR::changegear() { // 此处为汽车换挡程序 } void CAR::stop() { // 此处为汽车停止程序 }
在定义成员函数的过程中,我们使用一种双冒号 (::) 运算符,名为范围运算符。 其写法与常规函数类似,唯一的区别就是类名称和所添加的范围运算符。 您还会注意到,有一个函数已于类中被定义(成员函数 horn())。 如您所见,一个成员函数可以在类声明中定义,也可以在类声明之外定义。
在我们继续进行之前,我们来回顾一下函数的概念,这很重要。
1.4. 函数
顺便提一下,什么是函数呢?
从前一座房子里有三个孩子,您不能让其中的某一个孩子去做所有的家务。所以,就要求其中的一个每天晚饭后洗盘子,另外一个负责打扫,剩下的那个则被赋予每天早上整理床铺的任务。
房子里有多种工作,不要把所有工作都加到一个孩子身上,而是将其分担给他们三人。 这样一来,每个孩子的任务都很简单轻松,而不是由一人肩负重担。 而且,如果有哪个孩子没能完成任务,我们也能很快地找出,并予以严厉批评。 这就是函数背后的理念。
很多时候,我们都想要编写一套可完成多个任务的代码。 这就是函数的由来。我们可以决定将任务分解为几个较小的任务,然后编写一个函数以执行每一个较小的任务。 一个函数就是执行或实施一系列操作的一个代码块。 它是一组语句,从某个程序点被调用时会被执行。
可以对函数进行如下定义:
Return_type function_name (parameters1,parameters2,…)
{
Expressions; //(函数执行的操作)
}
- Return_type: 由函数返回的数据类型(必须是一种有效的数据类型,如果无返回,则为空)
- Function_name: 将被用于调用函数的函数名称(必须是一个有效的名称)
- Parameters: 参数是将在函数内充当局部变量的有效数据类型变量。 如果函数拥有一个以上的参数,则以逗号分隔。
- Expressions(表达式): 包含语句块的函数主体
函数示例:
int doaddition (int x, int y) { return (x+y); }
函数返回类型为整数 (int),doaddition 是函数名称,而 int x 和 int y 则是参数。 此函数要做的,就是将所提供的任意两个输入参数相加并返回结果。 所以,只要我们为函数提供两个整数变量 2 和 3,函数就会做加法并返回 5 作为结果。
int doaddition(2,3) // 返回 5
有关函数的更多详情,请参阅《MQL5 参考》手册。
现在,理论基础打好了,我们来点实际的吧。
本文的主旨,就是告诉您如何利用 MQL5 中提供的“面向对象”法,为您的“EA 交易”编写一个类。
开始动手了……
2. 编写“EA 交易”
现在,我们会以第一篇文章中创建过的“EA 交易”为参考。 如果您没读过该文,请马上去看,免得此后我们讨论的大多数内容,您会觉得陌生。 但我可能仍会修改一些必要的地方。
在您可以编写自己的类前,您需要坐下来,先制定好交易策略。 在第一篇文章中,我们已经这样做过。 接下来要做的,就是选择我们想要赋予给该类的功能。 而这些功能会决定类的成员变量。 只是我们第一篇文章中交易策略的简要重述。
EA 的功能:
- EA 将监视一个特定的指标,当满足某一条件时(或满足某些条件时),它将基于当前满足的条件进行交易(空头/卖出或多头/买入)。
以上被称之为交易策略。 您必须首先开发您想要 EA 自动执行的策略,然后您才能编写 EA。 所以在这种情况下,让我们修改上述语句以使其反映我们想要在 EA 中开发的策略。
- 我们将使用时间周期为 8、名为“移动平均线”(Moving Average) 的指标(您可以选择任意时间周期,但出于策略的考虑我们将使用 8)。
- 当“移动平均线-8”(为方便论述,下文将其简称为 MA-8)向上渐增且价格收在 MA-8 之上时,我们希望我们的 EA 实行多头(买入)交易;当 MA-8 向下渐减且价格收在 MA-8 之下时,EA 将实行空头(卖出)交易。
- 我们还将使用时间周期同样为 8、名为“平均方向性运动”(Average Directional Movement, ADX) 的指标帮助我们确定市场是否沿趋势运动。 我们这样做是因为我们只想在市场沿趋势运动时进行交易,而在市场无方向性时(即无趋势)放松监控。 要达到此目的,我们将仅在上述条件满足且 ADX 值大于 22 时进行我们的交易(买入或卖出)。如果 ADX 大于 22 但渐减,或 ADX 小于 22,即使条件 B 满足我们也不会进行交易。
- 我们还需要设置 30 点止损来保护自己;对于利润目标,我们将目标定在 100 点利润。
- 我们同样希望我们的 EA 仅在新柱形成时寻找买入/卖出机会,我们还将确保我们在买入条件满足而我们尚未建立买入持仓时建立买入持仓,并在卖出条件满足且我们尚未建立卖出持仓时建立卖出持仓。
此外,我们希望确保能够控制可用于进行交易的可用预付款百分比,还要确保在进行交易之前,检查可用预付款。 只有交易可用预付款足够的情况下,EA 才会进行交易。
现在您知道我们想要做什么了吧。我们想要赋予给该类的功能为:
- 检查买入与卖出条件
- 根据检查的条件结果买入/卖出
基本上,我们对 EA 的要求也就这些。上述两个是主要功能,但还有更多。 比如说,在检查买入/卖出持仓的过程中,就必须使用指标。 也就是说,获取指标的值也必须在我们的类中。 所以,即包括:
- 获取所有指标句柄 (位于 EA OnInit 部分)
- 获取所有指标缓冲区 (位于 EA OnTick 部分)
- 释放所有指标句柄 (位于 EA OnDeinit 部分)
在获取指标值的过程中,我们的类需要知道 MA 和 ADX 周期、图表时间周期以及交易品种(我们使用的货币对),所以我们还必须纳入:
- 获取 ADX 与 MA 周期,以及其它重要参数,比如图表时间周期及交易品种等。
还有,交易进行之前对于可用预付款的检查,我们还要纳入:
- 检查可用预付款/账户用于交易的百分比
有了这个,我们就对类中应有哪些变量和函数有所了解了。
好了,我都帮您想好了,该编写代码了。
2.1. 编写类
我们先启动 MetaEditor (相信不说您也知道)。 打开 MetaEditor 后,我们通过点击 New 工具栏或按下 Ctrl+N 打开一份新的 MQL 文档。在向导窗口中选择 "Include" 并点击 NEXT 按钮。
图 1. 启动新的 MQL5 文档
按如下所示键入文件名称,并点击 finish(完成):
图 2. 为新文档命名
我们选择了包含,因为我们的类会成为一个包含文件,一旦我们做好使用它的准备后,就会被包含到我们的 EA 代码中。 正因如此,您没有键入输入参数的余地。
同往常一样,编辑器会判断您想要的东西,然后给出一个 大致框架。
想要开始,则请删除 "#property link …" 代码行下方的所有内容。 现在您能看到的内容大体如下。
//+------------------------------------------------------------------+ //| my_expert_class.mqh | //| Copyright 2010, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2010, MetaQuotes Software Corp." #property link "http://www.mql5.com"
现在,我们来编写该类的声明,该类称为 MyExpert。
//+------------------------------------------------------------------+ //| 类的声明 | //+------------------------------------------------------------------+ class MyExpert {
我们来分析一下类声明。 声明从类的名称开始。 接下来,再声明类的私有成员。
私有成员:
//+------------------------------------------------------------------+ //| 类的声明 | //+------------------------------------------------------------------+ class MyExpert { //--- 私有变量 private: int Magic_No; // EA交易编号 int Chk_Margin; // 下单交易前检查预付款?(1 或 0) double LOTS; // 交易量 double TradePct; // 用于交易的可用预付款的比例 double ADX_min; // ADX最小值 int ADX_handle; // ADX句柄 int MA_handle; // 移动平均指标句柄 double plus_DI[]; //存储每一个柱形ADX +DI值的数组 double minus_DI[]; // 存储每一个柱形ADX -DI值的数组 double MA_val[]; // 存储每个柱形的移动平均值的数组 double ADX_val[]; //存储每一个柱形ADX值的数组 double Closeprice; // 存储前一个柱形收盘价的变量 MqlTradeRequest trequest; //用于发送交易请求的MQL5交易请求结构体 MqlTradeResult tresult; // 用于获取交易结果的MQL5交易结果结构体 string symbol; // 存储当前交易品种名称的变量 ENUM_TIMEFRAMES period; // 存储当前时间框架值的变量 string Errormsg; // 存储报错信息的变量 int Errcode; // 存储错误代码的变量
之前讲过了,这些私有成员变量不能被该类以外的任何函数访问。 大多数变量的声明内容都十分明晰,所以我不想在这里浪费时间。
但是,在探讨中您要记住我们说过的,成员变量可以是任何合法的数据类型、结构或类。
在声明 MqlTradeRequest 和 MqlTradeResults 类型之后,我相信您在这里就能看到成果了。
构造函数
//--- 公共成员/函数 public: void MyExpert(); //类的构造函数
构造函数不带有任何输入函数,编写自己的类时一定要牢记于心。
成员函数
//--- 公共成员/函数 public: void MyExpert(); //类的构造函数 void setSymbol(string syb){symbol = syb;} //设置当前交易品种的函数 void setPeriod(ENUM_TIMEFRAMES prd){period = prd;} //设置当前交易品种时间框架/周期的函数 void setCloseprice(double prc){Closeprice=prc;} //设置前一个柱形的收盘价格的函数 void setchkMAG(int mag){Chk_Margin=mag;} //设置预付款检查值的函数 void setLOTS(double lot){LOTS=lot;} //设置交易量大小的函数 void setTRpct(double trpct){TradePct=trpct/100;} //设置交易所使用的可用预付款百分比的函数 void setMagic(int magic){Magic_No=magic;} //设置EA交易编号的函数 void setadxmin(double adx){ADX_min=adx;} //设置ADX最小值的函数
我们已经定义了上述成员函数,从而能够设置类执行其函数所需的重要变量。 如不使用这些函数,我们的类也就不能使用上述变量。 您还会注意到,我们已经在类中声明了一个对应的变量,一旦上述函数完成设置,它就会保存相应的值。
还有一件事需要注意,那就是我们已经在类声明中定义了这些成员函数。 前面我说过了,这样做是允许的。 这就意味着,到定义其它成员函数(您不久就会见到)时,就无需再次定义了。
与常规函数类似,它们也拥有正确数据类型的参数(取决于每个函数的返回值)。 相信您一定不会感到陌生。
void doInit(int adx_period,int ma_period); //用于EA交易系统初始化的函数 void doUninit(); //用于EA交易系统去初始化的函数 bool checkBuy(); //检查买入条件的函数 bool checkSell(); //检查卖出条件的函数 void openBuy(ENUM_ORDER_TYPE otype,double askprice,double SL, double TP,int dev,string comment=""); //开买入持仓的函数 void openSell(ENUM_ORDER_TYPE otype,double bidprice,double SL, double TP,int dev,string comment=""); //开卖出持仓的函数
我们只是声明这些成员函数,并不定义。 因为,我们要把定义留到稍后。 这些函数会负责类的成员变量中存储的大部分值的处理,同时,它们还会构成类中的主要角色函数。 稍后我们再讨论。
受保护成员
此类成员可由本类衍生的任何类继承。 如果您无意从此类衍生任何其它类,则没什么必要。 您也可以将其作为私有成员。 我之所以这样做,就是要您明白我们曾经讨论过的关于类的种种问题。
//--- 保护成员 protected: void showError(string msg, int ercode); //用于显示错误信息的函数 void getBuffers(); //获取指标缓存的函数 bool MarginOK(); //检查交易所需预付款是否OK的函数
虽然在类的内部,但这三个函数也非常重要。 showError 会显示我们的错误,而 getBuffers 则用于获取指标缓冲区。 MarginOK 会检查是否有足够的可用预付款建仓。
一旦您结束类的声明后,不要忘了使用分号。 这重要得很。
}; // 类的声明结束
声明类后,紧接下来要做的事,就是定义那些未于声明部分定义的成员函数。
//+------------------------------------------------------------------+ // 定义类/成员的函数 | //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| 此类的构造函数 | //| *没有任何输入参数 | //| *初始化所有需要的变量 | //+------------------------------------------------------------------+ void MyExpert::MyExpert() { //初始化所有需要的变量 ZeroMemory(trequest); ZeroMemory(tresult); ZeroMemory(ADX_val); ZeroMemory(MA_val); ZeroMemory(plus_DI); ZeroMemory(minus_DI); Errormsg=""; Errcode=0; }
这就是我们的类构造函数。 在类名称与成员函数名称之间采用双冒号 (::) (范围运算符)。 我们真正想表达的是:
尽管我们是在类声明以外定义此成员函数,但它仍在该类的范围内。 它是该类的一个成员,在双冒号(范围运算符)之前要加上该类的名称。
它没有任何输入参数。 大多数必要成员变量均在此阶段初始化,而我们则使用 ZeroMemory 函数来实现。
void ZeroMemory(
void & variable // 重置变量
);
此函数会将传递给它的变量值复位。 本例中,我们利用它来重置结构类型(MqlTradeRequest 与 MqlTradeResult)和数组的值。
showError 函数:
//+------------------------------------------------------------------+ //| showError函数 | //| *输入参数 - 错误信息,错误代码 | //+------------------------------------------------------------------+ void MyExpert::showError(string msg,int ercode) { Alert(msg,"-error:",ercode,"!!"); // 显示错误 }
一种受保护成员函数,用于类中任何对象操作过程中出现的所有错误的显示。 带有两个自变量/参数 – 错误描述与错误代码。
getBuffers 函数:
//+------------------------------------------------------------------+ //| GETBUFFERS函数 | //| *无输入参数 | //| *使用类的数据成员来获取指标缓存 | //+------------------------------------------------------------------+ void MyExpert::getBuffers() { if(CopyBuffer(ADX_handle,0,0,3,ADX_val)<0 || CopyBuffer(ADX_handle,1,0,3,plus_DI)<0 || CopyBuffer(ADX_handle,2,0,3,minus_DI)<0 || CopyBuffer(MA_handle,0,0,3,MA_val)<0) { Errormsg="Error copying indicator Buffers"; Errcode = GetLastError(); showError(Errormsg,Errcode); } }
此函数用于将所有指标缓冲区复制到我们利用其各自指标句柄于成员变量中指定的数组。
CopyBuffer 函数在第一篇文章中已有阐述。 getBuffers 函数不具备任何输入参数,因为我们会采用类的成员变量的值。
在此,我们利用内部错误函数来显示复制缓冲区过程中可能发生的任何错误。
MarginOK 函数:
//+------------------------------------------------------------------+ //| MARGINOK函数 | //| *无输入参数 | //| *使用类的数据成员来检查 | //| 下单所需的预付款是否OK | //| *成功返回TRUE,失败返回FALSE | //+------------------------------------------------------------------+ bool MyExpert::MarginOK() { double one_lot_price; //交易一手所需的预付款 double act_f_mag = AccountInfoDouble(ACCOUNT_FREEMARGIN); //帐户的可用预付款 long levrage = AccountInfoInteger(ACCOUNT_LEVERAGE); //帐户的杠杆 double contract_size = SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE); //一手合约的大小 string base_currency = SymbolInfoString(symbol,SYMBOL_CURRENCY_BASE); //当前货币对的基础货币 // if(base_currency=="USD") { one_lot_price=contract_size/levrage; } else { double bprice= SymbolInfoDouble(symbol,SYMBOL_BID); one_lot_price=bprice*contract_size/levrage; } // 根据设置,检查交易所需的预付款是否足够 if(MathFloor(LOTS*one_lot_price)>MathFloor(act_f_mag*TradePct)) { return(false); } else { return(true); } }
此函数实际上有两种用途。 它会检查并确保我们拥有足够的可用预付款来进行交易,同时,还检查并确保我们进行交易使用的可用预付款,不会超过可用额度的某个指定百分比。 这样一来,我们即可控制每宗交易使用多少款额。
我们利用 AccountInfoDouble() 函数结合 ENUM_ACCOUNT_INFO_DOUBLE 标识符 来获取账户“可用预付款”。 我们还利用 AccountInfoInteger() 函数搭配 ENUM_ACCOUNT_INFO_INTEGER 标识符来获取账户“杠杆率”。 而 AccountInfoInteger() 与 AccountInfoDouble() 两种账户函数则用于获取当前使用 EA 的账户的相关详情。
double AccountInfoDouble( int property_id // 属性的标识符 );
我们还利用交易品种属性函数 SymbolInfoDouble() 和 SymbolInfoString() 来分别获取合约大小与当前交易品种(货币对)的基础货币。 SymbolInfoDouble() 函数会获取交易品种名称和一个作为参数的 ENUM_SYMBOL_INFO_DOUBLE标识符,而 SymbolInfoString() 函数则会将交易品种名称和一个 ENUM_SYMBOL_INFO_STRING 标识符作为参数。 上述函数的结果,均存储于每个数据类型的声明变量中。
double SymbolInfoDouble( string name, //交易品种 int prop_id // 属性的标识符 );
我们在这里完成的运算非常简单。
想获取进行交易的要求预付款,我们会考虑两种情况:
- 基础货币为 USD(USD/CAD、USD/CHF、USD/JPY 等)
要求预付款 = 每手合约大小/杠杆率
2. 基础货币非 USD(EUR/USD 等)
要求预付款= 交易品种时价* 每手合约大小/杠杆率。
现在,我们决定检查指定手数或交易量所要求的预付款,是否大于您想在某宗交易中使用的可用预付款百分比。 如果要求预付款小于该百分比,则函数返回 TRUE,且进行交易;否则,函数返回 FALSE,交易不会进行。
doInit 函数:
//+-----------------------------------------------------------------------+ // 公共函数 | //+-----------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| DOINIT函数 | //| *将ADX指标的周期和移动平均指标的周期 | //| 作为输入参数 | //| *在我们EA的Onint()函数中使用 | //+------------------------------------------------------------------+ void MyExpert::doInit(int adx_period,int ma_period) { //--- 获取ADX指标的句柄 ADX_handle=iADX(symbol,period,adx_period); //--- 获取移动平均指标的句柄 MA_handle=iMA(symbol,period,ma_period,0,MODE_EMA,PRICE_CLOSE); //--- 如果返回无效句柄 if(ADX_handle<0 || MA_handle<0) { Errormsg="Error Creating Handles for indicators"; Errcode=GetLastError(); showError(Errormsg,Errcode); } // 将数组设置为时间序列 // ADX值的数组 ArraySetAsSeries(ADX_val,true); // +DI值的数组 ArraySetAsSeries(plus_DI,true); // -DI值的数组 ArraySetAsSeries(minus_DI,true); // MA值的数组 ArraySetAsSeries(MA_val,true); }
我们准备在不久之后即将编写的 EA 的 OnInit() 函数中采用此公共函数,它的功用有两个。
首先,它会设置指标句柄,并就数组变量执行 array-set-as-series 操作。 它具备两个输入参数,将由 EA 代码中提供。
doUninit 函数:
//+------------------------------------------------------------------+ //| DOUNINIT函数 | //| *无输入参数 | //| *释放ADX和MA指标句柄 | //+------------------------------------------------------------------+ void MyExpert::doUninit() { //--- 释放指标句柄 IndicatorRelease(ADX_handle); IndicatorRelease(MA_handle); }
此函数也是一个公共成员函数,将被用于我们 EA 的 UnDeInit 函数中,释放我们用过的指标的所有句柄。 它没有任何输入参数。
checkBuy 函数:
//+------------------------------------------------------------------+ //| CHECKBUY函数 | //| *无输入参数 | //| *使用类的数据成员检查 | //| 基于指定交易策略的卖出条件 | //| *如果条件满足买入条件返回TRUE,不满足返回FALSE | //+------------------------------------------------------------------+ bool MyExpert::checkBuy() { /* 检查做多/买入条件:MA持续增长, 前收盘价格大于MA,ADX > ADX min,+DI > -DI */ getBuffers(); //--- 声明存储买入条件的布尔类型变量 bool Buy_Condition_1=(MA_val[0]>MA_val[1]) && (MA_val[1]>MA_val[2]); // MA持续向上 bool Buy_Condition_2=(Closeprice>MA_val[1]); // 前收盘价大于MA值 bool Buy_Condition_3=(ADX_val[0]>ADX_min); // 当前ADX值大于最小ADX值 bool Buy_Condition_4=(plus_DI[0]>minus_DI[0]); // +DI 大于 -DI //--- 全部和在一起 if(Buy_Condition_1 && Buy_Condition_2 && Buy_Condition_3 && Buy_Condition_4) { return(true); } else { return(false); } }
此函数用于检查买入条件是否已经设置。 正因如此,其返回类型为布尔。 也就是说,它或者返回 TRUE,或者返回 FALSE。 我们就在这里定义了我们的买入交易策略。 根据我们定义的策略,如果买入条件被满足,则返回 TRUE;如果买入条件未被满足,则返回 FALSE。 在代码中使用此函数时,如果返回 TRUE,我们就会执行一次买入。
在这里,我们完成的第一件事就是调用内部成员函数 getBuffers(),它会将 checkBuy 函数需要的所有数组值复制到相应的数组变量。
于此编入代码的条件,在第一篇文章中已有阐述。
checkSell 函数:
//+------------------------------------------------------------------+ //| CHECKSELL函数 | //| *无输入参数 | //| *使用类的数据成员检查 | //| 基于指定交易策略的卖出条件 | //| *如果条件满足卖出条件返回TRUE,不满足返回FALSE | //+------------------------------------------------------------------+ bool MyExpert::checkSell() { /* 检查做空/卖出条件:MA不断下降 前收盘价小于MA, ADX > ADX min, -DI > +DI */ getBuffers(); //--- 声明存储卖出条件的布尔类型变量 bool Sell_Condition_1=(MA_val[0]<MA_val[1]) && (MA_val[1]<MA_val[2]); // MA向下减小 bool Sell_Condition_2=(Closeprice <MA_val[1]); // 前收盘价小于MA值 bool Sell_Condition_3=(ADX_val[0]>ADX_min); // 当前ADX值大于最小ADX值 bool Sell_Condition_4=(plus_DI[0]<minus_DI[0]); // -DI 大于 +DI //--- 全部和在一起 if(Sell_Condition_1 && Sell_Condition_2 && Sell_Condition_3 && Sell_Condition_4) { return(true); } else { return(false); } }
就像 checkBuy 一样,此函数将用于检查某买入条件是否被设置。 正因如此,其返回类型亦为布尔。 也就是说,它要么返回 TRUE,要么返回 FALSE。 我们在这里定义卖出交易策略。 根据我们定义的策略,如果卖出条件被满足,则返回 TRUE;如果卖出条件未被满足,则返回 FALSE。
在代码中使用此函数时,如果返回 TRUE,我们就会执行一次卖出。 就像在 checkBuy 中一样,我们首先调用内部函数 getBuffers() 。 于此编入代码的条件,在第一篇文章中亦有阐述。
openBuy 函数:
//+------------------------------------------------------------------+ //| OPENBUY函数 | //| *输入参数 - 订单类型,当前买价, | //| 止损,获利,偏差,备注 | //| *如果交易者选择,在交易前检查可用预付款 | //| *如果持仓成功建立或者报错,报警提醒 | //+------------------------------------------------------------------+ void MyExpert::openBuy(ENUM_ORDER_TYPE otype,double askprice,double SL,double TP,int dev,string comment="") { //--- 如果开启,检查预付款 if(Chk_Margin==1) { if(MarginOK()==false) { Errormsg= "You do not have enough money to open this Position!!!"; Errcode =GetLastError(); showError(Errormsg,Errcode); } else { trequest.action=TRADE_ACTION_DEAL; trequest.type=otype; trequest.volume=LOTS; trequest.price=askprice; trequest.sl=SL; trequest.tp=TP; trequest.deviation=dev; trequest.magic=Magic_No; trequest.symbol=symbol; trequest.type_filling=ORDER_FILLING_FOK; // 发送 OrderSend(trequest,tresult); // 检查结果 if(tresult.retcode==10009 || tresult.retcode==10008) //请求成功完成 { Alert("A Buy order has been successfully placed with Ticket#:",tresult.order,"!!"); } else { Errormsg= "The Buy order request could not be completed"; Errcode =GetLastError(); showError(Errormsg,Errcode); } } } else { trequest.action=TRADE_ACTION_DEAL; trequest.type=otype; trequest.volume=LOTS; trequest.price=askprice; trequest.sl=SL; trequest.tp=TP; trequest.deviation=dev; trequest.magic=Magic_No; trequest.symbol=symbol; trequest.type_filling=ORDER_FILLING_FOK; //--- 发送 OrderSend(trequest,tresult); //--- 检查结果 if(tresult.retcode==10009 || tresult.retcode==10008) //请求成功完成 { Alert("A Buy order has been successfully placed with Ticket#:",tresult.order,"!!"); } else { Errormsg= "The Buy order request could not be completed"; Errcode =GetLastError(); showError(Errormsg,Errcode); } } }
不管何时在我们的 EA 中被调用,此函数都会建一个买入仓位。 它拥有进行交易所需的大部分变量(作为输入参数),有些变量则会由我们的 EA 代码提供。 您会注意到,在第一篇文章也讲过,我们在这里使用了 MqlTraderequest 类型变量。
EA 代码中无需使用它们。 进行一次交易之前,我们想要确认用户是否准备检查预付款,如果 Chk_Margin 的值(可由 EA 获取)是 1,则我们调用 MarginOK() 函数为我们完成。 此函数的结果会决定要采取的下一步。但是,如果用户不想检查预付款,我们则继续进行交易。
openSell 函数:
//+------------------------------------------------------------------+ //| OPENSELL函数 | //| *输入参数 - 订单类型,当前卖价, | //| 止损,获利,偏差,备注 | //| *如果交易者选择,在交易前检查可用预付款 | //| *如果持仓成功建立或者报错,报警提醒 | //+------------------------------------------------------------------+ void MyExpert::openSell(ENUM_ORDER_TYPE otype,double bidprice,double SL,double TP,int dev,string comment="") { //--- 如果开启,检查预付款 if(Chk_Margin==1) { if(MarginOK()==false) { Errormsg= "You do not have enough money to open this Position!!!"; Errcode =GetLastError(); showError(Errormsg,Errcode); } else { trequest.action=TRADE_ACTION_DEAL; trequest.type=otype; trequest.volume=LOTS; trequest.price=bidprice; trequest.sl=SL; trequest.tp=TP; trequest.deviation=dev; trequest.magic=Magic_No; trequest.symbol=symbol; trequest.type_filling=ORDER_FILLING_FOK; // 发送 OrderSend(trequest,tresult); // 检查结果 if(tresult.retcode==10009 || tresult.retcode==10008) //请求成功完成 { Alert("A Sell order has been successfully placed with Ticket#:",tresult.order,"!!"); } else { Errormsg= "The Sell order request could not be completed"; Errcode =GetLastError(); showError(Errormsg,Errcode); } } } else { trequest.action=TRADE_ACTION_DEAL; trequest.type=otype; trequest.volume=LOTS; trequest.price=bidprice; trequest.sl=SL; trequest.tp=TP; trequest.deviation=dev; trequest.magic=Magic_No; trequest.symbol=symbol; trequest.type_filling=ORDER_FILLING_FOK; //--- 发送 OrderSend(trequest,tresult); //--- 检查结果 if(tresult.retcode==10009 || tresult.retcode==10008) //请求成功完成 { Alert("A Sell order has been successfully placed with Ticket#:",tresult.order,"!!"); } else { Errormsg= "The Sell order request could not be completed"; Errcode =GetLastError(); showError(Errormsg,Errcode); } } }
就像 openBuy 函数一样,无论何时在 EA 中调用,此函数都会建一个卖出仓位。 它拥有进行交易所需的大部分变量(作为输入参数),有些变量则会由我们的 EA 代码提供。
就像在建一个买入仓位时的做法一样,进行一次交易之前,我们想要确认用户是否准备检查预付款,如果 Chk_Margin 的值(可由 EA 获取)是 1,则我们调用 MarginOK() 函数为我们完成。
此函数的结果会决定要采取的下一步。 但是,如果用户不想检查预付款,我们便可继续进行交易。
现在,我们完成了类和成员函数的声明及定义,但是,却漏掉了想在 EA 代码中处理的某些其它任务。 其中包括检查可用柱、新柱以及可用的已建仓位。 它们都将在 EA 代码中被处理。
想查看类的所有函数与方法的列表,请点击如下所示的 MetaEditor 上的函数命令/菜单。 此函数会显示所有成员函数,其中包括我们未于代码中显式声明的析构函数。
受保护成员由绿色箭头指出,而构造函数和析构函数则由蓝色箭头指出。
图 3. 我们的类成员函数呈现的类析构函数
那么,下一步做什么呢?
我没听太清,您是说调试吗? 您可能说对了。 通过测试来看看您的代码有没有错误,肯定有好处,否则,等到公布代码的时候,您就该不高兴了。 这里的问题是它只是一份包含文件,而不是可以附在图表后边的 EA 代码、脚本或指标代码。 现在您有两个选择(根据我的经验来看),
- 您可以冒险按下编辑器上的调试按钮,这样,调试程序就会报告您代码中存在的任何错误,只是“no executable file produced”错误例外,它只会在 .mqh 文件不能编译成 .ex5 文件时出现; 或者
- 继续为使用您的类的 EA 编写代码。 一旦您开始 EA 的调试,包含文件也会被一同检查。 而事实上,这也是最佳、最为人接受的方式。
图 4. .mqh 文件不能编译
2.2. 编写“EA 交易”
我猜您的编辑器还打开着。 再新建一份文档,但这次选择“EA 交易”。 (详情请参照第一篇文章)。 但这次将您的 EA 命名为“my_oop_ea”。
您现在应该在这里:
现在,我们可以基于 EA 编写自己的 OOP 了。
我们要做的第一件事,就是包含我们刚刚利用 #include 预处理程序命令编写的类。 在最后一个预处理程序属性命令之后立即包含此类
//+------------------------------------------------------------------+ //| my_oop_ea.mq5 | //| Copyright 2010, MetaQuotes Software Corp. | //| http://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2010, MetaQuotes Software Corp." #property link "http://www.mql5.com" #property version "1.00" // 包含我们的类 #include <my_expert_class.mqh>
包含文件有两种方法:
// 使用尖括号 #include <my_expert_class.mqh> // 使用引号 #include "my_expert_class.mqh"
如果我们使用尖括号(< ... >),则意味着待包含文件要从标准包含目录中获取(也就是说,包含文件夹位于 MQL5 目录内)。 当前目录(MQL5 目录中的 Experts 文件夹,不会被视为查找该文件的可能位置)。 但是,如果文件位于双引号内(" ... "),文件会被认为位于当前目录内(Experts 文件夹),且不会检查标准目录(Include 文件夹)。
如果您的类保存于 Include 文件夹(标准目录)中,而您有引号替代尖括号(或尖括号替代引号),编译代码时就会出错。
图 5. 如未找到包含文件,则会出现一个错误信息
EA 输入参数
//--- 输入参数 input int StopLoss=30; // 止损 input int TakeProfit=100; // 获利 input int ADX_Period=14; // ADX周期 input int MA_Period=10; // 移动平均周期 input int EA_Magic=12345; //EA系统编号 input double Adx_Min=22.0; // ADX最小值 input double Lot=0.2; // 交易量 input int Margin_Chk=0; // 下单交易前检查预付款(0不检查,1检查) input double Trd_percent=15.0; //用于交易的预付款占用百分比
此处的参数大多并不新鲜。 我们来研究研究新的参数。
我们介绍过一种整数变量,如果想使用预付款检查,则值为 1;如果不想,则值为 0。我们还声明过另一种变量,它能保存要在建仓中使用的可用预付款的最大百分比。 稍后,这些值都会在创建类对象时使用。
紧接输入参数之后,我们又定义了两个想要具备处理能力(满足 5 位和 3 位价格)的参数(STP 与 TKP),因为我们不能更改输入变量的值。 之后,我们创建一个类对象,以供 EA 代码中使用。
//--- 其他参数 int STP,TKP; // 用于止损和获利 // 创建一个我们的类的对象 MyExpert Cexpert;
前面已经讲过,想创建一个类对象,您要使用类名称,后接想要创建的对象的名称。 我们在这里创建了一个对象 Cexpert - MyExpert 类中的一种类型。 Cexpert 现在可用于访问 MyExpert类中的所有公共成员函数。
EA 初始化部分
//+------------------------------------------------------------------+ //| EA交易初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //---运行初始化函数 Cexpert.doInit(ADX_Period,MA_Period); //--- 设置类对象的所有其他所需变量 Cexpert.setPeriod(_Period); // 设置图表的周期/时间框架 Cexpert.setSymbol(_Symbol); // 设置图表的交易品种/货币对 Cexpert.setMagic(EA_Magic); // 设置EA编号 Cexpert.setadxmin(Adx_Min); // 设置ADX最小值 Cexpert.setLOTS(Lot); // 设置交易量大小 Cexpert.setchkMAG(Margin_Chk); // 设置预付款检查变量 Cexpert.setTRpct(Trd_percent); // 设置用于交易的可用预付款的比例 //--- 处理5位数报价的经纪商 STP = StopLoss; TKP = TakeProfit; if(_Digits==5 || _Digits==3) { STP = STP*10; TKP = TKP*10; } //--- return(0); }
现在,我们调用类的 doInit 函数,并将ADX 与 MA 周期变量传递给它。 接下来,我们对刚刚创建的对象所需的所有其它变量进行设置,从而利用编写类时已经描述过的函数,将其存储于该对象的成员变量中。
代码的下一行也并不陌生,我们只需决定将 Stop Loss (止损)与 Take Profit (获利)值调整为三位或五位价格。
EA 去初始化部分
//+------------------------------------------------------------------+ //| EA交易去初始化函数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- 运行去初始化函数 Cexpert.doUninit(); }
我们调用类的 doUninit 函数,从而释放已于 EA 初始化函数中创建的所有指标句柄。
EA ONTICK 部分
//+------------------------------------------------------------------+ //| EA的订单函数 | //+------------------------------------------------------------------+ void OnTick() { //--- 我们是否有足够的柱形用于操作 int Mybars=Bars(_Symbol,_Period); if(Mybars<60) // 如果所有的柱形数量少于60 { Alert("We have less than 60 bars, EA will now exit!!"); return; } //---定义一些用于交易的MQL5结构体 MqlTick latest_price; // 获取最新报价的结构体 MqlRates mrate[]; // 用于存储每个柱形的价格,交易量和点差的结构体 /* 让我们确定价格数组值 都按照时间序列数组存储了 */ // 价格数组 ArraySetAsSeries(mrate,true);
我们在这里要做的第一件事情,就是检查总可用柱数。 如果足够柱数执行 EA 交易,则交易;如果不够,则直到足够柱数(即 60 柱)方可。 之后,我们声明 MQL5 结构的两个变量 (MqlTick 与 MqlRates)。 最后,我们再使用汇率数组上的 ArraySetAsSeries 函数。
//---使用MQL5 MqlTick结构体获取最新的报价 if(!SymbolInfoTick(_Symbol,latest_price)) { Alert("Error getting the latest price quote - error:",GetLastError(),"!!"); return; } //--- 获取最近3个柱形的详细数据 if(CopyRates(_Symbol,_Period,0,3,mrate)<0) { Alert("Error copying rates/history data - error:",GetLastError(),"!!"); return; } //--- 仅当我们有了一个新的柱形,EA才检查新的交易机会 // 声明一个静态的时间变量 static datetime Prev_time; // l让我们获取当前柱形(Bar 0)的开始时间 datetime Bar_time[1]; // 复制时间 Bar_time[0] = mrate[0].time; // 当两个时间一样时,说明不是新的柱形 if(Prev_time==Bar_time[0]) { return; } //将时间复制到静态变量中,保存 Prev_time = Bar_time[0];
这里,我们利用 SymbolInfoTick 函数来获取最新报价,并利用 CopyRates 获取过去三柱(含当前柱)的最后汇率。 代码的下述行会检查是否有新柱。 我们声明两个 datetime 变量,一个是静态变量(Prev_Time),另一个是 Bar_Time。
如有新柱,则柱时间存储于静态变量 Prev_Time 中,以方便我们将其值与下一订单号中的 Bar_Time 值进行对比。 在下一订单号中,如果 Prev_Time 等于 Bar_Time,则其仍是存储了时间的同一个柱。 这样我们的 EA 就会放松了。
但如果 Bar_Time 不等于 Prev_Time,则我们有了一个新柱。 我们决定将新柱开始时间存储于静态 datetime 变量中,Prev_Time 和 EA 现在就可以继续检查新的买入或卖出机遇了。
//--- 没有错误,继续 //--- 我们是否已经有了未平仓持仓? bool Buy_opened = false, Sell_opened=false; //保存未平仓持仓结果的变量 if (PositionSelect(_Symbol) ==true) // 有未平仓持仓 { if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { Buy_opened = true; //是买入持仓 } else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { Sell_opened = true; // 是卖出持仓 } }
我们决定检查是否有一个已建仓位。 我们只是想要确定,如果没有买入建仓,则建立一个买入交易;如果没有卖出建仓,则建立一个卖出交易。
//复制当前柱形前一个柱形的收盘价,即柱形 1 Cexpert.setCloseprice(mrate[1].close); // 柱形1的收盘价 //--- 检查买入持仓 if(Cexpert.checkBuy()==true) { // 我们是否已经有一个未平仓的买入持仓了 if(Buy_opened) { Alert("We already have a Buy Position!!!"); return; // 不新开买入持仓 } double aprice = NormalizeDouble(latest_price.ask,_Digits); // 当前买价 double stl = NormalizeDouble(latest_price.ask - STP*_Point,_Digits); // 止损 double tkp = NormalizeDouble(latest_price.ask + TKP*_Point,_Digits); // 获利 int mdev = 100; // 最大偏差 // 下单 Cexpert.openBuy(ORDER_TYPE_BUY,aprice,stl,tkp,mdev); }
现在,我们又回到了创建的对象,为什么呢? 因为我们已经具备了开展所有必要检查的能力,而这些检查是对象执行命令所必需的。
我们做的第一件事,就是利用我们的对象成员函数 setCloseprice,获取上一柱的收盘价。
之后,我们调用 checkBuy 函数以查明是否设定了买入条件(如其返回 TRUE),然后,我们想要确定并没有已建的买入仓位。 如果我们没有已建的买入仓位,则准备我们订单将要使用的必要变量(订单类型、当前卖价、止损、获利及最大偏差),并调用 openBuy 函数。 看看使用我们编写的类有多容易吧。
//--- 检查卖出持仓 if(Cexpert.checkSell()==true) { // 我们是否已经有一个未平仓的卖出持仓了 if(Sell_opened) { Alert("We already have a Sell position!!!"); return; // 不新开卖出持仓 } double bprice=NormalizeDouble(latest_price.bid,_Digits); // 当前卖价 double bstl = NormalizeDouble(latest_price.bid + STP*_Point,_Digits); // 止损 double btkp = NormalizeDouble(latest_price.bid - TKP*_Point,_Digits); // 获利 int bdev=100; // 最大偏差 // 下单 Cexpert.openSell(ORDER_TYPE_SELL,bprice,bstl,btkp,bdev); }
这与我们上面的做法相同。 因为我们要检查卖出,我们调用 checkSell 函数,如其返回 TRUE,且我们尚无已建卖出仓位,那么我们准备下单所需的必要变量(订单类型、当前卖价、止损、获利和最大偏差),之后再调用 openSell 函数。
非常容易,不是吗? 我们已经完成代码的编写。 现在,到了调试的时候了。 如果您不清楚如何使用调试程序,请参阅第一篇文章 以得到更好的了解。
如您按下 F5 或调试按钮,包含文件(我们的类)就会被包含并检查,如有错误,则会报告。 一旦发现错误,您需要返回代码并予以纠正。
图 6. 我们的包含文件在调试主 EA 代码时被包含
如果一切正常,那您真是太棒了。 现在,我们要用策略测试程序来测试我们的 EA。 在利用策略测试程序测试之前,我们需要编译 EA。 为此,点击 Compile 按钮,或按下计算机键盘上的 F7。
图 7. 点击 Compile 菜单按钮以编译我们的代码
由交易终端菜单栏,前往 View --> Strategy Tester 或按下 CONTROL+R 以启动策略测试程序。 (欲了解测试程序使用方法详情,请参阅第一篇文章)。
为了您能够利用策略测试程序测试 EA,您必须首先完成编译。 如不编译,当您在策略测试程序的设置栏上选择“EA 交易”时,就会出错。 (我刚刚在终端的新版本中发现这点。)
图 8. 在策略测试程序使用之前,应完成 EA 代码的编译
在下面找到基于“EA 交易”的 OOP 的策略测试程序的结果。
图 9. 面向对象“EA 交易”的交易结果
图形:
图 10. 面向对象“EA 交易”的图形结果
交易活动报告/日志:
图 11. 面向对象“EA 交易”的交易活动结果
测试图表:
图 12. 面向对象“EA 交易”的交易图表结果
>总结
在本文中,我们在某种程度上一同探讨了类的基础知识,以及其在编写简单“EA 交易”中的运用方式。 我们并未过多地深入类的高级领域,但我们在本文讲过的内容,已经足以帮助您快速进阶,具备编写自己的面向对象“EA 交易”代码的能力。
我们来研究了如何检查可用预付款,如果可用可用预付款不足以建立我们想要的仓位,则 EA 不会进行交易。
现在您该同意我说的话了吧:新型的 MQL5 语言内涵极其丰富,而想好好利用这一语言,您也无需成为什么编程大师。 这也是编写这篇分步指南背后的主要原因。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/116
![使用TesterWithdrawal() 函数模拟利润提取](https://c.mql5.com/2/0/TesterWithdrawal_MQL5.png)
![MQL5 Cookbook: 减少过度配合的影响以及处理报价缺失](https://c.mql5.com/2/0/Reduce_Overfitting_avatar.png)
![如何快速创建一个"2010 年自动交易锦标赛"专用"EA 交易"](https://c.mql5.com/2/0/Fast_Expert_Advisor_Writing_MQL5.png)
![MQL5 Cookbook: 把交易历史写入文件以及为每个交易品种在Excel中创建余额图表](https://c.mql5.com/2/0/avatar11.png)