//+------------------------------------------------------------------+
//|                             ChaikinVolatilityCycleReversal.mq5    |
//|                                                          Algobot  |
//|                                        https://www.algobot.live   |
//+------------------------------------------------------------------+
//  Volatility-driven mean-reversion on CHAIKIN VOLATILITY - the rate-of-
//  change of an EMA of the bar's High-Low spread (a RANGE gauge, no volume):
//
//      HL       = High - Low
//      HLema    = EMA(HL, HlEmaPeriod)
//      ChaikinV = (HLema_now - HLema_[VolRocPeriod ago])
//                 / HLema_[VolRocPeriod ago] * 100
//
//  Chaikin's reading is asymmetric, and that asymmetry is the edge:
//    * BOTTOMS form on CONTRACTING volatility (selling burns out, spread dries).
//    * TOPS    form on EXPANDING volatility  (blow-off climax, spread balloons).
//
//  LONG  (buy a bottom): ChaikinV z <= -Threshold (contraction), price still
//        UNDER a slow baseline, and the just-closed bar RECLAIMS a fast EMA
//        with a bullish body.
//  SHORT (fade a top)  : ChaikinV z >= +Threshold (expansion), price ABOVE the
//        baseline, and the just-closed bar LOSES the fast EMA with a bearish body.
//
//  The z-score over CvLookback bars self-scales "low"/"high" across symbols/TFs.
//  Risk: ATR stop; TP = reward:risk multiple. One position per magic.
//+------------------------------------------------------------------+
#property copyright "Algobot"
#property link      "https://www.algobot.live"
#property version   "1.00"
#property strict

#include <Trade\Trade.mqh>
CTrade trade;

//--- inputs (mirror DescribeParameters) ---------------------------------------
input int    HlEmaPeriod     = 10;    // Smoothing of the High-Low spread before ROC
input int    VolRocPeriod    = 10;    // Look-back for the Chaikin Volatility ROC
input int    CvLookback      = 60;    // Window that normalises CV into a z-score
input double CvZThreshold    = 1.2;   // Std-devs from mean to flag an extreme
input int    PriceEmaPeriod  = 8;     // Fast EMA price must reclaim / lose
input int    BaselinePeriod  = 50;    // Slow baseline (prevailing decline / advance)
input int    AtrPeriod       = 14;    // ATR window
input double AtrStopMult     = 1.8;   // Stop distance in ATRs
input double RewardRiskRatio = 2.0;   // Take-profit as reward:risk multiple
input double Lots            = 0.10;  // Order volume
input long   Magic           = 7420;  // Magic number

//--- indicator handles --------------------------------------------------------
int g_emaFast = INVALID_HANDLE;   // PriceEmaPeriod EMA on close
int g_emaBase = INVALID_HANDLE;   // BaselinePeriod EMA on close
int g_atr     = INVALID_HANDLE;   // ATR

int g_digits  = 5;

//+------------------------------------------------------------------+
int OnInit()
{
    trade.SetExpertMagicNumber(Magic);

    g_digits  = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);

    g_emaFast = iMA(_Symbol, _Period, PriceEmaPeriod, 0, MODE_EMA, PRICE_CLOSE);
    g_emaBase = iMA(_Symbol, _Period, BaselinePeriod, 0, MODE_EMA, PRICE_CLOSE);
    g_atr     = iATR(_Symbol, _Period, AtrPeriod);

    if(g_emaFast == INVALID_HANDLE || g_emaBase == INVALID_HANDLE || g_atr == INVALID_HANDLE)
    {
        Print("ChaikinVolatilityCycleReversal: failed to create indicator handles");
        return INIT_FAILED;
    }
    return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
    if(g_emaFast != INVALID_HANDLE) IndicatorRelease(g_emaFast);
    if(g_emaBase != INVALID_HANDLE) IndicatorRelease(g_emaBase);
    if(g_atr     != INVALID_HANDLE) IndicatorRelease(g_atr);
}

//+------------------------------------------------------------------+
void OnTick()
{
    // Act once per finished bar.
    if(!IsNewBar()) return;
    if(Bars(_Symbol, _Period) < 3) return;

    // One position per magic at a time.
    if(HasPosition(Magic)) return;

    // Fast EMA, baseline EMA and ATR at the just-closed bar (shift 1).
    double emaFastBuf[], emaBaseBuf[], atrBuf[];
    ArraySetAsSeries(emaFastBuf, true);
    ArraySetAsSeries(emaBaseBuf, true);
    ArraySetAsSeries(atrBuf,     true);
    if(CopyBuffer(g_emaFast, 0, 0, 3, emaFastBuf) < 3) return;
    if(CopyBuffer(g_emaBase, 0, 0, 3, emaBaseBuf) < 3) return;
    if(CopyBuffer(g_atr,     0, 0, 3, atrBuf)     < 3) return;

    double ema      = emaFastBuf[1];
    double baseline = emaBaseBuf[1];
    double atr      = atrBuf[1];
    if(ema <= 0.0 || baseline <= 0.0 || atr <= 0.0) return;

    // Standardise the latest Chaikin Volatility over its recent distribution.
    double cvZ;
    if(!ComputeCvZ(cvZ)) return;

    // Just-closed bar (shift 1) and prior bar (shift 2).
    double c0Close = iClose(_Symbol, _Period, 1);
    double c0Open  = iOpen(_Symbol,  _Period, 1);
    double c1Close = iClose(_Symbol, _Period, 2);

    bool bullBody = c0Close > c0Open;
    bool bearBody = c0Close < c0Open;

    // A fast-EMA reclaim / loss on the just-closed bar (single latest EMA scalar).
    bool reclaimUp = (c1Close <= ema && c0Close > ema);
    bool breakDown = (c1Close >= ema && c0Close < ema);

    // LONG : volatility contraction (quiet bottom) still under baseline, turning up.
    bool longSetup  = (cvZ <= -CvZThreshold) && (c0Close < baseline) && reclaimUp && bullBody;
    // SHORT: volatility expansion (climax top) still above baseline, rolling down.
    bool shortSetup = (cvZ >=  CvZThreshold) && (c0Close > baseline) && breakDown && bearBody;

    if(longSetup)       TryEnter(true,  atr, cvZ);
    else if(shortSetup) TryEnter(false, atr, cvZ);
}

//+------------------------------------------------------------------+
//| Z-score of the latest Chaikin Volatility over CvLookback bars.   |
//| Chaikin Volatility = % ROC of an EMA of the High-Low spread over |
//| VolRocPeriod bars. Both EMA readings come from one consistent    |
//| recursion, so "now" and "VolRocPeriod ago" are one recursion     |
//| apart - exactly as the C# source computes them.                  |
//+------------------------------------------------------------------+
bool ComputeCvZ(double &cvZ)
{
    cvZ = 0.0;

    // EMA convergence warm-up ahead of the CV window it must feed.
    int emaWarm = 100;
    int total   = CvLookback + VolRocPeriod + emaWarm;   // HL bars, chronological
    if(Bars(_Symbol, _Period) < total + 2) return false; // need shift 1..total

    // High-Low spread series, oldest first: hl[0]=shift(total) .. hl[total-1]=shift 1.
    double hl[];
    ArrayResize(hl, total);
    for(int k = 0; k < total; k++)
    {
        int shift = total - k;
        hl[k] = iHigh(_Symbol, _Period, shift) - iLow(_Symbol, _Period, shift);
    }

    // EMA of the HL spread (standard alpha, first-value seed washed out by warm-up).
    double alpha = 2.0 / (HlEmaPeriod + 1.0);
    double ema[];
    ArrayResize(ema, total);
    ema[0] = hl[0];
    for(int k = 1; k < total; k++)
        ema[k] = alpha * hl[k] + (1.0 - alpha) * ema[k - 1];

    // Chaikin Volatility series -> last CvLookback values (newest last).
    double cv[];
    ArrayResize(cv, CvLookback);
    int startK = total - CvLookback;
    for(int j = 0; j < CvLookback; j++)
    {
        int    k    = startK + j;
        double past = ema[k - VolRocPeriod];
        cv[j] = (past <= 1e-12) ? 0.0 : (ema[k] - past) / past * 100.0;
    }

    // Population z-score of the most recent CV value (matches C# ZScoreLast).
    int    n    = CvLookback;
    double mean = 0.0;
    for(int i = 0; i < n; i++) mean += cv[i];
    mean /= n;
    double var = 0.0;
    for(int i = 0; i < n; i++) { double d = cv[i] - mean; var += d * d; }
    var /= n;
    double sd = MathSqrt(var);
    if(sd < 1e-12) { cvZ = 0.0; return true; }
    cvZ = (cv[n - 1] - mean) / sd;
    return true;
}

//+------------------------------------------------------------------+
//| Build and send an order with ATR stop and reward:risk target.    |
//+------------------------------------------------------------------+
void TryEnter(bool isLong, double atr, double cvZ)
{
    double point      = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
    long   stopsLevel = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
    double minDist    = (stopsLevel + 1) * point;                 // broker min stop distance
    double risk       = MathMax(AtrStopMult * atr, minDist);

    double vol = NormalizeVolume(Lots);
    if(vol <= 0.0) return;

    if(isLong)
    {
        double price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        double sl    = NormalizeDouble(price - risk, g_digits);
        double tp    = NormalizeDouble(price + RewardRiskRatio * risk, g_digits);
        bool ok = trade.Buy(vol, _Symbol, 0.0, sl, tp, "CVCR-long");
        PrintFormat("ChaikinVolatilityCycleReversal LONG @ %.5f SL %.5f TP %.5f cvZ %.2f ATR %.5f -> %s",
                    price, sl, tp, cvZ, atr, (ok ? "OK" : "FAIL"));
    }
    else
    {
        double price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
        double sl    = NormalizeDouble(price + risk, g_digits);
        double tp    = NormalizeDouble(price - RewardRiskRatio * risk, g_digits);
        bool ok = trade.Sell(vol, _Symbol, 0.0, sl, tp, "CVCR-short");
        PrintFormat("ChaikinVolatilityCycleReversal SHORT @ %.5f SL %.5f TP %.5f cvZ %.2f ATR %.5f -> %s",
                    price, sl, tp, cvZ, atr, (ok ? "OK" : "FAIL"));
    }
}

//+------------------------------------------------------------------+
//| Snap a requested volume to the symbol's step and min/max limits. |
//+------------------------------------------------------------------+
double NormalizeVolume(double lots)
{
    double vstep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
    if(vstep <= 0.0) vstep = 0.01;
    double v = MathRound(lots / vstep) * vstep;
    v = NormalizeDouble(v, 2);
    double vmin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
    double vmax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
    if(v < vmin) v = vmin;
    if(v > vmax) v = vmax;
    return v;
}

//+------------------------------------------------------------------+
bool IsNewBar()
{
    static datetime last = 0;
    datetime cur = iTime(_Symbol, _Period, 0);
    if(cur != last) { last = cur; return true; }
    return false;
}

//+------------------------------------------------------------------+
bool HasPosition(long magic)
{
    for(int i = PositionsTotal() - 1; i >= 0; i--)
    {
        ulong t = PositionGetTicket(i);
        if(PositionSelectByTicket(t) &&
           PositionGetString(POSITION_SYMBOL)  == _Symbol &&
           PositionGetInteger(POSITION_MAGIC)  == magic)
            return true;
    }
    return false;
}
//+------------------------------------------------------------------+
