//+------------------------------------------------------------------+
//|                                  EngulfingDisplacementHedge.mq5   |
//|                                                          Algobot  |
//|                                     https://www.algobot.live      |
//+------------------------------------------------------------------+
// PURE PRICE-ACTION break-of-structure momentum strategy with a protective,
// failed-breakout HEDGE. No indicators of any kind are used. Every decision is
// derived directly from raw OHLC of the primary timeframe:
//
//   1. BREAK OF STRUCTURE - signal bar CLOSES beyond a recent swing shelf
//      (highest high / lowest low of the last StructureLookback bars).
//   2. ENGULFING PATTERN  - signal bar fully engulfs the prior bar's body in
//      the breakout direction.
//   3. DISPLACEMENT / FVG - a 3-bar imbalance confirms institutional thrust:
//      long  -> bar[3].High < bar[1].Low ; short mirrors. (Toggle RequireFvg.)
//   + MOMENTUM filter - signal-bar body > MomentumBodyFactor * average range.
//
// STOPS/TARGETS are structure-based & fixed (no ATR): stop just beyond the
// signal bar's far extreme + StopBufferPoints; TP = RewardRisk * stop distance.
//
// THE HEDGE: if price reverses HedgeTriggerPoints against an open primary, the
// breakout is failing, so an OPPOSITE position (HedgeLots) is opened with its
// own fixed TP (HedgeRewardPoints) and SL (HedgeStopPoints). When the primary
// finally closes, any surviving hedge is flattened so we return flat.
//
// NOTE: requires a HEDGING account (it holds opposite positions at once).
//+------------------------------------------------------------------+
#property copyright "Algobot"
#property link      "https://www.algobot.live"
#property version   "1.00"
#property strict

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

//--- inputs (mirror C# DescribeParameters defaults) ---
input int    StructureLookback  = 20;    // Swing window (bars) for the S/R shelf
input int    AvgRangePeriod     = 14;    // Window for the average-range momentum yardstick
input double MomentumBodyFactor = 1.0;   // Body must exceed this * average range
input int    RequireFvg         = 1;     // Require 3-bar FVG (1=on, 0=off)
input int    StopBufferPoints   = 50;    // Extra points beyond signal extreme for the stop
input double RewardRisk         = 1.8;   // TP as a multiple of the stop distance
input int    HedgeTriggerPoints = 200;   // Adverse excursion (pts) that triggers the hedge
input int    HedgeRewardPoints  = 300;   // Hedge take-profit distance (points)
input int    HedgeStopPoints    = 400;   // Hedge stop-loss distance (points)
input int    MaxSpreadPoints    = 80;    // Skip entries while spread wider than this
input double HedgeLots          = 0.10;  // Hedge volume
input double Lots               = 0.10;  // Primary volume
input long   Magic              = 8423;  // Primary magic (hedge legs use Magic+1)

long g_hedgeMagic = 0;       // hedge legs are tagged separately (= Magic + 1)
bool g_hedgedThisTrade = false; // one hedge per trade lifecycle; reset when flat

//+------------------------------------------------------------------+
int OnInit()
{
    g_hedgeMagic = Magic + 1;
    trade.SetExpertMagicNumber(Magic);
    g_hedgedThisTrade = false;
    return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
void OnDeinit(const int reason) { }

//+------------------------------------------------------------------+
//| Tick handler - hedge logic runs EVERY tick, entries only on a    |
//| freshly closed bar (matching the C# OnTick structure).           |
//+------------------------------------------------------------------+
void OnTick()
{
    double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
    if(point <= 0) return;

    bool newBar = IsNewBar();

    int primaries = CountPositions(Magic);
    int hedges    = CountPositions(g_hedgeMagic);

    // Manage an active trade every tick; never seek a new entry while exposed.
    if(primaries > 0 || hedges > 0)
    {
        ManageOpenTrade(primaries, hedges, point);
        return;
    }

    g_hedgedThisTrade = false;          // flat -> arm a fresh lifecycle

    // Seek a fresh entry only on a brand-new closed bar.
    if(!newBar) return;
    if(SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) > MaxSpreadPoints) return;
    EvaluateEntry(point);
}

//+------------------------------------------------------------------+
//| Entry: break of structure + engulfing + displacement + momentum  |
//| Bar mapping (C# _bars newest-last  ->  MQL5 shift):              |
//|   c = signal bar = shift 1 ; b = shift 2 ; a (far FVG) = shift 3  |
//+------------------------------------------------------------------+
void EvaluateEntry(double point)
{
    // Need enough history for the structure / average-range / FVG windows.
    int need = MathMax(StructureLookback, AvgRangePeriod) + 3;
    if(Bars(_Symbol, _Period) < need + 1) return;

    // Swing shelf (support / resistance) over the window BEFORE the signal bar
    // -> shifts 2 .. StructureLookback+1 inclusive.
    double resistance = -DBL_MAX, support = DBL_MAX;
    for(int s = 2; s <= StructureLookback + 1; s++)
    {
        double hi = iHigh(_Symbol, _Period, s);
        double lo = iLow(_Symbol, _Period, s);
        if(hi > resistance) resistance = hi;
        if(lo < support)    support    = lo;
    }

    // Average range (momentum yardstick) over shifts 2 .. AvgRangePeriod+1.
    double rangeSum = 0.0;
    for(int s = 2; s <= AvgRangePeriod + 1; s++)
        rangeSum += iHigh(_Symbol, _Period, s) - iLow(_Symbol, _Period, s);
    double avgRange = rangeSum / AvgRangePeriod;
    if(avgRange <= 0) return;

    double cOpen  = iOpen(_Symbol,  _Period, 1);
    double cClose = iClose(_Symbol, _Period, 1);
    double cHigh  = iHigh(_Symbol,  _Period, 1);
    double cLow   = iLow(_Symbol,   _Period, 1);
    double bOpen  = iOpen(_Symbol,  _Period, 2);
    double bClose = iClose(_Symbol, _Period, 2);
    double aHigh  = iHigh(_Symbol,  _Period, 3);
    double aLow   = iLow(_Symbol,   _Period, 3);

    double body = MathAbs(cClose - cOpen);
    bool momentum = body >= MomentumBodyFactor * avgRange;
    if(!momentum) return;

    bool bullEngulf = bClose < bOpen && cClose > cOpen &&
                      cClose >= bOpen && cOpen <= bClose;
    bool bearEngulf = bClose > bOpen && cClose < cOpen &&
                      cClose <= bOpen && cOpen >= bClose;

    bool bullFvg = aHigh < cLow;    // unfilled gap left on the way up
    bool bearFvg = aLow  > cHigh;   // unfilled gap left on the way down
    bool needFvg = RequireFvg != 0;

    double buffer  = StopBufferPoints * point;
    long   stopsLv = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
    double minDist = (stopsLv + 1) * point;

    // ---- LONG: bullish engulfing closing above resistance with a bullish FVG ----
    if(bullEngulf && cClose > resistance && (!needFvg || bullFvg))
    {
        double entry = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        double sl    = cLow - buffer;
        double risk  = entry - sl;
        if(risk < minDist) risk = minDist;
        sl = entry - risk;
        double tp = entry + RewardRisk * risk;
        SendPrimary(0, sl, tp, resistance, bullFvg);
        return;
    }

    // ---- SHORT: bearish engulfing closing below support with a bearish FVG ----
    if(bearEngulf && cClose < support && (!needFvg || bearFvg))
    {
        double entry = SymbolInfoDouble(_Symbol, SYMBOL_BID);
        double sl    = cHigh + buffer;
        double risk  = sl - entry;
        if(risk < minDist) risk = minDist;
        sl = entry + risk;
        double tp = entry - RewardRisk * risk;
        SendPrimary(1, sl, tp, support, bearFvg);
        return;
    }
}

//+------------------------------------------------------------------+
//| Send the primary order (type: 0 = buy, 1 = sell).                |
//+------------------------------------------------------------------+
void SendPrimary(int type, double sl, double tp, double bos, bool fvg)
{
    double vol = NormalizeVolume(Lots);
    if(vol <= 0) return;
    trade.SetExpertMagicNumber(Magic);
    bool ok;
    if(type == 0) ok = trade.Buy (vol, _Symbol, 0.0, Norm(sl), Norm(tp), "EDH entry");
    else          ok = trade.Sell(vol, _Symbol, 0.0, Norm(sl), Norm(tp), "EDH entry");
    PrintFormat("EDH %s entry sl=%.5f tp=%.5f bos=%.5f fvg=%s -> ok=%s ret=%d",
                (type == 0 ? "BUY" : "SELL"), sl, tp, bos,
                (fvg ? "true" : "false"), (ok ? "true" : "false"),
                (int)trade.ResultRetcode());
}

//+------------------------------------------------------------------+
//| Manage the hedge / lifecycle every tick.                         |
//+------------------------------------------------------------------+
void ManageOpenTrade(int primaries, int hedges, double point)
{
    // Primary already closed (its SL/TP fired) but a hedge survives -> flatten
    // it so the basket returns flat and the lifecycle ends cleanly.
    if(primaries == 0)
    {
        CloseAll(g_hedgeMagic);
        return;
    }

    // Already hedged (or a hedge is live) -> let the fixed SL/TP do the work.
    if(g_hedgedThisTrade || hedges > 0) return;

    double priceOpen;
    long   ptype;
    if(!GetFirstPosition(Magic, priceOpen, ptype)) return;

    double adversePts;
    int    hedgeType;        // 0 = buy, 1 = sell
    double hedgeEntry;

    if(ptype == POSITION_TYPE_BUY)
    {
        hedgeEntry = SymbolInfoDouble(_Symbol, SYMBOL_BID);
        adversePts = (priceOpen - hedgeEntry) / point;   // long losing as price falls
        hedgeType  = 1;                                  // sell
    }
    else
    {
        hedgeEntry = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        adversePts = (hedgeEntry - priceOpen) / point;   // short losing as price rises
        hedgeType  = 0;                                  // buy
    }

    if(adversePts < HedgeTriggerPoints) return;          // breakout still holding

    // ---- Failing breakout -> open the protective opposite hedge ----
    long   stopsLv = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
    double minDist = (stopsLv + 1) * point;
    double reward  = MathMax(HedgeRewardPoints * point, minDist);
    double stop    = MathMax(HedgeStopPoints   * point, minDist);
    double tp, sl;
    if(hedgeType == 1) { tp = hedgeEntry - reward; sl = hedgeEntry + stop; }
    else               { tp = hedgeEntry + reward; sl = hedgeEntry - stop; }

    double vol = NormalizeVolume(HedgeLots);
    if(vol <= 0) return;

    trade.SetExpertMagicNumber(g_hedgeMagic);
    bool ok;
    if(hedgeType == 1) ok = trade.Sell(vol, _Symbol, 0.0, Norm(sl), Norm(tp), "EDH hedge");
    else               ok = trade.Buy (vol, _Symbol, 0.0, Norm(sl), Norm(tp), "EDH hedge");
    trade.SetExpertMagicNumber(Magic);   // restore default

    if(ok)
    {
        g_hedgedThisTrade = true;
        PrintFormat("EDH HEDGE %s adverse=%.0fpts entry=%.5f tp=%.5f sl=%.5f",
                    (hedgeType == 1 ? "SELL" : "BUY"), adversePts, hedgeEntry, tp, sl);
    }
}

//+------------------------------------------------------------------+
//| Helpers                                                          |
//+------------------------------------------------------------------+
bool IsNewBar()
{
    static datetime last = 0;
    datetime cur = iTime(_Symbol, _Period, 0);
    if(cur != last) { last = cur; return true; }
    return false;
}

int CountPositions(long magic)
{
    int count = 0;
    for(int i = PositionsTotal() - 1; i >= 0; i--)
    {
        ulong t = PositionGetTicket(i);
        if(PositionSelectByTicket(t) &&
           PositionGetString(POSITION_SYMBOL) == _Symbol &&
           PositionGetInteger(POSITION_MAGIC) == magic) count++;
    }
    return count;
}

// Fetch the first matching position's open price and type.
bool GetFirstPosition(long magic, double &priceOpen, long &ptype)
{
    for(int i = 0; i < PositionsTotal(); i++)
    {
        ulong t = PositionGetTicket(i);
        if(PositionSelectByTicket(t) &&
           PositionGetString(POSITION_SYMBOL) == _Symbol &&
           PositionGetInteger(POSITION_MAGIC) == magic)
        {
            priceOpen = PositionGetDouble(POSITION_PRICE_OPEN);
            ptype     = PositionGetInteger(POSITION_TYPE);
            return true;
        }
    }
    return false;
}

void CloseAll(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) trade.PositionClose(t);
    }
}

// Normalise volume to the symbol's step / limits.
double NormalizeVolume(double lots)
{
    double vstep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
    if(vstep <= 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, _Digits); }
//+------------------------------------------------------------------+
