gloomberb 0.18.0 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (365) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/commodity-balances.ts +6 -0
  3. package/src/api-client/company-kpis.ts +9 -0
  4. package/src/api-client/crypto-markets.ts +9 -0
  5. package/src/api-client/data.ts +6 -5
  6. package/src/api-client/equity-screener.ts +2 -0
  7. package/src/api-client/market-movers.ts +7 -0
  8. package/src/api-client/types.ts +14 -0
  9. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  10. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  11. package/src/app/runtime/plugin-bindings.ts +6 -2
  12. package/src/brokers/logos.ts +66 -0
  13. package/src/brokers/profile-form.ts +3 -0
  14. package/src/brokers/remote-broker-adapter.ts +13 -3
  15. package/src/capabilities/index.ts +1 -1
  16. package/src/capabilities/service-capabilities.ts +7 -1
  17. package/src/cli/base-converter.ts +60 -18
  18. package/src/cli/commands/fred-series.ts +15 -5
  19. package/src/cli/commands/market.ts +290 -159
  20. package/src/cli/commands/news-search.ts +35 -8
  21. package/src/cli/commands/overview.ts +31 -6
  22. package/src/cli/commands/system.ts +35 -1
  23. package/src/cli/commands/ticker.ts +105 -52
  24. package/src/cli/currency-hints.ts +27 -0
  25. package/src/cli/fx-pair.ts +41 -4
  26. package/src/cli/glossary.ts +57 -4
  27. package/src/cli/helpers.ts +36 -3
  28. package/src/cli/index.ts +4 -1
  29. package/src/cli/known-gaps.ts +37 -1
  30. package/src/cli/listing-arg.ts +37 -2
  31. package/src/cli/pane-functions/catalog.ts +3 -1
  32. package/src/cli/pane-functions/discovery.ts +15 -1
  33. package/src/cli/pane-functions/dom.ts +9 -4
  34. package/src/cli/pane-functions/freshness.ts +4 -4
  35. package/src/cli/pane-functions/function-help.ts +31 -7
  36. package/src/cli/pane-functions/headless.ts +6 -5
  37. package/src/cli/pane-functions/index.ts +3 -2
  38. package/src/cli/pane-functions/lock-note.ts +7 -4
  39. package/src/cli/pane-functions/resolver.ts +2 -1
  40. package/src/cli/quote-answer.ts +179 -0
  41. package/src/cli/series-search.ts +2 -1
  42. package/src/cli/symbol-list.ts +51 -16
  43. package/src/cli/types.ts +4 -0
  44. package/src/components/chart/composite/composite-chart.tsx +22 -7
  45. package/src/components/chart/composite/format.ts +13 -1
  46. package/src/components/chart/composite/level-labels.ts +30 -0
  47. package/src/components/chart/composite/levels.ts +17 -6
  48. package/src/components/chart/composite/panel-surface.tsx +64 -16
  49. package/src/components/chart/composite/price-axis-labels.tsx +2 -1
  50. package/src/components/chart/composite/types.ts +4 -0
  51. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  52. package/src/components/command-bar/workflow/collection-actions.ts +4 -0
  53. package/src/components/index.ts +2 -0
  54. package/src/components/layout/pane/content.tsx +16 -14
  55. package/src/components/layout/pane/footer/index.tsx +22 -3
  56. package/src/components/layout/pane/footer/model.ts +99 -4
  57. package/src/components/layout/pane/header-tabs.tsx +15 -1
  58. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  59. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  60. package/src/components/onboarding/wizard-model.ts +10 -3
  61. package/src/components/onboarding/wizard-positions.ts +8 -0
  62. package/src/components/ui/button.tsx +11 -4
  63. package/src/components/ui/choice-dialog.tsx +8 -5
  64. package/src/components/ui/display.tsx +11 -6
  65. package/src/components/ui/fields.tsx +100 -20
  66. package/src/components/ui/icon.tsx +6 -1
  67. package/src/components/ui/list-row-lines.tsx +87 -0
  68. package/src/components/ui/list-view.tsx +14 -0
  69. package/src/components/ui/logo-mark.tsx +64 -0
  70. package/src/components/ui/number-step.ts +54 -0
  71. package/src/components/ui/select-button.tsx +5 -4
  72. package/src/components/ui/select-field.tsx +3 -3
  73. package/src/components/ui/status.tsx +17 -3
  74. package/src/components/ui/tabs.tsx +9 -1
  75. package/src/components/ui/toggle.tsx +17 -3
  76. package/src/core/state/app/instrument.ts +8 -3
  77. package/src/data/config/store/normalize.ts +4 -1
  78. package/src/data/plugin-cache.ts +32 -6
  79. package/src/docs/snapshot.json +57 -22
  80. package/src/i18n/es.ts +9 -3
  81. package/src/i18n/ja.ts +9 -3
  82. package/src/i18n/ko.ts +9 -3
  83. package/src/i18n/zh-cn.ts +10 -4
  84. package/src/i18n/zh-tw.ts +10 -4
  85. package/src/market-data/commodity-names.ts +80 -0
  86. package/src/market-data/coordinator/auxiliary.ts +3 -1
  87. package/src/market-data/coordinator/chart.ts +3 -1
  88. package/src/market-data/futures-board.ts +38 -1
  89. package/src/market-data/futures-contract.ts +91 -0
  90. package/src/market-data/fx-closes.ts +78 -1
  91. package/src/market-data/market/change-basis.ts +219 -0
  92. package/src/market-data/market/format.ts +28 -12
  93. package/src/market-data/market/freshness.ts +38 -0
  94. package/src/market-data/market/status.ts +55 -9
  95. package/src/market-data/market/year-to-date.ts +46 -0
  96. package/src/market-data/quotes/contributions.ts +12 -0
  97. package/src/market-data/quotes/delisting.ts +34 -0
  98. package/src/market-data/quotes/live-valuation.ts +23 -0
  99. package/src/market-data/quotes/resolution.ts +8 -2
  100. package/src/market-data/tax-equivalent.ts +10 -2
  101. package/src/notifications/app-notifier.ts +2 -2
  102. package/src/notifications/notification-log.ts +8 -6
  103. package/src/plugins/builtin/alerts/cli.ts +26 -18
  104. package/src/plugins/builtin/alerts/headless.ts +1 -1
  105. package/src/plugins/builtin/alerts/index.tsx +13 -4
  106. package/src/plugins/builtin/analytics/index.tsx +1 -1
  107. package/src/plugins/builtin/analytics/risk-client.ts +44 -11
  108. package/src/plugins/builtin/analytics/risk-fx.ts +61 -0
  109. package/src/plugins/builtin/analytics/risk-headless.ts +10 -1
  110. package/src/plugins/builtin/analytics/risk-model.ts +79 -16
  111. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  112. package/src/plugins/builtin/analytics/risk-test-data.ts +3 -0
  113. package/src/plugins/builtin/attention/trending.tsx +8 -6
  114. package/src/plugins/builtin/backtest/client.ts +5 -2
  115. package/src/plugins/builtin/backtest/window.ts +16 -3
  116. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  117. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  118. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  119. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  120. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  121. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  122. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  123. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  124. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  125. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  126. package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
  127. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  128. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  129. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  130. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  131. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  132. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  133. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  134. package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
  135. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  136. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  137. package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
  138. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  139. package/src/plugins/builtin/buildout/format.ts +1 -0
  140. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  141. package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
  142. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  143. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  144. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  145. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  146. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  147. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  148. package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
  149. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  150. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  151. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  152. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  153. package/src/plugins/builtin/chat/index.tsx +1 -1
  154. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  155. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  156. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  157. package/src/plugins/builtin/cloud/team/pane-sections.tsx +10 -3
  158. package/src/plugins/builtin/cloud/team/pane.tsx +30 -9
  159. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  160. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  161. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  162. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  163. package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
  164. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  165. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  166. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  167. package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
  168. package/src/plugins/builtin/composite-plugins.ts +3 -2
  169. package/src/plugins/builtin/cot/headless.ts +1 -1
  170. package/src/plugins/builtin/cot/index.tsx +1 -1
  171. package/src/plugins/builtin/cot/model.ts +35 -12
  172. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  173. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  174. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  175. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  176. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  177. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  178. package/src/plugins/builtin/custom-view/loader.ts +3 -2
  179. package/src/plugins/builtin/custom-view/pane.tsx +5 -4
  180. package/src/plugins/builtin/custom-view/view-spec.ts +1 -0
  181. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  182. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  183. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  184. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  185. package/src/plugins/builtin/econ/calendar-model.ts +38 -2
  186. package/src/plugins/builtin/econ/fred-catalog.ts +6 -2
  187. package/src/plugins/builtin/econ/fred-series-map.ts +12 -1
  188. package/src/plugins/builtin/econ/index.tsx +31 -4
  189. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  190. package/src/plugins/builtin/equity-screener/headless.ts +20 -5
  191. package/src/plugins/builtin/equity-screener/model.ts +40 -2
  192. package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
  193. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  194. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  195. package/src/plugins/builtin/executives/data.ts +24 -8
  196. package/src/plugins/builtin/executives/index.tsx +2 -18
  197. package/src/plugins/builtin/executives/pane.tsx +34 -10
  198. package/src/plugins/builtin/exposure/client.ts +4 -1
  199. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  200. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  201. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  202. package/src/plugins/builtin/exposure/pane.tsx +37 -16
  203. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  204. package/src/plugins/builtin/futures/table.tsx +6 -3
  205. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  206. package/src/plugins/builtin/insider/base-values.ts +72 -0
  207. package/src/plugins/builtin/insider/headless.ts +29 -4
  208. package/src/plugins/builtin/jobs/headless.ts +2 -2
  209. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  210. package/src/plugins/builtin/market-halts/model.ts +31 -6
  211. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  212. package/src/plugins/builtin/market-movers/client.ts +2 -2
  213. package/src/plugins/builtin/market-movers/index.tsx +0 -3
  214. package/src/plugins/builtin/market-movers/session-body.tsx +27 -11
  215. package/src/plugins/builtin/market-movers/session.ts +25 -10
  216. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  217. package/src/plugins/builtin/members/pane.tsx +8 -3
  218. package/src/plugins/builtin/news/wire/feed-config.ts +33 -13
  219. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  220. package/src/plugins/builtin/news/wire/letters/client.ts +1 -0
  221. package/src/plugins/builtin/news/wire/news/command-bar-search.ts +6 -3
  222. package/src/plugins/builtin/news/wire/news/detail-view.tsx +2 -1
  223. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  224. package/src/plugins/builtin/news/wire/news/table.tsx +2 -1
  225. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  226. package/src/plugins/builtin/news/wire/rss/feed-xml.ts +6 -1
  227. package/src/plugins/builtin/news/wire/rss/managed-feed.ts +1 -0
  228. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  229. package/src/plugins/builtin/notes/cli.ts +33 -8
  230. package/src/plugins/builtin/notes/export.ts +49 -3
  231. package/src/plugins/builtin/notes/files.ts +14 -3
  232. package/src/plugins/builtin/notes/index.tsx +3 -3
  233. package/src/plugins/builtin/notes/quick-notes-pane.tsx +45 -10
  234. package/src/plugins/builtin/notes/store.ts +13 -3
  235. package/src/plugins/builtin/options/footer.ts +25 -20
  236. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  237. package/src/plugins/builtin/options/view.tsx +77 -22
  238. package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
  239. package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
  240. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  241. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -7
  242. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +166 -15
  243. package/src/plugins/builtin/portfolio-list/cli/render.ts +69 -15
  244. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +2 -2
  245. package/src/plugins/builtin/portfolio-list/headless.ts +3 -1
  246. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  247. package/src/plugins/builtin/portfolio-list/pane/index.tsx +44 -17
  248. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  249. package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -1
  250. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +42 -7
  251. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  252. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  253. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  254. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  255. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  256. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  257. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  258. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  259. package/src/plugins/builtin/scanner/feed.ts +6 -1
  260. package/src/plugins/builtin/scanner/headless.ts +1 -1
  261. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  262. package/src/plugins/builtin/series-inventory.ts +1 -0
  263. package/src/plugins/builtin/shared/broker-reconnect.ts +105 -0
  264. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  265. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  266. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  267. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  268. package/src/plugins/builtin/shared/pro-feature-wall.tsx +9 -4
  269. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  270. package/src/plugins/builtin/shared/research-trading.tsx +62 -0
  271. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  272. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  273. package/src/plugins/builtin/themes/headless.ts +8 -2
  274. package/src/plugins/builtin/themes/model.ts +43 -10
  275. package/src/plugins/builtin/themes/pane.tsx +8 -6
  276. package/src/plugins/builtin/thirteenf/api.ts +4 -1
  277. package/src/plugins/builtin/thirteenf/data.ts +5 -1
  278. package/src/plugins/builtin/thirteenf/headless.ts +3 -1
  279. package/src/plugins/builtin/thirteenf/pane.tsx +8 -3
  280. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  281. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  282. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  283. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  284. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  285. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  286. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  287. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  288. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  289. package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
  290. package/src/plugins/builtin-editorial.ts +1 -1
  291. package/src/plugins/compat.ts +3 -0
  292. package/src/plugins/external-runtime.ts +5 -0
  293. package/src/plugins/loader.ts +6 -0
  294. package/src/plugins/plugin-icon.ts +61 -0
  295. package/src/plugins/registry/host-actions.ts +2 -1
  296. package/src/plugins/registry/index.ts +9 -1
  297. package/src/plugins/runtime/context.tsx +5 -0
  298. package/src/plugins/runtime/index.tsx +3 -0
  299. package/src/portfolio/column-values.ts +13 -4
  300. package/src/portfolio/cost-fx.ts +61 -0
  301. package/src/portfolio/mutations.ts +16 -2
  302. package/src/portfolio/position-metrics.ts +45 -4
  303. package/src/portfolio/position-quantity.ts +43 -0
  304. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  305. package/src/public/broker.ts +8 -0
  306. package/src/public/market-data.ts +5 -0
  307. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  308. package/src/renderers/dom/desktop/controls.tsx +39 -7
  309. package/src/renderers/dom/desktop/icons.tsx +14 -0
  310. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  311. package/src/renderers/dom/desktop/menu.tsx +10 -1
  312. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  313. package/src/renderers/dom/host/style.ts +6 -1
  314. package/src/renderers/dom/host/tabs.tsx +6 -0
  315. package/src/renderers/dom/styles.css +12 -5
  316. package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
  317. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  318. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  319. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  320. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  321. package/src/series/validate.ts +2 -2
  322. package/src/sources/gloomberb-cloud/fred-series.ts +27 -2
  323. package/src/sources/gloomberb-cloud/index.ts +50 -17
  324. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  325. package/src/sources/history-result.ts +3 -0
  326. package/src/sources/provider-errors.ts +3 -1
  327. package/src/sources/provider-router/history.ts +49 -6
  328. package/src/sources/provider-router/index.ts +2 -1
  329. package/src/state/app/activity.ts +13 -0
  330. package/src/sync/controller.ts +4 -0
  331. package/src/sync/core-contributors.ts +63 -11
  332. package/src/sync/local-only.ts +121 -0
  333. package/src/sync/react.ts +6 -0
  334. package/src/sync/types.ts +3 -0
  335. package/src/team/collections.ts +2 -0
  336. package/src/tickers/listing-fate.ts +27 -8
  337. package/src/tickers/metadata.ts +10 -0
  338. package/src/tickers/search/index.ts +24 -3
  339. package/src/time-series/price-comparison.ts +35 -0
  340. package/src/time-series/resolve.ts +52 -11
  341. package/src/time-series/spec.ts +1 -1
  342. package/src/time-series/types.ts +5 -0
  343. package/src/types/broker.ts +56 -1
  344. package/src/types/config.ts +6 -0
  345. package/src/types/data-provider.ts +5 -1
  346. package/src/types/financials.ts +23 -0
  347. package/src/types/headless.ts +12 -2
  348. package/src/types/instrument.ts +4 -0
  349. package/src/types/plugin.ts +24 -6
  350. package/src/types/price-history.ts +6 -1
  351. package/src/types/ticker.ts +9 -0
  352. package/src/types/trading.ts +67 -0
  353. package/src/ui/host.tsx +4 -0
  354. package/src/utils/base-currency.ts +108 -0
  355. package/src/utils/currency-names.ts +128 -0
  356. package/src/utils/currency-pair.ts +2 -1
  357. package/src/utils/currency-units.ts +41 -1
  358. package/src/utils/dom-screenshot.ts +22 -9
  359. package/src/utils/exchange-notation.generated.ts +360 -0
  360. package/src/utils/exchanges.ts +15 -322
  361. package/src/utils/format.ts +31 -1
  362. package/src/utils/instrument-identity.ts +38 -1
  363. package/src/utils/options.ts +46 -1
  364. package/src/utils/price-history.ts +4 -0
  365. package/src/utils/reported-money.ts +8 -4
@@ -0,0 +1,72 @@
1
+ import type { HeadlessPaneColumn, HeadlessPaneRow } from "../../../types/headless";
2
+ import { fxCloseOnOrBefore, loadDailyFxCloses, type FxCloses, type FxHistoryClient } from "../../../market-data/fx-closes";
3
+ import { logFailure } from "../../../utils/log-failure";
4
+ import { formatCompact, formatNumber } from "../../../utils/format";
5
+
6
+ /** A Form 4 states its prices and values in US dollars. */
7
+ export const FORM4_CURRENCY = "USD";
8
+
9
+ /** The rows with their currency, and for a base currency other than USD each value in it, with the columns and note that say so. */
10
+ export interface InsiderBaseValues {
11
+ rows: HeadlessPaneRow[];
12
+ /** The money columns labelled with their currency, and the converted value and its rate; null for a USD base, whose columns stay as they are. */
13
+ columns: HeadlessPaneColumn[] | null;
14
+ note: string | null;
15
+ }
16
+
17
+ const dash = (value: unknown, format: (value: number) => string) => (typeof value === "number" && Number.isFinite(value) ? format(value) : "-");
18
+
19
+ /**
20
+ * Every row carries the currency its price and value are in (USD, as a Form 4
21
+ * states them). With another base currency (CAD), each value is also given
22
+ * in it at the daily close of USD/CAD on its transaction day, or the last
23
+ * close before a weekend or holiday (`fxCloseOnOrBefore` over the history
24
+ * PORT and PEB read), with the rate and that close's date beside it. When the
25
+ * closes cannot be read the values stay in USD and the note says why.
26
+ */
27
+ export async function withInsiderBaseValues(
28
+ rows: readonly HeadlessPaneRow[],
29
+ baseCurrency: string,
30
+ client: FxHistoryClient,
31
+ now = new Date(),
32
+ ): Promise<InsiderBaseValues> {
33
+ const labelled: HeadlessPaneRow[] = rows.map((row) => ({ ...row, currency: FORM4_CURRENCY }));
34
+ const base = baseCurrency.trim().toUpperCase();
35
+ if (!base || base === FORM4_CURRENCY) return { rows: labelled, columns: null, note: null };
36
+ const pair = `${FORM4_CURRENCY}/${base}`;
37
+ const days = labelled.flatMap((row) => typeof row.transactionDate === "string" && typeof row.totalValue === "number"
38
+ ? [row.transactionDate.slice(0, 10)] : []).sort();
39
+ let closes: FxCloses | null = null;
40
+ if (days.length > 0) {
41
+ try {
42
+ closes = await loadDailyFxCloses(base, days[0]!, client, now);
43
+ } catch (error) {
44
+ logFailure("insider.base-values", `Daily ${pair} closes failed`)(error);
45
+ }
46
+ }
47
+ const converted = labelled.map((row) => {
48
+ const day = typeof row.transactionDate === "string" ? row.transactionDate.slice(0, 10) : null;
49
+ const reading = closes && day && typeof row.totalValue === "number" ? fxCloseOnOrBefore(closes, day) : null;
50
+ // The close is USD per unit of the base currency; the rate is the base currency per dollar.
51
+ const fxRate = reading ? 1 / reading.rate : null;
52
+ return {
53
+ ...row,
54
+ baseCurrency: base,
55
+ valueBase: fxRate != null ? (row.totalValue as number) * fxRate : null,
56
+ fxRate,
57
+ fxDate: reading?.date ?? null,
58
+ };
59
+ });
60
+ const columns: HeadlessPaneColumn[] = [
61
+ { key: "pricePerShare", header: `Price (${FORM4_CURRENCY})`, align: "right", format: (value) => dash(value, (price) => formatNumber(price, 2)) },
62
+ { key: "totalValue", header: `Value (${FORM4_CURRENCY})`, align: "right", format: (value) => dash(value, formatCompact) },
63
+ ...closes ? [
64
+ { key: "valueBase", header: `Value (${base})`, align: "right" as const, format: (value: unknown) => dash(value, formatCompact) },
65
+ { key: "fxRate", header: pair, align: "right" as const, stackPriority: 1, format: (value: unknown) => dash(value, (rate) => formatNumber(rate, 4)) },
66
+ ] : [],
67
+ ];
68
+ const note = days.length === 0 ? null
69
+ : closes ? `Value (${base}) is each value at the ${pair} daily close of its transaction day, or of the trading day before it; ${pair} is that rate.`
70
+ : `Daily ${pair} closes are unavailable, so values stay in ${FORM4_CURRENCY}.`;
71
+ return { rows: closes ? converted : labelled, columns, note };
72
+ }
@@ -8,6 +8,7 @@ import type {
8
8
  import { formatCompact, formatCurrency } from "../../../utils/format";
9
9
  import { loadParsedInsiderFilings, type InsiderFilingsLoad } from "./client";
10
10
  import { insiderEmptyNotice } from "./issuer";
11
+ import { FORM4_CURRENCY, withInsiderBaseValues } from "./base-values";
11
12
  import {
12
13
  buildInsiderRows,
13
14
  buildInsiderSummary,
@@ -58,6 +59,7 @@ import {
58
59
  insiderUsualRatio,
59
60
  } from "./signals-model";
60
61
  import { REPORT_ROWS } from "../shared/headless-options";
62
+ import type { FxHistoryClient } from "../../../market-data/fx-closes";
61
63
 
62
64
  const INSIDER_COLUMNS: HeadlessPaneColumn[] = [
63
65
  { key: "form", header: "Form" },
@@ -106,6 +108,20 @@ const INSIDER_COLUMNS: HeadlessPaneColumn[] = [
106
108
  { key: "note", header: "Note", underRow: true, format: (value) => typeof value === "string" ? value : "" },
107
109
  ];
108
110
 
111
+ /** With a base currency other than USD, the dollar columns say they are in USD ("Bought (USD)"), as $ alone is several currencies' sign. */
112
+ function labelDollarColumns(columns: HeadlessPaneColumn[], keys: readonly string[], baseCurrency: string): HeadlessPaneColumn[] {
113
+ if (baseCurrency.trim().toUpperCase() === FORM4_CURRENCY) return columns;
114
+ return columns.map((column) => keys.includes(column.key) ? { ...column, header: `${column.header} (${FORM4_CURRENCY})` } : column);
115
+ }
116
+
117
+ /** The trades' columns with the money ones labelled by currency and the converted value and rate after them (`withInsiderBaseValues`). */
118
+ function withMoneyColumns(money: readonly HeadlessPaneColumn[]): HeadlessPaneColumn[] {
119
+ const byKey = new Map(money.map((column) => [column.key, column]));
120
+ return INSIDER_COLUMNS.flatMap((column) => column.key === "totalValue"
121
+ ? money.filter((entry) => entry.key !== "pricePerShare")
122
+ : [byKey.get(column.key) ?? column]);
123
+ }
124
+
109
125
  const figureOrDash = (value: unknown) => (typeof value === "number" ? value : null);
110
126
 
111
127
  /**
@@ -183,6 +199,7 @@ function insidersRows(activity: InsiderCompanyActivity, side: InsiderSignalSide)
183
199
  stakeBothWays: line.buyer.twoWay === true,
184
200
  bought: line.bought,
185
201
  sold: line.sold,
202
+ currency: FORM4_CURRENCY,
186
203
  shares: line.buyer.shares ?? null,
187
204
  last: line.buyer.lastBuy,
188
205
  }));
@@ -211,6 +228,7 @@ function signalRows(payload: InsiderSignalsPayload): HeadlessPaneRow[] {
211
228
  insiders: row.insiders,
212
229
  buys: row.buys,
213
230
  value: row.value,
231
+ currency: FORM4_CURRENCY,
214
232
  shares: row.shares,
215
233
  firstBuy: row.firstBuy,
216
234
  lastBuy: row.lastBuy,
@@ -251,6 +269,8 @@ function amendmentNote(entry: ParsedInsiderFiling): string {
251
269
  }
252
270
 
253
271
  export interface InsiderHeadlessDependencies {
272
+ /** Daily FX closes for values in a base currency other than USD; the report's API client unless a test stands in. */
273
+ fxHistory?: FxHistoryClient;
254
274
  loadSignals?(query: InsiderSignalsQuery, limit: number, ctx: HeadlessPaneContext): Promise<InsiderSignalsPayload>;
255
275
  loadCompany?(query: InsiderCompanyQuery, ctx: HeadlessPaneContext): Promise<InsiderCompanyActivity>;
256
276
  loadParsed(
@@ -441,9 +461,9 @@ export function createInsiderHeadless(
441
461
  const payload = await (dependencies.loadSignals ?? defaultDependencies.loadSignals!)(query, Number(args.options.limit), ctx);
442
462
  const side = payload.side ?? "buy";
443
463
  return {
444
- columns: signalColumns(side),
464
+ columns: labelDollarColumns(signalColumns(side), ["value"], ctx.config.baseCurrency),
445
465
  // The exports carry each row's insiders; the text leaves them to fn INS <ticker> --view insiders.
446
- textColumns: signalColumns(side).filter((column) => column.key !== "buyers"),
466
+ textColumns: labelDollarColumns(signalColumns(side).filter((column) => column.key !== "buyers"), ["value"], ctx.config.baseCurrency),
447
467
  rows: signalRows(payload),
448
468
  ...(payload.rows.length === 0 ? { empty: insiderSignalsEmptyTitle(payload.window, side, insiderSignalsFiltered(query)) } : {}),
449
469
  metadata: {
@@ -472,7 +492,7 @@ export function createInsiderHeadless(
472
492
  .filter((notice): notice is string => !!notice))];
473
493
  const rows = insidersRows(activity, side);
474
494
  return {
475
- columns: INSIDERS_COLUMNS,
495
+ columns: labelDollarColumns(INSIDERS_COLUMNS, ["bought", "sold"], ctx.config.baseCurrency),
476
496
  rows,
477
497
  ...(rows.length === 0 ? { empty: insiderActivityEmptyTitle(window) } : {}),
478
498
  metadata: {
@@ -503,12 +523,17 @@ export function createInsiderHeadless(
503
523
  .filter((row) => row.status !== "parsed" && row.status !== "disclosure")
504
524
  .map((row) => row.accessionNumber))];
505
525
  // A filing's lines sit together, so its note goes once, under the last of them.
506
- const rows: HeadlessPaneRow[] = buildInsiderRows(filtered, parsed).map((row, index) => {
526
+ const filed: HeadlessPaneRow[] = buildInsiderRows(filtered, parsed).map((row, index) => {
507
527
  const entry = filtered[index]!;
508
528
  const last = filtered[index + 1]?.filing.accessionNumber !== entry.filing.accessionNumber;
509
529
  return isAmendedInsiderFiling(entry) && last ? { ...row, note: amendmentNote(entry) || null } : row;
510
530
  });
531
+ // Every value says its currency; a base currency other than USD gets each value converted beside it.
532
+ const based = await withInsiderBaseValues(filed, ctx.config.baseCurrency, dependencies.fxHistory ?? ctx.apiClient);
533
+ const rows = based.rows;
511
534
  return {
535
+ ...(based.columns ? { columns: withMoneyColumns(based.columns) } : {}),
536
+ ...(based.note ? { notes: [based.note] } : {}),
512
537
  rows,
513
538
  // Nothing at all: the company has no Form 4 of its own, which is the whole answer.
514
539
  ...(empty ? { empty, unavailableSymbols: [] } : {}),
@@ -183,11 +183,11 @@ export const jobsHeadless: HeadlessPaneDefinition<"bundle"> = {
183
183
  const symbol = args.symbols[0];
184
184
  if (symbol) {
185
185
  const state = await fetchJobs(symbol, { force: true, client: ctx.apiClient as JobsCloudClient });
186
- if (state.kind === "denied") throw explainRefusal(new ApiRequestError(state.message, state.status), jobsLockNote, { symbol });
186
+ if (state.kind === "denied") throw await explainRefusal(new ApiRequestError(state.message, state.status), jobsLockNote, { symbol, context: ctx });
187
187
  return companyBundle(symbol, state, limit);
188
188
  }
189
189
  const payload = await fetchJobsMovers(ctx.apiClient as JobsCloudClient)
190
- .catch((error: unknown) => { throw explainRefusal(error, jobsLockNote, { symbol: null }); });
190
+ .catch(async (error: unknown) => { throw await explainRefusal(error, jobsLockNote, { symbol: null }); });
191
191
  const rows = buildMoverRows(payload.movers).slice(0, Math.max(limit, 15)).map((row) => ({
192
192
  ticker: row.ticker,
193
193
  company: row.company,
@@ -1,16 +1,27 @@
1
1
  import type { PaneLock } from "../../../types/plugin";
2
- import { proLockNote } from "../shared/pro-lock-note";
2
+ import type { HiringPayload } from "../../../api-client/hiring";
3
+ import { fetchAttention } from "../company-attention/client";
4
+ import { proLockNote, type FreeRoute } from "../shared/pro-lock-note";
5
+
6
+ /** HIRE's preview, for a company whose careers system it reads: Coffee Holding (JVA) is mapped but uncovered. */
7
+ const HIRE_ROUTE: FreeRoute = {
8
+ text: (ticker, company) => `gloomberb fn HIRE ${ticker} previews ${company}'s hiring with its latest roles, without the full history.`,
9
+ answers: async (symbol, context) => {
10
+ const data = await fetchAttention("hiring", symbol, { limit: 1 }, context.apiClient, context.signal) as HiringPayload;
11
+ return data.evidence.length + data.series.length + data.functions.length > 0;
12
+ },
13
+ };
3
14
 
4
15
  /**
5
16
  * JOBS is Pro whole. HIRE reads the same careers systems and needs no
6
17
  * account: a company's latest roles, without the full history.
7
18
  */
8
- export function jobsLockNote(lock: PaneLock): string {
19
+ export function jobsLockNote(lock: PaneLock): Promise<string> {
9
20
  return proLockNote(lock, {
10
21
  token: "JOBS",
11
22
  argument: "optional",
12
23
  does: "it reads each company's careers system into open roles, hiring pace and the mix by function and country",
13
24
  shows: "it",
14
- free: (ticker, company) => `gloomberb fn HIRE ${ticker} previews ${company}'s hiring with its latest roles, without the full history.`,
25
+ free: [HIRE_ROUTE],
15
26
  });
16
27
  }
@@ -180,14 +180,33 @@ const DAY_MS = 86_400_000;
180
180
  /** Unresolved halts move out of the recent views at 30 elapsed days. */
181
181
  const LONG_TERM_MS = 30 * DAY_MS;
182
182
 
183
- export function formatHaltAge(record: HaltRecord, now: number): string {
183
+ /** How long a halt has kept trading stopped: to the trade resumption, or to now while it lasts. Quotes resuming do not end it. */
184
+ function haltDurationMs(record: HaltRecord, now: number): number {
184
185
  const end = record.tradeResumeAt == null ? now : Math.min(now, record.tradeResumeAt);
185
- const elapsed = Math.max(0, end - record.haltedAt);
186
+ return Math.max(0, end - record.haltedAt);
187
+ }
188
+
189
+ /** How long since the halt began, whether or not trading has resumed. */
190
+ function haltAgeMs(record: HaltRecord, now: number): number {
191
+ return Math.max(0, now - record.haltedAt);
192
+ }
193
+
194
+ function formatElapsed(elapsed: number): string {
186
195
  if (elapsed >= DAY_MS) return `${Math.floor(elapsed / DAY_MS)}d`;
187
196
  if (elapsed >= 3_600_000) return `${Math.floor(elapsed / 3_600_000)}h`;
188
197
  return `${Math.floor(elapsed / 60_000)}m`;
189
198
  }
190
199
 
200
+ /** HALTED FOR: RAIL halted 16:40 and resumed 17:15 was halted for 35m, read at any later time. */
201
+ export function formatHaltDuration(record: HaltRecord, now: number): string {
202
+ return formatElapsed(haltDurationMs(record, now));
203
+ }
204
+
205
+ /** AGE: RAIL's 16:40 halt is 1h old at 17:45, though it lasted 35m. */
206
+ export function formatHaltAge(record: HaltRecord, now: number): string {
207
+ return formatElapsed(haltAgeMs(record, now));
208
+ }
209
+
191
210
  export const HALT_FILTERS: ReadonlyArray<{ label: string; value: HaltFilter }> = [
192
211
  { label: "All", value: "all" },
193
212
  { label: "Active", value: "active" },
@@ -221,6 +240,7 @@ export type HaltColumnId =
221
240
  | "halted"
222
241
  | "quote"
223
242
  | "trade"
243
+ | "duration"
224
244
  | "status";
225
245
 
226
246
  export type HaltColumn = DataTableColumn & { id: HaltColumnId };
@@ -235,6 +255,7 @@ export const HALT_SORT_COLUMN_IDS: readonly HaltColumnId[] = [
235
255
  "halted",
236
256
  "quote",
237
257
  "trade",
258
+ "duration",
238
259
  "status",
239
260
  ];
240
261
 
@@ -262,16 +283,18 @@ const HALT_COLUMNS: readonly HaltColumn[] = [
262
283
  { id: "halted", label: "HALT ET", width: 9, align: "left" },
263
284
  { id: "quote", label: "QUOTE ET", width: 11, align: "left" },
264
285
  { id: "trade", label: "TRADE ET", width: 11, align: "left" },
286
+ { id: "duration", label: "HALTED FOR", width: 12, align: "right" },
265
287
  { id: "status", label: "STATUS", width: 8, align: "left" },
266
288
  ];
267
289
 
268
290
  /**
269
291
  * What a narrow pane gives up first, so STATUS never scrolls out of view. MKT
270
292
  * is only the venue, STATUS already reads QUOTE once quoting resumes, and
271
- * REASON spells out CODE. Trade resumption and halt time give way only in
272
- * the narrowest panes; symbol, reason, halt date, age and status always stay.
293
+ * REASON spells out CODE. Trade resumption, halt time and then the age, which
294
+ * the date and halt time also give, go only in the narrowest panes; symbol,
295
+ * reason, halt date, how long it was halted for and status always stay.
273
296
  */
274
- const NARROW_DROP_ORDER: readonly HaltColumnId[] = ["market", "quote", "code", "company", "trade", "halted"];
297
+ const NARROW_DROP_ORDER: readonly HaltColumnId[] = ["market", "quote", "code", "company", "trade", "halted", "age"];
275
298
 
276
299
  export function buildHaltColumns(width: number): HaltColumn[] {
277
300
  const columns = fitChartTableColumns(HALT_COLUMNS, width, NARROW_DROP_ORDER);
@@ -304,7 +327,9 @@ function haltSortValue(columnId: HaltColumnId, record: HaltRecord, now: number):
304
327
  case "halted":
305
328
  return record.haltedAt;
306
329
  case "age":
307
- return Math.max(0, Math.min(now, record.tradeResumeAt ?? now) - record.haltedAt);
330
+ return haltAgeMs(record, now);
331
+ case "duration":
332
+ return haltDurationMs(record, now);
308
333
  case "quote":
309
334
  return record.quoteResumeAt;
310
335
  case "trade":
@@ -29,6 +29,7 @@ import {
29
29
  formatEtResumption,
30
30
  formatEtTime,
31
31
  formatHaltAge,
32
+ formatHaltDuration,
32
33
  haltStatusColor,
33
34
  haltStatusLabel,
34
35
  nextHaltFilter,
@@ -179,6 +180,8 @@ export function MarketHaltsPane({ focused, width, height }: PaneProps) {
179
180
  text: formatEtResumption(row.tradeResumeAt, row.haltedAt),
180
181
  color: selectedColor ?? colors.textDim,
181
182
  };
183
+ case "duration":
184
+ return { text: formatHaltDuration(row, now), color: selectedColor ?? colors.textMuted };
182
185
  case "status": {
183
186
  const rowStatus = resolveHaltStatus(row, now);
184
187
  return {
@@ -110,10 +110,10 @@ export async function loadMarketMoverTab(
110
110
  return { ...result, quotes: quotes.slice(0, LIST_ROWS), rankedBy, tab };
111
111
  }
112
112
 
113
- /** The server refused the list for the account's plan. */
113
+ /** The server refused the list for the account's plan (gaps, without Pro). */
114
114
  export class SessionMoversAccessError extends Error {
115
115
  constructor() {
116
- super("Pre-market, after-hours and gap lists need Pro");
116
+ super("Gap lists need Pro");
117
117
  this.name = "SessionMoversAccessError";
118
118
  }
119
119
  }
@@ -8,7 +8,6 @@ import type { PluginModule } from "../plugin-module";
8
8
  import { TICKER_RESEARCH_PANE_ID } from "../../../types/config";
9
9
  import { publicTickerKey } from "../../../utils/exchanges";
10
10
  import { usePaneSettingValue } from "../../../state/app/context";
11
- import { usePlanAccess } from "../../../api-client/plan-access";
12
11
  import { useAssetData, usePluginPaneState, usePluginTickerActions } from "../../runtime";
13
12
  import { useLiveQuoteEntries } from "../../../state/hooks/quote-streaming";
14
13
  import { useAutoRefresh } from "../../../react/auto-refresh";
@@ -87,7 +86,6 @@ function MarketMoversPane({ focused, width, height }: PaneProps) {
87
86
  const [savedTab, setSavedTab] = usePluginPaneState<TabId>("activeTab", tabs[0]!.id);
88
87
  const [pickedIn, setPickedIn] = usePluginPaneState<string | null>("activeTabSession", null);
89
88
  const [requestedTab, setRequestedTab] = usePluginPaneState<TabId | null>("requestedTab", null);
90
- const access = usePlanAccess();
91
89
  const session = useUsSession();
92
90
  const tabIds = useMemo(() => tabs.map((tab) => tab.id), [tabs]);
93
91
  const activeTab = resolveActiveTab({
@@ -96,7 +94,6 @@ function MarketMoversPane({ focused, width, height }: PaneProps) {
96
94
  pickedIn,
97
95
  session,
98
96
  requested: requestedTab,
99
- sessionListsOpen: access.signedIn && access.emailVerified && access.hasProAccess,
100
97
  });
101
98
 
102
99
  // The index summary is a quote board like any other, so it runs on the shared
@@ -24,6 +24,7 @@ import { summaryFooterSegments } from "./footer";
24
24
  import type { MarketSummaryQuote } from "./screener";
25
25
  import {
26
26
  earlierSessionLabel,
27
+ listAsOfLabel,
27
28
  overlaySessionMovers,
28
29
  resolveSide,
29
30
  sessionListIsLive,
@@ -46,7 +47,13 @@ interface LoadedList {
46
47
  payload: CloudSessionMoversPayload;
47
48
  }
48
49
 
49
- /** Pre-market, after-hours and gap lists: Pro only, like the ranked lists they sit beside. */
50
+ /**
51
+ * Pre-market, after-hours and gap lists. Pre-market and after hours open on
52
+ * every plan: without Pro the server answers with the list as it was built a
53
+ * delay before (`access: "delayed"`), as old as the account's quotes, and the
54
+ * footer gives that time. Gaps are Pro only, like the ranked lists they sit
55
+ * beside.
56
+ */
50
57
  export function SessionMoversBody(props: {
51
58
  view: CloudSessionMoversCategory;
52
59
  session: UsSession;
@@ -57,29 +64,31 @@ export function SessionMoversBody(props: {
57
64
  liveStreaming: boolean;
58
65
  }) {
59
66
  const access = usePlanAccess();
67
+ const proOnly = props.view === "gaps";
60
68
  const [denied, setDenied] = useState(false);
61
69
  const deny = useCallback(() => setDenied(true), []);
62
70
  // A refusal is about the plan the account had then; a new plan asks again.
63
71
  useEffect(() => setDenied(false), [access.signedIn, access.emailVerified, access.hasProAccess]);
64
- const entitled = access.emailVerified && access.hasProAccess && !denied;
72
+ const entitled = !proOnly || (access.emailVerified && access.hasProAccess && !denied);
65
73
  // Behind a wall the footer still carries the index summary; the list registers its own.
66
74
  usePaneFooter("market-movers-wall", () => (
67
75
  entitled ? null : { info: summaryFooterSegments(props.summaryQuotes) }
68
76
  ), [entitled, props.summaryQuotes]);
69
77
 
70
- if (!access.signedIn) return <SignInWall placement="most-signin" width={props.width} height={props.height} action="see pre-market, after-hours and gap movers" />;
71
- if (!access.emailVerified) return <SignInWall placement="most-signin" width={props.width} height={props.height} action="see pre-market, after-hours and gap movers" needsVerification />;
78
+ if (proOnly && !access.signedIn) return <SignInWall placement="most-signin" width={props.width} height={props.height} action="see gap movers" />;
79
+ if (proOnly && !access.emailVerified) return <SignInWall placement="most-signin" width={props.width} height={props.height} action="see gap movers" needsVerification />;
72
80
  if (!entitled) {
73
81
  return (
74
82
  <ProWall
75
83
  placement="most-wall"
76
84
  width={props.width} height={props.height}
77
- title="Pre-market, after-hours and gap movers are part of Gloom Cloud Pro."
78
- message="The whole listed US market from 04:00 to 20:00, with relative volume, VWAP, float and catalysts."
85
+ title="Gap movers are part of Gloom Cloud Pro."
86
+ message="Every listed US stock that opened away from its prior close, with relative volume, VWAP, float and catalysts."
79
87
  />
80
88
  );
81
89
  }
82
- return <SessionMoversTable {...props} onDenied={deny} />;
90
+ // A new plan reads the list again: live with Pro, a delay before without.
91
+ return <SessionMoversTable key={access.hasProAccess ? "pro" : "free"} {...props} onDenied={proOnly ? deny : undefined} />;
83
92
  }
84
93
 
85
94
  function SessionMoversTable({ view, session, focused, width, summaryQuotes, liveStreaming, onDenied }: {
@@ -89,7 +98,8 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
89
98
  width: number;
90
99
  summaryQuotes: MarketSummaryQuote[];
91
100
  liveStreaming: boolean;
92
- onDenied: () => void;
101
+ /** Gaps: the server refused the list for the account's plan. */
102
+ onDenied?: () => void;
93
103
  }) {
94
104
  const { pinTicker } = usePluginTickerActions();
95
105
  const [savedSide, setSavedSide] = usePluginPaneState<CloudSessionMoversSide>("sessionSide", "up");
@@ -106,7 +116,10 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
106
116
 
107
117
  const payload = loaded?.key === listKey ? loaded.payload : null;
108
118
  const listRows = payload?.items ?? NO_ROWS;
109
- const live = !!payload && sessionListIsLive(view, payload.session, session);
119
+ // A free account's list is the one built a delay before: no live prices on its rows.
120
+ const delayed = payload?.access === "delayed";
121
+ const sessionTrading = !!payload && sessionListIsLive(view, payload.session, session);
122
+ const live = sessionTrading && !delayed;
110
123
  // While the list's session trades it is rebuilt every minute, and a failed or
111
124
  // stale answer is asked for again as often; a finished list does not change,
112
125
  // so it follows the refresh interval the user set.
@@ -126,7 +139,7 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
126
139
  setLastLoadedAt(Date.now());
127
140
  } catch (error) {
128
141
  if (fetchGenRef.current !== gen) return;
129
- if (error instanceof SessionMoversAccessError) {
142
+ if (error instanceof SessionMoversAccessError && onDenied) {
130
143
  onDenied();
131
144
  return;
132
145
  }
@@ -172,11 +185,14 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
172
185
  }, [rows, selectedId]);
173
186
 
174
187
  const sessionLabel = payload ? earlierSessionLabel(payload.session, session) : null;
188
+ // While its session trades, a free list says when it was built; a finished one is final and its date says enough.
189
+ const asOfLabel = payload && delayed && sessionTrading ? listAsOfLabel(payload.asOf, payload.session, session) : null;
175
190
  usePaneFooter("market-movers", () => ({
176
191
  info: [
177
192
  ...(loadError ? [{ id: "load-error", parts: [{ text: loadError, tone: "warning" as const }] }] : []),
178
193
  ...summaryFooterSegments(summaryQuotes),
179
194
  ...(sessionLabel ? [{ id: "session", parts: [{ text: sessionLabel, tone: "muted" as const }] }] : []),
195
+ ...(asOfLabel ? [{ id: "as-of", asOf: true, parts: [{ text: asOfLabel, tone: "muted" as const }] }] : []),
180
196
  ...(loading ? [{ id: "loading", parts: [{ text: "loading", tone: "muted" as const }] }] : []),
181
197
  ...(feedStatus ? [{
182
198
  id: "feed",
@@ -184,7 +200,7 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
184
200
  }] : []),
185
201
  ...(payload?.stale ? [{ id: "stale", parts: [{ text: "stale", tone: "muted" as const }] }] : []),
186
202
  ],
187
- }), [feedStatus, loadError, loading, payload?.stale, sessionLabel, summaryQuotes]);
203
+ }), [asOfLabel, feedStatus, loadError, loading, payload?.stale, sessionLabel, summaryQuotes]);
188
204
 
189
205
  const openRow = useCallback((row: SessionMoverRow) => {
190
206
  pinTicker(rowKey(row), { floating: true, paneType: TICKER_RESEARCH_PANE_ID, instrument: null });
@@ -11,7 +11,7 @@ import { buildQuoteKey, resolveEntryData } from "../../../market-data/selectors"
11
11
  import type { QueryEntry } from "../../../market-data/result-types";
12
12
  import type { Quote } from "../../../types/financials";
13
13
  import { compareSortValues, type SortDirection } from "../../../utils/sort-values";
14
- import { zonedDateKey, zonedWallClockToUtcMs } from "../../../utils/zoned-date-time";
14
+ import { zonedDateKey, zonedDateTimeParts, zonedWallClockToUtcMs } from "../../../utils/zoned-date-time";
15
15
  import type { SessionTabId } from "./model";
16
16
 
17
17
  const NEW_YORK = "America/New_York";
@@ -67,9 +67,8 @@ export function usSessionAt(now: number): UsSession {
67
67
  * stays while the session it was picked in lasts; otherwise the pane follows
68
68
  * the session that is trading: Pre-market before 09:30, the day's lists during
69
69
  * the regular session, After hours after the close. Overnight and on closed
70
- * days the last pick stands. An account that cannot open the session lists
71
- * (signed out, unverified, free) keeps the day's lists instead of being moved
72
- * onto a wall.
70
+ * days the last pick stands. Every plan opens the two lists it follows, a
71
+ * free one as of a delay before (session-body.tsx).
73
72
  */
74
73
  export function resolveActiveTab<T extends string>(args: {
75
74
  tabs: readonly T[];
@@ -77,14 +76,13 @@ export function resolveActiveTab<T extends string>(args: {
77
76
  pickedIn: string | null | undefined;
78
77
  session: UsSession;
79
78
  requested?: string | null;
80
- sessionListsOpen: boolean;
81
79
  }): T {
82
80
  const { tabs, session } = args;
83
81
  const requested = tabs.find((tab) => tab === args.requested);
84
82
  if (requested) return requested;
85
83
  const saved = tabs.find((tab) => tab === args.saved) ?? tabs[0]!;
86
84
  if (session.phase === "closed" || args.pickedIn === session.key) return saved;
87
- if (session.phase === "regular" || !args.sessionListsOpen) {
85
+ if (session.phase === "regular") {
88
86
  return REGULAR_TABS.has(saved) ? saved : tabs.find((tab) => REGULAR_TABS.has(tab)) ?? saved;
89
87
  }
90
88
  const sessionTab = session.phase === "pre" ? "premarket" : "afterhours";
@@ -223,11 +221,28 @@ export function sortSessionRows(rows: SessionMoverRow[], preference: SessionMove
223
221
  return [...rows].sort((left, right) => compareSortValues(sortValue(columnId, left), sortValue(columnId, right), preference.direction));
224
222
  }
225
223
 
224
+ function shortDay(date: string): string | null {
225
+ const day = new Date(`${date}T12:00:00Z`);
226
+ return Number.isFinite(day.getTime())
227
+ ? day.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" })
228
+ : null;
229
+ }
230
+
226
231
  /** The session date when it is not today's, e.g. last evening's after-hours list. */
227
232
  export function earlierSessionLabel(listSession: string | null, session: UsSession): string | null {
228
233
  if (!listSession || listSession === session.date) return null;
229
- const date = new Date(`${listSession}T12:00:00Z`);
230
- return Number.isFinite(date.getTime())
231
- ? date.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" })
232
- : null;
234
+ return shortDay(listSession);
235
+ }
236
+
237
+ /**
238
+ * When a free account's list was built, New York time: "as of 07:42 ET", and
239
+ * "as of Oct 19 19:58 ET" when that is not on the list's own session day.
240
+ */
241
+ export function listAsOfLabel(asOf: string, listSession: string | null, session: UsSession): string | null {
242
+ const at = Date.parse(asOf);
243
+ if (!Number.isFinite(at)) return null;
244
+ const { hour, minute } = zonedDateTimeParts(at, NEW_YORK);
245
+ const clock = `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")} ET`;
246
+ const day = zonedDateKey(at, NEW_YORK);
247
+ return day === (listSession ?? session.date) ? `as of ${clock}` : `as of ${shortDay(day) ?? day} ${clock}`;
233
248
  }
@@ -1,5 +1,6 @@
1
1
  import { useMemo } from "react";
2
2
  import {
3
+ DELAYED_SEGMENT,
3
4
  PaneStatusBody, type DataTableCell,
4
5
  type DataTableColumn,
5
6
  type PaneFooterSegment
@@ -140,7 +141,7 @@ export function MarketValuationPane({ focused, width, height }: PaneProps) {
140
141
  if (!selectedView) return [];
141
142
  const info: PaneFooterSegment[] = [
142
143
  { id: "as-of", parts: [{ text: `as of ${selectedView.asOf}`, tone: "muted" }] },
143
- ...(width >= 80 ? [{ id: "delayed", parts: [{ text: "delayed", tone: "muted" as const }] }] : []),
144
+ ...(width >= 80 ? [DELAYED_SEGMENT] : []),
144
145
  ];
145
146
  if (selectedView.observationStale) {
146
147
  info.push({ id: "stale", parts: [{ text: "STALE", tone: "warning", bold: true }] });
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useMemo, useRef, useState } from "react";
2
2
  import type { FundChange, FundMember } from "../../../api-client/members";
3
- import { DataTableStackView, DataTableView, DetailScrollBody, EmptyState, KeyValueRow, PaneFooterScope, PaneStatusBody, Prose, QueryBar, StatGrid,
3
+ import { delayedFooterSegment, DataTableStackView, DataTableView, DetailScrollBody, EmptyState, KeyValueRow, PaneFooterScope, PaneStatusBody, Prose, QueryBar, StatGrid,
4
4
  isSectionedItemRow, renderSectionedRowHeader, useExternalLinkFooter, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, useQueryBarSearch,
5
5
  type DataTableCell, type DataTableColumn, type PaneFooterSegment, type SectionedRow } from "../../../components";
6
6
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
@@ -49,8 +49,13 @@ function Holdings({ fund, tab, width, height, focused, active }: ViewProps & { t
49
49
  return [
50
50
  { id: "coverage", parts: [{ text: coverageText(data.aggregate, width < 60), tone: partial ? "warning" : "muted" }] },
51
51
  // A capture's status line says the quotes' date and delay too; it does not say returns are unavailable.
52
- { id: "quote-date", title: `${date ?? "Quote date unavailable"} · 15m delayed · holdings ${data.asOf}`, quoteStatus: !data.quotesUnavailable,
53
- parts: [{ text: data.quotesUnavailable ? "returns unavailable" : width < 60 && dates.length === 1 ? `${date!.slice(5)} delayed` : `${date ? `${date} · ` : ""}15m delayed`, tone: "muted" }] },
52
+ ...(data.quotesUnavailable
53
+ ? [{ id: "quote-date", title: `${date ?? "Quote date unavailable"} · 15m delayed · holdings ${data.asOf}`, parts: [{ text: "returns unavailable", tone: "muted" as const }] }]
54
+ : [
55
+ delayedFooterSegment({ id: "quote-delay", detail: "15m delayed", delay: "15m", quoteStatus: true }),
56
+ ...(date ? [{ id: "quote-date", title: `${date} · 15m delayed · holdings ${data.asOf}`, quoteStatus: true,
57
+ parts: [{ text: width < 60 && dates.length === 1 ? date.slice(5) : date, tone: "muted" as const }] }] : []),
58
+ ]),
54
59
  ...(width >= 60 ? [{ id: "holdings-date", asOf: true, parts: [{ text: `holdings ${data.asOf}`, tone: "muted" as const }] }] : []),
55
60
  ];
56
61
  }, [data, width]);