Files
MQL5Data/Classes/x-saherelm.pivot-detector.class.mq5
T
2026-08-14 07:49:18 +03:30

1038 lines
24 KiB
Plaintext

///////////////////////////////////////////////////////
//
// SaherElm IT Center MQL5 Class
// -------------------------------------------------
// Name: XCPivotDetector
// Description: Forex Pivot Detector with Event System
//
// Maintainer:
// ------------
// Hadi Khazaee Asl (hadi_khazaee_asl@yahoo.com)
//
//////////////////////////////////////////////////////
//
// Global Properties ...
#property library
#property copyright "Copyright 2023, SaherElm IT Center"
#property link "https://saherelm.ir"
#property version "1.00"
#property description "SaherElm Forex Pivot Detector Class"
#property strict
//
// Imports ...
#include "../Classes/x-saherelm.base.class.mq5"
#include "../Classes/x-saherelm.x-alert.class.mq5"
#include "../Libraries/x-saherelm.common.extensions.lib.mq5"
#include "../Libraries/x-saherelm.x-enums.lib.mq5"
#include "../Libraries/x-saherelm.x-poi.extensions.lib.mq5"
#include "../Libraries/x-saherelm.x-poi.lib.mq5"
//
// Definitions ...
typedef void (*TOnPivotZoneEvent)(XBoxZone &box);
//
// Implementation ...
class XCPivotDetector : public XCBase
{
//
public:
//
// Props ...
//
XCAlert *alert; // Alert Handler
XBoxZone peaks[]; // Peaks ...
XBoxZone vales[]; // Vales ...
//
// Event Handlers ...
TOnPivotZoneEvent mOnPeakBreakedHandlers[];
TOnPivotZoneEvent mOnValeBreakedHandlers[];
TOnPivotZoneEvent mOnPeakDetectedHandlers[];
TOnPivotZoneEvent mOnValeDetectedHandlers[];
//
// Constructor ...
XCPivotDetector(
XCAlert *_alert = NULL, // Alert instance Pointer ...
string _pivotPrefix = "XPivot", // Detected Zones Prefix ...
bool _supportInvertedZones = false, // Specified Support Inverted Zone or not ...
int _maxAllowedProcessableZones = 0 // Max Allowed Processable Zones ...
)
{
//
alert = _alert;
//
// Normalize ...
if (_maxAllowedProcessableZones < 0)
{
_maxAllowedProcessableZones = 0;
}
mPivotPrefix = _pivotPrefix;
mSupportInvertedZones = _supportInvertedZones;
mMaxAllowedProcessableZones = _maxAllowedProcessableZones;
//
Initialize();
}
//
// Deconstructor ...
~XCPivotDetector()
{
//
Clean(peaks);
Clean(vales);
//
Clean(mOnPeakDetectedHandlers);
Clean(mOnValeDetectedHandlers);
//
ZeroMemory(alert);
}
// #region Actions ...
void Process(
XOHCL &bar,
int pivotApproved = 2,
int loopback = 20 //
)
{
//
// Validate ...
if (!bar.IsValid())
{
return;
}
//
// Normalize ...
//
// Privot Approve ...
if (pivotApproved < 1)
{
pivotApproved = 1;
}
//
// Loopback ...
if (loopback < pivotApproved * 2)
{
loopback = pivotApproved * 2;
}
// #region Peak/Vale Validation ...
UpdateZones(bar);
// #endregion
// #region Peak/Vale Detection ...
//
// Loop through Loopback ...
XOHCL iBar;
bool has = false;
XBoxZone peakBox;
XBoxZone valeBox;
bool hasPeak = false;
bool hasVale = false;
int start = bar.Index();
double jLow = EMPTY_VALUE;
double jHigh = EMPTY_VALUE;
int end = start + loopback;
double tmpPrice = EMPTY_VALUE;
double tmpZonePrice = EMPTY_VALUE;
for (int i = start; i < end; i++)
{
//
iBar.Clean();
//
// Ignore Loop if not pass Pivot Approve Length ...
if (i < start + pivotApproved)
{
continue;
}
//
// Initialize Bar ...
has = iBar.Init(
bar.symbol,
bar.period,
i //
);
if (!has)
{
continue;
}
//
hasPeak = true;
hasVale = true;
//
// Loop through Bars for Approved Side ...
int jStart = i + 1;
int jEnd = i + pivotApproved + 1;
for (int j = jStart; j < jEnd; j++)
{
//
// Reading High and Low Prices ...
jLow = iLow(iBar.symbol, iBar.period, j);
jHigh = iHigh(iBar.symbol, iBar.period, j);
//
hasVale = hasVale && (iBar.low < jLow);
hasPeak = hasPeak && (iBar.high > jHigh);
has = hasVale || hasPeak;
if (!has)
{
break;
}
}
//
// Check Peak or Vale Exists ...
has = hasPeak || hasVale;
if (!has)
{
//
iBar.Clean();
continue;
}
//
// Loop through Barse for Approved Side ...
jStart = i - pivotApproved;
jEnd = i;
for (int j = jStart; j < jEnd; j++)
{
//
// Reading High and Low Prices ...
jLow = iLow(iBar.symbol, iBar.period, j);
jHigh = iHigh(iBar.symbol, iBar.period, j);
//
if (hasPeak)
{
hasPeak = iBar.high > jHigh;
}
//
if (hasVale)
{
hasVale = iBar.low < jLow;
}
//
has = hasPeak || hasVale;
if (!has)
{
break;
}
}
//
// Check Peak or Vale Exists ...
has = hasPeak || hasVale;
if (!has)
{
//
iBar.Clean();
continue;
}
//
// Handle Peaks ...
if (hasPeak)
{
//
// Detect Lower Price ...
tmpPrice = EMPTY_VALUE;
tmpZonePrice = EMPTY_VALUE;
jStart = i - pivotApproved;
jEnd = i + pivotApproved + 1;
for (int j = jStart; j < jEnd; j++)
{
//
// Detect Max Price of Open/Close ...
tmpPrice = MathMax(
iClose(iBar.symbol, iBar.period, j),
iOpen(iBar.symbol, iBar.period, j) //
);
//
tmpZonePrice =
NotEmpty(tmpZonePrice)
? tmpPrice > tmpZonePrice
? tmpPrice
: tmpZonePrice
: tmpPrice;
}
//
// Initialize Peak Box ...
peakBox.Clean();
has = peakBox.Init(
iBar.symbol,
iBar.period,
iBar.high,
tmpZonePrice,
iBar.time,
bar.time,
X_DIRECTION_BEARISH,
mPivotPrefix //
);
//
HandlePeakDetected(peakBox);
}
//
// Handle Vales ...
if (hasVale)
{
//
// Detect Higher Price ...
tmpPrice = EMPTY_VALUE;
tmpZonePrice = EMPTY_VALUE;
jStart = i - pivotApproved;
jEnd = i + pivotApproved + 1;
for (int j = jStart; j < jEnd; j++)
{
//
// Detect Max Price of Open/Close ...
tmpPrice = MathMin(
iClose(iBar.symbol, iBar.period, j),
iOpen(iBar.symbol, iBar.period, j) //
);
//
tmpZonePrice =
NotEmpty(tmpZonePrice)
? tmpPrice < tmpZonePrice
? tmpPrice
: tmpZonePrice
: tmpPrice;
}
//
// Initialize Vale Box ...
valeBox.Clean();
has = valeBox.Init(
iBar.symbol,
iBar.period,
tmpZonePrice,
iBar.low,
iBar.time,
bar.time,
X_DIRECTION_BULLISH,
mPivotPrefix //
);
//
HandleValeDetected(valeBox);
}
//
iBar.Clean();
peakBox.Clean();
valeBox.Clean();
}
// #endregion
}
bool HasPeaks()
{
return HasChild(peaks);
}
bool HasVales()
{
return HasChild(vales);
}
int GetYoungestPeak()
{
//
int result = -1;
//
if (HasPeaks())
{
result = GetYoungest(peaks);
}
//
return result;
}
int GetYoungestVale()
{
//
int result = -1;
//
if (HasVales())
{
result = GetYoungest(vales);
}
//
return result;
}
// #endregion
// #region Event Listener Registration ...
/**
* Add Peak Breaked Event Handler
*
* @param handler: TOnPivotZoneEvent
*/
void AddOnPeakBreakedHandler(TOnPivotZoneEvent handler)
{
//
if (handler == NULL)
{
return;
}
//
Add(handler, mOnPeakBreakedHandlers);
}
/**
* Add Vale Breaked Event Handler
*
* @param handler: TOnPivotZoneEvent
*/
void AddOnValeBreakedHandler(TOnPivotZoneEvent handler)
{
//
if (handler == NULL)
{
return;
}
//
Add(handler, mOnValeBreakedHandlers);
}
/**
* Add Peak Detected Event Handler
*
* @param handler: TOnPivotZoneEvent
*/
void AddOnPeakDetectedHandler(TOnPivotZoneEvent handler)
{
//
if (handler == NULL)
{
return;
}
//
Add(handler, mOnPeakDetectedHandlers);
}
/**
* Add Vale Detected Event Handler
*
* @param handler: TOnPivotZoneEvent
*/
void AddOnValeDetectedHandler(TOnPivotZoneEvent handler)
{
//
if (handler == NULL)
{
return;
}
//
Add(handler, mOnValeDetectedHandlers);
}
// #endregion
//
protected:
/**
* Initialize Class ...
*/
void Initialize()
{
//
Clean(peaks);
Clean(vales);
//
Clean(mOnPeakDetectedHandlers);
Clean(mOnValeDetectedHandlers);
}
/**
* Handle Peak Box Detection ...
*
* @param peakBox: XBoxZone instance ...
*/
void HandlePeakDetected(
XBoxZone &peakBox //
)
{
//
// Validate ...
if (!peakBox.IsValid())
{
return;
}
//
// Validate Peak Box based on Price ...
if (!IsBoxValid(peakBox, 0, X_BOUNDARY_PRICE_UP_DOWN))
{
return;
}
//
// Update To ...
peakBox.to = TimeCurrent();
//
// Check Peak Exists ...
bool riseEvent = false;
bool hasPeaks = HasChild(peaks);
if (!hasPeaks)
{
//
// Add to Peaks ...
AddRef(peakBox, peaks);
riseEvent = true;
}
else
{
//
// Validate ...
int idx = -1;
hasPeaks = FindIndex(
idx,
peakBox,
peaks //
);
//
// Only Valid if there is not any Peak Registered ...
if (!hasPeaks)
{
//
// Detect Nearest Peak ...
idx = GetYoungest(peaks);
hasPeaks = IsValidIndex(idx);
if (hasPeaks)
{
//
// Validate New Peak based on Youngest One ...
hasPeaks = peakBox.upper < peaks[idx].upper;
if (hasPeaks)
{
//
AddRef(peakBox, peaks);
riseEvent = true;
}
}
}
}
//
// Handle Event ...
if (riseEvent)
{
NotifyPeakDetected(peakBox);
}
//
CleanupPeaks();
}
/**
* Handle Vale Box Detection ...
*
* @param valeBox: XBoxZone instance ...
*/
void HandleValeDetected(
XBoxZone &valeBox //
)
{
//
// Validate ...
if (!valeBox.IsValid())
{
return;
}
//
// Validate Peak Box based on Price ...
if (!IsBoxValid(valeBox, 0, X_BOUNDARY_PRICE_UP_DOWN))
{
return;
}
//
// Update To ...
valeBox.to = TimeCurrent();
//
bool riseEvent = false;
bool hasVales = HasChild(vales);
if (!hasVales)
{
//
// Add to Vales ...
AddRef(valeBox, vales);
riseEvent = true;
}
else
{
//
// Validate ...
int idx = -1;
hasVales = FindIndex(
idx,
valeBox,
vales //
);
//
// Only Valid if there is not any Vale Registered ...
if (!hasVales)
{
//
// Detect Nearest Vale ...
idx = GetYoungest(vales);
hasVales = IsValidIndex(idx);
if (hasVales)
{
//
// Validate New Vale based on Youngest One ...
hasVales = valeBox.lower > vales[idx].lower;
if (hasVales)
{
//
AddRef(valeBox, vales);
riseEvent = true;
}
}
}
}
//
// Handle Event ...
if (riseEvent)
{
NotifyValeDetected(valeBox);
}
//
CleanupVales();
}
// #region Event Notifiers ...
/**
* Notfiy New Peak Breaked ...
*
* @param box: XBoxZone instance ...
*/
void NotifyPeakBreaked(XBoxZone &box)
{
//
// Validate ...
if (!box.IsValid())
{
return;
}
//
// Handle Log ...
if (alert != NULL)
{
//
// Preparing Log Message ...
string message = "Peak Breaked: " + DoubleToString(box.upper, 4) +
" at " + TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS);
//
// Sending Alert ...
alert.SendAlert(message);
}
//
// Multiple Handlers ...
int count = ArraySize(mOnPeakBreakedHandlers);
for (int i = 0; i < count; i++)
{
mOnPeakBreakedHandlers[i](box);
}
}
/**
* Notfiy New Vale Breaked ...
*
* @param box: XBoxZone instance ...
*/
void NotifyValeBreaked(XBoxZone &box)
{
//
// Validate ...
if (!box.IsValid())
{
return;
}
//
// Handle Log ...
if (alert != NULL)
{
//
// Preparing Log Message ...
string message = "Vale Breaked: " + DoubleToString(box.lower, 4) +
" at " + TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS);
//
// Sending Alert ...
alert.SendAlert(message);
}
//
// Multiple Handlers ...
int count = ArraySize(mOnValeBreakedHandlers);
for (int i = 0; i < count; i++)
{
mOnValeBreakedHandlers[i](box);
}
}
/**
* Notfiy New Peak Detected ...
*
* @param box: XBoxZone instance ...
*/
void NotifyPeakDetected(XBoxZone &box)
{
//
// Validate ...
if (!box.IsValid())
{
return;
}
//
// Handle Log ...
if (alert != NULL)
{
//
// Preparing Log Message ...
string message = "Peak Detected: " + DoubleToString(box.upper, 4) +
" at " + TimeToString(box.from, TIME_DATE | TIME_SECONDS);
//
// Sending Alert ...
alert.SendAlert(message);
}
//
// Multiple Handlers ...
int count = ArraySize(mOnPeakDetectedHandlers);
for (int i = 0; i < count; i++)
{
mOnPeakDetectedHandlers[i](box);
}
}
/**
* Notfiy New Vale Detected ...
*
* @param box: XBoxZone instance ...
*/
void NotifyValeDetected(XBoxZone &box)
{
//
// Validate ...
if (!box.IsValid())
{
return;
}
//
// Handle Log ...
if (alert != NULL)
{
//
// Preparing Log Message ...
string message = "Vale Detected: " + DoubleToString(box.lower, 4) +
" at " + TimeToString(box.from, TIME_DATE | TIME_SECONDS);
//
// Sending Alert ...
alert.SendAlert(message);
}
//
// Multiple Handlers ...
int count = ArraySize(mOnValeDetectedHandlers);
for (int i = 0; i < count; i++)
{
mOnValeDetectedHandlers[i](box);
}
}
// #endregion
//
private:
//
// Props ...
string mPivotPrefix; // Prefix ...
bool mSupportInvertedZones; // Support Inverted Zones ...
int mMaxAllowedProcessableZones; // Max Allowed Processable Zones ...
/**
* Handle Apply Max Allowed Processable Peaks ...
*/
void CleanupPeaks()
{
//
if (ArraySize(peaks) <= mMaxAllowedProcessableZones)
{
return;
}
//
Sort(
peaks, // Collection to Sort ...
X_SORT_BY_DATE, // Sorted By ...
X_DIRECTION_BEARISH, // Direction of Sorting ...
mMaxAllowedProcessableZones // Max Allowed ...
);
}
/**
* Handle Apply Max Allowed Processable Vales ...
*/
void CleanupVales()
{
//
if (ArraySize(vales) <= mMaxAllowedProcessableZones)
{
return;
}
//
Sort(
vales, // Collection to Sort ...
X_SORT_BY_DATE, // Sorted By ...
X_DIRECTION_BEARISH, // Direction of Sorting ...
mMaxAllowedProcessableZones // Max Allowed ...
);
}
/**
* if there is Zones in Peaks or Vales
* Update their To Times ...
*
* @param bar: XOHCL instance ...
**/
void UpdateZones(XOHCL &bar)
{
//
// Check Bar is Valid ...
if (bar.IsValid())
{
//
// Retrieve Last Closed Bar if not Provided ...
XOHCL cBar;
if (bar.Index() == 1)
{
cBar = bar;
}
else
{
cBar.Init(bar.symbol, bar.period, 1);
}
//
// Validate CBar ...
// Validate Peaks and Vales ...
if (cBar.IsValid())
{
//
int removeIDX[];
//
if (HasChild(peaks))
{
//
Clean(removeIDX);
for (int i = 0; i < ArraySize(peaks); i++)
{
//
// Validate Zone by Close Price ...
if (cBar.close > peaks[i].upper)
{
//
Add(i, removeIDX);
NotifyPeakBreaked(peaks[i]);
HandleBreakedZone(peaks[i], cBar);
}
}
//
if (HasChild(removeIDX))
{
CleanupArray(removeIDX, peaks);
}
}
//
if (HasChild(vales))
{
//
Clean(removeIDX);
for (int i = 0; i < ArraySize(vales); i++)
{
//
// Validate Zone by Close Price ...
if (cBar.close < vales[i].lower)
{
//
Add(i, removeIDX);
NotifyValeBreaked(vales[i]);
HandleBreakedZone(vales[i], cBar);
}
}
//
if (HasChild(removeIDX))
{
CleanupArray(removeIDX, vales);
}
}
}
//
cBar.Clean();
}
//
datetime tc = TimeCurrent();
//
bool has = HasChild(peaks);
if (has)
{
//
for (int i = 0; i < ArraySize(peaks); i++)
{
peaks[i].to = tc;
}
}
//
has = HasChild(vales);
if (has)
{
//
for (int i = 0; i < ArraySize(vales); i++)
{
vales[i].to = tc;
}
}
//
CleanupPeaks();
CleanupVales();
}
/**
* Handle Breaked Zones ...
*
* @param box: XBoxZone instance ...
* @param bar: XOHCL instance ...
*/
void HandleBreakedZone(
XBoxZone &box,
XOHCL &bar //
)
{
//
// Validate ...
if (!box.IsValid() ||
!mSupportInvertedZones //
)
{
return;
}
//
XBoxZone inverted;
int maxAllowedInversion = 1;
int count = ArraySize(box.breaks);
bool has = count < maxAllowedInversion;
if (!has)
{
return;
}
//
// Invert to Peak ...
if (box.IsBullish())
{
//
// Initialize Inverted Box ...
has = inverted.Init(
box.symbol,
box.period,
box.upper,
box.lower,
bar.time, // From ...
TimeCurrent(), // To ...
X_DIRECTION_BEARISH,
"i" + mPivotPrefix // Type ...
);
if (has)
{
//
Add(bar.time, inverted.breaks);
AddRef(inverted, peaks);
}
}
//
// Invert to Vale ...
else if (box.IsBearish())
{
//
// Initialize Inverted Box ...
has = inverted.Init(
box.symbol,
box.period,
box.upper,
box.lower,
bar.time, // From ...
TimeCurrent(), // To ...
X_DIRECTION_BULLISH,
"i" + mPivotPrefix // Type ...
);
if (has)
{
//
Add(bar.time, inverted.breaks);
AddRef(inverted, vales);
}
}
//
inverted.Clean();
}
};
//