
### FILE: C:\Users\SaherElm\AppData\Roaming\MetaQuotes\Terminal\572C4A1F743028C65C8BA0B780030F0D\MQL5\Classes\x-saherelm.pivot-detector.class.mq5

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

//

### FILE: C:\Users\SaherElm\AppData\Roaming\MetaQuotes\Terminal\572C4A1F743028C65C8BA0B780030F0D\MQL5\Experts\x-saherelm.x-pivot-detector.test.mq5

///////////////////////////////////////////////////////
//
// SaherElm IT Center MQL5 Expert Advisor
// -------------------------------------------------
// Name: XBaseEA
// Description: Session Detector Test Expert Advisor ...
//
// Maintainer:
// ------------
// Hadi Khazaee Asl (hadi_khazaee_asl@yahoo.com)
//
//////////////////////////////////////////////////////
//
// Global Properties ...
#property copyright "Copyright 2023, SaherElm IT Center"
#property link "https://saherelm.ir"
#property version "1.00"
#property icon "../Images/SaherElm_Logo_Color.ico"
#property description "SaherElm Session Detector Test Expert Advisor"
#property strict

// #region Imports ...
#include "../Classes/x-saherelm.pivot-detector.class.mq5"
#include "../Classes/x-saherelm.session-detector.class.mq5"
#include "../Classes/x-saherelm.x-poi.drawer.class.mq5"
// #endregion

// #region Global Variables ...
#define ShortName "XPivotDetectorTestEA"

XCAlert *eaAlert;
XCPOIDrawer *eaDrawer;
XCSessionDetector *eaSessionDetector;

//
XCPivotDetector *eaMajorPivotDetector;
XCPivotDetector *eaMinorPivotDetector;

//
// Last Processing Pivots Time ...
bool eaDrawMajorPivots = true;
bool eaDrawMinorPivots = true;
XPOIDrawConfig eaMajorPivotsDrawConfig;
XPOIDrawConfig eaMinorPivotsDrawConfig;
XPOIDrawConfig eaPointedMajorPivotsDrawConfig;
XPOIDrawConfig eaPointedMinorPivotsDrawConfig;
datetime eaLastMajorPivotProcessing = NULL;
datetime eaLastMinorPivotProcessing = NULL;
// #endregion

// #region Global Event Handlers ...
/**
 * Initialize EA Process ...
 *
 * @return ( int )
 */
int OnInit()
{
    //
    bool result = true;

    //
    result = InitializeRequirements();
    if (!result)
    {
        return INIT_FAILED;
    }

    //
    return INIT_SUCCEEDED;
}

/**
 * De Initialize Expert ...
 *
 * @param  reason: int, DeInitialization Reason ...
 *
 * REASON_PROGRAM 0 The EA has stopped working calling the ExpertRemove() function
 * REASON_REMOVE 1 Program removed from a chart
 * REASON_RECOMPILE 2 Program recompiled
 * REASON_CHARTCHANGE 3 A symbol or a chart period is changed
 * REASON_CHARTCLOSE 4 Chart closed
 * REASON_PARAMETERS 5 Inputs changed by a user
 * REASON_ACCOUNT 6 Another account has been activated or reconnection to the trade server has occurred due to changes in the account settings
 * REASON_TEMPLATE 7 Another chart template applied
 * REASON_INITFAILED 8 The OnInit() handler returned a non-zero value
 * REASON_CLOSE 9 Terminal closed
 *
 */
void OnDeinit(const int reason)
{
    //
    // Destroy Timer ...
    // If Initialized ...

    //
    eaMajorPivotsDrawConfig.Clean();
    eaMinorPivotsDrawConfig.Clean();
    eaPointedMajorPivotsDrawConfig.Clean();
    eaPointedMinorPivotsDrawConfig.Clean();

    //
    // Cleanup Resources ...
    ZeroMemory(eaAlert);
    ZeroMemory(eaDrawer);
    ZeroMemory(eaSessionDetector);
    ZeroMemory(eaMajorPivotDetector);
    ZeroMemory(eaMinorPivotDetector);
}

/**
 * Handle Tick for Processing ...
 */
void OnTick()
{
    //
    // Perform Session Detector Process ...
    eaSessionDetector.Process();

    //
    // Process Pivot Detector ...
    HandleProcessMajorPviots();
    HandleProcessMinorPviots();
}

/**
 * Handle On Chart Event Processing ...

 * @param  id: int, Specified Event ID ...
 * @param  lparam: long, type event parameter
 * @param  dparam: double, type event parameter
 * @param  sparam: string, type event parameter
 */
void OnChartEvent(
    const int id,
    const long &lparam,
    const double &dparam,
    const string &sparam //
)
{
}
// #endregion

// #region Custom Actions ...
/**
 * Initial Requirements of EA ...
 *
 * @return ( bool )
 */
bool InitializeRequirements()
{
    //
    bool result = false;

    //
    eaDrawer = new XCPOIDrawer();

    //
    // Initialize Alert ...
    eaAlert = new XCAlert(
        ShortName,
        false, // Enable Alert ...
        false, // Log Alert ...
        false, // Terminal Alert ...
        false, // Mail Alert ...
        false, // Push Alerts ...
        false  // Save Alerts ...
    );
    result = eaAlert != NULL;
    if (!result)
    {
        return result;
    }

    //
    // Initialize Session Detector ...
    eaSessionDetector = new XCSessionDetector(
        eaAlert, // Alert ...
        true,    // Use Server Time ...
        0        // Timezone Offset ...
    );

    //
    // Register Event Handlers ...
    eaSessionDetector.AddOnSessionEndHandler(OnSessionEndHandler);
    eaSessionDetector.AddOnOverlapEndHandler(OnOverlapEndHandler);
    eaSessionDetector.AddOnSessionStartHandler(OnSessionStartHandler);
    eaSessionDetector.AddOnOverlapStartHandler(OnOverlapStartHandler);
    eaSessionDetector.AddOnSessionStateChangeHandler(OnSessionStateChangeHandler);

    //
    result = eaSessionDetector != NULL;
    if (!result)
    {
        return result;
    }

    //
    // Initialize Major Pivot Detector ...
    eaMajorPivotDetector = new XCPivotDetector(
        eaAlert,        // Alert ...
        "X121SDTMajor", // Pivot Prefix ...
        false,          // Support Inverted Zones ...
        0               // Processable Zones ...
    );

    //
    // Register Event Handlers ...
    eaMajorPivotDetector.AddOnPeakBreakedHandler(OnMajorPeakBreakedHandler);
    eaMajorPivotDetector.AddOnValeBreakedHandler(OnMajorValeBreakedHandler);
    eaMajorPivotDetector.AddOnPeakDetectedHandler(OnMajorPeakDetectedHandler);
    eaMajorPivotDetector.AddOnValeDetectedHandler(OnMajorValeDetectedHandler);

    //
    // Pivot Draw Config ...
    eaMajorPivotsDrawConfig.zonesWidth = 2;
    eaMajorPivotsDrawConfig.zonesFill = false;
    eaMajorPivotsDrawConfig.ignoreZonesAt = true;
    eaMajorPivotsDrawConfig.zonesStyle = STYLE_SOLID;
    eaMajorPivotsDrawConfig.bullishColor = clrAqua;
    eaMajorPivotsDrawConfig.bearishColor = clrMagenta;

    //
    // Pointed Pivot Draw Config ...
    eaPointedMajorPivotsDrawConfig.zonesWidth = 2;
    eaPointedMajorPivotsDrawConfig.zonesFill = false;
    eaPointedMajorPivotsDrawConfig.ignoreZonesAt = true;
    eaPointedMajorPivotsDrawConfig.zonesStyle = STYLE_SOLID;
    eaPointedMajorPivotsDrawConfig.bullishColor = clrYellow;
    eaPointedMajorPivotsDrawConfig.bearishColor = clrOrange;

    //
    result = eaMajorPivotDetector != NULL;
    if (!result)
    {
        return result;
    }

    //
    // Initialize Minor Pivot Detector ...
    eaMinorPivotDetector = new XCPivotDetector(
        eaAlert,        // Alert ...
        "X121SDTMinor", // Pivot Prefix ...
        false,          // Support Inverted Zones ...
        0               // Processable Zones ...
    );

    //
    // Register Event Handlers ...
    eaMinorPivotDetector.AddOnPeakBreakedHandler(OnMinorPeakBreakedHandler);
    eaMinorPivotDetector.AddOnValeBreakedHandler(OnMinorValeBreakedHandler);
    eaMinorPivotDetector.AddOnPeakDetectedHandler(OnMinorPeakDetectedHandler);
    eaMinorPivotDetector.AddOnValeDetectedHandler(OnMinorValeDetectedHandler);

    //
    // Pivot Draw Config ...
    eaMinorPivotsDrawConfig.zonesWidth = 1;
    eaMinorPivotsDrawConfig.zonesFill = false;
    eaMinorPivotsDrawConfig.ignoreZonesAt = true;
    eaMinorPivotsDrawConfig.bearishColor = clrRed;
    eaMinorPivotsDrawConfig.bullishColor = clrLime;
    eaMinorPivotsDrawConfig.zonesStyle = STYLE_DASHDOT;

    //
    // Pointed Pivot Draw Config ...
    eaPointedMinorPivotsDrawConfig.zonesWidth = 2;
    eaPointedMinorPivotsDrawConfig.zonesFill = false;
    eaPointedMinorPivotsDrawConfig.ignoreZonesAt = true;
    eaPointedMinorPivotsDrawConfig.bearishColor = clrOrange;
    eaPointedMinorPivotsDrawConfig.bullishColor = clrYellow;
    eaPointedMinorPivotsDrawConfig.zonesStyle = STYLE_DASHDOT;

    //
    result = eaMinorPivotDetector != NULL;
    if (!result)
    {
        return result;
    }

    //
    return result;
}

/**
 * Process Pivots ...
 */
void HandleProcessMajorPviots()
{
    //
    // Initialize Pivot Processing Bar ...
    XOHCL bar;
    bool has = bar.Init(
        _Symbol,
        PERIOD_H4,
        1 // Last Completed 4H Bar ...
    );
    if (!has)
    {
        return;
    }

    //
    // Validate Passing Bar ...
    has =
        !IsXValid(eaLastMajorPivotProcessing) ||
        bar.time > eaLastMajorPivotProcessing;
    if (!has)
    {
        return;
    }

    //
    eaLastMajorPivotProcessing = bar.time;

    //
    eaMajorPivotDetector.Process(
        bar, // XOHCL Bar ...
        2,   // Pivot Approvation Length ...
        20   // Loopback ...
    );

    //
    // Handle Draw Pivots ...
    if (!eaDrawMajorPivots)
    {
        return;
    }

    //
    XBoxZone tmps[];
    FillNotExists(
        tmps,                       // Dest ...
        eaMajorPivotDetector.peaks, // Source ...
        false                       // Force Clean ...
    );
    FillNotExists(
        tmps,                       // Dest ...
        eaMajorPivotDetector.vales, // Source ...
        false                       // Force Clean ...
    );
    if (HasChild(tmps))
    {
        eaDrawer.DrawZones(
            tmps,                   // Boxes ...
            eaMajorPivotsDrawConfig // Draw Config ...
        );
    }
    XClean(tmps);
}

/**
 * Process Pivots ...
 */
void HandleProcessMinorPviots()
{
    //
    // Initialize Pivot Processing Bar ...
    XOHCL bar;
    bool has = bar.Init(
        _Symbol,
        PERIOD_M15,
        1 // Last Completed 4H Bar ...
    );
    if (!has)
    {
        return;
    }

    //
    // Validate Passing Bar ...
    has =
        !IsXValid(eaLastMinorPivotProcessing) ||
        bar.time > eaLastMinorPivotProcessing;
    if (!has)
    {
        return;
    }

    //
    eaLastMinorPivotProcessing = bar.time;

    //
    eaMinorPivotDetector.Process(
        bar, // XOHCL Bar ...
        5,   // Pivot Approvation Length ...
        40   // Loopback ...
    );

    //
    // Handle Draw Pivots ...
    if (!eaDrawMinorPivots)
    {
        return;
    }

    //
    XBoxZone tmps[];
    FillNotExists(
        tmps,                       // Dest ...
        eaMinorPivotDetector.peaks, // Source ...
        false                       // Force Clean ...
    );
    FillNotExists(
        tmps,                       // Dest ...
        eaMinorPivotDetector.vales, // Source ...
        false                       // Force Clean ...
    );
    if (HasChild(tmps))
    {
        eaDrawer.DrawZones(
            tmps,                   // Boxes ...
            eaMinorPivotsDrawConfig // Draw Config ...
        );
    }
    XClean(tmps);
}
// #endregion

// #region Custom Event Handlers ...

// #region Pivot Detector Event Handlers ...
/**
 * Calls when a Peak Breaked ...
 *
 * @param  box: XBoxZone instance ...
 */
void OnMajorPeakBreakedHandler(XBoxZone &box)
{
    // DebugBreak();
}

/**
 * Calls when a Vale Breaked ...
 *
 * @param  box: XBoxZone instance ...
 */
void OnMajorValeBreakedHandler(XBoxZone &box)
{
    // DebugBreak();
}

/**
 * Calls when a New Peak Detected ...
 *
 * @param  box: XBoxZone instance ...
 */
void OnMajorPeakDetectedHandler(XBoxZone &box)
{
    //
    eaDrawer.DrawZone(
        box, //
        eaPointedMajorPivotsDrawConfig //
    );

    //
    DebugBreak();
}

/**
 * Calls when a New Vale Detected ...
 *
 * @param  box: XBoxZone instance ...
 */
void OnMajorValeDetectedHandler(XBoxZone &box)
{
    //
    eaDrawer.DrawZone(
        box, //
        eaPointedMajorPivotsDrawConfig //
    );

    //
    DebugBreak();
}

/**
 * Calls when a Peak Breaked ...
 *
 * @param  box: XBoxZone instance ...
 */
void OnMinorPeakBreakedHandler(XBoxZone &box)
{
    // DebugBreak();
}

/**
 * Calls when a Vale Breaked ...
 *
 * @param  box: XBoxZone instance ...
 */
void OnMinorValeBreakedHandler(XBoxZone &box)
{
    // DebugBreak();
}

/**
 * Calls when a New Peak Detected ...
 *
 * @param  box: XBoxZone instance ...
 */
void OnMinorPeakDetectedHandler(XBoxZone &box)
{
    //
    eaDrawer.DrawZone(
        box, //
        eaPointedMinorPivotsDrawConfig //
    );

    //
    DebugBreak();
}

/**
 * Calls when a New Vale Detected ...
 *
 * @param  box: XBoxZone instance ...
 */
void OnMinorValeDetectedHandler(XBoxZone &box)
{
    //
    eaDrawer.DrawZone(
        box, //
        eaPointedMinorPivotsDrawConfig //
    );

    //
    DebugBreak();
}
// #endregion

// #region Session Detector Event Handlers ...
/**
 * Handle Session Start Event ...
 *
 * @param  session: ENUM_X_SESSION_TYPE
 * @param  startTime: datetime
 */
void OnSessionStartHandler(
    ENUM_X_SESSION_TYPE session,
    datetime startTime //
)
{
    // Print("Session Started: ", EnumToString(session), " at ", TimeToString(startTime));

    // //
    // // Custom Testing ...
    // if (session == X_SESSION_NEWYORK)
    // {
    //     DebugBreak();
    // }
}

/**
 * Handle Session End Event ...
 *
 * @param  session: ENUM_X_SESSION_TYPE
 * @param  startTime: datetime
 */
void OnSessionEndHandler(
    ENUM_X_SESSION_TYPE session,
    datetime startTime //
)
{
    // Print("Session Ended: ", EnumToString(session), " at ", TimeToString(startTime));

    // //
    // // Custom Testing ...
    // if (session == X_SESSION_NEWYORK)
    // {
    //     DebugBreak();
    // }
}

/**
 * Handle Overlap Start Event ...
 *
 * @param  session: ENUM_X_SESSION_OVERLAP_TYPE
 * @param  startTime: datetime
 */
void OnOverlapStartHandler(
    ENUM_X_SESSION_OVERLAP_TYPE overlap,
    datetime startTime //
)
{
    // Print("Overlap Started: ", EnumToString(overlap), " at ", TimeToString(startTime));

    // //
    // // Custom Testing ...
    // if (overlap == X_OVERLAP_LONDON_NEWYORK)
    // {
    //     DebugBreak();
    // }
}

/**
 * Handle Overlap End Event ...
 *
 * @param  session: ENUM_X_SESSION_OVERLAP_TYPE
 * @param  startTime: datetime
 */
void OnOverlapEndHandler(
    ENUM_X_SESSION_OVERLAP_TYPE overlap,
    datetime startTime //
)
{
    // Print("Overlap Ended: ", EnumToString(overlap), " at ", TimeToString(startTime));

    // //
    // // Custom Testing ...
    // if (overlap == X_OVERLAP_LONDON_NEWYORK)
    // {
    //     DebugBreak();
    // }
}

/**
 * Handle Session State Change Event ...
 *
 * @param  session: ENUM_X_SESSION_TYPE
 * @param  state: ENUM_X_SESSION_STATE
 */
void OnSessionStateChangeHandler(
    ENUM_X_SESSION_TYPE session,
    ENUM_X_SESSION_STATE state //
)
{
    // Print("Session State Changed: ", EnumToString(session), " State: ", EnumToString(state));
}
// #endregion

// #endregion
