Эксперимент VonmoTrade. Часть 4: Торговые графики
В прошлых статьях мы разобрались, как создаются и обрабатываются торговые заявки. Темой сегодняшней статьи будут вопросы обработки и хранения информации, необходимой для графических инструментов анализа рынка – биржевых графиков.
Прежде чем начать, хочу сделать небольшое отступление. Для внутренних проектов Vonmo используется обычная схема именования V+слово, наиболее ёмко характеризующее функции проекта. Сегодня я обнаружил, что VTrade – уже существующая компания. Дабы не вносить путаницу, я переименовал эксперимент в VonmoTrade.
Чтобы оценить состояние рынка, одной книги ордеров и истории сделок недостаточно. Нужен инструмент, позволяющий наглядно и быстро выявить тренд рыночной цены. Торговые графики можно разделить на два типа:
- Линейные;
- Интервальные.
Линейные графики
Самый простой и понятный без подготовки график. Отображает зависимость цены финансового инструмента от времени.
Основное достоинство этого типа графиков – простота. Из него же вытекает основной недостаток – низкая информативность. Если график строится на основе сырых данных, то берется цена последнего закрытия. Но обычно графики строят на основе агрегированных данных. В этом случае берется цена закрытия каждого интервала. Так как мы отбрасываем все, что происходило в интервале, и берем только цену закрытия интервала, из-за этого теряется информативность.
Разрешение графика
Если мы начнем строить график на основе всех изменений цены, т.е каждая закрытая сделка будет попадать на график, то человеку будет сложно его воспринимать. Да и мощности, затраченные на обработку и доставку такого графика, будут израсходованы неэффективно.
Поэтому данные прореживают, разбивая ось времени на интервалы и агрегируя цены в этих интервалах. Разрешение – размер элементарного интервала разбиения оси времени: секунда, минута, час, день и так далее.
Бары
Относятся к интервальным графикам. Для повышения информативности, необходимо для каждого интервала времени отобразить информацию о цене в начале и конце интервала, а также максимальную и минимальную цену. Графическое отображение этого набора называется баром. Рассмотрим схему одного бара:
Последовательность баров формирует график:
Японские свечи
Как и бары, относятся к интервальным графикам. Являются самым популярным типом графика при техническом анализе. Свеча состоит из чёрного либо белого тела и теней: верхней и нижней. Иногда тень называют фитилем. Верхняя и нижняя граница тени отображает максимум и минимум цены за соответствующий период. Границы тела отображают цену открытия и закрытия. Изобразим свечу:
Последовательность свеч образуют график:
OHLCV нотация
В прошлой статье мы разобрались со схемой хранения данных для графика в postgresql и создали таблицу для источника данных, которая будет хранить агрегированные данные:
CREATE TABLE df ( t timestamp without time zone NOT NULL, r df_resolution NOT NULL DEFAULT '1m'::df_resolution, o numeric(64,32), h numeric(64,32), l numeric(64,32), c numeric(64,32), v numeric(64,32), CONSTRAINT df_pk PRIMARY KEY (t, r) )
Поля не требуют объяснения, кроме поля r – разрешение ряда. В postgresql есть перечисления, ими удобно пользоваться, когда заранее известен набор значений для какого-то поля. Через перечисления определим новый тип для допустимых разрешений графиков. Пусть это будет ряд от одной минуты до одного месяца:
CREATE TYPE df_resolution AS ENUM ('1m', '3m', '5m', '15m', '30m', '45m', '1h', '2h', '4h', '6h', '8h', '12h', '1d', '3d', '1w', '1M');
Важно найти баланс между производительностью дисковой системы, процессора и итоговой стоимостью владения. В системе на данный момент определены 16 резолюций. Очевидными являются два решения:
- Мы можем рассчитывать и хранить все резолюции в базе. Вариант удобен тем, что при выборке мы не тратим мощности на агрегацию интервалов, все данные сразу готовы к выдаче. В месяц для одного инструмента будет создано чуть более 72 тыс. записей. Выглядит просто и удобно, однако такая таблица будет слишком часто изменяться, так как на каждое обновление цен необходимо создать или обновить 16 записей в таблице и перестроить индекс. В postgresql дополнительно может возникнуть проблема со сборкой мусора.
- Другим вариантом является хранение единственной базовой резолюции. При выборке из базовой резолюции необходимо построить требуемые резолюции. Например, при хранении минутной резолюции в качестве базовой в месяц для каждого инструмента будет создано 43 тыс записей. Таким образом, по сравнению с прошлым вариантом, объем записи и накладных расходов уменьшается на 40%. Нагрузка на процессор, однако, возрастает.
Как говорилось выше, важно найти баланс. Поэтому компромиссным вариантом будет хранение не одной базовой резолюции, а нескольких: 1 минута, 1 час, 1 день. При такой схеме для каждого инструмента в месяц будет создано 44,6 тыс записей. Оптимизация объема записи составит 36%, но при этом нагрузка на процессор будет приемлемой. Например, для построения недельных интервалов вместо считывания и агрегации 10080 записей в случае минутной базовой резолюции, нам потребуется считать с диска и агрегировать данные всего 7-ми дневных резолюций.
Хранение OHLCV
По природе OHLCV – временной ряд. Как известно, реляционная база данных не очень хорошо подходит для хранения и обработки подобных данных. Для решения этих проблем в проекте используется расширение Timescale.
Timescale улучшает производительность операций вставки и обновления, позволяет настроить партиционирование, предоставляет оптимизированные специально для работы с временными рядами аналитические функции.
Для создания и обновления баров нам потребуются только стандартные функции:
date_trunc(‘minute’ | ’hour’ | ’day’, transaction_ts)
– для нахождения начала интервала минутной, часовой и дневной резолюции соответственно.greatest
иleast
для определения максимальной и минимальной цены.
Благодаря upsert api на каждую транзакцию выполняется только один запрос обновления. У меня получился вот такой SQL для фиксации изменений рынка в базовых резолюциях:
FOR i IN 1 .. array_upper(storage_resolutions, 1) LOOP resolution = storage_resolutions[i]; IF resolution = '1m' THEN SELECT DATE_TRUNC('minute', ts) INTO bar_start; ELSIF resolution = '1h' THEN SELECT DATE_TRUNC('hour', ts) INTO bar_start; ELSIF resolution = '1d' THEN SELECT DATE_TRUNC('day', ts) INTO bar_start; END IF; EXECUTE format( 'INSERT INTO %I (t,r,o,h,l,c,v) VALUES (%L,%L,%L::numeric,%L::numeric,%L::numeric,%L::numeric,%L::numeric) ON CONFLICT (t,r) DO UPDATE SET h = GREATEST(%I.h, %L::numeric), l = LEAST(%I.l, %L::numeric), c = %L::numeric, v = %I.v + %L::numeric;', df_table, bar_start, resolution, price, price, price, price, volume, df_table, price, df_table, price, price, df_table, volume ); END LOOP;
При выборке, для агрегации интервалов нам понадобятся следующие функции:
time_bucket
– для разбиения на интервалыfirst
– для нахождения цены открытия –O
max
– наибольшая цена за интервал –H
min
– наименьшая цена за интервал –L
last
– для нахождения цены закрытия –C
sum
– для нахождения объема торгов –V
Единственная найденная проблема при использовании Timescale – ограничения функции time_bucket
. Она позволяет оперировать только интервалами меньшими чем месяц. Для построения месячной резолюции необходимо использовать стандартную функцию date_trunc
.
API
Для отображения графиков на клиенте будем использовать lightweight-charts от Tradingview. Библиотека позволяет полностью настроить внешний вид графиков и удобна в работе. У меня получились вот такие графики:
Поскольку основная часть взаимодействия между браузером и платформой осуществляется через websocket, то проблем с интерактивностью не возникает.
Источник данных
Источник данных для графиков (датафид) должен возвращать необходимую часть временного ряда в требуемой резолюции. При этом для экономии трафика и уменьшения времени обработки на клиенте сервер должен упаковать точки.
API для датафида изначально нужно проектировать так, чтобы можно было запросить множество графиков в одном запросе и подписаться на их обновления. Это сократит количество команд и ответов в канале.
Рассмотрим пример запроса 50 последних минут для USDGBP, с автоматической подпиской на обновления графика:
{ "m":"market", "c":"get_chart", "v":{ "charts":[ { "ticker":"USDGBP", "resolution":"1h", "from":0, "cnt":50, "send_updates":true } ] } }
Можно, конечно же, запрашивать диапазон дат (from, to), но так как интервал каждого бара известен, то декларативное API с указанием момента и количества бар мне кажется более удобным. Датафид на этот запрос ответит подобным образом:
{ "m":"market", "c":"chart", "v":{ "bar_fields":[ "t","uts","o","h","l","c","v" ], "items":[ { "ticker":"USDGBP", "resolution":"1h", "bars":[ [ "2019-12-13 13:00:00",1576242000,"0.75236800", "0.76926400","0.75236800","0.76926400","138.10000000" ], .... ] } ] } }
Поле bar_fields содержит информацию о позициях элементов. Дальнейшая оптимизация – вынести это поле в конфигурацию клиента, которую он получает от сервера при загрузке.
Таким образом, клиент получает необходимую часть исторических данных и строит начальное состояние графика. Если состояние меняется, ему приходит обновление, затрагивающее только последний бар.
{ "m":"market", "c":"chart_tick", "v":{ "ticker":"USDGBP", "resolution":"1h", "items":{ "v":"140.600", "ut":1576242000, "t":"2019-12-13T13:00:00", "o":"0.752368", "l":"0.752368", "h":"0.770531", "c":"0.770531" } } }
Предварительный итог
На протяжении цикла статей мы с вами разбирали теорию и практику построения биржи. Пришло время собирать систему воедино.
В следующей статье мы затронем вопросы разработки графических интерфейсов пользователя: служебный UI для управления платформой и UI для конечных потребителей. Также будет представлена демонстрационная версия Vonmo Trade.