Skip to content

Widgets

Overview

Widgets are the building blocks of the trading terminal. Each dashboard contains a collection of widgets that can be freely positioned, resized, minimized, and grouped.

Widget Types

Widgets are resolved at runtime through the WidgetRegistry (src/modules/registry.ts), keyed by a type string. Built-ins are registered in src/modules/builtinWidgets.tsx; modules register theirs at load time. The type is a free-form string (WidgetSchema.type = z.string().min(1)); the former enum is kept as BUILTIN_WIDGET_TYPES for reference.

Every data widget renders live exchange data, never mock or placeholder fixtures. Public market data (chart, order book, trades) streams through the data-provider layer; private widgets (balances, trading data, deals, transaction history, portfolio, order form) resolve per account via the central-accounts { accountId } flow (want: 'read' for reads, 'trade' for orders) — the server attaches the vault keys before calling CCXT, so the browser never holds secrets. Each private widget gates on acc => !!acc.id (any listed account, own or shared-with-read) rather than client-side key presence.

category controls where a widget appears: public → Public Data menu, private → Private Data menu, diagnostics → Diagnostics submenu, module widgets → Modules. system widgets are registered (renderable from saved dashboards) but not offered in the add-widget menu.

Built-in types:

TypeComponentCategoryDescription
chartChart.tsxpublicLive OHLCV candlestick chart (Night Vision)
orderbookOrderBookWidget.tsxpublicLive order book (bid/ask depth, spread)
tradesTradesWidget.tsxpublicLive trade feed (filtering, aggregated mode)
userBalancesUserBalancesWidget.tsxprivateReal balances across all accounts, table or pie view (Recharts), USD-valued
userTradingDataUserTradingDataWidget.tsxprivateTabs: trades, positions, open orders (real, per account)
dealsDealsWidget.tsxprivateDeal tracking aggregated from real trade history
orderFormOrderForm.tsxprivatePlace buy/sell orders — live ticker + real balance
dataProviderSettingsDataProviderSettingsWidget.tsxdiagnosticsConfigure data providers
dataProviderSetupDataProviderSetupWidget.tsxdiagnosticsInitial provider setup wizard
portfolioPortfolio.tsxsystemCross-account allocation overview (USD-valued)
transactionHistoryTransactionHistory.tsxsystemReal exchange ledger, grouped by date
customPortfolio.tsxsystemLegacy alias, rendered as Portfolio
system.moduleStoreModuleStoreWidgetsystemBrowse / install / enable modules
system.docsDocsWidgetsystemIn-app API / CLI / MCP reference

Dev-only diagnostic widgets (registered only when import.meta.env.DEV) expose raw provider/exchange internals: exchanges, markets, pairs, and dataProviderDebug.

Trading Widgets in Detail

All of the widgets below render live exchange data. Reads route through the data-provider store; private reads (balances, trades, orders, positions, ledger) go via the central-accounts { accountId } flow.

  • Chart / Order Book / Trades (Chart.tsx, OrderBookWidget.tsx, TradesWidget.tsx) — public market data. Each carries a group selector and uses a transparent group, so picking exchange / market / pair flows to linked widgets. Streams over WebSocket (CCXT Pro) with REST fallback.

  • User Balances (UserBalancesWidget.tsx) — real balances across every account, both trading and funding wallets, fetched with initializeBalanceData(accountId, walletType). Filter / sort, hide-small-amounts, USD valuation (stablecoins 1:1, otherwise CURRENCY/USDT ticker bid), a total portfolio line, and a table-or-pie view (UserBalancesPieChart, Recharts). Header refresh button clears then re-fetches.

  • User Trading Data (UserTradingDataWidget.tsx) — three tabs backed by real per-account calls: Trades (UserTradesTabfetchMyTrades), Positions (UserPositionsTabfetchPositions), Orders (UserOrdersTabfetchOpenOrders). An account selector (all / one) drives every tab; the header refresh button re-fetches the active tab via an imperative handle. Long lists are virtualized.

  • Deals (DealsWidget.tsx) — deal tracking built from real trade history. “Sync from account” pulls fetchMyTrades for each account and aggregates trades into deals (grouped by symbol) in the persisted dealsStore; the per-deal “Add Trades” picker (MyTradesWidget, also fetchMyTrades-backed) is the other live entry point. P&L is computed in deal details, not faked at the source.

  • Order Form (OrderForm.tsx) — places real orders. Reads the instrument from the selected group (account / exchange / market / pair), subscribes to the live ticker for the last price, and loads the real account balance to drive available / holdings / max-amount and cost estimation. Market / limit / stop order types, optional stop-loss & take-profit, persisted defaults (order type, TIF, post/reduce-only), and an optional confirm-before-submit gate. Submit calls placeOrder (trade write via the accountId flow).

  • Transaction History (TransactionHistory.tsx) — real account ledger via fetchLedger(accountId) (deposits / withdrawals / transfers / trades / fees), merged across accounts, grouped by date (Today / Yesterday / full date), with in/out direction coloring and a client-side search. Empty when the account has no ledger movements.

  • Portfolio (Portfolio.tsx) — cross-account allocation. Loads every account’s balances, values them in USD (stablecoin 1:1, else CURRENCY/USDT ticker bid), and shows total value, Assets / Accounts KPIs, an allocation donut (one slice per asset, color-matched to the per-asset table below it), and a per-asset breakdown with allocation bars. No cost basis is available from balances, so P&L columns are intentionally omitted rather than faked.

Widget Container: WidgetSimple

Every widget is wrapped in WidgetSimple (src/components/WidgetSimple.tsx), which provides:

  • Drag — click and drag the title bar
  • Resize — 8 resize handles (edges + corners)
  • Minimize — collapses to a small bar at the bottom
  • Maximize — fills the viewport, restores on second click
  • Title editing — double-click the title to rename
  • Close — removes the widget from the dashboard
  • Settings — gear icon, opens widget-specific settings panel
  • Group selector — colored circle showing the widget’s group
  • Z-index — click brings widget to front

Props

interface WidgetSimpleProps {
id: string;
title: string;
defaultTitle: string;
userTitle?: string;
children: ReactNode; // The actual widget content
position: { x: number; y: number };
size: { width: number; height: number };
zIndex: number;
isActive: boolean;
groupId?: string;
widgetType: string;
showGroupSelector?: boolean;
headerActions?: ReactNode; // Extra buttons in the title bar
onRemove: () => void;
}

Widget Grouping

Widgets can be assigned to groups via the groupStore. A group shares context (exchange, market, trading pair, account) across all its widgets. This means selecting “BTC/USDT on Binance” in one group member automatically updates all others.

Groups are identified by a color indicator on each widget’s title bar.

How to Create a New Widget

For community or full-stack widgets, build a module instead of editing the core — see Modules. The steps below are for built-in widgets that ship with the terminal.

1. Create the component

Create src/components/widgets/MyNewWidget.tsx:

import React from 'react';
const MyNewWidget: React.FC = () => {
return (
<div className="p-4 h-full overflow-auto">
<h3 className="text-sm font-medium text-terminal-text">My Widget</h3>
{/* Widget content */}
</div>
);
};
export default MyNewWidget;

2. Register it in the WidgetRegistry

In src/modules/builtinWidgets.tsx, import your component and add a WidgetDefinition to BUILTIN_DEFINITIONS:

import MyNewWidget from '@/components/widgets/MyNewWidget';
// inside BUILTIN_DEFINITIONS:
{
type: 'myNewWidget',
title: 'My Widget',
icon: 'PieChart', // a lucide name in resolveIcon's map
category: 'public', // 'public' | 'private' | 'diagnostics' | 'system'
defaultSize: { width: 500, height: 400 },
Component: adaptComponent(MyNewWidget), // adapter maps WidgetProps -> {widgetId, selectedGroupId}
// Settings: adaptSettings(MyNewWidgetSettings), // optional gear panel
},

That’s the only wiring needed. category decides the add-widget menu section (public → Public Data, private → Private Data, diagnostics → Diagnostics submenu, module widgets → Modules; system widgets are registered but not in the menu). TradingTerminal, WidgetMenu, WidgetSettingsManager and WidgetSimple all read the registry — there is no separate component map, enum, or menu list to update.

If your widget needs a new icon, add it to the curated map in src/modules/resolveIcon.tsx (don’t import lucide’s full icons barrel — it bloats the bundle).

3. Use market data (optional)

If your widget needs market data, use the data provider store:

import { useDataProviderStore } from '@/store/dataProviderStore';
const MyNewWidget: React.FC = () => {
const subscribe = useDataProviderStore(s => s.subscribe);
const unsubscribe = useDataProviderStore(s => s.unsubscribe);
const getTrades = useDataProviderStore(s => s.getTrades);
useEffect(() => {
const widgetId = 'my-widget-123';
subscribe(widgetId, 'binance', 'BTC/USDT', 'trades', undefined, 'spot');
return () => {
unsubscribe(widgetId, 'binance', 'BTC/USDT', 'trades', undefined, 'spot');
};
}, []);
const trades = getTrades('binance', 'BTC/USDT', 'spot');
// render trades...
};

Widget-Specific Stores

Some complex widgets have their own dedicated Zustand stores:

StoreFilePurpose
chartWidgetStorestore/chartWidgetStore.tsChart settings per widget instance
orderBookWidgetStorestore/orderBookWidgetStore.tsOrder book display settings
tradesWidgetStorestore/tradesWidgetStore.tsTrades feed settings
userBalancesWidgetStorestore/userBalancesWidgetStore.tsBalance display preferences
userTradingDataWidgetStorestore/userTradingDataWidgetStore.tsTrading data tab state
placeOrderStorestore/placeOrderStore.tsOrder form live state (form data, validation, estimate)
orderFormWidgetStorestore/orderFormWidgetStore.tsPersisted Order Form defaults (order type, TIF, post/reduce-only, confirm)
dealsStorestore/dealsStore.tsPersisted deals, aggregated from real trades

Performance Notes

  • Widgets with large data sets (trades, orderbook) use TanStack Virtual for virtualized scrolling
  • Pre-calculate row heights for the virtualizer to avoid layout thrashing
  • Use Zustand selectors (useStore(s => s.field)) to avoid re-rendering on unrelated state changes
  • Subscription deduplication ensures multiple widgets watching the same stream share a single connection