Web trading terminal with charts and order book

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Web trading terminal with charts and order book
Complex
from 2 weeks to 3 months
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1217
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1046
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Development of Web Trading Terminal with Charts and Order Book

A web trading terminal with charts and order book is the most technically demanding type of web application. Simultaneously: real-time data in multiple streams, financial data visualization, high-frequency UI updates, complex business logic. Architectural decisions made at the beginning determine scalability and performance for years to come.

Technology Stack

Frontend: React 18 + TypeScript, Zustand (state management), TradingView Lightweight Charts or Advanced Charts, react-virtual (virtualization), WebSocket via custom hook.

Backend: FastAPI (Python) or Fastify (Node.js) for WebSocket gateway, Redis for caching order book state and pub/sub.

Data Layer: ClickHouse or TimescaleDB for historical OHLCV data.

Data Flow Graph

Binance WS → Exchange Connector → Redis PubSub → WS Gateway → Browser
                                              ↘
                                        ClickHouse (persist)

Browser subscribes to Gateway via WebSocket. Gateway multiplexes data from the exchange for all connected clients.

Order Book: Implementation Details

Order book updates via differential updates. Binance provides a snapshot via REST and subsequent diffs via WebSocket:

class OrderBookManager {
  private bids: Map<number, number> = new Map();
  private asks: Map<number, number> = new Map();
  private lastUpdateId: number = 0;
  private buffer: OrderBookDiff[] = [];

  async initialize(symbol: string) {
    // 1. Subscribe to diff stream
    const ws = this.connectToStream(`${symbol.toLowerCase()}@depth`);
    
    // 2. Get snapshot (after subscription!)
    const snapshot = await fetchOrderBookSnapshot(symbol, 1000);
    
    // 3. Apply snapshot
    this.lastUpdateId = snapshot.lastUpdateId;
    this.bids = new Map(snapshot.bids.map(([p, q]) => [+p, +q]));
    this.asks = new Map(snapshot.asks.map(([p, q]) => [+p, +q]));
    
    // 4. Apply buffered diffs, skipping outdated ones
    for (const diff of this.buffer) {
      if (diff.U <= this.lastUpdateId + 1 && diff.u >= this.lastUpdateId + 1) {
        this.applyDiff(diff);
      }
    }
    this.buffer = [];
  }

  applyDiff(diff: OrderBookDiff) {
    // Check sequence
    if (diff.U !== this.lastUpdateId + 1) {
      console.error('Gap detected, reinitializing...');
      this.initialize(this.symbol);
      return;
    }
    
    for (const [price, qty] of diff.b) {
      if (+qty === 0) this.bids.delete(+price);
      else this.bids.set(+price, +qty);
    }
    for (const [price, qty] of diff.a) {
      if (+qty === 0) this.asks.delete(+price);
      else this.asks.set(+price, +qty);
    }
    
    this.lastUpdateId = diff.u;
    this.notifySubscribers();
  }

  getTopLevels(depth: number = 20) {
    const sortedBids = [...this.bids.entries()]
      .sort(([a], [b]) => b - a)
      .slice(0, depth);
    const sortedAsks = [...this.asks.entries()]
      .sort(([a], [b]) => a - b)
      .slice(0, depth);
    
    return { bids: sortedBids, asks: sortedAsks };
  }
}

Order Book Component with Virtualization

import { useVirtualizer } from '@tanstack/react-virtual';
import { useOrderBook } from '../hooks/useOrderBook';

const ORDER_BOOK_ROW_HEIGHT = 20;

export function OrderBook({ symbol }: { symbol: string }) {
  const { bids, asks, spread } = useOrderBook(symbol);
  const parentRef = React.useRef<HTMLDivElement>(null);

  const bidVirtualizer = useVirtualizer({
    count: bids.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => ORDER_BOOK_ROW_HEIGHT,
    overscan: 5,
  });

  return (
    <div className="order-book">
      <div className="asks-section">
        {asks.slice(0, 20).reverse().map(([price, qty, total]) => (
          <OrderBookRow key={price} side="ask" price={price} qty={qty} total={total} />
        ))}
      </div>
      
      <div className="spread-row">
        Spread: {spread.toFixed(2)} ({spreadBps.toFixed(2)} bps)
      </div>
      
      <div ref={parentRef} className="bids-section">
        <div style={{ height: bidVirtualizer.getTotalSize() }}>
          {bidVirtualizer.getVirtualItems().map((virtualItem) => {
            const [price, qty, total] = bids[virtualItem.index];
            return (
              <div
                key={virtualItem.key}
                style={{ transform: `translateY(${virtualItem.start}px)` }}
              >
                <OrderBookRow side="bid" price={price} qty={qty} total={total} />
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

Charts: TradingView Lightweight Charts

For most web terminals, TradingView Lightweight Charts is the optimal choice:

import { createChart, CandlestickSeries } from 'lightweight-charts';

function TradingChart({ symbol }: { symbol: string }) {
  const chartContainerRef = useRef<HTMLDivElement>(null);
  const seriesRef = useRef<CandlestickSeries>();

  useEffect(() => {
    const chart = createChart(chartContainerRef.current!, {
      width: chartContainerRef.current!.clientWidth,
      height: 400,
      layout: {
        background: { color: '#1a1a2e' },
        textColor: '#d1d4dc',
      },
      grid: {
        vertLines: { color: '#2d2d4e' },
        horzLines: { color: '#2d2d4e' },
      },
      crosshair: { mode: 1 },
      timeScale: { borderColor: '#485c7b', timeVisible: true },
    });

    const candleSeries = chart.addCandlestickSeries({
      upColor: '#26a69a',
      downColor: '#ef5350',
      borderUpColor: '#26a69a',
      borderDownColor: '#ef5350',
    });
    seriesRef.current = candleSeries;

    // Load historical data
    loadHistoricalData(symbol, '1h').then((candles) => {
      candleSeries.setData(candles);
      chart.timeScale().fitContent();
    });

    // Subscribe to new candles
    const unsubscribe = wsGateway.subscribe(`candle:${symbol}:1h`, (candle) => {
      candleSeries.update(candle);
    });

    return () => {
      unsubscribe();
      chart.remove();
    };
  }, [symbol]);

  return <div ref={chartContainerRef} className="chart-container" />;
}

Performance

Critical optimization points:

Throttle updates — order book can receive 50+ updates per second. Apply all diffs in memory, but render no more than via requestAnimationFrame:

let pendingRender = false;

function scheduleRender() {
  if (!pendingRender) {
    pendingRender = true;
    requestAnimationFrame(() => {
      pendingRender = false;
      renderOrderBook();
    });
  }
}

Immutable updates with memo — React.memo + useMemo for order book rows to avoid unnecessary re-renders.

Canvas instead of DOM for depth chart — cumulative order book volume on Canvas performs significantly faster than DOM for frequent updates.

WebSocket Connection Management

export function useWebSocket(url: string) {
  const wsRef = useRef<WebSocket | null>(null);
  const reconnectTimerRef = useRef<NodeJS.Timeout>();

  const connect = useCallback(() => {
    wsRef.current = new WebSocket(url);
    
    wsRef.current.onclose = (e) => {
      if (!e.wasClean) {
        // Exponential backoff
        const delay = Math.min(1000 * 2 ** reconnectCount, 30000);
        reconnectTimerRef.current = setTimeout(connect, delay);
      }
    };
    
    wsRef.current.onerror = () => wsRef.current?.close();
  }, [url]);

  useEffect(() => {
    connect();
    return () => {
      clearTimeout(reconnectTimerRef.current);
      wsRef.current?.close();
    };
  }, [connect]);

  return wsRef;
}

Correct reconnect with exponential backoff — a mandatory part of a production terminal. User should not see "disconnected" for more than a few seconds.