MVC 设计范式及其应用(第 2 部分):三个组件之间相互作用示意图(基础篇)
📘

MVC 设计范式及其应用(第 2 部分):三个组件之间相互作用示意图(基础篇)

第 1/3 篇

「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 把这段拆出去,你的指标初始化会明显更耐改。

MQL5 / C++
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 的打印。

MQL5 / C++
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() 就能同步最新输入。外汇与贵金属杠杆高、滑点跳空频繁,这类自动同步若漏掉一次,策略可能按旧参数下单。

MQL5 / C++
  dDistance = dist * _Point;

  class="kw">return iResult;
}
class CInputManager: class="kw">public CInputParam {
  class="kw">public:
    ...
    class="type">bool  Update() {class="kw">return GetData(); }
    ...
};

常见问题

Model 负责所有计算和输入参数校验,View 只管把算好的数值画到图表上,两者通过 Controller 传数据,别把绘图逻辑塞进 Model。
不会。封装后统一在类里做兜底和校验,外部只调接口;改参数仍走属性面板,但能避免脏值导致指标报错。
可以。小布能读取你的指标代码,标出计算、绘图、参数三块边界,并给出拆分到三个类的参考写法,你照着挪就行。
在 Model 统一换算能保证全指标用同一标准,避免 View 多处重复算错;回传点数后 View 直接按点偏移画线更稳。
没有万能默认值。按品种波动率给合理区间,校验时越界就 clamp 到边界并写日志,别静默用错值跑图。