Tick_Forge/TF_Gui.mqh
//+------------------------------------------------------------------+
//| TF_Gui.mqh - Tick Forge window (CAppDialog) and overview plot |
//| Copyright (c) 2026 Joshua Dunnink. MIT License (see LICENSE). |
//| Shared by the indicator (Tick_Forge.mq5) and the developer Expert.|
//| No Sleep() and no MessageBox() (not allowed in indicators): long |
//| work runs one day / one month per Tick() call from a host timer. |
//+------------------------------------------------------------------+
#include <Controls\Dialog.mqh>
#include <Controls\Button.mqh>
#include <Controls\Edit.mqh>
#include <Controls\Label.mqh>
#include <Controls\ComboBox.mqh>
#include <Controls\CheckBox.mqh>
#include <Canvas\Canvas.mqh>
#include "TF_Engine.mqh"
#define TF_W 1030
#define TF_CW 540
#define TF_H 596
#define TF_PLOT_H 554
#define TF_LBL_W 150
#define TF_X 10
string g_presets_txt[] = {"Original prices (control)", "Multiply x2", "Multiply x0.5", "Squared move (extreme)", "Ramp x1 -> x2 over range", "Offset +1000", "Inverted move"};
string g_presets_fn[] = {"p", "p*2", "p*0.5", "p0*r^2", "p*(1+t)", "p+1000", "p0*p0/p"};
//+------------------------------------------------------------------+
//| Overview plot: source and result price over the whole range |
//+------------------------------------------------------------------+
class CTFPlot : public CWnd
{
private:
CCanvas m_cv;
string m_obj;
bool m_ok;
void Panel(const int top, const int h, const string title, const datetime &t[], const double &v[], const int n,
const long shift, const uint clr);
public:
CTFPlot() : m_ok(false) {}
virtual bool Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2);
virtual bool OnMove(void);
virtual bool OnShow(void) { if(m_ok) ObjectSetInteger(m_chart_id, m_obj, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); return true; }
virtual bool OnHide(void) { if(m_ok) ObjectSetInteger(m_chart_id, m_obj, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); return true; }
virtual bool OnDestroy(void) { if(m_ok) { m_cv.Destroy(); m_ok = false; } return true; }
void Hint(const string a, const string b = "");
void Lines(const string a, const string b = "", const string c = "");
void Para(const string a, const string b = "");
void Draw(const datetime &t[], const double &o[], const double &v[], const int n, const long shift,
const string srcTitle, const string dstTitle, const string note);
};
bool CTFPlot::Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2)
{
if(!CWnd::Create(chart, name, subwin, x1, y1, x2, y2)) return false;
m_obj = name + "cv";
m_ok = m_cv.CreateBitmapLabel(chart, subwin, m_obj, x1, y1, x2 - x1, y2 - y1, COLOR_FORMAT_ARGB_NORMALIZE);
if(m_ok) { ObjectSetInteger(chart, m_obj, OBJPROP_SELECTABLE, false); Hint("Press Preview to see the whole range:", "source on top, result below."); }
return m_ok;
}
bool CTFPlot::OnMove(void)
{
if(!m_ok) return true;
ObjectSetInteger(m_chart_id, m_obj, OBJPROP_XDISTANCE, Left());
ObjectSetInteger(m_chart_id, m_obj, OBJPROP_YDISTANCE, Top());
return true;
}
void CTFPlot::Hint(const string a, const string b)
{
if(!m_ok) return;
m_cv.Erase(ColorToARGB(clrWhite, 255));
m_cv.Rectangle(0, 0, m_cv.Width() - 1, m_cv.Height() - 1, ColorToARGB(C'150,150,150', 255));
m_cv.FontSet("Arial", -100);
m_cv.TextOut(m_cv.Width() / 2, m_cv.Height() / 2 - 14, a, ColorToARGB(C'90,90,90', 255), TA_CENTER);
m_cv.TextOut(m_cv.Width() / 2, m_cv.Height() / 2 + 6, b, ColorToARGB(C'90,90,90', 255), TA_CENTER);
m_cv.Update();
}
void CTFPlot::Para(const string a, const string b)
{
if(!m_ok) return;
m_cv.Erase(0);
m_cv.FontSet("Arial", -80);
const uint clr = ColorToARGB(C'40,40,40', 255);
int y = 0;
for(int pass = 0; pass < 2; pass++)
{
string rest = (pass == 0 ? a : b);
while(StringLen(rest) > 0 && y < m_cv.Height() - 12)
{
string line = rest;
if(StringLen(rest) > 92)
{
int cut = 92;
while(cut > 40 && StringGetCharacter(rest, cut) != ' ') cut--;
line = StringSubstr(rest, 0, cut);
rest = StringSubstr(rest, cut + 1);
}
else rest = "";
m_cv.TextOut(0, y, line, clr, TA_LEFT);
y += 14;
}
}
m_cv.Update();
}
void CTFPlot::Lines(const string a, const string b, const string c)
{
if(!m_ok) return;
m_cv.Erase(0);
m_cv.FontSet("Arial", -70);
const uint clr = ColorToARGB(C'60,60,60', 255);
m_cv.TextOut(0, 0, a, clr, TA_LEFT);
if(b != "") m_cv.TextOut(0, 16, b, clr, TA_LEFT);
if(c != "") m_cv.TextOut(0, 32, c, clr, TA_LEFT);
m_cv.Update();
}
void CTFPlot::Panel(const int top, const int h, const string title, const datetime &t[], const double &v[], const int n,
const long shift, const uint clr)
{
const int L = 62, R = m_cv.Width() - 10, B = top + h - 22, T = top + 18;
double lo = v[0], hi = v[0];
for(int i = 1; i < n; i++) { if(v[i] < lo) lo = v[i]; if(v[i] > hi) hi = v[i]; }
double pad = (hi - lo) * 0.05; if(pad <= 0.0) pad = MathMax(MathAbs(hi) * 0.01, 1e-9);
lo -= pad; hi += pad;
m_cv.FontSet("Arial", -80);
m_cv.TextOut(L, top, title, ColorToARGB(C'30,30,30', 255), TA_LEFT);
uint grid = ColorToARGB(C'225,225,225', 255), axis = ColorToARGB(C'120,120,120', 255);
for(int g = 0; g <= 4; g++)
{
int y = B - (B - T) * g / 4;
m_cv.Line(L, y, R, y, grid);
m_cv.TextOut(L - 4, y - 6, DoubleToString(lo + (hi - lo) * g / 4.0, hi > 1000 ? 0 : hi > 10 ? 2 : 5), ColorToARGB(C'80,80,80', 255), TA_RIGHT);
}
m_cv.Rectangle(L, T, R, B, axis);
for(int g = 0; g <= 4; g++)
{
int idx = (n - 1) * g / 4, x = L + (R - L) * idx / MathMax(n - 1, 1);
m_cv.Line(x, B, x, B + 3, axis);
m_cv.TextOut(x, B + 4, TimeToString((datetime)(t[idx] + shift), TIME_DATE), ColorToARGB(C'80,80,80', 255), g == 0 ? TA_LEFT : g == 4 ? TA_RIGHT : TA_CENTER);
}
int px = 0, py = 0;
for(int i = 0; i < n; i++)
{
int x = L + (int)((double)(R - L) * i / MathMax(n - 1, 1));
int y = B - (int)((B - T) * (v[i] - lo) / (hi - lo));
if(i > 0) { m_cv.Line(px, py, x, y, clr); m_cv.Line(px, py + 1, x, y + 1, clr); }
px = x; py = y;
}
}
void CTFPlot::Draw(const datetime &t[], const double &o[], const double &v[], const int n, const long shift,
const string srcTitle, const string dstTitle, const string note)
{
if(!m_ok || n < 2) return;
m_cv.Erase(ColorToARGB(clrWhite, 255));
m_cv.Rectangle(0, 0, m_cv.Width() - 1, m_cv.Height() - 1, ColorToARGB(C'150,150,150', 255));
const int h = (m_cv.Height() - 30) / 2;
Panel(4, h, srcTitle, t, o, n, 0, ColorToARGB(C'90,90,90', 255));
Panel(4 + h + 4, h, dstTitle, t, v, n, shift, ColorToARGB(C'20,110,60', 255));
m_cv.FontSet("Arial", -70);
m_cv.TextOut(8, m_cv.Height() - 16, note, ColorToARGB(C'90,90,90', 255), TA_LEFT);
m_cv.Update();
}
//+------------------------------------------------------------------+
class CTickForgeDlg : public CAppDialog
{
private:
CLabel m_l[16];
CComboBox m_src, m_mode, m_unit, m_preset;
CEdit m_symname, m_from, m_to, m_off, m_fn;
CTFPlot m_hint, m_paths; // canvas text blocks (a CLabel clips long text)
CCheckBox m_spread, m_digits;
CButton m_btnPrev, m_btnScan, m_btnFull, m_btnGo, m_btnAudit, m_btnDel;
CLabel m_avail, m_prev;
CTFPlot m_status; // wrapped status text (a CLabel clips long text)
string m_sa, m_sb;
CTFPlot m_plot;
bool m_busy;
bool m_closed;
CTFEngine m_eng; // stepwise generator, driven by Tick() from the host timer
CTFResult m_res;
bool m_running;
bool m_audit; // audit-set run in progress
int m_ai; // index of the audit symbol being written
CTFJob m_ajob;
string m_abase;
string m_confirm, m_jname; // overwrite confirmation (second click), symbol being written
bool m_scanning;
datetime m_sm, m_sto, m_srs, m_sl;
int m_srun;
string m_sres, m_ssrc;
bool AddLabel(const int idx, const string text, const int y);
bool BuildJob(CTFJob &job, string &err);
void RefreshAvail();
void RefreshPaths();
void Say(const string a, const string b = "");
void OnChangeSource();
void OnChangePreset();
void OnEndEditName() { ClearConfirm(); RefreshPaths(); }
void ClearConfirm() { if(m_confirm != "") { m_confirm = ""; m_btnGo.Text("GENERATE SYMBOL"); AuditButtonsReset(); } }
void ScanStep();
void OnClickFull();
void OnClickPreview();
void OnClickScan();
void OnClickGenerate();
void OnClickAudit();
void OnClickDeleteAudit();
bool StartAuditJob();
bool WriteManifest();
void AuditButtonsReset();
public:
CTickForgeDlg() : m_busy(false), m_closed(false), m_running(false), m_audit(false), m_ai(0), m_scanning(false), m_srun(0) {}
virtual bool Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2);
virtual bool OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam);
virtual void OnClickButtonClose(void) { m_closed = true; } // the host program removes itself
bool Closed() const { return m_closed; }
void Tick(); // call from the host timer (about every 30 ms)
void SetStatus(const string s) { m_sa = s; m_status.Para(m_sa, m_sb); ChartRedraw(); }
string Status() { return m_sa; }
bool Busy() const { return m_running || m_scanning; }
// developer hook: fill the form and press GENERATE exactly as a user would (twice, to pass the overwrite confirmation)
void FillAndGenerate(const string src, const string name, const string from, const string to, const int mode, const double offh, const string fn)
{
m_src.SelectByText(src);
OnChangeSource();
m_symname.Text(name);
m_from.Text(from); m_to.Text(to);
m_mode.Select(mode);
m_unit.Select(0);
m_off.Text(DoubleToString(offh, 2));
m_fn.Text(fn);
OnClickGenerate();
OnClickGenerate();
}
void FillAndAudit(const string src, const string name, const string from, const string to, const int mode)
{
m_src.SelectByText(src);
OnChangeSource();
m_symname.Text(name);
m_from.Text(from); m_to.Text(to);
m_mode.Select(mode);
m_unit.Select(0);
m_off.Text("0"); m_fn.Text("p");
OnClickAudit();
OnClickAudit();
}
void ShotPreview(const string src, const string from, const string to, const double offh, const string fn)
{
if(src != "") m_src.SelectByText(src);
OnChangeSource();
if(from != "") m_from.Text(from);
if(to != "") m_to.Text(to);
m_off.Text(DoubleToString(offh, 2));
for(int i = 0; i < ArraySize(g_presets_fn); i++) if(g_presets_fn[i] == fn) m_preset.Select(i);
m_fn.Text(fn);
OnClickPreview();
}
};
EVENT_MAP_BEGIN(CTickForgeDlg)
ON_EVENT(ON_CHANGE, m_src, OnChangeSource)
ON_EVENT(ON_CHANGE, m_preset, OnChangePreset)
ON_EVENT(ON_END_EDIT, m_symname, OnEndEditName)
ON_EVENT(ON_CLICK, m_btnFull, OnClickFull)
ON_EVENT(ON_CLICK, m_btnPrev, OnClickPreview)
ON_EVENT(ON_CLICK, m_btnScan, OnClickScan)
ON_EVENT(ON_CLICK, m_btnGo, OnClickGenerate)
ON_EVENT(ON_CLICK, m_btnAudit, OnClickAudit)
ON_EVENT(ON_CLICK, m_btnDel, OnClickDeleteAudit)
EVENT_MAP_END(CAppDialog)
CTickForgeDlg *g_dlg = NULL;
void TF_Status(const string s)
{
Print("TickForge: ", s);
if(g_dlg != NULL) g_dlg.SetStatus(s);
}
bool CTickForgeDlg::AddLabel(const int idx, const string text, const int y)
{
if(!m_l[idx].Create(m_chart_id, m_name + "L" + IntegerToString(idx), m_subwin, TF_X, y + 4, TF_X + TF_LBL_W, y + 24)) return false;
m_l[idx].Text(text);
return Add(m_l[idx]);
}
bool CTickForgeDlg::Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2)
{
if(!CAppDialog::Create(chart, name, subwin, x1, y1, x2, y2)) return false;
const int X2 = TF_X + TF_LBL_W + 5; // control column
const int XR = TF_CW - 30; // right edge
int y = 8;
AddLabel(0, "Source symbol", y);
m_src.Create(m_chart_id, m_name + "src", m_subwin, X2, y, XR, y + 24); Add(m_src);
for(int i = 0, k = 0; i < SymbolsTotal(true); i++)
{
string s = SymbolName(i, true);
if((bool)SymbolInfoInteger(s, SYMBOL_CUSTOM)) continue;
m_src.ItemAdd(s, k++);
}
m_src.Select(0);
string cur = Symbol();
m_src.SelectByText(cur);
y += 30;
AddLabel(1, "Custom symbol name", y);
m_symname.Create(m_chart_id, m_name + "nm", m_subwin, X2, y, XR, y + 24); Add(m_symname);
y += 30;
AddLabel(2, "Broker data available", y);
m_avail.Create(m_chart_id, m_name + "av", m_subwin, X2, y + 4, XR, y + 24); m_avail.Text(""); Add(m_avail);
y += 30;
AddLabel(3, "From (YYYY.MM.DD)", y);
m_from.Create(m_chart_id, m_name + "fr", m_subwin, X2, y, X2 + 110, y + 24); Add(m_from);
m_l[12].Create(m_chart_id, m_name + "L12", m_subwin, X2 + 120, y + 4, X2 + 170, y + 24); m_l[12].Text("to"); Add(m_l[12]);
m_to.Create(m_chart_id, m_name + "to", m_subwin, X2 + 140, y, X2 + 250, y + 24); Add(m_to);
m_btnFull.Create(m_chart_id, m_name + "bf", m_subwin, X2 + 262, y, XR, y + 24); m_btnFull.Text("Use full range"); Add(m_btnFull);
y += 30;
AddLabel(4, "Data type", y);
m_mode.Create(m_chart_id, m_name + "md", m_subwin, X2, y, XR, y + 24); Add(m_mode);
m_mode.ItemAdd("Real ticks (transformed) + M1 bars", 0);
m_mode.ItemAdd("M1 bars only (tester builds the ticks)", 1);
m_mode.Select(0);
y += 30;
AddLabel(5, "Time offset", y);
m_off.Create(m_chart_id, m_name + "of", m_subwin, X2, y, X2 + 90, y + 24); m_off.Text("0"); Add(m_off);
m_unit.Create(m_chart_id, m_name + "un", m_subwin, X2 + 100, y, X2 + 200, y + 24); Add(m_unit);
m_unit.ItemAdd("hours", 0); m_unit.ItemAdd("days", 1); m_unit.Select(0);
m_l[13].Create(m_chart_id, m_name + "L13", m_subwin, X2 + 210, y + 4, XR, y + 24); m_l[13].Text("raw clock shift, - = earlier"); Add(m_l[13]);
y += 30;
AddLabel(6, "Price function preset", y);
m_preset.Create(m_chart_id, m_name + "ps", m_subwin, X2, y, XR, y + 24); Add(m_preset);
for(int i = 0; i < ArraySize(g_presets_txt); i++) m_preset.ItemAdd(g_presets_txt[i], i);
m_preset.Select(0);
y += 30;
AddLabel(7, "New price = f(...)", y);
m_fn.Create(m_chart_id, m_name + "fn", m_subwin, X2, y, XR, y + 24); m_fn.Text("p"); Add(m_fn);
y += 26;
m_hint.Create(m_chart_id, m_name + "hint", m_subwin, TF_X, y, XR, y + 16); Add(m_hint);
m_hint.Lines("p price | p0 first price | r = p/p0 | t 0..1 over range | d days + - * / ^ ( ) abs sqrt ln log10 exp sin cos pow min max");
y += 26;
m_spread.Create(m_chart_id, m_name + "sp", m_subwin, X2, y, XR, y + 22); m_spread.Text("Spread follows the price factor"); m_spread.Checked(true); Add(m_spread);
y += 24;
m_digits.Create(m_chart_id, m_name + "dg", m_subwin, X2, y, XR, y + 22); m_digits.Text("Add digits when prices shrink"); m_digits.Checked(true); Add(m_digits);
y += 30;
AddLabel(8, "Old -> new price", y);
m_prev.Create(m_chart_id, m_name + "pv", m_subwin, X2, y + 4, XR, y + 24); m_prev.Text(""); Add(m_prev);
y += 34;
m_btnPrev.Create(m_chart_id, m_name + "b1", m_subwin, TF_X, y, TF_X + 120, y + 28); m_btnPrev.Text("Preview"); Add(m_btnPrev);
m_btnScan.Create(m_chart_id, m_name + "b2", m_subwin, TF_X + 130, y, TF_X + 290, y + 28); m_btnScan.Text("Scan tick availability"); Add(m_btnScan);
m_btnGo.Create(m_chart_id, m_name + "b3", m_subwin, TF_X + 300, y, XR, y + 28); m_btnGo.Text("GENERATE SYMBOL"); m_btnGo.ColorBackground(C'30,120,60'); m_btnGo.Color(clrWhite); Add(m_btnGo);
y += 34;
m_btnAudit.Create(m_chart_id, m_name + "b4", m_subwin, TF_X, y, TF_X + 250, y + 28); m_btnAudit.Text("Audit set (7 symbols)"); Add(m_btnAudit);
m_btnDel.Create(m_chart_id, m_name + "b5", m_subwin, TF_X + 260, y, XR, y + 28); m_btnDel.Text("Delete audit set"); Add(m_btnDel);
y += 38;
m_paths.Create(m_chart_id, m_name + "paths", m_subwin, TF_X, y, XR, y + 50); Add(m_paths);
y += 52;
m_status.Create(m_chart_id, m_name + "status", m_subwin, TF_X, y, XR, y + 60); Add(m_status);
Say("Ready.");
m_plot.Create(m_chart_id, m_name + "plot", m_subwin, TF_CW + 10, 8, TF_W - 40, 8 + TF_PLOT_H); Add(m_plot);
OnChangeSource();
return true;
}
void CTickForgeDlg::Say(const string a, const string b)
{
m_sa = a; m_sb = b; m_status.Para(a, b); ChartRedraw();
}
void CTickForgeDlg::OnChangeSource()
{
ClearConfirm();
string s = m_src.Select();
if(s != "") m_symname.Text(s + "_TF");
RefreshAvail();
RefreshPaths();
}
void CTickForgeDlg::OnChangePreset()
{
int i = (int)m_preset.Value();
if(i >= 0 && i < ArraySize(g_presets_fn)) m_fn.Text(g_presets_fn[i]);
}
void CTickForgeDlg::RefreshPaths()
{
string n = m_symname.Text(), root = TerminalInfoString(TERMINAL_DATA_PATH);
if(StringLen(root) > 92) root = StringSubstr(root, 0, 28) + " ... " + StringSubstr(root, StringLen(root) - 58);
m_paths.Lines("Data folder: " + root,
"Bars stored in: <data folder>\\bases\\Custom\\history\\" + n,
"Ticks stored in: <data folder>\\bases\\Custom\\ticks\\" + n);
}
void CTickForgeDlg::RefreshAvail()
{
string s = m_src.Select();
if(s == "") return;
SymbolSelect(s, true);
long a = SeriesInfoInteger(s, PERIOD_M1, SERIES_SERVER_FIRSTDATE);
long b = SeriesInfoInteger(s, PERIOD_M1, SERIES_LASTBAR_DATE);
if(a <= 0) { Bars(s, PERIOD_M1); a = SeriesInfoInteger(s, PERIOD_M1, SERIES_SERVER_FIRSTDATE); b = SeriesInfoInteger(s, PERIOD_M1, SERIES_LASTBAR_DATE); }
if(a > 0)
{
m_avail.Text("M1 bars " + TimeToString((datetime)a, TIME_DATE) + " .. " + TimeToString((datetime)b, TIME_DATE) + " (ticks: use Scan)");
m_from.Text(TimeToString((datetime)MathMax(a, (long)(b - 30 * 86400)), TIME_DATE));
m_to.Text(TimeToString((datetime)b, TIME_DATE));
}
else m_avail.Text("no M1 history yet - open a chart of this symbol first");
}
void CTickForgeDlg::OnClickFull()
{
string s = m_src.Select();
long a = SeriesInfoInteger(s, PERIOD_M1, SERIES_SERVER_FIRSTDATE);
long b = SeriesInfoInteger(s, PERIOD_M1, SERIES_LASTBAR_DATE);
if(a > 0) { m_from.Text(TimeToString((datetime)a, TIME_DATE)); m_to.Text(TimeToString((datetime)b, TIME_DATE)); }
}
bool CTickForgeDlg::BuildJob(CTFJob &job, string &err)
{
err = "";
job.src = m_src.Select();
job.name = m_symname.Text();
StringTrimLeft(job.name); StringTrimRight(job.name);
if(job.src == "") { err = "choose a source symbol"; return false; }
if(StringLen(job.name) == 0 || StringLen(job.name) > 31) { err = "symbol name must be 1-31 characters"; return false; }
for(int i = 0; i < StringLen(job.name); i++)
{
ushort c = job.name[i];
bool ok = (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' || c == '.' || c == '-' || c == '#';
if(!ok) { err = "symbol name: letters, digits, _ . - # only"; return false; }
}
job.from = StringToTime(m_from.Text() + " 00:00");
job.to = StringToTime(m_to.Text() + " 00:00");
if(job.from <= 0 || job.to <= 0) { err = "dates must look like 2024.03.15"; return false; }
if(job.to < job.from) { err = "end date is before start date"; return false; }
job.mode = (int)m_mode.Value();
double v = StringToDouble(m_off.Text());
job.offset_sec = (long)MathRound(v * (m_unit.Value() == 1 ? 86400.0 : 3600.0));
job.expr = m_fn.Text();
job.scale_spread = m_spread.Checked();
job.auto_digits = m_digits.Checked();
return true;
}
void CTickForgeDlg::OnClickPreview()
{
CTFJob job; string err;
if(!BuildJob(job, err)) { Say("Cannot preview: " + err); return; }
Say("Reading price history for the overview...");
CTFEngine eng; datetime t[]; double o[], v[]; string tfn;
if(!eng.Series(job, t, o, v, tfn, err)) { Say("Cannot preview: " + err); m_prev.Text(""); m_plot.Hint("Cannot preview:", err); return; }
int n = ArraySize(t);
int dg = (int)SymbolInfoInteger(job.src, SYMBOL_DIGITS);
m_prev.Text("start " + DoubleToString(o[0], dg) + " -> " + DoubleToString(v[0], dg) + " end " + DoubleToString(o[n - 1], dg) + " -> " + DoubleToString(v[n - 1], dg));
string off = (job.offset_sec == 0 ? "no time shift" : StringFormat("shifted %+.1f h", job.offset_sec / 3600.0));
m_plot.Draw(t, o, v, n, job.offset_sec, "Source " + job.src + " (broker dates)",
"Result " + job.name + " f(p) = " + job.expr + " " + off,
StringFormat("%d points, %s closes, whole range. Each panel has its own price axis.", n, tfn));
if(job.offset_sec > 0 && (long)job.to + 86400 + job.offset_sec > (long)TimeCurrent())
Say("Preview OK - check the shape in the panel on the right.", "NOTE: with this shift the data ends after today; the Strategy Tester can only use the part up to today.");
else
Say("Preview OK - check the shape in the panel on the right.", "");
}
void CTickForgeDlg::OnClickScan()
{
if(m_scanning || m_running) return;
CTFJob job; string err;
if(!BuildJob(job, err)) { Say("Cannot scan: " + err); return; }
m_ssrc = job.src; m_sto = job.to; m_sres = ""; m_srun = 0; m_srs = 0; m_sl = 0;
MqlDateTime d; TimeToStruct(job.from, d); d.day = 1; d.hour = d.min = d.sec = 0;
m_sm = StructToTime(d);
m_scanning = true;
m_btnScan.Text("Scanning...");
Say("Scanning tick history month by month (may download from the broker)...");
}
void CTickForgeDlg::ScanStep()
{
MqlTick tk[];
MqlDateTime e; TimeToStruct(m_sm, e); e.mon++; if(e.mon > 12) { e.mon = 1; e.year++; }
datetime nx = StructToTime(e);
bool has = (CopyTicks(m_ssrc, tk, COPY_TICKS_ALL, (ulong)m_sm * 1000, 1) > 0 && tk[0].time < nx);
if(has) { if(m_srun == 0) m_srs = m_sm; m_srun++; m_sl = m_sm; }
else if(m_srun > 0) { m_sres += TimeToString(m_srs, TIME_DATE) + ".." + TimeToString(m_sl, TIME_DATE) + " "; m_srun = 0; }
m_sm = nx;
if(m_sm <= m_sto) return;
if(m_srun > 0) m_sres += TimeToString(m_srs, TIME_DATE) + ".." + TimeToString(m_sl, TIME_DATE);
m_scanning = false;
m_btnScan.Text("Scan tick availability");
Say(m_sres == "" ? "No ticks for this range - use 'M1 bars only'." : "Months with ticks: " + m_sres, "Format: first day of first month .. first day of last month");
}
void CTickForgeDlg::OnClickGenerate()
{
if(m_running || m_scanning) return;
CTFJob job; string err;
if(!BuildJob(job, err)) { Say("Cannot generate: " + err); return; }
bool custom, exists = TF_SymbolExists(job.name, custom);
if(exists && !custom) { Say("'" + job.name + "' is a real broker symbol - choose another name."); return; }
if(exists && m_confirm != job.name)
{
m_confirm = job.name;
m_btnGo.Text("CONFIRM OVERWRITE");
Say("Custom symbol '" + job.name + "' already exists.", "Press the button again to overwrite all of its bars and ticks, or change the name.");
return;
}
ClearConfirm();
m_btnGo.Text("Working...");
Say("Starting...");
m_jname = job.name;
if(!m_eng.Begin(job, m_res))
{
m_btnGo.Text("GENERATE SYMBOL");
Say("FAILED: " + m_res.err, "Nothing was changed.");
return;
}
m_running = true;
}
//--- audit set: control + calendar shifts + price level + mirror, one confirmed click ---------------
string g_audit_sfx[] = {"_C", "_S7", "_S14", "_S28", "_L50", "_L200", "_MIR"};
string g_audit_lbl[] = {"control (no change)", "shifted +7 days", "shifted +14 days", "shifted +28 days", "prices x0.5", "prices x2", "mirrored moves"};
string g_audit_fn[] = {"p", "p", "p", "p", "p*0.5", "p*2", "p0*p0/p"};
int g_audit_day[] = {0, 7, 14, 28, 0, 0, 0};
void CTickForgeDlg::AuditButtonsReset()
{
m_btnAudit.Text("Audit set (7 symbols)");
m_btnDel.Text("Delete audit set");
}
void CTickForgeDlg::OnClickAudit()
{
if(m_running || m_scanning) return;
CTFJob job; string err;
if(!BuildJob(job, err)) { Say("Cannot build the audit set: " + err); return; }
if(StringLen(job.name) > 25) { Say("Cannot build the audit set: the name must be 25 characters or less (suffixes are added)."); return; }
const int n = ArraySize(g_audit_sfx);
int existing = 0;
for(int i = 0; i < n; i++)
{
bool custom, ex = TF_SymbolExists(job.name + g_audit_sfx[i], custom);
if(ex && !custom) { Say("'" + job.name + g_audit_sfx[i] + "' is a real broker symbol - choose another base name."); return; }
if(ex) existing++;
}
const string key = "AUDIT:" + job.name;
if(m_confirm != key)
{
ClearConfirm();
m_confirm = key;
m_btnAudit.Text("CONFIRM AUDIT SET");
const int days = (int)((job.to - job.from) / 86400) + 1;
Say(StringFormat("Audit set: %d symbols (%s_C, _S7, _S14, _S28, _L50, _L200, _MIR), %d days of %s each.", n, job.name, days,
job.mode == TF_MODE_TICKS ? "real ticks" : "M1 bars"),
(existing > 0 ? StringFormat("%d already exist and will be overwritten. ", existing) : "") +
((long)job.to + 86400 + 28 * 86400 > (long)TimeCurrent() ? "NOTE: the +14/+28 day symbols end after today; the tester only uses data up to today, so end the range earlier for a full comparison. " : "") +
"Each symbol uses the disk space of one generated symbol. Press again to create; 'Delete audit set' removes them.");
return;
}
ClearConfirm();
m_ajob = job; m_abase = job.name; m_ai = 0; m_audit = true;
StartAuditJob();
}
bool CTickForgeDlg::StartAuditJob()
{
CTFJob job = m_ajob;
job.name = m_abase + g_audit_sfx[m_ai];
job.offset_sec = (long)g_audit_day[m_ai] * 86400;
job.expr = g_audit_fn[m_ai];
m_jname = job.name;
m_btnAudit.Text(StringFormat("Audit %d/%d...", m_ai + 1, ArraySize(g_audit_sfx)));
Say(StringFormat("Audit set %d/%d: %s - %s", m_ai + 1, ArraySize(g_audit_sfx), job.name, g_audit_lbl[m_ai]));
if(!m_eng.Begin(job, m_res))
{
Say("FAILED at " + job.name + ": " + m_res.err, "Symbols made so far stay. Fix the cause and press Audit set again.");
m_audit = false; m_running = false;
AuditButtonsReset();
return false;
}
m_running = true;
return true;
}
bool CTickForgeDlg::WriteManifest()
{
FolderCreate("TickForge"); // inside MQL5\Files
const string fn = "TickForge\\audit_" + m_abase + ".json";
const int h = FileOpen(fn, FILE_WRITE | FILE_TXT | FILE_ANSI);
if(h == INVALID_HANDLE) return false;
const int n = ArraySize(g_audit_sfx);
FileWriteString(h, "{\n \"tool\": \"Tick Forge\",\n \"version\": \"1.04\",\n");
FileWriteString(h, " \"source\": \"" + m_ajob.src + "\",\n");
FileWriteString(h, " \"from\": \"" + TimeToString(m_ajob.from, TIME_DATE) + "\",\n");
FileWriteString(h, " \"to\": \"" + TimeToString(m_ajob.to, TIME_DATE) + "\",\n");
FileWriteString(h, StringFormat(" \"mode\": %d,\n \"scale_spread\": %s,\n \"symbols\": [\n", m_ajob.mode, m_ajob.scale_spread ? "true" : "false"));
for(int i = 0; i < n; i++)
FileWriteString(h, StringFormat(" {\"name\": \"%s\", \"label\": \"%s\", \"offset_sec\": %d, \"expr\": \"%s\"}%s\n",
m_abase + g_audit_sfx[i], g_audit_lbl[i], g_audit_day[i] * 86400, g_audit_fn[i], i < n - 1 ? "," : ""));
FileWriteString(h, " ]\n}\n");
FileClose(h);
return true;
}
void CTickForgeDlg::OnClickDeleteAudit()
{
if(m_running || m_scanning) return;
CTFJob job; string err;
if(!BuildJob(job, err)) { Say("Cannot delete: " + err); return; }
const int n = ArraySize(g_audit_sfx);
int found = 0;
for(int i = 0; i < n; i++)
{
const string nm = job.name + g_audit_sfx[i];
bool custom;
if(!TF_SymbolExists(nm, custom) || !custom) continue;
SymbolSelect(nm, true);
if(StringFind(SymbolInfoString(nm, SYMBOL_PATH), "Tick Forge") == 0) found++;
}
if(found == 0) { Say("No audit-set symbols named " + job.name + "_C, _S7 ... were found."); return; }
const string key = "DEL:" + job.name;
if(m_confirm != key)
{
ClearConfirm();
m_confirm = key;
m_btnDel.Text("CONFIRM DELETE");
Say(StringFormat("%d symbol(s) named %s_... and all their bars and ticks will be deleted.", found, job.name),
"Only symbols created by Tick Forge are touched. Press again to delete.");
return;
}
ClearConfirm();
int gone = 0, failed = 0;
for(int i = 0; i < n; i++)
{
const string nm = job.name + g_audit_sfx[i];
bool custom;
if(!TF_SymbolExists(nm, custom) || !custom) continue;
SymbolSelect(nm, true);
if(StringFind(SymbolInfoString(nm, SYMBOL_PATH), "Tick Forge") != 0) continue;
SymbolSelect(nm, false);
CustomRatesDelete(nm, 0, LONG_MAX);
CustomTicksDelete(nm, 0, LONG_MAX);
if(CustomSymbolDelete(nm)) gone++; else failed++;
}
FileDelete("TickForge\\audit_" + job.name + ".json");
Say(StringFormat("Deleted %d symbol(s).", gone), failed > 0 ? StringFormat("%d could not be deleted (open in a chart or the tester?). Close them and try again.", failed) : "");
}
void CTickForgeDlg::Tick()
{
if(m_scanning) { ScanStep(); return; }
if(!m_running) return;
if(!m_eng.Step(m_res))
{
m_running = false;
const bool was_audit = m_audit;
m_audit = false;
m_btnGo.Text("GENERATE SYMBOL");
AuditButtonsReset();
Say("FAILED: " + m_res.err, was_audit ? "Symbols made so far stay. Press Audit set again after fixing the cause."
: "The partly written symbol '" + m_jname + "' can be regenerated (it will ask before overwriting).");
return;
}
if(!m_eng.Done()) return;
SymbolSelect(m_jname, true);
Print("TickForge DONE ", m_jname, " ticks=", m_res.ticks, " bars=", m_res.bars, " digits=", m_res.digits, " p0=", m_res.p0);
if(m_audit)
{
m_ai++;
if(m_ai < ArraySize(g_audit_sfx)) { StartAuditJob(); return; }
m_audit = false; m_running = false;
AuditButtonsReset();
const bool wrote = WriteManifest();
Say(StringFormat("DONE: audit set %s_C ... %s_MIR (%d symbols).", m_abase, m_abase, ArraySize(g_audit_sfx)),
wrote ? "Manifest written to MQL5\\Files\\TickForge. Test your EA on each symbol, control first." : "Could not write the manifest file.");
RefreshPaths();
return;
}
m_running = false;
m_btnGo.Text("GENERATE SYMBOL");
Say(StringFormat("DONE: %s ticks %I64d, M1 bars %I64d, digits %d", m_jname, m_res.ticks, m_res.bars, m_res.digits),
"In the Strategy Tester choose symbol " + m_jname + ". Data lives in the folders above.");
RefreshPaths();
}