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
@@ -87,7 +87,7 @@ export function historyRows(payload: EarningsHistoryPayload | null, today: strin
87
87
  epsEstimate: coherentEarningsValue(upcoming, "epsEstimate"),
88
88
  epsAnalysts: coherentEarningsValue(upcoming, "epsAnalysts"),
89
89
  epsActual: null,
90
- epsBasis: null,
90
+ epsBasis: earningsEpsBasis(upcoming.epsBasis),
91
91
  epsSurprise: null,
92
92
  revenueEstimate: coherentEarningsValue(upcoming, "revenueEstimate"),
93
93
  revenueActual: null,
@@ -3,7 +3,7 @@ import { TABLE_COLUMN_GAP, tableColumnWidth } from "../../../components/ui/table
3
3
  import { colors } from "../../../theme/colors";
4
4
  import { TextAttributes } from "../../../ui";
5
5
  import { datedYear, epsText, fiscalMonth, impliedText, moneyText, signedPercent, TIMING_LABEL } from "./format";
6
- import { epsBasisShort } from "../shared/earnings-report";
6
+ import { epsBasisShort, showsEpsBasis } from "../shared/earnings-report";
7
7
  import type { HistoryRow } from "./history-model";
8
8
 
9
9
  type HistoryColumnId =
@@ -74,7 +74,7 @@ export function renderHistoryCell(row: HistoryRow, column: HistoryColumn): DataT
74
74
  case "epsActual":
75
75
  return { text: epsText(row.epsActual), color: colors.text };
76
76
  case "basis":
77
- return row.epsActual == null
77
+ return !showsEpsBasis(row)
78
78
  ? { text: "--", color: dim }
79
79
  : { text: epsBasisShort(row.epsBasis), color: row.epsBasis ? colors.textMuted : dim };
80
80
  case "epsSurprise":
@@ -8,8 +8,7 @@ import type {
8
8
  HeadlessPaneLoadArgs,
9
9
  HeadlessRowsResult,
10
10
  } from "../../../types/plugin";
11
- import { cloudAccountNeed, cloudDeniedStatus, type CloudDeniedStatus } from "../shared/cloud-denied";
12
- import { countOf, freePreviewNote } from "../shared/free-preview";
11
+ import { explainRefusal } from "../shared/cloud-denied";
13
12
  import { resolveHeadlessIssuerListing } from "../shared/headless-market-data";
14
13
  import {
15
14
  callStatusLabel,
@@ -18,6 +17,7 @@ import {
18
17
  loadTranscriptWithClient,
19
18
  type EarningsCallsResult,
20
19
  } from "./data";
20
+ import { callsLockNote } from "./lock-note";
21
21
  import {
22
22
  buildTranscriptSegments,
23
23
  findCallForQuarter,
@@ -68,23 +68,9 @@ const defaultDependencies: EarningsCallsHeadlessDependencies = {
68
68
  loadTranscript: (callId, context) => loadTranscriptWithClient(context.apiClient, callId),
69
69
  };
70
70
 
71
- /** The whole feature is Pro, so a free account's lock line says there is no preview rather than the server's bare "Pro plan required". */
72
- const CALLS_PRO_NOTE = freePreviewNote({ shows: countOf(0, "earnings call"), adds: "every call with its transcript, speaker turns and analyst Q&A" });
73
-
74
- /**
75
- * What a refused account needs, as the pane's walls say it: sign in, then
76
- * verify the email, then Pro. The list itself is Pro, so a signed-out reader
77
- * hears about Pro too rather than meeting it after signing in.
78
- */
79
- function callsDeniedMessage(status: CloudDeniedStatus): string {
80
- if (status === 401) return `CALLS needs ${cloudAccountNeed(status)} (log in or sign up free in the app), then Gloom Pro: earnings call transcripts are part of Gloom Cloud Pro.`;
81
- if (status === 403) return `CALLS needs ${cloudAccountNeed(status)} (Resend Verification Email in the app).`;
82
- return CALLS_PRO_NOTE;
83
- }
84
-
85
- function explainDenied(error: unknown): never {
86
- const status = cloudDeniedStatus(error);
87
- throw status ? new Error(callsDeniedMessage(status), { cause: error }) : error;
71
+ /** A refused read says what the lock note says (CALLS is Pro whole), so a free account meets the same words as a signed-out one. */
72
+ function explainDenied(ticker: string | null) {
73
+ return (error: unknown): never => { throw explainRefusal(error, callsLockNote, { symbol: ticker }); };
88
74
  }
89
75
 
90
76
  function callRow(call: CloudEarningsCallPayload) {
@@ -292,7 +278,7 @@ export function createEarningsCallsHeadless(
292
278
  },
293
279
  async load(args, context) {
294
280
  const ticker = typeof args.argument === "string" ? args.argument : null;
295
- const calls = await dependencies.loadCalls(ticker, 200, context).catch(explainDenied);
281
+ const calls = await dependencies.loadCalls(ticker, 200, context).catch(explainDenied(ticker));
296
282
  if (!ticker || !readsTranscript(args)) {
297
283
  return projectEarningsCallsHeadless(calls, { ...args, options: { ...args.options, limit: args.options.limit ?? 50 } });
298
284
  }
@@ -306,7 +292,7 @@ export function createEarningsCallsHeadless(
306
292
  }
307
293
  throw new Error(`No ${quarter} earnings call found for ${ticker}.`);
308
294
  }
309
- const transcript = await dependencies.loadTranscript(selected.id, context).catch(explainDenied);
295
+ const transcript = await dependencies.loadTranscript(selected.id, context).catch(explainDenied(ticker));
310
296
  if (isPendingTranscript(transcript)) {
311
297
  throw new Error(`Transcript for ${ticker} ${quarter} is still being produced.`);
312
298
  }
@@ -2,6 +2,7 @@ import type { PluginModule } from "../plugin-module";
2
2
  import { attachEarningsCallsPersistence, resetEarningsCallsPersistence } from "./data";
3
3
  import { EarningsCallsPane, EARNINGS_CALLS_PANE_ID } from "./pane";
4
4
  import { earningsCallsHeadless } from "./headless";
5
+ import { callsLockNote } from "./lock-note";
5
6
 
6
7
 
7
8
  const description =
@@ -32,6 +33,7 @@ export const earningsCallsModule: PluginModule = {
32
33
  panes: [
33
34
  {
34
35
  id: EARNINGS_CALLS_PANE_ID,
36
+ reportLockNote: callsLockNote,
35
37
  name: "Earnings Calls",
36
38
  icon: "C",
37
39
  component: EarningsCallsPane,
@@ -0,0 +1,17 @@
1
+ import type { PaneLock } from "../../../types/plugin";
2
+ import { proLockNote } from "../shared/pro-lock-note";
3
+
4
+ /**
5
+ * CALLS is Pro whole, the list of calls included. `events` needs no account
6
+ * and lists a company's earnings dates, each with the EPS it reported and the
7
+ * next one's estimate, without the call or its transcript.
8
+ */
9
+ export function callsLockNote(lock: PaneLock): string {
10
+ return proLockNote(lock, {
11
+ token: "CALLS",
12
+ argument: "optional",
13
+ does: "it reads each earnings call into a transcript with speaker turns, analyst Q&A and extracted guidance",
14
+ shows: "it",
15
+ free: (ticker, company) => `gloomberb events ${ticker} lists ${company}'s earnings dates, with the EPS reported at each and the next estimate.`,
16
+ });
17
+ }
@@ -1,9 +1,16 @@
1
1
  import type { HeadlessPaneDefinition } from "../../../types/plugin";
2
+ import type { HeadlessPaneColumn, HeadlessPaneContext } from "../../../types/headless";
2
3
  import {
3
4
  NUMERIC_FIELDS,
4
5
  type NumericField,
6
+ type ScreenRow,
5
7
  } from "../../../api-client/equity-screener";
8
+ import { EXTENDED_SESSION_LABELS, type ExtendedSession } from "../../../market-data/market/status";
9
+ import { buildQuoteKey } from "../../../market-data/selectors";
10
+ import type { Quote } from "../../../types/financials";
11
+ import { formatCurrency, formatPercentRaw } from "../../../utils/format";
6
12
  import { fetchScreen } from "./client";
13
+ import { overlayScreenQuotes, screenExtendedSessions } from "./live";
7
14
  import {
8
15
  DEFAULT_SCREEN,
9
16
  EXAMPLE_SCREEN_DEFINITION,
@@ -22,6 +29,41 @@ const spacedJson = (definition: ScreenDefinition) =>
22
29
  /** The `--definition` value of the catalog's worked example, quoted for a shell. */
23
30
  const EXAMPLE_DEFINITION_OPTION = `--definition '${spacedJson(EXAMPLE_SCREEN_DEFINITION)}'`;
24
31
 
32
+ /**
33
+ * Each row's quote through the standard quote path, keyed as the overlay
34
+ * reads it. A row without one, or every row when the batch fails, keeps the
35
+ * snapshot's figures.
36
+ */
37
+ async function loadRowQuotes(rows: readonly ScreenRow[], ctx: HeadlessPaneContext): Promise<Map<string, Quote>> {
38
+ const provider = ctx.marketData;
39
+ if (!provider?.getQuotesBatch || rows.length === 0) return new Map();
40
+ try {
41
+ const results = await provider.getQuotesBatch(
42
+ rows.map((row) => ({ symbol: row.symbol, exchange: row.exchange })),
43
+ { forceRefresh: ctx.refresh, signal: ctx.signal },
44
+ );
45
+ return new Map(results.flatMap((result) => result.quote ? [[buildQuoteKey(result.target), result.quote] as const] : []));
46
+ } catch {
47
+ return new Map();
48
+ }
49
+ }
50
+
51
+ const price = (value: unknown, row: Record<string, unknown>) =>
52
+ typeof value === "number" && typeof row.currency === "string" && row.currency ? formatCurrency(value, row.currency) : "-";
53
+ const percent = (value: unknown) => typeof value === "number" ? formatPercentRaw(value) : "-";
54
+
55
+ /** The pre-market, after-hours or overnight print and its move from the close in Last, for the rows that have one. */
56
+ function extendedColumn(session: ExtendedSession): HeadlessPaneColumn {
57
+ return {
58
+ key: "extendedPrice",
59
+ header: EXTENDED_SESSION_LABELS[session],
60
+ align: "right",
61
+ format: (value, row) => row.extendedSession === session && typeof value === "number"
62
+ ? `${price(value, row)}${row.extendedMark ? " mid" : ""} ${percent(row.extendedChangePercent)}`
63
+ : "-",
64
+ };
65
+ }
66
+
25
67
  /** A typed `--definition`, checked; every error points at the worked example. */
26
68
  function readDefinition(text: string): ScreenDefinition {
27
69
  const pointer = "The example in gloomberb catalog EQS is a valid definition to start from.";
@@ -81,6 +123,10 @@ export const equityScreenerHeadless: HeadlessPaneDefinition<"bundle"> = {
81
123
  : DEFAULT_SCREEN;
82
124
  const data = await fetchScreen(definition, null, ctx.signal, ctx.apiClient);
83
125
  const metric = (args.options.metric ?? "marketCap") as NumericField;
126
+ // Last, Chg% and market cap are the regular session, as `quote` and the
127
+ // pane read each row's quote; an extended print is a column of its own.
128
+ const rows = overlayScreenQuotes(data.rows, await loadRowQuotes(data.rows, ctx));
129
+ const extendedSessions = screenExtendedSessions(rows);
84
130
  // Payload status also counts ordinary per-field sparsity; only generation
85
131
  // warnings are failures, and more pages are not an error.
86
132
  return {
@@ -94,19 +140,30 @@ export const equityScreenerHeadless: HeadlessPaneDefinition<"bundle"> = {
94
140
  { key: "symbol", header: "Symbol" },
95
141
  { key: "exchange", header: "Exchange" },
96
142
  { key: "name", header: "Name" },
143
+ // The focus metric says it once when it is the price or the move.
144
+ ...(metric === "price" ? [] : [{ key: "price", header: "Last", align: "right" as const, format: price }]),
145
+ ...(metric === "changePercent" ? [] : [{ key: "changePercent", header: "Chg%", align: "right" as const, format: percent }]),
146
+ ...extendedSessions.map(extendedColumn),
97
147
  { key: "value", header: SHORT_LABELS[metric], align: "right" },
98
148
  { key: "unit", header: "Unit" },
99
149
  { key: "percentile", header: "Covered pctl", align: "right" },
100
150
  { key: "asOf", header: "As of" },
101
151
  { key: "state", header: "State" },
102
152
  ],
103
- rows: data.rows.map((row) => {
153
+ rows: rows.map((row) => {
104
154
  const observation = row.metrics[metric];
105
155
  const stamp = metricDate(observation);
106
156
  return {
107
157
  symbol: row.symbol,
108
158
  exchange: row.exchange,
109
159
  name: row.name,
160
+ currency: row.currency,
161
+ price: row.metrics.price.value,
162
+ changePercent: row.metrics.changePercent.value,
163
+ extendedSession: row.extended?.session ?? null,
164
+ extendedPrice: row.extended?.price ?? null,
165
+ extendedChangePercent: row.extended?.changePercent ?? null,
166
+ ...(row.extended?.mark ? { extendedMark: true } : {}),
110
167
  value: formatScreenValue(metric, observation.value),
111
168
  unit: observation.unit,
112
169
  percentile:
@@ -127,6 +184,7 @@ export const equityScreenerHeadless: HeadlessPaneDefinition<"bundle"> = {
127
184
  covered: data.universe.covered,
128
185
  returned: data.rows.length,
129
186
  hasMore: !!data.nextCursor,
187
+ priceBasis: "price, changePercent and the market cap are the regular session, read from each row's quote as `quote` reads it: after the close, its close and that close's move. A pre-market, after-hours or overnight print is in extendedPrice and extendedChangePercent, measured from that close. Membership, order and percentiles are the snapshot's, which ranks by the close.",
130
188
  notices: data.nextCursor
131
189
  ? [
132
190
  `Top ${data.rows.length} of ${data.universe.matched} matches; narrow the criteria to see the rest.`,
@@ -1,9 +1,19 @@
1
1
  import type { ScreenMetric, ScreenRow } from "../../../api-client/equity-screener";
2
+ import { getExtendedSessionDisplay, type ExtendedSession, type ExtendedSessionDisplay } from "../../../market-data/market/status";
3
+ import { quoteTradingDay } from "../../../market-data/quotes/day-range";
4
+ import { getRegularSessionQuoteDisplay } from "../../../market-data/quotes/screener-live-quotes";
2
5
  import type { QueryEntry } from "../../../market-data/result-types";
3
6
  import { buildQuoteKey, resolveEntryData } from "../../../market-data/selectors";
4
7
  import type { Quote } from "../../../types/financials";
5
8
  import { isFiniteNumber } from "../../../utils/guards";
6
9
 
10
+ /**
11
+ * A screen row with its quote laid over it: price, change, volume and market
12
+ * cap are the regular session's, and a pre-market, after-hours or overnight
13
+ * print rides beside them in `extended`, measured from that session's close.
14
+ */
15
+ export type LiveScreenRow = ScreenRow & { extended?: ExtendedSessionDisplay };
16
+
7
17
  function metricTime(metric: ScreenMetric | undefined): number | null {
8
18
  const stamp = metric?.observedAt ?? metric?.asOf ?? null;
9
19
  if (!stamp) return null;
@@ -15,54 +25,69 @@ function screenRowQuoteKey(row: Pick<ScreenRow, "symbol" | "exchange">): string
15
25
  return buildQuoteKey({ symbol: row.symbol, exchange: row.exchange });
16
26
  }
17
27
 
18
- function liveMetric(metric: ScreenMetric, value: number, observedAt: string): ScreenMetric {
19
- return { ...metric, value, asOf: observedAt, observedAt, state: "available" };
28
+ function liveMetric(metric: ScreenMetric, value: number, asOf: string, observedAt: string): ScreenMetric {
29
+ return { ...metric, value, asOf, observedAt, state: "available" };
20
30
  }
21
31
 
22
32
  /**
23
- * Price, day change, volume and market cap from a live quote, over a snapshot
24
- * row. Market cap scales with price so it keeps the snapshot's share basis.
25
- * Membership, sort order and percentiles stay the snapshot's, so rows neither
26
- * reshuffle nor vanish between snapshots. A quote in another currency unit
27
- * (GBp against GBP) or older than the snapshot leaves the row alone.
33
+ * Price, day change, volume and market cap from a quote, over a snapshot row,
34
+ * as `quote` headlines them: the live quote while the regular session trades,
35
+ * then the completed session's close and its move (getRegularSessionQuoteDisplay,
36
+ * as the boards read it), weekend and overnight included, so an extended print
37
+ * never moves them. That print is the row's `extended`. Market cap scales with
38
+ * the price so it keeps the snapshot's share basis. Membership, sort order and
39
+ * percentiles stay the snapshot's, which ranks by the close too, so rows
40
+ * neither reshuffle nor vanish between snapshots. A quote in another currency
41
+ * unit (GBp against GBP), a stale one, or one older than the snapshot (a
42
+ * live quote observed before it, a quote of an earlier day than the
43
+ * snapshot's price) leaves the row alone.
28
44
  */
29
- function overlayLiveScreenRow(row: ScreenRow, quote: Quote | null): ScreenRow {
30
- if (!quote || quote.stale === true || !isFiniteNumber(quote.price) || quote.price <= 0) return row;
31
- const snapshotPrice = row.metrics.price;
32
- if (!snapshotPrice) return row;
45
+ function overlayLiveScreenRow(row: ScreenRow, quote: Quote | null | undefined): LiveScreenRow {
46
+ if (!quote || quote.stale === true || !isFiniteNumber(quote.price) || quote.price <= 0 || !isFiniteNumber(quote.lastUpdated)) return row;
33
47
  const quoteCurrency = quote.currency?.trim();
34
48
  if (quoteCurrency && row.currency && quoteCurrency !== row.currency) return row;
35
- const snapshotAt = metricTime(snapshotPrice);
36
- if (snapshotAt != null && isFiniteNumber(quote.lastUpdated) && quote.lastUpdated < snapshotAt) return row;
37
- if (!isFiniteNumber(quote.lastUpdated)) return row;
38
- if (snapshotPrice.value === quote.price && row.metrics.changePercent?.value === quote.changePercent) return row;
49
+ const snapshotPrice = row.metrics.price;
50
+ const headline = getRegularSessionQuoteDisplay(quote);
51
+ if (!headline || !isFiniteNumber(headline.price) || headline.price <= 0) return row;
52
+ if (headline.completed) {
53
+ // The snapshot dates a session's price by that session (a pre-market
54
+ // print by the day it trades in); a quote of an earlier day is older.
55
+ const snapshotSession = snapshotPrice.asOf && /^\d{4}-\d{2}-\d{2}$/.test(snapshotPrice.asOf) ? snapshotPrice.asOf : null;
56
+ const quoteDay = quoteTradingDay(quote) ?? headline.closedSession;
57
+ if (quoteDay && snapshotSession && quoteDay < snapshotSession) return row;
58
+ } else {
59
+ const snapshotAt = metricTime(snapshotPrice);
60
+ if (snapshotAt != null && quote.lastUpdated < snapshotAt) return row;
61
+ }
62
+ const print = getExtendedSessionDisplay(quote);
63
+ const extended = print && isFiniteNumber(print.price) && print.price > 0 ? print : null;
64
+ if (!extended && snapshotPrice.value === headline.price && row.metrics.changePercent.value === headline.changePercent) return row;
39
65
 
40
66
  const observedAt = new Date(quote.lastUpdated).toISOString();
41
- const metrics = { ...row.metrics, price: liveMetric(snapshotPrice, quote.price, observedAt) };
42
- if (isFiniteNumber(quote.changePercent) && metrics.changePercent) {
43
- metrics.changePercent = liveMetric(metrics.changePercent, quote.changePercent, observedAt);
67
+ // A close is dated by its session, as the snapshot dates one.
68
+ const asOf = headline.completed ? headline.closedSession ?? observedAt : observedAt;
69
+ const metrics = { ...row.metrics, price: liveMetric(snapshotPrice, headline.price, asOf, observedAt) };
70
+ if (isFiniteNumber(headline.changePercent)) {
71
+ metrics.changePercent = liveMetric(metrics.changePercent, headline.changePercent, asOf, observedAt);
44
72
  }
45
- if (isFiniteNumber(quote.volume) && quote.volume >= 0 && metrics.volume) {
46
- metrics.volume = liveMetric(metrics.volume, quote.volume, observedAt);
73
+ // Outside the session the quote's volume may be the next pre-market's; the snapshot's is the close's session's.
74
+ if (!headline.completed && isFiniteNumber(quote.volume) && quote.volume >= 0) {
75
+ metrics.volume = liveMetric(metrics.volume, quote.volume, asOf, observedAt);
47
76
  }
48
77
  const cap = metrics.marketCap;
49
- if (cap && isFiniteNumber(cap.value) && isFiniteNumber(snapshotPrice.value) && snapshotPrice.value > 0) {
50
- metrics.marketCap = liveMetric(cap, cap.value * (quote.price / snapshotPrice.value), observedAt);
78
+ if (isFiniteNumber(cap.value) && isFiniteNumber(snapshotPrice.value) && snapshotPrice.value > 0) {
79
+ metrics.marketCap = liveMetric(cap, cap.value * (headline.price / snapshotPrice.value), asOf, observedAt);
51
80
  }
52
- return { ...row, metrics };
81
+ return { ...row, metrics, ...(extended ? { extended } : {}) };
53
82
  }
54
83
 
55
84
  /** The last overlay per snapshot row, so a row whose quote did not move keeps its object. */
56
- const liveRows = new WeakMap<ScreenRow, { quote: Quote | null; row: ScreenRow }>();
85
+ const liveRows = new WeakMap<ScreenRow, { quote: Quote | null; row: LiveScreenRow }>();
57
86
 
58
- export function overlayLiveScreenRows(
59
- rows: readonly ScreenRow[],
60
- entries: ReadonlyMap<string, QueryEntry<Quote>>,
61
- ): ScreenRow[] {
87
+ function overlayRows(rows: readonly ScreenRow[], quoteOf: (key: string) => Quote | null | undefined): LiveScreenRow[] {
62
88
  return rows.map((row) => {
63
- const entry = entries.get(screenRowQuoteKey(row));
64
- if (!entry) return row;
65
- const quote = resolveEntryData(entry);
89
+ const quote = quoteOf(screenRowQuoteKey(row));
90
+ if (quote === undefined) return row;
66
91
  const cached = liveRows.get(row);
67
92
  if (cached && cached.quote === quote) return cached.row;
68
93
  const next = overlayLiveScreenRow(row, quote);
@@ -70,3 +95,29 @@ export function overlayLiveScreenRows(
70
95
  return next;
71
96
  });
72
97
  }
98
+
99
+ /** The pane's rows with the streamed quotes on screen laid over them. */
100
+ export function overlayLiveScreenRows(
101
+ rows: readonly ScreenRow[],
102
+ entries: ReadonlyMap<string, QueryEntry<Quote>>,
103
+ ): LiveScreenRow[] {
104
+ return overlayRows(rows, (key) => {
105
+ const entry = entries.get(key);
106
+ return entry ? resolveEntryData(entry) : undefined;
107
+ });
108
+ }
109
+
110
+ /** Rows with quotes keyed by screenRowQuoteKey laid over them, for a report. */
111
+ export function overlayScreenQuotes(rows: readonly ScreenRow[], quotes: ReadonlyMap<string, Quote>): LiveScreenRow[] {
112
+ return overlayRows(rows, (key) => quotes.get(key));
113
+ }
114
+
115
+ /** The extended sessions some row has a print for, in the order their columns go. */
116
+ export function screenExtendedSessions(rows: readonly LiveScreenRow[]): ExtendedSession[] {
117
+ return (["PRE", "POST", "OVERNIGHT"] as const).filter((session) => rows.some((row) => row.extended?.session === session));
118
+ }
119
+
120
+ /** A row's move in `session`'s print from the close in its price, when it has one. */
121
+ export function screenExtendedMove(row: LiveScreenRow, session: ExtendedSession): number | null {
122
+ return row.extended?.session === session && isFiniteNumber(row.extended.changePercent) ? row.extended.changePercent : null;
123
+ }
@@ -1,4 +1,4 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1
+ import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import {
3
3
  Box,
4
4
  ScrollBox,
@@ -41,10 +41,10 @@ import {
41
41
  type NumericField,
42
42
  type ScreenDefinition,
43
43
  type ScreenField,
44
- type ScreenRow,
45
44
  } from "../../../api-client/equity-screener";
46
45
  import type { PaneProps } from "../../../types/plugin";
47
- import { formatPercentileRank } from "../../../utils/format";
46
+ import { formatPercentileRank, formatPercentRaw } from "../../../utils/format";
47
+ import { EXTENDED_SESSION_LABELS, extendedSessionStateLabel, type ExtendedSession } from "../../../market-data/market/status";
48
48
  import { useOpenDescription } from "../shared/open-description";
49
49
  import { useResearchCloudSession } from "../shared/research-cloud-session";
50
50
  import { INITIAL_STREAM_RANGE, streamWindowRows } from "../shared/use-quote-board";
@@ -71,7 +71,7 @@ import {
71
71
  screenRowId,
72
72
  } from "./model";
73
73
  import { useScreenResults } from "./results";
74
- import { overlayLiveScreenRows } from "./live";
74
+ import { overlayLiveScreenRows, screenExtendedMove, screenExtendedSessions, type LiveScreenRow } from "./live";
75
75
  import { useLiveQuoteEntries } from "../../../state/hooks/quote-streaming";
76
76
  import { buildScreenerQuoteTargets } from "../../../market-data/quotes/screener-live-quotes";
77
77
  import { useLiveStreamingSetting } from "../../../state/hooks/live-streaming";
@@ -88,15 +88,21 @@ const date = (value: string | null) => value?.slice(0, 10) ?? "--";
88
88
  const rank = (value: number | null) =>
89
89
  value == null ? "--" : value.toFixed(0);
90
90
 
91
+ const EXTENDED_COLUMN_PREFIX = "ext:";
92
+
91
93
  /**
92
94
  * Symbol and name, then the focus metric with its covered-universe percentile
93
95
  * and date, then the screen's criteria and context metrics as width allows.
96
+ * A PRE-MKT, AFTER-HRS or OVERNIGHT column, named as MOST names it, follows
97
+ * CHG% for each session in `extendedSessions`, and is added only whole, as
98
+ * the metrics are.
94
99
  */
95
100
  function resultColumns(
96
101
  width: number,
97
102
  definition: ScreenDefinition,
98
103
  metric: NumericField,
99
104
  asOf: boolean,
105
+ extendedSessions: readonly ExtendedSession[] = [],
100
106
  ): DataTableColumn[] {
101
107
  const [focus, ...others] = resultFields(definition, metric);
102
108
  const metricColumn = (field: NumericField): DataTableColumn => ({
@@ -105,6 +111,17 @@ function resultColumns(
105
111
  width: columnWidth(field),
106
112
  align: "right",
107
113
  });
114
+ const extendedColumns = extendedSessions.map((session): DataTableColumn => ({
115
+ id: `${EXTENDED_COLUMN_PREFIX}${session}`,
116
+ label: extendedSessionStateLabel(session),
117
+ width: 9,
118
+ align: "right",
119
+ }));
120
+ // The extended move reads beside CHG%, or first after the focus metric when that is CHG%.
121
+ const extras = [
122
+ ...(focus === "changePercent" ? extendedColumns : []),
123
+ ...others.flatMap((field) => field === "changePercent" ? [metricColumn(field), ...extendedColumns] : [metricColumn(field)]),
124
+ ];
108
125
  const build = (
109
126
  extra: DataTableColumn[],
110
127
  exchange: boolean,
@@ -123,8 +140,8 @@ function resultColumns(
123
140
  // column is only added when it fits whole.
124
141
  const fits = (columns: DataTableColumn[]) => getTableWidth(columns) <= width;
125
142
  let shown: DataTableColumn[] = [];
126
- for (const field of others) {
127
- const next = [...shown, metricColumn(field)];
143
+ for (const column of extras) {
144
+ const next = [...shown, column];
128
145
  if (!fits(build(next, false, false))) break;
129
146
  shown = next;
130
147
  }
@@ -138,7 +155,7 @@ function ScreenDetail({
138
155
  width,
139
156
  height,
140
157
  }: {
141
- row: ScreenRow;
158
+ row: LiveScreenRow;
142
159
  width: number;
143
160
  height: number;
144
161
  }) {
@@ -153,22 +170,33 @@ function ScreenDetail({
153
170
  {NUMERIC_FIELDS.map((field) => {
154
171
  const metric = row.metrics[field];
155
172
  const stamp = metricDate(metric);
173
+ const extended = field === "changePercent" ? row.extended : undefined;
156
174
  return (
157
- <KeyValueRow
158
- key={field}
159
- label={screenLabel(field)}
160
- labelWidth={24}
161
- value={`${formatScreenValue(field, metric.value)} ${metric.unit}`}
162
- detail={
163
- metric.value === null
164
- ? (metric.reason ?? "unavailable")
165
- : [
166
- metric.percentile.value == null ? null : formatPercentileRank(metric.percentile.value),
167
- stamp.collected ? `collected ${stamp.text}` : stamp.text,
168
- metric.state === "available" ? null : metric.state,
169
- ].filter(Boolean).join(" \u00b7 ")
170
- }
171
- />
175
+ <Fragment key={field}>
176
+ <KeyValueRow
177
+ label={screenLabel(field)}
178
+ labelWidth={24}
179
+ value={`${formatScreenValue(field, metric.value)} ${metric.unit}`}
180
+ detail={
181
+ metric.value === null
182
+ ? (metric.reason ?? "unavailable")
183
+ : [
184
+ metric.percentile.value == null ? null : formatPercentileRank(metric.percentile.value),
185
+ stamp.collected ? `collected ${stamp.text}` : stamp.text,
186
+ metric.state === "available" ? null : metric.state,
187
+ ].filter(Boolean).join(" \u00b7 ")
188
+ }
189
+ />
190
+ {extended ? (
191
+ // The pre-market, after-hours or overnight print, from the close in Price.
192
+ <KeyValueRow
193
+ label={EXTENDED_SESSION_LABELS[extended.session]}
194
+ labelWidth={24}
195
+ value={`${formatScreenValue("price", extended.price)} ${row.metrics.price.unit}`}
196
+ detail={`${formatPercentRaw(extended.changePercent)}${extended.mark ? " mid" : ""}`}
197
+ />
198
+ ) : null}
199
+ </Fragment>
172
200
  );
173
201
  })}
174
202
  </Box>
@@ -299,7 +327,9 @@ function EquityScreenView({
299
327
  // AS OF earns its column only when some row's focus metric is stale; the
300
328
  // snapshot decides, so a live quote freshening a row does not reflow the table.
301
329
  const staleFocus = snapshotRows.some((row) => row.metrics[metric]?.state === "stale");
302
- const columns = resultColumns(width, definition, metric, staleFocus);
330
+ // A pre-market, after-hours or overnight column only while a row has such a print.
331
+ const extendedSessions = screenExtendedSessions(rows);
332
+ const columns = resultColumns(width, definition, metric, staleFocus, extendedSessions);
303
333
  const apply = (next: ScreenDefinition) => {
304
334
  try {
305
335
  setDefinition(parseScreenDefinition(next));
@@ -895,6 +925,13 @@ function EquityScreenView({
895
925
  ) : null
896
926
  }
897
927
  renderCell={(row, column) => {
928
+ if (column.id.startsWith(EXTENDED_COLUMN_PREFIX)) {
929
+ // The extended print's move from the close in PRICE and CHG%.
930
+ const move = screenExtendedMove(row, column.id.slice(EXTENDED_COLUMN_PREFIX.length) as ExtendedSession);
931
+ return move == null
932
+ ? { text: "--", color: colors.textDim }
933
+ : { text: formatPercentRaw(move), color: priceColor(move, colors) };
934
+ }
898
935
  const field = column.id.startsWith("m:")
899
936
  ? (column.id.slice(2) as NumericField)
900
937
  : null;
@@ -248,7 +248,7 @@ export async function fetchEstimates(
248
248
  );
249
249
  } catch (error) {
250
250
  // The route answers a missing or unverified session with a bare 401/403; the pane's sign-in wall keys on the shared gate.
251
- if (isAccessDenied(error)) throw new Error(CLOUD_SESSION_REQUIRED);
251
+ if (isAccessDenied(error)) throw new Error(CLOUD_SESSION_REQUIRED, { cause: error });
252
252
  throw unavailableOnServer(error, "Estimate revisions are not available yet.");
253
253
  }
254
254
  }
@@ -1,5 +1,7 @@
1
1
  import type { HeadlessPaneDefinition } from "../../../types/plugin";
2
+ import { explainRefusal } from "../shared/cloud-denied";
2
3
  import { fetchEstimates } from "./client";
4
+ import { estimateRevisionsLockNote } from "./lock-note";
3
5
  import { estimateCurrent, periodLabel, pinnedEstimatePeriods } from "./model";
4
6
  export const estimateRevisionsHeadless: HeadlessPaneDefinition<"bundle"> = {
5
7
  shape: "bundle",
@@ -27,7 +29,7 @@ export const estimateRevisionsHeadless: HeadlessPaneDefinition<"bundle"> = {
27
29
  ],
28
30
  discovery: {
29
31
  aliases: ["EM", "GUID", "EEO"],
30
- dataRequirements: ["Gloom Cloud analyst snapshots"],
32
+ dataRequirements: ["Free Gloom sign-in", "Gloom Cloud analyst snapshots"],
31
33
  limitations: [
32
34
  "Actual collection days and provider backfills remain separate.",
33
35
  "Range dispersion is not standard deviation.",
@@ -42,7 +44,12 @@ export const estimateRevisionsHeadless: HeadlessPaneDefinition<"bundle"> = {
42
44
  instrument?.symbol ?? symbol,
43
45
  instrument?.exchange ?? "",
44
46
  ctx.apiClient,
45
- );
47
+ ).catch((error: unknown) => {
48
+ throw explainRefusal(error, estimateRevisionsLockNote, {
49
+ symbol: args.symbols[0] ?? null,
50
+ token: ctx.settings?.tab === "guidance" ? "GUID" : "EM",
51
+ });
52
+ });
46
53
  const periods = pinnedEstimatePeriods(
47
54
  data.periods,
48
55
  args.options.period,
@@ -2,11 +2,14 @@ import type { PluginModule } from "../plugin-module";
2
2
  import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
3
3
  import { estimateRevisionsCache } from "./client";
4
4
  import { estimateRevisionsHeadless } from "./headless";
5
+ import { estimateRevisionsLockNote } from "./lock-note";
5
6
  import { EstimateRevisionsPane } from "./pane";
6
7
  export const estimateRevisionsModule: PluginModule = {
7
8
  panes: [
8
9
  {
9
10
  id: "estimate-revisions",
11
+ reportLockNote: estimateRevisionsLockNote,
12
+ reportRequirements: ["Free Gloom sign-in"],
10
13
  name: "Estimate Revisions",
11
14
  icon: "E",
12
15
  component: EstimateRevisionsPane,
@@ -0,0 +1,23 @@
1
+ import type { PaneLock } from "../../../types/plugin";
2
+ import { proLockNote } from "../shared/pro-lock-note";
3
+
4
+ /**
5
+ * EM, EEO and GUID read one Gloom Cloud route that asks for a signed-in account
6
+ * with a verified email, and no plan. The report names the function it was run
7
+ * for. GUIDE lists a company's latest guidance ranges, as a free preview;
8
+ * `earnings` lists the next report date with its consensus EPS estimate.
9
+ */
10
+ export function estimateRevisionsLockNote(lock: PaneLock): string {
11
+ const guidance = lock.token === "GUID";
12
+ return proLockNote(lock, {
13
+ token: guidance ? "GUID" : "EM",
14
+ needs: "sign-in",
15
+ does: guidance
16
+ ? "it reads the EPS guidance a company issued, cited from its filings and calls, against consensus"
17
+ : "it reads consensus EPS estimates by fiscal period, how they were revised, the surprises against reported EPS and the guidance",
18
+ shows: "it",
19
+ free: guidance
20
+ ? (ticker, company) => `gloomberb fn GUIDE ${ticker} lists ${company}'s latest guidance ranges, with the date each was issued.`
21
+ : (ticker, company) => `gloomberb earnings ${ticker} lists ${company}'s next report date with its consensus EPS estimate.`,
22
+ });
23
+ }