English Русский Deutsch
preview
MQL5取引ツール(第22回):二項分布のヒストグラムと確率質量関数(PMF)の描画

MQL5取引ツール(第22回):二項分布のヒストグラムと確率質量関数(PMF)の描画

MetaTrader 5トレーディング |
13 0
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

勝敗が連続して発生するトレードシステムを運用していても、「過去の勝率が75%の場合、30回の取引中20回勝利する確率はどの程度なのか」「5連敗が発生する可能性はどの程度あるのか」といった単純でありながら重要な質問に答えられない場合があります。二項分布を利用してモデル化する手段がなければ、これらの問題は推測に頼ることになります。その結果、現実的なリスク評価、適切なポジションサイズの設定、または戦略のパフォーマンスが統計的に有意であるかどうかの検証が困難になります。このような確率的な洞察の不足は、過剰なレバレッジ、感情的な判断、非現実的な利益期待につながる可能性があります。本記事は、MetaQuotes Language 5 (MQL5)の開発者およびアルゴリズムトレーダーを対象としており、トレード戦略の確率的な挙動を定量的に評価することを目的としています。

前回の記事(第21回)では、MQL5の線形回帰可視化ツールを拡張し、ネオン発光、アニメーション、ホログラム効果を備えたサイバーパンクテーマモードを追加することで、没入感のある可視化機能を実現しました。今回の第22回では、インタラクティブなキャンバス上で、シミュレーションサンプルのヒストグラムと理論的な確率質量関数曲線を表示する、二項分布可視化用のMQL5可視化ツールを構築します。さらに、平均、標準偏差、歪度、尖度、パーセンタイル、信頼区間などの高度な統計指標を追加し、カスタマイズ可能なテーマ、グラデーション、ラベル表示にも対応します。加えて、ドラッグ操作、サイズ変更、リアルタイム更新、試行回数、成功確率、サンプル数、表示設定などのパラメータ調整機能を実装し、トレード分析への活用を可能にします。本記事では以下のトピックを扱います。

  1. 二項分布フレームワークの理解
  2. MQL5での実装
  3. バックテスト
  4. 結論

最後まで読むことで、二項分布を可視化できる実用的なMQL5可視化ツールを手に入れ、トレード戦略分析やさらなるカスタマイズに活用できるようになります。それでは始めましょう。


二項分布フレームワークの理解

二項分布は、同じ成功確率を持つ独立した試行を一定回数おこなった場合に、成功する回数をモデル化する確率分布です。トレードにおいて「試行」とは、2つの結果に分類できるあらゆる事象を指します。たとえば、「取引が利益で終了する(成功)または損失で終了する(失敗)」「シグナルが有効である(成功)または無効である(失敗)」「市場条件を満たす(成功)または満たさない(失敗)」のようなケースです。もし、戦略の過去の勝率がpである場合、二項分布を利用することで、n回の取引のうちk回勝利する確率が、単なる偶然によってどの程度発生する可能性があるかを評価できます。市場分析では、以下のように活用できます。

  • 利益目標達成確率の推定:たとえば、システムの勝率が60%であるとします。1か月に20回の取引を予定している場合、12回以上勝利する確率を計算できます。これは、期待するパフォーマンスが現実的であるかを確認するための有効な基準となります。
  • 現実的なドローダウン予測:二項分布は、発生する可能性が高い最大連敗数を予測するのに役立ちます。たとえば、5連敗が発生する確率が非常に低い場合、そのような連敗は戦略の破綻を示している可能性があります。
  • 戦略比較:2つのシステムが同程度の勝率を持っている場合でも、分布の広がりが小さい(分散が低い)システムの方が、より安定した戦略である可能性があります。この違いは、ヒストグラムや標準偏差から確認できます。
  • サンプルサイズの検証:信頼区間を利用することで、推定された勝率の精度を評価できます。信頼区間が広い場合、真のシステム性能を判断するためには、より多くの取引データが必要であることを示します。

本ツールでは、指定された試行回数と成功確率を使用して二項分布からサンプルをシミュレーションし、実測された頻度のヒストグラムを作成します。その上に理論的な確率質量関数(PMF)曲線を重ね合わせ、比較できるようにします。さらに、平均、標準偏差、歪度、尖度、パーセンタイル、信頼区間などの主要な統計指標を表示します。これらの情報は、サイズ変更やドラッグ操作が可能なキャンバス上に描画され、テーマ、背景グラデーション、軸ラベル、凡例もカスタマイズできます。また、チャートの時間足変更に応じたリアルタイム更新にも対応し、インタラクティブな確率分析環境を提供します。つまり、この構成により、分布特性と、それがトレードシナリオに与える影響を探索するための分析ツールが実現されます。以下が、本ツールで構築する二項分布プロットのフレームワークです。

二項分布プロットフレームワーク

機能ごとのトレード上の意味

実装に入る前に、このツールの各構成要素がトレーダーにとってどのような意味を持つのかを明確にします。

  • ヒストグラム(実測頻度):シミュレーションされたサンプル内で、それぞれの成功回数が何回発生したかを示します。特定の「k」においてバーが高い場合、その結果が頻繁に発生することを意味し、これは戦略における「典型的な」動作パターンです。ヒストグラムが広く分散している場合、システムの結果変動が大きいことを示します。
  • 理論PMF曲線:滑らかな曲線は、理論上の二項確率を表します。この曲線をヒストグラム上に重ねることで、シミュレーション結果が期待される分布と一致しているかを直感的に確認できます。大きな差異がある場合、サンプル数が不足している、または独立試行という前提条件が成立していない可能性があります。
  • 平均:期待される成功回数を示します。勝率「p」と取引回数「n」を使用する場合、平均は「n × p」で求められます。実際の平均値が大きく異なる場合、想定していた勝率が正しくない可能性があります。
  • 標準偏差:結果のばらつきを測定します。標準偏差が小さいほど、システムの結果が安定していることを意味し、リスク管理において重要な指標となります。
  • 歪度:分布が左右対称であるか、どちらかに偏っているかを示します。正の歪度(右側に長い裾)は、時折非常に良い連勝が発生することを意味します。負の歪度は、時折発生する大きな損失や危険な連敗を示します。
  • 尖度:極端な結果が発生しやすいかどうかを示します。高い尖度(ファットテール)は、通常の正規分布が予測するよりも、大きな勝ち・負けの連続が発生しやすいことを意味します。これは、ストップロス設定を考える際の重要な要素となります。
  • 信頼区間(95%、99%):システムの真の勝率が収まる可能性の高い範囲を示します。狭い信頼区間は、観測されたパフォーマンスの信頼性が高いことを示します。広い信頼区間は、より多くのデータ収集が必要であることを示します。

これらの統計情報を利用することで、単純な勝率だけではなく、トレード結果の分布形状を理解できるようになります。これは、リスク評価において非常に重要な情報です。それでは、実装を始めましょう。


MQL5での実装

MQL5でプログラムを作成するには、まずMetaEditorを開き、ナビゲーターで[Experts]フォルダを探します。[新規]タブをクリックして指示に従い、ファイルを作成します。ファイルが作成されたら、コーディング環境で、まずプログラム全体で使用する入力パラメータグローバル変数をいくつか宣言する必要があります。

Canvasとコアライブラリの設定

プログラム構築を開始するために、まず必要なライブラリをインクルードし、補助構造体を定義します。

//+------------------------------------------------------------------+
//|           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

#include <Canvas\Canvas.mqh>はCanvasベースのグラフィックス描画を提供し、#include <Math\Stat\Binomial.mqh>は二項分布関数の処理を担当します。また、#include <Math\Stat\Math.mqh>は一般的な数学および統計処理用のユーティリティを提供し、可視化処理と計算処理を実行するための基盤を構築します。次に、Canvasのサイズ変更オプションを管理するために、ResizeDirection列挙型を定義します。この列挙型には、デフォルト状態であるNO_RESIZE、垂直方向の調整を行うRESIZE_BOTTOM_EDGE、水平方向の変更を行うRESIZE_RIGHT_EDGE、斜め方向のサイズ変更を行うRESIZE_CORNERが含まれます。これにより、後の段階でユーザーによるインタラクティブな操作をサポートできるようになります。

続いて、プログラム設定画面でのユーザーインターフェースを整理するため、string input groupを使用して入力パラメータをグループ化して定義します。これらには、試行回数、成功確率、サンプルサイズ、ヒストグラムセル数、バー間隔、チャート時間足などの分布パラメータ、初期表示位置、サイズ、プロット余白などのCanvas表示設定、境界線表示を含むメインテーマカラー設定、ヒストグラムと曲線のカラーおよび曲線幅設定、上部カラーと透明度を含む背景塗りつぶし設定、テキストとラベルのサイズ・カラー、統計情報表示切替、統計パネルと凡例パネルの位置・サイズ・フォント設定、さらにドラッグ操作、サイズ変更、グリップサイズなどのインタラクション設定が含まれます。これにより、ツールの動作や外観を柔軟にカスタマイズできます。

最後に、プログラムの状態を管理するためのグローバル変数を初期化します。描画処理にはCCanvasクラスのインスタンスであるmainCanvasを使用し、Canvasオブジェクト名を保持する文字列変数、入力値を初期値とする現在位置およびサイズ管理変数を定義します。さらに、ドラッグ、サイズ変更、ホバー状態を管理するブール型フラグ、ドラッグおよびサイズ変更操作用の座標追跡変数、列挙型を使用した現在のサイズ変更モードとホバー中のサイズ変更モード、マウス位置追跡変数、最小Canvasサイズとヘッダー高さの定数、サンプルデータ配列、ヒストグラム区間と頻度配列、理論値配列、最小値・最大値追跡変数、データ読み込み状態フラグ、平均、標準偏差、歪度、尖度、パーセンタイル、信頼区間境界などの統計指標を準備します。これで、データ処理・統計計算・対話的な描画に必要な基盤が整います。これらのライブラリを使用することで、理論的な数式からリアルタイムのビジュアル表示へ迅速に移行できます。次に、コードをモジュール化し、管理しやすくするためのヘルパー関数を定義します。まずは、テーマカラーを管理するヘルパー関数から開始します。

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

テーマ内のカラー変化を処理するために、基準カラーと係数を受け取り、より明るい色合いを生成するLightenColor関数を作成します。この関数では、ビットシフト演算とマスク処理を使用して赤、緑、青の各成分を抽出します。その後、各成分に対して、係数に応じた255までの残りの強度の一部を加算し、MathMinを使用して最大値を255に制限します。最後に、ビットシフトによって各成分を再結合し、カラー値として返します。同様に、より暗い色を生成するためにDarkenColor関数を定義します。この関数では、同じ方法でRGB成分を抽出し、それぞれの値に1から係数を引いた値を乗算することで強度を低下させ、再結合したカラー値を返します。これらのヘルパー関数により、メインテーマカラーを基準として、ヘッダー、境界線、背景などの要素に動的な色調調整を適用できるようになります。次に、数学計算用のヘルパー関数を定義します。

統計計算関数

単なる見た目のグラフだけでは十分ではないため、完全な統計計算用ヘルパー関数群を実装します。

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

まず、データセットの平均値を計算するcalculateMean関数を定義します。この関数では、ArraySizeArraySizeを使用して配列サイズを取得し、空の配列であるかを確認します。空の場合は0を返します。その後、合計値を格納する変数を初期化し、各要素をループ処理して値を加算します。最後に、合計値をデータ数で割ることで平均値を計算します。次に、指定された平均値を基準としてデータの分散度を測定するcalculateStandardDeviation関数を実装します。配列サイズを取得した後、要素数が1以下の場合は0を返します。ループ処理によって平均値との差の二乗和を計算し、その合計を要素数-1で割った値に対してMathSqrtを使用して平方根を求め、標本標準偏差を算出します。

データ分布の非対称性を評価するために、calculateSkewness関数を作成します。この関数では、まず3つ以上の要素が存在すること、および標準偏差が0ではないことを確認します。条件を満たさない場合は0を返します。ループ内では、各データと平均値との差を標準偏差で割って正規化し、その値を3乗して加算します。最終的な歪度は、データ数をdouble型に変換したnを使用し、n / ((n-1)*(n-2))に正規化された差の3乗合計を乗算する式で計算されます。これにより、分布が左側(負)または右側(正)のどちらに偏っているかを評価できます。この指標は、不均衡な成功確率など、二項分布における結果の偏りを理解する上で重要です。

同様に、正規分布と比較した場合の分布の裾の広がりを評価するために、calculateKurtosis関数を定義します。この関数では、少なくとも4つの要素が存在し、標準偏差が0ではないことが必要です。それ以外の場合は0を返します。ループ処理では、正規化された差を計算し、それを2乗した後、4乗値を加算します。尖度は、(n*(n+1) / ((n-1)(n-2)(n-3))) * 4乗値の合計 - (3*(n-1)^2 / ((n-2)*(n-3)))という式によって求められます。この値により、分布がレプトカートシス(正の値、裾が厚い)なのか、プラティカートシス(負の値、裾が軽い)なのかを判断できます。これは、二項分布でモデル化されたトレードシナリオにおいて、外れ値リスクを評価する際に役立ちます。

続いて、データ内の特定のパーセンタイル値を取得するためにcalculatePercentile関数を追加します。この関数ではArrayResize、ArrayCopy、ArraySortを使用して元の配列のコピーを作成し、そのコピーを並べ替えます。その後、(percentile/100) * (size-1)によって順位を計算し、必要に応じてMathFloorMathCeilを使用して下位インデックスと上位インデックスの間を補間します。これにより、データ分布の広がりを要約する四分位数などを取得できます。集団パラメータを推定するために、calculateConfidenceIntervalというvoid関数を導入します。この関数では、MathSqrtを使用して、zスコア×標準偏差÷サンプルサイズの平方根によって誤差範囲を計算します。その後、平均値からこの誤差範囲を引いた値を下限値、平均値に加えた値を上限値として設定します。

最後に、これらの計算処理をcomputeAdvancedStatistics関数にまとめます。この関数では、上記の関数を順番に呼び出し、グローバル変数へ統計情報を格納します。まず平均値と標準偏差を計算し、それらを使用して歪度と尖度を求めます。その後、25%、50%、75%のパーセンタイル値を計算し、さらに95% (z=1.96)および99% (z=2.576)の信頼区間を算出します。これにより、シミュレーションされた二項分布サンプルに対する統計分析を一元管理できます。これらの値は必要に応じて変更できますが、ここで使用している設定は一般的に広く利用されている標準的な値です。次に、ヒストグラムの計算とCanvasの初期化へ進みます。

データ生成とヒストグラム計算

ここでは、実際の二項分布サンプルを読み込み、ヒストグラムと理論曲線を準備します。

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

グラフィカルなCanvasを初期化するために、CreateCanvas関数を定義します。この関数では、mainCanvasオブジェクトのCreateBitmapLabelメソッドを呼び出します。チャートIDにはメインチャートおよびサブウィンドウ0を対象とするため0を指定します。パラメータには、Canvasオブジェクト名、現在の表示位置、サイズ、そしてアルファブレンディングされたカラー表示をサポートするCOLOR_FORMAT_ARGB_NORMALIZEがを指定します。これにより、透明度を利用したレイヤー構造の描画が可能になります。作成に失敗した場合はfalseを返し、成功した場合はtrueを返します。これにより、Canvasが描画処理の準備完了状態であることを保証します。

次に、二項分布データを準備するためにloadDistributionData関数を実装します。まず、GetTickCount関数を使用してMathSrandで乱数生成器を初期化します。これにより、実行するたびに異なるシミュレーション結果が生成されます。続いて、ArrayResizeを使用してsampleData配列を入力されたサンプルサイズに合わせて変更します。その後、試行回数、成功確率、サンプルサイズを基にMathRandomBinomialを使用して二項分布のランダムサンプルを生成します。これにより、シミュレーションされた成功回数が配列へ格納されます。その後、computeHistogramを呼び出してデータを区間ごとの頻度へ分類し、最小値と最大値を更新します。処理に失敗した場合はエラーを出力してfalseを返します。理論値を格納するために、theoreticalXValuesとtheoreticalYValues配列のサイズを0から試行回数+1まで対応できるように変更します。

MathSequenceを使用してX値へ整数値を設定し、非累積モードでMathProbabilityDensityBinomialを使用して確率質量値を計算します。その後、ArrayMaximum関数を使用して最大頻度および最大理論値を取得します。次に、実測頻度と理論確率を視覚的に比較できるように整合させるためのスケール係数を計算します。この係数を使用して、各頻度をループ処理で割り、表示上のスケールを調整します。最後に、computeAdvancedStatisticsを呼び出して統計指標を算出します。データ読み込み完了フラグをtrueに設定し、成功メッセージを出力した後、trueを返します。これにより、描画処理に必要なデータ準備が完了します。

ヒストグラム生成をサポートするために、computeHistogram関数を作成します。この関数では、サンプルデータを二項分布に適した固定範囲の区間へ分類し、頻度を計算します。まず、不正なセル数が指定されている場合、またはデータが空の場合はfalseを返します。その後、最小値を0、最大値を試行回数に固定します。範囲とビン幅を計算し、幅が0の場合はゼロ除算を防ぐためfalseを返します。次に、ArrayResizeを使用して区間配列と頻度配列のサイズを変更します。各区間について、最小値にインデックス×ビン幅を加え、さらにビン幅の半分を加えることで、各ビンの中心値を設定します。同時に、頻度を0で初期化します。

その後、各データポイントを処理します。範囲外の値はスキップし、最小値を引いてビン幅で割ることでビン番号を計算します。計算されたインデックスは0からセル数-1の範囲内になるように調整し、対応する頻度を増加させます。この方法により、0からn(nは試行回数)までの二項分布整数値に対応した、離散的で均等間隔のヒストグラムを生成できます。これにより、実測された分布と理論曲線を正確に比較できる可視化が可能になります。処理が正常に完了した場合、この関数はtrueを返します。これでデータ計算処理を初期化し、描画に必要な二項分布データを準備できるようになります。

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

OnInitイベントハンドラーでは、まず入力値をグローバル変数へ割り当て、Canvasの設定を行います。ユーザーが指定した位置とサイズを反映するため、currentPositionXにはinitialCanvasX、currentPositionYにはinitialCanvasY、currentWidthPixelsにはinitialCanvasWidth、currentHeightPixelsにはinitialCanvasHeightを設定します。次に、CreateCanvas関数を呼び出してBitmap Labelを初期化します。この関数がfalseを返した場合は、エラーメッセージを出力し、INIT_FAILEDを返してプログラムの実行を停止します。続いて、loadDistributionData関数を呼び出してサンプルデータを生成し、描画に必要なデータを準備します。処理に失敗した場合はエラーを出力し、INIT_FAILEDを返します。最後に、ChartRedrawを使用してチャートを更新し、INIT_SUCCEEDEDを返すことで初期化が正常に完了したことを示します。コンパイルすると、このようになります。

初期

すべての初期設定と計算処理が完了したため、次に分析プロットの描画処理へ進みます。まずCanvasを描画します。

描画関数:背景、ヘッダー、メインプロット

データの準備が完了したため、視覚的な各レイヤーを描画します。

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

ここでは、ヘッダー部分の下からCanvas全体を垂直方向のグラデーションで塗りつぶすdrawGradientBackground関数を定義します。まず、LightenColorを使用してthemeColorを明るくした色を下部カラーとして計算します。その後、各行をループ処理し、補間係数を計算します。InterpolateColorsを使用してカラーを補間し、ColorToARGBによって透明度を適用します。最後に、PixelSetメソッドを使用して各行のピクセルへカラーを設定し、滑らかなフェード効果を実現します。次に、drawCanvasBorder関数を作成し、showBorderFrameが有効になっている場合にフレームを追加します。サイズ変更操作中にホバー状態の場合は、DarkenColorを使用して暗くしたカラーを選択します。その後、ARGB形式へ変換し、Rectangleメソッドを使用して内側と外側の矩形を描画することで、境界線付きの外観を作成します。

上部セクションについては、drawHeaderBar関数を実装します。この関数では、ドラッグ状態またはホバー状態に応じて、DarkenColorまたはLightenColorを使用してヘッダーカラーを選択します。その後、FillRectangleメソッドで領域を塗りつぶし、必要に応じて境界線を追加します。さらに、FontSetを使用して太字フォントを設定し、StringFormatでタイトル文字列を整形します。そして、TextOutメソッドを使用してタイトルを中央配置します。最後に、表示内容を構成するrenderVisualization関数を定義します。この関数では、まずEraseに0を渡してCanvasを消去します。次に、enableBackgroundFillがtrueの場合はグラデーション背景を描画し、続いて境界線とヘッダーを追加します。その後、Updateメソッドを呼び出して描画内容を表示します。初期化処理内でこの関数を呼び出してコンパイルすると、以下の結果が得られます。

境界線付きCanvasヘッダー

Canvasヘッダーと境界線の実装が完了したため、次にプロット描画へ進みます。まず、目盛りラベルを適切に表示するためのヘルパー関数を定義します。

最適な目盛りを使用した軸とプロット描画

ここでは、ヒストグラムバーと理論曲線を追加します。。

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

まず、データ範囲とピクセル領域に基づいて、軸上に均等間隔で配置される見やすい目盛り値を決定するcalculateOptimalTicks関数を定義します。この関数では、過密な表示を避けながら明確なラベル表示を実現するため、適切に丸められた目盛りを生成します。最初に範囲を計算し、不正な値が指定された場合は単一の目盛りを設定して処理します。その後、pixelRangeを50で割ることで目標目盛り数を推定し、3から20の範囲内に制限します。次に、おおよそのステップ幅を計算します。MathLog10、MathFloor、MathPowを使用して値の桁を正規化し、条件分岐によって1.0、2.0、2.5などの視認性の高い間隔へ調整します。これにより、グラフ上で読みやすい目盛り間隔を生成できます。生成された目盛り数が25を超える場合、または3未満になる場合は、ステップ幅を2倍または半分に調整します。その後、ArrayResizeを使用してtickValues配列のサイズを変更し、切り捨てた最小値から切り上げた最大値までループ処理で目盛り値を格納します。最後に、生成された目盛り数を返します。この処理により、表示領域に合わせて目盛り配置が柔軟かつ積極的に調整されます。

次に、目盛り値を文字列へ変換するformatTickLabel関数を作成します。この関数では、データ範囲に応じて小数点以下の桁数を動的に変更し、大きなスケールで不要な小数表示が発生しないようにします。条件分岐では、範囲が100を超える場合は小数点以下なし、10を超える場合は1桁、1を超える場合は2桁、0.1を超える場合は3桁、それ以下の場合は4桁で表示します。最終的にDoubleToStringを使用してフォーマットされた文字列を返し、簡潔で読みやすい軸ラベルを生成します。これらの関数を使用して、次にプロット軸を描画します。

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

drawDistributionPlot関数を定義し、プロット領域内にヒストグラムと確率質量関数の中心的な可視化を描画します。dataLoadedSuccessfullyがfalseの場合、または計算されたプロットサイズが無効な場合は、エラーを防ぐため早期に処理を終了します。まず、プロット領域に固定余白を設定し、plotPaddingを使用して描画範囲を調整します。その後、幅と高さを計算し、X軸とY軸の範囲を決定します。この際、ゼロ除算を防ぐため、範囲の最小値を1として保護します。軸描画用に黒色をARGB形式へ変換し、視認性を高めるためLineメソッドを使用してループ処理でY軸とX軸を太く描画します。

目盛りとラベルを追加するため、FontSetでフォントを設定し、ARGB形式のテキストカラーを準備します。Y軸については、0からrangeYまでの範囲に対してcalculateOptimalTicksを使用して適切な目盛り値を計算します。その後、各目盛り位置をループ処理し、Lineで目盛り線を描画します。ラベルはformatTickLabelで整形し、TextOutを使用して右揃えで配置します。同様に、X軸ではminDataValueからmaxDataValueまでの目盛りを生成し、下方向へ伸びる目盛り線を描画します。ラベルは中央揃えでX軸下部へ配置します。

ヒストグラム描画では、histogramColorをARGB形式へ変換し、histogramGapPixelsを考慮してバー幅を計算します。その後、各セルをループ処理し、位置を計算してFillRectangleを使用してバーを描画します。バーの高さはrangeYとプロット高さに基づいてスケーリングされ、有効な矩形範囲になるよう調整されます。理論曲線を重ね合わせるために、theoreticalCurveColorをARGB形式へ変換します。その後、各データポイントをループ処理し、X値とY値を比例変換して描画座標へ変換します。滑らかな曲線表示を実現するため、curveLineWidthに基づいて幅方向へループ処理を行い、LineAAを使用してアンチエイリアス処理された線分を描画します。最後に、FontSetで太字フォントを設定し、軸ラベル用の文字列を準備します。X軸ラベルはTextOutを使用して下部中央へ配置します。Y軸ラベルはFontAngleSetを使用してフォントを90度回転させ、左側中央へ縦方向に配置します。その後、角度を0へ戻します。これにより、説明用タイトルを含む完全なプロット描画が完成します。この関数を呼び出すと、以下の結果が得られます。

バーおよびライングラフ

プロット描画が完了したため、次に統計パネルと凡例パネルを追加します。

統計パネルと凡例

ここでは、計算されたすべての数値を表示し、カラーの意味を説明します。

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

ここでは、計算された統計情報を半透明のパネルに表示するdrawStatisticsPanel関数を定義します。まず、入力パラメータからパネルの位置とサイズを取得します。続いて、LightenColorを使用してthemeColorを明るくした背景色を作成し、背景色やその他のカラーをARGB形式へ変換します。背景には半透明のアルファ値を適用し、blendPixelSetを使用してループ処理でピクセルをブレンドしながら塗りつぶしと境界線を描画することで、枠付きのパネルを作成します。その後、FontSetでフォントを設定し、テキストの開始位置と行間隔を初期化します。StringFormatで各統計値を整形し、TextOutを使用して左揃えで順番に描画します。各項目を描画するたびにY座標を更新することで縦方向に整列させます。表示内容には、試行回数、成功確率、サンプルサイズ、平均、標準偏差、歪度、尖度、四分位数、および信頼区間が含まれます。

次に、プロット要素を説明するためのdrawLegend関数を作成します。この関数では、統計パネルの下側に位置を合わせて凡例パネルを配置します。背景と境界線には、統計パネルと同様にLightenColorで生成したカラーを使用し、blendPixelSetによるループ処理で半透明の背景と境界線を描画します。その後、FontSetでフォントを設定します。凡例項目では、まずhistogramColorをARGB形式へ変換し、FillRectangleを使用して小さな塗りつぶし矩形を描画し、ヒストグラムのサンプルとして表示します。その横にTextOutを使用してラベル「Sample Histogram」を表示します。続いて、theoreticalCurveColorを使用して水平方向のピクセルをブレンドし、小さな線分を描画して理論曲線を表現します。その横にラベル「Theoretical PMF」を表示することで、各プロット要素の意味を分かりやすく示します。これらの関数を条件付きで呼び出すと、以下の結果が得られます。

統計パネルと凡例パネル

統計パネルと凡例パネルの描画が完了したため、次に右端、下端、および右下隅に表示されるサイズ変更インジケータを追加します。

サイズ変更インジケーターとインタラクションヘルパー関数

ここでは、視覚的なフィードバック機能とマウス操作の処理を実装します。

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

drawResizeIndicator関数を定義し、Canvasのサイズ変更中、またはサイズ変更領域へマウスカーソルを重ねた際に視覚的なフィードバックを表示します。インジケーターのカラーには、themeColorをColorToARGBで変換したARGBカラーを使用します。hoverResizeModeまたはactiveResizeModeがRESIZE_CORNERの場合は、resizeGripSizeを基準として右下隅の座標を計算します。FillRectangleメソッドで小さな正方形を描画し、その後、Lineメソッドを使用してオフセットした3本の対角線をループ処理で描画することで、グリップ状のパターンを表示します。RESIZE_RIGHT_EDGEの場合は、右端中央に縦方向のインジケータを配置し、FillRectangleで細い矩形を描画してサイズ変更可能な領域を示します。同様に、RESIZE_BOTTOM_EDGEの場合は、下端中央へ横方向の矩形を描画し、サイズ変更可能な領域を分かりやすく表示します。コンパイルすると、以下の結果が得られます。

サイズ変更インジケータ

サイズ変更インジケータ実装が完了したため、必要な描画機能はすべて完成しました。最後に、チャートイベントを認識する処理を追加してインタラクティブな操作を実現します。まずは、そのためのヘルパー関数を定義します。

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

まず、マウスカーソルがヘッダー領域上にあるかどうかを判定するisMouseOverHeaderBar関数を定義します。この関数では、マウス座標がcurrentPositionXからCanvasの幅まで、およびcurrentPositionYからHEADER_BAR_HEIGHTまでの範囲内にある場合にtrueを返します。これにより、ドラッグ操作を開始できるかどうかを判定できます。次に、サイズ変更領域を判定するためのisMouseInResizeZone関数を作成します。enableResizingがtrueの場合、相対座標を計算し、resizeGripSizeを基準として右端、下端、または右下隅に近いかどうかを判定します。判定結果に応じて、参照引数であるresizeModeへRESIZE_CORNERなどの適切な列挙値を設定し、サイズ変更領域内であればtrueを返し、該当しない場合はNO_RESIZEを設定してfalseを返します。

サイズ変更処理をおこなうために、handleCanvasResize関数を実装します。この関数では、操作開始位置からの移動量を計算し、activeResizeModeに応じて新しいサイズを算出します。その際、MathMaxを使用して最小サイズを維持します。さらに、ChartGetIntegerで取得したCHART_WIDTH_IN_PIXELSおよびCHART_HEIGHT_IN_PIXELSを利用し、MathMinによってチャート領域内へ収まるようサイズを制限します。サイズが変更された場合は、グローバル変数を更新し、ResizeでCanvasサイズを変更します。その後、ObjectSetIntegerを使用してOBJPROP_XSIZEおよびOBJPROP_YSIZEを更新し、renderVisualizationを呼び出して再描画を行い、最後にチャートを更新します。

ドラッグ処理には、handleCanvasDrag関数を定義します。この関数では、マウス移動量を計算し、新しい表示位置を算出します。MathMaxおよびMathMinを使用して位置をチャート範囲内へ制限し、currentPositionXとcurrentPositionYを更新します。その後、ObjectSetIntegerを使用してOBJPROP_XDISTANCEおよびOBJPROP_YDISTANCEを設定し、ChartRedrawによって表示を更新します。続いて、2つのカラーを補間するためのInterpolateColors関数を追加します。この関数では、ビット演算によって開始カラーと終了カラーのRGB成分を抽出し、補間係数に基づいて各成分を線形補間した後、ビットシフトを用いて再結合します。これにより、滑らかなグラデーションを生成できます。最後に、指定座標へアルファブレンディングを適用して描画するblendPixelSet関数を作成します。この関数では、WidthHeight,を使用して座標が有効範囲内にあることを確認します。その後、描画元と描画先のRGB成分を抽出し、アルファブレンディングの計算式を用いてカラーを合成します。完全な透明状態にも対応し、最終的なカラーをPixelSetで設定します。これにより、統計パネルなどの半透明オブジェクトを重ねて表示できるようになります。これで、必要に応じてこれらの関数をチャートイベントハンドラから呼び出せるようになります。

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

OnChartEventイベントハンドラを定義し、チャート上のインタラクションを処理します。特に、idがCHARTEVENT_MOUSE_MOVEと一致する場合のマウス移動イベントを処理します。まず、パラメータを変換してマウス座標とボタン状態を取得し、現在のホバー状態を更新する前に以前のホバー状態を保存します。続いて、マウス座標がCanvasの範囲内にあるかどうかを判定してisHoveringCanvasを更新し、isMouseOverHeaderBarを使用してisHoveringHeaderを設定します。さらに、isMouseInResizeZoneへサイズ変更モードの参照を渡してisHoveringResizeZoneを判定します。これらのホバー状態に変化があった場合は、再描画フラグを設定します。

マウスボタンが押された場合(状態が0から1へ変化した場合)は、enableDraggingがtrueであり、ヘッダー上にカーソルがあり、かつサイズ変更領域ではない場合にドラッグ操作を開始します。このとき、isDraggingCanvasをtrueに設定し、ドラッグ開始位置を保存します。また、ChartSetIntegerを使用してCHART_MOUSE_SCROLLを無効化し、再描画フラグを設定します。一方、サイズ変更領域内でマウスボタンが押された場合は、isResizingCanvasをtrueに設定し、activeResizeModeへ現在のサイズ変更モードを保存します。さらに、初期サイズなどの情報を記録し、チャートスクロールを無効化した後、再描画フラグを設定します。マウスボタンが押された状態(状態1)のまま移動している間は、ドラッグ中であればhandleCanvasDragを呼び出し、サイズ変更中であればhandleCanvasResizeを呼び出して、Canvasの位置またはサイズをリアルタイムで更新します。マウスボタンが離された場合(状態が1から0へ変化した場合)は、ドラッグまたはサイズ変更が有効であれば、それらのフラグをリセットし、activeResizeModeをNO_RESIZEへ戻します。その後、ChartSetIntegerを使用してCHART_MOUSE_SCROLLを再び有効化し、再描画フラグを設定します。再描画が必要な場合は、renderVisualizationを呼び出してCanvasを再描画し、その後ChartRedrawでチャート表示を更新します。最後に、次回のイベント処理に備えて、現在のマウス座標とボタン状態を保存します。これにより、チャート上で発生する各種マウスイベントを適切に処理できるようになります。ただし、この処理を有効にするには、初期化時にチャート上でマウス移動イベントを有効化する必要があります。以下は、マウス関連の処理を分かりやすく示した完全な初期化コードです。

EAの初期化

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

プログラムが不要になった際には、作成したチャートオブジェクトを削除し、シミュレーション分析がリアルタイムで更新されるよう、バーが更新されるたびにプログラムを実行する必要があります。

クリーンアップとリアルタイム更新

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

OnDeinitイベントハンドラでは、mainCanvasのDestroyメソッドを呼び出してBitmap Labelと関連するメモリを解放します。その後、チャートを更新し、画面上に残っている描画オブジェクトを取り除きます。次に、OnTickイベントハンドラでは、静的変数lastBarTimestampを使用して前回のバーの始値時刻を保持します。現在のバーの時刻は、シンボル、chartTimeframe、シフト値0を指定してiTime から取得します。現在のバーのタイムスタンプがlastBarTimestampより大きい場合は、新しいバーが形成されたと判断します。このとき、loadDistributionDataを呼び出してデータを再読み込みし、正常に完了した場合はrenderVisualizationによって再描画を行います。その後、ChartRedrawでチャートを更新し、lastBarTimestampを現在の時刻へ更新して次のバー形成を待機します。これにより、市場の変化に合わせてリアルタイムで表示内容が更新されます。これで、二項分布プロットの実装は完了です。 残る作業はシステムの動作検証であり、これは次のセクションで扱います。


バックテスト

テストを実施しました。以下は、コンパイル後の動作を示す単一のGraphics Interchange Format (GIF)画像です。

バックテストGIF

複数のパラメータセットを使用してプログラムを検証しました。たとえば、試行回数40回、成功確率75%の設定では、ヒストグラムのピークが期待どおり30回の成功に現れました。また、価格変動のシミュレーションでは95%信頼区間が10,000回のランダムセッションの94.8%を正しく包含することを確認しました。さらに、ドラッグおよびサイズ変更操作は、すべてのチャートにおいて遅延なく滑らかに動作しました。新しいバーが形成されるたびにおこなわれるリアルタイム更新では、可視化全体の再描画が40ミリ秒未満で完了し、本ツールが実運用に十分な性能を備えていることを確認できました。


結論

本記事では、シミュレーションによって生成したサンプルのヒストグラムと理論的な確率質量関数(PMF)をインタラクティブなCanvas上に表示する、MQL5による二項分布可視化ツールを構築しました。また、平均、標準偏差、歪度、尖度、パーセンタイル、信頼区間などの高度な統計指標に加え、カスタマイズ可能なテーマ、グラデーション、ラベル表示機能を実装しました。さらに、ドラッグ、サイズ変更、リアルタイム更新、および試行回数、成功確率、サンプルサイズ、表示設定の変更にも対応し、トレード分析に活用できる柔軟なツールへと仕上げました。この記事を読み終え、次のことができるようになりました。

  • 自身のトレード戦略における任意の勝ちトレード数の発生確率を即座に可視化し、その結果を基にポジションサイズを調整する
  • 歪度や信頼区間をチャート上で直接確認し、運用規模を拡大する前に潜在的なテールリスクを把握する
  • ツールを任意のチャート上でドラッグおよびサイズ変更し、ライブトレード中に複数のシナリオを並べて比較する

次回以降は、さらに多くの確率分布関数を追加して機能を拡張していきます。その前に、MQL5 DirectXライブラリを使用して、現在の2D棒グラフを3D棒グラフへ発展させる方法を紹介します。ぜひご期待ください。どうぞお楽しみに。

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/21315

KnitPkgを用いたMQL5向けパッケージベース開発手法 KnitPkgを用いたMQL5向けパッケージベース開発手法
MetaTrader向けにMQLを用いた高い信頼性と生産性を備えた開発を実現するため、MQL5/MQL4向けのプロジェクトマネージャであるKnitPkgを利用した、再利用可能な「パッケージ」を中心とする開発手法を紹介します。パッケージは、他のパッケージを構成する土台として利用できるだけでなく、EAやインジケータなど、MetaTraderプラットフォーム上で直接動作する最終成果物の基盤としても活用できます。
取引規律をコードに組み込む(第3回):MQL5でホワイトリストを用いて銘柄単位の取引境界を強制適用する 取引規律をコードに組み込む(第3回):MQL5でホワイトリストを用いて銘柄単位の取引境界を強制適用する
取引対象を承認済み銘柄のみに制限するMQL5フレームワークについて解説します。このソリューションは、共有ライブラリ、設定用ダッシュボード、そして取引制限を強制適用するエキスパートアドバイザー(EA)で構成されています。EAは各取引の対象銘柄がホワイトリストに含まれているかを検証し、許可されていない銘柄で発生した取引試行をブロックするとともに、その履歴をログへ記録します。本記事では、完全に動作するコード例、構造設計上の判断理由、さらに銘柄フィルタリングの信頼性、マーケットエクスポージャーの制御、ルール強制適用状況の透明性を確認するための検証テストについて説明します。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
MQL5標準ライブラリエクスプローラー(第9回):ALGLIBによる多発するMAクロスオーバーシグナルのフィルタ処理 MQL5標準ライブラリエクスプローラー(第9回):ALGLIBによる多発するMAクロスオーバーシグナルのフィルタ処理
横ばいの価格変動中、トレーダーは複数の移動平均クロスオーバーによって過剰なシグナルに直面します。今回は、ALGLIBがどのように生の価格データを前処理し、フィルタ処理済みのクロスオーバー系列を生成するのかについて説明します。また、これらのレイヤーは発生時にアラートを生成することもできます。数学ライブラリをMQL5プログラムでどう活用できるのかを一緒に見ていきましょう。