MVC 设计范式及其应用(第 2 部分):三个组件之间相互作用示意图·进阶篇
(2/3)· 接上篇组件定义,本篇用 WPR 指标画出模型、视图、控制器的真实调用链路
控制器类怎么把输入、模型、视图串起来
在 MT5 的 Indicators 目录下建 Controller.mqh,用 CController 类统一管理生命周期。它持有一个 CInputManager 指针,Initialize() 里先初始化输入模块,若返回码不是 INIT_SUCCEEDED 就直接中断,避免源数据出错后继续跑空逻辑。 Update() 负责刷新源数据,任何一步失败就返回 false,调用方据此可跳过当根 K 线的计算。控制器后续还会持有 CModel 和 CView 指针,在构造时 new 出来,由它统一调 Initialize 和 Update,组件越多这套接管越省心。 指标主文件 WPR.mq5 因此极薄:OnInit 只 new 控制器并调 Initialize,OnDeinit 用 CheckPointer 判空后 delete,OnCalculate 把报价数组原样转交给 pController.Tick()。实测主文件代码行数可压到 30 行以内,逻辑改动基本只发生在控制器内部。 下面对核心片段做逐行拆解:CController 声明里 public 暴露构造、析构、Initialize(返回 int 状态码)、Update(返回 bool);private 里 CInputManager* pInput 是源数据入口。Initialize 实现先 pInput.Initialize(),非 INIT_SUCCEEDED 即返回错误码。Update 实现仅转调 pInput.Update() 并透传结果。带 pModel、pView 的版本在构造函数中依次 new 三件组件;OnInit 返回 pController.Initialize() 的 int,OnDeinit 判 POINTER_INVALID 才 delete 防重复释放;OnCalculate 把 11 个参数全转给 Tick(),主文件因此不写任何指标算法。
CInputManager pInput; class CController { class="kw">public: CController(); ~CController(); class="type">int Initialize(); class="type">bool Update(); class="kw">protected: class="kw">private: CInputManager* pInput; }; ... class="type">int CController::Initialize() { class="type">int iResult = pInput.Initialize(); if (iResult != INIT_SUCCEEDED) class="kw">return iResult; class="kw">return INIT_SUCCEEDED; } class="type">bool CController::Update() { class="type">bool bResult = pInput.Update(); class="kw">return bResult; } class CController { class="kw">public: ... class="kw">private: CInputManager* pInput; CModel* pModel; CView* pView; } ... CController::CController() { pInput = new CInputManager(); pModel = new CModel(); pView = new CView(); } ... CController* pController; class="type">int OnInit() { pController = new CController(); class="kw">return pController.Initialize(); } ... class="type">void OnDeinit(const class="type">int reason) { if (CheckPointer(pController) != POINTER_INVALID) class="kw">delete pController; } class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">class="kw">datetime &time[], const class="type">class="kw">double &open[], const class="type">class="kw">double &high[], const class="type">class="kw">double &low[], const class="type">class="kw">double &close[], const class="type">long &tick_volume[], const class="type">long &volume[], const class="type">int &spread[]) { class="kw">return pController.Tick(rates_total, prev_calculated, time, open, high, low, close, tick_volume, volume, spread); } class="type">int CController::Initialize() { if (CheckPointer(pInput) == POINTER_INVALID || CheckPointer(pModel) == POINTER_INVALID || CheckPointer(pView) == POINTER_INVALID) class="kw">return INIT_FAILED; class="type">int iResult = pInput.Initialize();
「控制器里的初始化链路与更新入口」
MQL5 里把输入、视图、模型拆成三层后,控制器得按顺序把指针喂进去,任何一步没拿到 INIT_SUCCEEDED 就直接返回错误码,避免半初始化状态跑策略。 下面这段就是典型的链式初始化:先 init 输入层,再把输入指针传给视图层 init,最后把输入和视图两个指针一起丢给模型层 init,三层全过才返成功。 [CODE] if (iResult != INIT_SUCCEEDED) return iResult; iResult = pView.Initialize(GetPointer(pInput)); if (iResult != INIT_SUCCEEDED) return iResult; iResult = pModel.Initialize(GetPointer(pInput), GetPointer(pView)); if (iResult != INIT_SUCCEEDED) return iResult; return INIT_SUCCEEDED; } ... bool CController::Update() { bool bResult = pInput.Update(); return bResult; } ... [/CODE] 逐行看:第1行检查上一步初始化结果,非成功立即退出;pView.Initialize 拿输入层指针做视图绑定;模型层 Initialize 同时收输入与视图指针,缺一个都可能让后续计算引用空对象。 Update 方法目前只调了 pInput.Update() 并返回其结果,视图和模型层没在每帧刷新——若你的 EA 依赖实时重算信号,这里大概率要补 pView.Update() 与 pModel.Update(),否则 MT5 回测里可能看到信号滞后。 外汇与贵金属杠杆高、滑点随机,改完这类结构先在策略测试器用 2023 年 XAUUSD 的 M1 数据跑一遍,确认不会因指针未刷新报 array out of range。
if (iResult != INIT_SUCCEEDED) class="kw">return iResult; iResult = pView.Initialize(GetPointer(pInput) ); if (iResult != INIT_SUCCEEDED) class="kw">return iResult; iResult = pModel.Initialize(GetPointer(pInput), GetPointer(pView) ); if (iResult != INIT_SUCCEEDED) class="kw">return iResult; class="kw">return INIT_SUCCEEDED; } ... class="type">bool CController::Update() { class="type">bool bResult = pInput.Update(); class="kw">return bResult; } ...
◍ 把决策逻辑收进模型层
模型是指标里真正做决策的那一层。控制器把源数据喂进来,模型基于这些数据算结果——在简单 WRP 指标里,就是判断威廉指标数值并准备输出。问题在于:如果模型直接 #include 控制器的 Input.mqh,就背上了不该有的依赖,业务逻辑反而被易变的输入模块绑死。 正确的反向约束是让模型定义自己要什么。我们在 Model 文件夹建 InputBase.mqh,只声明一个 GetPeriod() const 接口;模型 Initialize() 收 IInputBase* 指针,CInputParam 去实现这个接口。这样模型文件里不再出现控制器头文件,依赖方向翻了过来。 指标缓冲区也不能塞进模型,它属于视图层。于是再建 IOutputBase.h,留两个方法:SetValue(shift,value) 写值、GetValue(shift) 读值。模型私密字段加 IOutputBase* pOutput,计算里原本直接写缓冲区的动作全改成 pOutput.SetValue(...)。 下面这段代码是模型层依赖反转和输出解耦的核心骨架,注意接口指针取代了具体类: #include "..\Controller\Input.mqh" class CModel { public: ... void SetPeriod(int value) {iWprPeriod = value;} private: int iWprPeriod; }; #include "..\Controller\Input.mqh" class CModel { public: int Initialize(CInputParam* pI){ pInput = pI; return INIT_SUCCEEDED; } private: CInputParam* pInput; }; pInput.GetPeriod(); #include "..\Controller\Input.mqh" interface IInputBase { const int GetPeriod() const; }; class CModel { public: int Initialize(IInputBase* pI){ pInput = pI; return INIT_SUCCEEDED; } private: IInputBase* pInput; }; class CInputParam: public IInputBase interface IOutputBase { void SetValue(int shift, double value); const double GetValue(int shift) const; }; int Initialize(IInputBase* pI, IOutputBase* pO){ pInput = pI; pOutput = pO; ... } private: IInputBase* pInput; IOutputBase* pOutput; pOutput.SetValue(...); int CModel::Tick(const int rates_total,const int prev_calculated,const datetime &time[],const double &open[],const double &high[],const double &low[],const double &close[],const long &tick_volume[],const long &volume[],const int &spread[]) { if(rates_total < iLength) return(0); int i, pOutputs = prev_calculated - 1; if(pOutputs < iLength - 1) { pOutputs = iLength - 1; for(i = 0; i < pOutputs; i++) 把原 OnCalculate 里的 WRP 计算搬进 Tick(...) 后,模型几乎就是原指标处理程序的翻版,但所有缓冲区访问都走 pOutput。开 MT5 建这几个 .mqh 验证:当 iLength 设 14、rates_total 不足 14 根 K 线时 Tick 直接返 0,不会写越界。外汇与贵金属杠杆高,这类底层架构错误可能让 EA 在实盘静默失效,先在策略测试器跑通再上图。
class="macro">#include <span class="class="type">class="kw">string">"..\Controller\Input.mqh"</span> <span class="keyword">class</span> CModel { <span class="keyword">class="kw">public</span>: ... <span class="keyword">class="type">void</span> SetPeriod(<span class="keyword">class="type">int</span> <span class="keyword">value</span>) {iWprPeriod = <span class="keyword">value</span>;} ... <span class="keyword">class="kw">private</span>: <span class="keyword">class="type">int</span> iWprPeriod; ... }; <span class="preprocessor">class="macro">#include </span><span class="class="type">class="kw">string">"..\Controller\Input.mqh"</span> <span class="keyword">class</span> CModel { <span class="keyword">class="kw">public</span>: <span class="keyword">class="type">int</span> Initialize(CInputParam* pI){ pInput = pI; <span class="keyword">class="kw">return</span> <span class="macro">INIT_SUCCEEDED</span>; } <span class="keyword">class="kw">private</span>: CInputParam* pInput; }; pInput.GetPeriod(); <span class="preprocessor">class="macro">#include </span><span class="class="type">class="kw">string">"..\Controller\Input.mqh"</span> <span class="keyword">interface</span> IInputBase { <span class="keyword">const</span> <span class="keyword">class="type">int</span> GetPeriod() <span class="keyword">const</span>; }; <span class="keyword">class</span> CModel { <span class="keyword">class="kw">public</span>: ... <span class="keyword">class="type">int</span> Initialize(IInputBase* pI){ pInput = pI; <span class="keyword">class="kw">return</span> <span class="macro">INIT_SUCCEEDED</span>; } ... <span class="keyword">class="kw">private</span>: IInputBase* pInput; }; <span class="keyword">class</span> CInputParam: <span class="keyword">class="kw">public</span> IInputBase <span class="keyword">interface</span> IOutputBase { <span class="keyword">class="type">void</span> SetValue(<span class="keyword">class="type">int</span> shift, <span class="keyword">class="type">class="kw">double</span> <span class="keyword">value</span>); <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> GetValue(<span class="keyword">class="type">int</span> shift) <span class="keyword">const</span>; }; <span class="keyword">class="type">int</span> Initialize(IInputBase* pI, IOutputBase* pO){ pInput = pI; pOutput = pO; ... } ... <span class="keyword">class="kw">private</span>: IInputBase* pInput; IOutputBase* pOutput; pOutput.SetValue(...); <span class="keyword">class="type">int</span> CModel::Tick(<span class="keyword">const</span> <span class="keyword">class="type">int</span> rates_total,<span class="keyword">const</span> <span class="keyword">class="type">int</span> prev_calculated,<span class="keyword">const</span> <span class="keyword">class="type">class="kw">datetime</span> &time[],<span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &open[],<span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &high[],<span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &low[],<span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &close[],<span class="keyword">const</span> <span class="keyword">class="type">long</span> &tick_volume[],<span class="keyword">const</span> <span class="keyword">class="type">long</span> &volume[],<span class="keyword">const</span> <span class="keyword">class="type">int</span> &spread[]) { <span class="keyword">if</span>(rates_total < iLength) <span class="keyword">class="kw">return</span>(<span class="number">class="num">0</span>); <span class="keyword">class="type">int</span> i, pOutputs = prev_calculated - <span class="number">class="num">1</span>; <span class="keyword">if</span>(pOutputs < iLength - <span class="number">class="num">1</span>) { pOutputs = iLength - <span class="number">class="num">1</span>; <span class="keyword">for</span>(i = <span class="number">class="num">0</span>; i < pOutputs; i++)
WPR 在自定义缓冲区的落地写法
这段逻辑把威廉指标(WPR)写进了一个自定义输出缓冲区 pOutput,而不是默认指标线。循环从 pOutputs 开始遍历到 rates_total,并用 IsStopped() 做中断保护,避免在 MT5 策略测试器里强退时卡死。 核心计算取当前柱往前 iLength 根 K 线的最高价与最低价:max_high = Highest(high, iLength, i),min_low = Lowest(low, iLength, i)。当两者不等时,w = -(max_high - close[i]) * 100 / (max_high - min_low),结果落在 -100 到 0 之间,这是 WPR 的标准值域。 若区间高低相等(极端锁价或数据缺口),代码不填 0 也不报错,而是沿用前一柱的值 pOutput.GetValue(i-1),保证序列连续。外汇与贵金属品种在重大数据秒级跳空时易出现此类相等,属高风险场景,回测时建议打印该分支触发次数。 黄色高亮的三处 pOutput.SetValue 是真正落盘动作:初始化填 0、正常填 w、异常填前值。直接把这段粘进你的自定义指标 OnCalculate 末尾,改 iLength 就能在 EURUSD 的 M15 上验证缓冲线形状。
pOutput.SetValue(i, class="num">0); } class="type">class="kw">double w; for(i = pOutputs; i < rates_total && !IsStopped(); i++) { class="type">class="kw">double max_high = Highest(high, iLength,i); class="type">class="kw">double min_low = Lowest(low, iLength, i); class=class="str">"cmt">//--- calculate WPR if(max_high != min_low) { w = -(max_high - close[i]) * class="num">100 / (max_high - min_low); pOutput.SetValue(i, w); } else pOutput.SetValue(i, pOutput.GetValue(i - class="num">1) ); } class="kw">return(rates_total); }
「把显示层从主文件里剥出来」
视图是这套指标架构里最后一个拼图,专门负责把模型算出的数据画出来。它和源数据模块一样属于高频改动区——加缓冲区、改默认颜色、调线型都在这里动刀。一个现实约束是:视图的改动往往倒逼源数据模块跟着改,反之亦然,所以把视图和源数据从模型里拆出来,不是洁癖,是少改几处就能见效。 在 View 文件夹建 CView 类,挂上 IOutputBase 协定。模型与视图里目前都没写 Update(...) 和 Release(...),因为这个 WPR 变体还用不着生命周期管理,别提前堆抽象。 指标缓冲区做成私有数组字段,所有 IndicatorSetXXX、PlotIndexSetXXX 调用全塞进 Initialize(...)。主文件里的宏因此能删掉一大半,可读性直接回来。附件里原版 WPR 和这个定制版读数完全一致,但后者的显示逻辑已经独立成类。 下面这段是 CView 的核心:缓冲区下限锁 -100、上限 0,画两条灰点参考线在 -20 和 -80;主线用红色实线,标签后缀 _View。外汇与贵金属波动剧烈,这类动量线仅作概率参考,实盘须自担高风险。
class CView : class="kw">public IOutputBase { class="kw">private: const CInputParam* pInput; class="type">class="kw">double WPRlineBuffer[]; class="kw">public: CView(){} ~CView(){} class="type">int Initialize(const CInputParam* pI); class="type">void SetValue(class="type">int shift, class="type">class="kw">double value); const class="type">class="kw">double GetValue(class="type">int shift) const {class="kw">return WPRlineBuffer[shift];} }; class="type">int CView::Initialize(const CInputParam *pI) { pInput = pI; IndicatorSetString(INDICATOR_SHORTNAME, NAME ); IndicatorSetInteger(INDICATOR_DIGITS, class="num">2 ); IndicatorSetDouble(INDICATOR_MINIMUM,-class="num">100 ); IndicatorSetDouble(INDICATOR_MAXIMUM, class="num">0 ); IndicatorSetInteger(INDICATOR_LEVELCOLOR,clrGray ); IndicatorSetInteger(INDICATOR_LEVELWIDTH,class="num">1 ); IndicatorSetInteger(INDICATOR_LEVELSTYLE,STYLE_DOT); IndicatorSetInteger(INDICATOR_LEVELS, class="num">2 ); IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">0, -class="num">20 ); IndicatorSetDouble(INDICATOR_LEVELVALUE,class="num">1, -class="num">80 ); SetIndexBuffer(class="num">0, WPRlineBuffer); PlotIndexSetInteger(class="num">0, PLOT_DRAW_TYPE, DRAW_LINE ); PlotIndexSetInteger(class="num">0, PLOT_LINE_STYLE, STYLE_SOLID); PlotIndexSetInteger(class="num">0, PLOT_LINE_WIDTH, class="num">1 ); PlotIndexSetInteger(class="num">0, PLOT_LINE_COLOR, clrRed ); PlotIndexSetString(class="num">0, PLOT_LABEL, NAME + "_View" ); class="kw">return INIT_SUCCEEDED; } class="type">void CView::SetValue(class="type">int shift,class="type">class="kw">double value) { WPRlineBuffer[shift] = value; }