Скачать MetaTrader 5

Повышаем качество кода при помощи Unit Test

1 октября 2010, 11:39
Андрей
7
1 999

Введение

При программировании на MQL4 мне пришлось столкнуться с некоторыми готовыми программами на MQL4, а так же написать не один десяток своих. В итоге я пришёл к выводу, что MQL4 - это очень благоприятная среда для создания некачественных программ. Вот почему я так считаю:

  1. MetaTrader 4 не имеет встроенного отладчика. Процесс поиска ошибок иногда бывает достаточно мучительным.
  2. MQL4 не имеет встроенных средств обработки исключений, как это реализовано в C++ или Java.
  3. Программы на MQL4 часто пишутся впопыхах, с большей ориентацией на идею, нежели на качество кода.

Всё это ведёт к тому, что снижается качество кода, что в свою очередь влечёт:

  1. Сбои в работе советников, неправильный алгоритм работы (особенно критично при работе на реале).
  2. Медленная скорость выполнения. Делает оптимизацию очень медленной.
  3. Плохая обработка ошибочных ситуаций. Советник может оказаться неработоспособным.

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

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


Немного теории о Качестве

Итак, немного погуглив, узнаём, что:

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

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

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

  • Обеспечение Качества (Quality Assurance) - меры, нацеленные на предотвращение появления дефектов.
  • Контроль Качества (Quality Control) - проверка качества готовой программы с целью выявления дефектов, если QA не помогло :). Необходимо понимать, что дефект вряд ли будет исправлен, если он не обнаружен. При этом ситуация не будет выглядеть красиво, если дефект был обнаружен заказчиком...

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

Чем больше контроля качества - тем выше вероятность, что программа будет работать как надо :) В идеале контроль качества (или, другими словами, тестирование) должен осуществляться на каждом этапе разработки:

  1. Тестирование ТЗ (Техническое Задание) - на основании неправильного ТЗ невозможно разработать правильно работающую программу.
  2. Просмотр исходного кода (Code Review). Поиск недочётов, неэффективного кода, нарушение правил кодирования, очевидные ошибки.
  3. Тестирование отдельных функций программы в автоматическом режиме (Юнит Тестирование или Unit Testing).
  4. Тестирование всей получившейся программы в ручном режиме. Человек (тестировщик) проверяет, правильно ли работает программа.
  5. Тестирование программы в автоматическом режиме (Автоматизированное Тестирование или Automated Testing). Это когда Роботы сами проверяют качество программы. Звучит утопично, но иногда работает :)
  6. Тестирование программы заказчиком.

и т.д. Видов тестирования существует достаточно много...

Из всего этого разнообразия тестов нас больше всего может заинтересовать Unit Testing.


Немного теории о Unit Testing

Google нам выдаёт следующее определение Юнит Тестов. Unit Testing - это метод валидации приложения, в котором программист проверяет отдельные юниты (блоки) исходного кода на предмет пригодности их к использованию в остальной программе. Юнит - это наименьшая пригодная к тестированию часть программы. В функциональных языках (коим является MQL4) за юнит можно принимать отдельную функцию.

Чаще всего Юнит Тестирование выполняется автоматически. То есть, пишется программа, которая вызывает тестируемую функцию с разными параметрами, затем формирует отчёт о том, правильные значения вернула функция или нет.

Юнит Тесты могут быть чрезвычайно полезны по следующим причинам:

  1. В случае если обнаружен сбой - легко понять его причину. Т.к. тестируется только одна функция. Если сбой обнаружен в целом приложении, то нужно ещё потратить время на то, чтобы найти функцию порождающую проблему.
  2. Легко проверить исправлен дефект или нет. Достаточно просто запустить ещё раз автоматический Unit Test. Не нужно перезапускать всё приложение. Например, бывают ошибки, возникающие редко в определённых условиях, которые сложно воссоздать. В случае Юнит тесов эта проблема отпадает.
  3. Можно легко оптимизировать код функции и совершенно не бояться, что что-то сломается. Юнит тест всегда покажет, продолжает функция работать нормально или нет.
  4. Можно выявить проблемы, которые сразу себя не проявляют, но могут воспроизвестись у заказчика и затем потребуют много часов отладки и поиска.
  5. Можно применить современный Test Driven подход. Когда сперва пишется Unit Test, а уж только потом разрабатывается функция. Функция разрабатывается до тех пор, пока Юнит Тест не будет пройден. Этот подход был мной впервые испробован в одном приложении на C++ и зарекомендовал себя с наилучшей стороны. У меня было ощущение непередаваемого восторга, когда к окончанию создания функций я был полностью уверен в их работоспособности, и дальнейшее их использование в программе не порождало никаких проблем.

Как это выглядит. Предположим мы написали функцию извлечения квадратного корня:
y=sqrt(x)

Соответственно, для нашего теста мы напишем другую функцию, которая будет работать по следующему алгоритму:

  • проверить что sqrt(-1) == ошибка
  • проверить что sqrt(0) == 0
  • проверить что sqrt(0.01) == 0.1
  • проверить что sqrt(1) == 1
  • проверить что sqrt(4) == 2
  • проверить что sqrt(7) == 2.6....


Мы можем написать функцию проверки до того, как мы пишем основную функцию. Таким образом мы как бы определим требования, которым должна удовлетворять разрабатываемая функция. Это и будет наше применение подхода Test Driven. И только после того, как наш Юнит Тест отработает безошибочно, мы можем с уверенностью использовать функцию в основной программе.

Остаётся открытым один вопрос: как выбрать набор тестовых параметров для тестируемой функции? Безусловно, в идеальном случае нужно использовать все возможные значения, но почти всегда это невозможно или трудоёмко. На тему выбора тестовых значений можно написать отдельную статью. Здесь же я постараюсь дать какие-то общие рекомендации:

  1. Нужно использовать как корректные данные, так и данные приводящие к ошибке. Т.к. нужно не только проверить, что функция выполняет возложенные на неё обязанности, но так же насколько корректно она обрабатывает ошибки.
  2. Нужно использовать граничные значения. Например, если диапазон значений от 0 до 100, нужно использовать для тестов значения раавные и 0, и 100. Если входные данные строка - нужно попробовать пустую строку и строку с максимальной длиной.
  3. Нужно использовать значения, выходящие за границы дозволенного. Если брать пример из предыдущего пункта, то нужно будет использовать значение 101, -1, для строки значение длинной max+1.
  4. Нужно постараться разбить множество всех возможных значений на подмножества схожих, для которых поведение функции будет одинаковым (называемых классы эквивалентности). И для каждого класса выбрать одно значение. Например, не имеет смысла проверять оба sqrt(4) и sqrt(9). Гораздо интереснее проверить sqrt(4) и sqrt(5), т.к. во втором случае функция вернёт иррациональное значение, а в первом случае целое.
  5. Если в функции есть ветвления (if, switch), нужно постараться, чтобы каждая из ветвей была затронута Юнит Тестом.

Попробую в следующей главе показать это на примере.


Немного практики написания Unit Test

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

Определим прототип нашей функции:

void CreateSubset(int & a1[], int a2[]);

Попробуем для разработки функции применить подход Test Driven. Определим набор тестовых данных. Для этого выделим несколько классов эквивалентности входных данных:

  1. Оба массива пустые.
  2. A1 пустой, в A2 есть элементы.
  3. В A1 есть элементы, A2 пустой.
  4. Оба содержат одинаковый набор элементов и имеют одинаковый размер.
  5. A1 содержит элементы, которых нет в A2.
  6. Часть элементов A1 содержится в A2, часть A2 содержится в A1 (оба множества имеют пересечение).
  7. Все элементы A1 содержатся в A2, но размер A2 больше.
  8. Небольшая часть элементов A1 содержится в A2. При этом элементы разбросаны по всему массиву.
  9. Небольшая часть элементов A1 содержится в A2. При этом элементы сконцентрированы в начале массива.
  10. Небольшая часть элементов A1 содержится в A2. При этом элементы сконцентрированы в конце массива.

Если наша функция правильно сработает для всех 10 случаев, мы можем спать спокойно и быть уверены, что те эксперты, которые используют эту функцию, не пострадают от её несовершенства :) Однако необходимо понимать, что протестировать что-то на 100% невозможно, и всегда останутся какие-то возможные скрытые дефекты.

Для удобства я создал библиотечку mql4unit. В неё добавил функции, необходимые для юнит тестов:

//-------------------------------------------------------------------+
//Глобальные переменные хранят состояние текущего теста
//-------------------------------------------------------------------+
int tests_passed;    //Количество успешных тестов
int tests_failed;    //Количество неуспешных тестов
int tests_total;     //Обрщее количество тестов
string test_name;    //Имя теста

//-------------------------------------------------------------------+
//Функция выполняет инициализицию тестовой среды для одного теста
//-------------------------------------------------------------------+
void UnitTestStart(string TestName)
{
   test_name = TestName;
   tests_passed = 0;
   tests_failed = 0;
   tests_total = 0;
   Print("*--------------------------------------------------*");
   Print("Начинаем выполнение юнит теста ", test_name);
}

//-------------------------------------------------------------------+
//функция вызывается в конце теста. Возвращвает true если все тесты
//прошли успешно. False в противном случае.
//-------------------------------------------------------------------+
bool UnitTestEnd()
{
   if (tests_failed == 0)
   {
      Print("УРА!!! ", test_name, " PASSED. ", tests_passed, " тестов выполнено успешно.");
   }
   else
   {
      Print(":((( ", test_name, " FAILED. ", tests_passed,"/",tests_total, " тестов выполнено успешно.");   
   }
   Print("*--------------------------------------------------*");
}

//-------------------------------------------------------------------+
//Функция выполняет тест для двух массивов типа int
//Возвращает true если массивы равны
//-------------------------------------------------------------------+
bool TestIntArray(int actual[], int expected[]){
   tests_total++;
   //Сравниваем размеры массивов
   if (ArraySize(actual) != ArraySize(expected))
   {
      Print("Test #", tests_total," ERROR. Размер массива ", ArraySize(actual), " вместо ", ArraySize(expected));
      tests_failed++;
      return(false);      
   }
   //Далее сравниваем поэлементно
   for (int i=0; i<ArraySize(actual);i++)
   {
      if (actual[i]!=expected[i]){
         Print("Test #", tests_total," ERROR. Значение элемента #",i,"=", actual, " вместо ", expected);
         tests_failed++;
         return(false);
      }
   }
   //Если все элементы равны - тест прошёл
   Print("Test #", tests_total," OK: Passed!");  
   tests_passed++;
   return(true);
}
Создадим новый тестовый скрипт “mytests” с пустым телом нашей функции. Создадим в нём функцию test и опишем в ней все юнит тесты.
bool Test()
{
   UnitTestStart("Проверка функции CreateSubset");
   Print("1. Оба массива пустые.");
   int a1_1[], a1_2[];
   int result_1[]; //Ожидаем в результате выполнения функции пустой массив
   CreateSubset(a1_1, a1_2);
   TestIntArray(a1_1, result_1);
   
   Print("2. A1 пустой, в A2 есть элементы");
   int a2_1[], a2_2[] = {1,2,3};
   int result_2[]; //Ожидаем в результате выполнения функции пустой массив
   CreateSubset(a2_1, a2_2);
   TestIntArray(a2_1, result_2);

   Print("3. В A1 есть элементы, A2 пустой");
   int a3_1[] = {1,2,3}, a3_2[];
   int result_3[]; //Ожидаем в результате выполнения функции пустой массив
   CreateSubset(a3_1, a3_2);
   TestIntArray(a3_1, result_3);

   Print("4. Оба содержат одинаковый набор элементов и имеют одинаковый размер");
   int a4_1[] = {1,2,3}, a4_2[] = {1,2,3};
   int result_4[] = {1,2,3}; //Ожидаем в результате выполнения функции неизменённый массив
   CreateSubset(a4_1, a4_2);
   TestIntArray(a4_1, result_4);

   Print("5. A1 содержит элементы, которых нет в A2");
   int a5_1[] = {4,5,6}, a5_2[] = {1,2,3};
   int result_5[]; //Ожидаем в результате выполнения функции пустой массив
   CreateSubset(a5_1, a5_2);
   TestIntArray(a5_1, result_5);
   
   Print("6. Часть элементов A1 содержится в A2, часть A2 содержится в A1 (оба множества имеют пересечение)");
   int a6_1[] = {1,2,3,4,5,6,7,8,9,10}, a6_2[] = {3,5,7,9,11,13,15};
   int result_6[] = {3,5,7,9}; //Ожидаем в результате выполнения функции пересечение массивов
   CreateSubset(a6_1, a6_2);
   TestIntArray(a6_1, result_6);
   
   Print("7. Все элементы A1 содержатся в A2, но размер A2 больше");
   int a7_1[] = {3,4,5}, a7_2[] = {1,2,3,4,5,6,7,8,9,10};
   int result_7[] = {3,4,5}; //Ожидаем в результате выполнения функции пересечение массивов
   CreateSubset(a7_1, a7_2);
   TestIntArray(a7_1, result_7);
   
   Print("8. Небольшая часть элементов A1 содержится в A2. При этом элементы разбросаны по всему массиву.");
   int a8_1[] = {1,2,3,4,5,6,7,8,9,10}, a8_2[] = {2,5,9};
   int result_8[] = {2,5,9}; //Ожидаем в результате выполнения функции пересечение массивов
   CreateSubset(a8_1, a8_2);
   TestIntArray(a8_1, result_8);
   
   Print("9. Небольшая часть элементов A1 содержится в A2. При этом элементы сконцентрированы в начале массива.");
   int a9_1[] = {1,2,3,4,5,6,7,8,9,10}, a9_2[] = {1,2,3};
   int result_9[] = {1,2,3}; //Ожидаем в результате выполнения функции пересечение массивов
   CreateSubset(a9_1, a9_2);
   TestIntArray(a9_1, result_9);

   Print("10. Небольшая часть элементов A1 содержится в A2. При этом элементы сконцентрированы в конце массива.");
   int a10_1[] = {1,2,3,4,5,6,7,8,9,10}, a10_2[] = {8,9,10};
   int result_10[] = {8,9,10}; //Ожидаем в результате выполнения функции пересечение массивов
   CreateSubset(a10_1, a10_2);
   TestIntArray(a10_1, result_10);
   
   return (UnitTestEnd());
}

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

Запустим наш тест на выполнение.


Как видим - результат удручающий :). Этого и следовало ожидать, ведь функция совсем не готова. Однако! 4 теста из 10 прошли успешно. Это означает, что теоретически мы могли бы и не заметить, что функция пустая, т.к. в некоторых случаях она бы срабатывала нормально :)

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

Теперь создадим саму функцию CreateSubset. Не будем здесь обсуждать эффективность и красоту этой функции ;)

void CreateSubset(int & a1[], int a2[]){
   int i=0;
   while(i<ArraySize(a1)){
      bool b_exist = false;
      for (int j=0; j<ArraySize(a2);j++){
         if (a1[i] == a2[j]) b_exist = true;
      }
      if (!b_exist){
         for (j=i; j<ArraySize(a1)-1;j++){
            a1[j] = a1[j+1];   
         }
         ArrayResize(a1, ArraySize(a1)-1);
      }else{
         i++;
      }
   }
}
Снова запустим тест:


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

Конечно, идеально было бы иметь возможность запускать юнит тест сразу же после компиляции библиотеки, но можно ли это сделать в MQL4, я пока не понял :) Скорее всего, что нет. Если знаете как - напишите :)

Каждый раз после запуска теста, мы можем спокойно вздохнуть и быть уверены, что всё работает как надо.


Несколько замечаний

  1. Может показаться, что написание тестов отнимает дополнительное время. Однако будьте уверены: время, потраченное на разработку юнит тестов, окупиться с лихвой.
  2. Не стоит так же фанатично разрабатывать Юнит Тесты на все функции. Нужно соблюдать баланс между важностью функции, вероятностью поломки и количеством кода внутри функции. Например, не стоит писать тест на простейшую функцию из пары строчек.
  3. Внутри юнит тестов можно делать всё, что угодно: открывать/закрывать ордера, использовать индикаторы, графические объекты и т.д. Тут ваша фантазия не должна быть ограничена.


И, напоследок

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

Всем удачи и кода без ошибок! :)

Прикрепленные файлы |
mql4unit.mq4 (5.24 KB)
testscript.mq4 (4.18 KB)
Андрей
Андрей | 7 окт 2010 в 06:15
Klinch:

Гладко было на бумаге, да забыли про авраги )))

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


С точки зрения MQL4, таймсерии - это такие же массивы как и любые другие. Поэтому можно выделить отдельные функции для работы с тайм сериями, и передавать им тайм серии в качестве параметров. Так даже будет лучше с точки зрения изолированности кода. Например, если нужна функция которая определяет было ли пробитие уровня L на последних N барах, то можно определить её как:

bool IsExcess (double h[], double L, int N);

А затем вызвать, передав функции массив High.

if (IsExсess(High, 1.5000, 10)){
   //Что-нибудь делаем
}
Тогда UnitTest для этой функции может использовать любой произвольный массив. Тестовый массив может загружаться из CSV файла если данных нужно много. Можно сделать такой файл один раз и потом использовать во всех тестах.
MQL4 Comments
MQL4 Comments | 11 окт 2010 в 12:46
Evklid:Тогда UnitTest для этой функции может использовать любой произвольный массив. Тестовый массив может загружаться из CSV файла если данных нужно много. Можно сделать такой файл один раз и потом использовать во всех тестах.

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

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

В отличие от вопроса как тестировать, более важным считаю вопрос что тестировать. В статье говорилось "Остаётся открытым один вопрос: как выбрать набор тестовых параметров для тестируемой функции? Безусловно, в идеальном случае нужно использовать все возможные значения, но почти всегда это невозможно или трудоёмко. На тему выбора тестовых значений можно написать отдельную статью. Здесь же я постараюсь дать какие-то общие рекомендации: ..."

Не соглашусь с автором в том что использовать все возможные значения является идеальным вариантом )) Мои скудные знания мат. анализа подсказывают мне, что исследовать достаточно функцию в точках экстремума, но т.к. разрабатываемые нами функции в общем случае не являются линейными, то встает вопрос создания мат. аппарата схожего с применяемым для линейных функций. Из этого следует, что необходимо научиться брать производную из таких функций как "if", "while" и т.д. с целью определения точек экстремума этих функций.

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

MQL4 Comments
MQL4 Comments | 24 окт 2010 в 19:27

Маленькое замечание по поводу отладки кода. Функции print и особенно alert, мне показалось, работают по настроению. Мне стало намного проще определять величины переменных, процесс выполнения функций (короче - отладочные данные), когда вспомнил о возможности записи в файл. Оказалось, что организовать легко читаемый отчет о работе эксперта несложно даже такому ленивому как я. Пожалуй, запись в файл - единственная надежно выполняемая функция. У меня, по крайней мере. Подробности вряд ли кому нужны, все очень просто.

MQL4 Comments
MQL4 Comments | 22 мар 2011 в 15:09

Возможно, не в тему, точнее, не совсем в тему. Точнее, совсем не в тему.

Существует весьма благоприятный тестер (небесплатно, правда):

http://www.forextester.ru/

Он позволяет практически идентичный прокат любого материала на бывших ситуациях с любой скоростью, оч хорошая машина. Но огромный недостаток: он не работает с MQL4. Он позволяет писать стратегии, но на дельфи или си++.

Т.е. это для тестирования стратегий от руки или экспертов в целом, с выдачей полной статистической картины.

Тестер, встроенный в метатрейдер, рядом не валялся.

Пардон, но может кому интересно...

Я все хочу попросить автора тестера приделать MQL4, да боюсь, что мне тогда будет не по карману...

Bakhodir Radjabov
Bakhodir Radjabov | 29 сен 2012 в 08:22
vlad_cum:

Маленькое замечание по поводу отладки кода. Функции print и особенно alert, мне показалось, работают по настроению. Мне стало намного проще определять величины переменных, процесс выполнения функций (короче - отладочные данные), когда вспомнил о возможности записи в файл. Оказалось, что организовать легко читаемый отчет о работе эксперта несложно даже такому ленивому как я. Пожалуй, запись в файл - единственная надежно выполняемая функция. У меня, по крайней мере. Подробности вряд ли кому нужны, все очень просто.


Согласен иногда функции принт и алерт не пишут в журнал нужных логов
Переход на новые рельсы: пользовательские индикаторы в MQL5 Переход на новые рельсы: пользовательские индикаторы в MQL5

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

Вот мы и получили долгожданные MetaTrader 5 и MQL5 Вот мы и получили долгожданные MetaTrader 5 и MQL5

Это очень краткий обзор MetaTrader 5. Я не могу описать все новшества системы за столь короткий период времени - тестирование стартовало 09-09-2009. Это символическая дата, и я уверен, что это будет счастливым числом. Всего несколько дней у меня на руках бета-версия терминала MetaTrader 5 и MQL5. Я не успел опробовать все, что в нем есть нового, но то, что есть, уже впечатляет.

Портфельная торговля в MetaTrader 4 Портфельная торговля в MetaTrader 4

В статье обсуждаются принципы портфельной торговли и особенности применения к валютному рынку. Рассматриваются несколько простых математических моделей для формирования портфеля. Приводятся примеры практической реализации портфельной торговли в MetaTrader 4: портфельный индикатор и советник для полуавтоматической торговли. Описываются элементы торговых стратегий, их достоинства и "подводные камни".

Работа с корзинами валютных пар на рынке Форекс Работа с корзинами валютных пар на рынке Форекс

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