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
@@ -148,7 +148,8 @@ export async function applyListingArgument(
148
148
  // A list entry with spaces is a company's name search finds, or the symbols it holds.
149
149
  const typed = takesOneTicker ? [args.arg] : await readNameEntries(splitSymbolArgs([args.arg]), context.dataProvider);
150
150
  if (typed.length === 0) return args;
151
- let listings = await Promise.all(typed.map((raw) => resolveCliListing(raw, exchange, context, { ownExchangeWins: typed.length > 1 })));
151
+ // RY.TO is the listing its quote names, RY:TSX, as `quote` reads it: a function's data is read by venue.
152
+ let listings = await Promise.all(typed.map((raw) => resolveCliListing(raw, exchange, context, { ownExchangeWins: typed.length > 1, venueSuffix: true })));
152
153
  // A separator stuck to the end of a pasted list (`UPST;`) is not part of a symbol; it is named.
153
154
  const notes: string[] = takesOneTicker ? [] : stripStrayEdges(args.arg).notes;
154
155
  const token = found.template?.shortcut?.prefix ?? args.target.toUpperCase();
@@ -0,0 +1,179 @@
1
+ import type { QuoteBatchResult } from "../types/data-provider";
2
+ import type { Quote } from "../types/financials";
3
+ import { bareListingNote } from "../tickers/listing-fate";
4
+ import { notATickerSymbol } from "../sources/provider-errors";
5
+ import { looksLikeCurrencyPair } from "../utils/currency-pair";
6
+ import { canonicalExchange } from "../utils/exchanges";
7
+ import type { CliCommandDef } from "../types/plugin";
8
+ import { currencyCodeNote } from "./currency-hints";
9
+ import { describeSuggestion, suggestListing, suggestUsTicker, type ListingSuggestion } from "./listing-suggestion";
10
+ import { indexRootAlsoLine, indexRootFor, indexRootTryLine } from "./index-roots";
11
+ import { treasuryYieldLine, treasuryYieldTenor } from "./treasury-yields";
12
+ import { knownGapForTicker } from "./known-gaps";
13
+ import {
14
+ futuresTryLine,
15
+ isNoProviderError,
16
+ listingChoiceNotes,
17
+ listingReadNotes,
18
+ loadBareVenues,
19
+ notTradedMessage,
20
+ quoteBareListing,
21
+ requireCliListing,
22
+ tradingBoardNote,
23
+ type CliListing,
24
+ type ListingDataRequest,
25
+ type ListingVenue,
26
+ } from "./listing-arg";
27
+ import { withListingSuggestion } from "./not-a-ticker";
28
+ import type { MarketContext } from "./types";
29
+
30
+ /** The text of an error, or null for none. */
31
+ export function errorMessage(error: unknown): string | null {
32
+ if (error == null) return null;
33
+ return error instanceof Error ? error.message : String(error);
34
+ }
35
+
36
+ /**
37
+ * "APPL is not a US ticker: it is Aptus Pharma Limited on BSE (INR). Did you
38
+ * mean AAPL (Apple Inc.)?", for a symbol read on a listing abroad that is one
39
+ * slip from a US ticker, worded as the not-a-ticker answers word it.
40
+ */
41
+ export function notUsTickerSentence({ target, quote, listing, usSuggestion }: { target: { symbol: string }; quote: Quote | null; listing?: { name: string | null }; usSuggestion?: ListingSuggestion }): string | null {
42
+ if (!quote || !usSuggestion) return null;
43
+ const venue = canonicalExchange(quote.listingExchangeName || quote.exchangeName);
44
+ const name = quote.name || listing?.name;
45
+ const what = `${name ? `${name} on ` : ""}${venue}${quote.currency ? ` (${quote.currency})` : ""}`;
46
+ return `${target.symbol} is not a US ticker: it is ${what}. Did you mean ${describeSuggestion(usSuggestion)}?`;
47
+ }
48
+
49
+ export interface QuoteAnswer {
50
+ result: QuoteBatchResult;
51
+ /** The listing the row is: the one asked for, or the venue a bare symbol was quoted on. */
52
+ listing: CliListing;
53
+ /** Why there is no quote, when there is more to say than the source's own error. */
54
+ failure?: { message: string; details?: string; oneLine: string };
55
+ notes: string[];
56
+ }
57
+
58
+ /**
59
+ * One symbol's row of `quote` and `compare`. A listing on an exchange its symbol
60
+ * is not listed on says so. A bare symbol the data service read on its home
61
+ * listing abroad names that venue (SXR8 -> XETRA). One no source quotes is
62
+ * quoted on its listings (`quoteBareListing`), else names them, or asks for an
63
+ * exchange when several companies list it (2222); a bare index root (VIX) also
64
+ * names its index (^VIX), and a futures board code a security keeps (GC) its
65
+ * future (GC=F). Venues are only looked up for a
66
+ * symbol with no quote, so a request that succeeds costs no search.
67
+ */
68
+ export async function answerQuote(
69
+ result: QuoteBatchResult,
70
+ listing: CliListing,
71
+ market: MarketContext,
72
+ request: ListingDataRequest,
73
+ refresh: boolean,
74
+ venues: Promise<ListingVenue[]> | null,
75
+ ): Promise<QuoteAnswer> {
76
+ const root = listing.exchange ? null : indexRootFor(listing.symbol);
77
+ // A bare root that quoted something else, such as MOVE (Corvex), points at the index too.
78
+ const also = (quote: Quote) => root && quote.instrumentType?.trim().toUpperCase() !== "INDEX"
79
+ ? [indexRootAlsoLine(root, request.command)] : [];
80
+ if (result.quote) {
81
+ // "CAR = Avis Budget Group, NASDAQ (also XETRA, ASX)" names the venue too, so it replaces "SXR8 -> XETRA".
82
+ const choice = venues ? await listingChoiceNotes(listing, market, result.quote, venues) : listingReadNotes(listing);
83
+ const venue = choice.length > 0 ? null : bareListingNote(listing, result.quote);
84
+ // A row saved as 247540:KRX is quoted from KOSDAQ, the board it trades on.
85
+ const board = tradingBoardNote(listing, result.quote);
86
+ return { result, listing, notes: [...choice, venue ?? [], board ?? [], also(result.quote), currencyCodeNote(listing, result.quote) ?? []].flat() };
87
+ }
88
+ const notTraded = await notTradedMessage(listing, market);
89
+ if (notTraded) return { result, listing, failure: { message: notTraded, oneLine: notTraded }, notes: [] };
90
+ if (!isNoProviderError(result.error)) return { result, listing, notes: [] };
91
+ const other = await quoteBareListing(listing, market, request, { refresh });
92
+ if (other?.kind === "quoted") {
93
+ return {
94
+ result: { target: other.listing.request, quote: other.quote },
95
+ listing: other.listing,
96
+ notes: [other.note, ...also(other.quote), currencyCodeNote(listing, other.quote) ?? []].flat(),
97
+ };
98
+ }
99
+ // Other listings already name a futures board code's future (GC=F); with none, it is named here.
100
+ // A Treasury yield's root (TNX) names its FRED series and the curve beside its index.
101
+ const yieldTenor = root ? treasuryYieldTenor(listing.symbol) : null;
102
+ const tryLines = [
103
+ yieldTenor ? treasuryYieldLine(yieldTenor) : root ? indexRootTryLine(root, request.command) : null,
104
+ other || listing.exchange ? null : futuresTryLine(listing.symbol, request),
105
+ // BDI asks which exchange (Black Diamond Group) and says nowhere that the Baltic Dry Index is not here.
106
+ listing.exchange ? null : knownGapForTicker(listing.symbol),
107
+ ].filter((line): line is string => !!line);
108
+ if (!other && tryLines.length === 0) return { result: await withCurrencyPairHint(result, market), listing, notes: [] };
109
+ const message = other?.message ?? `${listing.symbol} has no ${request.noun}.`;
110
+ const details = [other?.details, ...tryLines].filter(Boolean).join("\n");
111
+ const oneLine = [
112
+ message,
113
+ other ? `${other.heading}: ${other.listings.join(", ")}.` : "",
114
+ ...tryLines,
115
+ ].filter(Boolean).join(" ");
116
+ return { result, listing, failure: { message, details, oneLine }, notes: [] };
117
+ }
118
+
119
+ /** A listing a command found a quote for, with what `quote` says beside it. */
120
+ interface QuotedListing {
121
+ /** The listing the quote is of: the one typed, or the venue a bare symbol was quoted on. */
122
+ listing: CliListing;
123
+ quote: Quote;
124
+ /** The venue a bare symbol was read on, the currency it may have meant, the US ticker it is one slip from. */
125
+ notes: string[];
126
+ }
127
+
128
+ /**
129
+ * The listing a command that keeps a symbol (`alerts add`) was given, read as
130
+ * `quote` reads it: the same resolver and the same quote request, so what is
131
+ * kept is the listing `quote` prints (USDMXN and USD/MXN are USDMXN=X, MXN is
132
+ * MXN:FWB2) and `quote`'s own notes come back with it. A symbol nothing
133
+ * quotes fails the command with why, and the nearest listing search finds when
134
+ * it is near ("Not a ticker: ZZZQQ. Did you mean ZZZQ?"), so nothing is kept
135
+ * for it.
136
+ */
137
+ export async function requireQuotedListing(
138
+ raw: string,
139
+ exchange: string | null | undefined,
140
+ market: MarketContext,
141
+ ctx: Pick<Parameters<CliCommandDef["execute"]>[1], "fail">,
142
+ command: string,
143
+ ): Promise<QuotedListing> {
144
+ const listing = await requireCliListing(raw, exchange, market, ctx);
145
+ const venues = loadBareVenues(listing, market);
146
+ const [result] = await market.dataProvider.getQuotesBatch([listing.request], { revalidate: true });
147
+ const answer = await answerQuote(result ?? { target: listing.request, quote: null }, listing, market, { command, noun: "quote" }, false, venues);
148
+ const quote = answer.result.quote;
149
+ if (!quote) {
150
+ if (answer.failure) ctx.fail(answer.failure.message, answer.failure.details);
151
+ const message = errorMessage(answer.result.error) ?? `No quote found for ${listing.key}.`;
152
+ // The listing search names, else the US ticker it is one slip from (AAPLL for AAPL), which search does not list for a typo.
153
+ const suggestion = notATickerSymbol(message) && !message.includes(" Did you mean ")
154
+ ? await suggestListing(listing.symbol, market) ?? await suggestUsTicker(listing.symbol, market)
155
+ : null;
156
+ ctx.fail(suggestion ? withListingSuggestion(message, describeSuggestion(suggestion)) : message);
157
+ }
158
+ // A bare symbol read on a listing abroad that is one slip from a US ticker says so, as `quote` does.
159
+ const venue = answer.listing.saved ? null : bareListingNote(answer.listing, quote);
160
+ const usSuggestion = venue && answer.notes.includes(venue) ? await suggestUsTicker(answer.listing.symbol, market) : null;
161
+ const notUs = notUsTickerSentence({ target: answer.result.target, quote, listing: { name: quote.name ?? null }, usSuggestion: usSuggestion ?? undefined });
162
+ return { listing: answer.listing, quote, notes: [...answer.notes, ...notUs ? [notUs] : []] };
163
+ }
164
+
165
+ /**
166
+ * A symbol written like a currency pair that is not a ticker (USD_NGN, as some
167
+ * FX feeds write a pair) names the listing `gloomberb search` finds nearest to
168
+ * it, as `watchlist add` does: "Did you mean USDNGN=X (US Dollar/Nigerian
169
+ * Naira)?". When search finds nothing near, the row keeps its own error and its
170
+ * search hint.
171
+ */
172
+ async function withCurrencyPairHint(result: QuoteBatchResult, market: MarketContext): Promise<QuoteBatchResult> {
173
+ const message = errorMessage(result.error);
174
+ const symbol = message ? notATickerSymbol(message) : null;
175
+ if (!message || !symbol || !looksLikeCurrencyPair(symbol)) return result;
176
+ const suggestion = await suggestListing(symbol, market);
177
+ return suggestion ? { ...result, error: withListingSuggestion(message, describeSuggestion(suggestion)) } : result;
178
+ }
179
+
@@ -1,6 +1,7 @@
1
1
  import { seriesExpression } from "../series/registry";
2
2
  import type { SeriesDefinition } from "../series/types";
3
3
  import { cliStyles, renderSection, renderTable, wrapText } from "../utils/cli-output";
4
+ import { currencyCodeLabel } from "../utils/currency-units";
4
5
 
5
6
  /** A data catalog series a query names, as the CLI lists it. */
6
7
  export interface SeriesMatch {
@@ -108,7 +109,7 @@ export function renderSeriesMatches(
108
109
  renderTable(
109
110
  [{ header: "Series" }, { header: "Unit" }, { header: "Frequency", optional: true }, { header: "Publisher", optional: true },
110
111
  { header: "Chart", shrink: false }],
111
- shown.map((match) => [match.name, match.unit, match.frequency, match.publisher ?? "", cliStyles.command(shellArgument(match.expression))]),
112
+ shown.map((match) => [match.name, currencyCodeLabel(match.unit), match.frequency, match.publisher ?? "", cliStyles.command(shellArgument(match.expression))]),
112
113
  ),
113
114
  "",
114
115
  ...wrapText(hint, width).map((line) => cliStyles.muted(line)),
package/src/cli/types.ts CHANGED
@@ -5,6 +5,7 @@ import { AssetDataRouter } from "../sources/provider-router";
5
5
  import type { AppServices } from "../core/app-services";
6
6
  import type { CapabilityInvoker } from "../capabilities/types";
7
7
  import type { StatementReportSources } from "./statement-filings";
8
+ import type { FxHistoryClient } from "../market-data/fx-closes";
8
9
 
9
10
  export type ConfigContext = {
10
11
  config: AppConfig;
@@ -15,12 +16,15 @@ export type ConfigContext = {
15
16
 
16
17
  export type MarketContext = ConfigContext & {
17
18
  dataProvider: AssetDataRouter;
19
+ resolveBroker?: (brokerType: string) => import("../types/broker").BrokerAdapter | null;
18
20
  /** Charts catalog series in a report run without the plugin runtime. */
19
21
  seriesCapabilities?: CapabilityInvoker;
20
22
  /** `--refresh`: prefetched data bypasses the local cache. */
21
23
  refresh?: boolean;
22
24
  /** Filings behind the statements and a bank's figures, read from Gloom Cloud; absent where the data service is a stand-in. */
23
25
  statementSources?: StatementReportSources;
26
+ /** Daily FX closes (`fx --date`, converted INS values); Gloom Cloud unless a test stands in for it. */
27
+ fxHistory?: FxHistoryClient;
24
28
  };
25
29
 
26
30
  export type CliServicesContext = {
@@ -11,6 +11,7 @@ import { ChartToolbar, CHART_TOOLBAR_WIDTH } from "./toolbar";
11
11
  import { AsciiText, Box, ChartSurface, Text, useUiCapabilities, type BoxRenderable } from "../../../ui";
12
12
  import { useLiveStretch } from "../../../ui/live-stretch";
13
13
  import { useOptionalPaneInstanceId } from "../../../state/app/context";
14
+ import { usePaneFocused } from "../../../state/app/activity";
14
15
  import { useTabCursorLink } from "../../../state/app/tab-cursor";
15
16
  import { useThemeColors, useThemeId } from "../../../theme/theme-context";
16
17
  import { themedSeriesColor, themeSeriesColors } from "../../../theme/series-colors";
@@ -155,6 +156,8 @@ function ThemedCompositeChart({
155
156
  const showTextFallback = useShowChartTextFallback();
156
157
  const [internalCursorDate, setInternalCursorDate] = useState<Date | null>(null);
157
158
  const [toolSpan, setToolSpan] = useState<ChartToolSpan | null>(null);
159
+ const [pointerDragging, setPointerDragging] = useState(false);
160
+ const paneFocused = usePaneFocused();
158
161
  const paneInstanceId = useOptionalPaneInstanceId();
159
162
  const [drawings, setDrawings] = useState<readonly ChartDrawing[]>(NO_DRAWINGS);
160
163
  const [selectedDrawingId, setSelectedDrawingId] = useState<string | null>(null);
@@ -333,6 +336,7 @@ function ThemedCompositeChart({
333
336
  availableAxisWidth,
334
337
  Math.max(
335
338
  Math.min(MINIMUM_AXIS_LABEL_WIDTH, maximumAxisWidth),
339
+ ...(levels?.items ?? []).map((level) => level.axisLabel?.length ?? 0),
336
340
  ...(layoutPanels ?? []).flatMap((panel) => (["left", "right"] as const).map((side) => {
337
341
  const width = compositeAxisLabelWidth(panel.axes[side], formatAxisValue, includeCursorLabels);
338
342
  if (!includeSeriesTags || !width) return includeCursorLabels ? width : Math.min(width, maximumAxisWidth);
@@ -343,7 +347,7 @@ function ThemedCompositeChart({
343
347
  return (width + 1) * lanes;
344
348
  })),
345
349
  ),
346
- ), [availableAxisWidth, formatAxisValue, includeCursorLabels, includeSeriesTags, layoutPanels, maximumAxisWidth]);
350
+ ), [availableAxisWidth, formatAxisValue, includeCursorLabels, includeSeriesTags, layoutPanels, maximumAxisWidth, levels?.items]);
347
351
  const leftAxisWidth = hasLeftAxis ? resolvedAxisWidth : 0;
348
352
  const rightAxisWidth = hasRightAxis ? resolvedAxisWidth : 0;
349
353
  const axisGap = resolvedAxisWidth > 0 ? 1 : 0;
@@ -650,7 +654,11 @@ function ThemedCompositeChart({
650
654
  );
651
655
  }
652
656
 
653
- const showToolbar = interactive && navigable && plotWidth > CHART_TOOLBAR_WIDTH + 4;
657
+ // The tools belong to the focused pane, and stay while one is in hand or a
658
+ // press drags the plot, so focus moving elsewhere never hides what is
659
+ // drawing. A chart outside a pane (a share page) keeps them.
660
+ const toolbarWanted = paneFocused !== false || armedTool !== null || pointerDragging || toolSpan !== null;
661
+ const showToolbar = interactive && navigable && toolbarWanted && plotWidth > CHART_TOOLBAR_WIDTH + 4;
654
662
  // Bars shorter than a day carry a time of day the cursor reads out.
655
663
  const intradayBars = scene.timeZone !== undefined || visibleSeries.some((entry) => (
656
664
  !!entry.historyResolution && isIntradayResolution(entry.historyResolution)
@@ -772,6 +780,7 @@ function ThemedCompositeChart({
772
780
  onZoomViewport={zoomViewport}
773
781
  onSetViewport={setViewportRange}
774
782
  onToolSpanChange={setToolSpan}
783
+ onDragActiveChange={setPointerDragging}
775
784
  keyboardToolDrag={keyboardToolDrag?.panelId === panel.id ? keyboardToolDrag : null}
776
785
  onPointerPress={cancelKeyboardPlacement}
777
786
  showTextFallback={showTextFallback}
@@ -298,6 +298,14 @@ export function formatChartLegendValue(
298
298
  return trimmed && trimmed.length <= 6 ? `${compact}${trimmed.startsWith("/") ? "" : " "}${trimmed}` : compact;
299
299
  }
300
300
 
301
+ /** compactNumber's scale, with the decimals a tick gap needs at that scale. */
302
+ function preciseCompactNumber(value: number, gap: number): string {
303
+ const absolute = Math.abs(value);
304
+ const [divisor, suffix] = ([[1e12, "T"], [1e9, "B"], [1e6, "M"], [1e3, "K"]] as const).find(([scale]) => absolute >= scale) ?? [1, ""];
305
+ const digits = Math.min(12, Math.max(0, Math.ceil(-Math.log10(gap / divisor) - 1e-9)));
306
+ return `${(value / divisor).toFixed(digits)}${suffix}`;
307
+ }
308
+
301
309
  const COMPACT_LABEL = /^(-?\d+)(?:\.(\d+))?([KMBT]?)$/;
302
310
 
303
311
  function trimmedDecimals(label: string): number {
@@ -332,13 +340,17 @@ export function formatCompositeAxisValue(value: number, domain: CompositeAxisDom
332
340
  // Where that happens the axis spends the digits needed to tell them apart,
333
341
  // shared across every tick so the column reads as one scale.
334
342
  const gap = narrowestAxisTickGap(domain);
343
+ const prefix = currencyPrefix(domain.unit);
335
344
  if (gap !== null && gap < compactResolution(value)) {
336
345
  const resolved = formatAxisPriceValue(value, domain, {
337
346
  fixedFractionDigits: Math.max(0, Math.ceil(-Math.log10(gap))),
338
347
  });
339
348
  if (resolved) return resolved;
349
+ // No currency to format with, such as a price whose quote has not
350
+ // arrived: the same digits still keep the ticks apart.
351
+ const precise = preciseCompactNumber(value, gap);
352
+ return prefix && precise.startsWith("-") ? `-${prefix}${precise.slice(1)}` : `${prefix}${precise}`;
340
353
  }
341
- const prefix = currencyPrefix(domain.unit);
342
354
  return prefix && compact.startsWith("-") ? `-${prefix}${compact.slice(1)}` : `${prefix}${compact}`;
343
355
  }
344
356
 
@@ -0,0 +1,30 @@
1
+ import type { CSSProperties } from "react";
2
+ import { blendForContrast, higherContrast } from "../../../theme/color-utils";
3
+ import { fitAxisLabels, type CompositeChartLevel, type ProjectedLevel } from "./levels";
4
+
5
+ /** Keep a precise price in the tag only when it has no readable axis marker. */
6
+ export function levelLabelText(level: CompositeChartLevel, axisVisible: boolean): string {
7
+ return `${level.label ?? ""}${!axisVisible && level.axisLabel ? ` ${level.axisLabel}` : ""}`;
8
+ }
9
+
10
+ /** Compact DOM tags straddle their line; crowded or clipped tags yield. */
11
+ export function layoutLevelLabels(levels: readonly ProjectedLevel[], plotHeight: number, textHeight: number) {
12
+ const candidates = levels.filter(({ level }) => level.label).map((entry) => ({
13
+ ...entry, top: entry.yRatio * plotHeight - textHeight / 2,
14
+ })).filter(({ top }) => top >= 0 && top + textHeight <= plotHeight);
15
+ return fitAxisLabels(candidates, [], ({ top }) => top / textHeight);
16
+ }
17
+
18
+ export function levelLabelStyle({ yRatio, level }: ProjectedLevel, background: string): CSSProperties {
19
+ return {
20
+ top: `${yRatio * 100}%`,
21
+ transform: "translateY(-50%)",
22
+ pointerEvents: "none",
23
+ paddingInline: 3,
24
+ height: "auto",
25
+ width: "max-content",
26
+ border: `1px solid ${level.color}`,
27
+ backgroundColor: background,
28
+ color: blendForContrast(level.color, background, higherContrast("#000000", "#ffffff", background), 4.5),
29
+ };
30
+ }
@@ -14,6 +14,11 @@ export interface CompositeChartLevel {
14
14
  editable: boolean;
15
15
  /** Whether the chart's level action applies to it. */
16
16
  actionable: boolean;
17
+ /** Optional read-only annotation, independent of drawing-tool selection. */
18
+ label?: string;
19
+ /** Exact instrument price, independent of the grid's compact tick formatter. */
20
+ axisLabel?: string;
21
+ dashed?: boolean;
17
22
  }
18
23
 
19
24
  export type CompositeLevelEdit =
@@ -111,13 +116,17 @@ export function fitAxisLabels<T>(
111
116
  });
112
117
  }
113
118
 
119
+ function levelSegments(dashed?: boolean): Array<[number, number]> {
120
+ return dashed ? Array.from({ length: 48 }, (_, i) => [i / 48, (i + 0.55) / 48]) : [[0, 1]];
121
+ }
122
+
114
123
  export function levelVectors(levels: readonly ProjectedLevel[], selectedId: string | null): ChartVectorShape[] {
115
- return levels.map(({ level, yRatio }) => ({
116
- id: `level:${level.id}`,
117
- points: [{ x: 0, y: yRatio }, { x: 1, y: yRatio }],
124
+ return levels.flatMap(({ level, yRatio }) => levelSegments(level.dashed).map(([start, end], index) => ({
125
+ id: `level:${level.id}:${index}`,
126
+ points: [{ x: start, y: yRatio }, { x: end, y: yRatio }],
118
127
  color: level.color,
119
128
  strokeWidth: level.id === selectedId ? 2.2 : 1.2,
120
- }));
129
+ })));
121
130
  }
122
131
 
123
132
  /** Levels painted on a copy of the panel raster, the one layer the terminal uploads. */
@@ -131,7 +140,9 @@ export function paintLevels(
131
140
  const maxY = Math.max(base.height - 1, 0);
132
141
  for (const { level, yRatio } of levels) {
133
142
  const y = Math.round(yRatio * maxY);
134
- drawLine(data, base.width, base.height, 0, y, maxX, y, parseHex(level.color), level.id === selectedId ? 2.2 : 1.2);
143
+ for (const [start, end] of levelSegments(level.dashed)) {
144
+ drawLine(data, base.width, base.height, start * maxX, y, end * maxX, y, parseHex(level.color), level.id === selectedId ? 2.2 : 1.2);
145
+ }
135
146
  }
136
147
  return { width: base.width, height: base.height, pixels: data };
137
148
  }
@@ -145,7 +156,7 @@ export function writeLevelText(lines: readonly string[], levels: readonly Projec
145
156
  const rows = lines.map((line) => Array.from(line));
146
157
  for (const { level, yRatio } of levels) {
147
158
  const row = rows[clamp(Math.round(yRatio * (rows.length - 1)), 0, rows.length - 1)]!;
148
- const glyph = level.id === selectedId ? "━" : level.editable ? "─" : "┄";
159
+ const glyph = level.id === selectedId ? "━" : level.dashed || !level.editable ? "┄" : "─";
149
160
  for (let x = 0; x < row.length; x += 1) {
150
161
  if (row[x] === " " || row[x] === "·" || row[x] === "╌") row[x] = glyph;
151
162
  }
@@ -12,7 +12,8 @@ import { colors as themeColors } from "../../../theme/colors";
12
12
  import { consumeChartMouseEvent, getGlobalMouseX, getLocalPlotPointer, type ChartMouseEvent } from "../core/pointer";
13
13
  import { useStaticChartBitmapSize } from "./bitmap";
14
14
  import { useMeasuredCells } from "../../../ui/measured-cells";
15
- import { PriceAxisLabels } from "./price-axis-labels";
15
+ import { PriceAxisLabels, placeAxisTicks } from "./price-axis-labels";
16
+ import { layoutLevelLabels, levelLabelStyle, levelLabelText } from "./level-labels";
16
17
  import { formatCompositeCursorValue, type CompositeAxisValueFormatter } from "./format";
17
18
  import {
18
19
  compositeViewportPositions,
@@ -111,6 +112,8 @@ interface CompositePanelSurfaceProps {
111
112
  onZoomViewport: (zoomFactor: number, anchorRatio: number) => void;
112
113
  onSetViewport: (range: CompositeViewportRange) => void;
113
114
  onToolSpanChange: (span: ChartToolSpan | null) => void;
115
+ /** A press on the plot started (true) or stopped (false) dragging: a pan, a tool, a drawing or a level. */
116
+ onDragActiveChange?: (dragging: boolean) => void;
114
117
  /** The tool being placed from the keyboard, when it lives in this panel. */
115
118
  keyboardToolDrag: KeyboardToolDrag | null;
116
119
  /** A press on the plot: the pointer takes over from a keyboard placement. */
@@ -154,6 +157,7 @@ export function CompositePanelSurface({
154
157
  onZoomViewport,
155
158
  onSetViewport,
156
159
  onToolSpanChange,
160
+ onDragActiveChange,
157
161
  keyboardToolDrag,
158
162
  onPointerPress,
159
163
  showTextFallback,
@@ -477,14 +481,16 @@ export function CompositePanelSurface({
477
481
  .sort((left, right) => Number(right.level.id === selectedLevelId) - Number(left.level.id === selectedLevelId)
478
482
  || Number(left.level.editable) - Number(right.level.editable))
479
483
  .map(({ level, yRatio }) => ({
484
+ id: level.id,
480
485
  side: levelDomain.side,
481
486
  yRatio,
482
- label: formatAxisValue ? formatAxisValue(level.value, levelDomain) : formatCompositeCursorValue(level.value, levelDomain),
487
+ label: level.axisLabel ?? (formatAxisValue ? formatAxisValue(level.value, levelDomain) : formatCompositeCursorValue(level.value, levelDomain)),
483
488
  color: level.color,
484
489
  })) : [], [formatAxisValue, levelDomain, projectedLevels, selectedLevelId]);
485
490
  const buildAxisMarkers = (side: "left" | "right") => {
486
491
  if (!panel.axes[side]) return undefined;
487
- const toAxisMarker = (marker: { yRatio: number; label: string; color: string }) => ({
492
+ const toAxisMarker = (marker: { id?: string; yRatio: number; label: string; color: string }) => ({
493
+ id: marker.id,
488
494
  row: Math.round(marker.yRatio * Math.max(panel.height - 1, 0)),
489
495
  pixelY: marker.yRatio * Math.max(panel.height * cellHeightPx - 1, 0),
490
496
  label: marker.label,
@@ -499,8 +505,14 @@ export function CompositePanelSurface({
499
505
  // A label is a row tall, so close levels would stack into an unreadable
500
506
  // pile and hide the last price: each keeps its line, and only the labels
501
507
  // with a row to themselves show. Desktop badges sit between rows.
508
+ const grid = fractionalViewport
509
+ ? placeAxisTicks({ ticks: side === "left" ? leftAxisTicks : rightAxisTicks, height: panel.height, cellHeightPx }).map((tick) => ({
510
+ ...toAxisMarker({ yRatio: tick.ratio, label: tick.label, color: colors.textDim }),
511
+ pixelY: tick.topPercent / 100 * Math.max(panel.height * cellHeightPx - 1, 1),
512
+ }))
513
+ : [...(side === "left" ? leftAxisLabels : rightAxisLabels)].map(([row, label]) => ({ id: undefined, row, pixelY: row * cellHeightPx, label, color: colors.textDim }));
502
514
  const markers = [
503
- ...fitAxisLabels(levelLabels, fixed, (marker) => fractionalViewport ? marker.pixelY / cellHeightPx : marker.row),
515
+ ...fitAxisLabels(levelLabels, [...fixed, ...grid], (marker) => fractionalViewport ? marker.pixelY / cellHeightPx : marker.row),
504
516
  ...fixed,
505
517
  ];
506
518
  return markers.length > 0 ? markers : undefined;
@@ -736,12 +748,29 @@ export function CompositePanelSurface({
736
748
  }
737
749
  finishToolDrag();
738
750
  }, [finishToolDrag]);
751
+ // Said once per change, so a pointer move costs the chart nothing.
752
+ const reportedDragRef = useRef(false);
753
+ const reportDrag = useCallback((dragging: boolean) => {
754
+ if (reportedDragRef.current === dragging) return;
755
+ reportedDragRef.current = dragging;
756
+ onDragActiveChange?.(dragging);
757
+ }, [onDragActiveChange]);
758
+ const pressPlot = useCallback((event: ChartMouseEvent) => {
759
+ const before = dragRef.current;
760
+ startDrag(event);
761
+ if (dragRef.current !== null && dragRef.current !== before) reportDrag(true);
762
+ }, [reportDrag, startDrag]);
763
+ const releasePlot = useCallback(() => {
764
+ resetDrag();
765
+ reportDrag(false);
766
+ }, [reportDrag, resetDrag]);
739
767
  const handleMouseMove = useCallback((event: ChartMouseEvent) => {
740
768
  // A release over a neighbouring pane never reaches this surface, so treat a
741
769
  // plain move, which only fires with no button held, as the missing release.
742
770
  finishToolDrag();
771
+ reportDrag(false);
743
772
  updateCursor(event);
744
- }, [finishToolDrag, updateCursor]);
773
+ }, [finishToolDrag, reportDrag, updateCursor]);
745
774
  // Trackpads fire wheel events faster than the plot can paint, so one frame
746
775
  // absorbs every event that arrived since the last one.
747
776
  const pendingWheelRef = useRef<PendingWheel | null>(null);
@@ -876,10 +905,10 @@ export function CompositePanelSurface({
876
905
  crosshair={crosshair}
877
906
  vectors={vectors}
878
907
  onMouseMove={interactive ? handleMouseMove : undefined}
879
- onMouseDown={interactive ? navigable ? startDrag : pressCursor : undefined}
908
+ onMouseDown={interactive ? navigable ? pressPlot : pressCursor : undefined}
880
909
  onMouseDrag={interactive && navigable ? dragViewport : undefined}
881
- onMouseUp={interactive && navigable ? resetDrag : undefined}
882
- onMouseDragEnd={interactive && navigable ? resetDrag : undefined}
910
+ onMouseUp={interactive && navigable ? releasePlot : undefined}
911
+ onMouseDragEnd={interactive && navigable ? releasePlot : undefined}
883
912
  onMouseScroll={interactive && navigable ? panFromWheel : undefined}
884
913
  onMouseOut={interactive ? clearCursor : undefined}
885
914
  cursor={interactive ? toolDrag || !navigable || onPickTime ? "crosshair" : "grab" : undefined}
@@ -890,6 +919,27 @@ export function CompositePanelSurface({
890
919
  >
891
920
  {textLines.map((line, index) => <Text key={index} fg={colors.text}>{line}</Text>)}
892
921
  </ChartSurface>
922
+ {(isDesktopWeb
923
+ ? layoutLevelLabels(projectedLevels, plotSize.height * cellHeightPx, cellHeightPx)
924
+ : fitAxisLabels(projectedLevels.filter((entry) => entry.level.label), [], (entry) => entry.yRatio * Math.max(1, panel.height - 1))
925
+ ).map((entry) => {
926
+ const { level, yRatio } = entry;
927
+ const axisWidth = levelDomain?.side === "left" ? leftAxisWidth : rightAxisWidth;
928
+ const axisMarkers = levelDomain?.side === "left" ? leftAxisMarkers : rightAxisMarkers;
929
+ const cursorCovers = activeCursorYRatio !== null && Math.abs(activeCursorYRatio - yRatio) * panel.height < 1;
930
+ const axisVisible = !cursorCovers && (level.axisLabel?.length ?? 0) <= axisWidth && !!axisMarkers?.some((marker) => marker.id === level.id);
931
+ const label = levelLabelText(level, axisVisible);
932
+ const labelStyle = isDesktopWeb ? {
933
+ ...levelLabelStyle(entry, colors.background), left: "auto", right: (rightAxisWidth + (rightAxisWidth ? axisGap : 0) + 1) * cellWidthPx,
934
+ maxWidth: Math.max(1, plotWidth - 2) * cellWidthPx,
935
+ } : undefined;
936
+ const labelWidth = Math.min(plotWidth - 2, label.length + 2);
937
+ return <Box key={`annotation:${level.id}`} position="absolute" left={leftAxisWidth + (leftAxisWidth ? axisGap : 0) + plotWidth - labelWidth - 1} top={yRatio * Math.max(0, panel.height - 1)} height={1}
938
+ width={labelWidth} flexDirection="row" justifyContent="flex-end" backgroundColor={colors.background} zIndex={20}
939
+ style={labelStyle} data-gloom-role="chart-level-label">
940
+ <Text fg={labelStyle?.color ?? level.color} wrapMode="none" style={isDesktopWeb ? { lineHeight: 1 } : undefined}>{label}</Text>
941
+ </Box>;
942
+ })}
893
943
  {rightAxisWidth > 0 ? (
894
944
  <>
895
945
  <Box width={axisGap} />
@@ -17,6 +17,7 @@ import {
17
17
  type TickerSearchInstrumentClass,
18
18
  } from "../../../../tickers/search";
19
19
  import type { ResultItem } from "../../list/model";
20
+ import { delistingLabel, parseListingDelisting } from "../../../../market-data/quotes/delisting";
20
21
  import {
21
22
  buildTickerSearchCacheKey,
22
23
  createQuickLookTickerCandidates,
@@ -74,6 +75,9 @@ export function useCommandBarTickerSearchActions({
74
75
  // The candidate's detail leads with the name; the rest repeats the badge or venue.
75
76
  const leadingName = candidate.detail.split(" | ")[0]?.trim();
76
77
  const name = leadingName && leadingName.toUpperCase() !== candidate.label.toUpperCase() ? leadingName : undefined;
78
+ // A listing that no longer trades says so under its row, with its last session.
79
+ const delisting = parseListingDelisting(candidate.result?.delisting);
80
+ const lines = delisting ? [{ segments: [{ text: delistingLabel(delisting), emphasis: "muted" as const }] }] : undefined;
77
81
 
78
82
  if (candidate.kind === "ticker" && candidate.ticker) {
79
83
  return {
@@ -86,6 +90,7 @@ export function useCommandBarTickerSearchActions({
86
90
  badge,
87
91
  name,
88
92
  right,
93
+ ...(lines ? { lines } : {}),
89
94
  category: candidate.category,
90
95
  kind: "ticker",
91
96
  resolveTicker: async () => candidate.ticker!,
@@ -112,6 +117,7 @@ export function useCommandBarTickerSearchActions({
112
117
  badge,
113
118
  name,
114
119
  right,
120
+ ...(lines ? { lines } : {}),
115
121
  category: candidate.category,
116
122
  kind: "search",
117
123
  resolveTicker: () => resolveSearchTicker(candidate.result!, publicTickerKey(
@@ -101,6 +101,8 @@ export { usePaneStatusFooter, usePaneStatusLinkFooter } from "./layout/pane/stat
101
101
  export { usePaneNoticeFooter, type UsePaneNoticeFooterOptions } from "./use-pane-notice-footer";
102
102
  export { loadingErrorFooterInfo } from "./data-table/table-pane";
103
103
  export type { PaneFooterPressEvent, PaneFooterSegment, PaneHint } from "./layout/pane/footer";
104
+ // Data a source delivers late: the footer's yellow clock, its detail on hover.
105
+ export { DELAYED_SEGMENT, delayedFooterSegment, splitDelayedStatus } from "./layout/pane/footer";
104
106
  export {
105
107
  getPaneSidebarWidth,
106
108
  getPaneSidebarWidthRange,
@@ -1,7 +1,7 @@
1
1
  import { memo, Profiler, useCallback, useMemo, type ReactNode } from "react";
2
2
  import { useAppLanguage } from "../../../i18n/react";
3
3
  import { isPerfTraceEnabled, recordPerfSample } from "../../../utils/perf-marks";
4
- import { PaneInViewProvider } from "../../../state/app/activity";
4
+ import { PaneFocusedProvider, PaneInViewProvider } from "../../../state/app/activity";
5
5
  import { PaneInstanceProvider } from "../../../state/app/context";
6
6
  import { RemoteUiPaneScope } from "../../../remote/semantic-tree";
7
7
  import { useThemeColors } from "../../../theme/theme-context";
@@ -91,19 +91,21 @@ export const PaneContent = memo(function PaneContent({
91
91
  <PaneInstanceProvider paneId={paneId}>
92
92
  <RemoteUiPaneScope paneId={paneId}>
93
93
  <PaneInViewProvider value={inView}>
94
- <PaneKeyboardScrollController paneId={paneId} focused={focused} />
95
- <Box
96
- flexDirection="column"
97
- flexGrow={1}
98
- flexShrink={1}
99
- flexBasis={0}
100
- minWidth={0}
101
- minHeight={0}
102
- overflow="hidden"
103
- data-gloom-role="pane-content"
104
- >
105
- {body}
106
- </Box>
94
+ <PaneFocusedProvider value={focused}>
95
+ <PaneKeyboardScrollController paneId={paneId} focused={focused} />
96
+ <Box
97
+ flexDirection="column"
98
+ flexGrow={1}
99
+ flexShrink={1}
100
+ flexBasis={0}
101
+ minWidth={0}
102
+ minHeight={0}
103
+ overflow="hidden"
104
+ data-gloom-role="pane-content"
105
+ >
106
+ {body}
107
+ </Box>
108
+ </PaneFocusedProvider>
107
109
  </PaneInViewProvider>
108
110
  </RemoteUiPaneScope>
109
111
  </PaneInstanceProvider>