//+------------------------------------------------------------------+ //| 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 #include //--- 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); } //+------------------------------------------------------------------+