321 lines
10 KiB
MQL5
321 lines
10 KiB
MQL5
|
//+------------------------------------------------------------------+
|
||
|
//| DeltaVolumeProfile.mq5 |
|
||
|
//| Copyright 2022, MetaQuotes Ltd. |
|
||
|
//| https://www.mql5.com |
|
||
|
//+------------------------------------------------------------------+
|
||
|
// indicator settings
|
||
|
#property indicator_chart_window
|
||
|
#property indicator_buffers 0
|
||
|
#property indicator_plots 0
|
||
|
|
||
|
// includes
|
||
|
#include "..\..\Include\IndCommon.mqh"
|
||
|
#include "..\..\Include\TickEnum.mqh"
|
||
|
|
||
|
// inputs
|
||
|
input bool ShowSplittedDelta = true;
|
||
|
input COPY_TICKS TickType = INFO_TICKS; // TickType (use TRADE_TICKS if real volumes available)
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Class for volume delta calculation |
|
||
|
//+------------------------------------------------------------------+
|
||
|
class DeltaVolumeProfile
|
||
|
{
|
||
|
const COPY_TICKS tickType;
|
||
|
const ENUM_SYMBOL_CHART_MODE barType;
|
||
|
const bool delta;
|
||
|
|
||
|
static const string prefix;
|
||
|
|
||
|
protected:
|
||
|
double price(const MqlTick &tick)
|
||
|
{
|
||
|
return barType == SYMBOL_CHART_MODE_LAST ? tick.last : tick.bid;
|
||
|
}
|
||
|
|
||
|
// main tick processing (on history only)
|
||
|
void calcProfile(const int b, const datetime time, const MqlTick &ticks[])
|
||
|
{
|
||
|
const string name = prefix + (string)(ulong)time;
|
||
|
const double high = iHigh(_Symbol, _Period, b);
|
||
|
const double low = iLow(_Symbol, _Period, b);
|
||
|
const double range = high - low;
|
||
|
|
||
|
if(fabs(range) < DBL_EPSILON) return;
|
||
|
if(ArraySize(ticks) == 0) return;
|
||
|
|
||
|
ObjectCreate(0, name, OBJ_BITMAP, 0, time, high);
|
||
|
|
||
|
int x1, y1, x2, y2;
|
||
|
ChartTimePriceToXY(0, 0, time, high, x1, y1);
|
||
|
ChartTimePriceToXY(0, 0, time, low, x2, y2);
|
||
|
|
||
|
const int h = y2 - y1 + 1;
|
||
|
const int w = (int)(ChartGetInteger(0, CHART_WIDTH_IN_PIXELS)
|
||
|
/ ChartGetInteger(0, CHART_WIDTH_IN_BARS));
|
||
|
|
||
|
if(h <= 0)
|
||
|
{
|
||
|
Print("Bad data: ", high, " ", low, " ", y1, " ", y2);
|
||
|
DebugBreak();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
uint data[];
|
||
|
ArrayResize(data, w * h);
|
||
|
ArrayInitialize(data, 0);
|
||
|
ResourceCreate(name + (string)ChartID(), data, w, h, 0, 0, w, COLOR_FORMAT_ARGB_NORMALIZE);
|
||
|
|
||
|
ObjectSetString(0, name, OBJPROP_BMPFILE, "::" + name + (string)ChartID());
|
||
|
ObjectSetInteger(0, name, OBJPROP_XSIZE, w);
|
||
|
ObjectSetInteger(0, name, OBJPROP_YSIZE, h);
|
||
|
ObjectSetInteger(0, name, OBJPROP_ANCHOR, ANCHOR_UPPER);
|
||
|
|
||
|
long plus[], minus[], max = 0;
|
||
|
ArrayResize(plus, h);
|
||
|
ArrayResize(minus, h);
|
||
|
ArrayInitialize(plus, 0);
|
||
|
ArrayInitialize(minus, 0);
|
||
|
|
||
|
const int n = ArraySize(ticks);
|
||
|
for(int j = 0; j < n; ++j)
|
||
|
{
|
||
|
const double p1 = price(ticks[j]);
|
||
|
/*const*/ int index = (int)((high - p1) / range * (h - 1));
|
||
|
if(index >= h)
|
||
|
{
|
||
|
Print("Correction: index=", index, " ", high, " ", p1, " ", h);
|
||
|
index = h - 1;
|
||
|
DebugBreak();
|
||
|
}
|
||
|
if(index < 0)
|
||
|
{
|
||
|
Print("Correction: index=", index, " ", high, " ", p1, " ", h);
|
||
|
index = 0;
|
||
|
DebugBreak();
|
||
|
}
|
||
|
// when real volumes are expected to be available, check them in the ticks
|
||
|
if(tickType == TRADE_TICKS)
|
||
|
{
|
||
|
// accumulate volumes for buy and sell deals separately
|
||
|
if((ticks[j].flags & TICK_FLAG_BUY) != 0)
|
||
|
{
|
||
|
plus[index] += (long)ticks[j].volume;
|
||
|
}
|
||
|
if((ticks[j].flags & TICK_FLAG_SELL) != 0)
|
||
|
{
|
||
|
minus[index] += (long)ticks[j].volume;
|
||
|
}
|
||
|
}
|
||
|
else // tickType == INFO_TICKS or tickType == ALL_TICKS
|
||
|
if(j > 0)
|
||
|
{
|
||
|
// when real volumes are unavailable, use price moves up/down to estimate volume change
|
||
|
if((ticks[j].flags & (TICK_FLAG_ASK | TICK_FLAG_BID)) != 0)
|
||
|
{
|
||
|
const double d = (((ticks[j].ask + ticks[j].bid)
|
||
|
- (ticks[j - 1].ask + ticks[j - 1].bid)) / _Point);
|
||
|
if(d > 0)
|
||
|
plus[index] += (long)d;
|
||
|
else
|
||
|
minus[index] -= (long)d;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(delta)
|
||
|
{
|
||
|
if(plus[index] > max) max = plus[index];
|
||
|
if(minus[index] > max) max = minus[index];
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
if(fabs(plus[index] - minus[index]) > max) max = fabs(plus[index] - minus[index]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(max == 0)
|
||
|
{
|
||
|
Print("No tick volumes for ", (string)time);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
for(int i = 0; i < h; i++)
|
||
|
{
|
||
|
if(delta)
|
||
|
{
|
||
|
const int dp = (int)(plus[i] * w / 2 / max);
|
||
|
const int dm = (int)(minus[i] * w / 2 / max);
|
||
|
for(int j = 0; j < dp; j++)
|
||
|
{
|
||
|
data[i * w + w / 2 + j] = ColorToARGB(clrBlue);
|
||
|
}
|
||
|
for(int j = 0; j < dm; j++)
|
||
|
{
|
||
|
data[i * w + w / 2 - j] = ColorToARGB(clrRed);
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
const int d = (int)((plus[i] - minus[i]) * w / 2 / max);
|
||
|
const int sign = d > 0 ? +1 : -1;
|
||
|
for(int j = 0; j < fabs(d); j++)
|
||
|
{
|
||
|
data[i * w + w / 2 + j * sign] = ColorToARGB(clrGreen);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
ResourceCreate(name + (string)ChartID(), data, w, h, 0, 0, w, COLOR_FORMAT_ARGB_NORMALIZE);
|
||
|
}
|
||
|
|
||
|
public:
|
||
|
DeltaVolumeProfile(const COPY_TICKS type, const bool d) :
|
||
|
tickType(type), delta(d),
|
||
|
barType((ENUM_SYMBOL_CHART_MODE)SymbolInfoInteger(_Symbol, SYMBOL_CHART_MODE))
|
||
|
{
|
||
|
}
|
||
|
|
||
|
~DeltaVolumeProfile()
|
||
|
{
|
||
|
// resources are not deleted with objects,
|
||
|
// so calling ObjectsDeleteAll(0, prefix, 0); is not enough
|
||
|
const int n = ObjectsTotal(0, 0);
|
||
|
for(int i = n - 1; i >= 0; --i)
|
||
|
{
|
||
|
const string name = ObjectName(0, i, 0);
|
||
|
if(StringFind(name, prefix) == 0)
|
||
|
{
|
||
|
ObjectDelete(0, name);
|
||
|
ResourceFree("::" + name + (string)ChartID());
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// get ticks for specific bar on the history
|
||
|
int createProfileBar(const int i)
|
||
|
{
|
||
|
MqlTick ticks[];
|
||
|
const datetime time = iTime(_Symbol, _Period, i);
|
||
|
// prev and next are timestamps of the bar boundaries
|
||
|
const datetime prev = time;
|
||
|
const datetime next = prev + PeriodSeconds();
|
||
|
ResetLastError();
|
||
|
const int n = CopyTicksRange(_Symbol, ticks, COPY_TICKS_ALL, prev * 1000, next * 1000 - 1);
|
||
|
if(n > -1 && _LastError == 0)
|
||
|
{
|
||
|
calcProfile(i, time, ticks);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
return -_LastError;
|
||
|
}
|
||
|
|
||
|
return n;
|
||
|
}
|
||
|
|
||
|
// update bars where objects with histograms located
|
||
|
void updateProfileBars()
|
||
|
{
|
||
|
const int n = ObjectsTotal(0, 0);
|
||
|
for(int i = n - 1; i >= 0; --i)
|
||
|
{
|
||
|
const string name = ObjectName(0, i, 0);
|
||
|
if(StringFind(name, prefix) == 0)
|
||
|
{
|
||
|
const datetime dt = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 0);
|
||
|
const int bar = iBarShift(_Symbol, _Period, dt, true);
|
||
|
if(createProfileBar(bar) < 0)
|
||
|
{
|
||
|
ObjectDelete(0, name);
|
||
|
ResourceFree("::" + name + (string)ChartID());
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
static const string DeltaVolumeProfile::prefix = "DVP";
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Global variables |
|
||
|
//+------------------------------------------------------------------+
|
||
|
DeltaVolumeProfile deltas(TickType, ShowSplittedDelta);
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Custom indicator initialization function |
|
||
|
//+------------------------------------------------------------------+
|
||
|
int OnInit()
|
||
|
{
|
||
|
if(_Period >= PERIOD_D1)
|
||
|
{
|
||
|
Alert("Use intraday timeframe smaller than D1, please");
|
||
|
return INIT_FAILED;
|
||
|
}
|
||
|
|
||
|
return INIT_SUCCEEDED;
|
||
|
}
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Custom indicator iteration function |
|
||
|
//+------------------------------------------------------------------+
|
||
|
int OnCalculate(ON_CALCULATE_STD_FULL_PARAM_LIST)
|
||
|
{
|
||
|
return rates_total;
|
||
|
}
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Helper data function |
|
||
|
//+------------------------------------------------------------------+
|
||
|
|
||
|
#define TRY_AGAIN 0xAAA
|
||
|
|
||
|
void RequestData(const int b, const datetime time, const int count = 0)
|
||
|
{
|
||
|
Comment("Requesting ticks for ", time);
|
||
|
if(deltas.createProfileBar(b) <= 0)
|
||
|
{
|
||
|
Print("No data on bar ", b, ", at ", TimeToString(time),
|
||
|
". Sending event for refresh...");
|
||
|
ChartSetSymbolPeriod(0, _Symbol, _Period); // refresh myself
|
||
|
EventChartCustom(0, TRY_AGAIN, b, count + 1, NULL);
|
||
|
}
|
||
|
Comment("");
|
||
|
}
|
||
|
|
||
|
//+------------------------------------------------------------------+
|
||
|
//| Chart event handler |
|
||
|
//+------------------------------------------------------------------+
|
||
|
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
|
||
|
{
|
||
|
if(id == CHARTEVENT_CLICK)
|
||
|
{
|
||
|
datetime time;
|
||
|
double price;
|
||
|
int window;
|
||
|
ChartXYToTimePrice(0, (int)lparam, (int)dparam, window, time, price);
|
||
|
time += PeriodSeconds() / 2;
|
||
|
const int b = iBarShift(_Symbol, _Period, time, true);
|
||
|
if(b != -1 && window == 0)
|
||
|
{
|
||
|
RequestData(b, iTime(_Symbol, _Period, b));
|
||
|
}
|
||
|
}
|
||
|
else if(id == CHARTEVENT_CHART_CHANGE)
|
||
|
{
|
||
|
deltas.updateProfileBars();
|
||
|
}
|
||
|
else if(id == CHARTEVENT_CUSTOM + TRY_AGAIN)
|
||
|
{
|
||
|
Print("Refreshing... ", (int)dparam);
|
||
|
const int b = (int)lparam;
|
||
|
if((int)dparam < 5)
|
||
|
{
|
||
|
RequestData(b, iTime(_Symbol, _Period, b), (int)dparam);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
Print("Give up. Check tick history manually, please, then click the bar again");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
//+------------------------------------------------------------------+
|