Активируйте JavaScript для полноценного использования elitetrader.ru Проверьте настройки браузера.
Построение системы » Элитный трейдер
Элитный трейдер
Искать автора

Построение системы

26 мая 2015 QuantAlgos

Подготовка данных

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

Тип данных

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

Внутри дня записывается средняя цена ((бид+аск)/2) контракта, предназначенного для торговли. Для тех же временных точек сохраняется величина спреда (аск минус бид) для измерения ликвидности. Таким образом данные Level 2 (для западных рынков) не используются. Также могут быть взяты цены закрытия для базового ( в случае фьючерсной торговли) или взаимосвязанного контракта для измерения контанго/роллирования и т.п. Кроме того могут понадобится цены закрытия линейки фьючерсов и их объемы для осуществления роллирования, подробнее об этом позже.

Средняя цена или цена сделок

Как было сказано выше, коллекционируется в качестве исходных данных средняя цена. Она не является той ценой, по которой совершаются сделки. Но в зависимости от вашей стратегии, вы можете использовать и цену сделок, или даже обе эти цены. Если ваши алгоритмы относительно медленны ( как и у автора блога) разница между ними невелика, на более высоких скоростях вы столкнетесь с эффектом скачков цены между бидом и аском ("bid-ask bounce") в случае применения средней цены, что добавит вашим данным дополнительную волатильность. Также в случае высокочастотных алгоритмов может оказывать серьезное влияние задержка в приходе данных.

HFT торговля и сложные алгоритмы исполнения используют данные из более глубоких уровней стакана (level 2), что значительно увеличивает объем сохраняемых данных.

Цены открытия, закрытия и внутри дня

В реальной торговли есть несколько вариантов выбора.

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

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

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

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

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

Когда и как часто?

Как часто вы должны забирать данные внутри дня? Скорость и сложность вашей системы, число торгуемых инструментов и частота запросов вашего провайдера могут накладывать верхний предел на этот показатель. Также не имеет большого смысла сохранять данные каждые несколько миллисекунд, если ваша стратегия удерживает позицию несколько недель. Много данных требуют большой емкости носителя и доступ к ним будет медленным.

Должны ли вы использовать фиксированные моменты времени? То есть сохранять данные в 12:00, 13:00 или случайным образом? Фиксированные моменты могут нанести урон, особенно крупным трейдерам, если все будут знать, когда они предпринимают какие-либо действия. Однако, если вы сохраняете данные с иррегулярными интервалами, вы должны потом их правильно обрабатывать, об этом ниже.

Автор собирает данные в часовые серии с иррегулярными промежутками между ними, для его алгоритмов это даже несколько чаще, чем необходимо.

Иррегулярные временные серии

Рассмотрим временную серию фьючерсного контракта на Евродоллар на август 2018 года:

2015-04-23 14:18:54 97.8525 184534
2015-04-23 15:19:41 97.8375 184535
2015-04-23 16:42:34 97.8575 184536
2015-04-23 17:42:56 97.8600 184537
2015-04-23 18:43:21 97.8675 184538
2015-04-23 19:43:42 97.8825 184539
2015-04-23 23:00:00 97.8750 184546
2015-04-24 12:14:24 97.8675 184550
2015-04-24 13:15:26 97.8575 184551
2015-04-24 14:17:33 97.9075 184552
2015-04-24 15:18:31 97.9125 184553
2015-04-24 16:41:23 97.9125 184554
2015-04-24 17:41:42 97.9075 184555
2015-04-24 18:42:01 97.9075 184556
2015-04-24 19:42:22 97.9025 184557
2015-04-24 23:00:00 97.9250 184564
2015-04-27 12:00:05 97.9125 184568
2015-04-27 13:00:29 97.8975 184569
2015-04-27 14:00:55 97.8875 184570


Обратите внимание на последний столбец, представляющий идентификационный номер записи в таблице данных. Цены закрытия помечены здесь временной меткой 23:00:00. Также можно видеть, что интервалы между данными различны, хотя и близки к часовым.

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

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

Некоторые вещи,которые делать не нужно:
убрать внутридневные цены и использовать только цены закрытия. Это сработает, но сделает сохранение цен внутри дня бесполезным занятием
преобразовать внутридневные цены в дневные, взяв среднюю цену за день. Это приведет к недооценке дневной волатильности.

Есть лучшее решение:
преобразовать цены к часовому интервалу ( применяя только цены открытия такого интервала). Код, который управляет пропущенными данными правильно, создает необходимую последовательность цен.
использовать функции для анализа нерегулярных серий. Например, для оценки волатильности приращений цены, вместо равновзвешенного среднеквадратичного отклонения, используем веса в зависимости от интервала времени, на котором вычисляется приращение. Это грубый эквивалент вычислений из предыдущего пункта, но более эффективный в смысле затрат времени на расчеты. Здесь есть некоторые сложности по периодам, когда рынок закрыт, которые нужно обозначить и правильно применять в расчетах.
преобразовать внутридневные данные в дневные, применив для этого последнюю цену для каждого дня. Это значит, что из всей внутридневной выборки мы возьмем последнюю сохраненную цену, а после закрытия мы увидим только цену закрытия. Недостаток этого подхода в отбрасывании потенциально полезной информации, содержащейся в удаленных ценах.

В коде автора используются две цены для каждого дня, в соответствии с последним пунктом. Например, на 6 часов 24 апреля применяется цена закрытия 23 апреля (отмеченная 23:00:00) и последняя цена 24 апреля с временной меткой 17:41

2015-04-23 23:00:00 97.8750 184546
2015-04-24 17:41:42 97.9075 184555


Временные метки

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

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

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

Будьте внимательны к рынкам, которые торгуются 24 часа (например, forex). Узнайте точно, как вычисляются на них цены открытия или закрытия. Нужно быть осторожным и на азиатских рынках, если вы торгуете по европейскому или американскому времени. Цены закрытия могут быть отмечены уже следующим днем относительно вашего часового пояса. И это будет неправильным подходом в случае использования времени вашего компьютера.

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

Построение системы


Другие контракты и синхронизация

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


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

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

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

Ниже приведены данные для Евродоллара (некоторые данные пропущены для второго и последующего дней):

                     PRICE    CARRY

2015-04-21 23:00:00 97.9050 97.985

2015-04-22 12:00:06 97.9175 NaN

2015-04-22 13:00:33 97.9025 NaN

2015-04-22 14:00:59 97.9075 NaN

2015-04-22 15:01:20 97.8675 NaN

2015-04-22 16:07:57 97.8475 NaN

2015-04-22 17:08:22 97.8425 NaN

2015-04-22 18:08:43 97.8275 NaN

2015-04-22 19:09:02 97.8325 NaN

2015-04-22 23:00:00 97.8250 97.905

2015-04-23 12:15:44 97.8575 NaN

…. snip ….

2015-04-23 19:43:42 97.8825 NaN

2015-04-23 23:00:00 97.8750 97.955

2015-04-24 12:14:24 97.8675 NaN

… snip …..

2015-04-24 19:42:22 97.9025 NaN

2015-04-24 23:00:00 97.9250 98.005

2015-04-27 12:00:05 97.9125 NaN 


Как видите, мы сохраняем только цены закрытия для неторгуемого контракта, обозначенного "Carry", но берем и внутридневные для торгуемого.

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

Минимум ликвидности

Должны ли вы собирать рыночные цены когда вы уже знаете, что рынок достаточно ликвиден, или проверять рынок на ликвидность при сохранении?

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

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

Выбросы и очистка данных

Даже с требованиями к минимуму ликвидности, "плохие" цены могут иногда попадать в выборку. Можно даже увидеть нулевые цены, в 10 и 100 раз больше рыночных, цены других контрактов в трансляции вашего инструмента и т.п. Для безопасности все получаемые цены должны быть автоматически проверены перед сохранением.

Есть четыре способа для отсеивания потенциально "плохих" цен ( иногда называемые "выбросами", так они выглядят на графиках).

Первый способ - просто исключать их. Не рекомендуется, кроме случаев очевидно неверных цен. Мы можем, как правило, исключить нулевые цены ( кроме случаев, когда мы получаем значения спрэда или других, где такое значение может иметь место). Также можно использовать определенные границы для отсеивания, например значение фьючерса на Евродоллар вряд ли опустится ниже 50% или возрастет более чем на 100% за ограниченный промежуток времени.

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

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

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

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

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

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

Объемы

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

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

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

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

Сшивание цен

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

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

Автор использует простой метод "panama". У него есть преимущество простоты и использования исторических цен для сшивания только в точке роллирования, состоящее в том, что текущий уровень цены торгуемого контракта сдвигается к подобным же образом скорректированной цене прошлого контракта, для получения непрерывной сглаженной линии. Два недостатка метода заключающиеся в недооценке тренда и потери относительной разницы цен, не так важны, по мнению автора.

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

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

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

Псевдокод для подготовки данных

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

Основной процесс обработки цен запускается сразу после полуночи местного времени и состоит из большого цикла while:

while okay_to_run:

    ## Большая петля собирания цен

    ## Is it after 8pm, or whenever we stop? Then autostop

     if now()>last_sample_at:

         log.info("Нормальная остановка процесса при закрытии рынков")

         okay_to_run=False

         break

     for code in all_codes:

         market_closed=check_market_is_closed(code)

         if market_closed:

             ## не записывать, если рынок закрыт

             continue  

         if check_process(dbtype, "SAMPLING", code)=="STOP":

         ## предотвращение запуска процесса

            continue


         last_run=get_last_run(code)

         if last_run is not None:

            if (now() - last_run).total_seconds()<(60*60):

                #При новом запуске запрашиваем часовые цены

                continue


         raw_sample_instrument(code)

         ## Сшивание цен. Возвращает число новых добавленных точек

         new_prices_added=sample_adj_instrument(code)

         if new_prices_added>0:

           ## Получили данные - запускаем код получения сигналов

               signals_runner(code)

      ## end of for loop

    ##

    ##Окончание цикла while

def raw_sample_instrument(code, entrymode=”AUTO”):

    ## Если мы запускаем вручную, то указываем entrymode=”MANUAL”

    ## Получаем начало данных книги заявок - см. следующую функцию  

    bookdata=get_market_data(code, snapshot=True)


    mid_price_value=midprice(bookdata)


    if isnan(mid_price_value):

       log.warning("Не найдены новые или доступные цены")

    else:

        ## Используем местное время для таймстампа

        sampletime=now()

        pricing_data=TimeSeries([mid_price_value], index=[sampletime])

        resolve_and_add_pricing_data(code, pricing_data, entrymode)

        size=inside_size(bookdata)

        spread=inside_spread(bookdata)

def get_market_data(code, snapshot=True, maxstaleseconds=60, maxwaitseconds=30):

"""

Возвращает рыночные данные с максимальной задержкой, если snapshot=False 
Если в течение maxwait получены не все поля, возвращает NaN
Алгоритм пользователя
- Это получение выборки цен. Нужен последний тик ( с определенной задержкой)
(Проверка последнего тика. Если его нет, стартует запись, пока не будет доступен полный список. Потом возвращает его, если ничего не записалось, возвращает NaN)
[также используется для диагностики цен, например при роллировании]
- Проверка ликвидности . Получение последних рыночных данных и принятие решения о продолжении записи

"""

    global mymarketdata

    ## Получение рыночных данных

   if mymarketdata is None:

      mymarketdata=simple_market_data()

      ## Запрос и получение данных

      stored_data=mymarketdata.get_contract( code, maxdelay=maxstaleseconds)

      if _no_nans(stored_data):

      ## Мы используем данные, если не встречается NaN

         useable_prices=True

      else:

         useable_prices=False


    if not snapshot or not useable_prices:

         ## Нужно запустить сервер тиков

         ## Это создает идентификационный номер, если нужно

         start_ticker_for_contract(dbtype, tws, contract)

         started_tick_server=True

    else:

         started_tick_server=False

    start_time=datetime.datetime.now()


    ## Этот цикл останавливается, если нет приемлемых цен

   timespent=0

      while not useable_prices and timespent < maxwaitseconds: stored_data=mymarketdata.get_contract(code, maxstaleseconds)

        useable_prices=_no_nans(stored_data)

        timespent=(now() - start_time).total_seconds()

     if started_tick_server and snapshot:

        ## need to stop the server because we are taking a snapshot

        ## don't want to continue ticking, reduce the number of prices we're getting

        ## note ticks may still arrive...

        ## note the tickid will still live on!

        ## note we won't turn off if active order

      stop_ticker_for_contract(code)


    return stored_data


def resolve_and_add_pricing_data(code, pricing_data, entrymode):


    MIN_OBSERVATIONS=10

   current_price_matrix=read_prices_for_contract(code)


   if current_price_matrix.shape[0] > MIN_OBSERVATIONS:

      ## resolve the mode, returns None if AUTO and prices fail checks

      pricing_data=resolveprice(current_price_matrix, pricing_data, entrymode)

   else:

      ## just leave prices as they are

      log.warning("No existing prices can't do any checks")


      add_price_matrix(code, pricing_data)

   return pricing_data)


def resolveprice(current_price_matrix, pricing_data, entrymode):

"""

Function that resolves prices depending on entrymode AUTO or MANUAL


Returns new pricing_data - if this is None then we don't have any valid prices (happens only in AUTO mode)

"""



   assert entrymode in ["AUTO", "MANUAL"]


   spike=checkspike(current_price_matrix, pricing_data)

   if spike:

      """

      Potentially bad price


      If running in manual mode then allow user to override, else flag

      """

      if entrymode=="MANUAL":


         ## Allows you to manually check prices, and weed out anything thats bad

         pricing_data=manual_pricing_data(current_price_matrix, pricing_data, describe)


      elif entrymode=="AUTO":

         ## no user interaction possible

         log.critical("Sample failed spike check price move")

         pricing_data=None

   return pricing_data


Конец кода

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