gloomberb 0.12.0 → 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/README.md +2 -0
- package/package.json +1 -2
- package/src/api-client/auth.ts +2 -1
- package/src/api-client/index.test.ts +34 -0
- package/src/api-client/index.ts +10 -2
- package/src/api-client/request.ts +4 -1
- package/src/api-client/research-activity.ts +61 -0
- package/src/api-client/types.ts +2 -1
- package/src/cli/commands/plugins.ts +4 -3
- package/src/cli/desktop-pane-shot.ts +79 -10
- package/src/cli/pane-functions/capabilities.ts +76 -23
- 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/dom.test.ts +41 -0
- package/src/cli/pane-functions/dom.ts +199 -0
- package/src/cli/pane-functions/index.test.ts +13 -2
- package/src/cli/pane-functions/index.ts +9 -5
- package/src/cli/pane-functions/intraday-shot.test.ts +307 -0
- package/src/cli/pane-functions/intraday-shot.ts +334 -0
- package/src/cli/pane-functions/report.test.ts +29 -1
- package/src/cli/pane-functions/report.ts +68 -7
- package/src/cli/pane-functions/screenshot.ts +195 -42
- package/src/cli/restore-plugins.ts +10 -3
- package/src/components/chart/composite/composite-chart.test.tsx +68 -1
- package/src/components/chart/composite/composite-chart.tsx +87 -21
- package/src/components/chart/composite/rasterizer.ts +33 -0
- package/src/components/chart/composite/renderers.test.ts +51 -4
- package/src/components/chart/composite/scene.test.ts +128 -2
- package/src/components/chart/composite/scene.ts +92 -16
- package/src/components/chart/composite/text-renderer.ts +13 -0
- package/src/components/chart/composite/time-scale.ts +30 -10
- package/src/components/chart/composite/types.ts +26 -0
- package/src/components/command-bar/assist/runtime.ts +38 -30
- package/src/components/command-bar/surface/assist.test.tsx +63 -9
- package/src/components/command-bar/surface/test-harness.tsx +23 -4
- package/src/components/onboarding/account-step/chooser-panel.tsx +2 -2
- package/src/components/onboarding/account-step/qr-panel.tsx +2 -10
- package/src/components/onboarding/onboarding-wizard.test.tsx +38 -12
- package/src/components/onboarding/onboarding-wizard.tsx +49 -14
- package/src/data/config/store/index.test.ts +23 -0
- package/src/data/config/store/node.ts +7 -1
- package/src/data/config/store/normalize.ts +4 -0
- package/src/news/types.ts +1 -0
- package/src/plugins/builtin/cds/client.test.ts +20 -0
- package/src/plugins/builtin/cds/client.ts +13 -3
- package/src/plugins/builtin/cds/headless.test.ts +30 -0
- package/src/plugins/builtin/cds/headless.ts +113 -0
- package/src/plugins/builtin/cds/index.tsx +4 -0
- package/src/plugins/builtin/chart-composer/cli-options.ts +4 -1
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- 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 +190 -0
- package/src/plugins/builtin/cloud/askg/protocol.ts +286 -0
- 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/auth-dialog.tsx +7 -1
- package/src/plugins/builtin/cloud/device-signin-dialog.tsx +21 -8
- package/src/plugins/builtin/cloud/plugin.tsx +2 -0
- package/src/plugins/builtin/cloud/verification-panel.tsx +56 -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/credit-conditions/client.ts +1 -1
- package/src/plugins/builtin/credit-conditions/headless.test.ts +31 -0
- package/src/plugins/builtin/credit-conditions/headless.ts +55 -0
- package/src/plugins/builtin/credit-conditions/index.tsx +4 -0
- 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/pane.tsx +4 -0
- package/src/plugins/builtin/earnings-calls/transcript-view.tsx +13 -11
- package/src/plugins/builtin/fear-greed/headless.test.ts +53 -0
- package/src/plugins/builtin/fear-greed/headless.ts +84 -0
- package/src/plugins/builtin/fear-greed/index.tsx +4 -0
- 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/market-movers/client.ts +76 -0
- package/src/plugins/builtin/market-movers/headless.test.ts +34 -0
- package/src/plugins/builtin/market-movers/headless.ts +73 -0
- package/src/plugins/builtin/market-movers/index.tsx +11 -51
- 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/corporate-actions-pane.tsx +3 -237
- package/src/plugins/builtin/research/event-model.ts +245 -0
- package/src/plugins/builtin/research/events-headless.test.ts +32 -0
- package/src/plugins/builtin/research/events-headless.ts +87 -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 +32 -18
- package/src/plugins/builtin/research-search/pane.tsx +4 -0
- package/src/plugins/builtin/scanner/headless.test.ts +59 -0
- package/src/plugins/builtin/scanner/headless.ts +125 -0
- package/src/plugins/builtin/scanner/index.ts +4 -0
- 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/sectors/client.ts +54 -0
- package/src/plugins/builtin/sectors/headless.test.ts +28 -0
- package/src/plugins/builtin/sectors/headless.ts +80 -0
- package/src/plugins/builtin/sectors/index.tsx +6 -51
- package/src/plugins/builtin/shared/cloud-upgrade.ts +8 -4
- 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/builtin/ticker-detail/pane.tsx +14 -0
- package/src/plugins/builtin/treasury-auctions/headless.test.ts +34 -0
- package/src/plugins/builtin/treasury-auctions/headless.ts +95 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +4 -0
- package/src/plugins/builtin/volatility/client.ts +16 -3
- package/src/plugins/builtin/volatility/headless.test.ts +35 -0
- package/src/plugins/builtin/volatility/headless.ts +75 -0
- package/src/plugins/builtin/volatility/index.tsx +4 -0
- package/src/plugins/builtin/world-indices/client.ts +43 -0
- package/src/plugins/builtin/world-indices/headless.test.ts +32 -0
- package/src/plugins/builtin/world-indices/headless.ts +109 -0
- package/src/plugins/builtin/world-indices/index.tsx +4 -0
- package/src/plugins/builtin/yield-curve/headless.test.ts +24 -0
- package/src/plugins/builtin/yield-curve/headless.ts +67 -0
- package/src/plugins/builtin/yield-curve/index.tsx +6 -8
- package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -6
- package/src/plugins/bundle.test.ts +58 -1
- package/src/plugins/bundle.ts +4 -2
- package/src/plugins/catalog-backend.test.ts +26 -0
- package/src/plugins/catalog-backend.ts +9 -2
- package/src/plugins/catalog-browser.test.ts +0 -13
- package/src/plugins/catalog-browser.ts +4 -0
- package/src/plugins/catalog-manifest.test.ts +15 -0
- package/src/plugins/catalog-manifest.ts +12 -5
- package/src/plugins/catalog-ui.ts +0 -6
- package/src/plugins/host-contract.ts +9 -0
- package/src/plugins/host-modules.ts +12 -0
- package/src/plugins/loader.ts +63 -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/plugins/seed.ts +3 -0
- package/src/public/broker.ts +6 -0
- package/src/remote/schema.test.ts +65 -0
- package/src/remote/schema.ts +322 -39
- package/src/remote/types.ts +49 -2
- package/src/renderers/browser/app-services.ts +3 -0
- package/src/renderers/browser/config-host.ts +24 -8
- package/src/renderers/browser/deeplink-bridge.test.ts +11 -0
- package/src/renderers/browser/deeplink-bridge.ts +15 -3
- package/src/renderers/browser/main.tsx +3 -1
- package/src/renderers/browser/research-entry.test.ts +8 -0
- package/src/renderers/browser/research-entry.ts +7 -0
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +2 -2
- package/src/renderers/electrobun/bun/desktop/initialization.ts +10 -3
- package/src/renderers/electrobun/bun/external-plugins.ts +20 -4
- package/src/renderers/electrobun/view/build-assets.ts +0 -2
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +63 -10
- package/src/sources/gloomberb-cloud/news.ts +1 -0
- package/src/sync/core-contributors.test.ts +111 -0
- package/src/sync/core-contributors.ts +155 -1
- package/src/time-series/resolve.test.ts +118 -1
- package/src/time-series/resolve.ts +14 -16
- package/src/types/config.ts +2 -0
- package/src/plugins/broker-sync/broker-sync.test.ts +0 -188
- package/src/plugins/broker-sync/native-loader.ts +0 -10
- package/src/plugins/broker-sync/normalize.ts +0 -308
- package/src/plugins/broker-sync/public.ts +0 -122
- package/src/plugins/broker-sync/robinhood-native.ts +0 -367
- package/src/plugins/broker-sync/robinhood.ts +0 -114
- package/src/plugins/broker-sync/simplefin-native.ts +0 -201
- package/src/plugins/broker-sync/simplefin.ts +0 -70
- package/src/renderers/electrobun/view/native-stubs/broker-sync-native-loader.ts +0 -7
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { DataTableColumn } from "../../../components";
|
|
2
|
+
import type { AnalystRatingRecord, AnalystResearchData } from "../../../types/financials";
|
|
3
|
+
import { formatCurrency, formatNumber } from "../../../utils/format";
|
|
4
|
+
import { compareSortValues, type SortDirection } from "../../../utils/sort-values";
|
|
5
|
+
|
|
6
|
+
function compactPeriod(period: string): string {
|
|
7
|
+
return period
|
|
8
|
+
.replace("current ", "")
|
|
9
|
+
.replace("previous ", "prev ")
|
|
10
|
+
.replace("next_", "next ")
|
|
11
|
+
.replace(/_/g, " ");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function targetUpside(target: AnalystResearchData["priceTarget"]): number | undefined {
|
|
15
|
+
if (!target?.average || !target.current) return undefined;
|
|
16
|
+
return (target.average - target.current) / target.current;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function latestRecommendation(data: AnalystResearchData | null) {
|
|
20
|
+
return data?.recommendations[0] ?? null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function recommendationTotal(data: AnalystResearchData | null): number {
|
|
24
|
+
const rec = latestRecommendation(data);
|
|
25
|
+
if (!rec) return 0;
|
|
26
|
+
return (rec.strongBuy ?? 0) + (rec.buy ?? 0) + (rec.hold ?? 0) + (rec.sell ?? 0) + (rec.strongSell ?? 0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatRatingLabel(value: number | undefined): string {
|
|
30
|
+
return value == null ? "-" : `${formatNumber(value, 1)}/10`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function formatPriceTarget(value: number | undefined, currency: string): string {
|
|
34
|
+
if (value == null) return "-";
|
|
35
|
+
return formatCurrency(value, currency)
|
|
36
|
+
.replace(/\.00\b/, "")
|
|
37
|
+
.replace(/(\.\d)0\b/, "$1");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const TARGET_SEPARATOR = " → ";
|
|
41
|
+
|
|
42
|
+
export interface RatingTargetColumnSizing {
|
|
43
|
+
targetPriorWidth: number;
|
|
44
|
+
targetCurrentWidth: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function formatRatingTarget(
|
|
48
|
+
row: AnalystResearchData["ratings"][number],
|
|
49
|
+
currency: string,
|
|
50
|
+
sizing?: Partial<RatingTargetColumnSizing>,
|
|
51
|
+
): string {
|
|
52
|
+
const current = row.currentPriceTarget;
|
|
53
|
+
const prior = row.priorPriceTarget;
|
|
54
|
+
if (current == null && prior == null) return "-";
|
|
55
|
+
if (current == null) return ` ${formatPriceTarget(prior, currency)}`;
|
|
56
|
+
if (prior == null) return ` ${formatPriceTarget(current, currency)}`;
|
|
57
|
+
|
|
58
|
+
const priorText = formatPriceTarget(prior, currency).padStart(sizing?.targetPriorWidth ?? 0);
|
|
59
|
+
const currentText = formatPriceTarget(current, currency).padEnd(sizing?.targetCurrentWidth ?? 0);
|
|
60
|
+
return ` ${priorText}${TARGET_SEPARATOR}${currentText}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function ratingTargetDelta(row: AnalystResearchData["ratings"][number]): number | null {
|
|
64
|
+
if (row.currentPriceTarget == null || row.priorPriceTarget == null) return null;
|
|
65
|
+
return row.currentPriceTarget - row.priorPriceTarget;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type RatingColumnId = "date" | "firm" | "action" | "current" | "target" | "prior";
|
|
69
|
+
export type RatingColumn = DataTableColumn & { id: RatingColumnId } & Partial<RatingTargetColumnSizing>;
|
|
70
|
+
|
|
71
|
+
export interface RatingSortPreference {
|
|
72
|
+
columnId: RatingColumnId;
|
|
73
|
+
direction: SortDirection;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const DEFAULT_RATING_SORT: RatingSortPreference = {
|
|
77
|
+
columnId: "date",
|
|
78
|
+
direction: "desc",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const DEFAULT_RATING_SORT_DIRECTIONS: Record<RatingColumnId, SortDirection> = {
|
|
82
|
+
date: "desc",
|
|
83
|
+
firm: "asc",
|
|
84
|
+
action: "asc",
|
|
85
|
+
current: "asc",
|
|
86
|
+
target: "desc",
|
|
87
|
+
prior: "asc",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const BASE_RATING_COLUMNS: RatingColumn[] = [
|
|
91
|
+
{ id: "date", label: "DATE", width: 10, align: "left" },
|
|
92
|
+
{ id: "firm", label: "FIRM", width: 20, align: "left" },
|
|
93
|
+
{ id: "action", label: "ACTION", width: 10, align: "left" },
|
|
94
|
+
{ id: "current", label: "RATING", width: 13, align: "left" },
|
|
95
|
+
{ id: "target", label: "TARGET", width: 13, align: "left" },
|
|
96
|
+
{ id: "prior", label: "PRIOR", width: 13, align: "left" },
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
export function buildRatingColumns(
|
|
100
|
+
rows: readonly AnalystRatingRecord[],
|
|
101
|
+
currency: string,
|
|
102
|
+
): RatingColumn[] {
|
|
103
|
+
const targetSizing = rows.reduce<RatingTargetColumnSizing>(
|
|
104
|
+
(sizing, row) => ({
|
|
105
|
+
targetPriorWidth: Math.max(
|
|
106
|
+
sizing.targetPriorWidth,
|
|
107
|
+
row.priorPriceTarget == null ? 0 : formatPriceTarget(row.priorPriceTarget, currency).length,
|
|
108
|
+
),
|
|
109
|
+
targetCurrentWidth: Math.max(
|
|
110
|
+
sizing.targetCurrentWidth,
|
|
111
|
+
row.currentPriceTarget == null ? 0 : formatPriceTarget(row.currentPriceTarget, currency).length,
|
|
112
|
+
),
|
|
113
|
+
}),
|
|
114
|
+
{ targetPriorWidth: 0, targetCurrentWidth: 0 },
|
|
115
|
+
);
|
|
116
|
+
const targetWidth = rows.reduce(
|
|
117
|
+
(width, row) => Math.max(width, formatRatingTarget(row, currency, targetSizing).length),
|
|
118
|
+
BASE_RATING_COLUMNS.find((column) => column.id === "target")?.width ?? 13,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
return BASE_RATING_COLUMNS.map((column) => {
|
|
122
|
+
return column.id === "target" ? { ...column, ...targetSizing, width: targetWidth } : column;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizedText(value: string | undefined): string | null {
|
|
127
|
+
const trimmed = value?.trim();
|
|
128
|
+
return trimmed ? trimmed.toLocaleLowerCase() : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function ratingTargetSortValue(row: AnalystRatingRecord): number | null {
|
|
132
|
+
return row.currentPriceTarget ?? row.priorPriceTarget ?? null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function ratingSortValue(row: AnalystRatingRecord, columnId: RatingColumnId): string | number | null {
|
|
136
|
+
switch (columnId) {
|
|
137
|
+
case "date":
|
|
138
|
+
return normalizedText(row.date);
|
|
139
|
+
case "firm":
|
|
140
|
+
return normalizedText(row.firm);
|
|
141
|
+
case "action":
|
|
142
|
+
return normalizedText(row.action);
|
|
143
|
+
case "current":
|
|
144
|
+
return normalizedText(row.current);
|
|
145
|
+
case "target":
|
|
146
|
+
return ratingTargetSortValue(row);
|
|
147
|
+
case "prior":
|
|
148
|
+
return normalizedText(row.prior);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function sortRatingRows<T extends AnalystRatingRecord>(
|
|
153
|
+
rows: readonly T[],
|
|
154
|
+
preference: RatingSortPreference,
|
|
155
|
+
): T[] {
|
|
156
|
+
return rows
|
|
157
|
+
.map((row, index) => ({ row, index }))
|
|
158
|
+
.sort((left, right) => {
|
|
159
|
+
const primary = compareSortValues(
|
|
160
|
+
ratingSortValue(left.row, preference.columnId),
|
|
161
|
+
ratingSortValue(right.row, preference.columnId),
|
|
162
|
+
preference.direction,
|
|
163
|
+
);
|
|
164
|
+
if (primary !== 0) return primary;
|
|
165
|
+
|
|
166
|
+
const dateTieBreak = preference.columnId === "date"
|
|
167
|
+
? 0
|
|
168
|
+
: compareSortValues(
|
|
169
|
+
ratingSortValue(left.row, "date"),
|
|
170
|
+
ratingSortValue(right.row, "date"),
|
|
171
|
+
"desc",
|
|
172
|
+
);
|
|
173
|
+
if (dateTieBreak !== 0) return dateTieBreak;
|
|
174
|
+
|
|
175
|
+
return left.index - right.index;
|
|
176
|
+
})
|
|
177
|
+
.map((entry) => entry.row);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function nextRatingSortPreference(
|
|
181
|
+
current: RatingSortPreference,
|
|
182
|
+
columnId: string,
|
|
183
|
+
): RatingSortPreference {
|
|
184
|
+
const typedColumnId = columnId as RatingColumnId;
|
|
185
|
+
if (current.columnId !== typedColumnId) {
|
|
186
|
+
return {
|
|
187
|
+
columnId: typedColumnId,
|
|
188
|
+
direction: DEFAULT_RATING_SORT_DIRECTIONS[typedColumnId] ?? "asc",
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
columnId: typedColumnId,
|
|
193
|
+
direction: current.direction === "asc" ? "desc" : "asc",
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function buildAnalystSummaryLines(data: AnalystResearchData | null): string[] {
|
|
198
|
+
if (!data) return [];
|
|
199
|
+
|
|
200
|
+
const target = data.priceTarget;
|
|
201
|
+
const currency = target?.currency ?? data.currency ?? "USD";
|
|
202
|
+
const price = (value: number | undefined) => value != null ? formatCurrency(value, currency) : "-";
|
|
203
|
+
const rec = latestRecommendation(data);
|
|
204
|
+
const total = recommendationTotal(data);
|
|
205
|
+
const lines: string[] = [];
|
|
206
|
+
|
|
207
|
+
if (target) {
|
|
208
|
+
lines.push(`low ${price(target.low)} med ${price(target.median)} high ${price(target.high)}`);
|
|
209
|
+
}
|
|
210
|
+
if (data.recommendationRating != null || rec || total > 0) {
|
|
211
|
+
lines.push([
|
|
212
|
+
data.recommendationRating != null ? `rating ${formatRatingLabel(data.recommendationRating)}` : null,
|
|
213
|
+
rec ? `SB ${rec.strongBuy ?? 0} B ${rec.buy ?? 0} H ${rec.hold ?? 0} S ${(rec.sell ?? 0) + (rec.strongSell ?? 0)}` : null,
|
|
214
|
+
total > 0 ? `${total} analysts${rec?.period ? ` (${compactPeriod(rec.period)})` : ""}` : null,
|
|
215
|
+
].filter(Boolean).join(" "));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return lines;
|
|
219
|
+
}
|
|
@@ -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
|
+
}
|