//+------------------------------------------------------------------+
//|                                     VWAP Adaptive Trend Stop.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property indicator_chart_window
#property indicator_buffers 8
#property indicator_plots   2
#include <Canvas/Canvas.mqh>
//+------------------------------------------------------------------+
//| VWAP reference enumeration                                       |
//+------------------------------------------------------------------+
enum ENUM_VWAP_REFERENCE
  {
   VWAP_SESSION, // Session VWAP
   VWAP_WEEK,    // Weekly VWAP
   VWAP_MONTH,   // Monthly VWAP
   VWAP_ANY,     // Any One Agrees
   VWAP_ALL      // All Must Agree
  };
//+------------------------------------------------------------------+
//| Volume source enumeration                                        |
//+------------------------------------------------------------------+
enum ENUM_VOLUME_SOURCE
  {
   VOLUME_SOURCE_TICK, // Tick Volume
   VOLUME_SOURCE_REAL  // Real Volume
  };
//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "TREND STOP"
input double InpAccelerationRate = 0.12;   // Acceleration Per Bar
input double InpMaxSpeed = 3.0;            // Maximum Speed (0 = uncapped)
input double InpStartDistanceAtr = 2.0;    // Start Distance From Price (slow ATR multiples)
input double InpStepSizeAtr = 0.15;        // Step Size Per Speed Unit (slow ATR multiples)
input int InpSmoothingBars = 3;            // Smoothing Bars
input int InpFastAtrPeriod = 14;           // Fast ATR Period
input int InpSlowAtrPeriod = 100;          // Slow ATR Period
input int InpMaxHistoryBars = 3000;        // Max History Bars To Scan (0 = all)

input group "VWAP"
input ENUM_VWAP_REFERENCE InpVwapReference = VWAP_SESSION;     // VWAP That Must Agree With A Flip
input ENUM_VOLUME_SOURCE InpVolumeSource = VOLUME_SOURCE_TICK; // Volume Source
input double InpVwapSpeedBoost = 1.5;                          // Speed Boost When Extended From VWAP
input double InpVwapBoostRangeAtr = 4.0;                       // Distance For Full Boost (fast ATR multiples)

input group "FLIP LEVELS"
input bool InpShowFlipLevels = true;                 // Show Flip Levels
input ENUM_LINE_STYLE InpFlipLevelStyle = STYLE_DASH; // Flip Level Style
input int InpFlipLevelWidth = 1;                     // Flip Level Width
input int InpFlipLevelOpacity = 80;                  // Flip Level Opacity Percent
input bool InpShowFlipLevelPrice = true;             // Show Price Label On Flip Levels
input int InpLabelFontSize = 12;                     // Price Label Font Size (pixels)
input int InpMaxFlipLevels = 10;                     // Max Flip Levels Kept (broken ones leave at once)

input group "DISPLAY"
input bool InpShowStopLine = true;      // Show The Stop Line
input int InpStopLineWidth = 1;         // Stop Line Width (pixels, measured across the line)
input bool InpShowCloud = true;         // Show The Cloud
input int InpCloudOpacity = 35;         // Cloud Opacity At The Stop Line (percent, fades to zero at price)
input bool InpColorCandles = true;      // Color Candles By Distance From The Stop
input bool InpShowFlipMarkers = true;   // Show Flip Arrows
input double InpArrowOffsetAtr = 1.2;   // Arrow Offset From The Stop (fast ATR multiples)
input int InpArrowSize = 10;            // Arrow Size (pixels from tip to base)
input int InpMaxFlipMarkersKept = 100;  // Flip Markers Kept On Chart

input group "COLORS"
input color InpBullishColor = C'26,194,0';   // Bullish Color
input color InpBearishColor = C'255,64,64';  // Bearish Color
input color InpStopLineColor = clrDimGray;   // Stop Line Color

input group "ALERTS"
input bool InpAlertConfirmedFlip = true; // Alert On A VWAP Confirmed Flip
input bool InpAlertFilteredFlip = false; // Alert On A Flip The VWAP Disagrees With
input bool InpAlertLevelBreak = false;   // Alert When A Flip Level Breaks
input bool InpAlertPopup = true;         // Popup Alert
input bool InpAlertJournal = true;       // Print To Journal
//+------------------------------------------------------------------+
//| Flip level structure                                             |
//+------------------------------------------------------------------+
struct FlipLevel
  {
   int               bar;       // Bar the flip happened on
   double            price;     // Level price
   bool              isBullish; // Born on a bullish flip
  };
//+------------------------------------------------------------------+
//| Flip marker structure                                            |
//+------------------------------------------------------------------+
struct FlipMarker
  {
   int               bar;         // Bar the flip happened on
   double            price;       // Anchor price for the arrow
   bool              isBullish;   // Direction of the new trend
   bool              isConfirmed; // The reference VWAP agreed
  };
//+------------------------------------------------------------------+
//| Canvas with a clean text compositor                              |
//+------------------------------------------------------------------+
class CTrendCanvas : public CCanvas
  {
public:
   void              BlendPixel(const int x, const int y, const uint argb);                                   // Source over blend of one pixel
   void              DrawTextClean(const int x, const int y, const string text, const uint argb, const uint alignment); // Text composited by glyph coverage
  };
//+------------------------------------------------------------------+
//| Source over blend of one pixel straight into the pixel buffer    |
//+------------------------------------------------------------------+
void CTrendCanvas::BlendPixel(const int x, const int y, const uint argb)
  {
//--- Clip to the canvas and skip transparent paint
   if(x < 0 || y < 0 || x >= m_width || y >= m_height)
      return;
   int sourceAlpha = (int)(argb >> 24);
   if(sourceAlpha <= 0)
      return;
//--- Destination and source channels
   int index = y * m_width + x;
   uint dest = m_pixels[index];
   int destAlpha = (int)(dest >> 24);
//--- Split both colors into channels
   int sourceRed = (int)((argb >> 16) & 0xFF);
   int sourceGreen = (int)((argb >> 8) & 0xFF);
   int sourceBlue = (int)(argb & 0xFF);
   int destRed = (int)((dest >> 16) & 0xFF);
   int destGreen = (int)((dest >> 8) & 0xFF);
   int destBlue = (int)(dest & 0xFF);
//--- Source over compositing in 8 bit fixed point
   int outAlpha = sourceAlpha + destAlpha * (255 - sourceAlpha) / 255;
   if(outAlpha <= 0)
     {
      //--- Both transparent
      m_pixels[index] = 0;
      return;
     }
   int outRed = (sourceRed * sourceAlpha + destRed * destAlpha * (255 - sourceAlpha) / 255) / outAlpha;
   int outGreen = (sourceGreen * sourceAlpha + destGreen * destAlpha * (255 - sourceAlpha) / 255) / outAlpha;
   int outBlue = (sourceBlue * sourceAlpha + destBlue * destAlpha * (255 - sourceAlpha) / 255) / outAlpha;
   m_pixels[index] = ((uint)outAlpha << 24) | ((uint)outRed << 16) | ((uint)outGreen << 8) | (uint)outBlue;
  }
//+------------------------------------------------------------------+
//| Composite text by true glyph coverage, no holes at the edges     |
//+------------------------------------------------------------------+
void CTrendCanvas::DrawTextClean(const int x, const int y, const string text, const uint argb, const uint alignment)
  {
//--- Measure the string in the font set by TextSetFont
   uint textWidth = 0;
   uint textHeight = 0;
   if(text == "" || !::TextGetSize(text, textWidth, textHeight) || textWidth == 0 || textHeight == 0)
      return;
   int width = (int)textWidth;
   int height = (int)textHeight;
   int count = width * height;
//--- Render black text on a black and on a white buffer
   uint onBlack[];
   uint onWhite[];
   if(ArrayResize(onBlack, count) != count || ArrayResize(onWhite, count) != count)
      return;
   ArrayFill(onBlack, 0, count, 0xFF000000);
   ArrayFill(onWhite, 0, count, 0xFFFFFFFF);
//--- Render the text onto the black and white bitmaps
   ::TextOut(text, 0, 0, TA_LEFT | TA_TOP, onBlack, width, height, 0xFF000000, COLOR_FORMAT_ARGB_NORMALIZE);
   ::TextOut(text, 0, 0, TA_LEFT | TA_TOP, onWhite, width, height, 0xFF000000, COLOR_FORMAT_ARGB_NORMALIZE);
//--- Resolve the top left from the requested alignment
   int left = x;
   int top = y;
   if((alignment & TA_CENTER) == TA_CENTER)
      left = x - width / 2;
   else
      if((alignment & TA_RIGHT) == TA_RIGHT)
         left = x - width;
   if((alignment & TA_VCENTER) == TA_VCENTER)
      top = y - height / 2;
   else
      if((alignment & TA_BOTTOM) == TA_BOTTOM)
         top = y - height;
//--- Text color and its own alpha
   int textAlpha = (int)(argb >> 24);
   uint textRgb = argb & 0x00FFFFFF;
//--- Coverage is how much the white background was darkened
   for(int row = 0; row < height; row++)
     {
      //--- One row of glyph pixels
      for(int column = 0; column < width; column++)
        {
         //--- Channel deltas between the two renders
         int index = row * width + column;
         int deltaRed = (int)((onWhite[index] >> 16) & 0xFF) - (int)((onBlack[index] >> 16) & 0xFF);
         int deltaGreen = (int)((onWhite[index] >> 8) & 0xFF) - (int)((onBlack[index] >> 8) & 0xFF);
         int deltaBlue = (int)(onWhite[index] & 0xFF) - (int)(onBlack[index] & 0xFF);
         //--- Full coverage leaves no delta, no coverage leaves the full white to black delta
         int coverage = 255 - (deltaRed + deltaGreen + deltaBlue) / 3;
         if(coverage <= 0)
            continue;
         if(coverage > 255)
            coverage = 255;
         //--- Scale by the text alpha and blend
         int alpha = coverage * textAlpha / 255;
         if(alpha <= 0)
            continue;
         BlendPixel(left + column, top + row, ((uint)alpha << 24) | textRgb);
        }
     }
  }
//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
#define OBJECT_PREFIX g_objectPrefix          // Prefix for the canvas object, unique per instance
#define CANDLE_SHADES 8                       // Gradient steps per direction for the candle plot
#define EDGE_MARGIN 64                        // Pixels beyond the canvas that still count as near
string g_objectPrefix = "VATS_";              // Instance prefix, finalized in OnInit
CTrendCanvas g_canvas;                        // The single full chart canvas
string g_canvasName = "";                     // Name of the bitmap label carrying the canvas
double g_candleOpen[];                        // Colored candle plot: open
double g_candleHigh[];                        // Colored candle plot: high
double g_candleLow[];                         // Colored candle plot: low
double g_candleClose[];                       // Colored candle plot: close
double g_candleColorIndex[];                  // Colored candle plot: palette index
double g_stopBuffer[];                        // Data: raw trailing stop on the bar, no holes
double g_trendBuffer[];                       // Data: 1 bullish, -1 bearish, 0 not started
double g_flipBuffer[];                        // Data: 2 confirmed up, 1 filtered up, -2 confirmed down, -1 filtered down
double g_stopValues[];                        // Raw stop per bar
double g_smoothedStop[];                      // Stop after smoothing, the drawn stop line
double g_midPrices[];                         // Bar midpoints
double g_priceReference[];                    // Smoothed midpoint the cloud fades toward
double g_trueRange[];                         // True range per bar
double g_fastAtr[];                           // Wilder fast ATR per bar
double g_slowAtr[];                           // Simple average true range per bar
double g_sessionVwap[];                       // Session VWAP per bar
double g_weeklyVwap[];                        // Weekly VWAP per bar
double g_monthlyVwap[];                       // Monthly VWAP per bar
double g_fastSeedSum = 0.0;                   // Running true range sum for the fast ATR seed
double g_slowSum = 0.0;                       // Rolling sum of true range over the slow window
int g_slowSumBar = -1;                        // Bar the rolling sum currently covers
double g_sessionPriceVolume = 0.0;            // Session cumulative price times volume
double g_sessionVolume = 0.0;                 // Session cumulative volume
double g_weeklyPriceVolume = 0.0;             // Week cumulative price times volume
double g_weeklyVolume = 0.0;                  // Week cumulative volume
double g_monthlyPriceVolume = 0.0;            // Month cumulative price times volume
double g_monthlyVolume = 0.0;                 // Month cumulative volume
bool g_engineStarted = false;                 // The stop has been seeded
bool g_isBullish = true;                      // Current trend direction
double g_stopPrice = 0.0;                     // Live raw stop
double g_speed = 0.0;                         // Live speed in step units
int g_firstDrawBar = 0;                       // First bar carrying a stop
FlipLevel g_flipLevels[];                     // Flip levels, newest last
FlipMarker g_flipMarkers[];                   // Ring of flip markers
int g_flipMarkerSlot = 0;                     // Next marker ring slot
bool g_sessionAgrees = false;                 // Session VWAP agrees with the trend
bool g_weeklyAgrees = false;                  // Weekly VWAP agrees with the trend
bool g_monthlyAgrees = false;                 // Monthly VWAP agrees with the trend
datetime g_lastProcessedBarTime = 0;          // Time of the last closed bar processed
int g_ratesTotal = 0;                         // Bars on the chart at the last calculation
int g_chartWidth = 0;                         // Viewport key: chart width in pixels
int g_chartHeight = 0;                        // Viewport key: chart height in pixels
int g_chartScale = -1;                        // Viewport key: chart scale
int g_firstVisibleBar = -1;                   // Viewport key: series index of the first visible bar
int g_visibleBars = -1;                       // Viewport key: number of visible bars
double g_priceMin = 0.0;                      // Viewport key: lowest visible price
double g_priceMax = 0.0;                      // Viewport key: highest visible price
int g_barPixels = 1;                          // Pixel width of one bar at the current scale
bool g_canvasReady = false;                   // Canvas created and sized
color g_chartBackgroundColor = clrWhite;      // Cached chart background for the candle palette
//+------------------------------------------------------------------+
//| Convert the chart timeframe to a short string                    |
//+------------------------------------------------------------------+
string TimeframeToString()
  {
//--- Get the enum name of the current period
   string periodName = EnumToString((ENUM_TIMEFRAMES)Period());
//--- Strip the PERIOD_ prefix when present
   if(StringFind(periodName, "PERIOD_") == 0)
      periodName = StringSubstr(periodName, 7);
//--- Return the short form
   return(periodName);
  }
//+------------------------------------------------------------------+
//| Build an ARGB value from a color and an opacity percent          |
//+------------------------------------------------------------------+
uint ArgbFromOpacity(const color baseColor, double opacityPercent)
  {
//--- Clamp the opacity
   if(opacityPercent < 0.0)
      opacityPercent = 0.0;
   if(opacityPercent > 100.0)
      opacityPercent = 100.0;
//--- Scale to a byte alpha
   uchar alpha = (uchar)(opacityPercent * 255.0 / 100.0 + 0.5);
   return(ColorToARGB(baseColor, alpha));
  }
//+------------------------------------------------------------------+
//| Blend a color toward the chart background by opacity percent     |
//+------------------------------------------------------------------+
color BlendTowardBackground(const color baseColor, int opacityPercent)
  {
//--- Clamp the opacity
   if(opacityPercent < 0)
      opacityPercent = 0;
   if(opacityPercent > 100)
      opacityPercent = 100;
   double weight = opacityPercent / 100.0;
//--- Extract both sets of channels
   int baseRed = baseColor & 0xFF;
   int baseGreen = (baseColor >> 8) & 0xFF;
   int baseBlue = (baseColor >> 16) & 0xFF;
   int backRed = g_chartBackgroundColor & 0xFF;
   int backGreen = (g_chartBackgroundColor >> 8) & 0xFF;
   int backBlue = (g_chartBackgroundColor >> 16) & 0xFF;
//--- Mix each channel by the weight
   int mixedRed = (int)(baseRed * weight + backRed * (1.0 - weight) + 0.5);
   int mixedGreen = (int)(baseGreen * weight + backGreen * (1.0 - weight) + 0.5);
   int mixedBlue = (int)(baseBlue * weight + backBlue * (1.0 - weight) + 0.5);
   return((color)(mixedRed | (mixedGreen << 8) | (mixedBlue << 16)));
  }
//+------------------------------------------------------------------+
//| Dispatch an alert through the enabled channels                   |
//+------------------------------------------------------------------+
void SendTrendAlert(const string message)
  {
//--- Prepend the symbol and timeframe
   string fullMessage = _Symbol + " " + TimeframeToString() + ": " + message;
//--- Show a popup when enabled
   if(InpAlertPopup)
      Alert(fullMessage);
//--- Write to the journal when enabled
   if(InpAlertJournal)
      Print(fullMessage);
  }
//+------------------------------------------------------------------+
//| Read the volume of a bar from the chosen source                  |
//+------------------------------------------------------------------+
double ReadBarVolume(const long &tick_volume[], const long &volume[], const int bar)
  {
//--- Return the chosen source as a double
   return((InpVolumeSource == VOLUME_SOURCE_TICK) ? (double)tick_volume[bar] : (double)volume[bar]);
  }
//+------------------------------------------------------------------+
//| Size every working array to the chart, growing only              |
//+------------------------------------------------------------------+
void EnsureArrays(const int size)
  {
//--- Nothing to do while the arrays are large enough
   if(ArraySize(g_stopValues) >= size)
      return;
//--- Grow and blank the new tail of each array
   int previous = ArraySize(g_stopValues);
   ArrayResize(g_stopValues, size);
   ArrayResize(g_smoothedStop, size);
   ArrayResize(g_midPrices, size);
   ArrayResize(g_priceReference, size);
//--- Range and average arrays
   ArrayResize(g_trueRange, size);
   ArrayResize(g_fastAtr, size);
   ArrayResize(g_slowAtr, size);
   ArrayResize(g_sessionVwap, size);
   ArrayResize(g_weeklyVwap, size);
   ArrayResize(g_monthlyVwap, size);
//--- Every new slot starts empty
   for(int i = previous; i < size; i++)
     {
      //--- Blank one slot
      g_stopValues[i] = EMPTY_VALUE;
      g_smoothedStop[i] = EMPTY_VALUE;
      g_midPrices[i] = EMPTY_VALUE;
      g_priceReference[i] = EMPTY_VALUE;
      //--- Ranges and averages
      g_trueRange[i] = EMPTY_VALUE;
      g_fastAtr[i] = EMPTY_VALUE;
      g_slowAtr[i] = EMPTY_VALUE;
      g_sessionVwap[i] = EMPTY_VALUE;
      g_weeklyVwap[i] = EMPTY_VALUE;
      g_monthlyVwap[i] = EMPTY_VALUE;
     }
  }
//+------------------------------------------------------------------+
//| True range of a bar against the previous close                   |
//+------------------------------------------------------------------+
double TrueRangeOf(const int bar, const double &high[], const double &low[], const double &close[])
  {
//--- Largest of the three spans
   return(MathMax(high[bar] - low[bar], MathMax(MathAbs(high[bar] - close[bar - 1]), MathAbs(low[bar] - close[bar - 1]))));
  }
//+------------------------------------------------------------------+
//| Update both average true ranges for one bar                      |
//+------------------------------------------------------------------+
void UpdateRanges(const int bar, const double &high[], const double &low[], const double &close[])
  {
//--- The first bar has no previous close
   if(bar < 1)
     {
      //--- Leave both empty
      g_fastAtr[bar] = EMPTY_VALUE;
      g_slowAtr[bar] = EMPTY_VALUE;
      return;
     }
//--- Store the true range once
   double trueRange = TrueRangeOf(bar, high, low, close);
   g_trueRange[bar] = trueRange;
//--- Fast ATR: simple average seed, then Wilder smoothing
   if(bar < InpFastAtrPeriod)
     {
      //--- Still seeding
      g_fastSeedSum += trueRange;
      g_fastAtr[bar] = EMPTY_VALUE;
     }
   else
      if(bar == InpFastAtrPeriod)
        {
         //--- Close the seed window
         g_fastSeedSum += trueRange;
         g_fastAtr[bar] = g_fastSeedSum / InpFastAtrPeriod;
        }
      else
         if(g_fastAtr[bar - 1] != EMPTY_VALUE)
           {
            //--- Recursive from here on
            g_fastAtr[bar] = (g_fastAtr[bar - 1] * (InpFastAtrPeriod - 1) + trueRange) / InpFastAtrPeriod;
           }
         else
           {
            //--- A gap in the recursion reseeds from the plain range
            g_fastAtr[bar] = trueRange;
           }
//--- Slow range: rolling simple average of true range
   if(bar < InpSlowAtrPeriod)
     {
      //--- Not enough bars yet
      g_slowAtr[bar] = EMPTY_VALUE;
      return;
     }
   if(g_slowSumBar == bar - 1)
     {
      //--- One value in, one value out
      g_slowSum += trueRange - g_trueRange[bar - InpSlowAtrPeriod];
     }
   else
     {
      //--- Seed the whole window, computing any range not yet stored
      g_slowSum = 0.0;
      for(int i = bar - InpSlowAtrPeriod + 1; i <= bar; i++)
        {
         //--- Fill gaps as they are met
         if(g_trueRange[i] == EMPTY_VALUE)
            g_trueRange[i] = TrueRangeOf(i, high, low, close);
         g_slowSum += g_trueRange[i];
        }
     }
   g_slowSumBar = bar;
   g_slowAtr[bar] = g_slowSum / InpSlowAtrPeriod;
  }
//+------------------------------------------------------------------+
//| Whether two times fall in different periods of a kind            |
//+------------------------------------------------------------------+
bool IsNewPeriod(const datetime previous, const datetime current, const int kind)
  {
//--- The first bar opens every period
   if(previous == 0)
      return(true);
//--- Break both times into parts
   MqlDateTime past;
   MqlDateTime now;
   TimeToStruct(previous, past);
   TimeToStruct(current, now);
//--- Day boundary
   if(kind == 0)
      return(past.day != now.day || past.mon != now.mon || past.year != now.year);
//--- Month boundary
   if(kind == 2)
      return(past.mon != now.mon || past.year != now.year);
//--- Week boundary: the weekday falls back, or a whole week has passed
   return(now.day_of_week < past.day_of_week || (current - previous) >= 7 * 86400);
  }
//+------------------------------------------------------------------+
//| Update the three volume weighted averages for one bar            |
//+------------------------------------------------------------------+
void UpdateVwaps(const int bar, const datetime &time[], const double &high[], const double &low[],
                 const long &tick_volume[], const long &volume[])
  {
//--- Reset each accumulator when its period turns over
   datetime previous = (bar > 0) ? time[bar - 1] : 0;
   if(IsNewPeriod(previous, time[bar], 0))
     {
      //--- New session
      g_sessionPriceVolume = 0.0;
      g_sessionVolume = 0.0;
     }
   if(IsNewPeriod(previous, time[bar], 1))
     {
      //--- New week
      g_weeklyPriceVolume = 0.0;
      g_weeklyVolume = 0.0;
     }
   if(IsNewPeriod(previous, time[bar], 2))
     {
      //--- New month
      g_monthlyPriceVolume = 0.0;
      g_monthlyVolume = 0.0;
     }
//--- Accumulate the midpoint against volume
   double midpoint = (high[bar] + low[bar]) / 2.0;
   double barVolume = ReadBarVolume(tick_volume, volume, bar);
   g_sessionPriceVolume += midpoint * barVolume;
   g_sessionVolume += barVolume;
//--- The longer periods take the same contribution
   g_weeklyPriceVolume += midpoint * barVolume;
   g_weeklyVolume += barVolume;
   g_monthlyPriceVolume += midpoint * barVolume;
   g_monthlyVolume += barVolume;
//--- Publish, falling back to the midpoint on a volumeless bar
   g_sessionVwap[bar] = g_sessionVolume > 0.0 ? g_sessionPriceVolume / g_sessionVolume : midpoint;
   g_weeklyVwap[bar] = g_weeklyVolume > 0.0 ? g_weeklyPriceVolume / g_weeklyVolume : midpoint;
   g_monthlyVwap[bar] = g_monthlyVolume > 0.0 ? g_monthlyPriceVolume / g_monthlyVolume : midpoint;
  }
//+------------------------------------------------------------------+
//| The VWAP the reference setting points at                         |
//+------------------------------------------------------------------+
double ReferenceVwap(const int bar)
  {
//--- Combined settings fall back to the session average for distance work
   if(InpVwapReference == VWAP_WEEK)
      return(g_weeklyVwap[bar]);
   if(InpVwapReference == VWAP_MONTH)
      return(g_monthlyVwap[bar]);
   return(g_sessionVwap[bar]);
  }
//+------------------------------------------------------------------+
//| Record which VWAPs sit on the same side as a direction           |
//+------------------------------------------------------------------+
void UpdateVwapAgreement(const int bar, const bool bullish, const double &close[])
  {
//--- Price above a VWAP agrees with a bullish trend and the reverse
   g_sessionAgrees = bullish ? (close[bar] >= g_sessionVwap[bar]) : (close[bar] <= g_sessionVwap[bar]);
   g_weeklyAgrees = bullish ? (close[bar] >= g_weeklyVwap[bar]) : (close[bar] <= g_weeklyVwap[bar]);
   g_monthlyAgrees = bullish ? (close[bar] >= g_monthlyVwap[bar]) : (close[bar] <= g_monthlyVwap[bar]);
  }
//+------------------------------------------------------------------+
//| Whether the reference setting counts the VWAPs as agreeing       |
//+------------------------------------------------------------------+
bool VwapAgrees()
  {
//--- Each setting reads the three flags differently
   switch(InpVwapReference)
     {
      case VWAP_SESSION:
         return(g_sessionAgrees);
      //--- Single timeframe settings read one flag
      case VWAP_WEEK:
         return(g_weeklyAgrees);
      case VWAP_MONTH:
         return(g_monthlyAgrees);
      //--- Combined settings read all three
      case VWAP_ANY:
         return(g_sessionAgrees || g_weeklyAgrees || g_monthlyAgrees);
      default:
         return(g_sessionAgrees && g_weeklyAgrees && g_monthlyAgrees);
     }
  }
//+------------------------------------------------------------------+
//| Speed gained on one bar, scaled by distance from the VWAP        |
//+------------------------------------------------------------------+
double AccelerationAt(const int bar, const double &close[])
  {
//--- Without a fast range there is no distance to measure
   double fastRange = g_fastAtr[bar];
   if(fastRange == EMPTY_VALUE || fastRange <= 0.0)
      return(InpAccelerationRate);
//--- Distance from the reference VWAP as a share of the full boost range
   double distance = MathAbs(close[bar] - ReferenceVwap(bar));
   double share = MathMin(distance / (fastRange * InpVwapBoostRangeAtr), 1.0);
//--- Scale between the base rate and the boosted rate
   return(InpAccelerationRate * (1.0 + (InpVwapSpeedBoost - 1.0) * share));
  }
//+------------------------------------------------------------------+
//| Average of the filled entries of an array over a window          |
//+------------------------------------------------------------------+
double AverageOver(const double &values[], const int bar, const int period)
  {
//--- Average only the filled entries in the window
   double sum = 0.0;
   int used = 0;
   for(int i = MathMax(0, bar - period + 1); i <= bar; i++)
     {
      //--- Skip empty entries
      if(values[i] == EMPTY_VALUE)
         continue;
      sum += values[i];
      used++;
     }
   return(used > 0 ? sum / used : EMPTY_VALUE);
  }
//+------------------------------------------------------------------+
//| Store a flip level and drop the oldest past the cap              |
//+------------------------------------------------------------------+
void AddFlipLevel(const int bar, const double price, const bool bullish)
  {
//--- Append
   int count = ArraySize(g_flipLevels);
   ArrayResize(g_flipLevels, count + 1);
   g_flipLevels[count].bar = bar;
   g_flipLevels[count].price = price;
   g_flipLevels[count].isBullish = bullish;
//--- Drop the oldest while over the cap
   while(ArraySize(g_flipLevels) > InpMaxFlipLevels)
     {
      //--- Shift down and shrink
      int size = ArraySize(g_flipLevels);
      for(int i = 0; i < size - 1; i++)
         g_flipLevels[i] = g_flipLevels[i + 1];
      ArrayResize(g_flipLevels, size - 1);
     }
  }
//+------------------------------------------------------------------+
//| Mark flip levels that this bar closed through                    |
//+------------------------------------------------------------------+
void UpdateFlipLevels(const int bar, const double barClose, const bool alertable)
  {
//--- A bullish level breaks on a close below it and the reverse, broken levels leave the registry
   int kept = 0;
   for(int i = 0; i < ArraySize(g_flipLevels); i++)
     {
      //--- Levels born on this bar cannot break yet
      bool broken = g_flipLevels[i].bar < bar
                    && (g_flipLevels[i].isBullish ? (barClose < g_flipLevels[i].price) : (barClose > g_flipLevels[i].price));
      if(broken)
        {
         //--- Announce and drop
         if(InpAlertLevelBreak && alertable)
            SendTrendAlert("Flip level " + DoubleToString(g_flipLevels[i].price, _Digits) + " broken");
         continue;
        }
      //--- Compact the survivors
      if(kept != i)
         g_flipLevels[kept] = g_flipLevels[i];
      kept++;
     }
   ArrayResize(g_flipLevels, kept);
  }
//+------------------------------------------------------------------+
//| Store a flip marker in the ring                                  |
//+------------------------------------------------------------------+
void AddFlipMarker(const int bar, const double price, const bool bullish, const bool confirmed)
  {
//--- Overwrite the oldest slot
   g_flipMarkers[g_flipMarkerSlot].bar = bar;
   g_flipMarkers[g_flipMarkerSlot].price = price;
   g_flipMarkers[g_flipMarkerSlot].isBullish = bullish;
   g_flipMarkers[g_flipMarkerSlot].isConfirmed = confirmed;
   g_flipMarkerSlot = (g_flipMarkerSlot + 1) % InpMaxFlipMarkersKept;
  }
//+------------------------------------------------------------------+
//| Read the chart viewport and report whether it changed            |
//+------------------------------------------------------------------+
bool ReadViewport()
  {
//--- Read every component of the mapping
   int width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0);
   int height = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, 0);
   int scale = (int)ChartGetInteger(0, CHART_SCALE, 0);
//--- Horizontal window and vertical window
   int firstVisible = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR, 0);
   int visible = (int)ChartGetInteger(0, CHART_VISIBLE_BARS, 0);
   double priceMin = ChartGetDouble(0, CHART_PRICE_MIN, 0);
   double priceMax = ChartGetDouble(0, CHART_PRICE_MAX, 0);
//--- Compare against the cached key
   bool changed = (width != g_chartWidth || height != g_chartHeight || scale != g_chartScale
                   || firstVisible != g_firstVisibleBar || visible != g_visibleBars
                   || priceMin != g_priceMin || priceMax != g_priceMax);
   if(!changed)
      return(false);
//--- Store the new key
   g_chartWidth = width;
   g_chartHeight = height;
   g_chartScale = scale;
//--- Store the visible window
   g_firstVisibleBar = firstVisible;
   g_visibleBars = visible;
   g_priceMin = priceMin;
   g_priceMax = priceMax;
//--- One bar is two to the power of the scale pixels wide
   g_barPixels = (int)MathPow(2.0, scale);
   if(g_barPixels < 1)
      g_barPixels = 1;
//--- Report the change
   return(true);
  }
//+------------------------------------------------------------------+
//| Pixel x of the center of a bar given its forward index           |
//+------------------------------------------------------------------+
int BarToX(const int bar)
  {
//--- Bars step right as the shift falls, this lands on the candle center
   int shift = g_ratesTotal - 1 - bar;
   return((g_firstVisibleBar - shift) * g_barPixels);
  }
//+------------------------------------------------------------------+
//| Antialiased one pixel line stepping along its longer axis        |
//+------------------------------------------------------------------+
void DrawLineWu(double x0, double y0, double x1, double y1, const uint argb)
  {
//--- Work in coordinates where the line runs along x, swapping axes for steep lines
   bool steep = MathAbs(y1 - y0) > MathAbs(x1 - x0);
   if(steep)
     {
      //--- Swap x and y on both ends
      double swapStart = x0;
      x0 = y0;
      y0 = swapStart;
      //--- Far end
      double swapEnd = x1;
      x1 = y1;
      y1 = swapEnd;
     }
   if(x0 > x1)
     {
      //--- Always draw left to right along the major axis
      double swapMajor = x0;
      x0 = x1;
      x1 = swapMajor;
      //--- Minor axis follows
      double swapMinor = y0;
      y0 = y1;
      y1 = swapMinor;
     }
//--- Rise per step along the major axis
   double gradient = (x1 - x0) != 0.0 ? (y1 - y0) / (x1 - x0) : 0.0;
//--- Step range clipped to the canvas along the major axis
   int majorLimit = steep ? g_chartHeight : g_chartWidth;
   int stepFrom = MathMax((int)MathCeil(x0), 0);
   int stepTo = MathMin((int)MathFloor(x1), majorLimit - 1);
   int alpha = (int)(argb >> 24);
   uint rgb = argb & 0x00FFFFFF;
   for(int step = stepFrom; step <= stepTo; step++)
     {
      //--- Exact position across the minor axis, split between the two pixels it straddles
      double minor = y0 + gradient * (step - x0);
      int lower = (int)MathFloor(minor);
      double upperShare = minor - lower;
      int alphaLower = (int)(alpha * (1.0 - upperShare));
      int alphaUpper = (int)(alpha * upperShare);
      if(steep)
        {
         //--- Steep: step is the row, minor is the column
         g_canvas.BlendPixel(lower, step, ((uint)alphaLower << 24) | rgb);
         g_canvas.BlendPixel(lower + 1, step, ((uint)alphaUpper << 24) | rgb);
        }
      else
        {
         //--- Shallow: step is the column, minor is the row
         g_canvas.BlendPixel(step, lower, ((uint)alphaLower << 24) | rgb);
         g_canvas.BlendPixel(step, lower + 1, ((uint)alphaUpper << 24) | rgb);
        }
     }
  }
//+------------------------------------------------------------------+
//| Exact pixel y of a price, never clamped to the visible range     |
//+------------------------------------------------------------------+
double PriceToY(const double price)
  {
//--- Guard a collapsed range
   double span = g_priceMax - g_priceMin;
   if(span <= 0.0)
      return(0.0);
//--- The true position, bounded only against arithmetic that could overflow later
   double y = (g_priceMax - price) / span * g_chartHeight;
   if(y < -1.0e6)
      return(-1.0e6);
   if(y > 1.0e6)
      return(1.0e6);
   return(y);
  }
//+------------------------------------------------------------------+
//| Paint a horizontal span in one row, soft at both ends            |
//+------------------------------------------------------------------+
void PaintHorizontalSpan(const int y, const double left, const double right, const uint argb)
  {
//--- Nothing to do outside the canvas or with an inverted span
   if(y < 0 || y >= g_chartHeight || right < left)
      return;
   if(right < 0.0 || left > g_chartWidth - 1)
      return;
//--- Whole columns are solid, the two boundary columns take partial coverage
   int firstFull = (int)MathCeil(left);
   int lastFull = (int)MathFloor(right);
//--- Color split once
   int alpha = (int)(argb >> 24);
   uint rgb = argb & 0x00FFFFFF;
   for(int x = MathMax(firstFull, 0); x <= MathMin(lastFull, g_chartWidth - 1); x++)
      g_canvas.BlendPixel(x, y, argb);
//--- Coverage of the column the span starts inside
   double leftCoverage = firstFull - left;
   if(leftCoverage > 0.001)
      g_canvas.BlendPixel(firstFull - 1, y, ((uint)(int)(alpha * leftCoverage) << 24) | rgb);
//--- Coverage of the column it ends inside
   double rightCoverage = right - lastFull;
   if(rightCoverage > 0.001)
      g_canvas.BlendPixel(lastFull + 1, y, ((uint)(int)(alpha * rightCoverage) << 24) | rgb);
  }
//+------------------------------------------------------------------+
//| Whether a point sits close enough to the canvas to draw on       |
//+------------------------------------------------------------------+
bool NearCanvas(const int x, const double y)
  {
//--- Inside the canvas grown by the margin
   return(x >= -EDGE_MARGIN && y >= -EDGE_MARGIN && x <= g_chartWidth + EDGE_MARGIN && y <= g_chartHeight + EDGE_MARGIN);
  }
//+------------------------------------------------------------------+
//| First and last bar indices that are on screen                    |
//+------------------------------------------------------------------+
void VisibleBarRange(int &firstBar, int &lastBar)
  {
//--- Convert the visible shift window into forward indices
   firstBar = g_ratesTotal - 1 - g_firstVisibleBar;
   lastBar = firstBar + g_visibleBars;
   if(firstBar < 0)
      firstBar = 0;
   if(lastBar > g_ratesTotal - 1)
      lastBar = g_ratesTotal - 1;
  }
//+------------------------------------------------------------------+
//| Make sure the canvas covers the current chart size               |
//+------------------------------------------------------------------+
bool EnsureCanvasSize()
  {
//--- Resize whenever the chart size differs, CCanvas rebinds the resource and sizes the label itself
   if(g_canvas.Width() != g_chartWidth || g_canvas.Height() != g_chartHeight)
     {
      //--- Reallocate to the exact chart size
      if(!g_canvas.Resize(g_chartWidth, g_chartHeight))
         return(false);
     }
//--- Canvas matches the chart
   return(true);
  }
//+------------------------------------------------------------------+
//| Paint one cloud column, fading from the stop line to price       |
//+------------------------------------------------------------------+
void PaintCloudColumn(const int x, const double arcY, const double priceY, const color sideColor)
  {
//--- Only columns on the canvas
   if(x < 0 || x >= g_chartWidth)
      return;
//--- The fade is measured across the true distance even when the stop line is off screen
   double span = MathAbs(priceY - arcY);
   if(span < 1.0)
      return;
//--- Only the rows that exist are painted, the fade still comes from the true span
   int rowFrom = MathMax(0, (int)MathCeil(MathMin(arcY, priceY)));
   int rowTo = MathMin(g_chartHeight - 1, (int)MathFloor(MathMax(arcY, priceY)));
   uint rgb = ColorToARGB(sideColor, 255) & 0x00FFFFFF;
   for(int y = rowFrom; y <= rowTo; y++)
     {
      //--- Full opacity on the stop line, none at the price
      double fade = 1.0 - MathAbs((double)y - arcY) / span;
      if(fade <= 0.0)
         continue;
      int alpha = (int)MathRound(255.0 * InpCloudOpacity / 100.0 * fade);
      if(alpha <= 0)
         continue;
      g_canvas.BlendPixel(x, y, ((uint)alpha << 24) | rgb);
     }
  }
//+------------------------------------------------------------------+
//| Paint the cloud between the stop line and the smoothed price     |
//+------------------------------------------------------------------+
void DrawCloud()
  {
//--- Skip when hidden
   if(!InpShowCloud)
      return;
//--- Every pair of neighboring visible bars that share a trend
   int firstBar = 0;
   int lastBar = 0;
   VisibleBarRange(firstBar, lastBar);
   for(int bar = MathMax(firstBar, g_firstDrawBar + 1); bar <= lastBar; bar++)
     {
      //--- Both bars must be filled and on the same side, the flip bar is empty and breaks the run
      if(g_smoothedStop[bar] == EMPTY_VALUE || g_smoothedStop[bar - 1] == EMPTY_VALUE)
         continue;
      if(g_priceReference[bar] == EMPTY_VALUE || g_priceReference[bar - 1] == EMPTY_VALUE)
         continue;
      if(g_trendBuffer[bar] != g_trendBuffer[bar - 1])
         continue;
      //--- Interpolate both edges across the bar so the boundary is a curve, not steps
      color sideColor = g_trendBuffer[bar] > 0.0 ? InpBullishColor : InpBearishColor;
      int xFrom = BarToX(bar - 1);
      int xTo = BarToX(bar);
      //--- Both edges at each end of the bar, at their true positions
      double arcFrom = PriceToY(g_smoothedStop[bar - 1]);
      double arcTo = PriceToY(g_smoothedStop[bar]);
      double priceFrom = PriceToY(g_priceReference[bar - 1]);
      double priceTo = PriceToY(g_priceReference[bar]);
      for(int x = xFrom; x < xTo; x++)
        {
         //--- Position across the bar
         double share = (double)(x - xFrom) / MathMax(xTo - xFrom, 1);
         PaintCloudColumn(x, arcFrom + (arcTo - arcFrom) * share, priceFrom + (priceTo - priceFrom) * share, sideColor);
        }
     }
  }
//+------------------------------------------------------------------+
//| Paint the stop line, one antialiased segment per bar pair        |
//+------------------------------------------------------------------+
void DrawStopLine()
  {
//--- Skip when hidden
   if(!InpShowStopLine)
      return;
//--- Every neighboring pair of visible bars that share a trend
   int firstBar = 0;
   int lastBar = 0;
   VisibleBarRange(firstBar, lastBar);
   uint lineArgb = ArgbFromOpacity(InpStopLineColor, 100);
   for(int bar = MathMax(firstBar, g_firstDrawBar + 1); bar <= lastBar; bar++)
     {
      //--- Both bars must be filled and on the same side, the flip bar is empty and breaks the run
      if(g_smoothedStop[bar] == EMPTY_VALUE || g_smoothedStop[bar - 1] == EMPTY_VALUE)
         continue;
      if(g_trendBuffer[bar] != g_trendBuffer[bar - 1])
         continue;
      //--- Segment between the two bar centers
      double xFrom = BarToX(bar - 1);
      double xTo = BarToX(bar);
      double yFrom = PriceToY(g_smoothedStop[bar - 1]);
      double yTo = PriceToY(g_smoothedStop[bar]);
      //--- Skip segments with no part on the canvas
      if(xTo < 0 || xFrom > g_chartWidth)
         continue;
      if(MathMax(yFrom, yTo) < 0 || MathMin(yFrom, yTo) > g_chartHeight)
         continue;
      //--- One antialiased line per pixel of width, offset across the axis the line does not run along
      bool steep = MathAbs(yTo - yFrom) > MathAbs(xTo - xFrom);
      for(int pass = 0; pass < InpStopLineWidth; pass++)
        {
         //--- Alternate the side so the stroke stays centered
         double offset = (pass + 1) / 2 * ((pass % 2) == 0 ? 1.0 : -1.0);
         if(steep)
            DrawLineWu(xFrom + offset, yFrom, xTo + offset, yTo, lineArgb);
         else
            DrawLineWu(xFrom, yFrom + offset, xTo, yTo + offset, lineArgb);
        }
     }
  }
//+------------------------------------------------------------------+
//| Paint a dashed or dotted horizontal line inside the canvas       |
//+------------------------------------------------------------------+
void DrawHorizontalRun(const int x1, const int x2, const double y, const uint argb, const int width, const ENUM_LINE_STYLE style)
  {
//--- Clip the run to the canvas
   int left = MathMax(0, MathMin(x1, x2));
   int right = MathMin(g_chartWidth - 1, MathMax(x1, x2));
   if(left > right)
      return;
//--- Exactly as many rows as the requested width, centered on the level
   int rowFrom = (int)MathRound(y - width / 2.0);
   int rowTo = rowFrom + width - 1;
//--- A solid style is one run per row
   if(style == STYLE_SOLID)
     {
      //--- Straight through
      for(int row = rowFrom; row <= rowTo; row++)
         PaintHorizontalSpan(row, left, right, argb);
      return;
     }
//--- Dots are short, dashes are long
   int on = (style == STYLE_DOT) ? 2 : 7;
   int off = (style == STYLE_DOT) ? 4 : 5;
   for(int x = left; x <= right; x += on + off)
     {
      //--- One dash across every row of the stroke
      for(int row = rowFrom; row <= rowTo; row++)
         PaintHorizontalSpan(row, x, MathMin(x + on - 1, right), argb);
     }
  }
//+------------------------------------------------------------------+
//| Paint a small diamond centered on a point                        |
//+------------------------------------------------------------------+
void DrawDiamond(const int centerX, const double centerY, const int radius, const uint argb)
  {
//--- Only near the canvas
   if(!NearCanvas(centerX, centerY))
      return;
//--- Rows narrow evenly toward both tips, so the shape is symmetric by construction
   int center = (int)MathRound(centerY);
   for(int row = -radius; row <= radius; row++)
     {
      //--- Half width at this row
      double half = radius - MathAbs(row);
      PaintHorizontalSpan(center + row, centerX - half, centerX + half, argb);
     }
  }
//+------------------------------------------------------------------+
//| Paint a price label to the right of a level                      |
//+------------------------------------------------------------------+
void DrawPriceLabel(const int rightX, const double centerY, const string text, const color levelColor)
  {
//--- Only near the canvas
   if(!NearCanvas(rightX, centerY))
      return;
//--- Measure
   TextSetFont("Arial", InpLabelFontSize, FW_BOLD);
   uint textWidth = 0;
   uint textHeight = 0;
   if(!TextGetSize(text, textWidth, textHeight))
      return;
//--- Solid chip in the level color with white text
   int padding = 3;
   int left = rightX + 4;
   int centerRow = (int)MathRound(centerY);
   int top = centerRow - (int)textHeight / 2 - padding;
   g_canvas.FillRectangle(left, top, left + (int)textWidth + padding * 2, top + (int)textHeight + padding * 2, ArgbFromOpacity(levelColor, 100));
   g_canvas.DrawTextClean(left + padding, centerRow, text, ColorToARGB(clrWhite, 255), TA_LEFT | TA_VCENTER);
  }
//+------------------------------------------------------------------+
//| Paint the unbroken flip levels with diamonds and price labels    |
//+------------------------------------------------------------------+
void DrawFlipLevels()
  {
//--- Skip when hidden
   if(!InpShowFlipLevels)
      return;
//--- Each level runs from its own bar to the current bar
   int xLive = BarToX(g_ratesTotal - 1);
   for(int i = 0; i < ArraySize(g_flipLevels); i++)
     {
      //--- Color by the side it was born on
      color levelColor = g_flipLevels[i].isBullish ? InpBullishColor : InpBearishColor;
      double y = PriceToY(g_flipLevels[i].price);
      if(!NearCanvas(0, y))
         continue;
      int xStart = BarToX(g_flipLevels[i].bar);
      DrawHorizontalRun(xStart, xLive, y, ArgbFromOpacity(levelColor, InpFlipLevelOpacity), InpFlipLevelWidth, InpFlipLevelStyle);
      //--- Diamond at the birth bar and the price at the right end
      DrawDiamond(xStart, y, 4, ArgbFromOpacity(levelColor, 100));
      if(InpShowFlipLevelPrice)
         DrawPriceLabel(xLive, y, DoubleToString(g_flipLevels[i].price, _Digits), levelColor);
     }
  }
//+------------------------------------------------------------------+
//| Paint the flip arrows, one filled triangle each                  |
//+------------------------------------------------------------------+
void DrawFlipMarkers()
  {
//--- Skip when hidden
   if(!InpShowFlipMarkers)
      return;
//--- Every stored marker still on screen
   for(int i = 0; i < ArraySize(g_flipMarkers); i++)
     {
      //--- Empty slot or off screen
      if(g_flipMarkers[i].bar < 0)
         continue;
      int x = BarToX(g_flipMarkers[i].bar);
      double tipY = PriceToY(g_flipMarkers[i].price);
      if(!NearCanvas(x, tipY))
         continue;
      //--- Direction sets the color and which way the tip points
      color markerColor = g_flipMarkers[i].isBullish ? InpBullishColor : InpBearishColor;
      int up = g_flipMarkers[i].isBullish ? 1 : -1;
      //--- A flip the VWAP disagreed with is drawn softer
      double opacity = g_flipMarkers[i].isConfirmed ? 100.0 : 45.0;
      uint markerArgb = ArgbFromOpacity(markerColor, opacity);
      //--- Rows widen evenly from the tip, so both sides match on every row
      int tipRow = (int)MathRound(tipY);
      for(int step = 0; step <= InpArrowSize; step++)
        {
         //--- Half width grows with the distance from the tip
         double half = (double)step * 0.6;
         PaintHorizontalSpan(tipRow + step * up, x - half, x + half, markerArgb);
        }
     }
  }
//+------------------------------------------------------------------+
//| Repaint everything from scratch                                  |
//+------------------------------------------------------------------+
void FullRedraw()
  {
//--- Need a live canvas, a usable viewport, and data
   if(!g_canvasReady || g_chartWidth <= 0 || g_chartHeight <= 0 || g_ratesTotal < 2)
      return;
   if(!EnsureCanvasSize())
      return;
//--- Clear, then paint back to front
   g_canvas.Erase(0);
   DrawCloud();
   DrawFlipLevels();
   DrawFlipMarkers();
//--- The stop line goes on last so nothing paints over it
   DrawStopLine();
//--- Publish
   g_canvas.Update();
  }
//+------------------------------------------------------------------+
//| Fill the colored candle plot for one bar                         |
//+------------------------------------------------------------------+
void PublishCandle(const int bar, const double &open[], const double &high[], const double &low[], const double &close[],
                   const double stopForShade, const bool bullish)
  {
//--- Without coloring or a stop the plot stays empty and the chart shows its own candle
   if(!InpColorCandles || stopForShade == EMPTY_VALUE)
     {
      //--- Nothing drawn on this bar
      g_candleOpen[bar] = EMPTY_VALUE;
      g_candleHigh[bar] = EMPTY_VALUE;
      g_candleLow[bar] = EMPTY_VALUE;
      g_candleClose[bar] = EMPTY_VALUE;
      g_candleColorIndex[bar] = 0.0;
      return;
     }
//--- Copy the bar into the plot
   g_candleOpen[bar] = open[bar];
   g_candleHigh[bar] = high[bar];
   g_candleLow[bar] = low[bar];
   g_candleClose[bar] = close[bar];
//--- Shade by distance from the stop, brighter further away
   double fastRange = g_fastAtr[MathMin(bar, g_ratesTotal - 2)];
   double share = 0.5;
   if(fastRange != EMPTY_VALUE && fastRange > 0.0)
      share = MathMin(MathAbs(close[bar] - stopForShade) / (fastRange * 3.0), 1.0);
   int shade = (int)MathRound(share * (CANDLE_SHADES - 1));
//--- Bullish shades come first in the palette
   g_candleColorIndex[bar] = (double)((bullish ? 0 : CANDLE_SHADES) + shade);
  }
//+------------------------------------------------------------------+
//| Process one closed bar through the full pipeline                 |
//+------------------------------------------------------------------+
void ProcessBar(const int bar, const bool alertable, const datetime &time[], const double &open[], const double &high[],
                const double &low[], const double &close[], const long &tick_volume[], const long &volume[])
  {
//--- Midpoint for the price reference
   g_midPrices[bar] = (high[bar] + low[bar]) / 2.0;
//--- Blank this bar's signal slot
   g_flipBuffer[bar] = 0.0;
//--- Wait for the slow range
   double slowRange = g_slowAtr[bar];
   if(slowRange == EMPTY_VALUE || slowRange <= 0.0)
     {
      //--- Nothing to publish yet
      g_stopValues[bar] = EMPTY_VALUE;
      g_smoothedStop[bar] = EMPTY_VALUE;
      g_priceReference[bar] = EMPTY_VALUE;
      //--- Buffers stay empty too
      g_stopBuffer[bar] = EMPTY_VALUE;
      g_trendBuffer[bar] = 0.0;
      PublishCandle(bar, open, high, low, close, EMPTY_VALUE, true);
      return;
     }
//--- Seed on the first usable bar
   bool flipped = false;
   if(!g_engineStarted)
     {
      //--- Start bullish under the bar
      g_isBullish = true;
      g_stopPrice = low[bar] - slowRange * InpStartDistanceAtr;
      g_speed = 0.0;
      //--- From here the engine runs and the first trend begins
      g_engineStarted = true;
      g_firstDrawBar = bar;
     }
   else
     {
      //--- A close through the stop flips the trend
      bool nowBullish = g_isBullish;
      if(close[bar] < g_stopPrice)
         nowBullish = false;
      if(close[bar] > g_stopPrice)
         nowBullish = true;
      flipped = (nowBullish != g_isBullish);
      g_isBullish = nowBullish;
     }
//--- Which VWAPs sit with the trend
   UpdateVwapAgreement(bar, g_isBullish, close);
//--- A flip reseeds the stop and records the level and the marker
   if(flipped)
     {
      //--- Reseed away from the bar that caused the flip
      g_stopPrice = g_isBullish ? low[bar] - slowRange * InpStartDistanceAtr : high[bar] + slowRange * InpStartDistanceAtr;
      g_speed = 0.0;
      //--- Grade the flip against the reference VWAP
      bool confirmed = VwapAgrees();
      g_flipBuffer[bar] = (g_isBullish ? 1.0 : -1.0) * (confirmed ? 2.0 : 1.0);
      AddFlipLevel(bar, g_isBullish ? low[bar] : high[bar], g_isBullish);
      //--- Arrow sits just beyond the flip candle, under a bullish flip and over a bearish one
      double fastRange = g_fastAtr[bar];
      double offset = (fastRange == EMPTY_VALUE ? slowRange : fastRange) * InpArrowOffsetAtr;
      AddFlipMarker(bar, g_isBullish ? low[bar] - offset : high[bar] + offset, g_isBullish, confirmed);
      //--- Announce
      string direction = g_isBullish ? "Bullish" : "Bearish";
      if(confirmed && InpAlertConfirmedFlip && alertable)
         SendTrendAlert(direction + " flip confirmed by VWAP at " + DoubleToString(close[bar], _Digits));
      if(!confirmed && InpAlertFilteredFlip && alertable)
         SendTrendAlert(direction + " flip, VWAP disagrees, at " + DoubleToString(close[bar], _Digits));
     }
   else
      if(bar > g_firstDrawBar)
        {
         //--- Accelerate, capped, then close the gap toward price
         g_speed += AccelerationAt(bar, close) / InpSmoothingBars;
         if(InpMaxSpeed > 0.0 && g_speed > InpMaxSpeed)
            g_speed = InpMaxSpeed;
         double step = slowRange * InpStepSizeAtr * g_speed;
         g_stopPrice += g_isBullish ? step : -step;
        }
//--- Publish the raw stop of this bar
   g_stopValues[bar] = g_stopPrice;
//--- Levels this bar closed through
   UpdateFlipLevels(bar, close[bar], alertable);
//--- Smoothed series the stop line and the cloud are drawn from, empty on the flip bar so the run breaks there
   g_smoothedStop[bar] = flipped ? EMPTY_VALUE : AverageOver(g_stopValues, bar, InpSmoothingBars);
   g_priceReference[bar] = AverageOver(g_midPrices, bar, InpSmoothingBars * 5);
//--- Publish the trend context
   g_stopBuffer[bar] = g_stopValues[bar];
   g_trendBuffer[bar] = g_isBullish ? 1.0 : -1.0;
//--- The flip bar shades from the raw stop
   PublishCandle(bar, open, high, low, close, flipped ? g_stopValues[bar] : g_smoothedStop[bar], g_isBullish);
  }
//+------------------------------------------------------------------+
//| Reset every piece of state and clear the canvas                  |
//+------------------------------------------------------------------+
void ResetIndicatorState()
  {
//--- Engine state
   g_engineStarted = false;
   g_isBullish = true;
   g_stopPrice = 0.0;
   g_speed = 0.0;
   g_firstDrawBar = 0;
//--- Range accumulators
   g_fastSeedSum = 0.0;
   g_slowSum = 0.0;
   g_slowSumBar = -1;
//--- Session, week, month accumulators
   g_sessionPriceVolume = 0.0;
   g_sessionVolume = 0.0;
   g_weeklyPriceVolume = 0.0;
   g_weeklyVolume = 0.0;
   g_monthlyPriceVolume = 0.0;
   g_monthlyVolume = 0.0;
//--- Levels and markers
   ArrayResize(g_flipLevels, 0);
   ArrayResize(g_flipMarkers, InpMaxFlipMarkersKept);
   for(int i = 0; i < InpMaxFlipMarkersKept; i++)
     {
      //--- Blank slot
      g_flipMarkers[i].bar = -1;
      g_flipMarkers[i].price = 0.0;
      g_flipMarkers[i].isBullish = true;
      g_flipMarkers[i].isConfirmed = false;
     }
   g_flipMarkerSlot = 0;
//--- Agreement flags and processing markers
   g_sessionAgrees = false;
   g_weeklyAgrees = false;
   g_monthlyAgrees = false;
   g_lastProcessedBarTime = 0;
//--- Working arrays start empty
   ArrayResize(g_stopValues, 0);
//--- Force the next viewport read to count as a change
   g_chartWidth = 0;
   g_chartHeight = 0;
//--- Clear the canvas when it exists
   if(g_canvasReady)
     {
      //--- Publish a transparent frame
      g_canvas.Erase(0);
      g_canvas.Update();
     }
  }
//+------------------------------------------------------------------+
//| Create the single canvas object                                  |
//+------------------------------------------------------------------+
bool CreateCanvas()
  {
//--- Start from the current chart size, one pixel minimum
   int width = MathMax(1, (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0));
   int height = MathMax(1, (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, 0));
//--- Create the bitmap label at the chart origin
   g_canvasName = OBJECT_PREFIX + "Canvas";
   if(!g_canvas.CreateBitmapLabel(0, 0, g_canvasName, 0, 0, width, height, COLOR_FORMAT_ARGB_NORMALIZE))
      return(false);
//--- The cloud belongs under the candles, set once here and never touched again
   ObjectSetInteger(0, g_canvasName, OBJPROP_BACK, true);
//--- Publish transparent so nothing flashes before the first paint
   g_canvas.Erase(0);
   g_canvas.Update();
   g_canvasReady = true;
   return(true);
  }
//+------------------------------------------------------------------+
//| Validate the inputs before the indicator starts                  |
//+------------------------------------------------------------------+
bool ValidateInputs()
  {
//--- Require positive rates and distances
   if(InpAccelerationRate <= 0.0 || InpMaxSpeed < 0.0 || InpStartDistanceAtr <= 0.0 || InpStepSizeAtr <= 0.0 || InpArrowOffsetAtr <= 0.0)
     {
      //--- Report the problem
      Print("Acceleration, distances and step size must be positive, max speed cannot be negative");
      return(false);
     }
//--- Require usable periods with the slow range above the fast one
   if(InpSmoothingBars < 1 || InpFastAtrPeriod < 1 || InpSlowAtrPeriod <= InpFastAtrPeriod)
     {
      //--- Report the problem
      Print("Smoothing at least 1, ATR periods at least 1, and the slow period above the fast one");
      return(false);
     }
//--- Require a sane boost and opacities
   if(InpVwapSpeedBoost < 1.0 || InpVwapBoostRangeAtr <= 0.0 || InpCloudOpacity < 0 || InpCloudOpacity > 100
      || InpFlipLevelOpacity < 0 || InpFlipLevelOpacity > 100)
     {
      //--- Report the problem
      Print("Speed boost at least 1, boost range positive, opacities between 0 and 100");
      return(false);
     }
//--- Require usable counts, widths and history
   if(InpMaxFlipLevels < 1 || InpMaxFlipMarkersKept < 1 || InpFlipLevelWidth < 1 || InpStopLineWidth < 1 || InpMaxHistoryBars < 0 || InpLabelFontSize < 7 || InpArrowSize < 3)
     {
      //--- Report the problem
      Print("Counts and widths at least 1, history not negative, label font at least 7");
      return(false);
     }
//--- Everything is acceptable
   return(true);
  }
//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- Stop on invalid inputs
   if(!ValidateInputs())
      return(INIT_PARAMETERS_INCORRECT);
//--- The colored candle plot takes the first five buffers
   SetIndexBuffer(0, g_candleOpen, INDICATOR_DATA);
   SetIndexBuffer(1, g_candleHigh, INDICATOR_DATA);
   SetIndexBuffer(2, g_candleLow, INDICATOR_DATA);
   SetIndexBuffer(3, g_candleClose, INDICATOR_DATA);
   SetIndexBuffer(4, g_candleColorIndex, INDICATOR_COLOR_INDEX);
//--- The trend context follows in data buffers an EA can read
   SetIndexBuffer(5, g_stopBuffer, INDICATOR_DATA);
   SetIndexBuffer(6, g_trendBuffer, INDICATOR_DATA);
   SetIndexBuffer(7, g_flipBuffer, INDICATOR_DATA);
//--- Use forward indexing on every buffer
   ArraySetAsSeries(g_candleOpen, false);
   ArraySetAsSeries(g_candleHigh, false);
   ArraySetAsSeries(g_candleLow, false);
   ArraySetAsSeries(g_candleClose, false);
   ArraySetAsSeries(g_candleColorIndex, false);
//--- Trend context buffers
   ArraySetAsSeries(g_stopBuffer, false);
   ArraySetAsSeries(g_trendBuffer, false);
   ArraySetAsSeries(g_flipBuffer, false);
//--- Plot zero draws the candles, hidden when coloring is switched off
   PlotIndexSetInteger(0, PLOT_DRAW_TYPE, InpColorCandles ? DRAW_COLOR_CANDLES : DRAW_NONE);
   PlotIndexSetInteger(0, PLOT_COLOR_INDEXES, CANDLE_SHADES * 2);
   PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetString(0, PLOT_LABEL, "Open;High;Low;Close");
//--- The palette runs from the faded trend color to the full one, bullish shades first
   g_chartBackgroundColor = (color)ChartGetInteger(0, CHART_COLOR_BACKGROUND);
   for(int shade = 0; shade < CANDLE_SHADES; shade++)
     {
      //--- Blend toward the background for the near shades
      int opacity = 45 + shade * (100 - 45) / (CANDLE_SHADES - 1);
      PlotIndexSetInteger(0, PLOT_LINE_COLOR, shade, BlendTowardBackground(InpBullishColor, opacity));
      PlotIndexSetInteger(0, PLOT_LINE_COLOR, CANDLE_SHADES + shade, BlendTowardBackground(InpBearishColor, opacity));
     }
//--- Plot one carries the stop into the Data Window without drawing
   PlotIndexSetInteger(1, PLOT_DRAW_TYPE, DRAW_NONE);
   PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetString(1, PLOT_LABEL, "Trend Stop");
//--- Give this instance its own object prefix so several copies can share a chart
   g_objectPrefix = "VATS_" + IntegerToString((int)(GetMicrosecondCount() % 1000000)) + "_";
//--- Create the canvas
   if(!CreateCanvas())
     {
      //--- Report the problem
      Print(__FUNCTION__, ": failed to create the canvas");
      return(INIT_FAILED);
     }
//--- Start from a clean state
   ResetIndicatorState();
//--- Name the indicator and match the chart digits
   IndicatorSetString(INDICATOR_SHORTNAME, "VWAP Adaptive Trend Stop");
   IndicatorSetInteger(INDICATOR_DIGITS, _Digits);
//--- Ready
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- Free the canvas and its bitmap label
   g_canvas.Destroy();
   g_canvasReady = false;
//--- Remove anything else carrying the prefix
   ObjectsDeleteAll(0, OBJECT_PREFIX);
//--- Refresh the chart
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| Custom indicator 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[])
  {
//--- Wait for the slow range plus a closed bar
   int minimumBars = InpSlowAtrPeriod + 2;
   if(rates_total < minimumBars)
      return(0);
//--- Identify the closed and forming bars
   int lastClosedBar = rates_total - 2;
   int formingBar = rates_total - 1;
   g_ratesTotal = rates_total;
//--- Detect a full recalculation
   bool fullRecompute = (prev_calculated == 0);
//--- Ticks that did not close a bar recolor the forming candle and repaint only when the view moved
   if(!fullRecompute && time[lastClosedBar] <= g_lastProcessedBarTime)
     {
      //--- Five buffer writes so the live candle follows the trend like the closed ones
      if(g_engineStarted)
         PublishCandle(formingBar, open, high, low, close, g_stopPrice, g_isBullish);
      //--- The view is read on every call, a moved mapping repaints everything
      if(ReadViewport())
         FullRedraw();
      return(rates_total);
     }
//--- Resolve where processing starts
   int startBar = 0;
   if(fullRecompute)
     {
      //--- Wipe every piece of state and blank the plot buffers
      ResetIndicatorState();
      ArrayInitialize(g_candleOpen, EMPTY_VALUE);
      ArrayInitialize(g_candleHigh, EMPTY_VALUE);
      ArrayInitialize(g_candleLow, EMPTY_VALUE);
      ArrayInitialize(g_candleClose, EMPTY_VALUE);
      ArrayInitialize(g_candleColorIndex, 0.0);
      //--- Trend context buffers
      ArrayInitialize(g_stopBuffer, EMPTY_VALUE);
      ArrayInitialize(g_trendBuffer, 0.0);
      ArrayInitialize(g_flipBuffer, 0.0);
      //--- Replay only the history window the user asked for
      startBar = 1;
      if(InpMaxHistoryBars > 0 && rates_total - InpMaxHistoryBars > startBar)
         startBar = rates_total - InpMaxHistoryBars;
      //--- Never start past the last closed bar
      if(startBar > lastClosedBar)
         startBar = lastClosedBar;
     }
   else
     {
      //--- Continue from the bar that was forming last time
      startBar = MathMax(1, prev_calculated - 1);
     }
//--- Working arrays always cover the chart
   EnsureArrays(rates_total);
//--- Ranges and averages are cheap and recursive, so they always run from the first bar
   int seriesStart = fullRecompute ? 1 : startBar;
   for(int bar = seriesStart; bar <= lastClosedBar; bar++)
     {
      //--- Skip bars already handled
      if(time[bar] <= g_lastProcessedBarTime)
         continue;
      UpdateRanges(bar, high, low, close);
      UpdateVwaps(bar, time, high, low, tick_volume, volume);
     }
//--- The stop engine runs only over the requested history window
   for(int bar = startBar; bar <= lastClosedBar; bar++)
     {
      //--- Skip bars already processed
      if(time[bar] <= g_lastProcessedBarTime)
         continue;
      //--- Allow alerts only for the newest closed bar during live updates
      bool alertable = (!fullRecompute && bar == lastClosedBar);
      //--- Run the pipeline
      ProcessBar(bar, alertable, time, open, high, low, close, tick_volume, volume);
      //--- Remember the bar as handled
      g_lastProcessedBarTime = time[bar];
     }
//--- The forming candle takes the live trend shade, the rest of its context is not final
   if(g_engineStarted)
      PublishCandle(formingBar, open, high, low, close, g_stopPrice, g_isBullish);
   else
      PublishCandle(formingBar, open, high, low, close, EMPTY_VALUE, true);
//--- The trend context of the forming bar is not final either
   g_stopBuffer[formingBar] = EMPTY_VALUE;
   g_trendBuffer[formingBar] = 0.0;
   g_flipBuffer[formingBar] = 0.0;
   g_smoothedStop[formingBar] = EMPTY_VALUE;
   g_priceReference[formingBar] = EMPTY_VALUE;
//--- A new bar shifts the picture, repaint everything
   ReadViewport();
   FullRedraw();
//--- Return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
//--- Only chart change events matter here
   if(id != CHARTEVENT_CHART_CHANGE)
      return;
//--- Repaint only when the mapping actually moved
   if(ReadViewport())
      FullRedraw();
  }
//+------------------------------------------------------------------+
