gloomberb 0.18.0 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (365) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/commodity-balances.ts +6 -0
  3. package/src/api-client/company-kpis.ts +9 -0
  4. package/src/api-client/crypto-markets.ts +9 -0
  5. package/src/api-client/data.ts +6 -5
  6. package/src/api-client/equity-screener.ts +2 -0
  7. package/src/api-client/market-movers.ts +7 -0
  8. package/src/api-client/types.ts +14 -0
  9. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  10. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  11. package/src/app/runtime/plugin-bindings.ts +6 -2
  12. package/src/brokers/logos.ts +66 -0
  13. package/src/brokers/profile-form.ts +3 -0
  14. package/src/brokers/remote-broker-adapter.ts +13 -3
  15. package/src/capabilities/index.ts +1 -1
  16. package/src/capabilities/service-capabilities.ts +7 -1
  17. package/src/cli/base-converter.ts +60 -18
  18. package/src/cli/commands/fred-series.ts +15 -5
  19. package/src/cli/commands/market.ts +290 -159
  20. package/src/cli/commands/news-search.ts +35 -8
  21. package/src/cli/commands/overview.ts +31 -6
  22. package/src/cli/commands/system.ts +35 -1
  23. package/src/cli/commands/ticker.ts +105 -52
  24. package/src/cli/currency-hints.ts +27 -0
  25. package/src/cli/fx-pair.ts +41 -4
  26. package/src/cli/glossary.ts +57 -4
  27. package/src/cli/helpers.ts +36 -3
  28. package/src/cli/index.ts +4 -1
  29. package/src/cli/known-gaps.ts +37 -1
  30. package/src/cli/listing-arg.ts +37 -2
  31. package/src/cli/pane-functions/catalog.ts +3 -1
  32. package/src/cli/pane-functions/discovery.ts +15 -1
  33. package/src/cli/pane-functions/dom.ts +9 -4
  34. package/src/cli/pane-functions/freshness.ts +4 -4
  35. package/src/cli/pane-functions/function-help.ts +31 -7
  36. package/src/cli/pane-functions/headless.ts +6 -5
  37. package/src/cli/pane-functions/index.ts +3 -2
  38. package/src/cli/pane-functions/lock-note.ts +7 -4
  39. package/src/cli/pane-functions/resolver.ts +2 -1
  40. package/src/cli/quote-answer.ts +179 -0
  41. package/src/cli/series-search.ts +2 -1
  42. package/src/cli/symbol-list.ts +51 -16
  43. package/src/cli/types.ts +4 -0
  44. package/src/components/chart/composite/composite-chart.tsx +22 -7
  45. package/src/components/chart/composite/format.ts +13 -1
  46. package/src/components/chart/composite/level-labels.ts +30 -0
  47. package/src/components/chart/composite/levels.ts +17 -6
  48. package/src/components/chart/composite/panel-surface.tsx +64 -16
  49. package/src/components/chart/composite/price-axis-labels.tsx +2 -1
  50. package/src/components/chart/composite/types.ts +4 -0
  51. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  52. package/src/components/command-bar/workflow/collection-actions.ts +4 -0
  53. package/src/components/index.ts +2 -0
  54. package/src/components/layout/pane/content.tsx +16 -14
  55. package/src/components/layout/pane/footer/index.tsx +22 -3
  56. package/src/components/layout/pane/footer/model.ts +99 -4
  57. package/src/components/layout/pane/header-tabs.tsx +15 -1
  58. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  59. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  60. package/src/components/onboarding/wizard-model.ts +10 -3
  61. package/src/components/onboarding/wizard-positions.ts +8 -0
  62. package/src/components/ui/button.tsx +11 -4
  63. package/src/components/ui/choice-dialog.tsx +8 -5
  64. package/src/components/ui/display.tsx +11 -6
  65. package/src/components/ui/fields.tsx +100 -20
  66. package/src/components/ui/icon.tsx +6 -1
  67. package/src/components/ui/list-row-lines.tsx +87 -0
  68. package/src/components/ui/list-view.tsx +14 -0
  69. package/src/components/ui/logo-mark.tsx +64 -0
  70. package/src/components/ui/number-step.ts +54 -0
  71. package/src/components/ui/select-button.tsx +5 -4
  72. package/src/components/ui/select-field.tsx +3 -3
  73. package/src/components/ui/status.tsx +17 -3
  74. package/src/components/ui/tabs.tsx +9 -1
  75. package/src/components/ui/toggle.tsx +17 -3
  76. package/src/core/state/app/instrument.ts +8 -3
  77. package/src/data/config/store/normalize.ts +4 -1
  78. package/src/data/plugin-cache.ts +32 -6
  79. package/src/docs/snapshot.json +57 -22
  80. package/src/i18n/es.ts +9 -3
  81. package/src/i18n/ja.ts +9 -3
  82. package/src/i18n/ko.ts +9 -3
  83. package/src/i18n/zh-cn.ts +10 -4
  84. package/src/i18n/zh-tw.ts +10 -4
  85. package/src/market-data/commodity-names.ts +80 -0
  86. package/src/market-data/coordinator/auxiliary.ts +3 -1
  87. package/src/market-data/coordinator/chart.ts +3 -1
  88. package/src/market-data/futures-board.ts +38 -1
  89. package/src/market-data/futures-contract.ts +91 -0
  90. package/src/market-data/fx-closes.ts +78 -1
  91. package/src/market-data/market/change-basis.ts +219 -0
  92. package/src/market-data/market/format.ts +28 -12
  93. package/src/market-data/market/freshness.ts +38 -0
  94. package/src/market-data/market/status.ts +55 -9
  95. package/src/market-data/market/year-to-date.ts +46 -0
  96. package/src/market-data/quotes/contributions.ts +12 -0
  97. package/src/market-data/quotes/delisting.ts +34 -0
  98. package/src/market-data/quotes/live-valuation.ts +23 -0
  99. package/src/market-data/quotes/resolution.ts +8 -2
  100. package/src/market-data/tax-equivalent.ts +10 -2
  101. package/src/notifications/app-notifier.ts +2 -2
  102. package/src/notifications/notification-log.ts +8 -6
  103. package/src/plugins/builtin/alerts/cli.ts +26 -18
  104. package/src/plugins/builtin/alerts/headless.ts +1 -1
  105. package/src/plugins/builtin/alerts/index.tsx +13 -4
  106. package/src/plugins/builtin/analytics/index.tsx +1 -1
  107. package/src/plugins/builtin/analytics/risk-client.ts +44 -11
  108. package/src/plugins/builtin/analytics/risk-fx.ts +61 -0
  109. package/src/plugins/builtin/analytics/risk-headless.ts +10 -1
  110. package/src/plugins/builtin/analytics/risk-model.ts +79 -16
  111. package/src/plugins/builtin/analytics/risk-pane.tsx +8 -2
  112. package/src/plugins/builtin/analytics/risk-test-data.ts +3 -0
  113. package/src/plugins/builtin/attention/trending.tsx +8 -6
  114. package/src/plugins/builtin/backtest/client.ts +5 -2
  115. package/src/plugins/builtin/backtest/window.ts +16 -3
  116. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  117. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  118. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  119. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  120. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  121. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  122. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  123. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  124. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  125. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  126. package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
  127. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  128. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  129. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  130. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  131. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  132. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  133. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  134. package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
  135. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  136. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  137. package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
  138. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  139. package/src/plugins/builtin/buildout/format.ts +1 -0
  140. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  141. package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
  142. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  143. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  144. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  145. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  146. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  147. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  148. package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
  149. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  150. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  151. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  152. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  153. package/src/plugins/builtin/chat/index.tsx +1 -1
  154. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  155. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  156. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  157. package/src/plugins/builtin/cloud/team/pane-sections.tsx +10 -3
  158. package/src/plugins/builtin/cloud/team/pane.tsx +30 -9
  159. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  160. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  161. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  162. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  163. package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
  164. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  165. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  166. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  167. package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
  168. package/src/plugins/builtin/composite-plugins.ts +3 -2
  169. package/src/plugins/builtin/cot/headless.ts +1 -1
  170. package/src/plugins/builtin/cot/index.tsx +1 -1
  171. package/src/plugins/builtin/cot/model.ts +35 -12
  172. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  173. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  174. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  175. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  176. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  177. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  178. package/src/plugins/builtin/custom-view/loader.ts +3 -2
  179. package/src/plugins/builtin/custom-view/pane.tsx +5 -4
  180. package/src/plugins/builtin/custom-view/view-spec.ts +1 -0
  181. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  182. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  183. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  184. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  185. package/src/plugins/builtin/econ/calendar-model.ts +38 -2
  186. package/src/plugins/builtin/econ/fred-catalog.ts +6 -2
  187. package/src/plugins/builtin/econ/fred-series-map.ts +12 -1
  188. package/src/plugins/builtin/econ/index.tsx +31 -4
  189. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  190. package/src/plugins/builtin/equity-screener/headless.ts +20 -5
  191. package/src/plugins/builtin/equity-screener/model.ts +40 -2
  192. package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
  193. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  194. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  195. package/src/plugins/builtin/executives/data.ts +24 -8
  196. package/src/plugins/builtin/executives/index.tsx +2 -18
  197. package/src/plugins/builtin/executives/pane.tsx +34 -10
  198. package/src/plugins/builtin/exposure/client.ts +4 -1
  199. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  200. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  201. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  202. package/src/plugins/builtin/exposure/pane.tsx +37 -16
  203. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  204. package/src/plugins/builtin/futures/table.tsx +6 -3
  205. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  206. package/src/plugins/builtin/insider/base-values.ts +72 -0
  207. package/src/plugins/builtin/insider/headless.ts +29 -4
  208. package/src/plugins/builtin/jobs/headless.ts +2 -2
  209. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  210. package/src/plugins/builtin/market-halts/model.ts +31 -6
  211. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  212. package/src/plugins/builtin/market-movers/client.ts +2 -2
  213. package/src/plugins/builtin/market-movers/index.tsx +0 -3
  214. package/src/plugins/builtin/market-movers/session-body.tsx +27 -11
  215. package/src/plugins/builtin/market-movers/session.ts +25 -10
  216. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  217. package/src/plugins/builtin/members/pane.tsx +8 -3
  218. package/src/plugins/builtin/news/wire/feed-config.ts +33 -13
  219. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  220. package/src/plugins/builtin/news/wire/letters/client.ts +1 -0
  221. package/src/plugins/builtin/news/wire/news/command-bar-search.ts +6 -3
  222. package/src/plugins/builtin/news/wire/news/detail-view.tsx +2 -1
  223. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  224. package/src/plugins/builtin/news/wire/news/table.tsx +2 -1
  225. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  226. package/src/plugins/builtin/news/wire/rss/feed-xml.ts +6 -1
  227. package/src/plugins/builtin/news/wire/rss/managed-feed.ts +1 -0
  228. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  229. package/src/plugins/builtin/notes/cli.ts +33 -8
  230. package/src/plugins/builtin/notes/export.ts +49 -3
  231. package/src/plugins/builtin/notes/files.ts +14 -3
  232. package/src/plugins/builtin/notes/index.tsx +3 -3
  233. package/src/plugins/builtin/notes/quick-notes-pane.tsx +45 -10
  234. package/src/plugins/builtin/notes/store.ts +13 -3
  235. package/src/plugins/builtin/options/footer.ts +25 -20
  236. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  237. package/src/plugins/builtin/options/view.tsx +77 -22
  238. package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
  239. package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
  240. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  241. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -7
  242. package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +166 -15
  243. package/src/plugins/builtin/portfolio-list/cli/render.ts +69 -15
  244. package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +2 -2
  245. package/src/plugins/builtin/portfolio-list/headless.ts +3 -1
  246. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  247. package/src/plugins/builtin/portfolio-list/pane/index.tsx +44 -17
  248. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  249. package/src/plugins/builtin/portfolio-list/summary/index.tsx +22 -1
  250. package/src/plugins/builtin/portfolio-list/summary/live-accounts.ts +42 -7
  251. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  252. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  253. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  254. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  255. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  256. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  257. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  258. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  259. package/src/plugins/builtin/scanner/feed.ts +6 -1
  260. package/src/plugins/builtin/scanner/headless.ts +1 -1
  261. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  262. package/src/plugins/builtin/series-inventory.ts +1 -0
  263. package/src/plugins/builtin/shared/broker-reconnect.ts +105 -0
  264. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  265. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  266. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  267. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  268. package/src/plugins/builtin/shared/pro-feature-wall.tsx +9 -4
  269. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  270. package/src/plugins/builtin/shared/research-trading.tsx +62 -0
  271. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  272. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  273. package/src/plugins/builtin/themes/headless.ts +8 -2
  274. package/src/plugins/builtin/themes/model.ts +43 -10
  275. package/src/plugins/builtin/themes/pane.tsx +8 -6
  276. package/src/plugins/builtin/thirteenf/api.ts +4 -1
  277. package/src/plugins/builtin/thirteenf/data.ts +5 -1
  278. package/src/plugins/builtin/thirteenf/headless.ts +3 -1
  279. package/src/plugins/builtin/thirteenf/pane.tsx +8 -3
  280. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  281. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  282. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  283. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  284. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  285. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  286. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  287. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  288. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  289. package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
  290. package/src/plugins/builtin-editorial.ts +1 -1
  291. package/src/plugins/compat.ts +3 -0
  292. package/src/plugins/external-runtime.ts +5 -0
  293. package/src/plugins/loader.ts +6 -0
  294. package/src/plugins/plugin-icon.ts +61 -0
  295. package/src/plugins/registry/host-actions.ts +2 -1
  296. package/src/plugins/registry/index.ts +9 -1
  297. package/src/plugins/runtime/context.tsx +5 -0
  298. package/src/plugins/runtime/index.tsx +3 -0
  299. package/src/portfolio/column-values.ts +13 -4
  300. package/src/portfolio/cost-fx.ts +61 -0
  301. package/src/portfolio/mutations.ts +16 -2
  302. package/src/portfolio/position-metrics.ts +45 -4
  303. package/src/portfolio/position-quantity.ts +43 -0
  304. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  305. package/src/public/broker.ts +8 -0
  306. package/src/public/market-data.ts +5 -0
  307. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  308. package/src/renderers/dom/desktop/controls.tsx +39 -7
  309. package/src/renderers/dom/desktop/icons.tsx +14 -0
  310. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  311. package/src/renderers/dom/desktop/menu.tsx +10 -1
  312. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  313. package/src/renderers/dom/host/style.ts +6 -1
  314. package/src/renderers/dom/host/tabs.tsx +6 -0
  315. package/src/renderers/dom/styles.css +12 -5
  316. package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
  317. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  318. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  319. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  320. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  321. package/src/series/validate.ts +2 -2
  322. package/src/sources/gloomberb-cloud/fred-series.ts +27 -2
  323. package/src/sources/gloomberb-cloud/index.ts +50 -17
  324. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  325. package/src/sources/history-result.ts +3 -0
  326. package/src/sources/provider-errors.ts +3 -1
  327. package/src/sources/provider-router/history.ts +49 -6
  328. package/src/sources/provider-router/index.ts +2 -1
  329. package/src/state/app/activity.ts +13 -0
  330. package/src/sync/controller.ts +4 -0
  331. package/src/sync/core-contributors.ts +63 -11
  332. package/src/sync/local-only.ts +121 -0
  333. package/src/sync/react.ts +6 -0
  334. package/src/sync/types.ts +3 -0
  335. package/src/team/collections.ts +2 -0
  336. package/src/tickers/listing-fate.ts +27 -8
  337. package/src/tickers/metadata.ts +10 -0
  338. package/src/tickers/search/index.ts +24 -3
  339. package/src/time-series/price-comparison.ts +35 -0
  340. package/src/time-series/resolve.ts +52 -11
  341. package/src/time-series/spec.ts +1 -1
  342. package/src/time-series/types.ts +5 -0
  343. package/src/types/broker.ts +56 -1
  344. package/src/types/config.ts +6 -0
  345. package/src/types/data-provider.ts +5 -1
  346. package/src/types/financials.ts +23 -0
  347. package/src/types/headless.ts +12 -2
  348. package/src/types/instrument.ts +4 -0
  349. package/src/types/plugin.ts +24 -6
  350. package/src/types/price-history.ts +6 -1
  351. package/src/types/ticker.ts +9 -0
  352. package/src/types/trading.ts +67 -0
  353. package/src/ui/host.tsx +4 -0
  354. package/src/utils/base-currency.ts +108 -0
  355. package/src/utils/currency-names.ts +128 -0
  356. package/src/utils/currency-pair.ts +2 -1
  357. package/src/utils/currency-units.ts +41 -1
  358. package/src/utils/dom-screenshot.ts +22 -9
  359. package/src/utils/exchange-notation.generated.ts +360 -0
  360. package/src/utils/exchanges.ts +15 -322
  361. package/src/utils/format.ts +31 -1
  362. package/src/utils/instrument-identity.ts +38 -1
  363. package/src/utils/options.ts +46 -1
  364. package/src/utils/price-history.ts +4 -0
  365. package/src/utils/reported-money.ts +8 -4
@@ -6,6 +6,8 @@ import { parsePublicTickerKey } from "../utils/exchanges";
6
6
  import { logFailure } from "../utils/log-failure";
7
7
  import { futuresBoardContract, type FuturesContract } from "./futures-board";
8
8
  import { formatMarketPrice, formatMarketPriceWithCurrency, quoteFormatOptions, withCurrencyMinorDigits } from "./market/format";
9
+ import { minorUnitSign } from "../utils/currency-units";
10
+ import { contractPriceUnit } from "../time-series/price-units";
9
11
 
10
12
  /**
11
13
  * What a futures symbol trades, as `ticker`, `quote` and the Overview tab read
@@ -120,6 +122,95 @@ export function futuresSettleText(
120
122
 
121
123
  const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
122
124
 
125
+ /** The quantities a cents price is per, as traders abbreviate them: 467.25¢/bu for corn. */
126
+ const PER_ABBREVIATIONS: Readonly<Record<string, string>> = { bushel: "bu", bushels: "bu", pound: "lb", pounds: "lb" };
127
+
128
+ /**
129
+ * What a price in a minor unit is per, to follow its sign: "/lb" for coffee at
130
+ * 314.95¢/lb, "/bu" for corn, from the contract's unit ("US cents / lb").
131
+ * Empty for a price in a major currency, which reads as it always has, and
132
+ * when the terms name no unit in the quote's own currency.
133
+ */
134
+ export function minorUnitPricePer(
135
+ terms: Pick<FuturesContractTermsPayload, "spec"> | null | undefined,
136
+ currency: string | null | undefined,
137
+ ): string {
138
+ if (!minorUnitSign(currency)) return "";
139
+ const per = contractPriceUnit(terms?.spec?.unit, currency)?.split("/")[1]?.trim();
140
+ return per ? `/${PER_ABBREVIATIONS[per.toLowerCase()] ?? per}` : "";
141
+ }
142
+
143
+ const DAY_MS = 86_400_000;
144
+ const isoDate = /^\d{4}-\d{2}-\d{2}$/;
145
+
146
+ /**
147
+ * The weekday on or before a date (`back` 0) or before it (`back` 1), Saturday
148
+ * and Sunday skipped. Futures venues' holidays are not in the app's calendars,
149
+ * so a holiday reads as a session.
150
+ */
151
+ function weekdayBefore(date: string, back: 0 | 1): string {
152
+ let at = Date.parse(`${date}T00:00:00Z`) - back * DAY_MS;
153
+ while (new Date(at).getUTCDay() % 6 === 0) at -= DAY_MS;
154
+ return new Date(at).toISOString().slice(0, 10);
155
+ }
156
+
157
+ /**
158
+ * How far apart two prices can be and still be the same settlement: half the
159
+ * contract's tick, so a move the source rounded still meets the settlement it
160
+ * was measured from, and a price a tick away does not. Without a tick, half a
161
+ * basis point of the price.
162
+ */
163
+ function settlementTolerance(tick: number | null | undefined, price: number): number {
164
+ const half = typeof tick === "number" && Number.isFinite(tick) && tick > 0 ? tick / 2 : Math.abs(price) * 5e-5;
165
+ // Floating point never splits a price exactly half a tick away from the settlement.
166
+ return half * (1 + 1e-9);
167
+ }
168
+
169
+ /**
170
+ * What a futures move is measured from: a session's settlement, the last
171
+ * trade of a session that settled elsewhere, or a session's close that the
172
+ * settlement in the contract terms cannot confirm either way. `session` is
173
+ * the date of that reference.
174
+ */
175
+ export interface FuturesChangeBasis {
176
+ kind: "settle" | "last trade" | "close";
177
+ session: string;
178
+ }
179
+
180
+ /**
181
+ * What the move a futures quote headlines is measured from, for the words
182
+ * after it ("vs Fri 16 Oct settle", quoteChangeBasis). `sessionDate` is the session the move belongs
183
+ * to (regularSessionDisplayDate); a Saturday or Sunday is Friday's, as no
184
+ * session is dated on one. The basis is only what the numbers show, against
185
+ * the one settlement the contract terms carry: a reference (the price less
186
+ * the move) within half a tick of it is that settlement, of whichever session
187
+ * it is. A reference that is not the settlement of the session just before
188
+ * the move's is that session's last trade. Otherwise, when the terms carry no
189
+ * settlement that old or one the reference can be checked against (the move's
190
+ * own session has settled, and the one before is not in the terms), it is the
191
+ * session before's close, left unnamed. Null without a session date, a move
192
+ * or terms.
193
+ */
194
+ export function futuresChangeBasis(
195
+ terms: Pick<FuturesContractTermsPayload, "settlement"> & { spec?: Pick<FuturesContractSpec, "tick"> | null } | null | undefined,
196
+ move: { price: number; change?: number | null; sessionDate?: string | null } | null | undefined,
197
+ ): FuturesChangeBasis | null {
198
+ const dated = move?.sessionDate;
199
+ if (!terms || !move || !dated || !isoDate.test(dated) || move.change == null) return null;
200
+ const reference = move.price - move.change;
201
+ if (!Number.isFinite(reference)) return null;
202
+ const session = weekdayBefore(dated, 0);
203
+ const previous = weekdayBefore(session, 1);
204
+ const settled = terms.settlement;
205
+ if (settled && settled.date <= session) {
206
+ if (Math.abs(reference - settled.price) <= settlementTolerance(terms.spec?.tick, settled.price)) {
207
+ return { kind: "settle", session: settled.date };
208
+ }
209
+ if (settled.date < session && settled.date >= previous) return { kind: "last trade", session: settled.date };
210
+ }
211
+ return { kind: "close", session: previous };
212
+ }
213
+
123
214
  /** A listed contract as code and month: "NGX26 (Nov 26)". */
124
215
  function contractWithMonth(contract: { contract: string; deliveryMonth: string }): string {
125
216
  const [year, month] = contract.deliveryMonth.split("-");
@@ -1,5 +1,7 @@
1
+ import type { CloudHistoryParams } from "../api-client/paths";
1
2
  import type { CloudMarketResponse, CloudPricePointPayload } from "../api-client/types";
2
- import type { FxLeg } from "./coordinator/fx-legs";
3
+ import { isCurrencyCode } from "../utils/currency-pair";
4
+ import { fxLegForCurrency, type FxLeg } from "./coordinator/fx-legs";
3
5
 
4
6
  /** USD per unit of one currency at each completed daily FX close, by date. */
5
7
  export interface FxCloses {
@@ -70,3 +72,78 @@ export function fxCloseOnOrBefore(fx: FxCloses, date: string): { date: string; r
70
72
  }
71
73
  return null;
72
74
  }
75
+
76
+ /** Reads a pair's daily bars: `apiClient` in the app, a fake in tests. */
77
+ export interface FxHistoryClient {
78
+ getCloudHistory(symbol: string, exchange: string, params: CloudHistoryParams): Promise<CloudMarketResponse<CloudPricePointPayload[]>>;
79
+ }
80
+
81
+ /** One unit of a currency in USD at a dated daily close. */
82
+ export interface FxCloseReading {
83
+ /** USD per unit. */
84
+ rate: number;
85
+ /** The UTC day of the close: the day asked for, or the last trading day before it. */
86
+ date: string;
87
+ }
88
+
89
+ /**
90
+ * The daily pair a currency's USD value is read from: the leg the app streams
91
+ * it on, or USD/XXX for any other ISO currency, which the service also keeps
92
+ * daily closes of. Null for a code that is no currency.
93
+ */
94
+ export function dailyFxLeg(currency: string): FxLeg | null {
95
+ const code = currency.trim().toUpperCase();
96
+ const leg = fxLegForCurrency(code);
97
+ if (leg) return leg;
98
+ return isCurrencyCode(code) ? { currency: code, instrument: { symbol: `${code}=X`, exchange: "" }, invert: true } : null;
99
+ }
100
+
101
+ const isoDay = (time: number) => new Date(time).toISOString().slice(0, 10);
102
+
103
+ /**
104
+ * USD per unit of the leg's currency at its last completed daily close on or
105
+ * before `date` (YYYY-MM-DD), looking back a week at most, so a weekend or a
106
+ * holiday finds the close before it but a gap in the data is not bridged.
107
+ * Today's bar, still trading, is never read. Null when the pair has no close
108
+ * in that week. A cost bought on a date and `fx --date` both read it.
109
+ */
110
+ export async function fxCloseOnDate(leg: FxLeg, date: string, client: FxHistoryClient, now = new Date()): Promise<FxCloseReading | null> {
111
+ const day = Date.parse(`${date}T00:00:00Z`);
112
+ const response = await client.getCloudHistory(leg.instrument.symbol, leg.instrument.exchange ?? "", {
113
+ interval: "1day", startDate: isoDay(day - FX_MAX_AGE_DAYS * DAY_MS), endDate: isoDay(day + DAY_MS), outputsize: FX_MAX_AGE_DAYS + 4,
114
+ });
115
+ if (response.status !== "success" && response.status !== "partial") return null;
116
+ const today = isoDay(now.getTime());
117
+ let best: FxCloseReading | null = null;
118
+ for (const row of Array.isArray(response.data) ? response.data : []) {
119
+ const at = typeof row?.date === "string" ? Date.parse(row.date) : Number.NaN;
120
+ if (!Number.isFinite(at) || !Number.isFinite(row.close) || row.close <= 0) continue;
121
+ const rowDay = isoDay(at);
122
+ if (rowDay > date || rowDay >= today || Date.parse(`${rowDay}T00:00:00Z`) < day - FX_MAX_AGE_DAYS * DAY_MS) continue;
123
+ if (!best || rowDay > best.date) best = { rate: leg.invert ? 1 / row.close : row.close, date: rowDay };
124
+ }
125
+ return best;
126
+ }
127
+
128
+ /**
129
+ * The completed daily closes of a currency's USD pair (`dailyFxLeg`) from a
130
+ * week before `since` (YYYY-MM-DD) to today, read by `dailyFxCloses`, so the
131
+ * first day finds the close before it. Throws `FxHistoryError` when the pair
132
+ * has no usable history, and a plain error for a code that is no currency.
133
+ */
134
+ export async function loadDailyFxCloses(
135
+ currency: string,
136
+ since: string,
137
+ client: FxHistoryClient,
138
+ now = new Date(),
139
+ ): Promise<FxCloses> {
140
+ const leg = dailyFxLeg(currency);
141
+ if (!leg) throw new Error(`${currency} is not a currency with daily FX closes`);
142
+ const start = Math.min(Date.parse(`${since}T00:00:00Z`), now.getTime()) - FX_MAX_AGE_DAYS * DAY_MS;
143
+ // A trading week has five closes; seven a week leaves room for a source that also dates weekends.
144
+ const rows = Math.ceil((now.getTime() - start) / DAY_MS) + 4;
145
+ const response = await client.getCloudHistory(leg.instrument.symbol, leg.instrument.exchange ?? "", {
146
+ interval: "1day", startDate: isoDay(start), endDate: isoDay(now.getTime()), outputsize: rows,
147
+ });
148
+ return dailyFxCloses(response, leg, now, Math.max(rows, 1500));
149
+ }
@@ -0,0 +1,219 @@
1
+ import { classifyInstrumentType, kindFromListingSyntax } from "../../tickers/instrument-kind";
2
+ import { isPerpetualExchange } from "../../tickers/perpetual-listing";
3
+ import type { Quote } from "../../types/financials";
4
+ import { canonicalExchange } from "../../utils/exchanges";
5
+ import { isFiniteNumber } from "../../utils/guards";
6
+ import { formatUtcDayTime } from "../../utils/utc-time";
7
+ import { zonedDateKey } from "../../utils/zoned-date-time";
8
+ import { futuresChangeBasis, isFuturesInstrument } from "../futures-contract";
9
+ import { quoteTradingDay } from "../quotes/day-range";
10
+ import { previousPublishedSession } from "./freshness";
11
+ import { getActiveQuoteDisplay, getRegularSessionDisplay, regularSessionDisplayDate } from "./status";
12
+
13
+ const DAY_MS = 86_400_000;
14
+ const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
15
+
16
+ /**
17
+ * What a quote's day change is measured from, so every change line can say
18
+ * it: the close of a dated session (a future's settlement or last trade), a
19
+ * previous close the venue's calendar cannot date, a coin's previous UTC
20
+ * day (its price at 00:00 UTC) or the last 24 hours.
21
+ */
22
+ export type QuoteChangeBasis =
23
+ | DatedChangeBasis
24
+ | { kind: "previous close" }
25
+ | { kind: "utc day" }
26
+ | { kind: "24h" };
27
+
28
+ /** A move from a dated session's close, settlement or last trade. */
29
+ interface DatedChangeBasis {
30
+ kind: "close" | "settle" | "last trade";
31
+ /** The exchange-local date of the session the move is measured from. */
32
+ date: string;
33
+ /** The venue, when its calendar dated the close. */
34
+ venue?: string;
35
+ /** Days of the venue's trading week it was closed between that session and the move's (holidays). */
36
+ closed?: readonly string[];
37
+ }
38
+
39
+ export interface QuoteChangeBasisOptions {
40
+ /**
41
+ * Which move: the regular session a headline shows (getRegularSessionDisplay,
42
+ * the default), or the latest print a position's day is valued at
43
+ * (getActiveQuoteDisplay), extended hours included.
44
+ */
45
+ session?: "regular" | "active";
46
+ /** A future's contract terms, whose settlement names what its move is from. */
47
+ terms?: Parameters<typeof futuresChangeBasis>[0];
48
+ }
49
+
50
+ const dayBefore = (date: string) => new Date(Date.parse(`${date}T00:00:00Z`) - DAY_MS).toISOString().slice(0, 10);
51
+
52
+ /**
53
+ * The weekday before the one a move is dated to, Saturday and Sunday skipped:
54
+ * a currency pair's week. A move dated on a weekend is Friday's, from Thursday.
55
+ */
56
+ function weekdayBefore(date: string): string {
57
+ let at = Date.parse(`${date}T00:00:00Z`);
58
+ while (new Date(at).getUTCDay() % 6 === 0) at -= DAY_MS;
59
+ at -= DAY_MS;
60
+ while (new Date(at).getUTCDay() % 6 === 0) at -= DAY_MS;
61
+ return new Date(at).toISOString().slice(0, 10);
62
+ }
63
+
64
+ function quoteVenue(quote: Quote): string {
65
+ return canonicalExchange(quote.listingExchangeName || quote.exchangeName);
66
+ }
67
+
68
+ function isCurrencyPair(quote: Quote): boolean {
69
+ return classifyInstrumentType(quote.instrumentType) === "currency"
70
+ || kindFromListingSyntax(quote.symbol, quote.listingExchangeName || quote.exchangeName) === "currency";
71
+ }
72
+
73
+ /** A coin on the spot crypto venue: its day is the UTC day. */
74
+ function isSpotCoin(quote: Quote): boolean {
75
+ return quoteVenue(quote) === "CCC";
76
+ }
77
+
78
+ /**
79
+ * The index session before the one a move is dated to, from the regular
80
+ * sessions an index quote carries (indexSession): the latest one on or before
81
+ * `date` is the move's (a quote read on a Saturday has Friday's), and the one
82
+ * before that is what it moves from.
83
+ */
84
+ function previousIndexSession(quote: Quote, date: string): string | null {
85
+ const session = quote.indexSession;
86
+ if (!session?.timeZone) return null;
87
+ const windows = [...session.regular, ...(session.current ? [session.current] : [])];
88
+ const days = [...new Set(windows.map((window) => zonedDateKey(window.start, session.timeZone)))]
89
+ .filter((day) => day <= date)
90
+ .sort();
91
+ return days.length >= 2 ? days.at(-2)! : null;
92
+ }
93
+
94
+ /**
95
+ * The session before the one a move is dated to: from the venue's published
96
+ * calendar, with the closures in between; for an index from its own sessions;
97
+ * for a currency pair the weekday before. Null when nothing dates it.
98
+ */
99
+ function previousSession(quote: Quote, sessionDate: string): DatedChangeBasis | null {
100
+ const venue = quoteVenue(quote);
101
+ const published = previousPublishedSession(venue, sessionDate);
102
+ if (published) return { kind: "close", date: published.date, venue, closed: published.closed };
103
+ const index = previousIndexSession(quote, sessionDate);
104
+ if (index) return { kind: "close", date: index };
105
+ if (isCurrencyPair(quote)) return { kind: "close", date: weekdayBefore(sessionDate) };
106
+ return null;
107
+ }
108
+
109
+ /**
110
+ * What the quote's move is measured from. A coin moves from its previous UTC
111
+ * day's close, the price at 00:00 UTC, unless Gloom Cloud says it moves over
112
+ * the last 24 hours; a perpetual always moves over 24 hours. A future's is the
113
+ * settlement or trade its terms name (futuresChangeBasis). Anything else is
114
+ * the close of the session before the move's, dated by the venue's calendar;
115
+ * a venue without one has a previous close that is left undated, never
116
+ * guessed. Null for a quote with no move: none reported, or a listing that no
117
+ * longer trades.
118
+ */
119
+ export function quoteChangeBasis(
120
+ quote: Quote | null | undefined,
121
+ { session = "regular", terms = null }: QuoteChangeBasisOptions = {},
122
+ ): QuoteChangeBasis | null {
123
+ if (!quote || quote.delisting || quote.changeBasis === "none") return null;
124
+ const display = session === "active" ? getActiveQuoteDisplay(quote) : getRegularSessionDisplay(quote);
125
+ if (!display || !isFiniteNumber(display.change)) return null;
126
+ if (quote.changeBasis === "24h" || isPerpetualExchange(quote.listingExchangeName || quote.exchangeName)) return { kind: "24h" };
127
+ if (isSpotCoin(quote)) return { kind: "utc day" };
128
+ const sessionDate = (session === "active" ? quote.changeSessionDate : regularSessionDisplayDate(quote)) ?? quoteTradingDay(quote);
129
+ if (isFuturesInstrument(quote.symbol, quote.instrumentType)) {
130
+ const basis = futuresChangeBasis(terms ?? { settlement: null }, { ...display, sessionDate });
131
+ return basis ? { kind: basis.kind, date: basis.session } : { kind: "previous close" };
132
+ }
133
+ if (!sessionDate || !ISO_DATE.test(sessionDate)) return { kind: "previous close" };
134
+ return previousSession(quote, sessionDate) ?? { kind: "previous close" };
135
+ }
136
+
137
+ /**
138
+ * The exchange-local date of `quote.previousClose`: the session before the
139
+ * one the quote's change is declared for (changeSessionDate), the UTC day
140
+ * before for a coin. Null when the quote has no previous close or nothing
141
+ * dates it, and for a coin or perpetual measured over 24 hours.
142
+ */
143
+ export function quotePreviousCloseDate(quote: Quote | null | undefined): string | null {
144
+ if (!quote || !isFiniteNumber(quote.previousClose) || quote.changeBasis != null) return null;
145
+ if (isPerpetualExchange(quote.listingExchangeName || quote.exchangeName)) return null;
146
+ const sessionDate = quote.changeSessionDate ?? quoteTradingDay(quote);
147
+ if (!sessionDate || !ISO_DATE.test(sessionDate)) return null;
148
+ if (isSpotCoin(quote)) return dayBefore(sessionDate);
149
+ if (isFuturesInstrument(quote.symbol, quote.instrumentType)) return futuresChangeBasis({ settlement: null }, { price: quote.price, change: 0, sessionDate })?.session ?? null;
150
+ return previousSession(quote, sessionDate)?.date ?? null;
151
+ }
152
+
153
+ /** `Fri 16 Oct`, with the year when it is not `now`'s. */
154
+ function sessionDay(date: string, now: number): string {
155
+ return formatUtcDayTime({ time: Date.parse(`${date}T00:00:00Z`), dateOnly: true }, now);
156
+ }
157
+
158
+ /**
159
+ * The words a change line ends with, the same on every surface: "vs Fri 16
160
+ * Oct close", "vs Fri 16 Oct settle", "vs prev close", "since 00:00 UTC",
161
+ * "24h".
162
+ */
163
+ export function changeBasisLabel(basis: QuoteChangeBasis, now = Date.now()): string {
164
+ switch (basis.kind) {
165
+ case "previous close":
166
+ return "vs prev close";
167
+ case "utc day":
168
+ return "since 00:00 UTC";
169
+ case "24h":
170
+ return "24h";
171
+ default:
172
+ return `vs ${sessionDay(basis.date, now)} ${basis.kind}`;
173
+ }
174
+ }
175
+
176
+ /**
177
+ * The days a venue was closed since the close a move is measured from, when
178
+ * there were any: "HKEX closed Mon 19 Oct, holiday"; three or more read as a
179
+ * span, "TWSE closed Thu 12 Feb to Fri 20 Feb, holidays". Null otherwise.
180
+ */
181
+ export function changeBasisNote(basis: QuoteChangeBasis | null | undefined, now = Date.now()): string | null {
182
+ if (!basis || basis.kind !== "close" || !basis.venue || !basis.closed?.length) return null;
183
+ const days = basis.closed.map((date) => sessionDay(date, now));
184
+ const span = days.length === 1 ? days[0] : days.length === 2 ? `${days[0]} and ${days[1]}` : `${days[0]} to ${days.at(-1)}`;
185
+ return `${basis.venue} closed ${span}, ${days.length === 1 ? "holiday" : "holidays"}`;
186
+ }
187
+
188
+ /** What several moves summed into one (a portfolio's day) are measured from, when they say. */
189
+ const MIXED_CHANGE_BASIS_LABEL = "vs mixed closes";
190
+
191
+ /**
192
+ * The label of a total over moves measured from different closes: the one
193
+ * label when they agree, "vs mixed closes" when they do not, so a total over
194
+ * an HKEX and a US listing never names one date for both. Null without a
195
+ * basis.
196
+ */
197
+ export function combinedChangeBasisLabel(bases: Iterable<QuoteChangeBasis | null | undefined>, now = Date.now()): string | null {
198
+ const labels = new Set<string>();
199
+ for (const basis of bases) if (basis) labels.add(changeBasisLabel(basis, now));
200
+ if (labels.size === 0) return null;
201
+ return labels.size === 1 ? [...labels][0]! : MIXED_CHANGE_BASIS_LABEL;
202
+ }
203
+
204
+ /**
205
+ * The basis as `--json` gives it beside a move: the kind, the dated session
206
+ * (`date`), the venue's closures in between and the label and note a change
207
+ * line shows.
208
+ */
209
+ export function changeBasisJson(basis: QuoteChangeBasis | null, now = Date.now()) {
210
+ if (!basis) return null;
211
+ const note = changeBasisNote(basis, now);
212
+ return {
213
+ kind: basis.kind,
214
+ date: "date" in basis ? basis.date : null,
215
+ ...("closed" in basis && basis.closed?.length ? { closed: [...basis.closed] } : {}),
216
+ label: changeBasisLabel(basis, now),
217
+ ...(note ? { note } : {}),
218
+ };
219
+ }
@@ -2,7 +2,8 @@ import type { PriceBasis } from "../../types/instrument";
2
2
  import type { Quote } from "../../types/financials";
3
3
  import { resolvePriceBasis } from "./price-basis";
4
4
  import { isIndexLevel } from "./index-level";
5
- import { currencyPriceDigits, formatCompact, formatCurrency } from "../../utils/format";
5
+ import { currencyPriceDigits, formatCompact, formatCurrency, formatCurrencyCode } from "../../utils/format";
6
+ import { minorUnitSign } from "../../utils/currency-units";
6
7
 
7
8
  export type AssetDisplayKind = "cash" | "crypto" | "equity" | "contract" | "other";
8
9
 
@@ -25,6 +26,8 @@ export interface MarketFormatOptions extends AssetDisplayContext {
25
26
  fixedFractionDigits?: number;
26
27
  /** Source units per displayed currency unit; a price quoted in pence keeps its pence decimals in pounds. */
27
28
  quotedUnitDivisor?: number;
29
+ /** Write the currency's code after a money price (259.36 USD) instead of its sign ($259.36): a report for a base currency other than USD. */
30
+ currencyCode?: boolean;
28
31
  /** A price that holds still for the session, such as the previous close. With
29
32
  * `fixedFractionDigits`, the kind's decimal ceiling is read at this price
30
33
  * instead of at the value, so a live price keeps its decimals as it moves. */
@@ -283,12 +286,12 @@ function compactScaledPrice(
283
286
  maxWidth: number | undefined,
284
287
  ): string {
285
288
  const sign = value < 0 ? "-" : "";
286
- const symbol = getCurrencySymbol(currency);
289
+ const marks = currencyMarks(currency, currency.trim().toUpperCase() || "USD");
287
290
  const numericWidth = maxWidth == null
288
291
  ? undefined
289
- : Math.max(1, maxWidth - sign.length - symbol.length - suffix.length);
292
+ : Math.max(1, maxWidth - sign.length - marks.prefix.length - suffix.length - marks.suffix.length);
290
293
  const formatted = formatVariableNumber(Math.abs(value) / divisor, 1, numericWidth, 1);
291
- return `${sign}${symbol}${formatted}${suffix}`;
294
+ return `${sign}${marks.prefix}${formatted}${suffix}${marks.suffix}`;
292
295
  }
293
296
 
294
297
  export function resolveAssetDisplayKind({
@@ -449,9 +452,10 @@ export function formatMarketChangeWithCurrency(
449
452
  return `${value > 0 ? "+" : ""}${formatMarketPriceWithCurrency(value, currency, options)}`;
450
453
  }
451
454
  }
455
+ const money = options.currencyCode ? formatCurrencyCode : formatCurrency;
452
456
  // A move that rounds to zero cents is unsigned rather than -$0.00.
453
- if (Math.abs(value) < smallestMove) return formatCurrency(0, currency);
454
- return `${value > 0 ? "+" : ""}${formatCurrency(value, currency)}`;
457
+ if (Math.abs(value) < smallestMove) return money(0, currency);
458
+ return `${value > 0 ? "+" : ""}${money(value, currency)}`;
455
459
  }
456
460
 
457
461
  /** Decimals the currency's minor unit has: two for GBP, none for JPY. */
@@ -612,6 +616,18 @@ export function liveQuoteFormatOptions(
612
616
  return withStablePriceDigits(quoteFormatOptions(quote, fallbackAssetCategory, metadataInstrumentType), currency, quote);
613
617
  }
614
618
 
619
+ /**
620
+ * Where an amount's currency is written: a major currency's symbol before it
621
+ * ($314.95, CHF314.95), a minor unit's sign after it (314.95¢ for US cents,
622
+ * 412.50p for pence), as `minorUnitSign` names them. A minor unit is read
623
+ * before upper-casing, as GBp is pence and GBP pounds.
624
+ */
625
+ function currencyMarks(currency: string, normalizedCurrency: string, code = false): { prefix: string; suffix: string } {
626
+ const minor = minorUnitSign(currency);
627
+ if (minor) return { prefix: "", suffix: minor };
628
+ return code ? { prefix: "", suffix: ` ${normalizedCurrency}` } : { prefix: getCurrencySymbol(normalizedCurrency), suffix: "" };
629
+ }
630
+
615
631
  export function formatMarketPriceWithCurrency(
616
632
  value: number | undefined,
617
633
  currency = "USD",
@@ -621,12 +637,12 @@ export function formatMarketPriceWithCurrency(
621
637
  if (isIndexLevel(options.assetCategory) || resolvePriceBasis(options.priceBasis, options.assetCategory) !== "per-unit") return formatMarketPrice(value, options);
622
638
  const normalizedCurrency = currency.trim().toUpperCase() || "USD";
623
639
  const sign = value < 0 ? "-" : "";
624
- const symbol = getCurrencySymbol(normalizedCurrency);
640
+ const { prefix, suffix } = currencyMarks(currency, normalizedCurrency, options.currencyCode);
625
641
  const numericWidth = options.maxWidth == null
626
642
  ? undefined
627
- : Math.max(1, options.maxWidth - sign.length - symbol.length);
643
+ : Math.max(1, options.maxWidth - sign.length - prefix.length - suffix.length);
628
644
  const body = formatMarketPrice(Math.abs(value), { ...options, currency: normalizedCurrency, maxWidth: numericWidth });
629
- return `${hasNonZeroDigit(body) ? sign : ""}${symbol}${body}`;
645
+ return `${hasNonZeroDigit(body) ? sign : ""}${prefix}${body}${suffix}`;
630
646
  }
631
647
 
632
648
  export function formatMarketCostWithCurrency(
@@ -638,10 +654,10 @@ export function formatMarketCostWithCurrency(
638
654
  if (resolvePriceBasis(options.priceBasis, options.assetCategory) !== "per-unit") return formatMarketCost(value, options);
639
655
  const normalizedCurrency = currency.trim().toUpperCase() || "USD";
640
656
  const sign = value < 0 ? "-" : "";
641
- const symbol = getCurrencySymbol(normalizedCurrency);
657
+ const { prefix, suffix } = currencyMarks(currency, normalizedCurrency, options.currencyCode);
642
658
  const numericWidth = options.maxWidth == null
643
659
  ? undefined
644
- : Math.max(1, options.maxWidth - sign.length - symbol.length);
660
+ : Math.max(1, options.maxWidth - sign.length - prefix.length - suffix.length);
645
661
  // A money cost keeps its currency's minor unit: $189.20, not $189.2 (JPY has none).
646
662
  const body = formatMarketCost(Math.abs(value), {
647
663
  minimumFractionDigits: currencyPriceMinDigits(normalizedCurrency),
@@ -649,7 +665,7 @@ export function formatMarketCostWithCurrency(
649
665
  currency: normalizedCurrency,
650
666
  maxWidth: numericWidth,
651
667
  });
652
- return `${sign}${symbol}${body}`;
668
+ return `${sign}${prefix}${body}${suffix}`;
653
669
  }
654
670
 
655
671
  /** @deprecated Slated for removal. Use `formatMarketPriceWithCurrency` with `maxWidth` to fit a column. */
@@ -498,6 +498,44 @@ export function hasPublishedSessionCalendar(exchange: string | undefined, date:
498
498
  return !!getPublishedUsEquityCalendarYears(canonical)?.includes(year);
499
499
  }
500
500
 
501
+ /**
502
+ * The venue's regular session before the one a move is dated to (`date`, its
503
+ * local `YYYY-MM-DD`), from its published calendar, with the days of its
504
+ * trading week it was closed in between (the Chung Yeung holiday HKEX took on
505
+ * Mon 19 Oct 2026 lies between Fri 16 and Tue 20 Oct). A `date` that is no
506
+ * session (a quote dated by when it was read, on a Saturday or a holiday) is
507
+ * the latest session on or before it: Friday's move, read on Saturday, is
508
+ * from Thursday's close. Null for round-the-clock and unknown venues, and
509
+ * when a day the walk passes is in a year whose closures the venue has not
510
+ * published: a holiday there would read as a session.
511
+ */
512
+ export function previousPublishedSession(
513
+ exchange: string | undefined,
514
+ date: string,
515
+ ): { date: string; closed: string[] } | null {
516
+ const canonical = canonicalExchange(exchange);
517
+ const day = isoLocalDateToUtcDay(date);
518
+ if (day == null || ALWAYS_OPEN_EXCHANGES.has(canonical) || !sessionCalendarTimeZone(canonical)) return null;
519
+ let session: number | null = null;
520
+ const closed: string[] = [];
521
+ for (let offset = 0; offset <= SESSION_LOOKBACK_DAYS * 2; offset++) {
522
+ const at = new Date((day - offset) * MS_PER_DAY);
523
+ const iso = at.toISOString().slice(0, 10);
524
+ if (!isTradingWeekday(canonical, at.getUTCDay())) continue;
525
+ if (!hasPublishedSessionCalendar(canonical, iso)) return null;
526
+ const open = !isPublishedClosure(canonical, iso);
527
+ // First the session the move is from's own day, then the one before it.
528
+ if (session == null) {
529
+ if (open) session = offset;
530
+ continue;
531
+ }
532
+ if (offset - session > SESSION_LOOKBACK_DAYS) return null;
533
+ if (open) return { date: iso, closed: closed.reverse() };
534
+ closed.push(iso);
535
+ }
536
+ return null;
537
+ }
538
+
501
539
  /** How far the delayed feed runs behind the venue's trades. */
502
540
  export function delayedFeedLagMs(exchange: string | undefined): number {
503
541
  return SLOWER_DELAYED_FEEDS.has(canonicalExchange(exchange)) ? SLOWER_DELAYED_FEED_LAG_MS : DELAYED_FEED_LAG_MS;