Блог им. uralpro

Построение системы. Подготовка данных-2

google_plot_01

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

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

 

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

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

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

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

                     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

Конец кода

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

Другие алгоритмы и стратегии смотрите на моем сайте.

★11
5 комментариев
Что же за мания-то такая, забугорную вату переводить и постить где только можно? Автор, вы переводчик?
avatar
Так и не понял смысла в выборке цен посреди дня по непонятно каким принципам, если используется цена закрытия.
Скорее похоже на поток бессознательного.
avatar
Автор ссылка не работает
avatar
tujh, спасибо, исправил
avatar
при всем уважении, но полная хрень написана:(
вы в открытом рынке то же будите фильтровать котировки?
avatar

теги блога uralpro

....все тэги



UPDONW
Новый дизайн