
理解编程范式(第 2 部分):面向对象方式开发价格行为智能系统
概述
在第一篇文章中,我讲述了编程范式,并专注于如何利用 MQL5 实现过程化编程。我还探索了函数化编程。在更深入地理解过程化编程的工作原理之后,我们运用指数移动平均线指标(EMA),和烛条价格数据打造了一款基本价格行为智能系统。
本文将更深入地切入面向对象的编程范式。我们随后将应用这些知识,把来自第一篇文章中先前开发的智能系统的过程化代码转换为面向对象的代码。这个过程将加深我们对这两种编程范式之间主要区别的理解。
如您所阅,请记住,主要目标并非展示价格行为策略。取而代之,我的意图是概括并帮助您更深入地理解各种编程范式是如何运作的,以及我们如何利用 MQL5 实现它们。我们开发的简单价格行为智能系统只是次要目标,可当作演示我们如何将其应用于实际示例的指南。
理解面向对象的编程
面向对象编程(也简称为 OOP)是一种围绕对象概念组织代码的编码风格。它主要将项目视为真实事物或概念的模型。
在尝试面向对象编程时,初学者通常会有具体的问题。我将首先解决这些问题,因为这将有助于巩固您对该编程范式的掌握。
面向对象编程中的类是什么?
类是创建对象的蓝图。它具有一组描述对象特征的属性(属性),以及执行不同任务需求的函数(方法)。
我用手机作为例子来更好地解释面向对象的范式。
想象一下,您正在创办一家新的手机制造公司,而您正在与产品设计部门负责人开会。您的目标是创建一款理想手机的蓝图,作为公司的主打产品。在本次会议中,您要讨论每部手机应具备的基本特性和功能。
您从创建一个蓝图开始,其将成为公司日后生产的每部手机的起点。在面向对象的编程中,该蓝图称为类。
产品设计师建议,要制作蓝图,您必须首先提出手机能够执行的不同任务的清单。您提出以下任务清单:
- 拨打和接听电话。
- 发送和接收短信(SMS)。
- 通过互联网发送和接收数据。
- 拍照和录制视频。
在面向对象编程中,上述蓝图中描述的任务称为方法。方法与普通函数相同,但在类中创建时,它们称为方法或成员函数。
然后,您决定每部手机都必须具有比其描述更好属性和特征。您集思广益 5 分钟,得出以下清单:
- 型号。
- 颜色。
- 输入类型。
- 屏幕类型。
- 屏幕尺寸。
在面向对象编程中,蓝图(类)中描述的属性和特征称为类属性或成员变量。属性在类中声明为变量。
面向对象编程中的对象是什么?
对象是类的具现。简言之,类是纸面上的计划或蓝图,而对象是计划或蓝图在现实生活中的真实具现。
继续我们的手机公司示例,您和您的产品设计师已经完成了纸面上的手机蓝图设计。您决定针对不同的手机消费者市场生产两种不同类型的手机。第一种型号是低端版本,只能拨打或接听电话,以及发送或接收短信。第二种型号将是高端版本(智能手机),拥有第一种低端型号的所有功能、附加高端摄像头、大容量电池、和高分辨率触摸屏。
您兴奋地前往工程部门,将手机蓝图(类)交给总工程师,并给他指示,让您的设计蓝图创生。他立即开始依照蓝图工作。这大约需要花费工程师们一周的时间才能完成手机的工程设计。当他们完工后,他们会递交成品供您测试。
您现在持有的手机是从类(蓝图)衍生的对象。低端手机模型仅实现了一些类方法,而高端手机模型(智能手机)实现了全部的类方法。
我用一些代码来演示这个手机示例。遵照以下步骤在 MetaEditor IDE 中创建类文件。
步骤 1:打开 MetaEditor IDE,并使用新建菜单项按钮启动 MQL 向导。
步骤 2:选择创建新类选项,然后单击下一步。
步骤 3:在创建类窗口中,选择类名:输入框,键入 PhoneClass 作为类名,然后在包含文件:输入框中键入 'Experts\OOP_Article\PhoneClass.mqh',将类文件保存在与我们的智能系统源代码相同的文件夹之中。基类:输入框留空。点击完成生成一个新的 MQL5 类文件。
我们现在有一个空白的 MQL5 类文件。我添加了一些注释来帮助我们分解类的不同部分。编写一个新的 MQL5 类现在是一个简单的过程,可借助 MetaEditor IDE 中的 MQL 向导自动为我们完成。研究下面的语法,因为它包含结构正确的 MQL5 类文件的起点。
class PhoneClass //class name { private: //access modifier public: //access modifier PhoneClass(); //constructor method declaration ~PhoneClass(); //destructor method declaration }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ PhoneClass::PhoneClass() //constructor method definition { } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ PhoneClass::~PhoneClass() //destructor method definition { } //+------------------------------------------------------------------+
不要关注 #properties 代码,因为它与手头的主题无关。重要的语法始自我们打开类的语法行:class PhoneClass {就在 #property version “1.00” 行的正下方。
//+------------------------------------------------------------------+ //| PhoneClass.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" //--------------------------------------- //Ignore the code segment above //---------------------------------------
我们讨论一下我们刚生成类文件的不同部分。
在类左大括号后,您可以找到两个访问修饰符:private 和 public。当我涵盖继承主题时,我会详细解释它们是什么。
在 private 访问修饰符下方,我们将自手机蓝图中添加类属性(手机属性和特征)。我们将这些属性添加为全局变量。
private: //access modifier //class attributes int modelNumber; string phoneColor; string inputType; string screenType; int screenSize;
public:访问修饰符之下的下一行是构造函数和析构函数方法的声明。这两种方法类似于智能系统的 OnInit() 和 OnDeInit() 标准函数。类构造函数运作类似于 OnInit() 的操作,而类析构函数执行类似于 OnDeinit() 的任务。
public: //access modifier PhoneClass(); //constructor method declaration ~PhoneClass(); //destructor method declaration
面向对象编程中的构造函数和析构函数是什么?
构造函数
构造函数是类中的一个特殊方法,在创建该类的对象时自动调用和执行该方法。其主要用途是初始化对象的属性,并执行对象所需的任何设置动作,如有效状态和更新操作。
构造函数的关键特征:
- 与类同名:构造函数方法与类同名。该命名惯例可帮助编程语言识别构造函数,并将其与类相关联。在 MQL5 中,默认情况下,所有构造函数都是 void 类型,这意味着它们不会返回任何数值。
- 初始化:构造函数负责按照默认值、或所提供数值初始化对象的属性。这可确保对象以明确定义的状态开始。我将在下面的 PhoneClass 中演示其工作原理。
- 自动执行:在创建对象时自动调用或执行构造函数。这发生在对象实例化的时刻。
- 可选参数:构造函数能够取用参数,允许在对象创建期间进行自定义。这些参数为构造函数提供设置对象初始状态的数值。
在一个类中您可以拥有多个构造函数,但它们需要通过所拥有的实参或型参来区分它们。根据它们的参数,构造函数分为:
- 默认构造函数:这是一个不带任何参数的构造函数。
- 参数型构造函数:这是一个拥有参数的构造函数。如果该构造函数中的任何参数引用一个同类对象,则它自动变成一个复制型构造函数。
- 复制型构造函数:该构造函数的一个或多个参数引用一个同类对象。
每次创建新的 PhoneClass 对象时,我们需要一种方式来初始化或保存具有具体手机详情的类属性(变量)。我们将用参数型构造函数完成此任务。我们修改当前的默认构造函数,并将其转换为拥有 5 个参数的参数型构造函数。在其下声明和定义新的参数型构造函数之前,先注释掉默认构造函数的声明。
//PhoneClass(); //constructor declaration and definition PhoneClass(int modelNo, string colorOfPhone, string typeOfInput, string typeOfScreen, int sizeOfScreen) { modelNumber = modelNo; phoneColor = colorOfPhone; inputType = typeOfInput; screenType = typeOfScreen; screenSize = sizeOfScreen; }
保存并编译类文件。编译类文件后,您会注意到第 40 行第 13 列有一个错误警告。错误:PhoneClass - 已有不同参数定义的成员函数。
请注意,您的类文件的代码行号可能会有所不同,具体取决于您选择的代码缩进或样式。有关正确的行号,请参阅 MetaEditor 编译器窗口底部的错误日志,如下所示。
我们已在一个代码模块中声明并定义了新的参数型构造函数,再往下看,在第 40 行的代码中,您会发现另一个代码段也定义了构造函数。您需要把第 40 行到第 42 行注释掉,当您编译类文件时,它就会成功编译,没有错误或警告。(请注意,在您的类文件中该代码段可能位于不同代码行上!)
/*PhoneClass::PhoneClass() //constructor method definition
{
}*/
析构函数
在类的内部析构函数是一种特殊方法,在对象终结时自动调用和执行该方法。它的主要目的是垃圾回收,以及清理对象在生命周期结束时已分配的任何资源。这有助于防止内存泄漏,和其它与资源相关的问题。一个类只能有一个析构函数。
析构函数的主要特征:
- 与类同名:析构函数与类同名,但前缀为波折号字符(~)。该命名惯例有助于编程语言识别析构函数,并将其与类相关联。
- 垃圾回收:清理对象已分配的任何资源,例如内存、字符串、动态数组、自动对象、或网络连接。
- 自动执行:当对象终结时,析构函数会被自动调用或执行。
- 无参数:所有析构函数都没有任何参数,默认情况下为 void 类型,这意味着它们不返回任何数值。
我们添加一些代码,每次执行析构函数时都会打印一些文本。转到析构函数方法代码定义段,并添加以下代码:
PhoneClass::~PhoneClass() //destructor method definition { Print("-------------------------------------------------------------------------------------"); PrintFormat("(ModelNo: %i) PhoneClass object terminated. The DESTRUCTOR is now cleaning up!", modelNumber); }
您会注意到,当我们声明或定义类、构造函数和析构函数方法时,我们不会给它们一个返回类型,即(void)。指定返回类型不是必需的,因为在 MQL5 中有一条简单的规则,就是所有构造函数和析构函数都是 void 类型,编译器会自动为我们这样做。
为了让您清晰地理解构造函数和析构函数是如何运作的,这里有一个简短的示例:想象一次野营旅行,期间您和朋友为你们自己分配了特定的角色。您的朋友负责搭建帐篷,并在抵达时安排一切,充当“构造函数”。而您则在旅行结束时处理打包和清理,扮演“析构函数”的角色。在面向对象的编程中,构造函数初始化对象,而析构函数在对象的生命周期结束时清理资源。
接下来,我们将添加类方法(手机将执行的任务,如蓝图中所述)。在析构函数方法声明的正下方添加以下方法:~PhoneClass();
//class methods bool MakePhoneCall(int phoneNumber); void ReceivePhoneCall(); bool SendSms(int phoneNumber, string message); void ReceiveSms(); bool SendInternetData(); void ReceiveInternetData(); void UseCamera(); void virtual PrintPhoneSpecs();
MQL5 中的虚拟方法是什么?
在 MQL5 中,虚拟方法是类中的特殊函数,可在派生类中被具有相同名称的方法覆盖。当一个方法在基类中标记为 “virtual” 时,它允许派生类提供该方法的不同实现。该机制对于多态性是根基,其意味着可以将不同类的对象当作公共基类的对象处置。它允许在子类中定义特定行为,同时在基类中维护公共接口,从而在面向对象编程中实现灵活性和可扩展性。
在涵盖面向对象的继承时,我将进一步演示如何重写 PrintPhoneSpecs() 方法。
按 PrintPhoneSpecs() 方法的定义编码。将该代码放在类文件底部的析构函数方法定义下方。
void PhoneClass::PrintPhoneSpecs() //method definition { Print("___________________________________________________________"); PrintFormat("Model: %i Phone Specs", modelNumber); Print("---------------------"); PrintFormat ( "Model Number: %i \nPhoneColor: %s \nInput Type: %s \nScreen Type: %s \nScreen Size: %i\n", modelNumber, phoneColor, inputType, screenType, screenSize ); }
有两种方式可以定义类方法。
- 在类主体内:您可以在类主体内一次性声明和定义一个方法,就像我们之前对参数型构造函数所做的那样。在类主体中声明和定义方法的语法与普通函数语法相同。
- 在类主体之外:第二种方式是首先在类主体中声明方法,然后在类主体外部定义它,就像我们对析构函数和 PrintPhoneSpecs() 方法所做的那样。若要在类主体之外定义 MQL5 方法,您必须首先从方法的返回类型开始,然后是类名、范围解析运算符 (::)、方法名称、然后是在括号中的参数列表。接下来,在大括号 {} 中编写方法主体。这种把声明与定义分离是首选选项,因为它允许清晰地组织类结构、及其关联的方法。
面向对象编程中的范围解析运算符 (::) 是什么?
:: 运算符称为范围解析运算符,它在 C++ 和 MQL5 中指定函数或方法所属的上下文。它定义或引用作为类成员的函数或方法,帮助我们指定它们是该特定类的成员。
我用 PrintPhoneSpecs() 方法定义更详尽地解释这一点:
void PhoneClass::PrintPhoneSpecs() //method definition { //method body }
从上面的 PrintPhoneSpecs() 方法定义中,您可看到类名放在范围运算符 “::” 之前。这示意该函数属于 PhoneClass 类。这就是您如何将方法链接到与其关联的类。:: 运算符对于定义和引用类中的方法至关重要。它有助于指定函数或方法所属的范围或上下文。
我们的类还包括以下声明的方法,同样也需要定义:
- MakePhoneCall(int phoneNumber);
- ReceivePhoneCall();
- SendSms(int phoneNumber, string message);
- ReceiveSms();
- SendInternetData();
- ReceiveInternetData();
- UseCamera();
将它们的方法定义代码放在 PrintPhoneSpecs() 定义代码段的上方。以下是添加上述方法定义后的类文件外观:
class PhoneClass //class name { private: //access modifier //class attributes int modelNumber; string phoneColor; string inputType; string screenType; int screenSize; public: //access modifier //PhoneClass(); //constructor declaration and definition PhoneClass(int modelNo, string colorOfPhone, string typeOfInput, string typeOfScreen, int sizeOfScreen) { modelNumber = modelNo; phoneColor = colorOfPhone; inputType = typeOfInput; screenType = typeOfScreen; screenSize = sizeOfScreen; } ~PhoneClass(); //destructor method declaration //class methods bool MakePhoneCall(int phoneNumber); void ReceivePhoneCall(); bool SendSms(int phoneNumber, string message); void ReceiveSms(); bool SendInternetData(); void ReceiveInternetData(); void UseCamera(); void virtual PrintPhoneSpecs(); }; /*PhoneClass::PhoneClass() //constructor method definition { }*/ PhoneClass::~PhoneClass() //destructor method definition { Print("-------------------------------------------------------------------------------------"); PrintFormat("(ModelNo: %i) PhoneClass object terminated. The DESTRUCTOR is now cleaning up!", modelNumber); } bool PhoneClass::MakePhoneCall(int phoneNumber) //method definition { bool callMade = true; Print("Making phone call..."); return(callMade); } void PhoneClass::ReceivePhoneCall(void) { //method definition Print("Receiving phone call..."); } bool PhoneClass::SendSms(int phoneNumber, string message) //method definition { bool smsSent = true; Print("Sending SMS..."); return(smsSent); } void PhoneClass::ReceiveSms(void) { //method definition Print("Receiving SMS..."); } bool PhoneClass::SendInternetData(void) //method definition { bool dataSent = true; Print("Sending internet data..."); return(dataSent); } void PhoneClass::ReceiveInternetData(void) { //method definition Print("Receiving internet data..."); } void PhoneClass::UseCamera(void) { //method definition Print("Using camera..."); } void PhoneClass::PrintPhoneSpecs() //method definition { Print("___________________________________________________________"); PrintFormat("Model: %i Phone Specs", modelNumber); Print("---------------------"); PrintFormat ( "Model Number: %i \nPhoneColor: %s \nInput Type: %s \nScreen Type: %s \nScreen Size: %i\n", modelNumber, phoneColor, inputType, screenType, screenSize ); }
在文章底部可找到随附的完整 PhoneClass.MQH 代码。
现在是时候创建一个 PhoneClass 对象了。这等同于手机工程师如何将我们的手机蓝图转换为能够执行不同任务(例如拨打和接听电话)的物理产品,正如我们前面手机公司示例中所述的。
请注意,类文件以 .mqh 扩展名保存,称为包含文件。在保存 PhoneClass.mqh 的同一文件夹中创建一个新的智能系统。我们将 PhoneClass.mqh 文件保存在以下文件路径之中:“Experts\OOP_Article\”。使用 MetaEditor MQL 向导生成一个新的智能系统(模板),并将其保存在以下目录路径 “Experts\OOP_Article\” 之内。将新 EA 命名为 'PhoneObject.mq5'。
将此代码放在 EA 的 #property version "1.00" 代码段下方:
// Include the PhoneClass file so that the PhoneClass code is available in this EA #include "PhoneClass.mqh" int OnInit() { //--- // Create instaces or objects of the PhoneClass with specific parameters // as specified in the 'PhoneClass' consturctor PhoneClass myPhoneObject1(101, "Black", "Keyboard", "Non-touch LCD", 4); PhoneClass myPhoneObject2(102, "SkyBlue", "Touchscreen", "Touch AMOLED", 6); // Invoke or call the PrintPhoneSpecs method to print the specifications myPhoneObject1.PrintPhoneSpecs(); myPhoneObject2.PrintPhoneSpecs(); //--- return(INIT_SUCCEEDED); } void OnDeinit(const int reason){} void OnTick(){}
在本文底部可找到随附的完整 PhoneObject.mq5 代码。
我们来分解 'PhoneObject.mq5' EA 代码的作用:
使用 include 语句添加类文件:
我们首先使用 #include “PhoneClass.mqh” 将我们的 PhoneClass 代码添加到 EA 中,如此它就可以在我们新创建的 EA(PhoneObject.mq5)中使用。我们已将该类包含在全局范畴内,令其在 EA 的所有部分中都可用。
创建 PhoneClass 对象:
在 EA 的 OnInit() 函数内,我们创建了 PhoneClass 的两个实例或对象。为了创建这些对象,我们从类名开始,后跟手机实例的描述性名称(myPhoneObject1 和 myPhoneObject2)。然后,我们把手机规格的数值用括号封起来,例如其型号、颜色、输入类型、屏幕类型,正如 PhoneClass 构造函数参数指定的屏幕大小。
调用或援引类方法:
这些行,myPhoneObject1.PrintPhoneSpecs() 和 myPhoneObject2.PrintPhoneSpecs() 调用 PhoneClass 对象的 PrintPhoneSpecs() 方法来打印手机规格。
输出手机规格:
在 MT5 交易终端的品种图表上加载 EA ,执行 PhoneObjectEA,转到工具箱窗口,然后选择智能系统选项卡去检查打印的手机规格。
打印的数据还显示来自 'PhoneClass' 析构函数('~PhoneClass()')的文本消息。我们能看到,每个手机对象都创建了一个唯一的独立析构函数,并在对象终结时调用它。
面向对象编程中的继承是什么?
继承是一个概念,其中新类(称为次级类或子类)可以从已有的类(称为父类或基类)继承属性和行为(形参和方法)。这允许子类重用和扩展父类的功能。
简言之,继承就像一部家谱。将“基类”想象成“父亲”或“母亲”。该类具有特殊的性状(属性和方法)。现在,将“次级类”视为“儿子”或“女儿”。次级类自动继承基类的所有性状(属性和方法),类似于儿子从其父亲继承性状。
举例,如果母亲有棕色的眼睛,那么女儿也有棕色的眼睛,无需明确说明。在编程中,子类从基类继承方法和属性,从而创建一个层次结构来组织和重用代码。
该“家族”结构有助于组织和重用代码。儿子(次级类)获得父亲(基类)所拥有的一切,甚至可以添加自己独特的功能。程序员对这些“家族成员”使用不同的术语:
- 基类:父类、超类、根类、基本类、主类
- 次级类:子类、派生类、后代类、继承类
以下是我们如何在所提供的 PhoneClass 代码的上下文中实现继承:
- PhoneClass 充当定义手机功能的基本构建模块的基类(蓝图)。
- 我们将创建另一个类来实现我们之前在手机公司示例中讨论的高端(智能手机)手机模型。
- 我们将这个新类命名为 SmartPhoneClass。它将继承 PhoneClass 的所有属性和方法,同时引入智能手机的特定新功能,并覆盖 PhoneClass 中已有的 PrintPhoneSpecs() 方法,从而实现智能手机行为。
#include "PhoneClass.mqh" // Include the PhoneClass file class SmartPhoneClass : public PhoneClass { private: string operatingSystem; int numberOfCameras; public: SmartPhoneClass(int modelNo, string colorOfPhone, string typeOfInput, string typeOfScreen, int sizeOfScreen, string os, int totalCameras) : PhoneClass(modelNo, colorOfPhone, typeOfInput, typeOfScreen, sizeOfScreen) { operatingSystem = os; numberOfCameras = totalCameras; } void UseFacialRecognition() { Print("Using facial recognition feature..."); } // Override methods from the base class if needed void PrintPhoneSpecs() override { Print("-----------------------------------------------------------"); Print("Smartphone Specifications (including base phone specs):"); Print("-----------------------------------------------------------"); PrintFormat("Operating System: %s \nNumber of Cameras: %i", operatingSystem, numberOfCameras); PhoneClass::PrintPhoneSpecs(); // Call the base class method Print("-----------------------------------------------------------"); } };
此处直接链接到完整的 SmartPhoneClass.mqh 代码。
在上面的示例中,SmartPhoneClass 继承自 PhoneClass。它引入了新属性(operatingSystem 和 numberOfCameras)以及新方法(UseFacialRecognition)。SmartPhoneClass 的构造函数还使用 ': PhoneClass(...)' 调用基类的构造函数(PhoneClass)。您还会注意到,我们覆盖了基类中的 PrintPhoneSpecs() 方法。我们在 SmartPhoneClass 的 PrintPhoneSpecs() 方法定义中包括了 override 指定符,从而让编译器知道我们是特意重写来自基类的方法。
以此方式,您可以创建 SmartPhoneClass 的实例,其中包括正规手机(PhoneClass)的所有功能,以及智能手机特定的新附加功能。
访问修饰符
访问修饰符在面向对象编程的继承中扮演着至关重要的角色,它定义了如何在派生类中继承和访问基类的成员(属性和方法)。
- public:公开访问允许类的属性和方法能从类的外部访问。您可以自由使用或修改程序任何部分的任何公开成员。这是最开放的访问级别。
- private:私密访问限制了属性和方法的可见性,只允许在类自身内访问或修改。声明为 private 的成员不能从类外部直接访问。这有助于隐藏实现细节,并强制数据完整性。这就是所谓的封装。
- protected:受保护访问是介于公开和私密之间的中间地带。声明为 protected 的成员可由类及其次级类(派生类)中访问。这允许在相关类之间进行一定程度的受控分享,同时仍然制约来自外部的访问。
若要创建 SmartPhoneClass 对象,创建一个新的 EA,将其另存为 SmartPhoneObject.mq5,并插入以下代码:
// Include the PhoneClass file so that it's code is available in this EA #include "SmartPhoneClass.mqh" int OnInit() { //--- // Create instaces or objects of the PhoneClass with specific parameters // as specified in the 'PhoneClass' consturctor (base/mother class) PhoneClass myPhoneObject1(103, "Grey", "Touchscreen", "Touch LCD", 8); // as specified in the 'SmartPhoneClass' consturctor SmartPhoneClass mySmartPhoneObject1(104, "White", "Touchscreen", "Touch AMOLED", 6, "Android", 3); // Invoke or call the PrintPhoneSpecs method to print the specifications myPhoneObject1.PrintPhoneSpecs(); // base class method mySmartPhoneObject1.PrintPhoneSpecs(); // overriden method by the derived class (SmartPhoneClass) //--- return(INIT_SUCCEEDED); } void OnDeinit(const int reason){} void OnTick(){}
可在文章底部找到完整的 SmartPhoneObject.mq5 代码。
保存并编译 SmartPhoneObject.mq5 源代码,然后将其加载到 MT5 交易终端的品种图表上,从而执行 SmartPhoneClass 对象。转到工具箱窗口,然后选择智能系统选项卡,以便检查 EA 的打印输出。
面向对象编程的主要属性
OOP 的六个主要属性是:
- 封装:将数据和方法捆绑到单个单元(类)中,从而隐藏内部详细信息。
- 抽象:通过专注于基本属性和行为将复杂系统简化。
- 继承:允许一个类从另一个类继承属性和行为,达成代码可重用性。
- 多态性:允许将不同类的对象视为公共基类的对象,达成灵活性。
- 类和对象:类是蓝图,对象是实例;它们以模块化的方式组织代码。
- 消息传递:对象通过发送消息进行通信,促进交互。
EIP(封装、继承、多态性):这些是面向对象编程的核心原则,达成代码组织和灵活性。通过理解和应用这些核心属性,您可以编写更有条理、可维护、和可重用的代码。
MQL5 类命名惯例
对于 MQL5 中的类名,常见命名惯例是给它们加上 'C' 前缀。不过,遵循该惯例并非强制性的。“C” 代表 “class”,是一种常见的做法,它明显是表示类的专指标识符。
例如,您也许在 MQL5 代码中看到的类名像是 CExpert、CIndicator 或 CStrategy。该命名惯例有助于将类与其它类型的标识符(如函数或变量)区分开来。
虽然用 'C' 作为前缀是一种惯例,并且为清晰起见通常会推荐,但 MQL5 并未就类的命名强制任何严格规定。技术上,您在给自己的类命名时无需 “C” 前缀,但最好遵循既定惯例来增强代码的可读性和可维护性。
以面向对象方式开发价格行为 EA
现在您已经理解了面向对象编程范式的所有信息,是时候解决一个实操示例,并将我们之前开发的基于价格行为的智能系统从过程化代码转换为面向对象的代码了。我在本文底部随附价格行为智能系统的过程化代码 Procedural_PriceActionEMA.mq5。
以下是价格行为交易策略细节的快速概览。有关交易策略的更详细说明,您可以在第一篇文章中找到它。
价格行为 EMA 策略
该策略非常简单,仅用到指数移动平均线(EMA),和烛条价格来制定交易决策。您应当用策略测试器对其进行过优化,从而获得最佳 EMA 设置和时间帧。我更偏爱在 H1 或更长的时间帧内进行交易,从而获得更好的结果。
入场规则:
- 买入:当最新收盘的蜡烛是阳线(开盘价 < 收盘价),并且其最低价和最高价均高于指数移动平均线(EMA)时,开立多头持仓。
- 卖出:当最收盘的蜡烛是阴线(开盘价 > 收盘价),并且其最低价和最高价均低于指数移动平均线(EMA)时,开立空头持仓。
- 当形成新的蜡烛时,如果满足上述条件之一,则继续开立新的多头或空头持仓。
离场规则:
- 当达到用户指定的账户百分比盈利、或亏损时,自动把所有持仓平仓。
- 或者,使用预定义的传统止损或止盈订单来管理持仓和离场。
由于本文的目标是展示如何使用面向对象原则开发上述策略的 mql5 EA,因此我们继续编写代码。
创建新的 CEmaExpertAdvisor 类
利用 MQL5 向导创建一个空白类文件,并以 'EmaExpertAdvisor' 作为类文件名,并将其保存在以下文件路径中:'Experts\OOP_Article\PriceActionEMA\'。在新创建的 EmaExpertAdvisor.mqh 类文件中,创建一个名为 CEmaExpertAdvisor 的新类。我们将使用 CEmaExpertAdvisor 类来封装 EA 的行为,以及成员变量来表示其状态。
在 CEmaExpertAdvisor 类中插入以下类属性变量、方法代码:
//+------------------------------------------------------------------+ // Include the trade class from the standard library //--- #include <Trade\Trade.mqh> class CEmaExpertAdvisor { public: CTrade myTrade; public: // Constructor CEmaExpertAdvisor( long _magicNumber, ENUM_TIMEFRAMES _tradingTimeframe, int _emaPeriod, int _emaShift, bool _enableTrading, bool _enableAlerts, double _accountPercentageProfitTarget, double _accountPercentageLossTarget, int _maxPositions, int _tp, int _sl ); // Destructor ~CEmaExpertAdvisor(); };
在 class 关键字之前,我包含了 MQL5 标准库中的 Trade 类,从而帮助我们以更少的代码有效地管理各种交易操作。这意味着我们必须重写 ManageProfitAndLoss() 和 BuySellPosition(...) 方法来顺应这种新的高效升级。
#include <Trade\Trade.mqh>
稍后下面的代码行,您可看到已经实例化的 CTrade 类,并创建了一个名为 myTrade 的即用型对象,我们将用它来开新仓和关仓。
//Create an instance/object of the included CTrade class
CTrade myTrade;
过程化代码的所有用户输入的全局变量都将成为 CEmaExpertAdvisor 类的私密全局变量。一旦类被实例化,就需要初始化 EA 用户输入变量,我们是通过将它们作为参数传递给构造函数来完成这一步。这将有助于把初始化过程封装在类的内部。
private: // Private member variables/attributes (formerly procedural global variables) //------------------------ // User input varibles long magicNumber; ENUM_TIMEFRAMES tradingTimeframe; int emaPeriod; int emaShift; bool enableTrading; bool enableAlerts; double accountPercentageProfitTarget; double accountPercentageLossTarget; int maxPositions; int TP; int SL;
过程化代码中的其余全局变量将被声明为 public 并定义为类中的全局变量。
public: //--- EA global variables // Moving average variables double movingAverage[]; int emaHandle; bool buyOk, sellOk; string movingAverageTrend; // Strings for the chart comments string commentString, accountCurrency, tradingStatus, accountStatus; // Capital management variables double startingCapital, accountPercentageProfit; // Orders and positions variables int totalOpenBuyPositions, totalOpenSellPositions; double buyPositionsProfit, sellPositionsProfit, buyPositionsVol, sellPositionsVol; datetime closedCandleTime;//used to detect new candle formations
在析构函数方法声明下方,且在类的结束语法大括号上方 ,添加所有过程化代码函数作为类方法声明。
// Class method declarations (formerly procedural standalone functions) int GetInit(); void GetDeinit(); void GetEma(); void GetPositionsData(); bool TradingIsAllowed(); void TradeNow(); void ManageProfitAndLoss(); void PrintOnChart(); bool BuySellPosition(int positionType, string positionComment); bool PositionFound(string symbol, int positionType, string positionComment);
我们将使用 C++ 风格的编码,并在类主体之下定义所有类方法,如下所示:
//+------------------------------------------------------------------+ //| METHODS DEFINITIONS | //+------------------------------------------------------------------+ CEmaExpertAdvisor::CEmaExpertAdvisor(long _magicNumber, ENUM_TIMEFRAMES _tradingTimeframe, int _emaPeriod, int _emaShift, bool _enableTrading, bool _enableAlerts, double _accountPercentageProfitTarget, double _accountPercentageLossTarget, int _maxPositions, int _tp, int _sl) { magicNumber = _magicNumber; tradingTimeframe = _tradingTimeframe; emaPeriod = _emaPeriod; emaShift = _emaShift; enableTrading = _enableTrading; enableAlerts = _enableAlerts; accountPercentageProfitTarget = _accountPercentageProfitTarget; accountPercentageLossTarget = _accountPercentageLossTarget; maxPositions = _maxPositions; TP = _tp; SL = _sl; } //+------------------------------------------------------------------+ CEmaExpertAdvisor::~CEmaExpertAdvisor() {} //+------------------------------------------------------------------+ int CEmaExpertAdvisor::GetInit() { //method body.... } //+------------------------------------------------------------------+ void CEmaExpertAdvisor::GetDeinit() { //method body.... } //+------------------------------------------------------------------+ void CEmaExpertAdvisor::GetEma() { //method body.... } //+------------------------------------------------------------------+ void CEmaExpertAdvisor::GetPositionsData() { //method body.... } //+------------------------------------------------------------------+ bool CEmaExpertAdvisor::TradingIsAllowed() { //method body.... } //+------------------------------------------------------------------+ void CEmaExpertAdvisor::TradeNow() { //method body.... } //+------------------------------------------------------------------+ void CEmaExpertAdvisor::ManageProfitAndLoss() { //method body.... } //+------------------------------------------------------------------+ void CEmaExpertAdvisor::PrintOnChart() { //method body.... } //+------------------------------------------------------------------+ bool CEmaExpertAdvisor::BuySellPosition(int positionType, string positionComment) { //method body.... } //+------------------------------------------------------------------+ bool CEmaExpertAdvisor::PositionFound(string symbol, int positionType, string positionComment) { //method body.... }
所有方法定义都将与过程化代码中的语法相同,除了 ManageProfitAndLoss() 和 BuySellPosition(...) 方法,它们已升级为利用我们早前导入到类代码中的 CTrade类新建的 myTrade 对象。
以下是新建并更新的 ManageProfitAndLoss() 方法:
void CEmaExpertAdvisor::ManageProfitAndLoss() { //if the account percentage profit or loss target is hit, delete all positions double lossLevel = -accountPercentageLossTarget; if( (accountPercentageProfit >= accountPercentageProfitTarget || accountPercentageProfit <= lossLevel) || ((totalOpenBuyPositions >= maxPositions || totalOpenSellPositions >= maxPositions) && accountPercentageProfit > 0) ) { //delete all open positions if(PositionsTotal() > 0) { //variables for storing position properties values ulong positionTicket; long positionMagic, positionType; string positionSymbol; int totalPositions = PositionsTotal(); //scan all the open positions for(int x = totalPositions - 1; x >= 0; x--) { positionTicket = PositionGetTicket(x);//gain access to other position properties by selecting the ticket positionMagic = PositionGetInteger(POSITION_MAGIC); positionSymbol = PositionGetString(POSITION_SYMBOL); positionType = PositionGetInteger(POSITION_TYPE); int positionDigits= (int)SymbolInfoInteger(positionSymbol, SYMBOL_DIGITS); double positionVolume = PositionGetDouble(POSITION_VOLUME); ENUM_POSITION_TYPE positionType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); if(positionMagic == magicNumber && positionSymbol == _Symbol) //close the position { //print the position details Print("*********************************************************************"); PrintFormat( "#%I64u %s %s %.2f %s [%I64d]", positionTicket, positionSymbol, EnumToString(positionType), positionVolume, DoubleToString(PositionGetDouble(POSITION_PRICE_OPEN), positionDigits), positionMagic ); //print the position close details PrintFormat("Close #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType)); //send the tradeRequest if(myTrade.PositionClose(positionTicket, SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) * 3)) //success, position has been closed { if(enableAlerts) { Alert( _Symbol + " PROFIT LIQUIDATION: Just successfully closed POSITION (#" + IntegerToString(positionTicket) + "). Check the EA journal for more details." ); } PrintFormat("Just successfully closed position: #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType)); myTrade.PrintResult(); } else //trade tradeRequest failed { //print the information about the operation if(enableAlerts) { Alert( _Symbol + " ERROR ** PROFIT LIQUIDATION: closing POSITION (#" + IntegerToString(positionTicket) + "). Check the EA journal for more details." ); } PrintFormat("Position clossing failed: #%I64d %s %s", positionTicket, positionSymbol, EnumToString(positionType)); PrintFormat("OrderSend error %d", GetLastError());//print the error code } } } } } }
以下是新建并更新的 BuySellPosition(...) 方法:
bool CEmaExpertAdvisor::BuySellPosition(int positionType, string positionComment) { double volumeLot = NormalizeDouble(((SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN) * AccountInfoDouble(ACCOUNT_EQUITY)) / 10000), 2); double tpPrice = 0.0, slPrice = 0.0, symbolPrice; if(positionType == POSITION_TYPE_BUY) { if(sellPositionsVol > volumeLot && AccountInfoDouble(ACCOUNT_MARGIN_LEVEL) > 200) { volumeLot = NormalizeDouble((sellPositionsVol + volumeLot), 2); } if(volumeLot < 0.01) { volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); } if(volumeLot > SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX)) { volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); } volumeLot = NormalizeDouble(volumeLot, 2); symbolPrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK); if(TP > 0) { tpPrice = NormalizeDouble(symbolPrice + (TP * _Point), _Digits); } if(SL > 0) { slPrice = NormalizeDouble(symbolPrice - (SL * _Point), _Digits); } //if(myTrade.Buy(volumeLot, NULL, 0.0, 0.0, 0.0, positionComment)) //successfully openend position if(myTrade.Buy(volumeLot, NULL, 0.0, slPrice, tpPrice, positionComment)) //successfully openend position { if(enableAlerts) { Alert(_Symbol, " Successfully openend BUY POSITION!"); } myTrade.PrintResult(); return(true); } else { if(enableAlerts) { Alert(_Symbol, " ERROR opening a BUY POSITION at: ", SymbolInfoDouble(_Symbol, SYMBOL_ASK)); } PrintFormat("ERROR: Opening a BUY POSITION: ErrorCode = %d",GetLastError());//OrderSend failed, output the error code return(false); } } if(positionType == POSITION_TYPE_SELL) { if(buyPositionsVol > volumeLot && AccountInfoDouble(ACCOUNT_MARGIN_LEVEL) > 200) { volumeLot = NormalizeDouble((buyPositionsVol + volumeLot), 2); } if(volumeLot < 0.01) { volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); } if(volumeLot > SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX)) { volumeLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); } volumeLot = NormalizeDouble(volumeLot, 2); symbolPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); if(TP > 0) { tpPrice = NormalizeDouble(symbolPrice - (TP * _Point), _Digits); } if(SL > 0) { slPrice = NormalizeDouble(symbolPrice + (SL * _Point), _Digits); } if(myTrade.Sell(volumeLot, NULL, 0.0, slPrice, tpPrice, positionComment)) //successfully openend position { if(enableAlerts) { Alert(_Symbol, " Successfully openend SELL POSITION!"); } myTrade.PrintResult(); return(true); } else { if(enableAlerts) { Alert(_Symbol, " ERROR opening a SELL POSITION at: ", SymbolInfoDouble(_Symbol, SYMBOL_ASK)); } PrintFormat("ERROR: Opening a SELL POSITION: ErrorCode = %d",GetLastError());//OrderSend failed, output the error code return(false); } } return(false); }
请记住,实现所有方法/成员函数的逻辑均来自原始过程化代码。您将在本文底部随附的 EmaExpertAdvisor.mqh 包含文件中找到完整的 CEmaExpertAdvisor 代码。
创建新的 OOP_PriceActionEMA EA
随着交易策略蓝图(CEmaExpertAdvisor 类) 的完成,是时候将其付诸行动了。我们通过创建有执行交易能力的真实对象来令我们的蓝图创生。
生成一个新的智能系统,将其命名为 “OOP_PriceActionEMA.mq5”,并将其保存在指定的文件路径中:'Experts\OOP_Article\PriceActionEMA'。该 EA 将负责执行我们的交易策略。
首先导入 'EmaExpertAdvisor.mqh' 包含文件,其中包含 CEmaExpertAdvisor 类。
// Include the CEmaExpertAdvisor file so that it's code is available in this EA #include "EmaExpertAdvisor.mqh"
接下来,我们声明并定义用户输入变量为全局变量。这些用户输入变量是为配置 EA 所用。它们类似于以前在程序化版本中作为全局变量的参数。
//--User input variables input long magicNumber = 101;//Magic Number (Set 0 [Zero] to disable input group "" input ENUM_TIMEFRAMES tradingTimeframe = PERIOD_H1;//Trading Timeframe input int emaPeriod = 15;//Moving Average Period input int emaShift = 0;//Moving Average Shift input group "" input bool enableTrading = true;//Enable Trading input bool enableAlerts = false;//Enable Alerts input group "" input double accountPercentageProfitTarget = 6.0;//Account Percentage (%) Profit Target input double accountPercentageLossTarget = 10.0;//Account Percentage (%) Loss Target input group "" input int maxPositions = 3;//Max Positions (Max open positions in one direction) input int TP = 5000;//TP (Take Profit Points/Pips [Zero (0) to diasable]) input int SL = 500;//SL (Stop Loss Points/Pips [Zero (0) to diasable])
随后,创建一个 CEmaExpertAdvisor 实例。该行使用构造函数生成 CEmaExpertAdvisor 类的实例(ea),并按用户输入变量的值对其进行初始化。
//Create an instance/object of the included CEmaExpertAdvisor class //with the user inputed data as the specified constructor parameters CEmaExpertAdvisor ea( magicNumber, tradingTimeframe, emaPeriod, emaShift, enableTrading, enableAlerts, accountPercentageProfitTarget, accountPercentageLossTarget, maxPositions, TP, SL );
在 OnInit 函数中,我们调用 ea 实例的 GetInit 方法。该方法是 CEmaExpertAdvisor 类的一部分,负责初始化 EA。如果初始化失败,则返回 INIT_FAILED;否则,它将返回 INIT_SUCCEEDED。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- if(ea.GetInit() <= 0) { return(INIT_FAILED); } //--- return(INIT_SUCCEEDED); }
在 OnDeinit 函数中,我们调用 ea 实例的 GetDeinit 方法。该方法是 CEmaExpertAdvisor 类的一部分,负责逆初始化 EA。
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- ea.GetDeinit(); }
在 OnTick 函数中,我们调用 ea 实例的各种方法,例如 GetEma、GetPositionsData、TradingIsAllowed、TradeNow、ManageProfitAndLoss 和 PrintOnChart。这些方法封装了 EA 不同方面的行为,令代码更加模块化和有序。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- ea.GetEma(); ea.GetPositionsData(); if(ea.TradingIsAllowed()) { ea.TradeNow(); ea.ManageProfitAndLoss(); } ea.PrintOnChart(); }
我已将完整的 EA 源代码随附在本文底部的 OOP_PriceActionEMA.mq5 文件之中。
在策略测试器中测试我们的 EA
强制性确认我们的 EA 按计划运行。这可以通过将其加载到有效品种图表,并在模拟账户中进行交易,或者利用策略测试器进行全面评估来完成。虽然您可以在模拟账户上测试它,但现在,我们将使用策略测试器来评估其性能。
以下是我们将在策略测试器中应用的设置:
-
经纪商:MT5 Metaquotes 模拟账户(在 MT5 安装时自动创建)
-
品种:EURJPY
-
测试区间(日期):1 年 2 个月(2023 年 1 月至 2024 年 3 月)
-
建模类型: 基于真实即刻报价的每次即刻报价
-
本金: $10,000 美元
-
杠杆: 1:100
结束语
我们探索面向对象编程范式至此结束,它是构建软件的强力工具。我们已经驾驭了这种强力范式的复杂性,将代码转换为模块化、可重用的结构。从过程化编程到面向对象编程的转变,带至一个新的组织、封装和抽象水平,为开发人员提供了一个管理复杂项目的强壮框架。
在本文中,您还学习了如何使用面向对象原则将过程化 MQL5 代码转换为面向对象的代码,并强调了类、对象和继承的重要性。通过将数据和功能封装在类内部,我们增强了代码的模块化和可维护性。
当您开始自己的 MQL5 项目时,请记住,面向对象编程的优势在于它能够针对现实世界的实体和关系进行建模,从而培育出能反映其所代表系统复杂性的代码。我在文章末尾随附了我们所创建的各种类,和 EA 的所有源代码文件。
感谢您陪伴我深入探讨不同的编程范式。愿我们揭示的原则和实践能丰富您所致力的编码。请继续关注我们,不断寻求利用深受喜爱、且强力的 MQL5 语言开发简单实用的交易系统的更多见解和实际示例。
感谢您抽出宝贵时间阅读本文,我希望您在 MQL5 开发之旅和致力的交易中得偿佳愿。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/14161


