MVC 设计范式及其可能的应用(基础篇)
「用 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 不只是指标的专利,下一节我们看智能交易系统怎么套用同一思路。
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 数据。
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 缓冲刷新是否跟手,再决定实盘参数。
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); }