Среда тестирования на истории C ++ - Часть 3: Интерфейс данных Bloomberg API

Недавно я наконец получил доступ к терминалу Bloomberg через мою школу. Таким образом, с этого момента в этой серии статей будут использоваться данные, полученные из Bloomberg API, а не из Yahoo Finance. Однако структура будет аналогичной, и переключаться между двумя источниками не должно быть слишком сложно. Тема остальной части этого сообщения будет сосредоточена исключительно на взаимодействии с Bloomberg API через терминал Bloomberg.

Bloomberg API - грозный ресурс. Он позволяет пользователям извлекать исторические данные EOD за многие годы, внутридневные тиковые данные за последние 140 дней и даже рыночные данные в реальном времени через подписку. В данный момент я сосредоточен только на исторических данных EOD, но я постараюсь структурировать свой код так, чтобы не было слишком сложно реализовать два других типа когда-нибудь в будущем.

Прежде чем мы начнем, нам нужно установить и связать сами библиотеки API. В Терминале войдите в систему и введите команду WAPI <GO>, чтобы перейти на страницу загрузки всего, что связано с API. Найдите пакет для вашей системы и C ++, а затем загрузите его. Затем вы захотите переместить папку внутри пакета с именем «C ++» в то место, на которое вы сможете ссылаться. Для меня это было в «C: // blp» с остальной частью кода Терминала. Затем в своем проекте вы хотите предоставить каталог include и ссылку на файлы .lib. Необходимые строки в моем CMakeLists.txt выглядят так:

# CMakeLists.txt
# Bloomberg library includes
include_directories("C:/blp/C++/include")
# Build the backtester executable named Backtester
add_executable(Backtester main.cpp ${BACKTEST_SRCS})
# Now link to the Bloomberg libraries
file(GLOB BLPAPI_LIBRARIES "C:/blp/C++/lib/*.lib")
message(STATUS "Bloomberg Libraries: ${BLPAPI_LIBRARIES}")
target_link_libraries(Backtester ${BLPAPI_LIBRARIES})

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

// bloombergincludes.hpp
// Bloomberg API includes
#include <blpapi_defs.h>
#include <blpapi_correlationid.h>
#include <blpapi_element.h>
#include <blpapi_event.h>
#include <blpapi_exception.h>
#include <blpapi_message.h>
#include <blpapi_session.h>
#include <blpapi_subscriptionlist.h>

Наконец, полезно использовать пространство имен Bloomberg :: blpapi в ваших файлах для повышения удобочитаемости. Я буду использовать это во всех блоках кода:

using namespace Bloomberg::blpapi;

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

Важно: перед включением соединения Bloomberg также требует запустить bbcomm.exe, чтобы установить соединение. Также достаточно иметь сеанс входа в систему в приложении Bloomberg Terminal, и я обычно так делаю. Если у вас возникли проблемы с подключением к API, вы можете запустить программу API Diagnostics Tool, поставляемую с приложением Bloomberg, чтобы попытаться выяснить причину проблемы и устранить ее.

В любом случае, с открытым сеансом мы можем затем открыть Service в зависимости от того, какой тип данных мы хотим использовать. Последний компонент - это Запрос, созданный с использованием Службы, который определяет параметры данных, которые мы хотим вернуть. Отправляя наш запрос через сеанс, мы можем получать события в EventQueue и обрабатывать их по мере их поступления. Нам нужна очередь по двум причинам: (1) ответы с большими данными разбиваются на несколько возвращаемых PartialResponses и (2) очередь событий позволяет нам делать несколько асинхронных запросов через один сеанс и не пропускать их все через единственный обработчик событий сеанса. Мы создадим собственный обработчик данных для интерпретации этих ответов и помещения их в стандартную библиотечную структуру для упрощения управления. Вот и все, теперь мы можем приступить к реализации ретривера.

Мы разделим наш код поиска на два класса: DataRetriever и HistoricalDataHandler, последний из которых будет обрабатывать события в очереди событий. DataRetriever содержит единственную функцию pullHistoricalData(), которая возвращает std::unordered_map с извлеченными данными. HistoricalDataHandler содержит три функции: processResponseEvent(), processExceptionsAndErrors() и processErrors(). Каждый из них будет объяснен по мере их реализации.

В конструкторе класса DataRetriever мы хотим создать сеанс и запустить его, чтобы нам не приходилось беспокоиться о перезапуске сеанса каждый раз, когда мы вызываем средство извлечения истории. В этом случае session - это std::unique_ptr для объекта сеанса, хранящегося как локальный член класса DataRetriever. Это позволяет нам сохранять открытый сеанс между вызовами функций и удовлетворять требованиям RAII. Для создания сеанса также требуется объект SessionOptions. В целом конструктор должен выглядеть так:

// dataretriever.cpp
// Data Retriever constructor
DataRetriever::DataRetriever(const std::string &p_type) : type(p_type) {
    // First initialize the session options
    SessionOptions session_options;
    session_options.setServerHost("localhost");
    session_options.setServerPost(8194);
    // Now use the options to build a session object
    session = std::make_unique<Session>(session_options);
    // Open up the session in preparation for data requests
    if (!session->start()) {
         throw std::runtime_error("Failed to start session!");
    }
}

Получатель исторических данных немного сложнее. В этой функции мы создаем Service, Request, EventQueue и Event Handler, через которые мы получаем данные. EventHandler заполнит std::unordered_map, который он вернет через std::move в std::unique_ptr для экономии места в памяти. Параметры этой функции будут указывать параметры для данных. Наконец, просто знайте, что SymbolHistoricalData - это настраиваемая структура данных, которая содержит строку для символа, а затем дополнительную упорядоченную карту дат и данных EOD.

// dataretriever.cpp
// Retrieves the historical data
std::unique_ptr<std::unordered_map<std::string, SymbolHistoricalData>> DataRetriever::pullHistoricalData(const std::vector<std::string> &securities, const Datetime &start_date, const Datetime &end_date, const std::vector<std::string> &fields, const std::string &frequency) {
    // Ensure that this instance of DataHandler is historical
    if (type != "HISTORICAL_DATA") return;
    // Open the pipeline to get historical data
    session->openService("//blp/refdata");
    Service serv = session->getService("//blp/refdata");
    // Build the request
    Request request = serv.createRequest("HistoricalDataRequest");
    for (const std::string& i : securities) { 
        request.append("securities", i.c_str()); }
    for (const std::string& i : fields) { 
        request.append("fields", i.c_str()); }
    request.set("startDate", get_date_format(start_date).c_str());
    request.set("endDate", get_date_format(end_date).c_str()); 
    request.set("periodicitySelection", frequency.c_str());}

Примечание. get_date_format() - это функция, которая просто возвращает формат даты и времени Bloomberg в формате ГГГГММДД.

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

    // Build an Event queue onto which the data will be put
    EventQueue queue;
    // Perform request with the queue
    session->sendRequest(request, CorrelationId(1), &queue);
    // Now handle events as they come into the queue
    HistoricalDataHandler handler;
    bool responseFinished = false;
    while (!responseFinished) {
         // Handle events until responseFinished becomes true
         Event event;
         if (queue.tryNextEvent(&event) == 0) {
             responseFinished = handler.processResponseEvent(event);
         }
    }
    // When the event finishes, return the handler's data map
    return std::move(handler.target);
}

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

// dataretriever.cpp
// HistoricalDataHandler constructor / initializer list
HistoricalDataHandler::HistoricalDataHandler() : target(std::make_unique<std::unordered_map<std::string, SymbolHistoricalData>>()) {}

Создав карту данных, мы, наконец, можем предоставить функциональность для обработки событий, переданных в ее processResponseEvent() функцию. Прежде чем мы сможем получить данные из события, нам действительно нужно сначала убедиться, что событие содержит данные и не является ошибкой или исключением. Это задание processExceptionsAndErrors(), которое вернет истину, если данное Событие является ошибкой или исключением:

// dataretriever.cpp
// Makes sure Events are not errors
bool HistoricalDataHandler::processExceptionsAndErrors(Message msg) {
    // If there is no security data, process the errors
    if (!msg.hasElement("securityData)) {
        if (msg.hasElement("responseError")) {
            Element response_data = msg.getElement("responseError);
            // Log the error and return true
            std::cout << response_data << std::endl;
        } 
    return true;   
    }
    // There could also be field exceptions within the security data
    Element security_data = msg.getElement("securityData"); 
    Element field_exceptions = security_data.getElement("fieldExceptions");
    
    // Ensure that the fieldExceptions element is empty
    if (field_exceptions.numValues() > 0) {
        Element element = field_exceptions.getValueAsElement(0);
        // Log the exception
        std::cout << element << std::endl;
        return true;
    }
    return false;
}

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

// dataretriever.cpp
// Processes responses, returning true once data finished
bool HistoricalDataHandler::processResponseEvent(const Event &event) {
    // Make sure the message is a response
    if ((event.eventType() != Event::PARTIAL_RESPONSE) && (event.eventType() != Event::RESPONSE)) { return false; }
    
    // Iterates through the messages returned by the event
    MessageIterator msgIter(event);
    while(msgIter.next()) {
        Message msg = msgIter.message();
        
        // Make sure no exceptions or errors
        if (!processExceptionsAndErrors(msg)) {
             // Handle the fields and load their data            
             Element security_data = msg.getElement("securityData");
             // Build our custom data type
             SymbolHistoricalData shd;
             shd.symbol = security_data.getElementAsString("securityName");
 
             // Fill data map with field data
             Element field_Data = security_data.getElement("fieldData");
             if (field_data.numValues() > 0) {
                  for (int i = 0; i < field_data.numValues(); ++i) {
                      Element element = field_data.getValueAsElement(i);
                      Datetime date = element.getElementAsDatetime("date");
                      // Fill the SHD with the data
                      for (int j = 0; j < element.numElements(); ++j) {
                          Element e = element.getElement(j);
                          shd.data[date][e.name().string()] = e.getValueAsFloat64();
                      }
                  }
              }
              // Once all data has been entered, append SHD
              if (target->find(shd.symbol) != target->end()) {
                  target->operator[](shd.symbol).append(shd);
              } else {
                  target->operator[](shd.symbol) = shd;
              }
        } else {
            // Exception occurred in message
            std::cout << "Exception occurred." << std::endl;
        }
    }
    // If the event was a RESPONSE, then the data is finished
    return event.eventType() == Event::RESPONSE;
}

Примечание. SymbolHistoricalData - это структура, содержащая символ std::string, обозначающий символ безопасности, который представляют данные, std::map<Datetime, std::unordered_map<std::string, double>> данные, содержащие все даты и поля данных для соответствующих дат, и, наконец, функцию append(), которая присоединяет еще один SymbolHistoricalData данные в текущий объект данных (при условии, что они имеют один и тот же символ).

Это должно быть все, что нам нужно для получения исторических данных EOD! Подведем итоги: мы создали класс DataRetriever с функцией pullHistoricalData, которой можно задать параметры запроса. Затем эта функция открывает службу исторических данных через Bloomberg и отправляет запросы, обрабатывая ответы с помощью нашего собственного HistoricalDataHandler, когда они поступают в очередь событий. Как только очередь событий встречает событие Response (обозначающее конец), pullHistoricalData() перемещает std::unique_ptr к данным в объект, назначаемый функции. Мы можем протестировать эту функциональность с помощью пары строк кода. Сначала убедитесь, что у вас есть соединение через запущенный bbcomm.exe или зарегистрированный экземпляр приложения Bloomberg Terminal!

// main.cpp
#include "dataretriever.hpp"
int main(int argc, char* argv[]) {
    // Build a Data Retriever for historical data
    DataRetriever dr("HISTORICAL DATA");
    // Now pull historical data into the unique ptr
    std::unique_ptr<std::unordered_map<std::string, SymbolHistoricalData>> data = dr.pullHistoricalData({"IBM US EQUITY"}, Datetime(2005, 3, 3, 0, 0, 0, 0), Datetime(2006, 3, 3, 0, 0, 0, 0), {"PX_LAST"});
 
    // Print the retrieved data to console
    auto i = data->operator[]("IBM US EQUITY").data.begin();
    while (i != data->operator[]("IBM US EQUITY").data.end()) {
         std::cout << "DATE: " << i->first << ", PX_LAST: " << i->second["PX_LAST"] << std::endl;
         ++i;
    }
 
    return 0;
}

Когда вы запустите это, вы должны получить 253 строки последних цен в каждый рыночный день с 3 марта 2005 г. по 3 марта 2006 г. (не 252 строки, потому что они включают обе конечные точки). Не стесняйтесь проверить это с другими полями данных.

Теперь, когда наша обработка данных запущена и работает (во второй раз), мы можем приступить к созданию нового улучшенного бэктестера!

Исходный код этого сообщения можно найти на моем GitHub. Если у вас есть другие вопросы, напишите мне по адресу [email protected]!