Другие контракты и синхронизация
В прошлой части мы не прояснили ситуацию, зачем нужны цены закрытия, наряду с внутридневными. Ведь цены внутри дня тоже сохраняются вплоть до закрытия. Причина состоит в том, что иногда полезно иметь синхронизированные данные. В нашем случае нужно знать цену текущего фьючерса относительно соседнего контракта, для вычисления контанго, осуществления роллирования и т.п ( нужен спрэд между этими инструментами). Другой пример - если вам необходимо создать систему торговли несколькими инструментами на основе их коинтеграции и возврата к среднему.
Получить синхронизированные межрыночные цены довольно сложно. Если вы собираете тиковые данные и выстраиваете временные серии из них, то можете столкнуться с очень зашумленными значениями из-за эффекта скачка между бидом и аском на одном рынке, который будет влиять на другой.
Однако цены закрытия автоматически синхронизируются, и для не высокочастотного трейдера работать с ними намного легче, чем синхронизировать множество тиковых цен.
Таким образом мы берем цены закрытия для торгуемого и связанного, необходимого нам, контракта. Для этого, неторгуемого, актива внутридневные цены собирать не нужно, так как они могут дать зашумленное значение спрэда, а для наших целей дневных данных достаточно.
Ниже приведены данные для Евродоллара (некоторые данные пропущены для второго и последующего дней):
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
Конец кода
От себя: статья из блога указанного автора написана достаточно сумбурно, но дает основные этапы подготовки данных. Может это покажется не столь важным, такая тщательная обработка, но в своей практике я несколько раз сталкивался с неправильной работой алгоритмов из-за неверных данных. Если вы последуете этим простым советам, то сэкономите себе много времени.
Сообщение