| //+------------------------------------------------------------------+
|
| //| DisplacementOrderBlocks.mq4 |
|
| //| Copyright 2026, Allan Niñal |
|
| //| https://www.allanninal.dev/mql5 |
|
| //+------------------------------------------------------------------+
|
| //| Demand and supply zones left by a displacement candle that |
|
| //| closes through the newest swing: the zone is the nearest candle |
|
| //| of the other colour before that candle. Each zone is fresh until|
|
| //| price comes back to it, tested after that (re-entries counted) |
|
| //| and broken once price closes (or wicks) beyond its far edge. |
|
| //| Decided from closed bars only, so nothing appears, moves or |
|
| //| disappears after a bar closed. Rules: DzEngine.mqh. |
|
| //| An indicator, not a trading system: it draws and alerts only. |
|
| //| MT4 edition: the same engine (DzEngine.mqh, an exact copy of the |
|
| //| MT5 one), the same inputs and the same 13 iCustom buffers. |
|
| //+------------------------------------------------------------------+
|
| // 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 "Demand and supply zones from displacement candles that close through a swing: fresh, tested and broken states."
|
| #property description "Non-repainting: zones are decided from closed bars and never change afterwards. Alerts for new, tested and broken zones."
|
| #property indicator_chart_window
|
| #property indicator_buffers 14
|
|
|
| #include "DzEngine.mqh"
|
| #include "DzDraw.mqh"
|
| #include <ChartKit/CkDebug.mqh>
|
|
|
| enum ENUM_DZ_ZONE
|
| {
|
| DZ_ZONE_FULL=0, // Whole base candle (high to low)
|
| DZ_ZONE_BODY=1, // Base candle body only
|
| DZ_ZONE_MIXED=2 // Body plus the wick on the far side
|
| };
|
| enum ENUM_DZ_BASE
|
| {
|
| DZ_BASE_NEAREST=0, // Nearest candle of the other colour
|
| DZ_BASE_MOVE=1 // Other-colour candle where the move began
|
| };
|
| enum ENUM_DZ_TEST
|
| {
|
| DZ_TEST_NEAR=0, // Price reaches the near edge
|
| DZ_TEST_MID=1, // Price reaches the middle
|
| DZ_TEST_FAR=2 // Price reaches the far edge
|
| };
|
| enum ENUM_DZ_BREAK
|
| {
|
| DZ_BREAK_CLOSE=0, // A bar closes beyond the far edge
|
| DZ_BREAK_WICK=1 // A wick goes beyond the far edge
|
| };
|
|
|
| input int InpSwing = 3; // Swing strength, bars on each side
|
| input int InpATR = 14; // ATR period
|
| input double InpDispMult = 1.0; // Candle range at least, x ATR
|
| input double InpBodyPct = 55.0; // Candle body at least, % of its range
|
| input int InpSearch = 12; // Bars searched back for the base candle
|
| input ENUM_DZ_BASE InpBaseMode = DZ_BASE_NEAREST; // Base candle
|
| input ENUM_DZ_ZONE InpZoneMode = DZ_ZONE_FULL; // Zone covers
|
| input ENUM_DZ_TEST InpTestMode = DZ_TEST_NEAR; // A zone is tested when
|
| input ENUM_DZ_BREAK InpBreakMode = DZ_BREAK_CLOSE; // A zone is broken when
|
| input int InpMaxLive = 12; // Live zones kept per side (0 = no limit)
|
| input bool InpTrendOnly = false; // Only zones in the validated trend (demand in an uptrend, supply in a downtrend)
|
| input double InpMinRR = 0.0; // Minimum reward-to-risk of a zone (0 = off)
|
| input bool InpShowFiltered = false; // Show the zones left out by these options faded (outline only)
|
| input bool InpShowTrend = false; // Show the validated trend (corner label)
|
| input bool InpShowTested = true; // Keep tested zones (dashed outline)
|
| input bool InpShowBroken = false; // Keep broken zones, up to the bar that broke them (grey)
|
| input bool InpMidline = true; // Middle line
|
| input bool InpLabels = true; // Labels (D / S, x2 = second entry)
|
| input bool InpSwings = false; // Mark the swing that was closed through
|
| input int InpDrawBars = 1000; // Draw zones made within this many bars
|
| input int InpMaxZones = 60; // Draw at most this many zones (newest first)
|
| input int InpExtendBars = 15; // Extend live zones this many bars past the last bar
|
| input bool InpFill = true; // Fill fresh zones
|
| input color InpClrDem = C'38,120,90'; // Demand colour
|
| input color InpClrSup = C'150,52,60'; // Supply colour
|
| input color InpClrBroken = C'110,110,110'; // Broken zone colour
|
|
|
| #include "SignalAlerts.mqh"
|
|
|
| input bool InpAlNew = true; // Alert: new zone
|
| input bool InpAlTest = true; // Alert: a fresh zone is tested
|
| input bool InpAlBroken = false; // Alert: a zone is broken
|
| input bool InpDebug = false; // Debug log for bug reports (Experts tab)
|
|
|
| const string PRODUCT="Displacement Order Blocks";
|
| const string PFX="DZ_";
|
|
|
| //--- the 14 buffers, series-indexed (0 = live bar) as every MT4 buffer; the engine counts bars from the oldest
|
| double b0[],b1[],b2[],b3[],b4[],b5[],b6[],b7[],b8[],b9[],b10[],b11[],b12[],b13[];
|
| CDzEngine g_eng;
|
| SDzStyle g_style;
|
| double g_out[];
|
| int g_draw_stamp=-1;
|
| bool g_no_draw=false;
|
| int g_dbg_fail=0;
|
|
|
| //--- buffer k at series index s
|
| void SetB(const int k,const int s,const double v)
|
| {
|
| switch(k)
|
| {
|
| case 0: b0[s]=v; break;
|
| case 1: b1[s]=v; break;
|
| case 2: b2[s]=v; break;
|
| case 3: b3[s]=v; break;
|
| case 4: b4[s]=v; break;
|
| case 5: b5[s]=v; break;
|
| case 6: b6[s]=v; break;
|
| case 7: b7[s]=v; break;
|
| case 8: b8[s]=v; break;
|
| case 9: b9[s]=v; break;
|
| case 10: b10[s]=v; break;
|
| case 11: b11[s]=v; break;
|
| case 12: b12[s]=v; break;
|
| case 13: b13[s]=v; break;
|
| }
|
| }
|
|
|
| int OnInit()
|
| {
|
| CkDebugStart(PRODUCT,"1.2",InpDebug,StringFormat("swing %d, ATR %d, disp %.2f x ATR body %.0f%%, search %d, base %d, zone %d, test %d, break %d, max live %d, trend only %d, min RR %.2f, filtered %d, tested %d, broken %d, draw %d bars, max %d zones, extend %d",
|
| InpSwing,InpATR,InpDispMult,InpBodyPct,InpSearch,(int)InpBaseMode,(int)InpZoneMode,(int)InpTestMode,(int)InpBreakMode,InpMaxLive,
|
| InpTrendOnly,InpMinRR,InpShowFiltered,InpShowTested,InpShowBroken,InpDrawBars,InpMaxZones,InpExtendBars));
|
| if(InpSwing<1 || InpSwing>20 || InpATR<1 || InpATR>1000 || InpDispMult<0 || InpDispMult>100 || InpBodyPct<0 || InpBodyPct>100 ||
|
| InpSearch<1 || InpSearch>200 || InpMaxLive<0 || InpMaxLive>1000 || InpMinRR<0 || InpMinRR>100 || InpDrawBars<1 ||
|
| InpMaxZones<0 || InpMaxZones>1000 || InpExtendBars<0 || InpExtendBars>1000)
|
| {
|
| Print(PRODUCT,": check the inputs (swing 1-20, ATR period 1-1000, body 0-100 %, search 1-200 bars, zones 0-1000, extension 0-1000 bars)");
|
| CkDbg("init refused: inputs out of range");
|
| return(INIT_PARAMETERS_INCORRECT);
|
| }
|
| string names[DZ_B_COUNT]={"New demand top","New demand bottom","New supply top","New supply bottom","Swing high closed through",
|
| "Swing low closed through","Zones tested","Retests","Zones broken","Live demand zones",
|
| "Live supply zones","Demand base, bars back","Supply base, bars back","Validated trend (+1 up, -1 down)"};
|
| ArrayResize(g_out,DZ_B_COUNT);
|
| IndicatorBuffers(DZ_B_COUNT);
|
| SetIndexBuffer(0,b0); SetIndexBuffer(1,b1); SetIndexBuffer(2,b2); SetIndexBuffer(3,b3);
|
| SetIndexBuffer(4,b4); SetIndexBuffer(5,b5); SetIndexBuffer(6,b6); SetIndexBuffer(7,b7);
|
| SetIndexBuffer(8,b8); SetIndexBuffer(9,b9); SetIndexBuffer(10,b10); SetIndexBuffer(11,b11);
|
| SetIndexBuffer(12,b12); SetIndexBuffer(13,b13);
|
| for(int k=0; k<DZ_B_COUNT; k++)
|
| {
|
| SetIndexStyle(k,DRAW_NONE);
|
| SetIndexEmptyValue(k,EMPTY_VALUE);
|
| SetIndexLabel(k,names[k]);
|
| }
|
| g_eng.Defaults();
|
| g_eng.swing=InpSwing;
|
| g_eng.atr_period=InpATR;
|
| g_eng.disp_mult=InpDispMult;
|
| g_eng.body_pct=InpBodyPct;
|
| g_eng.search=InpSearch;
|
| g_eng.base_mode=(int)InpBaseMode;
|
| g_eng.zone_mode=(int)InpZoneMode;
|
| g_eng.test_mode=(int)InpTestMode;
|
| g_eng.break_mode=(int)InpBreakMode;
|
| g_eng.max_live=InpMaxLive;
|
| g_eng.trend_only=InpTrendOnly;
|
| g_eng.min_rr=InpMinRR;
|
| g_eng.Reset();
|
| g_style.fill=InpFill;
|
| g_style.show_tested=InpShowTested;
|
| g_style.show_broken=InpShowBroken;
|
| g_style.midline=InpMidline;
|
| g_style.labels=InpLabels;
|
| g_style.swings=InpSwings;
|
| g_style.show_filtered=InpShowFiltered;
|
| g_style.draw_bars=InpDrawBars;
|
| g_style.max_zones=InpMaxZones;
|
| g_style.extend=InpExtendBars;
|
| g_style.dem=InpClrDem;
|
| g_style.sup=InpClrSup;
|
| g_style.broken=InpClrBroken;
|
| g_style.outline=InpClrBroken;
|
| g_draw_stamp=-1;
|
| g_dbg_fail=0;
|
| g_dz_fail=0;
|
| g_no_draw=(IsTesting() && !IsVisualMode());
|
| IndicatorShortName(PRODUCT);
|
| IndicatorDigits(_Digits);
|
| return(INIT_SUCCEEDED);
|
| }
|
|
|
| void OnDeinit(const int reason)
|
| {
|
| ObjectsDeleteAll(0,PFX);
|
| ChartRedraw();
|
| }
|
|
|
| void Redraw(const datetime &time[],const int n)
|
| {
|
| OsBegin();
|
| g_dz_fail=0;
|
| datetime from_t=time[MathMax(0,n-1-InpDrawBars)];
|
| datetime right_t=time[n-1]+(datetime)((long)InpExtendBars*PeriodSeconds(PERIOD_CURRENT));
|
| DzDrawEngine(g_eng,time,PFX,"",from_t,right_t,InpMaxZones,g_style,_Digits);
|
| if(InpShowTrend)
|
| DzTrendLabel(PFX+"trend",g_eng,"",CORNER_RIGHT_UPPER,10,20,InpClrBroken,_Digits);
|
| OsEnd(PFX);
|
| if(g_dz_fail!=g_dbg_fail)
|
| {
|
| g_dbg_fail=g_dz_fail;
|
| CkDbg(StringFormat("the chart refused %d object(s), error %d",g_dz_fail,GetLastError()));
|
| }
|
| ChartRedraw();
|
| }
|
|
|
| //--- the alert text for the closed bar just processed
|
| string Events(const double &o[])
|
| {
|
| string s="";
|
| if(InpAlNew)
|
| {
|
| if(o[DZ_B_DEM_TOP]!=EMPTY_VALUE)
|
| s+="new demand zone "+DoubleToString(o[DZ_B_DEM_BOT],_Digits)+"-"+DoubleToString(o[DZ_B_DEM_TOP],_Digits)+"; ";
|
| if(o[DZ_B_SUP_TOP]!=EMPTY_VALUE)
|
| s+="new supply zone "+DoubleToString(o[DZ_B_SUP_BOT],_Digits)+"-"+DoubleToString(o[DZ_B_SUP_TOP],_Digits)+"; ";
|
| }
|
| if(InpAlTest && o[DZ_B_TESTED]!=EMPTY_VALUE && o[DZ_B_TESTED]>0)
|
| s+=StringFormat("%d fresh zone(s) tested; ",(int)o[DZ_B_TESTED]);
|
| if(InpAlBroken && o[DZ_B_BROKEN]!=EMPTY_VALUE && o[DZ_B_BROKEN]>0)
|
| s+=StringFormat("%d zone(s) broken; ",(int)o[DZ_B_BROKEN]);
|
| return(s);
|
| }
|
|
|
| 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<3)
|
| return(0);
|
| //--- the engine and the drawing count bars from the oldest, as MT5 does
|
| ArraySetAsSeries(time,false);
|
| ArraySetAsSeries(open,false);
|
| ArraySetAsSeries(high,false);
|
| ArraySetAsSeries(low,false);
|
| ArraySetAsSeries(close,false);
|
| if(prev_calculated==0 || g_eng.done>rates_total-2)
|
| g_eng.Reset();
|
| bool moved=false;
|
| for(int i=g_eng.done+1; i<=rates_total-2; i++)
|
| {
|
| g_eng.Step(i,time,open,high,low,close,g_out);
|
| for(int k=0; k<DZ_B_COUNT; k++)
|
| SetB(k,rates_total-1-i,g_out[k]);
|
| moved=true;
|
| if(i==rates_total-2 && prev_calculated>0)
|
| {
|
| string ev=Events(g_out);
|
| if(ev!="")
|
| {
|
| CkDbg("alert for the closed bar "+TimeToString(time[i])+": "+ev);
|
| SignalAlert(PRODUCT,ev,time[i],true);
|
| }
|
| }
|
| }
|
| for(int k=0; k<DZ_B_COUNT; k++)
|
| SetB(k,0,EMPTY_VALUE);
|
| if(g_no_draw) // non-visual Strategy Tester: no chart, so no objects
|
| return(rates_total);
|
| if(moved || g_draw_stamp!=rates_total)
|
| {
|
| Redraw(time,rates_total);
|
| g_draw_stamp=rates_total;
|
| }
|
| return(rates_total);
|
| }
|
| //+------------------------------------------------------------------+
|