理解编程范式(第 2 部分):面向对象方式开发价格行为智能系统(基础篇)
📘

理解编程范式(第 2 部分):面向对象方式开发价格行为智能系统(基础篇)

第 1/3 篇

◍ 用面向对象思路搭价格行为智能系统

在 MT5 里做价格行为类策略,过程式写法很快会陷入函数与全局变量纠缠。把 K 线形态、信号过滤、订单管理各自封装成类,能让回测与实盘逻辑复用度明显提高。 以 2024 年 11 月 29 日发布的范例来说,原文在 MQL5 社区获得 955 次浏览、8 条讨论,说明这类范式迁移已有真实交易者关注。外汇与贵金属杠杆高,任何智能系统都只是概率工具,实盘前务必在策略测试器跑足样本。 下面这段骨架演示了如何把「锤子线识别」抽象为一个类方法,读者可直接拷进 MT5 的 EA 模板里改参数验证。

MQL5 / C++
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 属高风险操作,参数误用可能导致异常下单。

MQL5 / C++
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 编译能看到重定义报错,删掉空体那份即可过编。

MQL5 / C++
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 实例再让作用域结束,终端会连续打印三次析构提示,验证生命周期没问题。

MQL5 / C++
  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

常见问题

变量和逻辑全堆在一起,加一个新形态就要动老代码,容易改崩。面向对象把每种形态封成类,扩展时只加派生类,不动原有结构。
能。手机图纸是类,按图生产出的每台真机是对象;图纸规定有什么功能,真机才实际跑起来、存自己的状态。
小布可把你的形态规则转成类结构草图,并标出哪些该做基类、哪些该派生,省去手工理继承关系的重复劳动。
容易漏掉释放对象里开的资源(如动态数组、句柄)。构造里申请了什么,析构里就要对称清掉,否则跑久了占内存。
单个方法只管一件事,出错了能直接定位到那一个函数;后期调某个行为只改对应方法,不影响其他部分。