//+------------------------------------------------------------------+
//| VerifySessionRisk.mq5 - reproduce SPSS probabilities             |
//+------------------------------------------------------------------+
#property strict
#property script_show_inputs
//+------------------------------------------------------------------+
//| Shared calculation dependency                                    |
//+------------------------------------------------------------------+
#include <SessionRisk/SessionRiskCore.mqh>     	// Shared session arithmetic.

//+------------------------------------------------------------------+
//| Verification input                                               |
//+------------------------------------------------------------------+
input int ExpectedRows=1295;                   	// Set from the audited export for another snapshot.

//+------------------------------------------------------------------+
//| ReadRow - collect fields from one CSV record                     |
//+------------------------------------------------------------------+
int ReadRow(int file,string &values[])
  {
//--- Collect the fields of the current CSV record.
   ArrayResize(values,0);
   while(!FileIsEnding(file))
     {
      int n=ArraySize(values);
      ArrayResize(values,n+1);
      values[n]=FileReadString(file);
      if(FileIsLineEnding(file))
         break;
     }
   return ArraySize(values);
  }
//+------------------------------------------------------------------+
//| Column - find a case-sensitive header name                       |
//+------------------------------------------------------------------+
int Column(string &header[],string name)
  {
   for(int j=0;j<ArraySize(header);j++)
      if(header[j]==name)
         return j;
   return -1;
  }

//+------------------------------------------------------------------+
//| StrictNumber - validate and convert a complete numeric token     |
//+------------------------------------------------------------------+
bool StrictNumber(string s,double &value)
  {
//--- Validate the complete numeric token before conversion.
   int n=StringLen(s),i=0,digits=0;
   if(n==0)
      return false;
   if(StringGetCharacter(s,i)=='+' || StringGetCharacter(s,i)=='-')
      i++;
   while(i<n && StringGetCharacter(s,i)>='0' && StringGetCharacter(s,i)<='9')
     {
      i++;
      digits++;
     }
//--- Accept an optional decimal part.
   if(i<n && StringGetCharacter(s,i)=='.')
     {
      i++;
      while(i<n && StringGetCharacter(s,i)>='0' && StringGetCharacter(s,i)<='9')
        {
         i++;
         digits++;
        }
     }
   if(digits==0)
      return false;
//--- Accept an optional exponent only when it contains digits.
   if(i<n && (StringGetCharacter(s,i)=='e' || StringGetCharacter(s,i)=='E'))
     {
      i++;
      int exponentDigits=0;
      if(i<n && (StringGetCharacter(s,i)=='+' || StringGetCharacter(s,i)=='-'))
         i++;
      while(i<n && StringGetCharacter(s,i)>='0' && StringGetCharacter(s,i)<='9')
        {
         i++;
         exponentDigits++;
        }
      if(exponentDigits==0)
         return false;
     }
   if(i!=n)
      return false;
//--- Convert only after the entire token has passed syntax checks.
   value=StringToDouble(s);
   return MathIsValidNumber(value);
  }
//+------------------------------------------------------------------+
//| UniqueHeader - reject empty or duplicate header names            |
//+------------------------------------------------------------------+
bool UniqueHeader(string &header[])
  {
//--- Reject empty names and case-sensitive duplicates.
   for(int i=0;i<ArraySize(header);i++)
     {
      if(header[i]=="")
         return false;
      for(int j=0;j<i;j++)
         if(header[j]==header[i])
            return false;
     }
   return true;
  }

//+------------------------------------------------------------------+
//| OnStart - script entry point                                     |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- Create a fresh INCOMPLETE receipt for this comparison.
   int receipt=FileOpen("SessionRisk\\results\\native_parity.txt",FILE_WRITE|FILE_TXT|FILE_ANSI);
   if(receipt==INVALID_HANDLE)
     {
      Print("FAIL: cannot create parity receipt");
      return;
     }
   ResetLastError();
   bool receiptWritten=FileWrite(receipt,"INCOMPLETE: verification started; no success is established by this run yet.")>0;
   FileFlush(receipt);
   bool receiptOk=receiptWritten && GetLastError()==0;
   FileClose(receipt);
   if(!receiptOk)
     {Print("FAIL: cannot write initial parity receipt; no PASS established");return;}
//--- Invalidate the optional receipt and output before the main comparison.
   int optional=FileOpen("SessionRisk\\results\\forecast_only_status.txt",FILE_WRITE|FILE_TXT|FILE_ANSI);
   if(optional==INVALID_HANDLE)
     {Print("FAIL: cannot create forecast-only status");return;}
   ResetLastError();
   bool optionalWritten=FileWrite(optional,"INCOMPLETE: forecast-only scoring has not completed in this run.")>0;
   FileFlush(optional);
   bool optionalOk=optionalWritten && GetLastError()==0;
   FileClose(optional);
   if(!optionalOk)
     {Print("FAIL: cannot write initial forecast-only status");return;}
   if(FileIsExist("SessionRisk\\results\\forecast_only_predictions.csv") &&
      !FileDelete("SessionRisk\\results\\forecast_only_predictions.csv"))
     {Print("FAIL: cannot remove previous forecast-only output");return;}
   if(ExpectedRows<=0)
     {
      Print("FAIL: ExpectedRows must be positive");
      return;
     }
//--- Cross-check the configured count against the export manifest.
   int manifest=FileOpen("SessionRisk\\data\\native_manifest.txt",FILE_READ|FILE_TXT|FILE_ANSI);
   if(manifest==INVALID_HANDLE)
     {Print("FAIL: missing export manifest");return;}
   int manifestRows=-1;
   bool manifestOk=true;
   string rowKey="estimation_or_holdout_rows=";
   while(!FileIsEnding(manifest))
     {
      string line=FileReadString(manifest);
      if(StringFind(line,rowKey)==0)
        {
         double value=0;
         if(manifestRows!=-1 || !StrictNumber(StringSubstr(line,StringLen(rowKey)),value) ||
            value<=0 || value>2147483647 || MathFloor(value)!=value)
            manifestOk=false;
         else
            manifestRows=(int)value;
        }
     }
   FileClose(manifest);
   if(!manifestOk || manifestRows!=ExpectedRows)
     {Print("FAIL: ExpectedRows differs from the export manifest");return;}
//--- Open the coefficient, session and SPSS reference inputs.
   int coef=FileOpen("SessionRisk\\results\\model_coefficients.csv",FILE_READ|FILE_CSV|FILE_ANSI,',');
   int data=FileOpen("SessionRisk\\data\\sessions.csv",FILE_READ|FILE_CSV|FILE_ANSI,',');
   int ref=FileOpen("SessionRisk\\results\\spss_predictions.csv",FILE_READ|FILE_CSV|FILE_ANSI,',');
   if(coef==INVALID_HANDLE || data==INVALID_HANDLE || ref==INVALID_HANDLE)
     {
      if(coef!=INVALID_HANDLE) FileClose(coef);
      if(data!=INVALID_HANDLE) FileClose(data);
      if(ref!=INVALID_HANDLE) FileClose(ref);
      Print("FAIL: missing input file");
      return;
     }
   string v[],dh[],rh[],d[],r[];
   if(ReadRow(coef,v)!=2 || v[0]!="term" || v[1]!="coefficient")
     {
      Print("FAIL: coefficient header");
      return;
     }
//--- Read exactly one finite coefficient for each required term.
   double beta[3];
   bool found[3]= {false,false,false};
   while(ReadRow(coef,v)>0)
     {
      if(ArraySize(v)!=2)
        {
         Print("FAIL: coefficient row schema");
         return;
        }
      int k=v[0]=="Constant"?0:(v[0]=="x_range"?1:(v[0]=="x_body"?2:-1));
      if(k<0 || found[k])
        {
         Print("FAIL: coefficient name/duplicate");
         return;
        }
      if(!StrictNumber(v[1],beta[k]))
        {
         Print("FAIL: coefficient numeric token");
         return;
        }
      found[k]=true;
     }
   for(int k=0;k<3;k++)
      if(!found[k] || !MathIsValidNumber(beta[k]))
        {
         Print("FAIL: coefficients");
         return;
        }
//--- Check the session schema and locate the SPSS reference columns.
   ReadRow(data,dh);
   ReadRow(ref,rh);
   string expectedHeader[];
   StringSplit("session_date,year,decision_time,label_end,pre_open,pre_high,pre_low,pre_close,prior20_mean,target_high,target_low,previous60_count,previous60_events,comparison_eligible",',',expectedHeader);
   if(ArraySize(dh)!=14 || !UniqueHeader(rh))
     {
      Print("FAIL: schema/duplicate header");
      return;
     }
   for(int j=0;j<14;j++)
      if(dh[j]!=expectedHeader[j])
        {
         Print("FAIL: data header");
         return;
        }
   int id=Column(rh,"session_date"),pr=Column(rh,"p_logit"),x1ref=Column(rh,"x_range"),x2ref=Column(rh,"x_body");
   if(id<0 || pr<0 || x1ref<0 || x2ref<0 || ArraySize(dh)!=14)
     {
      Print("FAIL: schema");
      return;
     }
//--- Create the output for row-level arithmetic differences.
   int out=FileOpen("SessionRisk\\results\\native_predictions.csv",FILE_WRITE|FILE_CSV|FILE_ANSI,',');
   if(out==INVALID_HANDLE)
     {
      Print("FAIL: output");
      return;
     }
   ResetLastError();
   bool writesOk=FileWrite(out,"session_date","x_range","x_body","p_mql5","p_spss","absolute_difference")>0;
   double maxdiff=0,maxfeature=0;
   int count=0,failures=0;
   string previousId="";
//--- Match rows and reconstruct the features and probabilities.
   while(writesOk && !IsStopped() && ReadRow(data,d)>0)
     {
      if(ArraySize(d)!=14 || ReadRow(ref,r)!=ArraySize(rh) || d[0]!=r[id])
        {
         Print("FAIL: row identity/schema");
         FileClose(out);
         return;
        }
      if(previousId!="" && StringCompare(d[0],previousId)<=0)
        {
         Print("FAIL: duplicate or unordered session id");
         FileClose(out);
         return;
        }
      previousId=d[0];
      double x1=0,x2=0,prices[5],reference=0,referenceX1=0,referenceX2=0;
      for(int j=0;j<5;j++)
         if(!StrictNumber(d[j+4],prices[j]))
           {
            Print("FAIL: raw numeric token");
            FileClose(out);
            return;
           }
      if(!StrictNumber(r[pr],reference) || !StrictNumber(r[x1ref],referenceX1) || !StrictNumber(r[x2ref],referenceX2))
        {Print("FAIL: reference numeric token");FileClose(out);return;}
      if(!SessionFeatures(prices[0],prices[1],prices[2],prices[3],prices[4],x1,x2))
        {Print("FAIL: invalid raw features");FileClose(out);return;}
      double score=beta[0]+beta[1]*x1+beta[2]*x2;
      if(!MathIsValidNumber(score))
        {Print("FAIL: nonfinite model score");FileClose(out);return;}
      double p=RiskSigmoid(score);
      double error=MathAbs(p-reference);
      double ferr=MathMax(MathAbs(x1-referenceX1),MathAbs(x2-referenceX2));
      if(!MathIsValidNumber(reference) || reference<0 || reference>1 || !MathIsValidNumber(p) || error>1e-10 || ferr>1e-10)
         failures++;
      maxdiff=MathMax(maxdiff,error);
      maxfeature=MathMax(maxfeature,ferr);
      count++;
      writesOk=(FileWrite(out,d[0],DoubleToString(x1,16),DoubleToString(x2,16),DoubleToString(p,16),DoubleToString(reference,16),DoubleToString(error,16))>0) && writesOk;
     }
//--- Check the final row count and summarize the comparison.
   FileFlush(out);
   writesOk=writesOk && GetLastError()==0 && !IsStopped();
   if(!writesOk || ReadRow(ref,r)>0 || count!=ExpectedRows)
      failures++;
   FileClose(coef);
   FileClose(data);
   FileClose(ref);
   FileClose(out);
   receipt=FileOpen("SessionRisk\\results\\native_parity.tmp",FILE_WRITE|FILE_TXT|FILE_ANSI);
   if(receipt==INVALID_HANDLE)
     {
      Print("FAIL: cannot finalize parity receipt");
      return;
     }
   string summary=StringFormat("%s: %d rows; %d failures; max probability error %.17g; max feature error %.17g; tolerance 1e-10",failures==0 && count>0?"PASS":"FAIL",count,failures,maxdiff,maxfeature);
   ResetLastError();
   receiptWritten=FileWrite(receipt,summary)>0;
   receiptWritten=(FileWrite(receipt,"terminal_build="+IntegerToString(TerminalInfoInteger(TERMINAL_BUILD)))>0) && receiptWritten;
   FileFlush(receipt);
   receiptOk=receiptWritten && GetLastError()==0;
   FileClose(receipt);
   if(!receiptOk)
     {Print("FAIL: cannot finalize parity receipt; no PASS established");return;}
   if(!FileMove("SessionRisk\\results\\native_parity.tmp",0,"SessionRisk\\results\\native_parity.txt",FILE_REWRITE))
     {Print("FAIL: cannot publish parity receipt; receipt remains INCOMPLETE");return;}
   Print(summary);
//--- Score optional inputs only after the main arithmetic comparison passes.
   if(failures!=0 || count==0)
      return;
   int probe=FileOpen("SessionRisk\\results\\forecast_only.csv",FILE_READ|FILE_CSV|FILE_ANSI,',');
   if(probe==INVALID_HANDLE)
     {
      if(FileIsExist("SessionRisk\\results\\forecast_only.csv"))
        {Print("FAIL: cannot read forecast-only input");return;}
      optional=FileOpen("SessionRisk\\results\\forecast_only_status.tmp",FILE_WRITE|FILE_TXT|FILE_ANSI);
      if(optional==INVALID_HANDLE)
        {Print("FAIL: cannot finalize forecast-only status");return;}
      ResetLastError();
      optionalWritten=FileWrite(optional,"SKIP: no forecast-only input file supplied.")>0;
      FileFlush(optional);
      optionalOk=optionalWritten && GetLastError()==0;
      FileClose(optional);
      if(!optionalOk || !FileMove("SessionRisk\\results\\forecast_only_status.tmp",0,"SessionRisk\\results\\forecast_only_status.txt",FILE_REWRITE))
         Print("FAIL: cannot publish forecast-only status; receipt remains INCOMPLETE");
      return;
     }
//--- Require the exact optional schema and strictly parse each numeric token.
   string forecastHeader[];
   StringSplit("session_date,pre_open,pre_high,pre_low,pre_close,prior20_mean",',',forecastHeader);
   bool forecastOk=ReadRow(probe,v)==6;
   if(forecastOk)
      for(int j=0;j<6;j++)
         if(v[j]!=forecastHeader[j]) forecastOk=false;
   int scored=FileOpen("SessionRisk\\results\\forecast_only_predictions.csv",FILE_WRITE|FILE_CSV|FILE_ANSI,',');
   int forecastCount=0;
   if(scored==INVALID_HANDLE)
      forecastOk=false;
   else
     {
      ResetLastError();
      forecastOk=(FileWrite(scored,"session_date","probability","outcome","evaluated")>0) && forecastOk;
      previousId="";
      while(forecastOk && !IsStopped() && ReadRow(probe,v)>0)
        {
         if(ArraySize(v)!=6 || (previousId!="" && StringCompare(v[0],previousId)<=0) ||
            StringToTime(v[0])<=0 || TimeToString(StringToTime(v[0]),TIME_DATE)!=v[0])
           {forecastOk=false;break;}
         previousId=v[0];
         double prices[5],x1=0,x2=0;
         for(int j=0;j<5;j++)
            if(!StrictNumber(v[j+1],prices[j])) forecastOk=false;
         if(!forecastOk || !SessionFeatures(prices[0],prices[1],prices[2],prices[3],prices[4],x1,x2))
           {forecastOk=false;break;}
         double score=beta[0]+beta[1]*x1+beta[2]*x2;
         if(!MathIsValidNumber(score))
           {forecastOk=false;break;}
         double probability=RiskSigmoid(score);
         if(!MathIsValidNumber(probability) || probability<0 || probability>1)
           {forecastOk=false;break;}
         forecastOk=(FileWrite(scored,v[0],DoubleToString(probability,16),"",0)>0) && forecastOk;
         forecastCount++;
        }
      FileFlush(scored);
      forecastOk=forecastOk && GetLastError()==0 && !IsStopped();
      FileClose(scored);
     }
   FileClose(probe);
//--- The separate receipt certifies optional arithmetic, not H1 closure or performance.
   optional=FileOpen("SessionRisk\\results\\forecast_only_status.tmp",FILE_WRITE|FILE_TXT|FILE_ANSI);
   if(optional==INVALID_HANDLE)
     {Print("FAIL: cannot finalize forecast-only status");return;}
   ResetLastError();
   string forecastSummary=StringFormat("%s: %d forecast-only rows; no target labels; arithmetic only.",forecastOk?"PASS":"FAIL",forecastCount);
   optionalWritten=FileWrite(optional,forecastSummary)>0;
   FileFlush(optional);
   optionalOk=optionalWritten && GetLastError()==0;
   FileClose(optional);
   if(!optionalOk)
     {Print("FAIL: cannot write forecast-only status; no optional PASS established");return;}
   if(!FileMove("SessionRisk\\results\\forecast_only_status.tmp",0,"SessionRisk\\results\\forecast_only_status.txt",FILE_REWRITE))
     {Print("FAIL: cannot publish forecast-only status; receipt remains INCOMPLETE");return;}
   Print(forecastSummary);
  }
//+------------------------------------------------------------------+
