English Русский
preview
初心者からエキスパートへ:パラメータ制御ユーティリティ

初心者からエキスパートへ:パラメータ制御ユーティリティ

MetaTrader 5インディケータ |
17 0
Clemence Benjamin
Clemence Benjamin

内容


はじめに

本日は、前回の記事で築いた基盤をさらに発展させていきます。これまでの記事を追ってこられた方であれば、上位足(HTF: Higher TimeFrame)の期間を下位足のチャート上に直接可視化するインジケーターを開発したことを覚えていらっしゃるでしょう。この手法は、1本の上位足の内部に隠された複雑な値動きを明らかにする、非常に強力な分析ツールとなりました。

このような詳細情報は、トレーダーにとって極めて有益です。たとえば、上位足では単なるヒゲに見える部分も、下位足で確認すると、明確なパターン、サポートやレジスタンス、あるいはオーダーブロックとして現れることがあります。この内部的な市場構造を理解することで、将来の価格挙動をより正確に予測し、戦略を洗練させることが可能になります。以下の図1では、左側のH1足における期間Aが、強気のヒゲの中にレジスタンス(拒否ゾーン)を含んでいることが分かります。そして右側の期間Bにおいて、そのゾーンが再度テストされ、機能している様子が確認できます。このように、過去のヒゲ領域が将来の価格反応における重要な参照点となることが分かります。

H1とM1の時間軸による分析

図1:Market Periods Synchronizerを用いたM1上でのH1の洞察

しかし、複雑なツールを開発する際にしばしば問題となるのが、直感的で簡単にアクセスできる操作手段の欠如です。従来は、EAやインジケーターのパラメータ調整を[Inputs]タブでおこなってきましたが、多数の値を調整したり、視覚的な設定を試行錯誤したりする場合、この方法はすぐに煩雑になってしまいます。

このプロジェクトでは、この課題に対処するため、リアルタイム操作が可能なコントロールユーティリティを作成します。これは、インジケーターの静的な入力パラメータをオンチャートで操作可能なインタラクティブコントロールへと変換するエキスパートアドバイザー(EA)です。この「Market Periods Synchronizer制御ユーティリティ」は、従来のインジケーターの概念を拡張し、高度な機能を備えた動的ダッシュボードとして実装されます。これにより、即時のフィードバックと、より効率的な分析ワークフローが実現します。

新ユーティリティの主な利点

  1. 即時パラメータ操作:プロパティダイアログを開くことなく、チャート上から直接設定を変更できます。
  2. リアルタイムの視覚更新:コントロールを操作すると、オブジェクト、色、時間軸の変更が即座に反映されます。
  3. 分析の高速化:保存や再読み込みの繰り返しが不要になり、作業効率が大幅に向上します。
  4. モダンなビジュアルインターフェース:CCanvasを使用した、滑らかで半透明の美しいパネルを実現しています。
  5. マルチタイムフレーム(MTF)の同期表示:主要・補助時間軸の構造をシームレスに確認・制御できます。
  6. インタラクティブなスライダー:幅や更新間隔などの値を素早く微調整できます。
  7. トグルスイッチ:機能のオン・オフをワンクリックで切り替えられます。

以下の図は、本記事の最後に実現したい姿を示したものです。この後、実装フェーズに入り、各開発ステップを分解して解説し、コアとなるコード構造を説明します。最後に、導入およびパフォーマンステストについても触れていきます。

市場期間シンクロナイザー制御ユーティリティ

図2:パラメータ制御機能を示すスクリーンショット


実装

この章では、インジケーターの静的な入力パラメータをオンチャートのダッシュボードへ変換する、モジュール化されたEAを実装します。このEAは、(1)上位足(時間、始値、終値)を読み取り、(2)上位足の背景ビジュアル(垂直ライン、任意のボディ塗りつぶし、始値/終値の水平ライン)を描画し、(3)半透明キャンバスとチャートオブジェクト(ボタン、ラベル、縦型スライダー、ポップアップ時間軸ドロップダウン)を用いて実行時コントロールを提供します。UIは実行時状態変数(入力の可変コピー)を更新し、RefreshLines()がそれらの値を再利用してチャートオブジェクトを即座に更新します。設計の優先事項は、明瞭性(UIコンテナとラベルの整理)、応答性(スライダー操作が即時反映されること)、および非干渉性(HTFオブジェクトをOBJPROP_BACK=trueで作成し、UIのクリック操作を妨げないこと)です。

読者が見落としがちな重要な実装ポイントは以下のとおりです。

  • 入力パラメータはあくまでデフォルトとして扱われ、ダッシュボードは実行時コピー(g_接頭辞付き)を変更します。これにより、EA実行中はUI操作が保持されます。
  • スライダーは縦型で、2つのチャートボタン(トラックとノブ)で構成されます。ノブをドラッグすると、基礎となる値がリアルタイムで更新されます。
  • HTF形状は背景に描画され(OBJPROP_BACK = true)、UIからマウスイベントを奪いません。
  • OnTick()では、不要な再描画を避けるため、iTimeによる時間変化のみをチェックします。

1) ヘッダ、インクルード、ユーザー入力―目的と挙動

まず、Canvas.mqhをインクルードし、すべての入力パラメータを宣言します。「<Canvas/Canvas.mqh>」は、小さなUIヘルパークラス(CCanvas)を取り込み、ダッシュボード用の半透明背景ビットマップを描画するために使用します。チャートオブジェクト(ボタンやラベル)だけでは、チャートテーマによって見た目が不揃いになりがちですが、キャンバスを使うことで、一度スタイルを定義すれば再利用可能な、統一感のあるコンテナを提供できます。Canvas.mqhがインクルードパスに存在しない場合、EAはコンパイルに失敗する点に注意してください。また、「#property strict」を使用し、モダンなMQLの型およびシグネチャ規則をコンパイラに強制させています。

入力ブロックは、ユーザーが直感的に理解できるよう、実用的な3つのカテゴリに分けて整理しています。最初のカテゴリは「上位足とビジュアル」です。ここには、上位足を定義する時間軸(例:H1)、描画する上位足の本数を決めるルックバック、上位足のデフォルト色と線幅、リフレッシュ間隔(秒)が含まれます。これらの設定は、チャート上の基本構造を決定します。上位足はすべての描画(塗りつぶし、始値/終値の水平ライン、下位足)をコンテキスト化するフレームとして機能します。ルックバックは特に重要で、作成される上位足関連オブジェクトの数を制御します。ルックバックを大きく設定しすぎるとオブジェクトが増え、チャート描画が遅くなる可能性があるため、本番環境では妥当なデフォルト値(200)や上限値を設定することを推奨します。

2番目のカテゴリは、始値/終値マーカーとローソク足の塗りつぶしに関する設定です。第2のカテゴリは、始値/終値マーカーとローソク足の塗りつぶしに関する設定です。ここでは、上位足の始値や終値を示す水平ラインの表示のオン/オフや色、線幅やラインスタイル、現在の時間足単位での延長距離などを設定できます。また、陽線と陰線に対応する塗りつぶしの表示有無や色も指定可能です。これらの設定は任意ですが非常に有効で、塗りつぶしは陽線と陰線を一目で視覚化でき、水平ラインは上位足内部のサポートやレジスタンス、ヒゲ構造を把握するのに役立ちます。コード内ではこれらを入力パラメータのデフォルト値として保持し、実行時にはコピーした変数を用いてダッシュボード上で動的に切り替えられるようにしています。

3番目のカテゴリは下位足の設定です。Minor1とMinor2の2つの下位足レイヤーを用意しており、それぞれ表示のオン/オフ、時間足の選択、色、線幅を指定できます。この設定により、上位足内部の中間構造を可視化できます。たとえばH1の上位足に対してM15の下位足を描画すると、上位足内部の細かい分割が視覚化されます。EAは下位足の時間が2本の連続する上位足の間にある場合、あるいは現在の上位足内にある場合にのみ下位縦線を描画します。この動作は元のインジケーターのロジックを再現しており、2つの下位レイヤーを有効にすることで、ネスト構造(例えばH1上位、M30下位、M15マイクロ下位)を同時に確認できます。

重要なアーキテクチャ上のポイントであり、よくある落とし穴として、MQLの入力パラメータはコンパイル時定数であり、EA実行中に直接書き換えることができないという点があります。そのため、完全にインタラクティブなダッシュボードを実現するために、各入力パラメータはOnInit()の段階で対応するg_(グローバル可変)変数にコピーされます。ダッシュボードのUIはこのg_変数を更新し、RefreshLines()がg_変数の値を読み取ってチャート上のオブジェクトを即座に更新します。この仕組みにより、入力パラメータは安全なデフォルトとして保持されつつ、実行中に自由に操作できる状態を提供できます。また、ユーザーが設定をセッション間で保持したい場合には、明示的な保存/読み込みロジックを追加する必要があります。これは将来的な拡張として考えられます。

さらに、チャートオブジェクトの管理のために、命名規則と接頭辞を徹底的に使用しています。HTF_MAJ_、HTF_MIN1_、MPS_UI_といった接頭辞を付けることで、作成したオブジェクトだけを更新・削除でき、ユーザーがチャート上に描画した他のオブジェクトを誤って変更することを防ぎます。このような命名の一貫性は小さな工夫ですが非常に重要で、適切に行わないと意図せず関係のないオブジェクトを削除してしまう危険があります。また、内部的にtf_list[]配列を保持することで、許可された時間足のセットを管理し、ドロップダウンやサイクルロジックの整合性を保っています。

#include <Canvas/Canvas.mqh>   // Canvas helper library (expects Canvas.mqh to be present)

// --------------------------- USER INPUTS ---------------------------
// Major timeframe + lookback + default visuals
input ENUM_TIMEFRAMES InpHigherTF      = PERIOD_H1;   // Major higher timeframe
input int            InpLookback       = 200;         // Lookback (bars)
input color          InpColorMajor     = clrRed;      // Major line color
input int            InpWidthMajor     = 2;           // Major line width
input int            InpRefreshSec     = 5;           // Refresh interval (seconds)

// Open/Close marker settings
input bool           InpShowOpenClose  = true;        // show open/close markers
input color          InpColorOpen      = clrGreen;
input color          InpColorClose     = clrLime;
input int            InpWidthOC        = 1;
input ENUM_LINE_STYLE InpStyleOC       = STYLE_DASH;
input int            InpHorizOffsetBars= 3;

// Body fill for majors
input bool           InpShowFill       = true;
input color          InpFillBull       = clrLime;
input color          InpFillBear       = clrPink;

// Minor periods
input bool           InpShowMinor1     = false;
input ENUM_TIMEFRAMES InpMinor1TF     = PERIOD_M30;
input color          InpColorMin1      = clrOrange;
input int            InpWidthMin1      = 1;

input bool           InpShowMinor2     = false;
input ENUM_TIMEFRAMES InpMinor2TF     = PERIOD_M15;
input color          InpColorMin2      = clrYellow;
input int            InpWidthMin2      = 1;

2) グローバル変数と実行時コピー:入力と実行時状態を分ける理由

実行時に変更可能な状態を管理するため、配列やg_変数を宣言しています。これには、ランタイムで操作可能な状態、カラーパレット、スライダーの基盤、UI用の名前文字列などが含まれます。このように入力パラメータと実行時の状態を分けることが重要です。UI操作はg_変数に書き込みをおこない、RefreshLines()がそれらの値を読み取ってチャート上のオブジェクトを更新します。また、各スライダーを汎用的に作成・更新できるよう、スライダーのメタデータを保持する配列も準備しています。

// --------------------------- GLOBALS -------------------------------
enum SliderIndex { SLIDER_MAJ_WIDTH = 0, SLIDER_REFRESH = 1 };
const int SLIDER_COUNT = 2;
int Y_OFFSET = 50; // top offset for UI container

// runtime (mutable) copies of inputs (dashboard will change these)
ENUM_TIMEFRAMES g_HigherTF;
int             g_Lookback;
color           g_ColorMajor;
int             g_WidthMajor;
int             g_RefreshSec;
// ... (other g_ variables for toggles & minors)

// slider infrastructure (vertical sliders)
string g_slider_track_names[];
string g_slider_knob_names[];
int    g_slider_min[];
int    g_slider_max[];
int    g_slider_left_x[];
int    g_slider_top_y[];
int    g_vslider_height_px = 110;
int    g_vslider_width_px  = 14;
int    g_slider_knob_w     = 12;
bool   g_slider_drag = false;
int    g_current_slider = -1;

3) 時間軸ヘルパーユーティリティ:ラベルの一貫性とサイクル操作

TFToString()は、ボタンやオブジェクト名の時間軸ラベルを一元管理します。これにより、ドロップダウンやラベルが常に一致するようになります。FindNextTFIndex()は、tf_list内で次の時間軸に移動するシンプルなサイクルを実装しており、ボタンでの素早い切り替えに便利です。

string TFToString(ENUM_TIMEFRAMES tf)
  {
   switch(tf)
     {
      case PERIOD_M1:  return "M1";
      case PERIOD_M5:  return "M5";
      case PERIOD_M15: return "M15";
      case PERIOD_M30: return "M30";
      case PERIOD_H1:  return "H1";
      case PERIOD_H4:  return "H4";
      case PERIOD_D1:  return "D1";
      case PERIOD_W1:  return "W1";
      case PERIOD_MN1: return "MN";
     }
   return IntegerToString((int)tf);
  }

int FindNextTFIndex(ENUM_TIMEFRAMES current)
  {
   int n = ArraySize(tf_list);
   for(int i=0;i<n;i++) if(tf_list[i] == current) return (i+1)%n;
   return 0;
  }

4) OnInit():実行時状態、UI名、キャンバス、および初期描画の準備

OnInit()はオーケストレーションのステップです。入力値を「g_*」変数にコピーし、変化検知用の最終ローソク足時刻を計算し、UI用の名前文字列を作成し、スライダー配列を準備し、キャンバス背景を生成します。その後、UIウィジェット(ボタン、ラベル、カラーボタン)を作成し、縦型スライダーとラベルを作成してタイマーを開始します。最後にRefreshLines()を呼び出すことで、チャート上にHTFオブジェクトが即座に反映されます。

EAをコンパイルしてチャートにアタッチすると、半透明のUIパネルが表示され、コントロールが操作可能になります。また、HTFのラインや塗りつぶしも入力値のデフォルトに従ってチャート上に描画されます。

int OnInit()
  {
   main_chart_id = ChartID();

   // copy inputs -> runtime
   g_HigherTF        = InpHigherTF;
   g_Lookback        = MathMax(10, InpLookback);
   g_ColorMajor      = InpColorMajor;
   g_WidthMajor      = MathMax(1, InpWidthMajor);
   g_RefreshSec      = MathMax(1, InpRefreshSec);
   // ... (copy the rest of Inp->g_ variables)

   // initialize last bar times
   g_last_major_time = iTime(_Symbol, g_HigherTF, 0);
   if(g_ShowMinor1) g_last_minor1_time = iTime(_Symbol, g_Minor1TF, 0);
   if(g_ShowMinor2) g_last_minor2_time = iTime(_Symbol, g_Minor2TF, 0);

   // UI prefix and object names
   UI_PREFIX = StringFormat("MPS_UI_%d_", main_chart_id);
   lbl_title = UI_PREFIX + "LBL_TITLE";
   btn_major_tf = UI_PREFIX + "BTN_MAJ_TF";
   // ... (initialize other UI name strings)

   // prepare slider arrays
   ArrayResize(g_slider_track_names, SLIDER_COUNT);
   ArrayResize(g_slider_knob_names,  SLIDER_COUNT);
   ArrayResize(g_slider_min,        SLIDER_COUNT);
   ArrayResize(g_slider_max,        SLIDER_COUNT);
   ArrayResize(g_slider_left_x,     SLIDER_COUNT);
   ArrayResize(g_slider_top_y,      SLIDER_COUNT);
   for(int i=0;i<SLIDER_COUNT;i++)
     {
      g_slider_track_names[i] = UI_PREFIX + StringFormat("SL_TRK_%d", i);
      g_slider_knob_names[i]  = UI_PREFIX + StringFormat("SL_KNB_%d", i);
     }

   // background area coords and create UI
   g_bg_y = Y_OFFSET - 6;
   g_bg_h = 250;
   CreateUIBackground();
   CreateLabel(lbl_title, 12, 4 + Y_OFFSET, "Market Period Synchronizer Control Utility", 12);
   ObjectSetInteger(main_chart_id, lbl_title, OBJPROP_COLOR, XRGB(230,230,230));

   // create many UI buttons/labels and sliders...
   CreateButton(btn_major_tf, 12, 34 + Y_OFFSET, 70, 24, TFToString(g_HigherTF));
   CreateLabel(lbl_major_tf, 92, 36 + Y_OFFSET, "Major TF");
   // ... more buttons and color swatches

   // Major width slider (vertical)
   g_slider_left_x[SLIDER_MAJ_WIDTH] = 190;
   g_slider_top_y[SLIDER_MAJ_WIDTH]  = slider_base_top;
   g_slider_min[SLIDER_MAJ_WIDTH] = 1; g_slider_max[SLIDER_MAJ_WIDTH] = 10;
   CreateVerticalSliderAt(SLIDER_MAJ_WIDTH, g_slider_left_x[SLIDER_MAJ_WIDTH], g_slider_top_y[SLIDER_MAJ_WIDTH],
                          g_slider_track_names[SLIDER_MAJ_WIDTH], g_slider_knob_names[SLIDER_MAJ_WIDTH],
                          g_WidthMajor, g_slider_min[SLIDER_MAJ_WIDTH], g_slider_max[SLIDER_MAJ_WIDTH], g_vslider_height_px);

   // start events & timer
   ChartSetInteger(main_chart_id, CHART_EVENT_MOUSE_MOVE, true);
   EventSetTimer(g_RefreshSec);

   // initial drawing of HTF objects
   RefreshLines();

   return INIT_SUCCEEDED;
  }

5) キャンバス背景の作成:ビジュアルのグループ化とクリック挙動

CreateUIBackground()は、CCanvasヘルパーを使って、UI要素の背後に配置される暗めの半透明パネルとして機能するビットマップラベルを作成します。重要な設計上の決定として、キャンバスにはOBJPROP_BACK = false およびOBJPROP_SELECTABLE = falseを設定しています。これにより、キャンバスは表示されますがクリックを妨げず、前面に描かれたボタンはマウスイベントを正常に受け取ります。

洗練された暗色パネルは、シンボルやチャートテーマを問わず視認性を向上させます。

void CreateUIBackground()
  {
   g_bg_name = UI_PREFIX + "BG";
   ObjectDelete(main_chart_id, g_bg_name);

   bool ok = g_bgCanvas.CreateBitmapLabel(main_chart_id, 0, g_bg_name, g_bg_x, g_bg_y, g_bg_w, g_bg_h, COLOR_FORMAT_ARGB_RAW);
   if(!ok) { PrintFormat("CreateUIBackground: CreateBitmapLabel failed err=%d", GetLastError()); return; }

   uint dark_grey = ARGB(180, 30, 30, 30);
   uint border_col = XRGB(80, 80, 80);
   uint top_strip  = ARGB(210, 24, 24, 24);

   g_bgCanvas.FillRectangle(0, 0, g_bg_w - 1, g_bg_h - 1, dark_grey);
   g_bgCanvas.Rectangle(0, 0, g_bg_w - 1, g_bg_h - 1, border_col);
   g_bgCanvas.FillRectangle(0, 0, g_bg_w - 1, 28, top_strip);

   g_bgCanvas.Update(true);

   ObjectSetInteger(main_chart_id, g_bg_name, OBJPROP_BACK, false);
   ObjectSetInteger(main_chart_id, g_bg_name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(main_chart_id, g_bg_name, OBJPROP_HIDDEN, false);
  }

6) UI作成ヘルパー:ボタン/ラベルのスタイルを統一

CreateButton、CreateLabel、CreateColorButtonはUI作成を一元管理し、レイアウトやスタイルの一貫性を保ちます。ボタンは選択可能として前面に描画され(OBJPROP_BACK=false)、ラベルは選択不可に設定されます。これにより、イベントモデルが予測可能になり、見た目も統一されます。

void CreateButton(string name,int x,int y,int w,int h,string text)
  {
   if(StringLen(name) == 0) return;
   ObjectDelete(main_chart_id, name);
   if(!ObjectCreate(main_chart_id, name, OBJ_BUTTON, 0, 0, 0))
     { PrintFormat("CreateButton: failed to create %s err=%d", name, GetLastError()); return; }
   ObjectSetInteger(main_chart_id, name, OBJPROP_BACK, false);     // draw in front
   ObjectSetInteger(main_chart_id, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(main_chart_id, name, OBJPROP_XDISTANCE, x);
   ObjectSetInteger(main_chart_id, name, OBJPROP_YDISTANCE, y);
   ObjectSetInteger(main_chart_id, name, OBJPROP_XSIZE, w);
   ObjectSetInteger(main_chart_id, name, OBJPROP_YSIZE, h);
   ObjectSetString(main_chart_id, name, OBJPROP_TEXT, text);
   ObjectSetInteger(main_chart_id, name, OBJPROP_FONTSIZE, 10);
   ObjectSetInteger(main_chart_id, name, OBJPROP_SELECTABLE, true);
   ObjectSetInteger(main_chart_id, name, OBJPROP_HIDDEN, false);
  }

void CreateLabel(string name,int x,int y,string text, int fontsize=9)
  {
   if(StringLen(name) == 0) return;
   ObjectDelete(main_chart_id, name);
   if(!ObjectCreate(main_chart_id, name, OBJ_LABEL, 0, 0, 0))
     { PrintFormat("CreateLabel: failed to create %s err=%d", name, GetLastError()); return; }
   ObjectSetInteger(main_chart_id, name, OBJPROP_BACK, false);
   ObjectSetInteger(main_chart_id, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(main_chart_id, name, OBJPROP_XDISTANCE, x);
   ObjectSetInteger(main_chart_id, name, OBJPROP_YDISTANCE, y);
   ObjectSetString(main_chart_id, name, OBJPROP_TEXT, text);
   ObjectSetInteger(main_chart_id, name, OBJPROP_FONTSIZE, fontsize);
   ObjectSetInteger(main_chart_id, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(main_chart_id, name, OBJPROP_HIDDEN, false);
  }

void CreateColorButton(string name, int x, int y, int w, int h, color col)
  {
   CreateButton(name, x, y, w, h, "");
   ObjectSetInteger(main_chart_id, name, OBJPROP_BGCOLOR, col);
   ObjectSetInteger(main_chart_id, name, OBJPROP_FONTSIZE, 8);
  }

7) 縦型スライダー:アーキテクチャとリアルタイムドラッグ

縦型スライダーは、選択不可のトラック(視覚的表示)と選択可能なノブ(ボタン)から構成されます。CreateVerticalSliderAt()は値の範囲からノブの位置を計算し、スライダーのメタデータを配列に格納してノブを配置します。ドラッグのロジックはOnChartEventで処理され、CHARTEVENT_MOUSE_MOVEを使用してノブのY座標を設定し、比率を計算して値の範囲にマッピングします。スライダーが変更されると、対応するg_変数(例:g_WidthMajor、g_RefreshSec)が更新され、RefreshLines()が即座に呼び出されます。

値のマッピングではY座標が反転しており、上が最大値になります。

void CreateVerticalSliderAt(int id, int base_x, int base_y, string track_name, string knob_name, int current_value, int min_val, int max_val, int track_height)
  {
   // track
   ObjectDelete(main_chart_id, track_name);
   if(!ObjectCreate(main_chart_id, track_name, OBJ_BUTTON, 0, 0, 0))
     { PrintFormat("CreateVerticalSliderAt: failed track %s err=%d", track_name, GetLastError()); }
   ObjectSetInteger(main_chart_id, track_name, OBJPROP_BACK, false);
   ObjectSetInteger(main_chart_id, track_name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(main_chart_id, track_name, OBJPROP_XDISTANCE, base_x);
   ObjectSetInteger(main_chart_id, track_name, OBJPROP_YDISTANCE, base_y);
   ObjectSetInteger(main_chart_id, track_name, OBJPROP_XSIZE, g_vslider_width_px);
   ObjectSetInteger(main_chart_id, track_name, OBJPROP_YSIZE, track_height);
   ObjectSetString(main_chart_id, track_name, OBJPROP_TEXT, "");
   ObjectSetInteger(main_chart_id, track_name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(main_chart_id, track_name, OBJPROP_HIDDEN, false);

   // knob
   ObjectDelete(main_chart_id, knob_name);
   if(!ObjectCreate(main_chart_id, knob_name, OBJ_BUTTON, 0, 0, 0))
     { PrintFormat("CreateVerticalSliderAt: failed knob %s err=%d", knob_name, GetLastError()); }
   ObjectSetInteger(main_chart_id, knob_name, OBJPROP_BACK, false);
   ObjectSetInteger(main_chart_id, knob_name, OBJPROP_CORNER, CORNER_LEFT_UPPER);

   double ratio = 0.0;
   if(max_val > min_val) ratio = double(current_value - min_val) / double(max_val - min_val);
   int knob_y = base_y + (int)MathRound((1.0 - ratio) * (track_height - g_slider_knob_w));
   int knob_x = base_x - (g_slider_knob_w/2) + (g_vslider_width_px/2);

   ObjectSetInteger(main_chart_id, knob_name, OBJPROP_XDISTANCE, knob_x);
   ObjectSetInteger(main_chart_id, knob_name, OBJPROP_YDISTANCE, knob_y);
   ObjectSetInteger(main_chart_id, knob_name, OBJPROP_XSIZE, g_slider_knob_w);
   ObjectSetInteger(main_chart_id, knob_name, OBJPROP_YSIZE, g_slider_knob_w);
   ObjectSetString(main_chart_id, knob_name, OBJPROP_TEXT, "");
   ObjectSetInteger(main_chart_id, knob_name, OBJPROP_SELECTABLE, true);
   ObjectSetInteger(main_chart_id, knob_name, OBJPROP_HIDDEN, false);
   ObjectSetInteger(main_chart_id, knob_name, OBJPROP_FONTSIZE, 1);

   // store slider params
   g_slider_min[id] = min_val;
   g_slider_max[id] = max_val;
   g_slider_left_x[id] = base_x;
   g_slider_top_y[id] = base_y;
  }

8) スライダーラベルの更新:UIを同期させる

UpdateLabelsAfterSliderChange()は、スライダーの値が変更されるたびにスライダー下のテキストラベルを更新し、視覚情報の一貫性を保ちます。

void UpdateLabelsAfterSliderChange()
  {
   if(ObjectFind(main_chart_id, lbl_major_width) >= 0)
      ObjectSetString(main_chart_id, lbl_major_width, OBJPROP_TEXT, StringFormat("Maj W:%d", g_WidthMajor));
      

   if(ObjectFind(main_chart_id, lbl_refresh_label) >= 0)
      ObjectSetString(main_chart_id, lbl_refresh_label, OBJPROP_TEXT, StringFormat("Refresh:%ds", g_RefreshSec));
      
  }

9) 時間軸ドロップダウンのロジック:小さく確実なポップアップリスト

ShowTFDropdownFor()は、指定された時間軸ボタン(major / minor1 / minor2)の直下に小さなボタンをスタックして表示します。HideTFDropdown()でこれらを非表示にします。ドロップダウンには接頭辞を付けて構成しており、OnChartEventで選択イベントを簡単に解析できるようにしています。

この方法は意図的にシンプルかつ堅牢に設計されており、チャートボタンを使うことでフルカスタムのコンボボックスクラスを作る必要がなく、期待されるドロップダウンの挙動を提供します。

void ShowTFDropdownFor(int target)
  {
   if(g_tf_dropdown_visible && g_tf_dropdown_target == target) { HideTFDropdown(); return; }
   if(g_tf_dropdown_visible) HideTFDropdown();

   string target_btn = (target == 0 ? btn_major_tf : (target == 1 ? btn_minor1_tf : btn_minor2_tf));
   if(ObjectFind(main_chart_id, target_btn) < 0) return;

   int bx = (int)ObjectGetInteger(main_chart_id, target_btn, OBJPROP_XDISTANCE);
   int by = (int)ObjectGetInteger(main_chart_id, target_btn, OBJPROP_YDISTANCE);
   int bh = (int)ObjectGetInteger(main_chart_id, target_btn, OBJPROP_YSIZE);

   int base_x = bx;
   int base_y = by + bh + 4;
   int w = 60;
   int h = 20;
   for(int i=0;i<ArraySize(tf_list);i++)
     {
      string oname = TF_DROPDOWN_PREFIX + IntegerToString(target) + "_" + IntegerToString(i);
      CreateButton(oname, base_x, base_y + i*(h+2), w, h, TFToString(tf_list[i]));
     }
   g_tf_dropdown_visible = true;
   g_tf_dropdown_target = target;
  }

void HideTFDropdown()
  {
   if(!g_tf_dropdown_visible) return;
   for(int i=0; i<ArraySize(tf_list); i++)
     {
      string oname = TF_DROPDOWN_PREFIX + IntegerToString(g_tf_dropdown_target) + "_" + IntegerToString(i);
      ObjectDelete(main_chart_id, oname);
     }
   g_tf_dropdown_visible = false;
   g_tf_dropdown_target = -1;
  }

10) OnChartEvent():イベントモデルとドラッグ処理

これはインタラクションの中核部分です。以下を処理します。

  • ノブのドラッグ開始・終了(ノブのクリックでドラッグ開始、以降のオブジェクトクリックでドラッグ終了)。
  • ドラッグ中のマウス移動:ノブのピクセル位置を計算し、数値にマッピングしてg_変数を更新し、値が変化した場合はRefreshLines()を呼び出します。
  • ボタンクリック:トグル操作、ルックバックの増減ボタン、カラーピッカー、時間軸ドロップダウンの表示/非表示。
  • 時間軸ドロップダウンの選択:クリックされたボタン名を解析し、major/minorに応じて選択された時間軸を適用し、再描画します。
  • 設計上の選択:任意のオブジェクトクリックでドラッグを停止させる方法は、プラットフォームを問わず簡単に実装できるパターンです。これにより、グローバルなマウスボタンアップイベント(MQL5では専用のチャートイベントとして常に利用できるわけではない)を検出する必要がなくなります。

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   // stop dragging if an object click happens
   if(id == CHARTEVENT_OBJECT_CLICK && g_slider_drag)
     {
      g_slider_drag = false;
      g_current_slider = -1;
      return;
     }

   // dragging motion: use mouse Y from dparam
   if(id == CHARTEVENT_MOUSE_MOVE && g_slider_drag && g_current_slider >= 0)
     {
      int my = (int)dparam;
      int s = g_current_slider;
      string track_name = g_slider_track_names[s];
      string knob_name  = g_slider_knob_names[s];

      int track_top = (int)ObjectGetInteger(main_chart_id, track_name, OBJPROP_YDISTANCE);
      int track_h   = (int)ObjectGetInteger(main_chart_id, track_name, OBJPROP_YSIZE);
      int track_bottom = track_top + track_h - g_slider_knob_w;

      int new_knob_y = my;
      if(new_knob_y < track_top) new_knob_y = track_top;
      if(new_knob_y > track_bottom) new_knob_y = track_bottom;

      ObjectSetInteger(main_chart_id, knob_name, OBJPROP_YDISTANCE, new_knob_y);

      double ratio = 1.0 - double(new_knob_y - track_top) / double(track_h - g_slider_knob_w);
      int new_val = g_slider_min[s] + (int)MathRound(ratio * (g_slider_max[s] - g_slider_min[s]));
      if(new_val < g_slider_min[s]) new_val = g_slider_min[s];
      if(new_val > g_slider_max[s]) new_val = g_slider_max[s];

      bool changed = false;
      if(s == SLIDER_MAJ_WIDTH)
        { if(g_WidthMajor != new_val) { g_WidthMajor = new_val; changed = true; } }
      else if(s == SLIDER_REFRESH)
        { if(g_RefreshSec != new_val) { g_RefreshSec = new_val; EventSetTimer(g_RefreshSec); changed = true; } }

      if(changed) { UpdateLabelsAfterSliderChange(); RefreshLines(); }
      return;
     }

   // TF dropdown option clicked
   if(StringFind(sparam, TF_DROPDOWN_PREFIX) == 0 && id == CHARTEVENT_OBJECT_CLICK)
     {
      string rest = StringSubstr(sparam, StringLen(TF_DROPDOWN_PREFIX));
      int sep = StringFind(rest, "_");
      if(sep >= 0)
        {
         int target = (int)StringToInteger(StringSubstr(rest, 0, sep));
         int idx    = (int)StringToInteger(StringSubstr(rest, sep+1));
         if(idx >= 0 && idx < ArraySize(tf_list))
           {
            ENUM_TIMEFRAMES chosen = tf_list[idx];
            if(target == 0) { g_HigherTF = chosen; g_last_major_time = iTime(_Symbol, g_HigherTF, 0); ObjectSetString(main_chart_id, btn_major_tf, OBJPROP_TEXT, TFToString(g_HigherTF)); }
            else if(target == 1) { g_Minor1TF = chosen; if(g_ShowMinor1) g_last_minor1_time = iTime(_Symbol, g_Minor1TF, 0); ObjectSetString(main_chart_id, btn_minor1_tf, OBJPROP_TEXT, TFToString(g_Minor1TF)); }
            else if(target == 2) { g_Minor2TF = chosen; if(g_ShowMinor2) g_last_minor2_time = iTime(_Symbol, g_Minor2TF, 0); ObjectSetString(main_chart_id, btn_minor2_tf, OBJPROP_TEXT, TFToString(g_Minor2TF)); }
            HideTFDropdown();
            RefreshLines();
           }
        }
      return;
     }

   // If dropdown visible and user clicked elsewhere => hide
   if(g_tf_dropdown_visible && id == CHARTEVENT_OBJECT_CLICK && StringFind(sparam, TF_DROPDOWN_PREFIX) != 0)
     {
      HideTFDropdown();
     }

   // Handle other button clicks & knob starts...
   if(id == CHARTEVENT_OBJECT_CLICK)
     {
      string obj = sparam;
      if(obj == btn_major_tf) { ShowTFDropdownFor(0); return; }
      if(obj == btn_lookback_minus) { g_Lookback = MathMax(10, g_Lookback - 10); ObjectSetString(main_chart_id, lbl_lookback, OBJPROP_TEXT, StringFormat("Lookback:%d", g_Lookback)); RefreshLines(); return; }
      if(obj == btn_lookback_plus)  { g_Lookback += 10; ObjectSetString(main_chart_id, lbl_lookback, OBJPROP_TEXT, StringFormat("Lookback:%d", g_Lookback)); RefreshLines(); return; }
      if(obj == btn_toggle_openclose) { g_ShowOpenClose = !g_ShowOpenClose; ObjectSetString(main_chart_id, btn_toggle_openclose, OBJPROP_TEXT, g_ShowOpenClose ? "Open/Close: ON" : "Open/Close: OFF"); RefreshLines(); return; }
      if(obj == btn_toggle_fill)      { g_ShowFill = !g_ShowFill; ObjectSetString(main_chart_id, btn_toggle_fill, OBJPROP_TEXT, g_ShowFill ? "Fill: ON" : "Fill: OFF"); RefreshLines(); return; }

      // major colors
      if(obj == btn_major_col1) { g_ColorMajor = major_colors[0]; RefreshLines(); return; }
      // ... other colors

      // minors toggles / tf dropdown
      if(obj == btn_minor1_toggle) { g_ShowMinor1 = !g_ShowMinor1; if(g_ShowMinor1) g_last_minor1_time = iTime(_Symbol, g_Minor1TF, 0); ObjectSetString(main_chart_id, btn_minor1_toggle, OBJPROP_TEXT, g_ShowMinor1 ? "Min1: ON" : "Min1: OFF"); RefreshLines(); return; }
      if(obj == btn_minor1_tf)     { ShowTFDropdownFor(1); return; }

      if(obj == btn_minor2_toggle) { g_ShowMinor2 = !g_ShowMinor2; if(g_ShowMinor2) g_last_minor2_time = iTime(_Symbol, g_Minor2TF, 0); ObjectSetString(main_chart_id, btn_minor2_toggle, OBJPROP_TEXT, g_ShowMinor2 ? "Min2: ON" : "Min2: OFF"); RefreshLines(); return; }
      if(obj == btn_minor2_tf)     { ShowTFDropdownFor(2); return; }

      if(obj == btn_clear_all) { DeleteAllHTFLines(); return; }

      // slider knob clicked -> begin dragging
      for(int s=0; s<SLIDER_COUNT; s++)
        { if(obj == g_slider_knob_names[s]) { g_current_slider = s; g_slider_drag = true; return; } }
     }
  }

11) OnTick()とOnTimer():効率的なリフレッシュトリガー

OnTick()は、設定された上位(major)および下位(minor)時間軸の最新iTimeをチェックし、新しいローソク足が出現したときのみneed_refreshを設定します。これにより、不要なオブジェクトの再作成を防ぎます。OnTimer()は、単純にg_RefreshSec間隔でRefreshLines()を呼び出します(スライダーでリアルタイムに変更可能です)。

迅速でありながら無駄のない更新が可能です。UI操作による変更(例:塗りつぶしのトグル)は即座にRefreshLines()を呼び出し、定期チェックは新しい上位足が出現した場合の対応をおこないます。

void OnTimer()
  {
   RefreshLines();
  }

void OnTick()
  {
   bool need_refresh = false;
   datetime curr;

   curr = iTime(_Symbol, g_HigherTF, 0);
   if(curr != g_last_major_time && curr != 0) { g_last_major_time = curr; need_refresh = true; }

   if(g_ShowMinor1)
     {
      curr = iTime(_Symbol, g_Minor1TF, 0);
      if(curr != g_last_minor1_time && curr != 0) { g_last_minor1_time = curr; need_refresh = true; }
     }

   if(g_ShowMinor2)
     {
      curr = iTime(_Symbol, g_Minor2TF, 0);
      if(curr != g_last_minor2_time && curr != 0) { g_last_minor2_time = curr; need_refresh = true; }
     }

   if(need_refresh) RefreshLines();
  }

12) RefreshLines():コア描画およびガーベジコレクション

これはメインルーチンです。以下をおこないます。

  • 指定されたルックバックに対して、HTFの時間、始値、終値をコピーする
  • 配列を昇順に反転し、シンプルな時間間隔比較を可能にする
  • 各上位時間軸について、垂直線、(任意の)塗りつぶし矩形、始値/終値の水平線、およびラベルを作成または更新する
  • 下位時間軸についてはDrawMinorsBetweenIntervals()を呼び出し、連続する上位時間軸の間(および現在進行中の区間を含む)に厳密に入る場合のみ、下位時間軸の垂直線を描画する
  • 必要なすべてのHTFオブジェクト名をkeepNames[]に構築し、最後にすべてのチャートオブジェクトを走査して、keepNames[]に含まれないHTFオブジェクトを削除する(ガーベジコレクション)

 各パスで既存オブジェクトのプロパティ(色、太さなど)を更新するため、削除や再作成をおこなわなくても、UIの変更が即座に反映されます。

void RefreshLines()
  {
   datetime major_times[]; ArrayFree(major_times);
   double major_opens[]; ArrayFree(major_opens);
   double major_closes[]; ArrayFree(major_closes);

   int copiedMaj = CopyTime(_Symbol, g_HigherTF, 0, g_Lookback, major_times);
   if(copiedMaj <= 0) { PrintFormat("RefreshLines: CopyTime majors returned %d for %s", copiedMaj, TFToString(g_HigherTF)); return; }
   if(CopyOpen(_Symbol, g_HigherTF, 0, copiedMaj, major_opens) != copiedMaj) { Print("RefreshLines: CopyOpen failed"); return; }
   if(CopyClose(_Symbol, g_HigherTF, 0, copiedMaj, major_closes) != copiedMaj) { Print("RefreshLines: CopyClose failed"); return; }

   // reverse to ascending order (oldest first)
   int n = copiedMaj;
   datetime sorted_times[]; ArrayResize(sorted_times, n);
   double sorted_opens[]; ArrayResize(sorted_opens, n);
   double sorted_closes[]; ArrayResize(sorted_closes, n);
   for(int k = 0; k < n; k++)
     {
      sorted_times[k]  = major_times[n - 1 - k];
      sorted_opens[k]  = major_opens[n - 1 - k];
      sorted_closes[k] = major_closes[n - 1 - k];
     }

   string keepNames[]; ArrayResize(keepNames, 0);

   // create/update major objects
   for(int i = 0; i < n; ++i)
     {
      datetime t = sorted_times[i];
      double p_open = sorted_opens[i];
      double p_close = sorted_closes[i];

      // Major vertical
      string name = PREFIX_MAJ + TFToString(g_HigherTF) + "_" + IntegerToString((int)t);
      if(ObjectFind(0, name) == -1)
        {
         double dummy_price = 0.0;
         if(!ObjectCreate(0, name, OBJ_VLINE, 0, t, dummy_price))
            PrintFormat("Failed to create major %s error %d", name, GetLastError());
        }
      // update props each pass so UI changes apply immediately
      ObjectSetInteger(0, name, OBJPROP_COLOR, g_ColorMajor);
      ObjectSetInteger(0, name, OBJPROP_WIDTH, g_WidthMajor);
      ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_SOLID);
      ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
      ObjectSetInteger(0, name, OBJPROP_HIDDEN, false);
      ObjectSetInteger(0, name, OBJPROP_BACK, true); // draw in background

      int sz = ArraySize(keepNames); ArrayResize(keepNames, sz + 1); keepNames[sz] = name;

      // Optional fill / open-close code follows (omitted for brevity)
     }

   if(n >= 2)
     {
      if(g_ShowMinor1) DrawMinorsBetweenIntervals(PREFIX_MIN1, g_Minor1TF, g_ColorMin1, g_WidthMin1, sorted_times, keepNames);
      if(g_ShowMinor2) DrawMinorsBetweenIntervals(PREFIX_MIN2, g_Minor2TF, g_ColorMin2, g_WidthMin2, sorted_times, keepNames);
     }

   // cleanup old HTF objects not in keepNames
   int total = ObjectsTotal(0);
   for(int idx = total - 1; idx >= 0; --idx)
     {
      string oname = ObjectName(0, idx);
      if(StringFind(oname, "HTF_") != -1)
        {
         bool found = false;
         for(int k=0;k<ArraySize(keepNames);k++) if(oname == keepNames[k]) { found = true; break; }
         if(!found) ObjectDelete(0, oname);
        }
     }
   UpdateLabelsAfterSliderChange();
   ChartRedraw(main_chart_id);
  }

13) DrawMinorsBetweenIntervals():正確な下位時間軸配置ロジック

この関数は、上位時間軸の最初の時刻から現在までの時間幅に基づいて要求する下位足の数を計算し、下位時間軸の時間をコピーして昇順に反転し、その時間が連続する上位時間軸の時間の厳密に含まれる場合にのみ、垂直線を配置します。これはインジケーターの挙動をそのまま反映しています。また、上位時間軸の最後の時刻以降の、現在進行中の上位時間軸区間に含まれる下位時間軸も処理対象とします。

設計上の詳細として、アライメントずれに対して堅牢にするため、approx_barsにマージン(+20)を追加しています。また、少なくともg_Lookback本のローソク足を要求するようにしています。

void DrawMinorsBetweenIntervals(const string prefix,
                                const ENUM_TIMEFRAMES minorTF,
                                const color c,
                                const int width,
                                const datetime &major_times[],
                                string &keepNames[])
  {
   datetime current_minor_time = iTime(_Symbol, minorTF, 0);
   if(current_minor_time == 0) return;
   int time_span = (int)(current_minor_time - major_times[0]);
   int minor_sec = PeriodSeconds(minorTF);
   int approx_bars = (time_span / minor_sec) + 20;
   if(approx_bars < g_Lookback) approx_bars = g_Lookback;

   datetime minor_times[]; ArrayFree(minor_times);
   int copiedMin = CopyTime(_Symbol, minorTF, 0, approx_bars, minor_times);
   PrintFormat("DrawMinorsBetweenIntervals: TF=%s copiedMin=%d majors=%d", TFToString(minorTF), copiedMin, ArraySize(major_times));
   if(copiedMin <= 0) return;

   datetime sorted_minor_times[]; ArrayResize(sorted_minor_times, copiedMin);
   for(int k = 0; k < copiedMin; k++) sorted_minor_times[k] = minor_times[copiedMin - 1 - k];

   int num_maj = ArraySize(major_times);
   for(int m = 0; m < copiedMin; ++m)
     {
      datetime mt = sorted_minor_times[m];
      bool equals_major = false;
      for(int kk = 0; kk < num_maj; ++kk) if(major_times[kk] == mt) { equals_major = true; break; }
      if(equals_major) continue;

      bool placed = false;
      for(int j = 0; j < num_maj - 1; ++j)
        {
         if( major_times[j] < mt && mt < major_times[j+1] )
           {
            string name = prefix + TFToString(minorTF) + "_" + IntegerToString((int)mt);
            if(ObjectFind(0, name) == -1)
              {
               double dummy_price = 0.0;
               if(!ObjectCreate(0, name, OBJ_VLINE, 0, mt, dummy_price))
                 PrintFormat("Failed to create minor %s error %d", name, GetLastError());
               else
                 {
                  ObjectSetInteger(0, name, OBJPROP_COLOR, c);
                  ObjectSetInteger(0, name, OBJPROP_WIDTH, width);
                  ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_DOT);
                  ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
                  ObjectSetInteger(0, name, OBJPROP_HIDDEN, false);
                  ObjectSetInteger(0, name, OBJPROP_BACK, true);
                 }
              }
            int sz = ArraySize(keepNames); ArrayResize(keepNames, sz + 1); keepNames[sz] = name;
            placed = true;
            break;
           }
        }
      if(!placed && mt > major_times[num_maj - 1])
        {
         // create a minor in the ongoing major interval
         string name = prefix + TFToString(minorTF) + "_" + IntegerToString((int)mt);
         if(ObjectFind(0, name) == -1)
           {
            double dummy_price = 0.0;
            if(!ObjectCreate(0, name, OBJ_VLINE, 0, mt, dummy_price))
              PrintFormat("Failed to create minor (current) %s error %d", name, GetLastError());
            else
              {
               ObjectSetInteger(0, name, OBJPROP_COLOR, c);
               ObjectSetInteger(0, name, OBJPROP_WIDTH, width);
               ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_DOT);
               ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
               ObjectSetInteger(0, name, OBJPROP_HIDDEN, false);
               ObjectSetInteger(0, name, OBJPROP_BACK, true);
              }
           }
         int sz = ArraySize(keepNames); ArrayResize(keepNames, sz + 1); keepNames[sz] = name;
         placed = true;
        }
     }
   ChartRedraw(main_chart_id);
  }

14) 削除とクリーンアップ:DeleteAllHTFLines()およびOnDeinit()

DeleteAllHTFLines()はHTFオブジェクトのみを削除します。OnDeinit()はUIオブジェクトとスライダーコンポーネントを削除し、ドロップダウンを非表示にしてキャンバスを破棄します。意図的に、OnDeinit()ではデフォルトでHTFオブジェクトを削除しません(Clear HTFが押された場合を除く)。これは、ユーザーが希望すれば描画を保持できるようにするためです。

void DeleteAllHTFLines()
  {
   int total = ObjectsTotal(0);
   for(int idx = total - 1; idx >= 0; --idx)
     {
      string oname = ObjectName(0, idx);
      if(StringFind(oname, "HTF_") != -1)
         ObjectDelete(0, oname);
     }
  }

void OnDeinit(const int reason)
  {
   EventKillTimer();

   string names[] = {
     lbl_title, btn_major_tf, lbl_major_tf, btn_lookback_minus, lbl_lookback, btn_lookback_plus,
     btn_toggle_openclose, btn_toggle_fill, btn_major_col1, btn_major_col2, btn_major_col3, btn_major_col4,
     btn_minor1_toggle, btn_minor1_tf, btn_minor2_toggle, btn_minor2_tf,
     btn_clear_all, lbl_major_width, lbl_refresh_label
   };
   for(int i=0;i<ArraySize(names);i++) if(StringLen(names[i])>0) ObjectDelete(main_chart_id, names[i]);

   for(int s=0; s<SLIDER_COUNT; s++)
     {
      if(StringLen(g_slider_track_names[s])>0) ObjectDelete(main_chart_id, g_slider_track_names[s]);
      if(StringLen(g_slider_knob_names[s])>0)  ObjectDelete(main_chart_id, g_slider_knob_names[s]);
     }

   if(g_tf_dropdown_visible) HideTFDropdown();

   // destroy canvas
   g_bgCanvas.Destroy();
  }


テスト

コンパイルが正常に完了した後、EAをMetaTrader 5のライブチャート上でテストしました。重要な注意点として、CCanvasクラスはMQL5標準ライブラリの一部であるため、コンパイルエラーを回避するにはインクルードパスを正しく定義する必要があります。EAをチャートに適用すると、スムーズに初期化され、想定どおりコントロールダッシュボードが表示されました。

インターフェースは整然と描画され、すべてのスイッチ、スライダー、ラベルがキャンバス背景上に正しく配置されていました。テスト中は各コントロールがリアルタイムに反応し、スライダーは線の太さや更新間隔などの視覚的パラメータを動的に調整し、スイッチは表示や塗りつぶし機能を即座に切り替えました。その結果、リアルタイムのパラメータ制御という概念を体感できる、高い応答性とインタラクティブ性を備えた操作体験が実現しました。

以下の画像は、EAが正常に展開され、制御プロセスがアクティブに動作している様子を示しており、設計の安定性と実行精度を確認できます。

図3:ライブチャート上でのコントロールのテスト

このプロジェクトで特に興味深かった点の一つは、下位足チャート上で、上位足実体領域の塗りつぶし効果がリアルタイムに形成されていく様子を観察できたことです。従来のバージョンでは、ビジュアルを更新するためにインジケーターの再初期化が必要でしたが、この実装では、上位足の構造が動的に進化していく過程をそのまま確認できます。これにより、下位足の各値動きがどのように上位足の形成に寄与しているかを継続的かつ明確に把握でき、市場構造やモメンタムの形成過程について、より深い洞察を得ることができます。


結論

この探究は、MQL5言語との継続的な歩みにおいて、技術的かつ創造的なマイルストーンとなりました。チャート操作性を向上させたいという素朴な好奇心から始まった取り組みは、入力調整と可視化へのアプローチを再定義する、本格的なシステムへと発展しました。この開発を通じて、MQL5が単なる取引自動化のための言語ではなく、ユーザー体験を設計するためのキャンバスでもあることを学びました。インジケーターの静的な入力パラメータを、動的で応答性の高いリアルタイム制御環境へと変換できる力を備えています。

EAを通じたオブジェクト操作とグラフィカルインターフェースを活用することで、システムの視覚的要素と論理的要素を、チャート上から直接制御できることを実証しました。このブレイクスルーは、分析と操作の間にある隔たりを埋め、トレーダーと開発者に、スピード、精度、そして創造性という新たな次元をもたらします。リアルタイム制御により、複数の設定、時間足、可視化モードのテストが滑らかにおこなえるようになり、文脈を保ったまま、より迅速な意思決定を支援します。

Market Periods Synchronizer Control Utilityは、この思想をさらに推し進め、下位足の挙動が上位足の構造にどのように影響するかを観察できるようにします。これはマルチタイムフレーム分析に深みを与え、市場の鼓動を異なるスケールで捉え、小さな価格変動がどのように主要なローソク足の形を作り上げているのかを理解する助けとなります。本質的には、プライスアクション解釈に対して、より科学的なアプローチを促すものです。何が起きたのかだけでなく、なぜ、そしてどのように形成されたのかを見ることができます。

しかし、これはまだ始まりにすぎません。ここで探究したアイデアは、このユーティリティの範囲をはるかに超えて拡張できます。将来的な改良として、カスタムチャートコントロール、複数銘柄の同期、分析用データエクスポート、AIによる可視化チューニングなどが考えられます。MQL5の魅力は、その開放性にあります。実験、創造性、そして既成概念にとらわれない探究心に応えてくれます。

ぜひ、このアイデアを自身のプロジェクトで試してみてください。適応し、改変し、既存のツールと統合し、新たな可能性がどこまで広がるのかを発見してください。皆さまのフィードバックやアイデア、貢献は非常に貴重ですので、ぜひ下のコメント欄でご意見やご提案を共有してください。共に、MQL5コミュニティを、学びと成長のための、より活気に満ちた革新的で協調的な場へと育てていきましょう。

最後に、本探究から得られた主要なポイントをまとめたサマリーテーブルと、ダウンロードして学習し、拡張できるソースファイルを添付しています。覚えておいてください。何かを習得する最良の方法は、それを土台にして作り上げることです。大胆に実験し、深く学び、あなたの成果で他者を刺激してください。


重要な学び

学び説明
1. リアルタイム制御は可能です。構造化されたイベント処理とオブジェクト更新により、ユーザー入力に即座に反応するインターフェースを構築でき、チャート上で即時のフィードバックと視覚的更新が可能になります。
2. CCanvasは視覚的創造性を解放します。Canvasクラスは、プロフェッショナルで視覚的に魅力あるダッシュボードを設計する能力を提供します。背景描画、透過、レイヤリングを活用することで、ユーザー操作性を向上させることができます。
3.UIの応答性はオブジェクト管理に依存します。チャートオブジェクトの生成、更新、削除を適切に処理することで、リアルタイム更新中のパフォーマンスを滑らかに保ち、混雑や遅延を防ぐことができます。
4. 動的パラメータは試行錯誤を促進します。時間足、色、線幅などの設定を、プロパティウィンドウを再度開くことなく調整できるようにすることで、実験と分析の効率が大幅に向上します。
5. マルチタイムフレーム同期は分析に深みを加えます。上位と下位の時間足データを視覚的に組み合わせることで、市場内部構造を理解しやすくなり、微細な動きと大きな形成との関連を把握できます。
6. イベント駆動設計はインタラクティブツールの中核です。インタラクティブなシステムを構築するには、チャートイベント処理への深い理解が必要です。すべてのユーザー操作を明確なプログラム応答に対応付けることで、直感的な操作が実現します。
7. オブジェクトのレイヤリングは使いやすさを向上させます。背景と前景のプロパティを慎重に設定することで、インターフェースの応答性を保ち、重要な操作がオブジェクトに遮られることを防げます。
8. モジュール化されたコードは保守性を高めます。UI作成、イベント処理、描画関数などに論理的に分割することで、拡張、デバッグ、再利用が容易になります。
9. 可視化ツールのテストとデバッグ。リアルタイムの視覚テストにより、更新されないオブジェクトや重なりの問題が明らかになり、ログやイベント追跡を用いた体系的なデバッグ手法を学びました。
10. 更新と描画間隔の最適化。更新タイマーや再描画戦略を調整することで、特にリアルタイムチャート更新時のパフォーマンスと応答性を改善できることを学びました。
11. 視覚的フィードバックは分析への信頼を高めます。リアルタイムの視覚調整により、原因と結果を即座に確認でき、チャートデータの正確性と解釈に対する自信が高まります。
12. 実験こそが革新を生みます。本プロジェクトは、MQL5において既成概念にとらわれないアプローチを探究することで、新しいツールや発想が生まれることを示しました。創造性と技術理解の融合が、コミュニティ全体の進歩につながります。


添付ファイル

ファイル名バージョン説明
MarketPeriodsSynchronizer_EA.mq5
1.00このEAは、複数の時間足周期をチャート上で直接同期し、可視化するための、リアルタイム制御ダッシュボードを提供します。元のMarket Periods Synchronizerインジケーターの機能を拡張し、入力設定を開き直すことなく、インタラクティブなコントロール、スライダー、スイッチによってパラメータを即座に調整できるようにしています。

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

添付されたファイル |
EAのサンプル EAのサンプル
一般的なMACDを使ったEAを例として、MQL4開発の原則を紹介します。
リスク管理(第1回):リスク管理クラス構築の基礎 リスク管理(第1回):リスク管理クラス構築の基礎
本記事では、取引におけるリスク管理の基礎を解説し、適切なロットサイズやストップロスを計算するための最初の関数の作成方法を学びます。さらに、これらの機能がどのように動作するのかを、各ステップを追いながら詳しく説明します。本記事の目的は、自動売買においてこれらの概念をどのように適用するかを明確に理解することです。最後に、インクルードファイルを使用したシンプルなスクリプトを作成し、すべてを実践に落とし込みます。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
取引におけるニューラルネットワーク:金融市場向けマルチモーダルツール拡張エージェント(FinAgent) 取引におけるニューラルネットワーク:金融市場向けマルチモーダルツール拡張エージェント(FinAgent)
FinAgentを紹介します。FinAgentは、マーケットの動向や過去の取引パターンを反映するさまざまなタイプのデータを分析できるマルチモーダル金融取引エージェントのフレームワークです。