软件开发和 MQL5 中的设计范式(第 3 部分):行为范式 1(基础篇)
◍ 用行为范式切分 MQL5 交易逻辑
MQL5 里写 EA 最容易被忽略的一步,是先决定「行为范式」而不是先堆代码。2024 年 7 月 17 日发布的这套开发思路把交易程序按行为切分,使订单触发、仓位管理、风险控制各自独立成块,便于在 MT5 里单独回测某一段逻辑。 对于外汇与贵金属交易者,这种切法直接对应盯盘时的决策分层:信号归信号,执行归执行。高杠杆下混写容易在极端波动中断错流程,拆开后哪一段出问题一目了然。 开 MT5 新建一个 EA,先别写 OnTick 里的下单,而是建三个 void 函数分别对应信号计算、订单发送、止损跟进,跑通后再用全局变量串联,这是验证行为范式的最低成本做法。
「行为设计范式要解决什么」
这套系列的前两篇分别拆过创建型与结构型范式,本篇进入第三类:行为设计范式。它的核心不是对象怎么生、怎么拼,而是对象之间如何通信、职责如何分配,从而让 MT5 上的 MQL5 程序在改需求时不至于推倒重来。 文中会覆盖行为范式下几个关键类型:责任链、命令、解释器、迭代器、调解器。理解这些之后,你能在 MetaTrader 5 里用 MQL5 写出可维护、可重用、易扩展的 EA 或指标框架,而不是把所有逻辑塞进一个 OnTick。 如果你刚接触这个系列,建议先补创建型与结构型两篇,否则直接看行为范式容易漏掉依赖前提。外汇与贵金属交易本身高风险,文中代码与范式仅作技术教学,实盘使用须自行承担风险。
行为范式:对象之间怎么分活、怎么通话
前面聊过创建性范式管「对象怎么生出来」,结构性范式管「对象怎么拼成大结构」。行为范式换了个视角:它关心的是对象之间的责任分配和通信方式。换句话说,同样是几个对象协作,行为范式决定「谁该干哪块活、消息走哪条路」,而不是只关心它们长什么样。 这类范式数量不少,MQL5 工程里常见的有职责链、命令、解释器、迭代器、调解器、备忘录、观察者、状态、策略、模板方法、访问者共 11 种。本文先挑前 5 种拆:职责链把请求沿一条接收者链传递,发送方和接收方解耦;命令把请求封装成对象,可带参数、可进队列、可撤销;解释器用既定文法解析语言句子;迭代器让你不暴露聚合对象底层就能顺序访问元素;调解器用封装对象接管一组对象的交互,各自独立可改。 对做 EA 或指标的交易者来说,行为范式不是玄学:比如用命令范式把「平仓 / 改止盈」封装成对象,小布类 AIGC 工具就能统一调度、还能回滚误操作;用迭代器遍历多周期 K 线容器,比直接裸下标更不容易越界。外汇和贵金属杠杆高、滑点随机,任何封装都只是降低耦合,不消除爆仓风险。 我们后续每个范式都按三个问题走:它干什么用、解决什么设计痛点、在 MQL5 里怎么落地。先把前五种吃透,再碰观察者和策略这类更贴近信号分发的类型。
◍ MT5 里把请求甩给链条的写法
责任链本质是把客户端请求沿一组对象依次传递,谁该管谁处理,管不了就转交下一个。在 EA 或指标里,当你面对多类行情条件(比如突破、回踩、假突破)需要不同模块响应,又不想让发信号的地方知道具体由谁执行,这种解耦方式就派上用场。 它也不是免费午餐。链一长,请求可能被层层转发才落到处理者,存在效率损耗;更麻烦的是,如果链尾对象也无法处理,而你没有兜底逻辑,这次请求就静默丢失了。外汇与贵金属波动剧烈,这类静默失败在实盘可能让你错过关键风控触发,属高风险场景。 下面这段 MQL5 演示了最小可用链:Handler 定义接口并持有后继者指针,ConcreteHandler1 只接 request==1,ConcreteHandler2 只接 request==2,其余转交。Client 里把 h1.successor 设为 h2,再分别发 1 和 2,你能直接在 MT5 Experts 日志看到转发与处理记录。 [CODE] namespace Chain_Of_Responsibility class Handler { public: Handler* successor; virtual void HandleRequest(int)=0; ~Handler(void); }; Handler::~Handler(void) { delete successor; } class ConcreteHandler1:public Handler { public: void HandleRequest(int); }; void ConcreteHandler1::HandleRequest(int request) { if(request==1) Print("The request: ",request,". The request handled by: ",&this); else if(CheckPointer(successor)) { Print("The request: ",request,". The request cannot be handled by ",&this,", but it is forwarding to the successor..."); successor.HandleRequest(request); } } class ConcreteHandler2:public Handler { public: void HandleRequest(int); }; void ConcreteHandler2::HandleRequest(int request) { if(request==2) Print("The request: ",request,". The request handled by: ",&this); else if(CheckPointer(successor)) { Print("The request: ",request,". The request cannot be handled by ",&this,", forwarding to successor..."); successor.HandleRequest(request); } } class Client { public: string Output(); void Run(); }; string Client::Output() { return __FUNCTION__; } void Client::Run() { Handler* h1=new ConcreteHandler1(); Handler* h2=new ConcreteHandler2(); h1.successor=h2; h1.HandleRequest(1); h1.HandleRequest(2); delete h1; } //+------------------------------------------------------------------+
| // | Chain_Of_Responsibility.mqh |
|---|
[/CODE] 逐行看关键点:Handler 的析构里 delete successor,意味着链头释放会级联释放整条链,你只需 delete h1 一次;ConcreteHandler1 中 CheckPointer(successor) 先判空再转发,避免对空指针调用;Client::Run 里 h1 收到 2 时会打印转发日志再由 h2 处理,验证了请求未指定接收方也能落地的可能。 把这段代码存成 Chain_Of_Responsibility.mqh,在 EA 的 OnStart 里 #include 后 new Client()->Run(),跑一遍就知道链顺序怎么影响输出。后续你要接真实信号,只需把 request 换成你的枚举(如 ENUM_SIGNAL),并把 if(request==1) 改成条件匹配即可。
class="kw">namespace Chain_Of_Responsibility class Handler { class="kw">public: Handler* successor; class="kw">virtual class="type">void HandleRequest(class="type">int)=class="num">0; ~Handler(class="type">void); }; Handler::~Handler(class="type">void) { class="kw">delete successor; } class ConcreteHandler1:class="kw">public Handler { class="kw">public: class="type">void HandleRequest(class="type">int); }; class="type">void ConcreteHandler1::HandleRequest(class="type">int request) { if(request==class="num">1) Print("The request: ",request,". The request handled by: ",&this); else if(CheckPointer(successor)) { Print("The request: ",request,". The request cannot be handled by ",&this,", but it is forwarding to the successor..."); successor.HandleRequest(request); } } class ConcreteHandler2:class="kw">public Handler { class="kw">public: class="type">void HandleRequest(class="type">int); }; class="type">void ConcreteHandler2::HandleRequest(class="type">int request) { if(request==class="num">2) Print("The request: ",request,". The request handled by: ",&this); else if(CheckPointer(successor)) { Print("The request: ",request,". The request cannot be handled by ",&this,", forwarding to successor..."); successor.HandleRequest(request); } } class Client { class="kw">public: class="type">class="kw">string Output(); class="type">void Run(); }; class="type">class="kw">string Client::Output() { class="kw">return __FUNCTION__; } class="type">void Client::Run() { Handler* h1=new ConcreteHandler1(); Handler* h2=new ConcreteHandler2(); h1.successor=h2; h1.HandleRequest(class="num">1); h1.HandleRequest(class="num">2); class="kw">delete h1; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Chain_Of_Responsibility.mqh |
「用责任链把信号判定拆成接力」
在 MT5 的 EA 或指标里,把多个判定条件写成一条可传递的链,比堆 if-else 更利于后期加规则。下面这段 C++ 风格代码演示了基础骨架:Handler 基类留一个 successor 指针和纯虚函数 HandleRequest,具体类只认自己能处理的请求编号,处理不了就转给后继者。 ConcreteHandler1 只接 request==1,ConcreteHandler2 只接 request==2。Client::Run 里把 h1 的 successor 设成 h2,然后分别发 1 和 2:发 1 时 h1 直接打印处理,发 2 时 h1 打印转发再交给 h2。实跑会在专家日志看到两次不同对象的输出。 把这套映射到价格行为:h1 可改成「吞没形态过滤器」,h2 改成「ATR 阈值过滤器」,successor 串起来后,任一段不通过就自动滑到下一环。外汇与贵金属波动跳空频繁,这种链若漏了 CheckPointer 判空,可能在切换品种时崩 EA,务必在转发前确认指针有效。
class="kw">namespace Chain_Of_Responsibility { class Handler { class="kw">public: Handler* successor; class="kw">virtual class="type">void HandleRequest(class="type">int)=class="num">0; ~Handler(class="type">void); }; Handler::~Handler(class="type">void) { class="kw">delete successor; } class ConcreteHandler1:class="kw">public Handler { class="kw">public: class="type">void HandleRequest(class="type">int); }; class="type">void ConcreteHandler1::HandleRequest(class="type">int request) { if(request==class="num">1) Print("The request: ",request,". The request handled by: ",&this); else if(CheckPointer(successor)) { Print("The request: ",request,". The request cannot be handled by ",&this,", but it is forwarding to the successor..."); successor.HandleRequest(request); } } class ConcreteHandler2:class="kw">public Handler { class="kw">public: class="type">void HandleRequest(class="type">int); }; class="type">void ConcreteHandler2::HandleRequest(class="type">int request) { if(request==class="num">2) Print("The request: ",request,". The request handled by: ",&this); else if(CheckPointer(successor)) { Print("The request: ",request,". The request cannot be handled by ",&this,", but it is forwarding to successor..."); successor.HandleRequest(request); } } class Client { class="kw">public: class="type">class="kw">string Output(); class="type">void Run(); }; class="type">class="kw">string Client::Output() { class="kw">return __FUNCTION__; } class="type">void Client::Run() { Handler* h1=new ConcreteHandler1(); Handler* h2=new ConcreteHandler2(); h1.successor=h2; h1.HandleRequest(class="num">1); h1.HandleRequest(class="num">2); class="kw">delete h1; } }
把交易指令封进对象里
命令范式(也叫动作或业务范式)的核心,是把一次请求整个包成一个对象。这样做的好处是发送方、处理器、接收方三者解耦:改命令参数时不用动发送和接收代码,类里那种几百行的庞大函数也能拆开。它还能原生支持撤消操作,适合把‘下单’‘平仓’‘改止损’这类动作当成数据来传递。 不过在 MT5 里真用起来有几个坑:它通常得搭配别的范式;每个命令都 new 一个类,对象数量会膨胀;为每个命令单独建对象本身就有点违背面向对象精简原则。设计上它解决的是这几类事——按动作给对象传参、把操作排队到不同时间执行、支持撤消、崩溃后靠日志重放、在底层之上搭高级结构。 下面这段是 MQL5 里跑通该范式的最小骨架,五个参与者全在一个命名空间里。Receiver 只留了 Action 空壳,ConcreteCommand 执行时把 m_state 置 1,Invoker 存命令再调 Execute,Client 负责装配。 [CODE] namespace Command class Receiver { public: Receiver(void); Receiver(Receiver&); void Action(void); }; Receiver::Receiver(void) { } Receiver::Receiver(Receiver &src) { } void Receiver::Action(void) { } class Command { protected: Receiver* m_receiver; public: Command(Receiver*); ~Command(void); virtual void Execute(void)=0; }; Command::Command(Receiver* receiver) { m_receiver=new Receiver(receiver); } Command::~Command(void) { if(CheckPointer(m_receiver)==1) { delete m_receiver; } } class ConcreteCommand:public Command { protected: int m_state; public: ConcreteCommand(Receiver*); void Execute(void); }; ConcreteCommand::ConcreteCommand(Receiver* receiver): Command(receiver), m_state(0) { } void ConcreteCommand::Execute(void) { m_receiver.Action(); m_state=1; } class Invoker { public: ~Invoker(void); void StoreCommand(Command*); void Execute(void); protected: Command* m_command; }; Invoker::~Invoker(void) { if(CheckPointer(m_command)==1) { delete m_command; } } void Invoker::StoreCommand(Command* command) { m_command=command; } void Invoker::Execute(void) { m_command.Execute(); } class Client { public: string Output(void); void Run(void); }; string Client::Output(void) { 逐行拆解:namespace Command 把整套参与者收进独立作用域,避免和 EA 其它逻辑撞名。Receiver 类目前 Action 是空实现,实战里这里写具体下单或改仓逻辑;Command 基类用纯虚 Execute 强制子类实现,m_receiver 在构造时拷贝指针并在析构用 CheckPointer==1 判断后释放。ConcreteCommand 的 Execute 先调接收方动作再把 m_state 写成 1,相当于标记‘已执行’。Invoker 只持有当前命令指针,StoreCommand 装入、Execute 触发;Client 留 Output 和 Run 接口待填。 开 MT5 新建一个 mqh 把上面代码贴进去编译,先确认零报错;之后把 Receiver::Action 换成 Print("order sent") 就能在专家日志看到命令被调起。外汇和贵金属杠杆高、滑点随机,这类封装只解决代码结构,不暗示任何执行胜率。
class="kw">namespace Command class Receiver { class="kw">public: Receiver(class="type">void); Receiver(Receiver&); class="type">void Action(class="type">void); }; Receiver::Receiver(class="type">void) { } Receiver::Receiver(Receiver &src) { } class="type">void Receiver::Action(class="type">void) { } class Command { class="kw">protected: Receiver* m_receiver; class="kw">public: Command(Receiver*); ~Command(class="type">void); class="kw">virtual class="type">void Execute(class="type">void)=class="num">0; }; Command::Command(Receiver* receiver) { m_receiver=new Receiver(receiver); } Command::~Command(class="type">void) { if(CheckPointer(m_receiver)==class="num">1) { class="kw">delete m_receiver; } } class ConcreteCommand:class="kw">public Command { class="kw">protected: class="type">int m_state; class="kw">public: ConcreteCommand(Receiver*); class="type">void Execute(class="type">void); }; ConcreteCommand::ConcreteCommand(Receiver* receiver): Command(receiver), m_state(class="num">0) { } class="type">void ConcreteCommand::Execute(class="type">void) { m_receiver.Action(); m_state=class="num">1; } class Invoker { class="kw">public: ~Invoker(class="type">void); class="type">void StoreCommand(Command*); class="type">void Execute(class="type">void); class="kw">protected: Command* m_command; }; Invoker::~Invoker(class="type">void) { if(CheckPointer(m_command)==class="num">1) { class="kw">delete m_command; } } class="type">void Invoker::StoreCommand(Command* command) { m_command=command; } class="type">void Invoker::Execute(class="type">void) { m_command.Execute(); } class Client { class="kw">public: class="type">class="kw">string Output(class="type">void); class="type">void Run(class="type">void); }; class="type">class="kw">string Client::Output(class="type">void) {