gloomberb 0.18.1 → 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 (307) 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/types.ts +5 -0
  8. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  9. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  10. package/src/app/runtime/plugin-bindings.ts +6 -2
  11. package/src/brokers/logos.ts +66 -0
  12. package/src/brokers/profile-form.ts +3 -0
  13. package/src/brokers/remote-broker-adapter.ts +13 -3
  14. package/src/capabilities/index.ts +1 -1
  15. package/src/capabilities/service-capabilities.ts +7 -1
  16. package/src/cli/base-converter.ts +60 -18
  17. package/src/cli/commands/market.ts +281 -154
  18. package/src/cli/commands/news-search.ts +35 -8
  19. package/src/cli/commands/system.ts +35 -1
  20. package/src/cli/commands/ticker.ts +100 -50
  21. package/src/cli/currency-hints.ts +27 -0
  22. package/src/cli/fx-pair.ts +41 -4
  23. package/src/cli/glossary.ts +57 -4
  24. package/src/cli/helpers.ts +36 -3
  25. package/src/cli/index.ts +4 -1
  26. package/src/cli/known-gaps.ts +37 -1
  27. package/src/cli/listing-arg.ts +37 -2
  28. package/src/cli/pane-functions/catalog.ts +3 -1
  29. package/src/cli/pane-functions/discovery.ts +15 -1
  30. package/src/cli/pane-functions/dom.ts +9 -4
  31. package/src/cli/pane-functions/freshness.ts +4 -4
  32. package/src/cli/pane-functions/function-help.ts +27 -3
  33. package/src/cli/pane-functions/headless.ts +6 -5
  34. package/src/cli/pane-functions/index.ts +3 -2
  35. package/src/cli/pane-functions/lock-note.ts +7 -4
  36. package/src/cli/pane-functions/resolver.ts +2 -1
  37. package/src/cli/quote-answer.ts +179 -0
  38. package/src/cli/series-search.ts +2 -1
  39. package/src/cli/types.ts +4 -0
  40. package/src/components/chart/composite/composite-chart.tsx +11 -2
  41. package/src/components/chart/composite/format.ts +13 -1
  42. package/src/components/chart/composite/level-labels.ts +30 -0
  43. package/src/components/chart/composite/levels.ts +17 -6
  44. package/src/components/chart/composite/panel-surface.tsx +58 -8
  45. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  46. package/src/components/index.ts +2 -0
  47. package/src/components/layout/pane/content.tsx +16 -14
  48. package/src/components/layout/pane/footer/index.tsx +22 -3
  49. package/src/components/layout/pane/footer/model.ts +99 -4
  50. package/src/components/layout/pane/header-tabs.tsx +15 -1
  51. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  52. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  53. package/src/components/onboarding/wizard-model.ts +10 -3
  54. package/src/components/ui/button.tsx +11 -4
  55. package/src/components/ui/choice-dialog.tsx +8 -5
  56. package/src/components/ui/display.tsx +11 -6
  57. package/src/components/ui/fields.tsx +100 -20
  58. package/src/components/ui/icon.tsx +6 -1
  59. package/src/components/ui/list-row-lines.tsx +87 -0
  60. package/src/components/ui/list-view.tsx +14 -0
  61. package/src/components/ui/logo-mark.tsx +64 -0
  62. package/src/components/ui/number-step.ts +54 -0
  63. package/src/components/ui/select-button.tsx +5 -4
  64. package/src/components/ui/select-field.tsx +3 -3
  65. package/src/components/ui/status.tsx +17 -3
  66. package/src/components/ui/tabs.tsx +9 -1
  67. package/src/components/ui/toggle.tsx +17 -3
  68. package/src/core/state/app/instrument.ts +8 -3
  69. package/src/data/config/store/normalize.ts +4 -1
  70. package/src/data/plugin-cache.ts +32 -6
  71. package/src/docs/snapshot.json +5 -5
  72. package/src/i18n/es.ts +9 -3
  73. package/src/i18n/ja.ts +9 -3
  74. package/src/i18n/ko.ts +9 -3
  75. package/src/i18n/zh-cn.ts +10 -4
  76. package/src/i18n/zh-tw.ts +10 -4
  77. package/src/market-data/commodity-names.ts +80 -0
  78. package/src/market-data/coordinator/auxiliary.ts +3 -1
  79. package/src/market-data/coordinator/chart.ts +3 -1
  80. package/src/market-data/futures-board.ts +38 -1
  81. package/src/market-data/futures-contract.ts +91 -0
  82. package/src/market-data/fx-closes.ts +78 -1
  83. package/src/market-data/market/change-basis.ts +219 -0
  84. package/src/market-data/market/format.ts +28 -12
  85. package/src/market-data/market/freshness.ts +38 -0
  86. package/src/market-data/market/status.ts +55 -9
  87. package/src/market-data/market/year-to-date.ts +46 -0
  88. package/src/market-data/quotes/contributions.ts +12 -0
  89. package/src/market-data/quotes/delisting.ts +34 -0
  90. package/src/market-data/quotes/live-valuation.ts +23 -0
  91. package/src/market-data/quotes/resolution.ts +8 -2
  92. package/src/market-data/tax-equivalent.ts +10 -2
  93. package/src/notifications/app-notifier.ts +2 -2
  94. package/src/notifications/notification-log.ts +8 -6
  95. package/src/plugins/builtin/alerts/cli.ts +26 -18
  96. package/src/plugins/builtin/alerts/headless.ts +1 -1
  97. package/src/plugins/builtin/alerts/index.tsx +13 -4
  98. package/src/plugins/builtin/attention/trending.tsx +8 -6
  99. package/src/plugins/builtin/backtest/client.ts +5 -2
  100. package/src/plugins/builtin/backtest/window.ts +16 -3
  101. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  102. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  103. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  104. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  105. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  106. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  107. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  108. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  109. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  110. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  111. package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
  112. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  113. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  114. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  115. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  116. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  117. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  118. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  119. package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
  120. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  121. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  122. package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
  123. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  124. package/src/plugins/builtin/buildout/format.ts +1 -0
  125. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  126. package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
  127. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  128. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  129. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  130. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  131. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  132. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  133. package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
  134. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  135. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  136. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  137. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  138. package/src/plugins/builtin/chat/index.tsx +1 -1
  139. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  140. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  141. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  142. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  143. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  144. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  145. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  146. package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
  147. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  148. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  149. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  150. package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
  151. package/src/plugins/builtin/composite-plugins.ts +3 -2
  152. package/src/plugins/builtin/cot/headless.ts +1 -1
  153. package/src/plugins/builtin/cot/index.tsx +1 -1
  154. package/src/plugins/builtin/cot/model.ts +35 -12
  155. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  156. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  157. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  158. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  159. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  160. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  161. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  162. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  163. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  164. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  165. package/src/plugins/builtin/econ/calendar-model.ts +35 -0
  166. package/src/plugins/builtin/econ/index.tsx +31 -4
  167. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  168. package/src/plugins/builtin/equity-screener/headless.ts +20 -5
  169. package/src/plugins/builtin/equity-screener/model.ts +40 -2
  170. package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
  171. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  172. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  173. package/src/plugins/builtin/exposure/client.ts +4 -1
  174. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  175. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  176. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  177. package/src/plugins/builtin/exposure/pane.tsx +37 -16
  178. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  179. package/src/plugins/builtin/futures/table.tsx +6 -3
  180. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  181. package/src/plugins/builtin/insider/base-values.ts +72 -0
  182. package/src/plugins/builtin/insider/headless.ts +29 -4
  183. package/src/plugins/builtin/jobs/headless.ts +2 -2
  184. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  185. package/src/plugins/builtin/market-halts/model.ts +31 -6
  186. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  187. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  188. package/src/plugins/builtin/members/pane.tsx +8 -3
  189. package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
  190. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  191. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  192. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  193. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  194. package/src/plugins/builtin/options/footer.ts +25 -20
  195. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  196. package/src/plugins/builtin/options/view.tsx +77 -22
  197. package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
  198. package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
  199. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  200. package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
  201. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  202. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  203. package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
  204. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  205. package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
  206. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  207. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  208. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  209. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  210. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  211. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  212. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  213. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  214. package/src/plugins/builtin/scanner/feed.ts +6 -1
  215. package/src/plugins/builtin/scanner/headless.ts +1 -1
  216. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  217. package/src/plugins/builtin/series-inventory.ts +1 -0
  218. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  219. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  220. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  221. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  222. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  223. package/src/plugins/builtin/shared/research-trading.tsx +62 -0
  224. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  225. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  226. package/src/plugins/builtin/themes/headless.ts +8 -2
  227. package/src/plugins/builtin/themes/model.ts +43 -10
  228. package/src/plugins/builtin/themes/pane.tsx +8 -6
  229. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  230. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  231. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  232. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  233. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  234. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  235. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  236. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  237. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  238. package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
  239. package/src/plugins/builtin-editorial.ts +1 -1
  240. package/src/plugins/compat.ts +3 -0
  241. package/src/plugins/external-runtime.ts +5 -0
  242. package/src/plugins/loader.ts +6 -0
  243. package/src/plugins/plugin-icon.ts +61 -0
  244. package/src/plugins/registry/host-actions.ts +2 -1
  245. package/src/plugins/registry/index.ts +9 -1
  246. package/src/plugins/runtime/context.tsx +5 -0
  247. package/src/plugins/runtime/index.tsx +3 -0
  248. package/src/portfolio/column-values.ts +13 -4
  249. package/src/portfolio/cost-fx.ts +5 -28
  250. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  251. package/src/public/broker.ts +8 -0
  252. package/src/public/market-data.ts +5 -0
  253. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  254. package/src/renderers/dom/desktop/controls.tsx +39 -7
  255. package/src/renderers/dom/desktop/icons.tsx +14 -0
  256. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  257. package/src/renderers/dom/desktop/menu.tsx +10 -1
  258. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  259. package/src/renderers/dom/host/style.ts +6 -1
  260. package/src/renderers/dom/host/tabs.tsx +6 -0
  261. package/src/renderers/dom/styles.css +12 -5
  262. package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
  263. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  264. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  265. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  266. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  267. package/src/series/validate.ts +2 -2
  268. package/src/sources/gloomberb-cloud/index.ts +50 -17
  269. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  270. package/src/sources/history-result.ts +3 -0
  271. package/src/sources/provider-errors.ts +3 -1
  272. package/src/sources/provider-router/history.ts +49 -6
  273. package/src/sources/provider-router/index.ts +2 -1
  274. package/src/state/app/activity.ts +13 -0
  275. package/src/sync/controller.ts +4 -0
  276. package/src/sync/core-contributors.ts +62 -11
  277. package/src/sync/local-only.ts +121 -0
  278. package/src/sync/react.ts +6 -0
  279. package/src/sync/types.ts +3 -0
  280. package/src/tickers/listing-fate.ts +27 -8
  281. package/src/tickers/search/index.ts +24 -3
  282. package/src/time-series/price-comparison.ts +35 -0
  283. package/src/time-series/resolve.ts +52 -11
  284. package/src/time-series/spec.ts +1 -1
  285. package/src/time-series/types.ts +5 -0
  286. package/src/types/broker.ts +56 -1
  287. package/src/types/config.ts +6 -0
  288. package/src/types/data-provider.ts +5 -1
  289. package/src/types/financials.ts +23 -0
  290. package/src/types/headless.ts +12 -2
  291. package/src/types/instrument.ts +4 -0
  292. package/src/types/plugin.ts +24 -6
  293. package/src/types/price-history.ts +6 -1
  294. package/src/types/trading.ts +67 -0
  295. package/src/ui/host.tsx +4 -0
  296. package/src/utils/base-currency.ts +108 -0
  297. package/src/utils/currency-names.ts +128 -0
  298. package/src/utils/currency-pair.ts +2 -1
  299. package/src/utils/currency-units.ts +32 -1
  300. package/src/utils/dom-screenshot.ts +22 -9
  301. package/src/utils/exchange-notation.generated.ts +360 -0
  302. package/src/utils/exchanges.ts +15 -322
  303. package/src/utils/format.ts +31 -1
  304. package/src/utils/instrument-identity.ts +38 -1
  305. package/src/utils/options.ts +46 -1
  306. package/src/utils/price-history.ts +4 -0
  307. package/src/utils/reported-money.ts +8 -4
@@ -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")} />
@@ -39,6 +39,7 @@ import { ResearchTabKeysProvider, useResearchTabKeysHost } from "./research-tab-
39
39
  import { ResearchTabNavigationProvider } from "./research-tab-navigation";
40
40
  import { usePluginAppActions } from "../../../public/react";
41
41
  import { resolveTickerInstrumentKind } from "../../../tickers/instrument-kind";
42
+ import { ResearchTradeWorkspace } from "../broker-trading/dock";
42
43
 
43
44
  const TICKER_RESEARCH_TAB_COMMIT_DELAY_MS = 120;
44
45
  /** A tab counts as viewed once it stays open this long, not when h/l passes over it. */
@@ -207,7 +208,9 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
207
208
  const quoteFooterActive = resolvedTabId === "overview" || resolvedTabId === "chart";
208
209
  const cloudAccess = useCloudAccessFooter({
209
210
  delayLabel: tf("{count}m", { count: CLOUD_QUOTE_DELAY_MINUTES }),
210
- degraded: financials?.quote?.priceObservation !== "nav" && financials?.quote?.dataSource !== "live",
211
+ // A listing that no longer trades has no delay to speak of: its quote is its last session's close.
212
+ degraded: !financials?.quote?.delisting && financials?.quote?.priceObservation !== "nav"
213
+ && financials?.quote?.dataSource !== "live",
211
214
  focused: focused && quoteFooterActive,
212
215
  segmentId: "ticker-research-access",
213
216
  shortcutScope: "ticker-research:upgrade",
@@ -359,16 +362,14 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
359
362
  );
360
363
  }
361
364
 
362
- return (
363
- <Box flexDirection="column" flexGrow={1} flexBasis={0} overflow="hidden">
364
- {tabStrip}
365
-
366
- <Box height={contentHeight} flexGrow={1} flexBasis={0} overflow="hidden">
367
- <ResearchTabKeysProvider value={showTabs ? researchTabKeys.value : null}>
365
+ const tradeAvailable = visibleTabIds.has("broker-trade");
366
+ // Trade shows Chart beside its ticket: the workspace names the tab it shows.
367
+ const renderTabs = (contentWidth: number, contentFocused: boolean, shownTabId = resolvedTabId) => (
368
+ <ResearchTabKeysProvider value={showTabs ? researchTabKeys.value : null}>
368
369
  {tickerResearchTabs.map((tab) => {
369
- if (!renderedTabIds.has(tab.id) || !visibleTabIds.has(tab.id)) return null;
370
+ if (tradeAvailable && tab.id === "broker-trade" || !(renderedTabIds.has(tab.id) || tab.id === shownTabId) || !visibleTabIds.has(tab.id)) return null;
370
371
  const TickerResearchTab = tab.component;
371
- const isActive = resolvedTabId === tab.id;
372
+ const isActive = shownTabId === tab.id;
372
373
  return (
373
374
  <Box
374
375
  key={tab.id}
@@ -383,9 +384,9 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
383
384
  <NestedPaneTabs>
384
385
  <ResearchTabNavigationProvider value={showTabs ? openResearchTab : null}>
385
386
  <TickerResearchTab
386
- width={width}
387
+ width={contentWidth}
387
388
  height={contentHeight}
388
- focused={focused && isActive}
389
+ focused={contentFocused && isActive}
389
390
  onCapture={isActive ? handlePluginCapture : ignorePluginCapture}
390
391
  />
391
392
  </ResearchTabNavigationProvider>
@@ -394,8 +395,14 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
394
395
  </Box>
395
396
  );
396
397
  })}
397
- </ResearchTabKeysProvider>
398
- </Box>
399
- </Box>
398
+ </ResearchTabKeysProvider>
400
399
  );
400
+ return <Box flexDirection="column" flexGrow={1} flexBasis={0} overflow="hidden">
401
+ {tabStrip}
402
+ <Box height={contentHeight} flexGrow={1} flexBasis={0} overflow="hidden">
403
+ {tradeAvailable ? <ResearchTradeWorkspace width={width} height={contentHeight} focused={focused} symbol={ticker.metadata.ticker}
404
+ tabId={resolvedTabId} selectTab={selectResearchTab} onCapture={handlePluginCapture} locked={!!paneSettings.hideTabs}>{renderTabs}</ResearchTradeWorkspace>
405
+ : renderTabs(width, focused)}
406
+ </Box>
407
+ </Box>;
401
408
  }
@@ -2,6 +2,7 @@ import type { PaneFooterSegment } from "../../../components/layout/pane/footer";
2
2
  import { t } from "../../../i18n";
3
3
  import { isQuoteStaleForCurrentSession } from "../../../market-data/quotes/freshness";
4
4
  import { formatQuoteNavAsOf } from "../../../market-data/quotes/time";
5
+ import { delistingLabel } from "../../../market-data/quotes/delisting";
5
6
  import type { Quote } from "../../../types/financials";
6
7
  import { displayWidth } from "../../../utils/format";
7
8
 
@@ -12,7 +13,10 @@ export function tickerQuoteFooterInfo(
12
13
  ): PaneFooterSegment[] {
13
14
  const info: PaneFooterSegment[] = [];
14
15
  const navAsOf = formatQuoteNavAsOf(quote);
15
- if (isQuoteStaleForCurrentSession(quote)) {
16
+ if (quote?.delisting) {
17
+ // A listing that no longer trades: its quote is its last trade, said once with its date.
18
+ info.push({ id: "ticker-research-stale", parts: [{ text: delistingLabel(quote.delisting), tone: "warning" }] });
19
+ } else if (isQuoteStaleForCurrentSession(quote)) {
16
20
  const status: PaneFooterSegment = { id: "ticker-research-stale", parts: [{ text: t("Stale quote"), tone: "warning" }] };
17
21
  const timestamp = new Date(quote!.lastUpdated);
18
22
  if (navAsOf) {
@@ -20,6 +20,7 @@ import {
20
20
  } from "../../../../market-data/market/status";
21
21
  import { t } from "../../../../i18n";
22
22
  import { isQuoteStaleForCurrentSession } from "../../../../market-data/quotes/freshness";
23
+ import { delistingLabel, delistingStateLabel } from "../../../../market-data/quotes/delisting";
23
24
  import { formatQuoteNavAsOf } from "../../../../market-data/quotes/time";
24
25
  import { useQuoteFlashDirection } from "../../../../components/quote-flash";
25
26
  import { followLiveSparklinePrice } from "../../../../components/price-sparkline/model";
@@ -89,6 +90,8 @@ interface QuoteStatus {
89
90
  failed: boolean;
90
91
  text: string;
91
92
  stale?: boolean;
93
+ /** DELISTED or INACTIVE: the listing no longer trades, and `text` says since when. */
94
+ ended?: string;
92
95
  }
93
96
 
94
97
  function resolveQuoteStatus(entry: QueryEntry<Quote> | null, symbol: string, quote: Quote | null | undefined): QuoteStatus {
@@ -98,6 +101,7 @@ function resolveQuoteStatus(entry: QueryEntry<Quote> | null, symbol: string, quo
98
101
  ? { failed: true, text: `${symbol} not recognized` }
99
102
  : { failed: true, text: error.message || "Quote unavailable" };
100
103
  }
104
+ if (quote?.delisting) return { failed: true, text: delistingLabel(quote.delisting), stale: true, ended: delistingStateLabel(quote.delisting) };
101
105
  if (isQuoteStaleForCurrentSession(quote)) return { failed: true, text: "Stale quote", stale: true };
102
106
  if (entry?.phase === "ready") return { failed: false, text: "No quote data" };
103
107
  return { failed: false, text: "Loading quote..." };
@@ -175,8 +179,9 @@ export function QuoteMonitorCard({
175
179
  // One decimal count per instrument, so streamed ticks never narrow or widen the price column.
176
180
  const priceOptions = liveQuoteFormatOptions(quote, currency, assetCategory, cachedFinancials?.quoteMetadata?.instrumentType);
177
181
  const priceText = display ? formatMarketPriceWithCurrency(display.price, currency, priceOptions) : "";
178
- const changePercentText = display ? formatPercentRaw(display.changePercent) : "";
179
- const changeValueText = display ? formatSignedMarketPrice(display.change, priceOptions) : "";
182
+ // A listing that no longer trades shows its last close alone: no move, not even a dash for one.
183
+ const changePercentText = display && !quote?.delisting ? formatPercentRaw(display.changePercent) : "";
184
+ const changeValueText = display && !quote?.delisting ? formatSignedMarketPrice(display.change, priceOptions) : "";
180
185
  const priceColumnWidth = Math.max(priceText.length, changePercentText.length + changeValueText.length + 1);
181
186
  const nameMaxWidth = Math.max(10, width - priceColumnWidth - (nativePaneChrome ? 5 : 3));
182
187
  // The terminal sets the line beside the change, under the symbol (or after it in a
@@ -187,6 +192,8 @@ export function QuoteMonitorCard({
187
192
  const extendedParts = display && extended
188
193
  ? extendedLineParts(extended, formatMarketPriceWithCurrency(extended.price, currency, priceOptions), (text) => displayWidth(text) <= extendedRoom)
189
194
  : null;
195
+ // A desktop card says a listing no longer trades on a row of its own.
196
+ const endedRow = nativePaneChrome && quoteFailed && !!quoteStatus.ended && !extendedParts;
190
197
  const extendedColor = quoteFailed ? colors.textDim : priceColor(extended?.change ?? 0);
191
198
  const extendedDim = !!flashDirection && headlineHolds;
192
199
  const sparklineRange = resolvePriceSparklineRange(priceHistory, chartPeriod);
@@ -254,7 +261,7 @@ export function QuoteMonitorCard({
254
261
  <Text attributes={TextAttributes.BOLD} fg={symbolFg} bg={symbolBg} style={desktopSymbolStyle}>
255
262
  {symbol}
256
263
  </Text>
257
- <Text fg={quoteStatus.failed ? colors.negative : colors.textDim}>{quoteStatus.text}</Text>
264
+ <Text fg={quoteStatus.ended ? colors.warning : quoteStatus.failed ? colors.negative : colors.textDim}>{quoteStatus.text}</Text>
258
265
  </Box>
259
266
  ) : nativePaneChrome ? (
260
267
  <Box
@@ -276,7 +283,7 @@ export function QuoteMonitorCard({
276
283
  minWidth={0}
277
284
  style={{
278
285
  gridColumn: "1",
279
- gridRow: extendedParts ? "1" : "1 / span 2",
286
+ gridRow: extendedParts || endedRow ? "1" : "1 / span 2",
280
287
  minWidth: 0,
281
288
  overflow: "hidden",
282
289
  }}
@@ -284,8 +291,8 @@ export function QuoteMonitorCard({
284
291
  <Text attributes={TextAttributes.BOLD} fg={symbolFg} bg={symbolBg} style={desktopSymbolStyle}>
285
292
  {symbol}
286
293
  </Text>
287
- {quoteFailed && (
288
- <Text fg={colors.negative} style={{ fontSize: "var(--gloom-type-ui)", lineHeight: "14px" }}>
294
+ {quoteFailed && !endedRow && (
295
+ <Text fg={quoteStatus.ended ? colors.warning : colors.negative} style={{ fontSize: "var(--gloom-type-ui)", lineHeight: "14px" }}>
289
296
  {quoteStatus.text}
290
297
  </Text>
291
298
  )}
@@ -332,6 +339,28 @@ export function QuoteMonitorCard({
332
339
  </Box>
333
340
  </Box>
334
341
 
342
+ {endedRow && (
343
+ // Under the price, across the card, so the date is never cut on a narrow card;
344
+ // backed like the price so the sparkline does not run through it.
345
+ <Text
346
+ fg={colors.warning}
347
+ style={{
348
+ gridColumn: "1 / -1",
349
+ gridRow: "2",
350
+ justifySelf: "start",
351
+ alignSelf: "start",
352
+ fontSize: "var(--gloom-type-ui)",
353
+ lineHeight: "14px",
354
+ whiteSpace: "nowrap",
355
+ backgroundColor: colors.bg,
356
+ borderRadius: "var(--gloom-radius-sm)",
357
+ boxShadow: `0 0 0 2px ${colors.bg}`,
358
+ }}
359
+ >
360
+ {quoteStatus.text}
361
+ </Text>
362
+ )}
363
+
335
364
  {extendedParts && (
336
365
  <ExtendedSessionLine
337
366
  parts={extendedParts}
@@ -385,7 +414,7 @@ export function QuoteMonitorCard({
385
414
  </Box>
386
415
  <Box flexDirection="column">
387
416
  <FigureText fg={changeColor} dim={flashing} style={figureHalo}>
388
- {compactQuoteFailure ? `${priceText} · ${quoteStatus.stale ? "STALE" : "ERROR"}` : priceText}
417
+ {compactQuoteFailure ? `${priceText} · ${quoteStatus.ended ?? (quoteStatus.stale ? "STALE" : "ERROR")}` : priceText}
389
418
  </FigureText>
390
419
  <Box flexDirection="row" gap={1}>
391
420
  <FigureText part="sub" fg={changeColor} dim={flashing} style={figureHalo}>{changePercentText}</FigureText>
@@ -432,7 +461,7 @@ export function QuoteMonitorCard({
432
461
  )}
433
462
 
434
463
  {quoteFailed && !compactQuoteFailure && (
435
- <Box height={1} overflow="hidden"><Text fg={colors.negative}>{quoteStatus.text}</Text></Box>
464
+ <Box height={1} overflow="hidden"><Text fg={quoteStatus.ended ? colors.warning : colors.negative}>{quoteStatus.text}</Text></Box>
436
465
  )}
437
466
  {navAsOf && !compactNavAsOf && (
438
467
  <Box height={1} overflow="hidden"><Text fg={colors.textDim}>{navAsOf}</Text></Box>
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useMemo, useRef, useState } from "react";
2
2
  import { Box } from "../../../ui";
3
3
  import { usePaneSettingValue, useShortcut } from "../../../public/react";
4
- import { DataTableStackView, KeyValueRow, PaneStatusBody, StatGrid, usePaneFooter, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, usePaneTicker, type DataTableColumn } from "../../../components";
4
+ import { splitDelayedStatus, DataTableStackView, KeyValueRow, PaneStatusBody, StatGrid, usePaneFooter, usePaneNoticeFooter, usePaneStatusFooter, usePaneTabs, usePaneTicker, type DataTableColumn } from "../../../components";
5
5
  import { useThemeColors } from "../../../theme/theme-context";
6
6
  import type { PaneProps } from "../../../types/plugin";
7
7
  import type { TapeQuote, TapeSnapshot, TapeTrade } from "../../../api-client/tape";
@@ -109,7 +109,7 @@ function TimeSalesView({ width, height, focused, symbol, exchange }: PaneProps &
109
109
  usePaneNoticeFooter({ registrationId: "time-sales:notices", focused, notices: [...(data?.gaps ?? []), ...(resource.transport ? [resource.transport] : []),
110
110
  ...(pinnedBeforeTape ? [`The pinned moment is before this tape's first ${tab === "quotes" ? "quote" : "print"}, ${tapeStampNewYork(rowTime(rows.at(-1)!))}.`] : [])] });
111
111
  usePaneStatusFooter({ registrationId: "time-sales:status", loading: resource.loading, error: resource.error,
112
- info: data ? [{ id: "feed", quoteStatus: true, parts: [{ text: `${data.feed === "sip" ? "real-time" : "15m delayed"} · ${tapeStampNewYork(data.asOf)}`, tone: "muted" }] },
112
+ info: data ? [...splitDelayedStatus({ id: "feed", quoteStatus: true, parts: [{ text: `${data.feed === "sip" ? "real-time" : "15m delayed"} · ${tapeStampNewYork(data.asOf)}`, tone: "muted" }] }),
113
113
  ...(frozen ? [{ id: "paused", parts: [{ text: "paused", tone: "warning" as const }] }] : []),
114
114
  ...(!data.connected || resource.snapshotOnly ? [{ id: "snapshot", parts: [{ text: "snapshot", tone: "warning" as const }] }] : [])] : [] });
115
115
  const selectTab = (value: string) => { setTab(value); setDetail(null); setSelected(null); };