CoderCastrov logo
CoderCastrov
Машинное обучение

Как и почему я получил 75 Гб бесплатных данных о валютном рынке 'Тик'.

Как и почему я получил 75 Гб бесплатных данных о валютном рынке 'Тик'.
просмотров
14 мин чтение
#Машинное обучение

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

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

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

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

1. Некоторая информация для контекста

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

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

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

FXCM daily candle data.

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

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

Изображение ниже является визуальным представлением свечи:

Diagram showing the structure of a single data candle.

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

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

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

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

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

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

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



2. История — Как провалиться, а затем достичь успеха

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


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

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

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

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

FXCM доступные символы с некоторыми дубликатами.

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

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

FXCM доступные символы без дублирования.

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

Вывод fxcmpy_tick_data_reader() при запросе данных за 5 недель.

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

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

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

Вывод, показывающий, что индекс был преобразован в тип DatetimeIndex.

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

Вывод, показывающий, что тип индекса не был изменен.

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

При вызове функции get_data() вы заметите, что она занимает значительно больше времени, чем функция get_raw_data(). Это связано с преобразованием индекса из типа object в DatetimeIndex. Обе функции get_data() и get_raw_data() возвращают объекты DataFrame из библиотеки Pandas, однако функция get_data() обновляет индекс, используя столбец DateTime в качестве индекса и преобразует его в тип DatetimeIndex. Позже я обнаружил, что Pandas плохо справляется с преобразованием в DatetimeIndex, когда микросекунды включены без использования формата srftime. Дополнительную информацию можно найти в вопросе "Why is pandas.to_datetime slow for non standard time format such as '2014/12/31'" на stackoverflow.

В итоге, для ускорения преобразования индекса требуется использование формата srftime. Я вернусь к этому позже.

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

Переход дальше...

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

Парсер

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

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

Проверка

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

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

Результат работы скрипта подсчета файлов.

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

Пакетирование

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

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

Объяснение скрипта ETL

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

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

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

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

Числа, округленные до менее чем шести знаков, не включали нули, что в любом другом случае имело бы смысл. Например, .123000 вместо этого появлялось как .123. К сожалению, при указании директивы strftime (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 для получения дополнительной информации (спасибо за ссылку, Давиде).

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

К сожалению, мои проблемы еще не закончились. В середине преобразования и загрузки файлов "EURCHF" мой код снова сломался. Оказывается, что не все файлы следуют одному и тому же форматированию даты и времени. Другими словами, некоторые файлы форматируют свои даты как Месяц/День/Год, в то время как другие могут быть День/Месяц/Год или даже Год/Месяц/День.

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

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

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

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

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

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


3. Все вместе

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

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

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

Часть 1 — Сканирование данных и проверка загрузки

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

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

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