ut-bot-signal-alerts/mql4/Indicators/UTBotAlerts/UTBotAlerts.mq4
Allan Niñal Allan Niñal 76369cd38d Free source regenerated: pop-up and push on by default, debug log
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:14:21 +08:00
246 라인
10 KiB
MQL4
//+------------------------------------------------------------------+
//| UTBotAlerts.mq4 |
//| Copyright 2026, Allan Niñal |
//| https://www.allanninal.dev/mql5 |
//+------------------------------------------------------------------+
//| MT4 edition of UT Bot Signal Alerts: the same calculation as the |
//| MT5 indicator (an ATR trailing stop on the close, key value x |
//| ATR; buy when the close crosses above the stop line, sell when |
//| it crosses below; Wilder's ATR seeded with the SMA of the first |
//| values, as on TradingView; optional Heikin Ashi source and EMA |
//| trend filter). |
//| Arrows and alerts come from closed bars only, so a signal never |
//| appears and then disappears. The line moves on the live bar. |
//| MT4 differences: the stop line is two buffers (blue/red), and |
//| the arrow gap is a fraction of ATR (MT4 has no pixel shift). |
//| Buffers for iCustom match MT5: 0 stop line, 2 buy, 3 sell. |
//+------------------------------------------------------------------+
//+------------------------------------------------------------------+
// Licence: Mozilla Public License 2.0. This Source Code Form is subject
// to the terms of the MPL, v. 2.0. If a copy of the MPL was not
// distributed with this file, You can obtain one at
// https://mozilla.org/MPL/2.0/
// Origin: the UT Bot idea (an ATR trailing stop on the close) is by
// Yo_adriiiiaan ("UT Bot Strategy"), from an original idea by HPotter;
// the TradingView "UT Bot Alerts" script is by QuantNomad. This file is
// an independent MQL implementation of that published method.
// SPDX-License-Identifier: MPL-2.0
#property copyright "Copyright 2026, Allan Niñal <https://www.allanninal.dev/mql5>"
#property link "https://www.allanninal.dev/mql5"
#property version "1.1"
#property strict
#property description "UT Bot ATR trailing stop with buy/sell arrows on closed bars, optional EMA trend filter and Heikin Ashi source."
#property description "Alerts: pop-up, sound, push, e-mail. Non-repainting: signals only on closed bars."
#property indicator_chart_window
#property indicator_buffers 6
#property indicator_color1 clrNONE
#property indicator_color2 clrDodgerBlue
#property indicator_width2 2
#property indicator_color3 clrDodgerBlue
#property indicator_width3 2
#property indicator_color4 clrTomato
#property indicator_width4 2
#property indicator_color5 clrTomato
#property indicator_width5 2
#property indicator_color6 clrGoldenrod
input double InpKey = 1.0; // Key value (sensitivity: ATR multiple)
input int InpATR = 10; // ATR period
input bool InpHeikin = false; // Use the Heikin Ashi close as the source
input bool InpUseEMA = false; // Only signals with the EMA trend
input int InpEMA = 200; // EMA period
input bool InpShowEMA = true; // Draw the EMA (when the filter is on)
input bool InpShowLine = true; // Draw the stop line
input double InpArrowGap = 0.3; // Arrow distance from the candle (x ATR)
#include "SignalAlerts.mqh"
//--- series-indexed (0 = live bar), as every MT4 buffer
double g_line[],g_up[],g_buy[],g_sell[],g_dn[],g_ema_draw[];
double g_atr[],g_src[],g_hao[],g_ema[];
const string PRODUCT="UT Bot";
int OnInit()
{
CkDebugStart(PRODUCT,"1.1",InpDebug,StringFormat("key %.2f, ATR %d, Heikin Ashi %d, EMA filter %d (%d), arrow gap %s, line %d, alerts popup %d sound %d push %d e-mail %d",
InpKey,InpATR,(int)InpHeikin,(int)InpUseEMA,InpEMA,DoubleToString(InpArrowGap,2),(int)InpShowLine,(int)InpAlertPopup,(int)InpAlertSound,(int)InpAlertPush,(int)InpAlertEmail));
if(InpKey<=0 || InpKey>100 || InpATR<1 || InpATR>1000 || InpEMA<1 || InpEMA>5000 || InpArrowGap<0)
{
Print("UT Bot: key value 0-100 (above 0), ATR period 1-1000, EMA period 1-5000, arrow distance 0 or more");
CkDbg("init refused: parameters out of range");
return(INIT_PARAMETERS_INCORRECT);
}
IndicatorBuffers(10);
SetIndexBuffer(0,g_line);
SetIndexBuffer(1,g_up);
SetIndexBuffer(2,g_buy);
SetIndexBuffer(3,g_sell);
SetIndexBuffer(4,g_dn);
SetIndexBuffer(5,g_ema_draw);
SetIndexBuffer(6,g_atr,INDICATOR_CALCULATIONS);
SetIndexBuffer(7,g_src,INDICATOR_CALCULATIONS);
SetIndexBuffer(8,g_hao,INDICATOR_CALCULATIONS);
SetIndexBuffer(9,g_ema,INDICATOR_CALCULATIONS);
SetIndexStyle(0,DRAW_NONE);
SetIndexStyle(1,InpShowLine ? DRAW_LINE : DRAW_NONE);
SetIndexStyle(4,InpShowLine ? DRAW_LINE : DRAW_NONE);
SetIndexStyle(2,DRAW_ARROW);
SetIndexStyle(3,DRAW_ARROW);
SetIndexArrow(2,233);
SetIndexArrow(3,234);
SetIndexStyle(5,(InpUseEMA && InpShowEMA) ? DRAW_LINE : DRAW_NONE,STYLE_DOT);
SetIndexLabel(0,"UT Bot stop");
SetIndexLabel(1,NULL);
SetIndexLabel(2,"Buy");
SetIndexLabel(3,"Sell");
SetIndexLabel(4,NULL);
SetIndexLabel(5,InpUseEMA ? "Trend EMA" : NULL);
for(int b=0; b<10; b++)
SetIndexEmptyValue(b,EMPTY_VALUE);
IndicatorShortName(StringFormat("UT Bot (%.2f, %d%s%s)",InpKey,InpATR,
InpHeikin ? ", HA" : "",InpUseEMA ? ", EMA "+IntegerToString(InpEMA) : ""));
IndicatorDigits(_Digits);
return(INIT_SUCCEEDED);
}
//--- true range of the bar at series index s (n = bars in total; the oldest bar has no previous close)
double TrueRange(const int s,const int n,const double &high[],const double &low[],const double &close[])
{
if(s==n-1)
return(high[s]-low[s]);
return(MathMax(high[s]-low[s],MathMax(MathAbs(high[s]-close[s+1]),MathAbs(low[s]-close[s+1]))));
}
//--- the two coloured halves of the stop line at series index s, joined to the bar before when the colour changes
void Segments(const int s,const int n)
{
g_up[s]=EMPTY_VALUE;
g_dn[s]=EMPTY_VALUE;
if(g_line[s]==EMPTY_VALUE)
return;
bool up=(g_src[s]>g_line[s]);
if(up)
g_up[s]=g_line[s];
else
g_dn[s]=g_line[s];
if(s+1>=n || g_line[s+1]==EMPTY_VALUE)
return;
//--- the older bar back to its own colour (a join from an earlier tick of this bar may be stale), then the join
bool was_up=(g_src[s+1]>g_line[s+1]);
g_up[s+1]=(was_up ? g_line[s+1] : EMPTY_VALUE);
g_dn[s+1]=(was_up ? EMPTY_VALUE : g_line[s+1]);
if(was_up!=up)
{
if(up)
g_up[s+1]=g_line[s+1];
else
g_dn[s+1]=g_line[s+1];
}
}
int OnCalculate(const int rates_total,const int prev_calculated,const datetime &time[],
const double &open[],const double &high[],const double &low[],const double &close[],
const long &tick_volume[],const long &volume[],const int &spread[])
{
if(rates_total<2)
return(0);
if(prev_calculated==0)
CkDbg(StringFormat("full calculation, %d bars",rates_total));
ArraySetAsSeries(time,true);
ArraySetAsSeries(open,true);
ArraySetAsSeries(high,true);
ArraySetAsSeries(low,true);
ArraySetAsSeries(close,true);
//--- oldest bar first: i counts from the oldest bar (as in the MT5 code), s is the series index
int first=(prev_calculated>0 ? rates_total-prev_calculated : rates_total-1);
if(first>=rates_total-1)
first=rates_total-1;
for(int s=first; s>=0; s--)
{
int i=rates_total-1-s;
//--- source: close, or the Heikin Ashi close
if(InpHeikin)
{
double hac=(open[s]+high[s]+low[s]+close[s])/4.0;
g_hao[s]=(i==0 ? (open[s]+close[s])/2.0 : (g_hao[s+1]+g_src[s+1])/2.0);
g_src[s]=hac;
}
else
{
g_hao[s]=open[s];
g_src[s]=close[s];
}
//--- ATR: Wilder's smoothing of the true range, seeded with the SMA of the first InpATR values
if(i<InpATR-1)
g_atr[s]=EMPTY_VALUE;
else
if(i==InpATR-1)
{
double sum=0;
for(int k=0; k<InpATR; k++)
sum+=TrueRange(rates_total-1-k,rates_total,high,low,close);
g_atr[s]=sum/InpATR;
}
else
g_atr[s]=(g_atr[s+1]*(InpATR-1)+TrueRange(s,rates_total,high,low,close))/InpATR;
//--- EMA of the close, seeded with the SMA of the first InpEMA closes
if(i<InpEMA-1)
g_ema[s]=EMPTY_VALUE;
else
if(i==InpEMA-1)
{
double sum=0;
for(int k=0; k<InpEMA; k++)
sum+=close[rates_total-1-k];
g_ema[s]=sum/InpEMA;
}
else
g_ema[s]=g_ema[s+1]+2.0/(InpEMA+1)*(close[s]-g_ema[s+1]);
g_ema_draw[s]=g_ema[s];
//--- the trailing stop
double prev=(i>0 ? g_line[s+1] : 0.0);
g_buy[s]=EMPTY_VALUE;
g_sell[s]=EMPTY_VALUE;
if(g_atr[s]==EMPTY_VALUE)
{
g_line[s]=EMPTY_VALUE;
Segments(s,rates_total);
continue;
}
if(prev==EMPTY_VALUE)
prev=0.0;
double src=g_src[s],p1=(i>0 ? g_src[s+1] : src),nloss=InpKey*g_atr[s];
if(src>prev && p1>prev)
g_line[s]=MathMax(prev,src-nloss);
else
if(src<prev && p1<prev)
g_line[s]=MathMin(prev,src+nloss);
else
g_line[s]=(src>prev ? src-nloss : src+nloss);
Segments(s,rates_total);
//--- signals: the source crosses the stop line; only on closed bars (s>=1)
if(i==0 || s<1 || g_line[s+1]==EMPTY_VALUE)
continue;
bool up=(src>g_line[s] && p1<=g_line[s+1]);
bool dn=(src<g_line[s] && p1>=g_line[s+1]);
if(InpUseEMA)
{
up=up && g_ema[s]!=EMPTY_VALUE && close[s]>g_ema[s];
dn=dn && g_ema[s]!=EMPTY_VALUE && close[s]<g_ema[s];
}
if(up)
{
g_buy[s]=low[s]-InpArrowGap*g_atr[s];
if(s==1)
SignalAlert(PRODUCT,"BUY at "+DoubleToString(close[s],_Digits)+", stop "+DoubleToString(g_line[s],_Digits),time[s],prev_calculated>0);
}
if(dn)
{
g_sell[s]=high[s]+InpArrowGap*g_atr[s];
if(s==1)
SignalAlert(PRODUCT,"SELL at "+DoubleToString(close[s],_Digits)+", stop "+DoubleToString(g_line[s],_Digits),time[s],prev_calculated>0);
}
}
return(rates_total);
}
//+------------------------------------------------------------------+