MQL5Book/Experts/p7/EqualVolumeBars.mq5

511 lines
15 KiB
MQL5
Raw Permalink Normal View History

2025-05-30 16:09:41 +02:00
//+------------------------------------------------------------------+
//| EqualVolumeBars.mq5 |
//| Copyright © 2008-2022, MetaQuotes Ltd. |
//| https://www.mql5.com/ |
//+------------------------------------------------------------------+
#property copyright "Copyright © 2008-2022, MetaQuotes Ltd."
#property link "https://www.mql5.com/"
#property description "Non-trading EA generating equivolume and/or range bars as a custom symbol.\n"
#define TICKS_ARRAY 10000 // size of tick buffer
//+------------------------------------------------------------------+
//| Supported types of custom charts |
//+------------------------------------------------------------------+
enum mode
{
EqualTickVolumes = 0,
EqualRealVolumes = 1,
RangeBars = 2
};
//+------------------------------------------------------------------+
//| Inputs |
//+------------------------------------------------------------------+
input mode WorkMode = EqualTickVolumes;
input int TicksInBar = 1000;
input datetime StartDate = 0; // StartDate (default: 30 days back)
input string CustomPath = "MQL5Book\\Part7";
const uint DailySeconds = 60 * 60 * 24;
const string Suffixes[] = {"_Eqv", "_Qrv", "_Rng"};
datetime Start;
string SymbolName;
int BarCount;
bool InitDone = false;
//+------------------------------------------------------------------+
//| Virtual time and OHLCTV values of current bar for custom symbol |
//+------------------------------------------------------------------+
datetime now_time;
double now_close, now_open, now_low, now_high, now_real;
long now_volume;
//+------------------------------------------------------------------+
//| Custom symbol reset |
//+------------------------------------------------------------------+
bool Reset()
{
int size;
do
{
ResetLastError();
int deleted = CustomRatesDelete(SymbolName, 0, LONG_MAX);
int err = GetLastError();
if(err != ERR_SUCCESS)
{
Alert("CustomRatesDelete failed, ", err);
return false;
}
else
{
Print("Rates deleted: ", deleted);
}
ResetLastError();
deleted = CustomTicksDelete(SymbolName, 0, LONG_MAX);
if(deleted == -1)
{
Print("CustomTicksDelete failed ", GetLastError());
return false;
}
else
{
Print("Ticks deleted: ", deleted);
}
// wait for changes to take effect in the core threads
Sleep(1000);
MqlTick _array[];
size = CopyTicks(SymbolName, _array, COPY_TICKS_ALL, 0, 10);
Print("Remaining ticks: ", size);
}
while(size > 0 && !IsStopped());
// NB. this can not work everytime as expected
// if getting ERR_CUSTOM_TICKS_WRONG_ORDER or similar error - the last resort
// is to wipe out the custom symbol manually from GUI, and then restart this EA
return size > -1; // success
}
//+------------------------------------------------------------------+
//| Process history of real ticks |
//+------------------------------------------------------------------+
void BuildHistory(const datetime start)
{
ulong cursor = start * 1000;
uint trap = GetTickCount();
Print("Processing tick history...");
Comment("Processing tick history, this may take a while...");
TicksBuffer tb;
while(tb.fill(cursor, true) && !IsStopped())
{
MqlTick t;
while(tb.read(t))
{
HandleTick(t, true);
}
}
Comment("");
Print("Bar 0: ", now_time, " ", now_volume, " ", now_real);
if(now_volume > 0)
{
// write latest (incomplete) bar to the chart
WriteToChart(now_time, now_open, now_low, now_high, now_close, now_volume, (long)now_real);
// show stats
Print(BarCount, " bars written in ", (GetTickCount() - trap) / 1000, " sec");
}
else
{
Alert("No data");
}
}
//+------------------------------------------------------------------+
//| Start from scratch |
//+------------------------------------------------------------------+
datetime Init(const datetime start)
{
now_time = start;
now_close = 0;
now_open = 0;
now_low = DBL_MAX;
now_high = 0;
now_volume = 0;
now_real = 0;
return start;
}
//+------------------------------------------------------------------+
//| Rough estimation of continuation |
//+------------------------------------------------------------------+
datetime Resume(const datetime start)
{
MqlRates rates[2];
if(CopyRates(SymbolName, PERIOD_M1, 0, 2, rates) != 2) return Init(start);
ArrayPrint(rates); // tail
// rescan the last bar
// (but we don't know which tick inside the single minute rates[1].time
// did actually form this equal volume bar)
now_time = rates[1].time;
now_close = rates[1].open;
now_open = rates[1].open;
now_low = rates[1].open;
now_high = rates[1].open;
now_volume = 0; // rates[1].tick_volume;
now_real = 0; // (double)rates[1].real_volume;
Print("Resuming from ", rates[1].time);
return rates[1].time;
}
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
InitDone = false;
EventSetTimer(1);
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| Timer event handler |
//+------------------------------------------------------------------+
void OnTimer()
{
if(!TerminalInfoInteger(TERMINAL_CONNECTED))
{
Print("Waiting for connection...");
return;
}
if(!SymbolIsSynchronized(_Symbol))
{
Print("Unsynchronized, skipping ticks...");
return;
}
EventKillTimer();
BarCount = 0;
Start = StartDate == 0 ? TimeCurrent() - DailySeconds * 30 : StartDate;
SymbolName = _Symbol + Suffixes[WorkMode] + (string)TicksInBar;
bool justCreated = false;
if(!SymbolSelect(SymbolName, true))
{
Print("Creating \"", SymbolName, "\"");
if(!CustomSymbolCreate(SymbolName, CustomPath, _Symbol)
&& !SymbolSelect(SymbolName, true))
{
Alert("Can't select symbol:", SymbolName, " err:", GetLastError());
return;
}
justCreated = true;
Start = Init(Start);
}
else
{
if(IDYES == MessageBox(SymbolName + " exists. Rebuild?", NULL, MB_YESNO))
{
Print("Resetting \"", SymbolName, "\"");
if(!Reset()) return;
Start = Init(Start);
}
else
{
// find existing tail of custom quotes to supersede Start
Start = Resume(Start);
}
}
BuildHistory(Start);
if(IsStopped())
{
Print("Interrupted. The custom symbol data is inconsistent - please, delete");
return;
}
Print("Open \"", SymbolName, "\" chart to view results");
if(justCreated)
{
OpenCustomChart();
RefreshWindow(now_time);
}
InitDone = true;
}
//+------------------------------------------------------------------+
//| Ticks buffer (read from history by chunks) |
//+------------------------------------------------------------------+
class TicksBuffer
{
private:
MqlTick array[];
int tick;
public:
bool fill(ulong &cursor, const bool history = false)
{
int size = history ? CopyTicks(_Symbol, array, COPY_TICKS_ALL, cursor, TICKS_ARRAY) :
CopyTicksRange(_Symbol, array, COPY_TICKS_ALL, cursor);
if(size == -1)
{
Print("CopyTicks failed: ", _LastError);
return false;
}
else if(size == 0)
{
if(history)
{
Print("End of CopyTicks at ", (datetime)(cursor / 1000), " ", _LastError);
}
return false;
}
if((ulong)array[0].time_msc < cursor)
{
Print("Tick rewind bug, ", (datetime)(cursor / 1000));
return false;
}
cursor = array[size - 1].time_msc + 1;
tick = 0;
return true;
}
bool read(MqlTick &t)
{
if(tick < ArraySize(array))
{
t = array[tick++];
return true;
}
return false;
}
};
//+------------------------------------------------------------------+
//| Helper function to open custom symbol chart |
//+------------------------------------------------------------------+
void OpenCustomChart()
{
const long id = ChartOpen(SymbolName, PERIOD_M1);
if(id == 0)
{
Alert("Can't open new chart for ", SymbolName, ", code: ", _LastError);
}
else
{
Sleep(1000);
ChartSetSymbolPeriod(id, SymbolName, PERIOD_M1);
ChartSetInteger(id, CHART_MODE, CHART_CANDLES);
}
}
//+------------------------------------------------------------------+
//| Finalization handler |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
Comment("");
}
//+------------------------------------------------------------------+
//| Tick event handler |
//+------------------------------------------------------------------+
void OnTick()
{
if(!InitDone) return;
static ulong cursor = 0;
MqlTick t;
if(cursor == 0)
{
if(SymbolInfoTick(_Symbol, t))
{
HandleTick(t);
cursor = t.time_msc + 1;
}
}
else
{
TicksBuffer tb;
while(tb.fill(cursor))
{
while(tb.read(t))
{
HandleTick(t);
}
}
}
RefreshWindow(now_time);
}
//+------------------------------------------------------------------+
//| Process incoming ticks one by one |
//+------------------------------------------------------------------+
inline void HandleTick(const MqlTick &t, const bool history = false)
{
now_volume++;
now_real += t.volume_real;
// (long)t.volume; // NB: use 'long volume' to eliminate floating point error accumulation
const double bid = t.last != 0 ? t.last : t.bid;
if(!IsNewBar()) // bar continues
{
if(bid < now_low) now_low = bid;
if(bid > now_high) now_high = bid;
now_close = bid;
if(!history)
{
// write bar 0 to chart (-1 for volume stands for upcoming refresh)
WriteToChart(now_time, now_open, now_low, now_high, now_close, now_volume - !history, (long)now_real);
}
}
else // new bar tick
{
do
{
if(history)
{
BarCount++;
if((BarCount % 10) == 0)
{
Comment(t.time, " -> ", now_time, " [", BarCount, "]");
}
}
else
{
Comment("Complete bar: ", now_time);
}
if(WorkMode == RangeBars)
{
FixRange();
}
// write bar 1
WriteToChart(now_time, now_open, now_low, now_high, now_close,
WorkMode == EqualTickVolumes ? TicksInBar : now_volume,
WorkMode == EqualRealVolumes ? TicksInBar : (long)now_real);
// normalize down to a minute
datetime time = t.time / 60 * 60;
// eliminate bars with equal or too old times
if(time <= now_time) time = now_time + 60;
now_time = time;
now_open = bid;
now_low = bid;
now_high = bid;
now_close = bid;
now_volume = 1;
if(WorkMode == EqualRealVolumes) now_real -= TicksInBar;
// write bar 0 (-1 for volume stands for upcoming refresh)
WriteToChart(now_time, now_open, now_low, now_high, now_close, now_volume - !history, (long)now_real);
}
while(IsNewBar() && WorkMode == EqualRealVolumes);
}
}
//+------------------------------------------------------------------+
//| Simulate new tick on custom symbol chart |
//+------------------------------------------------------------------+
void RefreshWindow(const datetime t)
{
MqlTick ta[1];
SymbolInfoTick(_Symbol, ta[0]);
ta[0].time = t;
ta[0].time_msc = t * 1000;
if(CustomTicksAdd(SymbolName, ta) == -1) // NB! this call may increment number of ticks per bar
{
Print("CustomTicksAdd failed:", _LastError, " ", (long) ta[0].time);
ArrayPrint(ta);
}
}
//+------------------------------------------------------------------+
//| Add bar (MqlRates element) to custom symbol chart |
//+------------------------------------------------------------------+
void WriteToChart(datetime t, double o, double l, double h, double c, long v, long m = 0)
{
MqlRates r[1];
r[0].time = t;
r[0].open = o;
r[0].low = l;
r[0].high = h;
r[0].close = c;
r[0].tick_volume = v;
r[0].spread = 0;
r[0].real_volume = m;
if(CustomRatesUpdate(SymbolName, r) < 1)
{
Print("CustomRatesUpdate failed: ", _LastError);
}
}
//+------------------------------------------------------------------+
//| Check condition for new virtual bar formation according to mode |
//+------------------------------------------------------------------+
bool IsNewBar()
{
if(WorkMode == EqualTickVolumes)
{
if(now_volume > TicksInBar) return true;
}
else if(WorkMode == EqualRealVolumes)
{
if(now_real > TicksInBar) return true;
}
else if(WorkMode == RangeBars)
{
if((now_high - now_low) / _Point > TicksInBar) return true;
}
return false;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
void FixRange()
{
const int excess = (int)((now_high + (_Point / 2)) / _Point)
- (int)((now_low + (_Point / 2)) / _Point) - TicksInBar;
if(excess > 0)
{
if(now_close > now_open)
{
now_high -= excess * _Point;
if(now_high < now_close) now_close = now_high;
}
else if(now_close < now_open)
{
now_low += excess * _Point;
if(now_low > now_close) now_close = now_low;
}
}
}
//+------------------------------------------------------------------+