forked from rosh/MQL5Book
510 lines
15 KiB
MQL5
510 lines
15 KiB
MQL5
//+------------------------------------------------------------------+
| |
//| 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;
| |
}
| |
}
| |
}
| |
//+------------------------------------------------------------------+
|