Базовая, котировочная и маржинальная валюты инструмента

Как известно, одними из самых важных свойств каждого финансового инструмента являются его рабочие валюты:

  • базовая валюта, в которой выражается покупаемый или продаваемый актив (для инструментов Forex);
  • валюта расчета прибыли (котирования);
  • валюта расчета залога;

MQL-программа может получить названия этих валют с помощью функции SymbolInfoString и трех свойств из следующей таблицы.

Идентификатор

Описание

SYMBOL_CURRENCY_BASE

Базовая валюта

SYMBOL_CURRENCY_PROFIT

Валюта прибыли

SYMBOL_CURRENCY_MARGIN

Валюта, в которой вычисляются залоговые средства

С помощью них удобно анализировать инструменты Forex, в имена которых многие брокеры добавляют различные префиксы и суффиксы, а также биржевые инструменты. В частности, алгоритм сможет найти символ для получения кросс-курса двух заданных валют или подобрать корзину индексов с заданной общей валютой котирования.

Поскольку поиск инструментов согласно неким требованиям является частой задачей, создадим класс SymbolFilter (SymbolFilter.mqh) для построения списка подходящих символов и их избранных свойств. В дальнейшем используем этот класс не только для анализа валют, но и других характеристик.

Сначала рассмотрим упрощенную версию, а потом дополним её удобным функционалом.

В разработке воспользуемся уже готовыми вспомогательными средствами: ассоциативным массивом-картой (MapArray.mqh) для хранения пар ключ-значение выбранных типов и монитором свойств символа (SymbolMonitor.mqh).

#include <MQL5Book/MapArray.mqh>
#include <MQL5Book/SymbolMonitor.mqh>

Для упрощения инструкций по накоплению результатов работы в массивах применим усовершенствованную версию маркоса PUSH, уже встречавшегося ранее в примерах, а также его вариант EXPAND для многомерных массивов (простое присваивание в этом случае невозможно).

#define PUSH(A,V) (A[ArrayResize(AArraySize(A) + 1ArraySize(A) * 2) - 1] = V)
#define EXPAND(A) (ArrayResize(AArrayRange(A0) + 1ArrayRange(A0) * 2) - 1)

Объект класса SymbolFilter должен иметь хранилище значений свойств, по которым программист желает фильтровать символы. Поэтому опишем в классе 3 массива MapArray для целочисленных, вещественных и строковых свойств.

class SymbolFilter
{
   MapArray<ENUM_SYMBOL_INFO_INTEGER,longlongs;
   MapArray<ENUM_SYMBOL_INFO_DOUBLE,doubledoubles;
   MapArray<ENUM_SYMBOL_INFO_STRING,stringstrings;
   ...

Установка требуемых свойств фильтра производится с помощью перегруженных методов let.

public:
   SymbolFilter *let(const ENUM_SYMBOL_INFO_INTEGER propertyconst long value)
   {
      longs.put(propertyvalue);
      return &this;
   }
   
   SymbolFilter *let(const ENUM_SYMBOL_INFO_DOUBLE propertyconst double value)
   {
      doubles.put(propertyvalue);
      return &this;
   }
   
   SymbolFilter *let(const ENUM_SYMBOL_INFO_STRING propertyconst string value)
   {
      strings.put(propertyvalue);
      return &this;
   }
   ...

Обратите внимание, что методы возвращают указатель на сам фильтр, что позволяет записывать условия в виде цепочки: например, если ранее в коде был описан объекта f типа SymbolFilter, то для наложения двух условий на тип цены и название валюты прибыли можно написать:

f.let(SYMBOL_CHART_MODESYMBOL_CHART_MODE_LAST).let(SYMBOL_CURRENCY_PROFIT"USD");

Формирование массива символов, удовлетворяющих условиям, выполняется объектом-фильтром в нескольких вариантах метода select, самый простой из которых представлен ниже (другие варианты рассмотрим позднее).

Параметр watch определяет контекст поиска символов: среди выбранных в Обзор рынка (true) или всех доступных (false). Выходной массив symbols будет заполнен названиями походящих символов. Структура кода внутри метода нам уже знакома: здесь организован цикл по символам, для каждого из которых создается объект-монитор m.

   void select(const bool watchstring &symbols[]) const
   {
      const int n = SymbolsTotal(watch);
      for(int i = 0i < n; ++i)
      {
         const string s = SymbolName(iwatch);
         SymbolMonitor m(s);
         if(match<ENUM_SYMBOL_INFO_INTEGER,long>(mlongs)
         && match<ENUM_SYMBOL_INFO_DOUBLE,double>(mdoubles)
         && match<ENUM_SYMBOL_INFO_STRING,string>(mstrings))
         {
            PUSH(symbolss);
         }
      }
   }

Именно с помощью монитора мы можем унифицированно получить значение любого свойства. Проверка соответствия свойств текущего символа с сохраненным набором условий в массивах longs, doubles, strings делегируется вспомогательному методу match. Все запрошенные свойства должны совпасть, чтобы имя символа было сохранено в выходном массиве symbols.

В простейшем случае реализация метода match такова (впоследствии он будет изменен).

protected:
   template<typename K,typename V>
   bool match(const SymbolMonitor &mconst MapArray<K,V> &dataconst
   {
      for(int i = 0i < data.getSize(); ++i)
      {
         const K key = data.getKey(i);
         if(!equal(m.get(key), data.getValue(i)))
         {
            return false;
         }
      }
      return true;
   }

Если хоть одно из значений в массиве data не совпадает с соответствующим свойством символа, метод возвращает false. Если все свойства совпали (или условий для свойств данного типа нет), метод возвращает true.

Сравнение двух значений выполняется методом equal. Учитывая тот факт, что среди свойств могут быть свойства типа double, реализация не так проста, как можно было бы подумать.

   template<typename V>
   static bool equal(const V v1const V v2)
   {
      return v1 == v2 || eps(v1v2);
   }

Для типа double выражение v1 == v2 может не сработать для близких чисел, в связи с чем следует учитывать точность вещественного типа DBL_EPSILON. Это делается в отдельном методе eps, перегруженном раздельно для типа double и всех остальных типов за счет шаблона.

   static bool eps(const double v1const double v2)
   {
      return fabs(v1 - v2) < DBL_EPSILON * fmax(v1v2);
   }
   
   template<typename V>
   static bool eps(const V v1const V v2)
   {
      return false;
   }

При равенстве значений любых типов кроме double шаблонный метод eps просто не будет вызван, а во всех иных случаях (в том числе и при различии значений) он возвращает false, как и требовалось (таким образом, работать будет только условие v1 == v2).

Вышеописанный вариант фильтра позволяет проверять свойства только на равенство значениям. Однако на практике часто требуется анализировать условия на неравенство, а также на больше/меньше. В связи с этим в классе SymbolFilter определено перечисление IS с основными операциями сравнения (при желании его можно дополнить).

class SymbolFilter
{
   ...
   enum IS
   {
      EQUAL,
      GREATER,
      NOT_EQUAL,
      LESS
   };
   ...

Для каждого свойства из перечислений ENUM_SYMBOL_INFO_INTEGER, ENUM_SYMBOL_INFO_DOUBLE, ENUM_SYMBOL_INFO_STRING требуется сохранить не только искомое значение свойства (вспоминаем про ассоциативные массивы longs, doubles, strings), но и способ сравнения из нового перечисления IS.

Поскольку элементы стандартных перечислений имеют непересекающиеся значения (здесь есть одно исключение, связанное с объемами, но оно не критично), имеет смысл зарезервировать под способ сравнения один общий массив-карту conditions. При этом встает вопрос, какой тип выбрать для ключа карты, чтобы технически "объединить" разные перечисления. Для этого потребовалось описать фиктивное перечисление ENUM_ANY — оно лишь обозначает некий тип обобщенного перечисления. Напомним, что все перечисления имеют внутреннее представление, эквивалентное целому int, и потому могут приводиться одно к другому.

   enum ENUM_ANY
   {
   };
   
   MapArray<ENUM_ANY,ISconditions;
   MapArray<ENUM_ANY,longlongs;
   MapArray<ENUM_ANY,doubledoubles;
   MapArray<ENUM_ANY,stringstrings;
   ...

Теперь мы можем дополнить все методы let, устанавливающие искомое значение свойства, входным параметром cmp, который определяет способ сравнения. По умолчанию он задает проверку на равенство (EQUAL).

   SymbolFilter *let(const ENUM_SYMBOL_INFO_INTEGER propertyconst long value,
      const IS cmp = EQUAL)
   {
      longs.put((ENUM_ANY)propertyvalue);
      conditions.put((ENUM_ANY)propertycmp);
      return &this;
   }

Здесь представлен вариант для целочисленных свойств. Остальные две перегрузки изменяются аналогично.

С учетом новой информации о разных способах сравнения и одновременного избавления от разных типов ключей в массивах-картах мы модифицируем метод match. В нем для каждого заданного свойства, по ключу в массиве-карте data извлекается условие из массива conditions и с помощью оператора switch выполняются соответствующие проверки.

   template<typename V>
   bool match(const SymbolMonitor &mconst MapArray<ENUM_ANY,V> &dataconst
   {
      // фиктивная переменная для выбора перегрузки метода m.get ниже
      static const V type = (V)NULL;
      // цикл по условиям, наложенным на свойства символа
      for(int i = 0i < data.getSize(); ++i)
      {
         const ENUM_ANY key = data.getKey(i);
         // выбор способа сравнения в условии
         switch(conditions[key])
         {
         case EQUAL:
            if(!equal(m.get(keytype), data.getValue(i))) return false;
            break;
         case NOT_EQUAL:
            if(equal(m.get(keytype), data.getValue(i))) return false;
            break;
         case GREATER:
            if(!greater(m.get(keytype), data.getValue(i))) return false;
            break;
         case LESS:
            if(greater(m.get(keytype), data.getValue(i))) return false;
            break;
         }
      }
      return true;
   }

Новый шаблонный метод greater реализован тривиально.

   template<typename V>
   static bool greater(const V v1const V v2)
   {
      return v1 > v2;
   }

Теперь вызов метода match можно записать более кратко, так как единственный оставшийся тип шаблона V автоматически определяется по передаваемому аргументу data (а это один из массивов longs, doubles или strings).

   void select(const bool watchstring &symbols[]) const
   {
      const int n = SymbolsTotal(watch);
      for(int i = 0i < n; ++i)
      {
         const string s = SymbolName(iwatch);
         SymbolMonitor m(s);
         if(match(mlongs)
            && match(mdoubles)
            && match(mstrings))
         {
            PUSH(symbolss);
         }
      }
   }

Это еще не окончательная версия класса SymbolFilter, но мы уже можем проверить его в действии.

Создадим скрипт SymbolFilterCurrency.mq5, способный фильтровать символы по свойствам базовой валюты и валюты прибыли — в данном случае "USD". Параметр MarketWatchOnly по умолчанию задает поиск только по Обзору рынка.

#include <MQL5Book/SymbolFilter.mqh>
   
input bool MarketWatchOnly = true;
   
void OnStart()
{
   SymbolFilter f;   // объект фильтра
   string symbols[]; // массив для результатов
   ...

Допустим, что мы хотим найти инструменты Forex, которые имеют прямые котировки, то есть в их названиях "USD" фигурирует в начале. Чтобы не зависеть от особенностей формирования названий у конкретного брокера, воспользуемся свойством SYMBOL_CURRENCY_BASE. Именно оно содержит первую валюту.

Запишем условие, чтобы базовая валюта символа равнялась "USD", и применим фильтр.

   f.let(SYMBOL_CURRENCY_BASE"USD")
   .select(MarketWatchOnlysymbols);
   Print("===== Base is USD =====");
   ArrayPrint(symbols);
   ...

Полученный массив выводится в журнал.

===== Base is USD =====
"USDCHF" "USDJPY" "USDCNH" "USDRUB" "USDCAD" "USDSEK" "SP500m" "Brent" 

Как видно, в массив попали не только Forex-символы, у которых "USD" идет в начале тикера, но также индекс S&P500 и товар (нефть). Два последних символа котируются в долларах, но и базовая валюта у них такая же. В то же время у Forex-символов валюта котирования (она же валюта прибыли) идет второй и отличается от "USD". Это позволяет дополнить фильтр таким образом, чтобы не-Forex-символы перестали ему соответствовать.

Очистим массив, добавим в фильтр условие, чтобы валюта прибыли не равнялась "USD", и снова запросим подходящие символы (прежнее условие сохранилось в объекте f).

   ...
   ArrayResize(symbols0);
   
   f.let(SYMBOL_CURRENCY_PROFIT"USD"SymbolFilter::IS::NOT_EQUAL)
   .select(MarketWatchOnlysymbols);
   Print("===== Base is USD and Profit is not USD =====");
   ArrayPrint(symbols);
}

На этот раз в журнале выведены действительно только искомые символы.

===== Base is USD and Profit is not USD =====
"USDCHF" "USDJPY" "USDCNH" "USDRUB" "USDCAD" "USDSEK"