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
@@ -0,0 +1,164 @@
1
+ import type { HeadlessPaneContext } from "../../../types/headless";
2
+ import { MarketDataCoordinator, resolveEntryValue } from "../../../market-data/coordinator";
3
+ import { fxLegForCurrency } from "../../../market-data/coordinator/fx-legs";
4
+ import { resolveCurrencyUnit } from "../../../utils/currency-units";
5
+ import { errorMessage } from "../../../utils/errors";
6
+
7
+ /** One currency's US dollar rate, as the app's FX rates serve it: USD per unit, and when it was observed. */
8
+ export interface UsdRate {
9
+ rate: number;
10
+ /** The source's observation time; null when it states none. */
11
+ asOf: string | null;
12
+ /** When the rate was read, said in place of an observation time the source does not state. */
13
+ retrievedAt?: string | null;
14
+ }
15
+
16
+ /** A rate a market cap was converted at: `to` per `from`, and the older of its two legs' observation times. */
17
+ export interface CapConversion {
18
+ from: string;
19
+ to: string;
20
+ rate: number;
21
+ asOf: string | null;
22
+ /** When the rate was read, for a rate whose source states no observation time. */
23
+ retrievedAt: string | null;
24
+ }
25
+
26
+ type CapRow = { symbol: string; marketCap: number | null; marketCapCurrency: string | null };
27
+
28
+ export type ConvertedCapRow<Row extends CapRow> = Row & {
29
+ /** The cap as the source served it, and its currency, when it was converted. */
30
+ marketCapNative?: number;
31
+ marketCapNativeCurrency?: string;
32
+ };
33
+
34
+ const usable = (rate: number | undefined): rate is number => rate != null && Number.isFinite(rate) && rate > 0;
35
+
36
+ function olderOf(left: string | null, right: string | null): string | null {
37
+ if (!left || !right) return left ?? right;
38
+ return Date.parse(left) <= Date.parse(right) ? left : right;
39
+ }
40
+
41
+ /**
42
+ * Every row's market cap in `target`, through each currency's US dollar
43
+ * rate (a pence cap is first made pounds). A row already in `target`, or
44
+ * with no cap, stays as it is. A currency with no usable rate leaves its
45
+ * rows in their own currency and is listed in `unconverted`, so the table
46
+ * never mixes a guess in.
47
+ */
48
+ export function convertMarketCaps<Row extends CapRow>(
49
+ rows: readonly Row[],
50
+ target: string,
51
+ rates: ReadonlyMap<string, UsdRate>,
52
+ ): { rows: Array<ConvertedCapRow<Row>>; conversions: CapConversion[]; unconverted: Array<{ currency: string; symbols: string[] }> } {
53
+ const usd = (currency: string): UsdRate | undefined => (currency === "USD" ? { rate: 1, asOf: null } : rates.get(currency));
54
+ const conversions = new Map<string, CapConversion>();
55
+ const unconverted = new Map<string, string[]>();
56
+ const converted = rows.map((row): ConvertedCapRow<Row> => {
57
+ if (row.marketCap == null || !row.marketCapCurrency || row.marketCapCurrency === target) return row;
58
+ const unit = resolveCurrencyUnit(row.marketCapCurrency);
59
+ let rate = 1;
60
+ if (unit.currency !== target) {
61
+ const from = usd(unit.currency);
62
+ const to = usd(target);
63
+ if (!from || !to || !usable(from.rate) || !usable(to.rate)) {
64
+ unconverted.set(row.marketCapCurrency, [...unconverted.get(row.marketCapCurrency) ?? [], row.symbol]);
65
+ return row;
66
+ }
67
+ rate = from.rate / to.rate;
68
+ if (!conversions.has(unit.currency)) {
69
+ conversions.set(unit.currency, {
70
+ from: unit.currency, to: target, rate, asOf: olderOf(from.asOf, to.asOf), retrievedAt: olderOf(from.retrievedAt ?? null, to.retrievedAt ?? null),
71
+ });
72
+ }
73
+ }
74
+ return {
75
+ ...row,
76
+ marketCap: row.marketCap / unit.divisor * rate,
77
+ marketCapCurrency: target,
78
+ marketCapNative: row.marketCap,
79
+ marketCapNativeCurrency: row.marketCapCurrency,
80
+ };
81
+ });
82
+ return {
83
+ rows: converted,
84
+ conversions: [...conversions.values()],
85
+ unconverted: [...unconverted].map(([currency, symbols]) => ({ currency, symbols })),
86
+ };
87
+ }
88
+
89
+ /** A rate as a dealer quotes it: `EUR/USD 1.1612`, `USD/JPY 151.20`; a cross of two other currencies as `from/to`. */
90
+ export function conversionPairText({ from, to, rate }: Pick<CapConversion, "from" | "to" | "rate">): string {
91
+ const figure = (value: number) => value.toPrecision(5);
92
+ if (from !== "USD" && to !== "USD") return `${from}/${to} ${figure(rate)}`;
93
+ const other = to === "USD" ? from : to;
94
+ const usdPerOther = to === "USD" ? rate : 1 / rate;
95
+ const perDollar = fxLegForCurrency(other)?.invert ?? true;
96
+ return perDollar ? `USD/${other} ${figure(1 / usdPerOther)}` : `${other}/USD ${figure(usdPerOther)}`;
97
+ }
98
+
99
+ /** `2026-10-13 21:00 UTC`, or `2026-10-13` for a date. */
100
+ function rateTimeText(asOf: string): string {
101
+ const time = Date.parse(asOf);
102
+ if (!Number.isFinite(time)) return asOf;
103
+ return /^\d{4}-\d{2}-\d{2}$/.test(asOf) ? asOf : `${new Date(time).toISOString().slice(0, 16).replace("T", " ")} UTC`;
104
+ }
105
+
106
+ /**
107
+ * The status line's note naming each rate and its date:
108
+ * `market caps in USD at EUR/USD 1.1612 (2026-10-13 21:00 UTC)`.
109
+ */
110
+ export function conversionNote(target: string, conversions: readonly CapConversion[]): string | null {
111
+ if (conversions.length === 0) return null;
112
+ const rates = conversions.map((conversion) => `${conversionPairText(conversion)} (${conversion.asOf ? rateTimeText(conversion.asOf)
113
+ : conversion.retrievedAt ? `retrieved ${rateTimeText(conversion.retrievedAt)}` : "time not stated"})`);
114
+ return `market caps in ${target} at ${rates.join(", ")}`;
115
+ }
116
+
117
+ /**
118
+ * The US dollar rate of each currency through the app's FX path (the market
119
+ * data coordinator's `loadFxRate`, which converted totals read), a pence code
120
+ * as its pound. A currency whose rate fails is listed in `failed` with why.
121
+ */
122
+ export async function loadFxLegs(
123
+ currencies: readonly string[],
124
+ ctx: Pick<HeadlessPaneContext, "marketData">,
125
+ ): Promise<{ legs: Map<string, UsdRate>; failed: Array<{ currency: string; reason: string }> }> {
126
+ const wanted = [...new Set(currencies.map((currency) => resolveCurrencyUnit(currency).currency).filter((currency) => currency && currency !== "USD"))];
127
+ const legs = new Map<string, UsdRate>();
128
+ const failed: Array<{ currency: string; reason: string }> = [];
129
+ if (wanted.length === 0) return { legs, failed };
130
+ const coordinator = new MarketDataCoordinator(ctx.marketData);
131
+ try {
132
+ await Promise.all(wanted.map(async (currency) => {
133
+ try {
134
+ const entry = await coordinator.loadFxRate(currency);
135
+ const rate = resolveEntryValue(entry);
136
+ if (!usable(rate ?? undefined)) throw new Error(entry.error?.message ?? "no usable rate");
137
+ const time = (value: number | null | undefined) => (value != null && Number.isFinite(value) ? new Date(value).toISOString() : null);
138
+ legs.set(currency, { rate: rate!, asOf: time(entry.asOf), retrievedAt: time(entry.fetchedAt) });
139
+ } catch (error) {
140
+ failed.push({ currency, reason: errorMessage(error) });
141
+ }
142
+ }));
143
+ } finally {
144
+ coordinator.destroy();
145
+ }
146
+ return { legs, failed };
147
+ }
148
+
149
+ const CURRENCY_CODE = /^[A-Z]{3}$/;
150
+
151
+ /** `--currency`: a three-letter code the runtime knows, upper-cased. */
152
+ export function normalizeCapCurrency(value: string): string {
153
+ const code = value.trim().toUpperCase();
154
+ let known: readonly string[] = [];
155
+ try {
156
+ known = Intl.supportedValuesOf("currency");
157
+ } catch {
158
+ // An older runtime lists none; the shape check stands alone.
159
+ }
160
+ if (!CURRENCY_CODE.test(code) || (known.length > 0 && !known.includes(code))) {
161
+ throw new Error(`Invalid --currency value "${value}". Use a three-letter currency code such as USD or EUR.`);
162
+ }
163
+ return code;
164
+ }
@@ -1,11 +1,16 @@
1
1
  import { formatPriceEarnings, NOT_MEANINGFUL, PRICE_EARNINGS_NOTICE } from "../../../utils/price-earnings";
2
- import type { HeadlessPaneColumn, HeadlessPaneDefinition } from "../../../types/headless";
2
+ import type { HeadlessPaneColumn, HeadlessPaneContext, HeadlessPaneDefinition } from "../../../types/headless";
3
3
  import { EXTENDED_SESSION_LABELS, type ExtendedSession } from "../../../market-data/market/status";
4
4
  import { formatCompact, formatCurrency, formatLevelPercent, formatNumber, formatPercent, formatPercentRaw } from "../../../utils/format";
5
5
  import { loadHeadlessFinancials, loadHeadlessSymbols } from "../shared/headless-market-data";
6
6
  import { quoteFreshnessFields } from "../shared/report-freshness";
7
7
  import { RELATIVE_VALUATION_STALE_FUNDAMENTALS_NOTICE, RELATIVE_VALUATION_STALE_QUOTE_NOTICE, relativeValuationValues } from "./relative-valuation-model";
8
8
  import { paneSchemas } from "./headless-schema";
9
+ import { isRecord } from "../../../utils/guards";
10
+ import { resolveCurrencyUnit } from "../../../utils/currency-units";
11
+ import { pickPeers, peerMethodTitle, type PeerSelection } from "./relative-valuation-peers";
12
+ import { extraColumns, loadExtraColumns, parseExtraColumns } from "./relative-valuation-columns";
13
+ import { conversionNote, convertMarketCaps, loadFxLegs } from "./relative-valuation-currency";
9
14
 
10
15
  const price = (value: unknown, row: Record<string, unknown>) => (
11
16
  value == null || !row.currency ? "-" : formatCurrency(Number(value), String(row.currency))
@@ -46,7 +51,9 @@ const VALUATION_COLUMNS: HeadlessPaneColumn[] = [
46
51
  })),
47
52
  ...[["evSales", "EV/S"]].map(([key, header]) => ({
48
53
  key: key!, header: header!, align: "right" as const,
49
- format: (value: unknown) => value == null ? "-" : formatNumber(Number(value), 1),
54
+ // A withdrawn EV says so rather than leaving a bare dash; the note under the table says why.
55
+ format: (value: unknown, row: Record<string, unknown>) => value != null ? formatNumber(Number(value), 1)
56
+ : row.evSalesUnavailable ? "EV unavailable" : "-",
50
57
  })),
51
58
  ...[["fcfYield", "FCF Yield"], ["revenueGrowth", "Quarter Revenue YoY"], ["operatingMargin", "TTM Op Margin"]].map(([key, header]) => ({
52
59
  key: key!, header: header!, align: "right" as const,
@@ -54,36 +61,112 @@ const VALUATION_COLUMNS: HeadlessPaneColumn[] = [
54
61
  })),
55
62
  ];
56
63
 
64
+ /** Peers `--peers` chose, as the expansion left them in the settings or the load picked them. */
65
+ function peerSelectionOf(value: unknown, symbols: readonly string[]): PeerSelection | null {
66
+ if (!isRecord(value) || typeof value.subject !== "string" || !Array.isArray(value.peers)) return null;
67
+ const selection = value as unknown as PeerSelection;
68
+ const expected = [selection.subject, ...selection.peers];
69
+ return expected.length === symbols.length && expected.every((symbol, index) => symbol.toUpperCase() === symbols[index]?.toUpperCase())
70
+ ? selection : null;
71
+ }
72
+
73
+ /** The peers of the one symbol `--peers` was given; several symbols are already a peer list. */
74
+ async function choosePeers(symbols: readonly string[], ctx: HeadlessPaneContext): Promise<PeerSelection> {
75
+ if (symbols.length !== 1) {
76
+ throw new Error(`--peers picks the peers of one symbol, such as RV ${symbols[0] ?? "DUOL"} --peers; with several symbols, leave it out.`);
77
+ }
78
+ const subject = symbols[0]!;
79
+ return pickPeers(subject, await loadHeadlessFinancials(ctx, subject).catch(() => null), ctx);
80
+ }
81
+
82
+ const PEER_SELECTION_SETTING = "peerSelection";
83
+
57
84
  export const relativeValuationHeadless: HeadlessPaneDefinition<"rows"> = {
58
85
  ...paneSchemas["relative-valuation-pane"],
86
+ freshness: { datesEachVenue: true },
59
87
  shape: "rows",
60
- describe: ({ symbols }) => `Relative Valuation | ${symbols.join(", ")}`,
88
+ describe: ({ symbols }, result) => {
89
+ const selection = peerSelectionOf(result?.metadata?.peerSelection, result?.symbols ?? symbols);
90
+ return selection
91
+ ? `Relative Valuation | ${selection.subject} and ${peerMethodTitle(selection)}`
92
+ : `Relative Valuation | ${(result?.symbols ?? symbols).join(", ")}`;
93
+ },
61
94
  columns: [...QUOTE_COLUMNS, ...VALUATION_COLUMNS],
62
- async load({ symbols }, ctx) {
95
+ async expandArgument(args, ctx) {
96
+ if (args.options.peers !== true) return null;
97
+ const selection = await choosePeers(args.symbols, ctx);
98
+ return {
99
+ argument: [selection.subject, ...selection.peers].join(", "),
100
+ settings: { [PEER_SELECTION_SETTING]: selection },
101
+ title: `RV ${selection.subject} + ${peerMethodTitle(selection, { automatic: true })}`,
102
+ };
103
+ },
104
+ async load(args, ctx) {
105
+ // --peers picked in advance (fn and shot) reads the same peers; any other caller picks them here.
106
+ const preset = args.options.peers === true ? peerSelectionOf(ctx.settings?.[PEER_SELECTION_SETTING], args.symbols) : null;
107
+ const selection = args.options.peers === true ? preset ?? await choosePeers(args.symbols, ctx) : null;
108
+ const symbols = selection ? [selection.subject, ...selection.peers] : args.symbols;
63
109
  const loaded = await loadHeadlessSymbols(symbols, ctx, (symbol) => loadHeadlessFinancials(ctx, symbol));
64
- const rows = loaded.entries.map(({ symbol, data }) => ({ symbol, ...relativeValuationValues(data), ...quoteFreshnessFields(data?.quote) }));
65
- const unavailableSymbols = [...loaded.unavailableSymbols, ...rows.filter((row) => ![
110
+ const baseRows = loaded.entries.map(({ symbol, data }) => ({ symbol, ...relativeValuationValues(data), ...quoteFreshnessFields(data?.quote) }));
111
+ const unavailableSymbols = [...loaded.unavailableSymbols, ...baseRows.filter((row) => ![
66
112
  row.marketCap, row.trailingPE, row.forwardPE, row.evSales, row.fcfYield, row.revenueGrowth, row.operatingMargin,
67
113
  ].some((value) => value != null)).map(({ symbol }) => symbol)];
68
- const staleSymbols = rows.filter((row) => row.quoteStale).map((row) => row.symbol);
69
- const staleFundamentalsSymbols = rows.filter((row) => row.fundamentalsProvenance?.stale).map((row) => row.symbol);
114
+ const staleSymbols = baseRows.filter((row) => row.quoteStale).map((row) => row.symbol);
115
+ const staleFundamentalsSymbols = baseRows.filter((row) => row.fundamentalsProvenance?.stale).map((row) => row.symbol);
116
+
117
+ // --currency: every cap in one currency, at the rates the app converts with.
118
+ const capCurrency = typeof args.options.currency === "string" ? args.options.currency : null;
119
+ const fx = capCurrency ? await loadFxLegs([capCurrency, ...baseRows.flatMap((row) => row.marketCapCurrency ?? [])], ctx) : null;
120
+ const converted = capCurrency && fx ? convertMarketCaps(baseRows, capCurrency, fx.legs) : null;
121
+ const capRows = converted?.rows ?? baseRows;
122
+
123
+ // --columns: each added column read from the path its own function uses.
124
+ const extraIds = parseExtraColumns(args.options.columns);
125
+ const financials = new Map(loaded.entries.map(({ symbol, data }) => [symbol, data] as const));
126
+ const extra = await loadExtraColumns(extraIds, capRows.map((row) => row.symbol), financials, ctx);
127
+ const rows = capRows.map((row) => ({ ...row, ...extra.fields.get(row.symbol) }));
128
+
70
129
  const errors = [...loaded.errors,
71
130
  ...staleSymbols.map((symbol) => `${symbol}: ${RELATIVE_VALUATION_STALE_QUOTE_NOTICE}`),
72
131
  ...staleFundamentalsSymbols.map((symbol) => `${symbol}: ${RELATIVE_VALUATION_STALE_FUNDAMENTALS_NOTICE}`),
132
+ ...extra.errors,
133
+ ];
134
+ const warnings = converted?.unconverted.map(({ currency, symbols: left }) => {
135
+ const reason = fx?.failed.find((failure) => failure.currency === resolveCurrencyUnit(currency).currency)?.reason;
136
+ return `No ${currency} rate${reason ? ` (${reason})` : ""}: ${left.join(", ")} ${left.length === 1 ? "keeps its" : "keep their"} market cap in ${currency}.`;
137
+ }) ?? [];
138
+ const conversionText = capCurrency && converted ? conversionNote(capCurrency, converted.conversions) : null;
139
+ const notes = [
140
+ ...selection ? [`Peers chosen automatically: ${selection.basis}. To choose your own: gloomberb fn RV ${selection.subject} <tickers>.`] : [],
141
+ ...extra.notes,
73
142
  ];
74
143
  // Last and Chg% are the regular session; an extended print gets a column only when a peer has one.
75
144
  const extendedSessions = (["PRE", "POST", "OVERNIGHT"] as const).filter((session) => rows.some((row) => row.extendedSession === session));
145
+ const columns = [...QUOTE_COLUMNS, ...extendedSessions.map(extendedColumn), ...VALUATION_COLUMNS];
146
+ const added = extraIds.length > 0 ? extraColumns(extraIds) : null;
76
147
  return {
77
148
  rows, unavailableSymbols, errors, complete: unavailableSymbols.length === 0 && errors.length === 0,
78
- ...(extendedSessions.length > 0 ? { columns: [...QUOTE_COLUMNS, ...extendedSessions.map(extendedColumn), ...VALUATION_COLUMNS] } : {}),
149
+ ...(selection ? { symbols } : {}),
150
+ ...(added ? { columns: [...columns, ...added.export], textColumns: [...columns, ...added.text] }
151
+ : extendedSessions.length > 0 ? { columns } : {}),
152
+ ...(notes.length > 0 ? { notes } : {}),
153
+ ...(warnings.length > 0 ? { warnings } : {}),
154
+ ...(conversionText ? { freshness: { note: conversionText } } : {}),
79
155
  metadata: {
80
156
  staleSymbols,
81
157
  staleFundamentalsSymbols,
158
+ ...(selection ? { peerSelection: selection } : {}),
159
+ ...(converted && capCurrency ? { marketCapConversion: { currency: capCurrency, rates: converted.conversions,
160
+ basis: "marketCap is converted into the asked currency at each currency's US dollar rate, the rate the app's converted totals use; marketCapNative and marketCapNativeCurrency keep the cap as served." } } : {}),
161
+ ...(extraIds.length > 0 ? { addedColumnsBasis: "Each added column keeps its own source and as-of beside it: short interest at its FINRA settlement date, analyst targets at retrieval, the next earnings date at the calendar's retrieval, gross margin and EV/EBITDA at the screener's statement date." } : {}),
82
162
  quoteBasis: "Stale quote fields are excluded from comparison; reportedQuote retains the rejected observation and quoteAsOf retains its source timestamp.",
83
163
  priceBasis: "price, change and changePercent are the regular session: after the close, its close and that close's move. A pre-market, after-hours or overnight print is in extendedPrice, extendedChange and extendedChangePercent, measured from that close (extendedMark when it is a bid and ask midpoint). marketCap, the P/E multiples, evSales (through the equity part of the enterprise value) and fcfYield are measured at that regular session price.",
84
164
  fundamentalsBasis: "Provider multiples and operating metrics retain independent fundamentalsProvenance. Retrieval time does not establish the valuation date.",
85
- notices: rows.some((row) => Object.keys(row.notMeaningful).length > 0
86
- || Object.values(row.reportedMultiples).some((value) => value != null && value <= 0)) ? [PRICE_EARNINGS_NOTICE] : [],
165
+ notices: [
166
+ ...rows.some((row) => Object.keys(row.notMeaningful).length > 0
167
+ || Object.values(row.reportedMultiples).some((value) => value != null && value <= 0)) ? [PRICE_EARNINGS_NOTICE] : [],
168
+ ...rows.flatMap((row) => row.evSalesUnavailable ? [`${row.symbol}: ${row.evSalesUnavailable}`] : []),
169
+ ],
87
170
  multipleBasis: "Comparable P/E fields require a positive finite multiple over positive EPS; notMeaningful says why one is null, and reportedMultiples preserves the finite provider values.",
88
171
  marketCapBasis: "Market caps retain their own currency and source. Fundamentals retrieval time is not a valuation date; quote timestamps do not date fallback caps.",
89
172
  },
@@ -1,6 +1,6 @@
1
1
  import { comparablePriceEarnings, notMeaningfulMultiples } from "../../../utils/price-earnings";
2
2
  import { selectMarketCapitalization } from "../../../utils/market-capitalization";
3
- import { reportedEnterpriseValue } from "../../../utils/fundamentals";
3
+ import { enterpriseValueReconciliationNote, enterpriseValueWithdrawn, reportedEnterpriseValue } from "../../../utils/fundamentals";
4
4
  import { getExtendedSessionDisplay, getQuoteSessionFields } from "../../../market-data/market/status";
5
5
  import { atRegularSessionPrice, movePriceDerivedFigures } from "../../../market-data/quotes/regular-session-valuation";
6
6
  import type { Quote, TickerFinancials } from "../../../types/financials";
@@ -70,6 +70,9 @@ export function relativeValuationValues(snapshot: TickerFinancials | null) {
70
70
  ? fundamentals.enterpriseToRevenue
71
71
  : enterpriseValue != null && fundamentals?.revenue != null && fundamentals.revenue > 0
72
72
  ? enterpriseValue / fundamentals.revenue : null,
73
+ /** Why EV/S is missing when the provider's EV was withdrawn, as `valuation` says it; null otherwise. */
74
+ evSalesUnavailable: enterpriseValueWithdrawn(fundamentals)
75
+ ? enterpriseValueReconciliationNote(fundamentals) ?? "EV unavailable." : null,
73
76
  fcfYield: capitalization && fundamentals?.financialCurrency === capitalization.currency
74
77
  && fundamentals.freeCashFlow != null && Number.isFinite(fundamentals.freeCashFlow) && capitalization.value > 0
75
78
  ? fundamentals.freeCashFlow / capitalization.value : null,
@@ -159,7 +159,10 @@ export function RelativeValuationPane({ focused, width, height }: PaneProps) {
159
159
  const error = symbols.length > 0 ? peerSnapshot.error : "No tickers selected";
160
160
  const [selectedIdx, setSelectedIdx] = useState(0);
161
161
  const [sortPreference, setSortPreference] = useState<RelativeSortPreference>(DEFAULT_RELATIVE_SORT);
162
- const baseCurrency = useAppSelector((state) => state.config.baseCurrency);
162
+ const appBaseCurrency = useAppSelector((state) => state.config.baseCurrency);
163
+ // `--currency` on a capture asks for another currency than the app's.
164
+ const capCurrency = typeof pane?.settings?.capCurrency === "string" ? pane.settings.capCurrency : null;
165
+ const baseCurrency = capCurrency ?? appBaseCurrency;
163
166
  const quoteTargets = useMemo<QuoteSubscriptionTarget[]>(() => symbols.map((peer) => ({
164
167
  symbol: peer,
165
168
  exchange: "",
@@ -209,8 +212,11 @@ export function RelativeValuationPane({ focused, width, height }: PaneProps) {
209
212
 
210
213
  usePaneNoticeFooter({
211
214
  registrationId: "relative-valuation-notices",
212
- notices: rows.filter((row) => row.fundamentalsProvenance?.stale)
213
- .map((row) => `${row.symbol}: ${RELATIVE_VALUATION_STALE_FUNDAMENTALS_NOTICE}.`),
215
+ notices: [
216
+ ...rows.filter((row) => row.fundamentalsProvenance?.stale)
217
+ .map((row) => `${row.symbol}: ${RELATIVE_VALUATION_STALE_FUNDAMENTALS_NOTICE}.`),
218
+ ...rows.flatMap((row) => row.evSalesUnavailable ? [`${row.symbol}: ${row.evSalesUnavailable}`] : []),
219
+ ],
214
220
  focused,
215
221
  });
216
222
 
@@ -237,7 +243,10 @@ export function RelativeValuationPane({ focused, width, height }: PaneProps) {
237
243
  case "forwardPE":
238
244
  return { text: formatPriceEarnings(row.notMeaningful.forwardPE ? NOT_MEANINGFUL : row.reportedMultiples.forwardPE), color: colors.text };
239
245
  case "evSales":
240
- return { text: formatNumber(row.evSales ?? undefined, 1), color: colors.text };
246
+ // A withdrawn EV says so; the footer says why.
247
+ return row.evSales == null && row.evSalesUnavailable
248
+ ? { text: "EV n/a", color: colors.textDim }
249
+ : { text: formatNumber(row.evSales ?? undefined, 1), color: colors.text };
241
250
  case "fcfYield":
242
251
  return { text: formatLevelPercent(row.fcfYield ?? undefined), color: priceColor(row.fcfYield ?? 0) };
243
252
  case "revenueGrowth":