Локальные и глобальные переменные в советнике
Полный разбор области видимости переменных в MQL5 для начинающего разработчикаКогда торговый советник растёт, всё чаще возникает один и тот же вопрос: где объявить переменную, чтобы она вела себя предсказуемо? Ответ зависит от области видимости (scope) — то есть от того, из какого места программы переменную можно видеть и как долго она существует. Понимание этого механизма отделяет аккуратный, надёжный робот от кода, который «иногда работает, а иногда нет». В этой статье мы подробно разберём локальные и глобальные переменные программы, особенность функции OnTick, отличие от глобальных переменных терминала и типичные ошибки, которые встречаются у новичков.
Что такое область видимости переменнойОбласть видимости определяет, в каких местах кода доступна переменная. В MQL5, как и в языке C, действует правило блочной видимости: переменная видна внутри того блока, где она объявлена, и во всех вложенных блоках. Блок — это код, заключённый в фигурные скобки: тело функции, ветка условия, тело цикла.
Из этого правила следуют два следствия. Первое: переменная, объявленная внутри функции, недоступна за её пределами — компилятор просто не знает о ней в другом месте. Второе: две переменные с одинаковым именем в разных блоках никак не связаны между собой. Область видимости — это не про тип данных и не про значение, а именно про то, где имя переменной «действует».
Глобальные переменные программыГлобальные переменные программы объявляются вне функций — обычно в самом начале файла советника. Их видит любая функция, а существуют они всё время, пока советник загружен в память терминала. Именно поэтому здесь удобно хранить настройки, счётчики и состояние робота.
// =============================================================================================== //// = Глобальные переменные программы: доступны всем функциям советника = //// =============================================================================================== //int g_openedTrades = 0; // счётчик открытых роботом сделокdouble g_lastTradePrice = 0.0; // цена последней сделкиbool g_enteredThisBar = false; // флаг входа в текущем баре
// === Функция увеличивает счётчик сделок ===void RegisterTrade(double price) { g_openedTrades++; g_lastTradePrice = price; Print("Сделок открыто: ", g_openedTrades, ", цена: ", g_lastTradePrice); }
Здесь переменные g_openedTrades, g_lastTradePrice и g_enteredThisBar объявлены вне функций, поэтому любые изменения, сделанные внутри RegisterTrade, сохраняются и видны другим функциям. Префикс g_ — популярное соглашение об именовании, которое сразу подсказывает, что переменная глобальная.
Локальные переменныеЛокальные переменные объявляются внутри функции или блока. Они создаются в момент входа в блок и уничтожаются при выходе из него. Вне этого блока к ним обратиться нельзя.
// =============================================================================================== //// = Локальные переменные живут только во время выполнения функции = //// =============================================================================================== //double CalcRiskMoney(double percent) { double balance = AccountInfoDouble(ACCOUNT_BALANCE); // локальная double riskMoney = balance * percent / 100.0; // локальная
// === Возвращаем результат, локальные переменные прекращают существование === return riskMoney; }
Переменные balance и riskMoney существуют только внутри CalcRiskMoney. Это хорошо: они не засоряют пространство имён и не могут быть случайно изменены из другого места программы. Обратная сторона — их значение нигде не сохраняется.
Почему в OnTick нельзя обойтись локальными переменнымиГлавная особенность советника — в том, как работает функция OnTick. Она вызывается терминалом заново на каждом новом тике. Каждый такой вызов — отдельный проход: вошли в функцию, выполнили код, вышли. Значит, обычная локальная переменная внутри OnTick при каждом вызове создаётся заново.
Посмотрите на типичную ошибку новичка:
// === НЕПРАВИЛЬНО: счётчик всегда покажет 1 ===void OnTick() { int counter = 0; // создаётся заново на каждом тике counter++; Print(counter); // всегда 1 }
Значение counter сбрасывается на каждом тике, поэтому счётчик бесполезен. Счётчики открытых сделок, флаг «уже входили в этом баре», время последней сделки — всё это относится к состоянию робота, которое должно пережить вызов функции. Хранить такое состояние в обычной локальной переменной нельзя: на следующем тике оно обнулится.
Как сохранить состояние: static-переменныеПервое решение — ключевое слово static. Статическая переменная объявляется внутри функции, но, в отличие от обычной локальной, создаётся один раз и сохраняет значение между вызовами. При этом снаружи функции её по-прежнему не видно.
// =============================================================================================== //// = static-переменная помнит значение между вызовами OnTick = //// =============================================================================================== //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; }
Здесь lastBar — статическая переменная. Она сохраняет время последнего бара между вызовами, и именно поэтому функция может определить появление новой свечи. Если бы lastBar была обычной локальной переменной, она каждый раз обнулялась бы и сравнение теряло смысл.
Поля класса как альтернативаВторое решение — хранить состояние в полях класса. Когда советник оформлен как класс, его поля существуют вместе с объектом и доступны всем методам. Это более структурированный подход, чем разрозненные глобальные переменные.
// =============================================================================================== //// = Класс хранит состояние робота в своих полях = //// =============================================================================================== //class CTradeState {private: int m_trades; // число открытых сделок datetime m_lastTrade; // время последней сделки bool m_enteredBar; // входили ли в текущем баре
public: CTradeState() { m_trades = 0; m_lastTrade = 0; m_enteredBar = false; }
// === Метод обновляет состояние при открытии сделки === void OnTradeOpened() { m_trades++; m_lastTrade = TimeCurrent(); m_enteredBar = true; } };
Поля класса m_trades, m_lastTrade и m_enteredBar живут столько, сколько живёт объект класса. Такой код проще читать: видно, какие данные относятся к состоянию робота, а какие — временные.
Глобальные переменные терминала — это другой механизмОтдельно существуют глобальные переменные терминала. Несмотря на похожее название, это совершенно другой инструмент. Они хранятся не в памяти советника, а в самом терминале MetaTrader, доступны разным программам и переживают перезапуск. Для работы с ними есть отдельные функции: GlobalVariableSet, GlobalVariableGet, GlobalVariableCheck, GlobalVariableDel.
// =============================================================================================== //// = Глобальные переменные терминала: хранятся в терминале и переживают перезапуск = //// =============================================================================================== //void SaveState() { GlobalVariableSet("MyEA_Trades", 5); // записать значение }
// === Чтение значения с проверкой существования ===int ReadTrades() { if (GlobalVariableCheck("MyEA_Trades")) return (int)GlobalVariableGet("MyEA_Trades"); return 0; }
// === Удаление переменной терминала ===void ClearState() { GlobalVariableDel("MyEA_Trades"); }
Важно не путать эти два уровня. Глобальная переменная программы (g_openedTrades) существует только пока советник загружен, и её не видит никакая другая программа. Глобальная переменная терминала (MyEA_Trades) живёт в терминале, её видит любой советник и скрипт, и она сохраняется после перезапуска. Первые — для состояния внутри одного робота, вторые — для обмена данными и долговременного хранения.
Инициализация переменныхГлобальные переменные программы инициализируются один раз — в момент загрузки советника, ещё до первого вызова OnTick. Если при объявлении не задать значение, целочисленные переменные получат ноль, вещественные — тоже ноль, а строки останутся пустыми. Но полагаться на это не стоит: начальные значения лучше присваивать явно, чтобы намерение было очевидным.
Чего делать нельзя, так это инициализировать глобальную переменную значением, которое зависит от рыночных данных, прямо в строке объявления. Записать в глобальную переменную текущую цену инструмента при объявлении не получится надёжно: на момент запуска торговые данные могут быть ещё не готовы. Для таких значений служит функция OnInit — она вызывается при инициализации советника, в том числе после смены настроек и таймфрейма.
// =============================================================================================== //// = Инициализация глобального состояния в OnInit, а не при каждом тике = //// =============================================================================================== //datetime g_sessionStart = 0;double g_startPrice = 0.0;
// === OnInit вызывается один раз при загрузке советника ===int OnInit() { g_sessionStart = TimeCurrent(); g_startPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); Print("Старт сессии: ", g_sessionStart, ", цена: ", g_startPrice); return INIT_SUCCEEDED; }
Такой подход гарантирует, что начальные значения вычисляются один раз и корректно, а не заново на каждом тике.
Префиксы и правила именованияАккуратные префиксы почти полностью снимают риск перекрытия имён. Договоритесь с собой о простых правилах: глобальные переменные программы начинайте с g_, поля класса — с m_, а параметры функций и локальные переменные оставляйте без префикса. Тогда даже в большом проекте по одному имени видно, где и как долго живёт переменная.
Полезно отражать в имени смысл, а не тип. Переменная g_tradesOpened читается лучше, чем g_count, а m_lastTradeTime понятнее, чем m_time. Компилятор не подскажет, что g_count считает именно сделки, — а вам через полгода это может понадобиться.
И наоборот, никогда не давайте двум сущностям одно имя. Именно так и рождается эффект перекрытия: локальная переменная с именем глобальной молча скрывает её внутри функции, и вы меняете не то, что думали.
Типичные ошибки при работе с переменнымиПерекрытие имён (shadowing)Если локальная переменная названа так же, как глобальная, локальная «затеняет» глобальную внутри своей функции. Это классическая ловушка.
// === ОПАСНО: локальная переменная перекрывает глобальную ===int g_count = 100;
void Shadow() { int g_count = 0; // эта переменная перекрывает глобальную g_count++; // изменяется только локальная копия, глобальная остаётся 100 }
Чтобы избежать путаницы, придерживайтесь префиксов g_ для глобальных и m_ для полей класса, а локальным переменным давайте говорящие имена.
Случайное обнуление состоянияСамая частая ошибка — держать состояние робота в обычной локальной переменной внутри OnTick. Значение сбрасывается на каждом тике, и робот ведёт себя непредсказуемо. Всегда проверяйте: должна ли эта переменная помнить, что было на предыдущем тике? Если да — она обязана быть глобальной, статической или полем класса.
Конфликты при перезапускеЕсли хранить состояние в глобальных переменных программы, при перезапуске советника они сбрасываются. Если робот уже имел открытые позиции, его «внутренняя память» о них пропадёт. В таких случаях состояние лучше восстанавливать по фактическим позициям терминала или хранить в глобальных переменных терминала. При этом помните про конфликты: два экземпляра советника с одинаковыми именами глобальных переменных терминала будут мешать друг другу.
Глобальные переменные там, где нужны поля классаРазрозненные глобальные переменные удобны в маленьком советнике, но плохо масштабируются. Когда их становится десятки, отследить, кто и когда их изменяет, очень трудно. Если состояние логически связано с объектом стратегии, его стоит держать в полях класса, давая осмысленные имена.
Практические примерыСчётчик открытых сделокРоботу часто нужно ограничить число одновременных позиций. Надёжнее не хранить счётчик в памяти, а восстанавливать его из фактического состояния терминала.
// =============================================================================================== //// = Считаем число позиций робота по Magic Number = //// =============================================================================================== //int CountOwnPositions(long magic) { int count = 0;
// === Перебираем позиции терминала === for (int i = 0; i < PositionsTotal(); i++) { if (PositionGetTicket(i) == 0) continue; if (PositionGetInteger(POSITION_MAGIC) == magic) count++; }
return count; }
Такой счётчик не теряет данные при перезапуске, потому что каждый раз берётся из реальных позиций, а не из переменной в памяти.
Флаг «уже входили в этом баре»Часто нужно входить не более одного раза за свечу. Флаг должен сбрасываться при появлении нового бара и сохраняться между тиками, поэтому его держат глобально.
// === Глобальный флаг входа в текущем баре ===bool g_enteredThisBar = false;
void OnTick() { static datetime lastBar = 0; datetime currentBar = (datetime)SeriesInfoInteger(_Symbol, _Period, SERIES_LASTBAR_DATE);
// === Новый бар — сбрасываем флаг === if (currentBar != lastBar) { lastBar = currentBar; g_enteredThisBar = false; }
// === Вход только один раз за бар === if (!g_enteredThisBar) { // === ... здесь логика входа === g_enteredThisBar = true; } }
Время последней сделкиЕщё одно частое состояние — время последней сделки. Оно позволяет выдерживать паузу между входами.
// === Хранение времени последней сделки между тиками ===datetime g_lastTradeTime = 0;
// =============================================================================================== //// = Проверяем, прошёл ли заданный интервал с момента последней сделки = //// =============================================================================================== //bool CanTrade(int minSeconds) { return (TimeCurrent() - g_lastTradeTime >= minSeconds); }
Переменная g_lastTradeTime объявлена глобально, поэтому значение не теряется между тиками. После открытия очередной сделки достаточно присвоить ей TimeCurrent().
Что выбрать: глобальные переменные, поля класса или staticНебольшой советник вполне обходится глобальными переменными программы — это просто и наглядно. Когда логика усложняется, состояние стоит переносить в поля класса: так данные сгруппированы и защищены от случайных изменений. Статические переменные идеальны для небольших локальных «памятей» внутри одной функции, например для времени последнего бара. А для обмена данными между программами и хранения, не зависящего от жизненного цикла советника, используйте глобальные переменные терминала.
ЗаключениеОбласть видимости — это фундамент, на котором держится надёжность советника. Глобальные переменные программы видны всем функциям и живут, пока робот загружен. Локальные существуют только во время выполнения блока. Из-за того, что OnTick вызывается на каждом тике заново, состояние — счётчики, флаги, время — нельзя держать в обычных локальных переменных; для этого подходят глобальные переменные, статические переменные и поля класса. А глобальные переменные терминала — отдельный механизм для межпрограммного обмена и долговременного хранения.
Разберитесь с этими понятиями один раз, и вы перестанете удивляться «магическому» поведению робота: код станет предсказуемым, а ошибки с обнулением состояния исчезнут. Больше учебных материалов — на https://max.ru/channel_mqlmasters, а короткие практические разборы выходят в Телеграме: https://t.me/mqlmasters.
#MQL5 #переменные #программирование #MetaTrader5 #MQLMaster
Владимир, опубликовал запись .
С момента публикации зафиксировано 77 просмотров. Сейчас эту запись просматривают 2 незарегистрированных пользователя.
|
|