2026-11-04 15:03:18 +01:00
//+------------------------------------------------------------------+
//| 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
2026-11-04 15:50:31 +01:00
# define TF_H 596
# define TF_PLOT_H 554
2026-11-04 15:03:18 +01:00
# 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 = " " ) ;
2026-11-04 15:50:31 +01:00
void Para ( const string a , const string b = " " ) ;
2026-11-04 15:03:18 +01:00
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 ( ) ;
}
2026-11-04 15:50:31 +01:00
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 ( ) ;
}
2026-11-04 15:03:18 +01:00
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 ;
2026-11-04 15:50:31 +01:00
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 ;
2026-11-04 15:03:18 +01:00
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 ;
2026-11-04 15:50:31 +01:00
bool m_audit ; // audit-set run in progress
int m_ai ; // index of the audit symbol being written
CTFJob m_ajob ;
string m_abase ;
2026-11-04 15:03:18 +01:00
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 ( ) ; }
2026-11-04 15:50:31 +01:00
void ClearConfirm ( ) { if ( m_confirm ! = " " ) { m_confirm = " " ; m_btnGo . Text ( " GENERATE SYMBOL " ) ; AuditButtonsReset ( ) ; } }
2026-11-04 15:03:18 +01:00
void ScanStep ( ) ;
void OnClickFull ( ) ;
void OnClickPreview ( ) ;
void OnClickScan ( ) ;
void OnClickGenerate ( ) ;
2026-11-04 15:50:31 +01:00
void OnClickAudit ( ) ;
void OnClickDeleteAudit ( ) ;
bool StartAuditJob ( ) ;
bool WriteManifest ( ) ;
void AuditButtonsReset ( ) ;
2026-11-04 15:03:18 +01:00
public :
2026-11-04 15:50:31 +01:00
CTickForgeDlg ( ) : m_busy ( false ) , m_closed ( false ) , m_running ( false ) , m_audit ( false ) , m_ai ( 0 ) , m_scanning ( false ) , m_srun ( 0 ) { }
2026-11-04 15:03:18 +01:00
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)
2026-11-04 15:50:31 +01:00
void SetStatus ( const string s ) { m_sa = s ; m_status . Para ( m_sa , m_sb ) ; ChartRedraw ( ) ; }
string Status ( ) { return m_sa ; }
2026-11-04 15:03:18 +01:00
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 ( ) ;
}
2026-11-04 15:50:31 +01:00
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 ( ) ;
}
2026-11-04 15:03:18 +01:00
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 )
2026-11-04 15:50:31 +01:00
ON_EVENT ( ON_CLICK , m_btnAudit , OnClickAudit )
ON_EVENT ( ON_CLICK , m_btnDel , OnClickDeleteAudit )
2026-11-04 15:03:18 +01:00
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 ) ;
2026-11-04 15:50:31 +01:00
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 ) ;
2026-11-04 15:03:18 +01:00
y + = 38 ;
m_paths . Create ( m_chart_id , m_name + " paths " , m_subwin , TF_X , y , XR , y + 50 ) ; Add ( m_paths ) ;
y + = 52 ;
2026-11-04 15:50:31 +01:00
m_status . Create ( m_chart_id , m_name + " status " , m_subwin , TF_X , y , XR , y + 60 ) ; Add ( m_status ) ;
Say ( " Ready. " ) ;
2026-11-04 15:03:18 +01:00
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 )
{
2026-11-04 15:50:31 +01:00
m_sa = a ; m_sb = b ; m_status . Para ( a , b ) ; ChartRedraw ( ) ;
2026-11-04 15:03:18 +01:00
}
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 ) ) ;
2026-11-04 15:50:31 +01:00
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. " , " " ) ;
2026-11-04 15:03:18 +01:00
}
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 ;
}
2026-11-04 15:50:31 +01:00
//--- 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 ) : " " ) ;
}
2026-11-04 15:03:18 +01:00
void CTickForgeDlg::Tick ( )
{
if ( m_scanning ) { ScanStep ( ) ; return ; }
if ( ! m_running ) return ;
if ( ! m_eng . Step ( m_res ) )
{
m_running = false ;
2026-11-04 15:50:31 +01:00
const bool was_audit = m_audit ;
m_audit = false ;
2026-11-04 15:03:18 +01:00
m_btnGo . Text ( " GENERATE SYMBOL " ) ;
2026-11-04 15:50:31 +01:00
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). " ) ;
2026-11-04 15:03:18 +01:00
return ;
}
if ( ! m_eng . Done ( ) ) return ;
2026-11-04 15:50:31 +01:00
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 ;
}
2026-11-04 15:03:18 +01:00
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 ( ) ;
}