利用指标实时优化智能交易系统·综合运用
⚙️

利用指标实时优化智能交易系统·综合运用

(3/3)·从虚拟交易类到测试器指标,再用 EA 实时拣选最优参数,终结手动重优化的循环

新手友好 第 3/3 篇
很多人把 EA 跑起来就再也不碰参数,等曲线回撤才想起重优化,往往已经错过拐点。还有人以为策略测试器是唯一出路,不知道指标线程也能在近实时行情里替你跑通关测试。把重优化完全交给人工定时操作,本质是把不确定性留给了情绪和遗忘。

「把多指标参数扫描写进 EA 主框架」

EA 不像单一指标只盯一条线,它要在初始化阶段就把所有待扫参数的组合铺开。静态变量数量由输入参数固定,变化参数则给出起止值和步幅——这点跟策略测试器的优化面板思路一致,但 EA 内部用嵌套循环自己跑,能补上测试器漏掉的全局趋势干扰。 入场门槛必须前置声明:获利最小概率、最小盈利因子、最低成交数三者缺一不可。成交样本不够时统计意义薄弱,EA 会直接跳过该指标继续下一个,避免用小样本误导开仓。 OnInit 里先建交易类实例和指标句柄数组。循环初始化每个测试指标,下载失败立即返回 INIT_FAILED;成功才把句柄收进数组。全部就绪后再初始化交易类。 OnTick 开头两道闸:只认柱线开盘,且账户里已有持仓就退出。之后主循环逐个拉信号缓冲区,指标没重算或没信号就跳走;若收到与前面指标矛盾的信号,说明冲突概率走高,本根蜡烛不再动手。 信号筛选有细活:若某指标盈利概率和已查指标差小于 1%,就按盈利因子和盈利/风险比挑较优的通关测试存下来;明显更高时才去卡盈利因子需求。全部指标查完仍无重算就等下一 tick。末尾有信号才按最佳参数发单。 下面这段输入参数定义了手数、历史深度 500 根,以及 RSI/WPR 的周期与区域扫描范围,可直接贴进 MT5 看 EA 属性页如何映射。

MQL5 / C++
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,代表信号概率低于六成不下单。 外汇与贵金属杠杆高、滑点大,以上参数只是某套历史回测的入口,实盘前务必用策略测试器按品种重跑,别直接信默认值。

MQL5 / C++
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,若日志里句柄数等于各循环步长乘积,说明参数网格已完整生成。

MQL5 / C++
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 名与参数个数。

MQL5 / C++
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 附近跑一遍就能看到过滤效果。

MQL5 / C++
  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 到日志,确认过滤阈值符合预期再放开实盘。

MQL5 / C++
         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 在同一品种同一周期回测就能看见曲线分叉。 外汇与贵金属杠杆高、滑点跳空频繁,虚拟成交和实盘仍有差距,任何回测优势都只是概率倾向,别把对照结果当保本凭证。

让小布替你巡检指标实例
这些多实例指标的盈利跟踪和参数拣选逻辑,小布盯盘的 AIGC 已内置巡检视图,打开对应品种页即可看到哪些参数组合当前占优,你只管决定切不切。

常见问题

指标实例共用品种线程,按逐笔报价增量计算,不必每次重算整段历史,EA 只做决策调查,延迟更低但需注意实例总数以免终端减速。
为了降低计算资源占用,订单按开盘下单、止损优先再查极值止盈,是一种在实时性与精度间取平衡的假设,并非逐笔全真回放。
小布的品种页可接入你编译好的测试器指标实例,并汇总各参数组合的实时盈利曲线,省去自己开多个窗口比对。
外汇贵金属杠杆高、点差跳变频繁,实时逐笔报价下的止损触发可能与历史 M1 假设偏离,参数失效概率仍随行情结构变化而存在,需持续观察。
文末列了虚拟交易类、测试器指标和调度 EA 的源码,三者配合即可在终端内跑通近实时重优化闭环。