// Test driver for the TKZ codec: the MQL5 source is translated to C++ by // mql2cpp.py and compiled against mql5_shim.h, so this exercises the exact // code that runs in MetaTrader 5. // // usage: test_codec [-chunk=N] [-quiet] file.ticks ... // *.ticks files: little-endian records {int64 time_msc, double bid, double ask} // Without files only the synthetic tests run. #include "mql5_shim.h" #include "TkzCodec.h" #include "TkzArchive.h" #include "TkzSelfTest.h" #include static int g_failures = 0; #define CHECK(cond, ...) \ do { \ if (!(cond)) { \ printf(" FAIL %s:%d: ", __FILE__, __LINE__); \ printf(__VA_ARGS__); \ printf("\n"); \ g_failures++; \ } \ } while (0) static long bits_of(double d) { long l; memcpy(&l, &d, 8); return l; } static double double_of(long l) { double d; memcpy(&d, &l, 8); return d; } struct Series { MqlArray t; MqlArray b, a; int size() const { return ArraySize(t); } void push(long tt, double bb, double aa) { int n = size(); ArrayResize(t, n + 1); ArrayResize(b, n + 1); ArrayResize(a, n + 1); t[n] = tt; b[n] = bb; a[n] = aa; } }; static bool load_ticks(const char *path, Series &s) { FILE *f = fopen(path, "rb"); if (!f) return false; fseek(f, 0, SEEK_END); long n = ftell(f) / 24; fseek(f, 0, SEEK_SET); ArrayResize(s.t, (int)n); ArrayResize(s.b, (int)n); ArrayResize(s.a, (int)n); for (long i = 0; i < n; ++i) { long t; double b, a; if (fread(&t, 8, 1, f) != 1 || fread(&b, 8, 1, f) != 1 || fread(&a, 8, 1, f) != 1) { fclose(f); return false; } s.t[i] = t; s.b[i] = b; s.a[i] = a; } fclose(f); return true; } // encode s[start, start+count) into one chunk, decode it back and compare bitwise static int roundtrip(CTkzCodec &enc, CTkzCodec &dec, const Series &s, int start, int count, const char *name, MqlArray *keep = nullptr) { MqlArray buf; int sz = enc.Encode(s.t, s.b, s.a, start, count, buf); CHECK(sz > 0, "%s: encode failed: %s", name, enc.LastError().c_str()); if (sz <= 0) return -1; CHECK(sz == ArraySize(buf), "%s: size mismatch", name); MqlArray t; MqlArray b, a; int n = dec.Decode(buf, 0, t, b, a); CHECK(n == count, "%s: decode returned %d (expected %d): %s", name, n, count, dec.LastError().c_str()); if (n != count) return -1; int bad = 0; for (int i = 0; i < n && bad < 5; ++i) { if (t[i] != s.t[start + i] || bits_of(b[i]) != bits_of(s.b[start + i]) || bits_of(a[i]) != bits_of(s.a[start + i])) { printf(" mismatch %s at %d: got (%ld, %.17g, %.17g) expected (%ld, %.17g, %.17g)\n", name, i, t[i], b[i], a[i], s.t[start + i], s.b[start + i], s.a[start + i]); bad++; } } CHECK(bad == 0, "%s: %d mismatching ticks", name, bad); // MqlTick API MqlArray ticks; int n2 = dec.DecodeTicks(buf, 0, ticks); CHECK(n2 == count, "%s: DecodeTicks failed", name); for (int i = 0; i < n2 && i < count; ++i) { if (ticks[i].time_msc != s.t[start + i] || bits_of(ticks[i].bid) != bits_of(s.b[start + i]) || bits_of(ticks[i].ask) != bits_of(s.a[start + i]) || ticks[i].time != s.t[start + i] / 1000) { CHECK(false, "%s: DecodeTicks mismatch at %d", name, i); break; } } if (keep) *keep = buf; return sz; } // ------------------------------------------------------------------ synthetic data static void synthetic_tests() { printf("== synthetic edge cases\n"); CTkzCodec enc, dec; std::mt19937_64 rng(12345); const double nan = std::nan(""), inf = INFINITY; { Series s; roundtrip(enc, dec, s, 0, 0, "empty"); } { Series s; s.push(1700000000123L, 1.23456, 1.23467); roundtrip(enc, dec, s, 0, 1, "single"); } { // constant prices, constant times Series s; for (int i = 0; i < 1000; ++i) s.push(1700000000000L, 1.1, 1.1); roundtrip(enc, dec, s, 0, s.size(), "constant"); } { // random walk forex-like Series s; long t = 1700000000000L, b = 108000, sp = 2; for (int i = 0; i < 200000; ++i) { t += (long)(rng() % 3000); b += (long)(rng() % 7) - 3; sp = 1 + (long)(rng() % 4); s.push(t, b / 1e5, (b + sp) / 1e5); } int sz = roundtrip(enc, dec, s, 0, s.size(), "randomwalk5"); printf(" randomwalk5: %.3f bytes/tick\n", (double)sz / s.size()); } { // negative time steps, huge gaps, duplicates Series s; long t = 1700000000000L; for (int i = 0; i < 50000; ++i) { int r = (int)(rng() % 100); if (r == 0) t -= (long)(rng() % 100000); else if (r == 1) t += (long)(rng() % 4000000000000L); else if (r < 30) t += 0; else t += (long)(rng() % 500); s.push(t, 2000.0 + (double)(rng() % 1000) / 100.0, 2001.0 + (double)(rng() % 1000) / 100.0); } s.push(-5000000000000L, 1, 2); s.push(9000000000000000000L, 1, 2); s.push(-9000000000000000000L, 1, 2); roundtrip(enc, dec, s, 0, s.size(), "time-anomalies"); } { // irregular doubles: NaN, Inf, -0, denormals, non-normalized, huge Series s; long t = 1700000000000L; for (int 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 = -0.0; if (r == 13) b = 4.9e-324; if (r == 17) a = 0.1 + 0.2; // 0.30000000000000004 if (r == 19) b = 1e300; if (r == 23) a = -1e300; if (r == 29) b = double_of(bits_of(1.2345) + 1); s.push(t, b, a); } int sz = roundtrip(enc, dec, s, 0, s.size(), "irregular-doubles"); printf(" irregular-doubles: %.3f bytes/tick\n", (double)sz / s.size()); } { // all prices irregular Series s; for (int i = 0; i < 3000; ++i) s.push(1700000000000L + i * 7, double_of((long)rng()), double_of((long)rng())); roundtrip(enc, dec, s, 0, s.size(), "all-random-bits"); } { // tick size 0.25 at 2 digits with offset, second timestamps Series s; long t = 1700000000000L; long q = 20000; for (int i = 0; i < 100000; ++i) { t += 1000 * (1 + (long)(rng() % 5)); q += (long)(rng() % 9) - 4; double b = q * 0.25 + 0.01; s.push(t, b, b + 0.5); } int sz = roundtrip(enc, dec, s, 0, s.size(), "grid-0.25"); printf(" grid-0.25 + 1s timestamps: %.3f bytes/tick\n", (double)sz / s.size()); } { // negative prices (oil 2020 style), zero ask, 12 digits, large values Series s; long t = 1700000000000L; for (int i = 0; i < 30000; ++i) { t += (long)(rng() % 200); double b = -37.63 + (double)((long)(rng() % 2000) - 1000) / 100.0; double a = (i % 10 == 0) ? 0.0 : b + 0.03; s.push(t, b, a); } roundtrip(enc, dec, s, 0, s.size(), "negative-prices"); Series s2; for (int i = 0; i < 5000; ++i) s2.push(1700000000000L + i, 0.000012345678 + i * 1e-12, 0.000012345699 + i * 1e-12); roundtrip(enc, dec, s2, 0, s2.size(), "12-digits"); Series s3; for (int i = 0; i < 5000; ++i) s3.push(1700000000000L + i, 3999999999.5 + i, 3999999999.5 + 2 * i); roundtrip(enc, dec, s3, 0, s3.size(), "large-values"); Series s4; for (int i = 0; i < 5000; ++i) s4.push(1700000000000L + i, (double)(rng() % 1000000000000L), (double)(rng() % 3)); roundtrip(enc, dec, s4, 0, s4.size(), "random-integers"); } { // determinism: same input -> identical bytes Series s; long t = 1700000000000L; for (int i = 0; i < 30000; ++i) { t += (long)(rng() % 300); s.push(t, 1.1 + (double)(rng() % 100) / 1e5, 1.1002 + (double)(rng() % 100) / 1e5); } MqlArray b1, b2; roundtrip(enc, dec, s, 0, s.size(), "det-1", &b1); CTkzCodec fresh; fresh.Encode(s.t, s.b, s.a, 0, s.size(), b2); CHECK(ArraySize(b1) == ArraySize(b2) && memcmp(b1.v.data(), b2.v.data(), (size_t)ArraySize(b1)) == 0, "determinism: reused vs fresh codec output differs"); // corruption: flip bytes in the payload, decoder must fail cleanly (no crash) int detected = 0, trials = 200; for (int k = 0; k < trials; ++k) { MqlArray c = b1; int pos = TKZ_HEADER_SIZE + (int)(rng() % (ulong)(ArraySize(c) - TKZ_HEADER_SIZE)); c[pos] = (uchar)(c[pos] ^ (1 + rng() % 255)); MqlArray t2; MqlArray bb, aa; if (dec.Decode(c, 0, t2, bb, aa) < 0) detected++; } printf(" corruption: %d/%d payload corruptions detected\n", detected, trials); CHECK(detected >= trials - 2, "corruption detection too weak"); // truncated / bad header MqlArray c = b1; ArrayResize(c, ArraySize(c) - 10); MqlArray t2; MqlArray bb, aa; CHECK(dec.Decode(c, 0, t2, bb, aa) < 0, "truncated chunk accepted"); c = b1; c[0] = 0; CHECK(dec.Decode(c, 0, t2, bb, aa) < 0, "bad magic accepted"); } } // ------------------------------------------------------------------ fuzzing // Random mutations of valid chunks (header and payload bytes, truncation, // extension, random garbage) must be rejected or decoded, never crash. static void fuzz_tests(int iterations) { printf("== fuzzing (%d mutated chunks)\n", iterations); std::mt19937_64 rng(777); CTkzCodec enc, dec; MqlArray chunks[3]; Series orig[3]; { Series s; long t = 1700000000000L, b = 108000; for (int i = 0; i < 3000; ++i) { t += (long)(rng() % 900); b += (long)(rng() % 5) - 2; s.push(t, b / 1e5, (b + 1 + (long)(rng() % 3)) / 1e5); } enc.Encode(s.t, s.b, s.a, 0, s.size(), chunks[0]); orig[0] = s; Series s2; // with escapes and decimal sub-grid for (int i = 0; i < 3000; ++i) { double bb = 1900.005 + 0.01 * (double)(rng() % 50); if (i % 50 == 7) bb = double_of((long)rng()); s2.push(t + i * 37, bb, bb + 0.3); } enc.Encode(s2.t, s2.b, s2.a, 0, s2.size(), chunks[1]); orig[1] = s2; Series s3; s3.push(t, 1.0, 1.0); enc.Encode(s3.t, s3.b, s3.a, 0, 1, chunks[2]); orig[2] = s3; } int accepted = 0, rejected = 0, silent = 0; ulong t0 = GetMicrosecondCount(); for (int it = 0; it < iterations; ++it) { MqlArray c = chunks[it % 3]; int n = ArraySize(c); int kind = (int)(rng() % 6); if (kind == 0) { // header byte int pos = (int)(rng() % TKZ_HEADER_SIZE); c[pos] = (uchar)(c[pos] ^ (1 + rng() % 255)); } else if (kind == 1) { // several payload bytes for (int k = 0; k < 1 + (int)(rng() % 4); ++k) { int pos = TKZ_HEADER_SIZE + (int)(rng() % (ulong)(n - TKZ_HEADER_SIZE)); c[pos] = (uchar)rng(); } } else if (kind == 2) { // truncate ArrayResize(c, (int)(rng() % (ulong)n)); } else if (kind == 3) { // extend with garbage int extra = 1 + (int)(rng() % 64); ArrayResize(c, n + extra); for (int k = 0; k < extra; ++k) c[n + k] = (uchar)rng(); } else if (kind == 4) { // payload size field changed uint psz = (uint)c[16] | ((uint)c[17] << 8) | ((uint)c[18] << 16) | ((uint)c[19] << 24); psz = (uint)((long)psz + (long)(rng() % 9) - 4); c[16] = (uchar)psz; c[17] = (uchar)(psz >> 8); c[18] = (uchar)(psz >> 16); c[19] = (uchar)(psz >> 24); } else { // tick count field changed uint cnt = (uint)rng() % 100000u; c[12] = (uchar)cnt; c[13] = (uchar)(cnt >> 8); c[14] = (uchar)(cnt >> 16); c[15] = (uchar)(cnt >> 24); } MqlArray t; MqlArray b, a; int got = dec.Decode(c, 0, t, b, a); if (got < 0) { rejected++; continue; } // an accepted chunk must decode to exactly the original ticks accepted++; const Series &o = orig[it % 3]; bool same = got == o.size(); for (int i = 0; same && i < got; ++i) same = t[i] == o.t[i] && bits_of(b[i]) == bits_of(o.b[i]) && bits_of(a[i]) == bits_of(o.a[i]); if (!same) silent++; } printf(" %d rejected, %d accepted with identical output (mutations of unused bytes), %.1f s\n", rejected, accepted - silent, (double)(GetMicrosecondCount() - t0) / 1e6); CHECK(silent == 0, "%d mutated chunks decoded to different ticks without an error", silent); CHECK(rejected > iterations / 2, "too few mutations rejected"); } // ------------------------------------------------------------------ archive static void archive_tests(const Series &s) { printf("== archive\n"); CTkzWriter w; string fname = "test_archive.tkz"; CHECK(w.Open(fname, "TESTSYM", 5, false), "writer open: %s", w.LastError().c_str()); w.SetVerify(true); int n = s.size(); int chunk = n / 7 + 1; MqlArray ticks; ArrayResize(ticks, n); for (int i = 0; i < n; ++i) { memset(&ticks[i], 0, sizeof(MqlTick)); ticks[i].time_msc = s.t[i]; ticks[i].time = s.t[i] / 1000; ticks[i].bid = s.b[i]; ticks[i].ask = s.a[i]; } for (int st = 0; st < n; st += chunk) { int c = n - st < chunk ? n - st : chunk; CHECK(w.WriteTicks(ticks, st, c), "write chunk: %s", w.LastError().c_str()); } w.Close(); { CTkzWriter w2; CHECK(w2.Open(fname, "IGNORED", 0, false, true), "append open: %s", w2.LastError().c_str()); w2.SetVerify(true); CHECK(w2.Write(s.t, s.b, s.a, 0, 1000), "append write: %s", w2.LastError().c_str()); w2.Close(); } CTkzReader r; CHECK(r.Open(fname, false), "reader open: %s", r.LastError().c_str()); CHECK(r.ArchiveSymbol() == "TESTSYM", "symbol name %s", r.ArchiveSymbol().c_str()); CHECK(r.ArchiveDigits() == 5, "digits"); CHECK(r.ChunkCount() == (n + chunk - 1) / chunk + 1, "chunk count %d", r.ChunkCount()); CHECK(r.TickCount() == (long)n + 1000, "tick count %ld", r.TickCount()); MqlArray all; int got = r.ReadAll(all); CHECK(got == n + 1000, "ReadAll %d", got); int bad = 0; for (int i = 0; i < got && i < n; ++i) if (all[i].time_msc != s.t[i] || bits_of(all[i].bid) != bits_of(s.b[i]) || bits_of(all[i].ask) != bits_of(s.a[i])) bad++; CHECK(bad == 0, "archive mismatches: %d", bad); // range query long from = s.t[n / 3], to = s.t[n / 2]; MqlArray part; int pn = r.ReadRange(from, to, part); int expect = 0; for (int i = 0; i < n; ++i) if (s.t[i] >= from && s.t[i] < to) expect++; for (int i = 0; i < 1000; ++i) if (s.t[i] >= from && s.t[i] < to) expect++; CHECK(pn == expect, "ReadRange returned %d expected %d", pn, expect); r.Close(); FileDelete(fname); } int main(int argc, char **argv) { int chunk = 0; bool quiet = false; int ai = 1; for (; ai < argc && argv[ai][0] == '-'; ++ai) { if (!strncmp(argv[ai], "-chunk=", 7)) chunk = atoi(argv[ai] + 7); else if (!strcmp(argv[ai], "-quiet")) quiet = true; } synthetic_tests(); fuzz_tests(3000); CTkzCodec enc, dec; double tot_bytes = 0, tot_ticks = 0; Series first; bool have_first = false; for (; ai < argc; ++ai) { Series s; if (!load_ticks(argv[ai], s)) { printf("cannot load %s\n", argv[ai]); g_failures++; continue; } const char *nm = strrchr(argv[ai], '/'); nm = nm ? nm + 1 : argv[ai]; int n = s.size(); int csize = chunk > 0 ? chunk : n; double bytes = 0, tenc = 0, tdec = 0; for (int st = 0; st < n; st += csize) { int c = n - st < csize ? n - st : csize; MqlArray buf; ulong t0 = GetMicrosecondCount(); int sz = enc.Encode(s.t, s.b, s.a, st, c, buf); ulong t1 = GetMicrosecondCount(); CHECK(sz > 0, "%s: encode failed", nm); MqlArray t; MqlArray b, a; int dn = dec.Decode(buf, 0, t, b, a); ulong t2 = GetMicrosecondCount(); CHECK(dn == c, "%s: decode failed: %s", nm, dec.LastError().c_str()); int bad = 0; for (int i = 0; i < dn; ++i) if (t[i] != s.t[st + i] || bits_of(b[i]) != bits_of(s.b[st + i]) || bits_of(a[i]) != bits_of(s.a[st + i])) bad++; CHECK(bad == 0, "%s: %d mismatches in chunk at %d", nm, bad, st); bytes += sz; tenc += (double)(t1 - t0); tdec += (double)(t2 - t1); if (!quiet && st == 0) { STkzChunkInfo info; enc.ReadInfo(buf, 0, info); printf(" %s: digits=%d mod=%d price_step=%ld time_step=%ld flags=%d\n", nm, info.digits, info.modulus, info.price_step, info.time_step, info.flags); } } printf("%-28s ticks=%8d %.4f bytes/tick ratio %.1fx (vs 24 B) enc %.0f t/s dec %.0f t/s\n", nm, n, bytes / n, 24.0 * n / bytes, n / (tenc / 1e6), n / (tdec / 1e6)); tot_bytes += bytes; tot_ticks += n; if (!have_first) { Series cut; int m = n < 300000 ? n : 300000; for (int i = 0; i < m; ++i) cut.push(s.t[i], s.b[i], s.a[i]); first = cut; have_first = true; } } if (tot_ticks > 0) printf("TOTAL %.0f ticks %.4f bytes/tick\n", tot_ticks, tot_bytes / tot_ticks); if (have_first) archive_tests(first); // the self-test shipped for MetaTrader (TkzSelfTest.mqh) must pass as well { printf("== MQL5 self-test (TkzSelfTest.mqh)\n"); CTkzSelfTest st; st.RunGolden(); st.RunSynthetic(); if (have_first) { MqlArray ticks; ArrayResize(ticks, first.size()); for (int i = 0; i < first.size(); ++i) { memset(&ticks[i], 0, sizeof(MqlTick)); ticks[i].time_msc = first.t[i]; ticks[i].bid = first.b[i]; ticks[i].ask = first.a[i]; } st.RunTicks(ticks, first.size(), "history sample"); } st.RunArchive(); CHECK(st.Failed() == 0, "self-test reported %d failures", st.Failed()); CHECK(st.Passed() >= (have_first ? 16 : 14), "self-test passed only %d checks", st.Passed()); } if (g_failures) { printf("FAILURES: %d\n", g_failures); return 1; } printf("ALL TESTS PASSED\n"); return 0; }