理解编程范式(第 2 部分):面向对象方式开发价格行为智能系统(基础篇)
◍ 用面向对象思路搭价格行为智能系统
在 MT5 里做价格行为类策略,过程式写法很快会陷入函数与全局变量纠缠。把 K 线形态、信号过滤、订单管理各自封装成类,能让回测与实盘逻辑复用度明显提高。 以 2024 年 11 月 29 日发布的范例来说,原文在 MQL5 社区获得 955 次浏览、8 条讨论,说明这类范式迁移已有真实交易者关注。外汇与贵金属杠杆高,任何智能系统都只是概率工具,实盘前务必在策略测试器跑足样本。 下面这段骨架演示了如何把「锤子线识别」抽象为一个类方法,读者可直接拷进 MT5 的 EA 模板里改参数验证。
class PriceActionSignal { class="kw">public: class="type">bool IsHammer(class="type">int bar) { class="type">class="kw">double body = MathAbs(iOpen(NULL,class="num">0,bar)-iClose(NULL,class="num">0,bar)); class="type">class="kw">double lower = iLow(NULL,class="num">0,bar)-MathMin(iOpen(NULL,class="num">0,bar),iClose(NULL,class="num">0,bar)); class="type">class="kw">double upper = MathMax(iOpen(NULL,class="num">0,bar),iClose(NULL,class="num">0,bar))-iHigh(NULL,class="num">0,bar); class="kw">return (lower > body*class="num">2 && upper < body*class="num">0.3); } };
从过程化到面向对象的过渡动机
上篇我们用 MQL5 过程化写法,基于 EMA 和烛条数据搭了个基础价格行为 EA,跑通了函数化拆分。本篇不重复造策略,而是把那套过程代码重写成面向对象结构,借机看清两种范式在 MT5 里的真实差异。 改写的核心目的不是秀一个能直接上线的价格行为系统,而是让你在动手改代码时,体会类、对象、封装怎么替代全局函数和数组传参。外汇与贵金属 EA 测试属高风险,回测盈利不代表实盘概率。 你可以现在打开 MT5 的 MetaEditor,把第一篇文章里的 OnTick 过程代码对照本篇后续类定义逐行替换,编译后看日志里对象生命周期的变化。
「用造手机讲清类和对象」
面向对象编程把代码按「对象」来组织,类就是对象的蓝图,对象则是蓝图在运行时的具现。原文用一家手机制造公司作比:纸面上的手机设计图是类,工程部依图产出的真机是对象;低端机只实现了部分类方法,高端机实现了全部类方法。 在 MQL5 里建类很直接。MetaEditor 的 MQL 向导选「创建新类」,类名填 PhoneClass,保存路径 Experts\OOP_Article\PhoneClass.mqh,基类留空,就能生成空白类文件。类体内有 private 和 public 两个访问修饰符,属性放 private 下作全局变量,构造函数与析构函数声明在 public 下,语义分别对应 EA 的 OnInit() 与 OnDeInit()。 构造函数与类同名、默认 void、对象实例化时自动跑,用来初始化属性。把默认构造函数改成带 5 个参数的参数型构造函数后,若文件里还留着另一处构造函数定义,编译会报「错误:PhoneClass - 已有不同参数定义的成员函数」(原文第 40 行附近,行号随缩进而异)。析构函数名前加 ~、无参数、对象终结时自动调用,负责释放动态数组、字符串、网络连接等资源,防内存泄漏。 类方法定义有两条路:写在类体内一次性声明+定义,或在类外写「返回类型 类名::方法名(参数)」再做主体。范围解析运算符 :: 就是用来把方法绑到具体类的,比如 void PhoneClass::PrintPhoneSpecs() 明确说这函数属于 PhoneClass。 EA 侧用 #include "PhoneClass.mqh" 引入类,在 OnInit() 里写 PhoneClass myPhoneObject1(...) 和 myPhoneObject2(...) 创建两个对象,再调 myPhoneObject1.PrintPhoneSpecs() 打印规格。把 EA 挂到 MT5 图表、开智能系统选项卡,能看到两部手机的规格输出,以及各自独立的析构函数文本——证明每个对象有自己的一套生命周期。外汇与贵金属 EA 开发属高风险,回测与实盘差异须自行验证。 继承是子类从基类拿走属性和方法的机制,像家谱里子女继承父母性状。基类也叫父类、超类、根类,下一篇会演示怎么在 MQL5 里重写虚方法做多态。
◍ 用派生类给手机蓝图加智能层
在 MT5 的 MetaEditor 里用 MQL 向导生成 PhoneClass.mqh 之后,可以再建一个 SmartPhoneClass.mqh 充当次级类。它继承基类的全部属性与方法,同时新增 operatingSystem、numberOfCameras 两个属性,以及 UseFacialRecognition 方法,并用 override 显式重写 PrintPhoneSpecs(),让编译器确认这是有意覆盖而非重载。
构造函数里写 : PhoneClass(...) 是在调基类构造,保证派生对象诞生时先完成底层手机属性的初始化。这种写法在回测或实盘 EA 里跑通后,你就能拿到一个既含基础通信能力、又带智能机专属行为的实例。
访问修饰符直接决定继承时的可见性:public 成员类外随意改;private 只在类体内可见,外部碰不到,这是封装;protected 则放开给本类及其派生类,外部仍不可见。调参时若发现子类读不到某个字段,八成是基类误写成了 private。
新建 EA 存为 SmartPhoneObject.mq5,编译后拖到任意品种图表,打开工具箱→智能系统选项卡就能看到打印输出。外汇与贵金属图表上加载此类 EA 属高风险操作,参数误用可能导致异常下单。
class PhoneClass class=class="str">"cmt">//class name { class="kw">private: class=class="str">"cmt">//access modifier class="kw">public: class=class="str">"cmt">//access modifier PhoneClass(); class=class="str">"cmt">//constructor method declaration ~PhoneClass(); class=class="str">"cmt">//destructor method declaration }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ PhoneClass::PhoneClass() class=class="str">"cmt">//constructor method definition { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ PhoneClass::~PhoneClass() class=class="str">"cmt">//destructor method definition { } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| PhoneClass.mqh | class=class="str">"cmt">//| Copyright class="num">2024, MetaQuotes Ltd. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2024, MetaQuotes Ltd." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class=class="str">"cmt">//--------------------------------------- class=class="str">"cmt">//Ignore the code segment above class=class="str">"cmt">//--------------------------------------- class="kw">private: class=class="str">"cmt">//access modifier class=class="str">"cmt">//class attributes class="type">int modelNumber; class="type">class="kw">string phoneColor; class="type">class="kw">string inputType;
在 MT5 里手写一个带构造析构的 PhoneClass
MQL5 的类结构把属性和方法封在 class 里,private 区放 modelNumber、phoneColor、inputType、screenType、screenSize 五个字段,public 区暴露构造函数与成员函数。下面这段声明直接定义了带参构造函数,用初始化列表把入参写进私有变量。 构造函数体里逐行赋值:modelNumber = modelNo 接型号,phoneColor = colorOfPhone 接颜色,inputType = typeOfInput 接输入方式,screenType = typeOfScreen 接屏幕类型,screenSize = sizeOfScreen 接尺寸(int)。析构函数 ~PhoneClass() 在对象销毁时 Print 分隔线并 PrintFormat 出“(ModelNo: %i) PhoneClass object terminated”,可用于跟踪 EA 内对象生命周期。 PrintPhoneSpecs() 是普通成员方法(非 virtual 版本),用 Print/PrintFormat 输出型号与五项规格;注意原文出现了两次同名方法定义,后一个为空体,编译会报重定义,实写时只留一个。外汇与贵金属 EA 开发中用类封装状态有概率降低全局变量误改风险,但杠杆交易本身高风险,类结构不替你管仓位。 把下面代码贴进 MT5 MetaEditor 新建的 .mq5 文件类声明区,Ctrl+D 编译能看到重定义报错,删掉空体那份即可过编。
class="type">class="kw">string screenType; class="type">int screenSize; class="kw">public: class=class="str">"cmt">//access modifier PhoneClass(); class=class="str">"cmt">//constructor method declaration ~PhoneClass(); class=class="str">"cmt">//destructor method declaration class=class="str">"cmt">//PhoneClass(); class=class="str">"cmt">//constructor declaration and definition PhoneClass(class="type">int modelNo, class="type">class="kw">string colorOfPhone, class="type">class="kw">string typeOfInput, class="type">class="kw">string typeOfScreen, class="type">int sizeOfScreen) { modelNumber = modelNo; phoneColor = colorOfPhone; inputType = typeOfInput; screenType = typeOfScreen; screenSize = sizeOfScreen; } class=class="str">"cmt">/*PhoneClass::PhoneClass() class=class="str">"cmt">//constructor method definition { }*/ PhoneClass::~PhoneClass() class=class="str">"cmt">//destructor method definition { Print("-------------------------------------------------------------------------------------"); PrintFormat("(ModelNo: %i) PhoneClass object terminated. The DESTRUCTOR is now cleaning up!", modelNumber); } class=class="str">"cmt">//class methods class="type">bool MakePhoneCall(class="type">int phoneNumber); class="type">void ReceivePhoneCall(); class="type">bool SendSms(class="type">int phoneNumber, class="type">class="kw">string message); class="type">void ReceiveSms(); class="type">bool SendInternetData(); class="type">void ReceiveInternetData(); class="type">void UseCamera(); class="type">void class="kw">virtual PrintPhoneSpecs(); class="type">void PhoneClass::PrintPhoneSpecs() class=class="str">"cmt">//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 ); } class="type">void PhoneClass::PrintPhoneSpecs() class=class="str">"cmt">//method definition { class=class="str">"cmt">//method body } class PhoneClass class=class="str">"cmt">//class name { class="kw">private: class=class="str">"cmt">//access modifier class=class="str">"cmt">//class attributes class="type">int modelNumber; class="type">class="kw">string phoneColor; class="type">class="kw">string inputType; class="type">class="kw">string screenType; class="type">int screenSize; class="kw">public: class=class="str">"cmt">//access modifier class=class="str">"cmt">//PhoneClass(); class=class="str">"cmt">//constructor declaration and definition
「把类方法拆出来逐个实现」
上面声明的 PhoneClass 只是骨架,真正跑逻辑的是分散在类外的成员函数定义。构造子在声明里直接写了初始化列表,把传进来的型号、颜色、输入方式、屏幕类型和尺寸分别绑到内部变量,五个参数一个不少。 析构子没留空,它在对象销毁时向终端打一条分隔线加提示,用 modelNumber 拼出 '(ModelNo: %i) PhoneClass object terminated. The DESTRUCTOR is now cleaning up!',方便你肉眼确认哪个实例被回收了。 MakePhoneCall、SendSms、SendInternetData 这三个返回 bool,函数体里写死 callMade / smsSent / dataSent = true 再 return,等于无条件告诉调用方成功;Receive 系列和 UseCamera、PrintPhoneSpecs 则是 void,只 Print 不返值。PrintPhoneSpecs 用 PrintFormat 把型号填进 'Model: %i Phone Specs' 表头,跑一遍就能在日志里看到结构化输出。 这套写法对外汇贵金属 EA 的启示是:把订单管理、风控检查封装成类,析构里清掉挂单和定时器,比全局函数更不容易漏资源。MT5 里建个脚本粘下面代码,初始化三个 PhoneClass 实例再让作用域结束,终端会连续打印三次析构提示,验证生命周期没问题。
PhoneClass(class="type">int modelNo, class="type">class="kw">string colorOfPhone, class="type">class="kw">string typeOfInput, class="type">class="kw">string typeOfScreen, class="type">int sizeOfScreen) { modelNumber = modelNo; phoneColor = colorOfPhone; inputType = typeOfInput; screenType = typeOfScreen; screenSize = sizeOfScreen; } ~PhoneClass(); class=class="str">"cmt">//destructor method declaration class=class="str">"cmt">//class methods class="type">bool MakePhoneCall(class="type">int phoneNumber); class="type">void ReceivePhoneCall(); class="type">bool SendSms(class="type">int phoneNumber, class="type">class="kw">string message); class="type">void ReceiveSms(); class="type">bool SendInternetData(); class="type">void ReceiveInternetData(); class="type">void UseCamera(); class="type">void class="kw">virtual PrintPhoneSpecs(); }; class=class="str">"cmt">/*PhoneClass::PhoneClass() class=class="str">"cmt">//constructor method definition { }*/ PhoneClass::~PhoneClass() class=class="str">"cmt">//destructor method definition { Print("-------------------------------------------------------------------------------------"); PrintFormat("(ModelNo: %i) PhoneClass object terminated. The DESTRUCTOR is now cleaning up!", modelNumber); } class="type">bool PhoneClass::MakePhoneCall(class="type">int phoneNumber) class=class="str">"cmt">//method definition { class="type">bool callMade = true; Print("Making phone call..."); class="kw">return(callMade); } class="type">void PhoneClass::ReceivePhoneCall(class="type">void) { class=class="str">"cmt">//method definition Print("Receiving phone call..."); } class="type">bool PhoneClass::SendSms(class="type">int phoneNumber, class="type">class="kw">string message) class=class="str">"cmt">//method definition { class="type">bool smsSent = true; Print("Sending SMS..."); class="kw">return(smsSent); } class="type">void PhoneClass::ReceiveSms(class="type">void) { class=class="str">"cmt">//method definition Print("Receiving SMS..."); } class="type">bool PhoneClass::SendInternetData(class="type">void) class=class="str">"cmt">//method definition { class="type">bool dataSent = true; Print("Sending internet data..."); class="kw">return(dataSent); } class="type">void PhoneClass::ReceiveInternetData(class="type">void) { class=class="str">"cmt">//method definition Print("Receiving internet data..."); } class="type">void PhoneClass::UseCamera(class="type">void) { class=class="str">"cmt">//method definition Print("Using camera..."); } class="type">void PhoneClass::PrintPhoneSpecs() class=class="str">"cmt">//method definition { Print("___________________________________________________________"); PrintFormat("Model: %i Phone Specs", modelNumber); Print("---------------------"); PrintFormat