// End-to-end test of the MetaTrader scripts (MQL5/Scripts/Tkz/*.mq5) against an // emulated terminal whose tick history is loaded from a .ticks file. // // usage: test_scripts file.ticks [symbol digits] #define MQL5_TERMINAL_SIM #include "mql5_shim.h" #include "TkzCodec.h" #include "TkzArchive.h" #include "TkzSelfTest.h" namespace s_export { #include "script_TkzExport.h" } namespace s_verify { #include "script_TkzVerify.h" } namespace s_custom { #include "script_TkzToCustom.h" } namespace s_selftest { #include "script_TkzSelfTest.h" } 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 bool same_tick(const MqlTick &a, const MqlTick &b) { return a.time_msc == b.time_msc && bits_of(a.bid) == bits_of(b.bid) && bits_of(a.ask) == bits_of(b.ask); } static bool log_has(const char *text) { return MqlLog().find(text) != std::string::npos; } // ticks of the emulated history with from <= time < to (seconds) static std::vector history(datetime from, datetime to) { std::vector r; for (auto &t : MqlSim().ticks) if (t.time_msc >= from * 1000 && t.time_msc < to * 1000) r.push_back(t); return r; } // calendar period of a tick time: 0 = day, 1 = week starting Sunday, 2 = month (UTC/server time) static long period_of(long msc, int mode) { long day = (msc / 1000) / 86400; if (mode == 0) return day; if (mode == 1) return (day + 4) / 7; // 1970-01-01 was a Thursday time_t tt = (time_t)(msc / 1000); struct tm g; gmtime_r(&tt, &g); return (long)g.tm_year * 12 + g.tm_mon; } static void check_archive(const char *file, const std::vector &expect, int expect_chunks, const char *what, int mode) { CTkzReader r; CHECK(r.Open(file, false), "%s: cannot open archive", what); MqlArray all; int n = r.ReadAll(all); CHECK(n == (int)expect.size(), "%s: %d ticks in archive, expected %zu", what, n, expect.size()); int bad = 0; for (int i = 0; i < n && i < (int)expect.size(); ++i) if (!same_tick(all[i], expect[(size_t)i])) bad++; CHECK(bad == 0, "%s: %d ticks differ", what, bad); if (expect_chunks >= 0) CHECK(r.ChunkCount() == expect_chunks, "%s: %d chunks, expected %d", what, r.ChunkCount(), expect_chunks); // every chunk must stay inside one calendar period, consecutive chunks in different ones long prev = -1; for (int i = 0; i < r.ChunkCount(); ++i) { STkzChunkInfo info; r.GetChunkInfo(i, info); CHECK(info.min_msc <= info.max_msc, "%s: chunk %d time range", what, i); long p0 = period_of(info.min_msc, mode), p1 = period_of(info.max_msc, mode); CHECK(p0 == p1, "%s: chunk %d spans two calendar periods", what, i); CHECK(p0 > prev, "%s: chunk %d is not in a new period", what, i); prev = p1; } printf(" %s: %d ticks, %d chunks, archive OK\n", what, n, r.ChunkCount()); r.Close(); } int main(int argc, char **argv) { if (argc < 2) { printf("usage: test_scripts file.ticks [symbol digits]\n"); return 2; } auto &sim = MqlSim(); if (argc >= 4) { sim.symbol = argv[2]; sim.digits = atoi(argv[3]); } FILE *f = fopen(argv[1], "rb"); if (!f) { printf("cannot open %s\n", argv[1]); return 2; } long t; double b, a; while (fread(&t, 8, 1, f) == 1 && fread(&b, 8, 1, f) == 1 && fread(&a, 8, 1, f) == 1) { MqlTick k; memset(&k, 0, sizeof(k)); k.time_msc = t; k.time = t / 1000; k.bid = b; k.ask = a; k.flags = TICK_FLAG_BID | TICK_FLAG_ASK; sim.ticks.push_back(k); } fclose(f); printf("== scripts on %s (%zu ticks, %s)\n", argv[1], sim.ticks.size(), sim.symbol.c_str()); MqlQuiet() = true; datetime first = sim.ticks.front().time, last = sim.ticks.back().time; // a range that starts and ends in the middle of days (and of weeks for long histories) datetime span = last - first; datetime from = span > 10 * 86400 ? first + 3 * 86400 + 5 * 3600 + 17 : first + span / 4 + 17; datetime to = span > 10 * 86400 ? last - 4 * 86400 - 3 * 3600 : last - span / 4; // --- export, weekly chunks, verification on s_export::InpSymbol = ""; s_export::InpFrom = from; s_export::InpTo = to; s_export::InpChunk = s_export::TKZ_CHUNK_WEEK; s_export::InpFile = ""; s_export::InpVerify = true; MqlLog().clear(); s_export::OnStart(); CHECK(log_has("TKZ: exported"), "export did not finish: %s", MqlLog().c_str()); string file = sim.symbol + ".tkz"; // weeks touched by [from, to): boundaries are Sundays 00:00 int weeks = 0; for (datetime c = from; c < to;) { datetime nb = s_export::NextBoundary(c, s_export::TKZ_CHUNK_WEEK); if (!history(c, nb < to ? nb : to).empty()) weeks++; c = nb; } check_archive(file.c_str(), history(from, to), weeks, "export week", 1); printf(" export log: %s", MqlLog().c_str()); // --- verify script on that archive s_verify::InpFile = ""; s_verify::InpCompare = true; MqlLog().clear(); s_verify::OnStart(); CHECK(log_has(StringFormat("TKZ: %d chunks identical to the history, 0 chunks with problems", weeks).c_str()), "verify: %s", MqlLog().c_str()); // --- monthly and daily chunks, whole history, verification off s_export::InpFrom = first; s_export::InpTo = last + 1; s_export::InpChunk = s_export::TKZ_CHUNK_MONTH; s_export::InpVerify = false; s_export::InpFile = "month.tkz"; MqlLog().clear(); s_export::OnStart(); check_archive("month.tkz", history(first, last + 1), -1, "export month", 2); s_export::InpChunk = s_export::TKZ_CHUNK_DAY; s_export::InpFile = "day.tkz"; MqlLog().clear(); s_export::OnStart(); int days = 0; for (datetime c = first - first % 86400; c <= last; c += 86400) if (!history(c, c + 86400).empty()) days++; check_archive("day.tkz", history(first, last + 1), days, "export day", 0); // --- custom symbol import s_custom::InpFile = "day.tkz"; s_custom::InpCustom = ""; MqlLog().clear(); s_custom::OnStart(); string custom = sim.symbol + ".tkz"; CHECK(sim.custom.count(custom) == 1, "custom symbol not created: %s", MqlLog().c_str()); { auto &ct = sim.custom[custom]; auto ref = history(first, last + 1); bool ok = ct.size() == ref.size(); for (size_t i = 0; ok && i < ct.size(); ++i) ok = same_tick(ct[i], ref[i]); CHECK(ok, "custom symbol ticks differ (%zu vs %zu)", ct.size(), ref.size()); CHECK(sim.custom_digits[custom] == sim.digits, "custom digits %d", sim.custom_digits[custom]); printf(" custom symbol %s: %zu ticks OK\n", custom.c_str(), ct.size()); } // importing again replaces, it must not duplicate s_custom::OnStart(); CHECK(sim.custom[custom].size() == history(first, last + 1).size(), "re-import duplicated ticks"); // --- self-test script s_selftest::InpHistoryTicks = 300000; MqlLog().clear(); s_selftest::OnStart(); CHECK(log_has("passed, 0 failed"), "self-test: %s", MqlLog().c_str()); CHECK(!log_has("FAIL"), "self-test reported a failure"); { size_t p = MqlLog().find("TKZ self-test finished"); printf(" %s", p == std::string::npos ? "self-test summary missing\n" : MqlLog().substr(p).c_str()); } for (const char *fn : {"EURUSD.tkz", "month.tkz", "day.tkz"}) FileDelete(fn); FileDelete(file); if (g_failures) { printf("SCRIPT TEST FAILURES: %d\n", g_failures); return 1; } printf("ALL SCRIPT TESTS PASSED\n"); return 0; }