Finance scenario
Capital markets workflow showing research watchlist — movers, conviction, styled columns, charts, and research notes
Your Role
You are a research analyst monitoring a global equity watchlist before the market open.
Find Out More
- This showcase demo complements the Portfolio Risk Desk with a front-office lens
- For live ticking, flashing, and Observable Alerts, see Live Price Monitor
import { ActionColumnContext, AdaptableButton, AdaptableOptions, CellUpdateRequest, CustomToolbarFormContext, } from '@adaptabletools/adaptable'; import {WatchlistEntry} from './rowData'; function applyWatchlistToolbarFilter(context: CustomToolbarFormContext) { const sector = context.formData.sector as string | undefined; const moversOnly = Boolean(context.formData.moversOnly); const expressions: string[] = []; if (sector && sector !== 'All') { expressions.push(`[sector] = "${sector}"`); } if (moversOnly) { expressions.push('QUERY("Big Movers")'); } context.adaptableApi.filterApi.gridFilterApi.setGridFilterExpression( expressions.length ? expressions.join(' AND ') : null ); } export const adaptableOptions: AdaptableOptions<WatchlistEntry> = { primaryKey: 'id', adaptableId: 'Showcase: Equity Watchlist', userName: 'Research Analyst', dashboardOptions: { customToolbars: [ { name: 'WatchlistToolbar', title: 'Watchlist', toolbarForm: { fields: [ { name: 'sector', label: 'Sector', fieldType: 'select', defaultValue: 'All', options: [ {label: 'All sectors', value: 'All'}, {label: 'Technology', value: 'Technology'}, {label: 'Financials', value: 'Financials'}, {label: 'Healthcare', value: 'Healthcare'}, {label: 'Energy', value: 'Energy'}, {label: 'Consumer', value: 'Consumer'}, {label: 'Industrials', value: 'Industrials'}, ], onValueChange: (_value, ctx) => applyWatchlistToolbarFilter(ctx as CustomToolbarFormContext), }, { name: 'moversOnly', label: 'Big movers only', fieldType: 'checkbox', defaultValue: false, onValueChange: (_value, ctx) => applyWatchlistToolbarFilter(ctx as CustomToolbarFormContext), }, ], buttons: [ { label: 'Clear', buttonStyle: { tone: 'neutral', variant: 'raised', className: 'watchlist-clear-btn', }, onClick: ( _button: AdaptableButton<CustomToolbarFormContext>, context: CustomToolbarFormContext ) => { context.adaptableApi.filterApi.gridFilterApi.clearGridFilter(); context.adaptableApi.dashboardApi.resetCustomToolbarFormData( 'WatchlistToolbar' ); }, }, ], }, }, ], }, actionColumnOptions: { actionColumns: [ { columnId: 'quick_buy', friendlyName: 'Idea', actionColumnButton: [ { label: 'Buy', buttonStyle: {variant: 'outlined', tone: 'success'}, disabled: (_button, context: ActionColumnContext) => context.data?.recommendation === 'Buy', onClick: (_button: unknown, context: ActionColumnContext) => { const cellUpdateRequest: CellUpdateRequest = { columnId: 'recommendation', newValue: 'Buy', primaryKeyValue: context.primaryKeyValue, rowNode: context.rowNode, }; context.adaptableApi.gridApi.setCellValue(cellUpdateRequest); }, }, ], }, ], }, initialState: { Dashboard: { Tabs: [ { Name: 'Watch', Toolbars: ['WatchlistToolbar', 'Layout', 'Alert', 'GridFilter'], }, { Name: 'Charts', Toolbars: ['Charting', 'Layout'], }, ], ModuleButtons: [ 'Layout', 'StyledColumn', 'CalculatedColumn', 'Alert', 'Note', 'SettingsPanel', ], }, StatusBar: { StatusBars: [ { Key: 'Center Panel', StatusBarPanels: ['Layout', 'Alert'], }, ], }, Theme: {CurrentTheme: 'dark'}, NamedQuery: { NamedQueries: [ { Name: 'Big Movers', BooleanExpression: 'ABS([changePct]) > 2', }, ], }, Layout: { CurrentLayout: 'Compact', Layouts: [ { Name: 'Compact', AutoSizeColumns: true, TableColumns: [ 'listingCountry', 'symbol', 'last', 'changePct', 'priceTrend', 'recommendation', 'weightPct', 'quick_buy', ], ColumnSizing: { listingCountry: {Width: 64}, symbol: {Width: 92}, last: {Width: 88}, changePct: {Width: 82}, priceTrend: {Width: 168, MinWidth: 160}, recommendation: {Width: 72}, weightPct: {Width: 110, MinWidth: 110}, }, }, { Name: 'Research', AutoSizeColumns: true, TableColumns: [ 'listingCountry', 'symbol', 'name', 'sector', 'last', 'changePct', 'priceTrend', 'targetPrice', 'upside_pct', 'recommendation', 'analystScore', 'position', 'marketValue', 'quick_buy', ], ColumnSizing: { listingCountry: {Width: 64}, symbol: {Width: 88}, name: {Width: 132, MinWidth: 120}, sector: {Width: 104, MinWidth: 96}, last: {Width: 80}, changePct: {Width: 76}, priceTrend: {Width: 148, MinWidth: 140}, targetPrice: {Width: 84}, upside_pct: {Width: 84}, recommendation: {Width: 68}, analystScore: {Width: 100, MinWidth: 96}, position: {Width: 64}, marketValue: {Width: 96}, quick_buy: {Width: 72}, }, }, { Name: 'Movers', AutoSizeColumns: true, TableColumns: [ 'symbol', 'name', 'sector', 'last', 'changePct', 'priceTrend', 'recommendation', 'analystScore', 'quick_buy', ], GridFilter: { Expression: 'QUERY("Big Movers")', }, ColumnSizing: { sector: {Width: 104, MinWidth: 96}, last: {Width: 80}, changePct: {Width: 76}, priceTrend: {Width: 148, MinWidth: 140}, recommendation: {Width: 64, MinWidth: 60}, analystScore: {Width: 100, MinWidth: 96}, quick_buy: {Width: 72}, }, }, ], }, CalculatedColumn: { CalculatedColumns: [ { FriendlyName: 'Upside %', ColumnId: 'upside_pct', Query: { ScalarExpression: '(([targetPrice] - [last]) / [last]) * 100', }, CalculatedColumnSettings: { DataType: 'number', Filterable: true, }, }, ], }, StyledColumn: { StyledColumns: [ { Name: 'Listing country flag', ColumnId: 'listingCountry', IconStyle: { Preset: 'Flags', Mappings: [{Key: 'TW', Icon: '🇹🇼', Description: 'Taiwan'}], MatchMode: 'CaseInsensitive', ToolTipText: ['IconDescription', 'CellValue'], Size: 18, Gap: 4, FallbackProperties: { Mode: 'ShowText' }, }, }, { Name: '30d trend', ColumnId: 'priceTrend', SparklineStyle: { options: { type: 'line', stroke: 'rgb(100, 181, 246)', strokeWidth: 2, padding: {top: 4, bottom: 4}, }, }, }, { Name: 'Sector badge', ColumnId: 'sector', BadgeStyle: { Badges: [ { Predicate: {PredicateId: 'Is', Inputs: ['Technology']}, PillStyle: { BackColor: 'var(--ab-color-palette-7)', ForeColor: 'var(--ab-color-palette-8)', }, }, { Predicate: {PredicateId: 'Is', Inputs: ['Financials']}, PillStyle: { BackColor: 'var(--ab-color-palette-11)', ForeColor: 'var(--ab-color-palette-12)', }, }, { Predicate: {PredicateId: 'Is', Inputs: ['Healthcare']}, PillStyle: { BackColor: 'var(--ab-color-palette-5)', ForeColor: 'var(--ab-color-palette-6)', }, }, { Predicate: {PredicateId: 'Is', Inputs: ['Energy']}, PillStyle: { BackColor: 'var(--ab-color-palette-1)', ForeColor: 'var(--ab-color-palette-2)', }, }, { Predicate: {PredicateId: 'Is', Inputs: ['Consumer']}, PillStyle: { BackColor: 'var(--ab-color-palette-3)', ForeColor: 'var(--ab-color-palette-4)', }, }, { Predicate: {PredicateId: 'Is', Inputs: ['Industrials']}, PillStyle: { BackColor: 'var(--ab-color-palette-9)', ForeColor: 'var(--ab-color-palette-10)', }, }, ], }, }, { Name: 'Recommendation', ColumnId: 'recommendation', BadgeStyle: { Badges: [ { Predicate: {PredicateId: 'Is', Inputs: ['Buy']}, PillStyle: { BackColor: 'var(--ab-color-palette-5)', ForeColor: 'var(--ab-color-palette-6)', }, }, { Predicate: {PredicateId: 'Is', Inputs: ['Hold']}, PillStyle: { BackColor: 'var(--ab-color-palette-3)', ForeColor: 'var(--ab-color-palette-4)', }, }, { Predicate: {PredicateId: 'Is', Inputs: ['Sell']}, PillStyle: { BackColor: 'var(--ab-color-palette-1)', ForeColor: 'var(--ab-color-palette-2)', }, }, ], }, }, { Name: 'analystScore Rating', ColumnId: 'analystScore', RatingStyle: { Icon: 'Star', Max: 5, Size: 14, Gap: 2, AllowHalf: true, ShowValue: false, FilledColor: 'var(--ab-color-warn)', EmptyColor: 'color-mix(in srgb, currentColor 22%, transparent)', }, }, { Name: 'weightPct PercentBar', ColumnId: 'weightPct', PercentBarStyle: { RangeValueType: 'Number', CellRanges: [ {Min: 0, Max: 4, Color: 'var(--ab-color-palette-10)'}, {Min: 4, Max: 8, Color: 'var(--ab-color-info)'}, {Min: 8, Max: 15, Color: 'var(--ab-color-warn)'}, ], BackColor: 'var(--ab-color-palette-9)', }, }, { Name: 'changePct Gradient', ColumnId: 'changePct', GradientStyle: { ZeroCentred: { NegativeColor: 'var(--ab-color-destructive)', PositiveColor: 'var(--ab-color-success)', }, }, }, ], }, FormatColumn: { FormatColumns: [ { Name: 'last Dollar', Scope: {ColumnIds: ['last', 'targetPrice']}, DisplayFormat: 'Dollar', }, { Name: 'marketValue Million', Scope: {ColumnIds: ['marketValue']}, DisplayFormat: 'Million', }, { Name: 'format-pct', Scope: {ColumnIds: ['changePct', 'upside_pct', 'weightPct']}, DisplayFormat: { Formatter: 'NumberFormatter', Options: {Suffix: '%', FractionDigits: 2}, }, }, ], }, FlashingCell: { FlashingCellDefinitions: [ { Name: 'Upside % reacts to target', Scope: {ColumnIds: ['upside_pct']}, Rule: {BooleanExpression: 'ANY_CHANGE()'}, }, ], }, Alert: { AlertDefinitions: [ { Name: 'Upside revised', MessageType: 'Info', MessageHeader: 'Upside % updated', MessageText: 'The target change revised upside on this row — check the Research layout.', Scope: {ColumnIds: ['upside_pct']}, Rule: {BooleanExpression: 'ANY_CHANGE()'}, AlertProperties: { DisplayNotification: true, }, }, { Name: 'Big Movers', MessageType: 'Warning', MessageHeader: 'Big mover on watchlist', MessageText: 'Daily change exceeds ±2% (|Chg %| > 2) — review the name before the open.', Scope: {All: true}, Rule: { BooleanExpression: 'QUERY("Big Movers")', }, AlertProperties: { DisplayNotification: true, }, }, ], }, Note: { Notes: [ { PrimaryKeyValue: 5, ColumnId: 'symbol', Text: 'Raising estimates after data-centre demand print.', Timestamp: Date.now() - 86_400_000, }, { PrimaryKeyValue: 15, ColumnId: 'name', Text: 'Watch delivery numbers — high beta into earnings.', Timestamp: Date.now() - 172_800_000, }, { PrimaryKeyValue: 19, ColumnId: 'symbol', Text: 'Pipeline read-through weaker than peers; hold for now.', Timestamp: Date.now() - 259_200_000, }, ], }, Charting: { ChartDefinitions: [ { Name: 'Movers by change %', Model: { modelType: 'range', chartId: 'watchlist-movers-bar', chartType: 'bar', chartThemeName: 'ag-vivid-dark', chartOptions: { bar: { title: {text: 'Top movers (sample)'}, }, }, cellRange: { rowStartIndex: 0, rowEndIndex: 11, columns: ['symbol', 'changePct'], }, suppressChartRanges: false, unlinkChart: false, }, }, ], }, }, };