400 satır
14 KiB
MQL5
400 satır
14 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| FixedRangeVolumeProfile.mq5 |
|
|
//| Copyright 2026, MetaQuotes Ltd. |
|
|
//| https://www.mql5.com |
|
|
//+------------------------------------------------------------------+
|
|
#property copyright "Copyright 2026, MetaQuotes Ltd."
|
|
#property link "https://www.mql5.com"
|
|
#property version "1.00"
|
|
#property indicator_chart_window
|
|
#property indicator_buffers 1
|
|
#property indicator_plots 1
|
|
#property indicator_color1 clrDarkGray, clrDimGray
|
|
#property indicator_label1 "Close Prices"
|
|
|
|
|
|
#include <Canvas\Canvas.mqh>
|
|
#include <ChartObjects\ChartObjectsLines.mqh>
|
|
|
|
//--- input parameters
|
|
input int InpTicksPerBin = 10; // Ticks per bin
|
|
input int InpValueAreaPercent = 70; // Value Area Percentage
|
|
input int InpWidthPercent = 30; // Width % of Chart
|
|
|
|
input string InpSessionAsian = "23:00-08:00"; // Asian Session
|
|
input color InpColorAsian = clrSkyBlue; // Asian Color
|
|
input string InpSessionLondon = "07:00-16:30"; // London Session
|
|
input color InpColorLondon = clrLimeGreen; // London Color
|
|
input string InpSessionNY = "12:00-21:30"; // NY Session
|
|
input color InpColorNY = clrOrange; // NY Color
|
|
|
|
input color InpColorPOC = clrRed; // POC Color
|
|
|
|
//--- global variables
|
|
CCanvas m_canvas;
|
|
CChartObjectVLine m_line_start;
|
|
CChartObjectVLine m_line_end;
|
|
string m_canvas_name = "FRVP_Canvas";
|
|
string m_line_start_name = "FRVP_Start";
|
|
string m_line_end_name = "FRVP_End";
|
|
|
|
datetime m_last_start;
|
|
datetime m_last_end;
|
|
|
|
struct BinData
|
|
{
|
|
double price;
|
|
long volume;
|
|
bool is_va;
|
|
};
|
|
|
|
struct SessionData
|
|
{
|
|
BinData bins[];
|
|
long total_volume;
|
|
double poc_price;
|
|
int start_hour, start_min;
|
|
int end_hour, end_min;
|
|
color base_color;
|
|
};
|
|
|
|
SessionData m_sessions[3];
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Parse Session String |
|
|
//+------------------------------------------------------------------+
|
|
void ParseSessions()
|
|
{
|
|
string sessions[3] = {InpSessionAsian, InpSessionLondon, InpSessionNY};
|
|
color colors[3] = {InpColorAsian, InpColorLondon, InpColorNY};
|
|
|
|
for(int i = 0; i < 3; i++)
|
|
{
|
|
string parts[];
|
|
if(StringSplit(sessions[i], '-', parts) == 2)
|
|
{
|
|
string start_parts[];
|
|
string end_parts[];
|
|
if(StringSplit(parts[0], ':', start_parts) == 2 && StringSplit(parts[1], ':', end_parts) == 2)
|
|
{
|
|
m_sessions[i].start_hour = (int)StringToInteger(start_parts[0]);
|
|
m_sessions[i].start_min = (int)StringToInteger(start_parts[1]);
|
|
m_sessions[i].end_hour = (int)StringToInteger(end_parts[0]);
|
|
m_sessions[i].end_min = (int)StringToInteger(end_parts[1]);
|
|
}
|
|
}
|
|
m_sessions[i].base_color = colors[i];
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Check if time is in session |
|
|
//+------------------------------------------------------------------+
|
|
bool IsInSession(datetime t, int s_idx)
|
|
{
|
|
MqlDateTime dt;
|
|
TimeToStruct(t, dt);
|
|
int current_minutes = dt.hour * 60 + dt.min;
|
|
int start_minutes = m_sessions[s_idx].start_hour * 60 + m_sessions[s_idx].start_min;
|
|
int end_minutes = m_sessions[s_idx].end_hour * 60 + m_sessions[s_idx].end_min;
|
|
|
|
if(start_minutes < end_minutes)
|
|
return (current_minutes >= start_minutes && current_minutes < end_minutes);
|
|
else // Overnight session
|
|
return (current_minutes >= start_minutes || current_minutes < end_minutes);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Custom indicator initialization function |
|
|
//+------------------------------------------------------------------+
|
|
int OnInit()
|
|
{
|
|
ParseSessions();
|
|
|
|
//--- create lines
|
|
if(!m_line_start.Create(0, m_line_start_name, 0, iTime(_Symbol, _Period, 100)))
|
|
return(INIT_FAILED);
|
|
m_line_start.Color(clrGray);
|
|
m_line_start.Style(STYLE_DOT);
|
|
m_line_start.Selectable(true);
|
|
m_line_start.Selected(true);
|
|
m_line_start.Tooltip("FRVP Start");
|
|
|
|
if(!m_line_end.Create(0, m_line_end_name, 0, iTime(_Symbol, _Period, 0)))
|
|
return(INIT_FAILED);
|
|
m_line_end.Color(clrGray);
|
|
m_line_end.Style(STYLE_DOT);
|
|
m_line_end.Selectable(true);
|
|
m_line_end.Selected(true);
|
|
m_line_end.Tooltip("FRVP End");
|
|
|
|
//--- initialize canvas
|
|
if(!m_canvas.CreateBitmapLabel(m_canvas_name, 0, 0, (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS), (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS), COLOR_FORMAT_ARGB_NORMALIZE))
|
|
return(INIT_FAILED);
|
|
|
|
m_canvas.Erase(0);
|
|
m_canvas.Update();
|
|
|
|
return(INIT_SUCCEEDED);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Custom indicator deinitialization function |
|
|
//+------------------------------------------------------------------+
|
|
void OnDeinit(const int reason)
|
|
{
|
|
m_line_start.Delete();
|
|
m_line_end.Delete();
|
|
m_canvas.Destroy();
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Custom indicator iteration function |
|
|
//+------------------------------------------------------------------+
|
|
int OnCalculate(const int rates_total,
|
|
const int prev_calculated,
|
|
const datetime &time[],
|
|
const double &open[],
|
|
const double &high[],
|
|
const double &low[],
|
|
const double &close[],
|
|
const long &tick_volume[],
|
|
const long &volume[],
|
|
const int &spread[])
|
|
{
|
|
datetime t_start = m_line_start.Time(0);
|
|
datetime t_end = m_line_end.Time(0);
|
|
|
|
if(t_start != m_last_start || t_end != m_last_end)
|
|
{
|
|
CalculateProfile(t_start, t_end);
|
|
RenderProfile();
|
|
m_last_start = t_start;
|
|
m_last_end = t_end;
|
|
}
|
|
|
|
return(rates_total);
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| ChartEvent function |
|
|
//+------------------------------------------------------------------+
|
|
void OnChartEvent(const int id,
|
|
const long &lparam,
|
|
const double &dparam,
|
|
const string &sparam)
|
|
{
|
|
if(id == CHARTEVENT_OBJECT_DRAG || id == CHARTEVENT_CHART_CHANGE)
|
|
{
|
|
datetime t_start = m_line_start.Time(0);
|
|
datetime t_end = m_line_end.Time(0);
|
|
|
|
if(t_start != m_last_start || t_end != m_last_end || id == CHARTEVENT_CHART_CHANGE)
|
|
{
|
|
CalculateProfile(t_start, t_end);
|
|
RenderProfile();
|
|
m_last_start = t_start;
|
|
m_last_end = t_end;
|
|
}
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate Volume Profile |
|
|
//+------------------------------------------------------------------+
|
|
void CalculateProfile(datetime t1, datetime t2)
|
|
{
|
|
datetime start_time = (t1 < t2) ? t1 : t2;
|
|
datetime end_time = (t1 < t2) ? t2 : t1;
|
|
|
|
int start_bar = iBarShift(_Symbol, _Period, start_time);
|
|
int end_bar = iBarShift(_Symbol, _Period, end_time);
|
|
|
|
if(start_bar < 0 || end_bar < 0) return;
|
|
|
|
//--- Find price range
|
|
double min_price = 0, max_price = 0;
|
|
for(int i = end_bar; i <= start_bar; i++)
|
|
{
|
|
double h = iHigh(_Symbol, _Period, i);
|
|
double l = iLow(_Symbol, _Period, i);
|
|
if(i == end_bar) { min_price = l; max_price = h; }
|
|
else { if(l < min_price) min_price = l; if(h > max_price) max_price = h; }
|
|
}
|
|
|
|
double tick_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
|
|
double bin_size = tick_size * InpTicksPerBin;
|
|
|
|
if(bin_size <= 0) return;
|
|
|
|
int bin_count = (int)((max_price - min_price) / bin_size) + 1;
|
|
|
|
for(int s = 0; s < 3; s++)
|
|
{
|
|
ArrayResize(m_sessions[s].bins, bin_count);
|
|
for(int i = 0; i < bin_count; i++)
|
|
{
|
|
m_sessions[s].bins[i].price = min_price + i * bin_size;
|
|
m_sessions[s].bins[i].volume = 0;
|
|
m_sessions[s].bins[i].is_va = false;
|
|
}
|
|
m_sessions[s].total_volume = 0;
|
|
}
|
|
|
|
for(int i = end_bar; i <= start_bar; i++)
|
|
{
|
|
datetime bar_time = iTime(_Symbol, _Period, i);
|
|
double h = iHigh(_Symbol, _Period, i);
|
|
double l = iLow(_Symbol, _Period, i);
|
|
long vol = iTickVolume(_Symbol, _Period, i);
|
|
|
|
int h_idx = (int)((h - min_price) / bin_size);
|
|
int l_idx = (int)((l - min_price) / bin_size);
|
|
|
|
if(h_idx >= bin_count) h_idx = bin_count - 1;
|
|
if(l_idx < 0) l_idx = 0;
|
|
|
|
int bins_covered = h_idx - l_idx + 1;
|
|
long vol_per_bin = vol / bins_covered;
|
|
|
|
for(int s = 0; s < 3; s++)
|
|
{
|
|
if(IsInSession(bar_time, s))
|
|
{
|
|
for(int j = l_idx; j <= h_idx; j++)
|
|
{
|
|
m_sessions[s].bins[j].volume += vol_per_bin;
|
|
m_sessions[s].total_volume += vol_per_bin;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//--- Calculate POC and VA for each session
|
|
for(int s = 0; s < 3; s++)
|
|
{
|
|
if(m_sessions[s].total_volume <= 0) continue;
|
|
|
|
long max_vol = -1;
|
|
int poc_idx = 0;
|
|
for(int i = 0; i < bin_count; i++)
|
|
{
|
|
if(m_sessions[s].bins[i].volume > max_vol)
|
|
{
|
|
max_vol = m_sessions[s].bins[i].volume;
|
|
poc_idx = i;
|
|
}
|
|
}
|
|
m_sessions[s].poc_price = m_sessions[s].bins[poc_idx].price + bin_size / 2.0;
|
|
|
|
long target_va_vol = (long)(m_sessions[s].total_volume * (InpValueAreaPercent / 100.0));
|
|
long current_va_vol = m_sessions[s].bins[poc_idx].volume;
|
|
m_sessions[s].bins[poc_idx].is_va = true;
|
|
|
|
int up_idx = poc_idx + 1;
|
|
int dn_idx = poc_idx - 1;
|
|
|
|
while(current_va_vol < target_va_vol && (up_idx < bin_count || dn_idx >= 0))
|
|
{
|
|
long up_vol = (up_idx < bin_count) ? m_sessions[s].bins[up_idx].volume : -1;
|
|
long dn_vol = (dn_idx >= 0) ? m_sessions[s].bins[dn_idx].volume : -1;
|
|
|
|
if(up_vol >= dn_vol && up_vol != -1)
|
|
{
|
|
current_va_vol += up_vol;
|
|
m_sessions[s].bins[up_idx].is_va = true;
|
|
up_idx++;
|
|
}
|
|
else if(dn_vol != -1)
|
|
{
|
|
current_va_vol += dn_vol;
|
|
m_sessions[s].bins[dn_idx].is_va = true;
|
|
dn_idx--;
|
|
}
|
|
else break;
|
|
}
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Render Volume Profile |
|
|
//+------------------------------------------------------------------+
|
|
void RenderProfile()
|
|
{
|
|
int width = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
|
|
int height = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
|
|
|
|
if(width != m_canvas.Width() || height != m_canvas.Height())
|
|
{
|
|
m_canvas.Resize(width, height);
|
|
}
|
|
|
|
m_canvas.Erase(0);
|
|
|
|
int bin_count = ArraySize(m_sessions[0].bins);
|
|
if(bin_count == 0) { m_canvas.Update(); return; }
|
|
|
|
long global_max_vol = 0;
|
|
for(int i = 0; i < bin_count; i++)
|
|
{
|
|
long total_bin_vol = 0;
|
|
for(int s = 0; s < 3; s++) total_bin_vol += m_sessions[s].bins[i].volume;
|
|
if(total_bin_vol > global_max_vol) global_max_vol = total_bin_vol;
|
|
}
|
|
|
|
if(global_max_vol <= 0) { m_canvas.Update(); return; }
|
|
|
|
int max_draw_width = (int)(width * (InpWidthPercent / 100.0));
|
|
|
|
int x_right = width - 1;
|
|
|
|
double tick_size = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
|
|
double bin_height_price = tick_size * InpTicksPerBin;
|
|
|
|
for(int i = 0; i < bin_count; i++)
|
|
{
|
|
double p_top = m_sessions[0].bins[i].price + bin_height_price;
|
|
double p_bot = m_sessions[0].bins[i].price;
|
|
int y_top = PriceToY(p_top);
|
|
int y_bot = PriceToY(p_bot);
|
|
|
|
int current_x = x_right;
|
|
|
|
for(int s = 0; s < 3; s++)
|
|
{
|
|
if(m_sessions[s].bins[i].volume <= 0) continue;
|
|
|
|
int segment_w = (int)((double)m_sessions[s].bins[i].volume / global_max_vol * max_draw_width);
|
|
if(segment_w <= 0) continue;
|
|
|
|
uint base_clr_rgb = COLOR2RGB(m_sessions[s].base_color);
|
|
uint clr = ARGB(m_sessions[s].bins[i].is_va ? 180 : 80, (base_clr_rgb>>16)&0xFF, (base_clr_rgb>>8)&0xFF, base_clr_rgb&0xFF);
|
|
|
|
m_canvas.FillRectangle(current_x - segment_w, y_top, current_x, y_bot, clr);
|
|
current_x -= segment_w;
|
|
}
|
|
}
|
|
|
|
//--- Draw POCs
|
|
for(int s = 0; s < 3; s++)
|
|
{
|
|
if(m_sessions[s].total_volume <= 0) continue;
|
|
int y_poc = PriceToY(m_sessions[s].poc_price);
|
|
uint poc_rgb = COLOR2RGB(m_sessions[s].base_color); // Use session color for its POC
|
|
uint poc_clr = ARGB(255, (poc_rgb>>16)&0xFF, (poc_rgb>>8)&0xFF, poc_rgb&0xFF);
|
|
m_canvas.LineHorizontal(x_right - max_draw_width, x_right, y_poc, poc_clr);
|
|
}
|
|
|
|
m_canvas.Update();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Helper to convert price to Y coordinate |
|
|
//+------------------------------------------------------------------+
|
|
int PriceToY(double price)
|
|
{
|
|
double chart_min = ChartGetDouble(0, CHART_PRICE_MIN);
|
|
double chart_max = ChartGetDouble(0, CHART_PRICE_MAX);
|
|
int height = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
|
|
|
|
if(chart_max == chart_min) return 0;
|
|
return (int)((chart_max - price) / (chart_max - chart_min) * height);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
|