@gloom-sh/terminal 0.19.0 → 0.19.1
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +23 -1
- package/src/api-client/app-attention.ts +3 -0
- package/src/api-client/beneficial-owners.ts +3 -0
- package/src/api-client/company-kpis.ts +3 -0
- package/src/api-client/data.ts +2 -1
- package/src/api-client/hiring.ts +3 -0
- package/src/api-client/index.ts +4 -0
- package/src/api-client/ipo.ts +24 -1
- package/src/api-client/mna.ts +3 -0
- package/src/api-client/paths.ts +9 -1
- package/src/api-client/roadmap.ts +85 -0
- package/src/api-client/types.ts +12 -0
- package/src/architecture/core-builtin-allowlist.json +2 -2
- package/src/cli/commands/market.ts +35 -12
- package/src/cli/commands/news-search.ts +22 -0
- package/src/cli/commands/overview.ts +86 -20
- package/src/cli/commands/ticker.ts +32 -2
- package/src/cli/errors.ts +17 -2
- package/src/cli/glossary.ts +6 -0
- package/src/cli/index-roots.ts +0 -7
- package/src/cli/listing-arg.ts +57 -10
- package/src/cli/pane-functions/capabilities.ts +1 -0
- package/src/cli/pane-functions/discovery.ts +21 -1
- package/src/cli/pane-functions/freshness.ts +7 -1
- package/src/cli/pane-functions/function-help.ts +7 -0
- package/src/cli/pane-functions/index.ts +6 -3
- package/src/cli/quote-answer.ts +1 -1
- package/src/cli/saved-copy-persistence.ts +88 -0
- package/src/components/command-bar/list/model.ts +5 -0
- package/src/components/command-bar/routes/root/runtime.ts +6 -7
- package/src/components/command-bar/selection-runtime.ts +52 -3
- package/src/components/command-bar/surface/index.tsx +2 -0
- package/src/components/layout/pane/capture-stamp.ts +2 -2
- package/src/components/layout/pane/footer/index.tsx +22 -6
- package/src/components/layout/pane/footer/model.ts +83 -15
- package/src/components/layout/pane/status-footer.ts +35 -2
- package/src/components/layout/status-widget-space.ts +9 -0
- package/src/components/ticker/list-table-view.tsx +4 -20
- package/src/components/ui/display.tsx +18 -3
- package/src/components/ui/icon.tsx +4 -1
- package/src/data/plugin-cache.ts +5 -3
- package/src/data/saved-copies.ts +8 -0
- package/src/docs/snapshot.json +32 -5
- package/src/i18n/es.ts +2 -4
- package/src/i18n/ja.ts +2 -4
- package/src/i18n/ko.ts +2 -4
- package/src/i18n/zh-cn.ts +2 -4
- package/src/i18n/zh-tw.ts +2 -4
- package/src/market-data/depositary-pair.ts +25 -1
- package/src/market-data/quotes/metadata.ts +7 -0
- package/src/news/aggregator.ts +7 -1
- package/src/news/types.ts +6 -0
- package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
- package/src/plugins/builtin/alerts/cli.ts +34 -11
- package/src/plugins/builtin/alerts/event-rows.ts +24 -3
- package/src/plugins/builtin/alerts/events-pane.tsx +15 -6
- package/src/plugins/builtin/alerts/headless.ts +10 -6
- package/src/plugins/builtin/cds/client.ts +142 -30
- package/src/plugins/builtin/cds/headless.ts +35 -8
- package/src/plugins/builtin/cds/model.ts +145 -18
- package/src/plugins/builtin/cds/pane.tsx +16 -5
- package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
- package/src/plugins/builtin/chart-composer/index.tsx +4 -5
- package/src/plugins/builtin/chat/controller/index.ts +5 -1
- package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
- package/src/plugins/builtin/chat/controller/state.ts +25 -2
- package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
- package/src/plugins/builtin/chat/controller/utils.ts +7 -2
- package/src/plugins/builtin/chat/status-preview.ts +79 -0
- package/src/plugins/builtin/chat/status-widget.tsx +147 -10
- package/src/plugins/builtin/chat/unread-badge.ts +51 -10
- package/src/plugins/builtin/clinical-trials/client.ts +49 -4
- package/src/plugins/builtin/clinical-trials/model.ts +86 -22
- package/src/plugins/builtin/clinical-trials/pane.tsx +46 -27
- package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +87 -9
- package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
- package/src/plugins/builtin/company-attention/headless.ts +2 -1
- package/src/plugins/builtin/company-attention/pane.tsx +3 -0
- package/src/plugins/builtin/company-kpis/headless.ts +8 -3
- package/src/plugins/builtin/company-kpis/model.ts +73 -5
- package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
- package/src/plugins/builtin/composite-plugins.ts +2 -1
- package/src/plugins/builtin/credit-documents/headless.ts +3 -3
- package/src/plugins/builtin/doe/headless.ts +3 -2
- package/src/plugins/builtin/doe/pane.tsx +13 -3
- package/src/plugins/builtin/earnings-calls/data.ts +18 -6
- package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
- package/src/plugins/builtin/earnings-calls/pane.tsx +7 -0
- package/src/plugins/builtin/econ/calendar-model.ts +48 -1
- package/src/plugins/builtin/econ/calendar-source.ts +4 -2
- package/src/plugins/builtin/econ/detail-view.tsx +2 -2
- package/src/plugins/builtin/econ/figures.ts +48 -0
- package/src/plugins/builtin/econ/index.tsx +14 -5
- package/src/plugins/builtin/econ/print-watch.ts +3 -1
- package/src/plugins/builtin/econ-statistics/client.ts +11 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +10 -2
- package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +3 -0
- package/src/plugins/builtin/help/index.tsx +61 -59
- package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
- package/src/plugins/builtin/help/table-cells.tsx +13 -2
- package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
- package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
- package/src/plugins/builtin/holders/headless.ts +4 -2
- package/src/plugins/builtin/holders/pane.tsx +3 -0
- package/src/plugins/builtin/holders/report-rows.ts +4 -1
- package/src/plugins/builtin/insider/headless.ts +2 -2
- package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
- package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
- package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
- package/src/plugins/builtin/macro-day/headless.ts +12 -5
- package/src/plugins/builtin/macro-day/index.tsx +1 -1
- package/src/plugins/builtin/macro-day/model.ts +39 -5
- package/src/plugins/builtin/macro-day/pane.tsx +27 -19
- package/src/plugins/builtin/market-movers/client.ts +2 -0
- package/src/plugins/builtin/market-movers/headless.ts +3 -1
- package/src/plugins/builtin/market-movers/screener.ts +8 -4
- package/src/plugins/builtin/mna/headless.ts +2 -0
- package/src/plugins/builtin/mna/pane.tsx +3 -1
- package/src/plugins/builtin/news/headless.ts +3 -1
- package/src/plugins/builtin/news/index.tsx +3 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -6
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
- package/src/plugins/builtin/portfolio-list/table.tsx +4 -6
- package/src/plugins/builtin/realized-vol/client.ts +13 -5
- package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
- package/src/plugins/builtin/realized-vol/headless.ts +8 -4
- package/src/plugins/builtin/realized-vol/model.ts +37 -6
- package/src/plugins/builtin/realized-vol/pane.tsx +9 -7
- package/src/plugins/builtin/research/event-model.ts +30 -4
- package/src/plugins/builtin/research/index.tsx +7 -3
- package/src/plugins/builtin/research/relative-valuation-headless.ts +8 -25
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +52 -15
- package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
- package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
- package/src/plugins/builtin/risk-factors/data.ts +4 -2
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- package/src/plugins/builtin/roadmap/index.tsx +28 -0
- package/src/plugins/builtin/roadmap/model.ts +67 -0
- package/src/plugins/builtin/roadmap/pane.tsx +244 -0
- package/src/plugins/builtin/scanner/live-headless.ts +21 -2
- package/src/plugins/builtin/sec/headless.ts +2 -1
- package/src/plugins/builtin/sec/index.tsx +3 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +13 -5
- package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
- package/src/plugins/builtin/shared/report-freshness.ts +8 -0
- package/src/plugins/builtin/shared/research-cells.tsx +1 -1
- package/src/plugins/builtin/shared/series-cache.ts +4 -4
- package/src/plugins/builtin/thirteenf/api.ts +12 -1
- package/src/plugins/builtin/thirteenf/data.ts +7 -7
- package/src/plugins/builtin/thirteenf/headless.ts +31 -4
- package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +11 -6
- package/src/plugins/builtin/treasury-auctions/headless.ts +21 -12
- package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
- package/src/plugins/builtin/volatility/pane.tsx +5 -2
- package/src/plugins/builtin/yield-curve/client.ts +61 -1
- package/src/plugins/builtin/yield-curve/headless.ts +9 -6
- package/src/plugins/builtin/yield-curve/index.tsx +34 -10
- package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
- package/src/plugins/catalog-browser.ts +2 -1
- package/src/plugins/host-link.ts +1 -1
- package/src/portfolio/column-values.ts +11 -7
- package/src/renderers/dom/desktop/icons.tsx +7 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
- package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
- package/src/renderers/electrobun/bun/index.ts +26 -2
- package/src/renderers/electrobun/bun/window/create.ts +43 -4
- package/src/renderers/electrobun/bun/window/events.ts +20 -0
- package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
- package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
- package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/news-source.ts +2 -0
- package/src/renderers/electrobun/view/ui-host.tsx +8 -0
- package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
- package/src/sources/gloomberb-cloud/index.ts +42 -8
- package/src/tickers/company-name-listing.ts +53 -13
- package/src/tickers/company-name.ts +6 -1
- package/src/tickers/index-roots.ts +7 -0
- package/src/tickers/listing-fate.ts +2 -2
- package/src/tickers/search/index.ts +6 -2
- package/src/tickers/search/ranking.ts +5 -1
- package/src/tickers/search/upsert.ts +13 -0
- package/src/tickers/ticker-changes.ts +8 -4
- package/src/types/company-from.ts +40 -0
- package/src/types/data-provider.ts +7 -0
- package/src/types/financials.ts +27 -0
- package/src/types/headless.ts +6 -0
- package/src/utils/crypto-pair.ts +121 -7
- package/src/utils/declared-dividends.ts +1 -1
- package/src/utils/fundamentals.ts +36 -5
- package/src/version.ts +2 -2
|
@@ -46,6 +46,10 @@ import {
|
|
|
46
46
|
type TranscriptSection,
|
|
47
47
|
} from "./model";
|
|
48
48
|
import { REPORT_FILTER, REPORT_PAGE, REPORT_ROWS } from "../shared/headless-options";
|
|
49
|
+
import { companyDataNotes } from "../../../types/company-from";
|
|
50
|
+
import { savedCopyMark, unansweredRefreshError } from "../../../data/saved-copies";
|
|
51
|
+
|
|
52
|
+
const optionalErrors = (error: string | null) => error ? [error] : undefined;
|
|
49
53
|
|
|
50
54
|
const CALL_COLUMNS = [
|
|
51
55
|
{ key: "ticker", header: "Ticker" },
|
|
@@ -170,9 +174,12 @@ function projectEarningsCallsHeadless(
|
|
|
170
174
|
columns: CALL_COLUMNS,
|
|
171
175
|
rows,
|
|
172
176
|
...(next ? { textRows: rows.filter((row) => row.id !== next.id) } : {}),
|
|
173
|
-
errors: result
|
|
177
|
+
errors: optionalErrors(unansweredRefreshError(result)),
|
|
174
178
|
...(sourceLimitReached || result.pending ? { complete: false } : {}),
|
|
179
|
+
...(result.companyFrom ? { notes: companyDataNotes(result.companyFrom) } : {}),
|
|
175
180
|
metadata: {
|
|
181
|
+
// A list served from a saved copy ends with when it was saved, instead of its refresh failure.
|
|
182
|
+
...savedCopyMark(result),
|
|
176
183
|
...(next ? { notices: [nextCallNotice(next, now)], nextCall: { id: next.id, callAt: next.callAt, timeKnown: callTimeKnown(next), in: callCountdown(next, now) } } : {}),
|
|
177
184
|
fetchedAt: result.fetchedAt,
|
|
178
185
|
stale: result.stale,
|
|
@@ -211,9 +218,11 @@ function transcriptResult(
|
|
|
211
218
|
return {
|
|
212
219
|
columns: TRANSCRIPT_COLUMNS,
|
|
213
220
|
rows,
|
|
214
|
-
errors: calls
|
|
221
|
+
errors: optionalErrors(unansweredRefreshError(calls)),
|
|
215
222
|
...(calls.stale || calls.pending ? { complete: false } : {}),
|
|
223
|
+
...(calls.companyFrom ? { notes: companyDataNotes(calls.companyFrom) } : {}),
|
|
216
224
|
metadata: {
|
|
225
|
+
...savedCopyMark(calls),
|
|
217
226
|
stale: calls.stale,
|
|
218
227
|
callListFetchedAt: calls.fetchedAt,
|
|
219
228
|
callListPending: calls.pending ?? false,
|
|
@@ -283,10 +292,11 @@ function projectCalendar(
|
|
|
283
292
|
columns: CALENDAR_COLUMNS,
|
|
284
293
|
textColumns: sameClock ? CALENDAR_TEXT_COLUMNS.filter((column) => column.key !== "local") : CALENDAR_TEXT_COLUMNS,
|
|
285
294
|
rows,
|
|
286
|
-
errors: result
|
|
295
|
+
errors: optionalErrors(unansweredRefreshError(result)),
|
|
287
296
|
...(result.stale || result.truncated ? { complete: false } : {}),
|
|
288
297
|
empty: "No calls on the calendar these days.",
|
|
289
298
|
metadata: {
|
|
299
|
+
...savedCopyMark(result),
|
|
290
300
|
view: "upcoming",
|
|
291
301
|
fetchedAt: result.fetchedAt,
|
|
292
302
|
stale: result.stale,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type QueryBarView,
|
|
20
20
|
type StatItem,
|
|
21
21
|
} from "../../../components";
|
|
22
|
+
import { useCompanyDataFooter } from "../../../components/layout/pane/status-footer";
|
|
22
23
|
import { handleRefreshKey } from "../../../components/data-table/table-pane";
|
|
23
24
|
import { AudioPlayer, useCanPlayAudio } from "../../../components/ui";
|
|
24
25
|
import { useShortcut } from "../../../react/input";
|
|
@@ -277,6 +278,8 @@ function CallsListView({
|
|
|
277
278
|
const [listStatus, setListStatus] = useState<"idle" | "loading" | "loaded" | "error">(initialShelf ? "loaded" : "idle");
|
|
278
279
|
const [listError, setListError] = useState<{ message: string; status?: number } | null>(null);
|
|
279
280
|
const [stale, setStale] = useState(initialShelf?.stale === true);
|
|
281
|
+
// The company's home line the shelf is read from, for a receipt or a foreign line.
|
|
282
|
+
const [companyFrom, setCompanyFrom] = useState(initialShelf?.companyFrom ?? null);
|
|
280
283
|
const [listFetchedAt, setListFetchedAt] = useState(initialShelf?.fetchedAt ?? 0);
|
|
281
284
|
const listRequestVersion = useRef(0);
|
|
282
285
|
const [listCompletedRequest, setListCompletedRequest] = useState(0);
|
|
@@ -340,6 +343,7 @@ function CallsListView({
|
|
|
340
343
|
setNextOffset(result.calls.length);
|
|
341
344
|
setHasMore(result.sourceLimitReached === true);
|
|
342
345
|
setStale(result.stale);
|
|
346
|
+
setCompanyFrom(result.companyFrom ?? null);
|
|
343
347
|
setListFetchedAt(result.fetchedAt);
|
|
344
348
|
setListPending(result.pending === true && result.calls.length === 0);
|
|
345
349
|
setListError(
|
|
@@ -410,6 +414,7 @@ function CallsListView({
|
|
|
410
414
|
setListStatus(held ? "loaded" : "idle");
|
|
411
415
|
setListPending(held?.pending === true && held.calls.length === 0);
|
|
412
416
|
setStale(held?.stale === true);
|
|
417
|
+
setCompanyFrom(held?.companyFrom ?? null);
|
|
413
418
|
setListFetchedAt(held?.fetchedAt ?? 0);
|
|
414
419
|
setListError(null);
|
|
415
420
|
setLookup(null);
|
|
@@ -817,6 +822,8 @@ function CallsListView({
|
|
|
817
822
|
[focusSearch, scrollTranscriptBy, openSource, playerShown, playback.toggle, playback.seekBy],
|
|
818
823
|
);
|
|
819
824
|
|
|
825
|
+
// A receipt's or foreign line's calls are its company's, read from the home line.
|
|
826
|
+
useCompanyDataFooter(companyFrom, !readerOpen);
|
|
820
827
|
usePaneFooter(
|
|
821
828
|
footerId,
|
|
822
829
|
() => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createPluginCache } from "../../../data/plugin-cache";
|
|
2
2
|
import type { DataTableColumn } from "../../../components";
|
|
3
3
|
import { colors } from "../../../theme/colors";
|
|
4
|
-
import { displayWidth } from "../../../utils/format";
|
|
4
|
+
import { displayWidth, MISSING_VALUE } from "../../../utils/format";
|
|
5
5
|
import { fetchEconCalendar } from "./calendar-source";
|
|
6
6
|
import { parseEconFigure } from "./figures";
|
|
7
7
|
import { isAllDayEvent, type EconEvent, type EconImpact } from "./types";
|
|
@@ -235,6 +235,51 @@ export function eventCountry(event: Pick<EconEvent, "country" | "event">): strin
|
|
|
235
235
|
return euroAreaMember(event) ?? event.country;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
/** What the feeds call the Fed's rate decision. */
|
|
239
|
+
const FED_RATE_DECISION_NAMES = new Set(["federal funds rate", "fed interest rate decision"]);
|
|
240
|
+
/** A figure that is a range (3.75-4.00%) rather than one level. */
|
|
241
|
+
const RANGE_FIGURE = /\d\s*[-\u2013]\s*\d/;
|
|
242
|
+
const FED_FUNDS_UPPER_BOUND = "Federal Funds Rate (upper bound)";
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* A row's name as the calendar shows it. The Fed's decision is figured as one
|
|
246
|
+
* level, the top of the target range (4.00% for 3.75-4.00%), so it is named
|
|
247
|
+
* for that bound rather than read as the range or the effective rate; a row
|
|
248
|
+
* whose figures are the range keeps the feed's name. Whatever matches rows by
|
|
249
|
+
* name (alerts, the FRED history) reads the feed's own, `event.event`.
|
|
250
|
+
*/
|
|
251
|
+
export function econEventName(event: Pick<EconEvent, "country" | "event" | "actual" | "forecast" | "prior">): string {
|
|
252
|
+
if ((event.country !== "US" && event.country !== "USD") || !FED_RATE_DECISION_NAMES.has(event.event.trim().toLowerCase())) return event.event;
|
|
253
|
+
return [event.actual, event.forecast, event.prior].some((figure) => !!figure && RANGE_FIGURE.test(figure)) ? event.event : FED_FUNDS_UPPER_BOUND;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Letters and digits only, lower case: "Non-Farm" and "nonfarm" read alike. */
|
|
257
|
+
const compactName = (text: string) => text.toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Whether a row's name carries every word of `query`, case, spaces and
|
|
261
|
+
* punctuation aside: `nonfarm` finds Non Farm Payrolls and Non-Farm
|
|
262
|
+
* Employment Change, `fed funds` the Federal Funds Rate. The name shown and the
|
|
263
|
+
* feed's own are both read.
|
|
264
|
+
*/
|
|
265
|
+
export function matchesEventSearch(event: Pick<EconEvent, "country" | "event" | "actual" | "forecast" | "prior">, query: string): boolean {
|
|
266
|
+
const words = query.split(/\s+/).map(compactName).filter(Boolean);
|
|
267
|
+
if (words.length === 0) return true;
|
|
268
|
+
const names = [event.event, econEventName(event)].map(compactName);
|
|
269
|
+
return names.some((name) => words.every((word) => name.includes(word)));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* A release still to come that has a prior and no forecast: the consensus is
|
|
274
|
+
* not in yet, which the calendar draws as a dash.
|
|
275
|
+
*/
|
|
276
|
+
export function awaitsConsensus(event: Pick<EconEvent, "actual" | "forecast" | "prior">): boolean {
|
|
277
|
+
return !event.actual?.trim() && !event.forecast?.trim() && !!event.prior?.trim();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** What a dash under Forecast means on a row still to come. */
|
|
281
|
+
export const NO_CONSENSUS_NOTE = `${MISSING_VALUE} forecast = no consensus yet`;
|
|
282
|
+
|
|
238
283
|
/** A national euro-area release matches EU (and G7, as EU) and its own member's code. */
|
|
239
284
|
export function matchesCountry(event: Pick<EconEvent, "country" | "event">, filter: CountryFilter): boolean {
|
|
240
285
|
const member = euroAreaMember(event);
|
|
@@ -376,6 +421,8 @@ export function subscribeCalendar(listener: () => void): () => void {
|
|
|
376
421
|
}
|
|
377
422
|
|
|
378
423
|
export interface EconCalendarLoadResult extends EconCalendarCacheEntry {
|
|
424
|
+
/** `stale-fallback`: the copy an earlier read saved, answering because its refresh failed. */
|
|
425
|
+
source?: "cache" | "network" | "stale-fallback";
|
|
379
426
|
refreshError?: string;
|
|
380
427
|
}
|
|
381
428
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { apiClient, type CloudEconEventPayload } from "../../../api-client";
|
|
2
2
|
import { ApiRequestError, isPermanentClientError } from "../../../api-client/errors";
|
|
3
|
+
import { withPercentUnits } from "./figures";
|
|
3
4
|
import type { EconEvent } from "./types";
|
|
4
5
|
|
|
5
6
|
function toEconEvent(event: CloudEconEventPayload): EconEvent {
|
|
@@ -9,9 +10,10 @@ function toEconEvent(event: CloudEconEventPayload): EconEvent {
|
|
|
9
10
|
};
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
/** The calendar, a percent indicator's figures in percent on every row (`withPercentUnits`). */
|
|
12
14
|
export async function fetchEconCalendar(): Promise<EconEvent[]> {
|
|
13
15
|
const events = await apiClient.getCloudEconomicCalendar();
|
|
14
|
-
return events.map(toEconEvent);
|
|
16
|
+
return withPercentUnits(events.map(toEconEvent));
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/** The stored calendar from a day, or why it was not read so the caller reads the current calendar instead. */
|
|
@@ -40,7 +42,7 @@ export async function fetchEconCalendarFrom(from: string): Promise<EconCalendarF
|
|
|
40
42
|
if (!payload || !Array.isArray(payload.events) || !(payload.storedFrom === null || /^\d{4}-\d{2}-\d{2}$/.test(String(payload.storedFrom)))) {
|
|
41
43
|
return { events: null, failure: "the server returned an unreadable calendar" };
|
|
42
44
|
}
|
|
43
|
-
return { events: payload.events.map(toEconEvent), storedFrom: payload.storedFrom };
|
|
45
|
+
return { events: withPercentUnits(payload.events.map(toEconEvent)), storedFrom: payload.storedFrom };
|
|
44
46
|
} catch (error) {
|
|
45
47
|
if (error instanceof ApiRequestError && error.status === 404) return { events: null, failure: null };
|
|
46
48
|
if (isPermanentClientError(error)) throw error;
|
|
@@ -31,7 +31,7 @@ import { Box, Text } from "../../../ui";
|
|
|
31
31
|
import { displayWidth, truncateToDisplayWidth } from "../../../utils/format";
|
|
32
32
|
import { useOpenTickerChoice } from "../shared/ticker-choice";
|
|
33
33
|
import { resolveFredMapping, projectFredHistory, fredHistoryUnits } from "./fred-series-map";
|
|
34
|
-
import { actualColor, CALENDAR_TIME_ZONE_LABEL, timeLabel } from "./calendar-model";
|
|
34
|
+
import { actualColor, CALENDAR_TIME_ZONE_LABEL, econEventName, timeLabel } from "./calendar-model";
|
|
35
35
|
import { hasNoStatedTime, isAllDayEvent, type EconEvent } from "./types";
|
|
36
36
|
|
|
37
37
|
interface EconDetailViewProps {
|
|
@@ -238,7 +238,7 @@ export function EconDetailView({ event, width, height, focused }: EconDetailView
|
|
|
238
238
|
const rows = useMemo<HistoryRow[]>(() => projected
|
|
239
239
|
.map((obs, index) => ({ date: obs.date, value: obs.value, change: index > 0 ? obs.value - projected[index - 1]!.value : null }))
|
|
240
240
|
.reverse(), [projected]);
|
|
241
|
-
const label = event
|
|
241
|
+
const label = econEventName(event);
|
|
242
242
|
const series = useMemo(() => [staticSeries(
|
|
243
243
|
projected.map((obs) => scalarPoint(new Date(`${obs.date}T00:00:00Z`), obs.value)),
|
|
244
244
|
{ id: "fred-history", label, color: colors.positive, calendarSpaced: true },
|
|
@@ -70,3 +70,51 @@ export function econRowFigures(row: { actual?: string | null; forecast?: string
|
|
|
70
70
|
scale: first?.scale ?? null,
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
|
+
|
|
74
|
+
/** A figure printed as a bare number, with no unit and no scale letter: `0.6`, `-3`, `1,5`. */
|
|
75
|
+
const BARE_NUMBER = /^[+-]?(?:\d+(?:[.,]\d+)?|\.\d+)$/;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Release names that are a percent whatever one row's figures say: a change
|
|
79
|
+
* month on month, quarter on quarter or year on year, and the rates published
|
|
80
|
+
* in percent. "Consumer Inflation Expectations" is a balance, not a rate, and
|
|
81
|
+
* is not one of them.
|
|
82
|
+
*/
|
|
83
|
+
const PERCENT_NAME = /\b(?:mom|qoq|yoy)\b|(?:^|\s)[mqy]\/[mqy](?=\s|$)|\b(?:inflation|unemployment|jobless|interest|growth|mortgage|funds|participation|savings) rate\b/i;
|
|
84
|
+
|
|
85
|
+
type EconRowText = { country: string; event: string; indicatorId?: string | null; actual: string | null; forecast: string | null; prior: string | null };
|
|
86
|
+
|
|
87
|
+
const FIGURE_KEYS = ["actual", "forecast", "prior"] as const;
|
|
88
|
+
|
|
89
|
+
/** Every release of one indicator: its id, else its country and name. */
|
|
90
|
+
const indicatorKey = (event: Pick<EconRowText, "country" | "event" | "indicatorId">) => event.indicatorId || `${event.country}|${event.event}`;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The calendar with every figure of a percent indicator in percent. The feeds
|
|
94
|
+
* drop the sign on some rows: US Inflation Rate MoM's prior came as `0.6`, and
|
|
95
|
+
* the 30-year mortgage rate's as `7.32` beside its actual `7.41%`. An
|
|
96
|
+
* indicator is in percent when any of its figures is, or its name says so
|
|
97
|
+
* (`PERCENT_NAME`), and none of its figures is in another unit or scale; its
|
|
98
|
+
* bare figures then carry `%`. Anything else is left as the feed wrote it.
|
|
99
|
+
*/
|
|
100
|
+
export function withPercentUnits<T extends EconRowText>(events: readonly T[]): T[] {
|
|
101
|
+
const indicators = new Map<string, { percent: boolean; other: boolean }>();
|
|
102
|
+
for (const event of events) {
|
|
103
|
+
const key = indicatorKey(event);
|
|
104
|
+
const seen = indicators.get(key) ?? { percent: false, other: false };
|
|
105
|
+
if (PERCENT_NAME.test(event.event)) seen.percent = true;
|
|
106
|
+
for (const field of FIGURE_KEYS) {
|
|
107
|
+
const figure = parseEconFigure(event[field]);
|
|
108
|
+
if (!figure) continue;
|
|
109
|
+
if (figure.unit === "%") seen.percent = true;
|
|
110
|
+
else if (figure.unit || figure.scale !== 1) seen.other = true;
|
|
111
|
+
}
|
|
112
|
+
indicators.set(key, seen);
|
|
113
|
+
}
|
|
114
|
+
return events.map((event) => {
|
|
115
|
+
const seen = indicators.get(indicatorKey(event))!;
|
|
116
|
+
if (!seen.percent || seen.other) return event;
|
|
117
|
+
const bare = FIGURE_KEYS.filter((field) => BARE_NUMBER.test(event[field]?.trim() ?? ""));
|
|
118
|
+
return bare.length ? { ...event, ...Object.fromEntries(bare.map((field) => [field, `${event[field]!.trim()}%`])) } : event;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -36,10 +36,12 @@ import {
|
|
|
36
36
|
FILTER_CYCLE,
|
|
37
37
|
attachEconCalendarPersistence,
|
|
38
38
|
actualColor,
|
|
39
|
+
awaitsConsensus,
|
|
39
40
|
calendarDisplayRows,
|
|
40
41
|
CALENDAR_TIME_ZONE_LABEL,
|
|
41
42
|
dateKey,
|
|
42
43
|
dayLabel,
|
|
44
|
+
econEventName,
|
|
43
45
|
filteredEmptyTitle,
|
|
44
46
|
formatStaleness,
|
|
45
47
|
getCalendarCache,
|
|
@@ -50,6 +52,7 @@ import {
|
|
|
50
52
|
eventCountry,
|
|
51
53
|
matchesCountry,
|
|
52
54
|
matchesImpact,
|
|
55
|
+
NO_CONSENSUS_NOTE,
|
|
53
56
|
noScheduledReleasesMessage,
|
|
54
57
|
parseCountryFilter,
|
|
55
58
|
resetEconCalendarPersistence,
|
|
@@ -322,7 +325,7 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
322
325
|
: formatCountdown(nextEvent.date.getTime() - now);
|
|
323
326
|
// The countdown changes every tick, so it is footer status, not query bar
|
|
324
327
|
// context; the footer ellipsizes a long event name.
|
|
325
|
-
const nextText = nextEvent && nextCountdown ? `next ${nextEvent
|
|
328
|
+
const nextText = nextEvent && nextCountdown ? `next ${econEventName(nextEvent)} ${nextCountdown}` : null;
|
|
326
329
|
// The cached first paint is being replaced; its age only counts once a load has answered.
|
|
327
330
|
const showStale = stale && (!loading || calendar.updatedAt !== null);
|
|
328
331
|
// An empty list says it in its empty state instead.
|
|
@@ -330,6 +333,9 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
330
333
|
// What the High filter, where the calendar opens, leaves out, and the key that lists it (Shift+F steps back to All).
|
|
331
334
|
const hiddenCount = useMemo(() => lowerImpactHidden(events, impactFilter, countryFilter), [countryFilter, events, impactFilter]);
|
|
332
335
|
const hiddenStatus = hiddenCount > 0 && !detailEvent ? `${hiddenCount} lower-impact hidden` : null;
|
|
336
|
+
// The dash under Forecast on the row in hand, while its consensus is not in yet.
|
|
337
|
+
const focusedEvent = detailEvent ?? filtered[selectedIdx];
|
|
338
|
+
const noConsensus = !!focusedEvent && awaitsConsensus(focusedEvent);
|
|
333
339
|
const calendarStatus = useMemo<PaneFooterSegment[]>(() => [
|
|
334
340
|
...(hiddenStatus ? [{
|
|
335
341
|
id: "hidden-impact",
|
|
@@ -341,8 +347,11 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
341
347
|
...(nextText && !detailEvent ? [{ id: "next", parts: [{ text: nextText, tone: "muted" as const }] }] : []),
|
|
342
348
|
...(listedAfterStatus ? [{ id: "listed-after", parts: [{ text: listedAfterStatus, tone: "warning" as const }] }] : []),
|
|
343
349
|
...(showStale ? [{ id: "stale", parts: [{ text: "STALE", tone: "warning" as const }] }] : []),
|
|
350
|
+
// Behind the warnings, so a narrow footer gives it up before them, and ahead of the update time, which goes first.
|
|
351
|
+
...(noConsensus ? [{ id: "no-consensus", parts: [{ text: NO_CONSENSUS_NOTE, tone: "muted" as const }],
|
|
352
|
+
variants: [[{ text: "no consensus yet", tone: "muted" as const }]] }] : []),
|
|
344
353
|
...(staleness ? [{ id: "updated", parts: [{ text: staleness, tone: "muted" as const }] }] : []),
|
|
345
|
-
], [detailEvent, hiddenStatus, listedAfterStatus, nextText, showStale, staleness]);
|
|
354
|
+
], [detailEvent, hiddenStatus, listedAfterStatus, nextText, noConsensus, showStale, staleness]);
|
|
346
355
|
// A report of the pane (`gloom fn ECO`) says it too.
|
|
347
356
|
const listedThrough = listedEnd ? dateKey(listedEnd) : null;
|
|
348
357
|
useRemoteUiNode(listedAfter && listedThrough ? {
|
|
@@ -433,7 +442,7 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
433
442
|
return withCountryFlag({ text: country, color: colors.textMuted }, country, flagged);
|
|
434
443
|
}
|
|
435
444
|
case "event":
|
|
436
|
-
return { text: ev
|
|
445
|
+
return { text: econEventName(ev), color: colors.text };
|
|
437
446
|
case "actual":
|
|
438
447
|
return {
|
|
439
448
|
text: ev.actual ?? "—",
|
|
@@ -480,7 +489,7 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
|
|
|
480
489
|
focused={focused}
|
|
481
490
|
detailOpen={!!detailEvent}
|
|
482
491
|
onBack={() => setOpenKey(null)}
|
|
483
|
-
detailTitle={detailEvent
|
|
492
|
+
detailTitle={detailEvent ? econEventName(detailEvent) : undefined}
|
|
484
493
|
detailContent={detailContent}
|
|
485
494
|
rootWidth={width}
|
|
486
495
|
rootHeight={Math.max(1, height - 1)}
|
|
@@ -576,7 +585,7 @@ export const economicCalendarModule: PluginModule = {
|
|
|
576
585
|
description: "Upcoming economic events, releases, and indicators.",
|
|
577
586
|
// "econ" stays a keyword so the old ECON prefix still finds this pane.
|
|
578
587
|
keywords: ["econ", "economic", "calendar", "events", "macro", "releases", "fed", "cpi", "gdp", "rate decision", "ipca",
|
|
579
|
-
"brazil inflation", "pmi", "ism", "purchasing managers", "flash pmi"],
|
|
588
|
+
"brazil inflation", "pmi", "ism", "purchasing managers", "flash pmi", "jobs report", "nfp", "payrolls", "nonfarm"],
|
|
580
589
|
// ECO MX,BR opens the calendar on those countries.
|
|
581
590
|
shortcut: {
|
|
582
591
|
prefix: "ECO",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { GloomPluginContext } from "../../../types/plugin";
|
|
2
2
|
import {
|
|
3
|
+
econEventName,
|
|
3
4
|
eventCountry,
|
|
4
5
|
getCalendarCache,
|
|
5
6
|
landedHighImpactPrints,
|
|
@@ -57,7 +58,8 @@ const DEFAULT_DEPS: PrintWatchDeps = {
|
|
|
57
58
|
};
|
|
58
59
|
|
|
59
60
|
function printBody(event: EconEvent): string {
|
|
60
|
-
|
|
61
|
+
const name = econEventName(event);
|
|
62
|
+
return event.forecast ? `${name} ${event.actual} vs ${event.forecast}` : `${name} ${event.actual}`;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
type PrintWatchContext = Pick<GloomPluginContext, "notify" | "showPane">;
|
|
@@ -101,6 +101,17 @@ function bundleFrom(unaligned: StatBuild[], errors: string[]): StatsBundle {
|
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* The bundle's errors less the refresh failures of statistics the copy an
|
|
106
|
+
* earlier read saved answered for: those say when that copy is from instead.
|
|
107
|
+
*/
|
|
108
|
+
export function unansweredErrors(bundle: StatsBundle): string[] {
|
|
109
|
+
const answered = new Set(bundle.builds.flatMap(({ stat, cache }) => (
|
|
110
|
+
cache?.source === "stale-fallback" && cache.refreshError ? [`${stat.seriesId}: ${cache.refreshError}`] : []
|
|
111
|
+
)));
|
|
112
|
+
return bundle.errors.filter((error) => !answered.has(error));
|
|
113
|
+
}
|
|
114
|
+
|
|
104
115
|
/** Builds whatever the on-disk cache can already answer, for an instant first paint. */
|
|
105
116
|
export function getCachedStatsBundle(stats: readonly StatDef[] = STATS): StatsBundle | null {
|
|
106
117
|
const builds: StatBuild[] = [];
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
HeadlessPaneDefinition,
|
|
4
4
|
HeadlessPaneEntry,
|
|
5
5
|
} from "../../../types/plugin";
|
|
6
|
+
import { servedFromSavedCopy } from "../../../data/saved-copies";
|
|
6
7
|
import { fredSourceLabel } from "../../../sources/gloomberb-cloud/fred-series";
|
|
7
8
|
import { formatBasisPoints, toBasisPoints } from "../../../utils/basis-points";
|
|
8
9
|
import { formatNumber } from "../../../utils/format";
|
|
@@ -10,6 +11,7 @@ import type { StatDef } from "./defs";
|
|
|
10
11
|
import {
|
|
11
12
|
createStatSeriesLoader,
|
|
12
13
|
loadStatsBundle,
|
|
14
|
+
unansweredErrors,
|
|
13
15
|
type StatsBundle,
|
|
14
16
|
} from "./client";
|
|
15
17
|
import { historyStartNotice } from "../shared/history-span";
|
|
@@ -84,6 +86,11 @@ function detailEntries(view: StatViewModel, cutoffShown: boolean, text: boolean)
|
|
|
84
86
|
];
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
/** A statistic answered from the copy saved by an earlier read, its refresh having failed, says so in its row. */
|
|
90
|
+
function savedCopyRow<T extends object>(view: StatViewModel, row: T): T {
|
|
91
|
+
return view.cacheSource === "stale-fallback" && view.fetchedAt != null ? servedFromSavedCopy(row, view.fetchedAt) : row;
|
|
92
|
+
}
|
|
93
|
+
|
|
87
94
|
/** The window the %ile column counts over, in words. */
|
|
88
95
|
const PERCENTILE_WINDOW: Record<StatRangeId, string> = {
|
|
89
96
|
"5Y": "the last 5 years",
|
|
@@ -163,7 +170,7 @@ export function projectStatsHeadlessBundle(
|
|
|
163
170
|
textColumns: columns(true),
|
|
164
171
|
// `as_of` is the release; a figure with none stated is as of its own day when it is observed daily or weekly, never its period's first day.
|
|
165
172
|
rowAsOfKey: "exportAsOf",
|
|
166
|
-
rows: categoryViews.map((view) => ({
|
|
173
|
+
rows: categoryViews.map((view) => savedCopyRow(view, {
|
|
167
174
|
id: view.stat.id,
|
|
168
175
|
indicator: view.stat.shortLabel,
|
|
169
176
|
latest: view.latest.value,
|
|
@@ -208,7 +215,8 @@ export function projectStatsHeadlessBundle(
|
|
|
208
215
|
...exportDating(selected),
|
|
209
216
|
}] : []),
|
|
210
217
|
],
|
|
211
|
-
|
|
218
|
+
// A statistic answered from its saved copy says so in the source line, not as an error.
|
|
219
|
+
errors: unansweredErrors(bundle),
|
|
212
220
|
notes: [
|
|
213
221
|
`%ile is the share of past readings at or below the latest one, over ${PERCENTILE_WINDOW[range]}.`
|
|
214
222
|
+ (range === "ALL" ? "" : " Full history: --range ALL."),
|
|
@@ -12,8 +12,9 @@ import type { PaneProps } from "../../../types/plugin";
|
|
|
12
12
|
import { formatNumber } from "../../../utils/format";
|
|
13
13
|
import { useAutoRefresh } from "../../../react/auto-refresh";
|
|
14
14
|
import { usePaneStatusFooter } from "../../../components/layout/pane/status-footer";
|
|
15
|
+
import { savedCopyFooterSegment } from "../../../components/layout/pane/footer";
|
|
15
16
|
import { SeriesListDetail, useSeriesList } from "../shared/series-list-detail";
|
|
16
|
-
import { getCachedStatsBundle, loadStatsBundle } from "./client";
|
|
17
|
+
import { getCachedStatsBundle, loadStatsBundle, unansweredErrors } from "./client";
|
|
17
18
|
import { categoryLabel, changeColor, type StatCategoryId } from "./defs";
|
|
18
19
|
import { StatDetail } from "./detail";
|
|
19
20
|
import { DEFAULT_STAT_ID } from "./stats";
|
|
@@ -145,7 +146,7 @@ export function EconStatisticsPane({ focused, width, height }: PaneProps) {
|
|
|
145
146
|
const { selected, searchFocused } = list;
|
|
146
147
|
const rows = useMemo(() => withCategoryHeaders(list.visible), [list.visible]);
|
|
147
148
|
|
|
148
|
-
const error = resource.error ?? bundle
|
|
149
|
+
const error = resource.error ?? (bundle ? unansweredErrors(bundle)[0] : null) ?? null;
|
|
149
150
|
const footerInfo = useMemo<PaneFooterSegment[]>(() => {
|
|
150
151
|
if (!selected) return [];
|
|
151
152
|
// A Treasury yield names when in the day it was taken, as GC does, so it is not read against ^TNX's earlier close.
|
|
@@ -153,11 +154,14 @@ export function EconStatisticsPane({ focused, width, height }: PaneProps) {
|
|
|
153
154
|
const info: PaneFooterSegment[] = [
|
|
154
155
|
{ id: "as-of", parts: [{ text: `reading for ${cutoff ? selected.reading : selected.readingReleased}${cutoff}`, tone: "muted" }] },
|
|
155
156
|
];
|
|
156
|
-
// The cached first paint is being replaced; its age only counts once that load fails
|
|
157
|
+
// The cached first paint is being replaced; its age only counts once that load fails, and then it is the
|
|
158
|
+
// copy an earlier read saved, which says when that was.
|
|
157
159
|
const seeding = resource.loading && resource.updatedAt === null && !selected.refreshError;
|
|
158
|
-
|
|
160
|
+
const savedAt = selected.cacheSource === "stale-fallback" ? selected.fetchedAt : null;
|
|
161
|
+
if (selected.observationStale || (selected.cacheStale && !seeding && savedAt == null)) {
|
|
159
162
|
info.push({ id: "stale", parts: [{ text: "STALE", tone: "warning", bold: true }] });
|
|
160
163
|
}
|
|
164
|
+
if (savedAt != null) info.push(savedCopyFooterSegment("saved-copy", savedAt));
|
|
161
165
|
return info;
|
|
162
166
|
}, [resource.loading, resource.updatedAt, selected]);
|
|
163
167
|
|
|
@@ -144,6 +144,9 @@ export const STATS: readonly StatDef[] = [
|
|
|
144
144
|
reference: { value: 0, label: "flat" },
|
|
145
145
|
staleAfterMs: MONTHLY_EARLY,
|
|
146
146
|
note: "Jobs added or lost last month, the single most watched release.",
|
|
147
|
+
// The jobs report as it is called: `ECST nfp` opens it.
|
|
148
|
+
aliases: ["nfp", "nonfarm", "nonfarm payrolls", "jobs report"],
|
|
149
|
+
keywords: ["jobs report", "nfp", "nonfarm"],
|
|
147
150
|
limit: 900,
|
|
148
151
|
}),
|
|
149
152
|
stat({
|