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
@@ -1,4 +1,6 @@
1
1
  import type { PluginModule } from "../plugin-module";
2
+ import type { PaneLock } from "../../../types/plugin";
3
+ import { filesForm } from "../shared/free-routes";
2
4
  import { proLockNote } from "../shared/pro-lock-note";
3
5
  import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
4
6
  import {
@@ -18,14 +20,21 @@ const description =
18
20
  * RISK is Pro whole (`useProFeatureWall`). The 10-Ks it reads are in the SEC
19
21
  * filing list, which needs no account: `filings` lists them with a link to
20
22
  * each, where Item 1A is the risk factors as written, without RISK's grouping
21
- * or its changes since the year before.
23
+ * or its changes since the year before. A foreign issuer files a 20-F instead
24
+ * (ASML), whose Item 3.D holds them.
22
25
  */
23
- function riskFactorsLockNote(lock: { gate: "sign-in" | "pro"; symbol: string | null }): string {
26
+ function riskFactorsLockNote(lock: PaneLock): Promise<string> {
24
27
  return proLockNote(lock, {
25
28
  token: "RISK",
26
29
  does: "it groups a 10-K's risk factors by theme and shows what was added, dropped or reworded since the year before",
27
30
  shows: "it",
28
- free: (ticker, company) => `gloomberb filings ${ticker} --form 10-K lists ${company}'s annual reports, each with its filing date and a link to the filing, whose Item 1A holds the risk factors as written.`,
31
+ free: [{
32
+ text: (ticker, company) => `gloomberb filings ${ticker} --form 10-K lists ${company}'s annual reports, each with its filing date and a link to the filing, whose Item 1A holds the risk factors as written.`,
33
+ answers: (symbol, context) => filesForm(symbol, "10-K", context),
34
+ }, {
35
+ text: (ticker, company) => `gloomberb filings ${ticker} --form 20-F lists ${company}'s annual reports, each with its filing date and a link to the filing, whose Item 3.D holds the risk factors as written.`,
36
+ answers: (symbol, context) => filesForm(symbol, "20-F", context),
37
+ }],
29
38
  });
30
39
  }
31
40
 
@@ -12,6 +12,7 @@ import { usePaneStatusFooter } from "../../../components/layout/pane/status-foot
12
12
  import { CLOUD_QUOTE_DELAY_MINUTES } from "../../../api-client/plan-access";
13
13
  import { useAppVisible } from "../../../state/app/activity";
14
14
  import { ScannerClientContext } from "../shared/scanner-capture";
15
+ import { delayedFooterSegment } from "../../../components/layout/pane/footer/model";
15
16
 
16
17
  export interface ScannerFeedState<T extends ScannerPayload> {
17
18
  payload: T | null;
@@ -102,7 +103,11 @@ export function useScannerStatusFooter(
102
103
  const info = useMemo<PaneFooterSegment[]>(
103
104
  () => {
104
105
  const base = segment ? [segment, ...status]
105
- : delayMinutes != null ? [{ id: "scanner-delay", parts: [{ text: tf("{delay} delayed", { delay: tf("{count}m", { count: delayMinutes }) }), tone: "muted" as const }] }, ...status]
106
+ : delayMinutes != null ? [delayedFooterSegment({
107
+ id: "scanner-delay",
108
+ detail: tf("{delay} delayed", { delay: tf("{count}m", { count: delayMinutes }) }),
109
+ delay: tf("{count}m", { count: delayMinutes }),
110
+ }), ...status]
106
111
  : status;
107
112
  return extra ? [...base, extra] : base;
108
113
  },
@@ -236,7 +236,7 @@ function createFlowHeadless(
236
236
  describe: "Options Flow",
237
237
  async load(args, ctx) {
238
238
  const payload = await dependencies.load(args, ctx.apiClient, ctx.signal)
239
- .catch((error: unknown) => { throw explainRefusal(error, flowLockNote, { symbol: null }); });
239
+ .catch(async (error: unknown) => { throw await explainRefusal(error, flowLockNote, { symbol: null }); });
240
240
  return projectFlowSnapshot(payload, {
241
241
  ...DEFAULT_FLOW_FILTERS,
242
242
  ...(args.options as Partial<FlowFilters>),
@@ -5,13 +5,14 @@ import { proLockNote } from "../shared/pro-lock-note";
5
5
  * FLOW is Pro whole: the one scanner with no delayed tier to fall back to.
6
6
  * The options chain needs no account and lists each contract's day volume and
7
7
  * open interest, the raw counts behind a sweep or a block, without the prints.
8
+ * FLOW takes no ticker, so the route names none to check.
8
9
  */
9
- export function flowLockNote(lock: PaneLock): string {
10
+ export function flowLockNote(lock: PaneLock): Promise<string> {
10
11
  return proLockNote(lock, {
11
12
  token: "FLOW",
12
13
  argument: "none",
13
14
  does: "it streams real-time sweeps, blocks and large premium prints across US options exchanges",
14
15
  shows: "it",
15
- free: (ticker, company) => `gloomberb options ${ticker} lists ${company}'s option chain, each contract with its volume and open interest.`,
16
+ free: [{ text: (ticker, company) => `gloomberb options ${ticker} lists ${company}'s option chain, each contract with its volume and open interest.` }],
16
17
  });
17
18
  }
@@ -62,6 +62,7 @@ export const SERIES_INVENTORY: readonly SeriesInventoryEntry[] = [
62
62
  { module: "crypto-board", chart: "Price sparklines for the large coins", series: ["crypto:*"] },
63
63
  { module: "perps", chart: "Perpetual funding, open interest, premium and long/short account share", series: ["perps:*"] },
64
64
  { module: "perps", chart: "Perpetual mark price", notInCatalog: "not-yet", reason: "The pair's price is in the catalog; the perpetual's own mark price could be a crypto series." },
65
+ { module: "primary-dealers", chart: "A primary dealer position's weekly net level, the sum of the release's series behind the row", notInCatalog: "not-yet", reason: "Weekly and dated, one series per row of the board; the pane reads each only when its row is selected, and none is registered for the composer yet." },
65
66
  { module: "short-interest", chart: "A ticker's short interest and days to cover", series: ["short-interest:*"] },
66
67
  { module: "short-volume", chart: "A ticker's daily short volume ratio", series: ["short-volume:*"] },
67
68
  { module: "social-mentions", chart: "A ticker's daily posts", series: ["social-mentions:*"] },
@@ -0,0 +1,105 @@
1
+ import { useCallback, useMemo, useRef, useState } from "react";
2
+ import { signedInBrokerForProfile } from "../../../brokers/signed-in/connect";
3
+ import { isSignedInBrokerProfile } from "../../../brokers/signed-in/profile";
4
+ import { requestBrokerSignIn } from "../../../brokers/signed-in/sign-in-dialog";
5
+ import type { PaneFooterSegment, PaneHint } from "../../../components/layout/pane/footer/model";
6
+ import { t, tf } from "../../../i18n";
7
+ import type { BrokerInstanceConfig } from "../../../types/config";
8
+ import { formatShortDate } from "../../../utils/datetime-format";
9
+ import { usePluginAppActions, usePluginBrokerActions } from "../../runtime";
10
+
11
+ /** A broker profile Gloom cannot reach right now, so what a pane shows of it is from its last sync. */
12
+ export interface OfflineBroker {
13
+ instance: BrokerInstanceConfig;
14
+ brokerName: string;
15
+ /** When the profile last synced its positions, or null when it never has. */
16
+ syncedAt: number | null;
17
+ /** Why it is offline, in the broker's words when it gave some. */
18
+ reason: string;
19
+ /** False when no plugin connects it, so pressing a key could not help. */
20
+ reconnectable: boolean;
21
+ }
22
+
23
+ /** The key onboarding gives "Connect a broker". */
24
+ const RECONNECT_BROKER_KEY = "b";
25
+
26
+ /** `as of 14:02` today, `as of Oct 17 14:02` before. */
27
+ function formatBrokerSyncedAt(syncedAt: number, now = Date.now()): string {
28
+ const date = new Date(syncedAt);
29
+ const time = date.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false });
30
+ const today = date.toDateString() === new Date(now).toDateString();
31
+ return today
32
+ ? tf("as of {time}", { time })
33
+ : tf("as of {date} {time}", { date: formatShortDate(date, { year: false }), time });
34
+ }
35
+
36
+ export interface OfflineBrokerFooter {
37
+ /** `as of …`, when the profile has synced. */
38
+ asOf: PaneFooterSegment[];
39
+ /** Why the broker is offline, or that it is reconnecting. */
40
+ status: PaneFooterSegment[];
41
+ hints: PaneHint[];
42
+ }
43
+
44
+ const ONLINE: OfflineBrokerFooter = { asOf: [], status: [], hints: [] };
45
+
46
+ /**
47
+ * Footer status and key for a pane showing a broker that is offline: when its
48
+ * positions were synced, why it is offline, and `[b] reconnect`. Reconnecting
49
+ * does what Connect does in Brokers (a signed-in broker asks for a new
50
+ * sign-in, any other connects), then syncs the profile so the positions are
51
+ * current again. Nothing changes on screen until that sync lands.
52
+ */
53
+ export function useOfflineBrokerFooter(offline: OfflineBroker | null): OfflineBrokerFooter {
54
+ const { connectBrokerInstance, syncBrokerInstance } = usePluginBrokerActions();
55
+ const { notify } = usePluginAppActions();
56
+ const [reconnecting, setReconnecting] = useState(false);
57
+ const runningRef = useRef(false);
58
+ const offlineRef = useRef(offline);
59
+ offlineRef.current = offline;
60
+
61
+ const reconnect = useCallback(async () => {
62
+ const target = offlineRef.current;
63
+ if (!target?.reconnectable || runningRef.current) return;
64
+ runningRef.current = true;
65
+ setReconnecting(true);
66
+ try {
67
+ if (isSignedInBrokerProfile(target.instance)) {
68
+ if (!await requestBrokerSignIn(signedInBrokerForProfile(target.instance, target.brokerName))) return;
69
+ } else {
70
+ await connectBrokerInstance(target.instance.id);
71
+ }
72
+ await syncBrokerInstance(target.instance.id);
73
+ } catch (error) {
74
+ notify({
75
+ type: "error",
76
+ body: error instanceof Error ? error.message : tf("Could not reconnect {broker}.", { broker: target.brokerName }),
77
+ });
78
+ } finally {
79
+ runningRef.current = false;
80
+ setReconnecting(false);
81
+ }
82
+ }, [connectBrokerInstance, notify, syncBrokerInstance]);
83
+
84
+ return useMemo(() => {
85
+ if (!offline) return ONLINE;
86
+ // Dates the positions, not the quotes, so a shot that dates its quotes keeps it.
87
+ const asOf: PaneFooterSegment[] = offline.syncedAt != null
88
+ ? [{ id: "broker-as-of", asOf: false, parts: [{ text: formatBrokerSyncedAt(offline.syncedAt), tone: "muted" }] }]
89
+ : [];
90
+ const status: PaneFooterSegment[] = [reconnecting
91
+ ? { id: "broker-status", parts: [{ text: t("Reconnecting…"), tone: "muted" }] }
92
+ : { id: "broker-status", parts: [{ text: offline.reason, tone: "warning" }] }];
93
+ const hints: PaneHint[] = offline.reconnectable
94
+ ? [{
95
+ id: "reconnect-broker",
96
+ key: RECONNECT_BROKER_KEY,
97
+ label: " reconnect",
98
+ title: tf("Reconnect {broker}", { broker: offline.brokerName }),
99
+ onPress: () => { void reconnect(); },
100
+ disabled: reconnecting,
101
+ }]
102
+ : [];
103
+ return { asOf, status, hints };
104
+ }, [offline, reconnect, reconnecting]);
105
+ }
@@ -34,13 +34,14 @@ export function refusedLock(error: unknown): Pick<PaneLock, "gate" | "verifyEmai
34
34
  * What a headless read throws when the server refused it behind a wall: the
35
35
  * pane's lock note on one line, so every consumer of the read says what is
36
36
  * locked and how to unlock it, with the refusal as its cause for `fn`, which
37
- * prints the whole note. Any other failure is returned as it is.
37
+ * prints the whole note. Any other failure is returned as it is. The read's
38
+ * context lets the note check the free route it names.
38
39
  */
39
- export function explainRefusal(
40
+ export async function explainRefusal(
40
41
  error: unknown,
41
- note: (lock: PaneLock) => string,
42
- lock: Pick<PaneLock, "symbol" | "token">,
43
- ): unknown {
42
+ note: (lock: PaneLock) => string | Promise<string>,
43
+ lock: Pick<PaneLock, "symbol" | "token" | "context">,
44
+ ): Promise<unknown> {
44
45
  const refused = refusedLock(error);
45
- return refused ? new Error(lockNoteReason(note({ ...refused, ...lock })), { cause: error }) : error;
46
+ return refused ? new Error(lockNoteReason(await note({ ...refused, ...lock })), { cause: error }) : error;
46
47
  }
@@ -3,6 +3,7 @@ import { apiClient } from "../../../api-client";
3
3
  import { recordResearchActivity, researchUpgradeUrl } from "../../../api-client/research-activity";
4
4
  import { getCurrentPluginTarget } from "../../current-target";
5
5
  import type { PaneFooterSegment, PaneHint } from "../../../components";
6
+ import { delayedFooterSegment } from "../../../components/layout/pane/footer/model";
6
7
  import { t, tf } from "../../../i18n";
7
8
  import { useAppLanguage } from "../../../i18n/react";
8
9
  import { useRendererHost } from "../../../ui";
@@ -303,19 +304,17 @@ export function useCloudAccessFooter({
303
304
  };
304
305
  }
305
306
  if (!showUpgrade) return null;
306
- return {
307
+ // The footer's clock, pressed for the upgrade. With a key the pitch is the
308
+ // upgrade hint, so the clock says only what is true of the data; without
309
+ // one its tooltip carries the pitch too.
310
+ return delayedFooterSegment({
307
311
  id: segmentId,
312
+ detail: shortcutScope
313
+ ? tf("{delay} delayed", { delay: delayLabel })
314
+ : tf("{delay} delayed · try Pro live", { delay: delayLabel }),
315
+ delay: delayLabel,
308
316
  onPress: openUpgrade,
309
- parts: [{
310
- // With a key the pitch is the upgrade hint, so the status says only
311
- // what is true of the data. Without one there is nowhere else for it
312
- // to go.
313
- text: shortcutScope
314
- ? tf("{delay} delayed", { delay: delayLabel })
315
- : tf("{delay} delayed · try Pro live", { delay: delayLabel }),
316
- tone: "warning",
317
- }],
318
- };
317
+ });
319
318
  }, [
320
319
  access.trialDaysLeft,
321
320
  delayLabel,
@@ -0,0 +1,24 @@
1
+ import type { HeadlessPaneContext } from "../../../types/headless";
2
+ import { filingFormMatches } from "../sec/forms";
3
+ import { resolveHeadlessInstrument, resolveHeadlessIssuerListing } from "./headless-market-data";
4
+
5
+ /**
6
+ * Whether the SEC lists a filing of the form for the company, as
7
+ * `filings <ticker> --form <form>` would list it: ASML files 20-F and 6-K, so
8
+ * a route to its 10-Ks or 8-Ks answers "No filings". The service filters by
9
+ * form, so one filing answers; the form is checked again here, as a server
10
+ * without the filter sends the newest filing of any form.
11
+ */
12
+ export async function filesForm(symbol: string, form: string, context: HeadlessPaneContext): Promise<boolean> {
13
+ const [{ symbol: ticker }, listing] = await Promise.all([
14
+ resolveHeadlessInstrument(context, symbol),
15
+ resolveHeadlessIssuerListing(context, symbol),
16
+ ]);
17
+ // A listing abroad whose company is unknown cannot be matched to a registrant, and `filings` says so.
18
+ if (listing.exchange && !listing.name) return false;
19
+ const { filings } = await context.apiClient.getCloudSecFilings(
20
+ { ticker, ...listing, forms: [form], limit: 1 },
21
+ { signal: context.signal },
22
+ );
23
+ return filings.some((filing) => filingFormMatches(filing.form, form));
24
+ }
@@ -11,6 +11,17 @@ export const HISTORY_RANGE_VALUES: HeadlessPaneOptionValue[] = [
11
11
  { value: "ALL", aliases: ["max", "maximum"] },
12
12
  ];
13
13
 
14
+ /**
15
+ * The history windows and YTD, which runs from the prior year's last close
16
+ * (the one year-to-date returns are measured from), for a chart that compares
17
+ * returns.
18
+ */
19
+ export const RETURN_RANGE_VALUES: HeadlessPaneOptionValue[] = [
20
+ ...HISTORY_RANGE_VALUES.slice(0, 5),
21
+ { value: "YTD", aliases: ["year-to-date", "year to date"] },
22
+ ...HISTORY_RANGE_VALUES.slice(5),
23
+ ];
24
+
14
25
  export const FINANCIAL_PERIOD_OPTION: HeadlessPaneOptionDef = {
15
26
  key: "period",
16
27
  description: "Financial reporting period.",
@@ -23,8 +23,11 @@ export type ReadGuard = <T>(read: Promise<T>) => Promise<T>;
23
23
  * then upgrade. The session's plan decides before anything loads. Reads go
24
24
  * through `guard`, so a refusal from the server (a plan that ended while the
25
25
  * app was open, an expired session) shows the same wall, not an error.
26
+ *
27
+ * `preview`: the pane opens on every plan, the server serving a preview
28
+ * without Pro (EXEC), so only a refusal puts up the wall.
26
29
  */
27
- export function useProFeatureWall(copy: ProFeatureWallCopy, size: { width: number; height: number }): {
30
+ export function useProFeatureWall(copy: ProFeatureWallCopy, size: { width: number; height: number }, options: { preview?: boolean } = {}): {
28
31
  wall: ReactNode;
29
32
  guard: ReadGuard;
30
33
  } {
@@ -39,12 +42,14 @@ export function useProFeatureWall(copy: ProFeatureWallCopy, size: { width: numbe
39
42
  throw error;
40
43
  }), []);
41
44
 
45
+ // A preview pane is walled by the server's answer alone, never by the plan up front.
46
+ const gated = !options.preview;
42
47
  let wall: ReactNode = null;
43
- if (!access.signedIn || refused === 401) {
48
+ if ((gated && !access.signedIn) || refused === 401) {
44
49
  wall = <SignInWall placement={`${copy.placement.replace(/-wall$/, "")}-signin`} {...size} symbol={symbol} exchange={exchange} action={copy.action} />;
45
- } else if (!access.emailVerified || refused === 403) {
50
+ } else if ((gated && !access.emailVerified) || refused === 403) {
46
51
  wall = <SignInWall placement={`${copy.placement.replace(/-wall$/, "")}-signin`} {...size} symbol={symbol} exchange={exchange} action={copy.action} needsVerification />;
47
- } else if (!access.hasProAccess || refused === 402) {
52
+ } else if ((gated && !access.hasProAccess) || refused === 402) {
48
53
  wall = <ProWall placement={copy.placement} title={copy.title} message={copy.message} {...size} symbol={symbol} exchange={exchange} />;
49
54
  }
50
55
  return { wall, guard };
@@ -1,10 +1,90 @@
1
+ import type { HeadlessPaneContext } from "../../../types/headless";
1
2
  import type { PaneLock } from "../../../types/plugin";
3
+ import { settleWithin, signalWithTimeout } from "../../../utils/async-deadline";
4
+ import { logFailure } from "../../../utils/log-failure";
2
5
 
3
6
  /** A lock note on one line, as JSON and CSV say why a locked report is empty and as an error carries it. */
4
7
  export function lockNoteReason(note: string): string {
5
8
  return note.replace(/\s*\n\s*/g, " ");
6
9
  }
7
10
 
11
+ /**
12
+ * A free command a lock note can name in place of the locked function: what
13
+ * it lists, and whether it has data for the symbol the reader asked about. A
14
+ * note never sends a reader to a command that answers "No data".
15
+ */
16
+ export interface FreeRoute {
17
+ /** The command and what it lists, finishing "Free, with no sign-in: ...". Run it signed out before naming it. */
18
+ text(ticker: string, company: string): string;
19
+ /**
20
+ * Whether the route has data for the symbol, read as the route reads it.
21
+ * Leave it out only for a route that answers for every symbol the function
22
+ * takes. A read that fails, or is still out after a few seconds, counts as
23
+ * no data; pass `context.signal` on to reads that take one.
24
+ */
25
+ answers?(symbol: string, context: HeadlessPaneContext): Promise<boolean>;
26
+ }
27
+
28
+ /** A refused read asks for its note twice (the error it throws, then the report), so an answer is kept a while. */
29
+ const ANSWER_TTL_MS = 5 * 60_000;
30
+
31
+ /**
32
+ * How long a note waits on its routes' data checks, all of them together: a
33
+ * check still out then counts as no data, so a stalled service costs the note
34
+ * its route, never the reader's command.
35
+ */
36
+ const ROUTE_CHECK_WAIT_MS = 3_000;
37
+
38
+ interface KeptAnswer { at: number; answer: Promise<boolean> }
39
+ const answers = new WeakMap<FreeRoute, Map<string, KeptAnswer>>();
40
+
41
+ /**
42
+ * Whether the route has data for the symbol, null when its check failed or was
43
+ * still out at the deadline. A check that answered is kept for the TTL; one
44
+ * that failed or ran out of time is dropped, so the next note checks again
45
+ * rather than hiding the route for minutes over one slow read.
46
+ */
47
+ async function routeAnswers(route: FreeRoute, check: NonNullable<FreeRoute["answers"]>, symbol: string, context: HeadlessPaneContext, deadline: number): Promise<boolean | null> {
48
+ const bySymbol = answers.get(route) ?? new Map<string, KeptAnswer>();
49
+ answers.set(route, bySymbol);
50
+ const key = symbol.trim().toUpperCase();
51
+ let kept = bySymbol.get(key);
52
+ if (!kept || Date.now() - kept.at >= ANSWER_TTL_MS) {
53
+ // The check's own reads stop at the deadline where they take a signal.
54
+ const signal = signalWithTimeout(context.signal, Math.max(0, deadline - Date.now()));
55
+ const entry: KeptAnswer = { at: Date.now(), answer: check(symbol, { ...context, signal }) };
56
+ entry.answer.catch(logFailure("plugins.pro-lock-note", "Check a free route's data failed"));
57
+ bySymbol.set(key, entry);
58
+ kept = entry;
59
+ }
60
+ const answer = await settleWithin(kept.answer, Math.max(0, deadline - Date.now()));
61
+ if (answer === null && bySymbol.get(key) === kept) bySymbol.delete(key);
62
+ return answer;
63
+ }
64
+
65
+ /**
66
+ * The first of the routes, best first, that has data for the lock's symbol:
67
+ * GUIDE's guidance ranges where it covers the company, else its 8-Ks. The
68
+ * checks run together under one deadline, so a stalled first route neither
69
+ * holds the note past it nor keeps the next route from being named. Without a
70
+ * symbol the routes read `<ticker>` and the first is named; without a way to
71
+ * read data, only a route that answers for every symbol is.
72
+ */
73
+ async function answeringFreeRoute(lock: PaneLock, routes: readonly FreeRoute[]): Promise<FreeRoute | null> {
74
+ const { symbol, context } = lock;
75
+ if (!symbol) return routes[0] ?? null;
76
+ // Routes past the first that needs no check are never named, so they are not checked.
77
+ const unchecked = routes.findIndex((route) => !route.answers);
78
+ const candidates = unchecked < 0 ? routes : routes.slice(0, unchecked + 1);
79
+ const deadline = Date.now() + ROUTE_CHECK_WAIT_MS;
80
+ const checks = candidates.map((route) => !route.answers ? Promise.resolve(true)
81
+ : context ? routeAnswers(route, route.answers, symbol, context, deadline) : Promise.resolve(false));
82
+ for (const [index, check] of checks.entries()) {
83
+ if (await check) return candidates[index]!;
84
+ }
85
+ return null;
86
+ }
87
+
8
88
  /**
9
89
  * The `reportLockNote` of a pane its wall covers whole (`useProFeatureWall`, a
10
90
  * scanner's denied state): what the function is and that a wall shows none of
@@ -12,8 +92,8 @@ export function lockNoteReason(note: string): string {
12
92
  * to unlock it. One shape for every lock a reader meets in `fn`, so a refused
13
93
  * read never ends in a bare "Sign in required".
14
94
  */
15
- export function proLockNote(
16
- { gate, symbol, token: typed, verifyEmail }: PaneLock,
95
+ export async function proLockNote(
96
+ lock: PaneLock,
17
97
  pane: {
18
98
  /** The function's shortcut, such as `EK`; a pane that serves several names the one run in the lock. */
19
99
  token: string;
@@ -22,10 +102,10 @@ export function proLockNote(
22
102
  /** What the wall hides, as the object of "none of" and "to read": "it" or "them". */
23
103
  shows: "it" | "them";
24
104
  /**
25
- * The free route and what it lists, finishing "Free, with no sign-in: ...".
26
- * Name one only after running it signed out; leave it out when none answers.
105
+ * The free routes, best first: the note names the first with data for the
106
+ * symbol (`answeringFreeRoute`), or none.
27
107
  */
28
- free?(ticker: string, company: string): string;
108
+ free?: readonly FreeRoute[];
29
109
  /** What the wall asks for: Gloom Cloud Pro (default), or only a free Gloom account. */
30
110
  needs?: "pro" | "sign-in";
31
111
  /**
@@ -35,7 +115,8 @@ export function proLockNote(
35
115
  */
36
116
  argument?: "ticker" | "optional" | "none";
37
117
  },
38
- ): string {
118
+ ): Promise<string> {
119
+ const { gate, symbol, token: typed, verifyEmail } = lock;
39
120
  const token = typed ?? pane.token;
40
121
  const ticker = symbol ?? "<ticker>";
41
122
  // A plan refused at the server is Pro whatever the function usually asks for.
@@ -50,9 +131,10 @@ export function proLockNote(
50
131
  : `To read ${pane.shows} in ${token}, open the app (gloomberb), ${account}${pro ? ", then upgrade (Ctrl+P, UPGRADE)" : ""}`;
51
132
  const bare = pane.argument === "none" || (pane.argument === "optional" && !symbol);
52
133
  const command = bare ? `gloomberb fn ${token}` : `gloomberb fn ${token} ${ticker}`;
134
+ const free = pane.free ? await answeringFreeRoute(lock, pane.free) : null;
53
135
  return [
54
136
  `${token} ${what}: ${pane.does}. ${without} it shows none of ${pane.shows}.`,
55
- ...pane.free ? [`Free, with no sign-in: ${pane.free(ticker, symbol ?? "the company")}`] : [],
137
+ ...free ? [`Free, with no sign-in: ${free.text(ticker, symbol ?? "the company")}`] : [],
56
138
  `${unlock}; ${command} then reads with that session.`,
57
139
  ].join("\n");
58
140
  }
@@ -0,0 +1,62 @@
1
+ import { createContext, useContext, type ReactNode } from "react";
2
+ import { usePaneMenuItems } from "../../../components";
3
+ import type { CompositeChartLevel } from "../../../components/chart/composite/levels";
4
+ import { useShortcut } from "../../../react/input";
5
+ import { useDialogState } from "../../../ui/dialog";
6
+ import type { InstrumentRef } from "../../../market-data/request-types";
7
+ import type { BrokerContractRef } from "../../../types/instrument";
8
+ import { canonicalExchange, parsePublicTickerKey } from "../../../utils/exchanges";
9
+ import { scopedBrokerContractIdentityKey } from "../../../utils/instrument-identity";
10
+
11
+ export interface ResearchTradeProjection {
12
+ levels: readonly CompositeChartLevel[];
13
+ contract?: BrokerContractRef;
14
+ mode?: "simulation" | "live" | "unknown";
15
+ busy?: boolean;
16
+ }
17
+ export interface ResearchTrading extends ResearchTradeProjection {
18
+ symbol: string;
19
+ /** A trading-capable broker profile is enabled, so t leads somewhere. */
20
+ available?: boolean;
21
+ docked: boolean;
22
+ visible: boolean;
23
+ /** The docked ticket holds focus; the chart beside it only sees keys the ticket leaves. */
24
+ ticketFocused?: boolean;
25
+ /** Opens the ticket; without a side it keeps its draft, and Buy or Sell is chosen in it. */
26
+ open(side?: "BUY" | "SELL"): void;
27
+ close(): void;
28
+ /** This ticker's working orders and latest fills, for Overview; absent without a trading profile. */
29
+ orders?(width: number): ReactNode;
30
+ publish(projection: ResearchTradeProjection): void;
31
+ }
32
+ const Context = createContext<ResearchTrading | null>(null);
33
+ export const ResearchTradingProvider = Context.Provider;
34
+ export const useResearchTrading = () => useContext(Context);
35
+
36
+ /** Never put an option premium or another venue's price on the underlying chart. */
37
+ export function researchTradeMatches(trade: ResearchTradeProjection | null, listing: InstrumentRef | null): boolean {
38
+ const contract = trade?.contract;
39
+ if (!contract || !listing) return false;
40
+ if (listing.instrument) return scopedBrokerContractIdentityKey(contract) === scopedBrokerContractIdentityKey(listing.instrument);
41
+ if (contract.secType && !["STK", "ETF"].includes(contract.secType)) return false;
42
+ if (contract.symbol.toUpperCase() !== parsePublicTickerKey(listing.symbol).symbol.toUpperCase()) return false;
43
+ const exchange = canonicalExchange(listing.exchange ?? parsePublicTickerKey(listing.symbol).exchange);
44
+ // SMART is a route, not a listing: a share a broker routes is on any of its venues.
45
+ const venue = [contract.exchange, contract.primaryExchange].map(canonicalExchange).find((name) => name && name !== "SMART");
46
+ return !venue || !exchange || venue === exchange;
47
+ }
48
+
49
+ /** Trade stays in the pane menu and keyboard, not the range toolbar: one key, and the ticket picks the side. */
50
+ export function ResearchTradeActions({ focused }: { focused: boolean }) {
51
+ const trade = useResearchTrading();
52
+ const dialog = useDialogState((state) => state.isOpen);
53
+ useShortcut((event) => {
54
+ if (!trade || trade.ticketFocused || event.defaultPrevented || event.propagationStopped || event.targetEditable || event.ctrl || event.alt || event.meta || event.shift || event.super || event.repeat) return;
55
+ if (event.name !== "t") return;
56
+ event.preventDefault(); event.stopPropagation(); trade.open();
57
+ }, { enabled: focused && !!trade && !dialog, phase: "before", scope: "research:trade-open" });
58
+ usePaneMenuItems("research:trade-open", () => trade ? [
59
+ { id: "trade-open", label: "Trade", accelerator: "t", enabled: !trade.busy, onSelect: () => trade.open() },
60
+ ] : null, [trade]);
61
+ return null;
62
+ }
@@ -18,7 +18,7 @@ export const socialMentionsHeadless: HeadlessPaneDefinition<"bundle"> = {
18
18
  const range = (["1y", "5y", "max"].includes(String(args.options.range)) ? args.options.range : "1y") as SocialMentionsRange;
19
19
  const symbol = String(args.symbols[0]).toUpperCase();
20
20
  const data = await fetchSocialMentions(symbol, range, ctx.apiClient)
21
- .catch((error: unknown) => { throw explainRefusal(error, socialMentionsLockNote, { symbol }); });
21
+ .catch(async (error: unknown) => { throw await explainRefusal(error, socialMentionsLockNote, { symbol, context: ctx }); });
22
22
  return { complete: !data.pending.length, errors: data.warnings,
23
23
  sections: [{ title: "Daily posts on X", columns: [
24
24
  { key: "day", header: "Date" },
@@ -5,7 +5,7 @@ import { proLockNote } from "../shared/pro-lock-note";
5
5
  * BUZZ needs a signed-in account, not a plan. No command reads the posts it
6
6
  * counts without one, so the note names no free route.
7
7
  */
8
- export function socialMentionsLockNote(lock: PaneLock): string {
8
+ export function socialMentionsLockNote(lock: PaneLock): Promise<string> {
9
9
  return proLockNote(lock, {
10
10
  token: "BUZZ",
11
11
  needs: "sign-in",
@@ -4,11 +4,15 @@ import type { HeadlessPaneColumn, HeadlessPaneDefinition, HeadlessPaneFreshness
4
4
  import { formatRelativeAge } from "../../../utils/datetime-format";
5
5
  import { fetchThemeMembers, fetchThemes } from "./client";
6
6
  import {
7
- aggregateText, boardClose, DEFAULT_SORT, matchTheme, memberExtended, memberExtendedSessions, memberPrice, percent, PERIOD_LABELS,
7
+ aggregateText, boardClose, boardYtdBase, DEFAULT_SORT, matchTheme, memberExtended, memberExtendedSessions, memberPrice, percent, periodLabel,
8
8
  SNAPSHOT_STALE_MS, sortMembers, sortThemes,
9
9
  } from "./model";
10
10
 
11
- const periods: HeadlessPaneColumn[] = THEME_PERIODS.map((key) => ({ key, header: PERIOD_LABELS[key], align: "right", format: (value) => percent(value as number | null) }));
11
+ /** The period columns; YTD names the close it is measured from, `YTD vs 31 Dec close`. */
12
+ const periodColumns = (payload: ThemesPayload | ThemeMembersPayload): HeadlessPaneColumn[] => {
13
+ const base = boardYtdBase(payload);
14
+ return THEME_PERIODS.map((key) => ({ key, header: periodLabel(key, base), align: "right", format: (value) => percent(value as number | null) }));
15
+ };
12
16
 
13
17
  /** The pre-market or after-hours print and its move from the regular close in Price, for the members that have one. */
14
18
  function extendedColumn(session: ExtendedSession): HeadlessPaneColumn {
@@ -90,6 +94,7 @@ export const themesHeadless: HeadlessPaneDefinition<"rows"> = {
90
94
  const observation = boardObservation(data);
91
95
  // Price and 1D are the regular session's; an extended print gets a column only when a member has one.
92
96
  const extendedSessions = memberExtendedSessions(data.members);
97
+ const periods = periodColumns(data);
93
98
  return { rows: sortMembers(data.members, DEFAULT_SORT).map((row) => {
94
99
  const extended = memberExtended(row);
95
100
  return { ...row, extendedSession: extended?.session ?? null, extendedPrice: extended?.price ?? null,
@@ -104,6 +109,7 @@ export const themesHeadless: HeadlessPaneDefinition<"rows"> = {
104
109
  metadata: { ...observation.metadata, theme: data.theme } };
105
110
  }
106
111
  const observation = boardObservation(board);
112
+ const periods = periodColumns(board);
107
113
  return { rows: sortThemes(board.themes, DEFAULT_SORT).map((row) => ({ name: row.name, memberCount: row.memberCount,
108
114
  ...Object.fromEntries(THEME_PERIODS.map((period) => [period, row.returns[period].value])), breadth: row.breadth.value, coverage: row.returns,
109
115
  breadthCoverage: row.breadth, best: row.best?.symbol ?? null, worst: row.worst?.symbol ?? null })),