gloomberb 0.16.4 → 0.16.5

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 (554) hide show
  1. package/README.md +14 -91
  2. package/package.json +4 -1
  3. package/src/{plugins/builtin/cloud → account}/auth-form.tsx +9 -9
  4. package/src/{plugins/builtin/cloud → account}/auth-model.ts +4 -4
  5. package/src/account/billing.ts +57 -0
  6. package/src/{plugins/builtin/cloud → account}/device-signin-dialog.tsx +18 -12
  7. package/src/{plugins/builtin/cloud → account}/device-signin.ts +2 -2
  8. package/src/{plugins/builtin/cloud → account}/email-auth-attempt.ts +2 -2
  9. package/src/account/session.ts +16 -0
  10. package/src/{plugins/builtin/cloud → account}/upgrade-dialog.tsx +16 -15
  11. package/src/{plugins/builtin/cloud → account}/upgrade-personalization.ts +7 -7
  12. package/src/{plugins/builtin/cloud → account}/upgrade-tickers.ts +6 -6
  13. package/src/{plugins/builtin/cloud → account}/verification-panel.tsx +6 -6
  14. package/src/api-client/data.ts +24 -2
  15. package/src/api-client/earnings.ts +6 -0
  16. package/src/api-client/futures-curve.ts +56 -0
  17. package/src/api-client/index.ts +4 -1
  18. package/src/api-client/market-discovery.ts +10 -1
  19. package/src/api-client/paths.ts +7 -0
  20. package/src/api-client/research-activity.ts +15 -10
  21. package/src/api-client/types.ts +17 -1
  22. package/src/api-client/yield-curves.ts +8 -0
  23. package/src/app/global-shortcuts.ts +15 -1
  24. package/src/app/pane-runtime/ticker-open-runtime.ts +7 -3
  25. package/src/architecture/core-builtin-allowlist.json +4 -13
  26. package/src/brokers/signed-in/sign-in-dialog.tsx +11 -5
  27. package/src/capabilities/chart-series.ts +11 -1
  28. package/src/cli/commands/crypto-hints.ts +7 -0
  29. package/src/cli/commands/fred-series.ts +61 -0
  30. package/src/cli/commands/market.ts +407 -104
  31. package/src/cli/commands/news-hints.ts +62 -0
  32. package/src/cli/commands/news-search.ts +53 -0
  33. package/src/cli/commands/options-leaps.ts +5 -2
  34. package/src/cli/commands/overview.ts +93 -20
  35. package/src/cli/commands/remote.ts +2 -1
  36. package/src/cli/commands/search.ts +82 -9
  37. package/src/cli/commands/system.ts +55 -8
  38. package/src/cli/commands/ticker.ts +88 -11
  39. package/src/cli/context.ts +45 -14
  40. package/src/cli/desktop-pane-shot.ts +59 -7
  41. package/src/cli/earnings-calendar.ts +71 -0
  42. package/src/cli/freshness.ts +18 -7
  43. package/src/cli/glossary.ts +429 -0
  44. package/src/cli/history-rows.ts +48 -7
  45. package/src/cli/index-roots.ts +7 -0
  46. package/src/cli/index.ts +21 -7
  47. package/src/cli/known-gaps.ts +27 -0
  48. package/src/cli/listing-arg.ts +417 -56
  49. package/src/cli/not-a-ticker.ts +12 -5
  50. package/src/cli/pane-functions/access-gate.ts +5 -0
  51. package/src/cli/pane-functions/capabilities.ts +3 -2
  52. package/src/cli/pane-functions/catalog.ts +35 -5
  53. package/src/cli/pane-functions/data.ts +12 -8
  54. package/src/cli/pane-functions/discovery.ts +1 -0
  55. package/src/cli/pane-functions/dom.ts +20 -6
  56. package/src/cli/pane-functions/freshness.ts +363 -43
  57. package/src/cli/pane-functions/function-help.ts +12 -5
  58. package/src/cli/pane-functions/headless.ts +45 -17
  59. package/src/cli/pane-functions/index.ts +63 -10
  60. package/src/cli/pane-functions/market-session.ts +22 -8
  61. package/src/cli/pane-functions/options.ts +63 -22
  62. package/src/cli/pane-functions/report-notices.ts +25 -7
  63. package/src/cli/pane-functions/report.ts +15 -8
  64. package/src/cli/pane-functions/resolver.ts +49 -12
  65. package/src/cli/pane-functions/screenshot.ts +110 -23
  66. package/src/cli/report-tables.ts +3 -0
  67. package/src/cli/result.ts +4 -1
  68. package/src/cli/staged-tickers.ts +52 -0
  69. package/src/cli/symbol-args.ts +9 -0
  70. package/src/cli/symbol-list.ts +172 -0
  71. package/src/cli/ticker-resolution.ts +223 -30
  72. package/src/cli/types.ts +5 -0
  73. package/src/components/chart/composite/axis-overlays.tsx +6 -2
  74. package/src/components/chart/composite/composite-chart.tsx +40 -6
  75. package/src/components/chart/composite/legend.tsx +49 -2
  76. package/src/components/chart/composite/panel-surface.tsx +44 -17
  77. package/src/components/chart/composite/price-axis-labels.tsx +3 -1
  78. package/src/components/command-bar/panel/index.tsx +11 -3
  79. package/src/components/command-bar/panel/runtime.ts +3 -0
  80. package/src/components/command-bar/panel/types.ts +2 -0
  81. package/src/components/command-bar/routes/root/provider-search.ts +4 -3
  82. package/src/components/command-bar/routes/ticker-search/results.ts +12 -11
  83. package/src/components/command-bar/routes/ticker-search/route.ts +4 -3
  84. package/src/components/command-bar/workflow/tickers.ts +23 -4
  85. package/src/components/layout/detached-pane-shell.tsx +9 -0
  86. package/src/components/layout/floating-pane.tsx +3 -0
  87. package/src/components/layout/header.tsx +20 -5
  88. package/src/components/layout/pane/content.tsx +7 -13
  89. package/src/components/layout/pane/footer/index.tsx +6 -2
  90. package/src/components/layout/pane/footer/model.ts +35 -1
  91. package/src/components/layout/pane/header.tsx +24 -1
  92. package/src/components/layout/pane/index.tsx +12 -2
  93. package/src/components/layout/pane/sidebar.tsx +78 -10
  94. package/src/components/layout/shell/active-drag.ts +21 -5
  95. package/src/components/layout/shell/drag/index.ts +39 -9
  96. package/src/components/layout/shell/drag/live.ts +61 -51
  97. package/src/components/layout/shell/drag/runtime.ts +54 -12
  98. package/src/components/layout/shell/index.tsx +25 -7
  99. package/src/components/layout/shell/menu.ts +5 -0
  100. package/src/components/layout/shell/native/pointer-runtime.ts +3 -1
  101. package/src/components/layout/shell/pane/layers.tsx +192 -45
  102. package/src/components/layout/shell/terminal-pointer-runtime.ts +24 -0
  103. package/src/components/markdown-text.tsx +62 -20
  104. package/src/components/metric-treemap/heat-surface.tsx +10 -1
  105. package/src/components/onboarding/account-step/form-panel.tsx +1 -1
  106. package/src/components/onboarding/account-step/index.tsx +1 -1
  107. package/src/components/onboarding/account-step/qr-panel.tsx +1 -1
  108. package/src/components/onboarding/account-step/signed-in-panel.tsx +1 -1
  109. package/src/components/onboarding/onboarding-keyboard.ts +1 -1
  110. package/src/components/onboarding/onboarding-wizard.tsx +2 -2
  111. package/src/components/onboarding/use-onboarding-upgrade.ts +3 -3
  112. package/src/components/onboarding/wizard-account.ts +2 -2
  113. package/src/components/pane-template-wizard.tsx +9 -1
  114. package/src/components/price-selector-dialog.tsx +11 -2
  115. package/src/components/sign-in-gate.tsx +3 -3
  116. package/src/components/ticker/list-table-view.tsx +24 -1
  117. package/src/components/ui/action-row.tsx +2 -1
  118. package/src/components/ui/button.tsx +7 -1
  119. package/src/components/ui/choice-dialog.tsx +6 -4
  120. package/src/components/ui/confirm-dialog.tsx +28 -18
  121. package/src/components/ui/data-table/index.tsx +17 -6
  122. package/src/components/ui/data-table/opentui/index.tsx +37 -6
  123. package/src/components/ui/data-table/types.ts +12 -0
  124. package/src/components/ui/dialog-actions.tsx +64 -0
  125. package/src/components/ui/display.tsx +17 -3
  126. package/src/components/ui/frame.tsx +35 -2
  127. package/src/components/ui/icon.tsx +2 -0
  128. package/src/components/ui/text-prompt-dialog.tsx +11 -9
  129. package/src/data/cached-query.ts +2 -0
  130. package/src/data/config/store/normalize.ts +7 -2
  131. package/src/data/ticker-repository.ts +5 -0
  132. package/src/data/ticker-store.ts +15 -0
  133. package/src/docs/content.ts +215 -0
  134. package/src/docs/load.ts +139 -0
  135. package/src/docs/requests.ts +39 -0
  136. package/src/docs/snapshot.chunk.ts +8 -0
  137. package/src/docs/snapshot.json +7416 -0
  138. package/src/docs/topics.ts +216 -0
  139. package/src/i18n/es.ts +61 -4
  140. package/src/i18n/ja.ts +61 -4
  141. package/src/i18n/ko.ts +61 -4
  142. package/src/i18n/zh-cn.ts +61 -4
  143. package/src/i18n/zh-tw.ts +61 -4
  144. package/src/layout/desks.ts +1 -1
  145. package/src/layout/pane-manager/dock-tree.ts +18 -0
  146. package/src/layout/pane-manager/index.ts +1 -0
  147. package/src/market-data/coordinator/chart.ts +10 -4
  148. package/src/market-data/coordinator/index.ts +6 -3
  149. package/src/{plugins/builtin/futures/contracts.ts → market-data/futures-board.ts} +81 -28
  150. package/src/market-data/futures-contract.ts +151 -0
  151. package/src/market-data/hooks.tsx +10 -2
  152. package/src/market-data/market/format.ts +39 -21
  153. package/src/market-data/market/freshness.ts +81 -18
  154. package/src/market-data/market/status.ts +19 -23
  155. package/src/market-data/published-apac-sessions.ts +23 -1
  156. package/src/market-data/quotes/contributions.ts +3 -0
  157. package/src/market-data/quotes/metadata.ts +7 -24
  158. package/src/market-data/quotes/resolution.ts +2 -0
  159. package/src/market-data/quotes/screener-live-quotes.ts +5 -4
  160. package/src/market-data/result-types.ts +2 -0
  161. package/src/plugins/builtin/account-management/model.ts +1 -56
  162. package/src/plugins/builtin/account-management/pane.tsx +6 -4
  163. package/src/plugins/builtin/account-management/password-dialog.tsx +1 -1
  164. package/src/plugins/builtin/alerts/cli.ts +313 -47
  165. package/src/plugins/builtin/alerts/events-pane.tsx +45 -7
  166. package/src/plugins/builtin/alerts/events.ts +23 -2
  167. package/src/plugins/builtin/alerts/history-pane.tsx +7 -2
  168. package/src/plugins/builtin/alerts/history.ts +2 -0
  169. package/src/plugins/builtin/alerts/index.tsx +41 -9
  170. package/src/plugins/builtin/alerts/pane.tsx +8 -0
  171. package/src/plugins/builtin/alerts/research-builder.ts +8 -3
  172. package/src/plugins/builtin/alerts/research-rules.ts +18 -2
  173. package/src/plugins/builtin/alerts/scheduled-check.ts +122 -0
  174. package/src/plugins/builtin/alerts/scheduled-sources.ts +25 -0
  175. package/src/plugins/builtin/alerts/scheduled.ts +254 -0
  176. package/src/plugins/builtin/alerts/targets.ts +92 -0
  177. package/src/plugins/builtin/analytics/index.tsx +4 -2
  178. package/src/plugins/builtin/analytics/risk-headless.ts +30 -12
  179. package/src/plugins/builtin/analytics/risk-math.ts +91 -27
  180. package/src/plugins/builtin/analytics/risk-model.ts +35 -5
  181. package/src/plugins/builtin/analytics/risk-pane.tsx +15 -8
  182. package/src/plugins/builtin/attention/index.tsx +2 -0
  183. package/src/plugins/builtin/attention/pane.tsx +3 -1
  184. package/src/plugins/builtin/attention/series.ts +26 -0
  185. package/src/plugins/builtin/awards/index.tsx +2 -0
  186. package/src/plugins/builtin/awards/pane.tsx +3 -1
  187. package/src/plugins/builtin/awards/series.ts +29 -0
  188. package/src/plugins/builtin/bond-calculator/model.ts +2 -1
  189. package/src/plugins/builtin/catalysts/pane.tsx +3 -1
  190. package/src/plugins/builtin/cds/client.ts +70 -13
  191. package/src/plugins/builtin/cds/headless.ts +116 -19
  192. package/src/plugins/builtin/cds/index.tsx +3 -1
  193. package/src/plugins/builtin/cds/model.ts +193 -31
  194. package/src/plugins/builtin/cds/pane.tsx +56 -14
  195. package/src/plugins/builtin/cds/series.ts +19 -0
  196. package/src/plugins/builtin/central-bank-rates/index.tsx +2 -0
  197. package/src/plugins/builtin/central-bank-rates/series.ts +42 -0
  198. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +192 -255
  199. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +84 -46
  200. package/src/plugins/builtin/chart-composer/editor.tsx +5 -4
  201. package/src/plugins/builtin/chart-composer/field-series.ts +40 -0
  202. package/src/plugins/builtin/chart-composer/headless.ts +8 -3
  203. package/src/plugins/builtin/chart-composer/index.tsx +9 -2
  204. package/src/plugins/builtin/chart-composer/series-catalog.ts +58 -50
  205. package/src/plugins/builtin/chart-composer/series-expression.ts +18 -10
  206. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +59 -3
  207. package/src/plugins/builtin/chart-composer/user-series.ts +84 -0
  208. package/src/plugins/builtin/chat/content/new-dm-dialog.tsx +10 -2
  209. package/src/plugins/builtin/chat/controller/index.ts +2 -0
  210. package/src/plugins/builtin/cloud/askg/controller.ts +111 -19
  211. package/src/plugins/builtin/cloud/askg/executor.ts +57 -3
  212. package/src/plugins/builtin/cloud/askg/manifest.ts +32 -7
  213. package/src/plugins/builtin/cloud/askg/model.ts +27 -8
  214. package/src/plugins/builtin/cloud/askg/protocol.ts +10 -1
  215. package/src/plugins/builtin/cloud/askg/tool-display.ts +3 -0
  216. package/src/plugins/builtin/cloud/askg/undo.ts +2 -0
  217. package/src/plugins/builtin/cloud/auth-actions.tsx +1 -1
  218. package/src/plugins/builtin/cloud/auth-commands.ts +1 -1
  219. package/src/plugins/builtin/cloud/auth-dialog.tsx +3 -3
  220. package/src/plugins/builtin/cloud/company-picker.tsx +1 -1
  221. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +3 -0
  222. package/src/plugins/builtin/cloud/terminal-relay/engine.ts +77 -9
  223. package/src/plugins/builtin/cloud/terminal-relay/manifest.ts +6 -1
  224. package/src/plugins/builtin/cloud/terminal-relay/policy.ts +9 -0
  225. package/src/plugins/builtin/cloud/terminal-relay/prompts.tsx +35 -11
  226. package/src/plugins/builtin/cloud/thesis/exposure.ts +1 -1
  227. package/src/plugins/builtin/cloud/thesis/prompts.tsx +2 -1
  228. package/src/plugins/builtin/company-attention/app-rank.tsx +1 -1
  229. package/src/plugins/builtin/company-attention/headless.ts +19 -5
  230. package/src/plugins/builtin/company-attention/index.tsx +2 -0
  231. package/src/plugins/builtin/company-attention/model.ts +26 -7
  232. package/src/plugins/builtin/company-attention/pane.tsx +12 -9
  233. package/src/plugins/builtin/company-attention/series.ts +52 -0
  234. package/src/plugins/builtin/company-kpis/pane.tsx +4 -2
  235. package/src/plugins/builtin/composite-plugins.ts +19 -13
  236. package/src/plugins/builtin/cot/index.tsx +3 -1
  237. package/src/plugins/builtin/cot/model.ts +40 -11
  238. package/src/plugins/builtin/cot/series.ts +27 -0
  239. package/src/plugins/builtin/credit-boards/index.tsx +2 -0
  240. package/src/plugins/builtin/credit-boards/series.ts +66 -0
  241. package/src/plugins/builtin/credit-conditions/index.tsx +2 -0
  242. package/src/plugins/builtin/credit-conditions/series.ts +26 -0
  243. package/src/plugins/builtin/credit-documents/headless.ts +31 -1
  244. package/src/plugins/builtin/credit-documents/pane.tsx +3 -1
  245. package/src/plugins/builtin/crypto-board/headless.ts +2 -3
  246. package/src/plugins/builtin/crypto-board/index.tsx +2 -0
  247. package/src/plugins/builtin/crypto-board/model.ts +12 -0
  248. package/src/plugins/builtin/crypto-board/pane.tsx +19 -12
  249. package/src/plugins/builtin/crypto-board/series.ts +48 -0
  250. package/src/plugins/builtin/custom-view/live-quotes.ts +2 -2
  251. package/src/plugins/builtin/debt-maturities/index.tsx +2 -0
  252. package/src/plugins/builtin/debt-maturities/series.ts +45 -0
  253. package/src/plugins/builtin/dividend-yield/headless.ts +2 -2
  254. package/src/plugins/builtin/dividend-yield/index.tsx +2 -0
  255. package/src/plugins/builtin/dividend-yield/series.ts +20 -0
  256. package/src/plugins/builtin/docs/blocks.tsx +181 -0
  257. package/src/plugins/builtin/docs/index.tsx +68 -0
  258. package/src/plugins/builtin/docs/pane.tsx +426 -0
  259. package/src/plugins/builtin/docs/targets.ts +72 -0
  260. package/src/plugins/builtin/docs/use-docs.ts +60 -0
  261. package/src/plugins/builtin/doe/index.tsx +2 -0
  262. package/src/plugins/builtin/doe/series.ts +62 -0
  263. package/src/plugins/builtin/earnings/client.ts +3 -2
  264. package/src/plugins/builtin/earnings/format.ts +1 -5
  265. package/src/plugins/builtin/earnings/headless.ts +58 -6
  266. package/src/plugins/builtin/earnings/history-model.ts +15 -1
  267. package/src/plugins/builtin/earnings/history-table.ts +13 -2
  268. package/src/plugins/builtin/earnings/history.tsx +7 -3
  269. package/src/plugins/builtin/econ/fred-catalog.ts +189 -0
  270. package/src/plugins/builtin/econ/fred-series-map.ts +0 -46
  271. package/src/plugins/builtin/econ/index.tsx +2 -0
  272. package/src/plugins/builtin/econ-statistics/defs.ts +5 -0
  273. package/src/plugins/builtin/econ-statistics/headless.ts +19 -4
  274. package/src/plugins/builtin/econ-statistics/stats.ts +1 -0
  275. package/src/plugins/builtin/equity-screener/pane.tsx +4 -11
  276. package/src/plugins/builtin/exposure/holdings.ts +1 -1
  277. package/src/plugins/builtin/exposure/pane.tsx +3 -1
  278. package/src/plugins/builtin/fear-greed/index.tsx +4 -0
  279. package/src/plugins/builtin/fear-greed/series.ts +52 -0
  280. package/src/plugins/builtin/futures/index.tsx +43 -3
  281. package/src/plugins/builtin/futures/model.ts +1 -1
  282. package/src/plugins/builtin/futures/returns.ts +1 -1
  283. package/src/plugins/builtin/futures/series.ts +13 -0
  284. package/src/plugins/builtin/futures/table.tsx +1 -1
  285. package/src/plugins/builtin/futures/use-front-returns.ts +1 -1
  286. package/src/plugins/builtin/futures-curve/client.ts +12 -3
  287. package/src/plugins/builtin/futures-curve/headless.ts +70 -9
  288. package/src/plugins/builtin/futures-curve/index.tsx +1 -1
  289. package/src/plugins/builtin/futures-curve/model.ts +66 -11
  290. package/src/plugins/builtin/futures-curve/pane.tsx +52 -18
  291. package/src/plugins/builtin/fx-matrix/index.tsx +1 -1
  292. package/src/plugins/builtin/gpu/board.tsx +13 -10
  293. package/src/plugins/builtin/gpu/changes.tsx +4 -4
  294. package/src/plugins/builtin/gpu/client.ts +19 -1
  295. package/src/plugins/builtin/gpu/equities.tsx +14 -10
  296. package/src/plugins/builtin/gpu/headless.ts +56 -25
  297. package/src/plugins/builtin/gpu/history.tsx +3 -3
  298. package/src/plugins/builtin/gpu/index.tsx +2 -0
  299. package/src/plugins/builtin/gpu/model.ts +101 -16
  300. package/src/plugins/builtin/gpu/pane.tsx +28 -12
  301. package/src/plugins/builtin/gpu/series.ts +38 -0
  302. package/src/plugins/builtin/help/function-card.tsx +25 -6
  303. package/src/plugins/builtin/help/index.tsx +17 -8
  304. package/src/plugins/builtin/holders/beneficial-model.ts +90 -3
  305. package/src/plugins/builtin/holders/beneficial-report.ts +57 -5
  306. package/src/plugins/builtin/holders/beneficial-view.tsx +55 -11
  307. package/src/plugins/builtin/holders/client.ts +22 -3
  308. package/src/plugins/builtin/holders/format.ts +18 -3
  309. package/src/plugins/builtin/holders/headless.ts +36 -133
  310. package/src/plugins/builtin/holders/pane.tsx +11 -4
  311. package/src/plugins/builtin/holders/report-header.ts +40 -5
  312. package/src/plugins/builtin/holders/report-rows.ts +240 -0
  313. package/src/plugins/builtin/insider/client.ts +35 -20
  314. package/src/plugins/builtin/insider/headless.ts +41 -11
  315. package/src/plugins/builtin/insider/index.tsx +26 -9
  316. package/src/plugins/builtin/insider/insider-data.ts +10 -0
  317. package/src/plugins/builtin/insider/issuer.ts +49 -0
  318. package/src/plugins/builtin/insider/model.ts +3 -0
  319. package/src/plugins/builtin/iv-history/client.ts +14 -4
  320. package/src/plugins/builtin/iv-history/headless.ts +7 -4
  321. package/src/plugins/builtin/iv-history/index.tsx +2 -0
  322. package/src/plugins/builtin/iv-history/model.ts +111 -7
  323. package/src/plugins/builtin/iv-history/screen-pane.tsx +5 -9
  324. package/src/plugins/builtin/iv-history/series.ts +29 -0
  325. package/src/plugins/builtin/jobs/headless.ts +28 -3
  326. package/src/plugins/builtin/jobs/index.tsx +3 -1
  327. package/src/plugins/builtin/jobs/series.ts +19 -0
  328. package/src/plugins/builtin/kelly-sizer/pane.tsx +2 -1
  329. package/src/plugins/builtin/kelly-sizer/portfolio.ts +0 -39
  330. package/src/plugins/builtin/macro-day/client.ts +44 -3
  331. package/src/plugins/builtin/macro-day/headless.ts +13 -10
  332. package/src/plugins/builtin/macro-day/model.ts +56 -10
  333. package/src/plugins/builtin/macro-day/pane.tsx +30 -20
  334. package/src/plugins/builtin/market-heatmap/model.ts +19 -11
  335. package/src/plugins/builtin/market-heatmap/portfolio.ts +2 -2
  336. package/src/plugins/builtin/market-movers/client.ts +7 -5
  337. package/src/plugins/builtin/market-movers/index.tsx +3 -2
  338. package/src/plugins/builtin/market-movers/model.ts +30 -0
  339. package/src/plugins/builtin/market-movers/screener.ts +31 -0
  340. package/src/plugins/builtin/market-valuation/chart-series.ts +29 -69
  341. package/src/plugins/builtin/market-valuation/index.tsx +3 -3
  342. package/src/plugins/builtin/market-valuation/indicators.ts +0 -5
  343. package/src/plugins/builtin/members/headless.ts +7 -2
  344. package/src/plugins/builtin/members/model.ts +31 -3
  345. package/src/plugins/builtin/members/pane.tsx +3 -6
  346. package/src/plugins/builtin/mna/headless.ts +15 -3
  347. package/src/plugins/builtin/money-markets/index.tsx +2 -0
  348. package/src/plugins/builtin/money-markets/series.ts +41 -0
  349. package/src/plugins/builtin/notes/cli.ts +12 -7
  350. package/src/plugins/builtin/options/view.tsx +1 -1
  351. package/src/plugins/builtin/options-calculator/inputs.ts +2 -1
  352. package/src/plugins/builtin/options-positioning/client.ts +7 -3
  353. package/src/plugins/builtin/options-positioning/headless.ts +1 -0
  354. package/src/plugins/builtin/options-positioning/pane.tsx +2 -2
  355. package/src/plugins/builtin/options-scenario/client.ts +2 -1
  356. package/src/plugins/builtin/options-scenario/headless.ts +1 -0
  357. package/src/plugins/builtin/options-scenario/hedge.ts +2 -1
  358. package/src/plugins/builtin/perps/client.ts +40 -6
  359. package/src/plugins/builtin/perps/equity-row.tsx +33 -11
  360. package/src/plugins/builtin/perps/freshness.ts +66 -0
  361. package/src/plugins/builtin/perps/headless.ts +45 -30
  362. package/src/plugins/builtin/perps/index.tsx +18 -1
  363. package/src/plugins/builtin/perps/model.ts +20 -5
  364. package/src/plugins/builtin/perps/pane.tsx +76 -20
  365. package/src/plugins/builtin/perps/series.ts +76 -0
  366. package/src/plugins/builtin/plugin-marketplace/pane.tsx +67 -61
  367. package/src/plugins/builtin/plugin-module.ts +3 -0
  368. package/src/plugins/builtin/portfolio-list/account-metrics.ts +1 -1
  369. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +273 -0
  370. package/src/plugins/builtin/portfolio-list/cli/import-13f.ts +109 -0
  371. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +331 -65
  372. package/src/plugins/builtin/portfolio-list/cli/render.ts +200 -31
  373. package/src/plugins/builtin/portfolio-list/cli/shared.ts +24 -0
  374. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +120 -47
  375. package/src/plugins/builtin/portfolio-list/column-values.ts +1 -1
  376. package/src/plugins/builtin/portfolio-list/grid.tsx +1 -1
  377. package/src/plugins/builtin/portfolio-list/headless.ts +34 -4
  378. package/src/plugins/builtin/portfolio-list/imported-from.ts +8 -0
  379. package/src/plugins/builtin/portfolio-list/live-valuation.ts +6 -2
  380. package/src/plugins/builtin/portfolio-list/pane/index.tsx +6 -1
  381. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +17 -1
  382. package/src/plugins/builtin/portfolio-list/summary/totals.ts +1 -1
  383. package/src/plugins/builtin/portfolio-list/table.tsx +7 -1
  384. package/src/plugins/builtin/power/client.ts +16 -0
  385. package/src/plugins/builtin/power/evidence.ts +6 -2
  386. package/src/plugins/builtin/power/headless.ts +71 -7
  387. package/src/plugins/builtin/power/index.tsx +4 -1
  388. package/src/plugins/builtin/power/model.ts +72 -1
  389. package/src/plugins/builtin/power/pane.tsx +32 -9
  390. package/src/plugins/builtin/rate-path/headless.ts +11 -3
  391. package/src/plugins/builtin/realized-vol/headless.ts +5 -0
  392. package/src/plugins/builtin/research/corporate-actions-pane.tsx +5 -2
  393. package/src/plugins/builtin/research/event-model.ts +68 -24
  394. package/src/plugins/builtin/research/events-headless.ts +2 -1
  395. package/src/plugins/builtin/research/headless.ts +2 -1
  396. package/src/plugins/builtin/seasonality/client.ts +15 -7
  397. package/src/plugins/builtin/seasonality/headless.ts +36 -12
  398. package/src/plugins/builtin/seasonality/index.tsx +2 -2
  399. package/src/plugins/builtin/seasonality/model.ts +119 -0
  400. package/src/plugins/builtin/seasonality/pane.tsx +108 -37
  401. package/src/plugins/builtin/series-inventory.ts +91 -0
  402. package/src/plugins/builtin/shared/cloud-upgrade.ts +1 -1
  403. package/src/plugins/builtin/shared/command-tab.ts +30 -12
  404. package/src/plugins/builtin/shared/earnings-report.ts +55 -0
  405. package/src/plugins/builtin/shared/fred-points.ts +20 -0
  406. package/src/plugins/builtin/shared/fx-rate-freshness.ts +101 -0
  407. package/src/plugins/builtin/shared/open-description.ts +29 -0
  408. package/src/plugins/builtin/shared/report-freshness.ts +9 -0
  409. package/src/plugins/builtin/shared/series-warning.ts +5 -0
  410. package/src/plugins/builtin/short-interest/headless.ts +1 -0
  411. package/src/plugins/builtin/short-interest/index.tsx +2 -0
  412. package/src/plugins/builtin/short-interest/series.ts +36 -0
  413. package/src/plugins/builtin/short-interest/watch-headless.ts +1 -0
  414. package/src/plugins/builtin/short-interest/watch-model.ts +9 -5
  415. package/src/plugins/builtin/short-volume/headless.ts +1 -0
  416. package/src/plugins/builtin/short-volume/index.tsx +2 -0
  417. package/src/plugins/builtin/short-volume/series.ts +23 -0
  418. package/src/plugins/builtin/social-mentions/index.tsx +2 -0
  419. package/src/plugins/builtin/social-mentions/series.ts +23 -0
  420. package/src/plugins/builtin/supply-chain/graph-pane.tsx +3 -2
  421. package/src/plugins/builtin/supply-chain/pane.tsx +3 -1
  422. package/src/plugins/builtin/thirteenf/api.ts +1 -0
  423. package/src/plugins/builtin/thirteenf/data.ts +6 -4
  424. package/src/plugins/builtin/thirteenf/headless.ts +217 -36
  425. package/src/plugins/builtin/thirteenf/model.ts +39 -1
  426. package/src/plugins/builtin/thirteenf/overlap.ts +75 -0
  427. package/src/plugins/builtin/thirteenf/pane.tsx +10 -1
  428. package/src/plugins/builtin/thirteenf/report-notes.ts +103 -1
  429. package/src/plugins/builtin/thirteenf/types.ts +2 -0
  430. package/src/plugins/builtin/ticker-detail/index.tsx +3 -0
  431. package/src/plugins/builtin/ticker-detail/overview/components.tsx +1 -1
  432. package/src/plugins/builtin/ticker-detail/overview/futures-contract.ts +31 -0
  433. package/src/plugins/builtin/ticker-detail/overview/model.ts +3 -3
  434. package/src/plugins/builtin/ticker-detail/overview/pane.tsx +2 -1
  435. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +48 -4
  436. package/src/plugins/builtin/ticker-detail/quote-monitor/index.tsx +13 -3
  437. package/src/plugins/builtin/time-sales/headless.ts +1 -0
  438. package/src/plugins/builtin/treasury-auctions/headless.ts +16 -2
  439. package/src/plugins/builtin/treasury-auctions/index.tsx +1 -0
  440. package/src/plugins/builtin/treasury-auctions/model.ts +8 -0
  441. package/src/plugins/builtin/vol-surface/headless.ts +1 -0
  442. package/src/plugins/builtin/volatility/index.tsx +2 -0
  443. package/src/plugins/builtin/volatility/series.ts +84 -0
  444. package/src/plugins/builtin/world-venue-map/geo-series.ts +26 -1
  445. package/src/plugins/builtin/world-venue-map/headless.ts +4 -1
  446. package/src/plugins/builtin/world-venue-map/index.ts +2 -1
  447. package/src/plugins/builtin/world-venue-map/layered-pane.tsx +8 -4
  448. package/src/plugins/builtin/world-venue-map/layers.ts +29 -0
  449. package/src/plugins/builtin/yield-curve/chart.ts +2 -2
  450. package/src/plugins/builtin/yield-curve/curves.ts +21 -8
  451. package/src/plugins/builtin/yield-curve/headless.ts +71 -18
  452. package/src/plugins/builtin/yield-curve/index.tsx +3 -1
  453. package/src/plugins/builtin/yield-curve/series.ts +86 -0
  454. package/src/plugins/catalog-browser.ts +2 -1
  455. package/src/plugins/registry/index.ts +89 -4
  456. package/src/{plugins/builtin/portfolio-list → portfolio}/position-metrics.ts +6 -6
  457. package/src/portfolio/position-value.ts +40 -0
  458. package/src/public/__snapshots__/public-api.test.ts.snap +7 -0
  459. package/src/public/time-series.ts +17 -0
  460. package/src/react/auto-refresh.ts +13 -3
  461. package/src/remote/app-host.tsx +20 -4
  462. package/src/remote/confirm-change.tsx +85 -0
  463. package/src/remote/controller-utils.ts +8 -1
  464. package/src/remote/controller.ts +123 -6
  465. package/src/remote/pane-screenshot.ts +47 -0
  466. package/src/remote/schema.ts +62 -0
  467. package/src/remote/types.ts +30 -0
  468. package/src/remote/watchlist.ts +306 -0
  469. package/src/renderers/browser/main.tsx +3 -2
  470. package/src/renderers/cloudflare/worker.ts +1 -1
  471. package/src/renderers/dom/data-table/frozen-column.ts +3 -1
  472. package/src/renderers/dom/data-table/index.tsx +33 -6
  473. package/src/renderers/dom/data-table/row.tsx +27 -1
  474. package/src/renderers/dom/desktop/control-styles.ts +63 -1
  475. package/src/renderers/dom/desktop/controls.tsx +30 -23
  476. package/src/renderers/dom/desktop/dialog-actions.tsx +170 -0
  477. package/src/renderers/dom/desktop/icons.tsx +7 -0
  478. package/src/renderers/dom/dom-ui-host.tsx +3 -0
  479. package/src/renderers/dom/host/box.tsx +3 -0
  480. package/src/renderers/dom/host/chart-surface.tsx +6 -2
  481. package/src/renderers/dom/host/live-frame.ts +6 -2
  482. package/src/renderers/dom/host/overflow-fade.ts +14 -1
  483. package/src/renderers/dom/host/size-observer.ts +26 -0
  484. package/src/renderers/dom/host/surface-3d.tsx +7 -2
  485. package/src/renderers/dom/host/text.tsx +1 -1
  486. package/src/renderers/dom/styles.css +61 -0
  487. package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +17 -2
  488. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +19 -11
  489. package/src/renderers/electrobun/view/cli-pane-shot-health.ts +10 -26
  490. package/src/renderers/electrobun/view/main.tsx +2 -0
  491. package/src/series/capability.ts +75 -0
  492. package/src/series/csv.ts +160 -0
  493. package/src/series/files.ts +107 -0
  494. package/src/series/registry.ts +198 -0
  495. package/src/series/resolve.ts +107 -0
  496. package/src/series/standalone.ts +48 -0
  497. package/src/series/types.ts +145 -0
  498. package/src/series/validate.ts +171 -0
  499. package/src/sources/gloomberb-cloud/index.ts +7 -7
  500. package/src/sources/gloomberb-cloud/normalizers.ts +1 -0
  501. package/src/sources/history-result.ts +1 -0
  502. package/src/sources/listing-symbols.ts +4 -1
  503. package/src/sources/provider-router/batches.ts +4 -1
  504. package/src/sources/provider-router/cache.ts +4 -2
  505. package/src/sources/provider-router/cached-routes.ts +1 -1
  506. package/src/sources/provider-router/financial-routes.ts +1 -0
  507. package/src/sources/provider-router/financials.ts +13 -0
  508. package/src/sources/provider-router/history.ts +43 -25
  509. package/src/sources/provider-router/search.ts +1 -0
  510. package/src/sync/core-contributors.ts +1 -0
  511. package/src/tickers/company-name.ts +75 -0
  512. package/src/tickers/instrument-kind.ts +25 -0
  513. package/src/tickers/open-target.ts +3 -2
  514. package/src/tickers/search/index.ts +45 -7
  515. package/src/tickers/search/ranking.ts +33 -17
  516. package/src/tickers/search/types.ts +7 -1
  517. package/src/tickers/us-listing.ts +34 -0
  518. package/src/time-series/hooks.ts +9 -0
  519. package/src/time-series/resolve.ts +95 -16
  520. package/src/time-series/types.ts +4 -2
  521. package/src/time-series/use-chart-resolution.ts +32 -0
  522. package/src/types/config.ts +6 -0
  523. package/src/types/data-provider.ts +5 -0
  524. package/src/types/financials.ts +52 -1
  525. package/src/types/headless.ts +61 -0
  526. package/src/types/plugin.ts +19 -0
  527. package/src/types/price-history.ts +7 -0
  528. package/src/types/ticker.ts +14 -0
  529. package/src/ui/host.tsx +18 -0
  530. package/src/ui/live-resize.ts +81 -0
  531. package/src/ui/live-stretch.ts +121 -0
  532. package/src/ui/measured-cells.ts +58 -0
  533. package/src/ui/pane-fill.ts +11 -0
  534. package/src/utils/basis-points.ts +17 -0
  535. package/src/utils/cli-output.ts +7 -1
  536. package/src/utils/concurrency.ts +25 -0
  537. package/src/utils/crypto-pair.ts +20 -0
  538. package/src/utils/currency-pair.ts +19 -0
  539. package/src/utils/decimal-comma.ts +19 -0
  540. package/src/utils/dom-screenshot-css.ts +265 -0
  541. package/src/utils/dom-screenshot-rasters.ts +276 -0
  542. package/src/utils/dom-screenshot.ts +437 -52
  543. package/src/utils/exchange-rate-snapshot.ts +6 -2
  544. package/src/utils/exchanges.ts +107 -1
  545. package/src/utils/format.ts +16 -3
  546. package/src/utils/fundamentals.ts +66 -0
  547. package/src/utils/futures-generic.ts +6 -12
  548. package/src/utils/fx-market-hours.ts +15 -5
  549. package/src/utils/price-history.ts +99 -42
  550. package/src/utils/sec-eps-basis.ts +24 -11
  551. package/src/utils/sec.ts +61 -7
  552. package/src/utils/utc-time.ts +51 -4
  553. package/src/plugins/builtin/fx-matrix/freshness.ts +0 -65
  554. /package/src/{plugins/builtin/cloud → tickers}/starter-symbols.ts +0 -0
@@ -20,12 +20,16 @@ export const CANONICAL_EXCHANGE_ALIASES: Record<string, string> = {
20
20
  ARCA: "ARCA",
21
21
  ARCX: "ARCA",
22
22
  BATS: "BATS",
23
- // Cboe's US equities listing venue (BZX); quotes label it "CBOE US".
23
+ // Cboe's US equities listing venue (BZX); quotes label it "CBOE US" or BTS.
24
24
  "CBOE US": "BATS",
25
+ BTS: "BATS",
25
26
  TYO: "JPX",
27
+ // Toronto, as brokers and quote sites spell it; typed after a symbol it can
28
+ // also be Tokyo (SHARED_EXCHANGE_CODES).
26
29
  TSE: "TSX",
27
30
  TSEJ: "JPX",
28
31
  JPX: "JPX",
32
+ XTKS: "JPX",
29
33
  HKG: "HKEX",
30
34
  HKEX: "HKEX",
31
35
  SEHK: "HKEX",
@@ -40,6 +44,7 @@ export const CANONICAL_EXCHANGE_ALIASES: Record<string, string> = {
40
44
  TSXV: "TSXV",
41
45
  XTSX: "TSXV",
42
46
  VAN: "TSXV",
47
+ CVE: "TSXV",
43
48
  CN: "CSE",
44
49
  CNSX: "CSE",
45
50
  XCNQ: "CSE",
@@ -48,6 +53,7 @@ export const CANONICAL_EXCHANGE_ALIASES: Record<string, string> = {
48
53
  XASX: "ASX",
49
54
  NZX: "NZX",
50
55
  XNZE: "NZX",
56
+ NZE: "NZX",
51
57
  SIX: "SWX",
52
58
  SWX: "SWX",
53
59
  EBS: "SWX",
@@ -60,6 +66,7 @@ export const CANONICAL_EXCHANGE_ALIASES: Record<string, string> = {
60
66
  GER: "XETRA",
61
67
  FSE: "FWB2",
62
68
  FSX: "FWB2",
69
+ FRA: "FWB2",
63
70
  XFRA: "FWB2",
64
71
  FWB: "FWB2",
65
72
  FWB2: "FWB2",
@@ -112,14 +119,19 @@ export const CANONICAL_EXCHANGE_ALIASES: Record<string, string> = {
112
119
  SSE: "SSE",
113
120
  XSHG: "SSE",
114
121
  SHH: "SSE",
122
+ SHA: "SSE",
115
123
  SZSE: "SZSE",
116
124
  XSHE: "SZSE",
117
125
  SHE: "SZSE",
126
+ SHZ: "SZSE",
127
+ // KOSPI; typed after a symbol it can also be KOSDAQ, which the Korea Exchange runs too.
118
128
  KRX: "KRX",
119
129
  XKRX: "KRX",
120
130
  KOSPI: "KRX",
131
+ KSC: "KRX",
121
132
  KOSDAQ: "KOSDAQ",
122
133
  XKOS: "KOSDAQ",
134
+ KOE: "KOSDAQ",
123
135
  SGX: "SGX",
124
136
  XSES: "SGX",
125
137
  BMV: "BMV",
@@ -127,6 +139,7 @@ export const CANONICAL_EXCHANGE_ALIASES: Record<string, string> = {
127
139
  B3: "B3",
128
140
  BVMF: "B3",
129
141
  B3SA: "B3",
142
+ SAO: "B3",
130
143
  BUE: "BYMA",
131
144
  BCBA: "BYMA",
132
145
  JSE: "JSE",
@@ -138,9 +151,52 @@ export const CANONICAL_EXCHANGE_ALIASES: Record<string, string> = {
138
151
  QE: "QE",
139
152
  QSE: "QE",
140
153
  DSMD: "QE",
154
+ // Bahrain Bourse, which Gloom Cloud names BHB.
155
+ BHB: "BHB",
156
+ XBAH: "BHB",
157
+ BAHRAIN: "BHB",
158
+ "BAHRAIN BOURSE": "BHB",
141
159
  DFM: "DFM",
142
160
  XDFM: "DFM",
161
+ // The Muscat Stock Exchange, which Gloom Cloud names MSX.
162
+ MSX: "MSX",
163
+ XMUS: "MSX",
164
+ MSM: "MSX",
165
+ MUSCAT: "MSX",
166
+ OMAN: "MSX",
167
+ // Boursa Kuwait, which Gloom Cloud names KUWAIT.
168
+ KUWAIT: "KUWAIT",
169
+ XKUW: "KUWAIT",
170
+ BOURSA: "KUWAIT",
171
+ "BOURSA KUWAIT": "KUWAIT",
172
+ // The Egyptian Exchange, which Gloom Cloud names EGX.
173
+ EGX: "EGX",
174
+ CAI: "EGX",
175
+ XCAI: "EGX",
176
+ EGYPT: "EGX",
177
+ CAIRO: "EGX",
143
178
  CCC: "CCC",
179
+ // Futures venues, by the code search gives each (GC=F on CMX). Quotes spell
180
+ // the venue out (COMEX, NY MERCANTILE); both spellings are one venue, or the
181
+ // quote for GC=F:CMX reads as another venue's and is dropped.
182
+ CME: "CME",
183
+ CBT: "CBT",
184
+ CBOT: "CBT",
185
+ NYM: "NYM",
186
+ NYMEX: "NYM",
187
+ "NY MERCANTILE": "NYM",
188
+ CMX: "CMX",
189
+ COMEX: "CMX",
190
+ NYB: "NYB",
191
+ NYBOT: "NYB",
192
+ "ICE FUTURES": "NYB",
193
+ CFE: "CFE",
194
+ "CBOE FUTURES": "CFE",
195
+ // The currency venue: EURUSD:CCY and EURUSD:FX are the pair EURUSD=X. Pairs
196
+ // saved from an older search answer are on PHYSICAL CURRENCY.
197
+ CCY: "CCY",
198
+ FX: "CCY",
199
+ "PHYSICAL CURRENCY": "CCY",
144
200
  };
145
201
 
146
202
  const PUBLIC_EXCHANGE_ALIASES: Record<string, string> = {
@@ -231,7 +287,11 @@ export const EXCHANGE_TIME_ZONES: Record<string, string> = {
231
287
  TASE: "Asia/Jerusalem",
232
288
  TADAWUL: "Asia/Riyadh",
233
289
  QE: "Asia/Qatar",
290
+ BHB: "Asia/Bahrain",
234
291
  DFM: "Asia/Dubai",
292
+ MSX: "Asia/Muscat",
293
+ KUWAIT: "Asia/Kuwait",
294
+ EGX: "Africa/Cairo",
235
295
  CCC: "UTC",
236
296
  };
237
297
 
@@ -287,7 +347,16 @@ const EXCHANGE_NAMES: Record<string, string> = {
287
347
  TASE: "Tel Aviv Stock Exchange",
288
348
  TADAWUL: "Saudi Exchange",
289
349
  QE: "Qatar Stock Exchange",
350
+ BHB: "Bahrain Bourse",
290
351
  DFM: "Dubai Financial Market",
352
+ MSX: "Muscat Stock Exchange",
353
+ KUWAIT: "Boursa Kuwait",
354
+ EGX: "Egyptian Exchange",
355
+ CBT: "CBOT",
356
+ NYM: "NYMEX",
357
+ CMX: "COMEX",
358
+ NYB: "ICE Futures U.S.",
359
+ CFE: "Cboe Futures Exchange",
291
360
  };
292
361
 
293
362
  const KNOWN_EXCHANGE_CODES: ReadonlySet<string> = new Set([
@@ -323,6 +392,43 @@ export function canonicalExchange(value?: string): string {
323
392
  return CANONICAL_EXCHANGE_ALIASES[normalized] ?? normalized;
324
393
  }
325
394
 
395
+ /**
396
+ * Codes people type for more than one venue, the venue `canonicalExchange`
397
+ * reads them as first. TSE is the Tokyo Stock Exchange's own short name, and
398
+ * the code brokers and quote sites give Toronto; KRX is the Korea Exchange,
399
+ * which runs both KOSPI and KOSDAQ; EURONEXT runs Paris, Amsterdam, Brussels,
400
+ * Lisbon, Oslo and Milan. Data that arrives with one (a broker's contract)
401
+ * keeps the first venue; a listing typed with one is the venue its symbol
402
+ * trades on.
403
+ */
404
+ const SHARED_EXCHANGE_CODES: Record<string, readonly string[]> = {
405
+ TSE: ["TSX", "JPX"],
406
+ KRX: ["KRX", "KOSDAQ"],
407
+ EURONEXT: ["AMS", "EPA", "BRU", "LIS", "OSL", "BIT"],
408
+ };
409
+
410
+ /** Whether a typed exchange code names more than one venue (TSE: Tokyo or Toronto). */
411
+ export function isSharedExchangeCode(value?: string): boolean {
412
+ return normalizeExchange(value) in SHARED_EXCHANGE_CODES;
413
+ }
414
+
415
+ /**
416
+ * Every venue a typed exchange code can name, as canonical codes: TSE is
417
+ * TSX or JPX, PAR is EPA. Empty for no code.
418
+ */
419
+ export function exchangeCodeVenues(value?: string): string[] {
420
+ const normalized = normalizeExchange(value);
421
+ if (!normalized) return [];
422
+ return [...(SHARED_EXCHANGE_CODES[normalized] ?? [canonicalExchange(normalized)])];
423
+ }
424
+
425
+ /** The exchange code after a listing key's colon, as typed: TSE for 7203:TSE; "" when there is none. */
426
+ export function typedExchangeCode(key: string): string {
427
+ const normalized = normalizeSymbol(key);
428
+ const separator = normalized.lastIndexOf(":");
429
+ return separator > 0 ? normalized.slice(separator + 1) : "";
430
+ }
431
+
326
432
  /**
327
433
  * The US primary listing venues, as canonical exchanges. All of them trade the
328
434
  * US regular and extended-hours sessions.
@@ -1,7 +1,16 @@
1
1
  const currencyFormatters = new Map<string, Intl.NumberFormat>();
2
2
  const numberFormatters = new Map<number, Intl.NumberFormat>();
3
3
 
4
- function getCurrencyFormatter(currency: string, maximumFractionDigits = 2): Intl.NumberFormat {
4
+ /** Currencies whose minor unit is a thousandth (ISO 4217 exponent 3): an Omani share is OMR 1.366, not OMR 1.37.
5
+ * IQD is left out on purpose: it has the exponent in ISO 4217 but trades in whole units. */
6
+ const THREE_DECIMAL_CURRENCIES = new Set(["BHD", "JOD", "KWD", "LYD", "OMR", "TND"]);
7
+
8
+ /** Decimals a price or amount in this currency is shown to: 3 for the thousandth currencies, 2 for everything else. */
9
+ export function currencyPriceDigits(currency: string | undefined): 2 | 3 {
10
+ return currency != null && THREE_DECIMAL_CURRENCIES.has(currency.trim().toUpperCase()) ? 3 : 2;
11
+ }
12
+
13
+ function getCurrencyFormatter(currency: string, maximumFractionDigits: number = currencyPriceDigits(currency)): Intl.NumberFormat {
5
14
  const key = `${currency}:${maximumFractionDigits}`;
6
15
  let formatter = currencyFormatters.get(key);
7
16
  if (!formatter) {
@@ -9,7 +18,7 @@ function getCurrencyFormatter(currency: string, maximumFractionDigits = 2): Intl
9
18
  style: "currency",
10
19
  currency,
11
20
  // Whole units (0) drop the cents rather than throw.
12
- minimumFractionDigits: Math.min(2, maximumFractionDigits),
21
+ minimumFractionDigits: Math.min(currencyPriceDigits(currency), maximumFractionDigits),
13
22
  maximumFractionDigits,
14
23
  });
15
24
  currencyFormatters.set(key, formatter);
@@ -30,7 +39,11 @@ function getNumberFormatter(decimals: number): Intl.NumberFormat {
30
39
  }
31
40
 
32
41
  /** Format a number as currency (e.g., $1,234.56) */
33
- export function formatCurrency(value: number | undefined, currency = "USD", maximumFractionDigits = 2): string {
42
+ export function formatCurrency(
43
+ value: number | undefined,
44
+ currency = "USD",
45
+ maximumFractionDigits: number = currencyPriceDigits(currency),
46
+ ): string {
34
47
  if (value == null || !Number.isFinite(value)) return "—";
35
48
  return getCurrencyFormatter(currency, maximumFractionDigits).format(value);
36
49
  }
@@ -1,4 +1,5 @@
1
1
  import type { Fundamentals, TickerFinancials } from "../types/financials";
2
+ import { resolveCurrencyUnit } from "./currency-units";
2
3
 
3
4
  export const RETRACTABLE_VALUATION_FIELDS = ["enterpriseValue", "enterpriseToRevenue"] as const;
4
5
 
@@ -29,3 +30,68 @@ export function fundamentalsCurrency(
29
30
  ): string | undefined {
30
31
  return financials?.fundamentals?.financialCurrency?.trim() || financials?.financialCurrency?.trim() || undefined;
31
32
  }
33
+
34
+ /**
35
+ * The currency of the trailing EPS: the one stated for it when it differs from the block's amounts (an EPS converted
36
+ * to the listing's currency beside revenue left in the reporting one), else the block's.
37
+ */
38
+ export function fundamentalsEpsCurrency(
39
+ financials: Pick<TickerFinancials, "fundamentals" | "financialCurrency"> | null | undefined,
40
+ ): string | undefined {
41
+ return financials?.fundamentals?.epsCurrency?.trim() || fundamentalsCurrency(financials);
42
+ }
43
+
44
+ const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/;
45
+
46
+ /**
47
+ * "TTM converted from EUR (1 EUR = 1.1257 USD, 2026-10-09); EPS at report-date rates.": what a converted trailing
48
+ * block was converted from, at which rate and when. Undefined for a block in the currency the company reports in.
49
+ */
50
+ export function ttmConversionNote(fundamentals: Pick<Fundamentals, "financialCurrency" | "ttmConversion"> | undefined): string | undefined {
51
+ const conversion = fundamentals?.ttmConversion;
52
+ const from = conversion?.from?.trim();
53
+ const to = fundamentals?.financialCurrency?.trim();
54
+ if (!conversion || !from || !to || from === to) return undefined;
55
+ const rate = typeof conversion.rate === "number" && Number.isFinite(conversion.rate) && conversion.rate > 0
56
+ ? `1 ${from} = ${Number(conversion.rate.toPrecision(5))} ${to}`
57
+ : undefined;
58
+ const asOf = conversion.asOf && ISO_DAY.test(conversion.asOf) ? conversion.asOf : undefined;
59
+ const basis = [rate, asOf].filter(Boolean).join(", ");
60
+ const eps = conversion.eps === "report_date" ? "; EPS at report-date rates" : "";
61
+ return `TTM converted from ${from}${basis ? ` (${basis})` : ""}${eps}.`;
62
+ }
63
+
64
+ /**
65
+ * "EPS (TTM) in USD, converted from CLP at report-date rates.": an EPS in another currency than the trailing amounts
66
+ * beside it. Undefined when they share one.
67
+ */
68
+ export function epsConversionNote(fundamentals: Pick<Fundamentals, "financialCurrency" | "epsCurrency"> | undefined): string | undefined {
69
+ const eps = fundamentals?.epsCurrency?.trim();
70
+ const amounts = fundamentals?.financialCurrency?.trim();
71
+ return eps && amounts && eps !== amounts ? `EPS (TTM) in ${eps}, converted from ${amounts} at report-date rates.` : undefined;
72
+ }
73
+
74
+ type PriceEarningsBasis = Pick<Fundamentals, "financialCurrency" | "ttmConversion" | "epsCurrency" | "trailingPEConversion">;
75
+
76
+ /** The trailing EPS in the price's currency: converted at the rate the served P/E used when it says so. */
77
+ export function priceEarningsEps(fundamentals: Pick<Fundamentals, "eps" | "trailingPEConversion"> | undefined): number | undefined {
78
+ const eps = fundamentals?.eps;
79
+ const rate = fundamentals?.trailingPEConversion?.rate;
80
+ return typeof eps === "number" && typeof rate === "number" && Number.isFinite(rate) && rate > 0 ? eps * rate : eps;
81
+ }
82
+
83
+ /**
84
+ * The trailing P/E's footnote: the rate its EPS was converted at when that EPS is in another currency than the
85
+ * price, or that it divides a spot price by EPS converted at report-date rates.
86
+ */
87
+ export function convertedPriceEarningsNote(fundamentals: PriceEarningsBasis | undefined, quoteCurrency?: string): string | undefined {
88
+ const conversion = fundamentals?.trailingPEConversion;
89
+ const from = conversion?.from?.trim();
90
+ const to = resolveCurrencyUnit(quoteCurrency).currency;
91
+ if (from && to && from !== to && typeof conversion?.rate === "number" && Number.isFinite(conversion.rate) && conversion.rate > 0) {
92
+ return `P/E (TTM): price over EPS converted at 1 ${from} = ${Number(conversion.rate.toPrecision(5))} ${to}.`;
93
+ }
94
+ return (ttmConversionNote(fundamentals) && fundamentals?.ttmConversion?.eps === "report_date") || epsConversionNote(fundamentals)
95
+ ? "P/E (TTM): spot price over EPS at report-date rates."
96
+ : undefined;
97
+ }
@@ -7,7 +7,7 @@
7
7
  * ratio-adjusts, A difference-adjusts, neither leaves prices as traded.
8
8
  */
9
9
 
10
- import { parsePublicTickerKey } from "./exchanges";
10
+ import { canonicalExchange, parsePublicTickerKey } from "./exchanges";
11
11
 
12
12
  export type FuturesGenericRoll =
13
13
  | { rule: "open-interest" }
@@ -36,11 +36,6 @@ const ROOT_VENUES: Readonly<Record<string, string>> = Object.fromEntries(([
36
36
  ["CFE", ["VX"]],
37
37
  ] as const).flatMap(([venue, roots]) => roots.map((root) => [root, venue])));
38
38
 
39
- /** Other names quotes give each venue, which a listing may carry as its exchange. */
40
- const VENUE_NAMES: Readonly<Record<string, readonly string[]>> = {
41
- NYM: ["NYMEX", "NY MERCANTILE"], CBT: ["CBOT"], CMX: ["COMEX"], NYB: ["ICE FUTURES", "NYBOT"], CFE: ["CBOE FUTURES"],
42
- };
43
-
44
39
  /** Bloomberg's codes where they differ from the exchange's. */
45
40
  const ALIASES: Readonly<Record<string, string>> = {
46
41
  TY: "ZN", US: "ZB", FV: "ZF", TU: "ZT", WN: "UB", DM: "YM", FF: "ZQ", SFR: "SR3",
@@ -83,9 +78,9 @@ export function futuresGenericListing(symbol: string, exchange?: string | null):
83
78
  const key = parsePublicTickerKey(symbol);
84
79
  const generic = parseFuturesGeneric(key.symbol);
85
80
  if (!generic) return null;
86
- const venue = (key.exchange || exchange || "").trim().toUpperCase();
87
- const own = ROOT_VENUES[generic.root]!;
88
- return !venue || venue === own || VENUE_NAMES[own]?.includes(venue) ? generic : null;
81
+ // The alias table names each venue by its code, however a quote spells it (NYMEX is NYM).
82
+ const venue = key.exchange || canonicalExchange(exchange ?? "");
83
+ return !venue || venue === ROOT_VENUES[generic.root] ? generic : null;
89
84
  }
90
85
 
91
86
  const FUTURES_VENUES = new Set(Object.values(ROOT_VENUES));
@@ -98,9 +93,8 @@ const FUTURES_VENUES = new Set(Object.values(ROOT_VENUES));
98
93
  */
99
94
  export function futuresListingVenue(symbol: string, exchange?: string | null): { venue: string; root: string | null } | null {
100
95
  const ticker = symbol.trim().toUpperCase();
101
- const named = (exchange ?? "").trim().toUpperCase();
102
- const venue = FUTURES_VENUES.has(named) ? named
103
- : Object.entries(VENUE_NAMES).find(([, names]) => names.includes(named))?.[0] ?? null;
96
+ const named = canonicalExchange(exchange ?? "");
97
+ const venue = FUTURES_VENUES.has(named) ? named : null;
104
98
  const coded = /^([A-Z0-9]{1,4}?)(?:=F|[FGHJKMNQUVXZ]\d{2}(?:\.[A-Z]+)?)$/.exec(ticker)?.[1]
105
99
  ?? /^([A-Z]{2})-Y\.[A-Z]+$/.exec(ticker)?.[1];
106
100
  const root = futuresGenericListing(ticker, venue ?? named)?.root ?? (coded ? ALIASES[coded] ?? coded : null);
@@ -29,20 +29,30 @@ export function isFxMarketOpen(time: number): boolean {
29
29
  return true;
30
30
  }
31
31
 
32
- /** The latest close of the FX week, Friday 17:00 New York, at or before `time`. */
33
- export function latestFxWeekClose(time: number): number | null {
32
+ /** The latest 17:00 New York on `weekday` (0 is Sunday) at or before `time`. */
33
+ function latestNewYorkFivePm(time: number, weekday: number): number | null {
34
34
  if (!Number.isFinite(time)) return null;
35
35
  const { year, month, day } = zonedDateTimeParts(time, NEW_YORK);
36
36
  const today = Date.UTC(year, month - 1, day);
37
37
  for (let offset = 0; offset <= 7; offset += 1) {
38
38
  const date = new Date(today - offset * DAY_MS);
39
- if (date.getUTCDay() !== 5) continue;
40
- const close = zonedWallClockToUtcMs(NEW_YORK, date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(), 17, 0, 0);
41
- if (close <= time) return close;
39
+ if (date.getUTCDay() !== weekday) continue;
40
+ const at = zonedWallClockToUtcMs(NEW_YORK, date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(), 17, 0, 0);
41
+ if (at <= time) return at;
42
42
  }
43
43
  return null;
44
44
  }
45
45
 
46
+ /** The latest close of the FX week, Friday 17:00 New York, at or before `time`. */
47
+ export function latestFxWeekClose(time: number): number | null {
48
+ return latestNewYorkFivePm(time, 5);
49
+ }
50
+
51
+ /** The latest open of the FX week, Sunday 17:00 New York, at or before `time`. */
52
+ export function latestFxWeekOpen(time: number): number | null {
53
+ return latestNewYorkFivePm(time, 0);
54
+ }
55
+
46
56
  /**
47
57
  * When the closure the market is in began (Friday 17:00 New York time); null
48
58
  * while it trades. That is `latestFxWeekClose` for every moment the week is
@@ -1,15 +1,21 @@
1
1
  import type { PricePoint, TickerFinancials } from "../types/financials";
2
- import { canonicalExchange, resolveExchangeTimeZone } from "./exchanges";
2
+ import { canonicalExchange } from "./exchanges";
3
3
  import { isRoundTheClockCoin } from "./crypto-pair";
4
- import { isCurrencyPairListing, latestFxWeekClose } from "./fx-market-hours";
5
- import { hasPublishedSessionCalendar, isRegularSessionTime, isTimestampStaleForExchangeSession, latestRegularSessionClose, latestRegularSessionOpen, sessionCalendarTimeZone } from "../market-data/market/freshness";
4
+ import { isCurrencyPairListing, isFxMarketOpen, latestFxWeekClose, latestFxWeekOpen } from "./fx-market-hours";
5
+ import { hasPublishedSessionCalendar, historySourceWeekendDays, latestRegularSessionClose, latestRegularSessionOpen, sessionCalendarTimeZone } from "../market-data/market/freshness";
6
6
  import { zonedDateTimeParts } from "./zoned-date-time";
7
7
  import { regularHistorySessionStaleness } from "../market-data/history-session";
8
8
  import type { HistorySession } from "../types/price-history";
9
9
  import { BAR_INTERVAL_UNIT_MS, parseBarInterval } from "../time-series/resolution";
10
10
 
11
- const MAX_CURRENT_INTRADAY_HISTORY_LAG_MS = 18 * 60 * 60 * 1000;
11
+ // A venue without session hours here (OTC Markets, Cboe Canada) has no
12
+ // session to hold: its copy is current while the latest bar is this recent.
13
+ const UNSCHEDULED_HISTORY_MAX_AGE_MS = 18 * 60 * 60 * 1000;
12
14
  const MAX_SAME_SESSION_HISTORY_LAG_MS = 30 * 60 * 1000;
15
+ // Sources finish the closing auction and late prints some minutes after the
16
+ // bell, past the delayed feed's lag. A copy fetched later can still carry an
17
+ // unfinished bar when its source had not settled it; only the source can tell.
18
+ const SESSION_BAR_SETTLE_MS = 30 * 60 * 1000;
13
19
  const DELAYED_HISTORY_ALLOWANCE_MS = 15 * 60 * 1000;
14
20
  const DAY_MS = 24 * 60 * 60 * 1000;
15
21
  // A series that never closes must reach the present without a break: no hole
@@ -22,12 +28,19 @@ interface PriceHistoryFreshnessOptions {
22
28
  exchange?: string;
23
29
  /**
24
30
  * The listing's symbol. A coin quoted in dollars (BTC-USD) trades around the
25
- * clock whether or not the exchange says CCC; without the symbol a bare
26
- * pair is read as a US listing and judged on the NYSE session.
31
+ * clock whether or not the exchange says CCC, and a currency pair (EURUSD=X)
32
+ * on the FX week; without the symbol a bare pair is read as a US listing and
33
+ * judged on the NYSE session.
27
34
  */
28
35
  symbol?: string;
29
36
  intervalMs?: number | null;
30
37
  session?: HistorySession;
38
+ /**
39
+ * When the copy was fetched. A copy fetched once its session had settled
40
+ * holds that session however early its last bar: a thin listing need not
41
+ * trade late. Without it, the bars must reach the close.
42
+ */
43
+ fetchedAt?: number;
31
44
  }
32
45
 
33
46
  /**
@@ -217,38 +230,84 @@ export function isPriceHistoryStaleForCurrentWindow(
217
230
  const age = now - latestTime;
218
231
  const allowedLag = allowedBarLagMs(intervalMs);
219
232
  if (age <= allowedLag) return false;
220
- const exchange = options.exchange || "NASDAQ";
221
- // Bars that reach the close of the venue's latest session stay current
222
- // once it has closed, so a closed market keeps its last session until the
223
- // next one opens. A copy taken earlier in that session is still behind.
224
- // Asked before the quote rule below, which reads any US bar dated before
225
- // today as behind from 04:00 ET: right for a last price, wrong for bars
226
- // waiting for the next session's first one.
227
- const regularSession = isRegularSessionTime(exchange, now);
228
- if (regularSession !== null) {
229
- const open = latestRegularSessionOpen(exchange, now);
230
- const close = latestRegularSessionClose(exchange, now)?.close;
231
- if (open !== null && close !== undefined && latestTime >= close - allowedLag) {
232
- if (!regularSession && close > open && latestTime >= open) return false;
233
- // After the next open they answer until its first delayed bar is due.
234
- if (regularSession && close < open && latestTime < open && now - open <= allowedLag) return false;
235
- }
236
- }
237
- if (
238
- resolveExchangeTimeZone(exchange)
239
- && isTimestampStaleForExchangeSession(latestTime, exchange, now)
240
- ) {
241
- return true;
233
+ const sessions = listingSessionsAt(now, options);
234
+ if (!sessions) return age > UNSCHEDULED_HISTORY_MAX_AGE_MS;
235
+ const fetchedAt = options.fetchedAt ?? Number.NEGATIVE_INFINITY;
236
+ if (holdsLatestSession(latestTime, now, sessions, allowedLag, fetchedAt)) return false;
237
+ // A venue whose chart source files another week (DFM's Sunday to Thursday)
238
+ // may end on the last session that week has: current under either week.
239
+ // Bars of a day that week lacks (a DFM Friday) answer to the venue's own.
240
+ const exchange = listingSessionExchange(options.exchange);
241
+ const sourceWeekendDays = historySourceWeekendDays(exchange);
242
+ const timeZone = sessionCalendarTimeZone(exchange);
243
+ const latestWeekday = timeZone ? zonedWeekday(latestTime, timeZone) : null;
244
+ if (!sourceWeekendDays || latestWeekday == null || sourceWeekendDays.includes(latestWeekday)) return true;
245
+ const sourceSessions = listingSessionsAt(now, options, sourceWeekendDays);
246
+ return !sourceSessions || !holdsLatestSession(latestTime, now, sourceSessions, allowedLag, fetchedAt);
247
+ }
248
+
249
+ /** Bare symbols resolve to their US listing at the sources. */
250
+ function listingSessionExchange(exchange: string | undefined): string {
251
+ return canonicalExchange(exchange) || "NYSE";
252
+ }
253
+
254
+ interface ListingSessions {
255
+ /** The open of the session trading at the time asked, if one is. */
256
+ liveOpen: number | null;
257
+ /** The latest session closed at or before that time. */
258
+ last: { open: number; close: number } | null;
259
+ }
260
+
261
+ /**
262
+ * The sessions a listing's bars are read against at `time`, from the venue's
263
+ * session tables; `weekendDays` replaces its days off. A currency pair's
264
+ * session is the FX week, Sunday to Friday 17:00 New York, whatever venue it
265
+ * is quoted on. Null for a venue without session hours.
266
+ */
267
+ function listingSessionsAt(
268
+ time: number,
269
+ options: Pick<PriceHistoryFreshnessOptions, "exchange" | "symbol">,
270
+ weekendDays?: readonly number[],
271
+ ): ListingSessions | null {
272
+ if (isCurrencyPairListing(options.symbol, options.exchange)) {
273
+ const close = latestFxWeekClose(time);
274
+ const open = close === null ? null : latestFxWeekOpen(close);
275
+ return { liveOpen: isFxMarketOpen(time) ? latestFxWeekOpen(time) : null,
276
+ last: close !== null && open !== null ? { open, close } : null };
242
277
  }
243
- const hasExchangeSession = Boolean(resolveExchangeTimeZone(exchange));
244
- if (age <= MAX_CURRENT_INTRADAY_HISTORY_LAG_MS) return hasExchangeSession;
245
- return !hasExchangeSession;
278
+ const exchange = listingSessionExchange(options.exchange);
279
+ const open = latestRegularSessionOpen(exchange, time, weekendDays);
280
+ if (open === null) return null;
281
+ const latest = latestRegularSessionClose(exchange, time, weekendDays);
282
+ const last = latest
283
+ ? { open: latestRegularSessionOpen(exchange, latest.close, weekendDays) ?? latest.close, close: latest.close }
284
+ : null;
285
+ return { liveOpen: !last || last.close < open ? open : null, last };
286
+ }
287
+
288
+ /**
289
+ * Whether bars ending at `latestTime`, older than one bar's lag, hold the
290
+ * listing's latest session. Between sessions that is the whole of the last
291
+ * one. While one trades, bars older than the lag are behind it, and the whole
292
+ * of the one before answers only until this one's first delayed bar is due.
293
+ * A session is held whole by bars from its open that reach its close less
294
+ * that lag, or by a copy fetched once it had settled. Weeknights, weekends
295
+ * and holidays read alike: the session tables say which session is the last.
296
+ */
297
+ function holdsLatestSession(
298
+ latestTime: number, now: number, sessions: ListingSessions, allowedLag: number, fetchedAt: number,
299
+ ): boolean {
300
+ const { liveOpen, last } = sessions;
301
+ if (liveOpen !== null && (latestTime >= liveOpen || now - liveOpen > allowedLag)) return false;
302
+ return !!last && latestTime >= last.open
303
+ && (latestTime >= last.close - allowedLag || fetchedAt >= last.close + SESSION_BAR_SETTLE_MS);
304
+ }
305
+
306
+ function zonedWeekday(time: number, timeZone: string): number {
307
+ const { year, month, day } = zonedDateTimeParts(time, timeZone);
308
+ return new Date(Date.UTC(year, month - 1, day)).getUTCDay();
246
309
  }
247
310
 
248
- // Sources finish the closing auction and late prints some minutes after the
249
- // bell, past the delayed feed's lag. A copy fetched later can still carry an
250
- // unfinished bar when its source had not settled it; only the source can tell.
251
- const SESSION_BAR_SETTLE_MS = 30 * 60 * 1000;
252
311
  const CRYPTO_BAR_MAX_AGE_MS = 60 * 60 * 1000;
253
312
  // A copy missing a settled session is asked again at most this often, and less
254
313
  // often as that close recedes, so a halted, delisted or late listing costs a
@@ -323,18 +382,16 @@ function isBarBeforeSession(latestTime: number, session: string, intervalMs: num
323
382
  * next has begun, a copy whose last bar stops short of the close by more than
324
383
  * a current copy's bars may lag is refetched. A copy fetched after the
325
384
  * 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.
385
+ * need not trade late. The sessions are those isPriceHistoryStaleForCurrentWindow
386
+ * reads (listingSessionsAt): a currency pair's is the FX week. Coins never
387
+ * close, and within the FX week neither do pairs:
388
+ * isPriceHistoryStaleForCurrentWindow holds those copies to the present.
329
389
  */
330
390
  function intradayHistoryFetchState(
331
391
  points: PricePoint[], fetchedAt: number, now: number, intervalMs: number, options: CalendarHistoryFetchOptions,
332
392
  ): CalendarHistoryFetchState {
333
393
  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;
394
+ const close = listingSessionsAt(now - SESSION_BAR_SETTLE_MS, options)?.last?.close ?? null;
338
395
  if (close === null || fetchedAt >= close + SESSION_BAR_SETTLE_MS) return "current";
339
396
  const latest = points.findLast(hasFiniteClose);
340
397
  const latestTime = latest ? getPricePointTimestamp(latest) : Number.NaN;
@@ -98,7 +98,8 @@ function splitEvents(proofs: Proof[]): SplitEvent[] {
98
98
  /**
99
99
  * Share bases belong to source accessions, not filing dates. A split edge needs
100
100
  * an explicit SEC split fact and unchanged same-period income with inverse EPS
101
- * restatement. Repeated EPS AND diluted shares connect otherwise equal bases;
101
+ * restatement. Repeated EPS AND diluted shares (at the coarser filing's
102
+ * rounding) connect otherwise equal bases;
102
103
  * where a filing tags no diluted shares, repeated EPS AND income do, if no
103
104
  * disclosed split could leave that EPS unchanged at cent rounding.
104
105
  * No price jump, ticker exception, or chronological assumption establishes one.
@@ -131,23 +132,30 @@ export function createSecEpsBasisResolver(payload: unknown): (fact: Fact) => { v
131
132
  graph.set(after, [...graph.get(after) ?? [], { node: before, factor: ratio, evidence }]);
132
133
  graph.set(before, [...graph.get(before) ?? [], { node: after, factor: 1 / ratio, evidence }]);
133
134
  };
134
- const identical = new Map<string, Row>();
135
135
  const periods = new Map<string, Row[]>();
136
136
  for (const row of eps.values()) {
137
137
  const period = `${row.start}:${row.end}`;
138
138
  periods.set(period, [...periods.get(period) ?? [], row]);
139
- const denominator = shares.get(key(row))?.val;
140
- if (!row.val || !denominator || denominator <= 0) continue;
141
- const identity = `${period}:${row.val}:${denominator}`;
142
- const prior = identical.get(identity);
143
- if (prior && prior.accn !== row.accn) connect(prior.accn, row.accn, 1);
144
- else identical.set(identity, row);
145
139
  }
140
+ const ratios = [...new Set(splits.map((split) => split.val!))];
141
+ // A filing rounds its share counts to one unit, the largest power of ten
142
+ // dividing every diluted count it reports (Tesla gave thousands until 2019
143
+ // and millions after). Two counts are one basis when the finer one, rounded
144
+ // to the coarser filing's unit, gives the coarser one, and every disclosed
145
+ // split would move a count by more than that unit. Counts at exact share
146
+ // precision must be equal.
147
+ const unit = new Map<string, number>();
148
+ for (const row of entries(payload, "WeightedAverageNumberOfDilutedSharesOutstanding", "shares")) {
149
+ let power = 1;
150
+ while (row.val! > 0 && row.val! % (power * 10) === 0) power *= 10;
151
+ unit.set(row.accn!, Math.min(unit.get(row.accn!) ?? power, power));
152
+ }
153
+ const sameShares = (a: number, b: number, coarser: number) => a === b || coarser > 1 && Math.abs(a - b) * 2 <= coarser
154
+ && ratios.every((ratio) => Math.abs(a - a / ratio) > coarser && Math.abs(a - a * ratio) > coarser);
146
155
  // Not every filer tags diluted shares (Alphabet before 2024). Where a filing
147
156
  // tags none for the period, a repeated EPS and net income connect instead,
148
157
  // when no disclosed split ratio could leave that EPS unchanged at cent
149
158
  // rounding. Diluted shares that differ still keep the bases apart.
150
- const ratios = [...new Set(splits.map((split) => split.val!))];
151
159
  const telling = (value: number) => ratios.every((ratio) => Math.abs(value - value / ratio) > .005 + .005 / ratio
152
160
  && Math.abs(value - value * ratio) > .005 + .005 * ratio);
153
161
  const tagged = new Set(entries(payload, "WeightedAverageNumberOfDilutedSharesOutstanding", "shares").map(key));
@@ -155,8 +163,13 @@ export function createSecEpsBasisResolver(payload: unknown): (fact: Fact) => { v
155
163
  for (let i = 0; i < rows.length; i++) {
156
164
  for (let j = i + 1; j < rows.length; j++) {
157
165
  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;
166
+ if (a.accn === b.accn || a.val !== b.val || !a.val) continue;
167
+ const [sharesA, sharesB] = [shares.get(key(a))?.val, shares.get(key(b))?.val];
168
+ if (sharesA && sharesB && sharesA > 0 && sharesB > 0) {
169
+ if (sameShares(sharesA, sharesB, Math.max(unit.get(a.accn) ?? 1, unit.get(b.accn) ?? 1))) connect(a.accn, b.accn, 1);
170
+ continue;
171
+ }
172
+ if (!telling(a.val) || tagged.has(key(a)) && tagged.has(key(b))) continue;
160
173
  const repeated = income.get(key(a))?.val;
161
174
  if (repeated !== undefined && repeated !== 0 && repeated === income.get(key(b))?.val) connect(a.accn, b.accn, 1);
162
175
  }