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,13 +1,37 @@
1
1
  import type { PaneLock } from "../../../types/plugin";
2
- import { proLockNote } from "../shared/pro-lock-note";
2
+ import { fetchCompanyData } from "../company-kpis/client";
3
+ import { filesForm } from "../shared/free-routes";
4
+ import { resolveHeadlessIssuerListing } from "../shared/headless-market-data";
5
+ import { proLockNote, type FreeRoute } from "../shared/pro-lock-note";
6
+
7
+ /** GUIDE's free preview, where it holds guidance for the company: Apple issues none, so GUIDE has no AAPL rows. */
8
+ const GUIDE_ROUTE: FreeRoute = {
9
+ text: (ticker, company) => `gloomberb fn GUIDE ${ticker} lists ${company}'s latest guidance ranges, with the date each was issued.`,
10
+ answers: async (symbol, context) => {
11
+ const data = await fetchCompanyData("guidance", symbol, {}, context.apiClient, await resolveHeadlessIssuerListing(context, symbol), context.signal);
12
+ return "guidance" in data && data.guidance.length + data.history.length > 0;
13
+ },
14
+ };
15
+
16
+ /** The company's 8-Ks, where results and any guidance it gives are filed, for a company that files them. */
17
+ const EIGHT_K_ROUTE: FreeRoute = {
18
+ text: (ticker, company) => `gloomberb filings ${ticker} --form 8-K lists ${company}'s 8-Ks, where it files its results and any guidance, each with its items and a link to the filing.`,
19
+ answers: (symbol, context) => filesForm(symbol, "8-K", context),
20
+ };
21
+
22
+ const EARNINGS_ROUTE: FreeRoute = {
23
+ text: (ticker, company) => `gloomberb earnings ${ticker} lists ${company}'s next report date with its consensus EPS estimate.`,
24
+ answers: async (symbol, context) => (await context.marketData.getEarningsCalendar?.([symbol]) ?? []).length > 0,
25
+ };
3
26
 
4
27
  /**
5
28
  * EM, EEO and GUID read one Gloom Cloud route that asks for a signed-in account
6
29
  * with a verified email, and no plan. The report names the function it was run
7
- * for. GUIDE lists a company's latest guidance ranges, as a free preview;
8
- * `earnings` lists the next report date with its consensus EPS estimate.
30
+ * for. GUIDE lists a company's latest guidance ranges, as a free preview, and
31
+ * its 8-Ks hold what GUIDE does not cover; `earnings` lists the next report
32
+ * date with its consensus EPS estimate.
9
33
  */
10
- export function estimateRevisionsLockNote(lock: PaneLock): string {
34
+ export function estimateRevisionsLockNote(lock: PaneLock): Promise<string> {
11
35
  const guidance = lock.token === "GUID";
12
36
  return proLockNote(lock, {
13
37
  token: guidance ? "GUID" : "EM",
@@ -16,8 +40,6 @@ export function estimateRevisionsLockNote(lock: PaneLock): string {
16
40
  ? "it reads the EPS guidance a company issued, cited from its filings and calls, against consensus"
17
41
  : "it reads consensus EPS estimates by fiscal period, how they were revised, the surprises against reported EPS and the guidance",
18
42
  shows: "it",
19
- free: guidance
20
- ? (ticker, company) => `gloomberb fn GUIDE ${ticker} lists ${company}'s latest guidance ranges, with the date each was issued.`
21
- : (ticker, company) => `gloomberb earnings ${ticker} lists ${company}'s next report date with its consensus EPS estimate.`,
43
+ free: guidance ? [GUIDE_ROUTE, EIGHT_K_ROUTE] : [EARNINGS_ROUTE],
22
44
  });
23
45
  }
@@ -13,8 +13,20 @@ import { secListingKey } from "../../../utils/sec";
13
13
  * reads. A proxy is filed once a year and the extraction does not change
14
14
  * after it is verified, so a statement is kept for a long time; the list
15
15
  * of years is refreshed more often, since a new filing adds one.
16
+ *
17
+ * An account without Pro is served the preview (`access: "preview"`): each
18
+ * executive's total pay from the latest proxy. A kept answer is reused only
19
+ * for the plan it was served to, so a new Pro account never sees its old
20
+ * preview and an account that lost Pro never sees a kept breakdown.
16
21
  */
17
22
 
23
+ /** Pro's full statements, or the free preview. */
24
+ export type ProxyTier = "full" | "preview";
25
+
26
+ function tierOf(value: unknown): ProxyTier {
27
+ return (value as { access?: unknown } | null)?.access === "preview" ? "preview" : "full";
28
+ }
29
+
18
30
  const LIST_KIND = "proxies";
19
31
  const STATEMENT_KIND = "proxy";
20
32
  const CACHE_SOURCE = "executives";
@@ -57,6 +69,7 @@ export function resetExecutivesPersistence(): void {
57
69
  function loadCached<T>(
58
70
  kind: string,
59
71
  key: string,
72
+ tier: ProxyTier,
60
73
  active: Map<string, ActiveRequest<T>>,
61
74
  policy: { staleMs: number; expireMs: number },
62
75
  force: boolean,
@@ -65,14 +78,15 @@ function loadCached<T>(
65
78
  const store = persistence;
66
79
  const options = { sourceKey: CACHE_SOURCE, schemaVersion: CACHE_SCHEMA_VERSION };
67
80
  const failedKey = `${kind}:${key}`;
81
+ const activeKey = `${key}|${tier}`;
68
82
  const hit = store?.getResource<T>(kind, key, options);
69
- if (!force && !failedRefreshes.has(failedKey) && hit && !hit.stale) {
83
+ if (!force && !failedRefreshes.has(failedKey) && hit && !hit.stale && tierOf(hit.value) === tier) {
70
84
  return Promise.resolve({ data: hit.value, fetchedAt: hit.fetchedAt });
71
85
  }
72
- const existing = active.get(key);
86
+ const existing = active.get(activeKey);
73
87
  if (!force && existing?.store === store) return existing.promise;
74
88
  const request: ActiveRequest<T> = { store, promise: null! };
75
- const current = () => persistence === store && active.get(key) === request;
89
+ const current = () => persistence === store && active.get(activeKey) === request;
76
90
  request.promise = Promise.resolve().then(fetch).then((data) => {
77
91
  const fetchedAt = Date.now();
78
92
  if (current()) {
@@ -92,14 +106,14 @@ function loadCached<T>(
92
106
  throw error;
93
107
  }
94
108
  const fallback = store?.getResource<T>(kind, key, { ...options, allowExpired: true });
95
- if (!fallback) throw error;
109
+ if (!fallback || tierOf(fallback.value) !== tier) throw error;
96
110
  return {
97
111
  data: fallback.value,
98
112
  fetchedAt: fallback.fetchedAt,
99
113
  refreshError: (error instanceof Error ? error.message : String(error)).trim() || "Request failed",
100
114
  };
101
- }).finally(() => { if (active.get(key) === request) active.delete(key); });
102
- active.set(key, request);
115
+ }).finally(() => { if (active.get(activeKey) === request) active.delete(activeKey); });
116
+ active.set(activeKey, request);
103
117
  return request.promise;
104
118
  }
105
119
 
@@ -107,6 +121,8 @@ interface ProxyLoadOptions {
107
121
  force?: boolean;
108
122
  /** The listing's venue and company, for a listing outside the US. */
109
123
  listing?: IssuerListingParams;
124
+ /** What the account's plan is served: `full` unless it has no Pro. */
125
+ tier?: ProxyTier;
110
126
  }
111
127
 
112
128
  export async function loadProxyStatements(
@@ -116,7 +132,7 @@ export async function loadProxyStatements(
116
132
  const symbol = ticker.toUpperCase();
117
133
  // SAN in Paris is not SAN in New York: a listing abroad is cached under its venue.
118
134
  const key = secListingKey(symbol, options?.listing?.exchange);
119
- return loadCached(LIST_KIND, key, activeListFetches, LIST_CACHE_POLICY, options?.force ?? false,
135
+ return loadCached(LIST_KIND, key, options?.tier ?? "full", activeListFetches, LIST_CACHE_POLICY, options?.force ?? false,
120
136
  () => apiClient.getProxyStatements(symbol, options?.listing));
121
137
  }
122
138
 
@@ -127,6 +143,6 @@ export async function loadProxyStatement(
127
143
  ): Promise<ProxyResult<CloudProxyStatementPayload>> {
128
144
  const symbol = ticker.toUpperCase();
129
145
  const key = `${secListingKey(symbol, options?.listing?.exchange)}:${year}`;
130
- return loadCached(STATEMENT_KIND, key, activeStatementFetches, STATEMENT_CACHE_POLICY, options?.force ?? false,
146
+ return loadCached(STATEMENT_KIND, key, options?.tier ?? "full", activeStatementFetches, STATEMENT_CACHE_POLICY, options?.force ?? false,
131
147
  () => apiClient.getProxyStatement(symbol, year, options?.listing));
132
148
  }
@@ -1,5 +1,4 @@
1
1
  import type { PluginModule } from "../plugin-module";
2
- import { proLockNote } from "../shared/pro-lock-note";
3
2
  import { createTickerSurfacePaneTemplate } from "../shared/ticker-surface";
4
3
  import {
5
4
  attachExecutivesPersistence,
@@ -11,21 +10,6 @@ import { isKnownNonUsListing } from "../../../utils/sec";
11
10
  const description =
12
11
  "Named executive officers and what they were paid, read from the company's proxy statement and checked against the filing.";
13
12
 
14
- /**
15
- * EXEC is Pro whole (`useProFeatureWall`). The proxy statements it reads are
16
- * in the SEC filing list, which needs no account: `filings` lists them with a
17
- * link to each, where the Summary Compensation Table is the pay as filed,
18
- * without EXEC's parsed figures.
19
- */
20
- function executivesLockNote(lock: { gate: "sign-in" | "pro"; symbol: string | null }): string {
21
- return proLockNote(lock, {
22
- token: "EXEC",
23
- does: "it reads each proxy statement into the named executive officers and what they were paid",
24
- shows: "it",
25
- free: (ticker, company) => `gloomberb filings ${ticker} --form DEF14A lists ${company}'s proxy statements, each with its filing date and a link to the filing, whose Summary Compensation Table holds the pay as filed.`,
26
- });
27
- }
28
-
29
13
  export const executivesModule: PluginModule = {
30
14
  setup(ctx) {
31
15
  attachExecutivesPersistence(ctx.persistence);
@@ -47,8 +31,8 @@ export const executivesModule: PluginModule = {
47
31
  {
48
32
  id: EXECUTIVES_PANE_ID,
49
33
  reportFreshness: { source: "SEC EDGAR", status: "not-a-feed", basis: "proxy statements" },
50
- reportLockNote: executivesLockNote,
51
- reportRequirements: ["Gloom Cloud Pro"],
34
+ // Open on every plan: without Pro each executive's total pay from the
35
+ // latest proxy, with the breakdown and earlier years locked (pane.tsx).
52
36
  name: "Executives",
53
37
  icon: "X",
54
38
  component: ExecutivesPane,
@@ -27,11 +27,14 @@ import {
27
27
  } from "../../../ui";
28
28
  import { isPermanentClientError } from "../../../api-client/errors";
29
29
  import type { IssuerListingParams } from "../../../api-client/paths";
30
+ import { usePlanAccess } from "../../../api-client/plan-access";
31
+ import { CLOUD_PLAN_KEY, useCloudUpgradeAction } from "../shared/cloud-upgrade";
30
32
  import { useFilingYearReader } from "../shared/filing-year-reader";
33
+ import { UpgradeLabel } from "../shared/locked-rows";
31
34
  import { useProFeatureWall, type ProFeatureWallCopy, type ReadGuard } from "../shared/pro-feature-wall";
32
35
  import { useBoundTicker, useIssuerListing } from "../shared/ticker-request";
33
36
  import { SplitBar } from "../../../components/ui/split-bar";
34
- import { loadProxyStatement, loadProxyStatements } from "./data";
37
+ import { loadProxyStatement, loadProxyStatements, type ProxyTier } from "./data";
35
38
  import {
36
39
  equityShare,
37
40
  formatChange,
@@ -127,9 +130,12 @@ function renderOfficerCell(row: CloudExecutiveRowPayload, column: OfficerColumn)
127
130
  function ExecutiveRows({
128
131
  rows,
129
132
  width,
133
+ breakdown,
130
134
  }: {
131
135
  rows: CloudExecutiveRowPayload[];
132
136
  width: number;
137
+ /** False on the free preview, whose rows carry the total alone: no EQ% column. */
138
+ breakdown: boolean;
133
139
  }) {
134
140
  const isDesktopWeb = useUiCapabilities().nativePaneChrome === true;
135
141
  if (width < 52) {
@@ -146,7 +152,9 @@ function ExecutiveRows({
146
152
  }
147
153
  // The name column fits the longest name so the title starts after a kit gutter.
148
154
  const nameWidth = Math.min(24, Math.max(10, ...rows.map((row) => row.name.length)));
149
- const columns = OFFICER_COLUMNS.map((column) => column.id === "name" ? { ...column, width: nameWidth } : column);
155
+ const columns = OFFICER_COLUMNS
156
+ .filter((column) => breakdown || column.id !== "equity")
157
+ .map((column) => column.id === "name" ? { ...column, width: nameWidth } : column);
150
158
  const height = rows.length + 1;
151
159
  return (
152
160
  <Box flexDirection="column" height={isDesktopWeb ? undefined : height} flexShrink={0}>
@@ -251,22 +259,26 @@ export function ExecutivesPane({
251
259
  /** Inside Ticker Research, whose own tab strip keeps h/l and the arrows. */
252
260
  nested?: boolean;
253
261
  }) {
254
- const { wall, guard } = useProFeatureWall(EXEC_WALL, { width, height });
262
+ // Every plan reads EXEC: without Pro the server serves each executive's
263
+ // total pay, so only its refusal (an older server, an expired session) walls it.
264
+ const { wall, guard } = useProFeatureWall(EXEC_WALL, { width, height }, { preview: true });
265
+ const tier: ProxyTier = usePlanAccess().hasProAccess ? "full" : "preview";
255
266
  const { symbol, ticker: boundTicker } = useBoundTicker();
256
267
  const ticker = symbol ? symbol.toUpperCase() : null;
257
268
  const listing = useIssuerListing(boundTicker);
258
269
  if (wall) return wall;
259
270
  if (!ticker) return <EmptyState title="Select a ticker." />;
260
- return <ExecutiveResearch key={ticker} ticker={ticker} listing={listing} focused={focused} width={width} nested={nested} guard={guard} />;
271
+ return <ExecutiveResearch key={ticker} ticker={ticker} listing={listing} tier={tier} focused={focused} width={width} nested={nested} guard={guard} />;
261
272
  }
262
273
 
263
274
  export function ExecutivesResearchTab(props: { focused: boolean; width: number; height: number }) {
264
275
  return <ExecutivesPane {...props} nested />;
265
276
  }
266
277
 
267
- function ExecutiveResearch({ ticker, listing, focused, width, nested, guard }: {
278
+ function ExecutiveResearch({ ticker, listing, tier, focused, width, nested, guard }: {
268
279
  ticker: string;
269
280
  listing: IssuerListingParams;
281
+ tier: ProxyTier;
270
282
  focused: boolean;
271
283
  width: number;
272
284
  nested: boolean;
@@ -274,15 +286,20 @@ function ExecutiveResearch({ ticker, listing, focused, width, nested, guard }: {
274
286
  }) {
275
287
  const nativePaneChrome = useUiCapabilities().nativePaneChrome === true;
276
288
  const rendererHost = useRendererHost();
289
+ const openUpgrade = useCloudUpgradeAction("exec");
277
290
  const [selectedYear, setYear] = usePaneStateValue<number | null>("proxyYear", null);
278
- const loadYears = useCallback((force: boolean) => guard(loadProxyStatements(ticker, { force, listing })), [guard, listing, ticker]);
291
+ const loadYears = useCallback((force: boolean) => guard(loadProxyStatements(ticker, { force, listing, tier })), [guard, listing, ticker, tier]);
279
292
  const list = useAsyncResource(loadYears, { clearOnError: isPermanentClientError });
280
293
  const years = useMemo(() => (list.data?.data?.proxies ?? []).map((entry) => entry.proxyYear), [list.data]);
281
- const year = selectedYear !== null && years.includes(selectedYear)
294
+ // Without Pro only the latest proxy is served: an earlier pick from a Pro
295
+ // session waits for Pro again rather than asking for a year that is refused.
296
+ const year = tier === "full" && selectedYear !== null && years.includes(selectedYear)
282
297
  ? selectedYear : years[0] ?? null;
283
- const loadStatement = useCallback((force: boolean) => guard(loadProxyStatement(ticker, year!, { force, listing })), [guard, listing, ticker, year]);
298
+ const loadStatement = useCallback((force: boolean) => guard(loadProxyStatement(ticker, year!, { force, listing, tier })), [guard, listing, ticker, year, tier]);
284
299
  const detail = useAsyncResource(year === null ? null : loadStatement, { clearOnError: isPermanentClientError });
285
300
  const statement = detail.data?.data ?? null;
301
+ // The free preview: each executive's total pay; the breakdown and earlier years are Pro.
302
+ const preview = statement?.access === "preview" || list.data?.data?.access === "preview";
286
303
  const loading = list.loading || detail.loading;
287
304
  const listError = list.error ?? list.data?.refreshError;
288
305
  const statementError = detail.error ?? detail.data?.refreshError;
@@ -332,8 +349,9 @@ function ExecutiveResearch({ ticker, listing, focused, width, nested, guard }: {
332
349
  // `r` refreshes every pane, so it gets no hint here.
333
350
  const hints: PaneHint[] = statement ? [{ id: "open", key: "o", label: "pen filing", onPress: openFiling }] : [];
334
351
  if (yearHint) hints.push(yearHint);
352
+ if (preview) hints.push({ id: "upgrade", key: CLOUD_PLAN_KEY, label: "upgrade", title: "Upgrade to Pro", onPress: openUpgrade });
335
353
  return { info, hints };
336
- }, [loading, statement, openFiling, yearHint]);
354
+ }, [loading, statement, openFiling, yearHint, preview, openUpgrade]);
337
355
 
338
356
  const figures = useMemo(
339
357
  () => (statement ? figuresOf(statement) : []),
@@ -390,7 +408,7 @@ function ExecutiveResearch({ ticker, listing, focused, width, nested, guard }: {
390
408
  <FigureList figures={figures} width={proseWidth} minValueWidth={4} maxValueWidth={14} />
391
409
  </Box>
392
410
  )}
393
- {statement.ceo && (
411
+ {statement.ceo && !preview && (
394
412
  <Box flexDirection="column">
395
413
  <SectionHeading marginTop={figures.length > 0 ? 1 : 0}
396
414
  title={`How ${statement.ceo.name.trim().split(/\s+/).pop() || "the CEO"} Was Paid`}
@@ -404,9 +422,15 @@ function ExecutiveResearch({ ticker, listing, focused, width, nested, guard }: {
404
422
  <ExecutiveRows
405
423
  rows={statement.namedExecutives}
406
424
  width={proseWidth}
425
+ breakdown={!preview}
407
426
  />
408
427
  </Box>
409
428
  )}
429
+ {preview && (
430
+ <Box marginTop={1}>
431
+ <UpgradeLabel text="Upgrade for the pay breakdown and earlier years" onPress={openUpgrade} role="exec-upgrade" />
432
+ </Box>
433
+ )}
410
434
  {statement.highlights && (
411
435
  <Box flexDirection="column">
412
436
  <SectionHeading marginTop={1} title="What Changed" />
@@ -21,7 +21,10 @@ export function validateExposure(value: ExposurePayload): ExposurePayload {
21
21
  && typeof data.evaluatedAt === "string" && Number.isFinite(Date.parse(data.evaluatedAt))
22
22
  && Number.isInteger(data.requestedHoldings) && Number.isInteger(data.lockedHoldings) && data.lockedHoldings >= 0
23
23
  && Array.isArray(data.holdings) && strings(data.unknowns) && strings(data.methodology);
24
- try { parseCustomScenario(JSON.stringify(data.scenario)); } catch { valid = false; }
24
+ try { parseCustomScenario(JSON.stringify(data.scenario)); } catch {
25
+ // Intentional ignore: invalid scenarios use the payload validation error below.
26
+ valid = false;
27
+ }
25
28
  valid &&= data.holdings.every(h => typeof h.symbol === "string" && finite(h.weight) && Array.isArray(h.unknowns)
26
29
  && ["quantified", "partial", "unknown"].includes(h.status) && !!h.coverage
27
30
  && Array.isArray(h.measures) && h.measures.every(m => validRange(m.exposurePct) && nullableRange(m.impactPct) && strings(m.componentIds))
@@ -7,13 +7,13 @@ import type { ExposureScenario } from "../../../api-client/exposure";
7
7
  import { parseCustomScenario } from "./model";
8
8
 
9
9
  export interface ExposureInputs { input: string; source: string; nav: string; cash: string; depth: number; scenario: ExposureScenario; }
10
- export function ExposureEditor({ initial, sources, focused, width, onSave, onCancel }: {
11
- initial: ExposureInputs; sources: { value: string; label: string }[]; focused: boolean; width: number;
10
+ export function ExposureEditor({ initial, initialCustom, sources, focused, width, onSave, onCancel }: {
11
+ initial: ExposureInputs; initialCustom?: string; sources: { value: string; label: string }[]; focused: boolean; width: number;
12
12
  onSave: (value: ExposureInputs) => void; onCancel: () => void;
13
13
  }) {
14
14
  const [draft, setDraft] = useState(initial);
15
15
  const first = initial.scenario.shocks[0]!;
16
- const [custom, setCustom] = useState(initial.scenario.shocks.length > 1 || !!first.transmission ? JSON.stringify(initial.scenario) : "");
16
+ const [custom, setCustom] = useState(initialCustom ?? (initial.scenario.shocks.length > 1 || !!first.transmission ? JSON.stringify(initial.scenario) : ""));
17
17
  const [label, setLabel] = useState(initial.scenario.label);
18
18
  const [kind, setKind] = useState(first.kind);
19
19
  const [target, setTarget] = useState(first.target);
@@ -26,7 +26,10 @@ export const exposureScreenshotEvidence: PaneScreenshotEvidenceHook<ExposureCapt
26
26
  const rows = metadata.tab === "drivers" ? driverRows(payload) : metadata.tab === "paths" ? pathRows(payload) : metadata.tab === "portfolio" ? portfolioRows(payload, String(metadata.view)) : tableRows(payload);
27
27
  if (metadata.plottedValueCount !== rows.length || metadata.rowIds.length !== rows.length || metadata.rowIds.some(id => !rows.some(r => r.id === id))) return null;
28
28
  return metadata as unknown as ExposureCapture;
29
- } catch { return null; }
29
+ } catch {
30
+ // Intentional ignore: malformed capture metadata cannot prove the screenshot's exposure data.
31
+ return null;
32
+ }
30
33
  },
31
34
  mismatches(evidence, { resolved, payload }) {
32
35
  const snapshot = payload.config.layout.instances.find(i => i.instanceId === payload.paneId)?.settings?.exposureSnapshot;
@@ -4,6 +4,7 @@ import type { Portfolio, TickerRecord } from "../../../types/ticker";
4
4
  import { publicTickerKey } from "../../../utils/exchanges";
5
5
  import { resolveCurrencyUnit } from "../../../utils/currency-units";
6
6
  import { convertCurrency } from "../../../utils/format";
7
+ import { logFailure } from "../../../utils/log-failure";
7
8
  import { getPortfolioPositionMetrics, getPortfolioQuoteDisplay, resolvePortfolioMarketValue } from "../../../portfolio/position-metrics";
8
9
 
9
10
  /** Explicit NAV preserves cash, shorts and leverage; net holdings are never used as NAV. */
@@ -35,7 +36,8 @@ export async function portfolioHoldings(portfolio: Portfolio, tickers: TickerRec
35
36
  for (const currency of currencies) await ensureRate(currency);
36
37
  const holdings: ExposureHolding[] = [];
37
38
  for (const ticker of positions) {
38
- const quote = await market.getQuote(ticker.metadata.ticker, ticker.metadata.exchange).catch(() => null);
39
+ const quote = await market.getQuote(ticker.metadata.ticker, ticker.metadata.exchange)
40
+ .catch(logFailure("exposure.holdings", "Load portfolio holding quote failed", null));
39
41
  const currency = quote?.currency || ticker.metadata.currency;
40
42
  if (!rates.has(currency)) await ensureRate(currency);
41
43
  const metrics = getPortfolioPositionMetrics(ticker, portfolio.id, currency, {
@@ -2,12 +2,13 @@ import { useCallback, useMemo, useRef } from "react";
2
2
  import { isAccessDenied } from "../../../api-client/errors";
3
3
  import { usePlanAccess } from "../../../api-client/plan-access";
4
4
  import type { ExposureEvidence, ExposurePayload } from "../../../api-client/exposure";
5
- import { ActionRow, ChartTableHeader, DataTableView, EmptyState, KeyValueRow, PageStackView, PaneStatusBody, QueryBar, SectionHeading, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, type DataTableCell, type DataTableColumn, type PaneHint, type StatItem } from "../../../components";
5
+ import { ActionRow, Button, ChartTableHeader, DataTableView, EmptyState, KeyValueRow, PageStackView, PaneStatusBody, QueryBar, SectionHeading, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, type DataTableCell, type DataTableColumn, type PaneHint, type StatItem } from "../../../components";
6
6
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
7
7
  import { useAppSelector, useAsyncResource, useAutoRefresh, useMarketData, usePaneInstance, usePaneSettingValue, usePluginAppActions, usePluginPaneState } from "../../../public/react";
8
8
  import { useThemeColors } from "../../../theme/theme-context";
9
9
  import type { PaneProps } from "../../../types/plugin";
10
10
  import { Box, ScrollBox, Text, useRendererHost, useUiCapabilities } from "../../../ui";
11
+ import { logFailure } from "../../../utils/log-failure";
11
12
  import { SignInWall } from "../cloud/auth-actions";
12
13
  import { CLOUD_PLAN_KEY, useCloudUpgradeAction } from "../shared/cloud-upgrade";
13
14
  import { useOpenDescription } from "../shared/open-description";
@@ -82,14 +83,32 @@ export function ExposurePane({ width, height, focused }: PaneProps) {
82
83
  const { createPaneFromTemplate } = usePluginAppActions();
83
84
  const openDescription = useOpenDescription();
84
85
  const [openingScenario] = usePaneSettingValue("scenario", "taiwan-disruption");
85
- const [customSetting] = usePaneSettingValue("customScenario", "");
86
+ const [customSetting, setCustomSetting] = usePaneSettingValue("customScenario", "");
86
87
  const [openingDepth] = usePaneSettingValue("depth", 2);
87
88
  const [openingNav] = usePaneSettingValue("nav", "");
88
89
  const [openingCash] = usePaneSettingValue("cash", "");
89
- const [snapshotSetting] = usePaneSettingValue<ExposurePayload | null>("exposureSnapshot", null);
90
- const snapshot = useMemo(() => { try { return snapshotSetting ? validateExposure(snapshotSetting) : null; } catch { return null; } }, [snapshotSetting]);
90
+ const [snapshotSetting, setSnapshotSetting] = usePaneSettingValue<ExposurePayload | null>("exposureSnapshot", null);
91
+ const savedSnapshot = useMemo(() => {
92
+ try { return { data: snapshotSetting === null ? null : validateExposure(snapshotSetting), error: null }; } catch (error) {
93
+ return logFailure("exposure.pane", "Read saved exposure snapshot failed", {
94
+ data: null, error: "Cannot read the saved exposure snapshot. Repair or reset it before analyzing.",
95
+ })(error);
96
+ }
97
+ }, [snapshotSetting]);
98
+ const snapshot = savedSnapshot.data;
91
99
  const initialInput = snapshot ? snapshot.holdings.map(h => `${h.symbol}=${h.weight}`).join(" ") : instance?.params?.input ?? instance?.params?.arg ?? "";
92
- const initialScenario = useMemo(() => { if (snapshot) return snapshot.scenario; try { return customSetting ? parseCustomScenario(customSetting) : DEFAULT_SCENARIO; } catch { return DEFAULT_SCENARIO; } }, [snapshot, customSetting]);
100
+ const savedScenario = useMemo(() => {
101
+ try { return { data: snapshot?.scenario ?? (customSetting ? parseCustomScenario(customSetting) : DEFAULT_SCENARIO), error: null }; } catch (error) {
102
+ return logFailure("exposure.pane", "Read custom exposure scenario failed", {
103
+ data: null, error: "Cannot read the custom scenario. Repair or reset it before analyzing.",
104
+ })(error);
105
+ }
106
+ }, [snapshot, customSetting]);
107
+ const inputError = savedSnapshot.error ?? savedScenario.error;
108
+ // A repaired setting must replace any stale scenario in persisted editor state.
109
+ const repairingCustomSetting = useRef(false);
110
+ if (savedScenario.error) repairingCustomSetting.current = true;
111
+ const initialScenario = savedScenario.data ?? DEFAULT_SCENARIO;
93
112
  const [inputs, setInputs] = usePluginPaneState<ExposureInputs>("exposure:inputs", { input: initialInput, source: /^(PORT|WATCH):/.test(initialInput) ? initialInput : "typed", nav: String(openingNav), cash: String(openingCash), depth: snapshot?.depth ?? Number(openingDepth), scenario: initialScenario });
94
113
  const [scenarioId, setScenarioId] = usePluginPaneState("exposure:scenario", snapshot ? snapshot.scenario.id ?? "captured" : customSetting ? "custom" : openingScenario);
95
114
  const [openingTab] = usePaneSettingValue("tab", "table");
@@ -98,11 +117,11 @@ export function ExposurePane({ width, height, focused }: PaneProps) {
98
117
  const [view, setView] = usePluginPaneState("exposure:view", openingView);
99
118
  const [selectedId, setSelected] = usePluginPaneState<string | null>("exposure:selected", null);
100
119
  const [detailId, setDetail] = usePluginPaneState<string | null>("exposure:detail", null);
101
- const [editing, setEditing] = usePluginPaneState("exposure:editing", !initialInput && !snapshot);
120
+ const [editing, setEditing] = usePluginPaneState("exposure:editing", !initialInput && !snapshot && !inputError);
102
121
  const [sort, setSort] = usePluginPaneState("exposure:sort", { column: "exposure", direction: "desc" as "asc" | "desc" });
103
122
  const libraryLoader = useCallback(() => snapshot ? Promise.resolve([snapshot.scenario]) : fetchScenarios(), [snapshot, accessKey]);
104
- const library = useAsyncResource(libraryLoader, { clearOnError: isAccessDenied });
105
- const scenario = snapshot?.scenario ?? (scenarioId === "custom" ? inputs.scenario : library.data?.find(s => s.id === scenarioId) ?? (scenarioId === DEFAULT_SCENARIO.id ? DEFAULT_SCENARIO : null));
123
+ const library = useAsyncResource(inputError ? null : libraryLoader, { clearOnError: isAccessDenied });
124
+ const scenario = snapshot?.scenario ?? (scenarioId === "custom" ? repairingCustomSetting.current && customSetting ? savedScenario.data : inputs.scenario : library.data?.find(s => s.id === scenarioId) ?? (scenarioId === DEFAULT_SCENARIO.id ? DEFAULT_SCENARIO : null));
106
125
  const portfolio = config.portfolios.find(p => inputs.source === `PORT:${p.id}`);
107
126
  const localTickers = useMemo(() => [...tickers.values()], [tickers]);
108
127
  // Freeze a local selection until the user explicitly refreshes. Tick-by-tick marks must not rerun scenarios.
@@ -126,10 +145,10 @@ export function ExposurePane({ width, height, focused }: PaneProps) {
126
145
  return fetchExposure({ holdings, scenario, depth: inputs.depth, ...(cash === undefined ? {} : { cashWeight: cash }) });
127
146
  // Explicit request state, including the account, owns recalculation. Quote changes do not.
128
147
  }, [sourceKey, snapshot]);
129
- const resource = useAsyncResource(inputs.input || inputs.source !== "typed" || snapshot ? loader : null, { clearOnError: isAccessDenied });
148
+ const resource = useAsyncResource(!inputError && (inputs.input || inputs.source !== "typed" || snapshot) ? loader : null, { clearOnError: isAccessDenied });
130
149
  const data = resource.data;
131
150
  useAutoRefresh(resource.updatedAt, resource.load);
132
- usePaneRefreshKey(() => void resource.reload(), { focused: focused && !editing });
151
+ usePaneRefreshKey(() => void resource.reload(), { focused: focused && !editing && !inputError });
133
152
  const rawRows = useMemo(() => !data ? [] : tab === "drivers" ? driverRows(data) : tab === "paths" ? pathRows(data) : tab === "portfolio" ? portfolioRows(data, view) : tableRows(data), [data, tab, view]);
134
153
  const rows = useMemo(() => [...rawRows].sort((a, b) => {
135
154
  const get = (r: ExposureRow) => sort.column === "value" ? r.driver?.value ?? r.driver?.range?.high ?? -Infinity : sort.column === "units" ? r.driver ? driverUnits(r.driver) : "" : sort.column === "scope" ? r.driver ? driverScope(r.driver) : "" : sort.column === "classification" ? r.classification : sort.column === "label" ? r.label : sort.column === "period" ? r.period ?? "" : sort.column === "exposure" ? r.exposure?.high ?? -Infinity : sort.column === "impact" ? r.impact?.low ?? -Infinity : sort.column === "weight" ? r.weight ?? -Infinity : sort.column === "basis" ? r.basis ?? "" : r.symbol;
@@ -141,12 +160,12 @@ export function ExposurePane({ width, height, focused }: PaneProps) {
141
160
  const activeRow = detail ?? selected;
142
161
  const source = activeRow?.components.flatMap(c => [...c.evidence, ...c.path.flatMap(h => h.evidence)]).find(e => e.url)?.url;
143
162
  const symbol = activeRow?.symbol.split(",")[0]?.trim();
144
- const detailOpen = editing || !!detail || detailId === "unknowns";
163
+ const detailOpen = !savedSnapshot.error && (editing || (!inputError && (!!detail || detailId === "unknowns")));
145
164
  const { strip, rows: tabRows } = usePaneTabs({ tabs: TABS, activeValue: tab, onSelect: value => { setTab(value); setDetail(null); }, focused: focused && !editing, dense: true });
146
165
  useExposureEvidence(data, tab, view, rows.map(r => r.id));
147
- const hints: PaneHint[] = editing ? [] : [
166
+ const hints: PaneHint[] = editing || savedSnapshot.error ? [] : [
148
167
  { id: "edit", key: "s", label: "cenario", title: "Edit scenario and holdings", onPress: () => setEditing(true) },
149
- { id: "unknowns", key: "v", label: "isibility", title: "What we could not see", onPress: () => setDetail("unknowns") },
168
+ ...(!inputError ? [{ id: "unknowns", key: "v", label: "isibility", title: "What we could not see", onPress: () => setDetail("unknowns") }] : []),
150
169
  ...(selected ? [{ id: "evidence", key: "e", label: "vidence", onPress: () => setDetail(selected.id) }] : []),
151
170
  ...(source ? [{ id: "source", key: "o", label: "pen source", onPress: () => void host.openExternal(source) }] : []),
152
171
  ...(symbol ? [
@@ -158,7 +177,7 @@ export function ExposurePane({ width, height, focused }: PaneProps) {
158
177
  ] : []),
159
178
  ...(data?.access === "preview" ? [{ id: "upgrade", key: CLOUD_PLAN_KEY, label: "upgrade", title: "Upgrade to Pro", onPress: upgrade }] : []),
160
179
  ];
161
- usePaneStatusFooter({ registrationId: "exposure", loading: resource.loading, error: data ? resource.error : null, hints,
180
+ usePaneStatusFooter({ registrationId: "exposure", loading: resource.loading, error: inputError ?? (data ? resource.error : null), hints,
162
181
  info: data ? [{ id: "asof", parts: [{ text: `analyzed ${data.evaluatedAt.slice(0, 10)}`, tone: "muted" as const }] }] : [] });
163
182
  usePaneNoticeFooter({ registrationId: "exposure:notices", focused, notices: [...(library.error ? [library.error] : []), ...(data?.unknowns ?? []), ...(data?.holdings.flatMap(h => h.unknowns.map(u => `${h.symbol}: ${u}`)) ?? [])] });
164
183
  const sources = [{ value: "typed", label: "Typed holdings" }, ...config.portfolios.map(p => ({ value: `PORT:${p.id}`, label: `PORT · ${p.name}` })), ...config.watchlists.map(w => ({ value: `WATCH:${w.id}`, label: `Watchlist · ${w.name}` }))];
@@ -231,8 +250,10 @@ export function ExposurePane({ width, height, focused }: PaneProps) {
231
250
  {strip}
232
251
  <PageStackView focused={focused && !editing} detailOpen={detailOpen} onBack={() => { setDetail(null); setEditing(false); }}
233
252
  detailTitle={editing ? "Scenario and holdings" : detail ? `${detail.symbol} · ${detail.label}` : "What we could not see"}
234
- detailContent={editing ? <ExposureEditor initial={{ ...inputs, scenario: scenario ?? inputs.scenario }} sources={sources} focused={focused} width={width} onCancel={() => setEditing(false)} onSave={value => { setInputs(value); setScenarioId("custom"); setEditing(false); setDetail(null); }} /> : data ? <ExposureDetail row={detail} data={data} width={width} /> : null}
235
- rootContent={<PaneStatusBody loading={!data && resource.loading} error={!data ? resource.error : null} subject="exposure analysis">
253
+ detailContent={editing && !savedSnapshot.error ? <ExposureEditor initial={{ ...inputs, scenario: scenario ?? inputs.scenario }} initialCustom={savedScenario.error ? customSetting : undefined} sources={sources} focused={focused} width={width} onCancel={() => setEditing(false)} onSave={value => { if (repairingCustomSetting.current && customSetting) setCustomSetting(JSON.stringify(value.scenario)); setInputs(value); setScenarioId("custom"); setEditing(false); setDetail(null); }} /> : data ? <ExposureDetail row={detail} data={data} width={width} /> : null}
254
+ rootContent={<PaneStatusBody loading={!data && resource.loading} error={inputError ?? (!data ? resource.error : null)} subject="exposure analysis"
255
+ actions={savedSnapshot.error ? <Button label="Reset saved snapshot" onPress={() => { setSnapshotSetting(null); setEditing(false); setDetail(null); }} />
256
+ : savedScenario.error ? <><Button label="Edit scenario" onPress={() => setEditing(true)} /><Button label="Reset custom scenario" onPress={() => { setCustomSetting(""); setInputs({ ...inputs, scenario: DEFAULT_SCENARIO }); setScenarioId(DEFAULT_SCENARIO.id!); setEditing(false); setDetail(null); }} /></> : undefined}>
236
257
  {data ? <DataTableView<ExposureRow> columns={columns} items={items} getItemKey={r => r.id} rootWidth={width} rootHeight={bodyHeight} focused={focused && !detailOpen}
237
258
  selection={{ kind: "id", selectedId: selected?.id ?? null, getId: r => r.id, onChange: (id) => { if (!id.startsWith("locked:")) setSelected(id); } }}
238
259
  onActivate={row => row.id.startsWith("locked:") ? upgrade() : setDetail(row.id)} selectedTextOverridesCellColor showHorizontalScrollbar
@@ -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 { FILING_EVENTS_PANE_ID, FilingEventsPane } from "./pane";
@@ -11,14 +13,18 @@ const description =
11
13
  * EK is Pro whole (`useProFeatureWall`): signed out it shows the sign-in wall
12
14
  * and on a free account the Pro wall, never a partial feed. The 8-Ks themselves
13
15
  * are in SEC, which needs no account: its rows carry each 8-K's filing date,
14
- * acceptance time and items, by number and in words.
16
+ * acceptance time and items, by number and in words. A company filing no 8-Ks
17
+ * (ASML files 6-Ks) gets no route.
15
18
  */
16
- function filingEventsLockNote(lock: { gate: "sign-in" | "pro"; symbol: string | null }): string {
19
+ function filingEventsLockNote(lock: PaneLock): Promise<string> {
17
20
  return proLockNote(lock, {
18
21
  token: "EK",
19
22
  does: "it reads each 8-K and sums up what it said and who it names",
20
23
  shows: "them",
21
- free: (ticker, company) => `gloomberb fn SEC ${ticker} lists ${company}'s recent filings, 8-Ks among them, each with its filing date and its items by number and name.`,
24
+ free: [{
25
+ text: (ticker, company) => `gloomberb fn SEC ${ticker} lists ${company}'s recent filings, 8-Ks among them, each with its filing date and its items by number and name.`,
26
+ answers: (symbol, context) => filesForm(symbol, "8-K", context),
27
+ }],
22
28
  });
23
29
  }
24
30
 
@@ -3,6 +3,7 @@ import { colors, priceColor } from "../../../theme/colors";
3
3
  import type { Quote } from "../../../types/financials";
4
4
  import { TextAttributes } from "../../../ui";
5
5
  import { formatCompact, formatNumber, formatPercentRaw } from "../../../utils/format";
6
+ import { minorUnitSign } from "../../../utils/currency-units";
6
7
  import { isBoardRowLoading, renderQuoteBoardCell, type BoardQuoteMap } from "../shared/use-quote-board";
7
8
  import { tickDecimals, type FuturesContract } from "../../../market-data/futures-board";
8
9
  import { FUTURES_RETURN_COLUMNS, futuresContractHeadline, futuresUnitLabel, type FuturesColumnId, type FuturesTableRow } from "./model";
@@ -164,13 +165,15 @@ const currencySymbols = new Map<string, string>();
164
165
  /**
165
166
  * The board prints dollar prices bare. Anything else carries its unit on the
166
167
  * price and on the change, so Dutch TTF gas at 72.071 euros is not read as
167
- * dollars beside Henry Hub: US cents (`USX`) as a `c` suffix, other
168
- * currencies as their symbol.
168
+ * dollars beside Henry Hub: a minor unit as its sign after the amount (US
169
+ * cents 312.35¢, as every price in cents reads), other currencies as their
170
+ * symbol.
169
171
  */
170
172
  function currencyMarks(currency: string | undefined): { prefix: string; suffix: string } {
171
173
  const code = currency?.trim().toUpperCase() ?? "";
172
174
  if (!code || code === "USD") return { prefix: "", suffix: "" };
173
- if (code === "USX") return { prefix: "", suffix: "c" };
175
+ const minor = minorUnitSign(currency);
176
+ if (minor) return { prefix: "", suffix: minor };
174
177
  let symbol = currencySymbols.get(code);
175
178
  if (symbol === undefined) {
176
179
  try {
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
- import { chartTableChromeRows, ChartTableHeader, CurveSurface, curveGhostColors, DataTableView, Notice, PaneStatusBody, QueryBar, useChartTableSelection, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, type DataTableColumn, type PaneFooterSegment, type StatItem } from "../../../components";
2
+ import { splitDelayedStatus, chartTableChromeRows, ChartTableHeader, CurveSurface, curveGhostColors, DataTableView, Notice, PaneStatusBody, QueryBar, useChartTableSelection, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, type DataTableColumn, type PaneFooterSegment, type StatItem } from "../../../components";
3
3
  import { fitChartTableColumns } from "../../../components/chart-table";
4
4
  import { curveStrip, curveSurfaceMinRows } from "../../../components/chart/curve";
5
5
  import { isAccessDenied } from "../../../api-client/errors";
@@ -225,7 +225,7 @@ function FuturesCurveView({ width, height, focused, root, ownsTitle }: PaneProps
225
225
  ...requestedDate ? [{ id: "latest", key: "c", label: "urrent", title: "Current Curve", onPress: () => selectDate("") }] : [],
226
226
  ],
227
227
  info: data ? [
228
- { id: "source", parts: [{ text: `${requestedDate ? "daily archive" : data.source === "cboe" ? "settlement" : delay > 0 ? `${delay}m delayed` : "dated quotes"} · ${unit ?? "units unavailable"} · ${curveTimestamp(newest)}${newest?.includes("T") ? " UTC" : ""}`, tone: "muted" }] },
228
+ ...splitDelayedStatus({ id: "source", parts: [{ text: `${requestedDate ? "daily archive" : data.source === "cboe" ? "settlement" : delay > 0 ? `${delay}m delayed` : "dated quotes"} · ${unit ?? "units unavailable"} · ${curveTimestamp(newest)}${newest?.includes("T") ? " UTC" : ""}`, tone: "muted" }] }),
229
229
  ...(staleCount ? [{ id: "stale", parts: [{ text: `${staleCount} of ${data.contracts.length} stale`, tone: "warning" as const }] }] : []),
230
230
  ...(spot ? [spot.status === "ok" ? spotSegment(spot, root, clock)
231
231
  : { id: "spot", parts: [{ text: `Basis blank: ${spot.reason}`, tone: "warning" as const }] }] : []),