Finance scenario
Capital markets workflow showing middle-office blotter corrections — validation, bulk update, and audit trail
Your Role
You support the trading desk after imports and allocations land in the blotter.
import { ActionColumnContext, AdaptableOptions, CellEditableContext, CellUpdateRequest, ContextMenuContext, CustomContextMenuContext, GridCell, UserContextMenuItem, } from '@adaptabletools/adaptable'; import {TradeBlotterRow, TradeStatus} from './rowData'; const CORRECTABLE_TRADE_STATUSES: TradeStatus[] = ['Pending', 'Amended']; function isCorrectableTrade( row: TradeBlotterRow | undefined ): row is TradeBlotterRow { return !!row && CORRECTABLE_TRADE_STATUSES.includes(row.status); } function bookTrade(context: { adaptableApi: ContextMenuContext['adaptableApi']; rowNode: ContextMenuContext['gridCell']['rowNode']; }) { const row = context.rowNode?.data as TradeBlotterRow | undefined; if (!isCorrectableTrade(row)) { return; } const cellUpdateRequest: CellUpdateRequest = { columnId: 'status', newValue: 'Booked', primaryKeyValue: row.id, rowNode: context.rowNode, }; context.adaptableApi.gridApi.setCellValue(cellUpdateRequest); } export const adaptableOptions: AdaptableOptions<TradeBlotterRow> = { primaryKey: 'id', adaptableId: 'Showcase: Trade Blotter', userName: 'Middle Office', editOptions: { isCellEditable: (cellEditableContext: CellEditableContext) => { const gridCell: GridCell = cellEditableContext.gridCell; if (!gridCell?.column || !gridCell.rowNode) { return false; } const row = gridCell.rowNode.data as TradeBlotterRow | undefined; const amendableColumnIds = ['price', 'quantity', 'side']; if (amendableColumnIds.includes(gridCell.column.columnId)) { return ( isCorrectableTrade(row) && cellEditableContext.defaultColDefEditableValue ); } return cellEditableContext.defaultColDefEditableValue; }, }, userInterfaceOptions: { editableCellStyle: { BackColor: 'color-mix(in srgb, var(--ab-color-accent) 10%, transparent)', }, }, dataChangeHistoryOptions: { activeByDefault: true, }, contextMenuOptions: { customContextMenu: (menuContext: CustomContextMenuContext) => { const row = menuContext.gridCell?.rowNode?.data as | TradeBlotterRow | undefined; const canBook = isCorrectableTrade(row) && !menuContext.isRowGroupColumn; const bookTradeMenuItem: UserContextMenuItem = { menuType: 'User', label: 'Book trade', icon: {name: 'check'}, hidden: !canBook, onClick: (context: ContextMenuContext) => { bookTrade({ adaptableApi: context.adaptableApi, rowNode: context.gridCell.rowNode, }); }, }; return [ bookTradeMenuItem, '-', ...menuContext.defaultAgGridMenuStructure, '-', ...menuContext.defaultAdaptableMenuStructure, ]; }, }, actionColumnOptions: { actionColumns: [ { columnId: 'book_trade', friendlyName: 'Book', actionColumnButton: [ { label: 'Book', buttonStyle: {variant: 'outlined', tone: 'success'}, disabled: (_button, context: ActionColumnContext) => !isCorrectableTrade(context.data), onClick: (_button: unknown, context: ActionColumnContext) => { bookTrade({ adaptableApi: context.adaptableApi, rowNode: context.rowNode, }); }, }, ], }, ], }, initialState: { Dashboard: { Tabs: [ { Name: 'Blotter', Toolbars: [ 'Layout', 'GridFilter', 'BulkUpdate', 'DataChangeHistory', 'Alert', ], }, ], ModuleButtons: [ 'Layout', 'BulkUpdate', 'PlusMinus', 'DataChangeHistory', 'Alert', 'CalculatedColumn', 'FreeTextColumn', 'SettingsPanel', ], }, StatusBar: { StatusBars: [ { Key: 'Center Panel', StatusBarPanels: ['BulkUpdate', 'DataChangeHistory'], }, ], }, Theme: {CurrentTheme: 'dark'}, PlusMinus: { PlusMinusNudges: [ { Name: 'Price tick (correctable trades)', Scope: {ColumnIds: ['price']}, NudgeValue: 1, Rule: { BooleanExpression: 'QUERY("Needs Correction")', }, }, { Name: 'Quantity step (correctable trades)', Scope: {ColumnIds: ['quantity']}, NudgeValue: 100, Rule: { BooleanExpression: 'QUERY("Needs Correction")', }, }, ], }, NamedQuery: { NamedQueries: [ { Name: 'Needs Correction', BooleanExpression: '[status] = "Pending" OR [status] = "Amended"', }, { Name: 'Booked Trades', BooleanExpression: '[status] = "Booked"', }, ], }, Layout: { CurrentLayout: 'Blotter', Layouts: [ { Name: 'Blotter', AutoSizeColumns: true, TableColumns: [ 'tradeId', 'symbol', 'side', 'quantity', 'price', 'notional', 'status', 'desk', 'trader', 'tradeDate', 'amendNote', 'book_trade', ], }, { Name: 'Exceptions', AutoSizeColumns: true, TableColumns: [ 'tradeId', 'symbol', 'side', 'quantity', 'price', 'notional', 'status', 'desk', 'amendNote', 'book_trade', ], GridFilter: { Expression: 'QUERY("Needs Correction")', }, }, { Name: 'Equities Desk', AutoSizeColumns: true, TableColumns: [ 'tradeId', 'symbol', 'side', 'quantity', 'price', 'notional', 'status', 'trader', 'tradeDate', 'amendNote', 'book_trade', ], GridFilter: { Expression: '[desk] = "Equities"', }, }, ], }, CalculatedColumn: { CalculatedColumns: [ { FriendlyName: 'Notional', ColumnId: 'notional', Query: { ScalarExpression: '[quantity] * [price]', }, CalculatedColumnSettings: { DataType: 'number', Filterable: true, }, }, ], }, FreeTextColumn: { FreeTextColumns: [ { ColumnId: 'amendNote', FriendlyName: 'Amend Note', FreeTextStoredValues: [ {PrimaryKey: 5, FreeText: 'Missing price from feed'}, {PrimaryKey: 12, FreeText: 'Client recall — check qty'}, {PrimaryKey: 19, FreeText: 'Bad sign on import'}, {PrimaryKey: 32, FreeText: 'Zero qty placeholder'}, {PrimaryKey: 43, FreeText: 'Awaiting confirms'}, ], FreeTextColumnSettings: { DataType: 'text', Resizable: true, Filterable: true, }, }, ], }, FormatColumn: { FormatColumns: [ { Name: 'format-status-pending', Scope: {ColumnIds: ['status']}, Rule: {BooleanExpression: '[status] = "Pending"'}, Style: { BackColor: 'var(--ab-color-palette-3)', ForeColor: 'var(--ab-color-palette-4)', FontWeight: 'Bold', }, }, { Name: 'format-status-amended', Scope: {ColumnIds: ['status']}, Rule: {BooleanExpression: '[status] = "Amended"'}, Style: { BackColor: 'var(--ab-color-palette-7)', ForeColor: 'var(--ab-color-palette-8)', }, }, { Name: 'format-status-booked', Scope: {ColumnIds: ['status']}, Rule: {BooleanExpression: '[status] = "Booked"'}, Style: { ForeColor: 'var(--ab-color-success)', }, }, { Name: 'format-invalid-price', Scope: {ColumnIds: ['price']}, Rule: {BooleanExpression: '[price] <= 0'}, Style: { BackColor: 'var(--ab-color-palette-1)', ForeColor: 'var(--ab-color-palette-2)', FontWeight: 'Bold', }, }, { Name: 'format-money', Scope: {ColumnIds: ['price', 'notional']}, DisplayFormat: 'Dollar', }, { Name: 'format-qty', Scope: {ColumnIds: ['quantity']}, DisplayFormat: 'Integer', }, { Name: 'format-trade-date', Scope: {ColumnIds: ['tradeDate']}, DisplayFormat: { Formatter: 'DateFormatter', Options: {Pattern: 'dd MMM yyyy'}, }, }, ], }, Alert: { AlertDefinitions: [ { Name: 'Invalid price', MessageType: 'Error', MessageText: 'Price must be greater than zero.', Scope: {ColumnIds: ['price']}, Rule: { BooleanExpression: '[price] <= 0', }, AlertProperties: { PreventEdit: true, DisplayNotification: true, }, }, { Name: 'Invalid quantity', MessageType: 'Error', MessageText: 'Quantity must be greater than zero.', Scope: {ColumnIds: ['quantity']}, Rule: { BooleanExpression: '[quantity] <= 0', }, AlertProperties: { PreventEdit: true, DisplayNotification: true, }, }, { Name: 'Booked trade locked', MessageType: 'Warning', MessageText: 'Booked and cancelled trades cannot be amended.', Scope: {ColumnIds: ['price', 'quantity', 'side']}, Rule: { BooleanExpression: '[status] = "Booked" OR [status] = "Cancelled"', }, AlertProperties: { PreventEdit: true, DisplayNotification: true, }, }, ], }, }, };