gloomberb 0.18.1 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/commodity-balances.ts +6 -0
  3. package/src/api-client/company-kpis.ts +9 -0
  4. package/src/api-client/crypto-markets.ts +9 -0
  5. package/src/api-client/data.ts +6 -5
  6. package/src/api-client/equity-screener.ts +2 -0
  7. package/src/api-client/types.ts +5 -0
  8. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  9. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  10. package/src/app/runtime/plugin-bindings.ts +6 -2
  11. package/src/brokers/logos.ts +66 -0
  12. package/src/brokers/profile-form.ts +3 -0
  13. package/src/brokers/remote-broker-adapter.ts +13 -3
  14. package/src/capabilities/index.ts +1 -1
  15. package/src/capabilities/service-capabilities.ts +7 -1
  16. package/src/cli/base-converter.ts +60 -18
  17. package/src/cli/commands/market.ts +281 -154
  18. package/src/cli/commands/news-search.ts +35 -8
  19. package/src/cli/commands/system.ts +35 -1
  20. package/src/cli/commands/ticker.ts +100 -50
  21. package/src/cli/currency-hints.ts +27 -0
  22. package/src/cli/fx-pair.ts +41 -4
  23. package/src/cli/glossary.ts +57 -4
  24. package/src/cli/helpers.ts +36 -3
  25. package/src/cli/index.ts +4 -1
  26. package/src/cli/known-gaps.ts +37 -1
  27. package/src/cli/listing-arg.ts +37 -2
  28. package/src/cli/pane-functions/catalog.ts +3 -1
  29. package/src/cli/pane-functions/discovery.ts +15 -1
  30. package/src/cli/pane-functions/dom.ts +9 -4
  31. package/src/cli/pane-functions/freshness.ts +4 -4
  32. package/src/cli/pane-functions/function-help.ts +27 -3
  33. package/src/cli/pane-functions/headless.ts +6 -5
  34. package/src/cli/pane-functions/index.ts +3 -2
  35. package/src/cli/pane-functions/lock-note.ts +7 -4
  36. package/src/cli/pane-functions/resolver.ts +2 -1
  37. package/src/cli/quote-answer.ts +179 -0
  38. package/src/cli/series-search.ts +2 -1
  39. package/src/cli/types.ts +4 -0
  40. package/src/components/chart/composite/composite-chart.tsx +11 -2
  41. package/src/components/chart/composite/format.ts +13 -1
  42. package/src/components/chart/composite/level-labels.ts +30 -0
  43. package/src/components/chart/composite/levels.ts +17 -6
  44. package/src/components/chart/composite/panel-surface.tsx +58 -8
  45. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  46. package/src/components/index.ts +2 -0
  47. package/src/components/layout/pane/content.tsx +16 -14
  48. package/src/components/layout/pane/footer/index.tsx +22 -3
  49. package/src/components/layout/pane/footer/model.ts +99 -4
  50. package/src/components/layout/pane/header-tabs.tsx +15 -1
  51. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  52. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  53. package/src/components/onboarding/wizard-model.ts +10 -3
  54. package/src/components/ui/button.tsx +11 -4
  55. package/src/components/ui/choice-dialog.tsx +8 -5
  56. package/src/components/ui/display.tsx +11 -6
  57. package/src/components/ui/fields.tsx +100 -20
  58. package/src/components/ui/icon.tsx +6 -1
  59. package/src/components/ui/list-row-lines.tsx +87 -0
  60. package/src/components/ui/list-view.tsx +14 -0
  61. package/src/components/ui/logo-mark.tsx +64 -0
  62. package/src/components/ui/number-step.ts +54 -0
  63. package/src/components/ui/select-button.tsx +5 -4
  64. package/src/components/ui/select-field.tsx +3 -3
  65. package/src/components/ui/status.tsx +17 -3
  66. package/src/components/ui/tabs.tsx +9 -1
  67. package/src/components/ui/toggle.tsx +17 -3
  68. package/src/core/state/app/instrument.ts +8 -3
  69. package/src/data/config/store/normalize.ts +4 -1
  70. package/src/data/plugin-cache.ts +32 -6
  71. package/src/docs/snapshot.json +5 -5
  72. package/src/i18n/es.ts +9 -3
  73. package/src/i18n/ja.ts +9 -3
  74. package/src/i18n/ko.ts +9 -3
  75. package/src/i18n/zh-cn.ts +10 -4
  76. package/src/i18n/zh-tw.ts +10 -4
  77. package/src/market-data/commodity-names.ts +80 -0
  78. package/src/market-data/coordinator/auxiliary.ts +3 -1
  79. package/src/market-data/coordinator/chart.ts +3 -1
  80. package/src/market-data/futures-board.ts +38 -1
  81. package/src/market-data/futures-contract.ts +91 -0
  82. package/src/market-data/fx-closes.ts +78 -1
  83. package/src/market-data/market/change-basis.ts +219 -0
  84. package/src/market-data/market/format.ts +28 -12
  85. package/src/market-data/market/freshness.ts +38 -0
  86. package/src/market-data/market/status.ts +55 -9
  87. package/src/market-data/market/year-to-date.ts +46 -0
  88. package/src/market-data/quotes/contributions.ts +12 -0
  89. package/src/market-data/quotes/delisting.ts +34 -0
  90. package/src/market-data/quotes/live-valuation.ts +23 -0
  91. package/src/market-data/quotes/resolution.ts +8 -2
  92. package/src/market-data/tax-equivalent.ts +10 -2
  93. package/src/notifications/app-notifier.ts +2 -2
  94. package/src/notifications/notification-log.ts +8 -6
  95. package/src/plugins/builtin/alerts/cli.ts +26 -18
  96. package/src/plugins/builtin/alerts/headless.ts +1 -1
  97. package/src/plugins/builtin/alerts/index.tsx +13 -4
  98. package/src/plugins/builtin/attention/trending.tsx +8 -6
  99. package/src/plugins/builtin/backtest/client.ts +5 -2
  100. package/src/plugins/builtin/backtest/window.ts +16 -3
  101. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  102. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  103. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  104. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  105. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  106. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  107. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  108. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  109. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  110. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  111. package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
  112. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  113. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  114. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  115. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  116. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  117. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  118. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  119. package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
  120. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  121. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  122. package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
  123. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  124. package/src/plugins/builtin/buildout/format.ts +1 -0
  125. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  126. package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
  127. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  128. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  129. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  130. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  131. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  132. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  133. package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
  134. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  135. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  136. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  137. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  138. package/src/plugins/builtin/chat/index.tsx +1 -1
  139. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  140. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  141. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  142. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  143. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  144. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  145. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  146. package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
  147. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  148. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  149. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  150. package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
  151. package/src/plugins/builtin/composite-plugins.ts +3 -2
  152. package/src/plugins/builtin/cot/headless.ts +1 -1
  153. package/src/plugins/builtin/cot/index.tsx +1 -1
  154. package/src/plugins/builtin/cot/model.ts +35 -12
  155. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  156. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  157. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  158. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  159. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  160. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  161. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  162. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  163. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  164. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  165. package/src/plugins/builtin/econ/calendar-model.ts +35 -0
  166. package/src/plugins/builtin/econ/index.tsx +31 -4
  167. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  168. package/src/plugins/builtin/equity-screener/headless.ts +20 -5
  169. package/src/plugins/builtin/equity-screener/model.ts +40 -2
  170. package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
  171. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  172. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  173. package/src/plugins/builtin/exposure/client.ts +4 -1
  174. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  175. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  176. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  177. package/src/plugins/builtin/exposure/pane.tsx +37 -16
  178. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  179. package/src/plugins/builtin/futures/table.tsx +6 -3
  180. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  181. package/src/plugins/builtin/insider/base-values.ts +72 -0
  182. package/src/plugins/builtin/insider/headless.ts +29 -4
  183. package/src/plugins/builtin/jobs/headless.ts +2 -2
  184. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  185. package/src/plugins/builtin/market-halts/model.ts +31 -6
  186. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  187. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  188. package/src/plugins/builtin/members/pane.tsx +8 -3
  189. package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
  190. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  191. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  192. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  193. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  194. package/src/plugins/builtin/options/footer.ts +25 -20
  195. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  196. package/src/plugins/builtin/options/view.tsx +77 -22
  197. package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
  198. package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
  199. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  200. package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
  201. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  202. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  203. package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
  204. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  205. package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
  206. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  207. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  208. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  209. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  210. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  211. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  212. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  213. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  214. package/src/plugins/builtin/scanner/feed.ts +6 -1
  215. package/src/plugins/builtin/scanner/headless.ts +1 -1
  216. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  217. package/src/plugins/builtin/series-inventory.ts +1 -0
  218. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  219. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  220. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  221. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  222. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  223. package/src/plugins/builtin/shared/research-trading.tsx +62 -0
  224. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  225. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  226. package/src/plugins/builtin/themes/headless.ts +8 -2
  227. package/src/plugins/builtin/themes/model.ts +43 -10
  228. package/src/plugins/builtin/themes/pane.tsx +8 -6
  229. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  230. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  231. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  232. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  233. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  234. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  235. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  236. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  237. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  238. package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
  239. package/src/plugins/builtin-editorial.ts +1 -1
  240. package/src/plugins/compat.ts +3 -0
  241. package/src/plugins/external-runtime.ts +5 -0
  242. package/src/plugins/loader.ts +6 -0
  243. package/src/plugins/plugin-icon.ts +61 -0
  244. package/src/plugins/registry/host-actions.ts +2 -1
  245. package/src/plugins/registry/index.ts +9 -1
  246. package/src/plugins/runtime/context.tsx +5 -0
  247. package/src/plugins/runtime/index.tsx +3 -0
  248. package/src/portfolio/column-values.ts +13 -4
  249. package/src/portfolio/cost-fx.ts +5 -28
  250. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  251. package/src/public/broker.ts +8 -0
  252. package/src/public/market-data.ts +5 -0
  253. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  254. package/src/renderers/dom/desktop/controls.tsx +39 -7
  255. package/src/renderers/dom/desktop/icons.tsx +14 -0
  256. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  257. package/src/renderers/dom/desktop/menu.tsx +10 -1
  258. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  259. package/src/renderers/dom/host/style.ts +6 -1
  260. package/src/renderers/dom/host/tabs.tsx +6 -0
  261. package/src/renderers/dom/styles.css +12 -5
  262. package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
  263. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  264. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  265. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  266. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  267. package/src/series/validate.ts +2 -2
  268. package/src/sources/gloomberb-cloud/index.ts +50 -17
  269. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  270. package/src/sources/history-result.ts +3 -0
  271. package/src/sources/provider-errors.ts +3 -1
  272. package/src/sources/provider-router/history.ts +49 -6
  273. package/src/sources/provider-router/index.ts +2 -1
  274. package/src/state/app/activity.ts +13 -0
  275. package/src/sync/controller.ts +4 -0
  276. package/src/sync/core-contributors.ts +62 -11
  277. package/src/sync/local-only.ts +121 -0
  278. package/src/sync/react.ts +6 -0
  279. package/src/sync/types.ts +3 -0
  280. package/src/tickers/listing-fate.ts +27 -8
  281. package/src/tickers/search/index.ts +24 -3
  282. package/src/time-series/price-comparison.ts +35 -0
  283. package/src/time-series/resolve.ts +52 -11
  284. package/src/time-series/spec.ts +1 -1
  285. package/src/time-series/types.ts +5 -0
  286. package/src/types/broker.ts +56 -1
  287. package/src/types/config.ts +6 -0
  288. package/src/types/data-provider.ts +5 -1
  289. package/src/types/financials.ts +23 -0
  290. package/src/types/headless.ts +12 -2
  291. package/src/types/instrument.ts +4 -0
  292. package/src/types/plugin.ts +24 -6
  293. package/src/types/price-history.ts +6 -1
  294. package/src/types/trading.ts +67 -0
  295. package/src/ui/host.tsx +4 -0
  296. package/src/utils/base-currency.ts +108 -0
  297. package/src/utils/currency-names.ts +128 -0
  298. package/src/utils/currency-pair.ts +2 -1
  299. package/src/utils/currency-units.ts +32 -1
  300. package/src/utils/dom-screenshot.ts +22 -9
  301. package/src/utils/exchange-notation.generated.ts +360 -0
  302. package/src/utils/exchanges.ts +15 -322
  303. package/src/utils/format.ts +31 -1
  304. package/src/utils/instrument-identity.ts +38 -1
  305. package/src/utils/options.ts +46 -1
  306. package/src/utils/price-history.ts +4 -0
  307. package/src/utils/reported-money.ts +8 -4
@@ -7,6 +7,7 @@ import { fetchCryptoMarkets, fetchStablecoinSupply } from "./client";
7
7
  import {
8
8
  buildCryptoRows,
9
9
  CRYPTO_BOARD_FRESHNESS,
10
+ CRYPTO_CHANGE_BASIS_LABEL,
10
11
  formatCryptoPercent,
11
12
  formatDayRangeTime,
12
13
  formatOffPeg,
@@ -30,7 +31,9 @@ const COIN_COLUMNS: HeadlessPaneColumn[] = [
30
31
  { key: "code", header: "Coin" },
31
32
  { key: "name", header: "Name" },
32
33
  { key: "price", header: "Price", align: "right", format: (_value, row) => String(row.priceText ?? "—") },
33
- { key: "changePercent", header: "Chg %", align: "right", format: percent },
34
+ // Since 00:00 UTC, the day `quote` gives a coin (CRYPTO_CHANGE_BASIS_LABEL): the text says so, as the pane's
35
+ // CHG% UTC does, and an export keeps the header it has always had.
36
+ { key: "changePercent", header: "Chg %", textHeader: "Chg % UTC", align: "right", format: percent },
34
37
  { key: "return7d", header: "7D %", align: "right", format: percent },
35
38
  { key: "return30d", header: "30D %", align: "right", format: percent },
36
39
  { key: "return1y", header: "1Y %", align: "right", format: percent },
@@ -167,7 +170,7 @@ export const cryptoBoardHeadless: HeadlessPaneDefinition<"rows"> = {
167
170
  pegType: row.pegType,
168
171
  } : {}),
169
172
  }));
170
- const metadata = { list: kind, status: data.status, asOf: data.asOf, generatedAt: data.generatedAt };
173
+ const metadata = { list: kind, status: data.status, asOf: data.asOf, generatedAt: data.generatedAt, changeBasis: CRYPTO_CHANGE_BASIS_LABEL };
171
174
  if (kind !== "stablecoin") return { rows, errors: data.warnings, metadata };
172
175
  const summary = supply ? supplySummaryLine(supply) : null;
173
176
  const history = supply ? supplyHistoryTable(supply) : null;
@@ -57,6 +57,11 @@ export interface CryptoRow extends CryptoRowSupply {
57
57
  price: number;
58
58
  /** The price as the board prints it. */
59
59
  priceText: string;
60
+ /**
61
+ * The move since 00:00 UTC, from the previous UTC day's close, as `quote`
62
+ * measures a coin (CRYPTO_CHANGE_BASIS_LABEL); null when the coin has no
63
+ * such close to measure from.
64
+ */
60
65
  changePercent: number | null;
61
66
  return7d: number | null;
62
67
  return30d: number | null;
@@ -100,9 +105,28 @@ function closeDaysAgo(asset: CryptoMarketAsset, days: number, now: number): numb
100
105
  return isFiniteNumber(close) && close > 0 ? close : null;
101
106
  }
102
107
 
103
- const percentChange = (price: number, reference: number | null) =>
108
+ const percentChange = (price: number, reference: number | null | undefined) =>
104
109
  reference != null && reference > 0 ? (price / reference - 1) * 100 : null;
105
110
 
111
+ /** What the board's day change is measured from, as `quote` labels a coin's: its header says it (CHG% UTC). */
112
+ export const CRYPTO_CHANGE_BASIS_LABEL = "since 00:00 UTC";
113
+
114
+ /**
115
+ * A coin's move since 00:00 UTC, the basis `quote` gives a coin: its live
116
+ * quote's while that quote is measured from the previous close, otherwise the
117
+ * board's, which Gloom Cloud measures the same way (with a live price, from
118
+ * the board's previous close). A coin either says moves over 24 hours, or not
119
+ * at all, has none here, so the column never mixes the two.
120
+ */
121
+ function utcDayChangePercent(asset: CryptoMarketAsset, quote: Quote | null, price: number): number | null {
122
+ if (quote && quote.changeBasis == null) {
123
+ if (isFiniteNumber(quote.changePercent)) return quote.changePercent;
124
+ if (positive(quote.previousClose)) return percentChange(price, quote.previousClose);
125
+ }
126
+ if (asset.changeBasis != null) return null;
127
+ return quote ? percentChange(price, asset.previousClose) : asset.changePercent;
128
+ }
129
+
106
130
  /** A live quote replaces the board's snapshot once it is at least as new. */
107
131
  function liveQuote(
108
132
  asset: CryptoMarketAsset,
@@ -134,11 +158,7 @@ function buildCryptoRow(
134
158
  now = Date.now(),
135
159
  ): CryptoRow {
136
160
  const price = quote?.price ?? asset.price;
137
- const changePercent = quote && isFiniteNumber(quote.changePercent)
138
- ? quote.changePercent
139
- : quote
140
- ? percentChange(price, asset.previousClose)
141
- : asset.changePercent;
161
+ const changePercent = utcDayChangePercent(asset, quote, price);
142
162
  // Supply moves slowly; the price is what makes market cap live. Scaling the
143
163
  // reported cap keeps the source's supply basis and the board's ranking.
144
164
  const marketCap = isFiniteNumber(asset.marketCap)
@@ -389,7 +409,7 @@ const COLUMN_SPECS: CryptoColumnSpec[] = [
389
409
  { id: "code", label: "COIN", width: 7, align: "left" },
390
410
  { id: "name", label: "NAME", width: 10, align: "left", optional: 1 },
391
411
  { id: "price", label: "PRICE", width: 13, align: "right" },
392
- { id: "changePercent", label: "CHG%", width: 8, align: "right" },
412
+ { id: "changePercent", label: "CHG% UTC", title: "Change since 00:00 UTC", width: 8, align: "right" },
393
413
  { id: "return7d", label: "7D%", width: 8, align: "right" },
394
414
  { id: "return30d", label: "30D%", width: 8, align: "right", optional: 3 },
395
415
  { id: "return1y", label: "1Y%", width: 10, align: "right", optional: 5 },
@@ -415,7 +435,7 @@ const STABLECOIN_COLUMN_SPECS: CryptoColumnSpec[] = [
415
435
  { id: "low24hAt", label: "LOW UTC", width: 9, align: "left" },
416
436
  { id: "high24h", label: "24H HIGH", width: 8, align: "right", optional: 1 },
417
437
  { id: "high24hAt", label: "HIGH UTC", width: 9, align: "left", optional: 2 },
418
- { id: "changePercent", label: "CHG%", width: 8, align: "right", optional: 5 },
438
+ { id: "changePercent", label: "CHG% UTC", title: "Change since 00:00 UTC", width: 8, align: "right", optional: 5 },
419
439
  { id: "return7d", label: "7D%", width: 8, align: "right", optional: 6 },
420
440
  { id: "return30d", label: "30D%", width: 8, align: "right", optional: 8 },
421
441
  { id: "return1y", label: "1Y%", width: 10, align: "right", optional: 9 },
@@ -15,6 +15,7 @@ import type { Quote } from "../../../types/financials";
15
15
  import { regularSessionValuationPrice } from "../../../market-data/quotes/regular-session-valuation";
16
16
  import { dividendPriceStatus, dividendQuotePriceMetadata } from "./reference-price";
17
17
  import { mapWithConcurrency } from "../../../utils/concurrency";
18
+ import { logFailure } from "../../../utils/log-failure";
18
19
  import { projectDividendRanking, roundedRatio, type DividendRankingEntry } from "./ranking";
19
20
  import { REPORT_FILTER, REPORT_ROWS } from "../shared/headless-options";
20
21
  import type { FundBondFacts } from "../../../api-client/fund-facts";
@@ -50,6 +51,9 @@ export interface DividendYieldHeadlessDependencies {
50
51
  loadTreasury?(context: HeadlessPaneContext): Promise<TreasuryTenor[] | null>;
51
52
  }
52
53
 
54
+ /** Already logged at the quote boundary; keep it distinct from absent bond facts. */
55
+ class BondClassificationError extends Error {}
56
+
53
57
  const defaultDependencies: DividendYieldHeadlessDependencies = {
54
58
  async loadData(symbol, context) {
55
59
  let currentPrice: number | null = null;
@@ -63,7 +67,8 @@ const defaultDependencies: DividendYieldHeadlessDependencies = {
63
67
  currentPrice = regularSessionValuationPrice(quote);
64
68
  currentPriceCurrency = quote.currency;
65
69
  referenceQuote = quote;
66
- } catch {
70
+ } catch (error) {
71
+ logFailure("dividend-yield.headless", "Read dividend reference quote failed")(error);
67
72
  currentPrice = null;
68
73
  }
69
74
  const data = await fetchDividendData(symbol, currentPrice, exchange, currentPriceCurrency);
@@ -75,7 +80,11 @@ const defaultDependencies: DividendYieldHeadlessDependencies = {
75
80
  async loadBond(symbol, context) {
76
81
  const instrument = await context.resolveInstrument?.(symbol);
77
82
  const exchange = instrument?.exchange ?? "";
78
- const quote = await context.marketData.getQuote(symbol, exchange).catch(() => undefined);
83
+ const quote = await context.marketData.getQuote(symbol, exchange)
84
+ .catch((error) => {
85
+ logFailure("dividend-yield.headless", "Read bond fund classification quote failed")(error);
86
+ throw new BondClassificationError("Bond fund quote check failed");
87
+ });
79
88
  const venue = exchange || quote?.exchangeName || "";
80
89
  if (resolveTickerInstrumentKind(null, { quote }) !== "fund" || (venue && !isUsListingExchange(venue))) return null;
81
90
  return fetchFundBondFacts(instrument?.symbol ?? symbol, context.apiClient);
@@ -125,7 +134,7 @@ export function projectDividendYieldHeadless(
125
134
  const metrics = data.metrics;
126
135
  const currency = data.currency ?? data.payments[0]?.currency ?? "";
127
136
  const priceStatus = dividendPriceStatus(data.price, data.priceAsOf, data.priceStale);
128
- const errors = [data.historyError, data.summaryError].filter((error): error is string => !!error);
137
+ const errors = [data.historyError, data.summaryError, tax?.checkError].filter((error): error is string => !!error);
129
138
 
130
139
  return {
131
140
  ...(errors.length > 0 ? { complete: false, errors } : {}),
@@ -249,15 +258,26 @@ export function createDividendYieldHeadless(
249
258
  // One symbol is its full report; several are one table ranked by yield.
250
259
  if (args.symbols.length <= 1) {
251
260
  const symbol = args.symbols[0] ?? String(args.argument);
261
+ let bondCheckError: string | undefined;
252
262
  const [data, bond, treasury] = await Promise.all([
253
263
  dependencies.loadData(symbol, context),
254
- // A fund's bond facts never fail the report: without them its yields read as before.
255
- dependencies.loadBond?.(symbol, context).catch(() => null) ?? null,
256
- rates ? dependencies.loadTreasury?.(context).catch(() => null) ?? null : null,
264
+ // Optional enrichment without a tax rate; a requested tax check must distinguish failure from absence.
265
+ dependencies.loadBond?.(symbol, context)
266
+ .catch((error) => {
267
+ if (error instanceof BondClassificationError) {
268
+ bondCheckError = error.message;
269
+ } else {
270
+ logFailure("dividend-yield.headless", "Read bond fund facts for report failed")(error);
271
+ bondCheckError = "Bond fund facts check failed";
272
+ }
273
+ return null;
274
+ }) ?? null,
275
+ rates ? dependencies.loadTreasury?.(context)
276
+ .catch(logFailure("dividend-yield.headless", "Read Treasury curve for tax comparison failed", null)) ?? null : null,
257
277
  ]);
258
278
  const forward = data.metrics.lapse ? null : distributionBasis(data.metrics.forwardYield, "forward", data.metrics.forwardRateAsOf ?? null);
259
279
  const tax = rates ? analyzeTaxEquivalent({
260
- symbol: symbol.toUpperCase(), bond, rates, treasury,
280
+ symbol: symbol.toUpperCase(), bond, rates, treasury, checkError: bondCheckError,
261
281
  distribution: forward ?? distributionBasis(data.metrics.trailingYield, "trailing"),
262
282
  }) : null;
263
283
  return projectDividendYieldHeadless(data, args, { bond, tax });
@@ -275,7 +295,9 @@ export function createDividendYieldHeadless(
275
295
  }
276
296
  const entries = await mapWithConcurrency(args.symbols, RANKING_CONCURRENCY, async (symbol): Promise<DividendRankingEntry> => {
277
297
  // A fund's bond facts never fail its row: without them it ranks as before.
278
- const bond = dependencies.loadBond?.(symbol, context).catch(() => null) ?? null;
298
+ const bond = dependencies.loadBond?.(symbol, context)
299
+ .catch((error) => error instanceof BondClassificationError ? null
300
+ : logFailure("dividend-yield.headless", "Read bond fund facts for ranking failed", null)(error)) ?? null;
279
301
  try {
280
302
  return { symbol, data: await dependencies.loadData(symbol, context), bond: await bond };
281
303
  } catch (error) {
@@ -3,6 +3,7 @@ import { regularSessionValuationPrice } from "../../../market-data/quotes/regula
3
3
  import { resolveCurrencyUnit } from "../../../utils/currency-units";
4
4
  import { cashInMajorUnits } from "../../../utils/dividend-cash";
5
5
  import { dividendReferencePrice } from "../../../utils/trailing-dividend";
6
+ import { logFailure } from "../../../utils/log-failure";
6
7
  import { buildDividendMetrics, INCOMPLETE_DIVIDEND_HISTORY, MISSING_DIVIDEND_CURRENCY, toDividendPayment, type DividendData } from "./client";
7
8
  import { dividendQuotePriceMetadata } from "./reference-price";
8
9
 
@@ -17,7 +18,8 @@ export async function fetchProviderDividendData(
17
18
  if (!provider.getCorporateActions) throw new Error("Dividend history source unavailable.");
18
19
  const [sourceActions, quote] = await Promise.all([
19
20
  provider.getCorporateActions(symbol, exchange),
20
- provider.getQuote(symbol, exchange).catch(() => null),
21
+ provider.getQuote(symbol, exchange)
22
+ .catch(logFailure("dividend-yield.provider-client", "Read dividend reference quote failed", null)),
21
23
  ]);
22
24
  // In the major unit as the history arrives, as the provider router serves it.
23
25
  const actions = cashInMajorUnits(sourceActions);
@@ -70,8 +70,8 @@ const defaultDependencies: EarningsCallsHeadlessDependencies = {
70
70
  };
71
71
 
72
72
  /** A refused read says what the lock note says (CALLS is Pro whole), so a free account meets the same words as a signed-out one. */
73
- function explainDenied(ticker: string | null) {
74
- return (error: unknown): never => { throw explainRefusal(error, callsLockNote, { symbol: ticker }); };
73
+ function explainDenied(ticker: string | null, context: HeadlessPaneContext) {
74
+ return async (error: unknown): Promise<never> => { throw await explainRefusal(error, callsLockNote, { symbol: ticker, context }); };
75
75
  }
76
76
 
77
77
  function callRow(call: CloudEarningsCallPayload) {
@@ -285,7 +285,7 @@ export function createEarningsCallsHeadless(
285
285
  },
286
286
  async load(args, context) {
287
287
  const ticker = typeof args.argument === "string" ? args.argument : null;
288
- const calls = await dependencies.loadCalls(ticker, 200, context).catch(explainDenied(ticker));
288
+ const calls = await dependencies.loadCalls(ticker, 200, context).catch(explainDenied(ticker, context));
289
289
  if (!ticker || !readsTranscript(args)) {
290
290
  return projectEarningsCallsHeadless(calls, { ...args, options: { ...args.options, limit: args.options.limit ?? 50 } });
291
291
  }
@@ -299,7 +299,7 @@ export function createEarningsCallsHeadless(
299
299
  }
300
300
  throw new Error(`No ${quarter} earnings call found for ${ticker}.`);
301
301
  }
302
- const transcript = await dependencies.loadTranscript(selected.id, context).catch(explainDenied(ticker));
302
+ const transcript = await dependencies.loadTranscript(selected.id, context).catch(explainDenied(ticker, context));
303
303
  if (isPendingTranscript(transcript)) {
304
304
  throw new Error(`Transcript for ${ticker} ${quarter} is still being produced.`);
305
305
  }
@@ -1,17 +1,26 @@
1
1
  import type { PaneLock } from "../../../types/plugin";
2
- import { proLockNote } from "../shared/pro-lock-note";
2
+ import { resolveHeadlessInstrument } from "../shared/headless-market-data";
3
+ import { proLockNote, type FreeRoute } from "../shared/pro-lock-note";
4
+
5
+ const EVENTS_ROUTE: FreeRoute = {
6
+ text: (ticker, company) => `gloomberb events ${ticker} lists ${company}'s earnings dates, with the EPS reported at each and the next estimate.`,
7
+ answers: async (symbol, context) => {
8
+ const { symbol: ticker, exchange } = await resolveHeadlessInstrument(context, symbol);
9
+ return ((await context.marketData.getCorporateActions?.(ticker, exchange))?.earnings.length ?? 0) > 0;
10
+ },
11
+ };
3
12
 
4
13
  /**
5
14
  * CALLS is Pro whole, the list of calls included. `events` needs no account
6
15
  * and lists a company's earnings dates, each with the EPS it reported and the
7
16
  * next one's estimate, without the call or its transcript.
8
17
  */
9
- export function callsLockNote(lock: PaneLock): string {
18
+ export function callsLockNote(lock: PaneLock): Promise<string> {
10
19
  return proLockNote(lock, {
11
20
  token: "CALLS",
12
21
  argument: "optional",
13
22
  does: "it reads each earnings call into a transcript with speaker turns, analyst Q&A and extracted guidance",
14
23
  shows: "it",
15
- free: (ticker, company) => `gloomberb events ${ticker} lists ${company}'s earnings dates, with the EPS reported at each and the next estimate.`,
24
+ free: [EVENTS_ROUTE],
16
25
  });
17
26
  }
@@ -191,6 +191,37 @@ export function matchesCountry(event: EconEvent, filter: CountryFilter): boolean
191
191
  return event.country === filter;
192
192
  }
193
193
 
194
+ /**
195
+ * How long after its release time a high-impact print is still news: the
196
+ * calendar is asked for it this long, and a print that turns up later is
197
+ * recorded without a toast.
198
+ */
199
+ export const PRINT_PENDING_MS = 20 * 60 * 1000;
200
+
201
+ /** High-impact releases whose time has come in the last {@link PRINT_PENDING_MS} and that have no actual yet. */
202
+ export function pendingHighImpactPrints(events: readonly EconEvent[], now: number): EconEvent[] {
203
+ return events.filter((event) => {
204
+ if (event.impact !== "high" || event.actual) return false;
205
+ const releasedFor = now - event.date.getTime();
206
+ return releasedFor >= 0 && releasedFor <= PRINT_PENDING_MS;
207
+ });
208
+ }
209
+
210
+ export function hasPendingHighImpactPrint(events: readonly EconEvent[], now: number): boolean {
211
+ return pendingHighImpactPrints(events, now).length > 0;
212
+ }
213
+
214
+ /** High-impact rows that were blank in `previous` and have an actual in `next`. */
215
+ export function landedHighImpactPrints(
216
+ previous: ReadonlyMap<string, string | null>,
217
+ next: readonly EconEvent[],
218
+ ): EconEvent[] {
219
+ return next.filter((event) => {
220
+ if (event.impact !== "high" || !event.actual || !previous.has(event.id)) return false;
221
+ return !previous.get(event.id);
222
+ });
223
+ }
224
+
194
225
  export function formatCountdown(ms: number): string {
195
226
  if (ms <= 60_000) return "in <1m";
196
227
  const totalMin = Math.floor(ms / 60_000);
@@ -229,6 +260,10 @@ export function getCalendarCache(options?: { allowExpired?: boolean }): EconCale
229
260
  return cache.get(CACHE_KEY, options);
230
261
  }
231
262
 
263
+ export function subscribeCalendar(listener: () => void): () => void {
264
+ return cache.subscribe(CACHE_KEY, listener);
265
+ }
266
+
232
267
  export interface EconCalendarLoadResult extends EconCalendarCacheEntry {
233
268
  refreshError?: string;
234
269
  }
@@ -20,6 +20,7 @@ import type { PluginModule } from "../plugin-module";
20
20
  import { colors, blendHex } from "../../../theme/colors";
21
21
  import type { EconEvent } from "./types";
22
22
  import { FRED_CATALOG_SERIES } from "./fred-catalog";
23
+ import { ECON_PRINT_NOTIFICATIONS_ENABLED_KEY, retainVisiblePrintRefresh, setupHighImpactPrintNotifications } from "./print-watch";
23
24
  import { EconDetailView } from "./detail-view";
24
25
  import {
25
26
  COUNTRY_CYCLE,
@@ -35,6 +36,7 @@ import {
35
36
  formatCountdown,
36
37
  formatStaleness,
37
38
  getCalendarCache,
39
+ subscribeCalendar,
38
40
  impactIndicator,
39
41
  loadCalendar,
40
42
  matchesCountry,
@@ -75,10 +77,16 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
75
77
  // loadCalendar serves a fresh cache without a request, so the pane can always
76
78
  // ask and still follow the global cadence once the cache goes stale.
77
79
  const calendar = useAsyncResource<CalendarResult>(loadEvents, { initialData: cachedEvents });
80
+ // A load made elsewhere, such as the print watch asking while a release is
81
+ // due, reaches the open pane through the cache; the newer of the two wins.
82
+ const [cacheVersion, setCacheVersion] = useState(0);
83
+ useEffect(() => subscribeCalendar(() => setCacheVersion((version) => version + 1)), []);
84
+ const cached = useMemo(() => getCalendarCache(), [cacheVersion]);
85
+ const shown = cached && (!calendar.data || cached.fetchedAt > calendar.data.fetchedAt) ? cached : calendar.data;
78
86
  const { loading } = calendar;
79
- const events = calendar.data?.data ?? NO_EVENTS;
80
- const stale = calendar.data?.stale ?? false;
81
- const fetchedAt = calendar.data?.fetchedAt ?? null;
87
+ const events = shown?.data ?? NO_EVENTS;
88
+ const stale = shown?.stale ?? false;
89
+ const fetchedAt = shown?.fetchedAt ?? null;
82
90
  // A cache that could not be refreshed arrives as a result, not a failure.
83
91
  const error = calendar.error ?? (loading ? null : calendar.data?.refreshError ?? null);
84
92
  // The selected row and the open event are remembered by event id, so a
@@ -94,8 +102,11 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
94
102
 
95
103
  useAutoRefresh(stale ? null : fetchedAt, calendar.load);
96
104
 
97
- // Tick every 30s to update staleness + countdown, only while the pane can be seen.
98
105
  const paneVisible = usePaneVisible();
106
+ // In view, a due high-impact print is asked for on the print watch's timing
107
+ // and lands here through the cache; a covered or hidden pane asks nothing.
108
+ useEffect(() => (paneVisible ? retainVisiblePrintRefresh() : undefined), [paneVisible]);
109
+ // Tick every 30s to update staleness + countdown, only while the pane can be seen.
99
110
  useEffect(() => {
100
111
  if (!paneVisible) return;
101
112
  setNow(Date.now());
@@ -437,6 +448,8 @@ function EconCalendarPane({ focused, width, height }: PaneProps) {
437
448
  );
438
449
  }
439
450
 
451
+ let stopPrintWatch: (() => void) | null = null;
452
+
440
453
  export const economicCalendarModule: PluginModule = {
441
454
  series: FRED_CATALOG_SERIES,
442
455
  panes: [{
@@ -449,6 +462,16 @@ export const economicCalendarModule: PluginModule = {
449
462
  defaultMode: "floating",
450
463
  defaultFloatingSize: { width: 100, height: 30 },
451
464
  tableExport: true,
465
+ settings: {
466
+ title: "Economic Calendar Settings",
467
+ fields: [{
468
+ key: ECON_PRINT_NOTIFICATIONS_ENABLED_KEY,
469
+ label: "Notifications",
470
+ description: "Notify when a high-impact release prints, even while this pane is closed.",
471
+ type: "toggle",
472
+ storage: "plugin",
473
+ }],
474
+ },
452
475
  }],
453
476
  paneTemplates: [{
454
477
  id: "econ-calendar-pane",
@@ -461,8 +484,12 @@ export const economicCalendarModule: PluginModule = {
461
484
  }],
462
485
  setup(ctx) {
463
486
  attachEconCalendarPersistence(ctx.persistence);
487
+ stopPrintWatch?.();
488
+ stopPrintWatch = setupHighImpactPrintNotifications(ctx);
464
489
  },
465
490
  dispose() {
491
+ stopPrintWatch?.();
492
+ stopPrintWatch = null;
466
493
  resetEconCalendarPersistence();
467
494
  },
468
495
  };