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,162 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HeadlessPaneColumn,
|
|
3
|
+
HeadlessPaneContext,
|
|
4
|
+
HeadlessPaneDefinition,
|
|
5
|
+
HeadlessPaneLoadArgs,
|
|
6
|
+
} from "../../../types/plugin";
|
|
7
|
+
import { formatCompact } from "../../../utils/format";
|
|
8
|
+
import { loadHolderSnapshot } from "./client";
|
|
9
|
+
import {
|
|
10
|
+
displayDate,
|
|
11
|
+
formatHolderOwnershipPercent,
|
|
12
|
+
formatMaybePercent,
|
|
13
|
+
formatMoneyCompact,
|
|
14
|
+
formatSignedCompact,
|
|
15
|
+
resolveHolderOwnershipPercent,
|
|
16
|
+
} from "./format";
|
|
17
|
+
import { buildRows, sortRows } from "./table-model";
|
|
18
|
+
import type { HolderData } from "../../../types/financials";
|
|
19
|
+
import type { HolderColumnId } from "./types";
|
|
20
|
+
|
|
21
|
+
const HOLDER_COLUMNS: HeadlessPaneColumn[] = [
|
|
22
|
+
{ key: "name", header: "Holder" },
|
|
23
|
+
{ key: "ownerType", header: "Type" },
|
|
24
|
+
{
|
|
25
|
+
key: "value",
|
|
26
|
+
header: "Value",
|
|
27
|
+
align: "right",
|
|
28
|
+
format: (value, row) => formatMoneyCompact(
|
|
29
|
+
value == null ? undefined : Number(value),
|
|
30
|
+
String(row.currency ?? "USD"),
|
|
31
|
+
),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: "shares",
|
|
35
|
+
header: "Amount",
|
|
36
|
+
align: "right",
|
|
37
|
+
format: (value) => value == null ? "-" : formatCompact(Number(value)),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
key: "changeShares",
|
|
41
|
+
header: "Chg",
|
|
42
|
+
align: "right",
|
|
43
|
+
format: (value) => formatSignedCompact(value == null ? undefined : Number(value)),
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
key: "changePercent",
|
|
47
|
+
header: "Chg%",
|
|
48
|
+
align: "right",
|
|
49
|
+
format: (value) => formatMaybePercent(value == null ? undefined : Number(value)),
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: "percentHeld",
|
|
53
|
+
header: "Held",
|
|
54
|
+
align: "right",
|
|
55
|
+
format: (value) => formatHolderOwnershipPercent(value == null ? undefined : Number(value)),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
key: "reportDate",
|
|
59
|
+
header: "Date",
|
|
60
|
+
format: (value) => displayDate(typeof value === "string" ? value : undefined),
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const SORT_COLUMNS: Record<string, HolderColumnId> = {
|
|
65
|
+
holder: "holder",
|
|
66
|
+
value: "value",
|
|
67
|
+
shares: "shares",
|
|
68
|
+
change: "changeShares",
|
|
69
|
+
"change-percent": "changePercent",
|
|
70
|
+
held: "percentHeld",
|
|
71
|
+
date: "reportDate",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
interface HolderSnapshot {
|
|
75
|
+
data: HolderData;
|
|
76
|
+
marketCap?: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface HoldersHeadlessDependencies {
|
|
80
|
+
loadSnapshot(
|
|
81
|
+
symbol: string,
|
|
82
|
+
args: HeadlessPaneLoadArgs,
|
|
83
|
+
ctx: HeadlessPaneContext,
|
|
84
|
+
): Promise<HolderSnapshot>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const defaultDependencies: HoldersHeadlessDependencies = {
|
|
88
|
+
loadSnapshot: (symbol, _args, ctx) => loadHolderSnapshot(ctx.marketData, symbol),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export function createHoldersHeadless(
|
|
92
|
+
dependencies: HoldersHeadlessDependencies = defaultDependencies,
|
|
93
|
+
): HeadlessPaneDefinition<"rows"> {
|
|
94
|
+
return {
|
|
95
|
+
shape: "rows",
|
|
96
|
+
argument: {
|
|
97
|
+
kind: "ticker",
|
|
98
|
+
placeholder: "ticker",
|
|
99
|
+
description: "Ticker whose institutional holders should be loaded.",
|
|
100
|
+
},
|
|
101
|
+
options: [
|
|
102
|
+
{
|
|
103
|
+
key: "sort",
|
|
104
|
+
description: "Column used to sort holder rows.",
|
|
105
|
+
type: "enum",
|
|
106
|
+
values: Object.keys(SORT_COLUMNS).map((value) => ({ value })),
|
|
107
|
+
defaultValue: "value",
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
key: "order",
|
|
111
|
+
description: "Sort direction.",
|
|
112
|
+
type: "enum",
|
|
113
|
+
values: [{ value: "desc" }, { value: "asc" }],
|
|
114
|
+
defaultValue: "desc",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
key: "limit",
|
|
118
|
+
aliases: ["count", "rows"],
|
|
119
|
+
description: "Maximum holder rows.",
|
|
120
|
+
type: "integer",
|
|
121
|
+
defaultValue: 25,
|
|
122
|
+
minimum: 1,
|
|
123
|
+
maximum: 200,
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
columns: HOLDER_COLUMNS,
|
|
127
|
+
describe: (args) => `Holders | ${args.symbols[0]}`,
|
|
128
|
+
async load(args, ctx) {
|
|
129
|
+
const symbol = args.symbols[0]!;
|
|
130
|
+
const { data, marketCap } = await dependencies.loadSnapshot(symbol, args, ctx);
|
|
131
|
+
const currency = data.currency ?? "USD";
|
|
132
|
+
const rows = sortRows(buildRows(data), {
|
|
133
|
+
columnId: SORT_COLUMNS[String(args.options.sort)] ?? "value",
|
|
134
|
+
direction: args.options.order === "asc" ? "asc" : "desc",
|
|
135
|
+
}, marketCap)
|
|
136
|
+
.slice(0, Number(args.options.limit))
|
|
137
|
+
.map((row) => ({
|
|
138
|
+
name: row.name,
|
|
139
|
+
ownerType: row.ownerType,
|
|
140
|
+
value: row.value ?? null,
|
|
141
|
+
shares: row.shares ?? null,
|
|
142
|
+
changeShares: row.changeShares ?? null,
|
|
143
|
+
changePercent: row.changePercent ?? null,
|
|
144
|
+
percentHeld: resolveHolderOwnershipPercent(row, marketCap) ?? null,
|
|
145
|
+
reportDate: row.reportDate ?? null,
|
|
146
|
+
currency,
|
|
147
|
+
}));
|
|
148
|
+
return {
|
|
149
|
+
rows,
|
|
150
|
+
metadata: {
|
|
151
|
+
symbol: data.symbol || symbol,
|
|
152
|
+
name: data.name ?? null,
|
|
153
|
+
currency,
|
|
154
|
+
asOf: data.asOf ?? null,
|
|
155
|
+
summary: data.summary ?? null,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const holdersHeadless = createHoldersHeadless();
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { PluginModule } from "../plugin-module";
|
|
2
2
|
import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
|
|
3
|
+
import { holdersHeadless } from "./headless";
|
|
3
4
|
import { HoldersView } from "./pane";
|
|
4
5
|
|
|
6
|
+
export { holdersHeadless } from "./headless";
|
|
7
|
+
|
|
5
8
|
export const holdersModule: PluginModule = {
|
|
6
9
|
setup(ctx) {
|
|
7
10
|
ctx.registerTickerResearchTab({
|
|
@@ -27,13 +30,16 @@ export const holdersModule: PluginModule = {
|
|
|
27
30
|
],
|
|
28
31
|
|
|
29
32
|
paneTemplates: [
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
{
|
|
34
|
+
...createTickerSurfacePaneTemplate({
|
|
35
|
+
id: "holders-pane",
|
|
36
|
+
paneId: "holders",
|
|
37
|
+
label: "Holders",
|
|
38
|
+
description: "Institutional holders for the selected ticker.",
|
|
39
|
+
keywords: ["holders", "ownership", "institutional", "owners", "hds"],
|
|
40
|
+
shortcut: "HDS",
|
|
41
|
+
}),
|
|
42
|
+
headless: holdersHeadless,
|
|
43
|
+
},
|
|
38
44
|
],
|
|
39
45
|
};
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
sortRows,
|
|
32
32
|
VIEW_TABS,
|
|
33
33
|
} from "./table-model";
|
|
34
|
+
import { loadHolderData } from "./client";
|
|
34
35
|
import { HoldersTreemap } from "./treemap";
|
|
35
36
|
import type { HolderColumn, HolderRow, SortPreference, ViewMode } from "./types";
|
|
36
37
|
import { loadHolder13FMatches, type Holder13FMatch } from "./thirteenf-match";
|
|
@@ -77,7 +78,12 @@ export function HoldersView({ focused, width, height }: { focused: boolean; widt
|
|
|
77
78
|
setError(null);
|
|
78
79
|
|
|
79
80
|
try {
|
|
80
|
-
const nextData = await
|
|
81
|
+
const nextData = await loadHolderData(
|
|
82
|
+
dataProvider,
|
|
83
|
+
symbol,
|
|
84
|
+
exchange,
|
|
85
|
+
forceRefresh ? { cacheMode: "refresh" } : undefined,
|
|
86
|
+
);
|
|
81
87
|
if (fetchGenRef.current !== gen) return;
|
|
82
88
|
setData(nextData);
|
|
83
89
|
setSelectedId(null);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { DataProvider, SecFilingItem } from "../../../types/data-provider";
|
|
2
|
+
import { parseForm4Xml } from "./insider-data";
|
|
3
|
+
import type { ParsedInsiderFiling } from "./model";
|
|
4
|
+
|
|
5
|
+
export async function loadInsiderFilings(
|
|
6
|
+
provider: DataProvider,
|
|
7
|
+
symbol: string,
|
|
8
|
+
count: number,
|
|
9
|
+
exchange = "",
|
|
10
|
+
): Promise<SecFilingItem[]> {
|
|
11
|
+
if (!provider.getSecFilings) throw new Error("Insider filing data unavailable");
|
|
12
|
+
const filings = await provider.getSecFilings(symbol, count, exchange);
|
|
13
|
+
return filings.filter((filing) => filing.form.trim() === "4");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
17
|
+
if (!signal?.aborted) return;
|
|
18
|
+
throw signal.reason instanceof Error ? signal.reason : new Error("Insider filing load was aborted.");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function loadParsedInsiderFilings(
|
|
22
|
+
provider: DataProvider,
|
|
23
|
+
symbol: string,
|
|
24
|
+
options: { limit: number; exchange?: string; signal?: AbortSignal },
|
|
25
|
+
): Promise<ParsedInsiderFiling[]> {
|
|
26
|
+
if (!provider.getSecFilingContent) throw new Error("Insider filing content unavailable");
|
|
27
|
+
const filings = (await loadInsiderFilings(
|
|
28
|
+
provider,
|
|
29
|
+
symbol,
|
|
30
|
+
20_000,
|
|
31
|
+
options.exchange,
|
|
32
|
+
)).slice(0, options.limit);
|
|
33
|
+
|
|
34
|
+
const parsed: ParsedInsiderFiling[] = [];
|
|
35
|
+
for (const filing of filings) {
|
|
36
|
+
throwIfAborted(options.signal);
|
|
37
|
+
try {
|
|
38
|
+
const content = await provider.getSecFilingContent(filing);
|
|
39
|
+
parsed.push({
|
|
40
|
+
filing,
|
|
41
|
+
transaction: content ? parseForm4Xml(content) : null,
|
|
42
|
+
isLoading: false,
|
|
43
|
+
});
|
|
44
|
+
} catch {
|
|
45
|
+
throwIfAborted(options.signal);
|
|
46
|
+
parsed.push({ filing, transaction: null, isLoading: false });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
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 { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
|
|
5
|
+
import { loadParsedInsiderFilings } from "./client";
|
|
6
|
+
import { createInsiderHeadless } from "./headless";
|
|
7
|
+
import type { ParsedInsiderFiling } from "./model";
|
|
8
|
+
|
|
9
|
+
const parsed: ParsedInsiderFiling[] = [
|
|
10
|
+
{
|
|
11
|
+
filing: {
|
|
12
|
+
accessionNumber: "one",
|
|
13
|
+
form: "4",
|
|
14
|
+
filingDate: new Date("2026-08-26T00:00:00.000Z"),
|
|
15
|
+
cik: "0000002488",
|
|
16
|
+
filingUrl: "https://www.sec.gov/one",
|
|
17
|
+
},
|
|
18
|
+
transaction: {
|
|
19
|
+
filingDate: new Date("2026-08-25T00:00:00.000Z"),
|
|
20
|
+
reportedName: "SU LISA T",
|
|
21
|
+
title: "Chief Executive Officer",
|
|
22
|
+
transactionType: "P",
|
|
23
|
+
shares: 25_000,
|
|
24
|
+
pricePerShare: 150,
|
|
25
|
+
totalValue: 3_750_000,
|
|
26
|
+
sharesOwned: 4_000_000,
|
|
27
|
+
form: "4",
|
|
28
|
+
},
|
|
29
|
+
isLoading: false,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
filing: {
|
|
33
|
+
accessionNumber: "two",
|
|
34
|
+
form: "4",
|
|
35
|
+
filingDate: new Date("2026-08-24T00:00:00.000Z"),
|
|
36
|
+
cik: "0000002488",
|
|
37
|
+
filingUrl: "https://www.sec.gov/two",
|
|
38
|
+
},
|
|
39
|
+
transaction: {
|
|
40
|
+
filingDate: new Date("2026-08-23T00:00:00.000Z"),
|
|
41
|
+
reportedName: "OTHER OFFICER",
|
|
42
|
+
title: "Officer",
|
|
43
|
+
transactionType: "S",
|
|
44
|
+
shares: 1_000,
|
|
45
|
+
pricePerShare: 155,
|
|
46
|
+
totalValue: 155_000,
|
|
47
|
+
sharesOwned: 20_000,
|
|
48
|
+
form: "4",
|
|
49
|
+
},
|
|
50
|
+
isLoading: false,
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
function context(): HeadlessPaneContext {
|
|
55
|
+
return {
|
|
56
|
+
marketData: createTestDataProvider(),
|
|
57
|
+
apiClient: {} as HeadlessPaneContext["apiClient"],
|
|
58
|
+
config: createDefaultConfig("/tmp/gloomberb-headless-insider"),
|
|
59
|
+
signal: new AbortController().signal,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function args(name = "", limit = 20): HeadlessPaneLoadArgs {
|
|
64
|
+
return {
|
|
65
|
+
rawArgument: "AMD",
|
|
66
|
+
argument: "AMD",
|
|
67
|
+
symbols: ["AMD"],
|
|
68
|
+
options: { name, limit },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe("insider client", () => {
|
|
73
|
+
test("scans Form 4 filings and loads their content sequentially", async () => {
|
|
74
|
+
let activeContentLoads = 0;
|
|
75
|
+
let maxActiveContentLoads = 0;
|
|
76
|
+
const filingRequests: Array<{ symbol: string; count: number; exchange: string | undefined }> = [];
|
|
77
|
+
const provider = createTestDataProvider({
|
|
78
|
+
getSecFilings: async (symbol, count, exchange) => {
|
|
79
|
+
filingRequests.push({ symbol, count, exchange });
|
|
80
|
+
return parsed.map(({ filing }) => filing);
|
|
81
|
+
},
|
|
82
|
+
getSecFilingContent: async () => {
|
|
83
|
+
activeContentLoads += 1;
|
|
84
|
+
maxActiveContentLoads = Math.max(maxActiveContentLoads, activeContentLoads);
|
|
85
|
+
await Bun.sleep(1);
|
|
86
|
+
activeContentLoads -= 1;
|
|
87
|
+
return null;
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const result = await loadParsedInsiderFilings(provider, "NVDA", { limit: 2 });
|
|
92
|
+
|
|
93
|
+
expect(filingRequests).toEqual([{ symbol: "NVDA", count: 20_000, exchange: "" }]);
|
|
94
|
+
expect(result).toHaveLength(2);
|
|
95
|
+
expect(maxActiveContentLoads).toBe(1);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("insider headless model", () => {
|
|
100
|
+
test("projects parsed Form 4 values and applies the owner filter", async () => {
|
|
101
|
+
const requestedLimits: number[] = [];
|
|
102
|
+
const headless = createInsiderHeadless({
|
|
103
|
+
loadParsed: async (_symbol, limit) => {
|
|
104
|
+
requestedLimits.push(limit);
|
|
105
|
+
return parsed.slice(0, limit);
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const all = await headless.load(args("", 2), context());
|
|
110
|
+
expect(all.rows[0]).toMatchObject({
|
|
111
|
+
insider: "SU LISA T",
|
|
112
|
+
side: "BUY",
|
|
113
|
+
shares: 25_000,
|
|
114
|
+
totalValue: 3_750_000,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const filtered = await headless.load(args("other officer", 2), context());
|
|
118
|
+
expect(filtered.rows).toHaveLength(1);
|
|
119
|
+
expect(filtered.rows[0]).toMatchObject({ insider: "OTHER OFFICER", side: "SELL" });
|
|
120
|
+
expect(requestedLimits).toEqual([2, 2]);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HeadlessPaneColumn,
|
|
3
|
+
HeadlessPaneContext,
|
|
4
|
+
HeadlessPaneDefinition,
|
|
5
|
+
HeadlessPaneLoadArgs,
|
|
6
|
+
} from "../../../types/plugin";
|
|
7
|
+
import { formatCompact, formatCurrency } from "../../../utils/format";
|
|
8
|
+
import { loadParsedInsiderFilings } from "./client";
|
|
9
|
+
import {
|
|
10
|
+
buildInsiderRows,
|
|
11
|
+
buildInsiderSummary,
|
|
12
|
+
type ParsedInsiderFiling,
|
|
13
|
+
} from "./model";
|
|
14
|
+
|
|
15
|
+
const INSIDER_COLUMNS: HeadlessPaneColumn[] = [
|
|
16
|
+
{
|
|
17
|
+
key: "filingDate",
|
|
18
|
+
header: "Filed",
|
|
19
|
+
format: (value) => typeof value === "string" ? value.slice(0, 10) : "-",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
key: "transactionDate",
|
|
23
|
+
header: "Tx",
|
|
24
|
+
format: (value) => typeof value === "string" ? value.slice(0, 10) : "-",
|
|
25
|
+
},
|
|
26
|
+
{ key: "insider", header: "Insider" },
|
|
27
|
+
{ key: "title", header: "Title" },
|
|
28
|
+
{ key: "side", header: "Side" },
|
|
29
|
+
{
|
|
30
|
+
key: "shares",
|
|
31
|
+
header: "Shares",
|
|
32
|
+
align: "right",
|
|
33
|
+
format: (value) => value == null ? "-" : formatCompact(Number(value)),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
key: "pricePerShare",
|
|
37
|
+
header: "Price",
|
|
38
|
+
align: "right",
|
|
39
|
+
format: (value) => value == null ? "-" : formatCurrency(Number(value)),
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: "totalValue",
|
|
43
|
+
header: "Value",
|
|
44
|
+
align: "right",
|
|
45
|
+
format: (value) => value == null ? "-" : formatCurrency(Number(value)),
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
export interface InsiderHeadlessDependencies {
|
|
50
|
+
loadParsed(
|
|
51
|
+
symbol: string,
|
|
52
|
+
limit: number,
|
|
53
|
+
args: HeadlessPaneLoadArgs,
|
|
54
|
+
ctx: HeadlessPaneContext,
|
|
55
|
+
): Promise<ParsedInsiderFiling[]>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const defaultDependencies: InsiderHeadlessDependencies = {
|
|
59
|
+
loadParsed: (symbol, limit, _args, ctx) => loadParsedInsiderFilings(ctx.marketData, symbol, {
|
|
60
|
+
limit,
|
|
61
|
+
signal: ctx.signal,
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export function createInsiderHeadless(
|
|
66
|
+
dependencies: InsiderHeadlessDependencies = defaultDependencies,
|
|
67
|
+
): HeadlessPaneDefinition<"rows"> {
|
|
68
|
+
return {
|
|
69
|
+
shape: "rows",
|
|
70
|
+
argument: {
|
|
71
|
+
kind: "ticker",
|
|
72
|
+
placeholder: "ticker",
|
|
73
|
+
description: "US equity ticker.",
|
|
74
|
+
},
|
|
75
|
+
options: [
|
|
76
|
+
{
|
|
77
|
+
key: "name",
|
|
78
|
+
description: "Exact reporting owner name.",
|
|
79
|
+
type: "string",
|
|
80
|
+
defaultValue: "",
|
|
81
|
+
pluginState: { pluginId: "ticker-research", key: "nameFilter" },
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
key: "limit",
|
|
85
|
+
aliases: ["count", "rows"],
|
|
86
|
+
description: "Maximum Form 4 filings to parse.",
|
|
87
|
+
type: "integer",
|
|
88
|
+
defaultValue: 20,
|
|
89
|
+
minimum: 1,
|
|
90
|
+
maximum: 100,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
columns: INSIDER_COLUMNS,
|
|
94
|
+
describe: (args) => `Insider Transactions | ${args.symbols[0]}`,
|
|
95
|
+
async load(args, ctx) {
|
|
96
|
+
const symbol = args.symbols[0]!;
|
|
97
|
+
const limit = Number(args.options.limit);
|
|
98
|
+
const parsed = await dependencies.loadParsed(symbol, limit, args, ctx);
|
|
99
|
+
const name = String(args.options.name ?? "").trim().toLocaleLowerCase();
|
|
100
|
+
const filtered = name
|
|
101
|
+
? parsed.filter(({ transaction }) => transaction?.reportedName.toLocaleLowerCase() === name)
|
|
102
|
+
: parsed;
|
|
103
|
+
return {
|
|
104
|
+
rows: buildInsiderRows(filtered),
|
|
105
|
+
metadata: {
|
|
106
|
+
symbol,
|
|
107
|
+
summary: buildInsiderSummary(parsed),
|
|
108
|
+
parsed: parsed.filter(({ transaction }) => transaction != null).length,
|
|
109
|
+
requested: limit,
|
|
110
|
+
name: name || null,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const insiderHeadless = createInsiderHeadless();
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import type { PluginModule } from "../plugin-module";
|
|
3
|
-
import type { SecFilingItem } from "../../../types/data-provider";
|
|
4
3
|
import {
|
|
5
4
|
useResolvedEntryValue,
|
|
6
5
|
useSecFilingsQuery,
|
|
@@ -11,9 +10,8 @@ import type { ScrollBoxRenderable } from "../../../ui";
|
|
|
11
10
|
import { EmptyState, FeedDataTableStackView, Spinner, useExternalLinkFooter, useTableLoadMore, type FeedDataTableItem } from "../../../components";
|
|
12
11
|
import { usePluginPaneState } from "../../runtime";
|
|
13
12
|
import { isUsEquityTicker } from "../../../utils/sec";
|
|
14
|
-
import { formatCompact, formatCurrency } from "../../../utils/format";
|
|
15
13
|
import { truncateWithEllipsis as truncateText } from "../../../utils/text-wrap";
|
|
16
|
-
import { parseForm4Xml
|
|
14
|
+
import { parseForm4Xml } from "./insider-data";
|
|
17
15
|
import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
|
|
18
16
|
import {
|
|
19
17
|
buildInsiderTransactionDetailBody,
|
|
@@ -23,48 +21,18 @@ import {
|
|
|
23
21
|
renderFilingNotice,
|
|
24
22
|
} from "../sec/filing-display";
|
|
25
23
|
import { useSecFilingContentCache } from "../sec/filing-content";
|
|
24
|
+
import {
|
|
25
|
+
buildInsiderSummary,
|
|
26
|
+
type ParsedInsiderFiling as ParsedFiling,
|
|
27
|
+
} from "./model";
|
|
28
|
+
import { insiderHeadless } from "./headless";
|
|
29
|
+
|
|
30
|
+
export { insiderHeadless } from "./headless";
|
|
26
31
|
|
|
27
32
|
const FORM4_PAGE_SIZE = 20;
|
|
28
33
|
// Recent EDGAR dumps cap at 1,000 mixed forms. Older archives are fetched
|
|
29
34
|
// until this many filings or company history ends.
|
|
30
35
|
const SEC_FILING_SCAN_LIMIT = 20_000;
|
|
31
|
-
const NINETY_DAYS_MS = 90 * 24 * 60 * 60 * 1000;
|
|
32
|
-
|
|
33
|
-
interface ParsedFiling {
|
|
34
|
-
filing: SecFilingItem;
|
|
35
|
-
transaction: InsiderTransaction | null;
|
|
36
|
-
isLoading: boolean;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Null while filings are still parsing, so the pane never claims "no activity" early. */
|
|
40
|
-
function buildSummary(parsed: ParsedFiling[]): string | null {
|
|
41
|
-
if (parsed.some(({ isLoading }) => isLoading)) return null;
|
|
42
|
-
const cutoff = new Date(Date.now() - NINETY_DAYS_MS);
|
|
43
|
-
let buyShares = 0;
|
|
44
|
-
let sellShares = 0;
|
|
45
|
-
let buyValue = 0;
|
|
46
|
-
let sellValue = 0;
|
|
47
|
-
|
|
48
|
-
for (const { transaction } of parsed) {
|
|
49
|
-
if (!transaction) continue;
|
|
50
|
-
const txDate = transaction.filingDate instanceof Date ? transaction.filingDate : new Date(transaction.filingDate);
|
|
51
|
-
if (isNaN(txDate.getTime()) || txDate < cutoff) continue;
|
|
52
|
-
if (transaction.transactionType === "P") {
|
|
53
|
-
buyShares += transaction.shares;
|
|
54
|
-
buyValue += transaction.totalValue ?? 0;
|
|
55
|
-
} else if (transaction.transactionType === "S") {
|
|
56
|
-
sellShares += transaction.shares;
|
|
57
|
-
sellValue += transaction.totalValue ?? 0;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (buyShares === 0 && sellShares === 0) return "No buy/sell activity in last 90 days.";
|
|
62
|
-
|
|
63
|
-
const parts: string[] = [];
|
|
64
|
-
if (buyShares > 0) parts.push(`Bought ${formatCompact(buyShares)} shares (${formatCurrency(buyValue)})`);
|
|
65
|
-
if (sellShares > 0) parts.push(`Sold ${formatCompact(sellShares)} shares (${formatCurrency(sellValue)})`);
|
|
66
|
-
return parts.join(" | ");
|
|
67
|
-
}
|
|
68
36
|
|
|
69
37
|
function toFeedItems(parsed: ParsedFiling[]): FeedDataTableItem[] {
|
|
70
38
|
return parsed.map(({ filing, transaction, isLoading }) => {
|
|
@@ -165,7 +133,7 @@ function InsiderView({ width, height, focused }: { width: number; height: number
|
|
|
165
133
|
: allParsed
|
|
166
134
|
), [allParsed, nameFilter]);
|
|
167
135
|
const feedItems = useMemo(() => toFeedItems(parsed), [parsed]);
|
|
168
|
-
const summary = useMemo(() =>
|
|
136
|
+
const summary = useMemo(() => buildInsiderSummary(allParsed), [allParsed]);
|
|
169
137
|
const selectedTransaction = parsed[selectedIdx]?.transaction ?? null;
|
|
170
138
|
const openFiling = openItemId
|
|
171
139
|
? parsed.find(({ filing }) => filing.accessionNumber === openItemId)?.filing ?? null
|
|
@@ -271,15 +239,18 @@ export const insiderModule: PluginModule = {
|
|
|
271
239
|
],
|
|
272
240
|
|
|
273
241
|
paneTemplates: [
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
242
|
+
{
|
|
243
|
+
...createTickerSurfacePaneTemplate({
|
|
244
|
+
id: "insider-pane",
|
|
245
|
+
paneId: "insider",
|
|
246
|
+
label: "Insider",
|
|
247
|
+
description: "Insider transaction activity for the selected ticker.",
|
|
248
|
+
keywords: ["insider", "form 4", "ownership", "transactions", "ins"],
|
|
249
|
+
shortcut: "INS",
|
|
250
|
+
canCreate: (_context, options) => !options?.ticker || isUsEquityTicker(options.ticker),
|
|
251
|
+
}),
|
|
252
|
+
headless: insiderHeadless,
|
|
253
|
+
},
|
|
283
254
|
],
|
|
284
255
|
|
|
285
256
|
setup(ctx) {
|