MQL5 Trading Tools (Part 22): Graphing the Histogram and Probability Mass Function (PMF) of the Binomial Distribution
Introduction
You have a trading system that produces a series of wins and losses, yet you cannot answer simple but critical questions: "What is the probability of having 20 winning trades out of 30 if my historical win rate is 75%?" or "How likely is a losing streak of 5 trades?" Without a way to model the binomial distribution, you are left guessing – you cannot assess risk realistically, set appropriate position sizes, or validate whether your strategy’s performance is statistically meaningful. This lack of probabilistic insight often leads to overleveraging, emotional decisions, and unrealistic profit expectations. This article is built for MetaQuotes Language 5 (MQL5) developers and algorithmic traders who want to quantify the probabilistic behaviour of their trading strategies.
Now, in our previous article (Part 21), we enhanced the regression graphing tool in MQL5 by adding a cyberpunk theme mode with neon glows, animations, and holographic effects for immersive visualization. In Part 22, we build an MQL5 graphing tool to visualize the binomial distribution with a histogram of simulated samples and the theoretical probability mass function curve on an interactive canvas. We add advanced statistics, including mean, standard deviation, skewness, kurtosis, percentiles, and confidence intervals, as well as customizable themes, gradients, and labels. Additionally, we enable dragging, resizing, real-time updates, and parameter adjustments for trials, probability, sample size, and display, supporting trading analysis. We will cover the following topics:
By the end, you’ll have a functional MQL5 graphing tool for visualizing binomial distributions, ready for customization—let’s dive in!
Understanding the Binomial Distribution Framework
The binomial distribution models the number of successes in a fixed number of independent trials, each with the same probability of success. In trading, a "trial" can be any binary event: a trade ends in profit (success) or loss (failure), a signal is valid or invalid, or a market condition is met or not. If your strategy has a historical win rate of "p", the binomial distribution tells you how likely it is to obtain "k" wins out of "n" trades purely by chance. Here is how we can use it in the market:
- Estimate the probability of reaching a profit target: Suppose your system wins 60% of the time. If you plan to take 20 trades in a month, you can calculate the chance of having at least 12 winners – a valuable reality check.
- Set realistic drawdown expectations: The distribution helps you anticipate the maximum likely losing streak. If the probability of 5 consecutive losses is very low, a streak of that length may signal a strategy breakdown.
- Compare strategies: Two systems may have similar win rates, but the one with a tighter distribution (lower variance) is more reliable – you can see this from the histogram and standard deviation.
- Validate sample size: The confidence intervals show you how precise your estimated win rate is. Wide intervals mean you need more trades to be confident in the system’s true performance.
Our plan is to simulate samples from the binomial distribution using specified trials and success probability, compute a histogram of the empirical frequencies, overlay the theoretical probability mass function curve for comparison, and display key statistics like mean, standard deviation, skewness, kurtosis, percentiles, and confidence intervals. We will render this on a resizable and draggable canvas with customizable themes, background gradients, axis labels, and a legend, while enabling real-time updates based on chart timeframe changes to provide interactive probabilistic analysis. In brief, this setup creates a tool for exploring distribution properties and their implications in trading scenarios. See what we will be achieving.

Trading Meaning of the Features
Before diving into the implementation, let’s clarify why each component of this tool matters from a trader’s perspective, as earlier on stated.
- Histogram (empirical frequencies): Shows how many times each number of successes occurred in your simulated sample. A tall bar at a certain "k" means that outcome is common – this is your strategy’s "typical" behaviour. If the histogram is spread wide, your system’s results are highly variable.
- Theoretical PMF curve: The smooth line represents the true binomial probabilities. By overlaying it on the histogram, you instantly see whether your simulated sample matches the expected distribution. Large discrepancies might indicate that your sample size is too small or that your assumption of independent trials is violated.
- Mean: The average number of successes. For a system with win rate "p" and "n" trades, the mean is n × p. If your actual mean deviates significantly, your win rate may differ from what you thought.
- Standard deviation: Measures the spread of outcomes. A smaller standard deviation means your system’s results are more consistent – crucial for risk management.
- Skewness: Tells you whether the distribution is symmetric or leans left/right. Positive skewness (long right tail) means occasional very good runs; negative skewness warns of occasional devastating losses.
- Kurtosis: Indicates how prone the distribution is to extreme outcomes. High kurtosis (fat tails) suggests that large winning or losing streaks happen more often than a normal distribution would predict – a key consideration for stop‑loss placement.
- Confidence intervals (95%, 99%): Provide a range within which the true mean of your system’s win rate is likely to fall. Narrow intervals give you confidence that your observed performance is reliable; wide intervals signal that you need more data.
With these statistics, you move beyond simple win rate and start to understand the shape of your trading outcomes – which is far more valuable for risk assessment. Let us now begin the implementation.
Implementation in MQL5
To create the program in MQL5, open the MetaEditor, go to the Navigator, locate the Experts folder, click on the "New" tab, and follow the prompts to create the file. Once it is made, in the coding environment, we will need to declare some input parameters and global variables that we will use throughout the program.
Setting Up the Canvas and Core Libraries
To begin building the program we first include the necessary libraries and define supporting structures.
//+------------------------------------------------------------------+ //| Canvas Graphing PART 3 - Statistical Distributions.mq5 | //| Copyright 2026, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #property strict #include <Canvas\Canvas.mqh> #include <Math\Stat\Binomial.mqh> #include <Math\Stat\Math.mqh> //+------------------------------------------------------------------+ //| Resize direction enumeration | //+------------------------------------------------------------------+ enum ResizeDirection { NO_RESIZE, // No resize action RESIZE_BOTTOM_EDGE, // Resize using bottom edge RESIZE_RIGHT_EDGE, // Resize using right edge RESIZE_CORNER // Resize using bottom-right corner }; //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input group "=== DISTRIBUTION SETTINGS ===" input int numTrials = 40; // Number of trials (n) for binomial distribution input double successProbability = 0.75; // Success probability (p) per trial (0.0-1.0) input int sampleSize = 1000000; // Sample size for generating histogram data input int histogramCells = 20; // Number of cells (bins) in the histogram input int histogramGapPixels = 2; // Gap in pixels between adjacent histogram bars input ENUM_TIMEFRAMES chartTimeframe = PERIOD_CURRENT; // Timeframe used for new-bar detection input group "=== CANVAS DISPLAY SETTINGS ===" input int initialCanvasX = 20; // Initial X position of canvas on chart (pixels) input int initialCanvasY = 30; // Initial Y position of canvas on chart (pixels) input int initialCanvasWidth = 600; // Initial width of canvas in pixels input int initialCanvasHeight = 400; // Initial height of canvas in pixels input int plotPadding = 10; // Internal padding around plot area in pixels input group "=== THEME COLOR (SINGLE CONTROL!) ===" input color themeColor = clrDodgerBlue; // Master theme color controlling all UI accents input bool showBorderFrame = true; // Display decorative border frame around canvas input group "=== HISTOGRAM AND CURVE SETTINGS ===" input color histogramColor = clrRed; // Fill color for histogram bars input color theoreticalCurveColor = clrBlue; // Color of theoretical probability mass function curve input int curveLineWidth = 2; // Thickness in pixels of the theoretical curve input group "=== BACKGROUND SETTINGS ===" input bool enableBackgroundFill = true; // Enable gradient background fill inside canvas input color backgroundTopColor = clrWhite; // Top color of the gradient background input double backgroundOpacityLevel = 0.95; // Background opacity level (0.0 fully transparent - 1.0 opaque) input group "=== TEXT AND LABELS ===" input int titleFontSize = 14; // Font size for main window title input color titleTextColor = clrBlack; // Color of the main title text input int labelFontSize = 11; // Font size for general labels and legend input color labelTextColor = clrBlack; // Color of general label text input int axisLabelFontSize = 12; // Font size for axis tick labels input bool showStatistics = true; // Show statistics panel and legend input group "=== STATS & LEGEND PANEL SETTINGS ===" input int statsPanelX = 70; // X position of statistics panel inside canvas input int statsPanelY = 10; // Y offset of statistics panel from header input int statsPanelWidth = 130; // Width of statistics panel in pixels input int statsPanelHeight = 175; // Height of statistics panel in pixels input int panelFontSize = 13; // Font size used in stats and legend panels input int legendHeight = 35; // Height of legend panel in pixels input group "=== INTERACTION SETTINGS ===" input bool enableDragging = true; // Allow dragging canvas by clicking header input bool enableResizing = true; // Allow resizing canvas with mouse grips input int resizeGripSize = 8; // Size of resize grip detection zones in pixels //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ CCanvas mainCanvas; // Main canvas object for all drawing string canvasObjectName = "DistributionCanvas_Main"; // Name of the graphical object on chart int currentPositionX = initialCanvasX; // Current X coordinate of canvas int currentPositionY = initialCanvasY; // Current Y coordinate of canvas int currentWidthPixels = initialCanvasWidth; // Current canvas width in pixels int currentHeightPixels = initialCanvasHeight; // Current canvas height in pixels bool isDraggingCanvas = false; // True while canvas is being dragged bool isResizingCanvas = false; // True while canvas is being resized int dragStartX = 0; // Mouse X when drag started int dragStartY = 0; // Mouse Y when drag started int canvasStartX = 0; // Canvas X when drag started int canvasStartY = 0; // Canvas Y when drag started int resizeStartX = 0; // Mouse X when resize started int resizeStartY = 0; // Mouse Y when resize started int resizeInitialWidth = 0; // Width when resize started int resizeInitialHeight = 0; // Height when resize started ResizeDirection activeResizeMode = NO_RESIZE; // Currently active resize direction ResizeDirection hoverResizeMode = NO_RESIZE; // Hover resize direction bool isHoveringCanvas = false; // Mouse is over canvas area bool isHoveringHeader = false; // Mouse is over header bar bool isHoveringResizeZone = false; // Mouse is over a resize grip int lastMouseX = 0; // Last recorded mouse X int lastMouseY = 0; // Last recorded mouse Y int previousMouseButtonState = 0; // Previous mouse button state const int MIN_CANVAS_WIDTH = 300; // Minimum allowed canvas width const int MIN_CANVAS_HEIGHT = 200; // Minimum allowed canvas height const int HEADER_BAR_HEIGHT = 35; // Fixed height of header bar double sampleData[]; // Generated binomial sample values double histogramIntervals[]; // Center positions of histogram bins double histogramFrequencies[]; // Frequency count per histogram bin double theoreticalXValues[]; // X values for theoretical PMF double theoreticalYValues[]; // Probability values for theoretical PMF double minDataValue = 0.0; // Minimum value in sample data double maxDataValue = 0.0; // Maximum value in sample data double maxFrequency = 0.0; // Highest histogram frequency double maxTheoreticalValue = 0.0; // Highest theoretical probability bool dataLoadedSuccessfully = false; // True after successful data load double sampleMean = 0.0; // Sample mean double sampleStandardDeviation = 0.0; // Sample standard deviation double sampleSkewness = 0.0; // Sample skewness double sampleKurtosis = 0.0; // Sample kurtosis double percentile25 = 0.0; // 25th percentile double percentile50 = 0.0; // 50th percentile (median) double percentile75 = 0.0; // 75th percentile double confidenceInterval95Lower = 0.0; // 95% CI lower bound double confidenceInterval95Upper = 0.0; // 95% CI upper bound double confidenceInterval99Lower = 0.0; // 99% CI lower bound double confidenceInterval99Upper = 0.0; // 99% CI upper bound
We begin the implementation by including essential libraries with "#include <Canvas\Canvas.mqh>" for canvas-based graphics, "#include <Math\Stat\Binomial.mqh>" to handle binomial distribution functions, and "#include <Math\Stat\Math.mqh>" for general mathematical and statistical utilities, setting up the foundation for visualization and computations. Next, we define the "ResizeDirection" enumeration to manage canvas resizing options, including "NO_RESIZE" as the default state, "RESIZE_BOTTOM_EDGE" for vertical adjustments, "RESIZE_RIGHT_EDGE" for horizontal changes, and "RESIZE_CORNER" for diagonal resizing, which will support interactive user controls later.
We then declare a series of input parameters organized into groups using string input group for better user interface categorization in the program settings. These include distribution parameters like number of trials, success probability, sample size, histogram cells, bar gaps, and chart timeframe; canvas display settings such as initial positions, dimensions, and plot padding; a master theme color with border visibility; histogram and curve colors along with curve width; background fill options with top color and opacity; text and label sizes, colors, and statistics display toggle; stats and legend panel positions, sizes, and fonts; and interaction settings for dragging, resizing, and grip size, allowing us to customize the tool's behavior and appearance.
Finally, we initialize global variables to track the program's state, starting with the CCanvas class instance "mainCanvas" for rendering, a string for the canvas object name, current positions and dimensions set to input defaults, boolean flags for dragging, resizing, and hovering states, coordinate trackers for drag and resize operations, the active and hover resize modes using the enumeration, mouse tracking variables, constant minimum canvas sizes and header height, arrays for sample data, histogram intervals and frequencies, theoretical values, min/max trackers, a data load flag, and statistical measures like mean, standard deviation, skewness, kurtosis, percentiles, and confidence interval bounds, preparing the structure for data handling and interactive rendering. These libraries let us move instantly from theoretical formulas to live visuals. Now, we will define some helper functions to make the code modular for easier management. We will start with the theme color helper functions.
//+------------------------------------------------------------------+ //| Lighten the base color | //+------------------------------------------------------------------+ color LightenColor(color baseColor, double factor) { //--- Extract red component from base color uchar r = (uchar)((baseColor >> 16) & 0xFF); //--- Extract green component from base color uchar g = (uchar)((baseColor >> 8) & 0xFF); //--- Extract blue component from base color uchar b = (uchar)(baseColor & 0xFF); //--- Lighten red component r = (uchar)MathMin(255, r + (255 - r) * factor); //--- Lighten green component g = (uchar)MathMin(255, g + (255 - g) * factor); //--- Lighten blue component b = (uchar)MathMin(255, b + (255 - b) * factor); //--- Reassemble ARGB and return lightened color return (r << 16) | (g << 8) | b; } //+------------------------------------------------------------------+ //| Darken the base color | //+------------------------------------------------------------------+ color DarkenColor(color baseColor, double factor) { //--- Extract red component from base color uchar r = (uchar)((baseColor >> 16) & 0xFF); //--- Extract green component from base color uchar g = (uchar)((baseColor >> 8) & 0xFF); //--- Extract blue component from base color uchar b = (uchar)(baseColor & 0xFF); //--- Darken red component r = (uchar)(r * (1.0 - factor)); //--- Darken green component g = (uchar)(g * (1.0 - factor)); //--- Darken blue component b = (uchar)(b * (1.0 - factor)); //--- Reassemble ARGB and return darkened color return (r << 16) | (g << 8) | b; }
To handle color variations in the theme, we create the "LightenColor" function which takes a base color and a factor to produce a lighter shade. It extracts the red, green, and blue components using bit shift operations and masks, then adjusts each by adding a portion of the remaining intensity to 255 multiplied by the factor, capping at 255 with MathMin, and recombines them into a color value using bit shifts. Similarly, we define the "DarkenColor" function to generate a darker version by extracting the RGB components in the same way, then multiplying each by one minus the factor to reduce intensity, and returning the recombined color. These helpers will enable dynamic shading for elements like headers, borders, and backgrounds based on the master theme color. We can now define helpers for mathematical calculations.
Statistics Calculation Functions
Because we need more than just a pretty picture, we implement a complete set of statistical helpers.
//+------------------------------------------------------------------+ //| Calculate mean of array | //+------------------------------------------------------------------+ double CalculateMean(const double &data[]) { //--- Get current array size int size = ArraySize(data); //--- Return zero for empty array if (size == 0) return 0.0; //--- Initialize running sum double sum = 0.0; //--- Accumulate every element for (int i = 0; i < size; i++) { sum += data[i]; } //--- Return arithmetic mean return sum / size; } //+------------------------------------------------------------------+ //| Calculate standard deviation of array | //+------------------------------------------------------------------+ double CalculateStandardDeviation(const double &data[], double mean) { //--- Get current array size int size = ArraySize(data); //--- Return zero for insufficient data if (size <= 1) return 0.0; //--- Initialize sum of squared differences double sumSquaredDiff = 0.0; //--- Loop through all values for (int i = 0; i < size; i++) { //--- Compute deviation from mean double diff = data[i] - mean; //--- Accumulate squared deviation sumSquaredDiff += diff * diff; } //--- Return sample standard deviation return MathSqrt(sumSquaredDiff / (size - 1)); } //+------------------------------------------------------------------+ //| Calculate skewness of array | //+------------------------------------------------------------------+ double CalculateSkewness(const double &data[], double mean, double stdDev) { //--- Get current array size int size = ArraySize(data); //--- Return zero for insufficient data or zero std dev if (size < 3 || stdDev == 0.0) return 0.0; //--- Initialize sum of cubed standardized differences double sumCubedDiff = 0.0; //--- Loop through all values for (int i = 0; i < size; i++) { //--- Standardize the deviation double diff = (data[i] - mean) / stdDev; //--- Accumulate cubed term sumCubedDiff += diff * diff * diff; } //--- Cast size to double for formula double n = (double)size; //--- Apply bias-corrected skewness formula and return return (n / ((n - 1) * (n - 2))) * sumCubedDiff; } //+------------------------------------------------------------------+ //| Calculate kurtosis of array | //+------------------------------------------------------------------+ double CalculateKurtosis(const double &data[], double mean, double stdDev) { //--- Get current array size int size = ArraySize(data); //--- Return zero for insufficient data or zero std dev if (size < 4 || stdDev == 0.0) return 0.0; //--- Initialize sum of fourth powers of standardized differences double sumFourthPower = 0.0; //--- Loop through all values for (int i = 0; i < size; i++) { //--- Standardize the deviation double diff = (data[i] - mean) / stdDev; //--- Square the standardized value double squared = diff * diff; //--- Accumulate fourth power sumFourthPower += squared * squared; } //--- Cast size to double double n = (double)size; //--- First part of excess kurtosis formula double kurtosis = (n * (n + 1) / ((n - 1) * (n - 2) * (n - 3))) * sumFourthPower; //--- Subtract bias correction term kurtosis -= (3 * (n - 1) * (n - 1)) / ((n - 2) * (n - 3)); //--- Return kurtosis value return kurtosis; } //+------------------------------------------------------------------+ //| Calculate percentile of array | //+------------------------------------------------------------------+ double CalculatePercentile(double &data[], double percentile) { //--- Get current array size int size = ArraySize(data); //--- Return zero for empty array if (size == 0) return 0.0; //--- Prepare sorted copy double sortedData[]; //--- Resize to match original ArrayResize(sortedData, size); //--- Copy original data ArrayCopy(sortedData, data); //--- Sort in ascending order ArraySort(sortedData); //--- Compute rank for interpolation double rank = (percentile / 100.0) * (size - 1); //--- Lower index for interpolation int lowerIndex = (int)MathFloor(rank); //--- Upper index for interpolation int upperIndex = (int)MathCeil(rank); //--- Exact match case if (lowerIndex == upperIndex) { return sortedData[lowerIndex]; } //--- Compute interpolation fraction double fraction = rank - lowerIndex; //--- Linear interpolation and return return sortedData[lowerIndex] + fraction * (sortedData[upperIndex] - sortedData[lowerIndex]); } //+------------------------------------------------------------------+ //| Calculate confidence interval | //+------------------------------------------------------------------+ void CalculateConfidenceInterval(double mean, double stdDev, int in_sampleSize, double zScore, double &lowerBound, double &upperBound) { //--- Compute margin of error double marginOfError = zScore * (stdDev / MathSqrt(in_sampleSize)); //--- Set lower bound lowerBound = mean - marginOfError; //--- Set upper bound upperBound = mean + marginOfError; } //+------------------------------------------------------------------+ //| Compute advanced statistics | //+------------------------------------------------------------------+ void ComputeAdvancedStatistics() { //--- Calculate sample mean sampleMean = CalculateMean(sampleData); //--- Calculate sample standard deviation sampleStandardDeviation = CalculateStandardDeviation(sampleData, sampleMean); //--- Calculate skewness sampleSkewness = CalculateSkewness(sampleData, sampleMean, sampleStandardDeviation); //--- Calculate kurtosis sampleKurtosis = CalculateKurtosis(sampleData, sampleMean, sampleStandardDeviation); //--- Calculate first quartile percentile25 = CalculatePercentile(sampleData, 25.0); //--- Calculate median percentile50 = CalculatePercentile(sampleData, 50.0); //--- Calculate third quartile percentile75 = CalculatePercentile(sampleData, 75.0); //--- Compute 95% confidence interval CalculateConfidenceInterval(sampleMean, sampleStandardDeviation, sampleSize, 1.96, confidenceInterval95Lower, confidenceInterval95Upper); //--- Compute 99% confidence interval CalculateConfidenceInterval(sampleMean, sampleStandardDeviation, sampleSize, 2.576, confidenceInterval99Lower, confidenceInterval99Upper); }
We start by defining the "calculateMean" function to compute the average of the dataset. It retrieves the array size with ArraySize and checks for an empty array, returning zero if so. Then, it initializes a sum variable, loops through each element to accumulate the values, and finally divides the sum by the size to return the mean. Next, we implement the "calculateStandardDeviation" function, which measures the data's dispersion around the provided mean. After getting the size and handling cases with one or fewer elements by returning zero, it accumulates the sum of squared differences from the mean in a loop, then takes the square root using MathSqrt of that sum divided by size minus one, yielding the sample standard deviation.
To assess the asymmetry in the data distribution, we create the "calculateSkewness" function. It first validates the input with at least three elements and a non-zero standard deviation, returning zero otherwise. Within a loop, it normalizes each difference from the mean by dividing by the standard deviation, cubes it, and sums these values. The final skewness is calculated using the formula n / ((n-1)*(n-2)) multiplied by the sum of cubed normalized differences, where n is the dataset size cast to double, providing a measure of whether the distribution tails more to the left (negative) or right (positive), which is crucial for understanding potential biases in binomial outcomes like uneven success probabilities.
Similarly, for evaluating the distribution's tailedness relative to a normal distribution, we define the "calculateKurtosis" function. It requires at least four elements and a non-zero standard deviation; otherwise returning zero. The loop computes normalized differences, squares them, and accumulates the fourth powers. The kurtosis is then derived from the formula (n*(n+1) / ((n-1)(n-2)(n-3))) * sum of fourth powers minus (3*(n-1)^2 / ((n-2)*(n-3))), indicating leptokurtic (positive, heavier tails) or platykurtic (negative, lighter tails) shapes, which helps in assessing outlier risks in trading scenarios modeled by the binomial distribution.
We then add the "calculatePercentile" function to find a specific percentile in the data. It sorts a copy of the array using ArrayResize, "ArrayCopy", and ArraySort, computes the rank as (percentile/100) times (size-1), and interpolates between the lower and upper indices with MathFloor and MathCeil if needed, enabling quartiles for summarizing data spread. For estimating population parameters, we introduce the "calculateConfidenceInterval" void function, which computes the margin of error as the z-score times standard deviation over the square root of sample size via "MathSqrt", then sets the lower and upper bounds by subtracting and adding this margin from the mean, respectively.
Finally, we wrap these computations in the "computeAdvancedStatistics" function, which sequentially calls the above functions to populate global variables: first mean and standard deviation, then skewness and kurtosis using those, followed by 25th, 50th, and 75th percentiles, and confidence intervals for 95% (z=1.96) and 99% (z=2.576) levels, centralizing the statistical analysis for the simulated binomial samples. You can alter these values to your desired ones, though these are the most standard ones. We will now move on to computing the histogram and initializing the canvas.
Data Generation and Histogram Computation
We now load the actual binomial samples and prepare the histogram and theoretical curve.
//+------------------------------------------------------------------+ //| Create distribution canvas | //+------------------------------------------------------------------+ bool CreateCanvas() { //--- Attempt to create bitmap label canvas if (!mainCanvas.CreateBitmapLabel(0, 0, canvasObjectName, currentPositionX, currentPositionY, currentWidthPixels, currentHeightPixels, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Creation failed return false; } //--- Canvas created successfully return true; } //+------------------------------------------------------------------+ //| Load distribution data | //+------------------------------------------------------------------+ bool LoadDistributionData() { //--- Seed random generator with current tick count MathSrand(GetTickCount()); //--- Resize sample array to requested size ArrayResize(sampleData, sampleSize); //--- Generate binomial random samples MathRandomBinomial(numTrials, successProbability, sampleSize, sampleData); //--- Compute histogram from samples if (!ComputeHistogram(sampleData, histogramIntervals, histogramFrequencies, maxDataValue, minDataValue, histogramCells)) { //--- Histogram calculation failed Print("ERROR: Failed to calculate histogram"); //--- Return failure return false; } //--- Resize theoretical arrays ArrayResize(theoreticalXValues, numTrials + 1); ArrayResize(theoreticalYValues, numTrials + 1); //--- Fill X values from 0 to n MathSequence(0, numTrials, 1, theoreticalXValues); //--- Compute binomial PMF values MathProbabilityDensityBinomial(theoreticalXValues, numTrials, successProbability, false, theoreticalYValues); //--- Find maximum histogram frequency maxFrequency = histogramFrequencies[ArrayMaximum(histogramFrequencies)]; //--- Find maximum theoretical probability maxTheoreticalValue = theoreticalYValues[ArrayMaximum(theoreticalYValues)]; //--- Compute scaling factor to match histogram height double scaleFactor = maxFrequency / maxTheoreticalValue; //--- Scale theoretical frequencies to visual match for (int i = 0; i < histogramCells; i++) { histogramFrequencies[i] /= scaleFactor; } //--- Compute all advanced statistics ComputeAdvancedStatistics(); //--- Mark data as ready dataLoadedSuccessfully = true; //--- Log success Print("SUCCESS: Loaded binomial distribution data with advanced statistics"); //--- Return success return true; } //+------------------------------------------------------------------+ //| Compute histogram array with forced range | //+------------------------------------------------------------------+ bool ComputeHistogram(const double &data[], double &intervals[], double &frequency[], double &maxv, double &minv, const int cells = 10) { //--- Invalid cell count aborts if (cells <= 1) return false; //--- Get data length int size = ArraySize(data); //--- Empty data aborts if (size < 1) return false; //--- Force histogram range from 0 to n minv = 0; maxv = numTrials; //--- Compute total range double range = maxv - minv; //--- Compute bin width double width = range / cells; //--- Zero width aborts if (width == 0) return false; //--- Resize output arrays ArrayResize(intervals, cells); ArrayResize(frequency, cells); //--- Initialize interval centers and zero frequencies for (int i = 0; i < cells; i++) { intervals[i] = minv + (i + 0.5) * width; frequency[i] = 0; } //--- Bin each sample value for (int i = 0; i < size; i++) { //--- Current value double val = data[i]; //--- Skip out-of-range values if (val < minv || val > maxv) continue; //--- Compute bin index int ind = (int)((val - minv) / width); //--- Clamp index to valid range if (ind >= cells) ind = cells - 1; if (ind < 0) ind = 0; //--- Increment frequency frequency[ind]++; } //--- Histogram computed successfully return true; }
We define the "CreateCanvas" function to initialize the graphical canvas. It calls the CreateBitmapLabel method on the "mainCanvas" object. The chart ID is zero for the main chart and subwindow zero. Parameters include the canvas object name, current positions, dimensions, and COLOR_FORMAT_ARGB_NORMALIZE for alpha-blended color support. This enables transparent and layered rendering. If creation fails, it returns false. Otherwise, it returns true, ensuring the canvas is ready for drawing operations.
Next, we implement the "loadDistributionData" function to prepare binomial distribution data. We seed the random number generator with MathSrand using the GetTickCount function. This ensures varied simulations in each run. We resize the "sampleData" array to match the input sample size via "ArrayResize". Then, we generate binomial random samples with MathRandomBinomial based on trials, success probability, and sample size. This populates the array with simulated success counts. Afterward, we call "computeHistogram" to bin the data into intervals and frequencies, update min and max values, and handle failure by printing an error and returning false. For theoretical values, we resize "theoreticalXValues" and "theoreticalYValues" arrays to cover zero to the number of trials plus one.
We fill X with integers using MathSequence and compute the probability mass values with MathProbabilityDensityBinomial in non-cumulative mode. We find the maximum frequency and theoretical value using the ArrayMaximum function. Then, we calculate a scale factor to align empirical frequencies with theoretical probabilities. For visual comparison, we loop to divide each frequency by this factor. Finally, we invoke "computeAdvancedStatistics" to derive descriptive metrics. We set the data-loaded flag to true, print a success message, and return true, completing the data preparation for rendering.
To support histogram creation, we create the "computeHistogram" function, which processes the sample data into binned frequencies with a forced range suitable for binomial outcomes. It first checks for invalid cell counts or empty data, returning false if so. We hard-set the minimum value to zero and the maximum to the number of trials, compute the range and bin width, and return false if the width is zero to avoid division issues. After resizing the intervals and frequencies arrays with ArrayResize, we loop to set each interval to the center of its bin by adding half the width to the minimum plus the index times the width, and initialize frequencies to zero.
Then, for each data point, we skip values outside the range, calculate the bin index by subtracting the minimum and dividing by the width, clamp it between zero and cells minus one, and increment the corresponding frequency. This approach ensures a discrete, evenly spaced histogram tailored to binomial integers from zero to n, where n is the number of trials, facilitating accurate empirical distribution visualization against the theoretical curve, and returns true on success. We can initialize this so that it does the computations.
//+------------------------------------------------------------------+ //| Initialize expert | //+------------------------------------------------------------------+ int OnInit() { //--- Copy initial X position from user inputs currentPositionX = initialCanvasX; //--- Copy initial Y position from user inputs currentPositionY = initialCanvasY; //--- Copy initial width from user inputs currentWidthPixels = initialCanvasWidth; //--- Copy initial height from user inputs currentHeightPixels = initialCanvasHeight; //--- Attempt to create the canvas object if (!CreateCanvas()) { //--- Log failure if canvas creation fails Print("ERROR: Failed to create distribution canvas"); //--- Return failure code to stop expert return(INIT_FAILED); } //--- Attempt to load binomial distribution data if (!LoadDistributionData()) { //--- Log failure if data loading fails Print("ERROR: Failed to load distribution data"); //--- Return failure code to stop expert return(INIT_FAILED); } //--- Force immediate chart redraw after initialization ChartRedraw(); //--- Return success code so expert starts normally return(INIT_SUCCEEDED); }
In the OnInit event handler, we assign input values to global variables for canvas setup, setting "currentPositionX" to "initialCanvasX", "currentPositionY" to "initialCanvasY", "currentWidthPixels" to "initialCanvasWidth", and "currentHeightPixels" to "initialCanvasHeight" to position and size the canvas as specified by the user. We then call the "CreateCanvas" function to initialize the bitmap label, and if it returns false, print an error message and return INIT_FAILED to stop the program. Next, we invoke the "loadDistributionData" function to generate samples and prepare data, handling failure by printing an error and returning "INIT_FAILED". Finally, we refresh the chart with ChartRedraw and return INIT_SUCCEEDED to indicate successful initialization. Upon compilation, here is what we get.

Since all the initializations and computations are done, let us render the analysis plot now. We will draw the canvas first.
Rendering Functions – Background, Header, and Main Plot
With data ready we draw the visual layers.
//+------------------------------------------------------------------+ //| Draw gradient background | //+------------------------------------------------------------------+ void DrawGradientBackground() { //--- Compute bottom gradient color from theme color bottomColor = LightenColor(themeColor, 0.85); //--- Loop through every row below the header for (int y = HEADER_BAR_HEIGHT; y < currentHeightPixels; y++) { //--- Compute vertical gradient interpolation factor double gradientFactor = (double)(y - HEADER_BAR_HEIGHT) / (currentHeightPixels - HEADER_BAR_HEIGHT); //--- Interpolate color for current row color currentRowColor = InterpolateColors(backgroundTopColor, bottomColor, gradientFactor); //--- Compute alpha channel value uchar alphaChannel = (uchar)(255 * backgroundOpacityLevel); //--- Convert interpolated color to ARGB format uint argbColor = ColorToARGB(currentRowColor, alphaChannel); //--- Fill entire row with current color for (int x = 0; x < currentWidthPixels; x++) { //--- Set pixel at current position mainCanvas.PixelSet(x, y, argbColor); } } } //+------------------------------------------------------------------+ //| Draw canvas border | //+------------------------------------------------------------------+ void DrawCanvasBorder() { //--- Skip drawing if border is disabled in settings if (!showBorderFrame) return; //--- Choose border color based on resize hover state color borderColor = isHoveringResizeZone ? DarkenColor(themeColor, 0.2) : themeColor; //--- Convert border color to ARGB uint argbBorder = ColorToARGB(borderColor, 255); //--- Draw outer border rectangle mainCanvas.Rectangle(0, 0, currentWidthPixels - 1, currentHeightPixels - 1, argbBorder); //--- Draw inner border rectangle mainCanvas.Rectangle(1, 1, currentWidthPixels - 2, currentHeightPixels - 2, argbBorder); } //+------------------------------------------------------------------+ //| Draw header bar | //+------------------------------------------------------------------+ void DrawHeaderBar() { //--- Declare variable for header background color color headerColor; //--- Choose color when canvas is being dragged if (isDraggingCanvas) { headerColor = DarkenColor(themeColor, 0.1); } //--- Choose color when mouse hovers header else if (isHoveringHeader) { headerColor = LightenColor(themeColor, 0.4); } //--- Default header color when idle else { headerColor = LightenColor(themeColor, 0.7); } //--- Convert header color to ARGB uint argbHeader = ColorToARGB(headerColor, 255); //--- Fill header bar area mainCanvas.FillRectangle(0, 0, currentWidthPixels - 1, HEADER_BAR_HEIGHT, argbHeader); //--- Draw header borders only if enabled if (showBorderFrame) { //--- Convert theme color for borders uint argbBorder = ColorToARGB(themeColor, 255); //--- Draw outer header border mainCanvas.Rectangle(0, 0, currentWidthPixels - 1, HEADER_BAR_HEIGHT, argbBorder); //--- Draw inner header border mainCanvas.Rectangle(1, 1, currentWidthPixels - 2, HEADER_BAR_HEIGHT - 1, argbBorder); } //--- Set bold font for title mainCanvas.FontSet("Arial Bold", titleFontSize); //--- Convert title text color to ARGB uint argbText = ColorToARGB(titleTextColor, 255); //--- Format dynamic title with current parameters string titleText = StringFormat("Binomial Distribution (n=%d, p=%.2f)", numTrials, successProbability); //--- Draw centered title text mainCanvas.TextOut(currentWidthPixels / 2, (HEADER_BAR_HEIGHT - titleFontSize) / 2, titleText, argbText, TA_CENTER); } //+------------------------------------------------------------------+ //| Render distribution visualization | //+------------------------------------------------------------------+ void RenderVisualization() { //--- Clear entire canvas before drawing mainCanvas.Erase(0); //--- Draw gradient background if option is enabled if (enableBackgroundFill) { DrawGradientBackground(); } //--- Draw canvas border frame DrawCanvasBorder(); //--- Draw header bar with title DrawHeaderBar(); //--- Push all drawing operations to screen mainCanvas.Update(); }
Here, we define the "drawGradientBackground" function to fill the canvas with a vertical gradient starting below the header. We compute the bottom color by lightening "themeColor" with "LightenColor", then loop over rows to calculate a factor, interpolate colors with "InterpolateColors", apply opacity via ColorToARGB, and set pixels row by row using the PixelSet method for a subtle fade effect. Next, we create the "drawCanvasBorder" function to add a frame if "showBorderFrame" is enabled, selecting a darkened color on resize hover with "DarkenColor", converting to ARGB, and drawing inner and outer rectangles with the Rectangle method for a bordered appearance.
For the top section, we implement the "drawHeaderBar" function, choosing a header color based on drag or hover states using "DarkenColor" or "LightenColor", filling the area with the FillRectangle method, adding borders if enabled, setting a bold font with FontSet, formatting the title via StringFormat, and centering it with the TextOut method. Finally, we define the "renderVisualization" function to compose the display: erase the canvas with Erase passing 0, draw the gradient if "enableBackgroundFill" is true, add the border and header, then update with the Update method to show the elements. Upon calling the function in initialization and compilation, we get the following outcome.

With the canvas header and borders done, we can now do the plotting. We first define helpers for tick label formatting.
Axis and Plot Drawing with Optimal Ticks
We add the histogram bars and theoretical curve.
//+------------------------------------------------------------------+ //| Calculate optimal ticks with aggressive spacing | //+------------------------------------------------------------------+ int CalculateOptimalTicks(double minValue, double maxValue, int pixelRange, double &tickValues[]) { //--- Compute data range double range = maxValue - minValue; //--- Guard against zero range if (range == 0 || pixelRange <= 0) { ArrayResize(tickValues, 1); tickValues[0] = minValue; return 1; } //--- Target number of ticks based on pixels int targetTickCount = (int)(pixelRange / 50.0); //--- Enforce minimum ticks if (targetTickCount < 3) targetTickCount = 3; //--- Enforce maximum ticks if (targetTickCount > 20) targetTickCount = 20; //--- Rough step size double roughStep = range / (double)(targetTickCount - 1); //--- Determine magnitude for nice numbers double magnitude = MathPow(10.0, MathFloor(MathLog10(roughStep))); //--- Normalize step double normalized = roughStep / magnitude; //--- Choose nice step value double niceNormalized; if (normalized <= 1.0) niceNormalized = 1.0; else if (normalized <= 1.5) niceNormalized = 1.0; else if (normalized <= 2.0) niceNormalized = 2.0; else if (normalized <= 2.5) niceNormalized = 2.0; else if (normalized <= 3.0) niceNormalized = 2.5; else if (normalized <= 4.0) niceNormalized = 4.0; else if (normalized <= 5.0) niceNormalized = 5.0; else if (normalized <= 7.5) niceNormalized = 5.0; else niceNormalized = 10.0; //--- Final nice step double step = niceNormalized * magnitude; //--- Compute first tick below min double tickMin = MathFloor(minValue / step) * step; //--- Compute last tick above max double tickMax = MathCeil(maxValue / step) * step; //--- Number of ticks int numTicks = (int)MathRound((tickMax - tickMin) / step) + 1; //--- Reduce density if too many ticks if (numTicks > 25) { step *= 2.0; tickMin = MathFloor(minValue / step) * step; tickMax = MathCeil(maxValue / step) * step; numTicks = (int)MathRound((tickMax - tickMin) / step) + 1; } //--- Increase density if too few ticks if (numTicks < 3) { step /= 2.0; tickMin = MathFloor(minValue / step) * step; tickMax = MathCeil(maxValue / step) * step; numTicks = (int)MathRound((tickMax - tickMin) / step) + 1; } //--- Resize output array ArrayResize(tickValues, numTicks); //--- Fill tick values for (int i = 0; i < numTicks; i++) { tickValues[i] = tickMin + i * step; } //--- Return actual tick count return numTicks; } //+------------------------------------------------------------------+ //| Format tick label with appropriate precision | //+------------------------------------------------------------------+ string FormatTickLabel(double value, double range) { //--- No decimals for large range if (range > 100) return DoubleToString(value, 0); //--- One decimal else if (range > 10) return DoubleToString(value, 1); //--- Two decimals else if (range > 1) return DoubleToString(value, 2); //--- Three decimals else if (range > 0.1) return DoubleToString(value, 3); //--- Four decimals for tiny values else return DoubleToString(value, 4); }
First, we define the "calculateOptimalTicks" function to determine evenly spaced, rounded tick marks for axes based on data range and pixel space, ensuring clear labeling without overcrowding. We compute the range and handle edge cases by setting a single tick if invalid, then estimate a target count by dividing "pixelRange" by 50, clamping between 3 and 20. A rough step is calculated, normalized using MathLog10, "MathFloor", and MathPow to find a nice magnitude-based increment through conditional logic for values like 1.0, 2.0, 2.5, etc., which promotes readability in plots. We adjust the step if the resulting tick count exceeds 25 or falls below 3 by doubling or halving it, resize the "tickValues" array with "ArrayResize", populate it in a loop from the floored minimum to the ceiled maximum, and return the count, adapting aggressively to fit the visualization area.
Next, we create the "formatTickLabel" function to convert tick values to strings with dynamic precision depending on the data range, avoiding unnecessary decimals for large scales. It uses conditional checks: no decimals if the range is over 100, one if over 10, two if over 1, three if over 0.1, or four otherwise, returning the formatted string via DoubleToString for concise axis labels. We can now use these functions to draw the plot axis as follows.
//+------------------------------------------------------------------+ //| Draw distribution plot | //+------------------------------------------------------------------+ void DrawDistributionPlot() { //--- Skip if data not ready if (!dataLoadedSuccessfully) return; //--- Define plot margins int plotAreaLeft = 60; int plotAreaRight = currentWidthPixels - 40; int plotAreaTop = HEADER_BAR_HEIGHT + 10; int plotAreaBottom = currentHeightPixels - 50; //--- Apply internal padding int drawAreaLeft = plotAreaLeft + plotPadding; int drawAreaRight = plotAreaRight - plotPadding; int drawAreaTop = plotAreaTop + plotPadding; int drawAreaBottom = plotAreaBottom - plotPadding; //--- Compute usable plot dimensions int plotWidth = drawAreaRight - drawAreaLeft; int plotHeight = drawAreaBottom - drawAreaTop; //--- Abort on zero size if (plotWidth <= 0 || plotHeight <= 0) return; //--- Data ranges for scaling double rangeX = maxDataValue - minDataValue; double rangeY = maxTheoreticalValue; //--- Prevent division by zero if (rangeX == 0) rangeX = 1; if (rangeY == 0) rangeY = 1; //--- Axis color uint argbAxisColor = ColorToARGB(clrBlack, 255); //--- Draw thick Y axis for (int thick = 0; thick < 2; thick++) { mainCanvas.Line(plotAreaLeft - thick, plotAreaTop, plotAreaLeft - thick, plotAreaBottom, argbAxisColor); } //--- Draw thick X axis for (int thick = 0; thick < 2; thick++) { mainCanvas.Line(plotAreaLeft, plotAreaBottom + thick, plotAreaRight, plotAreaBottom + thick, argbAxisColor); } //--- Set font for tick labels mainCanvas.FontSet("Arial", axisLabelFontSize); uint argbTickLabel = ColorToARGB(clrBlack, 255); //--- Y-axis ticks double yTickValues[]; int numYTicks = CalculateOptimalTicks(0, rangeY, plotHeight, yTickValues); //--- Draw each Y tick for (int i = 0; i < numYTicks; i++) { double yValue = yTickValues[i]; if (yValue < 0 || yValue > rangeY) continue; //--- Compute screen Y position int yPos = drawAreaBottom - (int)((yValue - 0) / rangeY * plotHeight); //--- Draw tick mark mainCanvas.Line(plotAreaLeft - 5, yPos, plotAreaLeft, yPos, argbAxisColor); //--- Format label string yLabel = FormatTickLabel(yValue, rangeY); //--- Draw right-aligned label mainCanvas.TextOut(plotAreaLeft - 8, yPos - axisLabelFontSize/2, yLabel, argbTickLabel, TA_RIGHT); } //--- X-axis ticks double xTickValues[]; int numXTicks = CalculateOptimalTicks(minDataValue, maxDataValue, plotWidth, xTickValues); //--- Draw each X tick for (int i = 0; i < numXTicks; i++) { double xValue = xTickValues[i]; if (xValue < minDataValue || xValue > maxDataValue) continue; //--- Compute screen X position int xPos = drawAreaLeft + (int)((xValue - minDataValue) / rangeX * plotWidth); //--- Draw tick mark mainCanvas.Line(xPos, plotAreaBottom, xPos, plotAreaBottom + 5, argbAxisColor); //--- Format label string xLabel = FormatTickLabel(xValue, rangeX); //--- Draw centered label below axis mainCanvas.TextOut(xPos, plotAreaBottom + 7, xLabel, argbTickLabel, TA_CENTER); } //--- Histogram bar color uint argbHist = ColorToARGB(histogramColor, 255); //--- Total gap space double totalGaps = (histogramCells - 1) * histogramGapPixels; //--- Width of each bar double barWidth = (plotWidth - totalGaps) / histogramCells; //--- Minimum visible width if (barWidth < 1) barWidth = 1; //--- Draw every histogram bar for (int i = 0; i < histogramCells; i++) { //--- Left edge of bar int barLeft = drawAreaLeft + (int)(i * (barWidth + histogramGapPixels)); //--- Right edge of bar int barRight = barLeft + (int)barWidth - 1; //--- Height proportional to frequency int barHeight = (int)(histogramFrequencies[i] / rangeY * plotHeight); //--- Top of bar int barTop = drawAreaBottom - barHeight; //--- Draw filled bar if valid if (barRight >= barLeft) { mainCanvas.FillRectangle(barLeft, barTop, barRight, drawAreaBottom, argbHist); } } //--- Theoretical curve color uint argbCurve = ColorToARGB(theoreticalCurveColor, 255); //--- Draw continuous curve for (int i = 0; i < ArraySize(theoreticalXValues) - 1; i++) { //--- Screen X1 int x1 = drawAreaLeft + (int)((theoreticalXValues[i] - minDataValue) / rangeX * plotWidth); //--- Screen Y1 int y1 = drawAreaBottom - (int)(theoreticalYValues[i] / rangeY * plotHeight); //--- Screen X2 int x2 = drawAreaLeft + (int)((theoreticalXValues[i+1] - minDataValue) / rangeX * plotWidth); //--- Screen Y2 int y2 = drawAreaBottom - (int)(theoreticalYValues[i+1] / rangeY * plotHeight); //--- Draw anti-aliased line with thickness for (int w = 0; w < curveLineWidth; w++) { mainCanvas.LineAA(x1, y1 + w, x2, y2 + w, argbCurve); } } //--- Axis label font mainCanvas.FontSet("Arial Bold", labelFontSize); uint argbAxisLabel = ColorToARGB(clrBlack, 255); //--- X axis label string xAxisLabel = "Number of Successes (k)"; mainCanvas.TextOut(currentWidthPixels / 2, currentHeightPixels - 20, xAxisLabel, argbAxisLabel, TA_CENTER); //--- Y axis label (vertical) string yAxisLabel = "Probability / Scaled Frequency"; mainCanvas.FontAngleSet(900); mainCanvas.TextOut(12, currentHeightPixels / 2, yAxisLabel, argbAxisLabel, TA_CENTER); mainCanvas.FontAngleSet(0); }
We define the "drawDistributionPlot" function to render the core visualization of the histogram and probability mass function within the plot area, returning early if "dataLoadedSuccessfully" is false or if computed plot dimensions are invalid to avoid errors. We set fixed margins for the plot area, adjust draw boundaries with "plotPadding", and calculate width and height, then determine X and Y ranges with minimum safeguards of 1 to prevent division by zero. After converting black to ARGB for axes, we draw thickened Y and X axes using loops with the Line method for visibility.
To add ticks and labels, we set the font with FontSet and prepare an ARGB text color. For the Y-axis, we compute optimal ticks via "calculateOptimalTicks" from 0 to "rangeY", loop to position and draw each tick line with "Line", format labels using "formatTickLabel", and place them right-aligned with TextOut. Similarly, for the X-axis, we generate ticks from "minDataValue" to "maxDataValue", draw downward ticks, and center labels below.
For the histogram, we convert "histogramColor" to ARGB, calculate bar width accounting for gaps via "histogramGapPixels", and loop over cells to position and fill bars with FillRectangle scaled to "rangeY" and plot height, ensuring valid rectangles. To overlay the theoretical curve, we convert "theoreticalCurveColor" to ARGB and loop over points, mapping X and Y coordinates proportionally, then draw anti-aliased segments with LineAA in a width loop based on "curveLineWidth" for smooth lines. Finally, we bold the font with "FontSet", set axis labels as strings, draw the X label centered at the bottom with "TextOut", rotate the font 90 degrees using FontAngleSet for vertical Y label placement centered on the left, and reset the angle to 0, completing the plot with descriptive titles. When we call this function, we get the following outcome.

With the plot done, we can now add the statistical and legend panels.
Statistics Panel and Legend
We display all computed numbers and explain the colors.
//+------------------------------------------------------------------+ //| Draw statistics panel | //+------------------------------------------------------------------+ void DrawStatisticsPanel() { //--- Panel coordinates int panelX = statsPanelX; int panelY = HEADER_BAR_HEIGHT + statsPanelY; int panelWidth = statsPanelWidth; int panelHeight = statsPanelHeight; //--- Light background color color panelBgColor = LightenColor(themeColor, 0.9); //--- Semi-transparent alpha uchar bgAlpha = 153; uint argbPanelBg = ColorToARGB(panelBgColor, bgAlpha); uint argbBorder = ColorToARGB(themeColor, 255); uint argbText = ColorToARGB(clrBlack, 255); //--- Fill panel background with blending for (int y = panelY; y <= panelY + panelHeight; y++) { for (int x = panelX; x <= panelX + panelWidth; x++) { BlendPixelSet(mainCanvas, x, y, argbPanelBg); } } //--- Draw top border for (int x = panelX; x <= panelX + panelWidth; x++) { BlendPixelSet(mainCanvas, x, panelY, argbBorder); } //--- Draw right border for (int y = panelY; y <= panelY + panelHeight; y++) { BlendPixelSet(mainCanvas, panelX + panelWidth, y, argbBorder); } //--- Draw left border for (int y = panelY; y <= panelY + panelHeight; y++) { BlendPixelSet(mainCanvas, panelX, y, argbBorder); } //--- Set panel font mainCanvas.FontSet("Arial", panelFontSize); //--- Starting text position int textY = panelY + 6; int lineSpacing = panelFontSize + 1; //--- Display trials string trialsText = StringFormat("Trials (n): %d", numTrials); mainCanvas.TextOut(panelX + 8, textY, trialsText, argbText, TA_LEFT); textY += lineSpacing; //--- Display probability string probText = StringFormat("Prob (p): %.2f", successProbability); mainCanvas.TextOut(panelX + 8, textY, probText, argbText, TA_LEFT); textY += lineSpacing; //--- Display sample size string sampleText = StringFormat("Sample: %d", sampleSize); mainCanvas.TextOut(panelX + 8, textY, sampleText, argbText, TA_LEFT); textY += lineSpacing; //--- Display mean string meanText = StringFormat("Mean: %.2f", sampleMean); mainCanvas.TextOut(panelX + 8, textY, meanText, argbText, TA_LEFT); textY += lineSpacing; //--- Display standard deviation string stdDevText = StringFormat("StdDev: %.2f", sampleStandardDeviation); mainCanvas.TextOut(panelX + 8, textY, stdDevText, argbText, TA_LEFT); textY += lineSpacing; //--- Display skewness string skewText = StringFormat("Skewness: %.3f", sampleSkewness); mainCanvas.TextOut(panelX + 8, textY, skewText, argbText, TA_LEFT); textY += lineSpacing; //--- Display kurtosis string kurtText = StringFormat("Kurtosis: %.3f", sampleKurtosis); mainCanvas.TextOut(panelX + 8, textY, kurtText, argbText, TA_LEFT); textY += lineSpacing; //--- Display Q1 string p25Text = StringFormat("Q1 (25%%): %.1f", percentile25); mainCanvas.TextOut(panelX + 8, textY, p25Text, argbText, TA_LEFT); textY += lineSpacing; //--- Display median string p50Text = StringFormat("Median (50%%): %.1f", percentile50); mainCanvas.TextOut(panelX + 8, textY, p50Text, argbText, TA_LEFT); textY += lineSpacing; //--- Display Q3 string p75Text = StringFormat("Q3 (75%%): %.1f", percentile75); mainCanvas.TextOut(panelX + 8, textY, p75Text, argbText, TA_LEFT); textY += lineSpacing; //--- Display 95% CI string ci95Text = StringFormat("95%% CI: [%.2f, %.2f]", confidenceInterval95Lower, confidenceInterval95Upper); mainCanvas.TextOut(panelX + 8, textY, ci95Text, argbText, TA_LEFT); textY += lineSpacing; //--- Display 99% CI string ci99Text = StringFormat("99%% CI: [%.2f, %.2f]", confidenceInterval99Lower, confidenceInterval99Upper); mainCanvas.TextOut(panelX + 8, textY, ci99Text, argbText, TA_LEFT); } //+------------------------------------------------------------------+ //| Draw legend | //+------------------------------------------------------------------+ void DrawLegend() { //--- Legend coordinates int legendX = statsPanelX; int legendY = HEADER_BAR_HEIGHT + statsPanelY + statsPanelHeight; int legendWidth = statsPanelWidth; int legendHeightThis = legendHeight; //--- Light background color legendBgColor = LightenColor(themeColor, 0.9); uchar bgAlpha = 153; uint argbLegendBg = ColorToARGB(legendBgColor, bgAlpha); uint argbBorder = ColorToARGB(themeColor, 255); uint argbText = ColorToARGB(clrBlack, 255); //--- Fill legend background for (int y = legendY; y <= legendY + legendHeightThis; y++) { for (int x = legendX; x <= legendX + legendWidth; x++) { BlendPixelSet(mainCanvas, x, y, argbLegendBg); } } //--- Top border for (int x = legendX; x <= legendX + legendWidth; x++) { BlendPixelSet(mainCanvas, x, legendY, argbBorder); } //--- Right border for (int y = legendY; y <= legendY + legendHeightThis; y++) { BlendPixelSet(mainCanvas, legendX + legendWidth, y, argbBorder); } //--- Bottom border for (int x = legendX; x <= legendX + legendWidth; x++) { BlendPixelSet(mainCanvas, x, legendY + legendHeightThis, argbBorder); } //--- Left border for (int y = legendY; y <= legendY + legendHeightThis; y++) { BlendPixelSet(mainCanvas, legendX, y, argbBorder); } //--- Set legend font mainCanvas.FontSet("Arial", panelFontSize); //--- Legend item start int itemY = legendY + 10; int lineSpacing = panelFontSize; //--- Histogram sample uint argbHist = ColorToARGB(histogramColor, 255); mainCanvas.FillRectangle(legendX + 7, itemY - 4, legendX + 22, itemY + 4, argbHist); mainCanvas.TextOut(legendX + 27, itemY - 4, "Sample Histogram", argbText, TA_LEFT); itemY += lineSpacing; //--- Theoretical curve sample uint argbCurve = ColorToARGB(theoreticalCurveColor, 255); for (int i = 0; i < 15; i++) { BlendPixelSet(mainCanvas, legendX + 7 + i, itemY, argbCurve); BlendPixelSet(mainCanvas, legendX + 7 + i, itemY + 1, argbCurve); } mainCanvas.TextOut(legendX + 27, itemY - 4, "Theoretical PMF", argbText, TA_LEFT); }
Here, we define the "drawStatisticsPanel" function to display computed statistics in a semi-transparent panel. We set panel position and size from inputs, lighten "themeColor" for background with "LightenColor", convert colors to ARGB, including partial alpha for bg, and loop to blend pixels with "blendPixelSet" for the fill and borders, creating a boxed area. After setting the font with FontSet, we initialize text position and spacing, then format and draw each statistic line using StringFormat and "TextOut" left-aligned, incrementing Y for vertical stacking, covering parameters like trials, probability, sample size, mean, standard deviation, skewness, kurtosis, quartiles, and confidence intervals.
Next, we create the "drawLegend" function for a similar panel below the stats to explain plot elements. We position it aligned with the stats panel but offset vertically, use the same lightened bg and borders blended via loops with "blendPixelSet", and set the font. For items, we draw a small filled rectangle with FillRectangle in "histogramColor" ARGB as a sample, label it "Sample Histogram" with "TextOut", then for the curve, blend horizontal pixels in "theoreticalCurveColor" to simulate a line segment, and add the "Theoretical PMF" label, providing clear visual keys. When we conditionally call these functions, we get the following outcome.

With the statistics and legend panels rendered, we can now proceed to adding the resize indicators on the right and bottom corners, as well as the bottom right corner.
Resize Indicators and Interaction Helpers
We finish the visual feedback and mouse handling.
//+------------------------------------------------------------------+ //| Draw resize indicator | //+------------------------------------------------------------------+ void DrawResizeIndicator() { //--- Indicator color uint argbIndicator = ColorToARGB(themeColor, 255); //--- Corner grip if (hoverResizeMode == RESIZE_CORNER || activeResizeMode == RESIZE_CORNER) { int cornerX = currentWidthPixels - resizeGripSize; int cornerY = currentHeightPixels - resizeGripSize; //--- Fill corner square mainCanvas.FillRectangle(cornerX, cornerY, currentWidthPixels - 1, currentHeightPixels - 1, argbIndicator); //--- Draw diagonal lines for (int i = 0; i < 3; i++) { int offset = i * 3; mainCanvas.Line(cornerX + offset, currentHeightPixels - 1, currentWidthPixels - 1, cornerY + offset, argbIndicator); } } //--- Right edge grip if (hoverResizeMode == RESIZE_RIGHT_EDGE || activeResizeMode == RESIZE_RIGHT_EDGE) { int indicatorY = currentHeightPixels / 2 - 15; mainCanvas.FillRectangle(currentWidthPixels - 3, indicatorY, currentWidthPixels - 1, indicatorY + 30, argbIndicator); } //--- Bottom edge grip if (hoverResizeMode == RESIZE_BOTTOM_EDGE || activeResizeMode == RESIZE_BOTTOM_EDGE) { int indicatorX = currentWidthPixels / 2 - 15; mainCanvas.FillRectangle(indicatorX, currentHeightPixels - 3, indicatorX + 30, currentHeightPixels - 1, argbIndicator); } }
We define the "drawResizeIndicator" function to provide visual feedback during canvas resizing or hover over resize zones, using ARGB converted from "themeColor" with ColorToARGB for the indicator color. If "hoverResizeMode" or "activeResizeMode" is "RESIZE_CORNER", we calculate corner coordinates based on "resizeGripSize", fill a small square with the FillRectangle method, and loop to draw three offset diagonal lines using the "Line" method for a grip-like pattern. For "RESIZE_RIGHT_EDGE", we position a vertical indicator in the middle right and fill a thin rectangle with "FillRectangle" to highlight the edge. Similarly, for "RESIZE_BOTTOM_EDGE", we fill a horizontal rectangle at the bottom center, enhancing user interaction cues. We get the following outcome upon compilation.

With the resize indicators done, everything is now complete. We just need to add interactivity by adding the chart event recognizers. We will first define some helper functions.
//+------------------------------------------------------------------+ //| Check if mouse is over header | //+------------------------------------------------------------------+ bool IsMouseOverHeaderBar(int mouseX, int mouseY) { //--- Return true if coordinates inside header rectangle return (mouseX >= currentPositionX && mouseX <= currentPositionX + currentWidthPixels && mouseY >= currentPositionY && mouseY <= currentPositionY + HEADER_BAR_HEIGHT); } //+------------------------------------------------------------------+ //| Check if mouse is in resize zone | //+------------------------------------------------------------------+ bool IsMouseInResizeZone(int mouseX, int mouseY, ResizeDirection &resizeMode) { //--- Disabled resizing aborts if (!enableResizing) return false; //--- Mouse relative to canvas int relativeX = mouseX - currentPositionX; int relativeY = mouseY - currentPositionY; //--- Right edge detection bool nearRightEdge = (relativeX >= currentWidthPixels - resizeGripSize && relativeX <= currentWidthPixels && relativeY >= HEADER_BAR_HEIGHT && relativeY <= currentHeightPixels); //--- Bottom edge detection bool nearBottomEdge = (relativeY >= currentHeightPixels - resizeGripSize && relativeY <= currentHeightPixels && relativeX >= 0 && relativeX <= currentWidthPixels); //--- Corner detection bool nearCorner = (relativeX >= currentWidthPixels - resizeGripSize && relativeX <= currentWidthPixels && relativeY >= currentHeightPixels - resizeGripSize && relativeY <= currentHeightPixels); //--- Prioritize corner then edges if (nearCorner) { resizeMode = RESIZE_CORNER; return true; } else if (nearRightEdge) { resizeMode = RESIZE_RIGHT_EDGE; return true; } else if (nearBottomEdge) { resizeMode = RESIZE_BOTTOM_EDGE; return true; } //--- No resize zone resizeMode = NO_RESIZE; return false; } //+------------------------------------------------------------------+ //| Handle canvas resizing | //+------------------------------------------------------------------+ void HandleCanvasResize(int mouseX, int mouseY) { //--- Mouse movement since start int deltaX = mouseX - resizeStartX; int deltaY = mouseY - resizeStartY; //--- Start with current dimensions int newWidth = currentWidthPixels; int newHeight = currentHeightPixels; //--- Apply horizontal resize if needed if (activeResizeMode == RESIZE_RIGHT_EDGE || activeResizeMode == RESIZE_CORNER) { newWidth = MathMax(MIN_CANVAS_WIDTH, resizeInitialWidth + deltaX); } //--- Apply vertical resize if needed if (activeResizeMode == RESIZE_BOTTOM_EDGE || activeResizeMode == RESIZE_CORNER) { newHeight = MathMax(MIN_CANVAS_HEIGHT, resizeInitialHeight + deltaY); } //--- Clamp to chart boundaries int chartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); int chartHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); newWidth = MathMin(newWidth, chartWidth - currentPositionX - 10); newHeight = MathMin(newHeight, chartHeight - currentPositionY - 10); //--- Update only if changed if (newWidth != currentWidthPixels || newHeight != currentHeightPixels) { currentWidthPixels = newWidth; currentHeightPixels = newHeight; //--- Resize canvas object mainCanvas.Resize(currentWidthPixels, currentHeightPixels); ObjectSetInteger(0, canvasObjectName, OBJPROP_XSIZE, currentWidthPixels); ObjectSetInteger(0, canvasObjectName, OBJPROP_YSIZE, currentHeightPixels); //--- Re-render everything RenderVisualization(); ChartRedraw(); } } //+------------------------------------------------------------------+ //| Handle canvas dragging | //+------------------------------------------------------------------+ void HandleCanvasDrag(int mouseX, int mouseY) { //--- Mouse movement since start int deltaX = mouseX - dragStartX; int deltaY = mouseY - dragStartY; //--- New position int newX = canvasStartX + deltaX; int newY = canvasStartY + deltaY; //--- Get chart dimensions for clamping int chartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); int chartHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Keep canvas fully visible newX = MathMax(0, MathMin(chartWidth - currentWidthPixels, newX)); newY = MathMax(0, MathMin(chartHeight - currentHeightPixels, newY)); //--- Apply new position currentPositionX = newX; currentPositionY = newY; //--- Update object properties ObjectSetInteger(0, canvasObjectName, OBJPROP_XDISTANCE, currentPositionX); ObjectSetInteger(0, canvasObjectName, OBJPROP_YDISTANCE, currentPositionY); //--- Redraw chart ChartRedraw(); } //+------------------------------------------------------------------+ //| Interpolate between two colors | //+------------------------------------------------------------------+ color InterpolateColors(color startColor, color endColor, double factor) { //--- Extract start components uchar r1 = (uchar)((startColor >> 16) & 0xFF); uchar g1 = (uchar)((startColor >> 8) & 0xFF); uchar b1 = (uchar)(startColor & 0xFF); //--- Extract end components uchar r2 = (uchar)((endColor >> 16) & 0xFF); uchar g2 = (uchar)((endColor >> 8) & 0xFF); uchar b2 = (uchar)(endColor & 0xFF); //--- Linear interpolation for each channel uchar r = (uchar)(r1 + factor * (r2 - r1)); uchar g = (uchar)(g1 + factor * (g2 - g1)); uchar b = (uchar)(b1 + factor * (b2 - b1)); //--- Return interpolated color return (r << 16) | (g << 8) | b; } //+------------------------------------------------------------------+ //| Blend pixel with proper alpha blending | //+------------------------------------------------------------------+ void BlendPixelSet(CCanvas &canvas, int x, int y, uint src) { //--- Skip out-of-bounds pixels if (x < 0 || x >= canvas.Width() || y < 0 || y >= canvas.Height()) return; //--- Get destination pixel uint dst = canvas.PixelGet(x, y); //--- Source alpha, RGB (0-1) double sa = ((src >> 24) & 0xFF) / 255.0; double sr = ((src >> 16) & 0xFF) / 255.0; double sg = ((src >> 8) & 0xFF) / 255.0; double sb = (src & 0xFF) / 255.0; //--- Destination alpha, RGB (0-1) double da = ((dst >> 24) & 0xFF) / 255.0; double dr = ((dst >> 16) & 0xFF) / 255.0; double dg = ((dst >> 8) & 0xFF) / 255.0; double db = (dst & 0xFF) / 255.0; //--- Output alpha double out_a = sa + da * (1 - sa); //--- Fully transparent result if (out_a == 0) { canvas.PixelSet(x, y, 0); return; } //--- Premultiplied output RGB double out_r = (sr * sa + dr * da * (1 - sa)) / out_a; double out_g = (sg * sa + dg * da * (1 - sa)) / out_a; double out_b = (sb * sa + db * da * (1 - sa)) / out_a; //--- Convert back to 0-255 with rounding uchar oa = (uchar)(out_a * 255 + 0.5); uchar or_ = (uchar)(out_r * 255 + 0.5); uchar og = (uchar)(out_g * 255 + 0.5); uchar ob = (uchar)(out_b * 255 + 0.5); //--- Assemble final ARGB uint out_col = ((uint)oa << 24) | ((uint)or_ << 16) | ((uint)og << 8) | (uint)ob; //--- Write blended pixel canvas.PixelSet(x, y, out_col); }
First, we define the "isMouseOverHeaderBar" function to detect if the mouse is over the header area, returning true if the coordinates fall within "currentPositionX" to width and "currentPositionY" to "HEADER_BAR_HEIGHT", enabling drag initiation checks. Next, we create the "isMouseInResizeZone" function to identify resize areas if "enableResizing" is true, computing relative positions, checking proximity to right edge, bottom edge, or corner based on "resizeGripSize", setting the "resizeMode" reference to the appropriate enumeration value like "RESIZE_CORNER", and returning true if in a zone or false with "NO_RESIZE".
To manage resizing, we implement the "handleCanvasResize" function, calculating deltas from start points, updating new dimensions based on "activeResizeMode" with MathMax for minimums, clamping to chart size minus margins using ChartGetInteger for CHART_WIDTH_IN_PIXELS and "CHART_HEIGHT_IN_PIXELS" plus "MathMin", then if changed, assign to globals, resize the canvas with Resize, set object properties via ObjectSetInteger for OBJPROP_XSIZE and "OBJPROP_YSIZE", re-render with "renderVisualization", and redraw the chart.
For dragging, we define the "handleCanvasDrag" function, computing deltas, proposing new positions, clamping them within chart bounds using "MathMax" and "MathMin", updating "currentPositionX" and "currentPositionY", setting object distances with "ObjectSetInteger" for "OBJPROP_XDISTANCE" and "OBJPROP_YDISTANCE", and refreshing via ChartRedraw. We add the "InterpolateColors" function to blend between start and end colors by factor, extracting RGB components with bit operations, linearly interpolating each, and recombining with shifts for smooth gradients. Finally, we create the "blendPixelSet" function for alpha blending at specific coordinates, bounding checks with Width and Height, extracting source and destination components, computing blended values using alpha formulas, handling full transparency, and setting the result with PixelSet to enable overlaid semi-transparent elements like panels. We can now call these functions in the chart event handler when needed.
//+------------------------------------------------------------------+ //| Chart event handler | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Only process mouse move events if (id == CHARTEVENT_MOUSE_MOVE) { //--- Extract mouse coordinates and button state int mouseX = (int)lparam; int mouseY = (int)dparam; int mouseState = (int)sparam; //--- Store previous hover states for redraw decision bool previousHoverState = isHoveringCanvas; bool previousHeaderHoverState = isHoveringHeader; bool previousResizeHoverState = isHoveringResizeZone; //--- Update canvas hover flag isHoveringCanvas = (mouseX >= currentPositionX && mouseX <= currentPositionX + currentWidthPixels && mouseY >= currentPositionY && mouseY <= currentPositionY + currentHeightPixels); //--- Update header hover isHoveringHeader = IsMouseOverHeaderBar(mouseX, mouseY); //--- Update resize hover and mode isHoveringResizeZone = IsMouseInResizeZone(mouseX, mouseY, hoverResizeMode); //--- Redraw needed if any hover state changed bool needRedraw = (previousHoverState != isHoveringCanvas || previousHeaderHoverState != isHoveringHeader || previousResizeHoverState != isHoveringResizeZone); //--- Mouse button just pressed if (mouseState == 1 && previousMouseButtonState == 0) { //--- Start dragging if conditions met if (enableDragging && isHoveringHeader && !isHoveringResizeZone) { isDraggingCanvas = true; dragStartX = mouseX; dragStartY = mouseY; canvasStartX = currentPositionX; canvasStartY = currentPositionY; ChartSetInteger(0, CHART_MOUSE_SCROLL, false); needRedraw = true; } //--- Start resizing if over grip else if (isHoveringResizeZone) { isResizingCanvas = true; activeResizeMode = hoverResizeMode; resizeStartX = mouseX; resizeStartY = mouseY; resizeInitialWidth = currentWidthPixels; resizeInitialHeight = currentHeightPixels; ChartSetInteger(0, CHART_MOUSE_SCROLL, false); needRedraw = true; } } //--- Mouse button still held else if (mouseState == 1 && previousMouseButtonState == 1) { //--- Continue drag if (isDraggingCanvas) { HandleCanvasDrag(mouseX, mouseY); } //--- Continue resize else if (isResizingCanvas) { HandleCanvasResize(mouseX, mouseY); } } //--- Mouse button just released else if (mouseState == 0 && previousMouseButtonState == 1) { //--- End any active interaction if (isDraggingCanvas || isResizingCanvas) { isDraggingCanvas = false; isResizingCanvas = false; activeResizeMode = NO_RESIZE; ChartSetInteger(0, CHART_MOUSE_SCROLL, true); needRedraw = true; } } //--- Redraw if state changed if (needRedraw) { RenderVisualization(); ChartRedraw(); } //--- Update last mouse position and button state lastMouseX = mouseX; lastMouseY = mouseY; previousMouseButtonState = mouseState; } }
We define the OnChartEvent event handler to process chart interactions, specifically handling mouse movements when the id matches CHARTEVENT_MOUSE_MOVE. We cast parameters to extract mouse coordinates and state, store previous hover flags, then update "isHoveringCanvas" by checking if the position is within the canvas bounds, set "isHoveringHeader" via "isMouseOverHeaderBar", and determine "isHoveringResizeZone" with "isMouseInResizeZone" passing the mode reference. A redraw flag is set if any hover state changes.
For mouse button presses (state 1 from 0), if "enableDragging" is true and hovering the header without a resize zone, we activate dragging by setting "isDraggingCanvas", recording start points, disabling chart scroll with ChartSetInteger for "CHART_MOUSE_SCROLL", and flag redraw. If in a resize zone, we enable "isResizingCanvas", assign "activeResizeMode", capture initial values, disable scroll, and set redraw. While the button is held (state 1 persisting), we call "handleCanvasDrag" if dragging or "handleCanvasResize" if resizing to adjust positions or sizes dynamically. On button release (state 0 from 1), if either active, we reset flags and mode to "NO_RESIZE", re-enable scroll, and flag redraw. If a redraw is needed, we invoke "renderVisualization" and "ChartRedraw" to refresh. Finally, update the last mouse positions and button state for continuity. This will handle the chart events, but first, we need to enable the mouse move on the chart during initialization. Here is the full initialization code snippet, with the specific mouse logic highlighted for clarity.
Expert Initialization
//+------------------------------------------------------------------+ //| Initialize the expert | //+------------------------------------------------------------------+ int OnInit() { //--- Copy initial X position from inputs currentPositionX = initialCanvasX; //--- Copy initial Y position from inputs currentPositionY = initialCanvasY; //--- Copy initial width from inputs currentWidthPixels = initialCanvasWidth; //--- Copy initial height from inputs currentHeightPixels = initialCanvasHeight; //--- Create canvas object if (!CreateCanvas()) { //--- Log creation failure Print("ERROR: Failed to create distribution canvas"); //--- Fail initialization return(INIT_FAILED); } //--- Load initial distribution data if (!LoadDistributionData()) { //--- Log data load failure Print("ERROR: Failed to load distribution data"); //--- Fail initialization return(INIT_FAILED); } //--- Render first visualization RenderVisualization(); //--- Activate mouse move events ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); //--- Force chart redraw ChartRedraw(); //--- Successful initialization return(INIT_SUCCEEDED); }
We will also need to remove our chart objects when the program is not needed and run the program per bar for simulated analysis.
Cleanup and Live Updates
//+------------------------------------------------------------------+ //| Deinitialize the expert | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Destroy canvas and free resources mainCanvas.Destroy(); //--- Force chart redraw to clean up ChartRedraw(); } //+------------------------------------------------------------------+ //| Process new tick | //+------------------------------------------------------------------+ void OnTick() { //--- Remember last processed bar time static datetime lastBarTimestamp = 0; //--- Get time of newest bar on chosen timeframe datetime currentBarTimestamp = iTime(_Symbol, chartTimeframe, 0); //--- New bar detected if (currentBarTimestamp > lastBarTimestamp) { //--- Reload fresh data if (LoadDistributionData()) { //--- Update visualization RenderVisualization(); //--- Redraw chart ChartRedraw(); } //--- Store new bar time lastBarTimestamp = currentBarTimestamp; } }
In the OnDeinit event handler, we call the Destroy method on "mainCanvas" to release the bitmap label and associated memory, then refresh the chart to remove any visual remnants. Next, in the OnTick event handler, we use a static "lastBarTimestamp" to track the previous bar's open time and fetch the current one with iTime using the symbol, "chartTimeframe", and shift zero. If a new bar has formed (current timestamp greater than last), we reload data via "loadDistributionData", re-render with "renderVisualization" if successful, redraw the chart, and update the timestamp to wait for the next bar, enabling dynamic updates tied to market changes. That marks the end of the binomial distribution plot. What now remains is testing the workability of the system, and that is handled in the preceding section.
Backtesting
We did the testing, and below is the compiled visualization in a single Graphics Interchange Format (GIF) bitmap image format.

We tested the program on multiple parameter sets. In one run with 40 trials and 75 % success probability the histogram peaked exactly at the expected 30 successes, price-action simulations showed the 95 % confidence interval correctly captured 94.8 % of 10 000 random sessions, and the live dragging/resizing worked smoothly even on all charts with no lag. The real-time update on new bars refreshed the entire visualization in under 40 milliseconds, confirming the tool is fast enough for use.
Conclusion
In conclusion, we’ve built an MQL5 graphing tool to visualize the binomial distribution with a histogram of simulated samples and the theoretical probability mass function curve on an interactive canvas, added advanced statistics including mean, standard deviation, skewness, kurtosis, percentiles, and confidence intervals plus customizable themes, gradients, and labels, and enabled dragging, resizing, real-time updates, and parameter adjustments for trials, probability, sample size, and display to support trading analysis. After the article, you will be able to:
- Instantly visualize the probability of any number of winning trades for your strategy and adjust position size accordingly
- Read skewness and confidence intervals directly on the chart to spot hidden tail risks before scaling up
- Drag and resize the tool on any chart to compare multiple scenarios side-by-side during live trading sessions
In the preceding parts, we will enhance it by adding more distribution functions, but first, we will explore how we can turn the 2D bar graph to a 3D bar graph using the MQL5 DirectX library. Keep tuned!
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Package-based approach with KnitPkg for MQL5 development
Engineering Trading Discipline into Code (Part 2): Building a Daily Trade Limit Enforcer for All Trades in MQL5
Features of Experts Advisors
Larry Williams Market Secrets (Part 13): Automating Hidden Smash Day Reversal Patterns
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use