//+------------------------------------------------------------------+ //| SequentialVolumeProfileWithFVG.mq5 | //| Copyright 2025 | //+------------------------------------------------------------------+ #property copyright "Copyright 2025" #property link "https://www.mql5.com" #property version "1.00" #property indicator_chart_window //--- Input parameters for Volume Profile input int BinsCount=100; // Number of price bins input double ValueAreaPercent=70; // Value Area percentage (70% default) input color VALColor=clrYellow; // Value Area Low color input color VAHColor=clrYellow; // Value Area High color input color AbsLowColor=clrDarkOrange; // Absolute Low color input color AbsHighColor=clrDarkOrange; // Absolute High color input color TimeLineColor=clrRed; // Time marker line color input int LineWidth=2; // Line width for all value lines input int TimeLineWidth=2; // Line width for time marker lines input int MaxDaysBack=30; // Maximum number of trading days to look back input ENUM_LINE_STYLE VALStyle=STYLE_SOLID; // Value Area Low line style input ENUM_LINE_STYLE VAHStyle=STYLE_SOLID; // Value Area High line style input ENUM_LINE_STYLE AbsLowStyle=STYLE_SOLID; // Absolute Low line style input ENUM_LINE_STYLE AbsHighStyle=STYLE_SOLID; // Absolute High line style input bool ShowLabels=true; // Show price labels input bool ShowComment=true; // Show comment with most recent levels //--- Input parameters for Fair Value Gap (FVG) input bool ShowFVG=true; // Enable Fair Value Gap detection input color BullishFVGColor=clrLime; // Bullish FVG color input color BearishFVGColor=clrDeepPink; // Bearish FVG color input double MinFVGSize=0.0; // Minimum FVG size in points (0 = any size) input int MaxBarsBack=300; // How many bars to look back for FVG // Structure to hold volume profile data for a day struct VolumeProfileData { datetime date; // Trading day date datetime startTime; // Start time for calculation (23:00 previous day) datetime endTime; // End time for calculation (23:00 current day) datetime displayStart; // When to start displaying this profile (= endTime) datetime displayEnd; // When to stop displaying this profile (= next day's endTime) double val; // Value Area Low double vah; // Value Area High double poc; // Point of Control (needed for internal calculation) double absLow; // Absolute Low double absHigh; // Absolute High bool calculated; // Whether the calculation is complete }; // Array to store volume profile data for multiple days VolumeProfileData g_Profiles[]; // Prefix for FVG objects string prefix; //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { // Set up FVG object prefix prefix = "VProfFVG_"; // Initialize profile storage ArrayResize(g_Profiles, MaxDaysBack); for(int i = 0; i < MaxDaysBack; i++) { g_Profiles[i].date = 0; g_Profiles[i].startTime = 0; g_Profiles[i].endTime = 0; g_Profiles[i].displayStart = 0; g_Profiles[i].displayEnd = 0; g_Profiles[i].val = 0; g_Profiles[i].vah = 0; g_Profiles[i].poc = 0; g_Profiles[i].absLow = 0; g_Profiles[i].absHigh = 0; g_Profiles[i].calculated = false; } // Initialize all profiles CalculateAllVolumeProfiles(); // Set up timer to check for new day EventSetTimer(60); // Check every minute return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Custom indicator deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { // Clean up chart objects ObjectsDeleteAll(0, "VProfile_"); ObjectsDeleteAll(0, prefix); // Kill the timer EventKillTimer(); // Clear the comment Comment(""); } //+------------------------------------------------------------------+ //| Timer function | //+------------------------------------------------------------------+ void OnTimer() { // Check if we need to update the profiles datetime currentTime = TimeCurrent(); MqlDateTime mdt; TimeToStruct(currentTime, mdt); // Check if it's near the 23:00 boundary (update a bit before and after) // if((mdt.hour == 22 && mdt.min >= 59) || (mdt.hour == 23 && mdt.min <= 5)) if(newDayBar()) { CalculateAllVolumeProfiles(); } } //+------------------------------------------------------------------+ //| Helper function to round a value to the specified tick size | //+------------------------------------------------------------------+ double RoundToTickSize(double value, double tickSize) { return MathRound(value / tickSize) * tickSize; } //+------------------------------------------------------------------+ //| 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[]) { // Check for insufficient data if(rates_total < 3) return 0; if(newDayBar()) { CalculateAllVolumeProfiles(); } // Calculate Volume Profiles if needed //if(prev_calculated == 0) // { // CalculateAllVolumeProfiles(); // } // Detect Fair Value Gaps if enabled if(ShowFVG) { // Prepare arrays ArraySetAsSeries(open, true); ArraySetAsSeries(high, true); ArraySetAsSeries(low, true); ArraySetAsSeries(close, true); ArraySetAsSeries(time, true); // Clear existing FVG objects if recalculating all if(prev_calculated == 0) { ObjectsDeleteAll(0, prefix); } // Determine calculation starting point int limit; if(prev_calculated == 0) { // Calculate for all bars within MaxBarsBack limit = MathMin(MaxBarsBack, rates_total - 3); } else { // Recalculate only for new bars plus a few previous ones limit = rates_total - prev_calculated + 3; limit = MathMin(limit, MaxBarsBack); } // Ensure we don't exceed available bars limit = MathMin(limit, rates_total - 3); // Scan for Fair Value Gaps for(int i = 0; i < limit && !IsStopped(); i++) { // Check for bullish FVG (gap up) // A bullish FVG occurs when low[i] > high[i+2] if(low[i] - high[i+2] >= MinFVGSize * Point()) { // Calculate the FVG boundaries double upper = MathMin(high[i], low[i]); double lower = MathMax(high[i+2], low[i+2]); // Draw the bullish FVG area DrawFVGArea(i, upper, lower, time, BullishFVGColor, 1); } // Check for bearish FVG (gap down) // A bearish FVG occurs when low[i+2] > high[i] if(low[i+2] - high[i] >= MinFVGSize * Point()) { // Calculate the FVG boundaries double upper = MathMin(high[i+2], low[i+2]); double lower = MathMax(high[i], low[i]); // Draw the bearish FVG area DrawFVGArea(i, upper, lower, time, BearishFVGColor, 0); } } } return(rates_total); } //+------------------------------------------------------------------+ //| Draw Fair Value Gap area as a rectangle | //+------------------------------------------------------------------+ void DrawFVGArea(const int index, const double price_up, const double price_dn, const datetime &time[], const color color_area, const char dir) { string name = prefix + (dir > 0 ? "up_" : "dn_") + TimeToString(time[index]); // Create or update the rectangle object if(ObjectFind(0, name) < 0) ObjectCreate(0, name, OBJ_RECTANGLE, 0, 0, 0, 0); // Set object properties ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, name, OBJPROP_HIDDEN, true); ObjectSetInteger(0, name, OBJPROP_FILL, true); ObjectSetInteger(0, name, OBJPROP_BACK, true); ObjectSetString(0, name, OBJPROP_TOOLTIP, "\n"); // Set rectangle coordinates and color ObjectSetInteger(0, name, OBJPROP_COLOR, color_area); ObjectSetInteger(0, name, OBJPROP_TIME, 0, time[index+2]); ObjectSetInteger(0, name, OBJPROP_TIME, 1, time[index]); ObjectSetDouble(0, name, OBJPROP_PRICE, 0, price_up); ObjectSetDouble(0, name, OBJPROP_PRICE, 1, price_dn); } //+------------------------------------------------------------------+ //| Calculate all volume profiles up to MaxDaysBack | //+------------------------------------------------------------------+ void CalculateAllVolumeProfiles() { // Clear existing objects ObjectsDeleteAll(0, "VProfile_"); // Get current time datetime currentTime = TimeCurrent(); // Create a list of trading days going back MaxDaysBack days int calculatedDays = 0; datetime tradingDays[]; ArrayResize(tradingDays, MaxDaysBack); // Get the current day datetime currentDay = currentTime; MqlDateTime mdt; TimeToStruct(currentDay, mdt); mdt.hour = 0; mdt.min = 0; mdt.sec = 0; currentDay = StructToTime(mdt); // Fill the array with trading days for(int i = 0; i < MaxDaysBack * 2; i++) // Check twice as many days to account for weekends { // Go back one day datetime checkDay = currentDay - (i * 86400); // Skip weekends TimeToStruct(checkDay, mdt); if(mdt.day_of_week == 0 || mdt.day_of_week == 6) // Sunday or Saturday continue; tradingDays[calculatedDays++] = checkDay; if(calculatedDays >= MaxDaysBack) break; } // Now, calculate volume profiles for each trading day for(int i = 0; i < calculatedDays; i++) { datetime tradingDay = tradingDays[i]; // Store the date g_Profiles[i].date = tradingDay; // Calculate time boundaries CalculateTimeBoundaries(i); // For display purposes, set the display end of the current profile // to the display start of the previous profile if(i > 0) { g_Profiles[i].displayEnd = g_Profiles[i-1].displayStart; } else { // For the most recent profile, display until far future g_Profiles[i].displayEnd = D'2050.01.01 00:00:00'; } // Check if we have a valid calculation for this day //if(!g_Profiles[i].calculated) { CalculateVolumeProfileForDay(i); } // Draw this profile's time markers and levels DrawVolumeProfile(i); } // Update comment with the most recent profile (index 0) if(ShowComment && calculatedDays > 0) { string info = "Volume Profile (TradingView 23:00-23:00 UTC+2)\n" + "Date: " + TimeToString(g_Profiles[0].date, TIME_DATE) + " (" + GetDayOfWeekName(g_Profiles[0].date) + ")\n" + "Value Area: " + DoubleToString(ValueAreaPercent, 0) + "%\n" + "VAL: " + DoubleToString(g_Profiles[0].val, _Digits) + "\n" + "VAH: " + DoubleToString(g_Profiles[0].vah, _Digits) + "\n" + "AbsLow: " + DoubleToString(g_Profiles[0].absLow, _Digits) + "\n" + "AbsHigh: " + DoubleToString(g_Profiles[0].absHigh, _Digits); Comment(info); } } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ bool newDayBar() { static datetime lastbar; datetime curbar = iTime(Symbol(), PERIOD_D1, 0); if(lastbar != curbar) { lastbar = curbar; Print(" ---------------------- New Day Bar :: ---------------------- ",lastbar); return (true); } else { return (false); } } //+------------------------------------------------------------------+ //| Calculate time boundaries for a profile | //+------------------------------------------------------------------+ void CalculateTimeBoundaries(int index) { // Get trading day datetime tradingDay = g_Profiles[index].date; // Get the day before trading day datetime dayBeforeTradingDay = tradingDay - 86400; // Check and adjust for weekends MqlDateTime mdt; TimeToStruct(dayBeforeTradingDay, mdt); int dayOfWeek = mdt.day_of_week; // For Sunday, go back 2 more days to Friday if(dayOfWeek == 0) { int twoDaysInSeconds = 172800; // 2*86400 dayBeforeTradingDay = dayBeforeTradingDay - twoDaysInSeconds; } // For Saturday, go back 1 more day to Friday if(dayOfWeek == 6) { int oneDayInSeconds = 86400; dayBeforeTradingDay = dayBeforeTradingDay - oneDayInSeconds; } // Format date strings for times MqlDateTime tradingDayMdt; TimeToStruct(tradingDay, tradingDayMdt); string tradingDayStr = StringFormat("%04d.%02d.%02d", tradingDayMdt.year, tradingDayMdt.mon, tradingDayMdt.day); MqlDateTime beforeMdt; TimeToStruct(dayBeforeTradingDay, beforeMdt); string dayBeforeTradingDayStr = StringFormat("%04d.%02d.%02d", beforeMdt.year, beforeMdt.mon, beforeMdt.day); // Calculate start and end times g_Profiles[index].startTime = StringToTime(dayBeforeTradingDayStr + " 23:00:00"); g_Profiles[index].endTime = StringToTime(tradingDayStr + " 23:00:00"); g_Profiles[index].displayStart = g_Profiles[index].endTime; } //+------------------------------------------------------------------+ //| Calculate volume profile for a specific day | //+------------------------------------------------------------------+ void CalculateVolumeProfileForDay(int index) { datetime tradingDay = g_Profiles[index].date; datetime startTime = g_Profiles[index].startTime; datetime endTime = g_Profiles[index].endTime; Print("Calculating volume profile for ", TimeToString(tradingDay, TIME_DATE), " (", GetDayOfWeekName(tradingDay), ")"); Print("Time range: ", TimeToString(startTime), " to ", TimeToString(endTime), " Index: ", index); // Copy the OHLCV data for this day - using M1 timeframe for precision MqlRates rates[]; int copied = CopyRates(_Symbol, PERIOD_M1, startTime, endTime, rates); if(copied <= 0) { Print("Failed to copy rates data for ", TimeToString(tradingDay, TIME_DATE), ". Error: ", GetLastError()); g_Profiles[index].calculated = false; return; } Print("Copied ", copied, " bars for volume profile calculation"); // Find high and low for the day double dayHigh = DBL_MIN; double dayLow = DBL_MAX; for(int i = 0; i < copied; i++) { if(rates[i].high > dayHigh) dayHigh = rates[i].high; if(rates[i].low < dayLow) dayLow = rates[i].low; } // Check if we have valid high and low if(dayHigh <= dayLow || dayLow == DBL_MAX || dayHigh == DBL_MIN) { Print("Invalid high/low values. Calculation aborted."); g_Profiles[index].calculated = false; return; } // EXACTLY MATCH TRADINGVIEW LOGIC: Calculate tick size as in PineScript // First get minimum tick size for the instrument double minTick = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); // Calculate default tick size based on price range and bin count (matches TradingView more closely) double priceRange = dayHigh - dayLow; // Match the PineScript index_num calculation: math.floor(1000/lb_days)-1 int index_num = (int)MathFloor(1000.0 / BinsCount) - 1; // Match TradingView tick_size calculation: // tick_size = round_to(math.max(((roof - base)/index_num),syminfo.mintick),(syminfo.mintick/100)) double tickSize = MathMax((priceRange / index_num), minTick); tickSize = RoundToTickSize(tickSize, minTick / 100.0); Print("Using tick size: ", tickSize, " for volume profile calculation"); // Base and roof price levels (direct from TradingView code) double base = dayLow; double roof = dayHigh; // Calculate maximum number of bins needed int bins = (int)MathCeil((roof - base) / tickSize) + 1; // Arrays to store volume at each price level double binVolume[]; ArrayResize(binVolume, bins); // Initialize to zeros for(int i = 0; i < bins; i++) binVolume[i] = 0; // Process candles as in TradingView code for(int i = 0; i < copied; i++) { // Round high and low to the tickSize (match TradingView's c_hi and c_lo) double c_hi = RoundToTickSize(rates[i].high, tickSize); double c_lo = RoundToTickSize(rates[i].low, tickSize); // Calculate candle range and index as in TradingView double candle_range = c_hi - c_lo; int candle_index = (int)(candle_range / tickSize) + 1; // Calculate tick volume (matching PineScript tick_vol calculation) // In TradingView: tick_vol = _mp?1:volume/candle_index // We're always using real volume (mp = false), so: double tick_vol = rates[i].tick_volume / candle_index; // Loop through price levels covered by this candle for(int priceLevel = 0; priceLevel < bins; priceLevel++) { double index_price = base + (priceLevel * tickSize); // Check if this price level is within the candle's range if(index_price <= c_hi && index_price >= c_lo) { binVolume[priceLevel] += tick_vol; } } } // Store absolute high and low - use the exact values from calculation g_Profiles[index].absLow = base; g_Profiles[index].absHigh = roof; // Calculate total volume double totalVolume = 0; for(int i = 0; i < bins; i++) { totalVolume += binVolume[i]; } // Safety check for total volume if(totalVolume <= 0) { Print("No volume data for ", TimeToString(tradingDay, TIME_DATE), ". Calculation aborted."); g_Profiles[index].calculated = false; return; } // Find max volume index - EXACTLY match TradingView's POC calculation // In TradingView: max_index = math.round(math.avg(array.indexof(main,array.max(main)), array.lastindexof(main,array.max(main)))) double maxVolume = 0; int firstMaxIdx = 0; int lastMaxIdx = 0; // First find the maximum volume for(int i = 0; i < bins; i++) { if(binVolume[i] > maxVolume) { maxVolume = binVolume[i]; } } // Then find first and last indices with this max volume for(int i = 0; i < bins; i++) { if(binVolume[i] == maxVolume) { firstMaxIdx = i; break; } } for(int i = bins - 1; i >= 0; i--) { if(binVolume[i] == maxVolume) { lastMaxIdx = i; break; } } // Calculate POC index as average of first and last max volume index (exactly as TradingView) int pocIndex = (int)MathRound((firstMaxIdx + lastMaxIdx) / 2.0); // Calculate POC price double poc = base + (pocIndex * tickSize); g_Profiles[index].poc = poc; // EXACTLY match TradingView Value Area calculation double valueAreaThreshold = totalVolume * ValueAreaPercent / 100.0; double accumulatedVolume = pocIndex >= 0 ? binVolume[pocIndex] : 0; int upCount = pocIndex; int downCount = pocIndex; // Follow the TradingView algorithm precisely while(accumulatedVolume < valueAreaThreshold && (upCount < bins - 1 || downCount > 0)) { // Get upper and lower volumes exactly as in TradingView double upperVol = (upCount < bins - 1) ? binVolume[upCount + 1] : 0; double lowerVol = (downCount > 0) ? binVolume[downCount - 1] : 0; // Implement the exact TradingView condition: // if ((uppervol >= lowervol) and not na(uppervol)) or na(lowervol) if((upperVol >= lowerVol && upperVol > 0) || lowerVol == 0) { upCount += 1; accumulatedVolume += upperVol; } else { downCount -= 1; accumulatedVolume += lowerVol; } } // Calculate VAL and VAH exactly as in TradingView double val = base + (downCount * tickSize); double vah = base + (upCount * tickSize); // Store VAL and VAH g_Profiles[index].val = val; g_Profiles[index].vah = vah; // Mark as calculated g_Profiles[index].calculated = true; Print("Volume profile levels calculated for ", TimeToString(tradingDay, TIME_DATE), ": POC=", poc, ", VAL=", val, ", VAH=", vah, ", AbsLow=", base, ", AbsHigh=", roof); } //+------------------------------------------------------------------+ //| Draw volume profile lines and time markers | //+------------------------------------------------------------------+ void DrawVolumeProfile(int index) { // Skip if not calculated if(!g_Profiles[index].calculated) return; // Create a unique suffix based on the date string dateSuffix = TimeToString(g_Profiles[index].date, TIME_DATE); // Draw time markers at the boundaries (start and end of calculation) DrawTimeLine("StartTime_" + dateSuffix, g_Profiles[index].startTime, TimeLineColor, TimeLineWidth); DrawTimeLine("EndTime_" + dateSuffix, g_Profiles[index].endTime, TimeLineColor, TimeLineWidth); // Draw the volume profile levels between displayStart and displayEnd times DrawHorizontalLineWithinRange("VAL_" + dateSuffix, g_Profiles[index].val, VALColor, VALStyle, LineWidth, g_Profiles[index].displayStart, g_Profiles[index].displayEnd); DrawHorizontalLineWithinRange("VAH_" + dateSuffix, g_Profiles[index].vah, VAHColor, VAHStyle, LineWidth, g_Profiles[index].displayStart, g_Profiles[index].displayEnd); DrawHorizontalLineWithinRange("AbsLow_" + dateSuffix, g_Profiles[index].absLow, AbsLowColor, AbsLowStyle, LineWidth, g_Profiles[index].displayStart, g_Profiles[index].displayEnd); DrawHorizontalLineWithinRange("AbsHigh_" + dateSuffix, g_Profiles[index].absHigh, AbsHighColor, AbsHighStyle, LineWidth, g_Profiles[index].displayStart, g_Profiles[index].displayEnd); } //+------------------------------------------------------------------+ //| Helper function to get day of week name | //+------------------------------------------------------------------+ string GetDayOfWeekName(datetime date) { MqlDateTime mdt; TimeToStruct(date, mdt); // Use direct if statements instead of arrays if(mdt.day_of_week == 0) return "Sunday"; if(mdt.day_of_week == 1) return "Monday"; if(mdt.day_of_week == 2) return "Tuesday"; if(mdt.day_of_week == 3) return "Wednesday"; if(mdt.day_of_week == 4) return "Thursday"; if(mdt.day_of_week == 5) return "Friday"; if(mdt.day_of_week == 6) return "Saturday"; return "Unknown"; } //+------------------------------------------------------------------+ //| Draw a time marker vertical line | //+------------------------------------------------------------------+ void DrawTimeLine(string name, datetime time, color clr, int width) { string objName = "VProfile_" + name; if(ObjectFind(0, objName) >= 0) ObjectDelete(0, objName); ObjectCreate(0, objName, OBJ_VLINE, 0, time, 0); ObjectSetInteger(0, objName, OBJPROP_COLOR, clr); ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_SOLID); ObjectSetInteger(0, objName, OBJPROP_WIDTH, width); ObjectSetInteger(0, objName, OBJPROP_BACK, false); } //+------------------------------------------------------------------+ //| Draw a horizontal line between two time points | //+------------------------------------------------------------------+ void DrawHorizontalLineWithinRange(string name, double price, color clr, ENUM_LINE_STYLE style, int width, datetime startTime, datetime endTime) { string objName = "VProfile_" + name; if(ObjectFind(0, objName) >= 0) ObjectDelete(0, objName); // Create a trend line instead of a horizontal line to limit its display range ObjectCreate(0, objName, OBJ_TREND, 0, startTime, price, endTime, price); ObjectSetInteger(0, objName, OBJPROP_COLOR, clr); ObjectSetInteger(0, objName, OBJPROP_STYLE, style); ObjectSetInteger(0, objName, OBJPROP_WIDTH, width); ObjectSetInteger(0, objName, OBJPROP_BACK, false); ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, objName, OBJPROP_RAY_RIGHT, false); // Don't extend line past end point // Add price label if enabled if(ShowLabels) { string labelName = objName + "_Label"; if(ObjectFind(0, labelName) >= 0) ObjectDelete(0, labelName); // Place label at the middle of the line datetime labelTime = startTime + ((endTime - startTime) / 2); ObjectCreate(0, labelName, OBJ_TEXT, 0, labelTime, price); ObjectSetString(0, labelName, OBJPROP_TEXT, name + ": " + DoubleToString(price, _Digits)); ObjectSetInteger(0, labelName, OBJPROP_COLOR, clr); ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 8); ObjectSetInteger(0, labelName, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER); } } //+------------------------------------------------------------------+