//+------------------------------------------------------------------+
//|                                  VolatilityContractionBreakout.mq5 |
//|                                                           Algobot |
//|                                       https://www.algobot.live    |
//+------------------------------------------------------------------+
//  Pure price-action breakout. NO indicators.
//
//  Concept (volatility contraction pattern -> box expansion):
//    1. Squeeze test (no ATR/MA): the AVERAGE bar range (High-Low) over the
//       most-recent K bars must be a fraction of the average range over the
//       PRIOR K bars (recentAvg <= ContractionFactor * priorAvg).
//    2. Consolidation BOX: highest high / lowest low of the bars BEHIND the
//       breakout candle. The box edges are the fences energy is stored against.
//    3. Breakout trusted only when the just-completed candle CLOSES beyond a
//       fence (plus buffer = fraction of box height) AND is a strong-bodied
//       candle in the breakout direction (body >= MinBodyPct of its range).
//       Close above box -> long; close below -> short. Symmetric rules.
//
//  Stop is structural (the opposite extreme of the breakout candle); target is
//  a reward:risk multiple. Single timeframe (whatever the chart/test uses).
//
//  Bar mapping vs C# source: the C# list keeps completed bars (newest last) and
//  the breakout candle 'brk' is the newest completed bar = shift 1 here. The box
//  and contraction windows sit BEHIND brk:
//    box           = shifts [2 .. BoxLookback+1]
//    recent window = shifts [2 .. ContractionBars+1]
//    prior window  = shifts [ContractionBars+2 .. 2*ContractionBars+1]
//+------------------------------------------------------------------+
#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    BoxLookback       = 20;    // Bars behind brk that form the box
input int    ContractionBars   = 5;     // Size of each contraction window (K)
input double ContractionFactor = 0.80;  // recentAvg <= factor * priorAvg
input double BreakoutBufferPct = 0.05;  // Close must clear fence by this * boxHeight
input double MinBodyPct        = 0.50;  // Breakout body >= this * its High-Low range
input double RewardRiskRatio   = 2.0;   // TP = RR * structural stop distance
input double Lots              = 0.10;  // Order volume
input long   Magic             = 4209;  // Magic number

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

//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
    // No indicator handles to release (price-action only).
}

//+------------------------------------------------------------------+
//| New-bar detection: act once, when the prior bar has finished.    |
//+------------------------------------------------------------------+
bool IsNewBar()
{
    static datetime last = 0;
    datetime cur = iTime(_Symbol, _Period, 0);
    if(cur != last){ last = cur; return true; }
    return false;
}

//+------------------------------------------------------------------+
//| One position per magic at a time.                                |
//+------------------------------------------------------------------+
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;
}

//+------------------------------------------------------------------+
//| Average High-Low range of 'count' bars starting at 'startShift'. |
//+------------------------------------------------------------------+
double AvgRange(int startShift, int count)
{
    if(count <= 0) return 0.0;
    double sum = 0.0;
    for(int s = startShift; s < startShift + count; s++)
        sum += iHigh(_Symbol, _Period, s) - iLow(_Symbol, _Period, s);
    return sum / count;
}

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

    // Need enough history for the box + both contraction windows (+ slack),
    // plus brk at shift 1 and the forming bar at shift 0.
    int needed = MathMax(BoxLookback, 2 * ContractionBars) + 1;  // bars behind brk
    if(Bars(_Symbol, _Period) < needed + 2) return;

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

    // ---- Breakout candle = newest completed bar (shift 1); box sits BEHIND it ----
    double brkOpen  = iOpen (_Symbol, _Period, 1);
    double brkHigh  = iHigh (_Symbol, _Period, 1);
    double brkLow   = iLow  (_Symbol, _Period, 1);
    double brkClose = iClose(_Symbol, _Period, 1);

    double body  = MathAbs(brkClose - brkOpen);
    double range = brkHigh - brkLow;
    if(range <= 0) return;

    bool strongBody = body >= MinBodyPct * range;
    if(!strongBody) return;

    // ---- Consolidation box: highest high / lowest low of bars BEHIND brk ----
    // Box spans shifts [2 .. BoxLookback+1].
    double boxHigh = -DBL_MAX, boxLow = DBL_MAX;
    for(int s = 2; s <= BoxLookback + 1; s++)
    {
        double h = iHigh(_Symbol, _Period, s);
        double l = iLow (_Symbol, _Period, s);
        if(h > boxHigh) boxHigh = h;
        if(l < boxLow)  boxLow  = l;
    }
    double boxHeight = boxHigh - boxLow;
    if(boxHeight <= 0) return;

    // ---- Volatility-contraction (squeeze) test on the bars behind brk ----
    // recent window = K bars just before brk; prior window = the K before that.
    double recentAvg = AvgRange(2,                    ContractionBars);
    double priorAvg  = AvgRange(2 + ContractionBars,  ContractionBars);
    if(priorAvg <= 0) return;

    bool contracted = recentAvg <= ContractionFactor * priorAvg;
    if(!contracted) return;

    // ---- Breakout direction (close must clear a fence by the buffer) ----
    double buffer    = BreakoutBufferPct * boxHeight;
    bool   brokeUp   = (brkClose > boxHigh + buffer) && (brkClose > brkOpen);
    bool   brokeDown = (brkClose < boxLow  - buffer) && (brkClose < brkOpen);

    if(brokeUp)
        TryEnter(true,  brkLow);   // structural stop = below breakout candle
    else if(brokeDown)
        TryEnter(false, brkHigh);  // structural stop = above breakout candle
}

//+------------------------------------------------------------------+
//| Place the trade. Stop is structural; target is RR * stop dist.   |
//+------------------------------------------------------------------+
void TryEnter(bool isLong, double structuralStop)
{
    double price, sl, tp;

    if(isLong)
    {
        price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        sl    = structuralStop;            // below the breakout candle
        double risk = price - sl;
        if(risk <= 0) return;
        tp    = price + RewardRiskRatio * risk;
    }
    else
    {
        price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
        sl    = structuralStop;            // above the breakout candle
        double risk = sl - price;
        if(risk <= 0) return;
        tp    = price - RewardRiskRatio * risk;
    }

    sl = NormalizeDouble(sl, _Digits);
    tp = NormalizeDouble(tp, _Digits);

    bool ok;
    if(isLong) ok = trade.Buy (Lots, _Symbol, 0.0, sl, tp, "VCB-long");
    else       ok = trade.Sell(Lots, _Symbol, 0.0, sl, tp, "VCB-short");

    PrintFormat("VolatilityContractionBreakout %s @ %.5f SL %.5f TP %.5f -> %s (ret=%d)",
                (isLong ? "LONG" : "SHORT"), price, sl, tp,
                (ok ? "OK" : "FAIL"), trade.ResultRetcode());
}
//+------------------------------------------------------------------+
