Руководство по выходу из TradingView + WebSocket в режиме реального времени по К-линии

внешний интерфейс JavaScript WebSocket React.js
Руководство по выходу из TradingView + WebSocket в режиме реального времени по К-линии

[Впервые опубликовано вмой личный блог

0. Было предложено обновить

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

Воспользуйтесь праздником, поторопитесь обновить раунд... Подождите, когда это стало работой?

1. Что такое TradingView?

Сегодня мы поговорим о чем-то особенном...TradingView, это профессиональная библиотека графиков, которая специализируется на свечных графиках, а свечные графики необходимы для таких бирж, как акции и фонды. Сам проект бесплатный, но не с открытым исходным кодом.Официальный предоставляет частную библиотеку, размещенную на Github, и разработчикам нужно только предоставить официальному лицу некоторую необходимую информацию, чтобы получить доступ. Основной репозиторий содержит сжатые файлы библиотек и простые примеры доступа к данным; документация по разработке представлена ​​в Wiki, а некоторые практические примеры также представлены в других репозиториях.

Некоторые библиотеки графиков, обычно используемые во внешнем интерфейсе, такие как ECharts и DataV, на самом деле поддерживают отрисовку базовых графиков свечей (некоторые из них называются свечными графиками, но называются по-разному), а также могут рисовать объем, скользящую среднюю и т. д. с помощью гистограмм. линейные графики и другие индикаторы. Будучи профессиональным отраслевым продуктом, TradingView предоставляет большое количество профессиональных инструментов измерения для профессиональных инвесторов и аналитиков в дополнение к вышеупомянутым графикам.Если все это будет реализовано самими разработчиками, это будет стоить больших денег.Много энергии. , этот пакет-пакет, несомненно, является его самым привлекательным местом.

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

2. Профессионал === Проблема

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

По сравнению с ECharts и DataV, которые готовы ко всему, пока вы заполняете данные и настраиваете параметры библиотеки графиков «гражданского уровня», TradingView намного сложнее начать работу, он требует от разработчиков реализации набора источников данных по своим правилам.API, хотя официальное описание функции и параметров каждого API дано, но некоторые ключевые моменты не объяснены внятно.Многие разработчики (включая меня и некоторых коллег, с которыми я контактировал ) еще не читал документацию.Могу хорошо понять "как пользоваться этой тм". Я написал этот блог в надежде внести небольшой вклад в решение этой проблемы и облегчить ее для опоздавших.

3. Чтобы сэкономить время

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

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

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

4. Давайте сначала поговорим о концепции

В TradingView есть еще несколько профессиональных понятий, которые непросты для понимания, но очень важны, вот краткое пояснение.

4.1. Symbol

Символ буквально переводится как «символ, символ», что распространяется на «товар». Линия К показывает тренд изменения цены.Что касается цены чего-либо, то это может быть акция, валюта или любой товар.Торговля View предоставляет такое абстрактное понятие для общего пользования. Символ — это JS-объект, описывающий некоторые атрибуты продукта (название, десятичные знаки цены, поддерживаемое временное разрешение, время открытия транзакции и т. д., подробности см. в официальной документации). к определению символа какие данные.

Фиксированный формат названия товара — «ОБМЕН:СИМВОЛ», СИМВОЛ представляет товар, например, акцию, торговую пару; ОБМЕН — это название биржи, один и тот же товар может иметь разные цены на разных биржах, поэтому он должен чтобы отличиться.

4.2. Resolution

Буквальный перевод Разрешения называется «разрешение», которое относится к временному интервалу между двумя соседними столбцами на графике линии К. Я не изучал, является ли этот термин просто используемым словом, но лично я чувствую, что это Другими словами, вы можете выразить это значение другими словами, но TradingView выбирает именно это слово.

4.3. Study

Study буквально переводится как «обучение, исследование», что здесь объясняется как «индикаторы», такие как объем, скользящее среднее и различные другие индикаторы анализа. Разработчики могут добавлять их самостоятельно через API, предоставляемый TradingView.

4.4. Chart

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

4.5. Widget

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

4.6. FeatureSet

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

4.7. Overrides

Переопределения, часть параметров конфигурации виджета, используются для настройки стиля библиотеки диаграмм (в основном цвета различных частей диаграммы). Вся библиотека диаграмм состоит из внешней структуры DOM и нескольких внутренних холстов, поэтому настройки, связанные со стилями, также разделены на две части, здесь находятся настройки для части холста, а есть еще одна.custom_css_urlСвойства используются для указания файла css, в котором стили могут быть переопределены для частей DOM. Конкретные могут быть объединены с официальными документами и найти Chrome DevTool.

4.8. DataFeed

Источник данных, то есть следующее, о чем стоит поговорить. Это набор методов сбора и обработки данных для TradingView, а также ядро ​​доступа к данным TradingView, которое должны реализовать сами пользователи. Это может быть экземпляр класса или простой объект.

5. Как получить доступ к своим данным

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

Универсальность TradingView заключается в том, что он разделяет данные и производительность.Сама библиотека графиков предоставляет только часть производительности.Независимо от того, какие данные у вас есть, если их можно отсортировать в указанном формате и заполнить, все будет хорошо. . Грубо говоря, разработчикам нужно самим реализовать адаптер.

TradingView предоставляет два способа получения данных: решение на основе HTTP (UDF, Universal Data Feed, которое используется в демонстрации в основном репозитории) и решение на основе WebSocket (JS API).

udf_or_jsapi

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

5.1 Схема УДФ

udf
UDF — это набор протоколов, определенных самим TradingView. По сути, это также вызывается JS API. Протокол основан на HTTP+опросе, который запрашивает исторические данные при определенных условиях через HTTP-запросы, а затем непрерывно опрашивает для проверки наличия новых данных.

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

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

5.2. JS API

jsapi
Это ядро ​​доступа к данным TradingView.Через этот API разработчики могут получить доступ к любому типу данных.Конечно, наиболее распространенным является WebSocket. Упомянутая выше схема UDF фактически вызывает эти API.

Официальная документация описывает каждый API, а необходимыеonReady(),resolveSymbol(),getBars(),subscribeBars(),unsubscribeBars(), остальные реализуются по потребностям, здесь мы говорим только о самом основном использовании. Первые два несложны, давайте сосредоточимся на последнем. (Здесь мы реализуем его в виде методов экземпляра класса DataFeed, также можно просто создать JS-объект, содержащий эти функции)

5.2.1. getBars()

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

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

getBars (symbolInfo, resolution, from, to, onHistoryCallback, onErrorCallback, firstDataRequest) {
  function _send (data) {
    // 按时间筛选
    const dataInRange = data.length
      ? data.filter(n => n.time >= from && n.time <= to)
      : []

    // 没有数据就返回 noData
    const meta = {
      noData: !dataInRange.length
    }

    // 有数据,则整理成图表库要求的格式
    const bar = [...dataInRange]

    // 触发回调
    onHistoryCallback(bar, meta)
  }
}

Возьмем эту функцию какgetBars()внутренняя функция , гдеfrom,to,onHistoryCallbackпараметр, предоставляемый API,dataданные, которые мы получаем,(bar, meta)Фиксированный формат, требуемый TradingView.

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

getBars (symbolInfo, resolution, from, to, onHistoryCallback, onErrorCallback, firstDataRequest) {

  function _send (data) {
    // ...
  }
  
  // 一个简单的工具函数,实现倒序查找
  // 可以简单理解为 Array.prototype.findIndex 的倒序版本
  // 后面会用到
  function _findLastIndex (arr, fn) {
    for (var i = arr.length - 1; i >= 0; i--) {
      if (fn(arr[i])) return i
    }
    return -1
  }

  // 出于数据共享的需要
  // 我们把获取到的数据放到 Redux 里
  // 先尝试从 Redux 获取现有数据
  const existingData = store.getState().kChartData || []

  // 如果 Redux 中已有数据,则直接读取
  if (existingData.length) {
    _send(existingData)
    return
  }

  // 如果 Redux 中没数据,则通过 WebSocket 加载
  // 我们的设计是历史数据和实时更新都走 WebSocket
  // 首次推送历史数据,后续推送更新
  // 所以同一交易对、分辨率,只会发起一个 WebSocket 请求

  // 先判断功能支持度
  // 这里我们用 WebWorker 把 WebSocket 的逻辑独立到主线程之外
  // 以达到性能优化的目的,这个后面再详述。
  if (!window.Worker) return

  // 限制 Worker 单例
  const hasWSInstance = !!window.kChartWorker
  window.kChartWorker = window.kChartWorker || new window.Worker('./worker-kchart.js')

  // WebWorker 数据推送回调
  window.kChartWorker.onmessage = e => {
    const { data = {} } = e

    // 当有数据推送时
    if (data.kChartData) {
      // 获取已有数据
      const kChartData = store.getState().kChartData
      
      // 增量更新
      for (const item of data.kChartData) {
        // 因为 K 线的数据是按时间顺序排列的,
        // 数据的更新都在末端,所以倒序搜索更快
        const idx = _findLastIndex(kChartData, n => n.time === item.time)
        idx < 0
          ? kChartData.push(item)
          : kChartData[idx] = { ...kChartData[idx], ...item }
      }

      // 把新数据记录到 Redux
      const promise = new Promise((resolve, reject) => {
        store.dispatch(setKChartData(kChartData))
        resolve({
          full: kChartData, // 最新的完整数据 
          updates: data.kChartData // 本轮更新的内容
        })
      })

      promise.then(res => {
        // dataInited 是我们自定义的一个变量
        // 用来区分首次推送和后续推送
        // 初始为 false,首次推送后置为 true
        if (this.dataInited) {
          // 如非首次推送
          // 对全局 K 线订阅列表中的每个订阅者(后面详述)
          window.kChartSubscriberList = window.kChartSubscriberList || []
          for (const sub of window.kChartSubscriberList) {
            // 按交易对、分辨率筛选
            if (sub.symbol !== this.symbol) return
            if (sub.resolution !== resolution) return

            // 通过回调函数推送数据
            if (typeof sub.callback !== 'function') return
            // 图表库一次只能增加一条数据,或更新离现在时间最近的一条历史数据
            // 而我们的推送数据是个数组,可能会包含不止一条数据
            // 所以这里要逐个推送
            for (const update of res.updates) {
              sub.callback(update)
            }
          }
        } else {
          // 首次推送
          _send(res.full)
          this.dataInited = true
        }
      })
    }
  }

  // 准备 WebWorker 消息
  // 只有当没有现成数据的时候才会执行到这里
  // 因此只有在初始化、切换交易对/分辨率的时候
  // 才会发起 WebSocket 请求
  const msg = {
    // action 表示行为目的
    // init 为初始化
    // restart 为切换交易对/分辨率
    // 对应不同的 WebSocket 操作
    action: hasWSInstance ? 'restart' : 'init',
    symbol: symbolInfo,
    resolution: resolution,
    url: WEBSOCKET_URL
  }

  // 发送 WebWorker 消息
  window.kChartWorker.postMessage(msg)
}

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

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

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

5.2.2. subscribeBars()

В документации сказано, что эта функция используется для подписки на данные К-линии, плюс "getBars()изonHistoryCallbackОбратный вызов вызывается только один раз», эти два предложения ввели в заблуждение многих людей, думая, чтоgetBars()Он будет вызываться только один раз и завершится после получения исторических данных.subscribeBars()реализовано в. По сути, мы просто добавляем сюда подписчика, а callback-функцию для добавления обновленных данных храним во внешний слой, вызов callback-функции фактически впереди.getBars()Завершенный. Эквивалентом этой функции является только команда, все сбор и распространение данныхgetBars()осуществляется в.

subscribeBars (symbolInfo, resolution, onRealtimeCallback, subscriberUID, onResetCacheNeededCallback) {
  // 限制单例
  window.kChartSubscriberList = window.kChartSubscriberList || []

  // 避免重复订阅
  const found = window.kChartSubscriberList.some(n => n.uid === subscriberUID)
  if (found) return

  // 添加订阅
  window.kChartSubscriberList.push({
    symbol: symbolInfo,
    resolution: resolution,
    uid: subscriberUID,
    callback: onRealtimeCallback
  })
}

Эта функция будет вызываться один раз для каждой комбинации Символ + Разрешение и передавать соответствующую идентификационную информацию и функцию обратного вызова в список подписки.Когда поступят push-данные, она пройдет по списку подписки, найдет подходящих подписчиков и вызовет свою функцию обратного вызова. для передачи данных. По сути, это базовый «паттерн наблюдателя».

5.2.3. unsubscribeBars()

пониматьsubscribeBars(), то есть на самом делеunsubscribeBars()Тоже очень понятно, просто приведено:

unsubscribeBars (subscriberUID) {
  window.kChartSubscriberList = window.kChartSubscriberList || []

  const idx = window.kChartSubscriberList.findIndex(n => n.uid === subscriberUID)
  if (idx < 0) return

  window.kChartSubscriberList.splice(idx, 1)
}

6. Как сменить торговую пару/разрешение

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

this.widget = new window.TradingView.widget(widgetOptions)
this.widget.onChartReady(() => {
  this.chart = this.widget.chart()

  // 设置图表类型(比如分时图和常规的蜡烛图的类型就不一样)
  this.chart.setChartType(chartType)

  // 切换 Symbol
  this.chart.setSymbol(symbol, callback)

  // 切换 Resolution
  this.chart.setResolution(resolution, callback)
})

7. Другие подводные камни TradingView

  • Функции в JS API будут автоматически вызываться в нужное время, а фактические параметры будут передаваться. Нет необходимости рассматривать перенос функции на внешний уровень для ручного вызова.
  • в JS APIonReady()а такжеresolveSymbol()Эти две функции, их функции обратного вызова должны вызываться асинхронно, не спрашивайте почему, они необходимы.
  • Функции, которые переключаются между символом и разрешением, имеют обратный вызов, который не сработает, если новый набор значений совпадает с текущим существующим значением.

8. Оптимизация производительности K-линии

В процессе использования WebSocket мы использовали WebWorker для оптимизации производительности.

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

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

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

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

Некоторые люди будут обеспокоены совместимостью WebWorker, в конце концов, общая страница H5 будет редко использовать это, и они не знакомы с этим. Браузерная совместимость WebWorker примерно такая же, как и у WebSocket, по крайней мере, в рамках нашего беспокойства, она одинакова, оба IE 10 и выше, браузеры Ivy уже давно поддерживаются, так что если вам все еще не нужно быть совместимым со старым антиквариатом, не стесняйтесь использовать его.

9. Резюме

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