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
package/src/sync/types.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Dispatch } from "react";
2
2
  import type { AppAction, AppState } from "../core/state/app/state";
3
3
  import type { AppTickerRepositoryPort } from "../core/app-service-ports";
4
+ import type { LocalMarketDataLookup } from "./local-only";
4
5
 
5
6
  export const SYNC_SNAPSHOT_SCHEMA_VERSION = 1;
6
7
 
@@ -31,6 +32,8 @@ interface SyncContributorMemo {
31
32
 
32
33
  interface SyncCollectContext {
33
34
  state: AppState;
35
+ /** Sources whose quotes and bars never leave this device (`BrokerAdapter.localMarketData`); none when absent. */
36
+ isLocalMarketData?: LocalMarketDataLookup;
34
37
  /** The payload this device last pushed for this contributor, or null when it never synced. */
35
38
  baselinePayload?: unknown;
36
39
  memo?: SyncContributorMemo;
@@ -19,6 +19,7 @@ import { isQuoteBeforeLatestSession, isQuoteStaleForCurrentSession } from "../ma
19
19
  import { formatMarketPriceWithCurrency, quoteFormatOptions } from "../market-data/market/format";
20
20
  import { canonicalExchange, isUsListingExchange, parsePublicTickerKey, resolveExchangeTimeZone } from "../utils/exchanges";
21
21
  import { zonedDateKey } from "../utils/zoned-date-time";
22
+ import { delistingLabel } from "../market-data/quotes/delisting";
22
23
 
23
24
  /*
24
25
  * What became of a symbol, read the way `quote` reads it: the listing's own
@@ -33,8 +34,10 @@ const ENDED_AFTER_MS = 183 * 24 * 60 * 60_000;
33
34
  /**
34
35
  * The exchange-local date a listing last traded, when its quote is stale and
35
36
  * that was over half a year ago (TWTR's 2022-10-27); null for anything newer.
37
+ * A quote that says its listing no longer trades gives its last session.
36
38
  */
37
39
  function endedTradingDate(quote: Quote, now = Date.now()): string | null {
40
+ if (quote.delisting) return quote.delisting.lastTradeDate;
38
41
  const last = Math.max(...[quote.lastTradeTime, quote.lastUpdated].filter((time): time is number => Number.isFinite(time)));
39
42
  if (!(last > 0) || now - last < ENDED_AFTER_MS || !isQuoteStaleForCurrentSession(quote, now)) return null;
40
43
  return quoteDateKey(quote, last);
@@ -60,12 +63,18 @@ export function tradingListing(quote: Quote | null | undefined, now = Date.now()
60
63
  return quote && !endedTradingDate(quote, now) ? quotedListing(quote) : null;
61
64
  }
62
65
 
63
- /** "TWTR: no longer trading since 2022-10-27 (last price $53.86)", for a quote that stopped long ago. */
66
+ /**
67
+ * "TWTR: no longer trading since 2022-10-27 (last price $53.86)", for a quote
68
+ * that stopped long ago; "TWTR: Delisted Oct 27, 2022 (last price $53.86)"
69
+ * when the quote says the listing no longer trades.
70
+ */
64
71
  export function noLongerTradingNote(label: string, quote: Quote, now = Date.now()): string | null {
65
72
  const date = endedTradingDate(quote, now);
66
73
  if (!date) return null;
67
74
  const price = formatMarketPriceWithCurrency(quote.price, quote.currency, quoteFormatOptions(quote));
68
- return `${label}: no longer trading since ${date} (last price ${price})`;
75
+ return quote.delisting
76
+ ? `${label}: ${delistingLabel(quote.delisting)} (last price ${price})`
77
+ : `${label}: no longer trading since ${date} (last price ${price})`;
69
78
  }
70
79
 
71
80
  /**
@@ -81,11 +90,17 @@ export interface ListingFate {
81
90
  /**
82
91
  * The line a command with nothing to serve for the symbol says: `quote`'s
83
92
  * change line when its company left it ("FB now lists ...; Meta Platforms
84
- * trades as META (changed 2022-06-09)"), else, for a listing that stopped,
85
- * "TWTR stopped trading 2022-10-27; no history or options served". Null
86
- * for a symbol that trades as it always has.
93
+ * trades as META (changed 2022-06-09)"), else, for a listing the service
94
+ * says no longer trades, "TWTR: Delisted Oct 27, 2022", and for another
95
+ * that stopped, "XYZ stopped trading 2022-10-27; no history or options
96
+ * served". Null for a symbol that trades as it always has.
87
97
  */
88
98
  note: string | null;
99
+ /**
100
+ * "TWTR: Delisted Oct 27, 2022" when the listing's quote says it no longer
101
+ * trades: its history is served, ending on that session, and says so.
102
+ */
103
+ delisted?: string;
89
104
  /**
90
105
  * The change when the symbol's company trades under another symbol now
91
106
  * and its own quote shows nothing new listed under it (`movedAwayChange`):
@@ -156,9 +171,10 @@ export function describeListingFate(
156
171
  const abroad = !!quote && !!bareListingNote({ symbol, request: { exchange: listing.exchange ?? "" } }, quote);
157
172
  const current = tradingListing(quote, now);
158
173
  const change = abroad ? null : describeTickerChange(symbol, changes, current);
159
- const note = tickerChangeLine(change) ?? (endedOn ? `${symbol} stopped trading ${endedOn}; no history or options served` : null);
174
+ const delisted = quote?.delisting && !abroad ? `${symbol}: ${delistingLabel(quote.delisting)}` : undefined;
175
+ const note = tickerChangeLine(change) ?? delisted ?? (endedOn ? `${symbol} stopped trading ${endedOn}; no history or options served` : null);
160
176
  const formerIssuer = abroad ? null : describeFormerIssuer(symbol, changes, current);
161
- return { endedOn, change, note, moved: quote ? movedAwayChange(change) : null, formerIssuer };
177
+ return { endedOn, change, note, moved: quote ? movedAwayChange(change) : null, formerIssuer, ...(delisted ? { delisted } : {}) };
162
178
  }
163
179
 
164
180
  /**
@@ -200,7 +216,8 @@ export async function loadListingFate(
200
216
 
201
217
  /**
202
218
  * The lines a command says about a symbol's fate beside what it served. With
203
- * nothing served, `fate.note`. With data, `quote`'s change line when the
219
+ * nothing served, `fate.note`. With data, `fate.delisted` for a listing that
220
+ * no longer trades ("TWTR: Delisted Oct 27, 2022"), `quote`'s change line when the
204
221
  * symbol's company left it (FB's bars are the fund listed there now), and, for
205
222
  * bars from before the symbol's holder took it (META's from 2021), that they
206
223
  * are from its old symbol: "Includes FB-era bars: META was FB until 2022-06-09".
@@ -211,6 +228,8 @@ export function listingFateNotes(fate: ListingFate, served: { served: boolean; f
211
228
  const first = served.firstDate?.slice(0, 10);
212
229
  const earlier = change?.kind === "formerly" && first ? change.changes.filter((hop) => first < hop.date).map((hop) => hop.from) : [];
213
230
  return [
231
+ // What it served ends on the listing's last session, and says so.
232
+ fate.delisted ?? null,
214
233
  tickerChangeLine(change),
215
234
  earlier.length > 0 ? `Includes ${earlier.join("- and ")}-era bars: ${change!.note}` : null,
216
235
  ].filter((line): line is string => !!line);
@@ -38,6 +38,7 @@ import { readTerminalListingCode } from "../terminal-codes";
38
38
  import { isProviderBusy, ProviderBusyError, SEARCH_BUSY_MESSAGE } from "../../sources/provider-errors";
39
39
  import { settleWithin } from "../../utils/async-deadline";
40
40
  import { companyNameExtraWords } from "../company-name";
41
+ import { delistingLabel } from "../../market-data/quotes/delisting";
41
42
  export type {
42
43
  ResolvedTickerSearch,
43
44
  TickerSearchCandidate,
@@ -58,6 +59,17 @@ const MAX_NAMED_LISTINGS = 6;
58
59
  */
59
60
  export const LISTING_SEARCH_LIMIT = 30;
60
61
 
62
+ /**
63
+ * A listing's name as a choice between listings gives it: a listing that no
64
+ * longer trades says so and when ("First Republic Bank · Last traded Apr 28,
65
+ * 2023"), and drops a name that only repeats its symbol.
66
+ */
67
+ function listingChoiceName(result: InstrumentSearchResult): string {
68
+ if (!result.delisting) return result.name;
69
+ const name = result.name && normalizeTickerSymbol(result.name) !== normalizeTickerSymbol(result.symbol) ? result.name : "";
70
+ return [name, delistingLabel(result.delisting)].filter(Boolean).join(" · ");
71
+ }
72
+
61
73
  export class AmbiguousTickerError extends Error {
62
74
  constructor(readonly query: string, readonly listings: readonly string[], readonly listingNames: Readonly<Record<string, string>> = {}) {
63
75
  // Every listing is a choice: a list cut short could leave out the one meant.
@@ -467,7 +479,7 @@ async function resolveTickerSymbol(
467
479
  }
468
480
  const listings = new Set(matches.map((item) => publicTickerKey(item.symbol, listingExchange(item.result!))));
469
481
  const ambiguity = (query: string) => new AmbiguousTickerError(query, [...listings], Object.fromEntries(matches.map((item) => [
470
- publicTickerKey(item.symbol, listingExchange(item.result!)), item.result!.name,
482
+ publicTickerKey(item.symbol, listingExchange(item.result!)), listingChoiceName(item.result!),
471
483
  ])));
472
484
  if (pickerQuery && listings.size > 1) throw ambiguity(pickerQuery);
473
485
  // A code that names several venues (TSE) the symbol trades on more than one of is a choice.
@@ -485,7 +497,12 @@ async function resolveTickerSymbol(
485
497
  });
486
498
  const exchange = canonicalExchange(quote.listingExchangeName || quote.exchangeName);
487
499
  const currency = resolveCurrencyUnit(quote.currency).currency;
488
- if (exchange && currency && Number.isFinite(quote.price) && quote.price !== 0
500
+ const ended = quote.delisting;
501
+ if (ended) {
502
+ // The symbol's own quote is a US listing that no longer trades (a bare FRC, whose
503
+ // venue is not on record): it is the run search offers with the same last session.
504
+ verified = matches.filter((item) => item.result?.delisting?.lastTradeDate === ended.lastTradeDate);
505
+ } else if (exchange && currency && Number.isFinite(quote.price) && quote.price !== 0
489
506
  && Number.isFinite(quote.lastUpdated) && quote.lastUpdated > 0) {
490
507
  const sameSecurity = matches.filter((item) => {
491
508
  const result = item.result!;
@@ -663,7 +680,11 @@ async function searchProviderResults(
663
680
 
664
681
  // A sector word ("defense", which the server answers with rows tagged with it) lists its small companies by
665
682
  // name; the server orders "defense ETF" by size, which a name search does not. Those funds lead the list.
666
- const theme = withThemeFunds && !assetClass ? themeQueryWord([...byKey.values()], query) : null;
683
+ // So do they when the funds are asked for by name: "defense etf" searches "defense", whose answer holds
684
+ // only the few funds tagged with it (not ITA), and "gold miners etf" a phrase no symbol is.
685
+ const fundsAsked = assetClass?.code === "ETF" || assetClass?.code === "FUND";
686
+ const theme = withThemeFunds && (!assetClass || fundsAsked)
687
+ && (themeQueryWord([...byKey.values()], query) || (fundsAsked && /\s/.test(query.trim())));
667
688
  if (theme) {
668
689
  try {
669
690
  const { onPartial: _ignored, ...context } = searchContext ?? {};
@@ -123,6 +123,41 @@ export function resolvePriceComparison(
123
123
  };
124
124
  }
125
125
 
126
+ /**
127
+ * The bars a compared leg pans and zooms over: inside the view only its shared
128
+ * dates, as the chart shows them, and every loaded bar past either edge of the
129
+ * view. Clipping the whole buffer to the view left a pan or a zoom out nothing
130
+ * to move into until the next fetch, and none at all toward newer bars.
131
+ */
132
+ export function comparedBufferPoints(
133
+ points: readonly TimeSeriesPoint[],
134
+ shared: { start: number | null; end: number | null },
135
+ view: { start: number | null; end: number | null },
136
+ ): TimeSeriesPoint[] {
137
+ const { start, end } = shared;
138
+ if (start === null || end === null) return [];
139
+ const before = view.start === null ? Number.NEGATIVE_INFINITY : Math.min(start, view.start);
140
+ const after = view.end === null ? Number.POSITIVE_INFINITY : Math.max(end, view.end);
141
+ return points.filter((point) => {
142
+ const time = point.date.getTime();
143
+ return time < before || time > after || (time >= start && time <= end);
144
+ });
145
+ }
146
+
147
+ /** A compared leg's pan buffer around `view`; see comparedBufferPoints. */
148
+ export function bufferPriceComparison(
149
+ series: ResolvedSeries,
150
+ comparison: PriceComparison | null,
151
+ view: { start: number | null; end: number | null },
152
+ ): ResolvedSeries {
153
+ if (!comparison?.seriesIds.includes(series.id)) return series;
154
+ return {
155
+ ...series,
156
+ latestChangePercent: undefined,
157
+ points: comparedBufferPoints(series.points, priceComparisonBoundsForSeries(series, comparison)!, view),
158
+ };
159
+ }
160
+
126
161
  /** Clip only compared legs; raw observations and study inputs stay available in the source cache. */
127
162
  export function clipPriceComparison(series: ResolvedSeries, comparison: PriceComparison | null): ResolvedSeries {
128
163
  if (!comparison?.seriesIds.includes(series.id)) return series;
@@ -1,6 +1,7 @@
1
1
  import { logFailure } from "../utils/log-failure";
2
2
  import { resolveAssetDisplayKind } from "../market-data/market/format";
3
3
  import { hasValidQuoteObservationTime } from "../market-data/quotes/freshness";
4
+ import { delistingLabel } from "../market-data/quotes/delisting";
4
5
  import { SnapshotHistoryUnavailableError } from "../market-data/snapshot-provider";
5
6
  import { financialPeriodCoverage, financialPeriodCoverageWarnings, limitSeriesObservations } from "./financial-period-coverage";
6
7
  import { HistoryCoverageError, historyCoverageNotice } from "../sources/history-coverage";
@@ -10,6 +11,7 @@ import { getRouterEntityKey } from "../sources/provider-router/cache";
10
11
  import { publicListingTarget } from "../sources/listing-target";
11
12
  import { fetchHistoryResult } from "../sources/history-result";
12
13
  import type { HistorySession, PriceHistoryResult } from "../types/price-history";
14
+ import type { BrokerContractRef } from "../types/instrument";
13
15
  import { FINANCIAL_VINTAGE_NOTICE, SEC_EPS_BASIS_NOTICE } from "../utils/financial-statements";
14
16
  import { appendLiveQuotePoint, hasUnknownBondHistoryBasis } from "./chart-data";
15
17
  import { LiveBarAccumulator } from "./live-bars";
@@ -63,7 +65,7 @@ import {
63
65
  } from "./studies";
64
66
  import { applyResolvedSeriesTransform } from "./transforms";
65
67
  import { clipSeriesToWindow } from "./alignment";
66
- import { clipPriceComparison, priceComparisonBoundsForSeries, priceComparisonSeriesIds, priceObservationWindowFilter, resolvePriceComparison, type PriceComparison } from "./price-comparison";
68
+ import { bufferPriceComparison, clipPriceComparison, comparedBufferPoints, priceComparisonBoundsForSeries, priceComparisonSeriesIds, priceObservationWindowFilter, resolvePriceComparison, type PriceComparison } from "./price-comparison";
67
69
  import { reportingCurrencySeries } from "./reporting-currency";
68
70
  import { chartQuoteOverrideKeyForSource, compareChartQuoteRecency } from "./live-quotes";
69
71
  import { chartSeriesSourceKey } from "../capabilities/chart-series";
@@ -754,8 +756,8 @@ export function seedChartResolutionResult(
754
756
  const axisWarnings: string[] = [];
755
757
  const bufferedSeries = assignAxes([
756
758
  ...baseSeries,
757
- ...applyStudyPresentationTransforms(studies.series, spec.studies, series, comparisonBounds, undefined, priceComparison),
758
- ], [...spec.series, ...spec.studies], axisWarnings).map((entry) => clipPriceComparison(entry, priceComparison));
759
+ ...applyStudyPresentationTransforms(studies.series, spec.studies, series, comparisonBounds, undefined, priceComparison, bounds),
760
+ ], [...spec.series, ...spec.studies], axisWarnings).map((entry) => bufferPriceComparison(entry, priceComparison, bounds));
759
761
  const visible = bufferedSeries.map((entry) => {
760
762
  const entryBounds = presentationBounds(entry, spec.studies, priceComparison, bounds);
761
763
  const clipped = entryBounds.start !== null && entryBounds.end !== null
@@ -1168,6 +1170,17 @@ async function loadPriceHistory(
1168
1170
  return { ...result, expiresAt: retryAt ?? Date.now() + HISTORY_RETENTION_MAX_AGE_MS };
1169
1171
  }
1170
1172
 
1173
+ /**
1174
+ * A US stock or fund listing, including one a broker contract routes through
1175
+ * SMART in dollars. Options and futures have no pre-market session.
1176
+ */
1177
+ function tradesUsExtendedHours(exchange: string | undefined, contract: BrokerContractRef | null | undefined): boolean {
1178
+ if (contract?.secType && !["STK", "ETF"].includes(contract.secType.toUpperCase())) return false;
1179
+ const venue = exchange || contract?.primaryExchange || contract?.exchange;
1180
+ return isUsListingExchange(venue) || isUsListingExchange(contract?.primaryExchange)
1181
+ || (!!contract && canonicalExchange(venue) === "SMART" && (!contract.currency || contract.currency.toUpperCase() === "USD"));
1182
+ }
1183
+
1171
1184
  function baseSecuritySeries(
1172
1185
  spec: ChartSeriesSpec,
1173
1186
  financials: TickerFinancials,
@@ -1185,7 +1198,8 @@ function baseSecuritySeries(
1185
1198
  const symbol = instrumentLabel(spec.source);
1186
1199
  const statementCurrency = field.id.startsWith("fundamental.") && field.unit.startsWith("currency")
1187
1200
  ? reportingCurrencySeries(points, financials.financialCurrency) : null;
1188
- const currency = statementCurrency ? statementCurrency.currency : financials.quote?.currency || quoteMetadata?.currency;
1201
+ // A broker contract names its currency even before (or without) a quote.
1202
+ const currency = statementCurrency ? statementCurrency.currency : financials.quote?.currency || quoteMetadata?.currency || spec.source.instrument.instrument?.currency;
1189
1203
  const assetKind = resolveAssetDisplayKind({ assetCategory: financials.quote?.instrumentType || quoteMetadata?.instrumentType });
1190
1204
  const unknownBondBasis = hasUnknownBondHistoryBasis(financials.quote,
1191
1205
  spec.source.instrument.instrument?.secType, quoteMetadata?.instrumentType);
@@ -1220,10 +1234,25 @@ function baseSecuritySeries(
1220
1234
  || financials.quote?.listingExchangeName
1221
1235
  || financials.quote?.exchangeName;
1222
1236
  const marketField = isMarketFieldId(spec.source.fieldId);
1237
+ // A routing code such as a broker's SMART names no venue. The quote's
1238
+ // listing then supplies the session clock, so closed hours stay off the axis.
1239
+ const sessionExchange = [marketExchange, financials.quote?.listingExchangeName, quoteMetadata?.listingExchangeName, financials.quote?.exchangeName]
1240
+ .find((value) => resolveExchangeTimeZone(value));
1241
+ // A dollar listing saved with IBKR's SMART route and no other venue trades on
1242
+ // the US session, and so does a dollar option, which names no venue at all.
1243
+ const dollars = !currency || currency === "USD";
1244
+ const usOption = dollars && (spec.source.instrument.instrument?.secType?.toUpperCase() === "OPT"
1245
+ || /^OPTION$/i.test(financials.quote?.instrumentType ?? quoteMetadata?.instrumentType ?? ""));
1246
+ const routedUsListing = !sessionExchange && ((canonicalExchange(marketExchange) === "SMART" && dollars) || usOption);
1223
1247
  const marketTimeZone = marketField
1224
1248
  && canonicalExchange(marketExchange) !== "CCC"
1225
- ? resolveExchangeTimeZone(marketExchange)
1249
+ ? resolveExchangeTimeZone(sessionExchange) ?? (routedUsListing ? "America/New_York" : null)
1226
1250
  : null;
1251
+ // A route has no hours of its own: its sessions, which the day view and
1252
+ // VWAP follow, are the venue's, and NYSE's for an unnamed dollar listing.
1253
+ const listingExchange = canonicalExchange(marketExchange) === "SMART" || usOption
1254
+ ? canonicalExchange(sessionExchange) || (routedUsListing ? "NYSE" : marketExchange)
1255
+ : marketExchange;
1227
1256
  const latestChangePercent = marketField && field.unit.startsWith("currency")
1228
1257
  && !unknownBondBasis
1229
1258
  && financials.quote && hasValidQuoteObservationTime(financials.quote)
@@ -1259,12 +1288,12 @@ function baseSecuritySeries(
1259
1288
  panelId: spec.panelId,
1260
1289
  interpolation: spec.interpolation,
1261
1290
  observationKind: marketField ? "market" : undefined,
1262
- ...(marketField ? { listing: { symbol: spec.source.instrument.symbol, exchange: marketExchange ?? "" } } : {}),
1291
+ ...(marketField ? { listing: { symbol: spec.source.instrument.symbol, exchange: listingExchange ?? "" } } : {}),
1263
1292
  timeBasis: marketTimeZone
1264
1293
  ? {
1265
1294
  kind: "market",
1266
1295
  timeZone: marketTimeZone,
1267
- exchange: canonicalExchange(marketExchange) || undefined,
1296
+ exchange: canonicalExchange(sessionExchange) || undefined,
1268
1297
  cadenceMs: marketResolution
1269
1298
  ? CHART_RESOLUTION_STEP_MS[marketResolution]
1270
1299
  : undefined,
@@ -1487,6 +1516,7 @@ function applyStudyPresentationTransforms(
1487
1516
  visibleBounds: DateBounds,
1488
1517
  fallbackBaselineBounds?: DateBounds,
1489
1518
  priceComparison?: PriceComparison | null,
1519
+ view: DateBounds = visibleBounds,
1490
1520
  ): ResolvedSeries[] {
1491
1521
  const rawById = new Map(rawSeries.map((series) => [series.id, series] as const));
1492
1522
  return outputs.map((output) => {
@@ -1502,7 +1532,7 @@ function applyStudyPresentationTransforms(
1502
1532
  : undefined;
1503
1533
  const transformed = applyResolvedSeriesTransform(output, input.transform, { baseline });
1504
1534
  const inputBounds = priceComparison?.sourceBounds?.[input.id];
1505
- return inputBounds ? { ...transformed, points: filterPoints(transformed.points, inputBounds) } : transformed;
1535
+ return inputBounds ? { ...transformed, points: comparedBufferPoints(transformed.points, inputBounds, view) } : transformed;
1506
1536
  });
1507
1537
  }
1508
1538
 
@@ -1612,6 +1642,7 @@ async function resolveChartSpecPass(
1612
1642
  && entry.source.kind === "security" && isFundamentalFieldId(entry.source.fieldId))
1613
1643
  ? [FINANCIAL_VINTAGE_NOTICE] : [];
1614
1644
  const priorityWarnings: string[] = [];
1645
+ const delistings = new Map<string, { symbol: string; label: string; notice: string }>();
1615
1646
  const baseSeriesIds = new Set(spec.series.map((entry) => entry.id));
1616
1647
  const visibleSeriesIds = new Set(spec.series
1617
1648
  .filter((entry) => entry.visible !== false)
@@ -1813,10 +1844,10 @@ async function resolveChartSpecPass(
1813
1844
  const hasExplicitWindow = explicitBounds(spec) !== null || movedWindow;
1814
1845
  const initialCalculationBounds = calculationBounds(spec, requestVisibleBounds, initialResolution);
1815
1846
 
1816
- // Only US listings trade before and after the regular session; asking elsewhere costs a request for nothing.
1847
+ // Only US stocks and funds trade before and after the regular session; asking elsewhere costs a request for nothing.
1817
1848
  const extendedHoursFor = (source: Extract<ChartSeriesSpec["source"], { kind: "security" }>, resolution: ManualChartResolution) => (
1818
1849
  spec.viewport.extendedHours === true && isMarketFieldId(source.fieldId) && isIntradayResolution(resolution)
1819
- && !source.instrument.instrument && isUsListingExchange(source.instrument.exchange)
1850
+ && tradesUsExtendedHours(source.instrument.exchange, source.instrument.instrument)
1820
1851
  );
1821
1852
  // A history starts after the visible window because its source keeps no more.
1822
1853
  let retainedHistoryOnly = false;
@@ -1952,6 +1983,14 @@ async function resolveChartSpecPass(
1952
1983
  const history = loaded.points;
1953
1984
  const coverageNotice = historyCoverageNotice(loaded.coverageStart, request.visibleBounds.start);
1954
1985
  if (coverageNotice) priorityWarnings.push(coverageNotice);
1986
+ // A listing that no longer trades is named so beside its bars, which end on its last session.
1987
+ if (loaded.delisting) {
1988
+ const symbol = parsePublicTickerKey(source.instrument.symbol).symbol;
1989
+ const label = delistingLabel(loaded.delisting);
1990
+ const notice = `${symbol}: ${label}`;
1991
+ priorityWarnings.push(notice);
1992
+ if (!delistings.has(notice)) delistings.set(notice, { symbol, label, notice });
1993
+ }
1955
1994
  // Read per request: one recovered window answers every visible window that shares its acquisition.
1956
1995
  if (loaded.recovery && request.visibleBounds.start !== null && request.visibleBounds.start < loaded.recovery.start) {
1957
1996
  priorityWarnings.push(historyRetentionNotice(loaded.recovery.retention, loaded.resolution ?? request.resolution,
@@ -2294,6 +2333,7 @@ async function resolveChartSpecPass(
2294
2333
  comparisonBounds,
2295
2334
  priceComparison ? undefined : requestVisibleBounds,
2296
2335
  priceComparison,
2336
+ bounds,
2297
2337
  ),
2298
2338
  ];
2299
2339
  warnings.push(...studyResult.warnings);
@@ -2301,7 +2341,7 @@ async function resolveChartSpecPass(
2301
2341
  }
2302
2342
 
2303
2343
  const bufferedSeries = assignAxes(adoptPriceUnits(resolved, spec.series), [...spec.series, ...spec.studies], warnings)
2304
- .map((entry) => clipPriceComparison(entry, priceComparison));
2344
+ .map((entry) => bufferPriceComparison(entry, priceComparison, bounds));
2305
2345
  resolved = bufferedSeries.map((entry) => {
2306
2346
  const entryBounds = presentationBounds(entry, spec.studies, priceComparison, bounds);
2307
2347
  return entryBounds.start !== null && entryBounds.end !== null
@@ -2388,6 +2428,7 @@ async function resolveChartSpecPass(
2388
2428
  loading: false,
2389
2429
  errors,
2390
2430
  warnings: [...new Set([...priorityWarnings, ...warnings])],
2431
+ ...(delistings.size ? { delistings: [...delistings.values()] } : {}),
2391
2432
  viewport,
2392
2433
  resolution,
2393
2434
  };
@@ -85,7 +85,7 @@ export function coerceSeriesInterpolationForStyle(
85
85
  return style === "step" ? "step-after" : "none";
86
86
  }
87
87
 
88
- export const MAX_CHART_SERIES = 10;
88
+ export const MAX_CHART_SERIES = 20;
89
89
 
90
90
  export const DEFAULT_CHART_SPEC: ChartSpec = Object.freeze({
91
91
  version: CHART_SPEC_VERSION,
@@ -281,6 +281,11 @@ export interface ChartResolutionResult {
281
281
  loading: boolean;
282
282
  errors: string[];
283
283
  warnings: string[];
284
+ /**
285
+ * Each charted listing that no longer trades: its symbol and label
286
+ * ("Delisted Oct 27, 2022"), which `warnings` carries as "TWTR: Delisted Oct 27, 2022".
287
+ */
288
+ delistings?: { symbol: string; label: string; notice: string }[];
284
289
  /** Effective inclusive bounds used to clip the resolved chart data. */
285
290
  viewport?: { start: Date; end: Date };
286
291
  /** Bar resolution the loaded market history was fetched at. */
@@ -11,6 +11,7 @@ import type {
11
11
  BrokerOrderPreview,
12
12
  BrokerOrderRequest,
13
13
  BrokerPortfolioPerformance,
14
+ BrokerTradingCapabilities,
14
15
  } from "./trading";
15
16
  import type { CachePolicy, CachePolicyMap } from "./persistence";
16
17
 
@@ -85,10 +86,44 @@ export interface BrokerProfileAction {
85
86
  disabledReason?: string;
86
87
  }
87
88
 
89
+ /** How a chart asks a broker for history beyond the regular session. */
90
+ export interface BrokerHistoryOptions {
91
+ /** Include pre-market and after-hours bars. Only a broker that sets `extendedHoursHistory` is asked. */
92
+ session?: "extended";
93
+ }
94
+
95
+ /** What a broker's order stream calls. */
96
+ export interface BrokerOrderListener {
97
+ /**
98
+ * An order as the broker now reports it: placed, partly or wholly filled,
99
+ * replaced, cancelled, rejected or expired. The same `orderId` names the
100
+ * order through all of them, as `getOrderStatus` and `cancelOrder` take it.
101
+ */
102
+ order(order: BrokerOrder): void;
103
+ /**
104
+ * Whether changes are being pushed now. While false, as before the first
105
+ * call, the host polls open orders instead; on true it reconciles once.
106
+ */
107
+ live?(live: boolean): void;
108
+ }
109
+
88
110
  export interface BrokerAdapter {
89
111
  readonly id: string;
90
112
  readonly name: string;
91
113
  readonly cachePolicy?: CachePolicyMap;
114
+ /**
115
+ * Intraday history includes pre-market and after-hours bars when its
116
+ * `BrokerHistoryOptions` ask for the extended session. The chart then
117
+ * starts the day at the pre-market open; without it, the regular session.
118
+ */
119
+ readonly extendedHoursHistory?: boolean;
120
+ /**
121
+ * The quotes and bars this broker supplies stay on the user's device: Gloom Cloud sync never
122
+ * uploads them, for a broker whose terms restrict redistributing its market data. Its accounts
123
+ * sync like any broker's, unless the user keeps a connection on the device (`localOnly` on the
124
+ * profile). Hosts that honor it export `LOCAL_BROKER_MARKET_DATA` from `gloomberb/broker`.
125
+ */
126
+ readonly localMarketData?: boolean;
92
127
  validate(instance: BrokerInstanceConfig): Promise<boolean>;
93
128
  importPositions(instance: BrokerInstanceConfig): Promise<BrokerPosition[]>;
94
129
  importPortfolioSnapshot?(instance: BrokerInstanceConfig): Promise<{ accounts: BrokerAccount[]; positions: BrokerPosition[] }>;
@@ -116,9 +151,10 @@ export interface BrokerAdapter {
116
151
  bufferRange: TimeRange,
117
152
  resolution: ManualChartResolution,
118
153
  instrument?: BrokerContractRef | null,
154
+ options?: BrokerHistoryOptions,
119
155
  ): Promise<PricePoint[]>;
120
156
  /** Fetch higher-resolution price data for a specific date window (e.g. when zoomed in). */
121
- getDetailedPriceHistory?(ticker: string, instance: BrokerInstanceConfig, exchange: string, startDate: Date, endDate: Date, barSize: string, instrument?: BrokerContractRef | null): Promise<PricePoint[]>;
157
+ getDetailedPriceHistory?(ticker: string, instance: BrokerInstanceConfig, exchange: string, startDate: Date, endDate: Date, barSize: string, instrument?: BrokerContractRef | null, options?: BrokerHistoryOptions): Promise<PricePoint[]>;
122
158
  getChartResolutionSupport?(
123
159
  ticker: string,
124
160
  instance: BrokerInstanceConfig,
@@ -136,6 +172,8 @@ export interface BrokerAdapter {
136
172
  * Whether this profile can stream quotes at all. Return false for modes that
137
173
  * only sync statements, so their positions stream from the cloud instead.
138
174
  * Quotes are also only routed to a broker whose status is "connected".
175
+ * Answer from the profile's configuration, not from session state: the
176
+ * desktop app asks in its view, while the session lives in the backend.
139
177
  */
140
178
  canStreamQuotes?(instance: BrokerInstanceConfig): boolean;
141
179
  subscribeQuotes?(
@@ -144,8 +182,25 @@ export interface BrokerAdapter {
144
182
  onQuote: (target: QuoteSubscriptionTarget, quote: Quote) => void,
145
183
  ): () => void;
146
184
  listOpenOrders?(instance: BrokerInstanceConfig): Promise<BrokerOrder[]>;
185
+ /**
186
+ * Pushes order changes as the broker reports them, for every account the
187
+ * profile includes. The host baselines from `listOpenOrders` and polls it
188
+ * while `live` is false or never called; see BrokerOrderListener.
189
+ */
190
+ subscribeOrders?(instance: BrokerInstanceConfig, listener: BrokerOrderListener): () => void;
191
+ /** Explicit status, including terminal orders. Null means unconfirmed, never cancelled by absence. */
192
+ getOrderStatus?(instance: BrokerInstanceConfig, orderId: number): Promise<BrokerOrder | null>;
147
193
  listExecutions?(instance: BrokerInstanceConfig): Promise<BrokerExecution[]>;
194
+ /** Opt into the shared ticket with supported combinations and current trading permission. */
195
+ getTradingCapabilities?(instance: BrokerInstanceConfig, contract?: BrokerContractRef, orderType?: BrokerOrderRequest["orderType"]): BrokerTradingCapabilities;
196
+ /** Config patch for the host's explicit enable/disable trading confirmation. Must not perform I/O. */
197
+ getTradingConfigUpdate?(instance: BrokerInstanceConfig, enabled: boolean): Record<string, unknown>;
148
198
  previewOrder?(instance: BrokerInstanceConfig, request: BrokerOrderRequest): Promise<BrokerOrderPreview>;
199
+ /**
200
+ * Place, change and cancel throw when the broker did not take the action. An error with `refused: true` says the
201
+ * broker answered no, or nothing was sent: the order is as it was, and its message is one short line the app shows as
202
+ * it is ("Aries cannot find this order"). Any other error is a lost answer, whose outcome the app treats as unknown.
203
+ */
149
204
  placeOrder?(instance: BrokerInstanceConfig, request: BrokerOrderRequest): Promise<BrokerOrder>;
150
205
  modifyOrder?(instance: BrokerInstanceConfig, orderId: number, request: BrokerOrderRequest): Promise<BrokerOrder>;
151
206
  cancelOrder?(instance: BrokerInstanceConfig, orderId: number): Promise<void>;
@@ -18,6 +18,12 @@ export interface BrokerInstanceConfig {
18
18
  config: Record<string, unknown>;
19
19
  enabled?: boolean;
20
20
  lastSyncedAt?: number;
21
+ /**
22
+ * The user keeps this connection's data on this device: Cloud sync leaves out the profile, its
23
+ * accounts' portfolios and account values, and the positions in them. Daily roundups, the weekly
24
+ * brief, position alerts, the mobile app and Ask Gloom's portfolio context then go without it.
25
+ */
26
+ localOnly?: boolean;
21
27
  }
22
28
 
23
29
  export interface ColumnConfig {
@@ -119,7 +119,11 @@ export interface MarketDataRequestContext {
119
119
  brokerId?: string;
120
120
  brokerInstanceId?: string;
121
121
  instrument?: BrokerContractRef | null;
122
- cacheMode?: "default" | "refresh";
122
+ /**
123
+ * "refresh" always fetches. "current" refetches a stored value past its stale time before answering, and
124
+ * answers with it only if that fetch fails: for a caller that cannot see a refresh landing in the background.
125
+ */
126
+ cacheMode?: "default" | "refresh" | "current";
123
127
  statementHistory?: "extended";
124
128
  /** A validated, same-target detailed retry constrained to one exhausted source. */
125
129
  historyRecovery?: import("../sources/history-retention").HistoryRecoveryCandidate;
@@ -34,6 +34,20 @@ export interface QuoteProvenance {
34
34
  rejectedPriceProviders?: string[];
35
35
  }
36
36
 
37
+ /**
38
+ * A US listing that no longer trades, as Gloom Cloud states it: TWTR, last
39
+ * traded 2022-10-27 and removed from NYSE the next day. Its daily history is
40
+ * still served, ending on that session. Dates are New York trading dates.
41
+ */
42
+ export interface ListingDelisting {
43
+ /** "delisted": the exchange's removal of the listing is on record; "inactive": only its last trade is known. */
44
+ status: "delisted" | "inactive";
45
+ /** The last session with a trade (YYYY-MM-DD). */
46
+ lastTradeDate: string;
47
+ /** The first session served (YYYY-MM-DD). */
48
+ historyStart: string;
49
+ }
50
+
37
51
  /** Static listing facts from a quote observation, without an investable price. */
38
52
  export interface QuoteMetadataSource {
39
53
  providerId?: string;
@@ -81,6 +95,13 @@ export interface Quote extends DepositaryReceiptFacts {
81
95
  providerPriceDivisor?: number;
82
96
  change: number;
83
97
  changePercent: number;
98
+ /**
99
+ * What change and changePercent measure from when it is not previousClose,
100
+ * as Gloom Cloud says of a coin whose previous close it cannot date to the
101
+ * last two days: the last 24 hours ("24h", previousClose absent), or
102
+ * nothing ("none", no move). Absent on every other quote.
103
+ */
104
+ changeBasis?: "24h" | "none";
84
105
  previousClose?: number;
85
106
  /** Official close of a completed regular session, in this quote's currency. */
86
107
  regularClose?: number;
@@ -100,6 +121,8 @@ export interface Quote extends DepositaryReceiptFacts {
100
121
  receivedAt?: number; // local receipt timestamp ms for streamed/display freshness
101
122
  delivery?: QuoteDelivery;
102
123
  stale?: boolean;
124
+ /** The listing no longer trades: the price is its last trade (ListingDelisting). */
125
+ delisting?: ListingDelisting;
103
126
  exchangeName?: string;
104
127
  fullExchangeName?: string;
105
128
  listingExchangeName?: string;
@@ -103,6 +103,8 @@ export interface HeadlessPaneContext {
103
103
  /** Effective instance settings, after template creation and option normalization. */
104
104
  settings?: Record<string, unknown>;
105
105
  capabilities?: CapabilityInvoker;
106
+ /** Locally registered broker adapters. Reports may read account data; they never submit orders. */
107
+ resolveBroker?: (brokerType: string) => import("./broker").BrokerAdapter | null;
106
108
  /** Read one local portfolio without passing holdings through a remote endpoint. */
107
109
  resolvePortfolio?: (id: string) => Promise<{
108
110
  portfolio: import("./ticker").Portfolio;
@@ -131,6 +133,12 @@ export interface HeadlessPaneColumn {
131
133
  header: string;
132
134
  /** Print `header` as written, a field name such as `source`, where reports would otherwise title-case it. */
133
135
  literalHeader?: boolean;
136
+ /**
137
+ * What the text report heads the column with when it can say more than an
138
+ * export's header should (`Chg % UTC` over `Chg %`), printed as written;
139
+ * CSV keeps `header`, so a column read by its name stays where it was.
140
+ */
141
+ textHeader?: string;
134
142
  align?: "left" | "right" | "center";
135
143
  width?: number;
136
144
  /** False keeps the column whole when the text table is fitted to a narrow width, so a time or an id is never cut. */
@@ -229,9 +237,11 @@ export interface HeadlessPaneFreshness {
229
237
  /**
230
238
  * How often the data is published, when its dates mark the start of a period
231
239
  * (a monthly statistic is dated the 1st). The report then names the month,
232
- * quarter or year, not a day it was never observed on.
240
+ * quarter or year, not a day it was never observed on. A semiannual estimate
241
+ * is dated by the month it was made in (USDA's coffee and sugar balances),
242
+ * so it names that month.
233
243
  */
234
- periodicity?: "monthly" | "quarterly" | "annual";
244
+ periodicity?: "monthly" | "quarterly" | "semiannual" | "annual";
235
245
  /**
236
246
  * The newest row is a weekly or monthly bar whose period had not ended when
237
247
  * it was served (October's bar on 16 Oct), and the last day it covers so
@@ -1,3 +1,5 @@
1
+ import type { ListingDelisting } from "./financials";
2
+
1
3
  /**
2
4
  * What a ticker is, at the grain that decides which research has data for it:
3
5
  * filings, executives and analyst coverage exist for a company's stock,
@@ -77,4 +79,6 @@ export interface InstrumentSearchResult {
77
79
  * carries that name. A leftover listing of the merged company (GOGLC on
78
80
  * BYMA) is linked to it by search, naming the `successor`. */
79
81
  formerIssuer?: { symbol: string; name: string; until: string; successor?: { symbol: string; name: string | null } };
82
+ /** A US listing that no longer trades, found by its exact symbol (TWTR). Untrusted until parsed. */
83
+ delisting?: ListingDelisting;
80
84
  }