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
@@ -38,6 +38,7 @@ export function WebSelectField({
38
38
  const [open, setOpen] = useState(false);
39
39
  const buttonRef = useRef<HTMLButtonElement | null>(null);
40
40
  const inline = variant === "inline";
41
+ const well = variant === "well";
41
42
  const current = options.find((option) => option.value === value);
42
43
  const showUnset = includeUnsetOption && !current;
43
44
 
@@ -54,9 +55,9 @@ export function WebSelectField({
54
55
  setSelectRef.current = selectRef;
55
56
 
56
57
  const resolvedWidth = width ?? (inline ? "auto" : 184);
57
- // A field is one row inside its border, as a text field is; inline it is the row.
58
+ // A field is one row inside its border, as a text field is; inline it is the row, and a well has no border.
58
59
  const row = WEB_CELL_HEIGHT;
59
- const resolvedHeight = height ?? (inline ? row : row + 2);
60
+ const resolvedHeight = height ?? (inline || well ? row : row + 2);
60
61
  const style: CSSProperties = {
61
62
  width: resolvedWidth,
62
63
  height: resolvedHeight,
@@ -65,10 +66,11 @@ export function WebSelectField({
65
66
  justifyContent: "space-between",
66
67
  gap: "var(--gloom-space-xs)",
67
68
  color: disabled ? colors.textMuted : colors.text,
68
- // The field look: sunk into the surface, the control border, the accent while open.
69
- backgroundColor: inline ? "transparent" : "var(--gloom-surface-sunken)",
70
- border: inline ? "none" : `1px solid ${open ? "var(--gloom-accent)" : "var(--gloom-control-border)"}`,
71
- borderRadius: inline ? 0 : "var(--gloom-radius-md)",
69
+ // The field look: sunk into the surface, the control border, the accent while open. A well is the panel's
70
+ // fill with no border, for a dense form such as the order ticket.
71
+ backgroundColor: inline ? "transparent" : well ? "var(--gloom-panel)" : "var(--gloom-surface-sunken)",
72
+ border: inline || well ? "none" : `1px solid ${open ? "var(--gloom-accent)" : "var(--gloom-control-border)"}`,
73
+ borderRadius: inline ? 0 : well ? "var(--gloom-radius-sm)" : "var(--gloom-radius-md)",
72
74
  padding: inline ? 0 : "0 var(--gloom-space-sm)",
73
75
  cursor: disabled ? "default" : "pointer",
74
76
  font: "inherit",
@@ -18,6 +18,7 @@ function cellInset(value: unknown, axis: "x" | "y"): CSSProperties["paddingLeft"
18
18
  }
19
19
 
20
20
  export function commonStyle(props: Record<string, unknown>): CSSProperties {
21
+ const border = props.border;
21
22
  const hasFixedInlineSize = props.width != null || props.flexBasis != null;
22
23
  const hasFixedBlockSize = props.height != null;
23
24
  const gapUnit = props.flexDirection === "row" ? WEB_CELL_WIDTH : WEB_CELL_HEIGHT;
@@ -60,7 +61,11 @@ export function commonStyle(props: Record<string, unknown>): CSSProperties {
60
61
  ...(props.columnGap != null ? { columnGap: cellWidth(props.columnGap) as CSSProperties["columnGap"] } : {}),
61
62
  ...(props.rowGap != null ? { rowGap: cellHeight(props.rowGap) as CSSProperties["rowGap"] } : {}),
62
63
  overflow: props.overflow as CSSProperties["overflow"],
63
- border: props.border ? `1px solid ${typeof props.borderColor === "string" ? props.borderColor : "var(--gloom-border)"}` : undefined,
64
+ // A requested divider or accent is not a frame. Use longhands consistently
65
+ // so switching between one edge, all edges and no border clears stale sides.
66
+ borderStyle: props.border ? "solid" : undefined,
67
+ borderColor: props.border ? typeof props.borderColor === "string" ? props.borderColor : "var(--gloom-border)" : undefined,
68
+ borderWidth: Array.isArray(border) ? ["top", "right", "bottom", "left"].map(side => border.includes(side) ? "1px" : "0px").join(" ") : border ? "1px" : undefined,
64
69
  boxSizing: "border-box",
65
70
  minInlineSize: zeroMinInlineSize ? 0 : undefined,
66
71
  minBlockSize: zeroMinBlockSize ? 0 : undefined,
@@ -258,6 +258,11 @@ export function WebTabs({
258
258
  tab.onContextMenu?.(tab.value, event);
259
259
  } : undefined}
260
260
  >
261
+ {tab.leading !== undefined && (
262
+ <span data-gloom-role="tab-leading" aria-hidden="true" style={{ display: "flex", flex: "none", alignItems: "center" }}>
263
+ {tab.leading}
264
+ </span>
265
+ )}
261
266
  <span
262
267
  data-gloom-role="tab-label"
263
268
  style={{
@@ -349,6 +354,7 @@ export function WebTabs({
349
354
  items={tabs.map((tab) => ({
350
355
  id: tab.value,
351
356
  label: tab.label,
357
+ leading: tab.leading,
352
358
  disabled: tab.disabled,
353
359
  selected: tab.value === activeValue,
354
360
  }))}
@@ -70,6 +70,10 @@ button, [data-gloom-interactive="true"] { cursor: pointer; }
70
70
  [data-gloom-role="desktop-button"]:is([data-gloom-variant="plain"], [data-gloom-variant="ghost"], [data-gloom-variant="secondary"]):not(:disabled):not([aria-pressed="true"]):hover {
71
71
  background-color: var(--gloom-hover-fill);
72
72
  }
73
+ /* Quiet quick actions take the same wash, over a chosen one's fill too. */
74
+ [data-gloom-role="desktop-button"][data-gloom-variant="quiet"]:not(:disabled):hover {
75
+ background-color: var(--gloom-hover-fill) !important;
76
+ }
73
77
  [data-gloom-hover-bg="true"]:hover {
74
78
  background-color: var(--gloom-box-hover-bg) !important;
75
79
  }
@@ -941,12 +945,14 @@ html[data-gloom-screenshot="true"] [data-gloom-role="chart-watermark"] {
941
945
  }
942
946
  .gloom-qb-inline .gloom-qb-view { margin-left: 0; box-shadow: none; align-self: stretch; }
943
947
  /* A long inline choice (an expiry strip) scrolls by itself, as the terminal's
944
- does, so its label and the controls beside it stay in view. */
948
+ does, so its label and the controls beside it stay in view. It narrows with
949
+ its group and has no floor of its own: a floor wider than the room the bar
950
+ gives it drew its next choice under the control beside it. */
945
951
  .gloom-qb-filters[data-strip="true"],
946
952
  .gloom-qb-inline[data-strip="true"] { flex: 0 1 auto; min-width: 0; }
947
953
  .gloom-qb-inline[data-strip="true"] .gloom-qb-segments {
948
954
  flex: 0 1 auto;
949
- min-width: 12ch;
955
+ min-width: 0;
950
956
  overflow-x: auto;
951
957
  scrollbar-width: none;
952
958
  }
@@ -1354,8 +1360,9 @@ html[data-gloom-screenshot="true"] [data-gloom-role="chart-watermark"] {
1354
1360
  white-space: nowrap;
1355
1361
  }
1356
1362
 
1357
- /* Icon-only button: pane header, footer, dialog close, window controls. */
1358
- .gloom-icon-button {
1363
+ /* Icon-only button: pane header, footer, dialog close, window controls. A
1364
+ footer status icon (a delayed source's clock) takes the same box. */
1365
+ .gloom-icon-button, [data-gloom-role="pane-footer-icon"] {
1359
1366
  flex: none;
1360
1367
  display: inline-flex;
1361
1368
  align-items: center;
@@ -1372,7 +1379,7 @@ html[data-gloom-screenshot="true"] [data-gloom-role="chart-watermark"] {
1372
1379
  cursor: pointer;
1373
1380
  transition: background-color 120ms ease, color 120ms ease;
1374
1381
  }
1375
- .gloom-icon-button:disabled { cursor: default; }
1382
+ .gloom-icon-button:disabled, [data-gloom-role="pane-footer-icon"] { cursor: default; }
1376
1383
  .gloom-icon-button:not(:disabled):hover { background: var(--gloom-hover-fill); color: var(--gloom-text-bright); }
1377
1384
  .gloom-icon-button[aria-pressed="true"] { color: var(--gloom-warning); }
1378
1385
  .gloom-qb-view button:disabled { color: var(--gloom-text-muted); opacity: 0.55; cursor: default; }
@@ -30,6 +30,7 @@ const BROKER_INVOKE_OPERATIONS = new Set([
30
30
  "getChartResolutionCapabilities",
31
31
  "getOptionsChain",
32
32
  "listOpenOrders",
33
+ "getOrderStatus",
33
34
  "listExecutions",
34
35
  "previewOrder",
35
36
  "placeOrder",
@@ -116,6 +117,8 @@ function invokeBrokerOperation(
116
117
  case "previewOrder":
117
118
  case "placeOrder":
118
119
  return (broker[operation] as any)?.call(broker, instance, args[0]);
120
+ case "getOrderStatus":
121
+ return broker.getOrderStatus?.(instance, args[0] as number);
119
122
  case "modifyOrder":
120
123
  return broker.modifyOrder?.(instance, args[0] as number, args[1] as never);
121
124
  case "cancelOrder":
@@ -167,6 +170,15 @@ function createBrokerCapability(options: CoreCapabilityOptions): PluginCapabilit
167
170
  },
168
171
  );
169
172
  }),
173
+ orders: stream((input: any, emit) => {
174
+ const instanceId = requireString(input.instanceId, "Broker instance");
175
+ const { instance, broker } = resolveBrokerInstance(options, instanceId);
176
+ if (!broker.subscribeOrders) throw new Error(`Broker "${broker.name}" does not stream orders.`);
177
+ return broker.subscribeOrders(instance, {
178
+ order: (order) => emit({ kind: "order", order }),
179
+ live: (live) => emit({ kind: "orders-live", live }),
180
+ });
181
+ }),
170
182
  removeInstance: op(async (input: any) => {
171
183
  const { instance, broker } = resolveBrokerInstance(options, input.instanceId);
172
184
  await broker.disconnect?.(instance);
@@ -14,6 +14,7 @@ import {
14
14
  loadExternalPlugin,
15
15
  } from "../../../plugins/loader";
16
16
  import { createNodePluginManager } from "../../../plugins/manager-node";
17
+ import { readPluginIcon } from "../../../plugins/plugin-icon";
17
18
  import type { PluginRegistry } from "../../../plugins/registry";
18
19
  import { desktopRendererCapabilityManifests } from "./desktop/initialization";
19
20
  import { reportCrash } from "../../../telemetry/crash-reports";
@@ -70,6 +71,7 @@ async function bundlePluginDirectory(
70
71
 
71
72
  const { plugin } = loaded;
72
73
  const directory = basename(pluginDir);
74
+ const icon = readPluginIcon(pluginDir);
73
75
  const base = {
74
76
  id: plugin.id,
75
77
  name: plugin.name,
@@ -80,6 +82,7 @@ async function bundlePluginDirectory(
80
82
  ...(loaded.commit ? { commit: loaded.commit } : {}),
81
83
  ...(loaded.repo ? { repo: loaded.repo } : {}),
82
84
  ...(loaded.linked ? { linked: true } : {}),
85
+ ...(icon ? { icon } : {}),
83
86
  ...(plugin.targets ? { targets: plugin.targets } : {}),
84
87
  };
85
88
 
@@ -130,6 +130,8 @@ export interface DesktopExternalPluginBundle {
130
130
  /** `owner/repo` of the checkout's GitHub origin. */
131
131
  repo?: string;
132
132
  linked?: boolean;
133
+ /** The plugin's logo as a `data:` URL, read from the checkout by the Bun process. */
134
+ icon?: string;
133
135
  /** ES module source, absent when `error` or `unsupportedTarget` is set. */
134
136
  code?: string;
135
137
  targets?: readonly ("cli" | "tui" | "desktop" | "web")[];
@@ -125,6 +125,22 @@ const client: BrokerRemoteClient = {
125
125
  });
126
126
  },
127
127
 
128
+ subscribeOrders(instanceId, listener) {
129
+ return subscribeBackendCapability({
130
+ subscriptionId: `broker-orders:${instanceId}:${nextSubscriptionId++}`,
131
+ capabilityId: BROKER_CAPABILITY_ID,
132
+ operationId: "orders",
133
+ payload: { instanceId },
134
+ onEvent: (message) => {
135
+ const event = message as BrokerRemoteEvent;
136
+ if (event.kind === "order") listener.order(event.order);
137
+ else if (event.kind === "orders-live") listener.live?.(event.live);
138
+ },
139
+ // A stream that cannot open leaves the host polling open orders.
140
+ onError: (error) => { listener.live?.(false); console.error("Failed to subscribe to broker orders", error); },
141
+ });
142
+ },
143
+
128
144
  async removeInstance(instanceId) {
129
145
  await invokeBrokerCapability("removeInstance", { instanceId });
130
146
  statuses.delete(instanceId);
@@ -28,6 +28,7 @@ export async function loadDesktopExternalPlugin(bundle: DesktopExternalPluginBun
28
28
  ...(bundle.commit ? { commit: bundle.commit } : {}),
29
29
  ...(bundle.repo ? { repo: bundle.repo } : {}),
30
30
  ...(bundle.linked ? { linked: true } : {}),
31
+ ...(bundle.icon ? { icon: bundle.icon } : {}),
31
32
  };
32
33
  const fallback = {
33
34
  ...base,
@@ -14,8 +14,8 @@ const DRAW_STYLES: readonly SeriesDrawStyle[] = ["line", "area", "step", "column
14
14
  const PLACEMENTS: readonly SeriesPlacement[] = ["price", "panel"];
15
15
  const LINE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
16
16
  const COLOR_PATTERN = /^#[0-9A-Fa-f]{6}$/;
17
- /** A chart holds ten series: the ticker's price and the lines, with one to spare. */
18
- const MAX_SERIES_LINES = 8;
17
+ /** A chart holds twenty series: the ticker's price and up to sixteen lines, with room for a few more. */
18
+ const MAX_SERIES_LINES = 16;
19
19
 
20
20
  /** Units with no scale: a percentage of something, not a quantity of it. */
21
21
  const RATE_UNITS = ["%", "bp", "x", "ratio", "index", "score", "points"] as const;
@@ -53,9 +53,34 @@ export function resetFredSeriesPersistence(): void {
53
53
  cache.reset();
54
54
  }
55
55
 
56
- /** The series' page on the FRED site, where its notes and release schedule live. */
56
+ /** Who publishes a series Gloom Cloud serves in FRED's shape that FRED does not carry, and its page. */
57
+ interface SeriesNotOnFred {
58
+ /** Who compiles the figures (TurkStat). */
59
+ publisher: string;
60
+ /** Whose publication they are read from (the CBRT). */
61
+ via: string;
62
+ url: string;
63
+ }
64
+
65
+ const TURKISH_CPI: SeriesNotOnFred = {
66
+ publisher: "TurkStat",
67
+ via: "the CBRT",
68
+ url: "https://www.tcmb.gov.tr/wps/wcm/connect/EN/TCMB+EN/Main+Menu/Statistics/Inflation+Data/Consumer+Prices",
69
+ };
70
+
71
+ const NOT_ON_FRED: Readonly<Record<string, SeriesNotOnFred>> = {
72
+ TURCPIYOY: TURKISH_CPI,
73
+ TURCPIMOM: TURKISH_CPI,
74
+ };
75
+
76
+ /** The publisher of a served series FRED does not carry; null for FRED's own. */
77
+ export function seriesNotOnFred(seriesId: string): SeriesNotOnFred | null {
78
+ return NOT_ON_FRED[seriesId.trim().toUpperCase()] ?? null;
79
+ }
80
+
81
+ /** The series' page on the FRED site, where its notes and release schedule live, or its publisher's page when FRED does not carry it. */
57
82
  export function fredSeriesUrl(seriesId: string): string {
58
- return `https://fred.stlouisfed.org/series/${seriesId}`;
83
+ return seriesNotOnFred(seriesId)?.url ?? `https://fred.stlouisfed.org/series/${seriesId}`;
59
84
  }
60
85
 
61
86
  function cacheKey(request: FredSeriesRequest): string {
@@ -45,6 +45,7 @@ import { isRoundTheClockCoin } from "../../utils/crypto-pair";
45
45
  import { canonicalExchange, canonicalTickerKey, parsePublicTickerKey } from "../../utils/exchanges";
46
46
  import { normalizePriceHistory, priceHistoryIntervalMs, reachesLatestSettledSession } from "../../utils/price-history";
47
47
  import { createProviderMiss, ProviderBusyError, providerMissReason } from "../provider-errors";
48
+ import { parseListingDelisting } from "../../market-data/quotes/delisting";
48
49
  import { isRetryableApiError } from "../../api-client/errors";
49
50
  import { assertSecRegistrantMatches } from "../sec-registrant";
50
51
  import { nonUsSecListingVenue } from "../../utils/sec";
@@ -251,9 +252,11 @@ function mapCloudPriceHistory(
251
252
  }
252
253
  // Sessions of daily bars the answer left out for a close at or below zero.
253
254
  const nonPositiveCloseDates = resolution === "1d" ? cloudNonPositiveCloseDates(response.omittedSessions) : undefined;
255
+ const delisting = parseListingDelisting(response.delisting);
254
256
  return {
255
257
  points,
256
258
  resolution,
259
+ ...(delisting ? { delisting } : {}),
257
260
  ...(session && matchingSource && matchingIdentity ? { session } : {}),
258
261
  ...(nonPositiveCloseDates ? { nonPositiveCloseDates } : {}),
259
262
  ...(coverageStart ? { coverageStart } : {}),
@@ -262,6 +265,28 @@ function mapCloudPriceHistory(
262
265
  };
263
266
  }
264
267
 
268
+ const DAY_MS = 86_400_000;
269
+ /** Listings Gloom Cloud said no longer trade, by listing key: the day after each one's last session. */
270
+ const delistedHistoryEnds = new Map<string, Date>();
271
+
272
+ /**
273
+ * Where a trailing window ends for a listing that no longer trades: the day
274
+ * after its last session, as a window ending now ends the day after today's.
275
+ * A window ending now would hold none of its bars, or only the last few.
276
+ */
277
+ function delistedHistoryEnd(response: CloudMarketResponse<unknown>, key: string): Date | null {
278
+ const delisting = parseListingDelisting(response.delisting);
279
+ if (!delisting) {
280
+ // The symbol trades again (another issuer took it): its windows end now.
281
+ delistedHistoryEnds.delete(key);
282
+ return null;
283
+ }
284
+ const end = new Date(Date.parse(`${delisting.lastTradeDate}T00:00:00Z`) + DAY_MS);
285
+ if (delistedHistoryEnds.size >= 1024) delistedHistoryEnds.clear();
286
+ delistedHistoryEnds.set(key, end);
287
+ return end;
288
+ }
289
+
265
290
  function cloudHistoryResolution(interval: string): ManualChartResolution | null {
266
291
  const canonical = canonicalHistoryInterval(interval);
267
292
  return (["1m", "5m", "15m", "30m", "1h", "1d", "1wk", "1mo"] as const)
@@ -549,14 +574,14 @@ export class GloomberbCloudProvider implements AssetDataProvider {
549
574
  // A successful batch can contain an expired cache fallback for only
550
575
  // one listing. Match the single-quote freshness boundary and let the
551
576
  // normal router retry that item without discarding its healthy peers.
552
- if (item.stale === true) {
577
+ // A listing that no longer trades has only that last quote: kept, flagged stale.
578
+ const delisted = !!parseListingDelisting(item.data?.delisting);
579
+ if (item.stale === true && !delisted) {
553
580
  return { target, quote: null, error: createProviderMiss(`Cloud quotes are stale for ${target.symbol}`) };
554
581
  }
555
582
  if ((item.status === "success" || item.status === "partial") && item.data) {
556
- return {
557
- target,
558
- quote: retainRequestedQuoteSymbol(mapQuote(item.data), target.symbol),
559
- };
583
+ const quote = retainRequestedQuoteSymbol(mapQuote(item.data), target.symbol);
584
+ return { target, quote: item.stale === true ? { ...quote, stale: true } : quote };
560
585
  }
561
586
  return {
562
587
  target,
@@ -778,19 +803,27 @@ export class GloomberbCloudProvider implements AssetDataProvider {
778
803
  const target = cloudInstrumentTarget(ticker, exchange);
779
804
  exchange = target.exchange ?? "";
780
805
  const interval = toCloudInterval(resolution);
781
- const endDate = new Date();
782
- const startDate = subtractTimeRange(endDate, bufferRange);
783
806
  const includeTime = /^\d+(min|h)$/i.test(interval);
784
- const { response, extendedHours } = await withCloudFallback(
785
- () => loadCloudHistory(interval, (session) => apiClient.getCloudHistory(target.symbol, exchange, {
786
- interval,
787
- startDate: formatCloudDateTime(startDate, includeTime, exchange),
788
- endDate: formatCloudHistoryEnd(endDate, includeTime, exchange),
789
- ...(session ? { session } : {}),
790
- }), _context),
791
- `Cloud chart data is unavailable for ${ticker}`,
792
- );
793
- return mapCloudPriceHistory(response, target.symbol, exchange, interval, startDate, endDate, extendedHours);
807
+ const listingKey = canonicalTickerKey(target.symbol, exchange);
808
+ const load = (endDate: Date) => {
809
+ const startDate = subtractTimeRange(endDate, bufferRange);
810
+ return withCloudFallback(
811
+ () => loadCloudHistory(interval, (session) => apiClient.getCloudHistory(target.symbol, exchange, {
812
+ interval,
813
+ startDate: formatCloudDateTime(startDate, includeTime, exchange),
814
+ endDate: formatCloudHistoryEnd(endDate, includeTime, exchange),
815
+ ...(session ? { session } : {}),
816
+ }), _context),
817
+ `Cloud chart data is unavailable for ${ticker}`,
818
+ ).then((answer) => ({ ...answer, startDate, endDate }));
819
+ };
820
+ // A listing that no longer trades is read up to its last session, not up to now.
821
+ const knownEnd = delistedHistoryEnds.get(listingKey);
822
+ const now = new Date();
823
+ let answer = await load(knownEnd ?? now);
824
+ const delistedEnd = delistedHistoryEnd(answer.response, listingKey);
825
+ if (delistedEnd ? delistedEnd.getTime() < answer.endDate.getTime() : knownEnd) answer = await load(delistedEnd ?? now);
826
+ return mapCloudPriceHistory(answer.response, target.symbol, exchange, interval, answer.startDate, answer.endDate, answer.extendedHours);
794
827
  }
795
828
 
796
829
  async getDetailedPriceHistory(
@@ -27,6 +27,7 @@ import { createProviderMiss } from "../provider-errors";
27
27
  import { reconcileQuoteDayRange } from "../../market-data/quotes/day-range";
28
28
  import { redactUnavailableFundamentals } from "../../utils/fundamentals";
29
29
  import { readWithdrawnObservations } from "../../utils/statement-gaps";
30
+ import { parseListingDelisting } from "../../market-data/quotes/delisting";
30
31
 
31
32
  export const GLOOMBERB_CLOUD_PROVIDER_ID = "gloomberb-cloud" as const;
32
33
 
@@ -94,6 +95,7 @@ export function mapQuote(
94
95
  : futuresBasis ?? quote.priceBasis;
95
96
  return withoutUndefinedFields(reconcileQuoteDayRange({
96
97
  ...quote,
98
+ delisting: parseListingDelisting(quote.delisting),
97
99
  priceBasis,
98
100
  priceObservation,
99
101
  currency: currency || quote.currency,
@@ -130,6 +132,8 @@ export function mapQuote(
130
132
  divisor,
131
133
  ),
132
134
  extendedSessionPrint: typeof quote.extendedSessionPrint === "boolean" ? quote.extendedSessionPrint : undefined,
135
+ // A coin's move over 24 hours, or none, when its previous close is undatable.
136
+ changeBasis: quote.changeBasis === "24h" || quote.changeBasis === "none" ? quote.changeBasis : undefined,
133
137
  overnightPrice: normalizePriceValueByDivisor(quote.overnightPrice, divisor),
134
138
  overnightChange: normalizePriceValueByDivisor(quote.overnightChange, divisor),
135
139
  overnightPriceKind: quote.overnightPriceKind === "trade" || quote.overnightPriceKind === "mark"
@@ -6,6 +6,7 @@ import { parseHistorySession } from "../market-data/history-session";
6
6
  import { canonicalHistoryInterval } from "./history-retention";
7
7
  import { historyCoverageNoteText, isHistoryCoverageDate } from "./history-coverage";
8
8
  import { readNonPositiveCloseDates } from "./history-omitted";
9
+ import { parseListingDelisting } from "../market-data/quotes/delisting";
9
10
 
10
11
  export type HistoryResultRequest =
11
12
  | { kind: "range"; range: TimeRange }
@@ -38,6 +39,7 @@ export function normalizeHistoryResult(
38
39
  // A contradictory declaration cannot be laundered into unknown legacy data.
39
40
  if (result.session != null && (!session || result.resolution === null)) return null;
40
41
  const nonPositiveCloseDates = readNonPositiveCloseDates(result.nonPositiveCloseDates);
42
+ const delisting = parseListingDelisting(result.delisting);
41
43
  return { points: result.points, resolution: result.resolution,
42
44
  ...(session ? { session } : {}),
43
45
  ...(nonPositiveCloseDates ? { nonPositiveCloseDates } : {}),
@@ -47,6 +49,7 @@ export function normalizeHistoryResult(
47
49
  ? { coverageNote: historyCoverageNoteText(result.coverageNote) } : {}),
48
50
  ...(result.extendedHours === true ? { extendedHours: true } : {}),
49
51
  ...(result.savedCopyBehind === true ? { savedCopyBehind: true as const } : {}),
52
+ ...(delisting ? { delisting } : {}),
50
53
  };
51
54
  }
52
55
 
@@ -1,4 +1,6 @@
1
- const PROVIDER_MISS_BRAND = Symbol("provider-miss");
1
+ // Registered, so a miss raised by another copy of this module (a plugin that
2
+ // resolved `gloomberb/market-data` from the source tree) is still recognized.
3
+ const PROVIDER_MISS_BRAND = Symbol.for("gloomberb.provider-miss");
2
4
  const EXPECTED_PROVIDER_MISS = /No data found|symbol may be delisted|"code":"Not Found"|No history for /i;
3
5
 
4
6
  const MAX_REASON_LENGTH = 200;
@@ -4,6 +4,7 @@ import { canonicalHistoryInterval, HistoryRetentionError, isHistoryRetentionErro
4
4
  type HistoryRecoveryCandidate, type HistoryRetention, type HistorySourceOutcome } from "../history-retention";
5
5
  import type { BrokerCandidate } from "./brokers";
6
6
  import { withBrokerTimeout } from "./brokers";
7
+ import type { BrokerHistoryOptions } from "../../types/broker";
7
8
  import type { DataProvider, MarketDataRequestContext } from "../../types/data-provider";
8
9
  import type { PriceHistoryResult } from "../../types/price-history";
9
10
  import { fetchHistoryResult, historyResolutionForInterval, InvalidHistoryResultError, normalizeHistoryResult } from "../history-result";
@@ -244,6 +245,29 @@ function mergeRequestHistoryGaps(
244
245
  })));
245
246
  }
246
247
 
248
+ /** The extended session goes only to a broker that serves it, and only when the chart asked. */
249
+ function brokerHistoryOptions(candidate: BrokerCandidate, context?: MarketDataRequestContext): [BrokerHistoryOptions] | [] {
250
+ return context?.historySession === "extended" && candidate.broker.extendedHoursHistory === true ? [{ session: "extended" }] : [];
251
+ }
252
+
253
+ /**
254
+ * With Ext hours on, a broker that serves only the regular session answers
255
+ * after the market-data providers, which may hold the extended session; it is
256
+ * still the last resort before nothing. Otherwise the order is unchanged.
257
+ */
258
+ function brokersForSession(candidates: BrokerCandidate[], context?: MarketDataRequestContext): { first: BrokerCandidate[]; last: BrokerCandidate[] } {
259
+ if (context?.historySession !== "extended") return { first: candidates, last: [] };
260
+ return {
261
+ first: candidates.filter((candidate) => candidate.broker.extendedHoursHistory === true),
262
+ last: candidates.filter((candidate) => candidate.broker.extendedHoursHistory !== true),
263
+ };
264
+ }
265
+
266
+ function isIntradayInterval(interval: string | undefined): boolean {
267
+ const ms = interval ? priceHistoryIntervalMs(interval) : null;
268
+ return ms != null && ms < 24 * 60 * 60 * 1000;
269
+ }
270
+
247
271
  function normalizeRequestResult(value: PriceHistoryResult, request: HistoryRequestDescriptor): PriceHistoryResult {
248
272
  // Only this router says a copy is behind, when it answers: neither a source
249
273
  // nor a stored copy can carry that verdict in.
@@ -254,8 +278,12 @@ function normalizeRequestResult(value: PriceHistoryResult, request: HistoryReque
254
278
  };
255
279
  }
256
280
 
257
- /** A copy fetched at `fetchedAt`, now for a source's answer. */
281
+ /**
282
+ * A copy fetched at `fetchedAt`, now for a source's answer. A listing that no
283
+ * longer trades is never behind: its bars end on its last session for good.
284
+ */
258
285
  function resultIsStale(value: PriceHistoryResult, symbol: string, exchange: string, fetchedAt: number, intervalMs?: number | null): boolean {
286
+ if (value.delisting) return false;
259
287
  return isPriceHistoryStaleForCurrentWindow(value.points, Date.now(), {
260
288
  symbol, exchange, intervalMs: intervalMs ?? (value.resolution ? priceHistoryIntervalMs(value.resolution) : undefined), session: value.session,
261
289
  fetchedAt,
@@ -425,6 +453,7 @@ export class ProviderRouterHistoryRoutes {
425
453
  bufferRange,
426
454
  resolution,
427
455
  context?.instrument ?? null,
456
+ ...brokerHistoryOptions(candidate, context),
428
457
  )
429
458
  : null,
430
459
  fetchProvider: (provider) => fetchHistoryResult(provider, ticker, exchange, { kind: "resolution", range: bufferRange, resolution }, context),
@@ -543,6 +572,7 @@ export class ProviderRouterHistoryRoutes {
543
572
  endDate,
544
573
  barSize,
545
574
  context?.instrument ?? null,
575
+ ...brokerHistoryOptions(candidate, context),
546
576
  )
547
577
  : null,
548
578
  fetchProvider: (provider) => fetchHistoryResult(provider, ticker, exchange, { kind: "detail", start: startDate, end: endDate, interval: barSize }, context),
@@ -551,10 +581,13 @@ export class ProviderRouterHistoryRoutes {
551
581
 
552
582
  private async executeHistoryRequest(request: HistoryRequestDescriptor): Promise<PriceHistoryResult> {
553
583
  const recovery = this.validateRecovery(request);
554
- const brokerCandidates = recovery ? [] : this.deps.getBrokerCandidatesForContext(request.context, false);
584
+ const { first: brokerCandidates, last: regularSessionBrokers } = brokersForSession(
585
+ recovery ? [] : this.deps.getBrokerCandidatesForContext(request.context, false), request.context);
586
+ // Cached copies rank in the order the sources are asked.
555
587
  const sourceKeys = recovery ? [recovery.sourceKey] : [
556
588
  ...brokerCandidates.map((candidate) => this.deps.brokerSourceKey(candidate)),
557
589
  ...this.deps.getProviderSourceKeys(),
590
+ ...regularSessionBrokers.map((candidate) => this.deps.brokerSourceKey(candidate)),
558
591
  ];
559
592
  const cachedRecords = listCachedResources<unknown>(
560
593
  this.deps.resources,
@@ -584,6 +617,7 @@ export class ProviderRouterHistoryRoutes {
584
617
  const currentWindow = request.requestedEnd === undefined || isCurrentHistoryWindow(new Date(request.requestedEnd));
585
618
  const now = Date.now();
586
619
  const fetchState = (record: { fetchedAt: number; value: PriceHistoryResult }, checkedAt?: number): CalendarHistoryFetchState => currentWindow
620
+ && !record.value.delisting
587
621
  ? calendarHistoryFetchState(record.value.points, record.fetchedAt, now, {
588
622
  symbol: target.symbol,
589
623
  exchange: target.exchange || request.target.exchange,
@@ -661,6 +695,12 @@ export class ProviderRouterHistoryRoutes {
661
695
  });
662
696
  if (providerResult && hasUsablePriceHistory(providerResult.value.points)) return withReportedGaps(providerResult.value);
663
697
  if (providerResult) reportedGaps.push(providerResult.value);
698
+ if (regularSessionBrokers.length) {
699
+ const regularResult = await withBrokerTimeout(this.fetchBrokerHistory(request, regularSessionBrokers, onUnavailable, attempts));
700
+ for (const sourceKey of [...attempts.pending]) recordHistoryOutcome(attempts, sourceKey, "timeout");
701
+ if (regularResult && hasUsablePriceHistory(regularResult.value.points)) return withReportedGaps(regularResult.value);
702
+ if (regularResult) reportedGaps.push(regularResult.value);
703
+ }
664
704
  // A failed refetch falls back in the same order the cache hit used, so a
665
705
  // failing request answers what the next request will: a current copy
666
706
  // first, else a saved copy short of the latest session, which beats an
@@ -722,10 +762,12 @@ export class ProviderRouterHistoryRoutes {
722
762
  private async refreshHistory(request: HistoryRequestDescriptor): Promise<void> {
723
763
  try {
724
764
  const recovery = this.validateRecovery(request);
725
- const brokerCandidates = recovery ? [] : this.deps.getBrokerCandidatesForContext(request.context, false);
726
- const brokerResult = await withBrokerTimeout(this.fetchBrokerHistory(request, brokerCandidates));
765
+ const { first, last } = brokersForSession(recovery ? [] : this.deps.getBrokerCandidatesForContext(request.context, false), request.context);
766
+ const brokerResult = await withBrokerTimeout(this.fetchBrokerHistory(request, first));
727
767
  if (brokerResult && hasUsablePriceHistory(brokerResult.value.points)) return;
728
- await this.fetchProviderHistory(request);
768
+ const providerResult = await this.fetchProviderHistory(request);
769
+ if (providerResult && hasUsablePriceHistory(providerResult.value.points)) return;
770
+ if (last.length) await withBrokerTimeout(this.fetchBrokerHistory(request, last));
729
771
  } catch (error) {
730
772
  logFailure("sources.provider-router.history", "Refreshing background price history failed")(error);
731
773
  }
@@ -745,7 +787,8 @@ export class ProviderRouterHistoryRoutes {
745
787
  const fetched = await request.fetchBroker(candidate);
746
788
  if (fetched === null) { recordHistoryOutcome(attempts, sourceKey, "empty"); return null; }
747
789
  if (!Array.isArray(fetched)) { recordHistoryOutcome(attempts, sourceKey, "malformed"); return null; }
748
- const value = normalizeRequestResult({ points: fetched, resolution: historyResolutionForInterval(request.interval), sourceKey }, request);
790
+ const value = normalizeRequestResult({ points: fetched, resolution: historyResolutionForInterval(request.interval), sourceKey,
791
+ ...(brokerHistoryOptions(candidate, request.context).length && isIntradayInterval(request.interval) ? { extendedHours: true } : {}) }, request);
749
792
  if (hasUsablePriceHistory(value.points) && request.isFetchedValueStale(value)) { recordHistoryOutcome(attempts, sourceKey, "stale"); return null; }
750
793
  recordHistoryOutcome(attempts, sourceKey, hasUsablePriceHistory(value.points) ? "success" : value.points.length ? "reported-gaps" : "empty");
751
794
  onUnavailable?.(this.deps.brokerSourceKey(candidate), value);
@@ -412,7 +412,8 @@ export class AssetDataRouter implements DataProvider {
412
412
  }
413
413
 
414
414
  async getOptionsChain(ticker: string, exchange?: string, expirationDate?: number, context?: MarketDataRequestContext): Promise<OptionsChain> {
415
- return (await this.getCachedQuery("getOptionsChain", [ticker, exchange, expirationDate, context]).load({ force: context?.cacheMode === "refresh", background: this.revalidateInBackground })).value;
415
+ return (await this.getCachedQuery("getOptionsChain", [ticker, exchange, expirationDate, context])
416
+ .load({ force: context?.cacheMode === "refresh", background: this.revalidateInBackground && context?.cacheMode !== "current" })).value;
416
417
  }
417
418
 
418
419
  getCachedQuery<K extends CachedAssetMethod>(method: K, args: CachedAssetArgs<K>) {
@@ -190,6 +190,19 @@ export function usePaneInView(): boolean {
190
190
  return useContext(PaneInViewContext);
191
191
  }
192
192
 
193
+ /**
194
+ * Whether the pane around the caller is the focused one, as its frame draws
195
+ * it (a pane under the open command bar is not). Null outside a pane.
196
+ */
197
+ const PaneFocusedContext = createContext<boolean | null>(null);
198
+
199
+ export const PaneFocusedProvider = PaneFocusedContext.Provider;
200
+
201
+ /** For chrome a pane shows only while focused (a chart's drawing tools); null outside a pane. */
202
+ export function usePaneFocused(): boolean | null {
203
+ return useContext(PaneFocusedContext);
204
+ }
205
+
193
206
  /**
194
207
  * Gate a pane's market data (stream priority, polls, refresh clocks) on this:
195
208
  * the app can be seen and the pane is on screen. A covered pane should keep
@@ -13,6 +13,7 @@ import {
13
13
  type SyncSnapshot,
14
14
  type SyncTransport,
15
15
  } from "./types";
16
+ import type { LocalMarketDataLookup } from "./local-only";
16
17
 
17
18
  type CloudSyncPhase = "idle" | "disabled" | "syncing" | "synced" | "error";
18
19
 
@@ -32,6 +33,8 @@ interface SyncRuntime {
32
33
  getContributors: () => RegisteredSyncContributor[];
33
34
  getTransport: () => RegisteredSyncTransport | null;
34
35
  baselineStore?: SyncBaselineStore;
36
+ /** Sources whose quotes and bars never leave this device (`BrokerAdapter.localMarketData`); none when absent. */
37
+ isLocalMarketData?: LocalMarketDataLookup;
35
38
  }
36
39
 
37
40
  const CLIENT_ID_STORAGE_KEY = "gloomberb.sync.clientId";
@@ -357,6 +360,7 @@ export class CloudSyncController {
357
360
  state: runtime.getState(),
358
361
  baselinePayload: this.loadBaseline(runtime)?.[entry.contributor.id] ?? null,
359
362
  memo: this.memoFor(runtime, entry.contributor.id),
363
+ isLocalMarketData: runtime.isLocalMarketData,
360
364
  });
361
365
  payloads[entry.contributor.id] = {
362
366
  schemaVersion: entry.contributor.schemaVersion,