216 lines
16 KiB
MQL5
216 lines
16 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| TicksShortLite.mqh |
|
|
//| Copyright © 2025, Amr Ali |
|
|
//| https://www.mql5.com/en/users/amrali |
|
|
//+------------------------------------------------------------------+
|
|
#property copyright "Copyright © 2025, Amr Ali"
|
|
#property link "https://www.mql5.com/en/users/amrali"
|
|
#property version "1.03"
|
|
#property description "Standalone / lightweight version (no #include dependencies)"
|
|
#property description "Ideal for embedding in small projects or performance tests"
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Class: TICKS_SHORT |
|
|
//| Description: Delta + VLQ Compression for MqlTick[] |
|
|
//| Features: |
|
|
//| - Delta encoding of time, bid, and ask prices |
|
|
//| - ZigZag transform to handle signed values |
|
|
//| - VLQ variable-length integer encoding |
|
|
//| - Optimized for speed and minimal allocations |
|
|
//| - ZIP compression via SaveZipped() |
|
|
//+------------------------------------------------------------------+
|
|
class TICKS_SHORT
|
|
{
|
|
private:
|
|
static long PriceToInteger(const double price, const double power)
|
|
{
|
|
return (long)(price * power + 0.5);
|
|
}
|
|
|
|
//==============================================================
|
|
// ZigZag + VLQ utilities
|
|
//==============================================================
|
|
static ulong ZigZagEncode(const long n) { return (ulong)((n << 1) ^ (n >> 63)); }
|
|
static long ZigZagDecode(const ulong n) { return (long)((n >> 1) ^ -(long)(n & 1)); }
|
|
|
|
static void WriteVLQ(uchar &buf[], int &pos, ulong v)
|
|
{
|
|
while(v >= 0x80)
|
|
{
|
|
buf[pos++] = (uchar)((v & 0x7F) | 0x80);
|
|
v >>= 7;
|
|
}
|
|
buf[pos++] = (uchar)v;
|
|
}
|
|
|
|
static ulong ReadVLQ(const uchar &buf[], int &pos)
|
|
{
|
|
ulong v = 0;
|
|
int shift = 0;
|
|
while(pos < ArraySize(buf))
|
|
{
|
|
uchar b = buf[pos++];
|
|
v |= ((ulong)(b & 0x7F)) << shift;
|
|
if((b & 0x80) == 0)
|
|
break;
|
|
shift += 7;
|
|
}
|
|
return v;
|
|
}
|
|
|
|
public:
|
|
//==============================================================
|
|
// Compress ticks into uchar buffer
|
|
//==============================================================
|
|
static int Compress(const MqlTick &ticks[], uchar &buf[], const int iDigits = -1)
|
|
{
|
|
const int nticks = ArraySize(ticks);
|
|
if (nticks == 0) { ArrayFree(buf); return 0; }
|
|
|
|
const int digits = (iDigits < 0) ? _Digits : MathMin(iDigits, 7);
|
|
const double power = MathPow(10, digits);
|
|
|
|
// Pre-allocate 7 bytes per tick (rough upper bound)
|
|
if (ArrayResize(buf, nticks * 7) == -1)
|
|
{
|
|
Print("ERROR: Failed to resize buffer in Compress.");
|
|
return -1;
|
|
}
|
|
|
|
int pos = 0;
|
|
WriteVLQ(buf, pos, nticks);
|
|
WriteVLQ(buf, pos, digits);
|
|
|
|
long prev_time = 0, prev_bid = 0, prev_ask = 0;
|
|
for (int i = 0; i < nticks; i++)
|
|
{
|
|
const MqlTick tick = ticks[i];
|
|
long t = tick.time_msc;
|
|
long b = PriceToInteger(tick.bid, power);
|
|
long a = PriceToInteger(tick.ask, power);
|
|
|
|
WriteVLQ(buf, pos, ZigZagEncode(t - prev_time)); // ZigZag + VLQ encode each delta
|
|
WriteVLQ(buf, pos, ZigZagEncode(b - prev_bid));
|
|
WriteVLQ(buf, pos, ZigZagEncode(a - prev_ask));
|
|
|
|
prev_time = t; // Update previous
|
|
prev_bid = b;
|
|
prev_ask = a;
|
|
|
|
if (pos + 8 >= ArraySize(buf)) { ArrayResize(buf, ArraySize(buf) * 2); }
|
|
}
|
|
|
|
return ArrayResize(buf, pos); // Number of bytes written
|
|
}
|
|
|
|
//==============================================================
|
|
// Decompress uchar buffer back to ticks
|
|
//==============================================================
|
|
static int Decompress(const uchar &buf[], MqlTick &out_ticks[])
|
|
{
|
|
const int nbuf = ArraySize(buf);
|
|
if (nbuf == 0) { ArrayFree(out_ticks); return 0; }
|
|
|
|
int pos = 0;
|
|
const int totalCompressedTicks = (int)ReadVLQ(buf, pos);
|
|
const int digits = (int)ReadVLQ(buf, pos);
|
|
const double point = MathPow(10, -digits);
|
|
|
|
if (ArrayResize(out_ticks, totalCompressedTicks) == -1)
|
|
{
|
|
Print("ERROR: Failed to resize ticks array in Decompress.");
|
|
return -1;
|
|
}
|
|
|
|
long time = 0, bid = 0, ask = 0;
|
|
for (int i = 0; i < totalCompressedTicks; i++)
|
|
{
|
|
time += ZigZagDecode(ReadVLQ(buf, pos)); // Delta reconstruction
|
|
bid += ZigZagDecode(ReadVLQ(buf, pos));
|
|
ask += ZigZagDecode(ReadVLQ(buf, pos));
|
|
|
|
MqlTick tick = {};
|
|
tick.time_msc = time;
|
|
tick.bid = NormalizeDouble(bid * point, digits);
|
|
tick.ask = NormalizeDouble(ask * point, digits);
|
|
tick.time = (datetime)(time / 1000);
|
|
out_ticks[i] = tick; // Append reconstructed tick
|
|
}
|
|
|
|
return ArraySize(out_ticks); // Number of MqlTicks written
|
|
}
|
|
|
|
//==============================================================
|
|
// Compress ticks and save to file
|
|
//==============================================================
|
|
static bool Save(const string fileName, const MqlTick &ticks[],
|
|
const bool common = false, const int iDigits = -1)
|
|
{
|
|
uchar compressed[];
|
|
int count = Compress(ticks, compressed, iDigits);
|
|
if (count <= 0) return false; // Error in compression
|
|
|
|
return FileSave(fileName, compressed, (common ? FILE_COMMON : 0));
|
|
}
|
|
|
|
//==============================================================
|
|
// Compress + ZIP save
|
|
//==============================================================
|
|
static bool SaveZipped(const string fileName, const MqlTick &ticks[],
|
|
const bool common = false, const int iDigits = -1)
|
|
{
|
|
uchar compressed[];
|
|
int count = Compress(ticks, compressed, iDigits);
|
|
if (count <= 0) return false; // Error in compression
|
|
|
|
uchar zipped[], key[];
|
|
if (!CryptEncode(CRYPT_ARCH_ZIP, compressed, key, zipped))
|
|
{
|
|
Print("ERROR: ZIP compression failed in SaveZipped()");
|
|
return false;
|
|
}
|
|
|
|
return FileSave(fileName, zipped, (common ? FILE_COMMON : 0));
|
|
}
|
|
|
|
//==============================================================
|
|
// Load and decompress (auto-detect ZIP)
|
|
//==============================================================
|
|
static int Load(const string fileName, MqlTick &ticks[], const bool common = false)
|
|
{
|
|
uchar compressed[];
|
|
int countLoaded = (int)FileLoad(fileName, compressed, (common ? FILE_COMMON : 0));
|
|
if (countLoaded <= 0) { ArrayFree(ticks); return countLoaded; }
|
|
|
|
uchar unzipped[], key[];
|
|
if (CryptDecode(CRYPT_ARCH_ZIP, compressed, key, unzipped))
|
|
return Decompress(unzipped, ticks); // ZIP detected
|
|
else
|
|
return Decompress(compressed, ticks); // Plain data
|
|
}
|
|
|
|
//==============================================================
|
|
// Equality check functions
|
|
//==============================================================
|
|
static bool IsEqual(const MqlTick &Tick1, const MqlTick &Tick2)
|
|
{
|
|
return((Tick1.bid == Tick2.bid) && (Tick1.ask == Tick2.ask) && (Tick1.time_msc == Tick2.time_msc));
|
|
}
|
|
|
|
static bool IsEqual(const MqlTick &Ticks1[], const MqlTick &Ticks2[])
|
|
{
|
|
const int size = ArraySize(Ticks1);
|
|
bool Res = (ArraySize(Ticks2) == size);
|
|
for (int i = 0; Res && (i < size); i++)
|
|
Res = IsEqual(Ticks1[i], Ticks2[i]);
|
|
return(Res);
|
|
}
|
|
|
|
static bool IsEqual(const MqlTick &Ticks[], const uchar &Compressed[])
|
|
{
|
|
MqlTick Ticks2[];
|
|
int decompressedCount = Decompress(Compressed, Ticks2);
|
|
if (decompressedCount == -1) return false; // Decompression error
|
|
return IsEqual(Ticks, Ticks2);
|
|
}
|
|
};
|