gloomberb 0.16.2 → 0.16.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 (124) hide show
  1. package/package.json +1 -1
  2. package/src/brokers/account-cache.ts +45 -0
  3. package/src/cli/command-options.ts +74 -0
  4. package/src/cli/commands/command-utils.ts +29 -0
  5. package/src/cli/commands/market.ts +170 -54
  6. package/src/cli/commands/overview.ts +62 -19
  7. package/src/cli/commands/ticker.ts +138 -43
  8. package/src/cli/desktop-pane-shot.ts +3 -0
  9. package/src/cli/help.ts +26 -6
  10. package/src/cli/history-rows.ts +187 -15
  11. package/src/cli/index.ts +12 -7
  12. package/src/cli/options.ts +5 -1
  13. package/src/cli/pane-functions/capabilities.ts +5 -0
  14. package/src/cli/pane-functions/dom.ts +69 -16
  15. package/src/cli/pane-functions/function-help.ts +4 -2
  16. package/src/cli/pane-functions/headless.ts +163 -31
  17. package/src/cli/pane-functions/index.ts +9 -2
  18. package/src/cli/pane-functions/report.ts +8 -1
  19. package/src/cli/pane-functions/resolver.ts +28 -2
  20. package/src/cli/pane-functions/screenshot.ts +4 -0
  21. package/src/cli/registry.ts +6 -3
  22. package/src/cli/report-tables.ts +460 -0
  23. package/src/cli/result.ts +13 -3
  24. package/src/components/data-table/export.ts +6 -51
  25. package/src/components/data-table/view.tsx +3 -1
  26. package/src/components/layout/shell/active-drag.ts +67 -35
  27. package/src/components/layout/shell/cursor-occlusion.ts +6 -5
  28. package/src/components/layout/shell/drag/index.ts +12 -7
  29. package/src/components/layout/shell/drag/live.ts +122 -0
  30. package/src/components/layout/shell/drag/overlays.tsx +179 -35
  31. package/src/components/layout/shell/drag/runtime.ts +62 -50
  32. package/src/components/layout/shell/drag/slide.ts +10 -0
  33. package/src/components/layout/shell/index.tsx +37 -51
  34. package/src/components/layout/shell/native/pointer-runtime.ts +13 -13
  35. package/src/components/layout/shell/native/surfaces.ts +30 -14
  36. package/src/components/layout/shell/pane/layers.tsx +419 -179
  37. package/src/components/layout/shell/terminal-pointer-runtime.ts +18 -18
  38. package/src/components/layout/shell/window-mode/overlays.tsx +12 -4
  39. package/src/data/json-persistence.ts +17 -3
  40. package/src/data/json-storage.ts +2 -0
  41. package/src/data/memory-resource-store.ts +46 -0
  42. package/src/market-data/market/freshness.ts +45 -0
  43. package/src/market-data/quotes/freshness.ts +5 -0
  44. package/src/plugins/builtin/analytics/pane-model.ts +10 -6
  45. package/src/plugins/builtin/attention/pane.tsx +1 -1
  46. package/src/plugins/builtin/catalysts/detail.tsx +1 -1
  47. package/src/plugins/builtin/company-attention/pane.tsx +1 -1
  48. package/src/plugins/builtin/holders/client.ts +3 -6
  49. package/src/plugins/builtin/holders/format.ts +31 -1
  50. package/src/plugins/builtin/holders/headless.ts +26 -5
  51. package/src/plugins/builtin/holders/pane.tsx +22 -13
  52. package/src/plugins/builtin/holders/report-header.ts +46 -5
  53. package/src/plugins/builtin/kelly-sizer/pane.tsx +12 -6
  54. package/src/plugins/builtin/kelly-sizer/portfolio.ts +25 -0
  55. package/src/plugins/builtin/members/pane.tsx +10 -0
  56. package/src/plugins/builtin/perps/pane.tsx +1 -1
  57. package/src/plugins/builtin/portfolio-list/account-metrics.ts +16 -1
  58. package/src/plugins/builtin/portfolio-list/allocation.ts +3 -0
  59. package/src/plugins/builtin/portfolio-list/cached-account.ts +18 -9
  60. package/src/plugins/builtin/portfolio-list/cli/render.ts +2 -0
  61. package/src/plugins/builtin/portfolio-list/headless.ts +2 -0
  62. package/src/plugins/builtin/portfolio-list/pane/index.tsx +7 -4
  63. package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -4
  64. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +11 -1
  65. package/src/plugins/builtin/research/relative-valuation-model.ts +7 -3
  66. package/src/plugins/builtin/reverse-dcf/model.ts +2 -1
  67. package/src/plugins/builtin/shared/members-menu.tsx +260 -0
  68. package/src/plugins/builtin/short-volume/pane.tsx +1 -1
  69. package/src/plugins/builtin/social-mentions/pane.tsx +1 -1
  70. package/src/plugins/builtin/themes/pane.tsx +3 -0
  71. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +4 -0
  72. package/src/plugins/builtin/ticker-detail/financials/model.ts +25 -2
  73. package/src/plugins/builtin/ticker-detail/headless.ts +20 -4
  74. package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -4
  75. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +3 -2
  76. package/src/plugins/builtin/world-venue-map/basemap-layer.tsx +2 -0
  77. package/src/plugins/builtin/world-venue-map/basemap.ts +18 -0
  78. package/src/plugins/builtin/world-venue-map/country-labels.ts +78 -0
  79. package/src/plugins/builtin/world-venue-map/geo-svg.tsx +2 -1
  80. package/src/plugins/builtin/world-venue-map/index.ts +5 -5
  81. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +27 -14
  82. package/src/plugins/builtin/world-venue-map/layers.ts +68 -10
  83. package/src/plugins/builtin/world-venue-map/map.tsx +198 -62
  84. package/src/plugins/builtin/world-venue-map/model.ts +16 -0
  85. package/src/plugins/builtin/world-venue-map/natural-earth/index.chunk.ts +3 -0
  86. package/src/plugins/builtin/world-venue-map/natural-earth/labels.chunk.ts +244 -0
  87. package/src/plugins/builtin/world-venue-map/pane.tsx +16 -10
  88. package/src/renderers/browser/app-services.ts +3 -1
  89. package/src/renderers/dom/data-table/dom.ts +31 -0
  90. package/src/renderers/dom/data-table/index.tsx +5 -0
  91. package/src/renderers/dom/host/box.tsx +113 -17
  92. package/src/renderers/dom/host/drag-shield.ts +24 -0
  93. package/src/renderers/dom/host/live-frame.ts +54 -0
  94. package/src/renderers/dom/host/mouse.ts +8 -1
  95. package/src/renderers/dom/host/scroll-box.tsx +6 -5
  96. package/src/renderers/dom/host/tabs.tsx +3 -2
  97. package/src/renderers/dom/styles.css +13 -5
  98. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +7 -0
  99. package/src/renderers/electrobun/bun/desktop/backend.ts +2 -0
  100. package/src/renderers/electrobun/bun/desktop/initialization.ts +13 -0
  101. package/src/renderers/electrobun/shared/protocol.ts +10 -1
  102. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +3 -0
  103. package/src/renderers/electrobun/view/host/native.ts +0 -10
  104. package/src/renderers/electrobun/view/host/window-move.ts +28 -0
  105. package/src/renderers/electrobun/view/remote/persistence.ts +15 -2
  106. package/src/renderers/electrobun/view/ui-host.tsx +2 -5
  107. package/src/renderers/opentui/ui-host.tsx +7 -1
  108. package/src/sources/gloomberb-cloud/normalizers.ts +16 -20
  109. package/src/sources/provider-router/batches.ts +3 -3
  110. package/src/sources/provider-router/financial-routes.ts +2 -2
  111. package/src/sources/provider-router/financials.ts +70 -7
  112. package/src/sources/provider-router/primary.ts +2 -2
  113. package/src/state/pane-scroll-registry.tsx +67 -35
  114. package/src/test-support/margin-account.ts +108 -0
  115. package/src/time-series/resolution.ts +15 -0
  116. package/src/types/financials.ts +32 -3
  117. package/src/ui/host.tsx +12 -0
  118. package/src/ui/index.tsx +8 -3
  119. package/src/ui/scroll-box-axis.ts +26 -0
  120. package/src/utils/currency-units.ts +8 -3
  121. package/src/utils/depositary-receipt.ts +54 -10
  122. package/src/utils/display-number.ts +63 -0
  123. package/src/utils/fundamentals.ts +17 -1
  124. package/src/utils/price-history-integrity.ts +19 -6
@@ -1,19 +1,52 @@
1
1
  import type { PricePoint } from "../types/financials";
2
2
  import { marketPriceFractionDigitCeiling, resolveAssetDisplayKind } from "../market-data/market/format";
3
- import { isIntradayResolution, type ManualChartResolution } from "../time-series/resolution";
3
+ import { TIME_RANGES, type TimeRange } from "../time-series/range";
4
+ import { subtractTimeRange } from "../time-series/date-window";
5
+ import {
6
+ CHART_RESOLUTION_STEP_MS,
7
+ isIntradayResolution,
8
+ RANGE_HISTORY_RESOLUTION,
9
+ type ManualChartResolution,
10
+ } from "../time-series/resolution";
11
+ import { currencyUnitLabel } from "../utils/currency-units";
4
12
  import { getPricePointTimestamp } from "../utils/price-history";
13
+ import { pricePointIntegrityFlags, pricePointValues } from "../utils/price-history-integrity";
14
+ import { formatUtcTime } from "../utils/utc-time";
5
15
 
6
16
  const PRICE_KEYS = ["open", "high", "low", "close"] as const;
17
+ const DAY_MS = 24 * 60 * 60 * 1000;
7
18
  /** Daily bars are a day apart even when stamped at the session open. */
8
19
  const INTRADAY_MAX_GAP_MS = 20 * 60 * 60 * 1000;
20
+ const INTRADAY_RESOLUTIONS = ["1m", "5m", "15m", "30m", "45m", "1h"] as const;
21
+ /** A first bar later than this after the range's start is a late start, not a weekend or holiday. */
22
+ const LATE_START_MIN_MS = 10 * DAY_MS;
23
+ const LATE_START_MIN_BARS = 3;
9
24
 
10
- interface HistoryRow {
25
+ const BAR_INTERVAL_LABELS: Record<ManualChartResolution, string> = {
26
+ "1m": "1-minute",
27
+ "5m": "5-minute",
28
+ "15m": "15-minute",
29
+ "30m": "30-minute",
30
+ "45m": "45-minute",
31
+ "1h": "hourly",
32
+ "1d": "daily",
33
+ "1wk": "weekly",
34
+ "1mo": "monthly",
35
+ };
36
+
37
+ export interface HistoryRow {
11
38
  date: string;
12
39
  open: number | null;
13
40
  high: number | null;
14
41
  low: number | null;
15
- close: number;
42
+ close: number | null;
16
43
  volume: number | null;
44
+ /** Currency code of the prices; null for index points or when no source says. */
45
+ currency: string | null;
46
+ /** Bar size, in the codes of `resolution`: 5m, 1h, 1d, 1wk, 1mo. */
47
+ interval: ManualChartResolution | null;
48
+ /** Each way the bar's prices contradict each other, such as `high<open`; null when they agree. */
49
+ flag: string | null;
17
50
  }
18
51
 
19
52
  function decimalPlaces(value: number): number {
@@ -43,29 +76,63 @@ function isUtcMidnight(time: number): boolean {
43
76
  return new Date(time).toISOString().endsWith("T00:00:00.000Z");
44
77
  }
45
78
 
79
+ function medianGap(times: readonly number[]): number | null {
80
+ const finite = times.filter(Number.isFinite).sort((left, right) => left - right);
81
+ const gaps = finite.slice(1).map((time, index) => time - finite[index]!).filter((gap) => gap > 0).sort((left, right) => left - right);
82
+ return gaps.length > 0 ? gaps[Math.floor((gaps.length - 1) / 2)]! : null;
83
+ }
84
+
46
85
  /** Without a declared cadence, bar spacing decides: daily bars may be stamped at the session open. */
47
86
  function looksIntraday(times: readonly number[]): boolean {
48
- const finite = times.filter(Number.isFinite).sort((left, right) => left - right);
87
+ const finite = times.filter(Number.isFinite);
49
88
  if (finite.length < 2) return finite.some((time) => !isUtcMidnight(time));
50
- const gaps = finite.slice(1).map((time, index) => time - finite[index]!).filter((gap) => gap > 0).sort((left, right) => left - right);
51
- return gaps.length > 0 && gaps[Math.floor((gaps.length - 1) / 2)]! < INTRADAY_MAX_GAP_MS;
89
+ const gap = medianGap(finite);
90
+ return gap != null && gap < INTRADAY_MAX_GAP_MS;
91
+ }
92
+
93
+ /** The bar size served: as the source declared it, otherwise read from the typical spacing of the bars. */
94
+ function historyInterval(times: readonly number[], resolution: ManualChartResolution | null): ManualChartResolution | null {
95
+ if (resolution) return resolution;
96
+ const gap = medianGap(times);
97
+ if (gap == null) return null;
98
+ if (gap < INTRADAY_MAX_GAP_MS) {
99
+ return INTRADAY_RESOLUTIONS.reduce((best, candidate) => (
100
+ Math.abs(CHART_RESOLUTION_STEP_MS[candidate] - gap) < Math.abs(CHART_RESOLUTION_STEP_MS[best] - gap) ? candidate : best
101
+ ));
102
+ }
103
+ const days = gap / DAY_MS;
104
+ return days >= 25 ? "1mo" : days >= 6 ? "1wk" : "1d";
52
105
  }
53
106
 
54
- /** Intraday bars keep their UTC time; daily and longer bars are trading dates. */
55
- export function historyRows(points: readonly PricePoint[], resolution: ManualChartResolution | null): HistoryRow[] {
107
+ /**
108
+ * Intraday bars keep their UTC time; daily and longer bars are trading dates.
109
+ * A bar whose prices contradict each other is blanked exactly as HP blanks it,
110
+ * and says why in `flag`.
111
+ */
112
+ export function historyRows(
113
+ points: readonly PricePoint[],
114
+ resolution: ManualChartResolution | null,
115
+ currency: string | null = null,
116
+ ): HistoryRow[] {
56
117
  const times = points.map(getPricePointTimestamp);
57
118
  const intraday = resolution ? isIntradayResolution(resolution) : looksIntraday(times);
119
+ const interval = historyInterval(times, resolution);
58
120
  return points.map((point, index) => {
59
121
  const time = times[index]!;
60
122
  const iso = Number.isFinite(time) ? new Date(time).toISOString() : "";
61
- const price = (value: number | undefined) => value == null ? null : cleanFloat32Price(value);
123
+ const values = pricePointValues(point);
124
+ const flags = pricePointIntegrityFlags(point);
125
+ const price = (value: number | null) => value == null ? null : cleanFloat32Price(value);
62
126
  return {
63
127
  date: intraday ? iso.replace(".000Z", "Z") : iso.slice(0, 10),
64
- open: price(point.open),
65
- high: price(point.high),
66
- low: price(point.low),
67
- close: cleanFloat32Price(point.close),
68
- volume: point.volume ?? null,
128
+ open: price(values.open),
129
+ high: price(values.high),
130
+ low: price(values.low),
131
+ close: price(values.close),
132
+ volume: values.volume,
133
+ currency,
134
+ interval,
135
+ flag: flags.length > 0 ? flags.join(",") : null,
69
136
  };
70
137
  });
71
138
  }
@@ -84,7 +151,7 @@ function neededDecimals(value: number, cap: number): number {
84
151
  * one, the largest price sets a seven significant digit ceiling (BTC keeps
85
152
  * cents, FX keeps pips, sub-cent coins keep their digits). Cents at least above 1.
86
153
  */
87
- export function historyPriceDecimals(rows: readonly HistoryRow[], assetCategory?: string): number {
154
+ export function historyPriceDecimals(rows: readonly Pick<HistoryRow, typeof PRICE_KEYS[number]>[], assetCategory?: string): number {
88
155
  const values = rows.flatMap((row) => PRICE_KEYS.flatMap((key) => {
89
156
  const value = row[key];
90
157
  return value != null && Number.isFinite(value) && value !== 0 ? [Math.abs(value)] : [];
@@ -100,3 +167,108 @@ export function historyPriceDecimals(rows: readonly HistoryRow[], assetCategory?
100
167
  const decimals = needed[Math.floor(0.9 * (needed.length - 1))]!;
101
168
  return largest >= 1 ? Math.max(2, decimals) : decimals;
102
169
  }
170
+
171
+ /** How far back ALL reaches, in whole years, from the window a history request for it asks for. */
172
+ export function historyAllRangeYears(now = new Date()): number {
173
+ return Math.round((now.getTime() - subtractTimeRange(now, "ALL").getTime()) / (365.25 * DAY_MS));
174
+ }
175
+
176
+ /** "1D 5-minute, 1W hourly, 1M to 1Y daily, 5Y weekly, ALL monthly", from the table history requests use. */
177
+ export function historyIntervalsByRange(): string {
178
+ const groups: Array<{ ranges: TimeRange[]; resolution: ManualChartResolution }> = [];
179
+ for (const range of TIME_RANGES) {
180
+ const resolution = RANGE_HISTORY_RESOLUTION[range];
181
+ const last = groups.at(-1);
182
+ if (last?.resolution === resolution) last.ranges.push(range);
183
+ else groups.push({ ranges: [range], resolution });
184
+ }
185
+ return groups.map(({ ranges, resolution }) => (
186
+ `${ranges.length > 1 ? `${ranges[0]} to ${ranges.at(-1)}` : ranges[0]} ${BAR_INTERVAL_LABELS[resolution]}`
187
+ )).join(", ");
188
+ }
189
+
190
+ export interface HistoryUnit {
191
+ kind: "currency" | "pair" | "points" | "unknown";
192
+ /** Currency code of the prices; null for index points or when no source says. */
193
+ currency: string | null;
194
+ /** What one price is: "USD", "ILA (agorot, 1/100 ILS)", "ZAR per USD", "index points"; null when unknown. */
195
+ unit: string | null;
196
+ }
197
+
198
+ /**
199
+ * What the prices are in. An FX pair is named by its symbol (ZAR=X is ZAR per
200
+ * USD, EURUSD=X is USD per EUR), an index is in points, and anything else is
201
+ * in the currency its quote states, a sub-unit named as such. Without one the
202
+ * unit is unknown rather than guessed from the exchange.
203
+ */
204
+ export function historyUnit(symbol: string, quote: { currency?: string; instrumentType?: string } | null | undefined): HistoryUnit {
205
+ const pair = /^([A-Z]{3})([A-Z]{3})?=X$/.exec(symbol.trim().toUpperCase());
206
+ if (pair) {
207
+ const [base, counter] = pair[2] ? [pair[1]!, pair[2]] : ["USD", pair[1]!];
208
+ return { kind: "pair", currency: counter, unit: `${counter} per ${base}` };
209
+ }
210
+ if (quote?.instrumentType?.trim().toUpperCase() === "INDEX") return { kind: "points", currency: null, unit: "index points" };
211
+ const code = quote?.currency?.trim();
212
+ return code
213
+ ? { kind: "currency", currency: code, unit: currencyUnitLabel(code, { ofMajor: true }) }
214
+ : { kind: "unknown", currency: null, unit: null };
215
+ }
216
+
217
+ function historyDate(value: string): string {
218
+ return value.length > 10 ? formatUtcTime(value) : value;
219
+ }
220
+
221
+ /** The facts line under a history heading: "Currency USD", "daily bars", "2025-10-09 to 2026-10-08", "251 bars". */
222
+ export function historyFacts(unit: HistoryUnit, rows: readonly Pick<HistoryRow, "date" | "interval">[]): string[] {
223
+ const first = rows[0]?.date;
224
+ const last = rows.at(-1)?.date;
225
+ const interval = rows[0]?.interval;
226
+ return [
227
+ unit.kind === "pair" ? unit.unit! : unit.kind === "points" ? "Index points" : `Currency ${unit.unit ?? "unknown"}`,
228
+ interval ? `${BAR_INTERVAL_LABELS[interval]} bars` : "",
229
+ first && last ? (first === last ? historyDate(first) : `${historyDate(first)} to ${historyDate(last)}`) : "",
230
+ `${rows.length} ${rows.length === 1 ? "bar" : "bars"}`,
231
+ ].filter(Boolean);
232
+ }
233
+
234
+ /**
235
+ * What an export should know that its rows do not show: bars coarser than the
236
+ * range is served in, or a first bar well after the start asked for, as for a
237
+ * listing younger than the range. Weekends and holidays are within tolerance.
238
+ */
239
+ export function historyNotes(
240
+ range: TimeRange,
241
+ interval: ManualChartResolution | null,
242
+ firstDate: string | undefined,
243
+ now = Date.now(),
244
+ ): string[] {
245
+ if (!firstDate) return [];
246
+ const notes: string[] = [];
247
+ const expected = RANGE_HISTORY_RESOLUTION[range];
248
+ if (interval && CHART_RESOLUTION_STEP_MS[interval] > CHART_RESOLUTION_STEP_MS[expected]) {
249
+ const from = interval === "1mo" ? firstDate.slice(0, 7) : firstDate.slice(0, 10);
250
+ notes.push(`Asked ${range}, got ${BAR_INTERVAL_LABELS[interval]} bars from ${from}`);
251
+ }
252
+ const first = Date.parse(firstDate);
253
+ if (range !== "ALL" && Number.isFinite(first)) {
254
+ const tolerance = Math.max(LATE_START_MIN_MS, LATE_START_MIN_BARS * CHART_RESOLUTION_STEP_MS[interval ?? expected]);
255
+ if (first - subtractTimeRange(new Date(now), range).getTime() > tolerance) {
256
+ notes.push(`Asked ${range}, data starts ${firstDate.slice(0, 10)} (first available bar)`);
257
+ }
258
+ }
259
+ return notes;
260
+ }
261
+
262
+ /** "37 of 251 bars have high below open or close in the source data; ...", or null when every bar agrees. */
263
+ export function historyFlagNote(rows: readonly Pick<HistoryRow, "flag">[]): string | null {
264
+ const flagged = rows.filter((row) => row.flag);
265
+ if (flagged.length === 0) return null;
266
+ const codes = new Set(flagged.flatMap((row) => row.flag!.split(",")));
267
+ const kinds = [
268
+ codes.has("high<open") || codes.has("high<close") ? "high below open or close" : "",
269
+ codes.has("low>open") || codes.has("low>close") ? "low above open or close" : "",
270
+ codes.has("low>high") ? "low above high" : "",
271
+ ].filter(Boolean);
272
+ const count = `${flagged.length} of ${rows.length} ${flagged.length === 1 && rows.length === 1 ? "bar" : "bars"}`;
273
+ return `${count} ${flagged.length === 1 ? "has" : "have"} ${kinds.join(", or ")} in the source data; their prices are left blank`;
274
+ }
package/src/cli/index.ts CHANGED
@@ -16,14 +16,16 @@ import {
16
16
  renderCliHelp,
17
17
  renderCommandHelp,
18
18
  suggestCliCommand,
19
+ TABLE_SECTION_OPTION,
19
20
  type CliHelpEntry,
20
21
  } from "./help";
21
22
  import { parsePaneFunctionArgs } from "./pane-functions/options";
23
+ import { checkCliCommandOptions } from "./command-options";
22
24
  import { fail, inferCliErrorOptions, printCliError } from "./errors";
23
25
  import { setCliColorEnabledOverride } from "../utils/cli-output";
24
26
  import { search, searchCandidatesForCli, buildSearchReport } from "./commands/search";
25
27
  import { ticker } from "./commands/ticker";
26
- import { takeOption } from "./commands/command-utils";
28
+ import { requireOneArg, takeOption } from "./commands/command-utils";
27
29
  import { EXCHANGE_OPTION } from "./listing-arg";
28
30
  import { apiCliCommand } from "./commands/api";
29
31
  import { marketDataCliCommands } from "./commands/market";
@@ -107,11 +109,8 @@ function createCoreCliCommands(
107
109
  execute: async (rawArgs, ctx) => {
108
110
  const args = [...rawArgs];
109
111
  const exchange = takeOption(args, "--exchange");
110
- const symbol = args[0];
111
- if (!symbol) {
112
- ctx.fail("Usage: gloomberb ticker <symbol>");
113
- }
114
- await ticker(symbol!, {
112
+ const symbol = requireOneArg(args, "ticker <symbol>", "symbol", ctx);
113
+ await ticker(symbol, {
115
114
  exchange,
116
115
  initMarketData: ctx.initMarketData,
117
116
  fail: ctx.fail,
@@ -128,7 +127,7 @@ function createCoreCliCommands(
128
127
  usage: ["catalog [query] [--all] [--bot-safe]"],
129
128
  options: [
130
129
  { flags: "--all", description: "List every match instead of the first 25" },
131
- { flags: "--bot-safe", description: "Only functions with a verified unattended report" },
130
+ { flags: "--bot-safe, --botsafe", description: "Only functions with a verified unattended report" },
132
131
  ],
133
132
  examples: ["catalog", "catalog options", "catalog HP"],
134
133
  },
@@ -146,12 +145,17 @@ function createCoreCliCommands(
146
145
  options: [
147
146
  { flags: "--<option> <value>", description: "A function setting; gloomberb catalog <function> lists them" },
148
147
  { flags: "--require-bot-safe", description: "Fail unless the function has a verified, complete report" },
148
+ {
149
+ ...TABLE_SECTION_OPTION,
150
+ description: `${TABLE_SECTION_OPTION.description} (CALLS keeps --section for the transcript part)`,
151
+ },
149
152
  EXCHANGE_OPTION,
150
153
  ],
151
154
  examples: [
152
155
  "fn HP AAPL",
153
156
  "fn ANR SAN:EPA",
154
157
  "fn CBR --json",
158
+ "fn WEI --csv --section europe > europe.csv",
155
159
  "fn 13F AAPL --view=ticker-holdings",
156
160
  "fn OVME --spot 100 --strike 100 --days 30 --volatility 25",
157
161
  ],
@@ -354,6 +358,7 @@ export async function dispatchCli(args: string[], options: DispatchCliOptions =
354
358
  const target = helpOnly ? registry.lookup.get("help")! : resolved;
355
359
  const commandArgs = helpOnly ? [resolved.command.name] : parsed.args.slice(1);
356
360
  try {
361
+ if (!helpOnly && resolved.builtin) checkCliCommandOptions(resolved.command, commandArgs, parsed.literalStart - 1);
357
362
  const result = await target.command.execute(
358
363
  commandArgs,
359
364
  createCliCommandContext(target.ownerId, registry, parsed.options),
@@ -15,6 +15,8 @@ export interface ParsedCliArgs {
15
15
  options: CliGlobalOptions;
16
16
  /** `--help` or `-h` appeared before `--`. The flag is removed from args. */
17
17
  help: boolean;
18
+ /** Index in `args` where the arguments after a bare `--` begin; they are never options. */
19
+ literalStart: number;
18
20
  }
19
21
 
20
22
  export function isCliHelpFlag(arg: string): boolean {
@@ -43,10 +45,12 @@ export function parseCliGlobalArgs(rawArgs: string[]): ParsedCliArgs {
43
45
  const options: CliGlobalOptions = { ...DEFAULT_CLI_OPTIONS };
44
46
  const args: string[] = [];
45
47
  let help = false;
48
+ let literalStart: number | null = null;
46
49
 
47
50
  for (let index = 0; index < rawArgs.length; index += 1) {
48
51
  const arg = rawArgs[index]!;
49
52
  if (arg === "--") {
53
+ literalStart = args.length;
50
54
  args.push(...rawArgs.slice(index + 1));
51
55
  break;
52
56
  }
@@ -102,5 +106,5 @@ export function parseCliGlobalArgs(rawArgs: string[]): ParsedCliArgs {
102
106
  args.push(arg);
103
107
  }
104
108
 
105
- return { args, options, help };
109
+ return { args, options, help, literalStart: literalStart ?? args.length };
106
110
  }
@@ -134,6 +134,11 @@ function resolveOptionDef(
134
134
  ));
135
135
  }
136
136
 
137
+ /** Whether the function has an option of its own under this name or an alias. */
138
+ export function capabilityHasOption(capability: PaneFunctionCapability, key: string): boolean {
139
+ return resolveOptionDef(capability, key) !== undefined;
140
+ }
141
+
137
142
  function normalizeEnumValue(option: PaneFunctionOptionDef, rawValue: string): string {
138
143
  const token = optionToken(rawValue);
139
144
  const match = option.values?.find((candidate) => (
@@ -7,6 +7,7 @@ import type { ResolvedPaneFunction } from "./resolver";
7
7
  import { collectShotSymbols } from "./data";
8
8
  import { renderDesktopShot, type PaneScreenshotResult } from "./screenshot";
9
9
  import { deriveRenderedFreshness, formatFreshnessLine, type ReportFreshness } from "./freshness";
10
+ import { exportTextTable, reportFooterLines, type CliReportTables } from "../report-tables";
10
11
 
11
12
  const DOM_REPORT_WIDTH = 1280;
12
13
  const DOM_REPORT_HEIGHT = 720;
@@ -42,7 +43,13 @@ function cellText(cell: DesktopPaneShotRenderedRow["cells"][number]): string {
42
43
  return cell.instant ? formatUtcTime(cell.instant) || cell.text : cell.text;
43
44
  }
44
45
 
45
- function renderDomTables(rows: DesktopPaneShotRenderedRow[]): string[] {
46
+ interface DomTable {
47
+ tableIndex: number;
48
+ columns: Array<{ key: string; header: string }>;
49
+ rows: Array<Map<string, DesktopPaneShotRenderedRow["cells"][number]>>;
50
+ }
51
+
52
+ function domTables(rows: DesktopPaneShotRenderedRow[]): DomTable[] {
46
53
  const byTable = new Map<number, DesktopPaneShotRenderedRow[]>();
47
54
  for (const row of rows) {
48
55
  const tableRows = byTable.get(row.tableIndex) ?? [];
@@ -50,7 +57,7 @@ function renderDomTables(rows: DesktopPaneShotRenderedRow[]): string[] {
50
57
  byTable.set(row.tableIndex, tableRows);
51
58
  }
52
59
 
53
- return [...byTable.entries()].flatMap(([tableIndex, tableRows], index) => {
60
+ return [...byTable.entries()].map(([tableIndex, tableRows]) => {
54
61
  // Captured rows omit blank cells, so a cell's position is not its column.
55
62
  const columns: Array<{ key: string; header: string }> = [];
56
63
  for (const row of tableRows) {
@@ -66,22 +73,54 @@ function renderDomTables(rows: DesktopPaneShotRenderedRow[]): string[] {
66
73
  previous = position;
67
74
  }
68
75
  }
69
- const output = renderTable(
76
+ return {
77
+ tableIndex,
70
78
  columns,
71
- tableRows.map((row) => {
79
+ rows: tableRows.map((row) => {
72
80
  const keys = domCellKeys(row);
73
- const textByKey = new Map(row.cells.map((cell, cellIndex) => [keys[cellIndex]!, cellText(cell)]));
74
- return columns.map((column) => textByKey.get(column.key) ?? "");
81
+ return new Map(row.cells.map((cell, cellIndex) => [keys[cellIndex]!, cell]));
75
82
  }),
76
- );
77
- return [
78
- ...(index > 0 ? [""] : []),
79
- ...(byTable.size > 1 ? [renderSection(`Rendered table ${tableIndex + 1}`)] : []),
80
- output,
81
- ];
83
+ };
82
84
  });
83
85
  }
84
86
 
87
+ function domTableTitle(table: DomTable): string {
88
+ return `Rendered table ${table.tableIndex + 1}`;
89
+ }
90
+
91
+ function renderDomTables(rows: DesktopPaneShotRenderedRow[]): string[] {
92
+ const tables = domTables(rows);
93
+ return tables.flatMap((table, index) => [
94
+ ...(index > 0 ? [""] : []),
95
+ ...(tables.length > 1 ? [renderSection(domTableTitle(table))] : []),
96
+ renderTable(
97
+ table.columns,
98
+ table.rows.map((cells) => table.columns.map((column) => {
99
+ const cell = cells.get(column.key);
100
+ return cell ? cellText(cell) : "";
101
+ })),
102
+ ),
103
+ ]);
104
+ }
105
+
106
+ /** The rendered tables as `--csv` writes them, with the instant behind a shortened time. */
107
+ function domReportTables(
108
+ rows: DesktopPaneShotRenderedRow[],
109
+ title: string,
110
+ freshness: ReportFreshness,
111
+ incomplete: boolean | string,
112
+ ): CliReportTables {
113
+ const tables = domTables(rows);
114
+ return {
115
+ tables: tables.map((table) => exportTextTable(
116
+ tables.length > 1 ? domTableTitle(table) : title,
117
+ table.columns.map((column) => column.header),
118
+ table.rows.map((cells) => table.columns.map((column) => cells.get(column.key))),
119
+ )),
120
+ footer: reportFooterLines({ freshness, incomplete }),
121
+ };
122
+ }
123
+
85
124
  function renderedFailureReason(
86
125
  result: PaneScreenshotResult,
87
126
  rows: DesktopPaneShotRenderedRow[],
@@ -144,6 +183,7 @@ export function buildDomPaneReportFromRender(
144
183
  cellTimes: rows.flatMap((row) => row.cells.flatMap((cell) => cell.instant ?? [])),
145
184
  });
146
185
 
186
+ const complete = failureReason === null && !truncated;
147
187
  return {
148
188
  data: {
149
189
  kind: "rendered-view",
@@ -153,7 +193,7 @@ export function buildDomPaneReportFromRender(
153
193
  options: resolved.options,
154
194
  rowCount: rows.length,
155
195
  empty: rows.length === 0,
156
- complete: failureReason === null && !truncated,
196
+ complete,
157
197
  unavailableSymbols,
158
198
  rows,
159
199
  visibleText: result.render.visibleText,
@@ -164,6 +204,12 @@ export function buildDomPaneReportFromRender(
164
204
  freshness,
165
205
  },
166
206
  text: textLines.join("\n").trimEnd(),
207
+ tables: domReportTables(
208
+ rows,
209
+ resolved.label,
210
+ freshness,
211
+ !complete && (failureReason ?? (truncationReasons.length > 0 ? truncationReasons.join("; ") : true)),
212
+ ),
167
213
  };
168
214
  }
169
215
 
@@ -173,15 +219,20 @@ export function appendDomReportFooter(
173
219
  truncated: boolean,
174
220
  freshness: ReportFreshness,
175
221
  ): string {
176
- const clipping = truncated ? "The rendered view is clipped." : "The rendered view may be clipped.";
177
222
  return [
178
223
  text,
179
224
  "",
180
- `Rendered view: values come from the visible pane. ${clipping} Render time: ${elapsedMs} ms.`,
225
+ domReportNote(elapsedMs, truncated),
181
226
  cliStyles.muted(formatFreshnessLine(freshness)),
182
227
  ].join("\n");
183
228
  }
184
229
 
230
+ /** What a rendered-view report says about where its values come from. */
231
+ export function domReportNote(elapsedMs: number, truncated: boolean): string {
232
+ const clipping = truncated ? "The rendered view is clipped." : "The rendered view may be clipped.";
233
+ return `Rendered view: values come from the visible pane. ${clipping} Render time: ${elapsedMs} ms.`;
234
+ }
235
+
185
236
  function failedDomReport(
186
237
  resolved: ResolvedPaneFunction,
187
238
  rawArg: string,
@@ -193,6 +244,7 @@ function failedDomReport(
193
244
  .trim();
194
245
  const reason = `The rendered view could not be read: ${message || "unknown renderer error"}`;
195
246
  const symbols = collectShotSymbols(resolved, rawArg);
247
+ const freshness = deriveRenderedFreshness(resolved.pane.reportFreshness, { footerText: "", cellTimes: [] });
196
248
  return {
197
249
  data: {
198
250
  kind: "rendered-view",
@@ -210,9 +262,10 @@ function failedDomReport(
210
262
  truncationReasons: [],
211
263
  limitation: DOM_LIMITATION,
212
264
  reason,
213
- freshness: deriveRenderedFreshness(resolved.pane.reportFreshness, { footerText: "", cellTimes: [] }),
265
+ freshness,
214
266
  },
215
267
  text: [resolved.label, "", reason].join("\n"),
268
+ tables: { tables: [], footer: reportFooterLines({ freshness, incomplete: reason }) },
216
269
  };
217
270
  }
218
271
 
@@ -81,6 +81,8 @@ const SEARCH = key("/", "search");
81
81
  const OPEN_SOURCE = key("o", "pen source");
82
82
  const POP_OUT = key("p", "op out");
83
83
  const STEP = key("←/→", " step");
84
+ /** THEM and MEMB members: open them in RRG, CORR, SIW or RIPL, or save them as a watchlist. */
85
+ const MEMBERS_MENU = key(".", " open members in, save as watchlist");
84
86
  const CHART_KEYS = [key("s", "eries"), key("i", "ndicators"), key("t", "imeframe"), key("f", "ormulas")];
85
87
 
86
88
  export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
@@ -663,14 +665,14 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
663
665
  MEMB: {
664
666
  summary: "ETF holdings with weights, shares, member returns, daily contributions and index changes. SPX and SPY use IVV holdings.",
665
667
  usage: ["MEMB", "MEMB SPY", "MEMB IWM"],
666
- keys: [TABS, OPEN],
668
+ keys: [TABS, OPEN, MEMBERS_MENU],
667
669
  data: same("Dated fund holdings and partial delayed member returns. Nasdaq-100 is not covered."),
668
670
  bloomberg: ["MEMB", "MRR", "IMOV"],
669
671
  },
670
672
  THEM: {
671
673
  summary: "Curated thematic baskets with equal-weight returns and breadth. Open a theme to see its members, leaders and laggards. The Themes tab of BI.",
672
674
  usage: ["THEM", "THEM nuclear"],
673
- keys: [TABS, OPEN, key("Esc", "back")],
675
+ keys: [TABS, OPEN, key("Esc", "back"), MEMBERS_MENU],
674
676
  data: same("Updated every 15 minutes"),
675
677
  bloomberg: ["IMAP", "custom baskets"],
676
678
  },