//+------------------------------------------------------------------+
//|                            BollingerSqueezeReleaseMomentum.mq5    |
//|                                                          Algobot  |
//|                                    https://www.algobot.live       |
//+------------------------------------------------------------------+
//  Volatility-compression breakout ("squeeze release") system.
//
//  * SQUEEZE ON  - Bollinger Bands (SMA +/- k*stdev of close) sit ENTIRELY
//    INSIDE the Keltner Channels (EMA +/- m*ATR). Narrow BB inside wider KC.
//  * SQUEEZE RELEASE - the bar where the squeeze turns OFF (BB pushes back
//    outside KC): ON one (completed) bar ago, OFF on the latest completed bar.
//  * DIRECTION - least-squares momentum slope of recent completed closes
//    (rising -> long, falling -> short). Non-repainting.
//  * CONFIRMATION - release bar must agree with a long trend-baseline EMA
//    (longs only above it, shorts only below it) AND carry above-average volume.
//
//  Risk: ATR-based hard stop, reward:risk take-profit, and a chandelier
//  trailing stop that ratchets once the trade is 1R (SlAtrMult*ATR) in profit.
//  Lots scale inversely to short-term volatility vs a slower ATR baseline.
//
//  Single timeframe: every read uses _Symbol/_Period, so the EA runs on
//  whatever chart timeframe it is attached to.
//+------------------------------------------------------------------+
#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    BandPeriod      = 20;    // Bollinger/Keltner basis length (SMA & EMA)
input double BbStdMult       = 2.0;   // Bollinger width in standard deviations
input double KcAtrMult       = 1.5;   // Keltner width in ATRs
input int    MomentumPeriod  = 12;    // Least-squares momentum-slope lookback
input int    AtrPeriod       = 14;    // ATR length (Keltner/stops/targets/sizing)
input int    TrendEmaPeriod  = 50;    // Trend baseline EMA
input double VolumeFactor    = 1.10;  // Release volume >= factor * average volume
input double SlAtrMult       = 1.8;   // Initial stop distance in ATRs (also 1R trigger)
input double RewardRiskRatio = 2.0;   // Take-profit as reward:risk multiple of stop
input double TrailAtrMult    = 2.5;   // Chandelier trailing distance in ATRs
input double Lots            = 0.10;  // Base lot size
input long   Magic           = 730220;

//--- indicator handles -----------------------------------------------------
int h_sma   = INVALID_HANDLE;   // Bollinger basis  : SMA(BandPeriod)
int h_std   = INVALID_HANDLE;   // Bollinger stdev  : StdDev(BandPeriod) population
int h_ema   = INVALID_HANDLE;   // Keltner mid      : EMA(BandPeriod)
int h_atr   = INVALID_HANDLE;   // ATR(AtrPeriod)
int h_atr3  = INVALID_HANDLE;   // ATR(AtrPeriod*3) : slower sizing baseline
int h_trend = INVALID_HANDLE;   // Trend baseline   : EMA(TrendEmaPeriod)

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

    h_sma   = iMA(_Symbol, _Period, BandPeriod,     0, MODE_SMA, PRICE_CLOSE);
    h_std   = iStdDev(_Symbol, _Period, BandPeriod, 0, MODE_SMA, PRICE_CLOSE);
    h_ema   = iMA(_Symbol, _Period, BandPeriod,     0, MODE_EMA, PRICE_CLOSE);
    h_atr   = iATR(_Symbol, _Period, AtrPeriod);
    h_atr3  = iATR(_Symbol, _Period, AtrPeriod * 3);
    h_trend = iMA(_Symbol, _Period, TrendEmaPeriod, 0, MODE_EMA, PRICE_CLOSE);

    if(h_sma == INVALID_HANDLE || h_std == INVALID_HANDLE || h_ema == INVALID_HANDLE ||
       h_atr == INVALID_HANDLE || h_atr3 == INVALID_HANDLE || h_trend == INVALID_HANDLE)
    {
        Print("BollingerSqueezeReleaseMomentum: failed to create indicator handles");
        return INIT_FAILED;
    }
    return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
    if(h_sma   != INVALID_HANDLE) IndicatorRelease(h_sma);
    if(h_std   != INVALID_HANDLE) IndicatorRelease(h_std);
    if(h_ema   != INVALID_HANDLE) IndicatorRelease(h_ema);
    if(h_atr   != INVALID_HANDLE) IndicatorRelease(h_atr);
    if(h_atr3  != INVALID_HANDLE) IndicatorRelease(h_atr3);
    if(h_trend != INVALID_HANDLE) IndicatorRelease(h_trend);
}

//+------------------------------------------------------------------+
//| Evaluate once per completed bar.                                 |
//| Algobot appends Bar(...,0) (last closed bar) on a new-bar event; |
//| the MT5 equivalent of that closed bar is shift 1, the bar before |
//| it is shift 2 -- so "curr" = shift 1, "prev" = shift 2.          |
//+------------------------------------------------------------------+
void OnTick()
{
    if(!IsNewBar()) return;

    // Need a full band window PLUS one bar back (prior squeeze state) and a
    // slow ATR baseline for sizing.
    int need = Max4(BandPeriod, TrendEmaPeriod, MomentumPeriod, AtrPeriod * 3) + 2;
    if(Bars(_Symbol, _Period) < need) return;

    double atrArr[];
    if(!CopyN(h_atr, 3, atrArr)) return;
    double atr = atrArr[1];                 // ATR of latest completed bar
    if(atr <= 0) return;

    // Trailing management runs every bar, whether or not we open new trades.
    ManageTrailing(atr);

    // One position per magic at a time; stops/targets handle the exit.
    if(HasPosition(Magic)) return;

    // ---- Squeeze release: ON one bar ago (shift 2), OFF now (shift 1) ----
    double smaArr[], stdArr[], emaArr[];
    if(!CopyN(h_sma, 3, smaArr)) return;
    if(!CopyN(h_std, 3, stdArr)) return;
    if(!CopyN(h_ema, 3, emaArr)) return;

    bool currOn = SqueezeOn(smaArr[1], stdArr[1], emaArr[1], atrArr[1]);
    bool prevOn = SqueezeOn(smaArr[2], stdArr[2], emaArr[2], atrArr[2]);
    bool release = prevOn && !currOn;
    if(!release) return;

    // ---- Direction: least-squares momentum slope of recent closes ----
    double slope = Slope(MomentumPeriod);

    // ---- Trend baseline (market structure) ----
    double trendEma = BufVal(h_trend, 1);
    double close    = iClose(_Symbol, _Period, 1);

    // ---- Volume confirmation ----
    double avgVol = AvgVol(BandPeriod);
    double curVol = (double)iVolume(_Symbol, _Period, 1);
    bool   volOk  = (avgVol <= 0) || (curVol >= VolumeFactor * avgVol);

    bool longSig  = slope > 0 && close > trendEma && volOk;
    bool shortSig = slope < 0 && close < trendEma && volOk;

    if(longSig)       Enter(true,  atr);
    else if(shortSig) Enter(false, atr);
}

//+------------------------------------------------------------------+
//| Bollinger bands fully inside Keltner channels => squeeze on.     |
//| basis = SMA(close), sd = population stdev, kcMid = EMA(close).   |
//+------------------------------------------------------------------+
bool SqueezeOn(double basis, double sd, double kcMid, double atr)
{
    double bbUp = basis + BbStdMult * sd;
    double bbLo = basis - BbStdMult * sd;
    double kcUp = kcMid + KcAtrMult * atr;
    double kcLo = kcMid - KcAtrMult * atr;

    if(sd <= 0 || atr <= 0) return false;
    return bbUp < kcUp && bbLo > kcLo;
}

//+------------------------------------------------------------------+
//| Open a trade with ATR stop, RR target and volatility-scaled lot. |
//+------------------------------------------------------------------+
void Enter(bool isLong, double atr)
{
    double slDist = SlAtrMult * atr;
    if(slDist <= 0) return;

    // Volatility-normalised sizing: shrink lots when short-term ATR is
    // stretched relative to a slower ATR baseline (keeps risk steadier).
    double baseAtr  = BufVal(h_atr3, 1);
    double sizeMult = baseAtr > 0 ? Clamp(baseAtr / atr, 0.5, 2.0) : 1.0;
    double vol      = NormalizeDouble(Lots * sizeMult, 2);
    if(vol < 0.01) vol = 0.01;

    double price, sl, tp;
    bool   ok;
    if(isLong)
    {
        price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        sl    = price - slDist;
        tp    = price + RewardRiskRatio * slDist;
        ok    = trade.Buy(vol, _Symbol, 0.0,
                          NormalizeDouble(sl, _Digits), NormalizeDouble(tp, _Digits),
                          "SqzRelease-L");
    }
    else
    {
        price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
        sl    = price + slDist;
        tp    = price - RewardRiskRatio * slDist;
        ok    = trade.Sell(vol, _Symbol, 0.0,
                           NormalizeDouble(sl, _Digits), NormalizeDouble(tp, _Digits),
                           "SqzRelease-S");
    }

    PrintFormat("BollingerSqueezeReleaseMomentum %s %.2f @ %.5f SL %.5f TP %.5f -> ret=%d",
                isLong ? "LONG" : "SHORT", vol, price, sl, tp, (int)trade.ResultRetcode());
}

//+------------------------------------------------------------------+
//| Chandelier trail: ratchet the stop once a trade is 1R ahead.     |
//+------------------------------------------------------------------+
void ManageTrailing(double atr)
{
    double eps = atr * 0.05;   // ignore trivial adjustments to cut churn
    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);
        double open = PositionGetDouble(POSITION_PRICE_OPEN);
        double sl   = PositionGetDouble(POSITION_SL);
        double tp   = PositionGetDouble(POSITION_TP);

        if(type == POSITION_TYPE_BUY)
        {
            double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
            if(bid - open < SlAtrMult * atr) continue;      // not yet 1R ahead
            double newSl = bid - TrailAtrMult * atr;
            if(newSl > sl + eps && newSl < bid)
                trade.PositionModify(t, NormalizeDouble(newSl, _Digits), tp);
        }
        else if(type == POSITION_TYPE_SELL)
        {
            double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
            if(open - ask < SlAtrMult * atr) continue;      // not yet 1R ahead
            double newSl = ask + TrailAtrMult * atr;
            if((sl == 0.0 || newSl < sl - eps) && newSl > ask)
                trade.PositionModify(t, NormalizeDouble(newSl, _Digits), tp);
        }
    }
}

//+------------------------------------------------------------------+
//| Least-squares slope of the last `period` completed closes.       |
//| x = 0 (oldest, shift=period) .. period-1 (newest, shift=1).      |
//+------------------------------------------------------------------+
double Slope(int period)
{
    if(period < 2 || Bars(_Symbol, _Period) < period + 1) return 0.0;
    double n = period, sx = 0, sy = 0, sxy = 0, sxx = 0;
    for(int k = 0; k < period; k++)
    {
        double x = k;
        double y = iClose(_Symbol, _Period, period - k);   // shift period..1
        sx += x; sy += y; sxy += x * y; sxx += x * x;
    }
    double denom = n * sxx - sx * sx;
    return denom == 0 ? 0.0 : (n * sxy - sx * sy) / denom;
}

//+------------------------------------------------------------------+
//| Mean tick volume of the last `period` completed bars.            |
//+------------------------------------------------------------------+
double AvgVol(int period)
{
    if(period <= 0) return 0.0;
    double sum = 0;
    for(int k = 0; k < period; k++)
        sum += (double)iVolume(_Symbol, _Period, 1 + k);   // shift 1..period
    return sum / period;
}

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

// Copy `count` values (shift 0..count-1) from buffer 0 into a series array.
bool CopyN(int handle, int count, double &out[])
{
    ArraySetAsSeries(out, true);
    return CopyBuffer(handle, 0, 0, count, out) == count;
}

// Single value of buffer 0 at `shift`.
double BufVal(int handle, int shift)
{
    double b[];
    ArraySetAsSeries(b, true);
    if(CopyBuffer(handle, 0, shift, 1, b) < 1) return 0.0;
    return b[0];
}

int    Max4(int a, int b, int c, int d) { return MathMax(MathMax(a, b), MathMax(c, d)); }
double Clamp(double v, double lo, double hi) { return v < lo ? lo : (v > hi ? hi : v); }
//+------------------------------------------------------------------+
