//+------------------------------------------------------------------+
//| FXP_OBVSignal.mq5                                                |
//| FXPIPS ONLINE - https://fxpips.online/mt5-indicators/obv-signal/ |
//|                                                                  |
//| 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/obv-signal/"
#property version   "1.00"
#property description "On-Balance Volume with an EMA signal line in a subwindow."
#property description "Arrows mark closed-bar crosses of OBV and its signal line."

#property indicator_separate_window
#property indicator_buffers 4
#property indicator_plots   4

#property indicator_label1  "OBV"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrDodgerBlue
#property indicator_width1  2

#property indicator_label2  "Signal"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrOrange
#property indicator_width2  1

#property indicator_label3  "Cross up"
#property indicator_type3   DRAW_ARROW
#property indicator_color3  clrLimeGreen
#property indicator_width3  1

#property indicator_label4  "Cross down"
#property indicator_type4   DRAW_ARROW
#property indicator_color4  clrRed
#property indicator_width4  1

input group "OBV"
input ENUM_APPLIED_VOLUME InpVolume = VOLUME_TICK; // Volume type (real falls back to tick when zero)
input int  InpSignalPeriod = 20;    // Signal EMA period
input bool InpShowArrows   = true;  // Show cross arrows
input group "Alerts"
input bool InpAlerts       = false; // Popup alert on a new cross
input bool InpPush         = false; // Push notification on a new cross

double gObv[], gSig[], gUp[], gDn[];
int      gSigN = 20;
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 OBV Signal " + _Symbol + " " + TfName() + ": " + text;
   if(InpAlerts) Alert(msg);
   if(InpPush)   SendNotification(msg);
}

int OnInit()
{
   gSigN = MathMax(1, InpSignalPeriod);
   SetIndexBuffer(0, gObv, INDICATOR_DATA);
   SetIndexBuffer(1, gSig, INDICATOR_DATA);
   SetIndexBuffer(2, gUp,  INDICATOR_DATA);
   SetIndexBuffer(3, gDn,  INDICATOR_DATA);
   ArraySetAsSeries(gObv, false); ArraySetAsSeries(gSig, false);
   ArraySetAsSeries(gUp, false);  ArraySetAsSeries(gDn, false);

   PlotIndexSetInteger(2, PLOT_ARROW, 233);
   PlotIndexSetInteger(3, PLOT_ARROW, 234);
   PlotIndexSetInteger(2, PLOT_ARROW_SHIFT, 10);
   PlotIndexSetInteger(3, PLOT_ARROW_SHIFT, -10);
   for(int p = 0; p < 4; p++)
   {
      PlotIndexSetDouble(p, PLOT_EMPTY_VALUE, EMPTY_VALUE);
      PlotIndexSetInteger(p, PLOT_DRAW_BEGIN, gSigN);
   }
   IndicatorSetString(INDICATOR_SHORTNAME, StringFormat("FXP OBV Signal (%d)", gSigN));
   IndicatorSetInteger(INDICATOR_DIGITS, 0);
   return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason)
{
   // No indicator handles or chart objects are created by this indicator.
}

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 < gSigN + 3) return(0);
   ArraySetAsSeries(time, false);
   ArraySetAsSeries(close, false);
   ArraySetAsSeries(tick_volume, false);
   ArraySetAsSeries(volume, false);

   int start;
   if(prev_calculated <= 0 || prev_calculated > rates_total)
   {
      ArrayInitialize(gUp, EMPTY_VALUE);
      ArrayInitialize(gDn, EMPTY_VALUE);
      gObv[0] = 0.0;
      gSig[0] = 0.0;
      start = 1;
      gLastAlertBar = time[rates_total - 1];
   }
   else
      start = prev_calculated - 1;

   double alpha = 2.0 / (gSigN + 1.0);
   for(int i = start; i < rates_total && !IsStopped(); i++)
   {
      double v = (double)tick_volume[i];
      if(InpVolume == VOLUME_REAL && volume[i] > 0) v = (double)volume[i];

      if(close[i] > close[i - 1])      gObv[i] = gObv[i - 1] + v;
      else if(close[i] < close[i - 1]) gObv[i] = gObv[i - 1] - v;
      else                             gObv[i] = gObv[i - 1];

      gSig[i] = gSig[i - 1] + alpha * (gObv[i] - gSig[i - 1]);

      gUp[i] = EMPTY_VALUE;
      gDn[i] = EMPTY_VALUE;
      if(InpShowArrows && i < rates_total - 1 && i > gSigN)
      {
         if(gObv[i] > gSig[i] && gObv[i - 1] <= gSig[i - 1]) gUp[i] = gObv[i];
         if(gObv[i] < gSig[i] && gObv[i - 1] >= gSig[i - 1]) gDn[i] = gObv[i];
      }
   }

   if(prev_calculated > 0)
   {
      int b = rates_total - 2;
      if(gObv[b] > gSig[b] && gObv[b - 1] <= gSig[b - 1]) Notify("OBV crossed above its signal line", time[rates_total - 1]);
      if(gObv[b] < gSig[b] && gObv[b - 1] >= gSig[b - 1]) Notify("OBV crossed below its signal line", time[rates_total - 1]);
   }
   return(rates_total);
}
//+------------------------------------------------------------------+
