| //+------------------------------------------------------------------+
|
| //| TkzSelfTest.mqh |
|
| //| Self-test of the TKZ codec: synthetic edge cases, round trips |
|
| //| of real ticks and archive file handling. |
|
| //+------------------------------------------------------------------+
|
| #ifndef TKZ_SELFTEST_MQH
|
| #define TKZ_SELFTEST_MQH
|
|
|
| #include "TkzArchive.mqh"
|
|
|
| // Reference output of the C++ build of this exact code for the golden data
|
| // set below. A different value means the compiled codec does not produce the
|
| // reference byte stream (files would not be portable).
|
| #define TKZ_GOLDEN_SIZE 117563
|
| #define TKZ_GOLDEN_HASH 3252087341
|
|
|
| class CTkzSelfTest
|
| {
|
| private:
|
| CTkzCodec m_enc;
|
| CTkzCodec m_dec;
|
| ulong m_seed;
|
| int m_pass;
|
| int m_fail;
|
| int m_n;
|
| long m_t[];
|
| double m_b[];
|
| double m_a[];
|
| long m_ot[];
|
| double m_ob[];
|
| double m_oa[];
|
| uchar m_buf[];
|
|
|
| ulong Rnd(void)
|
| {
|
| m_seed^=m_seed<<13;
|
| m_seed^=m_seed>>7;
|
| m_seed^=m_seed<<17;
|
| return m_seed;
|
| }
|
| long RndInt(const long range) { return (long)(Rnd()%(ulong)range); }
|
| double FromBits(const long v)
|
| {
|
| UTkzBits u;
|
| u.l=v;
|
| return u.d;
|
| }
|
| long ToBits(const double v)
|
| {
|
| UTkzBits u;
|
| u.d=v;
|
| return u.l;
|
| }
|
| void Clear(void) { m_n=0; }
|
| void Push(const long t,const double b,const double a)
|
| {
|
| if(m_n>=ArraySize(m_t))
|
| {
|
| int cap=m_n*2+1024;
|
| ArrayResize(m_t,cap);
|
| ArrayResize(m_b,cap);
|
| ArrayResize(m_a,cap);
|
| }
|
| m_t[m_n]=t;
|
| m_b[m_n]=b;
|
| m_a[m_n]=a;
|
| m_n++;
|
| }
|
| void Result(const bool ok,const string name,const string details)
|
| {
|
| if(ok)
|
| m_pass++;
|
| else
|
| m_fail++;
|
| PrintFormat("TKZ self-test %s: %s: %s",ok ? "PASS" : "FAIL",name,details);
|
| }
|
| double Rate(const int n,const ulong us)
|
| {
|
| if(us==0)
|
| return 0.0;
|
| return (double)n*1e6/(double)us;
|
| }
|
|
|
| public:
|
| CTkzSelfTest(void)
|
| {
|
| m_seed=88172645463325252;
|
| m_pass=0;
|
| m_fail=0;
|
| m_n=0;
|
| }
|
| int Passed(void) { return m_pass; }
|
| int Failed(void) { return m_fail; }
|
|
|
| //--- encode, decode and compare bitwise; returns the chunk size or -1
|
| int RoundTrip(const long &t[],const double &b[],const double &a[],const int start,const int n,const string name)
|
| {
|
| ArrayResize(m_buf,0);
|
| ulong t0=GetMicrosecondCount();
|
| int sz=m_enc.Encode(t,b,a,start,n,m_buf);
|
| ulong t1=GetMicrosecondCount();
|
| if(sz<0)
|
| {
|
| Result(false,name,"encode error: "+m_enc.LastError());
|
| return -1;
|
| }
|
| int dn=m_dec.Decode(m_buf,0,m_ot,m_ob,m_oa);
|
| ulong t2=GetMicrosecondCount();
|
| if(dn!=n)
|
| {
|
| Result(false,name,"decode error: "+m_dec.LastError());
|
| return -1;
|
| }
|
| for(int i=0; i<n; i++)
|
| if(m_ot[i]!=t[start+i] || ToBits(m_ob[i])!=ToBits(b[start+i]) || ToBits(m_oa[i])!=ToBits(a[start+i]))
|
| {
|
| Result(false,name,"mismatch at tick "+IntegerToString(i));
|
| return -1;
|
| }
|
| Result(true,name,StringFormat("%d ticks, %d bytes, %.4f bytes/tick, encode %.0f ticks/s, decode %.0f ticks/s",
|
| n,sz,n>0 ? (double)sz/(double)n : 0.0,Rate(n,t1-t0),Rate(n,t2-t1)));
|
| return sz;
|
| }
|
|
|
| //--- FNV-1a of a byte range
|
| uint Fnv(const uchar &data[],const int n)
|
| {
|
| uint h=(uint)2166136261;
|
| for(int i=0; i<n; i++)
|
| h=(uint)((h^data[i])*(uint)16777619);
|
| return h;
|
| }
|
|
|
| //--- byte-exact comparison with the reference implementation: three chunks
|
| //--- that exercise negative operands of /, % and >>, the decimal sub-grid,
|
| //--- escapes, crossed quotes, a time step with negative steps and a price step
|
| void RunGolden(void)
|
| {
|
| ulong saved=m_seed;
|
| m_seed=0x2545F4914F6CDD1D;
|
| ArrayResize(m_buf,0);
|
| int i;
|
| //--- 1. forex-like stream
|
| Clear();
|
| long t=1700000000000;
|
| long p=108000;
|
| long sp=2;
|
| for(i=0; i<50000; i++)
|
| {
|
| long r=RndInt(1000);
|
| t+=(r<100) ? (long)0 : 1+RndInt(2000);
|
| if(r%3==0)
|
| p+=RndInt(5)-2;
|
| if(r%7==0)
|
| sp=1+RndInt(4);
|
| Push(t,(double)p/1e5,(double)(p+sp)/1e5);
|
| }
|
| int ok1=m_enc.Encode(m_t,m_b,m_a,0,m_n,m_buf);
|
| //--- 2. negative prices on a decimal sub-grid, crossed quotes, escapes, time going back
|
| Clear();
|
| t=1700000000000;
|
| long q=-5000;
|
| for(i=0; i<20000; i++)
|
| {
|
| long r=RndInt(1000);
|
| t+=(r<50) ? -RndInt(300) : RndInt(700);
|
| if(r%2==0)
|
| q+=(RndInt(5)-2)*10;
|
| long qa=q+((r%13==0) ? -20 : 30);
|
| long rb=(r%17==0) ? 8 : 5;
|
| double b=(double)(q+rb)/1e3;
|
| double a=(double)(qa+5)/1e3;
|
| if(i%500==7)
|
| b=FromBits((long)Rnd());
|
| Push(t,b,a);
|
| }
|
| int ok2=m_enc.Encode(m_t,m_b,m_a,0,m_n,m_buf);
|
| //--- 3. 250 ms time step with negative steps, tick size of 25 points
|
| Clear();
|
| t=1700000000000;
|
| long z=400000;
|
| for(i=0; i<10000; i++)
|
| {
|
| long r=RndInt(100);
|
| t+=250*((r<5) ? -RndInt(8) : RndInt(12));
|
| z+=25*(RndInt(7)-3);
|
| Push(t,(double)z/100.0,(double)(z+50)/100.0);
|
| }
|
| int ok3=m_enc.Encode(m_t,m_b,m_a,0,m_n,m_buf);
|
| m_seed=saved;
|
| int sz=ArraySize(m_buf);
|
| uint h=Fnv(m_buf,sz);
|
| bool ok=(ok1>0 && ok2>0 && ok3>0 && sz==TKZ_GOLDEN_SIZE && h==(uint)TKZ_GOLDEN_HASH);
|
| //--- the chunks must have taken the intended paths
|
| STkzChunkInfo i1,i2,i3;
|
| if(ok)
|
| ok=m_enc.ReadInfo(m_buf,0,i1) && m_enc.ReadInfo(m_buf,ok1,i2) && m_enc.ReadInfo(m_buf,ok1+ok2,i3);
|
| if(ok)
|
| ok=(i1.modulus==1 && i2.modulus==10 && i2.flags==1 && i3.time_step==250 && i3.price_step==25);
|
| Result(ok,"golden output",StringFormat("size %d hash %u (reference %d / %u)",sz,h,TKZ_GOLDEN_SIZE,(uint)TKZ_GOLDEN_HASH));
|
| }
|
|
|
| //--- synthetic edge cases
|
| void RunSynthetic(void)
|
| {
|
| int i;
|
| double nan=FromBits(0x7FF8000000000000);
|
| double inf=FromBits(0x7FF0000000000000);
|
| double negzero=FromBits(((long)1)<<63);
|
| //--- empty and single tick
|
| Clear();
|
| RoundTrip(m_t,m_b,m_a,0,0,"empty chunk");
|
| Clear();
|
| Push(1700000000123,1.23456,1.23467);
|
| RoundTrip(m_t,m_b,m_a,0,m_n,"single tick");
|
| //--- constant prices and times
|
| Clear();
|
| for(i=0; i<1000; i++)
|
| Push(1700000000000,1.1,1.1);
|
| RoundTrip(m_t,m_b,m_a,0,m_n,"constant");
|
| //--- forex-like random walk, 5 digits
|
| Clear();
|
| long t=1700000000000;
|
| long p=108000;
|
| for(i=0; i<100000; i++)
|
| {
|
| t+=RndInt(3000);
|
| p+=RndInt(7)-3;
|
| long sp=1+RndInt(4);
|
| Push(t,(double)p/1e5,(double)(p+sp)/1e5);
|
| }
|
| RoundTrip(m_t,m_b,m_a,0,m_n,"random walk");
|
| //--- time anomalies: negative steps, huge gaps, duplicates, extreme values
|
| Clear();
|
| t=1700000000000;
|
| for(i=0; i<30000; i++)
|
| {
|
| long r=RndInt(100);
|
| if(r==0)
|
| t-=RndInt(100000);
|
| else
|
| if(r==1)
|
| t+=RndInt(4000000000000);
|
| else
|
| if(r>=30)
|
| t+=RndInt(500);
|
| Push(t,2000.0+(double)RndInt(1000)/100.0,2001.0+(double)RndInt(1000)/100.0);
|
| }
|
| Push(-5000000000000,1.0,2.0);
|
| Push(9000000000000000000,1.0,2.0);
|
| Push(-9000000000000000000,1.0,2.0);
|
| RoundTrip(m_t,m_b,m_a,0,m_n,"time anomalies");
|
| //--- irregular doubles: NaN, Inf, -0.0, denormals, non-normalized values
|
| Clear();
|
| t=1700000000000;
|
| for(i=0; i<20000; i++)
|
| {
|
| t+=100;
|
| double b=1.2345+(double)(i%50)*1e-4;
|
| double a=b+0.0002;
|
| int r=i%97;
|
| if(r==3)
|
| b=nan;
|
| if(r==5)
|
| a=inf;
|
| if(r==7)
|
| b=-inf;
|
| if(r==11)
|
| a=negzero;
|
| if(r==13)
|
| b=FromBits(1);
|
| if(r==17)
|
| a=0.1+0.2;
|
| if(r==19)
|
| b=1e300;
|
| if(r==23)
|
| a=-1e300;
|
| if(r==29)
|
| b=FromBits(ToBits(1.2345)+1);
|
| Push(t,b,a);
|
| }
|
| RoundTrip(m_t,m_b,m_a,0,m_n,"irregular doubles");
|
| //--- random bit patterns
|
| Clear();
|
| for(i=0; i<3000; i++)
|
| Push(1700000000000+(long)i*7,FromBits((long)Rnd()),FromBits((long)Rnd()));
|
| RoundTrip(m_t,m_b,m_a,0,m_n,"random bit patterns");
|
| //--- 0.25 tick grid with an offset and whole-second timestamps
|
| Clear();
|
| t=1700000000000;
|
| long q=20000;
|
| for(i=0; i<50000; i++)
|
| {
|
| t+=1000*(1+RndInt(5));
|
| q+=RndInt(9)-4;
|
| double b=(double)q*0.25+0.01;
|
| Push(t,b,b+0.5);
|
| }
|
| RoundTrip(m_t,m_b,m_a,0,m_n,"0.25 grid, 1 s timestamps");
|
| //--- negative prices and zero ask
|
| Clear();
|
| t=1700000000000;
|
| for(i=0; i<30000; i++)
|
| {
|
| t+=RndInt(200);
|
| double b=-37.63+(double)(RndInt(2000)-1000)/100.0;
|
| double a=(i%10==0) ? 0.0 : b+0.03;
|
| Push(t,b,a);
|
| }
|
| RoundTrip(m_t,m_b,m_a,0,m_n,"negative prices");
|
| //--- 12 decimal digits and large values
|
| Clear();
|
| for(i=0; i<5000; i++)
|
| Push(1700000000000+i,0.000012345678+(double)i*1e-12,0.000012345699+(double)i*1e-12);
|
| RoundTrip(m_t,m_b,m_a,0,m_n,"12 digits");
|
| Clear();
|
| for(i=0; i<5000; i++)
|
| Push(1700000000000+i,3999999999.5+(double)i,3999999999.5+2.0*(double)i);
|
| RoundTrip(m_t,m_b,m_a,0,m_n,"large values");
|
| //--- corruption must be detected, never crash
|
| Clear();
|
| t=1700000000000;
|
| for(i=0; i<20000; i++)
|
| {
|
| t+=RndInt(300);
|
| Push(t,1.1+(double)RndInt(100)/1e5,1.1002+(double)RndInt(100)/1e5);
|
| }
|
| ArrayResize(m_buf,0);
|
| int sz=m_enc.Encode(m_t,m_b,m_a,0,m_n,m_buf);
|
| uchar damaged[];
|
| int detected=0;
|
| int trials=100;
|
| for(int k=0; k<trials && sz>TKZ_HEADER_SIZE; k++)
|
| {
|
| ArrayResize(damaged,0);
|
| ArrayCopy(damaged,m_buf,0,0,sz);
|
| int pos=TKZ_HEADER_SIZE+(int)RndInt(sz-TKZ_HEADER_SIZE);
|
| damaged[pos]=(uchar)(damaged[pos]^(uchar)(1+RndInt(255)));
|
| if(m_dec.Decode(damaged,0,m_ot,m_ob,m_oa)<0)
|
| detected++;
|
| }
|
| Result(detected>=trials-2,"corruption detection",StringFormat("%d of %d corrupted chunks rejected",detected,trials));
|
| }
|
|
|
| //--- round trip of real ticks, as one chunk and as several chunks
|
| void RunTicks(const MqlTick &ticks[],const int n,const string name)
|
| {
|
| Clear();
|
| for(int i=0; i<n; i++)
|
| Push(ticks[i].time_msc,ticks[i].bid,ticks[i].ask);
|
| RoundTrip(m_t,m_b,m_a,0,m_n,name+" (one chunk)");
|
| int parts=8;
|
| int step=n/parts+1;
|
| long total=0;
|
| bool ok=true;
|
| for(int s=0; s<n; s+=step)
|
| {
|
| int c=(n-s<step) ? n-s : step;
|
| ArrayResize(m_buf,0);
|
| int sz=m_enc.Encode(m_t,m_b,m_a,s,c,m_buf);
|
| if(sz<0 || m_dec.Decode(m_buf,0,m_ot,m_ob,m_oa)!=c)
|
| {
|
| ok=false;
|
| break;
|
| }
|
| for(int i=0; i<c; i++)
|
| if(m_ot[i]!=m_t[s+i] || ToBits(m_ob[i])!=ToBits(m_b[s+i]) || ToBits(m_oa[i])!=ToBits(m_a[s+i]))
|
| {
|
| ok=false;
|
| break;
|
| }
|
| total+=sz;
|
| }
|
| Result(ok,name+" (8 chunks)",StringFormat("%I64d bytes, %.4f bytes/tick",total,n>0 ? (double)total/(double)n : 0.0));
|
| }
|
|
|
| //--- archive writer/reader with a temporary file
|
| void RunArchive(void)
|
| {
|
| string file="tkz_selftest.tkz";
|
| Clear();
|
| long t=1700000000000;
|
| long p=150000;
|
| for(int i=0; i<60000; i++)
|
| {
|
| t+=1+RndInt(800);
|
| p+=RndInt(5)-2;
|
| Push(t,(double)p/1e3,(double)(p+20+RndInt(3))/1e3);
|
| }
|
| CTkzWriter w;
|
| w.SetVerify(true);
|
| bool ok=w.Open(file,"SELFTEST",3,false);
|
| for(int s=0; ok && s<40000; s+=10000)
|
| ok=w.Write(m_t,m_b,m_a,s,10000);
|
| w.Close();
|
| CTkzWriter w2;
|
| if(ok)
|
| ok=w2.Open(file,"IGNORED",0,false,true);
|
| if(ok)
|
| ok=w2.Write(m_t,m_b,m_a,40000,20000);
|
| w2.Close();
|
| if(!ok)
|
| {
|
| Result(false,"archive","write failed: "+w.LastError()+" "+w2.LastError());
|
| return;
|
| }
|
| CTkzReader r;
|
| if(!r.Open(file,false))
|
| {
|
| Result(false,"archive","open failed: "+r.LastError());
|
| return;
|
| }
|
| MqlTick all[];
|
| int got=r.ReadAll(all);
|
| ok=(got==m_n && r.ChunkCount()==5 && r.ArchiveSymbol()=="SELFTEST" && r.ArchiveDigits()==3);
|
| for(int i=0; ok && i<got; i++)
|
| if(all[i].time_msc!=m_t[i] || ToBits(all[i].bid)!=ToBits(m_b[i]) || ToBits(all[i].ask)!=ToBits(m_a[i]))
|
| ok=false;
|
| long from=m_t[15000];
|
| long to=m_t[45000];
|
| MqlTick part[];
|
| int pn=r.ReadRange(from,to,part);
|
| int expect=0;
|
| for(int i=0; i<m_n; i++)
|
| if(m_t[i]>=from && m_t[i]<to)
|
| expect++;
|
| ok=ok && (pn==expect);
|
| r.Close();
|
| FileDelete(file);
|
| Result(ok,"archive",StringFormat("%d chunks, %d ticks, range query %d ticks",5,got,pn));
|
| }
|
| };
|
|
|
| #endif // TKZ_SELFTEST_MQH
|
| //+------------------------------------------------------------------+
|