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
@@ -4,147 +4,214 @@ import { formatNumber } from "../../../utils/format";
4
4
 
5
5
  export const PRIMARY_DEALERS_PANE_ID = "primary-dealers";
6
6
 
7
- /** Aggregate totals only. Tenor and CUSIP buckets stay off the wire. */
8
- const SERIES_CAP = 12;
9
-
10
- /**
11
- * The current release names settlement fails PDFTD (deliver) and PDFTR
12
- * (receive). PDFAIL and PDFAS are included when a break still publishes them.
13
- */
14
- const FAIL_PREFIXES = ["PDFAIL", "PDFAS", "PDFTD", "PDFTR"] as const;
15
-
16
7
  export type DealerTab = "positions" | "fails";
17
8
 
18
- export interface CatalogEntry {
19
- seriesBreak: string;
20
- keyid: string;
21
- description: string;
9
+ /** One observation of a series. */
10
+ export interface SeriesPoint {
11
+ asOf: string;
12
+ value: number;
22
13
  }
23
14
 
24
- export interface SeriesSelection {
25
- seriesBreak: string;
26
- positions: CatalogEntry[];
27
- fails: CatalogEntry[];
15
+ /** What the "latest" read holds per series: its newest observation. */
16
+ export type LatestBySeries = ReadonlyMap<string, SeriesPoint>;
17
+
18
+ /** A row of the board: one series, or the sum of several that the release does not total. */
19
+ interface RowDef {
20
+ id: string;
21
+ label: string;
22
+ tab: DealerTab;
23
+ /** 1 for a breakdown of the total above it. */
24
+ depth: 0 | 1;
25
+ /** The levels the row sums, in the release's series ids. */
26
+ keys: readonly string[];
28
27
  }
29
28
 
30
29
  export interface DealerRow {
31
- keyid: string;
30
+ id: string;
32
31
  label: string;
33
32
  tab: DealerTab;
33
+ depth: 0 | 1;
34
+ /** The total a breakdown row belongs to; null for a total. */
35
+ parent: string | null;
36
+ /** The release's series behind the row; its history is their sum. */
37
+ keys: readonly string[];
34
38
  asOf: string | null;
35
39
  latest: number | null;
36
40
  previous: number | null;
37
41
  change: number | null;
38
42
  }
39
43
 
40
- function seriesBreakYear(seriesBreak: string): number {
41
- const match = /(\d{4})$/.exec(seriesBreak);
42
- return match ? Number(match[1]) : 0;
43
- }
44
+ const total = (id: string, label: string, ...keys: string[]): RowDef => ({ id, label, tab: "positions", depth: 0, keys });
45
+ const part = (id: string, label: string, ...keys: string[]): RowDef => ({ id, label, tab: "positions", depth: 1, keys });
44
46
 
45
- function newestSeriesBreak(entries: readonly CatalogEntry[]): string | null {
46
- let best: string | null = null;
47
- let bestYear = -1;
48
- for (const entry of entries) {
49
- const year = seriesBreakYear(entry.seriesBreak);
50
- if (best == null || year > bestYear || (year === bestYear && entry.seriesBreak > best)) {
51
- best = entry.seriesBreak;
52
- bestYear = year;
53
- }
54
- }
55
- return best;
56
- }
47
+ /**
48
+ * The positions the board shows, in reading order: a total, then what it is
49
+ * made of. Every total is the exact sum of its breakdown in each week of the
50
+ * release (the Fed's own description of the Treasury total says it leaves TIPS
51
+ * out, and its numbers include them), so the breakdown always adds up.
52
+ */
53
+ const POSITION_ROWS: readonly RowDef[] = [
54
+ total("treasuries", "Treasuries", "PDPOSGST-TOT"),
55
+ part("bills", "Bills", "PDPOSGS-B"),
56
+ part("coupons-0-2", "Coupons 0-2y", "PDPOSGSC-L2"),
57
+ part("coupons-2-3", "Coupons 2-3y", "PDPOSGSC-G2L3"),
58
+ part("coupons-3-6", "Coupons 3-6y", "PDPOSGSC-G3L6"),
59
+ part("coupons-6-7", "Coupons 6-7y", "PDPOSGSC-G6L7"),
60
+ part("coupons-7-11", "Coupons 7-11y", "PDPOSGSC-G7L11"),
61
+ part("coupons-11-21", "Coupons 11-21y", "PDPOSGSC-G11L21"),
62
+ part("coupons-21", "Coupons 21y+", "PDPOSGSC-G21"),
63
+ part("tips", "TIPS", "PDPOSTIPS-L2", "PDPOSTIPS-G2", "PDPOSTIPS-G6L11", "PDPOSTIPS-G11"),
64
+ part("frn", "FRN", "PDPOSGS-BFRN"),
65
+ total("agency", "Agency & GSE", "PDPOSFGS-TOT"),
66
+ part("agency-coupons", "Agency coupons", "PDPOSFGS-C"),
67
+ part("discount-notes", "Discount notes", "PDPOSFGS-DN"),
68
+ total("mbs", "MBS", "PDPOSMBS-TOT"),
69
+ part("tba", "TBA", "PDPOSMBSFGS-TBA"),
70
+ part("pools", "Specified pools", "PDPOSMBSFGS-ST"),
71
+ part("agency-resi", "Other agency MBS", "PDPOSMBSFGS-OR"),
72
+ part("agency-cmbs", "Agency CMBS", "PDPOSMBSFGS-C"),
73
+ part("nonagency-resi", "Non-agency residential", "PDPOSMBSNA-R"),
74
+ part("nonagency-cmbs", "Non-agency CMBS", "PDPOSMBSNA-O"),
75
+ total("corporates", "Corporates", "PDPOSCS-TOT"),
76
+ part("ig", "IG bonds", "PDPOSCSBND-L13", "PDPOSCSBND-G13", "PDPOSCSBND-G5L10", "PDPOSCSBND-G10"),
77
+ part("hy", "HY bonds", "PDPOSCSBND-BELL13", "PDPOSCSBND-BELG13", "PDPOSCSBND-BELG5L10", "PDPOSCSBND-BELG10"),
78
+ part("cp", "Commercial paper", "PDPOSCSCP"),
79
+ total("munis", "Munis", "PDPOSSMGO-TOT"),
80
+ total("abs", "ABS", "PDPOSABS-TOT"),
81
+ ];
57
82
 
58
- function isWeeklyChange(entry: CatalogEntry): boolean {
59
- return /change from previous/i.test(entry.description) || /C$/i.test(entry.keyid);
60
- }
83
+ /**
84
+ * Settlement fails: deliver, then receive, for each asset, as the release
85
+ * names them (PDFTD and PDFTR). Corporate and agency-debt fails stay off: they
86
+ * are a different order of size and not what the tab is read for.
87
+ */
88
+ const FAIL_ASSETS = [
89
+ ["USTET", "Treasuries ex-TIPS"],
90
+ ["UST", "TIPS"],
91
+ ["FGM", "Agency MBS"],
92
+ ["OM", "Other MBS"],
93
+ ] as const;
61
94
 
62
- function describesTreasury(description: string): boolean {
63
- return /\bTREASURY\b/i.test(description);
64
- }
95
+ const FAIL_ROWS: readonly RowDef[] = FAIL_ASSETS.flatMap(([asset, label]) => [
96
+ { id: `PDFTD-${asset}`, label: `${label}, fails to deliver`, tab: "fails" as const, depth: 0 as const, keys: [`PDFTD-${asset}`] },
97
+ { id: `PDFTR-${asset}`, label: `${label}, fails to receive`, tab: "fails" as const, depth: 0 as const, keys: [`PDFTR-${asset}`] },
98
+ ]);
65
99
 
66
- function describesMbs(description: string): boolean {
67
- const text = description.replace(/excluding mbs/gi, "");
68
- return /\bMBS\b/i.test(text) || /mortgage/i.test(text);
69
- }
100
+ /** Every series a board needs, with the series that holds each one's change from the week before. */
101
+ export const changeSeriesId = (key: string): string => `${key}C`;
70
102
 
71
- function isAggregateTotal(entry: CatalogEntry): boolean {
72
- if (isWeeklyChange(entry)) return false;
73
- return entry.keyid.toUpperCase().endsWith("-TOT") || /^\s*total\b/i.test(entry.description);
103
+ /** Sums with the float noise of fractional $mn figures rounded away. */
104
+ function sum(values: readonly number[]): number {
105
+ return Math.round(values.reduce((all, value) => all + value, 0) * 1e6) / 1e6;
74
106
  }
75
107
 
76
- function isExcludedAsset(entry: CatalogEntry): boolean {
77
- const id = entry.keyid.toUpperCase();
78
- if (id.endsWith("-CS") || id.endsWith("-CSC") || id.endsWith("-FGEM") || id.endsWith("-FGEMC")) return true;
79
- const text = entry.description.toUpperCase();
80
- if (/\bCORPORATE\b|\bMUNICIPAL\b|\bASSET-BACKED\b|\bEQUITIES\b|\bEQUITY\b/.test(text)) return true;
81
- return /EXCLUDING MBS/.test(text) && !/\bTREASURY\b/.test(text);
108
+ function parentLabels(defs: readonly RowDef[]): Map<string, string | null> {
109
+ const parents = new Map<string, string | null>();
110
+ let current: string | null = null;
111
+ for (const def of defs) {
112
+ if (def.depth === 0) current = def.label;
113
+ parents.set(def.id, def.depth === 0 ? null : current);
114
+ }
115
+ return parents;
82
116
  }
83
117
 
84
- function isPositionAggregate(entry: CatalogEntry): boolean {
85
- if (!entry.keyid.toUpperCase().startsWith("PDPOS")) return false;
86
- if (!isAggregateTotal(entry)) return false;
87
- return describesTreasury(entry.description) || describesMbs(entry.description);
118
+ /**
119
+ * The board from the latest observation of every series. A row needs every
120
+ * series behind it, all from the same week (a sum with a part missing or from
121
+ * another week would be a different number), and is left out otherwise. Its
122
+ * change is the sum of the series' own published changes, equal to this week
123
+ * minus last week, and is empty when one of them is suppressed or from another
124
+ * week.
125
+ */
126
+ export function buildDealerRows(latest: LatestBySeries): DealerRow[] {
127
+ const defs = [...POSITION_ROWS, ...FAIL_ROWS];
128
+ const parents = parentLabels(defs);
129
+ const rows: DealerRow[] = [];
130
+ for (const def of defs) {
131
+ const levels = def.keys.map((key) => latest.get(key));
132
+ const asOf = levels[0]?.asOf;
133
+ if (!asOf || levels.some((point) => point?.asOf !== asOf)) continue;
134
+ const changes = def.keys.map((key) => latest.get(changeSeriesId(key)));
135
+ const level = sum(levels.map((point) => point!.value));
136
+ const change = changes.every((point) => point?.asOf === asOf) ? sum(changes.map((point) => point!.value)) : null;
137
+ rows.push({
138
+ id: def.id,
139
+ label: def.label,
140
+ tab: def.tab,
141
+ depth: def.depth,
142
+ parent: parents.get(def.id) ?? null,
143
+ keys: def.keys,
144
+ asOf,
145
+ latest: level,
146
+ previous: change != null ? sum([level, -change]) : null,
147
+ change,
148
+ });
149
+ }
150
+ return rows;
88
151
  }
89
152
 
90
- function isFailKey(keyid: string): boolean {
91
- const id = keyid.toUpperCase();
92
- return FAIL_PREFIXES.some((prefix) => id.startsWith(prefix));
153
+ export function boardAsOf(rows: readonly DealerRow[]): string | null {
154
+ let best: string | null = null;
155
+ for (const row of rows) {
156
+ if (row.asOf && (best == null || row.asOf > best)) best = row.asOf;
157
+ }
158
+ return best;
93
159
  }
94
160
 
95
- function isFailAggregate(entry: CatalogEntry): boolean {
96
- if (!isFailKey(entry.keyid) || isWeeklyChange(entry) || isExcludedAsset(entry)) return false;
97
- const id = entry.keyid.toUpperCase();
98
- const named = describesTreasury(entry.description) || describesMbs(entry.description)
99
- || id.includes("TIPS") || /(?:USTET|UST|FGM|OM)$/.test(id);
100
- if (!named) return false;
101
- if (isAggregateTotal(entry)) return true;
102
- return /^(?:PDFAIL|PDFAS|PDFTD|PDFTR)-[A-Z0-9]+$/.test(id);
161
+ /**
162
+ * The newest series break: the one the release is published in now. Breaks
163
+ * restate the series, so keys are only meaningful inside one.
164
+ */
165
+ export function newestSeriesBreak(breaks: ReadonlyArray<{ seriesBreak: string; startDate: string }>): string | null {
166
+ let best: { seriesBreak: string; startDate: string } | null = null;
167
+ for (const entry of breaks) {
168
+ if (best == null || entry.startDate > best.startDate) best = entry;
169
+ }
170
+ return best?.seriesBreak ?? null;
103
171
  }
104
172
 
105
- function assetRank(entry: CatalogEntry): number {
106
- const id = entry.keyid.toUpperCase();
107
- if (id.includes("USTET") || id.includes("GST")) return 0;
108
- if (id.includes("TIPS") || /-(?:UST)$/.test(id)) return 1;
109
- if (id.includes("MBS") || id.endsWith("-FGM")) return 2;
110
- if (id.endsWith("-OM")) return 3;
111
- return 4;
173
+ /** The dates every series has, with the sum of their values: a week a series is missing is not a week of the sum. */
174
+ export function sumSeries(series: ReadonlyArray<readonly SeriesPoint[]>): SeriesPoint[] {
175
+ if (series.length === 0) return [];
176
+ const totals = new Map<string, { value: number; count: number }>();
177
+ for (const points of series) {
178
+ for (const point of points) {
179
+ const entry = totals.get(point.asOf);
180
+ if (entry) {
181
+ entry.value += point.value;
182
+ entry.count += 1;
183
+ } else {
184
+ totals.set(point.asOf, { value: point.value, count: 1 });
185
+ }
186
+ }
187
+ }
188
+ return [...totals.entries()]
189
+ .filter(([, entry]) => entry.count === series.length)
190
+ .map(([asOf, entry]) => ({ asOf, value: sum([entry.value]) }))
191
+ .sort((a, b) => (a.asOf < b.asOf ? -1 : a.asOf > b.asOf ? 1 : 0));
112
192
  }
113
193
 
114
- function byReadOrder(a: CatalogEntry, b: CatalogEntry): number {
115
- const side = (entry: CatalogEntry) => entry.keyid.toUpperCase().startsWith("PDFTR") ? 1 : 0;
116
- return assetRank(a) - assetRank(b) || side(a) - side(b) || a.keyid.localeCompare(b.keyid);
117
- }
194
+ /** The chart's ranges; the release's current series start in July 2024, so a long one is all of it. */
195
+ export const DEALER_RANGES = [
196
+ { value: "6M", label: "6M", hint: "1", days: 183 },
197
+ { value: "1Y", label: "1Y", hint: "2", days: 366 },
198
+ { value: "ALL", label: "All", hint: "3", days: null },
199
+ ] as const;
118
200
 
119
- export function selectSeries(entries: readonly CatalogEntry[]): SeriesSelection {
120
- const seriesBreak = newestSeriesBreak(entries);
121
- const current = seriesBreak ? entries.filter((entry) => entry.seriesBreak === seriesBreak) : [];
122
- const positions = current.filter(isPositionAggregate).sort(byReadOrder).slice(0, SERIES_CAP);
123
- const fails = current.filter(isFailAggregate).sort(byReadOrder).slice(0, Math.max(0, SERIES_CAP - positions.length));
124
- return { seriesBreak: seriesBreak ?? "", positions, fails };
125
- }
201
+ export type DealerRange = typeof DEALER_RANGES[number]["value"];
202
+ export const DEFAULT_DEALER_RANGE: DealerRange = "1Y";
126
203
 
127
- function fallbackLabel(description: string, keyid: string): string {
128
- let text = description.replace(/^\s*total\s*-\s*/i, "").trim();
129
- const cut = text.search(/\s+-\s+|\s*:\s*/);
130
- if (cut > 0) text = text.slice(0, cut).trim();
131
- text = text.replace(/\s+/g, " ");
132
- if (!text) return keyid;
133
- return text.length <= 48 ? text : `${text.slice(0, 47).trimEnd()}…`;
204
+ export function dealerRange(value: unknown): DealerRange {
205
+ return DEALER_RANGES.find((range) => range.value === value)?.value ?? DEFAULT_DEALER_RANGE;
134
206
  }
135
207
 
136
- function positionLabel(entry: CatalogEntry): string {
137
- const id = entry.keyid.toUpperCase();
138
- const asset = id.includes("USTET") || id.includes("GST") ? "Treasuries ex-TIPS"
139
- : id.includes("TIPS") || /-(?:UST)$/.test(id) ? "TIPS"
140
- : id.includes("MBS") || id.endsWith("-FGM") ? "Agency MBS"
141
- : id.endsWith("-OM") ? "Other MBS"
142
- : null;
143
- if (!asset) return fallbackLabel(entry.description, entry.keyid);
144
- if (id.startsWith("PDFTD")) return `${asset}, fails to deliver`;
145
- if (id.startsWith("PDFTR")) return `${asset}, fails to receive`;
146
- if (id.startsWith("PDFAIL") || id.startsWith("PDFAS")) return `${asset} fails`;
147
- return asset;
208
+ /** The points of the last `range`, counted back from the newest observation. */
209
+ export function pointsInRange(points: readonly SeriesPoint[], range: DealerRange): SeriesPoint[] {
210
+ const days = DEALER_RANGES.find((entry) => entry.value === range)?.days ?? null;
211
+ const last = points.at(-1);
212
+ if (days == null || !last) return [...points];
213
+ const cutoff = Date.parse(`${last.asOf}T00:00:00Z`) - days * 86_400_000;
214
+ return points.filter((point) => Date.parse(`${point.asOf}T00:00:00Z`) >= cutoff);
148
215
  }
149
216
 
150
217
  export function formatLevel(value: number | null): string {
@@ -161,30 +228,13 @@ export function formatChange(value: number | null): string {
161
228
  return value > 0 ? `+${text}` : `-${text}`;
162
229
  }
163
230
 
164
- export function toDealerRow(
165
- entry: CatalogEntry,
166
- parsed: { keyid: string; latest: { asOf: string; value: number } | null; previous: { asOf: string; value: number } | null },
167
- tab: DealerTab,
168
- ): DealerRow {
169
- const latest = parsed.latest?.value ?? null;
170
- const previous = parsed.previous?.value ?? null;
171
- return {
172
- keyid: parsed.keyid || entry.keyid,
173
- label: positionLabel(entry),
174
- tab,
175
- asOf: parsed.latest?.asOf ?? null,
176
- latest,
177
- previous,
178
- change: latest != null && previous != null ? latest - previous : null,
179
- };
180
- }
181
-
182
- export function boardAsOf(rows: readonly DealerRow[]): string | null {
183
- let best: string | null = null;
184
- for (const row of rows) {
185
- if (row.asOf && (best == null || row.asOf > best)) best = row.asOf;
186
- }
187
- return best;
231
+ /**
232
+ * The row's name, a breakdown set in under its total. The indent is no-break
233
+ * spaces: table cells do not wrap, and on the desktop that also folds ordinary
234
+ * leading spaces away.
235
+ */
236
+ export function dealerRowLabel(row: Pick<DealerRow, "label" | "depth">): string {
237
+ return `${"\u00a0\u00a0".repeat(row.depth)}${row.label}`;
188
238
  }
189
239
 
190
240
  type DealerColumnId = "position" | "asOf" | "latest" | "previous" | "change";
@@ -1,24 +1,40 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from "react";
2
2
  import {
3
+ ChartTableHeader,
3
4
  DataTableView,
4
5
  PaneStatusBody,
6
+ QueryBar,
7
+ usePaneNoticeFooter,
5
8
  usePaneStatusFooter,
6
9
  usePaneTabs,
10
+ type ChartTableChart,
7
11
  type DataTableCell,
12
+ type DataTableKeyEvent,
8
13
  } from "../../../components";
14
+ import { spanAxisFormatter } from "../../../components/chart-table";
15
+ import { useChartViewReset } from "../../../components/chart/core/view-reset";
16
+ import { scalarPoint, staticSeries } from "../../../components/chart/static/series";
9
17
  import { handleRefreshKey, usePaneRefreshKey } from "../../../components/data-table/table-pane";
10
18
  import { useAsyncResource } from "../../../react/async-resource";
11
19
  import { colors } from "../../../theme/colors";
12
20
  import type { PaneProps } from "../../../types/plugin";
13
21
  import { Box, TextAttributes } from "../../../ui";
22
+ import { formatNumber } from "../../../utils/format";
23
+ import { isPlainKeyboardEvent } from "../../../utils/keyboard";
14
24
  import { usePluginPaneState } from "../../runtime";
15
- import { fetchPrimaryDealers } from "./client";
25
+ import { cachedDealerHistory, fetchPrimaryDealers, hasDealerHistory, loadDealerHistory } from "./client";
16
26
  import {
27
+ DEALER_RANGES,
28
+ DEFAULT_DEALER_RANGE,
17
29
  PRIMARY_DEALERS_PANE_ID,
18
30
  buildDealerColumns,
31
+ dealerRange,
32
+ dealerRowLabel,
19
33
  formatChange,
20
34
  formatLevel,
35
+ pointsInRange,
21
36
  type DealerColumn,
37
+ type DealerRange,
22
38
  type DealerRow,
23
39
  type DealerTab,
24
40
  } from "./model";
@@ -28,19 +44,49 @@ const TABS = [
28
44
  { label: "Fails", value: "fails" },
29
45
  ];
30
46
  const EMPTY_ROWS: DealerRow[] = [];
47
+ const RANGE_OPTIONS = DEALER_RANGES.map(({ value, label, hint }) => ({ value, label, hint }));
48
+ /** How long the selection rests before its history is read, so holding a key down the board reads one row, not all it passes. */
49
+ const CHART_SETTLE_MS = 150;
50
+ /** Ticks in the table's own figures (whole $mn), with decimals only where the plotted range is narrow. */
51
+ const formatLevelAxis = spanAxisFormatter((value, digits) => formatNumber(value, digits));
31
52
 
32
53
  function changeColor(value: number | null): string {
33
54
  if (value == null || value === 0) return colors.textMuted;
34
55
  return value > 0 ? colors.positive : colors.negative;
35
56
  }
36
57
 
58
+ /** The row the chart follows: the selection once it rests, or at once when its history is already in hand. */
59
+ function useChartRow(rows: readonly DealerRow[], selected: DealerRow | null, known: boolean): DealerRow | null {
60
+ const [restedId, setRestedId] = useState<string | null>(null);
61
+ const selectedId = selected?.id ?? null;
62
+ useEffect(() => {
63
+ if (selectedId === restedId) return;
64
+ if (selectedId == null || restedId == null || known || !rows.some((row) => row.id === restedId)) {
65
+ setRestedId(selectedId);
66
+ return;
67
+ }
68
+ const timer = setTimeout(() => setRestedId(selectedId), CHART_SETTLE_MS);
69
+ return () => clearTimeout(timer);
70
+ }, [known, restedId, rows, selectedId]);
71
+ if (known) return selected;
72
+ return rows.find((row) => row.id === restedId) ?? selected;
73
+ }
74
+
37
75
  export function PrimaryDealersPane({ focused, width, height }: PaneProps) {
38
76
  const request = useCallback(() => fetchPrimaryDealers(), []);
39
77
  const { data, loading, error, load } = useAsyncResource(request);
40
78
  const [storedTab, setTab] = usePluginPaneState<string>("tab", "positions");
41
79
  const tab: DealerTab = storedTab === "fails" ? "fails" : "positions";
42
- const [selectedId, setSelectedId] = useState<string | null>(null);
80
+ const [positionId, setPositionId] = usePluginPaneState<string | null>("selected:positions", null);
81
+ const [failId, setFailId] = usePluginPaneState<string | null>("selected:fails", null);
82
+ const [storedRange, setStoredRange] = usePluginPaneState<string>("range", DEFAULT_DEALER_RANGE);
83
+ const range = dealerRange(storedRange);
84
+ const resetChartView = useChartViewReset();
43
85
  const rows = tab === "fails" ? data?.fails ?? EMPTY_ROWS : data?.positions ?? EMPTY_ROWS;
86
+ const selectedId = tab === "fails" ? failId : positionId;
87
+ const setSelectedId = tab === "fails" ? setFailId : setPositionId;
88
+ const selected = rows.find((row) => row.id === selectedId) ?? rows[0] ?? null;
89
+ const seriesBreak = data?.seriesBreak ?? null;
44
90
  const asOf = useMemo(() => {
45
91
  let best: string | null = null;
46
92
  for (const row of rows) {
@@ -54,15 +100,57 @@ export function PrimaryDealersPane({ focused, width, height }: PaneProps) {
54
100
  [asOf],
55
101
  );
56
102
 
57
- useEffect(() => {
58
- if (selectedId && rows.some((row) => row.keyid === selectedId)) return;
59
- setSelectedId(rows[0]?.keyid ?? null);
60
- }, [rows, selectedId]);
103
+ // The chart follows the selected row. Its history is read for that row alone.
104
+ const known = !!selected && !!seriesBreak && hasDealerHistory(seriesBreak, selected);
105
+ const chartRow = useChartRow(rows, selected, known);
106
+ const loadHistory = useMemo(
107
+ () => chartRow && seriesBreak ? (force: boolean) => loadDealerHistory(seriesBreak, chartRow, force) : null,
108
+ [chartRow, seriesBreak],
109
+ );
110
+ const historyResource = useAsyncResource(loadHistory, { keepPreviousData: true });
111
+ const history = useMemo(
112
+ () => (chartRow && seriesBreak ? cachedDealerHistory(seriesBreak, chartRow) : null) ?? historyResource.data,
113
+ [chartRow, historyResource.data, seriesBreak],
114
+ );
115
+ // While another row's history loads the chart keeps the last one drawn, unless that row is not on this tab.
116
+ const historyRow = history ? rows.find((row) => row.id === history.rowId) ?? null : null;
117
+ const series = useMemo(() => history && historyRow
118
+ ? [staticSeries(
119
+ pointsInRange(history.points, range).map((point) => scalarPoint(new Date(`${point.asOf}T00:00:00Z`), point.value)),
120
+ { id: historyRow.id, label: `${historyRow.label} $mn`, color: colors.positive, calendarSpaced: true },
121
+ )]
122
+ : [], [history, historyRow, range]);
123
+ const historyFailed = history?.rowId !== chartRow?.id && !!historyResource.error;
124
+ const chart: ChartTableChart | null = selected && !historyFailed ? {
125
+ series,
126
+ formatValue: formatLevel,
127
+ formatAxisValue: formatLevelAxis,
128
+ remoteKind: "primary-dealer-history",
129
+ loading: !historyRow,
130
+ empty: `No history for ${selected.label}`,
131
+ } : null;
61
132
 
62
- const refresh = useCallback(() => { void load(); }, [load]);
133
+ const { reload: reloadHistory } = historyResource;
134
+ const refresh = useCallback(() => {
135
+ void load();
136
+ void reloadHistory();
137
+ }, [load, reloadHistory]);
63
138
  const selectTab = useCallback((value: string) => setTab(value === "fails" ? "fails" : "positions"), [setTab]);
139
+ const selectRange = useCallback((value: DealerRange) => setStoredRange(value), [setStoredRange]);
64
140
  usePaneRefreshKey(refresh, { focused });
65
141
 
142
+ // The number keys pick the range, as the bar's hints say; picking the one showing sends the chart back to it.
143
+ const handleKeyDown = useCallback((event: DataTableKeyEvent) => {
144
+ if (handleRefreshKey(event, refresh, { stopPropagation: true })) return true;
145
+ if (!isPlainKeyboardEvent(event)) return false;
146
+ const picked = DEALER_RANGES.find((option) => option.hint === event.name);
147
+ if (!picked) return false;
148
+ event.preventDefault?.();
149
+ if (picked.value === range) resetChartView();
150
+ else selectRange(picked.value);
151
+ return true;
152
+ }, [range, refresh, resetChartView, selectRange]);
153
+
66
154
  const { strip: tabs, rows: tabRows } = usePaneTabs({
67
155
  tabs: TABS,
68
156
  activeValue: tab,
@@ -78,6 +166,11 @@ export function PrimaryDealersPane({ focused, width, height }: PaneProps) {
78
166
  error,
79
167
  info: footerInfo,
80
168
  });
169
+ usePaneNoticeFooter({
170
+ registrationId: `${PRIMARY_DEALERS_PANE_ID}:history`,
171
+ focused,
172
+ notices: historyFailed && selected ? [`History for ${selected.label} is unavailable: ${historyResource.error}`] : [],
173
+ });
81
174
 
82
175
  const renderCell = useCallback((
83
176
  row: DealerRow,
@@ -88,10 +181,12 @@ export function PrimaryDealersPane({ focused, width, height }: PaneProps) {
88
181
  const selectedColor = rowState.selected ? colors.selectedText : undefined;
89
182
  switch (column.id) {
90
183
  case "position":
184
+ // A breakdown reads under its total, in plain weight.
91
185
  return {
92
- text: row.label,
93
- color: selectedColor ?? colors.textBright,
94
- attributes: TextAttributes.BOLD,
186
+ text: dealerRowLabel(row),
187
+ value: row.label,
188
+ color: selectedColor ?? (row.depth === 0 ? colors.textBright : colors.text),
189
+ attributes: row.depth === 0 ? TextAttributes.BOLD : undefined,
95
190
  };
96
191
  case "asOf":
97
192
  return { text: row.asOf ?? "—", color: selectedColor ?? colors.textMuted };
@@ -122,26 +217,51 @@ export function PrimaryDealersPane({ focused, width, height }: PaneProps) {
122
217
  );
123
218
  }
124
219
 
220
+ const rootHeight = Math.max(1, height - tabRows);
125
221
  return (
126
222
  <Box flexDirection="column" width={width} height={height}>
127
223
  {tabs}
128
224
  <DataTableView<DealerRow, DealerColumn>
129
225
  focused={focused}
130
226
  rootWidth={width}
131
- rootHeight={Math.max(1, height - tabRows)}
227
+ rootHeight={rootHeight}
228
+ rootBefore={selected ? (
229
+ <ChartTableHeader
230
+ width={width}
231
+ height={rootHeight}
232
+ tableRows={rows.length}
233
+ tableColumns={columns}
234
+ query={(
235
+ <QueryBar
236
+ width={width}
237
+ filters={[{
238
+ id: "range",
239
+ label: "Range",
240
+ inline: true,
241
+ chartRange: true,
242
+ value: range,
243
+ options: RANGE_OPTIONS,
244
+ onChange: (value: string) => selectRange(dealerRange(value)),
245
+ }]}
246
+ />
247
+ )}
248
+ chart={chart}
249
+ />
250
+ ) : undefined}
132
251
  selection={{
133
252
  kind: "id",
134
- selectedId,
135
- getId: (row) => row.keyid,
253
+ selectedId: selected?.id ?? null,
254
+ getId: (row) => row.id,
136
255
  onChange: (id) => setSelectedId(id),
137
256
  }}
138
- onRootKeyDown={(event) => handleRefreshKey(event, refresh, { stopPropagation: true })}
257
+ onRootKeyDown={handleKeyDown}
139
258
  columns={columns}
140
259
  items={rows}
141
260
  sortColumnId={null}
142
261
  sortDirection="asc"
143
- getItemKey={(row) => row.keyid}
262
+ getItemKey={(row) => row.id}
144
263
  renderCell={renderCell}
264
+ resetScrollKey={tab}
145
265
  emptyStateTitle={tab === "fails" ? "No fails in this release." : "No positions in this release."}
146
266
  />
147
267
  </Box>
@@ -5,6 +5,7 @@ import { formatRelativeTime } from "../../../utils/datetime-format";
5
5
  import { displayWidth, formatCurrency, formatNumber } from "../../../utils/format";
6
6
  import { compareSortValues, type SortDirection } from "../../../utils/sort-values";
7
7
  import { loadingErrorFooterInfo } from "../../../components/data-table/table-pane";
8
+ import { delayedFooterSegment } from "../../../components/layout/pane/footer/model";
8
9
  import { regularSessionValuationPrice } from "../../../market-data/quotes/regular-session-valuation";
9
10
 
10
11
  function compactPeriod(period: string): string {
@@ -369,14 +370,17 @@ export function buildAnalystStatusSegments(
369
370
 
370
371
  // The upside is only as good as the price it was measured against.
371
372
  if (reference.price != null) {
373
+ // A delayed price is the footer's clock beside it; a real-time one says so in words.
374
+ const delay = reference.freshness?.endsWith(" delayed") ? reference.freshness.slice(0, -" delayed".length) : null;
372
375
  segments.push({
373
376
  id: "analyst-reference-price",
374
377
  parts: [
375
378
  { text: "upside vs", tone: "label" },
376
379
  { text: price(reference.price) },
377
- ...(reference.freshness ? [{ text: reference.freshness, tone: "muted" as const }] : []),
380
+ ...(reference.freshness && !delay ? [{ text: reference.freshness, tone: "muted" as const }] : []),
378
381
  ],
379
382
  });
383
+ if (delay) segments.push(delayedFooterSegment({ id: "analyst-reference-delay", detail: `Price ${reference.freshness}`, delay }));
380
384
  }
381
385
 
382
386
  if (!data.stale && data.fetchedAt) {