2026-11-04 15:03:18 +01:00 | | | //+------------------------------------------------------------------+
|
| | | //| TF_Dev.mq5 - DEVELOPER Expert for Tick Forge (not the product). |
|
| | | //| Runs the same window as the indicator inside an Expert, and adds |
|
| | | //| headless generation + screenshot modes used by tools/tf_run.py. |
|
| | | //| Copy next to TF_Gui.mqh / TF_Engine.mqh / TF_Expr.mqh to compile. |
|
| | | //| Copyright (c) 2026 Joshua Dunnink. MIT License. |
|
| | | //+------------------------------------------------------------------+
|
| | | #property copyright "Copyright (c) 2026 Joshua Dunnink - MIT License"
|
| | | #property link "https://forge.mql5.io/joshuadunnink/Tick_Forge"
|
2026-11-04 15:50:31 +01:00 | | | #property version "1.04"
|
| | | #define TF_VERSION "1.04"
|
2026-11-04 15:03:18 +01:00 | | | #property description "Tick Forge builds custom symbols for the Strategy Tester from your broker's own history."
|
| | | #property description "Shift the timestamps, rescale or reshape the prices with a typed function, test your EA on the result."
|
| | | #property description "Utility: it never trades. Free and open source (MIT). No DLL, no internet access."
|
| | | #property strict
|
| | |
|
| | | #include "TF_Gui.mqh"
|
| | |
|
| | | bool g_gui_run = false;
|
| | |
|
| | | input group "Automation (leave on defaults to use the window)"
|
| | | input bool InpAuto = false; // Run without the window, using the values below
|
| | | input string InpSource = ""; // Source symbol
|
| | | input string InpTarget = ""; // Custom symbol name
|
| | | input string InpFromDate = ""; // First day YYYY.MM.DD
|
| | | input string InpToDate = ""; // Last day YYYY.MM.DD
|
| | | input int InpDataMode = 0; // 0 = real ticks + M1, 1 = M1 bars only
|
| | | input double InpOffsetHours = 0; // Timestamp shift in hours (negative = earlier)
|
| | | input string InpFunction = "p"; // Price function
|
| | | input bool InpScaleSpread = true; // Spread follows the price factor
|
| | | input bool InpOverwrite = false; // Overwrite an existing custom symbol without asking
|
| | | input bool InpCloseTerminal = false; // Automation only: close the terminal when done (flushes the custom symbol list to disk)
|
| | | input bool InpGuiRun = false; // Developer: open the window, fill it from the values above, press GENERATE, report, exit
|
2026-11-04 15:50:31 +01:00 | | | input bool InpGuiAudit = false; // Developer: like InpGuiRun but press "Audit set" (7 symbols named InpTarget + suffix)
|
2026-11-04 15:03:18 +01:00 | | | input string InpScreenshot = ""; // Developer: open the window, preview the values above, save a screenshot (file name), then exit
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | void RunAuto()
|
| | | {
|
| | | CTFJob job;
|
| | | job.src = InpSource; job.name = InpTarget;
|
| | | job.from = StringToTime(InpFromDate + " 00:00"); job.to = StringToTime(InpToDate + " 00:00");
|
| | | job.mode = InpDataMode; job.offset_sec = (long)MathRound(InpOffsetHours * 3600.0);
|
| | | job.expr = InpFunction; job.scale_spread = InpScaleSpread; job.auto_digits = true;
|
| | | bool custom, exists = TF_SymbolExists(job.name, custom);
|
| | | if(job.src == "" || job.name == "" || job.from <= 0 || job.to <= 0) { Print("TickForge: automation needs InpSource, InpTarget, InpFromDate, InpToDate"); return; }
|
| | | if(exists && !custom) { Print("TickForge: '", job.name, "' is a real symbol - refusing"); return; }
|
| | | if(exists && !InpOverwrite) { Print("TickForge: '", job.name, "' exists - set InpOverwrite to replace it"); return; }
|
| | | SymbolSelect(job.src, true);
|
| | | CTFEngine eng; CTFResult res;
|
| | | bool ok = eng.Run(job, res);
|
| | | if(ok)
|
| | | {
|
| | | MqlRates a[], b[]; MqlTick ta[], tb[];
|
| | | datetime fb = job.from + (datetime)job.offset_sec;
|
| | | int na = CopyRates(job.src, PERIOD_M1, job.from + 3600, job.from + 3720, a), nb = CopyRates(job.name, PERIOD_M1, fb + 3600, fb + 3720, b);
|
| | | if(na > 0 && nb > 0)
|
| | | PrintFormat("TICKFORGE_VERIFY bar src %s O=%.2f C=%.2f spr=%d | dst %s O=%.2f C=%.2f spr=%d",
|
| | | TimeToString(a[0].time), a[0].open, a[0].close, a[0].spread, TimeToString(b[0].time), b[0].open, b[0].close, b[0].spread);
|
| | | int ka = CopyTicksRange(job.src, ta, COPY_TICKS_ALL, (ulong)job.from * 1000, (ulong)job.from * 1000 + 60000);
|
| | | int kb = CopyTicksRange(job.name, tb, COPY_TICKS_ALL, (ulong)fb * 1000, (ulong)fb * 1000 + 60000);
|
| | | if(ka > 0 && kb > 0)
|
| | | PrintFormat("TICKFORGE_VERIFY tick n=%d/%d src %s bid=%.2f ask=%.2f | dst %s bid=%.2f ask=%.2f",
|
| | | ka, kb, TimeToString(ta[0].time, TIME_DATE|TIME_SECONDS), ta[0].bid, ta[0].ask, TimeToString(tb[0].time, TIME_DATE|TIME_SECONDS), tb[0].bid, tb[0].ask);
|
| | | }
|
| | | PrintFormat("TICKFORGE_RESULT ok=%d name=%s ticks=%I64d bars=%I64d digits=%d p0=%.5f err=%s bars_path=%s",
|
| | | ok, job.name, res.ticks, res.bars, res.digits, res.p0, res.err, TF_DataPath(job.name));
|
| | | }
|
| | |
|
| | | int OnInit()
|
| | | {
|
2026-11-04 15:50:31 +01:00 | | | PrintFormat("Tick Forge v1.04 | chart %s", _Symbol);
|
2026-11-04 15:03:18 +01:00 | | | if(MQLInfoInteger(MQL_TESTER) || MQLInfoInteger(MQL_OPTIMIZATION))
|
| | | {
|
| | | Print("Tick Forge is a utility for a live chart (it creates symbols); it does nothing in the Strategy Tester.");
|
| | | return INIT_SUCCEEDED;
|
| | | }
|
| | | if(InpAuto)
|
| | | {
|
| | | RunAuto();
|
| | | if(InpCloseTerminal) { Sleep(3000); TerminalClose(0); }
|
| | | else ExpertRemove();
|
| | | return INIT_SUCCEEDED;
|
| | | }
|
| | | g_dlg = new CTickForgeDlg();
|
2026-11-04 15:50:31 +01:00 | | | if(!g_dlg.Create(0, "Tick Forge 1.04", 0, 20, 20, 20 + TF_W, 20 + TF_H))
|
2026-11-04 15:03:18 +01:00 | | | {
|
| | | const string need = StringFormat("Tick Forge needs a chart of at least %d x %d pixels (enlarge the window or maximise the chart).", TF_W + 40, TF_H + 40);
|
| | | Print(need);
|
| | | Comment(need);
|
| | | delete g_dlg; g_dlg = NULL;
|
| | | return INIT_SUCCEEDED;
|
| | | }
|
| | | g_dlg.Run();
|
| | | EventSetMillisecondTimer(30);
|
2026-11-04 15:50:31 +01:00 | | | if(InpGuiAudit)
|
| | | {
|
| | | g_dlg.FillAndAudit(InpSource, InpTarget, InpFromDate, InpToDate, InpDataMode);
|
| | | g_gui_run = true;
|
| | | }
|
| | | else if(InpGuiRun)
|
2026-11-04 15:03:18 +01:00 | | | {
|
| | | g_dlg.FillAndGenerate(InpSource, InpTarget, InpFromDate, InpToDate, InpDataMode, InpOffsetHours, InpFunction);
|
| | | g_gui_run = true;
|
| | | }
|
| | | if(InpScreenshot != "")
|
| | | {
|
| | | g_dlg.ShotPreview(InpSource, InpFromDate, InpToDate, InpOffsetHours, InpFunction);
|
| | | ChartRedraw(); Sleep(1500);
|
| | | PrintFormat("TICKFORGE_SHOT %s ok=%d chart=%dx%d", InpScreenshot, ChartScreenShot(0, InpScreenshot, 1100, 700),
|
| | | (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS), (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS));
|
| | | ExpertRemove();
|
| | | }
|
| | | return INIT_SUCCEEDED;
|
| | | }
|
| | |
|
| | | void OnDeinit(const int reason)
|
| | | {
|
| | | EventKillTimer();
|
| | | if(g_dlg != NULL) { g_dlg.Destroy(reason); delete g_dlg; g_dlg = NULL; }
|
| | | Comment("");
|
| | | }
|
| | |
|
| | | void OnTick() {}
|
| | |
|
| | | void OnTimer()
|
| | | {
|
| | | if(g_dlg == NULL) return;
|
| | | g_dlg.Tick();
|
| | | if(g_gui_run && !g_dlg.Busy())
|
| | | {
|
| | | g_gui_run = false;
|
| | | bool custom, ex = TF_SymbolExists(InpTarget, custom);
|
| | | PrintFormat("TICKFORGE_RESULT guirun exists=%d bars_in_tester_view=%d status=%s", ex, (int)Bars(InpTarget, PERIOD_M1), g_dlg.Status());
|
| | | if(InpCloseTerminal) { TerminalClose(0); return; }
|
| | | }
|
| | | if(g_dlg.Closed()) ExpertRemove();
|
| | | }
|
| | |
|
| | | void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
|
| | | {
|
| | | if(g_dlg != NULL) g_dlg.ChartEvent(id, lparam, dparam, sparam);
|
| | | }
|
| | | //+------------------------------------------------------------------+
|