gloomberb 0.19.0 → 0.19.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 (304) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +2 -1
  4. package/patches/electrobun@1.18.1.patch +23 -1
  5. package/src/api-client/app-attention.ts +3 -0
  6. package/src/api-client/beneficial-owners.ts +3 -0
  7. package/src/api-client/company-kpis.ts +3 -0
  8. package/src/api-client/data.ts +2 -1
  9. package/src/api-client/errors.ts +38 -0
  10. package/src/api-client/hiring.ts +3 -0
  11. package/src/api-client/index.ts +4 -0
  12. package/src/api-client/ipo.ts +24 -1
  13. package/src/api-client/mna.ts +3 -0
  14. package/src/api-client/paths.ts +9 -1
  15. package/src/api-client/roadmap.ts +85 -0
  16. package/src/api-client/types.ts +12 -0
  17. package/src/app/runtime/startup.ts +3 -1
  18. package/src/architecture/core-builtin-allowlist.json +2 -2
  19. package/src/capabilities/chart-series.ts +2 -1
  20. package/src/cli/commands/market.ts +44 -16
  21. package/src/cli/commands/news-search.ts +22 -0
  22. package/src/cli/commands/overview.ts +84 -19
  23. package/src/cli/commands/search.ts +13 -13
  24. package/src/cli/commands/ticker.ts +32 -2
  25. package/src/cli/context.ts +1 -0
  26. package/src/cli/errors.ts +17 -2
  27. package/src/cli/glossary.ts +8 -1
  28. package/src/cli/history-rows.ts +35 -7
  29. package/src/cli/index-roots.ts +0 -7
  30. package/src/cli/known-gaps.ts +11 -0
  31. package/src/cli/listing-arg.ts +74 -11
  32. package/src/cli/pane-functions/capabilities.ts +1 -0
  33. package/src/cli/pane-functions/discovery.ts +21 -1
  34. package/src/cli/pane-functions/freshness.ts +7 -1
  35. package/src/cli/pane-functions/function-help.ts +7 -0
  36. package/src/cli/pane-functions/index.ts +6 -3
  37. package/src/cli/quote-answer.ts +30 -7
  38. package/src/cli/saved-copy-persistence.ts +88 -0
  39. package/src/components/command-bar/list/model.ts +5 -0
  40. package/src/components/command-bar/routes/root/runtime.ts +29 -8
  41. package/src/components/command-bar/selection-runtime.ts +64 -32
  42. package/src/components/command-bar/surface/index.tsx +2 -0
  43. package/src/components/layout/detached-pane-shell.tsx +2 -2
  44. package/src/components/layout/header.tsx +2 -2
  45. package/src/components/layout/pane/capture-stamp.ts +2 -2
  46. package/src/components/layout/pane/footer/index.tsx +23 -6
  47. package/src/components/layout/pane/footer/model.ts +116 -16
  48. package/src/components/layout/pane/header.tsx +2 -2
  49. package/src/components/layout/pane/status-footer.ts +37 -3
  50. package/src/components/layout/status-widget-space.ts +9 -0
  51. package/src/components/onboarding/onboarding-frame.tsx +2 -1
  52. package/src/components/onboarding/onboarding-wizard.tsx +6 -3
  53. package/src/components/ticker/list-table-view.tsx +4 -20
  54. package/src/components/ui/display.tsx +29 -3
  55. package/src/components/ui/icon.tsx +12 -3
  56. package/src/core/app-services.ts +11 -2
  57. package/src/data/app-persistence.ts +9 -7
  58. package/src/data/plugin-cache.ts +20 -3
  59. package/src/data/plugin-state-store.ts +81 -24
  60. package/src/data/resource-store.ts +130 -58
  61. package/src/data/saved-copies.ts +8 -0
  62. package/src/data/session-store.ts +31 -8
  63. package/src/data/sqlite/access.ts +313 -0
  64. package/src/data/sqlite/checkpointer.ts +198 -0
  65. package/src/data/sqlite/database.ts +47 -4
  66. package/src/data/sqlite/retry.ts +50 -9
  67. package/src/data/ticker-store.ts +57 -23
  68. package/src/docs/snapshot.json +32 -5
  69. package/src/i18n/es.ts +2 -4
  70. package/src/i18n/ja.ts +2 -4
  71. package/src/i18n/ko.ts +2 -4
  72. package/src/i18n/zh-cn.ts +2 -4
  73. package/src/i18n/zh-tw.ts +2 -4
  74. package/src/market-data/depositary-pair.ts +25 -1
  75. package/src/market-data/quotes/metadata.ts +7 -0
  76. package/src/market-data/realized-volatility.ts +4 -2
  77. package/src/news/aggregator.ts +7 -1
  78. package/src/news/types.ts +6 -0
  79. package/src/plugins/auto-update-node.ts +10 -6
  80. package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
  81. package/src/plugins/builtin/alerts/cli.ts +34 -11
  82. package/src/plugins/builtin/alerts/event-rows.ts +24 -3
  83. package/src/plugins/builtin/alerts/events-pane.tsx +15 -6
  84. package/src/plugins/builtin/alerts/headless.ts +10 -6
  85. package/src/plugins/builtin/cds/client.ts +142 -30
  86. package/src/plugins/builtin/cds/headless.ts +35 -8
  87. package/src/plugins/builtin/cds/model.ts +145 -18
  88. package/src/plugins/builtin/cds/pane.tsx +16 -5
  89. package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
  90. package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
  91. package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
  92. package/src/plugins/builtin/chart-composer/headless-schema.ts +5 -0
  93. package/src/plugins/builtin/chart-composer/index.tsx +4 -5
  94. package/src/plugins/builtin/chart-composer/pane.tsx +5 -1
  95. package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
  96. package/src/plugins/builtin/chat/controller/index.ts +5 -1
  97. package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
  98. package/src/plugins/builtin/chat/controller/state.ts +25 -2
  99. package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
  100. package/src/plugins/builtin/chat/controller/utils.ts +7 -2
  101. package/src/plugins/builtin/chat/status-preview.ts +79 -0
  102. package/src/plugins/builtin/chat/status-widget.tsx +147 -10
  103. package/src/plugins/builtin/chat/unread-badge.ts +51 -10
  104. package/src/plugins/builtin/clinical-trials/client.ts +49 -4
  105. package/src/plugins/builtin/clinical-trials/model.ts +86 -22
  106. package/src/plugins/builtin/clinical-trials/pane.tsx +46 -27
  107. package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
  108. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +87 -9
  109. package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
  110. package/src/plugins/builtin/commodity-balances/client.ts +4 -1
  111. package/src/plugins/builtin/company-attention/headless.ts +2 -1
  112. package/src/plugins/builtin/company-attention/pane.tsx +3 -0
  113. package/src/plugins/builtin/company-kpis/headless.ts +8 -3
  114. package/src/plugins/builtin/company-kpis/model.ts +73 -5
  115. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  116. package/src/plugins/builtin/composite-plugins.ts +2 -1
  117. package/src/plugins/builtin/cot/client.ts +28 -2
  118. package/src/plugins/builtin/cot/headless.ts +4 -1
  119. package/src/plugins/builtin/cot/index.tsx +4 -2
  120. package/src/plugins/builtin/cot/model.ts +58 -7
  121. package/src/plugins/builtin/cot/pane.tsx +6 -2
  122. package/src/plugins/builtin/cpi/client.ts +6 -5
  123. package/src/plugins/builtin/credit-boards/series.ts +3 -4
  124. package/src/plugins/builtin/credit-conditions/client.ts +22 -14
  125. package/src/plugins/builtin/credit-documents/headless.ts +3 -3
  126. package/src/plugins/builtin/crypto-board/client.ts +5 -5
  127. package/src/plugins/builtin/crypto-board/series.ts +4 -4
  128. package/src/plugins/builtin/debt-maturities/series.ts +3 -3
  129. package/src/plugins/builtin/doe/client.ts +6 -5
  130. package/src/plugins/builtin/doe/headless.ts +3 -2
  131. package/src/plugins/builtin/doe/pane.tsx +13 -3
  132. package/src/plugins/builtin/doe/prices.ts +2 -3
  133. package/src/plugins/builtin/doe/series.ts +4 -4
  134. package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
  135. package/src/plugins/builtin/earnings-calls/data.ts +37 -17
  136. package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
  137. package/src/plugins/builtin/earnings-calls/pane.tsx +34 -14
  138. package/src/plugins/builtin/econ/calendar-model.ts +48 -1
  139. package/src/plugins/builtin/econ/calendar-source.ts +4 -2
  140. package/src/plugins/builtin/econ/detail-view.tsx +18 -4
  141. package/src/plugins/builtin/econ/figures.ts +48 -0
  142. package/src/plugins/builtin/econ/index.tsx +14 -5
  143. package/src/plugins/builtin/econ/print-watch.ts +3 -1
  144. package/src/plugins/builtin/econ-statistics/client.ts +11 -0
  145. package/src/plugins/builtin/econ-statistics/headless.ts +10 -2
  146. package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
  147. package/src/plugins/builtin/econ-statistics/stats.ts +3 -0
  148. package/src/plugins/builtin/executives/data.ts +6 -1
  149. package/src/plugins/builtin/executives/pane.tsx +9 -21
  150. package/src/plugins/builtin/fear-greed/cache.ts +4 -0
  151. package/src/plugins/builtin/fear-greed/series.ts +6 -4
  152. package/src/plugins/builtin/gpu/client.ts +4 -1
  153. package/src/plugins/builtin/gpu/series.ts +2 -3
  154. package/src/plugins/builtin/help/index.tsx +61 -59
  155. package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
  156. package/src/plugins/builtin/help/table-cells.tsx +13 -2
  157. package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
  158. package/src/plugins/builtin/holders/beneficial-view.tsx +4 -2
  159. package/src/plugins/builtin/holders/headless.ts +4 -2
  160. package/src/plugins/builtin/holders/pane.tsx +3 -0
  161. package/src/plugins/builtin/holders/report-rows.ts +4 -1
  162. package/src/plugins/builtin/insider/headless.ts +2 -2
  163. package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
  164. package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
  165. package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
  166. package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
  167. package/src/plugins/builtin/jodi/client.ts +4 -1
  168. package/src/plugins/builtin/macro-day/headless.ts +12 -5
  169. package/src/plugins/builtin/macro-day/index.tsx +1 -1
  170. package/src/plugins/builtin/macro-day/model.ts +39 -5
  171. package/src/plugins/builtin/macro-day/pane.tsx +27 -19
  172. package/src/plugins/builtin/market-movers/client.ts +3 -0
  173. package/src/plugins/builtin/market-movers/headless.ts +3 -1
  174. package/src/plugins/builtin/market-movers/index.tsx +10 -5
  175. package/src/plugins/builtin/market-movers/screener.ts +27 -6
  176. package/src/plugins/builtin/market-movers/session-body.tsx +2 -1
  177. package/src/plugins/builtin/mna/headless.ts +2 -0
  178. package/src/plugins/builtin/mna/pane.tsx +3 -1
  179. package/src/plugins/builtin/money-markets/series.ts +3 -5
  180. package/src/plugins/builtin/news/headless.ts +3 -1
  181. package/src/plugins/builtin/news/index.tsx +3 -0
  182. package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -6
  183. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
  184. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
  185. package/src/plugins/builtin/portfolio-list/table.tsx +4 -6
  186. package/src/plugins/builtin/power/gas-client.ts +2 -1
  187. package/src/plugins/builtin/power/prices-client.ts +3 -2
  188. package/src/plugins/builtin/realized-vol/client.ts +13 -5
  189. package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
  190. package/src/plugins/builtin/realized-vol/headless.ts +8 -4
  191. package/src/plugins/builtin/realized-vol/model.ts +37 -6
  192. package/src/plugins/builtin/realized-vol/pane.tsx +9 -7
  193. package/src/plugins/builtin/research/event-model.ts +30 -4
  194. package/src/plugins/builtin/research/index.tsx +7 -3
  195. package/src/plugins/builtin/research/relative-valuation-headless.ts +8 -25
  196. package/src/plugins/builtin/research/relative-valuation-pane.tsx +52 -15
  197. package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
  198. package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
  199. package/src/plugins/builtin/risk-factors/data.ts +12 -6
  200. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  201. package/src/plugins/builtin/risk-factors/index.tsx +3 -1
  202. package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
  203. package/src/plugins/builtin/roadmap/index.tsx +28 -0
  204. package/src/plugins/builtin/roadmap/model.ts +67 -0
  205. package/src/plugins/builtin/roadmap/pane.tsx +244 -0
  206. package/src/plugins/builtin/scanner/live-headless.ts +21 -2
  207. package/src/plugins/builtin/scanner/lock-note.ts +26 -3
  208. package/src/plugins/builtin/sec/headless.ts +2 -1
  209. package/src/plugins/builtin/sec/index.tsx +3 -0
  210. package/src/plugins/builtin/shared/cloud-resource.ts +20 -7
  211. package/src/plugins/builtin/shared/fred-points.ts +6 -1
  212. package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
  213. package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
  214. package/src/plugins/builtin/shared/report-freshness.ts +8 -0
  215. package/src/plugins/builtin/shared/research-cells.tsx +1 -1
  216. package/src/plugins/builtin/shared/series-cache.ts +4 -4
  217. package/src/plugins/builtin/shared/series-warning.ts +13 -4
  218. package/src/plugins/builtin/short-volume/series.ts +2 -3
  219. package/src/plugins/builtin/social-mentions/client.ts +15 -10
  220. package/src/plugins/builtin/social-mentions/series.ts +2 -3
  221. package/src/plugins/builtin/thirteenf/api.ts +15 -9
  222. package/src/plugins/builtin/thirteenf/data.ts +39 -11
  223. package/src/plugins/builtin/thirteenf/headless.ts +31 -4
  224. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
  225. package/src/plugins/builtin/thirteenf/pane.tsx +21 -5
  226. package/src/plugins/builtin/thirteenf/types.ts +2 -0
  227. package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -2
  228. package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
  229. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
  230. package/src/plugins/builtin/ticker-detail/headless.ts +59 -8
  231. package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
  232. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
  233. package/src/plugins/builtin/ticker-detail/pane.tsx +11 -6
  234. package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
  235. package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
  236. package/src/plugins/builtin/treasury-auctions/headless.ts +24 -13
  237. package/src/plugins/builtin/treasury-auctions/pane.tsx +18 -6
  238. package/src/plugins/builtin/volatility/client.ts +43 -16
  239. package/src/plugins/builtin/volatility/headless.ts +5 -2
  240. package/src/plugins/builtin/volatility/model.ts +17 -6
  241. package/src/plugins/builtin/volatility/pane.tsx +11 -4
  242. package/src/plugins/builtin/volatility/series.ts +2 -1
  243. package/src/plugins/builtin/yield-curve/client.ts +59 -1
  244. package/src/plugins/builtin/yield-curve/headless.ts +9 -6
  245. package/src/plugins/builtin/yield-curve/index.tsx +34 -10
  246. package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
  247. package/src/plugins/catalog-browser.ts +2 -1
  248. package/src/plugins/host-link.ts +1 -1
  249. package/src/plugins/installer.ts +50 -39
  250. package/src/portfolio/column-values.ts +11 -7
  251. package/src/renderers/dom/desktop/icons.tsx +10 -2
  252. package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
  253. package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
  254. package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
  255. package/src/renderers/electrobun/bun/desktop/initialization.ts +9 -1
  256. package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
  257. package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
  258. package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
  259. package/src/renderers/electrobun/bun/index.ts +26 -2
  260. package/src/renderers/electrobun/bun/window/attention.ts +54 -0
  261. package/src/renderers/electrobun/bun/window/create.ts +45 -4
  262. package/src/renderers/electrobun/bun/window/events.ts +20 -0
  263. package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
  264. package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
  265. package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
  266. package/src/renderers/electrobun/launcher/preload.ts +13 -0
  267. package/src/renderers/electrobun/launcher/windows-emulation.ts +50 -0
  268. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  269. package/src/renderers/electrobun/view/news-source.ts +2 -0
  270. package/src/renderers/electrobun/view/ui-host.tsx +16 -1
  271. package/src/series/resolve.ts +2 -1
  272. package/src/series/types.ts +5 -0
  273. package/src/sources/gloomberb-cloud/fred-series.ts +8 -4
  274. package/src/sources/gloomberb-cloud/index.ts +42 -8
  275. package/src/telemetry/stall-reports-node.ts +13 -2
  276. package/src/telemetry/stall-reports.ts +189 -13
  277. package/src/test-support/outage.ts +8 -0
  278. package/src/tickers/company-name-listing.ts +110 -40
  279. package/src/tickers/company-name.ts +71 -2
  280. package/src/tickers/index-roots.ts +7 -0
  281. package/src/tickers/listing-fate.ts +2 -2
  282. package/src/tickers/search/home-listing.ts +93 -8
  283. package/src/tickers/search/index.ts +7 -3
  284. package/src/tickers/search/ranking.ts +5 -1
  285. package/src/tickers/search/upsert.ts +13 -0
  286. package/src/tickers/ticker-changes.ts +8 -4
  287. package/src/time-series/market.ts +5 -3
  288. package/src/time-series/price-comparison.ts +20 -2
  289. package/src/time-series/resolve.ts +20 -1
  290. package/src/time-series/types.ts +7 -0
  291. package/src/types/company-from.ts +40 -0
  292. package/src/types/data-provider.ts +7 -0
  293. package/src/types/financials.ts +45 -3
  294. package/src/types/headless.ts +6 -0
  295. package/src/ui/host.tsx +6 -1
  296. package/src/utils/crypto-pair.ts +121 -7
  297. package/src/utils/declared-dividends.ts +1 -1
  298. package/src/utils/financial-statements.ts +73 -16
  299. package/src/utils/fundamentals.ts +53 -10
  300. package/src/utils/lease-liabilities.ts +54 -0
  301. package/src/utils/main-thread-stall.ts +47 -2
  302. package/src/utils/perf-marks.ts +23 -11
  303. package/src/utils/price-history-integrity.ts +47 -1
  304. package/src/version.ts +2 -2
@@ -79,7 +79,7 @@ import {
79
79
  removeTickerFromCollection,
80
80
  tickerRemovalSummary,
81
81
  } from "./remove-ticker";
82
- import { QuickAddTickerInput, type QuickAddCollectionKind } from "../quick-add";
82
+ import { QUICK_ADD_KEY, QuickAddTickerInput, type QuickAddCollectionKind } from "../quick-add";
83
83
  import { delistingLabel } from "../../../../market-data/quotes/delisting";
84
84
  import {
85
85
  buildTrackedCurrencies,
@@ -143,8 +143,6 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
143
143
 
144
144
  const [streamWindow, setStreamWindow] = useState({ start: 0, end: 24 });
145
145
  const [quickAddFocused, setQuickAddFocused] = useState(false);
146
- // Bumped by the empty table's "+ ticker" button to open the add row.
147
- const [quickAddRequest, setQuickAddRequest] = useState(0);
148
146
 
149
147
  const {
150
148
  cursorSymbol,
@@ -737,7 +735,6 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
737
735
  return null;
738
736
  }, [activeCollectionId, config, currentPortfolio]);
739
737
  const showQuickAdd = !!(activeCollectionId && activeCollectionEntry && quickAddCollectionKind);
740
- const requestQuickAdd = useCallback(() => setQuickAddRequest((count) => count + 1), []);
741
738
  const quickAddHeight = showQuickAdd ? 1 : 0;
742
739
  const selectedFinancials = cursorSymbol ? financialsMap.get(cursorSymbol) : undefined;
743
740
  const selectedCap = liveMarketCapitalization(selectedFinancials?.quote, selectedFinancials?.fundamentals);
@@ -765,7 +762,6 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
765
762
  width={width}
766
763
  onAdded={handleTickerAdded}
767
764
  onFocusChange={setQuickAddFocused}
768
- focusRequest={quickAddRequest}
769
765
  />
770
766
  ) : null;
771
767
 
@@ -807,7 +803,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
807
803
  visibleRangeBuffer={3}
808
804
  resetScrollKey={activeCollectionId}
809
805
  onRowActivate={handleRowActivate}
810
- onAddTicker={showQuickAdd ? requestQuickAdd : undefined}
806
+ addTickerShortcut={showQuickAdd ? QUICK_ADD_KEY : undefined}
811
807
  onOpenPerpetual={showsPerpetuals ? openPerpMarket : undefined}
812
808
  rootHeight={contentHeight}
813
809
  />
@@ -112,7 +112,6 @@ export function QuickAddTickerInput({
112
112
  width,
113
113
  onAdded,
114
114
  onFocusChange,
115
- focusRequest = 0,
116
115
  }: {
117
116
  collectionId: string;
118
117
  collectionKind: QuickAddCollectionKind;
@@ -121,12 +120,6 @@ export function QuickAddTickerInput({
121
120
  width: number;
122
121
  onAdded: (symbol: string) => void;
123
122
  onFocusChange?: (focused: boolean) => void;
124
- /**
125
- * Bumped by a control outside the row, such as the empty table's
126
- * "+ ticker" button, to open it. A click there focuses the pane in the same
127
- * press, so the row opens once the pane is focused.
128
- */
129
- focusRequest?: number;
130
123
  }) {
131
124
  const dispatch = useAppDispatch();
132
125
  const { notify } = usePluginAppActions();
@@ -150,13 +143,6 @@ export function QuickAddTickerInput({
150
143
  setInputFocused(false);
151
144
  }, []);
152
145
 
153
- const handledFocusRequestRef = useRef(focusRequest);
154
- useEffect(() => {
155
- if (focusRequest === handledFocusRequestRef.current || !focused) return;
156
- handledFocusRequestRef.current = focusRequest;
157
- focusInput();
158
- }, [focusInput, focusRequest, focused]);
159
-
160
146
  // A symbol with no listing leaves the field, so the next one typed replaces
161
147
  // it instead of running on (OMV.VIREP.MC); the line under it still says what
162
148
  // failed until something is typed.
@@ -22,7 +22,8 @@ import {
22
22
  } from "../../../../tickers/ticker-changes";
23
23
 
24
24
  const QUICK_ADD_MAX_QUERY_LENGTH = 32;
25
- const QUICK_ADD_SYMBOL_RE = /^[A-Z0-9][A-Z0-9.\-\s]*$/;
25
+ // A slash writes a pair as crypto exchanges and FX desks do: BTC/GBP, EUR/USD.
26
+ const QUICK_ADD_SYMBOL_RE = /^[A-Z0-9][A-Z0-9.\-/\s]*$/;
26
27
 
27
28
  export type QuickAddCollectionKind = "portfolio" | "watchlist";
28
29
 
@@ -1,5 +1,4 @@
1
1
  import { useCallback, useMemo, useRef } from "react";
2
- import { QUICK_ADD_KEY } from "./quick-add";
3
2
  import {
4
3
  TickerListTableView,
5
4
  type DataTableKeyEvent,
@@ -120,7 +119,7 @@ export function PortfolioTickerTable({
120
119
  visibleRangeBuffer,
121
120
  resetScrollKey,
122
121
  onRowActivate,
123
- onAddTicker,
122
+ addTickerShortcut,
124
123
  onOpenPerpetual,
125
124
  rootHeight,
126
125
  }: {
@@ -146,8 +145,8 @@ export function PortfolioTickerTable({
146
145
  visibleRangeBuffer?: number;
147
146
  resetScrollKey?: unknown;
148
147
  onRowActivate?: (ticker: TickerRecord) => void;
149
- /** Opens the add row; the empty table offers it as a button. */
150
- onAddTicker?: () => void;
148
+ /** The key of the add row, which the empty table names. The pane shows the row itself, once. */
149
+ addTickerShortcut?: string;
151
150
  /** Opens PERP on a row's stock perpetual: its PERP% and FUND 8H cells link to it. */
152
151
  onOpenPerpetual?: (marketId: string) => void;
153
152
  rootHeight?: number;
@@ -243,8 +242,7 @@ export function PortfolioTickerTable({
243
242
  resetScrollKey={resetScrollKey}
244
243
  onRowActivate={onRowActivate}
245
244
  rowMenuAccelerators={PORTFOLIO_ROW_MENU_KEYS}
246
- onAddTicker={onAddTicker}
247
- addTickerShortcut={onAddTicker ? QUICK_ADD_KEY : undefined}
245
+ addTickerShortcut={addTickerShortcut}
248
246
  rootHeight={rootHeight}
249
247
  />
250
248
  );
@@ -1,4 +1,5 @@
1
1
  import { apiClient } from "../../../api-client";
2
+ import { sourceDidNotAnswer } from "../../../api-client/errors";
2
3
  import type { GasStorageBoard, GasStorageHistory, GasStorageRow, GasStorageSeasonal } from "../../../api-client/europe-energy";
3
4
  import { createPluginCache } from "../../../data/plugin-cache";
4
5
  import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
@@ -77,7 +78,7 @@ export async function fetchGasStorage(client: Pick<GasApi, "getCloudEuropeGas">
77
78
  } catch (error) {
78
79
  throw unavailableOnServer(error, GAS_STORAGE_NOT_AVAILABLE, [404, 503]);
79
80
  }
80
- if (board.status === "unavailable") throw new Error(GAS_STORAGE_NOT_AVAILABLE);
81
+ if (board.status === "unavailable") throw sourceDidNotAnswer(GAS_STORAGE_NOT_AVAILABLE);
81
82
  return board;
82
83
  }
83
84
 
@@ -1,4 +1,5 @@
1
1
  import { apiClient } from "../../../api-client";
2
+ import { sourceDidNotAnswer } from "../../../api-client/errors";
2
3
  import type { CarbonBoard, CarbonHistory, EuropePowerBoard, EuropePowerHistory, PowerPriceRow } from "../../../api-client/europe-energy";
3
4
  import { createPluginCache } from "../../../data/plugin-cache";
4
5
  import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
@@ -87,7 +88,7 @@ export async function fetchPowerPrices(client: Pick<PricesApi, "getCloudEuropePo
87
88
  } catch (error) {
88
89
  throw unavailableOnServer(error, POWER_PRICES_NOT_AVAILABLE, [404, 503]);
89
90
  }
90
- if (board.status === "unavailable") throw new Error(POWER_PRICES_NOT_AVAILABLE);
91
+ if (board.status === "unavailable") throw sourceDidNotAnswer(POWER_PRICES_NOT_AVAILABLE);
91
92
  return board;
92
93
  }
93
94
 
@@ -110,7 +111,7 @@ export async function fetchCarbonPrice(client: Pick<PricesApi, "getCloudEuropeCa
110
111
  } catch (error) {
111
112
  throw unavailableOnServer(error, CARBON_PRICE_NOT_AVAILABLE, [404, 503]);
112
113
  }
113
- if (board.status === "unavailable") throw new Error(CARBON_PRICE_NOT_AVAILABLE);
114
+ if (board.status === "unavailable") throw sourceDidNotAnswer(CARBON_PRICE_NOT_AVAILABLE);
114
115
  return board;
115
116
  }
116
117
 
@@ -4,6 +4,7 @@ import type { QueryEntry } from "../../../market-data/result-types";
4
4
  import type { DataProvider } from "../../../types/data-provider";
5
5
  import type { PricePoint } from "../../../types/financials";
6
6
  import { createSurfaceDependencies, loadVolatilitySurface, type SurfaceLoaderDependencies } from "../vol-surface/client";
7
+ import type { SurfaceSnapshot } from "../vol-surface/model";
7
8
  import { projectCurrentAtmIv, type CurrentAtmIvSnapshot } from "./model";
8
9
  import { abortable, abortError } from "../../../utils/async-deadline";
9
10
  import { errorMessage } from "../../../utils/errors";
@@ -70,7 +71,9 @@ export interface CurrentAtmIvRequest extends RealizedHistoryRequest {
70
71
  * Re-read the current reference's own expiry at the live spot: that one chain
71
72
  * is refetched and refitted, the catalogue comes from cache, and the other
72
73
  * slices of the full load are not touched. When the refreshed slice cannot
73
- * price an ATM level, the previous dated reference stands.
74
+ * price an ATM level, the refresh rejects with why ("ATM IV 31d not
75
+ * refreshed: ..."): the dated reference already shown stays, and the pane
76
+ * warns that it is not moving.
74
77
  */
75
78
  export async function refreshCurrentAtmIv(
76
79
  request: CurrentAtmIvRequest,
@@ -80,14 +83,19 @@ export async function refreshCurrentAtmIv(
80
83
  const reference = previous.reference;
81
84
  if (!reference || !(request.spot > 0) || !Number.isFinite(request.spot)) return previous;
82
85
  if (request.signal?.aborted) throw abortError(CANCELLED);
86
+ const notRefreshed = (reason: string) => new Error(`${reference.label} not refreshed: ${reason}`);
87
+ let surface: SurfaceSnapshot;
83
88
  try {
84
- const next = projectCurrentAtmIv(await loadVolatilitySurface({ ...request, forceRefresh: true, expiries: [reference.expiration],
85
- settings: { ivSource: reference.ivSource } }, dependencies));
86
- return next.reference?.expiration === reference.expiration ? { ...previous, reference: next.reference } : previous;
89
+ surface = await loadVolatilitySurface({ ...request, forceRefresh: true, expiries: [reference.expiration],
90
+ settings: { ivSource: reference.ivSource } }, dependencies);
87
91
  } catch (error) {
88
92
  if (request.signal?.aborted || (error instanceof Error && error.name === "AbortError")) throw abortError(CANCELLED);
89
- return previous;
93
+ throw notRefreshed(errorMessage(error));
90
94
  }
95
+ const next = projectCurrentAtmIv(surface);
96
+ if (next.reference?.expiration === reference.expiration) return { ...previous, reference: next.reference };
97
+ const reasons = [...new Set(surface.failures.map((failure) => failure.message))];
98
+ throw notRefreshed(reasons.join("; ") || "no clean quote at the money");
91
99
  }
92
100
 
93
101
  /** Options are independent of daily history and use the existing surface chain cache. */
@@ -19,9 +19,18 @@ export interface RealizedVolEvidenceStatus {
19
19
  source: string | null;
20
20
  asOf: string | null;
21
21
  errors: string[];
22
+ /** The errors that are the current IV's alone: a chart missing only its IV point names that, not the symbol. */
23
+ ivErrors?: string[];
24
+ /** The windows the history is too short for: their series have no values, and the capture is whole without them. */
25
+ shortWindows?: number[];
22
26
  currentIv: { value: number; date: string; label: string; source: string | null; expiration: number } | null;
23
27
  }
24
28
 
29
+ /** A series a short history leaves without values: a selected window it is too short for. */
30
+ function shortSeries(id: string, shortWindows: readonly number[] | undefined): boolean {
31
+ return /^hv-\d+$/.test(id) && (shortWindows ?? []).includes(Number(id.slice(3)));
32
+ }
33
+
25
34
  export interface RealizedVolEvidence extends RealizedVolEvidenceStatus {
26
35
  kind: "realized-volatility";
27
36
  version: 1;
@@ -38,7 +47,8 @@ export function realizedVolSemanticEvidence(series: readonly ResolvedSeries[], s
38
47
  const plottedValueCount = plotted.reduce((sum, entry) => sum + entry.points.filter((point) => isFiniteNumber(point.value)).length, 0);
39
48
  return { ...status, kind: "realized-volatility", version: 1, series: plotted, plottedValueCount,
40
49
  complete: !status.loading && !status.stale && status.errors.length === 0 && !!status.asOf
41
- && plotted.length > 0 && plotted.every((entry) => entry.points.some((point) => isFiniteNumber(point.value)))
50
+ && plotted.length > 0 && plotted.some((entry) => entry.points.some((point) => isFiniteNumber(point.value)))
51
+ && plotted.every((entry) => shortSeries(entry.id, status.shortWindows) || entry.points.some((point) => isFiniteNumber(point.value)))
42
52
  && (!status.showIv || status.currentIv != null),
43
53
  };
44
54
  }
@@ -56,12 +66,15 @@ function readRealizedVolEvidence(value: unknown): RealizedVolEvidence | null {
56
66
  || !isFiniteNumber(value.lookbackYears) || typeof value.showIv !== "boolean" || typeof value.complete !== "boolean"
57
67
  || typeof value.loading !== "boolean" || typeof value.stale !== "boolean" || !isDateString(value.asOf)
58
68
  || !isStringArray(value.errors)
69
+ || (value.shortWindows !== undefined && (!Array.isArray(value.shortWindows) || !value.shortWindows.every(isFiniteNumber)))
70
+ || (value.ivErrors !== undefined && !isStringArray(value.ivErrors))
59
71
  || !Array.isArray(value.series) || !value.series.every((entry) => isRecord(entry) && typeof entry.id === "string"
60
72
  && typeof entry.unit === "string" && Array.isArray(entry.points) && entry.points.every((point) => isRecord(point)
61
73
  && isDateString(point.date) && (point.value === null || isFiniteNumber(point.value))))) return null;
62
74
  const evidence = value as unknown as RealizedVolEvidence;
75
+ // The IV point is drawn only when there is one: a chart without it matches its data, and is incomplete for it.
63
76
  const required = evidence.view === "graph" ? [...evidence.windows.map((window) => `hv-${window}`), "price",
64
- ...(evidence.showIv ? ["current-iv"] : [])] : ["min", "max", "mean", "current"];
77
+ ...(evidence.showIv && evidence.currentIv != null ? ["current-iv"] : [])] : ["min", "max", "mean", "current"];
65
78
  if (!required.length || (evidence.view === "graph" && !evidence.windows.length)
66
79
  || evidence.series.length !== required.length || new Set(evidence.series.map((entry) => entry.id)).size !== required.length
67
80
  || required.some((id) => !evidence.series.some((entry) => entry.id === id))) return null;
@@ -75,10 +88,19 @@ function readRealizedVolEvidence(value: unknown): RealizedVolEvidence | null {
75
88
  }
76
89
  if (evidence.complete && (evidence.loading || evidence.stale || evidence.errors.length > 0
77
90
  || (evidence.showIv && !evidence.currentIv)
78
- || evidence.series.some((entry) => !entry.points.some((point) => isFiniteNumber(point.value))))) return null;
91
+ || evidence.series.some((entry) => !shortSeries(entry.id, evidence.shortWindows) && !entry.points.some((point) => isFiniteNumber(point.value))))) return null;
79
92
  return evidence;
80
93
  }
81
94
 
95
+ /** Drawn, current and whole but for the current IV, whose errors are the only ones. */
96
+ function missingOnlyIv(evidence: RealizedVolEvidence): boolean {
97
+ const ivErrors = new Set(evidence.ivErrors ?? []);
98
+ return evidence.view === "graph" && evidence.showIv && evidence.currentIv == null
99
+ && !evidence.loading && !evidence.stale && evidence.plottedValueCount > 0
100
+ && evidence.errors.every((error) => ivErrors.has(error))
101
+ && evidence.series.every((entry) => shortSeries(entry.id, evidence.shortWindows) || entry.points.some((point) => isFiniteNumber(point.value)));
102
+ }
103
+
82
104
  export const realizedVolScreenshotEvidence: PaneScreenshotEvidenceHook<RealizedVolEvidence> = {
83
105
  paneId: "realized-vol",
84
106
  kind: "realized-volatility",
@@ -98,6 +120,9 @@ export const realizedVolScreenshotEvidence: PaneScreenshotEvidenceHook<RealizedV
98
120
  },
99
121
  unavailable(evidence, { resolved, payload }) {
100
122
  const symbol = payload.financials[0]?.[0] ?? resolved.createOptions?.symbol;
101
- return evidence?.complete && !evidence.loading ? [] : [symbol ?? "realized volatility"];
123
+ if (evidence?.complete && !evidence.loading) return [];
124
+ // The volatility drawn whole and only the IV point missing: the capture names the IV, not the symbol.
125
+ if (evidence && missingOnlyIv(evidence)) return [`${symbol ?? evidence.symbol} current ATM IV`];
126
+ return [symbol ?? "realized volatility"];
102
127
  },
103
128
  };
@@ -3,7 +3,7 @@ import type { HeadlessPaneDefinition } from "../../../types/plugin";
3
3
  import { createSurfaceDependencies } from "../vol-surface/client";
4
4
  import { resolveHeadlessInstrument } from "../shared/headless-market-data";
5
5
  import { createRealizedVolatilityDependencies, loadCurrentAtmIv, loadRealizedVolatilityHistory } from "./client";
6
- import { projectRealizedVolatility, type CurrentAtmIvSnapshot } from "./model";
6
+ import { atmIvObservedLabel, projectRealizedVolatility, type CurrentAtmIvSnapshot } from "./model";
7
7
  import { ESTIMATOR_OPTIONS, selectedWindows } from "./settings";
8
8
  import { DAILY_CLOSES } from "../shared/report-freshness";
9
9
  import { listingAbroadVenue } from "../../../utils/sec";
@@ -69,13 +69,17 @@ export function realizedVolHeadless(initialView: "graph" | "cone"): HeadlessPane
69
69
  formatted: iv?.reference ? new Date(iv.reference.expiration * 1000).toISOString().slice(0, 10) : "--" },
70
70
  { label: "Days", value: iv?.reference?.daysToExpiry ?? null,
71
71
  formatted: iv?.reference ? String(Math.round(iv.reference.daysToExpiry)) : "--" },
72
- { label: "Observed", value: iv?.reference?.date.toISOString() ?? null },
72
+ // To the minute, as the pane reads it; the JSON keeps the chain's read time whole.
73
+ { label: "Observed", value: iv?.reference?.date.toISOString() ?? null,
74
+ formatted: iv?.reference ? atmIvObservedLabel(iv.reference.date) : "--" },
73
75
  ] },
74
76
  ],
75
- complete: model.cone.every((row) => row.current != null) && model.warnings.length === 0 && !history.stale && errors.length === 0
77
+ // A short history is whole: the windows it is too short for have no estimate yet, and the note says when it starts.
78
+ complete: model.cone.every((row) => row.current != null || model.shortWindows.includes(row.window))
79
+ && model.warnings.length === 0 && !history.stale && errors.length === 0
76
80
  && (!args.options.showIv || iv?.reference != null || iv?.noOptionChain === true),
77
81
  unavailableSymbols: model.history.length ? [] : [instrument.symbol], errors,
78
- notes: [barsPerYearLabel(model.barsPerYear)],
82
+ notes: [barsPerYearLabel(model.barsPerYear), ...model.historyNote ? [model.historyNote] : []],
79
83
  metadata: { unit: "decimal annualized volatility", annualization: model.barsPerYear, model, currentIv: iv,
80
84
  ...(abroad && args.options.showIv ? { notices: [`Options data covers US listings only, so ${args.symbols[0]} has no current IV.`] } : {}),
81
85
  source: history.source, stale: history.stale, fetchedAt: history.fetchedAt,
@@ -19,6 +19,24 @@ export interface RealizedVolatilityModel {
19
19
  /** What the volatility is annualised at, read from the series' own dates. */
20
20
  barsPerYear: BarsPerYear;
21
21
  warnings: string[];
22
+ /**
23
+ * The windows the history is too short for, longest last: a stock that
24
+ * listed in May has no 180- or 260-bar estimate yet, and nothing failed.
25
+ */
26
+ shortWindows: number[];
27
+ /**
28
+ * What a short history leaves out, said once: "History starts 2026-05-07: 121
29
+ * daily bars so far; the 180- and 260-bar windows need more". Null when the
30
+ * history covers the lookback and every window.
31
+ */
32
+ historyNote: string | null;
33
+ }
34
+
35
+ /** "the 180-bar window", "the 180- and 260-bar windows", "the 90-, 180- and 260-bar windows". */
36
+ function windowsText(windows: readonly number[]): string {
37
+ if (windows.length === 1) return `the ${windows[0]}-bar window`;
38
+ const head = windows.slice(0, -1).map((window) => `${window}-`);
39
+ return `the ${head.join(", ")} and ${windows.at(-1)}-bar windows`;
22
40
  }
23
41
 
24
42
  export interface RealizedVolatilityProjectionOptions {
@@ -45,6 +63,10 @@ export function projectRealizedVolatility(
45
63
  const windows = [...new Set(options.windows ?? REALIZED_VOLATILITY_WINDOWS)]
46
64
  .filter((window) => REALIZED_VOLATILITY_WINDOWS.some((standard) => standard === window));
47
65
  const warnings: string[] = [];
66
+ // What follows from a history shorter than the windows and lookback asked for: notes, not failures.
67
+ let shortLookback = false;
68
+ let shortWarmup = false;
69
+ const shortWindows: number[] = [];
48
70
  const dates = new Map<number, PricePoint>();
49
71
  for (const point of points) {
50
72
  const date = new Date(point.date);
@@ -66,24 +88,28 @@ export function projectRealizedVolatility(
66
88
  const cone = volatilityCone(cadenceIssue ? [] : history, { estimator, lookbackYears });
67
89
  const visible = history.filter((point) => point.date.getTime() > cutoff);
68
90
  const visibleSeries = series.filter((point) => point.date.getTime() > cutoff);
69
- if (history.length > 0 && history[0]!.date.getTime() > cutoff) {
70
- warnings.push(`History covers less than the requested ${lookbackYears}Y lookback`);
71
- }
91
+ if (history.length > 0 && history[0]!.date.getTime() > cutoff) shortLookback = true;
72
92
  for (const window of history.length > 0 && !cadenceIssue ? REALIZED_VOLATILITY_WINDOWS : []) {
73
93
  const result = realizedVolatilityResult(history, window, estimator);
74
- if (result.reason === "insufficient-history") warnings.push(`HV${window} needs more daily history`);
94
+ if (result.reason === "insufficient-history") shortWindows.push(window);
75
95
  else if (result.reason) warnings.push(`HV${window} unavailable: ${result.reason.replaceAll("-", " ")}`);
76
96
  }
77
97
  const priorWarmup = history.filter((point) => point.date.getTime() <= cutoff).length;
78
98
  const requiredWarmup = estimator === "close-to-close" || estimator === "yang-zhang" ? 260 : 259;
79
99
  if (history.length > 0 && !cadenceIssue && priorWarmup < requiredWarmup) {
80
- warnings.push("Long-window cone estimates do not cover the complete lookback");
100
+ shortWarmup = true;
81
101
  } else if (!cadenceIssue && cone.some((row) => row.sampleSize < visible.length)) {
82
102
  warnings.push("Unavailable historical estimates reduce volatility-cone sample coverage");
83
103
  }
104
+ const first = history[0]?.date.toISOString().slice(0, 10);
105
+ // Where the history starts, never read as the listing date: a source's first bar is not proof of when it listed.
106
+ const historyNote = !first || (!shortLookback && !shortWarmup && !shortWindows.length) ? null
107
+ : `History starts ${first}: ${history.length} daily bars so far; ${shortWindows.length
108
+ ? `${windowsText(shortWindows)} need${shortWindows.length === 1 ? "s" : ""} more`
109
+ : "the long windows' cone covers part of the lookback"}`;
84
110
  return { symbol: options.symbol.trim().toUpperCase(), estimator, lookbackYears, windows,
85
111
  history: visible, series: visibleSeries, cone, asOf, barsPerYear: barsPerYear(history.map((point) => point.date)),
86
- warnings: [...new Set(warnings)] };
112
+ warnings: [...new Set(warnings)], shortWindows, historyNote };
87
113
  }
88
114
 
89
115
  interface CurrentAtmIvReference {
@@ -107,6 +133,11 @@ export interface CurrentAtmIvSnapshot {
107
133
  noOptionChain?: boolean;
108
134
  }
109
135
 
136
+ /** When an IV was observed, to the minute in UTC (`2026-10-27 20:14 UTC`), in the pane and the report alike. */
137
+ export function atmIvObservedLabel(date: Date): string {
138
+ return `${date.toISOString().slice(0, 16).replace("T", " ")} UTC`;
139
+ }
140
+
110
141
  /** A LEAPS ATM IV is not comparable with a 10 to 260 session realized cone. */
111
142
  const MAX_CURRENT_ATM_IV_DAYS = 90;
112
143
 
@@ -18,7 +18,7 @@ import { useLiveSessionRefresh } from "../shared/volatility/live-session";
18
18
  import type { RealizedVolatilityEstimator, VolatilityConeStatistics } from "../shared/volatility";
19
19
  import { loadCurrentAtmIv, loadRealizedVolatilityHistory, refreshCurrentAtmIv } from "./client";
20
20
  import type { CurrentAtmIvSnapshot } from "./model";
21
- import { projectRealizedVolatility } from "./model";
21
+ import { atmIvObservedLabel, projectRealizedVolatility } from "./model";
22
22
  import { RealizedVolGraph, VolatilityConeChart } from "./charts";
23
23
  import { coneWindowDate } from "./chart-model";
24
24
  import type { RealizedVolEvidenceStatus } from "./evidence";
@@ -118,7 +118,7 @@ export function RealizedVolPane({ width, height, focused }: PaneProps) {
118
118
  priceUnit: priceSeriesUnit(quote?.currency ?? "", quote?.instrumentType), iv: showIv ? iv.data?.reference ?? null : null } : null,
119
119
  [model, quote?.currency, quote?.instrumentType, showIv, iv.data]);
120
120
  const notices = [identityError, history.error, history.data?.error, iv.error, ivRefresh.error, iv.data?.error,
121
- ...(model?.warnings ?? []), ...(showIv ? iv.data?.warnings ?? [] : []),
121
+ ...(model?.warnings ?? []), model?.historyNote, ...(showIv ? iv.data?.warnings ?? [] : []),
122
122
  ...(showIv && symbol && !spotAvailable ? ["Current ATM IV unavailable: underlying quote is missing or stale"] : []),
123
123
  ].filter((value): value is string => !!value);
124
124
  usePaneNoticeFooter({ registrationId: "realized-vol-notices", notices: [...new Set(notices)], focused });
@@ -154,15 +154,17 @@ export function RealizedVolPane({ width, height, focused }: PaneProps) {
154
154
  role: "report-notice", label: "Chart window",
155
155
  getMetadata: () => ({ text: `Chart legend: ${legendWindow}-bar window`, key: "legendWindow", value: legendWindow }),
156
156
  } : null);
157
+ const ivErrors = showIv ? [...new Set([iv.error, ivRefresh.error, iv.data?.error, !iv.data?.reference ? "Current ATM IV unavailable" : null]
158
+ .filter((value): value is string => !!value))] : [];
157
159
  const evidence: RealizedVolEvidenceStatus = {
158
160
  symbol: model?.symbol ?? symbol ?? "", view: view === "cone" ? "cone" : "graph", estimator, legendWindow,
159
- windows, lookbackYears: Number(lookback) === 2 ? 2 : 1, showIv,
161
+ windows, lookbackYears: Number(lookback) === 2 ? 2 : 1, showIv, shortWindows: model?.shortWindows ?? [],
160
162
  loading: history.loading || (showIv && iv.loading),
161
163
  stale: history.data?.stale ?? false, source: history.data?.source ?? null,
162
164
  asOf: model?.asOf?.toISOString() ?? null,
163
- errors: [...new Set([history.error, history.data?.error, ...(model?.warnings ?? []),
164
- ...(showIv ? [iv.error, ivRefresh.error, iv.data?.error, !iv.data?.reference ? "Current ATM IV unavailable" : null] : []),
165
- ].filter((value): value is string => !!value))],
165
+ errors: [...new Set([history.error, history.data?.error, ...(model?.warnings ?? []), ...ivErrors]
166
+ .filter((value): value is string => !!value))],
167
+ ivErrors,
166
168
  currentIv: showIv && iv.data?.reference ? { value: iv.data.reference.value * 100,
167
169
  date: iv.data.reference.date.toISOString(), label: iv.data.reference.label,
168
170
  source: iv.data.reference.source, expiration: iv.data.reference.expiration } : null,
@@ -183,7 +185,7 @@ export function RealizedVolPane({ width, height, focused }: PaneProps) {
183
185
  <QueryBar width={width} filters={[
184
186
  { id: "estimator", label: "Estimator", value: estimator, options: ESTIMATOR_OPTIONS, onChange: (value: string) => setEstimator(value as RealizedVolatilityEstimator) },
185
187
  { id: "lookback", label: "Lookback", value: String(lookback), options: [{ value: "1", label: "1Y" }, { value: "2", label: "2Y" }], onChange: setLookback },
186
- ]} meta={reference ? `ATM IV observed ${reference.date.toISOString().slice(0, 16).replace("T", " ")} UTC` : "Annualized %"} />
188
+ ]} meta={reference ? `ATM IV observed ${atmIvObservedLabel(reference.date)}` : "Annualized %"} />
187
189
  {!symbol ? <EmptyState title="Select a ticker." /> : <PaneStatusBody subject="realized volatility" loading={history.loading && !model}
188
190
  error={!model ? history.error ?? identityError ?? null : null} empty={!!model && !model.history.length}>
189
191
  {model && view === "cone" ? <>
@@ -422,7 +422,8 @@ export function buildEventRows(
422
422
  date: dividend.exDate,
423
423
  status: "Dividend",
424
424
  period: "-",
425
- detail: dividend.payDate ? `Declared, payable ${dividend.payDate}` : "Declared",
425
+ // A one-off beside the regular payments says so: "Declared special, payable 2026-11-25".
426
+ detail: `Declared${dividend.special ? " special" : ""}${dividend.payDate ? `, payable ${dividend.payDate}` : ""}`,
426
427
  value: formatDistributionAmount(dividend.amount, data?.currency ?? currency),
427
428
  tone: "positive",
428
429
  });
@@ -480,9 +481,14 @@ export function eventSourceNotice(state: EventSourceState): EventSourceNotice |
480
481
  const unavailableSections = Object.entries(state.actions?.coverage ?? {})
481
482
  .filter(([section, status]) => status === "unavailable" && (state.variant !== "earnings-estimates" || section === "earnings"))
482
483
  .map(([section]) => section);
484
+ // Sections and estimates that a source failed to give share one line: "Unavailable: earnings, analyst estimates (the source did not answer)".
485
+ const estimatesCode = !!state.estimatesError && REASON_CODE.test(state.estimatesError.trim());
486
+ const sharedLine = !state.actionsError && unavailableSections.length > 0 && estimatesCode;
483
487
 
484
488
  if (state.actionsError) {
485
- notices.push(`${actionsLabel} unavailable: ${state.actionsError}`);
489
+ notices.push(`${actionsLabel} unavailable: ${sourceErrorText(state.actionsError)}`);
490
+ } else if (sharedLine) {
491
+ notices.push(`Unavailable: ${unavailableSections.join(", ")}, analyst estimates (${sourceErrorText(state.estimatesError!)})`);
486
492
  } else if (unavailableSections.length > 0) {
487
493
  notices.push(`Unavailable: ${unavailableSections.join(", ")}`);
488
494
  } else if (state.actions && !(state.variant === "earnings-estimates"
@@ -497,7 +503,7 @@ export function eventSourceNotice(state: EventSourceState): EventSourceNotice |
497
503
  if (state.estimates?.stale) notices.push(`Analyst estimates stale${state.estimates.fetchedAt ? ` (fetched ${state.estimates.fetchedAt})` : ""}`);
498
504
 
499
505
  if (state.estimatesError) {
500
- notices.push(`Analyst estimates unavailable: ${state.estimatesError}`);
506
+ if (!sharedLine) notices.push(`Analyst estimates unavailable: ${sourceErrorText(state.estimatesError)}`);
501
507
  } else if (state.estimates && !hasAnalystEstimates(state.estimates)) {
502
508
  notices.push(`No analyst estimates for ${state.symbol}`);
503
509
  }
@@ -509,8 +515,28 @@ export function eventSourceNotice(state: EventSourceState): EventSourceNotice |
509
515
  };
510
516
  }
511
517
 
518
+ /** A data service's reason code (UPSTREAM_ERROR) rather than words. */
519
+ const REASON_CODE = /^[A-Z][A-Z0-9_]+$/;
520
+
521
+ /**
522
+ * A source's failure in words: a reason code reads as what happened
523
+ * ("UPSTREAM_ERROR" is "the source did not answer"); a sentence stands.
524
+ */
525
+ export function sourceErrorText(error: string): string {
526
+ const text = error.trim();
527
+ if (!REASON_CODE.test(text)) return text;
528
+ switch (text) {
529
+ case "TIMEOUT": return "the source timed out";
530
+ case "NOT_FOUND":
531
+ case "NO_DATA": return "the source has none for this symbol";
532
+ case "RATE_LIMITED": return "the source is busy, try again shortly";
533
+ default: return "the source did not answer";
534
+ }
535
+ }
536
+
512
537
  function hasCorporateActionRows(data: CorporateActionsData): boolean {
513
- return data.dividends.length > 0 || data.splits.length > 0 || data.earnings.length > 0;
538
+ // A declared dividend is a row too: AIOK's $3.50 special with nothing paid yet.
539
+ return data.dividends.length > 0 || data.splits.length > 0 || data.earnings.length > 0 || declaredDividends(data).length > 0;
514
540
  }
515
541
 
516
542
  function hasAnalystEstimates(data: AnalystResearchData): boolean {
@@ -6,6 +6,8 @@ import { CorporateActionsView } from "./corporate-actions-pane";
6
6
  import { EquityDiagnosticView } from "./equity-diagnostic-pane";
7
7
  import { RelativeValuationPane } from "./relative-valuation-pane";
8
8
  import { relativeValuationHeadless } from "./relative-valuation-headless";
9
+ import { AUTO_PEERS_SETTING, relativeValuationTitle } from "./relative-valuation-peers";
10
+ import { buildRelativeValuationSettingsDef, MAX_RELATIVE_VALUATION_TICKERS } from "./relative-valuation-settings";
9
11
  import { analystResearchHeadless } from "./analyst-headless";
10
12
  import { eventsHeadless } from "./events-headless";
11
13
  import { earningsEstimatesHeadless } from "./headless";
@@ -95,6 +97,7 @@ export const researchModule: PluginModule = {
95
97
  defaultMode: "floating",
96
98
  defaultFloatingSize: { width: 104, height: 24 },
97
99
  tableExport: true,
100
+ settings: buildRelativeValuationSettingsDef,
98
101
  },
99
102
  {
100
103
  id: "earnings-estimates",
@@ -168,14 +171,15 @@ export const researchModule: PluginModule = {
168
171
  try {
169
172
  symbols = options?.symbols?.length
170
173
  ? options.symbols
171
- : parseTickerListInput(options?.arg ?? context.activeTicker ?? "", 12);
174
+ : parseTickerListInput(options?.arg ?? context.activeTicker ?? "", MAX_RELATIVE_VALUATION_TICKERS);
172
175
  } catch {
173
176
  return null;
174
177
  }
175
178
  return {
176
- title: `RV ${formatTickerListInput(symbols)}`,
179
+ title: relativeValuationTitle(symbols, null),
177
180
  placement: "floating",
178
- settings: { symbols, symbolsText: formatTickerListInput(symbols) },
181
+ // One ticker is compared with the peers `--peers` picks, which the pane finds once it opens.
182
+ settings: { symbols, symbolsText: formatTickerListInput(symbols), ...(symbols.length === 1 ? { [AUTO_PEERS_SETTING]: true } : {}) },
179
183
  };
180
184
  },
181
185
  },
@@ -8,11 +8,10 @@ import {
8
8
  RELATIVE_VALUATION_STALE_FUNDAMENTALS_NOTICE, RELATIVE_VALUATION_STALE_QUOTE_NOTICE, relativeValuationValues, type RelativeValuationGapKey,
9
9
  } from "./relative-valuation-model";
10
10
  import { paneSchemas } from "./headless-schema";
11
- import { isRecord } from "../../../utils/guards";
12
- import { errorMessage } from "../../../utils/errors";
13
- import { notATickerSymbol } from "../../../sources/provider-errors";
14
11
  import { resolveCurrencyUnit } from "../../../utils/currency-units";
15
- import { pickPeers, peerMethodTitle, type PeerSelection } from "./relative-valuation-peers";
12
+ import {
13
+ AUTO_PEERS_SETTING, findPeers, peerMethodTitle, peerSelectionOf, PEER_SELECTION_SETTING, relativeValuationTitle, type PeerSelection,
14
+ } from "./relative-valuation-peers";
16
15
  import { extraColumns, extraColumnsBasis, loadExtraColumns, parseExtraColumns } from "./relative-valuation-columns";
17
16
  import { fundamentalsBasisParts } from "./relative-valuation-basis";
18
17
  import { conversionNote, convertMarketCaps, loadFxLegs } from "./relative-valuation-currency";
@@ -103,33 +102,14 @@ const VALUATION_COLUMNS: HeadlessPaneColumn[] = [
103
102
  })),
104
103
  ];
105
104
 
106
- /** Peers `--peers` chose, as the expansion left them in the settings or the load picked them. */
107
- function peerSelectionOf(value: unknown, symbols: readonly string[]): PeerSelection | null {
108
- if (!isRecord(value) || typeof value.subject !== "string" || !Array.isArray(value.peers)) return null;
109
- const selection = value as unknown as PeerSelection;
110
- const expected = [selection.subject, ...selection.peers];
111
- return expected.length === symbols.length && expected.every((symbol, index) => symbol.toUpperCase() === symbols[index]?.toUpperCase())
112
- ? selection : null;
113
- }
114
-
115
105
  /** The peers of the one symbol `--peers` was given; several symbols are already a peer list. */
116
106
  async function choosePeers(symbols: readonly string[], ctx: HeadlessPaneContext): Promise<PeerSelection> {
117
107
  if (symbols.length !== 1) {
118
108
  throw new Error(`--peers picks the peers of one symbol, such as RV ${symbols[0] ?? "DUOL"} --peers; with several symbols, leave it out.`);
119
109
  }
120
- const subject = symbols[0]!;
121
- const financials = await loadHeadlessFinancials(ctx, subject).catch((error: unknown) => {
122
- ctx.signal.throwIfAborted();
123
- // A word that is no ticker is said as `fn RV duolingo lrn` says it, which the CLI follows with the search to run.
124
- if (notATickerSymbol(errorMessage(error))) throw error;
125
- // Any other failure leaves the peers to what the symbol alone places (a curated core's member).
126
- return null;
127
- });
128
- return pickPeers(subject, financials, ctx);
110
+ return findPeers(symbols[0]!, ctx);
129
111
  }
130
112
 
131
- const PEER_SELECTION_SETTING = "peerSelection";
132
-
133
113
  export const relativeValuationHeadless: HeadlessPaneDefinition<"rows"> = {
134
114
  ...paneSchemas["relative-valuation-pane"],
135
115
  // What Ask Gloom reads. The template's "across peers" never said this is where a list of holdings' P/E is.
@@ -149,9 +129,12 @@ export const relativeValuationHeadless: HeadlessPaneDefinition<"rows"> = {
149
129
  return {
150
130
  argument: [selection.subject, ...selection.peers].join(", "),
151
131
  settings: { [PEER_SELECTION_SETTING]: selection },
152
- title: `RV ${selection.subject} + ${peerMethodTitle(selection, { automatic: true })}`,
132
+ title: relativeValuationTitle([], selection),
153
133
  };
154
134
  },
135
+ // The app adds the peers of a pane opened on one ticker; a capture draws the tickers given, as the report does,
136
+ // unless --peers widened them.
137
+ shotState: (args) => args.options.peers === true ? {} : { settings: { [AUTO_PEERS_SETTING]: false } },
155
138
  async load(args, ctx) {
156
139
  // --peers picked in advance (fn and shot) reads the same peers; any other caller picks them here.
157
140
  const preset = args.options.peers === true ? peerSelectionOf(ctx.settings?.[PEER_SELECTION_SETTING], args.symbols) : null;