English Deutsch
preview
MQL5取引ツール(第7回):複数銘柄ポジションと口座監視のための情報ダッシュボード

MQL5取引ツール(第7回):複数銘柄ポジションと口座監視のための情報ダッシュボード

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

はじめに

前回の記事(第6回)では、MetaQuotes Language 5 (MQL5)でダイナミックホログラフィックダッシュボードを開発しました。このダッシュボードは、銘柄と時間足を監視でき、RSI、ボラティリティアラート、パルスアニメーション付きのインタラクティブコントロールを備えていました。今回の第7回では、複数銘柄のポジション、総取引数、ロット数、利益、未約定注文、スワップ、手数料、残高や証拠金といった口座指標を追跡する情報ダッシュボードを作成します。ソート可能な列やComma Separated Values (CSV)エクスポートも実装し、包括的な管理を可能にします。本記事では以下のトピックを扱います。

  1. 情報ダッシュボードのアーキテクチャの理解
  2. MQL5での実装
  3. バックテスト
  4. 結論

この記事を読み終える頃には、ポジションや口座管理用の強力なMQL5ダッシュボードを手に入れ、自由にカスタマイズできる状態になります。それでは始めましょう。


情報ダッシュボードのアーキテクチャの理解

複数銘柄にまたがるポジションや、重要な口座指標を一元的に確認できる情報ダッシュボードを開発しています。これにより、画面を切り替えることなくパフォーマンスを把握できるようになります。このアーキテクチャの重要なポイントは、分散した取引データをソート可能なグリッドに整理し、リアルタイムの合計表示やエクスポート機能を提供することで、過剰なドローダウンやポジションの偏りといった問題を素早く把握できる点です。

ダッシュボードでは、銘柄ごとの買いポジション、売りポジション、ロット数、利益などの詳細を収集すると同時に、口座の残高、証拠金、余剰証拠金も表示します。すべてインタラクティブなソート機能と、視覚効果を適度に組み合わせて、操作性と視認性を向上させます。銘柄をループしてデータを収集し、合計することで、ダッシュボードは軽量でライブトレード環境でも応答性を維持できる設計です。以下の可視化を確認後、実装に進みましょう。

建築計画


MQL5での実装

MQL5でプログラムを作成するには、まずプログラムのメタデータを定義し、その後、コードを直接編集せずに動作を簡単に変更できるように、入力パラメータを定義します。また、ダッシュボード上のオブジェクトもここで定義します。

//+------------------------------------------------------------------+
//|                                     Informational Dashboard.mq5  |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"

// Input parameters
input int UpdateIntervalMs = 100; // Update interval (milliseconds, min 10ms)
input long MagicNumber = -1; // Magic number (-1 for all positions and orders)

// Defines for object names
#define PREFIX "DASH_"                   //--- Prefix for all dashboard objects
#define HEADER "HEADER_"                 //--- Prefix for header labels
#define SYMB "SYMB_"                     //--- Prefix for symbol labels
#define DATA "DATA_"                     //--- Prefix for data labels
#define HEADER_PANEL "HEADER_PANEL"      //--- Name for header panel
#define ACCOUNT_PANEL "ACCOUNT_PANEL"    //--- Name for account panel
#define FOOTER_PANEL "FOOTER_PANEL"      //--- Name for footer panel
#define FOOTER_TEXT "FOOTER_TEXT"        //--- Name for footer text label
#define FOOTER_DATA "FOOTER_DATA_"       //--- Prefix for footer data labels
#define PANEL "PANEL"                    //--- Name for main panel
#define ACC_TEXT "ACC_TEXT_"             //--- Prefix for account text labels
#define ACC_DATA "ACC_DATA_"             //--- Prefix for account data labels

ここでは、MQL5の情報ダッシュボードにおける入力パラメータを設定し、ユーザーインターフェース(UI)要素のカスタマイズや整理された命名を可能にするオブジェクト名の定数を定義します。UpdateIntervalMsは100ミリ秒(最小10ミリ秒)として定義し、ダッシュボードの更新間隔を制御します。これにより、システムに過負荷をかけずにタイムリーな更新が可能になります。MagicNumberは「-1」に設定してすべてのポジションと注文を監視することもできますし、EAのマジックナンバーを指定して特定のポジションだけを追跡することもできます。

オブジェクトの命名を統一するために#defineを使用します。「PREFIX」はすべてのダッシュボードオブジェクトに「DASH_」を接頭辞として付与します。ヘッダーラベルには「HEADER」を、銘柄ラベルには「SYMB_」を、データラベルには「DATA_」を使用します。ヘッダーパネルは「HEADER_PANEL」、口座情報セクションは「ACCOUNT_PANEL」、フッターパネルは「FOOTER_PANEL」、フッターテキストは「FOOTER_TEXT」、フッターデータの接頭辞には「FOOTER_DATA_」を使用します。メインパネルは「PANEL」、口座テキストの接頭辞には「ACC_TEXT_」、口座データの接頭辞には「ACC_DATA_」をそれぞれ割り当てています。これらの定義により、オブジェクトの管理が容易になり、コードの可読性も向上します。次におこなうべきことは、情報ダッシュボードで扱うデータを保持する構造体を作成し、実装全体で使用するグローバル変数を定義することです。

// Dashboard settings
struct DashboardSettings {               //--- Structure for dashboard settings
   int panel_x;                          //--- X-coordinate of panel
   int panel_y;                          //--- Y-coordinate of panel
   int row_height;                       //--- Height of each row
   int font_size;                        //--- Font size for labels
   string font;                          //--- Font type for labels
   color bg_color;                       //--- Background color of main panel
   color border_color;                   //--- Border color of panels
   color header_color;                   //--- Default color for header text
   color text_color;                     //--- Default color for text
   color section_bg_color;               //--- Background color for header/footer panels
   int zorder_panel;                     //--- Z-order for main panel
   int zorder_subpanel;                  //--- Z-order for sub-panels
   int zorder_labels;                    //--- Z-order for labels
   int label_y_offset;                   //--- Y-offset for label positioning
   int label_x_offset;                   //--- X-offset for label positioning
   int header_x_distances[9];            //--- X-distances for header labels (9 columns)
   color header_shades[12];              //--- Array of header color shades for glow effect
} settings = {                           //--- Initialize settings with default values
   20,                                   //--- Set panel_x to 20 pixels
   20,                                   //--- Set panel_y to 20 pixels
   24,                                   //--- Set row_height to 24 pixels
   11,                                   //--- Set font_size to 11
   "Calibri Bold",                       //--- Set font to Calibri Bold
   C'240,240,240',                       //--- Set bg_color to light gray
   clrBlack,                             //--- Set border_color to black
   C'0,50,70',                           //--- Set header_color to dark teal
   clrBlack,                             //--- Set text_color to black
   C'200,220,230',                       //--- Set section_bg_color to light blue-gray
   100,                                  //--- Set zorder_panel to 100
   101,                                  //--- Set zorder_subpanel to 101
   102,                                  //--- Set zorder_labels to 102
   3,                                    //--- Set label_y_offset to 3 pixels
   25,                                   //--- Set label_x_offset to 25 pixels
   {10, 120, 170, 220, 280, 330, 400, 470, 530}, //--- X-distances for 9 columns
   {C'0,0,0', C'255,0,0', C'0,255,0', C'0,0,255', C'255,255,0', C'0,255,255', 
    C'255,0,255', C'255,255,255', C'255,0,255', C'0,255,255', C'255,255,0', C'0,0,255'}
};

// Data structure for symbol information
struct SymbolData {                      //--- Structure for symbol data
   string name;                          //--- Symbol name
   int buys;                             //--- Number of buy positions
   int sells;                            //--- Number of sell positions
   int trades;                           //--- Total number of trades
   double lots;                          //--- Total lots
   double profit;                        //--- Total profit
   int pending;                          //--- Number of pending orders
   double swaps;                         //--- Total swap
   double comm;                          //--- Total commission
   string buys_str;                      //--- String representation of buys
   string sells_str;                     //--- String representation of sells
   string trades_str;                    //--- String representation of trades
   string lots_str;                      //--- String representation of lots
   string profit_str;                    //--- String representation of profit
   string pending_str;                   //--- String representation of pending
   string swaps_str;                     //--- String representation of swap
   string comm_str;                      //--- String representation of commission
};

// Global variables
SymbolData symbol_data[];                //--- Array to store symbol data
long totalBuys = 0;                      //--- Total buy positions across symbols
long totalSells = 0;                     //--- Total sell positions across symbols
long totalTrades = 0;                    //--- Total trades across symbols
double totalLots = 0.0;                  //--- Total lots across symbols
double totalProfit = 0.0;                //--- Total profit across symbols
long totalPending = 0;                   //--- Total pending across symbols
double totalSwap = 0.0;                  //--- Total swap across symbols
double totalComm = 0.0;                  //--- Total commission across symbols
string headers[] = {"Symbol", "Buy P", "Sell P", "Trades", "Lots", "Profit", "Pending", "Swap", "Comm"}; //--- Header labels
int column_widths[] = {140, 50, 50, 50, 60, 90, 50, 60, 60}; //--- Widths for each column
color data_default_colors[] = {clrRed, clrGreen, clrDarkGray, clrOrange, clrGray, clrBlue, clrPurple, clrBrown};
int sort_column = 3;                     //--- Initial sort column (trades) 
bool sort_ascending = false;             //--- Sort direction (false for descending to show active first)
int glow_index = 0;                      //--- Current index for header glow effect
bool glow_direction = true;              //--- Glow direction (true for forward)
int glow_counter = 0;                    //--- Counter for glow timing
const int GLOW_INTERVAL_MS = 500;        //--- Glow cycle interval (500ms)
string total_buys_str = "";              //--- String for total buys display
string total_sells_str = "";             //--- String for total sells display
string total_trades_str = "";            //--- String for total trades display
string total_lots_str = "";              //--- String for total lots display
string total_profit_str = "";            //--- String for total profit display
string total_pending_str = "";           //--- String for total pending display
string total_swap_str = "";              //--- String for total swap display
string total_comm_str = "";              //--- String for total comm display
string account_items[] = {"Balance", "Equity", "Free Margin"}; //--- Account items
string acc_bal_str = "";                 //--- Strings for account data
string acc_eq_str = "";
string acc_free_str = "";
int prev_num_symbols = 0;                //--- Previous number of active symbols for dynamic resizing

UIとデータ管理を設定するために、「DashboardSettings」構造体を定義し、レイアウト設定を保持します。「panel_x」と「panel_y」は位置指定のために20ピクセル、「row_height」は行間隔として24ピクセル、「font_size」はテキスト用に11、「font」は「Calibri Bold」としてスタイルを指定します。「bg_color」はメインパネル用に淡いグレー、「border_color」はパネル枠用に黒、「header_color」はヘッダー用に濃いティール、「text_color」は一般テキスト用に黒、「section_bg_color」はヘッダーおよびフッターパネル用に淡いブルーグレーに設定します。レイヤリング用に「zorder_panel」を100、「zorder_subpanel」を101、「zorder_labels」を102とし、ラベルの位置合わせのために「label_y_offset」を3、「label_x_offset」を25に設定します。「header_x_distances」は9列の位置、「header_shades」は12色でグロー効果用に指定しています。

次に、銘柄ごとのデータを格納する「SymbolData」構造体を作成します。「name」は銘柄名、「buys」「sells」「trades」「pending」は件数、「lots」「profit」「swaps」「comm」は値を格納し、表示用に「buys_str」のような対応する文字列フィールドも用意します。グローバル変数としては、銘柄データ用の「symbol_data」配列、long型の「totalBuys」「totalSells」「totalTrades」「totalPending」を0で初期化、double型の「totalLots」「totalProfit」「totalSwap」「totalComm」を0で初期化します。また、列ラベル用の「headers」配列、列幅用の「column_widths」、列ごとの色指定用の「data_default_colors」を用意します。デフォルトのソートは取引件数で「sort_column」を3、降順として「sort_ascending」をfalseに設定します。ヘダーグロー用に「glow_index」と「glow_counter」を0で初期化、「glow_direction」をtrue、「GLOW_INTERVAL_MS」を500msに設定します。合計表示用の文字列変数として「total_buys_str」、口座情報ラベル用に「account_items」、残高・証拠金・余剰証拠金の文字列表現として「acc_bal_str」などを用意し、動的リサイズ用に「prev_num_symbols」を0で初期化します。

これらのコンポーネントにより、リアルタイムでポジションを追跡するためのダッシュボードのレイアウトとデータフレームワークが確立されます。次に、プログラムをよりモジュール化するためのヘルパー関数を定義していきます。まずはラベル用の関数から始めます。これは頻繁に扱う要素だからです。

//+------------------------------------------------------------------+
//| Create label function                                            |
//+------------------------------------------------------------------+
bool createLABEL(string objName, string txt, int xD, int yD, color clrTxt, int fontSize, string font, int anchor, bool selectable = false) {
   if(!ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0)) { //--- Create label object
      Print(__FUNCTION__, ": Failed to create label '", objName, "'. Error code = ", GetLastError()); //--- Log creation failure
      return(false);                     //--- Return failure
   }
   ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xD); //--- Set x-coordinate
   ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yD); //--- Set y-coordinate
   ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner alignment
   ObjectSetString(0, objName, OBJPROP_TEXT, txt); //--- Set text
   ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontSize); //--- Set font size
   ObjectSetString(0, objName, OBJPROP_FONT, font); //--- Set font type
   ObjectSetInteger(0, objName, OBJPROP_COLOR, clrTxt); //--- Set text color
   ObjectSetInteger(0, objName, OBJPROP_BACK, false); //--- Set to foreground
   ObjectSetInteger(0, objName, OBJPROP_STATE, selectable); //--- Set selectable state
   ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, selectable); //--- Set selectability
   ObjectSetInteger(0, objName, OBJPROP_SELECTED, false); //--- Set not selected
   ObjectSetInteger(0, objName, OBJPROP_ANCHOR, anchor); //--- Set anchor point
   ObjectSetInteger(0, objName, OBJPROP_ZORDER, settings.zorder_labels); //--- Set z-order
   ObjectSetString(0, objName, OBJPROP_TOOLTIP, selectable ? "Click to sort" : "Position data"); //--- Set tooltip
   ChartRedraw(0);                       //--- Redraw chart
   return(true);                         //--- Return success
}

//+------------------------------------------------------------------+
//| Update label function                                            |
//+------------------------------------------------------------------+
bool updateLABEL(string objName, string txt, color clrTxt) {
   int found = ObjectFind(0, objName);   //--- Find object
   if(found < 0) {                       //--- Check if object not found
      Print(__FUNCTION__, ": Failed to find label '", objName, "'. Error code = ", GetLastError()); //--- Log error
      return(false);                     //--- Return failure
   }
   string current_txt = ObjectGetString(0, objName, OBJPROP_TEXT); //--- Get current text
   if(current_txt != txt) {              //--- Check if text changed
      ObjectSetString(0, objName, OBJPROP_TEXT, txt); //--- Update text
      ObjectSetInteger(0, objName, OBJPROP_COLOR, clrTxt); //--- Update color
      return(true);                      //--- Indicate redraw needed
   }
   return(false);                        //--- No update needed
}

ダッシュボード用のテキストラベルを生成する「createLABEL」関数を実装します。この関数は「objName」「txt」「xD」「yD」「clrTxt」「fontSize」「font」「anchor」「selectable」をパラメータとして受け取ります。ラベルはObjectCreate関数でOBJ_LABELとして作成し、作成に失敗した場合は、Printでログを出力し、falseを返します。その後、ObjectSetInteger関数で各種プロパティを設定します。設定するプロパティはOBJPROP_XDISTANCE、OBJPROP_YDISTANCE、OBJPROP_CORNERを「CORNER_LEFT_UPPER」、OBJPROP_FONTSIZE、OBJPROP_COLOR、OBJPROP_BACKをfalse、OBJPROP_STATEとOBJPROP_SELECTABLEは「selectable」に応じて、OBJPROP_SELECTEDをfalse、OBJPROP_ANCHOR、OBJPROP_ZORDERは「settings.zorder_labels」です。さらに、ObjectSetString でOBJPROP_TEXTとOBJPROP_FONTを設定し、ソートやデータ用のツールチップとしてOBJPROP_TOOLTIPも指定します。最後にChartRedrawで再描画し、trueを返します。

既存ラベルを更新する「updateLABEL」関数では、まずObjectFindで「objName」を確認し、見つからなければログを出力してfalseを返します。ObjectGetStringで取得した「current_txt」が「txt」と異なる場合は、OBJPROP_TEXTとOBJPROP_COLORをObjectSetStringとObjectSetIntegerで更新し、再描画が必要であればtrueを返します。差異がなければfalseを返します。これらの関数により、ダッシュボードの表示用ラベルを柔軟に作成し、効率的に更新できるようになります。次に、必要な情報をすべて収集するための他のヘルパー関数を作成していきます。

//+------------------------------------------------------------------+
//| Count total positions for a symbol                               |
//+------------------------------------------------------------------+
string countPositionsTotal(string symbol) {
   int totalPositions = 0;               //--- Initialize position counter
   int count_Total_Pos = PositionsTotal(); //--- Get total positions
   for(int i = count_Total_Pos - 1; i >= 0; i--) { //--- Iterate through positions
      ulong ticket = PositionGetTicket(i); //--- Get position ticket
      if(ticket > 0 && PositionSelectByTicket(ticket)) { //--- Check if position selected
         if(PositionGetString(POSITION_SYMBOL) == symbol && (MagicNumber < 0 || PositionGetInteger(POSITION_MAGIC) == MagicNumber)) totalPositions++; //--- Check symbol and magic
      }
   }
   return IntegerToString(totalPositions); //--- Return total as string
}

//+------------------------------------------------------------------+
//| Count buy or sell positions for a symbol                         |
//+------------------------------------------------------------------+
string countPositions(string symbol, ENUM_POSITION_TYPE pos_type) {
   int totalPositions = 0;               //--- Initialize position counter
   int count_Total_Pos = PositionsTotal(); //--- Get total positions
   for(int i = count_Total_Pos - 1; i >= 0; i--) { //--- Iterate through positions
      ulong ticket = PositionGetTicket(i); //--- Get position ticket
      if(ticket > 0 && PositionSelectByTicket(ticket)) { //--- Check if position selected
         if(PositionGetString(POSITION_SYMBOL) == symbol && PositionGetInteger(POSITION_TYPE) == pos_type && (MagicNumber < 0 || PositionGetInteger(POSITION_MAGIC) == MagicNumber)) { //--- Check symbol, type, magic
            totalPositions++;            //--- Increment counter
         }
      }
   }
   return IntegerToString(totalPositions); //--- Return total as string
}

//+------------------------------------------------------------------+
//| Count pending orders for a symbol                                |
//+------------------------------------------------------------------+
string countOrders(string symbol) {
   int total = 0;                        //--- Initialize counter
   int tot = OrdersTotal();              //--- Get total orders
   for(int i = tot - 1; i >= 0; i--) {   //--- Iterate through orders
      ulong ticket = OrderGetTicket(i);  //--- Get order ticket
      if(ticket > 0 && OrderSelect(ticket)) { //--- Check if order selected
         if(OrderGetString(ORDER_SYMBOL) == symbol && (MagicNumber < 0 || OrderGetInteger(ORDER_MAGIC) == MagicNumber)) total++; //--- Check symbol and magic
      }
   }
   return IntegerToString(total);        //--- Return total as string
}

//+------------------------------------------------------------------+
//| Sum double property for positions of a symbol                    |
//+------------------------------------------------------------------+
string sumPositionDouble(string symbol, ENUM_POSITION_PROPERTY_DOUBLE prop) {
   double total = 0.0;                   //--- Initialize total
   int count_Total_Pos = PositionsTotal(); //--- Get total positions
   for(int i = count_Total_Pos - 1; i >= 0; i--) { //--- Iterate through positions
      ulong ticket = PositionGetTicket(i); //--- Get position ticket
      if(ticket > 0 && PositionSelectByTicket(ticket)) { //--- Check if position selected
         if(PositionGetString(POSITION_SYMBOL) == symbol && (MagicNumber < 0 || PositionGetInteger(POSITION_MAGIC) == MagicNumber)) { //--- Check symbol and magic
            total += PositionGetDouble(prop); //--- Add property value
         }
      }
   }
   return DoubleToString(total, 2);      //--- Return total as string
}

//+------------------------------------------------------------------+
//| Sum commission for positions of a symbol from history            |
//+------------------------------------------------------------------+
double sumPositionCommission(string symbol) {
   double total_comm = 0.0;              //--- Initialize total commission
   int pos_total = PositionsTotal();     //--- Get total positions
   for(int p = 0; p < pos_total; p++) {  //--- Iterate through positions
      ulong ticket = PositionGetTicket(p); //--- Get position ticket
      if(ticket > 0 && PositionSelectByTicket(ticket)) { //--- Check if selected
         if(PositionGetString(POSITION_SYMBOL) == symbol && (MagicNumber < 0 || PositionGetInteger(POSITION_MAGIC) == MagicNumber)) { //--- Check symbol and magic
            long pos_id = PositionGetInteger(POSITION_IDENTIFIER); //--- Get position ID
            if(HistorySelectByPosition(pos_id)) { //--- Select history by position
               int deals_total = HistoryDealsTotal(); //--- Get total deals
               for(int d = 0; d < deals_total; d++) { //--- Iterate through deals
                  ulong deal_ticket = HistoryDealGetTicket(d); //--- Get deal ticket
                  if(deal_ticket > 0) {    //--- Check valid
                     total_comm += HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION); //--- Add commission
                  }
               }
            }
         }
      }
   }
   return total_comm;                    //--- Return total commission
}

//+------------------------------------------------------------------+
//| Collect active symbols with positions or orders                  |
//+------------------------------------------------------------------+
void CollectActiveSymbols() {
   string symbols_temp[];
   int added = 0;
   // Collect from positions
   int pos_total = PositionsTotal();
   for(int i = 0; i < pos_total; i++) {
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0) continue;
      PositionSelectByTicket(ticket);
      if(MagicNumber < 0 || PositionGetInteger(POSITION_MAGIC) == MagicNumber) {
         string sym = PositionGetString(POSITION_SYMBOL);
         bool found = false;
         for(int k = 0; k < added; k++) {
            if(symbols_temp[k] == sym) {
               found = true;
               break;
            }
         }
         if(!found) {
            ArrayResize(symbols_temp, added + 1);
            symbols_temp[added] = sym;
            added++;
         }
      }
   }
   // Collect from orders
   int ord_total = OrdersTotal();
   for(int i = 0; i < ord_total; i++) {
      ulong ticket = OrderGetTicket(i);
      if(ticket == 0) continue;
      bool isSelected = OrderSelect(ticket);
      if(MagicNumber < 0 || OrderGetInteger(ORDER_MAGIC) == MagicNumber) {
         string sym = OrderGetString(ORDER_SYMBOL);
         bool found = false;
         for(int k = 0; k < added; k++) {
            if(symbols_temp[k] == sym) {
               found = true;
               break;
            }
         }
         if(!found) {
            ArrayResize(symbols_temp, added + 1);
            symbols_temp[added] = sym;
            added++;
         }
      }
   }
   // Set symbol_data
   ArrayResize(symbol_data, added);
   for(int i = 0; i < added; i++) {
      symbol_data[i].name = symbols_temp[i];
      symbol_data[i].buys = 0;
      symbol_data[i].sells = 0;
      symbol_data[i].trades = 0;
      symbol_data[i].lots = 0.0;
      symbol_data[i].profit = 0.0;
      symbol_data[i].pending = 0;
      symbol_data[i].swaps = 0.0;
      symbol_data[i].comm = 0.0;
      symbol_data[i].buys_str = "0";
      symbol_data[i].sells_str = "0";
      symbol_data[i].trades_str = "0";
      symbol_data[i].lots_str = "0.00";
      symbol_data[i].profit_str = "0.00";
      symbol_data[i].pending_str = "0";
      symbol_data[i].swaps_str = "0.00";
      symbol_data[i].comm_str = "0.00";
   }
}

ここでは、複数の銘柄にわたる正確なポジションおよび注文の追跡を可能にする、取引データを収集して集計するユーティリティ関数を実装します。「countPositionsTotal」関数は、指定した「symbol」のすべてのポジションをカウントします。PositionsTotalをループで回し、各「ticket」をPositionGetTicketPositionSelectByTicketで選択します。銘柄が一致し、かつ「MagicNumber」が-1またはPositionGetIntegerで取得したPOSITION_MAGICと一致する場合、「totalPositions」をインクリメントします。最終的にIntegerToStringで文字列としてカウントを返します。

「countPositions」関数は、「symbol」と「pos_type」に応じた買いまたは売りポジションをカウントします。同様にポジションをループで確認し、POSITION_TYPEが「pos_type」と一致する場合にカウントし、文字列として返します。「countOrders」関数は、「symbol」の未約定注文をカウントします。OrdersTotalをループで回し、OrderGetTicketとOrderSelectで各「ticket」を選択します。銘柄と「MagicNumber」が一致する場合に「total」をインクリメントし、文字列として返します。「sumPositionDouble」関数は、指定した「symbol」のボリューム、利益、スワップなどの倍精度プロパティを合計します。ポジションをループで確認し、条件が一致する場合にPositionGetDoubleで取得した値を加算し、DoubleToStringで小数点以下2桁に整形して返します。

「sumPositionCommission」関数は、指定した「symbol」の手数料合計を計算します。ポジションをループで確認し、PositionGetIntegerで取得した「pos_id」を用いてHistorySelectByPositionで取引履歴を取得します。各有効な「deal_ticket」をHistoryDealGetTicketで取得し、HistoryDealGetDoubleのDEAL_COMMISSIONを合計し、最終的に合計を返します。

「CollectActiveSymbols」関数は、アクティブなポジションや注文がある銘柄を「symbols_temp」に収集します。PositionsTotalとOrdersTotalをループで確認し、「MagicNumber」の条件をチェックして、一意の銘柄をArrayResizeで追加します。その後、symbol_dataをサイズに合わせてサイズ変更し、「name」や件数、文字列フィールドを0またはデフォルト値で初期化します。これらの関数により、ダッシュボードは正確な取引データを効率的に収集し、表示できるようになります。ここまでで、ダッシュボードを初期化するために必要なすべての関数が揃いました。次に、OnInitイベントハンドラ内でダッシュボードを作成し、アップグレードを継続的に追跡できるように進めていきます。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   // Collect active symbols first
   CollectActiveSymbols();
   int num_rows = ArraySize(symbol_data);
   // Calculate dimensions
   int num_columns = ArraySize(headers);    //--- Get number of columns
   int column_width_sum = 0;                //--- Initialize sum of column widths
   for(int i = 0; i < num_columns; i++)     //--- Iterate through columns
      column_width_sum += column_widths[i]; //--- Add column width to sum
   int panel_width = MathMax(settings.header_x_distances[num_columns - 1] + column_widths[num_columns - 1], column_width_sum) + 20 + settings.label_x_offset; //--- Calculate panel width

   // Create main panel in foreground
   string panel_name = PREFIX + PANEL;                                                   //--- Define main panel name
   ObjectCreate(0, panel_name, OBJ_RECTANGLE_LABEL, 0, 0, 0);                            //--- Create main panel
   ObjectSetInteger(0, panel_name, OBJPROP_CORNER, CORNER_LEFT_UPPER);                   //--- Set panel corner
   ObjectSetInteger(0, panel_name, OBJPROP_XDISTANCE, settings.panel_x);                 //--- Set panel x-coordinate
   ObjectSetInteger(0, panel_name, OBJPROP_YDISTANCE, settings.panel_y);                 //--- Set panel y-coordinate
   ObjectSetInteger(0, panel_name, OBJPROP_XSIZE, panel_width);                          //--- Set panel width
   ObjectSetInteger(0, panel_name, OBJPROP_YSIZE, (num_rows + 3) * settings.row_height); //--- Set panel height
   ObjectSetInteger(0, panel_name, OBJPROP_BGCOLOR, settings.bg_color);                  //--- Set background color
   ObjectSetInteger(0, panel_name, OBJPROP_BORDER_TYPE, BORDER_FLAT);                    //--- Set border type
   ObjectSetInteger(0, panel_name, OBJPROP_BORDER_COLOR, settings.border_color);         //--- Set border color
   ObjectSetInteger(0, panel_name, OBJPROP_BACK, false);                                 //--- Set panel to foreground
   ObjectSetInteger(0, panel_name, OBJPROP_ZORDER, settings.zorder_panel);               //--- Set z-order

   // Create header panel
   string header_panel = PREFIX + HEADER_PANEL;                                          //--- Define header panel name
   ObjectCreate(0, header_panel, OBJ_RECTANGLE_LABEL, 0, 0, 0);                          //--- Create header panel
   ObjectSetInteger(0, header_panel, OBJPROP_CORNER, CORNER_LEFT_UPPER);                 //--- Set header panel corner
   ObjectSetInteger(0, header_panel, OBJPROP_XDISTANCE, settings.panel_x);               //--- Set header panel x-coordinate
   ObjectSetInteger(0, header_panel, OBJPROP_YDISTANCE, settings.panel_y);               //--- Set header panel y-coordinate
   ObjectSetInteger(0, header_panel, OBJPROP_XSIZE, panel_width);                        //--- Set header panel width
   ObjectSetInteger(0, header_panel, OBJPROP_YSIZE, settings.row_height);                //--- Set header panel height
   ObjectSetInteger(0, header_panel, OBJPROP_BGCOLOR, settings.section_bg_color);        //--- Set header panel background color
   ObjectSetInteger(0, header_panel, OBJPROP_BORDER_TYPE, BORDER_FLAT);                  //--- Set header panel border type
   ObjectSetInteger(0, header_panel, OBJPROP_BORDER_COLOR, settings.border_color);       //--- Set border color
   ObjectSetInteger(0, header_panel, OBJPROP_ZORDER, settings.zorder_subpanel);          //--- Set header panel z-order

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

OnInitイベントハンドラ内では、ポジションおよび口座監視用のユーザーインターフェース基盤を設定するロジックを初期化します。まず「CollectActiveSymbols」関数を呼び出し、アクティブな銘柄で「symbol_data」配列を埋め、「num_rows」をArraySize関数でそのサイズに設定します。「num_columns」は「headers」配列から計算し、「column_width_sum」はforループで「column_widths」を順に加算して求めます。「panel_width」はMathMaxを用いて、最後の「header_x_distances」に対応する「column_widths」と「column_width_sum」を足し、パディングとして20と「settings.label_x_offset」を加えて決定します。

次に、メインパネルをObjectCreateOBJ_RECTANGLE_LABELとして作成し、名前は「PREFIX + PANEL」に設定します。プロパティとして「OBJPROP_CORNER」を「CORNER_LEFT_UPPER」、「OBJPROP_XDISTANCE」と「OBJPROP_YDISTANCE」を「settings.panel_x」と「settings.panel_y」から取得、「OBJPROP_XSIZE」を「panel_width」、「OBJPROP_YSIZE」を「(num_rows + 3) * settings.row_height」、「OBJPROP_BGCOLOR」を「settings.bg_color」、「OBJPROP_BORDER_TYPE」を「BORDER_FLAT」、「OBJPROP_BORDER_COLOR」を「settings.border_color」、「OBJPROP_BACK」をfalse、「OBJPROP_ZORDER」を「settings.zorder_panel」に設定します。ヘッダーパネルも同様の方法で作成します。最後に「INIT_SUCCEEDED」を返すことで、初期化が正常に完了したことを示します。これにより、データ表示用のダッシュボードのコアパネルが確立され、コンパイル後には指定した構成のパネルが表示されます。

メインパネルとヘッダーパネル

基盤が確立されたので、次に他のサブパネルやラベルを作成できます。これを実現するために、次のロジックを使用します。

// Create headers with manual X-distances
int header_y = settings.panel_y + 8 + settings.label_y_offset; //--- Calculate header y-coordinate
for(int i = 0; i < num_columns; i++) {                         //--- Iterate through headers
   string header_name = PREFIX + HEADER + IntegerToString(i);  //--- Define header label name
   int header_x = settings.panel_x + settings.header_x_distances[i] + settings.label_x_offset; //--- Calculate header x-coordinate
   createLABEL(header_name, headers[i], header_x, header_y, settings.header_color, 12, settings.font, ANCHOR_LEFT, true); //--- Create header label
}

// Create symbol labels and data labels
int first_row_y = header_y + settings.row_height;                                       //--- Calculate y-coordinate for first row
int symbol_x = settings.panel_x + 10 + settings.label_x_offset;                         //--- Set x-coordinate for symbol labels
for(int i = 0; i < num_rows; i++) {                                                     //--- Iterate through symbols
   string symbol_name = PREFIX + SYMB + IntegerToString(i);                             //--- Define symbol label name
   createLABEL(symbol_name, symbol_data[i].name, symbol_x, first_row_y + i * settings.row_height + settings.label_y_offset, settings.text_color, settings.font_size, settings.font, ANCHOR_LEFT); //--- Create symbol label
   int x_offset = settings.panel_x + 10 + column_widths[0] + settings.label_x_offset;   //--- Set initial x-offset for data labels
   for(int j = 0; j < num_columns - 1; j++) {                                           //--- Iterate through data columns
      string data_name = PREFIX + DATA + IntegerToString(i) + "_" + IntegerToString(j); //--- Define data label name
      color initial_color = data_default_colors[j];                                     //--- Set initial color
      string initial_txt = (j <= 2 || j == 5) ? "0" : "0.00";                           //--- Set initial text
      createLABEL(data_name, initial_txt, x_offset, first_row_y + i * settings.row_height + settings.label_y_offset, initial_color, settings.font_size, settings.font, ANCHOR_RIGHT); //--- Create data label
      x_offset += column_widths[j + 1];                                                 //--- Update x-offset
   }
}

// Create footer panel at the bottom
int footer_y = settings.panel_y + (num_rows + 3) * settings.row_height - settings.row_height - 5; //--- Calculate footer y-coordinate
string footer_panel = PREFIX + FOOTER_PANEL;                                    //--- Define footer panel name
ObjectCreate(0, footer_panel, OBJ_RECTANGLE_LABEL, 0, 0, 0);                    //--- Create footer panel
ObjectSetInteger(0, footer_panel, OBJPROP_CORNER, CORNER_LEFT_UPPER);           //--- Set footer panel corner
ObjectSetInteger(0, footer_panel, OBJPROP_XDISTANCE, settings.panel_x);         //--- Set footer panel x-coordinate
ObjectSetInteger(0, footer_panel, OBJPROP_YDISTANCE, footer_y);                 //--- Set footer panel y-coordinate
ObjectSetInteger(0, footer_panel, OBJPROP_XSIZE, panel_width);                  //--- Set footer panel width
ObjectSetInteger(0, footer_panel, OBJPROP_YSIZE, settings.row_height + 5);      //--- Set footer panel height
ObjectSetInteger(0, footer_panel, OBJPROP_BGCOLOR, settings.section_bg_color);  //--- Set footer panel background color
ObjectSetInteger(0, footer_panel, OBJPROP_BORDER_TYPE, BORDER_FLAT);            //--- Set footer panel border type
ObjectSetInteger(0, footer_panel, OBJPROP_BORDER_COLOR, settings.border_color); //--- Set border color
ObjectSetInteger(0, footer_panel, OBJPROP_ZORDER, settings.zorder_subpanel);    //--- Set footer panel z-order

// Create footer text and data
int footer_text_x = settings.panel_x + 10 + settings.label_x_offset;               //--- Set x-coordinate for footer text
createLABEL(PREFIX + FOOTER_TEXT, "Total:", footer_text_x, footer_y + 8 + settings.label_y_offset, settings.text_color, settings.font_size, settings.font, ANCHOR_LEFT); //--- Create footer text label
int x_offset = settings.panel_x + 10 + column_widths[0] + settings.label_x_offset; //--- Set initial x-offset for footer data
for(int j = 0; j < num_columns - 1; j++) {                                         //--- Iterate through footer data columns
   string footer_data_name = PREFIX + FOOTER_DATA + IntegerToString(j);            //--- Define footer data label name
   color footer_color = data_default_colors[j];                                    //--- Set footer data color
   string initial_txt = (j <= 2 || j == 5) ? "0" : "0.00";                         //--- Set initial text
   createLABEL(footer_data_name, initial_txt, x_offset, footer_y + 8 + settings.label_y_offset, footer_color, settings.font_size, settings.font, ANCHOR_RIGHT); //--- Create footer data label
   x_offset += column_widths[j + 1];                                               //--- Update x-offset
}

OnInit関数内でヘッダー、銘柄、データ、フッターのUI要素を作成し、取引データを表示するための視覚的構造を構築していきます。まずヘッダーについては、「header_y」を「settings.panel_y + 8 + settings.label_y_offset」として計算し、forループで「num_columns」を走査します。各ヘッダーラベルは「createLABEL」を用いて作成し、名前には一意性を持たせるため「PREFIX + HEADER + IntegerToString(i)」を使用します。テキストには「headers[i]」、X座標は「settings.panel_x + settings.header_x_distances[i] + settings.label_x_offset」から計算し、色は「settings.header_color」、フォントサイズは12、フォントは「settings.font」、アンカーは「ANCHOR_LEFT」、さらに後で並べ替え機能を有効にするため「selectable」をtrueに設定します。

銘柄とデータの作成では、「first_row_y」を「header_y + settings.row_height」、「symbol_x」を「settings.panel_x + 10 + settings.label_x_offset」として設定します。次に「num_rows」をループし、「createLABEL」を使って銘柄ラベルを作成します。名前は「PREFIX + SYMB + IntegerToString(i)」、テキストは「symbol_data[i].name」、Y座標は「first_row_y + i * settings.row_height + settings.label_y_offset」、色は「settings.text_color」です。各行について、さらに「num_columns - 1」データ列をループし、「createLABEL」を用いてデータラベルを作成します。名前は「PREFIX + DATA + IntegerToString(i) + '_' + IntegerToString(j)」、初期テキストは列に応じて「0」または「0.00」、X座標は「settings.panel_x + 10 + column_widths[0] + settings.label_x_offset」から始まり、「column_widths[j + 1]」でインクリメントして配置します。色は「data_default_colors[j]」、アンカーは「ANCHOR_RIGHT」とします。

フッターについては、「footer_y」を「settings.panel_y + (num_rows + 3) * settings.row_height - settings.row_height - 5」として計算します。フッターパネルはObjectCreateを用いてOBJ_RECTANGLE_LABELとして作成し、名前を「PREFIX + FOOTER_PANEL」に設定します。プロパティには「OBJPROP_CORNER」を「CORNER_LEFT_UPPER」、OBJPROP_XDISTANCEを「settings.panel_x」、「OBJPROP_YDISTANCE」を「footer_y」、「OBJPROP_XSIZE」を「panel_width」、「OBJPROP_YSIZE」を「settings.row_height + 5」、「OBJPROP_BGCOLOR」を「settings.section_bg_color」、「OBJPROP_BORDER_TYPE」を「BORDER_FLAT」、「OBJPROP_BORDER_COLOR」を「settings.border_color」、「OBJPROP_ZORDER」を「settings.zorder_subpanel」と設定します。

次に「createLABEL」を用いてフッターテキストを作成します。名前は「PREFIX + FOOTER_TEXT」、テキストは「Total:」、X座標「footer_text_x」は「settings.panel_x + 10 + settings.label_x_offset」です。そして「num_columns - 1」をループし、「createLABEL」を使用してフッターデータラベルを作成します。名前は「PREFIX + FOOTER_DATA + IntegerToString(j)」、初期テキストを設定し、X座標を「column_widths[j + 1]」で更新しながら配置し、色は配列「data_default_colors[j]」から取得します。コンパイルすると、次の結果が得られます。

入力されたデータ指標パネル

メインパネルにデータが配置されたので、次は口座データを動的に表示するため、メインパネルの下に配置する口座指標パネルの作成に進みましょう。

// Create account panel below footer
int account_panel_y = footer_y + settings.row_height + 10;                            //--- Calculate account panel y-coordinate
string account_panel_name = PREFIX + ACCOUNT_PANEL;                                   //--- Define account panel name
ObjectCreate(0, account_panel_name, OBJ_RECTANGLE_LABEL, 0, 0, 0);                    //--- Create account panel
ObjectSetInteger(0, account_panel_name, OBJPROP_CORNER, CORNER_LEFT_UPPER);           //--- Set corner
ObjectSetInteger(0, account_panel_name, OBJPROP_XDISTANCE, settings.panel_x);         //--- Set x-coordinate
ObjectSetInteger(0, account_panel_name, OBJPROP_YDISTANCE, account_panel_y);          //--- Set y-coordinate
ObjectSetInteger(0, account_panel_name, OBJPROP_XSIZE, panel_width);                  //--- Set width
ObjectSetInteger(0, account_panel_name, OBJPROP_YSIZE, settings.row_height);          //--- Set height
ObjectSetInteger(0, account_panel_name, OBJPROP_BGCOLOR, settings.section_bg_color);  //--- Set background color
ObjectSetInteger(0, account_panel_name, OBJPROP_BORDER_TYPE, BORDER_FLAT);            //--- Set border type
ObjectSetInteger(0, account_panel_name, OBJPROP_BORDER_COLOR, settings.border_color); //--- Set border color
ObjectSetInteger(0, account_panel_name, OBJPROP_ZORDER, settings.zorder_subpanel);    //--- Set z-order

// Create account text and data labels
int acc_x = settings.panel_x + 10 + settings.label_x_offset;                          //--- Set base x for account labels
int acc_data_offset = 160;                                                            //--- Increased offset for data labels to avoid overlap
int acc_spacing = (panel_width - 45) / ArraySize(account_items);                      //--- Adjusted spacing to fit
for(int k = 0; k < ArraySize(account_items); k++) {                                   //--- Iterate through account items
   string acc_text_name = PREFIX + ACC_TEXT + IntegerToString(k);                     //--- Define text label name
   int text_x = acc_x + k * acc_spacing;                                              //--- Calculate text x
   createLABEL(acc_text_name, account_items[k] + ":", text_x, account_panel_y + 8 + settings.label_y_offset, settings.text_color, settings.font_size, settings.font, ANCHOR_LEFT); //--- Create text label
   string acc_data_name = PREFIX + ACC_DATA + IntegerToString(k);                     //--- Define data label name
   int data_x = text_x + acc_data_offset;                                             //--- Calculate data x
   createLABEL(acc_data_name, "0.00", data_x, account_panel_y + 8 + settings.label_y_offset, settings.text_color, settings.font_size, settings.font, ANCHOR_RIGHT); //--- Create data label
}

ここでは、口座指標を表示するための口座パネルとそのラベルを作成し、OnInit関数内でのUI設定を完了させます。「account_panel_y」は「footer_y + settings.row_height + 10」として計算し、ObjectCreateを使用して OBJ_RECTANGLE_LABEL としてパネルを作成します。名前は「PREFIX + ACCOUNT_PANEL」とし、「OBJPROP_CORNER」を「CORNER_LEFT_UPPER」、「OBJPROP_XDISTANCE」を「settings.panel_x」、「OBJPROP_YDISTANCE」を「account_panel_y」、「OBJPROP_XSIZE」を「panel_width」、「OBJPROP_YSIZE」を「settings.row_height」、OBJPROP_BORDER_COLORを「settings.section_bg_color」、「OBJPROP_BORDER_TYPE」を「BORDER_FLAT」、「OBJPROP_BORDER_COLOR」を「settings.border_color」、「OBJPROP_ZORDER」を「settings.zorder_subpanel」に設定します。

口座ラベルについては、「acc_x」を「settings.panel_x + 10 + settings.label_x_offset」、「acc_data_offset」を160、「acc_spacing」を「(panel_width - 45) / ArraySize(account_items)」として均等な間隔を確保し、メインパネル作成ロジックと同様の形式で配置します。この設定により、フッターの下に整然と整列したパネル内に残高、純資産、余剰証拠金がわかりやすく表示されるようになります。以下をご覧ください。

口座指標パネル

画像からわかるように、口座指標セクションが作成されました。ここからは、パネルの更新処理をおこない、レスポンシブに動作させる必要があります。ダッシュボードを更新するための関数を作成しましょう。

//+------------------------------------------------------------------+
//| Sort dashboard by selected column                                |
//+------------------------------------------------------------------+
void SortDashboard() {
   int n = ArraySize(symbol_data);       //--- Get number of symbols
   for(int i = 0; i < n - 1; i++) {     //--- Iterate through symbols
      for(int j = 0; j < n - i - 1; j++) { //--- Compare adjacent symbols
         bool swap = false;              //--- Initialize swap flag
         switch(sort_column) {           //--- Check sort column
            case 0:                      //--- Sort by symbol name
               swap = sort_ascending ? symbol_data[j].name > symbol_data[j + 1].name : symbol_data[j].name < symbol_data[j + 1].name;
               break;
            case 1:                      //--- Sort by buys
               swap = sort_ascending ? symbol_data[j].buys > symbol_data[j + 1].buys : symbol_data[j].buys < symbol_data[j + 1].buys;
               break;
            case 2:                      //--- Sort by sells
               swap = sort_ascending ? symbol_data[j].sells > symbol_data[j + 1].sells : symbol_data[j].sells < symbol_data[j + 1].sells;
               break;
            case 3:                      //--- Sort by trades
               swap = sort_ascending ? symbol_data[j].trades > symbol_data[j + 1].trades : symbol_data[j].trades < symbol_data[j + 1].trades;
               break;
            case 4:                      //--- Sort by lots
               swap = sort_ascending ? symbol_data[j].lots > symbol_data[j + 1].lots : symbol_data[j].lots < symbol_data[j + 1].lots;
               break;
            case 5:                      //--- Sort by profit
               swap = sort_ascending ? symbol_data[j].profit > symbol_data[j + 1].profit : symbol_data[j].profit < symbol_data[j + 1].profit;
               break;
            case 6:                      //--- Sort by pending
               swap = sort_ascending ? symbol_data[j].pending > symbol_data[j + 1].pending : symbol_data[j].pending < symbol_data[j + 1].pending;
               break;
            case 7:                      //--- Sort by swaps
               swap = sort_ascending ? symbol_data[j].swaps > symbol_data[j + 1].swaps : symbol_data[j].swaps < symbol_data[j + 1].swaps;
               break;
            case 8:                      //--- Sort by comm
               swap = sort_ascending ? symbol_data[j].comm > symbol_data[j + 1].comm : symbol_data[j].comm < symbol_data[j + 1].comm;
               break;
         }
         if(swap) {                      //--- Check if swap needed
            SymbolData temp = symbol_data[j]; //--- Store temporary data
            symbol_data[j] = symbol_data[j + 1]; //--- Swap data
            symbol_data[j + 1] = temp;   //--- Complete swap
         }
      }
   }
}

「SortDashboard」関数を実装し、動的なソート機能を有効にすることで、選択した列ごとに銘柄データを整理できるようにします。まず、「symbol_data」に対してArraySizeを使用して銘柄数を取得し、「n」に格納します。次に、入れ子になったforループを使って「n-1」個の銘柄を走査し、隣り合うペアを「n-i-1」まで比較します。「swap」フラグをfalseで初期化し、「sort_column」に対するswitch文を使用してソート基準を判定します。0は「name」、1は「buys」、2は「sells」、3は「trades」、4は「lots」、5は「profit」、6は「pending」、7は「swaps」、8は「comm」を表し、「sort_ascending」に基づいて並び替えが必要であれば「swap」をtrueに設定します。

「swap」がtrueの場合、「symbol_data[j]」を一時的な「SymbolData」変数に保存し、「symbol_data[j]」と「symbol_data[j+1]」を入れ替えてスワップを完了します。このバブルソートの実装により、ダッシュボードは任意の列で昇順または降順にソートできるようになり、データの可視性が向上します。これで、この関数をメインの更新処理関数内に実装し、ダッシュボード全体の更新を管理できるようになります。

//+------------------------------------------------------------------+
//| Update dashboard function                                        |
//+------------------------------------------------------------------+
void UpdateDashboard() {
   bool needs_redraw = false;             //--- Initialize redraw flag
   CollectActiveSymbols();
   int current_num = ArraySize(symbol_data);
   if(current_num != prev_num_symbols) {
      // Delete old symbol and data labels
      for(int del_i = 0; del_i < prev_num_symbols; del_i++) {
         ObjectDelete(0, PREFIX + SYMB + IntegerToString(del_i));
         for(int del_j = 0; del_j < 8; del_j++) {
            ObjectDelete(0, PREFIX + DATA + IntegerToString(del_i) + "_" + IntegerToString(del_j));
         }
      }
      // Adjust panel sizes and positions
      int panel_height = (current_num + 3) * settings.row_height;
      ObjectSetInteger(0, PREFIX + PANEL, OBJPROP_YSIZE, panel_height);
      int footer_y = settings.panel_y + panel_height - settings.row_height - 5;
      ObjectSetInteger(0, PREFIX + FOOTER_PANEL, OBJPROP_YDISTANCE, footer_y);
      int account_panel_y = footer_y + settings.row_height + 10;
      ObjectSetInteger(0, PREFIX + ACCOUNT_PANEL, OBJPROP_YDISTANCE, account_panel_y);
      // Create new symbol and data labels
      int header_y = settings.panel_y + 8 + settings.label_y_offset;
      int first_row_y = header_y + settings.row_height;
      int symbol_x = settings.panel_x + 10 + settings.label_x_offset;
      for(int cr_i = 0; cr_i < current_num; cr_i++) {
         string symb_name = PREFIX + SYMB + IntegerToString(cr_i);
         createLABEL(symb_name, symbol_data[cr_i].name, symbol_x, first_row_y + cr_i * settings.row_height + settings.label_y_offset, settings.text_color, settings.font_size, settings.font, ANCHOR_LEFT);
         int x_offset = settings.panel_x + 10 + column_widths[0] + settings.label_x_offset;
         for(int cr_j = 0; cr_j < 8; cr_j++) {
            string data_name = PREFIX + DATA + IntegerToString(cr_i) + "_" + IntegerToString(cr_j);
            color init_color = data_default_colors[cr_j];
            string init_txt = (cr_j <= 2 || cr_j == 5) ? "0" : "0.00";
            createLABEL(data_name, init_txt, x_offset, first_row_y + cr_i * settings.row_height + settings.label_y_offset, init_color, settings.font_size, settings.font, ANCHOR_RIGHT);
            x_offset += column_widths[cr_j + 1];
         }
      }
      prev_num_symbols = current_num;
      needs_redraw = true;
   }
   // Reset totals
   totalBuys = 0;
   totalSells = 0;
   totalTrades = 0;
   totalLots = 0.0;
   totalProfit = 0.0;
   totalPending = 0;
   totalSwap = 0.0;
   totalComm = 0.0;
   // Calculate symbol data and totals (without updating labels yet)
   for(int i = 0; i < current_num; i++) {
      string symbol = symbol_data[i].name;
      for(int j = 0; j < 8; j++) {
         string value = "";
         color data_color = data_default_colors[j];
         double dval = 0.0;
         int ival = 0;
         switch(j) {
            case 0: // Buy positions
               value = countPositions(symbol, POSITION_TYPE_BUY);
               ival = (int)StringToInteger(value);
               if(value != symbol_data[i].buys_str) {
                  symbol_data[i].buys_str = value;
                  symbol_data[i].buys = ival;
               }
               totalBuys += ival;
               break;
            case 1: // Sell positions
               value = countPositions(symbol, POSITION_TYPE_SELL);
               ival = (int)StringToInteger(value);
               if(value != symbol_data[i].sells_str) {
                  symbol_data[i].sells_str = value;
                  symbol_data[i].sells = ival;
               }
               totalSells += ival;
               break;
            case 2: // Total trades
               value = countPositionsTotal(symbol);
               ival = (int)StringToInteger(value);
               if(value != symbol_data[i].trades_str) {
                  symbol_data[i].trades_str = value;
                  symbol_data[i].trades = ival;
               }
               totalTrades += ival;
               break;
            case 3: // Lots
               value = sumPositionDouble(symbol, POSITION_VOLUME);
               dval = StringToDouble(value);
               if(value != symbol_data[i].lots_str) {
                  symbol_data[i].lots_str = value;
                  symbol_data[i].lots = dval;
               }
               totalLots += dval;
               break;
            case 4: // Profit
               value = sumPositionDouble(symbol, POSITION_PROFIT);
               dval = StringToDouble(value);
               data_color = (dval > 0) ? clrGreen : (dval < 0) ? clrRed : clrGray;
               if(value != symbol_data[i].profit_str) {
                  symbol_data[i].profit_str = value;
                  symbol_data[i].profit = dval;
               }
               totalProfit += dval;
               break;
            case 5: // Pending
               value = countOrders(symbol);
               ival = (int)StringToInteger(value);
               if(value != symbol_data[i].pending_str) {
                  symbol_data[i].pending_str = value;
                  symbol_data[i].pending = ival;
               }
               totalPending += ival;
               break;
            case 6: // Swap
               value = sumPositionDouble(symbol, POSITION_SWAP);
               dval = StringToDouble(value);
               data_color = (dval > 0) ? clrGreen : (dval < 0) ? clrRed : data_color;
               if(value != symbol_data[i].swaps_str) {
                  symbol_data[i].swaps_str = value;
                  symbol_data[i].swaps = dval;
               }
               totalSwap += dval;
               break;
            case 7: // Comm
               dval = sumPositionCommission(symbol);
               value = DoubleToString(dval, 2);
               data_color = (dval > 0) ? clrGreen : (dval < 0) ? clrRed : data_color;
               if(value != symbol_data[i].comm_str) {
                  symbol_data[i].comm_str = value;
                  symbol_data[i].comm = dval;
               }
               totalComm += dval;
               break;
         }
      }
   }
   // Sort after calculating values
   SortDashboard();
}

ここでは、「UpdateDashboard」関数を実装し、ダッシュボードを更新してリアルタイムのポジションおよび口座データを反映させるとともに、銘柄の変化に動的に対応できるようにします。まず、「needs_redraw」をfalseで初期化し、「CollectActiveSymbols」関数を呼び出して「symbol_data」を更新します。ArraySize(symbol_data)で取得した「current_num」が「prev_num_symbols」と異なる場合、古いラベルをObjectDeleteで削除します。削除対象は「PREFIX+SYMB」および「PREFIX+DATA」ラベルです。次に、「PREFIX+PANEL」の「OBJPROP_YSIZE」を「(current_num+3)settings.row_height」に設定してパネルサイズを調整し、「PREFIX+FOOTER_PANEL」と「PREFIX+ACCOUNT_PANEL」の「OBJPROP_YDISTANCE」を新しい「footer_y」と「account_panel_y」に基づいて更新します。その後、「symbol_data[cr_i].name」や初期値を用いて「createLABEL」で銘柄ラベルとデータラベルを再作成します。座標には「symbol_x」と「first_row_y+cr_isettings.row_height+settings.label_y_offset」を使用し、「x_offset」は「column_widths」でインクリメントします。

「prev_num_symbols」に「current_num」を代入し、「needs_redraw」をtrueに設定します。さらに、「totalBuys」「totalSells」「totalTrades」「totalLots」「totalProfit」「totalPending」「totalSwap」「totalComm」をすべて0にリセットします。各銘柄について8つのデータ列をループし、「countPositions」「countPositionsTotal」「sumPositionDouble」「sumPositionCommission」などを使用して値を算出し、「symbol_data[i]」内の「buys_str」「sells」「profit_str」などのフィールドを更新します。また、「profit」「swaps」「comm」の値が正の場合は緑、負の場合は赤、それ以外は中立色に設定します。算出結果は合計値に加算されます。最後に、「SortDashboard」を呼び出して、現在の「sort_column」と「sort_ascending」に基づいて表示を並び替えます。この関数を有効にするためには、OnInitイベントハンドラの末尾にこの関数呼び出しを追加する必要があります。

// Set millisecond timer for updates
EventSetMillisecondTimer(MathMax(UpdateIntervalMs, 10)); //--- Set timer with minimum 10ms

// Initial update
prev_num_symbols = num_rows;
UpdateDashboard();                                       //--- Update dashboard

まず最初に、初期化のために前回の行数を計算済みの行数に設定し、「UpdateDashboard」関数を呼び出してダッシュボードを更新します。同じ関数をOnTimer関数内でも呼び出す必要があるため、EventSetMillisecondTimer関数を使用して更新間隔に基づいたタイマー時間を設定します。このとき、最小値を10ミリ秒に制限することで、システムリソースへの過負荷を防ぎます。また、タイマーを作成した場合は、不要になったときに必ず破棄してリソースを解放することを忘れないでください。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   ObjectsDeleteAll(0, PREFIX, -1, -1);  //--- Delete all objects with PREFIX
   EventKillTimer();                     //--- Stop timer
}

OnDeinitイベントハンドラでは、ObjectsDeleteAll関数を使用して「PREFIX」を持つすべてのオブジェクトを削除し、EventKillTimer関数でタイマーを停止します。これで、次に示すように「OnTimer」イベントハンドラ内で更新関数を呼び出し、定期的な更新を実行できるようになります。

//+------------------------------------------------------------------+
//| Timer function for millisecond-based updates                     |
//+------------------------------------------------------------------+
void OnTimer() {
   UpdateDashboard();                    //--- Update dashboard on timer event
}

バブルソート効果を有効にするために、OnChartEventイベントハンドラを実装します。以下に、そのために使用したロジックを示します。

//+------------------------------------------------------------------+
//| Chart event handler for sorting and export                       |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) {
   if(id == CHARTEVENT_OBJECT_CLICK) {              //--- Handle object click event
      for(int i = 0; i < ArraySize(headers); i++) { //--- Iterate through headers
         if(sparam == PREFIX + HEADER + IntegerToString(i)) { //--- Check if header clicked
            if(sort_column == i)                    //--- Check if same column clicked
               sort_ascending = !sort_ascending;    //--- Toggle sort direction
            else {
               sort_column = i;                     //--- Set new sort column
               sort_ascending = true;               //--- Set to ascending
            }
            UpdateDashboard();                      //--- Update dashboard display
            break;                                  //--- Exit loop
         }
      }
   }
}

OnChartEventイベントハンドラを実装し、ダッシュボードのソートに対するユーザー操作を処理することで、インタラクティブ性を向上させます。CHARTEVENT_OBJECT_CLICKが発生した場合、ArraySizeでヘッダーをループし、「sparam」が「PREFIX + HEADER + IntegerToString(i)」と一致するかどうかをチェックします。もしクリックされたヘッダーのインデックスが「sort_column」と同じであれば、「sort_ascending」を反転します。異なる場合は、「sort_column」をクリックされたインデックスに設定し、「sort_ascending」をtrueに初期化します。その後、「UpdateDashboard」を呼び出して新しいソート順に基づき表示を更新し、ループを抜けます。これにより、列ヘッダーをクリックするたびに動的にソートがおこなわれ、データ分析がより柔軟におこなえるようになります。コンパイルすると、次の結果が得られます。

静的ダッシュボード

可視化からわかるように、「Click to sort」のような操作方法を示すホバーツールチップは表示されますが、クリックしても何も起こりません。情報自体も表示されません。その理由は、情報は内部的には収集されているものの、ダッシュボード上には視覚的に更新されていないためです。そこで、「UpdateDashboard」関数をアップグレードし、収集された情報が画面に反映されるようにします。まず最初に、ヘッダーに呼吸するような効果(breathing header)を追加するロジックを定義します。これが最も簡単で、パネルに生命を吹き込むことができ、正しい方向に進んでいることを確認できます。

// Update header breathing effect every 500ms
glow_counter += MathMax(UpdateIntervalMs, 10); //--- Increment glow counter
if(glow_counter >= GLOW_INTERVAL_MS) { //--- Check if glow interval reached
   if(glow_direction) {                //--- Check if glowing forward
      glow_index++;                    //--- Increment glow index
      if(glow_index >= ArraySize(settings.header_shades) - 1) //--- Check if at end
         glow_direction = false;       //--- Reverse glow direction
   } else {                            //--- Glow backward
      glow_index--;                    //--- Decrement glow index
      if(glow_index <= 0)              //--- Check if at start
         glow_direction = true;        //--- Reverse glow direction
   }
   glow_counter = 0;                   //--- Reset glow counter
}
color header_shade = settings.header_shades[glow_index];          //--- Get current header shade
for(int i = 0; i < ArraySize(headers); i++) {                     //--- Iterate through headers
   string header_name = PREFIX + HEADER + IntegerToString(i);     //--- Define header name
   ObjectSetInteger(0, header_name, OBJPROP_COLOR, header_shade); //--- Update header color
   needs_redraw = true;                //--- Set redraw flag
}


// Batch redraw if needed
if(needs_redraw) {                     //--- Check if redraw needed
   ChartRedraw(0);                     //--- Redraw chart
}

ここでは、「UpdateDashboard」関数内でヘッダーのグロー効果と最終的な再描画を実装し、視覚的なフィードバックを強化します。「glow_counter」は「UpdateIntervalMs」と10の最大値分だけ加算され、「GLOW_INTERVAL_MS」(500ミリ秒)に達したかどうかをチェックします。条件を満たした場合、「glow_index」を調整します。「glow_direction」がtrueの場合はインクリメントし、「settings.header_shades」の末尾に達するとfalseに反転します。falseの場合はデクリメントし、0に達するとtrueに反転します。その後「glow_counter」を0にリセットします。「header_shade」は「settings.header_shades[glow_index]」から取得し、ArraySizeでヘッダーをループしてObjectSetIntegerによって各ラベル「PREFIX + HEADER + IntegerToString(i)」の「OBJPROP_COLOR」を「header_shade」に設定します。これにより「needs_redraw」をtrueにします。

「needs_redraw」がtrueの場合、ChartRedrawを呼び出してチャートを更新します。これにより、ヘッダーに循環するグロー効果が生まれ、UIの更新が効率的におこなわれます。色やサイクルの頻度、透明度は自由に変更可能です。以下の結果が得られます。

呼吸するようなヘッダー

呼吸するようなヘッダーが実装できたので、より複雑なロジック、つまりクリック操作にも対応できるダッシュボードの更新処理に進みましょう。

// Update symbol and data labels after sorting
bool labels_updated = false;
for(int i = 0; i < current_num; i++) {
   string symbol = symbol_data[i].name;
   string symb_name = PREFIX + SYMB + IntegerToString(i);
   string current_symb_txt = ObjectGetString(0, symb_name, OBJPROP_TEXT);
   if(current_symb_txt != symbol) {
      ObjectSetString(0, symb_name, OBJPROP_TEXT, symbol);
      labels_updated = true;
   }
   for(int j = 0; j < 8; j++) {
      string data_name = PREFIX + DATA + IntegerToString(i) + "_" + IntegerToString(j);
      string value;
      color data_color = data_default_colors[j];
      switch(j) {
         case 0:
            value = symbol_data[i].buys_str;
            data_color = clrRed;
            break;
         case 1:
            value = symbol_data[i].sells_str;
            data_color = clrGreen;
            break;
         case 2:
            value = symbol_data[i].trades_str;
            data_color = clrDarkGray;
            break;
         case 3:
            value = symbol_data[i].lots_str;
            data_color = clrOrange;
            break;
         case 4:
            value = symbol_data[i].profit_str;
            data_color = (symbol_data[i].profit > 0) ? clrGreen : (symbol_data[i].profit < 0) ? clrRed : clrGray;
            break;
         case 5:
            value = symbol_data[i].pending_str;
            data_color = clrBlue;
            break;
         case 6:
            value = symbol_data[i].swaps_str;
            data_color = (symbol_data[i].swaps > 0) ? clrGreen : (symbol_data[i].swaps < 0) ? clrRed : clrPurple;
            break;
         case 7:
            value = symbol_data[i].comm_str;
            data_color = (symbol_data[i].comm > 0) ? clrGreen : (symbol_data[i].comm < 0) ? clrRed : clrBrown;
            break;
      }
      if(updateLABEL(data_name, value, data_color)) labels_updated = true;
   }
}
if(labels_updated) needs_redraw = true;
// Update totals
string new_total_buys = IntegerToString(totalBuys); //--- Format total buys
if(new_total_buys != total_buys_str) { //--- Check if changed
   total_buys_str = new_total_buys;    //--- Update string
   if(updateLABEL(PREFIX + FOOTER_DATA + "0", new_total_buys, clrRed)) needs_redraw = true; //--- Update label
}
string new_total_sells = IntegerToString(totalSells); //--- Format total sells
if(new_total_sells != total_sells_str) { //--- Check if changed
   total_sells_str = new_total_sells;  //--- Update string
   if(updateLABEL(PREFIX + FOOTER_DATA + "1", new_total_sells, clrGreen)) needs_redraw = true; //--- Update label
}
string new_total_trades = IntegerToString(totalTrades); //--- Format total trades
if(new_total_trades != total_trades_str) { //--- Check if changed
   total_trades_str = new_total_trades; //--- Update string
   if(updateLABEL(PREFIX + FOOTER_DATA + "2", new_total_trades, clrDarkGray)) needs_redraw = true; //--- Update label
}
string new_total_lots = DoubleToString(totalLots, 2); //--- Format total lots
if(new_total_lots != total_lots_str) { //--- Check if changed
   total_lots_str = new_total_lots;    //--- Update string
   if(updateLABEL(PREFIX + FOOTER_DATA + "3", new_total_lots, clrOrange)) needs_redraw = true; //--- Update label
}
string new_total_profit = DoubleToString(totalProfit, 2); //--- Format total profit
color total_profit_color = (totalProfit > 0) ? clrGreen : (totalProfit < 0) ? clrRed : clrGray; //--- Set color
if(new_total_profit != total_profit_str) { //--- Check if changed
   total_profit_str = new_total_profit; //--- Update string
   if(updateLABEL(PREFIX + FOOTER_DATA + "4", new_total_profit, total_profit_color)) needs_redraw = true; //--- Update label
}
string new_total_pending = IntegerToString(totalPending); //--- Format total pending
if(new_total_pending != total_pending_str) { //--- Check if changed
   total_pending_str = new_total_pending; //--- Update string
   if(updateLABEL(PREFIX + FOOTER_DATA + "5", new_total_pending, clrBlue)) needs_redraw = true; //--- Update label
}
string new_total_swap = DoubleToString(totalSwap, 2); //--- Format total swap
color total_swap_color = (totalSwap > 0) ? clrGreen : (totalSwap < 0) ? clrRed : clrPurple; //--- Set color
if(new_total_swap != total_swap_str) { //--- Check if changed
   total_swap_str = new_total_swap;    //--- Update string
   if(updateLABEL(PREFIX + FOOTER_DATA + "6", new_total_swap, total_swap_color)) needs_redraw = true; //--- Update label
}
string new_total_comm = DoubleToString(totalComm, 2); //--- Format total comm
color total_comm_color = (totalComm > 0) ? clrGreen : (totalComm < 0) ? clrRed : clrBrown; //--- Set color
if(new_total_comm != total_comm_str) { //--- Check if changed
   total_comm_str = new_total_comm;    //--- Update string
   if(updateLABEL(PREFIX + FOOTER_DATA + "7", new_total_comm, total_comm_color)) needs_redraw = true; //--- Update label
}

// Update account info
double balance = AccountInfoDouble(ACCOUNT_BALANCE); //--- Get balance
double equity = AccountInfoDouble(ACCOUNT_EQUITY); //--- Get equity
double free_margin = AccountInfoDouble(ACCOUNT_MARGIN_FREE); //--- Get free margin
string new_bal = DoubleToString(balance, 2); //--- Format balance
if(new_bal != acc_bal_str) {          //--- Check if changed
   acc_bal_str = new_bal;             //--- Update string
   if(updateLABEL(PREFIX + ACC_DATA + "0", new_bal, clrBlack)) needs_redraw = true; //--- Update label
}
string new_eq = DoubleToString(equity, 2); //--- Format equity
color eq_color = (equity > balance) ? clrGreen : (equity < balance) ? clrRed : clrBlack; //--- Set color
if(new_eq != acc_eq_str) {            //--- Check if changed
   acc_eq_str = new_eq;               //--- Update string
   if(updateLABEL(PREFIX + ACC_DATA + "1", new_eq, eq_color)) needs_redraw = true; //--- Update label
}
string new_free = DoubleToString(free_margin, 2); //--- Format free margin
if(new_free != acc_free_str) {        //--- Check if changed
   acc_free_str = new_free;           //--- Update string
   if(updateLABEL(PREFIX + ACC_DATA + "2", new_free, clrBlack)) needs_redraw = true; //--- Update label
}

「UpdateDashboard」関数内で、銘柄ラベル、データラベル、合計ラベル、および口座ラベルを更新し、ソート後および最新の取引データを反映してレスポンシブな表示を実現します。まず、「labels_updated」をfalseに設定し、「current_num」銘柄をループして「PREFIX + SYMB +IntegerToString(i)」のラベルを「symbol_data[i].name」で更新します。ObjectGetStringの値が異なる場合にのみ更新し、「labels_updated」をtrueに設定します。各銘柄について8列をループし、switch文で「value」と「data_color」を選択します。「buys_str」はclrRed、「sells_str」はclrGreen、「trades_str」はclrDarkGray、「lots_str」はclrOrange、「profit_str」は「symbol_data[i].profit」に応じて色を設定、「pending_str」はclrBlue、「swaps_str」は「symbol_data[i].swaps」に応じて色を設定、「comm_str」は「symbol_data[i].comm」に応じて色を設定します。更新には「updateLABEL」を使用し、変更があれば「labels_updated」をtrueにします。

合計ラベルについては、「total_buys_str」をIntegerToString(totalBuys)で、「total_sells_str」「total_trades_str」「total_lots_str」をDoubleToString(totalLots, 2)で、「total_profit_str」は条件に応じたtotal_profit_color、「total_pending_str」「total_swap_str」はtotal_swap_color、「total_comm_str」はtotal_comm_colorで更新します。「PREFIX + FOOTER_DATA」ラベルに対して「updateLABEL」を使用し、更新された場合は「needs_redraw」をtrueにします。口座情報については、AccountInfoDoubleで「balance」「equity」「free_margin」を取得し、DoubleToStringでフォーマットして「acc_bal_str」「acc_eq_str」(条件付きでeq_color)、「acc_free_str」を更新します。「PREFIX + ACC_DATA」ラベルに対して「updateLABEL」を使用します。これにより、ダッシュボードは常に最新のデータを表示し、重要な値は動的な色で視覚的に分かりやすくなります。コンパイルすると、次の結果が得られます。

IP動的ソート

可視化からわかるように、ヘッダー列内のすべての情報が昇順・降順の両方で反映される、動的なバブルソート効果を実現できました。残っているのは、データをExcelにエクスポートしてさらに分析できるようにする関数です。そのために実装するロジックは次のとおりです。

//+------------------------------------------------------------------+
//| Export dashboard data to CSV                                     |
//+------------------------------------------------------------------+
void ExportToCSV() {
   string time_str = TimeToString(TimeCurrent(), TIME_DATE|TIME_MINUTES); //--- Get current time string
   StringReplace(time_str, " ", "_"); //--- Replace spaces
   StringReplace(time_str, ":", "-"); //--- Replace colons
   string filename = "Dashboard_" + time_str + ".csv"; //--- Define filename
   int handle = FileOpen(filename, FILE_WRITE|FILE_CSV); //--- Open CSV file in terminal's Files folder
   if(handle == INVALID_HANDLE) {        //--- Check for invalid handle
      Print("Failed to open CSV file '", filename, "'. Error code = ", GetLastError()); //--- Log error
      return;                            //--- Exit function
   }
   FileWrite(handle, "Symbol,Buy Positions,Sell Positions,Total Trades,Lots,Profit,Pending Orders,Swap,Comm"); //--- Write header
   for(int i = 0; i < ArraySize(symbol_data); i++) { //--- Iterate through symbols
      FileWrite(handle, symbol_data[i].name, symbol_data[i].buys, symbol_data[i].sells, symbol_data[i].trades, symbol_data[i].lots, symbol_data[i].profit, symbol_data[i].pending, symbol_data[i].swaps, symbol_data[i].comm); //--- Write symbol data
   }
   FileWrite(handle, "Total", totalBuys, totalSells, totalTrades, totalLots, totalProfit, totalPending, totalSwap, totalComm); //--- Write totals
   FileClose(handle);                    //--- Close file
   Print("Dashboard data exported to CSV: ", filename); //--- Log export success
}

「ExportToCSV」関数を実装し、取引データをエクスポートしてオフラインで分析できるようにします。まず、TimeToStringを使用してTimeCurrentから「time_str」を作成し、「TIME_DATE|TIME_MINUTES」を指定します。さらにStringReplace fでスペースをアンダースコアに、コロンをハイフンに置換して、ファイル名として適切な形式に整えます。その後、「filename」を「Dashboard_」+「time_str」+「.csv」と定義します。他の許可された拡張子を使うことも可能ですが、今回は最も一般的なCSVを選択しています。次に、FileOpenを使用してFILE_WRITE|FILE_CSVでファイルを開きます。ハンドルが「INVALID_HANDLE」の場合はPrintでエラーを出力し、処理を終了します。

ヘッダー行はFileWriteで列名を出力します。次にArraySize(symbol_data)で銘柄ごとにループし、各銘柄の「name」「buys」「sells」「trades」「lots」「profit」「pending」「swaps」「comm」を書き込みます。さらに、合計行を「Total」として「totalBuys」「totalSells」「totalTrades」「totalLots」「totalProfit」「totalPending」「totalSwap」「totalComm」を書き込みます。最後にFileCloseでファイルを閉じ、Printで成功をログに記録します。これにより、記録保持のための便利なCSVエクスポート機能が提供されます。この関数は、チャートイベントハンドラでキー「E」が押されたときに呼び出すことができます。「E」は「Export」を思い出しやすいように選びましたが、任意のキーを使用可能です。ボタンでエクスポート機能を実装することもできますが、今回は初期設計時には考慮していません。以下に、そのために使用したロジックを示します。

//+------------------------------------------------------------------+
//| Chart event handler for sorting and export                       |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) {
   if(id == CHARTEVENT_OBJECT_CLICK) {   //--- Handle object click event
      for(int i = 0; i < ArraySize(headers); i++) { //--- Iterate through headers
         if(sparam == PREFIX + HEADER + IntegerToString(i)) { //--- Check if header clicked
            if(sort_column == i)         //--- Check if same column clicked
               sort_ascending = !sort_ascending; //--- Toggle sort direction
            else {
               sort_column = i;          //--- Set new sort column
               sort_ascending = true;    //--- Set to ascending
            }
            UpdateDashboard();           //--- Update dashboard display
            break;                       //--- Exit loop
         }
      }
   }
   else if(id == CHARTEVENT_KEYDOWN && lparam == 'E') { //--- Handle 'E' key press
      ExportToCSV();                    //--- Export data to CSV
   }
}

ここでは、イベントIDがCHARTEVENT_KEYDOWNで、押されたキーが「E」であるかどうかをチェックし、ファイルを即座にエクスポートします。シンプルなロジックなので、明確にするため黄色でハイライトしています。得られた結果は次のとおりです。

IPエクスポートCSV

可視化からわかるように、現在の時刻に基づいて異なるファイルにデータをエクスポートしており、分単位で時刻が一致した場合は上書きされます。別のファイルとして保存するために1分待ちたくない場合は、時刻のフォーマットを分単位から秒単位に変更することができます。全体として、概ね目的を達成できたことが確認できます。残されているのはプロジェクトの実用性をテストすることであり、それは次のセクションで扱います。


バックテスト

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

IPバックテスト



結論

結論として、MetaQuotes Language 5で複数銘柄のポジションや「残高」「有効証拠金」「余剰証拠金」といった口座指標を監視する情報ダッシュボードを作成しました。列のソート機能やExcel CSVエクスポート機能を備え、取引状況を効率的に把握できるようになっています。構造体「SymbolData」や関数「SortDashboard」などを活用して、リアルタイムで整理された情報を提供するアーキテクチャと実装を詳しく解説しました。このダッシュボードは、取引ニーズに応じてカスタマイズ可能で、パフォーマンスを効率的に追跡する能力を高めます。 

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

添付されたファイル |
プライスアクション分析ツールキットの開発(第35回):予測モデルの学習とデプロイ プライスアクション分析ツールキットの開発(第35回):予測モデルの学習とデプロイ
履歴データは決して「ゴミ」ではありません。それは、堅牢な市場分析の基盤です。本記事では、履歴データの収集から、それを用いた予測モデルの学習、そして学習済みモデルを用いたリアルタイムの価格予測のデプロイまでを、ステップごとに解説します。ぜひ最後までお読みください。
プライスアクション分析ツールキットの開発(第34回):高度なデータ取得パイプラインを用いた生の市場データからの予測モデル構築 プライスアクション分析ツールキットの開発(第34回):高度なデータ取得パイプラインを用いた生の市場データからの予測モデル構築
突然のマーケットスパイクを見逃したり、それが発生したときに対応が間に合わなかったことはありませんか。ライブイベントを予測する最良の方法は、過去のパターンから学ぶことです。本記事では、MetaTrader 5で履歴データを取得し、それをPythonに送信して保存するスクリプトの作成方法を紹介します。これにより、スパイク検知システムの基礎を構築できます。以下で各ステップを詳しく見ていきましょう。
知っておくべきMQL5ウィザードのテクニック(第78回):ゲーター&A/Dオシレーター戦略による市場耐性の強化 知っておくべきMQL5ウィザードのテクニック(第78回):ゲーター&A/Dオシレーター戦略による市場耐性の強化
本記事では、ゲーターオシレーターとA/Dオシレーターを用いた取引の体系的アプローチの後半部分を紹介します。新たに5つのパターンを導入することで、偽の動きをフィルタリングし、早期の反転を検出し、時間軸をまたいでシグナルを整合させる方法を示します。明確なコーディング例とパフォーマンステストを通じて、この資料は理論と実践をMQL5開発者向けに橋渡ししています。
MQL5で自己最適化エキスパートアドバイザーを構築する(第10回):行列分解 MQL5で自己最適化エキスパートアドバイザーを構築する(第10回):行列分解
行列分解は、データの特性を理解するために用いられる数学的手法です。行と列で整理された大規模な市場データに行列分解を適用することで、市場のパターンや特性を明らかにすることができます。行列分解は非常に強力なツールであり、本記事ではMetaTrader 5のターミナル内でMQL5 APIを活用し、市場データをより深く分析する方法を紹介します。