
# Build a trading terminal in React

By Danylo Pravda, 2026-10-08

A trading screen is the hardest everyday UI there is: numbers that change several times a second, prices people click
to trade at, red and green that must not be the only signal, and an order form where a wrong default costs money.
Beamline has eleven trading parts and one whole screen built from them. This guide shows how they fit together on your
own data, and what each one already handles.

![The Beamline trading terminal: market header, candles, order book, order ticket and positions](https://beamline.io/components/og/trading-terminal.png "The trading terminal block, on demo data")

## Start from the whole screen

The [trading terminal](https://beamline.io/components/trading-terminal) is a perpetuals desk: a ticker tape, a market header with funding,
candles, the book, trades and depth, an order ticket, account figures and positions. It arranges itself by its own
width, from a four-column desk down to a single phone column where Buy and Sell open the ticket in a sheet. You give it
three things:

```tsx
import { TradingTerminal } from "@beamline/trading-terminal";

<TradingTerminal symbols={tickers} useFeed={useExchangeFeed} account={useExchangeAccount()} leverage={10} />
```

- `symbols`: the markets in the watchlist.
- `useFeed`: a hook you write around your exchange stream. It is called once per open market (a new symbol or interval
  remounts it) and returns the current market on every update.
- `account`: your order API, wrapped in one object.

The terminal never fills an order itself. It shows what your feed and your account say, and calls your functions.

## Your market feed

The feed is a plain shape, defined in `@beamline/lib-market`:

```ts
interface MarketFeed {
  candles: Candle[];
  bids: Level[]; // [price, size]
  asks: Level[];
  trades: Trade[];
  last: number;
  prev: number;
  change: number;
  funding?: { rate: number; nextAt: number }; // perpetuals only
}
```

Write the hook around whatever your exchange sends, a WebSocket or polling, and map each message onto that shape:

```tsx
import { useEffect, useState } from "react";
import type { MarketFeed, UseMarketFeed } from "@beamline/lib-market";

export const useExchangeFeed: UseMarketFeed = ({ symbol, intervalSec }) => {
  const [feed, setFeed] = useState<MarketFeed>({ candles: [], bids: [], asks: [], trades: [], last: 0, prev: 0, change: 0 });
  useEffect(() => {
    const socket = new WebSocket(`wss://your-exchange.example/stream?symbol=${symbol}&interval=${intervalSec}`);
    socket.onmessage = (e) => setFeed((prev) => applyUpdate(prev, JSON.parse(e.data))); // your mapping
    return () => socket.close();
  }, [symbol, intervalSec]);
  return feed;
};
```

While you build, `useMockFeed` from `@beamline/demo-market-simulator` produces the same shape from a random walk, and
`usePaperAccount` from `@beamline/demo-paper-account` fills orders against it in the browser. Both are labelled as
simulated wherever they appear, so a demo is never mistaken for a market.

## Your orders

The account is your exchange's order API behind one object: the balance, positions, resting orders and fills you hold
in state, and three calls.

```tsx
import type { TradingAccount } from "@beamline/lib-market";

function useExchangeAccount(): TradingAccount {
  const { balance, positions, orders, fills } = useAccountStream(); // your private socket or polling
  return {
    balance, positions, orders, fills,
    submitOrder: async (o) => {
      const res = await api.post("/orders", o);
      return res.filled
        ? { status: "filled", price: res.avgPrice, size: res.filledSize }
        : { status: "resting", order: res.order };
    },
    cancelOrder: (id) => api.delete(`/orders/${id}`),
    closePosition: (symbol) =>
      api.post(`/positions/${symbol}/close`).then((r) => ({ status: "filled", price: r.avgPrice, size: r.size })),
  };
}
```

A call that fails rejects with an `Error`, and its message appears in the order ticket as it is. Nothing on screen
claims an order went through until your API says so.

## Or compose your own screen from the parts

Every part of the terminal works on its own, with the same data shapes:

| part | what it is for | it handles |
|---|---|---|
| [Order book](https://beamline.io/components/order-book) | showing liquidity and picking a price | cumulative depth bars, grouping, change flashes, keyboard price picking (`onPriceSelect`) |
| [Price chart](https://beamline.io/components/price-chart) | the main price view | candles, line or area, volume, OHLC legend, reference lines; canvas, so several updates a second stay cheap |
| [Order ticket](https://beamline.io/components/order-ticket) | placing an order | limit, market and stop, % of balance, inline validation, a live cost summary |
| [Depth chart](https://beamline.io/components/depth-chart) | how much size sits within a price range | stepped bid and ask areas around the mid, hover readout |
| [Positions table](https://beamline.io/components/positions-table) | open positions | live unrealised PnL, liquidation warnings, close actions |
| [Trades tape](https://beamline.io/components/trades-tape) | recent trades | newest first, side glyphs, large prints marked, one flash on arrival |

The rest of the set: [watchlist](https://beamline.io/components/watchlist), [price ticker](https://beamline.io/components/price-ticker),
[ticker tape](https://beamline.io/components/ticker-tape), [market heatmap](https://beamline.io/components/market-heatmap) and
[funding countdown](https://beamline.io/components/funding-countdown). Each part's page says when to use something else: the order book is
for picking a price, the depth chart for seeing size over a range; business metrics over time belong in a
[line chart](https://beamline.io/components/line-chart), not the price chart.

## What the parts do that is easy to get wrong

- **The keyboard trades too.** The book's rows are one tab stop; arrow keys, Home and End move, Enter or Space sends the
  price to the ticket. The ticket's side is a radio group and its type a tab list, each one tab stop.
- **Colour is never the only signal.** The book's last price carries ▲ or ▼ as well as green or red, and the trades
  tape marks each side with a glyph, so a colour-blind trader and a grey-scale screenshot read the same thing.
- **Flashes happen once.** A changed size flashes once and settles; nothing pulses at rest, so a quiet market looks
  quiet.
- **Stale data says so.** The book takes a `stale` flag that puts your chip in its header while the levels stay
  readable and pickable, instead of silently freezing.
- **Missing is not zero.** A market with no data draws as unmeasured (hatched) in the heatmap, never as a flat 0 %.

## Ask your agent for it

With Beamline connected ([setup for your agent](https://beamline.io/connect)), ask for the screen and name your data: "a BTC and ETH
perpetuals screen on our WebSocket feed at wss://…, orders through our REST API". The agent installs the terminal and
the parts it needs, writes the feed hook and the account around your API, and keeps the demo feed only where you ask
for one. The data shapes are documented in [Connect your data](https://beamline.io/docs/data-integration).
