Simulink: EA 交易开发人员指南·进阶篇
(2/3)· 用手写极少代码把可视化交易系统搬进 MetaTrader 5 的历史回测里
◍ 把 MT5 历史价搬进 Simulink 搭策略骨架
做 Simulink 建模前,得先把 MT5 的历史价弄进 Matlab。用一段 testClose.mq5 脚本把开高低收和点差导出来,再靠 priceTxt.m 读进工作区;EMA 直接用 Matlab 自带的 movavg 算: [ema21, ema55] = movavg(close, 21, 55, 'e'); 同时建一个柱索引辅助数组 num=1:length(close);,策略参数先写死 K=3、sl=0.0065、tp=0.0295(绝对值止损止盈)。 建 mts 模型时有个坑:保存若报 windows-1251 不兼容字符,关掉所有模型窗,跑 bdclose all; set_param(0, 'CharacterEncoding', 'windows-1252'); 就能消掉。数据源用 FromWorkspace 子系统,从 Signal Processing Blockset 拖 4 个 Signal from Workspace 块,分别喂 open、ema21、ema55、num,再用 Bus Creator(输入数改 4)打包;另加 5 个 Constant 块给 spread、high、low、tp、sl,输出端口做 5 份副本接好。 交易触发靠数学运算块算 ema21-ema55 微分,接 K Delay(延迟 K=3 根)后进 Stateflow Chart:Buy 事件设 Falling 触发、Sell 设 Rising 触发,OpenBuy/OpenSell 用 Function call。虚拟通道对象 InputBus 要在 Bus Editor 里手建,元素 open/ema21/ema55/num,否则模型跑不起来。 黑盒 Chart 里建 OutputBus,元素含 barOpen、OpenPrice、TakeProfit、StopLoss、ClosePrice 等。首根处理条件写 [Input.num>=56 && Input.num>Output.barClose],即至少 56 根柱且新柱高过上一平仓柱才动手;非首仓时账户余额沿用,首仓赋 100000。OpenBuy 时 PositionDir=1、OpenPrice=Input.open+spread[i]*1e-5,止损止盈按 sl/tp 绝对值挂。跑一次 Start Simulation 让 Symbol Wizard 把变量吸进模型,Input 绑 InputBus、Output 绑 OutputBus,high/low 尺寸设 [len 1],手数常量填 1,图表就变成 7 进 1 出的仓位处理块。外汇与贵金属杠杆高,Simulink 回测仅是历史逻辑验证,实盘信号可能失真。
[ema21, ema55] = movavg(close, class="num">21, class="num">55, &class="macro">#x27;e&class="macro">#x27;); num=class="num">1:length(close); K=class="num">3; sl=class="num">0.0065; tp=class="num">0.0295; bdclose all set_param(class="num">0, &class="macro">#x27;CharacterEncoding&class="macro">#x27;, &class="macro">#x27;windows-class="num">1252&class="macro">#x27;);
「让结果回到 Matlab 工作区」
Position handling 子系统是被动触发的:只有 OpenBuy / OpenSell 事件进来把它“唤醒”,它才跑计算,平时不占资源。这种懒加载思路在 MT5 里也能借鉴——用事件过滤器替代轮询,省的是每 tick 的无谓开销。 把处理结果送回 Matlab 工作区靠 ToWorkspace 子系统。做法与 FromWorkspace 对称:从 Simulink Ports & Subsystems 拖一个 Subsystem 块重命名为 ToWorkspace,塞进 Position handling 里。因为数据走的是 OutputBus 非虚拟总线,得先接一个 Bus Selector 把信号拆出来——它默认 1 进 2 出,而我们要 10 路,全选左侧信号挪到 Selected signals 右侧即可。 接着从 Commonly used blocks 加 Mux,输入数填 10;再从 Sinks 拉 ToWorkspace 进子系统,变量名改 AccountBalance,保存格式由 Structure 改成 Array,删掉无用输出端口。构建前先确认工作区有这些变量:InputBus, K, OutputBus, close, ema21, ema55, high, low, num, open, sl, spread, tp。Stop Time 用 num 末尾元素定义,等于被处理向量长度。 编译前跑 mex-setup 选编译器(文内用的 MSVC 2008 SP1)。首次构建报过端口宽度错:spread 被当成双型标量,实际是 59739 元素向量。在模型浏览器把该变量设成 Inherit: Same as Simulink、Size 填 -1 就好。 改完重跑,模型在 40 秒内处理了约 60000 根 K 线,交易区间 2001.01.01 至 2010.08.16 11:00,未平仓总数 461。外汇与贵金属回测均属高风险,历史撮合结果不代表未来概率。
把 Simulink 逻辑搬进 EA 的实操落点
从 Simulink 导出的交易逻辑要落地到 MT5,第一步是区分哪些模块必须显式写进 MQL5,哪些可以隐去。像 FromWorkspace 里的 open signal 块就不需要存在——EA 在信号触发后直接实时取价即可,而 ema21 与 ema55 的差值计算、K 根延迟这些必须保留为代码里的数组与判断。 Take Profit 和 Stop Loss 在 EA 中定义为 input 变量,EURUSD 是 5 位报价,所以代码里遇到 _Digits==5 时要把 135 和 60 这类初始值乘 10,否则止损止盈会差一个数量级。spread、high、low 数组在 Simulink 里负责喂历史矩阵,MT5 中由平台自动执行平仓,因此代码不显式声明。 仓位处理子系统的第一道闸门很硬:可用柱数少于 56 直接 Alert 并 return。这是 ema55 周期加缓冲的最低要求,少一根都可能让均线参考失真。建仓事件 OpenBuy / OpenSell 由差值符号翻转产生,后续在 Position handling 里接手。 下面这段是核心声明与信号判断的原样代码,逐行对应上述模块关系: int ma1Handle; // 移动平均线1的指标句柄,对应 Simulink 的“ema21 信号”模块 int ma2Handle; // 移动平均线2的指标句柄,对应“ema55 信号” ma1Handle=iMA(_Symbol,_Period,MA1_Period,0,MODE_EMA,PRICE_CLOSE); // 获取 ema21 句柄 ma2Handle=iMA(_Symbol,_Period,MA2_Period,0,MODE_EMA,PRICE_CLOSE); // 获取 ema55 句柄 double ma1Val[]; // 动态数组存每根 ema21 值 Double ma2Val[]; // 动态数组存每根 ema55 值 ArraySetAsSeries(ma1Val,true); // 数组按时间倒序,索引0为最新柱 ArraySetAsSeries(ma2Val,true); // 同上 input int TakeProfit=135; // 获利点数,来自 FromWorkspace 的“获利” input int StopLoss=60; // 止损点数,来自 FromWorkspace 的“止损” int sl,tp; sl = StopLoss; tp = TakeProfit; if(_Digits==5) { sl = sl*10; tp = tp*10; } // 5位报价扩10倍 // 买入条件:ema 差值由负转非负
| bool Buy=((ma2Val[1+K]-ma1Val[1+K])>=0 && (ma2Val[K]-ma1Val[K])<0) |
|---|
((ma2Val[1+K]-ma1Val[1+K])>0 && (ma2Val[K]-ma1Val[K])==0); // 卖出条件:ema 差值由正转非正
| bool Sell=((ma2Val[1+K]-ma1Val[1+K])<=0 && (ma2Val[K]-ma1Val[K])>0) |
|---|
((ma2Val[1+K]-ma1Val[1+K])<0 && (ma2Val[K]-ma1Val[K])==0); if(Bars(_Symbol,_Period)<56) // 柱数不足56拒绝运行 { Alert("Not enough bars!"); return(-1); } 开 MT5 把这段贴进 EA 模板,把 K 设为 1 先用 EURUSD 的 M5 回测,能直接看到 ema 交叉信号在 56 根之后才激活。外汇与贵金属杠杆高,实盘前务必在策略测试器跑至少三个月 tick 数据验证滑点影响。
class="type">int ma1Handle; class=class="str">"cmt">// 移动平均线1的指标句柄:“ema21 信号”模块 class="type">int ma2Handle; class=class="str">"cmt">// 移动平均线2的指标句柄:“ema55 信号” ma1Handle=iMA(_Symbol,_Period,MA1_Period,class="num">0,MODE_EMA,PRICE_CLOSE); class=class="str">"cmt">// 获取移动平均线1的句柄 ma2Handle=iMA(_Symbol,_Period,MA2_Period,class="num">0,MODE_EMA,PRICE_CLOSE); class=class="str">"cmt">// 获取 移动平均线2的指标句柄 class="type">class="kw">double ma1Val[]; class=class="str">"cmt">// 存储每个柱形的移动平均线1的动态数组:“ema21 信号” class="type">class="kw">double ma2Val[]; class=class="str">"cmt">// 存储每个柱形的移动平均线2的动态数组:“ema55 信号” ArraySetAsSeries(ma1Val,true);class=class="str">"cmt">// MA1的指标值数组:“ema21信号”模块 ArraySetAsSeries(ma2Val,true);class=class="str">"cmt">// MA2的指标值数组:“ema55信号”模块 input class="type">int TakeProfit=class="num">135; class=class="str">"cmt">// 获利:FromWorkspace子系统中的“获利” input class="type">int StopLoss=class="num">60; class=class="str">"cmt">// 止损:FromWorkspace子系统中的“止损” class="type">int sl,tp; sl = StopLoss; tp = TakeProfit; if(_Digits==class="num">5) { sl = sl*class="num">10; tp = tp*class="num">10; } class=class="str">"cmt">// 买入(激活于否定值) class="type">bool Buy=((ma2Val[class="num">1+K]-ma1Val[class="num">1+K])>=class="num">0 && (ma2Val[K]-ma1Val[K])<class="num">0) || ((ma2Val[class="num">1+K]-ma1Val[class="num">1+K])>class="num">0 && (ma2Val[K]-ma1Val[K])==class="num">0); class=class="str">"cmt">// 卖出 (激活于肯定值) class="type">bool Sell=((ma2Val[class="num">1+K]-ma1Val[class="num">1+K])<=class="num">0 && (ma2Val[K]-ma1Val[K])>class="num">0)|| ((ma2Val[class="num">1+K]-ma1Val[class="num">1+K])<class="num">0 && (ma2Val[K]-ma1Val[K])==class="num">0); if(Bars(_Symbol,_Period)<class="num">56) class=class="str">"cmt">// “持仓处理”子系统的第1种转换:条件是 [Input.num>=class="num">56] { Alert("Not enough bars!"); class="kw">return(-class="num">1); }
◍ 持仓判定与市价买单的下单骨架
EA 跑之前得先搞清楚当前有没有单子。用 PositionSelect(_Symbol) 抓当前品种持仓,再读 POSITION_TYPE:是 BUY 就置 IsBought,是 SELL 就置 IsSold,两个布尔量后面直接喂给 IsTraded() 做拦截。 IsTraded() 的逻辑很直白:只要 IsBought 或 IsSold 任一为真,弹窗 'Transaction is complete' 并 return true,上层收到 true 就直接 return 退出,不再开新仓。这套写法能把『已持仓就不再交易』硬锁死,避免重复下单。 真要开买入,得把 MqlTradeRequest 结构填全。action 设 TRADE_ACTION_DEAL 走市价成交;price 取 NormalizeDouble(latest_price.ask,_Digits) 对齐报价精度;sl 用 bid - STP*_Point、tp 用 bid + TKP*_Point 算,STP/TKP 是你外置的止损点数和止盈点数。 type_filling 填 ORDER_FILLING_FOK,意味着要么按指定交易量全部成交、要么不成交,deviation 给 100 点滑点容忍。OrderSend 之后盯 retcode:10009(已成交)或 10008(下单成功)都算过,否则弹 GetLastError() 并 return。外汇与贵金属杠杆高,FOK 模式在流动性差时可能直接拒单,实盘前请在 MT5 策略测试器用历史 tick 跑一遍验证返回码分布。
class="type">bool IsBought = class="kw">false; class=class="str">"cmt">// 买入 class="type">bool IsSold = class="kw">false; class=class="str">"cmt">// 卖出 if(PositionSelect(_Symbol)==true) class=class="str">"cmt">// 有持仓 { if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY) { IsBought=true; class=class="str">"cmt">// 买入 } else if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL) { IsSold=true; class=class="str">"cmt">// 卖出 } } class=class="str">"cmt">// 检查持仓 if(IsTraded(IsBought,IsSold)) { class="kw">return; } class="type">bool IsTraded(class="type">bool IsBought,class="type">bool IsSold) { if(IsSold || IsBought) { Alert("Transaction is complete"); class="kw">return(true); } else class="kw">return(class="kw">false); } mrequest.action = TRADE_ACTION_DEAL; class=class="str">"cmt">// 市价买入 mrequest.price = NormalizeDouble(latest_price.ask,_Digits); class=class="str">"cmt">// 最近的买价 mrequest.sl = NormalizeDouble(latest_price.bid - STP*_Point,_Digits); class=class="str">"cmt">// 设置止损 mrequest.tp = NormalizeDouble(latest_price.bid + TKP*_Point,_Digits); class=class="str">"cmt">// 设置获利 mrequest.symbol = _Symbol; class=class="str">"cmt">// 交易品种 mrequest.volume = Lot; class=class="str">"cmt">// 所有的交易量 mrequest.magic = EA_Magic; class=class="str">"cmt">// EA的编号 mrequest.type = ORDER_TYPE_BUY; class=class="str">"cmt">// 买入订单 mrequest.type_filling = ORDER_FILLING_FOK; class=class="str">"cmt">// 指定交易量且对某一价格来说至少不差于指定的交易量 mrequest.deviation=class="num">100; class=class="str">"cmt">// 滑点 OrderSend(mrequest,mresult); if(mresult.retcode==class="num">10009 || mresult.retcode==class="num">10008) class=class="str">"cmt">// 请求完成或者成功下单 { Alert("A buy order has been placed, ticket #:",mresult.order); } else { Alert("A buy order has not been placed; error:",GetLastError()); class="kw">return; }