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
@@ -20,6 +20,7 @@ import type { PluginModule } from "../plugin-module";
20
20
  import { colors, blendHex } from "../../../theme/colors";
21
21
  import type { EconEvent } from "./types";
22
22
  import { FRED_CATALOG_SERIES } from "./fred-catalog";
23
+ import { ECON_PRINT_NOTIFICATIONS_ENABLED_KEY, retainVisiblePrintRefresh, setupHighImpactPrintNotifications } from "./print-watch";
23
24
  import { EconDetailView } from "./detail-view";
24
25
  import {
25
26
  COUNTRY_CYCLE,
@@ -35,6 +36,7 @@ import {
35
36
  formatCountdown,
36
37
  formatStaleness,
37
38
  getCalendarCache,
39
+ subscribeCalendar,
38
40
  impactIndicator,
39
41
  loadCalendar,
40
42
  matchesCountry,
@@ -75,10 +77,16 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
75
77
  // loadCalendar serves a fresh cache without a request, so the pane can always
76
78
  // ask and still follow the global cadence once the cache goes stale.
77
79
  const calendar = useAsyncResource<CalendarResult>(loadEvents, { initialData: cachedEvents });
80
+ // A load made elsewhere, such as the print watch asking while a release is
81
+ // due, reaches the open pane through the cache; the newer of the two wins.
82
+ const [cacheVersion, setCacheVersion] = useState(0);
83
+ useEffect(() => subscribeCalendar(() => setCacheVersion((version) => version + 1)), []);
84
+ const cached = useMemo(() => getCalendarCache(), [cacheVersion]);
85
+ const shown = cached && (!calendar.data || cached.fetchedAt > calendar.data.fetchedAt) ? cached : calendar.data;
78
86
  const { loading } = calendar;
79
- const events = calendar.data?.data ?? NO_EVENTS;
80
- const stale = calendar.data?.stale ?? false;
81
- const fetchedAt = calendar.data?.fetchedAt ?? null;
87
+ const events = shown?.data ?? NO_EVENTS;
88
+ const stale = shown?.stale ?? false;
89
+ const fetchedAt = shown?.fetchedAt ?? null;
82
90
  // A cache that could not be refreshed arrives as a result, not a failure.
83
91
  const error = calendar.error ?? (loading ? null : calendar.data?.refreshError ?? null);
84
92
  // The selected row and the open event are remembered by event id, so a
@@ -94,8 +102,11 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
94
102
 
95
103
  useAutoRefresh(stale ? null : fetchedAt, calendar.load);
96
104
 
97
- // Tick every 30s to update staleness + countdown, only while the pane can be seen.
98
105
  const paneVisible = usePaneVisible();
106
+ // In view, a due high-impact print is asked for on the print watch's timing
107
+ // and lands here through the cache; a covered or hidden pane asks nothing.
108
+ useEffect(() => (paneVisible ? retainVisiblePrintRefresh() : undefined), [paneVisible]);
109
+ // Tick every 30s to update staleness + countdown, only while the pane can be seen.
99
110
  useEffect(() => {
100
111
  if (!paneVisible) return;
101
112
  setNow(Date.now());
@@ -437,6 +448,8 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
437
448
  );
438
449
  }
439
450
 
451
+ let stopPrintWatch: (() => void) | null = null;
452
+
440
453
  export const economicCalendarModule: PluginModule = {
441
454
  series: FRED_CATALOG_SERIES,
442
455
  panes: [{
@@ -449,6 +462,16 @@ export const economicCalendarModule: PluginModule = {
449
462
  defaultMode: "floating",
450
463
  defaultFloatingSize: { width: 100, height: 30 },
451
464
  tableExport: true,
465
+ settings: {
466
+ title: "Economic Calendar Settings",
467
+ fields: [{
468
+ key: ECON_PRINT_NOTIFICATIONS_ENABLED_KEY,
469
+ label: "Notifications",
470
+ description: "Notify when a high-impact release prints, even while this pane is closed.",
471
+ type: "toggle",
472
+ storage: "plugin",
473
+ }],
474
+ },
452
475
  }],
453
476
  paneTemplates: [{
454
477
  id: "econ-calendar-pane",
@@ -461,8 +484,12 @@ export const economicCalendarModule: PluginModule = {
461
484
  }],
462
485
  setup(ctx) {
463
486
  attachEconCalendarPersistence(ctx.persistence);
487
+ stopPrintWatch?.();
488
+ stopPrintWatch = setupHighImpactPrintNotifications(ctx);
464
489
  },
465
490
  dispose() {
491
+ stopPrintWatch?.();
492
+ stopPrintWatch = null;
466
493
  resetEconCalendarPersistence();
467
494
  },
468
495
  };
@@ -0,0 +1,279 @@
1
+ import type { GloomPluginContext } from "../../../types/plugin";
2
+ import {
3
+ getCalendarCache,
4
+ landedHighImpactPrints,
5
+ loadCalendar,
6
+ pendingHighImpactPrints,
7
+ PRINT_PENDING_MS,
8
+ subscribeCalendar,
9
+ type EconCalendarCacheEntry,
10
+ } from "./calendar-model";
11
+ import type { EconEvent } from "./types";
12
+
13
+ /** The ECO setting that turns the print toast on. Off until the user turns it on, as breaking news is. */
14
+ export const ECON_PRINT_NOTIFICATIONS_ENABLED_KEY = "econPrintNotificationsEnabled";
15
+
16
+ const TOAST_DURATION_MS = 20_000;
17
+ /**
18
+ * The waits between asks while a print is due: short just after the release
19
+ * time, when most prints land, then longer until the window closes, which
20
+ * keeps a release to about twenty asks per app instead of eighty.
21
+ */
22
+ const PRINT_BACKOFF_MS = [15_000, 30_000, 60_000] as const;
23
+ /** Each wait moves by up to a fifth either way, so apps that started together drift apart. */
24
+ const PRINT_JITTER = 0.2;
25
+ /** How often the watch looks at the calendar it already has while nothing is due. */
26
+ const PRINT_IDLE_MS = 60_000;
27
+ /** While idle, a calendar this old is asked for again, so a release added since is known. */
28
+ const CALENDAR_RECHECK_MS = 6 * 60 * 60 * 1000;
29
+ /** With no calendar at all, how long a failed ask waits before the next. */
30
+ const NO_CALENDAR_RETRY_MS = 5 * 60 * 1000;
31
+
32
+ type CalendarRead = Pick<EconCalendarCacheEntry, "data" | "fetchedAt">;
33
+
34
+ export interface PrintWatchDeps {
35
+ now: () => number;
36
+ /** Asks for the calendar; `force` skips a fresh cache. */
37
+ load: (force: boolean) => Promise<CalendarRead>;
38
+ /** The cached calendar, however old, without a request. */
39
+ read: () => CalendarRead | null;
40
+ /** Called whenever anyone, a pane included, loads the calendar. */
41
+ subscribe: (listener: () => void) => () => void;
42
+ schedule: (run: () => Promise<void> | void, delay: number) => () => void;
43
+ random: () => number;
44
+ }
45
+
46
+ const DEFAULT_DEPS: PrintWatchDeps = {
47
+ now: Date.now,
48
+ load: (force) => loadCalendar(force),
49
+ read: () => getCalendarCache({ allowExpired: true }),
50
+ subscribe: subscribeCalendar,
51
+ schedule: (run, delay) => {
52
+ const timer = setTimeout(run, delay);
53
+ return () => clearTimeout(timer);
54
+ },
55
+ random: Math.random,
56
+ };
57
+
58
+ function printBody(event: EconEvent): string {
59
+ return event.forecast ? `${event.event} ${event.actual} vs ${event.forecast}` : `${event.event} ${event.actual}`;
60
+ }
61
+
62
+ type PrintWatchContext = Pick<GloomPluginContext, "notify" | "showPane">;
63
+
64
+ /**
65
+ * Toasts a high-impact print once, when it lands within
66
+ * {@link PRINT_PENDING_MS} of its release time. While one is due the
67
+ * calendar is asked for on {@link PRINT_BACKOFF_MS}, and every read is
68
+ * compared: the watch's own, its idle look at the cache, and a pane's
69
+ * refresh. The first read only records what has already printed.
70
+ */
71
+ export function startHighImpactPrintWatch(
72
+ ctx: PrintWatchContext,
73
+ overrides: Partial<PrintWatchDeps> = {},
74
+ ): () => void {
75
+ const deps: PrintWatchDeps = { ...DEFAULT_DEPS, ...overrides };
76
+ let stopped = false;
77
+ let running = false;
78
+ let cancel: (() => void) | null = null;
79
+ /** The newest calendar the watch has seen. */
80
+ let latest: CalendarRead | null = null;
81
+ let lastAskAt = Number.NEGATIVE_INFINITY;
82
+ let primed = false;
83
+ /**
84
+ * Each high-impact row not yet past its window, by id: null while blank,
85
+ * then its number. A number stays once seen, so a print that blanks and
86
+ * comes back is not a new print.
87
+ */
88
+ const actuals = new Map<string, string | null>();
89
+ const expiries = new Map<string, number>();
90
+ let attempt = 0;
91
+ let dueIds = new Set<string>();
92
+
93
+ const toast = (event: EconEvent) => {
94
+ ctx.notify({
95
+ title: event.country,
96
+ body: printBody(event),
97
+ type: "info",
98
+ desktop: "always",
99
+ sound: "Glass",
100
+ duration: TOAST_DURATION_MS,
101
+ refId: `econ:${event.id}`,
102
+ action: { label: "Open", onClick: () => ctx.showPane("econ-calendar") },
103
+ });
104
+ };
105
+
106
+ /** `now` is when the read was asked for, so a print the last ask in the window brings back still counts. */
107
+ const observe = (read: CalendarRead | null, now = deps.now()) => {
108
+ if (stopped || !read) return;
109
+ if (!latest || read.fetchedAt >= latest.fetchedAt) latest = read;
110
+ const landed = primed ? landedHighImpactPrints(actuals, read.data) : [];
111
+ for (const event of read.data) {
112
+ if (event.impact !== "high") continue;
113
+ const expiresAt = event.date.getTime() + PRINT_PENDING_MS;
114
+ if (expiresAt < now) continue;
115
+ expiries.set(event.id, expiresAt);
116
+ if (event.actual) actuals.set(event.id, event.actual);
117
+ else if (!actuals.has(event.id)) actuals.set(event.id, null);
118
+ }
119
+ for (const [id, expiresAt] of expiries) {
120
+ if (expiresAt >= now) continue;
121
+ expiries.delete(id);
122
+ actuals.delete(id);
123
+ }
124
+ primed = true;
125
+ // A print that turns up after its window is recorded, not announced late.
126
+ for (const event of landed) {
127
+ if (now - event.date.getTime() <= PRINT_PENDING_MS) toast(event);
128
+ }
129
+ };
130
+
131
+ /** Whether a print in the newest calendar is due and still within its window. */
132
+ const printDue = (now: number): boolean => {
133
+ const pending = latest ? pendingHighImpactPrints(latest.data, now) : [];
134
+ // A release that has just come due starts the waits over.
135
+ if (pending.length === 0 || pending.some((event) => !dueIds.has(event.id))) attempt = 0;
136
+ dueIds = new Set(pending.map((event) => event.id));
137
+ return pending.length > 0;
138
+ };
139
+
140
+ const backoff = (): number => {
141
+ const base = PRINT_BACKOFF_MS[Math.min(attempt, PRINT_BACKOFF_MS.length - 1)]!;
142
+ attempt += 1;
143
+ return Math.round(base * (1 + (deps.random() * 2 - 1) * PRINT_JITTER));
144
+ };
145
+
146
+ const askWhileIdle = (now: number): boolean => (latest
147
+ ? now - Math.max(latest.fetchedAt, lastAskAt) >= CALENDAR_RECHECK_MS
148
+ : now - lastAskAt >= NO_CALENDAR_RETRY_MS);
149
+
150
+ const arm = (delay: number) => {
151
+ if (stopped || cancel) return;
152
+ cancel = deps.schedule(() => {
153
+ cancel = null;
154
+ return run();
155
+ }, delay);
156
+ };
157
+
158
+ const run = async () => {
159
+ if (stopped || running) return;
160
+ observe(deps.read());
161
+ const before = deps.now();
162
+ const due = printDue(before);
163
+ if (!due && !askWhileIdle(before)) {
164
+ arm(PRINT_IDLE_MS);
165
+ return;
166
+ }
167
+ running = true;
168
+ lastAskAt = before;
169
+ try {
170
+ // A due print is asked for past the cache; otherwise a fresh cache answers without a request.
171
+ observe(await deps.load(due), before);
172
+ } catch {
173
+ // The next ask follows the same schedule.
174
+ } finally {
175
+ running = false;
176
+ }
177
+ if (stopped) return;
178
+ // Past the window nothing is due, so the asks stop and the watch goes back to idle looks.
179
+ arm(printDue(deps.now()) ? backoff() : PRINT_IDLE_MS);
180
+ };
181
+
182
+ // A load the watch is waiting on is compared when it answers, at the time it was asked.
183
+ const unsubscribe = deps.subscribe(() => {
184
+ if (!running) observe(deps.read());
185
+ });
186
+ void run();
187
+ return () => {
188
+ stopped = true;
189
+ unsubscribe();
190
+ cancel?.();
191
+ cancel = null;
192
+ };
193
+ }
194
+
195
+ type PrintNotificationContext = PrintWatchContext & Pick<GloomPluginContext, "configState" | "on">;
196
+
197
+ function printNotificationsEnabled(ctx: Pick<GloomPluginContext, "configState">): boolean {
198
+ return ctx.configState.get<boolean>(ECON_PRINT_NOTIFICATIONS_ENABLED_KEY) === true;
199
+ }
200
+
201
+ /**
202
+ * The one print watch, run while anything wants it: the Notifications
203
+ * setting, or an ECO pane in view, which fills a print in as it lands. Every
204
+ * reason shares its timing and its requests; only the setting toasts.
205
+ */
206
+ export interface PrintRefresh {
207
+ /** The plugin's context, for toasts and their Open; null once it is disposed. */
208
+ attach(ctx: PrintWatchContext | null): void;
209
+ setNotifying(on: boolean): void;
210
+ /** An ECO pane can be seen; the returned release says it no longer can. */
211
+ retainVisiblePane(): () => void;
212
+ }
213
+
214
+ export function createPrintRefresh(
215
+ start: (ctx: PrintWatchContext) => () => void = startHighImpactPrintWatch,
216
+ ): PrintRefresh {
217
+ let host: PrintWatchContext | null = null;
218
+ let notifying = false;
219
+ let visiblePanes = 0;
220
+ let stopWatch: (() => void) | null = null;
221
+ // A print that lands while only a pane wants the watch is recorded, not toasted.
222
+ const watchContext: PrintWatchContext = {
223
+ notify: (notice) => {
224
+ if (notifying) host?.notify(notice);
225
+ },
226
+ showPane: (paneId) => host?.showPane(paneId),
227
+ };
228
+ const sync = () => {
229
+ const wanted = notifying || visiblePanes > 0;
230
+ if (wanted && !stopWatch) stopWatch = start(watchContext);
231
+ if (!wanted && stopWatch) {
232
+ stopWatch();
233
+ stopWatch = null;
234
+ }
235
+ };
236
+ return {
237
+ attach(ctx) {
238
+ host = ctx;
239
+ },
240
+ setNotifying(on) {
241
+ notifying = on;
242
+ sync();
243
+ },
244
+ retainVisiblePane() {
245
+ visiblePanes += 1;
246
+ sync();
247
+ let released = false;
248
+ return () => {
249
+ if (released) return;
250
+ released = true;
251
+ visiblePanes -= 1;
252
+ sync();
253
+ };
254
+ },
255
+ };
256
+ }
257
+
258
+ const printRefresh = createPrintRefresh();
259
+
260
+ /** For the ECO pane: while it can be seen, a due high-impact print is asked for on the watch's timing. */
261
+ export function retainVisiblePrintRefresh(): () => void {
262
+ return printRefresh.retainVisiblePane();
263
+ }
264
+
265
+ /** Toasts prints while the Notifications setting is on; off, only a pane in view keeps the watch asking. */
266
+ export function setupHighImpactPrintNotifications(
267
+ ctx: PrintNotificationContext,
268
+ refresh: PrintRefresh = printRefresh,
269
+ ): () => void {
270
+ refresh.attach(ctx);
271
+ const sync = () => refresh.setNotifying(printNotificationsEnabled(ctx));
272
+ const disposeConfigListener = ctx.on("config:changed", sync);
273
+ sync();
274
+ return () => {
275
+ disposeConfigListener();
276
+ refresh.setNotifying(false);
277
+ refresh.attach(null);
278
+ };
279
+ }
@@ -1,10 +1,12 @@
1
1
  import type { HeadlessPaneDefinition } from "../../../types/plugin";
2
2
  import type { HeadlessPaneColumn, HeadlessPaneContext } from "../../../types/headless";
3
3
  import {
4
+ KPI_FIELDS,
4
5
  NUMERIC_FIELDS,
5
6
  type NumericField,
6
7
  type ScreenRow,
7
8
  } from "../../../api-client/equity-screener";
9
+ import { KPI_COVERAGE_NOTE } from "../company-kpis/model";
8
10
  import { EXTENDED_SESSION_LABELS, type ExtendedSession } from "../../../market-data/market/status";
9
11
  import { buildQuoteKey } from "../../../market-data/selectors";
10
12
  import type { Quote } from "../../../types/financials";
@@ -17,8 +19,11 @@ import {
17
19
  formatScreenValue,
18
20
  metricDate,
19
21
  parseScreenDefinition,
22
+ screenColumnLabel,
20
23
  screenDefinitionSyntax,
21
- SHORT_LABELS,
24
+ screenLabel,
25
+ screenYtdBase,
26
+ withheldMetricNote,
22
27
  } from "./model";
23
28
  import type { ScreenDefinition } from "../../../api-client/equity-screener";
24
29
 
@@ -128,12 +133,21 @@ export const equityScreenerHeadless: HeadlessPaneDefinition<"bundle"> = {
128
133
  // A sort on one of them is redone on those figures.
129
134
  const rows = sortLiveScreenRows(overlayScreenQuotes(data.rows, await loadRowQuotes(data.rows, ctx)), data.definition.sort);
130
135
  const extendedSessions = screenExtendedSessions(rows);
136
+ // A Pro metric the server held back from Free says so once, as KPIS says it; a KPI
137
+ // with gaps says what a gap is, as KPIS does.
138
+ const withheld = rows.some((row) => row.metrics[metric].access === "pro");
139
+ const errors = withheld ? [...data.warnings, withheldMetricNote(metric)] : data.warnings;
140
+ const missing = rows.filter((row) => row.metrics[metric].value === null).length;
141
+ const kpiGaps = !withheld && missing > 0 && (KPI_FIELDS as readonly string[]).includes(metric)
142
+ ? [`${missing} of ${rows.length} ${rows.length === 1 ? "row has" : "rows have"} no ${screenLabel(metric)}. ${KPI_COVERAGE_NOTE}`]
143
+ : [];
131
144
  // Payload status also counts ordinary per-field sparsity; only generation
132
145
  // warnings are failures, and more pages are not an error.
133
146
  return {
134
147
  freshness: { asOf: data.snapshot?.sourceNewestAt ?? null },
135
- complete: !data.nextCursor && data.warnings.length === 0,
136
- errors: data.warnings,
148
+ complete: !data.nextCursor && errors.length === 0,
149
+ errors,
150
+ ...(kpiGaps.length ? { notes: kpiGaps } : {}),
137
151
  sections: [
138
152
  {
139
153
  title: "Matching equities",
@@ -145,7 +159,8 @@ export const equityScreenerHeadless: HeadlessPaneDefinition<"bundle"> = {
145
159
  ...(metric === "price" ? [] : [{ key: "price", header: "Last", align: "right" as const, format: price }]),
146
160
  ...(metric === "changePercent" ? [] : [{ key: "changePercent", header: "Chg%", align: "right" as const, format: percent }]),
147
161
  ...extendedSessions.map(extendedColumn),
148
- { key: "value", header: SHORT_LABELS[metric], align: "right" },
162
+ // YTD names the close it is measured from: `YTD% vs 31 Dec close`.
163
+ { key: "value", header: screenColumnLabel(metric, screenYtdBase(data.rows, data.snapshot?.assembledAt), "report"), align: "right" },
149
164
  { key: "unit", header: "Unit" },
150
165
  { key: "percentile", header: "Covered pctl", align: "right" },
151
166
  { key: "asOf", header: "As of" },
@@ -172,7 +187,7 @@ export const equityScreenerHeadless: HeadlessPaneDefinition<"bundle"> = {
172
187
  ? null
173
188
  : Math.round(observation.percentile.value),
174
189
  asOf: stamp.collected ? `${stamp.text} (collected)` : stamp.text,
175
- state: observation.state,
190
+ state: observation.access === "pro" ? "locked" : observation.state,
176
191
  };
177
192
  }),
178
193
  },
@@ -17,9 +17,11 @@ import {
17
17
  type ScreenPayload,
18
18
  type ScreenRow,
19
19
  } from "../../../api-client/equity-screener";
20
+ import { yearToDateBaseLabel } from "../../../market-data/market/year-to-date";
20
21
  import { canonicalExchange } from "../../../utils/exchanges";
21
22
  import { LATEST_DISTRIBUTION, TRAILING_DIVIDEND_BASIS } from "../../../utils/forward-dividend";
22
23
  import { formatCompact, formatNumber } from "../../../utils/format";
24
+ import { kpiFreeLimitNote } from "../company-kpis/model";
23
25
 
24
26
  /**
25
27
  * Column headers. Currency sits in the footer; percentage fields carry %.
@@ -160,8 +162,29 @@ export function resultFields(definition: ScreenDefinition, metric: NumericField)
160
162
  return [...new Set(fields)].filter((field) => definition.currency || !MONETARY.has(field) || field === metric);
161
163
  }
162
164
 
165
+ /**
166
+ * The close the screen's YTD returns are measured from, `31 Dec`: NYSE's last
167
+ * session of the year before the newest session a row's YTD ends on, the
168
+ * snapshot's day without one.
169
+ */
170
+ export function screenYtdBase(rows: readonly ScreenRow[], assembledAt?: string | null, now = Date.now()): string | null {
171
+ const day = rows.map((row) => row.metrics.returnYtdPercent?.asOf?.slice(0, 10) ?? "").filter(Boolean).sort().at(-1)
172
+ ?? assembledAt?.slice(0, 10);
173
+ return day ? yearToDateBaseLabel(day, "NYSE", now) : null;
174
+ }
175
+
176
+ /**
177
+ * A column's header. YTD names the close it is measured from: `YTD% vs 31 Dec
178
+ * close` in a report, `YTD% vs 31 Dec` in the pane and `YTD% 31 Dec` where
179
+ * the pane is short of room.
180
+ */
181
+ export function screenColumnLabel(field: NumericField, ytdBase: string | null, form: "report" | "pane" | "compact" = "pane"): string {
182
+ if (field !== "returnYtdPercent" || !ytdBase) return SHORT_LABELS[field];
183
+ return form === "compact" ? `YTD% ${ytdBase}` : `YTD% vs ${ytdBase}${form === "report" ? " close" : ""}`;
184
+ }
185
+
163
186
  /** Prices get room for a separated six-figure quote (757,398.00). */
164
- export const columnWidth = (field: NumericField) => Math.max(field === "price" ? 10 : 8, SHORT_LABELS[field].length);
187
+ export const columnWidth = (field: NumericField, label = SHORT_LABELS[field]) => Math.max(field === "price" ? 10 : 8, label.length);
165
188
 
166
189
  export const DEFAULT_SCREEN: ScreenDefinition = {
167
190
  version: 1,
@@ -481,6 +504,20 @@ const unavailableMetric = (): ScreenMetric => ({
481
504
  state: "unavailable", scope: "reported", reason: "Not provided by this server", sourceUrl: null,
482
505
  percentile: { value: null, sampleCount: 0, scope: "covered-universe" },
483
506
  });
507
+ /** Company KPIs, guidance, hiring and app attention: the server leaves them out of a screen for a caller without Gloom Pro. */
508
+ const PRO_FIELDS: ReadonlySet<string> = new Set([...KPI_FIELDS, ...HIRING_APP_FIELDS]);
509
+ const withheldMetric = (): ScreenMetric => ({ ...unavailableMetric(), reason: "Needs Gloom Pro", access: "pro" });
510
+
511
+ /**
512
+ * What a screen says once of a metric the server held back from Free, a
513
+ * KPI's in the words `fn KPIS` uses ("The free screener shows no KPI values;
514
+ * Gloom Pro shows each company's Backlog.").
515
+ */
516
+ export function withheldMetricNote(field: NumericField): string {
517
+ return (KPI_FIELDS as readonly string[]).includes(field)
518
+ ? kpiFreeLimitNote({ metric: screenLabel(field), shown: 0, total: null, guidance: field.startsWith("guide"), where: "The free screener" })
519
+ : `${screenLabel(field)} needs Gloom Pro.`;
520
+ }
484
521
  export function validateScreenPayload(value: unknown): ScreenPayload {
485
522
  const data = value as ScreenPayload;
486
523
  if (
@@ -523,8 +560,9 @@ export function validateScreenPayload(value: unknown): ScreenPayload {
523
560
  ids.add(screenRowId(row));
524
561
  for (const field of NUMERIC_FIELDS) {
525
562
  // A server from before social, research, volatility or dividend fields omits them; they read as unavailable.
563
+ // A Pro field it left out was held back from this caller.
526
564
  if (!row.metrics[field] && ([...SOCIAL_FIELDS, ...RESEARCH_FIELDS, ...VOLATILITY_FIELDS, ...ATTENTION_FIELDS, ...HIRING_APP_FIELDS, ...KPI_FIELDS, ...DIVIDEND_FIELDS] as readonly string[]).includes(field))
527
- row.metrics[field] = unavailableMetric();
565
+ row.metrics[field] = PRO_FIELDS.has(field) ? withheldMetric() : unavailableMetric();
528
566
  const metric = row.metrics[field];
529
567
  if (
530
568
  !metric ||
@@ -67,10 +67,11 @@ import {
67
67
  metricDate,
68
68
  parseScreenDefinition,
69
69
  resultFields,
70
- SHORT_LABELS,
71
70
  SIGN_COLORED,
71
+ screenColumnLabel,
72
72
  screenLabel,
73
73
  screenRowId,
74
+ screenYtdBase,
74
75
  } from "./model";
75
76
  import { useScreenResults } from "./results";
76
77
  import { overlayLiveScreenRows, screenExtendedMove, screenExtendedSessions, sortLiveScreenRows, type LiveScreenRow } from "./live";
@@ -101,20 +102,19 @@ const EXTENDED_COLUMN_PREFIX = "ext:";
101
102
  * CHG% for each session in `extendedSessions`, and is added only whole, as
102
103
  * the metrics are.
103
104
  */
104
- function resultColumns(
105
+ export function resultColumns(
105
106
  width: number,
106
107
  definition: ScreenDefinition,
107
108
  metric: NumericField,
108
109
  asOf: boolean,
109
110
  extendedSessions: readonly ExtendedSession[] = [],
111
+ ytdBase: string | null = null,
110
112
  ): DataTableColumn[] {
111
113
  const [focus, ...others] = resultFields(definition, metric);
112
- const metricColumn = (field: NumericField): DataTableColumn => ({
113
- id: `m:${field}`,
114
- label: SHORT_LABELS[field],
115
- width: columnWidth(field),
116
- align: "right",
117
- });
114
+ const metricColumn = (field: NumericField, form: "pane" | "compact"): DataTableColumn => {
115
+ const label = screenColumnLabel(field, ytdBase, form);
116
+ return { id: `m:${field}`, label, width: columnWidth(field, label), align: "right" };
117
+ };
118
118
  const extendedColumns = extendedSessions.map((session): DataTableColumn => ({
119
119
  id: `${EXTENDED_COLUMN_PREFIX}${session}`,
120
120
  label: extendedSessionStateLabel(session),
@@ -122,20 +122,21 @@ function resultColumns(
122
122
  align: "right",
123
123
  }));
124
124
  // The extended move reads beside CHG%, or first after the focus metric when that is CHG%.
125
- const extras = [
125
+ const extras = (form: "pane" | "compact") => [
126
126
  ...(focus === "changePercent" ? extendedColumns : []),
127
- ...others.flatMap((field) => field === "changePercent" ? [metricColumn(field), ...extendedColumns] : [metricColumn(field)]),
127
+ ...others.flatMap((field) => field === "changePercent" ? [metricColumn(field, form), ...extendedColumns] : [metricColumn(field, form)]),
128
128
  ];
129
129
  const build = (
130
130
  extra: DataTableColumn[],
131
131
  exchange: boolean,
132
132
  sector: boolean,
133
+ form: "pane" | "compact",
133
134
  ): DataTableColumn[] => [
134
135
  { id: "symbol", label: "SYMBOL", width: 8, align: "left" },
135
136
  { id: "name", label: "NAME", width: 18, flexGrow: 1, align: "left" },
136
137
  ...(exchange ? [{ id: "exchange", label: "EXCH", width: 7, align: "left" as const }] : []),
137
138
  ...(sector ? [{ id: "sector", label: "SECTOR", width: 22, align: "left" as const }] : []),
138
- metricColumn(focus!),
139
+ metricColumn(focus!, form),
139
140
  { id: "percentile", label: "PCTL", width: 4, align: "right" },
140
141
  ...(asOf ? [{ id: "date", label: "AS OF", width: 10, align: "left" as const }] : []),
141
142
  ...extra,
@@ -143,15 +144,15 @@ function resultColumns(
143
144
  // Measured the way the table draws them (header floor, gaps, lead gaps), so a
144
145
  // column is only added when it fits whole.
145
146
  const fits = (columns: DataTableColumn[]) => getTableWidth(columns) <= width;
146
- let shown: DataTableColumn[] = [];
147
- for (const column of extras) {
148
- const next = [...shown, column];
149
- if (!fits(build(next, false, false))) break;
150
- shown = next;
151
- }
152
- const exchange = fits(build(shown, true, false));
153
- const sector = exchange && fits(build(shown, true, true));
154
- return build(shown, exchange, sector);
147
+ // Columns are chosen with YTD's short header (`YTD% 31 Dec`), which reads
148
+ // `YTD% vs 31 Dec` when that still fits: naming the close never costs a column.
149
+ const compact = extras("compact");
150
+ let shown = 0;
151
+ while (shown < compact.length && fits(build(compact.slice(0, shown + 1), false, false, "compact"))) shown++;
152
+ const exchange = fits(build(compact.slice(0, shown), true, false, "compact"));
153
+ const sector = exchange && fits(build(compact.slice(0, shown), true, true, "compact"));
154
+ const named = build(extras("pane").slice(0, shown), exchange, sector, "pane");
155
+ return fits(named) ? named : build(compact.slice(0, shown), exchange, sector, "compact");
155
156
  }
156
157
  const loadFields = () => fetchScreenFields();
157
158
  function ScreenDetail({
@@ -351,7 +352,9 @@ function EquityScreenView({
351
352
  const staleFocus = snapshotRows.some((row) => row.metrics[metric]?.state === "stale");
352
353
  // A pre-market, after-hours or overnight column only while a row has such a print.
353
354
  const extendedSessions = screenExtendedSessions(rows);
354
- const columns = resultColumns(width, definition, metric, staleFocus, extendedSessions);
355
+ // YTD names the close it is measured from, `31 Dec`.
356
+ const ytdBase = screenYtdBase(snapshotRows, data?.snapshot?.assembledAt);
357
+ const columns = resultColumns(width, definition, metric, staleFocus, extendedSessions, ytdBase);
355
358
  const apply = (next: ScreenDefinition) => {
356
359
  try {
357
360
  setDefinition(parseScreenDefinition(next));
@@ -45,10 +45,11 @@ export const estimateRevisionsHeadless: HeadlessPaneDefinition<"bundle"> = {
45
45
  instrument?.exchange ?? "",
46
46
  ctx.apiClient,
47
47
  ctx.marketData,
48
- ).catch((error: unknown) => {
49
- throw explainRefusal(error, estimateRevisionsLockNote, {
48
+ ).catch(async (error: unknown) => {
49
+ throw await explainRefusal(error, estimateRevisionsLockNote, {
50
50
  symbol: args.symbols[0] ?? null,
51
51
  token: ctx.settings?.tab === "guidance" ? "GUID" : "EM",
52
+ context: ctx,
52
53
  });
53
54
  });
54
55
  const periods = pinnedEstimatePeriods(