利用指标实时优化智能交易系统·进阶篇
⚙️

利用指标实时优化智能交易系统·进阶篇

(2/3)· 当历史优化参数随行情漂移失效,让指标在实时线程里替 EA 跑出当下最优解

偏理论 第 2/3 篇
把 EA 丢进实盘就以为一劳永逸,是多数人的隐性亏损源。行情结构变了,旧参数 silently 退化,而你还在等月度手动重优化。本篇把测试器塞进指标线程,让重选参数变成每根柱线都可能发生的动作。

◍ 句柄获取失败就直接退初始化

在 MT5 自定义指标里,RSI、WPR、ADX 三个底层句柄必须在 OnInit 阶段拿到,任一返回 INVALID_HANDLE 就说明当前图表品种或周期不支持该调用,此时应打印错误码并 return(INIT_FAILED),否则后续CopyBuffer会全盘错乱。 下面这段是 WPR 与 ADX 的获取及失败处理,结构同 RSI:先 iWPR(Symbol(),PERIOD_CURRENT,WPRPeriod) 取句柄,若等于 INVALID_HANDLE 则 Print 出句柄值和 GetLastError(),再 return(INIT_FAILED)。ADX 同理用 iADX 并校验。 拿到三个句柄后,要把 9 个计算缓冲(概率、成交次数、信号、盈利因子等)通过 SetIndexBuffer(0~8,...) 映射到 INDICATOR_CALCULATIONS,并对每个缓冲和 rsi/wpr/adx 数组执行 ArraySetAsSeries(...,true) 让索引 0 对应最新 K 线。最后清掉 Deals 容器、复位 last_deal=0、设短名,返回 INIT_SUCCEEDED 才算初始化过关。外汇与贵金属品种波动大,句柄失败常发生在非常规周期切换时,建议手动切周期验证。

MQL5 / C++
  wpr_handle=iWPR(Symbol(),PERIOD_CURRENT,WPRPeriod);
  if(wpr_handle==INVALID_HANDLE)
   {
     Print("Test Indicator",": Failed to get WPR handle");
     Print("Handle = ",wpr_handle,"  error = ",GetLastError());
     class="kw">return(INIT_FAILED);
   }
class=class="str">"cmt">//--- 获取 ADX 指标句柄
  adx_handle=iADX(Symbol(),PERIOD_CURRENT,ADXPeriod);
  if(adx_handle==INVALID_HANDLE)
   {
     Print("Test Indicator",": Failed to get ADX handle");
     Print("Handle = ",adx_handle,"  error = ",GetLastError());
     class="kw">return(INIT_FAILED);
   }
class=class="str">"cmt">//--- 指标缓存区映射
  SetIndexBuffer(class="num">0,Buffer_Probability,INDICATOR_CALCULATIONS);
  SetIndexBuffer(class="num">1,Buffer_DealsCount,INDICATOR_CALCULATIONS);
  SetIndexBuffer(class="num">2,Buffer_TradeSignal,INDICATOR_CALCULATIONS);
  SetIndexBuffer(class="num">3,Buffer_ProfitFactor,INDICATOR_CALCULATIONS);
  SetIndexBuffer(class="num">4,Buffer_ProfitCount,INDICATOR_CALCULATIONS);
  SetIndexBuffer(class="num">5,Buffer_TakeProfit,INDICATOR_CALCULATIONS);
  SetIndexBuffer(class="num">6,Buffer_StopLoss,INDICATOR_CALCULATIONS);
  SetIndexBuffer(class="num">7,Buffer_DealsCountCurrent,INDICATOR_CALCULATIONS);
  SetIndexBuffer(class="num">8,Buffer_ProfitCountCurrent,INDICATOR_CALCULATIONS);
  ArraySetAsSeries(Buffer_Probability,true);
  ArraySetAsSeries(Buffer_ProfitFactor,true);
  ArraySetAsSeries(Buffer_TradeSignal,true);
  ArraySetAsSeries(Buffer_DealsCount,true);
  ArraySetAsSeries(Buffer_ProfitCount,true);
  ArraySetAsSeries(Buffer_TakeProfit,true);
  ArraySetAsSeries(Buffer_StopLoss,true);
  ArraySetAsSeries(Buffer_DealsCountCurrent,true);
  ArraySetAsSeries(Buffer_ProfitCountCurrent,true);
class=class="str">"cmt">//--- 
  ArraySetAsSeries(rsi,true);
  ArraySetAsSeries(wpr,true);
  ArraySetAsSeries(adx,true);
  Deals.Clear();
  last_deal=class="num">0;
class=class="str">"cmt">//---
  IndicatorSetString(INDICATOR_SHORTNAME,"Test Indicator");
class=class="str">"cmt">//---
  class="kw">return(INIT_SUCCEEDED);
}

指标缓冲拉取与OnCalculate的初始化边界

多指标协同计算时,第一步必须确认三个缓冲都拿到了数据。下面这段判断若 WPR、ADX、RSI 任意一个 CopyBuffer 返回值 ≤0,直接 return -1 终止,避免后续用空数组算出垃圾信号。 depth 随后被重定义为三个数组尺寸的最小值,这是防止越界的最朴素做法——实际跑下来,若某一指标 handle 在切换周期时失效,depth 可能从请求的 500 缩到 0,信号直接断流。 OnCalculate 里对 prev_calculated<=0 的分支做了全缓冲清零,并用 HistoryDepth 限制首次加载长度。注意 Buffer_TakeProfit 初始化乘的是 _Point 而非 _Digits 换算后的绝对价,意味着 TakeProfit 参数单位是点,黄金和欧美在同一参数下绝对距离差十几倍,外汇贵金属高风险,参数移植前先核对点值。 数组倒序循环从 total-3 起步而非 total-1,给指标滞后留了 3 根的余量,否则第 0 根用未定稿的 close 算概率会抖动。

MQL5 / C++
if(CopyBuffer(wpr_handle,MAIN_LINE,class="num">0,depth,wpr)<=class="num">0 || CopyBuffer(adx_handle,MAIN_LINE,class="num">0,depth,adx)<=class="num">0 || CopyBuffer(rsi_handle,MAIN_LINE,class="num">0,depth,rsi)<=class="num">0)
      class="kw">return -class="num">1;
   depth=MathMin(ArraySize(rsi),MathMin(ArraySize(wpr),ArraySize(adx)));
class=class="str">"cmt">//---
   class="kw">return depth;
   }
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=class="str">"cmt">//---
   class="type">int total=rates_total-prev_calculated;
   if(prev_calculated<=class="num">0)
     {
      total=fmin(total,HistoryDepth);
class=class="str">"cmt">//---
      ArrayInitialize(Buffer_Probability,class="num">0);
      ArrayInitialize(Buffer_ProfitFactor,class="num">0);
      ArrayInitialize(Buffer_TradeSignal,class="num">0);
      ArrayInitialize(Buffer_DealsCount,class="num">0);
      ArrayInitialize(Buffer_ProfitCount,class="num">0);
      ArrayInitialize(Buffer_TakeProfit,TakeProfit*_Point);
      ArrayInitialize(Buffer_StopLoss,StopLoss*_Point);
      ArrayInitialize(Buffer_DealsCountCurrent,class="num">0);
      ArrayInitialize(Buffer_ProfitCountCurrent,class="num">0);
     }
   if(total>class="num">0)
     {
      total=MathMin(GetIndValue(total+class="num">2),rates_total);
      if(total<=class="num">0)
         class="kw">return prev_calculated;
      if(!ArraySetAsSeries(open,true) || !ArraySetAsSeries(high,true) || !ArraySetAsSeries(low,true) || !ArraySetAsSeries(close,true)
       || !ArraySetAsSeries(time,true) || !ArraySetAsSeries(spread,true))
         class="kw">return prev_calculated;
      for(class="type">int i=total-class="num">3;i>=class="num">0;i--)
        {
         Buffer_TakeProfit[i]=TakeProfit*_Point;

「回测引擎里的挂单与跨周期撮合」

这段逻辑干的事很直接:在非 M1 周期上,用历史 K 线的 OHLC 去回放已生成的虚拟订单。先给缓冲区写止损距离(StopLoss*_Point)并把成交与盈利计数清零,只有当前 bar 时间晚于上一次真实成交时才允许产生新信号。 买信号触发时,开盘价加了 spread[_Point] 作为入场,止损在下方 StopLoss 点、止盈在上方 TakeProfit 点;卖信号则不加 spread,直接以 open[i] 入场,止损在上、止盈在下。两种情况下都用 new CDeal 塞进 Deals 容器,并在 Buffer_TradeSignal 写 1 或 -1。 关键在周期对齐:若当前不是 PERIOD_M1(PeriodSeconds()!=60),就用 CopyRates 拉 M1 数据,从 time[i] 到下一根 bar 时间(或 TimeCurrent)逐根回放。CheckDeals 拿每根 M1 的 open/high/low/close 去判定平仓,closed 与 profit 累计后回流到缓冲区。 开 MT5 把这段塞进自定义指标,把 StopLoss、TakeProfit 设成输入参数,切到 H1 加载,就能看到 M1 撮合下的信号缓冲和理论平仓分布——外汇与贵金属杠杆高,回测盈利不代表实盘概率。

MQL5 / C++
Buffer_StopLoss[i]=StopLoss*_Point;
Buffer_DealsCount[i]=Buffer_DealsCountCurrent[i]=class="num">0;
Buffer_ProfitCount[i]=Buffer_ProfitCountCurrent[i]=class="num">0;
if(last_deal<time[i])
  {
  if(BuySignal(i))
    {
     class="type">class="kw">double open_price=open[i]+spread[i]*_Point;
     class="type">class="kw">double sl=open_price-StopLoss*_Point;
     class="type">class="kw">double tp=open_price+TakeProfit*_Point;
     CDeal *temp=new CDeal(_Symbol,rates_total-i,POSITION_TYPE_BUY,time[i],open_price,sl,tp);
     if(temp!=NULL)
        Deals.Add(temp);
     Buffer_TradeSignal[i]=class="num">1;
    }
  else class=class="str">"cmt">/*BuySignal*/
  if(SellSignal(i))
    {
     class="type">class="kw">double open_price=open[i];
     class="type">class="kw">double sl=open_price+StopLoss*_Point;
     class="type">class="kw">double tp=open_price-TakeProfit*_Point;
     CDeal *temp=new CDeal(_Symbol,rates_total-i,POSITION_TYPE_SELL,time[i],open_price,sl,tp);
     if(temp!=NULL)
        Deals.Add(temp);
     Buffer_TradeSignal[i]=-class="num">1;
    }
  else class=class="str">"cmt">/*SellSignal*/
     Buffer_TradeSignal[i]=class="num">0;
  }
if(Deals.Total()>class="num">0)
  {
  if(PeriodSeconds()!=class="num">60)
    {
     class="type">MqlRates rates[];
     class="type">int rat=CopyRates(_Symbol,PERIOD_M1,time[i],(i>class="num">0 ? time[i-class="num">1] : TimeCurrent()),rates);
     class="type">int closed=class="num">0, profit=class="num">0;
     for(class="type">int r=class="num">0;(r<rat && Deals.Total()>class="num">0);r++)
       {
       CheckDeals(rates[r].open,rates[r].high,rates[r].low,rates[r].close,rates[r].spread,rates[r].time,closed,profit);
       if(closed>class="num">0)

◍ 滚动窗口下的胜率与盈亏比递推

这段逻辑处理的是非 60 秒周期与 60 秒周期下,历史成交笔数、盈利笔数的滚动累加。若当前图表周期不是 1 分钟(PeriodSeconds()!=60),代码直接对每根 K 线调用 CheckDeals 把 closed、profit 计入当前柱的缓冲区;若是 1 分钟周期且 rat<0,则按方向分别补算并累加,保证不同时间框架下统计口径一致。 真正的关键在后面的滑动窗口归一。以 Buffer_DealsCount[i] 为例,它等于上一窗口值加上当前柱新增、再减去 HistoryDepth 根之前的柱贡献,用 NormalizeDouble(...,0) 取整,相当于一个长度为 HistoryDepth 的 deque 式计数,避免每根重算全历史。 胜率 Buffer_Probability 用了 EMA 式平滑:pr=2/(AveragePeriod-1),当样本数 ≥ AveragePeriod 时,当前胜率权重为 pr、前一柱胜率权重为 1-pr。若样本不足则直接用 Buffer_ProfitCount/Buffer_DealsCount*100 作裸值。 盈亏比 Buffer_ProfitFactor 在亏损笔数多于盈利笔数时才计算:temp=(盈利*TakeProfit)/(StopLoss*(总-盈利)),同样按 pr 做递推平滑。外汇与贵金属杠杆高,这类统计只反映历史样本倾向,实盘信号可能随点差跳变失效,开 MT5 把 HistoryDepth 与 AveragePeriod 调小到 50/20 可直观看缓冲区的递推抖动。

MQL5 / C++
{
       Buffer_DealsCountCurrent[i]+=closed;
       Buffer_ProfitCountCurrent[i]+=profit;
       }
     }
     if(rat<class="num">0)
       {
       CheckDeals(open[i],high[i],low[i],close[i],spread[i],time[i],closed,profit);
       Buffer_DealsCountCurrent[i]+=closed;
       Buffer_ProfitCountCurrent[i]+=profit;
       }
     }
     else class=class="str">"cmt">/* PeriodSeconds()!=class="num">60 */
       {
       class="type">int closed=class="num">0, profit=class="num">0;
       CheckDeals(open[i],high[i],low[i],close[i],spread[i],time[i],closed,profit);
       Buffer_DealsCountCurrent[i]+=closed;
       Buffer_ProfitCountCurrent[i]+=profit;
       }
     } class=class="str">"cmt">/* Deals.Total()>class="num">0 */
     Buffer_DealsCount[i+class="num">1]=NormalizeDouble(Buffer_DealsCount[i+class="num">2]+Buffer_DealsCountCurrent[i+class="num">1]-((i+HistoryDepth+class="num">1)<rates_total ? Buffer_DealsCountCurrent[i+HistoryDepth+class="num">1] : class="num">0),class="num">0);
     Buffer_ProfitCount[i+class="num">1]=NormalizeDouble(Buffer_ProfitCount[i+class="num">2]+Buffer_ProfitCountCurrent[i+class="num">1]-((i+HistoryDepth+class="num">1)<rates_total ? Buffer_ProfitCountCurrent[i+HistoryDepth+class="num">1] : class="num">0),class="num">0);
     Buffer_DealsCount[i]=NormalizeDouble(Buffer_DealsCount[i+class="num">1]+Buffer_DealsCountCurrent[i]-((i+HistoryDepth)<rates_total ? Buffer_DealsCountCurrent[i+HistoryDepth] : class="num">0),class="num">0);
     Buffer_ProfitCount[i]=NormalizeDouble(Buffer_ProfitCount[i+class="num">1]+Buffer_ProfitCountCurrent[i]-((i+HistoryDepth)<rates_total ? Buffer_ProfitCountCurrent[i+HistoryDepth] : class="num">0),class="num">0);
     if(Buffer_DealsCount[i]>class="num">0)
       {
       class="type">class="kw">double pr=class="num">2.0/(AveragePeriod-class="num">1.0);
       Buffer_Probability[i]=((i+class="num">1)<rates_total && Buffer_Probability[i+class="num">1]>class="num">0 && Buffer_DealsCount[i+class="num">1]>=AveragePeriod ? Buffer_ProfitCount[i]/Buffer_DealsCount[i]*class="num">100*pr+Buffer_Probability[i+class="num">1]*(class="num">1-pr) : Buffer_ProfitCount[i]/Buffer_DealsCount[i]*class="num">100);
       if(Buffer_DealsCount[i]>Buffer_ProfitCount[i])
         {
         class="type">class="kw">double temp=(Buffer_ProfitCount[i]*TakeProfit)/(StopLoss*(Buffer_DealsCount[i]-Buffer_ProfitCount[i]));
         Buffer_ProfitFactor[i]=((i+class="num">1)<rates_total && Buffer_ProfitFactor[i+class="num">1]>class="num">0 ? temp*pr+Buffer_ProfitFactor[i+class="num">1]*(class="num">1-pr) : temp);

逐笔成交的平仓判定逻辑

这段函数把历史挂着的模拟单按当前 K 线的 open/high/low/close 与 spread 逐一回放,决定哪些单在该根棒内被扫平。它返回 bool 表示整体执行是否干净,同时通过引用参数 closed 与 profit 把平仓总数和其中盈利单数交回上层,方便后续直接算胜率。 函数开头先清零 closed、profit 并把 result 置 true,随后用 for 循环遍历 Deals 容器里的每一笔。若发现指针失效就尝试 Delete 并回退下标,删不掉就把 result 改成 false 并继续;时间晚于当前 time 的单直接跳过,不参加本根判定。 核心判定分两路:先 deal.Tick(open,spread) 试平,命中就 closed++,利润为正则 profit++,然后删对象;不中再按多空类型走 switch。买单调 low 试止损、high 试止盈,卖单逻辑对称。每平一笔都做指针有效性复核再 delete,避免悬垂指针。 在 MT5 里把这段直接塞进自定义指标或 EA 的回测回路,把 Deals 换成你自己的持仓队列,就能看到某根棒实际扫掉几单、盈利几单——外汇与贵金属杠杆高,回测过关不代表实盘概率同分布,点差跳空都可能让 closed 统计偏离。

MQL5 / C++
class="type">bool CheckDeals(class="type">class="kw">double open,class="type">class="kw">double high,class="type">class="kw">double low,class="type">class="kw">double close,class="type">class="kw">double spread,class="type">class="kw">datetime time,class="type">int &closed, class="type">int &profit)
  {
   closed=class="num">0;
   profit=class="num">0;
   class="type">bool result=true;
   for(class="type">int i=class="num">0;i<Deals.Total();i++)
     {
      CDeal *deal=Deals.At(i);
      if(CheckPointer(deal)==POINTER_INVALID)
        {
         if(Deals.Delete(i))
            i--;
         else
            result=class="kw">false;
         class="kw">continue;
        }
      if(deal.GetTime()>time)
        class="kw">continue;
      if(deal.Tick(open,spread))
        {
         closed++;
         if(deal.GetProfit()>class="num">0)
            profit++;
         if(Deals.Delete(i))
            i--;
         if(CheckPointer(deal)!=POINTER_INVALID)
            class="kw">delete deal;
         class="kw">continue;
        }
      class="kw">switch(deal.Type())
        {
         case POSITION_TYPE_BUY:
           if(deal.Tick(low,spread))
             {
              closed++;
              if(deal.GetProfit()>class="num">0)
                 profit++;
              if(Deals.Delete(i))
                 i--;
              if(CheckPointer(deal)!=POINTER_INVALID)
                 class="kw">delete deal;
              class="kw">continue;
             }
           if(deal.Tick(high,spread))
             {
              closed++;
              if(deal.GetProfit()>class="num">0)
                 profit++;
              if(Deals.Delete(i))
                 i--;
              if(CheckPointer(deal)!=POINTER_INVALID)

「卖单平仓分支的指针回收细节」

上面这段是持仓遍历里针对 POSITION_TYPE_SELL 的处理尾段。逻辑上先尝试用 high 加 spread 去触碰止盈,命中就 closed++ 并统计 profit;没命中再用 low 加 spread 试一次,同样命中就计数。 每一处成功删除都先对 Deals 数组做 Delete(i) 并把 i 自减,避免漏检移位后的元素;随后用 CheckPointer(deal)!=POINTER_INVALID 判断对象有效性,再 delete deal 释放内存,最后 continue 跳到下一轮。 若两个 Tick 分支都没触发,则 break 退出 switch,由外层循环继续推进。这一写法在 MT5 回测中可将因悬空指针导致的异常概率压到接近零,建议你直接把这段贴进 EA 的持仓扫描函数里跑一遍 EURUSD 的 M15 历史数据验证。

MQL5 / C++
            class="kw">delete deal;
            class="kw">continue;
            }
            break;
            case POSITION_TYPE_SELL:
               if(deal.Tick(high,spread))
               {
                closed++;
                if(deal.GetProfit()>class="num">0)
                   profit++;
                if(Deals.Delete(i))
                   i--;
                if(CheckPointer(deal)!=POINTER_INVALID)
                   class="kw">delete deal;
                class="kw">continue;
               }
               if(deal.Tick(low,spread))
               {
                closed++;
                if(deal.GetProfit()>class="num">0)
                   profit++;
                if(Deals.Delete(i))
                   i--;
                if(CheckPointer(deal)!=POINTER_INVALID)
                   class="kw">delete deal;
                class="kw">continue;
               }
               break;
         }
   }
class=class="str">"cmt">//---
   class="kw">return result;
   }
把实时诊断交给小布盯盘
这些指标实例的盈利跟踪与参数漂移预警,小布盯盘的 AIGC 已内置,打开对应品种页即可看到线程占用和虚拟交易胜率,你只管决定切不切参数集。

常见问题

策略测试器每次从历史头重跑,且需程序化控制终端;指标实例只重算当前 tick,能在近乎实时下迭代参数,代价是单品种指标共线程的算力瓶颈。
在止损先检、再根据极值检止盈的假设下,偏差主要源于缺失低周期逐笔成交,概率上偏向保守估计,实盘需以经纪商报价复核。
用有意义参数步幅和逻辑过滤(如 MACD 最小步长)压缩实例数,并只在必要品种上部署,外汇贵金属高波动期尤需克制。
目前小布提供的是诊断视图而非托管指标实例,你可把本文指标逻辑接入后,在小布品种页看聚合信号,重复劳动交给小布,你专注决策。
不会,虚拟交易仅在指标计算缓冲内记账,不向经纪商发单,但需警惕线程阻塞导致主图 EA 事件延迟。