mql5-tick-compression/lib/TkzArchive.mqh
441 lines
13 KiB
MQL5
//+------------------------------------------------------------------+
//| TkzArchive.mqh |
//| .tkz archive files: a 64-byte file header followed by TKZ1 |
//| chunks (see TkzCodec.mqh and docs/FORMAT.md). |
//+------------------------------------------------------------------+
#ifndef TKZ_ARCHIVE_MQH
#define TKZ_ARCHIVE_MQH
#include "TkzCodec.mqh"
#define TKZ_FILE_MAGIC 0x465A4B54 // bytes "TKZF"
#define TKZ_FILE_VERSION 1
#define TKZ_FILE_HEADER_SIZE 64
#define TKZ_SYMBOL_BYTES 32
//+------------------------------------------------------------------+
//| Little-endian helpers shared by writer and reader |
//+------------------------------------------------------------------+
class CTkzBytes
{
public:
void PutU16(uchar &dst[],const int pos,const int v)
{
dst[pos]=(uchar)(v&255);
dst[pos+1]=(uchar)((v>>8)&255);
}
void PutU32(uchar &dst[],const int pos,const uint v)
{
dst[pos]=(uchar)(v&255);
dst[pos+1]=(uchar)((v>>8)&255);
dst[pos+2]=(uchar)((v>>16)&255);
dst[pos+3]=(uchar)((v>>24)&255);
}
int GetU16(const uchar &src[],const int pos)
{
return (int)src[pos]|((int)src[pos+1]<<8);
}
uint GetU32(const uchar &src[],const int pos)
{
return (uint)src[pos]|((uint)src[pos+1]<<8)|((uint)src[pos+2]<<16)|((uint)src[pos+3]<<24);
}
};
//+------------------------------------------------------------------+
//| Writer: creates (or appends to) a .tkz file |
//+------------------------------------------------------------------+
class CTkzWriter
{
private:
int m_h;
CTkzCodec m_codec;
CTkzCodec m_check;
CTkzBytes m_io;
uchar m_buf[];
string m_error;
long m_ticks;
long m_bytes;
bool m_verify;
ulong m_enc_us;
ulong m_ver_us;
long m_vt[];
double m_vb[];
double m_va[];
public:
CTkzWriter(void)
{
m_h=INVALID_HANDLE;
m_error="";
m_ticks=0;
m_bytes=0;
m_verify=false;
m_enc_us=0;
m_ver_us=0;
}
~CTkzWriter(void)
{
Close();
}
string LastError(void) { return m_error; }
long TicksWritten(void) { return m_ticks; }
long BytesWritten(void) { return m_bytes; }
ulong EncodeMicroseconds(void) { return m_enc_us; }
ulong VerifyMicroseconds(void) { return m_ver_us; }
//--- decode every written chunk again and compare it with the input
void SetVerify(const bool verify) { m_verify=verify; }
//--- create a new file (or append chunks to an existing one when append=true)
bool Open(const string filename,const string symbol,const int digits,const bool common,const bool append=false)
{
Close();
m_error="";
m_ticks=0;
m_bytes=0;
int flags=FILE_BIN|FILE_WRITE;
if(common)
flags|=FILE_COMMON;
if(append && FileIsExist(filename,common ? FILE_COMMON : 0))
{
m_h=FileOpen(filename,flags|FILE_READ);
if(m_h==INVALID_HANDLE)
{
m_error="cannot open file for append";
return false;
}
uchar old[];
ArrayResize(old,TKZ_FILE_HEADER_SIZE);
if((int)FileReadArray(m_h,old,0,TKZ_FILE_HEADER_SIZE)!=TKZ_FILE_HEADER_SIZE ||
m_io.GetU32(old,0)!=(uint)TKZ_FILE_MAGIC)
{
m_error="existing file is not a TKZ archive";
FileClose(m_h);
m_h=INVALID_HANDLE;
return false;
}
FileSeek(m_h,0,SEEK_END);
m_bytes=(long)FileTell(m_h);
return true;
}
m_h=FileOpen(filename,flags);
if(m_h==INVALID_HANDLE)
{
m_error="cannot create file";
return false;
}
uchar hdr[];
ArrayResize(hdr,TKZ_FILE_HEADER_SIZE);
ArrayInitialize(hdr,0);
m_io.PutU32(hdr,0,(uint)TKZ_FILE_MAGIC);
m_io.PutU16(hdr,4,TKZ_FILE_VERSION);
m_io.PutU16(hdr,6,TKZ_FILE_HEADER_SIZE);
uchar name[];
int len=StringToCharArray(symbol,name,0,-1,CP_UTF8);
for(int i=0; i<len && i<TKZ_SYMBOL_BYTES-1; i++)
hdr[8+i]=name[i];
m_io.PutU32(hdr,40,(uint)digits);
if((int)FileWriteArray(m_h,hdr,0,TKZ_FILE_HEADER_SIZE)!=TKZ_FILE_HEADER_SIZE)
{
m_error="cannot write file header";
Close();
return false;
}
m_bytes=TKZ_FILE_HEADER_SIZE;
return true;
}
//--- compress ticks[start..start+count) into one chunk and append it
bool WriteTicks(const MqlTick &ticks[],const int start,const int count)
{
if(m_h==INVALID_HANDLE)
{
m_error="file is not open";
return false;
}
ArrayResize(m_buf,0);
ulong t0=GetMicrosecondCount();
int sz=m_codec.EncodeTicks(ticks,start,count,m_buf);
m_enc_us+=GetMicrosecondCount()-t0;
if(sz<0)
{
m_error=m_codec.LastError();
return false;
}
if(m_verify)
{
t0=GetMicrosecondCount();
int n=m_check.Decode(m_buf,0,m_vt,m_vb,m_va);
m_ver_us+=GetMicrosecondCount()-t0;
if(n!=count)
{
m_error="verification failed: "+m_check.LastError();
return false;
}
for(int i=0; i<n; i++)
if(m_vt[i]!=ticks[start+i].time_msc || !m_check.SameBits(m_vb[i],ticks[start+i].bid) ||
!m_check.SameBits(m_va[i],ticks[start+i].ask))
{
m_error="verification failed at tick "+IntegerToString(start+i);
return false;
}
}
return Flush(sz,count);
}
bool Write(const long &time_msc[],const double &bid[],const double &ask[],const int start,const int count)
{
if(m_h==INVALID_HANDLE)
{
m_error="file is not open";
return false;
}
ArrayResize(m_buf,0);
ulong t0=GetMicrosecondCount();
int sz=m_codec.Encode(time_msc,bid,ask,start,count,m_buf);
m_enc_us+=GetMicrosecondCount()-t0;
if(sz<0)
{
m_error=m_codec.LastError();
return false;
}
if(m_verify)
{
t0=GetMicrosecondCount();
int n=m_check.Decode(m_buf,0,m_vt,m_vb,m_va);
m_ver_us+=GetMicrosecondCount()-t0;
if(n!=count)
{
m_error="verification failed: "+m_check.LastError();
return false;
}
for(int i=0; i<n; i++)
if(m_vt[i]!=time_msc[start+i] || !m_check.SameBits(m_vb[i],bid[start+i]) ||
!m_check.SameBits(m_va[i],ask[start+i]))
{
m_error="verification failed at tick "+IntegerToString(start+i);
return false;
}
}
return Flush(sz,count);
}
void Close(void)
{
if(m_h!=INVALID_HANDLE)
{
FileClose(m_h);
m_h=INVALID_HANDLE;
}
m_codec.Release();
m_check.Release();
ArrayFree(m_buf);
ArrayFree(m_vt);
ArrayFree(m_vb);
ArrayFree(m_va);
}
private:
bool Flush(const int sz,const int count)
{
if((int)FileWriteArray(m_h,m_buf,0,sz)!=sz)
{
m_error="file write failed";
return false;
}
FileFlush(m_h);
m_ticks+=count;
m_bytes+=sz;
return true;
}
};
//+------------------------------------------------------------------+
//| Reader: chunk index, random access by chunk or by time range |
//+------------------------------------------------------------------+
class CTkzReader
{
private:
int m_h;
CTkzCodec m_codec;
CTkzBytes m_io;
uchar m_buf[];
string m_error;
string m_symbol;
int m_digits;
int m_count;
long m_offset[];
STkzChunkInfo m_info[];
public:
CTkzReader(void)
{
m_h=INVALID_HANDLE;
m_error="";
m_symbol="";
m_digits=-1;
m_count=0;
}
~CTkzReader(void)
{
Close();
}
string LastError(void) { return m_error; }
string ArchiveSymbol(void) { return m_symbol; }
int ArchiveDigits(void) { return m_digits; }
int ChunkCount(void) { return m_count; }
bool Open(const string filename,const bool common)
{
Close();
m_error="";
int flags=FILE_BIN|FILE_READ|FILE_SHARE_READ;
if(common)
flags|=FILE_COMMON;
m_h=FileOpen(filename,flags);
if(m_h==INVALID_HANDLE)
{
m_error="cannot open file";
return false;
}
long fsize=(long)FileSize(m_h);
uchar hdr[];
ArrayResize(hdr,TKZ_FILE_HEADER_SIZE);
if(fsize<TKZ_FILE_HEADER_SIZE || (int)FileReadArray(m_h,hdr,0,TKZ_FILE_HEADER_SIZE)!=TKZ_FILE_HEADER_SIZE ||
m_io.GetU32(hdr,0)!=(uint)TKZ_FILE_MAGIC)
{
m_error="not a TKZ archive";
Close();
return false;
}
if(m_io.GetU16(hdr,4)!=TKZ_FILE_VERSION)
{
m_error="unsupported archive version";
Close();
return false;
}
int hsize=m_io.GetU16(hdr,6);
m_symbol=CharArrayToString(hdr,8,TKZ_SYMBOL_BYTES,CP_UTF8);
m_digits=(int)m_io.GetU32(hdr,40);
//--- scan chunk headers
long pos=hsize;
uchar ch[];
ArrayResize(ch,TKZ_HEADER_SIZE);
while(pos+TKZ_HEADER_SIZE<=fsize)
{
if(!FileSeek(m_h,pos,SEEK_SET) || (int)FileReadArray(m_h,ch,0,TKZ_HEADER_SIZE)!=TKZ_HEADER_SIZE)
{
m_error="read error";
break;
}
STkzChunkInfo info;
if(!m_codec.ReadInfo(ch,0,info))
{
m_error="bad chunk at offset "+IntegerToString(pos)+": "+m_codec.LastError();
break;
}
if(pos+info.total_size>fsize)
{
m_error="truncated last chunk";
break;
}
ArrayResize(m_offset,m_count+1,256);
ArrayResize(m_info,m_count+1,256);
m_offset[m_count]=pos;
m_info[m_count]=info;
m_count++;
pos+=info.total_size;
}
return true;
}
void Close(void)
{
if(m_h!=INVALID_HANDLE)
{
FileClose(m_h);
m_h=INVALID_HANDLE;
}
m_count=0;
ArrayFree(m_offset);
ArrayFree(m_info);
ArrayFree(m_buf);
m_codec.Release();
}
bool GetChunkInfo(const int i,STkzChunkInfo &info)
{
if(i<0 || i>=m_count)
return false;
info=m_info[i];
return true;
}
long TickCount(void)
{
long n=0;
for(int i=0; i<m_count; i++)
n+=m_info[i].tick_count;
return n;
}
//--- decode one chunk; ticks are appended when append=true
int ReadChunk(const int i,MqlTick &ticks[],const bool append=false)
{
if(i<0 || i>=m_count || m_h==INVALID_HANDLE)
{
m_error="bad chunk index";
return -1;
}
int sz=m_info[i].total_size;
ArrayResize(m_buf,sz);
if(!FileSeek(m_h,m_offset[i],SEEK_SET) || (int)FileReadArray(m_h,m_buf,0,sz)!=sz)
{
m_error="read error";
return -1;
}
int n=m_codec.DecodeTicks(m_buf,0,ticks,append);
if(n<0)
m_error=m_codec.LastError();
return n;
}
//--- all ticks of the archive
int ReadAll(MqlTick &ticks[])
{
ArrayResize(ticks,0);
for(int i=0; i<m_count; i++)
if(ReadChunk(i,ticks,true)<0)
return -1;
return ArraySize(ticks);
}
//--- ticks with from_msc <= time_msc < to_msc
int ReadRange(const long from_msc,const long to_msc,MqlTick &ticks[])
{
ArrayResize(ticks,0);
MqlTick tmp[];
int total=0;
for(int i=0; i<m_count; i++)
{
if(m_info[i].tick_count==0 || m_info[i].max_msc<from_msc || m_info[i].min_msc>=to_msc)
continue;
int n=ReadChunk(i,tmp,false);
if(n<0)
return -1;
ArrayResize(ticks,total+n,n);
for(int k=0; k<n; k++)
{
if(tmp[k].time_msc>=from_msc && tmp[k].time_msc<to_msc)
{
ticks[total]=tmp[k];
total++;
}
}
ArrayResize(ticks,total);
}
return total;
}
};
#endif // TKZ_ARCHIVE_MQH
//+------------------------------------------------------------------+