English 中文 Español Deutsch 日本語 Português
Готовые советники из Мастера MQL5 работают в MetaTrader 4

Готовые советники из Мастера MQL5 работают в MetaTrader 4

MetaTrader 5Интеграция | 9 марта 2017, 12:12
7 092 9
Stanislav Korotky
Stanislav Korotky

Клиентские терминалы MetaTrader 4 и MetaTrader 5 предоставляют своим пользователям возможность легко создавать прототипы программ на языке MQL с помощью встроенного Мастера (MQL Wizard). Мастера обоих версий терминалов очень похожи, но все же имеют одно важное отличие. В Мастере MetaTrader 5 есть пункт генерации готовых советников, а в MetaTrader 4 его нет. Дело в том, что такие советники работают на основе классов стандартной библиотеки MQL, т.е. набора заголовочных файлов, поставляемых вместе с терминалом. В MetaTrader 4 такая библиотека тоже есть, но в ней нет торговых классов из MQL5. В частности, там отсутствуют классы, ответственные за подготовку и отправку торговых приказов, вычисление сигналов на основе показаний индикаторов или структуры цен, трейлинга, управления деньгами, а все это — необходимая база для построения автоматически генерируемых советников.‌

Такая ситуация сложилась исторически в результате поэтапного развития MQL5. Новый язык появился изначально в MetaTrader 5, и именно для этого терминала была разработана стандартная библиотека классов. Только некоторое время спустя MQL5 был интегрирован и в MetaTrader 4, но поскольку торговые функции в API двух версий терминалов сильно разнятся, стандартная библиотека была перенесена в более ранний продукт не полностью — без торговых классов. В результате в Мастере MetaTrader 4 нет опции генерации готовых советников.

Вместе с тем, MetaTrader 4 до сих пор популярен, и возможность генерации готовых советников для него очень пригодилась бы. Поскольку новые функции в MetaTrader 4 больше не добавляются, а лишь вносятся исправления ошибок, мы вряд ли уже увидим усовершенствования в его Мастере. Однако нам никто не мешает использовать Мастер MetaTrader 5, а затем переносить полученный код в MetaTrader 4. Чтобы этот код заработал и там, нужна лишь малость — набор торговых классов стандартной библиотеки, адаптированных для прежнего MQL API MetaTrader 4. Иными словами, нужно скопировать из стандартной библиотеки MetaTrader 5 те классы, которых недостает в MetaTrader 4, и реализовать для них эмуляцию торгового окружения пятой версии.

Для понимания приведенного далее материала необходимо знать принципы торговых операций в MetaTrader 5, суть работы ордеров (приказов), сделок и позиций. Если Вы не знакомы с этой версией терминала, настоятельно рекомендуется прочитать статью "Ордера, позиции и сделки в MetaTrader 5".

Планирование

Любую работу лучше выполнять, придерживаясь заранее составленного плана. Пример такого подхода, который применяется при разработке программ, — так называемая модель водопада. Она хорошо подходит для нашего случая, когда разработка не только выполняется как таковая, но еще и описывается в статье вроде этой. Однако на практике портировать код MQL5 в MQL4 (или обратно) эффективнее с применением одного из гибких подходов, таких как экстремальное программирование. Его девиз: меньше планов — больше дела. Буквально это значит, что можно взять исходный код, попытаться откомпилировать его и затем последовательно править все возникающие ошибки. План, который я предлагаю в этой статье, родился не сразу, а постепенно, как раз на основе "подсказок" недовольного компилятора.‌

При сравнении библиотек двух терминалов легко заметить, что в версии 4 не хватает папок Trade, Expert, и Models. Значит, основная работа будет заключаться в портировании всех имеющихся в этих папках классов под "четверку". Помимо них, нам, очевидно, потребуется  подправить что-то и в папке Indicators. Она есть в библиотеке версии 4, но принципы работы с индикаторами в двух терминалах отличаются. Однако в любом случае, следует придерживаться принципа наименьших правок файлов библиотеки, ведь периодически она  обновляется, и тогда нужно будет синхронизировать, подгонять наши правки под официальные.

Все скопированные файлы в той или иной степени ссылаются на торговые MQL API пятой версии. Поэтому нам придется разработать более или менее полный набор определений и функций, которые, сохранив тот же программный интерфейс, будут преобразовывать все обращения к унаследованным MQL API четвертой версии. Рассмотрим подробнее, что именно должно войти в эмулируемое торговое окружение. Начнем с типов, так как это кирпичики, на которых будет затем строиться здание, т.е. алгоритм и программа целиком.‌

Самые простые типы — перечисления. Они используются в большинстве функций напрямую или опосредованно, через структуры. Поэтому порядок адаптации будет такой: перечисления, структуры, константы, функции.

Перечисления

Часть необходимых перечислений уже перенесена в MetaTrader 4. Это, например, свойства ордеров: ENUM_ORDER_TYPE, ENUM_ORDER_PROPERTY_INTEGER, ENUM_ORDER_PROPERTY_DOUBLE, ENUM_ORDER_PROPERTY_STRING. С одной стороны, это вроде бы удобно, но с другой, не все из этих перечислений определены точно так же, как в MetaTrader 5, и это создает сложности.

Например, ENUM_ORDER_TYPE в MetaTrader 5 содержит больше типов ордеров, чем в MetaTrader 4. Если оставить ENUM_ORDER_TYPE как есть, мы получим ошибки компиляции, поскольку скопированный код ссылается на отсутствующие элементы. Переопределить или доопределить перечисление невозможно. Поэтому наиболее простой вариант — макроопределение для препроцессора, вроде такого:

// ENUM_ORDER_TYPE extension
#define ORDER_TYPE_BUY_STOP_LIMIT ((ENUM_ORDER_TYPE)6)
#define ORDER_TYPE_SELL_STOP_LIMIT ((ENUM_ORDER_TYPE)7)
#define ORDER_TYPE_CLOSE_BY ((ENUM_ORDER_TYPE)8)

Другие перечисления, которых нет в MetaTrader 4, мы можем смело определить по аналогии с "пятеркой", например:

enum ENUM_ORDER_TYPE_FILLING
{
  ORDER_FILLING_FOK,
  ORDER_FILLING_IOC,
  ORDER_FILLING_RETURN
};

Таким образом, нам следует определить (или дополнить константами) указанные ниже перечисления. Их, на первый взгляд много, но работа тривиальная — достаточно скопировать их из документации (ссылки на соответствующие разделы приведены ниже; звездочкой помечены существующие перечисления, требующие "шлифовки").

  • Приказ (Order)
    • ENUM_ORDER_TYPE_TIME
    • ENUM_ORDER_STATE
    • ENUM_ORDER_TYPE_FILLING
    • ENUM_ORDER_TYPE (*)
    • ENUM_ORDER_PROPERTY_INTEGER (*)
    • ENUM_ORDER_PROPERTY_STRING (*)
  • Позиция (Position)
    • ENUM_POSITION_TYPE
    • ENUM_POSITION_PROPERTY_INTEGER
    • ENUM_POSITION_PROPERTY_DOUBLE
    • ENUM_POSITION_PROPERTY_STRING
  • Сделка (Deal)
    • ENUM_DEAL_ENTRY
    • ENUM_DEAL_TYPE
    • ENUM_DEAL_PROPERTY_INTEGER
    • ENUM_DEAL_PROPERTY_DOUBLE
    • ENUM_DEAL_PROPERTY_STRING
  • Типы торговых операций
    • ENUM_TRADE_REQUEST_ACTIONS

MetaTrader 4 уже содержит определения перечислений, описывающих символы, такие как ENUM_SYMBOL_INFO_INTEGER, ENUM_SYMBOL_INFO_DOUBLE, ENUM_SYMBOL_INFO_STRING. Некоторые элементы в них лишь зарезервированы, но не работают (что отражено в документации). Это ограничения платформы MetaTrader 4 по сравнению с MetaTrader 5, и мы должны это принять как есть. Для нас лишь важно, что данные перечисления не нужно определять в самом проекте.‌

Структуры

Помимо перечислений, в торговых функциях MetaTrader 5 используются структуры. Их определения тоже можно взять в документации (ссылки на соответствующие разделы приведены ниже).

Макроопределения

В дополнение к перечисленным выше типам, исходные коды "пятерки" используют множество констант, которые в этом проекте проще всего определить с помощью директивы #define препроцессора.

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

Последний и самый важный пункт нашего плана — непосредственно торговые функции. Мы сможем приступить к их реализации только после того, как будут определены все вышеперечисленные типы и константы.

Список торговых функций довольно внушительный. Их можно разбить на 4 группы:

  • Ордера
  • Позиции
  • История ордеров
  • История сделок

Наконец, нам пригодятся такие простые подстановки как:

#define MQL5InfoInteger MQLInfoInteger
#define MQL5InfoString  MQLInfoString

Это, по сути, одни и те же функции ядра терминала, но их имена слегка отличаются в MQL5 и MQL4.

Прежде чем приступить непосредственно к реализации, мы должны придумать, каким образом отображать торговую модель MetaTrader 5 на торговую модель MetaTrader 4.

Отображение

Попробуем провести параллель между сущностями MetaTrader 5 и MetaTrader 4. Начать легче с "четверки". Там есть одно универсальное понятие "ордер", и оно используется практически для всего — для рыночных ордеров, отложенных ордеров, истории торговых операций. Во всех этих случаях ордер находится в разных состояниях. В "пятерке" рыночные ордера — это позиции, отложенные ордера — просто ордера, а история операций записывается с помощью сделок.‌

В простейшем случае MetaTrader 5 работает примерно так. Для формирования позиции на торговый сервер отправляется приказ о входе в рынок. Для закрытия позиции поступает другой приказ, о выходе из рынка. Исполнение каждого из приказов происходит в рамках соответствующей сделки, которая попадает в историю торговли. Таким образом, один рыночный ордер "четверки" должен отображаться в эмулируемом "пятерочном" торговом окружении как:

  • ордер входа
  • сделка входа
  • позиция
  • ордер выхода
  • сделка выхода

Сразу же напомним, что MetaTrader 5 изначально был строго неттинговой платформой, то есть одновременно по одному символу могла существовать только одна позиция. Все приказы по одному символу увеличивали, уменьшали или целиком удаляли совокупный объем по символу, а также меняли для него общие уровни стопов и тейков. Такой режим отсутствует в MetaTrader 4, и нам пришлось бы довольно тяжело с воплощением данного проекта, если бы в MetaTrader 5 однажды не появилась поддержка хеджирования. Это тот самый режим, который исповедует MetaTrader 4: исполнение каждого приказа формирует отдельную "позицию" (в терминах MetaTrader 5), так что даже по одному и тому же символу может существовать несколько открытых ордеров, в том числе и разнонаправленных.

Внимание! Если Вы захотите сравнить работу сгенерированных экспертов в MetaTrader 5 и MetaTrader 4, не забудьте, что в MetaTrader 5 должен быть активирован тип счета с хеджированием. Желательно для сравнения использовать сервера одного и того же брокера.

Реализация

Эмуляция торгового окружения MetaTrader 5

Всё эмулируемое окружение, включая типы, константы и функции, мы разместим для простоты в одном-единственном заголовочном файле MT5Bridge.mqh. Хороший стиль программирования, вероятно, потребовал бы их разнесения по отдельным файлам. Такое структурирование особенно важно для больших проектов и проектов, над которыми работает группа людей. Однако с точки зрения распространения и установки один файл удобнее.

Итак, согласно плану, определим все перечисления, константы и структуры. Это рутинная работа копирования, без каких-либо сложностей. Вряд ли стоит её пояснять более подробно, чем те замечания, что уже были приведены при планировании. Затем заглянем вновь на страницу документации о Торговых функциях и приступим к более интеллектуальной части — написанию кода всех этих функций.‌

Начнем с текущих операций, включающих обработку рыночных и отложенных ордеров, а также позиций.

Для этого нужна суперуниверсальная "пятерочная" функция OrderSend.

bool OrderSend(MqlTradeRequest &request, MqlTradeResult &result)
{

В ней, в зависимости от типа запроса, необходимо использовать один из типов ордеров MetaTrader 4.

  int cmd;   
  result.retcode = 0;
  switch(request.type)
  {
    case ORDER_TYPE_BUY:
      cmd = OP_BUY;
      break;
    case ORDER_TYPE_SELL:
      cmd = OP_SELL;
      break;
    case ORDER_TYPE_BUY_LIMIT:
      cmd = OP_BUYLIMIT;
      break;
    case ORDER_TYPE_SELL_LIMIT:
      cmd = OP_SELLLIMIT;
      break;
    case ORDER_TYPE_BUY_STOP:
      cmd = OP_BUYSTOP;
      break;
    case ORDER_TYPE_SELL_STOP:
      cmd = OP_SELLSTOP;
      break;
    default:
      Print("Unsupported request type:", request.type);
      return false;
  }

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

  ResetLastError();
  if(request.action == TRADE_ACTION_DEAL || request.action == TRADE_ACTION_PENDING)
  {
    if(request.price == 0)
    {
      if(cmd == OP_BUY)
      {
        request.price = MarketInfo(request.symbol, MODE_ASK);
      }
      else
      if(cmd == OP_SELL)
      {
        request.price = MarketInfo(request.symbol, MODE_BID);
      }
    }
    if(request.position > 0)
    {
      if(!OrderClose((int)request.position, request.volume, request.price, (int)request.deviation))
      {
        result.retcode = GetLastError();
      }
      else
      {
        result.retcode = TRADE_RETCODE_DONE;
        result.deal = request.position | 0x8000000000000000;
        result.order = request.position | 0x8000000000000000;
        result.volume = request.volume;
        result.price = request.price;
      }
    }
    else
    {
      int ticket = OrderSend(request.symbol, cmd, request.volume, request.price, (int)request.deviation, request.sl, request.tp, request.comment, (int)request.magic, request.expiration);
      if(ticket == -1)
      {
        result.retcode = GetLastError();
      }
      else
      {
        result.retcode = TRADE_RETCODE_DONE;
        result.deal = ticket;
        result.order = ticket;
        result.request_id = ticket;
        if(OrderSelect(ticket, SELECT_BY_TICKET))
        {
          result.volume = OrderLots();
          result.price = OrderOpenPrice() > 0 ? OrderOpenPrice() : request.price;
          result.comment = OrderComment();
          result.ask = MarketInfo(OrderSymbol(), MODE_ASK);
          result.bid = MarketInfo(OrderSymbol(), MODE_BID);
        }
        else
        {
          result.volume = request.volume;
          result.price = request.price;
          result.comment = "";
        }
      }
    }
  }

Основную работу выполняет привычная для MetaTrader 4 функция OrderSend с множеством параметров. После её вызова результаты работы соответствующим образом записываются в выходную структуру.

Особо отметим, что в MetaTrader 5 закрытие имеющегося рыночного ордера происходит путем открытия другого ордера противоположного направления, а в поле position передается идентификатор закрываемой позиции. В этом случае, т.е. когда поле position не пустое, приведенный выше код пытается закрыть ордер с помощью функции OrderClose. При этом в качестве идентификатора позиции используется тикет самого ордера. Это логично, так как в "четверке" каждый ордер создает свою собственную позицию. Сделка получает тот же самый тикет.

Что же касается виртуального ордера закрытия позиции (его тут на самом деле нет), то в качестве его тикета искусственным образом используется исходный номер, дополненный установленным в 1 старшим битом. Это будет применяться в дальнейшем при перечислении ордеров и сделок.

Теперь посмотрим, как можно реализовать изменение уровней в открытой позиции.

  else if(request.action == TRADE_ACTION_SLTP) // change opened position
  {
    if(OrderSelect((int)request.position, SELECT_BY_TICKET))
    {
      if(!OrderModify((int)request.position, OrderOpenPrice(), request.sl, request.tp, 0))
      {
        result.retcode = GetLastError();
      }
      else
      {
        result.retcode = TRADE_RETCODE_DONE;
        result.deal = OrderTicket();
        result.order = OrderTicket();
        result.request_id = OrderTicket();
        result.volume = OrderLots();
        result.comment = OrderComment();
      }
    }
    else
    {
      result.retcode = TRADE_RETCODE_POSITION_CLOSED;
    }
  }

Вполне очевидно, что для этого используется OrderModify.

Эта же функция используется и для изменения отложенного ордера.‌

  else if(request.action == TRADE_ACTION_MODIFY) // change pending order
  {
    if(OrderSelect((int)request.order, SELECT_BY_TICKET))
    {
      if(!OrderModify((int)request.order, request.price, request.sl, request.tp, request.expiration))
      {
        result.retcode = GetLastError();
      }
      else
      {
        result.retcode = TRADE_RETCODE_DONE;
        result.deal = OrderTicket();
        result.order = OrderTicket();
        result.request_id = OrderTicket();
        result.price = request.price;
        result.volume = OrderLots();
        result.comment = OrderComment();
      }
    }
    else
    {
      result.retcode = TRADE_RETCODE_INVALID_ORDER;
    }
  }

Удаление отложенного ордера выполняет стандартная функция OrderDelete.

  else if(request.action == TRADE_ACTION_REMOVE)
  {
    if(!OrderDelete((int)request.order))
    {
      result.retcode = GetLastError();
    }
    else
    {
      result.retcode = TRADE_RETCODE_DONE;
    }
  }

Наконец, закрытие одной позиции с помощью другой (встречной) эквивалентно в контексте MetaTrader 4 закрытию встречных ордеров.

  else if(request.action == TRADE_ACTION_CLOSE_BY)
  {
    if(!OrderCloseBy((int)request.position, (int)request.position_by))
    {
      result.retcode = GetLastError();
    }
    else
    {
      result.retcode = TRADE_RETCODE_DONE;
    }
  }
  return true;
}

Помимо OrderSend, MetaTrader 5 предоставляет асинхронную функцию OrderSendAsync. Мы её реализовывать не будем, а все случаи применения асинхронного режима в библиотеке отключим, т.е. фактически заменим на синхронный вариант.

Установка рабочих ордеров часто сопровождается вызовами 3 других функций: OrderCalcMargin, OrderCalcProfit, OrderCheck.‌

Вот один из вариантов, как их можно реализовать с помощью средств, доступных в MetaTrader 4.

int EnumOrderType2Code(int action)

{   // ORDER_TYPE_BUY/ORDER_TYPE_SELL and derivatives   return (action % 2 == 0) ? OP_BUY : OP_SELL; }

bool OrderCalcMargin(   ENUM_ORDER_TYPE action,   string          symbol,   double          volume,   double          price,   double         &margin   ) {   int cmd = EnumOrderType2Code(action);   double m = AccountFreeMarginCheck(symbol, cmd, volume);   if(m <= 0 || GetLastError() == ERR_NOT_ENOUGH_MONEY)   {     return false;   }   margin = AccountFreeMargin() - m;   return true; }

bool OrderCalcProfit(   ENUM_ORDER_TYPE action,   string          symbol,   double          volume,   double          price_open,   double          price_close,   double         &profit   ) {   int cmd = EnumOrderType2Code(action);   if(cmd > -1)   {     int points = (int)((price_close - price_open) / MarketInfo(symbol, MODE_POINT));     if(cmd == OP_SELL) points = -points;     profit = points * volume * MarketInfo(symbol, MODE_TICKVALUE) / (MarketInfo(symbol, MODE_TICKSIZE) / MarketInfo(symbol, MODE_POINT));     return true;   }   return false; } bool OrderCheck(const MqlTradeRequest &request, MqlTradeCheckResult &result) {   if(request.volume > MarketInfo(request.symbol, MODE_MAXLOT)   || request.volume < MarketInfo(request.symbol, MODE_MINLOT)   || request.volume != MathFloor(request.volume / MarketInfo(request.symbol, MODE_LOTSTEP)) * MarketInfo(request.symbol, MODE_LOTSTEP))   {     result.retcode = TRADE_RETCODE_INVALID_VOLUME;     return false;   }   double margin;   if(!OrderCalcMargin(request.type, request.symbol, request.volume, request.price, margin))   {     result.retcode = TRADE_RETCODE_NO_MONEY;     return false;   }   if((request.action == TRADE_ACTION_DEAL || request.action == TRADE_ACTION_PENDING)   && SymbolInfoInteger(request.symbol, SYMBOL_TRADE_MODE) == SYMBOL_TRADE_EXECUTION_MARKET   && (request.sl != 0 || request.tp != 0))   {     result.retcode = TRADE_RETCODE_INVALID_STOPS;     return false;   }   result.balance = AccountBalance();   result.equity = AccountEquity();   result.profit = AccountEquity() - AccountBalance();   result.margin = margin;   result.margin_free = AccountFreeMargin();   result.margin_level = 0;   result.comment = "";   return true; }

Здесь активно используются встроенные функции AccountEquity, AccountFreeMargin, AccountFreeMarginCheck, а также стоимость пункта инструмента и прочие его настройки, получаемые с помощью вызовов MarketInfo.

Для получения общего числа позиций достаточно вернуть количество открытых рыночных ордеров.

int PositionsTotal()
{
  int count = 0;
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderType() <= OP_SELL)
      {
        count++;
      }
    }
  }
  return count;
}

Для получения символа позиции по её номеру необходимо перебрать в цикле все ордера, подсчитывая лишь рыночные.

string PositionGetSymbol(int index)
{
  int count = 0;
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderType() <= OP_SELL)
      {
        if(index == count)
        {
          return OrderSymbol();
        }
        count++;
      }
    }
  }
  return "";
}

Аналогичным образом строится функция для получения тикета позиции по её номеру.

ulong PositionGetTicket(int index)
{
  int count = 0;
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderType() <= OP_SELL)
      {
        if(index == count)
        {
          return OrderTicket();
        }
        count++;
      }
    }
  }
  return 0;
}

Для выбора позиции по названию символа также пройдемся в цикле по рыночным ордерам и остановимся на первом, совпадающем по символу.

bool PositionSelect(string symbol)
{
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderSymbol() == symbol && (OrderType() <= OP_SELL))
      {
        return true;
      }
    }
  }
  return false;
}

Реализация выбора позиции по тикету не требует цикла.

bool PositionSelectByTicket(ulong ticket)
{
  if(OrderSelect((int)ticket, SELECT_BY_TICKET))
  {
    if(OrderType() <= OP_SELL)
    {
      return true;
    }
  }
  return false;
}

Свойства выбранной позиции должна возвращать тройка функций, привычных для MetaTrader 5 — _GetDouble, _GetInteger, _GetString. Приведем здесь их реализацию для позиций, а для ордеров и сделок они будут выглядеть очень похоже, и потому останутся за рамками статьи. Желающие могут ознакомиться с их кодом в прилагаемом файле.

// позиция = ордер, только OP_BUY или OP_SELL
ENUM_POSITION_TYPE Order2Position(int type)
{
  return type == OP_BUY ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;
}

bool PositionGetInteger(ENUM_POSITION_PROPERTY_INTEGER property_id, long &long_var)
{
  switch(property_id)
  {
    case POSITION_TICKET:
    case POSITION_IDENTIFIER:
      long_var = OrderTicket();
      return true;
    case POSITION_TIME:
    case POSITION_TIME_UPDATE:
      long_var = OrderOpenTime();
      return true;
    case POSITION_TIME_MSC:
    case POSITION_TIME_UPDATE_MSC:
      long_var = OrderOpenTime() * 1000;
      return true;
    case POSITION_TYPE:
      long_var = Order2Position(OrderType());
      return true;
    case POSITION_MAGIC:
      long_var = OrderMagicNumber();
      return true;
  }
  return false;
}

bool PositionGetDouble(ENUM_POSITION_PROPERTY_DOUBLE property_id, double &double_var)
{
  switch(property_id)
  {
    case POSITION_VOLUME:
      double_var = OrderLots();
      return true;
    case POSITION_PRICE_OPEN:
      double_var = OrderOpenPrice();
      return true;
    case POSITION_SL:
      double_var = OrderStopLoss();
      return true;
    case POSITION_TP:
      double_var = OrderTakeProfit();
      return true;
    case POSITION_PRICE_CURRENT:
      double_var = MarketInfo(OrderSymbol(), OrderType() == OP_BUY ? MODE_BID : MODE_ASK);
      return true;
    case POSITION_COMMISSION:
      double_var = OrderCommission();
      return true;
    case POSITION_SWAP:
      double_var = OrderSwap();
      return true;
    case POSITION_PROFIT:
      double_var = OrderProfit();
      return true;
  }
  return false;
}

bool PositionGetString(ENUM_POSITION_PROPERTY_STRING property_id, string &string_var)
{
  switch(property_id)
  {
    case POSITION_SYMBOL:
      string_var = OrderSymbol();
      return true;
    case POSITION_COMMENT:
      string_var = OrderComment();
      return true;
  }
  return false;
}

Аналогично позициям, которые являются рыночными ордерами, следует реализовать и набор функций для обработки отложенных ордеров. Однако тут есть одна сложность. Мы не можем реализовать функцию OrdersTotal и прочие OrderGet_, поскольку они уже определены в ядре, а переопределять встроенные функции нельзя. Компилятор выдает ошибку вида:

'OrderGetString' - override system function MT5Bridge.mqh

Поэтому мы вынуждены дать другие имена всем функциям с именами, начинающимися с префикса Order_. Логично начинать их названия с PendingOrder_, поскольку они обрабатывают исключительно отложенные ордера. Например:

int PendingOrdersTotal()
{
  int count = 0;
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderType() > OP_SELL)
      {
        count++;
      }
    }
  }
  return count;
}

Затем в коде стандартной библиотеки нужно будет заменить все вызовы на наши новые функции из MT5Bridge.mqh.

Функция OrderGetTicket, возвращающая тикет ордера по номеру, отсутствует в MetaTrader 4, поэтому оставим её имя как есть, т.е. в соответствии с API MetaTrader 5.‌

ulong OrderGetTicket(int index)
{
  int count = 0;
  for(int i = 0; i < ::OrdersTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS))
    {
      if(OrderType() > OP_SELL)
      {
        if(index == count)
        {
          return OrderTicket();
        }
        count++;
      }
    }
  }
  return 0;
}

Функция OrderSelect существует в MetaTrader 4 с расширенным списком параметров по сравнению с MetaTrader 5, поэтому оставим её вызовы, дополнив их необходимым параметром SELECT_BY_TICKET.

Полную реализацию функций чтения свойств отложенных ордеров можно найти в прилагаемом заголовочном файле.‌

Теперь обратимся к функциям для работы с историей ордеров и сделок. Их реализация потребует некоторой изобретательности. Приведенный далее вариант — лишь один из многих возможных — выбран из-за простоты.

Каждый рыночный ордер MetaTrader 4 отображается в истории двумя ордерами а-ля MetaTrader 5: входным и выходным. Кроме того, в истории должна быть и соответствующая пара сделок. Отложенные ордера отображаются как есть. Храниться история будет в двух массивах с тикетами.

int historyDeals[], historyOrders[];

Заполнять их будет функция HistorySelect из MQL5 API.

bool HistorySelect(datetime from_date, datetime to_date)
{
  int deals = 0, orders = 0;
  ArrayResize(historyDeals, 0);
  ArrayResize(historyOrders, 0);
  for(int i = 0; i < OrdersHistoryTotal(); i++)
  {
    if(OrderSelect(i, SELECT_BY_POS, MODE_HISTORY))
    {
      if(OrderOpenTime() >= from_date || OrderCloseTime() <= to_date)
      {
        if(OrderType() <= OP_SELL) // deal
        {
          ArrayResize(historyDeals, deals + 1);
          historyDeals[deals] = OrderTicket();
          deals++;
        }
        ArrayResize(historyOrders, orders + 1);
        historyOrders[orders] = OrderTicket();
        orders++;
      }
    }
  }
  return true;
}

После того, как массивы заполнены, можно получить размер истории.

int HistoryDealsTotal()
{
  return ArraySize(historyDeals) * 2;
}

int HistoryOrdersTotal()
{
  return ArraySize(historyOrders) * 2;
}

Размеры массивов умножаются на 2, поскольку каждый ордер MetaTrader 4 — это два ордера или две сделки MetaTrader 5. Для отложенных ордеров это не так, но для сохранения общности подхода мы все равно резервируем 2 тикета, только один из них не будет использоваться (см. ниже функцию HistoryOrderGetTicket). Сделка входа в рынок будет получать тот же самый тикет, что у порождающего её ордера МетаТрейдера 4. А для сделки выхода будем этот тикет дополнять единичным старшим битом.

ulong HistoryDealGetTicket(int index)
{
  if(OrderSelect(historyDeals[index / 2], SELECT_BY_TICKET, MODE_HISTORY))
  {
    // odd - enter - positive, even - exit - negative
    return (index % 2 == 0) ? OrderTicket() : (OrderTicket() | 0x8000000000000000);
  }
  return 0;
}

Четные номера в истории всегда содержат тикеты на вход (реальные), нечетные — на выход (виртуальные).

Для ордеров все немного сложнее, поскольку среди них могут быть отложенные, которые отображаются один в один. В этом случае, четный номер вернет правильный тикет отложенного ордера, а следующий нечетный — 0.‌

ulong HistoryOrderGetTicket(int index)
{
  if(OrderSelect(historyOrders[index / 2], SELECT_BY_TICKET, MODE_HISTORY))
  {
    if(OrderType() <= OP_SELL)
    {
      return (index % 2 == 0) ? OrderTicket() : (OrderTicket() | 0x8000000000000000);
    }
    else if(index % 2 == 0) // pending order is returned once
    {
      return OrderTicket();
    }
    else
    {
      Print("History order ", OrderType(), " ticket[", index, "]=", OrderTicket(), " -> 0");
    }
  }
  return 0;
}

Выбор сделки по тикету реализуются с учетом этой особенности с установкой старшего бита — здесь его нужно сбрасывать.

bool HistoryDealSelect(ulong ticket)
{
  ticket &= ~0x8000000000000000;
  return OrderSelect((int)ticket, SELECT_BY_TICKET, MODE_HISTORY);
}

Для ордера все полностью аналогично.

#define HistoryOrderSelect HistoryDealSelect

Имея сделку, выбранную с помощью HistoryDealSelect или HistoryDealGetTicket, можно написать реализацию функций доступа к свойствам сделки.

#define REVERSE(type) ((type + 1) % 2)

ENUM_DEAL_TYPE OrderType2DealType(const int type)
{
  static ENUM_DEAL_TYPE types[] = {DEAL_TYPE_BUY, DEAL_TYPE_SELL, -1, -1, -1, -1, DEAL_TYPE_BALANCE};
  return types[type];
}

bool HistoryDealGetInteger(ulong ticket_number, ENUM_DEAL_PROPERTY_INTEGER property_id, long &long_var)
{
  bool exit = ((ticket_number & 0x8000000000000000) != 0);
  ticket_number &= ~0x8000000000000000;
  if(OrderSelect((int)ticket_number, SELECT_BY_TICKET, MODE_HISTORY))
  {
    switch(property_id)
    {
      case DEAL_TICKET:
      case DEAL_ORDER:
      case DEAL_POSITION_ID:
        long_var = OrderTicket();
        return true;
      case DEAL_TIME:
        long_var = exit ? OrderCloseTime() : OrderOpenTime();
        return true;
      case DEAL_TIME_MSC:
        long_var = (exit ? OrderCloseTime() : OrderOpenTime()) * 1000;
        return true;
      case DEAL_TYPE:
        long_var = OrderType2DealType(exit ? REVERSE(OrderType()) : OrderType());
        return true;
      case DEAL_ENTRY:
        long_var = exit ? DEAL_ENTRY_OUT : DEAL_ENTRY_IN;
        return true;
      case DEAL_MAGIC:
        long_var = OrderMagicNumber();
        return true;
    }
  }
  return false;
}
  
bool HistoryDealGetDouble(ulong ticket_number, ENUM_DEAL_PROPERTY_DOUBLE property_id, double &double_var)
{
  bool exit = ((ticket_number & 0x8000000000000000) != 0);
  ticket_number &= ~0x8000000000000000;
  switch(property_id)
  {
    case DEAL_VOLUME:
      double_var = OrderLots();
      return true;
    case DEAL_PRICE:
      double_var = exit ? OrderClosePrice() : OrderOpenPrice();
      return true;
    case DEAL_COMMISSION:
      double_var = exit? 0 : OrderCommission();
      return true;
    case DEAL_SWAP:
      double_var = exit ? OrderSwap() : 0;
      return true;
    case DEAL_PROFIT:
      double_var = exit ? OrderProfit() : 0;
      return true;
  }
  return false;
}

bool HistoryDealGetString(ulong ticket_number, ENUM_DEAL_PROPERTY_STRING property_id, string &string_var)
{
  switch(property_id)
  {
    case DEAL_SYMBOL:
      string_var = OrderSymbol();
      return true;
    case DEAL_COMMENT:
      string_var = OrderComment();
      return true;
  }
  return false;
}

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

Изменения в файлах стандартной библиотеки

Некоторые необходимые правки библиотеки уже обсуждались при реализации функций. Вы можете самостоятельно сравнить файлы из поставки MetaTrader 5 и те, что получились в этом проекте, чтобы получить полный список изменений. Далее рассматриваются только наиболее важные моменты, а комментарии по мелким правкам опущены. Во многие файлы вставлена новая директива #include для подключения MT5Bridge.mqh.

Таблица основных изменений в файлах стандартной библиотеки


Файл/Метод Изменения
Trade.mqh SetAsyncMode удалена строка, применяющая асинхронный режим, поскольку он не поддерживается
SetMarginMode явно прописан режим ACCOUNT_MARGIN_MODE_RETAIL_HEDGING
OrderOpen явно прописана комбинация флагов, задающих режим экспирации, как SYMBOL_EXPIRATION_GTC | SYMBOL_EXPIRATION_SPECIFIED
OrderTypeCheck исключены случаи обработки несуществующих типов ORDER_TYPE_BUY_STOP_LIMIT, ORDER_TYPE_SELL_STOP_LIMIT
OrderSend удален вызов отсутствующей асинхронной функции OrderSendAsync
 
OrderInfo.mqh все вызовы функций OrderGetInteger, OrderGetDouble, OrderGetString заменены на одноименные функции с префиксом PendingOrder
все вызовы OrderSelect(m_ticket) заменены на OrderSelect((int)m_ticket, SELECT_BY_TICKET)
 
PositionInfo.mqh FormatPosition
SelectByIndex
установлен режим маржи ACCOUNT_MARGIN_MODE_RETAIL_HEDGING
 
SymbolInfo.mqh Refresh удалены многие проверки, не поддерживаемые в MetaTrader 4
 
AccountInfo.mqh MarginMode возвращает константу ACCOUNT_MARGIN_MODE_RETAIL_HEDGING
 
Expert.mqh TimeframeAdd
TimeframesFlags
удалены неподдерживаемые таймфреймы
 
ExpertBase.mqh добавлен #include <Indicators\IndicatorsExt.mqh>
SetMarginMode установлен безусловно в ACCOUNT_MARGIN_MODE_RETAIL_HEDGING


Файл IndicatorsExt.mqh необходим для исправления мелких ошибок в стандартном файле Indicators.mqh. Кроме того, он включает другой необходимый для индикаторов заголовочный файл TimeSeriesExt.mqh.‌

Файл TimeSeriesExt.mqh содержит определение классов, которые нужны для торговли а-ля MetaTrader 5, но отсутствуют в стандартном файле TimeSeries.mqh, поставляемом с MetaTrader 4.

В частности, это классы: CTickVolumeBuffer, CSpreadBuffer, CiSpread, CiTickVolume, CRealVolumeBuffer, CiRealVolume. Многие из них — всего лишь заглушки, которые ничего не делают (и не могут делать в связи с недоступностью соответствующего функционала в MetaTrader 4).

Тестирование

Установив адаптированные торговые классы стандартной библиотеки в каталог Include MetaTrader 4 (с сохранением иерархии подкаталогов), а также скопировав MT5Bridge.mqh в папку Include/Trade, мы можем компилировать и запускать эксперты, сгенерированные Мастером MetaTrader 5, непосредственно в MetaTrader 4.

MetaTrader 5 поставляется с несколькими примерами сгенерированных экспертов (в папке Experts/Advisors). Возьмем один из них — ExpertMACD.mq5. Скопируем его в папку MQL4/Experts и переименуем в ExpertMACD.mq4. Компиляция в редакторе должна дать примерно такой результат:

Эксперт из Мастера MetaTrader 5 компилируется в MetaTrader 4

Эксперт из Мастера MetaTrader 5 компилируется в MetaTrader 4

Видно, что библиотечные файлы подключены и обрабатываются без ошибок и предупреждений. Конечно, отсутствие ошибок компиляции не гарантирует отсутствие проблем в логике программы, но это уже предмет для дальнейших проверок на практике.

Запустим скомпилированный эксперт с настройками по умолчанию в тестере MetaTrader 4.

Отчет тестирования MetaTrader 4 для эксперта, сгенерированного в MetaTrader 5

Отчет тестирования MetaTrader 4 для эксперта, сгенерированного в MetaTrader 5

При желании можно убедиться, что в журнале отсутствуют явные ошибки обработки приказов.‌

На графике EURUSD M15 торговля данного эксперта выглядит нормально, включая, в частности, установку уровней стоп-лосса и тейк-профита.

Окно с графиком, иллюстрирующем работу эксперта из Мастера MetaTrader 5 в MetaTrader 4

Окно с графиком, иллюстрирующим работу эксперта из Мастера MetaTrader 5 в MetaTrader 4

Сравним с результатами тестера MetaTrader 5.

Отчет тестирования MetaTrader 5 для сгенерированного эксперта

Отчет тестирования MetaTrader 5 для сгенерированного эксперта

Очевидно, что различия есть. Они могут объясняться расхождениями как в самих котировках (например, MetaTrader 5 использует плавающий спред), так и в алгоритмах тестера. В целом, тесты похожи: примерно совпадает количество трейдов и общий характер кривой баланса.

Разумеется, пользователь может сгенерировать в Мастере собственный эксперт с совершенно произвольным набором модулей, и он должен также просто переноситься в MetaTrader 4. В ходе тестирования проекта были проверены, в частности, эксперты с трейлингом и переменным размером лота.

Заключение

Итак, мы рассмотрели один из возможных способов переноса советников, генерируемых Мастером MetaTrader 5, на платформу MetaTrader 4. Основной его плюс — относительная простота реализации, основанная на максимально полном использовании имеющегося кода торговых классов из стандартной библиотеки MetaTrader 5. Основной недостаток — необходимость иметь на компьютере оба терминала: один — для генерации советников, а второй — для их использования.

Ниже приложены 2 файла:

  • архив с модифицированными файлами стандартной библиотеки, который нужно развернуть, с сохранением иерархии подкаталогов, в каталоге Include MetaTrader 4. В архиве находятся только файлы, которых нет в поставке терминала, поэтому нет опасности перезаписи существующих файлов;
  • файл MT5Bridge.mqh, который следует скопировать в папку Include/Trade.

Данная версия библиотеки была взята из 1545-й сборки MetaTrader 5. Будущие сборки могут содержать изменения в стандартной библиотеке, которые могут оказаться полезными (что потребует повторно выполнить слияние с правками эмулятора). В идеале было бы здорово когда-нибудь увидеть версию стандартной библиотеки от MetaQuotes, в которой с самого начала с помощью директив условной компиляции совмещены два варианта реализации торговых классов — для MetaTrader 5 и для MetaTrader 4.

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

Также не стоит забывать и о том, что предоставленная реализация эмулятора распространяется в статусе бета-версии и может содержать скрытые ошибки. Лишь долгое и разностороннее тестирование позволит получить окончательный программный продукт, пригодный для реальной торговли. Наличие исходных кодов позволяет делать это сообща.

Прикрепленные файлы |
MT4-1.1-SL5-b1545.zip (145.99 KB)
MT5Bridge.mqh (32.35 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (9)
fxsaber
fxsaber | 8 мая 2017 в 15:13
fxsaber:

Есть такое явление, как публикация в кодобазе советников, как результат конвертации MT4 -> MT5 через СБ.

Похоже, статью можно использовать и таким авторам для самопроверки. Если конвертация правильная, то обратная конвертация через MT5Bridge должна дать идентичный с MT4-оригиналом результат.

Форум по трейдингу, автоматическим торговым системам и тестированию торговых стратегий

MT4-Tester VS MT5-Tester

fxsaber, 2017.05.08 15:12

Сконвертировал Ваш код в MT4 через MT5Bridge. MT4build1072

EURUSD,M1: 1865415 tick events (7292 bars, 1865515 bar states) processed in 0:00:07.645 (total time 0:00:08.362)


Оригинальный код

EURUSD,M1: 1865415 tick events (7292 bars, 1865515 bar states) processed in 0:00:03.744 (total time 0:00:04.493)


Результаты после конвертации идентичны! Скорость упала в два раза.

Dr. Trader
Dr. Trader | 7 июл. 2017 в 07:49

Мой MT5 советник (сгенерированный, но со своей реализацией CExpertSignal вместо стандартных индикаторов) с вашими инклудами скомпилился и тестировался на MT4 без проблем, спасибо!

Но вот сейчас отправил советник на реал, и оказалось что он совсем не торгует. Никаких ошибок, ничего не показывает. Просто не торгует. Долго копался в коде, нашёл причину - функция bool CTrade::FillingCheck(const string symbol) в Trade.mqh

Для рыночных ордеров срабатывает такая проверка - 

// get possible filling policy types by symbol
  uint filling = (uint)SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE);
  // check execution mode again
  if(exec == SYMBOL_TRADE_EXECUTION_MARKET)
  {
    // for the MARKET execution mode
    // analyze order
    if(m_request.action != TRADE_ACTION_PENDING)
    {
      // in case of instant execution order
      // if the required filling policy is supported, add it to the request
      if(m_type_filling == ORDER_FILLING_FOK && (filling & SYMBOL_FILLING_FOK) != 0)
      {
        m_request.type_filling = m_type_filling;
        return(true);
      }
      if(m_type_filling == ORDER_FILLING_IOC && (filling & SYMBOL_FILLING_IOC) != 0)
      {
        m_request.type_filling = m_type_filling;
        return(true);
      }
      // wrong filling policy, set error code
      m_result.retcode = TRADE_RETCODE_INVALID_FILL;
      return(false);
    }
    return(true);
  }

В моём случае и m_type_filling и filling равны нулю, поэтому функция возвращает false.
filling по логике кода не должен быть равен нулю, но согласно справке SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE) для MT4 не поддерживается. Поэтому в тестере проверка почему-то проходит, и может быть у некоторых брокеров на реале тоже. Но для меня не прокатило, я пока-что просто поменял функцию чтоб весь код пропускался и из функции возвращалось true.

Stanislav Korotky
Stanislav Korotky | 7 июл. 2017 в 11:08
Dr. Trader:

Для рыночных ордеров срабатывает такая проверка - 

В моём случае и m_type_filling и filling равны нулю, поэтому функция возвращает false.
filling по логике кода не должен быть равен нулю, но согласно справке SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE) для MT4 не поддерживается. Поэтому в тестере проверка почему-то проходит, и может быть у некоторых брокеров на реале тоже. Но для меня не прокатило, я пока-что просто поменял функцию чтоб весь код пропускался и из функции возвращалось true.

Спасибо за сообщение. Я на такое не натыкался. Данный метод оставлен без изменений. Видимо, нужно все filling переменные устанавливать в конкретные константы в зависимости от типа ордера и/или инструмента (вероятно, это нельзя вытащить через API никак, а тестер пользуется какими-то умолчаниями). Если кто-то знает, как МТ4 внутри себя выбирает filling - поделитесь.

fxsaber
fxsaber | 7 июл. 2017 в 11:11
Stanislav Korotky:

Если кто-то знает, как МТ4 внутри себя выбирает filling - поделитесь.

Форум по трейдингу, автоматическим торговым системам и тестированию торговых стратегий

Особенности языка mql5, тонкости и приёмы работы

fxsaber, 2017.02.25 16:12

// Возвращает тип исполнения ордера, равный Type, если он доступен на символе Symb, иначе - корректный вариант.
ENUM_ORDER_TYPE_FILLING GetFilling( const string Symb, const uint Type = ORDER_FILLING_FOK )
{
  const ENUM_SYMBOL_TRADE_EXECUTION ExeMode = (ENUM_SYMBOL_TRADE_EXECUTION)::SymbolInfoInteger(Symb, SYMBOL_TRADE_EXEMODE);
  const int FillingMode = (int)::SymbolInfoInteger(Symb, SYMBOL_FILLING_MODE);

  return((FillingMode == 0 || (Type >= ORDER_FILLING_RETURN) || ((FillingMode & (Type + 1)) != Type + 1)) ?
         (((ExeMode == SYMBOL_TRADE_EXECUTION_EXCHANGE) || (ExeMode == SYMBOL_TRADE_EXECUTION_INSTANT)) ?
           ORDER_FILLING_RETURN : ((FillingMode == SYMBOL_FILLING_IOC) ? ORDER_FILLING_IOC : ORDER_FILLING_FOK)) :
          (ENUM_ORDER_TYPE_FILLING)Type);
}
Применение
Request.type_filling = GetFilling(Request.symbol);
Не проверял, исправили ли баг со стоповыми отложками.
Dr. Trader
Dr. Trader | 7 июл. 2017 в 16:59
fxsaber:

Спасибо, можно этот код чуть изменить чтоб получить аналог SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE) для MT4. Нужно чтоб функция возвращала не ENUM_ORDER_TYPE_FILLING а (SYMBOL_FILLING_FOK | SYMBOL_FILLING_IOC).

ORDER_FILLING_FOK = 0, ORDER_FILLING_IOC = 1, в то время как SYMBOL_FILLING_FOK = 1 и SYMBOL_FILLING_IOC = 2, так что можно результат просто увеличить на 1.

//фунция возвращает symbol filling mode, он же SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE) для MT4.
//Параметр Type - order filling type
uint GetSymbolFilling( const string Symb, const uint Type = ORDER_FILLING_FOK )
{
  const ENUM_SYMBOL_TRADE_EXECUTION ExeMode = (ENUM_SYMBOL_TRADE_EXECUTION)::SymbolInfoInteger(Symb, SYMBOL_TRADE_EXEMODE);
  const int FillingMode = (int)::SymbolInfoInteger(Symb, SYMBOL_FILLING_MODE);

  return ((FillingMode == 0 || (Type >= ORDER_FILLING_RETURN) || ((FillingMode & (Type + 1)) != Type + 1)) ?
         (((ExeMode == SYMBOL_TRADE_EXECUTION_EXCHANGE) || (ExeMode == SYMBOL_TRADE_EXECUTION_INSTANT)) ?
           ORDER_FILLING_RETURN : ((FillingMode == SYMBOL_FILLING_IOC) ? ORDER_FILLING_IOC : ORDER_FILLING_FOK)) :
          (ENUM_ORDER_TYPE_FILLING)Type) + 1;
}

И потом вызывать код в функции bool CTrade::FillingCheck(const string symbol)

uint filling = GetSymbolFilling(symbol, m_type_filling);
//вместо
//uint filling = (uint)SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE);


Хотя на самом деле это просто подгонка кода чтоб заработало. Лучше так не делать а найти правильное решение.

Графические интерфейсы X: Сортировка, реконструкция таблицы и элементы управления в ячейках (build 11) Графические интерфейсы X: Сортировка, реконструкция таблицы и элементы управления в ячейках (build 11)
Продолжаем добавлять в нарисованную таблицу новые возможности: сортировку данных, управление количеством столбцов и строк, установку типа ячеек таблицы для закрепления в них элементов управления.
Рецепты MQL5 - Торговые сигналы пивотов Рецепты MQL5 - Торговые сигналы пивотов
В статье представлен процесс разработки и реализации класса-сигнальщика на основе пивотов — разворотных уровней. На базе этого класса строится стратегия с использованием Стандартной библиотеки. Рассматриваются возможности развития стратегии пивотов посредством добавления фильтров.
Сравнительный анализ 10 трендовых стратегий Сравнительный анализ 10 трендовых стратегий
В статье сделан краткий обзор 10 трендовых стратегий, проведено их тестирование, сравнительный анализ. На основе полученных результатов сделан общий вывод о целесообразности, достоинствах и недостатках торговли по тренду.
Графические интерфейсы X: Обновления для нарисованной таблицы и оптимизация кода (build 10) Графические интерфейсы X: Обновления для нарисованной таблицы и оптимизация кода (build 10)
Продолжаем дополнять нарисованную таблицу (CCanvasTable) новыми возможностями. Теперь в таблице появятся: подсветка строк при наведении курсора мыши; возможность добавлять массив картинок для каждой ячейки и метод для их переключения; возможность задать или изменить текст в ячейках во время выполнения программы и многое другое.