Skip to main content

Live Grid

A widget that displays real-time data updates in a table format using WebSocket connections. The live grid widget can be configured to only update certain cells when their values change or all of the cells.

Live Grid Example
import asyncio
from datetime import datetime
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
from fastapi.websockets import WebSocketState
import numpy as np

app = FastAPI()

# Sample data store
WS_DATA = {
"AAPL": {
"price": 150.0,
"prev_close": 145.54,
"volume": 1000000,
"change": 4.46,
"change_percent": 0.03,
},
"GOOGL": {
"price": 140.0,
"prev_close": 138.20,
"volume": 800000,
"change": 1.80,
"change_percent": 0.013,
},
"MSFT": {
"price": 350.0,
"prev_close": 345.00,
"volume": 1200000,
"change": 5.00,
"change_percent": 0.014,
},
"AMZN": {
"price": 178.0,
"prev_close": 175.50,
"volume": 900000,
"change": 2.50,
"change_percent": 0.014,
},
"TSLA": {
"price": 245.0,
"prev_close": 240.00,
"volume": 1500000,
"change": 5.00,
"change_percent": 0.021,
},
}


def get_ws_data(symbol: str):
"""Generate real-time data for a symbol"""
data = WS_DATA.get(symbol, {"price": 100.0, "prev_close": 100.0, "volume": 1000000})

price = data["price"] + np.random.uniform(-10, 10)
volume = data["volume"] + np.random.randint(100, 1000)
change = price - data["prev_close"]
change_percent = change / data["prev_close"]

WS_DATA[symbol].update(dict(price=price, volume=volume))

return {
"symbol": symbol,
"price": price,
"change": change,
"change_percent": change_percent,
"volume": volume,
}


@register_widget({
"name": "Live Grid",
"description": "Live Grid with real-time WebSocket updates",
"type": "live_grid",
"endpoint": "live_grid_data",
"wsEndpoint": "live_grid_ws",
"gridData": {"w": 20, "h": 9},
"data": {
"wsRowIdColumn": "symbol",
"table": {
"showAll": True,
"columnsDefs": [
{
"field": "symbol",
"headerName": "Symbol"
},
{
"field": "price",
"headerName": "Price",
"renderFn": "showCellChange",
"renderFnParams": {
"colorValueKey": "change"
}
},
{
"field": "change_percent",
"headerName": "Change %",
"renderFn": "greenRed"
},
{
"field": "volume",
"enableCellChangeWs": False,
"headerName": "Volume"
}
]
}
},
"params": [
{
"paramName": "symbol",
"description": "The symbol to get details for",
"value": "TSLA",
"label": "Symbol",
"type": "text",
"multiSelect": True,
"options": [
{"label": "AAPL", "value": "AAPL"},
{"label": "GOOGL", "value": "GOOGL"},
{"label": "MSFT", "value": "MSFT"},
{"label": "AMZN", "value": "AMZN"},
{"label": "TSLA", "value": "TSLA"}
]
}
]
})
@app.get("/live_grid_data")
def get_live_grid_data(symbol: str):
"""Initial data endpoint for live grid"""
symbols = symbol.split(",")
return [
{
"date": str(datetime.now().date()),
**get_ws_data(sym),
"market_cap": np.random.randint(1000000000, 2000000000),
}
for sym in symbols
]


@app.websocket("/live_grid_ws")
async def websocket_endpoint(websocket: WebSocket):
"""WebSocket endpoint for live updates"""
await websocket.accept()
try:
await websocket_handler(websocket)
except WebSocketDisconnect:
return
except Exception as e:
await websocket.close(code=1011)
raise HTTPException(status_code=500, detail=str(e))


async def websocket_handler(websocket: WebSocket):
"""Handle WebSocket connections for live grid updates"""
subbed_symbols: set[str] = set()

async def consumer_handler(ws: WebSocket):
try:
async for data in ws.iter_json():
if symbols := data.get("params", {}).get("symbol"):
if isinstance(symbols, str):
symbols = symbols.split(",")

subbed_symbols.clear()
subbed_symbols.update(set(symbols))

except WebSocketDisconnect:
pass
except RuntimeError:
await ws.close()

async def producer_handler(ws: WebSocket):
try:
while websocket.client_state != WebSocketState.DISCONNECTED:
current_symbols = list(subbed_symbols)
np.random.shuffle(current_symbols)

for symbol in current_symbols:
await ws.send_json(get_ws_data(symbol))
await asyncio.sleep(np.random.uniform(0.5, 0.8))

await asyncio.sleep(np.random.uniform(0.1, 0.3))

except WebSocketDisconnect:
pass
except RuntimeError:
await ws.close()

consumer_task = asyncio.create_task(consumer_handler(websocket))
producer_task = asyncio.create_task(producer_handler(websocket))

done, pending = await asyncio.wait(
[consumer_task, producer_task], return_when=asyncio.FIRST_COMPLETED
)

for task in pending:
task.cancel()

Key Configuration Options

  • wsEndpoint: The WebSocket endpoint for receiving live updates
  • wsRowIdColumn: The column used to identify rows for updating (e.g., "symbol")
  • enableCellChangeWs: Boolean to control whether a cell updates via WebSocket. By default true for fields sent in the WebSocket. Set to false to prevent updates for specific columns
  • renderFn: The function used to render the cell. See Render Functions for more options

How It Works

  1. Initial Data: The GET endpoint (/live_grid_data) provides the initial table data
  2. WebSocket Connection: The WebSocket endpoint (/live_grid_ws) handles real-time updates
  3. Row Identification: The wsRowIdColumn links WebSocket updates to specific table rows
  4. Cell Updates: Only cells that receive new data via WebSocket are updated, with optional visual change indicators

Additional Resources

You can find more examples of how to set up your own backend in the Backend for OpenBB Workspace GitHub.