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,111 @@
1
+ import type {
2
+ HeadlessPaneColumn,
3
+ HeadlessPaneContext,
4
+ HeadlessPaneDefinition,
5
+ HeadlessPaneLoadArgs,
6
+ } from "../../../types/plugin";
7
+ import { formatCompact, formatNumber } from "../../../utils/format";
8
+ import { fetchShortInterest } from "./client";
9
+ import {
10
+ buildRows,
11
+ sortRows,
12
+ type SortPreference,
13
+ } from "./model";
14
+ import type { ShortInterestRecord } from "./types";
15
+
16
+ const SHORT_INTEREST_COLUMNS: HeadlessPaneColumn[] = [
17
+ { key: "settlementDate", header: "Date" },
18
+ {
19
+ key: "sharesShort",
20
+ header: "Shares Short",
21
+ align: "right",
22
+ format: (value) => formatCompact(Number(value)),
23
+ },
24
+ {
25
+ key: "daysToCover",
26
+ header: "Days to Cover",
27
+ align: "right",
28
+ format: (value) => value == null ? "-" : formatNumber(Number(value), 2),
29
+ },
30
+ {
31
+ key: "averageDailyVolume",
32
+ header: "Avg Daily Vol",
33
+ align: "right",
34
+ format: (value) => value == null ? "-" : formatCompact(Number(value)),
35
+ },
36
+ {
37
+ key: "shortPercentFloat",
38
+ header: "% Float",
39
+ align: "right",
40
+ format: (value) => value == null ? "-" : `${formatNumber(Number(value), 2)}%`,
41
+ },
42
+ ];
43
+
44
+ export interface ShortInterestHeadlessDependencies {
45
+ loadRecords(
46
+ symbol: string,
47
+ args: HeadlessPaneLoadArgs,
48
+ ctx: HeadlessPaneContext,
49
+ ): Promise<ShortInterestRecord[]>;
50
+ }
51
+
52
+ const defaultDependencies: ShortInterestHeadlessDependencies = {
53
+ loadRecords: (symbol, _args, ctx) => fetchShortInterest(symbol, ctx.apiClient),
54
+ };
55
+
56
+ export function createShortInterestHeadless(
57
+ dependencies: ShortInterestHeadlessDependencies = defaultDependencies,
58
+ ): HeadlessPaneDefinition<"rows"> {
59
+ return {
60
+ shape: "rows",
61
+ argument: {
62
+ kind: "ticker",
63
+ placeholder: "ticker",
64
+ description: "US equity ticker.",
65
+ },
66
+ options: [
67
+ {
68
+ key: "order",
69
+ description: "Settlement date order.",
70
+ type: "enum",
71
+ values: [{ value: "newest" }, { value: "oldest" }],
72
+ defaultValue: "newest",
73
+ },
74
+ {
75
+ key: "limit",
76
+ aliases: ["count", "rows"],
77
+ description: "Maximum settlement rows.",
78
+ type: "integer",
79
+ defaultValue: 24,
80
+ minimum: 1,
81
+ maximum: 100,
82
+ },
83
+ ],
84
+ columns: SHORT_INTEREST_COLUMNS,
85
+ describe: (args) => `Short Interest | ${args.symbols[0]}`,
86
+ async load(args, ctx) {
87
+ const symbol = args.symbols[0]!;
88
+ const records = await dependencies.loadRecords(symbol, args, ctx);
89
+ const preference: SortPreference = {
90
+ columnId: "settlementDate",
91
+ direction: args.options.order === "oldest" ? "asc" : "desc",
92
+ };
93
+ const limit = Number(args.options.limit);
94
+ const rows = sortRows(buildRows(records), preference)
95
+ .slice(0, limit)
96
+ .map((row) => ({
97
+ settlementDate: row.settlementDate,
98
+ sharesShort: row.record.sharesShort,
99
+ daysToCover: row.record.shortRatio,
100
+ averageDailyVolume: row.record.averageDailyVolume,
101
+ shortPercentFloat: row.record.shortPercentFloat,
102
+ }));
103
+ return {
104
+ rows,
105
+ metadata: { symbol, order: args.options.order },
106
+ };
107
+ },
108
+ };
109
+ }
110
+
111
+ export const shortInterestHeadless = createShortInterestHeadless();
@@ -5,8 +5,11 @@ import {
5
5
  resetShortInterestHealth,
6
6
  YAHOO_SHORT_INTEREST_CONNECTION_ID,
7
7
  } from "./client";
8
+ import { shortInterestHeadless } from "./headless";
8
9
  import { ShortInterestView } from "./pane";
9
10
 
11
+ export { shortInterestHeadless } from "./headless";
12
+
10
13
  let disposeConnection: (() => void) | null = null;
11
14
 
12
15
  export const shortInterestModule: PluginModule = {
@@ -50,14 +53,17 @@ export const shortInterestModule: PluginModule = {
50
53
  ],
51
54
 
52
55
  paneTemplates: [
53
- createTickerSurfacePaneTemplate({
54
- id: "short-interest-pane",
55
- paneId: "short-interest",
56
- label: "Short Interest",
57
- // Yahoo's key-statistics module only carries the current and prior settlement dates.
58
- description: "Bi-monthly short interest settlements from FINRA with days to cover and average daily volume.",
59
- keywords: ["short", "interest", "si", "shorts", "borrow", "days", "cover"],
60
- shortcut: "SI",
61
- }),
56
+ {
57
+ ...createTickerSurfacePaneTemplate({
58
+ id: "short-interest-pane",
59
+ paneId: "short-interest",
60
+ label: "Short Interest",
61
+ // Yahoo's key-statistics module only carries the current and prior settlement dates.
62
+ description: "Bi-monthly short interest settlements from FINRA with days to cover and average daily volume.",
63
+ keywords: ["short", "interest", "si", "shorts", "borrow", "days", "cover"],
64
+ shortcut: "SI",
65
+ }),
66
+ headless: shortInterestHeadless,
67
+ },
62
68
  ],
63
69
  };
@@ -45,35 +45,37 @@ export async function loadBrowserRows(
45
45
  tab: ThirteenFBrowserTab,
46
46
  query: string,
47
47
  signal?: AbortSignal,
48
- options: { forceRefresh?: boolean; offset?: number } = {},
48
+ options: { forceRefresh?: boolean; offset?: number; limit?: number } = {},
49
49
  ): Promise<BrowserLoadResult> {
50
50
  const now = new Date();
51
51
  const from = dateYearsAgo(2, now);
52
52
  const to = todayIso(now);
53
53
  const quarter = latestLikely13FQuarter(now);
54
54
  const offset = Math.max(0, options.offset ?? 0);
55
+ const browserLimit = Math.max(1, options.limit ?? BROWSER_PAGE_LIMIT);
56
+ const latestLimit = Math.max(1, options.limit ?? LATEST_FILINGS_PAGE_LIMIT);
55
57
  const apiOptions = { forceRefresh: options.forceRefresh };
56
58
  const pageApiOptions = { forceRefresh: options.forceRefresh, offset };
57
59
 
58
60
  if (tab === "performance") {
59
- const topFunds = await listTopThirteenFFunds(quarter, BROWSER_PAGE_LIMIT, signal, pageApiOptions);
61
+ const topFunds = await listTopThirteenFFunds(quarter, browserLimit, signal, pageApiOptions);
60
62
  const funds: ThirteenFFund[] = topFunds.map((fund) => ({ cik: fund.cik, name: fund.name }));
61
- const forms = await loadLatestFormsForFunds(funds.slice(0, FORM_ENRICHMENT_LIMIT), { from, to, signal, concurrency: 5, forceRefresh: options.forceRefresh });
63
+ const forms = await loadLatestFormsForFunds(funds.slice(0, Math.min(browserLimit, FORM_ENRICHMENT_LIMIT)), { from, to, signal, concurrency: 5, forceRefresh: options.forceRefresh });
62
64
  return {
63
65
  rows: buildBrowserRows({ topFunds, forms, source: "performance" }),
64
66
  quarter,
65
67
  period: topFunds[0]?.periodOfReport,
66
- hasMore: topFunds.length >= BROWSER_PAGE_LIMIT,
68
+ hasMore: topFunds.length >= browserLimit,
67
69
  nextOffset: offset + topFunds.length,
68
70
  };
69
71
  }
70
72
 
71
73
  if (tab === "latest") {
72
- const filings = await listThirteenFFilings(recentIso(21, now), to, LATEST_FILINGS_PAGE_LIMIT, signal, pageApiOptions);
74
+ const filings = await listThirteenFFilings(recentIso(21, now), to, latestLimit, signal, pageApiOptions);
73
75
  return {
74
76
  rows: buildBrowserRows({ latestFilings: filings, source: "latest" }),
75
77
  period: filings[0]?.periodOfReport,
76
- hasMore: filings.length >= LATEST_FILINGS_PAGE_LIMIT,
78
+ hasMore: filings.length >= latestLimit,
77
79
  nextOffset: offset + filings.length,
78
80
  };
79
81
  }
@@ -97,7 +99,7 @@ export async function loadBrowserRows(
97
99
  try {
98
100
  const holders = await lookupThirteenFHoldersByCusip(cusip, periodOfReport, signal, apiOptions);
99
101
  const forms = new Map<string, ThirteenFFormSummary>();
100
- const pageCiks = holders.ciks.slice(offset, offset + BROWSER_PAGE_LIMIT);
102
+ const pageCiks = holders.ciks.slice(offset, offset + browserLimit);
101
103
  const funds = await Promise.all(pageCiks.map(async (cik): Promise<ThirteenFFund> => {
102
104
  const fundForms = await listThirteenFForms(cik, from, to, 1, signal, apiOptions);
103
105
  if (fundForms[0]) forms.set(cik, fundForms[0]);
@@ -136,10 +138,10 @@ export async function loadBrowserRows(
136
138
  nextOffset: offset + 1,
137
139
  };
138
140
  }
139
- const funds = await searchThirteenFFunds(trimmed, BROWSER_PAGE_LIMIT, signal, pageApiOptions);
141
+ const funds = await searchThirteenFFunds(trimmed, browserLimit, signal, pageApiOptions);
140
142
  const [forms, topFunds] = await Promise.all([
141
- loadLatestFormsForFunds(funds.slice(0, FORM_ENRICHMENT_LIMIT), { from, to, signal, concurrency: 5, forceRefresh: options.forceRefresh }),
142
- listTopThirteenFFunds(quarter, BROWSER_PAGE_LIMIT, signal, { forceRefresh: options.forceRefresh }).catch(() => []),
143
+ loadLatestFormsForFunds(funds.slice(0, Math.min(browserLimit, FORM_ENRICHMENT_LIMIT)), { from, to, signal, concurrency: 5, forceRefresh: options.forceRefresh }),
144
+ listTopThirteenFFunds(quarter, browserLimit, signal, { forceRefresh: options.forceRefresh }).catch(() => []),
143
145
  ]);
144
146
  const topByCik = new Map(topFunds.map((fund) => [fund.cik, fund]));
145
147
  const matchedTopFunds = funds
@@ -153,7 +155,7 @@ export async function loadBrowserRows(
153
155
  source: "funds",
154
156
  }),
155
157
  quarter,
156
- hasMore: funds.length >= BROWSER_PAGE_LIMIT,
158
+ hasMore: funds.length >= browserLimit,
157
159
  nextOffset: offset + funds.length,
158
160
  };
159
161
  }
@@ -0,0 +1,112 @@
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 { createThirteenFHeadless } from "./headless";
6
+ import type { FundDetailData } from "./types";
7
+
8
+ const detail: FundDetailData = {
9
+ cik: "0001067983",
10
+ name: "Berkshire Hathaway",
11
+ forms: [
12
+ {
13
+ url: "https://www.sec.gov/latest",
14
+ accessionNumber: "latest",
15
+ submissionType: "13F-HR",
16
+ periodOfReport: "2026-06-30",
17
+ filedAsOfDate: "2026-08-14",
18
+ cik: "0001067983",
19
+ companyName: "Berkshire Hathaway",
20
+ tableValueTotal: 300,
21
+ tableEntryTotal: 2,
22
+ isAmendment: false,
23
+ },
24
+ {
25
+ url: "https://www.sec.gov/previous",
26
+ accessionNumber: "previous",
27
+ submissionType: "13F-HR",
28
+ periodOfReport: "2026-03-31",
29
+ filedAsOfDate: "2026-05-15",
30
+ cik: "0001067983",
31
+ companyName: "Berkshire Hathaway",
32
+ tableValueTotal: 200,
33
+ tableEntryTotal: 1,
34
+ isAmendment: false,
35
+ },
36
+ ],
37
+ latestForm: null,
38
+ previousForm: null,
39
+ latestHoldings: [],
40
+ previousHoldings: [],
41
+ };
42
+ detail.latestForm = detail.forms[0]!;
43
+ detail.previousForm = detail.forms[1]!;
44
+ detail.latestHoldings = [
45
+ {
46
+ accessionNumber: "latest",
47
+ cik: detail.cik,
48
+ issuer: "Apple Inc",
49
+ titleOfClass: "COM",
50
+ cusip: "037833100",
51
+ ticker: "AAPL",
52
+ value: 300,
53
+ shares: 3,
54
+ shareType: "SH",
55
+ investmentDiscretion: "DFND",
56
+ votingAuthoritySole: 3,
57
+ votingAuthorityShared: 0,
58
+ votingAuthorityNone: 0,
59
+ putCall: "",
60
+ },
61
+ ];
62
+ detail.previousHoldings = [{ ...detail.latestHoldings[0]!, accessionNumber: "previous", value: 200, shares: 2 }];
63
+
64
+ function context(): HeadlessPaneContext {
65
+ return {
66
+ marketData: createTestDataProvider(),
67
+ apiClient: {} as HeadlessPaneContext["apiClient"],
68
+ config: createDefaultConfig("/tmp/gloomberb-headless-thirteenf"),
69
+ signal: new AbortController().signal,
70
+ };
71
+ }
72
+
73
+ function args(view: string, limit = 50): HeadlessPaneLoadArgs {
74
+ return {
75
+ rawArgument: "1067983",
76
+ argument: "1067983",
77
+ symbols: [],
78
+ options: { view, limit },
79
+ };
80
+ }
81
+
82
+ describe("13F headless model", () => {
83
+ test("projects browser rows and switches to fund holdings", async () => {
84
+ const headless = createThirteenFHeadless({
85
+ loadBrowser: async () => ({
86
+ rows: [
87
+ { id: "one", cik: detail.cik, name: detail.name, estQuarterReturn: 8.2, source: "performance" },
88
+ { id: "two", cik: "0000000002", name: "Second Fund", estQuarterReturn: 4.1, source: "performance" },
89
+ ],
90
+ quarter: "2026Q2",
91
+ }),
92
+ loadDetail: async () => detail,
93
+ });
94
+
95
+ const browser = await headless.load(args("performance", 1), context());
96
+ expect(browser.rows).toEqual([expect.objectContaining({
97
+ cik: detail.cik,
98
+ name: detail.name,
99
+ estQuarterReturn: 8.2,
100
+ })]);
101
+
102
+ const holdings = await headless.load(args("holdings", 5), context());
103
+ expect(holdings.rows).toEqual([expect.objectContaining({
104
+ ticker: "AAPL",
105
+ value: 300,
106
+ sharesChange: 1,
107
+ action: "add",
108
+ actionLabel: "Add",
109
+ })]);
110
+ expect(holdings.metadata).toMatchObject({ view: "holdings", fund: detail.name });
111
+ });
112
+ });
@@ -0,0 +1,286 @@
1
+ import type {
2
+ HeadlessPaneColumn,
3
+ HeadlessPaneContext,
4
+ HeadlessPaneDefinition,
5
+ HeadlessPaneLoadArgs,
6
+ } from "../../../types/plugin";
7
+ import { normalizeCik } from "./api";
8
+ import {
9
+ loadBrowserRows,
10
+ loadFundDetail,
11
+ type BrowserLoadResult,
12
+ } from "./data";
13
+ import {
14
+ DEFAULT_BROWSER_SORT,
15
+ DEFAULT_HOLDING_SORT,
16
+ DEFAULT_TIMELINE_SORT,
17
+ buildFundHoldingRows,
18
+ buildTimelineRows,
19
+ inferBrowserTabFromQuery,
20
+ isCikQuery,
21
+ positionType,
22
+ sortBrowserRows,
23
+ sortHoldingRows,
24
+ sortTimelineRows,
25
+ } from "./model";
26
+ import {
27
+ actionLabel,
28
+ formatChangeShares,
29
+ formatMoneyCompact,
30
+ formatPercentMaybe,
31
+ formatRawPercentMaybe,
32
+ formatShares,
33
+ formatShortDate,
34
+ } from "./format";
35
+ import type {
36
+ FundDetailData,
37
+ ThirteenFBrowserTab,
38
+ } from "./types";
39
+
40
+ const BROWSER_COLUMNS: HeadlessPaneColumn[] = [
41
+ { key: "name", header: "Fund" },
42
+ { key: "cik", header: "CIK" },
43
+ { key: "periodOfReport", header: "Period" },
44
+ {
45
+ key: "estQuarterReturn",
46
+ header: "Est 13F",
47
+ align: "right",
48
+ format: (value) => formatRawPercentMaybe(value == null ? null : Number(value)),
49
+ },
50
+ {
51
+ key: "tableValueTotal",
52
+ header: "Value",
53
+ align: "right",
54
+ format: (value) => formatMoneyCompact(value == null ? null : Number(value)),
55
+ },
56
+ { key: "tableEntryTotal", header: "Rows", align: "right" },
57
+ {
58
+ key: "filedAsOfDate",
59
+ header: "Filed",
60
+ format: (value) => formatShortDate(typeof value === "string" ? value : null),
61
+ },
62
+ ];
63
+
64
+ const HOLDING_COLUMNS: HeadlessPaneColumn[] = [
65
+ { key: "ticker", header: "Ticker" },
66
+ { key: "type", header: "Type" },
67
+ { key: "issuer", header: "Issuer" },
68
+ {
69
+ key: "value",
70
+ header: "Value",
71
+ align: "right",
72
+ format: (value) => formatMoneyCompact(value == null ? null : Number(value)),
73
+ },
74
+ {
75
+ key: "estimatedPnl",
76
+ header: "Est P&L",
77
+ align: "right",
78
+ format: (value) => formatMoneyCompact(value == null ? null : Number(value)),
79
+ },
80
+ {
81
+ key: "weight",
82
+ header: "Weight",
83
+ align: "right",
84
+ format: (value) => formatPercentMaybe(value == null ? null : Number(value)),
85
+ },
86
+ {
87
+ key: "shares",
88
+ header: "Shares",
89
+ align: "right",
90
+ format: (value) => formatShares(value == null ? null : Number(value)),
91
+ },
92
+ {
93
+ key: "sharesChange",
94
+ header: "QoQ",
95
+ align: "right",
96
+ format: (value) => formatChangeShares(value == null ? null : Number(value)),
97
+ },
98
+ { key: "actionLabel", header: "Action" },
99
+ ];
100
+
101
+ const FILING_COLUMNS: HeadlessPaneColumn[] = [
102
+ { key: "periodOfReport", header: "Period" },
103
+ {
104
+ key: "filedAsOfDate",
105
+ header: "Filed",
106
+ format: (value) => formatShortDate(typeof value === "string" ? value : null),
107
+ },
108
+ {
109
+ key: "tableValueTotal",
110
+ header: "Value",
111
+ align: "right",
112
+ format: (value) => formatMoneyCompact(value == null ? null : Number(value)),
113
+ },
114
+ { key: "tableEntryTotal", header: "Rows", align: "right" },
115
+ {
116
+ key: "valueChangePercent",
117
+ header: "Value%",
118
+ align: "right",
119
+ format: (value) => formatPercentMaybe(value == null ? null : Number(value)),
120
+ },
121
+ { key: "submissionType", header: "Form" },
122
+ ];
123
+
124
+ type HeadlessThirteenFView =
125
+ | "auto"
126
+ | "performance"
127
+ | "funds"
128
+ | "by-ticker"
129
+ | "latest"
130
+ | "holdings"
131
+ | "filings";
132
+
133
+ export interface ThirteenFHeadlessDependencies {
134
+ loadBrowser(
135
+ tab: ThirteenFBrowserTab,
136
+ query: string,
137
+ limit: number,
138
+ args: HeadlessPaneLoadArgs,
139
+ ctx: HeadlessPaneContext,
140
+ ): Promise<BrowserLoadResult>;
141
+ loadDetail(
142
+ cik: string,
143
+ name: string,
144
+ args: HeadlessPaneLoadArgs,
145
+ ctx: HeadlessPaneContext,
146
+ ): Promise<FundDetailData>;
147
+ }
148
+
149
+ const defaultDependencies: ThirteenFHeadlessDependencies = {
150
+ loadBrowser: (tab, query, limit, _args, ctx) => loadBrowserRows(
151
+ tab,
152
+ query,
153
+ ctx.signal,
154
+ { limit },
155
+ ),
156
+ loadDetail: (cik, name, _args, ctx) => loadFundDetail(cik, name, ctx.signal),
157
+ };
158
+
159
+ function browserTab(view: HeadlessThirteenFView, query: string): ThirteenFBrowserTab {
160
+ if (view === "performance" || view === "funds" || view === "latest") return view;
161
+ if (view === "by-ticker") return "byTicker";
162
+ return inferBrowserTabFromQuery(query);
163
+ }
164
+
165
+ async function resolveFund(
166
+ query: string,
167
+ limit: number,
168
+ args: HeadlessPaneLoadArgs,
169
+ ctx: HeadlessPaneContext,
170
+ dependencies: ThirteenFHeadlessDependencies,
171
+ ): Promise<{ cik: string; name: string }> {
172
+ if (!query) throw new Error("13F holdings and filings require a fund name or CIK.");
173
+ if (isCikQuery(query)) return { cik: normalizeCik(query), name: query };
174
+ const result = await dependencies.loadBrowser("funds", query, Math.min(limit, 25), args, ctx);
175
+ const exact = result.rows.find((row) => row.name.toLocaleLowerCase() === query.toLocaleLowerCase());
176
+ const fund = exact ?? result.rows[0];
177
+ if (!fund) throw new Error(`No 13F fund found for "${query}".`);
178
+ return { cik: fund.cik, name: fund.name };
179
+ }
180
+
181
+ export function createThirteenFHeadless(
182
+ dependencies: ThirteenFHeadlessDependencies = defaultDependencies,
183
+ ): HeadlessPaneDefinition<"rows"> {
184
+ return {
185
+ shape: "rows",
186
+ argument: {
187
+ kind: "free-text",
188
+ placeholder: "fund, ticker, or CIK",
189
+ description: "Optional fund name, ticker, CIK, or latest keyword.",
190
+ optional: true,
191
+ },
192
+ options: [
193
+ {
194
+ key: "view",
195
+ description: "Fund browser mode or detail tab.",
196
+ type: "enum",
197
+ values: [
198
+ { value: "auto" },
199
+ { value: "performance" },
200
+ { value: "funds" },
201
+ { value: "by-ticker", aliases: ["ticker", "byTicker"] },
202
+ { value: "latest" },
203
+ { value: "holdings" },
204
+ { value: "filings" },
205
+ ],
206
+ defaultValue: "auto",
207
+ },
208
+ {
209
+ key: "limit",
210
+ aliases: ["count", "rows"],
211
+ description: "Maximum rows.",
212
+ type: "integer",
213
+ defaultValue: 50,
214
+ minimum: 1,
215
+ maximum: 200,
216
+ },
217
+ ],
218
+ describe: (args) => `13F Funds | ${String(args.options.view)}`,
219
+ async load(args, ctx) {
220
+ const query = typeof args.argument === "string" ? args.argument.trim() : "";
221
+ const requestedView = String(args.options.view) as HeadlessThirteenFView;
222
+ const view = requestedView === "auto" && isCikQuery(query) ? "holdings" : requestedView;
223
+ const limit = Number(args.options.limit);
224
+
225
+ if (view === "holdings" || view === "filings") {
226
+ const fund = await resolveFund(query, limit, args, ctx, dependencies);
227
+ const detail = await dependencies.loadDetail(fund.cik, fund.name, args, ctx);
228
+ if (view === "filings") {
229
+ const rows = sortTimelineRows(buildTimelineRows(detail.forms), DEFAULT_TIMELINE_SORT)
230
+ .slice(0, limit)
231
+ .map((row) => ({ ...row }));
232
+ return {
233
+ columns: FILING_COLUMNS,
234
+ rows,
235
+ metadata: {
236
+ view,
237
+ cik: detail.cik,
238
+ fund: detail.name,
239
+ latestPeriod: detail.latestForm?.periodOfReport ?? null,
240
+ },
241
+ };
242
+ }
243
+ const rows = sortHoldingRows(
244
+ buildFundHoldingRows(detail).filter((row) => row.value != null),
245
+ DEFAULT_HOLDING_SORT,
246
+ )
247
+ .slice(0, limit)
248
+ .map((row) => ({
249
+ ...row,
250
+ type: positionType(row),
251
+ actionLabel: actionLabel(row.action),
252
+ }));
253
+ return {
254
+ columns: HOLDING_COLUMNS,
255
+ rows,
256
+ metadata: {
257
+ view,
258
+ cik: detail.cik,
259
+ fund: detail.name,
260
+ latestPeriod: detail.latestForm?.periodOfReport ?? null,
261
+ previousPeriod: detail.previousForm?.periodOfReport ?? null,
262
+ },
263
+ };
264
+ }
265
+
266
+ const tab = browserTab(view, query);
267
+ const result = await dependencies.loadBrowser(tab, query, limit, args, ctx);
268
+ return {
269
+ columns: BROWSER_COLUMNS,
270
+ rows: sortBrowserRows(result.rows, DEFAULT_BROWSER_SORT)
271
+ .slice(0, limit)
272
+ .map((row) => ({ ...row })),
273
+ ...(result.warning ? { errors: [result.warning] } : {}),
274
+ metadata: {
275
+ view: tab,
276
+ query: query || null,
277
+ period: result.period ?? null,
278
+ quarter: result.quarter ?? null,
279
+ hasMore: result.hasMore ?? false,
280
+ },
281
+ };
282
+ },
283
+ };
284
+ }
285
+
286
+ export const thirteenFHeadless = createThirteenFHeadless();
@@ -11,6 +11,9 @@ import {
11
11
  attachThirteenFApiPersistence,
12
12
  resetThirteenFApiPersistence,
13
13
  } from "./api";
14
+ import { thirteenFHeadless } from "./headless";
15
+
16
+ export { thirteenFHeadless } from "./headless";
14
17
 
15
18
  function queryFromOptions(options?: PaneTemplateCreateOptions): string {
16
19
  return (options?.arg ?? options?.values?.query ?? "").trim();
@@ -55,6 +58,7 @@ export const thirteenFModule: PluginModule = {
55
58
  argKind: "text",
56
59
  argOptional: true,
57
60
  },
61
+ headless: thirteenFHeadless,
58
62
  createInstance(_context: PaneTemplateContext, options?: PaneTemplateCreateOptions) {
59
63
  const query = queryFromOptions(options);
60
64
  const browserMode = inferBrowserTabFromQuery(query);
@@ -615,6 +615,19 @@ export function selectedIndexById<T extends { id: string }>(rows: T[], selectedI
615
615
  return index >= 0 ? index : rows.length > 0 ? 0 : -1;
616
616
  }
617
617
 
618
+ export function positionType(row: {
619
+ putCall: string;
620
+ titleOfClass: string;
621
+ shareType: string;
622
+ }): string {
623
+ const putCall = row.putCall.trim().toUpperCase();
624
+ if (putCall === "PUT" || putCall === "CALL") return putCall;
625
+ const title = row.titleOfClass.trim().toUpperCase();
626
+ if (title) return title;
627
+ const shareType = row.shareType.trim().toUpperCase();
628
+ return shareType || "--";
629
+ }
630
+
618
631
  export function isCikQuery(query: string): boolean {
619
632
  return CIK_RE.test(query.trim());
620
633
  }