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
@@ -53,7 +53,9 @@ import {
53
53
  } from "../../plugins/builtin/research/analyst-model";
54
54
  import { getPublishedUsEquityCalendarYears, getPublishedUsEquitySession } from "../../market-data/published-us-sessions";
55
55
  import { isQuoteStaleForCurrentSession, noPrintsTodayText } from "../../market-data/quotes/freshness";
56
- import { isOptionsUnavailableError, listedOptionsAlternative, optionsUnavailableTitle } from "../../market-data/options-alternatives";
56
+ import {
57
+ coinOptionsAlternativesText, isCoinWithoutOptions, isOptionsUnavailableError, listedOptionsAlternative, optionsUnavailableTitle,
58
+ } from "../../market-data/options-alternatives";
57
59
  import {
58
60
  atReportTtmRate, cliFuturesContract, futuresContractMetrics, exportedDividendFundamentals, fundamentalsReportTables, loadDividendDistributions,
59
61
  loadReportBondFacts, renderFundamentalsReport, writesCurrencyCodes,
@@ -72,9 +74,15 @@ import {
72
74
  formatExpiryList,
73
75
  loadOptionModelInputs,
74
76
  loadOptionsChainOpenInterest,
77
+ chainCoinCurrency,
78
+ formatCoinPremiumCell,
75
79
  formatOptionDeltaCell,
80
+ formatOptionDollarCell,
81
+ formatOptionGammaCell,
76
82
  formatOptionIvCell,
77
83
  formatOptionQuoteCell,
84
+ isMarkedChain,
85
+ MARKED_OPTION_FIELDS,
78
86
  OPTION_MODEL_RATE,
79
87
  OPTION_VOLATILITY_FIELDS,
80
88
  OPTIONS_USAGE,
@@ -87,7 +95,7 @@ import {
87
95
  } from "./options-chain";
88
96
  import {
89
97
  expirationHint, expiredExpiryText, expiryIsoDate, isLiveOptionExpiry, liveDefaultExpiry, liveOptionExpiries, missingExpiryMessage,
90
- OPTION_EXPIRATION_FORMAT, OPTION_EXPIRATION_PLACEHOLDER, withLiveExpiryList,
98
+ OPTION_EXPIRATION_FORMAT, OPTION_EXPIRATION_PLACEHOLDER, optionExpiryVenue, withLiveExpiryList,
91
99
  } from "../../utils/option-expiry";
92
100
  import { DATE_OPTION_EXAMPLES, isoDateOption, isoDateToday } from "../../utils/calendar-date";
93
101
  import { apiClient } from "../../api-client";
@@ -115,6 +123,7 @@ import {
115
123
  closedPeriodBarDay,
116
124
  formingPeriodBar,
117
125
  historyBarStatusNote,
126
+ venueTradingDay,
118
127
  historyCoverageWarning,
119
128
  historyFacts,
120
129
  historyFlagNote,
@@ -683,7 +692,7 @@ function changeBasisNotes(results: QuoteCliRecord[], now = Date.now()): string[]
683
692
  function withPreviousCloseDate(quote: Quote | null | undefined): (Quote & { previousCloseDate?: string | null }) | null {
684
693
  if (!quote) return null;
685
694
  const previousCloseDate = quotePreviousCloseDate(quote);
686
- const entries = Object.entries(quote);
695
+ const entries = Object.entries(quote).filter(([key]) => key !== "previousCloseDate");
687
696
  const at = entries.findIndex(([key]) => key === "previousClose");
688
697
  entries.splice(at < 0 ? entries.length : at + 1, 0, ["previousCloseDate", previousCloseDate]);
689
698
  return Object.fromEntries(entries) as Quote & { previousCloseDate?: string | null };
@@ -1007,13 +1016,14 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
1007
1016
  : listings.map((listing) => [listing, startTickerChangeLookup(market.dataProvider, listing.symbol, listing.exchange)] as const));
1008
1017
  // The service's quote of the moment, not each form's local copy from whenever it was last asked for:
1009
1018
  // BRK.B saved on NYSE, BRK-B and BRK.B:NYSE print one quote. A local copy answers only offline.
1010
- const results = await market.dataProvider.getQuotesBatch(targets, { forceRefresh: ctx.cliOptions.refresh, revalidate: true });
1019
+ // Each row prints what its move is from (Chg Basis).
1020
+ const results = await market.dataProvider.getQuotesBatch(targets, { forceRefresh: ctx.cliOptions.refresh, revalidate: true, datesChangeBasis: true });
1011
1021
  const listingOf = (result: QuoteBatchResult, index: number) => listings[targets.indexOf(result.target)] ?? listings[index]!;
1012
1022
  const asked = results.map(listingOf);
1013
1023
  const request = { command: commandName, noun: "quote" };
1014
1024
  const answers = await Promise.all(results.map((result, index) => {
1015
1025
  const listing = asked[index]!;
1016
- return answerQuote(result, listing, market, request, ctx.cliOptions.refresh, venues.get(listing) ?? null);
1026
+ return answerQuote(result, listing, market, request, ctx.cliOptions.refresh, venues.get(listing) ?? null, { datesChangeBasis: true });
1017
1027
  }));
1018
1028
  const now = Date.now();
1019
1029
  const symbolChanges = await Promise.all(answers.map(async ({ result }, index) => {
@@ -1436,7 +1446,7 @@ async function loadListingHistory(
1436
1446
  // The status line says how far a bar still forming runs; the note is for the bars it does not name.
1437
1447
  gaps ?? [], historyJumpNote(data) ?? [], historyFlagNote(data) ?? [], historyMissingVolumeNote(data) ?? [],
1438
1448
  historyVenueVolumeNote(data) ?? [],
1439
- historyBarStatusNote(forming ? served : data) ?? [],
1449
+ historyBarStatusNote(forming ? served : data, { tradingDay: venueTradingDay(venue, Date.now()) }) ?? [],
1440
1450
  ].flat();
1441
1451
  return {
1442
1452
  listing, identity, unit, data, resolution, interval, intraday, volume, freshness, notes,
@@ -1641,7 +1651,7 @@ async function runHistoryList(
1641
1651
  oldest: null,
1642
1652
  });
1643
1653
  // A saved copy is marked on the bars a symbol loaded, not on the rows drawn from them (loadListingHistory
1644
- // reads it there): the list ends "couldn't refresh; saved <time>" for the oldest copy among its symbols.
1654
+ // reads it there): the list ends "couldn't refresh; saved copy from <time>" for the oldest copy among its symbols.
1645
1655
  const savedAt = served.flatMap((history) => history.freshness?.savedAt ?? []).sort()[0];
1646
1656
  const freshness = listFreshness && savedAt && !listFreshness.savedAt ? { ...listFreshness, savedAt } : listFreshness;
1647
1657
  const decimals = (row: Record<string, unknown>) => bySymbol.get(String(row.symbol))?.decimals ?? 2;
@@ -2642,6 +2652,8 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
2642
2652
  const fate = listingFateLoader(listing, market, quotePromise);
2643
2653
  const inputsPromise = loadOptionModelInputs(market.dataProvider, listing, quotePromise, refresh);
2644
2654
  const now = Date.now();
2655
+ // Expiries close on the listing's clock: a coin's options (Deribit) settle at 08:00 UTC on any day.
2656
+ const expiryVenue = optionExpiryVenue(symbol, exchange);
2645
2657
  const readChain = async (expiry: number | undefined) => {
2646
2658
  const cached = await market.dataProvider.getCachedQuery?.("getOptionsChain", [symbol, exchange, expiry, undefined])
2647
2659
  .load({ force: refresh });
@@ -2653,17 +2665,17 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
2653
2665
  };
2654
2666
  };
2655
2667
  // An expiry whose last session has closed has no chain to show: it says so, and names the nearest live one.
2656
- if (expirationDate != null && !isLiveOptionExpiry(expirationDate, now, exchange)) {
2668
+ if (expirationDate != null && !isLiveOptionExpiry(expirationDate, now, expiryVenue)) {
2657
2669
  const listed = await readChain(undefined).then(({ chain }) => chain.expirationDates,
2658
2670
  logFailure("cli-market", "Read available option expirations failed", []));
2659
2671
  ctx.fail(`expiry ${expiryIsoDate(expirationDate)} for ${listing.key} `
2660
- + expiredExpiryText(expirationDate, listed, now, { exchange, flag: "expiration" }),
2672
+ + expiredExpiryText(expirationDate, listed, now, { exchange: expiryVenue, flag: "expiration" }),
2661
2673
  `Run gloom options ${listing.key} to list the live expiries.`);
2662
2674
  }
2663
2675
  const { result, chain: read } = await loadForListing(listing, market, ctx, async () => {
2664
2676
  const first = await readChain(expirationDate);
2665
2677
  // A source can still default to the expiry that lapsed at Friday's close; the nearest live one is shown instead.
2666
- const live = expirationDate == null ? liveDefaultExpiry(first.chain, now, exchange) : undefined;
2678
+ const live = expirationDate == null ? liveDefaultExpiry(first.chain, now, expiryVenue) : undefined;
2667
2679
  if (live === null) return { ...first, chain: { ...first.chain, calls: [], puts: [] } };
2668
2680
  if (live == null) return first;
2669
2681
  // The default's list stays the catalogue: a dated chain can list fewer expiries.
@@ -2672,6 +2684,8 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
2672
2684
  expirationDates: [...new Set([...first.chain.expirationDates, ...shown.chain.expirationDates])].sort((a, b) => a - b) } };
2673
2685
  }, undefined, { command: "options", noun: "options chain", fate }).catch((error: unknown) => {
2674
2686
  if (!isOptionsUnavailableError(error)) throw error;
2687
+ // A coin other than BTC and ETH: say which coins have options, and the bitcoin-linked listings.
2688
+ if (isCoinWithoutOptions(listing.key)) return ctx.fail(optionsUnavailableTitle(listing.key), coinOptionsAlternativesText("options"));
2675
2689
  // A future or commodity with no chain of its own: name the listed fund whose options stand in.
2676
2690
  const alternative = listedOptionsAlternative(listing.key);
2677
2691
  // A name read as a fund or an index asks about the company's stock it means, whose options are listed.
@@ -2681,13 +2695,13 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
2681
2695
  + ` Strategy payoffs: gloom fn OSA ${alternative.symbol}`
2682
2696
  : "Strategy payoffs on a listed underlying: gloom fn OSA <ticker>");
2683
2697
  });
2684
- const loaded = withLiveExpiryList(read, now, exchange);
2698
+ const loaded = withLiveExpiryList(read, now, expiryVenue);
2685
2699
  const inputs = await inputsPromise;
2686
2700
  if (expirationDate != null && !chainHasExpiry(loaded, expirationDate)) {
2687
2701
  // An unlisted date comes back as an empty chain; its own list of expiries, else the default chain's, says what to pick.
2688
2702
  const listed = loaded.expirationDates.length > 0 ? loaded.expirationDates : await market.dataProvider
2689
2703
  .getOptionsChain(symbol, exchange, undefined, { cacheMode: "default" })
2690
- .then((fallback) => liveOptionExpiries(fallback.expirationDates, now, exchange),
2704
+ .then((fallback) => liveOptionExpiries(fallback.expirationDates, now, expiryVenue),
2691
2705
  logFailure("cli-market", "Read available option expirations failed", []));
2692
2706
  const miss = missingExpiryMessage(listing.key, expirationDate, listed);
2693
2707
  ctx.fail(miss.message, miss.details);
@@ -2706,7 +2720,10 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
2706
2720
  : `Options refresh failed; showing the chain stored ${new Date(result.fetchedAt).toISOString()}`
2707
2721
  + (chain.asOf ? ` (last trade ${chain.asOf})` : "");
2708
2722
  const sessionWarning = refreshWarning ? null : priorSessionChainWarning(chain, identity.exchange, Date.now());
2709
- const modelWarning = inputs.spot == null && (chain.calls.length > 0 || chain.puts.length > 0)
2723
+ // A venue that marks its contracts (Deribit) states IV and greeks without a quote.
2724
+ const marked = isMarkedChain(chain);
2725
+ const coin = chainCoinCurrency(chain);
2726
+ const modelWarning = !marked && inputs.spot == null && (chain.calls.length > 0 || chain.puts.length > 0)
2710
2727
  ? `IV and delta need a current ${listing.key} quote; those columns are blank`
2711
2728
  : null;
2712
2729
  const listed = chain.expirationDates.length > 0 || chain.calls.length > 0 || chain.puts.length > 0;
@@ -2733,25 +2750,28 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
2733
2750
  // `iv` is annualised volatility and `delta` the option's change per 1.00 move in the underlying, both decimals.
2734
2751
  units: { iv: "annualized decimal", delta: "decimal per 1.00 move in the underlying" },
2735
2752
  // What each contract's `iv` and `impliedVolatility` are, so neither is read as the other.
2736
- fields: OPTION_VOLATILITY_FIELDS,
2753
+ fields: marked ? MARKED_OPTION_FIELDS : OPTION_VOLATILITY_FIELDS,
2737
2754
  // What `iv` and `delta` on each contract are valued from; the provider's own impliedVolatility is left as sent.
2738
- model: {
2739
- spot: basisFields.spot, spotBasis: basisFields.spotBasis, spotAsOf: basisFields.spotAsOf,
2740
- valuationTime: modelSpot ? new Date(modelSpot.valuationTime).toISOString() : null,
2741
- dividendYield: inputs.dividendYield ?? null, rate: OPTION_MODEL_RATE,
2742
- },
2755
+ model: marked
2756
+ ? { venue: chain.venue ?? null, marks: "venue", rate: chain.rate ?? 0, basis: "the expiry's forward", coin }
2757
+ : {
2758
+ spot: basisFields.spot, spotBasis: basisFields.spotBasis, spotAsOf: basisFields.spotAsOf,
2759
+ valuationTime: modelSpot ? new Date(modelSpot.valuationTime).toISOString() : null,
2760
+ dividendYield: inputs.dividendYield ?? null, rate: OPTION_MODEL_RATE,
2761
+ },
2743
2762
  openInterest: { asOf: basisFields.openInterestAsOf, listedAsOf: basis.openInterest?.listedDate ?? null,
2744
2763
  compared: basis.openInterest?.compared ?? 0, matched: basis.openInterest?.matched ?? 0 },
2745
2764
  },
2746
2765
  ...(warnings.length > 0 ? { warnings } : {}),
2747
2766
  freshness,
2748
2767
  }, {
2749
- heading: listingHeading(identity),
2768
+ // Options a venue other than the US exchanges lists (Deribit's) are headed by that venue, not the coin's board.
2769
+ heading: chain.venue ? `${cliStyles.bold(identity.name || identity.symbol)}${cliStyles.muted(` · ${chain.venue}`)}` : listingHeading(identity),
2750
2770
  rows: (data: OptionsChain) => optionRows(data).map((row) => ({ ...row, ...basisFields })),
2751
2771
  // Choosing an expiry needs the list; once one is chosen the table is that expiry.
2752
2772
  summary: (data: OptionsChain) => expirationDate == null ? formatExpiryList(data.expirationDates) : "",
2753
2773
  empty: `No option contracts for ${listingTitle(identity)}.`,
2754
- ...(contractCount > 0 ? { footnote: optionsBasisFootnote(basis) } : {}),
2774
+ ...(contractCount > 0 ? { footnote: optionsBasisFootnote(basis, chain) } : {}),
2755
2775
  exportIdentity: { symbolOrSeries: listing.key, currencyOrUnit: currency, interval: OPTIONS_EXPORT_INTERVAL },
2756
2776
  // Exports state each decimal's unit in its header and end every row with the spot and dates it is as of.
2757
2777
  columns: [
@@ -2770,8 +2790,34 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
2770
2790
  { key: "spotBasis", header: "Spot basis" },
2771
2791
  { key: "spotAsOf", header: "Spot as of" },
2772
2792
  { key: "openInterestAsOf", header: "OI as of" },
2793
+ // A venue's marks and its coin prices (Deribit's BTC and ETH options).
2794
+ ...(marked ? [
2795
+ { key: "mark", header: "Mark (USD)" }, { key: "gamma", header: "Gamma (per 1.00)" },
2796
+ { key: "vega", header: "Vega (USD per vol point)" }, { key: "theta", header: "Theta (USD per day)" },
2797
+ { key: "forward", header: "Forward" }, { key: "expiresAt", header: "Settles" },
2798
+ ] : []),
2799
+ ...(coin ? [
2800
+ { key: "coinBid", header: `Bid (${coin})` }, { key: "coinAsk", header: `Ask (${coin})` },
2801
+ { key: "coinMark", header: `Mark (${coin})` }, { key: "coinLast", header: `Last (${coin})` },
2802
+ ] : []),
2773
2803
  ],
2774
- textColumns: [
2804
+ // A coin's options are quoted in the coin: its bid, ask and mark, then the mark in dollars and the venue's greeks.
2805
+ textColumns: coin ? [
2806
+ { key: "side", header: "Side" },
2807
+ { key: "contract", header: "Contract", shrink: false },
2808
+ { key: "strike", header: "Strike", align: "right" },
2809
+ { key: "coinBid", header: `Bid ${coin}`, align: "right", format: formatCoinPremiumCell },
2810
+ { key: "coinAsk", header: `Ask ${coin}`, align: "right", format: formatCoinPremiumCell },
2811
+ { key: "coinMark", header: `Mark ${coin}`, align: "right", format: formatCoinPremiumCell },
2812
+ { key: "mark", header: "Mark $", align: "right", format: formatOptionDollarCell },
2813
+ { key: "iv", header: "IV", align: "right", format: formatOptionIvCell },
2814
+ { key: "delta", header: "Delta", align: "right", format: formatOptionDeltaCell },
2815
+ { key: "gamma", header: "Gamma", align: "right", format: formatOptionGammaCell, optional: true, dropPriority: 3 },
2816
+ { key: "vega", header: "Vega $", align: "right", format: formatOptionDollarCell, optional: true, dropPriority: 2 },
2817
+ { key: "theta", header: "Theta $", align: "right", format: formatOptionDollarCell, optional: true, dropPriority: 2 },
2818
+ { key: "volume", header: "Vol", align: "right", format: formatCountCell, optional: true, dropPriority: 1 },
2819
+ { key: "openInterest", header: "OI", align: "right", format: formatCountCell, optional: true, dropPriority: 1 },
2820
+ ] : [
2775
2821
  { key: "side", header: "Side" },
2776
2822
  { key: "contract", header: "Contract", shrink: false },
2777
2823
  // The contract symbol already carries the date, so this goes first when the table is narrow.
@@ -13,8 +13,9 @@ import {
13
13
  chainOpenInterestAsOf, loadListedOpenInterest, openInterestAsOfLabel, type OpenInterestAsOf, statedOpenInterestAsOf,
14
14
  } from "../../plugins/builtin/options/open-interest-as-of";
15
15
  import { DEFAULT_OPTION_CALC_DRAFT } from "../../plugins/builtin/options-calculator/model";
16
- import { expiryIsoDate, OPTION_EXPIRATION_PLACEHOLDER, parseOptionExpiration } from "../../utils/option-expiry";
16
+ import { expiryIsoDate, OPTION_EXPIRATION_PLACEHOLDER, parseOptionExpiration, settlementStampTime } from "../../utils/option-expiry";
17
17
  import { loadModelDividendYield } from "../../market-data/model-dividend-yield";
18
+ import { formatUtcTime } from "../../utils/utc-time";
18
19
 
19
20
  export const OPTIONS_USAGE = `options <symbol> [--expiration <${OPTION_EXPIRATION_PLACEHOLDER}>]`;
20
21
 
@@ -68,6 +69,24 @@ interface OptionModelFigures {
68
69
  /** The risk-free rate every options valuation in the app assumes. */
69
70
  export const OPTION_MODEL_RATE = DEFAULT_OPTION_CALC_DRAFT.rate;
70
71
 
72
+ /** The venue's own marks a chain carries (Deribit's), stated in its JSON metadata in place of OPTION_VOLATILITY_FIELDS. */
73
+ export const MARKED_OPTION_FIELDS = {
74
+ iv: "The venue's mark implied volatility (decimal), one per contract. Delta and the other greeks under mark are the venue's too: "
75
+ + "Black-76 at the expiry's forward with no discounting, to its settlement.",
76
+ bid: "Prices in USD are the coin prices (coin) at the expiry's forward (mark.forward), the venue's own model price.",
77
+ mark: "price in USD; iv; delta and gamma per 1.00 move in the underlying; vega per volatility point and theta per calendar day, in USD.",
78
+ } as const;
79
+
80
+ /** Whether a venue marks the chain's contracts itself (Deribit): its IV and greeks are then used as they are. */
81
+ export function isMarkedChain(chain: OptionsChain): boolean {
82
+ return [...chain.calls, ...chain.puts].some((contract) => contract.mark != null);
83
+ }
84
+
85
+ /** The coin a chain's premiums are quoted in (BTC), when a venue quotes them in one. */
86
+ export function chainCoinCurrency(chain: OptionsChain): string | null {
87
+ return [...chain.calls, ...chain.puts].find((contract) => contract.coin)?.coin?.currency ?? null;
88
+ }
89
+
71
90
  /** What each contract's two volatilities are, stated in the chain's JSON metadata so neither is read as the other. */
72
91
  export const OPTION_VOLATILITY_FIELDS = {
73
92
  iv: "Annualized volatility (decimal) solved from the quote midpoint, one per strike from its out-of-the-money side, at the "
@@ -189,10 +208,23 @@ export async function loadOptionsChainOpenInterest(symbol: string, chain: Option
189
208
 
190
209
  /**
191
210
  * The line under the chain: `Greeks at spot 89.79 (close Mon 12 Oct 20:00 UTC), dividend yield 2.34%, rate 4% · OI as of Fri 9 Oct`.
192
- * An open interest whose date cannot be established says why.
211
+ * An open interest whose date cannot be established says why. A chain its
212
+ * venue marks says whose marks they are: `Deribit marks: IV and greeks at
213
+ * each expiry's forward, rate 0%; $ is the BTC price at the forward · OI live, Thu 22 Oct 22:42 UTC`.
193
214
  */
194
- export function optionsBasisFootnote(basis: OptionsChainBasis): string {
215
+ export function optionsBasisFootnote(basis: OptionsChainBasis, chain?: OptionsChain): string {
195
216
  const parts: string[] = [];
217
+ if (chain && isMarkedChain(chain)) {
218
+ const coin = chainCoinCurrency(chain);
219
+ parts.push(`${chain.venue ?? "Venue"} marks: IV and greeks at each expiry's forward, rate ${Number(((chain.rate ?? 0) * 100).toFixed(2))}%`
220
+ + (coin ? `; $ is the ${coin} price at the forward` : ""));
221
+ // Its expiries settle at a stated time (08:00 UTC), which the expiration stamps carry.
222
+ const settles = [...chain.calls, ...chain.puts].map((contract) => settlementStampTime(contract.expiration)).find(Boolean);
223
+ if (settles) parts.push(`expiries settle ${settles}`);
224
+ const asOf = chain.asOf ? Date.parse(chain.asOf) : Number.NaN;
225
+ parts.push(Number.isFinite(asOf) ? `OI live, ${formatUtcTime(new Date(asOf).toISOString())}` : openInterestAsOfLabel(basis.openInterest));
226
+ return parts.join(" · ");
227
+ }
196
228
  if (basis.spot) {
197
229
  parts.push(`Greeks at spot ${formatModelSpotPrice(basis.spot.price)} (${modelSpotLabel(basis.spot)}), `
198
230
  // An unknown yield is valued as none, and says so rather than read as a stated 0%.
@@ -236,10 +268,34 @@ export function optionRows(chain: OptionsChain) {
236
268
  iv: modeled.iv ?? null,
237
269
  delta: modeled.delta ?? null,
238
270
  expiration: expiryIsoDate(contract.expiration),
271
+ // A venue's marks and coin prices (Deribit), beside the dollar ones.
272
+ ...(contract.mark ? {
273
+ mark: contract.mark.price, gamma: contract.mark.gamma, vega: contract.mark.vega, theta: contract.mark.theta,
274
+ forward: contract.mark.forward, expiresAt: new Date(contract.expiration * 1000).toISOString(),
275
+ } : {}),
276
+ ...(contract.coin ? {
277
+ coinCurrency: contract.coin.currency, coinBid: contract.coin.bid, coinAsk: contract.coin.ask,
278
+ coinMark: contract.coin.mark, coinLast: contract.coin.last,
279
+ } : {}),
239
280
  };
240
281
  });
241
282
  }
242
283
 
284
+ /** A premium in a coin, to the venue's tick (0.0001 BTC); a dash for none. */
285
+ export function formatCoinPremiumCell(value: unknown): string {
286
+ return isFiniteNumber(value) && value > 0 ? value.toFixed(4) : "—";
287
+ }
288
+
289
+ /** A dollar greek or mark (vega, theta, mark) to cents; a dash for none. */
290
+ export function formatOptionDollarCell(value: unknown): string {
291
+ return isFiniteNumber(value) ? value.toFixed(2) : "—";
292
+ }
293
+
294
+ /** Gamma per 1.00 move, tiny for a coin worth 80,000 dollars: to six places, as the venue shows it to five. */
295
+ export function formatOptionGammaCell(value: unknown): string {
296
+ return isFiniteNumber(value) ? value.toFixed(6) : "—";
297
+ }
298
+
243
299
  export function formatOptionQuoteCell(row: Record<string, unknown>, side: "bid" | "ask"): string {
244
300
  const quote = optionQuoteSide({ bid: Number(row.bid), ask: Number(row.ask) }, side);
245
301
  return quote == null ? "—" : String(quote);
@@ -3,6 +3,8 @@ import { apiClient } from "../../api-client";
3
3
  import { ApiRequestError } from "../../api-client/errors";
4
4
  import type { CliCommandDef } from "../../types/plugin";
5
5
  import { withCliServices, withMarketData } from "../context";
6
+ import { CLI_ERROR_CODE, failWithFields } from "../errors";
7
+ import type { CliResultColumn } from "../result";
6
8
  import { formatCompact } from "../../utils/format";
7
9
  import { DATE_OPTION_EXAMPLES, DATE_OPTION_FORMATS, dateOptionError, readCalendarDate } from "../../utils/calendar-date";
8
10
  import {
@@ -40,6 +42,7 @@ import {
40
42
  type ImpactFilter,
41
43
  } from "../../plugins/builtin/econ/calendar-model";
42
44
  import { hasNoReleaseTime, hasNoStatedTime } from "../../plugins/builtin/econ/types";
45
+ import { econRowFigures } from "../../plugins/builtin/econ/figures";
43
46
  import { isoDate, rejectExtraArgs, requireArg, requireOneArg, takeFlag, takeOption } from "./command-utils";
44
47
  import { buildCorrelationSeries } from "../../plugins/builtin/correlation/matrix/model";
45
48
  import { alignDailyCloses, correlateDailyCloses } from "../../plugins/builtin/correlation/compute";
@@ -70,6 +73,7 @@ import {
70
73
  fredExportValue,
71
74
  fredHistoryNotes,
72
75
  fredInterval,
76
+ fredObservesDays,
73
77
  fredObservationsFromStart,
74
78
  fredProvenance,
75
79
  fredPublicationTime,
@@ -299,6 +303,54 @@ function publisherClock(localTime: unknown, timeZone: unknown): string {
299
303
  }
300
304
  const DAY_MS = 86_400_000;
301
305
 
306
+ /** The calendar's columns up to the event: shared by the text table and the exports, which keep the raw values under them. */
307
+ function econHeadColumns(viewerZone: string | null): CliResultColumn[] {
308
+ return [
309
+ // Text shows both halves of the event timestamp in UTC, never the host's zone; exports keep the source values.
310
+ { key: "date", header: "Date", format: (value) => utcDateTimePart(value, "date") },
311
+ // The weekday sits before the clock it is on: the UTC time's, or with --tz the viewer's.
312
+ ...viewerZone ? [] : [ECON_DAY_COLUMN],
313
+ // With --tz the viewer's clock has its own column, so the UTC time does not repeat it.
314
+ { key: "time", header: "Time", format: (value, row) => value === "All Day" ? "All day" : value === ""
315
+ ? "" : utcDateTimePart(row.date, "time", !viewerZone) },
316
+ ...viewerZone ? [ECON_DAY_COLUMN] : [],
317
+ // Only --tz fills it: the viewer's clock, its weekday in Day before it.
318
+ { key: "viewerTime", header: viewerZone ? timeZoneCity(viewerZone) : "Viewer", shrink: false,
319
+ format: (value) => typeof value === "string" && viewerZone ? value : "" },
320
+ // Only rows a publisher times itself fill it, so the feeds' calendar prints as before.
321
+ { key: "localTime", header: "Local", optional: true, dropPriority: 1, format: (value, row) => publisherClock(value, row.timeZone) },
322
+ { key: "country", header: "Country" },
323
+ { key: "impact", header: "Impact" },
324
+ { key: "event", header: "Event" },
325
+ ];
326
+ }
327
+
328
+ /**
329
+ * Actual, Forecast and Prior: as the text draws them (`30.3K`, `-4.1M bbl`) in the table, as
330
+ * numbers in the exports (30300, -4100000), where Unit and Scale say what they count and how they were drawn.
331
+ */
332
+ function econFigureColumns(text: boolean): CliResultColumn[] {
333
+ return text
334
+ ? [{ key: "actualText", header: "Actual" }, { key: "forecastText", header: "Forecast" }, { key: "priorText", header: "Prior" }]
335
+ : [{ key: "actual", header: "Actual" }, { key: "forecast", header: "Forecast" }, { key: "prior", header: "Prior" }];
336
+ }
337
+
338
+ const ECON_PUBLISHER_COLUMN: CliResultColumn = {
339
+ key: "publisher", header: "Publisher", format: (value) => typeof value === "string" ? value : "",
340
+ };
341
+
342
+ /** What only the exports say about a row: how to read its figures, who publishes it and where, and the figures as drawn. */
343
+ const econExportColumns: CliResultColumn[] = [
344
+ { key: "unit", header: "Unit" },
345
+ { key: "scale", header: "Scale" },
346
+ { key: "publisher", header: "Publisher" },
347
+ { key: "sourceUrl", header: "Link" },
348
+ // The figures as the calendar draws them, so a bound (`<1.25%`) that is no number keeps its text.
349
+ { key: "actualText", header: "actual_text" },
350
+ { key: "forecastText", header: "forecast_text" },
351
+ { key: "priorText", header: "prior_text" },
352
+ ];
353
+
302
354
  /**
303
355
  * What an empty calendar says. A window that starts after the last day listed
304
356
  * says where the listing stops, whatever the filters, rather than nothing.
@@ -357,6 +409,22 @@ function blankTimeNotes(rows: ReadonlyArray<{ event: string; country: string; ti
357
409
  return [...byNote].map(([note, events]) => `No time for ${events.join(", ")}: ${note.charAt(0).toLowerCase()}${note.slice(1)}.`);
358
410
  }
359
411
 
412
+ /**
413
+ * A `--from` the stored calendar does not reach back to, which Gloom Cloud refuses
414
+ * with "The calendar reaches back 400 days": an input error naming the earliest
415
+ * day it takes, and `--json` carries it as `earliest`. Any other failure goes on as it was.
416
+ */
417
+ function failBeforeCalendar(error: unknown, from: string, todayStart: number): never {
418
+ const days = error instanceof ApiRequestError && error.status === 422 ? /reaches back (\d+) days?/i.exec(error.message)?.[1] : undefined;
419
+ if (!days) throw error;
420
+ const earliest = isoDate(new Date(todayStart - Number(days) * DAY_MS)).slice(0, 10);
421
+ return failWithFields(
422
+ `--from ${from} is before the earliest day the calendar reaches back to, ${earliest}. Use --from ${earliest} or later.`,
423
+ CLI_ERROR_CODE,
424
+ { earliest, days: Number(days) },
425
+ );
426
+ }
427
+
360
428
  async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
361
429
  const rawArgs = [...args];
362
430
  const country = parseCountry(takeOption(rawArgs, "--country"), ctx);
@@ -372,7 +440,9 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
372
440
  const todayStart = Math.floor(Date.now() / DAY_MS) * DAY_MS;
373
441
  // An earlier day reads the stored calendar, which keeps past events after the week's listing drops them.
374
442
  // A server without it, a server error or no network lists the current calendar (cached when offline) instead.
375
- const stored = fromOption != null && fromOption < isoDate(new Date(todayStart)) ? await fetchEconCalendarFrom(fromOption) : null;
443
+ const stored = fromOption != null && fromOption < isoDate(new Date(todayStart))
444
+ ? await fetchEconCalendarFrom(fromOption).catch((error: unknown) => failBeforeCalendar(error, fromOption, todayStart))
445
+ : null;
376
446
  const past = stored?.events ? stored : null;
377
447
  const unread = stored && !stored.events && stored.failure ? `The stored calendar could not be read (${stored.failure}); listing the current calendar` : null;
378
448
  const { data: events, fetchedAt, stale } = past ? { data: past.events, fetchedAt: Date.now(), stale: false } : await loadCalendar(ctx.cliOptions.refresh);
@@ -400,6 +470,8 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
400
470
  .map((event) => {
401
471
  // The viewer's clock (--tz) for a row with a time: its date and time there.
402
472
  const clock = viewerZone && !hasNoReleaseTime(event) ? zonedClock(event.date.getTime(), viewerZone) : null;
473
+ // The figures as numbers, scale multiplied out, with their unit and the scale they are drawn in; the text stays as drawn.
474
+ const figures = econRowFigures(event);
403
475
  return {
404
476
  date: isoDate(event.date),
405
477
  // The weekday on the viewer's clock (--tz), else the UTC day's; a row with no time is its day's.
@@ -409,9 +481,14 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
409
481
  country: eventCountry(event),
410
482
  impact: event.impact,
411
483
  event: event.event,
412
- actual: event.actual ?? "",
413
- forecast: event.forecast ?? "",
414
- prior: event.prior ?? "",
484
+ actual: figures.actual,
485
+ forecast: figures.forecast,
486
+ prior: figures.prior,
487
+ unit: figures.unit,
488
+ scale: figures.scale,
489
+ actualText: event.actual ?? null,
490
+ forecastText: event.forecast ?? null,
491
+ priorText: event.prior ?? null,
415
492
  // The publisher's own clock, for rows Gloom Cloud adds from a bank's or a statistics office's calendar.
416
493
  localTime: event.localTime ?? null,
417
494
  timeZone: event.timeZone ?? null,
@@ -420,15 +497,19 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
420
497
  // Who publishes the figure (Freddie Mac for the 30-year mortgage rate), the period it covers and its page.
421
498
  publisher: event.source?.name ?? null,
422
499
  referencePeriod: event.referencePeriod ?? null,
423
- source: event.source?.name ?? null,
500
+ // When the figure came out: the row's time once it has an actual (its day alone when it states no time), null before.
501
+ released: event.actual?.trim() ? (hasNoReleaseTime(event) ? isoDate(event.date).slice(0, 10) : isoDate(event.date)) : null,
424
502
  sourceUrl: event.source?.url ?? null,
503
+ // The same indicator from one release to the next; empty until Gloom Cloud names it.
504
+ indicatorId: event.indicatorId ?? null,
425
505
  };
426
506
  });
427
507
  // A filter of countries only, none of which has anything in the window, says so as its empty list.
428
508
  const allUnscheduled = unscheduled.length > 0 && unscheduled.length === country.split(",").length;
429
- const footnote = [listedBefore, listedAfter, ...(rows.length > 0 ? blankTimeNotes(rows) : []),
509
+ const notes = [listedBefore, listedAfter, ...(rows.length > 0 ? blankTimeNotes(rows) : []),
430
510
  rows.length > 0 && unscheduled.length > 0 ? noScheduledReleasesMessage(unscheduled) : null]
431
- .filter((note): note is string => !!note).join("\n");
511
+ .filter((note): note is string => !!note);
512
+ const footnote = notes.join("\n");
432
513
  ctx.printResult({
433
514
  data: rows,
434
515
  ...(unread ? { warnings: [unread] } : {}),
@@ -440,28 +521,21 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
440
521
  defaultLimit: 50,
441
522
  empty: allUnscheduled ? noScheduledReleasesMessage(unscheduled) : econEmptyMessage(from, listedThrough, listedAfter != null),
442
523
  ...(footnote ? { footnote } : {}),
443
- columns: [
444
- // Text shows both halves of the event timestamp in UTC, never the host's zone; exports keep the source values.
445
- { key: "date", header: "Date", format: (value) => utcDateTimePart(value, "date") },
446
- // The weekday sits before the clock it is on: the UTC time's, or with --tz the viewer's.
447
- ...viewerZone ? [] : [ECON_DAY_COLUMN],
448
- // With --tz the viewer's clock has its own column, so the UTC time does not repeat it.
449
- { key: "time", header: "Time", format: (value, row) => value === "All Day" ? "All day" : value === ""
450
- ? "" : utcDateTimePart(row.date, "time", !viewerZone) },
451
- ...viewerZone ? [ECON_DAY_COLUMN] : [],
452
- // Only --tz fills it: the viewer's clock, its weekday in Day before it.
453
- { key: "viewerTime", header: viewerZone ? timeZoneCity(viewerZone) : "Viewer", shrink: false,
454
- format: (value) => typeof value === "string" && viewerZone ? value : "" },
455
- // Only rows a publisher times itself fill it, so the feeds' calendar prints as before.
456
- { key: "localTime", header: "Local", optional: true, dropPriority: 1, format: (value, row) => publisherClock(value, row.timeZone) },
457
- { key: "country", header: "Country" },
458
- { key: "impact", header: "Impact" },
459
- { key: "event", header: "Event" },
460
- { key: "actual", header: "Actual" },
461
- { key: "forecast", header: "Forecast" },
462
- { key: "prior", header: "Prior" },
463
- { key: "publisher", header: "Publisher", format: (value) => typeof value === "string" ? value : "" },
464
- ],
524
+ // What the text says under the table, a script reads in the CSV's closing lines.
525
+ exportNotes: notes,
526
+ // `as_of` is the release: when a row's figure came out, blank for one that has none yet.
527
+ exportDated: true,
528
+ exportIdentity: (row) => ({
529
+ symbolOrSeries: typeof row.indicatorId === "string" ? row.indicatorId : null,
530
+ // A percent is `pct` here as in `fred` and `yield-curve`; `unit` keeps the `%` the calendar draws.
531
+ currencyOrUnit: row.unit === "%" ? "pct" : typeof row.unit === "string" ? row.unit : null,
532
+ asOf: typeof row.released === "string" ? row.released : null,
533
+ released: typeof row.released === "string" ? row.released : null,
534
+ period: typeof row.referencePeriod === "string" ? row.referencePeriod : null,
535
+ }),
536
+ // The exports add what a script needs to read a row; the text table keeps the columns it had.
537
+ columns: [...econHeadColumns(viewerZone), ...econFigureColumns(false), ...econExportColumns],
538
+ textColumns: [...econHeadColumns(viewerZone), ...econFigureColumns(true), ECON_PUBLISHER_COLUMN],
465
539
  });
466
540
  });
467
541
  }
@@ -545,12 +619,16 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
545
619
  if (mismatch) ctx.fail(mismatch);
546
620
  // Each row's change in the unit its value exports in, and the date it is from; a holiday's stays blank.
547
621
  const changes = horizon ? fredChanges(seriesId, data.observations, horizon) : null;
622
+ const releasedOf = (observation: { publisher?: string; publishedAt?: string; releasedOn?: string }) =>
623
+ observation.releasedOn ?? (observation.publisher?.trim() ? observation.publishedAt : undefined);
548
624
  // A value FRED does not list yet carries its publisher and when it published, in every export.
549
- const exportRow = (observation: { date: string; value: number | null; publisher?: string; publishedAt?: string }) => ({
625
+ const exportRow = (observation: { date: string; value: number | null; publisher?: string; publishedAt?: string; releasedOn?: string }) => ({
550
626
  date: observation.date,
551
627
  value: fredExportValue(seriesId, observation.value),
552
628
  ...(changes ? { change: changes.get(observation.date)?.change ?? null, changeFrom: changes.get(observation.date)?.changeFrom ?? null } : {}),
553
629
  ...(observation.publisher?.trim() ? { publisher: observation.publisher.trim(), ...(observation.publishedAt ? { publishedAt: observation.publishedAt } : {}) } : {}),
630
+ // When the figure came out, where the data says: only a series' newest observation does. `date` is the period it covers.
631
+ ...releasedOf(observation) ? { released: releasedOf(observation) } : {},
554
632
  });
555
633
  // A --start before the series begins comes back padded with blank days: the rows start at its first observation.
556
634
  const rows = fredObservationsFromStart(data.observations, data.info?.observationStart)
@@ -570,6 +648,7 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
570
648
  ctx.fail(`No ${seriesId} observations on or after ${startDate}.`, fredEmptyAdvice(seriesId, latest));
571
649
  }
572
650
  const periodicity = periodicityOf(data.info?.frequency);
651
+ const observesDays = fredObservesDays(data.info?.frequency);
573
652
  const newestDate = rows.reduce<string | null>((newest, row) => (!newest || row.date > newest ? row.date : newest), null);
574
653
  // A newest value FRED does not list yet names its publisher, Treasury for a yield; FRED's own stay "FRED".
575
654
  const ahead = fredProvenance(data.observations.filter((observation) => observation.date === newestDate), seriesId);
@@ -598,7 +677,16 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
598
677
  dateKey: "date",
599
678
  empty: fredEmptyMessage(startDate, latest == null ? null : exportRow(latest)),
600
679
  ...(notes.length ? { footnote: notes.join("\n"), exportNotes: notes } : {}),
601
- exportIdentity: { symbolOrSeries: seriesId, currencyOrUnit: unit, interval },
680
+ // `as_of` is the release. A figure with none stated is as of its own day when the series is known to observe
681
+ // days (a daily or weekly rate), and of nothing otherwise, never the first day of the period it covers.
682
+ exportDated: true,
683
+ exportIdentity: (row) => {
684
+ const released = typeof row.released === "string" ? row.released : null;
685
+ const date = typeof row.date === "string" ? row.date : null;
686
+ return { symbolOrSeries: seriesId, currencyOrUnit: unit, interval, asOf: released ?? (observesDays ? date : null), period: date, released };
687
+ },
688
+ // The release closes the row with the other dating columns, so it is not one of the observation's own.
689
+ columns: [...new Set(rows.flatMap((row) => Object.keys(row)))].filter((key) => key !== "released").map((key) => ({ key, header: key })),
602
690
  textColumns: [
603
691
  { key: "date", header: "Date" },
604
692
  isSpreadSeries(seriesId)
@@ -25,6 +25,7 @@ import { formatMarketPriceWithCurrency, quoteFormatOptions } from "../../market-
25
25
  import { formerIssuerLine, startTickerChangeLookup, type TickerChangeLookup } from "../../tickers/ticker-changes";
26
26
  import { fail, failRetryable } from "../errors";
27
27
  import { isProviderBusy, SEARCH_BUSY_CODE, SEARCH_BUSY_MESSAGE } from "../../sources/provider-errors";
28
+ import { isNetworkFailure } from "../../utils/network-failure";
28
29
  import type { MarketContext } from "../types";
29
30
  import type { CliCommandContext, GloomPlugin } from "../../types/plugin";
30
31
  import { indexRootTryLine } from "../index-roots";
@@ -137,6 +138,8 @@ export async function searchCandidatesForCli({
137
138
  } catch (error) {
138
139
  // Not "No matches found": a script or a reader would take that for a name nothing lists.
139
140
  if (isProviderBusy(error)) failRetryable(SEARCH_BUSY_MESSAGE, SEARCH_BUSY_CODE);
141
+ // Nor is no answer at all: the command fails as "couldn't reach", as any command offline does.
142
+ if (isNetworkFailure(error)) throw error;
140
143
  logFailure("cli.commands-search", "Search ticker candidates failed")(error);
141
144
  return [];
142
145
  }
@@ -204,9 +207,19 @@ function formerIssuerNote(candidate: TickerSearchCandidate): string | null {
204
207
  return formerIssuerLine(former.symbol, former, former.successor ?? { symbol: candidate.label, name: resolveSearchName(candidate) });
205
208
  }
206
209
 
207
- /** The merged companies' lines, once each, unless the symbol's own line already says it. */
210
+ /**
211
+ * A row search found by the name its company had before it was renamed
212
+ * ("cassava" finds Filana Therapeutics' FLNA): says what it was called.
213
+ */
214
+ function formerNameNote(candidate: TickerSearchCandidate): string | null {
215
+ const formerName = candidate.result?.formerName;
216
+ if (typeof formerName !== "string" || !formerName.trim()) return null;
217
+ return `${candidate.label} is ${resolveSearchName(candidate)}, formerly ${formerName.trim()}`;
218
+ }
219
+
220
+ /** The merged companies' and renamed companies' lines, once each, unless the symbol's own line already says it. */
208
221
  function formerIssuerNotes(candidates: readonly TickerSearchCandidate[], fate: SearchFate): string[] {
209
- const notes = new Set(candidates.flatMap((candidate) => formerIssuerNote(candidate) ?? []));
222
+ const notes = new Set(candidates.flatMap((candidate) => formerIssuerNote(candidate) ?? formerNameNote(candidate) ?? []));
210
223
  return [...notes].filter((note) => !fate.notes.some((line) => line.split(" ")[0] === note.split(" ")[0]));
211
224
  }
212
225
 
@@ -366,6 +379,9 @@ function searchCandidateRows(candidates: TickerSearchCandidate[]) {
366
379
  saved: candidate.kind === "ticker",
367
380
  // The company that merged into this one, whose old name the query is.
368
381
  ...candidate.result?.formerIssuer ? { formerIssuer: candidate.result.formerIssuer } : {},
382
+ // The name a renamed company had, which the query is.
383
+ ...typeof candidate.result?.formerName === "string" && candidate.result.formerName.trim()
384
+ ? { formerName: candidate.result.formerName.trim() } : {},
369
385
  }));
370
386
  }
371
387