Веб-скрапинг данных о доходности облигаций

24 апреля 2019, 11:43
Steven Brown
2
1 588

Введение

При разработке систем автоматической торговли мы почти всегда используем данные технических индикаторов, которые анализируют прошлое, чтобы предсказать будущее поведение цены. Но без учета фундаментальных сил, движущих рынки, мы будем очевидно в менее выгодном положении по сравнению с теми, кто при принятии торговых решений дополнительно учитывает фундаментальные данные. Работу торгового советника может улучшить индикатор, основанный на автоматически собираемых фундаментальных данных. Наиболее сильное влияние на курсы соответствующих валют оказывает, пожалуй, информация о процентных ставках. Ставки центральных банков, как правило, гораздо менее волатильны, чем доходность государственных облигаций, таких как, например, 10-летние казначейские облигации в США. При этом колебания на мировых рынках облигаций наблюдаются на всех таймфреймах. Доходность отражает ожидания рынка относительно будущих ставок центрального банка. Доходность облигаций часто является опережающим индикатором изменения процентных ставок и обменных курсов. На форексе применительно к валютной паре анализируют дифференциал процентных ставок, дельту или изменение процентного дифференциала на разных таймфреймах. На рисунке 1 показан случай, когда движение процентного дифференциала в базовых пунктах в положительном направлении стало опережающим индикатором движения валютной пары EURUSD в том же направлении. В этой статье мы рассмотрим, как собирать данные о доходности облигаций из интернета и как получить на основе этих данных значения процентного дифференциала и дельту.


Опережающий индикатор дифференциала процентных ставок

Рис 1. Индикатор процентного дифференциала на часовом графике EURUSD.

Скрапинг 101

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

Как узнать URL

Самая первая задача в скрапинге — получение URL-адреса файла, содержащего элемент, который необходимо скачать. Это может быть URL-адрес веб-страницы, если элемент встроен в html-текст на этой странице. В этом случае элемент можно вычленить из HTML-текста страницы. Также URL-адрес может быть встроен в ссылку на странице. По этой ссылке веб-браузер выкачивает нужный элемент для отображения на странице. Скрапер также может извлечь ссылку из HTML-текста и получить по ней нужный элемент. Кроме того URL может передаваться браузеру через скрипт, ссылка на который расположена на странице. Браузер загружает эту страницу и запускает скрипт для подгрузки элемента. В этом случае программа-скрапер не будет запускать скрипт, а может напрямую использовать сгенерированный скриптом URL-адрес. URL-адрес элемента можно получить с помощью инструментов разработчика в Internet Explorer или Google Chrome. Откуда бы ни был получен URL, скрапер может использовать этот адрес для загрузки файла с веб-сервера и извлечения из него необходимых данных. Информация о доходности облигаций публикуется на нескольких финансовых веб-сайтах. Для начала рассмотрим сайт https://www.marketwatch.com/investing/bond/tmubmusd10y?countrycode=bx и на его основе создадим примеры скрапера.

Во-первых, давайте посмотрим на HTML-файл, который браузер загружает с веб-сервера при нажатии на эту ссылку. В веб-браузере Chrome нажмите на кнопку инструментов в правом верхнем углу, переместите курсор на "Дополнительные инструменты", выберите "Сохранить страницу как", скачайте html-файл и откройте его в текстовом редакторе, например Notepad. Теперь видно, что на сайте боты с легкостью могут получить значение котировки, поскольку она включена в одну из серий мета-тегов в заголовке HTML-файла. Метаданные не отображаются браузерами и не влияют на внешний вид страницы. Такие данные доступны программам, которые скачивают html-файл. Котировка содержится в теге <meta name="price" content="3.066">, через 28 символов после начала тега. Именно такое значение отображается на странице в веб-браузере. Программа-скрапер может найти в файле текстовую строку [<meta name="price" content=], отсчитать 28 символов от начала мета-тега и сконвертировать текст в число с плавающей точкой. Для большей ясности html-текст в статье отображается в квадратных скобках вместо используемых обычно кавычек.

Создаем лучшего бота

Если скачать html-файл по указанной выше ссылке, можно увидеть, что он содержит большие блоки данных из таблицы стилей, а общий размер файла — 295 килобайт. А интересующий нас мета-тег занимает всего 3 килобайта в начале файла. Хороший бот не должен скачивать больше данных, чем ему нужно, поэтому при каждом получении котировки разумно скачивать только первые 4 килобайта. К сожалению, в mql5-функции WebRequest() невозможно ограничить объем скачиваемых данных. Массив, содержащий данные ответа сервера, должен быть динамическим. Если же использовать статический массив указанного размера, программа скомпилируется, но во время выполнения возникнет ошибка, которая приведет к падению терминала. В запрос к серверу можно включить заголовок Range, но большинство серверов не поддерживают такой заголовок. Поэтому зачастую размер скачиваемых с сервера данных почти равен размеру запрашиваемого html-файла. Для этой задачи эффективнее использовать функции в библиотеке wininet.dll, включенной в Windows. Функция InternetReadFile() из библиотеки может загружать определенное количество байтов, даже если заголовок Range не поддерживается и загрузка начинается с начала файла. Функции WinINet можно импортировать в mql5-скрипт или советник. К статье приложен скрипт ScraperBot01.mq5, который выгружает первые 4 килобайта html-файла, определяет положение интересующего мета-тега в загруженном тексте, находит в этом теге текст с последней котировкой доходности 10-летних облигаций, конвертирует его в число с плавающей точкой и выводит полученное значение в терминал.

ScraperBot 01

В самом начале исходного кода ScraperBot01.mq5 происходит импорт библиотеки wininet.dll и создание прототипов вызываемых функций. При объявлении параметры имеют тип, совместимый с mql5. Документацию к функциям WinINet можно почитать на сайте Microsoft: https://docs.microsoft.com/en-us/windows/desktop/wininet/wininet-reference.

#import "wininet.dll"
  int InternetCheckConnectionW(string& lpszUrl, uint dwFlags, uint dwReserved);
  int InternetOpenW(string& lpszAgent, uint dwAccessType, string& lpszProxyName, string& lpszProxyBypass, uint dwFlags);
  int InternetOpenUrlW(int hInternetSession, string& lpszUrl, string& lpszHeaders, uint dwHeadersLength, uint dwFlags, uint dwContext);
  int InternetReadFile(int hFile, uchar& lpBuffer[], uint dwNumberOfBytesToRead, uint& lpdwNumberOfBytesRead);
  int InternetCloseHandle(int hInternet);
#import

uchar uc_Buffer[4096]; // для InternetReadFile() ожидается статический буфер.
float f_US;

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

Объявляем некоторые локальные переменные в начале OnStart(), а другие для большей ясности будем объявлять по мере необходимости. Для начала проверим подключение к интернету. Возвращаемые значения функций, вызываемых в этом скрипте, выводятся в терминал. Это позволяет сразу же увидеть, успешно ли сработала функция, а в случае ошибки открытые дескрипторы закрываются и работа скрипта завершается оператором return. Если есть подключение к интернету, происходит инициализация дескриптора iNet1 для последующих вызовов функций WinINet. Значение дескриптора должно быть больше нуля.

void OnStart() 
{ bool bResult;  int i, iNet1, iNet2;  

  string stURL = "http://www.msn.com"; 
  bResult = InternetCheckConnectionW(stURL, 1, 0); // 1 == FLAG_ICC_FORCE_CONNECTION
  Print("InternetCheckConnectionW() returned ", bResult);
  if(!bResult) return;
  
  string stAgent = "Mozilla/5.0", stNull = "";
  iNet1 = InternetOpenW(stAgent, // _In_ LPCTSTR lpszAgent 
                        1,       // 1 == INTERNET_OPEN_TYPE_DIRECT
                        stNull,  // _In_ LPCTSTR lpszProxyName
                        stNull,  // _In_ LPCTSTR lpszProxyBypass
                        NULL);   // _In_ DWORD dwFlags
  Print("iNet1 == ", iNet1);
  if(iNet1==0) return;


Затем устанавливается соединение с веб-сервером и инициализируется дескриптор iNet2 для загрузки html-файла.

  stURL = "https://www.marketwatch.com/investing/bond/tmubmusd10y?countrycode=bx";
  string stHdr = "Accept: text/*";
  iNet2 = InternetOpenUrlW(iNet1,            // HINTERNET hInternet,
                           stURL,            // LPCWSTR   lpszUrl,
                           stHdr,            // LPCWSTR   lpszHeaders,
                           StringLen(stHdr), // DWORD     dwHeadersLength,
                           0x00080000,       // DWORD     dwFlags, 0x00080000 == INTERNET_FLAG_NO_COOKIES
                           NULL);            // DWORD_PTR dwContext
  Print("iNet2 == ", iNet2);
  if(iNet2==0) 
  { InternetCloseHandle(iNet1);
    return;
  }


Теперь можем выгрузить данные с веб-сервера.

  uint uGet, uGot;
  uGet = 4080; // количество выгружаемых байтов
  bResult = InternetReadFile(iNet2,     // _In_  HINTERNET hFile
                             uc_Buffer, // _Out_ LPVOID lpBuffer
                             uGet,      // _In_  DWORD dwNumberOfBytesToRead
                             uGot);     // _Out_ LPDWORD lpdwNumberOfBytesRead

  Print("InternetReadFile() returned ", bResult, ". Number of bytes read: ", uGot);
  InternetCloseHandle(iNet2);  // загрузка завершена
  if(!bResult) {InternetCloseHandle(iNet1); return;}
  uc_Buffer[uGot] = 0// Добавляем ноль, чтобы завершить строку в буфере uc_Buffer.


Теперь в скачанном тексте ищем интересующий нас мета-тег и, если он найден, добавляем смещение в 28 символов — это будет индекс текста со значением в буфере uc_Buffer. Для доступа к тексту вызываем функцию StringSubstr(), передав в нее в переменной "i" значение индекса. Если в тексте про этому индексу не содержится нужное нам значение котировки, функция StringToDouble() возвращает ноль, что означает ошибку, кроме случаев, когда доходность по облигации равна нулю. Обратите внимание, что для кавычек в строке используется экранировние (\"), чтобы не спутать с кавычками в начале и конце строки.

  i = StringFind(CharArrayToString(uc_Buffer), "<meta name=\"price\" content=", 0); // 0 == Позиция начала поиска
  Print("Offset of \'<meta name=\"price\" content=\' == ", i); 
  if(i == -1) {Print("String not found.");  InternetCloseHandle(iNet1);  return;} 
  i += 28; // Следующий индекс для определения положения текста со значением доходности.
  f_US = StringToDouble(StringSubstr(CharArrayToString(uc_Buffer), i, 8));
  Print("US 10-year T-note yield, stored in variable f_US: ", f_US);
  InternetCloseHandle(iNet1); // Выполнено с помощью wininet.
}//END void OnStart()

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

Альтернативный вариант

Мы познакомились с общей процедурой веб-скрапинга данных. Теперь давайте рассмотрим вариант, когда данные о доходности не располагаются в мета-теге. В этом случае исследуем html-файл веб-страницы и поищем источник значения, отображаемого на странице. Зная последнюю котировку доходности облигации, можно использовать функцию поиска в текстовом редакторе и найти строку текста с этим значением. В выгруженном html-файле котировка может располагаться в трех других местах, помимо мета-тега. Первое — структурированные данные в формате JSON-LD. Этот элемент позволяет поисковым системам и веб-краулерам с легкостью получить доступ к информации о странице. Ниже показан такой фрагмент данных, отформатированный для большей наглядности с помощью разрывов строк.

<script type="application/ld+json">
{ "@context":"http://schema.org/",
  "@type":"Intangible/FinancialQuote",
  "url":"https://www.marketwatch.com/investing/bond/tmubmusd10y?countrycode=bx",
  "name":"U.S. 10 Year Treasury Note",
  "tickerSymbol":"TMUBMUSD10Y",
  "exchange":"Tullett Prebon",
  "price":"3.061",
  "priceChange":"0.007",
  "priceChangePercent":"0.22%",
  "quoteTime":"Sep 28, 2018 5:07 p.m.",
  "priceCurrency":"PERCENT"
}
</script>

Алгоритм сначала находит смещение тега <script type="application/ld+json">, а затем от этой позиции ищет смещение значения ["price":"] и тега </script>. Если смещение ["price":"] меньше смещения тега </script>, т.е. значение цены находится внутри фрагмента данных, к смещению текста ["price":"] добавляем 9 и получаем смещение значения самой котировки. Работа алгоритма показана в приложенном к статье скрипте ScraperBot02.mq5.

ScraperBot 02

Скрипт выкачивает html-файл до размера, указанного в uMax. При первом запуске для параметра uMax необходимо установить значение, гораздо больше ожидаемого размера, например 1 миллион. Скрипт покажет количество выгруженных байтов, и если это значение равно или примерно равно параметру uMax, значение uMax необходимо увеличить. Также скрипт сообщит о смещении тега <script type=\"application/ld+json\"> в файле. Так вот значение этого параметра uMax затем можно установить немного больше смещения тега. У нас смещение равно 166696, поэтому устанавливаем uMax равным 180224 — это позволит выгрузить достаточную часть файл, содержащую нужный нам фрагмент JSON-LD, не скачивая при этом весь файл. В скрипте используется статический массив для загрузки чанков по 16 kb, которые затем копируются и собираются в динамическом массиве. Массивы объявлены на глобальном уровне.

uchar uc_Buffer[16400], uc_DynBuf[];

ScraperBot02 идентичен скрипту ScraperBot01 до части, в которой данные выгружаются с веб-сервера — здесь данные разбиваются на чанки. Функция InternetReadFile вызывается в цикле do-while до тех пор пока необходимый объем данных не будет скачан.

  uint uGet, uGot, uDst, uMax;
  uGet = 16384;    // количество байтов, загружаемых за один вызов InternetReadFile, должно быть хотя бы на 1 байт меньше размера uc_Buffer
  uGot = uDst = 0; // uGot - количество байтов, скачанных за вызов InternetReadFile; uDst - общее количество скачанных байтов
  uMax = 180224;   // максимальное количество загружаемых байтов

  do
  { bResult = InternetReadFile(iNet2,     // _In_  HINTERNET hFile
                               uc_Buffer, // _Out_ LPVOID lpBuffer
                               uGet,      // _In_  DWORD dwNumberOfBytesToRead
                               uGot);     // _Out_ LPDWORD lpdwNumberOfBytesRead

    uc_Buffer[uGot] = 0; // Добавляем ноль, чтобы завершить строку в буфере uc_Buffer

    ArrayCopy(uc_DynBuf, // массив-приемник 
              uc_Buffer, // массив-источник 
              uDst,      // индекс в массиве-приемнике, с которого начинается запись 
              0,         // индекс в массиве-источнике, с которого начинается копирование 
              uGot);     // количество копируемых элементов 
    uDst += uGot; // индекс в массиве-приемнике со сдвигом для следующего прохода в цикле
  }while(bResult && uGot > 0 && uDst < uMax);
 
  Print("Size of uc_DynBuf == ", ArraySize(uc_DynBuf));
  Print("Bytes downloaded  == ", uDst);


Теперь скрипт ScraperBot02 находит тег <script type=\"application/ld+json\"> и сохраняет смещение как индекс в переменной i. Начиная с этого смещения скрипт находит текст ["price":"] и сохраняет смещение в переменной j. Затем находит тег </script> в конце фрагмента и сохраняет это смещение в переменной k. Если переменная j меньше k, к значению j прибавляется 9 и получается смещение для текста со значением котировки, которое преобразуется в значение с плавающей запятой в переменной f_US и выводится в терминал.

  int i, j, k; // индексы

  i = StringFind(CharArrayToString(uc_DynBuf), "<script type=\"application/ld+json\">", 0); // 0 == позиция для начала поиска 
  Print("Offset of <script type=\"application/ld+json\"> == ", i); 
  if(i == -1) {Print("<script type=\"application/ld+json\"> not found.");  InternetCloseHandle(iNet1);  return;}

  j = StringFind(CharArrayToString(uc_DynBuf), "\"price\":\"", i); // i == позиция для начала поиска 
  if(j == -1) {Print("\"price\":\" not found.");  InternetCloseHandle(iNet1);  return;}
  Print("Offset of \"price\":\" == ", j); 

  k = StringFind(CharArrayToString(uc_DynBuf), "</script>", i); // i == позиция для начала поиска
  Print("Offset of </script> == ", k); 
  if(j > k) {Print("Offset of \"price\":\" is greater than offset of </script>");  InternetCloseHandle(iNet1);  return;}

  j += 9; // Следующий индекс для определения положения текста со значением доходности.
  f_US = StringToDouble(StringSubstr(CharArrayToString(uc_DynBuf), j, 8));
  Print("US 10-year T-note yield, stored in variable f_US: ", f_US);
  InternetCloseHandle(iNet1); // Выполнено с помощью wininet.
}//END void OnStart()

Использование инструментов разработчика

Также источник котировки на веб-сервере можно определить с использованием инструментов разработчика в браузере Chrome. Нажмите на кнопку меню в правом верхнем углу экрана, откройте инструменты разработчика и в адресной строке укажите https://www.marketwatch.com/investing/bond/tmubmusd10y?countrycode=bx. Выберите сверху вкладку Network, а в типе отслеживаемых событий выберите "XHR". При выборе любого из событий справа открывается панель с деталями, такими как заголовки и ответ. Нас интересует ответ события, помеченного как "quoteByDialect...". Для его выделения щелкните правой кнопкой мыши на панели и выберите опцию "Выбрать все". Нажмите Ctrl+C, чтобы скопировать выделенный текст в буфер обмена, и вставьте его в текстовый редактор. Сама котировка находится в блоке текста после строки ["CompositeTrading":{"Last":{"Price":{"Iso":"PERCENT","Value":]. URL-адрес для получения этого блока можно найти на вкладке Headers. Адрес получается довольно длинный: https://api.wsj.net/api/dylan/quotes/v2/comp/quoteByDialect?dialect=official&needed=CompositeTrading|BluegrassChannels&MaxInstrumentMatches=1&accept=application/json&EntitlementToken=cecc4267a0194af89ca343805a3e57af&ckey=cecc4267a0&dialects=Charting&id=Bond-BX-TMUBMUSD10Y,Bond-BX-TMBMKDE-10Y. Нажмите на ссылку прямо из статьи чтобы увидеть блок текста в окне браузера. Фактически он состоит из двух блоков, расположенных один за другим, потому что в конце адреса через запятую стоят два тикера. Первый Bond-BX-TMUBMUSD10Y - котировки 10-летних казначейских облигаций США. Второй Bond-BX-TMBMKDE-10Y - котировки 10-летних государственных облигаций Германии. Если удалить второй тикер из URL-адреса, размер скачиваемого текста уменьшится с 7,1 до 3,6 килобайт.

Скрипт ScraperBot03 в приложении к статье скачивает блок текста для тикера "TMUBMUSD10Y", находит строку ["CompositeTrading":{"Last":{"Price":{"Iso":"PERCENT","Value":], добавляет смещение в 61 символ от начала строки, использует это в качестве индекса положения текста с котировкой, преобразует текст в число с плавающей запятой и выводит его в терминал. Код скрипта здесь не показан, потому что он создан на основе скрипта ScrapterBot01. Преимущество такого способа в небольшом размере загружаемого файла. Так как размер файла по указанному адресу составляет всего 3,6 килобайт, для его скачивания можно использовать mql5-функцию WebRequest вместо функций из wininet.dll.

ScraperBot 04

Этот скрипт выгружает те же данные, что и ScraperBot 03, используя при этом функцию WebRequest вместо WinINet. Для работы функции WebRequest, необходимо добавить основной URL сервера (в данном случае "https://api.wsj.net") в список разрешенных адресов в платформе MetaTrader 5 меню "Сервис" → "Настройки" → "Советники". Глобальный массив символов ch_Data не передает никаких данных в WebRequest, он нужен только для удовлетворения требований к параметру такого типа.

char ch_Buffer[], ch_Data[16];
float f_US;

void OnStart() 
{ int i;   
  string stURL = "https://api.wsj.net/api/dylan/quotes/v2/comp/quoteByDialect?dialect=official&needed=CompositeTrading|BluegrassChannels&"
                 "MaxInstrumentMatches=1&accept=application/json&EntitlementToken=cecc4267a0194af89ca343805a3e57af&ckey=cecc4267a0&"
                 "dialects=Charting&id=Bond-BX-TMUBMUSD10Y";
  string stHdr = "Accept: text/*, User-Agent: Mozilla/5.0";
  string stRspHdr; // заголовок ответа
  
  i = WebRequest("GET",     // const string  method, метод HTTP 
                 stURL,     // const string  url,       URL-адрес
                 stHdr,     // const string  заголовки,  
                 1024,      // int           timeout, 
                 ch_Data,   // const char    &data[],   массив тела HTTP-сообщения
                 ch_Buffer, // char          &result[], массив с данными ответа сервера
                 stRspHdr); // string        &result_headers 

  Print("Server response code: ", i);
  if(i == -1) {Print("GetLastError == ", GetLastError());  return;}
  Print("Size of ch_Buffer (bytes downloaded) == ", ArraySize(ch_Buffer));
  Print("Response header:\n", stRspHdr);   
 
  string stSearch = "\"CompositeTrading\":{\"Last\":{\"Price\":{\"Iso\":\"PERCENT\",\"Value\":";
  i = StringFind(CharArrayToString(ch_Buffer), stSearch, 0); // 0 == position from which search starts 
  Print("Offset of ", stSearch, " == ", i); 
  if(i == -1) {Print(stSearch, " not found.");  return;}
  i += 61; // Следующий индекс для определения положения текста со значением доходности.
  f_US = StringToDouble(StringSubstr(CharArrayToString(ch_Buffer), i, 8));
  Print("US 10-year T-note yield, stored in variable f_US: ", f_US);
}//END void OnStart()


Другие облигации

Для расчета процентного дифференциала нужны две процентные ставки. Скрипты, выгружающие данные о доходности 10-летних казначейских облигаций США, можно отредактировать и получать данные о доходности 10-летних государственных облигаций Германии, заменив название котировки в URL-адресах с "TMBMKDE-10Y" на "TMUBMUSD10Y". Также можно использовать тикеры для государственных бумаг других стран. На других финансовых порталах могут использоваться другие названия тикеров, но принцип тот же. Немецкие облигации часто используют в разрезе европейской процентной ставки, но при этом не учитывается влияние других стран Европейского Союза. Можно рассчитать композитную процентную ставку по евро на основе данных о гос облигаций двух или более европейских стран. В нашем примере мы используем данные о доходности гос облигаций трех крупнейших стран-членов ЕС (по значению ВВП) и рассчитаем ставку композитной "Европейской облигации". Страны: Германия, Франция и Италия. Добавим взвешивание доходности их облигаций в соответствии с относительными размерами их экономик из следующей таблицы.


2017 ВВП в млрд евро

Германия3197
Франция2241
Италия1681
Общая7119


Весами для каждой страны будут служить отношение ее ВВП к сумме трех значений. Получились такие веса: Германия = 0,449, Франция = 0,315, Италия = 0,236. Сумма значений весов равна единице, а сами они используются в качестве коэффициентов для расчета композитного значения доходности европейских облигаций, которая рассчитывается по формуле:

f_EU = 0.449*f_DE + 0.315*f_FR + 0.236*f_IT

где f_EU — это доходность композитной европейской облигации, f_DE — доходность немецкой облигации, f_FR — доходность французской облигации, а f_IT — доходность итальянской облигации.

Процентный дифференциал

Котировка EURUSD представляет собой стоимость евро, выраженную в долларах. Поэтому она движется в направлении евро и соответственно противоположно доллару. Чтобы процентный дифференциал предсказывал движение валютной пары в том же направлении, рост доходности композитной европейской облигации должна двигать процентный дифференциал в положительном направлении, а рост казначейской облигации США должен двигать его в отрицательном направлении. Поэтому процентный дифференциал рассчитывается как f_EU - f_US. Во время написания статьи значение f_EU составляло 1,255, а значение f_US — 3,142. Таким образом дифференциал ставок: 1,255 - 3,142 = -1,887. Движение в положительном направлении, скажем, с -1,887 до -1,798, предсказывало бы движение пары EURUSD вверх. Движение в отрицательном направлении, скажем, с -1,887 до -1.975, предсказывало бы движение пары EURUSD вниз. Сила или надежность индикатора зависит от величины изменения процентного дифференциала. Движение процентных ставок обычно измеряется в базисных пунктах или в сотых долях процента. Движение дифференциала процентных ставок от -1,887 к -1,975 означает движение в 8,8 базисных пунктов в отрицательном направлении, достаточно сильное движение для внутридневного диапазона, что, вероятно, указывает на нисходящее движение валютной пары на часовом таймфрейме. Движение в один или два базисных пункта относится к области рыночного шума и вряд ли может считаться надежным индикатором движения валютной пары.

ScraperBot 05

Скрипт выгружает информацию о доходности всех четырех облигаций, рассчитывает доходность композитной европейской облигации и выводит в терминал дифференциал ставок. Скрипт создан на основе кода ScraperBot 04, но вместо отправки на сервер отдельного запроса по каждой облигации к URL добавляются все четыре тикера. Все четыре котировки возвращаются одной загрузкой размером 14 kb, в которой содержатся четыре блока текста. Скрипт ScraperBot 05 находит тикер символа, затем определяет место строки, идущей перед котировкой, и возвращает ошибку, если такая строка не найдена.

char ch_Buffer[], ch_Data[16];      // глобальные буферы
float f_US, f_DE, f_FR, f_IT, f_EU; // глобальные переменные для хранения данных о доходности

void OnStart() 
{ int i;   
  string stURL = "https://api.wsj.net/api/dylan/quotes/v2/comp/quoteByDialect?dialect=official&needed=CompositeTrading|BluegrassChannels&"
                 "MaxInstrumentMatches=1&accept=application/json&EntitlementToken=cecc4267a0194af89ca343805a3e57af&ckey=cecc4267a0&"
                 "dialects=Charting&id=Bond-BX-TMUBMUSD10Y,Bond-BX-TMBMKDE-10Y,Bond-BX-TMBMKFR-10Y,Bond-BX-TMBMKIT-10Y"; // четыре тикера

  string stHdr = "Accept: text/*, User-Agent: Mozilla/5.0";
  string stRspHdr; // заголовок ответа

  i = WebRequest("GET",     // const string  method, метод HTTP 
                 stURL,     // const string  url,       URL-адрес
                 stHdr,     // const string  заголовки,  
                 1024,      // int           timeout, 
                 ch_Data,   // const char    &data[],   массив тела HTTP-сообщения
                 ch_Buffer, // char          &result[], массив с данными ответа сервера
                 stRspHdr); // string        &result_headers 

  Print("Server response code: ", i);
  if(i == -1) {Print("GetLastError == ", GetLastError());  return;}
  Print("Size of ch_Buffer (bytes downloaded) == ", ArraySize(ch_Buffer));
   
  string stSearch = "\"CompositeTrading\":{\"Last\":{\"Price\":{\"Iso\":\"PERCENT\",\"Value\":";

// Получаем доходность 10-летних казначейских облигаций США.
  i = StringFind(CharArrayToString(ch_Buffer),"\"Ticker\":\"TMUBMUSD10Y\"", 0); // 0 == position from which search starts 
  if(i == -1) {Print("\"Ticker\":\"TMUBMUSD10Y\" not found.");  return;}
  i = StringFind(CharArrayToString(ch_Buffer), stSearch, i); // i == позиция, с которой начинается поиск 
  Print("Offset of ", stSearch, " == ", i); 
  if(i == -1) {Print(stSearch, " not found.");  return;}
  i += 61; // Следующий индекс для определения положения текста со значением доходности.
  f_US = StringToDouble(StringSubstr(CharArrayToString(ch_Buffer), i, 8));
  Print("US 10-year T-note yield, stored in variable f_US: ", f_US);

// Получаем доходность 10-летних государственных облигаций Германии.
  i = StringFind(CharArrayToString(ch_Buffer),"\"Ticker\":\"TMBMKDE-10Y\"", i); // i == позиция, с которой начинается поиск 
  if(i == -1) {Print("\"Ticker\":\"TMBMKDE-10Y\" not found.");  return;}
  i = StringFind(CharArrayToString(ch_Buffer), stSearch, i); // i == позиция, с которой начинается поиск 
  Print("Offset of ", stSearch, " == ", i); 
  if(i == -1) {Print(stSearch, " not found.");  return;}
  i += 61; // Следующий индекс для определения положения текста со значением доходности.
  f_DE = StringToDouble(StringSubstr(CharArrayToString(ch_Buffer), i, 8));
  Print("German 10-year government bond yield, stored in variable f_DE: ", f_DE);

// Получаем доходность 10-летних государственных облигаций Франции.
  i = StringFind(CharArrayToString(ch_Buffer),"\"Ticker\":\"TMBMKFR-10Y\"", i); // i == позиция, с которой начинается поиск 
  if(i == -1) {Print("\"Ticker\":\"TMBMKFR-10Y\" not found.");  return;}
  i = StringFind(CharArrayToString(ch_Buffer), stSearch, i); // i == позиция, с которой начинается поиск 
  Print("Offset of ", stSearch, " == ", i); 
  if(i == -1) {Print(stSearch, " not found.");  return;}
  i += 61; // Следующий индекс для определения положения текста со значением доходности.
  f_FR = StringToDouble(StringSubstr(CharArrayToString(ch_Buffer), i, 8));
  Print("French 10-year government bond yield, stored in variable f_FR: ", f_FR);

// Получаем доходность 10-летних государственных облигаций Италии.
  i = StringFind(CharArrayToString(ch_Buffer),"\"Ticker\":\"TMBMKIT-10Y\"", i); // i == позиция, с которой начинается поиск 
  if(i == -1) {Print("\"Ticker\":\"TMBMKIT-10Y\" not found.");  return;}
  i = StringFind(CharArrayToString(ch_Buffer), stSearch, i); // i == позиция, с которой начинается поиск 
  Print("Offset of ", stSearch, " == ", i); 
  if(i == -1) {Print(stSearch, " not found.");  return;}
  i += 61; // Следующий индекс для определения положения текста со значением доходности.
  f_IT = StringToDouble(StringSubstr(CharArrayToString(ch_Buffer), i, 8));
  Print("Italian 10-year government bond yield, stored in variable f_IT: ", f_IT);

// Получаем доходность композитной европейской облигаций.
  f_EU = 0.449*f_DE + 0.315*f_FR + 0.236*f_IT;
  Print("European composite bond yield: ", f_EU);

// Calculate interest rate differential.
  Print("Interest rate differential, f_EU-f_US = ", f_EU-f_US);
}//END void OnStart()

Скрипт ScraperBot06.mq4 реализует работу скрипта ScraperBot05.mq5 с использованием функций WinINet вместо WebRequest, которая в платформе MetaTrader 4 считается ненадежной.


Delta

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

EMA = a*Delta + (1-a)*EMAp

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

a = 2.0 / (n+1)

где n — количество периодов EMA, может быть целым или дробным. Диапазон альфа обычно больше нуля и меньше или равен 1, т.е. 0 < a <= 1. Если альфа = 1, при расчете не учитывается предыдущее значение EMA, поэтому EMA показывает только текущую дельту.

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

Выводы

Вы можете скопировать исходный код прилагаемых скриптов в свой советник для автоматизации сбора фундаментальных данных. Показанные в этой статье методы сбора данных о доходности облигаций из интернета могут работать с другими финансовыми веб-сайтами и валютными парами, помимо используемых в примерах. Если ресурс по URL-адресу поменяется и перестанет работать, программист с легкостью определит причину и исправит ее. Хороший бот не должен скачивать больше данных, чем ему нужно, и не должен отправлять запросы слишком часто (точно не на каждом тике). Доходность облигаций менее изменчива, чем курсы валют, поэтому обновлять данные имеет смысл не чаще чем раз в пять минут. Кроме того веб-сайты с данными не всегда являются открытыми источниками, поэтому не стоит их распространять дальше. При правильном и грамотном использовании автоматический сбор таких фундаментальных данных может помочь повысить прибыльность автоматической торговли.

Перевод с английского произведен MetaQuotes Software Corp.
Оригинальная статья: https://www.mql5.com/en/articles/5204

Прикрепленные файлы |
ScraperBot01.mq5 (4.23 KB)
ScraperBot02.mq5 (5.66 KB)
ScraperBot03.mq5 (4.54 KB)
ScraperBot04.mq5 (2.59 KB)
ScraperBot05.mq5 (5.02 KB)
ScraperBot06.mq4 (7.48 KB)
Aleksandr Masterskikh
Aleksandr Masterskikh | 27 апр 2019 в 13:47
Спасибо за статью, очень интересно! Пожалуйста, сообщите о результатах использования этой системы в реальной торговле.
Aleksey Ivanov
Aleksey Ivanov | 5 май 2019 в 11:27
Реальный путь к созданию по настоящему прибыльных автоматических торговых систем.
Библиотека для простого и быстрого создания программ для MetaTrader (Часть V): Классы и коллекция торговых событий, отправка событий в программу Библиотека для простого и быстрого создания программ для MetaTrader (Часть V): Классы и коллекция торговых событий, отправка событий в программу

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

Визуализация истории мультивалютной торговли по отчетам в форматах HTML и CSV Визуализация истории мультивалютной торговли по отчетам в форматах HTML и CSV

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

Утилита для отбора и навигации на MQL5 и MQL4: повышаем информативность графиков Утилита для отбора и навигации на MQL5 и MQL4: повышаем информативность графиков

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

Библиотека для простого и быстрого создания программ для MetaTrader (Часть VI): События на счёте с типом неттинг Библиотека для простого и быстрого создания программ для MetaTrader (Часть VI): События на счёте с типом неттинг

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