MVC 设计范式及其可能的应用·进阶篇
「指标缓冲区的初始化与增量计算边界」
自定义指标在 OnCalculate 里最先要处理的是数据量不足的情况。代码用 if(rates_total <= 4) return 0; 直接拦截,说明该算法至少需要 5 根 K 线才能跑出第一个有效值,这是回测时前 4 根 bar 全为空值的根本原因。
增量刷新逻辑靠 limit = rates_total - prev_calculated 判断。limit 为 0 代表无新柱,1 代表只来一根新柱只算最新 shift,大于 1 才走全量重置:ArrayInitialize(lb, EMPTY_VALUE) 后从 rates_total-4 循环到 1 逐根重算,避免每次 tick 都全表扫。
视图类 CView 把缓冲区封装掉了。构造函数写死 _Width=2、_Name="Primitive",Initialize 里 SetIndexBuffer(0,_lb,INDICATOR_DATA) 绑定一号缓冲并 ArraySetAsSeries(_lb,true) 把时间轴倒序,开 MT5 新建指标时改这两个值就能换线宽和面板名。
外汇与贵金属杠杆高、滑点跳空频繁,这类依赖 close 数组的轻量指标在重大数据行情中可能漏算瞬时跳空柱,实盘前务必用 EURUSD 的 M1 历史数据跑一遍验证边界。
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[], 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); } class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property indicator_buffers class="num">1 class="macro">#class="kw">property indicator_plots class="num">1 class CView { class="kw">public: class="type">void CView(); class="type">void ResetBuffers(); class="type">int Initialize(); class="type">void SetData(class="type">class="kw">double value, class="type">int shift = class="num">0); class="kw">private: class="type">class="kw">double _lb[]; class="type">int _Width; class="type">class="kw">string _Name; class="type">class="kw">string _Label; };class=class="str">"cmt">// class CView class="type">void CView::CView() { _Width = class="num">2; _Name = "Primitive" ; _Label = "Label1"; }class=class="str">"cmt">// class="type">void CView::CView() class="type">void CView::ResetBuffers() { ArrayInitialize(_lb, EMPTY_VALUE); } class="type">int CView::Initialize() { SetIndexBuffer(class="num">0, _lb, INDICATOR_DATA); ArraySetAsSeries(_lb, true); IndicatorSetString(INDICATOR_SHORTNAME, _Name);
指标外观与缓冲写入的底层绑定
自定义指标在 MT5 里跑起来之前,得先把绘制属性钉死。上面这段把第 0 号绘图索引设成实线、深石板蓝、线宽走变量 _Width,标签用 _Label——改其中任意一个宏,终端里曲线的观感立刻变,但计算逻辑不受影响。 IndicatorSetInteger(INDICATOR_DIGITS, _Digits) 这行决定价格小数位跟随品种报价,黄金 XAUUSD 的 _Digits 通常是 2 或 3,EURUSD 多为 5,错设会让你在副图上看到错位的价格标注。 SetData 方法看着简单,_lb[shift] = value 直接把算好的值塞进线缓冲对应柱位移。shift=0 是当前柱,历史值靠正偏移回写;若外部调用时 shift 超出缓冲长度,MT5 会静默丢值,图表右侧出现断线,这种 bug 只能开 MT5 用专家日志抓。 IndicatorSetString(INDICATOR_SHORTNAME, _Name) 控制导航树里的短名,不设的话默认带参数尾巴,多品种加载时容易混淆。外汇与贵金属杠杆高,指标仅作概率参考,实盘前务必在策略测试器跑一遍不同点差环境。
IndicatorSetInteger(INDICATOR_DIGITS, _Digits); PlotIndexSetString(class="num">0, PLOT_LABEL, _Label); PlotIndexSetInteger(class="num">0, PLOT_DRAW_TYPE, DRAW_LINE); PlotIndexSetInteger(class="num">0, PLOT_LINE_COLOR, clrDarkSlateBlue); PlotIndexSetInteger(class="num">0, PLOT_LINE_STYLE, STYLE_SOLID); PlotIndexSetInteger(class="num">0, PLOT_LINE_WIDTH, _Width); class="kw">return(INIT_SUCCEEDED); } class="type">void CView::SetData(class="type">class="kw">double value,class="type">int shift) { _lb[shift] = value; } IndicatorSetString(INDICATOR_SHORTNAME, _Name); IndicatorSetInteger(INDICATOR_DIGITS, _Digits);
◍ 把伪EA拆成控制器、视图和模型
先搭一个极简伪EA:前一根蜡烛收阳就模拟多单,收阴就模拟空单,场内只允许一笔持仓,不真正下单。它的 OnTick 只判断新蜡烛和涨跌,然后切状态变量 iBuy / iSell。 在EA里,OnInit、OnDeinit、OnTick 这些生命周期函数天然属于控制器;视图不画图形,只负责把持仓状态呈现给用户,所以订单、虚拟止损止盈、加权均价都归视图;模型则装决策逻辑:怎么开仓、手数怎么算、止损止盈怎么定,资金管理也应在模型里实现。 把上面那个伪EA按职责拆开,函数挪进对应子文件夹后,OnTick 明显变短。EA 越复杂,MVC 这种分层就越能救命——第三方接手代码时,找问题不用通读全文。 为了验证范式在‘真实’代码上的效果,作者翻出 2013 年写的 $OrdersInTheMorning 旧草稿:周一指定时间挂一买一卖单,距市价一定距离,任一下单触发就删另一单,周五晚平仓,只跑指定货币对。原版是 MT4 的,重写成 MT5 后主函数又长又嵌套,WeekOrderParam 一个辅助函数既管入场又管开仓,明显越界。 重构时,先把货币对列表挪到输入参数,初始化垃圾清出 OnInit,建 EA_Init.mqh 用类装初始化(成员全公开只是为省访问器噪音,真实项目别学)。模型层建 Model.mqh 的 CModel 类,只留检查入场和离场两个方法加持仓标志;视图层建 View.mqh,先试了三个裸函数各 new 一个 CTrade 对象——不是好写法,随后收进 CView 类持有一个私密 CTrade 字段。控制器补 OnTrade 处理程序删未触发挂单,这是全场唯一有点绕的地方。 改完的主文件只剩调度,要加功能直接动对应组件,不影响另外两层。外汇与贵金属杠杆高,这类挂单策略在跳空时可能两边止损都被扫,实盘前务必在 MT5 策略测试器跑历史数据。
<span class="keyword">class="type">class="kw">datetime</span> dtNow; <span class="keyword">class="type">int</span> iBuy, iSell; <span class="keyword">class="type">int</span> <span class="functions">OnInit</span>() { iBuy = iSell = <span class="number">class="num">0</span>; <span class="keyword">class="kw">return</span>(<span class="macro">INIT_SUCCEEDED</span>); } <span class="keyword">class="type">void</span> <span class="functions">OnDeinit</span>(<span class="keyword">const</span> <span class="keyword">class="type">int</span> reason) { } <span class="keyword">class="type">void</span> <span class="functions">OnTick</span>() { <span class="keyword">if</span> (IsNewCandle() ) { <span class="keyword">class="type">class="kw">double</span> o = <span class="functions">iOpen</span>(<span class="macro">NULL</span>,<span class="macro">PERIOD_CURRENT</span>,<span class="number">class="num">1</span>); <span class="keyword">class="type">class="kw">double</span> c = <span class="functions">iClose</span>(<span class="macro">NULL</span>,<span class="macro">PERIOD_CURRENT</span>,<span class="number">class="num">1</span>); <span class="keyword">if</span> (c < o) { <span class="comment">class=class="str">"cmt">// Enter Sell</span> <span class="keyword">if</span> (GetSell() == <span class="number">class="num">1</span>) <span class="keyword">class="kw">return</span>; <span class="keyword">if</span> (GetBuy() == <span class="number">class="num">1</span>) CloseBuy(); EnterSell(); } <span class="keyword">else</span> { <span class="comment">class=class="str">"cmt">// Enter Buy</span> <span class="keyword">if</span> (GetBuy() == <span class="number">class="num">1</span>) <span class="keyword">class="kw">return</span>; <span class="keyword">if</span> (GetSell() == <span class="number">class="num">1</span>) CloseSell(); EnterBuy(); } }<span class="comment">class=class="str">"cmt">// if (IsNewCandle() ) </span> }<span class="comment">class=class="str">"cmt">// class="type">void OnTick()</span> <span class="keyword">class="type">bool</span> IsNewCandle() { <span class="keyword">class="type">class="kw">datetime</span> d = <span class="functions">iTime</span>(<span class="macro">NULL</span>, <span class="macro">PERIOD_CURRENT</span>, <span class="number">class="num">0</span>); <span class="keyword">if</span> (dtNow == -<span class="number">class="num">1</span> || dtNow != d) { dtNow = d; <span class="keyword">class="kw">return</span> <span class="macro">true</span>; } <span class="keyword">class="kw">return</span> <span class="macro">class="kw">false</span>; }<span class="comment">class=class="str">"cmt">// class="type">bool IsNewCandle()</span> <span class="keyword">class="type">void</span> CloseBuy() {iBuy = <span class="number">class="num">0</span>;} <span class="keyword">class="type">void</span> CloseSell() {iSell = <span class="number">class="num">0</span>;} <span class="keyword">class="type">void</span> EnterBuy() {iBuy = <span class="number">class="num">1</span>;}
「多币种 H4 参数与开关的落地写法」
这段逻辑把六个日元交叉盘(AUDJPY、CADJPY、EURJPY、NZDJPY、GBPJPY、CHFJPY)统一塞进一个字符串数组,在 OnTick 里按 H4 周期逐个跑 WeekOrderParam。外汇与贵金属杠杆高,跨币种同时挂单会放大回撤,实盘前先在策略测试器里用 0.03 手验证。 input 参数里 delta=200 是点数阈值基准, volumes=0.03 固定手数,止损系数 sTopLossKoeff=1、止盈系数 tAkeProfitKoeff=2,也就是盈亏比倾向 2:1。iTradeCount=3 限制单币种交易次数,iSlippage=15 点容差,iTimeOut=2000 毫秒网络超时。 oNlyMondeyOrders=true 时只在周一 DayOfWeek()==1 出手;bHLprocess 控制高低点处理逻辑;sTimeToOpenOrders="05:05"、sTimeToCloseOrders="22:00" 框定日内交易窗口。iTimeIntervalForWork=0.5 若不在 0~24 区间会弹 Alert 拦截,避免乱设周期。 把这段代码直接贴进 MT5 EA 的全局区,改 smb[] 数组就能换品种池;想降频就把 iTradeCount 调到 1,日元夜盘跳空可能触发额外滑点。
class="type">void EnterSell() {iSell = class="num">1;} class="type">int GetBuy() {class="kw">return iBuy;} class="type">int GetSell() {class="kw">return iSell;} class="type">void OnTick() { if (IsNewCandle() ) { class="type">class="kw">double o = iOpen(NULL,PERIOD_CURRENT,class="num">1); class="type">class="kw">double c = iClose(NULL,PERIOD_CURRENT,class="num">1); if (MaySell(o, c) ) EnterSell(); if (MayBuy(o, c) ) EnterBuy(); }class=class="str">"cmt">// if (IsNewCandle() ) }class=class="str">"cmt">// class="type">void OnTick() class="macro">#class="kw">property copyright "Copyright class="num">2013, MetaQuotes Software Corp." class="macro">#class="kw">property link "http:class=class="str">"cmt">//www.metaquotes.net" class="macro">#include <Trade\Trade.mqh> class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ 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 sTopLossKoeff = class="num">1; input class="type">class="kw">double tAkeProfitKoeff = class="num">2; input class="type">int iTHour = class="num">0; input class="type">bool bHLprocess = true; input class="type">bool oNlyMondeyOrders = class="kw">false; input class="type">class="kw">string sTimeToCloseOrders = "class="num">22:class="num">00"; input class="type">class="kw">string sTimeToOpenOrders = "class="num">05:class="num">05"; input class="type">class="kw">double iTimeIntervalForWork = class="num">0.5; input class="type">int iSlippage = class="num">15; input class="type">int iTradeCount = class="num">3; input class="type">int iTimeOut = class="num">2000; class="type">int dg; class="type">bool bflag; class="type">class="kw">string smb[] = {"AUDJPY","CADJPY","EURJPY","NZDJPY","GBPJPY","CHFJPY"}; class="type">int init() { if ( (iTimeIntervalForWork < class="num">0) || (iTimeIntervalForWork > class="num">24) ) { Alert("... ",iTimeIntervalForWork); } class="kw">return (class="num">0); } class="type">void OnTick() { if ((oNlyMondeyOrders == true) && (DayOfWeek() != class="num">1) ) { } else { class="type">int count=ArraySize(smb); class="type">bool br = true; for (class="type">int i=class="num">0; i<count;i++) { if (!WeekOrderParam(smb[i], PERIOD_H4, delta*SymbolInfoDouble(smb[i],SYMBOL_POINT) ) ) br = class="kw">false; } if (!br) Alert("...");
按星期与小时锚定周内的突破价位
这段逻辑把「只在周一交易」和「按指定小时取 K 线」两套分支揉在一个函数里。当 oNlyMondeyOrders 为 true 时,向后翻 k=3 根 0 周期 K 线,用 TimeDayOfWeek 滤出周一、再用 TimeHour 匹配 iTHour,命中即把下标 j 记下并 break;否则就在最近 3 根里只按小时匹配。 若 iTHour 小于 0,直接令 j=0,相当于拿当前最新根做基准;若循环完 j 仍是 -1,Print("tf?") 并返回 false,说明时间条件在最近 3 根内没碰上,调用方应放弃挂单。 价位计算分两种:bHLprocess 为 true 时,买突破价 bsp = 第 j 根最高价 + dlt,卖突破价 ssp = 第 j 根最低价 - dlt;为 false 时改用开收二者的最大值/最小值加减 dlt。全部经 NormalizeDouble 按 dg 位数舍入,避免跨品种点值错位。 外汇与贵金属杠杆高,这套锚定只解决「取哪根、算什么价」,实盘触发仍可能滑点吞掉 dlt 缓冲,建议先在 MT5 策略测试器用 EURUSD 的 M15 跑一遍 j 的 Alert 输出确认星期过滤符合预期。
class="type">bool WeekOrderParam(class="type">class="kw">string symbol,class="type">int tf, class="type">class="kw">double dlt) { class="type">int j = -class="num">1; class="type">class="kw">datetime mtime = class="num">0; class="type">int k = class="num">3; Alert(symbol); if (iTHour >= class="num">0) { if (oNlyMondeyOrders == true) { for (class="type">int i = class="num">0; i < k; i++) { mtime = iTime(symbol,class="num">0,i); if (TimeDayOfWeek(mtime) == class="num">1) { if (TimeHour(mtime) == iTHour) { j = i; break; } } } } else { for (class="type">int i = class="num">0; i < k; i++) { mtime = iTime(symbol,class="num">0,i); if (TimeHour(mtime) == iTHour) { j = i; break; } } } if (j == -class="num">1) { Print("tf?"); class="kw">return (class="kw">false); } } else j = class="num">0; Alert(j); class="type">class="kw">double bsp,ssp; if (bHLprocess) { bsp = NormalizeDouble(iHigh(symbol,class="num">0,j) + dlt, dg); ssp = NormalizeDouble(iLow(symbol,class="num">0,j) - dlt, dg); } else { bsp = NormalizeDouble(MathMax(iOpen(symbol,class="num">0,j),iClose(symbol,class="num">0,j)) + dlt, dg); ssp = NormalizeDouble(MathMin(iOpen(symbol,class="num">0,j),iClose(symbol,class="num">0,j)) - dlt, dg); }
◍ 挂单与时段开关的底层拼装
止损幅度先按系数折算:slsize 用 NormalizeDouble 把 (bsp - ssp) 乘 sTopLossKoeff 对齐到小数位 dg,买暂停损挂在 bsp、卖暂停损挂在 ssp。 止盈不是固定点值,而是 tAkeProfitKoeff 倍的 slsize:tpb = bsp + 系数*slsize,tps = ssp - 系数*slsize,这样盈亏比随通道宽度自适应,外汇与贵金属品种点值差异大时更不容易手抖填错。 下单走封装后的 mOrderSend,内部用 for 循环重试 iTradeCount 次,ticket<0 就打印错误码与超时值,成功即 break;原函数里真正的 OrderSend 被注释掉,实盘前必须换回官方发单调用。 CInit 负责把逗号分隔的品种对拆成 names[],再逐个填 points[i]=SymbolInfoDouble(name,SYMBOL_POINT),多品种遍历止损换算时不用反复查点值。 CModel 用 bFlag 锁住每周只开一次:TimeToOpen 限定 day_of_week==1 且 hour>=iHourOpen 才放行,TimeToClose 则卡 day_of_week==5 且 hour>=iHourClose,等于把仓位生命周期钉在周一开盘到周五某小时后,规避周末跳空这类高概率风险事件。 Enter() 里只看到 trade.SetExpertMagicNumber(Magic),具体 volume 与 symbol 选取在前述逻辑算完后再补,MT5 里把 Magic 与 iHourOpen / iHourClose 调成你经纪商冬夏令时对应的数值就能跑通这套框架。
class="type">class="kw">double slsize = NormalizeDouble(sTopLossKoeff * (bsp - ssp), dg); class="type">class="kw">double tpb = NormalizeDouble(bsp + tAkeProfitKoeff*slsize, dg); class="type">class="kw">double tps = NormalizeDouble(ssp - tAkeProfitKoeff*slsize, dg); class="type">class="kw">datetime expr = class="num">0; class="kw">return (mOrderSend(symbol,ORDER_TYPE_BUY_STOP,volumes,bsp,iSlippage,ssp,tpb,NULL,class="num">0,expr,CLR_NONE) && mOrderSend(symbol,ORDER_TYPE_SELL_STOP,volumes,ssp,iSlippage,bsp,tps,NULL,class="num">0,expr,CLR_NONE) ); } class="type">int mOrderSend( class="type">class="kw">string symbol, class="type">int cmd, class="type">class="kw">double volume, class="type">class="kw">double price, class="type">int slippage, class="type">class="kw">double stoploss, class="type">class="kw">double takeprofit, class="type">class="kw">string comment = "", class="type">int magic=class="num">0, class="type">class="kw">datetime expiration=class="num">0, class="type">color arrow_color=CLR_NONE) { class="type">int ticket = -class="num">1; for (class="type">int i = class="num">0; i < iTradeCount; i++) { class=class="str">"cmt">// ticket=OrderSend(symbol,cmd,volume,price,slippage,stoploss,takeprofit,comment,magic,expiration,arrow_color); if(ticket<class="num">0) Print(symbol,": ",GetNameOP(cmd), GetLastError() ,iTimeOut); else break; } class="kw">return (ticket); } class CInit { class="kw">public: class="type">void CInit(){} class="type">void Initialize(class="type">class="kw">string pair); class="type">class="kw">string names[]; class="type">class="kw">double points[]; class="type">int iCount; }; class="type">void CInit::Initialize(class="type">class="kw">string pair) { iCount = StringSplit(pair, StringGetCharacter(",", class="num">0), names); ArrayResize(points, iCount); for (class="type">int i = class="num">0; i < iCount; i++) { points[i] = SymbolInfoDouble(names[i], SYMBOL_POINT); } } class CModel { class="kw">public: class="type">void CModel(): bFlag(class="kw">false) {} class="type">bool TimeToOpen(); class="type">bool TimeToClose(); class="kw">private: class="type">bool bFlag; }; class="type">bool CModel::TimeToOpen() { if (bFlag) class="kw">return class="kw">false; class="type">MqlDateTime tm; TimeCurrent(tm); if (tm.day_of_week != class="num">1) class="kw">return class="kw">false; if (tm.hour < iHourOpen) class="kw">return class="kw">false; bFlag = true; class="kw">return true; } class="type">bool CModel::TimeToClose() { if (!bFlag) class="kw">return class="kw">false; class="type">MqlDateTime tm; TimeCurrent(tm); if (tm.day_of_week != class="num">5) class="kw">return class="kw">false; if (tm.hour < iHourClose) class="kw">return class="kw">false; bFlag = class="kw">false; class="kw">return true; } class="type">void Enter() { CTrade trade; trade.SetExpertMagicNumber(Magic);