//+------------------------------------------------------------------+
//|                                                       LogReg.mqh  |
//|                                   @temonba - binaryforexea.com    |
//|  Logistic regression from scratch, in 100% native MQL5.          |
//|  No Python, no ONNX, no DLL, no third-party libraries, not even   |
//|  the built-in matrix type: just double arrays, so it compiles     |
//|  and runs on any terminal with nothing installed. Part 1 of the   |
//|  "Machine Learning in Pure MQL5" series. The class trains by      |
//|  stochastic gradient descent, standardizes its own features, and  |
//|  can save and load a trained model to a plain file.               |
//+------------------------------------------------------------------+
#property copyright "@temonba"
#property link      "https://www.mql5.com/en/users/temonba"

//+------------------------------------------------------------------+
//| Logistic regression classifier (binary).                         |
//| Feature matrices are passed FLAT, row-major: element (i,j) lives  |
//| at X[i * nfeatures + j]. This keeps the class dependency-free     |
//| (MQL5 dynamic arrays cannot be flexibly sized in 2D).             |
//+------------------------------------------------------------------+
class CLogReg
  {
private:
   int      m_nf;             // number of features
   double   m_w[];            // weights, one per feature
   double   m_b;              // bias (intercept)
   double   m_mean[];         // per-feature mean  (standardization)
   double   m_std[];          // per-feature stdev (standardization)
   double   m_lr;             // learning rate
   bool     m_scaled;         // has the scaler been fitted?

   //+------------------------------------------------------------------+
   //| Sigmoid: numerically stable logistic function                    |
   //+------------------------------------------------------------------+
   double   Sigmoid(const double z)
     {
      if(z >= 0.0)
         return(1.0 / (1.0 + MathExp(-z)));
      double e = MathExp(z);              // stable form for very negative z
      return(e / (1.0 + e));
     }
public:
            CLogReg(void) { m_nf = 0; m_b = 0.0; m_lr = 0.05; m_scaled = false; }

   //--- set up the model for a given feature count and learning rate
   bool     Init(const int nfeatures, const double lr);
   //--- learn the standardization (mean/stdev) from the TRAINING rows only
   void     FitScaler(const double &X[], const int rows);
   //--- turn one raw feature row into a standardized row
   void     Standardize(const double &raw[], double &z[]);
   //--- probability that the row belongs to class 1
   double   Predict(const double &raw[]);
   //--- hard class using a probability threshold
   int      PredictClass(const double &raw[], const double thr = 0.5);
   //--- train by stochastic gradient descent; returns the final mean loss
   double   Fit(const double &X[], const int &y[], const int rows, const int epochs);
   //--- persist / restore a trained model (plain text, no dependencies)
   bool     Save(const string file);
   bool     Load(const string file);
   //--- read-only helpers for reporting
   int      Features(void) const { return(m_nf); }                                        // number of features
   double   Weight(const int j) const { return((j >= 0 && j < m_nf) ? m_w[j] : 0.0); }    // weight of feature j
   double   Bias(void) const { return(m_b); }                                             // the bias (intercept)
  };
//+------------------------------------------------------------------+
//| Allocate weights and scaler for nfeatures inputs.                |
//+------------------------------------------------------------------+
bool CLogReg::Init(const int nfeatures, const double lr)
  {
   if(nfeatures <= 0)
      return(false);
   m_nf = nfeatures;
   m_lr = lr;
   m_b = 0.0;
   m_scaled = false;
   ArrayResize(m_w, m_nf);
   ArrayResize(m_mean, m_nf);
   ArrayResize(m_std, m_nf);
   for(int j = 0; j < m_nf; j++)
     {
      m_w[j] = 0.0;
      m_mean[j] = 0.0;
      m_std[j] = 1.0;
     }
   return(true);
  }
//+------------------------------------------------------------------+
//| Compute per-feature mean and stdev over the training rows.       |
//| Fitting the scaler on TRAIN ONLY avoids look-ahead leakage.      |
//+------------------------------------------------------------------+
void CLogReg::FitScaler(const double &X[], const int rows)
  {
   for(int j = 0; j < m_nf; j++)
     {
      double s = 0.0;
      for(int i = 0; i < rows; i++)
         s += X[i * m_nf + j];
      double mean = s / rows;
      double v = 0.0;
      for(int i = 0; i < rows; i++)
        {
         double d = X[i * m_nf + j] - mean;
         v += d * d;
        }
      double sd = MathSqrt(v / rows);
      if(sd < 1e-12)
         sd = 1.0;               // a constant feature: leave it untouched
      m_mean[j] = mean;
      m_std[j]  = sd;
     }
   m_scaled = true;
  }
//+------------------------------------------------------------------+
//| Standardize one raw row into z = (x - mean) / std.               |
//+------------------------------------------------------------------+
void CLogReg::Standardize(const double &raw[], double &z[])
  {
   ArrayResize(z, m_nf);
   for(int j = 0; j < m_nf; j++)
      z[j] = (raw[j] - m_mean[j]) / m_std[j];
  }
//+------------------------------------------------------------------+
//| Probability of class 1 for a raw feature row.                    |
//+------------------------------------------------------------------+
double CLogReg::Predict(const double &raw[])
  {
   double z[];
   Standardize(raw, z);
   double s = m_b;
   for(int j = 0; j < m_nf; j++)
      s += m_w[j] * z[j];
   return(Sigmoid(s));
  }
//+------------------------------------------------------------------+
//| Hard class from the probability and a threshold.                 |
//+------------------------------------------------------------------+
int CLogReg::PredictClass(const double &raw[], const double thr = 0.5)
  {
   return(Predict(raw) >= thr ? 1 : 0);
  }
//+------------------------------------------------------------------+
//| Train by stochastic gradient descent over the flat matrix X.     |
//| One example at a time, one pass = one epoch, rows shuffled each   |
//| epoch. Returns the final average binary cross-entropy loss.      |
//+------------------------------------------------------------------+
double CLogReg::Fit(const double &X[], const int &y[], const int rows, const int epochs)
  {
   if(!m_scaled)
      FitScaler(X, rows);

   int order[];
   ArrayResize(order, rows);
   for(int i = 0; i < rows; i++)
      order[i] = i;

   double z[];
   ArrayResize(z, m_nf);
   double loss = 0.0;

   for(int e = 0; e < epochs; e++)
     {
      //--- Fisher-Yates shuffle so SGD does not see a fixed order
      for(int i = rows - 1; i > 0; i--)
        {
         int k = (int)(MathRand() % (i + 1));
         int tmp = order[i];
         order[i] = order[k];
         order[k] = tmp;
        }

      loss = 0.0;
      for(int t = 0; t < rows; t++)
        {
         int i = order[t];
         //--- standardize this row
         for(int j = 0; j < m_nf; j++)
            z[j] = (X[i * m_nf + j] - m_mean[j]) / m_std[j];
         //--- forward pass
         double s = m_b;
         for(int j = 0; j < m_nf; j++)
            s += m_w[j] * z[j];
         double p = Sigmoid(s);
         //--- gradient of cross-entropy is simply (p - y)
         double err = p - (double)y[i];
         //--- update weights and bias
         for(int j = 0; j < m_nf; j++)
            m_w[j] -= m_lr * err * z[j];
         m_b -= m_lr * err;
         //--- accumulate loss for reporting
         double pc = MathMax(1e-12, MathMin(1.0 - 1e-12, p));
         loss += -(y[i] * MathLog(pc) + (1 - y[i]) * MathLog(1.0 - pc));
        }
      loss /= rows;
     }
   return(loss);
  }
//+------------------------------------------------------------------+
//| Save the trained model to a plain text file (no dependencies).   |
//+------------------------------------------------------------------+
bool CLogReg::Save(const string file)
  {
   int h = FileOpen(file, FILE_WRITE | FILE_TXT | FILE_ANSI);
   if(h == INVALID_HANDLE)
      return(false);
   FileWriteString(h, (string)m_nf + "\n");
   FileWriteString(h, DoubleToString(m_b, 10) + "\n");
   for(int j = 0; j < m_nf; j++)
      FileWriteString(h, DoubleToString(m_w[j], 10) + " " +
                      DoubleToString(m_mean[j], 10) + " " +
                      DoubleToString(m_std[j], 10) + "\n");
   FileClose(h);
   return(true);
  }
//+------------------------------------------------------------------+
//| Load a model saved by Save().                                    |
//+------------------------------------------------------------------+
bool CLogReg::Load(const string file)
  {
   int h = FileOpen(file, FILE_READ | FILE_TXT | FILE_ANSI);
   if(h == INVALID_HANDLE)
      return(false);
   int nf = (int)StringToInteger(FileReadString(h));
   Init(nf, m_lr);                      // resets weights for nf features
   m_b = StringToDouble(FileReadString(h));
   for(int j = 0; j < m_nf; j++)
     {
      string line = FileReadString(h);
      string parts[];
      int n = StringSplit(line, ' ', parts);
      if(n >= 3)
        {
         m_w[j]    = StringToDouble(parts[0]);
         m_mean[j] = StringToDouble(parts[1]);
         m_std[j]  = StringToDouble(parts[2]);
        }
     }
   m_scaled = true;
   FileClose(h);
   return(true);
  }
//+------------------------------------------------------------------+
