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,334 @@
1
+ import type { DataProvider } from "../../types/data-provider";
2
+ import type { PricePoint } from "../../types/financials";
3
+ import type { TimeRange } from "../../time-series/range";
4
+ import {
5
+ getPresetResolution,
6
+ isIntradayResolution,
7
+ type ManualChartResolution,
8
+ } from "../../time-series/resolution";
9
+ import { resolveExchangeTimeZone } from "../../utils/exchanges";
10
+ import { getPricePointTimestamp } from "../../utils/price-history";
11
+ import { zonedWallClockToUtcMs } from "../../utils/zoned-date-time";
12
+
13
+ export type ShotIntradayRangePreset = "1D" | "1W";
14
+
15
+ export interface ShotIntradayRequest {
16
+ rangePreset: ShotIntradayRangePreset;
17
+ resolution: ManualChartResolution;
18
+ session: string | null;
19
+ }
20
+
21
+ export interface ShotIntradayWindow {
22
+ points: PricePoint[];
23
+ sessionDates: string[];
24
+ start: Date | null;
25
+ end: Date | null;
26
+ }
27
+
28
+ const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
29
+ const HISTORICAL_RETRY_DELAY_MS = 61 * 60_000;
30
+ const DAY_MS = 24 * 60 * 60_000;
31
+
32
+ function sessionDateFormatter(timeZone: string): Intl.DateTimeFormat {
33
+ return new Intl.DateTimeFormat("en-US", {
34
+ timeZone,
35
+ year: "numeric",
36
+ month: "2-digit",
37
+ day: "2-digit",
38
+ });
39
+ }
40
+
41
+ function sessionDate(timestamp: number, timeZone: string): string {
42
+ const parts = new Map(
43
+ sessionDateFormatter(timeZone)
44
+ .formatToParts(new Date(timestamp))
45
+ .filter((part) => part.type !== "literal")
46
+ .map((part) => [part.type, part.value]),
47
+ );
48
+ return `${parts.get("year")}-${parts.get("month")}-${parts.get("day")}`;
49
+ }
50
+
51
+ export function parseShotSessionDate(value: string): {
52
+ year: number;
53
+ month: number;
54
+ day: number;
55
+ } {
56
+ const match = DATE_ONLY_PATTERN.exec(value.trim());
57
+ if (!match) throw new Error(`Invalid --session value "${value}". Use YYYY-MM-DD.`);
58
+ const year = Number(match[1]);
59
+ const month = Number(match[2]);
60
+ const day = Number(match[3]);
61
+ const candidate = new Date(Date.UTC(year, month - 1, day));
62
+ if (
63
+ candidate.getUTCFullYear() !== year
64
+ || candidate.getUTCMonth() !== month - 1
65
+ || candidate.getUTCDate() !== day
66
+ ) {
67
+ throw new Error(`Invalid --session value "${value}". Use a real calendar date.`);
68
+ }
69
+ return { year, month, day };
70
+ }
71
+
72
+ export function shotSessionUtcBounds(value: string, timeZone: string): {
73
+ start: Date;
74
+ end: Date;
75
+ } {
76
+ const parsed = parseShotSessionDate(value);
77
+ const next = new Date(Date.UTC(parsed.year, parsed.month - 1, parsed.day + 1));
78
+ return {
79
+ start: new Date(zonedWallClockToUtcMs(
80
+ timeZone,
81
+ parsed.year,
82
+ parsed.month,
83
+ parsed.day,
84
+ 0,
85
+ 0,
86
+ 0,
87
+ )),
88
+ end: new Date(zonedWallClockToUtcMs(
89
+ timeZone,
90
+ next.getUTCFullYear(),
91
+ next.getUTCMonth() + 1,
92
+ next.getUTCDate(),
93
+ 0,
94
+ 0,
95
+ 0,
96
+ )),
97
+ };
98
+ }
99
+
100
+ function normalizedPoints(points: readonly PricePoint[]): PricePoint[] {
101
+ const byTimestamp = new Map<number, PricePoint>();
102
+ for (const point of points) {
103
+ const timestamp = getPricePointTimestamp(point);
104
+ if (!Number.isFinite(timestamp) || !Number.isFinite(point.close) || point.close <= 0) continue;
105
+ byTimestamp.set(timestamp, {
106
+ ...point,
107
+ date: point.date instanceof Date ? point.date : new Date(timestamp),
108
+ });
109
+ }
110
+ return [...byTimestamp.values()].sort(
111
+ (left, right) => getPricePointTimestamp(left) - getPricePointTimestamp(right),
112
+ );
113
+ }
114
+
115
+ export function resolveShotIntradaySessionWindow(
116
+ points: readonly PricePoint[],
117
+ options: {
118
+ rangePreset: ShotIntradayRangePreset;
119
+ session?: string | null;
120
+ timeZone?: string | null;
121
+ },
122
+ ): ShotIntradayWindow {
123
+ const timeZone = options.timeZone || "UTC";
124
+ const normalized = normalizedPoints(points);
125
+ const bySession = new Map<string, PricePoint[]>();
126
+ for (const point of normalized) {
127
+ const date = sessionDate(getPricePointTimestamp(point), timeZone);
128
+ const sessionPoints = bySession.get(date) ?? [];
129
+ sessionPoints.push(point);
130
+ bySession.set(date, sessionPoints);
131
+ }
132
+
133
+ const requestedSession = options.session?.trim() || null;
134
+ if (requestedSession) parseShotSessionDate(requestedSession);
135
+ const availableSessions = [...bySession.keys()].sort();
136
+ const selectedSessions = requestedSession
137
+ ? availableSessions.filter((date) => date === requestedSession)
138
+ : options.rangePreset === "1D"
139
+ ? availableSessions.slice(-1)
140
+ : availableSessions.slice(-5);
141
+ const selected = selectedSessions.flatMap((date) => bySession.get(date) ?? []);
142
+ const startTime = selected.length > 0 ? getPricePointTimestamp(selected[0]!) : Number.NaN;
143
+ const endTime = selected.length > 0 ? getPricePointTimestamp(selected.at(-1)!) : Number.NaN;
144
+ return {
145
+ points: selected,
146
+ sessionDates: selectedSessions,
147
+ start: Number.isFinite(startTime) ? new Date(startTime) : null,
148
+ end: Number.isFinite(endTime) ? new Date(endTime) : null,
149
+ };
150
+ }
151
+
152
+ export function hasIntradayBars(window: ShotIntradayWindow, timeZone = "UTC"): boolean {
153
+ if (window.points.length < 2) return false;
154
+ const pointsBySession = new Map<string, number[]>();
155
+ for (const point of window.points) {
156
+ const timestamp = getPricePointTimestamp(point);
157
+ const date = sessionDate(timestamp, timeZone);
158
+ const timestamps = pointsBySession.get(date) ?? [];
159
+ timestamps.push(timestamp);
160
+ pointsBySession.set(date, timestamps);
161
+ }
162
+ return [...pointsBySession.values()].some((timestamps) => (
163
+ timestamps.length >= 2
164
+ && timestamps.some((timestamp, index) => index > 0 && timestamp - timestamps[index - 1]! < DAY_MS)
165
+ ));
166
+ }
167
+
168
+ export function resolveShotIntradayRequest(options: {
169
+ rangePreset?: unknown;
170
+ chartResolution?: unknown;
171
+ session?: unknown;
172
+ }): ShotIntradayRequest {
173
+ const session = typeof options.session === "string" && options.session.trim()
174
+ ? options.session.trim()
175
+ : null;
176
+ if (session) parseShotSessionDate(session);
177
+ const rangePreset = session
178
+ ? "1D"
179
+ : options.rangePreset === "1W" ? "1W" : "1D";
180
+ const requestedResolution = typeof options.chartResolution === "string"
181
+ ? options.chartResolution
182
+ : "auto";
183
+ const resolution = requestedResolution === "auto"
184
+ ? getPresetResolution(rangePreset)
185
+ : requestedResolution as ManualChartResolution;
186
+ if (!isIntradayResolution(resolution)) {
187
+ throw new Error(`GIP requires an intraday chart resolution, got "${requestedResolution}".`);
188
+ }
189
+ return { rangePreset, resolution, session };
190
+ }
191
+
192
+ function unavailableReason(symbol: string, request: ShotIntradayRequest, kind: "empty" | "not-intraday"): string {
193
+ const window = request.session
194
+ ? `session ${request.session}`
195
+ : request.rangePreset === "1W" ? "the latest five sessions" : "the latest session";
196
+ return kind === "not-intraday"
197
+ ? `The market-data provider did not return intraday bars for ${symbol} for ${window}.`
198
+ : `No intraday price history is available for ${symbol} for ${window}.`;
199
+ }
200
+
201
+ async function loadTrailingHistory(
202
+ provider: DataProvider,
203
+ symbol: string,
204
+ exchange: string,
205
+ request: ShotIntradayRequest,
206
+ ): Promise<PricePoint[]> {
207
+ if (!provider.getPriceHistoryForResolution) return [];
208
+ const fetchRange: TimeRange = request.rangePreset === "1W" && request.resolution !== "1m"
209
+ ? "1M"
210
+ : "1W";
211
+ try {
212
+ return await provider.getPriceHistoryForResolution(
213
+ symbol,
214
+ exchange,
215
+ fetchRange,
216
+ request.resolution,
217
+ );
218
+ } catch {
219
+ return [];
220
+ }
221
+ }
222
+
223
+ async function loadHistoricalFallback(
224
+ provider: DataProvider,
225
+ symbol: string,
226
+ exchange: string,
227
+ request: ShotIntradayRequest,
228
+ now: Date,
229
+ ): Promise<PricePoint[]> {
230
+ if (!provider.getDetailedPriceHistory) return [];
231
+ if (request.session) {
232
+ const timeZone = resolveExchangeTimeZone(exchange) ?? "UTC";
233
+ const bounds = shotSessionUtcBounds(request.session, timeZone);
234
+ return provider.getDetailedPriceHistory(
235
+ symbol,
236
+ exchange,
237
+ bounds.start,
238
+ bounds.end,
239
+ request.resolution,
240
+ ).catch(() => []);
241
+ }
242
+ const end = new Date(now.getTime() - HISTORICAL_RETRY_DELAY_MS);
243
+ const lookbackDays = request.rangePreset === "1W" ? 35 : 8;
244
+ const start = new Date(end.getTime() - lookbackDays * DAY_MS);
245
+ return provider.getDetailedPriceHistory(
246
+ symbol,
247
+ exchange,
248
+ start,
249
+ end,
250
+ request.resolution,
251
+ ).catch(() => []);
252
+ }
253
+
254
+ export async function loadShotIntradayWindow(options: {
255
+ provider: DataProvider;
256
+ symbol: string;
257
+ exchange: string;
258
+ request: ShotIntradayRequest;
259
+ now?: Date;
260
+ }): Promise<ShotIntradayWindow & { unavailableReason: string | null }> {
261
+ const timeZone = resolveExchangeTimeZone(options.exchange) ?? "UTC";
262
+ let raw = options.request.session
263
+ ? await loadHistoricalFallback(
264
+ options.provider,
265
+ options.symbol,
266
+ options.exchange,
267
+ options.request,
268
+ options.now ?? new Date(),
269
+ )
270
+ : await loadTrailingHistory(
271
+ options.provider,
272
+ options.symbol,
273
+ options.exchange,
274
+ options.request,
275
+ );
276
+ let window = resolveShotIntradaySessionWindow(raw, {
277
+ rangePreset: options.request.rangePreset,
278
+ session: options.request.session,
279
+ timeZone,
280
+ });
281
+ let sawNonIntradayData = window.points.length > 0 && !hasIntradayBars(window, timeZone);
282
+
283
+ if (!hasIntradayBars(window, timeZone)) {
284
+ raw = options.request.session
285
+ ? await loadTrailingHistory(
286
+ options.provider,
287
+ options.symbol,
288
+ options.exchange,
289
+ options.request,
290
+ )
291
+ : await loadHistoricalFallback(
292
+ options.provider,
293
+ options.symbol,
294
+ options.exchange,
295
+ options.request,
296
+ options.now ?? new Date(),
297
+ );
298
+ window = resolveShotIntradaySessionWindow(raw, {
299
+ rangePreset: options.request.rangePreset,
300
+ session: options.request.session,
301
+ timeZone,
302
+ });
303
+ sawNonIntradayData ||= window.points.length > 0 && !hasIntradayBars(window, timeZone);
304
+ }
305
+
306
+ if (window.points.length === 0) {
307
+ const requestedWindow = options.request.session
308
+ ? shotSessionUtcBounds(options.request.session, timeZone)
309
+ : null;
310
+ return {
311
+ ...window,
312
+ start: requestedWindow?.start ?? window.start,
313
+ end: requestedWindow?.end ?? window.end,
314
+ unavailableReason: unavailableReason(
315
+ options.symbol,
316
+ options.request,
317
+ sawNonIntradayData ? "not-intraday" : "empty",
318
+ ),
319
+ };
320
+ }
321
+ if (!hasIntradayBars(window, timeZone)) {
322
+ const requestedWindow = options.request.session
323
+ ? shotSessionUtcBounds(options.request.session, timeZone)
324
+ : null;
325
+ return {
326
+ points: [],
327
+ sessionDates: window.sessionDates,
328
+ start: requestedWindow?.start ?? null,
329
+ end: requestedWindow?.end ?? null,
330
+ unavailableReason: unavailableReason(options.symbol, options.request, "not-intraday"),
331
+ };
332
+ }
333
+ return { ...window, unavailableReason: null };
334
+ }
@@ -3,7 +3,10 @@ import { createDefaultConfig } from "../../types/config";
3
3
  import { CHART_SPEC_VERSION, type ChartSpec } from "../../time-series/types";
4
4
  import { createTestDataProvider } from "../../test-support/data-provider";
5
5
  import { setSharedRegistryForTests } from "../../plugins/registry";
6
- import { buildFunctionReport } from "./report";
6
+ import {
7
+ buildFunctionReport,
8
+ resolvePaneFunctionReportSource,
9
+ } from "./report";
7
10
  import type { ResolvedPaneFunction } from "./resolver";
8
11
  import type { MarketContext } from "../types";
9
12
 
@@ -25,6 +28,31 @@ const spec: ChartSpec = {
25
28
 
26
29
  afterEach(() => setSharedRegistryForTests(undefined));
27
30
 
31
+ test("selects headless before legacy reports and rendered DOM fallback", () => {
32
+ const base = {
33
+ pane: { id: "market-data", name: "Market Data" },
34
+ template: undefined,
35
+ instance: {},
36
+ capability: { id: "market-data", reportReadiness: "live-dom" },
37
+ } as unknown as ResolvedPaneFunction;
38
+
39
+ expect(resolvePaneFunctionReportSource(base)).toBe("dom");
40
+ expect(resolvePaneFunctionReportSource({
41
+ ...base,
42
+ capability: { ...base.capability, id: "quote-comparison" },
43
+ })).toBe("report");
44
+ expect(resolvePaneFunctionReportSource({
45
+ ...base,
46
+ capability: { ...base.capability, id: "quote-comparison" },
47
+ headless: {
48
+ shape: "rows",
49
+ argument: { kind: "none" },
50
+ options: [],
51
+ load: () => ({ rows: [] }),
52
+ },
53
+ })).toBe("headless");
54
+ });
55
+
28
56
  test("chart composer reports accept capability-backed series", async () => {
29
57
  const date = new Date(Date.now() - 24 * 60 * 60 * 1_000);
30
58
  setSharedRegistryForTests({
@@ -45,9 +45,19 @@ import {
45
45
  } from "./data";
46
46
  import type { ResolvedPaneFunction } from "./resolver";
47
47
  import { buildHeadlessFunctionReport } from "./headless";
48
+ import {
49
+ appendDomReportFooter,
50
+ buildDomFunctionReport,
51
+ } from "./dom";
52
+
53
+ export type PaneFunctionReportSource = "headless" | "report" | "dom";
48
54
 
49
55
  export interface PaneFunctionReportData {
50
56
  kind: string;
57
+ /** Added by buildFunctionReport after the selected loader completes. */
58
+ source?: PaneFunctionReportSource;
59
+ /** Added by buildFunctionReport for every successful report. */
60
+ elapsedMs?: number;
51
61
  target: string;
52
62
  capabilityId: string;
53
63
  symbols: string[];
@@ -746,14 +756,40 @@ async function buildRelationshipReport(
746
756
  };
747
757
  }
748
758
 
749
- export async function buildFunctionReport(
759
+ const LEGACY_REPORT_CAPABILITY_IDS = new Set([
760
+ "chart-composer",
761
+ "financial-statements",
762
+ "fundamental-series",
763
+ "historical-prices",
764
+ "intraday-price-chart",
765
+ "price-chart",
766
+ "price-comparison",
767
+ "quote-comparison",
768
+ "relative-valuation",
769
+ "return-correlation",
770
+ "security-relationship",
771
+ "valuation-series",
772
+ ]);
773
+
774
+ export function resolvePaneFunctionReportSource(
775
+ resolved: Pick<ResolvedPaneFunction, "headless" | "capability" | "pane" | "template" | "instance">,
776
+ ): PaneFunctionReportSource | null {
777
+ if (resolved.headless) return "headless";
778
+ if (
779
+ isFinancialAnalysisFunction(resolved as ResolvedPaneFunction)
780
+ || LEGACY_REPORT_CAPABILITY_IDS.has(resolved.capability.id)
781
+ ) {
782
+ return "report";
783
+ }
784
+ if (resolved.capability.reportReadiness === "live-dom") return "dom";
785
+ return null;
786
+ }
787
+
788
+ async function buildLegacyFunctionReport(
750
789
  resolved: ResolvedPaneFunction,
751
790
  context: MarketContext,
752
791
  rawArg: string,
753
792
  ): Promise<PaneFunctionReport> {
754
- if (resolved.headless) {
755
- return buildHeadlessFunctionReport(resolved, context, rawArg);
756
- }
757
793
  if (isFinancialAnalysisFunction(resolved)) {
758
794
  return buildFinancialStatementReport(resolved, context, rawArg);
759
795
  }
@@ -785,12 +821,37 @@ export async function buildFunctionReport(
785
821
  return buildCorrelationReport(resolved, context);
786
822
  case "security-relationship":
787
823
  return buildRelationshipReport(resolved, context);
824
+ default:
825
+ throw new Error(`No structured report builder is registered for ${resolved.token}.`);
788
826
  }
827
+ }
789
828
 
790
- if (resolved.capability.reportReadiness === "unsupported") {
791
- return buildLegacyPaneReport(resolved, context, rawArg);
829
+ export async function buildFunctionReport(
830
+ resolved: ResolvedPaneFunction,
831
+ context: MarketContext,
832
+ rawArg: string,
833
+ ): Promise<PaneFunctionReport> {
834
+ const startedAt = performance.now();
835
+ const source = resolvePaneFunctionReportSource(resolved);
836
+ if (!source) {
837
+ throw new Error(`${resolved.token} is an interactive pane and does not expose a data report.`);
838
+ }
839
+
840
+ const report = source === "headless"
841
+ ? await buildHeadlessFunctionReport(resolved, context, rawArg)
842
+ : source === "report"
843
+ ? await buildLegacyFunctionReport(resolved, context, rawArg)
844
+ : await buildDomFunctionReport(resolved, context, rawArg);
845
+ const elapsedMs = Math.max(0, Math.round(performance.now() - startedAt));
846
+ report.data = { ...report.data, source, elapsedMs };
847
+ if (source === "dom") {
848
+ report.text = appendDomReportFooter(
849
+ report.text,
850
+ elapsedMs,
851
+ report.data.truncated === true,
852
+ );
792
853
  }
793
- throw new Error(`No structured report builder is registered for ${resolved.token}.`);
854
+ return report;
794
855
  }
795
856
 
796
857
  async function buildLegacyPaneReport(