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
@@ -2,9 +2,29 @@ import { THEME_PERIODS, type ThemeAggregate, type ThemeMember, type ThemePeriod,
2
2
  import type { DataTableColumn } from "../../../components";
3
3
  import { dropColumnsToFit, getTableWidth } from "../../../components/ui/table-layout";
4
4
  import { extendedSessionStateLabel, type ExtendedSession } from "../../../market-data/market/status";
5
+ import { yearToDateBaseLabel } from "../../../market-data/market/year-to-date";
5
6
  import { compareSortValues, type SortDirection } from "../../../utils/sort-values";
6
7
 
7
- export const PERIOD_LABELS: Record<ThemePeriod, string> = { changePercent: "1D", return1WPercent: "1W", return1MPercent: "1M", return3MPercent: "3M", returnYtdPercent: "YTD" };
8
+ const PERIOD_LABELS: Record<ThemePeriod, string> = { changePercent: "1D", return1WPercent: "1W", return1MPercent: "1M", return3MPercent: "3M", returnYtdPercent: "YTD" };
9
+
10
+ /**
11
+ * The close a board's YTD is measured from, `31 Dec`: NYSE's last session of
12
+ * the year before the session the board describes, as the server measures it.
13
+ */
14
+ export function boardYtdBase(payload: { asOf?: unknown; regularSessionDate?: unknown; regularSessionClosedAt?: unknown }, now = Date.now()): string {
15
+ const day = boardClose(payload)?.date ?? (typeof payload.asOf === "string" && /^\d{4}-\d{2}-\d{2}/.test(payload.asOf) ? payload.asOf.slice(0, 10) : new Date(now).toISOString().slice(0, 10));
16
+ return yearToDateBaseLabel(day, "NYSE", now);
17
+ }
18
+
19
+ /**
20
+ * A period's header. YTD names its base close, as long as `room` allows:
21
+ * `YTD vs 31 Dec close`, `YTD vs 31 Dec`, then `YTD 31 Dec`.
22
+ */
23
+ export function periodLabel(period: ThemePeriod, ytdBase: string | null, room = Infinity): string {
24
+ if (period !== "returnYtdPercent" || !ytdBase) return PERIOD_LABELS[period];
25
+ const labels = [`YTD vs ${ytdBase} close`, `YTD vs ${ytdBase}`, `YTD ${ytdBase}`];
26
+ return labels.find((label) => label.length <= room) ?? labels.at(-1)!;
27
+ }
8
28
  export interface ThemeSort { columnId: string; direction: SortDirection }
9
29
  export const DEFAULT_SORT: ThemeSort = { columnId: "changePercent", direction: "desc" };
10
30
  export function percent(value: number | null): string {
@@ -110,17 +130,27 @@ export function matchTheme(rows: readonly ThemeSummary[], input: string): ThemeS
110
130
  }
111
131
 
112
132
  const column = (id: string, label: string, width: number, align: "left" | "right" = "right"): DataTableColumn => ({ id, label, width, align });
113
- /** Drop leaders/laggards first, then longer windows; keep the day's move and breadth. */
114
- export function themeColumns(width: number, rows: readonly ThemeSummary[]): DataTableColumn[] {
133
+ /** Room a header has in its column: the sort indicator takes two cells. */
134
+ const HEADER_ROOM = 2;
135
+ /**
136
+ * Drop leaders/laggards first, then longer windows; keep the day's move and
137
+ * breadth. YTD names `ytdBase`, the close it is measured from, as fully as
138
+ * its column allows.
139
+ */
140
+ export function themeColumns(width: number, rows: readonly ThemeSummary[], ytdBase: string | null = null): DataTableColumn[] {
115
141
  const suffixWidth = (period: ThemePeriod) => coverageWidth(rows.map((row) => row.returns[period]));
116
142
  const metricWidth = (period: ThemePeriod) => Math.max(8, ...rows.map((row) => percent(row.returns[period].value).length)) + suffixWidth(period);
143
+ // The same trailing space is reserved in every cell and its header. A note
144
+ // occupies that space without moving the number away from its label.
145
+ const label = (period: ThemePeriod, room?: number) => periodLabel(period, ytdBase, room) + " ".repeat(suffixWidth(period));
117
146
  const columns = [column("name", "Theme", 26, "left"), column("memberCount", "N", 3),
118
- // The same trailing space is reserved in every cell and its header. A note
119
- // occupies that space without moving the number away from its label.
120
- ...THEME_PERIODS.map((period) => column(period, PERIOD_LABELS[period] + " ".repeat(suffixWidth(period)), metricWidth(period))),
147
+ ...THEME_PERIODS.map((period) => column(period, label(period, 0), Math.max(metricWidth(period), label(period, 0).length + HEADER_ROOM))),
121
148
  column("breadth", "Breadth" + " ".repeat(coverageWidth(rows.map((row) => row.breadth))), 7 + coverageWidth(rows.map((row) => row.breadth))),
122
149
  column("best", "Best 1D", 15), column("worst", "Worst 1D", 15)];
123
- return fitColumns(columns, width, ["worst", "best", "return3MPercent", "returnYtdPercent", "return1MPercent", "return1WPercent", "memberCount"]);
150
+ const fitted = fitColumns(columns, width, ["worst", "best", "return3MPercent", "returnYtdPercent", "return1MPercent", "return1WPercent", "memberCount"]);
151
+ const ytd = fitted.find((col) => col.id === "returnYtdPercent");
152
+ if (ytd) ytd.label = label("returnYtdPercent", ytd.width - HEADER_ROOM - suffixWidth("returnYtdPercent"));
153
+ return fitted;
124
154
  }
125
155
  /**
126
156
  * The member table at `width`. Price and 1D are the regular session's; a
@@ -128,12 +158,15 @@ export function themeColumns(width: number, rows: readonly ThemeSummary[]): Data
128
158
  * each session in `extendedSessions`. A narrow pane gives up the longer
129
159
  * windows first, then that column, then the name.
130
160
  */
131
- export function memberColumns(width: number, extendedSessions: readonly ExtendedSession[] = []): DataTableColumn[] {
161
+ export function memberColumns(width: number, extendedSessions: readonly ExtendedSession[] = [], ytdBase: string | null = null): DataTableColumn[] {
132
162
  const extended = extendedSessions.map((session) => column(session === "PRE" ? "preMarket" : "afterHours", extendedSessionStateLabel(session), 9));
133
- const [day, ...windows] = THEME_PERIODS.map((period) => column(period, PERIOD_LABELS[period], 9));
134
- return fitColumns([column("symbol", "Ticker", 7, "left"), column("name", "Name", 24, "left"), column("price", "Price $", 10),
163
+ const [day, ...windows] = THEME_PERIODS.map((period) => column(period, periodLabel(period, ytdBase, 0), Math.max(9, periodLabel(period, ytdBase, 0).length + HEADER_ROOM)));
164
+ const fitted = fitColumns([column("symbol", "Ticker", 7, "left"), column("name", "Name", 24, "left"), column("price", "Price $", 10),
135
165
  day!, ...extended, ...windows], width,
136
166
  ["return3MPercent", "returnYtdPercent", "return1MPercent", "return1WPercent", "preMarket", "afterHours", "name", "price"]);
167
+ const ytd = fitted.find((col) => col.id === "returnYtdPercent");
168
+ if (ytd) ytd.label = periodLabel("returnYtdPercent", ytdBase, ytd.width - HEADER_ROOM);
169
+ return fitted;
137
170
  }
138
171
  function fitColumns(columns: DataTableColumn[], width: number, drop: string[]): DataTableColumn[] {
139
172
  columns = dropColumnsToFit(columns, width, drop);
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useMemo, type ReactNode } from "react";
2
2
  import type { ThemeMember, ThemePeriod, ThemeSummary } from "../../../api-client/themes";
3
- import { DataTableStackView, DataTableView, PaneStatusBody, usePaneNoticeFooter, usePaneStatusFooter, type DataTableCell, type DataTableColumn } from "../../../components";
3
+ import { splitDelayedStatus, DataTableStackView, DataTableView, PaneStatusBody, usePaneNoticeFooter, usePaneStatusFooter, type DataTableCell, type DataTableColumn } from "../../../components";
4
4
  import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
5
5
  import type { ExtendedSession } from "../../../market-data/market/status";
6
6
  import { useAsyncResource, useAutoRefresh, usePluginPaneState, usePluginTickerActions, useUpdatedAgo } from "../../../public/react";
@@ -12,7 +12,7 @@ import { nextHeaderSort } from "../../../utils/sort-values";
12
12
  import { useMembersMenu } from "../shared/members-menu";
13
13
  import { cachedMembers, cachedThemes, loadMembers, loadThemes } from "./client";
14
14
  import {
15
- aggregateText, coverageWidth, DEFAULT_SORT, extendedMove, matchTheme, memberColumns, memberExtendedSessions, memberPrice, percent,
15
+ aggregateText, boardYtdBase, coverageWidth, DEFAULT_SORT, extendedMove, matchTheme, memberColumns, memberExtendedSessions, memberPrice, percent,
16
16
  SNAPSHOT_STALE_MS, snapshotFooterText, sortMembers, sortThemes, themeColumns, type ThemeSort,
17
17
  } from "./model";
18
18
 
@@ -24,7 +24,7 @@ function useSnapshotFooter(id: string, resource: { loading: boolean; error: stri
24
24
  const payload = data?.payload;
25
25
  // Outside the regular session the figures are a close, so the footer names it, not the snapshot's time.
26
26
  const text = age && payload ? snapshotFooterText(payload, age) : null;
27
- const info = useMemo(() => text ? [{ id: "snapshot", parts: [{ text, tone: "muted" as const }] }] : [], [text]);
27
+ const info = useMemo(() => text ? splitDelayedStatus({ id: "snapshot", parts: [{ text, tone: "muted" }] }) : [], [text]);
28
28
  // The server flag includes retained historical returns; it does not mean the daily board is old.
29
29
  const snapshotStale = !!data && Date.now() - Date.parse(data.payload.asOf) > SNAPSHOT_STALE_MS;
30
30
  usePaneStatusFooter({ registrationId: id, enabled, loading: resource.loading && !!data,
@@ -45,8 +45,10 @@ function Members({ id, width, height, focused }: { id: string; width: number; he
45
45
  const rows = useMemo(() => sortMembers(data?.members ?? [], sort), [data, sort]);
46
46
  // Price and 1D are the regular session's; a pre-market or after-hours print gets its own column when a member has one.
47
47
  const extendedSessionsKey = useMemo(() => memberExtendedSessions(data?.members ?? []).join(","), [data]);
48
- const columns = useMemo(() => memberColumns(width, extendedSessionsKey ? extendedSessionsKey.split(",") as ExtendedSession[] : []),
49
- [extendedSessionsKey, width]);
48
+ // YTD names the close it is measured from, `31 Dec`.
49
+ const ytdBase = data ? boardYtdBase(data) : null;
50
+ const columns = useMemo(() => memberColumns(width, extendedSessionsKey ? extendedSessionsKey.split(",") as ExtendedSession[] : [], ytdBase),
51
+ [extendedSessionsKey, width, ytdBase]);
50
52
  useAutoRefresh(resource.updatedAt, resource.load);
51
53
  usePaneRefreshKey(() => void resource.reload(), { focused });
52
54
  useSnapshotFooter("themes:members", resource, focused);
@@ -103,7 +105,7 @@ export function ThemesBoard({ width, height, focused, tabStrip, tabRows = 0 }: T
103
105
  const resource = useAsyncResource(loadThemes, { initialData: cachedThemes });
104
106
  const data = resource.data?.payload;
105
107
  const rows = useMemo(() => sortThemes(data?.themes ?? [], sort), [data, sort]);
106
- const columns = useMemo(() => themeColumns(width, data?.themes ?? []), [width, data]);
108
+ const columns = useMemo(() => themeColumns(width, data?.themes ?? [], data ? boardYtdBase(data) : null), [width, data]);
107
109
  const theme = opened && data ? matchTheme(data.themes, opened) : null;
108
110
  useAutoRefresh(resource.updatedAt, resource.load);
109
111
  usePaneRefreshKey(() => void resource.reload(), { focused, enabled: !opened });
@@ -13,6 +13,7 @@ import { apiClient } from "../../../api-client";
13
13
  import { ApiRequestError, isPermanentClientError } from "../../../api-client/errors";
14
14
  import { httpFetch } from "../../../utils/http-transport";
15
15
  import { asRecord, finiteOrNull } from "../../../utils/guards";
16
+ import { logFailure } from "../../../utils/log-failure";
16
17
 
17
18
  const FORMS_13F_BASE_URL = "https://forms13f.com/api/v1";
18
19
  const FORM_PAGE_LIMIT = 100;
@@ -169,7 +170,9 @@ async function fetchForms13F<T>(
169
170
  slot.promise = promise;
170
171
  activeRequests.set(key, promise);
171
172
  if (cached != null && !options.forceRefresh) {
172
- void promise.catch(() => {});
173
+ void promise.catch((error: unknown) => {
174
+ if (!options.signal?.aborted) logFailure("thirteenf.api", "Refresh cached filing data failed")(error);
175
+ });
173
176
  return cached.value;
174
177
  }
175
178
  return promise;
@@ -13,6 +13,7 @@ import {
13
13
  } from "./api";
14
14
  import { isoDateDaysAgo, isoDateToday, isoDateYearsAgo } from "../../../utils/calendar-date";
15
15
  import { abortable, withDeadline } from "../../../utils/async-deadline";
16
+ import { logFailure } from "../../../utils/log-failure";
16
17
  import {
17
18
  buildBrowserRows,
18
19
  filingGapFor,
@@ -239,7 +240,10 @@ async function loadBrowserPage(
239
240
  }
240
241
  const found = await searchThirteenFFunds(trimmed, browserLimit, signal, pageApiOptions);
241
242
  const funds = oneFundPerCik(found);
242
- const topFunds = await listTopThirteenFFunds(quarter, browserLimit, signal, apiOptions).catch((): ThirteenFTopFund[] => []);
243
+ const topFunds = await listTopThirteenFFunds(quarter, browserLimit, signal, apiOptions).catch((error: unknown): ThirteenFTopFund[] => {
244
+ if (!signal?.aborted) logFailure("thirteenf.data", "Load performance ranks for fund search failed")(error);
245
+ return [];
246
+ });
243
247
  const topByCik = new Map(topFunds.map((fund) => [fund.cik, fund]));
244
248
  const matchedTopFunds = funds
245
249
  .map((fund) => topByCik.get(fund.cik))
@@ -647,7 +647,9 @@ export function createThirteenFHeadless(
647
647
  if (ctx.signal?.aborted) throw error;
648
648
  return [];
649
649
  });
650
- shadowed.catch(() => {});
650
+ shadowed.catch(() => {
651
+ // Intentional ignore: only cancellation reaches this guard; the awaited promise still rejects.
652
+ });
651
653
  }
652
654
  try {
653
655
  holdings = await loadTickerHoldings(ticker, Number(args.options.offset ?? 0), ctx.signal);
@@ -17,6 +17,7 @@ import {
17
17
  } from "../../../ui";
18
18
  import { isDetailBackNavigationKey } from "../../../utils/back-navigation";
19
19
  import { isPlainKey } from "../../../utils/keyboard";
20
+ import { logFailure } from "../../../utils/log-failure";
20
21
  import { isPlainArrowUp, stopSearchFocusNavigation } from "../../../utils/search-focus-navigation";
21
22
  import { nextHeaderSort } from "../../../utils/sort-values";
22
23
  import { useAssetData, usePluginPaneState, usePluginTickerActions } from "../../runtime";
@@ -118,7 +119,7 @@ function ThirteenFBrowserPane({ focused, width, height, onDetailChange }: PanePr
118
119
  let alive = true;
119
120
  void Promise.all(browser.pages.map((page) => page.reportsLoaded ?? Promise.resolve())).then(() => {
120
121
  if (alive) setReportPaint((value) => value + 1);
121
- }, () => {});
122
+ }, logFailure("thirteenf.pane", "Load filing metadata for fund rows failed"));
122
123
  return () => { alive = false; };
123
124
  }, [browser.pages]);
124
125
  const error = browser.error?.message ?? null;
@@ -193,7 +194,9 @@ function ThirteenFBrowserPane({ focused, width, height, onDetailChange }: PanePr
193
194
  // Every request behind a fund detail is cached per path, so warming it
194
195
  // while the cursor rests on the row makes Enter read from cache.
195
196
  const prefetchDetail = useCallback((row: FundBrowserRow) => {
196
- void loadFundDetail(row.cik, row.name).catch(() => {});
197
+ void loadFundDetail(row.cik, row.name).catch(() => {
198
+ // Intentional ignore: speculative prefetch is retried and failures are shown when the fund opens.
199
+ });
197
200
  }, []);
198
201
 
199
202
  const browserStatusInfo = useMemo<PaneFooterSegment[]>(() => [
@@ -376,7 +379,9 @@ export function FundDetailView({
376
379
  : Promise.resolve(new Map<string, number | null>())
377
380
  ).then((factors) => {
378
381
  if (abortRef.current === controller) setSplitFactors(factors);
379
- }, () => {});
382
+ }, () => {
383
+ // Intentional ignore: split lookups reject only on cancellation; individual failures remain unconfirmed.
384
+ });
380
385
  })
381
386
  .catch((loadError) => {
382
387
  if (abortRef.current !== controller) return;
@@ -2,7 +2,9 @@ import { formatPriceEarnings } from "../../../../utils/price-earnings";
2
2
  import { convertMarketCapitalization } from "../../../../utils/market-capitalization";
3
3
  import { priceColor } from "../../../../theme/colors";
4
4
  import type { CompanyProfile, NextEarnings, Quote, TickerFinancials } from "../../../../types/financials";
5
- import type { TickerPosition, TickerRecord } from "../../../../types/ticker";
5
+ import type { Portfolio, TickerPosition, TickerRecord } from "../../../../types/ticker";
6
+ import type { BrokerInstanceConfig } from "../../../../types/config";
7
+ import type { BrokerAccount } from "../../../../types/trading";
6
8
  import {
7
9
  formatCompact,
8
10
  formatCompactCurrency,
@@ -65,15 +67,45 @@ function daysAhead(days: number, prefix?: string): string[] {
65
67
  return days === 0 ? [`${lead}today`] : [`${lead}in ${days}d`, `${lead}${days}d`];
66
68
  }
67
69
 
68
- function compactPositionAccount(position: TickerPosition): string {
69
- const rawAccount = position.brokerAccountId || position.portfolio;
70
- const isBrokerPortfolio = rawAccount.startsWith("broker:");
71
- const account = isBrokerPortfolio
72
- ? rawAccount.split(":").filter(Boolean).at(-1) || rawAccount
73
- : rawAccount;
74
- const prefix = !isBrokerPortfolio && position.broker && position.broker !== "manual" ? `${position.broker} ` : "";
70
+ /** What the Account column reads names from: the portfolios, the brokers' accounts and their profiles. */
71
+ export interface PositionAccountNames {
72
+ portfolios: readonly Pick<Portfolio, "id" | "name" | "brokerAccountId" | "brokerInstanceId">[];
73
+ /** Accounts each broker profile reported, by profile id. */
74
+ brokerAccounts?: Readonly<Record<string, readonly Pick<BrokerAccount, "accountId" | "name">[]>>;
75
+ brokerInstances?: readonly Pick<BrokerInstanceConfig, "id" | "label">[];
76
+ }
77
+
78
+ const NO_ACCOUNT_NAMES: PositionAccountNames = { portfolios: [] };
79
+
80
+ /**
81
+ * The account a position is held in, by the name the Portfolio pane gives its
82
+ * portfolio ("Aries individual (simulation)"), never the broker's raw account
83
+ * id: a portfolio still named by its id takes the name the broker reports for
84
+ * the account, then the broker profile's.
85
+ */
86
+ function positionAccountName(position: TickerPosition, names: PositionAccountNames): string {
87
+ const portfolio = names.portfolios.find((entry) => entry.id === position.portfolio);
88
+ const brokerPortfolio = position.portfolio.startsWith("broker:");
89
+ const accountId = position.brokerAccountId || portfolio?.brokerAccountId
90
+ || (brokerPortfolio ? position.portfolio.split(":").slice(2).join(":") : "");
91
+ const named = (name: string | undefined) => {
92
+ const text = name?.trim();
93
+ return text && text !== accountId && text !== position.portfolio ? text : null;
94
+ };
95
+ const portfolioName = named(portfolio?.name);
96
+ if (portfolioName) return portfolioName;
97
+ const instanceId = position.brokerInstanceId || portfolio?.brokerInstanceId;
98
+ if (!accountId && !brokerPortfolio && !instanceId) return portfolio?.name.trim() || position.portfolio;
99
+ const accounts = instanceId ? names.brokerAccounts?.[instanceId] ?? [] : [];
100
+ const accountName = named(accounts.find((account) => account.accountId === accountId)?.name);
101
+ if (accountName) return accountName;
102
+ const profileName = named(names.brokerInstances?.find((instance) => instance.id === instanceId)?.label);
103
+ return profileName ?? position.broker;
104
+ }
105
+
106
+ function positionAccountLabel(position: TickerPosition, names: PositionAccountNames): string {
75
107
  const suffix = signedPositionDirection(position) < 0 ? " SHORT" : "";
76
- return `${prefix}${account}${suffix}`;
108
+ return `${positionAccountName(position, names)}${suffix}`;
77
109
  }
78
110
 
79
111
  export function buildOverviewStats({
@@ -294,12 +326,14 @@ export function buildPositionRows({
294
326
  quoteCurrency,
295
327
  baseCurrency,
296
328
  toBase,
329
+ accountNames = NO_ACCOUNT_NAMES,
297
330
  }: {
298
331
  ticker: TickerRecord;
299
332
  quote: Quote | undefined;
300
333
  quoteCurrency: string;
301
334
  baseCurrency: string;
302
335
  toBase: CurrencyConverter;
336
+ accountNames?: PositionAccountNames;
303
337
  }): PositionTableRow[] {
304
338
  return ticker.metadata.positions.filter((position) => position.shares !== 0).map((position) => {
305
339
  const positionCurrency = getPortfolioPositionMetrics(
@@ -328,7 +362,7 @@ export function buildPositionRows({
328
362
  const unit = metrics.priceBasis === "percent-of-par" ? "" : ticker.metadata.assetCategory === "BOND" ? " units" : metrics.multiplierHint > 1 ? " ct" : " sh";
329
363
 
330
364
  return {
331
- account: compactPositionAccount(position),
365
+ account: positionAccountLabel(position, accountNames),
332
366
  qty: `${formatMarketQuantity(metrics.totalShares, { assetCategory: ticker.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis, quantityCurrency: positionCurrency, maxWidth: metrics.priceBasis === "percent-of-par" ? 11 : undefined })}${unit}`,
333
367
  quantityUnit: metrics.priceBasis === "percent-of-par" ? "face" : undefined,
334
368
  // Avg and Mark sit side by side, so both keep the currency's minor unit ($118.40 beside $224.36).
@@ -0,0 +1,64 @@
1
+ import { useMemo } from "react";
2
+ import { useOptionsQuery, useResolvedEntryValue } from "../../../../market-data/hooks";
3
+ import { formatMarketPriceWithCurrency } from "../../../../market-data/market/format";
4
+ import type { OptionContract } from "../../../../types/financials";
5
+ import type { TickerRecord } from "../../../../types/ticker";
6
+ import { formatCompact, formatNumber } from "../../../../utils/format";
7
+ import { expiryIsoDate, formatDaysToExpiry } from "../../../../utils/option-expiry";
8
+ import { resolveOptionsTarget, type ResolvedOptionsTarget } from "../../../../utils/options";
9
+ import type { StatField } from "./types";
10
+
11
+ const contractKey = (symbol: string) => symbol.replace(/\s+/g, "").toUpperCase();
12
+
13
+ /**
14
+ * An option contract's own row of its chain, the same query OMON reads: it
15
+ * carries the open interest and implied volatility a contract quote leaves out.
16
+ * Null for anything that is not an option, and until the chain answers.
17
+ */
18
+ export function useOptionContractRow(ticker: TickerRecord | null | undefined): OptionContract | null {
19
+ const target = resolveOptionsTarget(ticker);
20
+ const option = target?.isOptionTicker && target.parsedOption ? target : null;
21
+ const entry = useOptionsQuery(option ? {
22
+ instrument: {
23
+ symbol: option.effectiveTicker, exchange: option.effectiveExchange,
24
+ brokerId: option.instrument?.brokerId, brokerInstanceId: option.instrument?.brokerInstanceId, instrument: option.instrument,
25
+ },
26
+ expirationDate: option.parsedOption!.expTs,
27
+ } : null);
28
+ const chain = useResolvedEntryValue(entry);
29
+ const symbol = ticker?.metadata.ticker;
30
+ return useMemo(() => {
31
+ if (!chain || !symbol || !option) return null;
32
+ const key = contractKey(symbol);
33
+ return [...chain.calls, ...chain.puts].find((contract) => contractKey(contract.contractSymbol) === key) ?? null;
34
+ }, [chain, option !== null, symbol]);
35
+ }
36
+
37
+ /**
38
+ * What a contract is, then what its market says about it: underlying, side,
39
+ * strike, expiry and deliverable, with the open interest, implied volatility
40
+ * and volume the broker's chain gives. A figure the broker leaves out is left out.
41
+ */
42
+ export function optionContractFields(
43
+ target: Pick<ResolvedOptionsTarget, "parsedOption" | "effectiveTicker" | "instrument"> | null,
44
+ row: OptionContract | null,
45
+ currency: string,
46
+ now = Date.now(),
47
+ ): StatField[] {
48
+ const parsed = target?.parsedOption;
49
+ if (!target || !parsed) return [];
50
+ const fields: StatField[] = [
51
+ { label: "Underlying", value: target.effectiveTicker },
52
+ { label: "Type", value: parsed.side === "C" ? "Call" : "Put" },
53
+ { label: "Strike", value: formatMarketPriceWithCurrency(parsed.strike, currency) },
54
+ { label: "Expiry", value: expiryIsoDate(parsed.expTs), detail: formatDaysToExpiry(parsed.expTs, now) },
55
+ ];
56
+ const multiplier = Number(target.instrument?.multiplier);
57
+ if (Number.isFinite(multiplier) && multiplier > 0) fields.push({ label: "Multiplier", value: formatNumber(multiplier, 0) });
58
+ if (row?.openInterest != null && Number.isFinite(row.openInterest)) fields.push({ label: "Open Int", value: formatCompact(row.openInterest) });
59
+ if (Number.isFinite(row?.impliedVolatility) && row!.impliedVolatility > 0) {
60
+ fields.push({ label: "Implied Vol", value: `${(row!.impliedVolatility * 100).toFixed(1)}%` });
61
+ }
62
+ if (row?.volume != null && Number.isFinite(row.volume)) fields.push({ label: "Volume", value: formatCompact(row.volume) });
63
+ return fields;
64
+ }
@@ -3,7 +3,9 @@ import { fundFactOverviewFields } from "../sec/fund-facts";
3
3
  import { useFundFacts } from "../sec/fund-facts-view";
4
4
  import { resolveTickerInstrumentKind } from "../../../tickers/instrument-kind";
5
5
  import { mayBeUsEquityOrFundTicker } from "../../../utils/sec";
6
- import type { ReactNode } from "react";
6
+ import { ResearchTradeActions, researchTradeMatches, useResearchTrading } from "../shared/research-trading";
7
+ import { instrumentFromTicker } from "../../../market-data/request-types";
8
+ import { useMemo, type ReactNode } from "react";
7
9
  import { EmptyState, PaneLinkMenu, SectionHeading, usePaneNoticeFooter } from "../../../components";
8
10
  import { CompositeChart, pricePointsToResolvedSeries } from "../../../components/chart/composite";
9
11
  import { CompanyLogo, resolveCompanyLogoSrc } from "../../../components/company-logo";
@@ -22,7 +24,11 @@ import {
22
24
  quoteSessionLabel,
23
25
  } from "../../../market-data/market/status";
24
26
  import { appendQuoteToPriceReturnHistory, buildPriceReturnFields } from "../../../market-data/performance";
25
- import { fitFuturesContractLines, futuresContractLines, futuresSettleText, isFuturesInstrument, type FuturesContractLine } from "../../../market-data/futures-contract";
27
+ import { changeBasisLabel, changeBasisNote, quoteChangeBasis } from "../../../market-data/market/change-basis";
28
+ import {
29
+ fitFuturesContractLines, futuresContractLines, futuresSettleText, isFuturesInstrument, minorUnitPricePer,
30
+ type FuturesContractLine,
31
+ } from "../../../market-data/futures-contract";
26
32
  import { useViewport } from "../../../react/input";
27
33
  import { useAppSelector } from "../../../state/app/context";
28
34
  import { colors, priceColor } from "../../../theme/colors";
@@ -47,6 +53,8 @@ import {
47
53
  } from "./overview/components";
48
54
  import { useFuturesContractTerms } from "./overview/futures-contract";
49
55
  import type { OverviewDividendState } from "./overview/dividend";
56
+ import { optionContractFields, useOptionContractRow } from "./overview/option-contract";
57
+ import { resolveOptionsTarget } from "../../../utils/options";
50
58
  import { buildOverviewStats, buildPositionRows, buildProfileFields } from "./overview/model";
51
59
  import type { OverviewFunctionLink, StatField } from "./overview/types";
52
60
  import { describeFundamentalMarketCap } from "../../../utils/market-capitalization";
@@ -99,7 +107,15 @@ export function OverviewTab(props: OverviewTabProps) {
99
107
  const FUND_FACTS_LINK: OverviewFunctionLink = { name: "Fund Facts", templateId: "sec-etf-pane" };
100
108
 
101
109
  function ResolvedOverviewTab({ width, height, focused = false, ticker, financials, onOpenChart, onOpenFunction, perpetuals, dividend }: OverviewTabProps & { ticker: TickerRecord }) {
110
+ const trade = useResearchTrading();
102
111
  const baseCurrency = useAppSelector((state) => state.config.baseCurrency);
112
+ const portfolios = useAppSelector((state) => state.config.portfolios);
113
+ const brokerInstances = useAppSelector((state) => state.config.brokerInstances);
114
+ const brokerAccounts = useAppSelector((state) => state.brokerAccounts);
115
+ const accountNames = useMemo(
116
+ () => ({ portfolios, brokerInstances, brokerAccounts }),
117
+ [brokerAccounts, brokerInstances, portfolios],
118
+ );
103
119
  const { width: termWidth } = useViewport();
104
120
  const { fractionalViewport = false, nativePaneChrome, cellWidthPx = 8, cellHeightPx = 18, pixelRatio = 1 } = useUiCapabilities();
105
121
  const figureCells = useFigureCells();
@@ -114,6 +130,7 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
114
130
  // A futures symbol's settlement and contract terms load beside the quote, never in its way.
115
131
  const futuresSymbol = quote?.symbol || ticker.metadata.ticker;
116
132
  const contractTerms = useFuturesContractTerms(isFuturesInstrument(futuresSymbol, instrumentType) ? futuresSymbol : null);
133
+ const optionRow = useOptionContractRow(ticker);
117
134
  const exchangeRates = useFxRatesMap([
118
135
  baseCurrency,
119
136
  ticker.metadata.currency,
@@ -179,7 +196,9 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
179
196
  ],
180
197
  focused,
181
198
  });
182
- const hasBidAsk = quote?.bid != null || quote?.ask != null;
199
+ // A listing that no longer trades has no book: its quote is its last session's close.
200
+ const delisted = !!quote?.delisting;
201
+ const hasBidAsk = !delisted && (quote?.bid != null || quote?.ask != null);
183
202
  const quoteBookInline = hasBidAsk && contentWidth >= 68;
184
203
  const quoteBookWidth = quoteBookInline ? Math.min(32, Math.max(24, Math.floor(contentWidth * 0.3))) : Math.min(contentWidth, 32);
185
204
  const quoteSummaryWidth = quoteBookInline ? Math.max(20, contentWidth - quoteBookWidth - 2) : contentWidth;
@@ -188,9 +207,16 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
188
207
  // The headline is the regular session, which stops at the close; extended trading is the line below it.
189
208
  const sessionQuote = getRegularSessionDisplay(quote);
190
209
  const extendedQuote = getExtendedSessionDisplay(quote);
191
- const quotePriceText = sessionQuote ? formatMarketPriceWithCurrency(sessionQuote.price, quote?.currency, moneyOptions) : "";
192
- const quoteChangeText = sessionQuote ? formatSignedMarketPrice(sessionQuote.change, moneyOptions) : "";
193
- const quotePercentText = sessionQuote ? `(${formatPercentRaw(sessionQuote.changePercent)})` : "";
210
+ // A price in cents says what it is per (314.95¢/lb), and the move what it is measured from (vs Fri 16 Oct close).
211
+ const quotePriceText = sessionQuote
212
+ ? `${formatMarketPriceWithCurrency(sessionQuote.price, quote?.currency, moneyOptions)}${minorUnitPricePer(contractTerms, quote?.currency)}` : "";
213
+ // Nor a move: the close it ended on is where it stays.
214
+ const quoteChangeText = sessionQuote && !delisted ? formatSignedMarketPrice(sessionQuote.change, moneyOptions) : "";
215
+ const quotePercentText = sessionQuote && !delisted ? `(${formatPercentRaw(sessionQuote.changePercent)})` : "";
216
+ const changeBasis = delisted ? null : quoteChangeBasis(quote, { terms: contractTerms });
217
+ const changeBasisText = changeBasis ? changeBasisLabel(changeBasis) : "";
218
+ // The days the venue was closed since that close, under the move: "HKEX closed Mon 19 Oct, holiday".
219
+ const changeBasisNoteText = changeBasisNote(changeBasis);
194
220
  // The pane title already names the ticker, so the line leads with the company and drops a name that only repeats it.
195
221
  const companyName = [ticker.metadata.name, quote?.name].find((name) => name && name !== ticker.metadata.ticker) ?? "";
196
222
  const venueText = listingVenue ? (companyName ? ` (${listingVenue})` : listingVenue) : "";
@@ -217,15 +243,25 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
217
243
  ? truncateToDisplayWidth(futuresSettleText(contractTerms, quote),
218
244
  Math.max(1, quoteTextWidth - displayWidth(settleLabel) - 1))
219
245
  : "";
220
- const quoteChangeCells = figureCells(quoteChangeText, "sub") + 1 + figureCells(quotePercentText, "sub");
246
+ const quoteChangeCells = delisted ? 0 : figureCells(quoteChangeText, "sub") + 1 + figureCells(quotePercentText, "sub");
221
247
  const stackQuoteChange = quoteChangeCells > quoteTextWidth;
222
248
  const stackQuoteSummary = figureCells(quotePriceText) + 2 + quoteChangeCells > quoteTextWidth;
249
+ // What the move is from follows it on its line when there is room; otherwise it takes the line
250
+ // under the headline, so a narrow pane never splits the move itself to fit the words.
251
+ const changeBasisInline = !!changeBasisText && (stackQuoteSummary ? 0 : figureCells(quotePriceText) + 2)
252
+ + quoteChangeCells + 1 + displayWidth(changeBasisText) <= quoteTextWidth;
223
253
  const companyNameWidth = Math.max(8, quoteTextWidth
224
254
  - displayWidth(venueText)
225
255
  - (marketStateText ? displayWidth(marketStateText) + 1 : 0));
256
+ // A listing that no longer trades keeps its delisting date whole on a narrow pane: the venue gives way, then the name.
257
+ const delistedRoom = quote?.delisting && marketStateText ? quoteTextWidth - displayWidth(marketStateText) - 1 : null;
258
+ const identityVenue = delistedRoom !== null && delistedRoom - displayWidth(venueText) < 8 ? "" : venueText;
259
+ const identityNameWidth = delistedRoom !== null ? delistedRoom - displayWidth(identityVenue) : companyNameWidth;
260
+ const identityName = identityNameWidth >= 4 ? companyName : "";
226
261
  const hasDayRange = quote?.low != null && quote?.high != null && quote.high > quote.low;
227
262
  const yearRange = liveFiftyTwoWeekRange(quote);
228
- const dayRangeLabel = t("Day Range");
263
+ // A listing that no longer trades: its range is its last session's, years ago perhaps, never today's.
264
+ const dayRangeLabel = delisted ? t("Last Session") : t("Day Range");
229
265
  const yearRangeLabel = t("52W Range");
230
266
  const rangeLabelWidth = Math.max(
231
267
  hasDayRange ? displayWidth(dayRangeLabel) : 0,
@@ -243,7 +279,12 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
243
279
  const contractLines = contractTerms ? futuresContractLines(contractTerms, futuresSymbol) : [];
244
280
  // Columns that keep every term whole; a narrow pane stacks them in one.
245
281
  const contractColumns = textGridColumns(contractLines.map(contractField), contentWidth);
246
- const contractFields = fitFuturesContractLines(contractLines, contractColumns * contractRowBudget(height)).map(contractField);
282
+ const futuresFields = fitFuturesContractLines(contractLines, contractColumns * contractRowBudget(height)).map(contractField);
283
+ // An option contract states its own terms, and the chain's open interest and IV, where a stock has a profile.
284
+ const optionTarget = resolveOptionsTarget(ticker);
285
+ const optionFields = optionTarget?.isOptionTicker ? optionContractFields(optionTarget, optionRow, quoteCurrency) : [];
286
+ const contractFields = futuresFields.length > 0 ? futuresFields : optionFields;
287
+ const contractGridColumns = futuresFields.length > 0 ? contractColumns : textGridColumns(optionFields, contentWidth);
247
288
  const stats = buildOverviewStats({
248
289
  quote,
249
290
  fundamentals,
@@ -260,7 +301,8 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
260
301
  bond: fundFacts?.bond ?? null,
261
302
  });
262
303
  const profileFields = buildProfileFields({
263
- instrumentType: instrumentTypeLabel(instrumentType, quote?.name || ticker.metadata.name),
304
+ // The contract section names an option's type.
305
+ instrumentType: optionFields.length > 0 ? undefined : instrumentTypeLabel(instrumentType, quote?.name || ticker.metadata.name),
264
306
  sector,
265
307
  industry,
266
308
  profile,
@@ -276,6 +318,7 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
276
318
  quoteCurrency,
277
319
  baseCurrency,
278
320
  toBase,
321
+ accountNames,
279
322
  });
280
323
 
281
324
  return (
@@ -315,13 +358,13 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
315
358
  <Box flexDirection="column" flexGrow={1} flexShrink={1} minWidth={0}>
316
359
  {hasIdentityLine && (
317
360
  <Box flexDirection="row" minWidth={0} overflow="hidden">
318
- {companyName && (
319
- <ClippedText fg={colors.textDim} text={companyName} width={companyNameWidth} />
361
+ {identityName && (
362
+ <ClippedText fg={colors.textDim} text={identityName} width={identityNameWidth} />
320
363
  )}
321
- {venueText && <Text flexShrink={0} fg={colors.textDim}>{venueText}</Text>}
322
- {quote?.marketState && (
364
+ {identityVenue && <Text flexShrink={0} fg={colors.textDim}>{identityVenue}</Text>}
365
+ {marketStateText && (
323
366
  <Text flexShrink={0} fg={quoteSessionColor(quote)}>
324
- {companyName || venueText ? " " : ""}{marketStateText}
367
+ {identityName || identityVenue ? " " : ""}{marketStateText}
325
368
  </Text>
326
369
  )}
327
370
  </Box>
@@ -334,12 +377,26 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
334
377
  alignItems={stackQuoteSummary ? undefined : "baseline"}
335
378
  >
336
379
  <FigureText>{quotePriceText}</FigureText>
337
- <Box flexDirection={stackQuoteChange ? "column" : "row"} gap={stackQuoteChange ? 0 : 1}>
338
- <FigureText part="sub" change={sessionQuote.change}>{quoteChangeText}</FigureText>
339
- <FigureText part="sub" change={sessionQuote.change}>{quotePercentText}</FigureText>
340
- </Box>
380
+ {!delisted && (
381
+ <Box flexDirection={stackQuoteChange ? "column" : "row"} gap={stackQuoteChange ? 0 : 1}
382
+ alignItems={changeBasisInline ? "baseline" : undefined}>
383
+ <FigureText part="sub" change={sessionQuote.change}>{quoteChangeText}</FigureText>
384
+ <FigureText part="sub" change={sessionQuote.change}>{quotePercentText}</FigureText>
385
+ {changeBasisInline ? <Text flexShrink={0} fg={colors.textDim}>{changeBasisText}</Text> : null}
386
+ </Box>
387
+ )}
341
388
  </Box>
342
389
  )}
390
+ {sessionQuote && changeBasisText && !changeBasisInline ? (
391
+ <Box minWidth={0} overflow="hidden">
392
+ <ClippedText fg={colors.textDim} text={changeBasisText} width={quoteTextWidth} />
393
+ </Box>
394
+ ) : null}
395
+ {sessionQuote && changeBasisNoteText ? (
396
+ <Box minWidth={0} overflow="hidden">
397
+ <ClippedText fg={colors.textDim} text={changeBasisNoteText} width={quoteTextWidth} />
398
+ </Box>
399
+ ) : null}
343
400
  {extendedQuote && (
344
401
  <Box flexDirection="row" gap={2}>
345
402
  <Text fg={colors.textDim}>{extendedSessionLineLabel(extendedQuote, t)}:</Text>
@@ -384,7 +441,7 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
384
441
  low={quote.low!}
385
442
  high={quote.high!}
386
443
  label={dayRangeLabel}
387
- shortLabel={t("Day")}
444
+ shortLabel={delisted ? t("Last") : t("Day")}
388
445
  labelWidth={rangeLabelWidth}
389
446
  endpointWidth={rangeEndpointCells}
390
447
  width={rangeWidth}
@@ -414,10 +471,11 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
414
471
  {contractFields.length > 0 && (
415
472
  <Box flexDirection="column">
416
473
  <SectionHeading title={t("Contract")} />
417
- <FundamentalsGrid fields={contractFields} width={contentWidth} columns={contractColumns} />
474
+ <FundamentalsGrid fields={contractFields} width={contentWidth} columns={contractGridColumns} />
418
475
  </Box>
419
476
  )}
420
477
 
478
+ <ResearchTradeActions focused={focused} />
421
479
  {hasHistory && (
422
480
  <Box
423
481
  onMouseDown={onOpenChart
@@ -437,6 +495,7 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
437
495
  interactive={false}
438
496
  series={[priceSeries]}
439
497
  panels={[{ id: "price" }]}
498
+ levels={trade && researchTradeMatches(trade, instrumentFromTicker(ticker)) ? { seriesId: priceSeries.id, items: trade.levels } : undefined}
440
499
  axisWidth={8}
441
500
  showLegend={false}
442
501
  />
@@ -475,6 +534,8 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
475
534
  </Box>
476
535
  )}
477
536
 
537
+ {trade?.orders?.(contentWidth)}
538
+
478
539
  {profileFields.length > 0 && (
479
540
  <Box flexDirection="column">
480
541
  <SectionHeading title={t("Profile")} />