gloomberb 0.17.0 → 0.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/{plugins/builtin/cloud → account}/auth-dialog.tsx +7 -7
- package/src/api-client/data.ts +5 -1
- package/src/api-client/doe.ts +19 -0
- package/src/api-client/earnings.ts +4 -2
- package/src/api-client/index.ts +1 -0
- package/src/app/global-shortcuts.ts +10 -0
- package/src/app/keybindings/actions.ts +16 -0
- package/src/app/runtime/desktop-deeplink.ts +1 -1
- package/src/app/runtime/update.ts +16 -0
- package/src/app.tsx +1 -1
- package/src/architecture/core-builtin-allowlist.json +4 -9
- package/src/architecture/dom-cell-metrics-allowlist.json +100 -26
- package/src/cli/commands/market.ts +87 -17
- package/src/cli/commands/options-chain.ts +93 -23
- package/src/cli/commands/options-leaps.ts +462 -63
- package/src/cli/commands/search.ts +10 -7
- package/src/cli/commands/ticker.ts +19 -10
- package/src/cli/glossary.ts +1 -1
- package/src/cli/index.ts +14 -6
- package/src/cli/known-gaps.ts +22 -0
- package/src/cli/pane-functions/catalog.ts +45 -10
- package/src/cli/pane-functions/dom.ts +36 -10
- package/src/cli/pane-functions/function-help.ts +4 -2
- package/src/cli/pane-functions/headless.ts +12 -1
- package/src/cli/pane-functions/index.ts +44 -3
- package/src/cli/pane-functions/lock-note.ts +56 -0
- package/src/cli/pane-functions/report-notices.ts +15 -0
- package/src/cli/symbol-args.ts +19 -0
- package/src/components/command-bar/list/view.tsx +2 -1
- package/src/components/command-bar/workflow/builtin.ts +2 -2
- package/src/components/command-bar/workflow/collection-actions.ts +1 -1
- package/src/components/feedback-dialog.tsx +1 -1
- package/src/components/index.ts +3 -2
- package/src/components/layout/header.tsx +58 -14
- package/src/components/layout/shell/chrome.ts +10 -4
- package/src/components/layout/shell/index.tsx +1 -1
- package/src/components/layout/status-bar-groups.ts +1 -1
- package/src/components/layout/status-bar.tsx +2 -2
- package/src/components/markdown-text.tsx +14 -2
- package/src/components/reader/article-reader.tsx +1 -1
- package/src/components/reader/detail-reader.tsx +10 -7
- package/src/components/reader/reader-text.tsx +32 -17
- package/src/components/ui/column-grid.tsx +206 -0
- package/src/components/ui/display.tsx +40 -3
- package/src/components/ui/fill.ts +8 -0
- package/src/components/ui/index.ts +2 -0
- package/src/layout-marketplace/gallery.tsx +2 -2
- package/src/layout-marketplace/linked-sync.ts +1 -1
- package/src/market-data/market/freshness.ts +3 -0
- package/src/market-data/published-apac-sessions.ts +12 -2
- package/src/market-data/quotes/screener-live-quotes.ts +31 -7
- package/src/plugins/builtin/account-management/form-components.tsx +4 -7
- package/src/plugins/builtin/account-management/password-dialog.tsx +3 -2
- package/src/plugins/builtin/backtest/pane.tsx +9 -3
- package/src/plugins/builtin/buildout/detail/company.tsx +4 -3
- package/src/plugins/builtin/buildout/detail/ui.tsx +9 -12
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chart-composer/presets.ts +33 -1
- package/src/plugins/builtin/chat/channel-labels.ts +2 -2
- package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -1
- package/src/plugins/builtin/chat/content/index.tsx +2 -2
- package/src/plugins/builtin/chat/conversations.ts +1 -1
- package/src/plugins/builtin/chat/index.tsx +2 -2
- package/src/plugins/builtin/chat/share/flow.tsx +1 -1
- package/src/plugins/builtin/chat/sidebar-rows.ts +1 -1
- package/src/plugins/builtin/chat/sidebar.tsx +2 -2
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +2 -2
- package/src/plugins/builtin/cloud/askg/pane.tsx +2 -1
- package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +3 -2
- package/src/plugins/builtin/cloud/auth-actions.tsx +1 -1
- package/src/plugins/builtin/cloud/auth-commands.ts +1 -1
- package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -1
- package/src/plugins/builtin/cloud/team/acm-tab.tsx +3 -3
- package/src/plugins/builtin/cloud/team/capability.ts +2 -2
- package/src/plugins/builtin/cloud/team/command.ts +4 -4
- package/src/plugins/builtin/cloud/team/module.tsx +3 -3
- package/src/plugins/builtin/cloud/team/pane-model.ts +2 -2
- package/src/plugins/builtin/cloud/team/pane-sections.tsx +1 -1
- package/src/plugins/builtin/cloud/team/pane-ui.tsx +1 -1
- package/src/plugins/builtin/cloud/team/pane.tsx +3 -3
- package/src/plugins/builtin/cloud/team/status-widget.tsx +3 -3
- package/src/plugins/builtin/cloud/team/team-state-host.ts +1 -1
- package/src/plugins/builtin/cloud/team/views.ts +2 -2
- package/src/plugins/builtin/cloud/thesis/board-pane.tsx +1 -1
- package/src/plugins/builtin/cloud/thesis/detail.tsx +12 -10
- package/src/plugins/builtin/cloud/thesis/ticker-tab.tsx +1 -1
- package/src/plugins/builtin/cloud/upgrade-command.ts +1 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +8 -4
- package/src/plugins/builtin/connections/pane.tsx +16 -5
- package/src/plugins/builtin/cpi/pane.tsx +5 -4
- package/src/plugins/builtin/custom-view/pane.tsx +2 -2
- package/src/plugins/builtin/dividend-yield/headless.ts +8 -2
- package/src/plugins/builtin/dividend-yield/pane.tsx +4 -2
- package/src/plugins/builtin/docs/blocks.tsx +13 -2
- package/src/plugins/builtin/docs/pane.tsx +6 -5
- package/src/plugins/builtin/doe/index.tsx +11 -4
- package/src/plugins/builtin/doe/pane.tsx +5 -4
- package/src/plugins/builtin/doe/prices.ts +84 -0
- package/src/plugins/builtin/doe/series.ts +4 -1
- package/src/plugins/builtin/earnings/board-model.ts +18 -1
- package/src/plugins/builtin/earnings/board-table.ts +18 -3
- package/src/plugins/builtin/earnings/headless.ts +13 -2
- package/src/plugins/builtin/earnings/history-model.ts +1 -1
- package/src/plugins/builtin/earnings/history-table.ts +2 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +7 -21
- package/src/plugins/builtin/earnings-calls/index.tsx +2 -0
- package/src/plugins/builtin/earnings-calls/lock-note.ts +17 -0
- package/src/plugins/builtin/equity-screener/headless.ts +59 -1
- package/src/plugins/builtin/equity-screener/live.ts +82 -31
- package/src/plugins/builtin/equity-screener/pane.tsx +60 -23
- package/src/plugins/builtin/estimate-revisions/client.ts +1 -1
- package/src/plugins/builtin/estimate-revisions/headless.ts +9 -2
- package/src/plugins/builtin/estimate-revisions/index.tsx +3 -0
- package/src/plugins/builtin/estimate-revisions/lock-note.ts +23 -0
- package/src/plugins/builtin/executives/index.tsx +18 -0
- package/src/plugins/builtin/exposure/charts.tsx +4 -3
- package/src/plugins/builtin/filing-events/index.tsx +8 -10
- package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
- package/src/plugins/builtin/gpu/equities.tsx +9 -6
- package/src/plugins/builtin/help/feedback-tab.tsx +1 -1
- package/src/plugins/builtin/iv-history/format.ts +3 -0
- package/src/plugins/builtin/iv-history/headless.ts +3 -1
- package/src/plugins/builtin/iv-history/index.tsx +3 -1
- package/src/plugins/builtin/iv-history/model.ts +3 -1
- package/src/plugins/builtin/iv-history/screen-pane.tsx +3 -1
- package/src/plugins/builtin/iv-history/universe.ts +1 -1
- package/src/plugins/builtin/jobs/headless.ts +6 -20
- package/src/plugins/builtin/jobs/index.tsx +2 -0
- package/src/plugins/builtin/jobs/lock-note.ts +16 -0
- package/src/plugins/builtin/notes/index.tsx +1 -1
- package/src/plugins/builtin/notes/owner.tsx +2 -2
- package/src/plugins/builtin/options/carry.ts +36 -0
- package/src/plugins/builtin/options/index.tsx +2 -1
- package/src/plugins/builtin/options/model-spot.ts +94 -0
- package/src/plugins/builtin/options/open-interest-as-of.ts +74 -0
- package/src/plugins/builtin/options/view.tsx +69 -11
- package/src/plugins/builtin/options-calculator/pane.tsx +56 -1
- package/src/plugins/builtin/options-calculator/premium-history.ts +34 -0
- package/src/plugins/builtin/options-scenario/client.ts +33 -9
- package/src/plugins/builtin/options-scenario/csp.ts +168 -0
- package/src/plugins/builtin/options-scenario/evidence.ts +3 -0
- package/src/plugins/builtin/options-scenario/headless.ts +24 -8
- package/src/plugins/builtin/options-scenario/index.tsx +3 -1
- package/src/plugins/builtin/options-scenario/pane.tsx +32 -6
- package/src/plugins/builtin/portfolio-list/mutations.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +4 -2
- package/src/plugins/builtin/research/event-model.ts +7 -4
- package/src/plugins/builtin/risk-factors/index.tsx +18 -0
- package/src/plugins/builtin/risk-factors/pane.tsx +2 -1
- package/src/plugins/builtin/scanner/headless.ts +9 -2
- package/src/plugins/builtin/scanner/index.ts +3 -0
- package/src/plugins/builtin/scanner/lock-note.ts +17 -0
- package/src/plugins/builtin/series-inventory.ts +2 -1
- package/src/plugins/builtin/shared/cloud-denied.ts +39 -8
- package/src/plugins/builtin/shared/earnings-report.ts +10 -4
- package/src/plugins/builtin/shared/pro-lock-note.ts +58 -0
- package/src/plugins/builtin/shared/series-list-detail.tsx +10 -5
- package/src/plugins/builtin/social-mentions/client.ts +3 -3
- package/src/plugins/builtin/social-mentions/headless.ts +6 -2
- package/src/plugins/builtin/social-mentions/index.tsx +2 -1
- package/src/plugins/builtin/social-mentions/lock-note.ts +15 -0
- package/src/plugins/builtin/supply-chain/flow.tsx +15 -13
- package/src/plugins/builtin/supply-chain/graph.tsx +6 -4
- package/src/plugins/builtin/supply-chain/pane.tsx +8 -5
- package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +14 -1
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +9 -7
- package/src/plugins/builtin/ticker-detail/headless.ts +4 -1
- package/src/plugins/builtin/ticker-detail/overview/components.tsx +105 -104
- package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -2
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +34 -8
- package/src/plugins/builtin/vol-surface/pane.tsx +6 -3
- package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
- package/src/renderers/dom/data-table/index.tsx +48 -15
- package/src/renderers/dom/data-table/row.tsx +1 -1
- package/src/renderers/dom/desktop/popover.tsx +6 -15
- package/src/renderers/dom/host/input.tsx +27 -3
- package/src/renderers/dom/host/surface-3d.tsx +6 -4
- package/src/renderers/dom/styles.css +26 -6
- package/src/renderers/electrobun/view/app-services.ts +2 -22
- package/src/renderers/electrobun/view/news-source.ts +42 -0
- package/src/series/validate.ts +5 -0
- package/src/shares/dialog.tsx +2 -1
- package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
- package/src/sources/provider-router/financials.ts +5 -0
- package/src/{plugins/builtin/cloud/team → team}/collections-sync.ts +6 -6
- package/src/{plugins/builtin/cloud/team → team}/collections.ts +3 -3
- package/src/{plugins/builtin/cloud/team → team}/model.ts +2 -2
- package/src/{plugins/builtin/cloud/team → team}/pane-request.ts +1 -1
- package/src/{plugins/builtin/cloud/team → team}/store.ts +3 -3
- package/src/types/data-provider.ts +3 -0
- package/src/types/financials.ts +28 -0
- package/src/types/plugin.ts +17 -4
- package/src/utils/decimal-comma.ts +18 -0
- package/src/utils/exchanges.ts +7 -0
- package/src/utils/financial-statements.ts +25 -0
- package/src/utils/forward-dividend.ts +39 -9
- package/src/utils/fundamentals.ts +41 -1
- package/src/utils/option-expiry.ts +16 -0
- package/src/utils/sec.ts +2 -1
|
@@ -4,7 +4,8 @@ import type { SupplyRole, SupplyRow } from "../../../api-client/supply-chain";
|
|
|
4
4
|
import { Box, ChartSurface, Span, Text, TextAttributes, useUiCapabilities } from "../../../ui";
|
|
5
5
|
import { useShortcut } from "../../../react/input";
|
|
6
6
|
import { isPlainKey } from "../../../utils/keyboard";
|
|
7
|
-
import { displayWidth
|
|
7
|
+
import { displayWidth } from "../../../utils/format";
|
|
8
|
+
import { ClippedText } from "../../../components/ui/display";
|
|
8
9
|
import { blendHex } from "../../../theme/colors";
|
|
9
10
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
10
11
|
import { useStaticChartBitmapSize } from "../../../components/chart/composite/bitmap";
|
|
@@ -201,10 +202,10 @@ export function SupplyFlow({ rows, symbol, focusName, focusId, width, height, fo
|
|
|
201
202
|
onPress={() => selectNode(node)} onHover={(on) => setHovered(on ? node.id : null)} />)}
|
|
202
203
|
{geometry.groupRow !== null ? <Box position="absolute" top={geometry.groupRow} left={1} width={width - 2} flexDirection="row" overflow="hidden">
|
|
203
204
|
<Box width={RELATED_LABEL_CELLS} flexShrink={0}><Text fg={colors.textDim}>Groups</Text></Box>
|
|
204
|
-
<
|
|
205
|
+
<ClippedText fg={colors.textDim} text={geometry.groups.map((row) => {
|
|
205
206
|
const share = shareParts(row, focusId);
|
|
206
207
|
return `${counterpartyLabel(row)}${share ? ` ${share.value} ${share.basis}` : ""}`;
|
|
207
|
-
}).join(" · ")
|
|
208
|
+
}).join(" · ")} width={width - RELATED_LABEL_CELLS - 3} />
|
|
208
209
|
</Box> : null}
|
|
209
210
|
{legend ? <Box position="absolute" top={geometry.legendRow} left={1} width={width - 2} flexDirection="row" gap={2} overflow="hidden">
|
|
210
211
|
{legend.map((entry) => <Box key={entry.id} flexDirection="row" gap={1} flexShrink={0}>
|
|
@@ -253,7 +254,7 @@ function FocusCard({ symbol, name, x, y, width, height, desktop }: { symbol: str
|
|
|
253
254
|
backgroundColor={desktop ? colors.panel : undefined} border={!desktop} borderStyle="rounded" borderColor={colors.textMuted}
|
|
254
255
|
style={desktop ? { border: `1px solid ${blendHex(colors.bg, colors.textBright, 0.35)}`, borderRadius: 6, boxShadow: `0 0 0 3px ${blendHex(colors.bg, colors.textBright, 0.06)}` } : undefined}>
|
|
255
256
|
<Text fg={colors.textBright} attributes={TextAttributes.BOLD}>{symbol}</Text>
|
|
256
|
-
{showName ? <
|
|
257
|
+
{showName ? <ClippedText fg={colors.textDim} text={name!} width={width - 4} /> : null}
|
|
257
258
|
</Box>;
|
|
258
259
|
}
|
|
259
260
|
|
|
@@ -275,13 +276,15 @@ function FlowCard({ node, focusId, desktop, active, onPress, onHover }: {
|
|
|
275
276
|
style={desktop ? { borderLeft: `3px ${node.more ? "dotted" : "solid"} ${node.more ? blendHex(colors.bg, accent, 0.5) : accent}`, borderRadius: 3, paddingLeft: 6, cursor: "pointer",
|
|
276
277
|
boxShadow: active ? `inset 0 0 0 1px ${blendHex(colors.bg, accent, 0.6)}` : undefined } : undefined}
|
|
277
278
|
onMouseDown={onPress} onMouseOver={() => onHover(true)} onMouseOut={() => onHover(false)} data-gloom-role="supply-flow-node">
|
|
278
|
-
|
|
279
|
+
{/* The ticker shows only beside the whole name. On the desktop that is CSS: the row wraps, one line shows, and a ticker that does not fit goes to the hidden line. */}
|
|
280
|
+
<Box flexDirection="row" height={1} overflow="hidden" style={desktop ? { flexWrap: "wrap" } : undefined}>
|
|
279
281
|
{!desktop ? <Text fg={node.more ? colors.textMuted : accent}>▍</Text> : null}
|
|
280
|
-
<
|
|
281
|
-
{ticker && displayWidth(name) + displayWidth(ticker) + 1 <= inner
|
|
282
|
+
<ClippedText fg={active && !desktop ? colors.selectedText : titleColor} attributes={company ? TextAttributes.BOLD : 0} text={name} width={nameWidth} />
|
|
283
|
+
{ticker && (desktop || displayWidth(name) + displayWidth(ticker) + 1 <= inner)
|
|
284
|
+
? <Text fg={colors.textDim} style={desktop ? { flex: "0 0 auto" } : undefined}>{` ${ticker}`}</Text> : null}
|
|
282
285
|
</Box>
|
|
283
286
|
{metric ? <Box height={1} paddingLeft={desktop ? 0 : 1} overflow="hidden">
|
|
284
|
-
<
|
|
287
|
+
<ClippedText fg={active && !desktop ? colors.selectedText : colors.textDim} text={metric} width={inner - (desktop ? 0 : 1)} />
|
|
285
288
|
</Box> : null}
|
|
286
289
|
</Box>;
|
|
287
290
|
}
|
|
@@ -289,13 +292,12 @@ function FlowCard({ node, focusId, desktop, active, onPress, onHover }: {
|
|
|
289
292
|
function FlowChip({ node, desktop, active, onPress, onHover }: { node: PositionedNode; desktop: boolean; active: boolean; onPress: () => void; onHover: (on: boolean) => void }) {
|
|
290
293
|
const colors = useThemeColors();
|
|
291
294
|
const accent = node.role ? ROLE_COLORS[node.role] : colors.textMuted;
|
|
292
|
-
const label = truncateToDisplayWidth(flowChipLabel(node), node.width - 2);
|
|
293
295
|
const background = active ? blendHex(colors.bg, accent, desktop ? 0.32 : 0.45) : node.more ? undefined : blendHex(colors.bg, accent, 0.14);
|
|
294
|
-
const text = <
|
|
296
|
+
const text = <ClippedText fg={node.more ? colors.textDim : active ? colors.textBright : accent} text={flowChipLabel(node)} width={node.width - 2} />;
|
|
295
297
|
return <Box position="absolute" left={node.x} top={node.y} width={node.width} height={1} justifyContent={desktop ? "center" : undefined}
|
|
296
298
|
onMouseDown={onPress} onMouseOver={() => onHover(true)} onMouseOut={() => onHover(false)} data-gloom-role="supply-flow-chip">
|
|
297
299
|
{/* The desktop chip leaves a hairline of row above and below, like ticker chips. */}
|
|
298
|
-
{desktop ? <Box paddingX={1} backgroundColor={background} justifyContent="center" style={{ height: "calc(100% - 4px)", borderRadius: 3, cursor: "pointer" }}>{text}</Box>
|
|
300
|
+
{desktop ? <Box paddingX={1} minWidth={0} backgroundColor={background} justifyContent="center" style={{ height: "calc(100% - 4px)", borderRadius: 3, cursor: "pointer" }}>{text}</Box>
|
|
299
301
|
: <Box paddingX={1} height={1} backgroundColor={background}>{text}</Box>}
|
|
300
302
|
</Box>;
|
|
301
303
|
}
|
|
@@ -317,7 +319,7 @@ function FlowTooltip({ node, focusId, width, desktop, anchor }: { node: Position
|
|
|
317
319
|
flexDirection="column" paddingX={1} backgroundColor={colors.panel} border={!desktop} borderColor={ROLE_COLORS[row.role]}
|
|
318
320
|
style={desktop ? { border: `1px solid ${blendHex(colors.bg, ROLE_COLORS[row.role], 0.7)}`, borderRadius: 4, boxShadow: "0 6px 18px rgba(0,0,0,0.45)", zIndex: 20, pointerEvents: "none" } : undefined}
|
|
319
321
|
data-gloom-role="supply-flow-tooltip">
|
|
320
|
-
<
|
|
321
|
-
{lines.map((line, index) => <
|
|
322
|
+
<ClippedText fg={colors.textBright} attributes={TextAttributes.BOLD} text={counterpartyLabel(row)} width={tooltipWidth - 2} />
|
|
323
|
+
{lines.map((line, index) => <ClippedText key={index} fg={index === 0 ? colors.text : colors.textDim} text={line} width={tooltipWidth - 2} />)}
|
|
322
324
|
</Box>;
|
|
323
325
|
}
|
|
@@ -8,7 +8,8 @@ import { useThemeColors } from "../../../theme/theme-context";
|
|
|
8
8
|
import { useShortcut } from "../../../react/input";
|
|
9
9
|
import { useScrollBoxLayout } from "../../../components/use-scrollbox-layout";
|
|
10
10
|
import { isPlainKey } from "../../../utils/keyboard";
|
|
11
|
-
import { displayWidth
|
|
11
|
+
import { displayWidth } from "../../../utils/format";
|
|
12
|
+
import { ClippedText } from "../../../components/ui/display";
|
|
12
13
|
import { useStaticChartBitmapSize } from "../../../components/chart/composite/bitmap";
|
|
13
14
|
import { drawLine, fillOpaque, parseHex } from "../../../components/chart/native/raster/primitives";
|
|
14
15
|
import { entityLabel, graphNodes } from "./graph-model";
|
|
@@ -137,7 +138,8 @@ export function SupplyGraph({ data, width, height, focused, selectedId, selected
|
|
|
137
138
|
const tickerWidth = node.entity.ticker ? displayWidth(node.entity.ticker) + 3 : 0;
|
|
138
139
|
const showTicker = company && tickerWidth > 0 && inner >= tickerWidth + 5;
|
|
139
140
|
const prefix = collapsed.includes(node.entity.id) ? "+ " : "";
|
|
140
|
-
const name =
|
|
141
|
+
const name = prefix + (showTicker ? node.entity.name : entityLabel(node.entity));
|
|
142
|
+
const nameWidth = inner - (showTicker ? tickerWidth : 0);
|
|
141
143
|
const relation = kind ?? (node.related ? `${node.hops > 1 ? "via " : ""}${role}` : role);
|
|
142
144
|
const confidence = `${Math.round((node.path?.confidence ?? 0) * 100)}%`;
|
|
143
145
|
const detail = node.hops ? `${node.hops}H · ${relation}${inner >= displayWidth(relation ?? "") + 12 ? ` · ${confidence}` : ""}` : showTicker ? "Focus company" : node.entity.name;
|
|
@@ -150,10 +152,10 @@ export function SupplyGraph({ data, width, height, focused, selectedId, selected
|
|
|
150
152
|
<Box flexDirection="column" width="100%">
|
|
151
153
|
<Box flexDirection="row" height={1} gap={desktop ? 1 : 0} overflow="hidden">
|
|
152
154
|
{!desktop ? <Text fg={accent}>▍</Text> : null}
|
|
153
|
-
<
|
|
155
|
+
<ClippedText fg={active && !desktop ? colors.selectedText : company ? colors.textBright : colors.textDim} attributes={company ? TextAttributes.BOLD : 0} text={name} width={nameWidth} />
|
|
154
156
|
{showTicker ? desktop ? <Badge label={node.entity.ticker!} tone="accent" /> : <Text fg={active ? colors.selectedText : colors.textDim}>{` ${node.entity.ticker}`}</Text> : null}
|
|
155
157
|
</Box>
|
|
156
|
-
<Box height={1} paddingLeft={desktop ? 0 : 1} overflow="hidden"><
|
|
158
|
+
<Box height={1} paddingLeft={desktop ? 0 : 1} overflow="hidden"><ClippedText fg={active && !desktop ? colors.selectedText : colors.textDim} text={detail} width={inner} /></Box>
|
|
157
159
|
</Box>
|
|
158
160
|
</Button>
|
|
159
161
|
</Box>;
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
Badge, buildSectionedRows, DataTableView, EmptyState, isSectionedItemRow, PageStackView, renderSectionedRowHeader, PaneStatusBody, QueryBar, StatGrid, statGridRows, usePaneNoticeFooter,
|
|
9
9
|
usePaneStatusFooter, usePaneTabs, type DataTableCell, type DataTableColumn, type PaneHint, type SectionedRow, type StatItem,
|
|
10
10
|
} from "../../../components";
|
|
11
|
+
import { ClippedText } from "../../../components/ui/display";
|
|
11
12
|
import { RatioBar } from "../../../components/ui/ratio-bar";
|
|
12
13
|
import { getTableWidth } from "../../../components/ui/table-layout";
|
|
13
14
|
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
@@ -16,7 +17,7 @@ import { usePaneTickerIdentity } from "../../../state/hooks/pane-ticker";
|
|
|
16
17
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
17
18
|
import type { PaneProps } from "../../../types/plugin";
|
|
18
19
|
import { Box, Text, TextAttributes, useRendererHost, useUiCapabilities } from "../../../ui";
|
|
19
|
-
import { displayWidth
|
|
20
|
+
import { displayWidth } from "../../../utils/format";
|
|
20
21
|
import { SignInWall } from "../cloud/auth-actions";
|
|
21
22
|
import { CLOUD_PLAN_KEY, useCloudUpgradeAction } from "../shared/cloud-upgrade";
|
|
22
23
|
import { useOpenDescription } from "../shared/open-description";
|
|
@@ -241,13 +242,15 @@ function SupplyView({ symbol, width, height, focused }: Pick<PaneProps, "width"
|
|
|
241
242
|
case "name": {
|
|
242
243
|
const company = counterpartyKind(row) === "company";
|
|
243
244
|
// The kind label never gives way to a long cohort name; a column too narrow for the name says it in one word.
|
|
245
|
+
// The desktop always says it in one word: the Role column beside it names the role, and the name takes the room.
|
|
244
246
|
const full = counterpartyKindLabel(row);
|
|
245
|
-
const kind = full && displayWidth(counterpartyLabel(row)) + displayWidth(full) + 4 <= column.width ? full : counterpartyKindLabel(row, true);
|
|
247
|
+
const kind = !desktop && full && displayWidth(counterpartyLabel(row)) + displayWidth(full) + 4 <= column.width ? full : counterpartyKindLabel(row, true);
|
|
246
248
|
const ticker = !columns.some((entry) => entry.id === "ticker") ? row.counterparty.ticker : null;
|
|
247
249
|
const room = Math.max(8, column.width - (kind ? displayWidth(kind) + 4 : 0) - (ticker ? displayWidth(ticker) + 1 : 0));
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
250
|
+
// On the desktop the name gives way at the column's edge by CSS and the ticker and kind keep their room.
|
|
251
|
+
return { text: cellText(row, "name"), content: <Box flexDirection="row" height={1} gap={ticker ? 1 : 2} overflow="hidden" {...(desktop ? { flexShrink: 1, minWidth: 0 } : {})}>
|
|
252
|
+
<ClippedText fg={ink ?? (company ? colors.textBright : colors.textDim)} text={counterpartyLabel(row)} width={room} />
|
|
253
|
+
{ticker ? <Text fg={ink ?? colors.textDim} style={desktop ? { flex: "0 0 auto" } : undefined}>{ticker}</Text> : null}
|
|
251
254
|
{kind ? desktop ? <Badge label={kind} /> : <Text fg={ink ?? colors.textMuted}>{kind}</Text> : null}
|
|
252
255
|
</Box> };
|
|
253
256
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { FinancialStatement } from "../../../../types/financials";
|
|
1
|
+
import type { EarningsField, FinancialStatement } from "../../../../types/financials";
|
|
2
2
|
import { completeAvailability, statementFieldAvailability } from "../../../../utils/financial-statements";
|
|
3
|
+
import { hasStatementGap } from "../../../../utils/statement-gaps";
|
|
3
4
|
|
|
4
5
|
export type FinancialPeriod = "annual" | "quarterly";
|
|
5
6
|
|
|
@@ -91,6 +92,8 @@ const FLOW_KEYS = new Set<string>([
|
|
|
91
92
|
"effectOfExchangeRateChanges",
|
|
92
93
|
]);
|
|
93
94
|
|
|
95
|
+
const EARNINGS_KEYS = ["basicEps", "eps"] as const satisfies readonly EarningsField[];
|
|
96
|
+
|
|
94
97
|
const BALANCE_KEYS = new Set<string>([
|
|
95
98
|
"totalAssets",
|
|
96
99
|
"currentAssets",
|
|
@@ -195,7 +198,17 @@ function aggregateQuarterlyStatements(
|
|
|
195
198
|
const availableAt = completeAvailability(sources.map((source) => statementFieldAvailability(source, key)));
|
|
196
199
|
if (availableAt) availability[key] = availableAt;
|
|
197
200
|
};
|
|
201
|
+
// A quarter that declares an EPS line unavailable, or whose EPS share basis
|
|
202
|
+
// is unresolved, leaves the year's EPS unknown even when it still carries a
|
|
203
|
+
// number: the sum is withheld and the aggregate says why, as a quarter would.
|
|
204
|
+
const unresolvedEps = statements.find((statement) => statement.epsBasis?.status === "unresolved");
|
|
205
|
+
const gapped = EARNINGS_KEYS.filter((key) => statements.some((statement) => hasStatementGap(statement, key)));
|
|
206
|
+
if (gapped.length > 0) aggregate.unavailableEarnings = [...gapped];
|
|
207
|
+
if (unresolvedEps) aggregate.epsBasis = unresolvedEps.epsBasis;
|
|
208
|
+
const withheld = new Set<string>(gapped);
|
|
209
|
+
if (unresolvedEps) withheld.add("eps");
|
|
198
210
|
for (const key of FLOW_KEYS) {
|
|
211
|
+
if (withheld.has(key)) continue;
|
|
199
212
|
const values = statements
|
|
200
213
|
.map((statement) => (statement as unknown as Record<string, unknown>)[key])
|
|
201
214
|
.filter((value): value is number => typeof value === "number" && Number.isFinite(value));
|
|
@@ -63,8 +63,9 @@ interface RatioInputDef {
|
|
|
63
63
|
/** A flow over the period. Quarterly columns multiply it by four. */
|
|
64
64
|
annualized?: boolean;
|
|
65
65
|
/**
|
|
66
|
-
* A flow over a year: quarterly columns read
|
|
67
|
-
* (TTM), and show "< 4 quarters" rather than
|
|
66
|
+
* A flow over a year (leverage and valuation ratios): quarterly columns read
|
|
67
|
+
* the trailing four quarters (TTM), and show "< 4 quarters" rather than
|
|
68
|
+
* annualize a short history.
|
|
68
69
|
*/
|
|
69
70
|
trailing?: boolean;
|
|
70
71
|
/** A shorter name for quarterly columns, so the label, TTM and its unit fit the label column. */
|
|
@@ -189,7 +190,7 @@ const flow = (label: string, read: BalanceLine, operator?: RatioInputDef["operat
|
|
|
189
190
|
label, operator, format: "money", annualized: true, value: ({ statement }) => reported(read(statement)),
|
|
190
191
|
});
|
|
191
192
|
|
|
192
|
-
/** A flow a
|
|
193
|
+
/** A flow a ratio reads over a year: the trailing four quarters in a quarterly column. */
|
|
193
194
|
const trailingFlow = (
|
|
194
195
|
label: string,
|
|
195
196
|
read: BalanceLine,
|
|
@@ -474,7 +475,8 @@ export const RATIO_TABS: RatioTabDef[] = [
|
|
|
474
475
|
format: "multiple",
|
|
475
476
|
inputs: [
|
|
476
477
|
{ label: "Price", format: "perShare", value: ({ price }) => price },
|
|
477
|
-
|
|
478
|
+
// The summed quarters withhold EPS as a quarter does, when one has a gap or an unresolved basis.
|
|
479
|
+
{ label: "Diluted EPS", operator: "÷", format: "perShare", trailing: true, value: dilutedEps },
|
|
478
480
|
],
|
|
479
481
|
compute: ([price, eps]) => over(price!, eps!),
|
|
480
482
|
},
|
|
@@ -484,7 +486,7 @@ export const RATIO_TABS: RatioTabDef[] = [
|
|
|
484
486
|
format: "multiple",
|
|
485
487
|
inputs: [
|
|
486
488
|
{ label: "Enterprise Value", format: "money", value: enterpriseValue },
|
|
487
|
-
|
|
489
|
+
trailingFlow("EBITDA", line("ebitda"), "÷"),
|
|
488
490
|
],
|
|
489
491
|
// A negative enterprise value makes the multiple meaningless too.
|
|
490
492
|
compute: ([value, ebitda]) => (value! > 0 ? over(value!, ebitda!) : "not-meaningful"),
|
|
@@ -493,7 +495,7 @@ export const RATIO_TABS: RatioTabDef[] = [
|
|
|
493
495
|
id: "ps",
|
|
494
496
|
label: "P/S",
|
|
495
497
|
format: "multiple",
|
|
496
|
-
inputs: [{ label: "Market Cap", format: "money", value: marketCap },
|
|
498
|
+
inputs: [{ label: "Market Cap", format: "money", value: marketCap }, trailingFlow("Revenue", revenue, "÷")],
|
|
497
499
|
compute: ([cap, sales]) => over(cap!, sales!),
|
|
498
500
|
},
|
|
499
501
|
{
|
|
@@ -511,7 +513,7 @@ export const RATIO_TABS: RatioTabDef[] = [
|
|
|
511
513
|
label: "FCF Yield",
|
|
512
514
|
format: "percent",
|
|
513
515
|
signed: true,
|
|
514
|
-
inputs: [
|
|
516
|
+
inputs: [trailingFlow("Free Cash Flow", freeCashFlow), { label: "Market Cap", operator: "÷", format: "money", value: marketCap }],
|
|
515
517
|
compute: ([cash, cap]) => over(cash!, cap!),
|
|
516
518
|
},
|
|
517
519
|
],
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FINANCIAL_VINTAGE_NOTICE } from "../../../utils/financial-statements";
|
|
1
|
+
import { FINANCIAL_VINTAGE_NOTICE, statementHistoryNotes } from "../../../utils/financial-statements";
|
|
2
2
|
import { hasValidQuoteObservationTime } from "../../../market-data/quotes/freshness";
|
|
3
3
|
import { EXTENDED_SESSION_LABELS, getQuoteSessionFields, type ExtendedSession } from "../../../market-data/market/status";
|
|
4
4
|
import type { HeadlessPaneColumn, HeadlessPaneDefinition } from "../../../types/headless";
|
|
@@ -107,6 +107,8 @@ export const financialStatementsHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
|
107
107
|
return {
|
|
108
108
|
rows, columns, unavailableSymbols: rows.length ? [] : [symbol],
|
|
109
109
|
...(!hasRequestedStatements ? { errors: [`${symbol}: No ${requestedPeriod} financial statement coverage.`] } : {}),
|
|
110
|
+
// The report closes with how far back its periods go, and how to read further.
|
|
111
|
+
notes: statementHistoryNotes(requestedPeriod, dates.map(({ date }) => date), symbol),
|
|
110
112
|
metadata: {
|
|
111
113
|
symbol, name: financials.quote?.name ?? symbol, currency: statementCurrency ?? null, unit: table?.unit ?? null, quoteCurrency: financials.quote?.currency ?? null,
|
|
112
114
|
statement: table?.subTab.key ?? options.statement, statementLabel: table?.subTab.name ?? null,
|
|
@@ -177,6 +179,7 @@ async function loadRatioReport(
|
|
|
177
179
|
rows, columns, unavailableSymbols: rows.length ? [] : [symbol],
|
|
178
180
|
...(!hasRequestedStatements ? { errors: [`${symbol}: No ${period} financial statement coverage.`] } : {}),
|
|
179
181
|
...(tab.key === "valuation" && hasRequestedStatements && !history?.length ? { complete: false } : {}),
|
|
182
|
+
notes: statementHistoryNotes(period, dates.map(({ date }) => date), symbol),
|
|
180
183
|
metadata: {
|
|
181
184
|
symbol, name: financials.quote?.name ?? symbol, currency: statementCurrency ?? null,
|
|
182
185
|
quoteCurrency: financials.quote?.currency ?? null,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { ColumnGrid, ColumnGridPair, RangeTrack, usePaneLinkMenuEntry, type ColumnGridCell, type ColumnGridColumn } from "../../../../components";
|
|
3
3
|
import { t, tf } from "../../../../i18n";
|
|
4
4
|
import { formatMarketPriceWithCurrency, liveQuoteFormatOptions, type MarketFormatOptions } from "../../../../market-data/market/format";
|
|
5
5
|
import { colors, hoverBg, priceColor } from "../../../../theme/colors";
|
|
6
6
|
import type { Quote } from "../../../../types/financials";
|
|
7
|
-
import { Box, Text } from "../../../../ui";
|
|
8
|
-
import { displayWidth, formatNumber,
|
|
7
|
+
import { Box, Text, useUiCapabilities } from "../../../../ui";
|
|
8
|
+
import { displayWidth, formatNumber, truncateToDisplayWidth } from "../../../../utils/format";
|
|
9
9
|
import type { OverviewFunctionLink, PositionTableRow, StatField } from "./types";
|
|
10
10
|
import { portfolioPnlLabel } from "../../../../portfolio/position-metrics";
|
|
11
11
|
|
|
@@ -17,6 +17,8 @@ const RANGE_ENDPOINT_WIDTH = 11;
|
|
|
17
17
|
/** Shortest track worth drawing; below it the label shortens. */
|
|
18
18
|
const RANGE_MIN_TRACK_WIDTH = 6;
|
|
19
19
|
const POSITION_COLUMN_GAP = 1;
|
|
20
|
+
/** The account column never gets narrower than this. */
|
|
21
|
+
const ACCOUNT_MIN_WIDTH = 8;
|
|
20
22
|
|
|
21
23
|
interface PositionColumn {
|
|
22
24
|
key: keyof Omit<PositionTableRow, "pnlValue" | "pnlBasis" | "quantityUnit">;
|
|
@@ -105,27 +107,24 @@ export function CompactRangeBar({
|
|
|
105
107
|
);
|
|
106
108
|
}
|
|
107
109
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
value: string;
|
|
116
|
-
width: number;
|
|
117
|
-
valueColor: string;
|
|
118
|
-
}) {
|
|
119
|
-
return (
|
|
120
|
-
<Box flexDirection="row" height={1} width={width}>
|
|
121
|
-
<Text fg={colors.textDim}>{padTo(label, BOOK_LABEL_WIDTH)}</Text>
|
|
122
|
-
{/* Right-aligned, so a size changing length does not slide the price. */}
|
|
123
|
-
<Text fg={valueColor}>{padTo(value, Math.max(0, width - BOOK_LABEL_WIDTH), "right")}</Text>
|
|
124
|
-
</Box>
|
|
125
|
-
);
|
|
110
|
+
/** The quote book's columns: its labels, then the value right-aligned in the rest. */
|
|
111
|
+
function bookColumns(width: number): ColumnGridColumn[] {
|
|
112
|
+
return [
|
|
113
|
+
{ width: BOOK_LABEL_WIDTH },
|
|
114
|
+
// Right-aligned, so a size changing length does not slide the price.
|
|
115
|
+
{ width: Math.max(0, width - BOOK_LABEL_WIDTH), growFrom: 0, align: "right" },
|
|
116
|
+
];
|
|
126
117
|
}
|
|
127
118
|
|
|
128
|
-
export function QuoteBook({ quote, assetCategory, width }: {
|
|
119
|
+
export function QuoteBook({ quote, assetCategory, width, desktopWidth }: {
|
|
120
|
+
quote: Quote;
|
|
121
|
+
assetCategory?: string;
|
|
122
|
+
/** Cells on the terminal. */
|
|
123
|
+
width: number;
|
|
124
|
+
/** The desktop's width for the book, in CSS. */
|
|
125
|
+
desktopWidth?: string;
|
|
126
|
+
}) {
|
|
127
|
+
const { nativePaneChrome } = useUiCapabilities();
|
|
129
128
|
// Bid, ask and spread keep the instrument's decimals on every tick ($224.58 / $224.60 / $0.02).
|
|
130
129
|
const priceOptions = liveQuoteFormatOptions(quote, quote.currency, assetCategory);
|
|
131
130
|
const bidPrice = quote.bid != null ? formatMarketPriceWithCurrency(quote.bid, quote.currency, priceOptions) : "—";
|
|
@@ -140,12 +139,20 @@ export function QuoteBook({ quote, assetCategory, width }: { quote: Quote; asset
|
|
|
140
139
|
spreadText = `${formatMarketPriceWithCurrency(spread, quote.currency, priceOptions)}${spreadPercent}`;
|
|
141
140
|
}
|
|
142
141
|
|
|
142
|
+
const row = (label: string, value: string, color: string): ColumnGridCell[] => [{ text: label, color: colors.textDim }, { text: value, color }];
|
|
143
143
|
return (
|
|
144
|
-
<Box flexDirection="column" width={width} flexShrink={0}>
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
144
|
+
<Box flexDirection="column" width={nativePaneChrome ? undefined : width} flexShrink={0} style={nativePaneChrome ? { width: desktopWidth } : undefined}>
|
|
145
|
+
<ColumnGrid
|
|
146
|
+
columns={bookColumns(width)}
|
|
147
|
+
width={width}
|
|
148
|
+
role="quote-book"
|
|
149
|
+
rows={[
|
|
150
|
+
// Neutral: a red ask would read as a loss.
|
|
151
|
+
row(t("Bid"), bidText, colors.textBright),
|
|
152
|
+
row(t("Ask"), askText, colors.textBright),
|
|
153
|
+
row(t("Spr"), spreadText, colors.textDim),
|
|
154
|
+
]}
|
|
155
|
+
/>
|
|
149
156
|
</Box>
|
|
150
157
|
);
|
|
151
158
|
}
|
|
@@ -192,9 +199,12 @@ function LinkedStatCell({
|
|
|
192
199
|
}) {
|
|
193
200
|
// The pane menu lists each function once, so the keyboard reaches what a click opens.
|
|
194
201
|
usePaneLinkMenuEntry(`function:${link.templateId}`, tf("Open {label}", { label: t(link.name) }), () => onOpen(link));
|
|
202
|
+
const { nativePaneChrome } = useUiCapabilities();
|
|
195
203
|
return (
|
|
196
204
|
<Box
|
|
197
|
-
width={width}
|
|
205
|
+
width={nativePaneChrome ? undefined : width}
|
|
206
|
+
flexGrow={nativePaneChrome ? 1 : undefined}
|
|
207
|
+
style={nativePaneChrome ? { minWidth: 0 } : undefined}
|
|
198
208
|
flexDirection="row"
|
|
199
209
|
hoverBackgroundColor={hoverBg()}
|
|
200
210
|
onMouseDown={(event?: { button?: number }) => {
|
|
@@ -223,65 +233,64 @@ export function FundamentalsGrid({
|
|
|
223
233
|
/** Opens a figure's research function; without it figures are plain. */
|
|
224
234
|
onOpenLink?: (link: OverviewFunctionLink) => void;
|
|
225
235
|
}) {
|
|
236
|
+
const { nativePaneChrome } = useUiCapabilities();
|
|
226
237
|
const columnCount = columns ?? fundamentalsGridColumns(width);
|
|
227
238
|
const availableWidth = width - STAT_COLUMN_GAP * (columnCount - 1);
|
|
228
239
|
const baseColWidth = Math.floor(availableWidth / columnCount);
|
|
229
240
|
const extraWidth = availableWidth - baseColWidth * columnCount;
|
|
230
241
|
const colWidths = Array.from({ length: columnCount }, (_, index) => baseColWidth + (index === columnCount - 1 ? extraWidth : 0));
|
|
231
|
-
|
|
242
|
+
// Equal columns on the desktop too, sharing the pane's width there.
|
|
243
|
+
// A column never gets narrower than its labels, so a label cannot reach the next column.
|
|
244
|
+
const gridColumns: ColumnGridColumn[] = colWidths.map((colWidth, index) => ({
|
|
245
|
+
width: colWidth,
|
|
246
|
+
growFrom: gridLabelWidth(colWidth),
|
|
247
|
+
gapBefore: index > 0 ? STAT_COLUMN_GAP : 0,
|
|
248
|
+
}));
|
|
249
|
+
const rows: ColumnGridCell[][] = [];
|
|
232
250
|
for (let i = 0; i < fields.length; i += columnCount) {
|
|
233
|
-
rows.push(Array.from({ length: columnCount }, (_, offset) =>
|
|
251
|
+
rows.push(Array.from({ length: columnCount }, (_, offset): ColumnGridCell => {
|
|
252
|
+
const field = fields[i + offset];
|
|
253
|
+
if (!field) return null;
|
|
254
|
+
const colWidth = colWidths[offset] ?? baseColWidth;
|
|
255
|
+
const labelWidth = gridLabelWidth(colWidth);
|
|
256
|
+
const valueWidth = Math.max(1, colWidth - labelWidth);
|
|
257
|
+
// A long value (an industry, a SIC title) is cut from the end, never from the label side.
|
|
258
|
+
const value = truncateToDisplayWidth(field.value, valueWidth);
|
|
259
|
+
const detailRoom = valueWidth - displayWidth(value) - 1;
|
|
260
|
+
const details = typeof field.detail === "string" ? [field.detail] : field.detail ?? [];
|
|
261
|
+
// A cut "in 4..." misleads; a title cut to "Semicond..." still names it.
|
|
262
|
+
const detail = details.find((text) => displayWidth(text) <= detailRoom)
|
|
263
|
+
?? (field.clipDetail && details[0] && detailRoom >= 4 ? truncateToDisplayWidth(details[0], detailRoom) : "");
|
|
264
|
+
const content = (
|
|
265
|
+
<ColumnGridPair
|
|
266
|
+
label={t(field.label)}
|
|
267
|
+
labelWidth={labelWidth}
|
|
268
|
+
width={colWidth}
|
|
269
|
+
value={field.value}
|
|
270
|
+
valueColor={field.valueColor}
|
|
271
|
+
detail={detail || undefined}
|
|
272
|
+
url={field.url}
|
|
273
|
+
/>
|
|
274
|
+
);
|
|
275
|
+
return {
|
|
276
|
+
content: field.link && onOpenLink
|
|
277
|
+
? <LinkedStatCell link={field.link} width={colWidth} onOpen={onOpenLink}>{content}</LinkedStatCell>
|
|
278
|
+
: (
|
|
279
|
+
<Box
|
|
280
|
+
width={nativePaneChrome ? undefined : colWidth}
|
|
281
|
+
flexGrow={nativePaneChrome ? 1 : undefined}
|
|
282
|
+
flexDirection="row"
|
|
283
|
+
data-gloom-ui="key-value-row"
|
|
284
|
+
style={nativePaneChrome ? { minWidth: 0 } : undefined}
|
|
285
|
+
>
|
|
286
|
+
{content}
|
|
287
|
+
</Box>
|
|
288
|
+
),
|
|
289
|
+
};
|
|
290
|
+
}));
|
|
234
291
|
}
|
|
235
292
|
|
|
236
|
-
return
|
|
237
|
-
<Box flexDirection="column">
|
|
238
|
-
{rows.map((row, i) => (
|
|
239
|
-
<Box key={i} flexDirection="row" height={1}>
|
|
240
|
-
{row.map((field, j) => {
|
|
241
|
-
const colWidth = colWidths[j] ?? baseColWidth;
|
|
242
|
-
if (!field) {
|
|
243
|
-
return (
|
|
244
|
-
<Box key={j} flexDirection="row">
|
|
245
|
-
{j > 0 && <Box width={STAT_COLUMN_GAP} />}
|
|
246
|
-
<Box width={colWidth} />
|
|
247
|
-
</Box>
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
const labelWidth = gridLabelWidth(colWidth);
|
|
251
|
-
const valueWidth = Math.max(1, colWidth - labelWidth);
|
|
252
|
-
// A long value (an industry, a SIC title) is cut from the end, never from the label side.
|
|
253
|
-
const value = truncateToDisplayWidth(field.value, valueWidth);
|
|
254
|
-
const detailRoom = valueWidth - displayWidth(value) - 1;
|
|
255
|
-
const details = typeof field.detail === "string" ? [field.detail] : field.detail ?? [];
|
|
256
|
-
// A cut "in 4..." misleads; a title cut to "Semicond..." still names it.
|
|
257
|
-
const detail = details.find((text) => displayWidth(text) <= detailRoom)
|
|
258
|
-
?? (field.clipDetail && details[0] && detailRoom >= 4 ? truncateToDisplayWidth(details[0], detailRoom) : "");
|
|
259
|
-
const content = (
|
|
260
|
-
<>
|
|
261
|
-
<Box width={labelWidth} overflow="hidden">
|
|
262
|
-
<Text fg={colors.textDim}>{t(field.label)}</Text>
|
|
263
|
-
</Box>
|
|
264
|
-
<Box flexDirection="row" width={valueWidth} justifyContent="flex-end" overflow="hidden">
|
|
265
|
-
{field.url
|
|
266
|
-
? <ExternalLinkText url={field.url} label={value} color={field.valueColor ?? colors.text} />
|
|
267
|
-
: <Text fg={field.valueColor ?? colors.text}>{value}</Text>}
|
|
268
|
-
{detail ? <Text fg={colors.textDim}>{` ${detail}`}</Text> : null}
|
|
269
|
-
</Box>
|
|
270
|
-
</>
|
|
271
|
-
);
|
|
272
|
-
return (
|
|
273
|
-
<Box key={j} flexDirection="row">
|
|
274
|
-
{j > 0 && <Box width={STAT_COLUMN_GAP} />}
|
|
275
|
-
{field.link && onOpenLink
|
|
276
|
-
? <LinkedStatCell link={field.link} width={colWidth} onOpen={onOpenLink}>{content}</LinkedStatCell>
|
|
277
|
-
: <Box width={colWidth} flexDirection="row" data-gloom-ui="key-value-row">{content}</Box>}
|
|
278
|
-
</Box>
|
|
279
|
-
);
|
|
280
|
-
})}
|
|
281
|
-
</Box>
|
|
282
|
-
))}
|
|
283
|
-
</Box>
|
|
284
|
-
);
|
|
293
|
+
return <ColumnGrid columns={gridColumns} rows={rows} role="fundamentals-grid" />;
|
|
285
294
|
}
|
|
286
295
|
|
|
287
296
|
const POSITION_COLUMNS: readonly PositionColumn[] = [
|
|
@@ -301,36 +310,28 @@ function createPositionColumns(width: number, hasFaceQuantity: boolean): Positio
|
|
|
301
310
|
}));
|
|
302
311
|
const fixedWidth = columns.reduce((sum, column) => sum + column.width, 0) + POSITION_COLUMN_GAP * (columns.length - 1);
|
|
303
312
|
const accountColumn = columns[0]!;
|
|
304
|
-
accountColumn.width = Math.max(
|
|
313
|
+
accountColumn.width = Math.max(ACCOUNT_MIN_WIDTH, width - fixedWidth);
|
|
305
314
|
return columns;
|
|
306
315
|
}
|
|
307
316
|
|
|
308
317
|
export function PositionTable({ rows, width }: { rows: PositionTableRow[]; width: number }) {
|
|
309
318
|
const columns = createPositionColumns(width, rows.some(row => row.quantityUnit === "face"));
|
|
310
319
|
const pnlLabel = portfolioPnlLabel(rows.map((row) => row.pnlBasis));
|
|
320
|
+
const gridColumns: ColumnGridColumn[] = columns.map((column, index) => ({
|
|
321
|
+
width: column.width,
|
|
322
|
+
align: column.align,
|
|
323
|
+
gapBefore: index > 0 ? POSITION_COLUMN_GAP : 0,
|
|
324
|
+
// The account takes what the figures leave, on the desktop too.
|
|
325
|
+
...(column.key === "account" ? { growFrom: ACCOUNT_MIN_WIDTH } : {}),
|
|
326
|
+
}));
|
|
327
|
+
const header: ColumnGridCell[] = columns.map((column) => ({
|
|
328
|
+
text: t(column.key === "pnl" ? pnlLabel : column.label),
|
|
329
|
+
color: colors.textDim,
|
|
330
|
+
}));
|
|
331
|
+
const body = rows.map((row): ColumnGridCell[] => columns.map((column) => ({
|
|
332
|
+
text: row[column.key],
|
|
333
|
+
color: column.color?.(row) ?? (column.key === "account" ? colors.textBright : colors.text),
|
|
334
|
+
})));
|
|
311
335
|
|
|
312
|
-
return
|
|
313
|
-
<Box flexDirection="column" width={width}>
|
|
314
|
-
<Box flexDirection="row" height={1}>
|
|
315
|
-
{columns.map((column, index) => (
|
|
316
|
-
<Box key={column.key} flexDirection="row">
|
|
317
|
-
{index > 0 && <Box width={POSITION_COLUMN_GAP} />}
|
|
318
|
-
<Text fg={colors.textDim}>{padTo(t(column.key === "pnl" ? pnlLabel : column.label), column.width, column.align)}</Text>
|
|
319
|
-
</Box>
|
|
320
|
-
))}
|
|
321
|
-
</Box>
|
|
322
|
-
{rows.map((row, rowIndex) => (
|
|
323
|
-
<Box key={rowIndex} flexDirection="row" height={1}>
|
|
324
|
-
{columns.map((column, index) => (
|
|
325
|
-
<Box key={column.key} flexDirection="row">
|
|
326
|
-
{index > 0 && <Box width={POSITION_COLUMN_GAP} />}
|
|
327
|
-
<Text fg={column.color?.(row) ?? (column.key === "account" ? colors.textBright : colors.text)}>
|
|
328
|
-
{padTo(row[column.key], column.width, column.align)}
|
|
329
|
-
</Text>
|
|
330
|
-
</Box>
|
|
331
|
-
))}
|
|
332
|
-
</Box>
|
|
333
|
-
))}
|
|
334
|
-
</Box>
|
|
335
|
-
);
|
|
336
|
+
return <ColumnGrid columns={gridColumns} rows={[header, ...body]} width={width} role="position-table" />;
|
|
336
337
|
}
|
|
@@ -23,7 +23,7 @@ import { getPortfolioPositionMetrics, getPortfolioQuoteDisplay, resolvePortfolio
|
|
|
23
23
|
import { liveDividendYield, liveForwardPE, liveMarketCapitalization, liveTrailingPE } from "../../portfolio-list/live-valuation";
|
|
24
24
|
import { atRegularSessionPrice } from "../../../../market-data/quotes/regular-session-valuation";
|
|
25
25
|
import { formatReportedMoney } from "../../../../utils/reported-money";
|
|
26
|
-
import { fundamentalsCurrency, fundamentalsEpsCurrency, reportedEnterpriseValue } from "../../../../utils/fundamentals";
|
|
26
|
+
import { enterpriseValueCheck, fundamentalsCurrency, fundamentalsEpsCurrency, reportedEnterpriseValue } from "../../../../utils/fundamentals";
|
|
27
27
|
import { formatShortDate } from "../../../../utils/datetime-format";
|
|
28
28
|
import { safeExternalUrl } from "../../../../utils/external-url";
|
|
29
29
|
|
|
@@ -188,7 +188,14 @@ export function buildOverviewStats({
|
|
|
188
188
|
if (fundamentals?.unavailableFields?.includes("enterpriseValue")) {
|
|
189
189
|
stats.push({ label: "EV", value: "—" });
|
|
190
190
|
} else if (enterpriseValue != null) {
|
|
191
|
-
|
|
191
|
+
// More than 5% from market cap + debt - cash: the provider's, with the computed figure where it fits.
|
|
192
|
+
const check = enterpriseValueCheck(fundamentals);
|
|
193
|
+
const computed = check?.status === "differs" && check.computed != null ? formatCompact(check.computed) : undefined;
|
|
194
|
+
stats.push({
|
|
195
|
+
label: "EV",
|
|
196
|
+
value: formatCompactCurrency(enterpriseValue, quoteCurrency),
|
|
197
|
+
detail: computed ? [`provider vs ${computed}`, "provider"] : undefined,
|
|
198
|
+
});
|
|
192
199
|
}
|
|
193
200
|
if (finite(fundamentals?.beta)) {
|
|
194
201
|
stats.push({ label: "Beta", value: formatNumber(fundamentals.beta, 2), link: RELATIONSHIP_GRAPH });
|