| //+------------------------------------------------------------------+
|
| //| 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"
|
| #property version "1.04"
|
| #define TF_VERSION "1.04"
|
| #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
|
| input bool InpGuiAudit = false; // Developer: like InpGuiRun but press "Audit set" (7 symbols named InpTarget + suffix)
|
| 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()
|
| {
|
| PrintFormat("Tick Forge v1.04 | chart %s", _Symbol);
|
| 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();
|
| if(!g_dlg.Create(0, "Tick Forge 1.04", 0, 20, 20, 20 + TF_W, 20 + TF_H))
|
| {
|
| 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);
|
| if(InpGuiAudit)
|
| {
|
| g_dlg.FillAndAudit(InpSource, InpTarget, InpFromDate, InpToDate, InpDataMode);
|
| g_gui_run = true;
|
| }
|
| else if(InpGuiRun)
|
| {
|
| 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);
|
| }
|
| //+------------------------------------------------------------------+
|