English
preview
MQL5で取引管理者パネルを作成する(第12回):FX取引計算ツールの統合

MQL5で取引管理者パネルを作成する(第12回):FX取引計算ツールの統合

MetaTrader 5 |
18 10
Clemence Benjamin
Clemence Benjamin

内容


はじめに

本日のディスカッションでは、New Admin Panel EAのサブパネルである取引管理パネル(Trade Management Panel)にFX取引計算ツールを直接統合することで、従来、取引値を手動や外部ツールで計算しなければならないという不便さの解消に重点を置きます。

これまで多くのトレーダーが外部ウェブサイトを利用して計算をおこなってきました。これらのツールは非常に有用であり、価値あるサービスを提供してくれた開発者には感謝すべきです。現在でも外部計算ツールを使用するトレーダーはいますが、最終的には個人の好みによる部分が大きいと言えます。

しかし、MQL5のGUI機能を活用することで、取引ターミナル内に効率的かつ統合された計算環境を構築できるようになりました。この方法により、アプリケーション間を行き来する手間が省け、すべての必須ツールを一か所で管理できるため、ワークフローの効率化が期待できます。

また、MetaTrader 5が提供する強力なAPIにより、市場ニュースやデータフィードへシームレスにアクセスすることも可能です。もちろん、サードパーティのAPIを利用する方法もありますが、本プロジェクトではパネル専用に最適化されたネイティブ計算アルゴリズムを開発し、より直感的かつ統合された環境を目指します。

本プロジェクトは既存のソリューションを否定するものではなく、むしろトレーダーの選択肢を広げることを目的としています。また、MetaTrader 5の機能をより深く理解し、プラットフォームを効果的に活用するきっかけとなることを目指しています。ターミナル内で統合されたツールを提供することで、スムーズかつ生産性の高い取引体験を支援し、取引技術の進化が業界にもたらす可能性を示すことができます。

計算される値の一部は次のとおりです。

  • ポジションサイズ
  • リスク金額
  • pip価値
  • 証拠金の必要額
  • 損益見積もり
  • スワップ/オーバーナイト手数料
  • リスクリワード比
  • 証拠金維持率
  • スプレッドコスト
  • ブレークイーブン価格
  • 期待収益
  • レバレッジの影響、など

これらの計算は、FXトレーダーにとって非常に重要です。なぜなら、リスク管理をおこない、取引のセットアップを最適化し、口座を健全に維持するための体系的な枠組みを提供するからです。ポジションサイズとリスク金額の計算は、資金のあらかじめ決められた割合の小部分だけをリスクにさらすことを可能にし、大きな損失から保護します。pip価値や損益見積もりは、取引計画を精密に立て、現実的な目標やストップロスレベルを設定するのに役立ちます。証拠金の必要額や証拠金維持率の計算は、過度なレバレッジを防ぎ、証拠金不足や口座破綻を回避します。スワップ手数料は、特にキャリートレードのような長期ポジションにおいて重要で、保有コストに直接影響を与えます。

リスクリワード比は取引の選択を導き、潜在的な利益がリスクに見合うかを判断する基準になります。加えて、スプレッドコスト、ブレークイーブン価格、期待収益、レバレッジの影響といった指標を考慮することで、取引コスト、戦略の実現可能性、全体的なリスク露出を踏まえた意思決定が可能になります。これらを組み合わせて活用することで、トレーダーは情報に基づいた規律ある判断を下し、取引を自らの目標や市場環境に合わせることができます。その結果として、取引の一貫性と収益性が高まります。

次のセクションでは、本日の開発にどのように取り組むかについて、要点をまとめたアウトラインを示します。


概要

本連載では、モジュラー設計を取り入れたことにより、プログラムの特定のセクションに集中して作業できるようになり、他のコンポーネントに影響を与えずに開発を進められるようになりました。この柔軟性を活かし、計算ツールを統合するためのスペースを確保する形で、取引管理パネルをアップグレードできるようになっています。

これを実現するために、MQL5標準ライブラリに含まれる追加クラスを活用します。各注文タイプごとに入力セクションを用意するのではなく、ドロップダウンメニューで注文タイプを選択し、単一の入力行で対応する方式に変更します。このシンプルなレイアウトにより、計算ツール用コンポーネントのためのスペースを効率的に確保できます。

すべての取引値を表示する必要はありませんが、意思決定に不可欠な主要な値はいくつかあり、それらは必ず利用可能にする必要があります。なお、一部の値はMQL5のライブ市場データから直接取得できるため、改めて計算する必要はありません。

まずは主要なFX用語や取引値について詳しく解説し、定義、計算式、MQL5内での扱い方を確認します。その後、実装フェーズに進み、取引管理パネルの注文セクションを調整して計算ツールのフロントエンドを統合する準備をおこないます。

取引管理パネルの変更点

TradeManagementPanelの拡張

上の図でaと示したセクションには、ComboBoxクラスを利用して注文タイプの一覧と選択を実装します。bのセクションは1行レイアウトにまとめ、cの有効期限入力にはDatePickerを導入して操作性を高めます。

レイアウトを調整した後に、計算ロジックとGUI入力ロジックを統合します。計算ごとに必要となる入力項目は通常3つ未満で済むため、効率的に組み込むことが可能です。

最後に、テスト手順とその結果を共有し、新機能の評価をおこなって締めくくります。


FX取引計算と数式 

以下の表では、計算が必要となる一般的なFX用語と、それに対応する計算式およびMQL5で使用されるカスタム関数を示しています。これらの例は網羅的なものではありません。トレーダーとして特定の戦略を実行する際には、追加の計算が必要になる場合があります。表に示した計算式は、様々なオンライン情報源から得た数学的知見を組み合わせた結果です。さらに学習や確認をおこなう場合は、Googleやその他信頼できる情報源を参照することをお勧めします。

FX用語と説明 一般的な式 MQL5での式の実装
ポジションサイズ

口座残高、リスク割合、ストップロスに基づいて取引ロット数を計算し、トレーダーの戦略に沿ったリスク管理を可能にします。




double CalculatePositionSize(double accountBalance, 
   double riskPercent, double stopLossPips, 
   string symbol)
{
   if (accountBalance <= 0 || riskPercent <= 0 || 
       stopLossPips <= 0) return 0.0;
   double pipValue = CalculatePipValue(symbol, 1.0, 
       AccountCurrency());
   if (pipValue == 0) return 0.0;
   double positionSize = (accountBalance * (riskPercent / 
       100.0)) / (stopLossPips * pipValue);
   double lotStep = MarketInfo(symbol, 
       MODE_LOTSTEP);
   double minLot = MarketInfo(symbol, 
       MODE_MINLOT);
   double maxLot = MarketInfo(symbol, 
       MODE_MAXLOT);
   return NormalizeDouble(
       MathMax(minLot, MathMin(maxLot, 
       positionSize)), (int)-MathLog10(lotStep));
}
                


リスク金額

ポジションサイズとストップロスに基づき、取引でリスクにさらされる金額を算出し、損失が許容範囲内に収まるようにします。




double CalculateRiskAmount(double positionSize, 
   double stopLossPips, string symbol)
{
   if (positionSize <= 0 || stopLossPips <= 0) 
       return 0.0;
   double pipValue = CalculatePipValue(symbol, positionSize, 
       AccountCurrency());
   return NormalizeDouble(positionSize * stopLossPips * 
       pipValue, 2);
}
                


pip価値

指定したロットサイズにおける1 pipの金銭的価値を計算し、リスクや利益の算出に不可欠です。




double CalculatePipValue(string symbol, 
   double lotSize, string accountCurrency)
{
   double tickSize = MarketInfo(symbol, 
       MODE_TICKSIZE);
   double tickValue = MarketInfo(symbol, 
       MODE_TICKVALUE);
   double pipSize = StringFind(symbol, 
       "JPY") >= 0 ? 0.01 : 0.0001;
   double conversionRate = 1.0;
   if (accountCurrency != SymbolInfoString(symbol, 
       SYMBOL_CURRENCY_PROFIT)) {
      string conversionPair = SymbolInfoString(
          symbol, SYMBOL_CURRENCY_PROFIT) + accountCurrency;
      if (SymbolSelect(conversionPair, true)) {
         conversionRate = MarketInfo(conversionPair, 
             MODE_BID);
      } else {
         Print("Warning: Conversion pair ", 
             conversionPair, " not found, using 1.0");
      }
   }
   if (tickSize == 0) return 0.0;
   return NormalizeDouble((tickValue / tickSize) * 
       pipSize * lotSize * conversionRate, 2);
}
                


証拠金の必要額

ロットサイズ、契約サイズ、レバレッジに基づき、ポジションを建てるのに必要な資金を算出し、過剰なレバレッジを避けます。




double CalculateMarginRequirement(double lotSize, 
   string symbol)
{
   double marginRequired = MarketInfo(symbol, 
       MODE_MARGINREQUIRED);
   if (marginRequired == 0) {
      Print("Error: Margin requirement not available ", 
          symbol);
      return 0.0;
   }
   return NormalizeDouble(lotSize * marginRequired, 
       2);
}
                


損益見積もり

エントリー価格と決済価格に基づき、取引の潜在的な利益または損失を見積もり、現実的な目標設定をサポートします。




double CalculateProfitLoss(double entryPrice, 
   double exitPrice, double lotSize, 
   string symbol)
{
   if (lotSize <= 0 || entryPrice <= 0 || 
       exitPrice <= 0) return 0.0;
   double contractSize = MarketInfo(symbol, 
       MODE_LOTSIZE);
   double conversionRate = 1.0;
   if (AccountCurrency() != SymbolInfoString(symbol, 
       SYMBOL_CURRENCY_PROFIT)) {
      string conversionPair = SymbolInfoString(
          symbol, SYMBOL_CURRENCY_PROFIT) + AccountCurrency();
      if (SymbolSelect(conversionPair, true)) {
         conversionRate = MarketInfo(conversionPair, 
             MODE_BID);
      }
   }
   double priceDiff = exitPrice - entryPrice;
   double pips = priceDiff / (StringFind(symbol, 
       "JPY") >= 0 ? 0.01 : 0.0001);
   return NormalizeDouble(pips * CalculatePipValue(symbol, 
       lotSize, AccountCurrency()), 2);
}
                


スワップ/オーバーナイト手数料

ポジションを一晩保有した際の受取または支払利息を計算し、長期取引において重要です。




double CalculateSwap(double lotSize, 
   string symbol, bool isBuy, 
   int days = 1)
{
   double swapLong = MarketInfo(symbol, 
       MODE_SWAPLONG);
   double swapShort = MarketInfo(symbol, 
       MODE_SWAPSHORT);
   if (swapLong == 0 && swapShort == 0) {
      Print("Error: Swap rates not available ", 
          symbol);
      return 0.0;
   }
   double swap = isBuy ? swapLong : swapShort;
   datetime currentTime = TimeCurrent();
   if (TimeDayOfWeek(currentTime) == 3) 
       days *= 3;
   double totalSwap = lotSize * swap * days;
   return NormalizeDouble(totalSwap, 2);
}
                


リスクリワード比

潜在的な利益と潜在的な損失の比率を測定し、期待値の高い取引の選択をサポートします。




double CalculateRiskRewardRatio(double takeProfitPips, 
   double stopLossPips)
{
   if (stopLossPips <= 0 || takeProfitPips <= 0) 
       return 0.0;
   return NormalizeDouble(takeProfitPips / stopLossPips, 
       2);
}
                


証拠金維持率

口座資産に対する使用証拠金の割合を表示し、マージンコールを回避するために口座状況を監視します。




double CalculateMarginLevel()
{
   double equity = AccountEquity();
   double margin = AccountMargin();
   if (margin == 0) return 0.0;
   return NormalizeDouble((equity / margin) * 100, 
       2);
}
                


スプレッドコスト

取引における売値と買値のスプレッドの金銭的コストを計算し、短期取引戦略において重要です。




double CalculateSpreadCost(double lotSize, 
   string symbol)
{
   double spreadPips = MarketInfo(symbol, 
       MODE_SPREAD) / 10.0;
   double pipValue = CalculatePipValue(symbol, lotSize, 
       AccountCurrency());
   return NormalizeDouble(spreadPips * pipValue * lotSize, 
       2);
}
                


レバレッジの影響

取引で使用される実効レバレッジを測定し、口座資産に対するリスクエクスポージャーを明示します。




double CalculateLeverageImpact(double positionSize, 
   string symbol, double accountEquity)
{
   if (positionSize <= 0 || accountEquity <= 0) 
       return 0.0;
   double contractSize = MarketInfo(symbol, 
       MODE_LOTSIZE);
   double marketPrice = MarketInfo(symbol, 
       MODE_BID);
   return NormalizeDouble((positionSize * contractSize * 
       marketPrice) / accountEquity, 2);
}
                



次の実装セクションでは、MQL5標準ライブラリのCComboBoxを活用して、TradeManagementPanelに統合される計算ツールの操作パネルのスペース効率を最適化します。このアプローチは、効率的なUIデザインやコントロール管理に関する有益な知見を提供します。さらに、注文の有効期限を選択する際のユーザー体験を向上させるためにDatePickerコンポーネントも組み込みます。


実装

着実な進捗を確保するために、開発を以下の4つの主要ステージに分けて進めます。

これらのステップが完了したら、New Admin Panel EAを新機能に対応させ、テストを実行します。特にComboBoxやDatePickerコンポーネントを扱う際は、重要な詳細を見落とさないよう注意が必要です。


(1) 新しいコントロールを配置するためのPending Ordersセクションの調整

まず、TradeManagementPanelヘッダからPending Ordersセクションを抽出し、ComboBoxとDatePickerコンポーネントを実装しやすいように分離します。さらに、注文が完全に設定された後に押す[Place Order]ボタンも追加します。

Pending Ordersメンバーの宣言

これらのメンバー変数はCTradeManagementPanelクラスのPending Ordersセクションに属します。まず、Pending Ordersコントロール上部に表示されるラベルを1つ宣言します(セクションヘッダとして「Pending Orders:」と表示)。

//  Pending Orders
CLabel      m_secPendingLabel;    // “Pending Orders:” header
CLabel      m_pendingPriceHeader; // “Price:” column header
CLabel      m_pendingTPHeader;    // “TP:” column header
CLabel      m_pendingSLHeader;    // “SL:” column header
CLabel      m_pendingExpHeader;   // “Expiration:” column header

CComboBox   m_pendingOrderType;   // Combobox for “Buy Limit / Buy Stop / Sell Limit / Sell Stop”
CEdit       m_pendingPriceEdit;   // Edit box for pending‐order price
CEdit       m_pendingTPEdit;      // Edit box for pending‐order take‐profit
CEdit       m_pendingSLEdit;      // Edit box for pending‐order stop‐loss
CDatePicker m_pendingDatePicker;  // DatePicker for expiration date
CButton     m_placePendingButton; // “Place Order” button for pending orders

その直下に、さらに4つのラベル「Price:」、 「TP:」、「SL:」、「Expiration:」を配置して列ヘッダとします。ラベルの下にはComboBoxを配置し、ユーザーがBuy Limit、Buy Stop、Sell Limit、Sell Stopの4種類の未決注文から選択できるようにします。そのComboBoxの右側には、3つの編集ボックスを配置し、ユーザーがそれぞれ未決注文の価格(Price)、利確(TP)、損切り(SL)を入力できるようにします。さらにその横にはDatePickerを配置し、Expiration Dateの選択を簡単にします。最後にPlace Orderとラベル付けされたボタンを宣言し、押下時に指定されたパラメータで未決注文を実際に作成します。

これら6つのコントロールと5つのラベルをこのセクションの下にまとめることで、未決注文の構築と管理に必要な要素をすべて独立させます。この分離により、パネルの他部分に触れることなく、未決注文のロジックだけを説明したりリファクタリングしたりすることが容易になります。

Create(...)で未決注文コントロールを作成する

Create(...)メソッド内では、FX取引計算ツールの下に仕切り線を描画した直後に、Pending Ordersセクション全体を作成します。まず、上部の計算ツールと視覚的に区切るために小さな垂直ギャップを追加します。次に、セクションヘッダ用のラベル「Pending Orders:」を作成し、他のセクションと区別するために太字スタイルを適用します。

次に、注文タイプ選択用のComboBoxをこのヘッダの右側に配置します。ComboBoxを追加し垂直位置を下げた後、4つの列ヘッダ「Price:」「TP:」「SL:」「Expiration:」を作成します。それぞれのヘッダは入力行の上で横方向に均等に配置され、きれいに揃うようにします。

// In CTradeManagementPanel::Create(...), after Section separator:

// 10px vertical offset before “Section 3” header
curY += 10;
if(!CreateLabelEx(m_secPendingLabel, curX, curY, DEFAULT_LABEL_HEIGHT, 
                  "SecPend", "Pending Orders:", clrNavy))
   return(false);
m_secPendingLabel.Font("Arial Bold");
m_secPendingLabel.FontSize(10);

// Create the Combobox for order types
if(!CreateComboBox(m_pendingOrderType, "PendingOrderType", 
                   curX + SECTION_LABEL_WIDTH + GAP, curY, DROPDOWN_WIDTH, EDIT_HEIGHT))
   return(false);
curY += EDIT_HEIGHT + GAP;

// Column headers: Price, TP, SL, Expiration
int headerX = curX;
if(!CreateLabelEx(m_pendingPriceHeader, headerX, curY, DEFAULT_LABEL_HEIGHT, 
                  "PendPrice", "Price:", clrBlack))
   return(false);
if(!CreateLabelEx(m_pendingTPHeader, headerX + EDIT_WIDTH + GAP, curY, DEFAULT_LABEL_HEIGHT, 
                  "PendTP", "TP:", clrBlack))
   return(false);
if(!CreateLabelEx(m_pendingSLHeader, headerX + 2 * (EDIT_WIDTH + GAP), curY, DEFAULT_LABEL_HEIGHT, 
                  "PendSL", "SL:", clrBlack))
   return(false);
if(!CreateLabelEx(m_pendingExpHeader, headerX + 3 * (EDIT_WIDTH + GAP), curY, DEFAULT_LABEL_HEIGHT, 
                  "PendExp", "Expiration:", clrBlack))
   return(false);
curY += DEFAULT_LABEL_HEIGHT + GAP;

// Pending orders inputs row:
//  • Pending Price
int inputX = curX;
if(!CreateEdit(m_pendingPriceEdit, "PendingPrice", inputX, curY, EDIT_WIDTH, EDIT_HEIGHT))
   return(false);
double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
m_pendingPriceEdit.Text(DoubleToString(ask, 5));

//  • Pending TP
int input2X = inputX + EDIT_WIDTH + GAP;
if(!CreateEdit(m_pendingTPEdit, "PendingTP", input2X, curY, EDIT_WIDTH, EDIT_HEIGHT))
   return(false);
m_pendingTPEdit.Text("0.00000");

//  • Pending SL
int input3X = input2X + EDIT_WIDTH + GAP;
if(!CreateEdit(m_pendingSLEdit, "PendingSL", input3X, curY, EDIT_WIDTH, EDIT_HEIGHT))
   return(false);
m_pendingSLEdit.Text("0.00000");

//  • Pending Expiration (DatePicker)
int input4X = input3X + EDIT_WIDTH + GAP;
if(!CreateDatePicker(m_pendingDatePicker, "PendingExp", 
                     input4X, curY, DATEPICKER_WIDTH + 20, EDIT_HEIGHT))
   return(false);
datetime now = TimeCurrent();
datetime endOfDay = now - (now % 86400) + 86399;
m_pendingDatePicker.Value(endOfDay);

//  • Place Order button
int buttonX = input4X + DATEPICKER_WIDTH + GAP;
if(!CreateButton(m_placePendingButton, "Place Order", 
                 buttonX + 20, curY, BUTTON_WIDTH, BUTTON_HEIGHT, clrBlue))
   return(false);
curY += BUTTON_HEIGHT + GAP * 2;

ヘッダの配置が完了したら、垂直位置を下げて入力行を開始します。まず、未決注文の価格用編集フィールドを作成し、ユーザーに有効なデフォルトを示すため、現在のAsk価格で初期化します。その右にはTP編集フィールド(初期値0.00000)を配置し、続けてSL編集フィールド(初期値0.00000)を配置します。これらの横にはDatePickerを作成し、デフォルトで当日の終わり(23:59:59)に設定します。

最後に、Place OrderボタンをDatePickerの横に配置し、他のコントロールを圧迫しないようにします。すべてのコントロールが作成された後、垂直カーソルを進めて下部に余白を確保します。これらの手順により、ユーザーが未決注文の種類、価格、TP、SL、有効期限を設定し、ボタンを押して注文を作成できるように、すべてのコントロールが整列します。

Pending Ordersイベントハンドラ

これらのメソッドは、Pending Ordersセクション内でのユーザー操作に応答します。

void CTradeManagementPanel::OnChangePendingOrderType()
{
   string selected = m_pendingOrderType.Select();
   int    index    = (int)m_pendingOrderType.Value();
   Print("OnChangePendingOrderType: Selected='", selected, "', Index=", index);

   double price = 0.0;
   if(selected == "Buy Limit" || selected == "Buy Stop")
      price = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
   else
      price = SymbolInfoDouble(Symbol(), SYMBOL_BID);

   m_pendingPriceEdit.Text(DoubleToString(price, 5));
   ChartRedraw();
}

void CTradeManagementPanel::OnChangePendingDatePicker()
{
   datetime selected = m_pendingDatePicker.Value();
   Print("OnChangePendingDatePicker: Selected='", 
         TimeToString(selected, TIME_DATE|TIME_MINUTES), "'");
   ChartRedraw();
}

別の注文タイプを選択した場合:ユーザーがComboBoxで新しい未決注文タイプを選択した場合(例:Buy LimitからSell Limitへ切り替え)、選択されたテキストを読み取り、先頭が「Buy」か「Sell」かを判定します。先頭がBuyの場合は現在のAsk価格を取得し、それ以外の場合はBid価格を取得します。その値を価格入力フィールドに即座に反映させます。これにより、ユーザーは常に選択した未決注文タイプに対応した有効かつ最新のデフォルト価格を見ることができます。最後に、チャートUIを再描画し、新しい価格を即時に表示します。

有効期限変更時:ユーザーがDatePickerで有効期限を選択または変更した場合、新しい日付を取得してデバッグ用にログに記録します。その後、チャートUIを再描画し、パネル内の他の要素が選択した有効期限に依存している場合も即座に反映されるようにします。この段階では追加の検証はおこなわず、カレンダー上の有効な日付であれば受け入れます。

これらのハンドラを小さく、特化させることで、ComboBoxとDatePickerが常に現在の市場状況と同期し、ユーザーが無効な価格で未決注文を作成したり、期限切れの日付を誤って選択することを防ぎます。

Pending Orders検証ヘルパー

未決注文をブローカーに送信する前に、ユーザーの入力が妥当であることを確認します。このヘルパー関数では、以下の3つのルールを強制します。

  • ロット数は正の値であること:0または負のロット数の場合、エラーをログに記録し、注文を拒否します。
  • 価格は正の値であること:0または負の価格では、有効な未決注文を作成できません。

bool CTradeManagementPanel::ValidatePendingParameters(double volume, double price, string orderType)
{
   if(volume <= 0)
   {
      Print("Invalid volume for pending order");
      return(false);
   }
   if(price <= 0)
   {
      Print("Invalid price for pending order");
      return(false);
   }
   double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
   double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID);

   if(orderType == "Buy Limit" && price >= ask)
   {
      Print("Buy Limit price must be below Ask");
      return(false);
   }
   if(orderType == "Buy Stop" && price <= ask)
   {
      Print("Buy Stop price must be above Ask");
      return(false);
   }
   if(orderType == "Sell Limit" && price <= bid)
   {
      Print("Sell Limit price must be above Bid");
      return(false);
   }
   if(orderType == "Sell Stop" && price >= bid)
   {
      Print("Sell Stop price must be below Bid");
      return(false);
   }
   return(true);
}

市場状況チェック

  1. Buy Limitの場合、リミット価格が現在のAsk価格よりも厳密に低いことを確認します。
  2. Buy Stopの場合、ストップ価格が現在のAsk価格よりも厳密に高いことを確認します。

すべてのチェックが通過すれば、ヘルパー関数はtrueを返し、注文を進めることができることを示します。このように検証を構造化することで、市場価格より高いBuy Limitを設定する、Bid以上でSell Stopを設定するといった、一般的なミスを防止できます。また、入力が無効な場合には、即座にわかりやすいフィードバックを提供します。

「Place Pending」ボタンハンドラ

void CTradeManagementPanel::OnClickPlacePending()
{
   Print("OnClickPlacePending called");
   string     orderType = m_pendingOrderType.Select();
   double     price     = StringToDouble(m_pendingPriceEdit.Text());
   double     tp        = StringToDouble(m_pendingTPEdit.Text());
   double     sl        = StringToDouble(m_pendingSLEdit.Text());
   double     volume    = StringToDouble(m_volumeEdit.Text());      // reuse market‐order volume
   datetime   expiry    = m_pendingDatePicker.Value();
   ENUM_ORDER_TYPE_TIME type_time = (expiry == 0) ? ORDER_TIME_GTC : ORDER_TIME_SPECIFIED;

   // Validate inputs
   if(!ValidatePendingParameters(volume, price, orderType))
      return;

   // Place the correct type of pending order
   if(orderType == "Buy Limit")
      m_trade.BuyLimit(volume, price, Symbol(), sl, tp, type_time, expiry, "");
   else if(orderType == "Buy Stop")
      m_trade.BuyStop(volume, price, Symbol(), sl, tp, type_time, expiry, "");
   else if(orderType == "Sell Limit")
      m_trade.SellLimit(volume, price, Symbol(), sl, tp, type_time, expiry, "");
   else if(orderType == "Sell Stop")
      m_trade.SellStop(volume, price, Symbol(), sl, tp, type_time, expiry, "");
}

ユーザーが[Place Order]ボタンをクリックすると、このハンドラは必要な入力値をすべて取得します。

  • ComboBoxから選択された注文タイプ
  • 対応する編集フィールドから取得する未決注文の価格
  • TP(利確)およびSL(損切り)の値
  • Quick Order Executionセクションのボリューム編集フィールドから取得する取引量
  • DatePickerから取得する有効期限

次に、選択された有効期限の日時がゼロかどうかに応じて、GTC (Good Till Canceled)か指定日時による期限モードを決定します。続いて、検証ヘルパーを呼び出します。いずれかのチェックが失敗した場合、何もせずに処理を終了します。

検証が成功した場合、4つのCTradeメソッド(BuyLimit、BuyStop、SellLimit、SellStopのいずれか)を1つだけ呼び出します。引数として、出来高、価格、銘柄、SL、TP、時間モード、有効期限を渡します。各呼び出しはユーザーの入力値を使用するため、このハンドラの処理完了時点で、ブローカーは正しい未決注文リクエストを受け取ることになります。いずれかのパラメータが無効であった場合は、単に処理を終了し、ログに記録された診断情報で失敗を確認します。

Pending Orders OnEvent(…)ルーティング

bool CTradeManagementPanel::OnEvent(const int id, const long &lparam, 
                                    const double &dparam, const string &sparam)
{
   // 1) Forward all events to the calculator first
   if(m_calculator.OnEvent(id, lparam, dparam, sparam))
      return(true);

   // 2) Dispatch Pending‐section events
   if(id == CHARTEVENT_OBJECT_CLICK)
   {
      if(sparam == m_placePendingButton.Name())
      {
         OnClickPlacePending();
         return(true);
      }
   }
   else if(id == CHARTEVENT_OBJECT_CHANGE)
   {
      if(sparam == m_pendingOrderType.Name())
      {
         OnChangePendingOrderType();
         return(true);
      }
      else if(sparam == m_pendingDatePicker.Name())
      {
         OnChangePendingDatePicker();
         return(true);
      }
   }

   // 3) Fallback to the base class for any other events
   return CAppDialog::OnEvent(id, lparam, dparam, sparam);
}

CTradeManagementPanelのメインOnEvent(...)メソッド内では、未決注文イベントは次のようにルーティングされます。

計算ツールを最優先:まず、すべてのイベントを埋め込み計算ツールに転送します。計算ツールがイベントを処理した場合(例:ユーザーがpip価値入力を変更した場合)、そこで処理を終了します。

Pending Ordersロジック

  • イベントが「click」で、クリックされたオブジェクト名が未決注文ボタンに一致する場合、「Place Pending」ハンドラを呼び出します。
  • イベントが「object change」で、変更されたオブジェクト名がComboBoxまたはDatePickerに一致する場合、それぞれOnChangePendingOrderTypeまたはOnChangePendingDatePickerハンドラを呼び出します。
  • フォールバック:それ以外のイベントはすべてCAppDialog::OnEvent(...)に流し、Quick Order ExecutionセクションやAll Order Operationsセクションがクリックや編集を処理できるようにします。

このルーティングにより、未決注文関連の操作が他のセクションに干渉せず、独立してクリーンに処理されます。

ComboBoxとDatePickerの実装テスト

調整されたTradeManagementPanel(ComboBoxとDatePickerの実装)


(2) FX取引計算ツールコントロールクラスの開発

クラス定義の前に、Controlsディレクトリ下のMQL5標準ライブラリヘッダを5つインクルードします。これらにより、CForexCalculator内で活用できるGUIコントロールクラスが利用可能になります。

#include <Controls\Dialog.mqh>
#include <Controls\ComboBox.mqh>
#include <Controls\Edit.mqh>
#include <Controls\Label.mqh>
#include <Controls\Button.mqh>

Dialog.mqh

CAppDialogの基底クラスを提供します。このクラスは、コントロールのコレクションを管理し、レイアウトを処理し、イベントをルーティングします。CForexCalculatorは直接CAppDialogから派生していませんが、親ダイアログ(例:CTradeManagementPanel)に統合する必要があります。そのため、Dialog.mqhが存在することで、計算ツールのコントロールを追加するAddToDialog呼び出しやイベント転送が正しくコンパイルされます。Dialog.mqhがなければ、ラベルや編集フィールド、ボタンを親UIに追加するdlg.Add(...)呼び出しができません。

ComboBox.mqh

CComboBoxクラスを提供します。これを使用して計算オプション用のドロップダウンを作成します。このファイルをインクルードすることで、CComboBoxインスタンス(m_dropdown)を生成・操作でき、m_dropdown.Create(...)で位置を設定したり、AddItemで項目を追加したり、ユーザーが別の項目を選択したときのCHARTEVENT_OBJECT_CHANGEに応答できます。これがなければ、コンパイラはCComboBoxを認識できません。

Edit.mqh

CEditクラスを定義します。数値や文字列入力フィールド(口座残高、リスク割合、ストップロス、銘柄など)に使用します。選択された計算項目に応じて、m_inputs[]内に可変数のCEditコントロールを動的に作成します。各CEditは作成され、ダイアログに追加され、その後GetInputValueやGetInputStringでCEdit*にキャストされます。Edit.mqhを省略すると、これらの呼び出しはいずれもコンパイルされません。

Label.mqh

CLabelを提供します。画面上に静的テキストを表示する際に使用します。たとえば「Calculation Option:」ラベル(m_calcOptionLabel)、口座残高やリスク割合などの各入力ラベル、「Result:」ラベル(m_resultLabel)などです。各CLabelは作成され、ユーザーが各CEditに何を入力すべきかを認識できるようにします。Label.mqhがなければ、各編集ボックスにコンテキストを提供できません。

Button.mqh

CButtonクラスを提供します。[Calculate]ボタン(m_calculateButton)に使用します。このヘッダをインクルードすることで、m_calculateButton.Create(...)を呼び出し、背景色を設定したり、テキストを設定したり、OnEventでクリックを検知できます。Button.mqhを省略すると、コンパイラはCButtonを認識せず、[Calculate]ボタンクリックに対応できません。

プロジェクトレベルでのインクルード計画

大規模プロジェクトでは、これらのコントロールに依存する部分が2つあります。

ForexValuesCalculator.mqhは5つのControls*.mqhヘッダすべてを必要とします。なぜなら、さまざまなFX取引計算用の自立した再利用可能な「ミニダイアログ」を構築するからです。CLabel、CEdit、CComboBox、CButtonを使用する箇所では、対応するヘッダが存在しなければMQL5プリプロセッサがクラス定義を認識できません。

GUI関連のインクルードをすべて上部にまとめることで、他のEAやパネル(例:TradeManagementPanel.mqh)は単に「#include "ForexValuesCalculator.mqh"」するだけで、必要なすべてのGUIコントロールにアクセスできます。

メンバー宣言 

CForexCalculatorクラスは、計算ツールインターフェースを構成するいくつかのUIコントロールとデータ構造を宣言することから始まります。上部には、ラベル(m_calcOptionLabel)とドロップダウン(m_dropdown)があり、ユーザーはどの計算をおこなうか選択できます(例:ポジションサイズ、リスク額、pip価値、損益、リスク・リワード比)。その下には[Calculate]ボタン(m_calculateButton)があり、ユーザーがすべての入力を設定した後にクリックします。結果を表示するために、読み取り専用の編集フィールド(m_resultField)と、「Result: …」のような説明テキストを表示するラベル(m_resultLabel)をペアで配置します。
// Forex Calculator Class
class CForexCalculator {
private:
   CLabel      m_calcOptionLabel;   // “Calculation Option:” label
   CComboBox   m_dropdown;          // Dropdown for selecting calculation term
   CEdit       m_resultField;       // Read-only field to display result
   CLabel      m_resultLabel;       // Label preceding the result (e.g., “Result:”)
   CButton     m_calculateButton;   // “Calculate” button
   CWnd       *m_inputs[];          // Dynamically added label+edit pairs
   long        m_chart_id;          // Chart identifier
   string      m_name;              // Prefix for control names
   int         m_originX;           // X-coordinate origin for dynamic fields
   int         m_originY;           // Y-coordinate origin for dynamic fields

   InputField  m_positionSizeInputs[4];
   InputField  m_riskAmountInputs[3];
   InputField  m_pipValueInputs[3];
   InputField  m_profitLossInputs[4];
   InputField  m_riskRewardInputs[2];

   // … (other private methods follow) …
public:
   CForexCalculator();
   bool Create(const long chart, const string &name, const int subwin,
               const int x, const int y, const int w, const int h);
   bool AddToDialog(CAppDialog &dlg);
   void UpdateResult(const string term);
   double GetInputValue(const string name);
   string GetInputString(const string &name);
   CEdit* GetInputEdit(const string &name);
   string GetSelectedTerm();
   bool OnEvent(const int id, const long &lparam,
                const double &dparam, const string &sparam);
   ~CForexCalculator();
};

すべての可変入力フィールド(各フィールドはラベルと編集ボックスで構成)は、動的配列m_inputs[]に格納されます。内部的には、クラスはm_positionSizeInputs、m_riskAmountInputs、m_pipValueInputs、m_profitLossInputs、m_riskRewardInputsの5つの固定サイズ配列のInputField構造体を保持しています。各InputFieldエントリには、名前、ラベル文字列、およびデフォルト数値が含まれます。さらに、m_originXとm_originYは親ダイアログ内で計算ツールパネルが始まる位置を追跡し、m_chart_idm_nameはチャート識別子と一意のコントロール名の接頭辞を格納します。これらのメンバーによって、計算ツールのレイアウトと、各種類のFX取引計算に必要なデータが定義されます。

静的デフォルトの初期化(InitInputs)

InitInputsメソッドは、計算ツールオブジェクトの構築時に一度だけ実行されます。このメソッドは、5つのInputField配列に説明ラベルとフォールバック用の数値を設定します。たとえば、「ポジションサイズ」グループには口座残高、リスク割合、ストップロス(pip単位)、銘柄用のフィールドが含まれ、「リスク金額」グループにはポジションサイズ、ストップロス(pip単位)、銘柄用のフィールドが含まれます。各配列は、後でユーザーが計算タイプを選択した際に、対応するInputField配列が動的コントロールにコピーされるように設定されています。この段階では、口座残高フィールドには0.0のプレースホルダーデフォルトが設定され(実行時に置き換え)、リスク割合やpip価値には1%や20 pipなどの小さなデフォルト値が設定されます。この静的初期化により、各計算の入力欄が適切なラベルと初期数値で表示されることが保証されます。

void InitInputs()
{
   // Position Size inputs
   m_positionSizeInputs[0].name         = "accountBalance";
   m_positionSizeInputs[0].label        = "Account Balance (" + AccountInfoString(ACCOUNT_CURRENCY) + ")";
   m_positionSizeInputs[0].defaultValue = 0.0;  // updated at runtime
   m_positionSizeInputs[1].name         = "riskPercent";
   m_positionSizeInputs[1].label        = "Risk Percentage (%)";
   m_positionSizeInputs[1].defaultValue = 1.0;
   m_positionSizeInputs[2].name         = "stopLossPips";
   m_positionSizeInputs[2].label        = "Stop Loss (Pips)";
   m_positionSizeInputs[2].defaultValue = 20.0;
   m_positionSizeInputs[3].name         = "symbol";
   m_positionSizeInputs[3].label        = "Symbol";
   m_positionSizeInputs[3].defaultValue = 0.0;

   // Risk Amount inputs
   m_riskAmountInputs[0].name = "positionSize";
   m_riskAmountInputs[0].label = "Position Size (Lots)";
   m_riskAmountInputs[0].defaultValue = 0.1;
   m_riskAmountInputs[1].name = "stopLossPips";
   m_riskAmountInputs[1].label = "Stop Loss (Pips)";
   m_riskAmountInputs[1].defaultValue = 20.0;
   m_riskAmountInputs[2].name = "symbol";
   m_riskAmountInputs[2].label = "Symbol";
   m_riskAmountInputs[2].defaultValue = 0.0;

   // Pip Value inputs
   m_pipValueInputs[0].name = "lotSize";
   m_pipValueInputs[0].label = "Lot Size";
   m_pipValueInputs[0].defaultValue = 0.1;
   m_pipValueInputs[1].name = "symbol";
   m_pipValueInputs[1].label = "Symbol";
   m_pipValueInputs[1].defaultValue = 0.0;
   m_pipValueInputs[2].name = "accountCurrency";
   m_pipValueInputs[2].label = "Account Currency";
   m_pipValueInputs[2].defaultValue = 0.0;

   // Profit/Loss inputs
   m_profitLossInputs[0].name = "entryPrice";
   m_profitLossInputs[0].label = "Entry Price";
   m_profitLossInputs[0].defaultValue = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   m_profitLossInputs[1].name = "exitPrice";
   m_profitLossInputs[1].label = "Exit Price";
   m_profitLossInputs[1].defaultValue = SymbolInfoDouble(_Symbol, SYMBOL_BID) + 0.0020;
   m_profitLossInputs[2].name = "lotSize";
   m_profitLossInputs[2].label = "Lot Size";
   m_profitLossInputs[2].defaultValue = 0.1;
   m_profitLossInputs[3].name = "symbol";
   m_profitLossInputs[3].label = "Symbol";
   m_profitLossInputs[3].defaultValue = 0.0;

   // Risk-to-Reward inputs
   m_riskRewardInputs[0].name = "takeProfitPips";
   m_riskRewardInputs[0].label = "Take Profit (Pips)";
   m_riskRewardInputs[0].defaultValue = 40.0;
   m_riskRewardInputs[1].name = "stopLossPips";
   m_riskRewardInputs[1].label = "Stop Loss (Pips)";
   m_riskRewardInputs[1].defaultValue = 20.0;
}

実行時デフォルトの設定(SetDynamicDefaults)

ユーザーの実際の口座残高は実行時でしか判明しないため、SetDynamicDefaultsメソッドではm_positionSizeInputs[0].defaultValue(「口座残高」フィールド)をAccountInfoDouble(ACCOUNT_BALANCE)で上書きします。これにより、「ポジションサイズ」の入力フィールドが画面に表示されたとき、口座残高の編集ボックスにはトレーダーの実際の残高が事前に入力されることになります。その他の動的デフォルト値(Bid/Ask価格や換算レートなど)も、計算ツール作成時に同様に更新されます。静的デフォルトと実行時デフォルトを分けることで、クラスの柔軟性が保たれます。設計時の初期化はInitInputsでおこない、市場依存フィールドの迅速な調整はSetDynamicDefaultsでおこなう、という構造です。

void SetDynamicDefaults()
{
   // Overwrite the “Account Balance” default with the real balance at runtime
   m_positionSizeInputs[0].defaultValue = AccountInfoDouble(ACCOUNT_BALANCE);
}

コア計算ヘルパー

入力配列の下には、各計算式を実行する一連のヘルパーメソッドが配置されています。

1. pip価値の計算

double CalculatePipValue(const string symbol, const double lotSize, const string accountCurrency)
{
   double tickSize  = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
   double tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
   double pipSize   = (StringFind(symbol, "JPY") >= 0) ? 0.01 : 0.0001;
   double rate      = 1.0;
   string profitCcy = SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT);
   if(accountCurrency != profitCcy)
   {
      string pair = profitCcy + accountCurrency;
      if(SymbolSelect(pair, true))
         rate = SymbolInfoDouble(pair, SYMBOL_BID);
   }
   if(tickSize == 0.0) return 0.0;
   return NormalizeDouble((tickValue / tickSize) * pipSize * lotSize * rate, 2);
}

CalculatePipValueは、指定した銘柄とロットサイズに対して、1 pipの価値が口座通貨でいくらになるかを計算します。まずSymbolInfoDoubleを呼び出して、SYMBOL_TRADE_TICK_SIZESYMBOL_TRADE_TICK_VALUEを取得します。その後、pipサイズとして0.01(JPYペアの場合)または0.0001を選択します。通貨ペアの利益通貨が口座通貨と異なる場合、両者を組み合わせた銘柄(例:利益通貨がEUR、口座通貨がUSDの場合はEURUSD)を選択し、その変換銘柄の現在のBidをレートとして取得します。最後に、「tickValue ÷ tickSize × pipSize × lotSize × rate」を計算し、小数点2桁に丸めた値を返します。返り値が0.0の場合は、入力が無効(例:tickSizeがゼロ)であることを示します。

2. CalculatePositionSize

double CalculatePositionSize(double bal, double pct, double sl, string sym)
{
   double pv = CalculatePipValue(sym, 1.0, AccountInfoString(ACCOUNT_CURRENCY));
   if(bal <= 0 || pct <= 0 || sl <= 0 || pv <= 0) return 0.0;
   double size = (bal * (pct / 100.0)) / (sl * pv);
   double step = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP);
   double minL = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN);
   double maxL = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX);
   int dp = (int)-MathLog10(step);
   return NormalizeDouble(MathMax(minL, MathMin(maxL, size)), dp);
}

指定された口座残高、リスク割合、ストップロス(pip単位)に基づき、CalculatePositionSizeは最適なロットサイズを返します。まず、CalculatePipValue(sym, 1.0, AccountInfoString(ACCOUNT_CURRENCY))を呼び出して、1ロットあたりのpip価値を取得します。入力やpip価値が0または負の場合は、0を返します。

それ以外の場合は次の式で計算します。

positionSize = (balance × (riskPercent / 100)) ÷ (stopLossPips × pipValue)

次に、銘柄のSYMBOL_VOLUME_STEP、SYMBOL_VOLUME_MIN、SYMBOL_VOLUME_MAXを取得し、計算されたロットサイズをブローカーの制限に従って丸めます。小数点の桁数「dp」は「-MathLog10(step)」から求められ、ブローカーが許可するロット刻みに一致させます(例:0.01、0.1)。

3. CalculateRiskAmount

ユーザーがポジションサイズ(ロット単位:ps)とストップロス(pip単位:sl)を知っている場合、CalculateRiskAmountは口座通貨でリスクにさらされる資金額を計算します。まずCalculatePipValue(sym, ps, …)を呼び出してそのポジションサイズに対するpip価値を取得し、次に「ps × sl × pipValue」を計算します。結果は小数点2桁に正規化されます。入力のいずれかが0または負の場合、関数は0.0を返し、入力が無効であることを示します。

double CalculateRiskAmount(double ps, double sl, string sym)
{
   if(ps <= 0 || sl <= 0) return 0.0;
   double pv = CalculatePipValue(sym, ps, AccountInfoString(ACCOUNT_CURRENCY));
   return NormalizeDouble(ps * sl * pv, 2);
}

4. CalculateProfitLoss

double CalculateProfitLoss(double entry, double exit, double lotSize, string sym)
{
   if(entry <= 0 || exit <= 0 || lotSize <= 0) return 0.0;
   double pipSz = (StringFind(sym, "JPY") >= 0) ? 0.01 : 0.0001;
   double diff  = (exit - entry) / pipSz;
   return NormalizeDouble(diff * CalculatePipValue(sym, lotSize, AccountInfoString(ACCOUNT_CURRENCY)), 2);
}

CalculateProfitLossは、指定されたエントリー価格、エグジット価格、ロットサイズ、銘柄に対して、口座通貨での純損益(P/L)を算出します。まず、獲得または損失pip数を、「(exit − entry) ÷ pipSize」で計算します。ここでpipSizeはJPYペアの場合0.01、それ以外は0.0001です。次に、そのpip差にCalculatePipValue(sym, lotSize, accountCurrency)を掛けて、pipを口座通貨の損益に換算します。最終結果は小数点2桁に丸められます。数値入力のいずれかが無効な場合、メソッドは0.0を返します。

5. CalculateRiskRewardRatio

double CalculateRiskRewardRatio(double tp, double sl)
{
   if(tp <= 0 || sl <= 0) return 0.0;
   return NormalizeDouble(tp / sl, 2);
}

「リスクリワード比」の場合、ユーザーは利確pip (tp)とストップロスpip (sl)を入力するだけです。両方が正の値であれば、関数は比率(tp/sl)を計算し、小数点2桁に丸めて返します。いずれかの入力が0または負の場合、無効なデータとして0.0を返します。

レイアウトヘルパー:個別フィールドの追加(AddField)

AddFieldメソッドは、1つのInputFieldに対してラベル+編集ボックスのペアを作成する役割を担います。引数として、名前、ラベル文字列、デフォルト値を持つInputFieldへの参照と、現在の垂直カーソル位置「y」を受け取ります。メソッド内で「x0 = m_originX + CALC_INDENT_LEFT」を計算し、すべてのラベルが一貫した左マージンから開始するようにします。

bool AddField(const InputField &f, int &y)
{
   int x0 = m_originX + CALC_INDENT_LEFT;

   // Create label
   CLabel *lbl = new CLabel();
   if(!lbl.Create(m_chart_id, m_name + "Lbl_" + f.name, 0,
                  x0, y, 
                  x0 + CALC_LABEL_WIDTH, y + CALC_EDIT_HEIGHT))
   {
      delete lbl;
      return false;
   }
   lbl.Text(f.label);
   ArrayResize(m_inputs, ArraySize(m_inputs) + 1);
   m_inputs[ArraySize(m_inputs) - 1] = lbl;

   // Create edit
   CEdit *edt = new CEdit();
   if(!edt.Create(m_chart_id, m_name + "Inp_" + f.name, 0,
                  x0 + CALC_EDIT_OFFSET + RESULT_BUTTON_GAP, y,
                  x0 + CALC_EDIT_OFFSET + RESULT_BUTTON_GAP + CALC_EDIT_WIDTH,
                  y + CALC_EDIT_HEIGHT))
   {
      delete edt;
      return false;
   }
   if(f.name == "symbol")
      edt.Text(_Symbol);
   else if(f.name == "accountCurrency")
      edt.Text(AccountInfoString(ACCOUNT_CURRENCY));
   else
      edt.Text(StringFormat("%.2f", f.defaultValue));

   ArrayResize(m_inputs, ArraySize(m_inputs) + 1);
   m_inputs[ArraySize(m_inputs) - 1] = edt;

   y += CALC_EDIT_HEIGHT + CALC_CONTROLS_GAP_Y;
   return true;
}

AddFieldは、名前、ラベル、デフォルト値を含むInputFieldへの参照と、現在の垂直位置yを受け取ります。まず、ラベルの左端位置を決めるために「x0 = m_originX + CALC_INDENT_LEFT」を計算します。次に、「m_name + "Lbl_" + f.name」という名前の新しいCLabelを(x0, y)に固定幅と高さで作成し、テキストをf.labelに設定してm_inputs[]に追加します。

続いて、すべての編集ボックスが整列するように(x0 + CALC_EDIT_OFFSET + RESULT_BUTTON_GAP, y)にCEditを作成します。f.nameがsymbolの場合は「_Symbol」を事前入力し、accountCurrencyの場合は口座通貨を事前入力、それ以外の場合はf.defaultValueを小数点2桁にフォーマットして設定します。新しい編集コントロールもm_inputs[]に追加されます。最後に、yを「コントロールの高さ+CALC_CONTROLS_GAP_Y」だけ増加させ、次のフィールドの位置を準備します。各「ラベル+編集ボックス」をm_inputs[]に格納することで、AddFieldは後でダイアログに追加され、適切に管理されることを保証します。

指定計算項目用のすべての入力フィールド作成(CreateInputFields)

ユーザーが新しい計算項目を選択した場合(または初期作成時)、CreateInputFieldsは以前に生成されたコントロールをすべてクリアします(ArrayFree(m_inputs))。次にyをドロップダウンの直下に設定します。選択された計算項目を判定し、「ポジションサイズ」(4入力)、「リスク金額」(3入力)、「pip価値」(3入力)、「Profit/Loss」(4入力)、「リスクリワード比」(2入力)に応じて、該当するInputField配列の各要素に対してAddField(...)を呼び出します。いずれかのAddFieldが失敗した場合、メソッドはfalseを返してレイアウト処理を中止します。すべてのフィールドが正常に追加された場合はtrueを返します。この仕組みにより、実行時には選択された計算項目に関連するラベル+編集ボックスのみが画面に表示され、間隔を揃えてきれいに積み重なるようになります。

bool CreateInputFields(const string term)
{
   ArrayFree(m_inputs);
   int y = m_originY + CALC_INDENT_TOP + CALC_EDIT_HEIGHT + CALC_CONTROLS_GAP_Y;

   if(term == "Position Size")
      for(int i = 0; i < 4; i++)
         if(!AddField(m_positionSizeInputs[i], y)) return false;
   else if(term == "Risk Amount")
      for(int i = 0; i < 3; i++)
         if(!AddField(m_riskAmountInputs[i], y)) return false;
   else if(term == "Pip Value")
      for(int i = 0; i < 3; i++)
         if(!AddField(m_pipValueInputs[i], y)) return false;
   else if(term == "Profit/Loss")
      for(int i = 0; i < 4; i++)
         if(!AddField(m_profitLossInputs[i], y)) return false;
   else if(term == "Risk-to-Reward")
      for(int i = 0; i < 2; i++)
         if(!AddField(m_riskRewardInputs[i], y)) return false;
   else
      return false;

   return true;
}

パネル構築(Create)

Createが呼び出されると、計算ツールのUIが親ダイアログ内に生成されます。まず、チャートID、名前の接頭辞、および原点座標(x, y)が格納されます。次に以下のコントロールが作成されます。

  • オプションラベル

静的ラベルm_calcOptionLabelが(x, y)に作成され、テキストは「Calculation Option:」に設定されます。これはドロップダウンの上に表示されます。

  • ドロップダウン

CComboBox (m_dropdown)は、「Calculation Option」ラベルの右側(comboX + 70, y)に作成されます。5つの計算項目で初期化され、m_dropdown.Select(0)により「ポジションサイズ」がデフォルトとして選択されます。

  • [Calculate]ボタン

CButton (m_calculateButton)はパネル下部付近に配置されます(btnXとbtnYの計算を使用)。ラベルは「Calculate」、背景はスチールブルー、文字は白色でスタイル設定されます。クリックされるとUpdateResultをトリガーします。

bool Create(const long chart, const string &name, const int subwin,
            const int x, const int y, const int w, const int h)
{
   m_chart_id = chart;
   m_name     = name + "_Calc_";
   m_originX  = x;
   m_originY  = y;

   // 1) “Calculation Option:” label
   if(!m_calcOptionLabel.Create(chart, m_name + "CalcOptLbl", subwin,
                                x, y, x + CALC_LABEL_WIDTH, y + CALC_EDIT_HEIGHT))
      return false;
   m_calcOptionLabel.Text("Calculation Option:");

   // 2) Dropdown immediately to the right
   int comboX = x + CALC_LABEL_WIDTH + DROPDOWN_LABEL_GAP;
   if(!m_dropdown.Create(chart, m_name + "Dropdown", subwin,
                        comboX, y, comboX + (w - CALC_LABEL_WIDTH - DROPDOWN_LABEL_GAP), y + CALC_EDIT_HEIGHT))
      return false;
   m_dropdown.AddItem("Position Size");
   m_dropdown.AddItem("Risk Amount");
   m_dropdown.AddItem("Pip Value");
   m_dropdown.AddItem("Profit/Loss");
   m_dropdown.AddItem("Risk-to-Reward");
   m_dropdown.Select(0);

   // 3) “Calculate” button near the bottom of this panel area
   int btnX = x + w - CALC_BUTTON_WIDTH - 120;
   int btnY = y + h - CALC_BUTTON_HEIGHT + 30;
   if(!m_calculateButton.Create(chart, m_name + "CalcBtn", subwin,
                                btnX, btnY, btnX + CALC_BUTTON_WIDTH, btnY + CALC_BUTTON_HEIGHT))
      return false;
   m_calculateButton.Text("Calculate");
   m_calculateButton.ColorBackground(clrSteelBlue);
   m_calculateButton.Color(clrWhite);

   // 4) Result label and read-only field to the right of the button
   int blockX = btnX + CALC_BUTTON_WIDTH + RESULT_BUTTON_GAP;
   int lblY = btnY - 20;
   if(!m_resultLabel.Create(chart, m_name + "ResultLbl", subwin,
                            blockX, lblY, blockX + CALC_LABEL_WIDTH, lblY + CALC_EDIT_HEIGHT))
      return false;
   m_resultLabel.Text("Result:");

   int fldY = lblY + CALC_EDIT_HEIGHT + RESULT_VERTICAL_GAP;
   if(!m_resultField.Create(chart, m_name + "ResultFld", subwin,
                            blockX, fldY, blockX + CALC_EDIT_WIDTH, fldY + CALC_EDIT_HEIGHT))
      return false;
   m_resultField.ReadOnly(true);

   // 5) Populate dynamic defaults and input rows
   SetDynamicDefaults();
   string initialTerm = m_dropdown.Select();
   CreateInputFields(initialTerm);
   UpdateResult(initialTerm);

   return true;
}

結果ラベルとフィールド

ボタンの右側に「Result:」ラベルが作成され、その直下に読み取り専用の編集フィールド「m_resultField」が配置されます。このフィールドには、実行された計算の数値結果が表示されます。

動的行

SetDynamicDefaults()が口座残高のデフォルトを更新します。次に現在選択されている項目(m_dropdown.Select())を取得し、CreateInputFields(term)を呼び出して対応するラベル+編集ペアを生成します。最後に、UpdateResult(term)が初期計算の結果を結果フィールドに表示します。

ドロップダウン、[Calculate]ボタン、結果表示エリアが先にレイアウトされるため、その後の動的行はこれらの間に配置され、一貫したオフセットに基づいて整列します。いずれかの作成呼び出しが失敗した場合、Createはfalseを返し、呼び出し元に計算ツールの初期化が完了しなかったことを知らせます。完了しなかったことを認識できます。

親ダイアログへのコントロールの追加(AddToDialog)

Create(...)内で全てのコントロールが正常に作成された後、親EAまたはパネルがAddToDialogを呼び出します。このメソッドは、静的コントロール(m_calcOptionLabel、m_dropdown、m_calculateButton、m_resultLabel、m_resultField)をダイアログの内部コントロールリストに追加します。続いて、動的配列「m_inputs[]」(各ラベル+編集ペアを格納)をループ処理し、それらも追加します。いずれかのAdd(...)呼び出しが失敗した場合、このメソッドはfalseを返し、呼び出し元に計算ツールが完全に統合されなかったことを知らせます。

bool AddToDialog(CAppDialog &dlg)
{
   if(!dlg.Add(&m_calcOptionLabel)) return false;
   if(!dlg.Add(&m_dropdown))        return false;
   if(!dlg.Add(&m_calculateButton)) return false;
   if(!dlg.Add(&m_resultLabel))     return false;
   if(!dlg.Add(&m_resultField))     return false;

   for(int i = 0; i < ArraySize(m_inputs); i++)
      if(!dlg.Add(m_inputs[i])) return false;

   return true;
}

結果表示の更新(UpdateResult)

void UpdateResult(const string term)
{
   double res = 0.0;
   string txt = "Result: ";

   if(term == "Position Size")
   {
      double bal = GetInputValue("accountBalance");
      double pct = GetInputValue("riskPercent");
      double sl  = GetInputValue("stopLossPips");
      string sym = GetInputString("symbol");
      if(bal > 0 && pct > 0 && sl > 0 && SymbolSelect(sym, true))
      {
         res = CalculatePositionSize(bal, pct, sl, sym);
         txt += "Position Size (lots)";
      }
      else txt += "Invalid Input";
   }
   else if(term == "Risk Amount")
   {
      double ps  = GetInputValue("positionSize");
      double slp = GetInputValue("stopLossPips");
      string sym = GetInputString("symbol");
      if(ps > 0 && slp > 0 && SymbolSelect(sym, true))
      {
         res = CalculateRiskAmount(ps, slp, sym);
         txt += "Risk Amount (" + AccountInfoString(ACCOUNT_CURRENCY) + ")";
      }
      else txt += "Invalid Input";
   }
   else if(term == "Pip Value")
   {
      double ls  = GetInputValue("lotSize");
      string sym = GetInputString("symbol");
      string cur = GetInputString("accountCurrency");
      if(ls > 0 && SymbolSelect(sym, true))
      {
         res = CalculatePipValue(sym, ls, cur);
         txt += "Pip Value (" + cur + ")";
      }
      else txt += "Invalid Input";
   }
   else if(term == "Profit/Loss")
   {
      double e   = GetInputValue("entryPrice");
      double x   = GetInputValue("exitPrice");
      double ls  = GetInputValue("lotSize");
      string sym = GetInputString("symbol");
      if(e > 0 && x > 0 && ls > 0 && SymbolSelect(sym, true))
      {
         res = CalculateProfitLoss(e, x, ls, sym);
         txt += "Profit/Loss (" + AccountInfoString(ACCOUNT_CURRENCY) + ")";
      }
      else txt += "Invalid Input";
   }
   else if(term == "Risk-to-Reward")
   {
      double tp  = GetInputValue("takeProfitPips");
      double slp = GetInputValue("stopLossPips");
      if(tp > 0 && slp > 0)
      {
         res = CalculateRiskRewardRatio(tp, slp);
         txt += "Risk-to-Reward Ratio";
      }
      else txt += "Invalid Input";
   }

   m_resultField.Text(StringFormat("%.2f", res));
   m_resultLabel.Text(txt);
}

UpdateResultは現在選択されている計算項目を読み取り、GetInputValueおよびGetInputStringを適切に組み合わせて、必要な入力値をすべて取得します。以下はその例です。

  • Position Size:「accountBalance」「riskPercent」「stopLossPips」「symbol」を取得。入力が有効な場合は、CalculatePositionSize(...)を呼び出し、ラベルに「Position Size (lots)」を追加します。
  • Risk Amount:「positionSize」「stopLossPips」「symbol」を取得。入力が有効な場合はCalculateRiskAmount(...)を呼び出し、ラベルに「Risk Amount (USD)」を追加します。
  • Pip Value:「lotSize」「symbol」「accountCurrency」を取得。その後CalculatePipValue(...)を実行し、ラベルに「Pip Value (USD)」を追加します。
  • Profit/Loss:「entryPrice」「exitPrice」「lotSize」「symbol」を取得。その後CalculateProfitLoss(...)を実行し、ラベルに「Profit/Loss (USD)」を追加します。
  • Risk-to-Reward:「takeProfitPips」「stopLossPips」を取得。その後CalculateRiskRewardRatio(...)を実行し、ラベルに「Risk-to-Reward Ratio」を追加します。

いずれかの入力が無効、または銘柄を選択できない場合、このメソッドは「txt = "Result: Invalid Input"」を設定します。すべての場合において、m_resultField.Textに数値結果を小数点2桁にフォーマットして更新し、m_resultLabel.Text(txt)を呼び出してその上の説明テキストを調整します。これにより[Calculate]をクリックするかドロップダウンを変更するたびに、ラベルと数値フィールドが必ず最新の計算結果またはエラーメッセージで更新されます。

ユーザー入力の読み取り(GetInputValueおよびGetInputString)

double GetInputValue(const string name)
{
   for(int i = 0; i < ArraySize(m_inputs); i++)
      if(m_inputs[i].Name() == m_name + "Inp_" + name)
         return StringToDouble(((CEdit*)m_inputs[i]).Text());
   return 0.0;
}

string GetInputString(const string &name)
{
   for(int i = 0; i < ArraySize(m_inputs); i++)
      if(m_inputs[i].Name() == m_name + "Inp_" + name)
         return ((CEdit*)m_inputs[i]).Text();
   return "";
}

これらのヘルパーメソッドは、動的配列「m_inputs[]」の中から正しい編集コントロールを探す処理を抽象化しています。たとえば「stopLossPips」というフィールド名が与えられた場合、GetInputValueはすべてのm_inputs[i]をループし、そのName()が「m_name + "Inp_stopLossPips"」に一致するか確認します。一致した場合、そのText()の数値値を返します。同様に、GetInputStringは「symbol」や「accountCurrency」といったフィールド名が与えられると、そのテキスト値(例:「EURUSD」)をそのまま返します。一致するものが見つからない場合は、それぞれ0.0または空文字列を返し、入力が不足していることを示します。

ユーザーアクションのルーティング(OnEvent)

bool OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   if(id == CHARTEVENT_OBJECT_CHANGE && sparam == m_name + "Dropdown")
   {
      long idx = m_dropdown.Value();
      string term = GetSelectedTerm();
      CreateInputFields(term);
      UpdateResult(term);
      return true;
   }
   if(id == CHARTEVENT_OBJECT_CLICK && sparam == m_name + "CalcBtn")
   {
      string term = GetSelectedTerm();
      UpdateResult(term);
      return true;
   }
   return false;
}

計算ツールは次の2つのイベントタイプを処理します。

1. ドロップダウンの変更(CHARTEVENT_OBJECT_CHANGE)

  • sparamがドロップダウンのコントロール名と一致した場合、新しい項目をm_dropdown.Select()で取得します。
  • CreateInputFields(term)を呼び出し、その項目に対応するラベル+編集ペアをすべて再構築します。
  • 直ちにプレビューを表示するため、UpdateResult(term)を呼び出してデフォルトまたは既存の入力値で再計算します。
  • trueを返すことで、このイベントが消費されたことを親ダイアログに通知します。

2. [Calculate]ボタンのクリック(CHARTEVENT_OBJECT_CLICK)

  • sparamが「m_name + "CalcBtn"」に一致した場合、再度選択中の項目を読み取り、UpdateResult(term)を呼び出します。
  • これにより、ユーザーが入力値(例:ストップロスpip数)を変更し、[Calculate]を押すと結果が更新されます。

その他のイベントはfalseを返し、親のCAppDialog(または他のコード)が必要に応じて処理できるようにします。この明確な分離により、関連する操作(項目変更やボタンクリック)のみが再計算やUI更新を引き起こします。

クリーンアップ(~CForexCalculator)

~CForexCalculator()
{
   for(int i = 0; i < ArraySize(m_inputs); i++)
      delete m_inputs[i];
}

計算ツールオブジェクトが破棄されるとき、デストラクタはm_inputs[]をループし、それぞれの動的に割り当てられたコントロール(ラベルと編集ボックス)を削除します。これによりメモリリークを防ぎます。ユーザーが計算項目を切り替えるたびに、CreateInputFieldsがArrayFreeを使って古いコントロールを削除しますが、それらは後でdeleteされる必要があります。デストラクタの最終的なクリーンアップにより、計算ツールパネル全体が閉じられたりEAが終了した場合でも、このクラスが生成したすべてのコントロールが正しく解放されます。


(3) FX取引計算ツールを取引管理パネルに統合

CForexCalculatorをCTradeManagementPanelに統合するには、パネルのメンバーフィールドの1つとして計算ツールクラスのインスタンスを宣言することから始まります。m_calculatorをprotectedメンバーとして配置することで、パネル内に計算ツールの内部状態(ドロップダウン、ラベル、編集ボックス、ボタン)のための領域を確保します。

CForexCalculator m_calculator;

すでにパネルヘッダにはForexValuesCalculator.mqhがインクルードされているため、コンパイラはCForexCalculatorクラスの構造と依存関係を理解しています。実際には計算ツールのコントロールをコピー&ペーストしたりコードを並べ替えるのではなく、合成を利用します。パネルはm_calculatorを他のコントロールと同様に扱い、生成、サイズ指定、ダイアログへの追加、イベントの転送をおこなうことができますが、内部メンバーに直接触れる必要はありません。

#include <ForexValuesCalculator.mqh>

Create()メソッドでの作成とレイアウト

次のステップはパネルのCreate()メソッド内でおこなわれ、4つのセクションを順に構築します。「Quick Order Execution」をレイアウトし、最初の区切り線を描画した後、Forex Calculatorセクションに移ります。まずセクション見出しラベルを描画します。

if(!CreateLabelEx(m_secCalcLabel, curX, curY, DEFAULT_LABEL_HEIGHT, 

                  "SecCalc", "Forex Values Calculator:", clrNavy))

   return(false)

m_secCalcLabel.Font("Arial Bold");

m_secCalcLabel.FontSize(10);

curY += DEFAULT_LABEL_HEIGHT + GAP;

続いて、計算ツール自体のCreateメソッドを呼び出し、現在のチャート、ユニークな接頭辞(例:name + "_ForexCalc")、サブウィンドウインデックス、座標(x, y)、およびCALCULATOR_WIDTHとCALCULATOR_HEIGHTを渡します。

string calcName = name + "_ForexCalc";

if(!m_calculator.Create(chart, calcName, subwin, 

                        curX, curY, CALCULATOR_WIDTH, CALCULATOR_HEIGHT))

   return(false);

if(!m_calculator.AddToDialog(this))

   return(false);

curY += CALCULATOR_HEIGHT + GAP * 2;

内部的にCForexCalculator::Createは同じ座標系を用いて、ドロップダウンを計算ツールブロックの左上に配置し、さらに入力フィールド用の動的スペースを確保します。固定の高さを指定することで、計算ツールクラスは結果ラベルと結果フィールドを下部に正しく配置できます。m_calculator.Create()がtrueを返すとすぐにm_calculator.AddToDialog(this)を呼び出し、ドロップダウン、動的に構築されたすべてのCLabel/CEditペア、[Calculate]ボタン、結果表示を親CAppDialogに登録します。この登録が重要で、これによりダイアログのイベントループが計算ツールのコントロールを含み、正しいZオーダーで描画できるようになります。

サイズ調整、位置、間隔

セクション間の適切な間隔を維持することは、表示の重なりを避けるために不可欠です。計算ツールを追加した後、curYを「CALCULATOR_HEIGHT + GAP * 2」だけ進めることで、次の区切り線やPending Ordersセクションが計算ツールブロックの直下から始まり、コントロールの境界が曖昧になりません。このレイアウト過程では、他のコントロールを相対的に手動で再配置する必要はなく、見出しラベルを描画 → 計算ツールを既知の原点に生成 → 縦方向カーソルを進める、という流れで「Calculator」領域が自己完結的に構築されます。

また、CALCULATOR_WIDTHとCALCULATOR_HEIGHTという定数を明確に定義したことで、パネル側は計算ツールが何行の入力フィールドを表示するかを知る必要がありません。計算ツール内部は動的にm_inputs[]を調整しますが、全体の確保ブロックのサイズは変えません。したがって将来、新しい入力行(例:「Swap Rate」フィールド)を追加した場合でも、計算ツールはその固定高さの中で結果フィールドを押し下げるだけであり、パネル側は何も変更せずに済みます。

イベント転送と優先順位付け

イベント管理も同様に重要です。ユーザーが計算ツールのコントロールを操作した場合(例:ドロップダウンから新しい項目を選ぶ、または[Calculate]ボタンを押す)、そのイベントはCTradeManagementPanel::OnEvent(...)に届きます。その冒頭で、すべてのイベントを以下のように転送します。

if(m_calculator.OnEvent(id, lparam, dparam, sparam))

{
   Print("Calculator handled event: ", sparam);

   return(true);
}

計算ツールがイベントを認識した場合(例:sparamが「MyPanel_ForexCalcDropdown」や「MyPanel_ForexCalcCalcBtn」と一致する場合)、処理後にtrueを返し、その時点で即座に終了します。この早期戻り方式により、入力フィールドの再構築や結果ラベルの更新といった計算ツールのロジックが常に優先されます。

m_calculator.OnEvent(...)がfalseを返した場合のみ、他のパネル固有のイベント(Quick Order ExecutionセクションやPending Ordersセクションでのボタンクリックなど)の処理を継続します。これにより、計算ツールは事実上自分専用のサブダイアログを持つように機能し、動的コントロールの追加と削除、ユーザー入力への応答、表示の更新をパネルの他コントロールに干渉せず独立しておこなうことができます。


(4) New Admin Panel EAを新しい更新に対応させるための調整

EAの初期化処理やパネル表示処理でg_tradePanel.Run()を呼び出すことは、インタラクティブなGUI要素(特にComboBoxやDatePicker)を正しく動作させるために不可欠です。内部的にRun()は基底クラスCAppDialogのイベント処理ループに制御を渡し、ダイアログの子コントロールに向けられたマウスクリック、キーボード入力、その他のチャートイベントを積極的に監視します。Run()を呼び出さない場合、CTradeManagementPanelのインスタンスは単にメモリ上に存在するだけで、MQL5ランタイムに「アクティブなダイアログ」として登録されません。実際には、「Pending Order Type」ComboBoxで項目を選んでも、あるいはCDatePickerで有効期限を変更しても、必要なCHARTEVENT_OBJECT_CHANGEやCHARTEVENT_OBJECT_ENDEDITイベントが発生せず、パネル側で処理することができなくなります。

一方、g_tradePanel.Run()を呼び出すと、ダイアログは独自のメッセージループに入ります。その結果、ドロップダウンやDatePickerでの操作がすべてOnEvent(...)メソッドに届き、そこでOnChangePendingOrderType()やOnChangePendingDatePicker()といった適切なハンドラにルーティングされます。要するに、Run()こそが静的なコントロール群を、レスポンシブで対話可能なユーザーインターフェースへと変える仕組みです。これがないと、ComboBoxは初期値で固定されたままになり、DatePickerは未決注文の価格ロジックまたはカレンダー表示を更新するイベントを発生させません。

void HandleTradeManagement()
{
    if(g_tradePanel)
    {
        if(g_tradePanel.IsVisible())
            g_tradePanel.Hide();
        else
            g_tradePanel.Show();
        ChartRedraw();
        return;
    }
    g_tradePanel = new CTradeManagementPanel();
    if(!g_tradePanel.Create(g_chart_id, "TradeManagementPanel", g_subwin, 310, 20, 875, 700))
    {
        delete g_tradePanel;
        g_tradePanel = NULL;
        return;
    }
    // ← This line activates the dialog’s own message loop
    g_tradePanel.Run();
    g_tradePanel.Show();

    ChartRedraw();
}

ChartRedraw()の使用法

ユーザー体験にとって同様に重要なのは、ダイアログの表示や非表示、またはビジュアル要素の更新直後にChartRedraw()を適切に呼び出すことです。Show()やHide()をダイアログや個々のコントロール(ComboBox、DatePicker、計算ツールフィールドなど)に対して呼び出すとき、基盤となるチャートキャンバスを再描画しなければ、新しいコントロールが画面に表示されたり古いコントロールが消えたりすることはありません。今回のEAコードでは、HandleTradeManagement()、ToggleInterface()、さらにOnEvent(...)内のイベント処理後など、随所でChartRedraw()を呼び出しています。

各ChartRedraw()はMetaTrader 5に対してチャートオブジェクトとGUIコントロール全体を再描画するよう強制し、ドロップダウンリストが実際に展開されたり、DatePickerのカレンダーが正しく重なって表示されたり、計算ツールのフィールドに新しく計算された値が遅延やちらつきなく可視化されるようにします。ChartRedraw()を呼び出さなければ、状態変更後にチャートが一瞬「古いまま」残り、反応が鈍い動作になります。たとえばユーザーがドロップダウンをクリックしても古い選択肢が表示されたままで、次のティックや自動リフレッシュが発生するまで新しい選択が反映されない、といった事態が起こり得ます。このため、表示の切り替え、ラベル更新、計算結果の再描画といった重要な変更の直後に明示的にリフレッシュを要求することで、ComboBoxの選択が即座に反映され、DatePickerのカレンダーが遅延なく表示され、計算ツールの出力が瞬時に更新される「滑らかでリアルタイムなUI」を保証します。

// Toggling the main interface buttons
void ToggleInterface()
{
    bool state = ObjectGetInteger(0, toggleButtonName, OBJPROP_STATE);
    ObjectSetInteger(0, toggleButtonName, OBJPROP_STATE, !state);
    UpdateButtonVisibility(!state);
    // Redraw immediately so button positions update on screen
    ChartRedraw();
}

// In the OnEvent handler, after forwarding to sub‐panels:
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
    if(id == CHARTEVENT_OBJECT_CLICK)
    {
        // ... handle panel toggles ...
        ChartRedraw();  // Ensure any Show()/Hide() calls are rendered

        // Forward to communication panel
        if(g_commPanel && g_commPanel.IsVisible())
            g_commPanel.OnEvent(id, lparam, dparam, sparam);
        ChartRedraw();  // Redraw after commPanel’s changes

        // Forward to trade panel
        if(g_tradePanel && g_tradePanel.IsVisible())
            g_tradePanel.OnEvent(id, lparam, dparam, sparam);
        ChartRedraw();  // Redraw after tradePanel’s updates (e.g., combobox or date change)

        // Forward to analytics panel
        if(g_analyticsPanel && g_analyticsPanel.IsVisible())
            g_analyticsPanel.OnEvent(id, lparam, dparam, sparam);
        ChartRedraw();  // Final redraw to reflect any analytics updates
    }
}

  • 可視性変更後の再描画:HandleTradeManagement()内では、Show()やHide()の直後にChartRedraw()を呼び出しています。これにより、パネルが表示・非表示に切り替わったときに、外部のチャート更新を待たず即座に画面へ反映されます。
  • イベント委譲後の再描画:OnChartEvent(...)の内部では、イベントをg_tradePanel.OnEvent(...)へ転送した後に再度ChartRedraw()を呼び出しています。これにより、ユーザーが計算ツールのComboBoxを操作して「リスク金額」などの項目を選択した場合、計算ツールは入力フィールドや結果ラベルを再生成します。その後のChartRedraw()によって、新しく生成された入力ボックスやラベルが即座に画面に表示されるようになります。
  • 滑らかで即時のフィードバック:インターフェースボタンの切り替え後、パネルの表示/非表示操作後、サブパネルへのイベント転送後といった節目ごとにChartRedraw()を配置することで、流れるようなレスポンスを保証しています。ComboBoxのドロップダウンは即座に展開し、DatePickerのポップアップも遅延なく描画され、計算ツールの数値結果も即時に表示されます。

次のセクションでは、新機能のテストに進むみます。


テスト

コンパイルが成功した後、以下がMetaTrader 5で実行されました。更新されたTradeManagementPanelには、強化された未決注文設定ワークフローと、重要な外国為替指標を計算し、より情報に基づいた取引決定をサポートする組み込みのFX取引計算ツールが含まれています。

ForexValuesCalculatorのテスト

TradeManagementPanelに統合されたFX取引計算ツールのテスト


結論

今回の議論は非常に詳細になりましたが、主要な目標を達成できたことを嬉しく思います。ポジションサイズ、pip価値、リスクリワード比など、トレーダーが理解すべき主要なFX概念を取り上げ、それらの背後にある数学的理論も整理しました。これらの計算式をMQL5コードへ落とし込むことで、トレーダーが理論を実務に活かせるだけでなく、開発者が自プロジェクトで正確に計算を実装する際の助けにもなります。

TradeManagementPanelの作業で得られた重要な知見の一つは、MQL5標準ライブラリのウィジェット、特にCComboBoxとCDatePickerを活用することです。これらのコントロールにより、関連入力のレイアウトと操作性が向上し、未決注文の有効期限設定も簡素化されます。手動で日付を入力する手間が省けるだけでなく、ユーザー入力の誤りも減らすことができます。

また、モジュール設計に注力し、計算ツール、未決注文コントロール、クイック実行ボタンを独立したクラスとして分離しつつ、各コンポーネントが整然と連携する構造を構築しました。EA内でCComboBoxやCDatePickerのイベントが正しく動作することを確認できたことは、堅牢で再利用可能な設計パターンの実現を示しています。作成した各コンポーネントは、ほとんど修正を要さず今後のプロジェクトへ流用できます。

ただし、フロントエンドのUIは概ね整ったものの、値計算ロジックのさらなる精緻化や最適化の余地は残っています。コメント欄でのご意見や改善案をぜひお寄せください。皆様からのフィードバックは非常に貴重です。本演習が学びの多い機会となったことを願っています。次回の記事もぜひご期待ください。


以下に、このプロジェクトに関係するすべてのファイルがあります。

添付ファイル 詳細
TradeManagementPanel.mqh 主要な取引インターフェースのロジックを含むモジュール。成り行き/未決注文管理、リスク計算、組み込みのFX取引計算ツールを提供します。ドロップダウン、DatePicker、アクションボタンなどのGUIコントロールを備え、CAppDialog派生パネルとして実装されています。取引操作やインタラクティブなユーザー入力の処理で重要な役割を果たします。
ForexValuesCalculator.mqh 取引管理パネルで使用される計算エンジンを実装。pip価値、証拠金、ポジションサイズ、リスクリワード比などの取引パラメータを計算します。 
New_Admin_Panel.mq5 EAのメインエントリーポイント。取引管理、コミュニケーション、分析などのモジュールを統合した統一グラフィカルインターフェースを提供します。パネルの生成、イベントルーティング、チャートオブジェクト作成、全体レイアウト管理を担当。頻繁なChartRedraw()呼び出しによりスムーズなレスポンスを保証し、.Run()でパネル機能を有効化します。
Images.zip インターフェースボタンや視覚要素で使用されるビットマップリソース集。TradeManagementPanelButton.bmp、expand.bmp、collapse.bmpなどを含み、ボタン状態(通常/押下)でのインタラクティブなフィードバックを提供。これらのアセットは、アプリケーションの視覚的アイデンティティと使いやすさに必須です。
Communications.mqh コミュニケーションパネルを定義し、ユーザーがTelegram Botを通じてメッセージ送受信できる機能を提供。Chat IDやBot Token入力用のGUIコンポーネント、メッセージ入力フィールドを含みます。将来的な連絡先管理機能もサポート可能で、CChartCanvas、CBmpButton、CEditコントロールで構築されています。
AnalyticsPanel.mqh チャートベースの分析概要を提供。シグナル評価やパフォーマンス追跡が可能。EAに統合され、g_analyticsPanelを介して表示されます。構造はCAppDialogのモジュール方式に従い、ロジックを分離しつつ機能拡張可能です。
Telegram.mqh Telegram Bot APIと通信するための低レベルネットワーク処理およびJSON形式を扱うモジュール。テキストメッセージ送信用関数を含み、コミュニケーションパネルのバックエンドエンジンとして動作します。
Authentication.mqh 管理者パネル用のオプション二要素認証を実装。Telegramを検証チャネルとして使用し、ログイン確認を指定されたChat IDに送信、ユーザーパスワード入力を検証します。通常はEA初期化時に呼び出され、認証なしアクセスをブロックします。頻繁なテスト・開発サイクル中は繰り返しプロンプトが出るのを避けるため、現在は無効化されています。

すべてのヘッダファイルをMQL5\Includeフォルダに保存し、Images.zipの内容をMQL5\Imagesフォルダに展開してください。その後、New_Admin_Panel.mq5をコンパイルして、MetaTrader 5ターミナルで実行してください。


目次に戻る

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

添付されたファイル |
New_Admin_Panel.mq5 (17.98 KB)
Images.zip (6.17 KB)
Communications.mqh (30.32 KB)
AnalyticsPanel.mqh (32.77 KB)
Telegram.mqh (1.85 KB)
Authentication.mqh (8.92 KB)
最後のコメント | ディスカッションに移動 (10)
Clemence Benjamin
Clemence Benjamin | 21 6月 2025 において 08:58
CapeCoddah #:

また会ったね、

管理者用EAをダウンロードしたことを後悔するかもしれませんが、どうぞ:

EAのユニークなコードをAdmin Common.mqhに分割してコンパイルしたところ、うまくいきました。

しかし...

しかし、1つのパネルを別のパネルの上に移動させると、"力を合わせて "一緒に移動します。これは3つのパネルすべてで起こります。


画面1は、上から下へ順番にボタンを押したプログラムです。 注意:一番下のボタンは何もしません。

画面2は3つのパネルを重ね合わせたもの。 一つを動かそうとすると、全部が動いてしまう。 マウスの動きで、どのチャートを動かすべきかを区別する必要がある。

画面3は、1つの動きで3つすべてを動かしています。

画面4はパネルのxボタン(close)を押すと、プログラムがすべて閉じ、終了して自分自身を削除する。

画面5は、Xクローズボタンで3つ目のパネルを閉じ、再表示ボタンを押しても、背景パネルは再描画されない。


また、includesディレクトリを含むzipファイルを同梱し、!AdminPanelのソースと実行ファイルを入れました。 さらに、私のGoldBugコモンも入れました。99%は使い物にならないと思いますが、長い名前を入力するのが嫌になったので、DTS (Double to String))を入れました。 DTSCのバージョンは完全にはデバッグしていません。 通貨表示のカンマの挿入に使おうと思っていました。


さあ、ウィーティーを食べて、楽しいプログラミングを。


ケープ・コッダハ

CoddaH岬 さん、あなたのフィードバックと労力に感謝します。

このマルチパネル取引ツールのより安定したバージョンに貢献するものです。

私は現在、あなたが強調した問題をレビューしており、あなたが提出した修正についても確認する予定です。改良を進めていきます。

よろしくお願いします、

クレマンス・ベンジャミン

Clemence Benjamin
Clemence Benjamin | 21 6月 2025 において 09:04
Oluwafemi Olabisi #:

こんにちは、

インストールしようとしたのですが、ボタンが表示されず、2つのチェックボックスしか見えません。ファイルをインクルードフォルダに解凍し、画像はimagesフォルダに解凍しました。

こんにちは、@Oluwafemi Olabisi です、

より効果的にサポートするために、スクリーンショットを共有していただけますか?

Oluwafemi Olabisi
Oluwafemi Olabisi | 22 6月 2025 において 18:48
Clemence Benjamin #:

こんにちは、@Oluwafemi Olabisi です、

より効果的にサポートするために、スクリーンショットを共有していただけますか?

ファイルがそれぞれINCLUDEとIMAGESディレクトリに展開された様子を添付します。
CapeCoddah
CapeCoddah | 23 6月 2025 において 12:57

こんにちは、クレマンス、

いくつか質問があるのですが、そのうちのいくつかは解決できるかもしれません。

まず、ストラテジーテスターについて です。

私のEAをこのテスターで動かすと、テキストやパネルボタンなどがテストマシンに表示されません。 あなたのEAのいくつかは表示されていることに気づきました。 この違いの原因について何かご存知ですか? 私はあなたのEAを私のEAに組み込み、違いの原因を特定しようと考えています。

第二に、バグや改善提案をMetaQuotesに送信するには、どのように連絡するのですか? MQL5.comでかなりの時間を費やしましたが、方法が見つかりません。

CapeCoddah
CapeCoddah | 23 6月 2025 において 12:59
Oluwafemi Olabisi #:
EAがナビゲータペインに表示されるようにするには、EAを停止して再起動する必要があります。

EAはincludeフォルダではなく、expertsフォルダにあるべきです。 EAを移動した後、ナビゲータペインにEAを表示させるには、EAを停止し、再起動する必要があります。 これはMQが変更すべきことの1つです。少なくとも、ユーザーがIndicatorsかEXpertsのどちらかのフォルダを折りたたみ、Terminalを停止して再起動し、ターゲットに到達するまでサブディレクトリをすべて開くのではなく、expandコマンドの間にリストをリフレッシュできるようにする必要があります。 さらに良いのは、新しい実行ファイルがサブディレクトリに置かれたときに自動的に 実行することです。

MQL5で自己最適化エキスパートアドバイザーを構築する(第8回):複数戦略分析 MQL5で自己最適化エキスパートアドバイザーを構築する(第8回):複数戦略分析
複数の戦略をどのように組み合わせれば、最も効果的に強力なアンサンブル戦略を構築できるでしょうか。本記事では、3種類の戦略を1つの取引アプリケーションに統合する方法について検討します。トレーダーは通常、ポジションのエントリーとクローズに特化した戦略を用いますが、私たちは機械がこのタスクをより優れた形で遂行できるかどうかを探ります。最初の議論として、ストラテジーテスターの機能と、本タスクで必要となるオブジェクト指向プログラミング(OOP)の原則に慣れていきます。
知っておくべきMQL5ウィザードのテクニック(第69回):SARとRVIのパターンの使用 知っておくべきMQL5ウィザードのテクニック(第69回):SARとRVIのパターンの使用
パラボリックSAR (SAR)と相対活力指数(RVI)は、MQL5のエキスパートアドバイザー(EA)内で併用可能なもう一つのインジケーターペアです。このインジケーターペアは、これまでに取り上げたものと同様に補完的で、SARはトレンドを定義し、RVIはモメンタムを確認します。通常通り、MQL5ウィザードを使用してこのインジケーターペアリングを構築し、その可能性をテストします。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
MQL5での取引戦略の自動化(第19回):Envelopes Trend Bounce Scalping - 取引執行とリスク管理(その2) MQL5での取引戦略の自動化(第19回):Envelopes Trend Bounce Scalping - 取引執行とリスク管理(その2)
この記事では、MQL5でEnvelopes Trend Bounce Scalping戦略の取引実行とリスク管理を実装します。注文の発注、ストップロスやポジションサイズなどのリスク制御をおこないます。最後に、第18回の基盤をもとにバックテストと最適化をおこないます。