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
@@ -5,7 +5,7 @@ import type { PricePoint } from "../../../types/financials";
5
5
  import { formatPriceObservation } from "../../../market-data/market/format";
6
6
  import { cleanFloat32Price } from "../../../cli/history-rows";
7
7
  import { cryptoPairCoin, isFuturesSymbol } from "../shared/crypto-pair";
8
- import { FUTURES_CONTRACTS, futuresBoardContract, futuresBoardContractForTicker, type FuturesContract } from "../../../market-data/futures-board";
8
+ import { bestNameMatch, FUTURES_CONTRACTS, futuresBoardContract, futuresBoardContractForTicker, futuresContractByName, type FuturesContract } from "../../../market-data/futures-board";
9
9
 
10
10
  export const COT_CLASSES: Record<CotFamily, Array<{ value: CotClass; label: string }>> = {
11
11
  legacy: [{ value: "noncommercial", label: "Noncommercial" }, { value: "commercial", label: "Commercial" }, { value: "nonreportable", label: "Nonreportable" }],
@@ -124,25 +124,48 @@ export function cotScope(value: unknown): CotScope { return value === "all" ? "a
124
124
  const rootAlias = (input: string) => ROOTS[input === "VIX" ? "VX" : input]?.code ?? null;
125
125
  const marketCode = (input: string) => /^[0-9A-Z]{5}[0-9A-Z+]$/.test(input) ? input : null;
126
126
  /**
127
- * A CFTC code or root typed as an argument or stored in a pane: every alias,
128
- * crypto included, and every FUT board root the CFTC reports. A typed root is
129
- * always the future, so ZM here is soybean meal, never Zoom.
127
+ * The FUT board row a typed argument names, by code, symbol or commodity name
128
+ * as FUT reads them (CC, CC=F, cocoa), or a verified root named the same way
129
+ * (bitcoin, VIX, SOFR, fed funds).
130
+ */
131
+ function typedMarket(value: string): { board: FuturesContract } | { root: string } | null {
132
+ const board = futuresBoardContract(value) ?? futuresContractByName(value);
133
+ if (board) return { board };
134
+ const root = bestNameMatch(Object.keys(ROOTS), (key) => [ROOTS[key]!.name], value);
135
+ return root ? { root } : null;
136
+ }
137
+ /**
138
+ * A CFTC code, root or commodity name typed as an argument or stored in a
139
+ * pane: every alias, crypto included, every FUT board root the CFTC reports,
140
+ * and the names FUT finds them by (cocoa, coffee, sugar, gold, crude). A typed
141
+ * root is always the future, so ZM here is soybean meal, never Zoom. A CFTC
142
+ * code is tried after the names, as one could be spelled with six letters
143
+ * (COFFEE, COPPER).
130
144
  */
131
145
  export function cotContractCode(value: unknown): string | null {
132
146
  if (typeof value !== "string") return null;
133
147
  const input = value.trim().toUpperCase().replace(/=F$/, "");
134
- const board = futuresBoardContract(value);
135
- return rootAlias(input) ?? (board ? boardMarketCode(board) : null) ?? marketCode(input);
148
+ const alias = rootAlias(input);
149
+ if (alias) return alias;
150
+ const market = typedMarket(value);
151
+ if (market && "root" in market) return ROOTS[market.root]!.code;
152
+ return (market ? boardMarketCode(market.board) : null) ?? (market ? null : marketCode(input));
136
153
  }
154
+ /** The markets the unknown-market hint names, by the code and the word a newcomer knows them by. */
155
+ const SUGGESTED_MARKETS: ReadonlyArray<readonly [string, string]> = [
156
+ ["ES", "S&P 500"], ["CL", "crude oil"], ["NG", "natural gas"], ["GC", "gold"], ["HG", "copper"],
157
+ ["ZC", "corn"], ["CC", "cocoa"], ["KC", "coffee"], ["SB", "sugar"], ["ZN", "10-year note"],
158
+ ];
137
159
  /**
138
- * What a market `cotContractCode` does not know is told: a FUT board root the
139
- * CFTC does not report says so; anything else gets roots and a code that work.
160
+ * What a market `cotContractCode` does not know is told: a FUT board row the
161
+ * CFTC does not report says so; anything else gets codes and names that work,
162
+ * softs and metals among them, and a CFTC code.
140
163
  */
141
164
  export function cotUnknownMarketMessage(value: unknown): string {
142
- const board = futuresBoardContract(value);
143
- if (board) return `${board.code} (${board.name}) is not reported in the CFTC COT.`;
144
- const roots = ["ES", "CL", "GC", "ZN"].filter((root) => ROOTS[root]);
145
- return `Unknown market "${String(value)}". Try ${roots.join(", ")} or a CFTC market code such as ${ROOTS.GC!.code}.`;
165
+ const market = typeof value === "string" ? typedMarket(value) : null;
166
+ if (market && "board" in market) return `${market.board.code} (${market.board.name}) is not reported in the CFTC COT.`;
167
+ const known = SUGGESTED_MARKETS.map(([code, name]) => `${code} ${name}`).join(", ");
168
+ return `Unknown market "${String(value)}". Try a code or name such as ${known}, or a CFTC market code such as ${ROOTS.GC!.code}.`;
146
169
  }
147
170
  /**
148
171
  * The code for the ticker under the cursor, when COT is opened without an
@@ -1,5 +1,6 @@
1
1
  import { useMemo } from "react";
2
2
  import {
3
+ DELAYED_SEGMENT,
3
4
  ChartTableHeader,
4
5
  CompositeChart,
5
6
  MarketBoardStack,
@@ -107,7 +108,7 @@ export function CdxPane({ focused, width, height }: PaneProps) {
107
108
  const asOf = formatAsOf(payload?.asOf ?? null);
108
109
  const info = useMemo<PaneFooterSegment[]>(() => [
109
110
  ...(asOf ? [{ id: "as-of", parts: [{ text: `as of ${asOf}`, tone: "muted" as const }] }] : []),
110
- ...(payload ? [{ id: "delayed", parts: [{ text: "delayed", tone: "muted" as const }] }] : []),
111
+ ...(payload ? [DELAYED_SEGMENT] : []),
111
112
  ...(resource.data?.stale ? [{ id: "stale", parts: [{ text: "STALE", tone: "warning" as const }] }] : []),
112
113
  ], [asOf, payload, resource.data?.stale]);
113
114
  usePaneStatusFooter({
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useMemo } from "react";
2
2
  import {
3
+ DELAYED_SEGMENT,
3
4
  ChartTableHeader,
4
5
  CompositeChart,
5
6
  formatBpAxis,
@@ -112,7 +113,7 @@ export function SovrPane({ focused, width, height }: PaneProps) {
112
113
  const asOf = formatAsOf(payload?.asOf ?? null);
113
114
  const info = useMemo<PaneFooterSegment[]>(() => [
114
115
  ...(asOf ? [{ id: "as-of", parts: [{ text: `as of ${asOf}`, tone: "muted" as const }] }] : []),
115
- ...(payload ? [{ id: "delayed", parts: [{ text: "delayed", tone: "muted" as const }] }] : []),
116
+ ...(payload ? [DELAYED_SEGMENT] : []),
116
117
  ...(resource.data?.stale ? [{ id: "stale", parts: [{ text: "STALE", tone: "warning" as const }] }] : []),
117
118
  ], [asOf, payload, resource.data?.stale]);
118
119
  usePaneStatusFooter({
@@ -1,5 +1,6 @@
1
1
  import { useCallback, useMemo } from "react";
2
2
  import {
3
+ DELAYED_SEGMENT,
3
4
  ChartTableHeader,
4
5
  CompositeChart,
5
6
  formatBpAxis,
@@ -115,7 +116,7 @@ export function CreditConditionsPane({ paneId, focused, width, height }: PanePro
115
116
  notices: mixedDates ? ["The indexes carry different observation dates, so the spreads are not one session."] : [],
116
117
  });
117
118
  const footerInfo = useMemo<PaneFooterSegment[]>(() => [
118
- ...(rows.length > 0 ? [{ id: "delayed", parts: [{ text: "delayed", tone: "muted" as const }] }] : []),
119
+ ...(rows.length > 0 ? [DELAYED_SEGMENT] : []),
119
120
  ...(partial ? [{ id: "partial", parts: [{ text: `PARTIAL ${rows.length}/${series.length}`, tone: "warning" as const, bold: true }] }] : []),
120
121
  ...(stale ? [{ id: "stale", parts: [{ text: "STALE", tone: "warning" as const }] }] : []),
121
122
  ...loadingErrorFooterInfo(loading, error),
@@ -84,7 +84,34 @@ function withValidStablecoinFields(asset: CryptoMarketAsset): CryptoMarketAsset
84
84
  return { ...asset, ...(peg ? { peg: null } : {}), ...(range ? { range24h: null } : {}) };
85
85
  }
86
86
 
87
- export function validateCryptoMarkets(data: CryptoMarketsPayload): CryptoMarketsPayload {
87
+ const DAY_MS = 86_400_000;
88
+ /** How many UTC days old a coin's latest daily close may be for its previous close to stand, as Gloom Cloud judges a coin quote's. */
89
+ const PREVIOUS_CLOSE_MAX_AGE_DAYS = 2;
90
+
91
+ /**
92
+ * A coin of a board from before Gloom Cloud measured its moves since 00:00
93
+ * UTC (a server not yet updated, a copy cached from one), whose change runs
94
+ * over the screener's 24 hours: moved from its previous close instead, the
95
+ * close of its last UTC day while its daily closes reach the last two days.
96
+ * Otherwise that close may be weeks old (Toncoin on 18 Oct 2026), and the
97
+ * coin has no move.
98
+ */
99
+ function onUtcDayBasis(asset: CryptoMarketAsset, now: number): CryptoMarketAsset {
100
+ const history = asset.history;
101
+ const last = history?.closes.findLastIndex((close) => close != null) ?? -1;
102
+ const lastDay = history && last >= 0 ? Date.parse(`${history.start}T00:00:00Z`) / DAY_MS + last : null;
103
+ const holds = lastDay != null && asset.previousClose != null && asset.previousClose > 0
104
+ && Math.floor(now / DAY_MS) - lastDay <= PREVIOUS_CLOSE_MAX_AGE_DAYS;
105
+ const change = holds ? asset.price - asset.previousClose! : null;
106
+ return {
107
+ ...asset,
108
+ previousClose: holds ? asset.previousClose : null,
109
+ change,
110
+ changePercent: change != null ? (change / asset.previousClose!) * 100 : null,
111
+ };
112
+ }
113
+
114
+ export function validateCryptoMarkets(data: CryptoMarketsPayload, now = Date.now()): CryptoMarketsPayload {
88
115
  if (
89
116
  !data
90
117
  || data.version !== 1
@@ -103,7 +130,11 @@ export function validateCryptoMarkets(data: CryptoMarketsPayload): CryptoMarkets
103
130
  ) {
104
131
  throw new Error("The server returned an invalid crypto board");
105
132
  }
106
- const assets = data.assets.map(withValidStablecoinFields);
133
+ const legacy = data.dayChangeBasis !== "utc-day";
134
+ const assets = data.assets.map((asset) => {
135
+ const valid = withValidStablecoinFields(asset);
136
+ return legacy ? onUtcDayBasis(valid, now) : valid;
137
+ });
107
138
  return assets.every((asset, index) => asset === data.assets[index]) ? data : { ...data, assets };
108
139
  }
109
140
 
@@ -7,6 +7,7 @@ import { fetchCryptoMarkets, fetchStablecoinSupply } from "./client";
7
7
  import {
8
8
  buildCryptoRows,
9
9
  CRYPTO_BOARD_FRESHNESS,
10
+ CRYPTO_CHANGE_BASIS_LABEL,
10
11
  formatCryptoPercent,
11
12
  formatDayRangeTime,
12
13
  formatOffPeg,
@@ -30,7 +31,9 @@ const COIN_COLUMNS: HeadlessPaneColumn[] = [
30
31
  { key: "code", header: "Coin" },
31
32
  { key: "name", header: "Name" },
32
33
  { key: "price", header: "Price", align: "right", format: (_value, row) => String(row.priceText ?? "—") },
33
- { key: "changePercent", header: "Chg %", align: "right", format: percent },
34
+ // Since 00:00 UTC, the day `quote` gives a coin (CRYPTO_CHANGE_BASIS_LABEL): the text says so, as the pane's
35
+ // CHG% UTC does, and an export keeps the header it has always had.
36
+ { key: "changePercent", header: "Chg %", textHeader: "Chg % UTC", align: "right", format: percent },
34
37
  { key: "return7d", header: "7D %", align: "right", format: percent },
35
38
  { key: "return30d", header: "30D %", align: "right", format: percent },
36
39
  { key: "return1y", header: "1Y %", align: "right", format: percent },
@@ -167,7 +170,7 @@ export const cryptoBoardHeadless: HeadlessPaneDefinition<"rows"> = {
167
170
  pegType: row.pegType,
168
171
  } : {}),
169
172
  }));
170
- const metadata = { list: kind, status: data.status, asOf: data.asOf, generatedAt: data.generatedAt };
173
+ const metadata = { list: kind, status: data.status, asOf: data.asOf, generatedAt: data.generatedAt, changeBasis: CRYPTO_CHANGE_BASIS_LABEL };
171
174
  if (kind !== "stablecoin") return { rows, errors: data.warnings, metadata };
172
175
  const summary = supply ? supplySummaryLine(supply) : null;
173
176
  const history = supply ? supplyHistoryTable(supply) : null;
@@ -57,6 +57,11 @@ export interface CryptoRow extends CryptoRowSupply {
57
57
  price: number;
58
58
  /** The price as the board prints it. */
59
59
  priceText: string;
60
+ /**
61
+ * The move since 00:00 UTC, from the previous UTC day's close, as `quote`
62
+ * measures a coin (CRYPTO_CHANGE_BASIS_LABEL); null when the coin has no
63
+ * such close to measure from.
64
+ */
60
65
  changePercent: number | null;
61
66
  return7d: number | null;
62
67
  return30d: number | null;
@@ -100,9 +105,28 @@ function closeDaysAgo(asset: CryptoMarketAsset, days: number, now: number): numb
100
105
  return isFiniteNumber(close) && close > 0 ? close : null;
101
106
  }
102
107
 
103
- const percentChange = (price: number, reference: number | null) =>
108
+ const percentChange = (price: number, reference: number | null | undefined) =>
104
109
  reference != null && reference > 0 ? (price / reference - 1) * 100 : null;
105
110
 
111
+ /** What the board's day change is measured from, as `quote` labels a coin's: its header says it (CHG% UTC). */
112
+ export const CRYPTO_CHANGE_BASIS_LABEL = "since 00:00 UTC";
113
+
114
+ /**
115
+ * A coin's move since 00:00 UTC, the basis `quote` gives a coin: its live
116
+ * quote's while that quote is measured from the previous close, otherwise the
117
+ * board's, which Gloom Cloud measures the same way (with a live price, from
118
+ * the board's previous close). A coin either says moves over 24 hours, or not
119
+ * at all, has none here, so the column never mixes the two.
120
+ */
121
+ function utcDayChangePercent(asset: CryptoMarketAsset, quote: Quote | null, price: number): number | null {
122
+ if (quote && quote.changeBasis == null) {
123
+ if (isFiniteNumber(quote.changePercent)) return quote.changePercent;
124
+ if (positive(quote.previousClose)) return percentChange(price, quote.previousClose);
125
+ }
126
+ if (asset.changeBasis != null) return null;
127
+ return quote ? percentChange(price, asset.previousClose) : asset.changePercent;
128
+ }
129
+
106
130
  /** A live quote replaces the board's snapshot once it is at least as new. */
107
131
  function liveQuote(
108
132
  asset: CryptoMarketAsset,
@@ -134,11 +158,7 @@ function buildCryptoRow(
134
158
  now = Date.now(),
135
159
  ): CryptoRow {
136
160
  const price = quote?.price ?? asset.price;
137
- const changePercent = quote && isFiniteNumber(quote.changePercent)
138
- ? quote.changePercent
139
- : quote
140
- ? percentChange(price, asset.previousClose)
141
- : asset.changePercent;
161
+ const changePercent = utcDayChangePercent(asset, quote, price);
142
162
  // Supply moves slowly; the price is what makes market cap live. Scaling the
143
163
  // reported cap keeps the source's supply basis and the board's ranking.
144
164
  const marketCap = isFiniteNumber(asset.marketCap)
@@ -389,7 +409,7 @@ const COLUMN_SPECS: CryptoColumnSpec[] = [
389
409
  { id: "code", label: "COIN", width: 7, align: "left" },
390
410
  { id: "name", label: "NAME", width: 10, align: "left", optional: 1 },
391
411
  { id: "price", label: "PRICE", width: 13, align: "right" },
392
- { id: "changePercent", label: "CHG%", width: 8, align: "right" },
412
+ { id: "changePercent", label: "CHG% UTC", title: "Change since 00:00 UTC", width: 8, align: "right" },
393
413
  { id: "return7d", label: "7D%", width: 8, align: "right" },
394
414
  { id: "return30d", label: "30D%", width: 8, align: "right", optional: 3 },
395
415
  { id: "return1y", label: "1Y%", width: 10, align: "right", optional: 5 },
@@ -415,7 +435,7 @@ const STABLECOIN_COLUMN_SPECS: CryptoColumnSpec[] = [
415
435
  { id: "low24hAt", label: "LOW UTC", width: 9, align: "left" },
416
436
  { id: "high24h", label: "24H HIGH", width: 8, align: "right", optional: 1 },
417
437
  { id: "high24hAt", label: "HIGH UTC", width: 9, align: "left", optional: 2 },
418
- { id: "changePercent", label: "CHG%", width: 8, align: "right", optional: 5 },
438
+ { id: "changePercent", label: "CHG% UTC", title: "Change since 00:00 UTC", width: 8, align: "right", optional: 5 },
419
439
  { id: "return7d", label: "7D%", width: 8, align: "right", optional: 6 },
420
440
  { id: "return30d", label: "30D%", width: 8, align: "right", optional: 8 },
421
441
  { id: "return1y", label: "1Y%", width: 10, align: "right", optional: 9 },
@@ -141,8 +141,9 @@ export async function trialLoadViewSource(
141
141
  });
142
142
  try {
143
143
  const loading = loadViewSource(spec, config, controller.signal, registry);
144
- // A load that outlives the wait has nobody left to tell.
145
- loading.catch(() => {});
144
+ loading.catch(() => {
145
+ // Intentional ignore: the race reports load failures; after timeout its result already explains the unchecked source.
146
+ });
146
147
  const outcome = await Promise.race([loading, timedOut]);
147
148
  if (outcome === "timeout") {
148
149
  return [`The source had not answered after ${Math.max(1, Math.round(timeoutMs / 1_000))} s, so it was not checked. The view opens and keeps loading.`];
@@ -14,6 +14,7 @@ import { PaneStatusBody, TextPromptDialog, usePaneFooter } from "../../../compon
14
14
  import { useAsyncResource } from "../../../react/async-resource";
15
15
  import { useShortcut } from "../../../react/input";
16
16
  import { isPlainKey } from "../../../utils/keyboard";
17
+ import { logFailure } from "../../../utils/log-failure";
17
18
  import { usePaneInstance, usePaneAppConfig } from "../../../state/app/context";
18
19
  import { colors } from "../../../theme/colors";
19
20
  import type { PaneProps } from "../../../types/plugin";
@@ -241,7 +242,7 @@ export function CustomViewPane({ focused, width, height }: PaneProps) {
241
242
  footer="Shift+Enter newline · Esc cancel"
242
243
  />
243
244
  ),
244
- }).catch(() => undefined);
245
+ }).catch(logFailure("custom-view.pane", "The view revision prompt failed"));
245
246
  const edited = specText ? parseViewSpecOr(specText) : null;
246
247
  if (!edited) return;
247
248
  if ("error" in edited) throw new Error(edited.error);
@@ -268,7 +269,7 @@ export function CustomViewPane({ focused, width, height }: PaneProps) {
268
269
  selectedChoiceId="reload"
269
270
  />
270
271
  ),
271
- }).catch(() => undefined);
272
+ }).catch(logFailure("custom-view.pane", "The view conflict prompt failed"));
272
273
  if (choice === "overwrite") {
273
274
  const published = await apiClient.publishTeamViewRevision(current.id, {
274
275
  spec: edited.spec as unknown as Record<string, unknown>,
@@ -295,7 +296,7 @@ export function CustomViewPane({ focused, width, height }: PaneProps) {
295
296
  selectedChoiceId={defaultTeam ?? undefined}
296
297
  />
297
298
  ),
298
- }).catch(() => undefined);
299
+ }).catch(logFailure("custom-view.pane", "The view team prompt failed"));
299
300
  if (!teamId) return;
300
301
  const name = await dialog.prompt<string>({
301
302
  content: (context: PromptContext<string>) => (
@@ -308,7 +309,7 @@ export function CustomViewPane({ focused, width, height }: PaneProps) {
308
309
  width={40}
309
310
  />
310
311
  ),
311
- }).catch(() => undefined);
312
+ }).catch(logFailure("custom-view.pane", "The view name prompt failed"));
312
313
  if (!name) return;
313
314
  const created = await apiClient.createTeamView(teamId, {
314
315
  name,
@@ -270,6 +270,7 @@ export function parseViewSpec(value: unknown): ViewSpec | null {
270
270
  try {
271
271
  decoded = JSON.parse(value);
272
272
  } catch {
273
+ // Intentional ignore: malformed input is represented by a null spec.
273
274
  return null;
274
275
  }
275
276
  }
@@ -15,6 +15,7 @@ import type { Quote } from "../../../types/financials";
15
15
  import { regularSessionValuationPrice } from "../../../market-data/quotes/regular-session-valuation";
16
16
  import { dividendPriceStatus, dividendQuotePriceMetadata } from "./reference-price";
17
17
  import { mapWithConcurrency } from "../../../utils/concurrency";
18
+ import { logFailure } from "../../../utils/log-failure";
18
19
  import { projectDividendRanking, roundedRatio, type DividendRankingEntry } from "./ranking";
19
20
  import { REPORT_FILTER, REPORT_ROWS } from "../shared/headless-options";
20
21
  import type { FundBondFacts } from "../../../api-client/fund-facts";
@@ -50,6 +51,9 @@ export interface DividendYieldHeadlessDependencies {
50
51
  loadTreasury?(context: HeadlessPaneContext): Promise<TreasuryTenor[] | null>;
51
52
  }
52
53
 
54
+ /** Already logged at the quote boundary; keep it distinct from absent bond facts. */
55
+ class BondClassificationError extends Error {}
56
+
53
57
  const defaultDependencies: DividendYieldHeadlessDependencies = {
54
58
  async loadData(symbol, context) {
55
59
  let currentPrice: number | null = null;
@@ -63,7 +67,8 @@ const defaultDependencies: DividendYieldHeadlessDependencies = {
63
67
  currentPrice = regularSessionValuationPrice(quote);
64
68
  currentPriceCurrency = quote.currency;
65
69
  referenceQuote = quote;
66
- } catch {
70
+ } catch (error) {
71
+ logFailure("dividend-yield.headless", "Read dividend reference quote failed")(error);
67
72
  currentPrice = null;
68
73
  }
69
74
  const data = await fetchDividendData(symbol, currentPrice, exchange, currentPriceCurrency);
@@ -75,7 +80,11 @@ const defaultDependencies: DividendYieldHeadlessDependencies = {
75
80
  async loadBond(symbol, context) {
76
81
  const instrument = await context.resolveInstrument?.(symbol);
77
82
  const exchange = instrument?.exchange ?? "";
78
- const quote = await context.marketData.getQuote(symbol, exchange).catch(() => undefined);
83
+ const quote = await context.marketData.getQuote(symbol, exchange)
84
+ .catch((error) => {
85
+ logFailure("dividend-yield.headless", "Read bond fund classification quote failed")(error);
86
+ throw new BondClassificationError("Bond fund quote check failed");
87
+ });
79
88
  const venue = exchange || quote?.exchangeName || "";
80
89
  if (resolveTickerInstrumentKind(null, { quote }) !== "fund" || (venue && !isUsListingExchange(venue))) return null;
81
90
  return fetchFundBondFacts(instrument?.symbol ?? symbol, context.apiClient);
@@ -125,7 +134,7 @@ export function projectDividendYieldHeadless(
125
134
  const metrics = data.metrics;
126
135
  const currency = data.currency ?? data.payments[0]?.currency ?? "";
127
136
  const priceStatus = dividendPriceStatus(data.price, data.priceAsOf, data.priceStale);
128
- const errors = [data.historyError, data.summaryError].filter((error): error is string => !!error);
137
+ const errors = [data.historyError, data.summaryError, tax?.checkError].filter((error): error is string => !!error);
129
138
 
130
139
  return {
131
140
  ...(errors.length > 0 ? { complete: false, errors } : {}),
@@ -249,15 +258,26 @@ export function createDividendYieldHeadless(
249
258
  // One symbol is its full report; several are one table ranked by yield.
250
259
  if (args.symbols.length <= 1) {
251
260
  const symbol = args.symbols[0] ?? String(args.argument);
261
+ let bondCheckError: string | undefined;
252
262
  const [data, bond, treasury] = await Promise.all([
253
263
  dependencies.loadData(symbol, context),
254
- // A fund's bond facts never fail the report: without them its yields read as before.
255
- dependencies.loadBond?.(symbol, context).catch(() => null) ?? null,
256
- rates ? dependencies.loadTreasury?.(context).catch(() => null) ?? null : null,
264
+ // Optional enrichment without a tax rate; a requested tax check must distinguish failure from absence.
265
+ dependencies.loadBond?.(symbol, context)
266
+ .catch((error) => {
267
+ if (error instanceof BondClassificationError) {
268
+ bondCheckError = error.message;
269
+ } else {
270
+ logFailure("dividend-yield.headless", "Read bond fund facts for report failed")(error);
271
+ bondCheckError = "Bond fund facts check failed";
272
+ }
273
+ return null;
274
+ }) ?? null,
275
+ rates ? dependencies.loadTreasury?.(context)
276
+ .catch(logFailure("dividend-yield.headless", "Read Treasury curve for tax comparison failed", null)) ?? null : null,
257
277
  ]);
258
278
  const forward = data.metrics.lapse ? null : distributionBasis(data.metrics.forwardYield, "forward", data.metrics.forwardRateAsOf ?? null);
259
279
  const tax = rates ? analyzeTaxEquivalent({
260
- symbol: symbol.toUpperCase(), bond, rates, treasury,
280
+ symbol: symbol.toUpperCase(), bond, rates, treasury, checkError: bondCheckError,
261
281
  distribution: forward ?? distributionBasis(data.metrics.trailingYield, "trailing"),
262
282
  }) : null;
263
283
  return projectDividendYieldHeadless(data, args, { bond, tax });
@@ -275,7 +295,9 @@ export function createDividendYieldHeadless(
275
295
  }
276
296
  const entries = await mapWithConcurrency(args.symbols, RANKING_CONCURRENCY, async (symbol): Promise<DividendRankingEntry> => {
277
297
  // A fund's bond facts never fail its row: without them it ranks as before.
278
- const bond = dependencies.loadBond?.(symbol, context).catch(() => null) ?? null;
298
+ const bond = dependencies.loadBond?.(symbol, context)
299
+ .catch((error) => error instanceof BondClassificationError ? null
300
+ : logFailure("dividend-yield.headless", "Read bond fund facts for ranking failed", null)(error)) ?? null;
279
301
  try {
280
302
  return { symbol, data: await dependencies.loadData(symbol, context), bond: await bond };
281
303
  } catch (error) {
@@ -3,6 +3,7 @@ import { regularSessionValuationPrice } from "../../../market-data/quotes/regula
3
3
  import { resolveCurrencyUnit } from "../../../utils/currency-units";
4
4
  import { cashInMajorUnits } from "../../../utils/dividend-cash";
5
5
  import { dividendReferencePrice } from "../../../utils/trailing-dividend";
6
+ import { logFailure } from "../../../utils/log-failure";
6
7
  import { buildDividendMetrics, INCOMPLETE_DIVIDEND_HISTORY, MISSING_DIVIDEND_CURRENCY, toDividendPayment, type DividendData } from "./client";
7
8
  import { dividendQuotePriceMetadata } from "./reference-price";
8
9
 
@@ -17,7 +18,8 @@ export async function fetchProviderDividendData(
17
18
  if (!provider.getCorporateActions) throw new Error("Dividend history source unavailable.");
18
19
  const [sourceActions, quote] = await Promise.all([
19
20
  provider.getCorporateActions(symbol, exchange),
20
- provider.getQuote(symbol, exchange).catch(() => null),
21
+ provider.getQuote(symbol, exchange)
22
+ .catch(logFailure("dividend-yield.provider-client", "Read dividend reference quote failed", null)),
21
23
  ]);
22
24
  // In the major unit as the history arrives, as the provider router serves it.
23
25
  const actions = cashInMajorUnits(sourceActions);
@@ -70,8 +70,8 @@ const defaultDependencies: EarningsCallsHeadlessDependencies = {
70
70
  };
71
71
 
72
72
  /** A refused read says what the lock note says (CALLS is Pro whole), so a free account meets the same words as a signed-out one. */
73
- function explainDenied(ticker: string | null) {
74
- return (error: unknown): never => { throw explainRefusal(error, callsLockNote, { symbol: ticker }); };
73
+ function explainDenied(ticker: string | null, context: HeadlessPaneContext) {
74
+ return async (error: unknown): Promise<never> => { throw await explainRefusal(error, callsLockNote, { symbol: ticker, context }); };
75
75
  }
76
76
 
77
77
  function callRow(call: CloudEarningsCallPayload) {
@@ -285,7 +285,7 @@ export function createEarningsCallsHeadless(
285
285
  },
286
286
  async load(args, context) {
287
287
  const ticker = typeof args.argument === "string" ? args.argument : null;
288
- const calls = await dependencies.loadCalls(ticker, 200, context).catch(explainDenied(ticker));
288
+ const calls = await dependencies.loadCalls(ticker, 200, context).catch(explainDenied(ticker, context));
289
289
  if (!ticker || !readsTranscript(args)) {
290
290
  return projectEarningsCallsHeadless(calls, { ...args, options: { ...args.options, limit: args.options.limit ?? 50 } });
291
291
  }
@@ -299,7 +299,7 @@ export function createEarningsCallsHeadless(
299
299
  }
300
300
  throw new Error(`No ${quarter} earnings call found for ${ticker}.`);
301
301
  }
302
- const transcript = await dependencies.loadTranscript(selected.id, context).catch(explainDenied(ticker));
302
+ const transcript = await dependencies.loadTranscript(selected.id, context).catch(explainDenied(ticker, context));
303
303
  if (isPendingTranscript(transcript)) {
304
304
  throw new Error(`Transcript for ${ticker} ${quarter} is still being produced.`);
305
305
  }
@@ -1,17 +1,26 @@
1
1
  import type { PaneLock } from "../../../types/plugin";
2
- import { proLockNote } from "../shared/pro-lock-note";
2
+ import { resolveHeadlessInstrument } from "../shared/headless-market-data";
3
+ import { proLockNote, type FreeRoute } from "../shared/pro-lock-note";
4
+
5
+ const EVENTS_ROUTE: FreeRoute = {
6
+ text: (ticker, company) => `gloomberb events ${ticker} lists ${company}'s earnings dates, with the EPS reported at each and the next estimate.`,
7
+ answers: async (symbol, context) => {
8
+ const { symbol: ticker, exchange } = await resolveHeadlessInstrument(context, symbol);
9
+ return ((await context.marketData.getCorporateActions?.(ticker, exchange))?.earnings.length ?? 0) > 0;
10
+ },
11
+ };
3
12
 
4
13
  /**
5
14
  * CALLS is Pro whole, the list of calls included. `events` needs no account
6
15
  * and lists a company's earnings dates, each with the EPS it reported and the
7
16
  * next one's estimate, without the call or its transcript.
8
17
  */
9
- export function callsLockNote(lock: PaneLock): string {
18
+ export function callsLockNote(lock: PaneLock): Promise<string> {
10
19
  return proLockNote(lock, {
11
20
  token: "CALLS",
12
21
  argument: "optional",
13
22
  does: "it reads each earnings call into a transcript with speaker turns, analyst Q&A and extracted guidance",
14
23
  shows: "it",
15
- free: (ticker, company) => `gloomberb events ${ticker} lists ${company}'s earnings dates, with the EPS reported at each and the next estimate.`,
24
+ free: [EVENTS_ROUTE],
16
25
  });
17
26
  }
@@ -14,7 +14,8 @@ const CACHE_POLICY = {
14
14
  } as const;
15
15
 
16
16
  export type ImpactFilter = "high" | "medium" | "low" | "all";
17
- export type CountryFilter = "all" | "US" | "G7" | "EU";
17
+ /** A region the pane cycles through, or one country by its two-letter code (`econ --country TR`). */
18
+ export type CountryFilter = "all" | "US" | "G7" | "EU" | (string & {});
18
19
 
19
20
  export const FILTER_CYCLE: ImpactFilter[] = ["all", "high", "medium", "low"];
20
21
  /**
@@ -187,7 +188,38 @@ export function matchesCountry(event: EconEvent, filter: CountryFilter): boolean
187
188
  if (filter === "US") return event.country === "US";
188
189
  if (filter === "G7") return G7_COUNTRIES.has(event.country);
189
190
  if (filter === "EU") return event.country === "EU";
190
- return true;
191
+ return event.country === filter;
192
+ }
193
+
194
+ /**
195
+ * How long after its release time a high-impact print is still news: the
196
+ * calendar is asked for it this long, and a print that turns up later is
197
+ * recorded without a toast.
198
+ */
199
+ export const PRINT_PENDING_MS = 20 * 60 * 1000;
200
+
201
+ /** High-impact releases whose time has come in the last {@link PRINT_PENDING_MS} and that have no actual yet. */
202
+ export function pendingHighImpactPrints(events: readonly EconEvent[], now: number): EconEvent[] {
203
+ return events.filter((event) => {
204
+ if (event.impact !== "high" || event.actual) return false;
205
+ const releasedFor = now - event.date.getTime();
206
+ return releasedFor >= 0 && releasedFor <= PRINT_PENDING_MS;
207
+ });
208
+ }
209
+
210
+ export function hasPendingHighImpactPrint(events: readonly EconEvent[], now: number): boolean {
211
+ return pendingHighImpactPrints(events, now).length > 0;
212
+ }
213
+
214
+ /** High-impact rows that were blank in `previous` and have an actual in `next`. */
215
+ export function landedHighImpactPrints(
216
+ previous: ReadonlyMap<string, string | null>,
217
+ next: readonly EconEvent[],
218
+ ): EconEvent[] {
219
+ return next.filter((event) => {
220
+ if (event.impact !== "high" || !event.actual || !previous.has(event.id)) return false;
221
+ return !previous.get(event.id);
222
+ });
191
223
  }
192
224
 
193
225
  export function formatCountdown(ms: number): string {
@@ -228,6 +260,10 @@ export function getCalendarCache(options?: { allowExpired?: boolean }): EconCale
228
260
  return cache.get(CACHE_KEY, options);
229
261
  }
230
262
 
263
+ export function subscribeCalendar(listener: () => void): () => void {
264
+ return cache.subscribe(CACHE_KEY, listener);
265
+ }
266
+
231
267
  export interface EconCalendarLoadResult extends EconCalendarCacheEntry {
232
268
  refreshError?: string;
233
269
  }
@@ -1,4 +1,4 @@
1
- import { fredSeriesUrl } from "../../../sources/gloomberb-cloud/fred-series";
1
+ import { fredSeriesUrl, seriesNotOnFred } from "../../../sources/gloomberb-cloud/fred-series";
2
2
  import type { SeriesDefinition, SeriesFrequency } from "../../../series/types";
3
3
 
4
4
  type FredRow = readonly [seriesId: string, name: string, unit: string, frequency: SeriesFrequency, tags?: readonly string[]];
@@ -8,6 +8,8 @@ type FredRow = readonly [seriesId: string, name: string, unit: string, frequency
8
8
  * maturities (UST:2Y and so on, from the yield curve) and the credit spreads
9
9
  * and yields (from the credit plugin). Ids follow the server's allowlist: a
10
10
  * series added there is listed here to reach the catalog.
11
+ * Türkiye's CPI is served the same way but is not on FRED; its rows name
12
+ * TurkStat (`seriesNotOnFred`).
11
13
  */
12
14
  const GROUPS: ReadonlyArray<{ group: string; rows: readonly FredRow[] }> = [
13
15
  {
@@ -123,6 +125,8 @@ const GROUPS: ReadonlyArray<{ group: string; rows: readonly FredRow[] }> = [
123
125
  ["T5YIE", "5Y breakeven inflation", "%", "daily", ["breakeven", "expectations", "TIPS"]],
124
126
  ["T10YIE", "10Y breakeven inflation", "%", "daily", ["breakeven", "expectations", "TIPS"]],
125
127
  ["T5YIFR", "5Y, 5Y forward inflation expectation", "%", "daily", ["breakeven", "expectations", "5y5y"]],
128
+ ["TURCPIYOY", "Türkiye CPI, y/y", "%", "monthly", ["CPI", "headline", "Turkey", "TR", "inflation", "lira"]],
129
+ ["TURCPIMOM", "Türkiye CPI, m/m", "%", "monthly", ["CPI", "headline", "Turkey", "TR", "inflation", "lira"]],
126
130
  ],
127
131
  },
128
132
  {
@@ -191,7 +195,7 @@ export const FRED_CATALOG_SERIES: readonly SeriesDefinition[] = GROUPS.flatMap((
191
195
  frequency,
192
196
  group,
193
197
  ...(tags?.length ? { tags } : {}),
194
- publisher: "FRED",
198
+ publisher: seriesNotOnFred(seriesId)?.publisher ?? "FRED",
195
199
  url: fredSeriesUrl(seriesId),
196
200
  expression: `FRED:${seriesId}`,
197
201
  }))
@@ -51,12 +51,23 @@ const SERIES_MAP: Record<string, FredMapping> = {
51
51
  "unit labor costs q/q": { seriesId: "ULCNFB", displayMode: "qoq-annualized", relatedTickers: ["SPY", "^TNX"] },
52
52
  };
53
53
 
54
+ /** Other countries' releases Gloom Cloud serves a series for, by country and normalized title. */
55
+ const COUNTRY_SERIES_MAP: Record<string, Record<string, FredMapping>> = {
56
+ // TurkStat's CPI, published as percent changes; the related ticker is the US-listed MSCI Turkey ETF.
57
+ TR: {
58
+ "cpi y/y": { seriesId: "TURCPIYOY", displayMode: "level", relatedTickers: ["TUR"] },
59
+ "cpi m/m": { seriesId: "TURCPIMOM", displayMode: "level", relatedTickers: ["TUR"] },
60
+ },
61
+ };
62
+
54
63
  function normalizeEventTitle(title: string): string {
55
64
  return title.toLowerCase().trim();
56
65
  }
57
66
 
58
67
  export function resolveFredMapping(eventTitle: string, country: string): FredMapping | null {
59
- // Only US events have FRED data
68
+ const elsewhere = COUNTRY_SERIES_MAP[country];
69
+ if (elsewhere) return elsewhere[normalizeEventTitle(eventTitle)] ?? null;
70
+ // Otherwise only US events have FRED data
60
71
  if (country !== "US" && country !== "USD") return null;
61
72
 
62
73
  const normalized = normalizeEventTitle(eventTitle);