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
@@ -1494,7 +1494,8 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
1494
1494
 
1495
1495
  let financials: TickerFinancials | null = null;
1496
1496
  try {
1497
- financials = await dataProvider.getTickerFinancials(requestSymbol, exchange);
1497
+ // The Change line says what the move is from.
1498
+ financials = await dataProvider.getTickerFinancials(requestSymbol, exchange, { datesChangeBasis: true });
1498
1499
  } catch (error) {
1499
1500
  // A known exchange the symbol is not listed on: say where it is.
1500
1501
  await failIfNotTraded(listing, { store, dataProvider }, { fail: failCommand });
package/src/cli/errors.ts CHANGED
@@ -9,9 +9,9 @@ import { DATA_SERVICE_UNREACHABLE_MESSAGE, isNetworkFailure } from "../utils/net
9
9
 
10
10
  const USAGE_PREFIX = "Usage: ";
11
11
  /** The code of every error a command fails with: bad input, nothing found, an option it does not take. */
12
- const CLI_ERROR_CODE = "cli_error";
12
+ export const CLI_ERROR_CODE = "cli_error";
13
13
  /** The code of a command whose requests never reached the data service. */
14
- const SERVICE_UNREACHABLE_CODE = "service_unreachable";
14
+ export const SERVICE_UNREACHABLE_CODE = "service_unreachable";
15
15
 
16
16
  /** What to check when nothing answered: the connection, and the proxy when one is set. */
17
17
  function unreachableHint(): string {
@@ -3,7 +3,8 @@
3
3
  * writes CSV or JSON, so an export can go in an audit file as it is: CSV ends
4
4
  * with `#` lines and JSON carries a `source` object with the same fields.
5
5
  * CSV and NDJSON rows also end with five fixed columns naming what each row
6
- * is (`EXPORT_IDENTITY_COLUMNS`), less any the table already has by name.
6
+ * is (`EXPORT_IDENTITY_COLUMNS`), less any the table already has by name; a
7
+ * dated export adds `period`, `released` and `retrieved` after them.
7
8
  */
8
9
  import { formatFreshnessLine, type ReportFreshness } from "./pane-functions/freshness";
9
10
  import { parseReportTime } from "../utils/utc-time";
@@ -54,6 +55,14 @@ export function exportSource(freshness: ReportFreshness): ExportSource {
54
55
  */
55
56
  export const EXPORT_IDENTITY_COLUMNS = ["symbol_or_series", "currency_or_unit", "interval", "source", "as_of"] as const;
56
57
 
58
+ /**
59
+ * The columns a dated export (`econ`, `fred`, `fn ECST`) ends with after the
60
+ * five: the period a figure covers, when it was released, and when this export
61
+ * retrieved it. `as_of` is then the release, and these three say the rest, so a
62
+ * row never has to be read against the closing lines to be dated.
63
+ */
64
+ export const EXPORT_DATING_COLUMNS = ["period", "released", "retrieved"] as const;
65
+
57
66
  /** What a command knows about the rows it exports; source and as-of come from its freshness. */
58
67
  export interface ExportIdentity {
59
68
  /** The symbol or series ID asked for: `USDZAR=X`, `SAN:EPA`, `DEXSFUS`. */
@@ -68,6 +77,10 @@ export interface ExportIdentity {
68
77
  * blank; absent, it is the closing lines' observation date.
69
78
  */
70
79
  asOf?: string | null;
80
+ /** In a dated export: the period the figure covers, as its publisher names it (`Sep 2026`, `2026-09-01`). */
81
+ period?: string | null;
82
+ /** In a dated export: when the figure was released, an ISO date or instant; null when the data does not say. */
83
+ released?: string | null;
71
84
  }
72
85
 
73
86
  /** A row's own as-of in the closing lines' form: a date stays a date, an instant is ISO in UTC. */
@@ -94,6 +107,21 @@ export function exportIdentityCells(freshness: ReportFreshness, identity: Export
94
107
  ];
95
108
  }
96
109
 
110
+ /**
111
+ * The cells of `EXPORT_DATING_COLUMNS` for a row: its period and release as
112
+ * the data states them, blank when it does not, and the time this export
113
+ * retrieved the data.
114
+ */
115
+ export function exportDatingCells(freshness: ReportFreshness, identity: ExportIdentity = {}): string[] {
116
+ return [identity.period?.trim() ?? "", rowAsOf(identity.released ?? null), exportSource(freshness).retrievalTime];
117
+ }
118
+
119
+ /** The three dating fields of an NDJSON row, null where blank. */
120
+ export function exportDatingFields(freshness: ReportFreshness, identity: ExportIdentity = {}): Record<string, string | null> {
121
+ const cells = exportDatingCells(freshness, identity);
122
+ return Object.fromEntries(EXPORT_DATING_COLUMNS.map((column, index) => [column, cells[index] || null]));
123
+ }
124
+
97
125
  /** The same five as fields of an NDJSON row, null where blank. */
98
126
  export function exportIdentityFields(freshness: ReportFreshness, identity: ExportIdentity = {}): Record<string, string | null> {
99
127
  const cells = exportIdentityCells(freshness, identity);
@@ -778,6 +778,42 @@ export const GLOSSARY: readonly GlossaryEntry[] = [
778
778
  definition: "Cboe's index of the 30-day volatility that VIX options price: the VIX of the VIX. A high reading means traders are paying up "
779
779
  + "to protect against sudden jumps in VIX itself.",
780
780
  },
781
+ // Options positioning (OPX, GEX) and the volatility surface (OVDV).
782
+ {
783
+ term: "Max pain", aliases: ["max-pain", "maximum pain", "max pain strike", "pain strike"], functions: ["OPX", "GEX"],
784
+ definition: "The settlement price at which one expiry's options would pay their holders the least, summed over every call's and put's open interest: "
785
+ + "the strike where Payout $ is lowest. It says where option holders collect least at expiry, not where the price will go. "
786
+ + "Deribit's BTC and ETH options settle at 08:00 UTC on the day named, against its index averaged over the 30 minutes before.",
787
+ },
788
+ {
789
+ term: "Payout $", aliases: ["payout", "expiry payout", "option payout", "payout at expiry"], functions: ["OPX", "GEX"], shown: ["Payout $"],
790
+ definition: "What an expiry's options would pay their holders, in dollars, if the underlying settled at that strike: each call's open interest times "
791
+ + "how far the strike is above the call's, plus each put's times how far it is below, times the contract size (100 shares for a US listing, "
792
+ + "1 BTC or 1 ETH on Deribit). Max pain is the strike where it is lowest.",
793
+ },
794
+ {
795
+ term: "Call wall", aliases: ["put wall", "walls", "gamma wall", "gamma walls"], functions: ["OPX", "GEX"], shown: ["Call wall", "Put wall"],
796
+ definition: "The strike with the most dealer gamma from calls (call wall) or from puts (put wall), across every expiry or within one. "
797
+ + "Hedging flows are largest there, so it is often read as resistance above spot and support below it.",
798
+ },
799
+ {
800
+ term: "Risk reversal", short: "RR", aliases: ["25d risk reversal", "25-delta risk reversal", "25 delta risk reversal", "rr25", "risk reversals"],
801
+ functions: ["OVDV"], shown: ["RR"],
802
+ definition: "The 25-delta call's implied volatility minus the 25-delta put's, in vol points, for one expiry. Negative means puts cost more than calls "
803
+ + "as far from the forward (downside protection is bid), positive that upside calls are. OVDV signs it call minus put.",
804
+ },
805
+ {
806
+ term: "Butterfly", short: "BF", aliases: ["25d butterfly", "25-delta butterfly", "25 delta butterfly", "bf25", "smile curvature", "fly"],
807
+ functions: ["OVDV"], shown: ["BF"],
808
+ definition: "The average of the 25-delta call and put implied volatilities minus the implied volatility at the forward, in vol points, for one expiry: "
809
+ + "how much the wings cost over the middle of the smile. A higher butterfly prices larger moves in either direction.",
810
+ },
811
+ {
812
+ term: "DVOL", aliases: ["deribit volatility index", "bitcoin volatility index", "ether volatility index", "btc dvol", "eth dvol", "crypto vix"],
813
+ functions: ["OVDV", "G"], shown: ["DVOL"],
814
+ definition: "Deribit's volatility index for BTC or ETH: the 30-day implied volatility its options price, in vol points, computed from the whole "
815
+ + "strip of strikes as VIX is. OVDV shows it beside the coin's surface; chart it as CAP:volatility:dvol-btc or CAP:volatility:dvol-eth.",
816
+ },
781
817
  // The economic calendar (ECO).
782
818
  {
783
819
  term: "Actual", aliases: ["actual release", "actual value"], functions: ["ECO"], shown: ["ACTUAL", "Actual"],
@@ -795,6 +831,19 @@ export const GLOSSARY: readonly GlossaryEntry[] = [
795
831
  term: "Impact", short: "IMP", aliases: ["importance", "event impact", "high impact"], functions: ["ECO"],
796
832
  definition: "How much a release usually moves markets: HIGH for the big ones (CPI, jobs, rate decisions), MED and LOW for speeches and smaller data.",
797
833
  },
834
+ {
835
+ term: "Purchasing managers' index", short: "PMI", aliases: ["pmis", "purchasing managers", "purchasing managers index", "ism", "ism pmi",
836
+ "flash pmi", "caixin pmi", "ratingdog pmi"], functions: ["ECO"],
837
+ definition: "A monthly survey of companies' purchasing managers on orders, output, jobs and prices: 50 is no change from the month before, "
838
+ + "above it growth, below it contraction. A flash PMI is the month's early read, the final a week or so later. ECO lists each as a release: "
839
+ + "ISM for the US, S&P Global's elsewhere, NBS and RatingDog (formerly Caixin) for China, most at medium or low impact.",
840
+ },
841
+ // Economic statistics (ECST).
842
+ {
843
+ term: "Nowcast", aliases: ["nowcasts", "nowcasting", "gdpnow", "gdp nowcast"], functions: ["ECST", "ECO"], shown: ["GDPNow"],
844
+ definition: "An estimate of this quarter's GDP growth from the data already out, revised as each release lands until the official figure. "
845
+ + "ECST shows the Atlanta Fed's GDPNow, annualized, against its own history; ECO lists each update as a release.",
846
+ },
798
847
  ];
799
848
 
800
849
  const ANSI = /\x1b\[[0-9;]*m/g;
@@ -19,6 +19,8 @@ import { formatUtcTime } from "../utils/utc-time";
19
19
  import { historyCoverageNotice } from "../sources/history-coverage";
20
20
  import { barLastSession, barPeriodToDate, type BarPeriodToDate } from "../market-data/market/bar-period";
21
21
  import { barSizeFromGap, medianBarGap } from "../time-series/bar-spacing";
22
+ import { isRegularSessionTime, sessionCalendarTimeZone } from "../market-data/market/freshness";
23
+ import { zonedDateKey } from "../utils/zoned-date-time";
22
24
 
23
25
  const PRICE_KEYS = ["open", "high", "low", "close"] as const;
24
26
  const DAY_MS = 24 * 60 * 60 * 1000;
@@ -332,18 +334,35 @@ function listDates(dates: readonly string[]): string {
332
334
  return items.length > 1 ? `${items.slice(0, -1).join(", ")} and ${items.at(-1)}` : items[0] ?? "";
333
335
  }
334
336
 
337
+ /** The venue's day in session at `now`: null when it is not trading, undefined when its hours are not known. */
338
+ export function venueTradingDay(venue: string | undefined, now: number): string | null | undefined {
339
+ const zone = venue ? sessionCalendarTimeZone(venue) : null;
340
+ const trading = zone ? isRegularSessionTime(venue, now) : null;
341
+ if (!zone || trading === null) return undefined;
342
+ return trading ? zonedDateKey(now, zone) : null;
343
+ }
344
+
335
345
  /**
336
346
  * What the partial, filled and unsettled bars are: "2026-10-12 is still
337
- * forming (partial), with no volume until the day closes", the bars rebuilt
338
- * from hourly bars, a future's closed session that "closes on its last trade
339
- * until its settlement is confirmed (unsettled)", and the latest trade an
340
- * intraday history ends with when it has no volume (latestTradeIndex). Null
341
- * when no bar is any of them.
347
+ * forming (partial), with no volume until the day closes", a day whose
348
+ * session has closed that "closes on the session's last price until the
349
+ * day's bar is final (partial)", the bars rebuilt from hourly bars, a
350
+ * future's closed session that "closes on its last trade until its
351
+ * settlement is confirmed (unsettled)", and the latest trade an intraday
352
+ * history ends with when it has no volume (latestTradeIndex). Null when no
353
+ * bar is any of them. `tradingDay`: the venue's day in session now, null
354
+ * when none is; left out when the venue's hours are not known, every
355
+ * partial bar is then still forming.
342
356
  */
343
357
  export function historyBarStatusNote(
344
- rows: readonly Pick<HistoryRow, "date" | "partial" | "filled" | "unsettled" | "volume">[],
358
+ rows: readonly Pick<HistoryRow, "date" | "partial" | "filled" | "unsettled" | "volume" | "interval">[],
359
+ options: { tradingDay?: string | null } = {},
345
360
  ): string | null {
346
- const partial = rows.filter((row) => row.partial);
361
+ // A day's bar the data service still marks partial once its session has closed: its own daily bar is not in yet.
362
+ const closed = (row: (typeof rows)[number]) =>
363
+ options.tradingDay !== undefined && row.interval === "1d" && row.date.slice(0, 10) !== options.tradingDay;
364
+ const partial = rows.filter((row) => row.partial && !closed(row));
365
+ const settling = rows.filter((row) => row.partial && closed(row));
347
366
  const latest = rows[latestTradeIndex(rows.map((row) => Date.parse(row.date)))];
348
367
  const filled = rows.filter((row) => row.filled);
349
368
  const unsettled = rows.filter((row) => row.unsettled);
@@ -352,6 +371,10 @@ export function historyBarStatusNote(
352
371
  ? `${listDates(partial.map((row) => row.date))} ${partial.length === 1 ? "is" : "are"} still forming (partial)`
353
372
  + (partial.every((row) => row.volume == null) ? ", with no volume until the day closes" : "")
354
373
  : null,
374
+ settling.length > 0
375
+ ? `${listDates(settling.map((row) => row.date))} ${settling.length === 1 ? "closes" : "close"} on the session's last price`
376
+ + ` until the day's bar is final (partial)` + (settling.every((row) => row.volume == null) ? ", with no volume until then" : "")
377
+ : null,
355
378
  filled.length > 0
356
379
  ? `${listDates(filled.map((row) => row.date))} ${filled.length === 1 ? "was" : "were"} rebuilt from hourly bars (filled)`
357
380
  + (filled.every((row) => row.volume == null) ? ", without volume" : "")
@@ -1,5 +1,6 @@
1
1
  import { listingSuffixExchange } from "../sources/listing-symbols";
2
2
  import { kindFromListingSyntax } from "../tickers/instrument-kind";
3
+ import { macroReleaseTopic } from "../plugins/builtin/shared/macro-releases";
3
4
  import { parsePublicTickerKey } from "../utils/exchanges";
4
5
  import { listingAbroadVenue } from "../utils/sec";
5
6
  import { nimCommand } from "./bank-section";
@@ -387,7 +388,8 @@ export function knownGapForTicker(symbol: string): string | null {
387
388
 
388
389
  /**
389
390
  * The line for a query that names a known subject as a whole ("carbon allowances", "EUA"), a Treasury yield
390
- * typed as a ticker (US10Y), or a topic with no function ("defense budget"), else null.
391
+ * typed as a ticker (US10Y), a topic with no function ("defense budget"), or a macro release no series carries
392
+ * ("ISM manufacturing", "GDPNow"), else null.
391
393
  */
392
394
  export function knownGapMessage(query: string): string | null {
393
395
  const key = lookupKey(query);
@@ -395,5 +397,6 @@ export function knownGapMessage(query: string): string | null {
395
397
  const tenor = treasuryYieldTenor(query);
396
398
  return KNOWN_GAPS.find((gap) => gap.terms.some((term) => lookupKey(term) === key))?.message
397
399
  ?? (tenor ? treasuryYieldLine(tenor) : null)
398
- ?? topicPointer(query)?.message ?? null;
400
+ ?? topicPointer(query)?.message
401
+ ?? macroReleaseTopic(query)?.line ?? null;
399
402
  }
@@ -9,6 +9,7 @@ import {
9
9
  quotedOnVenue,
10
10
  readAmbiguousCurrencyPair,
11
11
  readSharedCode,
12
+ savedRowOfBareSymbol,
12
13
  type ListingDeps,
13
14
  type ListingStore,
14
15
  type ListingVenue,
@@ -55,6 +56,7 @@ import {
55
56
  type TerminalListingCode,
56
57
  } from "../tickers/terminal-codes";
57
58
  import { listingAbroadVenue } from "../utils/sec";
59
+ import { savedAbroadForBareSymbol, savedUsListingOf } from "../tickers/search/bare-symbol";
58
60
  import { listingSuffixCandidate, listingSuffixVenue, tickerHasListingSuffix } from "../sources/listing-symbols";
59
61
  import { notTheCoinNote } from "./commands/crypto-hints";
60
62
 
@@ -621,15 +623,18 @@ async function quoteListings(
621
623
  dataProvider: DataProvider,
622
624
  listings: readonly CliListing[],
623
625
  refresh: boolean,
626
+ datesChangeBasis = false,
624
627
  ): Promise<Array<Quote | null>> {
625
628
  if (listings.length === 0) return [];
626
629
  const targets = listings.map((listing) => listing.request);
627
630
  if (dataProvider.getQuotesBatch) {
628
- const results = await dataProvider.getQuotesBatch(targets, { forceRefresh: refresh }).catch(logFailure("cli.listing-arg", "Read listing quote batch failed", []));
631
+ const results = await dataProvider.getQuotesBatch(targets, { forceRefresh: refresh, ...(datesChangeBasis ? { datesChangeBasis } : {}) })
632
+ .catch(logFailure("cli.listing-arg", "Read listing quote batch failed", []));
629
633
  return targets.map((target, index) => (results.find((result) => result.target === target) ?? results[index])?.quote ?? null);
630
634
  }
631
635
  return Promise.all(targets.map((target) => dataProvider.getQuote(target.symbol, target.exchange, {
632
636
  cacheMode: refresh ? "refresh" : "default",
637
+ ...(datesChangeBasis ? { datesChangeBasis } : {}),
633
638
  }).catch(logFailure("cli.listing-arg", "Read listing quote failed", null))));
634
639
  }
635
640
 
@@ -661,7 +666,7 @@ export async function quoteBareListing(
661
666
  listing: CliListing,
662
667
  deps: ListingDeps,
663
668
  request: ListingDataRequest,
664
- { refresh = false }: { refresh?: boolean } = {},
669
+ { refresh = false, datesChangeBasis = false }: { refresh?: boolean; datesChangeBasis?: boolean } = {},
665
670
  ): Promise<BareListingQuote | null> {
666
671
  const venues = await bareListingVenues(listing, deps);
667
672
  if (!venues) return null;
@@ -672,10 +677,10 @@ export async function quoteBareListing(
672
677
  const first = [namedListing(listing, venues.exchange)];
673
678
  const rest = venues.others.map((venue) => namedListing(listing, venue.exchange));
674
679
  let tried = first;
675
- let quotes = await quoteListings(deps.dataProvider, first, refresh);
680
+ let quotes = await quoteListings(deps.dataProvider, first, refresh, datesChangeBasis);
676
681
  if (!quotes[0] && rest.length > 0) {
677
682
  tried = [...first, ...rest];
678
- quotes = [...quotes, ...await quoteListings(deps.dataProvider, rest, refresh)];
683
+ quotes = [...quotes, ...await quoteListings(deps.dataProvider, rest, refresh, datesChangeBasis)];
679
684
  }
680
685
  const found = quotes.findIndex(Boolean);
681
686
  if (found < 0) {
@@ -948,6 +953,10 @@ export async function loadSavedListingOf(
948
953
  for (const listing of listings) {
949
954
  if (found.some((entry) => entry.listing.key === listing.key)) continue;
950
955
  const ticker = await loadSavedListing(store, listing);
956
+ // A bare symbol's US listing comes before a row abroad saved under its bare key (SHEL, saved from SHEL.L).
957
+ if (ticker && !listing.exchange && savedAbroadForBareSymbol(listing.symbol, ticker)) {
958
+ add(listing, savedUsListingOf(listing.symbol, await store.loadAllTickers()));
959
+ }
951
960
  add(listing, ticker);
952
961
  // A suffixed key as typed may be saved with a venue (7203.T:JPX, GARAN.IS:XNYS): any row of that symbol.
953
962
  if (!ticker && !listing.exchange && tickerHasListingSuffix(listing.symbol)) {
@@ -1115,7 +1124,9 @@ export async function resolveCliListing(
1115
1124
  parsed = market;
1116
1125
  }
1117
1126
  let listing = await settleSharedCode(parsed, deps);
1118
- const saved = await loadSavedListing(deps.store, listing) ?? (typedKey ? await loadSuffixedRow(deps.store, typedKey, listing.exchange) : null);
1127
+ const found = await loadSavedListing(deps.store, listing) ?? (typedKey ? await loadSuffixedRow(deps.store, typedKey, listing.exchange) : null);
1128
+ // A bare symbol is its US listing when it has one, whatever row abroad holds the bare key (SHEL saved from SHEL.L).
1129
+ const saved = listing.exchange ? found : await savedRowOfBareSymbol(listing.symbol, found, deps);
1119
1130
  // A code nothing could read stays the venue it is saved on.
1120
1131
  const savedExchange = canonicalExchange(saved?.metadata.exchange);
1121
1132
  if (listing.sharedCode && savedExchange) listing = { symbol: listing.symbol, exchange: savedExchange, key: `${listing.symbol}:${savedExchange}` };
@@ -100,8 +100,8 @@ export interface ReportFreshness {
100
100
  */
101
101
  sources?: ReportSourceAsOf[];
102
102
  /**
103
- * Whole minutes from the oldest of `sources` (or, declared `oldestAge`, the
104
- * oldest observation) to when this report was built: `oldest 9 min`.
103
+ * Whole minutes from the oldest of `sources` (or, declared `oldestAge` or
104
+ * `ageOnly`, the oldest observation) to when this report was built: `oldest 9 min`.
105
105
  */
106
106
  oldestAgeMinutes?: number;
107
107
  /** The data states no feed (`status` is `unreported`) and its line says only its time and age (`ageOnly`). */
@@ -125,16 +125,21 @@ export interface ReportFreshness {
125
125
  /**
126
126
  * Refreshing the data failed and a copy saved earlier answered: when that
127
127
  * copy was saved, an ISO instant in UTC. The status line then says
128
- * `couldn't refresh; saved 21:49 UTC` in place of how current the data is.
128
+ * `couldn't refresh; saved copy from 21:49 UTC` in place of how current the data is.
129
129
  */
130
130
  savedAt?: string;
131
+ /**
132
+ * Nothing was served: the data service could not be reached and no saved
133
+ * copy answered. The line then names no time, since nothing was retrieved.
134
+ */
135
+ unreached?: true;
131
136
  }
132
137
 
133
138
  /**
134
139
  * The freshness a command prints, saying when what it shows (`shown`: its
135
140
  * rows, or the data its line is drawn from) came from a copy saved earlier
136
141
  * because refreshing it failed (`collectSavedCopies`): its status line then
137
- * reads `couldn't refresh; saved 21:49 UTC` instead of live or delayed.
142
+ * reads `couldn't refresh; saved copy from 21:49 UTC` instead of live or delayed.
138
143
  */
139
144
  export function withSavedCopy<F extends ReportFreshness | undefined>(freshness: F, shown: unknown): F {
140
145
  if (!freshness || freshness.savedAt) return freshness;
@@ -463,9 +468,11 @@ function resolveFreshness({ declared, signals, observations, now }: FreshnessInp
463
468
  // A bar still forming covers its period through a later day than the one it is dated by.
464
469
  const periodToDate = readPeriodToDate(declared.periodToDate, now);
465
470
  const asOf = declaredAsOf ?? periodToDate?.through ?? newest([...observations.units, ...observations.extra]);
466
- // The oldest source, not the newest, says how far behind the view can be.
467
- const agedFrom = sources.length ? oldest(sources.map(({ time }) => time))
468
- : declared.oldestAge || declared.ageOnly ? oldest(observations.units) ?? asOf : null;
471
+ // The oldest source, not the newest, says how far behind the view can be. A line that is dated by its age says how
472
+ // old its oldest observation is, even beside each source's newest: a source can stream some rows and not others.
473
+ const oldestSource = oldest(sources.map(({ time }) => time));
474
+ const agedFrom = declared.ageOnly ? oldest(observations.units) ?? oldestSource ?? asOf
475
+ : oldestSource ?? (declared.oldestAge ? oldest(observations.units) ?? asOf : null);
469
476
  const oldestAgeMinutes = agedFrom && !agedFrom.dateOnly ? Math.max(0, Math.floor((now - agedFrom.time) / 60_000)) : null;
470
477
  // A loader that dates its data itself also says how old the oldest part is, or nothing;
471
478
  // `oldest: null` says the rows are a history, whose first entry is not stale data.
@@ -819,7 +826,7 @@ function onRetrievalDay(iso: string, retrievedAt: string): boolean {
819
826
 
820
827
  /**
821
828
  * What a report answered from a saved copy says in place of how current its
822
- * data is: `couldn't refresh; saved 21:49 UTC`, with the day when the copy was
829
+ * data is: `couldn't refresh; saved copy from 21:49 UTC`, with the day when the copy was
823
830
  * saved on another day than the report was built.
824
831
  */
825
832
  function savedCopyText(savedAt: string, retrievedAt: string, local: boolean): string {
@@ -827,7 +834,7 @@ function savedCopyText(savedAt: string, retrievedAt: string, local: boolean): st
827
834
  if (!Number.isFinite(at)) return "couldn't refresh";
828
835
  const iso = new Date(at).toISOString();
829
836
  const time = onRetrievalDay(iso, retrievedAt) ? `${iso.slice(11, 16)} UTC${local ? localTimeSuffix(at) : ""}` : timeText(iso, local);
830
- return `couldn't refresh; saved ${time}`;
837
+ return `couldn't refresh; saved copy from ${time}`;
831
838
  }
832
839
 
833
840
  /** `US and XETRA`, `US, XETRA and EPA`. */
@@ -913,10 +920,11 @@ function reopenText(market: ReportMarket, now: number, local: boolean): { text:
913
920
 
914
921
  /**
915
922
  * `markets closed until Mon`; `short` is the compact form a narrow capture
916
- * falls back to (`reopens Mon`). A time is always the regular session's open,
917
- * and says so (`pre-market open · regular session opens 13:30 UTC`), so it is
923
+ * falls back to (`reopens Mon`). A time is the regular session's open, and
924
+ * says so (`pre-market open · regular session opens 13:30 UTC`), so it is
918
925
  * never read as when the pre-market opens, nor a US market as closed until
919
- * then while its pre-market trades first.
926
+ * then while its pre-market trades first. A futures venue's Sunday open is no
927
+ * regular session's, so it reads `markets closed · reopens 22:00 UTC`.
920
928
  */
921
929
  function marketText(market: ReportMarket, now: number, short: boolean, local: boolean): string {
922
930
  const reopen = reopenText(market, now, local);
@@ -930,6 +938,8 @@ function marketText(market: ReportMarket, now: number, short: boolean, local: bo
930
938
  return short ? `pre-market · regular opens ${reopen.text}` : `pre-market open · regular session opens ${reopen.text}`;
931
939
  case "closed":
932
940
  if (!reopen) return short ? "closed" : "markets closed";
941
+ // A futures venue's Sunday open is its first trade of the week, which no regular session is.
942
+ if (reopen.time && market.firstTrade) return short ? `reopens ${reopen.text}` : `markets closed · reopens ${reopen.text}`;
933
943
  if (reopen.time) return short ? `regular opens ${reopen.text}` : `markets closed · regular session opens ${reopen.text}`;
934
944
  return short ? `reopens ${reopen.text}` : `markets closed until ${reopen.text}`;
935
945
  default: {
@@ -970,6 +980,7 @@ function statusLineParts(freshness: ReportFreshness, detail: StatusLineDetail =
970
980
  const undated = detail.date === "none";
971
981
  return [
972
982
  ...(freshness.sources?.length ? sourcesAsOfParts(freshness, freshness.sources, undated)
983
+ : freshness.unreached && !freshness.asOf ? []
973
984
  : [asOfText(freshness, detail.asOf === "short", local, undated)]),
974
985
  ...(freshness.schedule && detail.schedule !== "none"
975
986
  ? [typeof detail.schedule === "number" ? freshness.scheduleVariants?.[detail.schedule] ?? freshness.schedule : freshness.schedule] : []),
@@ -1174,7 +1185,8 @@ export function captureStatusLine({ report: loaded, selfDated, quoted, rendered
1174
1185
  }
1175
1186
  if (report && loaded?.declaresData) return sourced(report);
1176
1187
  if (quoted) return plain(quoted, "quotes");
1177
- if (report && (report.status !== "unreported" || report.ageOnly)) return sourced(report);
1188
+ // A report that names no feed but dates its data by a session's close says so too: a board's figures outside the session.
1189
+ if (report && (report.status !== "unreported" || report.ageOnly || report.asOfClose)) return sourced(report);
1178
1190
  return plain(rendered(), null);
1179
1191
  }
1180
1192
 
@@ -734,10 +734,10 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
734
734
  bloomberg: ["IMAP"],
735
735
  },
736
736
  MEMB: {
737
- summary: "ETF holdings with weights, shares, member returns, daily contributions and index changes. SPX and SPY use IVV holdings.",
738
- usage: ["MEMB", "MEMB SPY", "MEMB IWM"],
737
+ summary: "ETF holdings with weights, shares, member returns, daily contributions, breadth and index changes. SPX and ^SPX use IVV holdings.",
738
+ usage: ["MEMB", "MEMB SPX", "MEMB ^RUT", "gloom fn MEMB SPX --tab breadth"],
739
739
  keys: [TABS, OPEN, MEMBERS_MENU],
740
- data: same("Dated fund holdings and partial delayed member returns. Nasdaq-100 is not covered."),
740
+ data: same("Dated holdings and member returns. Breadth history uses current members (survivorship bias). Nasdaq-100 is not covered."),
741
741
  bloomberg: ["MEMB", "MRR", "IMOV"],
742
742
  },
743
743
  THEM: {
@@ -894,6 +894,13 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
894
894
  data: US_SCANNER,
895
895
  bloomberg: [],
896
896
  },
897
+ SCAN: {
898
+ summary: "Your own intraday scan of US stocks over all listings, an index, a sector or a watchlist: moves over the last minutes, volume spikes against the average pace, new session highs and lows, VWAP crosses and gaps, above price, market cap and volume floors. Matches refresh through the session, and new ones can alert.",
899
+ usage: ["SCAN"],
900
+ keys: [TABS, OPEN, key("a", "dd condition"), key("s", "ave scan")],
901
+ data: US_SCANNER,
902
+ bloomberg: [],
903
+ },
897
904
  FLOW: {
898
905
  summary: "Sweeps, blocks and large premium prints as they hit the tape, with ticker, strike, expiry and premium.",
899
906
  usage: ["FLOW"],
@@ -43,6 +43,7 @@ import { isRecord } from "../../utils/guards";
43
43
  import { splitSymbolWords } from "../symbol-args";
44
44
  import { formatUtcTime, localTimeSuffix } from "../../utils/utc-time";
45
45
  import { deriveHeadlessFreshness, formatFreshnessLine, staleRowsText, withSavedCopy, type ReportFreshness } from "./freshness";
46
+ import { DATA_SERVICE_UNREACHABLE_MESSAGE } from "../../utils/network-failure";
46
47
  import { accessGateStatus, errorsAccessGate, isProLock, reportGateText } from "./access-gate";
47
48
  import { noArgumentMessage } from "./capabilities";
48
49
  import { lockedHeadlessReport } from "./lock-note";
@@ -780,13 +781,24 @@ export function headlessReportTables(
780
781
  // A section of one name or one date says so for every line it exports.
781
782
  const named = {
782
783
  ...(section.symbol?.trim() ? { symbolOrSeries: section.symbol.trim() } : {}),
783
- ...(section.asOf?.trim() ? { asOf: section.asOf.trim() } : {}),
784
+ ...(section.asOf === null ? { asOf: null } : section.asOf?.trim() ? { asOf: section.asOf.trim() } : {}),
785
+ ...(section.period?.trim() ? { period: section.period.trim() } : {}),
786
+ ...(section.released?.trim() ? { released: section.released.trim() } : {}),
784
787
  };
785
788
  if (!("rows" in section && section.rows)) {
786
789
  const entries = exportEntriesTable(section.title, section.entries);
787
790
  // A figure that declares its unit names it in the unit column of its own line too.
788
- const rowIdentities = section.entries.some((entry) => entry.unit?.trim())
789
- ? section.entries.map((entry) => entry.unit?.trim() ? { currencyOrUnit: entry.unit.trim() } : undefined) : undefined;
791
+ // A line of its own period or release (a previous reading) names them rather than the section's.
792
+ const own = (entry: HeadlessPaneEntry): ExportIdentity | undefined => {
793
+ const identity = {
794
+ ...(entry.unit?.trim() ? { currencyOrUnit: entry.unit.trim() } : {}),
795
+ ...(entry.period !== undefined ? { period: entry.period } : {}),
796
+ ...(entry.released !== undefined ? { released: entry.released } : {}),
797
+ ...(entry.asOf !== undefined ? { asOf: entry.asOf } : {}),
798
+ };
799
+ return Object.keys(identity).length > 0 ? identity : undefined;
800
+ };
801
+ const rowIdentities = section.entries.some((entry) => own(entry)) ? section.entries.map(own) : undefined;
790
802
  return { ...entries, ...(Object.keys(named).length > 0 ? { identity: named } : {}), ...(rowIdentities ? { rowIdentities } : {}) };
791
803
  }
792
804
  const table = exportRowsTable(section.title, displayedColumns(columnsFor(section.rows, section.columns, definition.columns)), section.rows, { status: true });
@@ -819,6 +831,7 @@ export function headlessReportTables(
819
831
  const gate = reportGate(definition, result);
820
832
  return {
821
833
  tables,
834
+ ...(definition.exportDated ? { dated: true } : {}),
822
835
  identity: reportIdentity(definition, result, args),
823
836
  footer: reportFooterLines({
824
837
  freshness,
@@ -961,6 +974,22 @@ export function reportGaps(loaded: LoadedHeadlessPaneModel, freshness: ReportFre
961
974
  .filter(Boolean).join(", ") || null;
962
975
  }
963
976
 
977
+ /**
978
+ * The freshness of a report that served nothing because the data service
979
+ * could not be reached (offline, a hung connection) and no saved copy
980
+ * answered: its line names no retrieval time, as nothing was retrieved.
981
+ */
982
+ function withUnreached(
983
+ freshness: ReportFreshness,
984
+ result: HeadlessPaneResult,
985
+ { rowCount, unavailableSymbols, symbols }: { rowCount: number; unavailableSymbols: readonly string[]; symbols: readonly string[] },
986
+ ): ReportFreshness {
987
+ if (freshness.savedAt || freshness.asOf) return freshness;
988
+ const nothingServed = rowCount === 0 || (symbols.length > 0 && symbols.every((symbol) => unavailableSymbols.includes(symbol)));
989
+ const unreached = (result.errors ?? []).some((error) => error.includes(DATA_SERVICE_UNREACHABLE_MESSAGE));
990
+ return nothingServed && unreached ? { ...freshness, unreached: true } : freshness;
991
+ }
992
+
964
993
  export async function buildHeadlessFunctionReport(
965
994
  resolved: ResolvedPaneFunction,
966
995
  context: MarketContext,
@@ -979,8 +1008,13 @@ export async function buildHeadlessFunctionReport(
979
1008
  }
980
1009
  const { complete, unavailableSymbols, rowCount, symbols } = reportCoverage(loaded);
981
1010
  const serialized = serializeHeadlessPaneResult(loaded.definition, loaded.result);
982
- // What the report shows from a saved copy, its refresh having failed, says so.
983
- const freshness = withSavedCopy(deriveHeadlessFreshness(loaded.definition, loaded.result), loaded.result);
1011
+ // What the report shows from a saved copy, its refresh having failed, says so; one that could not
1012
+ // reach the data service for anything names no time it was retrieved.
1013
+ const freshness = withUnreached(
1014
+ withSavedCopy(deriveHeadlessFreshness(loaded.definition, loaded.result), loaded.result),
1015
+ loaded.result,
1016
+ { rowCount, unavailableSymbols, symbols },
1017
+ );
984
1018
  return {
985
1019
  data: {
986
1020
  kind: loaded.definition.shape,
@@ -36,7 +36,8 @@ import {
36
36
  ListingArgError, listingFateLoader, listingReadNotes, loadForListing, loadListingQuoteAnswer, quotedListingChoiceNotes, refuseMovedListing,
37
37
  } from "../listing-arg";
38
38
  import { isNotATickerMessage } from "../not-a-ticker";
39
- import { isCliFailure } from "../errors";
39
+ import { isCliFailure, SERVICE_UNREACHABLE_CODE } from "../errors";
40
+ import { DATA_SERVICE_UNREACHABLE_MESSAGE } from "../../utils/network-failure";
40
41
  import { isTickerLinkPeer } from "../../layout/ticker-navigation";
41
42
  import { knownGapMessage, commandPointer, commandPointerLeads, topicPointer } from "../known-gaps";
42
43
  import { selectReportTables, type CliReportTables } from "../report-tables";
@@ -247,7 +248,9 @@ function tablesHoldValues(tables: CliReportTables): boolean {
247
248
  * is a list (`failsWithoutData`) with no name that has any. A report with data
248
249
  * for some of what was asked, warnings or errors beside it, is a result: a
249
250
  * loss-maker's P/E band without a P/E, or IV history with only realized
250
- * volatility, exits 0.
251
+ * volatility, exits 0. Not one missing a part because the data service could
252
+ * not be reached: that is no answer rather than no data, and fails as
253
+ * retryable `service_unreachable`, whatever else it shows.
251
254
  */
252
255
  export function reportFailure(
253
256
  data: { empty: boolean; rowCount: number; symbols: readonly string[]; unavailableSymbols: readonly string[]; errors?: readonly string[]; reason?: unknown },
@@ -257,8 +260,12 @@ export function reportFailure(
257
260
  const missing = missingListings(data.symbols, data.unavailableSymbols);
258
261
  const noListing = data.symbols.length > 0 && data.symbols.every((symbol) => missing.has(listingKey(symbol)));
259
262
  const noValues = tables ? !tablesHoldValues(tables) : false;
260
- if (!data.empty && !(noListing && noValues) && !(failsWithoutData && foundNothing(data))) return null;
261
263
  const reasons = [...data.errors ?? [], ...typeof data.reason === "string" ? [data.reason] : []];
264
+ const unreached = reasons.filter((reason) => reason.includes(DATA_SERVICE_UNREACHABLE_MESSAGE));
265
+ if (unreached.length > 0 && (data.empty || data.unavailableSymbols.length > 0)) {
266
+ return { code: SERVICE_UNREACHABLE_CODE, message: `${asked}: ${unreached.join("; ")}.`.replace(/\.\.$/, "."), retryable: true };
267
+ }
268
+ if (!data.empty && !(noListing && noValues) && !(failsWithoutData && foundNothing(data))) return null;
262
269
  const unavailable = data.unavailableSymbols.length > 0 ? `no data for ${data.unavailableSymbols.join(", ")}` : "no data";
263
270
  return { code: "no_data", message: `${asked}: ${reasons.length > 0 ? reasons.join("; ") : unavailable}.`.replace(/\.\.$/, ".") };
264
271
  }