Эксперимент VonmoTrade. Часть 4: Торговые графики


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


    Прежде чем начать, хочу сделать небольшое отступление. Для внутренних проектов Vonmo используется обычная схема именования V+слово, наиболее ёмко характеризующее функции проекта. Сегодня я обнаружил, что VTrade – уже существующая компания. Дабы не вносить путаницу, я переименовал эксперимент в VonmoTrade.


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


    1. Линейные;
    2. Интервальные.

    Линейные графики


    Самый простой и понятный без подготовки график. Отображает зависимость цены финансового инструмента от времени.



    Основное достоинство этого типа графиков – простота. Из него же вытекает основной недостаток – низкая информативность.


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


    Разрешение графика


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


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


    Бары


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



    Последовательность баров формирует график:



    Японские свечи


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



    Последовательность свеч образуют график:



    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.

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое