开发多模块智能交易系统·综合运用
(3/3)·拆好的模块若拼不对,依然只是散沙——本篇把19个环节串成能跑的EA骨架
「用生成器拼多模块EA省下手工时间」
辅助(外部)模块本质是只做一件事并把结果写进指标缓冲区的指标,主模块按需取数,这样同一套EA能按品种或经纪商重新组合,理论上能拼出无数种机器人。 编程里大量重复劳动容易出错,自动化不光提速还能压误差。MQL5附带的模块生成器能在几秒内产出最多8个挂到同一个多模块项目里的文件,开发和组装成本直接降一截。 生成器面板里设好模块组合,比如建一个叫"test"的项目,代码会自动剔除多余板块和文件,主文件命名规则是项目名加"_module_exp.mq5"后缀。 生成的文件落点在Files文件夹:主模块test_module_exp.mq5要丢进Experts,其余外部模块文件进Indicators\Market。之后全编译就能测。手工搭同样项目极耗时,正确顺序是先写主模块,等外部模块定义清楚再接进来。 关键坑在主模块读取辅助模块输出:指标缓冲区全是double实数,必须自己写从实数到目标类型的转换。外部模块最好设计成无输入参数也能被主模块调用,默认值机制能大幅简化数据管理。外汇和贵金属杠杆高,多模块EA回测通过也不代表实盘能扛住滑点和点差。
◍ 按净值百分比算开仓手数
做模块化 EA 时,资金管理常拆成独立模块,核心任务就一件:根据账户当前权益算出这单该下多少手。最朴素的写法是以净值的可用百分比为基准,避免手动写死手数带来的爆仓隐患。 若不传入参数,模块默认取净值的 0.1% 作为下单额度。以 1 万美金净值算,单次风险敞口约对应 10 美金权益占用,实际手数由经纪商合约规格换算——外汇与贵金属杠杆高,0.1% 也可能在极端波动下产生明显浮亏,需自行核对品种杠杆。 下面这段是模块骨架(指标式封装),重点看 input 与权益读取部分:
class=class="str">"cmt">// 声明可调参数:每笔占净值百分比,缺省 class="num">0.1 class="kw">input class="type">class="kw">double lot_perc=class="num">0.1; class=class="str">"cmt">// 净值百分比值 class="type">class="kw">double Buffer1[]; class=class="str">"cmt">// 初始化:缓冲区按时间序列倒序,绑定绘图缓冲 class="type">int OnInit() { ArraySetAsSeries(Buffer1,true); SetIndexBuffer(class="num">0,Buffer1,INDICATOR_DATA); class="kw">return(INIT_SUCCEEDED); }; class=class="str">"cmt">// 主计算:取账户权益 class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">class="kw">datetime& time[], class="kw">const class="type">class="kw">double& open[], class="kw">const class="type">class="kw">double& high[], class="kw">const class="type">class="kw">double& low[], class="kw">const class="type">class="kw">double& close[], class="kw">const class="type">long& tick_volume[], class="kw">const class="type">long& volume[], class="kw">const class="type">int &spread[]) { class="type">class="kw">double Equity=AccountInfoDouble(ACCOUNT_EQUITY); class=class="str">"cmt">// 读取实时净值 class=class="str">"cmt">//--- 依据净值计算手数
AccountInfoDouble(ACCOUNT_EQUITY) 读到的权益乘 lot_perc/100,再除以每点价值与止损点数,就能在 MT5 里直接验算你当前的仓位尺度是否合理。
class=class="str">"cmt">//****** project(module MM): test_module_MM_ind.mq5 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 程序代码由模块化项目生成器生成 | class=class="str">"cmt">//| 版权所有 class="num">2010-class="num">2017, Sergey Pavlov(DC2008) | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2010-class="num">2017, Sergey Pavlov(DC2008)" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property link "class="num">1.00" class="macro">#class="kw">property link "多模块智能系统示例: 项目 test 资金管理模块" class=class="str">"cmt">//--- 在图表窗口中显示指标 class="macro">#class="kw">property indicator_chart_window class=class="str">"cmt">//--- 指标计算的缓冲区数量 class="macro">#class="kw">property indicator_buffers class="num">1 class=class="str">"cmt">//--- 指标中的图形序列数量 class="macro">#class="kw">property indicator_plots class="num">1 class=class="str">"cmt">//--- class="kw">input class="type">class="kw">double lot_perc=class="num">0.1; class=class="str">"cmt">// 净值百分比值 class="type">class="kw">double Buffer1[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- ArraySetAsSeries(Buffer1,true); SetIndexBuffer(class="num">0,Buffer1,INDICATOR_DATA); class="kw">return(INIT_SUCCEEDED); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标迭代函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">class="kw">datetime& time[], class="kw">const class="type">class="kw">double& open[], class="kw">const class="type">class="kw">double& high[], class="kw">const class="type">class="kw">double& low[], class="kw">const class="type">class="kw">double& close[], class="kw">const class="type">long& tick_volume[], class="kw">const class="type">long& volume[], class="kw">const class="type">int &spread[]) { class="type">class="kw">double Equity=AccountInfoDouble(ACCOUNT_EQUITY); class=class="str">"cmt">//--- 依据净值计算手数
手数计算与逐笔报价里的缓冲同步
EA 里动态手数常写在自定义指标缓冲里,再由主程序在 OnTick 取回。下面这段把净值乘百分比再除 1000 压缩成手数,并强制地板到 0.01,是微型账户常用的裸手数逻辑。 Buffer1[0]=NormalizeDouble(Equity*lot_perc/1000.0,2); 先把权益按 lot_perc 比例缩到两位小数点;若结果低于 0.01 则直接赋 0.01,避免下不了单。注意 1000.0 这个除数不是标准手/迷你手换算,而是作者按自己杠杆偏好定的缩放因子,换平台要重算。 OnTick 中若开了 on_lot 开关,会用 ArraySetAsSeries 把 buffer_m1 设为时间序列,再用 CopyBuffer(handle_m1,0,0,1,buffer_m1) 只拉最新一根到本地数组,随后 lot=buffer_m1[0] 完成手数接管。CopyBuffer 返回小于 0 就直接 return,防止句柄失效时乱下单。 外汇与贵金属保证金波动大,这类按权益浮动手数的写法在连环止损时可能放大回撤,上 MT5 用策略测试器改 lot_perc 与除数跑一遍最稳妥。
Buffer1[class="num">0]=NormalizeDouble(Equity*lot_perc/class="num">1000.0,class="num">2); class=class="str">"cmt">// 确定手数的函数 if(Buffer1[class="num">0]<class="num">0.01) Buffer1[class="num">0]=class="num">0.01; class="kw">return(rates_total); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统逐笔报价函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class="type">class="kw">double Equity=AccountInfoDouble(ACCOUNT_EQUITY); class=class="str">"cmt">//--- 模块 class="num">1 if(on_lot) { class=class="str">"cmt">// 如果有额外模块 class="type">class="kw">double buffer_m1[]; ArraySetAsSeries(buffer_m1,true); if(CopyBuffer(handle_m1,class="num">0,class="num">0,class="num">1,buffer_m1)<class="num">0) class="kw">return; lot=buffer_m1[class="num">0]; } ... }
「持仓跟踪的三块积木:静态止损止盈、尾随与盈亏平衡」
把止损(SL)和止盈(TP)拆成两个独立模块最实用,若强行塞进单个模块,两者的数值必须放进不同指标缓冲区,否则 EA 无法区分。示例里 SL/TP 直接写死为常数点数而非走 input 外部参数,目的就是演示「无输入模块」的裸实现,新手也能照抄。 OnTrade 是挂静态 SL/TP 的最佳位置——开仓动作一旦发生就立刻写入,不依赖行情跳动。但持仓浮盈后,更常见的是切到尾随止损:用常量距离跟着当前价格把止损往盈利方向推,这部分必须放进 OnTick,因为逐笔报价每次刷新都要重算止损价。 另一块是盈亏平衡模块:当价格离开仓价足够远,把止损挪到开仓价(或微利位),触发即零利润或预定利润平仓。它同样在 OnTick 里调用,和尾随逻辑共享「连续跟踪」的节奏。外汇与贵金属波动剧烈,这类自动移动止损在高杠杆下可能频繁触发,实盘前务必在 MT5 策略测试器用历史数据验证滑点影响。 下面这段是 SL 指标模块的骨架代码,重点看缓冲区声明与 OnInit 里的绑定:ArraySetAsSeries 把 Buffer1 设为时间序列,SetIndexBuffer 把它挂到 0 号绘图缓冲。 别把常量距离当万能参数 示例为省事把尾随距离、盈亏平衡距离都硬编码,但真实行情里黄金一跳 0.1 点和欧美 0.00001 点差一个数量级,直接抄常数大概率在 XAUUSD 上过早扫损。开 MT5 后第一件事应是把这些距离改成 input double 再跑回测。
class=class="str">"cmt">//****** project(module SL): test_module_SL_ind.mq5 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 程序代码由模块化项目生成器生成 | class=class="str">"cmt">//| 版权所有 class="num">2010-class="num">2017, Sergey Pavlov(DC2008) | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2010-class="num">2017, Sergey Pavlov(DC2008)" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property link "class="num">1.00" class="macro">#class="kw">property link "多模块智能系统示例: 项目 test 止损模块" class=class="str">"cmt">//--- 在图表窗口中显示指标 class="macro">#class="kw">property indicator_chart_window class=class="str">"cmt">//--- 指标计算的缓冲区数量 class="macro">#class="kw">property indicator_buffers class="num">1 class=class="str">"cmt">//--- 指标中的图形序列数量 class="macro">#class="kw">property indicator_plots class="num">1 class=class="str">"cmt">//--- class="type">class="kw">double Buffer1[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- ArraySetAsSeries(Buffer1,true); SetIndexBuffer(class="num">0,Buffer1,INDICATOR_DATA); class="kw">return(INIT_SUCCEEDED); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标迭代函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">class="kw">datetime& time[], class="kw">const class="type">class="kw">double& open[], class="kw">const class="type">class="kw">double& high[], class="kw">const class="type">class="kw">double& low[], class="kw">const class="type">class="kw">double& close[], class="kw">const class="type">long& tick_volume[], class="kw">const class="type">long& volume[],
◍ 止盈模块里写死 100 点止损的坑
上面这段是从一个模块化智能系统项目里拆出来的止盈模块代码片段,版权信息指向 2010–2017 年的 Sergey Pavlov,项目名 test_module_TP_ind。它名义上是 TP 模块,却在 OnCalculate 里直接把 Buffer1[0] 赋成固定值 100,注释写的是 SL in points——也就是说这个所谓止盈模块实际塞了一个写死的 100 点止损值进缓冲区。 代码里 OnInit 只做了两件事:ArraySetAsSeries(Buffer1,true) 把缓冲区按时间序列倒序排列,SetIndexBuffer(0,Buffer1,INDICATOR_DATA) 把它绑到图表主窗口画指标。indicator_buffers 和 indicator_plots 都设成 1,说明整个模块就输出这一条线。 你在 MT5 里加载这种模块时要警觉:它返回的 rates_total 只是告诉系统计算了这么多根 K 线,但 Buffer1 永远只有最新一根是 100、其余默认 0。若下游 EA 直接读这个缓冲区当止损距离, EURUSD 在 5 位平台下 100 点 = 10 个报价单位, XAUUSD 点值差异更大,硬套可能触发异常平仓。外汇和贵金属杠杆高,这类写死参数的模块务必先回测再上实盘。 别把生成器吐的代码当成品 模块化项目生成器产出的代码只是骨架,SL=100 这种常量得改成从输入参数读,否则换个品种就失真。开 MT5 把这段贴进自定义指标,编译后切到 M1 看 Buffer1 数值,确认除 0 号外是否全零,就能立刻验证上面说的逻辑。
class="kw">const class="type">int &spread[]) { class="type">class="kw">double SL=class="num">100; class=class="str">"cmt">// SL in points class=class="str">"cmt">//--- 计算止损 Buffer1[class="num">0]=SL; class="kw">return(rates_total); }; class=class="str">"cmt">//****** project(module TP): test_module_TP_ind.mq5 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 程序代码由模块化项目生成器生成 | class=class="str">"cmt">//| 版权所有 class="num">2010-class="num">2017, Sergey Pavlov(DC2008) | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2010-class="num">2017, Sergey Pavlov(DC2008)" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property link "class="num">1.00" class="macro">#class="kw">property link "多模块智能系统示例: 项目 test 止盈模块" class=class="str">"cmt">//--- 在图表窗口中显示指标 class="macro">#class="kw">property indicator_chart_window class=class="str">"cmt">//--- 指标计算的缓冲区数量 class="macro">#class="kw">property indicator_buffers class="num">1 class=class="str">"cmt">//--- 指标中的图形序列数量 class="macro">#class="kw">property indicator_plots class="num">1 class=class="str">"cmt">//--- class="type">class="kw">double Buffer1[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- ArraySetAsSeries(Buffer1,true); SetIndexBuffer(class="num">0,Buffer1,INDICATOR_DATA); class="kw">return(INIT_SUCCEEDED); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标迭代函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">class="kw">datetime& time[], class="kw">const class="type">class="kw">double& open[], class="kw">const class="type">class="kw">double& high[], class="kw">const class="type">class="kw">double& low[], class="kw">const class="type">class="kw">double& close[], class="kw">const class="type">long& tick_volume[], class="kw">const class="type">long& volume[], class="kw">const class="type">int &spread[]) {
用外部模块动态补挂止损止盈
当主指标同时挂了 SL、TP 两个外部模块句柄(on_SL && on_TP 为真),就可以在 OnTrade 里把它们的缓冲值读出来,给当前持仓补挂风控线。 代码先通过 CopyBuffer 拉取模块2、模块3的最新一根数值,分别作为 SL、TP 的点数距离;若读取失败直接 return,避免脏数据写入交易指令。 持仓修改只针对 SL 尚为 0 的仓位:买入仓以开仓价 +TP*_Point 设止盈、-SL*_Point 设止损;卖出仓方向相反。调用 trade.PositionModify 前用 NormalizeDouble 按品种 Digits 对齐报价精度。 外汇与贵金属杠杆高,补挂距离若直接取模块原始输出,遇到点差扩张可能瞬间触发;建议先在 MT5 策略测试器用 2023 年 XAUUSD 1H 跑一遍,确认 SL/TP 缓冲值落在 50~300 点区间再上模拟盘。
class="type">class="kw">double TP=class="num">100; class=class="str">"cmt">// TP in points class=class="str">"cmt">//--- 计算止盈 Buffer1[class="num">0]=TP; class="kw">return(rates_total); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 交易函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTrade() { if(on_SL && on_TP) class=class="str">"cmt">// 如果有额外模块 { class=class="str">"cmt">//--- 模块 class="num">2 class="type">class="kw">double buffer_m2[]; ArraySetAsSeries(buffer_m2,true); if(CopyBuffer(handle_m2,class="num">0,class="num">0,class="num">1,buffer_m2)<class="num">0) class="kw">return; class="type">class="kw">double SL=buffer_m2[class="num">0]; class=class="str">"cmt">//--- 模块 class="num">3 class="type">class="kw">double buffer_m3[]; ArraySetAsSeries(buffer_m3,true); if(CopyBuffer(handle_m3,class="num">0,class="num">0,class="num">1,buffer_m3)<class="num">0) class="kw">return; class="type">class="kw">double TP=buffer_m3[class="num">0]; class=class="str">"cmt">//--- 持仓修改 if(PositionSelect(_Symbol)) if(PositionGetDouble(POSITION_SL)==class="num">0) { class=class="str">"cmt">//--- 买入 if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY) { class="type">class="kw">double priceTP=PositionGetDouble(POSITION_PRICE_OPEN)+TP*_Point; class="type">class="kw">double priceSL=PositionGetDouble(POSITION_PRICE_OPEN)-SL*_Point; trade.PositionModify(_Symbol,NormalizeDouble(priceSL,Digits()),NormalizeDouble(priceTP,Digits())); } class=class="str">"cmt">//--- 卖出 if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL) { class="type">class="kw">double priceTP=PositionGetDouble(POSITION_PRICE_OPEN)-TP*_Point; class="type">class="kw">double priceSL=PositionGetDouble(POSITION_PRICE_OPEN)+SL*_Point; trade.PositionModify(_Symbol,NormalizeDouble(priceSL,Digits()),NormalizeDouble(priceTP,Digits())); } } } }
「尾随模块的缓冲区与点数硬编码」
这段指标代码来自一个多模块智能系统的尾随(trail)子模块,挂在主图窗口运行,只声明了 1 个绘图缓冲区和 1 个数据序列,结构极简。 初始化里先把 Buffer1 设为时间序列(ArraySetAsSeries 置 true),再绑定到 0 号指标缓冲区,返回 INIT_SUCCEEDED 即完成挂载。 OnCalculate 中直接写死 double TR=50,注释标明 Trail in points,并把 Buffer1[0]=TR 作为输出。也就是说,当前尾随距离被硬编码成 50 点,没有读取外部输入或自适应逻辑。 在 MT5 里把这段贴进自定义指标编译后,图表上只会看到一条恒定 50 点的参考线;做黄金或外汇品种时,50 点在不同报价精度下实际跳动距离差异很大,属于高风险参数,建议改由外部 input 传入再观察滑点表现。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 程序代码由模块化项目生成器生成 | class=class="str">"cmt">//| 版权所有 class="num">2010-class="num">2017, Sergey Pavlov(DC2008) | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2010-class="num">2017, Sergey Pavlov(DC2008)" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property link "class="num">1.00" class="macro">#class="kw">property link "多模块智能系统示例: 项目 test 尾随模块" class=class="str">"cmt">//--- 在图表窗口中显示指标 class="macro">#class="kw">property indicator_chart_window class=class="str">"cmt">//--- 指标计算的缓冲区数量 class="macro">#class="kw">property indicator_buffers class="num">1 class=class="str">"cmt">//--- 指标中的图形序列数量 class="macro">#class="kw">property indicator_plots class="num">1 class=class="str">"cmt">//--- class="type">class="kw">double Buffer1[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- ArraySetAsSeries(Buffer1,true); SetIndexBuffer(class="num">0,Buffer1,INDICATOR_DATA); class="kw">return(INIT_SUCCEEDED); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标迭代函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">class="kw">datetime& time[], class="kw">const class="type">class="kw">double& open[], class="kw">const class="type">class="kw">double& high[], class="kw">const class="type">class="kw">double& low[], class="kw">const class="type">class="kw">double& close[], class="kw">const class="type">long& tick_volume[], class="kw">const class="type">long& volume[], class="kw">const class="type">int &spread[]) { class="type">class="kw">double TR=class="num">50; class=class="str">"cmt">// Trail in points class=class="str">"cmt">//--- 计算尾随 Buffer1[class="num">0]=TR; class="kw">return(rates_total); }; class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 用指标缓冲做动态追踪止损
在 OnTick 里挂一个追踪模块,只有当 on_Trail 开关打开、当前品种有持仓且浮动盈利为正时才进入逻辑。很多人把 trailing stop 写死成固定点数,遇到黄金这种平均波幅动辄 200~300 点的一天,固定值要么被毛刺扫掉、要么放得太宽等于没跟。 这段代码从 handle_m4 指标句柄取 0 号缓冲的最新值 TR,把它当作以 _Point 为单位的追踪距离。买仓满足「当前价 - TR*_Point > 开仓价」才把止损抬到「当前价 - TR*_Point」之上;卖仓对称处理,且允许在原 SL 为 NULL 时直接落止损。 逐行看关键点:ArraySetAsSeries 把缓冲设为时间序列,CopyBuffer 只取 1 根柱若失败直接 return;PositionModify 的第二个参数用 NormalizeDouble 按品种 Digits 规整,第三参数传 0 表示不改止盈。外汇与贵金属杠杆高,这类自动改 SL 的逻辑若 TR 算错可能瞬间平掉盈利单,上 MT5 用策略测试器跑 EURUSD 与 XAUUSD 各 3 个月再实盘。
class="type">void OnTick() { ... class=class="str">"cmt">//--- 模块 class="num">4 if(on_Trail) if(PositionSelect(_Symbol)) if(PositionGetDouble(POSITION_PROFIT)>class="num">0) { class=class="str">"cmt">// 如果有额外模块 class="type">class="kw">double buffer_m4[]; ArraySetAsSeries(buffer_m4,true); if(CopyBuffer(handle_m4,class="num">0,class="num">0,class="num">1,buffer_m4)<class="num">0) class="kw">return; class="type">class="kw">double TR=buffer_m4[class="num">0]; if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY) if(PositionGetDouble(POSITION_PRICE_CURRENT)-TR*_Point>PositionGetDouble(POSITION_PRICE_OPEN)) { class="type">class="kw">double price_SL=PositionGetDouble(POSITION_PRICE_CURRENT)-TR*_Point; if(price_SL>PositionGetDouble(POSITION_SL)) trade.PositionModify(_Symbol,NormalizeDouble(price_SL,Digits()),class="num">0); } if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL) if(PositionGetDouble(POSITION_PRICE_CURRENT)+TR*_Point<PositionGetDouble(POSITION_PRICE_OPEN)) { class="type">class="kw">double price_SL=PositionGetDouble(POSITION_PRICE_CURRENT)+TR*_Point; if(price_SL<PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL)==NULL) trade.PositionModify(_Symbol,NormalizeDouble(price_SL,Digits()),class="num">0); } } ... }
盈亏平衡模块的缓冲区与点数硬编码
这段指标代码把盈亏平衡逻辑压到了最简单的一层:只开 1 个绘图缓冲,主图直接画一条线。 #property indicator_buffers 1 和 #property indicator_plots 1 决定它不占独立子窗口,和价格叠在一起看;ArraySetAsSeries(Buffer1,true) 让索引 0 永远指向当前柱,写策略时不用自己倒腾序列方向。 OnCalculate 里 Breakeven 被写死成 100 点,Buffer1[0]=Breakeven 每根 K 线都把这条水平线刷成同一数值。外汇和贵金属波动大、点值随杠杆和合约变,硬编码 100 点可能和实际保本位偏差明显,开 MT5 把这个数改成你的持仓成本点差之和再验证。 OnTick 中 if(on_Breakeven) 是模块 7 的开关位,说明保本动作只在开关打开时触发,实盘前要在外部参数里确认 on_Breakeven 默认状态,别让模块静默不干活。
class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2010-class="num">2017, Sergey Pavlov(DC2008)" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property link "class="num">1.00" class="macro">#class="kw">property link "多模块智能系统示例: 项目 test 盈亏平衡模块" class=class="str">"cmt">//--- 在图表窗口中显示指标 class="macro">#class="kw">property indicator_chart_window class=class="str">"cmt">//--- 指标计算的缓冲区数量 class="macro">#class="kw">property indicator_buffers class="num">1 class=class="str">"cmt">//--- 指标中的图形序列数量 class="macro">#class="kw">property indicator_plots class="num">1 class=class="str">"cmt">//--- class="type">class="kw">double Buffer1[]; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- ArraySetAsSeries(Buffer1,true); SetIndexBuffer(class="num">0,Buffer1,INDICATOR_DATA); class="kw">return(INIT_SUCCEEDED); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标迭代函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">class="kw">datetime& time[], class="kw">const class="type">class="kw">double& open[], class="kw">const class="type">class="kw">double& high[], class="kw">const class="type">class="kw">double& low[], class="kw">const class="type">class="kw">double& close[], class="kw">const class="type">long& tick_volume[], class="kw">const class="type">long& volume[], class="kw">const class="type">int &spread[]) { class="type">class="kw">double Breakeven=class="num">100; class=class="str">"cmt">// Breakeven in points class=class="str">"cmt">//--- 计算盈亏平衡点 Buffer1[class="num">0]=Breakeven; class="kw">return(rates_total); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 智能系统逐笔报价函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { ... class=class="str">"cmt">//--- 模块 class="num">7 if(on_Breakeven)
「盈利持仓用指标动态推止损」
当当前品种已有持仓且浮动盈利为正时,这段逻辑会尝试用外部指标 buffer 来动态收紧止损。它先取名为 m7 的指标零号缓冲的最新一根值 TRB,再按多空方向判断价格相对开仓价的偏移是否越过 TRB 个点。 多头情形下,若现价减去 TRB*_Point 仍高于开仓价,就把止损提到「现价 - 5*_Point」;仅当新止损严格大于原 SL 才调用 PositionModify,避免把止损往下放。 空头则对称处理:现价加 TRB*_Point 低于开仓价时,候选 SL = 现价 + 5*_Point,只要它低于原 SL 或原 SL 为 NULL 就改单。外汇与贵金属杠杆高,这类自动推损可能在快速回撤中频繁触发,上 MT5 用策略测试器跑 EURUSD 十五分钟周期验证触发频率再实盘。 下面逐行拆这段核心判断:先选中持仓、确认盈利,再拷 buffer;多空分支各自比较价格与开仓价,最后用 NormalizeDouble 按品种精度改 SL,TP 保持不变。
if(PositionSelect(_Symbol)) if(PositionGetDouble(POSITION_PROFIT)>class="num">0) { class=class="str">"cmt">// 如果有额外模块 class="type">class="kw">double buffer_m7[]; ArraySetAsSeries(buffer_m7,true); if(CopyBuffer(handle_m7,class="num">0,class="num">0,class="num">1,buffer_m7)<class="num">0) class="kw">return; class="type">class="kw">double TRB=buffer_m7[class="num">0]; if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY) if(PositionGetDouble(POSITION_PRICE_CURRENT)-TRB*_Point>PositionGetDouble(POSITION_PRICE_OPEN)) { class="type">class="kw">double price_SL=PositionGetDouble(POSITION_PRICE_CURRENT)-class="num">5*_Point; if(price_SL>PositionGetDouble(POSITION_SL)) trade.PositionModify(_Symbol,NormalizeDouble(price_SL,Digits()),PositionGetDouble(POSITION_TP)); } if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL) if(PositionGetDouble(POSITION_PRICE_CURRENT)+TRB*_Point<PositionGetDouble(POSITION_PRICE_OPEN)) { class="type">class="kw">double price_SL=PositionGetDouble(POSITION_PRICE_CURRENT)+class="num">5*_Point; if(price_SL<PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL)==NULL) trade.PositionModify(_Symbol,NormalizeDouble(price_SL,Digits()),PositionGetDouble(POSITION_TP)); } }
◍ 信号模块为何不能套同一组参数
交易信号模块往往是整个 EA 里最麻烦的一环,因为它要直接触发下单、平仓动作,而几乎所有指标都必须按具体品种重新适配。不存在哪套输入值能跨品种通用——EURUSD 上跑出 2.0 买入信号的布林带参数,放到 XAUUSD 上可能完全失效,外汇与贵金属的高波动特性会放大这种错配。 主程序绝不该自己顺手配信号模块。信号模块太多、参数乱挂,很可能把整个模块化项目拖垮。正确做法是:先离线把信号指标调好,再接进正式项目;多模块 EA 的优化我们另章再拆。 信号模块往缓冲区写三类定值:2.0 代表买入信号形成,0.0 代表无信号,-2.0 代表卖出信号形成。主模块在指定函数里读这些数值即可,例如通过 iBands 句柄取 Buffer1 当前值判断方向。 策略文档得先写清楚,信号开发人员照着技术需求走。不同策略信号逻辑天差地别,不约束的话模块之间会互相打架。
class=class="str">"cmt">//****** project(module signals): test_module_signals_ind.mq5 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 程序代码由模块化项目生成器生成 | class=class="str">"cmt">//| 版权所有 class="num">2010-class="num">2017, Sergey Pavlov(DC2008) | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2010-class="num">2017, Sergey Pavlov(DC2008)" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property link "class="num">1.00" class="macro">#class="kw">property link "多模块智能系统示例: 项目 test 交易信号模块" class=class="str">"cmt">//--- 在图表窗口中显示指标 class="macro">#class="kw">property indicator_chart_window class=class="str">"cmt">//--- 指标计算的缓冲区数量 class="macro">#class="kw">property indicator_buffers class="num">1 class=class="str">"cmt">//--- 指标中的图形序列数量 class="macro">#class="kw">property indicator_plots class="num">1 class=class="str">"cmt">//--- class="type">class="kw">double Buffer1[]; class=class="str">"cmt">//--- 用于存储 iBands 指标句柄的变量 class="type">int handle_Bands; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { handle_Bands=iBands(_Symbol,_Period,class="num">20,class="num">0,class="num">3.5,PRICE_CLOSE); class=class="str">"cmt">//--- ArraySetAsSeries(Buffer1,true); SetIndexBuffer(class="num">0,Buffer1,INDICATOR_DATA); class="kw">return(INIT_SUCCEEDED); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标迭代函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">class="kw">datetime& time[], class="kw">const class="type">class="kw">double& open[], class="kw">const class="type">class="kw">double& high[], class="kw">const class="type">class="kw">double& low[], class="kw">const class="type">class="kw">double& close[], class="kw">const class="type">long& tick_volume[],
用布林带通道写穿破信号
下面这段自定义指标计算函数,把最高价和最低价数组设为倒序后,直接拿当前 K 线去碰布林带上下轨。high[0] 大于 UpperBuffer[0] 时给 signal 赋 -2.0,low[0] 小于 LowerBuffer[0] 时给 2.0,等于把「上轨上方偏空、下轨下方偏多」写死进 Buffer1[0]。 代码里 CopyBuffer 只拉 1 根柱(第三参数 0、第四参数 1),说明信号只看最新一根,不做历史回看。外汇与贵金属杠杆高,这种通道外穿信号在震荡市里假突破概率偏大,实盘前建议在 MT5 用 EURUSD 的 M15 跑一遍看信号频率。 交易信号生成器 Buy_or_Sell 里还留了两个可插拔模块:模块 5 读 handle_m5 的 buffer,小于 -1 开卖、大于 1 开买;模块 6 读 handle_m6 的 buffer 当 lot 过滤,buffer 小于 1 禁买、大于 -1 禁卖。两个模块都由 on_signals / on_Filter 开关控制,不打开就只跑后面的布林带中轨上下轨缓存。
class="kw">const class="type">long& volume[], class="kw">const class="type">int &spread[]) { class="type">class="kw">double signal=class="num">0.0; ArraySetAsSeries(high,true); ArraySetAsSeries(low,true); class=class="str">"cmt">//--- 指标缓存区 class="type">class="kw">double UpperBuffer[]; class="type">class="kw">double LowerBuffer[]; ArraySetAsSeries(UpperBuffer,true); CopyBuffer(handle_Bands,class="num">1,class="num">0,class="num">1,UpperBuffer); ArraySetAsSeries(LowerBuffer,true); CopyBuffer(handle_Bands,class="num">2,class="num">0,class="num">1,LowerBuffer); class=class="str">"cmt">//--- 计算交易信号 if(high[class="num">0]>UpperBuffer[class="num">0]) signal=-class="num">2.0; if(low[class="num">0]<LowerBuffer[class="num">0]) signal=class="num">2.0; Buffer1[class="num">0]=signal; class="kw">return(rates_total); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 交易信号生成器 | class=class="str">"cmt">//+------------------------------------------------------------------+ sSignal Buy_or_Sell() { sSignal res={class="kw">false,class="kw">false}; class=class="str">"cmt">//--- 模块 class="num">5 if(on_signals) { class=class="str">"cmt">// 如果有额外模块 class="type">class="kw">double buffer_m5[]; ArraySetAsSeries(buffer_m5,true); if(CopyBuffer(handle_m5,class="num">0,class="num">0,class="num">1,buffer_m5)<class="num">0) class="kw">return(res); if(buffer_m5[class="num">0]<-class="num">1) res.Sell=true; if(buffer_m5[class="num">0]>class="num">1) res.Buy=true; } class=class="str">"cmt">//--- 模块 class="num">6 if(on_Filter) { class=class="str">"cmt">// 如果有额外模块 class="type">class="kw">double buffer_m6[]; ArraySetAsSeries(buffer_m6,true); if(CopyBuffer(handle_m6,class="num">0,class="num">0,class="num">1,buffer_m6)<class="num">0) class="kw">return(res); lot=buffer_m6[class="num">0]; if(buffer_m6[class="num">0]<class="num">1) res.Buy=class="kw">false; if(buffer_m6[class="num">0]>-class="num">1) res.Sell=class="kw">false; } class=class="str">"cmt">//--- class=class="str">"cmt">//--- 指标缓存区 class="type">class="kw">double UpperBuffer[]; class="type">class="kw">double LowerBuffer[]; class="type">class="kw">double MiddleBuffer[]; ArraySetAsSeries(MiddleBuffer,true); CopyBuffer(handle_Bands,class="num">0,class="num">0,class="num">1,MiddleBuffer); ArraySetAsSeries(UpperBuffer,true); CopyBuffer(handle_Bands,class="num">1,class="num">0,class="num">1,UpperBuffer);
「用布林带通道边界卡突破信号」
在 MT5 自定义指标里,把 Bollinger Bands 的下轨缓冲设为时间序列后,只取最新 1 根的值就能拿到实时下轨:ArraySetAsSeries(LowerBuffer,true) 配合 CopyBuffer(handle_Bands,2,0,1,LowerBuffer) 拿到的 LowerBuffer[0] 就是当前下轨。 同样手法拉出当前 K 线的高低:CopyLow 和 CopyHigh 各取 1 根,L[0]、H[0] 即最新低价与高价。注意 CopyBuffer 的索引 2 对应下轨、1 是中轨、0 是上轨,写错会直接反信号。 突破判定很直白:当最高价刺穿上轨且最低价仍在中轨之上,倾向判为卖压外溢(res.Sell=true);当最低价跌破下轨且最高价压在中轨之下,倾向判为买盘承接(res.Buy=true)。外汇与贵金属波动受杠杆放大,此类边界穿透仅为概率信号,实盘须自验。 下面这段是可直接贴进信号函数的核心片段,逐行看逻辑即可:先排好时间序列方向,再取带轨与高低,最后两个 if 出信号。
ArraySetAsSeries(LowerBuffer,true); CopyBuffer(handle_Bands,class="num">2,class="num">0,class="num">1,LowerBuffer); class=class="str">"cmt">//--- 时间序列 class="type">class="kw">double L[]; class="type">class="kw">double H[]; ArraySetAsSeries(L,true); CopyLow(_Symbol,_Period,class="num">0,class="num">1,L); ArraySetAsSeries(H,true); CopyHigh(_Symbol,_Period,class="num">0,class="num">1,H); if(H[class="num">0]>UpperBuffer[class="num">0]&& L[class="num">0]>MiddleBuffer[class="num">0]) res.Sell=true; if(L[class="num">0]<LowerBuffer[class="num">0] && H[class="num">0]<MiddleBuffer[class="num">0]) res.Buy=true; class=class="str">"cmt">//--- class="kw">return(res); }
◍ 把布林带做成信号过滤器
多模块 EA 里,信号过滤模块通常独立成一个指标单元,只负责吐出「过 / 不过」的判定,不直接下单。这样主逻辑和过滤条件解耦,换品种或调参数时不用动核心代码。 下面这段是用模块化生成器产出的过滤指标示例,底层拿 iBands 做通道判定。周期 35、偏移 0、标准差 4.1,比常规 20/2 的布林带宽得多,只在极端脱离均值时才可能触发过滤信号,适合剔除震荡里的毛刺单。外汇与贵金属杠杆高,这类宽通道过滤也只能降低噪音概率,不保证胜率。 代码里 OnInit 只建句柄和缓冲区,真正的过滤写在 OnCalculate 开头:先给 filtr 赋 0,后续再按布林上下轨位置改写。你可以直接把这段贴进 MT5 新建指标,把 4.1 改成 2.5 对比一下信号密度。
class=class="str">"cmt">//****** project(module Filter): test_module_Filter_ind.mq5 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 程序代码由模块化项目生成器生成 | class=class="str">"cmt">//| 版权所有 class="num">2010-class="num">2017, Sergey Pavlov(DC2008) | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2010-class="num">2017, Sergey Pavlov(DC2008)" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property link "class="num">1.00" class="macro">#class="kw">property link "多模块智能系统示例: 项目 test 过滤模块" class=class="str">"cmt">//--- 在图表窗口中显示指标 class="macro">#class="kw">property indicator_chart_window class=class="str">"cmt">//--- 指标计算的缓冲区数量 class="macro">#class="kw">property indicator_buffers class="num">1 class=class="str">"cmt">//--- 指标中的图形序列数量 class="macro">#class="kw">property indicator_plots class="num">1 class=class="str">"cmt">//--- class="type">class="kw">double Buffer1[]; class=class="str">"cmt">//--- 用于存储 iBands 指标句柄的变量 class="type">int handle_Bands; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { handle_Bands=iBands(_Symbol,_Period,class="num">35,class="num">0,class="num">4.1,PRICE_CLOSE); class=class="str">"cmt">//--- ArraySetAsSeries(Buffer1,true); SetIndexBuffer(class="num">0,Buffer1,INDICATOR_DATA); class="kw">return(INIT_SUCCEEDED); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标迭代函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(class="kw">const class="type">int rates_total, class="kw">const class="type">int prev_calculated, class="kw">const class="type">class="kw">datetime& time[], class="kw">const class="type">class="kw">double& open[], class="kw">const class="type">class="kw">double& high[], class="kw">const class="type">class="kw">double& low[], class="kw">const class="type">class="kw">double& close[], class="kw">const class="type">long& tick_volume[], class="kw">const class="type">long& volume[], class="kw">const class="type">int &spread[]) { class="type">class="kw">double filtr=class="num">0.0;
用布林中轨给信号加一道滤网
把 high、low 以及指标缓存区 MiddleBuffer 都设为时间序列(ArraySetAsSeries 传 true),这样索引 0 永远是当前未平仓 K 线,回看历史只需加下标。 通过 CopyBuffer 把布林带句柄 handle_Bands 的 0 号缓冲(中轨)取 1 根到 MiddleBuffer。若当前最高价 high[0] 低于中轨,filtr 赋 2.0,视为偏空过滤;若当前最低价 low[0] 高于中轨,filtr 赋 -2.0,视为偏多过滤。 最终 Buffer1[0]=filtr 并 return(rates_total)。在 MT5 里把这段接进自定义指标 OnCalculate,能直接看到价格相对中轨的位置被量化为 ±2.0 的过滤值,外汇与贵金属波动剧烈,该数值仅作概率倾向参考,实操须自担高风险。
ArraySetAsSeries(high,true); ArraySetAsSeries(low,true); class=class="str">"cmt">//--- 指标缓存区 class="type">class="kw">double MiddleBuffer[]; ArraySetAsSeries(MiddleBuffer,true); CopyBuffer(handle_Bands,class="num">0,class="num">0,class="num">1,MiddleBuffer); class=class="str">"cmt">//--- 过滤计算 if(high[class="num">0]<MiddleBuffer[class="num">0]) filtr=class="num">2.0; if(low[class="num">0]>MiddleBuffer[class="num">0]) filtr=-class="num">2.0; Buffer1[class="num">0]=filtr; class="kw">return(rates_total); };
「把信号模块做成自优化指标」
多模块 EA 里,外部信号模块的参数若不在主模块暴露,策略测试器里就只能手动离散试值,效率低且容易跑偏。折中办法是把外部模块写成「自优化指标」:让指标在加载时用最差成交假设把自己调一遍,再把跑出最大盈利的那组参数固化下来供 EA 调用。 思路来自用蜡烛极值当虚拟成交价——买按最高价、卖按最低价,故意吃最差滑点,这样同段历史上调出的参数,实盘大概率不会比回测差。但外汇和贵金属波动剧烈,任何优化结果都不保证实盘盈利,高杠杆下仍可能快速回撤。 原布林带边界反向策略可换成轨道线指标:以均线为轴、固定距离画等距通道。优化对象选两个——MA 周期和边界位移。示例里 MA 周期扫 10–100(步幅 10),位移扫 1000–10000(步幅 1000),全量穷举。 优化在指标内同步跑,会占用计算时间,EA 那段时间不下单。若是 24 小时轮动的模块机器人,这点延迟对总持仓时长影响有限,可接受。 下面这段代码把优化结果塞进结构体,OnInit 里清零 optimum 标志,等计算阶段跑完再置位,EA 读 Buffer1 前应先判 optimum。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//| 版权所有 class="num">2018, Sergey Pavlov(DC2008) | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "版权所有 class="num">2018, Sergey Pavlov(DC2008)" class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property link "class="num">1.00" class="macro">#class="kw">property link "多模块智能系统示例: 项目 test 交易信号模块" class=class="str">"cmt">//--- class="macro">#include <MovingAverages.mqh> class=class="str">"cmt">//--- 在图表窗口中显示指标 class="macro">#class="kw">property indicator_chart_window class=class="str">"cmt">//--- 指标计算的缓冲区数量 class="macro">#class="kw">property indicator_buffers class="num">1 class=class="str">"cmt">//--- 指标中的图形序列数量 class="macro">#class="kw">property indicator_plots class="num">1 class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 优化结果结构 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="kw">struct Opt { class="type">int var1; class=class="str">"cmt">// 参数 class="num">1 优化值 class="type">int var2; class=class="str">"cmt">// 参数 class="num">2 优化值 class="type">class="kw">double profit; class=class="str">"cmt">// 盈利 }; class=class="str">"cmt">//--- class="type">class="kw">double Buffer1[]; class="type">bool optimum=class="kw">false; Opt test={NULL,NULL,NULL}; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { ArraySetAsSeries(Buffer1,true); SetIndexBuffer(class="num">0,Buffer1,INDICATOR_DATA); optimum=class="kw">false; class="kw">return(INIT_SUCCEEDED); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标迭代函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(class="kw">const class="type">int rates_total,
◍ 网格间距与均线周期的双层遍历
这段函数入口先把 OHLC、成交量、点差数组一并接进来,prevcalculated 用来判断是不是初次计算。signal 初始化为 0 并写进 Buffer1[0],意味着指标主缓冲首根默认无信号。 当 optimum 开关为 false 时,代码进入一套暴力参数扫描:外层 d 从 1000 到 10000 步长 1000,对应网格偏移 shift=d_Point;内层 j 从 10 到 100 步长 10,即均线周期。总共要跑 10×10=100 组组合,每组再对 count 根 K 线回测,计算量随品种历史长度线性放大。 回测里用 SimpleMA(i,j,close) 取均线,sell 线挂在 ma+shift、buy 线挂在 ma-shift。价格触 buy 且空仓时,以 high[i]+spread[i]_Point 模拟成交价,并把上一次卖单利润累加进 profit。外汇与贵金属杠杆高,这种偏移成交假设未计滑点,实盘表现可能偏离回测。 别把回测利润当实盘上限。spread 用的是 int 数组直接乘 _Point,若遇零点差平台或休市异常值,price_buy 可能失真,建议先打印几组 shift/j 的 profit 分布再决定要不要开 optimum。
class="kw">const class="type">int prev_calculated, class="kw">const class="type">class="kw">datetime &time[], class="kw">const class="type">class="kw">double &open[], class="kw">const class="type">class="kw">double &high[], class="kw">const class="type">class="kw">double &low[], class="kw">const class="type">class="kw">double &close[], class="kw">const class="type">long &tick_volume[], class="kw">const class="type">long &volume[], class="kw">const class="type">int &spread[]) { class="type">class="kw">double signal=class="num">0.0; Buffer1[class="num">0]=signal; class=class="str">"cmt">//--- 输入参数优化 if(!optimum) { ArraySetAsSeries(close,class="kw">false); class="type">int count=rates_total; class="type">int total=class="num">0; class="type">int total_profit=class="num">0; for(class="type">int d=class="num">1000;d<class="num">10001;d+=class="num">1000) for(class="type">int j=class="num">10;j<class="num">101;j+=class="num">10) { class="type">class="kw">double shift=d*_Point; class="type">bool open_buy=class="kw">false; class="type">bool open_sell=class="kw">false; class="type">class="kw">double price_buy=class="num">0; class="type">class="kw">double price_sell=class="num">0; class="type">class="kw">double profit=class="num">0; class="type">int order=class="num">0; for(class="type">int i=j+class="num">1;i<count;i++) { class="type">class="kw">double ma=SimpleMA(i,j,close); class="type">class="kw">double sell=ma+shift; class="type">class="kw">double buy=ma-shift; class=class="str">"cmt">//--- 买入 if(buy>close[i] && !open_buy) { price_buy=high[i]+spread[i]*_Point; if(order==class="num">0) profit=class="num">0; else profit+=price_sell-price_buy; order++; open_buy=true; open_sell=class="kw">false; }
用优化结果反推实时信号
优化跑完之后,代码会把 optimum 置为 true,并直接用 test.var1、test.var2 这组通关参数去算实时均线通道。注意这里只认 test.profit>0 才往下走,若优化全程没赚到,信号区根本不会赋值,Buffer1[0] 会停在上一根的值。 具体算价时,ma 取 SimpleMA(0, test.var1, close),sell 与 buy 分别等于 ma ± test.var2 * _Period。也就是说偏离宽度是按周期分钟数放大,不是固定点数——在 M15 上 test.var2=3 的通道宽度,会是 M1 上的 15 倍,调参时得把这个量纲记进脑子。 信号判定只有两行:close[0] 跌破 buy 给 2.0,升破 sell 给 -2.0。数值用 ±2.0 而不是 ±1.0,只是为了和指标缓冲区里「无信号=0」拉开层级,方便小布盯盘层做阈值过滤。外汇与贵金属杠杆高,这套信号仅作概率参考,实盘前务必在 MT5 用历史数据重跑一遍优化。
if(optimum) if(test.profit>class="num">0) { ArraySetAsSeries(close,true); class="type">class="kw">double ma=SimpleMA(class="num">0,test.var1,close); class="type">class="kw">double sell=ma+test.var2*_Period; class="type">class="kw">double buy=ma-test.var2*_Period; class=class="str">"cmt">//--- 计算交易信号 if(buy>close[class="num">0]) signal=class="num">2.0; if(sell<close[class="num">0]) signal=-class="num">2.0; } class=class="str">"cmt">//--- 指标缓存区 Buffer1[class="num">0]=signal; class="kw">return(rates_total); };
「记住这一条就够了」
模块化思路在 MT5 里确实跑得通:把信号、滤波、执行拆成独立外部模块,第三方团队能各自封装算法,代码版权也更好保护。附带的 Modular_project_generator.ex5(389.16 KB)可在所需配置下直接生成模块项目源码,想验证的人下载就能在 MT5 打开看结构。 信号类指标的自我优化仍是短板,尤其当模块用作信号源或滤波器时,参数自适应还没打通,这是后续要啃的硬骨头。外汇与贵金属杠杆高、滑点跳空频繁,模块再花哨也替代不了对风险的硬约束。 落地的第一步很简单:把 ZIP 里的 ex5 拖进 MT5,生成一套空模块框架,先改一个信号函数的阈值,跑一遍回测看看曲线怎么动。