#property copyright "Algobot"
#property link      "https://www.algobot.live"
#property version   "1.00"
#property strict

// RelativeVolatilityIndexBreakout
// -------------------------------
// Native MQL5 port of the Algobot C# strategy. Donald Dorsey's Relative Volatility
// Index (RVI) measures the direction of VOLATILITY (not price): a rolling population
// standard deviation is split into "up-move" and "down-move" volatility streams,
// each Wilder-smoothed, then RVI = 100 * avgUp / (avgUp + avgDn). The refined variant
// averages the RVI computed on HIGHs with the RVI computed on LOWs. RVI oscillates
// 0..100 around 50; above 50 => range expanding on up moves (bullish volatility).
//
// The EA trades a HYSTERESIS BREAKOUT of the midline: a long needs RVI to punch up
// through UpperThreshold, a short down through LowerThreshold, each gated by (1) the
// signal bar closing in the trade's direction and (2) a slow trend-EMA regime filter.
// A live position is flattened early if RVI recrosses the 50 midline against it.
// Every entry gets an ATR stop and ATR target. One position per Magic; no pyramiding.
//
// NOTE: MQL5 has no built-in Dorsey RVI (its iRVI is the unrelated Relative Vigor
// Index), so the RVI is computed manually below to match the C# construction exactly.

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

// ----------------------------- inputs -----------------------------
input int    StdPeriod       = 10;      // rolling std-dev lookback feeding the RVI
input int    RviSmoothPeriod = 14;      // Wilder smoothing length for the up/down streams
input double UpperThreshold  = 55.0;    // RVI must break UP through this to arm a long
input double LowerThreshold  = 45.0;    // RVI must break DOWN through this to arm a short
input int    AtrPeriod       = 14;      // ATR window used to size the stop and target
input double StopLossAtr     = 2.00;    // protective stop distance as a multiple of ATR
input double TakeProfitAtr   = 3.00;    // take-profit distance as a multiple of ATR
input int    TrendEmaPeriod  = 100;     // trend regime filter (EMA on closes)
input int    MaxSpreadPoints = 30;      // skip the trade if spread (points) exceeds this
input double Lots            = 0.10;    // base order volume (normalised to lot step/limits)
input long   Magic           = 7412;    // magic number

// --------------------------- indicator handles ---------------------------
int    g_atrHandle = INVALID_HANDLE;
int    g_emaHandle = INVALID_HANDLE;

// --------------------------- state ---------------------------
int    g_digits;
double g_upper, g_lower;   // thresholds after the inverted-band guard
int    g_cap;              // rolling window length for the RVI computation

//+------------------------------------------------------------------+
int OnInit()
{
    // Guard against an inverted band (Lower must sit at/below Upper).
    g_upper = UpperThreshold;
    g_lower = LowerThreshold;
    if(g_lower > g_upper){ double t = g_lower; g_lower = g_upper; g_upper = t; }

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

    // Match the C# buffer cap: enough bars for the RVI (std + smoothing), ATR and EMA,
    // plus headroom so the recursive Wilder smoothing is fully converged.
    int needed = MathMax(MathMax(StdPeriod + RviSmoothPeriod + 4, AtrPeriod + 2), TrendEmaPeriod + 2);
    g_cap = needed + 300;

    trade.SetExpertMagicNumber(Magic);

    g_atrHandle = iATR(_Symbol, _Period, AtrPeriod);
    g_emaHandle = iMA(_Symbol, _Period, TrendEmaPeriod, 0, MODE_EMA, PRICE_CLOSE);
    if(g_atrHandle == INVALID_HANDLE || g_emaHandle == INVALID_HANDLE)
        return INIT_FAILED;

    return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
    if(g_atrHandle != INVALID_HANDLE) IndicatorRelease(g_atrHandle);
    if(g_emaHandle != INVALID_HANDLE) IndicatorRelease(g_emaHandle);
}

//+------------------------------------------------------------------+
//| Population standard deviation of s[end-period+1 .. end].          |
//+------------------------------------------------------------------+
double StdDevAt(const double &s[], int end, int period)
{
    double mean = 0.0;
    for(int k = end - period + 1; k <= end; k++) mean += s[k];
    mean /= period;
    double v = 0.0;
    for(int k = end - period + 1; k <= end; k++){ double d = s[k] - mean; v += d * d; }
    return MathSqrt(v / period);
}

//+------------------------------------------------------------------+
//| Dorsey RVI on ONE price series s[0..count-1] (oldest-first).      |
//| Wilder-smooths the up/down volatility streams; 0..100 (50 if      |
//| undefined). Exact port of the C# ComputeRviOn.                    |
//+------------------------------------------------------------------+
double ComputeRviOn(const double &s[], int count, int stdPeriod, int smoothPeriod)
{
    int first = MathMax(stdPeriod - 1, 1);   // first i with a full std window and a prior bar
    double au = 0.0, ad = 0.0, sumU = 0.0, sumD = 0.0;
    int cnt = 0;
    for(int i = first; i < count; i++)
    {
        double stdv = StdDevAt(s, i, stdPeriod);
        double u = (s[i] > s[i - 1]) ? stdv : 0.0;
        double d = (s[i] < s[i - 1]) ? stdv : 0.0;
        cnt++;
        if(cnt <= smoothPeriod)
        {
            sumU += u; sumD += d;
            if(cnt == smoothPeriod){ au = sumU / smoothPeriod; ad = sumD / smoothPeriod; }
        }
        else
        {
            au = (au * (smoothPeriod - 1) + u) / smoothPeriod;
            ad = (ad * (smoothPeriod - 1) + d) / smoothPeriod;
        }
    }
    if(cnt < smoothPeriod) return 50.0;
    double denom = au + ad;
    return denom > 0.0 ? 100.0 * au / denom : 50.0;
}

//+------------------------------------------------------------------+
//| Refined RVI (avg of RVI-on-highs and RVI-on-lows) for the bar at  |
//| `shift`, over a rolling window of up to g_cap closed bars ending   |
//| at that bar. Returns false if there is not enough data.           |
//+------------------------------------------------------------------+
bool RviAtShift(int shift, double &outRvi)
{
    int bars = Bars(_Symbol, _Period);
    int win  = MathMin(g_cap, bars - shift);
    if(win < StdPeriod + RviSmoothPeriod) return false;   // matches C# warm-up guard

    double highs[], lows[];
    ArrayResize(highs, win);
    ArrayResize(lows,  win);
    // Oldest-first ordering so index (win-1) == the target bar at `shift`, mirroring
    // the C# newest-last List indexing (s[count-1] is the target bar).
    ArraySetAsSeries(highs, false);
    ArraySetAsSeries(lows,  false);

    if(CopyHigh(_Symbol, _Period, shift, win, highs) != win) return false;
    if(CopyLow(_Symbol,  _Period, shift, win, lows)  != win) return false;

    double rh = ComputeRviOn(highs, win, StdPeriod, RviSmoothPeriod);
    double rl = ComputeRviOn(lows,  win, StdPeriod, RviSmoothPeriod);
    outRvi = 0.5 * (rh + rl);
    return true;
}

//+------------------------------------------------------------------+
void OnTick()
{
    if(!IsNewBar()) return;

    int bars = Bars(_Symbol, _Period);
    if(bars < StdPeriod + RviSmoothPeriod + 3) return;   // need RVI at shift 1 and shift 2

    // Current + previous refined-RVI (just-closed bar = shift 1, prior = shift 2).
    double rviNow, rviPrev;
    if(!RviAtShift(1, rviNow))  return;
    if(!RviAtShift(2, rviPrev)) return;

    bool crossUp   = (rviPrev <= g_upper && rviNow > g_upper);   // volatility breaks bullish
    bool crossDown = (rviPrev >= g_lower && rviNow < g_lower);   // volatility breaks bearish

    // ----- Manage an existing position: flatten early on a 50-midline flip against us -----
    if(HasPosition(Magic))
    {
        ManagePosition(rviNow, rviPrev);
        return;   // one position per magic - no new entry while one is live
    }

    if(!crossUp && !crossDown) return;

    // Spread filter (points).
    long spread = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);
    if(spread > MaxSpreadPoints) return;

    // ATR + EMA read at the just-closed signal bar (shift 1), matching the C# buffers
    // whose newest element is that bar.
    double atrBuf[1], emaBuf[1];
    ArraySetAsSeries(atrBuf, true);
    ArraySetAsSeries(emaBuf, true);
    if(CopyBuffer(g_atrHandle, 0, 1, 1, atrBuf) != 1) return;
    if(CopyBuffer(g_emaHandle, 0, 1, 1, emaBuf) != 1) return;
    double atr = atrBuf[0];
    double ema = emaBuf[0];
    if(atr <= 0.0) return;

    double cOpen  = iOpen(_Symbol, _Period, 1);
    double cClose = iClose(_Symbol, _Period, 1);
    bool bullish = cClose > cOpen;
    bool bearish = cClose < cOpen;
    double price = cClose;

    double vol = NormalizeVolume(Lots);

    // ---- LONG: volatility breaks up through the upper band, bar committed up, uptrend ----
    if(crossUp && bullish && price > ema)
    {
        double entry = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        double sl = Norm(entry - StopLossAtr   * atr);
        double tp = Norm(entry + TakeProfitAtr * atr);
        trade.Buy(vol, _Symbol, 0.0, sl, tp, "RVIB long");
        PrintFormat("RVIB LONG rvi %.2f->%.2f x%.0f px>%.5f atr=%.5f sl=%.5f tp=%.5f",
                    rviPrev, rviNow, g_upper, ema, atr, sl, tp);
        return;
    }

    // ---- SHORT: volatility breaks down through the lower band, bar committed down, downtrend ----
    if(crossDown && bearish && price < ema)
    {
        double entry = SymbolInfoDouble(_Symbol, SYMBOL_BID);
        double sl = Norm(entry + StopLossAtr   * atr);
        double tp = Norm(entry - TakeProfitAtr * atr);
        trade.Sell(vol, _Symbol, 0.0, sl, tp, "RVIB short");
        PrintFormat("RVIB SHORT rvi %.2f->%.2f x%.0f px<%.5f atr=%.5f sl=%.5f tp=%.5f",
                    rviPrev, rviNow, g_lower, ema, atr, sl, tp);
    }
}

//+------------------------------------------------------------------+
//| Flatten the (single) position of this magic on a 50-midline flip. |
//+------------------------------------------------------------------+
void ManagePosition(double rviNow, double rviPrev)
{
    for(int i = PositionsTotal() - 1; i >= 0; i--)
    {
        ulong t = PositionGetTicket(i);
        if(!PositionSelectByTicket(t)) continue;
        if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
        if(PositionGetInteger(POSITION_MAGIC) != Magic)   continue;

        long type = PositionGetInteger(POSITION_TYPE);
        if(type == POSITION_TYPE_BUY && rviNow < 50.0 && rviPrev >= 50.0)
        {
            trade.PositionClose(t);
            PrintFormat("RVIB exit LONG on vol-flip rvi=%.2f->%.2f", rviPrev, rviNow);
        }
        else if(type == POSITION_TYPE_SELL && rviNow > 50.0 && rviPrev <= 50.0)
        {
            trade.PositionClose(t);
            PrintFormat("RVIB exit SHORT on vol-flip rvi=%.2f->%.2f", rviPrev, rviNow);
        }
        return;   // mirror C#'s open[0]: act on the first matching position only
    }
}

//+------------------------------------------------------------------+
//| Base lot normalised to the symbol's volume step and min/max.      |
//+------------------------------------------------------------------+
double NormalizeVolume(double lots)
{
    double vstep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
    if(vstep <= 0.0) vstep = 0.01;
    double vmin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
    double vmax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);

    double v = MathRound(lots / vstep) * vstep;
    v = NormalizeDouble(v, 2);
    if(v < vmin) v = vmin;
    if(v > vmax) v = vmax;
    return v;
}

//+------------------------------------------------------------------+
double Norm(double price){ return NormalizeDouble(price, g_digits); }

//+------------------------------------------------------------------+
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;
}
//+------------------------------------------------------------------+
