Логин: Пароль:
Забыли пароль?Зарегистрироваться
›
745

Локальные и глобальные переменные в советнике

Локальные и глобальные переменные в советнике

Полный разбор области видимости переменных в 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 незарегистрированных пользователя.
Добавить фото Добавить файл
Владимир

Трейдер-программист с 2006 г.
Автоматизирую торговые стратегии для MetaTrader 4 и 5.


Регистрация на проекте: 20.06.2010
Написал комментариев: 65
Записей в блоге: 384
Подписчиков: 745
Сайт: trading-go.ru/

Содержание блога:
Форекс-объявления:

Показано форекс-объявлений:
в декабре: 11 033 625;
вчера: 1 038 695 на 260 сайтах;
Разместить форекс-объявление
 Forex Magazine © 2004-2026