English Deutsch
preview
MQL5経済指標カレンダーを使った取引(第9回):動的スクロールバーと洗練表示によるニュースインタラクション強化

MQL5経済指標カレンダーを使った取引(第9回):動的スクロールバーと洗練表示によるニュースインタラクション強化

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

はじめに

本記事では、連載「MQL5経済指標カレンダーを使った取引」をさらに発展させ、垂直方向の動的スクロールバーと洗練された表示を導入することで、トレーダーがニュースイベントとやり取りする際の利便性を向上させます。これにより、直感的なナビゲーションと信頼性の高いイベント表示が可能になります。第8回の最適化されたバックテストとスマートフィルタリングを基に、クリック可能な状態を視覚的に示すスクロールバーを備えた応答性の高いユーザーインターフェース(UI)に重点を置き、ライブ環境とバックテスト環境の両方で経済ニュースへのアクセスを合理化します。この記事は次のトピックで構成されています。

  1. ニュース探索を容易にする動的スクロールバーの作成
  2. MQL5での実装
  3. テストと検証
  4. 結論

これらの機能強化について詳しく見ていきましょう。


ニュース探索を容易にする動的スクロールバーの作成

MQL5経済指標カレンダーとのインタラクションを向上させるために、動的スクロールバーを直感的なニュースナビゲーションの基盤として設計します。今回の目標は、クリック可能な状態を視覚的に示す応答性の高い縦スクロールバーと、すべてのフィルタ済みニュースを確実に保持する堅牢なイベントストレージシステムを組み合わせることです。これにより、ダッシュボードは流れるようなトレーダー中心のツールへと進化します。従来の最小表示イベント数制限を撤廃し、フィルタ済みすべてのイベントをリスト化することで、必要に応じてスクロールして全ニュースを閲覧可能にします。これにより、優先イベントのみを表示する制限も解消されます。これを実現する方法は次のとおりです。

  • 動的スクロールバーのデザイン:アイコンがクリック可能状態では黒、非クリック状態ではライトグレーに変化するスクロールバーを実装し、大量のイベントリストをスムーズにナビゲートできる即時フィードバックを提供します。
  • 信頼性の高いイベントストレージすべてのフィルタ済みニュースを格納するシステムを構築し、スクロールバーから全ニュースにアクセス可能にして表示制限を排除し、網羅的なニュースビューを確保します。
  • 効率的な更新メカニズム:フィルタ変更、新規イベントの出現、スクロール時のみダッシュボードを再描画し、滑らかで集中力を妨げない操作性を維持します。
  • 洗練されたUIの調整:精密なレイアウト調整により、スクロールバーとイベント表示の統合を実現し、プロフェッショナルでユーザーフレンドリーなデザインにします。

この戦略的ロードマップにより、経済ニュースを自在に探索できるダッシュボードを構築します。動的スクロールバーは、洗練された取引体験を解放する鍵となります。以下のようなイメージを目指しています。

ビジュアルプラン


MQL5での実装

MQL5でこれらの改良をおこなうには、まず追加のスクロールバーオブジェクトを#defineディレクティブで定義し、それに対応するレイアウト定数も設定する必要があります。さらに、スクロール状態や処理対象のイベントを追跡するための変数を用意し、スクロールや描画の対象となるイベントデータを格納する配列を作成します。これにより、ダッシュボード上でイベントデータをシームレスに処理・表示できる基盤が整います。

// Scrollbar UI elements
#define SCROLL_UP_REC "SCROLL_UP_REC"
#define SCROLL_UP_LABEL "SCROLL_UP_LABEL"
#define SCROLL_DOWN_REC "SCROLL_DOWN_REC"
#define SCROLL_DOWN_LABEL "SCROLL_DOWN_LABEL"
#define SCROLL_LEADER "SCROLL_LEADER"
#define SCROLL_SLIDER "SCROLL_SLIDER"

//---- Scrollbar layout constants
#define LIST_X 62
#define LIST_Y 162
#define LIST_WIDTH 716
#define LIST_HEIGHT 286
#define VISIBLE_ITEMS 11
#define ITEM_HEIGHT 26
#define SCROLLBAR_X (LIST_X + LIST_WIDTH + 2) // 780
#define SCROLLBAR_Y LIST_Y
#define SCROLLBAR_WIDTH 20
#define SCROLLBAR_HEIGHT LIST_HEIGHT // 286
#define BUTTON_SIZE 15
#define BUTTON_WIDTH (SCROLLBAR_WIDTH - 2)
#define BUTTON_OFFSET_X 1
#define SCROLL_AREA_HEIGHT (SCROLLBAR_HEIGHT - 2 * BUTTON_SIZE)
#define SLIDER_MIN_HEIGHT 20
#define SLIDER_WIDTH 18
#define SLIDER_OFFSET_X 1

//---- Event name tracking
string current_eventNames_data[];
string previous_eventNames_data[];
string last_dashboard_eventNames[];
string previous_displayable_eventNames[];
string current_displayable_eventNames[];
datetime last_dashboard_update = 0;

//---- Filter flags
bool enableCurrencyFilter = true;
bool enableImportanceFilter = true;
bool enableTimeFilter = true;
bool isDashboardUpdate = true;
bool filters_changed = true;

//---- Scrollbar flags and variables
bool scroll_visible = false;
bool moving_state_slider = false;
int scroll_pos = 0;
int prev_scroll_pos = -1; // Track previous scroll position
int mlb_down_x = 0;
int mlb_down_y = 0;
int mlb_down_yd_slider = 0;
int prev_mouse_state = 0;
int slider_height = SLIDER_MIN_HEIGHT;

//---- Event counters
int totalEvents_Considered = 0;
int totalEvents_Filtered = 0;
int totalEvents_Displayable = 0;

//---- Global arrays for events
EconomicEvent allEvents[];
EconomicEvent filteredEvents[];
EconomicEvent displayableEvents[];

ここでは、MQL5経済指標カレンダーの動的スクロールバーと洗練されたイベント表示の基盤を構築するために、直感的なナビゲーションと効率的なイベント処理を可能にする基本的なUI定数、変数、配列を定義します。スクロールバーの構成要素は、スクロールアップ・ダウンボタンのグラフィカル要素を識別するための定数、たとえばSCROLL_UP_LABEL、SCROLL_DOWN_LABEL、SCROLL_UP_REC、SCROLL_DOWN_RECを使用して確立します。

イベント表示領域はLIST_X(62)、LIST_Y(162)、LIST_WIDTH(716)、LIST_HEIGHT(286)などのレイアウト定数で定義し、スクロールバーはSCROLLBAR_X(780)、SCROLLBAR_Y(162)、SCROLLBAR_WIDTH(20)、SCROLLBAR_HEIGHT(286)で正確に配置します。さらに、VISIBLE_ITEMS(11)とITEM_HEIGHT(26)により、26ピクセル高さのイベントを11件表示でき、BUTTON_SIZE(15)とSLIDER_WIDTH(18)がコンパクトなボタンとスライダーの形状を決定します。

イベントとインタラクションを管理するために、配列current_displayable_eventNamesやprevious_displayable_eventNamesを宣言し、イベント名の変化を追跡してサイレントアップデートをサポートします。また、last_dashboard_updateでダッシュボードの最終更新時刻を記録します。イベント選択を制御するフィルタフラグとして、enableCurrencyFilter、enableImportanceFilter、enableTimeFilter(すべてtrue)があり、更新のタイミングはisDashboardUpdateやfilters_changedによって決定されます。スクロールバーの変数としては、表示・非表示と位置を管理するscroll_visible、scroll_pos、prev_scroll_pos、moving_state_sliderがあり、mlb_down_x、mlb_down_y、slider_heightがスライダーのドラッグ操作を可能にします。

イベント処理の進行状況を把握するために、totalEvents_Considered、totalEvents_Filtered、totalEvents_Displayableといったカウンターを使用し、イベントデータを格納する配列としてallEvents、filteredEvents、displayableEventsを用意します。これにより、フィルタ済みのニュースがすべてスクロール可能となり、応答性の高いトレーダー向けインターフェースの土台が整います。次のステップとして、既存の作成関数を用いてスクロールバーの要素を作成し、メイン矩形のサイズと位置を調整してスクロールバーが右側に収まるように設定します。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   // Enable mouse move events for scrollbar
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);

   // Create dashboard UI
   createRecLabel(MAIN_REC,50,50,740+13,410,clrSeaGreen,1);
   createRecLabel(SUB_REC1,50+3,50+30,740-3-3+13,410-30-3,clrWhite,1);
   createRecLabel(SUB_REC2,50+3+5,50+30+50+27,740-3-3-5-5,410-30-3-50-27-10+5,clrGreen,1);
   createLabel(HEADER_LABEL,50+3+5,50+5,"MQL5 Economic Calendar",clrWhite,15);

   //---

}

ここで、OnInitイベントハンドラ内でChartSetInteger関数を使用し、マウス移動イベントをtrueに初期化します。これにより、メインチャートとカスタムオブジェクトのスクロール優先度を制御でき、縦スクロールバーおよびその要素をスムーズにスクロールできるようになります。次に、メイン矩形とサブ矩形1の幅を13ピクセル追加して調整し、縦スクロールバーも収まるようにします。さらに、サブ矩形2の高さを5ピクセル拡張することで、11件のイベントを効率的に収容し、オーバーフローを防ぎます。コンパイルすると、次の結果が得られます。

変更

画像から、私たちがおこなった調整変更がすべて確認できます。それぞれ1から3までラベル付けされています。変更1は、キャンセルボタンをパネルの端まで押し込んだ位置を示しており、変更2はキャンセルボタンとスクロールバーを収めるためにメイン矩形の幅を調整したことを示しています。変更3は、最後の要素行ホルダーのオーバーフローに対応するために、ダッシュボード矩形の高さを調整したことを示しています。これで、作成したスペース内にスクロールバーを定義・作成する準備が整いました。しかし、スライダーを動的に表示させ、必要なときのみ見せる仕様にしたいため、そのための関数をいくつか定義する必要があります。

//+------------------------------------------------------------------+
//| Calculate slider height                                          |
//+------------------------------------------------------------------+
int calculateSliderHeight() {
   if (totalEvents_Filtered <= VISIBLE_ITEMS)
      return SCROLL_AREA_HEIGHT;
   double visible_ratio = (double)VISIBLE_ITEMS / totalEvents_Filtered;
   int height = (int)::floor(SCROLL_AREA_HEIGHT * visible_ratio);
   return MathMax(SLIDER_MIN_HEIGHT, MathMin(height, SCROLL_AREA_HEIGHT));
}

//+------------------------------------------------------------------+
//| Update slider position                                           |
//+------------------------------------------------------------------+
void updateSliderPosition() {
   int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
   if (max_scroll <= 0) return;
   double scroll_ratio = (double)scroll_pos / max_scroll;
   int scroll_area_y_min = SCROLLBAR_Y + BUTTON_SIZE;
   int scroll_area_y_max = scroll_area_y_min + SCROLL_AREA_HEIGHT - slider_height;
   int new_y = scroll_area_y_min + (int)(scroll_ratio * (scroll_area_y_max - scroll_area_y_min));
   ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE, new_y);
   if (debugLogging) Print("Slider moved to y=", new_y);
   ChartRedraw(0);
}

//+------------------------------------------------------------------+
//| Update button colors based on scroll position                    |
//+------------------------------------------------------------------+
void updateButtonColors() {
   int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
   if (scroll_pos == 0) {
      ObjectSetInteger(0, SCROLL_UP_LABEL, OBJPROP_COLOR, clrLightGray);
   } else {
      ObjectSetInteger(0, SCROLL_UP_LABEL, OBJPROP_COLOR, clrBlack);
   }
   if (scroll_pos >= max_scroll) {
      ObjectSetInteger(0, SCROLL_DOWN_LABEL, OBJPROP_COLOR, clrLightGray);
   } else {
      ObjectSetInteger(0, SCROLL_DOWN_LABEL, OBJPROP_COLOR, clrBlack);
   }
   ChartRedraw(0);
}

MQL5経済指標カレンダーの動的スクロールバーをさらに強化するために、直感的なナビゲーションと明確な視覚フィードバックを提供する3つの主要な関数(calculateSliderHeight、updateSliderPosition、updateButtonColors)を実装します。calculateSliderHeight関数では、表示中のイベント数がフィルタ済み全イベントに占める割合を視覚的に表すために、スクロールバーのスライダーの高さを決定します。

まず、totalEvents_FilteredがVISIBLE_ITEMS(11)以下の場合、スクロールエリアに収まるため、SCROLL_AREA_HEIGHT(256ピクセル)を返します。それ以外の場合は、VISIBLE_ITEMS ÷ totalEvents_Filteredでvisible_ratioを計算し、これにSCROLL_AREA_HEIGHTを掛けて高さを算出、floor関数で整数化します。最終的に、スライダーの高さはSLIDER_MIN_HEIGHT(20ピクセル)以上、SCROLL_AREA_HEIGHT以下に調整され、イベントリストの規模に応じた適切なサイズとなります。

updateSliderPosition関数では、イベントリスト内の現在のスクロール位置に応じてスライダーの位置を調整します。まず、max_scrollをArraySize(displayableEvents) − VISIBLE_ITEMSで計算し、MathMaxで0未満にならないよう処理します。max_scrollが0の場合はスクロール不要のため終了します。次に、scroll_pos ÷ max_scrollでscroll_ratioを算出し、スライダーの垂直範囲をscroll_area_y_min = SCROLLBAR_Y + BUTTON_SIZE、scroll_area_y_max = scroll_area_y_min + SCROLL_AREA_HEIGHT − slider_heightで定義します。範囲内での補間により、新しいY座標new_yを求めます。

その後、ObjectSetIntegerでSCROLL_SLIDERのOBJPROP_YDISTANCEをnew_yに設定し、debugLoggingがtrueの場合は移動をログ出力、最後にChartRedrawで表示を更新します。

updateButtonColors関数では、スクロールバーの上・下ボタンアイコンの色を動的に更新し、クリック可能状態を示すことでユーザーへのフィードバックを強化します。updateSliderPositionと同様にmax_scrollを計算し、scroll_posを確認してSCROLL_UP_LABELとSCROLL_DOWN_LABELの状態を決定します。scroll_pos = 0の場合、SCROLL_UP_LABELをclrLightGray(非クリック、トップ位置)に設定し、それ以外はclrBlack(クリック可能)に設定します。同様に、scroll_pos ≥ max_scrollの場合はSCROLL_DOWN_LABELをclrLightGray(非クリック、ボトム位置)に設定し、それ以外はclrBlackに設定します。

最後にChartRedrawを呼び出してチャートを更新し、アイコンが視覚的にナビゲーションをガイドするようにします。これにより、ダッシュボードの値を更新する際にスライダーを動的に生成できる準備が整います。

// Update TIME_LABEL
string timeText = updateServerTime ? "Server Time: "+TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS) : "Server Time: Static";
updateLabel(TIME_LABEL,timeText+"   |||   Total News: "+IntegerToString(totalEvents_Filtered)+"/"+IntegerToString(totalEvents_Considered));

// Update scrollbar visibility
bool new_scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS;
if (new_scroll_visible != scroll_visible || events_changed || filters_changed) {
   scroll_visible = new_scroll_visible;
   if (debugLogging) Print("Scrollbar visibility: ", scroll_visible ? "Visible" : "Hidden");
   if (scroll_visible) {
      if (ObjectFind(0, SCROLL_LEADER) < 0) {
         createRecLabel(SCROLL_LEADER, SCROLLBAR_X, SCROLLBAR_Y, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, clrSilver, 1, clrNONE);
         int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
         color up_color = (scroll_pos == 0) ? clrLightGray : clrBlack;
         color down_color = (scroll_pos >= max_scroll) ? clrLightGray : clrBlack;
         createRecLabel(SCROLL_UP_REC, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
         createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-5, CharToString(0x35), up_color, 15, "Webdings");
         int down_y = SCROLLBAR_Y + SCROLLBAR_HEIGHT - BUTTON_SIZE;
         createRecLabel(SCROLL_DOWN_REC, SCROLLBAR_X + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
         createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings");
         slider_height = calculateSliderHeight();
         int slider_y = SCROLLBAR_Y + BUTTON_SIZE;
         createButton(SCROLL_SLIDER, SCROLLBAR_X + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, 12, clrLightSlateGray, clrDarkGray, "Arial Bold");
         ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_WIDTH, 2);
         if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height);
      }
      updateSliderPosition();
      updateButtonColors();
   } else {
      ObjectDelete(0, SCROLL_LEADER);
      ObjectDelete(0, SCROLL_UP_REC);
      ObjectDelete(0, SCROLL_UP_LABEL);
      ObjectDelete(0, SCROLL_DOWN_REC);
      ObjectDelete(0, SCROLL_DOWN_LABEL);
      ObjectDelete(0, SCROLL_SLIDER);
      if (debugLogging) Print("Scrollbar removed: totalEvents_Filtered=", totalEvents_Filtered);
   }
}

ダッシュボードの時刻表示と動的スクロールバーに重要な更新を加えることで、ニュースイベントとのシームレスなインタラクションとナビゲーションを可能にするユーザーインターフェースを強化します。まず、TIME_LABELをリアルタイムのサーバー時刻とイベント統計に更新し、ダッシュボードの状態を明確にフィードバックします。さらに、スクロールバーの表示・非表示を管理し、アイコン色を動的に初期化することで、直感的なナビゲーションシステムを構築します。

まず、TIME_LABELの更新に注目します。現在時刻とイベント処理状況を常に把握できるように、条件式でtimeText文字列を作成します。updateServerTimeがtrueの場合はTimeToString関数をTimeCurrentとフラグTIME_DATE|TIME_SECONDSで呼び出してサーバー時刻を整形します。falseの場合は「Server Time:Static」と設定します。その後、updateLabel関数でTIME_LABELのテキストをtimeTextと区切り文字「 ||| 」およびtotalEvents_FilteredとtotalEvents_ConsideredをIntegerToStringで結合して設定します。これにより、表示はたとえば「Server Time: 2025.03.01 12:00:00   |||   Total News:1711/3000」のようになり、フィルタ済みイベントと全イベント数を明確に表示し、従来バージョンで行っていた表示可能ニュースの制限を排除します。

次に、スクロールバーの表示ロジックとコンポーネント作成を実装します。スクロールバーは必要なときのみ表示され、視覚的フィードバックを提供します。まず、totalEvents_Filtered > VISIBLE_ITEMS(11)でnew_scroll_visibleを判定し、一度に表示できるイベント以上であればスクロールバーを表示する必要があります。new_scroll_visibleがscroll_visibleと異なる場合、またはevents_changedやfilters_changedがtrueの場合、scroll_visibleを更新し、debugLoggingが有効ならPrintでログを残します。scroll_visibleがtrueの場合、ObjectFindでSCROLL_LEADERが存在するか確認し、存在しなければスクロールバーを作成します。createRecLabelを使用して、SCROLL_LEADER、SCROLL_UP_REC、SCROLL_DOWN_RECをSCROLLBAR_X、SCROLLBAR_Y、BUTTON_OFFSET_X、BUTTON_SIZEに従い配置し、色はclrSilverとclrDarkGrayを使用します。

次に、max_scrollをMathMax(ArraySize(displayableEvents) − VISIBLE_ITEMS, 0)で計算し、scroll_posとmax_scrollに応じてup_color、down_colorをclrLightGrayまたはclrBlackに設定します。SCROLL_UP_LABELとSCROLL_DOWN_LABELはcreateLabelで作成し、Webdings矢印はCharToString(5)とCharToString(6)を使用します。

Webフォント

補足として、なぜ0x35を使用したかというと、これは16進数形式での指定であり、文字列「5」として使用しても結果は同じです。ASCII 文字コードを使用したい場合は、53をCharToString(53)などの文字列に直接変換しても同様に動作します。選択肢は広く、用途に応じて選べます。ここにコードの情報があります。

6進数0x35を5として

その後、calculateSliderHeightでslider_heightを計算し、createButtonでSCROLL_SLIDERをslider_yに作成、OBJPROP_WIDTHを2に設定し、debugLoggingが有効なら詳細をログに出力します。さらに、updateSliderPositionとupdateButtonColorsを呼び出してスライダーとアイコンの初期状態を設定します。もしscroll_visibleがfalseの場合、ObjectDeleteを使用してSCROLL_LEADER、SCROLL_UP_REC、SCROLL_DOWN_REC、SCROLL_UP_LABEL、SCROLL_DOWN_LABEL、SCROLL_SLIDERを削除し、スクロールが不要な場合にクリーンなインターフェースを維持します。このロジックを含む関数と、イベントを格納する関数の残りの処理は以下の通りです。

//+------------------------------------------------------------------+
//| Update dashboard values                                          |
//+------------------------------------------------------------------+
void update_dashboard_values(string &curr_filter_array[], ENUM_CALENDAR_EVENT_IMPORTANCE &imp_filter_array[]) {
   totalEvents_Considered = 0;
   totalEvents_Filtered = 0;
   totalEvents_Displayable = 0;
   ArrayFree(current_eventNames_data);
   ArrayFree(current_displayable_eventNames);

   datetime timeRange = PeriodSeconds(range_time);
   datetime timeBefore = TimeTradeServer() - timeRange;
   datetime timeAfter = TimeTradeServer() + timeRange;

   // Populate displayableEvents
   if (MQLInfoInteger(MQL_TESTER)) {
      if (filters_changed) {
         FilterEventsForTester();
         ArrayFree(displayableEvents); // Clear displayableEvents on filter change
      }
      int eventIndex = 0;
      for (int i = 0; i < ArraySize(filteredEvents); i++) {
         totalEvents_Considered++;
         datetime eventDateTime = filteredEvents[i].eventDateTime;
         if (eventDateTime < StartDate || eventDateTime > EndDate) {
            if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to date range.");
            continue;
         }

         bool timeMatch = !enableTimeFilter;
         if (enableTimeFilter) {
            if (eventDateTime <= TimeTradeServer() && eventDateTime >= timeBefore) timeMatch = true;
            else if (eventDateTime >= TimeTradeServer() && eventDateTime <= timeAfter) timeMatch = true;
         }
         if (!timeMatch) {
            if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to time filter.");
            continue;
         }

         bool currencyMatch = !enableCurrencyFilter;
         if (enableCurrencyFilter) {
            for (int j = 0; j < ArraySize(curr_filter_array); j++) {
               if (filteredEvents[i].currency == curr_filter_array[j]) {
                  currencyMatch = true;
                  break;
               }
            }
         }
         if (!currencyMatch) {
            if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to currency filter.");
            continue;
         }

         bool importanceMatch = !enableImportanceFilter;
         if (enableImportanceFilter) {
            string imp_str = filteredEvents[i].importance;
            ENUM_CALENDAR_EVENT_IMPORTANCE event_imp = (imp_str == "None") ? CALENDAR_IMPORTANCE_NONE :
                                                      (imp_str == "Low") ? CALENDAR_IMPORTANCE_LOW :
                                                      (imp_str == "Medium") ? CALENDAR_IMPORTANCE_MODERATE :
                                                      CALENDAR_IMPORTANCE_HIGH;
            for (int k = 0; k < ArraySize(imp_filter_array); k++) {
               if (event_imp == imp_filter_array[k]) {
                  importanceMatch = true;
                  break;
               }
            }
         }
         if (!importanceMatch) {
            if (debugLogging) Print("Event ", filteredEvents[i].event, " skipped due to importance filter.");
            continue;
         }

         ArrayResize(displayableEvents, eventIndex + 1);
         displayableEvents[eventIndex] = filteredEvents[i];
         ArrayResize(current_displayable_eventNames, eventIndex + 1);
         current_displayable_eventNames[eventIndex] = filteredEvents[i].event;
         eventIndex++;
      }
      totalEvents_Filtered = ArraySize(displayableEvents);
      if (debugLogging) Print("Tester mode: Stored ", totalEvents_Filtered, " displayable events.");
   } else {
      MqlCalendarValue values[];
      datetime startTime = TimeTradeServer() - PeriodSeconds(start_time);
      datetime endTime = TimeTradeServer() + PeriodSeconds(end_time);
      int allValues = CalendarValueHistory(values,startTime,endTime,NULL,NULL);
      int eventIndex = 0;
      if (filters_changed) ArrayFree(displayableEvents); // Clear displayableEvents on filter change
      for (int i = 0; i < allValues; i++) {
         MqlCalendarEvent event;
         CalendarEventById(values[i].event_id, event);
         MqlCalendarCountry country;
         CalendarCountryById(event.country_id, country);
         MqlCalendarValue value;
         CalendarValueById(values[i].id, value);
         totalEvents_Considered++;

         bool currencyMatch = false;
         if (enableCurrencyFilter) {
            for (int j = 0; j < ArraySize(curr_filter_array); j++) {
               if (country.currency == curr_filter_array[j]) {
                  currencyMatch = true;
                  break;
               }
            }
            if (!currencyMatch) continue;
         }

         bool importanceMatch = false;
         if (enableImportanceFilter) {
            for (int k = 0; k < ArraySize(imp_filter_array); k++) {
               if (event.importance == imp_filter_array[k]) {
                  importanceMatch = true;
                  break;
               }
            }
            if (!importanceMatch) continue;
         }

         bool timeMatch = false;
         if (enableTimeFilter) {
            datetime eventTime = values[i].time;
            if (eventTime <= TimeTradeServer() && eventTime >= timeBefore) timeMatch = true;
            else if (eventTime >= TimeTradeServer() && eventTime <= timeAfter) timeMatch = true;
            if (!timeMatch) continue;
         }

         ArrayResize(displayableEvents, eventIndex + 1);
         displayableEvents[eventIndex].eventDate = TimeToString(values[i].time,TIME_DATE);
         displayableEvents[eventIndex].eventTime = TimeToString(values[i].time,TIME_MINUTES);
         displayableEvents[eventIndex].currency = country.currency;
         displayableEvents[eventIndex].event = event.name;
         displayableEvents[eventIndex].importance = (event.importance == CALENDAR_IMPORTANCE_NONE) ? "None" :
                                                   (event.importance == CALENDAR_IMPORTANCE_LOW) ? "Low" :
                                                   (event.importance == CALENDAR_IMPORTANCE_MODERATE) ? "Medium" : "High";
         displayableEvents[eventIndex].actual = value.GetActualValue();
         displayableEvents[eventIndex].forecast = value.GetForecastValue();
         displayableEvents[eventIndex].previous = value.GetPreviousValue();
         displayableEvents[eventIndex].eventDateTime = values[i].time;
         ArrayResize(current_displayable_eventNames, eventIndex + 1);
         current_displayable_eventNames[eventIndex] = event.name;
         eventIndex++;
      }
      totalEvents_Filtered = ArraySize(displayableEvents);
      if (debugLogging) Print("Live mode: Stored ", totalEvents_Filtered, " displayable events.");
   }

   // Check for changes in displayable events
   bool events_changed = isChangeInStringArrays(previous_displayable_eventNames, current_displayable_eventNames);
   bool scroll_changed = (scroll_pos != prev_scroll_pos);
   if (events_changed || filters_changed || scroll_changed) {
      if (debugLogging) {
         if (events_changed) Print("Changes detected in displayable events.");
         if (filters_changed) Print("Filter changes detected.");
         if (scroll_changed) Print("Scroll position changed: ", prev_scroll_pos, " -> ", scroll_pos);
      }
      ArrayFree(previous_displayable_eventNames);
      ArrayCopy(previous_displayable_eventNames, current_displayable_eventNames);
      prev_scroll_pos = scroll_pos;

      // Clear and redraw UI
      ObjectsDeleteAll(0, DATA_HOLDERS);
      ObjectsDeleteAll(0, ARRAY_NEWS);

      int startY = LIST_Y;
      int start_idx = scroll_visible ? scroll_pos : 0;
      int end_idx = MathMin(start_idx + VISIBLE_ITEMS, ArraySize(displayableEvents));
      for (int i = start_idx; i < end_idx; i++) {
         totalEvents_Displayable++;
         color holder_color = (totalEvents_Displayable % 2 == 0) ? C'213,227,207' : clrWhite;
         createRecLabel(DATA_HOLDERS+string(totalEvents_Displayable),LIST_X,startY-1,LIST_WIDTH,ITEM_HEIGHT+1,holder_color,1,clrNONE);

         int startX = LIST_X + 3;
         string news_data[ArraySize(array_calendar)];
         news_data[0] = displayableEvents[i].eventDate;
         news_data[1] = displayableEvents[i].eventTime;
         news_data[2] = displayableEvents[i].currency;
         color importance_color = clrBlack;
         if (displayableEvents[i].importance == "Low") importance_color = clrYellow;
         else if (displayableEvents[i].importance == "Medium") importance_color = clrOrange;
         else if (displayableEvents[i].importance == "High") importance_color = clrRed;
         news_data[3] = ShortToString(0x25CF);
         news_data[4] = displayableEvents[i].event;
         news_data[5] = DoubleToString(displayableEvents[i].actual, 3);
         news_data[6] = DoubleToString(displayableEvents[i].forecast, 3);
         news_data[7] = DoubleToString(displayableEvents[i].previous, 3);

         for (int k = 0; k < ArraySize(array_calendar); k++) {
            if (k == 3) {
               createLabel(ARRAY_NEWS+IntegerToString(i)+" "+array_calendar[k],startX,startY-(22-12),news_data[k],importance_color,22,"Calibri");
            } else {
               createLabel(ARRAY_NEWS+IntegerToString(i)+" "+array_calendar[k],startX,startY,news_data[k],clrBlack,12,"Calibri");
            }
            startX += buttons[k]+3;
         }

         ArrayResize(current_eventNames_data, ArraySize(current_eventNames_data)+1);
         current_eventNames_data[ArraySize(current_eventNames_data)-1] = displayableEvents[i].event;
         startY += ITEM_HEIGHT;
      }

      if (debugLogging) Print("Displayed ", totalEvents_Displayable, " events, start_idx=", start_idx, ", end_idx=", end_idx);
   } else {
      if (debugLogging) Print("No changes detected. Skipping redraw.");
   }

   // Update TIME_LABEL
   string timeText = updateServerTime ? "Server Time: "+TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS) : "Server Time: Static";
   updateLabel(TIME_LABEL,timeText+"   |||   Total News: "+IntegerToString(totalEvents_Filtered)+"/"+IntegerToString(totalEvents_Considered));

   // Update scrollbar visibility
   bool new_scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS;
   if (new_scroll_visible != scroll_visible || events_changed || filters_changed) {
      scroll_visible = new_scroll_visible;
      if (debugLogging) Print("Scrollbar visibility: ", scroll_visible ? "Visible" : "Hidden");
      if (scroll_visible) {
         if (ObjectFind(0, SCROLL_LEADER) < 0) {
            createRecLabel(SCROLL_LEADER, SCROLLBAR_X, SCROLLBAR_Y, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, clrSilver, 1, clrNONE);
            int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
            color up_color = (scroll_pos == 0) ? clrLightGray : clrBlack;
            color down_color = (scroll_pos >= max_scroll) ? clrLightGray : clrBlack;
            createRecLabel(SCROLL_UP_REC, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-5, CharToString(0x35), up_color, 15, "Webdings");
            int down_y = SCROLLBAR_Y + SCROLLBAR_HEIGHT - BUTTON_SIZE;
            createRecLabel(SCROLL_DOWN_REC, SCROLLBAR_X + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings");
            slider_height = calculateSliderHeight();
            int slider_y = SCROLLBAR_Y + BUTTON_SIZE;
            createButton(SCROLL_SLIDER, SCROLLBAR_X + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, 12, clrLightSlateGray, clrDarkGray, "Arial Bold");
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_WIDTH, 2);
            if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height);
         }
         updateSliderPosition();
         updateButtonColors();
      } else {
         ObjectDelete(0, SCROLL_LEADER);
         ObjectDelete(0, SCROLL_UP_REC);
         ObjectDelete(0, SCROLL_UP_LABEL);
         ObjectDelete(0, SCROLL_DOWN_REC);
         ObjectDelete(0, SCROLL_DOWN_LABEL);
         ObjectDelete(0, SCROLL_SLIDER);
         if (debugLogging) Print("Scrollbar removed: totalEvents_Filtered=", totalEvents_Filtered);
      }
   }

   if (isChangeInStringArrays(previous_eventNames_data, current_eventNames_data)) {
      if (debugLogging) Print("CHANGES IN EVENT NAMES DETECTED.");
      ArrayFree(previous_eventNames_data);
      ArrayCopy(previous_eventNames_data, current_eventNames_data);
   }
}

ここでは、update_dashboard_values関数内でダッシュボードを洗練させ、新しいロジックを導入して、イベント表示の精緻化と動的スクロールバー機能を実現しつつ、効率的なサイレントアップデートを優先します。まず、displayableEvents配列を活用してすべてのフィルタ済みイベントを格納し、変更検出を組み込むことで不要な再描画を避け、直感的なナビゲーションを可能にするスクロールバーを統合します。これにより、従来の制限を解消し、トレーダーの操作性を向上させます。最初に、カウンターtotalEvents_Considered、totalEvents_Filtered、totalEvents_Displayableを0にリセットし、配列current_eventNames_dataとcurrent_displayable_eventNamesをArrayFreeでクリアして、更新されるイベントデータの準備をおこないます。

テスターおよびライブモードの既存フィルタリングロジックを保持しつつ、displayableEvents配列を導入してすべてのフィルタ済みイベントを格納し、従来の5~6件しか表示されなかった問題とは異なり、(例として1711件など)イベントへ包括的にアクセスできるようにします。テスターモードでは、filters_changedがtrueの場合にFilterEventsForTesterを呼び出し、displayableEventsをArrayFreeでクリア後、filteredEventsをループ処理し、enableTimeFilter、enableCurrencyFilter、enableImportanceFilterを適用してdisplayableEventsとcurrent_displayable_eventNamesを更新、totalEvents_FilteredをArraySize(displayableEvents)に設定します。

ライブモードでは、CalendarValueHistoryを使用してイベントを取得し、filters_changedがtrueの場合にdisplayableEventsをクリアしてフィルタ済みイベントを格納し、debugLoggingが有効であれば件数をログに出力します。

サイレントアップデートは、isChangeInStringArraysでprevious_displayable_eventNamesとcurrent_displayable_eventNamesを比較し、events_changedを設定することで実現します。また、scroll_posとprev_scroll_posの差異をチェックしてscroll_changedを判定します。events_changed、filters_changed、scroll_changedのいずれかがtrueの場合、debugLoggingが有効であればPrintでログを出力し、previous_displayable_eventNamesをArrayCopyで更新、prev_scroll_posも設定します。UI要素はObjectsDeleteAllでDATA_HOLDERSとARRAY_NEWSをクリアした後、displayableEventsからVISIBLE_ITEMS(11)件までを描画します。描画範囲は、scroll_visibleがtrueの場合はscroll_posに基づくstart_idxからArraySize(displayableEvents)までのend_idxで制御します。

各イベントについて、createRecLabelで背景を交互色(例:"C'213,227,207'"またはclrWhite)に描画し、news_dataにイベント情報を格納、createLabelで各フィールドを表示します。重要度に応じた色付け(例:低の場合はclrYellow)を行い、debugLoggingが有効であれば表示件数をログ出力します。変更がなければ再描画をスキップし、パフォーマンスを維持します。

スクロールバーは、totalEvents_Filtered > VISIBLE_ITEMSでnew_scroll_visibleを判定し、scroll_visibleが変化した場合やevents_changed、filters_changedがtrueの場合に更新します。必要に応じて、SCROLL_LEADER、SCROLL_UP_REC、SCROLL_UP_LABEL、SCROLL_DOWN_REC、SCROLL_DOWN_LABEL、SCROLL_SLIDERをcreateRecLabel、createLabel、createButtonで作成します。アイコン色はscroll_posとmax_scrollに応じてclrBlackまたはclrLightGrayを設定します。不要な場合はObjectDeleteで削除し、クリーンなインターフェースを維持します。最後に、isChangeInStringArraysでcurrent_eventNames_dataに変化があればprevious_eventNames_dataを更新し、堅牢でナビゲート可能かつ効率的なダッシュボードを実現します。新しいオブジェクトを作成したため、メインダッシュボードの一部としてこれらをクリアする必要があります。

//+------------------------------------------------------------------+
//| Destroy dashboard                                                |
//+------------------------------------------------------------------+
void destroy_Dashboard() {
   ObjectDelete(0,"MAIN_REC");
   ObjectDelete(0,"SUB_REC1");
   ObjectDelete(0,"SUB_REC2");
   ObjectDelete(0,"HEADER_LABEL");
   ObjectDelete(0,"TIME_LABEL");
   ObjectDelete(0,"IMPACT_LABEL");
   ObjectsDeleteAll(0,"ARRAY_CALENDAR");
   ObjectsDeleteAll(0,"ARRAY_NEWS");
   ObjectsDeleteAll(0,"DATA_HOLDERS");
   ObjectsDeleteAll(0,"IMPACT_LABEL");
   ObjectDelete(0,"FILTER_LABEL");
   ObjectDelete(0,"FILTER_CURR_BTN");
   ObjectDelete(0,"FILTER_IMP_BTN");
   ObjectDelete(0,"FILTER_TIME_BTN");
   ObjectDelete(0,"CANCEL_BTN");
   ObjectsDeleteAll(0,"CURRENCY_BTNS");
   ObjectDelete(0, SCROLL_LEADER);
   ObjectDelete(0, SCROLL_UP_REC);
   ObjectDelete(0, SCROLL_UP_LABEL);
   ObjectDelete(0, SCROLL_DOWN_REC);
   ObjectDelete(0, SCROLL_DOWN_LABEL);
   ObjectDelete(0, SCROLL_SLIDER);
   ArrayFree(displayableEvents);
   ArrayFree(current_displayable_eventNames);
   ArrayFree(previous_displayable_eventNames);
   ChartRedraw(0);
}

新たに作成したオブジェクトをクリアするには、単純にObjectDelete関数を呼び出し、それぞれのオブジェクト名を渡すだけです。これにより、ダッシュボードを削除する際に、これらのオブジェクトも同時に削除され、ダッシュボードの一部として扱われます。コンパイルすると、次の結果が得られます。

フィルタリングされたイベントの数が11以下の場合:

フィッティングイベント

フィルタリングされたイベントが11件を超える場合:

不適切なイベント

広範囲にフィルタリングされたイベント範囲:

幅広いイベントに対応

上記の画像から、利用可能なイベントに応じてカレンダースクロールバーを動的に作成していることが確認できます。次におこなうべきは、スクロールバーの各要素に動的な動作を持たせることです。これを実現するために、OnChartEvent関数を以下のように活用します。

//+------------------------------------------------------------------+
//| Chart event handler                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) {
   int mouse_x = (int)lparam;
   int mouse_y = (int)dparam;
   int mouse_state = (int)sparam;

   if (id == CHARTEVENT_OBJECT_CLICK) {

      // Scrollbar button clicks
      if (scroll_visible && (sparam == SCROLL_UP_REC || sparam == SCROLL_UP_LABEL)) {
         scrollUp();
         updateButtonColors();
         if (debugLogging) Print("Up button clicked (", sparam, "). CurrPos: ", scroll_pos);
         ChartRedraw(0);
      }
      if (scroll_visible && (sparam == SCROLL_DOWN_REC || sparam == SCROLL_DOWN_LABEL)) {
         scrollDown();
         updateButtonColors();
         if (debugLogging) Print("Down button clicked (", sparam, "). CurrPos: ", scroll_pos);
         ChartRedraw(0);
      }
   }
   else if (id == CHARTEVENT_MOUSE_MOVE && scroll_visible) {
      if (prev_mouse_state == 0 && mouse_state == 1) {
         int xd = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_XDISTANCE);
         int yd = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE);
         int xs = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_XSIZE);
         int ys = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE);
         if (mouse_x >= xd && mouse_x <= xd + xs && mouse_y >= yd && mouse_y <= yd + ys) {
            moving_state_slider = true;
            mlb_down_x = mouse_x;
            mlb_down_y = mouse_y;
            mlb_down_yd_slider = yd;
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrDodgerBlue);
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height + 2);
            ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
            if (debugLogging) Print("Slider drag started at y=", mouse_y);
         }
      }
      if (moving_state_slider && mouse_state == 1) {
         int delta_y = mouse_y - mlb_down_y;
         int new_y = mlb_down_yd_slider + delta_y;
         int scroll_area_y_min = SCROLLBAR_Y + BUTTON_SIZE;
         int scroll_area_y_max = scroll_area_y_min + SCROLL_AREA_HEIGHT - slider_height;
         new_y = MathMax(scroll_area_y_min, MathMin(new_y, scroll_area_y_max));
         ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE, new_y);
         int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
         double scroll_ratio = (double)(new_y - scroll_area_y_min) / (scroll_area_y_max - scroll_area_y_min);
         int new_scroll_pos = (int)MathRound(scroll_ratio * max_scroll);
         if (new_scroll_pos != scroll_pos) {
            scroll_pos = new_scroll_pos;
            update_dashboard_values(curr_filter_selected, imp_filter_selected);
            updateButtonColors();
            if (debugLogging) Print("Slider dragged. CurrPos: ", scroll_pos, ", Total steps: ", max_scroll, ", Slider y=", new_y);
         }
         ChartRedraw(0);
      }
      if (mouse_state == 0) {
         if (moving_state_slider) {
            moving_state_slider = false;
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrLightSlateGray);
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height);
            ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
            if (debugLogging) Print("Slider drag stopped.");
            ChartRedraw(0);
         }
      }
      prev_mouse_state = mouse_state;
   }
}

ここでは、OnChartEvent関数内でスクロールバー関連の新しいロジックを実装し、クリックやドラッグによるニュースイベントのシームレスなナビゲーションを可能にすることで、インタラクティブ性を向上させます。まず、動的スクロールバーとのユーザー操作を処理します。具体的には、上・下ボタンのクリック処理とスライダーのドラッグ操作を扱い、ダッシュボード表示が応答的に更新されるようにします。CHARTEVENT_OBJECT_CLICKイベントでは、scroll_visibleがtrueの場合にスクロールバーのボタンクリックを処理します。クリックされたオブジェクト(sparam)がSCROLL_UP_RECまたはSCROLL_UP_LABELの場合、scrollUp関数を呼び出してscroll_posを減算し、updateButtonColorsでアイコン色(clrBlackまたはclrLightGray)を更新します。debugLoggingが有効ならPrintで操作をログ出力し、ChartRedrawで表示を更新します。

同様に、SCROLL_DOWN_RECまたはSCROLL_DOWN_LABELがクリックされた場合は、scrollDownを呼び出してscroll_posを増加させ、updateButtonColorsを呼び出し、操作をログ出力し、ChartRedrawでスクロール後のイベントリストを反映させます。これらの関数は既に定義済みで、後ほど詳細を説明します。

CHARTEVENT_MOUSE_MOVEイベントでは、scroll_visibleがtrueの場合にスライダーのドラッグ操作を管理します。prev_mouse_state = 0かつmouse_state = 1の場合、ObjectGetIntegerでSCROLL_SLIDERの位置(OBJPROP_XDISTANCE、OBJPROP_YDISTANCE)とサイズ(OBJPROP_XSIZE、OBJPROP_YSIZE)を取得し、xd、yd、xs、ysに格納します。マウス座標(mouse_x、mouse_y)がスライダー内にある場合、moving_state_slider = trueとし、mlb_down_x、mlb_down_y、mlb_down_yd_sliderを保存、SCROLL_SLIDERのOBJPROP_BGCOLORをclrDodgerBlueに変更し、OBJPROP_YSIZEを2増加させ、ChartSetIntegerでチャートスクロールを無効化、debugLoggingが有効ならドラッグ開始をログ出力します。

moving_state_sliderかつmouse_stateがtrueの間、まずdelta_y = mouse_y − mlb_down_yを計算し、スクロール範囲内でnew_yを算出します。垂直範囲はscroll_area_y_min = SCROLLBAR_Y + BUTTON_SIZE、scroll_area_y_max = scroll_area_y_min + SCROLL_AREA_HEIGHT − slider_heightで定義され、MathMaxとMathMinを用いて範囲内に収めます。その後、SCROLL_SLIDERのOBJPROP_YDISTANCEをnew_yに設定します。次に、new_yのスクロール範囲における比率からnew_scroll_posを導出し、もしnew_scroll_pos ≠ scroll_posであれば、scroll_posを更新します。その後、update_dashboard_values(curr_filter_selected, imp_filter_selected)を呼び出し、ダッシュボード表示を更新、updateButtonColorsでボタンアイコン色を反映し、ドラッグ詳細をdebugLoggingが有効な場合はログ出力し、最後にChartRedrawでチャートをリフレッシュします。

mouse_state = 0の場合、moving_state_sliderをリセットし、SCROLL_SLIDERのOBJPROP_BGCOLORをclrLightSlateGrayに、OBJPROP_YSIZEを元のslider_heightに戻します。チャートスクロールを再度有効化し、ドラッグ停止をログに出力後、ChartRedrawを呼び出してスムーズなスライダー操作を保証します。スクロールロジックを担当する関数は以下のとおりです。

//+------------------------------------------------------------------+
//| Scroll up                                                        |
//+------------------------------------------------------------------+
void scrollUp() {
   if (scroll_pos > 0) {
      scroll_pos--;
      update_dashboard_values(curr_filter_selected, imp_filter_selected);
      updateSliderPosition();
      if (debugLogging) Print("Scrolled up. CurrPos: ", scroll_pos);
   } else {
      if (debugLogging) Print("Cannot scroll up further. Already at top.");
   }
}

//+------------------------------------------------------------------+
//| Scroll down                                                      |
//+------------------------------------------------------------------+
void scrollDown() {
   int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
   if (scroll_pos < max_scroll) {
      scroll_pos++;
      update_dashboard_values(curr_filter_selected, imp_filter_selected);
      updateSliderPosition();
      if (debugLogging) Print("Scrolled down. CurrPos: ", scroll_pos);
   } else {
      if (debugLogging) Print("Cannot scroll down further. Max scroll reached: ", max_scroll);
   }
}
//+------------------------------------------------------------------+

ここでは、scrollUp関数でイベントリストを上方向にナビゲートできるようにします。まず、scroll_pos > 0であるかを確認し、スクロールアップ可能かを判定します。条件がtrueの場合、scroll_posを1減算し、update_dashboard_values(curr_filter_selected, imp_filter_selected)を呼び出して表示イベントを更新、updateSliderPositionでSCROLL_SLIDERの位置を調整します。さらに、debugLoggingが有効であればPrintで新しいscroll_posをログに出力します。scroll_pos = 0の場合は、リストの先頭に到達したことをログ出力し、不要な更新を防ぎ、操作の流れをクリーンに保ちます。

同様に、scrollDown関数ではイベントリストを下方向にナビゲート可能にします。まず、max_scrollをMathMaxを使用して計算します。具体的には、ArraySize(displayableEvents) − VISIBLE_ITEMS(11)から算出され、非負となるよう調整されます。

scroll_pos < max_scrollの場合、scroll_posを1増加させ、update_dashboard_values(curr_filter_selected, imp_filter_selected)で表示イベントを更新し、updateSliderPositionでSCROLL_SLIDERの位置を調整します。debugLoggingが有効であれば、新しいscroll_posをPrintでログに出力します。scroll_pos ≥ max_scrollの場合は、リストの末尾に到達したことをログ出力し、不要な更新を避けつつ直感的な操作を維持します。このように、定義した関数はOnInitおよびOnTickイベントハンドラ内でも呼び出され、スムーズなスクロール操作をサポートします。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   UpdateFilterInfo();
   CheckForNewsTrade();
   if (isDashboardUpdate) {
      if (MQLInfoInteger(MQL_TESTER)) {
         datetime currentTime = TimeTradeServer();
         datetime timeRange = PeriodSeconds(range_time);
         datetime timeAfter = currentTime + timeRange;
         if (filters_changed || last_dashboard_update < timeAfter) {
            update_dashboard_values(curr_filter_selected, imp_filter_selected);
            ArrayFree(last_dashboard_eventNames);
            ArrayCopy(last_dashboard_eventNames, current_eventNames_data);
            last_dashboard_update = currentTime;
         }
      } else {
         update_dashboard_values(curr_filter_selected, imp_filter_selected);
      }
   }
}

//+------------------------------------------------------------------+
//| Chart event handler                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) {
   int mouse_x = (int)lparam;
   int mouse_y = (int)dparam;
   int mouse_state = (int)sparam;

   if (id == CHARTEVENT_OBJECT_CLICK) {
      UpdateFilterInfo();
      CheckForNewsTrade();
      if (sparam == CANCEL_BTN) {
         isDashboardUpdate = false;
         destroy_Dashboard();
      }
      if (sparam == FILTER_CURR_BTN) {
         bool btn_state = ObjectGetInteger(0,sparam,OBJPROP_STATE);
         enableCurrencyFilter = btn_state;
         if (debugLogging) Print(sparam+" STATE = "+(string)btn_state+", FLAG = "+(string)enableCurrencyFilter);
         string filter_curr_text = enableCurrencyFilter ? ShortToString(0x2714)+"Currency" : ShortToString(0x274C)+"Currency";
         color filter_curr_txt_color = enableCurrencyFilter ? clrLime : clrRed;
         ObjectSetString(0,FILTER_CURR_BTN,OBJPROP_TEXT,filter_curr_text);
         ObjectSetInteger(0,FILTER_CURR_BTN,OBJPROP_COLOR,filter_curr_txt_color);
         if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
         update_dashboard_values(curr_filter_selected,imp_filter_selected);
         if (debugLogging) Print("Success. Changes updated! State: "+(string)enableCurrencyFilter);
         ChartRedraw(0);
      }
      if (sparam == FILTER_IMP_BTN) {
         bool btn_state = ObjectGetInteger(0,sparam,OBJPROP_STATE);
         enableImportanceFilter = btn_state;
         if (debugLogging) Print(sparam+" STATE = "+(string)btn_state+", FLAG = "+(string)enableImportanceFilter);
         string filter_imp_text = enableImportanceFilter ? ShortToString(0x2714)+"Importance" : ShortToString(0x274C)+"Importance";
         color filter_imp_txt_color = enableImportanceFilter ? clrLime : clrRed;
         ObjectSetString(0,FILTER_IMP_BTN,OBJPROP_TEXT,filter_imp_text);
         ObjectSetInteger(0,FILTER_IMP_BTN,OBJPROP_COLOR,filter_imp_txt_color);
         if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
         update_dashboard_values(curr_filter_selected,imp_filter_selected);
         if (debugLogging) Print("Success. Changes updated! State: "+(string)enableImportanceFilter);
         ChartRedraw(0);
      }
      if (sparam == FILTER_TIME_BTN) {
         bool btn_state = ObjectGetInteger(0,sparam,OBJPROP_STATE);
         enableTimeFilter = btn_state;
         if (debugLogging) Print(sparam+" STATE = "+(string)btn_state+", FLAG = "+(string)enableTimeFilter);
         string filter_time_text = enableTimeFilter ? ShortToString(0x2714)+"Time" : ShortToString(0x274C)+"Time";
         color filter_time_txt_color = enableTimeFilter ? clrLime : clrRed;
         ObjectSetString(0,FILTER_TIME_BTN,OBJPROP_TEXT,filter_time_text);
         ObjectSetInteger(0,FILTER_TIME_BTN,OBJPROP_COLOR,filter_time_txt_color);
         if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
         update_dashboard_values(curr_filter_selected,imp_filter_selected);
         if (debugLogging) Print("Success. Changes updated! State: "+(string)enableTimeFilter);
         ChartRedraw(0);
      }
      if (StringFind(sparam,CURRENCY_BTNS) >= 0) {
         string selected_curr = ObjectGetString(0,sparam,OBJPROP_TEXT);
         if (debugLogging) Print("BTN NAME = ",sparam,", CURRENCY = ",selected_curr);
         bool btn_state = ObjectGetInteger(0,sparam,OBJPROP_STATE);
         if (btn_state == false) {
            if (debugLogging) Print("BUTTON IS IN UN-SELECTED MODE.");
            for (int i = 0; i < ArraySize(curr_filter_selected); i++) {
               if (curr_filter_selected[i] == selected_curr) {
                  for (int j = i; j < ArraySize(curr_filter_selected) - 1; j++) {
                     curr_filter_selected[j] = curr_filter_selected[j + 1];
                  }
                  ArrayResize(curr_filter_selected, ArraySize(curr_filter_selected) - 1);
                  if (debugLogging) Print("Removed from selected filters: ", selected_curr);
                  break;
               }
            }
         } else {
            if (debugLogging) Print("BUTTON IS IN SELECTED MODE. TAKE ACTION");
            bool already_selected = false;
            for (int j = 0; j < ArraySize(curr_filter_selected); j++) {
               if (curr_filter_selected[j] == selected_curr) {
                  already_selected = true;
                  break;
               }
            }
            if (!already_selected) {
               ArrayResize(curr_filter_selected, ArraySize(curr_filter_selected) + 1);
               curr_filter_selected[ArraySize(curr_filter_selected) - 1] = selected_curr;
               if (debugLogging) Print("Added to selected filters: ", selected_curr);
            } else {
               if (debugLogging) Print("Currency already selected: ", selected_curr);
            }
         }
         if (debugLogging) Print("SELECTED ARRAY SIZE = ",ArraySize(curr_filter_selected));
         if (debugLogging) ArrayPrint(curr_filter_selected);
         if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
         update_dashboard_values(curr_filter_selected,imp_filter_selected);
         if (debugLogging) Print("SUCCESS. DASHBOARD UPDATED");
         ChartRedraw(0);
      }
      if (StringFind(sparam, IMPACT_LABEL) >= 0) {
         string selected_imp = ObjectGetString(0, sparam, OBJPROP_TEXT);
         ENUM_CALENDAR_EVENT_IMPORTANCE selected_importance_lvl = get_importance_level(impact_labels,allowed_importance_levels,selected_imp);
         if (debugLogging) Print("BTN NAME = ", sparam, ", IMPORTANCE LEVEL = ", selected_imp,"(",selected_importance_lvl,")");
         bool btn_state = ObjectGetInteger(0, sparam, OBJPROP_STATE);
         color color_border = btn_state ? clrNONE : clrBlack;
         if (btn_state == false) {
            if (debugLogging) Print("BUTTON IS IN UN-SELECTED MODE.");
            for (int i = 0; i < ArraySize(imp_filter_selected); i++) {
               if (impact_filter_selected[i] == selected_imp) {
                  for (int j = i; j < ArraySize(imp_filter_selected) - 1; j++) {
                     imp_filter_selected[j] = imp_filter_selected[j + 1];
                     impact_filter_selected[j] = impact_filter_selected[j + 1];
                  }
                  ArrayResize(imp_filter_selected, ArraySize(imp_filter_selected) - 1);
                  ArrayResize(impact_filter_selected, ArraySize(impact_filter_selected) - 1);
                  if (debugLogging) Print("Removed from selected importance filters: ", selected_imp,"(",selected_importance_lvl,")");
                  break;
               }
            }
         } else {
            if (debugLogging) Print("BUTTON IS IN SELECTED MODE. TAKE ACTION");
            bool already_selected = false;
            for (int j = 0; j < ArraySize(imp_filter_selected); j++) {
               if (impact_filter_selected[j] == selected_imp) {
                  already_selected = true;
                  break;
               }
            }
            if (!already_selected) {
               ArrayResize(imp_filter_selected, ArraySize(imp_filter_selected) + 1);
               imp_filter_selected[ArraySize(imp_filter_selected) - 1] = selected_importance_lvl;
               ArrayResize(impact_filter_selected, ArraySize(impact_filter_selected) + 1);
               impact_filter_selected[ArraySize(impact_filter_selected) - 1] = selected_imp;
               if (debugLogging) Print("Added to selected importance filters: ", selected_imp,"(",selected_importance_lvl,")");
            } else {
               if (debugLogging) Print("Importance level already selected: ", selected_imp,"(",selected_importance_lvl,")");
            }
         }
         if (debugLogging) Print("SELECTED ARRAY SIZE = ", ArraySize(imp_filter_selected)," >< ",ArraySize(impact_filter_selected));
         if (debugLogging) ArrayPrint(imp_filter_selected);
         if (debugLogging) ArrayPrint(impact_filter_selected);
         if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
         update_dashboard_values(curr_filter_selected,imp_filter_selected);
         ObjectSetInteger(0,sparam,OBJPROP_BORDER_COLOR,color_border);
         if (debugLogging) Print("SUCCESS. DASHBOARD UPDATED");
         ChartRedraw(0);
      }
      // Scrollbar button clicks
      if (scroll_visible && (sparam == SCROLL_UP_REC || sparam == SCROLL_UP_LABEL)) {
         scrollUp();
         updateButtonColors();
         if (debugLogging) Print("Up button clicked (", sparam, "). CurrPos: ", scroll_pos);
         ChartRedraw(0);
      }
      if (scroll_visible && (sparam == SCROLL_DOWN_REC || sparam == SCROLL_DOWN_LABEL)) {
         scrollDown();
         updateButtonColors();
         if (debugLogging) Print("Down button clicked (", sparam, "). CurrPos: ", scroll_pos);
         ChartRedraw(0);
      }
   }
   else if (id == CHARTEVENT_MOUSE_MOVE && scroll_visible) {
      if (prev_mouse_state == 0 && mouse_state == 1) {
         int xd = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_XDISTANCE);
         int yd = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE);
         int xs = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_XSIZE);
         int ys = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE);
         if (mouse_x >= xd && mouse_x <= xd + xs && mouse_y >= yd && mouse_y <= yd + ys) {
            moving_state_slider = true;
            mlb_down_x = mouse_x;
            mlb_down_y = mouse_y;
            mlb_down_yd_slider = yd;
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrDodgerBlue);
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height + 2);
            ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
            if (debugLogging) Print("Slider drag started at y=", mouse_y);
         }
      }
      if (moving_state_slider && mouse_state == 1) {
         int delta_y = mouse_y - mlb_down_y;
         int new_y = mlb_down_yd_slider + delta_y;
         int scroll_area_y_min = SCROLLBAR_Y + BUTTON_SIZE;
         int scroll_area_y_max = scroll_area_y_min + SCROLL_AREA_HEIGHT - slider_height;
         new_y = MathMax(scroll_area_y_min, MathMin(new_y, scroll_area_y_max));
         ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE, new_y);
         int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
         double scroll_ratio = (double)(new_y - scroll_area_y_min) / (scroll_area_y_max - scroll_area_y_min);
         int new_scroll_pos = (int)MathRound(scroll_ratio * max_scroll);
         if (new_scroll_pos != scroll_pos) {
            scroll_pos = new_scroll_pos;
            update_dashboard_values(curr_filter_selected, imp_filter_selected);
            updateButtonColors();
            if (debugLogging) Print("Slider dragged. CurrPos: ", scroll_pos, ", Total steps: ", max_scroll, ", Slider y=", new_y);
         }
         ChartRedraw(0);
      }
      if (mouse_state == 0) {
         if (moving_state_slider) {
            moving_state_slider = false;
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrLightSlateGray);
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height);
            ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
            if (debugLogging) Print("Slider drag stopped.");
            ChartRedraw(0);
         }
      }
      prev_mouse_state = mouse_state;
   }
}

ここでは、先ほど実装した関数やロジックをイベントハンドラ内で呼び出すことで、必要なイベントハンドラ上で変更が反映されるようにしています。コンパイルすると、次の出力が得られます。

スクロールバーの可視化

上の可視化から、ダッシュボードに動的スクロールバーを追加したことが確認できます。残されているのは、システムを徹底的にバックテストすることです。その内容は次のセクションで扱います。


テストと検証

強化されたダッシュボードをテストし、動的スクロールバーとイベント表示が意図通りに機能することを確認しました。テストでは、スクロールバーの視覚的フィードバック、フィルタ済みイベントの全表示、サイレントアップデートの効率性に重点を置き、ライブモードおよびストラテジーテスターモードの両方で検証をおこないました。これらのテストは、ダッシュボードのパフォーマンスを視覚的に示すため、簡潔なGraphics Interchange Format(GIF)で記録しました。以下にその様子を示します。

テスト1

可視化から、スクロールバー自体は正しく動作していることが確認できます。しかし、フィルターをクリックして変更した際にスクロールバーが動的に変化しない問題があります。イベント自体は正しく更新されますが、スクロールバーの再計算がおこなわれていないため、完全な動的機能が無効になっているのが原因です。この問題を解決するには、ボタンが押されるたびにスクロールバーを再キャリブレーションする必要があります。同様の処理は、データに変更があった際にスクロールバーを更新することでも実現可能ですが、その場合は不要な処理が発生する可能性があります。以下に、この動的スクロールバーを完全に機能させるためのフルロジックを示します。

//+------------------------------------------------------------------+
//| Chart event handler                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam) {
   int mouse_x = (int)lparam;
   int mouse_y = (int)dparam;
   int mouse_state = (int)sparam;

   if (id == CHARTEVENT_OBJECT_CLICK) {
      UpdateFilterInfo();
      CheckForNewsTrade();
      if (sparam == CANCEL_BTN) {
         isDashboardUpdate = false;
         destroy_Dashboard();
      }
      if (sparam == FILTER_CURR_BTN) {
         bool btn_state = ObjectGetInteger(0,sparam,OBJPROP_STATE);
         enableCurrencyFilter = btn_state;
         if (debugLogging) Print(sparam+" STATE = "+(string)btn_state+", FLAG = "+(string)enableCurrencyFilter);
         string filter_curr_text = enableCurrencyFilter ? ShortToString(0x2714)+"Currency" : ShortToString(0x274C)+"Currency";
         color filter_curr_txt_color = enableCurrencyFilter ? clrLime : clrRed;
         ObjectSetString(0,FILTER_CURR_BTN,OBJPROP_TEXT,filter_curr_text);
         ObjectSetInteger(0,FILTER_CURR_BTN,OBJPROP_COLOR,filter_curr_txt_color);
         if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
         update_dashboard_values(curr_filter_selected,imp_filter_selected);
         // Recalculate scrollbar
         ObjectDelete(0, SCROLL_LEADER);
         ObjectDelete(0, SCROLL_UP_REC);
         ObjectDelete(0, SCROLL_UP_LABEL);
         ObjectDelete(0, SCROLL_DOWN_REC);
         ObjectDelete(0, SCROLL_DOWN_LABEL);
         ObjectDelete(0, SCROLL_SLIDER);
         scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS;
         if (debugLogging) Print("Scrollbar visibility: ", scroll_visible ? "Visible" : "Hidden");
         if (scroll_visible) {
            createRecLabel(SCROLL_LEADER, SCROLLBAR_X, SCROLLBAR_Y, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, clrSilver, 1, clrNONE);
            int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
            color up_color = (scroll_pos == 0) ? clrLightGray : clrBlack;
            color down_color = (scroll_pos >= max_scroll) ? clrLightGray : clrBlack;
            createRecLabel(SCROLL_UP_REC, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-5, CharToString(0x35), up_color, 15, "Webdings");
            int down_y = SCROLLBAR_Y + SCROLLBAR_HEIGHT - BUTTON_SIZE;
            createRecLabel(SCROLL_DOWN_REC, SCROLLBAR_X + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings");
            slider_height = calculateSliderHeight();
            int slider_y = SCROLLBAR_Y + BUTTON_SIZE;
            createButton(SCROLL_SLIDER, SCROLLBAR_X + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, 12, clrLightSlateGray, clrDarkGray, "Arial Bold");
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_WIDTH, 2);
            if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height);
            updateSliderPosition();
            updateButtonColors();
         }
         if (debugLogging) Print("Success. Changes updated! State: "+(string)enableCurrencyFilter);
         ChartRedraw(0);
      }
      if (sparam == FILTER_IMP_BTN) {
         bool btn_state = ObjectGetInteger(0,sparam,OBJPROP_STATE);
         enableImportanceFilter = btn_state;
         if (debugLogging) Print(sparam+" STATE = "+(string)btn_state+", FLAG = "+(string)enableImportanceFilter);
         string filter_imp_text = enableImportanceFilter ? ShortToString(0x2714)+"Importance" : ShortToString(0x274C)+"Importance";
         color filter_imp_txt_color = enableImportanceFilter ? clrLime : clrRed;
         ObjectSetString(0,FILTER_IMP_BTN,OBJPROP_TEXT,filter_imp_text);
         ObjectSetInteger(0,FILTER_IMP_BTN,OBJPROP_COLOR,filter_imp_txt_color);
         if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
         update_dashboard_values(curr_filter_selected,imp_filter_selected);
         // Recalculate scrollbar
         ObjectDelete(0, SCROLL_LEADER);
         ObjectDelete(0, SCROLL_UP_REC);
         ObjectDelete(0, SCROLL_UP_LABEL);
         ObjectDelete(0, SCROLL_DOWN_REC);
         ObjectDelete(0, SCROLL_DOWN_LABEL);
         ObjectDelete(0, SCROLL_SLIDER);
         scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS;
         if (debugLogging) Print("Scrollbar visibility: ", scroll_visible ? "Visible" : "Hidden");
         if (scroll_visible) {
            createRecLabel(SCROLL_LEADER, SCROLLBAR_X, SCROLLBAR_Y, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, clrSilver, 1, clrNONE);
            int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
            color up_color = (scroll_pos == 0) ? clrLightGray : clrBlack;
            color down_color = (scroll_pos >= max_scroll) ? clrLightGray : clrBlack;
            createRecLabel(SCROLL_UP_REC, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-5, CharToString(0x35), up_color, 15, "Webdings");
            int down_y = SCROLLBAR_Y + SCROLLBAR_HEIGHT - BUTTON_SIZE;
            createRecLabel(SCROLL_DOWN_REC, SCROLLBAR_X + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings");
            slider_height = calculateSliderHeight();
            int slider_y = SCROLLBAR_Y + BUTTON_SIZE;
            createButton(SCROLL_SLIDER, SCROLLBAR_X + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, 12, clrLightSlateGray, clrDarkGray, "Arial Bold");
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_WIDTH, 2);
            if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height);
            updateSliderPosition();
            updateButtonColors();
         }
         if (debugLogging) Print("Success. Changes updated! State: "+(string)enableImportanceFilter);
         ChartRedraw(0);
      }
      if (sparam == FILTER_TIME_BTN) {
         bool btn_state = ObjectGetInteger(0,sparam,OBJPROP_STATE);
         enableTimeFilter = btn_state;
         if (debugLogging) Print(sparam+" STATE = "+(string)btn_state+", FLAG = "+(string)enableTimeFilter);
         string filter_time_text = enableTimeFilter ? ShortToString(0x2714)+"Time" : ShortToString(0x274C)+"Time";
         color filter_time_txt_color = enableTimeFilter ? clrLime : clrRed;
         ObjectSetString(0,FILTER_TIME_BTN,OBJPROP_TEXT,filter_time_text);
         ObjectSetInteger(0,FILTER_TIME_BTN,OBJPROP_COLOR,filter_time_txt_color);
         if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
         update_dashboard_values(curr_filter_selected,imp_filter_selected);
         // Recalculate scrollbar
         ObjectDelete(0, SCROLL_LEADER);
         ObjectDelete(0, SCROLL_UP_REC);
         ObjectDelete(0, SCROLL_UP_LABEL);
         ObjectDelete(0, SCROLL_DOWN_REC);
         ObjectDelete(0, SCROLL_DOWN_LABEL);
         ObjectDelete(0, SCROLL_SLIDER);
         scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS;
         if (debugLogging) Print("Scrollbar visibility: ", scroll_visible ? "Visible" : "Hidden");
         if (scroll_visible) {
            createRecLabel(SCROLL_LEADER, SCROLLBAR_X, SCROLLBAR_Y, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, clrSilver, 1, clrNONE);
            int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
            color up_color = (scroll_pos == 0) ? clrLightGray : clrBlack;
            color down_color = (scroll_pos >= max_scroll) ? clrLightGray : clrBlack;
            createRecLabel(SCROLL_UP_REC, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-5, CharToString(0x35), up_color, 15, "Webdings");
            int down_y = SCROLLBAR_Y + SCROLLBAR_HEIGHT - BUTTON_SIZE;
            createRecLabel(SCROLL_DOWN_REC, SCROLLBAR_X + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings");
            slider_height = calculateSliderHeight();
            int slider_y = SCROLLBAR_Y + BUTTON_SIZE;
            createButton(SCROLL_SLIDER, SCROLLBAR_X + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, 12, clrLightSlateGray, clrDarkGray, "Arial Bold");
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_WIDTH, 2);
            if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height);
            updateSliderPosition();
            updateButtonColors();
         }
         if (debugLogging) Print("Success. Changes updated! State: "+(string)enableTimeFilter);
         ChartRedraw(0);
      }
      if (StringFind(sparam,CURRENCY_BTNS) >= 0) {
         string selected_curr = ObjectGetString(0,sparam,OBJPROP_TEXT);
         if (debugLogging) Print("BTN NAME = ",sparam,", CURRENCY = ",selected_curr);
         bool btn_state = ObjectGetInteger(0,sparam,OBJPROP_STATE);
         if (btn_state == false) {
            if (debugLogging) Print("BUTTON IS IN UN-SELECTED MODE.");
            for (int i = 0; i < ArraySize(curr_filter_selected); i++) {
               if (curr_filter_selected[i] == selected_curr) {
                  for (int j = i; j < ArraySize(curr_filter_selected) - 1; j++) {
                     curr_filter_selected[j] = curr_filter_selected[j + 1];
                  }
                  ArrayResize(curr_filter_selected, ArraySize(curr_filter_selected) - 1);
                  if (debugLogging) Print("Removed from selected filters: ", selected_curr);
                  break;
               }
            }
         } else {
            if (debugLogging) Print("BUTTON IS IN SELECTED MODE. TAKE ACTION");
            bool already_selected = false;
            for (int j = 0; j < ArraySize(curr_filter_selected); j++) {
               if (curr_filter_selected[j] == selected_curr) {
                  already_selected = true;
                  break;
               }
            }
            if (!already_selected) {
               ArrayResize(curr_filter_selected, ArraySize(curr_filter_selected) + 1);
               curr_filter_selected[ArraySize(curr_filter_selected) - 1] = selected_curr;
               if (debugLogging) Print("Added to selected filters: ", selected_curr);
            } else {
               if (debugLogging) Print("Currency already selected: ", selected_curr);
            }
         }
         if (debugLogging) Print("SELECTED ARRAY SIZE = ",ArraySize(curr_filter_selected));
         if (debugLogging) ArrayPrint(curr_filter_selected);
         if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
         update_dashboard_values(curr_filter_selected,imp_filter_selected);
         // Recalculate scrollbar
         ObjectDelete(0, SCROLL_LEADER);
         ObjectDelete(0, SCROLL_UP_REC);
         ObjectDelete(0, SCROLL_UP_LABEL);
         ObjectDelete(0, SCROLL_DOWN_REC);
         ObjectDelete(0, SCROLL_DOWN_LABEL);
         ObjectDelete(0, SCROLL_SLIDER);
         scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS;
         if (debugLogging) Print("Scrollbar visibility: ", scroll_visible ? "Visible" : "Hidden");
         if (scroll_visible) {
            createRecLabel(SCROLL_LEADER, SCROLLBAR_X, SCROLLBAR_Y, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, clrSilver, 1, clrNONE);
            int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
            color up_color = (scroll_pos == 0) ? clrLightGray : clrBlack;
            color down_color = (scroll_pos >= max_scroll) ? clrLightGray : clrBlack;
            createRecLabel(SCROLL_UP_REC, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-5, CharToString(0x35), up_color, 15, "Webdings");
            int down_y = SCROLLBAR_Y + SCROLLBAR_HEIGHT - BUTTON_SIZE;
            createRecLabel(SCROLL_DOWN_REC, SCROLLBAR_X + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings");
            slider_height = calculateSliderHeight();
            int slider_y = SCROLLBAR_Y + BUTTON_SIZE;
            createButton(SCROLL_SLIDER, SCROLLBAR_X + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, 12, clrLightSlateGray, clrDarkGray, "Arial Bold");
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_WIDTH, 2);
            if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height);
            updateSliderPosition();
            updateButtonColors();
         }
         if (debugLogging) Print("SUCCESS. DASHBOARD UPDATED");
         ChartRedraw(0);
      }
      if (StringFind(sparam, IMPACT_LABEL) >= 0) {
         string selected_imp = ObjectGetString(0, sparam, OBJPROP_TEXT);
         ENUM_CALENDAR_EVENT_IMPORTANCE selected_importance_lvl = get_importance_level(impact_labels,allowed_importance_levels,selected_imp);
         if (debugLogging) Print("BTN NAME = ", sparam, ", IMPORTANCE LEVEL = ", selected_imp,"(",selected_importance_lvl,")");
         bool btn_state = ObjectGetInteger(0, sparam, OBJPROP_STATE);
         color color_border = btn_state ? clrNONE : clrBlack;
         if (btn_state == false) {
            if (debugLogging) Print("BUTTON IS IN UN-SELECTED MODE.");
            for (int i = 0; i < ArraySize(imp_filter_selected); i++) {
               if (impact_filter_selected[i] == selected_imp) {
                  for (int j = i; j < ArraySize(imp_filter_selected) - 1; j++) {
                     imp_filter_selected[j] = imp_filter_selected[j + 1];
                     impact_filter_selected[j] = impact_filter_selected[j + 1];
                  }
                  ArrayResize(imp_filter_selected, ArraySize(imp_filter_selected) - 1);
                  ArrayResize(impact_filter_selected, ArraySize(impact_filter_selected) - 1);
                  if (debugLogging) Print("Removed from selected importance filters: ", selected_imp,"(",selected_importance_lvl,")");
                  break;
               }
            }
         } else {
            if (debugLogging) Print("BUTTON IS IN SELECTED MODE. TAKE ACTION");
            bool already_selected = false;
            for (int j = 0; j < ArraySize(imp_filter_selected); j++) {
               if (impact_filter_selected[j] == selected_imp) {
                  already_selected = true;
                  break;
               }
            }
            if (!already_selected) {
               ArrayResize(imp_filter_selected, ArraySize(imp_filter_selected) + 1);
               imp_filter_selected[ArraySize(imp_filter_selected) - 1] = selected_importance_lvl;
               ArrayResize(impact_filter_selected, ArraySize(impact_filter_selected) + 1);
               impact_filter_selected[ArraySize(impact_filter_selected) - 1] = selected_imp;
               if (debugLogging) Print("Added to selected importance filters: ", selected_imp,"(",selected_importance_lvl,")");
            } else {
               if (debugLogging) Print("Importance level already selected: ", selected_imp,"(",selected_importance_lvl,")");
            }
         }
         if (debugLogging) Print("SELECTED ARRAY SIZE = ", ArraySize(imp_filter_selected)," >< ",ArraySize(impact_filter_selected));
         if (debugLogging) ArrayPrint(imp_filter_selected);
         if (debugLogging) ArrayPrint(impact_filter_selected);
         if (MQLInfoInteger(MQL_TESTER)) filters_changed = true;
         update_dashboard_values(curr_filter_selected,imp_filter_selected);
         ObjectSetInteger(0,sparam,OBJPROP_BORDER_COLOR,color_border);
         // Recalculate scrollbar
         ObjectDelete(0, SCROLL_LEADER);
         ObjectDelete(0, SCROLL_UP_REC);
         ObjectDelete(0, SCROLL_UP_LABEL);
         ObjectDelete(0, SCROLL_DOWN_REC);
         ObjectDelete(0, SCROLL_DOWN_LABEL);
         ObjectDelete(0, SCROLL_SLIDER);
         scroll_visible = totalEvents_Filtered > VISIBLE_ITEMS;
         if (debugLogging) Print("Scrollbar visibility: ", scroll_visible ? "Visible" : "Hidden");
         if (scroll_visible) {
            createRecLabel(SCROLL_LEADER, SCROLLBAR_X, SCROLLBAR_Y, SCROLLBAR_WIDTH, SCROLLBAR_HEIGHT, clrSilver, 1, clrNONE);
            int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
            color up_color = (scroll_pos == 0) ? clrLightGray : clrBlack;
            color down_color = (scroll_pos >= max_scroll) ? clrLightGray : clrBlack;
            createRecLabel(SCROLL_UP_REC, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_UP_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, SCROLLBAR_Y-5, CharToString(0x35), up_color, 15, "Webdings");
            int down_y = SCROLLBAR_Y + SCROLLBAR_HEIGHT - BUTTON_SIZE;
            createRecLabel(SCROLL_DOWN_REC, SCROLLBAR_X + BUTTON_OFFSET_X, down_y, BUTTON_WIDTH, BUTTON_SIZE, clrDarkGray, 1, clrDarkGray);
            createLabel(SCROLL_DOWN_LABEL, SCROLLBAR_X + BUTTON_OFFSET_X, down_y-5, CharToString(0x36), down_color, 15, "Webdings");
            slider_height = calculateSliderHeight();
            int slider_y = SCROLLBAR_Y + BUTTON_SIZE;
            createButton(SCROLL_SLIDER, SCROLLBAR_X + SLIDER_OFFSET_X, slider_y, SLIDER_WIDTH, slider_height, "", clrWhite, 12, clrLightSlateGray, clrDarkGray, "Arial Bold");
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_WIDTH, 2);
            if (debugLogging) Print("Scrollbar created: totalEvents_Filtered=", totalEvents_Filtered, ", slider_height=", slider_height);
            updateSliderPosition();
            updateButtonColors();
         }
         if (debugLogging) Print("SUCCESS. DASHBOARD UPDATED");
         ChartRedraw(0);
      }
      // Scrollbar button clicks
      if (scroll_visible && (sparam == SCROLL_UP_REC || sparam == SCROLL_UP_LABEL)) {
         scrollUp();
         updateButtonColors();
         if (debugLogging) Print("Up button clicked (", sparam, "). CurrPos: ", scroll_pos);
         ChartRedraw(0);
      }
      if (scroll_visible && (sparam == SCROLL_DOWN_REC || sparam == SCROLL_DOWN_LABEL)) {
         scrollDown();
         updateButtonColors();
         if (debugLogging) Print("Down button clicked (", sparam, "). CurrPos: ", scroll_pos);
         ChartRedraw(0);
      }
   }
   else if (id == CHARTEVENT_MOUSE_MOVE && scroll_visible) {
      if (prev_mouse_state == 0 && mouse_state == 1) {
         int xd = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_XDISTANCE);
         int yd = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE);
         int xs = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_XSIZE);
         int ys = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE);
         if (mouse_x >= xd && mouse_x <= xd + xs && mouse_y >= yd && mouse_y <= yd + ys) {
            moving_state_slider = true;
            mlb_down_x = mouse_x;
            mlb_down_y = mouse_y;
            mlb_down_yd_slider = yd;
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrDodgerBlue);
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height + 2);
            ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
            if (debugLogging) Print("Slider drag started at y=", mouse_y);
         }
      }
      if (moving_state_slider && mouse_state == 1) {
         int delta_y = mouse_y - mlb_down_y;
         int new_y = mlb_down_yd_slider + delta_y;
         int scroll_area_y_min = SCROLLBAR_Y + BUTTON_SIZE;
         int scroll_area_y_max = scroll_area_y_min + SCROLL_AREA_HEIGHT - slider_height;
         new_y = MathMax(scroll_area_y_min, MathMin(new_y, scroll_area_y_max));
         ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE, new_y);
         int max_scroll = MathMax(0, ArraySize(displayableEvents) - VISIBLE_ITEMS);
         double scroll_ratio = (double)(new_y - scroll_area_y_min) / (scroll_area_y_max - scroll_area_y_min);
         int new_scroll_pos = (int)MathRound(scroll_ratio * max_scroll);
         if (new_scroll_pos != scroll_pos) {
            scroll_pos = new_scroll_pos;
            update_dashboard_values(curr_filter_selected, imp_filter_selected);
            updateButtonColors();
            if (debugLogging) Print("Slider dragged. CurrPos: ", scroll_pos, ", Total steps: ", max_scroll, ", Slider y=", new_y);
         }
         ChartRedraw(0);
      }
      if (mouse_state == 0) {
         if (moving_state_slider) {
            moving_state_slider = false;
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_BGCOLOR, clrLightSlateGray);
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height);
            ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
            if (debugLogging) Print("Slider drag stopped.");
            ChartRedraw(0);
         }
      }
      prev_mouse_state = mouse_state;
   }
}

ここでは、スクロールバー要素のクリック時に実装していたロジックを、個別のフィルタボタンにも拡張しただけです。特に複雑な処理ではないため、詳細な説明は省略します。コンパイルすると、次の結果が得られます。

可視化から、すべてが正常に動作しており、イベントが動的に更新され、洗練された表示になっていることが確認できます。


結論

まとめると、連載「MQL5経済指標カレンダーを使った取引」を進化させ、動的スクロールバーと洗練されたイベント表示を導入しました。これにより、直感的なナビゲーションとニュースイベントへの確実なアクセスが可能になり、GIFで示した通り、操作性が大幅に向上しています。これらの強化は、第8回での最適化済みバックテストの上に構築されており、ライブモードとテスターモードの両方でシームレスな操作を保証します。この洗練されたダッシュボードを基盤として活用し、独自の取引要件に合わせてカスタマイズすることで、ニュース駆動型取引戦略をより効率的に運用できます。

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

添付されたファイル |
機械学習の限界を克服する(第2回):再現性の欠如 機械学習の限界を克服する(第2回):再現性の欠如
本記事では、同一の戦略と金融銘柄を用いても、ブローカーによって取引結果が大きく異なる理由について探ります。その背景には、価格が分散的に形成されていることや、データの不一致があるためです。本記事は、MQL5開発者がMQL5マーケットプレイスで自らの製品に対して賛否両論の評価を受ける理由を理解し、透明性が高く再現可能な成果を確保するためには、特定のブローカーに合わせたアプローチを取る必要があることを示唆しています。この取り組みが広く受け入れられれば、コミュニティにとって重要な実務上のベストプラクティスへと発展する可能性があります。
MQL5における高度な注文執行アルゴリズム:TWAP、VWAP、アイスバーグ注文 MQL5における高度な注文執行アルゴリズム:TWAP、VWAP、アイスバーグ注文
MQL5フレームワークで、機関投資家向けの高度な執行アルゴリズム(TWAP、VWAP、アイスバーグ注文)を小口トレーダー向けに提供します。統合された実行マネージャーとパフォーマンスアナライザーを用いて、注文の分割(スライシング)や分析をよりスムーズかつ正確に行える環境を提供します。
プライスアクション分析ツールキットの開発(第23回):Currency Strength Meter プライスアクション分析ツールキットの開発(第23回):Currency Strength Meter
通貨ペアの方向性を本当に決定しているのは何でしょうか。それは各通貨自体の強さです。本記事では、通貨の強さを、その通貨が含まれるすべてのペアを順に分析することで測定します。この洞察により、各通貨ペアが相対的な強さに基づいてどのように動くかを予測することができます。詳しくは本稿をご覧ください。
データサイエンスとML(第40回):機械学習データにおけるフィボナッチリトレースメントの利用 データサイエンスとML(第40回):機械学習データにおけるフィボナッチリトレースメントの利用
フィボナッチリトレースメントはテクニカル分析で人気のツールであり、トレーダーが潜在的な反転ゾーンを特定するのに役立ちます。本記事では、これらのリトレースメントレベルを機械学習モデルの目的変数に変換し、この強力なツールを使用して市場をより深く理解できるようにする方法について説明します。