//+------------------------------------------------------------------+
//|                                      VolatilityScaledThrust.mq5   |
//|                                                          Algobot  |
//|                                     https://www.algobot.live      |
//+------------------------------------------------------------------+
//  VolatilityScaledThrust - a momentum-ignition breakout built on the
//  Pretty Good Oscillator (PGO):  PGO = (Close - SMA) / ATR.
//
//  The displacement of the close from its own SMA is normalised by ATR,
//  so a single threshold ("thrust") travels across symbols/regimes. An
//  entry only fires when PGO CROSSES the volatility-scaled threshold on
//  this bar (inside the band last bar, outside it now):
//    * LONG  when PGO crosses UP   through +ThrustThreshold
//    * SHORT when PGO crosses DOWN through -ThrustThreshold
//  Exits: ATR-based SL/TP, PLUS an early "exhaustion" exit - a long is
//  closed when PGO falls back through zero (mean reached, impulse spent)
//  and a short when PGO climbs back through zero.
//
//  All evaluation is done on the just-closed bar (shift 1) so signals
//  are on confirmed, non-repainting data.
//+------------------------------------------------------------------+
#property copyright "Algobot"
#property link      "https://www.algobot.live"
#property version   "1.00"
#property strict

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

//--- inputs -------------------------------------------------------------------
input int    BasisPeriod     = 21;    // SMA length defining the "mean" (PGO basis)
input int    AtrPeriod       = 14;    // ATR length normalising the displacement
input double ThrustThreshold = 2.50;  // Displacement (ATR units) a thrust must cross
input double StopAtrMult     = 2.00;  // Stop-loss distance as an ATR multiple
input double TargetAtrMult   = 3.50;  // Take-profit distance as an ATR multiple
input double Lots            = 0.10;  // Position size in lots
input long   Magic           = 1001;  // Magic number

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

//--- PGO cross state ----------------------------------------------------------
double g_prevPgo   = 0.0;
bool   g_hasPrevPgo = false;

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

    g_smaHandle = iMA(_Symbol, _Period, BasisPeriod, 0, MODE_SMA, PRICE_CLOSE);
    g_atrHandle = iATR(_Symbol, _Period, AtrPeriod);

    if(g_smaHandle == INVALID_HANDLE || g_atrHandle == INVALID_HANDLE)
    {
        Print("VST init failed: could not create indicator handles");
        return INIT_FAILED;
    }

    g_prevPgo    = 0.0;
    g_hasPrevPgo = false;
    return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| Expert deinitialization                                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
    if(g_smaHandle != INVALID_HANDLE) IndicatorRelease(g_smaHandle);
    if(g_atrHandle != INVALID_HANDLE) IndicatorRelease(g_atrHandle);
}

//+------------------------------------------------------------------+
//| New-bar gate: act once per newly-closed bar                      |
//+------------------------------------------------------------------+
bool IsNewBar()
{
    static datetime last = 0;
    datetime cur = iTime(_Symbol, _Period, 0);
    if(cur != last){ last = cur; return true; }
    return false;
}

//+------------------------------------------------------------------+
//| Main tick handler                                                |
//+------------------------------------------------------------------+
void OnTick()
{
    // Act once per newly-closed bar: evaluate the bar at shift 1 (just closed).
    if(!IsNewBar()) return;

    // ---- Pull SMA and ATR aligned to the just-closed bar (shift 1) ----
    double smaBuf[];
    double atrBuf[];
    ArraySetAsSeries(smaBuf, true);
    ArraySetAsSeries(atrBuf, true);

    // Need index 0 and 1 -> copy two values starting at the current bar.
    if(CopyBuffer(g_smaHandle, 0, 0, 2, smaBuf) < 2) return;
    if(CopyBuffer(g_atrHandle, 0, 0, 2, atrBuf) < 2) return;

    double sma = smaBuf[1];   // SMA over the last BasisPeriod closed bars ending at shift 1
    double atr = atrBuf[1];   // ATR value at shift 1
    if(atr <= 0.0) return;

    // ---- Pretty Good Oscillator on the just-closed bar ----
    double close = iClose(_Symbol, _Period, 1);
    double pgo   = (close - sma) / atr;

    // Establish the previous reading before we can detect a threshold CROSS.
    if(!g_hasPrevPgo)
    {
        g_prevPgo    = pgo;
        g_hasPrevPgo = true;
        return;
    }
    double prev = g_prevPgo;
    g_prevPgo   = pgo;   // roll forward for next bar (always, even while in a position)

    // ---- Manage an existing position: zero-cross (exhaustion) early exit ----
    if(HasPosition(Magic))
    {
        ManageExhaustion(Magic, pgo);
        return;   // never stack entries on top of a managed position within the same bar
    }

    // ---- Fresh thrust detection: a CROSS through the volatility-scaled threshold ----
    double up = ThrustThreshold;
    double dn = -ThrustThreshold;

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

    // LONG: PGO was at/inside the band last bar and thrust above +Threshold this bar.
    if(prev <= up && pgo > up)
    {
        double entry = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        double sl    = NormalizeDouble(entry - StopAtrMult   * atr, digits);
        double tp    = NormalizeDouble(entry + TargetAtrMult * atr, digits);
        if(trade.Buy(Lots, _Symbol, 0.0, sl, tp, "VST-long"))
            PrintFormat("VST long: PGO thrust %.2f->%.2f above +%.2f, ATR %.5f", prev, pgo, up, atr);
        return;
    }

    // SHORT: PGO was at/inside the band last bar and thrust below -Threshold this bar.
    if(prev >= dn && pgo < dn)
    {
        double entry = SymbolInfoDouble(_Symbol, SYMBOL_BID);
        double sl    = NormalizeDouble(entry + StopAtrMult   * atr, digits);
        double tp    = NormalizeDouble(entry - TargetAtrMult * atr, digits);
        if(trade.Sell(Lots, _Symbol, 0.0, sl, tp, "VST-short"))
            PrintFormat("VST short: PGO thrust %.2f->%.2f below %.2f, ATR %.5f", prev, pgo, dn, atr);
    }
}

//+------------------------------------------------------------------+
//| Is there an open position for this symbol + magic?               |
//+------------------------------------------------------------------+
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;
}

//+------------------------------------------------------------------+
//| Close positions whose momentum has exhausted (PGO back to mean)  |
//|   long  closed when PGO <= 0 (impulse returned to mean)          |
//|   short closed when PGO >= 0                                     |
//+------------------------------------------------------------------+
void ManageExhaustion(long magic, double pgo)
{
    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);
        bool exhaustedLong  = (type == POSITION_TYPE_BUY)  && pgo <= 0.0;
        bool exhaustedShort = (type == POSITION_TYPE_SELL) && pgo >= 0.0;
        if(exhaustedLong || exhaustedShort)
        {
            if(trade.PositionClose(t))
                PrintFormat("VST exit (%s) momentum exhausted: PGO back to %.2f",
                            (type == POSITION_TYPE_BUY ? "Buy" : "Sell"), pgo);
        }
    }
}
//+------------------------------------------------------------------+
