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,
},
},
],
},
},
};Read-onlyLoading demo…