Skip to main content

Overview

The order book system provides real-time visualization of market depth, displaying bids and asks with cumulative depth indicators. It includes a tabbed interface to switch between the order book and recent trades views.

Depth Component

The Depth component is the container that manages WebSocket subscriptions and state:
Depth.tsx
export const Depth = ({ market }: { market: string }) => {
  const [activeTab, setActiveTab] = useState("orderbook");
  const { setTrades, setBids, setAsks, setPrice } = useContext(TradesContext);

  useEffect(() => {
    // Subscribe to depth updates
    WsManager.getInstance().registerCallback("depth", (data: any) => {
      setBids((originalBids) => {
        let bidsAfterUpdate = [...(originalBids || [])];
        // Update logic...
        return bidsAfterUpdate.slice(-30);
      });
    }, `DEPTH-${market}`);

    // Subscribe to trade updates
    WsManager.getInstance().registerCallback("trade", (data: any) => {
      const newTrade: Trade = {
        id: data.t,
        isBuyerMaker: data.m,
        price: data.p,
        quantity: data.q,
        timestamp: data.T,
      };
      setTrades((oldTrades) => {
        const newTrades = [...oldTrades];
        newTrades.unshift(newTrade);
        newTrades.pop();
        return newTrades;
      });
    }, `TRADE-${market}`);

    WsManager.getInstance().sendMessage({
      method: "SUBSCRIBE",
      params: [`depth.${market}`],
    });

    return () => {
      WsManager.getInstance().deRegisterCallback("depth", `DEPTH-${market}`);
    };
  }, [market]);

  return (
    <div className="h-full bg-container-bg rounded-xl border">
      {activeTab === "orderbook" ? <OrderBook /> : <RecentTrades />}
    </div>
  );
};

OrderBook Component

The order book displays bids (buy orders) and asks (sell orders) with visual depth indicators:
OrderBook.tsx
export const OrderBook = () => {
  const { bids, asks, price, totalBidSize, totalAskSize } = useContext(TradesContext);

  const calculateCumulativeWidth = (
    cumulativeSize: number,
    totalSize: number
  ): string => {
    return totalSize ? `${(cumulativeSize * 100) / totalSize}%` : "0%";
  };

  let cumulativeBidSize = 0;

  return (
    <div className="h-full">
      <div className="flex-1 overflow-y-auto">
        {bids?.map((order, index) => {
          const size = parseFloat(order[1]);
          cumulativeBidSize += size;

          return (
            <div key={index} className="relative w-full">
              <div className="flex justify-between">
                <div className="text-text-positive-green-button">
                  {order[0]}
                </div>
                <div>{order[1]}</div>
              </div>
              {/* Cumulative background */}
              <div className="absolute opacity-20">
                <div
                  className="bg-positive-green-pressed"
                  style={{
                    width: calculateCumulativeWidth(cumulativeBidSize, totalBidSize),
                    transition: 'width 0.3s ease-in-out'
                  }}
                />
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
};

Recent Trades Component

Displays the latest executed trades with time, price, and size:
RecentTrades.tsx
export const RecentTrades = () => {
  const { trades } = useContext(TradesContext);

  const formatTime = (timestamp: number) => {
    const date = new Date(timestamp);
    return date.toLocaleTimeString("en-US", {
      hour: "2-digit",
      minute: "2-digit",
      second: "2-digit",
    });
  };

  return (
    <div className="h-full flex flex-col">
      {/* Header */}
      <div className="grid grid-cols-3 gap-4 py-2">
        <span>Price (USD)</span>
        <span>Size (SOL)</span>
        <span>Time</span>
      </div>

      {/* Trades List */}
      <div className="flex-1 overflow-y-auto">
        {trades.map((trade, index) => (
          <div key={index} className="grid grid-cols-3 py-2">
            <span className={trade.isBuyerMaker ? "text-positive-green" : "text-negative-red"}>
              {trade.price}
            </span>
            <span>{trade.quantity}</span>
            <span>{formatTime(trade.timestamp)}</span>
          </div>
        ))}
      </div>
    </div>
  );
};

WebSocket Updates

The order book receives real-time updates through two WebSocket channels:
  • depth.{market}: Order book updates
  • trade.{market}: Recent trade updates

Update Logic

When a depth update is received:
  1. Iterate through existing bids/asks
  2. Update matching price levels with new size
  3. Remove orders with size = 0
  4. Add new price levels not in current book
  5. Sort by price (descending for bids, ascending for asks)
  6. Limit to 30 levels per side

Data Structure

type Order = [price: string, size: string];

type Trade = {
  id: string;
  isBuyerMaker: boolean;
  price: string;
  quantity: string;
  quoteQuantity: string;
  timestamp: number;
};

Usage Example

import { Depth } from "@/components/Depth";
import { TradesProvider } from "@/state/TradesProvider";

function App() {
  return (
    <TradesProvider>
      <Depth market="SOL_USDC" />
    </TradesProvider>
  );
}

Props

PropTypeDescription
marketstringMarket identifier for subscriptions

Performance Optimizations

  • Order book limited to 30 levels per side to reduce rendering overhead
  • Updates use functional state updates to prevent race conditions
  • Trades list limited to 50 most recent trades
  • Smooth scrolling disabled by default for better performance

State Management

The order book relies on TradesContext for state management. Ensure the component is wrapped in TradesProvider.

Visual Customization

Colors are defined in Tailwind configuration:
{
  'positive-green': '#34cb88',
  'positive-green-pressed': '#2ba872',
  'negative-red': '#ff615c',
  'negative-red-pressed': '#e54d49'
}

Build docs developers (and LLMs) love