//+------------------------------------------------------------------+
//|                                  Liquidity_Surge_Markov_3D.mq5   |
//|      Liquidity Surge Forecast with Markov Chains [TechnicalZen]  |
//|               MQL5 High-Performance 3D Projection Port           |
//+------------------------------------------------------------------+
#property copyright   "TechnicalZen / MQL5 Port"
#property link        "https://www.tradingview.com/script/TechnicalZen/"
#property version     "2.50"
#property description "Liquidity Surge Forecast with 2nd-Order Markov Chains, 3D Momentum Terrain & Oscillator Carpet"
#property indicator_separate_window
#property indicator_buffers 9
#property indicator_plots   0

#property indicator_minimum -100
#property indicator_maximum 100

#include <Canvas\Canvas.mqh>

//--- ENUM DEFINITIONS
enum ENUM_MOMENTUM_SRC {
   MOM_MFI = 0,    // MFI (Money Flow Index)
   MOM_RSI = 1     // RSI (Relative Strength Index)
};

enum ENUM_OSC_TYPE {
   OSC_HULL_VWMA = 0,  // Hull-VWMA
   OSC_ADX       = 1   // ADX (Signed DMI)
};

enum ENUM_DASH_POS {
   DASH_TOP_RIGHT = 0,   // Top Right
   DASH_TOP_LEFT  = 1,   // Top Left
   DASH_MID_RIGHT = 2,   // Middle Right
   DASH_MID_LEFT  = 3,   // Middle Left
   DASH_BOT_RIGHT = 4,   // Bottom Right
   DASH_BOT_LEFT  = 5    // Bottom Left
};

enum ENUM_DASH_SIZE {
   SIZE_TINY   = 0,  // Tiny
   SIZE_SMALL  = 1,  // Small
   SIZE_NORMAL = 2,  // Normal
   SIZE_LARGE  = 3,  // Large
   SIZE_HUGE   = 4   // Huge
};

//--- INPUT PARAMETERS
input group "---- CORE SETTINGS ----"
input int               InpTimeSpan       = 15;             // Time Span (bars each side of 0)
input ENUM_MOMENTUM_SRC InpMomentumSrc    = MOM_MFI;        // Momentum Source
input int               InpMomentumLen    = 14;             // Momentum Length
input double            InpRibAmp         = 2.0;            // Rib Amplification (0.5 - 5.0)
input int               InpCalcBars       = 2000;           // Calculation Bars Limit

input group "---- 3D CAMERA & PROJECTION ----"
input double            InpYawDeg         = -20.0;          // Yaw (°) [-180..180]
input double            InpPitchDeg       = 15.0;           // Pitch (°) [1..89]
input double            InpScaleTime      = 1.2;            // Scale X (Time, bars per unit)
input double            InpScaleAlt       = 0.4;            // Scale Y (Vertical)
input double            InpScaleDepth     = 0.4;            // Scale Z (Depth)
input double            InpBoxScale       = 1.0;            // Overall 3D Box Size Multiplier
input int               InpOffsetX        = 0;              // Box Offset X (Pixels)
input int               InpOffsetY        = 0;              // Box Offset Y (Pixels)

input group "---- OSCILLATOR CARPET ----"
input ENUM_OSC_TYPE     InpOscType        = OSC_HULL_VWMA;  // Oscillator Type
input int               InpVWMALen        = 30;             // VWMA Length
input double            InpOscScale       = 500.0;          // Hull-VWMA Scale
input int               InpADXLen         = 14;             // ADX Length
input bool              InpShowOsc        = true;           // Show Oscillator Carpet
input color             InpOscBullCol     = C'0,188,212';   // Bull Color (Cyan #00BCD4)
input color             InpOscBearCol     = C'128,0,0';     // Bear Color (Maroon #800000)
input color             InpOscNeutCol     = C'135,206,235'; // Neutral Tint (Light Blue #87CEEB)

input group "---- DISPLAY OPTIONS ----"
input bool              InpShowAxisLabels = false;          // Show Axis Markers
input bool              InpShowEmoji      = true;           // Show Emoji Markers

input group "---- FORECAST ----"
input bool              InpShowForecast   = true;           // Show Slope Forecast
input int               InpSlopeLookback  = 6;              // Slope Lookback (bars)
input double            InpForecastDecay  = 0.92;           // Forecast Decay per Bar

input group "---- CHOPPINESS FILTER ----"
input bool              InpUseChopFilter  = true;           // Gate Whale by Choppiness
input int               InpChopLen        = 14;             // CI Length
input double            InpChopThresh     = 61.8;           // CI Threshold (chop above)
input int               InpAngleShortLen  = 12;             // Angle Short Period
input int               InpAngleLongLen   = 26;             // Angle Long Period
input double            InpAngleTrendThresh = 5.0;          // Angle Trend Threshold (°)
input double            InpAngleSideThresh  = 2.0;          // Angle Sideways Threshold (°)

input group "---- MARKOV PREDICTOR ----"
input bool              InpUseMarkov      = true;           // Use Markov Predictor
input double            InpMarkovDecay    = 0.997;          // Count Decay per Bar
input double            InpPreWhaleThresh = 55.0;           // Hatchling Threshold %
input bool              InpShowMarkovRow  = true;           // Show Markov Row in Dash

input group "---- DASHBOARD ----"
input bool              InpShowDash       = true;           // Show Win-Rate Dashboard
input ENUM_DASH_POS     InpDashPos        = DASH_BOT_RIGHT; // Dashboard Position
input ENUM_DASH_SIZE    InpDashSize       = SIZE_NORMAL;    // Dashboard Text Size
input int               InpATRLen         = 14;             // ATR Length
input double            InpATRMult        = 0.5;            // Win Threshold (× ATR)
input color             InpDashTextCol    = clrWhite;       // Dashboard Text
input color             InpDashBgCol      = C'18,18,24';    // Dashboard Background

input group "---- STYLE & DEPTH FOG ----"
input bool              InpShowFloorGrid  = true;           // Y=0 Floor Grid (dotted)
input bool              InpShowBackGrid   = false;          // Back Wall Grid (dotted)
input bool              InpShowSideGrid   = false;          // Side Wall Grid (dotted)
input int               InpGridStep       = 25;             // Grid Spacing
input color             InpBoxColor       = C'70,70,85';    // Box Wireframe Color
input color             InpNowPlaneCol    = C'120,120,140'; // X=0 Plane Color
input color             InpYZeroEdge      = C'100,181,246'; // Y=0 Horizon Color (#64B5F6)
input color             InpAxisLabelCol   = C'160,160,180'; // Axis Markers Color
input color             InpGridColor      = C'50,50,65';    // Wireframe Grid Color
input color             InpFogColor       = C'26,26,46';    // Depth Fog Color (#1A1A2E)
input double            InpFogStrength    = 0.55;           // Depth Fog Strength

//--- INDICATOR CALCULATION BUFFERS (Accessible via iCustom)
double BufferBullSignal[];
double BufferBearSignal[];
double BufferRegime[];
double BufferChoppiness[];
double BufferIsChoppy[];
double BufferMarkovPBull[];
double BufferMarkovPBear[];
double BufferMomMid[];
double BufferOscMid[];

//--- 3D CONSTANTS
#define MOM_LAYERS 20
#define OSC_LAYERS 12
#define MAX_RING_BUF 128

//--- LAYER LENGTHS
const int c_momLengths[MOM_LAYERS] = {3, 4, 5, 6, 8, 10, 12, 14, 17, 20, 23, 26, 30, 34, 38, 42, 46, 50, 55, 60};
const int c_oscLengths[OSC_LAYERS] = {3, 5, 7, 10, 14, 19, 25, 32, 40, 49, 59, 70};

//--- RING BUFFER FOR 3D TERRAIN
struct SLayerData {
   float momentum[MOM_LAYERS];
   float oscillator[OSC_LAYERS];
};
SLayerData g_ringBuf[MAX_RING_BUF];
int        g_ringHead = 0;

//+------------------------------------------------------------------+
//| High-Performance 3D Software Alpha-Blending Canvas               |
//+------------------------------------------------------------------+
class C3DCanvas : public CCanvas {
public:
   // Blend a pixel in XRGB format with alpha 0..255
   inline void BlendPixel(int x, int y, uint srcRGB, uchar alpha) {
      if(x < 0 || x >= m_width || y < 0 || y >= m_height || alpha == 0) return;
      int idx = y * m_width + x;
      uint dst = m_pixels[idx];
      uint invA = 255 - alpha;
      uint r = (((srcRGB >> 16) & 0xFF) * alpha + ((dst >> 16) & 0xFF) * invA) / 255;
      uint g = (((srcRGB >> 8) & 0xFF) * alpha + ((dst >> 8) & 0xFF) * invA) / 255;
      uint b = ((srcRGB & 0xFF) * alpha + (dst & 0xFF) * invA) / 255;
      m_pixels[idx] = 0xFF000000 | (r << 16) | (g << 8) | b;
   }

   // Alpha-blended horizontal scanline
   inline void BlendHLine(int x1, int x2, int y, uint srcRGB, uchar alpha) {
      if(y < 0 || y >= m_height || alpha == 0) return;
      if(x1 > x2) { int tmp = x1; x1 = x2; x2 = tmp; }
      if(x1 < 0) x1 = 0;
      if(x2 >= m_width) x2 = m_width - 1;
      if(x1 > x2) return;

      int idx = y * m_width + x1;
      uint invA = 255 - alpha;
      uint sr = (srcRGB >> 16) & 0xFF;
      uint sg = (srcRGB >> 8) & 0xFF;
      uint sb = srcRGB & 0xFF;

      for(int x = x1; x <= x2; x++, idx++) {
         uint dst = m_pixels[idx];
         uint r = (sr * alpha + ((dst >> 16) & 0xFF) * invA) / 255;
         uint g = (sg * alpha + ((dst >> 8) & 0xFF) * invA) / 255;
         uint b = (sb * alpha + (dst & 0xFF) * invA) / 255;
         m_pixels[idx] = 0xFF000000 | (r << 16) | (g << 8) | b;
      }
   }

   // Alpha-blended Triangle rasterization (Standard fast scanline)
   void FillTriangleAlpha(int x1, int y1, int x2, int y2, int x3, int y3, uint srcRGB, uchar alpha) {
      if(alpha == 0) return;
      // Sort vertices by Y (y1 <= y2 <= y3)
      if(y1 > y2) { int tx = x1; x1 = x2; x2 = tx; int ty = y1; y1 = y2; y2 = ty; }
      if(y1 > y3) { int tx = x1; x1 = x3; x3 = tx; int ty = y1; y1 = y3; y3 = ty; }
      if(y2 > y3) { int tx = x2; x2 = x3; x3 = tx; int ty = y2; y2 = y3; y3 = ty; }

      if(y1 == y3) return; // Degenerate triangle

      int totalH = y3 - y1;
      for(int i = 0; i < totalH; i++) {
         int cy = y1 + i;
         if(cy < 0 || cy >= m_height) continue;
         bool secondHalf = (i > y2 - y1 || y2 == y1);
         int segmentH = secondHalf ? (y3 - y2) : (y2 - y1);
         if(segmentH <= 0) segmentH = 1;

         double alphaF = (double)i / totalH;
         double betaF  = (double)(i - (secondHalf ? (y2 - y1) : 0)) / segmentH;

         int ax = (int)MathRound(x1 + (x3 - x1) * alphaF);
         int bx = secondHalf ? (int)MathRound(x2 + (x3 - x2) * betaF) : (int)MathRound(x1 + (x2 - x1) * betaF);

         BlendHLine(ax, bx, cy, srcRGB, alpha);
      }
   }

   // Fast Alpha-blended Quad rasterization (2 triangles)
   void FillQuadAlpha(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4, uint srcRGB, uchar alpha) {
      FillTriangleAlpha(x1, y1, x2, y2, x3, y3, srcRGB, alpha);
      FillTriangleAlpha(x1, y1, x3, y3, x4, y4, srcRGB, alpha);
   }

   // Alpha-blended Bresenham Line
   void DrawLineAlpha(int x1, int y1, int x2, int y2, uint srcRGB, uchar alpha) {
      if(alpha == 0) return;
      int dx = MathAbs(x2 - x1), dy = MathAbs(y2 - y1);
      int sx = (x1 < x2) ? 1 : -1;
      int sy = (y1 < y2) ? 1 : -1;
      int err = dx - dy;

      while(true) {
         BlendPixel(x1, y1, srcRGB, alpha);
         if(x1 == x2 && y1 == y2) break;
         int e2 = 2 * err;
         if(e2 > -dy) { err -= dy; x1 += sx; }
         if(e2 < dx)  { err += dx; y1 += sy; }
      }
   }

   // Subtle Dotted Line with Alpha
   void DrawDottedLineAlpha(int x1, int y1, int x2, int y2, uint srcRGB, uchar alpha, int step = 4) {
      int dx = x2 - x1, dy = y2 - y1;
      double len = MathSqrt((double)(dx * dx + dy * dy));
      if(len < 3.0) {
         DrawLineAlpha(x1, y1, x2, y2, srcRGB, alpha);
         return;
      }
      int numSegments = (int)(len / (double)step);
      if(numSegments <= 0) numSegments = 1;
      for(int i = 0; i < numSegments; i += 2) {
         int xa = (int)MathRound(x1 + dx * ((double)i / numSegments));
         int ya = (int)MathRound(y1 + dy * ((double)i / numSegments));
         int xb = (int)MathRound(x1 + dx * ((double)(i + 1) / numSegments));
         int yb = (int)MathRound(y1 + dy * ((double)(i + 1) / numSegments));
         DrawLineAlpha(xa, ya, xb, yb, srcRGB, alpha);
      }
   }
};

//--- GRAPHICS CANVAS
C3DCanvas  g_canvas;
int        g_w = 0, g_h = 0;
int        g_subwindow = -1;
string     g_canvasName = "";
bool       g_isReady = false;
uint       g_lastFrameTick = 0;

//--- MARKOV CHAIN PERSISTENT STATE
double McShort[9][3];
double McLong[9][3];
int    McState = 1, McStateDur = 1;
int    McPrevState1 = 1, McPrevState2 = 1, McPrevDur1 = 1;
bool   McHavePrev1 = false, McHavePrev2 = false;
double PBull = 0.333, PSide = 0.333, PBear = 0.333;
double McSampleN = 0.0;
bool   McConfident = false;
int    McVote = 1;
const int    McDurSplit  = 5;
const double McAlpha     = 1.0;
const int    McMinSample = 10;

//--- CHOPPINESS PERSISTENT STATE
bool   ChopCIPrev = false, ChopAnglePrev = false;
int    DashSharkEvents = 0, DashSharkBars = 0;
int    DashCrabEvents  = 0, DashCrabBars  = 0;
bool   IsChopCI_Last = false, IsChopAngle_Last = false, IsChoppy_Last = false;

//--- WIN-RATE & SIGNAL TRACKING
int    PrevSigDir = 0;      // 1 = bull, -1 = bear, 0 = none
double PrevSigEntry = 0.0;
double PrevSigATR = 0.0;
double RunMaxSince = 0.0;
double RunMinSince = 0.0;
int    DashBullSignals = 0, DashBearSignals = 0;
int    DashBullWins    = 0, DashBearWins    = 0;
bool   DashBullConfPrev = false, DashBearConfPrev = false;

//--- CURRENT STATE DASHBOARD SUMMARY
string CurStateEmoji = "";
double PStayCurrent = 0.333;
int    ExpChangeBars = 1;
bool   StateBalanced = true;
int    NextLikelyState = 1;
string NextLikelyEmoji = "";

//--- BAR TRACKING
long   g_lastStatefulBar = -1;
int    g_ratesTotal = 0;

//+------------------------------------------------------------------+
//| Utility Math Functions                                           |
//+------------------------------------------------------------------+
double Clampd(double v, double lo, double hi) {
   return MathMax(lo, MathMin(hi, v));
}

uint LerpColor(uint c1, uint c2, double t) {
   if(t <= 0.0) return c1;
   if(t >= 1.0) return c2;
   int r1 = (int)((c1 >> 16) & 0xFF), r2 = (int)((c2 >> 16) & 0xFF);
   int g1 = (int)((c1 >> 8) & 0xFF),  g2 = (int)((c2 >> 8) & 0xFF);
   int b1 = (int)(c1 & 0xFF),         b2 = (int)(c2 & 0xFF);
   uint r = (uint)(r1 + (int)((r2 - r1) * t));
   uint g = (uint)(g1 + (int)((g2 - g1) * t));
   uint b = (uint)(b1 + (int)((b2 - b1) * t));
   return 0xFF000000 | (r << 16) | (g << 8) | b;
}

uint DepthTint(uint baseCol, double zPos, uint fogCol, double fogStrength) {
   double depthFactor = (zPos + 100.0) / 200.0;
   depthFactor = MathMax(0.0, MathMin(1.0, depthFactor * fogStrength));
   return LerpColor(baseCol, fogCol, depthFactor);
}

uint GetMomentumColor(double v) {
   uint yellow = 0xFFFFEB3B; // #FFEB3B
   uint green  = 0xFF00E676; // #00E676
   uint red    = 0xFFFF5252; // #FF5252
   if(v >= 50.0) return green;
   if(v <= -50.0) return red;
   if(v >= 0.0) return LerpColor(yellow, green, v / 50.0);
   return LerpColor(yellow, red, (-v) / 50.0);
}

uint GetOscColor(double avgAmp, double magAmp, uint bullCol, uint bearCol, uint neutCol) {
   uint sideCol = (avgAmp >= 0) ? bullCol : bearCol;
   double factor = MathMin(1.0, magAmp / 50.0);
   return LerpColor(neutCol, sideCol, factor);
}

//+------------------------------------------------------------------+
//| UTF-16 Surrogate Emoji Glyphs                                    |
//+------------------------------------------------------------------+
string EmojiWhaleBull() { const ushort u[] = {0xD83D, 0xDC33, 0}; return ShortArrayToString(u); } // 🐳
string EmojiWhaleBear() { const ushort u[] = {0xD83D, 0xDC0B, 0}; return ShortArrayToString(u); } // 🐋
string EmojiShark()     { const ushort u[] = {0xD83E, 0xDD88, 0}; return ShortArrayToString(u); } // 🦈
string EmojiCrab()      { const ushort u[] = {0xD83E, 0xDD80, 0}; return ShortArrayToString(u); } // 🦀
string EmojiSurfer()    { const ushort u[] = {0xD83C, 0xDFC4, 0}; return ShortArrayToString(u); } // 🏄
string EmojiSailboat()  { const ushort u[] = {0x26F5, 0};         return ShortArrayToString(u); } // ⛵

//+------------------------------------------------------------------+
//| Technical Analysis Core Indicators                               |
//+------------------------------------------------------------------+
double GetTR(const double &h[], const double &l[], const double &c[], int idx) {
   if(idx <= 0) return h[0] - l[0];
   double hl = h[idx] - l[idx];
   double hc = MathAbs(h[idx] - c[idx - 1]);
   double lc = MathAbs(l[idx] - c[idx - 1]);
   return MathMax(hl, MathMax(hc, lc));
}

double CalcATR(const double &h[], const double &l[], const double &c[], int len, int idx) {
   if(idx < 0) return 0.0;
   double sum = 0.0;
   int count = 0;
   for(int i = 0; i < len; i++) {
      int j = idx - i;
      if(j < 0) break;
      sum += GetTR(h, l, c, j);
      count++;
   }
   return count > 0 ? sum / count : 0.0;
}

double CalcMFI(const double &h[], const double &l[], const double &c[], const long &v[], int len, int idx) {
   if(idx < len) return 50.0;
   double posFlow = 0.0, negFlow = 0.0;
   for(int i = 0; i < len; i++) {
      int cur = idx - i;
      int prev = cur - 1;
      if(prev < 0) break;
      double tpCur  = (h[cur] + l[cur] + c[cur]) / 3.0;
      double tpPrev = (h[prev] + l[prev] + c[prev]) / 3.0;
      double rawMoneyFlow = tpCur * (double)v[cur];
      if(tpCur > tpPrev)      posFlow += rawMoneyFlow;
      else if(tpCur < tpPrev) negFlow += rawMoneyFlow;
   }
   if(posFlow + negFlow == 0.0) return 50.0;
   return 100.0 * posFlow / (posFlow + negFlow);
}

double CalcRSI(const double &c[], int len, int idx) {
   if(idx < len) return 50.0;
   double gain = 0.0, loss = 0.0;
   for(int i = 0; i < len; i++) {
      int cur = idx - i;
      int prev = cur - 1;
      if(prev < 0) break;
      double diff = c[cur] - c[prev];
      if(diff > 0) gain += diff;
      else         loss -= diff;
   }
   if(loss == 0.0) return 100.0;
   double rs = gain / loss;
   return 100.0 - (100.0 / (1.0 + rs));
}

double CalcVWMA(const double &c[], const long &v[], int len, int idx) {
   if(idx < 0) return 0.0;
   double sumPV = 0.0, sumV = 0.0;
   for(int i = 0; i < len; i++) {
      int j = idx - i;
      if(j < 0) break;
      sumPV += c[j] * (double)v[j];
      sumV  += (double)v[j];
   }
   return sumV != 0.0 ? sumPV / sumV : c[idx];
}

double CalcSignedADX(const double &h[], const double &l[], const double &c[], int len, int idx) {
   if(idx < len + 1) return 0.0;
   double sumPDM = 0.0, sumMDM = 0.0, sumTR = 0.0;
   for(int i = 0; i < len; i++) {
      int j = idx - i;
      if(j < 1) break;
      double pDM = h[j] - h[j - 1];
      double mDM = l[j - 1] - l[j];
      if(pDM < 0) pDM = 0;
      if(mDM < 0) mDM = 0;
      if(pDM > mDM)      mDM = 0;
      else if(mDM > pDM) pDM = 0;
      else { pDM = 0; mDM = 0; }
      sumTR  += GetTR(h, l, c, j);
      sumPDM += pDM;
      sumMDM += mDM;
   }
   if(sumTR == 0.0) return 0.0;
   double pdi = 100.0 * sumPDM / sumTR;
   double mdi = 100.0 * sumMDM / sumTR;
   double diSum = pdi + mdi;
   double adx = (diSum > 0.0) ? (MathAbs(pdi - mdi) / diSum * 100.0) : 0.0;
   return adx * (pdi >= mdi ? 1.0 : -1.0);
}

double ChoppinessIndexAt(const double &h[], const double &l[], const double &c[], int len, int idx) {
   if(idx < len) return 50.0;
   double sumTR = 0.0, hh = -DBL_MAX, ll = DBL_MAX;
   for(int k = 0; k < len; k++) {
      int j = idx - k;
      if(j < 0) break;
      sumTR += GetTR(h, l, c, j);
      if(h[j] > hh) hh = h[j];
      if(l[j] < ll) ll = l[j];
   }
   double range = hh - ll;
   if(range <= 0.0 || sumTR <= 0.0) return 50.0;
   return 100.0 * MathLog10(sumTR / range) / MathLog10((double)len);
}

double AngleFn(const double &src[], int idx, int len) {
   if(idx - len < 0 || idx - 1 < 0) return 0.0;
   double meanChg = (src[idx] - src[idx - len]) / (double)len;
   double sum = 0.0;
   double changes[];
   ArrayResize(changes, len);
   for(int k = 0; k < len; k++) {
      int j = idx - k;
      changes[k] = (j - 1 >= 0) ? (src[j] - src[j - 1]) : 0.0;
      sum += changes[k];
   }
   double mean = sum / len;
   double varSum = 0.0;
   for(int k = 0; k < len; k++) {
      double diff = changes[k] - mean;
      varSum += diff * diff;
   }
   double stdevChg = MathSqrt(varSum / len);
   double normSlope = (stdevChg == 0.0) ? (meanChg != 0.0 ? (meanChg > 0.0 ? 100.0 : -100.0) : 0.0) : (meanChg / stdevChg);
   return MathArctan(normSlope) * (180.0 / M_PI);
}

int AngleRegime(double aShort, double aLong, double trendT, double sideT) {
   if(aShort >= trendT && aLong >= trendT) return 1;    // Up
   if(aShort <= -trendT && aLong <= -trendT) return -1; // Down
   if(MathAbs(aShort) < sideT && MathAbs(aLong) < sideT) return 0; // Sideways
   return 2; // Neutral
}

//+------------------------------------------------------------------+
//| Moving Averages for Hull Filter                                  |
//+------------------------------------------------------------------+
double GetWMA(const double &p[], int len, int idx) {
   if(idx < len - 1 || idx >= ArraySize(p)) return EMPTY_VALUE;
   double s = 0.0, ws = 0.0;
   for(int i = 0; i < len; i++) {
      double w = (double)(len - i);
      s  += p[idx - i] * w;
      ws += w;
   }
   return ws != 0.0 ? s / ws : EMPTY_VALUE;
}

double GetHMA(const double &p[], int len, int idx) {
   if(idx < len || idx >= ArraySize(p)) return EMPTY_VALUE;
   int halfLen = len / 2;
   int sqrtLen = (int)MathRound(MathSqrt((double)len));
   if(sqrtLen <= 0) sqrtLen = 1;

   double tb[];
   ArrayResize(tb, sqrtLen + 1);
   for(int i = 0; i < sqrtLen + 1; i++) {
      int pos = idx - (sqrtLen - i);
      double w1 = GetWMA(p, halfLen, pos);
      double w2 = GetWMA(p, len, pos);
      tb[i] = (w1 != EMPTY_VALUE && w2 != EMPTY_VALUE) ? (2.0 * w1 - w2) : EMPTY_VALUE;
   }
   return GetWMA(tb, sqrtLen, sqrtLen - 1);
}

double MomLayerAt(const double &momBuffer[], int len, int idx) {
   double h = GetHMA(momBuffer, len, idx);
   double v = (h == EMPTY_VALUE ? 0.0 : (h - 50.0)) * 2.0 * InpRibAmp;
   return Clampd(v, -100.0, 100.0);
}

double OscLayerAt(const double &vwmaBuffer[], int len, int idx) {
   if(InpOscType == OSC_HULL_VWMA) {
      double h1 = GetHMA(vwmaBuffer, len, idx);
      double h2 = GetHMA(vwmaBuffer, len, idx - 2);
      double slope = (h1 != EMPTY_VALUE && h2 != EMPTY_VALUE && h2 != 0.0) ? ((h1 - h2) / h2 * 100.0 * InpOscScale) : 0.0;
      return Clampd(slope, -100.0, 100.0);
   }
   double h = GetHMA(vwmaBuffer, len, idx);
   return (h == EMPTY_VALUE) ? 0.0 : Clampd(h, -100.0, 100.0);
}

//+------------------------------------------------------------------+
//| 3D Perspective Projection Engine (Faithful TradingView Geometry) |
//+------------------------------------------------------------------+
class CCamera3D {
private:
   double m_cYaw, m_sYaw;
   double m_cPit, m_sPit;
   double m_sT, m_sA, m_sD;
   int    m_centerX, m_centerY;
   double m_pixelScale;

public:
   void Init(double yawDeg, double pitchDeg, double sT, double sA, double sD,
             int w, int h, double boxScale, int offX, int offY, ENUM_DASH_POS dashPos) {
      double yR = yawDeg   * M_PI / 180.0;
      double pR = pitchDeg * M_PI / 180.0;
      m_cYaw = MathCos(yR); m_sYaw = MathSin(yR);
      m_cPit = MathCos(pR); m_sPit = MathSin(pR);
      m_sT = sT; m_sA = sA; m_sD = sD;

      // In TradingView, pane anchors are scaled to maxProjY ≈ 50.0.
      // We scale the 3D cube vertically to fill ~85% of subwindow height:
      double targetHeight = (double)h * 0.82 * boxScale;
      m_pixelScale = targetHeight / 100.0;

      int dashWidth = 450;
      if(dashPos == DASH_TOP_RIGHT || dashPos == DASH_MID_RIGHT || dashPos == DASH_BOT_RIGHT) {
         m_centerX = (int)((w - dashWidth) * 0.52) + offX;
      } else {
         m_centerX = (int)(dashWidth + (w - dashWidth) * 0.48) + offX;
      }
      m_centerY = (h / 2) + offY;
   }

   void Project(double x, double y, double z, int &sx, int &sy) {
      double xs = x * m_sT;
      double ys = y * m_sA;
      double zs = z * m_sD;

      double screenX = xs * m_cYaw - zs * m_sYaw;
      double screenY = ys * m_cPit + (xs * m_sYaw + zs * m_cYaw) * m_sPit;

      sx = (int)MathRound(m_centerX + screenX * (m_pixelScale * 1.55));
      sy = (int)MathRound(m_centerY - screenY * m_pixelScale);
   }
};
CCamera3D g_cam;

//+------------------------------------------------------------------+
//| Subtle Dotted Line Rendering (step=4 for fine dotted grid)       |
//+------------------------------------------------------------------+
void DrawDottedLine(int x1, int y1, int x2, int y2, uint c, int step = 4) {
   int dx = x2 - x1, dy = y2 - y1;
   double len = MathSqrt((double)(dx * dx + dy * dy));
   if(len < 3.0) {
      g_canvas.Line(x1, y1, x2, y2, c);
      return;
   }
   int numSegments = (int)(len / (double)step);
   if(numSegments <= 0) numSegments = 1;
   for(int i = 0; i < numSegments; i += 2) {
      int xa = (int)MathRound(x1 + dx * ((double)i / numSegments));
      int ya = (int)MathRound(y1 + dy * ((double)i / numSegments));
      int xb = (int)MathRound(x1 + dx * ((double)(i + 1) / numSegments));
      int yb = (int)MathRound(y1 + dy * ((double)(i + 1) / numSegments));
      g_canvas.Line(xa, ya, xb, yb, c);
   }
}

//+------------------------------------------------------------------+
//| Incremental Markov State Processing                              |
//+------------------------------------------------------------------+
void ProcessBarMarkov(const double &high[], const double &low[], const double &close[], int i) {
   if(i < InpAngleLongLen + 1) return;

   double aShortC = AngleFn(close, i, InpAngleShortLen), aLongC = AngleFn(close, i, InpAngleLongLen);
   double aShortH = AngleFn(high,  i, InpAngleShortLen), aLongH = AngleFn(high,  i, InpAngleLongLen);
   double aShortL = AngleFn(low,   i, InpAngleShortLen), aLongL = AngleFn(low,   i, InpAngleLongLen);

   int rC = AngleRegime(aShortC, aLongC, InpAngleTrendThresh, InpAngleSideThresh);
   int rH = AngleRegime(aShortH, aLongH, InpAngleTrendThresh, InpAngleSideThresh);
   int rL = AngleRegime(aShortL, aLongL, InpAngleTrendThresh, InpAngleSideThresh);

   int upV = (rC == 1  ? 1 : 0) + (rH == 1  ? 1 : 0) + (rL == 1  ? 1 : 0);
   int dnV = (rC == -1 ? 1 : 0) + (rH == -1 ? 1 : 0) + (rL == -1 ? 1 : 0);
   int sdV = (rC == 0  ? 1 : 0) + (rH == 0  ? 1 : 0) + (rL == 0  ? 1 : 0);
   int ntV = (rC == 2  ? 1 : 0) + (rH == 2  ? 1 : 0) + (rL == 2  ? 1 : 0);

   bool consensusUp   = (upV > dnV && upV > sdV && upV > ntV);
   bool consensusDown = (dnV > upV && dnV > sdV && dnV > ntV);
   bool isChopAngle   = (!consensusUp && !consensusDown);

   double ci = ChoppinessIndexAt(high, low, close, InpChopLen, i);
   bool isChopCI = (ci >= InpChopThresh);
   bool isChoppy = InpUseChopFilter && (isChopCI || isChopAngle);

   int mkState = consensusUp ? 2 : (consensusDown ? 0 : 1);
   McStateDur = (McHavePrev1 && mkState == McPrevState1) ? (McStateDur + 1) : 1;

   if(InpUseMarkov) {
      for(int r = 0; r < 9; r++) {
         for(int c = 0; c < 3; c++) {
            McShort[r][c] *= InpMarkovDecay;
            McLong[r][c]  *= InpMarkovDecay;
         }
      }
   }

   if(InpUseMarkov && McHavePrev1 && McHavePrev2) {
      int ctxU = McPrevState2 * 3 + McPrevState1;
      if(ctxU >= 0 && ctxU < 9) {
         if(McPrevDur1 < McDurSplit) McShort[ctxU][mkState] += 1.0;
         else                        McLong[ctxU][mkState]  += 1.0;
      }
   }

   int ctxP = McHavePrev1 ? (McPrevState1 * 3 + mkState) : -1;
   PBull = 0.333; PSide = 0.333; PBear = 0.333;
   McSampleN = 0.0; McConfident = false;

   if(InpUseMarkov && ctxP >= 0 && ctxP < 9) {
      bool useLong = (McStateDur >= McDurSplit);
      double cB = useLong ? McLong[ctxP][0] : McShort[ctxP][0];
      double cS = useLong ? McLong[ctxP][1] : McShort[ctxP][1];
      double cU = useLong ? McLong[ctxP][2] : McShort[ctxP][2];
      McSampleN = cB + cS + cU;
      double den = McSampleN + 3.0 * McAlpha;
      PBear = (cB + McAlpha) / den;
      PSide = (cS + McAlpha) / den;
      PBull = (cU + McAlpha) / den;
      McConfident = (McSampleN >= McMinSample);
   }
   McVote = (PBull >= PSide && PBull >= PBear) ? 2 : (PBear >= PSide ? 0 : 1);

   CurStateEmoji = isChopCI ? (EmojiShark() + " Chop-zone") : (consensusUp ? (EmojiWhaleBull() + " Bull") : (consensusDown ? (EmojiWhaleBear() + " Bear") : (EmojiCrab() + " Sideways")));
   PStayCurrent = (mkState == 2) ? PBull : (mkState == 0 ? PBear : PSide);
   ExpChangeBars = (PStayCurrent >= 0.999) ? 99 : (PStayCurrent <= 0.0 ? 1 : (int)MathMin(99.0, MathRound(1.0 / (1.0 - PStayCurrent))));
   double probRange = MathMax(PBull, MathMax(PSide, PBear)) - MathMin(PBull, MathMin(PSide, PBear));
   StateBalanced = (probRange < 0.15);

   NextLikelyState = 1;
   double pNextLikely = -1.0;
   if(mkState != 2 && PBull > pNextLikely) { NextLikelyState = 2; pNextLikely = PBull; }
   if(mkState != 0 && PBear > pNextLikely) { NextLikelyState = 0; pNextLikely = PBear; }
   if(mkState != 1 && PSide > pNextLikely) { NextLikelyState = 1; pNextLikely = PSide; }
   NextLikelyEmoji = (NextLikelyState == 2) ? (EmojiWhaleBull() + " Bull") : (NextLikelyState == 0 ? (EmojiWhaleBear() + " Bear") : (EmojiCrab() + " Sideways"));

   bool sharkEntry = isChopCI && !ChopCIPrev;
   bool crabEntry  = isChopAngle && !ChopAnglePrev;
   if(isChopCI)    { DashSharkBars++; if(sharkEntry) DashSharkEvents++; }
   if(isChopAngle) { DashCrabBars++;  if(crabEntry)  DashCrabEvents++; }

   ChopCIPrev = isChopCI;
   ChopAnglePrev = isChopAngle;
   IsChopCI_Last = isChopCI;
   IsChopAngle_Last = isChopAngle;
   IsChoppy_Last = isChoppy;

   McPrevState2 = McPrevState1;
   McPrevState1 = mkState;
   McPrevDur1   = McStateDur;
   McHavePrev2  = McHavePrev1;
   McHavePrev1  = true;
   McState      = mkState;
}

//+------------------------------------------------------------------+
//| Incremental Win-Rate Signal Evaluation                           |
//+------------------------------------------------------------------+
void ProcessBarWinRate(const double &high[], const double &low[], const double &close[],
                       const double &momBuffer[], const double &vwmaBuffer[], int i) {
   int sL = MathMin(InpSlopeLookback, InpTimeSpan);
   if(i - sL < 0) return;

   double rsiMid  = MomLayerAt(momBuffer, 20, i);
   double rsiPast = MomLayerAt(momBuffer, 20, i - sL);
   double rsiSlope = (rsiMid - rsiPast) / sL;

   double oscMid  = OscLayerAt(vwmaBuffer, 19, i);
   double oscPast = OscLayerAt(vwmaBuffer, 19, i - sL);
   double oscSlope = (oscMid - oscPast) / sL;

   double projFactor = MathPow(InpForecastDecay, InpTimeSpan);
   double rsiEnd = Clampd(rsiMid + rsiSlope * InpTimeSpan * projFactor, -100.0, 100.0);
   double oscEnd = Clampd(oscMid + oscSlope * InpTimeSpan * projFactor, -100.0, 100.0);

   bool bullConf = (rsiEnd > 0.0 && oscEnd > 0.0 && !IsChoppy_Last);
   bool bearConf = (rsiEnd < 0.0 && oscEnd < 0.0 && !IsChoppy_Last);
   bool bullEntry = (bullConf && !DashBullConfPrev);
   bool bearEntry = (bearConf && !DashBearConfPrev);
   DashBullConfPrev = bullConf;
   DashBearConfPrev = bearConf;

   // Update running extrema
   if(PrevSigDir != 0) {
      RunMaxSince = MathMax(RunMaxSince, high[i]);
      RunMinSince = MathMin(RunMinSince, low[i]);
   }

   // New signal triggered -> resolve previous signal
   if(bullEntry || bearEntry) {
      if(PrevSigDir != 0) {
         bool dirWon = (PrevSigDir == 1) ? (close[i] > PrevSigEntry) : (close[i] < PrevSigEntry);
         double mfe  = (PrevSigDir == 1) ? (RunMaxSince - PrevSigEntry) : (PrevSigEntry - RunMinSince);
         bool mfeWon = (PrevSigATR > 0.0 && mfe >= InpATRMult * PrevSigATR);
         bool won    = (dirWon || mfeWon);
         if(PrevSigDir == 1) {
            DashBullSignals++;
            if(won) DashBullWins++;
         } else {
            DashBearSignals++;
            if(won) DashBearWins++;
         }
      }
      PrevSigDir   = bullEntry ? 1 : -1;
      PrevSigEntry = close[i];
      PrevSigATR   = CalcATR(high, low, close, InpATRLen, i);
      RunMaxSince  = high[i];
      RunMinSince  = low[i];

      // Store in indicator buffer
      if(bullEntry) BufferBullSignal[i] = 1.0;
      if(bearEntry) BufferBearSignal[i] = -1.0;
   }
}

//+------------------------------------------------------------------+
//| Front-End 3D Canvas Rendering (Back-to-Front Painter's Order)    |
//+------------------------------------------------------------------+
void Render() {
   if(!g_isReady || g_w <= 10 || g_h <= 10) return;

   color bgChart = (color)ChartGetInteger(0, CHART_COLOR_BACKGROUND);
   color fgChart = (color)ChartGetInteger(0, CHART_COLOR_FOREGROUND);
   uint bgCol = ColorToARGB(bgChart, 255);
   uint fgCol = ColorToARGB(fgChart, 255);
   uint fogC  = ColorToARGB(InpFogColor, 255);

   g_canvas.Erase(bgCol);
   g_cam.Init(InpYawDeg, InpPitchDeg, InpScaleTime, InpScaleAlt, InpScaleDepth,
              g_w, g_h, InpBoxScale, InpOffsetX, InpOffsetY, InpDashPos);

   double T = (double)InpTimeSpan;
   int x1, y1, x2, y2, x3, y3, x4, y4;
   uint bc  = ColorToARGB(InpBoxColor, 255);
   uint gc  = ColorToARGB(InpGridColor, 255);
   uint np  = ColorToARGB(InpNowPlaneCol, 255);
   uint yHz = ColorToARGB(InpYZeroEdge, 255);

   // 1) BACK WALL & SIDE GRIDS (farthest elements)
   if(InpShowBackGrid) {
      for(int gy = -100; gy <= 100; gy += InpGridStep) {
         g_cam.Project(-T, gy, 100, x1, y1); g_cam.Project(T, gy, 100, x2, y2);
         g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, gc, 60, 4);
      }
      for(int gx = -(int)T; gx <= (int)T; gx += InpGridStep) {
         g_cam.Project(gx, -100, 100, x1, y1); g_cam.Project(gx, 100, 100, x2, y2);
         g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, gc, 60, 4);
      }
   }
   if(InpShowSideGrid) {
      for(int gy = -100; gy <= 100; gy += InpGridStep) {
         g_cam.Project(-T, gy, -100, x1, y1); g_cam.Project(-T, gy, 100, x2, y2);
         g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, gc, 60, 4);
      }
      for(int gz = -100; gz <= 100; gz += InpGridStep) {
         g_cam.Project(-T, -100, gz, x1, y1); g_cam.Project(-T, 100, gz, x2, y2);
         g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, gc, 60, 4);
      }
   }

   // 2) FLOOR GRID (dotted, Y=0 plane)
   if(InpShowFloorGrid) {
      for(int gz = -100; gz <= 100; gz += InpGridStep) {
         g_cam.Project(-T, 0, gz, x1, y1); g_cam.Project(T, 0, gz, x2, y2);
         g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, gc, 50, 4);
      }
      for(int gx = -(int)T; gx <= (int)T; gx += InpGridStep) {
         g_cam.Project(gx, 0, -100, x1, y1); g_cam.Project(gx, 0, 100, x2, y2);
         g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, gc, 50, 4);
      }
   }

   // 3) BOX BACK WIREFRAME EDGES (Subtle dark dotted lines)
   g_cam.Project(-T, -100, 100, x1, y1);  g_cam.Project(T, -100, 100, x2, y2);  g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, bc, 70, 4);
   g_cam.Project(T, -100, 100, x1, y1);   g_cam.Project(T, 100, 100, x2, y2);   g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, bc, 70, 4);
   g_cam.Project(T, 100, 100, x1, y1);    g_cam.Project(-T, 100, 100, x2, y2);  g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, bc, 70, 4);
   g_cam.Project(-T, 100, 100, x1, y1);   g_cam.Project(-T, -100, 100, x2, y2); g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, bc, 70, 4);

   // 4) X=0 PLANE (past/future divider, subtle dotted)
   g_cam.Project(0, -100, -100, x1, y1); g_cam.Project(0, 100, -100, x2, y2); g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, np, 80, 4);
   g_cam.Project(0, 100, -100, x1, y1);  g_cam.Project(0, 100, 100, x2, y2);  g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, np, 80, 4);
   g_cam.Project(0, 100, 100, x1, y1);   g_cam.Project(0, -100, 100, x2, y2); g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, np, 80, 4);
   g_cam.Project(0, -100, 100, x1, y1);  g_cam.Project(0, -100, -100, x2, y2);g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, np, 80, 4);

   // 5) MOMENTUM TERRAIN (Rendered BACK-TO-FRONT from d = 18 down to 0)
   double zStepR = 200.0 / (MOM_LAYERS - 1);
   for(int d = MOM_LAYERS - 2; d >= 0; d--) {
      double zd1 = -100.0 + d * zStepR;
      double zd2 = -100.0 + (d + 1) * zStepR;
      double midZ = (zd1 + zd2) / 2.0;

      int midT = InpTimeSpan / 2;
      int midIdx = (g_ringHead - (InpTimeSpan - 1 - midT) + MAX_RING_BUF * 10) % MAX_RING_BUF;
      float avgR = (g_ringBuf[midIdx].momentum[d] + g_ringBuf[midIdx].momentum[d + 1]) / 2.0f;

      uint rCol = GetMomentumColor(avgR);
      uint rColFog = DepthTint(rCol, midZ, fogC, InpFogStrength);

      for(int t = 0; t < InpTimeSpan - 1; t++) {
         int i1 = (g_ringHead - (InpTimeSpan - 1 - t) + MAX_RING_BUF * 10) % MAX_RING_BUF;
         int i2 = (g_ringHead - (InpTimeSpan - 1 - (t + 1)) + MAX_RING_BUF * 10) % MAX_RING_BUF;

         g_cam.Project(t - InpTimeSpan,       g_ringBuf[i1].momentum[d],     zd1, x1, y1);
         g_cam.Project((t + 1) - InpTimeSpan, g_ringBuf[i2].momentum[d],     zd1, x2, y2);
         g_cam.Project((t + 1) - InpTimeSpan, g_ringBuf[i2].momentum[d + 1], zd2, x3, y3);
         g_cam.Project(t - InpTimeSpan,       g_ringBuf[i1].momentum[d + 1], zd2, x4, y4);

         g_canvas.FillQuadAlpha(x1, y1, x2, y2, x3, y3, x4, y4, rColFog, 65);
         g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, rColFog, 140, 4);
         g_canvas.DrawDottedLineAlpha(x1, y1, x4, y4, rColFog, 110, 4);
      }
   }

   // 6) OSCILLATOR CARPET (Rendered BACK-TO-FRONT from d = 10 down to 0)
   if(InpShowOsc) {
      double zStepV = 200.0 / (OSC_LAYERS - 1);
      uint oBullCol = ColorToARGB(InpOscBullCol, 255);
      uint oBearCol = ColorToARGB(InpOscBearCol, 255);
      uint oNeutCol = ColorToARGB(InpOscNeutCol, 255);

      for(int d = OSC_LAYERS - 2; d >= 0; d--) {
         double zd1 = -100.0 + d * zStepV;
         double zd2 = -100.0 + (d + 1) * zStepV;
         double midZ = (zd1 + zd2) / 2.0;

         int midT = InpTimeSpan / 2;
         int midIdx = (g_ringHead - (InpTimeSpan - 1 - midT) + MAX_RING_BUF * 10) % MAX_RING_BUF;
         float midAmpD  = g_ringBuf[midIdx].oscillator[d];
         float midAmpD1 = g_ringBuf[midIdx].oscillator[d + 1];
         float avgAmp   = (midAmpD + midAmpD1) / 2.0f;
         float magAmp   = (MathAbs(midAmpD) + MathAbs(midAmpD1)) / 2.0f;

         uint baseCol = GetOscColor(avgAmp, magAmp, oBullCol, oBearCol, oNeutCol);
         uint baseFog = DepthTint(baseCol, midZ, fogC, InpFogStrength);

         for(int t = 0; t < InpTimeSpan - 1; t++) {
            int i1 = (g_ringHead - (InpTimeSpan - 1 - t) + MAX_RING_BUF * 10) % MAX_RING_BUF;
            int i2 = (g_ringHead - (InpTimeSpan - 1 - (t + 1)) + MAX_RING_BUF * 10) % MAX_RING_BUF;

            g_cam.Project(t - InpTimeSpan,       g_ringBuf[i1].oscillator[d],     zd1, x1, y1);
            g_cam.Project((t + 1) - InpTimeSpan, g_ringBuf[i2].oscillator[d],     zd1, x2, y2);
            g_cam.Project((t + 1) - InpTimeSpan, g_ringBuf[i2].oscillator[d + 1], zd2, x3, y3);
            g_cam.Project(t - InpTimeSpan,       g_ringBuf[i1].oscillator[d + 1], zd2, x4, y4);

            g_canvas.FillQuadAlpha(x1, y1, x2, y2, x3, y3, x4, y4, baseFog, 55);
            g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, baseFog, 120, 4);
         }
      }
   }

   // 7) FORECAST GHOST TERRAINS (Rendered BACK-TO-FRONT)
   if(InpShowForecast) {
      int sL = MathMin(InpSlopeLookback, InpTimeSpan);
      int pI = (g_ringHead - sL + MAX_RING_BUF * 10) % MAX_RING_BUF;

      // Momentum forecast (back-to-front)
      for(int d = MOM_LAYERS - 2; d >= 0; d--) {
         float curr0 = g_ringBuf[g_ringHead].momentum[d],     past0 = g_ringBuf[pI].momentum[d];
         float curr1 = g_ringBuf[g_ringHead].momentum[d + 1], past1 = g_ringBuf[pI].momentum[d + 1];
         float sl0   = (curr0 - past0) / (float)sL;
         float sl1   = (curr1 - past1) / (float)sL;

         double zd1  = -100.0 + d * zStepR;
         double zd2  = -100.0 + (d + 1) * zStepR;
         double midZ = (zd1 + zd2) / 2.0;

         float avgR = (curr0 + curr1) / 2.0f;
         uint rCol = GetMomentumColor(avgR);
         uint rColFog = DepthTint(rCol, midZ, fogC, InpFogStrength);

         for(int ft = 0; ft < InpTimeSpan - 1; ft++) {
            float vA0 = MathMax((float)-100.0, MathMin((float)100.0, curr0 + sl0 * ft * (float)MathPow(InpForecastDecay, ft)));
            float vB0 = MathMax((float)-100.0, MathMin((float)100.0, curr0 + sl0 * (ft + 1) * (float)MathPow(InpForecastDecay, ft + 1)));
            float vA1 = MathMax((float)-100.0, MathMin((float)100.0, curr1 + sl1 * ft * (float)MathPow(InpForecastDecay, ft)));
            float vB1 = MathMax((float)-100.0, MathMin((float)100.0, curr1 + sl1 * (ft + 1) * (float)MathPow(InpForecastDecay, ft + 1)));

            g_cam.Project(ft,     vA0, zd1, x1, y1);
            g_cam.Project(ft + 1, vB0, zd1, x2, y2);
            g_cam.Project(ft + 1, vB1, zd2, x3, y3);
            g_cam.Project(ft,     vA1, zd2, x4, y4);

            g_canvas.FillQuadAlpha(x1, y1, x2, y2, x3, y3, x4, y4, rColFog, 35);
            g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, rColFog, 95, 4);
         }
      }

      // Oscillator forecast (back-to-front)
      if(InpShowOsc) {
         double zStepV = 200.0 / (OSC_LAYERS - 1);
         uint oBullCol = ColorToARGB(InpOscBullCol, 255);
         uint oBearCol = ColorToARGB(InpOscBearCol, 255);
         uint oNeutCol = ColorToARGB(InpOscNeutCol, 255);

         for(int d = OSC_LAYERS - 2; d >= 0; d--) {
            float c0  = g_ringBuf[g_ringHead].oscillator[d],     p0 = g_ringBuf[pI].oscillator[d];
            float c1  = g_ringBuf[g_ringHead].oscillator[d + 1], p1 = g_ringBuf[pI].oscillator[d + 1];
            float sl0 = (c0 - p0) / (float)sL;
            float sl1 = (c1 - p1) / (float)sL;

            double zd1  = -100.0 + d * zStepV;
            double zd2  = -100.0 + (d + 1) * zStepV;
            double midZ = (zd1 + zd2) / 2.0;

            float avgAmp = (c0 + c1) / 2.0f;
            float magAmp = (MathAbs(c0) + MathAbs(c1)) / 2.0f;
            uint baseCol = GetOscColor(avgAmp, magAmp, oBullCol, oBearCol, oNeutCol);
            uint baseFog = DepthTint(baseCol, midZ, fogC, InpFogStrength);

            for(int ft = 0; ft < InpTimeSpan - 1; ft++) {
               float vA0 = MathMax((float)-100.0, MathMin((float)100.0, c0 + sl0 * ft * (float)MathPow(InpForecastDecay, ft)));
               float vB0 = MathMax((float)-100.0, MathMin((float)100.0, c0 + sl0 * (ft + 1) * (float)MathPow(InpForecastDecay, ft + 1)));
               float vA1 = MathMax((float)-100.0, MathMin((float)100.0, c1 + sl1 * ft * (float)MathPow(InpForecastDecay, ft)));
               float vB1 = MathMax((float)-100.0, MathMin((float)100.0, c1 + sl1 * (ft + 1) * (float)MathPow(InpForecastDecay, ft + 1)));

               g_cam.Project(ft,     vA0, zd1, x1, y1);
               g_cam.Project(ft + 1, vB0, zd1, x2, y2);
               g_cam.Project(ft + 1, vB1, zd2, x3, y3);
               g_cam.Project(ft,     vA1, zd2, x4, y4);

               g_canvas.FillQuadAlpha(x1, y1, x2, y2, x3, y3, x4, y4, baseFog, 28);
               g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, baseFog, 85, 4);
            }
         }
      }
   }

   // 8) Y=0 HORIZON OUTLINE (Crisp cyan tide line border #64B5F6)
   g_cam.Project(-T, 0, -100, x1, y1); g_cam.Project(T, 0, -100, x2, y2);
   g_canvas.DrawLineAlpha(x1, y1, x2, y2, yHz, 240);
   g_cam.Project(T, 0, -100, x1, y1);  g_cam.Project(T, 0, 100, x2, y2);
   g_canvas.DrawLineAlpha(x1, y1, x2, y2, yHz, 240);
   g_cam.Project(T, 0, 100, x1, y1);   g_cam.Project(-T, 0, 100, x2, y2);
   g_canvas.DrawLineAlpha(x1, y1, x2, y2, yHz, 240);
   g_cam.Project(-T, 0, 100, x1, y1);  g_cam.Project(-T, 0, -100, x2, y2);
   g_canvas.DrawLineAlpha(x1, y1, x2, y2, yHz, 240);

   // 9) BOX FRONT WIREFRAME EDGES (Subtle dark dotted lines)
   g_cam.Project(-T, -100, -100, x1, y1); g_cam.Project(T, -100, -100, x2, y2); g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, bc, 70, 4);
   g_cam.Project(T, -100, -100, x1, y1);  g_cam.Project(T, 100, -100, x2, y2);  g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, bc, 70, 4);
   g_cam.Project(T, 100, -100, x1, y1);   g_cam.Project(-T, 100, -100, x2, y2); g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, bc, 70, 4);
   g_cam.Project(-T, 100, -100, x1, y1);  g_cam.Project(-T, -100, -100, x2, y2);g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, bc, 70, 4);

   g_cam.Project(-T, -100, -100, x1, y1); g_cam.Project(-T, -100, 100, x2, y2); g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, bc, 70, 4);
   g_cam.Project(T, -100, -100, x1, y1);  g_cam.Project(T, -100, 100, x2, y2);  g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, bc, 70, 4);
   g_cam.Project(T, 100, -100, x1, y1);   g_cam.Project(T, 100, 100, x2, y2);   g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, bc, 70, 4);
   g_cam.Project(-T, 100, -100, x1, y1);  g_cam.Project(-T, 100, 100, x2, y2);  g_canvas.DrawDottedLineAlpha(x1, y1, x2, y2, bc, 70, 4);

   // 10) 3D FLOATING EMOJIS (Faithful TradingView 3D Object Rendering)
   if(InpShowEmoji) {
      int sL = MathMin(InpSlopeLookback, InpTimeSpan);
      int pI = (g_ringHead - sL + MAX_RING_BUF * 10) % MAX_RING_BUF;
      float rM = g_ringBuf[g_ringHead].momentum[9],   rP = g_ringBuf[pI].momentum[9],   rS = (rM - rP) / (float)sL;
      float oM = g_ringBuf[g_ringHead].oscillator[5], oP = g_ringBuf[pI].oscillator[5], oS = (oM - oP) / (float)sL;

      int ex, ey;

      // Surfer at Momentum leading edge (X=0)
      g_cam.Project(0, rM, 0, ex, ey);
      if(ex > 0 && ex < g_w && ey > 0 && ey < g_h) {
         uint surfCol = (rM >= 0.0) ? 0xFF00E676 : 0xFFFF5252;
         g_canvas.FontSet("Segoe UI Emoji", 22, FW_BOLD);
         g_canvas.TextOut(ex, ey, EmojiSurfer(), surfCol, TA_CENTER | TA_VCENTER);
      }

      // Sailboat at Oscillator leading edge (X=0)
      g_cam.Project(0, oM, 0, ex, ey);
      if(ex > 0 && ex < g_w && ey > 0 && ey < g_h) {
         uint oscC = (oM >= 0.0) ? 0xFF00E5FF : 0xFFFF5252;
         g_canvas.FontSet("Segoe UI Emoji", 22, FW_BOLD);
         g_canvas.TextOut(ex, ey, EmojiSailboat(), oscC, TA_CENTER | TA_VCENTER);
      }

      bool markovBullOK = !InpUseMarkov || !McConfident || McVote != 0;
      bool markovBearOK = !InpUseMarkov || !McConfident || McVote != 2;
      bool hatchBull    = InpUseMarkov && McConfident && McState == 1 && PBull >= InpPreWhaleThresh / 100.0;
      bool hatchBear    = InpUseMarkov && McConfident && McState == 1 && PBear >= InpPreWhaleThresh / 100.0;

      uint bullCol3D = 0xFF00E5FF; // High-contrast Cyan #00E5FF
      uint bearCol3D = 0xFFFF5252; // High-contrast Coral/Red #FF5252
      uint chopCol3D = 0xFFFFA726; // High-contrast Amber/Orange #FFA726
      uint sideCol3D = 0xFFFF4081; // Bright Pink/Magenta Crab #FF4081 (TradingView style)

      if(IsChopCI_Last && InpUseChopFilter) {
         g_cam.Project(T, 0, 0, ex, ey);
         g_canvas.FontSet("Segoe UI Emoji", 26, FW_BOLD);
         if(ex > 0 && ex < g_w) g_canvas.TextOut(ex, ey, EmojiShark(), chopCol3D, TA_CENTER | TA_VCENTER);
      }
      else if(IsChopAngle_Last && InpUseChopFilter && !(hatchBull || hatchBear)) {
         g_cam.Project(T, 0, 0, ex, ey);
         g_canvas.FontSet("Segoe UI Emoji", 26, FW_BOLD);
         if(ex > 0 && ex < g_w) g_canvas.TextOut(ex, ey, EmojiCrab(), sideCol3D, TA_CENTER | TA_VCENTER);
      }
      else {
         double projFactorE = MathPow(InpForecastDecay, InpTimeSpan);
         double rsiEndY = Clampd(rM + rS * InpTimeSpan * projFactorE, -100.0, 100.0);
         double oscEndY = Clampd(oM + oS * InpTimeSpan * projFactorE, -100.0, 100.0);

         bool bullProjected = (rsiEndY > 0.0 && oscEndY > 0.0 && markovBullOK);
         bool bearProjected = (rsiEndY < 0.0 && oscEndY < 0.0 && markovBearOK);

         if(bullProjected) {
            g_cam.Project(T, rsiEndY, 0, ex, ey);
            g_canvas.FontSet("Segoe UI Emoji", 28, FW_BOLD);
            if(ex > 0 && ex < g_w) g_canvas.TextOut(ex, ey, EmojiWhaleBull(), bullCol3D, TA_CENTER | TA_VCENTER);
         }
         else if(bearProjected) {
            g_cam.Project(T, rsiEndY, 0, ex, ey);
            g_canvas.FontSet("Segoe UI Emoji", 28, FW_BOLD);
            if(ex > 0 && ex < g_w) g_canvas.TextOut(ex, ey, EmojiWhaleBear(), bearCol3D, TA_CENTER | TA_VCENTER);
         }
         else if(hatchBull) {
            g_cam.Project(T / 2.0, 0, 0, ex, ey);
            g_canvas.FontSet("Segoe UI Emoji", 18, FW_BOLD);
            if(ex > 0 && ex < g_w) g_canvas.TextOut(ex, ey, EmojiWhaleBull(), bullCol3D, TA_CENTER | TA_VCENTER);
         }
         else if(hatchBear) {
            g_cam.Project(T / 2.0, 0, 0, ex, ey);
            g_canvas.FontSet("Segoe UI Emoji", 18, FW_BOLD);
            if(ex > 0 && ex < g_w) g_canvas.TextOut(ex, ey, EmojiWhaleBear(), bearCol3D, TA_CENTER | TA_VCENTER);
         }
      }
   }

   // 11) WIN-RATE & MARKOV DASHBOARD TABLE
   if(InpShowDash) {
      int baseFontSize   = (InpDashSize == SIZE_TINY) ? 9 : (InpDashSize == SIZE_SMALL ? 11 : (InpDashSize == SIZE_NORMAL ? 13 : (InpDashSize == SIZE_LARGE ? 15 : 18)));
      int headerFontSize = baseFontSize + 2;
      int footerFontSize = (InpDashSize == SIZE_TINY) ? 8 : (InpDashSize == SIZE_SMALL ? 9 : (InpDashSize == SIZE_NORMAL ? 10 : (InpDashSize == SIZE_LARGE ? 12 : 14)));

      int colW = (InpDashSize == SIZE_TINY) ? 90 : (InpDashSize == SIZE_SMALL ? 100 : (InpDashSize == SIZE_NORMAL ? 112 : (InpDashSize == SIZE_LARGE ? 126 : 142)));
      int w    = colW * 4;
      int rowH = baseFontSize + 14;
      int hdrH = headerFontSize + 16;
      int extraRows = (InpShowMarkovRow ? 2 : 0) + 2 /* current state */ + 2 /* footer */;
      int h    = hdrH + 4 * rowH + extraRows * (rowH - 4);
      int tx, ty;

      switch(InpDashPos) {
         case DASH_TOP_RIGHT:  tx = g_w - 20 - w; ty = 20; break;
         case DASH_TOP_LEFT:   tx = 20; ty = 20; break;
         case DASH_MID_RIGHT:  tx = g_w - 20 - w; ty = (g_h - h) / 2; break;
         case DASH_MID_LEFT:   tx = 20; ty = (g_h - h) / 2; break;
         case DASH_BOT_LEFT:   tx = 20; ty = g_h - 20 - h; break;
         default:              tx = g_w - 20 - w; ty = g_h - 20 - h; break;
      }

      int cx0 = tx + colW / 2;
      int cx1 = tx + colW + colW / 2;
      int cx2 = tx + 2 * colW + colW / 2;
      int cx3 = tx + 3 * colW + colW / 2;

      uint bgT       = ColorToARGB(InpDashBgCol, 235);
      uint lineC     = ColorToARGB(C'50,50,65', 255);
      uint txtC      = ColorToARGB(InpDashTextCol, 255);
      uint bullCol   = ColorToARGB(InpOscBullCol, 255);
      uint bearCol   = ColorToARGB(InpOscBearCol, 255);
      uint orangeCol = ColorToARGB(clrOrange, 255);
      uint yellowCol = 0xFFFFEE58; // High-contrast Yellow

      // Card panel with clean glass shading
      for(int r = ty; r <= ty + h; r++)
         g_canvas.BlendHLine(tx, tx + w, r, bgT, 235);
      g_canvas.Rectangle(tx, ty, tx + w, ty + h, lineC);

      // Header Row 0
      int curY = ty;
      g_canvas.FontSet("Trebuchet MS", headerFontSize, FW_BOLD);
      g_canvas.TextOut(tx + w / 2, curY + hdrH / 2, "Liquidity Surge + Markov · Win Rate", txtC, TA_CENTER | TA_VCENTER);
      curY += hdrH;
      g_canvas.Line(tx, curY, tx + w, curY, lineC);

      // Row 1 - Category Header Labels (Clean transparent cells without background fills)
      g_canvas.FontSet("Segoe UI Emoji", baseFontSize, FW_BOLD);
      g_canvas.TextOut(cx0, curY + rowH / 2, EmojiWhaleBull() + " Bull", bullCol, TA_CENTER | TA_VCENTER);
      g_canvas.TextOut(cx1, curY + rowH / 2, EmojiWhaleBear() + " Bear", bearCol, TA_CENTER | TA_VCENTER);
      g_canvas.TextOut(cx2, curY + rowH / 2, EmojiShark() + " Chop zone", orangeCol, TA_CENTER | TA_VCENTER);
      g_canvas.TextOut(cx3, curY + rowH / 2, EmojiCrab() + " Sideways", yellowCol, TA_CENTER | TA_VCENTER);
      curY += rowH;
      g_canvas.Line(tx, curY, tx + w, curY, lineC);

      // Row 2 - Counts
      g_canvas.FontSet("Trebuchet MS", baseFontSize, FW_NORMAL);
      g_canvas.TextOut(cx0, curY + rowH / 2, IntegerToString(DashBullSignals) + " sigs", txtC, TA_CENTER | TA_VCENTER);
      g_canvas.TextOut(cx1, curY + rowH / 2, IntegerToString(DashBearSignals) + " sigs", txtC, TA_CENTER | TA_VCENTER);
      g_canvas.TextOut(cx2, curY + rowH / 2, IntegerToString(DashSharkEvents) + " events", txtC, TA_CENTER | TA_VCENTER);
      g_canvas.TextOut(cx3, curY + rowH / 2, IntegerToString(DashCrabEvents)  + " events", txtC, TA_CENTER | TA_VCENTER);
      curY += rowH;
      g_canvas.Line(tx, curY, tx + w, curY, lineC);

      // Row 3 - Wins / Bars
      g_canvas.TextOut(cx0, curY + rowH / 2, IntegerToString(DashBullWins) + " wins", txtC, TA_CENTER | TA_VCENTER);
      g_canvas.TextOut(cx1, curY + rowH / 2, IntegerToString(DashBearWins) + " wins", txtC, TA_CENTER | TA_VCENTER);
      g_canvas.TextOut(cx2, curY + rowH / 2, IntegerToString(DashSharkBars) + " bars", txtC, TA_CENTER | TA_VCENTER);
      g_canvas.TextOut(cx3, curY + rowH / 2, IntegerToString(DashCrabBars)  + " bars", txtC, TA_CENTER | TA_VCENTER);
      curY += rowH;
      g_canvas.Line(tx, curY, tx + w, curY, lineC);

      // Row 4 - Rates
      double bullRate = DashBullSignals > 0 ? ((double)DashBullWins / (double)DashBullSignals * 100.0) : 0.0;
      double bearRate = DashBearSignals > 0 ? ((double)DashBearWins / (double)DashBearSignals * 100.0) : 0.0;
      uint bullRC = bullRate >= 60.0 ? (0xFF000000 | 0x00E676) : (bullRate >= 40.0 ? (0xFF000000 | 0xFFEB3B) : (0xFF000000 | 0xFF5252));
      uint bearRC = bearRate >= 60.0 ? (0xFF000000 | 0x00E676) : (bearRate >= 40.0 ? (0xFF000000 | 0xFFEB3B) : (0xFF000000 | 0xFF5252));
      double totalBars = MathMax(1.0, (double)g_ratesTotal);
      double sharkPct  = (double)DashSharkBars / totalBars * 100.0;
      double crabPct   = (double)DashCrabBars  / totalBars * 100.0;

      g_canvas.TextOut(cx0, curY + rowH / 2, DashBullSignals > 0 ? DoubleToString(bullRate, 1) + "%" : "—", bullRC, TA_CENTER | TA_VCENTER);
      g_canvas.TextOut(cx1, curY + rowH / 2, DashBearSignals > 0 ? DoubleToString(bearRate, 1) + "%" : "—", bearRC, TA_CENTER | TA_VCENTER);
      g_canvas.TextOut(cx2, curY + rowH / 2, DoubleToString(sharkPct, 1) + "%", orangeCol, TA_CENTER | TA_VCENTER);
      g_canvas.TextOut(cx3, curY + rowH / 2, DoubleToString(crabPct, 1)  + "%", yellowCol, TA_CENTER | TA_VCENTER);

      // Column divider lines
      g_canvas.Line(tx + colW,     ty + hdrH, tx + colW,     curY + rowH, lineC);
      g_canvas.Line(tx + 2 * colW, ty + hdrH, tx + 2 * colW, curY + rowH, lineC);
      g_canvas.Line(tx + 3 * colW, ty + hdrH, tx + 3 * colW, curY + rowH, lineC);

      curY += rowH;
      g_canvas.Line(tx, curY, tx + w, curY, lineC);

      // Optional Markov Section (2 rows)
      if(InpShowMarkovRow) {
         int mkRowH = rowH - 4;
         g_canvas.FontSet("Trebuchet MS", baseFontSize, FW_BOLD);
         g_canvas.TextOut(tx + w / 2, curY + mkRowH / 2, "Markov Forecast Next", txtC, TA_CENTER | TA_VCENTER);
         curY += mkRowH;
         g_canvas.Line(tx, curY, tx + w, curY, lineC);

         g_canvas.FontSet("Segoe UI Emoji", baseFontSize - 1, FW_NORMAL);
         string mkConfTag = McConfident ? "" : " · low-n";
         string mkLine = EmojiWhaleBull() + " " + DoubleToString(PBull * 100.0, 1) + "%  ·  " +
                         EmojiCrab()      + " " + DoubleToString(PSide * 100.0, 1) + "%  ·  " +
                         EmojiWhaleBear() + " " + DoubleToString(PBear * 100.0, 1) + "%   (n=" +
                         IntegerToString((int)MathRound(McSampleN)) + mkConfTag + ")";
         g_canvas.TextOut(tx + w / 2, curY + mkRowH / 2, mkLine, txtC, TA_CENTER | TA_VCENTER);
         curY += mkRowH;
         g_canvas.Line(tx, curY, tx + w, curY, lineC);
      }

      // Current State Section (2 rows)
      int csRowH = rowH - 4;
      string csLine1 = (InpUseMarkov && McConfident)
                       ? (CurStateEmoji + " · " + IntegerToString(McStateDur) + " bars · stay " + IntegerToString((int)MathRound(PStayCurrent * 100.0)) + "%")
                       : (InpUseMarkov ? (CurStateEmoji + " · " + IntegerToString(McStateDur) + " bars · learning") : (CurStateEmoji + " · " + IntegerToString(McStateDur) + " bars"));

      string csLine2 = (InpUseMarkov && McConfident)
                       ? ("change in ~" + IntegerToString(ExpChangeBars) + " bars → " + NextLikelyEmoji)
                       : "";

      uint csCol = (StateBalanced && InpUseMarkov && McConfident) ? (0xFF000000 | 0xFFEB3B) : (InpUseMarkov && McConfident ? (0xFF000000 | 0x00E676) : txtC);

      g_canvas.FontSet("Segoe UI Emoji", baseFontSize, FW_NORMAL);
      g_canvas.TextOut(tx + w / 2, curY + csRowH / 2, csLine1, csCol, TA_CENTER | TA_VCENTER);
      curY += csRowH;
      g_canvas.Line(tx, curY, tx + w, curY, lineC);

      if(csLine2 != "") {
         g_canvas.TextOut(tx + w / 2, curY + csRowH / 2, csLine2, csCol, TA_CENTER | TA_VCENTER);
         curY += csRowH;
         g_canvas.Line(tx, curY, tx + w, curY, lineC);
      }

      // Footer Rows
      int ftRowH = rowH - 6;
      uint ftCol = 0xFF000000 | (clrSilver & 0xFFFFFF);
      g_canvas.FontSet("Trebuchet MS", footerFontSize, FW_NORMAL);
      g_canvas.TextOut(tx + w / 2, curY + ftRowH / 2, "⚠ Not advice · MFE " + DoubleToString(InpATRMult, 2) + "×ATR (" + IntegerToString(InpATRLen) + ")", ftCol, TA_CENTER | TA_VCENTER);
      curY += ftRowH;
      g_canvas.Line(tx, curY, tx + w, curY, lineC);

      g_canvas.TextOut(tx + w / 2, curY + ftRowH / 2, "Markov α=1  decay=" + DoubleToString(InpMarkovDecay, 3), ftCol, TA_CENTER | TA_VCENTER);
   }

   g_canvas.Update();
   ChartRedraw();
}

//+------------------------------------------------------------------+
//| Custom Indicator Initialization Function                         |
//+------------------------------------------------------------------+
int OnInit() {
   SetIndexBuffer(0, BufferBullSignal, INDICATOR_CALCULATIONS);
   SetIndexBuffer(1, BufferBearSignal, INDICATOR_CALCULATIONS);
   SetIndexBuffer(2, BufferRegime,     INDICATOR_CALCULATIONS);
   SetIndexBuffer(3, BufferChoppiness, INDICATOR_CALCULATIONS);
   SetIndexBuffer(4, BufferIsChoppy,   INDICATOR_CALCULATIONS);
   SetIndexBuffer(5, BufferMarkovPBull,INDICATOR_CALCULATIONS);
   SetIndexBuffer(6, BufferMarkovPBear,INDICATOR_CALCULATIONS);
   SetIndexBuffer(7, BufferMomMid,     INDICATOR_CALCULATIONS);
   SetIndexBuffer(8, BufferOscMid,     INDICATOR_CALCULATIONS);

   IndicatorSetInteger(INDICATOR_DIGITS, 2);
   IndicatorSetString(INDICATOR_SHORTNAME, "Liquidity Surge + Markov [TechnicalZen]");

   g_subwindow = ChartWindowFind();
   g_canvasName = "LSF_Markov3D_" + IntegerToString(ChartID());
   ObjectDelete(0, g_canvasName);

   g_w = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   g_h = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, g_subwindow);
   if(g_h <= 0) g_h = 260;

   if(!g_canvas.CreateBitmapLabel(ChartID(), g_subwindow, g_canvasName, 0, 0, g_w, g_h, COLOR_FORMAT_XRGB_NOALPHA)) {
      Print("Error creating canvas bitmap label: ", GetLastError());
      return INIT_FAILED;
   }
   ObjectSetInteger(0, g_canvasName, OBJPROP_BACK, false);

   ZeroMemory(g_ringBuf);
   ArrayInitialize(McShort, 0.0);
   ArrayInitialize(McLong,  0.0);
   g_lastStatefulBar = -1;
   g_isReady = false;
   g_lastFrameTick = 0;

   return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| Custom Indicator Deinitialization Function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   g_canvas.Destroy();
   ObjectDelete(0, g_canvasName);
   ChartRedraw();
}

//+------------------------------------------------------------------+
//| Chart Event Handler (Resize & Redraw)                            |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) {
   if(id == CHARTEVENT_CHART_CHANGE) {
      g_w = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
      g_h = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, g_subwindow);
      if(g_w > 10 && g_h > 10 && (g_w != (int)g_canvas.Width() || g_h != (int)g_canvas.Height())) {
         g_canvas.Resize(g_w, g_h);
         Render();
      }
   }
}

//+------------------------------------------------------------------+
//| Custom Indicator Iteration Function                              |
//+------------------------------------------------------------------+
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 < 80) return 0;
   g_ratesTotal = rates_total;

   int lookback = (prev_calculated == 0) ? MathMax(0, rates_total - InpCalcBars) : (prev_calculated - 1);

   static double momBuffer[];
   static double vwmaBuffer[];
   if(ArraySize(momBuffer) != rates_total)  ArrayResize(momBuffer, rates_total);
   if(ArraySize(vwmaBuffer) != rates_total) ArrayResize(vwmaBuffer, rates_total);

   // Populate Raw Momentum (MFI/RSI) and Oscillator series
   for(int i = lookback; i < rates_total; i++) {
      if(InpMomentumSrc == MOM_MFI) momBuffer[i] = CalcMFI(high, low, close, tick_volume, InpMomentumLen, i);
      else                          momBuffer[i] = CalcRSI(close, InpMomentumLen, i);

      if(InpOscType == OSC_HULL_VWMA) vwmaBuffer[i] = CalcVWMA(close, tick_volume, InpVWMALen, i);
      else                            vwmaBuffer[i] = CalcSignedADX(high, low, close, InpADXLen, i);
   }

   // Maintain 3D Ring Buffer for the visible time span
   int termStart = MathMax(lookback, rates_total - InpTimeSpan - 2);
   for(int i = termStart; i < rates_total; i++) {
      for(int d = 0; d < MOM_LAYERS; d++)
         g_ringBuf[g_ringHead].momentum[d] = (float)MomLayerAt(momBuffer, c_momLengths[d], i);

      for(int d = 0; d < OSC_LAYERS; d++)
         g_ringBuf[g_ringHead].oscillator[d] = (float)OscLayerAt(vwmaBuffer, c_oscLengths[d], i);

      if(i < rates_total - 1)
         g_ringHead = (g_ringHead + 1) % MAX_RING_BUF;
   }

   // Incremental state evaluation on newly CLOSED bars
   long stStart = (g_lastStatefulBar < 0) ? (long)MathMax(0, rates_total - InpCalcBars) : (g_lastStatefulBar + 1);
   for(long i = stStart; i < rates_total - 1; i++) {
      ProcessBarMarkov(high, low, close, (int)i);
      ProcessBarWinRate(high, low, close, momBuffer, vwmaBuffer, (int)i);

      // Populate calculation buffers
      BufferRegime[(int)i]      = (double)McState;
      BufferChoppiness[(int)i]  = ChoppinessIndexAt(high, low, close, InpChopLen, (int)i);
      BufferIsChoppy[(int)i]    = IsChoppy_Last ? 1.0 : 0.0;
      BufferMarkovPBull[(int)i] = PBull;
      BufferMarkovPBear[(int)i] = PBear;
      BufferMomMid[(int)i]      = MomLayerAt(momBuffer, 20, (int)i);
      BufferOscMid[(int)i]      = OscLayerAt(vwmaBuffer, 19, (int)i);
   }

   if(rates_total >= 2)
      g_lastStatefulBar = rates_total - 2;

   g_isReady = true;
   uint now = GetTickCount();
   if(now - g_lastFrameTick > 30) { // Limit render to ~33 FPS
      g_w = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
      g_h = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, g_subwindow);
      if(g_w != (int)g_canvas.Width() || g_h != (int)g_canvas.Height())
         g_canvas.Resize(g_w, g_h);

      Render();
      g_lastFrameTick = now;
   }

   return rates_total;
}
//+------------------------------------------------------------------+
