MQL5取引ツール(第22回):二項分布のヒストグラムと確率質量関数(PMF)の描画
はじめに
勝敗が連続して発生するトレードシステムを運用していても、「過去の勝率が75%の場合、30回の取引中20回勝利する確率はどの程度なのか」「5連敗が発生する可能性はどの程度あるのか」といった単純でありながら重要な質問に答えられない場合があります。二項分布を利用してモデル化する手段がなければ、これらの問題は推測に頼ることになります。その結果、現実的なリスク評価、適切なポジションサイズの設定、または戦略のパフォーマンスが統計的に有意であるかどうかの検証が困難になります。このような確率的な洞察の不足は、過剰なレバレッジ、感情的な判断、非現実的な利益期待につながる可能性があります。本記事は、MetaQuotes Language 5 (MQL5)の開発者およびアルゴリズムトレーダーを対象としており、トレード戦略の確率的な挙動を定量的に評価することを目的としています。
前回の記事(第21回)では、MQL5の線形回帰可視化ツールを拡張し、ネオン発光、アニメーション、ホログラム効果を備えたサイバーパンクテーマモードを追加することで、没入感のある可視化機能を実現しました。今回の第22回では、インタラクティブなキャンバス上で、シミュレーションサンプルのヒストグラムと理論的な確率質量関数曲線を表示する、二項分布可視化用のMQL5可視化ツールを構築します。さらに、平均、標準偏差、歪度、尖度、パーセンタイル、信頼区間などの高度な統計指標を追加し、カスタマイズ可能なテーマ、グラデーション、ラベル表示にも対応します。加えて、ドラッグ操作、サイズ変更、リアルタイム更新、試行回数、成功確率、サンプル数、表示設定などのパラメータ調整機能を実装し、トレード分析への活用を可能にします。本記事では以下のトピックを扱います。
最後まで読むことで、二項分布を可視化できる実用的な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)によって順位を計算し、必要に応じてMathFloorとMathCeilを使用して下位インデックスと上位インデックスの間を補間します。これにより、データ分布の広がりを要約する四分位数などを取得できます。集団パラメータを推定するために、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ヘッダーと境界線の実装が完了したため、次にプロット描画へ進みます。まず、目盛りラベルを適切に表示するためのヘルパー関数を定義します。
最適な目盛りを使用した軸とプロット描画
ここでは、ヒストグラムバーと理論曲線を追加します。。
//+------------------------------------------------------------------+ //| 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関数を作成します。この関数では、WidthとHeight,を使用して座標が有効範囲内にあることを確認します。その後、描画元と描画先の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)画像です。

複数のパラメータセットを使用してプログラムを検証しました。たとえば、試行回数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
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
KnitPkgを用いたMQL5向けパッケージベース開発手法
取引規律をコードに組み込む(第3回):MQL5でホワイトリストを用いて銘柄単位の取引境界を強制適用する
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
MQL5標準ライブラリエクスプローラー(第9回):ALGLIBによる多発するMAクロスオーバーシグナルのフィルタ処理
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索