gloomberb 0.17.2 → 0.17.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/futures-curve.ts +16 -0
  3. package/src/api-client/index.ts +18 -8
  4. package/src/api-client/types.ts +3 -1
  5. package/src/cli/commands/market.ts +60 -9
  6. package/src/cli/commands/search.ts +43 -2
  7. package/src/cli/commands/ticker.ts +100 -21
  8. package/src/cli/desktop-pane-shot.ts +34 -9
  9. package/src/cli/fx-pair.ts +17 -0
  10. package/src/cli/glossary.ts +134 -6
  11. package/src/cli/history-rows.ts +16 -4
  12. package/src/cli/index.ts +44 -12
  13. package/src/cli/known-gaps.ts +21 -2
  14. package/src/cli/listing-arg.ts +19 -4
  15. package/src/cli/listing-suggestion.ts +33 -2
  16. package/src/cli/pane-functions/dom.ts +39 -6
  17. package/src/cli/pane-functions/freshness.ts +48 -5
  18. package/src/cli/pane-functions/function-help.ts +1 -1
  19. package/src/cli/pane-functions/headless.ts +85 -19
  20. package/src/cli/pane-functions/index.ts +60 -4
  21. package/src/cli/pane-functions/market-session.ts +69 -6
  22. package/src/cli/pane-functions/resolver.ts +4 -2
  23. package/src/cli/pane-functions/screenshot.ts +14 -1
  24. package/src/cli/scroll-truncation.ts +5 -1
  25. package/src/cli/symbol-args.ts +41 -3
  26. package/src/cli/symbol-list.ts +19 -7
  27. package/src/cli/unknown-command.ts +70 -0
  28. package/src/components/chart/native/surface/visibility.ts +37 -7
  29. package/src/components/layout/header.tsx +348 -102
  30. package/src/components/markdown-text.tsx +40 -66
  31. package/src/components/market-board/index.tsx +12 -6
  32. package/src/components/reader/detail-reader.tsx +28 -6
  33. package/src/components/ticker/badge/index.tsx +2 -0
  34. package/src/components/ui/display.tsx +6 -1
  35. package/src/components/ui/external-link.tsx +10 -2
  36. package/src/data/memory-resource-store.ts +1 -1
  37. package/src/data/resource-store.ts +13 -1
  38. package/src/docs/snapshot.json +13 -13
  39. package/src/i18n/es.ts +1 -0
  40. package/src/i18n/ja.ts +1 -0
  41. package/src/i18n/ko.ts +1 -0
  42. package/src/i18n/zh-cn.ts +1 -0
  43. package/src/i18n/zh-tw.ts +1 -0
  44. package/src/market-data/futures-contract.ts +10 -0
  45. package/src/market-data/market/format.ts +7 -2
  46. package/src/market-data/market/freshness.ts +25 -1
  47. package/src/market-data/quotes/regular-session-valuation.ts +32 -1
  48. package/src/market-data/quotes/resolution.ts +2 -0
  49. package/src/plugins/builtin/account-management/pane.tsx +17 -13
  50. package/src/plugins/builtin/attention/headless.ts +19 -10
  51. package/src/plugins/builtin/attention/model.ts +22 -0
  52. package/src/plugins/builtin/bond-calculator/pane.tsx +2 -1
  53. package/src/plugins/builtin/changelog/index.tsx +11 -8
  54. package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +2 -0
  55. package/src/plugins/builtin/chat/attachments/desktop.tsx +2 -1
  56. package/src/plugins/builtin/chat/attachments/model.ts +5 -3
  57. package/src/plugins/builtin/chat/content/footer.tsx +6 -0
  58. package/src/plugins/builtin/chat/content/open-targets.ts +13 -4
  59. package/src/plugins/builtin/chat/content/transcript.tsx +2 -0
  60. package/src/plugins/builtin/chat/content/whole-message-copy.ts +70 -0
  61. package/src/plugins/builtin/chat/layout.ts +91 -31
  62. package/src/plugins/builtin/chat/markdown.ts +94 -0
  63. package/src/plugins/builtin/chat/message/desktop.tsx +3 -2
  64. package/src/plugins/builtin/chat/message/inline-tokens.tsx +197 -80
  65. package/src/plugins/builtin/chat/message/render-state.ts +3 -0
  66. package/src/plugins/builtin/chat/message/terminal.tsx +4 -4
  67. package/src/plugins/builtin/cloud/thesis/exposure.ts +18 -2
  68. package/src/plugins/builtin/company-attention/headless.ts +8 -5
  69. package/src/plugins/builtin/company-attention/model.ts +53 -2
  70. package/src/plugins/builtin/company-attention/pane.tsx +1 -1
  71. package/src/plugins/builtin/congress-trades/detail.tsx +7 -4
  72. package/src/plugins/builtin/congress-trades/pane.tsx +3 -1
  73. package/src/plugins/builtin/correlation/relationship/pane.tsx +3 -7
  74. package/src/plugins/builtin/cot/client.ts +12 -4
  75. package/src/plugins/builtin/cot/pane.tsx +2 -2
  76. package/src/plugins/builtin/crypto-board/model.ts +7 -3
  77. package/src/plugins/builtin/debt-maturities/pane.tsx +1 -6
  78. package/src/plugins/builtin/debug/index.tsx +15 -15
  79. package/src/plugins/builtin/dividend-yield/client.ts +8 -1
  80. package/src/plugins/builtin/dividend-yield/headless.ts +4 -1
  81. package/src/plugins/builtin/dividend-yield/pane.tsx +6 -5
  82. package/src/plugins/builtin/docs/index.tsx +5 -3
  83. package/src/plugins/builtin/docs/pane.tsx +18 -1
  84. package/src/plugins/builtin/docs/targets.ts +5 -3
  85. package/src/plugins/builtin/earnings-calls/data.ts +22 -0
  86. package/src/plugins/builtin/earnings-calls/pane.tsx +24 -14
  87. package/src/plugins/builtin/equity-screener/headless.ts +4 -3
  88. package/src/plugins/builtin/equity-screener/live.ts +27 -1
  89. package/src/plugins/builtin/equity-screener/pane.tsx +21 -2
  90. package/src/plugins/builtin/fear-greed/charts.tsx +5 -1
  91. package/src/plugins/builtin/fear-greed/pane.tsx +1 -1
  92. package/src/plugins/builtin/futures/index.tsx +32 -4
  93. package/src/plugins/builtin/futures/model.ts +65 -3
  94. package/src/plugins/builtin/futures/table.tsx +31 -11
  95. package/src/plugins/builtin/futures/use-board-terms.ts +89 -0
  96. package/src/plugins/builtin/holders/pane.tsx +6 -1
  97. package/src/plugins/builtin/mna/client.ts +12 -2
  98. package/src/plugins/builtin/mna/detail.tsx +2 -2
  99. package/src/plugins/builtin/mna/pane.tsx +2 -2
  100. package/src/plugins/builtin/notes/owner.tsx +5 -4
  101. package/src/plugins/builtin/notes/ticker-notes-tab.tsx +2 -3
  102. package/src/plugins/builtin/options-calculator/pane.tsx +2 -3
  103. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
  104. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -3
  105. package/src/plugins/builtin/portfolio-list/cli/shared.ts +9 -0
  106. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +6 -3
  107. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +2 -9
  108. package/src/plugins/builtin/research/event-model.ts +14 -0
  109. package/src/plugins/builtin/research/headless-schema.ts +31 -1
  110. package/src/plugins/builtin/research/relative-valuation-columns.ts +289 -0
  111. package/src/plugins/builtin/research/relative-valuation-currency.ts +164 -0
  112. package/src/plugins/builtin/research/relative-valuation-headless.ts +94 -11
  113. package/src/plugins/builtin/research/relative-valuation-model.ts +4 -1
  114. package/src/plugins/builtin/research/relative-valuation-pane.tsx +13 -4
  115. package/src/plugins/builtin/research/relative-valuation-peers.ts +302 -0
  116. package/src/plugins/builtin/research-search/data.ts +11 -0
  117. package/src/plugins/builtin/research-search/pane.tsx +13 -2
  118. package/src/plugins/builtin/scanner/hilo-pane.tsx +7 -17
  119. package/src/plugins/builtin/seasonality/client.ts +23 -0
  120. package/src/plugins/builtin/seasonality/pane.tsx +4 -2
  121. package/src/plugins/builtin/sectors/index.tsx +7 -3
  122. package/src/plugins/builtin/shared/report-freshness.ts +6 -2
  123. package/src/plugins/builtin/short-interest/headless.ts +13 -3
  124. package/src/plugins/builtin/short-interest/index.tsx +1 -1
  125. package/src/plugins/builtin/short-interest/model.ts +62 -3
  126. package/src/plugins/builtin/short-interest/pane.tsx +2 -2
  127. package/src/plugins/builtin/short-interest/schedule.ts +107 -0
  128. package/src/plugins/builtin/short-interest/watch-client.ts +54 -8
  129. package/src/plugins/builtin/short-interest/watch-headless.ts +8 -3
  130. package/src/plugins/builtin/short-interest/watch-model.ts +52 -23
  131. package/src/plugins/builtin/short-interest/watch-pane.tsx +1 -1
  132. package/src/plugins/builtin/supply-chain/flow.tsx +14 -4
  133. package/src/plugins/builtin/themes/headless.ts +1 -1
  134. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +23 -27
  135. package/src/plugins/builtin/ticker-detail/index.tsx +1 -1
  136. package/src/plugins/builtin/ticker-detail/overview/model.ts +4 -3
  137. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +4 -4
  138. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +9 -4
  139. package/src/plugins/builtin/vol-surface/pane.tsx +16 -1
  140. package/src/plugins/builtin/volatility/chart-model.ts +1 -1
  141. package/src/plugins/builtin/volatility/charts.tsx +1 -1
  142. package/src/renderers/dom/styles.css +6 -0
  143. package/src/sources/gloomberb-cloud/normalizers.ts +1 -0
  144. package/src/sources/provider-router/batches.ts +2 -1
  145. package/src/sources/provider-router/cache.ts +3 -0
  146. package/src/sync/baseline.ts +19 -14
  147. package/src/sync/controller.ts +140 -40
  148. package/src/sync/core-contributors.ts +256 -32
  149. package/src/sync/types.ts +44 -0
  150. package/src/time-series/resolve.ts +19 -2
  151. package/src/types/data-provider.ts +8 -0
  152. package/src/types/financials.ts +40 -4
  153. package/src/types/headless.ts +43 -0
  154. package/src/types/plugin.ts +7 -0
  155. package/src/utils/declared-dividends.ts +39 -0
  156. package/src/utils/forward-dividend.ts +34 -11
  157. package/src/utils/fundamentals.ts +13 -2
  158. package/src/utils/markdown.ts +180 -0
  159. package/src/utils/market-capitalization.ts +22 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gloomberb",
3
- "version": "0.17.2",
3
+ "version": "0.17.3",
4
4
  "packageManager": "bun@1.3.11",
5
5
  "description": "Market research and portfolio tracker for the terminal",
6
6
  "module": "src/public/index.ts",
@@ -30,6 +30,22 @@ export interface FuturesContractTermsPayload {
30
30
  inDelivery: boolean
31
31
  /** The newest exchange settlement confirmed for the contract, with its session. */
32
32
  settlement: { price: number; date: string } | null
33
+ /**
34
+ * For a continuous symbol (CL=F) or a front generic: the nearest contract
35
+ * still trading when the symbol has rolled past it to the most active one,
36
+ * with its last trade; null when it prices the nearest itself. Absent from
37
+ * servers that do not send it.
38
+ */
39
+ nearest?: FuturesNearestContract | null
40
+ }
41
+
42
+ /** The nearest contract still trading, which a continuous symbol has rolled past (CLX26 under CL=F on Dec). */
43
+ interface FuturesNearestContract {
44
+ contract: string
45
+ contractSymbol: string
46
+ /** YYYY-MM */
47
+ deliveryMonth: string
48
+ lastTrade: string | null
33
49
  }
34
50
 
35
51
  /** Gloom Cloud futures prices retain provider denomination and explicit quote units. */
@@ -12,7 +12,7 @@ import {
12
12
  parseCloudLayoutEntry,
13
13
  parseCloudLayoutList,
14
14
  } from "../layout-marketplace/cloud";
15
- import type { SyncSnapshot } from "../sync/types";
15
+ import { SyncRevisionConflictError, type SyncSnapshot } from "../sync/types";
16
16
  import { withDeadline } from "../utils/async-deadline";
17
17
  import { CloudASKGApi } from "./askg";
18
18
  import { CloudAuthApi } from "./auth";
@@ -450,14 +450,24 @@ class GloomApiClient {
450
450
  return this.request<CloudSyncSnapshotResponse>("/sync/snapshot", { method: "GET" });
451
451
  }
452
452
 
453
+ /**
454
+ * `baseRevision` goes out as If-Match: the server ignores the body field and
455
+ * would replace a snapshot another device wrote since this one pulled. With
456
+ * the header it refuses that write (409), raised as SyncRevisionConflictError
457
+ * so the controller pulls again instead.
458
+ */
453
459
  async putSyncSnapshot(snapshot: SyncSnapshot, options?: { baseRevision?: number | null }): Promise<CloudSyncPushResponse> {
454
- return this.request<CloudSyncPushResponse>("/sync/snapshot", {
455
- method: "PUT",
456
- body: JSON.stringify({
457
- snapshot,
458
- baseRevision: options?.baseRevision ?? null,
459
- }),
460
- });
460
+ const baseRevision = options?.baseRevision ?? null;
461
+ try {
462
+ return await this.request<CloudSyncPushResponse>("/sync/snapshot", {
463
+ method: "PUT",
464
+ ...(baseRevision !== null ? { headers: { "If-Match": String(baseRevision) } } : {}),
465
+ body: JSON.stringify({ snapshot, baseRevision }),
466
+ });
467
+ } catch (error) {
468
+ if (error instanceof ApiRequestError && error.status === 409) throw new SyncRevisionConflictError(error.message);
469
+ throw error;
470
+ }
461
471
  }
462
472
 
463
473
  async getMarketplaceLayout(
@@ -459,10 +459,12 @@ export interface CloudPricePointPayload {
459
459
  low?: number;
460
460
  close: number;
461
461
  volume?: number;
462
- /** A coin's daily bar for the UTC day still forming when served; it has no volume yet. */
462
+ /** A daily bar still forming when served: a coin's UTC day, or the trade date a future is trading. */
463
463
  partial?: boolean;
464
464
  /** A coin's daily bar whose prices were rebuilt from hourly bars; a day the source left empty has no volume. */
465
465
  filled?: boolean;
466
+ /** A future's closed session whose settlement is not confirmed yet: it closes on the last trade. */
467
+ unsettled?: boolean;
466
468
  }
467
469
 
468
470
  type CloudEconImpact = "high" | "medium" | "low";
@@ -24,6 +24,7 @@ import { withCliServices, withMarketData } from "../context";
24
24
  import { isoDate, parsePositiveInt, requireOneArg, takeFlag, takeOption } from "./command-utils";
25
25
  import type { BuiltinCliCommandDef } from "../command-options";
26
26
  import { CLI_COMMAND_GROUPS, TABLE_SECTION_OPTION } from "../help";
27
+ import { CLI_EXIT_CODES } from "../exit-codes";
27
28
  import {
28
29
  formatChangePercentCell,
29
30
  formatCountCell,
@@ -44,7 +45,9 @@ import {
44
45
  import { getPublishedUsEquityCalendarYears, getPublishedUsEquitySession } from "../../market-data/published-us-sessions";
45
46
  import { isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshness";
46
47
  import { isOptionsUnavailableError, listedOptionsAlternative, optionsUnavailableTitle } from "../../market-data/options-alternatives";
47
- import { cliFuturesContract, futuresContractMetrics, fundamentalsReportTables, renderFundamentalsReport } from "./ticker";
48
+ import {
49
+ cliFuturesContract, futuresContractMetrics, fundamentalsReportTables, loadForwardYieldDeclarations, renderFundamentalsReport,
50
+ } from "./ticker";
48
51
  import type { FuturesContractTermsPayload } from "../../api-client/futures-curve";
49
52
  import { futuresSettleText, isFuturesInstrument } from "../../market-data/futures-contract";
50
53
  import {
@@ -65,6 +68,7 @@ import {
65
68
  type OptionsChainBasis,
66
69
  } from "./options-chain";
67
70
  import { missingExpiryMessage } from "../../utils/option-expiry";
71
+ import { declaredDividends } from "../../utils/declared-dividends";
68
72
  import {
69
73
  DEFAULT_LEAPS_CRITERIA, LEAPS_EXTRA_COLUMNS, LEAPS_USAGE, parseLeapsCriteria, runLeapsScreen, takeLeapsOptions,
70
74
  } from "./options-leaps";
@@ -143,14 +147,14 @@ import { earningsCalendarFailure, earningsChoiceNote, earningsDateLabel, isListi
143
147
  import { formatBasisPoints, toBasisPoints } from "../../utils/basis-points";
144
148
  import { notATickerSymbol, providerMissReason } from "../../sources/provider-errors";
145
149
  import { isNotATickerMessage, withListingSuggestion } from "../not-a-ticker";
146
- import { describeSuggestion, suggestListing } from "../listing-suggestion";
150
+ import { describeSuggestion, suggestListing, suggestUsTicker, type ListingSuggestion } from "../listing-suggestion";
147
151
  import { splitSymbolArgs, uniqueSecuritySymbols } from "../symbol-args";
148
152
  import { cliPerpetualContract, perpetualContractStats, type PerpetualContractPayload } from "../perpetual-contract";
149
153
  import { subMinuteDataMessage } from "../known-gaps";
150
154
  import { secRegistrantMismatchMessage, SecRegistrantMismatchError, areDifferentCompanies } from "../../sources/sec-registrant";
151
155
  import { isCryptoPairSymbol } from "../../utils/crypto-pair";
152
156
  import { looksLikeCurrencyPair } from "../../utils/currency-pair";
153
- import { isUsListingExchange } from "../../utils/exchanges";
157
+ import { canonicalExchange, isUsListingExchange } from "../../utils/exchanges";
154
158
  import { describeTickerChange, startTickerChangeLookup, tickerChangeLine, type TickerChange } from "../../tickers/ticker-changes";
155
159
  import { noSecReportsNotice, nonUsSecListingVenue } from "../../utils/sec";
156
160
  import type { MarketContext } from "../types";
@@ -208,6 +212,8 @@ type QuoteCliRecord = Omit<QuoteBatchResult, "error"> & {
208
212
  perpetualContract?: PerpetualContractPayload;
209
213
  /** The symbol changed: its issuer moved to another symbol, or its holder had another before. */
210
214
  tickerChange?: TickerChange;
215
+ /** A bare symbol read on a listing abroad that is one slip from a US ticker: the US ticker it was most likely typed for. */
216
+ usSuggestion?: ListingSuggestion;
211
217
  };
212
218
  type FinancialsCliData = TickerFinancials & {
213
219
  symbol: string;
@@ -504,6 +510,13 @@ function corporateActionRows(data: CorporateActionsData) {
504
510
  date: event.exDate,
505
511
  detail: currency ? formatDistributionAmount(event.amount, currency) : cleanDecimal(event.amount),
506
512
  })),
513
+ // Declared and unpaid, at its ex-date beside the paid rows, never one of them.
514
+ ...declaredDividends(data).map((event) => ({
515
+ type: "dividend",
516
+ date: event.exDate,
517
+ detail: `declared ${currency ? formatDistributionAmount(event.amount, currency) : cleanDecimal(event.amount)}`
518
+ + (event.payDate ? `, payable ${event.payDate}` : ""),
519
+ })),
507
520
  ...data.splits.map((event) => ({
508
521
  type: "split",
509
522
  date: event.date,
@@ -568,7 +581,9 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
568
581
  // So is whether a symbol changed (FB is now META), for the same lists.
569
582
  const changes = new Map(listings.length > MAX_CHOICE_LOOKUPS ? []
570
583
  : listings.map((listing) => [listing, startTickerChangeLookup(market.dataProvider, listing.symbol, listing.exchange)] as const));
571
- const results = await market.dataProvider.getQuotesBatch(targets, { forceRefresh: ctx.cliOptions.refresh });
584
+ // The service's quote of the moment, not each form's local copy from whenever it was last asked for:
585
+ // BRK.B saved on NYSE, BRK-B and BRK.B:NYSE print one quote (GLO-735). A local copy answers only offline.
586
+ const results = await market.dataProvider.getQuotesBatch(targets, { forceRefresh: ctx.cliOptions.refresh, revalidate: true });
572
587
  const listingOf = (result: QuoteBatchResult, index: number) => listings[targets.indexOf(result.target)] ?? listings[index]!;
573
588
  const asked = results.map(listingOf);
574
589
  const request = { command: commandName, noun: "quote" };
@@ -595,6 +610,11 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
595
610
  const perpetualContracts = await Promise.all(answers.map(({ result }) => commandName === "quote" && result.quote
596
611
  ? cliPerpetualContract(result.quote)
597
612
  : null));
613
+ // A bare symbol quoted on a listing abroad (APPL, Aptus Pharma on BSE) that is one slip from a US ticker (AAPL) says so.
614
+ const usSuggestions = await Promise.all(answers.map(({ result, listing, notes }) => {
615
+ const venue = result.quote && !listing.saved ? bareListingNote(listing, result.quote) : null;
616
+ return venue && notes.includes(venue) ? suggestUsTicker(listing.symbol, market) : null;
617
+ }));
598
618
  // Each row names its listing by key (SAN:EPA) and company, so the table needs no line above it.
599
619
  const data = answers.map(({ result, listing, failure }, index) => ({
600
620
  target: result.target,
@@ -604,6 +624,7 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
604
624
  quote: result.quote,
605
625
  ...contracts[index] ? { futuresContract: contracts[index] } : {},
606
626
  ...perpetualContracts[index] ? { perpetualContract: perpetualContracts[index] } : {},
627
+ ...usSuggestions[index] ? { usSuggestion: usSuggestions[index] } : {},
607
628
  error: failure?.oneLine ?? errorMessage(result.error),
608
629
  ...yieldIndexFields(result),
609
630
  ...tickerChanges[index] ? { tickerChange: tickerChanges[index] } : {},
@@ -630,15 +651,38 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
630
651
  freshness: quotesFreshness(data.map((row) => row.quote)),
631
652
  }, {
632
653
  rows: quoteRows,
633
- details: quoteFuturesContractText,
654
+ details: quoteDetailsText,
634
655
  columns,
635
656
  ...(yieldsOnly ? { textColumns: columns.map((column) => column.key === "changePercent" ? { ...column, header: "Chg" } : column) } : {}),
636
657
  });
658
+ // The table names each symbol it found nothing for; with none found, the exit status says so too.
659
+ if (data.every((row) => !row.quote)) process.exitCode = CLI_EXIT_CODES.error;
637
660
  });
638
661
  }
639
662
 
640
663
  /**
641
- * Under the table, each futures row's Settle line and Contract block, worded
664
+ * Under the table: each futures row's Settle line and Contract block, then the
665
+ * line for a symbol that is not a US ticker though it is one slip from one.
666
+ */
667
+ function quoteDetailsText(data: QuoteCliRecord[]): string {
668
+ return [quoteFuturesContractText(data), ...data.flatMap((row) => notUsTickerLine(row) ?? [])].filter(Boolean).join("\n\n");
669
+ }
670
+
671
+ /**
672
+ * "APPL is not a US ticker: it is Aptus Pharma Limited on BSE (INR). Did you
673
+ * mean AAPL (Apple Inc.)?", for a symbol read on a listing abroad that is one
674
+ * slip from a US ticker, worded as the not-a-ticker answers word it.
675
+ */
676
+ function notUsTickerLine({ target, quote, listing, usSuggestion }: QuoteCliRecord): string | null {
677
+ if (!quote || !usSuggestion) return null;
678
+ const venue = canonicalExchange(quote.listingExchangeName || quote.exchangeName);
679
+ const name = quote.name || listing?.name;
680
+ const what = `${name ? `${name} on ` : ""}${venue}${quote.currency ? ` (${quote.currency})` : ""}`;
681
+ return cliStyles.warning(`${target.symbol} is not a US ticker: it is ${what}. Did you mean ${describeSuggestion(usSuggestion)}?`);
682
+ }
683
+
684
+ /**
685
+ * Each futures row's Settle line and Contract block, worded
642
686
  * as `ticker` words them. Several symbols name theirs in the block's title.
643
687
  */
644
688
  function quoteFuturesContractText(data: QuoteCliRecord[]): string {
@@ -842,16 +886,18 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
842
886
  const flagColumn: CliResultColumn = {
843
887
  key: "flag", header: "Flag", format: (value) => typeof value === "string" ? cliStyles.warning(value) : "",
844
888
  };
845
- // Text also marks a bar still forming or rebuilt from hourly bars there; exports keep them as their own fields.
889
+ // Text also marks a bar still forming, rebuilt from hourly bars or closing before its settlement; exports keep them as fields.
846
890
  const flagTextColumn: CliResultColumn = {
847
891
  ...flagColumn,
848
892
  format: (value, row) => [
849
893
  typeof value === "string" ? cliStyles.warning(value) : "",
850
894
  row.partial ? cliStyles.muted("partial") : "",
851
895
  row.filled ? cliStyles.muted("filled") : "",
896
+ row.unsettled ? cliStyles.muted("unsettled") : "",
852
897
  ].filter(Boolean).join(" "),
853
898
  };
854
899
  const marked = data.some((row) => row.partial || row.filled);
900
+ const unsettled = data.some((row) => row.unsettled);
855
901
  ctx.printResult({
856
902
  data,
857
903
  metadata: {
@@ -880,6 +926,7 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
880
926
  ...priceColumns,
881
927
  flagColumn,
882
928
  ...marked ? [{ key: "partial", header: "Partial" }, { key: "filled", header: "Filled" }] : [],
929
+ ...unsettled ? [{ key: "unsettled", header: "Unsettled" }] : [],
883
930
  ...volume ? [{ key: "volumeUnit", header: "Volume unit" }, { key: "volumeNote", header: "Volume note" }] : [],
884
931
  ],
885
932
  // Every exported row says what it is a bar of, the currency a price is in and its bar size, so it survives head and concatenation.
@@ -937,6 +984,10 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
937
984
  // Market cap, the multiples and the yield at the regular session price, as the text gives them; the quote stays as sent.
938
985
  const priced = atRegularSessionPrice(financials);
939
986
  const capitalization = selectMarketCapitalization(priced.quote, priced.fundamentals);
987
+ // A forward yield dated ahead names the declared distribution it annualizes, in the text and tables.
988
+ const report = ctx.cliOptions.format === "json" ? data : {
989
+ ...data, declaredDividends: await loadForwardYieldDeclarations(market.dataProvider, listing.request, financials.fundamentals),
990
+ };
940
991
  ctx.printResult({
941
992
  // JSON reads a multiple over a loss as null with its reason, never as a number.
942
993
  data: {
@@ -951,8 +1002,8 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
951
1002
  freshness,
952
1003
  ...(choice.length > 0 ? { warnings: choice } : {}),
953
1004
  }, {
954
- text: () => renderFundamentalsReport(data, view),
955
- ...(tabular ? { tables: selectTables(fundamentalsReportTables(data, view, freshness)) } : {}),
1005
+ text: () => renderFundamentalsReport(report, view),
1006
+ ...(tabular ? { tables: selectTables(fundamentalsReportTables(report, view, freshness)) } : {}),
956
1007
  exportIdentity: { symbolOrSeries: listing.key },
957
1008
  });
958
1009
  return;
@@ -26,7 +26,7 @@ import { indexRootFor, indexRootTryLine } from "../index-roots";
26
26
  import { knownGapMessage, settingsPointer } from "../known-gaps";
27
27
  import { instrumentTypeLabel } from "../../tickers/instrument-kind";
28
28
  import { canonicalExchange, isUsListingExchange } from "../../utils/exchanges";
29
- import { groupByCompany } from "../../tickers/company-name";
29
+ import { groupByCompany, sameCompany } from "../../tickers/company-name";
30
30
  import { findCatalogSeries, renderSeriesMatches, type SeriesMatch } from "../series-search";
31
31
  import { centralBankCiting, centralBankNamed, type CentralBank } from "../../utils/central-banks";
32
32
  import { createPaneCatalog } from "../pane-functions/discovery";
@@ -120,7 +120,7 @@ export async function searchCandidatesForCli({
120
120
  const quote = BARE_SYMBOL.test(symbol) && candidates.some((candidate) => candidate.symbol.toUpperCase() === symbol)
121
121
  ? await (quoted ?? loadSearchedQuote(symbol, savedExchange, dataProvider))
122
122
  : null;
123
- return withQuotedNames(symbol, withPrimaryListingFirst(symbol, candidates, quote), quote).slice(0, totalLimit);
123
+ return withQuotedNames(symbol, withHomeListingPrimary(symbol, withPrimaryListingFirst(symbol, candidates, quote)), quote).slice(0, totalLimit);
124
124
  } catch {
125
125
  return [];
126
126
  }
@@ -217,6 +217,47 @@ function withPrimaryListingFirst(
217
217
  ));
218
218
  }
219
219
 
220
+ /** Venues that only quote shares listed elsewhere: the OTC tiers, Frankfurt's open market and Germany's regional exchanges. */
221
+ const SECONDARY_VENUES = new Set(["OTC", "PNK", "OTCQX", "OTCQB", "FWB2", "MUNICH", "XSTU", "XDUS", "XBER", "XHAM", "HANOVER"]);
222
+
223
+ /**
224
+ * How far a listing is from being its company's own line: 0 a plain one, 1 a
225
+ * line that only quotes shares listed elsewhere (a regional venue, OTC, a
226
+ * Milan 4MSFT or London 0XXX code), 2 a depositary receipt.
227
+ */
228
+ function listingDistance(candidate: TickerSearchCandidate): 0 | 1 | 2 {
229
+ if (/depositary|^(?:ADR|ADS|GDR|CDR|BDR)$/i.test(candidate.instrumentType ?? "")) return 2;
230
+ return SECONDARY_VENUES.has(candidateExchange(candidate)) || /\d/.test(candidate.symbol) && /[A-Za-z]/.test(candidate.symbol) ? 1 : 0;
231
+ }
232
+
233
+ /**
234
+ * A company searched by name lists its home line as the primary listing. A
235
+ * search row names no home exchange or volume, so a company's lines go by
236
+ * how far each is from being its own line (`listingDistance`), then by the
237
+ * search score the service gives the listings people look up most. The
238
+ * listing search put first, often the one-off cross-listing whose short name
239
+ * matched the query (LUFTHANSA on Budapest), gives way only to a closer line,
240
+ * or to one searched at least twice as often, or any scored line when it has
241
+ * no score. A listing searched by its own symbol is not moved.
242
+ */
243
+ function withHomeListingPrimary(symbol: string, candidates: TickerSearchCandidate[]): TickerSearchCandidate[] {
244
+ const primary = candidates.find((candidate) => candidate.category === "Primary Listing");
245
+ if (!primary || primary.kind !== "search" || primary.symbol.toUpperCase() === symbol) return candidates;
246
+ const company = resolveSearchName(primary);
247
+ const lines = candidates.filter((candidate) => candidate.kind === "search" && candidate.category !== "Funds & Derivatives"
248
+ && (candidate.instrumentClass === "equity" || candidate.instrumentClass === "other") && sameCompany(resolveSearchName(candidate), company));
249
+ const score = (candidate: TickerSearchCandidate) => candidate.result?.popularity ?? candidate.popularity ?? null;
250
+ const [home] = [...lines].sort((a, b) => listingDistance(a) - listingDistance(b) || (score(b) ?? -1) - (score(a) ?? -1) || candidates.indexOf(a) - candidates.indexOf(b));
251
+ if (!home || home === primary) return candidates;
252
+ const closer = listingDistance(home) < listingDistance(primary);
253
+ const [homeScore, primaryScore] = [score(home), score(primary)];
254
+ const moreSearched = listingDistance(home) === listingDistance(primary) && homeScore != null && (primaryScore == null || homeScore >= primaryScore * 2);
255
+ if (!closer && !moreSearched) return candidates;
256
+ const rest = candidates.filter((candidate) => candidate !== home);
257
+ const at = rest.indexOf(primary);
258
+ return [...rest.slice(0, at), { ...home, category: "Primary Listing" as const }, { ...primary, category: "Other Listings" as const }, ...rest.slice(at + 1)];
259
+ }
260
+
220
261
  /** The candidate on the venue the quote prices; the US exchanges share one symbol, so a quote on any of them is its one US line. */
221
262
  function quotedCandidate(candidates: readonly TickerSearchCandidate[], quote: Quote | null): TickerSearchCandidate | undefined {
222
263
  const quoted = canonicalExchange(quote?.listingExchangeName || quote?.exchangeName);
@@ -3,7 +3,7 @@ import { formatReportedMoney } from "../../utils/reported-money";
3
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
- import { describeFundamentalMarketCap, selectMarketCapitalization } from "../../utils/market-capitalization";
6
+ import { describeFundamentalMarketCap, marketCapShareBasisNote, selectMarketCapitalization } from "../../utils/market-capitalization";
7
7
  import {
8
8
  formatCompact,
9
9
  formatCurrency,
@@ -32,9 +32,9 @@ import {
32
32
  import type { FuturesContractTermsPayload } from "../../api-client/futures-curve";
33
33
  import { futuresContractLines, futuresSettleText, isFuturesInstrument, loadFuturesContractTerms } from "../../market-data/futures-contract";
34
34
  import type { AppConfig } from "../../types/config";
35
- import type { FinancialStatement, Quote, TickerFinancials } from "../../types/financials";
35
+ import type { DeclaredDividendAction, FinancialStatement, Quote, TickerFinancials } from "../../types/financials";
36
36
  import { quoteTradingDay } from "../../market-data/quotes/day-range";
37
- import { atRegularSessionPrice } from "../../market-data/quotes/regular-session-valuation";
37
+ import { atRegularSessionPrice, regularSessionValuationPrice } from "../../market-data/quotes/regular-session-valuation";
38
38
  import { computeTickerPriceReturns } from "../../market-data/ticker-price-returns";
39
39
  import { instrumentTypeLabel, isCryptoVenuePerpetual, resolveTickerInstrumentKind } from "../../tickers/instrument-kind";
40
40
  import type { SecFilingItem } from "../../types/data-provider";
@@ -69,6 +69,7 @@ import { exportEntriesTable, reportFooterLines, type CliReportTables } from "../
69
69
  import type { ReportFreshness } from "../pane-functions/freshness";
70
70
  import { fundamentalsFreshness, quotesFreshness } from "../freshness";
71
71
  import { forwardDividendBasis } from "../../utils/forward-dividend";
72
+ import { declaredDividends } from "../../utils/declared-dividends";
72
73
  import { notTheCoinNote } from "./crypto-hints";
73
74
 
74
75
  const NEWS_ITEM_LIMIT = 5;
@@ -347,11 +348,38 @@ function percentFigure(fraction: number | null | undefined, format: (value: numb
347
348
  return fraction != null ? { text: format(fraction), value: fraction * 100, unit: "%" } : { text: "—" };
348
349
  }
349
350
 
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. */
351
- function forwardYieldFigure(fundamentals: TickerFinancials["fundamentals"]): MetricFigure {
351
+ /** The declared, unpaid distributions a forward yield dated ahead is named from, in their own currency. */
352
+ export interface ForwardYieldDeclarations {
353
+ rows: DeclaredDividendAction[];
354
+ currency?: string;
355
+ }
356
+
357
+ /**
358
+ * The listing's declared distributions, read only for a forward yield whose
359
+ * ex-date is still ahead (the one case its basis names one); null otherwise,
360
+ * and when they cannot be read, which leaves the basis to its dates.
361
+ */
362
+ export async function loadForwardYieldDeclarations(
363
+ dataProvider: Pick<MarketContext["dataProvider"], "getCorporateActions">,
364
+ listing: { symbol: string; exchange: string },
365
+ fundamentals: TickerFinancials["fundamentals"],
366
+ now = new Date(),
367
+ ): Promise<ForwardYieldDeclarations | null> {
368
+ const exDate = fundamentals?.dividendYieldBasis === "forward" ? fundamentals.exDividendDate ?? "" : "";
369
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(exDate) || exDate <= now.toISOString().slice(0, 10) || !dataProvider.getCorporateActions) return null;
370
+ try {
371
+ const actions = await dataProvider.getCorporateActions(listing.symbol, listing.exchange);
372
+ return { rows: declaredDividends(actions, now), ...(actions.currency ? { currency: actions.currency } : {}) };
373
+ } catch {
374
+ return null;
375
+ }
376
+ }
377
+
378
+ /** A forward yield says what it annualizes and when, as DVD does (a declared distribution by its ex-date and stated amount); any other yield is the figure alone. */
379
+ function forwardYieldFigure(fundamentals: TickerFinancials["fundamentals"], declared: ForwardYieldDeclarations | null): MetricFigure {
352
380
  const figure = percentFigure(fundamentals?.dividendYield, formatFractionPercentCell);
353
381
  return fundamentals?.dividendYieldBasis === "forward" && fundamentals.dividendYield != null
354
- ? { ...figure, text: `${figure.text} (${forwardDividendBasis(fundamentals.exDividendDate)})` }
382
+ ? { ...figure, text: `${figure.text} (${forwardDividendBasis(fundamentals.exDividendDate, { declared: declared?.rows, currency: declared?.currency })})` }
355
383
  : figure;
356
384
  }
357
385
 
@@ -368,6 +396,7 @@ function fundamentalsMetrics(
368
396
  priceReturns: { return1Y?: number | null; return3Y?: number | null },
369
397
  reportingCurrency: string | undefined,
370
398
  enterpriseValue = reportedMoney(reportedEnterpriseValue(fundamentals), enterpriseValueCurrency(quote, fundamentals)),
399
+ declared: ForwardYieldDeclarations | null = null,
371
400
  ): FundamentalsMetric[] {
372
401
  const signed = (value: number) => colorBySign(formatPercent(value), value);
373
402
  const receipt = fundamentals?.sharesOutstanding != null && sharesOutstandingInReceipts(quote, fundamentals, profile?.description);
@@ -382,7 +411,7 @@ function fundamentalsMetrics(
382
411
  { label: "EPS (TTM)", ...reportedMoney(fundamentals?.eps, fundamentals?.epsCurrency?.trim() || reportingCurrency, true) },
383
412
  {
384
413
  label: `Dividend Yield${fundamentals?.dividendYieldBasis ? ` (${fundamentals.dividendYieldBasis})` : ""}`,
385
- ...forwardYieldFigure(fundamentals),
414
+ ...forwardYieldFigure(fundamentals, declared),
386
415
  },
387
416
  { label: "Revenue (TTM)", ...reportedMoney(fundamentals?.revenue, reportingCurrency) },
388
417
  { label: "Net Income (TTM)", ...reportedMoney(fundamentals?.netIncome, reportingCurrency) },
@@ -404,6 +433,34 @@ function fundamentalsMetrics(
404
433
  ];
405
434
  }
406
435
 
436
+ /**
437
+ * The float and the short interest `fundamentals` prints after the share count: the shares short, short % of float
438
+ * and days to cover of one settlement, dated, as the JSON gives them and `SI` and `SIW` show them.
439
+ */
440
+ function floatAndShortInterestMetrics(
441
+ quote: TickerFinancials["quote"],
442
+ fundamentals: TickerFinancials["fundamentals"],
443
+ profile: TickerFinancials["profile"],
444
+ ): FundamentalsMetric[] {
445
+ const finite = (value: number | undefined): value is number => value != null && Number.isFinite(value);
446
+ const float = fundamentals?.floatShares;
447
+ const short = fundamentals?.sharesShort;
448
+ const days = fundamentals?.shortRatio;
449
+ const date = fundamentals?.shortInterestDate;
450
+ const receipt = finite(float) && sharesOutstandingInReceipts(quote, fundamentals, profile?.description);
451
+ return [
452
+ finite(float) && float > 0
453
+ ? { label: "Float", text: formatCompact(float), value: float, ...(receipt ? { unit: "ADR equivalent" } : {}) }
454
+ : { label: "Float", text: "—" },
455
+ finite(short)
456
+ ? { label: "Short Interest", text: `${formatCompact(short)}${date ? ` (settled ${date})` : ""}`, value: short, unit: "shares" }
457
+ : { label: "Short Interest", text: "—" },
458
+ ...finite(short) && date ? [{ label: "Short Interest Settlement", text: date, value: date, exportOnly: true }] : [],
459
+ { label: "Short % of Float", ...percentFigure(fundamentals?.shortPercentOfFloat, formatFractionPercentCell) },
460
+ finite(days) ? { label: "Days to Cover", text: formatNumber(days, 2), value: days } : { label: "Days to Cover", text: "—" },
461
+ ];
462
+ }
463
+
407
464
  function metricLines(metrics: readonly FundamentalsMetric[]): Array<[string, string]> {
408
465
  return metrics.filter((metric) => !metric.exportOnly).map((metric) => [metric.label, metric.text]);
409
466
  }
@@ -411,28 +468,35 @@ function metricLines(metrics: readonly FundamentalsMetric[]): Array<[string, str
411
468
  const TRAILING_MONEY_METRICS = new Set(["EPS (TTM)", "Revenue (TTM)", "Net Income (TTM)", "Operating Cash Flow (TTM)", "Free Cash Flow (TTM)"]);
412
469
 
413
470
  /**
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.
471
+ * The notes under shown fundamentals: a market cap that does not rest on the share count shown, how a labelled or
472
+ * withdrawn EV compares with market cap + debt - cash, what a converted TTM block was converted from, an EPS in
473
+ * another currency than the amounts beside it, and what a P/E on such an EPS divides. `priced` is the snapshot at
474
+ * the regular session price (atRegularSessionPrice), the figures the lines show, so each note reads those.
417
475
  */
418
476
  function fundamentalsNotes(
419
- fundamentals: TickerFinancials["fundamentals"],
477
+ priced: Pick<TickerFinancials, "quote" | "fundamentals">,
420
478
  metrics: readonly FundamentalsMetric[],
421
479
  quoteCurrency: string | undefined,
422
480
  ): string[] {
481
+ const fundamentals = priced.fundamentals;
423
482
  const shown = (label: string) => metrics.some((metric) => metric.label === label && metric.text !== "—");
483
+ const shareBasis = shown("Market Cap") && shown("Shares Outstanding")
484
+ ? marketCapShareBasisNote(selectMarketCapitalization(priced.quote, fundamentals), fundamentals?.sharesOutstanding,
485
+ regularSessionValuationPrice(priced.quote), priced.quote?.currency)
486
+ : undefined;
424
487
  const conversion = [...TRAILING_MONEY_METRICS].some(shown) ? ttmConversionNote(fundamentals) : undefined;
425
488
  const eps = shown("EPS (TTM)") ? epsConversionNote(fundamentals) : undefined;
426
489
  const priceEarnings = metrics.some((metric) => metric.label === "P/E (TTM)" && typeof metric.value === "number")
427
490
  ? convertedPriceEarningsNote(fundamentals, quoteCurrency)
428
491
  : undefined;
429
492
  const enterpriseValue = shown("Enterprise Value") ? enterpriseValueReconciliationNote(fundamentals) : undefined;
430
- return [enterpriseValue, conversion, eps, priceEarnings].filter((note): note is string => !!note);
493
+ return [shareBasis, enterpriseValue, conversion, eps, priceEarnings].filter((note): note is string => !!note);
431
494
  }
432
495
 
433
- const VALUATION_METRICS = new Set(["Market Cap", "Enterprise Value", "P/E (TTM)", "Forward P/E", "PEG", "EPS (TTM)"]);
496
+ // The share count is the market cap's basis: the cap is the count times the price, or a note says why not.
497
+ const VALUATION_METRICS = new Set(["Market Cap", "Shares Outstanding", "Enterprise Value", "P/E (TTM)", "Forward P/E", "PEG", "EPS (TTM)"]);
434
498
 
435
- type FundamentalsReportData = TickerFinancials & { symbol: string; exchange?: string };
499
+ type FundamentalsReportData = TickerFinancials & { symbol: string; exchange?: string; declaredDividends?: ForwardYieldDeclarations | null };
436
500
 
437
501
  /**
438
502
  * What a fund's report cannot show yet, and for a US listing where its SEC
@@ -502,9 +566,16 @@ function fundamentalsReportMetrics(snapshot: FundamentalsReportData, view: "fund
502
566
  const marketCap: MetricFigure = capitalization
503
567
  ? { text: `${formatCompact(capitalization.value)} ${capitalization.currency}`, value: capitalization.value, unit: capitalization.currency }
504
568
  : { text: "—" };
505
- const metrics = fundamentalsMetrics(quote, fundamentals, financials.profile, marketCap, computeTickerPriceReturns(financials), fundamentalsCurrency(financials));
506
- if (view === "fundamentals") return metrics.filter((metric) => metric.text !== "—");
507
- const valuation = metrics.filter(({ label, text }) => (VALUATION_METRICS.has(label) || label.startsWith("Dividend Yield")) && text !== "—");
569
+ const metrics = fundamentalsMetrics(quote, fundamentals, financials.profile, marketCap, computeTickerPriceReturns(financials), fundamentalsCurrency(financials),
570
+ undefined, snapshot.declaredDividends ?? null);
571
+ if (view === "fundamentals") {
572
+ return [...metrics, ...floatAndShortInterestMetrics(quote, fundamentals, financials.profile)]
573
+ .filter((metric) => metric.text !== "—");
574
+ }
575
+ const valued = metrics.filter(({ label, text }) => (VALUATION_METRICS.has(label) || label.startsWith("Dividend Yield")) && text !== "—");
576
+ // The share count follows the market cap it is the basis of.
577
+ const shares = valued.filter((metric) => metric.label === "Shares Outstanding");
578
+ const valuation = valued.flatMap((metric) => metric.label === "Shares Outstanding" ? [] : metric.label === "Market Cap" ? [metric, ...shares] : [metric]);
508
579
  // A listing with no valuation (a fund) says so rather than printing its price alone.
509
580
  return valuation.length === 0 ? [] : [...valuationPriceMetrics(financials).filter((metric) => metric.text !== "—"), ...valuation];
510
581
  }
@@ -532,7 +603,7 @@ export function renderFundamentalsReport(
532
603
  const fund = fundFactsNote(symbol, financials.exchange || quote?.exchangeName, financials);
533
604
  if (lines.length === before) lines.push("", cliStyles.muted([`No ${view} reported for ${financials.symbol}.`, fund].filter(Boolean).join(" ")));
534
605
  else if (fund) lines.push(cliStyles.muted(fund));
535
- for (const note of fundamentalsNotes(financials.fundamentals, metrics, quote?.currency)) lines.push(cliStyles.muted(note));
606
+ for (const note of fundamentalsNotes(atRegularSessionPrice(financials), metrics, quote?.currency)) lines.push(cliStyles.muted(note));
536
607
  if (view === "fundamentals") appendTextSection(lines, "Description", profile?.description);
537
608
  return lines.join("\n");
538
609
  }
@@ -572,7 +643,7 @@ export function fundamentalsReportTables(
572
643
  notes: [
573
644
  ...metrics.length === 0
574
645
  ? [`No ${view} reported for ${financials.symbol}.`]
575
- : fundamentalsNotes(financials.fundamentals, metrics, financials.quote?.currency),
646
+ : fundamentalsNotes(atRegularSessionPrice(financials), metrics, financials.quote?.currency),
576
647
  ...fund ? [fund] : [],
577
648
  ],
578
649
  }),
@@ -605,8 +676,11 @@ export async function buildTickerReport({
605
676
  recentNews = [],
606
677
  recentSecFilings = [],
607
678
  futuresContract = null,
679
+ declaredDividends: declared = null,
608
680
  }: {
609
681
  symbol: string;
682
+ /** The declared distributions a forward yield dated ahead is named from (loadForwardYieldDeclarations). */
683
+ declaredDividends?: ForwardYieldDeclarations | null;
610
684
  /** The exchange the command named, as a canonical code. */
611
685
  listingExchange?: string;
612
686
  /** A futures symbol's contract terms and latest confirmed settlement. */
@@ -732,10 +806,11 @@ export async function buildTickerReport({
732
806
  ]);
733
807
  }
734
808
 
735
- const metrics = fundamentalsMetrics(quote, priced.fundamentals, profile, { text: marketCapText }, priceReturns, fundamentalsCurrency(financials), { text: enterpriseValueText });
809
+ const metrics = fundamentalsMetrics(quote, priced.fundamentals, profile, { text: marketCapText }, priceReturns, fundamentalsCurrency(financials),
810
+ { text: enterpriseValueText }, declared);
736
811
  const beforeFundamentals = lines.length;
737
812
  appendMetricSection(lines, "Fundamentals", metricLines(metrics));
738
- for (const note of fundamentalsNotes(fundamentals, metrics, quote?.currency)) lines.push(cliStyles.muted(note));
813
+ for (const note of fundamentalsNotes(priced, metrics, quote?.currency)) lines.push(cliStyles.muted(note));
739
814
  // Where a fund's facts would go: under its fundamentals, or in their place.
740
815
  const fund = fundFactsNote(quote?.symbol ?? symbol,
741
816
  quote?.tradingExchangeName || listingExchange || quote?.listingExchangeName || quote?.exchangeName || tickerFile?.metadata.exchange, financials, tickerFile);
@@ -948,6 +1023,9 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
948
1023
 
949
1024
  const notesFiles = new NotesFiles(dataDir);
950
1025
  const listingName = listingIdentity(listing, quote).name;
1026
+ // The text report's yield basis, read beside the rest for a forward yield dated ahead only; it never fails the report.
1027
+ const declarations = dependencies.printResult ? null
1028
+ : loadForwardYieldDeclarations(dataProvider, { symbol: requestSymbol, exchange }, resolvedFinancials.fundamentals);
951
1029
  const [notesResult, newsResult, secFilingsResult] = await Promise.allSettled([
952
1030
  notesFiles.load(tickerFile?.metadata.ticker ?? normalized),
953
1031
  dataProvider.getNews({
@@ -1013,6 +1091,7 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
1013
1091
  recentNews,
1014
1092
  recentSecFilings,
1015
1093
  futuresContract,
1094
+ declaredDividends: await declarations,
1016
1095
  }));
1017
1096
  if (freshness) console.log(`\n${cliFreshnessFooter(freshness)}`);
1018
1097
  });