//+------------------------------------------------------------------+
//| AccumulationConfirmedBreakout.mq5                                |
//| Ported from the Algobot C# strategy of the same name.            |
//|                                                                  |
//| Trend-following Donchian-style channel breakout that only fires  |
//| when RAW PRICE STRUCTURE and CUMULATIVE VOLUME FLOW agree.       |
//| Granville On-Balance Volume (OBV) is used as a leadership /      |
//| confirmation filter: a breakout must be matched by a FRESH OBV   |
//| extreme (accumulation/distribution leading the move), otherwise  |
//| the low-conviction (often false) break is rejected.              |
//|                                                                  |
//|   LONG  : close breaks ABOVE the prior `Channel`-bar high by     |
//|           BreakoutBuffer*ATR AND OBV prints a NEW HIGH vs the    |
//|           prior `ObvLookback` bars.                              |
//|   SHORT : close breaks BELOW the prior `Channel`-bar low by      |
//|           BreakoutBuffer*ATR AND OBV prints a NEW LOW vs the     |
//|           prior `ObvLookback` bars.                              |
//|                                                                  |
//| Risk is fully volatility-scaled: stop = AtrStopMult*ATR beyond   |
//| entry, target = RewardRisk * stop distance. One position per     |
//| magic; the ATR stop / RR target manage every exit.               |
//+------------------------------------------------------------------+
#property copyright "Algobot"
#property link      "https://www.algobot.live"
#property version   "1.00"
#property strict

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

//--- inputs (mirror DescribeParameters in the C# source) -----------
input int    Channel         = 20;      // Price high/low channel lookback (bars)
input int    ObvLookback     = 20;      // OBV must beat this many prior bars (fresh OBV extreme)
input int    AtrPeriod       = 14;      // ATR lookback for buffer, stop and target sizing
input double BreakoutBuffer  = 0.25;    // Close must clear the channel by this * ATR
input double AtrStopMult     = 2.00;    // Stop distance as a multiple of ATR beyond entry
input double RewardRisk      = 2.00;    // Take-profit distance as a multiple of the stop distance
input int    MaxSpreadPoints = 60;      // Skip new entries when spread (points) is wider than this
input double Lots            = 0.10;    // Order volume
input long   Magic           = 8123;    // Magic number

//--- indicator handles ---------------------------------------------
int g_atr = INVALID_HANDLE;
int g_obv = INVALID_HANDLE;

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

    g_atr = iATR(_Symbol, _Period, AtrPeriod);
    g_obv = iOBV(_Symbol, _Period, VOLUME_TICK);   // Granville OBV on tick volume

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

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

//+------------------------------------------------------------------+
//| Act once per newly-closed primary bar.                           |
//+------------------------------------------------------------------+
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 (ctx.OpenPositions(sym, magic).Count > 0).|
//+------------------------------------------------------------------+
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;
}

//+------------------------------------------------------------------+
void OnTick()
{
    // Act once per newly-closed primary bar (the C# guards on bar time).
    if(!IsNewBar()) return;

    // Need enough PRIOR history for every window. The signal bar is shift 1,
    // and each channel/OBV window needs its lookback of bars BEFORE it.
    int need = MathMax(Channel, MathMax(ObvLookback, AtrPeriod)) + 2;
    if(Bars(_Symbol, _Period) < need + 2) return;

    // One position per magic — the ATR stop / RR target manage exits.
    if(HasPosition(Magic)) return;

    // Skip new entries when the current spread is too wide.
    if((int)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) > MaxSpreadPoints) return;

    // The bar that was forming has just closed -> it is now shift 1 (signal bar `c`).
    double cClose = iClose(_Symbol, _Period, 1);

    //--- Channel of the `Channel` bars STRICTLY BEFORE the signal bar ---
    // Newest-last C# window [last-Channel, last) == shifts 2 .. Channel+1 here.
    double entryHigh = -DBL_MAX;
    double entryLow  =  DBL_MAX;
    for(int s = 2; s <= Channel + 1; s++)
    {
        double h = iHigh(_Symbol, _Period, s);
        double l = iLow(_Symbol,  _Period, s);
        if(h > entryHigh) entryHigh = h;
        if(l < entryLow)  entryLow  = l;
    }

    //--- OBV series (index 0 = current forming bar) ---
    double obvBuf[];
    ArraySetAsSeries(obvBuf, true);
    if(CopyBuffer(g_obv, 0, 0, ObvLookback + 2, obvBuf) < ObvLookback + 2) return;

    double obvNow = obvBuf[1];                 // OBV at the signal bar
    // OBV extreme of the `ObvLookback` bars STRICTLY BEFORE the signal bar
    // (shifts 2 .. ObvLookback+1), so the level never "sees itself".
    double priorObvMax = -DBL_MAX;
    double priorObvMin =  DBL_MAX;
    for(int s = 2; s <= ObvLookback + 1; s++)
    {
        double o = obvBuf[s];
        if(o > priorObvMax) priorObvMax = o;
        if(o < priorObvMin) priorObvMin = o;
    }

    //--- Volatility for buffer, stop and target (ATR at the signal bar, shift 1) ---
    double atrBuf[];
    ArraySetAsSeries(atrBuf, true);
    if(CopyBuffer(g_atr, 0, 0, 3, atrBuf) < 3) return;
    double atr = atrBuf[1];
    if(atr <= 0) return;

    double buffer   = BreakoutBuffer * atr;
    double stopDist = AtrStopMult * atr;
    if(stopDist <= 0) return;

    //--- LONG: decisive up-break of structure CONFIRMED by a fresh OBV high ---
    if(cClose > entryHigh + buffer && obvNow > priorObvMax)
    {
        double entry = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        double sl    = NormalizeDouble(entry - stopDist,              _Digits);
        double tp    = NormalizeDouble(entry + stopDist * RewardRisk, _Digits);
        if(entry - sl > 0)
        {
            trade.Buy(Lots, _Symbol, 0.0, sl, tp, "ACB long");
            PrintFormat("ACB LONG  close=%.5f > chan=%.5f+buf obv=%.0f > %.0f sl=%.5f tp=%.5f",
                        cClose, entryHigh, obvNow, priorObvMax, sl, tp);
        }
        return;
    }

    //--- SHORT: decisive down-break of structure CONFIRMED by a fresh OBV low ---
    if(cClose < entryLow - buffer && obvNow < priorObvMin)
    {
        double entry = SymbolInfoDouble(_Symbol, SYMBOL_BID);
        double sl    = NormalizeDouble(entry + stopDist,              _Digits);
        double tp    = NormalizeDouble(entry - stopDist * RewardRisk, _Digits);
        if(sl - entry > 0)
        {
            trade.Sell(Lots, _Symbol, 0.0, sl, tp, "ACB short");
            PrintFormat("ACB SHORT close=%.5f < chan=%.5f-buf obv=%.0f < %.0f sl=%.5f tp=%.5f",
                        cClose, entryLow, obvNow, priorObvMin, sl, tp);
        }
        return;
    }
}
//+------------------------------------------------------------------+
