gloomberb 0.17.0 → 0.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/{plugins/builtin/cloud → account}/auth-dialog.tsx +7 -7
- package/src/api-client/data.ts +5 -1
- package/src/api-client/doe.ts +19 -0
- package/src/api-client/earnings.ts +4 -2
- package/src/api-client/index.ts +1 -0
- package/src/app/global-shortcuts.ts +10 -0
- package/src/app/keybindings/actions.ts +16 -0
- package/src/app/runtime/desktop-deeplink.ts +1 -1
- package/src/app/runtime/update.ts +16 -0
- package/src/app.tsx +1 -1
- package/src/architecture/core-builtin-allowlist.json +4 -9
- package/src/architecture/dom-cell-metrics-allowlist.json +100 -26
- package/src/cli/commands/market.ts +87 -17
- package/src/cli/commands/options-chain.ts +93 -23
- package/src/cli/commands/options-leaps.ts +462 -63
- package/src/cli/commands/search.ts +10 -7
- package/src/cli/commands/ticker.ts +19 -10
- package/src/cli/glossary.ts +1 -1
- package/src/cli/index.ts +14 -6
- package/src/cli/known-gaps.ts +22 -0
- package/src/cli/pane-functions/catalog.ts +45 -10
- package/src/cli/pane-functions/dom.ts +36 -10
- package/src/cli/pane-functions/function-help.ts +4 -2
- package/src/cli/pane-functions/headless.ts +12 -1
- package/src/cli/pane-functions/index.ts +44 -3
- package/src/cli/pane-functions/lock-note.ts +56 -0
- package/src/cli/pane-functions/report-notices.ts +15 -0
- package/src/cli/symbol-args.ts +19 -0
- package/src/components/command-bar/list/view.tsx +2 -1
- package/src/components/command-bar/workflow/builtin.ts +2 -2
- package/src/components/command-bar/workflow/collection-actions.ts +1 -1
- package/src/components/feedback-dialog.tsx +1 -1
- package/src/components/index.ts +3 -2
- package/src/components/layout/header.tsx +58 -14
- package/src/components/layout/shell/chrome.ts +10 -4
- package/src/components/layout/shell/index.tsx +1 -1
- package/src/components/layout/status-bar-groups.ts +1 -1
- package/src/components/layout/status-bar.tsx +2 -2
- package/src/components/markdown-text.tsx +14 -2
- package/src/components/reader/article-reader.tsx +1 -1
- package/src/components/reader/detail-reader.tsx +10 -7
- package/src/components/reader/reader-text.tsx +32 -17
- package/src/components/ui/column-grid.tsx +206 -0
- package/src/components/ui/display.tsx +40 -3
- package/src/components/ui/fill.ts +8 -0
- package/src/components/ui/index.ts +2 -0
- package/src/layout-marketplace/gallery.tsx +2 -2
- package/src/layout-marketplace/linked-sync.ts +1 -1
- package/src/market-data/market/freshness.ts +3 -0
- package/src/market-data/published-apac-sessions.ts +12 -2
- package/src/market-data/quotes/screener-live-quotes.ts +31 -7
- package/src/plugins/builtin/account-management/form-components.tsx +4 -7
- package/src/plugins/builtin/account-management/password-dialog.tsx +3 -2
- package/src/plugins/builtin/backtest/pane.tsx +9 -3
- package/src/plugins/builtin/buildout/detail/company.tsx +4 -3
- package/src/plugins/builtin/buildout/detail/ui.tsx +9 -12
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chart-composer/presets.ts +33 -1
- package/src/plugins/builtin/chat/channel-labels.ts +2 -2
- package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -1
- package/src/plugins/builtin/chat/content/index.tsx +2 -2
- package/src/plugins/builtin/chat/conversations.ts +1 -1
- package/src/plugins/builtin/chat/index.tsx +2 -2
- package/src/plugins/builtin/chat/share/flow.tsx +1 -1
- package/src/plugins/builtin/chat/sidebar-rows.ts +1 -1
- package/src/plugins/builtin/chat/sidebar.tsx +2 -2
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +2 -2
- package/src/plugins/builtin/cloud/askg/pane.tsx +2 -1
- package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +3 -2
- package/src/plugins/builtin/cloud/auth-actions.tsx +1 -1
- package/src/plugins/builtin/cloud/auth-commands.ts +1 -1
- package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -1
- package/src/plugins/builtin/cloud/team/acm-tab.tsx +3 -3
- package/src/plugins/builtin/cloud/team/capability.ts +2 -2
- package/src/plugins/builtin/cloud/team/command.ts +4 -4
- package/src/plugins/builtin/cloud/team/module.tsx +3 -3
- package/src/plugins/builtin/cloud/team/pane-model.ts +2 -2
- package/src/plugins/builtin/cloud/team/pane-sections.tsx +1 -1
- package/src/plugins/builtin/cloud/team/pane-ui.tsx +1 -1
- package/src/plugins/builtin/cloud/team/pane.tsx +3 -3
- package/src/plugins/builtin/cloud/team/status-widget.tsx +3 -3
- package/src/plugins/builtin/cloud/team/team-state-host.ts +1 -1
- package/src/plugins/builtin/cloud/team/views.ts +2 -2
- package/src/plugins/builtin/cloud/thesis/board-pane.tsx +1 -1
- package/src/plugins/builtin/cloud/thesis/detail.tsx +12 -10
- package/src/plugins/builtin/cloud/thesis/ticker-tab.tsx +1 -1
- package/src/plugins/builtin/cloud/upgrade-command.ts +1 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +8 -4
- package/src/plugins/builtin/connections/pane.tsx +16 -5
- package/src/plugins/builtin/cpi/pane.tsx +5 -4
- package/src/plugins/builtin/custom-view/pane.tsx +2 -2
- package/src/plugins/builtin/dividend-yield/headless.ts +8 -2
- package/src/plugins/builtin/dividend-yield/pane.tsx +4 -2
- package/src/plugins/builtin/docs/blocks.tsx +13 -2
- package/src/plugins/builtin/docs/pane.tsx +6 -5
- package/src/plugins/builtin/doe/index.tsx +11 -4
- package/src/plugins/builtin/doe/pane.tsx +5 -4
- package/src/plugins/builtin/doe/prices.ts +84 -0
- package/src/plugins/builtin/doe/series.ts +4 -1
- package/src/plugins/builtin/earnings/board-model.ts +18 -1
- package/src/plugins/builtin/earnings/board-table.ts +18 -3
- package/src/plugins/builtin/earnings/headless.ts +13 -2
- package/src/plugins/builtin/earnings/history-model.ts +1 -1
- package/src/plugins/builtin/earnings/history-table.ts +2 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +7 -21
- package/src/plugins/builtin/earnings-calls/index.tsx +2 -0
- package/src/plugins/builtin/earnings-calls/lock-note.ts +17 -0
- package/src/plugins/builtin/equity-screener/headless.ts +59 -1
- package/src/plugins/builtin/equity-screener/live.ts +82 -31
- package/src/plugins/builtin/equity-screener/pane.tsx +60 -23
- package/src/plugins/builtin/estimate-revisions/client.ts +1 -1
- package/src/plugins/builtin/estimate-revisions/headless.ts +9 -2
- package/src/plugins/builtin/estimate-revisions/index.tsx +3 -0
- package/src/plugins/builtin/estimate-revisions/lock-note.ts +23 -0
- package/src/plugins/builtin/executives/index.tsx +18 -0
- package/src/plugins/builtin/exposure/charts.tsx +4 -3
- package/src/plugins/builtin/filing-events/index.tsx +8 -10
- package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
- package/src/plugins/builtin/gpu/equities.tsx +9 -6
- package/src/plugins/builtin/help/feedback-tab.tsx +1 -1
- package/src/plugins/builtin/iv-history/format.ts +3 -0
- package/src/plugins/builtin/iv-history/headless.ts +3 -1
- package/src/plugins/builtin/iv-history/index.tsx +3 -1
- package/src/plugins/builtin/iv-history/model.ts +3 -1
- package/src/plugins/builtin/iv-history/screen-pane.tsx +3 -1
- package/src/plugins/builtin/iv-history/universe.ts +1 -1
- package/src/plugins/builtin/jobs/headless.ts +6 -20
- package/src/plugins/builtin/jobs/index.tsx +2 -0
- package/src/plugins/builtin/jobs/lock-note.ts +16 -0
- package/src/plugins/builtin/notes/index.tsx +1 -1
- package/src/plugins/builtin/notes/owner.tsx +2 -2
- package/src/plugins/builtin/options/carry.ts +36 -0
- package/src/plugins/builtin/options/index.tsx +2 -1
- package/src/plugins/builtin/options/model-spot.ts +94 -0
- package/src/plugins/builtin/options/open-interest-as-of.ts +74 -0
- package/src/plugins/builtin/options/view.tsx +69 -11
- package/src/plugins/builtin/options-calculator/pane.tsx +56 -1
- package/src/plugins/builtin/options-calculator/premium-history.ts +34 -0
- package/src/plugins/builtin/options-scenario/client.ts +33 -9
- package/src/plugins/builtin/options-scenario/csp.ts +168 -0
- package/src/plugins/builtin/options-scenario/evidence.ts +3 -0
- package/src/plugins/builtin/options-scenario/headless.ts +24 -8
- package/src/plugins/builtin/options-scenario/index.tsx +3 -1
- package/src/plugins/builtin/options-scenario/pane.tsx +32 -6
- package/src/plugins/builtin/portfolio-list/mutations.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +4 -2
- package/src/plugins/builtin/research/event-model.ts +7 -4
- package/src/plugins/builtin/risk-factors/index.tsx +18 -0
- package/src/plugins/builtin/risk-factors/pane.tsx +2 -1
- package/src/plugins/builtin/scanner/headless.ts +9 -2
- package/src/plugins/builtin/scanner/index.ts +3 -0
- package/src/plugins/builtin/scanner/lock-note.ts +17 -0
- package/src/plugins/builtin/series-inventory.ts +2 -1
- package/src/plugins/builtin/shared/cloud-denied.ts +39 -8
- package/src/plugins/builtin/shared/earnings-report.ts +10 -4
- package/src/plugins/builtin/shared/pro-lock-note.ts +58 -0
- package/src/plugins/builtin/shared/series-list-detail.tsx +10 -5
- package/src/plugins/builtin/social-mentions/client.ts +3 -3
- package/src/plugins/builtin/social-mentions/headless.ts +6 -2
- package/src/plugins/builtin/social-mentions/index.tsx +2 -1
- package/src/plugins/builtin/social-mentions/lock-note.ts +15 -0
- package/src/plugins/builtin/supply-chain/flow.tsx +15 -13
- package/src/plugins/builtin/supply-chain/graph.tsx +6 -4
- package/src/plugins/builtin/supply-chain/pane.tsx +8 -5
- package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +14 -1
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +9 -7
- package/src/plugins/builtin/ticker-detail/headless.ts +4 -1
- package/src/plugins/builtin/ticker-detail/overview/components.tsx +105 -104
- package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -2
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +34 -8
- package/src/plugins/builtin/vol-surface/pane.tsx +6 -3
- package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
- package/src/renderers/dom/data-table/index.tsx +48 -15
- package/src/renderers/dom/data-table/row.tsx +1 -1
- package/src/renderers/dom/desktop/popover.tsx +6 -15
- package/src/renderers/dom/host/input.tsx +27 -3
- package/src/renderers/dom/host/surface-3d.tsx +6 -4
- package/src/renderers/dom/styles.css +26 -6
- package/src/renderers/electrobun/view/app-services.ts +2 -22
- package/src/renderers/electrobun/view/news-source.ts +42 -0
- package/src/series/validate.ts +5 -0
- package/src/shares/dialog.tsx +2 -1
- package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
- package/src/sources/provider-router/financials.ts +5 -0
- package/src/{plugins/builtin/cloud/team → team}/collections-sync.ts +6 -6
- package/src/{plugins/builtin/cloud/team → team}/collections.ts +3 -3
- package/src/{plugins/builtin/cloud/team → team}/model.ts +2 -2
- package/src/{plugins/builtin/cloud/team → team}/pane-request.ts +1 -1
- package/src/{plugins/builtin/cloud/team → team}/store.ts +3 -3
- package/src/types/data-provider.ts +3 -0
- package/src/types/financials.ts +28 -0
- package/src/types/plugin.ts +17 -4
- package/src/utils/decimal-comma.ts +18 -0
- package/src/utils/exchanges.ts +7 -0
- package/src/utils/financial-statements.ts +25 -0
- package/src/utils/forward-dividend.ts +39 -9
- package/src/utils/fundamentals.ts +41 -1
- package/src/utils/option-expiry.ts +16 -0
- package/src/utils/sec.ts +2 -1
|
@@ -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:
|
|
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
|
|
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 {
|
|
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
|
-
/**
|
|
72
|
-
|
|
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:
|
|
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
|
|
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
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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):
|
|
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
|
|
36
|
-
|
|
37
|
-
if (!isFiniteNumber(
|
|
38
|
-
if (
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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 (
|
|
50
|
-
metrics.marketCap = liveMetric(cap, cap.value * (
|
|
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:
|
|
85
|
+
const liveRows = new WeakMap<ScreenRow, { quote: Quote | null; row: LiveScreenRow }>();
|
|
57
86
|
|
|
58
|
-
|
|
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
|
|
64
|
-
if (
|
|
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
|
|
127
|
-
const next = [...shown,
|
|
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:
|
|
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
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
+
}
|