@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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { logFailure } from "../../utils/log-failure";
|
|
2
2
|
import { apiClient } from "../../api-client";
|
|
3
|
-
import { ApiRequestError } from "../../api-client/errors";
|
|
3
|
+
import { ApiRequestError, isPermanentClientError } from "../../api-client/errors";
|
|
4
4
|
import type { CliCommandDef } from "../../types/plugin";
|
|
5
|
-
import { withCliServices, withMarketData } from "../context";
|
|
5
|
+
import { withCliServices, withConfigData, withMarketData } from "../context";
|
|
6
6
|
import { CLI_ERROR_CODE, failWithFields } from "../errors";
|
|
7
7
|
import type { CliResultColumn } from "../result";
|
|
8
8
|
import { formatCompact } from "../../utils/format";
|
|
@@ -25,17 +25,27 @@ import {
|
|
|
25
25
|
import { MOVER_CANDIDATES } from "../../plugins/builtin/market-movers/client";
|
|
26
26
|
import { hasPartialFloat, meetsMinFloat, minFloatShares, normalizeMinFloat, PARTIAL_FLOAT_NOTE } from "../../plugins/builtin/market-movers/min-float";
|
|
27
27
|
import { fetchEconCalendarFrom } from "../../plugins/builtin/econ/calendar-source";
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
attachFredSeriesPersistence, computedFredSeriesNote, loadCachedFredSeries, resetFredSeriesPersistence, seriesNotOnFred,
|
|
30
|
+
} from "../../sources/gloomberb-cloud/fred-series";
|
|
31
|
+
import type { CloudFredSeriesPayload } from "../../api-client";
|
|
32
|
+
import { savedCopyMark } from "../../data/saved-copies";
|
|
33
|
+
import { ratesMacroPluginMeta } from "../../plugins/builtin/builtin-plugin-meta";
|
|
34
|
+
import { createCommandPluginPersistence } from "../saved-copy-persistence";
|
|
29
35
|
import { EXTENDED_SESSION_LABELS, getQuoteSessionFields, type ExtendedSession } from "../../market-data/market/status";
|
|
30
36
|
import {
|
|
31
37
|
COUNTRY_CYCLE,
|
|
32
38
|
COUNTRY_FILTER_HINT,
|
|
39
|
+
awaitsConsensus,
|
|
33
40
|
countriesWithoutEvents,
|
|
41
|
+
econEventName,
|
|
34
42
|
FILTER_CYCLE,
|
|
35
43
|
loadCalendar,
|
|
36
44
|
eventCountry,
|
|
37
45
|
matchesCountry,
|
|
46
|
+
matchesEventSearch,
|
|
38
47
|
matchesImpact,
|
|
48
|
+
NO_CONSENSUS_NOTE,
|
|
39
49
|
noScheduledReleasesMessage,
|
|
40
50
|
parseCountryFilter,
|
|
41
51
|
shortCalendarEnd,
|
|
@@ -303,7 +313,7 @@ function utcDateTimePart(value: unknown, part: "date" | "time", readerClock = tr
|
|
|
303
313
|
return part === "date" ? iso.slice(0, 10) : `${iso.slice(11, 16)} UTC${readerClock ? localTimeSuffix(date.getTime()) : ""}`;
|
|
304
314
|
}
|
|
305
315
|
|
|
306
|
-
const ECON_USAGE = "econ [--country <regions or countries>] [--impact <level>] [--from <date>] [--tz <zone>]";
|
|
316
|
+
const ECON_USAGE = "econ [--country <regions or countries>] [--impact <level>] [--search <words>] [--from <date>] [--tz <zone>]";
|
|
307
317
|
const WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const;
|
|
308
318
|
/** "Sat" for 2026-10-31. */
|
|
309
319
|
const weekdayOf = (date: string) => WEEKDAYS[new Date(`${date}T00:00:00Z`).getUTCDay()]!;
|
|
@@ -470,13 +480,15 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
|
|
|
470
480
|
const rawArgs = [...args];
|
|
471
481
|
const country = parseCountry(takeOption(rawArgs, "--country"), ctx);
|
|
472
482
|
const impact: ImpactFilter = parseFilter(takeOption(rawArgs, "--impact"), FILTER_CYCLE, "impact", ctx);
|
|
483
|
+
const search = takeOption(rawArgs, "--search")?.trim() ?? null;
|
|
484
|
+
if (search === "") ctx.fail("--search needs the words of an event's name.", "Such as econ --search nonfarm.");
|
|
473
485
|
const fromOption = parseDateOption(takeOption(rawArgs, "--from"), "--from", ctx);
|
|
474
486
|
const tzOption = takeOption(rawArgs, "--tz");
|
|
475
487
|
const viewerZone = tzOption == null ? null : canonicalTimeZone(tzOption);
|
|
476
488
|
if (tzOption != null && !viewerZone) {
|
|
477
489
|
ctx.fail(`Unknown time zone "${tzOption}".`, "Use an IANA zone name, such as America/New_York, Europe/London or Asia/Tokyo.");
|
|
478
490
|
}
|
|
479
|
-
rejectArgs(rawArgs, ECON_USAGE, ctx, "Filter with --country and --
|
|
491
|
+
rejectArgs(rawArgs, ECON_USAGE, ctx, "Filter with --country, --impact and --search.");
|
|
480
492
|
await withCliServices(ctx, async () => {
|
|
481
493
|
const now = Date.now();
|
|
482
494
|
const todayStart = Math.floor(now / DAY_MS) * DAY_MS;
|
|
@@ -487,7 +499,9 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
|
|
|
487
499
|
: null;
|
|
488
500
|
const past = stored?.events ? stored : null;
|
|
489
501
|
const unread = stored && !stored.events && stored.failure ? `The stored calendar could not be read (${stored.failure}); listing the current calendar` : null;
|
|
490
|
-
const { data: events, fetchedAt, stale } = past
|
|
502
|
+
const { data: events, fetchedAt, stale, source } = past
|
|
503
|
+
? { data: past.events, fetchedAt: Date.now(), stale: false, source: "network" as const }
|
|
504
|
+
: await loadCalendar(ctx.cliOptions.refresh);
|
|
491
505
|
// The calendar lists from today (UTC). --from moves the first day; --tail
|
|
492
506
|
// alone keeps the newest events of the whole calendar, the past week's too.
|
|
493
507
|
const from = fromOption ?? (ctx.cliOptions.tail != null ? null : new Date(todayStart).toISOString().slice(0, 10));
|
|
@@ -506,9 +520,10 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
|
|
|
506
520
|
// Countries asked for that nothing in the window is for: a quiet week there, not an unknown country.
|
|
507
521
|
const inWindow = events.filter((event) => event.date.getTime() >= fromTime);
|
|
508
522
|
const unscheduled = countriesWithoutEvents(country, inWindow);
|
|
509
|
-
const
|
|
510
|
-
.filter((event) => matchesCountry(event, country) && matchesImpact(event, impact))
|
|
511
|
-
.sort((left, right) => left.date.getTime() - right.date.getTime())
|
|
523
|
+
const listed = inWindow
|
|
524
|
+
.filter((event) => matchesCountry(event, country) && matchesImpact(event, impact) && (search == null || matchesEventSearch(event, search)))
|
|
525
|
+
.sort((left, right) => left.date.getTime() - right.date.getTime());
|
|
526
|
+
const rows = listed
|
|
512
527
|
.map((event) => {
|
|
513
528
|
// The viewer's clock (--tz) for a row with a time: its date and time there.
|
|
514
529
|
const clock = viewerZone && !hasNoReleaseTime(event) ? zonedClock(event.date.getTime(), viewerZone) : null;
|
|
@@ -522,7 +537,8 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
|
|
|
522
537
|
...(viewerZone ? { viewerDate: clock?.date ?? null, viewerTime: clock?.time ?? null } : {}),
|
|
523
538
|
country: eventCountry(event),
|
|
524
539
|
impact: event.impact,
|
|
525
|
-
|
|
540
|
+
// As the calendar names it: the Fed's decision is the upper bound of its range.
|
|
541
|
+
event: econEventName(event),
|
|
526
542
|
actual: figures.actual,
|
|
527
543
|
forecast: figures.forecast,
|
|
528
544
|
prior: figures.prior,
|
|
@@ -553,15 +569,20 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
|
|
|
553
569
|
// A filter of countries only, none of which has anything in the window, says so as its empty list.
|
|
554
570
|
const allUnscheduled = unscheduled.length > 0 && unscheduled.length === country.split(",").length;
|
|
555
571
|
const notes = [listedBefore, listedAfter, ...(rows.length > 0 ? blankTimeNotes(rows) : []),
|
|
556
|
-
rows.length > 0 && unscheduled.length > 0 ? noScheduledReleasesMessage(unscheduled) : null
|
|
572
|
+
rows.length > 0 && unscheduled.length > 0 ? noScheduledReleasesMessage(unscheduled) : null,
|
|
573
|
+
// A dash under Forecast on a release still to come is a consensus not in yet, not a missing figure.
|
|
574
|
+
listed.some(awaitsConsensus) ? NO_CONSENSUS_NOTE : null]
|
|
557
575
|
.filter((note): note is string => !!note);
|
|
558
576
|
const footnote = notes.join("\n");
|
|
577
|
+
// The calendar as read, not its rows: those are scheduled, mostly ahead. One that could not be
|
|
578
|
+
// refreshed is the copy an earlier read saved, and says when that was.
|
|
579
|
+
const read = rowsFreshness([], { status: "not-a-feed", basis: "calendar", asOf: fetchedAt }, { stale });
|
|
580
|
+
const calendarFreshness = read && source === "stale-fallback" ? { ...read, savedAt: new Date(fetchedAt).toISOString() } : read;
|
|
559
581
|
ctx.printResult({
|
|
560
582
|
data: rows,
|
|
561
583
|
...(unread ? { warnings: [unread] } : {}),
|
|
562
|
-
metadata: { country, impact, from, listedThrough, storedFrom: past?.storedFrom ?? null, ...(viewerZone ? { timeZone: viewerZone } : {}) },
|
|
563
|
-
|
|
564
|
-
freshness: rowsFreshness([], { status: "not-a-feed", basis: "calendar", asOf: fetchedAt }, { stale }),
|
|
584
|
+
metadata: { country, impact, ...(search != null ? { search } : {}), from, listedThrough, storedFrom: past?.storedFrom ?? null, ...(viewerZone ? { timeZone: viewerZone } : {}) },
|
|
585
|
+
freshness: calendarFreshness,
|
|
565
586
|
}, {
|
|
566
587
|
dateKey: "date",
|
|
567
588
|
defaultLimit: 50,
|
|
@@ -607,6 +628,36 @@ function isUnsupportedSeries(error: unknown): boolean {
|
|
|
607
628
|
return error.details?.code === "unsupported_series" || /^Unsupported FRED series\b/i.test(message);
|
|
608
629
|
}
|
|
609
630
|
|
|
631
|
+
/**
|
|
632
|
+
* Runs `run` with the FRED series cache saving what it fetches in the profile,
|
|
633
|
+
* where the app's rates and macro panes keep it: a refresh that fails then
|
|
634
|
+
* answers from the copy an earlier read saved, and the report says so.
|
|
635
|
+
*/
|
|
636
|
+
function withSavedFredSeries<T>(ctx: Parameters<CliCommandDef["execute"]>[1], run: () => Promise<T>): Promise<T> {
|
|
637
|
+
return withConfigData(ctx, async ({ persistence }) => {
|
|
638
|
+
attachFredSeriesPersistence(createCommandPluginPersistence(persistence.resources, `plugin:${ratesMacroPluginMeta.stateId}`));
|
|
639
|
+
try {
|
|
640
|
+
return await run();
|
|
641
|
+
} finally {
|
|
642
|
+
resetFredSeriesPersistence();
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* A FRED series from Gloom Cloud, or the copy an earlier read saved when it
|
|
649
|
+
* cannot be refreshed (marked as such). A refusal asking again will not change
|
|
650
|
+
* (a series Gloom Cloud no longer serves, access denied) is said, not covered.
|
|
651
|
+
*/
|
|
652
|
+
async function loadFredSeries(
|
|
653
|
+
seriesId: string,
|
|
654
|
+
params: { startDate?: string; limit?: number; sortOrder: "asc" | "desc" },
|
|
655
|
+
): Promise<CloudFredSeriesPayload> {
|
|
656
|
+
const result = await loadCachedFredSeries({ seriesId, ...params }, () => apiClient.getCloudFredSeries(seriesId, params));
|
|
657
|
+
if (result.source === "stale-fallback" && isPermanentClientError(result.error)) throw result.error;
|
|
658
|
+
return result.data;
|
|
659
|
+
}
|
|
660
|
+
|
|
610
661
|
async function runFredList(filter: string, ctx: Parameters<CliCommandDef["execute"]>[1]) {
|
|
611
662
|
let catalog;
|
|
612
663
|
try {
|
|
@@ -652,9 +703,19 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
|
|
|
652
703
|
ctx.fail(change ? `Unknown change "${change}".` : "--change needs 1D or 1W.", "Use 1D (from the previous observation) or 1W (from a week before).");
|
|
653
704
|
}
|
|
654
705
|
const seriesId = requireOneArg(args, FRED_USAGE, "series", ctx).toUpperCase();
|
|
706
|
+
await withSavedFredSeries(ctx, () => printFredSeries(ctx, { seriesId, start, startDate, sortOrder, horizon }));
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
async function printFredSeries(ctx: Parameters<CliCommandDef["execute"]>[1], { seriesId, start, startDate, sortOrder, horizon }: {
|
|
710
|
+
seriesId: string;
|
|
711
|
+
start: string | undefined;
|
|
712
|
+
startDate: string;
|
|
713
|
+
sortOrder: "asc" | "desc";
|
|
714
|
+
horizon: ReturnType<typeof parseFredChange>;
|
|
715
|
+
}) {
|
|
655
716
|
// A change needs the observation it is measured from: the fetch reaches back before the first row shown.
|
|
656
717
|
const fetchStart = horizon ? fredChangeFetchStart(startDate) : startDate;
|
|
657
|
-
const data = await
|
|
718
|
+
const data = await loadFredSeries(seriesId, { startDate: fetchStart, sortOrder }).catch((error: unknown) => {
|
|
658
719
|
if (isUnsupportedSeries(error)) ctx.fail(`Unsupported FRED series ${seriesId}. List supported series with: gloom fred --list`);
|
|
659
720
|
throw error;
|
|
660
721
|
});
|
|
@@ -668,7 +729,9 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
|
|
|
668
729
|
const releasedOf = (observation: { publisher?: string; publishedAt?: string; releasedOn?: string }) =>
|
|
669
730
|
observation.releasedOn ?? (observation.publisher?.trim() ? observation.publishedAt : undefined);
|
|
670
731
|
// A value FRED does not list yet carries its publisher and when it published, in every export.
|
|
732
|
+
// A row from the copy an earlier read saved, the refresh having failed, carries its mark: the source line says so.
|
|
671
733
|
const exportRow = (observation: { date: string; value: number | null; publisher?: string; publishedAt?: string; releasedOn?: string }) => ({
|
|
734
|
+
...savedCopyMark(data),
|
|
672
735
|
date: observation.date,
|
|
673
736
|
value: fredExportValue(seriesId, observation.value),
|
|
674
737
|
...(changes ? { change: changes.get(observation.date)?.change ?? null, changeFrom: changes.get(observation.date)?.changeFrom ?? null } : {}),
|
|
@@ -687,7 +750,7 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
|
|
|
687
750
|
computedFredSeriesNote(seriesId),
|
|
688
751
|
].filter((note): note is string => note != null);
|
|
689
752
|
// Nothing on or after the start: name where the series ends, so an empty answer is not a dead end.
|
|
690
|
-
const latest = rows.length > 0 ? null : await
|
|
753
|
+
const latest = rows.length > 0 ? null : await loadFredSeries(seriesId, { limit: 1, sortOrder: "desc" })
|
|
691
754
|
.then((payload) => payload.observations[0] ?? null).catch(logFailure("cli.commands-overview", "Read latest economic series observation failed", null));
|
|
692
755
|
// CSV and NDJSON are rows alone: an empty one would read as a series with no data, so it fails and says where the series ends.
|
|
693
756
|
if (rows.length === 0 && (ctx.cliOptions.format === "csv" || ctx.cliOptions.format === "ndjson")) {
|
|
@@ -759,10 +822,12 @@ async function runYieldCurve(rawArgs: string[], ctx: Parameters<CliCommandDef["e
|
|
|
759
822
|
const startDate = parseStartDate(takeOption(args, "--start"), ctx);
|
|
760
823
|
rejectArgs(args, "yield-curve [--start <date>]", ctx);
|
|
761
824
|
const series = Object.keys(YIELD_TENORS);
|
|
762
|
-
const results = await Promise.all(series.map(async (seriesId) => {
|
|
763
|
-
const data = await
|
|
825
|
+
const results = await withSavedFredSeries(ctx, () => Promise.all(series.map(async (seriesId) => {
|
|
826
|
+
const data = await loadFredSeries(seriesId, { startDate, sortOrder: "desc" });
|
|
764
827
|
const [latest, previous] = data.observations;
|
|
765
828
|
return {
|
|
829
|
+
// A tenor from the copy an earlier read saved, the refresh having failed, says so in the source line.
|
|
830
|
+
...savedCopyMark(data),
|
|
766
831
|
seriesId,
|
|
767
832
|
date: latest?.date ?? "",
|
|
768
833
|
// Yields in percent, which the exports' `currency_or_unit` column says (`pct`), as `fred` does.
|
|
@@ -775,7 +840,7 @@ async function runYieldCurve(rawArgs: string[], ctx: Parameters<CliCommandDef["e
|
|
|
775
840
|
latest,
|
|
776
841
|
publishedAt: latest?.publisher ? null : fredPublicationTime(data.info, latest?.date),
|
|
777
842
|
};
|
|
778
|
-
}));
|
|
843
|
+
})));
|
|
779
844
|
// Tenors whose latest yield FRED does not list yet name the Treasury and when it published them.
|
|
780
845
|
const ahead = fredProvenance(results.flatMap((row) => row.latest ? [row.latest] : []));
|
|
781
846
|
// The curve shown was published with its last tenor: the newest of their FRED updates, when each has one.
|
|
@@ -907,10 +972,11 @@ export const overviewCliCommands: CliCommandDef[] = [
|
|
|
907
972
|
options: [
|
|
908
973
|
{ flags: "--country <regions or countries>", description: "US, G7, EU, all (default), or countries by their two-letter codes, comma separated, such as MX or MX,BR,ZA" },
|
|
909
974
|
{ flags: "--impact <level>", description: "high, medium, low, or all (default all)" },
|
|
975
|
+
{ flags: "--search <words>", description: "Events whose name has these words, case, spaces and hyphens aside, such as nonfarm or fed funds" },
|
|
910
976
|
{ flags: "--from <date>", description: `First day listed, as ${DATE_OPTION_EXAMPLES} (default today, UTC); an earlier day lists past events from the stored calendar and says where it starts` },
|
|
911
977
|
{ flags: "--tz <zone>", description: "Add a column with each release's time on that clock, an IANA zone such as America/New_York; with the weekday when it falls on another day than the UTC date" },
|
|
912
978
|
],
|
|
913
|
-
examples: ["econ", "econ --country US --impact high", "econ --country MX,BR,ZA,IN,KR", "econ --from 2026-10-12 --country US",
|
|
979
|
+
examples: ["econ", "econ --country US --impact high", "econ --search nonfarm", "econ --country MX,BR,ZA,IN,KR", "econ --from 2026-10-12 --country US",
|
|
914
980
|
"econ --country BR --tz America/New_York", "econ --tail 20"],
|
|
915
981
|
},
|
|
916
982
|
execute: runEcon,
|
|
@@ -83,7 +83,7 @@ import { movedAwayChange } from "../../tickers/ticker-changes";
|
|
|
83
83
|
import { companyMemberships, loadCompanyMemberships, type CompanyMemberships, type ListMembership } from "../issuer-membership";
|
|
84
84
|
import { isNotATickerMessage } from "../not-a-ticker";
|
|
85
85
|
import { sharesOutstandingInReceipts } from "../../utils/depositary-receipt";
|
|
86
|
-
import { depositaryPairText, isReceiptSide, listingDepositaryPair } from "../../market-data/depositary-pair";
|
|
86
|
+
import { cedearRatioText, depositaryPairText, isReceiptSide, listingCedearRatio, listingDepositaryPair } from "../../market-data/depositary-pair";
|
|
87
87
|
import { cliFreshnessFooter } from "../result";
|
|
88
88
|
import { providerMissReason } from "../../sources/provider-errors";
|
|
89
89
|
import { exportEntriesTable, reportFooterLines, type CliReportTables } from "../report-tables";
|
|
@@ -100,6 +100,7 @@ import { bondFigures, NOT_SEC_YIELD } from "../../market-data/bond-funds";
|
|
|
100
100
|
import { readFundFacts } from "../../market-data/fund-facts";
|
|
101
101
|
import { taxEquivalentLines, type TaxEquivalentAnalysis } from "../../market-data/tax-equivalent";
|
|
102
102
|
import { isRoundTheClockCoin } from "../../utils/crypto-pair";
|
|
103
|
+
import { ipoDetailNotes, ipoMetricRows, loadTickerIpo, type TickerIpo } from "../../plugins/builtin/ipo-calendar/listing-ipo";
|
|
103
104
|
import { coinNewsFallbackNotice } from "../../sources/gloomberb-cloud/news";
|
|
104
105
|
|
|
105
106
|
const NEWS_ITEM_LIMIT = 5;
|
|
@@ -126,6 +127,8 @@ const METADATA_SEPARATOR = " · ";
|
|
|
126
127
|
interface TickerCommandDependencies {
|
|
127
128
|
/** `--exchange`: the listing, for a symbol that trades in several places. */
|
|
128
129
|
exchange?: string;
|
|
130
|
+
/** A stock's IPO of the last year with its lock-up news; a failure leaves the IPO section out. */
|
|
131
|
+
listingIpo?: (listing: { symbol: string; exchange?: string | null }) => Promise<TickerIpo | null>;
|
|
129
132
|
initMarketData?: () => Promise<MarketContext>;
|
|
130
133
|
fail?: (message: string, details?: string) => never;
|
|
131
134
|
printResult?: CliCommandContext["printResult"];
|
|
@@ -1081,8 +1084,11 @@ export async function buildTickerReport({
|
|
|
1081
1084
|
fundFacts = null,
|
|
1082
1085
|
memberships,
|
|
1083
1086
|
offerings = [],
|
|
1087
|
+
ipo = null,
|
|
1084
1088
|
}: {
|
|
1085
1089
|
symbol: string;
|
|
1090
|
+
/** A stock's IPO of the last year, with its lock-up estimate and news (loadTickerIpo). */
|
|
1091
|
+
ipo?: TickerIpo | null;
|
|
1086
1092
|
/** The lists holding the company, by any of its listings (loadCompanyMemberships); the saved listing's own lists when absent. */
|
|
1087
1093
|
memberships?: CompanyMemberships;
|
|
1088
1094
|
/** The filings behind the latest statements, and a newer quarter's results release. */
|
|
@@ -1165,6 +1171,9 @@ export async function buildTickerReport({
|
|
|
1165
1171
|
if (depositaryPair) {
|
|
1166
1172
|
lines.push(cliStyles.muted(`ADR ratio ${depositaryPairText(depositaryPair, isReceiptSide(financials))}. Implied FX and premium: gloom fn ADR ${quote?.symbol ?? symbol}`));
|
|
1167
1173
|
}
|
|
1174
|
+
// A CEDEAR states its ratio in its company's shares, as a receipt does.
|
|
1175
|
+
const cedearRatio = listingCedearRatio(financials);
|
|
1176
|
+
if (cedearRatio) lines.push(cliStyles.muted(`CEDEAR ratio ${cedearRatioText(cedearRatio)}.`));
|
|
1168
1177
|
|
|
1169
1178
|
// A list that holds the company as another of its listings says which: "Watchlists R (as RCI.B:TSX)".
|
|
1170
1179
|
const lists = memberships ?? companyMemberships(config, tickerFile, [], null);
|
|
@@ -1250,7 +1259,15 @@ export async function buildTickerReport({
|
|
|
1250
1259
|
]);
|
|
1251
1260
|
}
|
|
1252
1261
|
|
|
1253
|
-
|
|
1262
|
+
// A stock that listed in the last year: its IPO, the move since, and when its lock-up is estimated to end.
|
|
1263
|
+
if (ipo) {
|
|
1264
|
+
appendMetricSection(lines, "IPO", ipoMetricRows(ipo.deal, quote ? getRegularSessionDisplay(quote)?.price : null));
|
|
1265
|
+
for (const note of ipoDetailNotes(ipo.deal, ipo.news)) lines.push(cliStyles.muted(note));
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// A listing of the last year may have no statements yet: its IPO says how recent it is.
|
|
1269
|
+
const listing = { listedSince: ipo ? ipo.deal.listingDate ?? ipo.deal.firstDay?.session ?? null : null };
|
|
1270
|
+
const metrics = fundamentalsMetrics(quote, priced.fundamentals, profile, { text: marketCapText }, priceReturns, fundamentalsCurrency(financials, listing),
|
|
1254
1271
|
{ text: enterpriseValueText }, distributions, bondFund, codes);
|
|
1255
1272
|
const reportedCurrency = financials.financialCurrency?.trim();
|
|
1256
1273
|
const statements = [...financials.annualStatements, ...financials.quarterlyStatements];
|
|
@@ -1349,9 +1366,11 @@ function buildTickerStructuredData({
|
|
|
1349
1366
|
bondFund = null,
|
|
1350
1367
|
memberships,
|
|
1351
1368
|
offerings = [],
|
|
1369
|
+
ipo = null,
|
|
1352
1370
|
}: {
|
|
1353
1371
|
symbol: string;
|
|
1354
1372
|
listing: CliListing;
|
|
1373
|
+
ipo?: TickerIpo | null;
|
|
1355
1374
|
memberships: CompanyMemberships;
|
|
1356
1375
|
statementFilings: StatementFilingsReport | null;
|
|
1357
1376
|
offerings?: readonly OfferingFiling[];
|
|
@@ -1434,6 +1453,8 @@ function buildTickerStructuredData({
|
|
|
1434
1453
|
// and the offering prospectuses filed since that the cash does not include.
|
|
1435
1454
|
cashRunway: runway,
|
|
1436
1455
|
offeringsAfterBalanceSheet: runway ? offeringsAfter(offerings, runway.periodEnd) : [],
|
|
1456
|
+
// A stock that listed in the last year: its IPO deal (offer price, first day, lock-up estimate) and lock-up news.
|
|
1457
|
+
...ipo ? { ipo: { ...ipo.deal, lockupNews: ipo.news } } : {},
|
|
1437
1458
|
// The form and filed date of the latest periods' filings, and a newer quarter's results released before its statements.
|
|
1438
1459
|
statementFilings,
|
|
1439
1460
|
notes,
|
|
@@ -1462,6 +1483,8 @@ function buildTickerStructuredData({
|
|
|
1462
1483
|
}
|
|
1463
1484
|
|
|
1464
1485
|
function depositaryPairJson(financials: TickerFinancials) {
|
|
1486
|
+
const cedear = listingCedearRatio(financials);
|
|
1487
|
+
if (cedear) return { cedearRatio: { ...cedear, text: cedearRatioText(cedear) } };
|
|
1465
1488
|
const pair = listingDepositaryPair(financials);
|
|
1466
1489
|
if (!pair) return {};
|
|
1467
1490
|
const receipt = isReceiptSide(financials);
|
|
@@ -1573,6 +1596,11 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
|
|
|
1573
1596
|
});
|
|
1574
1597
|
// An offering since the latest balance sheet, read only when the report has a runway to flag.
|
|
1575
1598
|
const offerings = loadOfferingFilings(statementSources, { symbol: requestSymbol, exchange }, cashRunway(resolvedFinancials) != null);
|
|
1599
|
+
// A stock's IPO of the last year, read beside the rest; it never fails the report.
|
|
1600
|
+
const ipoLoad = quote && resolveTickerInstrumentKind(tickerFile, resolvedFinancials) === "equity"
|
|
1601
|
+
? (dependencies.listingIpo ?? loadTickerIpo)({ symbol: requestSymbol, exchange: exchange || quote.listingExchangeName || quote.exchangeName })
|
|
1602
|
+
.catch(logFailure("cli.commands-ticker", "Read a listing's IPO failed", null))
|
|
1603
|
+
: Promise.resolve(null);
|
|
1576
1604
|
const [notesResult, newsResult, secFilingsResult] = await Promise.allSettled([
|
|
1577
1605
|
notesFiles.load(tickerFile?.metadata.ticker ?? normalized),
|
|
1578
1606
|
dataProvider.getNews({
|
|
@@ -1640,6 +1668,7 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
|
|
|
1640
1668
|
bondFund: await bondFund,
|
|
1641
1669
|
memberships,
|
|
1642
1670
|
offerings: await offerings,
|
|
1671
|
+
ipo: await ipoLoad,
|
|
1643
1672
|
}),
|
|
1644
1673
|
});
|
|
1645
1674
|
return;
|
|
@@ -1666,6 +1695,7 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
|
|
|
1666
1695
|
fundFacts: await fundFacts,
|
|
1667
1696
|
memberships,
|
|
1668
1697
|
offerings: await offerings,
|
|
1698
|
+
ipo: await ipoLoad,
|
|
1669
1699
|
}));
|
|
1670
1700
|
if (freshness) console.log(`\n${cliFreshnessFooter(freshness, quote ?? resolvedFinancials)}`);
|
|
1671
1701
|
});
|
package/src/cli/errors.ts
CHANGED
|
@@ -13,7 +13,7 @@ export const CLI_ERROR_CODE = "cli_error";
|
|
|
13
13
|
/** The code of a command that ran, and the data service has nothing for what it asked. */
|
|
14
14
|
export const NO_DATA_CODE = "no_data";
|
|
15
15
|
/** The code of a command whose requests never reached the data service. */
|
|
16
|
-
|
|
16
|
+
const SERVICE_UNREACHABLE_CODE = "service_unreachable";
|
|
17
17
|
|
|
18
18
|
/** What to check when nothing answered: the connection, and the proxy when one is set. */
|
|
19
19
|
function unreachableHint(): string {
|
|
@@ -74,6 +74,21 @@ export function failRetryable(message: string, code: string): never {
|
|
|
74
74
|
throw new CliFailure(message, undefined, code, true);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* What a command says when its requests never reached the data service:
|
|
79
|
+
* `message` names what it could not get (by default only that), and the
|
|
80
|
+
* details what to check. `--json` marks it retryable.
|
|
81
|
+
*/
|
|
82
|
+
export function unreachableError(message = DATA_SERVICE_UNREACHABLE_MESSAGE): CliErrorObject {
|
|
83
|
+
return { code: SERVICE_UNREACHABLE_CODE, message, details: unreachableHint(), retryable: true };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Fails as a command whose requests never reached the data service (`unreachableError`). */
|
|
87
|
+
export function failUnreachable(message?: string): never {
|
|
88
|
+
const error = unreachableError(message);
|
|
89
|
+
throw new CliFailure(error.message, error.details, error.code, error.retryable);
|
|
90
|
+
}
|
|
91
|
+
|
|
77
92
|
export function closeAndFail(persistence: AppPersistence, message: string, details?: string): never {
|
|
78
93
|
persistence.close();
|
|
79
94
|
fail(message, details);
|
|
@@ -104,7 +119,7 @@ function cliErrorObject(error: unknown): CliErrorObject {
|
|
|
104
119
|
}
|
|
105
120
|
// No answer came back at all: in Gloom's words, whichever fetch failed (Bun's "Unable to connect", a refused proxy).
|
|
106
121
|
if (isNetworkFailure(error)) {
|
|
107
|
-
return
|
|
122
|
+
return unreachableError();
|
|
108
123
|
}
|
|
109
124
|
// Gloom Cloud refused what was asked (an unknown series, a bad date): the input was wrong, not the program.
|
|
110
125
|
if (error instanceof ApiRequestError && error.status === 400) {
|
package/src/cli/glossary.ts
CHANGED
|
@@ -437,6 +437,12 @@ export const GLOSSARY: readonly GlossaryEntry[] = [
|
|
|
437
437
|
definition: "How many ordinary shares one depositary receipt holds: 1 ADR = 10 ordinary shares means one GGAL in New York is ten GGAL.BA shares in Buenos Aires. "
|
|
438
438
|
+ "A receipt's price compares with its home price only once the home price is multiplied by the ratio.",
|
|
439
439
|
},
|
|
440
|
+
{
|
|
441
|
+
term: "CEDEAR ratio", aliases: ["cedear", "cedears", "certificado de deposito argentino"],
|
|
442
|
+
functions: ["DES"], shown: ["CEDEAR ratio", "CEDEAR Ratio"],
|
|
443
|
+
definition: "How many Buenos Aires CEDEARs make the company's shares they stand for, from BYMA's published list: 20 AAPL:BYMA = 1 Apple Inc. share. "
|
|
444
|
+
+ "A CEDEAR's market cap is its company's, in the currency of the listing it stands for (Apple's in dollars), not the certificate's peso price times a share count.",
|
|
445
|
+
},
|
|
440
446
|
{
|
|
441
447
|
term: "ADR premium", aliases: ["adr discount", "adr vs home", "receipt premium", "adr premium/discount"],
|
|
442
448
|
functions: ["ADR"], shown: ["ADR vs home", "premium", "discount"],
|
package/src/cli/index-roots.ts
CHANGED
|
@@ -35,10 +35,3 @@ export function indexRootTryLine(root: IndexRoot, command = "quote"): string {
|
|
|
35
35
|
export function indexRootAlsoLine(root: IndexRoot, command = "quote"): string {
|
|
36
36
|
return `also ${root.symbol} ${root.name} (gloom ${command} ${root.symbol})`;
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
/** Cboe's Treasury yield indices: 13-week bill, 5-, 10- and 30-year notes. Each level is a yield in percent. */
|
|
40
|
-
const YIELD_INDEX_SYMBOLS: ReadonlySet<string> = new Set(["^IRX", "^FVX", "^TNX", "^TYX"]);
|
|
41
|
-
|
|
42
|
-
export function isYieldIndexSymbol(symbol: string): boolean {
|
|
43
|
-
return YIELD_INDEX_SYMBOLS.has(symbol.trim().toUpperCase());
|
|
44
|
-
}
|
package/src/cli/listing-arg.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
import { findUsListingOfCompany, type UsListing } from "../tickers/us-listing";
|
|
18
18
|
import { findListingOfCompanyName, isCompanyNameInput, readTerminalCodeArg, type CompanyNameListing } from "../tickers/company-name-listing";
|
|
19
19
|
import { movedAwayChange, startTickerChangeLookup, type MovedTickerChange } from "../tickers/ticker-changes";
|
|
20
|
-
import { loadListingFate, MovedSymbolError, type ListingFate } from "../tickers/listing-fate";
|
|
20
|
+
import { lineMayHaveMoved, loadListingFate, MovedSymbolError, type ListingFate } from "../tickers/listing-fate";
|
|
21
21
|
import { quotedListingName } from "../tickers/listing-name";
|
|
22
22
|
import { cliStyles } from "../utils/cli-output";
|
|
23
23
|
import { groupByCompany, namesDepositaryReceipt, sameCompany, sameListedName, shortCompanyName } from "../tickers/company-name";
|
|
@@ -36,7 +36,7 @@ import { withMarketData } from "./scoped-context";
|
|
|
36
36
|
import { failWithFields } from "./errors";
|
|
37
37
|
import { stripStrayEdges } from "./symbol-args";
|
|
38
38
|
import { isNoProviderMessage, isNoSourceAnswer, isProviderBusy } from "../sources/provider-errors";
|
|
39
|
-
import { dashedCoinPair, MAJOR_COIN_NAMES } from "../utils/crypto-pair";
|
|
39
|
+
import { dashedCoinPair, exchangeLineCoinListing, isSpotCommodityOrCurrencyPair, MAJOR_COIN_NAMES } from "../utils/crypto-pair";
|
|
40
40
|
import { boardCoinFor, primeBoardCoins } from "./board-coins";
|
|
41
41
|
import { knownGapMessage } from "./known-gaps";
|
|
42
42
|
import { currencyPairSpelling } from "../utils/currency-pair";
|
|
@@ -107,6 +107,12 @@ export interface ListingArg {
|
|
|
107
107
|
* `readAs` asks about it, and a command the listing fails asks too.
|
|
108
108
|
*/
|
|
109
109
|
meant?: { symbol: string; name: string };
|
|
110
|
+
/**
|
|
111
|
+
* A bare symbol a company's name was read as still says what it was read
|
|
112
|
+
* as (`listingChoiceNotes`): GETYQ for "getty images", whose NYSE line
|
|
113
|
+
* became GETYQ. Other bare symbols say where the data service chose instead.
|
|
114
|
+
*/
|
|
115
|
+
readFromName?: true;
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
/** "Did you mean JPM (JPMorgan Chase & Co.)?", for a name read as a fund or an index; empty for any other listing. */
|
|
@@ -176,8 +182,13 @@ function parseCleanListingArg(
|
|
|
176
182
|
// `SAN:` asks which listing; with --exchange it has its answer.
|
|
177
183
|
if (option && typed.endsWith(":")) typed = typed.slice(0, -1);
|
|
178
184
|
const venueKey = parsePublicTickerKey(typed);
|
|
179
|
-
//
|
|
180
|
-
const
|
|
185
|
+
// A crypto exchange's line of a pair (BTC/GBP:COINBASE PRO, as search offered it) is the crypto venue's BTC-GBP:CCC.
|
|
186
|
+
const line = venueKey.exchange || optionCode ? exchangeLineCoinListing(venueKey.symbol, venueKey.exchange ?? optionCode) : null;
|
|
187
|
+
if (line) return { symbol: line.symbol, exchange: line.exchange, key: `${line.symbol}:${line.exchange}` };
|
|
188
|
+
// BTC/USD, as crypto venues write a pair, is the coin's own BTC-USD; spot crude (WTI/USD) and gold by the gram
|
|
189
|
+
// (GAU/USD:COMMODITY) are not coins, though tokens share their letters.
|
|
190
|
+
const coinPair = isSpotCommodityOrCurrencyPair(venueKey.symbol, venueKey.exchange ?? optionCode) ? null : dashedCoinPair(venueKey.symbol);
|
|
191
|
+
const parsed = { ...venueKey, symbol: coinPair ?? venueKey.symbol };
|
|
181
192
|
const ownCode = parsed.exchange ? typedExchangeCode(typed) : "";
|
|
182
193
|
let exchange = parsed.exchange ?? option;
|
|
183
194
|
let code = ownCode || optionCode;
|
|
@@ -452,7 +463,7 @@ export function coinListingNotes(
|
|
|
452
463
|
* its warnings.
|
|
453
464
|
*/
|
|
454
465
|
export async function listingChoiceNotes(
|
|
455
|
-
listing: Pick<CliListing, "symbol" | "exchange" | "saved" | "readAs">,
|
|
466
|
+
listing: Pick<CliListing, "symbol" | "exchange" | "saved" | "readAs" | "readFromName">,
|
|
456
467
|
deps: ListingDeps,
|
|
457
468
|
quote: Pick<Quote, "name" | "listingExchangeName" | "exchangeName" | "instrumentType" | "stale" | "delisting"> | null | undefined,
|
|
458
469
|
/** The venues `loadBareVenues` already started; looked up here when not given. */
|
|
@@ -460,6 +471,8 @@ export async function listingChoiceNotes(
|
|
|
460
471
|
): Promise<string[]> {
|
|
461
472
|
const coin = coinListingNotes(listing, quote);
|
|
462
473
|
if (!isBareListing(listing)) return [...listingReadNotes(listing), ...coin];
|
|
474
|
+
// A bare symbol a name was read as says so: "Read "getty images" as GETYQ (...). GETY became GETYQ on 2026-10-22."
|
|
475
|
+
if (listing.readFromName) return [...listingReadNotes(listing), ...coin];
|
|
463
476
|
if (!quote || UNLISTED_TYPES.has(quote.instrumentType?.trim().toUpperCase() ?? "")) return coin;
|
|
464
477
|
const exchange = canonicalExchange(quote.listingExchangeName || quote.exchangeName);
|
|
465
478
|
if (!exchange) return coin;
|
|
@@ -475,7 +488,7 @@ export async function listingChoiceNotes(
|
|
|
475
488
|
* as another listing (BTC, the Grayscale trust) ends with the coin's pair.
|
|
476
489
|
*/
|
|
477
490
|
export async function quotedListingChoiceNotes(
|
|
478
|
-
listing: Pick<CliListing, "symbol" | "exchange" | "saved" | "request" | "readAs">,
|
|
491
|
+
listing: Pick<CliListing, "symbol" | "exchange" | "saved" | "request" | "readAs" | "readFromName">,
|
|
479
492
|
deps: ListingDeps,
|
|
480
493
|
): Promise<string[]> {
|
|
481
494
|
const venues = loadBareVenues(listing, deps);
|
|
@@ -1346,7 +1359,36 @@ function isMajorCoinName(word: string): boolean {
|
|
|
1346
1359
|
}
|
|
1347
1360
|
|
|
1348
1361
|
function findCompanyName(typed: string, deps: Pick<ListingDeps, "dataProvider">, options: { oneWord?: boolean } = {}): Promise<CompanyNameListing | null> {
|
|
1349
|
-
return findListingOfCompanyName(deps.dataProvider, typed, { meant: true, quoted: true, ...options });
|
|
1362
|
+
return findListingOfCompanyName(deps.dataProvider, typed, { meant: true, quoted: true, movedLine: (listing) => movedLineOf(listing, deps), ...options });
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/**
|
|
1366
|
+
* Where the company of a US line that stopped trading, or has no quote, trades
|
|
1367
|
+
* now when it moved off the symbol (`loadListingFate`): GETY became GETYQ on
|
|
1368
|
+
* 2026-10-22, so "getty images" is GETYQ rather than its delisted NYSE line or
|
|
1369
|
+
* the Munich line still quoting the old shares. A line that trades is not
|
|
1370
|
+
* looked up.
|
|
1371
|
+
*/
|
|
1372
|
+
async function movedLineOf(
|
|
1373
|
+
listing: { symbol: string; exchange: string; quote: Quote | null; name: string | null },
|
|
1374
|
+
deps: Pick<ListingDeps, "dataProvider">,
|
|
1375
|
+
): Promise<{ symbol: string; note: string } | null> {
|
|
1376
|
+
const { quote, name, ...line } = listing;
|
|
1377
|
+
if (quote && !lineMayHaveMoved(quote)) return null;
|
|
1378
|
+
const fate = await loadListingFate(line, deps.dataProvider, { quote });
|
|
1379
|
+
const moved = fate.moved ?? (quote ? null : movedWithoutQuote(fate.change, name));
|
|
1380
|
+
return moved ? { symbol: moved.tradesAs, note: moved.note } : null;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* The move of a line with no quote, when it is the move of the company search
|
|
1385
|
+
* names on the line. With no quote nothing tells the company's change from an
|
|
1386
|
+
* older one's: a reused symbol's change data is the company that held it
|
|
1387
|
+
* before, so it is followed only when that company is this one.
|
|
1388
|
+
*/
|
|
1389
|
+
function movedWithoutQuote(change: ListingFate["change"], name: string | null): MovedTickerChange | null {
|
|
1390
|
+
const moved = movedAwayChange(change);
|
|
1391
|
+
return moved && sameCompany(name, moved.issuer) ? moved : null;
|
|
1350
1392
|
}
|
|
1351
1393
|
|
|
1352
1394
|
async function readCompanyName(typed: string, deps: Pick<ListingDeps, "dataProvider">, options: { oneWord?: boolean } = {}): Promise<ListingArg | null> {
|
|
@@ -1355,11 +1397,16 @@ async function readCompanyName(typed: string, deps: Pick<ListingDeps, "dataProvi
|
|
|
1355
1397
|
}
|
|
1356
1398
|
|
|
1357
1399
|
function companyListingArg(typed: string, found: CompanyNameListing): ListingArg {
|
|
1358
|
-
const key = `${found.symbol}:${found.exchange}
|
|
1400
|
+
const key = found.exchange ? `${found.symbol}:${found.exchange}` : found.symbol;
|
|
1359
1401
|
// A fund or an index carrying the name asks about the company's stock: "Did you mean JPM (JPMorgan Chase & Co.)?".
|
|
1360
1402
|
const meant = found.meant ? { symbol: found.meant.symbol, name: found.meant.name } : null;
|
|
1361
1403
|
const question = meant ? ` ${meantQuestion({ meant })}` : "";
|
|
1362
|
-
|
|
1404
|
+
// A company that moved off the line search lists says so: "GETY became GETYQ on 2026-10-22."
|
|
1405
|
+
const moved = found.moved ? ` ${found.moved}.` : "";
|
|
1406
|
+
return {
|
|
1407
|
+
...parseListingArg(key), readAs: `Read "${typed.trim()}" as ${key} (${found.name}).${moved}${question}`,
|
|
1408
|
+
...meant ? { meant } : {}, ...found.exchange ? {} : { readFromName: true as const },
|
|
1409
|
+
};
|
|
1363
1410
|
}
|
|
1364
1411
|
|
|
1365
1412
|
/**
|
|
@@ -1377,7 +1424,7 @@ export async function companyNameSuggestion(typed: string, deps: Pick<ListingDep
|
|
|
1377
1424
|
const found = isCompanyNameInput(words) ? await findCompanyName(words, deps).catch(busyAsMiss) : await findOneWordCompany(words, deps);
|
|
1378
1425
|
const listing = found?.meant ?? found;
|
|
1379
1426
|
if (!listing || listing.symbol.toUpperCase() === words.toUpperCase()) return null;
|
|
1380
|
-
return { symbol: isUsListingExchange(listing.exchange) ? listing.symbol : `${listing.symbol}:${listing.exchange}`, name: listing.name };
|
|
1427
|
+
return { symbol: !listing.exchange || isUsListingExchange(listing.exchange) ? listing.symbol : `${listing.symbol}:${listing.exchange}`, name: listing.name };
|
|
1381
1428
|
} catch (error) {
|
|
1382
1429
|
logFailure("cli.listing-arg", "Search a company name for a suggestion failed")(error);
|
|
1383
1430
|
return null;
|
|
@@ -12,7 +12,9 @@ import type {
|
|
|
12
12
|
PluginPersistence,
|
|
13
13
|
} from "../../types/plugin";
|
|
14
14
|
import { createPluginTeamState } from "../../plugins/team-state";
|
|
15
|
+
import { bindPluginContextNamespaces } from "../../plugins/registry/plugin-state";
|
|
15
16
|
import type { MarketContext } from "../types";
|
|
17
|
+
import { createCommandPluginPersistence } from "../saved-copy-persistence";
|
|
16
18
|
import type { PaneFunctionCatalog } from "./catalog";
|
|
17
19
|
|
|
18
20
|
export interface PaneDiscoveryOptions {
|
|
@@ -173,23 +175,41 @@ export async function createPaneCatalog(context: MarketContext, plugins: GloomPl
|
|
|
173
175
|
brokers,
|
|
174
176
|
});
|
|
175
177
|
|
|
178
|
+
// Each plugin's caches save what a command fetches where the app keeps them, so a failed refresh answers from them.
|
|
179
|
+
const resources = context.persistence?.resources;
|
|
180
|
+
const persistences: Array<ReturnType<typeof createCommandPluginPersistence>> = [];
|
|
181
|
+
const commandPersistence = (stateId: string) => {
|
|
182
|
+
const persistence = createCommandPluginPersistence(resources, `plugin:${stateId}`);
|
|
183
|
+
persistences.push(persistence);
|
|
184
|
+
return persistence;
|
|
185
|
+
};
|
|
186
|
+
|
|
176
187
|
const destroy = () => {
|
|
177
188
|
let failure: unknown;
|
|
178
189
|
for (const plugin of setupPlugins.splice(0).reverse()) {
|
|
179
190
|
try { plugin.dispose?.(); } catch (error) { failure ??= error; }
|
|
180
191
|
}
|
|
192
|
+
// A plugin that keeps its persistence past dispose must not reach a store the command closes.
|
|
193
|
+
for (const persistence of persistences.splice(0)) persistence.release();
|
|
181
194
|
brokers.clear();
|
|
182
195
|
if (context.resolveBroker === resolveBroker) context.resolveBroker = previousResolver;
|
|
183
196
|
if (failure) throw failure;
|
|
184
197
|
};
|
|
185
198
|
|
|
199
|
+
const namespaced = (stateId: string) => ({
|
|
200
|
+
persistence: commandPersistence(stateId),
|
|
201
|
+
resume: discoveryContext.resume,
|
|
202
|
+
configState: discoveryContext.configState,
|
|
203
|
+
teamState: discoveryContext.teamState,
|
|
204
|
+
});
|
|
186
205
|
try {
|
|
187
206
|
for (const plugin of plugins) {
|
|
188
207
|
for (const pane of plugin.panes ?? []) panes.set(pane.id, pane);
|
|
189
208
|
for (const template of plugin.paneTemplates ?? []) paneTemplates.set(template.id, template);
|
|
190
209
|
if (plugin.setup) {
|
|
191
210
|
setupPlugins.push(plugin);
|
|
192
|
-
|
|
211
|
+
const stateId = plugin.stateId ?? plugin.id;
|
|
212
|
+
await plugin.setup(bindPluginContextNamespaces({ ...discoveryContext, ...namespaced(stateId) }, namespaced));
|
|
193
213
|
}
|
|
194
214
|
}
|
|
195
215
|
} catch (error) {
|