# Portfolio Risk Desk

Canonical page: https://www.adaptabletools.com/docs/showcase-demos-portfolio-risk

**Example: Showcase: Portfolio Risk Desk**

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 {AdaptableOptions} from '@adaptabletools/adaptable';
import {PortfolioPosition} from './rowData';

export const adaptableOptions: AdaptableOptions<PortfolioPosition> = {
  primaryKey: 'id',
  adaptableId: 'Showcase: Portfolio Risk Desk',
  exportOptions: {
    appendFileTimestamp: true,
    exportDateFormat: 'yyyy-MM-dd',
    exportDataFormat: 'formattedValue',
  },
  initialState: {
    Dashboard: {
      Tabs: [
        {
          Name: 'Desk',
          Toolbars: ['Layout', 'Export', 'GridFilter'],
        },
      ],
      ModuleButtons: [
        'Layout',
        'Export',
        'CalculatedColumn',
        'StyledColumn',
      ],
    },
    StatusBar: {
      StatusBars: [
        {
          Key: 'Center Panel',
          StatusBarPanels: ['Layout', 'Export'],
        },
      ],
    },
    Theme: {CurrentTheme: 'dark'},
    NamedQuery: {
      NamedQueries: [
        {
          Name: 'Stressed Positions',
          Expression:
            '[limit_util_pct] > 100 OR [pnl] < 0 OR [weightPct] > 8',
        },
      ],
    },
    Layout: {
      CurrentLayout: 'PM Overview',
      Layouts: [
        {
          Name: 'PM Overview',
          AutoSizeColumns: true,
          TableColumns: [
            'symbol',
            'securityName',
            'sector',
            'region',
            'quantity',
            'lastPrice',
            'marketValue',
            'pnl',
            'pnlPct',
            'weightPct',
            'rating',
          ],
        },
        {
          Name: 'Risk View',
          AutoSizeColumns: true,
          TableColumns: [
            'symbol',
            'securityName',
            'sector',
            'marketValue',
            'limitUsd',
            'limit_util_pct',
            'pnl',
            'pnlPct',
            'weightPct',
            'rating',
          ],
          ColumnSizing: {limit_util_pct: {Width: 160}},
          GridFilter: {
            Expression: 'QUERY("Stressed Positions")',
          },
        },
        {
          Name: 'Sector Exposure',
          PivotColumns: ['region'],
          PivotGroupedColumns: ['sector'],
          PivotAggregationColumns: [
            {ColumnId: 'marketValue', AggFunc: 'sum'},
            {ColumnId: 'pnl', AggFunc: 'sum'},
          ],
          SuppressAggFuncInHeader: true,
        },
      ],
    },
    StyledColumn: {
      StyledColumns: [
        {
          Name: 'Sector badge',
          ColumnId: 'sector',
          BadgeStyle: {
            Badges: [
              {
                Rule: '[sector] = "Technology"',
                PillStyle: {
                  BackColor: 'var(--ab-color-palette-7)',
                  ForeColor: 'var(--ab-color-palette-8)',
                },
              },
              {
                Rule: '[sector] = "Financials"',
                PillStyle: {
                  BackColor: 'var(--ab-color-palette-11)',
                  ForeColor: 'var(--ab-color-palette-12)',
                },
              },
              {
                Rule: '[sector] = "Healthcare"',
                PillStyle: {
                  BackColor: 'var(--ab-color-palette-5)',
                  ForeColor: 'var(--ab-color-palette-6)',
                },
              },
              {
                Rule: '[sector] = "Energy"',
                PillStyle: {
                  BackColor: 'var(--ab-color-palette-1)',
                  ForeColor: 'var(--ab-color-palette-2)',
                },
              },
              {
                Rule: '[sector] = "Consumer"',
                PillStyle: {
                  BackColor: 'var(--ab-color-palette-3)',
                  ForeColor: 'var(--ab-color-palette-4)',
                },
              },
              {
                Rule: '[sector] = "Industrials"',
                PillStyle: {
                  BackColor: 'var(--ab-color-palette-9)',
                  ForeColor: 'var(--ab-color-palette-10)',
                },
              },
            ],
          },
        },
        {
          ColumnId: 'lastPrice',
          Name: 'Last vs 52-week range',
          RangeBarStyle: {
            Min: 'weekLow',
            Max: 'weekHigh',
            Reference: {
              Value: 'previousClose',
              Marker: {
                Shape: 'Line',
                Color: '#aaa',
                Size: 2,
              },
            },
            Marker: {
              Shape: 'Diamond',
              Color: '#22d3ee',
              Size: 8,
            },
            Track: {Height: 4},
            CellTextProperties: {
              CellTextLayout: {
                PercentValue: {Horizontal: 'Left', Vertical: 'Below'},
              },
            },
            ToolTipText: ['CellValue', 'PercentageValue'],
            OutOfRange: {Mode: 'Overflow'},
          },
        },
        {
          ColumnId: 'limit_util_pct',
          Name: 'Limit utilisation bullet',
          BulletChartStyle: {
            RangeValueType: 'Number',
            CellRanges: [
              {Min: 0, Max: 80, Color: 'rgba(40, 167, 69, 0.35)'},
              {Min: 80, Max: 100, Color: 'rgba(255, 193, 7, 0.35)'},
              {Min: 100, Max: 160, Color: 'rgba(220, 53, 69, 0.35)'},
            ],
            TargetProperties: {
              Target: 100,
              Marker: {
                Shape: 'Line',
                Color: '#e2e2e2',
                Size: 2,
              },
            },
            Bar: {Color: '#5b9bd5', Height: 8},
            CellTextProperties: {
              CellTextLayout: {
                CellValue: {Horizontal: 'Left', Vertical: 'Below'},
              },
            },
          },
        },
      ],
    },
    CalculatedColumn: {
      CalculatedColumns: [
        {
          FriendlyName: 'Limit Util %',
          ColumnId: 'limit_util_pct',
          Expression: '([marketValue] / [limitUsd]) * 100',
          CalculatedColumnSettings: {
            DataType: 'number',
            Filterable: true,
          },
        },
      ],
    },
    FormatColumn: {
      FormatColumns: [
        {
          Name: 'format-limit-breach',
          Scope: {ColumnIds: ['limit_util_pct', 'marketValue', 'limitUsd']},
          Rule: '[limit_util_pct] > 100',
          Style: {
            BackColor: 'var(--ab-color-palette-1)',
            ForeColor: 'var(--ab-color-palette-2)',
            FontWeight: 'Bold',
          },
        },
        {
          Name: 'format-negative-pnl',
          Scope: {ColumnIds: ['pnl', 'pnlPct']},
          Rule: '[pnl] < 0',
          Style: {
            ForeColor: '#ff6b6b',
          },
        },
        {
          Name: 'format-positive-pnl',
          Scope: {ColumnIds: ['pnl', 'pnlPct']},
          Rule: '[pnl] > 0',
          Style: {
            ForeColor: '#4ade80',
          },
        },
        {
          Name: 'format-concentration',
          Scope: {ColumnIds: ['weightPct']},
          Rule: '[weightPct] > 8',
          Style: {
            BackColor: 'var(--ab-color-palette-3)',
            ForeColor: 'var(--ab-color-palette-4)',
          },
        },
        {
          Name: 'format-notional-size',
          Scope: {ColumnIds: ['marketValue', 'limitUsd']},
          DisplayFormat: 'Million',
        },
        {
          Name: 'format-last-price',
          Scope: {ColumnIds: ['lastPrice']},
          DisplayFormat: 'Dollar',
        },
        {
          Name: 'format-pnl',
          Scope: {ColumnIds: ['pnl']},
          DisplayFormat: 'Accounting',
        },
        {
          Name: 'format-quantity',
          Scope: {ColumnIds: ['quantity']},
          DisplayFormat: 'Integer',
        },
        {
          Name: 'format-pnl-pct',
          Scope: {ColumnIds: ['pnlPct', 'limit_util_pct', 'weightPct']},
          DisplayFormat: {
            Formatter: 'NumberFormatter',
            Options: {
              Suffix: '%',
              FractionDigits: 2,
            },
          },
        },
      ],
    },
    Export: {
      CurrentReport: 'Risk Pack',
      CurrentFormat: 'Excel',
      Reports: [
        {
          Name: 'Risk Pack',
          ReportColumnScope: 'ScopeColumns',
          ReportRowScope: 'ExpressionRows',
          Scope: {
            ColumnIds: [
              'symbol',
              'securityName',
              'sector',
              'region',
              'marketValue',
              'limitUsd',
              'limit_util_pct',
              'pnl',
              'pnlPct',
              'weightPct',
              'rating',
            ],
          },
          Expression: 'QUERY("Stressed Positions")',
        },
        {
          Name: 'Full Portfolio',
          ReportColumnScope: 'AllColumns',
          ReportRowScope: 'AllRows',
        },
      ],
      ReportSchedules: [
        {
          Name: 'Weekday risk pack (EOD)',
          ReportName: 'Risk Pack',
          ReportFormat: 'Excel',
          ExportDestination: 'Download',
          Schedule: {
            IsOneOff: false,
            CronExpression: '0 17 * * 1-5',
          },
        },
      ],
    },
  },
};
```

**`src/rowData.ts`**

```ts
export interface PortfolioPosition {
  id: number;
  symbol: string;
  securityName: string;
  sector: string;
  region: string;
  quantity: number;
  lastPrice: number;
  marketValue: number;
  pnl: number;
  pnlPct: number;
  limitUsd: number;
  weightPct: number;
  rating: string;
  weekLow: number;
  weekHigh: number;
  previousClose: number;
}

const SECTORS = [
  'Technology',
  'Financials',
  'Healthcare',
  'Energy',
  'Consumer',
  'Industrials',
] as const;

const REGIONS = ['Americas', 'EMEA', 'APAC'] as const;

const RATINGS = ['AAA', 'AA', 'A', 'BBB', 'BB', 'B'] as const;

const HOLDINGS: {
  symbol: string;
  securityName: string;
  sector: (typeof SECTORS)[number];
  region: (typeof REGIONS)[number];
  basePrice: number;
}[] = [
  {symbol: 'AAPL', securityName: 'Apple Inc', sector: 'Technology', region: 'Americas', basePrice: 189},
  {symbol: 'MSFT', securityName: 'Microsoft Corp', sector: 'Technology', region: 'Americas', basePrice: 415},
  {symbol: 'NVDA', securityName: 'NVIDIA Corp', sector: 'Technology', region: 'Americas', basePrice: 875},
  {symbol: 'GOOGL', securityName: 'Alphabet Inc', sector: 'Technology', region: 'Americas', basePrice: 172},
  {symbol: 'META', securityName: 'Meta Platforms', sector: 'Technology', region: 'Americas', basePrice: 505},
  {symbol: 'AMZN', securityName: 'Amazon.com Inc', sector: 'Consumer', region: 'Americas', basePrice: 178},
  {symbol: 'TSLA', securityName: 'Tesla Inc', sector: 'Consumer', region: 'Americas', basePrice: 248},
  {symbol: 'JPM', securityName: 'JPMorgan Chase', sector: 'Financials', region: 'Americas', basePrice: 198},
  {symbol: 'BAC', securityName: 'Bank of America', sector: 'Financials', region: 'Americas', basePrice: 38},
  {symbol: 'GS', securityName: 'Goldman Sachs', sector: 'Financials', region: 'Americas', basePrice: 462},
  {symbol: 'UNH', securityName: 'UnitedHealth Group', sector: 'Healthcare', region: 'Americas', basePrice: 528},
  {symbol: 'JNJ', securityName: 'Johnson & Johnson', sector: 'Healthcare', region: 'Americas', basePrice: 156},
  {symbol: 'PFE', securityName: 'Pfizer Inc', sector: 'Healthcare', region: 'Americas', basePrice: 28},
  {symbol: 'XOM', securityName: 'Exxon Mobil', sector: 'Energy', region: 'Americas', basePrice: 118},
  {symbol: 'CVX', securityName: 'Chevron Corp', sector: 'Energy', region: 'Americas', basePrice: 162},
  {symbol: 'CAT', securityName: 'Caterpillar Inc', sector: 'Industrials', region: 'Americas', basePrice: 348},
  {symbol: 'BA', securityName: 'Boeing Co', sector: 'Industrials', region: 'Americas', basePrice: 188},
  {symbol: 'SAP', securityName: 'SAP SE', sector: 'Technology', region: 'EMEA', basePrice: 198},
  {symbol: 'ASML', securityName: 'ASML Holding', sector: 'Technology', region: 'EMEA', basePrice: 945},
  {symbol: 'NESN', securityName: 'Nestlé SA', sector: 'Consumer', region: 'EMEA', basePrice: 98},
  {symbol: 'HSBA', securityName: 'HSBC Holdings', sector: 'Financials', region: 'EMEA', basePrice: 7.2},
  {symbol: 'BP', securityName: 'BP plc', sector: 'Energy', region: 'EMEA', basePrice: 5.4},
  {symbol: 'SHEL', securityName: 'Shell plc', sector: 'Energy', region: 'EMEA', basePrice: 31},
  {symbol: 'RIO', securityName: 'Rio Tinto', sector: 'Industrials', region: 'EMEA', basePrice: 62},
  {symbol: '7203', securityName: 'Toyota Motor', sector: 'Consumer', region: 'APAC', basePrice: 28},
  {symbol: '005930', securityName: 'Samsung Electronics', sector: 'Technology', region: 'APAC', basePrice: 72},
  {symbol: 'BABA', securityName: 'Alibaba Group', sector: 'Consumer', region: 'APAC', basePrice: 78},
  {symbol: 'TSM', securityName: 'Taiwan Semiconductor', sector: 'Technology', region: 'APAC', basePrice: 142},
  {symbol: 'RELIANCE', securityName: 'Reliance Industries', sector: 'Energy', region: 'APAC', basePrice: 29},
  {symbol: 'CBA', securityName: 'Commonwealth Bank', sector: 'Financials', region: 'APAC', basePrice: 108},
];

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

function buildPosition(
  id: number,
  holding: (typeof HOLDINGS)[number],
  variant: number
): PortfolioPosition {
  const priceJitter = 0.88 + pseudoRandom(id * 7 + variant) * 0.24;
  const lastPrice = Math.round(holding.basePrice * priceJitter * 100) / 100;
  const quantity =
    Math.round(
      (8000 + pseudoRandom(id * 3 + variant) * 420000) /
        Math.max(lastPrice, 1)
    ) * 100;
  const marketValue = Math.round(quantity * lastPrice);
  const pnlPct = Math.round((pseudoRandom(id * 11 + variant) - 0.48) * 800) / 100;
  const pnl = Math.round((marketValue * pnlPct) / 100);
  const baseLimit = marketValue * (0.72 + pseudoRandom(id * 5 + variant) * 0.55);
  const limitUsd = Math.round(baseLimit / 100000) * 100000;
  const weightPct =
    Math.round((marketValue / 48_000_000) * 10000 + pseudoRandom(id) * 40) / 100;
  const weekLow =
    Math.round(lastPrice * (0.78 + pseudoRandom(id * 17 + variant) * 0.1) * 100) /
    100;
  const weekHigh =
    Math.round(lastPrice * (1.08 + pseudoRandom(id * 19 + variant) * 0.14) * 100) /
    100;
  const previousClose =
    Math.round(lastPrice * (0.94 + pseudoRandom(id * 23 + variant) * 0.1) * 100) /
    100;

  return {
    id,
    symbol: holding.symbol,
    securityName: holding.securityName,
    sector: holding.sector,
    region: holding.region,
    quantity,
    lastPrice,
    marketValue,
    pnl,
    pnlPct,
    limitUsd: Math.max(limitUsd, 500_000),
    weightPct,
    rating: RATINGS[Math.floor(pseudoRandom(id * 13) * RATINGS.length)],
    weekLow,
    weekHigh,
    previousClose,
  };
}

const generatedRows: PortfolioPosition[] = [];
let id = 1;

for (let pass = 0; pass < 6; pass++) {
  for (const holding of HOLDINGS) {
    generatedRows.push(buildPosition(id++, holding, pass));
  }
}

// Ensure a visible set of risk scenarios for the demo narrative.
generatedRows[4] = {
  ...generatedRows[4],
  marketValue: 42_500_000,
  limitUsd: 35_000_000,
  pnl: -1_240_000,
  pnlPct: -2.84,
  weightPct: 11.2,
};
generatedRows[18] = {
  ...generatedRows[18],
  marketValue: 28_100_000,
  limitUsd: 22_000_000,
  pnl: -890_000,
  pnlPct: -3.07,
  weightPct: 9.6,
};
generatedRows[31] = {
  ...generatedRows[31],
  marketValue: 19_800_000,
  limitUsd: 30_000_000,
  pnl: 420_000,
  pnlPct: 2.17,
  weightPct: 8.4,
};
generatedRows[52] = {
  ...generatedRows[52],
  marketValue: 51_200_000,
  limitUsd: 40_000_000,
  pnl: -2_100_000,
  pnlPct: -3.94,
  weightPct: 12.8,
};
generatedRows[77] = {
  ...generatedRows[77],
  marketValue: 15_400_000,
  limitUsd: 18_000_000,
  pnl: 310_000,
  pnlPct: 2.05,
  weightPct: 7.9,
};

export const rowData: PortfolioPosition[] = generatedRows;
```

**`src/columnDefs.ts`**

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

export const columnDefs: ColDef[] = [
  {field: 'symbol', headerName: 'Symbol', cellDataType: 'text', width: 110},
  {
    field: 'securityName',
    headerName: 'Security',
    cellDataType: 'text',
    flex: 1,
    minWidth: 180,
  },
  {
    field: 'sector',
    cellDataType: 'text',
    enableRowGroup: true,
    enablePivot: true,
  },
  {
    field: 'region',
    cellDataType: 'text',
    enableRowGroup: true,
    enablePivot: true,
  },
  {
    field: 'quantity',
    headerName: 'Qty',
    cellDataType: 'number',
    type: 'rightAligned',
  },
  {
    field: 'lastPrice',
    headerName: 'Last',
    cellDataType: 'number',
    type: 'rightAligned',
  },
  {
    field: 'marketValue',
    headerName: 'Market Value',
    cellDataType: 'number',
    type: 'rightAligned',
    enableValue: true,
  },
  {
    field: 'pnl',
    headerName: 'P&L',
    cellDataType: 'number',
    type: 'rightAligned',
    enableValue: true,
  },
  {
    field: 'pnlPct',
    headerName: 'P&L %',
    cellDataType: 'number',
    type: 'rightAligned',
  },
  {
    field: 'limitUsd',
    headerName: 'Limit (USD)',
    cellDataType: 'number',
    type: 'rightAligned',
  },
  {
    field: 'weightPct',
    headerName: 'Weight %',
    cellDataType: 'number',
    type: 'rightAligned',
    enableValue: true,
  },
  {field: 'rating', cellDataType: 'text'},
  {field: 'weekLow', headerName: '52W Low', hide: true, cellDataType: 'number'},
  {field: 'weekHigh', headerName: '52W High', hide: true, cellDataType: 'number'},
  {
    field: 'previousClose',
    headerName: 'Prev Close',
    hide: true,
    cellDataType: 'number',
  },
  {field: 'id', hide: true, editable: false, enableValue: true},
];
```

**`src/gridOptions.ts`**

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

import {columnDefs} from './columnDefs';
import {PortfolioPosition, rowData} from './rowData';

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