Создание торговой панели администратора на MQL5 (Часть XII): Интеграция форекс-калькулятора
Оглавление
- Введение
- Обзор
- Форекс-расчеты и формулы
- Реализация:
- (1) Настройка раздела отложенных ордеров для освобождения места под новые элементы управления.
- (2) Разработка класса управления для калькулятора валютных значений.
- (3) Интеграция калькулятора валютных значений в панель управления торговлей.
- (4) Доработка нового советника административной панели для обеспечения корректной работы с обновлениями.
- Тестирование
- Заключение
Введение
Сегодняшнее обсуждение посвящено решению проблемы ручного или внешнего расчета торговых показателей путем интеграции Форекс-калькулятора непосредственно в Панель управления торговлей — подпанель нового советника административной панели.
В прошлом многие трейдеры полагались на внешние веб-сайты для выполнения этих расчетов. Эти инструменты были невероятно полезны, и разработчики таких платформ заслуживают признания за предоставление ценных сервисов. Даже сегодня некоторые трейдеры продолжают использовать эти онлайн-калькуляторы — в конечном счете, это вопрос предпочтений.
Однако, благодаря возможностям MQL5 и его инструментарию для создания графического интерфейса, у нас теперь есть возможность создавать более эффективные, интегрированные решения непосредственно внутри торгового терминала. Такой подход исключает необходимость переключения между приложениями и повышает эффективность рабочего процесса, сохраняя все необходимые инструменты в одном месте.
Благодарим MetaTrader 5 за предоставление надежного API, который обеспечивает беспрепятственный доступ к рыночным новостям и потокам данных непосредственно в терминале. Хотя для калькуляторов и новостных лент существуют сторонние API, мы стремимся разработать собственный алгоритм калькулятора, адаптированный специально для нашей панели.
Этот проект не умаляет достоинств существующих решений, а скорее расширяет спектр выбора, доступного трейдерам. Он способствует более глубокому пониманию возможностей MetaTrader 5 и побуждает пользователей более эффективно взаимодействовать с платформой. Предлагая полностью интегрированный набор инструментов непосредственно в терминале, мы стремимся обеспечить более гладкий и продуктивный торговый опыт, демонстрируя, как достижения в области торговых технологий продолжают революционизировать индустрию.
Некоторые из рассчитываемых значений:
- Размер позиции
- Сумма риска
- Стоимость пункта
- Требование к марже
- Оценка прибыли/убытка
- Свопы / Ночная комиссия
- Соотношение риска к прибыли
- Уровень маржи
- Стоимость спреда
- Цена безубыточности
- Ожидаемая доходность
- Влияние кредитного плеча и т.д.
Эти расчеты жизненно важны для трейдеров на Форекс, поскольку они обеспечивают структурированную основу для управления рисками, оптимизации торговых установок и поддержания устойчивости счета. Расчеты размера позиции и суммы риска гарантируют, что трейдеры рискуют лишь небольшой, заранее определенной частью своего капитала, защищая от значительных убытков. Оценка стоимости пункта и прибыли/убытка позволяет точно планировать сделки, помогая трейдерам устанавливать реалистичные цели и уровни стоп-лосс. Расчеты требований к марже и уровня маржи предотвращают чрезмерное использование кредитного плеча, которое может привести к маржин-коллу или полной потере средств на счете. Комиссии за своп (овернайт) критически важны для долгосрочных трейдеров, особенно тех, кто использует стратегии кэрри-трейд, поскольку они влияют на затраты на удержание позиции.
Соотношение риска к прибыли помогает в выборе сделок, гарантируя, что потенциальная прибыль оправдывает риски. Дополнительные метрики, такие как стоимость спреда, цена безубыточности, ожидаемая доходность и влияние кредитного плеча, улучшают процесс принятия решений, учитывая транзакционные издержки, жизнеспособность стратегии и общий уровень риска. Вместе эти инструменты позволяют трейдерам принимать обоснованные и дисциплинированные решения, согласовывая сделки со своими финансовыми целями и рыночными условиями, что в конечном итоге повышает стабильность и прибыльность.
В следующем разделе я представлю краткий обзор того, как мы подойдем к сегодняшней разработке.
Обзор
Благодаря внедрению модульной архитектуры в этой серии статей мы получили возможность сосредоточиться на отдельных частях программы, не нарушая работу других компонентов. Эта гибкость теперь позволяет нам модернизировать панель управления торговлей, чтобы освободить место для интеграции инструментов расчета.
Для этого мы задействуем дополнительные классы из стандартной библиотеки MQL5. Вместо того чтобы поддерживать отдельные секции ввода для каждого типа ордеров, мы реализуем выпадающее меню для выбора ордера в сочетании с одной строкой ввода. Такая оптимизированная компоновка освободит место для компонентов нашего калькулятора.
Хотя нет необходимости отображать все торговые показатели, определенные ключевые значения необходимы для принятия обоснованных решений и должны быть доступны. Некоторые из этих значений вообще не нужно рассчитывать, так как они уже доступны через данные рынка MQL5 в реальном времени.
Мы начнем с детального изучения ключевых форекс-терминов и величин, включая их определения, формулы и способы их представления в MQL5. Затем мы перейдем к этапу реализации, начиная с настройки раздела Ордеров в Панели управления торговлей, чтобы приспособить ее для пользовательского интерфейса калькулятора.

Усовершенствование панели управления торговлей
В разделе, обозначенном буквой A на приведенной выше иллюстрации, мы будем использовать класс ComboBox для вывода списка и выбора типа ордера. Раздел B будет преобразован в однострочный макет, а раздел с датой истечения (C) будет улучшен с помощью элемента выбора даты (DatePicker) для повышения удобства использования.
После корректировки макета мы интегрируем как логику расчетов, так и логику графического интерфейса для ввода данных, которая обычно требует не более трех полей ввода для каждого расчета.
В заключение я поделюсь процессом тестирования и его результатами, и мы завершим статью оценкой новой функциональности.
Форекс-расчеты и формулы
В таблице ниже представлены некоторые распространенные форекс-термины, которые обычно требуют расчета, а также соответствующие формулы и пользовательские функции MQL5, используемые для их вычисления. Эти примеры не являются исчерпывающими; как трейдер, вам может потребоваться выполнять дополнительные расчеты в зависимости от конкретной реализуемой стратегии. Формулы в приведенной ниже таблице являются результатом обширного исследования и объединения математических идей из различных онлайн-источников. Для дальнейшего изучения или проверки вы можете найти дополнительную информацию, используя Google или другие авторитетные ресурсы.
| Форекс-термин и описание | Общая формула | Формула на 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); } |
| Стоимость пункта Рассчитывает денежную стоимость движения на один пункт (пипс) для заданного размера лота, что необходимо для расчетов риска и прибыли. | ![]() | 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); } |
В следующем разделе, посвященном реализации, мы будем использовать CComboBox из стандартной библиотеки MQL5 для оптимизации использования пространства под элементы управления калькулятора, которые будут интегрированы в панель управления торговлей. Этот подход дает ценные уроки по эффективному дизайну пользовательского интерфейса и управлению элементами управления. Кроме того, мы добавим компонент DatePicker для улучшения пользовательского опыта при выборе даты истечения срока действия ордера.
Реализация
Чтобы обеспечить неуклонный прогресс, мы разобьем нашу разработку на четыре основных этапа:
- (1) Настройка раздела отложенных ордеров для освобождения места под новые элементы управления.
- (2) Разработка класса управления для калькулятора валютных значений.
- (3) Интеграция калькулятора валютных значений в Панель управления торговлей.
- (4) Доработка нового советника административной панели для обеспечения корректной работы с обновлениями.
Как только эти шаги будут выполнены, мы обновим советник NewAdminPanel для поддержки новых функций и проведем тестирование. Важно внимательно следить за процессом, чтобы не упустить критические детали — особенно при работе с компонентами ComboBox и DatePicker.
(1) Настройка раздела отложенных ордеров для освобождения места под новые элементы управления.
Теперь мы извлечем раздел отложенных ордеров из заголовка панели управления торговлей, чтобы изолировать его для более легкой реализации компонентов ComboBox и DatePicker. Кроме того, мы добавим кнопку размещения ордера, которую нужно будет нажать после того, как ордер будет полностью настроен.
Объявления членов для отложенных ордеров
Эти переменные-члены находятся в разделе “Отложенные ордеры” класса CTradeManagementPanel. Мы начнем с объявления одной метки, которая отображается над элементами управления отложенными ордерами в качестве заголовка раздела (“Отложенные ордеры:”).
// 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
Непосредственно под ней расположены еще четыре метки, служащие заголовками столбцов: “Цена:”, “TP:”, “SL”: и “Истечение:”. Под метками мы размещаем ComboBox, позволяющий пользователю выбрать один из четырех типов отложенных ордеров: Buy Limit, Buy Stop, Sell Limit и Sell Stop. Справа от этого ComboBox находятся три поля ввода, дающие пользователю возможность ввести цену отложенного ордера, тake-profit (TP) и stop-loss (SL) соответственно. Рядом с этими полями ввода расположен элемент выбора даты (DatePicker), который упрощает выбор даты истечения срока действия ордера. В завершение мы объявляем кнопку с надписью “Разместить ордер”; при ее нажатии будет инициировано фактическое размещение отложенного ордера с указанными параметрами.
Сгруппировав эти шесть элементов управления и пять меток в рамках данного раздела, мы изолируем все необходимое для создания отложенных ордеров и управления ими. Такое разделение упрощает объяснение или рефакторинг логики, касающейся только отложенных ордеров, не затрагивая остальную часть панели.
Создание элементов управления отложенными ордерами в методе Create(...)
Внутри метода Create(...) мы создаем весь раздел “Отложенные ордеры” сразу после отрисовки разделительной линии под калькулятором “Форекс калькулятор”. Сначала мы добавляем небольшой вертикальный отступ, чтобы визуально отделить его от расположенного выше калькулятора. Затем создается метка заголовка раздела (“Отложенные ордеры:”) с жирным начертанием, чтобы выделить его среди других разделов.
Далее, справа от этого заголовка размещается ComboBox для выбора типа ордера. После добавления ComboBox и смещения вертикальной позиции мы создаем четыре заголовка столбцов: “Цена:”, “TP:”, “SL”: и “Истечение:”. Каждый заголовок позиционируется горизонтально с равными интервалами, чтобы они располагались над строкой ввода.
// 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).
Наконец, создается кнопка “Разместить ордер”, которая располагается рядом с элементом выбора даты, чтобы не загромождать другие элементы управления. После успешного создания всех элементов управления мы сдвигаем вертикальный курсор вниз, чтобы оставить место ниже. В совокупности эти шаги формируют все элементы управления, необходимые пользователю для настройки отложенного ордера — типа, цены, TP, SL, даты истечения — и последующего нажатия кнопки для его размещения.
Обработчики событий для отложенных ордеров
Эти методы реагируют на взаимодействия пользователя в рамках раздела “Отложенные ордеры”:
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). Затем мы немедленно заполняем поле ввода цены этим рыночным значением. Это гарантирует, что пользователь всегда видит допустимую, актуальную цену по умолчанию, соответствующую выбранному типу ордера. Наконец, мы перерисовываем интерфейс графика, чтобы новая цена отобразилась немедленно.
При изменении даты истечения: Всякий раз, когда пользователь выбирает или изменяет дату истечения в элементе выбора даты (DatePicker), мы извлекаем новую дату и записываем ее в журнал для отладки. Затем мы перерисовываем интерфейс графика, чтобы немедленно отразить любые изменения, если, например, другие части панели визуально зависят от выбранной даты истечения. На этом этапе никакой дополнительной проверки не проводится; принимается любая допустимая календарная дата.
Сохраняя эти обработчики небольшими и сфокусированными, мы гарантируем, что ComboBox и DatePicker остаются синхронизированными с текущими рыночными условиями, предотвращая случайное размещение пользователем ордера с неверной ценой или неосознанный выбор просроченной даты.
Вспомогательная функция проверки для отложенных ордеров
Прежде чем отложенный ордер будет фактически отправлен брокеру, мы проверяем, имеют ли смысл введенные пользователем данные. Эта единая вспомогательная функция обеспечивает соблюдение трех правил:
- Объем должен быть положительным. Если размер лота равен нулю или отрицателен, мы записываем ошибку в журнал и отклоняем ордер.
- Цена должна быть положительной. Неположительная цена не может использоваться для формирования действительного отложенного ордера.
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); }
Проверки рыночных условий:
Например:
- Для ордера “Buy Limit” убедиться, что цена лимита строго ниже текущей цены аск (ask).
- Для ордера “Buy Stop” убедиться, что цена стоп строго выше текущей цены аск (ask).
Если все проверки пройдены, вспомогательная функция возвращает true, указывая, что ордер может быть отправлен. Структурируя проверку таким образом, мы предотвращаем распространенные ошибки — такие как размещение лимитного ордера на покупку (Buy Limit) выше рынка или стоп-ордера на продажу (Sell Stop) на уровне или выше цены бид (bid) — и обеспечиваем немедленную, четкую обратную связь, когда введенные данные недействительны.
Обработчик нажатия кнопки “Разместить отложенный ордер” (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, ""); }
Когда пользователь нажимает кнопку “Разместить ордер”, этот обработчик собирает все необходимые входные данные:
- Выбранный тип ордера из ComboBox.
- Цену отложенного ордера из соответствующего поля ввода.
- Значения тейк-профита и стоп-лосса из их полей ввода.
- Торговый объем, повторно используемый из поля ввода объема из раздела “Быстрое исполнение” (Quick Execution).
- Дату истечения из элемента выбора даты (DatePicker).
Затем мы определяем, следует ли использовать режим GTC ((действителен до отмены) или режим с указанием конкретного срока действия, на основе того, равна ли нулю выбранная дата/время истечения. Далее мы вызываем нашу вспомогательную функцию проверки. Если какая-либо проверка не пройдена, мы выходим, не предпринимая никаких действий.
Если проверка пройдена успешно, мы вызываем ровно один из четырех методов CTrade — BuyLimit, BuyStop, SellLimit или SellStop — передавая объем, цену, символ, стоп-лосс (SL), тейк-профит (TP), режим времени и дату истечения. Каждый вызов использует введенные пользователем данные, поэтому к моменту завершения работы этого обработчика брокер получает корректный запрос на отложенный ордер. Если какой-либо параметр был недействителен, мы просто возвращаемся, полагаясь на записанные в журнал диагностические сообщения, чтобы сигнализировать о сбое.
Маршрутизация событий в 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); }
Внутри основного метода OnEvent(...) класса CTradeManagementPanel события, связанные с отложенными ордерами, маршрутизируются следующим образом:
Сначала калькулятор: Мы перенаправляем все события встроенному калькулятору. Если калькулятор обрабатывает событие (например, пользователь изменяет ввод стоимости пункта), мы останавливаемся там.
Логика отложенных ордеров:
- Если событие является “щелчком” (click) и имя объекта, по которому щелкнули, совпадает с именем кнопки отложенного ордера, мы вызываем обработчик “Разместить отложенный” (Place Pending).
- Если событие является “изменением объекта” (object change) и имя измененного объекта совпадает с именем комбобокса (combobox) или элемента выбора даты (date picker), мы вызываем соответствующий обработчик (OnChangePendingOrderType или OnChangePendingDatePicker).
- Резервный вариант: Любое другое событие передается обратно в базовый метод CAppDialog::OnEvent(...), чтобы разделы «Быстрое исполнение» (Quick Execution) и «Все операции» (All-Ops) получили возможность обработать щелчки или изменения.
Такая маршрутизация гарантирует, что взаимодействия с отложенными ордерами обрабатываются четко и изолированно, без вмешательства в работу других разделов панели.

Обновленная Панель управления торговлей (реализация ComboBox и DatePicker)
(2) Разработка класса управления ForexValuesCalculator
Перед любыми определениями классов мы подключаем пять заголовочных файлов стандартной библиотеки MQL5 из каталога Controls. Каждый из них предоставляет класс графического элемента управления, который мы будем использовать внутри CForexCalculator:
#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 мы не смогли бы вызвать dlg.Add(...) для присоединения наших меток, полей ввода и кнопок к родительскому интерфейсу.
ComboBox.mqh
Предоставляет класс CComboBox, который мы используем для выпадающего списка опций расчета. Включая этот файл, мы можем создать и управлять экземпляром CComboBox (m_dropdown), вызвать m_dropdown.Create(...) для его позиционирования, заполнить его с помощью AddItem и реагировать на CHARTEVENT_OBJECT_CHANGE, когда пользователь выбирает другой термин. Без него компилятор не знал бы, что означает CComboBox.
Определяет класс CEdit, используемый для всех полей ввода чисел и текста (например, баланс счета, процент риска, стоп-лосс, символ и т.д.). Мы динамически создаем переменное количество элементов управления CEdit внутри m_inputs[] в зависимости от выбранного термина расчета. Каждый CEdit должен быть создан, добавлен в диалог и позже преобразован обратно в CEdit с помощью GetInputValue или GetInputString. Если бы мы опустили Edit.mqh, ни один из этих вызовов не скомпилировался бы.
Подключает CLabel, который мы используем везде, где нужен статический текст на экране: метка “Опция расчета:” (m_calcOptionLabel), каждая отдельная метка ввода (для баланса счета, процента риска и т.д.) и метка “Результат:” (m_resultLabel). Каждый CLabel должен быть создан, чтобы пользователь знал, что вводить в каждое поле CEdit. Без Label.mqh мы не могли бы предоставить контекст для каждого поля ввода.
Предоставляет класс CButton. Мы используем CButton для кнопки “Рассчитать” (m_calculateButton). Включая этот заголовок, мы можем вызвать m_calculateButton.Create(...), задать цвет фона, установить текст и обнаруживать клики по ней через OnEvent. Если бы мы опустили Button.mqh, компилятор не распознал бы CButton, и мы не могли бы реагировать на клики “Рассчитать”.
План на уровне проекта для включений
В более крупном проекте у нас есть две части, которые зависят от этих элементов управления:
ForexValuesCalculator.mqh нуждается во всех пяти заголовочных файлах Controls\*.mqh, поскольку он создает самодостаточный, многократно используемый “мини-диалог” для расчета различных валютных значений. Везде, где мы используем CLabel, CEdit, CComboBox или CButton, соответствующий заголовок должен присутствовать, чтобы препроцессор MQL5 мог найти определения классов.
Группируя все включения, связанные с GUI, вместе вверху, мы гарантируем, что любой другой советник или панель (например, TradeManagementPanel.mqh) могут просто #include "ForexValuesCalculator.mqh" и немедленно получить доступ ко всем необходимым элементам управления GUI, без необходимости разбрасывать дополнительные включения повсюду.
Объявления членов класса
Класс CForexCalculator начинается с объявления нескольких элементов управления UI и структур данных, которые в совокупности образуют интерфейс калькулятора. Вверху метка (m_calcOptionLabel) и выпадающий список (m_dropdown) позволяют пользователю выбрать, какой расчет он хочет выполнить (например, размер позиции, сумма риска, стоимость пункта, прибыль/убыток или соотношение риска к прибыли). Под ними находится кнопка “Рассчитать” (m_calculateButton), по которой пользователь нажимает после того, как все входные данные установлены. Для отображения результатов доступное только для чтения поле редактирования (m_resultField) объединено с другой меткой (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[]). Внутри класс содержит пять массивов фиксированного размера структур InputField (m_positionSizeInputs, m_riskAmountInputs, m_pipValueInputs, m_profitLossInputs, m_riskRewardInputs). Каждая запись InputField содержит имя, строку метки и числовое значение по умолчанию. Наконец, m_originX и m_originY отслеживают, где начинается панель калькулятора внутри родительского диалога, в то время как m_chart_id и m_name хранят идентификатор графика и префикс для уникальных имен элементов управления. В совокупности эти члены определяют как компоновку калькулятора, так и данные, необходимые для каждого типа форекс-расчета.
Инициализация статических значений по умолчанию (InitInputs)
Метод IInitInputs выполняется один раз при создании объекта калькулятора. Он заполняет пять массивов структур InputField описательными метками и запасными числами. Например, группа “Размер позиции” включает поля для баланса счета, процента риска, стоп-лосса в пунктах и символа. Группа “Сумма риска” включает размер позиции, стоп-лосс в пунктах и символ. Каждый массив настраивается так, что позже, когда пользователь выбирает тип расчета, соответствующий массив InputField копируется в динамические элементы интерфейса. На этом этапе полю “баланс счета” присваивается значение-заполнитель по умолчанию 0.0 (которое будет заменено во время выполнения), в то время как проценты риска и значения в пунктах получают небольшие значения по умолчанию, такие как 1% или 20 пипсов. Эта статическая инициализация гарантирует, что вводные данные для каждого расчета появляются с понятными метками и некоторыми числовыми отправными точками.
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. CalculatePipValue (рассчитать стоимость пункта)
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 (Рассчитать стоимость пункта) вычисляет, сколько стоит один пункт в валюте счета для заданного символа и размера лота. Сначала он вызывает SymbolInfoDouble для получения SYMBOL_TRADE_TICK_SIZE и SYMBOL_TRADE_TICK_VALUE. Затем он выбирает либо 0.01 (для валютных пар с иеной), либо 0.0001 в качестве «размера пункта» (pip size). Если валюта прибыли пары отличается от валюты счета, он объединяет их (например, «EURUSD», если прибыль в EUR, а счет в USD), выбирает этот символ конвертации и получает его текущую цену bid в качестве курса (rate). В заключение, он делит tickValue на tickSize, умножает на pipSize, lotSize и rate и возвращает результат, округленный до двух знаков после запятой. Возврат 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); }
Имея баланс счета, процент риска и стоп-лосс в пунктах, CalculatePositionSize возвращает оптимальный размер лота. Сначала он вызывает CalculatePipValue(sym, 1.0, AccountInfoString(ACCOUNT_CURRENCY)), чтобы найти стоимость пункта для одного лота. Если любые входные данные или стоимость пункта (pv) равны нулю или отрицательны, функция возвращает 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 в лотах) и стоп-лосс в пунктах (sl), CalculateRiskAmount вычисляет, какая сумма капитала будет под риском в валюте счета. Он получает стоимость пункта для данного ps через CalculatePipValue(sym, ps, ...), а затем умножает ps × sl × стоимостьПункта. Результат нормализуется до двух знаков после запятой. Если любой из входных параметров равен нулю или отрицателен, функция возвращает 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 (Рассчитать прибыль/убыток) определяет чистую прибыль/убыток в валюте счета для заданной цены входа, цены выхода, размера лота и символа. Он вычисляет количество приобретенных или утраченных пунктов как (ценаВыхода − ценаВхода) ÷ размерПункта, где размерПункта равен 0.01 для валютных пар с иеной и 0.0001 в остальных случаях. Затем он умножает разницу в пунктах на CalculatePipValue(sym, lotSize, accountCurrency), чтобы преобразовать пункты в прибыль в валюте счета. Окончательный результат округляется до двух знаков после запятой. Если какие-либо входные числовые данные недействительны, метод возвращает 0.0.
5. CalculateRiskRewardRatio (Рассчитать соотношение риска к прибыли)
double CalculateRiskRewardRatio(double tp, double sl) { if(tp <= 0 || sl <= 0) return 0.0; return NormalizeDouble(tp / sl, 2); }
Для расчета “Соотношения риска к прибыли” пользователю нужны только тейк-профит в пунктах (tp) и стоп-лосс в пунктах (sl). При условии, что оба значения положительны, функция возвращает отношение tp / sl, округленное до двух знаков после запятой. Если любой из входных параметров равен нулю или отрицателен, она возвращает 0.0, что указывает на недопустимые данные.
Вспомогательные методы компоновки: Добавление отдельных полей (AddField)
Метод AddField отвечает за создание пары «метка + поле редактирования» для одного 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 для позиционирования левого края метки. Новая метка CLabel с именем m_name + "Lbl_" + f.name создается в точке (x0, y) с фиксированной шириной/высотой. Ее текст устанавливается равным f.label, и она добавляется в массив m_inputs[].
Затем создается CEdit в точке (x0 + CALC_EDIT_OFFSET + RESULT_BUTTON_GAP, y), чтобы все поля редактирования выравнивались согласованно. Если f.name равно "symbol", оно предварительно заполняется значением _Symbol; если "accountCurrency" — предварительно заполняется валютой счета; в противном случае оно форматирует f.defaultValue до двух знаков после запятой. Новый элемент управления редактированием добавляется в m_inputs[]. Наконец, y увеличивается на высоту элемента управления плюс CALC_CONTROLS_GAP_Y, устанавливая позицию для следующего поля. Добавляя каждую новую пару «метка+редактирование» в m_inputs[], AddField гарантирует, что они будут позже добавлены в диалог и правильно управляться.
Создание всех полей ввода для заданного термина (CreateInputFields)
Всякий раз, когда пользователь выбирает новый термин расчета (или при первоначальном создании), CreateInputFields очищает все ранее сгенерированные элементы управления (ArrayFree(m_inputs)), затем устанавливает y чуть ниже выпадающего списка. Он проверяет, какой термин был выбран: “Размер позиции” (4 поля ввода), “Сумма риска” (3), “Стоимость пункта” (3), “Прибыль/Убыток” (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, пользовательский интерфейс калькулятора создается внутри родительского диалога. Сначала сохраняются идентификатор графика, префикс имени и исходные координаты (x, y). Затем:
- Метка опции
Статическая метка m_calcOptionLabel создается в точке (x, y) с текстом “Опция расчета:”. Она располагается над выпадающим списком.
- Выпадающий список
CComboBox (m_dropdown) создается в точке (comboX + 70, y) справа от метки “Опция расчета:”. Он заполняется пятью терминами расчета. m_dropdown.Select(0) устанавливает “Размер позиции” в качестве выбранного по умолчанию.
- Кнопка “Рассчитать”
CButton (m_calculateButton) позиционируется в нижней части блока панели (с использованием вычислений btnX и btnY). На ней написано “Рассчитать”, и ей задан стально-голубой фон с белым текстом. При нажатии она будет запускать метод 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; }
Метка результата и поле
Отдельный блок с меткой “Результат:” создается справа от кнопки, а под ним сразу же размещается доступное только для чтения поле редактирования (m_resultField). Это поле отображает числовой результат выполненного расчета.
Динамические строки
Метод SetDynamicDefaults() обновляет значение баланса счета по умолчанию. Затем извлекается текущий выбранный термин (m_dropdown.Select()) и вызывается CreateInputFields(термин) для генерации соответствующих пар “метка-поле”. Наконец, UpdateResult(термин) заполняет поле результата начальным расчетом.
Поскольку выпадающий список, кнопка расчета и область результата были размещены в первую очередь, последующие динамические строки появляются между ними, все с использованием согласованных отступов. Если какой-либо вызов создания завершается неудачей, Create возвращает false, что позволяет вызывающему коду узнать, что инициализация калькулятора не была завершена.
Добавление элементов управления в родительский диалог (AddToDialog)
После успешного создания всех элементов управления в Create(...), родительский советник или панель вызывает 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 считывает текущий выбранный термин расчета (term) и, используя соответствующую комбинацию GetInputValue и GetInputString, собирает все необходимые входные данные. Например:
- Размер позиции: получить “accountBalance,” “riskPercent”, “stopLossPips” и “symbol”. Если данные действительны, вызвать CalculatePositionSize(...) и добавить к метке текст “Размер позиции (лоты)”.
- Сумма риска: получить “positionSize”, “stopLossPips” и “symbol”. Если данные действительны, вызвать CalculateRiskAmount(...) и добавить текст ”Сумма риска (USD)”
- Стоимость пункта: получить “lotSize“, “symbol“ и “accountCurrency“. Затем вызвать CalculatePipValue(...) и добавить текст “Стоимость пункта (USD)“.
- Прибыль/Убыток: получить “entryPrice“, “exitPrice“, “lotSize“ и “symbol“. Затем вызвать CalculateProfitLoss(...) и добавить текст “Прибыль/Убыток (USD)“.
- Соотношение риска к прибыли: получить “takeProfitPips“ и “stopLossPips“. Затем вызвать CalculateRiskRewardRatio(...) и добавить текст “Соотношение риска к прибыли“.
Если какой-либо входной параметр недействителен или символ не может быть выбран, метод устанавливает txt = "Result: Invalid Input" ("Результат: Недействительный ввод"). Во всех случаях он обновляет m_resultField.Text числовым результатом (res), отформатированным до двух знаков после запятой, и вызывает m_resultLabel.Text(txt), чтобы скорректировать описательный текст над ним. Этот метод гарантирует, что нажатие кнопки “Рассчитать“ или изменение выпадающего списка всегда будет обновлять как метку, так и числовое поле последним вычислением или сообщением об ошибке.
Чтение пользовательского ввода (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 возвращает необработанный текст (например, "EURUSD"), когда получает такие имена, как "symbol" или "accountCurrency". Если совпадение не найдено, они возвращают 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; }
Калькулятор обрабатывает два типа событий:
1. Изменения в выпадающем списке (CHARTEVENT_OBJECT_CHANGE)
- Если sparam совпадает с именем элемента управления выпадающего списка, мы получаем новый термин через m_dropdown.Select().
- Мы вызываем CreateInputFields(термин), чтобы перестроить все динамические пары «метка-поле» для этого термина.
- Затем, чтобы показать немедленный предварительный просмотр, UpdateResult(термин) выполняет повторный расчет, используя значения по умолчанию или существующие входные данные.
- Возврат true сообщает родительскому диалогу, что событие было обработано.
2. Нажатия на кнопку “Рассчитать“ (CHARTEVENT_OBJECT_CLICK)
- Если sparam совпадает с m_name + "CalcBtn", мы снова считываем выбранный термин и вызываем UpdateResult(термин).
- Это позволяет пользователю изменить любые входные значения (например, скорректировать стоп-лосс в пунктах), а затем нажать «Рассчитать», чтобы обновить результаты.
Любые другие события возвращают false, чтобы родительский CAppDialog (или другой код) мог обработать их при необходимости. Такое четкое разделение гарантирует, что только соответствующие взаимодействия — изменение термина или нажатие кнопки — запускают пересчет или обновление пользовательского интерфейса.
Очистка (~CForexCalculator)
~CForexCalculator()
{
for(int i = 0; i < ArraySize(m_inputs); i++)
delete m_inputs[i];
}
Когда объект калькулятора уничтожается, деструктор проходит по массиву m_inputs[], удаляя каждый динамически выделенный элемент управления (метки и поля редактирования). Это предотвращает утечки памяти. Поскольку каждый раз, когда пользователь переключает термины, CreateInputFields использует ArrayFree для удаления старых элементов управления, эти старые элементы управления должны быть удалены позже. Заключительная очистка в деструкторе гарантирует, что если вся панель калькулятора будет закрыта или советник завершит работу, все элементы управления, созданные этим классом, будут корректно освобождены.
(3) Интеграция калькулятора валютных значений в панель управления торговлей.
Интеграция CForexCalculator в CTradeManagementPanel начинается просто с объявления экземпляра класса калькулятора в качестве одного из полей-членов панели. Помещая m_calculator среди защищенных членов, мы фактически резервируем часть памяти панели для внутреннего состояния калькулятора (его выпадающего списка, меток, полей редактирования и кнопок).
CForexCalculator m_calculator;
Поскольку заголовочный файл панели уже включает заголовочный файл ForexValuesCalculator, компилятор точно знает, как устроен класс CForexCalculator и какие у него зависимости. На практике это означает, что мы не копируем элементы управления калькулятора и не изменяем порядок его кода; вместо этого мы полагаемся на композицию. Панель может обращаться с m_calculator как с любым другим элементом управления — создавать его, задавать размер, добавлять в диалог и перенаправлять ему события — не заглядывая в его закрытые члены.
#include <ForexValuesCalculator.mqh> Создание и компоновка в методе Create()
Следующий шаг происходит внутри метода Create() панели, где мы последовательно выстраиваем все четыре раздела. После компоновки раздела “Быстрое исполнение ордеров” и отрисовки первого разделителя мы переходим к разделу ”Форекс Калькулятор”, сначала отрисовывая метку заголовка раздела:
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), который перебирает все дочерние элементы управления (m_dropdown, все динамически созданные пары CLabel/CEdit, кнопку “Рассчитать“ и отображение результата) и добавляет их в родительский CAppDialog. Этот шаг регистрации крайне важен: внутренний цикл событий диалога теперь будет включать элементы управления калькулятора и отображать их в правильном порядке (z-order).
Определение размера, позиционирование и интервалы
Поддержание правильных интервалов между разделами является ключом к предотвращению визуального наложения. После добавления калькулятора мы сдвигаем наш curY на полную высоту CALCULATOR_HEIGHT плюс двойной межраздельный промежуток. Поступая так, мы гарантируем, что следующий разделитель или последующий раздел “Отложенные Ордеры“ начнутся точно под блоком калькулятора, не оставляя неопределенности относительно границ элементов управления. В процессе этой компоновки ни один элемент управления не перемещается вручную относительно другого; вместо этого последовательность отрисовки метки заголовка, создания калькулятора в известной исходной точке и последующего сдвига вертикального курсора гарантирует, что область “Калькулятор“ остается самодостаточной.
Поскольку мы определили четкие константы — CALCULATOR_WIDTH и CALCULATOR_HEIGHT — панели не нужно знать, сколько строк ввода будет отображать калькулятор. Собственная внутренняя логика калькулятора динамически регулирует размер m_inputs[], но никогда не изменяет общий зарезервированный блок. Следовательно, если в будущем мы добавим больше строк ввода (например, поле “Ставка Свопа“), калькулятор просто сместит свое поле результата вниз в пределах этой фиксированной высоты; панель может оставаться в неведении относительно этих деталей.
Перенаправление событий и приоритизация
Не менее важно управление событиями. Если пользователь взаимодействует с любым из элементов управления калькулятора — например, выбирает новый термин из выпадающего списка или нажимает кнопку “Рассчитать“, — эти события, связанные с объектами, поступают в CTradeManagementPanel::OnEvent(...).. В самом начале 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, мы продолжаем обработку других событий, специфичных для панели — таких как нажатия кнопок в разделах “Быстрый ордер“ или “Отложенные ордеры“. Таким образом, калькулятор, по сути, владеет собственным поддиалогом: он может добавлять и удалять свои динамические элементы управления, реагировать на пользовательский ввод и обновлять свое отображение, не вмешиваясь в работу других элементов управления панели и не прерываясь ими.
(4) Доработка NewAdminPanel под новые обновления
Вызов метода g_tradePanel.Run() в функции инициализации советника или в процедуре отображения панели является абсолютно критическим для корректной работы всех интерактивных элементов графического интерфейса, особенно выпадающих списков (ComboBox) и выбора даты (datepickers). Внутренне метод Run() передает управление циклу обработки событий базового класса CAppDialog, который активно отслеживает клики мыши, нажатия клавиш и другие события графика, предназначенные для дочерних элементов управления диалога. Без вызова Run() экземпляр CTradeManagementPanel просто существует в памяти, но не регистрируется в среде выполнения MQL5 как активный диалог. На практике это означает, что выбор элемента из выпадающего списка "Тип отложенного ордера" или изменение даты истечения через CDatePicker не будут генерировать необходимые события CHARTEVENT_OBJECT_CHANGE или CHARTEVENT_OBJECT_ENDEDIT, которые панель должна обрабатывать.
Сразу после вызова g_tradePanel.Run() диалог входит в свой собственный цикл обработки сообщений: каждый клик по раскрывающемуся списку или выбору даты запускает метод OnEvent(...) панели, который проверяет тип события и направляет его в OnChangePendingOrderType() или OnChangePendingDatePicker(). Короче говоря, Run() — это то, что превращает статический набор элементов управления в отзывчивый, интерактивный пользовательский интерфейс. Без него ComboBox останется застывшим на своем начальном значении, а выбор даты никогда не сгенерирует событие для обновления логики цены отложенного ордера или отображения календаря.
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() usage
Не менее важным для пользовательского опыта является разумное использование функции ChartRedraw() сразу после отображения или скрытия диалогов, а также после обновления любых визуальных элементов. Всякий раз, когда вы вызываете Show() или Hide() для диалога или отдельного элемента управления — такого как ComboBox, DatePicker или поля калькулятора — базовый холст графика должен быть перерисован, чтобы новые элементы управления появились на экране (или старые исчезли). В коде нашего советника вы можете видеть частые вызовы ChartRedraw() в обработчиках, таких как HandleTradeManagement(), ToggleInterface(), а также внутри OnEvent(...) после обработки события.
Каждый вызов ChartRedraw() заставляет MetaTrader 5 перерендерить все объекты графика и элементы графического интерфейса, гарантируя, что выпадающие списки действительно раскрываются, календари DatePicker правильно накладываются на экран, а недавно вычисленные значения в полях калькулятора становятся видимыми без мерцания или задержки. Без вызова ChartRedraw() график может оставаться "устаревшим" в течение заметной доли секунды после изменений состояния, что приводит к неотзывчивости: пользователь может щелкнуть по другому элементу выпадающего списка, но по-прежнему видеть старый выбор до следующего тика или автоматического обновления. Явно запрашивая перерисовку после каждого значительного изменения — будь то переключение видимости панели, обновление надписей или пересчет результатов — мы гарантируем неизменно плавный интерфейс реального времени, в котором выбор в ComboBox появляется мгновенно, календари DatePicker отображаются без задержки, а результаты калькулятора обновляются немедленно.
// 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() мы вызываем ChartRedraw() сразу после Show() или Hide(). Это заставляет панель появляться или исчезать мгновенно, избегая устаревшего состояния экрана, при котором панель остается скрытой или видимой до тех пор, пока не произойдет какая-либо внешняя активность на графике.
- Перерисовка после делегирования событий: Внутри OnChartEvent(...), после перенаправления события в g_tradePanel.OnEvent(...), мы снова вызываем ChartRedraw(). Если пользователь взаимодействовал с выпадающим списком калькулятора — например, выбрав "Сумма риска" — калькулятор мог воссоздать свои поля ввода или обновить метку с результатом. Последующий ChartRedraw() гарантирует, что эти новые поля ввода и метки значений будут отрисованы немедленно, предотвращая мерцание или частично отрисованные элементы интерфейса.
- Плавная обратная связь: Размещая ChartRedraw() на каждом этапе — после переключения кнопок интерфейса, после отображения/скрытия панели и после перенаправления событий подпанелям — мы гарантируем плавный и отзывчивый пользовательский опыт. Выпадающие списки открываются немедленно, всплывающие окна выбора даты появляются корректно, а недавно вычисленные значения в полях калькулятора становятся видимыми без какой-либо заметной задержки.
Теперь мы можем перейти к тестированию новых функций в следующем разделе.
Тестирование
Нижеописанное было выполнено в MetaTrader 5 после успешной компиляции. Обновленная панель TradeManagementPanel включает улучшенный процесс выставления отложенных ордеров, а также встроенный калькулятор для вычисления ключевых показателей на рынке Forex, что способствует принятию более обоснованных торговых решений. 
Проверка работы калькулятора форекс-показателей в составе TradeManagementPanel
Заключение
Это было невероятно глубокое и содержательное обсуждение, и я рад, что мы достигли нашей основной цели. Мы рассмотрели несколько важнейших концепций Форекс — расчет размера позиции, стоимости пункта (пипса), соотношения риска и прибыли и так далее — и описали лежащую в их основе математику, которую должен понимать каждый трейдер. Преобразование этих формул в код MQL5 закрепляет теорию для трейдеров, а также помогает разработчикам правильно реализовывать эти вычисления в своих собственных проектах.
Ключевым выводом из нашей работы над TradeManagementPanel стало использование виджетов из стандартной библиотеки MQL5, а именно CComboBox и CDatePicker. Используя эти элементы управления, мы улучшили компоновку и доступность связанных полей ввода, а также упростили процесс установки даты истечения для отложенных ордеров. Это обеспечивает значительную экономию времени по сравнению с ручным вводом дат и снижает вероятность ошибок со стороны пользователя.
В процессе работы мы сосредоточились на модульном дизайне: разделении калькулятора, элементов управления отложенными ордерами и кнопок быстрого исполнения на отдельные классы, которые чисто взаимодействуют друг с другом. Обеспечение правильной реакции наших ComboBox и DatePicker на события внутри советника демонстрирует надежный и многократно используемый шаблон. Каждый созданный нами компонент может быть извлечен и интегрирован в будущие проекты с минимальными правками.
Тем не менее, хотя пользовательский интерфейс теперь стабилен, все еще есть возможность доработать и оптимизировать нашу логику расчета значений. Я приветствую ваши отзывы и предложения в комментариях — ваши идеи по улучшению существующих концепций будут бесценны. Надеюсь, это занятие было для вас познавательным, и я с нетерпением жду нашей следующей публикации. Оставайтесь с нами!
Ниже представлен список всех файлов, задействованных в этом проекте:
| Прикрепленный файл | Описание |
|---|---|
| TradeManagementPanel.mqh | Содержит основную логику интерфейса торговой панели, включая управление рыночными и отложенными ордерами, расчет рисков и встроенный калькулятор трейдера. Предоставляет элементы графического интерфейса, такие как выпадающие списки, поля для выбора даты и кнопки действий, инкапсулированные в панель, производную от CAppDialog. Играет ключевую роль в обработке торговых операций и взаимодействии с пользователем. |
| ForexValuesCalculator.mqh | Реализует основной вычислительный механизм, используемый в панели управления торговлей для расчета таких параметров сделки, как стоимость пункта, маржа, объем позиции и соотношение риска и прибыли. |
| New_Admin_Panel.mq5 | Главная точка входа в советник, которая объединяет все отдельные модули (управление торговлей, коммуникации, аналитика) в единый графический интерфейс. Отвечает за создание экземпляров панелей, маршрутизацию событий, создание графических объектов и общую компоновку. Также обеспечивает плавную реакцию интерфейса с помощью частых вызовов ChartRedraw() и активирует функциональность панели через метод .Run(). |
| Images.zip | Коллекция ресурсов с растровыми изображениями, используемых для кнопок интерфейса и визуальных элементов. Включает файлы, такие как TradeManagementPanelButton.bmp, expand.bmp, collapse.bmp и другие, которые обеспечивают интерактивную обратную связь через состояния кнопок (обычное/нажатое). Эти ресурсы необходимы для визуальной идентификации и удобства использования приложения. |
| Communications.mqh | Определяет панель коммуникаций, позволяющую пользователям отправлять и получать сообщения через Telegram-бота. Включает графические компоненты для ввода учетных данных (Chat ID, токен бота) и поле для ввода сообщения. Эта панель также поддерживает будущие функции управления контактами и построена с использованием CChartCanvas, CBmpButton и CEdit. |
| AnalyticsPanel.mqh | Предоставляет аналитическую сводку на основе графика, включая оценку сигналов или отслеживание производительности. Панель интегрирована в основной советник и отображается через g_analyticsPanel. Ее структура следует тому же модульному подходу CAppDialog, что позволяет изолировать логику и расширять функциональность. |
| Telegram.mqh | Обрабатывает низкоуровневые сетевые взаимодействия и форматирование JSON, необходимые для связи с API Telegram-бота. Включает функции для отправки текстовых сообщений. Этот модуль выступает в роли внутреннего движка для панели коммуникаций. |
| Authentication.mqh | Реализует опциональную двухфакторную аутентификацию для административной панели, используя Telegram в качестве канала верификации. Отправляет подтверждения входа на указанный Chat ID и проверяет введенный пользователем пароль. Этот модуль обычно вызывается во время инициализации советника для обеспечения аутентификации пользователя и блокировки несанкционированного доступа. В настоящее время отключен, чтобы избежать повторяющихся запросов во время частого тестирования и разработки. |
Пожалуйста, сохраните все заголовочные файлы в директорию MQL5\include, а содержимое Images.zip распакуйте в папку MQL5\Images. Затем скомпилируйте файл New_Admin_Panel.mq5, чтобы запустить его в терминале MetaTrader 5.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/18289
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Знакомство с языком MQL5 (Часть 35): Освоение API и функции WebRequest в языке MQL5 (IX)
Торговые инструменты на языке MQL5 (Часть 8): Улучшенная информационная панель с возможностью перетаскивания и сворачивания
Возможности Мастера MQL5, которые вам нужно знать (Часть 69): Использование паттернов SAR и RVI
Автоматизация торговых стратегий на MQL5 (Часть 19): Envelopes Trend Bounce Scalping — Исполнение сделок и управление рисками (Часть II)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования







Будьте здоровы! Ешьте ваши Wheaties и счастливого программирования
Кейп-КоддаХ
Спасибо@CapeCoddah за все ваши отзывы и усилия, которые вы приложили - они действительно способствуют созданию более стабильной версии этого многопанельного торгового инструмента.
Я очень ценю время, которое вы тратите на изучение и выяснение ситуации.
В настоящее время я изучаю проблемы, на которые вы обратили внимание, а также буду проверять представленные вами модификации. Улучшения уже на подходе.
С наилучшими пожеланиями,
Клеменс Бенджамин
Здравствуйте,
Я пытался установить его, но ни одна кнопка не отображается, я вижу только два флажка. Я извлек файлы в папку Include, как упоминалось, и изображения были извлечены в папку images
Здравствуйте @Oluwafemi Olabisi,
Не могли бы вы предоставить скриншот, чтобы я мог помочь вам более эффективно?
Здравствуйте @Oluwafemi Olabisi,
Не могли бы вы предоставить скриншот, чтобы я мог помочь вам более эффективно?
Привет, Клеманс,
У меня есть несколько вопросов, и, возможно, вы сможете разрешить некоторые из них.
Первый - это тестер стратегий.
Когда я запускаю в нем свой советник, на тестовой машине не отображается ни один из текстов, кнопок панелей и т.д. Я заметил, что некоторые из ваших отображаются. У вас есть идеи, чем вызвана эта разница? Я планирую включить ваш советник в свой и попытаться определить, что вызывает различия.
Во-вторых, как вы связываетесь с MetaQuotes, чтобы передать им ошибки и предложения по улучшению. Я потратил немало времени на MQL5.com и не могу найти способ.
Я прикрепил здесь, как файлы были извлечены в директории INCLUDE и IMAGES соответственно.
Советник должен находиться в папке experts, а не в папке include. После того, как вы переместите его, вы должны остановить советник и перезапустить его, чтобы советник появился в панели навигатора. Это одна из вещей, которую MQ должен изменить. По крайней мере, позволить пользователям сворачивать папку, либо Indicators, либо EXperts, а затем обновлять список во время команды expand, а не останавливать Terminal и перезапускать его, а затем открывать все подкаталоги, пока не достигнете цели. Еще лучше, чтобы они делали это автоматически, когда новый исполняемый файл помещается в подкаталог.