创建MQL5交易管理员面板(第九部分):代码组织(1)
概述
冗长且缺乏合理组织的代码往往难以理解,尤其是当代码结构混乱时。这常常会导致项目半途而废。如今我的交易管理面板项目已经集成了多个模块、规模日益庞大,但这是否意味着,我就必须放弃它了呢?绝对不是!相反,我们需要一些策略来确保它能够平稳运行。
这就引出了我们今天的讨论主题,我们将探讨在MQL5中,代码组织如何助力算法开发。MQL5以及其他大型项目的成功,往往可归因于它们采用的结构化方法——这使它们能够高效地管理和维护庞大的代码库。
如果没有恰当的文档记录和结构规划,维护代码将变成一项挑战,甚至会使未来的修改工作变得异常艰难。
在本次讨论中,我们将针对这些挑战探索切实可行的解决方案,重点是为交易管理面板构建长期可扩展的结构。代码组织不仅仅是为了方便;它是编写高效、可维护程序的关键因素。通过采用这些最佳实践,我们可以确保MQL5项目保持稳健、可共享且可扩展——使个人开发者能够构建并维护复杂的应用程序。
在我们深入探讨之前,让我先梳理一下本次讨论的关键要点:
讨论概述
在本系列的前一篇文章中,我们见证了随着为管理面板引入更多专业子面板,程序实现了显著扩展,使其成为任何交易者不可或缺的仪表板。随着这些新增功能的加入,我们现在拥有四个子面板:管理主页面板、通信面板、交易管理面板和分析面板。代码量已大幅增加,勾勒出了主要结构,但为增强每个功能特性,仍有许多工作要做。
当我考虑进入下一步并打算添加更多功能时,我意识到重新审视整个代码以进行更好组织的重要性。这就是本主题的由来。我并不打算只展示一个完成的程序,而是觉得与您一起梳理代码精炼与组织的过程会更有价值。在下一节中,将基于我的研究进一步探讨代码组织。
我认为,到本次讨论结束时,必定有人已掌握了足够的知识来回答以下问题:
- 如何开发大型程序?
- 如何让他人理解我的大型程序?
理解代码组织
根据各种资料,代码组织是指以增强代码可读性、可维护性和可扩展性的方式来构建和排列代码的做法。组织良好的代码使开发者更容易理解、调试和扩展他们的程序。
正如软件工程师张兆军曾提到的:“代码的组织就像您家的整洁程度:您不需要每天都整理,无论它有多乱,您仍然可以住在里面,只要能够忍受。只有当您迫切需要找到很久没碰过的东西,或者当您想要邀请客人来享用一顿丰盛的晚餐时,才会困扰您。”
我认为这个比喻清楚地表明,代码组织不仅对您自己的工作流程很重要,对那些可能与您共享代码的其他人也很重要。让我们分解代码组织的这些关键概念——可读性、可维护性和可扩展性——并探讨它们的意义,特别是在MQL5算法开发的背景下。
1. 可读性:
可读性指的是读者理解代码逻辑和结构的难易程度。在MQL5的背景下,这一点尤为重要,因为代码可能由多个开发者共同完成,而且即使您独自工作,如我之前所述,您也希望在某个时候回顾或调试自己的代码。
关键特征:
- 清晰的变量命名:为变量、函数和类使用有意义的名称。不要使用像a、b或temp这样模糊的名称,而是选择能够传达其用途的描述性名称,如movingAveragePeriod或signalStrength。
- 注释:好的注释解释为什么存在某些代码块,而不仅仅是它们做什么。这对于记录算法的意图至关重要.
- 一致的格式化:缩进和行间距有助于将代码分解为可读的块。例如,对循环、条件语句和函数使用一致的缩进。
- 模块化代码:将代码分解为小型、自包含的函数或类,每个函数或类处理一个特定任务(如计算移动平均值或检查交易条件),这样可以提高可读性。
- 快速调试:可读性强的代码更容易发现错误并进行纠正。
- 协作:如果您的代码简洁且易于理解,其他开发者就更容易与您协作或帮助您解决问题。
- 更快上手:当重新审视一个项目时,可读性强的代码确保您不会浪费时间重新理解自己的工作。
2. 可维护性:
可维护性是指代码随时间推移进行修改或扩展的难易程度,特别是在需要添加新功能或修复错误时。在算法交易中,如MQL5,策略经常演变,因此可维护性对于长期成功至关重要。
关键特征:
- 模块化:通过使用函数或类将不同任务分隔开来(例如,用一个函数处理交易执行,另一个函数计算指标),您可以创建出系统中相互独立的部分。这样一来,对某个区域的修改不会影响到其他区域。
- 关注点分离:代码的每个部分都应该承担单一职责。例如,下单逻辑应该与评估市场状况的逻辑相分离。
- 使用库和内置函数:与其重复造轮子,不如利用MQL5的内置函数和库来处理常见任务,如移动平均线计算或下单操作,这样可以降低复杂性和错误率。
- 版本控制:使用版本控制系统(例如,Git 和MQL5存储库)来跟踪代码变更,这样若某次修改引入了错误或意外行为,您便可回滚到之前版本。
优势:
- 未来修改:随着策略的不断发展,维护结构良好的代码库能让开发者以最小的精力实现新功能或做出调整。
- 错误修复:当发现错误时,可维护性强的代码能帮助您快速解决问题,而不会干扰系统的其他部分。
- 效率:开发者无需花费过多时间理清代码的工作原理,从而能更快地进行更新并减少错误。
3. 可伸缩性:
可伸缩性指的是代码处理不断增加的工作量或适应不断增长的数据/功能需求的能力。随着交易策略变得愈发复杂且对数据的需求日益增大,可伸缩性对于交易系统的平稳运行至关重要。
关键特征:
- 高效算法:在算法交易中,您可能需要处理大量历史数据、执行众多交易或同时分析多种资产。优化算法以提高速度和内存的使用效率至关重要。
- 数据结构:选择合适的数据结构,如数组、列表或映射,有助于高效管理更大的数据集。MQL5提供了如数组和结构体等数据结构,可用来扩展您的交易策略。
- 并行处理:MQL5支持多线程,允许您并行运行多个任务。这在复杂的交易策略或回测中特别有用,因为不同的任务(如市场分析和订单执行)可以同时处理。
- 异步操作:对于无需阻塞算法其他部分执行的任务(例如,从外部API获取数据),使用异步操作有助于保持系统的响应能力。
优势:
- 处理更大规模的数据:可伸缩的代码可以处理更大规模的市场数据集或纳入更多资产,而不会显著降低性能。
- 支持扩展:如果算法需要容纳更多功能(如交易多种货币对、应用机器学习模型或加强风险管理),可伸缩的代码提供了无需大规模改造即可进行扩展的灵活性。
- 实时性能:在实时交易环境中,可伸缩性确保您的算法能够处理实时数据馈送和订单执行,而不会出现延迟。
在MQL5中,可读性、可维护性和可伸缩性常常相互重叠并相互加强。例如,一个可读性强且模块化的函数在进行调整时更容易维护。同样,可伸缩的代码往往更具模块化,这也增强了其可读性和可维护性。在开发交易算法时,这种平衡确保了代码现在表现良好,并且可以随着交易策略的演变或随着数据量的增加对性能要求的提高,从而进行适应或扩展。
例如,在这个开发过程中,我们从第一部分中的通信面板开始。随着项目的迭代,我们无缝地集成了具有不同专业功能的新面板,而没有破坏核心逻辑。这里展示了可伸缩性,但仍有关键概念需要考虑,以提高代码中现有功能的可重用性。
在管理面板(EA)上实现
在应用代码组织优化方案时,我们将参考前一篇文章中的代码。代码结构的设计方法可能因人而异——有些开发者喜欢在构建过程中同步组织代码,而另一些则可能选择在构建完成后进行评估和完善。无论采用哪种方法,快速评估都有助于确定代码是否符合基本标准。正如我在张兆军 的引言中提到的,代码组织得井井有条并非强制要求。只要代码能够正常运行,部分开发者对代码杂乱无章并不在意。然而,这往往会在项目扩展时带来重大挑战。结构不良的代码会加大维护、扩展和调试的难度,从而限制项目的长期发展。因此,我强烈建议采用代码组织的最佳实践。让我们在下一节中进一步探讨。
识别代码问题
在重读管理面板V1.24的源代码时,我决定对其组件进行总结,以便自己能更快速地理解代码并识别出潜在问题。一般来说,作为原始开发者,我对自己程序的组件是有所了解的,但真正的挑战在于如何将这些组件组织起来,在保持代码可读性的同时缩短代码长度。因此,以下我提炼了九个主要部分,帮助我们快速把握程序的整体架构。接下来,我将进一步分享将要解决的一些问题。
1. 用户界面元素与全局变量
// 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()函数,包含:
- 按钮点击检查
- 混合的用户界面/交易/认证逻辑
- 直接函数调用,无路由机制
7. 安全组件:
- 明文密码存储
- 基础双因素认证实现
- Telegram凭证未加密
- 无会话管理
8. 初始化/清理:
- OnInit()函数按顺序创建用户界面
- OnDeinit()函数销毁面板
- 无资源管理系统
9. 错误处理:
- 使用基本的Print()语句输出错误
- 无错误恢复机制
- 无事务回滚
- 交易操作验证限制
10. 格式、缩进与空格:
- 这方面MetaEditor的处理得相当完善。
- 我们的代码可读性尚可,但存在其他问题,例如重复代码,我们将在下一节中处理这些问题。
审视代码后,以下是需要注意的一系列组织架构问题:
- 整体式结构——所有功能都集中在一个文件中
- 紧密耦合——用户界面逻辑与业务逻辑混杂在一起
- 重复模式——按钮/面板创建的代码存在相似性
- 安全风险——硬编码凭据,缺乏加密措施
- 扩展性受限——缺乏模块化架构
- 命名不一致——主要存在于(用户界面元素和全局变量)中
重新组织代码
完成此步骤后,程序必须仍能运行并保持原有的功能。基于之前的评估,我们将在下文讨论一些改进代码组织的方式。
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. 重复的代码模式
这里的问题是用户界面创建代码的重复,特别是面板和按钮的创建代码。我们可以通过创建用户界面辅助函数来解决这一问题,这将简化构建界面及其元素的过程。
以下是一个用于创建按钮的辅助函数的示例:
//+------------------------------------------------------------------+ //| 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. 安全风险:
为简化流程,我们继续使用硬编码密码,但我们在第七部分中经已讨论过这方面的问题。这些可以通过使用加密配置来解决。
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/en/users/billionaire2024/seller | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, Clemence Benjamin" #property link "https://www.mql5.com/en/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版本中,大部分用户界面的初始化和管理是分散的,或者没有清晰的分隔。新版本将代码组织成定义明确的各个部分,如“交易管理功能”“认证管理”和“面板初始化功能”。这种分隔使代码更易读且更易于维护。现在,每个部分都以一个清晰的标题开头,表明正在实现何种功能,这有助于快速浏览整个代码库。
2. 关注点分离:在v1.24版本中,像ShowAuthenticationPrompt(显示身份验证提示)和ShowTwoFactorAuthPrompt(显示双因素身份验证提示)这样的函数与全局声明混在一起,并且与初始化逻辑没有清晰的分隔。v1.25版本中的更新代码更有效地分离了这些关注点。像InitializeAuthenticationDialog(初始化身份验证对话框)和InitializeTwoFactorAuthDialog(初始化双因素身份验证对话框)这样的初始化函数,现在与事件处理程序或实用函数区分开来,降低了每个代码段的复杂度。这种分离有助于理解EA不同组件的生命周期,从初始化到交互处理。此外,引入用于处理分析的特定类(CAnalyticsChart和CCustomPieChart)封装了复杂的图表绘制逻辑,促进了代码复用,并确保每个类或函数都遵循单一职责原则。
成功编译后,管理面板会成功启动,并首先提示进行安全验证。输入正确的凭据后,系统将授予访问管理主页面板的权限。
作为参考,代码中指定的默认密码设置为“2024”。以下是展示了在图表上启动EA的图片:

在新的源代码中将管理面板添加到图表
一个代码组织良好的内置示例
在撰写本文之前,我偶然发现了MQL5中内置的Dialog类的实现。这成了一个极具启发性的示例,激发了人们编写更具可读性和可复用性代码的动力。该示例是一个控件EA,位于Examples下的Experts文件夹中。请看下面的图片。
在MetaTrader 5中定位控件EA
该示例应用程序响应性极高,且包含众多界面功能。请看下面的图片。

将控件添加到图表中
要查看源代码,请打开MetaEditor,导航至Experts文件夹,然后在Examples文件夹中找到Controls的源代码,如下图所示。
在MetaEditor中访问控件源代码
这段代码是该程序的主要代码,其中大部分用户界面逻辑分布在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); } //+------------------------------------------------------------------+
该示例通过模块化架构和最优实践设计原则,展示了专业的代码组织方式。代码通过将所有用户界面逻辑委托给CControlsDialog类(在随附的ControlsDialog.mqh模块中定义),实现了清晰的关注点分离,而主文件则仅专注于应用程序生命周期管理。
这种模块化方法封装了实现细节,仅暴露标准化的接口,如用于初始化、执行和清理的Create()、Run()和Destroy()方法。通过ExtDialog将图表事件直接转发给对话框组件。ChartEvent()架构将事件处理与核心应用程序逻辑解耦,确保了代码的可复用性和可测试性。
该结构通过极简的主文件设计(不含任何UI声明)并强制实施严格的组件边界,达到了高标准,从而能够安全地进行修改并支持团队协作。这种模式体现了可扩展的MQL5开发方式,其中离散的模块负责管理特定的职责,通过清晰的接口契约和系统的资源管理,减少了认知负担,同时促进了代码的可维护性。
结论
我们已踏上实现更结构化、达到企业级水准的代码组织之路。通过解决命名规范不一致的问题、增强注释说明、优化错误处理以及按逻辑对相关功能进行分组,我们取得了显著的改进。这些变更带来了主文件体积的缩小、关注点的清晰分离、可复用组件的创建以及命名规范的一致性确立。
这些组织层面的改变使得代码库更易于浏览且更具可扩展性,允许我们在不影响其他功能区域的情况下,更轻松地对特定功能区域进行更新和添加。这种结构化的方法还通过隔离系统的不同部分,为更好的测试和调试提供了便利。
我们的下一步计划是进一步模块化我们的程序,以确保其组件能够轻松地在其他EA和指标项目中复用。这项持续的努力最终将惠及整个交易社区。尽管我们已经打下了坚实的基础,但仍有一些方面值得进行更深入的探讨和分析,将在下一篇文章中对此进行更详细的阐述。
我坚信,依靠这份指南,我们能够开发出整洁、易读且可扩展的代码。通过这样做,我们不仅能够改进自己的项目,还能吸引其他开发者,为构建一个大型的代码库以供未来复用做出贡献。大家集体努力的成果将提升我们社区的效率,并激发创新。
欢迎在下方评论区留下您的意见和反馈。
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/16539
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
精通日志记录(第五部分):通过缓存和轮转优化处理程序
价格行为分析工具包开发(第10部分):外部资金流(二)VWAP
在MQL5中构建自优化智能交易系统(EA)(第五部分):自适应交易规则
基于Python与MQL5的特征工程(第三部分):价格角度(2)——极坐标(Polar Coordinates)法