MVC 设计范式及其可能的应用·综合运用
(3/3)· 前两层铺垫完概念与单组件写法,这篇用完整 EA 把三者真正接起来跑通
◍ 挂单簇的止损对称与台账回收
这段逻辑先把交易对象设为当前品种的成交填充方式,再用上一根 K 线的高低点外扩 delta 点距,分别挂出 BuyStop 与 SellStop。买挂单触发价取前一根高点加 delta*point,卖挂单取前一根低点减 delta*point,两者互为对方的止损位——也就是突破反向即砍,属于典型的通道外破挂单。 止损跨度按 (dEnterBuy - dEnterSell) * tAkeProfitKoeff 算,再均摊到两端做止盈。若 tAkeProfitKoeff 设为 1.5,则止盈距离是挂单间距的 1.5 倍,盈亏比倾向大于 1,但外汇与贵金属杠杆高,滑点扩大时实际触发价可能偏离。 ClosePositions 用倒序循环清掉所有持仓,CloseOrder 则按交易对名精确删挂单并 break,避免重复扫。OnTrade 里用 bTicket/sTicket 数组对账,某一侧挂单失效就删掉对侧挂单并减 iOrders,防止半边仓裸奔。 开 MT5 把这段塞进 EA 的 CView 类,先只跑一个符号、把 iCount 设 1,观察挂单间距与止损对称是否符合你的点值预期再扩品种。
trade.SetMarginMode(); trade.SetDeviationInPoints(iSlippage); class="type">class="kw">double dEnterBuy, dEnterSell; class="type">class="kw">double dTpBuy, dTpSell; class="type">class="kw">double dSlBuy, dSlSell; class="type">class="kw">double dSlSize; for (class="type">int i = class="num">0; i < init.iCount; i++) { dEnterBuy = NormalizeDouble(iHigh(init.names[i],class="num">0,class="num">1) + delta * init.points[i], _Digits); dEnterSell = NormalizeDouble(iLow(init.names[i],class="num">0,class="num">1) - delta * init.points[i], _Digits); dSlSell = dEnterBuy; dSlBuy = dEnterSell; dSlSize = (dEnterBuy - dEnterSell) * tAkeProfitKoeff; dTpBuy = NormalizeDouble(dEnterBuy + dSlSize, _Digits); dTpSell = NormalizeDouble(dEnterSell - dSlSize, _Digits); trade.SetTypeFillingBySymbol(init.names[i]); trade.BuyStop(volumes, dEnterBuy, init.names[i], dSlBuy, dTpBuy); trade.SellStop(volumes, dEnterSell, init.names[i], dSlSell, dTpSell); } } class="type">void ClosePositions() { CTrade trade; for (class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--) { trade.PositionClose(PositionGetTicket(i) ); } } class="type">void CloseOrder(class="type">class="kw">string pair) { CTrade trade; class="type">class="kw">ulong ticket; for (class="type">int i = OrdersTotal() - class="num">1; i >= class="num">0; i--) { ticket = OrderGetTicket(i); if (StringCompare(OrderGetString(ORDER_SYMBOL), pair) == class="num">0) { trade.OrderDelete(ticket); break; } } } class="macro">#include <Trade\Trade.mqh> class CView { class="kw">public: class="type">void CView() {} class="type">void Initialize(); class="type">void Enter(); class="type">void ClosePositions(); class="type">void CloseAllOrder(); class="type">void OnTrade(); class="kw">private: class="type">void InitTicketArray() { ArrayInitialize(bTicket, class="num">0); ArrayInitialize(sTicket, class="num">0); iOrders = class="num">0; } CTrade trade; class="type">int iOrders; class="type">class="kw">ulong bTicket[], sTicket[]; }; class="type">void CView::OnTrade() { if (OrdersTotal() == iOrders) class="kw">return; for (class="type">int i = class="num">0; i < init.iCount; i++) { if (bTicket[i] != class="num">0 && !OrderSelect(bTicket[i]) ) { bTicket[i] = class="num">0; iOrders--; if (sTicket[i] != class="num">0) { trade.OrderDelete(sTicket[i]); sTicket[i] = class="num">0; iOrders--; } class="kw">continue; }
「挂单与参数怎么落进代码里」
这套多品种日元交叉盘策略,核心是把上一根 K 线的高低点外扩一定点数后挂 BuyStop / SellStop,靠止损互换和倍数止盈来框定盈亏比。下面这段是实际挂在 MT5 上能跑的挂单与初始化逻辑,输入参数直接决定了触发位置和仓位。 先看外部输入:delta=200 表示在 iHigh(...,0,1) 之上 200 点挂买、iLow(...,0,1) 之下 200 点挂卖;tAkeProfitKoeff=2 意味着止盈距离是买卖挂单价差的 2 倍;volumes=0.03 是每单手数,六个品种同时挂双向单,一开就是 12 张挂单。外汇与贵金属杠杆高,0.03 手在 AUDJPY 这类高波动交叉盘上也可能在单日吐掉远超本金比例的点数,需自测承受度。 Enter() 里把 dSlBuy 设为 dEnterSell、dSlSell 设为 dEnterBuy,相当于多空互为止损;dSlSize 算出价差后乘 2 给到 TP。Initialize() 里 trade.SetDeviationInPoints(15) 把滑点容差锁在 15 点,ArrayResize 按 init.iCount 开好票号数组。 如果 OrderSelect 失败会把对应挂单票号清零并减计数,同时尝试删掉反向挂单,避免僵尸单堆着吃保证金。ClosePositions() 和 CloseAllOrder() 分别按持仓和挂单倒序清场,最后 InitTicketArray() 复位。
if (sTicket[i] != class="num">0 && !OrderSelect(sTicket[i]) ) { sTicket[i] = class="num">0; iOrders--; if (bTicket[i] != class="num">0) { trade.OrderDelete(bTicket[i]); bTicket[i] = class="num">0; iOrders--; } } } class="type">void CView::Initialize() { trade.SetExpertMagicNumber(Magic); trade.SetMarginMode(); trade.SetDeviationInPoints(iSlippage); ArrayResize(bTicket, init.iCount); ArrayResize(sTicket, init.iCount); InitTicketArray(); } class="type">void CView::Enter() { class="type">class="kw">double dEnterBuy, dEnterSell; class="type">class="kw">double dTpBuy, dTpSell; class="type">class="kw">double dSlBuy, dSlSell; class="type">class="kw">double dSlSize; for (class="type">int i = class="num">0; i < init.iCount; i++) { dEnterBuy = NormalizeDouble(iHigh(init.names[i],class="num">0,class="num">1) + delta * init.points[i], _Digits); dEnterSell = NormalizeDouble(iLow(init.names[i],class="num">0,class="num">1) - delta * init.points[i], _Digits); dSlSell = dEnterBuy; dSlBuy = dEnterSell; dSlSize = (dEnterBuy - dEnterSell) * tAkeProfitKoeff; dTpBuy = NormalizeDouble(dEnterBuy + dSlSize, _Digits); dTpSell = NormalizeDouble(dEnterSell - dSlSize, _Digits); trade.SetTypeFillingBySymbol(init.names[i]); trade.BuyStop(volumes, dEnterBuy, init.names[i], dSlBuy, dTpBuy); bTicket[i] = trade.ResultOrder(); trade.SellStop(volumes, dEnterSell, init.names[i], dSlSell, dTpSell); sTicket[i] = trade.ResultOrder(); iOrders +=class="num">2; } } class="type">void CView::ClosePositions() { for (class="type">int i = PositionsTotal() - class="num">1; i >= class="num">0; i--) { trade.PositionClose(PositionGetTicket(i) ); } InitTicketArray(); } class="type">void CView::CloseAllOrder() { for (class="type">int i = OrdersTotal() - class="num">1; i >= class="num">0; i--) { trade.OrderDelete(OrderGetTicket(i)); } } input class="type">class="kw">string smb = "AUDJPY, CADJPY, EURJPY, NZDJPY, GBPJPY, CHFJPY"; input class="type">class="kw">double delta = class="num">200; input class="type">class="kw">double volumes = class="num">0.03; input class="type">class="kw">double tAkeProfitKoeff = class="num">2; input class="type">int iHourOpen = class="num">5; input class="type">int iHourClose = class="num">22; input class="type">int iSlippage = class="num">15; input class="type">int Magic = class="num">12345;
把开仓平仓逻辑拆进三件套
这段 MT5 EA 骨架把初始化、模型判断和界面动作分成了三个头文件引用:EA_Init.mqh 管全局参数,View 管下单与持仓界面,Model 管时间窗判断。实际跑起来,OnInit 只做两件事——把品种符号 smb 传给 init,再把 view 初始化好,返回 INIT_SUCCEEDED 即代表加载成功。 OnTick 是盯盘核心:先问 model.TimeToOpen() 是否到开仓时间窗,是就调 view.Enter() 并直接 return,不再往下走;否则检查 model.TimeToClose(),触发则一次性平掉所有挂单和持仓。注意这里平仓分 CloseAllOrder 和 ClosePositions 两步,漏掉任一都会留尾巴。 OnTrade 里只转发给 view.OnTrade(),说明成交回执的刷新完全交给视图层。想在 MT5 验证,把这三个 mqh 补齐后加载到 EURUSD 的 M1,观察日志里 Initialize 是否只跑一次、Tick 触发频率是否跟 K 线对齐。外汇与贵金属杠杆高,实盘前务必用策略测试器跑至少 3 个月历史。
class="macro">#include "EA_Init.mqh" class="macro">#include "View\View.mqh" class="macro">#include "Model\Model.mqh" CInit init; CModel model; CView view; class="type">int OnInit() { init.Initialize(smb); view.Initialize(); class="kw">return INIT_SUCCEEDED; } class="type">void OnTick() { if (model.TimeToOpen() ) { view.Enter(); class="kw">return; } if (model.TimeToClose() ) { view.CloseAllOrder(); view.ClosePositions(); } } class="type">void OnTrade() { view.OnTrade(); }
◍ 视图与控制器的高频耦合隐患
当前这套简易 EA 里,视图对控制器方法的直接调用达到 11 次,全部走全局对象中转。在只挂一个对话框加一个交易面板的轻量结构下还没显出问题,但交互完全依赖 OnTick 里的控制器兜转,模型与视图彼此不直接通信。 外汇与贵金属行情跳动频繁,EA 逻辑一旦扩张,这类跨组件调用很可能翻几倍,把 MVC 原本解耦的优势磨平。想验证的话,开 MT5 在策略测试器里跑扩展前的版本,数一下日志里 CInint 相关入口就能复现这 11 次。 绕开全局对象、改用引用传参访问组件,是更干净的做法,类似 MFC 里文档与视图的挂接方式。范式本身没硬性规定组件怎么互叫,所以这条路留给你按实盘复杂度自己取舍。
「多模型下的模块拆分思路」
当指标或 EA 最初套用 MVC 范式后,若再引入两个模型、一个视图,控制器逻辑会变重。守住 MVC 的关键不是堆代码,而是把每个组件拆成独立模块,各自暴露访问接口,控制器只做调度。 原文给出的六个附件可作验证样本:MVC_primitive_1.zip 仅 0.9 KB 是最粗糙的单文件指标;MVC_EA_primitive.zip 1.47 KB 已是 MVC 骨架的伪 EA;EA_Real.zip 达 71.7 KB,是含模块管理的实盘级 EA。打开 MT5 加载这几个文件,能直观比对范式演进带来的结构差异。 模块级交互在那篇专门讲模块管理的文章里有具体写法。你在自己的 EA 里试把信号模型、仓位模型、绘图视图分别丢进不同 mqh,会比全塞进一个 mq5 更好维护,但也意味着参数传递要更克制,别把 Magik 这类参数散得到处都是。