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.
Files changed (219) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -2
  3. package/src/api-client/auth.ts +2 -1
  4. package/src/api-client/index.test.ts +34 -0
  5. package/src/api-client/index.ts +10 -2
  6. package/src/api-client/request.ts +4 -1
  7. package/src/api-client/research-activity.ts +61 -0
  8. package/src/api-client/types.ts +2 -1
  9. package/src/cli/commands/plugins.ts +4 -3
  10. package/src/cli/desktop-pane-shot.ts +79 -10
  11. package/src/cli/pane-functions/capabilities.ts +76 -23
  12. package/src/cli/pane-functions/cloud-session.test.ts +54 -0
  13. package/src/cli/pane-functions/cloud-session.ts +38 -0
  14. package/src/cli/pane-functions/dom.test.ts +41 -0
  15. package/src/cli/pane-functions/dom.ts +199 -0
  16. package/src/cli/pane-functions/index.test.ts +13 -2
  17. package/src/cli/pane-functions/index.ts +9 -5
  18. package/src/cli/pane-functions/intraday-shot.test.ts +307 -0
  19. package/src/cli/pane-functions/intraday-shot.ts +334 -0
  20. package/src/cli/pane-functions/report.test.ts +29 -1
  21. package/src/cli/pane-functions/report.ts +68 -7
  22. package/src/cli/pane-functions/screenshot.ts +195 -42
  23. package/src/cli/restore-plugins.ts +10 -3
  24. package/src/components/chart/composite/composite-chart.test.tsx +68 -1
  25. package/src/components/chart/composite/composite-chart.tsx +87 -21
  26. package/src/components/chart/composite/rasterizer.ts +33 -0
  27. package/src/components/chart/composite/renderers.test.ts +51 -4
  28. package/src/components/chart/composite/scene.test.ts +128 -2
  29. package/src/components/chart/composite/scene.ts +92 -16
  30. package/src/components/chart/composite/text-renderer.ts +13 -0
  31. package/src/components/chart/composite/time-scale.ts +30 -10
  32. package/src/components/chart/composite/types.ts +26 -0
  33. package/src/components/command-bar/assist/runtime.ts +38 -30
  34. package/src/components/command-bar/surface/assist.test.tsx +63 -9
  35. package/src/components/command-bar/surface/test-harness.tsx +23 -4
  36. package/src/components/onboarding/account-step/chooser-panel.tsx +2 -2
  37. package/src/components/onboarding/account-step/qr-panel.tsx +2 -10
  38. package/src/components/onboarding/onboarding-wizard.test.tsx +38 -12
  39. package/src/components/onboarding/onboarding-wizard.tsx +49 -14
  40. package/src/data/config/store/index.test.ts +23 -0
  41. package/src/data/config/store/node.ts +7 -1
  42. package/src/data/config/store/normalize.ts +4 -0
  43. package/src/news/types.ts +1 -0
  44. package/src/plugins/builtin/cds/client.test.ts +20 -0
  45. package/src/plugins/builtin/cds/client.ts +13 -3
  46. package/src/plugins/builtin/cds/headless.test.ts +30 -0
  47. package/src/plugins/builtin/cds/headless.ts +113 -0
  48. package/src/plugins/builtin/cds/index.tsx +4 -0
  49. package/src/plugins/builtin/chart-composer/cli-options.ts +4 -1
  50. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  51. package/src/plugins/builtin/chat/controller/channels.ts +2 -1
  52. package/src/plugins/builtin/chat/controller/index.ts +13 -5
  53. package/src/plugins/builtin/chat/controller/realtime.ts +24 -0
  54. package/src/plugins/builtin/chat/controller/session-runtime.ts +76 -8
  55. package/src/plugins/builtin/chat/controller/state.ts +1 -0
  56. package/src/plugins/builtin/chat/controller.test.ts +260 -4
  57. package/src/plugins/builtin/cloud/askg/executor.test.ts +160 -0
  58. package/src/plugins/builtin/cloud/askg/executor.ts +429 -0
  59. package/src/plugins/builtin/cloud/askg/manifest.test.ts +153 -0
  60. package/src/plugins/builtin/cloud/askg/manifest.ts +314 -0
  61. package/src/plugins/builtin/cloud/askg/protocol.test.ts +190 -0
  62. package/src/plugins/builtin/cloud/askg/protocol.ts +286 -0
  63. package/src/plugins/builtin/cloud/askg/undo.test.ts +44 -0
  64. package/src/plugins/builtin/cloud/askg/undo.ts +277 -0
  65. package/src/plugins/builtin/cloud/askg/verify.ts +130 -0
  66. package/src/plugins/builtin/cloud/auth-dialog.tsx +7 -1
  67. package/src/plugins/builtin/cloud/device-signin-dialog.tsx +21 -8
  68. package/src/plugins/builtin/cloud/plugin.tsx +2 -0
  69. package/src/plugins/builtin/cloud/verification-panel.tsx +56 -0
  70. package/src/plugins/builtin/congress-trades/client.ts +11 -0
  71. package/src/plugins/builtin/congress-trades/detail.tsx +3 -3
  72. package/src/plugins/builtin/congress-trades/headless.test.ts +98 -0
  73. package/src/plugins/builtin/congress-trades/headless.ts +145 -0
  74. package/src/plugins/builtin/congress-trades/pane.tsx +3 -3
  75. package/src/plugins/builtin/credit-conditions/client.ts +1 -1
  76. package/src/plugins/builtin/credit-conditions/headless.test.ts +31 -0
  77. package/src/plugins/builtin/credit-conditions/headless.ts +55 -0
  78. package/src/plugins/builtin/credit-conditions/index.tsx +4 -0
  79. package/src/plugins/builtin/dividend-yield/headless.test.ts +88 -0
  80. package/src/plugins/builtin/dividend-yield/headless.ts +135 -0
  81. package/src/plugins/builtin/dividend-yield/index.tsx +12 -8
  82. package/src/plugins/builtin/dividend-yield/model.ts +4 -22
  83. package/src/plugins/builtin/dividend-yield/view.ts +17 -0
  84. package/src/plugins/builtin/earnings/headless.test.ts +82 -0
  85. package/src/plugins/builtin/earnings/headless.ts +162 -0
  86. package/src/plugins/builtin/earnings/index.tsx +2 -0
  87. package/src/plugins/builtin/earnings-calls/data.ts +29 -8
  88. package/src/plugins/builtin/earnings-calls/headless.test.ts +192 -0
  89. package/src/plugins/builtin/earnings-calls/headless.ts +300 -0
  90. package/src/plugins/builtin/earnings-calls/index.tsx +19 -9
  91. package/src/plugins/builtin/earnings-calls/model.ts +238 -0
  92. package/src/plugins/builtin/earnings-calls/pane.tsx +4 -0
  93. package/src/plugins/builtin/earnings-calls/transcript-view.tsx +13 -11
  94. package/src/plugins/builtin/fear-greed/headless.test.ts +53 -0
  95. package/src/plugins/builtin/fear-greed/headless.ts +84 -0
  96. package/src/plugins/builtin/fear-greed/index.tsx +4 -0
  97. package/src/plugins/builtin/holders/client.ts +29 -0
  98. package/src/plugins/builtin/holders/headless.test.ts +58 -0
  99. package/src/plugins/builtin/holders/headless.ts +162 -0
  100. package/src/plugins/builtin/holders/index.tsx +14 -8
  101. package/src/plugins/builtin/holders/pane.tsx +7 -1
  102. package/src/plugins/builtin/insider/client.ts +50 -0
  103. package/src/plugins/builtin/insider/headless.test.ts +122 -0
  104. package/src/plugins/builtin/insider/headless.ts +117 -0
  105. package/src/plugins/builtin/insider/index.tsx +21 -50
  106. package/src/plugins/builtin/insider/insider-data.ts +1 -1
  107. package/src/plugins/builtin/insider/model.ts +72 -0
  108. package/src/plugins/builtin/ipo-calendar/client.ts +15 -0
  109. package/src/plugins/builtin/ipo-calendar/headless.test.ts +89 -0
  110. package/src/plugins/builtin/ipo-calendar/headless.ts +127 -0
  111. package/src/plugins/builtin/ipo-calendar/index.tsx +2 -0
  112. package/src/plugins/builtin/ipo-calendar/model.ts +3 -15
  113. package/src/plugins/builtin/market-movers/client.ts +76 -0
  114. package/src/plugins/builtin/market-movers/headless.test.ts +34 -0
  115. package/src/plugins/builtin/market-movers/headless.ts +73 -0
  116. package/src/plugins/builtin/market-movers/index.tsx +11 -51
  117. package/src/plugins/builtin/news/headless.test.ts +138 -0
  118. package/src/plugins/builtin/news/headless.ts +190 -0
  119. package/src/plugins/builtin/news/index.tsx +13 -9
  120. package/src/plugins/builtin/news/wire/index.tsx +2 -1
  121. package/src/plugins/builtin/research/analyst-headless.test.ts +60 -0
  122. package/src/plugins/builtin/research/analyst-headless.ts +172 -0
  123. package/src/plugins/builtin/research/analyst-model.ts +219 -0
  124. package/src/plugins/builtin/research/analyst-pane.tsx +31 -221
  125. package/src/plugins/builtin/research/client.ts +12 -0
  126. package/src/plugins/builtin/research/corporate-actions-pane.tsx +3 -237
  127. package/src/plugins/builtin/research/event-model.ts +245 -0
  128. package/src/plugins/builtin/research/events-headless.test.ts +32 -0
  129. package/src/plugins/builtin/research/events-headless.ts +87 -0
  130. package/src/plugins/builtin/research/headless.test.ts +93 -0
  131. package/src/plugins/builtin/research/headless.ts +161 -0
  132. package/src/plugins/builtin/research/index.tsx +32 -18
  133. package/src/plugins/builtin/research-search/pane.tsx +4 -0
  134. package/src/plugins/builtin/scanner/headless.test.ts +59 -0
  135. package/src/plugins/builtin/scanner/headless.ts +125 -0
  136. package/src/plugins/builtin/scanner/index.ts +4 -0
  137. package/src/plugins/builtin/sec/client.ts +16 -0
  138. package/src/plugins/builtin/sec/headless.test.ts +72 -0
  139. package/src/plugins/builtin/sec/headless.ts +69 -0
  140. package/src/plugins/builtin/sec/index.tsx +20 -75
  141. package/src/plugins/builtin/sec/model.ts +89 -0
  142. package/src/plugins/builtin/sectors/client.ts +54 -0
  143. package/src/plugins/builtin/sectors/headless.test.ts +28 -0
  144. package/src/plugins/builtin/sectors/headless.ts +80 -0
  145. package/src/plugins/builtin/sectors/index.tsx +6 -51
  146. package/src/plugins/builtin/shared/cloud-upgrade.ts +8 -4
  147. package/src/plugins/builtin/short-interest/client.ts +5 -2
  148. package/src/plugins/builtin/short-interest/headless.test.ts +96 -0
  149. package/src/plugins/builtin/short-interest/headless.ts +111 -0
  150. package/src/plugins/builtin/short-interest/index.tsx +15 -9
  151. package/src/plugins/builtin/thirteenf/data.ts +13 -11
  152. package/src/plugins/builtin/thirteenf/headless.test.ts +112 -0
  153. package/src/plugins/builtin/thirteenf/headless.ts +286 -0
  154. package/src/plugins/builtin/thirteenf/index.tsx +4 -0
  155. package/src/plugins/builtin/thirteenf/model.ts +13 -0
  156. package/src/plugins/builtin/thirteenf/table.ts +3 -15
  157. package/src/plugins/builtin/ticker-detail/pane.tsx +14 -0
  158. package/src/plugins/builtin/treasury-auctions/headless.test.ts +34 -0
  159. package/src/plugins/builtin/treasury-auctions/headless.ts +95 -0
  160. package/src/plugins/builtin/treasury-auctions/index.tsx +4 -0
  161. package/src/plugins/builtin/volatility/client.ts +16 -3
  162. package/src/plugins/builtin/volatility/headless.test.ts +35 -0
  163. package/src/plugins/builtin/volatility/headless.ts +75 -0
  164. package/src/plugins/builtin/volatility/index.tsx +4 -0
  165. package/src/plugins/builtin/world-indices/client.ts +43 -0
  166. package/src/plugins/builtin/world-indices/headless.test.ts +32 -0
  167. package/src/plugins/builtin/world-indices/headless.ts +109 -0
  168. package/src/plugins/builtin/world-indices/index.tsx +4 -0
  169. package/src/plugins/builtin/yield-curve/headless.test.ts +24 -0
  170. package/src/plugins/builtin/yield-curve/headless.ts +67 -0
  171. package/src/plugins/builtin/yield-curve/index.tsx +6 -8
  172. package/src/plugins/builtin/yield-curve/treasury-data.ts +16 -6
  173. package/src/plugins/bundle.test.ts +58 -1
  174. package/src/plugins/bundle.ts +4 -2
  175. package/src/plugins/catalog-backend.test.ts +26 -0
  176. package/src/plugins/catalog-backend.ts +9 -2
  177. package/src/plugins/catalog-browser.test.ts +0 -13
  178. package/src/plugins/catalog-browser.ts +4 -0
  179. package/src/plugins/catalog-manifest.test.ts +15 -0
  180. package/src/plugins/catalog-manifest.ts +12 -5
  181. package/src/plugins/catalog-ui.ts +0 -6
  182. package/src/plugins/host-contract.ts +9 -0
  183. package/src/plugins/host-modules.ts +12 -0
  184. package/src/plugins/loader.ts +63 -15
  185. package/src/plugins/prediction-markets/headless.test.ts +119 -0
  186. package/src/plugins/prediction-markets/headless.ts +255 -0
  187. package/src/plugins/prediction-markets/index.tsx +2 -0
  188. package/src/plugins/seed.ts +3 -0
  189. package/src/public/broker.ts +6 -0
  190. package/src/remote/schema.test.ts +65 -0
  191. package/src/remote/schema.ts +322 -39
  192. package/src/remote/types.ts +49 -2
  193. package/src/renderers/browser/app-services.ts +3 -0
  194. package/src/renderers/browser/config-host.ts +24 -8
  195. package/src/renderers/browser/deeplink-bridge.test.ts +11 -0
  196. package/src/renderers/browser/deeplink-bridge.ts +15 -3
  197. package/src/renderers/browser/main.tsx +3 -1
  198. package/src/renderers/browser/research-entry.test.ts +8 -0
  199. package/src/renderers/browser/research-entry.ts +7 -0
  200. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +2 -2
  201. package/src/renderers/electrobun/bun/desktop/initialization.ts +10 -3
  202. package/src/renderers/electrobun/bun/external-plugins.ts +20 -4
  203. package/src/renderers/electrobun/view/build-assets.ts +0 -2
  204. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +63 -10
  205. package/src/sources/gloomberb-cloud/news.ts +1 -0
  206. package/src/sync/core-contributors.test.ts +111 -0
  207. package/src/sync/core-contributors.ts +155 -1
  208. package/src/time-series/resolve.test.ts +118 -1
  209. package/src/time-series/resolve.ts +14 -16
  210. package/src/types/config.ts +2 -0
  211. package/src/plugins/broker-sync/broker-sync.test.ts +0 -188
  212. package/src/plugins/broker-sync/native-loader.ts +0 -10
  213. package/src/plugins/broker-sync/normalize.ts +0 -308
  214. package/src/plugins/broker-sync/public.ts +0 -122
  215. package/src/plugins/broker-sync/robinhood-native.ts +0 -367
  216. package/src/plugins/broker-sync/robinhood.ts +0 -114
  217. package/src/plugins/broker-sync/simplefin-native.ts +0 -201
  218. package/src/plugins/broker-sync/simplefin.ts +0 -70
  219. 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
- createTickerSurfacePaneTemplate({
31
- id: "holders-pane",
32
- paneId: "holders",
33
- label: "Holders",
34
- description: "Institutional holders for the selected ticker.",
35
- keywords: ["holders", "ownership", "institutional", "owners", "hds"],
36
- shortcut: "HDS",
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 dataProvider.getHolders(symbol, exchange, forceRefresh ? { cacheMode: "refresh" } : undefined);
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, type InsiderTransaction } from "./insider-data";
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(() => buildSummary(allParsed), [allParsed]);
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
- createTickerSurfacePaneTemplate({
275
- id: "insider-pane",
276
- paneId: "insider",
277
- label: "Insider",
278
- description: "Insider transaction activity for the selected ticker.",
279
- keywords: ["insider", "form 4", "ownership", "transactions", "ins"],
280
- shortcut: "INS",
281
- canCreate: (_context, options) => !options?.ticker || isUsEquityTicker(options.ticker),
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) {
@@ -45,6 +45,6 @@ export function transactionTypeLabel(type: InsiderTransaction["transactionType"]
45
45
  case "S": return "SELL";
46
46
  case "A": return "AWARD";
47
47
  case "D": return "DISPOSE";
48
- default: return "—";
48
+ default: return "-";
49
49
  }
50
50
  }