2026-11-06 16:07:23 -08:00 | | | //+------------------------------------------------------------------+
|
| | | //| 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
|
| | |
|
2026-11-06 20:26:15 -08:00 | | | #include "TkzCodec.mqh"
|
2026-11-06 16:07:23 -08:00 | | |
|
| | | #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;
|
2026-11-06 16:11:33 -08:00 | | | CTkzCodec m_check;
|
2026-11-06 16:07:23 -08:00 | | | CTkzBytes m_io;
|
| | | uchar m_buf[];
|
| | | string m_error;
|
| | | long m_ticks;
|
| | | long m_bytes;
|
2026-11-06 16:11:33 -08:00 | | | bool m_verify;
|
| | | ulong m_enc_us;
|
| | | ulong m_ver_us;
|
| | | long m_vt[];
|
| | | double m_vb[];
|
| | | double m_va[];
|
2026-11-06 16:07:23 -08:00 | | |
|
| | | public:
|
| | | CTkzWriter(void)
|
| | | {
|
| | | m_h=INVALID_HANDLE;
|
| | | m_error="";
|
| | | m_ticks=0;
|
| | | m_bytes=0;
|
2026-11-06 16:11:33 -08:00 | | | m_verify=false;
|
| | | m_enc_us=0;
|
| | | m_ver_us=0;
|
2026-11-06 16:07:23 -08:00 | | | }
|
| | | ~CTkzWriter(void)
|
| | | {
|
| | | Close();
|
| | | }
|
| | |
|
| | | string LastError(void) { return m_error; }
|
| | | long TicksWritten(void) { return m_ticks; }
|
| | | long BytesWritten(void) { return m_bytes; }
|
2026-11-06 16:11:33 -08:00 | | | 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; }
|
2026-11-06 16:07:23 -08:00 | | |
|
| | | //--- 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;
|
| | | }
|
2026-11-06 16:11:33 -08:00 | | | 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)
|
2026-11-06 16:07:23 -08:00 | | | {
|
| | | 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);
|
2026-11-06 16:11:33 -08:00 | | | if((int)FileWriteArray(m_h,hdr,0,TKZ_FILE_HEADER_SIZE)!=TKZ_FILE_HEADER_SIZE)
|
2026-11-06 16:07:23 -08:00 | | | {
|
| | | 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);
|
2026-11-06 16:11:33 -08:00 | | | ulong t0=GetMicrosecondCount();
|
2026-11-06 16:07:23 -08:00 | | | int sz=m_codec.EncodeTicks(ticks,start,count,m_buf);
|
2026-11-06 16:11:33 -08:00 | | | m_enc_us+=GetMicrosecondCount()-t0;
|
2026-11-06 16:07:23 -08:00 | | | if(sz<0)
|
| | | {
|
| | | m_error=m_codec.LastError();
|
| | | return false;
|
| | | }
|
2026-11-06 16:11:33 -08:00 | | | 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;
|
| | | }
|
| | | }
|
2026-11-06 16:07:23 -08:00 | | | 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);
|
2026-11-06 16:11:33 -08:00 | | | ulong t0=GetMicrosecondCount();
|
2026-11-06 16:07:23 -08:00 | | | int sz=m_codec.Encode(time_msc,bid,ask,start,count,m_buf);
|
2026-11-06 16:11:33 -08:00 | | | m_enc_us+=GetMicrosecondCount()-t0;
|
2026-11-06 16:07:23 -08:00 | | | if(sz<0)
|
| | | {
|
| | | m_error=m_codec.LastError();
|
| | | return false;
|
| | | }
|
2026-11-06 16:11:33 -08:00 | | | 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;
|
| | | }
|
| | | }
|
2026-11-06 16:07:23 -08:00 | | | return Flush(sz,count);
|
| | | }
|
| | | void Close(void)
|
| | | {
|
| | | if(m_h!=INVALID_HANDLE)
|
| | | {
|
| | | FileClose(m_h);
|
| | | m_h=INVALID_HANDLE;
|
| | | }
|
| | | m_codec.Release();
|
2026-11-06 16:11:33 -08:00 | | | m_check.Release();
|
2026-11-06 16:07:23 -08:00 | | | ArrayFree(m_buf);
|
2026-11-06 16:11:33 -08:00 | | | ArrayFree(m_vt);
|
| | | ArrayFree(m_vb);
|
| | | ArrayFree(m_va);
|
2026-11-06 16:07:23 -08:00 | | | }
|
| | |
|
| | | 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; }
|
2026-11-06 16:11:33 -08:00 | | | string ArchiveSymbol(void) { return m_symbol; }
|
| | | int ArchiveDigits(void) { return m_digits; }
|
2026-11-06 16:07:23 -08:00 | | | 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);
|
2026-11-06 16:11:33 -08:00 | | | if(fsize<TKZ_FILE_HEADER_SIZE || (int)FileReadArray(m_h,hdr,0,TKZ_FILE_HEADER_SIZE)!=TKZ_FILE_HEADER_SIZE ||
|
2026-11-06 16:07:23 -08:00 | | | 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)
|
| | | {
|
2026-11-06 16:11:33 -08:00 | | | if(!FileSeek(m_h,pos,SEEK_SET) || (int)FileReadArray(m_h,ch,0,TKZ_HEADER_SIZE)!=TKZ_HEADER_SIZE)
|
2026-11-06 16:07:23 -08:00 | | | {
|
| | | 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
|
| | | //+------------------------------------------------------------------+
|