//+------------------------------------------------------------------+
//|                                               ZenithGridEA.mq5   |
//| Intelligent Trend Grid EA for XAUUSD - MT5                      |
//| Version 1.00                                                    |
//+------------------------------------------------------------------+
#property strict
#property version   "1.00"
#property description "Trend-following adaptive grid with basket management and DD protection."

#include <Trade/Trade.mqh>

CTrade trade;

//============================== INPUTS ==============================//
input group "=== GENERAL ==="
input ulong  InpMagicNumber          = 5302700;
input string InpTradeComment         = "ZENITH GRID";
input bool   InpAllowBuy             = true;
input bool   InpAllowSell            = true;
input int    InpMaxPositions         = 8;
input int    InpDeviationPoints      = 30;

input group "=== TREND FILTER ==="
input ENUM_TIMEFRAMES InpTrendTF     = PERIOD_H4;
input int    InpFastEMA              = 50;
input int    InpSlowEMA              = 200;
input int    InpADXPeriod             = 14;
input double InpMinADX               = 18.0;
input bool   InpUseSidewaysFilter    = true;

input group "=== GRID / VOLATILITY ==="
input ENUM_TIMEFRAMES InpATRTimeframe = PERIOD_M15;
input int    InpATRPeriod            = 14;
input double InpATRMultiplier        = 1.20;
input double InpMinGridPoints        = 500.0;
input double InpMaxGridPoints        = 2500.0;
input bool   InpDynamicGrid          = true;

input group "=== MONEY MANAGEMENT ==="
input double InpStartLot             = 0.01;
input bool   InpUseLotMultiplier     = true;
input double InpLotMultiplier        = 1.25;
input double InpMaxLot               = 0.50;

input group "=== BASKET EXIT ==="
input bool   InpUseBasketMoneyTarget = true;
input double InpBasketProfitMoney    = 5.00;
input bool   InpUseBasketPriceTarget = false;
input double InpBasketTargetPoints   = 500.0;

input group "=== PROTECTION ==="
input double InpMaxDrawdownPercent   = 15.0;
input double InpEmergencyDDPercent   = 25.0;
input bool   InpCloseAllOnEmergency  = true;
input int    InpCooldownMinutes      = 30;
input bool   InpUseEquityStop        = true;
input double InpMaxDailyLossPercent  = 8.0;

input group "=== SESSION FILTER (BROKER SERVER TIME) ==="
input bool   InpUseSessionFilter     = true;
input int    InpStartHour            = 7;
input int    InpEndHour              = 22;

input group "=== ENTRY ==="
input bool   InpOneEntryPerBar       = true;
input ENUM_TIMEFRAMES InpEntryTF     = PERIOD_M5;
input bool   InpUseRSIConfirmation   = true;
input int    InpRSIPeriod            = 14;
input double InpBuyRSIMin            = 45.0;
input double InpSellRSIMax           = 55.0;

//============================== GLOBALS =============================//
int hFastEMA = INVALID_HANDLE;
int hSlowEMA = INVALID_HANDLE;
int hADX     = INVALID_HANDLE;
int hATR     = INVALID_HANDLE;
int hRSI     = INVALID_HANDLE;

datetime g_lastEntryBar = 0;
datetime g_pauseUntil   = 0;
double   g_dayStartEquity = 0.0;
int      g_dayOfYear = -1;

enum TrendState
{
   TREND_SIDEWAYS = 0,
   TREND_UP       = 1,
   TREND_DOWN     = -1
};

//============================== HELPERS =============================//
double NormalizeVolume(double volume)
{
   double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   double step   = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);

   volume = MathMax(minLot, MathMin(volume, MathMin(maxLot, InpMaxLot)));
   if(step > 0.0)
      volume = MathFloor(volume / step) * step;

   return NormalizeDouble(volume, 2);
}

bool GetBufferValue(const int handle, const int shift, double &value)
{
   double buffer[];
   ArraySetAsSeries(buffer, true);
   if(CopyBuffer(handle, 0, shift, 1, buffer) < 1)
      return false;
   value = buffer[0];
   return true;
}

TrendState GetTrend()
{
   double fast, slow, adx;
   if(!GetBufferValue(hFastEMA, 1, fast)) return TREND_SIDEWAYS;
   if(!GetBufferValue(hSlowEMA, 1, slow)) return TREND_SIDEWAYS;
   if(!GetBufferValue(hADX, 1, adx)) return TREND_SIDEWAYS;

   if(InpUseSidewaysFilter && adx < InpMinADX)
      return TREND_SIDEWAYS;

   if(fast > slow) return TREND_UP;
   if(fast < slow) return TREND_DOWN;
   return TREND_SIDEWAYS;
}

double GetGridDistancePrice()
{
   double points = InpMinGridPoints;

   if(InpDynamicGrid)
   {
      double atr;
      if(GetBufferValue(hATR, 1, atr))
         points = (atr / _Point) * InpATRMultiplier;
   }

   points = MathMax(InpMinGridPoints, MathMin(points, InpMaxGridPoints));
   return points * _Point;
}

bool IsTradingSession()
{
   if(!InpUseSessionFilter)
      return true;

   MqlDateTime tm;
   TimeToStruct(TimeCurrent(), tm);

   if(InpStartHour == InpEndHour)
      return true;

   if(InpStartHour < InpEndHour)
      return (tm.hour >= InpStartHour && tm.hour < InpEndHour);

   // Session crossing midnight
   return (tm.hour >= InpStartHour || tm.hour < InpEndHour);
}

void UpdateDayStartEquity()
{
   MqlDateTime tm;
   TimeToStruct(TimeCurrent(), tm);

   if(tm.day_of_year != g_dayOfYear)
   {
      g_dayOfYear = tm.day_of_year;
      g_dayStartEquity = AccountInfoDouble(ACCOUNT_EQUITY);
   }
}

bool DailyLossExceeded()
{
   if(!InpUseEquityStop || g_dayStartEquity <= 0.0)
      return false;

   double equity = AccountInfoDouble(ACCOUNT_EQUITY);
   double lossPct = ((g_dayStartEquity - equity) / g_dayStartEquity) * 100.0;
   return (lossPct >= InpMaxDailyLossPercent);
}

double CurrentDrawdownPercent()
{
   double balance = AccountInfoDouble(ACCOUNT_BALANCE);
   double equity  = AccountInfoDouble(ACCOUNT_EQUITY);

   if(balance <= 0.0)
      return 0.0;

   return MathMax(0.0, ((balance - equity) / balance) * 100.0);
}

int CountPositions(const ENUM_POSITION_TYPE type = WRONG_VALUE)
{
   int count = 0;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;

      if(PositionGetString(POSITION_SYMBOL) != _Symbol)
         continue;
      if((ulong)PositionGetInteger(POSITION_MAGIC) != InpMagicNumber)
         continue;

      ENUM_POSITION_TYPE ptype = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      if(type == WRONG_VALUE || ptype == type)
         count++;
   }

   return count;
}

double TotalProfit()
{
   double total = 0.0;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;

      if(PositionGetString(POSITION_SYMBOL) != _Symbol)
         continue;
      if((ulong)PositionGetInteger(POSITION_MAGIC) != InpMagicNumber)
         continue;

      total += PositionGetDouble(POSITION_PROFIT)
            + PositionGetDouble(POSITION_SWAP)
            + PositionGetDouble(POSITION_COMMISSION);
   }
   return total;
}

double TotalVolume(const ENUM_POSITION_TYPE type)
{
   double total = 0.0;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;

      if(PositionGetString(POSITION_SYMBOL) != _Symbol)
         continue;
      if((ulong)PositionGetInteger(POSITION_MAGIC) != InpMagicNumber)
         continue;
      if((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE) != type)
         continue;

      total += PositionGetDouble(POSITION_VOLUME);
   }
   return total;
}

double ExtremeOpenPrice(const ENUM_POSITION_TYPE type, bool wantLowest)
{
   bool found = false;
   double result = 0.0;

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;

      if(PositionGetString(POSITION_SYMBOL) != _Symbol)
         continue;
      if((ulong)PositionGetInteger(POSITION_MAGIC) != InpMagicNumber)
         continue;
      if((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE) != type)
         continue;

      double price = PositionGetDouble(POSITION_PRICE_OPEN);

      if(!found)
      {
         result = price;
         found = true;
      }
      else if(wantLowest && price < result)
         result = price;
      else if(!wantLowest && price > result)
         result = price;
   }

   return result;
}

double NextLot(const ENUM_POSITION_TYPE type)
{
   int count = CountPositions(type);
   double lot = InpStartLot;

   if(InpUseLotMultiplier && count > 0)
      lot *= MathPow(InpLotMultiplier, count);

   return NormalizeVolume(lot);
}

bool RSIConfirms(const TrendState trend)
{
   if(!InpUseRSIConfirmation)
      return true;

   double rsi;
   if(!GetBufferValue(hRSI, 1, rsi))
      return false;

   if(trend == TREND_UP)
      return (rsi >= InpBuyRSIMin);
   if(trend == TREND_DOWN)
      return (rsi <= InpSellRSIMax);

   return false;
}

bool IsNewEntryBar()
{
   datetime barTime = iTime(_Symbol, InpEntryTF, 0);

   if(!InpOneEntryPerBar)
      return true;

   if(barTime == g_lastEntryBar)
      return false;

   g_lastEntryBar = barTime;
   return true;
}

bool OpenBuy()
{
   trade.SetExpertMagicNumber(InpMagicNumber);
   trade.SetDeviationInPoints(InpDeviationPoints);
   return trade.Buy(NextLot(POSITION_TYPE_BUY), _Symbol, 0.0, 0.0, 0.0, InpTradeComment);
}

bool OpenSell()
{
   trade.SetExpertMagicNumber(InpMagicNumber);
   trade.SetDeviationInPoints(InpDeviationPoints);
   return trade.Sell(NextLot(POSITION_TYPE_SELL), _Symbol, 0.0, 0.0, 0.0, InpTradeComment);
}

bool ShouldAddBuy()
{
   int buys = CountPositions(POSITION_TYPE_BUY);
   if(buys == 0)
      return true;

   double lowestBuy = ExtremeOpenPrice(POSITION_TYPE_BUY, true);
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double distance = GetGridDistancePrice();

   // Add only when price moves adversely below the lowest buy
   return (bid <= lowestBuy - distance);
}

bool ShouldAddSell()
{
   int sells = CountPositions(POSITION_TYPE_SELL);
   if(sells == 0)
      return true;

   double highestSell = ExtremeOpenPrice(POSITION_TYPE_SELL, false);
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double distance = GetGridDistancePrice();

   // Add only when price moves adversely above the highest sell
   return (ask >= highestSell + distance);
}

bool CloseAllPositions()
{
   bool allClosed = true;
   trade.SetExpertMagicNumber(InpMagicNumber);

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0 || !PositionSelectByTicket(ticket))
         continue;

      if(PositionGetString(POSITION_SYMBOL) != _Symbol)
         continue;
      if((ulong)PositionGetInteger(POSITION_MAGIC) != InpMagicNumber)
         continue;

      if(!trade.PositionClose(ticket))
         allClosed = false;
   }

   return allClosed;
}

void ManageBasket()
{
   int total = CountPositions();
   if(total == 0)
      return;

   if(InpUseBasketMoneyTarget && TotalProfit() >= InpBasketProfitMoney)
   {
      CloseAllPositions();
      return;
   }

   if(InpUseBasketPriceTarget)
   {
      int buys = CountPositions(POSITION_TYPE_BUY);
      int sells = CountPositions(POSITION_TYPE_SELL);

      if(buys > 0 && sells == 0)
      {
         double avg = 0.0, vol = 0.0;
         for(int i = PositionsTotal()-1; i >= 0; i--)
         {
            ulong ticket = PositionGetTicket(i);
            if(ticket == 0 || !PositionSelectByTicket(ticket)) continue;
            if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
            if((ulong)PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
            if((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE) != POSITION_TYPE_BUY) continue;

            double v = PositionGetDouble(POSITION_VOLUME);
            avg += PositionGetDouble(POSITION_PRICE_OPEN) * v;
            vol += v;
         }
         if(vol > 0.0)
         {
            avg /= vol;
            if(SymbolInfoDouble(_Symbol, SYMBOL_BID) >= avg + InpBasketTargetPoints * _Point)
               CloseAllPositions();
         }
      }
      else if(sells > 0 && buys == 0)
      {
         double avg = 0.0, vol = 0.0;
         for(int i = PositionsTotal()-1; i >= 0; i--)
         {
            ulong ticket = PositionGetTicket(i);
            if(ticket == 0 || !PositionSelectByTicket(ticket)) continue;
            if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
            if((ulong)PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
            if((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE) != POSITION_TYPE_SELL) continue;

            double v = PositionGetDouble(POSITION_VOLUME);
            avg += PositionGetDouble(POSITION_PRICE_OPEN) * v;
            vol += v;
         }
         if(vol > 0.0)
         {
            avg /= vol;
            if(SymbolInfoDouble(_Symbol, SYMBOL_ASK) <= avg - InpBasketTargetPoints * _Point)
               CloseAllPositions();
         }
      }
   }
}

string TrendText(const TrendState trend)
{
   if(trend == TREND_UP) return "UP TREND";
   if(trend == TREND_DOWN) return "DOWN TREND";
   return "SIDEWAYS / NO TRADE";
}

string StatusText(const TrendState trend)
{
   int buys = CountPositions(POSITION_TYPE_BUY);
   int sells = CountPositions(POSITION_TYPE_SELL);

   if(buys > 0 && sells > 0) return "HEDGE";
   if(buys > 0) return "GRID BUY";
   if(sells > 0) return "GRID SELL";

   if(trend == TREND_UP) return "WAIT BUY";
   if(trend == TREND_DOWN) return "WAIT SELL";
   return "NO TRADE";
}

void DrawDashboard(const TrendState trend)
{
   double balance = AccountInfoDouble(ACCOUNT_BALANCE);
   double equity = AccountInfoDouble(ACCOUNT_EQUITY);
   double dd = CurrentDrawdownPercent();
   double profit = TotalProfit();

   int buys = CountPositions(POSITION_TYPE_BUY);
   int sells = CountPositions(POSITION_TYPE_SELL);

   double minBuy  = ExtremeOpenPrice(POSITION_TYPE_BUY, true);
   double maxBuy  = ExtremeOpenPrice(POSITION_TYPE_BUY, false);
   double minSell = ExtremeOpenPrice(POSITION_TYPE_SELL, true);
   double maxSell = ExtremeOpenPrice(POSITION_TYPE_SELL, false);

   string txt =
      "        ZENITH GRID\n"
      "H4: " + TrendText(trend) + "\n"
      "STATUS: " + StatusText(trend) + "\n"
      "--------------------------------\n"
      "Balance: " + DoubleToString(balance, 2) + "\n"
      "Equity:  " + DoubleToString(equity, 2) + "\n"
      "Profit:  " + DoubleToString(profit, 2) + "\n"
      "Current DD: " + DoubleToString(dd, 2) + "%\n"
      "--------------------------------\n"
      "Total Buy: " + IntegerToString(buys) + "\n"
      "Total Lot Buy: " + DoubleToString(TotalVolume(POSITION_TYPE_BUY), 2) + "\n"
      "--------------------------------\n"
      "Total Sell: " + IntegerToString(sells) + "\n"
      "Total Lot Sell: " + DoubleToString(TotalVolume(POSITION_TYPE_SELL), 2) + "\n"
      "--------------------------------\n"
      "Risk Level: " + string(dd < InpMaxDrawdownPercent*0.33 ? "1" : (dd < InpMaxDrawdownPercent*0.66 ? "2" : "3")) + "/3\n"
      "Grid: " + DoubleToString(GetGridDistancePrice()/_Point, 0) + " pts\n"
      "--------------------------------\n"
      "minBuy:  " + DoubleToString(minBuy, _Digits) + "\n"
      "maxBuy:  " + DoubleToString(maxBuy, _Digits) + "\n"
      "minSell: " + DoubleToString(minSell, _Digits) + "\n"
      "maxSell: " + DoubleToString(maxSell, _Digits) + "\n"
      "--------------------------------\n"
      "Magic: " + (string)InpMagicNumber;

   Comment(txt);
}

void ProcessProtection()
{
   double dd = CurrentDrawdownPercent();

   if(dd >= InpEmergencyDDPercent)
   {
      if(InpCloseAllOnEmergency)
         CloseAllPositions();

      g_pauseUntil = TimeCurrent() + (InpCooldownMinutes * 60);
      return;
   }

   if(dd >= InpMaxDrawdownPercent || DailyLossExceeded())
   {
      g_pauseUntil = TimeCurrent() + (InpCooldownMinutes * 60);
   }
}

void ProcessEntries(const TrendState trend)
{
   if(TimeCurrent() < g_pauseUntil)
      return;

   if(!IsTradingSession())
      return;

   if(DailyLossExceeded())
      return;

   if(trend == TREND_SIDEWAYS)
      return;

   if(!RSIConfirms(trend))
      return;

   if(CountPositions() >= InpMaxPositions)
      return;

   if(!IsNewEntryBar())
      return;

   if(trend == TREND_UP && InpAllowBuy && ShouldAddBuy())
      OpenBuy();

   if(trend == TREND_DOWN && InpAllowSell && ShouldAddSell())
      OpenSell();
}

//============================== EVENTS ==============================//
int OnInit()
{
   trade.SetExpertMagicNumber(InpMagicNumber);
   trade.SetDeviationInPoints(InpDeviationPoints);

   hFastEMA = iMA(_Symbol, InpTrendTF, InpFastEMA, 0, MODE_EMA, PRICE_CLOSE);
   hSlowEMA = iMA(_Symbol, InpTrendTF, InpSlowEMA, 0, MODE_EMA, PRICE_CLOSE);
   hADX     = iADX(_Symbol, InpTrendTF, InpADXPeriod);
   hATR     = iATR(_Symbol, InpATRTimeframe, InpATRPeriod);
   hRSI     = iRSI(_Symbol, InpEntryTF, InpRSIPeriod, PRICE_CLOSE);

   if(hFastEMA == INVALID_HANDLE || hSlowEMA == INVALID_HANDLE ||
      hADX == INVALID_HANDLE || hATR == INVALID_HANDLE || hRSI == INVALID_HANDLE)
   {
      Print("Failed to create indicator handles.");
      return INIT_FAILED;
   }

   UpdateDayStartEquity();
   Print("ZENITH GRID EA initialized on ", _Symbol);
   return INIT_SUCCEEDED;
}

void OnDeinit(const int reason)
{
   Comment("");

   if(hFastEMA != INVALID_HANDLE) IndicatorRelease(hFastEMA);
   if(hSlowEMA != INVALID_HANDLE) IndicatorRelease(hSlowEMA);
   if(hADX != INVALID_HANDLE) IndicatorRelease(hADX);
   if(hATR != INVALID_HANDLE) IndicatorRelease(hATR);
   if(hRSI != INVALID_HANDLE) IndicatorRelease(hRSI);
}

void OnTick()
{
   UpdateDayStartEquity();

   TrendState trend = GetTrend();

   ProcessProtection();
   ManageBasket();
   ProcessEntries(trend);
   DrawDashboard(trend);
}
//+------------------------------------------------------------------+
