/////////////////////////////////////////////////////// // // 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(); } }; //