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.
Files changed (201) hide show
  1. package/package.json +1 -1
  2. package/src/{plugins/builtin/cloud → account}/auth-dialog.tsx +7 -7
  3. package/src/api-client/data.ts +5 -1
  4. package/src/api-client/doe.ts +19 -0
  5. package/src/api-client/earnings.ts +4 -2
  6. package/src/api-client/index.ts +1 -0
  7. package/src/app/global-shortcuts.ts +10 -0
  8. package/src/app/keybindings/actions.ts +16 -0
  9. package/src/app/runtime/desktop-deeplink.ts +1 -1
  10. package/src/app/runtime/update.ts +16 -0
  11. package/src/app.tsx +1 -1
  12. package/src/architecture/core-builtin-allowlist.json +4 -9
  13. package/src/architecture/dom-cell-metrics-allowlist.json +100 -26
  14. package/src/cli/commands/market.ts +87 -17
  15. package/src/cli/commands/options-chain.ts +93 -23
  16. package/src/cli/commands/options-leaps.ts +462 -63
  17. package/src/cli/commands/search.ts +10 -7
  18. package/src/cli/commands/ticker.ts +19 -10
  19. package/src/cli/glossary.ts +1 -1
  20. package/src/cli/index.ts +14 -6
  21. package/src/cli/known-gaps.ts +22 -0
  22. package/src/cli/pane-functions/catalog.ts +45 -10
  23. package/src/cli/pane-functions/dom.ts +36 -10
  24. package/src/cli/pane-functions/function-help.ts +4 -2
  25. package/src/cli/pane-functions/headless.ts +12 -1
  26. package/src/cli/pane-functions/index.ts +44 -3
  27. package/src/cli/pane-functions/lock-note.ts +56 -0
  28. package/src/cli/pane-functions/report-notices.ts +15 -0
  29. package/src/cli/symbol-args.ts +19 -0
  30. package/src/components/command-bar/list/view.tsx +2 -1
  31. package/src/components/command-bar/workflow/builtin.ts +2 -2
  32. package/src/components/command-bar/workflow/collection-actions.ts +1 -1
  33. package/src/components/feedback-dialog.tsx +1 -1
  34. package/src/components/index.ts +3 -2
  35. package/src/components/layout/header.tsx +58 -14
  36. package/src/components/layout/shell/chrome.ts +10 -4
  37. package/src/components/layout/shell/index.tsx +1 -1
  38. package/src/components/layout/status-bar-groups.ts +1 -1
  39. package/src/components/layout/status-bar.tsx +2 -2
  40. package/src/components/markdown-text.tsx +14 -2
  41. package/src/components/reader/article-reader.tsx +1 -1
  42. package/src/components/reader/detail-reader.tsx +10 -7
  43. package/src/components/reader/reader-text.tsx +32 -17
  44. package/src/components/ui/column-grid.tsx +206 -0
  45. package/src/components/ui/display.tsx +40 -3
  46. package/src/components/ui/fill.ts +8 -0
  47. package/src/components/ui/index.ts +2 -0
  48. package/src/layout-marketplace/gallery.tsx +2 -2
  49. package/src/layout-marketplace/linked-sync.ts +1 -1
  50. package/src/market-data/market/freshness.ts +3 -0
  51. package/src/market-data/published-apac-sessions.ts +12 -2
  52. package/src/market-data/quotes/screener-live-quotes.ts +31 -7
  53. package/src/plugins/builtin/account-management/form-components.tsx +4 -7
  54. package/src/plugins/builtin/account-management/password-dialog.tsx +3 -2
  55. package/src/plugins/builtin/backtest/pane.tsx +9 -3
  56. package/src/plugins/builtin/buildout/detail/company.tsx +4 -3
  57. package/src/plugins/builtin/buildout/detail/ui.tsx +9 -12
  58. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  59. package/src/plugins/builtin/chart-composer/presets.ts +33 -1
  60. package/src/plugins/builtin/chat/channel-labels.ts +2 -2
  61. package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -1
  62. package/src/plugins/builtin/chat/content/index.tsx +2 -2
  63. package/src/plugins/builtin/chat/conversations.ts +1 -1
  64. package/src/plugins/builtin/chat/index.tsx +2 -2
  65. package/src/plugins/builtin/chat/share/flow.tsx +1 -1
  66. package/src/plugins/builtin/chat/sidebar-rows.ts +1 -1
  67. package/src/plugins/builtin/chat/sidebar.tsx +2 -2
  68. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +2 -2
  69. package/src/plugins/builtin/cloud/askg/pane.tsx +2 -1
  70. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +3 -2
  71. package/src/plugins/builtin/cloud/auth-actions.tsx +1 -1
  72. package/src/plugins/builtin/cloud/auth-commands.ts +1 -1
  73. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -1
  74. package/src/plugins/builtin/cloud/team/acm-tab.tsx +3 -3
  75. package/src/plugins/builtin/cloud/team/capability.ts +2 -2
  76. package/src/plugins/builtin/cloud/team/command.ts +4 -4
  77. package/src/plugins/builtin/cloud/team/module.tsx +3 -3
  78. package/src/plugins/builtin/cloud/team/pane-model.ts +2 -2
  79. package/src/plugins/builtin/cloud/team/pane-sections.tsx +1 -1
  80. package/src/plugins/builtin/cloud/team/pane-ui.tsx +1 -1
  81. package/src/plugins/builtin/cloud/team/pane.tsx +3 -3
  82. package/src/plugins/builtin/cloud/team/status-widget.tsx +3 -3
  83. package/src/plugins/builtin/cloud/team/team-state-host.ts +1 -1
  84. package/src/plugins/builtin/cloud/team/views.ts +2 -2
  85. package/src/plugins/builtin/cloud/thesis/board-pane.tsx +1 -1
  86. package/src/plugins/builtin/cloud/thesis/detail.tsx +12 -10
  87. package/src/plugins/builtin/cloud/thesis/ticker-tab.tsx +1 -1
  88. package/src/plugins/builtin/cloud/upgrade-command.ts +1 -1
  89. package/src/plugins/builtin/congress-trades/detail.tsx +8 -4
  90. package/src/plugins/builtin/connections/pane.tsx +16 -5
  91. package/src/plugins/builtin/cpi/pane.tsx +5 -4
  92. package/src/plugins/builtin/custom-view/pane.tsx +2 -2
  93. package/src/plugins/builtin/dividend-yield/headless.ts +8 -2
  94. package/src/plugins/builtin/dividend-yield/pane.tsx +4 -2
  95. package/src/plugins/builtin/docs/blocks.tsx +13 -2
  96. package/src/plugins/builtin/docs/pane.tsx +6 -5
  97. package/src/plugins/builtin/doe/index.tsx +11 -4
  98. package/src/plugins/builtin/doe/pane.tsx +5 -4
  99. package/src/plugins/builtin/doe/prices.ts +84 -0
  100. package/src/plugins/builtin/doe/series.ts +4 -1
  101. package/src/plugins/builtin/earnings/board-model.ts +18 -1
  102. package/src/plugins/builtin/earnings/board-table.ts +18 -3
  103. package/src/plugins/builtin/earnings/headless.ts +13 -2
  104. package/src/plugins/builtin/earnings/history-model.ts +1 -1
  105. package/src/plugins/builtin/earnings/history-table.ts +2 -2
  106. package/src/plugins/builtin/earnings-calls/headless.ts +7 -21
  107. package/src/plugins/builtin/earnings-calls/index.tsx +2 -0
  108. package/src/plugins/builtin/earnings-calls/lock-note.ts +17 -0
  109. package/src/plugins/builtin/equity-screener/headless.ts +59 -1
  110. package/src/plugins/builtin/equity-screener/live.ts +82 -31
  111. package/src/plugins/builtin/equity-screener/pane.tsx +60 -23
  112. package/src/plugins/builtin/estimate-revisions/client.ts +1 -1
  113. package/src/plugins/builtin/estimate-revisions/headless.ts +9 -2
  114. package/src/plugins/builtin/estimate-revisions/index.tsx +3 -0
  115. package/src/plugins/builtin/estimate-revisions/lock-note.ts +23 -0
  116. package/src/plugins/builtin/executives/index.tsx +18 -0
  117. package/src/plugins/builtin/exposure/charts.tsx +4 -3
  118. package/src/plugins/builtin/filing-events/index.tsx +8 -10
  119. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  120. package/src/plugins/builtin/gpu/equities.tsx +9 -6
  121. package/src/plugins/builtin/help/feedback-tab.tsx +1 -1
  122. package/src/plugins/builtin/iv-history/format.ts +3 -0
  123. package/src/plugins/builtin/iv-history/headless.ts +3 -1
  124. package/src/plugins/builtin/iv-history/index.tsx +3 -1
  125. package/src/plugins/builtin/iv-history/model.ts +3 -1
  126. package/src/plugins/builtin/iv-history/screen-pane.tsx +3 -1
  127. package/src/plugins/builtin/iv-history/universe.ts +1 -1
  128. package/src/plugins/builtin/jobs/headless.ts +6 -20
  129. package/src/plugins/builtin/jobs/index.tsx +2 -0
  130. package/src/plugins/builtin/jobs/lock-note.ts +16 -0
  131. package/src/plugins/builtin/notes/index.tsx +1 -1
  132. package/src/plugins/builtin/notes/owner.tsx +2 -2
  133. package/src/plugins/builtin/options/carry.ts +36 -0
  134. package/src/plugins/builtin/options/index.tsx +2 -1
  135. package/src/plugins/builtin/options/model-spot.ts +94 -0
  136. package/src/plugins/builtin/options/open-interest-as-of.ts +74 -0
  137. package/src/plugins/builtin/options/view.tsx +69 -11
  138. package/src/plugins/builtin/options-calculator/pane.tsx +56 -1
  139. package/src/plugins/builtin/options-calculator/premium-history.ts +34 -0
  140. package/src/plugins/builtin/options-scenario/client.ts +33 -9
  141. package/src/plugins/builtin/options-scenario/csp.ts +168 -0
  142. package/src/plugins/builtin/options-scenario/evidence.ts +3 -0
  143. package/src/plugins/builtin/options-scenario/headless.ts +24 -8
  144. package/src/plugins/builtin/options-scenario/index.tsx +3 -1
  145. package/src/plugins/builtin/options-scenario/pane.tsx +32 -6
  146. package/src/plugins/builtin/portfolio-list/mutations.ts +1 -1
  147. package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
  148. package/src/plugins/builtin/research/corporate-actions-pane.tsx +4 -2
  149. package/src/plugins/builtin/research/event-model.ts +7 -4
  150. package/src/plugins/builtin/risk-factors/index.tsx +18 -0
  151. package/src/plugins/builtin/risk-factors/pane.tsx +2 -1
  152. package/src/plugins/builtin/scanner/headless.ts +9 -2
  153. package/src/plugins/builtin/scanner/index.ts +3 -0
  154. package/src/plugins/builtin/scanner/lock-note.ts +17 -0
  155. package/src/plugins/builtin/series-inventory.ts +2 -1
  156. package/src/plugins/builtin/shared/cloud-denied.ts +39 -8
  157. package/src/plugins/builtin/shared/earnings-report.ts +10 -4
  158. package/src/plugins/builtin/shared/pro-lock-note.ts +58 -0
  159. package/src/plugins/builtin/shared/series-list-detail.tsx +10 -5
  160. package/src/plugins/builtin/social-mentions/client.ts +3 -3
  161. package/src/plugins/builtin/social-mentions/headless.ts +6 -2
  162. package/src/plugins/builtin/social-mentions/index.tsx +2 -1
  163. package/src/plugins/builtin/social-mentions/lock-note.ts +15 -0
  164. package/src/plugins/builtin/supply-chain/flow.tsx +15 -13
  165. package/src/plugins/builtin/supply-chain/graph.tsx +6 -4
  166. package/src/plugins/builtin/supply-chain/pane.tsx +8 -5
  167. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +14 -1
  168. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +9 -7
  169. package/src/plugins/builtin/ticker-detail/headless.ts +4 -1
  170. package/src/plugins/builtin/ticker-detail/overview/components.tsx +105 -104
  171. package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -2
  172. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +34 -8
  173. package/src/plugins/builtin/vol-surface/pane.tsx +6 -3
  174. package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
  175. package/src/renderers/dom/data-table/index.tsx +48 -15
  176. package/src/renderers/dom/data-table/row.tsx +1 -1
  177. package/src/renderers/dom/desktop/popover.tsx +6 -15
  178. package/src/renderers/dom/host/input.tsx +27 -3
  179. package/src/renderers/dom/host/surface-3d.tsx +6 -4
  180. package/src/renderers/dom/styles.css +26 -6
  181. package/src/renderers/electrobun/view/app-services.ts +2 -22
  182. package/src/renderers/electrobun/view/news-source.ts +42 -0
  183. package/src/series/validate.ts +5 -0
  184. package/src/shares/dialog.tsx +2 -1
  185. package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
  186. package/src/sources/provider-router/financials.ts +5 -0
  187. package/src/{plugins/builtin/cloud/team → team}/collections-sync.ts +6 -6
  188. package/src/{plugins/builtin/cloud/team → team}/collections.ts +3 -3
  189. package/src/{plugins/builtin/cloud/team → team}/model.ts +2 -2
  190. package/src/{plugins/builtin/cloud/team → team}/pane-request.ts +1 -1
  191. package/src/{plugins/builtin/cloud/team → team}/store.ts +3 -3
  192. package/src/types/data-provider.ts +3 -0
  193. package/src/types/financials.ts +28 -0
  194. package/src/types/plugin.ts +17 -4
  195. package/src/utils/decimal-comma.ts +18 -0
  196. package/src/utils/exchanges.ts +7 -0
  197. package/src/utils/financial-statements.ts +25 -0
  198. package/src/utils/forward-dividend.ts +39 -9
  199. package/src/utils/fundamentals.ts +41 -1
  200. package/src/utils/option-expiry.ts +16 -0
  201. package/src/utils/sec.ts +2 -1
@@ -1,6 +1,7 @@
1
1
  import { aggregateLoadedCongress } from "./aggregates";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
- import { Box, ScrollBox, Text, useRendererHost, type ScrollBoxRenderable } from "../../../ui";
3
+ import { Box, ScrollBox, Text, useRendererHost, useUiCapabilities, type ScrollBoxRenderable } from "../../../ui";
4
+ import { ClippedText } from "../../../components/ui/display";
4
5
  import {
5
6
  DataTableView,
6
7
  KeyValueRow,
@@ -51,6 +52,9 @@ export function TradeDetail({
51
52
  width: number;
52
53
  }) {
53
54
  const lineWidth = Math.max(1, width - 2);
55
+ // The desktop's rows cut long values where they end, by CSS; the terminal at its columns.
56
+ const desktop = !!useUiCapabilities().nativePaneChrome;
57
+ const fit = (value: string) => desktop ? value : truncateWithEllipsis(value, Math.max(10, lineWidth - 16));
54
58
  return (
55
59
  <ScrollBox scrollY focusable={false} flexGrow={1} paddingX={1}>
56
60
  <Box flexDirection="column" width={lineWidth}>
@@ -58,7 +62,7 @@ export function TradeDetail({
58
62
  <KeyValueRow {...DETAIL_ROW} label="chamber" value={`${trade.chamber === "senate" ? "Senate" : "House"}${trade.stateDistrict ? ` · ${trade.stateDistrict}` : ""}`} tone="muted" />
59
63
  <KeyValueRow {...DETAIL_ROW} label="party" value={trade.party ?? "--"} />
60
64
  <KeyValueRow {...DETAIL_ROW} label="side" value={trade.transactionType} tone={trade.side === "BUY" ? "positive" : trade.side === "SELL" ? "negative" : trade.side === "OTHER" ? "muted" : undefined} />
61
- <KeyValueRow {...DETAIL_ROW} label="asset" value={truncateWithEllipsis(trade.assetName, Math.max(10, lineWidth - 16))} />
65
+ <KeyValueRow {...DETAIL_ROW} label="asset" value={fit(trade.assetName)} />
62
66
  <KeyValueRow {...DETAIL_ROW} label="amount" value={trade.amount} color={colors.textBright} />
63
67
  <KeyValueRow {...DETAIL_ROW} label="owner" value={trade.owner} />
64
68
  <KeyValueRow {...DETAIL_ROW} label="tx return" value={formatCongressReturn(trade.returnSinceTx)} />
@@ -69,12 +73,12 @@ export function TradeDetail({
69
73
  <KeyValueRow {...DETAIL_ROW} label="filed" value={trade.filingDate} />
70
74
  <KeyValueRow {...DETAIL_ROW} label="lag" value={`${formatLag(trade.lagDays)}${(trade.lagDays ?? 0) > 45 ? " (past 45 days)" : ""}`} tone={(trade.lagDays ?? 0) > 45 ? "warning" : undefined} />
71
75
  {trade.filingStatus ? <KeyValueRow {...DETAIL_ROW} label="status" value={trade.filingStatus} /> : null}
72
- {trade.subholdingOf ? <KeyValueRow {...DETAIL_ROW} label="subholding" value={truncateWithEllipsis(trade.subholdingOf, Math.max(10, lineWidth - 16))} /> : null}
76
+ {trade.subholdingOf ? <KeyValueRow {...DETAIL_ROW} label="subholding" value={fit(trade.subholdingOf)} /> : null}
73
77
  {trade.description ? (
74
78
  <>
75
79
  <Text>{" "}</Text>
76
80
  <Text fg={colors.textDim}>description</Text>
77
- <Text fg={colors.text}>{truncateWithEllipsis(trade.description, lineWidth)}</Text>
81
+ <ClippedText fg={colors.text} text={trade.description} width={lineWidth} cut={truncateWithEllipsis} wrap />
78
82
  </>
79
83
  ) : null}
80
84
  </Box>
@@ -9,7 +9,8 @@ import type {
9
9
  } from "../../../core/connection-health";
10
10
  import { colors } from "../../../theme/colors";
11
11
  import type { PaneProps } from "../../../types/plugin";
12
- import { Box, ScrollBox, Text } from "../../../ui";
12
+ import { Box, ScrollBox, Text, useUiCapabilities } from "../../../ui";
13
+ import { ClippedText } from "../../../components/ui/display";
13
14
  import { truncateToDisplayWidth } from "../../../utils/format";
14
15
  import { formatRelativeAge } from "../../../utils/datetime-format";
15
16
  import { nextHeaderSort } from "../../../utils/sort-values";
@@ -29,6 +30,15 @@ const STATUS_ORDER: Record<ConnectionHealthStatus, number> = {
29
30
  idle: 4,
30
31
  };
31
32
 
33
+ function connectionKindLabel(kind: string): string {
34
+ if (kind === "asset-data") return "Market data";
35
+ if (kind === "news") return "News";
36
+ if (kind === "api") return "Request";
37
+ if (kind === "websocket") return "Live feed";
38
+ if (kind === "capability") return "Plugin";
39
+ return "Connection";
40
+ }
41
+
32
42
  function statusLabel(status: ConnectionHealthStatus): string {
33
43
  if (status === "connected") return "Connected";
34
44
  if (status === "connecting") return "Connecting";
@@ -64,12 +74,13 @@ function columnsForWidth(width: number): ConnectionColumn[] {
64
74
 
65
75
  function ConnectionDetail({ source, width, now }: { source: ConnectionHealthState; width: number; now: number }) {
66
76
  const lineWidth = Math.max(24, width - 2);
77
+ // The desktop's detail row cuts a long value where it ends, by CSS.
78
+ const desktop = !!useUiCapabilities().nativePaneChrome;
67
79
  return (
68
80
  <ScrollBox scrollY focusable={false} flexGrow={1} paddingX={1}>
69
81
  <Box flexDirection="column" width={lineWidth}>
70
82
  <KeyValueRow labelWidth={16} emphasis={false} label="Status" value={statusLabel(source.status)} color={statusColor(source.status)} />
71
- <KeyValueRow labelWidth={16} emphasis={false} label="Type" value={source.kind} color={colors.textDim} />
72
- {source.ownerId ? <KeyValueRow labelWidth={16} emphasis={false} label="Owner" value={source.ownerId} color={colors.textDim} /> : null}
83
+ <KeyValueRow labelWidth={16} emphasis={false} label="Type" value={connectionKindLabel(source.kind)} color={colors.textDim} />
73
84
  {source.socketState ? <KeyValueRow labelWidth={16} emphasis={false} label="Socket" value={source.socketState} color={statusColor(source.status)} /> : null}
74
85
  {source.lastTransitionAt ? (
75
86
  <KeyValueRow labelWidth={16} emphasis={false} label="Transition" value={formatRelativeAge(source.lastTransitionAt, now)} color={colors.textMuted} />
@@ -78,7 +89,7 @@ function ConnectionDetail({ source, width, now }: { source: ConnectionHealthStat
78
89
  <KeyValueRow labelWidth={16} emphasis={false} label="Operation" value={source.lastOperation ?? "-"} color={colors.textDim} />
79
90
  <KeyValueRow labelWidth={16} emphasis={false} label="Latency" value={formatLatency(source.lastLatencyMs)} color={colors.textMuted} />
80
91
  {source.currentDetail ? (
81
- <KeyValueRow labelWidth={16} emphasis={false} label="Detail" value={truncateToDisplayWidth(source.currentDetail, Math.max(8, lineWidth - 16))} color={source.status === "error" ? colors.negative : colors.textDim} />
92
+ <KeyValueRow labelWidth={16} emphasis={false} label="Detail" value={desktop ? source.currentDetail : truncateToDisplayWidth(source.currentDetail, Math.max(8, lineWidth - 16))} color={source.status === "error" ? colors.negative : colors.textDim} />
82
93
  ) : null}
83
94
  {source.lastSuccess ? (
84
95
  <KeyValueRow labelWidth={16} emphasis={false}
@@ -103,7 +114,7 @@ function ConnectionDetail({ source, width, now }: { source: ConnectionHealthStat
103
114
  <Box width={7}><Text fg={request.success ? colors.positive : colors.negative}>{request.success ? "ok" : "error"}</Text></Box>
104
115
  <Box width={9}><Text fg={colors.textMuted}>{formatLatency(request.latencyMs)}</Text></Box>
105
116
  <Box width={10}><Text fg={colors.textMuted}>{formatRelativeAge(request.at, now)}</Text></Box>
106
- <Text fg={colors.textDim}>{truncateToDisplayWidth(request.error ?? request.operation, Math.max(8, lineWidth - 26))}</Text>
117
+ <ClippedText fg={colors.textDim} text={request.error ?? request.operation} width={Math.max(8, lineWidth - 26)} />
107
118
  </Box>
108
119
  ))}
109
120
  </>
@@ -19,8 +19,8 @@ import { priceColor } from "../../../theme/colors";
19
19
  import { useThemeColors } from "../../../theme/theme-context";
20
20
  import type { ResolvedSeries } from "../../../time-series/types";
21
21
  import type { PaneProps } from "../../../types/plugin";
22
- import { Box, Text, TextAttributes } from "../../../ui";
23
- import { truncateToDisplayWidth } from "../../../utils/format";
22
+ import { Box, TextAttributes } from "../../../ui";
23
+ import { ClippedText } from "../../../components/ui/display";
24
24
  import { nextHeaderSort } from "../../../utils/sort-values";
25
25
  import { CPI_NOT_AVAILABLE, getCachedCpiBoard, loadCpiBoard } from "./client";
26
26
  import {
@@ -67,14 +67,15 @@ function CpiChart({ row, series, width, height }: { row: CpiRow; series: Resolve
67
67
  const colors = useThemeColors();
68
68
  const latest = row.history.at(-1)?.[0] ?? null;
69
69
  const axis = useMemo(() => cpiTimeAxis(row.history), [row.history]);
70
- const read = row.read ? truncateToDisplayWidth(row.read, Math.max(0, width - 2)) : "";
70
+ // The plain read under the axis; the desktop cuts it where its line ends.
71
+ const read = row.read && width > 2 ? row.read : "";
71
72
  return (
72
73
  <Box width={width} height={height} flexDirection="column">
73
74
  <CompositeChart series={series} panels={PANELS} width={width} height={Math.max(3, height - (read ? 1 : 0))}
74
75
  focused={false} navigable={false} showLegend showTimeAxis cursorDate={latest ? cpiMonthDate(latest) : null}
75
76
  viewport={axis?.viewport} xAxis={axis?.xAxis} clipToViewport={!!axis}
76
77
  formatValue={legendValue} formatAxisValue={formatPercentAxis} remoteKind="cpi-component-history" />
77
- {read ? <Box height={1} paddingX={1}><Text fg={colors.textMuted}>{read}</Text></Box> : null}
78
+ {read ? <Box height={1} paddingX={1}><ClippedText fg={colors.textMuted} text={read} width={width - 2} /></Box> : null}
78
79
  </Box>
79
80
  );
80
81
  }
@@ -4,8 +4,8 @@ import { DataTableView } from "../../../components/data-table/view";
4
4
  import { ChoiceDialog } from "../../../components/ui/choice-dialog";
5
5
  import { useDialog, type PromptContext } from "../../../ui/dialog";
6
6
  import { getSharedRegistry } from "../../registry";
7
- import { teamPrefix } from "../cloud/team/model";
8
- import { teamStore } from "../cloud/team/store";
7
+ import { teamPrefix } from "../../../team/model";
8
+ import { teamStore } from "../../../team/store";
9
9
  import { teamViewsStore } from "../cloud/team/views";
10
10
  import { setPaneSettings } from "../../../pane-settings";
11
11
  import { customViewInstanceSettings } from "./index";
@@ -55,6 +55,12 @@ function dateEntry(label: string, date: Date | null) {
55
55
  return { label, value: date, formatted: date ? date.toISOString().slice(0, 10) : "—" };
56
56
  }
57
57
 
58
+ /** The next pay date, when it is the one stated beside the ex-date the forward rate is dated by. */
59
+ function forwardPayDate(metrics: DividendData["metrics"]): Date | null {
60
+ const exDate = metrics.nextExDividendDate?.toISOString().slice(0, 10);
61
+ return exDate != null && exDate === metrics.forwardRateAsOf ? metrics.nextPayDate : null;
62
+ }
63
+
58
64
  function matchesType(payment: DividendPayment, type: string): boolean {
59
65
  return type === "all" || payment.type === type;
60
66
  }
@@ -95,7 +101,7 @@ export function projectDividendYieldHeadless(
95
101
  : "Reference price time unavailable; cash yield may be out of date." }] : []),
96
102
  { label: "Trailing yield", value: metrics.trailingYield, formatted: formatDividendYield(metrics.trailingYield) },
97
103
  { label: "Forward yield", value: metrics.forwardYield, formatted: metrics.forwardYield == null ? formatDividendYield(null)
98
- : `${formatDividendYield(metrics.forwardYield)} (${forwardDividendBasis(metrics.forwardRateAsOf)})` },
104
+ : `${formatDividendYield(metrics.forwardYield)} (${forwardDividendBasis(metrics.forwardRateAsOf, { payDate: forwardPayDate(metrics) })})` },
99
105
  { label: "Trailing rate", value: metrics.trailingRate, formatted: currency ? formatDistributionAmount(metrics.trailingRate ?? undefined, currency) : "—" },
100
106
  { label: "Forward rate", value: metrics.forwardRate, formatted: currency ? formatDistributionAmount(metrics.forwardRate ?? undefined, currency) : "—" },
101
107
  { label: "Earnings Payout", value: metrics.payoutRatio, formatted: formatDividendYield(metrics.payoutRatio) },
@@ -132,7 +138,7 @@ export function projectDividendYieldHeadless(
132
138
  limitations: data.notes ?? [],
133
139
  yieldMethod: "Cash distributions with ex-dates in the preceding 12 months, one payment per period for a regular cadence, divided by the reference share price; excludes reinvestment and is not SEC yield or total return.",
134
140
  forwardRateAsOf: metrics.forwardRateAsOf,
135
- forwardRateMethod: "Indicated annual cash rate, the one fundamentals use: the latest regular distribution annualized, dated by its ex-date (forwardRateAsOf). Only when its currency units are comparable with the share price.",
141
+ forwardRateMethod: "Indicated annual cash rate, the one fundamentals use: the latest regular distribution annualized, or once the next one is declared that one, dated by its ex-date (forwardRateAsOf; a date still ahead is the declared distribution's). Only when its currency units are comparable with the share price.",
136
142
  paymentFrequencyMethod: "Cadence inferred from ex-dates in the last two years, unavailable when no cash was reported in the last year; not an announced payment schedule.",
137
143
  },
138
144
  };
@@ -22,6 +22,7 @@ import { colors, priceColor } from "../../../theme/colors";
22
22
  import { TextAttributes, useUiCapabilities } from "../../../ui";
23
23
  import { displayWidth, formatCurrency, formatDistributionAmount, formatPercentRaw } from "../../../utils/format";
24
24
  import { resolveCurrencyUnit } from "../../../utils/currency-units";
25
+ import { forwardDividendNote } from "../../../utils/forward-dividend";
25
26
  import { nextHeaderSort } from "../../../utils/sort-values";
26
27
  import { handleRefreshKey, loadingErrorFooterInfo } from "../../../components/data-table/table-pane";
27
28
  import { SignInWall } from "../cloud/auth-actions";
@@ -76,11 +77,12 @@ function buildMetricItems(metrics: DividendMetrics, currency: string, hasHistory
76
77
  const exDate: StatItem[] = metrics.nextExDividendDate
77
78
  ? [{ id: "next-ex", label: "Next ex", value: formatDate(metrics.nextExDividendDate) }]
78
79
  : hasHistory ? [] : [{ id: "last-ex", label: "Last ex", value: formatDate(metrics.lastExDividendDate) }];
80
+ const forwardNote = metrics.forwardYield != null ? forwardDividendNote(metrics.forwardRateAsOf) : null;
79
81
  return [
80
82
  { id: "ttm-yield", label: short ? "TTM yld" : "TTM yield", value: formatDividendYield(metrics.trailingYield), color: priceColor(metrics.trailingYield ?? 0) },
81
- // Dated by the distribution it annualizes; docs/research-data.md says how it is built.
83
+ // Dated by the distribution it annualizes, or marked declared beside the next ex-date; docs/research-data.md says how it is built.
82
84
  { id: "forward-yield", label: short ? "Fwd yld" : "Fwd yield", value: formatDividendYield(metrics.forwardYield), color: priceColor(metrics.forwardYield ?? 0),
83
- ...(metrics.forwardYield != null && metrics.forwardRateAsOf ? { detail: `as of ${metrics.forwardRateAsOf}` } : {}) },
85
+ ...(forwardNote ? { detail: forwardNote } : {}) },
84
86
  { id: "ttm-rate", label: short ? "TTM/sh" : "TTM/share", value: formatRate(metrics.trailingRate, currency) },
85
87
  { id: "forward-rate", label: short ? "Fwd/sh" : "Fwd/share", value: formatRate(metrics.forwardRate, currency) },
86
88
  { id: "growth-1y", label: short ? "1Y chg" : "1Y growth", value: formatGrowth(metrics.growth1Y), color: priceColor(metrics.growth1Y ?? 0) },
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { useState } from "react";
8
8
  import { ActionRow, MarkdownText, RemoteImage, usePaneLinkMenuEntry } from "../../../components";
9
+ import { READER_HEADING_STYLE } from "../../../components/reader/reader-text";
9
10
  import type { DocsBlock } from "../../../docs/content";
10
11
  import { tf } from "../../../i18n";
11
12
  import { isDarkTheme } from "../../../theme/themes";
@@ -25,6 +26,9 @@ const CALLOUT_LABELS: Record<string, string> = {
25
26
  example: "Example",
26
27
  };
27
28
 
29
+ /** The reader's section heading size; the space around it is the blocks' own. */
30
+ const SECTION_HEADING_STYLE = { fontSize: READER_HEADING_STYLE.fontSize, lineHeight: READER_HEADING_STYLE.lineHeight } as const;
31
+
28
32
  const LINK_PATTERN = /\[([^\]]+)\]\(([^)\s]+)\)/g;
29
33
 
30
34
  /** Inline links of a block, for the pane menu: the terminal cannot press a word. */
@@ -122,9 +126,16 @@ function Block({ block, width, onOpenLink }: { block: DocsBlock; width: number;
122
126
  case "paragraph":
123
127
  return <MarkdownText text={block.text.replace(/\n/g, " ")} lineWidth={width} onOpenLink={onOpenLink} />;
124
128
  case "heading":
125
- // A section reads bright; a step inside it, in the text colour.
129
+ // A section reads bright and, on the desktop and the web, a step larger, as in every reader; a step inside it, in the text colour.
126
130
  return (
127
- <Text fg={block.level === 2 ? colors.textBright : colors.text} attributes={TextAttributes.BOLD} wrapText selectable width={nativePaneChrome ? "100%" : width}>
131
+ <Text
132
+ fg={block.level === 2 ? colors.textBright : colors.text}
133
+ attributes={TextAttributes.BOLD}
134
+ wrapText
135
+ selectable
136
+ width={nativePaneChrome ? "100%" : width}
137
+ style={nativePaneChrome && block.level === 2 ? SECTION_HEADING_STYLE : undefined}
138
+ >
128
139
  {block.text.replace(/[*`]/g, "")}
129
140
  </Text>
130
141
  );
@@ -54,6 +54,7 @@ import type { PaneProps } from "../../../types/plugin";
54
54
  import { Box, Text, TextAttributes, useRendererHost, useUiCapabilities, type ScrollBoxRenderable } from "../../../ui";
55
55
  import { usePluginAppActions, usePluginPaneState } from "../../runtime";
56
56
  import { badgeCell } from "../help/table-cells";
57
+ import { READER_COLUMN_FONT } from "../../../components/reader/reader-text";
57
58
  import { DocsBlocks } from "./blocks";
58
59
  import { useDocsFeed, useDocsLesson } from "./use-docs";
59
60
 
@@ -73,12 +74,12 @@ const STACK_LIMIT = 30;
73
74
  /** Prose reads best at about seventy columns, however wide the pane. */
74
75
  const MEASURE = 72;
75
76
  /**
76
- * On the desktop and the web the text reads a size up from the table text,
77
- * with more air between lines, at most 69 characters of it to the line (as
78
- * wide as a screenshot of the measure): the column fills a narrower pane and
79
- * stops there in a wider one.
77
+ * On the desktop and the web the text reads at the reader's body size, a step
78
+ * up from the table text, with more air between lines, at most 69 characters
79
+ * of it to the line (as wide as a screenshot of the measure): the column fills
80
+ * a narrower pane and stops there in a wider one.
80
81
  */
81
- const READING_STYLE = { fontSize: "1.1667em", "--cell-h": "1.7em", maxWidth: "69ch" } as const;
82
+ const READING_STYLE = { fontSize: READER_COLUMN_FONT, "--cell-h": "1.7em", maxWidth: "69ch" } as const;
82
83
  /** The terminal wraps at `width` columns; the desktop and the web at the reading column's own width. */
83
84
  function useTextWidth(width: number): number | "100%" {
84
85
  return useUiCapabilities().nativePaneChrome ? "100%" : width;
@@ -1,8 +1,9 @@
1
1
  import type { DoeTab } from "../../../api-client/doe";
2
2
  import type { PaneTemplateCreateOptions, PaneTemplateInstanceConfig } from "../../../types/plugin";
3
3
  import type { PluginModule } from "../plugin-module";
4
- import { DOE_SERIES } from "./series";
4
+ import { DOE_CATALOG_SERIES } from "./series";
5
5
  import { doeBoardCache } from "./client";
6
+ import { doePriceCache } from "./prices";
6
7
  import { doeHeadless } from "./headless";
7
8
  import { DOE_SERIES_OPTIONS, doeSeriesOption } from "./model";
8
9
  import { DoePane } from "./pane";
@@ -21,7 +22,7 @@ const shortcut = (prefix: string, tab: DoeTab | null) => ({
21
22
  });
22
23
 
23
24
  export const doeModule: PluginModule = {
24
- series: DOE_SERIES,
25
+ series: DOE_CATALOG_SERIES,
25
26
  panes: [{ id: "doe", name: "Oil and Gas Inventories", icon: "D", component: DoePane,
26
27
  defaultPosition: "right", defaultMode: "floating", defaultFloatingSize: { width: 96, height: 34 },
27
28
  tableExport: true, headless: doeHeadless("all") }],
@@ -38,6 +39,12 @@ export const doeModule: PluginModule = {
38
39
  shortcut: shortcut("NGS", "gas"), headless: doeHeadless("gas"),
39
40
  createInstance: (_context, options) => doeInstance("gas", options),
40
41
  }],
41
- setup(ctx) { doeBoardCache.attach(ctx.persistence); },
42
- dispose() { doeBoardCache.reset(); },
42
+ setup(ctx) {
43
+ doeBoardCache.attach(ctx.persistence);
44
+ doePriceCache.attach(ctx.persistence);
45
+ },
46
+ dispose() {
47
+ doeBoardCache.reset();
48
+ doePriceCache.reset();
49
+ },
43
50
  };
@@ -18,8 +18,8 @@ import { usePaneInstance } from "../../../state/app/context";
18
18
  import { useThemeColors } from "../../../theme/theme-context";
19
19
  import type { ResolvedSeries } from "../../../time-series/types";
20
20
  import type { PaneProps } from "../../../types/plugin";
21
- import { Box, Text } from "../../../ui";
22
- import { truncateToDisplayWidth } from "../../../utils/format";
21
+ import { Box } from "../../../ui";
22
+ import { ClippedText } from "../../../components/ui/display";
23
23
  import { DOE_NOT_AVAILABLE, getCachedDoeBoard, loadDoeBoard } from "./client";
24
24
  import {
25
25
  DOE_TABS,
@@ -71,14 +71,15 @@ function SeasonalChart({ row, series, width, height }: { row: DoeSeriesRow; seri
71
71
  const latestWeek = row.seasonal?.current.at(-1)?.[0] ?? null;
72
72
  const viewport = useMemo(() => ({ start: doeWeekDate(1), end: doeWeekDate(weeks) }), [weeks]);
73
73
  const xAxis = useMemo(() => ({ ticks: doeWeekTicks(weeks), formatCursor: (ratio: number) => `W${Math.round(ratio * (weeks - 1)) + 1}` }), [weeks]);
74
- const read = row.read ? truncateToDisplayWidth(row.read, Math.max(0, width - 2)) : "";
74
+ // The plain read under the axis; the desktop cuts it where its line ends.
75
+ const read = row.read && width > 2 ? row.read : "";
75
76
  return (
76
77
  <Box width={width} height={height} flexDirection="column">
77
78
  <CompositeChart series={series} panels={PANELS} width={width} height={Math.max(3, height - (read ? 1 : 0))}
78
79
  focused={false} navigable={false} showLegend showTimeAxis viewport={viewport} clipToViewport xAxis={xAxis}
79
80
  cursorDate={latestWeek ? doeWeekDate(latestWeek) : null} formatValue={legendValue(row)} formatAxisValue={doeAxisFormatter(row.unit)}
80
81
  remoteKind="doe-seasonal" />
81
- {read ? <Box height={1} paddingX={1}><Text fg={colors.textMuted}>{read}</Text></Box> : null}
82
+ {read ? <Box height={1} paddingX={1}><ClippedText fg={colors.textMuted} text={read} width={width - 2} /></Box> : null}
82
83
  </Box>
83
84
  );
84
85
  }
@@ -0,0 +1,84 @@
1
+ import { apiClient } from "../../../api-client";
2
+ import type { DoePricesPayload } from "../../../api-client/doe";
3
+ import { createPluginCache } from "../../../data/plugin-cache";
4
+ import type { SeriesDefinition } from "../../../series/types";
5
+ import { cachedSeriesWarning } from "../shared/series-warning";
6
+ import { loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
7
+
8
+ /**
9
+ * EIA weekly retail prices the petroleum board does not carry, served by Gloom
10
+ * Cloud from the EIA's history workbooks. `key` is the EIA series id a report
11
+ * of the series cites.
12
+ */
13
+ const PRICE_SERIES = [
14
+ {
15
+ id: "gasoline-price",
16
+ name: "US regular gasoline retail price",
17
+ key: "EMM_EPMR_PTE_NUS_DPG",
18
+ url: "https://www.eia.gov/dnav/pet/hist/LeafHandler.ashx?n=PET&s=EMM_EPMR_PTE_NUS_DPG&f=W",
19
+ tags: ["gasoline", "retail", "pump price"],
20
+ },
21
+ {
22
+ id: "diesel-price",
23
+ name: "US No. 2 diesel retail price",
24
+ key: "EMD_EPD2D_PTE_NUS_DPG",
25
+ url: "https://www.eia.gov/dnav/pet/hist/LeafHandler.ashx?n=PET&s=EMD_EPD2D_PTE_NUS_DPG&f=W",
26
+ tags: ["diesel", "retail", "pump price"],
27
+ },
28
+ ] as const;
29
+
30
+ export const doePriceCache = createPluginCache<DoePricesPayload>({
31
+ kind: "doe-retail-prices", source: "gloom-cloud", schemaVersion: 1,
32
+ // Weekly data: a quarter hour is soon enough to catch a Monday release.
33
+ policy: { staleMs: 15 * 60_000, expireMs: 14 * 24 * 60 * 60_000 },
34
+ });
35
+
36
+ const isDate = (value: unknown) => typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value)
37
+ && new Date(`${value}T00:00:00Z`).toISOString().slice(0, 10) === value;
38
+
39
+ /** A payload that breaks the contract is refused whole rather than charted with holes in it. */
40
+ function validatePrices(payload: DoePricesPayload): DoePricesPayload {
41
+ if (!payload || payload.source !== "EIA" || !Number.isFinite(Date.parse(payload.generatedAt))
42
+ || !["available", "partial", "unavailable"].includes(payload.status)
43
+ || !Array.isArray(payload.gaps) || !payload.gaps.every((gap) => typeof gap === "string")
44
+ || !Array.isArray(payload.series) || !payload.series.every((series) => typeof series?.id === "string"
45
+ && typeof series.key === "string" && series.unit === "usd-per-gallon" && Array.isArray(series.points)
46
+ && series.points.every((point) => Array.isArray(point) && isDate(point[0])
47
+ && typeof point[1] === "number" && Number.isFinite(point[1])))) {
48
+ throw new Error("The server returned invalid EIA retail prices");
49
+ }
50
+ return payload;
51
+ }
52
+
53
+ /** A server without the route (404), or without its tables yet (503), has no prices to give. */
54
+ async function fetchDoePrices(): Promise<DoePricesPayload> {
55
+ try {
56
+ return validatePrices(await apiClient.getCloudDoePrices());
57
+ } catch (error) {
58
+ throw unavailableOnServer(error, "EIA retail prices are not available yet.", [404, 503]);
59
+ }
60
+ }
61
+
62
+ export const DOE_PRICE_SERIES: readonly SeriesDefinition[] = PRICE_SERIES.map((series) => ({
63
+ id: `doe:${series.id}`,
64
+ name: series.name,
65
+ unit: "USD/gallon",
66
+ frequency: "weekly",
67
+ group: "Energy",
68
+ publisher: "EIA",
69
+ url: series.url,
70
+ tags: ["oil", "petroleum", "EIA", "weekly", ...series.tags],
71
+ description: "EIA weekly US retail price. Not gasoline stocks, and not the AAA daily average.",
72
+ load: async () => {
73
+ const resource = await loadCloudResource(doePriceCache, "prices", () => fetchDoePrices(), { validate: validatePrices });
74
+ const row = resource.payload.series.find((entry) => entry.id === series.id);
75
+ if (!row?.points.length) throw new Error(`${series.name} is not available yet.`);
76
+ const warning = cachedSeriesWarning(resource);
77
+ return {
78
+ points: row.points.map(([date, value]) => ({ date, value })),
79
+ ...(warning ? { warning } : {}),
80
+ // Cited as the EIA series it is, not as the route that served it.
81
+ attribution: { source: `EIA series ${series.key}`, publisher: "EIA" },
82
+ };
83
+ },
84
+ }));
@@ -3,6 +3,7 @@ import type { SeriesDefinition, SeriesPoint } from "../../../series/types";
3
3
  import { cachedSeriesWarning } from "../shared/series-warning";
4
4
  import { loadDoeBoard } from "./client";
5
5
  import { DOE_SERIES_OPTIONS } from "./model";
6
+ import { DOE_PRICE_SERIES } from "./prices";
6
7
 
7
8
  /** Stocks in million barrels, as the pane shows them; flows, rates and gas as published. */
8
9
  const UNITS: Record<DoeUnit, { unit: string; scale: number }> = {
@@ -40,7 +41,7 @@ function doeRowPoints(row: DoeSeriesRow): SeriesPoint[] {
40
41
  return [...dated(seasonal.year - 1, seasonal.previous), ...dated(seasonal.year, seasonal.current)];
41
42
  }
42
43
 
43
- export const DOE_SERIES: readonly SeriesDefinition[] = DOE_SERIES_OPTIONS.map((option): SeriesDefinition => {
44
+ const DOE_SERIES: readonly SeriesDefinition[] = DOE_SERIES_OPTIONS.map((option): SeriesDefinition => {
44
45
  const unit = ROW_UNITS[option.value] ?? (option.tab === "gas" ? "bcf" : "kb");
45
46
  const gas = option.tab === "gas";
46
47
  return {
@@ -60,3 +61,5 @@ export const DOE_SERIES: readonly SeriesDefinition[] = DOE_SERIES_OPTIONS.map((o
60
61
  },
61
62
  };
62
63
  });
64
+
65
+ export const DOE_CATALOG_SERIES = [...DOE_SERIES, ...DOE_PRICE_SERIES];
@@ -1,7 +1,9 @@
1
1
  import type { EarningsCalendarReport, EarningsTiming } from "../../../api-client/earnings";
2
2
  import { getPublishedUsEquityCalendarDay } from "../../../market-data/published-us-sessions";
3
3
  import type { EarningsEvent } from "../../../types/data-provider";
4
+ import type { EarningsDateStatus, EarningsEpsBasis } from "../../../types/financials";
4
5
  import type { TickerRecord } from "../../../types/ticker";
6
+ import { dateStatusFromEstimateFlag, earningsDateStatus, earningsEpsBasis } from "../shared/earnings-report";
5
7
  import { coherentEarningsValue } from "./estimate-basis";
6
8
  import { shortDate, weekday } from "./format";
7
9
 
@@ -16,9 +18,15 @@ export interface BoardReport {
16
18
  timing: EarningsTiming | null;
17
19
  /** Timing taken from the company's last reports, not announced yet. */
18
20
  expectedTiming: boolean;
21
+ /** Whether the date is out, announced or projected; null when the source does not say. */
22
+ dateStatus: EarningsDateStatus | null;
19
23
  marketCap: number | null;
20
24
  epsEstimate: number | null;
25
+ /** Analysts behind `epsEstimate`. */
26
+ epsAnalysts: number | null;
21
27
  epsActual: number | null;
28
+ /** Null when the reported figure's basis cannot be determined. */
29
+ epsBasis: EarningsEpsBasis | null;
22
30
  revenueEstimate: number | null;
23
31
  impliedMove: number | null;
24
32
  averageMove: number | null;
@@ -120,9 +128,12 @@ export function boardReport(report: EarningsCalendarReport, owned: ReadonlyMap<s
120
128
  date: report.date,
121
129
  timing: report.timing,
122
130
  expectedTiming: report.timingSource === "history",
131
+ dateStatus: earningsDateStatus(report.dateStatus),
123
132
  marketCap: report.marketCap,
124
133
  epsEstimate: report.epsEstimate,
134
+ epsAnalysts: report.epsAnalysts,
125
135
  epsActual: report.epsActual,
136
+ epsBasis: earningsEpsBasis(report.epsBasis),
126
137
  revenueEstimate: report.revenueEstimate,
127
138
  impliedMove: report.implied?.move ?? null,
128
139
  averageMove: report.averageReports >= MIN_AVERAGE_REPORTS ? report.averageMove : null,
@@ -139,6 +150,7 @@ export function fallbackReport(event: EarningsEvent, owned: ReadonlyMap<string,
139
150
  const currency = event.estimateBasis?.[field]?.currency;
140
151
  return currency && currency !== "USD" ? null : coherentEarningsValue(event, field);
141
152
  };
153
+ const epsEstimate = dollars("epsEstimate");
142
154
  return {
143
155
  key: `${event.symbol}:${date}`,
144
156
  symbol: event.symbol,
@@ -146,9 +158,14 @@ export function fallbackReport(event: EarningsEvent, owned: ReadonlyMap<string,
146
158
  date,
147
159
  timing,
148
160
  expectedTiming: false,
161
+ dateStatus: dateStatusFromEstimateFlag(event.isDateEstimate),
149
162
  marketCap: null,
150
- epsEstimate: dollars("epsEstimate"),
163
+ epsEstimate,
164
+ // The count belongs to the estimate beside it.
165
+ epsAnalysts: epsEstimate == null ? null : coherentEarningsValue(event, "epsAnalysts"),
151
166
  epsActual: null,
167
+ // The estimate's basis goes with it.
168
+ epsBasis: epsEstimate == null ? null : earningsEpsBasis(event.epsBasis),
152
169
  revenueEstimate: dollars("revenueEstimate"),
153
170
  impliedMove: null,
154
171
  averageMove: null,
@@ -2,10 +2,13 @@ import type { DataTableCell, DataTableColumn } from "../../../components";
2
2
  import { TABLE_COLUMN_GAP, tableColumnWidth } from "../../../components/ui/table-layout";
3
3
  import { colors } from "../../../theme/colors";
4
4
  import { TextAttributes } from "../../../ui";
5
+ import { epsBasisShort, showsEpsBasis } from "../shared/earnings-report";
5
6
  import type { BoardRow, BoardSort } from "./board-model";
6
7
  import { epsText, impliedText, moneyText, moveSize, TIMING_LABEL, weekday } from "./format";
7
8
 
8
- type BoardColumnId = "owned" | "symbol" | "name" | "when" | "cap" | "epsEstimate" | "epsActual" | "revenueEstimate" | "implied" | "average";
9
+ type BoardColumnId =
10
+ | "owned" | "symbol" | "name" | "when" | "status" | "cap" | "epsEstimate" | "analysts" | "epsActual" | "basis"
11
+ | "revenueEstimate" | "implied" | "average";
9
12
 
10
13
  export type BoardColumn = DataTableColumn & { id: BoardColumnId };
11
14
 
@@ -16,16 +19,19 @@ const ALL_COLUMNS: BoardColumn[] = [
16
19
  { id: "symbol", label: "TICKER", width: 6, align: "left" },
17
20
  { id: "name", label: "NAME", width: NAME_MIN_WIDTH, align: "left" },
18
21
  { id: "when", label: "WHEN", width: 7, align: "left" },
22
+ { id: "status", label: "STATUS", width: 9, align: "left" },
19
23
  { id: "implied", label: "IMPLIED", width: 7, align: "right" },
20
24
  { id: "average", label: "AVG MOVE", width: 8, align: "right" },
21
25
  { id: "epsEstimate", label: "EPS EST", width: 7, align: "right" },
26
+ { id: "analysts", label: "ESTS", width: 4, align: "right" },
22
27
  { id: "epsActual", label: "EPS ACT", width: 7, align: "right" },
28
+ { id: "basis", label: "BASIS", width: 5, align: "left" },
23
29
  { id: "revenueEstimate", label: "SALES EST", width: 9, align: "right" },
24
30
  { id: "cap", label: "MKT CAP", width: 8, align: "right" },
25
31
  ];
26
32
 
27
- /** Dropped first when narrow; the implied move and its average stay beside the name. */
28
- const DROP_ORDER: BoardColumnId[] = ["epsActual", "cap", "revenueEstimate", "epsEstimate", "owned"];
33
+ /** Dropped first when narrow, the report's labels before its figures; the implied move and its average stay beside the name. */
34
+ const DROP_ORDER: BoardColumnId[] = ["basis", "analysts", "status", "epsActual", "cap", "revenueEstimate", "epsEstimate", "owned"];
29
35
 
30
36
  /** Header clicks sort these; the rest keep the ranking. */
31
37
  export const SORTABLE: Partial<Record<BoardColumnId, BoardSort["column"]>> = {
@@ -66,14 +72,23 @@ export function renderBoardCell(row: BoardRow, column: BoardColumn): DataTableCe
66
72
  // An expected time, from the company's last reports, reads quieter than an announced one.
67
73
  return { text: row.showDay ? `${weekday(report.date)} ${timing}` : timing, color: report.expectedTiming ? colors.textMuted : dim };
68
74
  }
75
+ case "status":
76
+ // A projected date reads quieter than an announced one, as an expected time does.
77
+ return { text: report.dateStatus ?? "--", color: report.dateStatus === "projected" ? colors.textMuted : dim };
69
78
  case "cap":
70
79
  return { text: moneyText(report.marketCap), color: dim };
71
80
  case "epsEstimate":
72
81
  return { text: epsText(report.epsEstimate), color: dim };
82
+ case "analysts":
83
+ return { text: report.epsAnalysts == null ? "--" : String(report.epsAnalysts), color: dim };
73
84
  case "epsActual": {
74
85
  const beat = report.epsActual != null && report.epsEstimate != null ? report.epsActual - report.epsEstimate : null;
75
86
  return { text: report.epsActual == null ? "" : epsText(report.epsActual), color: beat == null || beat === 0 ? colors.text : beat > 0 ? colors.positive : colors.negative };
76
87
  }
88
+ case "basis":
89
+ return !showsEpsBasis(report)
90
+ ? { text: "" }
91
+ : { text: epsBasisShort(report.epsBasis), color: report.epsBasis ? colors.textMuted : dim };
77
92
  case "revenueEstimate":
78
93
  return { text: moneyText(report.revenueEstimate), color: dim };
79
94
  case "implied":
@@ -13,7 +13,7 @@ import { fetchEarningsCalendar, fetchEarningsHistory } from "./client";
13
13
  import { loadEarningsCalendar } from "./data/cache";
14
14
  import { epsText, impliedText, moneyText, moveSize, signedPercent, TIMING_LABEL } from "./format";
15
15
  import { historyRows } from "./history-model";
16
- import { EPS_BASIS_NOTE, epsBasisName } from "../shared/earnings-report";
16
+ import { EPS_BASIS_NOTE, epsBasisName, showsEpsBasis } from "../shared/earnings-report";
17
17
  import { isUsListingExchange } from "../../../utils/exchanges";
18
18
  import { listingAbroadVenue } from "../../../utils/sec";
19
19
  import { findUsListingOfCompany } from "../../../tickers/us-listing";
@@ -24,17 +24,23 @@ const num = (value: unknown) => (typeof value === "number" && Number.isFinite(va
24
24
 
25
25
  const BOARD_COLUMNS: HeadlessPaneColumn[] = [
26
26
  { key: "date", header: "Date" },
27
+ { key: "dateStatus", header: "Status" },
27
28
  { key: "timing", header: "When" },
28
29
  { key: "symbol", header: "Ticker" },
29
30
  { key: "name", header: "Name" },
30
31
  { key: "marketCap", header: "Mkt cap", align: right, format: (value) => moneyText(num(value)) },
31
32
  { key: "epsEstimate", header: "EPS est", align: right, format: (value) => epsText(num(value)) },
33
+ { key: "epsAnalysts", header: "Ests", align: right },
32
34
  { key: "epsActual", header: "EPS act", align: right, format: (value) => epsText(num(value)) },
35
+ { key: "epsBasis", header: "Basis" },
33
36
  { key: "revenueEstimate", header: "Sales est", align: right, format: (value) => moneyText(num(value)) },
34
37
  { key: "impliedMove", header: "Implied", align: right, format: (value) => impliedText(num(value)) },
35
38
  { key: "averageMove", header: "Avg move", align: right, format: (value) => moveSize(num(value)) },
36
39
  ];
37
40
 
41
+ /** Where the board's caps come from and how old they can be, in its metadata. */
42
+ const MARKET_CAP_NOTE = "Each company's market cap as RV shows it: its latest financials snapshot at the regular session's price (the close outside the session), from the newest quote Gloom Cloud holds for it. A company with no snapshot shows the earnings calendar's figure.";
43
+
38
44
  const HISTORY_COLUMNS: HeadlessPaneColumn[] = [
39
45
  { key: "date", header: "Date" },
40
46
  { key: "dateStatus", header: "Status" },
@@ -105,13 +111,16 @@ async function boardRows(args: HeadlessPaneLoadArgs, context: HeadlessPaneContex
105
111
  const all = [...reports, ...extra].sort((left, right) => left.date.localeCompare(right.date) || (right.marketCap ?? 0) - (left.marketCap ?? 0));
106
112
  const rows = all.slice(0, limit).map((report) => ({
107
113
  date: report.date,
114
+ dateStatus: report.dateStatus,
108
115
  timing: report.timing ? TIMING_LABEL[report.timing] : null,
109
116
  timingExpected: report.expectedTiming,
110
117
  symbol: report.symbol,
111
118
  name: report.name,
112
119
  marketCap: report.marketCap,
113
120
  epsEstimate: report.epsEstimate,
121
+ epsAnalysts: report.epsAnalysts,
114
122
  epsActual: report.epsActual,
123
+ epsBasis: showsEpsBasis(report) ? epsBasisName(report.epsBasis) : null,
115
124
  revenueEstimate: report.revenueEstimate,
116
125
  impliedMove: report.impliedMove,
117
126
  averageMove: report.averageMove,
@@ -127,6 +136,8 @@ async function boardRows(args: HeadlessPaneLoadArgs, context: HeadlessPaneContex
127
136
  from: query.from,
128
137
  to: query.to,
129
138
  averageMoveOver: `last 8 reports, at least ${MIN_AVERAGE_REPORTS}`,
139
+ epsBasis: EPS_BASIS_NOTE,
140
+ marketCap: MARKET_CAP_NOTE,
130
141
  total: all.length,
131
142
  returned: rows.length,
132
143
  truncated: rows.length < all.length,
@@ -168,7 +179,7 @@ async function historyReport(symbol: string, context: HeadlessPaneContext, deps:
168
179
  epsEstimate: row.epsEstimate,
169
180
  epsAnalysts: row.epsAnalysts,
170
181
  epsActual: row.epsActual,
171
- epsBasis: row.epsActual == null ? null : epsBasisName(row.epsBasis),
182
+ epsBasis: showsEpsBasis(row) ? epsBasisName(row.epsBasis) : null,
172
183
  epsSurprise: row.epsSurprise,
173
184
  revenueEstimate: row.revenueEstimate,
174
185
  revenueActual: row.revenueActual,