С полным кодом Python для очистки, извлечения, преобразования и загрузки в хранилище данных HDF5, чтобы удовлетворить ваше будущее.

Ближе к концу получения степени магистра в области науки о данных я начал представлять, что делаю умные вещи с помощью машинного обучения и автоматической торговли. Если вы, как и я, столкнулись с загадкой «как мне получить исторические данные о бесплатных тиках», то этот пост для вас.

Я разделил свой пост на три раздела:

  1. Немного фона для контекста.
  2. Storytime - Как потерпеть неудачу, а затем добиться успеха.
  3. Собираем все вместе - полный код

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

1. Некоторые предпосылки для контекста

Как я упоминал ранее, все это началось, когда я заинтересовался применением некоторого машинного обучения (ML) к проблеме прогнозирования рыночной цены или, что проще, рыночного направления. То есть я хочу использовать ML для анализа некоторых исторических данных для данной валютной пары, а затем сказать мне, какой будет цена в какой-то конкретный момент в будущем. Затем я бы использовал эту прогнозируемую цену, чтобы принять решение о том, как вложить свои деньги. Поскольку мне нужно только знать, будет ли цена целевой валюты увеличиваться или уменьшаться, я мог бы упростить свой алгоритм, чтобы просто предсказать направление рынка (положительное или отрицательное). Это, конечно, чрезмерное упрощение того, что необходимо для прибыльной торговли, но точное прогнозирование будущего состояния рынка (лучше, чем в 50% случаев) является критически важным первым шагом.

Прежде чем я смогу построить модель рынка с использованием машинного обучения, мне сначала нужны некоторые данные о рынке. Эти данные бывают разных форм. Чаще всего данные доступны в виде так называемых «свечных данных» или «столбцов», основанных на времени. Данные свечи поступают с приращениями (частотами), зависящими от времени. Это может быть 1 минута, 5 минут, 1 день, месяц и т. Д. Ниже приведен пример данных свечей дневного уровня от FXCM:

Результат будет выглядеть примерно так:

Каждая строка представляет собой свечу данных за один день для данной валютной пары (в данном случае USD / JPY). Он сообщает вам, какой была цена в начале свечи, какова была цена в конце свечи, какова была самая высокая цена и самая низкая цена в течение окна свечи. Он делает это для обеих сторон рынка, Bid и Ask.

Вместе эти значения представляют собой границы времени и значения цены, в пределах которых произошли все изменения цены (известные как тики). Помимо границ свечи, данные также включают значение количества тиков. Это значение представляет количество изменений цены, произошедших в границах свечи. Однако он не сообщает вам, когда произошли изменения или насколько они велики или малы.

На изображении ниже наглядно показано, что такое свеча:

Глядя на диаграмму выше, можно увидеть, что расстояние между границами открытия и закрытия определяется размером свечи. В примере USD / JPY это пространство представляет собой целый день. Однако это может быть 1 минута, 1 час или все, что вам может предоставить поставщик данных о свечах.

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

Почему использование данных свечей в качестве основного источника данных для машинного обучения бесполезно.

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

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

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

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

2. Время рассказов - как потерпеть неудачу, а затем добиться успеха

Достаточно предыстории ... цель здесь состоит в том, чтобы рассказать вам техническую историю о том, как получить доступ к приличному количеству бесплатных тиковых данных. Я расскажу вам, как я это сделал, а также расскажу о проблемах, с которыми я столкнулся, и о том, как их преодолеть.

Я покажу вам, как я скопировал тиковые данные за 2 года (2017 и 2018) для 21 валютной пары и загрузил их в файл иерархических данных (HDF5). Данные поступают из FXCM и изначально используют их RestAPI. В конце концов я отказался от их api и выбрал другой подход.

Версия 1 - ›Ошибка API:

Прежде всего, вот библиотеки, которые я использовал во время своей первой попытки:

FXCM предоставляет функцию под названием fxcmpy_tick_data_reader (также обозначенную здесь как tdr), которая возвращает информацию о доступных валютных парах, но также позволяет вам загружать целевую валюту.

Результат будет выглядеть следующим образом:

Подсчет количества символов показывает, что их 24, однако здесь происходит некоторое дублирование, поэтому давайте создадим новый список без дубликатов:

Как видите, сейчас у нас всего 21 валютная пара:

Чтобы получить некоторые данные, вы указываете дату начала и окончания диапазона, а затем вызываете функцию, запрашивающую целевую валютную пару, как показано ниже:

Как оказалось, данные предоставляются блоками по 1 неделе за раз, независимо от того, нужен ли вам только 1 день. Итак, когда вы посмотрите на URL-адрес, мы увидим, что приведенный выше код вернул файл gzip за первую неделю 2017 года. Если вы измените даты и запустите код самостоятельно, вы увидите, что URL-адреса следуют тому же шаблону, просто изменив номер года или недели в зависимости от запрошенного диапазона дат. Если вы запрашиваете данные за более чем неделю, он просто возвращает больше файлов gzip.

Чтобы просмотреть данные, вам нужно вызвать некоторые методы для загруженных данных, которые будут извлекать файл и преобразовывать содержащийся CSV в фрейм данных pandas. В зависимости от того, какой метод вы используете, он также обновит индекс с типа object на тип DataTimeIndex.

Как вы можете видеть ниже, метод get_data() преобразовал индекс с object (он же str) в DatetimeIndex:.

В случае get_raw_data() индекс остается типа object`.

Медленное преобразование индекса:

При вызове get_data() вы заметите, что для завершения требуется значительно больше времени, чем get_raw_data(). Это связано с преобразованием индекса с object на DatetimeIndex. Оба get_data() и get_raw_data() создают Pandas DataFrame, однако get_data() обновляет индекс, чтобы использовать столбец DateTime в качестве индекса, и преобразует его в тип DatetimeIndex. Позже я обнаружил, что Pandas плохо справляется с преобразованием в DatetimeIndex, когда поле микросекунды включено без директивы формата srftime. См. 'Почему pandas.to_datetime работает медленно для нестандартного формата времени, такого как' 2014/12/31 ' »в stackoverflow для получения дополнительной информации.

Суть в том, что директива srftime необходима для ускорения работы (для преобразования индекса). Я вернусь к этому позже.

Вы можете спросить, почему важно иметь DatetimeIndex? .. ну, это важно, учитывая, что мы хотим иметь возможность разрезать данные по времени позже. Для этого данные должны быть правильно загружены в хранилище данных HDF5 с самого начала. Мы не хотим преобразовывать индекс позже.

двигаемся дальше…

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

Приведенный ниже сценарий определяет правильную дату для каждого понедельника в году и организует их в списки по 4 в списке (точка) и объединяет все списки в один большой список списков. Позже я использую этот список для автоматизации загрузки.

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

Затем я создал функцию, которая делает следующее:

  • Скачивает файлы.
  • Извлекает данные.
  • Обновляет индекс до DatetimeIndex.
  • Добавляет данные в правильную таблицу.
  • Очищает таблицу в файле HDF5.

Я также включил несколько операторов печати, чтобы помочь мне отслеживать, что было загружено, извлечено и выгружено.

Затем я использую эту функцию в небольшой программе, которая открывает файл HDF5, использует функцию ETL и, наконец, закрывает файл HDF5.

Почему приведенный выше код недостаточно хорош:

Итак, после запуска сценария довольно быстро стало ясно, что это займет некоторое время. Когда я говорю "какое-то время" ... я имею в виду дни !! .. буквально ДНИ !!

Очевидно, этого недостаточно, и поскольку я не знал, почему это происходит так медленно, я просто предположил, что это проблема с Интернетом, вызванная космическими обезьянами. Медленность процесса вызвала у меня множество неприятных проблем.

  1. Из-за того, что загрузка длилась так долго, я уходил от своего компьютера, что в конечном итоге приводило к спящему режиму и прерывало процесс загрузки. Это привело к сбою сценария.
  2. Самое неприятное, что из-за размера данных я решил записать данные на внешний жесткий диск. Несмотря на то, что я снял все флажки энергосбережения, мой ноутбук все равно время от времени отключал и снова подключал жесткий диск. Когда это произойдет до закрытия файла HDF5, все хранилище данных будет повреждено!

Поковырявшись, я понял, что космические обезьяны не виноваты. Вместо этого преобразование индекса, упомянутое ранее, было причиной того, что все это заняло так много времени. Я также обнаружил, что загружаемые мной файлы были файлами gzip размером около 5 МБ каждый. После извлечения они увеличиваются примерно до 80 МБ.

Это дало возможность ускорить весь этот процесс ..

Версия 2 - ›Успех

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

Как упоминалось ранее, размер каждого файла gzip составляет всего несколько мегабайт. Я решил сначала очистить все файлы и сохранить их в каталоге, который потом распакую. Таким образом, мне не придется повторно загружать файлы каждый раз, когда что-то пойдет не так.

Библиотеки, которые вам понадобятся для версии 2:

Скребок

Если в коде не ясно, то я создаю URL-адреса, соответствующие формату FXCM, а затем использую библиотеку requests для загрузки каждого файла по созданному URL-адресу. Чтобы обеспечить правильную загрузку каждого файла, я решил выполнить потоковую загрузку файлов и записать данные на диск в chunks. Ниже приведен скрипт парсинга:

Несмотря на то, что размер каждого файла составляет всего около 5–10 МБ, стоит отметить, что их более 2000, и на их загрузку потребовалось время. Возможно, вы уходите, чтобы съесть тост или посмотреть шоу на Netflix.

Контрольно-пропускной пункт

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

Предполагая, что вы все загрузили успешно, вывод скрипта должен выглядеть примерно так:

Итак, теперь, когда у вас есть все загруженные файлы, следующим шагом будет их извлечение и загрузка в нашу базу данных (файл HDF5). На данный момент я еще не решил проблему медленного обновления индекса. Таким образом, я создал пакетные папки по валютным парам, чтобы я мог контролировать извлечение, преобразование и загрузку (ETL) данных в базу данных, не застревая на своем компьютере в течение нескольких дней. Для завершения каждой партии требовалось около 3-4 часов.

Дозирование

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

Он создает папку для каждой валюты, затем находит файлы, принадлежащие этой папке, сопоставляя первые 6 символов с именем папки, к которой она принадлежит, а затем копирует файл в папку назначения. К концу у вас будет 21 папка по 104 файла в каждой.

Сценарий ETL объяснил

Теперь цель состоит в том, чтобы распаковать файлы gzip, преобразовать данные в правильный формат и затем загрузить их в базу данных HDF5. Если вам комфортно читать код, просто пропустите его до конца, но если вам интересна история того, как все это сложилось, продолжайте читать ..

Итак, теперь, когда я решил отказаться от API FXCM для распаковки этих файлов, мне нужно было извлечь их вручную. Я больше не мог использовать функцию tick_data_reader(), потому что она вызывает веб-сервер, ищущий файл, связанный с датой, которую я запрашиваю. Все файлы теперь находятся на моем жестком диске, поэтому мне нужно было написать собственный код для извлечения.

Извлечение данных оказалось немного сложнее, чем я ожидал, из-за проблем с кодеком. В конце концов, я просмотрел исходный код API FXCM, чтобы найти функцию (см. Строку 181), которая выполняет извлечение. Я немного адаптировал код и отправился на скачки.

Затем я столкнулся с более неясной проблемой. Мой сценарий будет работать нормально, пока он не задействует данные, в которых поле Datetime в миллисекундах не содержит требуемых 6 десятичных знаков.

Числа, округленные менее чем до шести знаков, не будут включать нули, что в любых других обстоятельствах имело бы смысл. Например, .123000 вместо этого будет отображаться как .123. К сожалению, когда вы указываете директиву srftime (format="%m/%d/%Y %H:%M:%S.%f"), это нарушает ваш код.

К счастью, есть замечательный метод dataframe.DateTime.str.pad(), который позволяет дополнять строки любым символом или числом, которое вы хотите. Так что все, что мне нужно было сделать, это заполнить столбец «DateTime» нулями там, где длина строки не соответствует требуемым 26 символам. Еще лучше то, что для этого потребовалась всего одна строка кода.

Затем была мучительно медленная проблема преобразования индекса из типа object в тип DatetimeIndex.

Задав вопрос нескольким старым одноклассникам, один из них (спасибо, Алекс) предположил, что это может иметь какое-то отношение к тому факту, что я просил pd.to_datetime() вывести формат. В документации Pandas специально упоминается, что определение формата даты и времени может потенциально ускорить преобразование до 10 раз. Поскольку я имел дело примерно с 2 миллиардами строк данных, я был благодарен за любое возможное ускорение.

Однако, как оказалось, определение формата даты и времени на самом деле не очень хорошо работает, если он включает микросекунды. См. 'Почему pandas.to_datetime работает медленно для нестандартного формата времени, такого как' 2014/12/31 ' »в stackoverflow для получения дополнительной информации (спасибо, что поделился ссылкой Davide).

Итак, вооружившись этим советом, я попытался указать формат с помощью директивы формата srftime, например: format="%m/%d/%Y %H:%M:%S.%f". Это сильно повлияло на время выполнения. Использование спецификации "infer" заняло минуты, чтобы преобразовать всего один файл. Если указать формат, преобразование заняло буквально несколько секунд !!.

К сожалению, мои проблемы еще не закончились. На полпути преобразования и загрузки файлов «EURCHF» мой код снова сломался. Оказывается, не все файлы имеют одинаковое форматирование даты и времени. Это означает, что некоторые файлы форматируют свои даты как месяц / день / год, в то время как другие могут быть как день / месяц / год или даже год / месяц / день.

Чтобы справиться с этой проблемой, я решил выбрать первую и последнюю записи в наборе данных, зная, что каждый файл содержит данные за 5 рабочих дней. Затем я использовал цикл for и несколько вложенных операторов if, чтобы выяснить, с каким форматом я имел дело для каждой строки в данном файле, перед преобразованием индекса с использованием правильной директивы srftime. Эта часть скрипта позволила мне извлечь и загрузить все файлы (2184) менее чем за 2 часа, тогда как раньше требовалось 3–5 часов, чтобы загрузить только 104.

Вот код для преобразования индекса в Dtaetimeindex:

И последнее, но не менее важное - загрузить данные в базу данных. К счастью, на самом деле это довольно просто. PyTables делает этот шаг очень простым. Все, что требуется, это:

  • Откройте файл.
  • Добавьте данные в правильную таблицу (создайте ее, если таковая не существует)
  • Запишите обновление на диск («очистив» файл)
  • Закройте файл

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

И последнее, но не менее важное: я выполнял свой ETL партиями. Мне нужно было какое-то звуковое уведомление после завершения пакета, поэтому я позаимствовал код с realpython.com, который создает синусоидальную волну 440 Гц с нуля и воспроизводит ее в течение 2 секунд. Я просто использовал его как есть в конце сценария.

3. Собираем все вместе

Итак, как и было обещано, вот весь код, который вам нужен:

  • Очистите тиковые данные за два года по 21 валютной паре
  • Извлеките эти данные
  • Преобразуйте индекс в DatetimeIndex
  • Загрузите его в базу данных HDF5

ВАЖНО: не забудьте обновить расположение каталогов для вашей среды перед запуском кода.

Часть 1 - Очистите данные и проверьте загрузку

Часть 2 - Извлечение, преобразование и загрузка файлов в хранилище данных HDF5.

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

Спасибо за прочтение.