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
@@ -12,6 +12,7 @@ import {
12
12
  formatShares,
13
13
  formatShortDate,
14
14
  } from "./format";
15
+ import { positionType } from "./model";
15
16
  import type {
16
17
  FilingPositionColumn,
17
18
  FilingPositionRow,
@@ -82,7 +83,7 @@ export function renderHoldingCell(
82
83
  attributes: TextAttributes.BOLD,
83
84
  };
84
85
  case "type":
85
- return { text: formatPositionType(row), color: selectedColor ?? colors.textDim };
86
+ return { text: positionType(row), color: selectedColor ?? colors.textDim };
86
87
  case "issuer":
87
88
  return { text: row.issuer, color: selectedColor ?? colors.text };
88
89
  case "value":
@@ -136,7 +137,7 @@ export function renderFilingPositionCell(
136
137
  attributes: TextAttributes.BOLD,
137
138
  };
138
139
  case "type":
139
- return { text: formatPositionType(row), color: selectedColor ?? colors.textDim };
140
+ return { text: positionType(row), color: selectedColor ?? colors.textDim };
140
141
  case "issuer":
141
142
  return { text: row.issuer, color: selectedColor ?? colors.text };
142
143
  case "value":
@@ -185,19 +186,6 @@ export function renderTimelineCell(
185
186
  }
186
187
  }
187
188
 
188
- function formatPositionType(row: {
189
- putCall: string;
190
- titleOfClass: string;
191
- shareType: string;
192
- }): string {
193
- const putCall = row.putCall.trim().toUpperCase();
194
- if (putCall === "PUT" || putCall === "CALL") return putCall;
195
- const title = row.titleOfClass.trim().toUpperCase();
196
- if (title) return title;
197
- const shareType = row.shareType.trim().toUpperCase();
198
- return shareType || "--";
199
- }
200
-
201
189
  function actionColor(action: FundHoldingRow["action"]): string {
202
190
  switch (action) {
203
191
  case "new":
@@ -1,4 +1,6 @@
1
1
  import { Box } from "../../../ui";
2
+ import { getCurrentPluginTarget } from "../../current-target";
3
+ import { recordResearchActivity } from "../../../api-client/research-activity";
2
4
  import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from "react";
3
5
  import type { PaneProps, TickerResearchTabDef } from "../../../types/plugin";
4
6
  import { t, tf } from "../../../i18n";
@@ -98,7 +100,19 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
98
100
  TICKER_RESEARCH_TAB_COMMIT_DELAY_MS,
99
101
  { commitPendingOnUnmount: true },
100
102
  );
103
+ useEffect(() => {
104
+ if (!focused || !ticker || getCurrentPluginTarget() !== "web") return;
105
+ const url = new URL(window.location.href);
106
+ url.searchParams.set("ticker", ticker.metadata.ticker);
107
+ url.searchParams.set("tab", activeTabId);
108
+ window.history.replaceState(window.history.state, "", url.href);
109
+ }, [focused, ticker?.metadata.ticker, activeTabId]);
101
110
  const [pluginCaptured, setPluginCaptured] = useState(false);
111
+ useEffect(() => {
112
+ if (focused && Number.isFinite(financials?.quote?.price) && (financials?.quote?.price ?? 0) > 0) {
113
+ recordResearchActivity("research_viewed", "research");
114
+ }
115
+ }, [focused, financials?.quote?.price]);
102
116
  const [mountedTabIds, setMountedTabIds] = useState<Set<string>>(() => new Set());
103
117
  const paneSettings = getTickerResearchPaneSettings(paneInstance?.settings);
104
118
  const hasOptionsChain = !!resolveOptionsTarget(ticker)?.effectiveTicker;
@@ -0,0 +1,34 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
3
+ import { createTreasuryAuctionsHeadless } from "./headless";
4
+ import type { TreasuryAuction } from "./types";
5
+
6
+ const bill: TreasuryAuction = {
7
+ id: "bill", cusip: "A", secType: "Bill", securityTerm: "4-Week", auctionDate: "2026-09-03",
8
+ highInvestmentRate: 4.1, highYield: null, avgMedYield: null, highPrice: null, lowPrice: null,
9
+ avgMedPrice: null, bidToCoverRatio: 2.8, competitiveAccepted: 80, indirectAccepted: 20,
10
+ primaryDealerAccepted: 10, totalAccepted: 100, offeringAmount: 100,
11
+ };
12
+ const bond: TreasuryAuction = { ...bill, id: "bond", cusip: "B", secType: "Bond", securityTerm: "30-Year" };
13
+
14
+ function args(filter: string): HeadlessPaneLoadArgs {
15
+ return { rawArgument: "", argument: null, symbols: [], options: { historyDays: "90", filter } };
16
+ }
17
+
18
+ describe("treasury auctions headless model", () => {
19
+ test("applies history and security filters to derived auction rows", async () => {
20
+ const requested: number[] = [];
21
+ const headless = createTreasuryAuctionsHeadless({
22
+ load: async (_args, historyDays) => {
23
+ requested.push(historyDays);
24
+ return { auctions: [bill, bond], fetchedAt: 123, stale: false };
25
+ },
26
+ });
27
+
28
+ const all = await headless.load(args("all"), {} as HeadlessPaneContext);
29
+ const bonds = await headless.load(args("bond"), {} as HeadlessPaneContext);
30
+ expect(all.rows).toHaveLength(2);
31
+ expect(bonds.rows).toEqual([expect.objectContaining({ id: "bond", rate: 4.1, indirectPercent: 20 })]);
32
+ expect(requested).toEqual([90, 90]);
33
+ });
34
+ });
@@ -0,0 +1,95 @@
1
+ import type {
2
+ HeadlessPaneDefinition,
3
+ HeadlessPaneLoadArgs,
4
+ } from "../../../types/plugin";
5
+ import { formatCompact } from "../../../utils/format";
6
+ import { fetchTreasuryAuctions } from "./client";
7
+ import { loadTreasuryAuctions, type TreasuryAuctionsResult } from "./cache";
8
+ import {
9
+ auctionSize,
10
+ DEFAULT_AUCTION_SORT,
11
+ indirectPct,
12
+ rateValue,
13
+ visibleAuctions,
14
+ type AuctionFilter,
15
+ } from "./model";
16
+
17
+ const COLUMNS = [
18
+ { key: "auctionDate", header: "Date" },
19
+ { key: "secType", header: "Type" },
20
+ { key: "securityTerm", header: "Term" },
21
+ { key: "rate", header: "Rate", align: "right" as const, format: (value: unknown) => value == null ? "-" : `${Number(value).toFixed(3)}%` },
22
+ { key: "bidToCoverRatio", header: "B/C", align: "right" as const, format: (value: unknown) => value == null ? "-" : Number(value).toFixed(2) },
23
+ { key: "indirectPercent", header: "Indirect", align: "right" as const, format: (value: unknown) => value == null ? "-" : `${Number(value).toFixed(1)}%` },
24
+ { key: "size", header: "Size", align: "right" as const, format: (value: unknown) => value == null ? "-" : `$${formatCompact(Number(value))}` },
25
+ { key: "cusip", header: "CUSIP" },
26
+ ];
27
+
28
+ export interface TreasuryAuctionsHeadlessDependencies {
29
+ load(args: HeadlessPaneLoadArgs, historyDays: number): Promise<TreasuryAuctionsResult>;
30
+ }
31
+
32
+ const defaultDependencies: TreasuryAuctionsHeadlessDependencies = {
33
+ load: (_args, historyDays) => loadTreasuryAuctions(false, fetchTreasuryAuctions, historyDays),
34
+ };
35
+
36
+ export function createTreasuryAuctionsHeadless(
37
+ dependencies: TreasuryAuctionsHeadlessDependencies = defaultDependencies,
38
+ ): HeadlessPaneDefinition<"rows"> {
39
+ return {
40
+ shape: "rows",
41
+ argument: {
42
+ kind: "free-text",
43
+ placeholder: "search",
44
+ description: "Optional security type, term, or auction date filter.",
45
+ optional: true,
46
+ },
47
+ options: [
48
+ {
49
+ key: "historyDays",
50
+ aliases: ["history"],
51
+ settingKey: "historyDays",
52
+ description: "Auction history window in days.",
53
+ type: "enum",
54
+ values: [{ value: "30" }, { value: "90" }, { value: "120" }, { value: "365" }],
55
+ defaultValue: "120",
56
+ },
57
+ {
58
+ key: "filter",
59
+ description: "Security type group.",
60
+ type: "enum",
61
+ values: [{ value: "all" }, { value: "bill" }, { value: "note" }, { value: "bond" }],
62
+ defaultValue: "all",
63
+ },
64
+ ],
65
+ columns: COLUMNS,
66
+ describe: (args) => `Treasury Auctions | ${String(args.options.historyDays)} days | ${String(args.options.filter)}`,
67
+ async load(args) {
68
+ const historyDays = Number(args.options.historyDays);
69
+ const filter = args.options.filter as AuctionFilter;
70
+ const result = await dependencies.load(args, historyDays);
71
+ const query = typeof args.argument === "string" ? args.argument : "";
72
+ const auctions = visibleAuctions(result.auctions, {
73
+ filter,
74
+ query,
75
+ sort: DEFAULT_AUCTION_SORT,
76
+ });
77
+ return {
78
+ rows: auctions.map((auction) => ({
79
+ ...auction,
80
+ rate: rateValue(auction),
81
+ indirectPercent: indirectPct(auction),
82
+ size: auctionSize(auction),
83
+ })),
84
+ metadata: {
85
+ fetchedAt: result.fetchedAt,
86
+ stale: result.stale,
87
+ historyDays,
88
+ filter,
89
+ },
90
+ };
91
+ },
92
+ };
93
+ }
94
+
95
+ export const treasuryAuctionsHeadless = createTreasuryAuctionsHeadless();
@@ -8,6 +8,9 @@ import {
8
8
  import { TreasuryAuctionsPane } from "./pane";
9
9
  import { TREASURY_AUCTIONS_PANE_ID } from "./types";
10
10
  import { createPublicPaneShare } from "../shared/public-pane";
11
+ import { treasuryAuctionsHeadless } from "./headless";
12
+
13
+ export { treasuryAuctionsHeadless } from "./headless";
11
14
 
12
15
  let disposeConnection: (() => void) | null = null;
13
16
 
@@ -86,6 +89,7 @@ export const treasuryAuctionsModule: PluginModule = {
86
89
  "issuance",
87
90
  ],
88
91
  shortcut: { prefix: "AUCT" },
92
+ headless: treasuryAuctionsHeadless,
89
93
  createInstance: () => ({ placement: "floating" }),
90
94
  publicShare: createPublicPaneShare("Treasury Auctions"),
91
95
  },
@@ -21,6 +21,11 @@ export interface VolatilityLoadResult {
21
21
 
22
22
  const HISTORY_LIMIT = 120;
23
23
 
24
+ export type VolatilitySeriesLoader = (
25
+ seriesId: VolatilitySeriesId,
26
+ options: { limit?: number; sortOrder?: "asc" | "desc" },
27
+ ) => Promise<VolatilitySeriesInput>;
28
+
24
29
  function requestFor(seriesId: VolatilitySeriesId): FredSeriesRequest {
25
30
  return { seriesId, limit: HISTORY_LIMIT, sortOrder: "desc" };
26
31
  }
@@ -48,11 +53,12 @@ export function getCachedVolatilityData(): VolatilityLoadResult | null {
48
53
  async function loadSeries(
49
54
  seriesId: VolatilitySeriesId,
50
55
  force: boolean,
56
+ loader: VolatilitySeriesLoader,
51
57
  ): Promise<FredSeriesLoadResult> {
52
58
  const request = requestFor(seriesId);
53
59
  return loadCachedFredSeries(
54
60
  request,
55
- async () => toInput({ data: await apiClient.getCloudFredSeries(seriesId, {
61
+ async () => toInput({ data: await loader(seriesId, {
56
62
  limit: request.limit,
57
63
  sortOrder: request.sortOrder,
58
64
  }) }),
@@ -60,9 +66,16 @@ async function loadSeries(
60
66
  );
61
67
  }
62
68
 
63
- export async function loadVolatilityData(force = false): Promise<VolatilityLoadResult> {
69
+ const defaultSeriesLoader: VolatilitySeriesLoader = (seriesId, options) => (
70
+ apiClient.getCloudFredSeries(seriesId, options)
71
+ );
72
+
73
+ export async function loadVolatilityData(
74
+ force = false,
75
+ loader: VolatilitySeriesLoader = defaultSeriesLoader,
76
+ ): Promise<VolatilityLoadResult> {
64
77
  const settled = await Promise.allSettled(
65
- VOLATILITY_SERIES.map(({ seriesId }) => loadSeries(seriesId, force)),
78
+ VOLATILITY_SERIES.map(({ seriesId }) => loadSeries(seriesId, force, loader)),
66
79
  );
67
80
  const inputs: Partial<Record<VolatilitySeriesId, VolatilitySeriesInput>> = {};
68
81
  const errors: string[] = [];
@@ -0,0 +1,35 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
3
+ import { createVolatilityHeadless } from "./headless";
4
+
5
+ const args: HeadlessPaneLoadArgs = { rawArgument: "", argument: null, symbols: [], options: {} };
6
+
7
+ describe("volatility headless model", () => {
8
+ test("projects aligned tenors and the derived curve state", async () => {
9
+ const headless = createVolatilityHeadless({
10
+ load: async () => ({
11
+ data: {
12
+ metrics: [
13
+ { seriesId: "VIXCLS", label: "VIX", tenor: "30D", title: "VIX", value: 18, date: "2026-09-03", history: [] },
14
+ { seriesId: "VXVCLS", label: "VIX 3M", tenor: "3M", title: "VIX 3M", value: 21, date: "2026-09-03", history: [] },
15
+ ],
16
+ termPoints: [],
17
+ termDate: "2026-09-03",
18
+ ratio: 21 / 18,
19
+ slope: 3,
20
+ ratioHistory: [],
21
+ termState: "normal",
22
+ },
23
+ stale: false,
24
+ errors: [],
25
+ }),
26
+ });
27
+ const result = await headless.load(args, {} as HeadlessPaneContext);
28
+
29
+ expect(result.sections[0]?.entries?.slice(0, 2)).toEqual([
30
+ { label: "State", value: "normal" },
31
+ { label: "As of", value: "2026-09-03" },
32
+ ]);
33
+ expect(result.sections[1]?.rows?.[0]).toMatchObject({ label: "VIX", value: 18 });
34
+ });
35
+ });
@@ -0,0 +1,75 @@
1
+ import type {
2
+ HeadlessBundleResult,
3
+ HeadlessPaneDefinition,
4
+ HeadlessPaneLoadArgs,
5
+ } from "../../../types/plugin";
6
+ import {
7
+ loadVolatilityData,
8
+ type VolatilityLoadResult,
9
+ type VolatilitySeriesLoader,
10
+ } from "./client";
11
+ import type { VolatilityData } from "./model";
12
+
13
+ function formattedValue(value: number | null, digits = 2): string {
14
+ return value == null ? "-" : value.toFixed(digits);
15
+ }
16
+
17
+ export function projectVolatilityHeadless(data: VolatilityData): HeadlessBundleResult {
18
+ return {
19
+ sections: [
20
+ {
21
+ title: "Term structure",
22
+ entries: [
23
+ { label: "State", value: data.termState },
24
+ { label: "As of", value: data.termDate },
25
+ { label: "3M / 30D", value: data.ratio, formatted: formattedValue(data.ratio) },
26
+ { label: "3M spread", value: data.slope, formatted: data.slope == null ? "-" : `${data.slope > 0 ? "+" : ""}${data.slope.toFixed(2)} pts` },
27
+ ],
28
+ },
29
+ {
30
+ title: "Volatility tenors",
31
+ columns: [
32
+ { key: "label", header: "Index" },
33
+ { key: "tenor", header: "Tenor" },
34
+ { key: "value", header: "Close", align: "right", format: (value) => formattedValue(value as number | null) },
35
+ { key: "date", header: "As of" },
36
+ { key: "title", header: "Series" },
37
+ ],
38
+ rows: data.metrics.map((metric) => ({ ...metric })),
39
+ },
40
+ ],
41
+ };
42
+ }
43
+
44
+ export interface VolatilityHeadlessDependencies {
45
+ load(args: HeadlessPaneLoadArgs, loader: VolatilitySeriesLoader): Promise<VolatilityLoadResult>;
46
+ }
47
+
48
+ const defaultDependencies: VolatilityHeadlessDependencies = {
49
+ load: (_args, loader) => loadVolatilityData(false, loader),
50
+ };
51
+
52
+ export function createVolatilityHeadless(
53
+ dependencies: VolatilityHeadlessDependencies = defaultDependencies,
54
+ ): HeadlessPaneDefinition<"bundle"> {
55
+ return {
56
+ shape: "bundle",
57
+ argument: { kind: "none" },
58
+ options: [],
59
+ describe: "VIX 30D/3M Curve",
60
+ async load(args, ctx) {
61
+ const result = await dependencies.load(
62
+ args,
63
+ (seriesId, options) => ctx.apiClient.getCloudFredSeries(seriesId, options),
64
+ );
65
+ const projected = projectVolatilityHeadless(result.data);
66
+ return {
67
+ ...projected,
68
+ errors: result.errors,
69
+ metadata: { stale: result.stale },
70
+ };
71
+ },
72
+ };
73
+ }
74
+
75
+ export const volatilityHeadless = createVolatilityHeadless();
@@ -18,6 +18,9 @@ import { colors } from "../../../theme/colors";
18
18
  import type { PluginModule } from "../plugin-module";
19
19
  import { getCachedVolatilityData, loadVolatilityData } from "./client";
20
20
  import type { TermState, VolatilityData } from "./model";
21
+ import { volatilityHeadless } from "./headless";
22
+
23
+ export { volatilityHeadless } from "./headless";
21
24
 
22
25
  function formatValue(value: number | null, suffix = ""): string {
23
26
  return value == null || !Number.isFinite(value) ? "--" : `${value.toFixed(2)}${suffix}`;
@@ -189,5 +192,6 @@ export const volatilityModule: PluginModule = {
189
192
  description: "Aligned daily 30-day and three-month VIX implied-volatility closes from FRED.",
190
193
  keywords: ["vix", "volatility", "implied volatility", "curve", "slope", "normal", "inverted", "macro"],
191
194
  shortcut: { prefix: "VIX" },
195
+ headless: volatilityHeadless,
192
196
  }],
193
197
  };
@@ -0,0 +1,43 @@
1
+ import type { DataProvider } from "../../../types/data-provider";
2
+ import type { Quote } from "../../../types/financials";
3
+ import type { IndexEntry } from "./indices";
4
+
5
+ export interface WorldIndexQuoteResult {
6
+ quotes: Map<string, Quote | null>;
7
+ errors: string[];
8
+ }
9
+
10
+ function errorMessage(error: unknown): string {
11
+ return error instanceof Error ? error.message : String(error);
12
+ }
13
+
14
+ export async function loadWorldIndexQuotes(
15
+ entries: readonly IndexEntry[],
16
+ provider: DataProvider,
17
+ ): Promise<WorldIndexQuoteResult> {
18
+ const quotes = new Map<string, Quote | null>();
19
+ const errors: string[] = [];
20
+ if (provider.getQuotesBatch) {
21
+ const results = await provider.getQuotesBatch(
22
+ entries.map((entry) => ({ symbol: entry.symbol, exchange: "" })),
23
+ );
24
+ const bySymbol = new Map(results.map((result) => [result.target.symbol, result]));
25
+ for (const entry of entries) {
26
+ const result = bySymbol.get(entry.symbol);
27
+ quotes.set(entry.symbol, result?.quote ?? null);
28
+ if (result?.error) errors.push(`${entry.symbol}: ${errorMessage(result.error)}`);
29
+ else if (!result?.quote) errors.push(`${entry.symbol}: quote unavailable`);
30
+ }
31
+ return { quotes, errors };
32
+ }
33
+
34
+ await Promise.all(entries.map(async (entry) => {
35
+ try {
36
+ quotes.set(entry.symbol, await provider.getQuote(entry.symbol, ""));
37
+ } catch (error) {
38
+ quotes.set(entry.symbol, null);
39
+ errors.push(`${entry.symbol}: ${errorMessage(error)}`);
40
+ }
41
+ }));
42
+ return { quotes, errors };
43
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
3
+ import { createWorldIndicesHeadless } from "./headless";
4
+
5
+ const args: HeadlessPaneLoadArgs = { rawArgument: "", argument: null, symbols: [], options: {} };
6
+
7
+ describe("world indices headless model", () => {
8
+ test("groups quote rows by the same world regions as the pane", async () => {
9
+ const headless = createWorldIndicesHeadless({
10
+ load: async (_args, entries) => ({
11
+ quotes: new Map(entries.map((entry) => [entry.symbol, {
12
+ symbol: entry.symbol,
13
+ price: entry.symbol === "^GSPC" ? 6_800 : 100,
14
+ currency: "USD",
15
+ change: 10,
16
+ changePercent: 0.5,
17
+ lastUpdated: 1_700_000_000_000,
18
+ marketState: "REGULAR" as const,
19
+ }])),
20
+ errors: [],
21
+ }),
22
+ });
23
+ const result = await headless.load(args, {} as HeadlessPaneContext);
24
+
25
+ expect(result.sections.map((section) => section.title)).toEqual([
26
+ "Americas", "Europe", "Asia-Pacific", "Other",
27
+ ]);
28
+ expect(result.sections[0]?.rows?.[0]).toMatchObject({
29
+ symbol: "^GSPC", shortName: "SPX", price: 6_800,
30
+ });
31
+ });
32
+ });
@@ -0,0 +1,109 @@
1
+ import type { DataProvider } from "../../../types/data-provider";
2
+ import type {
3
+ HeadlessBundleResult,
4
+ HeadlessPaneDefinition,
5
+ HeadlessPaneLoadArgs,
6
+ } from "../../../types/plugin";
7
+ import { formatCurrency, formatNumber, formatPercentRaw } from "../../../utils/format";
8
+ import { loadWorldIndexQuotes, type WorldIndexQuoteResult } from "./client";
9
+ import {
10
+ getIndicesByRegion,
11
+ REGION_LABELS,
12
+ REGION_ORDER,
13
+ WORLD_INDICES,
14
+ type IndexEntry,
15
+ } from "./indices";
16
+
17
+ const COLUMNS = [
18
+ { key: "shortName", header: "Index" },
19
+ { key: "name", header: "Name" },
20
+ {
21
+ key: "price",
22
+ header: "Last",
23
+ align: "right" as const,
24
+ format: (value: unknown, row: Record<string, unknown>) => value == null
25
+ ? "-"
26
+ : formatCurrency(Number(value), String(row.currency ?? "USD")),
27
+ },
28
+ {
29
+ key: "change",
30
+ header: "Change",
31
+ align: "right" as const,
32
+ format: (value: unknown) => value == null ? "-" : `${Number(value) >= 0 ? "+" : ""}${formatNumber(Number(value), 2)}`,
33
+ },
34
+ {
35
+ key: "changePercent",
36
+ header: "Change %",
37
+ align: "right" as const,
38
+ format: (value: unknown) => value == null ? "-" : formatPercentRaw(Number(value)),
39
+ },
40
+ { key: "marketState", header: "Session" },
41
+ {
42
+ key: "lastUpdated",
43
+ header: "Updated",
44
+ format: (value: unknown) => value == null ? "-" : new Date(Number(value)).toISOString(),
45
+ },
46
+ ];
47
+
48
+ export function projectWorldIndicesHeadless(
49
+ entries: readonly IndexEntry[],
50
+ loaded: WorldIndexQuoteResult,
51
+ ): HeadlessBundleResult {
52
+ const grouped = getIndicesByRegion(entries);
53
+ return {
54
+ sections: REGION_ORDER.flatMap((region) => {
55
+ const regionEntries = grouped.get(region) ?? [];
56
+ if (regionEntries.length === 0) return [];
57
+ return [{
58
+ title: REGION_LABELS[region],
59
+ columns: COLUMNS,
60
+ rows: regionEntries.map((entry) => {
61
+ const quote = loaded.quotes.get(entry.symbol);
62
+ return {
63
+ ...entry,
64
+ price: quote?.price ?? null,
65
+ currency: quote?.currency ?? null,
66
+ change: quote?.change ?? null,
67
+ changePercent: quote?.changePercent ?? null,
68
+ marketState: quote?.marketState ?? null,
69
+ lastUpdated: quote?.lastUpdated ?? null,
70
+ };
71
+ }),
72
+ }];
73
+ }),
74
+ errors: loaded.errors,
75
+ metadata: {
76
+ requested: entries.length,
77
+ available: [...loaded.quotes.values()].filter(Boolean).length,
78
+ },
79
+ };
80
+ }
81
+
82
+ export interface WorldIndicesHeadlessDependencies {
83
+ load(
84
+ args: HeadlessPaneLoadArgs,
85
+ entries: readonly IndexEntry[],
86
+ provider: DataProvider,
87
+ ): Promise<WorldIndexQuoteResult>;
88
+ }
89
+
90
+ const defaultDependencies: WorldIndicesHeadlessDependencies = {
91
+ load: (_args, entries, provider) => loadWorldIndexQuotes(entries, provider),
92
+ };
93
+
94
+ export function createWorldIndicesHeadless(
95
+ dependencies: WorldIndicesHeadlessDependencies = defaultDependencies,
96
+ ): HeadlessPaneDefinition<"bundle"> {
97
+ return {
98
+ shape: "bundle",
99
+ argument: { kind: "none" },
100
+ options: [],
101
+ describe: "World Equity Indices",
102
+ async load(args, ctx) {
103
+ const loaded = await dependencies.load(args, WORLD_INDICES, ctx.marketData);
104
+ return projectWorldIndicesHeadless(WORLD_INDICES, loaded);
105
+ },
106
+ };
107
+ }
108
+
109
+ export const worldIndicesHeadless = createWorldIndicesHeadless();
@@ -9,6 +9,7 @@ import { useQuoteBoard } from "../shared/use-quote-board";
9
9
  import { WORLD_INDICES, REGION_LABELS, getIndicesByRegion, resolveIndexEntries } from "./indices";
10
10
  import { useWorldIndicesFooter } from "./footer";
11
11
  import { createPublicPaneShare } from "../shared/public-pane";
12
+ import { worldIndicesHeadless } from "./headless";
12
13
  import {
13
14
  buildFlatRows,
14
15
  DEFAULT_SORT_PREFERENCE,
@@ -23,6 +24,8 @@ import {
23
24
  type WorldIndexColumn,
24
25
  } from "./table";
25
26
 
27
+ export { worldIndicesHeadless } from "./headless";
28
+
26
29
  /** Stable identity: a fresh literal here would remount the board every render. */
27
30
  const NO_SAVED_SYMBOLS: string[] = [];
28
31
 
@@ -160,6 +163,7 @@ export const worldIndicesModule: PluginModule = {
160
163
  description: "Monitor global equity indices grouped by region.",
161
164
  keywords: ["world", "indices", "global", "equity", "markets", "international"],
162
165
  shortcut: { prefix: "WEI" },
166
+ headless: worldIndicesHeadless,
163
167
  publicShare: createPublicPaneShare("World Equity Indices"),
164
168
  },
165
169
  ],
@@ -0,0 +1,24 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
3
+ import { createYieldCurveHeadless } from "./headless";
4
+
5
+ const args: HeadlessPaneLoadArgs = { rawArgument: "", argument: null, symbols: [], options: {} };
6
+
7
+ describe("yield curve headless model", () => {
8
+ test("returns ordered maturities and the shared 2Y to 10Y spread", async () => {
9
+ const headless = createYieldCurveHeadless({
10
+ load: async () => [
11
+ { maturity: "10Y", maturityYears: 10, yield: 4.1, asOf: "2026-09-03" },
12
+ { maturity: "2Y", maturityYears: 2, yield: 4.35, asOf: "2026-09-04" },
13
+ ],
14
+ });
15
+ const result = await headless.load(args, {} as HeadlessPaneContext);
16
+
17
+ expect(result.rows.map((row) => row.maturity)).toEqual(["2Y", "10Y"]);
18
+ expect(result.metadata).toEqual({
19
+ asOf: "2026-09-04",
20
+ inverted: true,
21
+ spread2Y10YBasisPoints: -25,
22
+ });
23
+ });
24
+ });