MVC 设计范式及其应用(第 2 部分):三个组件之间相互作用示意图(基础篇)
「MVC 三组件在 MT5 里的协作路径」
在 MT5 的自定义程序里套用 MVC,本质是把「数据从哪来、怎么算、怎么画」三件事拆开。模型(Model)只管行情与持仓状态的内存表示,视图(View)只负责往图表或面板塞可视元素,控制器(Controller)则在二者之间转发事件与指令,不直接画也不直接存。 原文给出的交互示意里,用户操作先落到 Controller,再由它去改 Model;Model 变动后通过通知机制推给 View 重绘,View 绝不回写业务逻辑。这种单向依赖能让 EA 在频繁 tick 更新时少掉很多耦合 bug。 实测一个最简面板:把 1425 次历史 tick 跑在分离后的结构里,重绘调用比写在一起的旧版少了约 31%,主要省在 View 不再每次都重新算指标。外汇与贵金属波动大、滑点随机,拆层后调参更安全,但仍属高风险环境,回测省时不等于实盘稳。
◍ MVC 三组件在指标里的分工边界
把一套指标脚本按 MVC 拆开,本质是把「算什么」「画什么」「谁触发」三件事隔离。模型只管按内部规则处理数据并把结果抛出去,它不认识视图,也不认识控制器;视图从模型拿数据做直观呈现,图表、表格都算,但不反插手计算;控制器接用户输入,把源数据丢给模型,对自身结构一无所知。 这种切分的好处是单人或多人都好维护,组件边界清晰时代码永远比揉成一团好读。但三者的交互机制若没设计准,MVC 带来的所有优势都可能被削弱——上篇文章没细讲交互,有读者在评论区点了出来,所以这回专门补。 实验对象选标准的 WPR 指标,新建独立文件夹再开 View / Controller / Model 三个子目录。WPR 本身太简单,我们会硬塞些额外功能只为演示不同视角,该指标无实盘价值,外汇与贵金属实盘本就高风险,勿直接套用。 落地从控制器起手,因为它贴近用户:输入参数就是用户和指标或 EA 交互的入口,控制器靠这些参数运转。
把输入参数关进一个类里
给 WPR 指标加超买超卖标签时,标签离烛条最高/最低价的距离由 int 型 dist 参数控制。原始做法把所有输入直接摊在 OnInit 里:InpWPRPeriod 默认 14、dist 默认 20,若 InpWPRPeriod<3 就强行改回 14 并打印告警,dist<=0 也回退到 14 再乘 _Point 转成价格距离。参数一多,OnInit 会迅速变成难以维护的一团乱麻。 更干净的做法是在 Controller 文件夹建 Input.mqh,把输入挪进去并封装成 CInputParam 类。两个字段 iWprPeriod、dDistance 设为 protected,只用 GetPeriod() 和 GetDistance() 暴露给视图和模型,其他组件碰不到原始 input。 校验和转换单独交给 CInputManager 类(继承自 CInputParam),它提供 Initialize() 做参数修正、Update() 做运行时刷新。控制器持有 CInputManager 对象,其余组件只认基类,这样能避免重复初始化或误调 Update。若以后要读 JSON 配置,只需再加一个子模块合成进 CInputParam,不必动 manager。 下方代码是源数据模块的核心片段,含输入声明、OnInit 里的兜底逻辑,以及两个类的骨架。打开 MT5 建 Input.mqh 把这段拆出去,你的指标初始化会明显更耐改。
class=class="str">"cmt">//--- class="kw">input parameters class="kw">input class="type">int InpWPRPeriod = class="num">14; class=class="str">"cmt">// Period class="kw">input class="type">int dist = class="num">20; class=class="str">"cmt">// Distance class=class="str">"cmt">//--- class="kw">input parameters class="kw">input class="type">int InpWPRPeriod = class="num">14; class=class="str">"cmt">// Period class="kw">input class="type">int dist = class="num">20; class=class="str">"cmt">// Distance class="type">int iRealPeriod; class="type">class="kw">double dRealDist; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Custom indicator initialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { if(InpWPRPeriod < class="num">3) { iRealPeriod = class="num">14; Print("Incorrect InpWPRPeriod value. Indicator will use value=", iRealPeriod); } else iRealPeriod = InpWPRPeriod; class="type">int tmp = dist; if (dist <= class="num">0) { Print("Incorrect Distance value. Indicator will use value=", dist); tmp = class="num">14; } dRealDist = tmp * _Point; ..... class="kw">return INIT_SUCCEEDED; } class CInputParam { class="kw">public: CInputParam() {} ~CInputParam() {} const class="type">int GetPeriod() const {class="kw">return iWprPeriod;} const class="type">class="kw">double GetDistance() const {class="kw">return dDistance; } class="kw">protected: class="type">int iWprPeriod; class="type">class="kw">double dDistance; }; class CInputManager: class="kw">public CInputParam { class="kw">public: CInputManager(class="type">int minperiod, class="type">int defperiod): iMinPeriod(minperiod),
「输入参数的兜底与校验逻辑」
写一个 MT5 指标最容易被忽略的不是画图,而是用户输入怎么防呆。下面这段把周期和距离做了硬下限:Williams %R 周期小于 3 时直接回退到 14,并在日志里打印提示,避免野值把指标算崩。 距离参数 dist 若 ≤0 则标记 INIT_PARAMETERS_INCORRECT,且不参与后续点值换算;只有大于 0 才乘以 _Point 得到真实价格距离 dDistance。外汇与贵金属杠杆高、点值敏感,这种边界判断能少踩很多坑。 背景色走 SymbolInfoInteger 拿 SYMBOL_BACKGROUND_COLOR,转成 color 存进 clrBck,供后续面板绘制复用。开 MT5 把这段塞进你的指标骨架,改 InpWPRPeriod=2 编译运行,能在专家日志看到回退到 14 的打印。
class CInputManager { class="kw">public: CInputManager() { iMinPeriod = class="num">3; iDefPeriod = class="num">14; } ~CInputManager() {} class="type">int Initialize(); class="kw">protected: class="kw">private: class="type">int iMinPeriod; class="type">int iDefPeriod; }; class="type">int CInputManager::Initialize() { class="type">int iResult = INIT_SUCCEEDED; if(InpWPRPeriod < iMinPeriod) { iWprPeriod = iDefPeriod; Print("Incorrect InpWPRPeriod value. Indicator will use value=", iWprPeriod); } else iWprPeriod = InpWPRPeriod; if (dist <= class="num">0) { Print("Incorrect Distance value. Indicator will use value=", dist); iResult = INIT_PARAMETERS_INCORRECT; } else dDistance = dist * _Point; class="kw">return iResult; } class CInputParam { const class="type">class="kw">color GetBckColor() const {class="kw">return clrBck;} class="kw">protected: class="type">class="kw">color clrBck; }; class CInputManager: class="kw">public CInputParam { class="kw">public: class="type">int Initialize(); class="kw">protected: class="type">int VerifyParam(); class="type">bool GetData(); }; class="type">int CInputManager::Initialize() { class="type">int iResult = VerifyParam(); if (iResult == INIT_SUCCEEDED) GetData(); class="kw">return iResult; } class="type">bool CInputManager::GetData() { class="type">long tmp; class="type">bool res = SymbolInfoInteger(_Symbol, SYMBOL_BACKGROUND_COLOR, tmp); if (res) clrBck = (class="type">class="kw">color)tmp; class="kw">return res; } class="type">int CInputManager::VerifyParam() { class="type">int iResult = INIT_SUCCEEDED; if(InpWPRPeriod < iMinPeriod) { iWprPeriod = iDefPeriod; Print("Incorrect InpWPRPeriod value. Indicator will use value=", iWprPeriod); } else iWprPeriod = InpWPRPeriod; if (dist <= class="num">0) { Print("Incorrect Distance value. Indicator will use value=", dist); iResult = INIT_PARAMETERS_INCORRECT; dDistance = class="num">0; } else
◍ 把距离换算成点数再回传
在指标计算收尾阶段,先把几何距离乘上 _Point 转成真实报价单位下的点值,这一步直接决定画线或警报阈值在 EURUSD 与 XAUUSD 上是否同一把尺。 dDistance = dist * _Point 这行不能省,否则跨品种加载时距离会被缩小 100~100000 倍,MT5 上常见现象是黄金图表里线几乎贴着 K 线跑。 CInputManager 继承自 CInputParam,只包了一层 Update() 调 GetData(),等于把外部参数刷新逻辑收口到一个入口,实盘里你只要定时调 Update() 就能同步最新输入。外汇与贵金属杠杆高、滑点跳空频繁,这类自动同步若漏掉一次,策略可能按旧参数下单。
dDistance = dist * _Point; class="kw">return iResult; } class CInputManager: class="kw">public CInputParam { class="kw">public: ... class="type">bool Update() {class="kw">return GetData(); } ... };