//+------------------------------------------------------------------+
//| FXP_RoundNumberLevels.mq5                                        |
//| FXPIPS ONLINE - https://fxpips.online/mt5-indicators/round-number-levels/
//|                                                                  |
//| License: MIT. Copyright (c) 2026 FXPIPS ONLINE.                  |
//| Permission is granted to use, copy, modify and distribute this   |
//| software provided this notice is kept. The software is provided  |
//| "as is", without warranty of any kind.                           |
//|                                                                  |
//| Educational tool - not financial advice. Trading involves        |
//| substantial risk of loss. Test on a demo account first.          |
//+------------------------------------------------------------------+
#property copyright "FXPIPS ONLINE"
#property link      "https://fxpips.online/mt5-indicators/round-number-levels/"
#property version   "1.00"
#property description "Psychological round-number levels around the current price."
#property description "Automatic step from the price magnitude, optional half levels."

#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots   0

input group "Levels"
input double InpStep           = 0.0;   // Step in price units (0 = automatic)
input double InpAutoScale      = 1.0;   // Multiplier applied to the automatic step
input int    InpLevelsEachSide = 5;     // Major levels above and below price
input bool   InpShowHalf       = true;  // Draw half levels between majors
input group "Style"
input color  InpMajorColor     = clrSlateGray;  // Major level colour
input color  InpHalfColor      = clrDarkGray;   // Half level colour
input int    InpMajorWidth     = 1;             // Major level width
input group "Alerts"
input bool   InpAlerts         = false; // Popup alert when a closed bar crosses a major level
input bool   InpPush           = false; // Push notification when a closed bar crosses a major level

string   gPrefix = "FXP_RNL_";
double   gStep = 0.0;
double   gBase = -1.0e300;
datetime gLastAlertBar = 0;

string TfName() { return StringSubstr(EnumToString(Period()), 7); }

void Notify(const string text, const datetime barTime)
{
   if(!InpAlerts && !InpPush) return;
   if(barTime == gLastAlertBar) return;
   gLastAlertBar = barTime;
   string msg = "FXP Round Numbers " + _Symbol + " " + TfName() + ": " + text;
   if(InpAlerts) Alert(msg);
   if(InpPush)   SendNotification(msg);
}

// Automatic step: one hundredth of the nearest power of ten to the price.
// Examples: 1.08 -> 0.01, 150 -> 1, 2400 -> 10, 18000 -> 100, 60000 -> 1000.
double StepFor(const double price)
{
   if(InpStep > 0.0) return(InpStep);
   double p = MathAbs(price);
   if(p <= 0.0) return(_Point * 1000.0);
   double e = MathRound(MathLog10(p));
   double step = MathPow(10.0, e - 2.0) * MathMax(0.0001, InpAutoScale);
   return(MathMax(step, _Point * 10.0));
}

double Bucket(const double price, const double step) { return(MathFloor(price / step + 1.0e-9)); }

void MakeLine(const string name, const double price, const color clr, const ENUM_LINE_STYLE style, const int width)
{
   ObjectCreate(0, name, OBJ_HLINE, 0, 0, price);
   ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
   ObjectSetInteger(0, name, OBJPROP_STYLE, style);
   ObjectSetInteger(0, name, OBJPROP_WIDTH, width);
   ObjectSetInteger(0, name, OBJPROP_BACK, true);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
   ObjectSetString(0, name, OBJPROP_TEXT, DoubleToString(price, _Digits));
}

void Redraw(const double base, const double step)
{
   ObjectsDeleteAll(0, gPrefix);
   int n = MathMax(1, InpLevelsEachSide);
   for(int k = -n + 1; k <= n; k++)
   {
      double lvl = NormalizeDouble((base + k) * step, _Digits);
      MakeLine(gPrefix + "M" + IntegerToString(k + n), lvl, InpMajorColor, STYLE_SOLID, InpMajorWidth);
      if(InpShowHalf && k < n)
      {
         double half = NormalizeDouble((base + k + 0.5) * step, _Digits);
         MakeLine(gPrefix + "H" + IntegerToString(k + n), half, InpHalfColor, STYLE_DOT, 1);
      }
   }
   ChartRedraw(0);
}

int OnInit()
{
   IndicatorSetString(INDICATOR_SHORTNAME, "FXP Round Number Levels");
   return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason)
{
   ObjectsDeleteAll(0, gPrefix);
   ChartRedraw(0);
}

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
   if(rates_total < 3) return(0);
   ArraySetAsSeries(time, false);
   ArraySetAsSeries(close, false);

   double price = close[rates_total - 1];
   double step  = StepFor(price);
   double base  = Bucket(price, step);
   if(prev_calculated <= 0)
   {
      gLastAlertBar = time[rates_total - 1];
      gBase = -1.0e300;
   }
   // Rebuild the ladder only when price moves into a different step or the step changes
   if(base != gBase || step != gStep)
   {
      gBase = base;
      gStep = step;
      Redraw(base, step);
   }

   if(prev_calculated > 0)
   {
      int b = rates_total - 2;
      double now = Bucket(close[b], step), before = Bucket(close[b - 1], step);
      if(now > before)
         Notify("closed above round number " + DoubleToString(now * step, _Digits), time[rates_total - 1]);
      else if(now < before)
         Notify("closed below round number " + DoubleToString(before * step, _Digits), time[rates_total - 1]);
   }
   return(rates_total);
}
//+------------------------------------------------------------------+
