gloomberb 0.18.1 → 0.18.3

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 (416) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +106 -5
  3. package/src/api-client/commodity-balances.ts +6 -0
  4. package/src/api-client/company-kpis.ts +9 -0
  5. package/src/api-client/crypto-markets.ts +9 -0
  6. package/src/api-client/data.ts +21 -5
  7. package/src/api-client/equity-screener.ts +2 -0
  8. package/src/api-client/index.ts +2 -0
  9. package/src/api-client/request.ts +5 -3
  10. package/src/api-client/telemetry.ts +17 -0
  11. package/src/api-client/types.ts +5 -0
  12. package/src/app/global-shortcuts.ts +1 -16
  13. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  14. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  15. package/src/app/runtime/plugin-bindings.ts +6 -2
  16. package/src/app/runtime/update.ts +28 -8
  17. package/src/architecture/pane-tabs-allowlist.json +65 -0
  18. package/src/brokers/logos.ts +66 -0
  19. package/src/brokers/profile-form.ts +3 -0
  20. package/src/brokers/remote-broker-adapter.ts +13 -3
  21. package/src/capabilities/index.ts +1 -1
  22. package/src/capabilities/service-capabilities.ts +7 -1
  23. package/src/cli/base-converter.ts +60 -18
  24. package/src/cli/commands/market.ts +354 -187
  25. package/src/cli/commands/news-search.ts +35 -8
  26. package/src/cli/commands/system.ts +35 -1
  27. package/src/cli/commands/ticker.ts +167 -60
  28. package/src/cli/currency-hints.ts +27 -0
  29. package/src/cli/fx-pair.ts +41 -4
  30. package/src/cli/glossary.ts +57 -4
  31. package/src/cli/help.ts +1 -1
  32. package/src/cli/helpers.ts +36 -3
  33. package/src/cli/index.ts +12 -1
  34. package/src/cli/issuer-membership.ts +103 -0
  35. package/src/cli/known-gaps.ts +37 -1
  36. package/src/cli/listing-arg.ts +51 -3
  37. package/src/cli/name-entries.ts +51 -12
  38. package/src/cli/pane-functions/capabilities.ts +2 -0
  39. package/src/cli/pane-functions/catalog.ts +19 -1
  40. package/src/cli/pane-functions/discovery.ts +15 -1
  41. package/src/cli/pane-functions/dom.ts +11 -5
  42. package/src/cli/pane-functions/freshness.ts +4 -4
  43. package/src/cli/pane-functions/function-help.ts +27 -3
  44. package/src/cli/pane-functions/headless.ts +31 -11
  45. package/src/cli/pane-functions/index.ts +67 -7
  46. package/src/cli/pane-functions/lock-note.ts +7 -4
  47. package/src/cli/pane-functions/resolver.ts +21 -6
  48. package/src/cli/quote-answer.ts +179 -0
  49. package/src/cli/report-tables.ts +136 -18
  50. package/src/cli/result.ts +12 -4
  51. package/src/cli/series-search.ts +2 -1
  52. package/src/cli/symbol-list.ts +47 -11
  53. package/src/cli/types.ts +4 -0
  54. package/src/components/chart/composite/composite-chart.tsx +25 -7
  55. package/src/components/chart/composite/drawing-store.ts +26 -6
  56. package/src/components/chart/composite/format.ts +13 -1
  57. package/src/components/chart/composite/interactions.ts +2 -1
  58. package/src/components/chart/composite/level-labels.ts +30 -0
  59. package/src/components/chart/composite/levels.ts +17 -6
  60. package/src/components/chart/composite/panel-surface.tsx +69 -8
  61. package/src/components/chart/composite/types.ts +6 -0
  62. package/src/components/chart/composite/use-composite-chart-keyboard.ts +1 -1
  63. package/src/components/command-bar/routes/root/command-items.ts +32 -6
  64. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  65. package/src/components/data-table/view.tsx +23 -3
  66. package/src/components/index.ts +3 -0
  67. package/src/components/layout/detached-pane-shell.tsx +1 -0
  68. package/src/components/layout/floating-pane.tsx +1 -0
  69. package/src/components/layout/header.tsx +228 -55
  70. package/src/components/layout/pane/content.tsx +16 -14
  71. package/src/components/layout/pane/footer/index.tsx +22 -3
  72. package/src/components/layout/pane/footer/model.ts +99 -4
  73. package/src/components/layout/pane/header-tabs.tsx +15 -1
  74. package/src/components/layout/pane/header.tsx +10 -3
  75. package/src/components/layout/pane/index.tsx +1 -0
  76. package/src/components/layout/pane/pane-tabs.tsx +9 -1
  77. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  78. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  79. package/src/components/onboarding/wizard-model.ts +10 -3
  80. package/src/components/ui/button.tsx +11 -4
  81. package/src/components/ui/choice-dialog.tsx +8 -5
  82. package/src/components/ui/display.tsx +11 -6
  83. package/src/components/ui/fields.tsx +100 -20
  84. package/src/components/ui/icon.tsx +6 -1
  85. package/src/components/ui/list-row-lines.tsx +87 -0
  86. package/src/components/ui/list-view.tsx +14 -0
  87. package/src/components/ui/logo-mark.tsx +64 -0
  88. package/src/components/ui/number-step.ts +54 -0
  89. package/src/components/ui/select-button.tsx +5 -4
  90. package/src/components/ui/select-field.tsx +3 -3
  91. package/src/components/ui/stat-grid.tsx +28 -3
  92. package/src/components/ui/status.tsx +20 -4
  93. package/src/components/ui/tabs.tsx +9 -1
  94. package/src/components/ui/toggle.tsx +17 -3
  95. package/src/core/app-services.ts +4 -0
  96. package/src/core/state/app/instrument.ts +8 -3
  97. package/src/data/config/store/normalize.ts +4 -1
  98. package/src/data/plugin-cache.ts +32 -6
  99. package/src/docs/snapshot.json +5 -5
  100. package/src/i18n/es.ts +31 -3
  101. package/src/i18n/ja.ts +31 -3
  102. package/src/i18n/ko.ts +31 -3
  103. package/src/i18n/zh-cn.ts +32 -4
  104. package/src/i18n/zh-tw.ts +32 -4
  105. package/src/market-data/commodity-names.ts +80 -0
  106. package/src/market-data/coordinator/auxiliary.ts +3 -1
  107. package/src/market-data/coordinator/chart.ts +3 -1
  108. package/src/market-data/futures-board.ts +38 -1
  109. package/src/market-data/futures-contract.ts +91 -0
  110. package/src/market-data/fx-closes.ts +78 -1
  111. package/src/market-data/market/change-basis.ts +219 -0
  112. package/src/market-data/market/format.ts +28 -12
  113. package/src/market-data/market/freshness.ts +38 -0
  114. package/src/market-data/market/status.ts +55 -9
  115. package/src/market-data/market/year-to-date.ts +46 -0
  116. package/src/market-data/quotes/contributions.ts +12 -0
  117. package/src/market-data/quotes/delisting.ts +34 -0
  118. package/src/market-data/quotes/live-valuation.ts +23 -0
  119. package/src/market-data/quotes/resolution.ts +8 -2
  120. package/src/market-data/tax-equivalent.ts +10 -2
  121. package/src/notifications/app-notifier.ts +2 -2
  122. package/src/notifications/notification-log.ts +8 -6
  123. package/src/plugins/builtin/alerts/alert-engine.ts +3 -1
  124. package/src/plugins/builtin/alerts/cli.ts +26 -18
  125. package/src/plugins/builtin/alerts/events.ts +1 -0
  126. package/src/plugins/builtin/alerts/headless.ts +1 -1
  127. package/src/plugins/builtin/alerts/index.tsx +13 -4
  128. package/src/plugins/builtin/alerts/research-rules.ts +1 -0
  129. package/src/plugins/builtin/analytics/risk-pane.tsx +10 -7
  130. package/src/plugins/builtin/attention/pane.tsx +16 -14
  131. package/src/plugins/builtin/attention/trending.tsx +8 -6
  132. package/src/plugins/builtin/awards/pane.tsx +6 -6
  133. package/src/plugins/builtin/backtest/client.ts +5 -2
  134. package/src/plugins/builtin/backtest/export.ts +1 -1
  135. package/src/plugins/builtin/backtest/window.ts +16 -3
  136. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  137. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  138. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  139. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  140. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  141. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  142. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  143. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  144. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  145. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  146. package/src/plugins/builtin/broker-trading/dock.tsx +184 -0
  147. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  148. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  149. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  150. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  151. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  152. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  153. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  154. package/src/plugins/builtin/broker-trading/pane.tsx +507 -0
  155. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  156. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  157. package/src/plugins/builtin/broker-trading/ticket.tsx +578 -0
  158. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  159. package/src/plugins/builtin/buildout/format.ts +1 -0
  160. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  161. package/src/plugins/builtin/buildout/pane/index.tsx +11 -7
  162. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  163. package/src/plugins/builtin/catalysts/client.ts +2 -2
  164. package/src/plugins/builtin/catalysts/pane.tsx +3 -3
  165. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  166. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  167. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  168. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  169. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  170. package/src/plugins/builtin/chart-composer/pane.tsx +32 -11
  171. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  172. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  173. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  174. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  175. package/src/plugins/builtin/chat/index.tsx +1 -1
  176. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  177. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  178. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  179. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  180. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  181. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  182. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  183. package/src/plugins/builtin/commodity-balances/pane.tsx +39 -9
  184. package/src/plugins/builtin/company-attention/app-rank.tsx +4 -5
  185. package/src/plugins/builtin/company-attention/pane.tsx +13 -11
  186. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  187. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  188. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  189. package/src/plugins/builtin/company-kpis/pane.tsx +30 -15
  190. package/src/plugins/builtin/composite-plugins.ts +3 -2
  191. package/src/plugins/builtin/correlation/headless.ts +2 -1
  192. package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
  193. package/src/plugins/builtin/correlation/settings.ts +1 -0
  194. package/src/plugins/builtin/cot/headless.ts +1 -1
  195. package/src/plugins/builtin/cot/index.tsx +1 -1
  196. package/src/plugins/builtin/cot/model.ts +35 -12
  197. package/src/plugins/builtin/cot/pane.tsx +4 -3
  198. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  199. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  200. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  201. package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
  202. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  203. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  204. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  205. package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
  206. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  207. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  208. package/src/plugins/builtin/doe/pane.tsx +1 -1
  209. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  210. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  211. package/src/plugins/builtin/econ/calendar-model.ts +35 -0
  212. package/src/plugins/builtin/econ/index.tsx +31 -4
  213. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  214. package/src/plugins/builtin/equity-screener/headless.ts +30 -5
  215. package/src/plugins/builtin/equity-screener/model.ts +68 -2
  216. package/src/plugins/builtin/equity-screener/pane.tsx +129 -119
  217. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  218. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  219. package/src/plugins/builtin/exposure/client.ts +4 -1
  220. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  221. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  222. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  223. package/src/plugins/builtin/exposure/pane.tsx +43 -21
  224. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  225. package/src/plugins/builtin/futures/table.tsx +6 -3
  226. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  227. package/src/plugins/builtin/gpu/board.tsx +4 -1
  228. package/src/plugins/builtin/gpu/changes.tsx +10 -10
  229. package/src/plugins/builtin/gpu/equities.tsx +4 -1
  230. package/src/plugins/builtin/gpu/history.tsx +4 -2
  231. package/src/plugins/builtin/gpu/pane.tsx +5 -3
  232. package/src/plugins/builtin/holders/client.ts +2 -1
  233. package/src/plugins/builtin/holders/headless.ts +2 -1
  234. package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
  235. package/src/plugins/builtin/insider/base-values.ts +76 -0
  236. package/src/plugins/builtin/insider/client.ts +3 -0
  237. package/src/plugins/builtin/insider/headless.ts +48 -10
  238. package/src/plugins/builtin/insider/index.tsx +17 -8
  239. package/src/plugins/builtin/insider/issuer.ts +34 -4
  240. package/src/plugins/builtin/insider/signals-client.ts +2 -2
  241. package/src/plugins/builtin/insider/table-model.ts +35 -13
  242. package/src/plugins/builtin/insider/table.tsx +5 -2
  243. package/src/plugins/builtin/iv-history/events.ts +145 -0
  244. package/src/plugins/builtin/iv-history/format.ts +3 -0
  245. package/src/plugins/builtin/iv-history/headless.ts +91 -7
  246. package/src/plugins/builtin/jobs/headless.ts +2 -2
  247. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  248. package/src/plugins/builtin/jodi/pane.tsx +4 -4
  249. package/src/plugins/builtin/market-halts/model.ts +31 -6
  250. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  251. package/src/plugins/builtin/market-movers/client.ts +4 -3
  252. package/src/plugins/builtin/market-movers/screener.ts +4 -3
  253. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  254. package/src/plugins/builtin/members/pane.tsx +8 -3
  255. package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
  256. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  257. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  258. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  259. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  260. package/src/plugins/builtin/notes/cli.ts +1 -1
  261. package/src/plugins/builtin/notes/export.ts +17 -6
  262. package/src/plugins/builtin/notes/files.ts +16 -5
  263. package/src/plugins/builtin/options/enrichment-model.ts +10 -9
  264. package/src/plugins/builtin/options/footer.ts +25 -20
  265. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  266. package/src/plugins/builtin/options/model-spot.ts +70 -4
  267. package/src/plugins/builtin/options/view.tsx +118 -35
  268. package/src/plugins/builtin/options-calculator/pane.tsx +7 -4
  269. package/src/plugins/builtin/options-positioning/headless.ts +11 -3
  270. package/src/plugins/builtin/options-positioning/model.ts +24 -0
  271. package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
  272. package/src/plugins/builtin/options-scenario/client.ts +19 -1
  273. package/src/plugins/builtin/options-scenario/headless.ts +13 -6
  274. package/src/plugins/builtin/options-scenario/pane.tsx +54 -11
  275. package/src/plugins/builtin/plugin-marketplace/pane.tsx +5 -4
  276. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  277. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
  278. package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
  279. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  280. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  281. package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
  282. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  283. package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
  284. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  285. package/src/plugins/builtin/power/client.ts +2 -2
  286. package/src/plugins/builtin/power/pane.tsx +36 -31
  287. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  288. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  289. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  290. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  291. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  292. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  293. package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
  294. package/src/plugins/builtin/reverse-dcf/model.ts +4 -3
  295. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  296. package/src/plugins/builtin/scanner/feed.ts +6 -1
  297. package/src/plugins/builtin/scanner/headless.ts +1 -1
  298. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  299. package/src/plugins/builtin/seasonality/client.ts +2 -1
  300. package/src/plugins/builtin/sectors/client.ts +4 -3
  301. package/src/plugins/builtin/series-inventory.ts +1 -0
  302. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  303. package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
  304. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  305. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  306. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  307. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  308. package/src/plugins/builtin/shared/research-trading.tsx +64 -0
  309. package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
  310. package/src/plugins/builtin/shared/volatility/options.ts +2 -1
  311. package/src/plugins/builtin/social-mentions/client.ts +2 -1
  312. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  313. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  314. package/src/plugins/builtin/themes/headless.ts +8 -2
  315. package/src/plugins/builtin/themes/model.ts +43 -10
  316. package/src/plugins/builtin/themes/pane.tsx +8 -6
  317. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  318. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  319. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  320. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  321. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  322. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  323. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  324. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  325. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  326. package/src/plugins/builtin/vol-surface/client.ts +23 -3
  327. package/src/plugins/builtin/vol-surface/headless.ts +33 -14
  328. package/src/plugins/builtin/vol-surface/model.ts +30 -10
  329. package/src/plugins/builtin/vol-surface/pane.tsx +35 -15
  330. package/src/plugins/builtin/vol-surface/stored.ts +34 -3
  331. package/src/plugins/builtin/vol-surface/tables.ts +14 -0
  332. package/src/plugins/builtin-editorial.ts +1 -1
  333. package/src/plugins/compat.ts +3 -0
  334. package/src/plugins/external-runtime.ts +5 -0
  335. package/src/plugins/loader.ts +6 -0
  336. package/src/plugins/plugin-icon.ts +61 -0
  337. package/src/plugins/registry/host-actions.ts +2 -1
  338. package/src/plugins/registry/index.ts +9 -1
  339. package/src/plugins/runtime/context.tsx +5 -0
  340. package/src/plugins/runtime/index.tsx +3 -0
  341. package/src/portfolio/column-values.ts +13 -4
  342. package/src/portfolio/cost-fx.ts +5 -28
  343. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  344. package/src/public/broker.ts +8 -0
  345. package/src/public/market-data.ts +5 -0
  346. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  347. package/src/renderers/dom/desktop/controls.tsx +40 -8
  348. package/src/renderers/dom/desktop/icons.tsx +14 -0
  349. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  350. package/src/renderers/dom/desktop/menu.tsx +10 -1
  351. package/src/renderers/dom/desktop/query-bar.tsx +2 -0
  352. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  353. package/src/renderers/dom/host/style.ts +6 -1
  354. package/src/renderers/dom/host/tabs.tsx +22 -5
  355. package/src/renderers/dom/host/top-surface.ts +127 -0
  356. package/src/renderers/dom/styles.css +51 -5
  357. package/src/renderers/electrobun/bun/core-capabilities.ts +21 -1
  358. package/src/renderers/electrobun/bun/desktop/backend.ts +10 -0
  359. package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
  360. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  361. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  362. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  363. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  364. package/src/renderers/electrobun/view/news-source.ts +4 -2
  365. package/src/renderers/electrobun/view/remote/asset-data-client.ts +8 -1
  366. package/src/renderers/opentui/start.tsx +7 -0
  367. package/src/series/validate.ts +2 -2
  368. package/src/sources/gloomberb-cloud/index.ts +57 -17
  369. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  370. package/src/sources/history-result.ts +3 -0
  371. package/src/sources/provider-errors.ts +3 -1
  372. package/src/sources/provider-router/history.ts +49 -6
  373. package/src/sources/provider-router/index.ts +18 -1
  374. package/src/state/app/activity.ts +13 -0
  375. package/src/sync/controller.ts +4 -0
  376. package/src/sync/core-contributors.ts +62 -11
  377. package/src/sync/local-only.ts +121 -0
  378. package/src/sync/react.ts +6 -0
  379. package/src/sync/types.ts +3 -0
  380. package/src/telemetry/crash-reports.ts +4 -0
  381. package/src/telemetry/update-failures-node.ts +59 -0
  382. package/src/telemetry/update-failures.ts +307 -0
  383. package/src/tickers/company-name-listing.ts +9 -7
  384. package/src/tickers/company-name.ts +43 -0
  385. package/src/tickers/listing-fate.ts +57 -8
  386. package/src/tickers/search/index.ts +24 -3
  387. package/src/time-series/price-comparison.ts +35 -0
  388. package/src/time-series/resolve.ts +52 -11
  389. package/src/time-series/spec.ts +1 -1
  390. package/src/time-series/types.ts +5 -0
  391. package/src/types/broker.ts +56 -1
  392. package/src/types/config.ts +6 -0
  393. package/src/types/data-provider.ts +11 -1
  394. package/src/types/financials.ts +23 -0
  395. package/src/types/headless.ts +19 -2
  396. package/src/types/instrument.ts +4 -0
  397. package/src/types/issuer-listings.ts +28 -0
  398. package/src/types/plugin.ts +24 -6
  399. package/src/types/price-history.ts +6 -1
  400. package/src/types/trading.ts +67 -0
  401. package/src/ui/host.tsx +4 -0
  402. package/src/updater/failure.ts +163 -0
  403. package/src/updater/index.ts +87 -23
  404. package/src/utils/base-currency.ts +108 -0
  405. package/src/utils/currency-names.ts +128 -0
  406. package/src/utils/currency-pair.ts +28 -1
  407. package/src/utils/currency-units.ts +32 -1
  408. package/src/utils/dom-screenshot.ts +22 -9
  409. package/src/utils/exchange-notation.generated.ts +360 -0
  410. package/src/utils/exchanges.ts +15 -322
  411. package/src/utils/format.ts +31 -1
  412. package/src/utils/instrument-identity.ts +38 -1
  413. package/src/utils/options.ts +46 -1
  414. package/src/utils/price-history.ts +4 -0
  415. package/src/utils/reported-money.ts +8 -4
  416. package/src/utils/sec.ts +68 -0
@@ -0,0 +1,179 @@
1
+ import type { QuoteBatchResult } from "../types/data-provider";
2
+ import type { Quote } from "../types/financials";
3
+ import { bareListingNote } from "../tickers/listing-fate";
4
+ import { notATickerSymbol } from "../sources/provider-errors";
5
+ import { looksLikeCurrencyPair } from "../utils/currency-pair";
6
+ import { canonicalExchange } from "../utils/exchanges";
7
+ import type { CliCommandDef } from "../types/plugin";
8
+ import { currencyCodeNote } from "./currency-hints";
9
+ import { describeSuggestion, suggestListing, suggestUsTicker, type ListingSuggestion } from "./listing-suggestion";
10
+ import { indexRootAlsoLine, indexRootFor, indexRootTryLine } from "./index-roots";
11
+ import { treasuryYieldLine, treasuryYieldTenor } from "./treasury-yields";
12
+ import { knownGapForTicker } from "./known-gaps";
13
+ import {
14
+ futuresTryLine,
15
+ isNoProviderError,
16
+ listingChoiceNotes,
17
+ listingReadNotes,
18
+ loadBareVenues,
19
+ notTradedMessage,
20
+ quoteBareListing,
21
+ requireCliListing,
22
+ tradingBoardNote,
23
+ type CliListing,
24
+ type ListingDataRequest,
25
+ type ListingVenue,
26
+ } from "./listing-arg";
27
+ import { withListingSuggestion } from "./not-a-ticker";
28
+ import type { MarketContext } from "./types";
29
+
30
+ /** The text of an error, or null for none. */
31
+ export function errorMessage(error: unknown): string | null {
32
+ if (error == null) return null;
33
+ return error instanceof Error ? error.message : String(error);
34
+ }
35
+
36
+ /**
37
+ * "APPL is not a US ticker: it is Aptus Pharma Limited on BSE (INR). Did you
38
+ * mean AAPL (Apple Inc.)?", for a symbol read on a listing abroad that is one
39
+ * slip from a US ticker, worded as the not-a-ticker answers word it.
40
+ */
41
+ export function notUsTickerSentence({ target, quote, listing, usSuggestion }: { target: { symbol: string }; quote: Quote | null; listing?: { name: string | null }; usSuggestion?: ListingSuggestion }): string | null {
42
+ if (!quote || !usSuggestion) return null;
43
+ const venue = canonicalExchange(quote.listingExchangeName || quote.exchangeName);
44
+ const name = quote.name || listing?.name;
45
+ const what = `${name ? `${name} on ` : ""}${venue}${quote.currency ? ` (${quote.currency})` : ""}`;
46
+ return `${target.symbol} is not a US ticker: it is ${what}. Did you mean ${describeSuggestion(usSuggestion)}?`;
47
+ }
48
+
49
+ export interface QuoteAnswer {
50
+ result: QuoteBatchResult;
51
+ /** The listing the row is: the one asked for, or the venue a bare symbol was quoted on. */
52
+ listing: CliListing;
53
+ /** Why there is no quote, when there is more to say than the source's own error. */
54
+ failure?: { message: string; details?: string; oneLine: string };
55
+ notes: string[];
56
+ }
57
+
58
+ /**
59
+ * One symbol's row of `quote` and `compare`. A listing on an exchange its symbol
60
+ * is not listed on says so. A bare symbol the data service read on its home
61
+ * listing abroad names that venue (SXR8 -> XETRA). One no source quotes is
62
+ * quoted on its listings (`quoteBareListing`), else names them, or asks for an
63
+ * exchange when several companies list it (2222); a bare index root (VIX) also
64
+ * names its index (^VIX), and a futures board code a security keeps (GC) its
65
+ * future (GC=F). Venues are only looked up for a
66
+ * symbol with no quote, so a request that succeeds costs no search.
67
+ */
68
+ export async function answerQuote(
69
+ result: QuoteBatchResult,
70
+ listing: CliListing,
71
+ market: MarketContext,
72
+ request: ListingDataRequest,
73
+ refresh: boolean,
74
+ venues: Promise<ListingVenue[]> | null,
75
+ ): Promise<QuoteAnswer> {
76
+ const root = listing.exchange ? null : indexRootFor(listing.symbol);
77
+ // A bare root that quoted something else, such as MOVE (Corvex), points at the index too.
78
+ const also = (quote: Quote) => root && quote.instrumentType?.trim().toUpperCase() !== "INDEX"
79
+ ? [indexRootAlsoLine(root, request.command)] : [];
80
+ if (result.quote) {
81
+ // "CAR = Avis Budget Group, NASDAQ (also XETRA, ASX)" names the venue too, so it replaces "SXR8 -> XETRA".
82
+ const choice = venues ? await listingChoiceNotes(listing, market, result.quote, venues) : listingReadNotes(listing);
83
+ const venue = choice.length > 0 ? null : bareListingNote(listing, result.quote);
84
+ // A row saved as 247540:KRX is quoted from KOSDAQ, the board it trades on.
85
+ const board = tradingBoardNote(listing, result.quote);
86
+ return { result, listing, notes: [...choice, venue ?? [], board ?? [], also(result.quote), currencyCodeNote(listing, result.quote) ?? []].flat() };
87
+ }
88
+ const notTraded = await notTradedMessage(listing, market);
89
+ if (notTraded) return { result, listing, failure: { message: notTraded, oneLine: notTraded }, notes: [] };
90
+ if (!isNoProviderError(result.error)) return { result, listing, notes: [] };
91
+ const other = await quoteBareListing(listing, market, request, { refresh });
92
+ if (other?.kind === "quoted") {
93
+ return {
94
+ result: { target: other.listing.request, quote: other.quote },
95
+ listing: other.listing,
96
+ notes: [other.note, ...also(other.quote), currencyCodeNote(listing, other.quote) ?? []].flat(),
97
+ };
98
+ }
99
+ // Other listings already name a futures board code's future (GC=F); with none, it is named here.
100
+ // A Treasury yield's root (TNX) names its FRED series and the curve beside its index.
101
+ const yieldTenor = root ? treasuryYieldTenor(listing.symbol) : null;
102
+ const tryLines = [
103
+ yieldTenor ? treasuryYieldLine(yieldTenor) : root ? indexRootTryLine(root, request.command) : null,
104
+ other || listing.exchange ? null : futuresTryLine(listing.symbol, request),
105
+ // BDI asks which exchange (Black Diamond Group) and says nowhere that the Baltic Dry Index is not here.
106
+ listing.exchange ? null : knownGapForTicker(listing.symbol),
107
+ ].filter((line): line is string => !!line);
108
+ if (!other && tryLines.length === 0) return { result: await withCurrencyPairHint(result, market), listing, notes: [] };
109
+ const message = other?.message ?? `${listing.symbol} has no ${request.noun}.`;
110
+ const details = [other?.details, ...tryLines].filter(Boolean).join("\n");
111
+ const oneLine = [
112
+ message,
113
+ other ? `${other.heading}: ${other.listings.join(", ")}.` : "",
114
+ ...tryLines,
115
+ ].filter(Boolean).join(" ");
116
+ return { result, listing, failure: { message, details, oneLine }, notes: [] };
117
+ }
118
+
119
+ /** A listing a command found a quote for, with what `quote` says beside it. */
120
+ interface QuotedListing {
121
+ /** The listing the quote is of: the one typed, or the venue a bare symbol was quoted on. */
122
+ listing: CliListing;
123
+ quote: Quote;
124
+ /** The venue a bare symbol was read on, the currency it may have meant, the US ticker it is one slip from. */
125
+ notes: string[];
126
+ }
127
+
128
+ /**
129
+ * The listing a command that keeps a symbol (`alerts add`) was given, read as
130
+ * `quote` reads it: the same resolver and the same quote request, so what is
131
+ * kept is the listing `quote` prints (USDMXN and USD/MXN are USDMXN=X, MXN is
132
+ * MXN:FWB2) and `quote`'s own notes come back with it. A symbol nothing
133
+ * quotes fails the command with why, and the nearest listing search finds when
134
+ * it is near ("Not a ticker: ZZZQQ. Did you mean ZZZQ?"), so nothing is kept
135
+ * for it.
136
+ */
137
+ export async function requireQuotedListing(
138
+ raw: string,
139
+ exchange: string | null | undefined,
140
+ market: MarketContext,
141
+ ctx: Pick<Parameters<CliCommandDef["execute"]>[1], "fail">,
142
+ command: string,
143
+ ): Promise<QuotedListing> {
144
+ const listing = await requireCliListing(raw, exchange, market, ctx);
145
+ const venues = loadBareVenues(listing, market);
146
+ const [result] = await market.dataProvider.getQuotesBatch([listing.request], { revalidate: true });
147
+ const answer = await answerQuote(result ?? { target: listing.request, quote: null }, listing, market, { command, noun: "quote" }, false, venues);
148
+ const quote = answer.result.quote;
149
+ if (!quote) {
150
+ if (answer.failure) ctx.fail(answer.failure.message, answer.failure.details);
151
+ const message = errorMessage(answer.result.error) ?? `No quote found for ${listing.key}.`;
152
+ // The listing search names, else the US ticker it is one slip from (AAPLL for AAPL), which search does not list for a typo.
153
+ const suggestion = notATickerSymbol(message) && !message.includes(" Did you mean ")
154
+ ? await suggestListing(listing.symbol, market) ?? await suggestUsTicker(listing.symbol, market)
155
+ : null;
156
+ ctx.fail(suggestion ? withListingSuggestion(message, describeSuggestion(suggestion)) : message);
157
+ }
158
+ // A bare symbol read on a listing abroad that is one slip from a US ticker says so, as `quote` does.
159
+ const venue = answer.listing.saved ? null : bareListingNote(answer.listing, quote);
160
+ const usSuggestion = venue && answer.notes.includes(venue) ? await suggestUsTicker(answer.listing.symbol, market) : null;
161
+ const notUs = notUsTickerSentence({ target: answer.result.target, quote, listing: { name: quote.name ?? null }, usSuggestion: usSuggestion ?? undefined });
162
+ return { listing: answer.listing, quote, notes: [...answer.notes, ...notUs ? [notUs] : []] };
163
+ }
164
+
165
+ /**
166
+ * A symbol written like a currency pair that is not a ticker (USD_NGN, as some
167
+ * FX feeds write a pair) names the listing `gloomberb search` finds nearest to
168
+ * it, as `watchlist add` does: "Did you mean USDNGN=X (US Dollar/Nigerian
169
+ * Naira)?". When search finds nothing near, the row keeps its own error and its
170
+ * search hint.
171
+ */
172
+ async function withCurrencyPairHint(result: QuoteBatchResult, market: MarketContext): Promise<QuoteBatchResult> {
173
+ const message = errorMessage(result.error);
174
+ const symbol = message ? notATickerSymbol(message) : null;
175
+ if (!message || !symbol || !looksLikeCurrencyPair(symbol)) return result;
176
+ const suggestion = await suggestListing(symbol, market);
177
+ return suggestion ? { ...result, error: withListingSuggestion(message, describeSuggestion(suggestion)) } : result;
178
+ }
179
+
@@ -69,7 +69,8 @@ export interface CliReportEntry {
69
69
  }
70
70
 
71
71
  type Cell =
72
- | { kind: "blank" }
72
+ /** `unit`: what the column draws a number of this row in, so a missing figure still names its unit. */
73
+ | { kind: "blank"; unit?: string }
73
74
  | { kind: "text"; text: string; time?: boolean }
74
75
  | { kind: "number"; value: number; unit: string }
75
76
  /** Text that may read as a number, once the whole column is known. */
@@ -80,6 +81,8 @@ type ResolvedCell = Exclude<Cell, { kind: "textual" }>;
80
81
  interface ResolvedColumn {
81
82
  header: string;
82
83
  cells: ResolvedCell[];
84
+ /** The unit the column draws its numbers in, for a table with no rows to say it. */
85
+ unit?: string;
83
86
  }
84
87
 
85
88
  const BLANK = { kind: "blank" } as const;
@@ -265,8 +268,9 @@ function resolveColumn(header: string, cells: Cell[], parseText: boolean): Resol
265
268
  };
266
269
  }
267
270
 
271
+ /** The units a column's numbers are in, a missing figure's own among them, so the header does not change with which figures are missing. */
268
272
  function numberUnits(cells: readonly ResolvedCell[]): Set<string> {
269
- return new Set(cells.flatMap((cell) => cell.kind === "number" ? [cell.unit] : []));
273
+ return new Set(cells.flatMap((cell) => cell.kind === "number" ? [cell.unit] : cell.kind === "blank" && cell.unit != null ? [cell.unit] : []));
270
274
  }
271
275
 
272
276
  function cellValue(cell: ResolvedCell): CliReportValue {
@@ -312,8 +316,8 @@ function rowUnits(columns: readonly ResolvedColumn[], mixed: readonly boolean[],
312
316
  * metrics whose rows each have one, and otherwise a `<header> unit` column
313
317
  * beside a column that mixes them (a rate in % and a spread in bp).
314
318
  */
315
- function assembleTable(title: string, columns: readonly ResolvedColumn[], rowCount: number): CliReportTable {
316
- const units = columns.map((column) => numberUnits(column.cells));
319
+ function assembleTable(title: string, columns: readonly ResolvedColumn[], rowCount: number, status?: StatusColumn): CliReportTable {
320
+ const units = columns.map((column) => rowCount === 0 && column.unit != null ? new Set([column.unit]) : numberUnits(column.cells));
317
321
  const mixed = units.map((columnUnits) => columnUnits.size > 1);
318
322
  const byRow = mixed.some(Boolean) ? rowUnits(columns, mixed, rowCount) : null;
319
323
  const layout = columns.flatMap((column, index) => {
@@ -338,6 +342,7 @@ function assembleTable(title: string, columns: readonly ResolvedColumn[], rowCou
338
342
  },
339
343
  ];
340
344
  });
345
+ if (status) layout.push({ header: status.header, value: (row: number) => status.values[row] ?? "" });
341
346
  return {
342
347
  title,
343
348
  columns: layout.map((column) => column.header),
@@ -345,28 +350,141 @@ function assembleTable(title: string, columns: readonly ResolvedColumn[], rowCou
345
350
  };
346
351
  }
347
352
 
353
+ /** The `status` column a table of rows ends its own columns with: why a figure is missing, by row. */
354
+ interface StatusColumn {
355
+ header: string;
356
+ values: string[];
357
+ }
358
+
359
+ /**
360
+ * A number a probe draws through a column's format, to read the unit the
361
+ * column draws numbers in. A figure format rounds it; a format that prints
362
+ * it whole passes values through as text and draws no figures of its own.
363
+ */
364
+ const UNIT_PROBE = 0.1234567891;
365
+
366
+ /**
367
+ * A row with nothing in it that a format can still read through: every
368
+ * field, call and conversion of it is empty. For a format that reads fields of
369
+ * the row beyond its value and would throw on an empty one.
370
+ */
371
+ const EMPTY_ROW: unknown = (() => {
372
+ const empty: unknown = new Proxy(function empty() {}, {
373
+ get: (_target, key) => key === Symbol.toPrimitive ? (hint: string) => (hint === "number" ? Number.NaN : "") : empty,
374
+ apply: () => empty,
375
+ });
376
+ return empty;
377
+ })();
378
+
379
+ /**
380
+ * The unit a column draws a number of this row in (`%` for an IV, `""` for a
381
+ * plain figure), read from what its format draws for one; null when it draws
382
+ * no figure for one, passes the value through as it is, or throws.
383
+ */
384
+ function drawnUnit<Row>(column: CliReportColumn<Row>, row: Row): string | null {
385
+ const unit = column.unitKey ? (row as Record<string, unknown> | undefined)?.[column.unitKey] : undefined;
386
+ if (typeof unit === "string" && unit) return unit;
387
+ if (!column.format) return null;
388
+ try {
389
+ const drawn = column.format(UNIT_PROBE, row);
390
+ if (typeof drawn !== "string" || drawn.trim() === String(UNIT_PROBE)) return null;
391
+ const read = readDisplay(drawn);
392
+ return read && read !== BLANK_DISPLAY ? read.unit : null;
393
+ } catch {
394
+ // Intentional ignore: a format that cannot draw a number for this row draws no unit for it.
395
+ return null;
396
+ }
397
+ }
398
+
399
+ /**
400
+ * The unit a column draws its figures in whatever the rows, read without one:
401
+ * on an empty row, or through a row of nothing when the format throws on an
402
+ * empty one. The same with rows and without, so a header and the table's
403
+ * status column do not depend on what was loaded. Null for a column of text.
404
+ */
405
+ function columnUnit<Row>(column: CliReportColumn<Row>): string | null {
406
+ if (column.unitKey) return "";
407
+ if (!column.format) return null;
408
+ try {
409
+ column.format(UNIT_PROBE, {} as Row);
410
+ return drawnUnit(column, {} as Row);
411
+ } catch {
412
+ // Intentional ignore: the format reads a field an empty row lacks; read it through a row of nothing.
413
+ return drawnUnit(column, EMPTY_ROW as Row);
414
+ }
415
+ }
416
+
417
+ /** What a status cell says: each reason once, after the columns it is the reason for. */
418
+ function statusText(reasons: ReadonlyArray<{ header: string; text: string }>): string {
419
+ const byText = new Map<string, string[]>();
420
+ for (const { header, text } of reasons) byText.set(text, [...byText.get(text) ?? [], header]);
421
+ return [...byText].map(([text, headers]) => `${headers.join(", ")}: ${text}`).join("; ");
422
+ }
423
+
424
+ const isMissing = (value: unknown) => value == null || (typeof value === "number" && !Number.isFinite(value));
425
+
348
426
  // Codes a spreadsheet must not turn into numbers, even when they are digits.
349
427
  const IDENTIFIER_KEY = /^(?:id|cik|code|cusip|figi|isin|symbol|ticker)$|(?:Id|Cik|Code|Cusip|Figi|Isin|Symbol|Ticker)$/;
350
428
 
351
- /** A table of rows under the columns the text view draws. */
429
+ /**
430
+ * A table of rows under the columns the text view draws. A figure that is
431
+ * missing still names the unit its column draws it in, so a header is the same
432
+ * whichever figures a symbol has. With `status`, a reason the text draws in
433
+ * place of a missing figure (`not quoted`, `under 7d`) leaves the cell empty
434
+ * and goes to the `status` column the table ends its own columns with, so a
435
+ * column of figures holds only numbers. Only a row whose own value is missing,
436
+ * in a column that draws that row's figures, has a reason: text a format draws
437
+ * from the rest of the row (a range, N/M, ATM) stays. A table has the column
438
+ * when one of its columns draws figures through a format, read from the
439
+ * columns alone so it is there with or without rows, and a table of text has
440
+ * none; beside a column of its own named Status it is `value_status`.
441
+ */
352
442
  export function exportRowsTable<Row extends Record<string, unknown>>(
353
443
  title: string,
354
444
  columns: readonly CliReportColumn<Row>[],
355
445
  rows: readonly Row[],
446
+ { status = false }: { status?: boolean } = {},
356
447
  ): CliReportTable {
357
- return assembleTable(title, columns.map((column) => resolveColumn(
358
- column.header,
359
- rows.map((row): Cell => {
360
- const record = row as Record<string, unknown> | undefined;
361
- const value = record?.[column.key];
362
- const unit = column.unitKey ? record?.[column.unitKey] : undefined;
363
- if (typeof unit === "string" && unit && typeof value === "number" && Number.isFinite(value)) {
364
- return { kind: "number", value: exportNumber(value), unit };
365
- }
366
- return exportCell(value, column.format ? column.format(value, row) : null, column.key);
367
- }),
368
- !IDENTIFIER_KEY.test(column.key),
369
- )), rows.length);
448
+ const reasons = rows.map((): Array<{ header: string; text: string }> => []);
449
+ const units = columns.map(columnUnit);
450
+ // Decided by the columns alone, so a table has the column, or not, whatever rows it has.
451
+ const withStatus = status && units.some((unit) => unit != null);
452
+ const resolved = columns.map((column, columnIndex): ResolvedColumn => {
453
+ const rowUnits = rows.map((row) => drawnUnit(column, row));
454
+ const values = rows.map((row) => (row as Record<string, unknown> | undefined)?.[column.key]);
455
+ const { header, cells } = resolveColumn(
456
+ column.header,
457
+ rows.map((row, index): Cell => {
458
+ const value = values[index];
459
+ const unit = column.unitKey ? (row as Record<string, unknown> | undefined)?.[column.unitKey] : undefined;
460
+ if (typeof unit === "string" && unit && typeof value === "number" && Number.isFinite(value)) {
461
+ return { kind: "number", value: exportNumber(value), unit };
462
+ }
463
+ return exportCell(value, column.format ? column.format(value, row) : null, column.key);
464
+ }),
465
+ !IDENTIFIER_KEY.test(column.key),
466
+ );
467
+ const table: ResolvedColumn = {
468
+ header,
469
+ cells: cells.map((cell, index): ResolvedCell => {
470
+ const unit = rowUnits[index];
471
+ const blank: ResolvedCell = unit != null ? { kind: "blank", unit } : BLANK;
472
+ if (cell.kind === "blank") return cell.unit == null ? blank : cell;
473
+ // Text drawn in place of a figure this row does not have, where the column draws this row's figures: a reason.
474
+ // Text drawn from the rest of the row (a range, N/M, ATM) is no figure's absence and stays.
475
+ if (!withStatus || cell.kind !== "text" || cell.time || unit == null || !isMissing(values[index])) return cell;
476
+ reasons[index]!.push({ header: column.header, text: cell.text });
477
+ return blank;
478
+ }),
479
+ };
480
+ const unit = units[columnIndex];
481
+ return unit == null ? table : { ...table, unit };
482
+ });
483
+ const statusColumn = withStatus ? {
484
+ header: columns.some((column) => column.header.trim().toLowerCase() === "status") ? "value_status" : "status",
485
+ values: reasons.map(statusText),
486
+ } : undefined;
487
+ return assembleTable(title, resolved, rows.length, statusColumn);
370
488
  }
371
489
 
372
490
  /** Label/value lines as `Metric,Value`; a number's unit moves into its label. */
package/src/cli/result.ts CHANGED
@@ -46,6 +46,12 @@ export interface CliResult<T = unknown> {
46
46
  * `fear-greed` prints its row alone.
47
47
  */
48
48
  exportFreshness?: ReportFreshness;
49
+ /**
50
+ * Why a result that still prints counts as a failure (a report with no
51
+ * data): the command exits 1, and JSON says `ok: false` with this as
52
+ * `error` beside the data, so the envelope and the exit status agree.
53
+ */
54
+ failure?: CliErrorObject;
49
55
  }
50
56
 
51
57
  export interface CliErrorObject {
@@ -110,8 +116,9 @@ export interface CliResultRenderOptions<T = unknown, Row = Record<string, unknow
110
116
  exportNotes?: readonly string[];
111
117
  }
112
118
 
113
- interface CliResultJsonEnvelope<T> extends Omit<CliResult<T>, "freshness" | "exportFreshness"> {
114
- ok: true;
119
+ interface CliResultJsonEnvelope<T> extends Omit<CliResult<T>, "freshness" | "exportFreshness" | "failure"> {
120
+ ok: boolean;
121
+ error?: CliErrorObject;
115
122
  /** Where the data comes from, as CSV's closing `#` lines say it. */
116
123
  source?: ExportSource;
117
124
  columns?: Array<Pick<CliResultColumn, "key" | "header" | "align" | "width">>;
@@ -398,14 +405,15 @@ export function serializeCliResult<T, Row extends Record<string, unknown> = Reco
398
405
  options: CliGlobalOptions,
399
406
  renderOptions: CliResultRenderOptions<T, Row> = {},
400
407
  ): string {
401
- const { freshness, exportFreshness, ...rest } = result;
408
+ const { freshness, exportFreshness, failure, ...rest } = result;
402
409
  const cited = freshness ?? exportFreshness;
403
410
  if (options.format === "json") {
404
411
  const columns = serializeColumns(renderOptions.columns);
405
412
  const window = Array.isArray(result.data) ? windowed(result.data as unknown[], options, renderOptions) : null;
406
413
  const cut = window ? cutMetadata(window) : null;
407
414
  const envelope: CliResultJsonEnvelope<T> = {
408
- ok: true,
415
+ ok: !failure,
416
+ ...(failure ? { error: failure } : {}),
409
417
  ...rest,
410
418
  ...(freshness || cut ? { metadata: { ...rest.metadata, ...(freshness ? { freshness } : {}), ...(cut ? { rows: cut } : {}) } } : {}),
411
419
  data: window ? window.rows as T : result.data,
@@ -1,6 +1,7 @@
1
1
  import { seriesExpression } from "../series/registry";
2
2
  import type { SeriesDefinition } from "../series/types";
3
3
  import { cliStyles, renderSection, renderTable, wrapText } from "../utils/cli-output";
4
+ import { currencyCodeLabel } from "../utils/currency-units";
4
5
 
5
6
  /** A data catalog series a query names, as the CLI lists it. */
6
7
  export interface SeriesMatch {
@@ -108,7 +109,7 @@ export function renderSeriesMatches(
108
109
  renderTable(
109
110
  [{ header: "Series" }, { header: "Unit" }, { header: "Frequency", optional: true }, { header: "Publisher", optional: true },
110
111
  { header: "Chart", shrink: false }],
111
- shown.map((match) => [match.name, match.unit, match.frequency, match.publisher ?? "", cliStyles.command(shellArgument(match.expression))]),
112
+ shown.map((match) => [match.name, currencyCodeLabel(match.unit), match.frequency, match.publisher ?? "", cliStyles.command(shellArgument(match.expression))]),
112
113
  ),
113
114
  "",
114
115
  ...wrapText(hint, width).map((line) => cliStyles.muted(line)),
@@ -4,6 +4,8 @@ import { splitSymbolArgs, splitSymbolWords, stripStrayEdges, uniqueSymbols } fro
4
4
 
5
5
  /** Header cells that name a spreadsheet's symbol column, compared without case, spaces or punctuation. */
6
6
  const SYMBOL_HEADERS = new Set(["symbol", "symbols", "ticker", "tickers", "code", "stockcode", "securitycode", "instrument"]);
7
+ /** A header naming a column of company names, read when no column names the symbols. */
8
+ const NAME_HEADERS = new Set(["name", "names", "company", "companies", "companyname", "security", "securityname", "issuer", "issuername"]);
7
9
 
8
10
  /** One symbol as it can be typed: AAPL, $nvda, TTF.AS, SAN:EPA, BRK-B, 7203.T, ^GSPC, ES=F. */
9
11
  const SYMBOL_TOKEN = /^\$?[\p{L}\p{N}^][\p{L}\p{N}.:\-^=/&]{0,23}$/u;
@@ -82,17 +84,32 @@ interface SymbolText {
82
84
  symbols: string[];
83
85
  /** Cells beside the symbol column of a table with no header, in reading order; empty for a list or a CSV with a header. */
84
86
  ignored: string[];
87
+ /**
88
+ * The symbols are a list's entries, each a line or a cell of a list split
89
+ * by semicolons or commas, and are read as typed entries are
90
+ * (`readNameEntries`): one with spaces is a company's name (`Altria
91
+ * Group`), or symbols written on one line (`AAPL MSFT`).
92
+ */
93
+ entries?: true;
85
94
  }
86
95
 
87
96
  /**
88
97
  * The symbols in a file or piped text, in order. A spreadsheet export reads
89
- * from the column its header names (Symbol, Ticker, Code). Without a header,
98
+ * from the column its header names (Symbol, Ticker, Code), else from its
99
+ * Name or Company column, as `entries`. Without a header,
90
100
  * every cell is a symbol unless some column after the first holds a name, a
91
101
  * price or a weight: that table is read by its first column, and the cells
92
102
  * beside it are returned as `ignored` so the command can name them. A list
93
103
  * split by commas, spaces or newlines gives every entry, even when a line of
94
- * it is ragged or carries a stray separator (`"BIMAS ,"`). A byte order mark,
95
- * Windows line ends, blank lines and lines starting with # or // are ignored.
104
+ * it is ragged or carries a stray separator (`"BIMAS ,"`). A list of lines
105
+ * gives each line whole, and a list of names split by semicolons or commas
106
+ * (one line, or one name a line) each cell, as `entries`: a line with spaces
107
+ * is a company's name (`Altria Group`) or symbols on one line, which reading
108
+ * the entries tells apart. Names beside other text (`Apple Inc,Technology`)
109
+ * are a table read by its first column, its names as `entries`. A list split
110
+ * by spaces with figures beside the symbols (`AAPL 150.00`) is a table. A
111
+ * byte order mark, Windows line ends, blank lines and lines starting with #
112
+ * or // are ignored.
96
113
  */
97
114
  export function parseSymbolText(text: string): SymbolText {
98
115
  const normalized = text.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n");
@@ -105,13 +122,27 @@ export function parseSymbolText(text: string): SymbolText {
105
122
  const exact = header.findIndex((cell) => SYMBOL_HEADERS.has(cell));
106
123
  const column = exact >= 0 ? exact : header.findIndex((cell) => cell.includes("symbol") || cell.includes("ticker"));
107
124
  if (column >= 0) return { symbols: records.slice(1).map((cells) => cells[column] ?? "").filter(Boolean), ignored: [] };
125
+ // Without one, a delimited export's Name or Company column holds the companies, read as typed names are.
126
+ const nameColumn = delimiter ? header.findIndex((cell) => NAME_HEADERS.has(cell)) : -1;
127
+ if (nameColumn >= 0) return { symbols: records.slice(1).map((cells) => cells[nameColumn] ?? "").filter(Boolean), ignored: [], entries: true };
108
128
  // A separator or quote mark stuck to a cell is dropped (and named) when it is read, so it does not make the list a table.
109
129
  const cleaned = records.map((cells) => cells.map((cell) => stripStrayEdges(cell).text));
130
+ const figures = cleaned.some((cells) => cells.some((cell, index) => index > 0 && NUMBER_CELL.test(cell) && /\d/.test(cell)));
131
+ // Lines split by spaces alone are entries, unless figures sit beside the symbols.
132
+ if (!delimiter && !figures) return { symbols: records.map((cells) => cells.join(" ")), ignored: [], entries: true };
110
133
  const grid = cleaned.every((cells) => cells.every((cell, index) => !cell || (looksLikeSymbol(cell) && (index === 0 || !NUMBER_CELL.test(cell)))));
111
134
  if (grid) return { symbols: records.flat().filter(Boolean), ignored: [] };
135
+ // Names split by semicolons or commas, with no figures: one pasted line, or one name a line with a separator left
136
+ // on it, is a list. A name beside a sector or any other text is a table, read by its first column below.
137
+ const names = cleaned.some((cells) => /\s/.test(cells[0] ?? ""));
138
+ if (!figures && (records.length === 1 || (names && cleaned.every((cells) => cells.filter(Boolean).length <= 1)))) {
139
+ return { symbols: records.flat().filter(Boolean), ignored: [], entries: true };
140
+ }
112
141
  return {
113
142
  symbols: records.map((cells) => cells[0] ?? "").filter(Boolean),
114
143
  ignored: records.flatMap((cells) => cells.slice(1)).filter(Boolean),
144
+ // A first column of names (Apple Inc) is read as typed names are.
145
+ ...names ? { entries: true as const } : {},
115
146
  };
116
147
  }
117
148
 
@@ -151,8 +182,10 @@ export interface SymbolListSource {
151
182
  stdinPiped?: boolean;
152
183
  /**
153
184
  * Reads the typed entries (`splitSymbolArgs`) as symbols, such as
154
- * `readNameEntries`, which keeps a company's name whole. Without it every
155
- * entry is its words, each a symbol.
185
+ * `readNameEntries`, which keeps a company's name whole, and then the
186
+ * entries of each list a file or standard input holds. Called first for
187
+ * what was typed, even when that is nothing. Without it every entry is its
188
+ * words, each a symbol.
156
189
  */
157
190
  readEntries?: (entries: string[]) => Promise<string[]>;
158
191
  }
@@ -176,17 +209,20 @@ export async function readSymbolList(source: SymbolListSource): Promise<{ symbol
176
209
  const readStdin = source.readStdin ?? (() => Bun.stdin.text());
177
210
  const wantsStdin = source.args.includes("-") || source.file === "-";
178
211
  const typedArgs = source.args.filter((arg) => arg !== "-");
212
+ const readEntries = (entries: string[]) => source.readEntries ? source.readEntries(entries) : Promise.resolve(splitSymbolWords(entries));
179
213
  const typed = source.readEntries ? await source.readEntries(splitSymbolArgs(typedArgs)) : splitSymbolWords(typedArgs);
180
214
  const collected = [...typed];
181
215
  // A separator left on the end of what was typed: `UPST;`, or a list that ends in a comma.
182
216
  const strays = new Set<string>(stripStrayEdges(typedArgs.join(" ")).notes);
183
217
  const ignored: string[] = [];
184
- const addCells = ({ symbols, ignored: beside }: SymbolText) => {
185
- for (const cell of symbols) {
218
+ const addCells = async ({ symbols, ignored: beside, entries }: SymbolText) => {
219
+ const cells = symbols.map((cell) => {
186
220
  const cleaned = stripStrayEdges(cell);
187
221
  cleaned.notes.forEach((note) => strays.add(note));
188
- collected.push(cleaned.text);
189
- }
222
+ return cleaned.text;
223
+ }).filter(Boolean);
224
+ // A list's lines and names are read as typed entries are: `Altria Group` stays one company.
225
+ collected.push(...entries ? await readEntries(cells) : cells);
190
226
  ignored.push(...beside);
191
227
  };
192
228
  if (source.file != null && source.file !== "-") {
@@ -198,10 +234,10 @@ export async function readSymbolList(source: SymbolListSource): Promise<{ symbol
198
234
  const code = (error as NodeJS.ErrnoException).code;
199
235
  throw new SymbolListError(code === "ENOENT" ? `No file at ${source.file}.` : `Could not read ${source.file}: ${error instanceof Error ? error.message : String(error)}`);
200
236
  }
201
- addCells(parseSymbolText(text));
237
+ await addCells(parseSymbolText(text));
202
238
  }
203
239
  if (wantsStdin || (typed.length === 0 && source.file == null && (source.stdinPiped ?? stdinIsPiped()))) {
204
- addCells(parseSymbolText(await readStdin()));
240
+ await addCells(parseSymbolText(await readStdin()));
205
241
  }
206
242
  const ignoredNote = ignoredCellsNote(ignored);
207
243
  return { ...uniqueSymbols(collected), notes: [...strays, ...ignoredNote ? [ignoredNote] : []] };
package/src/cli/types.ts CHANGED
@@ -5,6 +5,7 @@ import { AssetDataRouter } from "../sources/provider-router";
5
5
  import type { AppServices } from "../core/app-services";
6
6
  import type { CapabilityInvoker } from "../capabilities/types";
7
7
  import type { StatementReportSources } from "./statement-filings";
8
+ import type { FxHistoryClient } from "../market-data/fx-closes";
8
9
 
9
10
  export type ConfigContext = {
10
11
  config: AppConfig;
@@ -15,12 +16,15 @@ export type ConfigContext = {
15
16
 
16
17
  export type MarketContext = ConfigContext & {
17
18
  dataProvider: AssetDataRouter;
19
+ resolveBroker?: (brokerType: string) => import("../types/broker").BrokerAdapter | null;
18
20
  /** Charts catalog series in a report run without the plugin runtime. */
19
21
  seriesCapabilities?: CapabilityInvoker;
20
22
  /** `--refresh`: prefetched data bypasses the local cache. */
21
23
  refresh?: boolean;
22
24
  /** Filings behind the statements and a bank's figures, read from Gloom Cloud; absent where the data service is a stand-in. */
23
25
  statementSources?: StatementReportSources;
26
+ /** Daily FX closes (`fx --date`, converted INS values); Gloom Cloud unless a test stands in for it. */
27
+ fxHistory?: FxHistoryClient;
24
28
  };
25
29
 
26
30
  export type CliServicesContext = {