﻿//+------------------------------------------------------------------+
//|                        Interactive AnchorFlow Volume Profile.mq5 |
//|                                             © 2026, ChukwuBuikem |
//|                             https://www.mql5.com/en/users/bikeen |
//+------------------------------------------------------------------+
#property copyright "© 2026, ChukwuBuikem"
#property link      "https://www.mql5.com/en/users/bikeen"

#define _PROG_NAME "Interactive AnchorFlow Volume Profile (MTF)"

#property description _PROG_NAME
#property indicator_separate_window
#property indicator_plots 0
//--- PROFILE MACROS
#define _VP_ANCHOR _PROG_NAME + "VP Anchor"
#define _VOLUME_PROFILE _PROG_NAME + "Profile"
#define _LTF_VOLUME_PROFILE _VOLUME_PROFILE + "LTF"
#define _HTF_VOLUME_PROFILE _VOLUME_PROFILE + "HTF"
//--- KEYSTROKE MACROS
#define _DBL_CLICK_TIME 500 //MILLISECONDS
#define _KEY_E 69 // EDIT
#define _KEY_S 83 // SAVE
//--- DATA STRUCTURE
struct st_priceVolume
  {
   //---
   double            price;
   long              volume;
   //--- CONSTRUCTOR
                     st_priceVolume(): price(EMPTY_VALUE), volume(0) {}
  };
//--- INPUT SETTING
input ENUM_TIMEFRAMES inpHtf = PERIOD_H4;// Higher timeframe

//--- GLOBAL STATE VARIABLES
datetime anchorTime;
ENUM_TIMEFRAMES htf = inpHtf;
int indSubWindow = -1;
int interactiveBins = 20;
string inputBuffer = "";
bool editMode = false;
//+------------------------------------------------------------------+
//|                  INITIALIZATION FUNCTION                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- ROBUST INPUT TIMEFRAME VALIDATION
   if(htf <= _Period)
      htf = getNextHFT();
//--- CHART AND INDICATOR SETUP
   ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true);
   IndicatorSetString(INDICATOR_SHORTNAME, _PROG_NAME + " " + EnumToString(htf));
   indSubWindow = ChartWindowFind(0, _PROG_NAME + " " + EnumToString(htf));
//--- RENDER VOLUME PROFILE
   datetime start, end;
   getViewportValues(start, end);
   anchorTime = getMiddleCandleTime(start, end);
   calcVolumeProfile(anchorTime, false);

   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//|                  DEINITIALIZATION FUNCTION                       |
//+------------------------------------------------------------------+
void OnDeinit(const int32_t reason)
  {
//--- CHART SETTING RESTORATION AND CLEAN UP
   ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, false);
   ObjectsDeleteAll(0, _PROG_NAME);
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//|                  ITERATION FUNCTION                              |
//+------------------------------------------------------------------+
int OnCalculate(const int32_t rates_total,
                const int32_t 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 int32_t &spread[])
  {
//---
   if(prev_calculated > 0 && rates_total != prev_calculated)
     {
      bool isSelected = (bool) ObjectGetInteger(0, _VP_ANCHOR, OBJPROP_SELECTED);
      calcVolumeProfile(anchorTime, isSelected);
     }
   return(rates_total);
  }
//+------------------------------------------------------------------+
//|                   CHARTEVENT FUNCTION                            |
//+------------------------------------------------------------------+
void OnChartEvent(const int32_t id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//--- CHART CHANGE
   if(id == CHARTEVENT_CHART_CHANGE)
     {
      bool isSelected = (bool) ObjectGetInteger(0, _VP_ANCHOR, OBJPROP_SELECTED);
      //--- OTHER SUB-INDICATOR DELETION, CHART SCROLL, AND ZOOM
      calcVolumeProfile(anchorTime, isSelected);
      //--- UPDATE INDICATOR SUB WINDOW
      indSubWindow = ChartWindowFind(0, _PROG_NAME + " " + EnumToString(htf));
     }
//--- INTERACTIVE BIN EDITING
   if(id == CHARTEVENT_KEYDOWN)
     {
      int key = (int)lparam;
      static ulong lastEClick = 0;
      static ulong lastSClick = 0;

      if((bool)ObjectGetInteger(0, _VP_ANCHOR, OBJPROP_SELECTED))
        {
         //--- EDIT-SAVE MECHANISM
         if(key == _KEY_E || key == _KEY_S)
           {
            switch(key)
              {
               //--- EDIT MODE
               case _KEY_E:
                  if(isDoubleClick(lastEClick))
                    {
                     inputBuffer = "";
                     editMode = true;
                     Print("Edit mode activated");
                    }
                  break;
               //--- SAVE INPUT
               case _KEY_S:
                  if(isDoubleClick(lastSClick))
                    {
                     //--- GET NUMBER OF BINS FROM USER INPUT
                     int temp = (int) StringToInteger(inputBuffer);
                     //--- CLAMP (5 - 100)
                     temp = (temp < 5) ? 5 : (temp > 100) ? 100 : temp;
                     //--- IF NO INPUT, USE LAST VALUE IN MEMORY
                     interactiveBins = (inputBuffer == "") ? interactiveBins : temp;
                     Print("Saved: [", interactiveBins, "]");
                     editMode = false;
                    }
                  break;
              }
           }
         //--- CAPTURE USER INPUTS
         if(editMode)
           {
            //--- NUMERIC INPUTS (0 - 9)
            if(key >= 48 && key <= 57)
              {
               inputBuffer += CharToString((char)key);
              }
           }
        }
     }
//---
   if(sparam != _VP_ANCHOR)
      return;

   switch(id)
     {
      //--- ANCHOR LINE DRAG AND DROP
      case CHARTEVENT_OBJECT_DRAG:
        {
         anchorTime = (datetime)ObjectGetInteger(0, _VP_ANCHOR, OBJPROP_TIME);
         anchorTime = normalizeToChartTime(anchorTime);
         //--- ENHANCED USER-EXPERIENCE
         if(anchorTime > iTime(_Symbol, PERIOD_CURRENT, 1))
           {
            datetime start = 0, end = 0;
            getViewportValues(start, end);
            anchorTime = getMiddleCandleTime(start, end);
           }
         calcVolumeProfile(anchorTime, true);
         break;
        }
        //--- ANCHOR RESTORATION
      case CHARTEVENT_OBJECT_DELETE:
        {
         Print("Volume Profile Anchor line deleted. RESTORED");
         //--- RESTORE IMMEDIATELY
         createVLine(_VP_ANCHOR, anchorTime, clrBlack, "VP Anchor", true);
         break;
        }
     }
  }
//+------------------------------------------------------------------+
//|                   NEXT HIGHER TIMEFRAME DETECTION                |
//+------------------------------------------------------------------+
ENUM_TIMEFRAMES getNextHFT()
  {
//---
   ENUM_TIMEFRAMES timeframes[] = {PERIOD_M1, PERIOD_M2, PERIOD_M3,
                                   PERIOD_M4, PERIOD_M5, PERIOD_M6,
                                   PERIOD_M10, PERIOD_M12, PERIOD_M15,
                                   PERIOD_M20, PERIOD_M30, PERIOD_H1,
                                   PERIOD_H2, PERIOD_H3, PERIOD_H4,
                                   PERIOD_H6, PERIOD_H8, PERIOD_H12,
                                   PERIOD_D1, PERIOD_W1, PERIOD_MN1
                                  };

   int chartSecs = PeriodSeconds();
//--- FIND NEXT HIGHER THAN CHART TF
   for(int w = 0; w < ArraySize(timeframes); w++)
     {
      if(PeriodSeconds(timeframes[w]) > chartSecs)
         return timeframes[w];
     }
//--- FALLBACK
   return timeframes[ArraySize(timeframes) - 1];
  }
//+------------------------------------------------------------------+
//|                    VIEWPORT DETECTION                            |
//+------------------------------------------------------------------+
int getViewportValues(datetime &start, datetime &end)
  {
//---
   int first = (int) ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR);
   int visibleBars = (int) ChartGetInteger(0, CHART_VISIBLE_BARS);

   int last = first - visibleBars + 2;
   if(last < 0)
      last = 0;

   visibleBars = visibleBars - 2;
   start = iTime(_Symbol, PERIOD_CURRENT, last);
   end = iTime(_Symbol, PERIOD_CURRENT, first);

   return visibleBars;
  }
//+------------------------------------------------------------------+
//|                      CHART ALIGNMENT                             |
//+------------------------------------------------------------------+
datetime normalizeToChartTime(datetime time, ENUM_TIMEFRAMES tf = PERIOD_CURRENT)
  {
//---
   int shift = iBarShift(_Symbol, tf, time);
   if(shift < 0)
      shift = 0;

   return iTime(_Symbol, tf, shift);
  }
//+------------------------------------------------------------------+
//|                   MIDDLE TIME DETECTION                          |
//+------------------------------------------------------------------+
datetime getMiddleCandleTime(const datetime time1, const datetime time2)
  {
//---
   datetime rawMiddleTime = (time1 + time2) / 2;

   int nearestBar = iBarShift(_Symbol, PERIOD_CURRENT, rawMiddleTime, false);

   if(nearestBar < 0)
      return rawMiddleTime;

   return iTime(_Symbol, PERIOD_CURRENT, nearestBar);// Normalized value
  }
//+------------------------------------------------------------------+
//|                   VOLUME NORMALIZATION                           |
//+------------------------------------------------------------------+
double normalizeMinMax(const long value, const long maxVol, const long minVol)
  {
//---
   if(maxVol <= minVol)
      return 0;

   return (double)(value - minVol) / (maxVol - minVol);
  }
//+------------------------------------------------------------------+
//|           KEYSTROKE DOUBLE-CLICK DETECTION                       |
//+------------------------------------------------------------------+
bool isDoubleClick(ulong &lastClick)
  {
//---
   ulong now = GetTickCount();
   bool doubleClick = (now - lastClick <= _DBL_CLICK_TIME);
   lastClick = now;

   return doubleClick;
  }
//+------------------------------------------------------------------+
//|                      VERTICAL LINE CREATION                      |
//+------------------------------------------------------------------+
bool createVLine(const string objName, const datetime time,
                 const color clr, const string tooltip = "\n",
                 const bool selected = false)
  {
//---
   if(ObjectCreate(0, objName, OBJ_VLINE, 0, time, 0))
     {
      ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);
      ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, true);
      ObjectSetInteger(0, objName, OBJPROP_SELECTED, selected);
      ObjectSetInteger(0, objName, OBJPROP_WIDTH, 2);
      ObjectSetInteger(0, objName, OBJPROP_HIDDEN, true);
      ObjectSetString(0, objName, OBJPROP_TOOLTIP, tooltip);
      ChartRedraw();
      return true;
     }
   return false;
  }
//+------------------------------------------------------------------+
//|                      HORIZONTAL LINE CREATION                    |
//+------------------------------------------------------------------+
bool createHLine(const string objName, const double price,
                 const int subWind, const color clr, const string tooltip)
  {
//---
   if(ObjectCreate(0, objName, OBJ_HLINE, subWind, 0, price))
     {
      ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);
      ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false);
      ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1);
      ObjectSetInteger(0, objName, OBJPROP_HIDDEN, true);
      ObjectSetString(0, objName, OBJPROP_TOOLTIP, tooltip);
      return true;
     }
   return false;
  }
//+------------------------------------------------------------------+
//|                      RECTANGLE CREATION                          |
//+------------------------------------------------------------------+
bool createRect(const string objName, const datetime time1, const double price1,
                const datetime time2, const double price2, const int subWind, const color clr)
  {
//---
   if(ObjectCreate(0, objName, OBJ_RECTANGLE, subWind, time1, price1, time2, price2))
     {
      ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);
      ObjectSetInteger(0, objName, OBJPROP_BACK, true);
      ObjectSetInteger(0, objName, OBJPROP_FILL, true);
      ObjectSetInteger(0, objName, OBJPROP_HIDDEN, true);
      ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false);
      ObjectSetString(0, objName, OBJPROP_TOOLTIP, "\n");
      return true;
     }
   return false;
  }
//+------------------------------------------------------------------+
//|                  HTF VOLUME PROFILE COMPUTATION                  |
//+------------------------------------------------------------------+
void htfVolumeProfile(const datetime vpAnchor, const int maxProfile,
                      const int minProfile)
  {
//---
   datetime  viewStart = 0, viewEnd = 0;
   int count = getViewportValues(viewStart, viewEnd);
   viewStart = normalizeToChartTime(viewStart, htf);
   viewEnd =  normalizeToChartTime(vpAnchor, htf);

   count = MathAbs(iBarShift(_Symbol, htf, viewStart) -
                   iBarShift(_Symbol, htf, viewEnd)) + 1;

   int firstBar = -1, highestIndex = -1, lowestIndex = -1;
   int numberOfBins = -1;
   bool useRealVol = false;
   double rangeHigh = EMPTY_VALUE, rangeLow = EMPTY_VALUE;
   double binHigh = EMPTY_VALUE,  binLow = EMPTY_VALUE;
   long maxVol = 0, minVol = LONG_MAX;
   int extendBars = 0;
   color clr = clrNONE;
   double dominance = -1;

   MqlRates rates[];
   ArraySetAsSeries(rates, true);
   st_priceVolume bins[];
//--- ENSURE DATA IS SYNCHRONIZED
   while(!SeriesInfoInteger(_Symbol, htf, SERIES_SYNCHRONIZED) && !IsStopped())
     {
      Sleep(100);
     }
//--- CLEAR SUB WINDOW
   ObjectsDeleteAll(0, _HTF_VOLUME_PROFILE);
   ChartRedraw();

   if(CopyRates(_Symbol, htf, viewStart, viewEnd, rates) > 0)
     {
      firstBar = iBarShift(_Symbol, htf, viewStart);
      highestIndex = iHighest(_Symbol, htf, MODE_HIGH, count, firstBar);
      lowestIndex = iLowest(_Symbol, htf, MODE_LOW, count, firstBar);
      //---
      rangeHigh = iHigh(_Symbol, htf, highestIndex);
      rangeLow = iLow(_Symbol, htf, lowestIndex);
      //--- INTERACTIVE BIN CONSTRUCTION
      numberOfBins = interactiveBins;
      double step = (rangeHigh - rangeLow) / numberOfBins;
      ArrayResize(bins, numberOfBins);
      useRealVol = (rates[0].real_volume > 0);
      //--- VOLUME ACCUMULATION
      for(int w = 0; w < numberOfBins; w++)
        {
         binLow = rangeLow + step * w;
         binHigh = binLow + step;
         //---
         for(int c = 0; c < ArraySize(rates); c++)
           {
            if(rates[c].close >= binLow && rates[c].close <= binHigh)
              {
               bins[w].volume += (useRealVol) ? rates[c].real_volume : rates[c].tick_volume;
               bins[w].price = rates[c].close;
              }
           }
         if(bins[w].volume < 1)
            bins[w].volume = 1;
        }
      //--- VOLUME EXTREMES
      for(int b = 0; b < ArraySize(bins); b++)
        {
         if(bins[b].volume > maxVol)
            maxVol = bins[b].volume;

         if(bins[b].volume < minVol)
            minVol = bins[b].volume;
        }
      bool pocDrawn = false;
      //--- PROFILE RENDERING
      for(int p = 0; p < ArraySize(bins); p++)
        {
         binLow = rangeLow + step * p;
         binHigh = binLow + step;

         extendBars = minProfile + (int)MathRound(normalizeMinMax(bins[p].volume, maxVol, minVol) *
                      (maxProfile - minProfile));

         dominance = (double)extendBars / maxProfile;
         clr = (dominance >= 0.85) ? clrGreen :
               (dominance >= 0.65) ? clrPurple :
               (dominance >= 0.45) ? clrCoral :
               (dominance >= 0.25) ? clrRed :
               (dominance >= 0.10) ? clrBurlyWood :
               clrDarkSlateGray;
         createRect(_HTF_VOLUME_PROFILE + (string)p, vpAnchor, binLow, vpAnchor + (PeriodSeconds()*extendBars), binHigh, indSubWindow, clr);
         if(bins[p].volume == maxVol && !pocDrawn)
           {
            //--- DRAW MAXIMUM VOLUME'S POC : ONCE
            createHLine(_HTF_VOLUME_PROFILE + "POC", bins[p].price, indSubWindow, clr, "HTF POC");
            pocDrawn = true;
           }
        }
     }
  }
//+------------------------------------------------------------------+
//|                 CORE VOLUME PROFILE COMPUTATIONS                 |
//+------------------------------------------------------------------+
void calcVolumeProfile(const datetime vpAnchor, const bool vpSelected)
  {
//---
   datetime  viewStart = 0, viewEnd = 0;
   int count = getViewportValues(viewStart, viewEnd);
   int firstBar = -1, highestIndex = -1, lowestIndex = -1;
   int numberOfBins = -1;
   bool useRealVol = false;
   double rangeHigh = EMPTY_VALUE, rangeLow = EMPTY_VALUE;
   double binHigh = EMPTY_VALUE,  binLow = EMPTY_VALUE;
   long maxVol = 0, minVol = LONG_MAX;
   int extendBars = 0, maxProfileLength = 0, minProfileLength = 0;
   color clr = clrNONE;
   double dominance = -1;

   MqlRates rates[];
   ArraySetAsSeries(rates, true);
   st_priceVolume bins[];

   createVLine(_VP_ANCHOR, vpAnchor, clrBlack, "VP Anchor", vpSelected);

   count = MathAbs(iBarShift(_Symbol, PERIOD_CURRENT, viewStart) -
                   iBarShift(_Symbol, PERIOD_CURRENT, vpAnchor)) + 1;
//--- ENSURE DATA IS SYNCHRONIZED
   while(!SeriesInfoInteger(_Symbol, PERIOD_CURRENT, SERIES_SYNCHRONIZED) && !IsStopped())
     {
      Sleep(100);
     }
//--- CLEAR MAIN CHART
   ObjectsDeleteAll(0, _LTF_VOLUME_PROFILE);
   ChartRedraw();

   if(CopyRates(_Symbol, PERIOD_CURRENT, viewStart, vpAnchor, rates) > 0)
     {
      firstBar = iBarShift(_Symbol, PERIOD_CURRENT, viewStart);
      highestIndex = iHighest(_Symbol, PERIOD_CURRENT, MODE_HIGH, count, firstBar);
      lowestIndex = iLowest(_Symbol, PERIOD_CURRENT, MODE_LOW, count, firstBar);
      //---
      rangeHigh = iHigh(_Symbol, PERIOD_CURRENT, highestIndex);
      rangeLow = iLow(_Symbol, PERIOD_CURRENT, lowestIndex);
      //--- DYNAMIC SUB-WINDOW PRICE SCALING
      IndicatorSetDouble(INDICATOR_MAXIMUM, rangeHigh + (100 * _Point));
      IndicatorSetDouble(INDICATOR_MINIMUM, rangeLow - (100 * _Point));
      //--- INTERACTIVE BIN CONSTRUCTION
      numberOfBins = interactiveBins;
      double step = (rangeHigh - rangeLow) / numberOfBins;
      ArrayResize(bins, numberOfBins);
      useRealVol = (rates[0].real_volume > 0);
      //--- VOLUME ACCUMULATION
      for(int w = 0; w < numberOfBins; w++)
        {
         binLow = rangeLow + step * w;
         binHigh = binLow + step;
         //---
         for(int c = 0; c < ArraySize(rates); c++)
           {
            if(rates[c].close >= binLow && rates[c].close <= binHigh)
              {
               bins[w].volume += (useRealVol) ? rates[c].real_volume : rates[c].tick_volume;
               bins[w].price = rates[c].close;
              }
           }
         if(bins[w].volume < 1)
            bins[w].volume = 1;
        }
      //--- VOLUME EXTREMES
      for(int b = 0; b < ArraySize(bins); b++)
        {
         if(bins[b].volume > maxVol)
            maxVol = bins[b].volume;

         if(bins[b].volume < minVol)
            minVol = bins[b].volume;
        }
      //--- PROFILE SCALING
      maxProfileLength = (int)(count * 0.4);
      maxProfileLength = MathMax(4, maxProfileLength);
      minProfileLength = (int)MathRound(maxProfileLength * 0.05);
      minProfileLength = MathMax(2, minProfileLength);

      bool pocDrawn = false;
      //--- PROFILE RENDERING
      for(int p = 0; p < ArraySize(bins); p++)
        {
         binLow = rangeLow + step * p;
         binHigh = binLow + step;

         extendBars = minProfileLength + (int)MathRound(normalizeMinMax(bins[p].volume, maxVol, minVol) *
                      (maxProfileLength - minProfileLength));

         //--- AVOID ZERO DIVISION ERROR
         if(maxProfileLength > 0)
            dominance = (double)extendBars / maxProfileLength;
         clr = (dominance >= 0.85) ? clrFireBrick :
               (dominance >= 0.65) ? clrBlue :
               (dominance >= 0.45) ? clrChocolate :
               (dominance >= 0.25) ? clrBrown :
               (dominance >= 0.10) ? clrPaleVioletRed :
               clrOlive;
         createRect(_LTF_VOLUME_PROFILE + (string)p, vpAnchor, binLow, vpAnchor + (PeriodSeconds()*extendBars), binHigh, 0, clr);
         if(bins[p].volume == maxVol && !pocDrawn)
           {
            //--- DRAW MAXIMUM VOLUME'S POC : ONCE
            createHLine(_LTF_VOLUME_PROFILE + "POC", bins[p].price, 0, clr, "LTF POC");
            pocDrawn = true;
           }
        }
      htfVolumeProfile(vpAnchor, maxProfileLength, minProfileLength);
      ChartRedraw();
     }
  }
//+------------------------------------------------------------------+
