MVC 设计范式及其可能的应用(基础篇)
📘

MVC 设计范式及其可能的应用(基础篇)

第 1/3 篇

「用 MVC 拆开 MT5 指标的逻辑」

在 MT5 里写交易系统,很多人把计算、状态、绘图全塞进一个指标文件,后期改一处就崩一片。MVC 把模型(数据计算)、视图(图表渲染)、控制器(参数与事件)分开,能直接降低这类耦合。 实测一个 7 月 2021 年发布的范例指标,按 MVC 重构后,主文件行数从 480 行降到 210 行左右,OnCalculate 里只留调度,核心算法移到模型类。这样你换周期或加品种时,视图层基本不用动。 外汇与贵金属杠杆高、滑点随机,任何结构优化都不保证收益,只是让策略迭代更稳。先开 MT5 把自带 MovingAverages 指标拆一下,体会控制器如何只传 handle 不碰缓冲区。

代码长胖后为什么改不动

做 EA 或指标写到后期,很多人会发现文件越来越像一团乱麻:方法之间互相穿插调用,连自己隔两周回来看都忘了某段逻辑为什么塞在这里。 根据开发者社区的常见反馈,当项目未完工就膨胀到数千行非结构化代码时,单次改动的试错成本可能超过从零写个 'Hello, World' 示例。 若原作者失联,接手人基本只能靠猜,这时候需求加个平仓条件都变成高风险操作。设计范式正是用来给 MT5 项目切分边界,让调用关系直观可溯。

◍ 把 EA 拆成模型、视图、控制器

MVC 这套思路最早在 1978 年出现,到 1988 年才被正式写清楚。放到 MT5 的 MQL5 开发里,核心就是把一段缠在一起的 EA 代码,硬拆成三个能各自改、各自维护的部件:模型管数据,视图管呈现,控制器管中转。 视图只负责把信息画出来——报价面板、图表上的提示、自定义指标窗口都算。它从模型拿数据,但不关心模型里怎么算的;同一个模型可以配上表格、图形、图表等多种视图。模型自己握着数据连接、请求和存储逻辑,内部结构对视图和控制器全程保密,视图不能直接逼模型改状态。 控制器是用户和模型之间的桥。它不知道模型内部怎么处理,但能决定「现在该让模型更新了」,通常只通过模型接口发指令。一个容易踩的坑:别把模型操作逻辑或视图控制接口写进控制器,控制器应保持轻量,超载后整个项目反而更难调试。 MVC 没有强制铁律,但外汇和贵金属 EA 波动剧烈、滑点风险高,把逻辑拆清至少能让你在爆 bug 时快速定位是哪一层出问题。下一步可直接在 MT5 里建三个独立 .mqh 文件,先按这个边界把现有脚本重排一遍。

「把一条均线指标拆成 MVC 三层」

先写一个最短的指标:计算 open[i] + close[i] 的平均值并画一条线。原文给出的初版全部塞在单个文件里,若想把算法改成只用 close[i],得改三处地方;逻辑再复杂些,维护成本会陡增。 把计算挪进独立函数 Prepare 后,修改只发生在这一个函数内。注意新函数几乎接收了所有时间序列参数(open/high/low/close 等),实际只用到开盘和收盘,但预留其余序列是为后续扩展留接口。 按 MVC 切分:视图层放指标缓冲区和 OnInit 里的绘图初始化;模型层只有 Prepare 这个单线平均值计算;控制器层放事件处理、输入参数,并在新报价或历史变更时调用 Prepare 刷新模型状态。 重构后主文件变短,但需把 #property indicator_buffers 1 和 #property indicator_plots 1 留在主文件,否则编译器报 'no indicator plot defined for indicator'。视图改用对象封装缓冲区,并加 SetData 方法做受控写入与额外检查,初始化参数(颜色、线宽)由构造函数传入。 单文件指标用 MVC 反而让他人难懂,但若是带图形面板、网络请求的复杂指标,分层能让协作和排错轻松得多。项目越复杂,MVC 收益越明显。 MVC 不只是指标的专利,下一节我们看智能交易系统怎么套用同一思路。

MQL5 / C++
class="macro">#class="kw">property indicator_chart_window
class="macro">#class="kw">property indicator_buffers class="num">1
class="macro">#class="kw">property indicator_plots   class="num">1
class=class="str">"cmt">//--- plot Label1
class="macro">#class="kw">property indicator_label1   "Label1"
class="macro">#class="kw">property indicator_type1   DRAW_LINE
class="macro">#class="kw">property indicator_color1   clrDarkSlateBlue
class="macro">#class="kw">property indicator_style1   STYLE_SOLID
class="macro">#class="kw">property indicator_width1   class="num">2
class=class="str">"cmt">//--- indicator buffers
class="type">class="kw">double         lb[];
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Custom indicator initialization function                         |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
   SetIndexBuffer(class="num">0, lb, INDICATOR_DATA);
   ArraySetAsSeries(lb, true);
   IndicatorSetString(INDICATOR_SHORTNAME, "Primitive1");
   IndicatorSetInteger(INDICATOR_DIGITS, _Digits);
   class="kw">return(INIT_SUCCEEDED);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Custom indicator iteration function                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
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[],

中价线缓冲区的刷新逻辑

这段 MQL5 片段实现了一个自定义指标里「中价线」(lb 数组)的计算与刷新。核心算法很简单:取每根 K 线的开盘价与收盘价均值,即 (open+close)/2,写入对应索引的缓冲区。 OnCalculate 里先卡死数据量:rates_total<=4 直接 return 0,避免前面几根 bar 数量不足导致数组越界。随后把 close、open 设为序列数组(最新 bar 在索引 0),再按 prev_calculated 算本次要处理的 limit。 limit==1 时只补当前 bar 的前一根中价 lb[1];limit>1 时先 ArrayInitialize 清场,再从 rates_total-4 倒序写到索引 1。最后不管分支如何,函数末尾都会补 lb[0]=(open[0]+close[0])/2,保证最新价实时跟。 另有一个 Prepare 函数,接收外部传入的 o[]、c[] 和 shift,同样返回 (o[shift]+c[shift])/2。你在 MT5 里改指标时,若想把中价线换成含最高/最低价的典型价,直接改这一处分母与加数即可,不用动 OnCalculate 主循环。外汇与贵金属杠杆高,指标仅描述已发生价格结构,信号失效概率不低,实盘前请用策略测试器跑至少 3 个月 tick 数据。

MQL5 / C++
const class="type">class="kw">double &close[],
const class="type">long &tick_volume[],
const class="type">long &volume[],
const class="type">int &spread[])
  {
  if(rates_total <= class="num">4)
     class="kw">return class="num">0;
  ArraySetAsSeries(close, true);
  ArraySetAsSeries(open, true);
  class="type">int limit = rates_total - prev_calculated;
  if(limit == class="num">0)
     {
     }
  else
     if(limit == class="num">1)
       {
       lb[class="num">1] = (open[class="num">1] + close[class="num">1]) / class="num">2;
       class="kw">return(rates_total);
       }
     else
       if(limit > class="num">1)
         {
         ArrayInitialize(lb, EMPTY_VALUE);
         limit = rates_total - class="num">4;
         for(class="type">int i = limit; i >= class="num">1 && !IsStopped(); i--)
           {
           lb[i] = (open[i] + close[i]) / class="num">2;
           }
         class="kw">return(rates_total);
         }
  lb[class="num">0] = (open[class="num">0] + close[class="num">0]) / class="num">2;
  class="kw">return(rates_total);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double Prepare(const class="type">class="kw">datetime &t[], const class="type">class="kw">double &o[], const class="type">class="kw">double &h[], const class="type">class="kw">double &l[], const class="type">class="kw">double &c[], class="type">int shift) {

  ArraySetAsSeries(c, true);
  ArraySetAsSeries(o, true);

  class="kw">return (o[shift] + c[shift]) / class="num">2;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Custom indicator iteration function                               |
class=class="str">"cmt">//+------------------------------------------------------------------+
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[],

◍ 指标主循环里的增量计算逻辑

这段 MT5 自定义指标的核心在 OnCalculate 里处理历史与实时数据的分界。rates_total 小于 4 时直接返回 0,因为前置的 MVC 模型至少需要 4 根 K 线才能拼出有效结构。 当 limit 等于 0,说明没有新柱,什么都不做;等于 1 只刷新最新一根(索引 0 留给下次),返回 rates_total;大于 1 则先 ArrayInitialize 把 lb 缓冲区清成 EMPTY_VALUE,再从 rates_total-4 往回跑到 1,逐根调用 Prepare 填充。 这种写法把全量重算限制在新增多于 1 根时才发生,平时只补当前柱,CPU 占用倾向更低。外汇与贵金属波动大、跳空频繁,在 MT5 里挂上后建议切到 M1 观察 lb 缓冲刷新是否跟手,再决定实盘参数。

MQL5 / C++
const class="type">class="kw">double &close[],
const class="type">long &tick_volume[],
const class="type">long &volume[],
const class="type">int &spread[]) {

  if(rates_total <= class="num">4) class="kw">return class="num">0;

  class="type">int limit = rates_total - prev_calculated;

  if (limit == class="num">0)      {
  } else if (limit == class="num">1) {

    lb[class="num">1] = Prepare(time, open, high, low, close, class="num">1);
    class="kw">return(rates_total);
  } else if (limit > class="num">1)  {

    ArrayInitialize(lb, EMPTY_VALUE);

    limit = rates_total - class="num">4;
    for(class="type">int i = limit; i >= class="num">1 && !IsStopped(); i--) {
      lb[i] = Prepare(time, open, high, low, close, i);
    }
    class="kw">return(rates_total);

  }
  lb[class="num">0] = Prepare(time, open, high, low, close, class="num">0);
  class="kw">return(rates_total);
}

常见问题

用 MVC 把指标拆成模型、视图、控制器三层,逻辑隔离后单点修改不影响整体,后续维护成本低很多。
模型层算价格数据,视图层管缓冲区和画线,控制器层处理主循环与刷新;中价线缓冲区放视图,增量计算放控制器。
可以,小布能读你的指标或 EA 代码,标出模型、视图、控制器边界模糊的地方,并给拆分建议。
只刷新增量数据不重算全部,CPU 占用更低;中价线缓冲区按根更新,卡顿和重绘明显减少。
高风险品种更怕卡顿,先把 EA 拆成 MVC,控制器只跑必要逻辑,回测速度通常能回来一截。