gloomberb 0.11.0 → 0.11.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 (392) hide show
  1. package/README.md +38 -15
  2. package/README.zh-CN.md +12 -12
  3. package/package.json +10 -3
  4. package/src/api-client/auth.ts +14 -10
  5. package/src/api-client/data.ts +54 -0
  6. package/src/api-client/index.test.ts +124 -0
  7. package/src/api-client/index.ts +139 -4
  8. package/src/api-client/paths.ts +89 -0
  9. package/src/api-client/request.ts +15 -2
  10. package/src/api-client/socket-health.test.ts +1 -0
  11. package/src/api-client/socket.ts +2 -1
  12. package/src/api-client/types.ts +163 -0
  13. package/src/app/pane-runtime/index.tsx +4 -1
  14. package/src/app/pane-runtime/plugin-bindings.test.ts +117 -0
  15. package/src/app/pane-runtime/plugin-bindings.ts +25 -4
  16. package/src/app/pane-runtime/ticker-inspector-runtime.ts +25 -14
  17. package/src/app/runtime/desktop-deeplink.test.ts +16 -0
  18. package/src/app/runtime/desktop-deeplink.ts +66 -2
  19. package/src/app/runtime/desktop-menu.ts +3 -0
  20. package/src/app/runtime/plugin-bindings.ts +30 -28
  21. package/src/app/runtime/update.ts +8 -6
  22. package/src/app.tsx +21 -7
  23. package/src/architecture/import-boundaries.test.ts +5 -1
  24. package/src/capabilities/factories.ts +4 -0
  25. package/src/cli/commands/automation.test.ts +9 -0
  26. package/src/cli/commands/automation.ts +22 -73
  27. package/src/cli/index.ts +10 -6
  28. package/src/cli/registry.ts +1 -1
  29. package/src/components/chart/composite/composite-chart.test.tsx +45 -2
  30. package/src/components/chart/composite/composite-chart.tsx +16 -12
  31. package/src/components/chart/composite/format.test.ts +34 -0
  32. package/src/components/chart/composite/format.ts +33 -10
  33. package/src/components/chart/composite/scene.ts +0 -21
  34. package/src/components/chart/composite/tools.ts +49 -0
  35. package/src/components/chart/core/axis-format.ts +13 -3
  36. package/src/components/chart/core/index.test.ts +7 -0
  37. package/src/components/chart/core/renderer.ts +3 -4
  38. package/src/components/command-bar/commands/direct/index.ts +4 -0
  39. package/src/components/command-bar/commands/registry.ts +8 -1
  40. package/src/components/command-bar/layout-items/current-layout.ts +1 -1
  41. package/src/components/command-bar/pane-templates/workflow.ts +4 -0
  42. package/src/components/command-bar/routes/root/shortcut-feedback.ts +3 -0
  43. package/src/components/command-bar/routes/root/shortcuts.test.ts +19 -0
  44. package/src/components/command-bar/routes/root/shortcuts.ts +3 -1
  45. package/src/components/command-bar/surface/index.test.tsx +6 -6
  46. package/src/components/command-bar/surface/index.tsx +4 -1
  47. package/src/components/command-bar/surface/pane.test.tsx +66 -2
  48. package/src/components/command-bar/view-model.test.ts +2 -1
  49. package/src/components/command-bar/workflow/ops.test.ts +54 -0
  50. package/src/components/command-bar/workflow/ops.ts +1 -1
  51. package/src/components/company-logo.test.ts +19 -0
  52. package/src/components/company-logo.tsx +60 -0
  53. package/src/components/feed-data-table/stack-view.tsx +7 -1
  54. package/src/components/index.ts +1 -0
  55. package/src/components/layout/detached-pane-shell.tsx +85 -15
  56. package/src/components/layout/header.tsx +5 -5
  57. package/src/components/layout/pane/title.ts +31 -3
  58. package/src/components/layout/shell/active-drag.ts +4 -11
  59. package/src/components/layout/shell/drag/index.ts +4 -20
  60. package/src/components/layout/shell/drag/runtime.ts +3 -4
  61. package/src/components/layout/shell/index.test.tsx +17 -3
  62. package/src/components/layout/shell/index.tsx +89 -31
  63. package/src/components/layout/shell/menu.test.ts +37 -1
  64. package/src/components/layout/shell/menu.ts +14 -0
  65. package/src/components/layout/shell/native/pointer-runtime.ts +22 -8
  66. package/src/components/layout/shell/pane/actions.ts +0 -5
  67. package/src/components/layout/shell/pane/management-shortcuts.ts +9 -4
  68. package/src/components/layout/shell/shortcuts.ts +6 -3
  69. package/src/components/layout/shell/ticker-link-menu.test.ts +82 -0
  70. package/src/components/layout/shell/ticker-link-menu.ts +78 -0
  71. package/src/components/layout/status-bar.test.tsx +62 -61
  72. package/src/components/layout/status-bar.tsx +53 -75
  73. package/src/components/onboarding/account-step/form-panel.tsx +4 -2
  74. package/src/components/onboarding/account-step/index.tsx +1 -1
  75. package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
  76. package/src/components/onboarding/onboarding-wizard.test.tsx +44 -1
  77. package/src/components/onboarding/onboarding-wizard.tsx +13 -5
  78. package/src/components/onboarding/wizard-account.ts +4 -23
  79. package/src/components/table-view-shared.test.ts +11 -0
  80. package/src/components/table-view-shared.tsx +26 -0
  81. package/src/components/ui/data-table/index.tsx +19 -1
  82. package/src/components/ui/fields.tsx +5 -1
  83. package/src/core/state/app/layout-reducer.ts +36 -0
  84. package/src/core/state/app/layout.ts +10 -5
  85. package/src/core/state/app/state.ts +0 -13
  86. package/src/core/state/app/types.ts +6 -4
  87. package/src/data/config/chart-settings.ts +1 -1
  88. package/src/data/config/layout.ts +2 -0
  89. package/src/data/config/store/index.test.ts +27 -6
  90. package/src/data/config/store/normalize.ts +3 -5
  91. package/src/data/plugin-state-store.ts +11 -0
  92. package/src/data/sqlite/cache.test.ts +10 -0
  93. package/src/i18n/es.ts +13 -5
  94. package/src/i18n/ja.ts +13 -5
  95. package/src/i18n/ko.ts +13 -5
  96. package/src/i18n/zh-cn.ts +13 -5
  97. package/src/i18n/zh-tw.ts +13 -5
  98. package/src/layout-marketplace/api.test.ts +54 -0
  99. package/src/layout-marketplace/api.ts +65 -0
  100. package/src/layout-marketplace/gallery-desktop.test.tsx +262 -0
  101. package/src/layout-marketplace/gallery-desktop.tsx +375 -0
  102. package/src/layout-marketplace/gallery-terminal.tsx +376 -0
  103. package/src/layout-marketplace/gallery.test.tsx +84 -0
  104. package/src/layout-marketplace/gallery.tsx +323 -0
  105. package/src/layout-marketplace/geometry.test.ts +47 -0
  106. package/src/layout-marketplace/geometry.ts +80 -0
  107. package/src/layout-marketplace/mini-workspace.test.tsx +122 -0
  108. package/src/layout-marketplace/mini-workspace.tsx +428 -0
  109. package/src/layout-marketplace/model.test.ts +118 -0
  110. package/src/layout-marketplace/model.ts +171 -0
  111. package/src/layout-marketplace/name-dialog.tsx +60 -0
  112. package/src/layout-marketplace/pane-imagery.ts +85 -0
  113. package/src/layout-marketplace/pane.tsx +20 -0
  114. package/src/layout-marketplace/payload.test.ts +246 -0
  115. package/src/layout-marketplace/payload.ts +624 -0
  116. package/src/layout-marketplace/use-marketplace.ts +63 -0
  117. package/src/market-data/coordinator/index.test.ts +2 -2
  118. package/src/market-data/coordinator/index.ts +1 -1
  119. package/src/market-data/hooks.tsx +0 -41
  120. package/src/market-data/market/format.test.ts +7 -0
  121. package/src/market-data/market/freshness.ts +5 -2
  122. package/src/market-data/quotes/freshness.test.ts +16 -0
  123. package/src/news/aggregator.test.ts +92 -3
  124. package/src/news/aggregator.ts +98 -18
  125. package/src/news/hooks.test.tsx +2 -0
  126. package/src/news/hooks.ts +27 -1
  127. package/src/news/news-model.ts +5 -2
  128. package/src/news/types.ts +7 -0
  129. package/src/plugins/broker-sync/broker-sync.test.ts +58 -0
  130. package/src/plugins/broker-sync/normalize.ts +42 -5
  131. package/src/plugins/builtin/account-management/pane.tsx +5 -5
  132. package/src/plugins/builtin/ai/index.tsx +18 -2
  133. package/src/plugins/builtin/ai/screener/pane.test.tsx +47 -4
  134. package/src/plugins/builtin/ai/screener/pane.tsx +35 -12
  135. package/src/plugins/builtin/alerts/alert-engine.test.ts +53 -1
  136. package/src/plugins/builtin/alerts/alert-engine.ts +34 -1
  137. package/src/plugins/builtin/alerts/index.test.tsx +49 -6
  138. package/src/plugins/builtin/alerts/index.tsx +11 -6
  139. package/src/plugins/builtin/alerts/pane.tsx +51 -9
  140. package/src/plugins/builtin/alerts/quotes.ts +11 -3
  141. package/src/plugins/builtin/alerts/types.ts +1 -0
  142. package/src/plugins/builtin/analytics/index.tsx +11 -1
  143. package/src/plugins/builtin/analytics/pane-model.ts +6 -4
  144. package/src/plugins/builtin/broker-manager/index.tsx +3 -0
  145. package/src/plugins/builtin/buildout/model/index.ts +1 -1
  146. package/src/plugins/builtin/buildout/pane/index.tsx +17 -18
  147. package/src/plugins/builtin/cds/client.test.ts +83 -0
  148. package/src/plugins/builtin/cds/client.ts +67 -0
  149. package/src/plugins/builtin/cds/index.tsx +41 -0
  150. package/src/plugins/builtin/cds/model.test.ts +232 -0
  151. package/src/plugins/builtin/cds/model.ts +401 -0
  152. package/src/plugins/builtin/cds/pane.test.tsx +132 -0
  153. package/src/plugins/builtin/cds/pane.tsx +358 -0
  154. package/src/plugins/builtin/chart-composer/chart-spec.ts +31 -0
  155. package/src/plugins/builtin/chart-composer/index.tsx +88 -2
  156. package/src/plugins/builtin/chart-composer/pane-share.test.ts +71 -0
  157. package/src/plugins/builtin/chart-composer/pane.tsx +103 -15
  158. package/src/plugins/builtin/chat/content/index.tsx +4 -1
  159. package/src/plugins/builtin/chat/controller/channels.ts +7 -1
  160. package/src/plugins/builtin/chat/controller/index.ts +14 -9
  161. package/src/plugins/builtin/chat/controller/notifications.ts +13 -3
  162. package/src/plugins/builtin/chat/controller/persistence.ts +0 -3
  163. package/src/plugins/builtin/chat/controller/realtime.ts +3 -3
  164. package/src/plugins/builtin/chat/controller/send.ts +1 -3
  165. package/src/plugins/builtin/chat/controller/session-runtime.ts +18 -11
  166. package/src/plugins/builtin/chat/controller/state.ts +1 -3
  167. package/src/plugins/builtin/chat/controller/storage.ts +0 -2
  168. package/src/plugins/builtin/chat/controller/utils.ts +4 -6
  169. package/src/plugins/builtin/chat/controller/view.ts +2 -2
  170. package/src/plugins/builtin/chat/controller.test.ts +142 -14
  171. package/src/plugins/builtin/chat/message/desktop.test.tsx +88 -0
  172. package/src/plugins/builtin/chat/message/desktop.tsx +3 -1
  173. package/src/plugins/builtin/chat/message/profile-popover.test.tsx +1 -29
  174. package/src/plugins/builtin/chat/profile-popover.ts +53 -22
  175. package/src/plugins/builtin/chat/sidebar.test.tsx +81 -0
  176. package/src/plugins/builtin/chat/sidebar.tsx +63 -0
  177. package/src/plugins/builtin/chat/test-harness.tsx +29 -1
  178. package/src/plugins/builtin/cloud/auth-actions.tsx +14 -7
  179. package/src/plugins/builtin/cloud/auth-commands.ts +21 -51
  180. package/src/plugins/builtin/cloud/auth-dialog.tsx +296 -0
  181. package/src/{components/onboarding/account-step/model.test.ts → plugins/builtin/cloud/auth-model.test.ts} +1 -1
  182. package/src/{components/onboarding/account-step/model.ts → plugins/builtin/cloud/auth-model.ts} +37 -3
  183. package/src/plugins/builtin/cloud/browser.tsx +9 -0
  184. package/src/plugins/builtin/cloud/buildout-module.tsx +23 -0
  185. package/src/plugins/builtin/cloud/device-signin.ts +2 -8
  186. package/src/plugins/builtin/cloud/index.tsx +2 -0
  187. package/src/plugins/builtin/cloud/plugin.tsx +14 -24
  188. package/src/plugins/builtin/cloud/upgrade-command.ts +13 -6
  189. package/src/plugins/builtin/cloud-tweets/pane-share.test.ts +56 -0
  190. package/src/plugins/builtin/cloud-tweets/pane.tsx +20 -5
  191. package/src/plugins/builtin/cloud-tweets/registration.ts +50 -6
  192. package/src/plugins/builtin/composite-plugins.ts +3 -1
  193. package/src/plugins/builtin/congress-trades/detail.tsx +43 -1
  194. package/src/plugins/builtin/congress-trades/model.test.ts +85 -0
  195. package/src/plugins/builtin/congress-trades/model.ts +69 -1
  196. package/src/plugins/builtin/congress-trades/pane.tsx +51 -2
  197. package/src/plugins/builtin/connections/index.ts +3 -0
  198. package/src/plugins/builtin/dividend-yield/client.ts +18 -0
  199. package/src/plugins/builtin/dividend-yield/pane.tsx +7 -6
  200. package/src/plugins/builtin/earnings/index.tsx +5 -3
  201. package/src/plugins/builtin/earnings/model.test.ts +19 -1
  202. package/src/plugins/builtin/earnings/model.ts +17 -15
  203. package/src/plugins/builtin/econ/calendar-model.ts +0 -5
  204. package/src/plugins/builtin/econ/index.test.tsx +6 -1
  205. package/src/plugins/builtin/fear-greed/charts.tsx +12 -8
  206. package/src/plugins/builtin/help/index.tsx +2 -2
  207. package/src/plugins/builtin/holders/thirteenf-match.ts +1 -2
  208. package/src/plugins/builtin/insider/index.tsx +27 -11
  209. package/src/plugins/builtin/kelly-sizer/index.tsx +4 -1
  210. package/src/plugins/builtin/layout-manager/index.tsx +17 -4
  211. package/src/plugins/builtin/news/index.tsx +11 -6
  212. package/src/plugins/builtin/news/wire/breaking/notifications.test.ts +2 -0
  213. package/src/plugins/builtin/news/wire/breaking/pane.test.tsx +2 -0
  214. package/src/plugins/builtin/news/wire/breaking/pane.tsx +4 -1
  215. package/src/plugins/builtin/news/wire/feed-config.test.ts +15 -1
  216. package/src/plugins/builtin/news/wire/feed-config.ts +25 -5
  217. package/src/plugins/builtin/news/wire/index.tsx +3 -3
  218. package/src/plugins/builtin/news/wire/industry-pane.tsx +7 -2
  219. package/src/plugins/builtin/news/wire/news/footer.ts +34 -1
  220. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +4 -1
  221. package/src/plugins/builtin/news/wire/news/table.tsx +8 -2
  222. package/src/plugins/builtin/news/wire/rss/source.test.ts +12 -0
  223. package/src/plugins/builtin/news/wire/rss/source.ts +1 -1
  224. package/src/plugins/builtin/notes/files.ts +80 -0
  225. package/src/plugins/builtin/notes/index.tsx +3 -0
  226. package/src/plugins/builtin/notes/sync.test.ts +49 -0
  227. package/src/plugins/builtin/notes/sync.ts +98 -0
  228. package/src/plugins/builtin/options/index.tsx +1 -0
  229. package/src/plugins/builtin/options/view.test.tsx +25 -3
  230. package/src/plugins/builtin/options/view.tsx +1 -0
  231. package/src/plugins/builtin/portfolio-list/account-metrics.test.ts +22 -0
  232. package/src/plugins/builtin/portfolio-list/account-metrics.ts +12 -7
  233. package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -3
  234. package/src/plugins/builtin/portfolio-list/column-values.ts +15 -4
  235. package/src/plugins/builtin/portfolio-list/header.tsx +13 -3
  236. package/src/plugins/builtin/portfolio-list/index.tsx +8 -0
  237. package/src/plugins/builtin/portfolio-list/metrics.test.ts +31 -1
  238. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +1 -0
  239. package/src/plugins/builtin/portfolio-list/pane/index.tsx +10 -7
  240. package/src/plugins/builtin/portfolio-list/pane/streaming.ts +6 -1
  241. package/src/plugins/builtin/portfolio-list/position-metrics.ts +25 -6
  242. package/src/plugins/builtin/portfolio-list/summary/index.tsx +35 -21
  243. package/src/plugins/builtin/portfolio-list/summary/totals.ts +20 -6
  244. package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -12
  245. package/src/plugins/builtin/research/equity-diagnostic-pane.test.tsx +329 -0
  246. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +582 -0
  247. package/src/plugins/builtin/research/index.test.ts +14 -0
  248. package/src/plugins/builtin/research/index.tsx +29 -0
  249. package/src/plugins/builtin/sec/index.tsx +24 -9
  250. package/src/plugins/builtin/sectors/index.tsx +14 -0
  251. package/src/plugins/builtin/shared/cloud-upgrade.ts +18 -11
  252. package/src/plugins/builtin/shared/public-pane.test.ts +12 -0
  253. package/src/plugins/builtin/shared/public-pane.ts +9 -0
  254. package/src/plugins/builtin/shared/public-share.ts +23 -0
  255. package/src/plugins/builtin/shared/ticker-source.ts +32 -0
  256. package/src/plugins/builtin/shared/ticker-surface.test.ts +27 -0
  257. package/src/plugins/builtin/shared/ticker-surface.ts +17 -0
  258. package/src/plugins/builtin/substack/api/cache.ts +7 -3
  259. package/src/plugins/builtin/substack/api/loaders.ts +36 -12
  260. package/src/plugins/builtin/substack/api/types.ts +2 -0
  261. package/src/plugins/builtin/substack/pane-state.ts +1 -1
  262. package/src/plugins/builtin/substack/pane.tsx +55 -12
  263. package/src/plugins/builtin/thirteenf/api.test.ts +1 -1
  264. package/src/plugins/builtin/thirteenf/api.ts +40 -18
  265. package/src/plugins/builtin/thirteenf/data.ts +15 -7
  266. package/src/plugins/builtin/thirteenf/pane.tsx +45 -10
  267. package/src/plugins/builtin/ticker-detail/index.tsx +10 -0
  268. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +12 -4
  269. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -2
  270. package/src/plugins/builtin/ticker-detail/settings.ts +12 -0
  271. package/src/plugins/builtin/treasury-auctions/index.tsx +2 -0
  272. package/src/plugins/builtin/world-indices/index.tsx +2 -0
  273. package/src/plugins/catalog-browser.test.ts +78 -0
  274. package/src/plugins/catalog-browser.ts +136 -0
  275. package/src/plugins/gridlock-notification.ts +2 -1
  276. package/src/plugins/ibkr/flex/index.test.ts +19 -0
  277. package/src/plugins/ibkr/flex/index.ts +1 -1
  278. package/src/plugins/pane-manager/gridlock.ts +87 -3
  279. package/src/plugins/pane-manager.test.ts +30 -0
  280. package/src/plugins/pane-manager.ts +6 -1
  281. package/src/plugins/prediction-markets/chart.tsx +2 -2
  282. package/src/plugins/prediction-markets/controller/catalog.ts +77 -2
  283. package/src/plugins/prediction-markets/controller/data.ts +7 -1
  284. package/src/plugins/prediction-markets/controller/index.ts +9 -2
  285. package/src/plugins/prediction-markets/controller/keyboard.ts +10 -14
  286. package/src/plugins/prediction-markets/detail/overview.tsx +21 -0
  287. package/src/plugins/prediction-markets/detail/pane.tsx +43 -34
  288. package/src/plugins/prediction-markets/detail/trades.tsx +34 -9
  289. package/src/plugins/prediction-markets/detail.test.tsx +29 -2
  290. package/src/plugins/prediction-markets/index.tsx +8 -4
  291. package/src/plugins/prediction-markets/navigation.ts +0 -1
  292. package/src/plugins/prediction-markets/pane.test.tsx +77 -0
  293. package/src/plugins/prediction-markets/pane.tsx +12 -0
  294. package/src/plugins/prediction-markets/plugin.test.tsx +36 -0
  295. package/src/plugins/prediction-markets/services/kalshi/adapter.ts +49 -9
  296. package/src/plugins/prediction-markets/services/polymarket/adapter.ts +31 -0
  297. package/src/plugins/prediction-markets/test-helpers.tsx +9 -0
  298. package/src/plugins/registry/index.ts +2 -0
  299. package/src/plugins/ticker-navigation.test.ts +24 -0
  300. package/src/plugins/ticker-navigation.ts +40 -0
  301. package/src/remote/schema.ts +1 -1
  302. package/src/renderers/browser/app-services.ts +61 -0
  303. package/src/renderers/browser/cloud-transport.test.ts +68 -0
  304. package/src/renderers/browser/cloud-transport.ts +48 -0
  305. package/src/renderers/browser/config-host.ts +58 -0
  306. package/src/renderers/browser/deeplink-bridge.test.ts +41 -0
  307. package/src/renderers/browser/deeplink-bridge.ts +22 -0
  308. package/src/renderers/browser/error-boundary.tsx +23 -0
  309. package/src/renderers/browser/main.tsx +64 -0
  310. package/src/renderers/browser/persistence.ts +74 -0
  311. package/src/renderers/browser/storage.test.ts +66 -0
  312. package/src/renderers/browser/storage.ts +50 -0
  313. package/src/renderers/browser/ticker-repository.ts +59 -0
  314. package/src/renderers/browser/ui-host.tsx +40 -0
  315. package/src/renderers/cloudflare/worker.test.ts +136 -0
  316. package/src/renderers/cloudflare/worker.ts +116 -0
  317. package/src/renderers/electrobun/bun/application-menu/index.ts +3 -2
  318. package/src/renderers/electrobun/bun/desktop/initialization.ts +1 -0
  319. package/src/renderers/electrobun/bun/desktop/plugin-state.ts +7 -1
  320. package/src/renderers/electrobun/view/app-services.ts +14 -3
  321. package/src/renderers/electrobun/view/build-assets.ts +12 -10
  322. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  323. package/src/renderers/electrobun/view/data-table/index.tsx +10 -1
  324. package/src/renderers/electrobun/view/desktop/controls.tsx +2 -0
  325. package/src/renderers/electrobun/view/dom-ui-host.tsx +143 -0
  326. package/src/renderers/electrobun/view/host/box.test.tsx +33 -0
  327. package/src/renderers/electrobun/view/host/box.tsx +11 -6
  328. package/src/renderers/electrobun/view/host/chart-surface.tsx +7 -1
  329. package/src/renderers/electrobun/view/host/input.tsx +11 -7
  330. package/src/renderers/electrobun/view/host/native.ts +0 -5
  331. package/src/renderers/electrobun/view/main.tsx +2 -0
  332. package/src/renderers/electrobun/view/remote/persistence.ts +5 -0
  333. package/src/renderers/electrobun/view/styles.css +107 -7
  334. package/src/renderers/electrobun/view/toast-host.test.tsx +78 -0
  335. package/src/renderers/electrobun/view/toast-host.tsx +69 -43
  336. package/src/renderers/electrobun/view/ui-host.tsx +6 -137
  337. package/src/renderers/opentui/start.tsx +2 -0
  338. package/src/renderers/share/layout-view.test.tsx +58 -0
  339. package/src/renderers/share/layout-view.tsx +133 -0
  340. package/src/renderers/share/main.tsx +102 -0
  341. package/src/renderers/share/styles.css +54 -0
  342. package/src/renderers/share/view.test.tsx +100 -0
  343. package/src/renderers/share/view.tsx +150 -0
  344. package/src/shares/api.test.ts +105 -0
  345. package/src/shares/api.ts +127 -0
  346. package/src/shares/chart-snapshot.test.ts +34 -0
  347. package/src/shares/chart-snapshot.ts +28 -0
  348. package/src/shares/location.test.ts +19 -0
  349. package/src/shares/location.ts +11 -0
  350. package/src/shares/pane.test.ts +110 -0
  351. package/src/shares/pane.ts +54 -0
  352. package/src/shares/payload.ts +177 -0
  353. package/src/sources/gloomberb-cloud/index.test.ts +61 -1
  354. package/src/sources/gloomberb-cloud/index.ts +86 -14
  355. package/src/sources/gloomberb-cloud/normalizers.test.ts +34 -0
  356. package/src/sources/gloomberb-cloud/normalizers.ts +5 -5
  357. package/src/sources/provider-router/history.test.ts +2 -2
  358. package/src/sources/provider-router/news.test.ts +34 -0
  359. package/src/sources/provider-router/news.ts +15 -1
  360. package/src/sources/sec-edgar.test.ts +59 -0
  361. package/src/sources/sec-edgar.ts +63 -23
  362. package/src/sources/yahoo-finance/mappers.ts +3 -0
  363. package/src/sources/yahoo-finance/snapshots.ts +10 -2
  364. package/src/sources/yahoo-finance/symbols.test.ts +10 -0
  365. package/src/sources/yahoo-finance/symbols.ts +13 -8
  366. package/src/sources/yahoo-finance.test.ts +1 -1
  367. package/src/sources/yahoo-finance.ts +6 -2
  368. package/src/state/app/bootstrap.ts +6 -7
  369. package/src/state/app/context/index.test.ts +41 -1
  370. package/src/state/app/context/index.test.tsx +34 -16
  371. package/src/state/selectors-ui.ts +0 -8
  372. package/src/sync/baseline.ts +32 -0
  373. package/src/sync/controller.test.ts +124 -1
  374. package/src/sync/controller.ts +54 -12
  375. package/src/sync/core-contributors.test.ts +44 -0
  376. package/src/sync/core-contributors.ts +59 -10
  377. package/src/sync/react.ts +22 -2
  378. package/src/sync/types.ts +12 -0
  379. package/src/theme/font-scale.ts +0 -11
  380. package/src/tickers/search/index.test.ts +27 -0
  381. package/src/tickers/search/upsert.ts +10 -0
  382. package/src/types/capability-route-source.ts +2 -1
  383. package/src/types/config.ts +35 -5
  384. package/src/types/desktop-menu.ts +1 -0
  385. package/src/types/plugin.ts +36 -0
  386. package/src/ui/context-menu.tsx +1 -1
  387. package/src/ui/host.tsx +4 -0
  388. package/src/ui/toast.tsx +2 -0
  389. package/src/utils/back-navigation.ts +0 -4
  390. package/src/utils/price-history.ts +11 -10
  391. package/src/utils/sec.test.ts +6 -0
  392. package/src/utils/sec.ts +1 -1
@@ -8,6 +8,8 @@ import { assetDataProvider, newsProvider, type PluginCapability } from "../../ca
8
8
  import type {
9
9
  AssetDataProvider,
10
10
  CachedFinancialsTarget,
11
+ SecFilingDocument,
12
+ SecFilingItem,
11
13
  MarketDataRequestContext,
12
14
  QuoteBatchResult,
13
15
  QuoteSubscriptionTarget,
@@ -66,6 +68,34 @@ const CLOUD_PROVIDER_MISS_PATTERNS = [
66
68
  /error in the query/i,
67
69
  ];
68
70
 
71
+ function mapCloudSecFiling(item: {
72
+ accessionNumber: string;
73
+ form: string;
74
+ filingDate: string;
75
+ acceptedAt?: string;
76
+ primaryDocument?: string;
77
+ primaryDocDescription?: string;
78
+ items?: string;
79
+ cik: string;
80
+ companyName?: string;
81
+ filingUrl: string;
82
+ primaryDocumentUrl?: string;
83
+ }): SecFilingItem {
84
+ return {
85
+ accessionNumber: item.accessionNumber,
86
+ form: item.form,
87
+ filingDate: new Date(`${item.filingDate}T00:00:00Z`),
88
+ acceptedAt: item.acceptedAt ? new Date(item.acceptedAt) : undefined,
89
+ primaryDocument: item.primaryDocument,
90
+ primaryDocDescription: item.primaryDocDescription,
91
+ items: item.items,
92
+ cik: item.cik,
93
+ companyName: item.companyName,
94
+ filingUrl: item.filingUrl,
95
+ primaryDocumentUrl: item.primaryDocumentUrl,
96
+ };
97
+ }
98
+
69
99
  function isCloudProviderMiss(error: unknown): boolean {
70
100
  const message = error instanceof Error ? error.message : String(error);
71
101
  return CLOUD_PROVIDER_MISS_PATTERNS.some((pattern) => pattern.test(message));
@@ -155,11 +185,10 @@ export class GloomberbCloudProvider implements AssetDataProvider {
155
185
  }
156
186
 
157
187
  async canProvide(): Promise<boolean> {
158
- return !!(await apiClient.ensureVerifiedSession());
188
+ return true;
159
189
  }
160
190
 
161
191
  async getTickerFinancials(ticker: string, exchange = "", _context?: MarketDataRequestContext): Promise<TickerFinancials> {
162
- await requireVerifiedSession();
163
192
  return withCloudFallback(async () => {
164
193
  const response = await apiClient.getCloudFinancials(ticker, exchange);
165
194
  if (isStaleCloudResponse(response)) {
@@ -176,7 +205,6 @@ export class GloomberbCloudProvider implements AssetDataProvider {
176
205
  targets: CachedFinancialsTarget[],
177
206
  options: { forceRefresh?: boolean } = {},
178
207
  ): Promise<TickerFinancialsBatchResult[]> {
179
- await requireVerifiedSession();
180
208
  return withCloudFallback(async () => {
181
209
  const response = await apiClient.getCloudFinancialsBatch(
182
210
  targets.map((target) => ({
@@ -185,6 +213,9 @@ export class GloomberbCloudProvider implements AssetDataProvider {
185
213
  })),
186
214
  options.forceRefresh ? "refresh" : "cache-first",
187
215
  );
216
+ if (isStaleCloudResponse(response)) {
217
+ throw createProviderMiss("Cloud financials are stale");
218
+ }
188
219
  const payload = unwrapRequiredCloudResponse(response, "Cloud financials are unavailable");
189
220
  return payload.items.map((item, index) => {
190
221
  const target = targets[index] ?? {
@@ -207,7 +238,6 @@ export class GloomberbCloudProvider implements AssetDataProvider {
207
238
  }
208
239
 
209
240
  async getQuote(ticker: string, exchange = "", _context?: MarketDataRequestContext): Promise<Quote> {
210
- await requireVerifiedSession();
211
241
  return withCloudFallback(
212
242
  async () => {
213
243
  const response = await apiClient.getCloudQuote(ticker, exchange);
@@ -227,7 +257,6 @@ export class GloomberbCloudProvider implements AssetDataProvider {
227
257
  targets: QuoteSubscriptionTarget[],
228
258
  options: { forceRefresh?: boolean } = {},
229
259
  ): Promise<QuoteBatchResult[]> {
230
- await requireVerifiedSession();
231
260
  return withCloudFallback(async () => {
232
261
  const response = await apiClient.getCloudQuotesBatch(
233
262
  targets.map((target) => ({
@@ -236,6 +265,9 @@ export class GloomberbCloudProvider implements AssetDataProvider {
236
265
  })),
237
266
  options.forceRefresh ? "refresh" : "cache-first",
238
267
  );
268
+ if (isStaleCloudResponse(response)) {
269
+ throw createProviderMiss("Cloud quotes are stale");
270
+ }
239
271
  const payload = unwrapRequiredCloudResponse(response, "Cloud quotes are unavailable");
240
272
  return payload.items.map((item, index) => {
241
273
  const target = targets[index] ?? {
@@ -258,19 +290,54 @@ export class GloomberbCloudProvider implements AssetDataProvider {
258
290
  }
259
291
 
260
292
  async getExchangeRate(fromCurrency: string): Promise<number> {
261
- await requireVerifiedSession();
262
293
  const response = await apiClient.getCloudExchangeRate(fromCurrency);
263
294
  return unwrapRequiredCloudResponse(response, `Cloud exchange rate is unavailable for ${fromCurrency}`).rate;
264
295
  }
265
296
 
266
297
  async search(query: string, _context?: SearchRequestContext): Promise<InstrumentSearchResult[]> {
267
- await requireVerifiedSession();
268
298
  return withCloudFallback(
269
299
  () => apiClient.searchInstruments(query, 10),
270
300
  "Cloud search is unavailable",
271
301
  );
272
302
  }
273
303
 
304
+ async getSecFilings(ticker: string, count = 15): Promise<SecFilingItem[]> {
305
+ await requireVerifiedSession();
306
+ return withCloudFallback(async () => {
307
+ const response = await apiClient.getCloudSecFilings({ ticker, limit: count, offset: 0 });
308
+ return response.filings.map(mapCloudSecFiling);
309
+ }, `Cloud SEC filings are unavailable for ${ticker}`);
310
+ }
311
+
312
+ async getSecFilingDocuments(filing: SecFilingItem): Promise<SecFilingDocument[]> {
313
+ await requireVerifiedSession();
314
+ return withCloudFallback(async () => {
315
+ const response = await apiClient.getCloudSecFilingDocuments({
316
+ cik: filing.cik,
317
+ accession: filing.accessionNumber,
318
+ form: filing.form,
319
+ primaryDocument: filing.primaryDocument,
320
+ filingUrl: filing.filingUrl,
321
+ });
322
+ return response.documents;
323
+ }, "Cloud SEC filing documents are unavailable");
324
+ }
325
+
326
+ async getSecFilingContent(filing: SecFilingItem): Promise<string | null> {
327
+ await requireVerifiedSession();
328
+ return withCloudFallback(async () => {
329
+ const response = await apiClient.getCloudSecFilingContent({
330
+ cik: filing.cik,
331
+ accession: filing.accessionNumber,
332
+ form: filing.form,
333
+ primaryDocument: filing.primaryDocument,
334
+ primaryDocumentUrl: filing.primaryDocumentUrl,
335
+ filingUrl: filing.filingUrl,
336
+ });
337
+ return response.content;
338
+ }, "Cloud SEC filing content is unavailable");
339
+ }
340
+
274
341
  async getHolders(ticker: string, exchange = "", _context?: MarketDataRequestContext): Promise<HolderData> {
275
342
  await requireVerifiedSession();
276
343
  return withCloudFallback(async () => {
@@ -300,7 +367,6 @@ export class GloomberbCloudProvider implements AssetDataProvider {
300
367
  }
301
368
 
302
369
  async getPriceHistory(ticker: string, exchange: string, range: TimeRange, _context?: MarketDataRequestContext): Promise<PricePoint[]> {
303
- await requireVerifiedSession();
304
370
  const request = toHistoryRequest(range);
305
371
  const response = await withCloudFallback(
306
372
  () => apiClient.getCloudHistory(ticker, exchange, request),
@@ -316,7 +382,6 @@ export class GloomberbCloudProvider implements AssetDataProvider {
316
382
  resolution: ManualChartResolution,
317
383
  _context?: MarketDataRequestContext,
318
384
  ): Promise<PricePoint[]> {
319
- await requireVerifiedSession();
320
385
  const interval = toCloudInterval(resolution);
321
386
  const endDate = new Date();
322
387
  const startDate = getRangeStartDate(bufferRange, endDate);
@@ -340,7 +405,6 @@ export class GloomberbCloudProvider implements AssetDataProvider {
340
405
  barSize: string,
341
406
  _context?: MarketDataRequestContext,
342
407
  ): Promise<PricePoint[]> {
343
- await requireVerifiedSession();
344
408
  const interval = toCloudInterval(barSize);
345
409
  const includeTime = /(min|h)$/i.test(interval);
346
410
  const response = await withCloudFallback(
@@ -355,7 +419,6 @@ export class GloomberbCloudProvider implements AssetDataProvider {
355
419
  }
356
420
 
357
421
  async getOptionsChain(ticker: string, exchange?: string, expirationDate?: number, _context?: MarketDataRequestContext): Promise<OptionsChain> {
358
- await requireVerifiedSession();
359
422
  return withCloudFallback(async () => {
360
423
  const response = await apiClient.getCloudOptionsChain(ticker, exchange, expirationDate);
361
424
  const chain = unwrapRequiredCloudResponse(
@@ -370,9 +433,8 @@ export class GloomberbCloudProvider implements AssetDataProvider {
370
433
  targets: QuoteSubscriptionTarget[],
371
434
  onQuote: (target: QuoteSubscriptionTarget, quote: Quote) => void,
372
435
  ): () => void {
373
- if (apiClient.getSessionToken()) {
374
- void apiClient.ensureVerifiedSession().catch(() => {});
375
- }
436
+ // No-op without a session credential; covers browser cookie sessions too.
437
+ void apiClient.ensureVerifiedSession().catch(() => {});
376
438
  const targetMap = new Map<string, QuoteSubscriptionTarget[]>();
377
439
  for (const target of targets) {
378
440
  const key = quoteTargetKey(target.symbol, target.exchange);
@@ -421,6 +483,16 @@ export function createGloomberbCloudCapabilities(provider = createGloomberbCloud
421
483
  const feed = query.feed ?? (query.scope === "ticker" ? "ticker" : "latest");
422
484
  return feed === "ticker" ? !!query.ticker : true;
423
485
  },
486
+ async fetchNewsPage(query: NewsQuery) {
487
+ const response = await withCloudFallback(
488
+ () => apiClient.getCloudNews(cloudNewsParams(query)),
489
+ "Cloud news is unavailable",
490
+ );
491
+ return {
492
+ articles: response.items.map((item) => mapCloudNewsArticle(item, query.ticker)),
493
+ nextCursor: response.nextCursor ?? null,
494
+ };
495
+ },
424
496
  async fetchNews(query: NewsQuery): Promise<NewsArticle[]> {
425
497
  const response = await withCloudFallback(
426
498
  () => apiClient.getCloudNews(cloudNewsParams(query)),
@@ -0,0 +1,34 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { mapCloudFinancials } from "./normalizers";
3
+
4
+ describe("mapCloudFinancials", () => {
5
+ test("divides GBp history with the raw quote currency, not the normalized GBP quote", () => {
6
+ const financials = mapCloudFinancials({
7
+ quote: {
8
+ symbol: "VOD",
9
+ price: 23.1,
10
+ currency: "GBp",
11
+ change: 1,
12
+ changePercent: 4.5,
13
+ lastUpdated: Date.parse("2026-05-13T15:00:00Z"),
14
+ listingExchangeName: "LSE",
15
+ exchangeName: "LSE",
16
+ },
17
+ annualStatements: [],
18
+ quarterlyStatements: [],
19
+ priceHistory: [{
20
+ date: "2026-05-13 10:15:00",
21
+ open: 23,
22
+ high: 23.4,
23
+ low: 22.8,
24
+ close: 23.1,
25
+ volume: 1000,
26
+ }],
27
+ });
28
+
29
+ expect(financials.quote?.currency).toBe("GBP");
30
+ expect(financials.quote?.price).toBeCloseTo(0.231);
31
+ expect(financials.priceHistory[0]?.close).toBeCloseTo(0.231);
32
+ expect(financials.priceHistory[0]?.date.toISOString()).toBe("2026-05-13T09:15:00.000Z");
33
+ });
34
+ });
@@ -190,10 +190,10 @@ export function mapCloudFinancials(
190
190
  financials: TickerFinancials,
191
191
  providerMeta?: CloudProviderMeta,
192
192
  ): TickerFinancials {
193
- const quote = financials.quote
194
- ? mapQuote(financials.quote as CloudQuotePayload, providerMeta)
195
- : undefined;
196
- const divisor = quote ? resolveCurrencyUnit(quote.currency).divisor : 1;
193
+ const rawQuote = financials.quote as CloudQuotePayload | undefined;
194
+ const quote = rawQuote ? mapQuote(rawQuote, providerMeta) : undefined;
195
+ const divisor = rawQuote ? resolveCurrencyUnit(rawQuote.currency).divisor : 1;
196
+ const exchange = rawQuote?.listingExchangeName ?? rawQuote?.exchangeName ?? "";
197
197
  return {
198
198
  quote,
199
199
  quoteContributions: financials.quoteContributions,
@@ -204,7 +204,7 @@ export function mapCloudFinancials(
204
204
  priceHistory: (financials.priceHistory ?? []).map((point) =>
205
205
  point.date instanceof Date
206
206
  ? point
207
- : mapPricePoint(point as unknown as CloudPricePointPayload, divisor),
207
+ : mapPricePoint(point as unknown as CloudPricePointPayload, divisor, exchange),
208
208
  ),
209
209
  };
210
210
  }
@@ -391,7 +391,7 @@ describe("AssetDataRouter chart history", () => {
391
391
  expect(history[0]?.close).toBe(102);
392
392
  });
393
393
 
394
- test("accepts previous-session short-range chart history while the exchange is closed", async () => {
394
+ test("accepts previous-session history without persisted exchange metadata while closed", async () => {
395
395
  const originalDateNow = Date.now;
396
396
  Date.now = () => Date.parse("2026-05-17T12:00:00Z");
397
397
 
@@ -419,7 +419,7 @@ describe("AssetDataRouter chart history", () => {
419
419
  };
420
420
 
421
421
  const router = new AssetDataRouter(yahooProvider, [cloudProvider]);
422
- const history = await router.getPriceHistoryForResolution("AAPL", "NASDAQ", "1M", "15m");
422
+ const history = await router.getPriceHistoryForResolution("AAPL", "", "1M", "15m");
423
423
 
424
424
  expect(history.map((point) => point.close)).toEqual([101, 102]);
425
425
  } finally {
@@ -0,0 +1,34 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { ProviderRouterNewsRoutes } from "./news";
3
+
4
+ function failingSource() {
5
+ return {
6
+ id: "cloud",
7
+ name: "cloud",
8
+ news: {
9
+ fetchNews: async () => {
10
+ throw new Error("cloud down");
11
+ },
12
+ },
13
+ };
14
+ }
15
+
16
+ describe("ProviderRouterNewsRoutes", () => {
17
+ test("throws when every ticker news source fails", async () => {
18
+ const routes = new ProviderRouterNewsRoutes({
19
+ newsSourcesInPriorityOrder: () => [failingSource()],
20
+ logProviderError: () => {},
21
+ });
22
+
23
+ await expect(routes.getNews({ feed: "ticker", ticker: "AAPL" })).rejects.toThrow("cloud down");
24
+ });
25
+
26
+ test("throws when every global news source fails", async () => {
27
+ const routes = new ProviderRouterNewsRoutes({
28
+ newsSourcesInPriorityOrder: () => [failingSource()],
29
+ logProviderError: () => {},
30
+ });
31
+
32
+ await expect(routes.getNews({ feed: "latest" })).rejects.toThrow("cloud down");
33
+ });
34
+ });
@@ -16,21 +16,35 @@ export class ProviderRouterNewsRoutes {
16
16
  const feed = query.feed ?? (query.scope === "ticker" ? "ticker" : "latest");
17
17
  if (feed === "ticker") {
18
18
  let firstEmpty: NewsArticle[] | null = null;
19
+ let lastError: unknown = null;
20
+ let sawSuccess = false;
19
21
  for (const source of sources) {
20
22
  try {
21
23
  const articles = await source.news!.fetchNews(query);
24
+ sawSuccess = true;
22
25
  if (articles.length > 0) return articles;
23
26
  firstEmpty ??= articles;
24
27
  } catch (error) {
28
+ lastError = error;
25
29
  if (shouldLogProviderError(error)) {
26
30
  this.options.logProviderError(`${source.id} failed: ${error}`);
27
31
  }
28
32
  }
29
33
  }
34
+ if (!sawSuccess && lastError) throw lastError;
30
35
  return firstEmpty ?? [];
31
36
  }
32
37
 
33
38
  const settled = await Promise.allSettled(sources.map((source) => source.news!.fetchNews(query)));
34
- return settled.flatMap((result) => result.status === "fulfilled" ? result.value : []);
39
+ const articles = settled.flatMap((result) => result.status === "fulfilled" ? result.value : []);
40
+ if (
41
+ articles.length === 0
42
+ && settled.length > 0
43
+ && settled.every((result) => result.status === "rejected")
44
+ ) {
45
+ const rejected = settled.find((result): result is PromiseRejectedResult => result.status === "rejected");
46
+ throw rejected?.reason ?? new Error("News sources are unavailable");
47
+ }
48
+ return articles;
35
49
  }
36
50
  }
@@ -5,6 +5,7 @@ import {
5
5
  parseCompanyFactsFinancialStatements,
6
6
  parseFilingDocuments,
7
7
  parseRecentFilings,
8
+ parseSubmissionArchiveNames,
8
9
  parseTickerLookup,
9
10
  } from "./sec-edgar";
10
11
 
@@ -65,6 +66,17 @@ describe("parseRecentFilings", () => {
65
66
  expect(filings[0]?.filingUrl).toBe("https://www.sec.gov/Archives/edgar/data/320193/0000320193-24-000123-index.htm");
66
67
  expect(filings[0]?.primaryDocumentUrl).toBe("https://www.sec.gov/Archives/edgar/data/320193/000032019324000123/aapl-8k.htm");
67
68
  });
69
+
70
+ test("reads older submission archive names", () => {
71
+ expect(parseSubmissionArchiveNames({
72
+ filings: {
73
+ files: [
74
+ { name: "CIK0000320193-submissions-001.json", filingCount: 1000 },
75
+ { name: "readme.txt" },
76
+ ],
77
+ },
78
+ })).toEqual(["CIK0000320193-submissions-001.json"]);
79
+ });
68
80
  });
69
81
 
70
82
  describe("parseFilingDocuments", () => {
@@ -613,6 +625,53 @@ describe("SecEdgarClient", () => {
613
625
  expect(headersSeen[0]?.From).toBeTruthy();
614
626
  });
615
627
 
628
+ test("loads older EDGAR submission archives when recent filings run out", async () => {
629
+ const urls: string[] = [];
630
+ globalThis.fetch = (async (input: Request | string | URL) => {
631
+ const url = String(input);
632
+ urls.push(url);
633
+ if (url.includes("company_tickers_exchange.json")) {
634
+ return new Response(JSON.stringify({
635
+ fields: ["cik", "name", "ticker", "exchange"],
636
+ data: [[320193, "Apple Inc.", "AAPL", "Nasdaq"]],
637
+ }), { status: 200, headers: { "content-type": "application/json" } });
638
+ }
639
+ if (url.endsWith("CIK0000320193.json")) {
640
+ return new Response(JSON.stringify({
641
+ cik: "0000320193",
642
+ name: "Apple Inc.",
643
+ filings: {
644
+ recent: {
645
+ accessionNumber: ["0000320193-24-000123"],
646
+ form: ["8-K"],
647
+ filingDate: ["2024-08-02"],
648
+ acceptanceDateTime: ["20240802120000"],
649
+ primaryDocument: ["aapl-8k.htm"],
650
+ primaryDocDescription: ["Current report"],
651
+ items: [""],
652
+ },
653
+ files: [{ name: "CIK0000320193-submissions-001.json" }],
654
+ },
655
+ }), { status: 200, headers: { "content-type": "application/json" } });
656
+ }
657
+ return new Response(JSON.stringify({
658
+ accessionNumber: ["0000320193-18-000001"],
659
+ form: ["4"],
660
+ filingDate: ["2018-01-02"],
661
+ acceptanceDateTime: ["20180102120000"],
662
+ primaryDocument: ["xslF345X03/wk-form4.xml"],
663
+ primaryDocDescription: ["Form 4"],
664
+ items: [""],
665
+ }), { status: 200, headers: { "content-type": "application/json" } });
666
+ }) as unknown as typeof fetch;
667
+
668
+ const client = new SecEdgarClient();
669
+ const filings = await client.getRecentFilings("AAPL", 2);
670
+ expect(urls.some((url) => url.endsWith("CIK0000320193-submissions-001.json"))).toBe(true);
671
+ expect(filings.map((filing) => filing.form)).toEqual(["8-K", "4"]);
672
+ expect(filings[1]?.accessionNumber).toBe("0000320193-18-000001");
673
+ });
674
+
616
675
  test("surfaces SEC bot blocking errors clearly", async () => {
617
676
  globalThis.fetch = (async () => new Response(
618
677
  "<html><body>Undeclared Automated Tool</body></html>",
@@ -252,25 +252,35 @@ export function parseTickerLookup(payload: unknown): Map<string, LookupEntry> {
252
252
  return results;
253
253
  }
254
254
 
255
- export function parseRecentFilings(payload: unknown, count = 15): SecFilingItem[] {
256
- const record = asRecord(payload);
257
- const filings = asRecord(record?.filings);
258
- const recent = asRecord(filings?.recent);
259
- if (!record || !recent) return [];
260
-
261
- const accessionNumbers = Array.isArray(recent.accessionNumber) ? recent.accessionNumber : [];
262
- const forms = Array.isArray(recent.form) ? recent.form : [];
263
- const filingDates = Array.isArray(recent.filingDate) ? recent.filingDate : [];
264
- const acceptanceTimes = Array.isArray(recent.acceptanceDateTime) ? recent.acceptanceDateTime : [];
265
- const primaryDocuments = Array.isArray(recent.primaryDocument) ? recent.primaryDocument : [];
266
- const primaryDescriptions = Array.isArray(recent.primaryDocDescription) ? recent.primaryDocDescription : [];
267
- const items = Array.isArray(recent.items) ? recent.items : [];
268
- const cik = zeroPadCik(record.cik) ?? "";
269
- const displayCik = String(Number(cik || "0"));
270
- const companyName = typeof record.name === "string" ? record.name : undefined;
255
+ export function parseSubmissionArchiveNames(payload: unknown): string[] {
256
+ const files = asRecord(asRecord(payload)?.filings)?.files;
257
+ if (!Array.isArray(files)) return [];
258
+ const names: string[] = [];
259
+ for (const file of files) {
260
+ const name = asRecord(file)?.name;
261
+ if (typeof name === "string" && /^CIK\d+-submissions-\d+\.json$/i.test(name)) {
262
+ names.push(name);
263
+ }
264
+ }
265
+ return names;
266
+ }
267
+
268
+ function parseFilingColumns(
269
+ columns: Record<string, unknown> | null | undefined,
270
+ company: { cik: string; displayCik: string; companyName?: string },
271
+ limit: number,
272
+ ): SecFilingItem[] {
273
+ if (!columns || !company.displayCik) return [];
274
+ const accessionNumbers = Array.isArray(columns.accessionNumber) ? columns.accessionNumber : [];
275
+ const forms = Array.isArray(columns.form) ? columns.form : [];
276
+ const filingDates = Array.isArray(columns.filingDate) ? columns.filingDate : [];
277
+ const acceptanceTimes = Array.isArray(columns.acceptanceDateTime) ? columns.acceptanceDateTime : [];
278
+ const primaryDocuments = Array.isArray(columns.primaryDocument) ? columns.primaryDocument : [];
279
+ const primaryDescriptions = Array.isArray(columns.primaryDocDescription) ? columns.primaryDocDescription : [];
280
+ const items = Array.isArray(columns.items) ? columns.items : [];
271
281
  const total = Math.min(
272
282
  Math.max(accessionNumbers.length, forms.length, filingDates.length),
273
- Math.max(count, 0),
283
+ Math.max(limit, 0),
274
284
  );
275
285
 
276
286
  const results: SecFilingItem[] = [];
@@ -278,13 +288,13 @@ export function parseRecentFilings(payload: unknown, count = 15): SecFilingItem[
278
288
  const accessionNumber = String(accessionNumbers[index] ?? "").trim();
279
289
  const form = String(forms[index] ?? "").trim();
280
290
  const filingDate = parseDate(filingDates[index]);
281
- if (!accessionNumber || !form || !filingDate || !displayCik) continue;
291
+ if (!accessionNumber || !form || !filingDate) continue;
282
292
 
283
293
  const accessionNumberNoDashes = stripAccessionDashes(accessionNumber);
284
294
  const primaryDocument = String(primaryDocuments[index] ?? "").trim() || undefined;
285
- const filingUrl = `https://www.sec.gov/Archives/edgar/data/${displayCik}/${accessionNumber}-index.htm`;
295
+ const filingUrl = `https://www.sec.gov/Archives/edgar/data/${company.displayCik}/${accessionNumber}-index.htm`;
286
296
  const primaryDocumentUrl = primaryDocument
287
- ? `https://www.sec.gov/Archives/edgar/data/${displayCik}/${accessionNumberNoDashes}/${primaryDocument}`
297
+ ? `https://www.sec.gov/Archives/edgar/data/${company.displayCik}/${accessionNumberNoDashes}/${primaryDocument}`
288
298
  : undefined;
289
299
 
290
300
  results.push({
@@ -295,8 +305,8 @@ export function parseRecentFilings(payload: unknown, count = 15): SecFilingItem[
295
305
  primaryDocument,
296
306
  primaryDocDescription: String(primaryDescriptions[index] ?? "").trim() || undefined,
297
307
  items: String(items[index] ?? "").trim() || undefined,
298
- cik,
299
- companyName,
308
+ cik: company.cik,
309
+ companyName: company.companyName,
300
310
  filingUrl,
301
311
  primaryDocumentUrl,
302
312
  });
@@ -305,6 +315,28 @@ export function parseRecentFilings(payload: unknown, count = 15): SecFilingItem[
305
315
  return results;
306
316
  }
307
317
 
318
+ function submissionCompany(payload: unknown, fallbackCik = ""): {
319
+ cik: string;
320
+ displayCik: string;
321
+ companyName?: string;
322
+ } {
323
+ const record = asRecord(payload);
324
+ const cik = zeroPadCik(record?.cik) ?? fallbackCik;
325
+ return {
326
+ cik,
327
+ displayCik: String(Number(cik || "0")),
328
+ companyName: typeof record?.name === "string" ? record.name : undefined,
329
+ };
330
+ }
331
+
332
+ export function parseRecentFilings(payload: unknown, count = 15): SecFilingItem[] {
333
+ const record = asRecord(payload);
334
+ if (!record) return [];
335
+ const recent = asRecord(asRecord(record.filings)?.recent)
336
+ ?? (Array.isArray(record.accessionNumber) ? record : null);
337
+ return parseFilingColumns(recent, submissionCompany(record), count);
338
+ }
339
+
308
340
  export function parseFilingDocuments(indexHtml: string, filing: SecFilingItem): SecFilingDocument[] {
309
341
  const documents: SecFilingDocument[] = [];
310
342
  const primaryUrl = normalizeDocumentName(filing.primaryDocumentUrl);
@@ -666,7 +698,15 @@ export class SecEdgarClient {
666
698
  if (!entry) return [];
667
699
 
668
700
  const payload = await this.fetchJson<unknown>(`${SUBMISSIONS_URL}/CIK${entry.cik}.json`);
669
- return parseRecentFilings(payload, count);
701
+ const filings = parseRecentFilings(payload, count);
702
+ if (filings.length >= count) return filings;
703
+ const company = submissionCompany(payload, entry.cik);
704
+ for (const name of parseSubmissionArchiveNames(payload)) {
705
+ if (filings.length >= count) break;
706
+ const older = await this.fetchJson<unknown>(`${SUBMISSIONS_URL}/${name}`);
707
+ filings.push(...parseFilingColumns(asRecord(older), company, count - filings.length));
708
+ }
709
+ return filings;
670
710
  }
671
711
 
672
712
  async getFinancialStatements(ticker: string): Promise<SecCompanyFactsStatements | null> {
@@ -81,6 +81,9 @@ export function yahooRawDate(value: unknown): string | undefined {
81
81
  }
82
82
 
83
83
  function inferEarningsTiming(date: Date): EarningsEvent["timing"] {
84
+ if (date.getUTCHours() === 0 && date.getUTCMinutes() === 0 && date.getUTCSeconds() === 0) {
85
+ return "";
86
+ }
84
87
  const hour = date.getUTCHours();
85
88
  if (hour >= 20) return "AMC";
86
89
  if (hour <= 13) return "BMO";
@@ -154,7 +154,7 @@ export async function loadYahooTickerFinancials(
154
154
  low52w: meta.fiftyTwoWeekLow,
155
155
  marketCap: latest("trailingMarketCap"),
156
156
  name: meta.shortName || meta.longName,
157
- lastUpdated: Date.now(),
157
+ lastUpdated: yahooMarketTimestamp(meta),
158
158
  exchangeName: meta.exchangeName,
159
159
  fullExchangeName: meta.fullExchangeName,
160
160
  listingExchangeName: meta.exchangeName,
@@ -233,7 +233,7 @@ export async function loadYahooQuote(
233
233
  high52w: meta.fiftyTwoWeekHigh,
234
234
  low52w: meta.fiftyTwoWeekLow,
235
235
  name: meta.shortName || meta.longName,
236
- lastUpdated: Date.now(),
236
+ lastUpdated: yahooMarketTimestamp(meta),
237
237
  exchangeName: meta.exchangeName,
238
238
  fullExchangeName: meta.fullExchangeName,
239
239
  listingExchangeName: meta.exchangeName,
@@ -245,3 +245,11 @@ export async function loadYahooQuote(
245
245
  ...extHours,
246
246
  };
247
247
  }
248
+
249
+ function yahooMarketTimestamp(meta: NonNullable<ChartResult["meta"]>): number {
250
+ const marketTime = meta.regularMarketTime;
251
+ if (typeof marketTime === "number" && Number.isFinite(marketTime) && marketTime > 0) {
252
+ return marketTime < 1e12 ? marketTime * 1000 : marketTime;
253
+ }
254
+ return Date.now();
255
+ }
@@ -0,0 +1,10 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { getYahooSymbol, getYahooSymbolsToTry } from "./symbols";
3
+
4
+ describe("Yahoo symbol routing", () => {
5
+ test("canonicalizes MIC aliases before applying exchange suffixes", () => {
6
+ expect(getYahooSymbol("VOD", "XLON")).toBe("VOD.L");
7
+ expect(getYahooSymbolsToTry("0700", "XHKG")).toEqual(["0700.HK"]);
8
+ expect(getYahooSymbolsToTry("SHOP", "XTSE")).toEqual(["SHOP.TO"]);
9
+ });
10
+ });