Most trading interfaces start with a single asset class. Spot crypto, or equities, or futures. The UI is designed around the assumptions of that one domain. Then the product expands, and suddenly you need to display BTC perpetuals alongside ETH spot alongside tokenised equities in a single portfolio view.
This is where things get complicated.
The data normalisation problem
Each asset class has its own data shape. A spot crypto position has a quantity and an average entry price. A futures position has a notional value, margin requirement, and funding rate. An options position has a strike, expiry, and greeks.
The naive approach is to build separate components for each asset class. This leads to duplicated code, inconsistent styling, and a fragmented user experience. The better approach is to normalise positions into a common model and handle the differences at the edges.
interface BasePosition { id: string; instrument: string; assetClass: "spot" | "futures" | "options"; side: "long" | "short"; quantity: number; entryPrice: number; currentPrice: number; unrealisedPnl: number; realisedPnl: number; } interface SpotPosition extends BasePosition { assetClass: "spot"; } interface FuturesPosition extends BasePosition { assetClass: "futures"; leverage: number; marginUsed: number; liquidationPrice: number; fundingRate: number; } interface OptionsPosition extends BasePosition { assetClass: "options"; strike: number; expiry: string; optionType: "call" | "put"; iv: number; delta: number; gamma: number; theta: number; vega: number; } type Position = SpotPosition | FuturesPosition | OptionsPosition;
The BasePosition fields are enough to render the portfolio summary. Asset-class-specific fields are only accessed in detail views or expanded rows.
The portfolio table
A unified portfolio table needs to handle columns that exist for some asset classes but not others. There are two workable approaches.
Approach 1: Common columns with expandable details
Show only the shared columns (instrument, side, quantity, P&L) in the main table. Asset-specific details appear in an expandable row or a side panel.
function PortfolioTable({ positions }: { positions: Position[] }) { return ( <table> <thead> <tr> <th>Instrument</th> <th>Type</th> <th>Side</th> <th>Quantity</th> <th>Entry</th> <th>Current</th> <th>Unrealised P&L</th> </tr> </thead> <tbody> {positions.map((pos) => ( <PortfolioRow key={pos.id} position={pos} /> ))} </tbody> </table> ); }
Approach 2: Dynamic columns based on active asset classes
Detect which asset classes are present in the portfolio and add columns accordingly. If the user holds futures, show a leverage column. If they hold options, show greeks.
function getVisibleColumns(positions: Position[]): Column[] { const assetClasses = new Set(positions.map((p) => p.assetClass)); const columns: Column[] = [...baseColumns]; if (assetClasses.has("futures")) { columns.push( { key: "leverage", label: "Leverage" }, { key: "liquidationPrice", label: "Liq. Price" }, ); } if (assetClasses.has("options")) { columns.push( { key: "strike", label: "Strike" }, { key: "expiry", label: "Expiry" }, { key: "delta", label: "Delta" }, ); } return columns; }
Approach 1 is simpler and works well when the portfolio is diverse. Approach 2 is better when users tend to focus on a single asset class at a time.
Aggregated P&L across asset classes
Displaying total P&L across asset classes requires careful handling of currency. Spot crypto P&L might be denominated in USDT. Futures P&L might be in USD. Options P&L might be in the underlying asset.
You need a consistent denomination layer:
function aggregatePortfolioPnl( positions: Position[], rates: Record<string, number>, targetCurrency: string ): { unrealised: number; realised: number } { let unrealised = 0; let realised = 0; for (const pos of positions) { const currency = getSettlementCurrency(pos); const rate = currency === targetCurrency ? 1 : rates[`${currency}/${targetCurrency}`]; unrealised += pos.unrealisedPnl * rate; realised += pos.realisedPnl * rate; } return { unrealised, realised }; }
Display the aggregated figure prominently, but always let users drill down into per-asset-class breakdowns. A trader who sees a combined $500 loss needs to quickly identify which position is responsible.
Instrument search and switching
When a platform supports hundreds of instruments across multiple asset classes, the instrument search becomes a critical UX element. Group results by asset class and show relevant metadata:
function InstrumentSearchResults({ results }: { results: Instrument[] }) { const grouped = groupBy(results, "assetClass"); return ( <div> {Object.entries(grouped).map(([assetClass, instruments]) => ( <div key={assetClass}> <h3 className="text-xs text-muted-foreground uppercase"> {assetClass} </h3> {instruments.map((inst) => ( <InstrumentRow key={inst.symbol} instrument={inst} /> ))} </div> ))} </div> ); }
Include the settlement currency, contract size, and expiry (for derivatives) in the search results so traders can distinguish between similar instruments without clicking into each one.
Order forms that adapt
The order form changes significantly between asset classes. A spot order needs price and quantity. A futures order adds leverage and margin mode. An options order needs strike selection and expiry.
Rather than building three separate forms, use a single form shell that swaps in the relevant fields:
function OrderForm({ instrument }: { instrument: Instrument }) { return ( <form> <SideSelector /> <OrderTypeSelector /> <PriceInput instrument={instrument} /> <QuantityInput instrument={instrument} /> {instrument.assetClass === "futures" && ( <> <LeverageSlider max={instrument.maxLeverage} /> <MarginModeToggle /> </> )} {instrument.assetClass === "options" && ( <> <StrikeSelector chain={instrument.optionChain} /> <ExpirySelector expiries={instrument.expiries} /> </> )} <OrderSummary instrument={instrument} /> <SubmitButton /> </form> ); }
The form summary should always show the maximum loss or margin impact before submission, regardless of asset class. This is where traders make expensive mistakes.
Risk overview
A multi-asset portfolio needs a risk dashboard that goes beyond P&L. Show:
- Total margin usage across all futures and options positions
- Largest single position as a percentage of total portfolio value
- Correlation exposure - if 80% of the portfolio is long crypto, that is important context
- Liquidation proximity for leveraged positions
This does not need to be a separate page. A collapsible panel at the top of the portfolio view gives traders a quick health check without navigating away from their positions.
Performance considerations
Multi-asset portfolios create more data to manage. Each asset class may have its own WebSocket feed, its own update frequency, and its own data shape. Keep the state layer organised by asset class to avoid unnecessary re-renders:
interface PortfolioState { spot: Record<string, SpotPosition>; futures: Record<string, FuturesPosition>; options: Record<string, OptionsPosition>; }
A futures price update should not trigger a re-render of spot positions. Use selectors or slices to isolate updates to the components that need them.
The takeaway
Multi-asset portfolio support is not just a matter of showing different data in the same table. It touches the data model, the order form, the risk display, and the state architecture. Getting it right means designing a normalisation layer early and letting asset-class-specific details surface only where they add value. Getting it wrong means building three separate UIs and hoping they feel like one.