Как создавать собственные функции в MQL
Полный разбор собственных функций для начинающего разработчика советниковЛюбой серьёзный торговый советник со временем перестаёт помещаться в одну функцию OnTick. Сначала это несколько строк проверки условий, затем — расчёт лота, работа с позициями, фильтры и логирование. Если писать всё подряд, код превращается в длинную и хрупкую конструкцию, в которой легко запутаться и ещё легче допустить ошибку. Именно на этом этапе разработчик открывает для себя главный инструмент структурирования программы — собственные функции.
Функция в MQL5 — это именованный блок кода, который выполняет одну законченную задачу и может быть вызван из любого места программы. Вместо того чтобы копировать одинаковые команды десятки раз, вы пишете их один раз, даёте блоку понятное имя и обращаетесь к нему по этому имени. Такой подход называется декомпозицией: большая задача разбивается на маленькие, каждая из которых решается отдельной функцией.
В этой статье мы подробно разберём, как объявлять собственные функции в MQL5, как работать с параметрами и возвращаемым значением, чем отличаются функции от процедур, как передавать данные по значению и по ссылке, а также рассмотрим практические примеры из реальных советников — расчёт лота, проверку нового бара и подсчёт прибыли по позициям.
Зачем торговому роботу собственные функцииПока стратегия работает с одной позицией, можно обойтись простыми проверками внутри OnTick. Но реальные советники почти всегда имеют дело с несколькими задачами: рассчитать объём, проверить условия входа, выставить уровни, вывести диагностику. Если писать всё в одном месте, функция OnTick быстро разрастается до сотен строк, и найти в ней нужный фрагмент становится всё труднее.
Собственная функция решает эту проблему. Она собирает связанные действия под одним понятным именем. Вместо десяти строк расчёта лота в середине OnTick появляется одна строка вызова CalcLot(...). Читая код, вы сразу понимаете, что происходит, не вникая в детали каждой формулы. Это первый и главный плюс — читаемость.
Второй плюс — повторное использование. Хорошо написанная функция проверки нового бара или подсчёта прибыли переносится из одного советника в другой без изменений. Вы один раз решаете задачу и больше к ней не возвращаетесь.
Третий плюс — удобство отладки. Когда логика разбита на небольшие функции, ошибку проще локализовать: вы точно знаете, какая именно функция отвечает за расчёт, а какая — за работу с позициями. Править поведение можно в одном месте, и изменение автоматически распространится на все вызовы.
Синтаксис функции в MQL5Объявление функции в MQL5 состоит из нескольких частей. Сначала указывается тип возвращаемого значения, затем — имя функции, после него в круглых скобках перечисляются параметры. Тело функции заключается в фигурные скобки.
// =============================================================================================== //// = Полный синтаксис собственной функции в MQL5 = //// =============================================================================================== //тип_возврата ИмяФункции(тип1 параметр1, тип2 параметр2) { // === Тело функции === return значение; }
Если функция не возвращает результат, а только выполняет действие, вместо типа пишут void. Такие функции часто называют процедурами. Например, функция вывода сообщения в журнал:
// =============================================================================================== //// = Функция печатает сообщение в журнал с меткой времени = //// =============================================================================================== //void LogMessage(string text) { Print(TimeToString(TimeCurrent(), TIME_DATE | TIME_MINUTES), " | ", text); }
Компилятор MQL5 должен «знать» о функции до момента её вызова. Поэтому либо объявляйте функцию выше места использования, либо добавляйте прототип — объявление без тела.
Параметры и возвращаемое значениеПараметры — это входные данные функции. Они позволяют одной и той же логике работать с разными значениями. В примере ниже функция принимает процент риска и расстояние до стоп-лосса, а возвращает уже готовый объём позиции.
Ключевое слово return завершает работу функции и передаёт результат в место вызова. Важно, чтобы тип результата совпадал с типом возврата: если функция объявлена как double, возвращать нужно число с плавающей точкой.
Параметры могут иметь значения по умолчанию. Тогда при вызове их можно не указывать — функция подставит заготовленное значение.
// =============================================================================================== //// = Параметры по умолчанию: риск можно не указывать при вызове = //// =============================================================================================== //double PositionRisk(double riskPercent = 1.0, double slPoints = 300) { // === Если параметры не заданы, используются 1% риска и 300 пунктов SL === return riskPercent * slPoints; }
Такой приём делает функции гибкими: в простых случаях вызов короткий, а при необходимости параметры легко переопределить.
Функции void и функции со значениемУсловно функции делят на два вида. Первый — те, что возвращают результат: их вызов можно поставить справа от знака присваивания, как в double lot = CalcLot(1.0, 300);. Второй — процедуры void, которые просто выполняют действие: закрывают позицию, печатают сообщение, отправляют ордер.
// =============================================================================================== //// = Процедура void: выполняет действие и ничего не возвращает = //// =============================================================================================== //void PrintBalance() { Print("Баланс счёта: ", AccountInfoDouble(ACCOUNT_BALANCE)); }
Понимание разницы помогает правильно проектировать код: если функция должна «отдать» значение — она возвращает его, если только «сделать» — остаётся void.
Практический пример: расчёт размера лотаСамый востребованный пример собственной функции в советниках — расчёт объёма позиции по заданному риску. Ниже функция принимает процент риска и расстояние до стоп-лосса в пунктах, а возвращает объём, приведённый к шагу и границам брокера.
// =============================================================================================== //// = Функция рассчитывает размер лота по риску и расстоянию до Stop Loss = //// =============================================================================================== //double CalcLot(double riskPercent, double slPoints) { double riskMoney = AccountInfoDouble(ACCOUNT_BALANCE) * riskPercent / 100.0; double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
// === Стоимость одного пункта при объёме в один лот === double pointValue = tickValue * (point / tickSize); double lot = riskMoney / (slPoints * pointValue);
// === Приводим объём к шагу и границам брокера === double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); lot = MathRound(lot / step) * step; lot = MathMax(SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN), lot); lot = MathMin(SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX), lot);
return lot; }
Обратите внимание на структуру: сначала собираются рыночные данные, затем выполняется расчёт, и только в конце объём приводится к допустимым значениям. Каждый шаг отделён мини-разделителем — так код легче читать и проверять.
Практический пример: проверка нового бараВторая частая задача — выполнять торговую логику не на каждом тике, а только при появлении новой свечи. Для этого пишут функцию, которая хранит время последнего бара в статической переменной.
// =============================================================================================== //// = Функция проверяет, появился ли новый бар на текущем таймфрейме = //// =============================================================================================== //bool IsNewBar() { static datetime lastBar = 0; datetime currentBar = (datetime)SeriesInfoInteger(_Symbol, _Period, SERIES_LASTBAR_DATE);
// === Первый вызов: запоминаем время бара и выходим === if (lastBar == 0) { lastBar = currentBar; return false; }
// === Если время изменилось — бар новый === if (currentBar != lastBar) { lastBar = currentBar; return true; }
return false; }
Функция возвращает bool, и вызывать её можно прямо в условии: if (IsNewBar()) { /* логика на новой свече */ }. Ключевое слово static сохраняет значение переменной между вызовами — именно это и позволяет сравнивать время предыдущего и текущего бара.
Практический пример: подсчёт прибыли по своим позициямТретий пример — обход всех позиций и суммирование прибыли только по сделкам робота, отобранным по Magic Number.
// =============================================================================================== //// = Функция считает суммарную прибыль позиций робота по Magic Number = //// =============================================================================================== //double TotalProfitByMagic(long magic) { double total = 0;
// === Перебираем все позиции терминала === for (int i = 0; i < PositionsTotal(); i++) { ulong ticket = PositionGetTicket(i); if (ticket == 0) continue;
// === Пропускаем чужие позиции === if (PositionGetInteger(POSITION_MAGIC) != magic) continue;
// === Накапливаем прибыль своей позиции === total += PositionGetDouble(POSITION_PROFIT); }
return total; }
Здесь хорошо видно, как функция скрывает от основной логики всю рутину: вызывающий код просто получает готовую сумму и не думает о переборе позиций.
Передача данных по значению и по ссылкеПо умолчанию параметры передаются по значению: функция получает копию, и её изменения не влияют на исходную переменную. Если же нужно, чтобы функция изменила переданные данные, используется передача по ссылке — перед именем параметра ставится знак &.
// =============================================================================================== //// = Функция заполняет массив и возвращает среднюю цену закрытия = //// =============================================================================================== //double FillAndAverage(double &closes[], int count) { double sum = 0; ArrayResize(closes, count);
// === Заполняем массив ценами закрытия === for (int i = 0; i < count; i++) { closes[i] = iClose(_Symbol, _Period, i); sum += closes[i]; }
// === Возвращаем среднее значение === return (count > 0) ? sum / count : 0.0; }
Массивы в MQL5 и так передаются по ссылке, но явный знак & делает намерение очевидным. Для обычных переменных & обязателен, если функция должна их изменить: например, чтобы вернуть сразу два результата — количество прибыльных и убыточных сделок.
Перегрузка функцийВ MQL5 можно объявлять несколько функций с одним именем, но разным набором параметров. Это называется перегрузкой. Компилятор сам выбирает нужный вариант по типам и количеству аргументов.
// =============================================================================================== //// = Перегрузка: две функции с одним именем и разными параметрами = //// =============================================================================================== //double Normalize(double value, int digits) { return NormalizeDouble(value, digits); }double Normalize(double value) { return NormalizeDouble(value, _Digits); }
Перегрузка удобна, когда одна и та же операция может выполняться с разными наборами данных. Но увлекаться ею не стоит: слишком много вариантов одного имени усложняют чтение кода.
Где хранить собственные функцииСуществует два подхода. Первый — держать функции прямо в файле советника, ниже OnTick. Это удобно для небольших проектов. Второй — выносить их в отдельный файл-библиотеку с расширением .mqh, а затем подключать через директиву #include.
Библиотечный подход хорош тем, что один набор функций можно использовать во всех своих советниках. Например, файл с расчётом лота и проверкой нового бара подключается в любой проект и сразу даёт готовые инструменты.
// === Подключаем свою библиотеку функций ===#include "MyTools.mqh"
Главное — держать функции в одном стиле и давать им говорящие имена, чтобы через полгода вы сами легко вспомнили, что делает каждая из них.
Типичные ошибки начинающихПервая и самая частая ошибка — вызов функции до её объявления без прототипа. Компилятор просто не знает о существовании функции и сообщает об ошибке. Решение — объявить функцию выше или добавить прототип.
Вторая ошибка — несовпадение типа возврата. Если функция объявлена как int, а возвращает double, компилятор предупредит о потере точности. Проверяйте соответствие типов.
Третья ошибка — забытый return. Если функция должна возвращать значение, но какая-то ветка кода не доходит до return, поведение станет непредсказуемым. Убедитесь, что все пути выполнения возвращают результат.
Четвёртая ошибка — слишком большая функция. Если внутри десятки строк и несколько условий, её стоит разбить на несколько маленьких. Каждая функция должна решать одну задачу.
Пятая ошибка — неудачные имена. Функция f1 или doIt ничего не говорит о своём назначении. Имя CalcLot или IsNewBar делает код самодокументируемым.
Отладка собственных функцийОтладка функций не сложнее, чем отладка обычного кода. Простейший приём — вывести ключевые значения в журнал с помощью Print() прямо из функции.
// === Диагностика внутри функции ===Print("CalcLot: riskMoney=", riskMoney, " lot=", lot);
Если значение не то, что ожидалось, сузьте область поиска: проверьте входные параметры, затем промежуточные вычисления и только потом результат. Разбивая расчёт на короткие шаги с мини-разделителями, вы облегчаете себе эту работу.
Ещё один приём — вызывать функцию из простого скрипта с заранее известными входами. Так вы проверите её отдельно от сложной логики советника и убедитесь, что она считает верно.
Производительность и функцииВызов функции в MQL5 — операция дешёвая, но не бесплатная. Внутри OnTick, который выполняется на каждом тике, не стоит без нужды вызывать тяжёлые функции сотни раз. Если результат функции не меняется между вызовами, посчитайте его один раз и сохраните в переменной.
Хорошая практика — выносить в функции понятные, законченные операции и не смешивать в одной функции расчёт и торговые действия. Тогда код остаётся читаемым, а производительность — предсказуемой.
Практические рекомендации новичкуНачинайте с одной небольшой функции. Вынесите в неё расчёт лота — и посмотрите, как упростился OnTick. Затем добавьте функцию проверки нового бара, следом — подсчёт прибыли. Каждый шаг проверяйте в тестере стратегий, чтобы видеть, как изменение структуры кода влияет на поведение робота.
Давайте функциям говорящие имена, следите за типами и не забывайте про return. Держите тело функции коротким и понятным, а повторяющуюся логику выносите в отдельные блоки. Так вы постепенно придёте к модульной архитектуре, где каждый файл и каждая функция отвечают за свою часть работы.
ЗаключениеСобственные функции — это фундамент аккуратного кода в MQL5. Они собирают повторяющиеся действия под понятными именами, делают советник читаемым, упрощают отладку и позволяют переиспользовать готовые решения. Функции со значением возвращают результат, процедуры void просто выполняют действие, параметры позволяют одной логике работать с разными данными, а передача по ссылке — изменять исходные переменные.
Освоив собственные функции, вы сделаете важный шаг от коротких скриптов к настоящим модульным советникам. Начните с малого: вынесите в функцию расчёт лота, затем проверку нового бара, потом подсчёт прибыли. Больше учебных материалов — на https://max.ru/channel_mqlmasters, а короткие практические разборы выходят в Телеграме: https://t.me/mqlmasters.
Владимир, опубликовал запись .
С момента публикации зафиксировано 79 просмотров. Сейчас эту запись просматривает 1 незарегистрированный пользователь.
|
|