#property copyright "Algobot"
#property link      "https://www.algobot.live"
#property version   "1.00"
#property strict

//+------------------------------------------------------------------+
//| TestedLevelBreakout                                              |
//| -------------------                                             |
//| Support / Resistance + Breakout-Momentum engine (single TF).    |
//|                                                                 |
//| Horizontal S/R levels are mapped from CONFIRMED fractal swing   |
//| pivots. New pivots within an ATR-scaled tolerance of an existing|
//| level are MERGED (bumping the touch count). A level becomes     |
//| tradable after MinTouches touches; we then trade the MOMENTUM   |
//| breakout of that tested level. Stops sit beyond the broken      |
//| level / breakout-bar extreme (ATR buffer); the take-profit is   |
//| the NEXT mapped level in the breakout direction, else a fixed   |
//| RewardRisk multiple. One position at a time, tagged with Magic. |
//+------------------------------------------------------------------+

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

//--- inputs (mirror of DescribeParameters) ---
input int    Fractal       = 3;     // fractal half-width (bars each side)
input int    MinTouches    = 2;     // touches before a level is tradable
input int    AtrPeriod     = 14;    // ATR period
input double TolAtrMult    = 0.50;  // merge tolerance (x ATR)
input double BreakBuffer   = 0.10;  // break buffer (x ATR)
input double ExpAtrMult    = 1.20;  // expansion threshold (range >= x ATR)
input double RewardRisk    = 2.00;  // fallback reward:risk multiple
input int    LevelLifetime = 200;   // bars before a stale level expires
input double Lots          = 0.10;  // fixed lot size
input long   Magic         = 5117;  // magic number

//--- mapped level ---
struct Level
{
   double price;
   int    touches;
   int    lastBar;
};

Level g_res[];   // mapped swing-high (resistance) levels
Level g_sup[];   // mapped swing-low  (support)    levels

int      g_atr      = INVALID_HANDLE;
int      g_barCount = 0;             // total closed bars seen since start (= C# _barCount)

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

   g_atr = iATR(_Symbol, _Period, AtrPeriod);
   if(g_atr == INVALID_HANDLE)
   {
      Print("Failed to create ATR handle");
      return INIT_FAILED;
   }

   ArrayResize(g_res, 0);
   ArrayResize(g_sup, 0);
   g_barCount = 0;
   return INIT_SUCCEEDED;
}

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

//+------------------------------------------------------------------+
void OnTick()
{
   if(!IsNewBar()) return;          // act once per newly-closed bar
   g_barCount++;                    // matches C# _barCount++ on each new closed bar

   // need = Math.Max(2*Fractal+1, AtrPeriod+1)
   int need = MathMax(2 * Fractal + 1, AtrPeriod + 1);
   if(g_barCount < need) return;    // C#: if (n < need) return;

   // --- ATR of the just-closed bar (newest in the C# _bars list) ---
   double atrbuf[];
   ArraySetAsSeries(atrbuf, true);
   if(CopyBuffer(g_atr, 0, 0, 3, atrbuf) < 3) return;
   double atr = atrbuf[1];          // shift 1 == just-closed bar
   if(atr <= 0) return;

   double tol = TolAtrMult  * atr;
   double buf = BreakBuffer * atr;

   // bar shorthands (C# _bars newest-last -> MT5 shifts)
   //   closed = _bars[n-1] = shift 1   (the bar that just closed)
   //   prev   = _bars[n-2] = shift 2
   double closedHigh  = iHigh (_Symbol, _Period, 1);
   double closedLow   = iLow  (_Symbol, _Period, 1);
   double closedClose = iClose(_Symbol, _Period, 1);
   double prevClose   = iClose(_Symbol, _Period, 2);

   // ---- map newly CONFIRMED fractal pivots (Fractal bars back from newest) ----
   //   C#: p = n-1-Fractal  ->  MT5 shift = 1 + Fractal (the candidate pivot)
   //       pivotBar = _barCount - Fractal
   int center   = 1 + Fractal;
   int pivotBar = g_barCount - Fractal;
   if(IsSwingHigh(center, Fractal)) Register(g_res, iHigh(_Symbol, _Period, center), pivotBar, tol);
   if(IsSwingLow (center, Fractal)) Register(g_sup, iLow (_Symbol, _Period, center), pivotBar, tol);

   // ---- expire stale levels ----
   Expire(g_res);
   Expire(g_sup);

   if(HasPosition(Magic)) return;   // one position at a time

   bool expansion = (closedHigh - closedLow) >= ExpAtrMult * atr;
   if(!expansion) return;

   // ---- BUY: momentum breakout of a tested resistance ----
   bool   foundR  = false;
   double brokenR = 0.0;
   for(int i = 0; i < ArraySize(g_res); i++)
   {
      if(g_res[i].touches >= MinTouches &&
         prevClose <= g_res[i].price &&
         closedClose > g_res[i].price + buf)
      {
         if(!foundR || g_res[i].price > brokenR) brokenR = g_res[i].price;  // strongest cleared shelf
         foundR = true;
      }
   }

   if(foundR)
   {
      double entry = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      double sl    = MathMin(brokenR, closedLow) - buf;
      double risk  = entry - sl;
      if(risk > 0)
      {
         double tp = NextAbove(closedClose + buf, entry, risk);
         trade.Buy(Lots, _Symbol, 0.0,
                   NormalizeDouble(sl, _Digits),
                   NormalizeDouble(tp, _Digits),
                   "TestedLevelBreakout-L");
         RemoveBelow(g_res, closedClose);   // broken resistance flips off the map
         return;
      }
   }

   // ---- SELL: momentum breakdown of a tested support ----
   bool   foundS  = false;
   double brokenS = 0.0;
   for(int i = 0; i < ArraySize(g_sup); i++)
   {
      if(g_sup[i].touches >= MinTouches &&
         prevClose >= g_sup[i].price &&
         closedClose < g_sup[i].price - buf)
      {
         if(!foundS || g_sup[i].price < brokenS) brokenS = g_sup[i].price;  // strongest cleared shelf
         foundS = true;
      }
   }

   if(foundS)
   {
      double entry = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      double sl    = MathMax(brokenS, closedHigh) + buf;
      double risk  = sl - entry;
      if(risk > 0)
      {
         double tp = NextBelow(closedClose - buf, entry, risk);
         trade.Sell(Lots, _Symbol, 0.0,
                    NormalizeDouble(sl, _Digits),
                    NormalizeDouble(tp, _Digits),
                    "TestedLevelBreakout-S");
         RemoveAbove(g_sup, closedClose);   // broken support flips off the map
      }
   }
}

//+------------------------------------------------------------------+
//| strict swing-high: High[center] is the unique max [center-f,+f]  |
//+------------------------------------------------------------------+
bool IsSwingHigh(int center, int f)
{
   double h = iHigh(_Symbol, _Period, center);
   for(int s = center - f; s <= center + f; s++)
   {
      if(s == center) continue;
      if(iHigh(_Symbol, _Period, s) >= h) return false;
   }
   return true;
}

bool IsSwingLow(int center, int f)
{
   double lo = iLow(_Symbol, _Period, center);
   for(int s = center - f; s <= center + f; s++)
   {
      if(s == center) continue;
      if(iLow(_Symbol, _Period, s) <= lo) return false;
   }
   return true;
}

//+------------------------------------------------------------------+
//| merge a pivot into the nearest level within tol, else add fresh  |
//+------------------------------------------------------------------+
void Register(Level &list[], double price, int barNo, double tol)
{
   int n = ArraySize(list);
   for(int i = 0; i < n; i++)
   {
      if(MathAbs(list[i].price - price) <= tol)
      {
         list[i].price   = (list[i].price * list[i].touches + price) / (list[i].touches + 1);
         list[i].touches = list[i].touches + 1;
         list[i].lastBar = barNo;
         return;
      }
   }
   ArrayResize(list, n + 1);
   list[n].price   = price;
   list[n].touches = 1;
   list[n].lastBar = barNo;
   if(ArraySize(list) > 128) RemoveAt(list, 0);
}

//+------------------------------------------------------------------+
void Expire(Level &list[])
{
   for(int i = ArraySize(list) - 1; i >= 0; i--)
      if(g_barCount - list[i].lastBar > LevelLifetime) RemoveAt(list, i);
}

//+------------------------------------------------------------------+
//| nearest mapped resistance strictly above floor; level-to-level   |
//| TP, else RewardRisk fallback                                     |
//+------------------------------------------------------------------+
double NextAbove(double floorp, double entry, double risk)
{
   double best = DBL_MAX;
   for(int i = 0; i < ArraySize(g_res); i++)
      if(g_res[i].price > floorp && g_res[i].price < best) best = g_res[i].price;
   if(best != DBL_MAX && best - entry >= risk) return best;
   return entry + RewardRisk * risk;
}

double NextBelow(double ceilp, double entry, double risk)
{
   double best = -DBL_MAX;
   for(int i = 0; i < ArraySize(g_sup); i++)
      if(g_sup[i].price < ceilp && g_sup[i].price > best) best = g_sup[i].price;
   if(best != -DBL_MAX && entry - best >= risk) return best;
   return entry - RewardRisk * risk;
}

//+------------------------------------------------------------------+
void RemoveBelow(Level &list[], double price)
{
   for(int i = ArraySize(list) - 1; i >= 0; i--)
      if(list[i].price < price) RemoveAt(list, i);
}

void RemoveAbove(Level &list[], double price)
{
   for(int i = ArraySize(list) - 1; i >= 0; i--)
      if(list[i].price > price) RemoveAt(list, i);
}

//+------------------------------------------------------------------+
//| remove element idx from a dynamic Level array (order preserved)  |
//+------------------------------------------------------------------+
void RemoveAt(Level &list[], int idx)
{
   int n = ArraySize(list);
   if(idx < 0 || idx >= n) return;
   for(int i = idx; i < n - 1; i++) list[i] = list[i + 1];
   ArrayResize(list, n - 1);
}

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

//+------------------------------------------------------------------+
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;
}
//+------------------------------------------------------------------+
