Simulink: EA 交易开发人员指南·进阶篇
🧩

Simulink: EA 交易开发人员指南·进阶篇

(2/3)· 用手写极少代码把可视化交易系统搬进 MetaTrader 5 的历史回测里

含代码示例偏理论 第 2/3 篇
不少交易者以为 Simulink 只是学术玩具,真要做 EA 还得从头手写 MQL5。结果卡在模型与终端的接口上,回测跑了却对不上逻辑。把可视化系统直接编译进 MT5,才是少走弯路的做法。

◍ 把 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 回测仅是历史逻辑验证,实盘信号可能失真。

MQL5 / C++
[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 数据验证滑点影响。

MQL5 / C++
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 跑一遍验证返回码分布。

MQL5 / C++
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;
}
交给小布盯盘看回测断层
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 Simulink 导出信号与 MT5 实盘 tick 的偏差提示,你专注调模型而不是对账。

常见问题

MTS 指在 Simulink 环境内运行的交易系统模型,侧重可视化逻辑与动态仿真;EA 交易是编译后跑在 MetaTrader 5 的 MQL5 程序,负责真实下单与历史测试,二者通过接口对接。
目前小布盯盘不解析 .slx 模型文件,但可接入 EA 输出的信号流做盘口偏离预警,帮你发现由简入繁过程中被忽略的滑点风险。
它处理事件驱动的状态切换,比如从观望到持仓的跳变条件,比纯 Simulink 连续块更贴合外汇贵金属的会话时段断裂。
Simulink 负责策略骨架,MQL5 负责终端契约与订单控制,后者无法完全图形化,概率上仅保留必要手写可降低出错面。