| //+------------------------------------------------------------------+
|
| //| HarmonicSignalAlerts.mq5 |
|
| //| Copyright 2026, Allan Niñal |
|
| //| https://www.allanninal.dev/mql5 |
|
| //+------------------------------------------------------------------+
|
| //| Free indicator: ABCD, Gartley and Bat patterns on one chart. |
|
| //| A pattern is drawn (XABCD lines, PRZ box, arrow) on the closed |
|
| //| bar where price reached its potential reversal zone, and only |
|
| //| from swings already confirmed, so nothing is ever drawn into the |
|
| //| past or removed. |
|
| //| Engine: Include/Harmonic/Harm.mqh (the same as the scanner). |
|
| //| Optional (off by default): a Fibonacci-extension exhaustion |
|
| //| marker, Include/Harmonic/Exhaust.mqh. An A-B-C impulse and |
|
| //| pullback projects 261.8% of A-B from C; when price reaches it |
|
| //| and then closes beyond the previous 3 bars, an arrow and the |
|
| //| word Exhaustion are drawn. An analysis marker, not a trade rule. |
|
| //+------------------------------------------------------------------+
|
| // SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
|
| #property copyright "Copyright 2026, Allan Niñal <https://www.allanninal.dev/mql5>"
|
| #property link "https://www.allanninal.dev/mql5"
|
| #property version "1.2"
|
| #property description "ABCD, Gartley and Bat harmonic patterns: XABCD lines and the reversal zone, drawn when price reaches the zone."
|
| #property description "Confirmed swings and closed bars only: a pattern never moves or disappears. Alerts: pop-up, sound, push, e-mail."
|
| #property description "Optional exhaustion marker (off by default): price reaches the 261.8% Fibonacci extension of an A-B-C and then turns."
|
| #property indicator_chart_window
|
| #property indicator_buffers 6
|
| #property indicator_plots 6
|
| #property indicator_label1 "Bullish pattern"
|
| #property indicator_type1 DRAW_ARROW
|
| #property indicator_color1 C'38,166,98'
|
| #property indicator_width1 2
|
| #property indicator_label2 "Bearish pattern"
|
| #property indicator_type2 DRAW_ARROW
|
| #property indicator_color2 C'226,74,70'
|
| #property indicator_width2 2
|
| #property indicator_label3 "Bullish patterns (1 ABCD, 2 Gartley, 4 Bat)"
|
| #property indicator_type3 DRAW_NONE
|
| #property indicator_label4 "Bearish patterns (1 ABCD, 2 Gartley, 4 Bat)"
|
| #property indicator_type4 DRAW_NONE
|
| #property indicator_label5 "Exhaustion up (bottom)"
|
| #property indicator_type5 DRAW_ARROW
|
| #property indicator_color5 C'236,163,43'
|
| #property indicator_width5 2
|
| #property indicator_label6 "Exhaustion down (top)"
|
| #property indicator_type6 DRAW_ARROW
|
| #property indicator_color6 C'236,163,43'
|
| #property indicator_width6 2
|
|
|
| #include "Harm.mqh"
|
| #include "Exhaust.mqh"
|
|
|
| input group "Patterns"
|
| input bool InpABCD = true; // ABCD
|
| input bool InpGartley = true; // Gartley
|
| input bool InpBat = true; // Bat
|
| input group "Swings and ratios"
|
| input int InpLeft = 10; // Swing: bars on the left
|
| input int InpRight = 5; // Swing: bars on the right (the confirmation delay)
|
| input double InpTol = 5.0; // Ratio tolerance, %
|
| input double InpPRZ = 3.0; // Reversal zone half-width, % of XA (ABCD: of AB)
|
| input group "Display"
|
| input int InpHistory = 3000; // Closed bars to scan on the chart (100-20000)
|
| input bool InpLines = true; // Draw the XABCD lines
|
| input bool InpZone = true; // Draw the reversal zone
|
| input bool InpLabels = true; // Point and pattern labels
|
| input color InpColBull = C'38,166,98'; // Bullish colour
|
| input color InpColBear = C'226,74,70'; // Bearish colour
|
| input group "Exhaustion marker (Fibonacci extension)"
|
| input bool InpExhaust = false; // Show the exhaustion marker
|
| input int InpExhPivot = 5; // Exhaustion: swing bars on each side (2-30)
|
| input int InpExhConf = 0; // Exhaustion: earlier swings needed near the levels (0 = not required, 1-5)
|
| input bool InpExhDraw = true; // Exhaustion: draw A-B-C, the 100 / 161.8 / 261.8% levels and the touch
|
| input color InpColExh = C'236,163,43'; // Exhaustion colour
|
|
|
| #include "SignalAlerts.mqh"
|
| #include <ChartKit/CkDebug.mqh>
|
| input group "Support"
|
| input bool InpDebug = false; // Debug log for bug reports (Experts tab)
|
|
|
| const string PRODUCT="Harmonic Signal Alerts";
|
| const string PREFIX ="HMP_";
|
|
|
| double g_bull[],g_bear[],g_mbull[],g_mbear[],g_xbull[],g_xbear[];
|
| HMParams g_p;
|
| HMState g_s;
|
| EXParams g_xp;
|
| EXState g_xs;
|
| int g_start=0; // chart bar of engine bar 0
|
| int g_fed=0; // chart bars fed so far
|
| datetime g_alert_bar=0,g_xalert_bar=0;
|
|
|
| void Clear()
|
| {
|
| ObjectsDeleteAll(0,PREFIX);
|
| HMInit(g_s);
|
| EXInit(g_xs);
|
| g_fed=0;
|
| ObjBudgetReset();
|
| }
|
|
|
| void Seg(const string name,const datetime t1,const double p1,const datetime t2,const double p2,const color clr,
|
| const ENUM_LINE_STYLE st,const int w)
|
| {
|
| if(ObjectFind(0,name)>=0)
|
| return;
|
| if(!ObjectCreate(0,name,OBJ_TREND,0,t1,p1,t2,p2))
|
| {
|
| CkDbg("ObjectCreate failed, error "+IntegerToString(GetLastError()));
|
| return;
|
| }
|
| ObjBudget(name);
|
| ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
|
| ObjectSetInteger(0,name,OBJPROP_STYLE,st);
|
| ObjectSetInteger(0,name,OBJPROP_WIDTH,w);
|
| ObjectSetInteger(0,name,OBJPROP_RAY_RIGHT,false);
|
| ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
|
| ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
|
| ObjectSetInteger(0,name,OBJPROP_BACK,true);
|
| }
|
|
|
| void Label(const string name,const datetime t,const double p,const string txt,const color clr,const bool above)
|
| {
|
| if(ObjectFind(0,name)>=0)
|
| return;
|
| if(!ObjectCreate(0,name,OBJ_TEXT,0,t,p))
|
| {
|
| CkDbg("ObjectCreate failed, error "+IntegerToString(GetLastError()));
|
| return;
|
| }
|
| ObjBudget(name);
|
| ObjectSetString(0,name,OBJPROP_TEXT,txt);
|
| ObjectSetString(0,name,OBJPROP_FONT,"Arial");
|
| ObjectSetInteger(0,name,OBJPROP_FONTSIZE,8);
|
| ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
|
| ObjectSetInteger(0,name,OBJPROP_ANCHOR,above ? ANCHOR_LOWER : ANCHOR_UPPER);
|
| ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
|
| ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
|
| }
|
|
|
| //--- one completed pattern; chart bars of the points are g_start + engine bar
|
| void DrawPattern(const HMEvent &e,const datetime &time[],const int rates_total)
|
| {
|
| string id=PREFIX+IntegerToString((long)time[g_start+e.bar])+(e.dir>0 ? "b" : "s")+IntegerToString(e.pat)+"_";
|
| color clr=(e.dir>0 ? InpColBull : InpColBear);
|
| datetime tx=(e.xb>=0 ? time[g_start+e.xb] : 0),ta=time[g_start+e.ab],tb=time[g_start+e.bb];
|
| datetime tc=time[g_start+e.cb],td=time[g_start+e.db],te=time[g_start+e.bar];
|
| bool bull=(e.dir>0);
|
| if(InpLines)
|
| {
|
| if(e.xb>=0)
|
| {
|
| Seg(id+"xa",tx,e.xp,ta,e.ap,clr,STYLE_SOLID,2);
|
| Seg(id+"xb",tx,e.xp,tb,e.bp,clr,STYLE_DOT,1);
|
| }
|
| Seg(id+"ab",ta,e.ap,tb,e.bp,clr,STYLE_SOLID,2);
|
| Seg(id+"bc",tb,e.bp,tc,e.cp,clr,STYLE_SOLID,2);
|
| Seg(id+"cd",tc,e.cp,td,e.dp,clr,STYLE_SOLID,2);
|
| Seg(id+"bd",tb,e.bp,td,e.dp,clr,STYLE_DOT,1);
|
| if(e.xb<0)
|
| Seg(id+"ac",ta,e.ap,tc,e.cp,clr,STYLE_DOT,1);
|
| }
|
| if(InpZone)
|
| {
|
| string z=id+"prz";
|
| if(ObjectFind(0,z)<0)
|
| {
|
| datetime t2=te+(datetime)(10*PeriodSeconds());
|
| if(ObjectCreate(0,z,OBJ_RECTANGLE,0,tc,e.near_,t2,e.far_))
|
| ObjBudget(z);
|
| ObjectSetInteger(0,z,OBJPROP_COLOR,clr);
|
| ObjectSetInteger(0,z,OBJPROP_STYLE,STYLE_DOT);
|
| ObjectSetInteger(0,z,OBJPROP_FILL,false);
|
| ObjectSetInteger(0,z,OBJPROP_BACK,true);
|
| ObjectSetInteger(0,z,OBJPROP_SELECTABLE,false);
|
| ObjectSetInteger(0,z,OBJPROP_HIDDEN,true);
|
| ObjectSetString(0,z,OBJPROP_TOOLTIP,StringFormat("%s %s reversal zone %s - %s",HMName(e.pat),bull ? "bullish" : "bearish",
|
| DoubleToString(MathMin(e.near_,e.far_),_Digits),DoubleToString(MathMax(e.near_,e.far_),_Digits)));
|
| }
|
| }
|
| if(InpLabels)
|
| {
|
| //--- swing labels: lows below, highs above (X and B are lows of a bullish pattern). Named by the point, so
|
| //--- patterns sharing swings share one label; D lists every pattern that completed there
|
| string pt=PREFIX+"L"+(bull ? "b" : "s");
|
| if(e.xb>=0)
|
| Label(pt+IntegerToString((long)tx),tx,e.xp,"X",clr,!bull);
|
| Label(pt+IntegerToString((long)ta),ta,e.ap,"A",clr,bull);
|
| Label(pt+IntegerToString((long)tb),tb,e.bp,"B",clr,!bull);
|
| Label(pt+IntegerToString((long)tc),tc,e.cp,"C",clr,bull);
|
| string dn=pt+"D"+IntegerToString((long)td);
|
| if(ObjectFind(0,dn)>=0)
|
| ObjectSetString(0,dn,OBJPROP_TEXT,ObjectGetString(0,dn,OBJPROP_TEXT)+", "+HMName(e.pat));
|
| else
|
| Label(dn,td,e.dp,"D "+HMName(e.pat),clr,!bull);
|
| }
|
| }
|
|
|
| //--- one exhaustion signal: A-B-C, the levels, the touch and the word; chart bars are g_start + engine bar
|
| void DrawExhaustion(const EXEvent &e,const datetime &time[],const double barprice)
|
| {
|
| string id=PREFIX+"X"+IntegerToString((long)time[g_start+e.bar])+"_";
|
| bool top=(e.dir<0); // a signal down follows a top
|
| datetime ta=time[g_start+e.ab],tb=time[g_start+e.bb],tc=time[g_start+e.cb],tt=time[g_start+e.touch],ts=time[g_start+e.bar];
|
| if(InpExhDraw)
|
| {
|
| Seg(id+"ab",ta,e.ap,tb,e.bp,InpColExh,STYLE_SOLID,1);
|
| Seg(id+"bc",tb,e.bp,tc,e.cp,InpColExh,STYLE_SOLID,1);
|
| double leg=MathAbs(e.bp-e.ap),sd=(top ? 1.0 : -1.0);
|
| double r[3]={1.0,1.618,2.618};
|
| string nm[3]={"100%","161.8%","261.8%"};
|
| for(int i=0; i<3; i++)
|
| {
|
| double y=e.cp+sd*r[i]*leg;
|
| Seg(id+"l"+IntegerToString(i),tc,y,ts,y,InpColExh,STYLE_DOT,1);
|
| Label(id+"t"+IntegerToString(i),ts,y,nm[i],InpColExh,!top);
|
| }
|
| Label(id+"A",ta,e.ap,"A",InpColExh,!top);
|
| Label(id+"B",tb,e.bp,"B",InpColExh,top);
|
| Label(id+"C",tc,e.cp,"C",InpColExh,!top);
|
| string tn=id+"touch";
|
| if(ObjectFind(0,tn)<0)
|
| {
|
| if(ObjectCreate(0,tn,OBJ_ARROW,0,tt,e.e))
|
| ObjBudget(tn);
|
| ObjectSetInteger(0,tn,OBJPROP_ARROWCODE,159);
|
| ObjectSetInteger(0,tn,OBJPROP_COLOR,InpColExh);
|
| ObjectSetInteger(0,tn,OBJPROP_WIDTH,2);
|
| ObjectSetInteger(0,tn,OBJPROP_SELECTABLE,false);
|
| ObjectSetInteger(0,tn,OBJPROP_HIDDEN,true);
|
| ObjectSetString(0,tn,OBJPROP_TOOLTIP,"Price reached the 261.8% extension: "+DoubleToString(e.e,_Digits));
|
| }
|
| }
|
| if(InpLabels)
|
| Label(id+"x",ts,barprice,"Exhaustion",InpColExh,top);
|
| }
|
|
|
| int OnInit()
|
| {
|
| CkDebugStart(PRODUCT,"1.2",InpDebug,StringFormat("ABCD %d Gartley %d Bat %d, swings %d/%d, tolerance %.1f%%, zone %.1f%%, history %d, lines %d, zone %d, labels %d, exhaustion %d (pivot %d, earlier swings %d)",
|
| (int)InpABCD,(int)InpGartley,(int)InpBat,InpLeft,InpRight,InpTol,InpPRZ,InpHistory,(int)InpLines,(int)InpZone,(int)InpLabels,(int)InpExhaust,InpExhPivot,InpExhConf));
|
| if(!InpABCD && !InpGartley && !InpBat)
|
| {
|
| Print(PRODUCT,": switch on at least one pattern");
|
| CkDbg("init refused: an input is out of range (see the line above)");
|
| return(INIT_PARAMETERS_INCORRECT);
|
| }
|
| if(InpExhPivot<2 || InpExhPivot>EX_MAXW || InpExhConf<0 || InpExhConf>5)
|
| {
|
| Print(PRODUCT,StringFormat(": exhaustion swing bars 2-%d, earlier swings 0-5",EX_MAXW));
|
| CkDbg("init refused: an input is out of range (see the line above)");
|
| return(INIT_PARAMETERS_INCORRECT);
|
| }
|
| if(InpLeft<1 || InpRight<1 || InpLeft+InpRight+2>=HM_N || InpTol<0 || InpTol>20 || InpPRZ<=0 || InpPRZ>20 ||
|
| InpHistory<100 || InpHistory>20000)
|
| {
|
| Print(PRODUCT,StringFormat(": swing bars 1+ (left + right below %d), tolerance 0-20%%, zone 0-20%%, history 100-20000",HM_N-2));
|
| CkDbg("init refused: an input is out of range (see the line above)");
|
| return(INIT_PARAMETERS_INCORRECT);
|
| }
|
| ZeroMemory(g_p);
|
| g_p.left=InpLeft; g_p.right=InpRight; g_p.tol=InpTol/100.0; g_p.prz=InpPRZ/100.0;
|
| g_p.use[HM_ABCD]=InpABCD; g_p.use[HM_GARTLEY]=InpGartley; g_p.use[HM_BAT]=InpBat;
|
| g_xp.w=InpExhPivot; g_xp.conf=InpExhConf;
|
| SetIndexBuffer(0,g_bull,INDICATOR_DATA);
|
| SetIndexBuffer(1,g_bear,INDICATOR_DATA);
|
| SetIndexBuffer(2,g_mbull,INDICATOR_DATA);
|
| SetIndexBuffer(3,g_mbear,INDICATOR_DATA);
|
| SetIndexBuffer(4,g_xbull,INDICATOR_DATA);
|
| SetIndexBuffer(5,g_xbear,INDICATOR_DATA);
|
| PlotIndexSetInteger(0,PLOT_ARROW,233);
|
| PlotIndexSetInteger(1,PLOT_ARROW,234);
|
| PlotIndexSetInteger(0,PLOT_ARROW_SHIFT,14);
|
| PlotIndexSetInteger(1,PLOT_ARROW_SHIFT,-14);
|
| PlotIndexSetInteger(4,PLOT_ARROW,217);
|
| PlotIndexSetInteger(5,PLOT_ARROW,218);
|
| PlotIndexSetInteger(4,PLOT_ARROW_SHIFT,24);
|
| PlotIndexSetInteger(5,PLOT_ARROW_SHIFT,-24);
|
| PlotIndexSetInteger(4,PLOT_LINE_COLOR,InpColExh);
|
| PlotIndexSetInteger(5,PLOT_LINE_COLOR,InpColExh);
|
| PlotIndexSetInteger(0,PLOT_LINE_COLOR,InpColBull);
|
| PlotIndexSetInteger(1,PLOT_LINE_COLOR,InpColBear);
|
| for(int i=0; i<6; i++)
|
| PlotIndexSetDouble(i,PLOT_EMPTY_VALUE,EMPTY_VALUE);
|
| IndicatorSetString(INDICATOR_SHORTNAME,PRODUCT);
|
| Clear();
|
| return(INIT_SUCCEEDED);
|
| }
|
|
|
| void OnDeinit(const int reason)
|
| {
|
| CkDbg("removed, reason "+IntegerToString(reason));
|
| ObjectsDeleteAll(0,PREFIX);
|
| }
|
|
|
| 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<InpLeft+InpRight+3)
|
| return(0);
|
| ArraySetAsSeries(time,false);
|
| ArraySetAsSeries(high,false);
|
| ArraySetAsSeries(low,false);
|
| ArraySetAsSeries(close,false);
|
| if(prev_calculated==0) // first run or the history changed: start again
|
| {
|
| Clear();
|
| g_start=MathMax(0,rates_total-1-InpHistory);
|
| CkDbg(StringFormat("full scan: %d bars on the chart, the last %d closed bars are scanned",rates_total,MathMin(InpHistory,rates_total-1)));
|
| ArrayInitialize(g_bull,EMPTY_VALUE);
|
| ArrayInitialize(g_bear,EMPTY_VALUE);
|
| ArrayInitialize(g_mbull,EMPTY_VALUE);
|
| ArrayInitialize(g_mbear,EMPTY_VALUE);
|
| ArrayInitialize(g_xbull,EMPTY_VALUE);
|
| ArrayInitialize(g_xbear,EMPTY_VALUE);
|
| }
|
| else
|
| for(int i=MathMax(prev_calculated-1,0); i<rates_total; i++)
|
| if(i>=g_start+g_fed)
|
| { g_bull[i]=EMPTY_VALUE; g_bear[i]=EMPTY_VALUE; g_mbull[i]=EMPTY_VALUE; g_mbear[i]=EMPTY_VALUE; g_xbull[i]=EMPTY_VALUE; g_xbear[i]=EMPTY_VALUE; }
|
| bool live=(prev_calculated>0);
|
| string events="";
|
| int nsig=0;
|
| //--- feed every closed bar not fed yet (the forming bar rates_total-1 never)
|
| for(int i=g_start+g_fed; i<=rates_total-2; i++)
|
| {
|
| bool ev=HMFeed(g_s,g_p,high[i],low[i],close[i]);
|
| g_fed++;
|
| if(ev)
|
| nsig++;
|
| if(InpExhaust && EXFeed(g_xs,g_xp,high[i],low[i],close[i]))
|
| {
|
| DrawExhaustion(g_xs.ev,time,g_xs.ev.dir>0 ? low[i] : high[i]);
|
| if(g_xs.ev.dir>0)
|
| g_xbull[i]=low[i];
|
| else
|
| g_xbear[i]=high[i];
|
| if(live && i==rates_total-2 && time[i]>g_xalert_bar)
|
| {
|
| g_xalert_bar=time[i];
|
| string tfx=StringSubstr(EnumToString((ENUM_TIMEFRAMES)_Period),7);
|
| events+=(events=="" ? "" : "; ")+StringFormat("%s exhaustion (261.8%% extension reached, then a close beyond the previous 3 bars) on %s %s",
|
| g_xs.ev.dir>0 ? "Bullish" : "Bearish",_Symbol,tfx);
|
| }
|
| }
|
| if(!ev)
|
| continue;
|
| int mb=0,ms=0;
|
| for(int k=0; k<g_s.nev; k++)
|
| {
|
| if(g_s.ev[k].dir>0)
|
| mb|=(1<<g_s.ev[k].pat);
|
| else
|
| ms|=(1<<g_s.ev[k].pat);
|
| DrawPattern(g_s.ev[k],time,rates_total);
|
| }
|
| if(mb>0)
|
| { g_bull[i]=low[i]; g_mbull[i]=mb; }
|
| if(ms>0)
|
| { g_bear[i]=high[i]; g_mbear[i]=ms; }
|
| if(live && i==rates_total-2 && time[i]>g_alert_bar)
|
| {
|
| g_alert_bar=time[i];
|
| string tf=StringSubstr(EnumToString((ENUM_TIMEFRAMES)_Period),7);
|
| if(mb>0)
|
| events+=StringFormat("Bullish %s reached its zone on %s %s",HMEventNames(g_s,1),_Symbol,tf);
|
| if(ms>0)
|
| events+=(events=="" ? "" : "; ")+StringFormat("Bearish %s reached its zone on %s %s",HMEventNames(g_s,-1),_Symbol,tf);
|
| }
|
| }
|
| if(!live)
|
| CkDbg(StringFormat("scan done: %d closed bars fed, %d signal bars",g_fed,nsig));
|
| if(events!="")
|
| SignalAlertMessage(PRODUCT,events);
|
| return(rates_total);
|
| }
|
| //+------------------------------------------------------------------+
|