//+------------------------------------------------------------------+
//|                                       EngulfingReversalHedge.mq5  |
//|                                                          Algobot  |
//|                                     https://www.algobot.live      |
//+------------------------------------------------------------------+
// Pure PRICE-ACTION reversal/scalp -- NO indicators of any kind.
// Every decision is derived from raw candle geometry & swing structure.
//
//   PIVOT     : a recent fractal swing low/high (local extreme with
//               PivotSpan strictly lower/higher bars on EACH side).
//   ENGULFING : a textbook 2-candle bullish/bearish engulfing printed
//               at that pivot is the reversal trigger.
//   REVERSAL  : fade the prior leg -> bull engulf @ swing low = buy turn,
//               bear engulf @ swing high = sell turn.
//   BREAKOUT  : do NOT enter on close. Arm a STOP order just beyond the
//               engulfing extreme; fills only if the reversal follows
//               through and BREAKS OUT. Unfilled stops expire after a
//               few bars.
//   HEDGE     : if after filling the breakout proves false and price
//               travels back by HedgeFrac of the pattern range, deploy
//               an opposite MARKET leg -> a genuine locked hedge that
//               rides the adverse move until the basket is banked.
//
// Distances scale to the engulfing candle's own range (zero pip hard-
// coding). A basket money-stop/target govern the hedged pair.
//
// NOTE: the hedge logic requires a HEDGING-mode account so the opposite
//       leg coexists with the base leg instead of netting it out.
//+------------------------------------------------------------------+
#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    PivotSpan       = 3;       // fractal half-width (bars each side of a pivot)
input int    SwingLookback   = 30;      // closed bars scanned for swing pivot & range
input double ZoneFrac        = 0.35;    // engulf extreme must sit within this frac of range from pivot
input double TriggerFrac     = 0.10;    // breakout buffer beyond engulf extreme (x engRange)
input double HedgeFrac       = 0.60;    // adverse travel past fill that flags a FALSE break (x engRange)
input double StopFrac        = 0.50;    // extra base-stop pad beyond the hedge trigger (x engRange)
input double RewardRatio     = 1.50;    // base TP as multiple of base risk
input int    EntryExpiryBars = 3;       // cancel an unfilled breakout stop after this many closed bars
input double BasketTpMoney   = 25.0;    // close WHOLE basket once net floating profit >= this (acct ccy)
input double BasketSlMoney   = 300.0;   // flatten WHOLE basket once net floating loss >= this (acct ccy)
input int    MaxSpreadPoints = 50;      // skip setups while spread(points) exceeds this (0 = off)
input double Lots            = 0.10;    // order volume
input long   Magic           = 4815;    // magic number

//--- live state -----------------------------------------------------
ulong  g_pendingTicket  = 0;   // armed breakout stop order (0 = none)
int    g_pendingAgeBars = 0;   // closed bars elapsed since the stop order was placed
int    g_intendedDir    = 0;   // +1 long / -1 short for the armed-or-filled setup (0 = idle)
double g_engRange       = 0.0; // engulfing candle range -> unit for every live distance
bool   g_hedged         = false; // has the opposite hedge leg been deployed for this base trade?

//+------------------------------------------------------------------+
//| Init                                                             |
//+------------------------------------------------------------------+
int OnInit()
{
   trade.SetExpertMagicNumber(Magic);
   trade.SetTypeFillingBySymbol(_Symbol);

   g_pendingTicket  = 0;
   g_pendingAgeBars = 0;
   g_intendedDir    = 0;
   g_engRange       = 0.0;
   g_hedged         = false;

   if((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE) != ACCOUNT_MARGIN_MODE_RETAIL_HEDGING)
      Print("ERH WARNING: account is not HEDGING-mode; the hedge leg will net against the base position.");

   return INIT_SUCCEEDED;
}

void OnDeinit(const int reason) { }

//+------------------------------------------------------------------+
//| Tick                                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   //--- gather this symbol/magic positions ---
   int    posCount      = 0;
   double floating      = 0.0;
   long   baseType      = -1;
   double basePriceOpen = 0.0;
   for(int i = PositionsTotal()-1; i >= 0; i--)
   {
      ulong t = PositionGetTicket(i);
      if(PositionSelectByTicket(t) &&
         PositionGetString(POSITION_SYMBOL) == _Symbol &&
         PositionGetInteger(POSITION_MAGIC) == Magic)
      {
         posCount++;
         floating     += PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP);
         baseType      = PositionGetInteger(POSITION_TYPE);
         basePriceOpen = PositionGetDouble(POSITION_PRICE_OPEN);
      }
   }

   // ---- A) A base trade is live: basket money-mgmt + intrabar false-break hedge ----
   if(posCount > 0)
   {
      g_pendingTicket  = 0;   // the stop order has filled; it is no longer pending
      g_pendingAgeBars = 0;

      if(floating >=  BasketTpMoney){ FlattenAll(StringFormat("basket TP %.2f", floating)); return; }
      if(floating <= -BasketSlMoney){ FlattenAll(StringFormat("basket SL %.2f", floating)); return; }

      // Deploy the hedge the instant the breakout is proven false.
      if(g_intendedDir != 0 && !g_hedged && posCount == 1)
      {
         double point     = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
         long   stopsLvl  = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
         double hedgeDist = MathMax(HedgeFrac * g_engRange, (stopsLvl + 1) * point);
         double bid       = SymbolInfoDouble(_Symbol, SYMBOL_BID);
         double ask       = SymbolInfoDouble(_Symbol, SYMBOL_ASK);

         if(baseType == POSITION_TYPE_BUY && bid <= basePriceOpen - hedgeDist)
            DeployHedge(false);                 // base long -> ride the drop with a SELL
         else if(baseType == POSITION_TYPE_SELL && ask >= basePriceOpen + hedgeDist)
            DeployHedge(true);                  // base short -> ride the rally with a BUY
      }
      return;
   }

   // ---- B) No position: reset trade flags once everything is closed ----
   if(g_intendedDir != 0 && g_pendingTicket == 0)
   {
      g_intendedDir = 0;
      g_hedged      = false;
   }

   // ---- C) One pass per freshly-closed bar: age the stop order, hunt new setups ----
   if(!IsNewBar()) return;

   // Expire a breakout stop order that has waited too long without filling.
   int pendCount = CountPendings(Magic);
   if(pendCount > 0)
   {
      g_pendingAgeBars++;
      if(g_pendingAgeBars > EntryExpiryBars)
      {
         CancelPendings(Magic);
         g_pendingTicket  = 0;
         g_pendingAgeBars = 0;
         g_intendedDir    = 0;
      }
      return;   // never stack a new setup on top of a live pending order
   }

   // Genuinely flat & idle -> look for the next engulfing-at-swing breakout.
   TrySeekSetup();
}

//+------------------------------------------------------------------+
//| Seek a new engulfing-at-swing breakout setup                     |
//+------------------------------------------------------------------+
void TrySeekSetup()
{
   // Need enough closed history (mirrors n < SwingLookback + 2*PivotSpan + 2).
   if(Bars(_Symbol, _Period) < SwingLookback + 2*PivotSpan + 2) return;
   if(MaxSpreadPoints > 0 && (int)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD) > MaxSpreadPoints) return;

   // The two most-recent CLOSED candles form the engulfing pair.
   //   a = older  -> shift 2     b = newer -> shift 1
   double aOpen  = iOpen (_Symbol, _Period, 2);
   double aClose = iClose(_Symbol, _Period, 2);
   double aHigh  = iHigh (_Symbol, _Period, 2);
   double aLow   = iLow  (_Symbol, _Period, 2);
   double bOpen  = iOpen (_Symbol, _Period, 1);
   double bClose = iClose(_Symbol, _Period, 1);
   double bHigh  = iHigh (_Symbol, _Period, 1);
   double bLow   = iLow  (_Symbol, _Period, 1);

   double engHigh  = MathMax(aHigh, bHigh);
   double engLow   = MathMin(aLow,  bLow);
   double engRange = engHigh - engLow;
   if(engRange <= 0) return;

   // Range over the lookback window (shifts 1..SwingLookback) -> swing-proximity tolerance.
   double hh = -DBL_MAX, ll = DBL_MAX;
   for(int s = 1; s <= SwingLookback; s++)
   {
      double h = iHigh(_Symbol, _Period, s);
      double l = iLow (_Symbol, _Period, s);
      if(h > hh) hh = h;
      if(l < ll) ll = l;
   }
   double range = hh - ll;
   if(range <= 0) return;
   double tol = ZoneFrac * range;

   bool bullEngulf = bClose > bOpen && aClose < aOpen &&
                     bClose >= aOpen && bOpen <= aClose;
   bool bearEngulf = bClose < bOpen && aClose > aOpen &&
                     bClose <= aOpen && bOpen >= aClose;

   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   if(ask <= 0 || bid <= 0) return;
   double vol = NormalizeVolume(Lots);
   if(vol <= 0) return;

   double point   = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   long   stopsLvl= SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   double minDist = (stopsLvl + 1) * point;

   // ---- LONG: bullish engulfing AT a confirmed swing LOW -> arm an upside breakout ----
   if(bullEngulf)
   {
      double swingLow;
      if(FindRecentSwing(true, swingLow) &&
         engLow <= swingLow + tol && engLow >= swingLow - tol)
      {
         double trigger = engHigh + TriggerFrac * engRange;
         if(trigger < ask + minDist) trigger = ask + minDist;          // valid BuyStop distance
         double slDist  = MathMax((HedgeFrac + StopFrac) * engRange, minDist);
         double sl      = trigger - slDist;
         double tp      = trigger + MathMax(RewardRatio * slDist, minDist);
         ArmBreakout(ORDER_TYPE_BUY_STOP, vol, trigger, sl, tp, +1, engRange, "bull engulf @ swing low");
      }
   }
   // ---- SHORT: bearish engulfing AT a confirmed swing HIGH -> arm a downside breakout ----
   else if(bearEngulf)
   {
      double swingHigh;
      if(FindRecentSwing(false, swingHigh) &&
         engHigh >= swingHigh - tol && engHigh <= swingHigh + tol)
      {
         double trigger = engLow - TriggerFrac * engRange;
         if(trigger > bid - minDist) trigger = bid - minDist;          // valid SellStop distance
         double slDist  = MathMax((HedgeFrac + StopFrac) * engRange, minDist);
         double sl      = trigger + slDist;
         double tp      = trigger - MathMax(RewardRatio * slDist, minDist);
         ArmBreakout(ORDER_TYPE_SELL_STOP, vol, trigger, sl, tp, -1, engRange, "bear engulf @ swing high");
      }
   }
}

//+------------------------------------------------------------------+
//| Most-recent CONFIRMED fractal swing within the lookback window.  |
//| A swing low has PivotSpan strictly-lower neighbours each side;   |
//| swing high is the mirror. Returns false if none found.           |
//|   pivot at shift s :  older neighbours = shift s+k,              |
//|                       newer neighbours = shift s-k.              |
//+------------------------------------------------------------------+
bool FindRecentSwing(bool isLow, double &outVal)
{
   outVal = 0.0;
   // Scan from the newest valid pivot candidate (needs PivotSpan bars to its
   // right) outward to the oldest in the lookback window.
   for(int s = PivotSpan + 1; s <= SwingLookback; s++)
   {
      double pivotVal = isLow ? iLow(_Symbol, _Period, s) : iHigh(_Symbol, _Period, s);
      bool   isPivot  = true;
      for(int k = 1; k <= PivotSpan && isPivot; k++)
      {
         if(isLow)
         {
            if(iLow(_Symbol, _Period, s + k) <= pivotVal ||
               iLow(_Symbol, _Period, s - k) <= pivotVal) isPivot = false;
         }
         else
         {
            if(iHigh(_Symbol, _Period, s + k) >= pivotVal ||
               iHigh(_Symbol, _Period, s - k) >= pivotVal) isPivot = false;
         }
      }
      if(isPivot){ outVal = pivotVal; return true; }
   }
   return false;
}

//+------------------------------------------------------------------+
//| Arm a breakout STOP order                                        |
//+------------------------------------------------------------------+
void ArmBreakout(ENUM_ORDER_TYPE type, double vol,
                 double trigger, double sl, double tp,
                 int dir, double engRange, string why)
{
   trigger = Norm(trigger);
   sl      = Norm(sl);
   tp      = Norm(tp);
   string comment = "ERH " + why;

   bool ok = false;
   if(type == ORDER_TYPE_BUY_STOP)
      ok = trade.BuyStop (vol, trigger, _Symbol, sl, tp, ORDER_TIME_GTC, 0, comment);
   else
      ok = trade.SellStop(vol, trigger, _Symbol, sl, tp, ORDER_TIME_GTC, 0, comment);

   if(ok)
   {
      g_pendingTicket  = trade.ResultOrder();
      g_pendingAgeBars = 0;
      g_intendedDir    = dir;
      g_engRange       = engRange;
      g_hedged         = false;
   }
   PrintFormat("ERH ARM %s (%s) trig %s SL %s TP %s -> ok=%d ret=%u",
               EnumToString(type), why,
               DoubleToString(trigger, _Digits),
               DoubleToString(sl, _Digits),
               DoubleToString(tp, _Digits),
               ok, trade.ResultRetcode());
}

//+------------------------------------------------------------------+
//| The breakout failed -> lock an opposite MARKET leg               |
//|   hedgeIsBuy=false -> SELL hedge (base was long)                 |
//|   hedgeIsBuy=true  -> BUY  hedge (base was short)                |
//+------------------------------------------------------------------+
void DeployHedge(bool hedgeIsBuy)
{
   double vol = NormalizeVolume(Lots);
   if(vol <= 0) return;

   double point    = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   long   stopsLvl = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   double minDist  = (stopsLvl + 1) * point;
   double rideDist = MathMax(RewardRatio * HedgeFrac * g_engRange, minDist);
   double slDist   = MathMax((HedgeFrac + StopFrac) * g_engRange, minDist);

   double entry, sl, tp;
   bool   ok = false;

   if(!hedgeIsBuy)   // base was long; breakout failed -> ride the drop
   {
      entry = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      sl    = entry + slDist;
      tp    = entry - rideDist;
      ok    = trade.Sell(vol, _Symbol, 0.0, Norm(sl), Norm(tp), "ERH hedge");
   }
   else              // base was short; breakout failed -> ride the rally
   {
      entry = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      sl    = entry - slDist;
      tp    = entry + rideDist;
      ok    = trade.Buy(vol, _Symbol, 0.0, Norm(sl), Norm(tp), "ERH hedge");
   }

   if(ok) g_hedged = true;
   PrintFormat("ERH HEDGE %s @ %s SL %s TP %s (false break) -> ok=%d ret=%u",
               (hedgeIsBuy ? "Buy" : "Sell"),
               DoubleToString(entry, _Digits),
               DoubleToString(sl, _Digits),
               DoubleToString(tp, _Digits),
               ok, trade.ResultRetcode());
}

//+------------------------------------------------------------------+
//| Close every leg + cancel any stray pending, then reset state     |
//+------------------------------------------------------------------+
void FlattenAll(string why)
{
   int legs = 0;
   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);
         legs++;
      }
   }
   CancelPendings(Magic);

   g_pendingTicket  = 0;
   g_pendingAgeBars = 0;
   g_intendedDir    = 0;
   g_hedged         = false;
   PrintFormat("ERH FLATTEN (%s) legs=%d", why, legs);
}

//+------------------------------------------------------------------+
//| Pending-order helpers (this symbol + magic)                      |
//+------------------------------------------------------------------+
int CountPendings(long magic)
{
   int c = 0;
   for(int i = OrdersTotal()-1; i >= 0; i--)
   {
      ulong t = OrderGetTicket(i);
      if(OrderSelect(t) &&
         OrderGetString(ORDER_SYMBOL) == _Symbol &&
         OrderGetInteger(ORDER_MAGIC) == magic)
         c++;
   }
   return c;
}

void CancelPendings(long magic)
{
   for(int i = OrdersTotal()-1; i >= 0; i--)
   {
      ulong t = OrderGetTicket(i);
      if(OrderSelect(t) &&
         OrderGetString(ORDER_SYMBOL) == _Symbol &&
         OrderGetInteger(ORDER_MAGIC) == magic)
         trade.OrderDelete(t);
   }
}

//+------------------------------------------------------------------+
//| Volume / price normalisation                                     |
//+------------------------------------------------------------------+
double NormalizeVolume(double lots)
{
   double vstep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   if(vstep <= 0) vstep = 0.01;
   double v = MathRound(lots / vstep) * vstep;
   v = NormalizeDouble(v, 2);
   double vmin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double vmax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   if(v < vmin) v = vmin;
   if(v > vmax) v = vmax;
   return v;
}

double Norm(double price){ return NormalizeDouble(price, _Digits); }

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