gloomberb 0.16.3 → 0.16.4

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 (451) hide show
  1. package/package.json +4 -1
  2. package/src/api-client/askg.ts +43 -0
  3. package/src/api-client/auth.ts +15 -0
  4. package/src/api-client/beneficial-owners.ts +4 -0
  5. package/src/api-client/chat.ts +19 -0
  6. package/src/api-client/company-kpis.ts +14 -2
  7. package/src/api-client/data.ts +34 -16
  8. package/src/api-client/index.ts +12 -0
  9. package/src/api-client/normalizers.ts +27 -0
  10. package/src/api-client/paths.ts +88 -23
  11. package/src/api-client/request.ts +2 -1
  12. package/src/api-client/research-activity.ts +31 -0
  13. package/src/api-client/socket.ts +39 -8
  14. package/src/api-client/types.ts +57 -1
  15. package/src/api-client/upgrade-experiment.ts +90 -0
  16. package/src/app/global-shortcuts.ts +11 -1
  17. package/src/app/keybindings/actions.ts +10 -0
  18. package/src/app/pane-runtime/index.tsx +8 -0
  19. package/src/app/pane-runtime/pane-template-runtime.ts +5 -0
  20. package/src/app/pane-runtime/plugin-bindings.ts +65 -25
  21. package/src/app/pane-runtime/ticker-inspector-runtime.ts +12 -0
  22. package/src/app/pane-runtime/ticker-open-runtime.ts +18 -1
  23. package/src/app/runtime/desktop-deeplink.ts +5 -0
  24. package/src/app/star-prompt/model.ts +98 -0
  25. package/src/app/star-prompt/runtime.ts +41 -0
  26. package/src/app/star-prompt/status-line.tsx +190 -0
  27. package/src/app.tsx +13 -7
  28. package/src/architecture/core-builtin-allowlist.json +70 -0
  29. package/src/brokers/signed-in/adapter.ts +33 -2
  30. package/src/brokers/signed-in/client.ts +10 -0
  31. package/src/brokers/signed-in/sign-in-dialog.tsx +23 -9
  32. package/src/brokers/signed-in/sign-in.ts +62 -5
  33. package/src/capabilities/registry.ts +8 -1
  34. package/src/capabilities/types.ts +2 -0
  35. package/src/cli/commands/crypto-hints.ts +12 -4
  36. package/src/cli/commands/market.ts +359 -99
  37. package/src/cli/commands/options-chain.ts +172 -0
  38. package/src/cli/commands/options-leaps.ts +422 -0
  39. package/src/cli/commands/overview.ts +164 -26
  40. package/src/cli/commands/search.ts +29 -8
  41. package/src/cli/commands/system.ts +99 -19
  42. package/src/cli/commands/themes.ts +64 -0
  43. package/src/cli/commands/ticker.ts +62 -30
  44. package/src/cli/desktop-pane-shot.ts +97 -16
  45. package/src/cli/errors.ts +25 -2
  46. package/src/cli/freshness.ts +7 -4
  47. package/src/cli/fx-pair.ts +57 -0
  48. package/src/cli/help.ts +14 -1
  49. package/src/cli/index-roots.ts +51 -0
  50. package/src/cli/index.ts +9 -6
  51. package/src/cli/listing-arg.ts +211 -3
  52. package/src/cli/not-a-ticker.ts +37 -0
  53. package/src/cli/options.ts +50 -6
  54. package/src/cli/pane-functions/capabilities.ts +8 -2
  55. package/src/cli/pane-functions/catalog.ts +13 -2
  56. package/src/cli/pane-functions/data.ts +36 -5
  57. package/src/cli/pane-functions/dom.ts +21 -13
  58. package/src/cli/pane-functions/freshness.ts +205 -26
  59. package/src/cli/pane-functions/function-help.ts +67 -22
  60. package/src/cli/pane-functions/headless.ts +5 -4
  61. package/src/cli/pane-functions/index.ts +3 -0
  62. package/src/cli/pane-functions/market-session.ts +133 -0
  63. package/src/cli/pane-functions/options.ts +15 -4
  64. package/src/cli/pane-functions/report-notices.ts +63 -0
  65. package/src/cli/pane-functions/report.ts +4 -0
  66. package/src/cli/pane-functions/resolver.ts +4 -6
  67. package/src/cli/pane-functions/screenshot-evidence.ts +2 -0
  68. package/src/cli/pane-functions/screenshot.ts +105 -23
  69. package/src/cli/registry.ts +2 -1
  70. package/src/cli/result.ts +50 -13
  71. package/src/cli/row-window.ts +76 -0
  72. package/src/cli/scroll-truncation.ts +34 -0
  73. package/src/components/chart/composite/composite-chart.tsx +43 -4
  74. package/src/components/chart/composite/legend.tsx +39 -6
  75. package/src/components/chart/composite/rasterizer.ts +64 -4
  76. package/src/components/chart/composite/series-cues.ts +71 -0
  77. package/src/components/chart/composite/text-renderer.ts +16 -8
  78. package/src/components/chart/composite/types.ts +3 -0
  79. package/src/components/command-bar/commands/direct/index.ts +9 -1
  80. package/src/components/command-bar/commands/plugin/items.ts +3 -1
  81. package/src/components/command-bar/commands/registry.ts +7 -0
  82. package/src/components/command-bar/layout-items/focused-pane.ts +38 -0
  83. package/src/components/command-bar/pane-templates/workflow.ts +5 -0
  84. package/src/components/command-bar/routes/root/command-items.ts +2 -0
  85. package/src/components/command-bar/routes/root/plugin-turn-on.ts +92 -0
  86. package/src/components/command-bar/routes/root/results.ts +40 -10
  87. package/src/components/command-bar/routes/root/runtime.ts +8 -0
  88. package/src/components/command-bar/routes/root/search-providers.ts +97 -14
  89. package/src/components/command-bar/surface/index.tsx +18 -4
  90. package/src/components/command-bar/theme-picker.tsx +48 -13
  91. package/src/components/command-bar/workflow/ops.ts +5 -3
  92. package/src/components/data-table/view.tsx +28 -9
  93. package/src/components/layout/floating-pane.tsx +6 -2
  94. package/src/components/layout/market-summary.ts +23 -9
  95. package/src/components/layout/pane/content.tsx +46 -29
  96. package/src/components/layout/pane/cycle-order.ts +2 -11
  97. package/src/components/layout/pane/header.tsx +2 -1
  98. package/src/components/layout/pane/title.ts +1 -1
  99. package/src/components/layout/presentation.ts +22 -0
  100. package/src/components/layout/shell/active-drag.ts +1 -1
  101. package/src/components/layout/shell/drag/index.ts +4 -0
  102. package/src/components/layout/shell/drag/live.ts +76 -5
  103. package/src/components/layout/shell/drag/runtime.ts +8 -8
  104. package/src/components/layout/shell/index.tsx +47 -6
  105. package/src/components/layout/shell/layout-state.ts +5 -11
  106. package/src/components/layout/shell/menu.ts +28 -1
  107. package/src/components/layout/shell/pane/actions.ts +2 -1
  108. package/src/components/layout/shell/pane/layers.tsx +51 -10
  109. package/src/components/layout/shell/pane/management-shortcuts.ts +5 -0
  110. package/src/components/layout/shell/pane/new-layout.ts +116 -0
  111. package/src/components/layout/shell/shortcuts.ts +5 -2
  112. package/src/components/layout/shell/visible-layout.ts +44 -1
  113. package/src/components/layout/status-bar.tsx +4 -1
  114. package/src/components/markdown-text.tsx +9 -5
  115. package/src/components/metric-treemap/heat-surface.tsx +27 -8
  116. package/src/components/metric-treemap/heat-tiles.ts +48 -17
  117. package/src/components/onboarding/onboarding-frame.tsx +33 -10
  118. package/src/components/onboarding/onboarding-wizard.tsx +49 -40
  119. package/src/components/onboarding/portfolio-step/positions-panel.tsx +5 -2
  120. package/src/components/onboarding/pro-step-layout.ts +86 -0
  121. package/src/components/onboarding/use-onboarding-upgrade.ts +6 -2
  122. package/src/components/ui/button.tsx +10 -0
  123. package/src/components/ui/data-table/index.tsx +38 -0
  124. package/src/components/ui/data-table/types.ts +8 -1
  125. package/src/components/ui/data-table/visible-range.ts +2 -0
  126. package/src/components/ui/icon.tsx +11 -2
  127. package/src/components/ui/index.ts +1 -1
  128. package/src/core/connection-health.ts +1 -1
  129. package/src/core/state/app/layout-reducer.ts +89 -3
  130. package/src/core/state/app/state.ts +8 -5
  131. package/src/core/state/app/types.ts +13 -2
  132. package/src/data/config/layout.ts +55 -21
  133. package/src/data/config/store/fold-panes.ts +149 -0
  134. package/src/data/config/store/migrations.ts +7 -0
  135. package/src/data/config/store/node.ts +27 -6
  136. package/src/data/config/store/normalize.ts +37 -3
  137. package/src/i18n/es.ts +14 -1
  138. package/src/i18n/ja.ts +14 -1
  139. package/src/i18n/ko.ts +14 -1
  140. package/src/i18n/zh-cn.ts +14 -1
  141. package/src/i18n/zh-tw.ts +14 -1
  142. package/src/layout/desks.ts +1 -2
  143. package/src/layout/pane-layout-move.ts +264 -0
  144. package/src/layout/pane-manager/hidden-panes.ts +160 -0
  145. package/src/layout/pane-manager/index.ts +1 -0
  146. package/src/market-data/coordinator/entries.ts +7 -1
  147. package/src/market-data/coordinator/quotes.ts +2 -2
  148. package/src/market-data/market/freshness.ts +118 -18
  149. package/src/market-data/market/status.ts +40 -0
  150. package/src/market-data/options-alternatives.ts +69 -0
  151. package/src/market-data/quotes/contributions.ts +19 -2
  152. package/src/market-data/quotes/metadata.ts +16 -0
  153. package/src/market-data/quotes/screener-live-quotes.ts +56 -5
  154. package/src/plugins/builtin/account-management/keyboard.ts +3 -0
  155. package/src/plugins/builtin/account-management/model.ts +1 -0
  156. package/src/plugins/builtin/account-management/navigation.ts +1 -1
  157. package/src/plugins/builtin/account-management/pane.tsx +31 -4
  158. package/src/plugins/builtin/alerts/quotes.ts +3 -0
  159. package/src/plugins/builtin/analytics/risk-headless.ts +3 -1
  160. package/src/plugins/builtin/analytics/risk-model.ts +37 -8
  161. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -4
  162. package/src/plugins/builtin/backtest/pane.tsx +1 -1
  163. package/src/plugins/builtin/broker-manager/detail.tsx +4 -0
  164. package/src/plugins/builtin/broker-manager/index.tsx +7 -3
  165. package/src/plugins/builtin/broker-manager/model.ts +8 -0
  166. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -2
  167. package/src/plugins/builtin/broker-manager/sign-in-end.ts +82 -0
  168. package/src/plugins/builtin/broker-manager/table.ts +5 -0
  169. package/src/plugins/builtin/builtin-plugin-meta.ts +121 -11
  170. package/src/plugins/builtin/chart-composer/pane.tsx +1 -1
  171. package/src/plugins/builtin/chat/channels.ts +34 -36
  172. package/src/plugins/builtin/chat/command-bar.ts +138 -0
  173. package/src/plugins/builtin/chat/composer-commands.ts +21 -1
  174. package/src/plugins/builtin/chat/content/composer-runtime.ts +11 -1
  175. package/src/plugins/builtin/chat/content/footer.tsx +5 -3
  176. package/src/plugins/builtin/chat/content/index.tsx +21 -1
  177. package/src/plugins/builtin/chat/content/mentions.ts +2 -1
  178. package/src/plugins/builtin/chat/content/user-map.ts +3 -1
  179. package/src/plugins/builtin/chat/controller/index.ts +48 -0
  180. package/src/plugins/builtin/chat/conversations.ts +256 -0
  181. package/src/plugins/builtin/chat/discord-command.ts +91 -0
  182. package/src/plugins/builtin/chat/edit-window.ts +12 -6
  183. package/src/plugins/builtin/chat/ghost-user.ts +14 -0
  184. package/src/plugins/builtin/chat/index.tsx +40 -2
  185. package/src/plugins/builtin/chat/layout.ts +2 -0
  186. package/src/plugins/builtin/chat/message/desktop.tsx +2 -1
  187. package/src/plugins/builtin/chat/message/header.tsx +24 -15
  188. package/src/plugins/builtin/chat/message/profile-popover.tsx +2 -1
  189. package/src/plugins/builtin/chat/message/render-state.ts +3 -0
  190. package/src/plugins/builtin/chat/pane-requests.ts +60 -0
  191. package/src/plugins/builtin/chat/pane-state.ts +32 -2
  192. package/src/plugins/builtin/chat/pane.tsx +8 -0
  193. package/src/plugins/builtin/cloud/askg/controller.ts +112 -4
  194. package/src/plugins/builtin/cloud/askg/conversation-store.ts +21 -0
  195. package/src/plugins/builtin/cloud/askg/manifest.ts +1 -0
  196. package/src/plugins/builtin/cloud/askg/model.ts +129 -70
  197. package/src/plugins/builtin/cloud/askg/pane.tsx +772 -377
  198. package/src/plugins/builtin/cloud/askg/protocol.ts +40 -2
  199. package/src/plugins/builtin/cloud/askg/tool-display.ts +437 -0
  200. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +403 -0
  201. package/src/plugins/builtin/cloud/company-picker.tsx +1 -9
  202. package/src/plugins/builtin/cloud/mcp-connect/command.ts +24 -0
  203. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +360 -0
  204. package/src/plugins/builtin/cloud/mcp-connect/model.ts +135 -0
  205. package/src/plugins/builtin/cloud/mcp-connect/sections.ts +62 -0
  206. package/src/plugins/builtin/cloud/plugin.tsx +36 -1
  207. package/src/plugins/builtin/cloud/starter-symbols.ts +8 -0
  208. package/src/plugins/builtin/cloud/terminal-relay/acm-tab.tsx +89 -0
  209. package/src/plugins/builtin/cloud/terminal-relay/client.ts +97 -0
  210. package/src/plugins/builtin/cloud/terminal-relay/connect-section.tsx +57 -0
  211. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +666 -0
  212. package/src/plugins/builtin/cloud/terminal-relay/grants.ts +137 -0
  213. package/src/plugins/builtin/cloud/terminal-relay/host.tsx +81 -0
  214. package/src/plugins/builtin/cloud/terminal-relay/indicator.tsx +82 -0
  215. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +234 -0
  216. package/src/plugins/builtin/cloud/terminal-relay/policy.ts +153 -0
  217. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +168 -0
  218. package/src/plugins/builtin/cloud/terminal-relay/redact.ts +73 -0
  219. package/src/plugins/builtin/cloud/terminal-relay/summary.ts +62 -0
  220. package/src/plugins/builtin/cloud/upgrade-dialog.tsx +26 -2
  221. package/src/plugins/builtin/cloud/upgrade-personalization.ts +57 -0
  222. package/src/plugins/builtin/cloud/upgrade-tickers.ts +124 -0
  223. package/src/plugins/builtin/comment-letters/client.ts +1 -3
  224. package/src/plugins/builtin/company-kpis/client.ts +23 -8
  225. package/src/plugins/builtin/company-kpis/headless.ts +3 -1
  226. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  227. package/src/plugins/builtin/company-kpis/screenshot-evidence.ts +10 -3
  228. package/src/plugins/builtin/composite-plugins.ts +107 -64
  229. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +4 -4
  230. package/src/plugins/builtin/credit-boards/index.tsx +21 -21
  231. package/src/plugins/builtin/credit-boards/model.ts +0 -1
  232. package/src/plugins/builtin/credit-boards/pane.tsx +30 -0
  233. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +4 -4
  234. package/src/plugins/builtin/debt-maturities/client.ts +37 -9
  235. package/src/plugins/builtin/debt-maturities/headless.ts +3 -1
  236. package/src/plugins/builtin/debt-maturities/pane.tsx +5 -4
  237. package/src/plugins/builtin/dividend-yield/index.tsx +1 -1
  238. package/src/plugins/builtin/dividend-yield/pane.tsx +1 -1
  239. package/src/plugins/builtin/earnings-calls/data.ts +28 -7
  240. package/src/plugins/builtin/earnings-calls/headless.ts +3 -2
  241. package/src/plugins/builtin/earnings-calls/pane.tsx +10 -5
  242. package/src/plugins/builtin/econ/calendar-model.ts +42 -17
  243. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  244. package/src/plugins/builtin/econ/index.tsx +68 -22
  245. package/src/plugins/builtin/econ-statistics/headless.ts +12 -2
  246. package/src/plugins/builtin/econ-statistics/pane.tsx +1 -1
  247. package/src/plugins/builtin/econ-statistics/view.ts +17 -0
  248. package/src/plugins/builtin/executives/data.ts +17 -6
  249. package/src/plugins/builtin/executives/pane.tsx +16 -7
  250. package/src/plugins/builtin/filing-events/pane.tsx +22 -5
  251. package/src/plugins/builtin/fx-matrix/freshness.ts +65 -0
  252. package/src/plugins/builtin/fx-matrix/index.tsx +22 -1
  253. package/src/plugins/builtin/fx-matrix/pairs.ts +1 -1
  254. package/src/plugins/builtin/fx-matrix/settings.ts +42 -2
  255. package/src/plugins/builtin/gpu/headless.ts +9 -3
  256. package/src/plugins/builtin/gpu/model.ts +14 -5
  257. package/src/plugins/builtin/help/index.tsx +1 -0
  258. package/src/plugins/builtin/holders/beneficial-client.ts +29 -7
  259. package/src/plugins/builtin/holders/beneficial-view.tsx +7 -3
  260. package/src/plugins/builtin/holders/headless.ts +6 -1
  261. package/src/plugins/builtin/holders/pane.tsx +3 -0
  262. package/src/plugins/builtin/insider/index.tsx +4 -4
  263. package/src/plugins/builtin/iv-history/charts.tsx +21 -7
  264. package/src/plugins/builtin/iv-history/client.ts +12 -2
  265. package/src/plugins/builtin/iv-history/format.ts +33 -2
  266. package/src/plugins/builtin/iv-history/headless.ts +22 -10
  267. package/src/plugins/builtin/iv-history/index.tsx +3 -3
  268. package/src/plugins/builtin/iv-history/model.ts +61 -13
  269. package/src/plugins/builtin/iv-history/pane.tsx +11 -3
  270. package/src/plugins/builtin/iv-history/screen-pane.tsx +42 -18
  271. package/src/plugins/builtin/jobs/pane.tsx +1 -1
  272. package/src/plugins/builtin/kelly-sizer/headless.ts +149 -0
  273. package/src/plugins/builtin/kelly-sizer/history.ts +80 -0
  274. package/src/plugins/builtin/kelly-sizer/index.tsx +2 -0
  275. package/src/plugins/builtin/kelly-sizer/pane.tsx +109 -39
  276. package/src/plugins/builtin/kelly-sizer/sections.tsx +14 -0
  277. package/src/plugins/builtin/kelly-sizer/types.ts +9 -1
  278. package/src/plugins/builtin/layout-manager/index.tsx +9 -1
  279. package/src/plugins/builtin/macro-day/client.ts +24 -0
  280. package/src/plugins/builtin/macro-day/headless.ts +11 -8
  281. package/src/plugins/builtin/macro-day/index.tsx +3 -0
  282. package/src/plugins/builtin/macro-day/model.ts +11 -6
  283. package/src/plugins/builtin/macro-day/pane.tsx +12 -8
  284. package/src/plugins/builtin/macro-day/release-table.ts +40 -0
  285. package/src/plugins/builtin/macro-day/releases.ts +62 -44
  286. package/src/plugins/builtin/market-heatmap/data.ts +25 -5
  287. package/src/plugins/builtin/market-heatmap/evidence.ts +90 -0
  288. package/src/plugins/builtin/market-heatmap/index.tsx +144 -63
  289. package/src/plugins/builtin/market-heatmap/model.ts +94 -3
  290. package/src/plugins/builtin/market-heatmap/portfolio.ts +12 -2
  291. package/src/plugins/builtin/market-movers/headless.ts +31 -4
  292. package/src/plugins/builtin/market-movers/index.tsx +8 -1
  293. package/src/plugins/builtin/market-movers/model.ts +71 -17
  294. package/src/plugins/builtin/market-movers/table.ts +50 -23
  295. package/src/plugins/builtin/news/headless.ts +9 -2
  296. package/src/plugins/builtin/news/wire/news/footer.ts +10 -0
  297. package/src/plugins/builtin/openfda/client.ts +1 -1
  298. package/src/plugins/builtin/options/carry.ts +95 -0
  299. package/src/plugins/builtin/options/index.tsx +58 -5
  300. package/src/plugins/builtin/options/strike-window.ts +183 -0
  301. package/src/plugins/builtin/options/table.ts +55 -3
  302. package/src/plugins/builtin/options/types.ts +8 -1
  303. package/src/plugins/builtin/options/view.tsx +161 -12
  304. package/src/plugins/builtin/options-positioning/headless.ts +4 -2
  305. package/src/plugins/builtin/options-scenario/charts.tsx +2 -2
  306. package/src/plugins/builtin/options-scenario/client.ts +73 -17
  307. package/src/plugins/builtin/options-scenario/headless.ts +71 -24
  308. package/src/plugins/builtin/options-scenario/hedge.ts +284 -0
  309. package/src/plugins/builtin/options-scenario/index.tsx +9 -2
  310. package/src/plugins/builtin/options-scenario/model.ts +33 -0
  311. package/src/plugins/builtin/options-scenario/pane.tsx +40 -16
  312. package/src/plugins/builtin/pe-band/model.ts +2 -2
  313. package/src/plugins/builtin/pe-band/report-dates.ts +11 -7
  314. package/src/plugins/builtin/perps/equity-row.tsx +8 -5
  315. package/src/plugins/builtin/perps/model.ts +16 -5
  316. package/src/plugins/builtin/plugin-marketplace/model.ts +64 -21
  317. package/src/plugins/builtin/plugin-marketplace/packs.ts +118 -0
  318. package/src/plugins/builtin/plugin-marketplace/pane.tsx +344 -32
  319. package/src/plugins/builtin/plugin-marketplace/store.ts +7 -1
  320. package/src/plugins/builtin/plugin-module.ts +87 -4
  321. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +71 -36
  322. package/src/plugins/builtin/portfolio-list/cli/render.ts +87 -18
  323. package/src/plugins/builtin/portfolio-list/cli/shared.ts +2 -16
  324. package/src/plugins/builtin/portfolio-list/headless.ts +38 -7
  325. package/src/plugins/builtin/realized-vol/headless.ts +3 -0
  326. package/src/plugins/builtin/realized-vol/pane.tsx +1 -1
  327. package/src/plugins/builtin/research/corporate-actions-pane.tsx +2 -2
  328. package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +1 -1
  329. package/src/plugins/builtin/revenue-breakdown/client.ts +13 -6
  330. package/src/plugins/builtin/revenue-breakdown/headless.ts +4 -1
  331. package/src/plugins/builtin/revenue-breakdown/pane.tsx +7 -4
  332. package/src/plugins/builtin/reverse-dcf/headless.ts +5 -1
  333. package/src/plugins/builtin/reverse-dcf/pane.tsx +1 -1
  334. package/src/plugins/builtin/risk-factors/data.ts +23 -9
  335. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  336. package/src/plugins/builtin/risk-factors/pane.tsx +6 -5
  337. package/src/plugins/builtin/seasonality/pane.tsx +1 -1
  338. package/src/plugins/builtin/sec/index.tsx +5 -5
  339. package/src/plugins/builtin/sectors/client.ts +10 -2
  340. package/src/plugins/builtin/sectors/headless.ts +35 -6
  341. package/src/plugins/builtin/sectors/index.tsx +18 -1
  342. package/src/plugins/builtin/sectors/sector-model.ts +150 -41
  343. package/src/plugins/builtin/shared/command-tab.ts +51 -0
  344. package/src/plugins/builtin/shared/headless-market-data.ts +33 -1
  345. package/src/plugins/builtin/shared/members-menu.tsx +4 -4
  346. package/src/plugins/builtin/shared/report-freshness.ts +50 -2
  347. package/src/plugins/builtin/shared/ticker-request.ts +25 -1
  348. package/src/plugins/builtin/shared/use-quote-board.ts +25 -5
  349. package/src/plugins/builtin/shared/volatility/options.ts +1 -46
  350. package/src/plugins/builtin/short-interest/index.tsx +19 -4
  351. package/src/plugins/builtin/short-interest/surface.tsx +4 -4
  352. package/src/plugins/builtin/short-volume/index.tsx +1 -10
  353. package/src/plugins/builtin/supply-chain/model.ts +1 -1
  354. package/src/plugins/builtin/ticker-detail/headless.ts +43 -21
  355. package/src/plugins/builtin/ticker-detail/pane.tsx +4 -5
  356. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +94 -6
  357. package/src/plugins/builtin/treasury-auctions/cache.ts +5 -2
  358. package/src/plugins/builtin/treasury-auctions/client.ts +22 -8
  359. package/src/plugins/builtin/treasury-auctions/headless.ts +40 -4
  360. package/src/plugins/builtin/treasury-auctions/index.tsx +11 -9
  361. package/src/plugins/builtin/treasury-auctions/model.ts +197 -30
  362. package/src/plugins/builtin/treasury-auctions/pane.tsx +35 -11
  363. package/src/plugins/builtin/treasury-auctions/types.ts +11 -1
  364. package/src/plugins/builtin/vol-surface/charts.tsx +1 -3
  365. package/src/plugins/builtin/vol-surface/client.ts +6 -2
  366. package/src/plugins/builtin/vol-surface/evidence.ts +11 -6
  367. package/src/plugins/builtin/vol-surface/headless.ts +124 -20
  368. package/src/plugins/builtin/vol-surface/model.ts +23 -11
  369. package/src/plugins/builtin/vol-surface/pane.tsx +38 -27
  370. package/src/plugins/builtin/vol-surface/tables.ts +124 -0
  371. package/src/plugins/builtin/world-indices/client.ts +22 -4
  372. package/src/plugins/builtin/world-indices/headless.ts +54 -27
  373. package/src/plugins/builtin/world-indices/model.ts +4 -2
  374. package/src/plugins/builtin/world-indices/table.tsx +13 -2
  375. package/src/plugins/builtin/world-venue-map/geo-series.ts +1 -1
  376. package/src/plugins/builtin/world-venue-map/index.ts +1 -1
  377. package/src/plugins/builtin/yield-curve/index.tsx +1 -1
  378. package/src/plugins/builtin-editorial.ts +126 -0
  379. package/src/plugins/catalog-backend.ts +16 -9
  380. package/src/plugins/catalog-browser.ts +127 -63
  381. package/src/plugins/catalog-manifest.ts +73 -40
  382. package/src/plugins/catalog-ui.ts +26 -6
  383. package/src/plugins/extracted-plugins.ts +7 -1
  384. package/src/plugins/ownership.ts +150 -7
  385. package/src/plugins/plugin-functions.ts +54 -0
  386. package/src/plugins/plugin-off.ts +36 -0
  387. package/src/plugins/registry/host-actions.ts +16 -0
  388. package/src/plugins/registry/index.ts +84 -24
  389. package/src/plugins/registry/plugin-state.ts +25 -1
  390. package/src/plugins/runtime/context.tsx +29 -0
  391. package/src/react/input.ts +48 -1
  392. package/src/remote/app-host.tsx +5 -0
  393. package/src/remote/controller.ts +3 -53
  394. package/src/remote/matching.ts +69 -0
  395. package/src/remote/schema.ts +1 -1
  396. package/src/remote/semantic-tree.tsx +14 -0
  397. package/src/remote/types.ts +2 -0
  398. package/src/renderers/dom/desktop/icons.tsx +14 -0
  399. package/src/renderers/dom/desktop/query-bar.tsx +59 -32
  400. package/src/renderers/dom/host/box.tsx +1 -1
  401. package/src/renderers/dom/host/drag-shield.ts +17 -6
  402. package/src/renderers/dom/host/live-frame.ts +10 -2
  403. package/src/renderers/dom/input-host.tsx +6 -1
  404. package/src/renderers/dom/styles.css +13 -0
  405. package/src/renderers/electrobun/bun/desktop/backend.ts +5 -1
  406. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +12 -1
  407. package/src/renderers/electrobun/bun/desktop/initialization.ts +2 -1
  408. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  409. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +1 -1
  410. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +101 -5
  411. package/src/renderers/electrobun/view/main.tsx +4 -1
  412. package/src/renderers/opentui/input-host.tsx +6 -1
  413. package/src/renderers/opentui/start.tsx +9 -0
  414. package/src/shares/portable-layout.ts +16 -6
  415. package/src/sources/gloomberb-cloud/index.ts +19 -10
  416. package/src/sources/gloomberb-cloud/normalizers.ts +4 -1
  417. package/src/sources/listing-symbols.ts +11 -5
  418. package/src/sources/provider-errors.ts +88 -3
  419. package/src/sources/provider-router/cached-routes.ts +8 -4
  420. package/src/sources/provider-router/financial-routes.ts +11 -5
  421. package/src/sources/provider-router/financials.ts +42 -15
  422. package/src/sources/provider-router/history.ts +27 -11
  423. package/src/sources/provider-router/index.ts +1 -1
  424. package/src/sources/provider-router/primary.ts +23 -5
  425. package/src/sources/sec-edgar.ts +68 -8
  426. package/src/sync/core-contributors.ts +4 -3
  427. package/src/test-support/cli-context.ts +2 -1
  428. package/src/test-support/data-provider.ts +34 -0
  429. package/src/test-support/shell-pane-keys.tsx +1 -0
  430. package/src/theme/series-colors.ts +46 -0
  431. package/src/theme/themes.ts +149 -0
  432. package/src/tickers/search/index.ts +22 -2
  433. package/src/tickers/search/ranking.ts +45 -1
  434. package/src/tickers/search/types.ts +3 -1
  435. package/src/time-series/resolve.ts +1 -14
  436. package/src/types/config.ts +61 -1
  437. package/src/types/headless.ts +11 -0
  438. package/src/types/instrument.ts +8 -0
  439. package/src/types/plugin.ts +49 -3
  440. package/src/ui/host.tsx +8 -4
  441. package/src/ui/index.tsx +2 -0
  442. package/src/utils/cli-output.ts +14 -1
  443. package/src/utils/exchanges.ts +13 -0
  444. package/src/utils/format.ts +2 -1
  445. package/src/utils/fx-market-hours.ts +34 -0
  446. package/src/utils/option-expiry.ts +126 -0
  447. package/src/utils/price-history.ts +39 -6
  448. package/src/utils/rpc-timeout-error.ts +6 -2
  449. package/src/utils/sec-eps-basis.ts +159 -30
  450. package/src/utils/sec.ts +38 -34
  451. package/src/utils/throttled-fetch.ts +51 -22
@@ -1,6 +1,7 @@
1
1
  import type { PricePoint, TickerFinancials } from "../types/financials";
2
2
  import { canonicalExchange, resolveExchangeTimeZone } from "./exchanges";
3
3
  import { isRoundTheClockCoin } from "./crypto-pair";
4
+ import { isCurrencyPairListing, latestFxWeekClose } from "./fx-market-hours";
4
5
  import { hasPublishedSessionCalendar, isRegularSessionTime, isTimestampStaleForExchangeSession, latestRegularSessionClose, latestRegularSessionOpen, sessionCalendarTimeZone } from "../market-data/market/freshness";
5
6
  import { zonedDateTimeParts } from "./zoned-date-time";
6
7
  import { regularHistorySessionStaleness } from "../market-data/history-session";
@@ -29,6 +30,16 @@ interface PriceHistoryFreshnessOptions {
29
30
  session?: HistorySession;
30
31
  }
31
32
 
33
+ /**
34
+ * How old the newest bar of a current copy may be. Completed bars are
35
+ * timestamped at their opening time, so before the next one is delivered the
36
+ * newest may be almost two intervals plus the feed delay old; finer bars keep
37
+ * a floor.
38
+ */
39
+ function allowedBarLagMs(intervalMs: number | null): number {
40
+ return Math.max(MAX_SAME_SESSION_HISTORY_LAG_MS, intervalMs != null ? 2 * intervalMs + DELAYED_HISTORY_ALLOWANCE_MS : 0);
41
+ }
42
+
32
43
  export function priceHistoryIntervalMs(interval: string): number | null {
33
44
  const parsed = parseBarInterval(interval);
34
45
  return parsed ? parsed.count * BAR_INTERVAL_UNIT_MS[parsed.unit] : null;
@@ -204,11 +215,7 @@ export function isPriceHistoryStaleForCurrentWindow(
204
215
  if (regularStale !== null) return regularStale;
205
216
  }
206
217
  const age = now - latestTime;
207
- // Completed bars are timestamped at their opening time. Before the next
208
- // completed bar is delivered, the newest bar may be almost two intervals
209
- // plus the feed delay old. Keep the existing tolerance for finer bars.
210
- const allowedLag = Math.max(MAX_SAME_SESSION_HISTORY_LAG_MS,
211
- intervalMs != null ? 2 * intervalMs + DELAYED_HISTORY_ALLOWANCE_MS : 0);
218
+ const allowedLag = allowedBarLagMs(intervalMs);
212
219
  if (age <= allowedLag) return false;
213
220
  const exchange = options.exchange || "NASDAQ";
214
221
  // Bars that reach the close of the venue's latest session stay current
@@ -310,6 +317,31 @@ function isBarBeforeSession(latestTime: number, session: string, intervalMs: num
310
317
  return dayNumber(session) >= dayNumber(bar) + Math.max(1, Math.round(intervalMs / DAY_MS));
311
318
  }
312
319
 
320
+ /**
321
+ * An intraday copy fetched before the venue's latest session settled holds
322
+ * that session only as far as the fetch. Once the session is over, or the
323
+ * next has begun, a copy whose last bar stops short of the close by more than
324
+ * a current copy's bars may lag is refetched. A copy fetched after the
325
+ * session settled holds all of it, however early its last bar: a thin listing
326
+ * need not trade late. A currency pair's session is the FX week, which closes
327
+ * Friday 17:00 New York. Coins never close, and within the FX week neither do
328
+ * pairs: isPriceHistoryStaleForCurrentWindow holds those copies to the present.
329
+ */
330
+ function intradayHistoryFetchState(
331
+ points: PricePoint[], fetchedAt: number, now: number, intervalMs: number, options: CalendarHistoryFetchOptions,
332
+ ): CalendarHistoryFetchState {
333
+ if (isRoundTheClockCoin(options.symbol, options.exchange)) return "current";
334
+ const settledBy = now - SESSION_BAR_SETTLE_MS;
335
+ // Bare symbols resolve to their US listing at the sources.
336
+ const close = isCurrencyPairListing(options.symbol, options.exchange) ? latestFxWeekClose(settledBy)
337
+ : latestRegularSessionClose(canonicalExchange(options.exchange) || "NYSE", settledBy)?.close ?? null;
338
+ if (close === null || fetchedAt >= close + SESSION_BAR_SETTLE_MS) return "current";
339
+ const latest = points.findLast(hasFiniteClose);
340
+ const latestTime = latest ? getPricePointTimestamp(latest) : Number.NaN;
341
+ if (!Number.isFinite(latestTime) || latestTime >= close - allowedBarLagMs(intervalMs)) return "current";
342
+ return unsettledState(close + SESSION_BAR_SETTLE_MS, options.checkedAt, now);
343
+ }
344
+
313
345
  /**
314
346
  * A daily or coarser series changes at every close. A copy fetched before the
315
347
  * latest settled close is outdated whatever its cache TTL. A copy fetched
@@ -327,7 +359,8 @@ export function calendarHistoryFetchState(
327
359
  if (!Number.isFinite(fetchedAt) || !Number.isFinite(now) || fetchedAt >= now) return "current";
328
360
  const normalized = normalizePriceHistory(points);
329
361
  const intervalMs = options.intervalMs ?? inferredHistoryIntervalMs(normalized);
330
- if (intervalMs == null || intervalMs < DAY_MS) return "current";
362
+ if (intervalMs == null) return "current";
363
+ if (intervalMs < DAY_MS) return intradayHistoryFetchState(normalized, fetchedAt, now, intervalMs, options);
331
364
  const exchange = canonicalExchange(options.exchange);
332
365
  if (isRoundTheClockCoin(options.symbol, options.exchange)) {
333
366
  return now - fetchedAt > CRYPTO_BAR_MAX_AGE_MS
@@ -1,5 +1,9 @@
1
- /** A timeout this many times past its limit means the machine slept through it. */
2
- const SLEEP_TIMEOUT_FACTOR = 5;
1
+ /**
2
+ * A timeout this many times past its limit means the page was suspended, not
3
+ * that the Bun process stalled. A page that is running fires the timer within
4
+ * a second or two of its limit, and a hidden one within a minute.
5
+ */
6
+ const SLEEP_TIMEOUT_FACTOR = 2;
3
7
 
4
8
  /**
5
9
  * A call to the desktop's Bun process that did not complete in time: the
@@ -23,6 +23,8 @@ export interface SecEpsBasis {
23
23
  type Fact = { start?: string; end?: string; val?: number; accn?: string; filed?: string; form?: string };
24
24
  type Row = Fact & { start: string; end: string; val: number; accn: string; filed: string };
25
25
  type Edge = { node: string; factor: number; evidence?: SecEpsSplitEvidence };
26
+ type Proof = { split: Fact; before: Row; after: Row; evidence: SecEpsSplitEvidence };
27
+ type SplitEvent = { ratio: number; proofs: Proof[]; consistent: boolean; evidence: SecEpsSplitEvidence; node: string };
26
28
  const date = (value: unknown): value is string => typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value)
27
29
  && Number.isFinite(Date.parse(`${value}T00:00:00Z`)) && new Date(`${value}T00:00:00Z`).toISOString().slice(0, 10) === value;
28
30
  const record = (value: unknown): Record<string, unknown> => value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
@@ -47,27 +49,83 @@ function uniqueObservations(rows: Fact[]): Map<string, Row> {
47
49
  return result;
48
50
  }
49
51
 
52
+ /** When the last restated filing and the first restating one were filed: the split falls between. */
53
+ function cut(proofs: Proof[]): { lastRestated: string; firstRestating: string } {
54
+ return {
55
+ lastRestated: proofs.reduce((max, proof) => proof.before.filed > max ? proof.before.filed : max, ""),
56
+ firstRestating: proofs.reduce((min, proof) => proof.after.filed < min ? proof.after.filed : min, "9999"),
57
+ };
58
+ }
59
+ const separable = (proofs: Proof[]) => cut(proofs).lastRestated < cut(proofs).firstRestating;
60
+
61
+ /**
62
+ * One split, however many filings repeat it and under whatever context dates
63
+ * (an approval date, the effective date, each period of the equity statement).
64
+ * Rows with one ratio and one context date are one disclosure. A split divides
65
+ * the filings once, so disclosures with one ratio are one split when a single
66
+ * filing date still separates every filing they restate from every filing that
67
+ * restates; two splits with one ratio leave no such date (the later split
68
+ * restates filings that already carry the earlier one).
69
+ */
70
+ function splitEvents(proofs: Proof[]): SplitEvent[] {
71
+ const disclosures = new Map<string, Proof[]>();
72
+ for (const proof of proofs) {
73
+ const key = `${proof.split.val}:${proof.split.end}`;
74
+ disclosures.set(key, [...disclosures.get(key) ?? [], proof]);
75
+ }
76
+ const groups: Proof[][] = [];
77
+ for (const ratio of new Set(proofs.map((proof) => proof.split.val!))) {
78
+ const sorted = [...disclosures.values()].filter((group) => group[0]!.split.val === ratio)
79
+ .sort((a, b) => cut(a).firstRestating.localeCompare(cut(b).firstRestating) || cut(a).lastRestated.localeCompare(cut(b).lastRestated));
80
+ let current: Proof[] = [];
81
+ for (const disclosure of sorted) {
82
+ // A disclosure whose own filings admit no such date proves nothing.
83
+ if (!separable(disclosure)) groups.push(disclosure);
84
+ else if (current.length && separable([...current, ...disclosure])) current.push(...disclosure);
85
+ else {
86
+ current = [...disclosure];
87
+ groups.push(current);
88
+ }
89
+ }
90
+ }
91
+ return groups.map((group, index) => {
92
+ const first = [...group].sort((a, b) => a.after.filed.localeCompare(b.after.filed) || a.after.accn.localeCompare(b.after.accn)
93
+ || a.split.end!.localeCompare(b.split.end!) || a.before.filed.localeCompare(b.before.filed) || a.before.end.localeCompare(b.before.end))[0]!;
94
+ return { ratio: group[0]!.split.val!, proofs: group, consistent: separable(group), evidence: first.evidence, node: `split:${index}` };
95
+ });
96
+ }
97
+
50
98
  /**
51
99
  * Share bases belong to source accessions, not filing dates. A split edge needs
52
100
  * an explicit SEC split fact and unchanged same-period income with inverse EPS
53
- * restatement. Repeated EPS AND diluted shares connect otherwise equal bases.
101
+ * restatement. Repeated EPS AND diluted shares connect otherwise equal bases;
102
+ * where a filing tags no diluted shares, repeated EPS AND income do, if no
103
+ * disclosed split could leave that EPS unchanged at cent rounding.
54
104
  * No price jump, ticker exception, or chronological assumption establishes one.
105
+ *
106
+ * Every filing that states a split and restates an earlier filing by its ratio
107
+ * is evidence of that split. The filings it restated share one basis and the
108
+ * restating filings share the next, so a split filed in several filings under
109
+ * several context dates is one factor, not a run of unproved disclosures.
55
110
  */
56
111
  export function createSecEpsBasisResolver(payload: unknown): (fact: Fact) => { value?: number; basis?: SecEpsBasis; availableAt?: string } {
57
112
  const splits = entries(payload, "StockholdersEquityNoteStockSplitConversionRatio1", "pure")
58
113
  .filter((row) => row.val! > 0 && row.val !== 1)
59
114
  .sort((a, b) => a.filed!.localeCompare(b.filed!));
60
115
  if (splits.length === 0) return (fact) => ({ value: fact.val, availableAt: fact.filed });
61
- const eventRows = new Map<string, Fact>();
62
- const conflicts = new Set<string>();
116
+ // A filing that gives one split context two ratios contradicts itself.
117
+ const stated = new Map<string, number>();
118
+ const contradicted = new Set<string>();
63
119
  for (const split of splits) {
64
- if (eventRows.has(split.end!) && eventRows.get(split.end!)!.val !== split.val) conflicts.add(split.end!);
65
- if (!eventRows.has(split.end!)) eventRows.set(split.end!, split);
120
+ const context = `${split.accn}:${split.end}`;
121
+ if (stated.has(context) && stated.get(context) !== split.val) contradicted.add(context);
122
+ stated.set(context, split.val!);
66
123
  }
67
- const latest = [...eventRows.values()].sort((a, b) => a.end!.localeCompare(b.end!)).at(-1)!;
124
+ const latest = [...splits].sort((a, b) => b.end!.localeCompare(a.end!))[0]!;
68
125
  const eps = uniqueObservations(entries(payload, "EarningsPerShareDiluted", "USD/shares"));
69
126
  const shares = uniqueObservations(entries(payload, "WeightedAverageNumberOfDilutedSharesOutstanding", "shares"));
70
127
  const income = uniqueObservations(entries(payload, "NetIncomeLoss", "USD"));
128
+ const key = (row: Fact) => `${row.accn}:${row.start}:${row.end}`;
71
129
  const graph = new Map<string, Edge[]>();
72
130
  const connect = (before: string, after: string, ratio: number, evidence?: SecEpsSplitEvidence) => {
73
131
  graph.set(after, [...graph.get(after) ?? [], { node: before, factor: ratio, evidence }]);
@@ -75,44 +133,116 @@ export function createSecEpsBasisResolver(payload: unknown): (fact: Fact) => { v
75
133
  };
76
134
  const identical = new Map<string, Row>();
77
135
  const periods = new Map<string, Row[]>();
78
- for (const [key, row] of eps) {
136
+ for (const row of eps.values()) {
79
137
  const period = `${row.start}:${row.end}`;
80
138
  periods.set(period, [...periods.get(period) ?? [], row]);
81
- const denominator = shares.get(key)?.val;
139
+ const denominator = shares.get(key(row))?.val;
82
140
  if (!row.val || !denominator || denominator <= 0) continue;
83
141
  const identity = `${period}:${row.val}:${denominator}`;
84
142
  const prior = identical.get(identity);
85
143
  if (prior && prior.accn !== row.accn) connect(prior.accn, row.accn, 1);
86
144
  else identical.set(identity, row);
87
145
  }
88
- const proved = new Map<string, SecEpsSplitEvidence>();
89
- for (const split of eventRows.values()) {
90
- if (conflicts.has(split.end!)) continue;
146
+ // Not every filer tags diluted shares (Alphabet before 2024). Where a filing
147
+ // tags none for the period, a repeated EPS and net income connect instead,
148
+ // when no disclosed split ratio could leave that EPS unchanged at cent
149
+ // rounding. Diluted shares that differ still keep the bases apart.
150
+ const ratios = [...new Set(splits.map((split) => split.val!))];
151
+ const telling = (value: number) => ratios.every((ratio) => Math.abs(value - value / ratio) > .005 + .005 / ratio
152
+ && Math.abs(value - value * ratio) > .005 + .005 * ratio);
153
+ const tagged = new Set(entries(payload, "WeightedAverageNumberOfDilutedSharesOutstanding", "shares").map(key));
154
+ for (const rows of periods.values()) {
155
+ for (let i = 0; i < rows.length; i++) {
156
+ for (let j = i + 1; j < rows.length; j++) {
157
+ const [a, b] = [rows[i]!, rows[j]!];
158
+ if (a.accn === b.accn || a.val !== b.val || !a.val || !telling(a.val)) continue;
159
+ if (tagged.has(key(a)) && tagged.has(key(b))) continue;
160
+ const repeated = income.get(key(a))?.val;
161
+ if (repeated !== undefined && repeated !== 0 && repeated === income.get(key(b))?.val) connect(a.accn, b.accn, 1);
162
+ }
163
+ }
164
+ }
165
+ // A later filing restates an earlier one's EPS for the same period by the
166
+ // ratio, with income unchanged.
167
+ const restates = (before: Row, after: Row, ratio: number) => {
168
+ if (before.accn === after.accn || before.filed >= after.filed || before.val === after.val || before.val * after.val <= 0) return false;
169
+ const beforeIncome = income.get(key(before))?.val;
170
+ const afterIncome = income.get(key(after))?.val;
171
+ if (beforeIncome === undefined || beforeIncome === 0 || beforeIncome !== afterIncome) return false;
172
+ // EPS reports rounded to cents have overlapping rounding intervals.
173
+ // This accommodates 11.91 / 4 -> 2.98 without inventing precision.
174
+ const tolerance = .005 + .005 / ratio;
175
+ return Math.abs(before.val / ratio - after.val) <= tolerance + 1e-12 && Math.abs(before.val - after.val) > tolerance * 2;
176
+ };
177
+ const proofs: Proof[] = [];
178
+ for (const split of splits) {
179
+ if (contradicted.has(`${split.accn}:${split.end}`)) continue;
91
180
  for (const after of eps.values()) {
92
181
  if (after.accn !== split.accn || after.filed !== split.filed || after.end >= split.end! || after.val === 0) continue;
93
182
  for (const before of periods.get(`${after.start}:${after.end}`) ?? []) {
94
- if (before.accn === after.accn || before.filed >= after.filed || before.val === after.val || before.val * after.val <= 0) continue;
95
- const beforeIncome = income.get(`${before.accn}:${before.start}:${before.end}`)?.val;
96
- const afterIncome = income.get(`${after.accn}:${after.start}:${after.end}`)?.val;
97
- if (beforeIncome === undefined || beforeIncome === 0 || beforeIncome !== afterIncome) continue;
98
- // EPS reports rounded to cents have overlapping rounding intervals.
99
- // This accommodates 11.91 / 4 -> 2.98 without inventing precision.
100
- const tolerance = .005 + .005 / split.val!;
101
- if (Math.abs(before.val / split.val! - after.val) > tolerance + 1e-12
102
- || Math.abs(before.val - after.val) <= tolerance * 2) continue;
103
- const evidence: SecEpsSplitEvidence = {
183
+ if (!restates(before, after, split.val!)) continue;
184
+ proofs.push({ split, before, after, evidence: {
104
185
  date: split.end!, ratio: split.val!, accessionNumber: split.accn!, filed: split.filed!,
105
186
  comparativePeriodEnd: before.end, beforeAccessionNumber: before.accn, beforeEps: before.val, afterEps: after.val,
106
- };
107
- connect(before.accn, after.accn, split.val!, evidence);
108
- proved.set(split.end!, evidence);
187
+ } });
109
188
  }
110
189
  }
111
190
  }
191
+ const events = splitEvents(proofs);
192
+ const consistent = events.filter((event) => event.consistent);
193
+ // The filings a split restated share one basis and its restating filings the
194
+ // next. Evidence that mixes splits only connects the two filings it compares.
195
+ const eventOf = new Map<Fact, SplitEvent>();
196
+ for (const event of events) {
197
+ if (!event.consistent) {
198
+ for (const proof of event.proofs) connect(proof.before.accn, proof.after.accn, event.ratio, proof.evidence);
199
+ continue;
200
+ }
201
+ connect(`${event.node}:before`, `${event.node}:after`, event.ratio, event.evidence);
202
+ for (const proof of event.proofs) {
203
+ connect(proof.before.accn, `${event.node}:before`, 1);
204
+ connect(`${event.node}:after`, proof.after.accn, 1);
205
+ eventOf.set(proof.split, event);
206
+ }
207
+ }
208
+ // A filing that repeats a proved split's restatement without stating its
209
+ // ratio (a 10-K after the split's 10-Qs) restates on that split when exactly
210
+ // one split fits: the restated filing before the split's first restating
211
+ // filing, the restating one after its last restated filing, and a period
212
+ // before the split's context dates.
213
+ const known = new Set([...proofs.flatMap((proof) => [proof.before.accn, proof.after.accn]),
214
+ ...splits.filter((split) => contradicted.has(`${split.accn}:${split.end}`)).map((split) => split.accn!)]);
215
+ for (const after of eps.values()) {
216
+ if (known.has(after.accn) || after.val === 0) continue;
217
+ for (const before of periods.get(`${after.start}:${after.end}`) ?? []) {
218
+ const fits = consistent.filter((event) => restates(before, after, event.ratio)
219
+ && before.filed < cut(event.proofs).firstRestating && after.filed > cut(event.proofs).lastRestated
220
+ && event.proofs.some((proof) => after.end < proof.split.end!));
221
+ if (fits.length !== 1) continue;
222
+ connect(before.accn, `${fits[0]!.node}:before`, 1);
223
+ connect(`${fits[0]!.node}:after`, after.accn, 1);
224
+ }
225
+ }
226
+ // A row that proves nothing itself repeats a proved split: the same ratio at
227
+ // the context date of a row that proves it, or else stated in one of that
228
+ // split's filings. Only an unambiguous match counts.
229
+ const proving = new Set(proofs.map((proof) => proof.split));
230
+ for (const split of splits) {
231
+ if (proving.has(split) || contradicted.has(`${split.accn}:${split.end}`)) continue;
232
+ const candidates = consistent.filter((event) => event.ratio === split.val);
233
+ const dated = candidates.filter((event) => event.proofs.some((proof) => proof.split.end === split.end));
234
+ const filed = candidates.filter((event) => event.proofs.some((proof) => proof.before.accn === split.accn || proof.after.accn === split.accn));
235
+ const match = dated.length ? dated : filed;
236
+ if (match.length === 1) eventOf.set(split, match[0]!);
237
+ }
238
+ // The latest disclosed split sets the current basis. Its filings must also be
239
+ // the latest restating filings, or the disclosures and the filings disagree.
240
+ const target = eventOf.get(latest);
241
+ let inconsistent = !!target && events.some((event) => event.consistent && event !== target
242
+ && cut(event.proofs).firstRestating >= cut(target.proofs).firstRestating);
112
243
  const paths = new Map<string, { factor: number; evidence: SecEpsSplitEvidence[] }>();
113
- const queue = [latest.accn!];
114
- paths.set(latest.accn!, { factor: 1, evidence: [] });
115
- let inconsistent = false;
244
+ const queue = target ? [`${target.node}:after`] : [];
245
+ if (target) paths.set(queue[0]!, { factor: 1, evidence: [] });
116
246
  for (let i = 0; i < queue.length; i++) {
117
247
  const node = queue[i]!;
118
248
  const path = paths.get(node)!;
@@ -131,8 +261,7 @@ export function createSecEpsBasisResolver(payload: unknown): (fact: Fact) => { v
131
261
  return (fact) => {
132
262
  if (!fact.end || fact.end >= latest.end!) return { value: fact.val, availableAt: fact.filed };
133
263
  const path = fact.accn ? paths.get(fact.accn) : undefined;
134
- const target = proved.get(latest.end!);
135
- const evidence = [...new Map([...(path?.evidence ?? []), target ?? {
264
+ const evidence = [...new Map([...(path?.evidence ?? []), target?.evidence ?? {
136
265
  date: latest.end!, ratio: latest.val!, accessionNumber: latest.accn!, filed: latest.filed!,
137
266
  }].map((item) => [item.date, item])).values()].sort((a, b) => a.date.localeCompare(b.date));
138
267
  const basis: SecEpsBasis = {
@@ -140,7 +269,7 @@ export function createSecEpsBasisResolver(payload: unknown): (fact: Fact) => { v
140
269
  basisDate: latest.end!, evidence,
141
270
  };
142
271
  // Any unproved intervening disclosure can represent an additional share basis.
143
- const unproved = [...eventRows.values()].some((event) => event.end! > fact.end! && !proved.has(event.end!));
272
+ const unproved = splits.some((split) => split.end! > fact.end! && !eventOf.has(split));
144
273
  if (!path || !target || inconsistent || unproved || !date(fact.filed)) return { basis };
145
274
  const value = fact.val! / path.factor;
146
275
  if (!Number.isFinite(value)) return { basis };
package/src/utils/sec.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { TickerRecord } from "../types/ticker";
2
+ import { tickerHasListingSuffix } from "../sources/listing-symbols";
2
3
  import { canonicalExchange, parsePublicTickerKey, US_LISTING_EXCHANGES } from "./exchanges";
3
4
 
4
5
  /** Canonical US equity venues: the listing exchanges plus other lit and OTC venues. */
@@ -39,40 +40,36 @@ function isFundType(value?: string): boolean {
39
40
  return FUND_TYPES.has(normalize(value).replace(/[\s_-]/g, ""));
40
41
  }
41
42
 
42
- export function isUsEquityTicker(ticker: TickerRecord | null | undefined): boolean {
43
- return isUsListingOfType(ticker, isEquityType);
43
+ /**
44
+ * Whether an equity SEC pane (filings, insider transactions) can show the
45
+ * ticker: an equity, or a type not filled in yet, that is not known to be
46
+ * listed outside the US. A ticker whose venue and currency were never filled
47
+ * in, as when its quote is unavailable, is looked up as a US ticker, as the
48
+ * SEC issuer lookup does with a symbol that names no venue.
49
+ */
50
+ export function mayBeUsEquityTicker(ticker: TickerRecord | null | undefined): boolean {
51
+ return mayBeUsListingOfType(ticker, isEquityType);
44
52
  }
45
53
 
46
- /** A US equity, or a US-listed fund, which files fund forms with the SEC. */
47
- export function isUsEquityOrFundTicker(ticker: TickerRecord | null | undefined): boolean {
48
- return isUsListingOfType(ticker, (type) => isEquityType(type) || isFundType(type));
54
+ /** The same for a view that also lists a US-listed fund's filings, which funds file with the SEC. */
55
+ export function mayBeUsEquityOrFundTicker(ticker: TickerRecord | null | undefined): boolean {
56
+ return mayBeUsListingOfType(ticker, (type) => isEquityType(type) || isFundType(type));
49
57
  }
50
58
 
51
- function isUsListingOfType(
59
+ function mayBeUsListingOfType(
52
60
  ticker: TickerRecord | null | undefined,
53
61
  acceptsType: (type?: string) => boolean,
54
62
  ): boolean {
55
63
  if (!ticker) return false;
56
-
57
64
  const primaryContract = ticker.metadata.broker_contracts?.[0];
58
- const type = primaryContract?.secType ?? ticker.metadata.assetCategory;
59
- const currency = normalize(primaryContract?.currency ?? ticker.metadata.currency);
60
- const exchangeCandidates = [
61
- primaryContract?.primaryExchange,
62
- primaryContract?.exchange,
63
- ticker.metadata.exchange,
64
- ];
65
-
66
- // A listing saved without a known currency counts by its venue.
67
- return acceptsType(type)
68
- && (currency === "USD" || !currency)
69
- && exchangeCandidates.some((exchange) => isUsExchange(exchange));
65
+ return acceptsType(primaryContract?.secType ?? ticker.metadata.assetCategory) && !isKnownNonUsListing(ticker);
70
66
  }
71
67
 
72
68
  /**
73
- * The metadata places the listing outside the US: a non-USD currency, or
74
- * venues none of which is a US exchange. A missing currency or venue is
75
- * unknown rather than foreign. SEC filings, FINRA short interest, 13F and
69
+ * The metadata places the listing outside the US: a non-USD currency, venues
70
+ * none of which is a US exchange, or, with no venue, a symbol that names one
71
+ * abroad (SAN:EPA, VOD.L, 7203.T). A missing currency or venue is unknown
72
+ * rather than foreign. SEC filings, FINRA short interest, 13F and
76
73
  * congressional disclosures only cover US listings.
77
74
  */
78
75
  export function isKnownNonUsListing(ticker: TickerRecord | null | undefined): boolean {
@@ -83,23 +80,20 @@ export function isKnownNonUsListing(ticker: TickerRecord | null | undefined): bo
83
80
  const venues = [primaryContract?.primaryExchange, primaryContract?.exchange, ticker.metadata.exchange]
84
81
  .map(normalize)
85
82
  .filter((exchange) => exchange.length > 0 && !ROUTING_EXCHANGES.has(exchange));
86
- return venues.length > 0 && !venues.some((exchange) => isUsExchange(exchange));
83
+ if (venues.length > 0) return !venues.some((exchange) => isUsExchange(exchange));
84
+ const symbol = parsePublicTickerKey(ticker.metadata.ticker ?? "");
85
+ if (symbol.exchange) return !ROUTING_EXCHANGES.has(symbol.exchange) && !isUsExchange(symbol.exchange);
86
+ return tickerHasListingSuffix(symbol.symbol);
87
87
  }
88
88
 
89
89
  /**
90
- * The metadata shows the ticker is not a US equity: a non-USD currency, a
91
- * non-equity type, or only non-US exchanges. A USD ticker with no exchange is
92
- * unknown rather than foreign, e.g. an unsaved symbol from a quote without a
93
- * listing exchange.
90
+ * The metadata shows the ticker is not a US equity: a non-equity type or a
91
+ * listing known to be outside the US. A ticker with no venue or currency is
92
+ * unknown rather than foreign, e.g. an unsaved symbol whose quote is
93
+ * unavailable.
94
94
  */
95
95
  export function isKnownNonUsEquityTicker(ticker: TickerRecord | null | undefined): boolean {
96
- if (!ticker || isUsEquityTicker(ticker)) return false;
97
- const primaryContract = ticker.metadata.broker_contracts?.[0];
98
- const currency = normalize(primaryContract?.currency ?? ticker.metadata.currency);
99
- if (currency && currency !== "USD") return true;
100
- if (!isEquityType(primaryContract?.secType ?? ticker.metadata.assetCategory)) return true;
101
- return [primaryContract?.primaryExchange, primaryContract?.exchange, ticker.metadata.exchange]
102
- .some((exchange) => normalize(exchange).length > 0);
96
+ return !!ticker && !mayBeUsEquityTicker(ticker);
103
97
  }
104
98
 
105
99
  /**
@@ -114,6 +108,16 @@ export function nonUsSecListingVenue(ticker: string, exchange?: string): string
114
108
  return venue;
115
109
  }
116
110
 
111
+ /**
112
+ * The key an issuer read is cached and deduplicated under: the ticker as given
113
+ * for a US listing or a symbol with no venue, SYMBOL:VENUE for a listing
114
+ * elsewhere, so SAN in Paris and SAN in New York never share an entry.
115
+ */
116
+ export function secListingKey(ticker: string, exchange?: string): string {
117
+ const venue = nonUsSecListingVenue(ticker, exchange);
118
+ return venue ? `${parsePublicTickerKey(ticker).symbol}:${venue}` : normalize(ticker);
119
+ }
120
+
117
121
  /**
118
122
  * The item codes in EDGAR's `items` field ("2.02,9.01"). EDGAR fills the same
119
123
  * field with other values for some forms, such as the order dates of a CT
@@ -7,6 +7,7 @@
7
7
  * const data = await client.fetch("https://api.example.com/data");
8
8
  */
9
9
 
10
+ import { abortError, signalWithTimeout } from "./async-deadline";
10
11
  import { httpFetch } from "./http-transport";
11
12
 
12
13
  const DEFAULT_REQUESTS_PER_MINUTE = 30;
@@ -17,9 +18,9 @@ const BACKOFF_BASE_MS = 1_000;
17
18
  export interface ThrottledFetchOptions {
18
19
  /** Max requests per minute per host. Default: 30 */
19
20
  requestsPerMinute?: number;
20
- /** Max retries on 429 or 5xx. Default: 2 */
21
+ /** Max retries on 429, 5xx, timeouts and network errors. Default: 2 */
21
22
  maxRetries?: number;
22
- /** Request timeout in ms. Default: 10000 */
23
+ /** Timeout of each attempt in ms, also when the caller passes a signal. Default: 10000 */
23
24
  timeoutMs?: number;
24
25
  /** Initial retry backoff in ms. Default: 1000 */
25
26
  backoffBaseMs?: number;
@@ -103,33 +104,63 @@ export function createThrottledFetch(
103
104
  return Math.max(retryAfterMs, backoff);
104
105
  }
105
106
 
106
- function isRetryableFetchError(
107
- error: unknown,
108
- retryAbortError: boolean,
109
- ): boolean {
107
+ // Retry rule: a timeout or network error is retried while retries remain,
108
+ // unless the caller's own signal has aborted. Once the caller aborted, the
109
+ // request ends with their abort reason: no further attempt and no backoff,
110
+ // including while waiting on the rate limit or a backoff. An AbortError that
111
+ // is not the caller's can only come from the attempt's timeout (or the
112
+ // transport giving up), so it counts as a timeout.
113
+ function isRetryableFetchError(error: unknown): boolean {
110
114
  const detail =
111
115
  error instanceof Error
112
116
  ? `${error.name} ${error.message} ${String((error as { code?: unknown }).code ?? "")}`
113
117
  : String(error);
114
- return (
115
- /TimeoutError|ECONNRESET|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|socket connection|socket hang up|fetch failed|network connection|connection closed/i.test(
116
- detail,
117
- ) ||
118
- (retryAbortError && /AbortError/i.test(detail))
118
+ return /TimeoutError|AbortError|ECONNRESET|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|socket connection|socket hang up|fetch failed|network connection|connection closed/i.test(
119
+ detail,
119
120
  );
120
121
  }
121
122
 
123
+ function abortReason(signal: AbortSignal): unknown {
124
+ return signal.reason ?? abortError("The operation was aborted.");
125
+ }
126
+
127
+ function throwIfAborted(signal: AbortSignal | null | undefined): void {
128
+ if (signal?.aborted) throw abortReason(signal);
129
+ }
130
+
131
+ /** Waits `ms`, or rejects with the caller's abort reason as soon as the signal fires. */
132
+ function wait(ms: number, signal: AbortSignal | null | undefined): Promise<void> {
133
+ return new Promise<void>((resolve, reject) => {
134
+ if (signal?.aborted) {
135
+ reject(abortReason(signal));
136
+ return;
137
+ }
138
+ const onAbort = () => {
139
+ clearTimeout(timer);
140
+ reject(abortReason(signal!));
141
+ };
142
+ const timer = setTimeout(() => {
143
+ signal?.removeEventListener("abort", onAbort);
144
+ resolve();
145
+ }, ms);
146
+ signal?.addEventListener("abort", onAbort, { once: true });
147
+ });
148
+ }
149
+
122
150
  async function executeRequest(
123
151
  url: string,
124
152
  init: RequestInit | undefined,
125
153
  retriesLeft: number,
126
154
  ): Promise<Response> {
155
+ const callerSignal = init?.signal;
156
+ throwIfAborted(callerSignal);
157
+
127
158
  const host = getHost(url);
128
159
 
129
160
  // Wait for rate limit window
130
161
  const delay = getDelayMs(host);
131
162
  if (delay > 0) {
132
- await new Promise((resolve) => setTimeout(resolve, delay));
163
+ await wait(delay, callerSignal);
133
164
  }
134
165
 
135
166
  recordRequest(host);
@@ -140,17 +171,16 @@ export function createThrottledFetch(
140
171
  ...defaultHeaders,
141
172
  ...((init?.headers as Record<string, string>) ?? {}),
142
173
  },
143
- signal: init?.signal ?? AbortSignal.timeout(timeoutMs),
174
+ signal: signalWithTimeout(callerSignal ?? undefined, timeoutMs),
144
175
  };
145
176
 
146
177
  let resp: Response;
147
178
  try {
148
179
  resp = await fetchTransport(url, mergedInit);
149
180
  } catch (error) {
150
- if (retriesLeft > 0 && isRetryableFetchError(error, !init?.signal)) {
151
- await new Promise((resolve) =>
152
- setTimeout(resolve, getBackoffMs(retriesLeft)),
153
- );
181
+ throwIfAborted(callerSignal);
182
+ if (retriesLeft > 0 && isRetryableFetchError(error)) {
183
+ await wait(getBackoffMs(retriesLeft), callerSignal);
154
184
  return executeRequest(url, init, retriesLeft - 1);
155
185
  }
156
186
  throw error;
@@ -158,11 +188,10 @@ export function createThrottledFetch(
158
188
 
159
189
  // Retry on 429 or 5xx
160
190
  if ((resp.status === 429 || resp.status >= 500) && retriesLeft > 0) {
161
- await new Promise((resolve) =>
162
- setTimeout(
163
- resolve,
164
- getBackoffMs(retriesLeft, resp.headers.get("retry-after")),
165
- ),
191
+ throwIfAborted(callerSignal);
192
+ await wait(
193
+ getBackoffMs(retriesLeft, resp.headers.get("retry-after")),
194
+ callerSignal,
166
195
  );
167
196
  return executeRequest(url, init, retriesLeft - 1);
168
197
  }