gloomberb 0.17.0 → 0.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/{plugins/builtin/cloud → account}/auth-dialog.tsx +7 -7
- package/src/api-client/data.ts +5 -1
- package/src/api-client/doe.ts +19 -0
- package/src/api-client/earnings.ts +4 -2
- package/src/api-client/index.ts +1 -0
- package/src/app/global-shortcuts.ts +10 -0
- package/src/app/keybindings/actions.ts +16 -0
- package/src/app/runtime/desktop-deeplink.ts +1 -1
- package/src/app/runtime/update.ts +16 -0
- package/src/app.tsx +1 -1
- package/src/architecture/core-builtin-allowlist.json +4 -9
- package/src/architecture/dom-cell-metrics-allowlist.json +100 -26
- package/src/cli/commands/market.ts +87 -17
- package/src/cli/commands/options-chain.ts +93 -23
- package/src/cli/commands/options-leaps.ts +462 -63
- package/src/cli/commands/search.ts +10 -7
- package/src/cli/commands/ticker.ts +19 -10
- package/src/cli/glossary.ts +1 -1
- package/src/cli/index.ts +14 -6
- package/src/cli/known-gaps.ts +22 -0
- package/src/cli/pane-functions/catalog.ts +45 -10
- package/src/cli/pane-functions/dom.ts +36 -10
- package/src/cli/pane-functions/function-help.ts +4 -2
- package/src/cli/pane-functions/headless.ts +12 -1
- package/src/cli/pane-functions/index.ts +44 -3
- package/src/cli/pane-functions/lock-note.ts +56 -0
- package/src/cli/pane-functions/report-notices.ts +15 -0
- package/src/cli/symbol-args.ts +19 -0
- package/src/components/command-bar/list/view.tsx +2 -1
- package/src/components/command-bar/workflow/builtin.ts +2 -2
- package/src/components/command-bar/workflow/collection-actions.ts +1 -1
- package/src/components/feedback-dialog.tsx +1 -1
- package/src/components/index.ts +3 -2
- package/src/components/layout/header.tsx +58 -14
- package/src/components/layout/shell/chrome.ts +10 -4
- package/src/components/layout/shell/index.tsx +1 -1
- package/src/components/layout/status-bar-groups.ts +1 -1
- package/src/components/layout/status-bar.tsx +2 -2
- package/src/components/markdown-text.tsx +14 -2
- package/src/components/reader/article-reader.tsx +1 -1
- package/src/components/reader/detail-reader.tsx +10 -7
- package/src/components/reader/reader-text.tsx +32 -17
- package/src/components/ui/column-grid.tsx +206 -0
- package/src/components/ui/display.tsx +40 -3
- package/src/components/ui/fill.ts +8 -0
- package/src/components/ui/index.ts +2 -0
- package/src/layout-marketplace/gallery.tsx +2 -2
- package/src/layout-marketplace/linked-sync.ts +1 -1
- package/src/market-data/market/freshness.ts +3 -0
- package/src/market-data/published-apac-sessions.ts +12 -2
- package/src/market-data/quotes/screener-live-quotes.ts +31 -7
- package/src/plugins/builtin/account-management/form-components.tsx +4 -7
- package/src/plugins/builtin/account-management/password-dialog.tsx +3 -2
- package/src/plugins/builtin/backtest/pane.tsx +9 -3
- package/src/plugins/builtin/buildout/detail/company.tsx +4 -3
- package/src/plugins/builtin/buildout/detail/ui.tsx +9 -12
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chart-composer/presets.ts +33 -1
- package/src/plugins/builtin/chat/channel-labels.ts +2 -2
- package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -1
- package/src/plugins/builtin/chat/content/index.tsx +2 -2
- package/src/plugins/builtin/chat/conversations.ts +1 -1
- package/src/plugins/builtin/chat/index.tsx +2 -2
- package/src/plugins/builtin/chat/share/flow.tsx +1 -1
- package/src/plugins/builtin/chat/sidebar-rows.ts +1 -1
- package/src/plugins/builtin/chat/sidebar.tsx +2 -2
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +2 -2
- package/src/plugins/builtin/cloud/askg/pane.tsx +2 -1
- package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +3 -2
- package/src/plugins/builtin/cloud/auth-actions.tsx +1 -1
- package/src/plugins/builtin/cloud/auth-commands.ts +1 -1
- package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -1
- package/src/plugins/builtin/cloud/team/acm-tab.tsx +3 -3
- package/src/plugins/builtin/cloud/team/capability.ts +2 -2
- package/src/plugins/builtin/cloud/team/command.ts +4 -4
- package/src/plugins/builtin/cloud/team/module.tsx +3 -3
- package/src/plugins/builtin/cloud/team/pane-model.ts +2 -2
- package/src/plugins/builtin/cloud/team/pane-sections.tsx +1 -1
- package/src/plugins/builtin/cloud/team/pane-ui.tsx +1 -1
- package/src/plugins/builtin/cloud/team/pane.tsx +3 -3
- package/src/plugins/builtin/cloud/team/status-widget.tsx +3 -3
- package/src/plugins/builtin/cloud/team/team-state-host.ts +1 -1
- package/src/plugins/builtin/cloud/team/views.ts +2 -2
- package/src/plugins/builtin/cloud/thesis/board-pane.tsx +1 -1
- package/src/plugins/builtin/cloud/thesis/detail.tsx +12 -10
- package/src/plugins/builtin/cloud/thesis/ticker-tab.tsx +1 -1
- package/src/plugins/builtin/cloud/upgrade-command.ts +1 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +8 -4
- package/src/plugins/builtin/connections/pane.tsx +16 -5
- package/src/plugins/builtin/cpi/pane.tsx +5 -4
- package/src/plugins/builtin/custom-view/pane.tsx +2 -2
- package/src/plugins/builtin/dividend-yield/headless.ts +8 -2
- package/src/plugins/builtin/dividend-yield/pane.tsx +4 -2
- package/src/plugins/builtin/docs/blocks.tsx +13 -2
- package/src/plugins/builtin/docs/pane.tsx +6 -5
- package/src/plugins/builtin/doe/index.tsx +11 -4
- package/src/plugins/builtin/doe/pane.tsx +5 -4
- package/src/plugins/builtin/doe/prices.ts +84 -0
- package/src/plugins/builtin/doe/series.ts +4 -1
- package/src/plugins/builtin/earnings/board-model.ts +18 -1
- package/src/plugins/builtin/earnings/board-table.ts +18 -3
- package/src/plugins/builtin/earnings/headless.ts +13 -2
- package/src/plugins/builtin/earnings/history-model.ts +1 -1
- package/src/plugins/builtin/earnings/history-table.ts +2 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +7 -21
- package/src/plugins/builtin/earnings-calls/index.tsx +2 -0
- package/src/plugins/builtin/earnings-calls/lock-note.ts +17 -0
- package/src/plugins/builtin/equity-screener/headless.ts +59 -1
- package/src/plugins/builtin/equity-screener/live.ts +82 -31
- package/src/plugins/builtin/equity-screener/pane.tsx +60 -23
- package/src/plugins/builtin/estimate-revisions/client.ts +1 -1
- package/src/plugins/builtin/estimate-revisions/headless.ts +9 -2
- package/src/plugins/builtin/estimate-revisions/index.tsx +3 -0
- package/src/plugins/builtin/estimate-revisions/lock-note.ts +23 -0
- package/src/plugins/builtin/executives/index.tsx +18 -0
- package/src/plugins/builtin/exposure/charts.tsx +4 -3
- package/src/plugins/builtin/filing-events/index.tsx +8 -10
- package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
- package/src/plugins/builtin/gpu/equities.tsx +9 -6
- package/src/plugins/builtin/help/feedback-tab.tsx +1 -1
- package/src/plugins/builtin/iv-history/format.ts +3 -0
- package/src/plugins/builtin/iv-history/headless.ts +3 -1
- package/src/plugins/builtin/iv-history/index.tsx +3 -1
- package/src/plugins/builtin/iv-history/model.ts +3 -1
- package/src/plugins/builtin/iv-history/screen-pane.tsx +3 -1
- package/src/plugins/builtin/iv-history/universe.ts +1 -1
- package/src/plugins/builtin/jobs/headless.ts +6 -20
- package/src/plugins/builtin/jobs/index.tsx +2 -0
- package/src/plugins/builtin/jobs/lock-note.ts +16 -0
- package/src/plugins/builtin/notes/index.tsx +1 -1
- package/src/plugins/builtin/notes/owner.tsx +2 -2
- package/src/plugins/builtin/options/carry.ts +36 -0
- package/src/plugins/builtin/options/index.tsx +2 -1
- package/src/plugins/builtin/options/model-spot.ts +94 -0
- package/src/plugins/builtin/options/open-interest-as-of.ts +74 -0
- package/src/plugins/builtin/options/view.tsx +69 -11
- package/src/plugins/builtin/options-calculator/pane.tsx +56 -1
- package/src/plugins/builtin/options-calculator/premium-history.ts +34 -0
- package/src/plugins/builtin/options-scenario/client.ts +33 -9
- package/src/plugins/builtin/options-scenario/csp.ts +168 -0
- package/src/plugins/builtin/options-scenario/evidence.ts +3 -0
- package/src/plugins/builtin/options-scenario/headless.ts +24 -8
- package/src/plugins/builtin/options-scenario/index.tsx +3 -1
- package/src/plugins/builtin/options-scenario/pane.tsx +32 -6
- package/src/plugins/builtin/portfolio-list/mutations.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +4 -2
- package/src/plugins/builtin/research/event-model.ts +7 -4
- package/src/plugins/builtin/risk-factors/index.tsx +18 -0
- package/src/plugins/builtin/risk-factors/pane.tsx +2 -1
- package/src/plugins/builtin/scanner/headless.ts +9 -2
- package/src/plugins/builtin/scanner/index.ts +3 -0
- package/src/plugins/builtin/scanner/lock-note.ts +17 -0
- package/src/plugins/builtin/series-inventory.ts +2 -1
- package/src/plugins/builtin/shared/cloud-denied.ts +39 -8
- package/src/plugins/builtin/shared/earnings-report.ts +10 -4
- package/src/plugins/builtin/shared/pro-lock-note.ts +58 -0
- package/src/plugins/builtin/shared/series-list-detail.tsx +10 -5
- package/src/plugins/builtin/social-mentions/client.ts +3 -3
- package/src/plugins/builtin/social-mentions/headless.ts +6 -2
- package/src/plugins/builtin/social-mentions/index.tsx +2 -1
- package/src/plugins/builtin/social-mentions/lock-note.ts +15 -0
- package/src/plugins/builtin/supply-chain/flow.tsx +15 -13
- package/src/plugins/builtin/supply-chain/graph.tsx +6 -4
- package/src/plugins/builtin/supply-chain/pane.tsx +8 -5
- package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +14 -1
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +9 -7
- package/src/plugins/builtin/ticker-detail/headless.ts +4 -1
- package/src/plugins/builtin/ticker-detail/overview/components.tsx +105 -104
- package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -2
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +34 -8
- package/src/plugins/builtin/vol-surface/pane.tsx +6 -3
- package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
- package/src/renderers/dom/data-table/index.tsx +48 -15
- package/src/renderers/dom/data-table/row.tsx +1 -1
- package/src/renderers/dom/desktop/popover.tsx +6 -15
- package/src/renderers/dom/host/input.tsx +27 -3
- package/src/renderers/dom/host/surface-3d.tsx +6 -4
- package/src/renderers/dom/styles.css +26 -6
- package/src/renderers/electrobun/view/app-services.ts +2 -22
- package/src/renderers/electrobun/view/news-source.ts +42 -0
- package/src/series/validate.ts +5 -0
- package/src/shares/dialog.tsx +2 -1
- package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
- package/src/sources/provider-router/financials.ts +5 -0
- package/src/{plugins/builtin/cloud/team → team}/collections-sync.ts +6 -6
- package/src/{plugins/builtin/cloud/team → team}/collections.ts +3 -3
- package/src/{plugins/builtin/cloud/team → team}/model.ts +2 -2
- package/src/{plugins/builtin/cloud/team → team}/pane-request.ts +1 -1
- package/src/{plugins/builtin/cloud/team → team}/store.ts +3 -3
- package/src/types/data-provider.ts +3 -0
- package/src/types/financials.ts +28 -0
- package/src/types/plugin.ts +17 -4
- package/src/utils/decimal-comma.ts +18 -0
- package/src/utils/exchanges.ts +7 -0
- package/src/utils/financial-statements.ts +25 -0
- package/src/utils/forward-dividend.ts +39 -9
- package/src/utils/fundamentals.ts +41 -1
- package/src/utils/option-expiry.ts +16 -0
- package/src/utils/sec.ts +2 -1
|
@@ -23,7 +23,7 @@ import { fail } from "../errors";
|
|
|
23
23
|
import type { MarketContext } from "../types";
|
|
24
24
|
import type { CliCommandContext, GloomPlugin } from "../../types/plugin";
|
|
25
25
|
import { indexRootFor, indexRootTryLine } from "../index-roots";
|
|
26
|
-
import { knownGapMessage } from "../known-gaps";
|
|
26
|
+
import { knownGapMessage, settingsPointer } from "../known-gaps";
|
|
27
27
|
import { instrumentTypeLabel } from "../../tickers/instrument-kind";
|
|
28
28
|
import { canonicalExchange, isUsListingExchange } from "../../utils/exchanges";
|
|
29
29
|
import { groupByCompany } from "../../tickers/company-name";
|
|
@@ -325,9 +325,9 @@ function catalogHint(query: string): string {
|
|
|
325
325
|
|
|
326
326
|
/**
|
|
327
327
|
* The notes beside the results: the index a bare root names, data the query
|
|
328
|
-
* asks for that no source carries yet, the central bank it names,
|
|
329
|
-
*
|
|
330
|
-
* listed already said where to look instead.
|
|
328
|
+
* asks for that no source carries yet, the central bank it names, the setting
|
|
329
|
+
* it names ("light theme"), and for an empty search the catalog, unless one of
|
|
330
|
+
* those or the functions listed already said where to look instead.
|
|
331
331
|
*/
|
|
332
332
|
function searchNotes(
|
|
333
333
|
query: string,
|
|
@@ -337,9 +337,10 @@ function searchNotes(
|
|
|
337
337
|
): string[] {
|
|
338
338
|
const gap = knownGapMessage(query);
|
|
339
339
|
const bank = searchedCentralBank(query);
|
|
340
|
+
const setting = settingsPointer(query);
|
|
340
341
|
const empty = candidates.length === 0 && !fate.stopped;
|
|
341
|
-
return [...fate.notes, indexRootNote(query, candidates), gap, bank ? centralBankLine(bank) : null,
|
|
342
|
-
empty && !gap && !bank && functions.length === 0 ? catalogHint(query) : null]
|
|
342
|
+
return [...fate.notes, indexRootNote(query, candidates), gap, bank ? centralBankLine(bank) : null, setting,
|
|
343
|
+
empty && !gap && !bank && !setting && functions.length === 0 ? catalogHint(query) : null]
|
|
343
344
|
.filter((note): note is string => !!note);
|
|
344
345
|
}
|
|
345
346
|
|
|
@@ -379,6 +380,8 @@ export function buildSearchReport({
|
|
|
379
380
|
if (gap) lines.push("", gap);
|
|
380
381
|
const bank = searchedCentralBank(query);
|
|
381
382
|
if (bank) lines.push("", centralBankLine(bank));
|
|
383
|
+
const setting = settingsPointer(query);
|
|
384
|
+
if (setting) lines.push("", setting);
|
|
382
385
|
const charted = renderSeriesMatches(query, series, SEARCH_SERIES_LIMIT, cliTerminalWidth() ?? 100, "catalog");
|
|
383
386
|
// A query that names the data ("carbon", "day-ahead") lists its series first; words that happen to
|
|
384
387
|
// be in a series name ("apple") list them after the tickers.
|
|
@@ -390,7 +393,7 @@ export function buildSearchReport({
|
|
|
390
393
|
if (candidates.length === 0) {
|
|
391
394
|
if (fate.stopped) return lines.join("\n");
|
|
392
395
|
if (functions.length > 0) lines.push("", renderFunctionMatches(functions));
|
|
393
|
-
else if (!gap && !bank && !charted) lines.push(cliStyles.muted("No matches found."), cliStyles.muted(catalogHint(query)));
|
|
396
|
+
else if (!gap && !bank && !setting && !charted) lines.push(cliStyles.muted("No matches found."), cliStyles.muted(catalogHint(query)));
|
|
394
397
|
return lines.join("\n");
|
|
395
398
|
}
|
|
396
399
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { formatShortDate, parseDisplayDate } from "../../utils/datetime-format";
|
|
2
2
|
import { formatReportedMoney } from "../../utils/reported-money";
|
|
3
|
-
import { convertedPriceEarningsNote, epsConversionNote, fundamentalsCurrency, reportedEnterpriseValue, ttmConversionNote } from "../../utils/fundamentals";
|
|
3
|
+
import { convertedPriceEarningsNote, enterpriseValueLabel, enterpriseValueReconciliationNote, epsConversionNote, fundamentalsCurrency, reportedEnterpriseValue, ttmConversionNote } from "../../utils/fundamentals";
|
|
4
4
|
import { latestFinancialPeriod } from "../../utils/latest-financial-period";
|
|
5
5
|
import { exportedFundamentals, formatPriceEarnings, priceEarningsOnEarnings } from "../../utils/price-earnings";
|
|
6
6
|
import { describeFundamentalMarketCap, selectMarketCapitalization } from "../../utils/market-capitalization";
|
|
@@ -336,11 +336,18 @@ function reportedMoney(value: number | undefined, currency: string | undefined,
|
|
|
336
336
|
return { text: formatReportedMoney(value, currency, perShare), value, unit: currency?.trim() || "ccy?" };
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
/** The EV line: labelled "(provider)" when it is more than 5% from market cap + debt - cash, "unavailable" when withdrawn. */
|
|
340
|
+
function enterpriseValueFigure(figure: MetricFigure, fundamentals: TickerFinancials["fundamentals"]): MetricFigure {
|
|
341
|
+
const label = enterpriseValueLabel(fundamentals);
|
|
342
|
+
if (label === "unavailable") return { text: "unavailable", value: null, unit: figure.unit };
|
|
343
|
+
return label === "provider" && figure.text !== "—" ? { ...figure, text: `${figure.text} (provider)` } : figure;
|
|
344
|
+
}
|
|
345
|
+
|
|
339
346
|
function percentFigure(fraction: number | null | undefined, format: (value: number) => string): MetricFigure {
|
|
340
347
|
return fraction != null ? { text: format(fraction), value: fraction * 100, unit: "%" } : { text: "—" };
|
|
341
348
|
}
|
|
342
349
|
|
|
343
|
-
/** A forward yield says what it annualizes and when, as DVD does; any other yield is the figure alone. */
|
|
350
|
+
/** A forward yield says what it annualizes and when, as DVD does (a declared distribution by its ex-date); any other yield is the figure alone. */
|
|
344
351
|
function forwardYieldFigure(fundamentals: TickerFinancials["fundamentals"]): MetricFigure {
|
|
345
352
|
const figure = percentFigure(fundamentals?.dividendYield, formatFractionPercentCell);
|
|
346
353
|
return fundamentals?.dividendYieldBasis === "forward" && fundamentals.dividendYield != null
|
|
@@ -366,7 +373,7 @@ function fundamentalsMetrics(
|
|
|
366
373
|
const receipt = fundamentals?.sharesOutstanding != null && sharesOutstandingInReceipts(quote, fundamentals, profile?.description);
|
|
367
374
|
return [
|
|
368
375
|
{ label: "Market Cap", ...marketCap },
|
|
369
|
-
{ label: "Enterprise Value", ...enterpriseValue },
|
|
376
|
+
{ label: "Enterprise Value", ...enterpriseValueFigure(enterpriseValue, fundamentals) },
|
|
370
377
|
// A multiple over a loss is N/M, even beside a positive one the source served from an older period.
|
|
371
378
|
{ label: "P/E (TTM)", ...multipleFigure(priceEarningsOnEarnings(fundamentals?.trailingPE, fundamentals?.eps)) },
|
|
372
379
|
{ label: "Forward P/E", ...multipleFigure(priceEarningsOnEarnings(fundamentals?.forwardPE, fundamentals?.forwardEps)) },
|
|
@@ -404,10 +411,11 @@ function metricLines(metrics: readonly FundamentalsMetric[]): Array<[string, str
|
|
|
404
411
|
const TRAILING_MONEY_METRICS = new Set(["EPS (TTM)", "Revenue (TTM)", "Net Income (TTM)", "Operating Cash Flow (TTM)", "Free Cash Flow (TTM)"]);
|
|
405
412
|
|
|
406
413
|
/**
|
|
407
|
-
* The
|
|
408
|
-
* currency than the amounts beside it, and what a P/E on
|
|
414
|
+
* The notes under shown fundamentals: how a labelled or withdrawn EV compares with market cap + debt - cash, what a
|
|
415
|
+
* converted TTM block was converted from, an EPS in another currency than the amounts beside it, and what a P/E on
|
|
416
|
+
* such an EPS divides.
|
|
409
417
|
*/
|
|
410
|
-
function
|
|
418
|
+
function fundamentalsNotes(
|
|
411
419
|
fundamentals: TickerFinancials["fundamentals"],
|
|
412
420
|
metrics: readonly FundamentalsMetric[],
|
|
413
421
|
quoteCurrency: string | undefined,
|
|
@@ -418,7 +426,8 @@ function trailingConversionNotes(
|
|
|
418
426
|
const priceEarnings = metrics.some((metric) => metric.label === "P/E (TTM)" && typeof metric.value === "number")
|
|
419
427
|
? convertedPriceEarningsNote(fundamentals, quoteCurrency)
|
|
420
428
|
: undefined;
|
|
421
|
-
|
|
429
|
+
const enterpriseValue = shown("Enterprise Value") ? enterpriseValueReconciliationNote(fundamentals) : undefined;
|
|
430
|
+
return [enterpriseValue, conversion, eps, priceEarnings].filter((note): note is string => !!note);
|
|
422
431
|
}
|
|
423
432
|
|
|
424
433
|
const VALUATION_METRICS = new Set(["Market Cap", "Enterprise Value", "P/E (TTM)", "Forward P/E", "PEG", "EPS (TTM)"]);
|
|
@@ -523,7 +532,7 @@ export function renderFundamentalsReport(
|
|
|
523
532
|
const fund = fundFactsNote(symbol, financials.exchange || quote?.exchangeName, financials);
|
|
524
533
|
if (lines.length === before) lines.push("", cliStyles.muted([`No ${view} reported for ${financials.symbol}.`, fund].filter(Boolean).join(" ")));
|
|
525
534
|
else if (fund) lines.push(cliStyles.muted(fund));
|
|
526
|
-
for (const note of
|
|
535
|
+
for (const note of fundamentalsNotes(financials.fundamentals, metrics, quote?.currency)) lines.push(cliStyles.muted(note));
|
|
527
536
|
if (view === "fundamentals") appendTextSection(lines, "Description", profile?.description);
|
|
528
537
|
return lines.join("\n");
|
|
529
538
|
}
|
|
@@ -563,7 +572,7 @@ export function fundamentalsReportTables(
|
|
|
563
572
|
notes: [
|
|
564
573
|
...metrics.length === 0
|
|
565
574
|
? [`No ${view} reported for ${financials.symbol}.`]
|
|
566
|
-
:
|
|
575
|
+
: fundamentalsNotes(financials.fundamentals, metrics, financials.quote?.currency),
|
|
567
576
|
...fund ? [fund] : [],
|
|
568
577
|
],
|
|
569
578
|
}),
|
|
@@ -726,7 +735,7 @@ export async function buildTickerReport({
|
|
|
726
735
|
const metrics = fundamentalsMetrics(quote, priced.fundamentals, profile, { text: marketCapText }, priceReturns, fundamentalsCurrency(financials), { text: enterpriseValueText });
|
|
727
736
|
const beforeFundamentals = lines.length;
|
|
728
737
|
appendMetricSection(lines, "Fundamentals", metricLines(metrics));
|
|
729
|
-
for (const note of
|
|
738
|
+
for (const note of fundamentalsNotes(fundamentals, metrics, quote?.currency)) lines.push(cliStyles.muted(note));
|
|
730
739
|
// Where a fund's facts would go: under its fundamentals, or in their place.
|
|
731
740
|
const fund = fundFactsNote(quote?.symbol ?? symbol,
|
|
732
741
|
quote?.tradingExchangeName || listingExchange || quote?.listingExchangeName || quote?.exchangeName || tickerFile?.metadata.exchange, financials, tickerFile);
|
package/src/cli/glossary.ts
CHANGED
|
@@ -351,7 +351,7 @@ export const GLOSSARY: readonly GlossaryEntry[] = [
|
|
|
351
351
|
term: "TTM", aliases: ["trailing twelve months", "trailing 12 months", "trailing four quarters", "ltm", "last twelve months"],
|
|
352
352
|
functions: ["FA"],
|
|
353
353
|
definition: "Trailing twelve months: the four latest quarters added up, a year that ends with the latest quarter rather than the fiscal year. "
|
|
354
|
-
+ "FA's quarterly leverage ratios read their flows over it, and show < 4 quarters when fewer are on file.",
|
|
354
|
+
+ "FA's quarterly leverage and valuation ratios (P/E, EV / EBITDA, P/S, FCF yield) read their flows over it, and show < 4 quarters when fewer are on file.",
|
|
355
355
|
},
|
|
356
356
|
// Funds (ETF). Named in what ETF says it cannot show yet.
|
|
357
357
|
{
|
package/src/cli/index.ts
CHANGED
|
@@ -37,7 +37,7 @@ import { createSystemCliCommands } from "./commands/system";
|
|
|
37
37
|
import { brokerCliCommand, ibkrCliCommand } from "./commands/broker";
|
|
38
38
|
import { listPlugins, updatePlugins } from "./commands/plugins";
|
|
39
39
|
import { installListedPlugin, removePlugin } from "../plugins/installer";
|
|
40
|
-
import { runPaneCatalog, runPaneFunction, runPaneScreenshot } from "./pane-functions";
|
|
40
|
+
import { functionCodeCommand, runPaneCatalog, runPaneFunction, runPaneScreenshot } from "./pane-functions";
|
|
41
41
|
import { catalogFredSeriesIds } from "./series-search";
|
|
42
42
|
|
|
43
43
|
const { width: SHOT_WIDTH, height: SHOT_HEIGHT } = SHOT_SIZE_LIMITS;
|
|
@@ -60,11 +60,11 @@ function createCoreCliCommands(
|
|
|
60
60
|
usage: ["help [command]", "<command> --help"],
|
|
61
61
|
examples: ["help", "help quote", "quote --help"],
|
|
62
62
|
},
|
|
63
|
-
execute: (args, ctx) => {
|
|
63
|
+
execute: async (args, ctx) => {
|
|
64
64
|
const topic = args[0];
|
|
65
65
|
if (topic) {
|
|
66
66
|
const command = lookupCommand(topic);
|
|
67
|
-
if (!command) failUnknownCommand(topic, allCommands());
|
|
67
|
+
if (!command) failUnknownCommand(topic, allCommands(), await functionCodeCommand(ctx, topic, []));
|
|
68
68
|
printCommandHelp(command, ctx);
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
@@ -306,7 +306,9 @@ function printCommandHelp(command: CliCommandDef, ctx: CliCommandContext): void
|
|
|
306
306
|
ctx.printResult({ data: { ...describeCliCommand(command), sections: command.help?.sections ?? [] } });
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
-
function
|
|
309
|
+
/** `function` is what the word is when it is a function's code, and the command that runs it. */
|
|
310
|
+
function failUnknownCommand(token: string, commands: CliCommandDef[], fn: { code: string; command: string } | null = null): never {
|
|
311
|
+
if (fn) fail(`${fn.code} is a function: ${fn.command}`);
|
|
310
312
|
const suggestion = suggestCliCommand(
|
|
311
313
|
normalizeCliCommandToken(token),
|
|
312
314
|
commands.flatMap((command) => [command.name, ...(command.aliases ?? [])]),
|
|
@@ -397,14 +399,20 @@ export async function dispatchCli(args: string[], options: DispatchCliOptions =
|
|
|
397
399
|
/** Explains why dispatch found no command in `args`, suggesting the closest one. */
|
|
398
400
|
export async function failUnknownCliCommand(args: string[], options: DispatchCliOptions = {}): Promise<never> {
|
|
399
401
|
let token = args[0] ?? "";
|
|
402
|
+
let parsed: ReturnType<typeof parseCliGlobalArgs> | null = null;
|
|
400
403
|
try {
|
|
401
404
|
// Global flags may come first, so name the token dispatch actually looked up.
|
|
402
|
-
|
|
405
|
+
parsed = parseCliGlobalArgs(args);
|
|
406
|
+
token = parsed.args[0] ?? token;
|
|
403
407
|
} catch {
|
|
404
408
|
// dispatchCli already reported flags that do not parse.
|
|
405
409
|
}
|
|
406
410
|
const registry = await createRegistry(options);
|
|
407
|
-
|
|
411
|
+
// A function's code typed as a command is pointed at `fn`, with the arguments the reader gave.
|
|
412
|
+
const fn = token && parsed
|
|
413
|
+
? await functionCodeCommand(createCliCommandContext("core", registry, parsed.options, parsed.args.slice(1)), token, parsed.args.slice(1))
|
|
414
|
+
: null;
|
|
415
|
+
return failUnknownCommand(token, listedCommands(registry).map((entry) => entry.command), fn);
|
|
408
416
|
}
|
|
409
417
|
|
|
410
418
|
export async function runCli(args: string[], options: DispatchCliOptions = {}): Promise<boolean> {
|
package/src/cli/known-gaps.ts
CHANGED
|
@@ -82,6 +82,28 @@ export function topicPointer(query: string): { message: string; codes: string[]
|
|
|
82
82
|
return { message: `${pointer.topic}: not in the terminal yet; closest: ${closest}`, codes: pointer.closest.map(([code]) => code) };
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Settings people look for by name that are a `config` command, not a function or a ticker: the line `search` and
|
|
87
|
+
* `catalog` print beside whatever else the query finds. A query asks for one when every word is a word of that
|
|
88
|
+
* setting and one of them names it: "light theme", "dark", "colour-blind", but not "ai theme" (THEM's basket) or
|
|
89
|
+
* "dark pool".
|
|
90
|
+
*/
|
|
91
|
+
const SETTING_POINTERS: ReadonlyArray<{ names: readonly string[]; words: readonly string[]; line: string }> = [
|
|
92
|
+
{
|
|
93
|
+
names: ["theme", "themes", "light", "dark", "colorblind", "colourblind", "blind"],
|
|
94
|
+
words: ["mode", "color", "colour", "colors", "colours", "high", "contrast", "change", "switch", "set", "setting", "settings"],
|
|
95
|
+
line: "Themes: gloomberb config themes",
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
/** The `config` command for a query that names a setting ("light theme"), else null. */
|
|
100
|
+
export function settingsPointer(query: string): string | null {
|
|
101
|
+
const asked = words(query);
|
|
102
|
+
if (asked.length === 0) return null;
|
|
103
|
+
return SETTING_POINTERS.find(({ names, words: other }) => asked.some((word) => names.includes(word))
|
|
104
|
+
&& asked.every((word) => names.includes(word) || other.includes(word)))?.line ?? null;
|
|
105
|
+
}
|
|
106
|
+
|
|
85
107
|
/** Letters and digits only, lower case: `EU-ETS`, `eu ets` and `EUETS` are one lookup. */
|
|
86
108
|
function lookupKey(text: string): string {
|
|
87
109
|
return text.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
@@ -115,6 +115,43 @@ export function buildPaneFunctionLookup(registry: PaneFunctionCatalog): Map<stri
|
|
|
115
115
|
return lookup;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
/** What a function takes after its code, as the catalog writes it: `<ticker>`, `<fund, ticker, or CIK>`, or nothing. */
|
|
119
|
+
function argumentHint({ argKind, argPlaceholder }: Pick<PaneCatalogEntry, "argKind" | "argPlaceholder">): string {
|
|
120
|
+
if (argPlaceholder) return `<${argPlaceholder}>`;
|
|
121
|
+
return argKind && argKind !== "none" ? `<${argKind}>` : "";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A command-line token that is the code or an alias of a function, with what the function takes. */
|
|
125
|
+
export interface FunctionCode {
|
|
126
|
+
argument: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Whether `token` is the code or an alias of a function (`OVDV`, `EEO`), in any
|
|
131
|
+
* case: the function `fn` opens for it, not one it merely matches by a keyword
|
|
132
|
+
* or a label.
|
|
133
|
+
*/
|
|
134
|
+
export function findFunctionCode(registry: PaneFunctionCatalog, token: string): FunctionCode | null {
|
|
135
|
+
const wanted = normalizeLookupToken(token);
|
|
136
|
+
const found = wanted ? buildPaneFunctionLookup(registry).get(wanted) : undefined;
|
|
137
|
+
if (!found || !("paneId" in found && "description" in found)) return null;
|
|
138
|
+
const pane = registry.panes.get(found.paneId);
|
|
139
|
+
const headless = pane ? getHeadlessPaneDefinition(found, pane) : undefined;
|
|
140
|
+
const codes = [
|
|
141
|
+
found.shortcut?.prefix,
|
|
142
|
+
...(found.shortcut?.aliases ?? []),
|
|
143
|
+
headless?.discovery?.id,
|
|
144
|
+
...(headless?.discovery?.aliases ?? []),
|
|
145
|
+
];
|
|
146
|
+
if (!codes.some((code) => code && normalizeLookupToken(code) === wanted)) return null;
|
|
147
|
+
return {
|
|
148
|
+
argument: argumentHint({
|
|
149
|
+
argKind: headless?.argument.kind ?? found.shortcut?.argKind,
|
|
150
|
+
argPlaceholder: headless?.argument.placeholder ?? found.shortcut?.argPlaceholder,
|
|
151
|
+
}),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
118
155
|
function sampleArgForTemplate(template: PaneTemplateDef): string {
|
|
119
156
|
switch (template.shortcut?.argKind) {
|
|
120
157
|
case "ticker":
|
|
@@ -438,11 +475,6 @@ function readinessCell(readiness: PaneFunctionReadiness): string {
|
|
|
438
475
|
return cliStyles.muted(label);
|
|
439
476
|
}
|
|
440
477
|
|
|
441
|
-
function catalogArgument(entry: PaneCatalogEntry): string {
|
|
442
|
-
if (entry.argPlaceholder) return `<${entry.argPlaceholder}>`;
|
|
443
|
-
return entry.argKind && entry.argKind !== "none" ? `<${entry.argKind}>` : "";
|
|
444
|
-
}
|
|
445
|
-
|
|
446
478
|
// Choices longer than this move from the flag into its description, where they can wrap.
|
|
447
479
|
const MAX_INLINE_CHOICES = 24;
|
|
448
480
|
|
|
@@ -473,7 +505,7 @@ export function limitationSentence(text: string): string {
|
|
|
473
505
|
function renderCatalogEntry(entry: PaneCatalogEntry): string {
|
|
474
506
|
const width = Math.min(cliTerminalWidth() ?? CATALOG_TEXT_WIDTH, CATALOG_TEXT_WIDTH);
|
|
475
507
|
const capability = entry.capability;
|
|
476
|
-
const argument =
|
|
508
|
+
const argument = argumentHint(entry);
|
|
477
509
|
const invocation = `${entry.token}${argument ? ` ${argument}` : ""}`;
|
|
478
510
|
const lines = [
|
|
479
511
|
`${cliStyles.accent(entry.token)} ${cliStyles.bold(entry.label)}`,
|
|
@@ -543,25 +575,28 @@ export function renderPaneCatalogReport(
|
|
|
543
575
|
glossary: GlossaryLookup = { exact: [], partial: [] },
|
|
544
576
|
gap: string | null = null,
|
|
545
577
|
series: readonly SeriesMatch[] = [],
|
|
578
|
+
/** The `config` command a query names a setting of ("light theme"), printed last, beside the functions. */
|
|
579
|
+
setting: string | null = null,
|
|
546
580
|
): string {
|
|
547
581
|
const query = args.query.trim().toLowerCase();
|
|
548
582
|
const exact = query
|
|
549
583
|
? entries.find((entry) => entry.token.toLowerCase() === query)
|
|
550
584
|
?? entries.find((entry) => entry.aliases.some((alias) => alias.toLowerCase() === query))
|
|
551
585
|
: undefined;
|
|
552
|
-
|
|
586
|
+
const withSetting = (text: string) => [text, setting ?? ""].filter(Boolean).join("\n\n");
|
|
587
|
+
if (exact) return withSetting(renderCatalogEntry(exact));
|
|
553
588
|
|
|
554
589
|
const width = Math.min(cliTerminalWidth() ?? CATALOG_TEXT_WIDTH, CATALOG_TEXT_WIDTH);
|
|
555
590
|
const terms = renderCatalogGlossary(glossary, width);
|
|
556
591
|
const charted = renderSeriesMatches(args.query.trim(), series, Number.isFinite(args.limit) ? CATALOG_SERIES_LIMIT : series.length, width);
|
|
557
|
-
const withGap = (text: string) => [gap ? wrapText(gap, width).join("\n") : "", text].filter(Boolean).join("\n\n");
|
|
592
|
+
const withGap = (text: string) => withSetting([gap ? wrapText(gap, width).join("\n") : "", text].filter(Boolean).join("\n\n"));
|
|
558
593
|
const withTerms = (text: string) => withGap([text, charted, terms].filter(Boolean).join("\n\n"));
|
|
559
594
|
if (args.query && entries.length === 1) return withTerms(renderCatalogEntry(entries[0]!));
|
|
560
595
|
|
|
561
596
|
const note = (text: string) => wrapText(text, width).map((line) => cliStyles.muted(line)).join("\n");
|
|
562
597
|
if (entries.length === 0) {
|
|
563
598
|
if (charted) return withGap([charted, terms].filter(Boolean).join("\n\n"));
|
|
564
|
-
if (glossary.exact.length > 0 || gap) return withGap(terms);
|
|
599
|
+
if (glossary.exact.length > 0 || gap || setting) return withGap(terms);
|
|
565
600
|
return withTerms(note(args.query
|
|
566
601
|
? `No functions match "${args.query}". Run gloomberb catalog to browse them all.`
|
|
567
602
|
: "No functions are available."));
|
|
@@ -586,7 +621,7 @@ export function renderPaneCatalogReport(
|
|
|
586
621
|
shown.map((entry) => [
|
|
587
622
|
cliStyles.command(entry.token),
|
|
588
623
|
entry.label,
|
|
589
|
-
|
|
624
|
+
argumentHint(entry),
|
|
590
625
|
readinessCell(entry.capability.reportReadiness),
|
|
591
626
|
entry.description,
|
|
592
627
|
]),
|
|
@@ -1,34 +1,39 @@
|
|
|
1
1
|
import type { DesktopPaneShotRenderedRow } from "../desktop-pane-shot";
|
|
2
|
-
import { cliStyles,
|
|
2
|
+
import { cliStyles, renderSection, renderTable } from "../../utils/cli-output";
|
|
3
3
|
import { formatUtcTime } from "../../utils/utc-time";
|
|
4
4
|
import type { MarketContext } from "../types";
|
|
5
5
|
import type { PaneFunctionReport } from "./report";
|
|
6
6
|
import type { ResolvedPaneFunction } from "./resolver";
|
|
7
7
|
import { collectShotSymbols } from "./data";
|
|
8
|
+
import { lockNoteLines } from "./lock-note";
|
|
9
|
+
import { lockNoteReason } from "../../plugins/builtin/shared/pro-lock-note";
|
|
8
10
|
import { renderDesktopShot, type PaneScreenshotResult } from "./screenshot";
|
|
9
11
|
import { deriveRenderedFreshness, formatFreshnessLine, type ReportFreshness } from "./freshness";
|
|
10
12
|
import { exportTextTable, reportFooterLines, type CliReportTables } from "../report-tables";
|
|
11
|
-
import { renderedReportFreshness, renderedReportNotices, renderedReportObservations } from "./report-notices";
|
|
13
|
+
import { renderedReportFailure, renderedReportFreshness, renderedReportNotices, renderedReportObservations } from "./report-notices";
|
|
12
14
|
|
|
13
15
|
const DOM_REPORT_WIDTH = 1280;
|
|
14
16
|
const DOM_REPORT_HEIGHT = 720;
|
|
15
17
|
const DOM_LIMITATION =
|
|
16
18
|
"Only values visible in the rendered pane are returned; clipped or off-screen data may be omitted.";
|
|
17
|
-
/** The widest a lock note wraps to, as the catalog's text. */
|
|
18
|
-
const LOCK_NOTE_WIDTH = 100;
|
|
19
19
|
const DOM_EVERY_ROW =
|
|
20
20
|
"Every row of the pane's table is returned, including rows scrolled out of view.";
|
|
21
21
|
|
|
22
22
|
/** The sign-in wall's buttons, which a rendered view reads as words: `Log in ↵ Sign up free`. */
|
|
23
23
|
const SIGN_IN_WALL_BUTTONS = /\s*\b(?:Log in|Sign up free|Resend Verification Email)(?:\s*\u21B5)?(?=\s|$)/g;
|
|
24
24
|
|
|
25
|
+
/** Whether the wall asks for the account's email to be verified, not for a sign-in. */
|
|
26
|
+
function asksToVerifyEmail(visibleText: string): boolean {
|
|
27
|
+
return /\bVerify your email\b/i.test(visibleText);
|
|
28
|
+
}
|
|
29
|
+
|
|
25
30
|
/**
|
|
26
31
|
* A pane behind the sign-in wall, as a report reads it: the wall's headline
|
|
27
32
|
* without its buttons, and what it needs in place of them.
|
|
28
33
|
*/
|
|
29
34
|
function signInWallText(visibleText: string): string {
|
|
30
35
|
const headline = visibleText.replace(SIGN_IN_WALL_BUTTONS, "").trim();
|
|
31
|
-
const needs =
|
|
36
|
+
const needs = asksToVerifyEmail(visibleText)
|
|
32
37
|
? "Requires a verified email on your Gloom account; resend the verification email in the app."
|
|
33
38
|
: "Requires a free Gloom sign-in: log in or sign up in the app.";
|
|
34
39
|
return headline ? `${headline} ${needs}` : needs;
|
|
@@ -195,14 +200,20 @@ export function buildDomPaneReportFromRender(
|
|
|
195
200
|
// A wall in place of the whole pane: nothing was read, so the report says
|
|
196
201
|
// what is locked and how to reach it, and cites no source.
|
|
197
202
|
const gate = !hasStructuredRows ? result.render.accessGate ?? null : null;
|
|
198
|
-
const lockNote = gate
|
|
203
|
+
const lockNote = gate
|
|
204
|
+
? resolved.pane.reportLockNote?.({
|
|
205
|
+
gate,
|
|
206
|
+
symbol: result.symbols[0] ?? null,
|
|
207
|
+
token: resolved.token,
|
|
208
|
+
...gate === "sign-in" && asksToVerifyEmail(result.render.visibleText ?? "") ? { verifyEmail: true } : {},
|
|
209
|
+
}) ?? null
|
|
210
|
+
: null;
|
|
199
211
|
const locked = gate === "sign-in" || lockNote ? gate : null;
|
|
200
|
-
|
|
201
|
-
const lockReason = lockNote?.replace(/\s*\n\s*/g, " ") ?? null;
|
|
212
|
+
const lockReason = lockNote ? lockNoteReason(lockNote) : null;
|
|
202
213
|
if (hasStructuredRows) {
|
|
203
214
|
textLines.push(...renderDomTables(rows));
|
|
204
215
|
} else if (lockNote) {
|
|
205
|
-
textLines.push(...
|
|
216
|
+
textLines.push(...lockNoteLines(lockNote));
|
|
206
217
|
} else if (result.render.visibleText) {
|
|
207
218
|
textLines.push(gate === "sign-in" ? signInWallText(result.render.visibleText) : result.render.visibleText);
|
|
208
219
|
} else {
|
|
@@ -321,13 +332,21 @@ function failedDomReport(
|
|
|
321
332
|
};
|
|
322
333
|
}
|
|
323
334
|
|
|
335
|
+
/** A report the rendered pane says it cannot give; the command fails with the pane's words and advice line. */
|
|
336
|
+
class RenderedReportFailure extends Error {
|
|
337
|
+
constructor(message: string, readonly details?: string) {
|
|
338
|
+
super(message);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
324
342
|
export async function buildDomFunctionReport(
|
|
325
343
|
resolved: ResolvedPaneFunction,
|
|
326
344
|
context: MarketContext,
|
|
327
345
|
rawArg: string,
|
|
328
346
|
): Promise<PaneFunctionReport> {
|
|
347
|
+
let result: PaneScreenshotResult;
|
|
329
348
|
try {
|
|
330
|
-
|
|
349
|
+
result = await renderDesktopShot({
|
|
331
350
|
resolved,
|
|
332
351
|
context,
|
|
333
352
|
rawArg,
|
|
@@ -340,6 +359,13 @@ export async function buildDomFunctionReport(
|
|
|
340
359
|
options: {},
|
|
341
360
|
captureImage: false,
|
|
342
361
|
});
|
|
362
|
+
} catch (error) {
|
|
363
|
+
return failedDomReport(resolved, rawArg, error);
|
|
364
|
+
}
|
|
365
|
+
// No header and no partial view: the error alone, and a failed exit, as the plain command gives.
|
|
366
|
+
const failure = renderedReportFailure(result.render.semanticUi ?? []);
|
|
367
|
+
if (failure) throw new RenderedReportFailure(failure.message, failure.details);
|
|
368
|
+
try {
|
|
343
369
|
return buildDomPaneReportFromRender(resolved, result);
|
|
344
370
|
} catch (error) {
|
|
345
371
|
return failedDomReport(resolved, rawArg, error);
|
|
@@ -264,8 +264,10 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
|
|
|
264
264
|
},
|
|
265
265
|
OSA: {
|
|
266
266
|
summary: "Build a multi-leg options position and value it at any spot, date and vol shift: payoff, P&L grid, breakevens and Greeks. European pricing, so no early exercise. "
|
|
267
|
-
+ "With a NAV and a budget in bps it sizes the position as a hedge: contracts, premium, notional protected and coverage of an equity sleeve."
|
|
267
|
+
+ "With a NAV and a budget in bps it sizes the position as a hedge: contracts, premium, notional protected and coverage of an equity sleeve. "
|
|
268
|
+
+ "--strategy csp sells a cash-secured put: cash secured, premium, yield on cash and annualised, distance out of the money, break-even, days to expiry and an earnings flag.",
|
|
268
269
|
usage: ["OSA AAPL", "gloomberb fn OSA AAPL --strategy vertical --expiration 2027-01-15",
|
|
270
|
+
"gloomberb fn OSA KO --strategy csp --strike 85 --expiration 2026-11-20",
|
|
269
271
|
"gloomberb fn OSA SPY --strategy put --strike 700 --expiration 2027-01-15 --nav 100m --budget-bps 50 --sleeve 100m"],
|
|
270
272
|
keys: [key("a", "dd leg"), key("c", "hain"), key("d", "ate"), key("v", "ol shift")],
|
|
271
273
|
data: OPTIONS,
|
|
@@ -393,7 +395,7 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
|
|
|
393
395
|
bloomberg: ["ERN"],
|
|
394
396
|
},
|
|
395
397
|
DVD: {
|
|
396
|
-
summary: "Cash distributions, trailing yield, the forward yield fundamentals show (latest distribution annualized), and payout growth. Future payments are not forecast.",
|
|
398
|
+
summary: "Cash distributions, trailing yield, the forward yield fundamentals show (the latest distribution annualized, or the declared one once announced), and payout growth. Future payments are not forecast.",
|
|
397
399
|
usage: ["DVD KO"],
|
|
398
400
|
keys: [STEP],
|
|
399
401
|
data: same("Daily"),
|
|
@@ -40,6 +40,7 @@ import { isRecord } from "../../utils/guards";
|
|
|
40
40
|
import { formatUtcTime, localTimeSuffix } from "../../utils/utc-time";
|
|
41
41
|
import { deriveHeadlessFreshness, formatFreshnessLine, staleRowsText, type ReportFreshness } from "./freshness";
|
|
42
42
|
import { accessGateStatus, errorsAccessGate, isProLock } from "./access-gate";
|
|
43
|
+
import { lockedHeadlessReport } from "./lock-note";
|
|
43
44
|
|
|
44
45
|
interface SerializableHeadlessColumn {
|
|
45
46
|
key: string;
|
|
@@ -721,7 +722,17 @@ export async function buildHeadlessFunctionReport(
|
|
|
721
722
|
context: MarketContext,
|
|
722
723
|
rawArgument: string,
|
|
723
724
|
): Promise<PaneFunctionReport> {
|
|
724
|
-
|
|
725
|
+
let loaded: LoadedHeadlessPaneModel;
|
|
726
|
+
try {
|
|
727
|
+
loaded = await loadResolvedHeadlessPaneModel(resolved, context, rawArgument);
|
|
728
|
+
} catch (error) {
|
|
729
|
+
// A Pro pane the server refused reports what its wall covers, as its rendered view does.
|
|
730
|
+
const locked = lockedHeadlessReport(resolved, error, () => (
|
|
731
|
+
buildHeadlessPaneLoadArgs(resolved.headless!, resolved.token, rawArgument, resolved.options).symbols
|
|
732
|
+
));
|
|
733
|
+
if (locked) return locked;
|
|
734
|
+
throw error;
|
|
735
|
+
}
|
|
725
736
|
const { complete, unavailableSymbols, rowCount, symbols } = reportCoverage(loaded);
|
|
726
737
|
const serialized = serializeHeadlessPaneResult(loaded.definition, loaded.result);
|
|
727
738
|
const freshness = deriveHeadlessFreshness(loaded.definition, loaded.result);
|
|
@@ -5,6 +5,7 @@ import { withMarketData } from "../context";
|
|
|
5
5
|
import { cliStyles, renderStats, type CliStatEntry } from "../../utils/cli-output";
|
|
6
6
|
import {
|
|
7
7
|
filterPaneCatalogEntries,
|
|
8
|
+
findFunctionCode,
|
|
8
9
|
renderPaneCatalogReport,
|
|
9
10
|
selectPaneCatalogMatches,
|
|
10
11
|
type PaneFunctionCatalog,
|
|
@@ -25,7 +26,7 @@ import {
|
|
|
25
26
|
import { incompleteReportGateMessage } from "./access-gate";
|
|
26
27
|
import { withPersistedCloudSession } from "./cloud-session";
|
|
27
28
|
import { ListingArgError, listingReadNotes, loadForListing, quotedListingChoiceNotes } from "../listing-arg";
|
|
28
|
-
import { knownGapMessage, topicPointer } from "../known-gaps";
|
|
29
|
+
import { knownGapMessage, settingsPointer, topicPointer } from "../known-gaps";
|
|
29
30
|
import { selectReportTables } from "../report-tables";
|
|
30
31
|
import { cliOutputWritten } from "../result";
|
|
31
32
|
import { CLI_EXIT_CODES } from "../exit-codes";
|
|
@@ -63,6 +64,44 @@ async function withPaneRuntime<T>(
|
|
|
63
64
|
});
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
/** A word that can be a function code: letters and digits, with the dots and dashes some codes carry. */
|
|
68
|
+
const FUNCTION_CODE_WORD = /^[\w.$-]{1,24}$/;
|
|
69
|
+
|
|
70
|
+
/** An argument as a shell reads it back: bare when it is one word, in quotes when it is not. */
|
|
71
|
+
function commandWord(argument: string): string {
|
|
72
|
+
return /^[\w.:,=%+@/-]+$/.test(argument) ? argument : JSON.stringify(argument);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* What to run when a command name is a function's code (`gloomberb OVDV KO`,
|
|
77
|
+
* `gloomberb help OVDV`): `fn` with the reader's own arguments, or the
|
|
78
|
+
* function's argument hint when they gave none. Null when the word is no
|
|
79
|
+
* function code or the catalog cannot be read, and the command stays unknown.
|
|
80
|
+
*/
|
|
81
|
+
export async function functionCodeCommand(
|
|
82
|
+
ctx: CliCommandContext,
|
|
83
|
+
token: string,
|
|
84
|
+
args: readonly string[],
|
|
85
|
+
): Promise<{ code: string; command: string } | null> {
|
|
86
|
+
const word = token.trim();
|
|
87
|
+
if (!FUNCTION_CODE_WORD.test(word)) return null;
|
|
88
|
+
try {
|
|
89
|
+
const found = await withMarketData(ctx, async (market) => {
|
|
90
|
+
const registry = await createPaneCatalog(market, ctx.plugins);
|
|
91
|
+
try {
|
|
92
|
+
return findFunctionCode(registry, word);
|
|
93
|
+
} finally {
|
|
94
|
+
registry.destroy();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
if (!found) return null;
|
|
98
|
+
const code = word.replace(/^\$/, "").toUpperCase();
|
|
99
|
+
return { code, command: ["gloomberb fn", code, args.length > 0 ? args.map(commandWord).join(" ") : found.argument].filter(Boolean).join(" ") };
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
66
105
|
export async function runPaneFunction(args: string[], ctx: CliCommandContext) {
|
|
67
106
|
await runPaneCliCommand(ctx, async () => {
|
|
68
107
|
await withPaneRuntime(ctx, args, async ({ parsed, context, resolved }) => {
|
|
@@ -277,6 +316,8 @@ export async function runPaneCatalog(args: string[], ctx: CliCommandContext) {
|
|
|
277
316
|
const glossary = lookupGlossary(parsed.query);
|
|
278
317
|
// Data no source carries yet is said once: in the text, else as the envelope's warning.
|
|
279
318
|
const gap = knownGapMessage(parsed.query);
|
|
319
|
+
// A setting is a command, not a function: "light theme" names `config themes` beside THEM.
|
|
320
|
+
const setting = settingsPointer(parsed.query);
|
|
280
321
|
// The data catalog series the query names, which `fn G` charts.
|
|
281
322
|
const series = findCatalogSeries(ctx.plugins, parsed.query);
|
|
282
323
|
const listed = pointer
|
|
@@ -285,9 +326,9 @@ export async function runPaneCatalog(args: string[], ctx: CliCommandContext) {
|
|
|
285
326
|
ctx.printResult({
|
|
286
327
|
data: listed,
|
|
287
328
|
...series.length ? { metadata: { series } } : {},
|
|
288
|
-
...gap && ctx.cliOptions.format !== "text" ? { warnings: [gap] } : {},
|
|
329
|
+
...(gap || setting) && ctx.cliOptions.format !== "text" ? { warnings: [gap, setting].filter((note): note is string => !!note) } : {},
|
|
289
330
|
}, {
|
|
290
|
-
text: () => renderPaneCatalogReport(filtered, effectiveParsed, glossary, gap, series),
|
|
331
|
+
text: () => renderPaneCatalogReport(filtered, effectiveParsed, glossary, gap, series, setting),
|
|
291
332
|
});
|
|
292
333
|
} finally {
|
|
293
334
|
registry.destroy();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { refusedLock } from "../../plugins/builtin/shared/cloud-denied";
|
|
2
|
+
import { lockNoteReason } from "../../plugins/builtin/shared/pro-lock-note";
|
|
3
|
+
import { cliTerminalWidth, wrapText } from "../../utils/cli-output";
|
|
4
|
+
import { reportFooterLines } from "../report-tables";
|
|
5
|
+
import type { PaneFunctionReport } from "./report";
|
|
6
|
+
import type { ResolvedPaneFunction } from "./resolver";
|
|
7
|
+
|
|
8
|
+
/** The widest a lock note wraps to, as the catalog's text. */
|
|
9
|
+
const LOCK_NOTE_WIDTH = 100;
|
|
10
|
+
|
|
11
|
+
/** The closing line of a headless report whose pane was walled whole, as a rendered view's says it. */
|
|
12
|
+
const LOCKED_VIEW_NOTE = "The pane is locked, so no values were read.";
|
|
13
|
+
|
|
14
|
+
/** A pane's `reportLockNote`, wrapped for the text report. */
|
|
15
|
+
export function lockNoteLines(note: string): string[] {
|
|
16
|
+
return wrapText(note, Math.min(cliTerminalWidth() ?? LOCK_NOTE_WIDTH, LOCK_NOTE_WIDTH));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* What a headless report prints when the server refused its read behind a
|
|
21
|
+
* wall, as the rendered view of the same pane prints that wall: the pane's
|
|
22
|
+
* `reportLockNote`, no source line and `locked` in JSON. The refusal may be
|
|
23
|
+
* the error itself or the cause of the one a headless read wrote for it
|
|
24
|
+
* (`explainRefusal`). Null when the error is no such refusal or the pane has
|
|
25
|
+
* no note, and the error stands.
|
|
26
|
+
*/
|
|
27
|
+
export function lockedHeadlessReport(
|
|
28
|
+
resolved: ResolvedPaneFunction,
|
|
29
|
+
error: unknown,
|
|
30
|
+
/** The symbols the report was asked for, read once the error is known to be a refusal. */
|
|
31
|
+
symbolsOf: () => string[],
|
|
32
|
+
): PaneFunctionReport | null {
|
|
33
|
+
const refused = refusedLock(error);
|
|
34
|
+
if (!refused || !resolved.pane.reportLockNote) return null;
|
|
35
|
+
const gate = refused.gate;
|
|
36
|
+
const symbols = symbolsOf();
|
|
37
|
+
const note = resolved.pane.reportLockNote({ ...refused, symbol: symbols[0] ?? null, token: resolved.token });
|
|
38
|
+
const reason = lockNoteReason(note);
|
|
39
|
+
return {
|
|
40
|
+
data: {
|
|
41
|
+
kind: resolved.headless?.shape ?? "rendered-view",
|
|
42
|
+
target: resolved.token,
|
|
43
|
+
capabilityId: resolved.capability.id,
|
|
44
|
+
symbols,
|
|
45
|
+
options: resolved.options,
|
|
46
|
+
rowCount: 0,
|
|
47
|
+
empty: true,
|
|
48
|
+
complete: false,
|
|
49
|
+
unavailableSymbols: symbols,
|
|
50
|
+
reason,
|
|
51
|
+
locked: gate,
|
|
52
|
+
},
|
|
53
|
+
text: [resolved.label, "", ...lockNoteLines(note), "", LOCKED_VIEW_NOTE].join("\n"),
|
|
54
|
+
tables: { tables: [], footer: reportFooterLines({ incomplete: reason, notes: [LOCKED_VIEW_NOTE] }) },
|
|
55
|
+
};
|
|
56
|
+
}
|