Среда тестирования на истории 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]!