//+------------------------------------------------------------------+
//| TickParityDiagnostic_V5.mq5                                      |
//| Minute-by-minute OnTick vs CopyTicksRange parity, local / Cloud  |
//| Based on V4 hashing (FNV-1a 32-bit, timestamp/bid/ask).          |
//| Diagnostic only: no trades, files, DLLs or external dependencies. |
//+------------------------------------------------------------------+
#property strict
#property version "5.00"
#property description "Per-minute OnTick vs CopyTicksRange counts and hashes; no trades or files."

input datetime DiagnosticHour = D'2026.06.22 10:00:00'; // Broker/server time, HH:00:00
input int DiagnosticVariant = 1; // Optimize 1..240, step 1, Custom max
input int CacheBuster = 0; // Change for each fresh optimization (prevents cached reuse)

// Four variants per minute (minute index 0..59):
//  4*m+1: OnTick count; 4*m+2: OnTick combined hash;
//  4*m+3: CopyTicksRange count; 4*m+4: CopyTicksRange combined hash.
// Example: 10:00 -> 1..4; 10:01 -> 5..8; 10:59 -> 237..240.
// Hash: FNV-1a 32-bit over 8-byte little-endian integers in the order
// (time_msc, round(bid/_Point), round(ask/_Point)) for every tick.

const uint HASH_SEED=2166136261;
ulong g_on_count[60];
uint  g_on_hash[60];
long  g_hour_start_ms=0;

uint HashByte(uint h,uchar b)
  {
   h^=(uint)b;
   h*=16777619;
   return h;
  }
uint HashLong(uint h,long x)
  {
   ulong u=(ulong)x;
   for(int i=0;i<8;i++) h=HashByte(h,(uchar)((u>>(8*i))&0xFF));
   return h;
  }
long PricePts(double p) { return (long)MathRound(p/_Point); }
uint HashTick(uint h,const MqlTick &t)
  {
   h=HashLong(h,(long)t.time_msc);
   h=HashLong(h,PricePts(t.bid));
   h=HashLong(h,PricePts(t.ask));
   return h;
  }

int OnInit()
  {
   MqlDateTime dt;
   if(!TimeToStruct(DiagnosticHour,dt) || dt.min!=0 || dt.sec!=0 ||
      DiagnosticVariant<1 || DiagnosticVariant>240)
     {
      Print("[V5/ERROR] DiagnosticHour must be HH:00:00; DiagnosticVariant must be 1..240");
      return INIT_PARAMETERS_INCORRECT;
     }
   g_hour_start_ms=(long)DiagnosticHour*1000;
   for(int m=0;m<60;m++)
     {
      g_on_count[m]=0;
      g_on_hash[m]=HASH_SEED;
     }
   return INIT_SUCCEEDED;
  }

void OnTick()
  {
   MqlTick t;
   if(!SymbolInfoTick(_Symbol,t)) return;
   long offset=(long)t.time_msc-g_hour_start_ms;
   if(offset<0 || offset>=3600000) return;
   int m=(int)(offset/60000);
   g_on_count[m]++;
   g_on_hash[m]=HashTick(g_on_hash[m],t);
  }

bool CopyOneMinute(int m,ulong &count,uint &hash)
  {
   count=0;
   hash=HASH_SEED;
   ulong lo=(ulong)(g_hour_start_ms+(long)m*60000);
   ulong hi=lo+59999; // Inclusive end; avoids double counting boundaries
   MqlTick ticks[];
   ResetLastError();
   int n=CopyTicksRange(_Symbol,ticks,COPY_TICKS_ALL,lo,hi);
   if(n<0)
     {
      PrintFormat("[V5/COPY_ERROR] minute=%d error=%d",m,GetLastError());
      return false;
     }
   for(int i=0;i<n;i++)
     {
      count++;
      hash=HashTick(hash,ticks[i]);
     }
   return true;
  }

// Important: each optimization pass processes the full test interval.
// Custom max returns one scalar for the selected minute/metric.
// Use COMPLETE optimization, NOT genetic optimization.
double OnTester()
  {
   int zero=DiagnosticVariant-1;
   int m=zero/4;
   int metric=zero%4;
   if(metric==0) return (double)g_on_count[m];
   if(metric==1) return (double)g_on_hash[m];
   ulong count=0;
   uint hash=HASH_SEED;
   if(!CopyOneMinute(m,count,hash)) return -1.0;
   if(metric==2) return (double)count;
   return (double)hash;
  }
