# Trade Blotter & Corrections

Canonical page: https://www.adaptabletools.com/docs/showcase-demos-trade-blotter

**Example: Showcase: Trade Blotter & Corrections**

Demo project (Vanilla); the files without a marker are shown below:

```text
src/
  adaptableOptions.ts
  agGridModules.ts     [template]
  columnDefs.ts
  gridOptions.ts
  index.ts             [template]
  onAdaptableReady.ts  [template]
  rowData.ts
  styles.css           [template]
index.html             [template]
package.json           [template]
tsconfig.json          [template]
```

Files marked `[template]` are the same in every demo; see the [demo template](https://www.adaptabletools.com/llms/demo-template.md#vanilla).

**`src/adaptableOptions.ts`**

```ts
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',
        controls: [
          {
            type: 'button',
            label: 'Book',
            style: {variant: 'outlined', tone: 'success'},
            disabled: (context: ActionColumnContext) =>
              !isCorrectableTrade(context.data),
            onClick: (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',
      ],
    },
    StatusBar: {
      StatusBars: [
        {
          Key: 'Center Panel',
          StatusBarPanels: ['BulkUpdate', 'DataChangeHistory'],
        },
      ],
    },
    Theme: {CurrentTheme: 'dark'},
    PlusMinus: {
      PlusMinusNudges: [
        {
          Name: 'Price tick (correctable trades)',
          Scope: {ColumnIds: ['price']},
          NudgeValue: 1,
          Rule: 'QUERY("Needs Correction")',
        },
        {
          Name: 'Quantity step (correctable trades)',
          Scope: {ColumnIds: ['quantity']},
          NudgeValue: 100,
          Rule: 'QUERY("Needs Correction")',
        },
      ],
    },
    NamedQuery: {
      NamedQueries: [
        {
          Name: 'Needs Correction',
          Expression:
            '[status] = "Pending" OR [status] = "Amended"',
        },
        {
          Name: 'Booked Trades',
          Expression: '[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',
          Expression: '[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: '[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: '[status] = "Amended"',
          Style: {
            BackColor: 'var(--ab-color-palette-7)',
            ForeColor: 'var(--ab-color-palette-8)',
          },
        },
        {
          Name: 'format-status-booked',
          Scope: {ColumnIds: ['status']},
          Rule: '[status] = "Booked"',
          Style: {
            ForeColor: 'var(--ab-color-success)',
          },
        },
        {
          Name: 'format-invalid-price',
          Scope: {ColumnIds: ['price']},
          Rule: '[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: '[price] <= 0',
          AlertProperties: {
            PreventEdit: true,
            DisplayNotification: true,
          },
        },
        {
          Name: 'Invalid quantity',
          MessageType: 'Error',
          MessageText: 'Quantity must be greater than zero.',
          Scope: {ColumnIds: ['quantity']},
          Rule: '[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: '[status] = "Booked" OR [status] = "Cancelled"',
          AlertProperties: {
            PreventEdit: true,
            DisplayNotification: true,
          },
        },
      ],
    },
  },
};
```

**`src/rowData.ts`**

```ts
export type TradeSide = 'Buy' | 'Sell';
export type TradeStatus = 'Pending' | 'Booked' | 'Amended' | 'Cancelled';
export type TradeDesk = 'Equities' | 'Fixed Income' | 'EM';

export interface TradeBlotterRow {
  id: number;
  tradeId: string;
  symbol: string;
  side: TradeSide;
  quantity: number;
  price: number;
  status: TradeStatus;
  desk: TradeDesk;
  trader: string;
  tradeDate: string;
}

const SYMBOLS = [
  'AAPL',
  'MSFT',
  'NVDA',
  'JPM',
  'XOM',
  'BP',
  'SAP',
  'HSBA',
  'BABA',
  'TSM',
] as const;

const TRADERS = ['Chen', 'Patel', 'Okafor', 'Smith', 'Garcia'] as const;
const DESKS: TradeDesk[] = ['Equities', 'Fixed Income', 'EM'];
const STATUSES: TradeStatus[] = ['Pending', 'Booked', 'Amended', 'Cancelled'];
const SIDES: TradeSide[] = ['Buy', 'Sell'];

function pseudoRandom(seed: number): number {
  const x = Math.sin(seed * 4242) * 10000;
  return x - Math.floor(x);
}

function toDateString(daysAgo: number): string {
  const d = new Date();
  d.setDate(d.getDate() - daysAgo);
  return d.toDateString();
}

const generated: TradeBlotterRow[] = [];

for (let i = 1; i <= 72; i++) {
  const desk = DESKS[Math.floor(pseudoRandom(i * 3) * DESKS.length)];
  const status = STATUSES[Math.floor(pseudoRandom(i * 7) * STATUSES.length)];
  const price =
    Math.round((40 + pseudoRandom(i * 11) * 460) * 100) / 100;
  generated.push({
    id: i,
    tradeId: `T-${(240000 + i).toString()}`,
    symbol: SYMBOLS[Math.floor(pseudoRandom(i * 5) * SYMBOLS.length)],
    side: SIDES[Math.floor(pseudoRandom(i * 9) * 2)],
    quantity: Math.round((100 + pseudoRandom(i * 13) * 4900) / 100) * 100,
    price,
    status,
    desk,
    trader: TRADERS[Math.floor(pseudoRandom(i * 17) * TRADERS.length)],
    tradeDate: toDateString(Math.floor(pseudoRandom(i * 19) * 5)),
  });
}

// Narrative rows for the corrections workflow.
generated[4] = {
  ...generated[4],
  status: 'Pending',
  price: 0,
};
generated[11] = {
  ...generated[11],
  status: 'Amended',
  price: 142.5,
  quantity: 1200,
};
generated[18] = {
  ...generated[18],
  status: 'Pending',
  price: -12.4,
};
generated[25] = {
  ...generated[25],
  status: 'Booked',
  price: 198.2,
  quantity: 5000,
};
generated[31] = {
  ...generated[31],
  status: 'Pending',
  quantity: 0,
  price: 88.1,
};
generated[42] = {
  ...generated[42],
  status: 'Amended',
  desk: 'Equities',
};

export const rowData: TradeBlotterRow[] = generated;
```

**`src/columnDefs.ts`**

```ts
import {ColDef} from 'ag-grid-enterprise';

export const columnDefs: ColDef[] = [
  {field: 'tradeId', headerName: 'Trade ID', cellDataType: 'text', width: 110},
  {field: 'symbol', cellDataType: 'text', width: 90},
  {field: 'side', cellDataType: 'text', width: 80},
  {
    field: 'quantity',
    headerName: 'Qty',
    cellDataType: 'number',
    type: 'rightAligned',
    editable: true,
  },
  {
    field: 'price',
    cellDataType: 'number',
    type: 'rightAligned',
    editable: true,
  },
  {field: 'status', cellDataType: 'text', width: 100},
  {field: 'desk', cellDataType: 'text', enableRowGroup: true},
  {field: 'trader', cellDataType: 'text'},
  {
    field: 'tradeDate',
    headerName: 'Trade Date',
    cellDataType: 'date',
    editable: false,
  },
  {field: 'id', hide: true, editable: false},
];
```

**`src/gridOptions.ts`**

```ts
import {GridOptions, themeBalham} from 'ag-grid-enterprise';

import {columnDefs} from './columnDefs';
import {rowData as tradeBlotterRowData, TradeBlotterRow} from './rowData';

export const gridOptions: GridOptions<TradeBlotterRow> = {
  theme: themeBalham,
  defaultColDef: {
    resizable: true,
    sortable: true,
    editable: false,
    filter: true,
    floatingFilter: true,
  },
  columnDefs: columnDefs,
  rowData: tradeBlotterRowData,
  sideBar: true,
  suppressMenuHide: true,
  columnMenu: 'new',
  cellSelection: true,
  statusBar: {
    statusPanels: [
      {statusPanel: 'agTotalRowCountComponent', align: 'left'},
      {statusPanel: 'agFilteredRowCountComponent'},
      {
        key: 'Center Panel',
        statusPanel: 'AdaptableStatusPanel',
        align: 'center',
      },
    ],
  },
};
```
