242 lines
8.1 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| Dump4.mqh |
| |||
//| Copyright 2026, Allan Niñal |
| |||
//| https://www.allanninal.dev/mql5 |
| |||
//+------------------------------------------------------------------+
| |||
//| MT4 test harness for an indicator, run as an EA in the Strategy |
| |||
//| Tester (MT4 iCustom takes its inputs at compile time, so each |
| |||
//| product has a small EA that defines IndValue(buf,shift) with its |
| |||
//| own iCustom call BEFORE including this file). |
| |||
//| During the test: on every new bar, records which signal buffers |
| |||
//| are set on the bar that just closed; on every tick, counts |
| |||
//| signals drawn on the live bar (must stay 0). |
| |||
//| At the end (tester/files): |
| |||
//| <tag>_<SYM>_<TF>.csv time,open,high,low,close,b<n>... |
| |||
//| for every bar (empty = no value) |
| |||
//| repaint_<tag>_<SYM>_<TF>.csv closed bars checked, signals |
| |||
//| seen live, mismatches with the |
| |||
//| final values, live-bar signals |
| |||
//+------------------------------------------------------------------+
| |||
// SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE)
| |||
#property strict
| |||
| |||
//--- a test EA may #define DUMP4_BUFFERS / DUMP4_PROBE before the include (a .set value cannot hold the commas)
| |||
#ifndef DUMP4_BUFFERS
| |||
#define DUMP4_BUFFERS "0,2,3"
| |||
#endif
| |||
#ifndef DUMP4_PROBE
| |||
#define DUMP4_PROBE "2,3"
| |||
#endif
| |||
| |||
input string InpTag = "default"; // Tag for the output file names
| |||
input string InpBuffers = DUMP4_BUFFERS; // Buffers to dump
| |||
input string InpProbe = DUMP4_PROBE; // Signal buffers checked for repainting
| |||
| |||
int g_bufs[],g_probe[];
| |||
datetime g_bar=0;
| |||
datetime g_rec_t[];
| |||
int g_rec_i[]; // the bar's position counted from the oldest bar (stable as bars are added)
| |||
int g_rec_f[];
| |||
int g_rec_n=0,g_live0=0;
| |||
| |||
//--- DUMP4_LIVE (define before the include): for an indicator that calculates only its last N bars. MT4 runs the
| |||
//--- indicator through a fresh full calculation when the test ends, so the final values hold only the last N bars.
| |||
//--- Live mode stores every bar's values from the running instance when the bar closes, re-checks the last
| |||
//--- DUMP4_RECHECK closed bars on every new bar (a repaint after the close shows there), and dumps the stored values.
| |||
#ifndef DUMP4_RECHECK
| |||
#define DUMP4_RECHECK 100
| |||
#endif
| |||
//--- DUMP4_LATE "<buffers>": buffers the indicator may still write up to DUMP4_RECHECK bars into the past (a swing
| |||
//--- pivot drawn on an older bar once the swing turns); live mode re-stores them for the last DUMP4_RECHECK bars.
| |||
double g_lv[][16]; // stored values per bar position (oldest = 0), buffers in InpBuffers order
| |||
int g_lf[]; // probe flags stored at the close
| |||
bool g_lset[],g_lbad[];
| |||
int g_ln=0; // positions allocated
| |||
| |||
void StoreBar(const int s)
| |||
{
| |||
int pos=Bars-1-s;
| |||
if(pos>=g_ln)
| |||
{
| |||
g_ln=pos+1;
| |||
ArrayResize(g_lv,g_ln,100000);
| |||
ArrayResize(g_lf,g_ln,100000);
| |||
ArrayResize(g_lset,g_ln,100000);
| |||
ArrayResize(g_lbad,g_ln,100000);
| |||
}
| |||
for(int k=0; k<ArraySize(g_bufs) && k<16; k++)
| |||
g_lv[pos][k]=IndValue(g_bufs[k],s);
| |||
g_lf[pos]=Flags(s);
| |||
g_lset[pos]=true;
| |||
g_lbad[pos]=false;
| |||
}
| |||
| |||
void ParseInts(const string text,int &out[])
| |||
{
| |||
string parts[];
| |||
int n=StringSplit(text,',',parts);
| |||
ArrayResize(out,n);
| |||
for(int k=0; k<n; k++)
| |||
out[k]=(int)StringToInteger(parts[k]);
| |||
}
| |||
| |||
string TfText()
| |||
{
| |||
switch(_Period)
| |||
{
| |||
case PERIOD_M1: return("M1");
| |||
case PERIOD_M5: return("M5");
| |||
case PERIOD_M15: return("M15");
| |||
case PERIOD_M30: return("M30");
| |||
case PERIOD_H1: return("H1");
| |||
case PERIOD_H4: return("H4");
| |||
case PERIOD_D1: return("D1");
| |||
}
| |||
return("M"+IntegerToString(_Period));
| |||
}
| |||
| |||
//--- bit k set = probe buffer k has a value on the bar at `shift`
| |||
int Flags(const int shift)
| |||
{
| |||
int f=0;
| |||
for(int k=0; k<ArraySize(g_probe); k++)
| |||
if(IndValue(g_probe[k],shift)!=EMPTY_VALUE)
| |||
f|=(1<<k);
| |||
return(f);
| |||
}
| |||
| |||
int OnInit()
| |||
{
| |||
ParseInts(InpBuffers,g_bufs);
| |||
ParseInts(InpProbe,g_probe);
| |||
ArrayResize(g_rec_t,0,100000);
| |||
ArrayResize(g_rec_f,0,100000);
| |||
ArrayResize(g_rec_i,0,100000);
| |||
return(INIT_SUCCEEDED);
| |||
}
| |||
| |||
void OnTick()
| |||
{
| |||
if(Flags(0)!=0)
| |||
g_live0++;
| |||
if(Time[0]==g_bar)
| |||
return;
| |||
g_bar=Time[0];
| |||
if(Bars<3)
| |||
return;
| |||
ArrayResize(g_rec_t,g_rec_n+1,100000);
| |||
ArrayResize(g_rec_f,g_rec_n+1,100000);
| |||
ArrayResize(g_rec_i,g_rec_n+1,100000);
| |||
g_rec_t[g_rec_n]=Time[1];
| |||
g_rec_i[g_rec_n]=Bars-2;
| |||
g_rec_f[g_rec_n]=Flags(1);
| |||
g_rec_n++;
| |||
#ifdef DUMP4_LIVE
| |||
if(g_ln==0)
| |||
for(int s=Bars-1; s>=1; s--)
| |||
StoreBar(s); // the bars already closed at the first new bar
| |||
else
| |||
StoreBar(1);
| |||
#ifdef DUMP4_LATE
| |||
int late[];
| |||
ParseInts(DUMP4_LATE,late);
| |||
for(int s=2; s<=DUMP4_RECHECK && s<Bars; s++)
| |||
{
| |||
int pos=Bars-1-s;
| |||
if(pos<g_ln && g_lset[pos])
| |||
for(int k=0; k<ArraySize(g_bufs) && k<16; k++)
| |||
for(int j=0; j<ArraySize(late); j++)
| |||
if(g_bufs[k]==late[j])
| |||
g_lv[pos][k]=IndValue(g_bufs[k],s);
| |||
}
| |||
#endif
| |||
for(int s=2; s<=DUMP4_RECHECK && s<Bars; s++)
| |||
{
| |||
int pos=Bars-1-s;
| |||
if(pos<g_ln && g_lset[pos] && Flags(s)!=g_lf[pos])
| |||
g_lbad[pos]=true;
| |||
}
| |||
#endif
| |||
}
| |||
| |||
//--- the dump and the repaint check, once (from OnTester, or OnDeinit outside the tester)
| |||
bool g_done=false;
| |||
void Finish()
| |||
{
| |||
if(g_done)
| |||
return;
| |||
g_done=true;
| |||
string name=InpTag+"_"+_Symbol+"_"+TfText()+".csv";
| |||
int h=FileOpen(name,FILE_WRITE|FILE_TXT|FILE_ANSI);
| |||
if(h==INVALID_HANDLE)
| |||
{
| |||
Print("Dump4: cannot write ",name," error ",GetLastError());
| |||
return;
| |||
}
| |||
string head="time,open,high,low,close";
| |||
for(int k=0; k<ArraySize(g_bufs); k++)
| |||
head+=",b"+IntegerToString(g_bufs[k]);
| |||
FileWriteString(h,head+"\n");
| |||
for(int s=Bars-1; s>=0; s--)
| |||
{
| |||
string row=TimeToString(Time[s],TIME_DATE|TIME_MINUTES)+","+DoubleToString(Open[s],_Digits)+","+
| |||
DoubleToString(High[s],_Digits)+","+DoubleToString(Low[s],_Digits)+","+DoubleToString(Close[s],_Digits);
| |||
for(int k=0; k<ArraySize(g_bufs); k++)
| |||
{
| |||
#ifdef DUMP4_LIVE
| |||
int pos=Bars-1-s;
| |||
double v=(pos<g_ln && g_lset[pos] && k<16 ? g_lv[pos][k] : EMPTY_VALUE);
| |||
#else
| |||
double v=IndValue(g_bufs[k],s);
| |||
#endif
| |||
row+=","+(v==EMPTY_VALUE ? "" : StringFormat("%.16g",v));
| |||
}
| |||
FileWriteString(h,row+"\n");
| |||
}
| |||
FileClose(h);
| |||
//--- repainting: the signals recorded when each bar closed against the final values
| |||
int bad=0,sig=0,lost=0;
| |||
#ifdef DUMP4_LIVE
| |||
for(int r=0; r<g_rec_n; r++)
| |||
if(g_rec_f[r]!=0)
| |||
sig++;
| |||
for(int pos=0; pos<g_ln; pos++)
| |||
if(g_lbad[pos])
| |||
bad++;
| |||
#else
| |||
for(int r=0; r<g_rec_n; r++)
| |||
{
| |||
int s=Bars-1-g_rec_i[r];
| |||
if(s<1 || s>=Bars || Time[s]!=g_rec_t[r])
| |||
{
| |||
lost++; // the bar could not be found again: counts as a failure
| |||
continue;
| |||
}
| |||
if(g_rec_f[r]!=0)
| |||
sig++;
| |||
if(Flags(s)!=g_rec_f[r])
| |||
bad++;
| |||
}
| |||
#endif
| |||
h=FileOpen("repaint_"+name,FILE_WRITE|FILE_TXT|FILE_ANSI);
| |||
if(h!=INVALID_HANDLE)
| |||
{
| |||
FileWriteString(h,StringFormat("closed_bars_checked,%d\nsignals_seen_live,%d\nmismatches,%d\nlive_bar_signals,%d\nbars,%d\nnot_found,%d\n",
| |||
g_rec_n-lost,sig,bad,g_live0,Bars,lost));
| |||
FileClose(h);
| |||
}
| |||
PrintFormat("Dump4 %s: %d bars dumped; %d closed bars checked (%d not found), %d signals, %d mismatches, %d live-bar signals",
| |||
name,Bars,g_rec_n-lost,lost,sig,bad,g_live0);
| |||
}
| |||
| |||
double OnTester()
| |||
{
| |||
Finish();
| |||
return(0.0);
| |||
}
| |||
| |||
void OnDeinit(const int reason)
| |||
{
| |||
Finish();
| |||
}
| |||
//+------------------------------------------------------------------+
|