//+------------------------------------------------------------------+
//|                                   PhaseSpaceAnalogProjection.mq5  |
//|                                                          Algobot  |
//|                                     https://www.algobot.live      |
//+------------------------------------------------------------------+
//  Native MQL5 port of the Algobot C# strategy PhaseSpaceAnalogProjection.
//
//  THE MARKET AS A TRAJECTORY.
//  Treat price as the observable output of an unknown dynamical system.
//  Reconstruct its state with DELAY COORDINATES built from one-bar log
//  returns:  s(t) = ( r_t , r_{t-1} , ... , r_{t-(m-1)} ).  Two moments in
//  history that occupy nearly the same point in this m-dimensional phase
//  space are dynamical ANALOGS -- their next few bars are informative
//  about what comes next now.
//
//  FORECAST (analog projection).  For the current state s* find the K
//  nearest neighbours by Euclidean distance, weight each by exp(-D/d0)
//  (d0 = mean neighbour distance), and take the distance-weighted average
//  of their realised h-bar forward log returns.  That average F is the
//  expected h-bar move from the current geometry.
//
//  SAFEGUARDS.  A Theiler window forces neighbours to be >= max(h,m) bars
//  apart (distinct episodes, no overlapping run counted K times), and an
//  agreement gate rejects projections where too few analogs share F's sign.
//
//  ENTRY: long when F > +Threshold*sigma*sqrt(h) and analogs agree;
//         short when F < -Threshold*sigma*sqrt(h) and analogs agree.
//  EXIT:  ATR stop + ATR target; adaptive close if the live projection
//         flips against the trade; time stop once horizon h has elapsed.
//+------------------------------------------------------------------+
#property copyright "Algobot"
#property link      "https://www.algobot.live"
#property version   "1.00"
#property strict

#include <Trade\Trade.mqh>
CTrade trade;

//--- inputs (mirror DescribeParameters) --------------------------------------
input int    EmbedDim       = 4;      // embedding dimension m (state length)   [2..12]
input int    Neighbors      = 10;     // K nearest analogs pooled               [3..40]
input int    Horizon        = 6;      // forecast horizon h (bars ahead)        [1..30]
input double ThresholdSigma = 0.5;    // signal threshold in volatility units   [0..3]
input double MinAgreement   = 0.60;   // fraction of analogs that must agree     [0.5..1]
input int    LibraryBars    = 1000;   // searchable analog library size (bars)  [300..4000]
input int    AtrPeriod      = 14;     // ATR period                              [7..40]
input double AtrSlMult      = 2.0;    // ATR stop multiplier                     [0.8..5]
input double AtrTpMult      = 3.0;    // ATR target multiplier                   [1..6]
input double Lots           = 0.10;   // fixed lot size                          [0.01..1]
input long   Magic          = 5138;   // magic number

//--- clamped working copies of the inputs ------------------------------------
int    g_embedDim, g_neighbors, g_horizon, g_libraryBars, g_atrPeriod;
double g_thrSigma, g_minAgree, g_atrSlMult, g_atrTpMult, g_lots;
long   g_magic;

//--- indicator handles -------------------------------------------------------
int g_atr = INVALID_HANDLE;

//--- monotonic bar bookkeeping (mirrors _barIndex / _entryBarIndex) ----------
long g_barIndex     = 0;
long g_entryBarIndex = LONG_MIN;

//+------------------------------------------------------------------+
//| Init                                                             |
//+------------------------------------------------------------------+
int OnInit()
{
   g_embedDim    = EmbedDim;
   g_neighbors   = Neighbors;
   g_horizon     = Horizon;
   g_thrSigma    = ThresholdSigma;
   g_minAgree    = MinAgreement;
   g_libraryBars = LibraryBars;
   g_atrPeriod   = AtrPeriod;
   g_atrSlMult   = AtrSlMult;
   g_atrTpMult   = AtrTpMult;
   g_lots        = Lots;
   g_magic       = Magic;

   // Guard against pathological inputs so the phase-space search stays bounded.
   if(g_embedDim < 2)  g_embedDim = 2;
   if(g_embedDim > 32) g_embedDim = 32;
   if(g_neighbors < 3) g_neighbors = 3;
   if(g_horizon < 1)   g_horizon = 1;

   g_barIndex      = 0;
   g_entryBarIndex = LONG_MIN;

   trade.SetExpertMagicNumber(g_magic);

   g_atr = iATR(_Symbol, _Period, g_atrPeriod);
   if(g_atr == INVALID_HANDLE)
   {
      Print("PhaseSpaceAnalogProjection: failed to create ATR handle");
      return INIT_FAILED;
   }
   return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| Deinit                                                           |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   if(g_atr != INVALID_HANDLE) IndicatorRelease(g_atr);
}

//+------------------------------------------------------------------+
//| New-bar gate                                                     |
//+------------------------------------------------------------------+
bool IsNewBar()
{
   static datetime last = 0;
   datetime cur = iTime(_Symbol, _Period, 0);
   if(cur != last){ last = cur; return true; }
   return false;
}

//+------------------------------------------------------------------+
//| Tick -- act once per newly closed bar                            |
//+------------------------------------------------------------------+
void OnTick()
{
   // Need at least two bars (a forming bar + one closed bar).
   if(Bars(_Symbol, _Period) < 2) return;
   if(!IsNewBar()) return;

   // A new closed bar has appeared: advance the monotonic counter.
   g_barIndex++;

   // Size of the analog library window (mirror of the C# rolling cap).
   int cap     = g_libraryBars + g_horizon + g_atrPeriod + 8;
   int minBars = g_embedDim + g_horizon + g_atrPeriod + 5;

   int avail = Bars(_Symbol, _Period) - 1;   // closed bars available (exclude forming bar 0)
   int n = (avail < cap) ? avail : cap;
   if(n < minBars) return;

   // Build the analog library, newest-last:  close[n-1] = last CLOSED bar (shift 1),
   // close[0] = oldest bar in the window (shift n).  Matches the C# span indexing.
   double close[];
   ArrayResize(close, n);
   for(int j = 0; j < n; j++)
      close[j] = iClose(_Symbol, _Period, n - j);

   // ATR of the last closed bar.
   double atrbuf[];
   ArraySetAsSeries(atrbuf, true);
   if(CopyBuffer(g_atr, 0, 1, 1, atrbuf) < 1) return;
   double atr = atrbuf[0];
   if(atr <= 0) return;

   // ---- delay-embedded analog projection ----
   int    desired  = 0;
   double forecast = 0.0, agreement = 0.0;
   int    used     = 0;
   if(Forecast(close, n, forecast, agreement, used))
   {
      double sigma1 = ReturnStdev(close, n);
      double thr    = g_thrSigma * sigma1 * MathSqrt((double)g_horizon);
      if(agreement >= g_minAgree)
      {
         if(forecast >  thr) desired =  1;
         else if(forecast < -thr) desired = -1;
      }
   }

   // ---- manage an open position ----
   ulong posTicket = 0;
   int   held      = 0;   // +1 long, -1 short
   if(FindPosition(posTicket, held))
   {
      if(desired != 0 && desired != held)
         trade.PositionClose(posTicket);                 // projection now points the other way
      else if(g_barIndex - g_entryBarIndex >= g_horizon)
         trade.PositionClose(posTicket);                 // forecast horizon (shelf life) elapsed
      return;                                            // otherwise let the ATR stop / target work
   }

   // ---- flat: open on a confirmed projection ----
   if(desired == 0) return;
   Enter(desired > 0, atr, forecast);
   g_entryBarIndex = g_barIndex;
}

//+------------------------------------------------------------------+
//| Delay-coordinate nearest-neighbour forecast.                     |
//| Returns false when the phase space is too sparse or the analogs  |
//| are too incoherent to project from.                              |
//| close[] is newest-last, length n.                                |
//+------------------------------------------------------------------+
bool Forecast(const double &close[], int n, double &forecast, double &agreement, int &used)
{
   forecast = 0.0; agreement = 0.0; used = 0;
   int m = g_embedDim, h = g_horizon;

   // Log-return series R[i] = ln(C[i]/C[i-1]) for i in [1, n-1]; R[0] unused.
   double R[];
   ArrayResize(R, n);
   R[0] = 0.0;
   for(int i = 1; i < n; i++)
      R[i] = MathLog(close[i] / close[i - 1]);

   // Current state vector s* :  cur[k] = R[n-1-k]  (needs n-1-(m-1) >= 1).
   if(n - m < 1) return(false);
   double cur[];
   ArrayResize(cur, m);
   for(int k = 0; k < m; k++)
      cur[k] = R[n - 1 - k];

   // Candidate analog anchors a in [m, n-1-h] : state defined AND future known.
   int lo = m, hi = n - 1 - h;
   int count = hi - lo + 1;
   if(count < g_neighbors) return(false);

   double dist[];    // squared distances (monotonic; sqrt applied to selected only)
   int    anchor[];
   ArrayResize(dist, count);
   ArrayResize(anchor, count);
   for(int idx = 0; idx < count; idx++)
   {
      int a = lo + idx;
      double s = 0.0;
      for(int k = 0; k < m; k++)
      {
         double d = R[a - k] - cur[k];
         s += d * d;
      }
      dist[idx]   = s;
      anchor[idx] = a;
   }

   // Rank candidates by ascending distance.
   int order[];
   ArrayResize(order, count);
   for(int i = 0; i < count; i++) order[i] = i;
   SortIndicesByDist(order, dist, 0, count - 1);

   // Greedily select K analogs, enforcing a Theiler window so neighbours are
   // temporally independent episodes rather than one overlapping run.
   int theiler = (h > m) ? h : m;
   int K = g_neighbors;
   int    selAnchor[];
   double selDist[];
   ArrayResize(selAnchor, K);
   ArrayResize(selDist, K);
   int sel = 0;
   for(int oi = 0; oi < count && sel < K; oi++)
   {
      int a = anchor[order[oi]];
      bool tooClose = false;
      for(int s2 = 0; s2 < sel; s2++)
         if(MathAbs(a - selAnchor[s2]) < theiler){ tooClose = true; break; }
      if(tooClose) continue;
      selAnchor[sel] = a;
      selDist[sel]   = MathSqrt(dist[order[oi]]);
      sel++;
   }

   int minNeeded = (3 > K / 2) ? 3 : K / 2;
   if(sel < minNeeded) return(false);

   // Exponential distance weighting scaled by the mean neighbour distance.
   double d0 = 0.0;
   for(int i = 0; i < sel; i++) d0 += selDist[i];
   d0 /= sel;
   if(d0 < 1e-12) d0 = 1e-12;

   double wsum = 0.0, fsum = 0.0;
   double fwd[];
   ArrayResize(fwd, sel);
   for(int i = 0; i < sel; i++)
   {
      int a = selAnchor[i];
      fwd[i] = MathLog(close[a + h] / close[a]);          // realised future of the analog
      double w = MathExp(-selDist[i] / d0);
      wsum += w;
      fsum += w * fwd[i];
   }
   if(wsum < 1e-18) return(false);
   forecast = fsum / wsum;

   int fsign = (forecast > 0.0) ? 1 : ((forecast < 0.0) ? -1 : 0);
   if(fsign == 0) return(false);

   int agree = 0;
   for(int i = 0; i < sel; i++)
   {
      int fs = (fwd[i] > 0.0) ? 1 : ((fwd[i] < 0.0) ? -1 : 0);
      if(fs == fsign) agree++;
   }
   agreement = (double)agree / sel;
   used = sel;
   return(true);
}

//+------------------------------------------------------------------+
//| Standard deviation of one-bar log returns across the library.    |
//+------------------------------------------------------------------+
double ReturnStdev(const double &close[], int n)
{
   if(n < 3) return(0.0);
   int cnt = n - 1;
   double mean = 0.0;
   for(int i = 1; i < n; i++) mean += MathLog(close[i] / close[i - 1]);
   mean /= cnt;
   double var = 0.0;
   for(int i = 1; i < n; i++)
   {
      double d = MathLog(close[i] / close[i - 1]) - mean;
      var += d * d;
   }
   var /= cnt;
   return(MathSqrt(var));
}

//+------------------------------------------------------------------+
//| Index quicksort: order[] ascending by dist[order[]].             |
//+------------------------------------------------------------------+
void SortIndicesByDist(int &order[], const double &dist[], int left, int right)
{
   int i = left, j = right;
   double pivot = dist[order[(left + right) / 2]];
   while(i <= j)
   {
      while(dist[order[i]] < pivot) i++;
      while(dist[order[j]] > pivot) j--;
      if(i <= j)
      {
         int tmp = order[i]; order[i] = order[j]; order[j] = tmp;
         i++; j--;
      }
   }
   if(left < j)  SortIndicesByDist(order, dist, left, j);
   if(i < right) SortIndicesByDist(order, dist, i, right);
}

//+------------------------------------------------------------------+
//| Locate this EA's open position; returns side via 'held'.         |
//+------------------------------------------------------------------+
bool FindPosition(ulong &ticket, int &held)
{
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong t = PositionGetTicket(i);
      if(PositionSelectByTicket(t) &&
         PositionGetString(POSITION_SYMBOL) == _Symbol &&
         PositionGetInteger(POSITION_MAGIC) == g_magic)
      {
         ticket = t;
         held = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) ? 1 : -1;
         return(true);
      }
   }
   return(false);
}

//+------------------------------------------------------------------+
//| Open a trade with ATR-based stop and target.                     |
//+------------------------------------------------------------------+
void Enter(bool isLong, double atr, double forecast)
{
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double price = isLong ? ask : bid;
   double sl = isLong ? price - g_atrSlMult * atr : price + g_atrSlMult * atr;
   double tp = isLong ? price + g_atrTpMult * atr : price - g_atrTpMult * atr;

   sl = NormalizeDouble(sl, _Digits);
   tp = NormalizeDouble(tp, _Digits);

   bool ok;
   if(isLong) ok = trade.Buy(g_lots, _Symbol, 0.0, sl, tp, "PSAP-long");
   else       ok = trade.Sell(g_lots, _Symbol, 0.0, sl, tp, "PSAP-short");

   PrintFormat("PhaseSpaceAnalogProjection %s F=%.5f @ %.5f -> %s (ret=%d)",
               (isLong ? "LONG" : "SHORT"), forecast, price,
               (ok ? "ok" : "fail"), (int)trade.ResultRetcode());
}
//+------------------------------------------------------------------+
