使用 SMA 和 EMA 自动优化止盈和指标参数的示例·进阶篇
🤖

使用 SMA 和 EMA 自动优化止盈和指标参数的示例·进阶篇

(2/3)· 当指标周期不再写死,EA 如何在 OnTick 里动态重算止盈与手数

偏理论 第 2/3 篇
多数自建 EA 把 SMA、EMA 周期写死在输入参数里,行情一变就钝化。本篇看一套在 tick 级动态重算指标与止盈的框架,把静态规则变成会呼吸的系统。

「用分类信号卡住均线过滤的进场口」

EA 把模型输出的 ExtPredictedClass 当成方向开关:等于 0 倾向多,等于 2 倾向空,其余值不动作。光有分类还不够,代码里再用收盘价同时跌破/突破 SMA 与 EMA 做二层确认,避免模型单帧噪声直接触发下单。 CopyRates 只取最新 1 根 K 线(参数 0,0,1),ArraySetAsSeries 置为时间序列后 rates[0] 即当前柱。若 copied<=0 直接 Print 错误并 return,这一步能帮你当场抓到品种不可交易或历史数据未加载的问题。 信号成立且 TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) 为真才进入发单分支。注意 sl、tp 初始化为 0,代表本段逻辑暂不挂止损止盈——实盘接黄金或欧美这类高波动品种前,你得自己补风控参数,否则裸奔进场概率上很被动。 让小布替你跑这套:把 ExtPredictedClass 打印到日志,观察模型判 0/2 时 close 与双均线的位置关系,能快速验证过滤器是不是太松。

MQL5 / C++
class="type">void CheckForOpen(class="type">class="kw">double sma, class="type">class="kw">double ema)class=class="str">"cmt">//, class="type">class="kw">double willr)
  {
   class="type">MqlRates rates[];
   ArraySetAsSeries(rates,true);
   class="type">int copied = CopyRates(_Symbol,class="num">0,class="num">0,class="num">1,rates);
   if(copied <= class="num">0)
     {
       Print("Error copying rates: ", GetLastError());
       class="kw">return;
     }
   class="type">class="kw">double Close[class="num">1];
   Close[class="num">0]=rates[class="num">0].close;
   class="type">class="kw">double close = Close[class="num">0];
   ENUM_ORDER_TYPE signal = WRONG_VALUE;
   Print("ExtPredictedClass ",ExtPredictedClass);
class=class="str">"cmt">//--- check signals
   if(ExtPredictedClass == class="num">2)class=class="str">"cmt">//PRICE_DOWN)
     {
       Print("ExtPredictedClass Sell ",ExtPredictedClass);
       Print("close ",close, " sma ",sma, " ema ", ema);
       class=class="str">"cmt">// Venta
       if((close < sma && close < ema))
         {
          signal = ORDER_TYPE_SELL;
          Print("Order Sell detected");
         }
     }
   else
     if(ExtPredictedClass == class="num">0)class=class="str">"cmt">//PRICE_UP)
       {
        Print("ExtPredictedClass Buy ",ExtPredictedClass);
        Print("close ",close, " sma ",sma, " ema ", ema);
        class=class="str">"cmt">// Compra
        if((close > sma && close > ema))
          {
            signal = ORDER_TYPE_BUY;
            Print("Order Buy detected");
          }
       }
class=class="str">"cmt">//--- open position if possible according to signal
   if(signal != WRONG_VALUE && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
     {
       Print("Proceding open order");
       class="type">class="kw">double price, sl=class="num">0, tp=class="num">0;
       class="type">class="kw">double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
       class="type">class="kw">double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
       class="type">MqlTradeRequest request = {};
       class="type">MqlTradeResult result = {};
       request.action = TRADE_ACTION_DEAL;
       request.symbol = _Symbol;
       request.deviation = Slippage;
       request.magic = MAGIC_SE;

卖单建仓里的 FOK 与 ATR 止损分支

这段逻辑只处理 signal 为 ORDER_TYPE_SELL 时的下单装配。先把成交方式钉死成 ORDER_FILLING_FOK,意味着挂出的卖单要么按指定价全量成交,要么直接废单,在流动性骤断的外汇/贵金属行情里能避免半成仓被动留缝,但也可能错过回补窗口。 手数走两条路:固定模式直接取 inp_lot_fix,动态模式交给 get_lot(price) 按账户风险算。CheckVolumeValue 不通过就 return,这是防止最小交易单位违规的最前闸门。 ATR 开关翻开时,止损放在 bid + StopLossATR*ATRValue,止盈放在 ask - TakeProfitATR*ATRValue,都用 NormalizeDouble 对齐 _Digits;同时 CheckMoneyForTrade 校验保证金,不够就打印西语提示并退出。若 ATR 关掉但代码里 !InpUseStops && ATR 仍为真,sl/tp 被置 0,否则走优化后的点值止损:sl=bid+InpStopLoss1*_Point,tp=ask-InpTakeProfit1*_Point。 下面这段是原文里卖单分支的 MQL5 片段,逐行拆完你能直接抄进 EA 的 OrderSend 前装配段。外汇与贵金属杠杆高,FOK 拒单率会随点差跳涨而上升,实盘前务必在 MT5 策略测试器用 Tick 级数据跑一遍。

MQL5 / C++
request.type_filling = ORDER_FILLING_FOK;
class=class="str">"cmt">//request.comment = "AKWr";
class="type">class="kw">double lotaje;
if(signal == ORDER_TYPE_SELL)
  {
   price = bid;
   Print("Price: ",price);
   if(inp_lot_type == LOT_TYPE_FIX)
     lotaje=inp_lot_fix ;
   else
     lotaje=get_lot(price);
   if(!CheckVolumeValue(lotaje))
    class="kw">return;
   if(!InpUseStops && ATR)
     {
     sl = NormalizeDouble(bid + StopLossATR * ATRValue, _Digits);
     tp = NormalizeDouble(ask - TakeProfitATR * ATRValue, _Digits);
     if(!CheckMoneyForTrade(_Symbol, lotaje,ORDER_TYPE_SELL))
       {
        Print("No hay suficiente margen para abrir la posición");
        class="kw">return;
       }
     request.type = ORDER_TYPE_SELL;
     request.price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
     request.volume = lotaje;
     request.sl = sl;
     request.tp = tp;
     request.comment = "SEW Opened sell order";
     }
   if(!InpUseStops && ATR)
     {
     sl = class="num">0;
     tp = class="num">0;
     }
   else
     {
     InpTakeProfit1 =optimizedTakeProfit;
     InpStopLoss1= optimizedStopLoss;
     sl = NormalizeDouble(bid + InpStopLoss1*_Point, _Digits);
     tp = NormalizeDouble(ask - InpTakeProfit1*_Point, _Digits);
     }
  }
else
  {
   price = ask;
   Print("Price: ",price);
   if(inp_lot_type == LOT_TYPE_FIX)
     lotaje=inp_lot_fix ;
   else
     lotaje=get_lot(price);
   if(!CheckVolumeValue(lotaje))
    class="kw">return;
   if(!InpUseStops)
     {
     sl = NormalizeDouble(ask - StopLossATR * ATRValue, _Digits);

◍ 止损止盈与下单前的保证金闸门

这段逻辑先把 ATR 倍数换算成目标价:tp 用 NormalizeDouble 把 bid 加上 TakeProfitATR*ATRValue 对齐到 _Digits 小数位,避免跨品种点数精度错配。若 InpUseStops 关闭但 ATR 生效,sl 和 tp 直接置 0,走无硬止损的通道;否则用优化后的 StopLoss、TakeProfit 乘 _Point 换算,买单 sl 挂在 ask 下方、tp 挂在 bid 上方。 每次发单前都跑 CheckMoneyForTrade 校验保证金,西班牙语报错“No hay suficiente margen”只是日志提示,真实拦截靠 return 中止。外汇与贵金属杠杆高,保证金不足会直接废单,不是滑点能解释的。 下单结构体 request 填完 type/price/volume/sl/tp/comment 后走 OrderSend,失败就 Print 出 GetLastError 码。开 MT5 把这段代码塞进 EA 测试前,先确认 InpUseStops 和 ATR 开关组合,不然 sl/tp 会被静默清零。

MQL5 / C++
tp = NormalizeDouble(bid + TakeProfitATR * ATRValue, _Digits);
if(!CheckMoneyForTrade(_Symbol, lotaje,ORDER_TYPE_BUY))
  {
   Print("No hay suficiente margen para abrir la posición");
   class="kw">return;
  }
request.type = ORDER_TYPE_BUY;
request.price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
request.volume = lotaje;
request.sl = sl;
request.tp = tp;
request.comment = "SEW Opened buy order";
 }
 if(!InpUseStops && ATR)
  {
   sl = class="num">0;
   tp = class="num">0;
  }
 else
  {
   InpTakeProfit1 =optimizedTakeProfit;
   InpStopLoss1= optimizedStopLoss;
   sl = NormalizeDouble(ask - InpStopLoss1*_Point, _Digits);
   tp = NormalizeDouble(bid + InpTakeProfit1*_Point, _Digits);
  }
   }
  Print("No InpUseStops used");
 class=class="str">"cmt">//ExtTrade.PositionOpen(_Symbol, signal, lotaje, price, sl, tp);
 if(!CheckMoneyForTrade(_Symbol, lotaje, (ENUM_ORDER_TYPE)signal))
  {
   Print("No hay suficiente margen para abrir la posición");
   class="kw">return;
  }
 Print("Volume ", lotaje);
 request.type = signal;
 request.price = price;class=class="str">"cmt">//SymbolInfoDouble(_Symbol, SYMBOL_ASK);
 request.volume = lotaje;
 request.sl = sl;
 request.tp = tp;
 request.comment = "SEW";
 if(!OrderSend(request, result))
  {
   Print("Error opening the order: ", GetLastError());
   class="kw">return;
  }
 }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check for close position conditions                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CheckForClose(class="type">class="kw">double sma, class="type">class="kw">double ema)class=class="str">"cmt">//, class="type">class="kw">double willr)
  {
  if(InpUseStops)
   class="kw">return;
  class="type">bool bsignal = false;
class=class="str">"cmt">//--- position already selected before

「用 ONNX 预测价反手平仓的实盘逻辑」

持仓反手信号来自模型对后一帧价格的分类。若持多单且预测类别为 2(价格下行),或持空单且预测类别为 0(价格上行),bsignal 置真,随后在终端允许交易的前提下平掉当前符号仓位并触发开仓检查。 PredictPrice 先把近 SAMPLE_SIZE 根收盘价做 min-max 归一化,再丢进 ONNX 推理。推理失败时 Print 报错并把 ExtPredictedClass 设为 -1,避免用脏数据交易。输出还原为真实价后,用 last_close 与 predicted 的差值判定 PRICE_UP / DOWN / SAME,阈值写死在 0.00001——外汇与贵金属波动小,这个精度在 5 位数报价下可能偏紧。 OptimizeParameters 用双层循环扫 TP、SL:tp 从 65 到 500、sl 从 65 到 500,步长均 5 点。也就是说总共约 88×88 ≈ 7744 次 TestStrategy 调用,EA 回测优化时这块最吃 CPU,建议先在策略测试器里缩小样本再跑。 小布盯盘接这套逻辑时,可把 ONNX 模型换成自己的轻量网络,但归一化上下界 ExtMin/ExtMax 必须和训练时一致,否则 Predicted Class 会系统性偏移。外汇贵金属杠杆高,信号仅作概率参考,实盘前务必用 MT5 历史数据复跑一遍优化区间。

MQL5 / C++
class="type">long type = PositionGetInteger(POSITION_TYPE);
class=class="str">"cmt">//--- check signals
  if(type == POSITION_TYPE_BUY && ExtPredictedClass == class="num">2)class=class="str">"cmt">//PRICE_DOWN)
     bsignal = true;
  if(type == POSITION_TYPE_SELL && ExtPredictedClass == class="num">0)class=class="str">"cmt">//PRICE_UP)
     bsignal = true;
class=class="str">"cmt">//--- close position if possible
  if(bsignal && TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
   {
     ExtTrade.PositionClose(_Symbol);
     CheckForOpen(sma, ema);class=class="str">"cmt">//, willr);
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Predict next price                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void PredictPrice(class="type">void)
  {
  class="kw">static vectorf output_data(class="num">1);
  class="kw">static vectorf x_norm(SAMPLE_SIZE);
  if(ExtMin >= ExtMax)
   {
     Print("ExtMin >= ExtMax");
     ExtPredictedClass = -class="num">1;
     class="kw">return;
   }
  if(!x_norm.CopyRates(_Symbol, _Period, COPY_RATES_CLOSE, class="num">1, SAMPLE_SIZE))
   {
     Print("CopyRates ", x_norm.Size());
     ExtPredictedClass = -class="num">1;
     class="kw">return;
   }
  class="type">float last_close = x_norm[SAMPLE_SIZE - class="num">1];
  x_norm -= ExtMin;
  x_norm /= (ExtMax - ExtMin);
  if(!OnnxRun(ExtHandle, ONNX_NO_CONVERSION, x_norm, output_data))
   {
     Print("OnnxRun");
     ExtPredictedClass = -class="num">1;
     class="kw">return;
   }
  class="type">float predicted = output_data[class="num">0] * (ExtMax - ExtMin) + ExtMin;
  class="type">float delta = last_close - predicted;
  if(fabs(delta) <= class="num">0.00001)
     ExtPredictedClass = PRICE_SAME;
  else
    if(delta < class="num">0)
       ExtPredictedClass = PRICE_UP;
    else
       ExtPredictedClass = PRICE_DOWN;
class=class="str">"cmt">// Debugging output
  Print("Predicted price: ", predicted, " Delta: ", delta, " Predicted Class: ", ExtPredictedClass);
  }
class="type">void OptimizeParameters()
  {
  class="type">class="kw">double bestTakeProfit = InpTakeProfit1;
  class="type">class="kw">double bestStopLoss = InpStopLoss1;
  class="type">class="kw">double bestPerformance = -DBL_MAX;
  for(class="type">int tp = class="num">65; tp <= class="num">500; tp += class="num">5) class=class="str">"cmt">// rango de TakeProfit
   {
     for(class="type">int sl = class="num">65; sl <= class="num">500; sl += class="num">5) class=class="str">"cmt">// rango de StopLoss
      {
      class="type">class="kw">double performance = TestStrategy(tp, sl);

把止损止盈和均线周期交给回测去挑

上面这段逻辑干的事很直接:在双层循环里挨个试 TakeProfit、StopLoss 组合,只要当前 performance 跑赢历史最优,就把对应的 tp、sl 记到 best 变量里。外层循环扫 tp,内层扫 sl,最后把胜出的数值写进 optimizedTakeProfit 与 optimizedStopLoss,并用 Print 打到日志,方便你进 MT5 终端的 Experts 标签核对。 指标周期也是同一套思路。OptimizeIndicators 先用 TimeCurrent 减去 LookbackPeriod 乘 H1 的秒数圈定回测窗口,再对 SMA 与 EMA 周期做嵌套遍历,调用 TestIndicatorPerformance 打分,保留 bestPerformance 最高的那对周期。外汇与贵金属波动受时段影响大,这种遍历式寻优只能给出历史样本里概率占优的参数,实盘仍可能失效,属高风险操作。 IsMarketClosed 用 TimeToStruct 拆出星期与小时,周末直接返回 true;若小时落在 after 到 before 的空白时段也判为休市,具体边界要按你盯的品种时区改。CheckVolumeValue 则拿 SymbolInfoDouble 取 SYMBOL_VOLUME_MIN,手数低于下限直接 false,避免报单被拒。 开 MT5 把这几段塞进 EA,先把 MinSMAPeriod、MaxEMAPeriod 和 after、before 按自己品种填好,跑一轮优化日志,看看打印出来的最优 SL/TP 和均线周期是否和肉眼看的支撑阻力对得上。

MQL5 / C++
  if(performance > bestPerformance)
    {
      bestPerformance = performance;
      bestTakeProfit = tp;
      bestStopLoss = sl;
      class=class="str">"cmt">//Print("Best Take Profit",bestTakeProfit);
      class=class="str">"cmt">//Print("Best Stop Loss",bestStopLoss);
    }
   }
  }
  optimizedTakeProfit = bestTakeProfit;
  optimizedStopLoss = bestStopLoss;
  Print("Optimized TakeProfit: ", optimizedTakeProfit);
  Print("Optimized StopLoss: ", optimizedStopLoss);
}
class="type">void OptimizeIndicators()
  {
   class="type">class="kw">datetime startTime = TimeCurrent() - LookbackPeriod * PeriodSeconds(PERIOD_H1);
   class="type">class="kw">datetime endTime = TimeCurrent();
   class="type">int bestSMAPeriod = SMA_Period;
   class="type">int bestEMAPeriod = EMA_Period;
   class="type">class="kw">double bestPerformance = -DBL_MAX;
   for(class="type">int smaPeriod = MinSMAPeriod; smaPeriod <= MaxSMAPeriod; smaPeriod++)
    {
     for(class="type">int emaPeriod = MinEMAPeriod; emaPeriod <= MaxEMAPeriod; emaPeriod++)
      {
       class="type">class="kw">double performance = TestIndicatorPerformance(smaPeriod, emaPeriod, startTime, endTime);
       if(performance > bestPerformance)
        {
         bestPerformance = performance;
         bestSMAPeriod = smaPeriod;
         bestEMAPeriod = emaPeriod;
        }
      }
    }
   SMA_Period = bestSMAPeriod;
   EMA_Period = bestEMAPeriod;
   Print("Optimized SMA Period: ", SMA_Period);
   Print("Optimized EMA Period: ", EMA_Period);
  }
class="type">bool IsMarketClosed()
  {
   class="type">class="kw">datetime currentTime = TimeCurrent();
   class="type">MqlDateTime tm;
   TimeToStruct(currentTime, tm);
   class="type">int dayOfWeek = tm.day_of_week;
   class="type">int hour = tm.hour;
class=class="str">"cmt">// Verifica si es fin de semana
   if(dayOfWeek <= Sunday || dayOfWeek >= Saturday)
    {
     class="kw">return true;
    }
class=class="str">"cmt">// Verifica si está fuera del horario habitual de mercado(ejemplo: class="num">21:class="num">00 a class="num">21:class="num">59 UTC)
   if(hour >= after || hour < before)  class=class="str">"cmt">// Ajusta estos valores según el horario del mercado
    {
     class="kw">return true;
    }
   class="kw">return false;
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Check the correctness of the order volume                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CheckVolumeValue(class="type">class="kw">double volume)class=class="str">"cmt">//,class="type">class="kw">string &description)
  {
class=class="str">"cmt">//--- minimal allowed volume for trade operations
   class="type">class="kw">double min_volume=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN);
   if(volume<min_volume)
    {
     class=class="str">"cmt">//description=StringFormat("Volume is less than the minimal allowed SYMBOL_VOLUME_MIN=%.2f",min_volume);
     class="kw">return(false);
    }
class=class="str">"cmt">//--- maximal allowed volume of trade operations
交给小布盯盘看品种波动
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时 ATR 与趋势强度,你拿它对照 EA 的动态调参是否跟上了节奏。

常见问题

取决于优化频率与样本长度,文中用定时触发而非每 tick 重算,概率上能控制开销在毫秒级。
需做样本外回测与滚动验证,外汇贵金属高风险,模型在震荡市可能频繁失效。
文中未显式算 ATR,而是复用指标句柄输出的波动估计,再乘系数映射为止盈距离。
目前小布提供的是诊断与看板,不托管 EA 运行;你可把 EA 输出的参数快照贴进品种页做比对。
模型与指标层解耦,换品种只需重训 ONNX 与重设手数上限,但跨市场流动性差异可能改变表现。