gloomberb 0.18.4 → 0.18.5

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 (282) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +1 -1
  4. package/patches/electrobun@1.18.1.patch +32 -1
  5. package/src/account/auth-model.ts +7 -3
  6. package/src/api-client/auth.ts +9 -4
  7. package/src/api-client/catalysts.ts +2 -1
  8. package/src/api-client/data.ts +14 -0
  9. package/src/api-client/index.ts +160 -11
  10. package/src/api-client/live-scan.ts +91 -0
  11. package/src/api-client/members.ts +12 -0
  12. package/src/api-client/request.ts +222 -41
  13. package/src/api-client/research-activity.ts +3 -10
  14. package/src/api-client/types.ts +6 -0
  15. package/src/app/keybindings/index.ts +3 -0
  16. package/src/app/keybindings/labels.ts +18 -1
  17. package/src/app/keybindings/resolve.ts +14 -0
  18. package/src/app/runtime/broker-import.ts +43 -26
  19. package/src/architecture/core-builtin-allowlist.json +1 -1
  20. package/src/cli/commands/fred-series.ts +5 -0
  21. package/src/cli/commands/keybindings.ts +23 -0
  22. package/src/cli/commands/market.ts +68 -22
  23. package/src/cli/commands/options-chain.ts +59 -3
  24. package/src/cli/commands/overview.ts +119 -31
  25. package/src/cli/commands/search.ts +18 -2
  26. package/src/cli/commands/ticker.ts +2 -1
  27. package/src/cli/errors.ts +2 -2
  28. package/src/cli/export-source.ts +29 -1
  29. package/src/cli/glossary.ts +49 -0
  30. package/src/cli/history-rows.ts +30 -7
  31. package/src/cli/known-gaps.ts +5 -2
  32. package/src/cli/listing-arg.ts +16 -5
  33. package/src/cli/pane-functions/freshness.ts +25 -13
  34. package/src/cli/pane-functions/function-help.ts +10 -3
  35. package/src/cli/pane-functions/headless.ts +39 -5
  36. package/src/cli/pane-functions/index.ts +10 -3
  37. package/src/cli/pane-functions/market-session.ts +15 -5
  38. package/src/cli/pane-functions/report.ts +5 -1
  39. package/src/cli/pane-functions/resolver.ts +18 -2
  40. package/src/cli/quote-answer.ts +10 -4
  41. package/src/cli/report-tables.ts +18 -7
  42. package/src/cli/restore-plugins.ts +13 -1
  43. package/src/cli/result.ts +57 -5
  44. package/src/components/chart/composite/composite-chart.tsx +22 -4
  45. package/src/components/chart/composite/format.ts +27 -0
  46. package/src/components/chart/composite/levels.ts +2 -0
  47. package/src/components/chart/composite/panel-series.ts +1 -0
  48. package/src/components/chart/composite/panel-surface.tsx +9 -2
  49. package/src/components/chart/composite/range-statistics.ts +62 -0
  50. package/src/components/chart/composite/scene.ts +7 -3
  51. package/src/components/chart/composite/series-axis-tags.tsx +16 -9
  52. package/src/components/chart/composite/types.ts +4 -0
  53. package/src/components/command-bar/panel/prompt-binding.ts +0 -8
  54. package/src/components/command-bar/workflow/alert.ts +1 -1
  55. package/src/components/layout/header.tsx +26 -40
  56. package/src/components/layout/pane/footer/index.tsx +1 -0
  57. package/src/components/layout/pane/footer/model.ts +27 -0
  58. package/src/components/onboarding/onboarding-frame.tsx +1 -1
  59. package/src/components/onboarding/portfolio-step/positions-panel.tsx +2 -2
  60. package/src/components/ui/display.tsx +5 -1
  61. package/src/core/state/app/layout.ts +2 -0
  62. package/src/core/state/app/state.ts +9 -0
  63. package/src/core/state/app/types.ts +3 -0
  64. package/src/data/saved-copies.ts +55 -16
  65. package/src/docs/snapshot.json +361 -84
  66. package/src/i18n/es.ts +1 -1
  67. package/src/i18n/ja.ts +1 -1
  68. package/src/i18n/ko.ts +1 -1
  69. package/src/i18n/zh-cn.ts +1 -1
  70. package/src/i18n/zh-tw.ts +1 -1
  71. package/src/market-data/coordinator/financials.ts +10 -2
  72. package/src/market-data/coordinator/index.ts +3 -1
  73. package/src/market-data/coordinator/quotes.ts +4 -2
  74. package/src/market-data/market/change-basis.ts +33 -5
  75. package/src/market-data/market/dated-previous-close.ts +203 -0
  76. package/src/market-data/market/futures-week.ts +68 -0
  77. package/src/market-data/market/status.ts +16 -4
  78. package/src/market-data/options-alternatives.ts +60 -0
  79. package/src/market-data/quotes/contributions.ts +20 -2
  80. package/src/market-data/quotes/resolution.ts +3 -0
  81. package/src/market-data/selectors.ts +3 -0
  82. package/src/notifications/notification-log.ts +6 -4
  83. package/src/plugins/builtin/alerts/alert-engine.ts +5 -1
  84. package/src/plugins/builtin/alerts/calendar-events.ts +57 -6
  85. package/src/plugins/builtin/alerts/cli.ts +59 -20
  86. package/src/plugins/builtin/alerts/command.ts +7 -0
  87. package/src/plugins/builtin/alerts/constants.ts +1 -1
  88. package/src/plugins/builtin/alerts/fired-log.ts +45 -4
  89. package/src/plugins/builtin/alerts/format.ts +8 -2
  90. package/src/plugins/builtin/alerts/headless.ts +10 -5
  91. package/src/plugins/builtin/alerts/history-rows.ts +5 -2
  92. package/src/plugins/builtin/alerts/history.ts +2 -0
  93. package/src/plugins/builtin/alerts/index.tsx +66 -16
  94. package/src/plugins/builtin/alerts/live.ts +3 -3
  95. package/src/plugins/builtin/alerts/metric-command.ts +61 -0
  96. package/src/plugins/builtin/alerts/metric-data.ts +70 -0
  97. package/src/plugins/builtin/alerts/metric-model.ts +175 -0
  98. package/src/plugins/builtin/alerts/metric-parity-fixture.json +725 -0
  99. package/src/plugins/builtin/alerts/pane.tsx +42 -29
  100. package/src/plugins/builtin/alerts/storage.ts +33 -10
  101. package/src/plugins/builtin/alerts/types.ts +3 -0
  102. package/src/plugins/builtin/backtest/headless.ts +10 -5
  103. package/src/plugins/builtin/backtest/pane.tsx +9 -4
  104. package/src/plugins/builtin/backtest/presets.ts +29 -3
  105. package/src/plugins/builtin/broker-manager/index.tsx +3 -2
  106. package/src/plugins/builtin/broker-manager/model.ts +6 -1
  107. package/src/plugins/builtin/broker-trading/controller.ts +22 -12
  108. package/src/plugins/builtin/broker-trading/index.tsx +13 -1
  109. package/src/plugins/builtin/broker-trading/order-feed.ts +9 -4
  110. package/src/plugins/builtin/broker-trading/pane.tsx +1 -1
  111. package/src/plugins/builtin/builtin-plugin-meta.ts +1 -1
  112. package/src/plugins/builtin/catalysts/model.ts +2 -1
  113. package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -1
  114. package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +1 -1
  115. package/src/plugins/builtin/chart-composer/cli-options.ts +5 -5
  116. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +5 -2
  117. package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -0
  118. package/src/plugins/builtin/chart-composer/editor/model.ts +2 -0
  119. package/src/plugins/builtin/chart-composer/editor.tsx +7 -5
  120. package/src/plugins/builtin/chart-composer/formula.ts +71 -18
  121. package/src/plugins/builtin/chart-composer/headless-schema.ts +14 -3
  122. package/src/plugins/builtin/chart-composer/headless.ts +13 -2
  123. package/src/plugins/builtin/chart-composer/pane.tsx +17 -2
  124. package/src/plugins/builtin/chart-composer/presets.ts +50 -15
  125. package/src/plugins/builtin/chart-composer/quick-add.tsx +40 -25
  126. package/src/plugins/builtin/chart-composer/session-history.ts +18 -9
  127. package/src/plugins/builtin/chart-composer/use-series-catalog.ts +40 -26
  128. package/src/plugins/builtin/chat/content/command-suggestions.ts +92 -0
  129. package/src/plugins/builtin/chat/content/composer.tsx +13 -6
  130. package/src/plugins/builtin/chat/content/footer.tsx +6 -3
  131. package/src/plugins/builtin/chat/content/index.tsx +50 -21
  132. package/src/plugins/builtin/chat/content/open-targets.ts +14 -3
  133. package/src/plugins/builtin/chat/controller/session-runtime.ts +4 -2
  134. package/src/plugins/builtin/chat/index.tsx +23 -0
  135. package/src/plugins/builtin/chat/message/desktop.tsx +1 -0
  136. package/src/plugins/builtin/chat/message/inline-tokens.tsx +23 -0
  137. package/src/plugins/builtin/chat/message/pane-chip.tsx +50 -20
  138. package/src/plugins/builtin/chat/pane-links/commands.ts +265 -0
  139. package/src/plugins/builtin/chat/pane-links/model.ts +12 -3
  140. package/src/plugins/builtin/chat/pane-links/open.ts +8 -1
  141. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +10 -1
  142. package/src/plugins/builtin/cloud/plugin.tsx +1 -1
  143. package/src/plugins/builtin/econ/calendar-model.ts +14 -9
  144. package/src/plugins/builtin/econ/figures.ts +72 -0
  145. package/src/plugins/builtin/econ/index.tsx +33 -3
  146. package/src/plugins/builtin/econ/types.ts +2 -0
  147. package/src/plugins/builtin/econ-statistics/headless.ts +79 -31
  148. package/src/plugins/builtin/econ-statistics/stats.ts +1 -0
  149. package/src/plugins/builtin/econ-statistics/view.ts +3 -0
  150. package/src/plugins/builtin/equity-screener/pane.tsx +2 -2
  151. package/src/plugins/builtin/exposure/charts.tsx +2 -2
  152. package/src/plugins/builtin/exposure/model.ts +13 -1
  153. package/src/plugins/builtin/exposure/pane.tsx +4 -4
  154. package/src/plugins/builtin/futures/index.tsx +1 -0
  155. package/src/plugins/builtin/futures/model.ts +2 -1
  156. package/src/plugins/builtin/futures/table.tsx +2 -2
  157. package/src/plugins/builtin/fx-matrix/client.ts +6 -1
  158. package/src/plugins/builtin/fx-matrix/headless.ts +5 -2
  159. package/src/plugins/builtin/fx-matrix/index.tsx +13 -3
  160. package/src/plugins/builtin/fx-matrix/live-legs.ts +12 -0
  161. package/src/plugins/builtin/help/keybindings-editor.tsx +18 -14
  162. package/src/plugins/builtin/iv-history/headless.ts +3 -0
  163. package/src/plugins/builtin/macro-day/pane.tsx +7 -5
  164. package/src/plugins/builtin/market-movers/footer.ts +2 -2
  165. package/src/plugins/builtin/members/breadth-pane.tsx +81 -0
  166. package/src/plugins/builtin/members/client.ts +19 -4
  167. package/src/plugins/builtin/members/headless.ts +20 -7
  168. package/src/plugins/builtin/members/index.tsx +9 -9
  169. package/src/plugins/builtin/members/model.ts +23 -4
  170. package/src/plugins/builtin/members/pane.tsx +25 -15
  171. package/src/plugins/builtin/options/analytics.ts +26 -2
  172. package/src/plugins/builtin/options/model-spot.ts +5 -2
  173. package/src/plugins/builtin/options/strike-window.ts +2 -1
  174. package/src/plugins/builtin/options/table.ts +1 -7
  175. package/src/plugins/builtin/options/view.tsx +25 -8
  176. package/src/plugins/builtin/options-positioning/client.ts +19 -0
  177. package/src/plugins/builtin/options-positioning/headless.ts +54 -14
  178. package/src/plugins/builtin/options-positioning/index.tsx +3 -3
  179. package/src/plugins/builtin/options-positioning/model.ts +47 -8
  180. package/src/plugins/builtin/options-positioning/pane.tsx +68 -38
  181. package/src/plugins/builtin/perps/freshness.ts +12 -10
  182. package/src/plugins/builtin/perps/live-rows.ts +10 -3
  183. package/src/plugins/builtin/perps/pane.tsx +15 -11
  184. package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -2
  185. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  186. package/src/plugins/builtin/portfolio-list/summary/index.tsx +11 -8
  187. package/src/plugins/builtin/rate-path/pane.tsx +2 -2
  188. package/src/plugins/builtin/research/relative-valuation-pane.tsx +4 -4
  189. package/src/plugins/builtin/scanner/feed.ts +2 -2
  190. package/src/plugins/builtin/scanner/index.ts +24 -1
  191. package/src/plugins/builtin/scanner/live-condition-editor.tsx +222 -0
  192. package/src/plugins/builtin/scanner/live-feed.ts +168 -0
  193. package/src/plugins/builtin/scanner/live-headless.ts +114 -0
  194. package/src/plugins/builtin/scanner/live-model.ts +351 -0
  195. package/src/plugins/builtin/scanner/live-pane.tsx +677 -0
  196. package/src/plugins/builtin/scanner/live-track.ts +186 -0
  197. package/src/plugins/builtin/series-inventory.ts +1 -0
  198. package/src/plugins/builtin/shared/macro-releases.ts +65 -0
  199. package/src/plugins/builtin/shared/scanner-capture.ts +3 -0
  200. package/src/plugins/builtin/shared/use-quote-board.ts +8 -2
  201. package/src/plugins/builtin/shared/volatility/pricing.ts +7 -1
  202. package/src/plugins/builtin/themes/headless.ts +6 -4
  203. package/src/plugins/builtin/themes/model.ts +1 -1
  204. package/src/plugins/builtin/themes/pane.tsx +3 -3
  205. package/src/plugins/builtin/ticker-detail/quote-footer.ts +6 -1
  206. package/src/plugins/builtin/vol-surface/charts.tsx +9 -4
  207. package/src/plugins/builtin/vol-surface/client.ts +6 -4
  208. package/src/plugins/builtin/vol-surface/headless.ts +62 -13
  209. package/src/plugins/builtin/vol-surface/index.tsx +2 -2
  210. package/src/plugins/builtin/vol-surface/model.ts +22 -7
  211. package/src/plugins/builtin/vol-surface/pane.tsx +44 -7
  212. package/src/plugins/builtin/vol-surface/tables.ts +31 -3
  213. package/src/plugins/builtin/volatility/dvol.ts +46 -0
  214. package/src/plugins/builtin/volatility/pane.tsx +2 -2
  215. package/src/plugins/builtin/volatility/series.ts +26 -0
  216. package/src/plugins/builtin/world-indices/footer.ts +1 -1
  217. package/src/plugins/catalog-browser.ts +13 -8
  218. package/src/portfolio/column-values.ts +5 -5
  219. package/src/public/__snapshots__/public-api.test.ts.snap +1 -0
  220. package/src/renderers/browser/app-services.ts +1 -7
  221. package/src/renderers/browser/config-host.ts +2 -55
  222. package/src/renderers/browser/main.tsx +7 -57
  223. package/src/renderers/electrobun/bun/desktop/app-name.ts +222 -0
  224. package/src/renderers/electrobun/bun/desktop/backend.ts +17 -7
  225. package/src/renderers/electrobun/bun/desktop/relaunch.ts +74 -25
  226. package/src/renderers/electrobun/bun/desktop/state-broadcaster.ts +1 -0
  227. package/src/renderers/electrobun/bun/desktop/update.ts +70 -3
  228. package/src/renderers/electrobun/bun/desktop/workspace/index.ts +2 -0
  229. package/src/renderers/electrobun/bun/index.ts +25 -1
  230. package/src/renderers/electrobun/shared/app-name.ts +17 -0
  231. package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +3 -3
  232. package/src/renderers/electrobun/view/desktop/window/snapshot.ts +1 -0
  233. package/src/renderers/opentui/toast-host.tsx +71 -43
  234. package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
  235. package/src/sources/provider-router/batches.ts +10 -2
  236. package/src/sources/provider-router/financial-routes.ts +3 -3
  237. package/src/sources/provider-router/history.ts +28 -7
  238. package/src/sources/provider-router/index.ts +146 -10
  239. package/src/sources/provider-router/search.ts +13 -2
  240. package/src/state/app/context/index.tsx +3 -0
  241. package/src/sync/auth-transition.ts +19 -9
  242. package/src/sync/baseline.ts +10 -4
  243. package/src/sync/controller.ts +66 -16
  244. package/src/sync/react.ts +4 -1
  245. package/src/sync/types.ts +3 -0
  246. package/src/telemetry/usage-counts.ts +0 -18
  247. package/src/theme/colors.ts +8 -0
  248. package/src/tickers/company-name.ts +59 -0
  249. package/src/tickers/listing-venues.ts +23 -0
  250. package/src/tickers/search/bare-symbol.ts +36 -0
  251. package/src/tickers/search/index.ts +36 -7
  252. package/src/tickers/search/ranking.ts +10 -1
  253. package/src/tickers/search/types.ts +5 -0
  254. package/src/time-series/base-series.ts +13 -5
  255. package/src/time-series/daily-valuation.ts +62 -0
  256. package/src/time-series/field-catalog.ts +1 -3
  257. package/src/time-series/forward-valuation.ts +32 -0
  258. package/src/time-series/fundamentals.ts +155 -7
  259. package/src/time-series/hooks.ts +1 -0
  260. package/src/time-series/price-units.ts +72 -2
  261. package/src/time-series/resolve-context.ts +5 -1
  262. package/src/time-series/resolve.ts +61 -12
  263. package/src/time-series/series-presentation.ts +40 -6
  264. package/src/time-series/spec.ts +2 -1
  265. package/src/time-series/studies.ts +270 -72
  266. package/src/time-series/transforms.ts +1 -0
  267. package/src/time-series/types.ts +5 -0
  268. package/src/time-series/valuation-releases.ts +39 -0
  269. package/src/types/data-provider.ts +9 -1
  270. package/src/types/desktop-window.ts +2 -0
  271. package/src/types/financials.ts +41 -1
  272. package/src/types/headless.ts +42 -4
  273. package/src/types/instrument.ts +3 -0
  274. package/src/types/plugin.ts +1 -1
  275. package/src/updater/command-names.ts +12 -7
  276. package/src/utils/financial-statements.ts +6 -1
  277. package/src/utils/network-failure.ts +9 -6
  278. package/src/utils/option-expiry.ts +48 -1
  279. package/src/utils/options.ts +10 -0
  280. package/src/utils/plugin-proxy-hosts.json +1 -0
  281. package/src/utils/price-history.ts +5 -0
  282. package/src/renderers/browser/first-screen.ts +0 -193
@@ -291,6 +291,14 @@ export function priceColor(value: number, palette: ThemeColors = colors): string
291
291
  return palette.neutral;
292
292
  }
293
293
 
294
+ /**
295
+ * `priceColor` for a figure as it is printed, to `digits` decimals: a move that
296
+ * rounds to zero (0.00%, never -0.00%) is neutral, not red or green.
297
+ */
298
+ export function shownPriceColor(value: number, digits: number, palette: ThemeColors = colors): string {
299
+ return priceColor(Number(value.toFixed(digits)), palette);
300
+ }
301
+
294
302
  const TONE_TINT = 0.18;
295
303
 
296
304
  /**
@@ -129,3 +129,62 @@ export function shortListingTitle(listing: { symbol: string; exchange: string; n
129
129
  const name = listing.name?.trim() ? shortCompanyName(listing.name) : listing.symbol;
130
130
  return listing.exchange ? `${name} (${listing.exchange})` : name;
131
131
  }
132
+
133
+ /** Legal forms a list abbreviates, and short words that are words where short capitals would read as initials. */
134
+ const NAME_WORDS: Readonly<Record<string, string>> = {
135
+ INC: "Inc", CORP: "Corp", CO: "Co", LTD: "Ltd", HLDG: "Hldg", HLDGS: "Hldgs", INTL: "Intl", GRP: "Grp", ST: "St",
136
+ OLD: "Old", NEW: "New", AIR: "Air", OIL: "Oil", GAS: "Gas", FOX: "Fox", ONE: "One", TWO: "Two", NON: "Non", NET: "Net",
137
+ PET: "Pet", BIO: "Bio", SUN: "Sun", SIX: "Six", RED: "Red", BOX: "Box", GAP: "Gap", ELI: "Eli", DOW: "Dow", LAB: "Lab",
138
+ MID: "Mid", SON: "Son", GEN: "Gen", HUB: "Hub", OWL: "Owl", REX: "Rex", LA: "La", LAM: "Lam", JOE: "Joe", CAL: "Cal",
139
+ MET: "Met", GRO: "Gro", BUY: "Buy", COM: "Com", SHS: "Shs",
140
+ };
141
+ /** Words a name keeps in lower case unless it opens with one. */
142
+ const NAME_MINOR_WORDS = new Set(["AND", "OF", "THE", "FOR", "DE", "DEL"]);
143
+ /** Spellings that title case gets wrong, for the largest names; any other name reads in plain title case. */
144
+ const NAME_BRANDS: Readonly<Record<string, string>> = {
145
+ NVIDIA: "NVIDIA", JPMORGAN: "JPMorgan", UNITEDHEALTH: "UnitedHealth", ABBVIE: "AbbVie", EXXONMOBIL: "ExxonMobil",
146
+ PEPSICO: "PepsiCo", BLACKROCK: "BlackRock", SERVICENOW: "ServiceNow", CROWDSTRIKE: "CrowdStrike", FEDEX: "FedEx",
147
+ PAYPAL: "PayPal", EBAY: "eBay", DOORDASH: "DoorDash", DRAFTKINGS: "DraftKings", MONGODB: "MongoDB", HUBSPOT: "HubSpot",
148
+ GODADDY: "GoDaddy", CARMAX: "CarMax", MARKETAXESS: "MarketAxess", AUTOZONE: "AutoZone", AUTONATION: "AutoNation",
149
+ COSTAR: "CoStar", IDACORP: "IDACORP", AECOM: "AECOM", CBRE: "CBRE", MSCI: "MSCI",
150
+ };
151
+ /** Longer than three letters, yet still capitals: a legal form, a share structure or an index. */
152
+ const NAME_INITIALISMS = new Set(["REIT", "USA", "ADR", "ADS", "VIII"]);
153
+ const NAME_DOMAINS = new Set(["COM", "NET", "ORG", "IO", "AI"]);
154
+
155
+ /** One run of letters in a name written in capitals. */
156
+ function nameWord(word: string, opening: boolean, domain: boolean): string {
157
+ if (!/\p{L}/u.test(word)) return word;
158
+ if (domain && NAME_DOMAINS.has(word)) return word.toLowerCase();
159
+ const brand = NAME_BRANDS[word];
160
+ if (brand) return brand;
161
+ if (NAME_MINOR_WORDS.has(word)) return opening ? `${word.charAt(0)}${word.slice(1).toLowerCase()}` : word.toLowerCase();
162
+ const plain = NAME_WORDS[word];
163
+ if (plain) return plain;
164
+ // Initials and tickers read as capitals: up to three letters with no word of their own, or no vowel at all.
165
+ if (word.length <= 3 || NAME_INITIALISMS.has(word) || !/[AEIOUY]/.test(word)) return word;
166
+ if (/^MC[A-Z]{3}/.test(word)) return `Mc${word.charAt(2)}${word.slice(3).toLowerCase()}`;
167
+ return `${word.charAt(0)}${word.slice(1).toLowerCase()}`;
168
+ }
169
+
170
+ /**
171
+ * A company name a source writes in capitals (an ETF's holdings file: "APPLE INC", "MERCK & CO INC") as it is
172
+ * normally written: "Apple Inc", "Merck & Co Inc". Legal forms and short words take their usual case, initials
173
+ * inside a name stay capitals ("GE Aerospace", "AT&T", "L3Harris"). A name with any lower case letter is written
174
+ * the way its source means it and comes back as it is.
175
+ */
176
+ export function companyNameCase(name: string): string {
177
+ if (!/\p{Lu}/u.test(name) || name !== name.toUpperCase()) return name;
178
+ let words = 0;
179
+ return name.replace(/\S+/g, (token) => {
180
+ const opening = words++ === 0;
181
+ // A separator inside a word (Coca-Cola, AT&T, Amazon.com, McDonald's) cuts it into words of its own.
182
+ return token.split(/([-/.&,'\u2019]+)/).map((part, index, parts) => {
183
+ if (index % 2 === 1) return part;
184
+ const before = parts[index - 1];
185
+ if (part === "S" && before && /['\u2019]$/.test(before)) return "s";
186
+ // Digits split a word too: Five9, L3Harris. A run of one or two letters beside a number is an initial.
187
+ return part.split(/(\d+)/).map((run, at) => at % 2 === 1 ? run : nameWord(run, opening && index === 0 && at === 0, before === ".")).join("");
188
+ }).join("");
189
+ });
190
+ }
@@ -12,6 +12,7 @@ import {
12
12
  typedExchangeCode,
13
13
  } from "../utils/exchanges";
14
14
  import { currencyPairSpelling } from "../utils/currency-pair";
15
+ import { savedAbroadForBareSymbol } from "./search/bare-symbol";
15
16
 
16
17
  export interface ListingStore {
17
18
  loadTicker(symbol: string): Promise<TickerRecord | null>;
@@ -119,6 +120,28 @@ export async function readAmbiguousCurrencyPair(typed: string, { store, dataProv
119
120
  }
120
121
  }
121
122
 
123
+ /**
124
+ * The saved row a bare symbol stands for, read as the command bar reads it
125
+ * (`savedAbroadForBareSymbol`): a row abroad saved under the bare key (SHEL on
126
+ * LSE, saved from SHEL.L) gives way to the symbol's US listing, the row of it
127
+ * saved under another key, or null when that listing is not saved yet. The
128
+ * row abroad stands when the symbol lists on no US exchange, or search could
129
+ * not say.
130
+ */
131
+ export async function savedRowOfBareSymbol(symbol: string, saved: TickerRecord | null, { store, dataProvider }: ListingDeps): Promise<TickerRecord | null> {
132
+ if (!saved || !savedAbroadForBareSymbol(symbol, saved)) return saved;
133
+ try {
134
+ const tickers = new Map((await store.loadAllTickers()).map((ticker) => [ticker.metadata.ticker.toUpperCase(), ticker] as const));
135
+ const resolved = await resolveTickerSearch({ query: symbol, activeTicker: null, tickers, dataProvider });
136
+ return resolved?.kind === "local" ? resolved.ticker : resolved ? null : saved;
137
+ } catch (error) {
138
+ // US listings no quote settles between: the lookup asks which, as with nothing saved.
139
+ if (error instanceof AmbiguousTickerError && !(error instanceof AmbiguousContractError)) return null;
140
+ logFailure("tickers-listing-venues", "Could not resolve bare symbol")(error);
141
+ return saved;
142
+ }
143
+ }
144
+
122
145
  /** What a typed code that names several venues (7203:TSE) was read as. */
123
146
  export type SharedCodeReading =
124
147
  /** The one of the code's venues the symbol trades on. */
@@ -0,0 +1,36 @@
1
+ import type { TickerRecord } from "../../types/ticker";
2
+ import { tickerHasListingSuffix } from "../../sources/listing-symbols";
3
+ import { canonicalExchange, isUsListingExchange, parsePublicTickerKey } from "../../utils/exchanges";
4
+ import { listingAbroadVenue } from "../../utils/sec";
5
+ import { tickerChangeSymbol } from "../ticker-changes";
6
+
7
+ /**
8
+ * One rule for a bare symbol, whatever the profile saved before: a US-shaped
9
+ * symbol (SHEL, not SHEL.L, SHEL:LSE or 7203) is its US listing whenever it
10
+ * has one. A saved listing abroad (SHEL on LSE, saved from SHEL.L under the
11
+ * bare key) answers for it only when the symbol lists on no US exchange
12
+ * (GARAN, on BIST alone).
13
+ */
14
+ function isBareUsShapedSymbol(symbol: string): boolean {
15
+ return !symbol.includes(":") && !!tickerChangeSymbol(symbol) && !tickerHasListingSuffix(symbol);
16
+ }
17
+
18
+ /** Whether a saved row is a listing abroad that a bare US-shaped symbol settles on only when it has no US listing. */
19
+ export function savedAbroadForBareSymbol(symbol: string, ticker: TickerRecord): boolean {
20
+ if (!isBareUsShapedSymbol(symbol)) return false;
21
+ const venue = savedVenue(ticker);
22
+ return !!venue && !!listingAbroadVenue(`${symbol}:${venue}`);
23
+ }
24
+
25
+ function savedVenue(ticker: TickerRecord): string {
26
+ return canonicalExchange(ticker.metadata.exchange || parsePublicTickerKey(ticker.metadata.ticker).exchange);
27
+ }
28
+
29
+ /** The saved row of a bare symbol's US listing, under any key (SHEL:NYSE beside SHEL on LSE); null when none is saved. */
30
+ export function savedUsListingOf(symbol: string, tickers: Iterable<TickerRecord>): TickerRecord | null {
31
+ for (const ticker of tickers) {
32
+ if (parsePublicTickerKey(ticker.metadata.ticker).symbol !== symbol) continue;
33
+ if (isUsListingExchange(savedVenue(ticker))) return ticker;
34
+ }
35
+ return null;
36
+ }
@@ -39,8 +39,10 @@ import { isProviderBusy, ProviderBusyError, SEARCH_BUSY_MESSAGE } from "../../so
39
39
  import { settleWithin } from "../../utils/async-deadline";
40
40
  import { companyNameExtraWords } from "../company-name";
41
41
  import { withHomeListingLead } from "./home-listing";
42
+ import { savedAbroadForBareSymbol, savedUsListingOf } from "./bare-symbol";
42
43
  import { delistingLabel } from "../../market-data/quotes/delisting";
43
44
  import { hasNonLatinLetters } from "../../utils/typed-text";
45
+ import { isNetworkFailure } from "../../utils/network-failure";
44
46
  export type {
45
47
  ResolvedTickerSearch,
46
48
  TickerSearchCandidate,
@@ -224,6 +226,7 @@ function createProviderTickerSearchCandidates(
224
226
  instrumentClass: classifyInstrumentKind(result.brokerContract?.secType || result.type),
225
227
  instrumentType: result.brokerContract?.secType || result.type,
226
228
  searchAliases: buildSearchResultAliases(result),
229
+ ...(searchResultNames(result) ? { nameAliases: searchResultNames(result) } : {}),
227
230
  ...(searchResultKeywords(result) ? { searchKeywords: searchResultKeywords(result) } : {}),
228
231
  result,
229
232
  }];
@@ -260,7 +263,7 @@ export async function searchTickerCandidates({
260
263
  // A search that hides contracts still shows them when asked: "AAPL OPT".
261
264
  includeOptionContracts: includeOptionContracts || parseAssetClassQuery(query)?.code === "OPT",
262
265
  });
263
- const { results, busy } = await searchProviderResults(
266
+ const { results, busy, unreachable } = await searchProviderResults(
264
267
  dataProvider,
265
268
  query,
266
269
  searchContext,
@@ -268,7 +271,9 @@ export async function searchTickerCandidates({
268
271
  true,
269
272
  );
270
273
  const candidates = assemble(results);
271
- // Saved tickers or another source's rows still answer. Nothing at all, because a source was busy, is no answer.
274
+ // Saved tickers or another source's rows still answer. Nothing at all, because the data service
275
+ // could not be reached or a source was busy, is no answer: not a name nothing lists.
276
+ if (unreachable && candidates.length === 0) throw unreachable;
272
277
  if (busy && candidates.length === 0) throw new ProviderBusyError(SEARCH_BUSY_MESSAGE);
273
278
  return candidates;
274
279
  }
@@ -465,15 +470,27 @@ async function resolveTickerSymbol(
465
470
  if (contracts.size > 1) throw new AmbiguousContractError(symbol,
466
471
  [...contracts.values()].map((contract) => tickerInstrumentLabel(local.metadata.ticker, contract)));
467
472
  }
473
+ // A bare symbol is its US listing whenever it has one (`savedAbroadForBareSymbol`): a row abroad saved under it
474
+ // (SHEL on LSE, from SHEL.L) gives way to the US row saved beside it, else to search's US listing.
475
+ const abroad = !pickerQuery && !!local && savedAbroadForBareSymbol(symbol, local);
476
+ const savedUs = abroad ? savedUsListingOf(symbol, tickers.values()) : null;
477
+ if (savedUs) return { kind: "local", symbol: savedUs.metadata.ticker, ticker: savedUs };
468
478
  // A saved symbol keeps its company. The colon still offers the others.
469
- const saved: ResolvedTickerSearch | null = local ? { kind: "local", symbol: local.metadata.ticker, ticker: local } : null;
470
- if (saved && !pickerQuery) return saved;
479
+ let saved: ResolvedTickerSearch | null = local ? { kind: "local", symbol: local.metadata.ticker, ticker: local } : null;
480
+ if (saved && !pickerQuery && !abroad) return saved;
471
481
 
472
482
  const { results, busy } = await searchProviderResults(dataProvider, symbol, searchContext);
473
483
  const providerItems = createProviderTickerSearchCandidates(results, tickers);
474
484
  const literalMatches = providerItems.filter((item) => normalizeTickerSymbol(item.symbol) === symbol);
475
- const matches = literalMatches.length ? literalMatches
485
+ let matches = literalMatches.length ? literalMatches
476
486
  : providerItems.filter((item) => findExactTickerSearchMatch([item], symbol));
487
+ if (abroad) {
488
+ const us = matches.filter((item) => isUsListingExchange(listingExchange(item.result!)));
489
+ // No US listing: the row abroad is the symbol's.
490
+ if (us.length === 0) return saved;
491
+ matches = us;
492
+ saved = null;
493
+ }
477
494
  let exactMatch = matches[0];
478
495
  if (!exactMatch?.result) {
479
496
  // Nothing matched while a source was too busy to answer: that is no answer, not a miss.
@@ -599,6 +616,8 @@ interface ProviderSearch {
599
616
  results: InstrumentSearchResult[];
600
617
  /** A spelling went unanswered because a source was busy, so an empty list may not mean nothing matches. */
601
618
  busy: boolean;
619
+ /** Why a spelling went unanswered when the data service could not be reached (offline, a hung connection). */
620
+ unreachable: unknown;
602
621
  }
603
622
 
604
623
  async function searchProviderResults(
@@ -651,6 +670,7 @@ async function searchProviderResults(
651
670
  for (const [key, result] of named) byKey.set(key, result);
652
671
  };
653
672
  let busy = false;
673
+ let unreachable: unknown = null;
654
674
  await Promise.all(searchQueries.map(async (searchQuery, index) => {
655
675
  try {
656
676
  const results = await dataProvider.search(searchQuery, {
@@ -669,7 +689,8 @@ async function searchProviderResults(
669
689
  answers[index] = results;
670
690
  merge();
671
691
  } catch (error) {
672
- if (isProviderBusy(error)) busy = true;
692
+ if (isNetworkFailure(error)) unreachable ??= error;
693
+ else if (isProviderBusy(error)) busy = true;
673
694
  else logFailure("tickers-search", "Could not search ticker spelling")(error);
674
695
  }
675
696
  }));
@@ -736,7 +757,7 @@ async function searchProviderResults(
736
757
 
737
758
  // Listings are named in Latin letters, and a source can fail on a name typed in another script (任天堂)
738
759
  // every time it is asked: nothing found for it is no answer to wait for and ask again.
739
- return { results: [...byKey.values()], busy: busy && !hasNonLatinLetters(query) };
760
+ return { results: [...byKey.values()], busy: busy && !hasNonLatinLetters(query), unreachable };
740
761
  }
741
762
 
742
763
  /** Two search texts the server answers alike: its search ignores case and runs of spaces. */
@@ -846,6 +867,14 @@ function getProviderHintScore(result: InstrumentSearchResult, preferredExchange?
846
867
  return getProviderHintRichness(result) + (matchesPreferredExchange ? 10_000 : 0);
847
868
  }
848
869
 
870
+ /** The names the service sent the row for, without the symbol spellings; older servers send none. */
871
+ function searchResultNames(result: InstrumentSearchResult): string[] | undefined {
872
+ const names = Array.isArray(result.searchAliases)
873
+ ? result.searchAliases.filter((alias): alias is string => typeof alias === "string" && !!alias.trim())
874
+ : [];
875
+ return names.length > 0 ? names : undefined;
876
+ }
877
+
849
878
  /** Older servers send none; anything malformed is ignored. */
850
879
  function searchResultKeywords(result: InstrumentSearchResult): string[] | undefined {
851
880
  const keywords = Array.isArray(result.searchKeywords)
@@ -254,7 +254,16 @@ export function rankTickerSearchItems<T extends Pick<TickerSearchRankableItem, "
254
254
  const explicitIntentScore = scoreAssetPreference(intent, item.instrumentClass)
255
255
  + scoreExchangePreference(intent, item);
256
256
  const priorityScore = explicitIntentScore + scoreListingPriority(item);
257
- const companyMatchRank = scoreCompanyNameMatch(intent, companyNameKey);
257
+ // A company the service found by a name it does not bear in this row (the one it had before it was renamed,
258
+ // a trade name) is that company by the name as much as its listing's own name says so. An index answers to
259
+ // its codes, never as a company.
260
+ const companyAliases = item.instrumentClass === "equity" && !(item.symbol || item.label).startsWith("^")
261
+ ? item.nameAliases ?? []
262
+ : [];
263
+ const companyMatchRank = Math.max(
264
+ scoreCompanyNameMatch(intent, companyNameKey),
265
+ ...companyAliases.map((alias) => scoreCompanyNameMatch(intent, normalizeCompanyName(alias))),
266
+ );
258
267
  return {
259
268
  item,
260
269
  index,
@@ -22,6 +22,11 @@ export interface TickerSearchRankableItem {
22
22
  providerRank?: number;
23
23
  popularity?: number;
24
24
  searchAliases?: string[];
25
+ /**
26
+ * The names the service sent the row for (`InstrumentSearchResult.searchAliases`: a trade name, or the name a
27
+ * renamed company had), apart from the symbol spellings `searchAliases` also holds.
28
+ */
29
+ nameAliases?: string[];
25
30
  /** Themes the row answers to that its name does not say (`InstrumentSearchResult.searchKeywords`). */
26
31
  searchKeywords?: string[];
27
32
  }
@@ -10,6 +10,7 @@ import { extractFredSeries } from "./economic";
10
10
  import { getTimeSeriesField, isMarketFieldId } from "./field-catalog";
11
11
  import {
12
12
  unreportedStatementPeriod,
13
+ dailyValuationFallbackPeriod,
13
14
  valuationCurrencyWarning,
14
15
  valuationPriceIssues,
15
16
  } from "./fundamentals";
@@ -20,7 +21,7 @@ import { compareChartQuoteRecency } from "./live-quotes";
20
21
  import { marketSeriesFrequency } from "./market";
21
22
  import { canonicalExchange, resolveExchangeTimeZone } from "../utils/exchanges";
22
23
  import { futuresGenericCaption, futuresGenericListing, futuresListingPriceBasis } from "../utils/futures-generic";
23
- import { contractPriceUnit } from "./price-units";
24
+ import { contractPriceUnit, currencyPairPriceUnit } from "./price-units";
24
25
  import { INDEX_POINTS_UNIT, isIndexLevel } from "../market-data/market/index-level";
25
26
  import { isOhlcSeriesStyle } from "./spec";
26
27
  import { SERIES_COLORS } from "../theme/series-colors";
@@ -118,6 +119,7 @@ export function baseSecuritySeries(
118
119
  const field = getTimeSeriesField(spec.source.fieldId);
119
120
  if (!field) return null;
120
121
  const points = extractSecuritySeries(financials, spec.source);
122
+ const valuationFallback = dailyValuationFallbackPeriod(financials, spec.source);
121
123
  const unreportedPeriod = points.length === 0 ? unreportedStatementPeriod(financials, spec.source) : null;
122
124
  const symbol = instrumentLabel(spec.source);
123
125
  const statementCurrency = field.id.startsWith("fundamental.") && field.unit.startsWith("currency")
@@ -147,8 +149,13 @@ export function baseSecuritySeries(
147
149
  // A future's price in a currency counts what its contract terms say: heating
148
150
  // oil is USD / gallon and crude USD / barrel, which a bare USD does not tell apart.
149
151
  const statedUnit = !genericUnit && unitTemplate === "currency" ? contractPriceUnit(contractUnit, currency) : null;
152
+ // A currency pair's rate is the quote currency one unit of the base buys:
153
+ // EURUSD=X is USD / EUR, so Brent over it reads EUR / barrel.
154
+ const pairUnit = !genericUnit && unitTemplate === "currency" && field.unitGroup === "price" && isMarketFieldId(field.id)
155
+ ? currencyPairPriceUnit(spec.source.instrument.symbol, spec.source.instrument.exchange, currency) : null;
150
156
  const unit = field.id === "market.volume" ? volumeUnit ?? ""
151
- : statedUnit ?? genericUnit ?? (unitTemplate.startsWith("currency") && currency ? unitTemplate.replace("currency", currency) : unitTemplate);
157
+ : statedUnit ?? genericUnit ?? pairUnit
158
+ ?? (unitTemplate.startsWith("currency") && currency ? unitTemplate.replace("currency", currency) : unitTemplate);
152
159
  const currencyUnitGroup = genericUnit ? `${field.unitGroup}:${genericUnit}`
153
160
  : unknownBondBasis && field.unitGroup === "price" ? "price:unknown"
154
161
  : field.unit.startsWith("currency") && currency
@@ -198,10 +205,11 @@ export function baseSecuritySeries(
198
205
  ? financials.quote?.instrumentType || quoteMetadata?.instrumentType : undefined,
199
206
  volumeUnit,
200
207
  ...(priceIssues.length ? { valuationPriceIssues: priceIssues } : {}),
201
- warning: field.id === "market.volume" && !volumeUnit && points.length > 0
202
- ? "Volume unit unknown." : statementCurrency?.warning ?? valuationCurrencyWarning(financials, spec.source),
208
+ warning: [valuationFallback ? "Release dates unavailable; showing reported periods." : undefined,
209
+ field.id === "market.volume" && !volumeUnit && points.length > 0
210
+ ? "Volume unit unknown." : statementCurrency?.warning ?? valuationCurrencyWarning(financials, spec.source)].filter(Boolean).join(" ") || undefined,
203
211
  nativeFrequency: marketField ? marketSeriesFrequency(spec.source, marketResolution)
204
- : spec.source.period && spec.source.period !== "auto" ? spec.source.period : field.nativeFrequency,
212
+ : valuationFallback ?? (spec.source.period && spec.source.period !== "auto" ? spec.source.period : field.nativeFrequency),
205
213
  ...(unreportedPeriod ? { unreportedPeriod } : {}),
206
214
  ...(marketField ? { historyResolution: marketResolution } : {}),
207
215
  timestampMode: spec.source.timestampMode,
@@ -0,0 +1,62 @@
1
+ import type { PricePoint } from "../types/financials";
2
+ import type { TimeSeriesPoint } from "./types";
3
+ import { valuationPriceAtOrBefore } from "./valuation-price";
4
+ import { regularSessionCloseOn } from "../market-data/market/freshness";
5
+ import { financialAvailabilityTime } from "../utils/financial-statements";
6
+
7
+ export interface DailyValuationBasis {
8
+ /** ISO publication date or timestamp. A date without a time takes effect after that session. */
9
+ availableAt?: string;
10
+ /** An undated, unusable statement invalidates the old basis from its period end. */
11
+ observedAt: Date;
12
+ atPrice: (price: number) => number | null;
13
+ periodLabel?: string;
14
+ }
15
+
16
+ /** Reprice a stepped financial figure at each source daily close. */
17
+ export function dailyValuationHistory(
18
+ history: readonly PricePoint[],
19
+ bases: readonly DailyValuationBasis[],
20
+ exchange?: string,
21
+ ): TimeSeriesPoint[] {
22
+ const byDate = new Map<number, PricePoint[]>();
23
+ for (const bar of history) {
24
+ const time = bar.date instanceof Date ? bar.date.getTime() : Date.parse(bar.date);
25
+ if (!Number.isFinite(time) || bar.partial) continue;
26
+ byDate.set(time, [...byDate.get(time) ?? [], bar]);
27
+ }
28
+ const effectiveTime = (basis: DailyValuationBasis) => basis.availableAt
29
+ ? financialAvailabilityTime(basis.availableAt)
30
+ : basis.observedAt.getTime();
31
+ const releases = [...bases].sort((a, b) => effectiveTime(a) - effectiveTime(b)
32
+ || a.observedAt.getTime() - b.observedAt.getTime());
33
+ let next = 0;
34
+ let active: DailyValuationBasis | undefined;
35
+ const points: TimeSeriesPoint[] = [];
36
+ for (const [time, bars] of [...byDate].sort(([a], [b]) => a - b)) {
37
+ // Daily bars name their session at midnight. A timed report belongs to
38
+ // that close only if it was already public, including on early closes.
39
+ const day = new Date(time).toISOString().slice(0, 10);
40
+ const closeTime = regularSessionCloseOn(exchange, day) ?? time;
41
+ while (next < releases.length && effectiveTime(releases[next]!) <= closeTime) {
42
+ const release = releases[next++]!;
43
+ if (!active || release.observedAt >= active.observedAt) active = release;
44
+ }
45
+ const date = new Date(time);
46
+ const price = valuationPriceAtOrBefore(bars, date.toISOString());
47
+ const value = active && price?.price != null ? active.atPrice(price.price) : null;
48
+ points.push({
49
+ date,
50
+ observedAt: date,
51
+ availableAt: active?.availableAt ? new Date(active.availableAt) : undefined,
52
+ value: value !== null && Number.isFinite(value) ? value : null,
53
+ periodLabel: active?.periodLabel,
54
+ provenance: {
55
+ quality: "derived",
56
+ ...(price?.integrity ? { priceHistoryIntegrity: price.integrity } : {}),
57
+ ...(price?.issue ? { valuationPriceIssues: [{ ...price.issue, affectedAt: date.toISOString() }] } : {}),
58
+ },
59
+ });
60
+ }
61
+ return points;
62
+ }
@@ -226,9 +226,7 @@ const FIELDS = [
226
226
  transforms: FUNDAMENTAL_TRANSFORMS,
227
227
  }),
228
228
  ...([
229
- // Trailing twelve-month EPS over the regular session price (the live price in session, the close after it), as
230
- // `valuation` measures it, never an extended print.
231
- ["trailingPE", "Trailing P/E", "P/E", "P/E (TTM, regular session)"],
229
+ ["trailingPE", "Trailing P/E", "P/E"],
232
230
  ["forwardPE", "Forward P/E", "Forward P/E"],
233
231
  ["realizedNtmPE", "Realized NTM P/E", "Realized P/E"],
234
232
  ["pegRatio", "PEG Ratio", "PEG"],
@@ -9,6 +9,7 @@ import type { ValuationCurrencyContext } from "./valuation-currency";
9
9
  import { currentValuationPrice, valuationPriceAtOrBefore } from "./valuation-price";
10
10
  import type { TimeSeriesPoint } from "./types";
11
11
  import { isFiniteNumber } from "../utils/guards";
12
+ import { dailyValuationHistory, type DailyValuationBasis } from "./daily-valuation";
12
13
 
13
14
  /**
14
15
  * Forward multiples over time. No source serves the consensus as it stood on
@@ -236,6 +237,37 @@ export function forwardPeHistory(
236
237
  return points;
237
238
  }
238
239
 
240
+ /** Daily closes over consensus that was actually observed by that date. */
241
+ export function dailyForwardPeHistory(
242
+ financials: TickerFinancials,
243
+ currencies: ValuationCurrencyContext,
244
+ exchange?: string,
245
+ ): TimeSeriesPoint[] {
246
+ const history = financials.epsEstimates;
247
+ if (!history) return [];
248
+ // Pre-report estimates from future quarters are final-vintage figures,
249
+ // not the forecasts investors had on an earlier day. Only dated snapshots
250
+ // establish a historical daily forward denominator.
251
+ const bases: DailyValuationBasis[] = [...snapshotsByDay(history.snapshots)].map(([day, periods]) => {
252
+ const current = periods.get("current year");
253
+ const next = periods.get("next year");
254
+ const eps = blendedNextTwelveMonthsEps(day,
255
+ current ? { periodEnd: current.periodEnd, eps: current.epsAverage } : undefined,
256
+ next ? { eps: next.epsAverage } : undefined);
257
+ const date = new Date(day);
258
+ return {
259
+ availableAt: day,
260
+ observedAt: date,
261
+ periodLabel: `Consensus observed ${day}`,
262
+ atPrice: (price: number) => {
263
+ const comparable = currencies.priceInStatementUnits({ date: day, currency: history.currency ?? financials.financialCurrency }, price);
264
+ return comparable !== null && eps !== null ? comparable / eps : null;
265
+ },
266
+ };
267
+ });
268
+ return dailyValuationHistory(financials.priceHistory, bases, financials.quote?.listingExchangeName || financials.quote?.exchangeName || exchange);
269
+ }
270
+
239
271
  /** Price at each report date over the EPS reported in the following four quarters. */
240
272
  export function realizedNtmPeHistory(
241
273
  financials: TickerFinancials,