//+------------------------------------------------------------------+
//|                                                  SignalStats.mqh |
//| Honest statistics for indicator signals: how often price went    |
//| N x ATR with the signal before N x ATR against it, next to the   |
//| same question asked from random earlier candles, with the range  |
//| that luck alone could produce. Works in MQL5 and MQL4.           |
//+------------------------------------------------------------------+
#property strict

//--- one signal: the bar that confirmed it (closed) and its direction
struct SStatsSignal
  {
   int               bar;          // index in the rates array, oldest first
   bool              bull;
  };

//--- counters for one side (0 = bullish, 1 = bearish)
struct SStatsCount
  {
   int               signals;      // signals found
   int               decided;      // signals whose outcome was decided
   int               hits;         // ... of them: went N x ATR the signal's way first
   int               rndDecided;   // random candles whose outcome was decided
   int               rndHits;      // ... of them: went N x ATR the same way first
  };

//+------------------------------------------------------------------+
//| CSignalStats                                                     |
//+------------------------------------------------------------------+
class CSignalStats
  {
private:
   double            m_reach;      // distance in ATR
   int               m_look;       // candles allowed to decide
   int               m_fakes;      // random candles per signal
   int               m_minBack;    // random candle: at least this many bars before the signal
   int               m_maxBack;    // ... and at most this many
   int               m_first;      // first bar with a valid ATR
   SStatsSignal      m_sig[];
   SStatsCount       m_fin[2];     // signals whose window is closed - never change again
   SStatsCount       m_all[2];     // m_fin + the still open ones
   int               m_finUpTo;    // m_sig[0 .. m_finUpTo) are counted in m_fin

   double            Rand(const long seed) const;
   int               Outcome(const MqlRates &r[], const int n, const int start, const double dist, const bool bull) const;
   void              Count(const MqlRates &r[], const double &atr[], const int n, const int from, const int to,
                           SStatsCount &c[]) const;

public:
                     CSignalStats(void);
   void              Init(const double reach, const int look, const int fakes = 3, const int minBack = 5,
                          const int maxBack = 200, const int first = 15);
   void              Reset(void);
   void              Add(const int bar, const bool bull);
   int               Total(void) const { return ArraySize(m_sig); }
   SStatsSignal      Get(const int k) const { return m_sig[k]; }
   void              Update(const MqlRates &r[], const double &atr[], const int n);
   SStatsCount       Result(const int side) const { return m_all[side]; }
   static void       Edge(const SStatsCount &c, double &diff, double &margin);
   static bool       Beats(const SStatsCount &c, const int minCases = 100);
  };

//+------------------------------------------------------------------+
CSignalStats::CSignalStats(void)
  {
   Init(1.0, 50);
  }

void CSignalStats::Init(const double reach, const int look, const int fakes, const int minBack, const int maxBack,
                        const int first)
  {
   m_reach = reach;
   m_look = look;
   m_fakes = fakes;
   m_minBack = minBack;
   m_maxBack = maxBack;
   m_first = first;
   Reset();
  }

void CSignalStats::Reset(void)
  {
   ArrayResize(m_sig, 0);
   ZeroMemory(m_fin);
   ZeroMemory(m_all);
   m_finUpTo = 0;
  }

void CSignalStats::Add(const int bar, const bool bull)
  {
   int k = ArraySize(m_sig);
   ArrayResize(m_sig, k + 1, 1024);
   m_sig[k].bar = bar;
   m_sig[k].bull = bull;
  }

//+------------------------------------------------------------------+
//| A fixed number in [0, 1) for each seed: the same chart gives the |
//| same random candles on every reload.                             |
//+------------------------------------------------------------------+
double CSignalStats::Rand(const long seed) const
  {
   long x = (seed * 1103515245 + 12345) & 0x7FFFFFFF;
   x = (x * 1103515245 + 12345) & 0x7FFFFFFF;
   return x / 2147483648.0;
  }

//+------------------------------------------------------------------+
//| From the close of bar start-1: 1 = price went dist the signal's  |
//| way first, 0 = dist against it first, -1 = not decided within    |
//| m_look bars, or one bar touched both levels (order unknown).     |
//+------------------------------------------------------------------+
int CSignalStats::Outcome(const MqlRates &r[], const int n, const int start, const double dist, const bool bull) const
  {
   double p = r[start - 1].close;
   double good = bull ? p + dist : p - dist;
   double bad  = bull ? p - dist : p + dist;
   int end = MathMin(start + m_look, n);
   for(int k = start; k < end; k++)
     {
      bool g = bull ? r[k].high >= good : r[k].low <= good;
      bool b = bull ? r[k].low <= bad : r[k].high >= bad;
      if(g && b)
         return -1;
      if(g)
         return 1;
      if(b)
         return 0;
     }
   return -1;
  }

//+------------------------------------------------------------------+
//| Count signals [from, to) and their random twins into c[].        |
//+------------------------------------------------------------------+
void CSignalStats::Count(const MqlRates &r[], const double &atr[], const int n, const int from, const int to,
                         SStatsCount &c[]) const
  {
   int span = m_maxBack - m_minBack + 1;
   for(int s = from; s < to; s++)
     {
      int j = m_sig[s].bar, side = m_sig[s].bull ? 0 : 1;
      if(j < m_first || atr[j] <= 0)
         continue;
      c[side].signals++;
      int res = Outcome(r, n, j + 1, m_reach * atr[j], m_sig[s].bull);
      if(res >= 0)
        {
         c[side].decided++;
         c[side].hits += res;
        }
      //--- seeded by the signal's TIME, not its index: when old bars drop off a full chart,
      //--- every index shifts, but the time stays - so the random candles stay the same
      long t = (long)(r[j].time / 60);
      for(int f = 0; f < m_fakes; f++)
        {
         int m = j - m_minBack - (int)(Rand(t * 7 + f * 3 + side) * span);
         if(m < m_first || atr[m] <= 0)
            continue;
         int fr = Outcome(r, n, m + 1, m_reach * atr[m], m_sig[s].bull);
         if(fr >= 0)
           {
            c[side].rndDecided++;
            c[side].rndHits += fr;
           }
        }
     }
  }

//+------------------------------------------------------------------+
//| n = number of CLOSED bars in r[] (r[n] may be the forming one).  |
//| Signals whose whole window is closed are counted once and kept;  |
//| only the last few are recounted on every new bar.                |
//+------------------------------------------------------------------+
void CSignalStats::Update(const MqlRates &r[], const double &atr[], const int n)
  {
   int done = m_finUpTo, total = ArraySize(m_sig);
   while(done < total && m_sig[done].bar + 1 + m_look <= n)
      done++;
   if(done > m_finUpTo)
     {
      Count(r, atr, n, m_finUpTo, done, m_fin);
      m_finUpTo = done;
     }
   m_all[0] = m_fin[0];
   m_all[1] = m_fin[1];
   Count(r, atr, n, m_finUpTo, total, m_all);
  }

//+------------------------------------------------------------------+
//| diff = signal hit rate - random hit rate; margin = the largest   |
//| difference luck alone produces in 95% of cases (two-proportion   |
//| test, pooled rate).                                              |
//+------------------------------------------------------------------+
void CSignalStats::Edge(const SStatsCount &c, double &diff, double &margin)
  {
   diff = 0;
   margin = 0;
   if(c.decided == 0 || c.rndDecided == 0)
      return;
   double ps = c.hits / (double)c.decided;
   double pr = c.rndHits / (double)c.rndDecided;
   double p  = (c.hits + c.rndHits) / (double)(c.decided + c.rndDecided);
   diff = ps - pr;
   margin = 1.96 * MathSqrt(p * (1 - p) * (1.0 / c.decided + 1.0 / c.rndDecided));
  }

bool CSignalStats::Beats(const SStatsCount &c, const int minCases)
  {
   double diff, margin;
   Edge(c, diff, margin);
   return c.decided >= minCases && diff > margin;
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| The table on the chart: OBJ_LABEL cells, columns sized with      |
//| TextGetSize, green only where a result beats luck.               |
//+------------------------------------------------------------------+
void StatsLabel(const string name, const int x, const int y, const string text, const color col,
                const ENUM_ANCHOR_POINT anchor, const string font, const int size)
  {
   ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, name, OBJPROP_ANCHOR, anchor);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
   ObjectSetString(0, name, OBJPROP_FONT, font);
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, size);
   ObjectSetInteger(0, name, OBJPROP_COLOR, col);
   ObjectSetString(0, name, OBJPROP_TEXT, text == "" ? " " : text);   // an empty label would show "Label"
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
  }

string StatsPct(const int a, const int b) { return b > 0 ? StringFormat("%.0f%%", 100.0 * a / b) : "-"; }

void DrawStatsTable(const CSignalStats &st, const string prefix, const string what, const double reach,
                    const int look, const int bars, const ENUM_BASE_CORNER corner, const int fontSize = 9)
  {
   const string font = "Segoe UI";
   const color  gray = C'140,146,160';
   ObjectsDeleteAll(0, prefix);
   string g[6][3];
   int    gc[6][3];                          // colours (int: ArrayInitialize works on it in MQL4 too)
   ArrayInitialize(gc, clrWhite);
   g[0][0] = " ";                            g[0][1] = "Bullish"; g[0][2] = "Bearish";
   g[1][0] = what + " found";
   g[2][0] = StringFormat("Then price went %.1f ATR its way first", reach);
   g[3][0] = "From a random candle, same direction";
   g[4][0] = "Difference (% points)";
   g[5][0] = "Luck alone could give (+/- points)";
   string notes[];
   color  noteCol[];
   for(int s = 0; s < 2; s++)
     {
      SStatsCount c = st.Result(s);
      double diff, margin;
      CSignalStats::Edge(c, diff, margin);
      g[1][s + 1] = IntegerToString(c.signals);
      g[2][s + 1] = StatsPct(c.hits, c.decided);
      g[3][s + 1] = StatsPct(c.rndHits, c.rndDecided);
      g[4][s + 1] = (c.decided > 0 && c.rndDecided > 0) ? StringFormat("%+.1f", diff * 100) : "-";
      g[5][s + 1] = (c.decided > 0 && c.rndDecided > 0) ? StringFormat("%.1f", margin * 100) : "-";
      for(int r = 0; r < 6; r++)
         gc[r][s + 1] = (r == 0 || r == 3 || r == 5) ? gray : clrWhite;
      if(CSignalStats::Beats(c))
        {
         gc[4][s + 1] = clrLime;
         int k = ArraySize(notes);
         ArrayResize(notes, k + 1);
         ArrayResize(noteCol, k + 1);
         notes[k] = StringFormat("%s: beyond luck (luck alone: up to %.1f)", s == 0 ? "Bullish" : "Bearish", margin * 100);
         noteCol[k] = clrLime;
        }
     }
   for(int r = 0; r < 6; r++)
      gc[r][0] = (r == 0 || r == 5) ? gray : clrWhite;
   string fixed[3];
   fixed[0] = StringFormat("Went = %.1f ATR(14) before %.1f ATR against, within %d bars", reach, reach, look);
   fixed[1] = "Random = 3 random candles 5-200 bars before each signal";
   fixed[2] = StringFormat("Based on %d closed bars on this chart", bars);
   for(int i = 0; i < 3; i++)
     {
      int k = ArraySize(notes);
      ArrayResize(notes, k + 1);
      ArrayResize(noteCol, k + 1);
      notes[k] = fixed[i];
      noteCol[k] = gray;
     }
   //--- size the columns from the real text width
   TextSetFont(font, -fontSize * 10);
   uint tw, th;
   TextGetSize("Ag", tw, th);
   int rowH = (int)th + 6, pad = 8, gap = 14;
   int colW[3] = {0, 0, 0};
   for(int r = 0; r < 6; r++)
      for(int c = 0; c < 3; c++)
        {
         TextGetSize(g[r][c], tw, th);
         colW[c] = MathMax(colW[c], (int)(tw * 1.1));
        }
   int width = colW[0] + colW[1] + colW[2] + 2 * gap;
   for(int i = 0; i < ArraySize(notes); i++)
     {
      TextGetSize(notes[i], tw, th);
      width = MathMax(width, (int)(tw * 1.1));
     }
   width += 2 * pad;
   int height = (6 + ArraySize(notes)) * rowH + 2 * pad + 4;
   int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   bool right = corner == CORNER_RIGHT_UPPER || corner == CORNER_RIGHT_LOWER;
   bool lower = corner == CORNER_LEFT_LOWER || corner == CORNER_RIGHT_LOWER;
   int x0 = right ? MathMax(0, chartW - width - 10) : 10;
   int y0 = lower ? MathMax(0, chartH - height - 10) : 24;
   string bg = prefix + "bg";
   ObjectCreate(0, bg, OBJ_RECTANGLE_LABEL, 0, 0, 0);
   ObjectSetInteger(0, bg, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, bg, OBJPROP_XDISTANCE, x0);
   ObjectSetInteger(0, bg, OBJPROP_YDISTANCE, y0);
   ObjectSetInteger(0, bg, OBJPROP_XSIZE, width);
   ObjectSetInteger(0, bg, OBJPROP_YSIZE, height);
   ObjectSetInteger(0, bg, OBJPROP_BGCOLOR, C'19,23,34');
   ObjectSetInteger(0, bg, OBJPROP_BORDER_TYPE, BORDER_FLAT);
   ObjectSetInteger(0, bg, OBJPROP_COLOR, C'60,66,80');
   ObjectSetInteger(0, bg, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, bg, OBJPROP_HIDDEN, true);
   int y = y0 + pad;
   for(int r = 0; r < 6; r++, y += rowH)
     {
      int x = x0 + pad;
      for(int c = 0; c < 3; c++)
        {
         StatsLabel(prefix + StringFormat("c%d_%d", r, c), c == 0 ? x : x + colW[c], y, g[r][c], (color)gc[r][c],
                    c == 0 ? ANCHOR_LEFT_UPPER : ANCHOR_RIGHT_UPPER, font, fontSize);
         x += colW[c] + gap;
        }
     }
   y += 4;
   for(int i = 0; i < ArraySize(notes); i++, y += rowH)
      StatsLabel(prefix + StringFormat("n%d", i), x0 + pad, y, notes[i], noteCol[i], ANCHOR_LEFT_UPPER, font, fontSize);
   ChartRedraw();
  }
//+------------------------------------------------------------------+
