Summary: 完整的UT Bot ATR自适应追踪止损策略MT4 EA源码。基于ATR的动态止损、多空信号生成、趋势跟踪逻辑和警报系统。编译即用。




UT Bot ATR自适应追踪止损EA - 完整MQL4源码(2026版)



本文提供一个基于流行UT Bot Alerts策略的完整自动交易EA。该策略最初由HPotter开发,经QuantNomad在TradingView上推广后广受欢迎,原始指标获得超过110万次浏览和35,500次收藏,是有史以来最受欢迎的开源交易工具之一。

策略逻辑



UT Bot策略从根本上不同于传统的布林带或RSI等指标。它使用基于ATR的自适应追踪止损机制,动态响应市场波动率。与反向交易的均值回归策略不同,UT Bot通过识别价格突破追踪止损线的位置来跟随趋势。

算法工作原理



追踪止损在每根K线上使用递归的四分支逻辑:

  • <strong>上升趋势延续</strong>:当当前价格和前一价格都高于前一止损 → 止损只向上移动(棘轮机制锁定利润)


  • <strong>下降趋势延续</strong>:当当前价格和前一价格都低于前一止损 → 止损只向下移动


  • <strong>多头反转</strong>:当价格从下方向上穿越止损 → 止损重置为(价格 - nLoss)


  • <strong>空头反转</strong>:当价格从上方向下穿越止损 → 止损重置为(价格 + nLoss)


  • 距离计算公式为 nLoss = Key Value × ATR,其中ATR使用Wilder平滑法(RMA)。Key Value参数控制灵敏度:较低的值产生更多信号,较高的值产生更少但质量更高的信号。

    完整MQL4代码



    ``mql4
    //+------------------------------------------------------------------+
    //| UT_Bot_EA.mq4 |
    //| 自主编译 |
    //| Based on UT Bot Alerts |
    //+------------------------------------------------------------------+
    #property copyright "AI助手"
    #property link ""
    #property version "1.00"
    #property strict

    //--- 输入参数
    input double KeyValue = 1.0; // Key Value(ATR倍数)
    input int ATRPeriod = 10; // ATR周期(Wilder平滑)
    input double LotSize = 0.1; // 固定手数
    input double RiskPercent = 1.0; // 风险百分比(0=使用固定手数)
    input int StopLossPoints = 0; // 额外固定止损点数(0=使用追踪止损)
    input int TakeProfitPoints = 0; // 止盈点数(0=禁用)
    input int Slippage = 10; // 最大滑点
    input int MagicNumber = 202606; // EA魔术号
    input bool UseTrendFilter = true; // 使用EMA200趋势过滤
    input int TrendEMAPeriod = 200; // 趋势过滤EMA周期
    input int MaxSpread = 35; // 最大点差
    input bool UseHeikinAshi = false; // 使用Heikin Ashi收盘价
    input bool SendAlerts = true; // 信号时发送弹窗提醒
    input bool CloseOnOppositeSignal = true; // 反向信号时平仓反向单

    //--- 全局变量
    double trailingStop = 0;
    int trendDirection = 0; // 1=上升趋势, -1=下降趋势, 0=中性
    datetime lastSignalTime = 0;
    int pointMultiplier = 10;
    double lastBuySignalPrice = 0;
    double lastSellSignalPrice = 0;

    //--- ATR缓冲区
    double atrBuffer[];
    int atrCount = 0;
    bool isNewBar = false;

    //+------------------------------------------------------------------+
    //| EA初始化函数 |
    //+------------------------------------------------------------------+
    int OnInit()
    {
    // 检测平台报价格式
    if(Digits == 3 || Digits == 5)
    pointMultiplier = 10;
    else if(Digits == 2 || Digits == 4)
    pointMultiplier = 1;

    // 参数验证
    if(ATRPeriod < 2)
    {
    Print("错误: ATR周期至少为2");
    return(INIT_PARAMETERS_INCORRECT);
    }

    if(KeyValue <= 0)
    {
    Print("错误: Key Value必须大于0");
    return(INIT_PARAMETERS_INCORRECT);
    }

    // 初始化ATR缓冲区
    ArrayResize(atrBuffer, ATRPeriod + 2);

    Print("UT Bot EA初始化成功");
    Print("Key Value: ", KeyValue, " | ATR周期: ", ATRPeriod);

    return(INIT_SUCCEEDED);
    }

    //+------------------------------------------------------------------+
    //| EA反初始化函数 |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason)
    {
    Print("UT Bot EA已移除. 原因: ", reason);
    }

    //+------------------------------------------------------------------+
    //| 检测新K线 |
    //+------------------------------------------------------------------+
    bool IsNewBar()
    {
    static datetime lastBarTime = 0;
    datetime currentBarTime = iTime(Symbol(), 0, 0);

    if(currentBarTime != lastBarTime)
    {
    lastBarTime = currentBarTime;
    return true;
    }
    return false;
    }

    //+------------------------------------------------------------------+
    //| 获取收盘价(标准或Heikin Ashi) |
    //+------------------------------------------------------------------+
    double GetClosePrice(int shift)
    {
    if(UseHeikinAshi)
    {
    double haClose = (Open[shift] + High[shift] + Low[shift] + Close[shift]) / 4.0;
    return haClose;
    }
    return Close[shift];
    }

    //+------------------------------------------------------------------+
    //| 计算Wilder RMA平滑 |
    //+------------------------------------------------------------------+
    double WilderRMA(int period, int shift, double price)
    {
    static double prevRMA = 0;
    static int calcCount = 0;

    if(shift == 0 && prevRMA == 0)
    {
    double sum = 0;
    for(int i = 0; i < period; i++)
    sum += GetClosePrice(i);
    prevRMA = sum / period;
    calcCount = period;
    }

    if(shift < calcCount && shift > 0)
    {
    double sum = 0;
    for(int i = shift; i < shift + period; i++)
    sum += GetClosePrice(i);
    prevRMA = sum / period;
    }
    else if(shift < calcCount)
    {
    return prevRMA;
    }
    else
    {
    prevRMA = (price + (period - 1) prevRMA) / period;
    }

    return prevRMA;
    }

    //+------------------------------------------------------------------+
    //| 计算Wilder平滑ATR(RMA) |
    //+------------------------------------------------------------------+
    double CalculateWilderATR(int period, int shift)
    {
    if(period < 2 || shift + period >= Bars) return 0;

    double prevClose = iClose(Symbol(), 0, shift + 1);
    double high = iHigh(Symbol(), 0, shift);
    double low = iLow(Symbol(), 0, shift);

    double tr = MathMax(high, prevClose) - MathMin(low, prevClose);

    if(shift >= Bars - period - 1)
    return tr;

    double sumTR = 0;
    for(int i = shift; i < shift + period; i++)
    {
    double h = iHigh(Symbol(), 0, i);
    double l = iLow(Symbol(), 0, i);
    double pc = iClose(Symbol(), 0, i + 1);
    sumTR += MathMax(h, pc) - MathMin(l, pc);
    }
    double initialATR = sumTR / period;

    double rma = initialATR;
    for(int i = shift + period - 1; i >= shift; i--)
    {
    double h = iHigh(Symbol(), 0, i);
    double l = iLow(Symbol(), 0, i);
    double pc = iClose(Symbol(), 0, i + 1);
    double currentTR = MathMax(h, pc) - MathMin(l, pc);
    rma = (currentTR + (period - 1)
    rma) / period;
    }

    return rma;
    }

    //+------------------------------------------------------------------+
    //| 计算UT Bot追踪止损值 |
    //+------------------------------------------------------------------+
    double CalculateUTTrailingStop(int shift)
    {
    if(shift + ATRPeriod + 2 >= Bars) return 0;

    double atr = CalculateWilderATR(ATRPeriod, shift);
    if(atr <= 0) return 0;

    double nLoss = KeyValue atr;
    double closePrice = GetClosePrice(shift);
    double prevClose = GetClosePrice(shift + 1);

    double prevStop = 0;
    double stop = 0;

    if(trendDirection == 0)
    {
    stop = closePrice - nLoss;
    }
    else if(trendDirection == 1)
    {
    stop = closePrice - nLoss;
    if(stop < trailingStop && trailingStop > 0)
    stop = trailingStop;
    }
    else
    {
    stop = closePrice + nLoss;
    if(stop > trailingStop && trailingStop > 0)
    stop = trailingStop;
    }

    return stop;
    }

    //+------------------------------------------------------------------+
    //| 获取趋势方向(EMA过滤器) |
    //+------------------------------------------------------------------+
    int GetTrendDirection()
    {
    if(!UseTrendFilter)
    return 0;

    double emaValue = iMA(Symbol(), 0, TrendEMAPeriod, 0, MODE_EMA, PRICE_CLOSE, 0);
    double prevEMA = iMA(Symbol(), 0, TrendEMAPeriod, 0, MODE_EMA, PRICE_CLOSE, 1);

    if(Close[0] > emaValue)
    return 1;
    else if(Close[0] < emaValue)
    return -1;

    return 0;
    }

    //+------------------------------------------------------------------+
    //| 检查买入信号(价格向上穿越追踪止损) |
    //+------------------------------------------------------------------+
    bool IsBuySignal()
    {
    double currentStop = CalculateUTTrailingStop(0);
    double prevStop = CalculateUTTrailingStop(1);
    double currentClose = GetClosePrice(0);
    double prevClose = GetClosePrice(1);

    if(currentStop <= 0 || prevStop <= 0)
    return false;

    bool signal = (prevClose <= prevStop && currentClose > currentStop);

    if(signal && UseTrendFilter)
    {
    int trend = GetTrendDirection();
    if(trend != 1)
    return false;
    }

    if(signal && currentClose == lastBuySignalPrice && Time[0] == lastSignalTime)
    return false;

    if(signal)
    {
    lastBuySignalPrice = currentClose;
    lastSignalTime = Time[0];
    }

    return signal;
    }

    //+------------------------------------------------------------------+
    //| 检查卖出信号(价格向下穿越追踪止损) |
    //+------------------------------------------------------------------+
    bool IsSellSignal()
    {
    double currentStop = CalculateUTTrailingStop(0);
    double prevStop = CalculateUTTrailingStop(1);
    double currentClose = GetClosePrice(0);
    double prevClose = GetClosePrice(1);

    if(currentStop <= 0 || prevStop <= 0)
    return false;

    bool signal = (prevClose >= prevStop && currentClose < currentStop);

    if(signal && UseTrendFilter)
    {
    int trend = GetTrendDirection();
    if(trend != -1)
    return false;
    }

    if(signal && currentClose == lastSellSignalPrice && Time[0] == lastSignalTime)
    return false;

    if(signal)
    {
    lastSellSignalPrice = currentClose;
    lastSignalTime = Time[0];
    }

    return signal;
    }

    //+------------------------------------------------------------------+
    //| 基于风险百分比计算手数 |
    //+------------------------------------------------------------------+
    double CalculateLotSize()
    {
    if(RiskPercent <= 0)
    return LotSize;

    double accountBalance = AccountBalance();
    double riskAmount = accountBalance
    RiskPercent / 100.0;

    double stopDistance = StopLossPoints;
    if(stopDistance <= 0)
    {
    double atr = CalculateWilderATR(ATRPeriod, 0);
    stopDistance = atr / Point / pointMultiplier 1.5;
    if(stopDistance < 10) stopDistance = 10;
    }

    double tickValue = MarketInfo(Symbol(), MODE_TICKVALUE);
    double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP);

    if(tickValue <= 0 || lotStep <= 0)
    return LotSize;

    double calculatedLot = riskAmount / (stopDistance
    tickValue);
    calculatedLot = MathFloor(calculatedLot / lotStep) lotStep;

    double minLot = MarketInfo(Symbol(), MODE_MINLOT);
    double maxLot = MarketInfo(Symbol(), MODE_MAXLOT);

    if(calculatedLot < minLot) calculatedLot = minLot;
    if(calculatedLot > maxLot) calculatedLot = maxLot;

    return NormalizeDouble(calculatedLot, 2);
    }

    //+------------------------------------------------------------------+
    //| 开仓函数 |
    //+------------------------------------------------------------------+
    void OpenOrder(int cmd)
    {
    double price = (cmd == OP_BUY) ? Ask : Bid;
    double sl = 0, tp = 0;
    double lot = CalculateLotSize();

    if(StopLossPoints > 0)
    {
    if(cmd == OP_BUY)
    sl = price - StopLossPoints
    Point pointMultiplier;
    else
    sl = price + StopLossPoints
    Point pointMultiplier;
    }
    else
    {
    double atrStop = CalculateUTTrailingStop(0);
    if(atrStop > 0)
    {
    if(cmd == OP_BUY)
    sl = atrStop;
    else
    sl = atrStop;
    }
    }

    if(TakeProfitPoints > 0)
    {
    if(cmd == OP_BUY)
    tp = price + TakeProfitPoints
    Point pointMultiplier;
    else
    tp = price - TakeProfitPoints
    Point * pointMultiplier;
    }

    int ticket = OrderSend(Symbol(), cmd, lot, price, Slippage, sl, tp, "UT Bot EA", MagicNumber, 0, clrNONE);

    if(ticket < 0)
    {
    Print("开仓失败. 错误码: ", GetLastError());
    }
    else
    {
    Print("开仓成功. 订单号: ", ticket, " | 手数: ", lot, " | 方向: ", cmd == OP_BUY ? "做多" : "做空");

    if(SendAlerts)
    {
    Alert("UT Bot信号: ", cmd == OP_BUY ? "做多" : "做空", " 在 ", Symbol());
    SendNotification("UT Bot EA: " + (cmd == OP_BUY ? "做多" : "做空") + "信号在 " + Symbol());
    }
    }
    }

    //+------------------------------------------------------------------+
    //| 平仓所有持仓 |
    //+------------------------------------------------------------------+
    void CloseAllPositions()
    {
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
    if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
    {
    if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
    {
    double closePrice = (OrderType() == OP_BUY) ? Bid : Ask;
    bool closed = OrderClose(OrderTicket(), OrderLots(), closePrice, Slippage, clrNONE);

    if(!closed)
    Print("平仓失败 订单", OrderTicket(), ". 错误码: ", GetLastError());
    }
    }
    }
    }

    //+------------------------------------------------------------------+
    //| 平仓反向持仓 |
    //+------------------------------------------------------------------+
    void CloseOppositePositions(int newDirection)
    {
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
    if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
    {
    if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
    {
    bool isOpposite = (newDirection == OP_BUY && OrderType() == OP_SELL) ||
    (newDirection == OP_SELL && OrderType() == OP_BUY);

    if(isOpposite)
    {
    double closePrice = (OrderType() == OP_BUY) ? Bid : Ask;
    OrderClose(OrderTicket(), OrderLots(), closePrice, Slippage, clrNONE);
    }
    }
    }
    }
    }

    //+------------------------------------------------------------------+
    //| 统计持仓数量 |
    //+------------------------------------------------------------------+
    int CountPositions(int type = -1)
    {
    int count = 0;
    for(int i = 0; i < OrdersTotal(); i++)
    {
    if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
    {
    if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
    {
    if(type == -1 || OrderType() == type)
    count++;
    }
    }
    }
    return count;
    }

    //+------------------------------------------------------------------+
    //| 更新现有持仓的追踪止损 |
    //+------------------------------------------------------------------+
    void UpdateTrailingStop()
    {
    if(StopLossPoints > 0) return;

    double currentStop = CalculateUTTrailingStop(0);
    if(currentStop <= 0) return;

    for(int i = 0; i < OrdersTotal(); i++)
    {
    if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
    {
    if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
    {
    double newSL = 0;

    if(OrderType() == OP_BUY)
    {
    newSL = currentStop;
    if(newSL > OrderStopLoss() && newSL < Ask)
    {
    OrderModify(OrderTicket(), OrderOpenPrice(), newSL, OrderTakeProfit(), 0, clrNONE);
    Print("追踪止损已更新 买单 #", OrderTicket(), " 至 ", newSL);
    }
    }
    else if(OrderType() == OP_SELL)
    {
    newSL = currentStop;
    if((newSL < OrderStopLoss() || OrderStopLoss() == 0) && newSL > Bid)
    {
    OrderModify(OrderTicket(), OrderOpenPrice(), newSL, OrderTakeProfit(), 0, clrNONE);
    Print("追踪止损已更新 卖单 #", OrderTicket(), " 至 ", newSL);
    }
    }
    }
    }
    }
    }

    //+------------------------------------------------------------------+
    //| 检查点差是否在限制范围内 |
    //+------------------------------------------------------------------+
    bool IsSpreadOK()
    {
    if(MaxSpread <= 0) return true;

    int currentSpread = (int)((Ask - Bid) / Point / pointMultiplier);
    return (currentSpread <= MaxSpread);
    }

    //+------------------------------------------------------------------+
    //| EA报价处理函数 |
    //+------------------------------------------------------------------+
    void OnTick()
    {
    // 点差检查
    if(!IsSpreadOK())
    return;

    // 更新现有持仓的追踪止损
    UpdateTrailingStop();

    // 仅在新K线时检测信号,减少噪音
    if(!IsNewBar())
    return;

    // 检查买入信号
    if(IsBuySignal())
    {
    if(CloseOnOppositeSignal)
    CloseOppositePositions(OP_BUY);

    if(CountPositions(OP_BUY) == 0)
    OpenOrder(OP_BUY);
    }
    // 检查卖出信号
    else if(IsSellSignal())
    {
    if(CloseOnOppositeSignal)
    CloseOppositePositions(OP_SELL);

    if(CountPositions(OP_SELL) == 0)
    OpenOrder(OP_SELL);
    }
    }
    //+------------------------------------------------------------------+
    ``

    参数详解



    | 参数 | 说明 | 推荐值 |
    |------|------|--------|
    | Key Value | ATR倍数控制止损距离,数值越低信号越多 | FX:1.0-2.0, 黄金:2.5-3.5 |
    | ATR周期 | ATR计算周期(使用Wilder平滑) | 10(标准) |
    | 固定手数 | 固定交易手数(RiskPercent=0时使用) | 0.01-0.1 |
    | 风险百分比 | 每笔风险占账户余额百分比 | 1.0-2.0 |
    | 固定止损点数 | 额外固定止损(0=仅使用追踪止损) | 0或30-50 |
    | 止盈点数 | 固定止盈(0=禁用) | 0或80-150 |
    | 使用趋势过滤 | 启用EMA200趋势过滤 | true |
    | 趋势EMA周期 | 趋势过滤EMA周期 | 200 |
    | 最大点差 | 允许的最大点差 | 30-40 |
    | 使用Heikin Ashi | 使用平滑后的HA收盘价 | false |
    | 发送提醒 | 启用弹窗和移动端提醒 | true |
    | 反向平仓 | 反向信号时平仓反向单 | true |

    安装步骤



  • 复制代码到MT4的MetaEditor(按F4)

  • 点击编译(F7)- 确保无错误

  • 将EA附加到图表(建议EURUSD、GBPUSD或XAUUSD)

  • 在输入参数选项卡中调整参数

  • 启用自动交易(Alt+T)


  • 各品种推荐设置



    | 品种 | Key Value | ATR周期 | 时间周期 |
    |------|-----------|---------|----------|
    | EURUSD, USDJPY | 1.0-1.5 | 10-14 | M15-H1 |
    | GBPJPY, GBPNZD | 2.0-3.0 | 10-14 | M15-H1 |
    | 黄金(XAUUSD) | 2.5-3.5 | 10-14 | H1-H4 |
    | 比特币(BTCUSD) | 2.0-3.5 | 10-14 | H1-H4 |
    | 超短线(M1-M5) | 0.5-1.0 | 8-10 | M1-M5 |
    | 波段交易(H4-D1) | 2.0-3.5 | 10-14 | H4-D1 |

    编译与修改技巧



    主要修改方向:

  • 根据波动率调整KeyValue:稳定货币对使用较低值,波动品种使用较高值

  • 将UseHeikinAshi设为true可获得更平滑的价格走势,减少假信号

  • 添加多周期确认:在入场前检查更高时间周期的趋势方向

  • 设置UseTrendFilter为false可获得纯UT Bot信号(无EMA过滤)


  • 最佳市场环境:

    本趋势跟踪策略在具有持续方向性走势的市场中表现最佳。基于ATR的止损会在高波动率时自动放宽,在低波动率时自动收紧,使其能够适应不断变化的市场条件。

    参考来源



    本文EA源码为自主编译,基于MQL5市场上记载的UT Bot Alerts逻辑。原始UT Bot概念由HPotter开发,经QuantNomad在TradingView上推广,MT4/MT5移植版于2026年2月发布。

    如需更专业的优化版EA策略(含多周期分析、AI市场状态检测、完整回测报告和专业技术支持),请查看我们的付费EA合集。订阅后可每周获取更新和独家交易工具。