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