Среда тестирования на истории C ++ - Часть 4: Менеджер структуры и данных
После того, как API Bloomberg успешно интегрирован в C ++ для упрощения поиска данных, пришло время заложить основу для структуры бэктестера в целом. Это будет в значительной степени зависеть от событийной системы, описанной Майклом Холлс-Муром в QuantStart, а также от собственной среды разработки алгоритмов Quantopian, но в будущем она будет отличаться для моделирования в реальном времени и реализации внутридневных данных.
Мы хотим, чтобы каждый алгоритм был самодостаточным, чтобы потенциально допускать многопоточность, и просто чтобы наш код оставался кратким. Таким образом, каждый класс алгоритма будет унаследован от Strategy
, который содержит все функции, необходимые пользователю для перебора событий, извлечения истории, размещения смоделированных заказов и отслеживания владений, позиций и статистики производительности. Мы хотим, чтобы каждый элемент бэктеста был как можно более независимым друг от друга. Схема базовой исторической структуры стратегии приведена ниже:
Таким образом, мы можем видеть отношения между каждым из наших предстоящих классов. Все функции для обработки событий будут содержаться в объектах-членах Portfolio
и ExecutionHandler
в зависимости от типа события, а данные будут доступны из Bloomberg через наш DataManager
базовый класс, который будет иметь разные реализации в зависимости от типа стратегии. выполняются (прямые, дневные, исторические). В идеале нам нужно изменить только одну переменную для переключения между этими различными режимами.
Имея эту основу, мы можем приступить к созданию легкодоступной оболочки для HistoricalDataRetriever
, которую мы создали в Части 3. Это необходимо, потому что нам нужен метод, который инициализирует цикл событий в исторических бэктестах с ежедневными рыночными событиями для расчета производительности. Нам также необходимо реализовать простую в использовании функцию для получения исторических данных в самом алгоритме, что должно быть сделано без использования параметра для текущего времени, который мы сделали в HistoricalDataRetriever
, чтобы у пользователя не было возможности для предвзятого смещения или получения данных. за смоделированной текущей датой цикла событий.
Во-первых, нам нужно создать абстрактный базовый класс, из которого будут производиться все различные типы менеджеров данных, DataManager
. Единственное, что необходимо, - это ссылка на смоделированное текущее время и функция с именем history()
, которая возвращает карту SymbolHistoricalDatas, содержащую поля, указанные на заданный промежуток времени до смоделированного времени. Поскольку DataManager
является абстрактным базовым классом, мы не будем здесь реализовывать функцию history()
.
// data.hpp class DataManager { public: explicit DataManager(Datetime* p_currentTime) : currentTime(p_currentTime) {} virtual std::unique_ptr<std::unordered_map<std::string, SymbolHistoricalData>> history( const std::vector<std::string>& symbols, const std::vector<std::string>& fields, unsigned int time_units_back, const std::string& frequency) = 0; protected: Datetime* currentTime{}; };
Наш следующий класс HistoricalDataManager
будет немного сложнее. Он является производным от DataManager
, поэтому он должен реализовывать функцию history()
, а также функцию для заполнения цикла событий рыночными событиями, как это необходимо для нашего исторического отчета о производительности EOD на исторических данных.
Во-первых, мы должны сконструировать сам класс. Единственным членом будет dr, HistoricalDataRetriever
, который мы можем использовать для извлечения данных. Таким образом, наш конструктор просто строит dr со спецификацией того, какие данные будут запрашиваться.
// data.cpp HistoricalDataManager::HistoricalDataManager(Datetime* p_currentTime) : DataManager(p_currentTime), dr("HISTORICAL_DATA") {}
Следующей самой простой функцией для построения будет функция history()
, которая по сути является просто оболочкой для функции pullHistoricalData()
, которую мы написали в средстве извлечения данных в части 3. Она просто вызывает последнюю, не требуя от пользователя указания текущего Datetime. В данный момент тройка не имеет значения, но будет полезна, когда мы реализуем события, требующие актуальных цен, которые не обязательно могут быть такими же низкочастотными, как ежедневные.
// data.cpp std::unique_ptr<std::unordered_map<std::string, SymbolHistoricalData>> HistoricalDataManager::history( const std::vector<std::string> &symbols, const std::vector<std::string> &fields, unsigned int time_units_back, const std::string &frequency) { std::string freq = (frequency == "RECENT") ? daily : frequency; Datetime beginDate = date_funcs::add_seconds(*currentTime, 24 * 60 * 60 * timeunitsback * -1); return std::move(dr.pullHistoricalData(symbols, beginDate, *currentTime, fields, freq)); }
Примечание. date_funcs::add_seconds()
- это настраиваемая функция, которая добавляет заданное количество секунд к Bloomberg :: blpapi :: Datetime. Он делает это с помощью серии time_t
и struct tm
перестановок, и ему могут быть заданы разные параметры для указания специальных опций, например, только дни недели или одна и та же неделя / месяц. Реализация находится здесь на моем GitHub на данный момент, но она будет рассмотрена в следующей части, когда мы перейдем к планированию событий.
Наконец, мы должны создать fillHistory()
, функцию, которая будет генерировать события MarketEvents для заданного количества рыночных дней для статистики производительности. Это позволит обновлять портфель много раз, а не только тогда, когда выполняются запланированные функции алгоритма и когда он размещает заказы. Это просто требует ежедневного получения последней цены EOD за период тестирования на истории и создания MarketEvent для каждого. Сами события будут представлены в следующей части руководства, но MarketEvent просто отслеживает последнюю цену («PX_LAST» от Bloomberg) для нескольких акций на заданную дату. Эту функцию также достаточно легко реализовать, хотя она выглядит сложной:
// data.cpp void HistoricalDataManager::fillHistory(const std::vector<std::string>& symbols, const Datetime& start, const Datetime& end, std::list<std::unique_ptr<Event>>* location) { std::unique_ptr<std::unordered_map<std::string, SymbolHistoricalData>> data = dr.pullHistoricalData(symbols, start, end); for (auto i = data->operator[](symbols[0]).data.begin(); i != data->operator[](symbols[0]).data.end(); ++i) { std::unordered_map<std::string, double> temp = {{symbols[0], i->second["PX_LAST"]}}; for (int j = 1; j < symbols.size(); ++j) { temp[symbols[j]] = data->operator[](symbols[j]).data[i->first]["PX_LAST"]; } location->emplace_back(std::make_unique<MarketEvent>(symbols, temp, i->first)); } }
Хотя это выглядит беспорядочно, все, что делает эта функция, - это извлекает исторические данные из HistoricalDataRetriever
, перебирает каждую дату и, наконец, вытаскивает цены на каждую дату в общие события MarketEvents, которые помещаются в основной цикл событий, хранящийся в виде указателя, location
.
На этом мы завершили функциональность нашего HistoricalDataManager
и теперь готовы приступить к более сложным аспектам классов событий и правилам для функций планирования только в рыночные дни и с учетом праздников года.
Исходный код этого сообщения можно найти на моем GitHub. Если у вас есть другие вопросы, напишите мне по адресу [email protected]!