
MQL5で取引管理者パネルを作成する(第9回):コード編成(I)
はじめに
コードが長くなると、特に構造が整理されていない場合には理解や保守が難しくなります。その結果、プロジェクトが途中で放棄されてしまうこともあります。現在、複数のパネルが統合された取引管理パネルはかなり大規模な構成となっていますが、それを理由にプロジェクトを終了させる必要はまったくありません。むしろ、今こそ継続的に運用するための戦略が必要です。
このディスカッションでは、コード構成がMQL5におけるアルゴリズム開発にどのように役立つかを検討します。MQL5や他の大規模なプロジェクトが成功している背景には、構造化された設計によって、複雑なコードベースを効率的に管理・保守できている点があります。
コードにドキュメントや適切な構造が欠けていると、保守が困難となり、将来的な修正や機能追加にも支障が出てしまいます。
このディスカッションでは、取引管理パネルを長期的にスケーラブルに維持するための構成方法に焦点を当て、実践的な解決策を紹介します。コードの構成は単なる整理整頓ではなく、効率的で保守性の高いプログラムを書くために非常に重要な要素です。ベストプラクティスを取り入れることで、MQL5のプロジェクトを堅牢で共有可能、かつ拡張性のあるものにすることができ、個人開発者でも複雑なアプリケーションを安定して構築・維持できるようになります。
それでは、議論のポイントを以下に整理します。
議論の概要
前回のディスカッションでは、管理パネルにより専門的なパネルを導入することで、プログラムが大幅に拡張された様子を確認しました。これにより、管理ホームパネル、通信パネル、取引管理パネル、分析パネルの4つのパネルが揃い、管理パネルはすべてのトレーダーにとって不可欠なダッシュボードへと進化しました。コード全体もかなり大きくなり、全体構造が明確になってきましたが、それぞれの機能をさらに強化するためには、まだ多くの作業が残っています。
次のステップとしてさらに機能を追加しようと考えたとき、コード全体を見直して整理することの重要性に気づきました。そこで今回のトピックが生まれました。完成したプログラムをただ紹介するのではなく、コードを整理・改善していく過程を皆さんと一緒に見ていくことが有意義だと感じたのです。次のセクションでは、私の調査に基づいて、コードの整理・構成に関する知見を共有します。
この議論を通じて、次のような問いに答えられるだけの知識を得ることができるはずです。
- 大規模なプログラムを開発するにはどうすればよいか
- 自分の大規模なプログラムを、他の人に理解してもらうにはどうすればよいか
コード構成の理解
さまざまな情報源によると、「コードの構成」とは、コードを読みやすく、保守しやすく、拡張しやすいように整理・構造化する手法を指します。整理されたコードは、開発者がコードを理解しやすくし、バグの修正や機能追加をスムーズにおこなうための基盤となります。
ソフトウェア エンジニアのZhaojun Zhang氏は、こう語っています。「コードの構成は、家の整理整頓のようなものです。毎日片付けなくても暮らしていけますし、散らかっていても我慢できる限り生活に支障はありません。ただ、本当に困るのは、しばらく使っていなかった物を急に探す時や、誰かを豪華なディナーに招待しようとする時です。」
この例えは、コードの整理が自分の作業効率を上げるだけでなく、他の人がそのコードに関わる際にも大きな助けとなることをうまく表しています。ここでは、可読性・保守性・スケーラビリティという3つの主要な観点から、コード構成の意義を特にMQL5でのアルゴリズム開発の文脈で掘り下げていきます。
1. 可読性
可読性とは、他の人、あるいは未来の自分がそのコードの構造やロジックをどれだけ簡単に理解できるかを示します。特にMQL5では、複数の開発者が関わることもあり、一人で開発していても後で再確認・デバッグする場面が出てくるため、非常に重要です。
主なポイント
- 意味のある命名:変数、関数、クラスには、a、b、tempなどの抽象的な名前ではなく、movingAveragePeriodやsignalStrengthのように、役割が一目で分かる名前を使います。
- コメント:良いコメントは「コードが何をしているか」だけでなく、「なぜそのコードが存在するのか」も説明します。これはアルゴリズムの意図を記録するうえで非常に重要です。
- 一貫したフォーマット:インデントや行間を統一することで、コードが視覚的に整理され、読みやすくなります。たとえば、ループや条件文、関数などでは常に同じインデントスタイルを使います。
- モジュラー化されたコード:コードを、特定の機能(たとえば移動平均の計算や取引条件のチェックなど)を処理する、小さく自己完結した関数やクラスに分けることで、全体の可読性が向上します。
- 素早いデバッグ:読みやすいコードはバグを見つけやすく、修正も容易です。
- 協業のしやすさ:コードが明確で理解しやすいほど、他の開発者と共同で作業したり、問題を一緒に解決したりするのが簡単になります。
- 再訪時の効率化:しばらく時間が空いた後にプロジェクトを見直す際も、読みやすいコードであれば、内容を再理解するのに時間がかかりません。
2. 保守性
保守性とは、コードが時間の経過とともに修正・拡張しやすい状態であることを指します。MQL5のように、戦略が発展していくことの多いアルゴリズム取引では、保守性が長期的な成功を支える重要な要素です。
主なポイント
- モジュール性:異なる処理を関数やクラスで分離することで(例:取引の実行を処理する関数と、インジケーターを計算する関数を分ける)、システムを分割して扱えるようになります。これにより、1つの部分を変更しても他の部分に影響を与えずに済みます。
- 関心の分離:コードの各部分が単一の責任を持つように設計します。たとえば、取引のロジックは、市場の状況を評価するロジックとは分離しておくべきです。
- ライブラリや組み込み関数の使用:すでに用意されているMQL5の標準関数やライブラリを活用することで、複雑さを減らし、エラーも抑えることができます。
- バージョン管理:Gitや MQL5 Storageのようなバージョン管理システムを使えば、変更の履歴を追跡でき、バグや予期しない動作が発生した際に、以前の状態に戻すことができます。
メリット
- 将来的な変更:戦略が進化しても、構造化されたコードベースであれば、新しい機能の追加や修正が効率よくおこなえます。
- バグ修正:バグが発生しても、他の部分に影響を与えずに問題のある箇所だけに集中して対応できます。
- 効率:コードの構造が明確であれば、開発者は理解や作業にかける時間を短縮でき、更新もミスなくおこなえます。
3. スケーラビリティ
スケーラビリティとは、増加する処理量やデータ、機能要求に対応できるコードの能力を意味します。取引戦略が複雑化したり、大量のデータを扱うようになるにつれて、この特性はますます重要になります。
主なポイント
- 効率的なアルゴリズム:履歴データの大量処理、取引の頻度増加、複数銘柄の同時分析などに対応できるよう、スピードとメモリ効率を考慮して設計されたアルゴリズムが求められます。
- データ構造:配列、リスト、マップなどのデータ構造を適切に選ぶことで、大規模なデータを効率よく扱えます。MQL5にはArrayやStructといったデータ構造があり、これらを活用することで戦略の拡張がしやすくなります。
- 並列処理:MQL5はマルチスレッドに対応しており、市場分析と注文実行など複数の処理を同時におこなうことができます。これは特に、複雑な戦略やバックテストで役立ちます。
- 非同期操作:外部APIからデータを取得するなど、他の処理をブロックしないタスクにおいては、非同期的に実行することでシステム全体の応答性を維持できます。
メリット
- 大量データの処理:大規模な市場データや複数銘柄に対しても、パフォーマンスを大きく落とすことなく対応できます。
- 成長への対応:アルゴリズムが将来的に、複数通貨ペアの取引、機械学習モデルの適用、リスク管理機能の強化などの新たな機能を必要とする場合でも、スケーラブルなコードであれば、大幅な設計変更をせずに柔軟に対応できます。
- リアルタイムパフォーマンス:ライブ取引の環境では、スケーラビリティがあることで、アルゴリズムはリアルタイムのデータフィードや注文執行を滞りなく処理できるようになります。
MQL5においては、可読性・保守性・スケーラビリティはしばしば重なり合い、互いに強化し合います。たとえば、読みやすくモジュール化された関数は、調整が必要なときに保守しやすくなります。同様に、スケーラブルなコードはたいていモジュール構成になっているため、可読性と保守性の両方が向上します。取引アルゴリズムの開発においては、この3つのバランスを取ることが重要です。それにより、コードは現在の要件を満たすだけでなく、戦略の進化や市場データの増加に応じたパフォーマンス要求の変化にも柔軟に対応・拡張できるようになります。
たとえば、今回の開発では、第1回でコミュニケーションパネルから始めました。プロジェクトが進むにつれて、異なる専門分野に特化した新しいパネルを、コアのロジックを変更することなくシームレスに統合することができました。これはスケーラビリティの一例であると同時に、既存機能の再利用性をさらに高めるために重要な概念がまだ残されていることも示しています。
管理パネル(EA)への実装
ここでは、前回の記事のコードを参照しながら、コード構成の改善を実際に適用していきます。コードの構造化にはさまざまなアプローチがあり、開発と同時に整理していくスタイルもあれば、後から全体を見直して整えていくスタイルもあります。いずれの場合でも、簡単な見直しをおこなうことで、そのコードが基本的な基準を満たしているかどうかを判断できます。前述のZhaojun Zhang氏の言葉にもあったように、コードをきれいに整理することは必須ではありません。動作さえすれば、多少散らかったコードでも問題ないと感じる開発者もいるでしょう。しかし、こうしたスタイルではプロジェクトの拡大時に大きな問題が生じやすくなります。構造化されていないコードは、保守・拡張・デバッグが難しくなり、長期的な成長を妨げる要因となります。これが、私がコード構成におけるベストプラクティスの実践を強く推奨している理由です。次のセクションでは、その具体的な方法についてさらに掘り下げていきましょう。
コード上の問題点の特定
Admin Panel V1.24のソースコードを見直していく中で、自分自身がより素早く全体を把握し、問題点を見つけやすくするために、構成要素を要約することにしました。一般的に、自分で開発したプログラムであれば、その構成要素は頭に入っています。しかし、問題はそれをどう整理するか、そして読みやすさを保ちつつ、いかに簡潔にできるかという点にあります。そこでまずは、このプログラムの全体像を把握するために重要な9つの主要コンポーネントを以下にまとめました。この概要をもとに、後ほど具体的な改善点についても述べていきます。
1. UI要素とグローバル変数
// Panels CDialog adminHomePanel, tradeManagementPanel, communicationsPanel, analyticsPanel; // Authentication UI CDialog authentication, twoFactorAuth; CEdit passwordInputBox, twoFACodeInput; CButton loginButton, closeAuthButton, twoFALoginButton, close2FAButton; // Trade Management UI (12+ buttons) CButton buyButton, sellButton, closeAllButton, closeProfitButton, ...; // Communications UI CEdit inputBox; CButton sendButton, clearButton, quickMessageButtons[8];
2. 認証システム
- ハードコードされたパスワード(Password = "2024")
- 基本的な2FAワークフロー
- ログイン試行回数のカウンタ(failedAttempts)
- 認証ダイアログ: ShowAuthenticationPrompt()とShowTwoFactorAuthPrompt()
3. 取引管理関数
- ポジションクローズ関数
- 注文削除関数
- 取引執行
4. 通信機能
- SendMessageToTelegram()によるTelegram 統合
- クイックメッセージボタン(定義済みメッセージ8件)
- 文字数カウンタ付きメッセージ入力ボックス
5. 分析パネル
- 円グラフの可視化(CreateAnalyticsPanel())
- 取引履歴分析(GetTradeData())
- カスタムチャートクラス: CCustomPieChartとCAnalyticsChart
6. イベント処理構造
- 以下を含むモノリシックなOnChartEvent()
- ボタンクリックのチェック
- UI/取引/認証ロジックの混合
- ルーティングなしの直接関数呼び出し
7. セキュリティコンポーネント
- プレーンテキストパスワードの格納
- 基本的な2FAの実装
- Telegramの認証情報は暗号化されない
- セッション管理なし
8. 初期化/クリーンアップ
- 順次UI作成を伴うOnInit()
- パネル破壊を伴うOnDeinit()
- リソース管理システムがない
9. エラー処理
- エラー用の基本的なPrint()文
- エラー回復メカニズムがない
- トランザクションのロールバックがない
- 取引操作の限定的な検証
10. 書式設定、インデント、スペース
- この側面はMetaEditorによって十分にカバーされている
- 私たちのコードは、次のセクションで説明する繰り返しコードなどの他の部分を除けば読みやすい
コードを確認した結果、注意が必要な構造上の問題を以下にまとめました。
- モノリシック構造:すべての機能が単一ファイルに含まれている
- 密結合:UI ロジックとビジネスロジックが混在している
- パターンの繰り返し:ボタンやパネルの作成コードが類似している
- セキュリティリスク:認証情報のハードコーディング、暗号化なし
- 拡張性の欠如:モジュールアーキテクチャがない
- 命名規則の不一致(主に UI 要素およびグローバル変数)
コードの再編成
このステップの後でも、プログラムは引き続き動作し、元の機能を維持していなければなりません。これまでの評価に基づいて、コードの構成を改善するためのいくつかの観点について以下に説明します。
1. モノリシック構造
コードが不必要に長くなってしまうため、このような状況は課題となります。これを解決するには、コードをモジュール化されたコンポーネントに分割することが有効です。これは、機能ごとに別々のファイルを作成し、それらを再利用可能にしつつ、メインのコードをシンプルで管理しやすく保つことを意味します。宣言および実装はメインファイルの外に置き、必要に応じてインクルードします。
記事が情報過多にならないよう、詳細な説明は次回に持ち越すことにしますが、ここでは簡単な例を紹介します。たとえば、認証のためのインクルードファイルを作成することができます。以下のコードをご覧ください。
class AuthManager { private: string m_password; int m_failedAttempts; public: AuthManager(string password) : m_password(password), m_failedAttempts(0) {} bool Authenticate(string input) { if(input == m_password) { m_failedAttempts = 0; return true; } m_failedAttempts++; return false; } bool Requires2FA() const { return m_failedAttempts >= 3; } };
このファイルは、以下に示すように、メインの管理パネル コードにインクルードされます。
#include <AuthManager.mqh>
2. 蜜結合
この実装では、ユーザーインターフェースのハンドラと取引ロジックが混在しているという問題に対処します。これを改善するには、インターフェイスを使って両者を分離することが効果的です。そのためには、組み込みのCTradeクラスをベースに、専用のヘッダーファイルやクラスを作成することができます。
より良い構成のために、取引関連のロジックを個別に管理するTradeManagerヘッダーファイルを作成します。これにより、再利用性が高まり、管理も容易になります。カスタムクラスをインクルードし、取引ロジックをユーザーインターフェイスロジックから明確に分離することで、コードの保守性と可読性が向上します。
#include<TradeManager.mqh>
3. コードパターンの冗長性
この問題は、特にパネルやボタンの作成に関するユーザーインターフェイス(UI)コードの重複にあります。このような重複は、UIヘルパー関数を作成することで解消できます。これにより、インターフェイスおよびその要素の構築が簡潔になります。
以下は、ボタン作成のためのヘルパー関数の例です。
//+------------------------------------------------------------------+ //| Generic Button Creation Helper | //+------------------------------------------------------------------+ bool CreateButton(CButton &button, CDialog &panel, const string name, const string text, int x1, int y1, int x2, int y2) { if(!button.Create(ChartID(), name, 0, x1, y1, x2, y2)) { Print("Failed to create button: ", name); return false; } button.Text(text); panel.Add(button); return true; }
残りのボタンもこのアプローチを用いて作成することで、冗長なコードを排除し、より構造的で保守性の高い実装が可能になります。以下は、取引管理パネルにアクセスするためのボタン作成例です。実装の大部分は、結果セクションに記載された最終的な整理済みコードに含まれています。
CreateButton(TradeMgmtAccessButton, AdminHomePanel, "TradeMgmtAccessButton", "Trade Management Panel", 10, 20, 250, 60)
4. セキュリティリスク
簡略化のため、今回もハードコードされたパスワードを使用していますが、この点については第VII部ですでに取り上げています。これらは暗号化された構成を用いることで解決可能です。
5. 一貫性のない命名:
ある時点で、テキストの長さを短縮するために名前の省略形を使用しました。しかし、これは他の人と協力して作業する際に問題を引き起こす可能性があります。この問題に対処する最善の方法は、一貫した命名規則を徹底することです。
例えば、以下のコードスニペットでは、大文字の「T」の代わりに小文字の「t」を使用し、「management」の省略形も使用しています。これにより、他の開発者が作者の意図を理解できず混乱する可能性があります。また、テーマボタンの関数名も冗長で、可読性を高めるためにもっと簡潔にすることができます。以下の例は、これらの問題を示しています。
CButton tradeMgmtAccessButton; // Inconsistent void OnToggleThemeButtonClick(); // Verbose
修正されたコードは次のとおりです。
CButton TradeManagementAccessButton; // PascalCase void HandleThemeToggle(); // Action-oriented
結果とテスト
ここで議論した解決策を慎重に適用した結果、最終的に修正されたコードが以下になります。このコードでは、テーマ関連の機能を削除しました。これは、テーマ専用のヘッダーファイルを別途作成するためです。この変更の目的は、テーマ関連の機能を既存のクラスに拡張して実装する際に生じる問題に対応するためです。
//+------------------------------------------------------------------+ //| Admin Panel.mq5 | //| Copyright 2024, Clemence Benjamin | //| https://www.mql5.com/ja/users/billionaire2024/seller | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, Clemence Benjamin" #property link "https://www.mql5.com/ja/users/billionaire2024/seller" #property description "A secure and responsive, communications, trade management and analytics Panel" #property version "1.25" //Essential header files included #include <Trade\Trade.mqh> #include <Controls\Dialog.mqh> #include <Controls\Button.mqh> #include <Controls\Edit.mqh> #include <Controls\Label.mqh> #include <Canvas\Charts\PieChart.mqh> #include <Canvas\Charts\ChartCanvas.mqh> // Input parameters for quick messages input string QuickMessage1 = "Updates"; input string QuickMessage2 = "Close all"; input string QuickMessage3 = "In deep profits"; input string QuickMessage4 = "Hold position"; input string QuickMessage5 = "Swing Entry"; input string QuickMessage6 = "Scalp Entry"; input string QuickMessage7 = "Book profit"; input string QuickMessage8 = "Invalid Signal"; input string InputChatId = "YOUR_CHAT_ID"; // Telegram chat ID for notifications input string InputBotToken = "YOUR_BOT_TOKEN"; // Telegram bot token // Security Configuration const string TwoFactorAuthChatId = "REPLACE_WITH_YOUR_CHAT_ID"; // 2FA notification channel const string TwoFactorAuthBotToken = "REPLACE_WITH_YOUR_BOT_TOKEN"; // 2FA bot credentials const string DefaultPassword = "2024"; // Default access password // Global UI Components CDialog AdminHomePanel, TradeManagementPanel, CommunicationsPanel, AnalyticsPanel; CButton HomeButtonComm, HomeButtonTrade, SendButton, ClearButton; CButton ChangeFontButton, ToggleThemeButton, LoginButton, CloseAuthButton; CButton TwoFactorAuthLoginButton, CloseTwoFactorAuthButton, MinimizeCommsButton; CButton CloseCommsButton, TradeMgmtAccessButton, CommunicationsPanelAccessButton; CButton AnalyticsPanelAccessButton, ShowAllButton, QuickMessageButtons[8]; CEdit InputBox, PasswordInputBox, TwoFactorAuthCodeInput; CLabel CharCounter, PasswordPromptLabel, FeedbackLabel; CLabel TwoFactorAuthPromptLabel, TwoFactorAuthFeedbackLabel; // Trade Execution Components CButton BuyButton, SellButton, CloseAllButton, CloseProfitButton; CButton CloseLossButton, CloseBuyButton, CloseSellButton; CButton DeleteAllOrdersButton, DeleteLimitOrdersButton; CButton DeleteStopOrdersButton, DeleteStopLimitOrdersButton; // Security State Management int FailedAttempts = 0; // Track consecutive failed login attempts bool IsTrustedUser = false; // Flag for verified users string ActiveTwoFactorAuthCode = ""; // Generated 2FA verification code // Trade Execution Constants const double DefaultLotSize = 1.0; // Standard trade volume const double DefaultSlippage = 3; // Allowed price deviation const double DefaultStopLoss = 0; // Default risk management const double DefaultTakeProfit = 0; // Default profit target //+------------------------------------------------------------------+ //| Program Initialization | //+------------------------------------------------------------------+ int OnInit() { if(!InitializeAuthenticationDialog() || !InitializeAdminHomePanel() || !InitializeTradeManagementPanel() || !InitializeCommunicationsPanel()) { Print("Initialization failed"); return INIT_FAILED; } AdminHomePanel.Hide(); TradeManagementPanel.Hide(); CommunicationsPanel.Hide(); AnalyticsPanel.Hide(); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ //| Trade Management Functions | //+------------------------------------------------------------------+ CTrade TradeExecutor; // Centralized trade execution handler // Executes a market order with predefined parameters // name="orderType">Type of order (ORDER_TYPE_BUY/ORDER_TYPE_SELL) // <returns>True if order execution succeeded</returns> bool ExecuteMarketOrder(int orderType) { double executionPrice = (orderType == ORDER_TYPE_BUY) ? SymbolInfoDouble(Symbol(), SYMBOL_ASK) : SymbolInfoDouble(Symbol(), SYMBOL_BID); if(executionPrice <= 0) { Print("Price retrieval failed. Error: ", GetLastError()); return false; } bool orderResult = (orderType == ORDER_TYPE_BUY) ? TradeExecutor.Buy(DefaultLotSize, Symbol(), executionPrice, DefaultSlippage, DefaultStopLoss, DefaultTakeProfit) : TradeExecutor.Sell(DefaultLotSize, Symbol(), executionPrice, DefaultSlippage, DefaultStopLoss, DefaultTakeProfit); if(orderResult) { Print(orderType == ORDER_TYPE_BUY ? "Buy" : "Sell", " order executed successfully"); } else { Print("Order execution failed. Error: ", GetLastError()); } return orderResult; } // Closes positions based on specified criteria // name="closureCondition" // 0=All, 1=Profitable, -1=Losing, 2=Buy, 3=Sell bool ClosePositions(int closureCondition) { CPositionInfo positionInfo; for(int i = PositionsTotal()-1; i >= 0; i--) { if(positionInfo.SelectByIndex(i) && (closureCondition == 0 || (closureCondition == 1 && positionInfo.Profit() > 0) || (closureCondition == -1 && positionInfo.Profit() < 0) || (closureCondition == 2 && positionInfo.Type() == POSITION_TYPE_BUY) || (closureCondition == 3 && positionInfo.Type() == POSITION_TYPE_SELL))) { TradeExecutor.PositionClose(positionInfo.Ticket()); } } return true; } //+------------------------------------------------------------------+ //| Authentication Management | //+------------------------------------------------------------------+ CDialog AuthenticationDialog, TwoFactorAuthDialog; /// Initializes the primary authentication dialog bool InitializeAuthenticationDialog() { if(!AuthenticationDialog.Create(ChartID(), "Authentication", 0, 100, 100, 500, 300)) return false; // Create dialog components if(!PasswordInputBox.Create(ChartID(), "PasswordInput", 0, 20, 70, 260, 95) || !PasswordPromptLabel.Create(ChartID(), "PasswordPrompt", 0, 20, 20, 260, 40) || !FeedbackLabel.Create(ChartID(), "AuthFeedback", 0, 20, 140, 380, 160) || !LoginButton.Create(ChartID(), "LoginButton", 0, 20, 120, 100, 140) || !CloseAuthButton.Create(ChartID(), "CloseAuthButton", 0, 120, 120, 200, 140)) { Print("Authentication component creation failed"); return false; } // Configure component properties PasswordPromptLabel.Text("Enter Administrator Password:"); FeedbackLabel.Text(""); FeedbackLabel.Color(clrRed); LoginButton.Text("Login"); CloseAuthButton.Text("Cancel"); // Assemble dialog AuthenticationDialog.Add(PasswordInputBox); AuthenticationDialog.Add(PasswordPromptLabel); AuthenticationDialog.Add(FeedbackLabel); AuthenticationDialog.Add(LoginButton); AuthenticationDialog.Add(CloseAuthButton); AuthenticationDialog.Show(); return true; } /// Generates a 6-digit 2FA code and sends via Telegram void HandleTwoFactorAuthentication() { ActiveTwoFactorAuthCode = StringFormat("%06d", MathRand() % 1000000); SendMessageToTelegram("Your verification code: " + ActiveTwoFactorAuthCode, TwoFactorAuthChatId, TwoFactorAuthBotToken); } //+------------------------------------------------------------------+ //| Panel Initialization Functions | //+------------------------------------------------------------------+ bool InitializeAdminHomePanel() { if(!AdminHomePanel.Create(ChartID(), "Admin Home Panel", 0, 30, 80, 335, 350)) return false; return CreateButton(TradeMgmtAccessButton, AdminHomePanel, "TradeMgmtAccessButton", "Trade Management Panel", 10, 20, 250, 60) && CreateButton(CommunicationsPanelAccessButton, AdminHomePanel, "CommunicationsPanelAccessButton", "Communications Panel", 10, 70, 250, 110) && CreateButton(AnalyticsPanelAccessButton, AdminHomePanel, "AnalyticsPanelAccessButton", "Analytics Panel", 10, 120, 250, 160) && CreateButton(ShowAllButton, AdminHomePanel, "ShowAllButton", "Show All 💥", 10, 170, 250, 210); } bool InitializeTradeManagementPanel() { if (!TradeManagementPanel.Create(ChartID(), "Trade Management Panel", 0, 500, 30, 1280, 170)) { Print("Failed to create Trade Management Panel."); return false; } CreateButton(HomeButtonTrade, TradeManagementPanel, "HomeButtonTrade", "Home 🏠", 20, 10, 120, 30) && CreateButton(BuyButton, TradeManagementPanel, "BuyButton", "Buy", 130, 5, 210, 40) && CreateButton(SellButton, TradeManagementPanel, "SellButton", "Sell", 220, 5, 320, 40) && CreateButton(CloseAllButton, TradeManagementPanel, "CloseAllButton", "Close All", 130, 50, 230, 70) && CreateButton(CloseProfitButton, TradeManagementPanel, "CloseProfitButton", "Close Profitable", 240, 50, 380, 70) && CreateButton(CloseLossButton, TradeManagementPanel, "CloseLossButton", "Close Losing", 390, 50, 510, 70) && CreateButton(CloseBuyButton, TradeManagementPanel, "CloseBuyButton", "Close Buys", 520, 50, 620, 70) && CreateButton(CloseSellButton, TradeManagementPanel, "CloseSellButton", "Close Sells", 630, 50, 730, 70) && CreateButton(DeleteAllOrdersButton, TradeManagementPanel, "DeleteAllOrdersButton", "Delete All Orders", 40, 50, 180, 70) && CreateButton(DeleteLimitOrdersButton, TradeManagementPanel, "DeleteLimitOrdersButton", "Delete Limits", 190, 50, 300, 70) && CreateButton(DeleteStopOrdersButton, TradeManagementPanel, "DeleteStopOrdersButton", "Delete Stops", 310, 50, 435, 70) && CreateButton(DeleteStopLimitOrdersButton, TradeManagementPanel, "DeleteStopLimitOrdersButton", "Delete Stop Limits", 440, 50, 580, 70); return true; } //+------------------------------------------------------------------+ //| Two-Factor Authentication Dialog | //+------------------------------------------------------------------+ bool InitializeTwoFactorAuthDialog() { if(!TwoFactorAuthDialog.Create(ChartID(), "Two-Factor Authentication", 0, 100, 100, 500, 300)) return false; if(!TwoFactorAuthCodeInput.Create(ChartID(), "TwoFACodeInput", 0, 20, 70, 260, 95) || !TwoFactorAuthPromptLabel.Create(ChartID(), "TwoFAPromptLabel", 0, 20, 20, 380, 40) || !TwoFactorAuthFeedbackLabel.Create(ChartID(), "TwoFAFeedbackLabel", 0, 20, 140, 380, 160) || !TwoFactorAuthLoginButton.Create(ChartID(), "TwoFALoginButton", 0, 20, 120, 100, 140) || !CloseTwoFactorAuthButton.Create(ChartID(), "Close2FAButton", 0, 120, 120, 200, 140)) { return false; } TwoFactorAuthPromptLabel.Text("Enter verification code sent to Telegram:"); TwoFactorAuthFeedbackLabel.Text(""); TwoFactorAuthFeedbackLabel.Color(clrRed); TwoFactorAuthLoginButton.Text("Verify"); CloseTwoFactorAuthButton.Text("Cancel"); TwoFactorAuthDialog.Add(TwoFactorAuthCodeInput); TwoFactorAuthDialog.Add(TwoFactorAuthPromptLabel); TwoFactorAuthDialog.Add(TwoFactorAuthFeedbackLabel); TwoFactorAuthDialog.Add(TwoFactorAuthLoginButton); TwoFactorAuthDialog.Add(CloseTwoFactorAuthButton); return true; } //+------------------------------------------------------------------+ //| Telegram Integration | //+------------------------------------------------------------------+ bool SendMessageToTelegram(string message, string chatId, string botToken) { string url = "https://api.telegram.org/bot" + botToken + "/sendMessage"; string headers; char postData[], result[]; string requestData = "{\"chat_id\":\"" + chatId + "\",\"text\":\"" + message + "\"}"; StringToCharArray(requestData, postData, 0, StringLen(requestData)); int response = WebRequest("POST", url, headers, 5000, postData, result, headers); if(response == 200) { Print("Telegram notification sent successfully"); return true; } Print("Failed to send Telegram notification. Error: ", GetLastError()); return false; } //+------------------------------------------------------------------+ //| Generic Button Creation Helper | //+------------------------------------------------------------------+ bool CreateButton(CButton &button, CDialog &panel, const string name, const string text, int x1, int y1, int x2, int y2) { if(!button.Create(ChartID(), name, 0, x1, y1, x2, y2)) { Print("Failed to create button: ", name); return false; } button.Text(text); panel.Add(button); return true; } //+------------------------------------------------------------------+ //| Enhanced Event Handling | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id == CHARTEVENT_OBJECT_CLICK || id == CHARTEVENT_OBJECT_ENDEDIT) { if(sparam == "InputBox") { int length = StringLen(InputBox.Text()); CharCounter.Text(IntegerToString(length) + "/4096"); } else if(id == CHARTEVENT_OBJECT_CLICK) { // Authentication event handling if(sparam == "LoginButton") { string enteredPassword = PasswordInputBox.Text(); if(enteredPassword == DefaultPassword) { FailedAttempts = 0; IsTrustedUser = true; AuthenticationDialog.Destroy(); AdminHomePanel.Show(); } else { if(++FailedAttempts >= 3) { HandleTwoFactorAuthentication(); AuthenticationDialog.Destroy(); InitializeTwoFactorAuthDialog(); } else { FeedbackLabel.Text("Invalid credentials. Attempts remaining: " + IntegerToString(3 - FailedAttempts)); PasswordInputBox.Text(""); } } } if(sparam == "AnalyticsPanelAccessButton") { OnAnalyticsButtonClick(); AdminHomePanel.Hide(); if(!InitializeAnalyticsPanel()) { Print("Failed to initialize Analytics Panel"); return; } // Communications Handling if(sparam == "SendButton") { if(SendMessageToTelegram(InputBox.Text(), InputChatId, InputBotToken)) InputBox.Text(""); } else if(sparam == "ClearButton") { InputBox.Text(""); CharCounter.Text("0/4096"); } else if(StringFind(sparam, "QuickMsgBtn") != -1) { int index = (int)StringToInteger(StringSubstr(sparam, 11)) - 1; if(index >= 0 && index < 8) SendMessageToTelegram(QuickMessageButtons[index].Text(), InputChatId, InputBotToken); } // Trade execution handlers else if(sparam == "BuyButton") ExecuteMarketOrder(ORDER_TYPE_BUY); else if(sparam == "SellButton") ExecuteMarketOrder(ORDER_TYPE_SELL); // Panel Navigation if(sparam == "TradeMgmtAccessButton") { TradeManagementPanel.Show(); AdminHomePanel.Hide(); } else if(sparam == "CommunicationsPanelAccessButton") { CommunicationsPanel.Show(); AdminHomePanel.Hide(); } else if(sparam == "AnalyticsPanelAccessButton") { OnAnalyticsButtonClick(); AdminHomePanel.Hide(); } else if(sparam == "ShowAllButton") { TradeManagementPanel.Show(); CommunicationsPanel.Show(); AnalyticsPanel.Show(); AdminHomePanel.Hide(); } else if(sparam == "HomeButtonTrade") { AdminHomePanel.Show(); TradeManagementPanel.Hide(); } else if(sparam == "HomeButtonComm") { AdminHomePanel.Show(); CommunicationsPanel.Hide(); } } } } } //+------------------------------------------------------------------+ //| Communications Management | //+------------------------------------------------------------------+ bool InitializeCommunicationsPanel() { if(!CommunicationsPanel.Create(ChartID(), "Communications Panel", 0, 20, 150, 490, 650)) return false; // Create main components if(!InputBox.Create(ChartID(), "InputBox", 0, 5, 25, 460, 95) || !CharCounter.Create(ChartID(), "CharCounter", 0, 380, 5, 460, 25)) return false; // Create control buttons with corrected variable names const bool buttonsCreated = CreateButton(SendButton, CommunicationsPanel, "SendButton", "Send", 350, 95, 460, 125) && CreateButton(ClearButton, CommunicationsPanel, "ClearButton", "Clear", 235, 95, 345, 125) && CreateButton(ChangeFontButton, CommunicationsPanel, "ChangeFontButton", "Font<>", 95, 95, 230, 115) && CreateButton(ToggleThemeButton, CommunicationsPanel, "ToggleThemeButton", "Theme<>", 5, 95, 90, 115); CommunicationsPanel.Add(InputBox); CommunicationsPanel.Add(CharCounter); CommunicationsPanel.Add(SendButton); CommunicationsPanel.Add(ClearButton); CommunicationsPanel.Add(ChangeFontButton); CommunicationsPanel.Add(ToggleThemeButton); return buttonsCreated && CreateQuickMessageButtons(); } bool CreateQuickMessageButtons() { string quickMessages[] = {QuickMessage1, QuickMessage2, QuickMessage3, QuickMessage4, QuickMessage5, QuickMessage6, QuickMessage7, QuickMessage8}; const int startX = 5, startY = 160, width = 222, height = 65, spacing = 5; for(int i = 0; i < 8; i++) { const int xPos = startX + (i % 2) * (width + spacing); const int yPos = startY + (i / 2) * (height + spacing); if(!QuickMessageButtons[i].Create(ChartID(), "QuickMsgBtn" + IntegerToString(i+1), 0, xPos, yPos, xPos + width, yPos + height)) return false; QuickMessageButtons[i].Text(quickMessages[i]); CommunicationsPanel.Add(QuickMessageButtons[i]); } return true; } //+------------------------------------------------------------------+ //| Data for Pie Chart | //+------------------------------------------------------------------+ void GetTradeData(int &wins, int &losses, int &forexTrades, int &stockTrades, int &futuresTrades) { wins = 0; losses = 0; forexTrades = 0; stockTrades = 0; futuresTrades = 0; if (!HistorySelect(0, TimeCurrent())) { Print("Failed to select trade history."); return; } int totalDeals = HistoryDealsTotal(); for (int i = 0; i < totalDeals; i++) { ulong dealTicket = HistoryDealGetTicket(i); if (dealTicket > 0) { double profit = HistoryDealGetDouble(dealTicket, DEAL_PROFIT); if (profit > 0) wins++; else if (profit < 0) losses++; string symbol = HistoryDealGetString(dealTicket, DEAL_SYMBOL); if (SymbolInfoInteger(symbol, SYMBOL_SELECT)) { if (StringFind(symbol, ".") == -1) forexTrades++; else { string groupName; if (SymbolInfoString(symbol, SYMBOL_PATH, groupName)) { if (StringFind(groupName, "Stocks") != -1) stockTrades++; else if (StringFind(groupName, "Futures") != -1) futuresTrades++; } } } } } } //+------------------------------------------------------------------+ //| Custom Pie Chart Class | //+------------------------------------------------------------------+ class CCustomPieChart : public CPieChart { public: void DrawPieSegment(double fi3, double fi4, int idx, CPoint &p[], const uint clr) { DrawPie(fi3, fi4, idx, p, clr); // Expose protected method } }; //+------------------------------------------------------------------+ //| Analytics Chart Class | //+------------------------------------------------------------------+ class CAnalyticsChart : public CWnd { private: CCustomPieChart pieChart; // Declare pieChart as a member of this class public: bool CreatePieChart(string label, int x, int y, int width, int height) { if (!pieChart.CreateBitmapLabel(label, x, y, width, height)) { Print("Error creating Pie Chart: ", label); return false; } return true; } void SetPieChartData(const double &values[], const string &labels[], const uint &colors[]) { pieChart.SeriesSet(values, labels, colors); pieChart.ShowPercent(); } void DrawPieChart(const double &values[], const uint &colors[], int x0, int y0, int radius) { double total = 0; int seriesCount = ArraySize(values); if (seriesCount == 0) { Print("No data for pie chart."); return; } for (int i = 0; i < seriesCount; i++) total += values[i]; double currentAngle = 0.0; // Resize the points array CPoint points[]; ArrayResize(points, seriesCount + 1); for (int i = 0; i < seriesCount; i++) { double segmentValue = values[i] / total * 360.0; double nextAngle = currentAngle + segmentValue; // Define points for the pie slice points[i].x = x0 + (int)(radius * cos(currentAngle * M_PI / 180.0)); points[i].y = y0 - (int)(radius * sin(currentAngle * M_PI / 180.0)); pieChart.DrawPieSegment(currentAngle, nextAngle, i, points, colors[i]); currentAngle = nextAngle; } // Define the last point to close the pie points[seriesCount].x = x0 + (int)(radius * cos(0)); // Back to starting point points[seriesCount].y = y0 - (int)(radius * sin(0)); } }; //+------------------------------------------------------------------+ //| Initialize Analytics Panel | //+------------------------------------------------------------------+ bool InitializeAnalyticsPanel() { if (!AnalyticsPanel.Create(ChartID(), "Analytics Panel",0, 500, 450, 1285, 750)) { Print("Failed to create Analytics Panel"); return false; } int wins, losses, forexTrades, stockTrades, futuresTrades; GetTradeData(wins, losses, forexTrades, stockTrades, futuresTrades); CAnalyticsChart winLossChart, tradeTypeChart; // Win vs Loss Pie Chart if (!winLossChart.CreatePieChart("Win vs. Loss", 690, 480, 250, 250)) { Print("Error creating Win/Loss Pie Chart"); return false; } double winLossValues[] = {wins, losses}; string winLossLabels[] = {"Wins", "Losses"}; uint winLossColors[] = {clrGreen, clrRed}; winLossChart.SetPieChartData(winLossValues, winLossLabels, winLossColors); winLossChart.DrawPieChart(winLossValues, winLossColors, 150, 150, 140); AnalyticsPanel.Add(winLossChart); // Trade Type Pie Chart if (!tradeTypeChart.CreatePieChart("Trade Type", 950, 480, 250, 250)) { Print("Error creating Trade Type Pie Chart"); return false; } double tradeTypeValues[] = {forexTrades, stockTrades, futuresTrades}; string tradeTypeLabels[] = {"Forex", "Stocks", "Futures"}; uint tradeTypeColors[] = {clrBlue, clrOrange, clrYellow}; tradeTypeChart.SetPieChartData(tradeTypeValues, tradeTypeLabels, tradeTypeColors); tradeTypeChart.DrawPieChart(tradeTypeValues, tradeTypeColors, 500, 150, 140); AnalyticsPanel.Add(tradeTypeChart); return true; } //+------------------------------------------------------------------+ //| Analytics Button Click Handler | //+------------------------------------------------------------------+ void OnAnalyticsButtonClick() { // Clear any previous pie charts because we're redrawing them ObjectDelete(0, "Win vs. Loss Pie Chart"); ObjectDelete(0, "Trade Type Distribution"); // Update the analytics panel with fresh data AnalyticsPanel.Destroy(); InitializeAnalyticsPanel(); AnalyticsPanel.Show(); } //+------------------------------------------------------------------+ //| Cleanup Operations | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { // Release all UI components AuthenticationDialog.Destroy(); TwoFactorAuthDialog.Destroy(); AdminHomePanel.Destroy(); AnalyticsPanel.Destroy(); }
コード編成のプロセスにおいて、元のバージョン(v1.24)から更新バージョン(v1.25)への移行は、いくつかの重要な改善点を反映しています。
1. モジュール性と構造の強化:更新されたコードでは、各機能がより論理的にセクションごとにグループ化されています。たとえば、v1.24では UI の初期化や管理に関するコードが散在しており、明確な区分がされていませんでした。v1.25 では、「取引管理機能」、「認証管理」、「パネル初期化機能」など、明確に定義されたセクションごとに整理されており、コードの可読性と保守性が大幅に向上しています。各セクションの冒頭には、実装される機能を示す見出しが追加されており、コード全体のナビゲーションも容易になっています。
2. 関心の分離:v1.24では、ShowAuthenticationPromptやShowTwoFactorAuthPromptなどの関数がグローバル宣言と混在しており、初期化ロジックとの区別が不明瞭でした。v1.25では、InitializeAuthenticationDialogやInitializeTwoFactorAuthDialogといった初期化専用の関数が、イベントハンドラやユーティリティ関数と明確に分離され、各処理単位の複雑さが軽減されています。これにより、EAの各コンポーネントが「初期化」から「ユーザー操作の処理」までどのようにライフサイクルを持つかが、より理解しやすくなりました。さらに、CAnalyticsChartやCCustomPieChartといった分析用の専用クラスが導入されており、複雑なチャート描画ロジックがカプセル化されています。これによりコードの再利用性が高まり、各クラス・関数が単一の責任を持つよう設計されています。
コンパイルが正常に完了すると、管理パネルが起動し、最初のステップとしてセキュリティ認証が求められます。正しい認証情報を入力すると、システムは「管理ホームパネル」へのアクセスを許可します。
なお、デフォルトのパスワードはコード内で「2024」と設定されています。以下の画像は、チャート上でエキスパートアドバイザー(EA)が起動した際の様子を示しています。
新しいソースコードからチャートに管理パネルを追加する
組み込みのよく整理されたコードの一例
この記事を書く前に、MQL5 におけるDailogクラスの組み込み実装を目にする機会がありました。これは非常に参考になる例であり、より読みやすく再利用可能なコードを目指すうえで大きな動機付けとなりました。この例は、Controls EAとして提供されており、Examplesフォルダ内のExpertsディレクトリに配置されています。下の画像をご覧ください。
MetaTrader 5でコントロールEAを見つける
サンプル アプリケーションは応答性が高く、多数のインターフェイス機能が含まれています。下の画像をご覧ください。
チャートにコントロールを追加する
ソースコードを表示するには、MetaEditorを開き、Experts フォルダに移動します。以下に示すように、Examplesフォルダー内のControlsにソースコードがあります。
MetaEditorでコントロールソースにアクセスする
これはプログラムのメインコードとして機能しており、UI ロジックの大部分はCControlsDialogに分散されています。このクラスは、インターフェイスの作成を簡素化するためにDialogクラスを活用しています。ソースコードはコンパクトで読みやすく、スケーラブルです。//+------------------------------------------------------------------+ //| Controls.mq5 | //| Copyright 2000-2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2000-2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include "ControlsDialog.mqh" //+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ CControlsDialog ExtDialog; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- create application dialog if(!ExtDialog.Create(0,"Controls",0,20,20,360,324)) return(INIT_FAILED); //--- run application ExtDialog.Run(); //--- succeed return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- destroy dialog ExtDialog.Destroy(reason); } //+------------------------------------------------------------------+ //| Expert chart event function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, // event ID const long& lparam, // event parameter of the long type const double& dparam, // event parameter of the double type const string& sparam) // event parameter of the string type { ExtDialog.ChartEvent(id,lparam,dparam,sparam); } //+------------------------------------------------------------------+
この例は、モジュール化されたアーキテクチャとベストプラクティスに基づく設計原則により、プロフェッショナルなコード構成を示しています。コードは関心の分離を明確に実現しており、すべての UIロジックをCControlsDialogクラス(ControlsDialog.mqhモジュール内に定義)に委任し、メインファイルはアプリケーションのライフサイクル管理のみに専念しています。
このモジュール化アプローチにより、実装の詳細はカプセル化され、初期化(Create())、実行(Run())、終了処理(Destroy())といった標準化されたインターフェースのみが外部に公開されます。チャートイベントは ExtDialog.ChartEvent()を通じてダイアログコンポーネントに直接転送されるため、イベント処理がコアアプリケーションロジックから分離され、再利用性とテストのしやすさが向上します。
構造としては、UI に関する宣言を一切含まない簡潔なメインファイル設計を通じて、高い設計基準を維持しています。また、コンポーネント間の厳密な境界が守られており、安全なコードの修正やチーム開発を可能にしています。このような設計パターンは、MQL5開発におけるスケーラビリティの高い手法の好例であり、各モジュールが明確な責任を持つことで、認知負荷を軽減し、明確なインターフェイス契約と体系的なリソース管理を通じて保守性を高めています。
結論
より構造化され、エンタープライズグレードのコード組織を実現するための取り組みを開始しました。命名規則の一貫性の改善、コメントの充実、エラー処理の洗練、関連機能の論理的なグループ化といった点で大きな進歩を遂げました。これらの変更により、メインファイルのサイズが縮小し、関心事の明確な分離が実現され、再利用可能なコンポーネントが生まれ、一貫した命名規則も確立されました。
これらの組織的な改善により、コードベースはより扱いやすく、スケーラブルなものとなっています。これによって、他の機能に影響を及ぼすことなく、特定の機能領域の更新や追加が容易になりました。また、この構造化されたアプローチは、システムの各部分を分離することで、より効率的なテストやデバッグも可能にしています。
次のステップとしては、プログラムのさらなるモジュール化を進め、コンポーネントを他のEAやインジケーターのプロジェクトで簡単に再利用できるようにしていきます。この継続的な取り組みは、最終的に取引コミュニティ全体に貢献するものと確信しています。強固な基盤はすでに築かれましたが、なお深く議論すべき点や分析が必要な側面が残っており、それらについては次回の記事で詳しく検討していく予定です。
このガイドを通じて、クリーンで読みやすく、スケーラブルなコードを開発できることを確信しています。そうすることで、自身のプロジェクトが改善されるだけでなく、他の開発者も引き寄せ、将来的な再利用のための大規模なコードライブラリの構築にも寄与できます。このような共同作業がコミュニティ全体の効率を高め、イノベーションの促進にもつながるでしょう。
ご意見やフィードバックは、以下のコメント欄にてお寄せください。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/16539





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索