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
@@ -9,21 +9,17 @@ import {
9
9
  } from "../../../components";
10
10
  import type { SecFilingDocument, SecFilingItem } from "../../../types/data-provider";
11
11
  import type {
12
- AnalystEstimateRecord,
13
12
  AnalystResearchData,
14
13
  CorporateActionsData,
15
- FinancialStatement,
16
- TickerFinancials,
17
14
  } from "../../../types/financials";
18
15
  import { blendHex, colors } from "../../../theme/colors";
19
- import { formatCompact, formatCurrency, formatNumber, formatPercent, formatPercentRaw } from "../../../utils/format";
16
+ import { formatCompact, formatNumber } from "../../../utils/format";
20
17
  import { isPlainKey } from "../../../utils/keyboard";
21
18
  import { wrapTextLines } from "../../../utils/text-wrap";
22
19
  import { useResolvedEntryValue, useSecFilingDocuments, useSecFilingsQuery } from "../../../market-data/hooks";
23
20
  import { instrumentFromTicker } from "../../../market-data/request-types";
24
21
  import { usePaneTicker } from "../../../state/app/context";
25
22
  import { isUsEquityTicker } from "../../../utils/sec";
26
- import { computeTTM } from "../ticker-detail/financials/aggregation";
27
23
  import { useAssetData } from "../../runtime";
28
24
  import { handleRefreshKey, loadingErrorFooterInfo } from "../shared/table-pane";
29
25
  import { useBoundTicker as useSymbolBinding, useTickerRequest } from "../shared/ticker-request";
@@ -38,33 +34,13 @@ import {
38
34
  buildInlineFilingContentTargets,
39
35
  useSecFilingContentCache,
40
36
  } from "../sec/filing-content";
37
+ import { buildEventRows, type EventRow, type EventStatus } from "./event-model";
41
38
 
42
- type EventStatus = "Earnings" | "Q Est" | "FY Est" | "TTM" | "Dividend" | "Split";
43
-
44
- type EventRow = {
45
- id: string;
46
- date: string;
47
- status: EventStatus;
48
- period: string;
49
- detail: string;
50
- qEps?: number;
51
- qRevenue?: number;
52
- annualEps?: number;
53
- annualRevenue?: number;
54
- value: string;
55
- tone: "positive" | "negative" | "muted" | "text";
56
- };
39
+ export { buildEventRows } from "./event-model";
57
40
 
58
41
  type EventColumnId = "date" | "status" | "period" | "qEps" | "qRevenue" | "annualEps" | "annualRevenue" | "value" | "detail";
59
42
  type EventColumn = DataTableColumn & { id: EventColumnId };
60
43
 
61
- type EstimatePair = {
62
- date: string;
63
- period: string;
64
- eps?: AnalystEstimateRecord;
65
- revenue?: AnalystEstimateRecord;
66
- };
67
-
68
44
  const SEC_EVENT_FILING_LIMIT = 50;
69
45
  const SEC_EVENT_MATCH_WINDOW_DAYS = 7;
70
46
 
@@ -76,36 +52,6 @@ function todayDateKey(): string {
76
52
  return `${year}-${month}-${day}`;
77
53
  }
78
54
 
79
- function formatPeriod(period: string): string {
80
- const label = period.replace(/_/g, " ");
81
- return label
82
- .replace(/\bcurrent\b/g, "cur")
83
- .replace(/\bquarter\b/g, "qtr")
84
- .replace(/\byear\b/g, "yr")
85
- || "-";
86
- }
87
-
88
- function isFiscalEstimatePeriod(period: string): boolean {
89
- return /\byear\b|^current_year$|^next_year$|^next_5y$/i.test(period);
90
- }
91
-
92
- function sortedQuarterlyStatements(financials: Pick<TickerFinancials, "quarterlyStatements"> | null): FinancialStatement[] {
93
- return [...(financials?.quarterlyStatements ?? [])]
94
- .filter((statement) => statement.date)
95
- .sort((left, right) => left.date.localeCompare(right.date));
96
- }
97
-
98
- function quarterLabel(statement: FinancialStatement | undefined): string {
99
- return statement?.date ? `Q${statement.date.slice(2)}` : "-";
100
- }
101
-
102
- function daysBetween(leftDate: string, rightDate: string): number {
103
- const left = new Date(`${leftDate}T00:00:00Z`).getTime();
104
- const right = new Date(`${rightDate}T00:00:00Z`).getTime();
105
- if (!Number.isFinite(left) || !Number.isFinite(right)) return Number.POSITIVE_INFINITY;
106
- return Math.abs(left - right) / 86_400_000;
107
- }
108
-
109
55
  function dateKeyToEpochDay(dateKey: string): number | null {
110
56
  const timestamp = new Date(`${dateKey}T00:00:00Z`).getTime();
111
57
  if (!Number.isFinite(timestamp)) return null;
@@ -175,186 +121,6 @@ export function matchEarningsSecFiling(row: { status: string; date: string } | n
175
121
  return best?.filing ?? null;
176
122
  }
177
123
 
178
- function statementForEarningsDate(
179
- quarterlyStatements: readonly FinancialStatement[],
180
- earningsDate: string,
181
- ): FinancialStatement | undefined {
182
- let best: FinancialStatement | undefined;
183
- for (const statement of quarterlyStatements) {
184
- if (statement.date > earningsDate) continue;
185
- if (!best || statement.date > best.date) best = statement;
186
- }
187
- return best && daysBetween(best.date, earningsDate) <= 140 ? best : undefined;
188
- }
189
-
190
- function estimateKey(estimate: AnalystEstimateRecord): string {
191
- return `${estimate.date}|${estimate.period}`;
192
- }
193
-
194
- function hasEstimateValue(estimate: AnalystEstimateRecord | undefined): boolean {
195
- return estimate?.average != null
196
- || estimate?.low != null
197
- || estimate?.high != null
198
- || estimate?.yearAgo != null
199
- || estimate?.growth != null
200
- || estimate?.analysts != null;
201
- }
202
-
203
- function buildEstimatePairs(data: AnalystResearchData | null): EstimatePair[] {
204
- const pairs = new Map<string, EstimatePair>();
205
- const ensurePair = (estimate: AnalystEstimateRecord): EstimatePair => {
206
- const key = estimateKey(estimate);
207
- const existing = pairs.get(key);
208
- if (existing) return existing;
209
- const pair: EstimatePair = { date: estimate.date, period: estimate.period };
210
- pairs.set(key, pair);
211
- return pair;
212
- };
213
-
214
- for (const estimate of data?.earningsEstimates ?? []) {
215
- if (hasEstimateValue(estimate)) ensurePair(estimate).eps = estimate;
216
- }
217
- for (const estimate of data?.revenueEstimates ?? []) {
218
- if (hasEstimateValue(estimate)) ensurePair(estimate).revenue = estimate;
219
- }
220
-
221
- return [...pairs.values()];
222
- }
223
-
224
- function formatEstimateDetail(pair: EstimatePair): string {
225
- const analystCounts: string[] = [];
226
- if (pair.eps?.analysts != null) analystCounts.push(`${pair.eps.analysts}E`);
227
- if (pair.revenue?.analysts != null) analystCounts.push(`${pair.revenue.analysts}R`);
228
- if (analystCounts.length) return analystCounts.join("/");
229
- return "Consensus";
230
- }
231
-
232
- function estimateTone(pair: EstimatePair): EventRow["tone"] {
233
- const growth = pair.eps?.growth ?? pair.revenue?.growth;
234
- if (growth == null) return "muted";
235
- return growth >= 0 ? "positive" : "negative";
236
- }
237
-
238
- function estimateValue(pair: EstimatePair): string {
239
- const growth = pair.eps?.growth ?? pair.revenue?.growth;
240
- return growth == null ? "-" : formatPercent(growth);
241
- }
242
-
243
- function ttmRow(quarterlyStatements: readonly FinancialStatement[]): EventRow | null {
244
- const latestFour = quarterlyStatements.slice(-4);
245
- const ttm = computeTTM([...quarterlyStatements]);
246
- if (!ttm || (ttm.totalRevenue == null && ttm.eps == null)) return null;
247
- const latest = latestFour.at(-1);
248
- return {
249
- id: `ttm:${latest?.date ?? ""}`,
250
- date: latest?.date ?? "",
251
- status: "TTM",
252
- period: "4 qtrs",
253
- detail: "sum",
254
- annualEps: ttm.eps,
255
- annualRevenue: ttm.totalRevenue,
256
- value: "-",
257
- tone: "muted",
258
- };
259
- }
260
-
261
- function earningsDetail(earning: CorporateActionsData["earnings"][number]): string {
262
- if (earning.epsActual == null) return "Pending";
263
- if (earning.difference == null) return "Reported";
264
- return `diff ${formatNumber(earning.difference, 2)}`;
265
- }
266
-
267
- function eventSortRank(status: EventStatus): number {
268
- switch (status) {
269
- case "Q Est":
270
- return 0;
271
- case "FY Est":
272
- return 1;
273
- case "Earnings":
274
- return 2;
275
- case "TTM":
276
- return 3;
277
- case "Dividend":
278
- return 4;
279
- case "Split":
280
- return 5;
281
- }
282
- }
283
-
284
- export function buildEventRows(
285
- data: CorporateActionsData | null,
286
- estimates: AnalystResearchData | null,
287
- financials: Pick<TickerFinancials, "quarterlyStatements"> | null,
288
- currency: string,
289
- ): EventRow[] {
290
- const rows: EventRow[] = [];
291
- const quarterlyStatements = sortedQuarterlyStatements(financials);
292
- const ttm = ttmRow(quarterlyStatements);
293
- if (ttm) rows.push(ttm);
294
-
295
- for (const earning of data?.earnings ?? []) {
296
- const statement = statementForEarningsDate(quarterlyStatements, earning.date);
297
- rows.push({
298
- id: `earn:${earning.date}`,
299
- date: earning.date,
300
- status: "Earnings",
301
- period: statement ? quarterLabel(statement) : earning.time?.trim() || "-",
302
- detail: earningsDetail(earning),
303
- qEps: earning.epsActual ?? statement?.eps ?? earning.epsEstimate,
304
- qRevenue: statement?.totalRevenue,
305
- value: earning.surprisePercent != null ? formatPercentRaw(earning.surprisePercent) : "-",
306
- tone: earning.surprisePercent == null ? "muted" : earning.surprisePercent >= 0 ? "positive" : "negative",
307
- });
308
- }
309
-
310
- for (const pair of buildEstimatePairs(estimates)) {
311
- const isFiscal = isFiscalEstimatePeriod(pair.period);
312
- rows.push({
313
- id: `estimate:${pair.date}:${pair.period}`,
314
- date: pair.date,
315
- status: isFiscal ? "FY Est" : "Q Est",
316
- period: formatPeriod(pair.period),
317
- detail: formatEstimateDetail(pair),
318
- qEps: isFiscal ? undefined : pair.eps?.average,
319
- qRevenue: isFiscal ? undefined : pair.revenue?.average,
320
- annualEps: isFiscal ? pair.eps?.average : undefined,
321
- annualRevenue: isFiscal ? pair.revenue?.average : undefined,
322
- value: estimateValue(pair),
323
- tone: estimateTone(pair),
324
- });
325
- }
326
-
327
- for (const dividend of data?.dividends ?? []) {
328
- rows.push({
329
- id: `div:${dividend.exDate}`,
330
- date: dividend.exDate,
331
- status: "Dividend",
332
- period: "-",
333
- detail: "Ex-date",
334
- value: formatCurrency(dividend.amount, currency),
335
- tone: "positive",
336
- });
337
- }
338
-
339
- for (const split of data?.splits ?? []) {
340
- rows.push({
341
- id: `split:${split.date}:${split.description ?? ""}`,
342
- date: split.date,
343
- status: "Split",
344
- period: "-",
345
- detail: split.description ?? "Split",
346
- value: split.fromFactor && split.toFactor ? `${split.fromFactor}:${split.toFactor}` : formatNumber(split.ratio, 4),
347
- tone: "muted",
348
- });
349
- }
350
-
351
- return rows.sort((left, right) => (
352
- right.date.localeCompare(left.date)
353
- || eventSortRank(left.status) - eventSortRank(right.status)
354
- || left.period.localeCompare(right.period)
355
- ));
356
- }
357
-
358
124
  function buildEventColumns(): EventColumn[] {
359
125
  return [
360
126
  { id: "date", label: "DATE", width: 10, align: "left" },
@@ -0,0 +1,245 @@
1
+ import type {
2
+ AnalystEstimateRecord,
3
+ AnalystResearchData,
4
+ CorporateActionsData,
5
+ FinancialStatement,
6
+ TickerFinancials,
7
+ } from "../../../types/financials";
8
+ import {
9
+ formatCurrency,
10
+ formatNumber,
11
+ formatPercent,
12
+ formatPercentRaw,
13
+ } from "../../../utils/format";
14
+ import { computeTTM } from "../ticker-detail/financials/aggregation";
15
+
16
+ export type EventStatus = "Earnings" | "Q Est" | "FY Est" | "TTM" | "Dividend" | "Split";
17
+
18
+ export interface EventRow {
19
+ id: string;
20
+ date: string;
21
+ status: EventStatus;
22
+ period: string;
23
+ detail: string;
24
+ qEps?: number;
25
+ qRevenue?: number;
26
+ annualEps?: number;
27
+ annualRevenue?: number;
28
+ value: string;
29
+ tone: "positive" | "negative" | "muted" | "text";
30
+ }
31
+
32
+ interface EstimatePair {
33
+ date: string;
34
+ period: string;
35
+ eps?: AnalystEstimateRecord;
36
+ revenue?: AnalystEstimateRecord;
37
+ }
38
+
39
+ function formatPeriod(period: string): string {
40
+ const label = period.replace(/_/g, " ");
41
+ return label
42
+ .replace(/\bcurrent\b/g, "cur")
43
+ .replace(/\bquarter\b/g, "qtr")
44
+ .replace(/\byear\b/g, "yr")
45
+ || "-";
46
+ }
47
+
48
+ function isFiscalEstimatePeriod(period: string): boolean {
49
+ return /\byear\b|^current_year$|^next_year$|^next_5y$/i.test(period);
50
+ }
51
+
52
+ function sortedQuarterlyStatements(
53
+ financials: Pick<TickerFinancials, "quarterlyStatements"> | null,
54
+ ): FinancialStatement[] {
55
+ return [...(financials?.quarterlyStatements ?? [])]
56
+ .filter((statement) => statement.date)
57
+ .sort((left, right) => left.date.localeCompare(right.date));
58
+ }
59
+
60
+ function quarterLabel(statement: FinancialStatement | undefined): string {
61
+ return statement?.date ? `Q${statement.date.slice(2)}` : "-";
62
+ }
63
+
64
+ function daysBetween(leftDate: string, rightDate: string): number {
65
+ const left = new Date(`${leftDate}T00:00:00Z`).getTime();
66
+ const right = new Date(`${rightDate}T00:00:00Z`).getTime();
67
+ if (!Number.isFinite(left) || !Number.isFinite(right)) return Number.POSITIVE_INFINITY;
68
+ return Math.abs(left - right) / 86_400_000;
69
+ }
70
+
71
+ function statementForEarningsDate(
72
+ quarterlyStatements: readonly FinancialStatement[],
73
+ earningsDate: string,
74
+ ): FinancialStatement | undefined {
75
+ let best: FinancialStatement | undefined;
76
+ for (const statement of quarterlyStatements) {
77
+ if (statement.date > earningsDate) continue;
78
+ if (!best || statement.date > best.date) best = statement;
79
+ }
80
+ return best && daysBetween(best.date, earningsDate) <= 140 ? best : undefined;
81
+ }
82
+
83
+ function estimateKey(estimate: AnalystEstimateRecord): string {
84
+ return `${estimate.date}|${estimate.period}`;
85
+ }
86
+
87
+ function hasEstimateValue(estimate: AnalystEstimateRecord | undefined): boolean {
88
+ return estimate?.average != null
89
+ || estimate?.low != null
90
+ || estimate?.high != null
91
+ || estimate?.yearAgo != null
92
+ || estimate?.growth != null
93
+ || estimate?.analysts != null;
94
+ }
95
+
96
+ function buildEstimatePairs(data: AnalystResearchData | null): EstimatePair[] {
97
+ const pairs = new Map<string, EstimatePair>();
98
+ const ensurePair = (estimate: AnalystEstimateRecord): EstimatePair => {
99
+ const key = estimateKey(estimate);
100
+ const existing = pairs.get(key);
101
+ if (existing) return existing;
102
+ const pair: EstimatePair = { date: estimate.date, period: estimate.period };
103
+ pairs.set(key, pair);
104
+ return pair;
105
+ };
106
+
107
+ for (const estimate of data?.earningsEstimates ?? []) {
108
+ if (hasEstimateValue(estimate)) ensurePair(estimate).eps = estimate;
109
+ }
110
+ for (const estimate of data?.revenueEstimates ?? []) {
111
+ if (hasEstimateValue(estimate)) ensurePair(estimate).revenue = estimate;
112
+ }
113
+
114
+ return [...pairs.values()];
115
+ }
116
+
117
+ function formatEstimateDetail(pair: EstimatePair): string {
118
+ const analystCounts: string[] = [];
119
+ if (pair.eps?.analysts != null) analystCounts.push(`${pair.eps.analysts}E`);
120
+ if (pair.revenue?.analysts != null) analystCounts.push(`${pair.revenue.analysts}R`);
121
+ if (analystCounts.length) return analystCounts.join("/");
122
+ return "Consensus";
123
+ }
124
+
125
+ function estimateTone(pair: EstimatePair): EventRow["tone"] {
126
+ const growth = pair.eps?.growth ?? pair.revenue?.growth;
127
+ if (growth == null) return "muted";
128
+ return growth >= 0 ? "positive" : "negative";
129
+ }
130
+
131
+ function estimateValue(pair: EstimatePair): string {
132
+ const growth = pair.eps?.growth ?? pair.revenue?.growth;
133
+ return growth == null ? "-" : formatPercent(growth);
134
+ }
135
+
136
+ function ttmRow(quarterlyStatements: readonly FinancialStatement[]): EventRow | null {
137
+ const latestFour = quarterlyStatements.slice(-4);
138
+ const ttm = computeTTM([...quarterlyStatements]);
139
+ if (!ttm || (ttm.totalRevenue == null && ttm.eps == null)) return null;
140
+ const latest = latestFour.at(-1);
141
+ return {
142
+ id: `ttm:${latest?.date ?? ""}`,
143
+ date: latest?.date ?? "",
144
+ status: "TTM",
145
+ period: "4 qtrs",
146
+ detail: "sum",
147
+ annualEps: ttm.eps,
148
+ annualRevenue: ttm.totalRevenue,
149
+ value: "-",
150
+ tone: "muted",
151
+ };
152
+ }
153
+
154
+ function earningsDetail(earning: CorporateActionsData["earnings"][number]): string {
155
+ if (earning.epsActual == null) return "Pending";
156
+ if (earning.difference == null) return "Reported";
157
+ return `diff ${formatNumber(earning.difference, 2)}`;
158
+ }
159
+
160
+ function eventSortRank(status: EventStatus): number {
161
+ switch (status) {
162
+ case "Q Est": return 0;
163
+ case "FY Est": return 1;
164
+ case "Earnings": return 2;
165
+ case "TTM": return 3;
166
+ case "Dividend": return 4;
167
+ case "Split": return 5;
168
+ }
169
+ }
170
+
171
+ export function buildEventRows(
172
+ data: CorporateActionsData | null,
173
+ estimates: AnalystResearchData | null,
174
+ financials: Pick<TickerFinancials, "quarterlyStatements"> | null,
175
+ currency: string,
176
+ ): EventRow[] {
177
+ const rows: EventRow[] = [];
178
+ const quarterlyStatements = sortedQuarterlyStatements(financials);
179
+ const ttm = ttmRow(quarterlyStatements);
180
+ if (ttm) rows.push(ttm);
181
+
182
+ for (const earning of data?.earnings ?? []) {
183
+ const statement = statementForEarningsDate(quarterlyStatements, earning.date);
184
+ rows.push({
185
+ id: `earn:${earning.date}`,
186
+ date: earning.date,
187
+ status: "Earnings",
188
+ period: statement ? quarterLabel(statement) : earning.time?.trim() || "-",
189
+ detail: earningsDetail(earning),
190
+ qEps: earning.epsActual ?? statement?.eps ?? earning.epsEstimate,
191
+ qRevenue: statement?.totalRevenue,
192
+ value: earning.surprisePercent != null ? formatPercentRaw(earning.surprisePercent) : "-",
193
+ tone: earning.surprisePercent == null ? "muted" : earning.surprisePercent >= 0 ? "positive" : "negative",
194
+ });
195
+ }
196
+
197
+ for (const pair of buildEstimatePairs(estimates)) {
198
+ const isFiscal = isFiscalEstimatePeriod(pair.period);
199
+ rows.push({
200
+ id: `estimate:${pair.date}:${pair.period}`,
201
+ date: pair.date,
202
+ status: isFiscal ? "FY Est" : "Q Est",
203
+ period: formatPeriod(pair.period),
204
+ detail: formatEstimateDetail(pair),
205
+ qEps: isFiscal ? undefined : pair.eps?.average,
206
+ qRevenue: isFiscal ? undefined : pair.revenue?.average,
207
+ annualEps: isFiscal ? pair.eps?.average : undefined,
208
+ annualRevenue: isFiscal ? pair.revenue?.average : undefined,
209
+ value: estimateValue(pair),
210
+ tone: estimateTone(pair),
211
+ });
212
+ }
213
+
214
+ for (const dividend of data?.dividends ?? []) {
215
+ rows.push({
216
+ id: `div:${dividend.exDate}`,
217
+ date: dividend.exDate,
218
+ status: "Dividend",
219
+ period: "-",
220
+ detail: "Ex-date",
221
+ value: formatCurrency(dividend.amount, currency),
222
+ tone: "positive",
223
+ });
224
+ }
225
+
226
+ for (const split of data?.splits ?? []) {
227
+ rows.push({
228
+ id: `split:${split.date}:${split.description ?? ""}`,
229
+ date: split.date,
230
+ status: "Split",
231
+ period: "-",
232
+ detail: split.description ?? "Split",
233
+ value: split.fromFactor && split.toFactor
234
+ ? `${split.fromFactor}:${split.toFactor}`
235
+ : formatNumber(split.ratio, 4),
236
+ tone: "muted",
237
+ });
238
+ }
239
+
240
+ return rows.sort((left, right) => (
241
+ right.date.localeCompare(left.date)
242
+ || eventSortRank(left.status) - eventSortRank(right.status)
243
+ || left.period.localeCompare(right.period)
244
+ ));
245
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { HeadlessPaneContext, HeadlessPaneLoadArgs } from "../../../types/plugin";
3
+ import { createEventsHeadless } from "./events-headless";
4
+
5
+ const args: HeadlessPaneLoadArgs = {
6
+ rawArgument: "AAPL", argument: "AAPL", symbols: ["AAPL"], options: {},
7
+ };
8
+
9
+ describe("corporate actions headless model", () => {
10
+ test("maps corporate actions through the same event projection as the pane", async () => {
11
+ const headless = createEventsHeadless({
12
+ load: async () => ({
13
+ actions: {
14
+ symbol: "AAPL",
15
+ currency: "USD",
16
+ dividends: [{ exDate: "2026-08-08", amount: 0.26 }],
17
+ splits: [],
18
+ earnings: [{ date: "2026-07-31", epsActual: 1.57, surprisePercent: 5.4 }],
19
+ },
20
+ estimates: null,
21
+ financials: null,
22
+ currency: "USD",
23
+ }),
24
+ });
25
+ const result = await headless.load(args, {} as HeadlessPaneContext);
26
+
27
+ expect(result.rows).toEqual([
28
+ expect.objectContaining({ status: "Dividend", value: "$0.26" }),
29
+ expect.objectContaining({ status: "Earnings", qEps: 1.57, value: "+5.40%" }),
30
+ ]);
31
+ });
32
+ });
@@ -0,0 +1,87 @@
1
+ import type { DataProvider } from "../../../types/data-provider";
2
+ import type {
3
+ AnalystResearchData,
4
+ CorporateActionsData,
5
+ TickerFinancials,
6
+ } from "../../../types/financials";
7
+ import type {
8
+ HeadlessPaneDefinition,
9
+ HeadlessPaneLoadArgs,
10
+ } from "../../../types/plugin";
11
+ import { formatCompact, formatNumber } from "../../../utils/format";
12
+ import { buildEventRows } from "./event-model";
13
+
14
+ const COLUMNS = [
15
+ { key: "date", header: "Date" },
16
+ { key: "status", header: "Event" },
17
+ { key: "period", header: "Period" },
18
+ { key: "qEps", header: "Q EPS", align: "right" as const, format: (value: unknown) => value == null ? "-" : formatNumber(Number(value), 2) },
19
+ { key: "qRevenue", header: "Q revenue", align: "right" as const, format: (value: unknown) => value == null ? "-" : formatCompact(Number(value)) },
20
+ { key: "annualEps", header: "Annual EPS", align: "right" as const, format: (value: unknown) => value == null ? "-" : formatNumber(Number(value), 2) },
21
+ { key: "annualRevenue", header: "Annual revenue", align: "right" as const, format: (value: unknown) => value == null ? "-" : formatCompact(Number(value)) },
22
+ { key: "value", header: "Value", align: "right" as const },
23
+ { key: "detail", header: "Detail" },
24
+ ];
25
+
26
+ export interface EventHeadlessData {
27
+ actions: CorporateActionsData | null;
28
+ estimates: AnalystResearchData | null;
29
+ financials: TickerFinancials | null;
30
+ currency: string;
31
+ }
32
+
33
+ export interface EventsHeadlessDependencies {
34
+ load(
35
+ args: HeadlessPaneLoadArgs,
36
+ symbol: string,
37
+ provider: DataProvider,
38
+ ): Promise<EventHeadlessData>;
39
+ }
40
+
41
+ const defaultDependencies: EventsHeadlessDependencies = {
42
+ async load(_args, symbol, provider) {
43
+ if (!provider.getCorporateActions) {
44
+ throw new Error("Corporate actions source unavailable");
45
+ }
46
+ const [actions, estimates, financials] = await Promise.all([
47
+ provider.getCorporateActions(symbol, ""),
48
+ provider.getAnalystResearch
49
+ ? provider.getAnalystResearch(symbol, "").catch(() => null)
50
+ : null,
51
+ provider.getTickerFinancials(symbol, "").catch(() => null),
52
+ ]);
53
+ return {
54
+ actions,
55
+ estimates,
56
+ financials,
57
+ currency: actions.currency ?? estimates?.currency ?? financials?.quote?.currency ?? "USD",
58
+ };
59
+ },
60
+ };
61
+
62
+ export function createEventsHeadless(
63
+ dependencies: EventsHeadlessDependencies = defaultDependencies,
64
+ ): HeadlessPaneDefinition<"rows"> {
65
+ return {
66
+ shape: "rows",
67
+ argument: {
68
+ kind: "ticker",
69
+ placeholder: "ticker",
70
+ description: "Ticker whose corporate actions and estimates should be returned.",
71
+ },
72
+ options: [],
73
+ columns: COLUMNS,
74
+ describe: (args) => `Corporate Actions | ${String(args.argument)}`,
75
+ async load(args, ctx) {
76
+ const symbol = args.symbols[0]!;
77
+ const data = await dependencies.load(args, symbol, ctx.marketData);
78
+ return {
79
+ rows: buildEventRows(data.actions, data.estimates, data.financials, data.currency)
80
+ .map((row) => ({ ...row })),
81
+ metadata: { symbol, currency: data.currency },
82
+ };
83
+ },
84
+ };
85
+ }
86
+
87
+ export const eventsHeadless = createEventsHeadless();