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