用 UML 给 EA 搭骨架
◍ 用 UML 给 EA 搭骨架
写 EA 前先画 UML,不是学院派癖好,而是把交易逻辑从脑子里搬进 MT5 的最省错路径。爱因斯坦那句「科学家探索已有的,工程师创造没有的」放在 MQL5 开发里很贴切:指标和行情是既存的,EA 是你要从无到有造出来的东西。
- 年 3 月那篇原帖在论坛拿到 5091 次浏览、15 条讨论,说明即便老教程,UML 建模对 EA 结构设计的参考价值仍被实盘开发者反复翻出来用。
类图先把 CExpert、CIndicators 这些标准库依赖关系理清楚,状态图管住「挂单—持仓—平仓」的跳转条件,比直接上手写 OnTick 更容易避开重码和逻辑死循环。外汇与贵金属杠杆高、滑点随机,EA 任何状态遗漏都可能放大成实亏,建模阶段多花一小时,回测阶段少烧几天。
为什么用 UML 给 MQL5 交易系统建模
前面有文章建议用动态系统思路给 EA 建模,但那只覆盖了系统的动态行为,没把面向对象的设计结构说清楚。专业开发者手里有更通用的表达工具,UML 就是其中一种能补全这块认知的图形语言。 MQL5 本身就属于面向对象语言家族,类、继承、封装这些概念和 UML 的语义是对得上的,用图形先画清楚再写代码,反而比直接堆逻辑更容易避坑。 建模工具我选了免费非商业的 Software Ideas Modeler,不花钱就能画用例图、类图、状态机,读者可以现在装一个,把下一版 EA 的类关系先拖出来看看。
「用 UML 给复杂 EA 搭骨架」
写 EA 最怕逻辑长胖后自己都看不懂。UML 的价值不在花哨,而是把模块、绑定和图表三类构造块画出来,让大而不乱的系统在图上一目了然。人类读图带字比纯文字快,这是 UML 设计者反复强调的感知事实。 UML 结构由实体(模型元素)、关系(绑定)和图表(模型表示)组成,公共机制里规范写语义、修饰标重点、通用划分分抽象与实例、扩展机制用约束/构造型/标记值兜底。 架构层最实用的是「4+1 视图」:逻辑、进程、开发、物理加场景。落到 EA 开发,UML 2.2 版提供 14 类图,足以从抽象一路切到具体交易模块。复杂策略先画视图再写代码,MT5 里改一个模块不会牵一发而动全身。外汇与贵金属杠杆高,模型清晰也只是降低开发错误率,不承诺实盘收益。
◍ 用 UML 给 EA 先画骨架再写代码
做 MT5 自动化交易,不少人一上来就写 EA,结果逻辑改一次乱一茬。先把系统用 UML 图钉死结构,再让编辑器生成类壳,能省掉大量返工。用例图负责从用户视角圈定边界:开发人员建系统、风控员管风险、管理员监控,末端用户(交易者/投资者)通过泛化关系归入“User”,建仓平仓又泛化自“Trading”基础用例,盈利用例只在卖价高于买价条件下以 extend 关系挂上去。 类图直接对应 OOP 静态结构。以 CTradeExpert 为例,属性 Magic_No 标“-”为 private、e_pnt 标“+”为 public,其余 protected;冒号后跟类型,如 int、CTrade。把它通过 dependency 虚线连到 CTrade / CAccountInfo 等标准库类,再泛化自 CObject,在 MetaEditor 侧栏点 Generate 选 C++ 导出,稍改就能转 MQL5。 活动图补足运行时行为。Test_TradeExpert.mq5 从初始节点建实例、初始化,决策节点分流;可中断区处理卸载,迭代区按多空两向循环跑“检查信号→建仓→检查仓位→平仓”。序列图则把时间轴竖起来:客户端发 Init 事件,1.1 虚线创建 CTradeExpert() 实例,1.2 控制权返回,2.1 调 OnInit() 再回传,消息编号 #.# 为同步、# 为异步。 外汇与贵金属杠杆高,模型画得再清楚也只是降低编程风险,不预示任何收益。开 MT5 建个空 EA,照类图补 TradeExpert.mqh,比从头硬写更容易控住嵌套层级。
从序列图到 EA 骨架的代码落地
Init 返回非零时,EA 会先走 Deinit 再交还控制权,标准模板里其实没写这块失败处理,用 UML 的 opt 块(警备条件 OnInit()!=0)补上逻辑才完整。NewTick 进来后由 OnTick 接管,里面是一个无限循环,直到手动禁用 EA 才停。 循环体里用 4 个自返消息跑 CheckSignal、OpenPosition、CheckPosition、ClosePosition,所谓自返就是 EA 对象给自己发消息。外层 loop(2) 表示两次遍历,分别对应多空两个方向——从步骤 7 到 10 就是把长仓和短仓各跑一遍。 下面这段是照着图转出来的类骨架与调用结构。CTradeExpert 先聚合了交易、账户、成交、品种四类对象,Magic_No 私有,e_pnt 公有暴露点数价值;OnTick 里用 for(dir=0;dir<2;dir++) 把四个函数按方向各调一次。
class=class="str">"cmt">//includes class="macro">#include <Trade\Trade.mqh> class="macro">#include <Trade\AccountInfo.mqh> class="macro">#include <Trade\DealInfo.mqh> class="macro">#include <Trade\SymbolInfo.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class CTradeExpert { class="kw">private: class="type">int Magic_No; class=class="str">"cmt">// Expert Advisor magic class="kw">protected: CTrade e_trade; class=class="str">"cmt">// An object for executing trade orders CAccountInfo e_account; class=class="str">"cmt">// An object for receiving account properties CDealInfo e_deal; class=class="str">"cmt">// An object for receiving deal properties CSymbolInfo e_symbol; class=class="str">"cmt">// An object for receiving symbol properties class="kw">public: class="type">class="kw">double e_pnt; class=class="str">"cmt">// Value in points class="kw">public: CTradeExpert() { } }; class=class="str">"cmt">//+------------------------------------------------------------------+ for(class="type">long dir=class="num">0;dir<class="num">2;dir++) { myTE.CheckSignal(dir); myTE.OpenPosition(dir); myTE.CheckPosition(dir); myTE.ClosePosition(dir); }
class=class="str">"cmt">//includes class="macro">#include <Trade\Trade.mqh> class="macro">#include <Trade\AccountInfo.mqh> class="macro">#include <Trade\DealInfo.mqh> class="macro">#include <Trade\SymbolInfo.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class CTradeExpert { class="kw">private: class="type">int Magic_No; class=class="str">"cmt">// Expert Advisor magic class="kw">protected: CTrade e_trade; class=class="str">"cmt">// An object for executing trade orders CAccountInfo e_account; class=class="str">"cmt">// An object for receiving account properties CDealInfo e_deal; class=class="str">"cmt">// An object for receiving deal properties CSymbolInfo e_symbol; class=class="str">"cmt">// An object for receiving symbol properties class="kw">public: class="type">class="kw">double e_pnt; class=class="str">"cmt">// Value in points class="kw">public: CTradeExpert() { } }; class=class="str">"cmt">//+------------------------------------------------------------------+ for(class="type">long dir=class="num">0;dir<class="num">2;dir++) { myTE.CheckSignal(dir); myTE.OpenPosition(dir); myTE.CheckPosition(dir); myTE.ClosePosition(dir); }
「用 UML 拆开标准库里的 Expert 类族」
想真正搞懂 MT5 标准库的交易骨架,光读 Expert.mqh 不够,得把它反向画成类图。CExpert 往上追有两层基类:直接继承 CExpertBase,再往上追到 CObject。手动逆向虽然慢,但比直接丢给 IBM Rational Rose 更能记住依赖链。 CExpert 本身不是孤岛。它内部挂了 CExpertTrade、CExpertSignal、CExpertMoney、CExpertTrailing、CIndicators、CPositionInfo、COrderInfo 等实例,还引用了 ENUM_TRADE_EVENTS、ENUM_TIMEFRAMES 和 MqlDateTime 结构。把这些块用「use」构造型连起来,图 13 那版就已经能看清主依赖了。 图太挤怎么办?用包封装。CTrade 单独封一个包,通过橙色接口向外暴露关系,点接口就能跳回原类图(图 15、16)。CExpertBase 更夸张:它用到 CiOpen、CiHigh、CiLow、CiSpread 等八个序列类,前四个派生于 CPriceSeries,后四个派生于 CSeries,而 CPriceSeries 又是 CSeries 的子类。把这些塞进包里再调样式,图 18 才勉强能读。 到自己写 EA 时,建一个 CmyExpert 继承 CExpert,再派生 CmyExpertSignal / CmyExpertMoney / CmyExpertTrailing 覆盖虚函数(图 25)。这一步才是你把策略塞进标准库模板的落点。 序列图管运行流。全局创建 CmyExpert 实例 myTrader 后,OnInit、Deinit 各自拆成独立序列图,用 ref 边框跳转(图 26、27、28)。EA 主体一大,就靠这种块级拆分保住可读性。打开 MT5 看 Expert.mqh 第 1 行起的继承声明,对照上面包图一条条核对,比盲抄代码强。
◍ 把 UML 草图直接丢进 MT5 工程
用 UML 给 EA 开发画可视草图,核心价值就一条:不读代码也能记住类结构和调用关系,后续维护、重构都快一截。作者随文给了四个落地文件,路径分别是 %MetaTrader%\MQL5\Include 下的 TradeExpert.mqh(EA 父类)、%MetaTrader%\MQL5\Experts 下的 Test_TradeExpert.mq5(可直接编译跑的样例)、%Documents%\UML projects 里的 Expert.simp(UML 图源)、以及 %Program Files%\SoftwareIdeasModeler 里的建模工具分发包。 这套方法不是银弹。UML 本身有冗余和语义不精确的毛病,复杂 EA 画多了图比代码还难读,新手容易在画图阶段过度设计。外汇与贵金属杠杆高、滑点随机,任何可视化设计都不替代实盘风险,回测通过也不代表实盘概率占优。 想验证就现在开 MT5:把 TradeExpert.mqh 塞进 Include,Test_TradeExpert.mq5 丢进 Experts 编译,对照 Expert.simp 看类图怎么映射成头文件里的继承。能跑通一次,比看十篇 UML 教程都实在。