gloomberb 0.13.1 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/index.test.ts +34 -0
  3. package/src/api-client/request.ts +4 -1
  4. package/src/cli/pane-functions/cloud-session.test.ts +54 -0
  5. package/src/cli/pane-functions/cloud-session.ts +38 -0
  6. package/src/cli/pane-functions/index.ts +5 -1
  7. package/src/cli/pane-functions/screenshot.ts +2 -19
  8. package/src/components/command-bar/assist/runtime.ts +38 -30
  9. package/src/components/command-bar/surface/assist.test.tsx +43 -0
  10. package/src/news/types.ts +1 -0
  11. package/src/plugins/builtin/chat/controller/channels.ts +2 -1
  12. package/src/plugins/builtin/chat/controller/index.ts +13 -5
  13. package/src/plugins/builtin/chat/controller/realtime.ts +24 -0
  14. package/src/plugins/builtin/chat/controller/session-runtime.ts +76 -8
  15. package/src/plugins/builtin/chat/controller/state.ts +1 -0
  16. package/src/plugins/builtin/chat/controller.test.ts +260 -4
  17. package/src/plugins/builtin/cloud/askg/executor.test.ts +160 -0
  18. package/src/plugins/builtin/cloud/askg/executor.ts +429 -0
  19. package/src/plugins/builtin/cloud/askg/manifest.test.ts +153 -0
  20. package/src/plugins/builtin/cloud/askg/manifest.ts +314 -0
  21. package/src/plugins/builtin/cloud/askg/protocol.test.ts +3 -2
  22. package/src/plugins/builtin/cloud/askg/protocol.ts +16 -4
  23. package/src/plugins/builtin/cloud/askg/undo.test.ts +44 -0
  24. package/src/plugins/builtin/cloud/askg/undo.ts +277 -0
  25. package/src/plugins/builtin/cloud/askg/verify.ts +130 -0
  26. package/src/plugins/builtin/cloud/plugin.tsx +2 -0
  27. package/src/plugins/builtin/congress-trades/client.ts +11 -0
  28. package/src/plugins/builtin/congress-trades/detail.tsx +3 -3
  29. package/src/plugins/builtin/congress-trades/headless.test.ts +98 -0
  30. package/src/plugins/builtin/congress-trades/headless.ts +145 -0
  31. package/src/plugins/builtin/congress-trades/pane.tsx +3 -3
  32. package/src/plugins/builtin/dividend-yield/headless.test.ts +88 -0
  33. package/src/plugins/builtin/dividend-yield/headless.ts +135 -0
  34. package/src/plugins/builtin/dividend-yield/index.tsx +12 -8
  35. package/src/plugins/builtin/dividend-yield/model.ts +4 -22
  36. package/src/plugins/builtin/dividend-yield/view.ts +17 -0
  37. package/src/plugins/builtin/earnings/headless.test.ts +82 -0
  38. package/src/plugins/builtin/earnings/headless.ts +162 -0
  39. package/src/plugins/builtin/earnings/index.tsx +2 -0
  40. package/src/plugins/builtin/earnings-calls/data.ts +29 -8
  41. package/src/plugins/builtin/earnings-calls/headless.test.ts +192 -0
  42. package/src/plugins/builtin/earnings-calls/headless.ts +300 -0
  43. package/src/plugins/builtin/earnings-calls/index.tsx +19 -9
  44. package/src/plugins/builtin/earnings-calls/model.ts +238 -0
  45. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +8 -9
  46. package/src/plugins/builtin/holders/client.ts +29 -0
  47. package/src/plugins/builtin/holders/headless.test.ts +58 -0
  48. package/src/plugins/builtin/holders/headless.ts +162 -0
  49. package/src/plugins/builtin/holders/index.tsx +14 -8
  50. package/src/plugins/builtin/holders/pane.tsx +7 -1
  51. package/src/plugins/builtin/insider/client.ts +50 -0
  52. package/src/plugins/builtin/insider/headless.test.ts +122 -0
  53. package/src/plugins/builtin/insider/headless.ts +117 -0
  54. package/src/plugins/builtin/insider/index.tsx +21 -50
  55. package/src/plugins/builtin/insider/insider-data.ts +1 -1
  56. package/src/plugins/builtin/insider/model.ts +72 -0
  57. package/src/plugins/builtin/ipo-calendar/client.ts +15 -0
  58. package/src/plugins/builtin/ipo-calendar/headless.test.ts +89 -0
  59. package/src/plugins/builtin/ipo-calendar/headless.ts +127 -0
  60. package/src/plugins/builtin/ipo-calendar/index.tsx +2 -0
  61. package/src/plugins/builtin/ipo-calendar/model.ts +3 -15
  62. package/src/plugins/builtin/news/headless.test.ts +138 -0
  63. package/src/plugins/builtin/news/headless.ts +190 -0
  64. package/src/plugins/builtin/news/index.tsx +13 -9
  65. package/src/plugins/builtin/news/wire/index.tsx +2 -1
  66. package/src/plugins/builtin/research/analyst-headless.test.ts +60 -0
  67. package/src/plugins/builtin/research/analyst-headless.ts +172 -0
  68. package/src/plugins/builtin/research/analyst-model.ts +219 -0
  69. package/src/plugins/builtin/research/analyst-pane.tsx +31 -221
  70. package/src/plugins/builtin/research/client.ts +12 -0
  71. package/src/plugins/builtin/research/headless.test.ts +93 -0
  72. package/src/plugins/builtin/research/headless.ts +161 -0
  73. package/src/plugins/builtin/research/index.tsx +28 -18
  74. package/src/plugins/builtin/sec/client.ts +16 -0
  75. package/src/plugins/builtin/sec/headless.test.ts +72 -0
  76. package/src/plugins/builtin/sec/headless.ts +69 -0
  77. package/src/plugins/builtin/sec/index.tsx +20 -75
  78. package/src/plugins/builtin/sec/model.ts +89 -0
  79. package/src/plugins/builtin/short-interest/client.ts +5 -2
  80. package/src/plugins/builtin/short-interest/headless.test.ts +96 -0
  81. package/src/plugins/builtin/short-interest/headless.ts +111 -0
  82. package/src/plugins/builtin/short-interest/index.tsx +15 -9
  83. package/src/plugins/builtin/thirteenf/data.ts +13 -11
  84. package/src/plugins/builtin/thirteenf/headless.test.ts +112 -0
  85. package/src/plugins/builtin/thirteenf/headless.ts +286 -0
  86. package/src/plugins/builtin/thirteenf/index.tsx +4 -0
  87. package/src/plugins/builtin/thirteenf/model.ts +13 -0
  88. package/src/plugins/builtin/thirteenf/table.ts +3 -15
  89. package/src/plugins/prediction-markets/headless.test.ts +119 -0
  90. package/src/plugins/prediction-markets/headless.ts +255 -0
  91. package/src/plugins/prediction-markets/index.tsx +2 -0
  92. package/src/sources/gloomberb-cloud/news.ts +1 -0
@@ -4,43 +4,36 @@ import {
4
4
  DataTableView,
5
5
  usePaneFooter,
6
6
  type DataTableCell,
7
- type DataTableColumn,
8
7
  type DataTableKeyEvent,
9
8
  } from "../../../components";
10
- import type { AnalystRatingRecord, AnalystResearchData } from "../../../types/financials";
9
+ import type { AnalystResearchData } from "../../../types/financials";
11
10
  import { blendHex, colors, priceColor } from "../../../theme/colors";
12
- import { formatCurrency, formatNumber, formatPercent } from "../../../utils/format";
13
- import { compareSortValues, type SortDirection } from "../../../utils/sort-values";
11
+ import { formatCurrency, formatPercent } from "../../../utils/format";
14
12
  import { useAssetData } from "../../runtime";
15
13
  import { handleRefreshKey, loadingErrorFooterInfo } from "../shared/table-pane";
16
14
  import { useBoundTicker as useSymbolBinding, useTickerRequest } from "../shared/ticker-request";
17
-
18
- function compactPeriod(period: string): string {
19
- return period
20
- .replace("current ", "")
21
- .replace("previous ", "prev ")
22
- .replace("next_", "next ")
23
- .replace(/_/g, " ");
24
- }
25
-
26
- function targetUpside(target: AnalystResearchData["priceTarget"]): number | undefined {
27
- if (!target?.average || !target.current) return undefined;
28
- return (target.average - target.current) / target.current;
29
- }
30
-
31
- function latestRecommendation(data: AnalystResearchData | null) {
32
- return data?.recommendations[0] ?? null;
33
- }
34
-
35
- function recommendationTotal(data: AnalystResearchData | null): number {
36
- const rec = latestRecommendation(data);
37
- if (!rec) return 0;
38
- return (rec.strongBuy ?? 0) + (rec.buy ?? 0) + (rec.hold ?? 0) + (rec.sell ?? 0) + (rec.strongSell ?? 0);
39
- }
40
-
41
- function formatRatingLabel(value: number | undefined): string {
42
- return value == null ? "-" : `${formatNumber(value, 1)}/10`;
43
- }
15
+ import { loadAnalystResearch } from "./client";
16
+ import {
17
+ DEFAULT_RATING_SORT,
18
+ buildAnalystSummaryLines,
19
+ buildRatingColumns,
20
+ formatRatingTarget,
21
+ nextRatingSortPreference,
22
+ ratingTargetDelta,
23
+ sortRatingRows,
24
+ targetUpside,
25
+ type RatingColumn,
26
+ type RatingSortPreference,
27
+ } from "./analyst-model";
28
+
29
+ export {
30
+ buildAnalystSummaryLines,
31
+ buildRatingColumns,
32
+ formatRatingTarget,
33
+ nextRatingSortPreference,
34
+ sortRatingRows,
35
+ type RatingSortPreference,
36
+ } from "./analyst-model";
44
37
 
45
38
  function ratingActionColor(action: string | undefined): string {
46
39
  const normalized = action?.toLowerCase() ?? "";
@@ -49,41 +42,6 @@ function ratingActionColor(action: string | undefined): string {
49
42
  return colors.textDim;
50
43
  }
51
44
 
52
- function formatPriceTarget(value: number | undefined, currency: string): string {
53
- if (value == null) return "-";
54
- return formatCurrency(value, currency)
55
- .replace(/\.00\b/, "")
56
- .replace(/(\.\d)0\b/, "$1");
57
- }
58
-
59
- const TARGET_SEPARATOR = " → ";
60
-
61
- interface RatingTargetColumnSizing {
62
- targetPriorWidth: number;
63
- targetCurrentWidth: number;
64
- }
65
-
66
- export function formatRatingTarget(
67
- row: AnalystResearchData["ratings"][number],
68
- currency: string,
69
- sizing?: Partial<RatingTargetColumnSizing>,
70
- ): string {
71
- const current = row.currentPriceTarget;
72
- const prior = row.priorPriceTarget;
73
- if (current == null && prior == null) return "-";
74
- if (current == null) return ` ${formatPriceTarget(prior, currency)}`;
75
- if (prior == null) return ` ${formatPriceTarget(current, currency)}`;
76
-
77
- const priorText = formatPriceTarget(prior, currency).padStart(sizing?.targetPriorWidth ?? 0);
78
- const currentText = formatPriceTarget(current, currency).padEnd(sizing?.targetCurrentWidth ?? 0);
79
- return ` ${priorText}${TARGET_SEPARATOR}${currentText}`;
80
- }
81
-
82
- function ratingTargetDelta(row: AnalystResearchData["ratings"][number]): number | null {
83
- if (row.currentPriceTarget == null || row.priorPriceTarget == null) return null;
84
- return row.currentPriceTarget - row.priorPriceTarget;
85
- }
86
-
87
45
  function ratingTargetBackground(delta: number | null): string | undefined {
88
46
  if (delta == null || delta === 0) return undefined;
89
47
  return blendHex(colors.bg, delta > 0 ? colors.positive : colors.negative, 0.42);
@@ -93,30 +51,6 @@ function ratingTargetBackground(delta: number | null): string | undefined {
93
51
  * Price targets and the recommendation mix are pane content, not status, so the
94
52
  * summary block owns them and the footer stays empty unless something changed.
95
53
  */
96
- export function buildAnalystSummaryLines(data: AnalystResearchData | null): string[] {
97
- if (!data) return [];
98
-
99
- const target = data.priceTarget;
100
- const currency = target?.currency ?? data.currency ?? "USD";
101
- const price = (value: number | undefined) => value != null ? formatCurrency(value, currency) : "-";
102
- const rec = latestRecommendation(data);
103
- const total = recommendationTotal(data);
104
- const lines: string[] = [];
105
-
106
- if (target) {
107
- lines.push(`low ${price(target.low)} med ${price(target.median)} high ${price(target.high)}`);
108
- }
109
- if (data.recommendationRating != null || rec || total > 0) {
110
- lines.push([
111
- data.recommendationRating != null ? `rating ${formatRatingLabel(data.recommendationRating)}` : null,
112
- rec ? `SB ${rec.strongBuy ?? 0} B ${rec.buy ?? 0} H ${rec.hold ?? 0} S ${(rec.sell ?? 0) + (rec.strongSell ?? 0)}` : null,
113
- total > 0 ? `${total} analysts${rec?.period ? ` (${compactPeriod(rec.period)})` : ""}` : null,
114
- ].filter(Boolean).join(" "));
115
- }
116
-
117
- return lines;
118
- }
119
-
120
54
  function AnalystSummary({ data }: { data: AnalystResearchData | null }) {
121
55
  const target = data?.priceTarget;
122
56
  const upside = targetUpside(target);
@@ -147,142 +81,18 @@ function AnalystSummary({ data }: { data: AnalystResearchData | null }) {
147
81
  );
148
82
  }
149
83
 
150
- type RatingColumnId = "date" | "firm" | "action" | "current" | "target" | "prior";
151
- type RatingColumn = DataTableColumn & { id: RatingColumnId } & Partial<RatingTargetColumnSizing>;
152
-
153
- export interface RatingSortPreference {
154
- columnId: RatingColumnId;
155
- direction: SortDirection;
156
- }
157
-
158
- const DEFAULT_RATING_SORT: RatingSortPreference = {
159
- columnId: "date",
160
- direction: "desc",
161
- };
162
-
163
- const DEFAULT_RATING_SORT_DIRECTIONS: Record<RatingColumnId, SortDirection> = {
164
- date: "desc",
165
- firm: "asc",
166
- action: "asc",
167
- current: "asc",
168
- target: "desc",
169
- prior: "asc",
170
- };
171
-
172
- const BASE_RATING_COLUMNS: RatingColumn[] = [
173
- { id: "date", label: "DATE", width: 10, align: "left" },
174
- { id: "firm", label: "FIRM", width: 20, align: "left" },
175
- { id: "action", label: "ACTION", width: 10, align: "left" },
176
- { id: "current", label: "RATING", width: 13, align: "left" },
177
- { id: "target", label: "TARGET", width: 13, align: "left" },
178
- { id: "prior", label: "PRIOR", width: 13, align: "left" },
179
- ];
180
-
181
- export function buildRatingColumns(
182
- rows: readonly AnalystRatingRecord[],
183
- currency: string,
184
- ): RatingColumn[] {
185
- const targetSizing = rows.reduce<RatingTargetColumnSizing>(
186
- (sizing, row) => ({
187
- targetPriorWidth: Math.max(
188
- sizing.targetPriorWidth,
189
- row.priorPriceTarget == null ? 0 : formatPriceTarget(row.priorPriceTarget, currency).length,
190
- ),
191
- targetCurrentWidth: Math.max(
192
- sizing.targetCurrentWidth,
193
- row.currentPriceTarget == null ? 0 : formatPriceTarget(row.currentPriceTarget, currency).length,
194
- ),
195
- }),
196
- { targetPriorWidth: 0, targetCurrentWidth: 0 },
197
- );
198
- const targetWidth = rows.reduce(
199
- (width, row) => Math.max(width, formatRatingTarget(row, currency, targetSizing).length),
200
- BASE_RATING_COLUMNS.find((column) => column.id === "target")?.width ?? 13,
201
- );
202
-
203
- return BASE_RATING_COLUMNS.map((column) => {
204
- return column.id === "target" ? { ...column, ...targetSizing, width: targetWidth } : column;
205
- });
206
- }
207
-
208
- function normalizedText(value: string | undefined): string | null {
209
- const trimmed = value?.trim();
210
- return trimmed ? trimmed.toLocaleLowerCase() : null;
211
- }
212
-
213
- function ratingTargetSortValue(row: AnalystRatingRecord): number | null {
214
- return row.currentPriceTarget ?? row.priorPriceTarget ?? null;
215
- }
216
-
217
- function ratingSortValue(row: AnalystRatingRecord, columnId: RatingColumnId): string | number | null {
218
- switch (columnId) {
219
- case "date":
220
- return normalizedText(row.date);
221
- case "firm":
222
- return normalizedText(row.firm);
223
- case "action":
224
- return normalizedText(row.action);
225
- case "current":
226
- return normalizedText(row.current);
227
- case "target":
228
- return ratingTargetSortValue(row);
229
- case "prior":
230
- return normalizedText(row.prior);
231
- }
232
- }
233
-
234
- export function sortRatingRows<T extends AnalystRatingRecord>(
235
- rows: readonly T[],
236
- preference: RatingSortPreference,
237
- ): T[] {
238
- return rows
239
- .map((row, index) => ({ row, index }))
240
- .sort((left, right) => {
241
- const primary = compareSortValues(
242
- ratingSortValue(left.row, preference.columnId),
243
- ratingSortValue(right.row, preference.columnId),
244
- preference.direction,
245
- );
246
- if (primary !== 0) return primary;
247
-
248
- const dateTieBreak = preference.columnId === "date"
249
- ? 0
250
- : compareSortValues(
251
- ratingSortValue(left.row, "date"),
252
- ratingSortValue(right.row, "date"),
253
- "desc",
254
- );
255
- if (dateTieBreak !== 0) return dateTieBreak;
256
-
257
- return left.index - right.index;
258
- })
259
- .map((entry) => entry.row);
260
- }
261
-
262
- export function nextRatingSortPreference(
263
- current: RatingSortPreference,
264
- columnId: string,
265
- ): RatingSortPreference {
266
- const typedColumnId = columnId as RatingColumnId;
267
- if (current.columnId !== typedColumnId) {
268
- return {
269
- columnId: typedColumnId,
270
- direction: DEFAULT_RATING_SORT_DIRECTIONS[typedColumnId] ?? "asc",
271
- };
272
- }
273
- return {
274
- columnId: typedColumnId,
275
- direction: current.direction === "asc" ? "desc" : "asc",
276
- };
277
- }
278
-
279
84
  export function AnalystResearchView({ focused, width, height }: { focused: boolean; width: number; height: number }) {
280
85
  const dataProvider = useAssetData();
281
86
  const { symbol, exchange } = useSymbolBinding();
282
87
  const [sortPreference, setSortPreference] = useState<RatingSortPreference>(DEFAULT_RATING_SORT);
283
88
  const loader = useCallback((nextSymbol: string, nextExchange: string, forceRefresh: boolean) => {
284
- if (!dataProvider?.getAnalystResearch) throw new Error("Analyst data unavailable");
285
- return dataProvider.getAnalystResearch(nextSymbol, nextExchange, forceRefresh ? { cacheMode: "refresh" } : undefined);
89
+ if (!dataProvider) throw new Error("Analyst data unavailable");
90
+ return loadAnalystResearch(
91
+ dataProvider,
92
+ nextSymbol,
93
+ nextExchange,
94
+ forceRefresh ? { cacheMode: "refresh" } : undefined,
95
+ );
286
96
  }, [dataProvider]);
287
97
  const { data, loading, error, reload } = useTickerRequest<AnalystResearchData>(loader, symbol, exchange);
288
98
  const rows = useMemo(() => sortRatingRows(data?.ratings ?? [], sortPreference), [data?.ratings, sortPreference]);
@@ -0,0 +1,12 @@
1
+ import type { DataProvider, MarketDataRequestContext } from "../../../types/data-provider";
2
+ import type { AnalystResearchData } from "../../../types/financials";
3
+
4
+ export async function loadAnalystResearch(
5
+ provider: DataProvider,
6
+ symbol: string,
7
+ exchange = "",
8
+ context?: MarketDataRequestContext,
9
+ ): Promise<AnalystResearchData> {
10
+ if (!provider.getAnalystResearch) throw new Error("Analyst data unavailable");
11
+ return provider.getAnalystResearch(symbol, exchange, context);
12
+ }
@@ -0,0 +1,93 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
3
+ import {
4
+ createEarningsEstimatesHeadless,
5
+ type EarningsEstimateSources,
6
+ } from "./headless";
7
+
8
+ const sources: EarningsEstimateSources = {
9
+ currency: "USD",
10
+ actions: {
11
+ symbol: "AMD",
12
+ currency: "USD",
13
+ dividends: [],
14
+ splits: [],
15
+ earnings: [{
16
+ date: "2026-07-28",
17
+ epsEstimate: 0.45,
18
+ epsActual: 0.48,
19
+ difference: 0.03,
20
+ surprisePercent: 6.67,
21
+ }],
22
+ },
23
+ estimates: {
24
+ symbol: "AMD",
25
+ currency: "USD",
26
+ recommendations: [],
27
+ ratings: [],
28
+ earningsEstimates: [
29
+ { date: "2026-09-30", period: "next_quarter", average: 1.2, analysts: 30 },
30
+ { date: "2026-12-31", period: "current_year", average: 4.8, analysts: 32 },
31
+ ],
32
+ revenueEstimates: [
33
+ { date: "2026-09-30", period: "next_quarter", average: 9_000_000_000, analysts: 28 },
34
+ { date: "2026-12-31", period: "current_year", average: 35_000_000_000, analysts: 29 },
35
+ ],
36
+ },
37
+ financials: {
38
+ annualStatements: [],
39
+ quarterlyStatements: [],
40
+ priceHistory: [],
41
+ },
42
+ };
43
+
44
+ function args(options: Record<string, string | number | boolean>): HeadlessPaneLoadArgs {
45
+ return {
46
+ rawArgument: "AMD",
47
+ argument: "AMD",
48
+ symbols: ["AMD"],
49
+ options,
50
+ };
51
+ }
52
+
53
+ const context = {} as HeadlessPaneContext;
54
+
55
+ describe("earnings estimates headless", () => {
56
+ test("maps the shared event model into estimate rows", async () => {
57
+ const definition = createEarningsEstimatesHeadless({
58
+ loadSources: async () => sources,
59
+ });
60
+
61
+ const result = await definition.load(args({ kind: "all", limit: 50 }), context);
62
+
63
+ expect(result.rows).toEqual(expect.arrayContaining([
64
+ expect.objectContaining({
65
+ status: "Q Est",
66
+ period: "next qtr",
67
+ qEps: 1.2,
68
+ qRevenue: 9_000_000_000,
69
+ detail: "30E/28R",
70
+ }),
71
+ expect.objectContaining({
72
+ status: "FY Est",
73
+ annualEps: 4.8,
74
+ annualRevenue: 35_000_000_000,
75
+ }),
76
+ expect.objectContaining({
77
+ status: "Earnings",
78
+ qEps: 0.48,
79
+ value: "+6.67%",
80
+ }),
81
+ ]));
82
+ });
83
+
84
+ test("applies kind and limit options", async () => {
85
+ const definition = createEarningsEstimatesHeadless({ loadSources: async () => sources });
86
+
87
+ const result = await definition.load(args({ kind: "estimates", limit: 1 }), context);
88
+
89
+ expect(result.rows).toHaveLength(1);
90
+ expect(["Q Est", "FY Est"]).toContain(result.rows[0]?.status);
91
+ expect(result.metadata).toMatchObject({ kind: "estimates", total: 2, truncated: true });
92
+ });
93
+ });
@@ -0,0 +1,161 @@
1
+ import type {
2
+ AnalystResearchData,
3
+ CorporateActionsData,
4
+ TickerFinancials,
5
+ } from "../../../types/financials";
6
+ import type {
7
+ HeadlessPaneContext,
8
+ HeadlessPaneDefinition,
9
+ HeadlessPaneLoadArgs,
10
+ HeadlessRowsResult,
11
+ } from "../../../types/plugin";
12
+ import { buildEventRows, type EventRow } from "./event-model";
13
+
14
+ const ESTIMATE_COLUMNS = [
15
+ { key: "date", header: "Date" },
16
+ { key: "status", header: "Event" },
17
+ { key: "period", header: "Period" },
18
+ { key: "qEps", header: "Q EPS", align: "right" as const },
19
+ { key: "qRevenue", header: "Q revenue", align: "right" as const },
20
+ { key: "annualEps", header: "Annual EPS", align: "right" as const },
21
+ { key: "annualRevenue", header: "Annual revenue", align: "right" as const },
22
+ { key: "value", header: "Value", align: "right" as const },
23
+ { key: "detail", header: "Detail" },
24
+ ];
25
+
26
+ export interface EarningsEstimateSources {
27
+ actions: CorporateActionsData | null;
28
+ estimates: AnalystResearchData | null;
29
+ financials: TickerFinancials | null;
30
+ currency: string;
31
+ errors?: string[];
32
+ }
33
+
34
+ export interface EarningsEstimatesHeadlessDependencies {
35
+ loadSources(
36
+ symbol: string,
37
+ context: HeadlessPaneContext,
38
+ ): Promise<EarningsEstimateSources>;
39
+ }
40
+
41
+ async function settledValue<T>(
42
+ promise: Promise<T> | null,
43
+ errors: string[],
44
+ label: string,
45
+ ): Promise<T | null> {
46
+ if (!promise) return null;
47
+ try {
48
+ return await promise;
49
+ } catch (error) {
50
+ errors.push(`${label}: ${error instanceof Error ? error.message : String(error)}`);
51
+ return null;
52
+ }
53
+ }
54
+
55
+ const defaultDependencies: EarningsEstimatesHeadlessDependencies = {
56
+ async loadSources(symbol, context) {
57
+ const errors: string[] = [];
58
+ const provider = context.marketData;
59
+ const [actions, estimates, financials] = await Promise.all([
60
+ settledValue(
61
+ provider.getCorporateActions?.(symbol, "") ?? null,
62
+ errors,
63
+ "corporate actions",
64
+ ),
65
+ settledValue(
66
+ provider.getAnalystResearch?.(symbol, "") ?? null,
67
+ errors,
68
+ "analyst estimates",
69
+ ),
70
+ settledValue(provider.getTickerFinancials(symbol, ""), errors, "financials"),
71
+ ]);
72
+ if (!actions && !estimates && !financials && errors.length > 0) {
73
+ throw new Error(errors.join("; "));
74
+ }
75
+ return {
76
+ actions,
77
+ estimates,
78
+ financials,
79
+ currency: actions?.currency ?? estimates?.currency ?? financials?.quote?.currency ?? "USD",
80
+ errors,
81
+ };
82
+ },
83
+ };
84
+
85
+ function matchesKind(row: EventRow, kind: string): boolean {
86
+ if (kind === "all") return true;
87
+ if (kind === "estimates") return row.status === "Q Est" || row.status === "FY Est";
88
+ return row.status === "Earnings" || row.status === "TTM";
89
+ }
90
+
91
+ export function projectEarningsEstimatesHeadless(
92
+ sources: EarningsEstimateSources,
93
+ args: HeadlessPaneLoadArgs,
94
+ ): HeadlessRowsResult {
95
+ const kind = String(args.options.kind ?? "all");
96
+ const limit = Number(args.options.limit ?? 50);
97
+ const matching = buildEventRows(
98
+ sources.actions,
99
+ sources.estimates,
100
+ sources.financials,
101
+ sources.currency,
102
+ ).filter((row) => matchesKind(row, kind));
103
+ const rows = matching.slice(0, limit).map((row) => ({ ...row }));
104
+
105
+ return {
106
+ columns: ESTIMATE_COLUMNS,
107
+ rows,
108
+ errors: sources.errors?.length ? sources.errors : undefined,
109
+ metadata: {
110
+ currency: sources.currency,
111
+ kind,
112
+ total: matching.length,
113
+ returned: rows.length,
114
+ truncated: rows.length < matching.length,
115
+ },
116
+ };
117
+ }
118
+
119
+ export function createEarningsEstimatesHeadless(
120
+ dependencies: EarningsEstimatesHeadlessDependencies = defaultDependencies,
121
+ ): HeadlessPaneDefinition<"rows"> {
122
+ return {
123
+ shape: "rows",
124
+ argument: {
125
+ kind: "ticker",
126
+ placeholder: "ticker",
127
+ description: "Company symbol.",
128
+ },
129
+ options: [
130
+ {
131
+ key: "kind",
132
+ description: "Estimate or reported rows to include.",
133
+ type: "enum",
134
+ values: [
135
+ { value: "all" },
136
+ { value: "estimates" },
137
+ { value: "reported" },
138
+ ],
139
+ defaultValue: "all",
140
+ },
141
+ {
142
+ key: "limit",
143
+ description: "Maximum rows to return.",
144
+ type: "integer",
145
+ defaultValue: 50,
146
+ minimum: 1,
147
+ maximum: 200,
148
+ },
149
+ ],
150
+ columns: ESTIMATE_COLUMNS,
151
+ describe: (args) => `Earnings Estimates | ${String(args.argument)}`,
152
+ async load(args, context) {
153
+ return projectEarningsEstimatesHeadless(
154
+ await dependencies.loadSources(String(args.argument), context),
155
+ args,
156
+ );
157
+ },
158
+ };
159
+ }
160
+
161
+ export const earningsEstimatesHeadless = createEarningsEstimatesHeadless();
@@ -5,9 +5,13 @@ import { AnalystResearchView } from "./analyst-pane";
5
5
  import { CorporateActionsView } from "./corporate-actions-pane";
6
6
  import { EquityDiagnosticView } from "./equity-diagnostic-pane";
7
7
  import { RelativeValuationPane } from "./relative-valuation-pane";
8
+ import { analystResearchHeadless } from "./analyst-headless";
8
9
  import { eventsHeadless } from "./events-headless";
10
+ import { earningsEstimatesHeadless } from "./headless";
9
11
 
12
+ export { analystResearchHeadless } from "./analyst-headless";
10
13
  export { eventsHeadless } from "./events-headless";
14
+ export { earningsEstimatesHeadless } from "./headless";
11
15
 
12
16
  function EarningsEstimatesPane(props: { focused: boolean; width: number; height: number }) {
13
17
  return (
@@ -98,15 +102,18 @@ export const researchModule: PluginModule = {
98
102
  ],
99
103
 
100
104
  paneTemplates: [
101
- createTickerSurfacePaneTemplate({
102
- id: "analyst-research-pane",
103
- paneId: "analyst-research",
104
- label: "Analyst Research",
105
- description: "Price targets, recommendations, and recent analyst actions.",
106
- keywords: ["analyst", "research", "ratings", "target", "anr"],
107
- shortcut: "ANR",
108
- publicShare: true,
109
- }),
105
+ {
106
+ ...createTickerSurfacePaneTemplate({
107
+ id: "analyst-research-pane",
108
+ paneId: "analyst-research",
109
+ label: "Analyst Research",
110
+ description: "Price targets, recommendations, and recent analyst actions.",
111
+ keywords: ["analyst", "research", "ratings", "target", "anr"],
112
+ shortcut: "ANR",
113
+ publicShare: true,
114
+ }),
115
+ headless: analystResearchHeadless,
116
+ },
110
117
  createTickerSurfacePaneTemplate({
111
118
  id: "equity-diagnostic-pane",
112
119
  paneId: "equity-diagnostic",
@@ -125,15 +132,18 @@ export const researchModule: PluginModule = {
125
132
  shortcut: "EVT",
126
133
  publicShare: true,
127
134
  }),
128
- createTickerSurfacePaneTemplate({
129
- id: "earnings-estimates-pane",
130
- paneId: "earnings-estimates",
131
- label: "Earnings Estimates",
132
- description: "EPS and revenue estimates with reported earnings.",
133
- keywords: ["earnings", "estimates", "ee", "analyst", "eps", "revenue", "events"],
134
- shortcut: "EE",
135
- publicShare: true,
136
- }),
135
+ {
136
+ ...createTickerSurfacePaneTemplate({
137
+ id: "earnings-estimates-pane",
138
+ paneId: "earnings-estimates",
139
+ label: "Earnings Estimates",
140
+ description: "EPS and revenue estimates with reported earnings.",
141
+ keywords: ["earnings", "estimates", "ee", "analyst", "eps", "revenue", "events"],
142
+ shortcut: "EE",
143
+ publicShare: true,
144
+ }),
145
+ headless: earningsEstimatesHeadless,
146
+ },
137
147
  {
138
148
  id: "relative-valuation-pane",
139
149
  paneId: "relative-valuation",
@@ -0,0 +1,16 @@
1
+ import type {
2
+ DataProvider,
3
+ MarketDataRequestContext,
4
+ SecFilingItem,
5
+ } from "../../../types/data-provider";
6
+
7
+ export async function loadSecFilings(
8
+ provider: DataProvider,
9
+ symbol: string,
10
+ count: number,
11
+ exchange = "",
12
+ context?: MarketDataRequestContext,
13
+ ): Promise<SecFilingItem[]> {
14
+ if (!provider.getSecFilings) throw new Error("SEC filing data unavailable");
15
+ return provider.getSecFilings(symbol, count, exchange, context);
16
+ }