| // 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<MqlTick> history(datetime from, datetime to) {
|
| std::vector<MqlTick> 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<MqlTick> &expect, int expect_chunks, const char *what,
|
| int mode) {
|
| CTkzReader r;
|
| CHECK(r.Open(file, false), "%s: cannot open archive", what);
|
| MqlArray<MqlTick> 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;
|
| }
|