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.
- package/package.json +1 -1
- package/src/api-client/index.test.ts +34 -0
- package/src/api-client/request.ts +4 -1
- package/src/cli/pane-functions/cloud-session.test.ts +54 -0
- package/src/cli/pane-functions/cloud-session.ts +38 -0
- package/src/cli/pane-functions/index.ts +5 -1
- package/src/cli/pane-functions/screenshot.ts +2 -19
- package/src/components/command-bar/assist/runtime.ts +38 -30
- package/src/components/command-bar/surface/assist.test.tsx +43 -0
- package/src/news/types.ts +1 -0
- package/src/plugins/builtin/chat/controller/channels.ts +2 -1
- package/src/plugins/builtin/chat/controller/index.ts +13 -5
- package/src/plugins/builtin/chat/controller/realtime.ts +24 -0
- package/src/plugins/builtin/chat/controller/session-runtime.ts +76 -8
- package/src/plugins/builtin/chat/controller/state.ts +1 -0
- package/src/plugins/builtin/chat/controller.test.ts +260 -4
- package/src/plugins/builtin/cloud/askg/executor.test.ts +160 -0
- package/src/plugins/builtin/cloud/askg/executor.ts +429 -0
- package/src/plugins/builtin/cloud/askg/manifest.test.ts +153 -0
- package/src/plugins/builtin/cloud/askg/manifest.ts +314 -0
- package/src/plugins/builtin/cloud/askg/protocol.test.ts +3 -2
- package/src/plugins/builtin/cloud/askg/protocol.ts +16 -4
- package/src/plugins/builtin/cloud/askg/undo.test.ts +44 -0
- package/src/plugins/builtin/cloud/askg/undo.ts +277 -0
- package/src/plugins/builtin/cloud/askg/verify.ts +130 -0
- package/src/plugins/builtin/cloud/plugin.tsx +2 -0
- package/src/plugins/builtin/congress-trades/client.ts +11 -0
- package/src/plugins/builtin/congress-trades/detail.tsx +3 -3
- package/src/plugins/builtin/congress-trades/headless.test.ts +98 -0
- package/src/plugins/builtin/congress-trades/headless.ts +145 -0
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -3
- package/src/plugins/builtin/dividend-yield/headless.test.ts +88 -0
- package/src/plugins/builtin/dividend-yield/headless.ts +135 -0
- package/src/plugins/builtin/dividend-yield/index.tsx +12 -8
- package/src/plugins/builtin/dividend-yield/model.ts +4 -22
- package/src/plugins/builtin/dividend-yield/view.ts +17 -0
- package/src/plugins/builtin/earnings/headless.test.ts +82 -0
- package/src/plugins/builtin/earnings/headless.ts +162 -0
- package/src/plugins/builtin/earnings/index.tsx +2 -0
- package/src/plugins/builtin/earnings-calls/data.ts +29 -8
- package/src/plugins/builtin/earnings-calls/headless.test.ts +192 -0
- package/src/plugins/builtin/earnings-calls/headless.ts +300 -0
- package/src/plugins/builtin/earnings-calls/index.tsx +19 -9
- package/src/plugins/builtin/earnings-calls/model.ts +238 -0
- package/src/plugins/builtin/earnings-calls/transcript-view.tsx +8 -9
- package/src/plugins/builtin/holders/client.ts +29 -0
- package/src/plugins/builtin/holders/headless.test.ts +58 -0
- package/src/plugins/builtin/holders/headless.ts +162 -0
- package/src/plugins/builtin/holders/index.tsx +14 -8
- package/src/plugins/builtin/holders/pane.tsx +7 -1
- package/src/plugins/builtin/insider/client.ts +50 -0
- package/src/plugins/builtin/insider/headless.test.ts +122 -0
- package/src/plugins/builtin/insider/headless.ts +117 -0
- package/src/plugins/builtin/insider/index.tsx +21 -50
- package/src/plugins/builtin/insider/insider-data.ts +1 -1
- package/src/plugins/builtin/insider/model.ts +72 -0
- package/src/plugins/builtin/ipo-calendar/client.ts +15 -0
- package/src/plugins/builtin/ipo-calendar/headless.test.ts +89 -0
- package/src/plugins/builtin/ipo-calendar/headless.ts +127 -0
- package/src/plugins/builtin/ipo-calendar/index.tsx +2 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +3 -15
- package/src/plugins/builtin/news/headless.test.ts +138 -0
- package/src/plugins/builtin/news/headless.ts +190 -0
- package/src/plugins/builtin/news/index.tsx +13 -9
- package/src/plugins/builtin/news/wire/index.tsx +2 -1
- package/src/plugins/builtin/research/analyst-headless.test.ts +60 -0
- package/src/plugins/builtin/research/analyst-headless.ts +172 -0
- package/src/plugins/builtin/research/analyst-model.ts +219 -0
- package/src/plugins/builtin/research/analyst-pane.tsx +31 -221
- package/src/plugins/builtin/research/client.ts +12 -0
- package/src/plugins/builtin/research/headless.test.ts +93 -0
- package/src/plugins/builtin/research/headless.ts +161 -0
- package/src/plugins/builtin/research/index.tsx +28 -18
- package/src/plugins/builtin/sec/client.ts +16 -0
- package/src/plugins/builtin/sec/headless.test.ts +72 -0
- package/src/plugins/builtin/sec/headless.ts +69 -0
- package/src/plugins/builtin/sec/index.tsx +20 -75
- package/src/plugins/builtin/sec/model.ts +89 -0
- package/src/plugins/builtin/short-interest/client.ts +5 -2
- package/src/plugins/builtin/short-interest/headless.test.ts +96 -0
- package/src/plugins/builtin/short-interest/headless.ts +111 -0
- package/src/plugins/builtin/short-interest/index.tsx +15 -9
- package/src/plugins/builtin/thirteenf/data.ts +13 -11
- package/src/plugins/builtin/thirteenf/headless.test.ts +112 -0
- package/src/plugins/builtin/thirteenf/headless.ts +286 -0
- package/src/plugins/builtin/thirteenf/index.tsx +4 -0
- package/src/plugins/builtin/thirteenf/model.ts +13 -0
- package/src/plugins/builtin/thirteenf/table.ts +3 -15
- package/src/plugins/prediction-markets/headless.test.ts +119 -0
- package/src/plugins/prediction-markets/headless.ts +255 -0
- package/src/plugins/prediction-markets/index.tsx +2 -0
- 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 {
|
|
9
|
+
import type { AnalystResearchData } from "../../../types/financials";
|
|
11
10
|
import { blendHex, colors, priceColor } from "../../../theme/colors";
|
|
12
|
-
import { formatCurrency,
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
285
|
-
return
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
+
}
|