Разработка отображения глубины рынка (depth chart)

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1Все 1306 услуг
Разработка отображения глубины рынка (depth chart)
Средний
~3-5 дней
Часто задаваемые вопросы

Направления блокчейн-разработки

Этапы блокчейн-разработки

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1288
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1198
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    902
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1122
  • image_logo-advance_0.webp
    Разработка логотипа компании B2B Advance
    589
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    859

Разработка визуализации DOM (Depth of Market)

DOM (Depth of Market), также известный как Level 2 данные — это визуализация всего стакана ордеров, а не только лучшей цены. Профессиональные трейдеры читают DOM как книгу: видят стены ликвидности, поглощение объёмов, спуфинг. Хорошая реализация DOM — один из ключевых аргументов для привлечения профессионалов на биржу.

Структура DOM

DOM отображает два столбца: bid (покупки) и ask (продажи) с агрегированными объёмами на каждом ценовом уровне.

     BID                ASK
Volume    Price    Price    Volume
  0.5   42,100  | 42,101   1.2
  1.8   42,095  | 42,102   0.7
  3.2   42,090  | 42,105   4.5  ← wall
  0.4   42,085  | 42,110   0.9
  2.1   42,080  | 42,115   1.1

"Стена" (wall) — аномально большой объём на уровне — часто указывает на зону поддержки/сопротивления. Трейдеры отслеживают как эти объёмы появляются, изменяются и исчезают.

Реализация WebSocket обновлений

DOM требует минимальной latency. Обновления через WebSocket diff, клиент поддерживает локальную копию:

interface DOMState {
  bids: Map<string, string>;  // price -> size
  asks: Map<string, string>;
  sequence: number;
}

class DOMManager {
  private state: DOMState = { bids: new Map(), asks: new Map(), sequence: 0 };
  private ws: WebSocket;
  
  async initialize(pair: string) {
    // 1. Загружаем снэпшот
    const snap = await fetch(`/api/v1/markets/${pair}/orderbook?depth=100`).then(r => r.json());
    snap.bids.forEach(([p, s]: string[]) => this.state.bids.set(p, s));
    snap.asks.forEach(([p, s]: string[]) => this.state.asks.set(p, s));
    this.state.sequence = snap.sequence;
    
    // 2. Подписываемся на diff updates
    this.ws = new WebSocket(`wss://api.exchange.com/ws`);
    this.ws.send(JSON.stringify({ op: 'subscribe', channel: `orderbook.${pair}.100` }));
    this.ws.onmessage = (e) => this.applyUpdate(JSON.parse(e.data));
  }
  
  private applyUpdate(msg: OrderBookDiff) {
    if (msg.seq !== this.state.sequence + 1) {
      this.reinitialize();  // gap — нужен новый снэпшот
      return;
    }
    
    msg.bids.forEach(([p, s]: string[]) => {
      if (s === '0') this.state.bids.delete(p);
      else this.state.bids.set(p, s);
    });
    msg.asks.forEach(([p, s]: string[]) => {
      if (s === '0') this.state.asks.delete(p);
      else this.state.asks.set(p, s);
    });
    this.state.sequence = msg.seq;
    
    this.notifyRenderers();
  }
  
  // Возвращает топ N уровней в нужном формате
  getTopLevels(depth: number = 20) {
    const bids = [...this.state.bids.entries()]
      .map(([p, s]) => [parseFloat(p), parseFloat(s)] as [number, number])
      .sort((a, b) => b[0] - a[0])
      .slice(0, depth);
    
    const asks = [...this.state.asks.entries()]
      .map(([p, s]) => [parseFloat(p), parseFloat(s)] as [number, number])
      .sort((a, b) => a[0] - b[0])
      .slice(0, depth);
    
    return { bids, asks };
  }
}

Рендеринг DOM компонента

Высокочастотные обновления DOM (до 20–50 раз/сек на активных парах) требуют оптимизированного рендеринга. Использование обычного React state с re-render при каждом обновлении даст проблемы с производительностью.

import { useRef, useCallback } from 'react';

// Прямое DOM manipulation для hot path
const DOMRow = React.memo(({ price, size, total, maxTotal, side, highlight }: RowProps) => {
  const rowRef = useRef<HTMLDivElement>(null);
  
  // Обновляем DOM напрямую без React re-render
  const update = useCallback((newSize: string, newTotal: number) => {
    if (!rowRef.current) return;
    const sizeEl = rowRef.current.querySelector('.size');
    const depthEl = rowRef.current.querySelector('.depth-bar') as HTMLElement;
    if (sizeEl) sizeEl.textContent = newSize;
    if (depthEl) depthEl.style.width = `${(newTotal / maxTotal) * 100}%`;
  }, [maxTotal]);
  
  // Flash animation при изменении
  const flash = useCallback((direction: 'up' | 'down') => {
    rowRef.current?.classList.add(`flash-${direction}`);
    setTimeout(() => rowRef.current?.classList.remove(`flash-${direction}`), 300);
  }, []);
  
  return (
    <div ref={rowRef} className={`dom-row ${side}`}>
      <div className="depth-bar" style={{ width: `${(total/maxTotal)*100}%` }} />
      <span className="price">{formatPrice(price)}</span>
      <span className="size">{formatSize(size)}</span>
      <span className="total">{formatSize(total)}</span>
    </div>
  );
});

Визуальные особенности профессионального DOM

Подсветка изменений

// Detect changes between updates
function diffDOMStates(prev: DOMLevel[], curr: DOMLevel[]) {
  const changes: Map<string, 'increased' | 'decreased' | 'new' | 'removed'> = new Map();
  
  const prevMap = new Map(prev.map(l => [l.price, l.size]));
  const currMap = new Map(curr.map(l => [l.price, l.size]));
  
  for (const [price, size] of currMap) {
    const prevSize = prevMap.get(price);
    if (!prevSize) changes.set(price, 'new');
    else if (size > prevSize) changes.set(price, 'increased');
    else if (size < prevSize) changes.set(price, 'decreased');
  }
  
  for (const price of prevMap.keys()) {
    if (!currMap.has(price)) changes.set(price, 'removed');
  }
  
  return changes;
}

Тик группировка

// Пользователь переключает группировку: 1, 5, 10, 25, 100
function groupByTick(levels: DOMLevel[], tickSize: number): DOMLevel[] {
  const grouped = new Map<number, number>();
  
  for (const { price, size } of levels) {
    const bucket = Math.floor(price / tickSize) * tickSize;
    grouped.set(bucket, (grouped.get(bucket) ?? 0) + size);
  }
  
  return [...grouped.entries()]
    .map(([price, size]) => ({ price, size }))
    .sort((a, b) => b.price - a.price);
}

Cumulative volume visualization

Накопительный объём показывает суммарную ликвидность до каждого уровня — видно, насколько глубокий стакан:

function addCumulative(levels: DOMLevel[]): DOMLevelWithCum[] {
  let cumulative = 0;
  return levels.map(level => {
    cumulative += level.size;
    return { ...level, cumulative };
  });
}

Производительность

На активных парах (BTC/USDT на крупных биржах) DOM может обновляться 10–50 раз/сек. Ограничения:

  • Throttle updates: не более 10 рендеров/сек для DOM (человеческий глаз не воспринимает быстрее)
  • Virtual scrolling: если показываем > 50 уровней — FlashList или react-window
  • Canvas rendering: для максимальной производительности можно рендерить DOM в Canvas вместо HTML
// Throttle рендеринга до 10 fps
const throttledRender = useCallback(
  throttle((domData: DOMData) => {
    setDisplayData(domData);
  }, 100),  // 100ms = 10 fps
  []
);

Разработка DOM визуализатора с real-time обновлениями, группировкой тиков и подсветкой изменений: 3–5 недель.