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,138 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { CloudNewsPayload } from "../../../api-client";
|
|
3
|
+
import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
|
|
4
|
+
import type { NewsQuery } from "../../../news/types";
|
|
5
|
+
import {
|
|
6
|
+
createNewsFeedHeadless,
|
|
7
|
+
createTickerNewsHeadless,
|
|
8
|
+
type NewsHeadlessDependencies,
|
|
9
|
+
} from "./headless";
|
|
10
|
+
|
|
11
|
+
function story(
|
|
12
|
+
id: string,
|
|
13
|
+
sentiment: CloudNewsPayload["sentiment"],
|
|
14
|
+
importance: number,
|
|
15
|
+
publishedAt: string,
|
|
16
|
+
): CloudNewsPayload {
|
|
17
|
+
return {
|
|
18
|
+
id,
|
|
19
|
+
headline: `${id} headline`,
|
|
20
|
+
summary: `${id} summary`,
|
|
21
|
+
category: "markets",
|
|
22
|
+
sentiment,
|
|
23
|
+
sectors: ["technology"],
|
|
24
|
+
firstPublishedAt: publishedAt,
|
|
25
|
+
lastPublishedAt: publishedAt,
|
|
26
|
+
firstSeenAt: publishedAt,
|
|
27
|
+
lastSeenAt: publishedAt,
|
|
28
|
+
primaryUrl: `https://example.com/${id}`,
|
|
29
|
+
primarySource: "Example Wire",
|
|
30
|
+
scores: {
|
|
31
|
+
importance,
|
|
32
|
+
urgency: importance - 1,
|
|
33
|
+
marketImpact: importance - 2,
|
|
34
|
+
novelty: 7,
|
|
35
|
+
confidence: 90,
|
|
36
|
+
},
|
|
37
|
+
flags: { breaking: importance > 80 },
|
|
38
|
+
variantCount: 1,
|
|
39
|
+
sourceCount: 1,
|
|
40
|
+
sources: ["Example Wire"],
|
|
41
|
+
entities: [],
|
|
42
|
+
tickerLinks: [{
|
|
43
|
+
symbol: "AMD",
|
|
44
|
+
exchange: "NASDAQ",
|
|
45
|
+
canonicalTicker: "AMD",
|
|
46
|
+
relationType: "issuer",
|
|
47
|
+
displayTier: "primary",
|
|
48
|
+
confidence: 1,
|
|
49
|
+
relevanceScore: 1,
|
|
50
|
+
}],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const stories = [
|
|
55
|
+
story("newer", "positive", 95, "2026-09-04T20:00:00.000Z"),
|
|
56
|
+
story("older", "negative", 70, "2026-09-04T18:00:00.000Z"),
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
function args(
|
|
60
|
+
symbol: string | null,
|
|
61
|
+
options: Record<string, string | number | boolean>,
|
|
62
|
+
): HeadlessPaneLoadArgs {
|
|
63
|
+
return {
|
|
64
|
+
rawArgument: symbol ?? "",
|
|
65
|
+
argument: symbol,
|
|
66
|
+
symbols: symbol ? [symbol] : [],
|
|
67
|
+
options,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const context = {} as HeadlessPaneContext;
|
|
72
|
+
|
|
73
|
+
function dependencies(seen: NewsQuery[]): NewsHeadlessDependencies {
|
|
74
|
+
return {
|
|
75
|
+
loadNews: async (query) => {
|
|
76
|
+
seen.push(query);
|
|
77
|
+
return { items: stories, nextCursor: "page-2" };
|
|
78
|
+
},
|
|
79
|
+
now: () => new Date("2026-09-04T21:00:00.000Z"),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe("news feed headless", () => {
|
|
84
|
+
test("maps cloud stories into a point-in-time snapshot", async () => {
|
|
85
|
+
const seen: NewsQuery[] = [];
|
|
86
|
+
const definition = createNewsFeedHeadless(dependencies(seen));
|
|
87
|
+
|
|
88
|
+
const result = await definition.load(
|
|
89
|
+
args(null, { sentiment: "any", minImportance: 0, limit: 50 }),
|
|
90
|
+
context,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
expect(result.asOf).toBe("2026-09-04T21:00:00.000Z");
|
|
94
|
+
expect(result.items[0]).toMatchObject({
|
|
95
|
+
id: "newer",
|
|
96
|
+
headline: "newer headline",
|
|
97
|
+
tickers: ["AMD"],
|
|
98
|
+
sentiment: "positive",
|
|
99
|
+
importance: 95,
|
|
100
|
+
breaking: true,
|
|
101
|
+
sourceCount: 1,
|
|
102
|
+
});
|
|
103
|
+
expect(seen[0]).toMatchObject({ feed: "latest", limit: 50 });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("applies sentiment and importance options", async () => {
|
|
107
|
+
const definition = createNewsFeedHeadless(dependencies([]));
|
|
108
|
+
|
|
109
|
+
const result = await definition.load(
|
|
110
|
+
args(null, { sentiment: "positive", minImportance: 90, limit: 50 }),
|
|
111
|
+
context,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(result.items.map((item) => item.id)).toEqual(["newer"]);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("ticker news headless", () => {
|
|
119
|
+
test("requests the ticker feed and applies the row limit", async () => {
|
|
120
|
+
const seen: NewsQuery[] = [];
|
|
121
|
+
const definition = createTickerNewsHeadless(dependencies(seen));
|
|
122
|
+
|
|
123
|
+
const result = await definition.load(
|
|
124
|
+
args("AMD:XNAS", { sentiment: "any", minImportance: 0, limit: 1 }),
|
|
125
|
+
context,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
expect(seen[0]).toMatchObject({
|
|
129
|
+
feed: "ticker",
|
|
130
|
+
ticker: "AMD",
|
|
131
|
+
exchange: "NASDAQ",
|
|
132
|
+
tickerTier: "primary",
|
|
133
|
+
limit: 1,
|
|
134
|
+
});
|
|
135
|
+
expect(result.items).toHaveLength(1);
|
|
136
|
+
expect(result.metadata).toMatchObject({ truncated: true });
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { CloudNewsListResponse } from "../../../api-client";
|
|
2
|
+
import type {
|
|
3
|
+
HeadlessPaneContext,
|
|
4
|
+
HeadlessPaneDefinition,
|
|
5
|
+
HeadlessPaneLoadArgs,
|
|
6
|
+
HeadlessSnapshotResult,
|
|
7
|
+
} from "../../../types/plugin";
|
|
8
|
+
import type { NewsArticle, NewsQuery } from "../../../news/types";
|
|
9
|
+
import {
|
|
10
|
+
cloudNewsParams,
|
|
11
|
+
mapCloudNewsArticle,
|
|
12
|
+
} from "../../../sources/gloomberb-cloud/news";
|
|
13
|
+
import { parsePublicTickerKey } from "../../../utils/exchanges";
|
|
14
|
+
|
|
15
|
+
const NEWS_COLUMNS = [
|
|
16
|
+
{ key: "publishedAt", header: "Published" },
|
|
17
|
+
{ key: "source", header: "Source" },
|
|
18
|
+
{ key: "headline", header: "Headline" },
|
|
19
|
+
{ key: "tickers", header: "Tickers" },
|
|
20
|
+
{ key: "sentiment", header: "Sentiment" },
|
|
21
|
+
{ key: "importance", header: "Score", align: "right" as const },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export interface NewsHeadlessDependencies {
|
|
25
|
+
loadNews(
|
|
26
|
+
query: NewsQuery,
|
|
27
|
+
context: HeadlessPaneContext,
|
|
28
|
+
): Promise<CloudNewsListResponse>;
|
|
29
|
+
now(): Date;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const defaultDependencies: NewsHeadlessDependencies = {
|
|
33
|
+
loadNews: (query, context) => context.apiClient.getCloudNews(cloudNewsParams(query)),
|
|
34
|
+
now: () => new Date(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function newsRow(article: NewsArticle) {
|
|
38
|
+
return {
|
|
39
|
+
id: article.id,
|
|
40
|
+
publishedAt: article.publishedAt.toISOString(),
|
|
41
|
+
source: article.source,
|
|
42
|
+
headline: article.title,
|
|
43
|
+
summary: article.summary ?? null,
|
|
44
|
+
topic: article.topic,
|
|
45
|
+
topics: article.topics,
|
|
46
|
+
sectors: article.sectors,
|
|
47
|
+
categories: article.categories,
|
|
48
|
+
tickers: article.tickers,
|
|
49
|
+
sentiment: article.sentiment ?? null,
|
|
50
|
+
importance: article.scores.importance,
|
|
51
|
+
urgency: article.scores.urgency,
|
|
52
|
+
marketImpact: article.scores.marketImpact,
|
|
53
|
+
novelty: article.scores.novelty,
|
|
54
|
+
confidence: article.scores.confidence,
|
|
55
|
+
breaking: article.isBreaking,
|
|
56
|
+
developing: article.isDeveloping,
|
|
57
|
+
sourceCount: article.sourceCount ?? article.items?.length ?? 0,
|
|
58
|
+
url: article.url,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function selectedSentiment(args: HeadlessPaneLoadArgs): string {
|
|
63
|
+
return String(args.options.sentiment ?? "any");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function projectNewsHeadless(
|
|
67
|
+
response: CloudNewsListResponse,
|
|
68
|
+
args: HeadlessPaneLoadArgs,
|
|
69
|
+
now: Date,
|
|
70
|
+
): HeadlessSnapshotResult {
|
|
71
|
+
const sentiment = selectedSentiment(args);
|
|
72
|
+
const minImportance = Number(args.options.minImportance ?? 0);
|
|
73
|
+
const limit = Number(args.options.limit ?? 50);
|
|
74
|
+
const matching = response.items
|
|
75
|
+
.map((item) => mapCloudNewsArticle(item, args.symbols[0]))
|
|
76
|
+
.filter((article) => sentiment === "any" || article.sentiment === sentiment)
|
|
77
|
+
.filter((article) => article.importance >= minImportance)
|
|
78
|
+
.sort((left, right) => right.publishedAt.getTime() - left.publishedAt.getTime());
|
|
79
|
+
const items = matching.slice(0, limit).map(newsRow);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
asOf: now.toISOString(),
|
|
83
|
+
items,
|
|
84
|
+
metadata: {
|
|
85
|
+
nextCursor: response.nextCursor,
|
|
86
|
+
sentiment,
|
|
87
|
+
minImportance,
|
|
88
|
+
total: matching.length,
|
|
89
|
+
returned: items.length,
|
|
90
|
+
truncated: items.length < matching.length || response.nextCursor != null,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function newsOptions() {
|
|
96
|
+
return [
|
|
97
|
+
{
|
|
98
|
+
key: "sentiment",
|
|
99
|
+
description: "Story sentiment to include.",
|
|
100
|
+
type: "enum" as const,
|
|
101
|
+
values: [
|
|
102
|
+
{ value: "any" },
|
|
103
|
+
{ value: "positive" },
|
|
104
|
+
{ value: "neutral" },
|
|
105
|
+
{ value: "negative" },
|
|
106
|
+
],
|
|
107
|
+
defaultValue: "any",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
key: "minImportance",
|
|
111
|
+
aliases: ["importance"],
|
|
112
|
+
description: "Minimum importance score.",
|
|
113
|
+
type: "integer" as const,
|
|
114
|
+
defaultValue: 0,
|
|
115
|
+
minimum: 0,
|
|
116
|
+
maximum: 100,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
key: "limit",
|
|
120
|
+
description: "Maximum stories to return.",
|
|
121
|
+
type: "integer" as const,
|
|
122
|
+
defaultValue: 50,
|
|
123
|
+
minimum: 1,
|
|
124
|
+
maximum: 200,
|
|
125
|
+
},
|
|
126
|
+
];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function queryFor(args: HeadlessPaneLoadArgs, feed: "latest" | "ticker"): NewsQuery {
|
|
130
|
+
const sentiment = selectedSentiment(args);
|
|
131
|
+
const ticker = args.symbols[0] ? parsePublicTickerKey(args.symbols[0]) : null;
|
|
132
|
+
return {
|
|
133
|
+
feed,
|
|
134
|
+
...(feed === "ticker"
|
|
135
|
+
? {
|
|
136
|
+
ticker: ticker?.symbol,
|
|
137
|
+
exchange: ticker?.exchange,
|
|
138
|
+
tickerTier: "primary" as const,
|
|
139
|
+
}
|
|
140
|
+
: {}),
|
|
141
|
+
...(sentiment === "any" ? {} : { sentiment: sentiment as "positive" | "neutral" | "negative" }),
|
|
142
|
+
minImportance: Number(args.options.minImportance ?? 0),
|
|
143
|
+
limit: Number(args.options.limit ?? 50),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function createNewsFeedHeadless(
|
|
148
|
+
dependencies: NewsHeadlessDependencies = defaultDependencies,
|
|
149
|
+
): HeadlessPaneDefinition<"snapshot"> {
|
|
150
|
+
return {
|
|
151
|
+
shape: "snapshot",
|
|
152
|
+
argument: { kind: "none" },
|
|
153
|
+
options: newsOptions(),
|
|
154
|
+
columns: NEWS_COLUMNS,
|
|
155
|
+
describe: "News Feed",
|
|
156
|
+
async load(args, context) {
|
|
157
|
+
return projectNewsHeadless(
|
|
158
|
+
await dependencies.loadNews(queryFor(args, "latest"), context),
|
|
159
|
+
args,
|
|
160
|
+
dependencies.now(),
|
|
161
|
+
);
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function createTickerNewsHeadless(
|
|
167
|
+
dependencies: NewsHeadlessDependencies = defaultDependencies,
|
|
168
|
+
): HeadlessPaneDefinition<"snapshot"> {
|
|
169
|
+
return {
|
|
170
|
+
shape: "snapshot",
|
|
171
|
+
argument: {
|
|
172
|
+
kind: "ticker",
|
|
173
|
+
placeholder: "ticker",
|
|
174
|
+
description: "Company symbol, optionally qualified as SYMBOL:EXCHANGE.",
|
|
175
|
+
},
|
|
176
|
+
options: newsOptions(),
|
|
177
|
+
columns: NEWS_COLUMNS,
|
|
178
|
+
describe: (args) => `Ticker News | ${String(args.argument)}`,
|
|
179
|
+
async load(args, context) {
|
|
180
|
+
return projectNewsHeadless(
|
|
181
|
+
await dependencies.loadNews(queryFor(args, "ticker"), context),
|
|
182
|
+
args,
|
|
183
|
+
dependencies.now(),
|
|
184
|
+
);
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const newsFeedHeadless = createNewsFeedHeadless();
|
|
190
|
+
export const tickerNewsHeadless = createTickerNewsHeadless();
|
|
@@ -17,6 +17,7 @@ import { useNewsArticleFooter } from "./wire/news/footer";
|
|
|
17
17
|
import { usePersistedNewsArticles } from "./wire/persisted-articles";
|
|
18
18
|
import { useNewsReadState } from "./wire/read-state";
|
|
19
19
|
import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
|
|
20
|
+
import { tickerNewsHeadless } from "./headless";
|
|
20
21
|
|
|
21
22
|
const NEWS_ITEM_LIMIT = 50;
|
|
22
23
|
const DEFAULT_SORT: NewsSortPreference = { columnId: "time", direction: "desc" };
|
|
@@ -140,15 +141,18 @@ export const tickerNewsModule: PluginModule = {
|
|
|
140
141
|
],
|
|
141
142
|
|
|
142
143
|
paneTemplates: [
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
144
|
+
{
|
|
145
|
+
...createTickerSurfacePaneTemplate({
|
|
146
|
+
id: "ticker-news-pane",
|
|
147
|
+
paneId: "ticker-news",
|
|
148
|
+
label: "Ticker News",
|
|
149
|
+
description: "Company news for the selected ticker.",
|
|
150
|
+
keywords: ["company", "ticker", "news", "headlines", "cn"],
|
|
151
|
+
shortcut: "CN",
|
|
152
|
+
publicShare: true,
|
|
153
|
+
}),
|
|
154
|
+
headless: tickerNewsHeadless,
|
|
155
|
+
},
|
|
152
156
|
],
|
|
153
157
|
|
|
154
158
|
setup(ctx) {
|
|
@@ -23,6 +23,7 @@ import { NewsPresetPane } from "./news/preset-pane";
|
|
|
23
23
|
import { NEWS_QUERY_PRESETS } from "./news/query-presets";
|
|
24
24
|
import type { NewsColumnId, NewsSortPreference } from "./news/table";
|
|
25
25
|
import { createRssNewsCapability } from "./rss/source";
|
|
26
|
+
import { newsFeedHeadless } from "../headless";
|
|
26
27
|
|
|
27
28
|
interface NewsPresetPaneConfig {
|
|
28
29
|
paneKey: string;
|
|
@@ -107,7 +108,7 @@ export const newsWireModule: PluginModule = {
|
|
|
107
108
|
],
|
|
108
109
|
paneTemplates: [
|
|
109
110
|
{ id: "news-top-pane", paneId: "news-top", label: "Top News", description: "Curated top market stories ranked by importance", keywords: ["top", "news", "headlines", "stories"], shortcut: { prefix: "TOP" } },
|
|
110
|
-
{ id: "news-feed-pane", paneId: "news-feed", label: "News Feed", description: "Chronological market news firehose", keywords: ["news", "feed", "firehose", "wire", "stream"], shortcut: { prefix: "N" } },
|
|
111
|
+
{ id: "news-feed-pane", paneId: "news-feed", label: "News Feed", description: "Chronological market news firehose", keywords: ["news", "feed", "firehose", "wire", "stream"], shortcut: { prefix: "N" }, headless: newsFeedHeadless },
|
|
111
112
|
{ id: "news-industry-pane", paneId: "news-industry", label: "Sector News", description: "Market news filtered by sector", keywords: ["news", "industry", "sector", "ni", "filter"], shortcut: { prefix: "NI" } },
|
|
112
113
|
{ id: "news-breaking-pane", paneId: "news-breaking", label: "Breaking News", description: "Breaking and urgent market news", keywords: ["first", "breaking", "urgent", "alert", "flash"], shortcut: { prefix: "FIRST" } },
|
|
113
114
|
],
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createTestDataProvider } from "../../../test-support/data-provider";
|
|
3
|
+
import { createDefaultConfig } from "../../../types/config";
|
|
4
|
+
import type { AnalystResearchData } from "../../../types/financials";
|
|
5
|
+
import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
|
|
6
|
+
import { createAnalystResearchHeadless } from "./analyst-headless";
|
|
7
|
+
|
|
8
|
+
const data: AnalystResearchData = {
|
|
9
|
+
symbol: "AMD",
|
|
10
|
+
currency: "USD",
|
|
11
|
+
priceTarget: { current: 150, average: 180, low: 120, median: 175, high: 230 },
|
|
12
|
+
recommendationRating: 8.4,
|
|
13
|
+
recommendations: [{ period: "current month", strongBuy: 10, buy: 8, hold: 4, sell: 1 }],
|
|
14
|
+
ratings: [
|
|
15
|
+
{ date: "2026-08-20", firm: "Beta", action: "Maintains", current: "Buy", currentPriceTarget: 190 },
|
|
16
|
+
{ date: "2026-08-25", firm: "Alpha", action: "Raises", current: "Buy", prior: "Hold", currentPriceTarget: 210, priorPriceTarget: 180 },
|
|
17
|
+
],
|
|
18
|
+
earningsEstimates: [],
|
|
19
|
+
revenueEstimates: [],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function context(): HeadlessPaneContext {
|
|
23
|
+
return {
|
|
24
|
+
marketData: createTestDataProvider(),
|
|
25
|
+
apiClient: {} as HeadlessPaneContext["apiClient"],
|
|
26
|
+
config: createDefaultConfig("/tmp/gloomberb-headless-analyst"),
|
|
27
|
+
signal: new AbortController().signal,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function args(overrides: Partial<HeadlessPaneLoadArgs["options"]> = {}): HeadlessPaneLoadArgs {
|
|
32
|
+
return {
|
|
33
|
+
rawArgument: "AMD",
|
|
34
|
+
argument: "AMD",
|
|
35
|
+
symbols: ["AMD"],
|
|
36
|
+
options: { sort: "date", order: "desc", limit: 25, ...overrides },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("analyst research headless model", () => {
|
|
41
|
+
test("projects summary and ratings while applying sort and limit", async () => {
|
|
42
|
+
const headless = createAnalystResearchHeadless({ loadData: async () => data });
|
|
43
|
+
|
|
44
|
+
const latest = await headless.load(args({ limit: 1 }), context());
|
|
45
|
+
expect(latest.sections[0]?.title).toBe("Summary");
|
|
46
|
+
const entries = "entries" in latest.sections[0]! ? latest.sections[0]!.entries : [];
|
|
47
|
+
expect(entries.slice(0, 2)).toMatchObject([
|
|
48
|
+
{ label: "Average target", value: 180 },
|
|
49
|
+
{ label: "Target upside", value: 0.2 },
|
|
50
|
+
]);
|
|
51
|
+
expect(latest.sections[1]).toMatchObject({
|
|
52
|
+
title: "Recent analyst actions",
|
|
53
|
+
rows: [{ firm: "Alpha", currentPriceTarget: 210, priorPriceTarget: 180 }],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const byFirm = await headless.load(args({ sort: "firm", order: "desc", limit: 2 }), context());
|
|
57
|
+
const ratings = "rows" in byFirm.sections[1]! ? byFirm.sections[1]!.rows : [];
|
|
58
|
+
expect(ratings.map((row) => row.firm)).toEqual(["Beta", "Alpha"]);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HeadlessBundleSection,
|
|
3
|
+
HeadlessPaneColumn,
|
|
4
|
+
HeadlessPaneContext,
|
|
5
|
+
HeadlessPaneDefinition,
|
|
6
|
+
HeadlessPaneEntry,
|
|
7
|
+
HeadlessPaneLoadArgs,
|
|
8
|
+
} from "../../../types/plugin";
|
|
9
|
+
import type { AnalystResearchData } from "../../../types/financials";
|
|
10
|
+
import { formatCurrency, formatPercent } from "../../../utils/format";
|
|
11
|
+
import { loadAnalystResearch } from "./client";
|
|
12
|
+
import {
|
|
13
|
+
formatRatingLabel,
|
|
14
|
+
formatRatingTarget,
|
|
15
|
+
latestRecommendation,
|
|
16
|
+
recommendationTotal,
|
|
17
|
+
sortRatingRows,
|
|
18
|
+
targetUpside,
|
|
19
|
+
type RatingColumnId,
|
|
20
|
+
} from "./analyst-model";
|
|
21
|
+
|
|
22
|
+
const RATING_COLUMNS: HeadlessPaneColumn[] = [
|
|
23
|
+
{ key: "date", header: "Date" },
|
|
24
|
+
{ key: "firm", header: "Firm" },
|
|
25
|
+
{ key: "action", header: "Action" },
|
|
26
|
+
{ key: "current", header: "Rating" },
|
|
27
|
+
{
|
|
28
|
+
key: "target",
|
|
29
|
+
header: "Target",
|
|
30
|
+
format: (_value, row) => formatRatingTarget(
|
|
31
|
+
row as unknown as AnalystResearchData["ratings"][number],
|
|
32
|
+
String(row.currency ?? "USD"),
|
|
33
|
+
).trim(),
|
|
34
|
+
},
|
|
35
|
+
{ key: "prior", header: "Prior" },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const SORT_COLUMNS: Record<string, RatingColumnId> = {
|
|
39
|
+
date: "date",
|
|
40
|
+
firm: "firm",
|
|
41
|
+
action: "action",
|
|
42
|
+
rating: "current",
|
|
43
|
+
target: "target",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function overviewEntries(data: AnalystResearchData): HeadlessPaneEntry[] {
|
|
47
|
+
const target = data.priceTarget;
|
|
48
|
+
const currency = target?.currency ?? data.currency ?? "USD";
|
|
49
|
+
const recommendation = latestRecommendation(data);
|
|
50
|
+
const upside = targetUpside(target);
|
|
51
|
+
return [
|
|
52
|
+
{
|
|
53
|
+
label: "Average target",
|
|
54
|
+
value: target?.average ?? null,
|
|
55
|
+
formatted: target?.average == null ? "-" : formatCurrency(target.average, currency),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
label: "Target upside",
|
|
59
|
+
value: upside ?? null,
|
|
60
|
+
formatted: upside == null ? "-" : formatPercent(upside),
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
label: "Target range",
|
|
64
|
+
value: target ? { low: target.low, median: target.median, high: target.high } : null,
|
|
65
|
+
formatted: target
|
|
66
|
+
? [target.low, target.median, target.high]
|
|
67
|
+
.map((value) => value == null ? "-" : formatCurrency(value, currency))
|
|
68
|
+
.join(" / ")
|
|
69
|
+
: "-",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
label: "Recommendation rating",
|
|
73
|
+
value: data.recommendationRating ?? null,
|
|
74
|
+
formatted: formatRatingLabel(data.recommendationRating),
|
|
75
|
+
},
|
|
76
|
+
{ label: "Analysts", value: recommendationTotal(data) },
|
|
77
|
+
{
|
|
78
|
+
label: "Recommendation mix",
|
|
79
|
+
value: recommendation ? {
|
|
80
|
+
period: recommendation.period,
|
|
81
|
+
strongBuy: recommendation.strongBuy ?? 0,
|
|
82
|
+
buy: recommendation.buy ?? 0,
|
|
83
|
+
hold: recommendation.hold ?? 0,
|
|
84
|
+
sell: recommendation.sell ?? 0,
|
|
85
|
+
strongSell: recommendation.strongSell ?? 0,
|
|
86
|
+
} : null,
|
|
87
|
+
formatted: recommendation
|
|
88
|
+
? `SB ${recommendation.strongBuy ?? 0} B ${recommendation.buy ?? 0} H ${recommendation.hold ?? 0} S ${(recommendation.sell ?? 0) + (recommendation.strongSell ?? 0)}`
|
|
89
|
+
: "-",
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface AnalystResearchHeadlessDependencies {
|
|
95
|
+
loadData(
|
|
96
|
+
symbol: string,
|
|
97
|
+
args: HeadlessPaneLoadArgs,
|
|
98
|
+
ctx: HeadlessPaneContext,
|
|
99
|
+
): Promise<AnalystResearchData>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const defaultDependencies: AnalystResearchHeadlessDependencies = {
|
|
103
|
+
loadData: (symbol, _args, ctx) => loadAnalystResearch(ctx.marketData, symbol),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export function createAnalystResearchHeadless(
|
|
107
|
+
dependencies: AnalystResearchHeadlessDependencies = defaultDependencies,
|
|
108
|
+
): HeadlessPaneDefinition<"bundle"> {
|
|
109
|
+
return {
|
|
110
|
+
shape: "bundle",
|
|
111
|
+
argument: {
|
|
112
|
+
kind: "ticker",
|
|
113
|
+
placeholder: "ticker",
|
|
114
|
+
description: "Ticker whose analyst research should be loaded.",
|
|
115
|
+
},
|
|
116
|
+
options: [
|
|
117
|
+
{
|
|
118
|
+
key: "sort",
|
|
119
|
+
description: "Column used to sort analyst actions.",
|
|
120
|
+
type: "enum",
|
|
121
|
+
values: Object.keys(SORT_COLUMNS).map((value) => ({ value })),
|
|
122
|
+
defaultValue: "date",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
key: "order",
|
|
126
|
+
description: "Sort direction.",
|
|
127
|
+
type: "enum",
|
|
128
|
+
values: [{ value: "desc" }, { value: "asc" }],
|
|
129
|
+
defaultValue: "desc",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
key: "limit",
|
|
133
|
+
aliases: ["count", "rows"],
|
|
134
|
+
description: "Maximum analyst action rows.",
|
|
135
|
+
type: "integer",
|
|
136
|
+
defaultValue: 25,
|
|
137
|
+
minimum: 1,
|
|
138
|
+
maximum: 100,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
describe: (args) => `Analyst Research | ${args.symbols[0]}`,
|
|
142
|
+
async load(args, ctx) {
|
|
143
|
+
const symbol = args.symbols[0]!;
|
|
144
|
+
const data = await dependencies.loadData(symbol, args, ctx);
|
|
145
|
+
const currency = data.priceTarget?.currency ?? data.currency ?? "USD";
|
|
146
|
+
const ratings = sortRatingRows(data.ratings, {
|
|
147
|
+
columnId: SORT_COLUMNS[String(args.options.sort)] ?? "date",
|
|
148
|
+
direction: args.options.order === "asc" ? "asc" : "desc",
|
|
149
|
+
})
|
|
150
|
+
.slice(0, Number(args.options.limit))
|
|
151
|
+
.map((rating) => ({
|
|
152
|
+
...rating,
|
|
153
|
+
target: rating.currentPriceTarget ?? rating.priorPriceTarget ?? null,
|
|
154
|
+
currency,
|
|
155
|
+
}));
|
|
156
|
+
const sections: HeadlessBundleSection[] = [
|
|
157
|
+
{ title: "Summary", entries: overviewEntries(data) },
|
|
158
|
+
{ title: "Recent analyst actions", columns: RATING_COLUMNS, rows: ratings },
|
|
159
|
+
];
|
|
160
|
+
return {
|
|
161
|
+
sections,
|
|
162
|
+
metadata: {
|
|
163
|
+
symbol: data.symbol || symbol,
|
|
164
|
+
name: data.name ?? null,
|
|
165
|
+
currency,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export const analystResearchHeadless = createAnalystResearchHeadless();
|