利用指标实时优化智能交易系统·综合运用
(3/3)·从虚拟交易类到测试器指标,再用 EA 实时拣选最优参数,终结手动重优化的循环
「把多指标参数扫描写进 EA 主框架」
EA 不像单一指标只盯一条线,它要在初始化阶段就把所有待扫参数的组合铺开。静态变量数量由输入参数固定,变化参数则给出起止值和步幅——这点跟策略测试器的优化面板思路一致,但 EA 内部用嵌套循环自己跑,能补上测试器漏掉的全局趋势干扰。 入场门槛必须前置声明:获利最小概率、最小盈利因子、最低成交数三者缺一不可。成交样本不够时统计意义薄弱,EA 会直接跳过该指标继续下一个,避免用小样本误导开仓。 OnInit 里先建交易类实例和指标句柄数组。循环初始化每个测试指标,下载失败立即返回 INIT_FAILED;成功才把句柄收进数组。全部就绪后再初始化交易类。 OnTick 开头两道闸:只认柱线开盘,且账户里已有持仓就退出。之后主循环逐个拉信号缓冲区,指标没重算或没信号就跳走;若收到与前面指标矛盾的信号,说明冲突概率走高,本根蜡烛不再动手。 信号筛选有细活:若某指标盈利概率和已查指标差小于 1%,就按盈利因子和盈利/风险比挑较优的通关测试存下来;明显更高时才去卡盈利因子需求。全部指标查完仍无重算就等下一 tick。末尾有信号才按最佳参数发单。 下面这段输入参数定义了手数、历史深度 500 根,以及 RSI/WPR 的周期与区域扫描范围,可直接贴进 MT5 看 EA 属性页如何映射。
class="kw">input class="type">class="kw">double Lot = class="num">0.01; class="kw">input class="type">int HistoryDepth = class="num">500; class=class="str">"cmt">//历史数据深度(柱线) class=class="str">"cmt">//--- RSI 指标参数 class="kw">input class="type">int RSIPeriod_Start = class="num">5; class=class="str">"cmt">//RSI 周期 class="kw">input class="type">int RSIPeriod_Stop = class="num">30; class=class="str">"cmt">//RSI 周期 class="kw">input class="type">int RSIPeriod_Step = class="num">5; class=class="str">"cmt">//RSI 周期 class=class="str">"cmt">//--- class="kw">input class="type">class="kw">double RSITradeZone_Start = class="num">30; class=class="str">"cmt">//起始超买/超卖区域大小 class="kw">input class="type">class="kw">double RSITradeZone_Stop = class="num">30; class=class="str">"cmt">//结束超买/超卖区域大小 class="kw">input class="type">class="kw">double RSITradeZone_Step = class="num">5; class=class="str">"cmt">//超买/超卖区域大小步幅 class=class="str">"cmt">//--- WPR 指标参数 class="kw">input class="type">int WPRPeriod_Start = class="num">5; class=class="str">"cmt">//WPR 起始周期 class="kw">input class="type">int WPRPeriod_Stop = class="num">30; class=class="str">"cmt">//WPR 结束周期 class="kw">input class="type">int WPRPeriod_Step = class="num">5; class=class="str">"cmt">//WPR 周期步幅 class=class="str">"cmt">//--- class="kw">input class="type">class="kw">double WPRTradeZone_Start = class="num">20; class=class="str">"cmt">//超买/超卖区域大小起始
◍ WPR 与 ADX 的参数边界怎么设
这段输入参数把两个过滤维度的扫描范围直接写死了,开 MT5 把 EA 挂上之前,先看清这几个 Start/Stop/Step 的含义:它们不是单值,而是给优化器用的遍历区间。 WPR 超买超卖区默认收在 20,步幅 5,意味着从 20 往下扫时会以 5 为单位缩窄极端区。ADX 周期从 5 到 30、步幅 5,配合横盘等级 ADX 起始/结束都锁在 40、步幅 10——也就是说,判定「横盘」的 ADX 阈值实际只跑 40 这一个值,不会随周期变。 止盈止损的 Start 和 Stop 相等(TP 600 / SL 200),步幅却分别给了 100,这种写法在优化器里等于不遍历,只是固定值占位。MinProbability 设 60.0,代表信号概率低于六成不下单。 外汇与贵金属杠杆高、滑点大,以上参数只是某套历史回测的入口,实盘前务必用策略测试器按品种重跑,别直接信默认值。
class="kw">input class="type">class="kw">double WPRTradeZone_Stop = class="num">20; class=class="str">"cmt">//超买/超卖区域大小结束 class="kw">input class="type">class="kw">double WPRTradeZone_Step = class="num">5; class=class="str">"cmt">//超买/超卖区域大小步幅 class=class="str">"cmt">//--- ADX 指标参数 class="kw">input class="type">int ADXPeriod_Start = class="num">5; class=class="str">"cmt">//ADX 周期起始 class="kw">input class="type">int ADXPeriod_Stop = class="num">30; class=class="str">"cmt">//ADX 周期结束 class="kw">input class="type">int ADXPeriod_Step = class="num">5; class=class="str">"cmt">//ADX 周期步幅 class=class="str">"cmt">//--- class="kw">input class="type">int ADXTradeZone_Start = class="num">40; class=class="str">"cmt">//横盘等级 ADX 起始 class="kw">input class="type">int ADXTradeZone_Stop = class="num">40; class=class="str">"cmt">//横盘等级 ADX 结束 class="kw">input class="type">int ADXTradeZone_Step = class="num">10; class=class="str">"cmt">//横盘等级 ADX 步幅 class=class="str">"cmt">//--- 成交设置 class="kw">input class="type">int TakeProfit_Start = class="num">600; class=class="str">"cmt">//止盈起始 class="kw">input class="type">int TakeProfit_Stop = class="num">600; class=class="str">"cmt">//止盈结束 class="kw">input class="type">int TakeProfit_Step = class="num">100; class=class="str">"cmt">//止盈步幅 class=class="str">"cmt">//--- class="kw">input class="type">int StopLoss_Start = class="num">200; class=class="str">"cmt">//止损起始 class="kw">input class="type">int StopLoss_Stop = class="num">200; class=class="str">"cmt">//止损结束 class="kw">input class="type">int StopLoss_Step = class="num">100; class=class="str">"cmt">//止损步幅 class=class="str">"cmt">//--- class="kw">input class="type">class="kw">double MinProbability = class="num">60.0; class=class="str">"cmt">//Minimal Probability
参数组合的穷举与指标句柄收集
做多指标参数寻优,第一步不是跑回测,而是先把参数空间铺开。下面这段代码用八层嵌套循环,把 RSI 周期、RSI 交易区、WPR 周期、WPR 交易区、ADX 周期、ADX 交易区、止盈、止损以及交易方向全部遍历一遍,每一个组合都通过 iCustom 向自定义指标 TestIndicator.ex5 申请一个句柄。 两个输入参数卡住了筛选门槛:MinProfitFactor 设为 1.6,意味着只保留盈利因子不低于 1.6 的组合;MinOrders 设为 10,要求历史成交至少 10 笔,样本太少的直接剔除。外汇和贵金属波动大、滑点不定,这两个阈值偏低时容易把噪声当信号,实盘前建议在 MT5 里先拉历史数据看分布。 句柄统一塞进 CArrayInt 类型的 ar_Handles,后续回测或实时判定直接按索引取用,不必重复创建。注意 iCustom 路径写的是 ::Indicators\\TestIndicator\\TestIndicator.ex5,指标必须提前编译进对应目录,否则 handle 会返回 INVALID_HANDLE。开 MT5 把这段贴进 EA 的 OnInit,若日志里句柄数等于各循环步长乘积,说明参数网格已完整生成。
class="kw">input class="type">class="kw">double MinProfitFactor = class="num">1.6; class=class="str">"cmt">//Minimal Profitfactor class="kw">input class="type">int MinOrders = class="num">10; class=class="str">"cmt">//历史当中最少成交数量 CArrayInt ar_Handles; CTrade Trade; class="type">int OnInit() { class=class="str">"cmt">//--- for(class="type">int rsi=RSIPeriod_Start;rsi<=RSIPeriod_Stop;rsi+=RSIPeriod_Step) for(class="type">class="kw">double rsi_tz=RSITradeZone_Start;rsi_tz<=RSITradeZone_Stop;rsi_tz+=RSITradeZone_Step) for(class="type">int wpr=WPRPeriod_Start;wpr<=WPRPeriod_Stop;wpr+=WPRPeriod_Step) for(class="type">class="kw">double wpr_tz=WPRTradeZone_Start;wpr_tz<=WPRTradeZone_Stop;wpr_tz+=WPRTradeZone_Step) for(class="type">int adx=ADXPeriod_Start;adx<=ADXPeriod_Stop;adx+=ADXPeriod_Step) for(class="type">class="kw">double adx_tz=ADXTradeZone_Start;adx_tz<=ADXTradeZone_Stop;adx_tz+=ADXTradeZone_Step) for(class="type">int tp=TakeProfit_Start;tp<=TakeProfit_Stop;tp+=TakeProfit_Step) for(class="type">int sl=StopLoss_Start;sl<=StopLoss_Stop;sl+=StopLoss_Step) for(class="type">int dir=class="num">0;dir<class="num">2;dir++) { class="type">int handle=iCustom(_Symbol,PERIOD_CURRENT,"::Indicators\\TestIndicator\\TestIndicator.ex5",HistoryDepth, sl, tp, rsi,
「指标句柄注册与初始化收尾」
这段片段处在 EA 初始化函数尾部,负责把多组指标句柄(rsi、rsi_tz、wpr、wpr_tz、adx、adx_tz、dir)一次性塞进自定义数组 ar_Handles,并做有效性拦截。 若 iCustom 返回的 handle 等于 INVALID_HANDLE,直接 return INIT_FAILED,避免后续 OnTick 读到空句柄导致异常平仓或漏单。 初始化收口处还有关键三行:Trade.SetAsyncMode(false) 强制同步下单,SetTypeFillingBySymbol(_Symbol) 按品种匹配成交模式,失败同样 INIT_FAILED;最后 SetMarginMode 与 return INIT_SUCCEEDED 才算跑通。 开 MT5 把这段接在你自己的指标创建循环后,若日志报 INIT_FAILED,优先查 _Symbol 是否支持该 iCustom 的 indicator 名与参数个数。
rsi_tz, wpr, wpr_tz, adx, adx_tz, dir); if(handle==INVALID_HANDLE) class="kw">return INIT_FAILED; ar_Handles.Add(handle); } Trade.SetAsyncMode(class="kw">false); if(!Trade.SetTypeFillingBySymbol(_Symbol)) class="kw">return INIT_FAILED; Trade.SetMarginMode(); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class="type">void OnTick() { class=class="str">"cmt">//--- class="kw">static class="type">class="kw">datetime last_bar=class="num">0;
◍ 多指标信号冲突时的跳过逻辑
EA 在每根 K 线只处理一次,用 SeriesInfoInteger 取当前柱收盘时间,若与记录的 last_bar 相同直接 return,避免重复计算。若当前品种已有持仓(PositionSelect 为真),同样标记 last_bar 后退出,不追单。 遍历指标句柄数组时,先取 buffer 2 的 signal 值:若已存在非零 signal 且本次 temp[0] 与之不同,说明多指标方向打架,直接标记 last_bar 并 return,这一根不再开仓。信号一致才继续往下读 buffer 1(订单数)、buffer 0(概率)、buffer 3(盈利因子)等。 概率与盈利因子都有最小阈值:temp[0] 需大于 MathMax(probability, MinProbability),盈利因子差 ≤0.01 时还要比 tp/sl 与 ind_tp/ind_sl 的比值,只保留更优盈亏比的参数。外汇与贵金属杠杆高,信号冲突时跳过才是降低回撤概率的实际手段,开 MT5 把 MinProbability 设 0.6 附近跑一遍就能看到过滤效果。
class="type">class="kw">datetime cur_bar=(class="type">class="kw">datetime)SeriesInfoInteger(_Symbol,PERIOD_CURRENT,SERIES_LASTBAR_DATE); if(cur_bar==last_bar) class="kw">return; if(PositionSelect(_Symbol)) { last_bar=cur_bar; class="kw">return; } class="type">int signal=class="num">0; class="type">class="kw">double probability=class="num">0; class="type">class="kw">double profit_factor=class="num">0; class="type">class="kw">double tp=class="num">0,sl=class="num">0; class="type">bool ind_caclulated=class="kw">false; class="type">class="kw">double temp[]; for(class="type">int i=class="num">0;i<ar_Handles.Total();i++) { if(CopyBuffer(ar_Handles.At(i),class="num">2,class="num">1,class="num">1,temp)<=class="num">0) class="kw">continue; ind_caclulated=true; if(temp[class="num">0]==class="num">0) class="kw">continue; if(signal!=class="num">0 && temp[class="num">0]!=signal) { last_bar=cur_bar; class="kw">return; } signal=(class="type">int)temp[class="num">0]; if(CopyBuffer(ar_Handles.At(i),class="num">1,class="num">1,class="num">1,temp)<=class="num">0 || temp[class="num">0]<MinOrders) class="kw">continue; if(CopyBuffer(ar_Handles.At(i),class="num">0,class="num">1,class="num">1,temp)<=class="num">0 || temp[class="num">0]<MathMax(probability,MinProbability)) class="kw">continue; if(MathAbs(temp[class="num">0]-probability)<=class="num">1) { class="type">class="kw">double ind_probability=temp[class="num">0]; class=class="str">"cmt">//--- if(CopyBuffer(ar_Handles.At(i),class="num">3,class="num">1,class="num">1,temp)<=class="num">0 || temp[class="num">0]<MathMax(profit_factor,MinProfitFactor)) class="kw">continue; class="type">class="kw">double ind_profit_factor=temp[class="num">0]; if(CopyBuffer(ar_Handles.At(i),class="num">5,class="num">1,class="num">1,temp)<=class="num">0) class="kw">continue; class="type">class="kw">double ind_tp=temp[class="num">0]; if(CopyBuffer(ar_Handles.At(i),class="num">6,class="num">1,class="num">1,temp)<=class="num">0) class="kw">continue; class="type">class="kw">double ind_sl=temp[class="num">0]; if(MathAbs(ind_profit_factor-profit_factor)<=class="num">0.01) { if(sl<=class="num">0 || tp/sl>=ind_tp/ind_sl)
信号落地前的最后一道过滤
这段逻辑处在实时优化器的收口位置:前面算出的概率、盈利因子、TP、SL 若任一为 0,或 TP/SL 小于等于 0,直接 return,不进场。外汇与贵金属杠杆高,这类硬过滤能挡掉大部分残缺信号,但挡不住行情跳空,实盘仍属高风险。 当 signal==1 时,用 SYMBOL_ASK 取买价,tp+=price、sl=price-sl,再 Trade.Buy 发单;signal==-1 则取 SYMBOL_BID,tp=price-tp、sl+=price 后 Trade.Sell。注意这里 TP/SL 在指标里存的是「点数距离」,代码在发单前才叠加现价,复制时别漏了这步换算。 ind_caclulated 为 false 也会直接 return,说明若本轮指标未算完,绝不拿旧值下单。开 MT5 把这段接在你自己的指标句柄循环后,先打印 probability / profit_factor 到日志,确认过滤阈值符合预期再放开实盘。
class="kw">continue; } class=class="str">"cmt">//--- probability=ind_probability; profit_factor=ind_profit_factor; tp=ind_tp; sl=ind_sl; } else class=class="str">"cmt">/* MathAbs(temp[class="num">0]-probability)<=class="num">1 */ { class="type">class="kw">double ind_probability=temp[class="num">0]; class=class="str">"cmt">//--- if(CopyBuffer(ar_Handles.At(i),class="num">3,class="num">1,class="num">1,temp)<=class="num">0 || temp[class="num">0]<MinProfitFactor) class="kw">continue; class="type">class="kw">double ind_profit_factor=temp[class="num">0]; if(CopyBuffer(ar_Handles.At(i),class="num">5,class="num">1,class="num">1,temp)<=class="num">0) class="kw">continue; class="type">class="kw">double ind_tp=temp[class="num">0]; if(CopyBuffer(ar_Handles.At(i),class="num">6,class="num">1,class="num">1,temp)<=class="num">0) class="kw">continue; class="type">class="kw">double ind_sl=temp[class="num">0]; probability=ind_probability; profit_factor=ind_profit_factor; tp=ind_tp; sl=ind_sl; } } if(!ind_caclulated) class="kw">return; last_bar=cur_bar; class=class="str">"cmt">//--- if(signal==class="num">0 || probability==class="num">0 || profit_factor==class="num">0 || tp<=class="num">0 || sl<=class="num">0) class="kw">return; if(signal==class="num">1) { class="type">class="kw">double price=SymbolInfoDouble(_Symbol,SYMBOL_ASK); tp+=price; sl=price-sl; Trade.Buy(Lot,_Symbol,price,sl,tp,"Real Time Optimizator"); } else if(signal==-class="num">1) { class="type">class="kw">double price=SymbolInfoDouble(_Symbol,SYMBOL_BID); tp=price-tp; sl+=price; Trade.Sell(Lot,_Symbol,price,sl,tp,"Real Time Optimizator"); } }
「把标准EA和新EA拉到同一条起跑线测」
想看清方法有没有用,最直白的办法是把标准 EA 和新 EA 放在完全一样的条件下跑。我们把标准 EA 的可调参数范围设成和新 EA 相似,测试时段也锁死,用前瞻模式做优化测试。自建的对照 EA 只挂一个测试指标,信号一出来就开仓,刻意不接统计过滤模块,结构跟前文 EA 基本一致,区别仅在这一块。 测试放在 H1 周期、2018 年共 7 个月的数据上,其中 1/3 用来给标准 EA 做前瞻验证。优化只动指标计算周期这一个参数,所有指标统一取 5 到 30、步长 5 的离散值。 结果很打脸:优化里能挤出薄利的参数,到了前瞻段直接转亏;整个分析区间里,标准 EA 没有一个通关参数真正赚到钱。图形叠加后能看出,价格走势结构变了,WPR 指标的盈利区在周期轴上发生了偏移。 换用本文方法开发的 EA,同样周期、同样参数框架,只把入场门槛钉死:最小盈利成交概率 60%、最小盈利因子 2,测试深度 500 根蜡烛。跑完分析期它是盈利的,实测算术盈利因子 1.66;可视测试时测试代理吃了 1250 MB 内存。外汇与贵金属品种波动剧烈,这类回测结论仅代表历史样本,实盘仍属高风险。
◍ 实时优化 EA 的算力门槛与策略延伸
实时优化 EA 的思路在测试中验证了可落地性,但有个硬约束常被低估:策略在盈利期拟合出的参数,进入行情结构切换后很可能快速失效并转亏。这不是代码 bug,而是在线重估本身的特性。 该方法对计算资源极度敏感。CPU 必须能在每个重算周期跑完所有已加载指标,内存则要常驻全部应用指标的状态数据;低于这个配置的机器,实时优化会直接卡成滞后信号。 顺着同一套解析入场为指标的技术,可以派生新策略——把入场条件本身写成可观测指标,再交给优化器在线挑参数,比传统写死条件的 EA 更适配漂移市。外汇与贵金属波动剧烈,这类高算力方案先在小资金账户验证再放大。
画得少,看得清
这套实时优化方案落地时,随文只带了四个文件:Deal.mqh 是虚拟成交类库,TestIndicator.mq5 做信号测试,RealTimeOptimization.mq5 是按本文方法跑的 EA,ClassicExpert.mq5 则留作标准优化对照。压缩包 MQL5.zip 体积 360.42 KB,在 MT5 里解压即可直接编译验证。 对比跑下来,实时优化 EA 与经典离线优化 EA 的差异主要在参数更新频率,而非信号逻辑本身;想确认效果,自己用这两份 EA 在同一品种同一周期回测就能看见曲线分叉。 外汇与贵金属杠杆高、滑点跳空频繁,虚拟成交和实盘仍有差距,任何回测优势都只是概率倾向,别把对照结果当保本凭证。