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
@@ -16,12 +16,15 @@ import { qualifySharpeCadence } from "./sharpe-cadence";
16
16
  import { evidenceDay } from "./risk-evidence";
17
17
  import { FX_LIVE_RATE_MAX_DEVIATION } from "../../../market-data/coordinator/fx-legs";
18
18
  import {
19
+ accountFxPair,
20
+ accountFxReason,
19
21
  convertClosesToUsd,
20
22
  FX_DISAGREES,
21
23
  FX_NO_PAIR,
22
24
  FX_UNAVAILABLE,
23
25
  riskConversion,
24
26
  validateFxHistory,
27
+ type AccountFx,
25
28
  type FxCloses,
26
29
  type RiskConversion,
27
30
  } from "./risk-fx";
@@ -65,8 +68,10 @@ export interface RiskMarketSnapshot {
65
68
  fetchedAt: string;
66
69
  warnings: string[];
67
70
  brokerOptions?: import("./risk-options").PortfolioOptionBook;
68
- /** USD per unit of each listing or position currency of the holdings, at load time. */
71
+ /** USD per unit of the account currency and of each listing or position currency of the holdings, at load time. */
69
72
  fxRates?: Record<string, number>;
73
+ /** For an account not in USD: the daily closes its returns convert at, or why they cannot. */
74
+ accountFx?: AccountFx;
70
75
  }
71
76
  /**
72
77
  * The most holdings one load requests daily history for, largest current USD
@@ -81,13 +86,14 @@ const RISK_QUOTE_LIMIT = 500;
81
86
  const HISTORY_CONCURRENCY = 4;
82
87
  const MAX_FX_CURRENCIES = 24;
83
88
  export const BEYOND_SIZE_REASON = `Beyond the supported size: only the ${RISK_HISTORY_LIMIT} largest holdings by value are modelled`;
84
- // Listing venues as the quotes report them; MTUM lists on Cboe BZX, not Arca.
89
+ // Listing venues as the quotes report them; MTUM and QUAL list on Cboe BZX, not Arca.
85
90
  export const RISK_FACTOR_INSTRUMENTS: RiskInstrument[] = [
86
91
  ["SPY", "ARCA"],
87
92
  ["IWM", "ARCA"],
88
93
  ["IWD", "ARCA"],
89
94
  ["IWF", "ARCA"],
90
95
  ["MTUM", "BATS"],
96
+ ["QUAL", "BATS"],
91
97
  ["IEF", "ARCA"],
92
98
  ["HYG", "ARCA"],
93
99
  ].map(([symbol, exchange]) => ({ symbol: symbol!, exchange: exchange! }));
@@ -313,7 +319,9 @@ const largestFirst = (
313
319
  * competes at its value at the current rate and, when requested, also loads
314
320
  * its currency's daily FX history once per currency, so its closes convert to
315
321
  * USD. Holdings with no FX pair, positions the caller marks unsupported and
316
- * holdings past the limit get no history request; their entry says why. The
322
+ * holdings past the limit get no history request; their entry says why. An
323
+ * account in another currency than USD also loads that currency's current
324
+ * rate and daily FX history, which the model converts every return with. The
317
325
  * signal stops the fetch: no request starts once it
318
326
  * aborts, the ones in flight are cancelled, and the call rejects instead of
319
327
  * returning a partial snapshot nobody is waiting for.
@@ -323,6 +331,7 @@ export async function fetchPortfolioRiskMarket(
323
331
  client: RiskCloudClient = apiClient,
324
332
  now = new Date(),
325
333
  signal?: AbortSignal,
334
+ accountCurrency = "USD",
326
335
  ): Promise<RiskMarketSnapshot> {
327
336
  signal?.throwIfAborted();
328
337
  const normalized = (row: RiskInstrument): RiskInstrument => ({
@@ -417,14 +426,16 @@ export async function fetchPortfolioRiskMarket(
417
426
  }
418
427
  }
419
428
  // Current rates value a foreign holding: its rank, its mark, and its size when left out.
429
+ // The account currency's comes first: it values every holding.
430
+ const account = accountCurrency === "USD" ? null : accountCurrency;
420
431
  const fxCurrencies = [
421
432
  ...new Set(
422
- [...entries.values()].flatMap((row) => {
433
+ [...(account ? [account] : []), ...[...entries.values()].flatMap((row) => {
423
434
  const currency = resolveCurrencyUnit(
424
435
  listings.get(row.id)?.currency ?? (quotes.has(row.id) ? null : row.request.currency),
425
436
  ).currency;
426
437
  return currency && currency !== "USD" ? [currency] : [];
427
- }),
438
+ })],
428
439
  ),
429
440
  ].slice(0, MAX_FX_CURRENCIES);
430
441
  const fx = await Promise.allSettled(
@@ -539,6 +550,22 @@ export async function fetchPortfolioRiskMarket(
539
550
  }
540
551
  return loading;
541
552
  };
553
+ // The account currency's daily closes, shared with any listing that converts from it.
554
+ const accountWork: Promise<AccountFx | undefined> = (async () => {
555
+ if (!account) return undefined;
556
+ const failed = (error: string): AccountFx =>
557
+ ({ currency: account, pair: accountFxPair(account), closes: {}, first: null, asOf: null, error });
558
+ const conversion = riskConversion(account);
559
+ if (!conversion || conversion.divisor !== 1) return failed(accountFxReason(account, FX_NO_PAIR));
560
+ try {
561
+ const closes = await fxHistory(conversion);
562
+ return { currency: account, pair: accountFxPair(account), closes: Object.fromEntries(closes.closes),
563
+ first: closes.first, asOf: closes.asOf, error: null };
564
+ } catch (error) {
565
+ if (isAccessDenied(error) || signal?.aborted) throw error;
566
+ return failed(accountFxReason(account, message(error)));
567
+ }
568
+ })();
542
569
  const pending = instruments.flatMap(([id], index) =>
543
570
  requestedIds.has(id) ? [index] : [],
544
571
  );
@@ -596,7 +623,7 @@ export async function fetchPortfolioRiskMarket(
596
623
  }
597
624
  }),
598
625
  );
599
- const [, fred] = await Promise.all([
626
+ const [, fred, accountFx] = await Promise.all([
600
627
  historyWork,
601
628
  Promise.allSettled(
602
629
  ["DGS10", "VIXCLS"].map(async (id) =>
@@ -611,6 +638,7 @@ export async function fetchPortfolioRiskMarket(
611
638
  ),
612
639
  ),
613
640
  ),
641
+ accountWork,
614
642
  ]);
615
643
  signal?.throwIfAborted();
616
644
  for (const [index, result] of fred.entries()) {
@@ -628,25 +656,30 @@ export async function fetchPortfolioRiskMarket(
628
656
  fetchedAt: now.toISOString(),
629
657
  warnings,
630
658
  fxRates,
659
+ ...(accountFx ? { accountFx } : {}),
631
660
  };
632
661
  }
633
- /** What changes the requests: identity, size and whether a holding can enter the basket. */
634
- const cacheKey = (holdings: readonly RiskHoldingRequest[]) =>
662
+ /**
663
+ * What changes the requests: identity, size, whether a holding can enter the
664
+ * basket, and an account currency other than USD (whose daily FX the load adds).
665
+ */
666
+ const cacheKey = (holdings: readonly RiskHoldingRequest[], accountCurrency: string) =>
635
667
  [
636
668
  ...new Set(
637
669
  holdings.map(
638
670
  (row) => `${riskInstrumentId(row)}|${row.quantity ?? ""}|${row.currency ?? ""}|${row.unsupported ? 1 : 0}`,
639
671
  ),
640
672
  ),
641
- ].sort().join(",");
673
+ ].sort().join(",") + (accountCurrency === "USD" ? "" : `@${accountCurrency}`);
642
674
  export async function loadPortfolioRiskMarket(
643
675
  holdings: readonly RiskHoldingRequest[],
644
676
  force = false,
677
+ accountCurrency = "USD",
645
678
  ) {
646
679
  const { payload, stale, refreshError } = await loadCloudResource(
647
680
  portfolioRiskCache,
648
- cacheKey(holdings),
649
- () => fetchPortfolioRiskMarket(holdings),
681
+ cacheKey(holdings, accountCurrency),
682
+ () => fetchPortfolioRiskMarket(holdings, undefined, undefined, undefined, accountCurrency),
650
683
  { force },
651
684
  );
652
685
  return {
@@ -4,9 +4,26 @@ import { dailyFxCloses, FxHistoryError, type FxCloses } from "../../../market-da
4
4
  import { getPublishedUsEquityCalendarDay } from "../../../market-data/published-us-sessions";
5
5
  import type { PricePoint } from "../../../types/financials";
6
6
  import { resolveCurrencyUnit } from "../../../utils/currency-units";
7
+ import type { DatedReturn } from "./metrics";
7
8
 
8
9
  export type { FxCloses };
9
10
 
11
+ /**
12
+ * The daily closes that restate a non-USD account's returns, as the market
13
+ * load keeps them (plain data, so the cache can hold them), or why there are none.
14
+ */
15
+ export interface AccountFx {
16
+ currency: string;
17
+ /** "USD/TRY" or "EUR/USD", as the pair trades; null when no daily pair prices the currency. */
18
+ pair: string | null;
19
+ /** USD per unit of the account currency at each completed daily close; empty with an error. */
20
+ closes: Record<string, number>;
21
+ first: string | null;
22
+ asOf: string | null;
23
+ /** Why returns cannot convert into the account currency; every holding is left out with it. */
24
+ error: string | null;
25
+ }
26
+
10
27
  /** How a listing quoted in another currency is restated in USD. */
11
28
  export interface RiskConversion {
12
29
  /** The listing currency as quoted, sub-units included (GBp, ZAc). */
@@ -35,6 +52,50 @@ const MAX_LOCAL_CLOSED_DAYS = 10;
35
52
  const BASKET_CALENDAR = "NYSE";
36
53
  const DAY_MS = 86_400_000;
37
54
 
55
+ /** The daily pair of an account currency as dealers quote it ("USD/TRY", "EUR/USD"), or null when none prices it. */
56
+ export function accountFxPair(currency: string): string | null {
57
+ const leg = fxLegForCurrency(currency);
58
+ return !leg ? null : leg.invert ? `USD/${leg.currency}` : `${leg.currency}/USD`;
59
+ }
60
+
61
+ /**
62
+ * Why the account currency's daily closes cannot convert a holding: the
63
+ * problem a listing's own conversion would report (stale, unavailable,
64
+ * gapped, disagreeing with the current rate), said of the account's pair.
65
+ */
66
+ export function accountFxReason(currency: string, problem: string): string {
67
+ const pair = accountFxPair(currency);
68
+ if (!pair) return `No daily FX pair converts USD into the account currency ${currency}`;
69
+ const tail = /^Foreign holdings: (daily FX .*)$/.exec(problem)?.[1];
70
+ return `${pair} ${tail ?? "daily FX closes unavailable"}`;
71
+ }
72
+
73
+ /**
74
+ * Restates daily USD returns in the account currency at the FX closes of each
75
+ * return's own start and end dates: the holding's USD value is divided by the
76
+ * USD price of one account unit on both dates, so a return becomes
77
+ * (1 + r) * fx(start) / fx(end) - 1. A listing already converted from its own
78
+ * currency to USD therefore converts at the cross of the two closes of the
79
+ * same date. Returns before the first FX close or after the last are dropped,
80
+ * so the series starts and ends with the FX history; a return inside it with an
81
+ * endpoint that has no FX close rejects the whole series instead of bridging it.
82
+ */
83
+ export function convertReturnsToAccount(
84
+ returns: readonly DatedReturn[],
85
+ fx: Pick<AccountFx, "currency" | "closes" | "first" | "asOf">,
86
+ ): DatedReturn[] {
87
+ if (!fx.first || !fx.asOf) throw new Error(accountFxReason(fx.currency, FX_UNAVAILABLE));
88
+ const converted: DatedReturn[] = [];
89
+ for (const point of returns) {
90
+ if (point.startDateKey < fx.first || point.dateKey > fx.asOf) continue;
91
+ const start = fx.closes[point.startDateKey],
92
+ end = fx.closes[point.dateKey];
93
+ if (!(start! > 0) || !(end! > 0)) throw new Error(accountFxReason(fx.currency, FX_GAP));
94
+ converted.push({ ...point, value: (1 + point.value) * (start! / end!) - 1 });
95
+ }
96
+ return converted;
97
+ }
98
+
38
99
  /** The pair that restates a listing currency in USD, or null when there is none. */
39
100
  export function riskConversion(listingCurrency: string): RiskConversion | null {
40
101
  const unit = resolveCurrencyUnit(listingCurrency);
@@ -15,6 +15,7 @@ import {
15
15
  RISK_VIEWS,
16
16
  riskEvidence,
17
17
  riskPercentile,
18
+ riskReturnsCurrency,
18
19
  riskValue,
19
20
  sinceFallbackNote,
20
21
  sinceFiledMethod,
@@ -142,7 +143,7 @@ export const portfolioRiskHeadless: HeadlessPaneDefinition<"bundle"> = {
142
143
  "Explicit local evidence for account returns and attribution",
143
144
  ],
144
145
  limitations: [
145
- `Fixed-current-weight USD equity basket over the holdings that qualify, largest ${RISK_HISTORY_LIMIT} by value; foreign listings convert to USD at daily FX closes; metadata.coverage states the covered share of market value and each holding left out.`,
146
+ `Fixed-current-weight equity basket over the holdings that qualify, largest ${RISK_HISTORY_LIMIT} by value, in the account's currency; foreign listings convert to USD at daily FX closes, and a non-USD account's returns then convert into its currency at its own daily FX closes; metadata.coverage states the covered share of market value and each holding left out.`,
146
147
  "--since filed buys the 13F's own weights at the filing-date close and holds them (--weights current holds today's weights fixed instead).",
147
148
  "No account performance inferred.",
148
149
  "ETF price-return factor proxies.",
@@ -246,6 +247,7 @@ export const portfolioRiskHeadless: HeadlessPaneDefinition<"bundle"> = {
246
247
  ctx.apiClient,
247
248
  new Date(),
248
249
  ctx.signal,
250
+ local.portfolio.currency,
249
251
  ),
250
252
  localOnly
251
253
  ? Promise.resolve(undefined)
@@ -270,7 +272,12 @@ export const portfolioRiskHeadless: HeadlessPaneDefinition<"bundle"> = {
270
272
  // Options for views the report does not print are named, not dropped silently; the fallback from --since opens the report.
271
273
  const fallback = sinceFallbackNote(local.portfolio);
272
274
  const shifted = ["equity-shift", "rate-shift", "vol-shift"].filter((key) => args.options[key] != null);
275
+ // A non-USD account's returns were converted, and the basket views say into what.
276
+ const returnsCurrency = riskReturnsCurrency(model);
273
277
  const notices = [
278
+ ...(returnsCurrency && (!requestedView || (BASKET_VIEWS.has(requestedView) && requestedView !== "holdings"))
279
+ ? [`${returnsCurrency[0]!.toUpperCase()}${returnsCurrency.slice(1)}.`]
280
+ : []),
274
281
  ...(model.window.kind === "since-filed" ? [sinceFiledMethod(model.window, model.benchmark)] : []),
275
282
  ...(args.options.since === "filed" && model.window.kind !== "since-filed" ? [fallback] : []),
276
283
  ...(requestedView && requestedView !== "stress" && shifted.length ? [`${shifted.map((key) => `--${key}`).join(", ")} change${shifted.length === 1 ? "s" : ""} the stress view only, not --view ${requestedView}`] : []),
@@ -335,6 +342,8 @@ export const portfolioRiskHeadless: HeadlessPaneDefinition<"bundle"> = {
335
342
  ...(basketView && model.coverage
336
343
  ? { coverage: coverageSummary(model.coverage, COMPACT_LEFT_OUT_LIMIT) }
337
344
  : {}),
345
+ // The currency a non-USD account's returns and amounts are in, and the daily pair they converted at.
346
+ ...(basketView && model.accountConversion ? { accountConversion: model.accountConversion } : {}),
338
347
  asOf: model.fetchedAt,
339
348
  ...(notices.length ? { notices } : {}),
340
349
  },
@@ -36,6 +36,7 @@ import {
36
36
  type RiskHoldingRequest,
37
37
  type RiskMarketSnapshot,
38
38
  } from "./risk-client";
39
+ import { accountFxPair, accountFxReason, convertReturnsToAccount, FX_UNAVAILABLE, type AccountFx } from "./risk-fx";
39
40
 
40
41
  interface PortfolioRiskHolding {
41
42
  id: string;
@@ -43,7 +44,10 @@ interface PortfolioRiskHolding {
43
44
  exchange: string;
44
45
  quantity: number;
45
46
  currency: string;
46
- /** Signed USD value at the current mark; only these marks weight the basket. */
47
+ /**
48
+ * Signed value at the current USD mark, in the account currency at its
49
+ * current rate; only these marks weight the basket.
50
+ */
47
51
  value: number | null;
48
52
  /** Weight in the covered basket, null outside it. */
49
53
  weight: number | null;
@@ -59,6 +63,7 @@ interface PortfolioRiskHolding {
59
63
  /** The listing currency its closes were converted from at daily FX closes; null for a USD listing. */
60
64
  convertedFrom: string | null;
61
65
  historyAsOf: string | null;
66
+ /** Daily returns in the account currency. */
62
67
  returns: DatedReturn[];
63
68
  error: string | null;
64
69
  /** Why the holding is outside the basket; null when it qualifies. */
@@ -325,6 +330,24 @@ function outsideValue(
325
330
  const value = resolvePortfolioMarketValue(metrics, unit != null && Number.isFinite(unit) ? unit : null)?.gross;
326
331
  return value != null && Number.isFinite(value) ? value : null;
327
332
  }
333
+ /** USD returns in the account currency, or none with the reason its daily closes cannot convert them. */
334
+ function accountReturns(returns: readonly DatedReturn[], account: AccountFx): { returns: DatedReturn[]; error: string | null } {
335
+ if (account.error) return { returns: [], error: account.error };
336
+ try {
337
+ return { returns: convertReturnsToAccount(returns, account), error: null };
338
+ } catch (error) {
339
+ return { returns: [], error: error instanceof Error ? error.message : accountFxReason(account.currency, FX_UNAVAILABLE) };
340
+ }
341
+ }
342
+ /**
343
+ * "returns in TRY at daily USD/TRY closes": the currency the basket views of a
344
+ * non-USD account are in and how they got there; null for a USD account, or
345
+ * when its daily closes were unavailable and nothing converted.
346
+ */
347
+ export function riskReturnsCurrency(model: Pick<PortfolioRiskModel, "accountConversion"> | null | undefined): string | null {
348
+ const conversion = model?.accountConversion;
349
+ return conversion ? `returns in ${conversion.currency} at daily ${conversion.pair} closes` : null;
350
+ }
328
351
  // The epsilon keeps 29 of 100 at 29%, not the 28 that 0.29 * 100 rounds down to.
329
352
  const coveragePercent = (share: number) => Math.floor(share * 100 + 1e-9);
330
353
  const holdingCount = (count: number) => `${count} holding${count === 1 ? "" : "s"}`;
@@ -441,7 +464,27 @@ export function buildPortfolioRisk(
441
464
  const byId = new Map(
442
465
  market.histories.map((row) => [riskInstrumentId(row.instrument), row]),
443
466
  );
444
- const rates = market.fxRates ?? {};
467
+ // A non-USD account converts every USD return at its currency's daily closes and every USD mark at its current rate.
468
+ const account: AccountFx | null =
469
+ portfolio.currency === "USD"
470
+ ? null
471
+ : market.accountFx?.currency === portfolio.currency
472
+ ? market.accountFx
473
+ : {
474
+ currency: portfolio.currency,
475
+ pair: accountFxPair(portfolio.currency),
476
+ closes: {},
477
+ first: null,
478
+ asOf: null,
479
+ error: accountFxReason(portfolio.currency, FX_UNAVAILABLE),
480
+ };
481
+ // Without a current rate the latest daily close values the account.
482
+ const latestAccountClose = account?.asOf ? account.closes[account.asOf] : undefined;
483
+ const rates: Readonly<Record<string, number>> =
484
+ account && !((market.fxRates?.[account.currency] ?? 0) > 0) && latestAccountClose
485
+ ? { ...market.fxRates, [account.currency]: latestAccountClose }
486
+ : (market.fxRates ?? {});
487
+ const accountRate = account ? rates[account.currency] : undefined;
445
488
  const holdings: PortfolioRiskHolding[] = portfolioRiskTickers(
446
489
  tickers,
447
490
  portfolio.id,
@@ -471,14 +514,11 @@ export function buildPortfolioRisk(
471
514
  const listingCurrency =
472
515
  quote?.currency ?? close?.currency ?? history?.listing?.currency ?? ticker.metadata.currency;
473
516
  const foreign = !convertedFrom && listingCurrency !== "USD";
474
- const error =
517
+ const usdError =
475
518
  syntheticPositionUnsupportedReason(ticker, "USD", portfolio.id) ??
476
519
  (lots.some((row) => !Number.isFinite(row.shares))
477
520
  ? "Invalid position quantity"
478
521
  : null) ??
479
- (portfolio.currency !== "USD"
480
- ? "Historical account-currency FX is required"
481
- : null) ??
482
522
  (ticker.metadata.currency &&
483
523
  (convertedFrom ?? mark?.currency) &&
484
524
  ticker.metadata.currency !== (convertedFrom ?? mark?.currency)
@@ -490,10 +530,13 @@ export function buildPortfolioRisk(
490
530
  : null) ??
491
531
  history?.error ??
492
532
  (!history ? "Daily history unavailable" : null);
533
+ // A holding with a USD history converts into the account currency, or is left out with the reason it cannot.
534
+ const inAccount = account && !usdError ? accountReturns(history?.returns ?? [], account) : null;
535
+ const error = usdError ?? inAccount?.error ?? null;
493
536
  const priceDate = close ? close.date : regularClose ? regularClose.date : quoteDate(quote?.lastUpdated);
494
537
  const marked =
495
538
  mark &&
496
- mark.currency === portfolio.currency &&
539
+ mark.currency === "USD" &&
497
540
  !mark.stale &&
498
541
  Number.isFinite(mark.price) &&
499
542
  mark.price > 0 &&
@@ -506,8 +549,9 @@ export function buildPortfolioRisk(
506
549
  error?.startsWith("Short positions"))
507
550
  ? quantity * mark.price
508
551
  : null;
509
- const value = marked != null && Number.isFinite(marked) ? marked : null;
510
- const returns = history?.returns ?? [];
552
+ const accountValue = marked == null || !account ? marked : accountRate! > 0 ? marked / accountRate! : null;
553
+ const value = accountValue != null && Number.isFinite(accountValue) ? accountValue : null;
554
+ const returns = inAccount?.returns ?? history?.returns ?? [];
511
555
  const leftOut =
512
556
  error ??
513
557
  (value == null || value <= 0
@@ -529,7 +573,8 @@ export function buildPortfolioRisk(
529
573
  priceAsOf: priceDate,
530
574
  markSource: close ? "close" : regularClose ? "regular-close" : quote ? "quote" : null,
531
575
  convertedFrom,
532
- historyAsOf: history?.asOf ?? null,
576
+ // Converted returns end with the account currency's last daily close.
577
+ historyAsOf: (inAccount ? inAccount.returns.at(-1)?.dateKey : null) ?? history?.asOf ?? null,
533
578
  returns,
534
579
  error,
535
580
  leftOut,
@@ -610,7 +655,14 @@ export function buildPortfolioRisk(
610
655
  );
611
656
  return (instrument && byId.get(riskInstrumentId(instrument))?.returns) || [];
612
657
  };
613
- const benchmark = proxy(RISK_BENCHMARK);
658
+ // SPY's USD returns are the market factor and the equity stress; the risk
659
+ // rows compare the basket with SPY in the basket's own currency. A session
660
+ // without an account close is a gap in that benchmark, never bridged, so it
661
+ // cannot blank the holdings the closes do cover.
662
+ const spy = proxy(RISK_BENCHMARK);
663
+ const benchmark = account
664
+ ? accountReturns(spy.filter((point) => account.closes[point.startDateKey] && account.closes[point.dateKey]), account).returns
665
+ : spy;
614
666
  // Since the filing only for a portfolio that has one; any other keeps the trailing window and says why.
615
667
  const filed = window.since === "filed" ? portfolio.importedFrom?.filed ?? null : null;
616
668
  if (window.since === "filed" && !filed)
@@ -647,12 +699,13 @@ export function buildPortfolioRisk(
647
699
  const metrics = filed
648
700
  ? sinceBasketRisk(sample, filed, RISK_BENCHMARK)
649
701
  : rollingBasketRisk(sample, undefined, RISK_BENCHMARK);
702
+ // Factor proxies stay in USD: a beta is the account-currency move per unit of the factor as it trades.
650
703
  const factors = [
651
- { id: "market", label: "Market (SPY)", series: benchmark },
704
+ { id: "market", label: "Market (SPY)", series: spy },
652
705
  {
653
706
  id: "size",
654
707
  label: "Size (IWM - SPY)",
655
- series: subtractReturns(proxy("IWM"), benchmark),
708
+ series: subtractReturns(proxy("IWM"), spy),
656
709
  },
657
710
  {
658
711
  id: "value",
@@ -662,7 +715,12 @@ export function buildPortfolioRisk(
662
715
  {
663
716
  id: "momentum",
664
717
  label: "Momentum (MTUM - SPY)",
665
- series: subtractReturns(proxy("MTUM"), benchmark),
718
+ series: subtractReturns(proxy("MTUM"), spy),
719
+ },
720
+ {
721
+ id: "quality",
722
+ label: "Quality (QUAL - SPY)",
723
+ series: subtractReturns(proxy("QUAL"), spy),
666
724
  },
667
725
  { id: "rates", label: "Rates (IEF)", series: proxy("IEF") },
668
726
  {
@@ -690,7 +748,7 @@ export function buildPortfolioRisk(
690
748
  {
691
749
  id: "equity",
692
750
  label: `Index ${shifts.equity > 0 ? "+" : ""}${shifts.equity}%`,
693
- series: benchmark,
751
+ series: spy,
694
752
  shock: shifts.equity / 100,
695
753
  source: "SPY price return",
696
754
  },
@@ -772,7 +830,7 @@ export function buildPortfolioRisk(
772
830
  asOf: row.priceAsOf,
773
831
  detail: row.leftOut
774
832
  ? row.leftOut
775
- : `${row.quantity} shares; ${formatNumber(row.value ?? undefined)} ${row.currency}${row.markSource === "close" || row.markSource === "regular-close" ? ` at ${row.priceAsOf} close` : ""}${row.convertedFrom ? ` from ${row.convertedFrom} at daily FX` : ""}; history ${row.historyAsOf}`,
833
+ : `${row.quantity} shares; ${formatNumber(row.value ?? undefined)} ${portfolio.currency}${row.markSource === "close" || row.markSource === "regular-close" ? ` at ${row.priceAsOf} close` : ""}${row.convertedFrom ? ` from ${row.convertedFrom} at daily FX` : ""}; history ${row.historyAsOf}`,
776
834
  ...(row.leftOut ? { leftOut: row.leftOut } : {}),
777
835
  })),
778
836
  correlation: correlation.map((row) => ({
@@ -947,6 +1005,11 @@ export function buildPortfolioRisk(
947
1005
  notes,
948
1006
  fetchedAt: market.fetchedAt,
949
1007
  benchmark: RISK_BENCHMARK,
1008
+ /** The currency a non-USD account's returns converted into, at the daily closes of `pair`; null for USD. */
1009
+ accountConversion:
1010
+ account && !account.error && account.pair
1011
+ ? { currency: account.currency, pair: account.pair, asOf: account.asOf }
1012
+ : null,
950
1013
  window: filed
951
1014
  ? {
952
1015
  kind: "since-filed" as const,
@@ -58,6 +58,7 @@ import {
58
58
  riskPercentile,
59
59
  riskAmountBase,
60
60
  riskEvidence,
61
+ riskReturnsCurrency,
61
62
  riskValue,
62
63
  RISK_VIEWS,
63
64
  type PortfolioRiskModel,
@@ -301,7 +302,7 @@ export function PortfolioRiskPane({ focused, width, height }: PaneProps) {
301
302
  const loader = useCallback(
302
303
  async (force: boolean) => {
303
304
  const [market, brokerOptions] = await Promise.all([
304
- loadPortfolioRiskMarket(requests, force),
305
+ loadPortfolioRiskMarket(requests, force, portfolio!.currency),
305
306
  loadPortfolioOptionBook(localTickers, portfolio!),
306
307
  ]);
307
308
  return { ...market, brokerOptions };
@@ -466,6 +467,9 @@ export function PortfolioRiskPane({ focused, width, height }: PaneProps) {
466
467
  // What the basket views leave out, unless the body already says why they show nothing.
467
468
  const coverageText = model && BASKET_VIEWS.has(view) && !basketBody ? riskCoverageHeadline(model.coverage) : null;
468
469
  const asOfDate = model?.rows[view].find((row) => row.asOf)?.asOf?.slice(0, 10) ?? null;
470
+ // The views estimated from returns name the currency a non-USD account's returns were converted into.
471
+ const returnsCurrency = BASKET_VIEWS.has(view) && view !== "holdings" ? riskReturnsCurrency(model) : null;
472
+ const amountBase = rows.map(riskAmountBase).find(Boolean);
469
473
  // Dated evidence only when a row has a date; a missing date is not a claim about history.
470
474
  const asOfText = !model
471
475
  ? null
@@ -473,7 +477,9 @@ export function PortfolioRiskPane({ focused, width, height }: PaneProps) {
473
477
  ...(basketBody ? [] : [
474
478
  ...(asOfDate && sharedEvidence ? [sharedEvidence] : []),
475
479
  // The value the tail losses in money are taken on names the currency too.
476
- rows.map(riskAmountBase).find(Boolean) ?? model.portfolio.currency,
480
+ ...(amountBase
481
+ ? [amountBase, ...(returnsCurrency ? [returnsCurrency] : [])]
482
+ : [returnsCurrency ?? model.portfolio.currency]),
477
483
  ...(asOfDate ? [asOfDate] : []),
478
484
  ...(EVIDENCE_VIEWS.has(view) && model.evidence ? [model.evidence.source] : []),
479
485
  ]),
@@ -76,6 +76,9 @@ const BROKER_FX_RATES: Readonly<Record<string, number>> = {
76
76
  HKD: 0.128,
77
77
  JPY: 0.0068,
78
78
  TWD: 0.031,
79
+ // An account currency, and a listing currency with no daily FX pair.
80
+ TRY: 0.0202,
81
+ SAR: 0.2667,
79
82
  };
80
83
  export const BROKER_PORTFOLIO: Portfolio = { id: "broker", name: "Broker account", currency: "USD" };
81
84
 
@@ -21,18 +21,20 @@ function useTrending(symbol?: string) {
21
21
  useAutoRefresh(resource.updatedAt, resource.load);
22
22
  return { resource, session, access };
23
23
  }
24
- /** Optional profile context. A failure here never hides or blocks the description. */
24
+ /**
25
+ * Optional profile context. A failure here never hides or blocks the description,
26
+ * and a symbol with no published research hours shows no section at all: the
27
+ * Overview leaves out what it has nothing to say about.
28
+ */
25
29
  export function TrendingSummary({ symbol, onOpen }: { symbol: string; onOpen?: () => void }) {
26
30
  const { resource, access } = useTrending(symbol);
27
31
  const data = resource.data?.payload;
28
32
  const row = data?.rows[0];
29
- if (!access.signedIn || !data) return null;
33
+ if (!access.signedIn || !data || !row) return null;
30
34
  return <Box flexDirection="column">
31
35
  <SectionHeading title="Gloom Trending" />
32
- {row ? <>
33
- <KeyValueRow label="Research rank" value={`#${row.rank}`} detail={`Today · ${number(row.researchUnits)} research hours`} />
34
- <KeyValueRow label="Latest-hour Z" value={signed(row.zScore)} detail={data.asOf ? date(data.asOf) : undefined} />
35
- </> : <EmptyState title={data.status === "ready" ? data.entitlement === "preview" ? "Outside the current preview." : "No published research hours today." : emptyAttention(data)} />}
36
+ <KeyValueRow label="Research rank" value={`#${row.rank}`} detail={`Today · ${number(row.researchUnits)} research hours`} />
37
+ <KeyValueRow label="Latest-hour Z" value={signed(row.zScore)} detail={data.asOf ? date(data.asOf) : undefined} />
36
38
  {onOpen ? <ActionRow label="Open research attention" onPress={onOpen} /> : null}
37
39
  </Box>;
38
40
  }
@@ -2,7 +2,7 @@ import { getSharedMarketDataCoordinator, MarketDataCoordinator, resolveEntryValu
2
2
  import type { ChartRequest, InstrumentRef } from "../../../market-data/request-types";
3
3
  import type { QueryEntry } from "../../../market-data/result-types";
4
4
  import type { DataProvider } from "../../../types/data-provider";
5
- import type { PricePoint } from "../../../types/financials";
5
+ import type { ListingDelisting, PricePoint } from "../../../types/financials";
6
6
  import type { BacktestBar } from "./rules";
7
7
  import { isFiniteNumber } from "../../../utils/guards";
8
8
 
@@ -27,6 +27,8 @@ export interface BacktestHistory {
27
27
  skippedDates: string[];
28
28
  undatedSkipped: number;
29
29
  fetchedAt: number;
30
+ /** The listing no longer trades: its last bar is its last session, and no later one will come. */
31
+ delisting?: ListingDelisting;
30
32
  }
31
33
 
32
34
  /**
@@ -85,5 +87,6 @@ export async function loadBacktestHistory(
85
87
  options.signal?.throwIfAborted();
86
88
  const history = toBacktestBars(resolveEntryValue(entry) ?? [], entry.history?.nonPositiveCloseDates);
87
89
  if (!history.bars.length) throw new Error(entry.error?.message ?? "Daily price history unavailable");
88
- return { symbol, ...history, fetchedAt: entry.fetchedAt ?? Date.now() };
90
+ const delisting = entry.history?.delisting;
91
+ return { symbol, ...history, fetchedAt: entry.fetchedAt ?? Date.now(), ...(delisting ? { delisting } : {}) };
89
92
  }
@@ -2,6 +2,7 @@ import { parsePublicTickerKey } from "../../../utils/exchanges";
2
2
  import { futuresGenericCaption, futuresGenericListing } from "../../../utils/futures-generic";
3
3
  import type { BacktestHistory } from "./client";
4
4
  import { runBacktest, type BacktestOptions, type BacktestResult } from "./engine";
5
+ import { delistingLabel } from "../../../market-data/quotes/delisting";
5
6
  import type { BacktestBar, Rule } from "./rules";
6
7
 
7
8
  /** Which served sessions a run tests. */
@@ -39,10 +40,12 @@ export interface HistoryBacktestResult extends BacktestResult {
39
40
  * and a session still forming, which is left out unless asked for so the
40
41
  * newest complete session ends the run and a repeat gives the same answer.
41
42
  * An end after the newest complete session is warned too: sessions still to
42
- * come up to it change the run until it has passed.
43
+ * come up to it change the run until it has passed. A listing that no longer
44
+ * trades has none to come: a run that reaches its last session ends there,
45
+ * says so, and is repeatable.
43
46
  */
44
47
  export function backtestWindow(
45
- history: Pick<BacktestHistory, "bars" | "skippedDates" | "undatedSkipped">,
48
+ history: Pick<BacktestHistory, "bars" | "skippedDates" | "undatedSkipped" | "delisting"> & { symbol?: string },
46
49
  options: BacktestWindowOptions = {},
47
50
  ): { bars: BacktestBar[]; warnings: string[]; repeatable: boolean } {
48
51
  const end = options.end?.trim() || null;
@@ -70,6 +73,16 @@ export function backtestWindow(
70
73
  + `${through ? `: the test runs through ${through}, the last complete session` : ""}.`);
71
74
  }
72
75
  const newestComplete = history.bars.findLast((bar) => !bar.partial)?.date;
76
+ const delisting = history.delisting;
77
+ if (delisting) {
78
+ const listing = `${history.symbol ? `${parsePublicTickerKey(history.symbol).symbol}: ` : ""}${delistingLabel(delisting)}`;
79
+ if (through && through >= delisting.lastTradeDate) {
80
+ warnings.push(`${listing}. The run ends on its last session, ${through}, and a position still held`
81
+ + " is valued at that close, not at anything holders received after it.");
82
+ return { bars, warnings, repeatable: true };
83
+ }
84
+ warnings.push(`${listing}.`);
85
+ }
73
86
  const repeatable = !!end && !!newestComplete && end <= newestComplete;
74
87
  if (end && !repeatable && through) {
75
88
  warnings.push(`The end date ${end} is after the newest complete session, so the run ends at ${through}`
@@ -80,7 +93,7 @@ export function backtestWindow(
80
93
 
81
94
  /** The run over a loaded history: its window's bars, then the engine, every left-out session warned first. */
82
95
  export function runHistoryBacktest(
83
- history: Pick<BacktestHistory, "bars" | "skippedDates" | "undatedSkipped">,
96
+ history: Pick<BacktestHistory, "bars" | "skippedDates" | "undatedSkipped" | "delisting"> & { symbol?: string },
84
97
  entry: Rule,
85
98
  exit: Rule,
86
99
  options: BacktestOptions & BacktestWindowOptions,