demo D210
A trading book in React
Money units, shadow columns, a running P&L and a live feed, in one grid
createUnitType · shadow · running · statistics
This is a trading book in one grid: money units, shadow columns, a running profit and loss, and a live feed. It shows the statistics and type system working together on a realistic financial screen that keeps updating.
This is the React version. The grid mounts through the createLatticeGrid adapter, which takes its configuration as ordinary props and hands back the live grid through a ref. The grid below is the same one every other tab runs.
The configuration
<link rel="stylesheet" href="https://cdn.latticegrid.dev/lattice-grid@1.107.0/lattice-grid.min.css">
<div id="grid"></div>
<script type="module">
import React from 'https://esm.sh/react@18';
import { createRoot } from 'https://esm.sh/react-dom@18/client';
import { createGrid, registerUnitSystem, defineUnit, createUnitType } from 'https://cdn.latticegrid.dev/lattice-grid@1.107.0/lattice-grid.esm.min.js';
import createLatticeGrid from 'https://cdn.latticegrid.dev/lattice-grid@1.107.0/modules/react.esm.min.js';
const LatticeGrid = createLatticeGrid({ React, createGrid });
// Money as a unit family of its own: notional reads in auto k£ / M£, price and
// P&L to the penny.
registerUnitSystem('gbp', [
defineUnit('£', 1, ['gbp', 'pound', 'pounds']),
defineUnit('k£', 1e3, [], { prefix: 'k' }),
defineUnit('M£', 1e6, [], { prefix: 'M' }),
]);
const dataTypes = {
money: createUnitType({ system: 'gbp', unit: '£', placement: 'prefix', decimals: 2 }),
moneyAuto: createUnitType({ system: 'gbp', unit: '£', placement: 'prefix', display: 'auto' }),
};
// Shadow columns read another column and report on it: the tick since the last
// price, the rank by P&L and the change in that rank, and each row's share of
// the notional total. A running column carries the cumulative P&L down the book.
const columns = [
{ field: 'instrument', title: 'Instrument', layout: { pin: 'start', width: 120 } },
{ field: 'desk', title: 'Desk', filter: { type: 'set' }, layout: { width: 110 } },
{ field: 'notional', title: 'Notional', type: 'moneyAuto', total: 'sum', layout: { width: 130 } },
{ field: 'price', title: 'Price', type: 'money', layout: { width: 120 } },
{ field: 'spread', title: 'Spread', type: 'number', format: { decimals: 1, suffix: ' bp' }, total: 'median', layout: { width: 120 } },
{ field: 'pnl', title: 'P&L', type: 'money', total: 'sum', layout: { width: 130 } },
{ id: 'moved', title: 'Change', type: 'number', shadow: { of: 'price', kind: 'delta' }, format: { decimals: 4, signed: true }, layout: { width: 120 } },
{ id: 'rank', title: 'Rank', type: 'number', shadow: { of: 'pnl', kind: 'rank' }, layout: { width: 90 } },
{ id: 'climb', title: 'Moved', type: 'number', shadow: { of: 'pnl', kind: 'rankChange' }, format: { signed: true }, layout: { width: 110 } },
{ id: 'share', title: 'Share %', type: 'number', shadow: { of: 'notional', kind: 'shareOfTotal' }, format: { style: 'percent', decimals: 1 }, layout: { width: 110 } },
{ id: 'cum', title: 'Cum. P&L', type: 'money', running: { of: 'pnl', kind: 'total' }, layout: { width: 140 } },
];
const formatting = {
pnl: [
{ id: 'pnl-neg', when: { op: 'lt', value: 0 }, style: { color: '#a4262c' } },
{ id: 'pnl-top', when: { op: 'topPercent', value: 10 }, style: { background: '#e8f4ed' } },
],
spread: [{ id: 'spread-out', when: { op: 'outlier' }, style: { background: '#fbeceb' } }],
};
const rows = [/* instruments, each with a price, its open, a notional and spread */];
function App() {
const gridRef = React.useRef(null);
// A quote feed reaching the live grid through the ref, so Price, P&L and the
// shadow columns that read them move on the page. A mean-reverting walk keeps
// price near where it opened rather than drifting off in one direction.
React.useEffect(() => {
const grid = gridRef.current.grid;
const timer = setInterval(() => {
if (document.hidden) return;
const update = [];
for (let n = 0; n < 40; n++) {
const row = rows[Math.floor(Math.random() * rows.length)];
if (!row) continue;
const drift = (row.open - row.price) * 0.06;
const shock = row.price * (Math.random() - 0.5) * 0.01;
const next = Math.round((row.price + drift + shock) * 1e4) / 1e4;
row.price = next;
row.pnl = Math.round((next / row.open - 1) * row.notional);
update.push({ id: row.id, price: next, pnl: row.pnl });
}
grid.rows.queue({ update });
}, 120);
return () => clearInterval(timer);
}, []);
return (
<LatticeGrid
ref={gridRef}
rowKey="id"
highlightOnChange={{ duration: 500 }}
dataTypes={dataTypes}
toolPanel={{ side: 'left', panels: ['columns', 'filters', 'statistics'], exportName: 'book' }}
columns={columns}
formatting={formatting}
state={{ sort: [{ col: 'climb', dir: 'desc' }] }}
rows={rows}
style={{ height: '540px' }}
/>
);
}
createRoot(document.getElementById('grid')).render(<App />);
</script>