English Deutsch
preview
MQL5でカスタムインジケーターを作成する(第5回):WaveTrend Crossover Evolution:Canvasを用いたフォグ状グラデーション、シグナルバブル、リスク管理

MQL5でカスタムインジケーターを作成する(第5回):WaveTrend Crossover Evolution:Canvasを用いたフォグ状グラデーション、シグナルバブル、リスク管理

MetaTrader 5トレーディングシステム |
21 0
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

前回の記事(第4回)では、MetaQuotes Language 5 (MQL5)を用いてSmart WaveTrend Crossoverインジケーターを開発しました。このインジケーターは、シグナル用とトレンドフィルタ用の2つのオシレーターを備え、クロスオーバーに基づく売買シグナルを生成するとともに、任意でトレンド確認もおこないます。第5回では、このWaveTrend Crossoverインジケーターをさらに発展させ、Canvasベースの描画によるフォグ状グラデーション、ブレイクアウトを検出するシグナルボックス、視覚的アラートとしてのカスタマイズ可能なバブルや三角形表示を追加します。また、リスク管理機能として、ローソク足倍率やパーセンテージに基づいて算出される動的なテイクプロフィット(TP)およびストップロス(SL)を導入し、ラインやテーブル形式で表示できるようにします。この進化により、フォグ状グラデーションによる相場環境の可視化に加え、トレンドフィルタリング、ボックス延長、そしてリスク計算機能を統合した高度な視覚表現が可能になります。本記事では以下のトピックを扱います。

  1. ビジュアルおよびリスク機能を備えた拡張CanvasベースWaveTrend Crossoverフレーム
  2. MQL5での実装
  3. バックテスト
  4. 結論

最終的に、本記事を通じてカスタマイズ可能な拡張WaveTrendクロスオーバーインジケーターを構築し、ビジュアル要素とリスク管理機能を統合した実用的なMQL5ツールを完成させることを目指します。それでは解説に進みます。


ビジュアルおよびリスク機能を備えた拡張CanvasベースWaveTrend Crossoverフレーム

拡張されたCanvasベースのWaveTrendクロスオーバーフレームワークは、コアとなるモメンタムオシレーターを基盤としつつ、ビジュアルオーバーレイとリスク管理ツールを組み込むことで、より直感的で実用的な取引インターフェースになります。本構造は、従来通りデュアルWaveTrend構成を維持しており、1つはクロスオーバーを検出するための高感度オシレーターとして機能し、エントリーシグナルを生成します。もう1つはより遅い設定でトレンドフィルタとして機能し、ノイズを抑えつつ方向性を確認します。これに加えて、クロスオーバーポイント周辺にシグナルボックスを生成し、価格がそのレンジを上抜け/下抜けした際にボックスをクローズし、ブレイクアウトを検出します。単なるクロスオーバーではなく確認されたモメンタム変化を捉える設計です。さらに、フォグ状のグラデーションをチャート上に重ね、透明度の変化でトレンドの強さを表現します。これにより相場状況をひと目で把握できます。さらに、買いと売りのシグナルはラベル付きのバブルやシンプルな三角形で表示され、視認性の高いアラートとして機能します。

上昇トレンドのシナリオでは、シグナル用オシレーターが上方向にクロスした場合、必要に応じてスローモードのオシレーターによる上昇トレンド確認を条件として、該当バーのレンジ周辺にボックスを生成します。その後、価格が上方向にブレイクした際に、ボックスの方向と一致していれば買いシグナルが発生し、視覚的要素によってその機会が強調されます。一方で下降トレンドでは、下方向クロスによってボックスが形成され、下方向へのブレイクアウトが発生した場合に売りシグナルが生成されます。これにより、反転だけでなくトレンド継続局面もノイズを抑えながら捉えることが可能になります。リスク管理機能としては、平均的なローソク足サイズまたはパーセンテージ変動を基準にTPおよびSLを動的に計算し、それらをラインおよびテーブルとしてチャート上に表示します。これによって、ポジションサイズ設計やエグジット計画を視覚的に補助できるようになります。こうすることで、ヒット率を把握することができます。

MQL5 Canvasライブラリを活用して、バー間を補間する霧状のグラデーションを描画し、滑らかなトレンドの視覚化を実現します。また、シグナルボックスを追跡してブレイクアウトを検出し、平均ローソク足乗数を使用したオプションの拡張機能でブレイクアウトを決済します。さらに、ラベル付きバブルなどの柔軟なシグナルタイプを提供し、視認性を向上させます。ユーザー定義モードで利益確定と損切りを設定し、リスクレベルを計算します。これらすべてを実現すると同時に、チャートの変更時に効率的な再描画を保証します。以下に想定されるビジュアル表示の例を示します。

MQL5 CANVASベースの進化フレームワーク


MQL5での実装

拡張機能の実装を開始するにあたり、まず最初におこなうべきことは、追加予定の機能に対応するためにインジケーター内部のバッファ構成を調整し、より多くのデータを保持できるようにすることです。

//+------------------------------------------------------------------+
//|                           1. Smart WaveTrend Crossover PART2.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 indicator_chart_window
#property indicator_buffers 28
#property indicator_plots 3
#property indicator_label1 "Colored Candles"
#property indicator_type1 DRAW_COLOR_CANDLES
#property indicator_color1 clrTeal, clrRed
#property indicator_style1 STYLE_SOLID
#property indicator_width1 1
#property indicator_label2 "Buy Signals"
#property indicator_type2 DRAW_ARROW
#property indicator_color2 clrForestGreen
#property indicator_style2 STYLE_SOLID
#property indicator_width2 1
#property indicator_label3 "Sell Signals"
#property indicator_type3 DRAW_ARROW
#property indicator_color3 clrOrangeRed
#property indicator_style3 STYLE_SOLID
#property indicator_width3 1

ここでは、追加機能に対応するためにインジケーターバッファ数を23から28へ増加させます。これで、拡張された機能で必要となる追加計算データを格納できるようになります。変更箇所は分かりやすくするために該当行を強調しています。次におこなうのは、チャート上でのカスタム描画を可能にするためのCanvasライブラリのインクルードです。標準のMQL5プロット機能では実現できないフォグ状グラデーション、カスタムボックス、ラベルなどの高度なビジュアル要素を描画するために必要となります。そのために、以下の手法を使用します。

#include <Canvas/Canvas.mqh>

このように#include <Canvas/Canvas.mqh>を追加すると、チャート上にカスタムグラフィックを描画するためのクラスおよび関数が利用可能になります。その結果、標準プロットに依存せず、フォグ表現やボックス描画、ラベル表示などをプログラム的に制御でき、シグナルやトレンドの視覚表現を大幅に強化できます。続いて、インターフェースからより柔軟に制御できるように、追加の入力パラメータを定義していきます。

input group "Signal Settings"
input bool use_trend_filter = true;        // Use Trend Filter for Boxes?

enum signal_options {
   Triangles,                              // Triangles
   Labels_Buy_Sell                         // Labels Buy Sell
};
input signal_options signal_type = Labels_Buy_Sell; // Signal Type
input color signal_buy_col = clrForestGreen; // Buy Signal Color
input color signal_sell_col = clrOrangeRed; // Sell Signal Color
input bool show_only_matching = true;      // Show Only Matching Signals?
input bool use_box_multiplier = false;     // Extend Box by Average Candle Size?
input double box_multiplier = 1.0;         // Box Extension Multiplier
input int base_offset = 10;                // Base Signal Offset from Candle

input group "Box Settings"
input color box_bull_fill = clrBlue;       // Box Bull Fill Color
input color box_bear_fill = clrGold;       // Box Bear Fill Color
input int box_fill_transp = 80;            // Box Fill Transparency (0-100)

input group "Fog"
input bool show_fog = true;                // Fog
input double offset_mult = 0.7;            // Fog Height × Avg Candle
input int base_transp = 80;                // Base Transparency
input int transp_inc = 4;                  // Transparency Increment

input group "Risk Management"
input bool showTPSL = true;                // Show TP/SL Levels

enum tp_sl_modes {
   Candle_Multiplier,                      // Candle Multiplier
   Percentage                              // Percentage
};
input tp_sl_modes tpSlMode = Candle_Multiplier; // TP/SL Calculation Mode
input int tp_sl_length = 50;               // Average Candle Length Period
input double tp1Multiplier = 2.0;          // TP1×
input double tp2Multiplier = 3.0;          // TP2×
input double tp3Multiplier = 4.0;          // TP3×
input double slMultiplier = 2.0;           // SL×
input double tp1Percent = 2.0;             // TP1 %
input double tp2Percent = 3.0;             // TP2 %
input double tp3Percent = 4.0;             // TP3 %
input double slPercent = 1.5;              // SL %

ユーザー入力は引き続きグループ化されたセクションとして定義し、拡張機能を柔軟にカスタマイズできる構成にしています。まずSignal Settingsグループでは、ボックスベースのシグナルにトレンドフィルタを適用するかどうかを制御する真偽値入力をデフォルトtrueで用意します。続いてsignal_optionsの列挙型を追加し、シグナル表示形式としてTriangles(矢印表示)またはLabels_Buy_Sell(テキストバブル表示)を選択できるようにし、デフォルトは後者に設定して視認性の高いラベル表示を採用します。さらに、買いと売りのシグナルの色設定としてそれぞれフォレストグリーンとオレンジレッドをデフォルト値で指定します。また、ボックス方向と一致するシグナルのみを表示するためのフィルタ機能を持つ真偽値をデフォルト有効で追加します。ボックス拡張関連では、平均ローソク足サイズに基づいてボックスを延長する機能を制御する真偽値をデフォルト無効で追加し、その拡張倍率を指定するdouble値(1.0)と、シグナル位置調整用のオフセット値(10ポイント)を整数で設定します。

次にBox Settingsグループでは、強気のボックスおよび弱気のボックスの塗りつぶしカラーをそれぞれブルーとゴールドで定義し、さらに透明度を0〜100の範囲で制御する整数入力を追加します。この値はデフォルト80とし、ボックス描画の不透明度を調整するために使用します。Fogグループでは、フォグ状グラデーション表示の有効化を制御する真偽値をデフォルトtrueで設定します。さらに、平均ローソク足サイズに基づいてフォグの高さをスケーリングする倍率(0.7)、グラデーションのベース透明度(80)、および滑らかなフェードを実現するための透明度増分(4)を定義します。最後にRisk Managementグループでは、TPおよびSL表示の有効化を制御する真偽値をデフォルトtrueで追加します。さらに、計算方式としてCandle_MultiplierまたはPercentageを選択できるtp_sl_modes列挙型を導入し、デフォルトは前者とします。加えて、平均ローソク足サイズの算出期間(50)を整数で設定し、TPおよびSLについては倍率またはパーセンテージをdouble型で定義します。たとえば、第一TPの倍率は2.0、SLのパーセンテージは1.5とし、複数段階のリスク設定に対応します。

次に、ボックス拡張、フォグの高さ、TP/SL計算といった新機能で必要となるボラティリティベースのスケーリングをおこなうため、平均ローソク足サイズを保持するグローバルバッファを拡張します。このバッファは、ボックスの延長距離、フォグの表示範囲、TPおよびSLの計算などに利用され、すべての視覚要素と価格レベルを動的にスケーリングするための基準値として機能します。

double avg_candle_size[];     //--- Average candle size buffer

これに加えて、グローバル変数も拡張する必要があります。具体的には、Canvasオブジェクトの管理、動的描画のためのチャートプロパティ、再描画タイミングを制御するタイムスタンプ、ボックスおよびシグナル情報を保持する構造体、それらを格納する配列、TP/SL表示用のラインやテーブル名、ボックス拡張期間、オブジェクト識別用プレフィックス、そしてフォントサイズなどを定義します。これらの変数は、カスタム描画の管理、チャートの可視領域追跡による最適化、ボックスやシグナルの永続データ保持、そしてTP/SL表示の制御に使用されます。結果として、効率的な再描画処理とスケーラブルなビジュアル描画を実現するための基盤となります。

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
CCanvas obj_Canvas;           //--- Canvas object

int currentChartWidth = 0;    //--- Current chart width
int currentChartHeight = 0;   //--- Current chart height
int currentChartScale = 0;    //--- Current chart scale
int firstVisibleBarIndex = 0; //--- First visible bar index
int visibleBarsCount = 0;     //--- Visible bars count
double minPrice = 0.0;        //--- Minimum price
double maxPrice = 0.0;        //--- Maximum price

static datetime lastRedrawTime = 0; //--- Last redraw time

//+------------------------------------------------------------------+
//| Box information structure                                        |
//+------------------------------------------------------------------+
struct BoxInfo {
   datetime left_time;        // Store left time
   datetime right_time;       // Store right time
   double   top;              // Store top price
   double   bottom;           // Store bottom price
   int      dir;              // Store direction
};
BoxInfo all_boxes[];          //--- All boxes array

//+------------------------------------------------------------------+
//| Signal information structure                                     |
//+------------------------------------------------------------------+
struct SignalInfo {
   datetime time;             // Store signal time
   int      dir;              // Store signal direction
};
SignalInfo all_signals[];     //--- All signals array

string slLine = "SL_Line";    //--- SL line name
string tp1Line = "TP1_Line";  //--- TP1 line name
string tp2Line = "TP2_Line";  //--- TP2 line name
string tp3Line = "TP3_Line";  //--- TP3 line name
string tpSlTableObjects[11];  //--- TP/SL table objects

long extendSeconds = PeriodSeconds() * 100; //--- Extend seconds

string objPrefix = "SWTC_";   //--- Object prefix

int current_font_size;        //--- Current font size

ここでは、インジケーターの状態管理およびカスタムビジュアル制御のためにグローバル変数を宣言します。まず、Canvasベースの描画処理を全体で扱うために、CCanvasクラスのインスタンスとしてobj_Canvasを用意します。次に、チャートの動的描画に必要な情報を保持するための整数変数として、現在のチャート幅、高さ、スケール、最初に表示されているバーのインデックス、そして可視バー数を定義します。また、可視チャート領域の最小価格および最大価格を保持するdouble型変数を用意し、再描画時の動的調整に対応できるようにします。さらに、最後にCanvasを更新した時刻を記録するための静的datetime変数lastRedrawTimeを0で初期化し、描画頻度を最適化します。

シグナルボックス管理のためにBoxInfo構造体を定義します。この構造体には、左端および右端のタイムスタンプ、上限および下限価格、そして方向を示す整数値を含めます。その後、複数のボックス情報を管理するための配列all_boxesを用意します。

同様に、シグナル管理用としてSignalInfo構造体を定義し、タイムスタンプと方向情報を保持します。生成されたシグナルを表示用に保持するための配列all_signalsも併せて用意します。リスク管理表示のために、TPおよびSLラインの命名用文字列を定義します。例えばストップロス用としてSL_Lineを設定し、リスク管理テーブルのオブジェクト参照用としてサイズ11の文字列配列tpSlTableObjectsを用意します。さらに、チャート周期に基づいてPeriodSecondsに100を掛けた値を用い、表示要素の延長期間を制御するためのlong型変数extendSecondsを設定します。最後に、オブジェクト名の衝突を防ぐためのプレフィックスとしてSWTC_を定義し、チャートスケールに応じてテキストサイズを動的に調整するための整数変数current_font_sizeを用意します。次に、オブジェクトの可視化のためのヘルパー関数をいくつか定義します。

//+------------------------------------------------------------------+
//| Darken color                                                     |
//+------------------------------------------------------------------+
color DarkenColor(color c, double factor = 0.5) {
   uchar r = uchar((c & 0xFF) * factor);         //--- Compute red component
   uchar g = uchar(((c >> 8) & 0xFF) * factor);  //--- Compute green component
   uchar b = uchar(((c >> 16) & 0xFF) * factor); //--- Compute blue component
   return (color)((b << 16) | (g << 8) | r);     //--- Return darkened color
}

//+------------------------------------------------------------------+
//| Draw rectangle label                                             |
//+------------------------------------------------------------------+
bool drawRectangleLabel(string objectName, int xDistance, int yDistance, int xSize, int ySize, color rectColor, int borderType = BORDER_FLAT, bool back = true) {
   bool objectExists = (ObjectFind(0, objectName) >= 0);                //--- Check if object exists
   if (!objectExists) {                                                 //--- Handle new object
      if (!ObjectCreate(0, objectName, OBJ_RECTANGLE_LABEL, 0, 0, 0)) { //--- Create rectangle label
         Print("Failed to create ", objectName);                        //--- Log failure
         return false;                                                  //--- Return failure
      }
   }
   ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, xDistance);       //--- Set x distance
   ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, yDistance);       //--- Set y distance
   ObjectSetInteger(0, objectName, OBJPROP_XSIZE, xSize);               //--- Set x size
   ObjectSetInteger(0, objectName, OBJPROP_YSIZE, ySize);               //--- Set y size
   ObjectSetInteger(0, objectName, OBJPROP_COLOR, rectColor);           //--- Set color
   ObjectSetInteger(0, objectName, OBJPROP_BORDER_TYPE, borderType);    //--- Set border type
   ObjectSetInteger(0, objectName, OBJPROP_BACK, back);                 //--- Set background
   ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false);          //--- Disable selectable
   ObjectSetInteger(0, objectName, OBJPROP_SELECTED, false);            //--- Disable selected
   return true;                                                         //--- Return success
}

//+------------------------------------------------------------------+
//| Draw label                                                       |
//+------------------------------------------------------------------+
bool drawLabel(string objectName, int xDistance, int yDistance, string text, color labelColor) {
   bool objectExists = (ObjectFind(0, objectName) >= 0);                //--- Check if object exists
   if (!objectExists) {                                                 //--- Handle new object
      if (!ObjectCreate(0, objectName, OBJ_LABEL, 0, 0, 0)) {           //--- Create label
         Print("Failed to create ", objectName);                        //--- Log failure
         return false;                                                  //--- Return failure
      }
   }
   ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, xDistance);       //--- Set x distance
   ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, yDistance);       //--- Set y distance
   ObjectSetString(0, objectName, OBJPROP_TEXT, text);                  //--- Set text
   ObjectSetInteger(0, objectName, OBJPROP_COLOR, labelColor);          //--- Set color
   ObjectSetInteger(0, objectName, OBJPROP_FONTSIZE, 10);               //--- Set font size
   ObjectSetString(0, objectName, OBJPROP_FONT, "Arial");               //--- Set font
   ObjectSetInteger(0, objectName, OBJPROP_BACK, false);                //--- Disable background
   ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false);          //--- Disable selectable
   ObjectSetInteger(0, objectName, OBJPROP_SELECTED, false);            //--- Disable selected
   return true;                                                         //--- Return success
}

まずDarkenColor関数では、入力された色値と任意の係数(デフォルト0.5)を使用して、色を暗くした新しい色を生成します。色は赤、緑、青の各成分に分解され、ビット演算によって抽出されます。具体的には、赤は&0xFF、緑は8ビット右シフト後に&0xFF、青は16ビット右シフト後に&0xFFで取得します。その後、各成分に係数を掛けて減衰させ、uchar型にキャストしたうえで再構築し、左シフトとビットOR演算によって再び1つの色値として結合します。

次にdrawRectangleLabel関数を定義し、チャート上の矩形ラベルの描画および更新を管理します。まずObjectFindでオブジェクトの存在を確認し、存在しない場合はObjectCreateを用いてOBJ_RECTANGLE_LABEL型として作成します。作成に失敗した場合はPrintでエラーメッセージを出力しfalseを返します。既に存在する場合は、xとyの距離、サイズ、色、枠線タイプ(デフォルトはBORDER_FLAT)、背景表示(デフォルトtrue)などのプロパティを設定し、選択不可および操作不可に設定したうえでtrueを返します。

同様にdrawLabel関数ではテキストラベルの描画処理をおこないます。まずObjectFindで存在確認をおこない、存在しない場合はObjectCreateでOBJ_LABELとして生成します。生成失敗時にはエラーメッセージを出力しfalseを返します。その後、xとyの距離、表示テキスト、色、フォントサイズ(10)、フォント(Arial)を設定し、背景非表示、選択不可、操作不可に設定したうえで、正常終了時にtrueを返します。これらの基盤関数の実装後、初期化処理では新たに追加した平均ローソク足サイズ用バッファのバインドをおこなう必要があります。また、Canvasの初期化処理を実行し、リスク管理テーブル用ラベルのセットアップもここでおこないます。

//+------------------------------------------------------------------+
//| Initialize indicator                                             |
//+------------------------------------------------------------------+
int OnInit() {
   IndicatorSetString(INDICATOR_SHORTNAME, "Smart WaveTrend Crossover"); //--- Set short name

   PlotIndexSetInteger(1, PLOT_ARROW, 233);                              //--- Set buy arrow symbol
   PlotIndexSetInteger(1, PLOT_SHOW_DATA, signal_type == Triangles);     //--- Set buy visibility
   PlotIndexSetInteger(1, PLOT_LINE_COLOR, 0, signal_buy_col);           //--- Set buy color

   PlotIndexSetInteger(2, PLOT_ARROW, 234);                              //--- Set sell arrow symbol
   PlotIndexSetInteger(2, PLOT_SHOW_DATA, signal_type == Triangles);     //--- Set sell visibility
   PlotIndexSetInteger(2, PLOT_LINE_COLOR, 0, signal_sell_col);          //--- Set sell color

   SetIndexBuffer(23, avg_candle_size, INDICATOR_CALCULATIONS);          //--- Bind avg candle size

   currentChartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);   //--- Get chart width
   currentChartHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Get chart height

   string canvas_name = "SWTC_Canvas";                                   //--- Set canvas name
   if (!obj_Canvas.CreateBitmapLabel(0, 0, canvas_name, 0, 0, currentChartWidth, currentChartHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create canvas
      Print("Failed to create canvas");                                  //--- Log failure
      return(INIT_FAILED);                                               //--- Return failure
   }

   tpSlTableObjects[0] = objPrefix + "Table_Frame";                      //--- Set table frame
   tpSlTableObjects[1] = objPrefix + "Table_Level";                      //--- Set table level
   tpSlTableObjects[2] = objPrefix + "Table_Price";                      //--- Set table price
   tpSlTableObjects[3] = objPrefix + "Table_TP1";                        //--- Set TP1 label
   tpSlTableObjects[4] = objPrefix + "Table_TP1_Price";                  //--- Set TP1 price
   tpSlTableObjects[5] = objPrefix + "Table_TP2";                        //--- Set TP2 label
   tpSlTableObjects[6] = objPrefix + "Table_TP2_Price";                  //--- Set TP2 price
   tpSlTableObjects[7] = objPrefix + "Table_TP3";                        //--- Set TP3 label
   tpSlTableObjects[8] = objPrefix + "Table_TP3_Price";                  //--- Set TP3 price
   tpSlTableObjects[9] = objPrefix + "Table_SL";                         //--- Set SL label
   tpSlTableObjects[10] = objPrefix + "Table_SL_Price";                  //--- Set SL price

   current_font_size = 10;                                               //--- Initialize font size

   return(INIT_SUCCEEDED);                                               //--- Return success
}

OnInitイベントハンドラでは、インジケーターの追加プロパティを設定します。まずIndicatorSetStringを使用して短縮名を設定しますが、ここでは一貫性を持たせるために名称を変更しています。買いシグナルのプロットについては、これまで通りPlotIndexSetIntegerを用いて矢印コード233を設定します。ただし、シグナルタイプがTrianglesの場合はCanvas側でシグナルバブルを描画するため、このプロットは表示と非表示を切り替える形で制御し、ユーザー定義の買いシグナルカラーを適用します。同様に売りシグナルでは矢印コード234を設定し、同様の条件で表示制御をおこない、売り用カラーを適用します。また、平均ローソク足サイズ用バッファをインデックス23にバインドし、フォグおよびボックス拡張の計算に利用できるようにします。

Canvasを適切に初期化するために、ChartGetIntegerを使用して現在のチャートの寸法(幅と高さ)を取得します。Canvasオブジェクトに対して、Canvas名を定義し、サブウィンドウ、位置、サイズ、およびカラーフォーマットCOLOR_FORMAT_ARGB_NORMALIZEを指定して、その上にビットマップラベルを作成します。作成に失敗した場合は、Printを使用してエラーをログに記録し、INIT_FAILEDを返します。リスク管理表示用のテーブルオブジェクトについては、配列に対してプレフィックス付きの名前を割り当てて初期化します。結果として、フレーム本体、レベル表示用ラベル、価格表示用ラベル、そして各TPおよびSLに対応する個別要素を一元的に管理できるようになります。続いて、テキスト描画に使用するフォントサイズ変数を10で初期化します。最後に、設定が正常に完了したことを示すためINIT_SUCCEEDEDを返します。初期化後、次のような結果が表示されます。

初期化されたCanvas

画像から、Canvasが初期化され、描画の準備が整っていることがわかります。次におこなう必要があるのはCanvasオブジェクトの描画ですが、描画処理をより簡単で分かりやすくするために、ボックスと取引レベルの適切な価格を描画するためのヘルパー関数を定義し、それらを用いてテーブルに値を反映していきます。

//+------------------------------------------------------------------+
//| Draw right price label                                           |
//+------------------------------------------------------------------+
bool drawRightPrice(string objectName, datetime lineTime, double linePrice, color lineColor, ENUM_LINE_STYLE lineStyle = STYLE_SOLID, int lineWidth = 1) {
   bool objectExists = (ObjectFind(0, objectName) >= 0);           //--- Check if object exists
   if (!objectExists) {                                            //--- Handle new object
      if (!ObjectCreate(0, objectName, OBJ_ARROW_RIGHT_PRICE, 0, lineTime, linePrice)) { //--- Create right price
         Print("Failed to create ", objectName);                   //--- Log failure
         return false;                                             //--- Return failure
      }
   } else {                                                        //--- Handle existing object
      ObjectSetInteger(0, objectName, OBJPROP_TIME, 0, lineTime);  //--- Set time
      ObjectSetDouble(0, objectName, OBJPROP_PRICE, 0, linePrice); //--- Set price
   }

   ObjectSetInteger(0, objectName, OBJPROP_COLOR, lineColor);      //--- Set color
   ObjectSetInteger(0, objectName, OBJPROP_WIDTH, lineWidth);      //--- Set width
   ObjectSetInteger(0, objectName, OBJPROP_STYLE, lineStyle);      //--- Set style
   ObjectSetInteger(0, objectName, OBJPROP_FONTSIZE, 10);          //--- Set font size
   ObjectSetString(0, objectName, OBJPROP_FONT, "Arial");          //--- Set font
   ObjectSetInteger(0, objectName, OBJPROP_BACK, false);           //--- Disable background
   ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false);     //--- Disable selectable
   ObjectSetInteger(0, objectName, OBJPROP_SELECTED, false);       //--- Disable selected
   ChartRedraw(0);                                                 //--- Redraw chart
   return true;                                                    //--- Return success
}

//+------------------------------------------------------------------+
//| Update font sizes                                                |
//+------------------------------------------------------------------+
void UpdateFontSizes() {
   long scale = 0;                                                 //--- Initialize scale
   if (ChartGetInteger(0, CHART_SCALE, 0, scale)) {                //--- Get chart scale
      current_font_size = (int)(8 + scale * 1.5);                  //--- Compute font size
      current_font_size = MathMax(8, MathMin(18, current_font_size)); //--- Clamp font size
      ChartRedraw(0);                                              //--- Redraw chart
   }
}

//+------------------------------------------------------------------+
//| Convert bar width                                                |
//+------------------------------------------------------------------+
int BarWidth(int chartScale) {
   return (int)MathPow(2.0, chartScale);                           //--- Return bar width
}

//+------------------------------------------------------------------+
//| Convert shift to x                                               |
//+------------------------------------------------------------------+
int ShiftToX(int bar_index) {
   return (firstVisibleBarIndex - bar_index) * BarWidth(currentChartScale); //--- Return x position
}

//+------------------------------------------------------------------+
//| Convert price to y                                               |
//+------------------------------------------------------------------+
int PriceToY(double price) {
   if (maxPrice - minPrice == 0.0) return 0;                       //--- Handle zero range
   return (int)MathRound(currentChartHeight * (maxPrice - price) / (maxPrice - minPrice)); //--- Return y position
}

//+------------------------------------------------------------------+
//| Draw box on canvas                                               |
//+------------------------------------------------------------------+
void DrawBoxOnCanvas(int x_left, int y_top, int x_right, int y_bottom, color fillColor, int fillTransp) {
   int x1 = MathMin(x_left, x_right);                              //--- Set min x
   int x2 = MathMax(x_left, x_right);                              //--- Set max x
   int y1 = MathMin(y_top, y_bottom);                              //--- Set min y
   int y2 = MathMax(y_top, y_bottom);                              //--- Set max y

   uchar alpha_fill = (uchar)(255 * (100 - fillTransp) / 100);     //--- Compute fill alpha
   uint argb_fill = ColorToARGB(fillColor, alpha_fill);            //--- Get fill ARGB
   obj_Canvas.FillRectangle(x1, y1, x2, y2, argb_fill);            //--- Fill rectangle

   color borderColor = DarkenColor(fillColor, 0.7);                //--- Get border color
   uint argb_border = ColorToARGB(borderColor, 255);               //--- Get border ARGB

   obj_Canvas.LineAA(x1, y1, x2, y1, argb_border);                 //--- Draw top border
   obj_Canvas.LineAA(x1, y1 + 1, x2, y1 + 1, argb_border);         //--- Draw top inner

   obj_Canvas.LineAA(x1, y2, x2, y2, argb_border);                 //--- Draw bottom border
   obj_Canvas.LineAA(x1, y2 - 1, x2, y2 - 1, argb_border);         //--- Draw bottom inner

   obj_Canvas.LineAA(x1, y1, x1, y2, argb_border);                 //--- Draw left border
   obj_Canvas.LineAA(x1 + 1, y1, x1 + 1, y2, argb_border);         //--- Draw left inner

   obj_Canvas.LineAA(x2, y1, x2, y2, argb_border);                 //--- Draw right border
   obj_Canvas.LineAA(x2 - 1, y1, x2 - 1, y2, argb_border);         //--- Draw right inner
}

まず、drawRightPrice関数を作成し、チャート上に右寄せの価格ラベルを描画または更新します。ObjectFindでオブジェクトの存在を確認し、存在しない場合はObjectCreateOBJ_ARROW_RIGHT_PRICE型として作成します。作成に失敗した場合はPrintでエラーを出力しfalseを返します。既存オブジェクトの場合は時間と価格プロパティを調整し、その後、色、幅、スタイル(デフォルトはSTYLE_SOLID)、フォントサイズ10、フォントArialを設定し、背景、選択可否、選択状態を無効化したうえでChartRedrawにより再描画し、trueを返します。次に、UpdateFontSizes関数を定義し、テキストサイズを動的に調整します。ChartGetIntegerでチャートスケールを取得し、フォントサイズを8 + 1.5 × スケールとして計算し、MathMaxMathMinで8から18の範囲に制限し、チャートを再描画します。

BarWidth関数では、チャートスケールに基づくバーのピクセル幅を計算し、MathPowを用いて2のスケール乗を求めて整数として返します。ShiftToX関数は、最初の可視バーからの差分にバー幅を掛けることで、バーインデックスをCanvas上のx座標に変換します。同様にPriceToY関数では価格をy座標に変換し、価格レンジが0の場合は0を返し、それ以外では最大値から最小値への比率をMathRoundで算出し、チャート高さでスケーリングします。

最後に、DrawBoxOnCanvas関数を実装し、Canvas上に塗りつぶし矩形と枠線を描画します。MathMinとMathMaxで最小と最大座標を決定し、透明度からアルファ値を算出し、ColorToARGBで色を変換してFillRectangleで塗りつぶします。その後、DarkenColorでより暗い枠線色を生成し、LineAAを用いて上下左右にアンチエイリアス付きの外枠および内枠ラインを描画します。AAはアンチエイリアスを意味し、線のギザギザを滑らかにするために使用しています。より理解を深めるために、以下の画像をご覧ください。

アンチエイリアシング(AA)比較

画像から分かるように、アンチエイリアスを採用した理由は線を滑らかにするためです。これで、これらの関数をインジケーターの計算およびビジュアル描画で使用していくことができます。まず、テーブル化された取引レベルをチャート上に維持したいため、新しいシグナルで明示的に変更されない限り、呼び出し間でそれらを静的に保持する必要があります。

//+------------------------------------------------------------------+
//| Calculate indicator values                                       |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[]) {
   static int last_dir = 0;                      //--- Last direction
   static double last_sl = 0.0;                  //--- Last SL
   static double last_tp1 = 0.0;                 //--- Last TP1
   static double last_tp2 = 0.0;                 //--- Last TP2
   static double last_tp3 = 0.0;                 //--- Last TP3
   static datetime last_signal_time = 0;         //--- Last signal time

   if (prev_calculated == 0) {                   //--- Handle initial calc

      //--- Existing initializations

      ArrayInitialize(avg_candle_size, EMPTY_VALUE); //--- Init avg candle
      ArrayInitialize(buyArrowBuf, EMPTY_VALUE); //--- Init buy arrows
      ArrayInitialize(sellArrowBuf, EMPTY_VALUE); //--- Init sell arrows

      ArrayResize(all_boxes, 0);                 //--- Clear boxes
      ArrayResize(all_signals, 0);               //--- Clear signals

      last_dir = 0;                              //--- Reset direction
      last_sl = 0.0;                             //--- Reset SL
      last_tp1 = 0.0;                            //--- Reset TP1
      last_tp2 = 0.0;                            //--- Reset TP2
      last_tp3 = 0.0;                            //--- Reset TP3
      last_signal_time = 0;                      //--- Reset signal time
   }
}

OnCalculateイベントハンドラ内では、再計算間で状態を保持するためにstatic変数を宣言します。これには、直近のシグナル方向を保持する整数、最新のSLおよび3つのTPレベルを保持するdouble、そして最後のシグナル時刻を保持するdatetimeが含まれ、リスク管理表示の連続性を確保します。prev_calculatedが0の場合、すなわち初回計算または完全リセット時には、初期化処理を拡張します。ArrayInitializeを用いて平均ローソク足サイズバッファおよび矢印バッファをEMPTY_VALUEに設定し、ArrayResizeでボックス配列およびシグナル配列をサイズ0にして過去データをクリアし、すべてのstatic変数を0や0.0といった初期値にリセットしてクリーンな状態から開始します。Canvasの再描画を必要な場合のみに限定するため、以下のように再描画フラグを追加します。

bool new_signal_redraw = false;               //--- New redraw flag

計算ループ内では、ボラティリティを扱うために平均ローソク足サイズの計算ロジックを組み込みます。

double sum_co = 0;                         //--- Init sum
int cnt_co = 0;                            //--- Init count
for (int k = 0; k < 50; k++) {             //--- Loop candles
   if (i - k < 0) break;                   //--- Skip invalid
   sum_co += MathAbs(close[i - k] - open[i - k]); //--- Accumulate
   cnt_co++;                               //--- Increment
}
if (cnt_co > 0) avg_candle_size[i] = sum_co / cnt_co; //--- Set average
else avg_candle_size[i] = MathAbs(close[i] - open[i]); //--- Set default

ループ内では、まずsum用のdouble変数を0で初期化し、count用の整数も0で初期化します。その後、現在のインデックスから過去方向へ最大50本のバーをループし、有効なバーに対してはcloseとopenの差の絶対値をMathAbsで算出してsumに加算し、countをインクリメントします。インデックスが負になる場合は途中でループを終了します。countが正の場合は、その合計値をcountで割ることで現在バーの平均ローソク足サイズを設定し、そうでない場合は現在バー単体の実体サイズをデフォルト値として設定します。

さらに、従来の矢印配置ロジックは、クロス時に即座に矢印を出す方式から、ボックス生成およびイベント駆動ロジックへ置き換える必要があります。具体的には、クロス発生時にレンジボックスを生成し、ブレイクアウトによってシグナルを確定させ、配列サイズを制限しつつ、条件に応じて矢印またはラベルを表示す構成にします。これにより、ブレイクアウトまで保持されるレンジボックスを導入し、方向性に基づいてフィルタリングしながら、複数の表示形式に対応した、よりコンテキストを持つ売買シグナルを実現します。

//--- BEFORE

// buyArrowBuf[i] = EMPTY_VALUE;              //--- Reset buy arrow
// sellArrowBuf[i] = EMPTY_VALUE;             //--- Reset sell arrow
// if (signal_bull_cross[i] == 1 && (!use_trend_filter || trend_is_bull[i] == 1)) { //--- Check buy condition
//    buyArrowBuf[i] = low[i] - _Point * base_offset; //--- Place buy arrow
// }
// if (signal_bear_cross[i] == 1 && (!use_trend_filter || trend_is_bear[i] == 1)) { //--- Check sell condition
//    sellArrowBuf[i] = high[i] + _Point * base_offset; //--- Place sell arrow
// }

//--- AFTER

double box_top = use_box_multiplier ? high[i] + avg_candle_size[i] * box_multiplier : high[i]; //--- Set top
double box_bottom = use_box_multiplier ? low[i] - avg_candle_size[i] * box_multiplier : low[i]; //--- Set bottom

if (signal_bull_cross[i] == 1 && (!use_trend_filter || trend_is_bull[i] == 1)) { //--- Check bull signal
   BoxInfo b;                              //--- Create box
   b.left_time = time[i];                  //--- Set left
   b.right_time = 0;                       //--- Set right
   b.top = box_top;                        //--- Set top
   b.bottom = box_bottom;                  //--- Set bottom
   b.dir = 1;                              //--- Set dir
   ArrayResize(all_boxes, ArraySize(all_boxes) + 1); //--- Resize boxes
   all_boxes[ArraySize(all_boxes) - 1] = b; //--- Add box
}

if (signal_bear_cross[i] == 1 && (!use_trend_filter || trend_is_bear[i] == 1)) { //--- Check bear signal
   BoxInfo b;                              //--- Create box
   b.left_time = time[i];                  //--- Set left
   b.right_time = 0;                       //--- Set right
   b.top = box_top;                        //--- Set top
   b.bottom = box_bottom;                  //--- Set bottom
   b.dir = -1;                             //--- Set dir
   ArrayResize(all_boxes, ArraySize(all_boxes) + 1); //--- Resize boxes
   all_boxes[ArraySize(all_boxes) - 1] = b; //--- Add box
}

bool buy_event = false;                    //--- Buy event flag
bool sell_event = false;                   //--- Sell event flag

for (int j = ArraySize(all_boxes) - 1; j >= 0; j--) { //--- Loop boxes
   if (all_boxes[j].right_time == 0) {     //--- Check active
      if (close[i] > all_boxes[j].top) {   //--- Check break up
         if (!show_only_matching || all_boxes[j].dir == 1) buy_event = true; //--- Set buy
         all_boxes[j].right_time = time[i]; //--- Close box
      }
      if (close[i] < all_boxes[j].bottom) { //--- Check break down
         if (!show_only_matching || all_boxes[j].dir == -1) sell_event = true; //--- Set sell
         all_boxes[j].right_time = time[i]; //--- Close box
      }
   }
}

while (ArraySize(all_boxes) > 500) {       //--- Limit boxes
   bool removed = false;                   //--- Removed flag
   for (int j = 0; j < ArraySize(all_boxes); j++) { //--- Loop to remove
      if (all_boxes[j].right_time != 0) {  //--- Check closed
         ArrayRemove(all_boxes, j, 1);     //--- Remove box
         removed = true;                   //--- Set removed
         break;                            //--- Exit loop
      }
   }
   if (!removed) break;                    //--- No more to remove
}

if (signal_type == Triangles) {            //--- Check triangles
   if (buy_event) {                        //--- Handle buy
      buyArrowBuf[i] = low[i] - _Point * base_offset; //--- Set arrow
   }
   if (sell_event) {                       //--- Handle sell
      sellArrowBuf[i] = high[i] + _Point * base_offset; //--- Set arrow
   }
} else {                                   //--- Handle labels
   if (buy_event) {                        //--- Handle buy
      SignalInfo s;                        //--- Create signal
      s.time = time[i];                    //--- Set time
      s.dir = 1;                           //--- Set dir
      ArrayResize(all_signals, ArraySize(all_signals) + 1); //--- Resize signals
      all_signals[ArraySize(all_signals) - 1] = s; //--- Add signal
      new_signal_redraw = (i == rates_total - 1); //--- Set redraw
   }
   if (sell_event) {                       //--- Handle sell
      SignalInfo s;                        //--- Create signal
      s.time = time[i];                    //--- Set time
      s.dir = -1;                          //--- Set dir
      ArrayResize(all_signals, ArraySize(all_signals) + 1); //--- Resize signals
      all_signals[ArraySize(all_signals) - 1] = s; //--- Add signal
      new_signal_redraw = (i == rates_total - 1); //--- Set redraw
   }
}

while (ArraySize(all_signals) > 500) {     //--- Limit signals
   ArrayRemove(all_signals, 0, 1);         //--- Remove oldest
}

シグナルボックスの上限と下限は、基本的にバーの高値と安値を使用して決定しますが、ボックス拡張が有効な場合は、平均ローソク足サイズにボックス倍率を掛けた値を加減することで、価格レンジにバッファを持たせます。強気クロスオーバーが検出され、かつトレンドフィルタ条件を満たしている場合は、BoxInfo構造体をインスタンス化し、leftに現在時刻、rightに0(アクティブ状態を示す)、topとbottomに計算された価格、directionに1(強気)を設定します。その後、ArrayResize とArraySizeを使ってall_boxes配列のサイズを1つ拡張し、新しい構造体を末尾に追加します。弱気クロスオーバーの場合も同様に、directionを-1(弱気)として設定し、配列に追加します。

次に、買いイベントおよび売りイベント用のブールフラグをfalseで初期化し、all_boxes配列を末尾から逆順に走査します。各ボックスについて、rightが0のアクティブ状態であれば、現在の終値がtopを上回った場合に上方向ブレイクアウトと判定し、方向が一致しているか、または一致条件が無効であれば買いイベントフラグをtrueに設定し、rightに現在時刻を代入してボックスをクローズします。同様に、終値がbottomを下回った場合は下方向ブレイクアウトとして売りイベントを設定し、ボックスをクローズします。配列サイズの管理として、all_boxesが500要素を超えた場合は、先頭から走査して最初に見つかったクローズ済みボックスをArrayRemoveで削除します。削除に成功した場合はフラグを立ててループを抜け、削除できる要素がない場合は処理を終了します。

シグナルタイプがTrianglesの場合、買いイベント時には安値の下にoffset × _Point分だけずらした位置に買い矢印を配置し、売りイベント時には高値の上に同様に配置します。それ以外のラベルタイプの場合、買いイベント時にはSignalInfo構造体を生成し、timeに現在時刻、directionに1を設定してall_signals配列に追加し、最後のバーであれば再描画フラグを立てます。売りイベントについてもdirectionを-1として同様に処理します。最後に、all_signalsが500要素を超えた場合は、ArrayRemoveを用いて先頭の最も古い要素を削除します。これでCanvas描画の準備が整いました。まずはフォグ描画から開始し、処理のモジュール化のために専用関数としてロジックを実装していきます。

//+------------------------------------------------------------------+
//| Redraw canvas                                                    |
//+------------------------------------------------------------------+
void Redraw(int rates_total) {
   if (currentChartWidth <= 0 || currentChartHeight <= 0) return; //--- Handle invalid size

   double h[], l[], c[], acs[], th[];            //--- Declare arrays
   datetime t[];                                 //--- Declare time

   if (CopyHigh(_Symbol, _Period, 0, rates_total, h) != rates_total) return;  //--- Copy high
   if (CopyLow(_Symbol, _Period, 0, rates_total, l) != rates_total) return;   //--- Copy low
   if (CopyClose(_Symbol, _Period, 0, rates_total, c) != rates_total) return; //--- Copy close
   if (CopyTime(_Symbol, _Period, 0, rates_total, t) != rates_total) return;  //--- Copy time

   ArrayCopy(acs, avg_candle_size, 0, 0, rates_total); //--- Copy avg size
   ArrayCopy(th, trend_hist, 0, 0, rates_total); //--- Copy hist

   uint default_color = 0;                       //--- Default color
   obj_Canvas.Erase(default_color);              //--- Erase canvas

   current_font_size = (int)(10 + currentChartScale * 1.5);         //--- Compute font
   current_font_size = MathMax(10, MathMin(24, current_font_size)); //--- Clamp font

   if (show_fog) {                               //--- Check fog
      int total = visibleBarsCount;              //--- Set total
      int previousX = -1;                        //--- Prev x
      double previous_hl2 = 0.0;                 //--- Prev hl2
      double previous_offset = 0.0;              //--- Prev offset
      int previous_dir = 0;                      //--- Prev dir
      color previous_fog_color = clrNONE;        //--- Prev color

      for (int i = 0; i < total; i++) {                         //--- Loop visible
         int bar_index = firstVisibleBarIndex - i;              //--- Compute index
         if (bar_index < 0 || bar_index >= rates_total) continue; //--- Skip invalid

         int x = ShiftToX(bar_index);                           //--- Get x
         if (x >= currentChartWidth) continue;                  //--- Skip offscreen

         int buffer_index = rates_total - 1 - bar_index;        //--- Compute buffer
         double hl2 = (h[buffer_index] + l[buffer_index]) / 2.0; //--- Compute hl2
         double offset_val = acs[buffer_index] * offset_mult;   //--- Compute offset
         int dir = th[buffer_index] >= 0 ? -1 : 1;              //--- Set dir
         color fog_color = th[buffer_index] >= 0 ? col_up : col_dn; //--- Set color

         if (previousX != -1 && x > previousX) {                //--- Check previous
            double deltaX = x - previousX;                      //--- Compute delta
            int endColumn = MathMin(x, currentChartWidth - 1);  //--- Set end

            for (int column = previousX + 1; column <= endColumn; column++) { //--- Loop columns
               double t_val = (column - previousX) / deltaX;     //--- Compute t
               double interp_hl2 = previous_hl2 + t_val * (hl2 - previous_hl2); //--- Interp hl2
               double interp_offset = previous_offset + t_val * (offset_val - previous_offset); //--- Interp offset
               int interp_dir = previous_dir;                    //--- Interp dir
               color interp_fog_color = previous_fog_color;      //--- Interp color

               double full_offset = 6.0 * interp_offset;         //--- Full offset
               double edge_price = interp_hl2 + interp_dir * full_offset; //--- Edge price

               int slow_y = PriceToY(interp_hl2);                //--- Slow y
               int fast_y = PriceToY(edge_price);                //--- Fast y

               int upperY = MathMin(slow_y, fast_y);             //--- Upper y
               int lowerY = MathMax(slow_y, fast_y);             //--- Lower y
               upperY = MathMax(0, upperY);                      //--- Clamp upper
               lowerY = MathMin(currentChartHeight - 1, lowerY); //--- Clamp lower

               double height_pixels = MathAbs(slow_y - fast_y);  //--- Height
               if (height_pixels == 0.0) continue;               //--- Skip zero

               double total_inc = transp_inc * 6.0;              //--- Total inc

               for (int row = upperY; row <= lowerY; row++) {                 //--- Loop rows
                  double distanceFromSlow_pixels = MathAbs(row - slow_y);     //--- Distance
                  double gradientFraction = distanceFromSlow_pixels / height_pixels; //--- Fraction
                  double transp = base_transp + total_inc * gradientFraction; //--- Transp
                  if (transp > 100.0) transp = 100.0;                         //--- Clamp transp

                  uchar alpha = (uchar)(255 * (100 - transp) / 100.0);        //--- Alpha
                  uint argb = ColorToARGB(interp_fog_color, alpha);           //--- ARGB
                  obj_Canvas.PixelSet(column, row, argb);                     //--- Set pixel
               }
            }
         }

         previousX = x;                             //--- Update prev x
         previous_hl2 = hl2;                        //--- Update prev hl2
         previous_offset = offset_val;              //--- Update prev offset
         previous_dir = dir;                        //--- Update prev dir
         previous_fog_color = fog_color;            //--- Update prev color
      }
   }

   obj_Canvas.Update();                          //--- Update canvas
}

Redraw関数では、まず現在のチャート幅または高さが正の値であるかを確認し、いずれかが0または負の場合は無効な処理を避けるために早期リターンします。次に、高値、安値、終値、平均ローソク足サイズ、トレンドヒストグラム、時間のローカル配列を宣言し、CopyHigh、CopyLow、CopyClose、CopyTimeを使用して先頭から全バー分のデータをコピーし、いずれかが期待件数と一致しない場合は終了します。平均ローソク足サイズおよびトレンドヒストグラムについては、ArrayCopyを用いてバッファからローカル配列へ転送し、描画に使用します。描画前にEraseメソッドでCanvasを0のデフォルト色でクリアし、再描画に備えます。フォントサイズは10 + 1.5 × チャートスケールで計算し、MathMaxMathMinで10から24の範囲に制限します。

フォグ表示が有効な場合、ループ回数を可視バー数に設定し、前回のx座標、hl2価格、オフセット、方向、色の追跡用変数を初期化します。左から右へ各可視バーを処理し、バーインデックスを最初の可視バーからループカウンタを引いて求め、範囲外や無効インデックスはスキップします。ShiftToXでx座標を取得し、チャート幅を超える場合はスキップします。バッファインデックスは総バー数から1を引き、さらにバーインデックスを引いて求め、hl2は高値と安値の中間値、オフセットは平均ローソク足サイズに倍率を掛けて算出し、方向はトレンドヒストグラムが0以上なら-1、それ以外は1とします。フォグ色はヒストグラムの符号に応じてユーザー定義の上昇色または下降色を使用します。

前回のxが存在し、かつ現在のxがそれより大きい場合、ピクセル差分を求め、終了列を現在のxまたはチャート幅-1の小さい方とします。その範囲の各列について、差分に対する相対位置から補間係数tを求め、hl2とオフセットを前回値と現在値の間で線形補間し、方向と色は前回値を使用します。補間オフセットに6を掛けた値をフルオフセットとし、方向に応じてhl2に加減してエッジ価格を算出します。PriceToYでhl2とエッジをy座標へ変換し、上端と下端をそれぞれ最小と最大として求め、MathMaxとMathMinで0からチャート高さ-1の範囲に制限します。y差が0の場合はスキップし、透明度増分に6を掛けた値を総増分とします。

上端から下端まで各行について、hl2位置からのピクセル距離を求め、高さに対する比率としてグラデーション係数を算出します。透明度は基準値に総増分と係数を掛けて加算し、最大100に制限します。アルファ値は255 × (100 - 透明度) / 100としてucharに変換し、ColorToARGBで色とアルファを合成します。PixelSetで該当列と行のピクセルを設定します。各バー処理後に前回値を現在値で更新します。最後にUpdateでCanvas表示を更新し、描画内容を反映します。進行状況を明確にするため、この関数は計算イベントハンドラ内の主要な更新処理の後に呼び出し、新しい情報に基づいて再描画します。

bool hasChartChanged = false;                                         //--- Change flag
int newChartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);   //--- Get new width
int newChartHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Get new height
int newChartScale = (int)ChartGetInteger(0, CHART_SCALE);             //--- Get new scale
int newFirstVisibleBar = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR); //--- Get new first bar
int newVisibleBars = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);     //--- Get new visible
double newMinPrice = ChartGetDouble(0, CHART_PRICE_MIN, 0);           //--- Get new min
double newMaxPrice = ChartGetDouble(0, CHART_PRICE_MAX, 0);           //--- Get new max

if (newChartWidth != currentChartWidth || newChartHeight != currentChartHeight) { //--- Check size change
   obj_Canvas.Resize(newChartWidth, newChartHeight);                  //--- Resize canvas
   currentChartWidth = newChartWidth;          //--- Update width
   currentChartHeight = newChartHeight;        //--- Update height
   hasChartChanged = true;                     //--- Set changed
}

if (newChartScale != currentChartScale || newFirstVisibleBar != firstVisibleBarIndex || newVisibleBars != visibleBarsCount ||
    newMinPrice != minPrice || newMaxPrice != maxPrice) { //--- Check other changes
   currentChartScale = newChartScale;          //--- Update scale
   firstVisibleBarIndex = newFirstVisibleBar;  //--- Update first bar
   visibleBarsCount = newVisibleBars;          //--- Update visible
   minPrice = newMinPrice;                     //--- Update min
   maxPrice = newMaxPrice;                     //--- Update max
   hasChartChanged = true;                     //--- Set changed
}

datetime currentTime = TimeCurrent();         //--- Get current time
if (hasChartChanged || rates_total > prev_calculated || new_signal_redraw) { //--- Check redraw
   Redraw(rates_total);                        //--- Call redraw
   lastRedrawTime = currentTime;               //--- Update time
}

ChartRedraw(0);                               //--- Redraw chart

OnCalculateイベントハンドラでは、まずチャートの変更を追跡するためのブールフラグを初期化します。その後、ChartGetIntegerを使用して更新後のチャートプロパティを取得します。具体的には、CHART_WIDTH_IN_PIXELS とCHART_HEIGHT_IN_PIXELSで新しい幅と高さ、CHART_SCALEでスケール、CHART_FIRST_VISIBLE_BARで最初の可視バー、CHART_VISIBLE_BARSで可視バー数を取得します。また、ChartGetDoubleを用いてCHART_PRICE_MINおよびCHART_PRICE_MAXから最小価格と最大価格を取得します。新しい幅または高さが現在の値と異なる場合、新しい寸法を渡してResizeメソッドを使用してCanvasオブジェクトのサイズを変更し、グローバルな幅と高さの変数を更新し、変更フラグをtrueに設定します。

同様に、スケール、最初の可視バー、可視バー数、最小価格、最大価格のいずれかが変更された場合も、対応するグローバル変数を更新し、フラグをtrueに設定します。次に、TimeCurrentで現在のサーバー時刻を取得し、フラグがtrueであるか、rates_totalとprev_calculatedを比較して新しいバーが追加された場合、または新しいシグナルにより再描画が必要な場合のいずれかに該当すれば、Redraw関数をrates_totalとともに呼び出し、最後の再描画タイムスタンプを更新します。最後に、ChartRedrawを呼び出してチャートを強制的に更新し、すべての変更が画面に反映されるようにします。チャートに適用すると、以下のような結果が表示されます。

フォグの再描画

画像を見ると、ローソク足の範囲に基づいてフォグが正しく設定されていることが分かります。次にボックスを描画する必要があります。この処理は同じ再描画関数の中に組み込み、条件に関係なく常に実行する形にします。

for (int j = 0; j < ArraySize(all_boxes); j++) { //--- Loop boxes
   int left_bar = iBarShift(_Symbol, _Period, all_boxes[j].left_time); //--- Get left bar
   int x_left = ShiftToX(left_bar);              //--- Get left x
   int x_right;                                  //--- Declare right x

   if (all_boxes[j].right_time == 0) {           //--- Check active
      x_right = currentChartWidth - 1;           //--- Set to end
   } else {                                      //--- Handle closed
      int right_bar = iBarShift(_Symbol, _Period, all_boxes[j].right_time); //--- Get right bar
      x_right = ShiftToX(right_bar);             //--- Get right x
   }

   int y_top = PriceToY(all_boxes[j].top);       //--- Get top y
   int y_bottom = PriceToY(all_boxes[j].bottom); //--- Get bottom y

   color fill_col = all_boxes[j].dir == 1 ? box_bull_fill : box_bear_fill;       //--- Set fill

   DrawBoxOnCanvas(x_left, y_top, x_right, y_bottom, fill_col, box_fill_transp); //--- Draw box
}

ボックスを描画するために、まずall_boxes配列をループします。配列の総数はArraySizeを使用して取得し、インデックス0から末尾まで処理をおこないます。各ボックスについて、まず左側のバーインデックスを取得します。iBarShiftに銘柄、時間足、ボックスの左時間を渡し、タイムスタンプをバー位置に変換します。その後、ShiftToXを用いてCanvas上の左側X座標を計算します。

次に右側のX座標用の変数を用意します。もしボックスの右時間が0であれば(つまりまだ有効な状態であれば)、右端まで伸ばすために右Xをチャート幅から1引いた値に設定します。一方、ボックスがクローズされている場合は、同様にiBarShiftを使って右側のバーインデックスを取得し、ShiftToXでX座標に変換します。さらに、ボックスの上限価格と下限価格をPriceToYを使ってY座標に変換します。方向に応じて塗りつぶし色を選択し、方向が1の場合は強気の色、-1の場合は弱気の色を使用します。最後に、計算されたXとYの座標、選択した色、および透明度を引数としてDrawBoxOnCanvasを呼び出し、Canvas上にボックスを描画します。次のような結果が得られます。

Canvasボックス

ボックスが正しく描画されていることが確認できます。次に残る最も重要な部分は、選択された場合にのみシグナルバブルを条件付きで描画する処理です。これを実現するために、次のロジックを使用します。

if (signal_type == Labels_Buy_Sell) {                             //--- Check labels
   for (int j = 0; j < ArraySize(all_signals); j++) {             //--- Loop signals
      int bar = iBarShift(_Symbol, _Period, all_signals[j].time); //--- Get bar
      if (bar > firstVisibleBarIndex || bar < firstVisibleBarIndex - visibleBarsCount) continue; //--- Skip off view

      int x = ShiftToX(bar);                                      //--- Get x
      int buffer_index = rates_total - 1 - bar;                   //--- Get buffer
      double price = (all_signals[j].dir == 1) ? l[buffer_index] : h[buffer_index]; //--- Set price
      int y = PriceToY(price);                                    //--- Get y

      string text = (all_signals[j].dir == 1) ? "BUY" : "SELL";   //--- Set text
      color bg_col = (all_signals[j].dir == 1) ? signal_buy_col : signal_sell_col; //--- Set bg
      color border_col = DarkenColor(bg_col, 0.5);                //--- Set border
      color text_col = clrWhite;                                  //--- Set text color

      obj_Canvas.FontSet("Arial Bold", (uint)current_font_size, FW_BOLD); //--- Set font
      int text_width = obj_Canvas.TextWidth(text);                //--- Get width
      int text_height = obj_Canvas.TextHeight(text);              //--- Get height

      int padding_width = 6 + current_font_size / 3;              //--- Compute width pad
      int padding_height = 4 + current_font_size / 4;             //--- Compute height pad

      int rect_width = text_width + padding_width;                //--- Set rect width
      int rect_height = text_height + padding_height;             //--- Set rect height
      int label_offset = base_offset + currentChartScale;         //--- Set offset
      int tri_base = 6 + currentChartScale * 2;                   //--- Set tri base
      tri_base = (tri_base / 2) * 2;                              //--- Ensure even
      int tri_height = (int)(tri_base * 0.5);                     //--- Set tri height

      int x_rect = x - rect_width / 2;                            //--- Set rect x
      int y_rect = (all_signals[j].dir == 1) ? y + label_offset : y - rect_height - label_offset; //--- Set rect y

      uchar alpha = (uchar)(255 * (100 - 20) / 100);              //--- Set alpha
      uint argb_fill = ColorToARGB(bg_col, alpha);                //--- Get fill
      obj_Canvas.FillRectangle(x_rect, y_rect, x_rect + rect_width, y_rect + rect_height, argb_fill); //--- Fill rect

      uint argb_border = ColorToARGB(border_col, 255);            //--- Get border
      obj_Canvas.LineAA(x_rect, y_rect, x_rect + rect_width, y_rect, argb_border); //--- Draw top
      obj_Canvas.LineAA(x_rect + rect_width, y_rect, x_rect + rect_width, y_rect + rect_height, argb_border); //--- Draw right
      obj_Canvas.LineAA(x_rect + rect_width, y_rect + rect_height, x_rect, y_rect + rect_height, argb_border); //--- Draw bottom
      obj_Canvas.LineAA(x_rect, y_rect + rect_height, x_rect, y_rect, argb_border); //--- Draw left

      int x_center = x_rect + rect_width / 2;     //--- Set center
      if (all_signals[j].dir == 1) {              //--- Handle buy
         int tri_left = x_center - tri_base / 2;  //--- Set left
         int tri_right = x_center + tri_base / 2; //--- Set right
         int tri_tip_y = y_rect - tri_height;     //--- Set tip
         obj_Canvas.FillTriangle(tri_left, y_rect, tri_right, y_rect, x_center, tri_tip_y, argb_fill); //--- Fill tri
         obj_Canvas.LineAA(tri_left, y_rect, x_center, tri_tip_y, argb_border); //--- Draw left slant
         obj_Canvas.LineAA(tri_right, y_rect, x_center, tri_tip_y, argb_border); //--- Draw right slant
      } else {                                    //--- Handle sell
         int tri_bottom_y = y_rect + rect_height; //--- Set bottom
         int tri_left = x_center - tri_base / 2;  //--- Set left
         int tri_right = x_center + tri_base / 2; //--- Set right
         int tri_tip_y = tri_bottom_y + tri_height; //--- Set tip
         obj_Canvas.FillTriangle(tri_left, tri_bottom_y, tri_right, tri_bottom_y, x_center, tri_tip_y, argb_fill); //--- Fill tri
         obj_Canvas.LineAA(tri_left, tri_bottom_y, x_center, tri_tip_y, argb_border); //--- Draw left slant
         obj_Canvas.LineAA(tri_right, tri_bottom_y, x_center, tri_tip_y, argb_border); //--- Draw right slant
      }

      int text_x = x_rect + rect_width / 2;   //--- Set text x
      int text_y = y_rect + rect_height / 2;  //--- Set text y
      uint argb_text = ColorToARGB(text_col, 255); //--- Get text ARGB
      obj_Canvas.TextOut(text_x, text_y, text, argb_text, TA_CENTER | TA_VCENTER); //--- Draw text
   }
}

まず、シグナルタイプがLabels_Buy_Sellであることを確認し、テキストバブルを描画する条件を満たしている場合のみ処理をおこないます。その後、all_signals配列をArraySizeを用いて総数分ループします。各シグナルについて、まずiBarShiftを使用して銘柄と時間足を渡し、タイムスタンプをバーインデックスに変換します。その際、バーが表示範囲外にある場合はスキップするため、最初の表示インデックスと表示バー数を比較して判定します。次にShiftToXを使ってX座標を計算し、バッファインデックスを総レート数 - 1 - バーとして導出します。参照価格は、買いシグナル(direction 1)の場合はロー価格、売りシグナルの場合はハイ価格とし、それをPriceToYでY座標に変換します。ラベルテキストは方向に応じてBUYまたはSELLとし、背景色はユーザー入力に基づいて選択します。枠線色はDarkenColorを使用して背景色を0.5の係数で暗くして生成し、テキスト色は白に設定します。

CanvasのフォントはFontSetを使ってArial Boldに設定し、現在のフォントサイズと太字フラグを適用します。TextWidthとTextHeightで文字サイズを測定し、フォントサイズに基づいてパディングを算出します。これによりテキストサイズにパディングを加えた矩形サイズを決定します。ラベルのオフセットは基本オフセットとチャートスケールを組み合わせて設定し、三角形の底辺は6 + スケール×2とし、整数除算と乗算により偶数化します。三角形の高さは底辺の半分を整数として設定します。矩形のX位置は、シグナルXから幅の半分を引くことで中央揃えにし、Y位置は方向に応じて上下にオフセットします。塗りつぶしアルファ値はuchar(255 × (100 - 20) / 100)として80%の透明度に設定し、ColorToARGBでARGB色を取得し、FillRectangleで矩形を塗りつぶします。境界線にはARGB値を取得し、長方形の上、右、下、左の辺にLineAAを使用してアンチエイリアス処理された線を描画します。

買いシグナルの場合、三角形は矩形の下側に配置し、左右の点は中心から底辺の半分だけオフセットします。頂点は高さ分だけ上に配置します。FillTriangleで塗りつぶし、その後LineAAで斜辺を描画します。この方法が採用された理由については既に理解している通りです。標準的な方法ではなく、この方式を選んでいる意図があります。売りシグナルの場合、三角形は矩形の上側ではなく下側を基準に配置され、同様に左右点を設定し、頂点は高さ分だけ下に配置されます。塗りつぶして、斜辺を描画します。最後に、テキストのXとYを矩形内で中央揃えにし、テキストカラーをARGBに変換した上で、TextOutを使用して水平と垂直中央揃えのフラグ付きで描画します。コンパイルすると、次の結果が得られます。

バブルシグナル

シグナルがバブルとして表示されていることが確認できます。次におこなうべきは、TPおよびSLのレベルを計算することです。

if (showTPSL && (buy_event || sell_event) && i == rates_total - 1) { //--- Check TP/SL
   int lastSignal = buy_event ? 1 : -1;     //--- Set signal
   double lastClose = close[i];             //--- Set close
   double sum_range = 0;                    //--- Init sum
   int cnt_range = 0;                       //--- Init count
   for (int k = 0; k < tp_sl_length; k++) { //--- Loop range
      if (i - k < 0) break;                 //--- Skip invalid
      sum_range += high[i - k] - low[i - k]; //--- Accumulate
      cnt_range++;                          //--- Increment
   }
   double avgRange = (cnt_range > 0) ? sum_range / cnt_range : 0; //--- Compute avg

   double sl_val = (lastSignal == 1) ? 
                   (tpSlMode == Candle_Multiplier ? lastClose - avgRange * slMultiplier : lastClose * (1 - slPercent / 100)) : 
                   (tpSlMode == Candle_Multiplier ? lastClose + avgRange * slMultiplier : lastClose * (1 + slPercent / 100)); //--- Compute SL

   double tp1_val = (lastSignal == 1) ? 
                    (tpSlMode == Candle_Multiplier ? lastClose + avgRange * tp1Multiplier : lastClose * (1 + tp1Percent / 100)) : 
                    (tpSlMode == Candle_Multiplier ? lastClose - avgRange * tp1Multiplier : lastClose * (1 - tp1Percent / 100)); //--- Compute TP1

   double tp2_val = (lastSignal == 1) ? 
                    (tpSlMode == Candle_Multiplier ? lastClose + avgRange * tp2Multiplier : lastClose * (1 + tp2Percent / 100)) : 
                    (tpSlMode == Candle_Multiplier ? lastClose - avgRange * tp2Multiplier : lastClose * (1 - tp2Percent / 100)); //--- Compute TP2

   double tp3_val = (lastSignal == 1) ? 
                    (tpSlMode == Candle_Multiplier ? lastClose + avgRange * tp3Multiplier : lastClose * (1 + tp3Percent / 100)) : 
                    (tpSlMode == Candle_Multiplier ? lastClose - avgRange * tp3Multiplier : lastClose * (1 - tp3Percent / 100)); //--- Compute TP3

   last_dir = lastSignal;                  //--- Update dir
   last_sl = sl_val;                       //--- Update SL
   last_tp1 = tp1_val;                     //--- Update TP1
   last_tp2 = tp2_val;                     //--- Update TP2
   last_tp3 = tp3_val;                     //--- Update TP3
   last_signal_time = time[i];             //--- Update time
   new_signal_redraw = true;               //--- Set redraw
}

if (i == rates_total - 1) {                //--- Check last bar
   static bool last_buy = false;           //--- Last buy
   static bool last_sell = false;          //--- Last sell

   if (buy_event && !last_buy) {           //--- Handle new buy
      Alert("WaveTrend BUY " + _Symbol + " @" + DoubleToString(close[i], _Digits)); //--- Alert buy
      last_buy = true;                     //--- Set last buy
      new_signal_redraw = true;            //--- Set redraw
   } else last_buy = buy_event;            //--- Update last buy

   if (sell_event && !last_sell) {         //--- Handle new sell
      Alert("WaveTrend SELL " + _Symbol + " @" + DoubleToString(close[i], _Digits)); //--- Alert sell
      last_sell = true;                    //--- Set last sell
      new_signal_redraw = true;            //--- Set redraw
   } else last_sell = sell_event;          //--- Update last sell
}

まず、TP/SLの表示が有効になっていること、買いまたは売りイベントが発生していること、そして現在がレート配列の最終バーであることを確認します。これらの条件がすべて満たされた場合、シグナル方向を(買いなら1、売りなら-1)として決定し、現在の終値を取得します。その後、sumとcountを0で初期化し、tp_sl_lengthまでの過去バーをループします。このとき、無効なインデックスはスキップし、各バーの高値−安値レンジをsumに加算し、有効なバーごとにcountをインクリメントします。

ループ終了後、countが正であればsum / countとして平均レンジを計算し、そうでなければ0とします。シグナル方向と選択されたtpSlMode(Candle_MultiplierまたはPercentage列挙型)に応じてストップロス値を計算します。買いの場合はマルチプライヤーモードではclose −(平均レンジ × slMultiplier)、パーセンテージモードではclose ×(1 − slPercent / 100)として算出し、売りの場合はその逆でclose +(平均レンジ × slMultiplier)、またはclose ×(1 + slPercent / 100)として計算します。同様に、3つのTP値についてもそれぞれ対応する倍率またはパーセンテージを用いて計算し、方向に応じて加算と減算を切り替えます。

その後、staticの最終シグナル方向、SLおよびTP、およびシグナル時刻を更新し、描画更新のためにredrawフラグをtrueに設定します。さらに、現在が最終バーである場合にはstaticなブール値を用いて直前の買いと売り状態を管理します。新規の買いシグナルで未フラグのケースでは、「WaveTrend BUY」に銘柄、"@"、およびDoubleToStringにより銘柄桁数でフォーマットされた終値を連結したメッセージでAlertを発行し、lastBuyをtrueに設定し、再描画を有効化します。それ以外の場合はlastBuyフラグを更新します。売りイベントも同様に処理し、「WaveTrend SELL」のアラートを発行してlastSellを更新し、新規イベント時にはredrawを有効化します。これにより、シグナル発生時にアラートが出力されるようになります。

シグナルアラート

チャート上のレベルを視覚化するために、以下のロジックを採用します。

if (showTPSL) {                               //--- Check TP/SL
   if (last_dir == 0) {                       //--- Handle no dir
      ObjectDelete(0, slLine);                //--- Delete SL
      ObjectDelete(0, tp1Line);               //--- Delete TP1
      ObjectDelete(0, tp2Line);               //--- Delete TP2
      ObjectDelete(0, tp3Line);               //--- Delete TP3
      for (int i = 0; i < ArraySize(tpSlTableObjects); i++) { //--- Loop table
         ObjectDelete(0, tpSlTableObjects[i]); //--- Delete object
      }
   } else {                                   //--- Handle dir
      datetime extension_time = last_signal_time; //--- Set time
      color tp_color = (last_dir == 1) ? col_up : col_dn; //--- Set TP color
      color sl_color = (last_dir == 1) ? col_dn : col_up; //--- Set SL color

      drawRightPrice(slLine, extension_time, last_sl, sl_color, STYLE_SOLID, 2); //--- Draw SL
      drawRightPrice(tp1Line, extension_time, last_tp1, tp_color, STYLE_SOLID, 1); //--- Draw TP1
      drawRightPrice(tp2Line, extension_time, last_tp2, tp_color, STYLE_SOLID, 1); //--- Draw TP2
      drawRightPrice(tp3Line, extension_time, last_tp3, tp_color, STYLE_SOLID, 1); //--- Draw TP3

      currentChartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Update width
      drawRectangleLabel(tpSlTableObjects[0], currentChartWidth - 150, 20, 120, 120, clrGray, BORDER_FLAT, true); //--- Draw frame
      drawLabel(tpSlTableObjects[1], currentChartWidth - 140, 30, "Level", clrBlack); //--- Draw level
      drawLabel(tpSlTableObjects[2], currentChartWidth - 80, 30, "Price", clrBlack); //--- Draw price
      drawLabel(tpSlTableObjects[3], currentChartWidth - 140, 50, "TP1", tp_color); //--- Draw TP1
      drawLabel(tpSlTableObjects[4], currentChartWidth - 80, 50, DoubleToString(last_tp1, _Digits), tp_color); //--- Draw TP1 price
      drawLabel(tpSlTableObjects[5], currentChartWidth - 140, 70, "TP2", tp_color); //--- Draw TP2
      drawLabel(tpSlTableObjects[6], currentChartWidth - 80, 70, DoubleToString(last_tp2, _Digits), tp_color); //--- Draw TP2 price
      drawLabel(tpSlTableObjects[7], currentChartWidth - 140, 90, "TP3", tp_color); //--- Draw TP3
      drawLabel(tpSlTableObjects[8], currentChartWidth - 80, 90, DoubleToString(last_tp3, _Digits), tp_color); //--- Draw TP3 price
      drawLabel(tpSlTableObjects[9], currentChartWidth - 140, 110, "SL", sl_color); //--- Draw SL
      drawLabel(tpSlTableObjects[10], currentChartWidth - 80, 110, DoubleToString(last_sl, _Digits), sl_color); //--- Draw SL price
   }
}

TP/SLの表示が有効かどうかを確認し、有効であればさらに最後のシグナル方向が0(アクティブなシグナルなし)であるかを判定します。この場合、関連するオブジェクトをすべて削除するため、TPおよびSLのラインに対してObjectDeleteを呼び出し、さらにテーブルオブジェクトの配列をArraySizeでループし、各要素を個別に削除します。一方、アクティブな方向が存在する場合は、まず拡張時間として最後のシグナル時刻を設定します。次にTPの色を、買いの場合は上昇カラー、売りの場合は下降カラーに設定し、ストップロスの色はその逆に設定します。その後、drawRightPriceを呼び出し、SLラインを太さ2の実線で描画し、各TPラインも太さの実線で、それぞれ拡張時間上に対応する価格と色で描画します。

続いて、チャート幅をChartGetIntegerCHART_WIDTH_IN_PIXELSで取得し更新します。その後、右端から150ピクセル、Y座標20の位置にサイズ120×120でテーブルフレームをdrawRectangleLabelで描画し、グレー色、フラットボーダー、背景有効の設定とします。さらにdrawLabelを用いてラベルを配置します。ヘッダとしてLevelPriceを黒色で描画し、各TPおよびSLについてもTP1などのラベルと、DoubleToStringで銘柄桁数にフォーマットした価格を表示し、それぞれ適切な色で右端からのオフセット位置に配置します。この部分は必要に応じて調整可能です。コンパイル後、以下のようにレベルが描画されます。

SL/TPレベル付きCanvas描画完成図

Canvas上でレベルが正しく描画されていることが確認できます。次に残るのは、チャート変更時にもシームレスに更新されるようにする処理です。そのためにOnChartEventイベントハンドラを使用し、チャートの変更を検知します。

//+------------------------------------------------------------------+
//| Handle chart event                                               |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) {
   if (id == CHARTEVENT_CHART_CHANGE) {          //--- Check change
      Redraw(Bars(_Symbol, _Period));            //--- Redraw
      UpdateFontSizes();                         //--- Update fonts
   }
}

OnChartEventイベントハンドラでは、イベントID、long値、double値、string値といった受信パラメータを処理し、ユーザーによるチャート操作を検知して反応します。IDがCHARTEVENT_CHART_CHANGEと一致する場合、これはズーム、パン、リサイズなどのチャート変更を示すため、更新処理をトリガーします。このとき、現在の銘柄と時間足を用いてBarsから取得した総バー数を引数にRedrawを呼び出し、さらにUpdateFontSizesを実行してテキスト要素のフォントサイズを再計算し、視認性を最適化します。また、インジケーターがチャートから削除される際には、チャート上に生成されたオブジェクトをすべて削除する必要があります。

//+------------------------------------------------------------------+
//| Deinitialize indicator                                           |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   obj_Canvas.Destroy();                         //--- Destroy canvas

   ArrayResize(all_boxes, 0);                    //--- Clear boxes
   ArrayResize(all_signals, 0);                  //--- Clear signals

   ObjectDelete(0, slLine);                      //--- Delete SL line
   ObjectDelete(0, tp1Line);                     //--- Delete TP1 line
   ObjectDelete(0, tp2Line);                     //--- Delete TP2 line
   ObjectDelete(0, tp3Line);                     //--- Delete TP3 line

   for (int i = 0; i < ArraySize(tpSlTableObjects); i++) { //--- Loop through table objects
      ObjectDelete(0, tpSlTableObjects[i]);      //--- Delete object
   }

   ChartRedraw(0);                               //--- Redraw chart
}

OnDeinitイベントハンドラでは、インジケーターがチャートから削除される際にリソースをクリーンアップします。まず、CanvasオブジェクトをDestroyメソッドで破棄し、描画要素を解放します。次に、all_boxesとall_signals配列をArrayResizeによりサイズ0へリサイズし、保持しているデータをクリアしてメモリを解放します。続いて、TPおよびSLの各ラインオブジェクトをObjectDeleteにより削除します。その後、テーブルオブジェクト配列についてもArraySizeを基準にループし、各要素をObjectDeleteで削除することで、リスク管理表示を完全に除去します。最後にChartRedrawを呼び出してチャートを更新し、すべての削除内容を反映させます。これで一連のクリーンアップ処理は完了です。 残っている作業は、このプログラムのバックテストをおこなうことです。バックテストについては次のセクションで扱います。


バックテスト

テストを実施しました。以下はコンパイル後の可視化を単一のGraphics Interchange Format (GIF)ビットマップ画像形式で示したものです。

バックテストGIF


結論

WaveTrend CrossoverインジケーターをMQL5上で強化し、Canvasベースの描画によるフォグ状グラデーション、ブレイクアウトを検知するシグナルボックス、視覚的なアラートとしてのカスタマイズ可能な買いと売りのバブルまたは三角形、さらに動的なTPおよびSLによる統合的なリスク管理を実装しました。このアップグレードでは、相場環境を示すフォグ状グラデーションといった高度な視覚表現に加え、トレンドフィルタリング、ボックスの延長設定、そしてローソク足倍率またはパーセンテージによる計算オプションが組み込まれています。これらはラインやテーブルを用いて表示されます。この構成により、モメンタムの転換、トレンド分析、リスク評価をより直感的に把握できる没入型ツールが提供されます。この強化されたWaveTrend Crossoverインジケーターによって、視覚的機能とリスク管理機能を活用し、より深い取引へのインサイトを得ることが可能になります。今後の取引の最適化にも対応できる設計です。取引をお楽しみください。

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

添付されたファイル |
Python-MetaTrader 5ストラテジーテスター(第3回):MetaTrader 5風の取引操作 — 処理と管理 Python-MetaTrader 5ストラテジーテスター(第3回):MetaTrader 5風の取引操作 — 処理と管理
シミュレーター内で注文の開始、終了、変更などの取引操作を処理するための、Python-MetaTrader5と同様の方法を紹介します。シミュレーションがMT5と同様の動作となるように、取引リクエストに対して厳密な検証処理が実装されており、銘柄取引パラメータや一般的なブローカーの制限事項が考慮されています。
ラリー・ウィリアムズの『市場の秘密』(第6回):市場変動を利用したボラティリティブレイクアウトの測定 ラリー・ウィリアムズの『市場の秘密』(第6回):市場変動を利用したボラティリティブレイクアウトの測定
MQL5を用いてラリー・ウィリアムズのボラティリティブレイクアウト型エキスパートアドバイザーを設計および実装する方法を解説します。スイングレンジの測定、エントリーレベルの算出、リスクベースのポジションサイジング、さらに実際の市場データを用いたバックテストまでを網羅します。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
プライスアクション分析ツールキットの開発(第54回):EMAと平滑化された価格変動によるトレンドのフィルタリング プライスアクション分析ツールキットの開発(第54回):EMAと平滑化された価格変動によるトレンドのフィルタリング
取引の明確さとタイミングを向上させるために、平均足による平滑化とEMA20の高値および安値のバンド、さらにEMA50のトレンドフィルターを組み合わせた手法を解説します。これらのツールにより、トレーダーは真のモメンタムを見極め、ノイズを排除し、ボラティリティの高い局面やトレンド相場により適切に対応できます。