14 октября 2019 г.

Миграция кода с MQL4 на MQL5

Опишу свой метод перехода с MQL4 на MQL5. Этот способ часто позволяет сделать и обратный переход и вообще писать одновременно для двух платформ, не добавляя себе почти никакой работы. Эта заметка предназначена тем, кто уже пишет на MQL, столкнулся с проблемой миграции и не хочет ничего терять в процессе.

Задача

Изначальная задача довольно амбициозна - не только перейти на MT5, но и сохранить как можно больше рабочего кода. И даже больше - получить возможность писать одновременно для MT4 и MT5.

Этот способ миграции упростился с выходом MT4 билд 600, где MQL4 был сильно приближен к MQL5, однако и очень старый код - не приговор, достаточно подогнать его сначала под новый MQL4. Новый... 5 лет уже как.

Разное поведение стандартных функций в 4 и 5

Например, возьмём StringTrimLeft. В 4 она возвращает модифицированную строку, а в 5 строка изменяется по месту. ArrayMinimum имеет то же число параметров, но у них другой порядок. И так далее. Во всех таких случаях достаточно сделать функцию-обёртку, которая бы вызывала соответствующую функцию в зависимости от версии MQL.

string trim_end(string s)
{
#ifdef __MQL4__
    return(::StringTrimRight(s));
#else
    ::StringTrimRight(s);
    return(s);
#endif
}

Т.к. я почти не работаю со строками, и производительность работы с ними не очень важна, я решил сделать обёртку, где поведение ближе к MQL4, это также упрощает переход с 4 на 5, когда есть код только для 4. Достаточно заменить в 4 код с StringTrimRight на код с trim_end и он становится совместим с обеими версиями MQL.

Скрипты

Скрипты в 4 и 5 почти ничем не отличаются. Если создать обёртки над стандартными функциями и подправить обработчики событий, то всё должно сразу заработать, кроме случаев, когда скрипты работают с рыночными позициям. Торговые скрипты обычно применяются для помощи в ручной работе, часто они сильно специализированы, и универсального способа работы в 4 и 5 может не быть, далее ещё к этому вернусь.

Индикаторы

Все типы линий индикатора из 4 есть и в 5, за исключением специального поведения гистограммы в 4. Но нужно учесть, что после установки буфера через SetIndexBuffer в 4 массив буфера становится серийным, однако можно сразу поменять его на обычный с помощью ArraySetAsSeries (mki#13).

Работу с буферами линий можно сразу обернуть в класс, чтобы подобные мелочи делались автоматически. Ещё лучше - сделать классы для каждого типа линии индикатора, таким образом можно не только сделать использование буферов линий идентичным в 4 и 5, но и в некоторых случаях имитировать новые линии из 5 в 4 (многоцветные линии, гистограммы, свечи и т.д., см. примеры в моих индикаторах).

В OnInit обычно устанавливаются некоторые свойства индикатора (имя, число знаков и т.п.). В старой четвёрке для каждого параметра была своя функция, теперь же почти для всего есть универсальные IndicatorSet.... Необходимо учесть некоторые отличия в 4 и 5 при установке свойств, например в 5 каждый уровень может иметь свой цвет, а в 4 только один цвет на все уровни, поэтому я, например, в обёртку добавил функцию, которая устанавливает цвет всем уровням, но перенёс функцию установки цвета отдельного уровня в блок #ifndef __MQL4__, чтобы в 4 его вообще не было.

#ifdef __MQL4__
    CBIndicator *level_color(color value) { return(set(INDICATOR_LEVELCOLOR, value)); }
#else
    CBIndicator *level_color(color value) { for (int i = 0; i < max_levels_; i++) set(INDICATOR_LEVELCOLOR, i, value); return(&this); }
    CBIndicator *level_color(int index, color value) { return(set(INDICATOR_LEVELCOLOR, index, value)); }
#endif

Обёртка функций свойств индикатора также даст возможность их считывать, в MQL такой возможности изначально нет. Для этого в обёртке кроме собственно вызова IndicatorSet... можно сохранить передаваемое значение, чтобы затем можно было его считывать из этого же индикатора. Это почти никогда не нужно, но достаётся почти бесплатно.

class CIndicatorWrapper
{
protected:
    string short_name_;

public:
    void short_name(string value)
    {
        short_name_ = value;
        IndicatorSetString(INDICATOR_SHORTNAME, value);
    }

    string short_name() const { return(short_name_); }
} _indicator;

void test()
{
    _indicator.short_name("test");
    string name = _indicator.short_name(); // "test"
}

Особенностей и различий между 4 и 5 может встретиться довольно много, придётся поработать. Причём различия есть не только в коде, программном интерфейсе, но и в поведении MT, например в загрузке истории.

Советники

Основная проблема здесь лишь в торговых функциях. Всё остальные проблемы перехода описаны выше.

Торговые функции

Самый простой способ миграции здесь - это использование библиотеки для работы в 5 с ордерами как в 4 (например, MT4Orders от fxsaber). При этом есть некоторые ограничения, например нельзя работать с неттинговыми счетами, есть небольшие ограничения при поиске ордеров, возможно что-то ещё.

Я давно перешёл на другую концепцию ведения позиций, которая может быть совместима с любой торговой платформой. В каждый момент времени советник должен выдавать лишь общую позицию со знаком по каждому рабочему символу, а подсистема ведения позиций корректируют её тем или иным способом. Помимо универсальности это также экономит расходы на открытие и закрытие ордеров. Например если работает несколько подсистем, и одна открывает некий объём по символу, а другая - закрывает тот же объём, то система в целом ничего не сделает. Этот метод требует внутреннего контроля за текущими позициями, но это того стоит.

// Суммировать позиции по всем системам
double new_pos = 0;
for (int i = 0; i < systems_count_; i++)
    new_pos += systems_[i].position();

// Скорректировать позицию
position_modify(old_pos, new_pos);

При этом появляются и другие интересные возможности, но уже совсем вне темы этой заметки.

Вызов сторонних индикаторов

Очень часто в старом коде мне встречались вызовы других индикаторов, как стандартных типа iMA, так и прочих других через iCustom. Если у вас та же фигня, то здесь есть варианты:
  1. Быстрый и относительно простой - учесть новый способ вызова этих функций, сделать обёртку для получения значений отдельных баров. Работать будет медленно, но как временное решение может сработать.
  2. Полностью перейти на новый механизм, копируя данные сразу блоками. Но так не будет обратной совместимости с 4.
  3. Перейти на ручной расчёт таких индикаторов. Стандартные индикаторы все довольно простые, формулы можно найти в инете или даже стандартной библиотеке (не особо ей доверяйте, там тоже бывают баги). С пользовательскими индикаторами, особенно чужими, может быть чуть сложнее, особенно если нет исходного кода.
Первый и третий способы можно совмещать, сделав сначала по первому варианту, а затем постепенно переходя на третий.

Алгоритм

Как-то так:
  1. Решить, какие скрипты вообще имеет смысл переносить в 5, приоритет дать тем, которые используются постоянно. Остальное можно добить потом, либо выкинуть.
  2. Использовать специфику, ставшую доступной с билда 600 четвёрки (т.н. новый MQL4), например это #property strict, другие сигнатуры обработчиков событий (init() -> OnInit() и т.п.), другая область видимости (теперь можно в каждом цикле использовать новую переменную с тем же именем: for (int i; ...)), другой порядок выполнения логических операторов (избавитесь от дурацкого || перед &&), переход с extern на input и т.д., см. также: Что нового в MQL4, Переход с MQL4.
  3. Заменить стандартные функции на обёртки. Некоторые функции будут доступны только для 4, другие - только для 5, а ещё у некоторых будут разные аргументы. Обёртки для начала можно делать лишь для того, что есть в 4. Потом добавите пятёрочные функции для удобства, чтобы можно было писать в 5 так, чтобы потом сразу работало и в 4.
  4. Обернуть вызовы внешних индикаторов, либо переписать их логику самому.
  5. Для торговых операций подключить библиотеку MT4Orders, она совместима и с 4, и с 5. Либо перейдите на более общую концепцию ведения позиций, которая будет совместима с обеими версиями MT.
  6. Обработать прочие несовместимости.
В итоге должен получиться код, работающий как в MQL4, так и в MQL5. Во время манипуляций с кодом лучше постоянно сравнивать работу индикатора со старой версией в MT4, а потом и в MT5.

Пример

Буду действовать по алгоритму. Возможно, будут какие-то отклонения, но для простого скрипта это не существенно. Разбор сложного примера не даст ничего принципиально иного и лишь запутает демонстрацию алгоритма.

MetaEditor не может компилировать один и тот же файл и как MQL4, и как MQL5, различая версию по расширению файла (mki#27). Поэтому придётся либо постоянно делать копию из файла .mq4 в .mq5 и обратно, либо связать два файла жёсткой или мягкой ссылкой, не допуская одновременного их открытия (ME не понимает такую магию и удаляет изменения, сделанные в другой открытой копии-ссылке).

После каждого изменения, особенно удачного, очень желательно делать фиксацию изменений. Если используете систему контроля версий (Git, Mercurial, Subversion...), то для вас всё просто. Иначе хотя бы делайте копии на каждом этапе, чтобы можно было вернуть изменения, да и вообще посмотреть, что изменилось.

Выбор кода

В качестве подопытного возьмём индикатор из стандартной поставки, они там все простейшие. В старых архивах нашёл индикатор StdDev, не уверен, что полностью оригинальный, сойдёт. Вот так изначально он выглядит: init.

Перед продолжением я всё-таки отформатирую код в нормальном стиле и сменю кодировку на UTF-8, вот так будет получше: format.

Переход на новый MQL4 (build 600+)

Настоятельно рекомендую во всех условиях if и всех логических формулах проставить скобки для явного указания приоритета. В старом MQL4 другой порядок выполнения операторов || и &&.

a && b || c    =>    a && (b || c)    // старый MQL4
a && b || c    =>    (a && b) || c    // новый MQL4 и MQL5

Здесь это не потребовалось, но напомнить не помешает.

Далее:
  • удаляю переменную nLimit, т.к. компилятор ругается на неё как на ненужную (variable 'nLimit' not used)
  • добавляю #property strict для включения режима нового MQL4 и исправляю код на основе предупреждения о преобразовании типов (implicit conversion from 'number' to 'string')
Почти ничего не изменилось, но фиксирую этот момент: minimal new mql4, remove warning (немного напутал и разбил этап на два коммита).
  • меняю extern на input
  • меняю названия обработчиков событий:
    • int init() меняю на void OnInit(), убрав ненужный здесь возврат результата
    • int start() меняю на "длинный" вариант int OnCalculate(), т.к. только он доступен в MQL4, меняю логику возврата результата в конце (возвращаю число рассчитанных баров, что при успехе равно rates_total)
Проверяю в MT4, оно всё ещё работает, изменения: new mql4.

Забыл ещё про #property indicator_plots 1, добавлю позже. В 4 оно проигнорируется, а в 5 без него будут проблемы.

Обёртки

Надо собрать все использования стандартных функций, массивов, констант и обернуть их в свои функции, либо как-то ещё изменить код, чтобы он одинаково работал в 4 и 5.

Сначала займусь тем, что находится в OnInit, а это инициализация буфера линии, установка названия индикатора и прочее. Вот это всё надо завернуть:

SetIndexBuffer(0, ExtStdDevBuffer);
SetIndexStyle(0, DRAW_LINE);
SetIndexShift(0, ExtStdDevShift);
IndicatorShortName(sShortName);
SetIndexLabel(0, sShortName);
SetIndexDrawBegin(0, ExtStdDevPeriod);

Хотя в таком простом случае достаточно было бы сделать условную компиляцию, написав разный код для для 4 и 5, всё-таки лучше приучить себя сразу делать так, как будет удобнее в дальнейшем. Обёртки проще выделять в отдельный mqh (или просто копировать) и использовать повторно в другом коде.

Для IndicatorShortName возможна замена на функцию IndicatorSetString(INDICATOR_SHORTNAME, sShortName), которая поддерживается в обеих версиях MQL.

SetIndexBuffer также есть в обеих версиях, но с немного разными параметрами, что здесь не важно, однако в 4 после вызова этой функции меняется серийность буфера линии. Чтобы минимально вмешиваться в остальной код я установлю серийность и для 5 (обычно я поступаю наоборот). Для всего остального придётся писать обёртки, т.к. функций PlotSet... в четвёрке нет.

Здесь можно пойти другим путём, пример с обёрткой ещё попадётся. Вместо обёртки можно написать вариант функции из четвёрки для пятёрки, например так:

#ifndef __MQL4__
void SetIndexLabel(int index, string text)
{
    PlotIndexSetString(index, PLOT_LABEL, text);
}
#endif

Пишу для 5 #ifndef __MQL4__, т.к. если вдруг появится что-то новое, то больше шансов, что оно будет походить на MQL5, а не на MQL4.

Такой вот кусочек изменений получился: OnInit.

Далее принимаюсь за OnCalculate:
  • константы Bars в 5 нет, но есть функция, делающая то же самое и доступная и в 4, Bars(_Symbol, _Period), однако в данном случае (внутри OnCalculate) можно просто взять rates_total
  • IndicatorCounted в 5 нет, но в OnCalculate её заменяет аргумент prev_calculated
Компилируется в 4, работает, фиксирую: fix standard functions.

Вызов индикаторов

Вот он, мерзкий вызов внешнего индикатора:

dMovingAverage = iMA(NULL, 0, ExtStdDevPeriod, 0, ExtStdDevMAMethod, ExtStdDevAppliedPrice, i);

Как выше писал, есть два-три рабочих варианта, здесь для простоты сделаю вариант с побаровым расчётом. Как вариант, можно сделать наоборот - для 4 написать обёртку с расчётом массива значений. Но тогда придётся изменить и вызывающий код. В конечном итоге этот вариант будет лучше, но в тех случаях, когда код исходного индикатора большой, лучше для начала сделать всё попроще и лишь потом заниматься оптимизациями. Т.к. в 5 есть функция iMA с другим поведением, теперь придётся написать настоящую обёртку, назову её просто MA.

double MA(string symbol, ENUM_TIMEFRAMES timeframe, int ma_period, int ma_shift, ENUM_MA_METHOD ma_method, int applied_price, int shift)
{
#ifdef __MQL4__
    return(iMA(symbol, timeframe, ma_period, ma_shift, ma_method, applied_price, shift));
#else
    static int ma_handle = INVALID_HANDLE;
 
    if (ma_handle == INVALID_HANDLE)
        ma_handle = iMA(symbol, timeframe, ma_period, ma_shift, ma_method, applied_price);
 
    if (ma_handle == INVALID_HANDLE)
        return(0);

    double values[];
    return(CopyBuffer(ma_handle, 0, shift, 1, values) == 1 ? values[0] : 0);
#endif
}

CopyBuffer для каждого бара - это очень плохо. При первой же возможности лучше сделать "обратную" обёртку.

Компилируется, работает, изменения: ma wrapper.

Стандартные массивы Close, Open...

В 5 их как бы нет. Как вариант, можно заменить на iClose, iOpen и т.п. Но здесь есть возможность получше. Дело в том, что в OnCalculate передаются массивы close, open и подобные, которые можно использовать вместо Close, Open и остальных. Т.к. расчёт с обращением к ним происходит в отдельной функции, нужно передать ссылки на эти массивы, после чего использовать их в этой функции.

Компилируется теперь и в 4, и в 5, но не работает в 5. Дело в том, что эти массивы-аргументы в 5 не серийные, это надо исправить в начале OnCalculate. Кроме того, все эти массовые CopyBuffer, кажется, не очень хорошо влияют на расчёты, почему-то нулевой бар оказывается нерассчитанным, машка на первом проходе получается нулевая (баг?), поэтому добавлю проверку:

if (dMovingAverage == 0.0)
    return(rates_total - i - 1);

Так в случае ошибки расчёта MA будет возвращено уже рассчитанное число баров, после чего индикатор будет вызван снова, и расчёт пойдёт дальше.

После этого изменения индикатор заработал в MT5, что и требовалось. Последние изменения: price arrays, ma check.

P.S.

Примеры классов-обёрток для наборов стандартных функций можно посмотреть в коде любого моего индикатора в папке BSL (например, в коде индикатора VP). Некоторые из них довольно простые (например, terminal.mqh, event.mqh), другие же набиты дополнительными функциями, кэшами, макросами и т.п. Если стоит задача не усложнять всё без причины, я бы посоветовал создавать обёртки самому и только для того, что будет использоваться, создавать свою библиотеку.

Надеюсь, эта информация поможет тем, кто вообще не знает, с какого конца взяться за эту проблему.

2 комментария:

  1. В сети есть примеры, как переводят советники в MT5 без какого-либо изменения исходного кода.

    "Помимо универсальности это также экономит расходы на открытие и закрытие ордеров. Например если работает несколько подсистем, и одна открывает некий объём по символу, а другая - закрывает тот же объём, то система в целом ничего не сделает."

    Нет никакой экономии при таком подходе. Этим подсистемам свои "открытые" позиции надо будет потом "закрыть".

    ОтветитьУдалить
    Ответы
    1. Да, макросами наверно можно натрюкачить так, что ничего почти менять не нужно будет, это не мой подход.

      В идеале, если две подсистемы одновременно открылись и закрылись, то экономия будет. Т.е. "мой" подход будет не хуже. Синхронность иногда можно даже навязать. Конечно, это всё не касается систем с отложенными ордерами (надо было об этом сразу упомянуть), там уже проще использовать MT4Orders.

      Удалить