MQL5取引ツール(第7回):複数銘柄ポジションと口座監視のための情報ダッシュボード
はじめに
前回の記事(第6回)では、MetaQuotes Language 5 (MQL5)でダイナミックホログラフィックダッシュボードを開発しました。このダッシュボードは、銘柄と時間足を監視でき、RSI、ボラティリティアラート、パルスアニメーション付きのインタラクティブコントロールを備えていました。今回の第7回では、複数銘柄のポジション、総取引数、ロット数、利益、未約定注文、スワップ、手数料、残高や証拠金といった口座指標を追跡する情報ダッシュボードを作成します。ソート可能な列やComma Separated Values (CSV)エクスポートも実装し、包括的な管理を可能にします。本記事では以下のトピックを扱います。
この記事を読み終える頃には、ポジションや口座管理用の強力な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」をPositionGetTicketとPositionSelectByTicketで選択します。銘柄が一致し、かつ「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」を加えて決定します。
次に、メインパネルをObjectCreateでOBJ_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」を使用します。これにより、ダッシュボードは常に最新のデータを表示し、重要な値は動的な色で視覚的に分かりやすくなります。コンパイルすると、次の結果が得られます。

可視化からわかるように、ヘッダー列内のすべての情報が昇順・降順の両方で反映される、動的なバブルソート効果を実現できました。残っているのは、データを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」であるかどうかをチェックし、ファイルを即座にエクスポートします。シンプルなロジックなので、明確にするため黄色でハイライトしています。得られた結果は次のとおりです。
可視化からわかるように、現在の時刻に基づいて異なるファイルにデータをエクスポートしており、分単位で時刻が一致した場合は上書きされます。別のファイルとして保存するために1分待ちたくない場合は、時刻のフォーマットを分単位から秒単位に変更することができます。全体として、概ね目的を達成できたことが確認できます。残されているのはプロジェクトの実用性をテストすることであり、それは次のセクションで扱います。
バックテスト
テストを実施しました。以下はコンパイル後の可視化を単一のGraphics Interchange Format (GIF)ビットマップ画像形式で示したものです。

結論
結論として、MetaQuotes Language 5で複数銘柄のポジションや「残高」「有効証拠金」「余剰証拠金」といった口座指標を監視する情報ダッシュボードを作成しました。列のソート機能やExcel CSVエクスポート機能を備え、取引状況を効率的に把握できるようになっています。構造体「SymbolData」や関数「SortDashboard」などを活用して、リアルタイムで整理された情報を提供するアーキテクチャと実装を詳しく解説しました。このダッシュボードは、取引ニーズに応じてカスタマイズ可能で、パフォーマンスを効率的に追跡する能力を高めます。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/18986
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
プライスアクション分析ツールキットの開発(第35回):予測モデルの学習とデプロイ
プライスアクション分析ツールキットの開発(第34回):高度なデータ取得パイプラインを用いた生の市場データからの予測モデル構築
知っておくべきMQL5ウィザードのテクニック(第78回):ゲーター&A/Dオシレーター戦略による市場耐性の強化
MQL5で自己最適化エキスパートアドバイザーを構築する(第10回):行列分解
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索