gloomberb 0.18.1 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (307) hide show
  1. package/package.json +1 -1
  2. package/src/api-client/commodity-balances.ts +6 -0
  3. package/src/api-client/company-kpis.ts +9 -0
  4. package/src/api-client/crypto-markets.ts +9 -0
  5. package/src/api-client/data.ts +6 -5
  6. package/src/api-client/equity-screener.ts +2 -0
  7. package/src/api-client/types.ts +5 -0
  8. package/src/app/pane-runtime/plugin-bindings.ts +3 -1
  9. package/src/app/pane-runtime/ticker-open-runtime.ts +17 -5
  10. package/src/app/runtime/plugin-bindings.ts +6 -2
  11. package/src/brokers/logos.ts +66 -0
  12. package/src/brokers/profile-form.ts +3 -0
  13. package/src/brokers/remote-broker-adapter.ts +13 -3
  14. package/src/capabilities/index.ts +1 -1
  15. package/src/capabilities/service-capabilities.ts +7 -1
  16. package/src/cli/base-converter.ts +60 -18
  17. package/src/cli/commands/market.ts +281 -154
  18. package/src/cli/commands/news-search.ts +35 -8
  19. package/src/cli/commands/system.ts +35 -1
  20. package/src/cli/commands/ticker.ts +100 -50
  21. package/src/cli/currency-hints.ts +27 -0
  22. package/src/cli/fx-pair.ts +41 -4
  23. package/src/cli/glossary.ts +57 -4
  24. package/src/cli/helpers.ts +36 -3
  25. package/src/cli/index.ts +4 -1
  26. package/src/cli/known-gaps.ts +37 -1
  27. package/src/cli/listing-arg.ts +37 -2
  28. package/src/cli/pane-functions/catalog.ts +3 -1
  29. package/src/cli/pane-functions/discovery.ts +15 -1
  30. package/src/cli/pane-functions/dom.ts +9 -4
  31. package/src/cli/pane-functions/freshness.ts +4 -4
  32. package/src/cli/pane-functions/function-help.ts +27 -3
  33. package/src/cli/pane-functions/headless.ts +6 -5
  34. package/src/cli/pane-functions/index.ts +3 -2
  35. package/src/cli/pane-functions/lock-note.ts +7 -4
  36. package/src/cli/pane-functions/resolver.ts +2 -1
  37. package/src/cli/quote-answer.ts +179 -0
  38. package/src/cli/series-search.ts +2 -1
  39. package/src/cli/types.ts +4 -0
  40. package/src/components/chart/composite/composite-chart.tsx +11 -2
  41. package/src/components/chart/composite/format.ts +13 -1
  42. package/src/components/chart/composite/level-labels.ts +30 -0
  43. package/src/components/chart/composite/levels.ts +17 -6
  44. package/src/components/chart/composite/panel-surface.tsx +58 -8
  45. package/src/components/command-bar/routes/ticker-search/actions.ts +6 -0
  46. package/src/components/index.ts +2 -0
  47. package/src/components/layout/pane/content.tsx +16 -14
  48. package/src/components/layout/pane/footer/index.tsx +22 -3
  49. package/src/components/layout/pane/footer/model.ts +99 -4
  50. package/src/components/layout/pane/header-tabs.tsx +15 -1
  51. package/src/components/onboarding/onboarding-frame.tsx +9 -11
  52. package/src/components/onboarding/use-onboarding-broker-form.ts +5 -2
  53. package/src/components/onboarding/wizard-model.ts +10 -3
  54. package/src/components/ui/button.tsx +11 -4
  55. package/src/components/ui/choice-dialog.tsx +8 -5
  56. package/src/components/ui/display.tsx +11 -6
  57. package/src/components/ui/fields.tsx +100 -20
  58. package/src/components/ui/icon.tsx +6 -1
  59. package/src/components/ui/list-row-lines.tsx +87 -0
  60. package/src/components/ui/list-view.tsx +14 -0
  61. package/src/components/ui/logo-mark.tsx +64 -0
  62. package/src/components/ui/number-step.ts +54 -0
  63. package/src/components/ui/select-button.tsx +5 -4
  64. package/src/components/ui/select-field.tsx +3 -3
  65. package/src/components/ui/status.tsx +17 -3
  66. package/src/components/ui/tabs.tsx +9 -1
  67. package/src/components/ui/toggle.tsx +17 -3
  68. package/src/core/state/app/instrument.ts +8 -3
  69. package/src/data/config/store/normalize.ts +4 -1
  70. package/src/data/plugin-cache.ts +32 -6
  71. package/src/docs/snapshot.json +5 -5
  72. package/src/i18n/es.ts +9 -3
  73. package/src/i18n/ja.ts +9 -3
  74. package/src/i18n/ko.ts +9 -3
  75. package/src/i18n/zh-cn.ts +10 -4
  76. package/src/i18n/zh-tw.ts +10 -4
  77. package/src/market-data/commodity-names.ts +80 -0
  78. package/src/market-data/coordinator/auxiliary.ts +3 -1
  79. package/src/market-data/coordinator/chart.ts +3 -1
  80. package/src/market-data/futures-board.ts +38 -1
  81. package/src/market-data/futures-contract.ts +91 -0
  82. package/src/market-data/fx-closes.ts +78 -1
  83. package/src/market-data/market/change-basis.ts +219 -0
  84. package/src/market-data/market/format.ts +28 -12
  85. package/src/market-data/market/freshness.ts +38 -0
  86. package/src/market-data/market/status.ts +55 -9
  87. package/src/market-data/market/year-to-date.ts +46 -0
  88. package/src/market-data/quotes/contributions.ts +12 -0
  89. package/src/market-data/quotes/delisting.ts +34 -0
  90. package/src/market-data/quotes/live-valuation.ts +23 -0
  91. package/src/market-data/quotes/resolution.ts +8 -2
  92. package/src/market-data/tax-equivalent.ts +10 -2
  93. package/src/notifications/app-notifier.ts +2 -2
  94. package/src/notifications/notification-log.ts +8 -6
  95. package/src/plugins/builtin/alerts/cli.ts +26 -18
  96. package/src/plugins/builtin/alerts/headless.ts +1 -1
  97. package/src/plugins/builtin/alerts/index.tsx +13 -4
  98. package/src/plugins/builtin/attention/trending.tsx +8 -6
  99. package/src/plugins/builtin/backtest/client.ts +5 -2
  100. package/src/plugins/builtin/backtest/window.ts +16 -3
  101. package/src/plugins/builtin/broker-manager/add-flow.tsx +40 -8
  102. package/src/plugins/builtin/broker-manager/detail.tsx +26 -1
  103. package/src/plugins/builtin/broker-manager/index.tsx +20 -9
  104. package/src/plugins/builtin/broker-manager/model.ts +5 -0
  105. package/src/plugins/builtin/broker-manager/pane-actions.tsx +8 -1
  106. package/src/plugins/builtin/broker-manager/table.ts +5 -1
  107. package/src/plugins/builtin/broker-trading/account-picker.tsx +91 -0
  108. package/src/plugins/builtin/broker-trading/chart-levels.ts +54 -0
  109. package/src/plugins/builtin/broker-trading/command.ts +103 -0
  110. package/src/plugins/builtin/broker-trading/controller.ts +688 -0
  111. package/src/plugins/builtin/broker-trading/dock.tsx +182 -0
  112. package/src/plugins/builtin/broker-trading/headless.ts +76 -0
  113. package/src/plugins/builtin/broker-trading/index.tsx +77 -0
  114. package/src/plugins/builtin/broker-trading/order-feed.ts +383 -0
  115. package/src/plugins/builtin/broker-trading/orders-data.ts +133 -0
  116. package/src/plugins/builtin/broker-trading/orders.tsx +437 -0
  117. package/src/plugins/builtin/broker-trading/overview-orders.tsx +121 -0
  118. package/src/plugins/builtin/broker-trading/pane-model.ts +207 -0
  119. package/src/plugins/builtin/broker-trading/pane.tsx +505 -0
  120. package/src/plugins/builtin/broker-trading/strategy.ts +153 -0
  121. package/src/plugins/builtin/broker-trading/ticket-parts.tsx +117 -0
  122. package/src/plugins/builtin/broker-trading/ticket.tsx +573 -0
  123. package/src/plugins/builtin/buildout/data-runtime.ts +1 -1
  124. package/src/plugins/builtin/buildout/format.ts +1 -0
  125. package/src/plugins/builtin/buildout/model/index.ts +16 -7
  126. package/src/plugins/builtin/buildout/pane/index.tsx +3 -2
  127. package/src/plugins/builtin/buildout/pane/status.tsx +3 -3
  128. package/src/plugins/builtin/chart-composer/cli-options.ts +19 -0
  129. package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +2 -1
  130. package/src/plugins/builtin/chart-composer/headless-schema.ts +3 -3
  131. package/src/plugins/builtin/chart-composer/headless.ts +2 -1
  132. package/src/plugins/builtin/chart-composer/option-range.ts +39 -0
  133. package/src/plugins/builtin/chart-composer/pane.tsx +31 -11
  134. package/src/plugins/builtin/chart-composer/presets.ts +2 -2
  135. package/src/plugins/builtin/chart-composer/series-catalog.ts +4 -2
  136. package/src/plugins/builtin/chart-composer/shortcuts.ts +3 -3
  137. package/src/plugins/builtin/chart-composer/viewport-labels.ts +5 -2
  138. package/src/plugins/builtin/chat/index.tsx +1 -1
  139. package/src/plugins/builtin/chat/status-widget.tsx +1 -1
  140. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +6 -1
  141. package/src/plugins/builtin/chat/unread-inbox.ts +1 -1
  142. package/src/plugins/builtin/commodity-balances/client.ts +6 -4
  143. package/src/plugins/builtin/commodity-balances/headless.ts +41 -7
  144. package/src/plugins/builtin/commodity-balances/index.tsx +19 -4
  145. package/src/plugins/builtin/commodity-balances/model.ts +22 -0
  146. package/src/plugins/builtin/commodity-balances/pane.tsx +36 -5
  147. package/src/plugins/builtin/company-kpis/client.ts +3 -2
  148. package/src/plugins/builtin/company-kpis/headless.ts +5 -7
  149. package/src/plugins/builtin/company-kpis/model.ts +85 -2
  150. package/src/plugins/builtin/company-kpis/pane.tsx +26 -12
  151. package/src/plugins/builtin/composite-plugins.ts +3 -2
  152. package/src/plugins/builtin/cot/headless.ts +1 -1
  153. package/src/plugins/builtin/cot/index.tsx +1 -1
  154. package/src/plugins/builtin/cot/model.ts +35 -12
  155. package/src/plugins/builtin/credit-boards/cdx-pane.tsx +2 -1
  156. package/src/plugins/builtin/credit-boards/sovr-pane.tsx +2 -1
  157. package/src/plugins/builtin/credit-conditions/index.tsx +2 -1
  158. package/src/plugins/builtin/crypto-board/client.ts +33 -2
  159. package/src/plugins/builtin/crypto-board/headless.ts +5 -2
  160. package/src/plugins/builtin/crypto-board/model.ts +28 -8
  161. package/src/plugins/builtin/dividend-yield/headless.ts +30 -8
  162. package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
  163. package/src/plugins/builtin/earnings-calls/headless.ts +4 -4
  164. package/src/plugins/builtin/earnings-calls/lock-note.ts +12 -3
  165. package/src/plugins/builtin/econ/calendar-model.ts +35 -0
  166. package/src/plugins/builtin/econ/index.tsx +31 -4
  167. package/src/plugins/builtin/econ/print-watch.ts +279 -0
  168. package/src/plugins/builtin/equity-screener/headless.ts +20 -5
  169. package/src/plugins/builtin/equity-screener/model.ts +40 -2
  170. package/src/plugins/builtin/equity-screener/pane.tsx +24 -21
  171. package/src/plugins/builtin/estimate-revisions/headless.ts +3 -2
  172. package/src/plugins/builtin/estimate-revisions/lock-note.ts +29 -7
  173. package/src/plugins/builtin/exposure/client.ts +4 -1
  174. package/src/plugins/builtin/exposure/editor.tsx +3 -3
  175. package/src/plugins/builtin/exposure/evidence.ts +4 -1
  176. package/src/plugins/builtin/exposure/holdings.ts +3 -1
  177. package/src/plugins/builtin/exposure/pane.tsx +37 -16
  178. package/src/plugins/builtin/filing-events/index.tsx +9 -3
  179. package/src/plugins/builtin/futures/table.tsx +6 -3
  180. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  181. package/src/plugins/builtin/insider/base-values.ts +72 -0
  182. package/src/plugins/builtin/insider/headless.ts +29 -4
  183. package/src/plugins/builtin/jobs/headless.ts +2 -2
  184. package/src/plugins/builtin/jobs/lock-note.ts +14 -3
  185. package/src/plugins/builtin/market-halts/model.ts +31 -6
  186. package/src/plugins/builtin/market-halts/pane.tsx +3 -0
  187. package/src/plugins/builtin/market-valuation/pane.tsx +2 -1
  188. package/src/plugins/builtin/members/pane.tsx +8 -3
  189. package/src/plugins/builtin/news/wire/feed-config.ts +29 -12
  190. package/src/plugins/builtin/news/wire/index.tsx +7 -2
  191. package/src/plugins/builtin/news/wire/news/preset-pane.tsx +11 -3
  192. package/src/plugins/builtin/news/wire/rss/cli.ts +3 -2
  193. package/src/plugins/builtin/news/wire/rss/source.ts +13 -4
  194. package/src/plugins/builtin/options/footer.ts +25 -20
  195. package/src/plugins/builtin/options/live-quotes.ts +33 -18
  196. package/src/plugins/builtin/options/view.tsx +77 -22
  197. package/src/plugins/builtin/options-calculator/pane.tsx +4 -2
  198. package/src/plugins/builtin/options-scenario/pane.tsx +41 -6
  199. package/src/plugins/builtin/portfolio-list/account-metrics.ts +3 -0
  200. package/src/plugins/builtin/portfolio-list/cli/render.ts +34 -10
  201. package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
  202. package/src/plugins/builtin/portfolio-list/pane/data.ts +23 -1
  203. package/src/plugins/builtin/portfolio-list/pane/index.tsx +33 -14
  204. package/src/plugins/builtin/portfolio-list/settings.ts +20 -2
  205. package/src/plugins/builtin/portfolio-list/summary/index.tsx +12 -0
  206. package/src/plugins/builtin/portfolio-list/summary/totals.ts +24 -3
  207. package/src/plugins/builtin/primary-dealers/client.ts +137 -62
  208. package/src/plugins/builtin/primary-dealers/headless.ts +30 -5
  209. package/src/plugins/builtin/primary-dealers/index.tsx +3 -2
  210. package/src/plugins/builtin/primary-dealers/model.ts +179 -129
  211. package/src/plugins/builtin/primary-dealers/pane.tsx +135 -15
  212. package/src/plugins/builtin/research/analyst-model.ts +5 -1
  213. package/src/plugins/builtin/risk-factors/index.tsx +12 -3
  214. package/src/plugins/builtin/scanner/feed.ts +6 -1
  215. package/src/plugins/builtin/scanner/headless.ts +1 -1
  216. package/src/plugins/builtin/scanner/lock-note.ts +3 -2
  217. package/src/plugins/builtin/series-inventory.ts +1 -0
  218. package/src/plugins/builtin/shared/cloud-denied.ts +7 -6
  219. package/src/plugins/builtin/shared/cloud-upgrade.ts +10 -11
  220. package/src/plugins/builtin/shared/free-routes.ts +24 -0
  221. package/src/plugins/builtin/shared/headless-options.ts +11 -0
  222. package/src/plugins/builtin/shared/pro-lock-note.ts +89 -7
  223. package/src/plugins/builtin/shared/research-trading.tsx +62 -0
  224. package/src/plugins/builtin/social-mentions/headless.ts +1 -1
  225. package/src/plugins/builtin/social-mentions/lock-note.ts +1 -1
  226. package/src/plugins/builtin/themes/headless.ts +8 -2
  227. package/src/plugins/builtin/themes/model.ts +43 -10
  228. package/src/plugins/builtin/themes/pane.tsx +8 -6
  229. package/src/plugins/builtin/ticker-detail/overview/model.ts +44 -10
  230. package/src/plugins/builtin/ticker-detail/overview/option-contract.ts +64 -0
  231. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +82 -21
  232. package/src/plugins/builtin/ticker-detail/pane.tsx +21 -14
  233. package/src/plugins/builtin/ticker-detail/quote-footer.ts +5 -1
  234. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +37 -8
  235. package/src/plugins/builtin/time-sales/pane.tsx +2 -2
  236. package/src/plugins/builtin/trace-bonds/client.ts +14 -3
  237. package/src/plugins/builtin/trace-bonds/headless.ts +1 -1
  238. package/src/plugins/builtin/vol-surface/pane.tsx +2 -2
  239. package/src/plugins/builtin-editorial.ts +1 -1
  240. package/src/plugins/compat.ts +3 -0
  241. package/src/plugins/external-runtime.ts +5 -0
  242. package/src/plugins/loader.ts +6 -0
  243. package/src/plugins/plugin-icon.ts +61 -0
  244. package/src/plugins/registry/host-actions.ts +2 -1
  245. package/src/plugins/registry/index.ts +9 -1
  246. package/src/plugins/runtime/context.tsx +5 -0
  247. package/src/plugins/runtime/index.tsx +3 -0
  248. package/src/portfolio/column-values.ts +13 -4
  249. package/src/portfolio/cost-fx.ts +5 -28
  250. package/src/public/__snapshots__/public-api.test.ts.snap +5 -0
  251. package/src/public/broker.ts +8 -0
  252. package/src/public/market-data.ts +5 -0
  253. package/src/renderers/dom/desktop/control-styles.ts +7 -1
  254. package/src/renderers/dom/desktop/controls.tsx +39 -7
  255. package/src/renderers/dom/desktop/icons.tsx +14 -0
  256. package/src/renderers/dom/desktop/list-view.tsx +51 -19
  257. package/src/renderers/dom/desktop/menu.tsx +10 -1
  258. package/src/renderers/dom/desktop/select-field.tsx +8 -6
  259. package/src/renderers/dom/host/style.ts +6 -1
  260. package/src/renderers/dom/host/tabs.tsx +6 -0
  261. package/src/renderers/dom/styles.css +12 -5
  262. package/src/renderers/electrobun/bun/core-capabilities.ts +12 -0
  263. package/src/renderers/electrobun/bun/external-plugins.ts +3 -0
  264. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  265. package/src/renderers/electrobun/view/broker-remote-client.ts +16 -0
  266. package/src/renderers/electrobun/view/external-plugins.ts +1 -0
  267. package/src/series/validate.ts +2 -2
  268. package/src/sources/gloomberb-cloud/index.ts +50 -17
  269. package/src/sources/gloomberb-cloud/normalizers.ts +4 -0
  270. package/src/sources/history-result.ts +3 -0
  271. package/src/sources/provider-errors.ts +3 -1
  272. package/src/sources/provider-router/history.ts +49 -6
  273. package/src/sources/provider-router/index.ts +2 -1
  274. package/src/state/app/activity.ts +13 -0
  275. package/src/sync/controller.ts +4 -0
  276. package/src/sync/core-contributors.ts +62 -11
  277. package/src/sync/local-only.ts +121 -0
  278. package/src/sync/react.ts +6 -0
  279. package/src/sync/types.ts +3 -0
  280. package/src/tickers/listing-fate.ts +27 -8
  281. package/src/tickers/search/index.ts +24 -3
  282. package/src/time-series/price-comparison.ts +35 -0
  283. package/src/time-series/resolve.ts +52 -11
  284. package/src/time-series/spec.ts +1 -1
  285. package/src/time-series/types.ts +5 -0
  286. package/src/types/broker.ts +56 -1
  287. package/src/types/config.ts +6 -0
  288. package/src/types/data-provider.ts +5 -1
  289. package/src/types/financials.ts +23 -0
  290. package/src/types/headless.ts +12 -2
  291. package/src/types/instrument.ts +4 -0
  292. package/src/types/plugin.ts +24 -6
  293. package/src/types/price-history.ts +6 -1
  294. package/src/types/trading.ts +67 -0
  295. package/src/ui/host.tsx +4 -0
  296. package/src/utils/base-currency.ts +108 -0
  297. package/src/utils/currency-names.ts +128 -0
  298. package/src/utils/currency-pair.ts +2 -1
  299. package/src/utils/currency-units.ts +32 -1
  300. package/src/utils/dom-screenshot.ts +22 -9
  301. package/src/utils/exchange-notation.generated.ts +360 -0
  302. package/src/utils/exchanges.ts +15 -322
  303. package/src/utils/format.ts +31 -1
  304. package/src/utils/instrument-identity.ts +38 -1
  305. package/src/utils/options.ts +46 -1
  306. package/src/utils/price-history.ts +4 -0
  307. package/src/utils/reported-money.ts +8 -4
@@ -1,6 +1,7 @@
1
1
  import { logFailure } from "../../utils/log-failure";
2
2
  import type { CliCommandDef } from "../../types/plugin";
3
3
  import { TIME_RANGES, type TimeRange } from "../../time-series/range";
4
+ import { yearToDateStartIndex } from "../../market-data/market/year-to-date";
4
5
  import type { EarningsEvent, QuoteBatchResult, QuoteSubscriptionTarget, SecFilingItem } from "../../types/data-provider";
5
6
  import type { NewsArticle, NewsFeed, NewsQuery } from "../../news/types";
6
7
  import type {
@@ -11,8 +12,9 @@ import type {
11
12
  Quote,
12
13
  TickerFinancials,
13
14
  } from "../../types/financials";
14
- import { currencyPriceMinDigits, formatMarketPrice, formatMarketPriceWithCurrency, quoteFormatOptions, resolveAssetDisplayKind } from "../../market-data/market/format";
15
+ import { currencyPriceMinDigits, formatMarketPrice, formatMarketPriceWithCurrency, quoteFormatOptions, resolveAssetDisplayKind, withCurrencyMinorDigits } from "../../market-data/market/format";
15
16
  import { INDEX_POINTS_EXPORT_UNIT, isIndexLevel, reportedQuoteVolume } from "../../market-data/market/index-level";
17
+ import { changeBasisJson, changeBasisLabel, changeBasisNote, quoteChangeBasis, quotePreviousCloseDate } from "../../market-data/market/change-basis";
16
18
  import {
17
19
  EXTENDED_SESSION_LABELS,
18
20
  getExtendedSessionDisplay,
@@ -34,6 +36,7 @@ import {
34
36
  formatCountCell,
35
37
  formatFractionPercentCell,
36
38
  formatPriceRange,
39
+ quoteChangeText,
37
40
  } from "../helpers";
38
41
  import { cliStyles, colorBySign, renderSection, renderStats } from "../../utils/cli-output";
39
42
  import { statementHistoryNotes } from "../../utils/financial-statements";
@@ -51,13 +54,14 @@ import { isQuoteStaleForCurrentSession } from "../../market-data/quotes/freshnes
51
54
  import { isOptionsUnavailableError, listedOptionsAlternative, optionsUnavailableTitle } from "../../market-data/options-alternatives";
52
55
  import {
53
56
  cliFuturesContract, futuresContractMetrics, exportedDividendFundamentals, fundamentalsReportTables, loadDividendDistributions, loadReportBondFacts,
54
- renderFundamentalsReport,
57
+ renderFundamentalsReport, writesCurrencyCodes,
55
58
  } from "./ticker";
56
59
  import { loadStatementFilings } from "../statement-filings";
57
60
  import { loadBankFigures } from "../bank-section";
58
61
  import { latestFinancialPeriod } from "../../utils/latest-financial-period";
59
62
  import type { FuturesContractTermsPayload } from "../../api-client/futures-curve";
60
- import { futuresContractLines, futuresRollLines, futuresSettleText, isFuturesInstrument } from "../../market-data/futures-contract";
63
+ import { currencyCodeLabel } from "../../utils/currency-units";
64
+ import { futuresContractLines, futuresRollLines, futuresSettleText, isFuturesInstrument, minorUnitPricePer } from "../../market-data/futures-contract";
61
65
  import {
62
66
  chainHasExpiry,
63
67
  formatExpiryList,
@@ -77,7 +81,9 @@ import {
77
81
  type OptionsChainBasis,
78
82
  } from "./options-chain";
79
83
  import { missingExpiryMessage, OPTION_EXPIRATION_FORMAT, OPTION_EXPIRATION_PLACEHOLDER } from "../../utils/option-expiry";
80
- import { DATE_OPTION_EXAMPLES } from "../../utils/calendar-date";
84
+ import { DATE_OPTION_EXAMPLES, isoDateOption, isoDateToday } from "../../utils/calendar-date";
85
+ import { apiClient } from "../../api-client";
86
+ import { dailyFxLeg, fxCloseOnDate, type FxCloseReading } from "../../market-data/fx-closes";
81
87
  import { declaredDividends } from "../../utils/declared-dividends";
82
88
  import {
83
89
  DEFAULT_LEAPS_CRITERIA, LEAPS_EXTRA_COLUMNS, LEAPS_USAGE, parseLeapsCriteria, runLeapsScreen, takeLeapsOptions,
@@ -113,9 +119,10 @@ import {
113
119
  } from "../history-rows";
114
120
  import { MISSING_VOLUME_TEXT } from "../../utils/price-history-integrity";
115
121
  import { CRYPTO_BOARD_HINT, quoteNotes } from "./crypto-hints";
116
- import { bareFuturesCode } from "../../market-data/futures-board";
122
+ import { bareFuturesCode, futuresBoardContract } from "../../market-data/futures-board";
123
+ import { commodityNamedIn, commoditySearchName } from "../../market-data/commodity-names";
117
124
  import { newsEmptyMessage } from "./news-hints";
118
- import { commonWordCandidate, newsFallbackText, newsSearchHeading, newsSearchText, newsSearchTextProblem, quotedSearchCommand, searchNewsStories, wordMeansMore } from "./news-search";
125
+ import { commodityNewsRoute, commonWordCandidate, newsFallbackText, newsSearchHeading, newsSearchText, newsSearchTextProblem, noStoriesMessage, quotedSearchCommand, searchNewsStories, wordMeansMore } from "./news-search";
119
126
  import { formatUtcTime } from "../../utils/utc-time";
120
127
  import { DAY_SO_FAR_TEXT, withSessionBar } from "../../time-series/session-bar";
121
128
  import { isFiniteNumber } from "../../utils/guards";
@@ -141,10 +148,7 @@ import {
141
148
  showsEpsBasis,
142
149
  } from "../../plugins/builtin/shared/earnings-report";
143
150
  import {
144
- tradingBoardNote,
145
151
  EXCHANGE_OPTION,
146
- futuresTryLine,
147
- isNoProviderError,
148
152
  listingHeading,
149
153
  listingIdentity,
150
154
  listingTitle,
@@ -152,48 +156,43 @@ import {
152
156
  listingChoiceNotes,
153
157
  listingReadNotes,
154
158
  loadBareVenues,
155
- type ListingVenue,
156
159
  loadForListing,
157
160
  loadListingQuote,
158
161
  listingFateLoader,
159
162
  movedSymbolLine,
160
- notTradedMessage,
161
- quoteBareListing,
162
163
  requireCliListing,
163
164
  type CliListing,
164
- type ListingDataRequest,
165
165
  type ListingIdentity,
166
166
  } from "../listing-arg";
167
167
  import { bareListingNote, listingFateNotes, noLongerTradingNote, tradingListing } from "../../tickers/listing-fate";
168
- import { indexRootAlsoLine, indexRootFor, indexRootTryLine, isYieldIndexSymbol } from "../index-roots";
169
- import { treasuryYieldLine, treasuryYieldTenor } from "../treasury-yields";
168
+ import { isYieldIndexSymbol } from "../index-roots";
170
169
  import {
171
170
  analyzeTaxEquivalent, distributionBasis, loadTreasuryTenors, parseTaxRates, TAX_RATE_HELP_OPTIONS, taxEquivalentJson, type TaxRates,
172
171
  } from "../../market-data/tax-equivalent";
173
172
  import { CBOE_YIELD_INDEX_CLOSE } from "../../plugins/builtin/shared/treasury-cutoffs";
174
173
  import { earningsCalendarFailure, earningsChoiceNote, earningsDateLabel, isListingAbroad, noListingCalendarNote } from "../earnings-calendar";
175
174
  import { formatBasisPoints, toBasisPoints } from "../../utils/basis-points";
176
- import { notATickerSymbol, providerMissReason } from "../../sources/provider-errors";
177
- import { isNotATickerMessage, withListingSuggestion } from "../not-a-ticker";
175
+ import { providerMissReason } from "../../sources/provider-errors";
176
+ import { isNotATickerMessage } from "../not-a-ticker";
178
177
  import { primeBoardCoins } from "../board-coins";
179
- import { describeSuggestion, suggestListing, suggestUsTicker, type ListingSuggestion } from "../listing-suggestion";
178
+ import { suggestUsTicker, type ListingSuggestion } from "../listing-suggestion";
180
179
  import { quotedPhrase, splitSymbolArgs, uniqueSecuritySymbols } from "../symbol-args";
181
180
  import { isCompanyNameInput } from "../../tickers/company-name-listing";
182
181
  import { readNameEntries, readPhraseArgument } from "../name-entries";
183
182
  import { readSymbolList, SymbolListError } from "../symbol-list";
184
183
  import { cliPerpetualContract, perpetualContractStats, type PerpetualContractPayload } from "../perpetual-contract";
185
- import { knownGapForTicker, knownGapMessage, subMinuteDataMessage } from "../known-gaps";
184
+ import { knownGapMessage, subMinuteDataMessage } from "../known-gaps";
186
185
  import { phraseSearchReport } from "./search";
187
186
  import { secRegistrantMismatchMessage, SecRegistrantMismatchError, areDifferentCompanies } from "../../sources/sec-registrant";
188
187
  import { isCryptoPairSymbol } from "../../utils/crypto-pair";
189
- import { looksLikeCurrencyPair } from "../../utils/currency-pair";
190
- import { canonicalExchange, isUsListingExchange } from "../../utils/exchanges";
188
+ import { isUsListingExchange } from "../../utils/exchanges";
191
189
  import {
192
190
  describeFormerIssuer, describeTickerChange, movedAwayChange, startTickerChangeLookup, tickerChangeLine, type FormerIssuer, type TickerChange,
193
191
  } from "../../tickers/ticker-changes";
194
192
  import { noSecReportsNotice, nonUsSecListingVenue, secFilingItemCodes } from "../../utils/sec";
195
193
  import type { MarketContext } from "../types";
196
194
  import { windowRows } from "../row-window";
195
+ import { answerQuote, errorMessage, notUsTickerSentence } from "../quote-answer";
197
196
  import { crossRate, describeFxConversion, describeFxRate, FX_USAGE, isUsableRate, parseFxArgs } from "../fx-pair";
198
197
  import {
199
198
  holderListFacts,
@@ -235,7 +234,10 @@ import {
235
234
  type HolderForm,
236
235
  } from "../../plugins/builtin/holders/beneficial-report";
237
236
 
238
- const VALID_RANGES = new Set<TimeRange>(TIME_RANGES);
237
+ /** The ranges `history` takes: the chart ranges, and YTD, which runs from the prior year's last close. */
238
+ type HistoryRange = TimeRange | "YTD";
239
+ const HISTORY_RANGES: HistoryRange[] = [...TIME_RANGES.slice(0, TIME_RANGES.indexOf("1Y")), "YTD", ...TIME_RANGES.slice(TIME_RANGES.indexOf("1Y"))];
240
+ const VALID_RANGES = new Set<HistoryRange>(HISTORY_RANGES);
239
241
  const VALID_NEWS_FEEDS = new Set<NewsFeed>(["latest", "top", "breaking", "ticker", "sector", "topic"]);
240
242
 
241
243
  type QuoteCliRecord = Omit<QuoteBatchResult, "error"> & {
@@ -261,9 +263,9 @@ type FinancialsCliData = TickerFinancials & {
261
263
  providerId: string | null;
262
264
  };
263
265
 
264
- function parseRange(value: string | undefined, ctx: Parameters<CliCommandDef["execute"]>[1]): TimeRange {
265
- const range = (value ?? "1Y").toUpperCase() as TimeRange;
266
- if (!VALID_RANGES.has(range)) ctx.fail(`Unknown range "${value}".`, `Use one of ${TIME_RANGES.join(", ")}.`);
266
+ function parseRange(value: string | undefined, ctx: Parameters<CliCommandDef["execute"]>[1]): HistoryRange {
267
+ const range = (value ?? "1Y").toUpperCase() as HistoryRange;
268
+ if (!VALID_RANGES.has(range)) ctx.fail(`Unknown range "${value}".`, `Use one of ${HISTORY_RANGES.join(", ")}.`);
267
269
  return range;
268
270
  }
269
271
 
@@ -363,6 +365,28 @@ const QUOTE_LEAD_COLUMNS = [
363
365
 
364
366
  type QuoteRow = ReturnType<typeof quoteRows>[number];
365
367
 
368
+ /**
369
+ * What Chg% is measured from, as every change line says it ("vs Fri 16 Oct
370
+ * close", "since 00:00 UTC"): the table sets it after Chg% and names the
371
+ * venue's holidays under it; an export adds it, and the holiday note, after
372
+ * its existing columns, so a column read by position stays where it was.
373
+ */
374
+ const CHANGE_BASIS_COLUMN = {
375
+ key: "changeBasis", header: "Chg Basis", shrink: false, optional: true, format: (value: unknown) => cliStyles.muted(String(value ?? "")),
376
+ };
377
+ const CHANGE_BASIS_EXPORT_COLUMNS = [
378
+ { key: "changeBasis", header: "Chg Basis" },
379
+ { key: "changeBasisNote", header: "Chg Basis Note" },
380
+ ];
381
+
382
+ /** The table's columns: Chg Basis follows Chg%, and the note is a line under the table. */
383
+ function quoteTextColumns<Column extends { key: string }>(columns: Column[]): Array<Column | typeof CHANGE_BASIS_COLUMN> {
384
+ const exported = new Set(CHANGE_BASIS_EXPORT_COLUMNS.map((column) => column.key));
385
+ const kept = columns.filter((column) => !exported.has(column.key));
386
+ const at = kept.findIndex((column) => column.key === "changePercent") + 1;
387
+ return [...kept.slice(0, at), CHANGE_BASIS_COLUMN, ...kept.slice(at)];
388
+ }
389
+
366
390
  /** A Cboe yield index level is a yield: `5.24%`, not a price. */
367
391
  function yieldLevelCell(value: unknown, row: QuoteRow): string {
368
392
  const text = String(value ?? "");
@@ -398,10 +422,15 @@ function roundedPercent(value: number | undefined): number | null {
398
422
  return value == null ? null : Number(value.toFixed(2));
399
423
  }
400
424
 
425
+ /** The quote's currency code, US cents as US¢; an export keeps the code (USX). */
426
+ function currencyCell(value: unknown): string {
427
+ return currencyCodeLabel(typeof value === "string" ? value : "");
428
+ }
429
+
401
430
  function quoteColumns() {
402
431
  return [
403
432
  ...QUOTE_LEAD_COLUMNS,
404
- { key: "currency", header: "Cur", shrink: false, optional: true, dropPriority: 1 },
433
+ { key: "currency", header: "Cur", shrink: false, optional: true, dropPriority: 1, format: currencyCell },
405
434
  // Whether the price is real-time, delayed or stale: a feed state, not where it came from. A stale
406
435
  // row says so here, so the closing line's "N of M stale" always points at rows the table marks.
407
436
  {
@@ -414,6 +443,7 @@ function quoteColumns() {
414
443
  },
415
444
  // In a narrow terminal the closing line's as-of stands in for each row's.
416
445
  { key: "updatedAt", header: "Updated", shrink: false, optional: true, dropPriority: 3 },
446
+ ...CHANGE_BASIS_EXPORT_COLUMNS,
417
447
  ];
418
448
  }
419
449
 
@@ -424,15 +454,11 @@ function compareColumns() {
424
454
  // A narrow terminal drops these before it cuts the names short.
425
455
  { key: "dayRange", header: "Day Range", align: "right" as const, optional: true, dropPriority: 1 },
426
456
  { key: "volume", header: "Volume", align: "right" as const, format: formatCountCell, optional: true, dropPriority: 2 },
427
- { key: "currency", header: "Cur", shrink: false, optional: true, dropPriority: 3 },
457
+ { key: "currency", header: "Cur", shrink: false, optional: true, dropPriority: 3, format: currencyCell },
458
+ ...CHANGE_BASIS_EXPORT_COLUMNS,
428
459
  ];
429
460
  }
430
461
 
431
- function errorMessage(error: unknown): string | null {
432
- if (error == null) return null;
433
- return error instanceof Error ? error.message : String(error);
434
- }
435
-
436
462
  /** What the Feed cell says: stale wins over the feed's own live or delayed, as the closing line counts it. */
437
463
  function quoteFeedState(quote: QuoteCliRecord["quote"], now: number): "stale" | "live" | "delayed" | "" {
438
464
  if (!quote) return "";
@@ -455,6 +481,34 @@ function nearParCoin(quote: QuoteCliRecord["quote"]): boolean {
455
481
  return reference != null && reference >= 0.9 && reference < 1.1;
456
482
  }
457
483
 
484
+ /** What a row's move is measured from, worded as its Chg Basis cell: empty without a move. */
485
+ function rowChangeBasis(result: Pick<QuoteCliRecord, "quote" | "futuresContract">) {
486
+ return quoteChangeBasis(result.quote, { terms: result.futuresContract });
487
+ }
488
+
489
+ function basisLabel(result: Pick<QuoteCliRecord, "quote" | "futuresContract">, now: number): string {
490
+ const basis = rowChangeBasis(result);
491
+ return basis ? changeBasisLabel(basis, now) : "";
492
+ }
493
+
494
+ /**
495
+ * The lines under the table naming the days a row's venue was closed since
496
+ * the close its move is from ("HKEX closed Mon 19 Oct, holiday"), once each.
497
+ */
498
+ function changeBasisNotes(results: QuoteCliRecord[], now = Date.now()): string[] {
499
+ return [...new Set(results.flatMap((result) => changeBasisNote(rowChangeBasis(result), now) ?? []))];
500
+ }
501
+
502
+ /** The quote as `--json` gives it: `previousCloseDate`, the exchange-local date of its previous close, follows `previousClose`. */
503
+ function withPreviousCloseDate(quote: Quote | null | undefined): (Quote & { previousCloseDate?: string | null }) | null {
504
+ if (!quote) return null;
505
+ const previousCloseDate = quotePreviousCloseDate(quote);
506
+ const entries = Object.entries(quote);
507
+ const at = entries.findIndex(([key]) => key === "previousClose");
508
+ entries.splice(at < 0 ? entries.length : at + 1, 0, ["previousCloseDate", previousCloseDate]);
509
+ return Object.fromEntries(entries) as Quote & { previousCloseDate?: string | null };
510
+ }
511
+
458
512
  function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
459
513
  return results.map((result) => {
460
514
  const quote = result.quote;
@@ -477,10 +531,12 @@ function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
477
531
  ? indexPoints ? formatMarketPrice(value, options) : formatMarketPriceWithCurrency(value, quote.currency, options)
478
532
  : ""
479
533
  );
534
+ // A price in cents says what it is per, as the contract terms word it: 314.95¢/lb.
535
+ const per = display?.price != null ? minorUnitPricePer(result.futuresContract, quote?.currency) : "";
480
536
  return {
481
537
  symbol: result.target.symbol,
482
538
  name: quote?.name || result.listing?.name || "",
483
- price: price(display?.price),
539
+ price: `${price(display?.price)}${per}`,
484
540
  rawPrice: display?.price ?? null,
485
541
  priceBasis: quote?.priceBasis ?? null,
486
542
  instrumentType: quote?.instrumentType ?? null,
@@ -512,6 +568,9 @@ function quoteRows(results: QuoteCliRecord[], now = Date.now()) {
512
568
  // When the price Last shows was observed: in a pre-market nothing has traded in, the close it stands at.
513
569
  updatedAt: quote?.lastUpdated ? new Date(quoteHeadlineTime(quote)).toISOString() : "",
514
570
  error: result.error ?? "",
571
+ // Last, so a row read by position keeps every field where it was.
572
+ changeBasis: basisLabel(result, now),
573
+ changeBasisNote: changeBasisNote(rowChangeBasis(result), now) ?? "",
515
574
  };
516
575
  });
517
576
  }
@@ -783,7 +842,9 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
783
842
  listing: listingMetadata(listingIdentity(listing, result.quote)),
784
843
  // The figures the table shows, beside the quote as the source sent it.
785
844
  ...getQuoteSessionFields(result.quote),
786
- quote: result.quote,
845
+ // What `change` is measured from, with the label and holiday note the table shows.
846
+ changeBasis: changeBasisJson(quoteChangeBasis(result.quote, { terms: contracts[index] }), now),
847
+ quote: withPreviousCloseDate(result.quote),
787
848
  ...contracts[index] ? { futuresContract: contracts[index] } : {},
788
849
  ...fronts[index] ? { futuresFront: fronts[index] } : {},
789
850
  ...perpetualContracts[index] ? { perpetualContract: perpetualContracts[index] } : {},
@@ -809,6 +870,7 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
809
870
  ...ctx.cliOptions.format === "text" ? quoteNotes(data, { exchange }) : [],
810
871
  ];
811
872
  const columns = commandName === "compare" ? compareColumns() : quoteColumns();
873
+ const basisNotes = changeBasisNotes(data, now);
812
874
  if (ctx.cliOptions.format !== "json") await primeBoardCoins(notes);
813
875
  // Only yield indices: their change is in bp, so the text header drops the percent sign (CSV keeps the percent it holds).
814
876
  const yieldsOnly = data.length > 0 && data.every((row) => isYieldIndexSymbol(row.target.symbol));
@@ -826,8 +888,11 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
826
888
  currencyOrUnit: row.currency || (row.yieldIndex ? "pct" : isIndexLevel(row.instrumentType) ? INDEX_POINTS_EXPORT_UNIT : null),
827
889
  }),
828
890
  details: quoteDetailsText,
891
+ ...(basisNotes.length ? { footnote: basisNotes.join("\n") } : {}),
829
892
  columns,
830
- ...(yieldsOnly ? { textColumns: columns.map((column) => column.key === "changePercent" ? { ...column, header: "Chg" } : column) } : {}),
893
+ textColumns: quoteTextColumns(yieldsOnly
894
+ ? columns.map((column) => column.key === "changePercent" ? { ...column, header: "Chg" } : column)
895
+ : columns),
831
896
  });
832
897
  // The table names each symbol it found nothing for; with none found, the exit status says so too.
833
898
  if (data.every((row) => !row.quote)) process.exitCode = CLI_EXIT_CODES.error;
@@ -864,15 +929,11 @@ function quoteDetailsText(data: QuoteCliRecord[]): string {
864
929
 
865
930
  /**
866
931
  * "APPL is not a US ticker: it is Aptus Pharma Limited on BSE (INR). Did you
867
- * mean AAPL (Apple Inc.)?", for a symbol read on a listing abroad that is one
868
- * slip from a US ticker, worded as the not-a-ticker answers word it.
932
+ * mean AAPL (Apple Inc.)?" in the warning colour, for a row that has one.
869
933
  */
870
- function notUsTickerLine({ target, quote, listing, usSuggestion }: QuoteCliRecord): string | null {
871
- if (!quote || !usSuggestion) return null;
872
- const venue = canonicalExchange(quote.listingExchangeName || quote.exchangeName);
873
- const name = quote.name || listing?.name;
874
- const what = `${name ? `${name} on ` : ""}${venue}${quote.currency ? ` (${quote.currency})` : ""}`;
875
- return cliStyles.warning(`${target.symbol} is not a US ticker: it is ${what}. Did you mean ${describeSuggestion(usSuggestion)}?`);
934
+ function notUsTickerLine(row: QuoteCliRecord): string | null {
935
+ const sentence = notUsTickerSentence(row);
936
+ return sentence && cliStyles.warning(sentence);
876
937
  }
877
938
 
878
939
  /** Each futures row's front contract quote, when its terms name a front other than its own contract; null otherwise. */
@@ -903,12 +964,17 @@ function quoteFuturesContractText(data: QuoteCliRecord[]): string {
903
964
  const terms = row.futuresContract;
904
965
  if (!terms || !row.quote) return [];
905
966
  const settle: [string, string] = ["Settle", futuresSettleText(terms, row.quote)];
967
+ // The move Chg% gives, in the quote's money and with what it is measured from: "vs Fri settle".
968
+ const session = getRegularSessionDisplay(row.quote);
969
+ const change: Array<[string, string]> = session?.change != null && Number.isFinite(session.change)
970
+ ? [["Change", quoteChangeText(row.quote, session, withCurrencyMinorDigits(quoteFormatOptions(row.quote, "FUTURE"), row.quote.currency), terms)]]
971
+ : [];
906
972
  const metrics = futuresContractMetrics(terms, row.quote.symbol || row.target.symbol);
907
973
  const rolls = futuresRollLines(terms, row.futuresFront).map((line): [string, string] => [line.label, line.value]);
908
974
  const at = futuresContractLines(terms, row.quote.symbol || row.target.symbol)[0]?.key === "contract" ? 1 : 0;
909
975
  const contract = [...metrics.slice(0, at), ...rolls, ...metrics.slice(at)];
910
- if (data.length > 1) return [`${renderSection(`${row.target.symbol} Contract`)}\n${renderStats([settle, ...contract])}`];
911
- return [[renderStats([settle]), ...contract.length ? ["", renderSection("Contract"), renderStats(contract)] : []].join("\n")];
976
+ if (data.length > 1) return [`${renderSection(`${row.target.symbol} Contract`)}\n${renderStats([settle, ...change, ...contract])}`];
977
+ return [[renderStats([settle, ...change]), ...contract.length ? ["", renderSection("Contract"), renderStats(contract)] : []].join("\n")];
912
978
  }).join("\n\n");
913
979
  }
914
980
 
@@ -923,91 +989,6 @@ function yieldIndexFields(result: QuoteBatchResult): { unit?: "percent"; changeB
923
989
  return { unit: "percent", ...(change == null ? {} : { changeBasisPoints: toBasisPoints(change, 1) }) };
924
990
  }
925
991
 
926
- interface QuoteAnswer {
927
- result: QuoteBatchResult;
928
- /** The listing the row is: the one asked for, or the venue a bare symbol was quoted on. */
929
- listing: CliListing;
930
- /** Why there is no quote, when there is more to say than the source's own error. */
931
- failure?: { message: string; details?: string; oneLine: string };
932
- notes: string[];
933
- }
934
-
935
- /**
936
- * One symbol's row of `quote` and `compare`. A listing on an exchange its symbol
937
- * is not listed on says so. A bare symbol the data service read on its home
938
- * listing abroad names that venue (SXR8 -> XETRA). One no source quotes is
939
- * quoted on its listings (`quoteBareListing`), else names them, or asks for an
940
- * exchange when several companies list it (2222); a bare index root (VIX) also
941
- * names its index (^VIX), and a futures board code a security keeps (GC) its
942
- * future (GC=F). Venues are only looked up for a
943
- * symbol with no quote, so a request that succeeds costs no search.
944
- */
945
- async function answerQuote(
946
- result: QuoteBatchResult,
947
- listing: CliListing,
948
- market: MarketContext,
949
- request: ListingDataRequest,
950
- refresh: boolean,
951
- venues: Promise<ListingVenue[]> | null,
952
- ): Promise<QuoteAnswer> {
953
- const root = listing.exchange ? null : indexRootFor(listing.symbol);
954
- // A bare root that quoted something else, such as MOVE (Corvex), points at the index too.
955
- const also = (quote: Quote) => root && quote.instrumentType?.trim().toUpperCase() !== "INDEX"
956
- ? [indexRootAlsoLine(root, request.command)] : [];
957
- if (result.quote) {
958
- // "CAR = Avis Budget Group, NASDAQ (also XETRA, ASX)" names the venue too, so it replaces "SXR8 -> XETRA".
959
- const choice = venues ? await listingChoiceNotes(listing, market, result.quote, venues) : listingReadNotes(listing);
960
- const venue = choice.length > 0 ? null : bareListingNote(listing, result.quote);
961
- // A row saved as 247540:KRX is quoted from KOSDAQ, the board it trades on.
962
- const board = tradingBoardNote(listing, result.quote);
963
- return { result, listing, notes: [...choice, venue ?? [], board ?? [], also(result.quote)].flat() };
964
- }
965
- const notTraded = await notTradedMessage(listing, market);
966
- if (notTraded) return { result, listing, failure: { message: notTraded, oneLine: notTraded }, notes: [] };
967
- if (!isNoProviderError(result.error)) return { result, listing, notes: [] };
968
- const other = await quoteBareListing(listing, market, request, { refresh });
969
- if (other?.kind === "quoted") {
970
- return {
971
- result: { target: other.listing.request, quote: other.quote },
972
- listing: other.listing,
973
- notes: [other.note, ...also(other.quote)],
974
- };
975
- }
976
- // Other listings already name a futures board code's future (GC=F); with none, it is named here.
977
- // A Treasury yield's root (TNX) names its FRED series and the curve beside its index.
978
- const yieldTenor = root ? treasuryYieldTenor(listing.symbol) : null;
979
- const tryLines = [
980
- yieldTenor ? treasuryYieldLine(yieldTenor) : root ? indexRootTryLine(root, request.command) : null,
981
- other || listing.exchange ? null : futuresTryLine(listing.symbol, request),
982
- // BDI asks which exchange (Black Diamond Group) and says nowhere that the Baltic Dry Index is not here.
983
- listing.exchange ? null : knownGapForTicker(listing.symbol),
984
- ].filter((line): line is string => !!line);
985
- if (!other && tryLines.length === 0) return { result: await withCurrencyPairHint(result, market), listing, notes: [] };
986
- const message = other?.message ?? `${listing.symbol} has no ${request.noun}.`;
987
- const details = [other?.details, ...tryLines].filter(Boolean).join("\n");
988
- const oneLine = [
989
- message,
990
- other ? `${other.heading}: ${other.listings.join(", ")}.` : "",
991
- ...tryLines,
992
- ].filter(Boolean).join(" ");
993
- return { result, listing, failure: { message, details, oneLine }, notes: [] };
994
- }
995
-
996
- /**
997
- * A symbol written like a currency pair that is not a ticker (USD_NGN, as some
998
- * FX feeds write a pair) names the listing `gloomberb search` finds nearest to
999
- * it, as `watchlist add` does: "Did you mean USDNGN=X (US Dollar/Nigerian
1000
- * Naira)?". When search finds nothing near, the row keeps its own error and its
1001
- * search hint.
1002
- */
1003
- async function withCurrencyPairHint(result: QuoteBatchResult, market: MarketContext): Promise<QuoteBatchResult> {
1004
- const message = errorMessage(result.error);
1005
- const symbol = message ? notATickerSymbol(message) : null;
1006
- if (!message || !symbol || !looksLikeCurrencyPair(symbol)) return result;
1007
- const suggestion = await suggestListing(symbol, market);
1008
- return suggestion ? { ...result, error: withListingSuggestion(message, describeSuggestion(suggestion)) } : result;
1009
- }
1010
-
1011
992
  /** A crypto pair that will not load points at the crypto board. Structured formats keep the error as thrown. */
1012
993
  function failHistory(error: unknown, symbol: string, ctx: Parameters<CliCommandDef["execute"]>[1]): never {
1013
994
  if (ctx.cliOptions.format !== "text" || !isCryptoPairSymbol(symbol)) throw error;
@@ -1016,6 +997,7 @@ function failHistory(error: unknown, symbol: string, ctx: Parameters<CliCommandD
1016
997
 
1017
998
  const HISTORY_USAGE = "history <symbol> [--range <range>]";
1018
999
  const HISTORY_ALL_RANGE = `ALL gives the full history the source has, up to ${historyAllRangeYears()} years`;
1000
+ const HISTORY_YTD_RANGE = "YTD starts at the prior year's last close, the one year-to-date returns are measured from";
1019
1001
  /** Options people reach for to ask for dates, which history answers with a range. */
1020
1002
  const DATE_WINDOW_OPTIONS = new Set(["--from", "--to", "--start", "--end", "--since", "--until", "--date", "--start-date", "--end-date"]);
1021
1003
  /** Options people reach for to pick a bar size, which history derives from the range. */
@@ -1052,14 +1034,23 @@ function financialsOptionHint(flag: string, args: readonly string[]): string | n
1052
1034
  }
1053
1035
 
1054
1036
  function historyOptionHint(flag: string, args: readonly string[]): string | null {
1055
- if (DATE_WINDOW_OPTIONS.has(flag)) return `history takes a --range instead of dates: ${TIME_RANGES.join(", ")}. ${HISTORY_ALL_RANGE}.`;
1037
+ if (DATE_WINDOW_OPTIONS.has(flag)) return `history takes a --range instead of dates: ${HISTORY_RANGES.join(", ")}. ${HISTORY_YTD_RANGE}; ${HISTORY_ALL_RANGE}.`;
1056
1038
  if (BAR_SIZE_OPTIONS.has(flag)) return `history serves each --range in one bar size. ${subMinuteDataMessage(historySymbolArg(args))}`;
1057
1039
  return null;
1058
1040
  }
1059
1041
 
1042
+ /** "Asked YTD, data starts 2026-03-05 (first available bar)" when no bar reaches the prior year's last close. */
1043
+ function ytdHistoryNotes(rows: readonly { date: string }[]): string[] {
1044
+ const first = rows[0]?.date.slice(0, 10);
1045
+ const last = rows.at(-1)?.date.slice(0, 10);
1046
+ return first && last && first > `${Number(last.slice(0, 4)) - 1}-12-31` ? [`Asked YTD, data starts ${first} (first available bar)`] : [];
1047
+ }
1048
+
1060
1049
  async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
1061
1050
  const args = [...rawArgs];
1062
1051
  const range = parseRange(takeOption(args, "--range"), ctx);
1052
+ // YTD reads the year's daily bars and starts at the last one on or before 31 Dec.
1053
+ const loadRange: TimeRange = range === "YTD" ? "1Y" : range;
1063
1054
  const requestedExchange = takeOption(args, "--exchange");
1064
1055
  const raw = requireOneArg(args, HISTORY_USAGE, "symbol", ctx);
1065
1056
  await withMarketData(ctx, async (market) => {
@@ -1068,8 +1059,8 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1068
1059
  const { symbol, exchange } = listing.request;
1069
1060
  const context = { cacheMode: ctx.cliOptions.refresh ? "refresh" as const : "default" as const };
1070
1061
  const load = () => market.dataProvider.getPriceHistoryWithMetadata
1071
- ? market.dataProvider.getPriceHistoryWithMetadata(symbol, exchange, range, context)
1072
- : market.dataProvider.getPriceHistory(symbol, exchange, range, context).then((points): PriceHistoryResult => ({ points, resolution: null }));
1062
+ ? market.dataProvider.getPriceHistoryWithMetadata(symbol, exchange, loadRange, context)
1063
+ : market.dataProvider.getPriceHistory(symbol, exchange, loadRange, context).then((points): PriceHistoryResult => ({ points, resolution: null }));
1073
1064
  const quoted = loadListingQuote(market.dataProvider, listing);
1074
1065
  const fate = listingFateLoader(listing, market, quoted);
1075
1066
  const [loaded, quote] = await Promise.all([
@@ -1092,7 +1083,8 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1092
1083
  };
1093
1084
  // A coin's volume is traded value in its quote currency: said in every row and on the status line.
1094
1085
  const volume = historyVolume(unit, traded);
1095
- const served = historyRows(points, resolution, unit.currency, unit.unit, volume);
1086
+ const rows = historyRows(points, resolution, unit.currency, unit.unit, volume);
1087
+ const served = range === "YTD" ? rows.slice(Math.max(0, yearToDateStartIndex(rows.map((row) => row.date), rows[0]?.interval))) : rows;
1096
1088
  // Bars stop arriving on the listing's own trading days, not New York's.
1097
1089
  const venue = quote?.listingExchangeName || exchange || undefined;
1098
1090
  // October's monthly bar on 16 Oct runs through the 15th: marked partial, and the status line says how far.
@@ -1122,7 +1114,7 @@ async function runHistory(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1122
1114
  // FB's bars are the fund listed there now; META's from 2021 are from when it was FB.
1123
1115
  ...listingFateNotes(await fate(), { served: data.length > 0, firstDate: data[0]?.date }),
1124
1116
  ...await listingChoiceNotes(listing, market, quote, venues),
1125
- ...historyNotes(range, interval, data[0]?.date), historyCoverageWarning(range, loaded) ?? [],
1117
+ ...range === "YTD" ? ytdHistoryNotes(data) : [...historyNotes(range, interval, data[0]?.date), historyCoverageWarning(range, loaded) ?? []],
1126
1118
  // The status line says how far a bar still forming runs; the note is for the bars it does not name.
1127
1119
  gaps ?? [], historyJumpNote(data) ?? [], historyFlagNote(data) ?? [], historyMissingVolumeNote(data) ?? [],
1128
1120
  historyBarStatusNote(forming ? served : data) ?? [],
@@ -1283,7 +1275,9 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
1283
1275
  symbol: identity.symbol || listing.key, bond, rates, treasury: await treasury,
1284
1276
  distribution: distributionBasis(priced.fundamentals?.dividendYield, priced.fundamentals?.dividendYieldBasis === "trailing" ? "trailing" : "forward"),
1285
1277
  }) : null;
1286
- const report = { ...data, distributions: await distributions, statementFilings, bank: bank.bank, bankNote: bank.note, bondFund: bond, taxEquivalent };
1278
+ const report = { ...data, distributions: await distributions, statementFilings, bank: bank.bank, bankNote: bank.note, bondFund: bond, taxEquivalent,
1279
+ // A base currency other than USD reads every amount with its code, as `ticker` does.
1280
+ currencyCodes: writesCurrencyCodes(market.config) };
1287
1281
  ctx.printResult({
1288
1282
  // JSON reads a multiple over a loss as null with its reason, never as a number.
1289
1283
  data: {
@@ -1359,22 +1353,33 @@ const NEWS_COLUMNS = [
1359
1353
  { key: "url", header: "URL", optional: true },
1360
1354
  ];
1361
1355
 
1362
- /** Stories matching text, printed as a symbol's news is, under a line saying what they match. */
1356
+ /**
1357
+ * Stories matching text, printed as a symbol's news is, under a line saying
1358
+ * what they match. A text naming a commodity that matched nothing says so and
1359
+ * offers its future's news when that has stories; `commodity: false` for the
1360
+ * future's own news, which is that route. A search that failed (`failure`,
1361
+ * its message) says so, with `empty`, the lookup's own answer, rather than
1362
+ * that no story matches.
1363
+ */
1363
1364
  async function printNewsSearch(
1364
1365
  text: string,
1365
1366
  articles: NewsArticle[],
1366
- { reason, empty }: { reason?: string; empty?: string },
1367
+ { reason, empty, commodity = true, failure }: { reason?: string; empty?: string; commodity?: boolean; failure?: string },
1367
1368
  ctx: Parameters<CliCommandDef["execute"]>[1],
1368
1369
  ) {
1370
+ const searched = failure === undefined;
1371
+ const named = searched && commodity && articles.length === 0 && commodityNamedIn(text) !== null;
1372
+ const route = named ? await commodityNewsRoute(text) : null;
1369
1373
  ctx.printResult({
1370
1374
  data: articles,
1371
- metadata: { ticker: null, search: text, feed: null },
1375
+ ...(searched ? {} : { warnings: [`Searching stories for "${text}" failed: ${failure}`] }),
1376
+ metadata: { ticker: null, search: text, feed: null, ...(route ? { futuresNews: route.command } : {}) },
1372
1377
  freshness: rowsFreshness(newsRows(articles), cloudNewsFreshness(await cloudRealtimeAccess())),
1373
1378
  }, {
1374
1379
  heading: newsSearchHeading(text, reason),
1375
1380
  rows: newsRows,
1376
1381
  columns: NEWS_COLUMNS,
1377
- empty: empty ?? `No stories match "${text}".`,
1382
+ empty: named ? noStoriesMessage(text, route) : empty ?? (searched ? noStoriesMessage(text, null) : "No stories to show."),
1378
1383
  });
1379
1384
  }
1380
1385
 
@@ -1409,18 +1414,29 @@ async function runNews(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
1409
1414
  ]);
1410
1415
  const identity = listing ? listingIdentity(listing, quote) : null;
1411
1416
  const resolved = quote !== null || identity?.name != null;
1417
+ const unfiled = !!listing && !listing.exchange && !feed && articles.length === 0;
1418
+ // A future's news is its commodity's: news is filed under company tickers, so KC=F (or a bare code
1419
+ // no security keeps, CT) searches the stories of coffee, which the service matches to KC=F's links too.
1420
+ const future = unfiled && isFuturesInstrument(listing!.symbol, quote?.instrumentType)
1421
+ ? futuresBoardContract(listing!.symbol) ?? bareFuturesCode(args[0]) : null;
1422
+ const commodity = future ? commoditySearchName(future) : null;
1412
1423
  // A bare word no listing answers to is text, and so is a futures board code (TTF), which resolves
1413
- // to its future: news is filed under company tickers, so the stories that mention the word are the news.
1414
- const word = listing && !listing.exchange && !feed && articles.length === 0 ? newsFallbackText(args[0]!) : null;
1424
+ // to its future: the stories that mention the word are the news.
1425
+ const word = commodity ?? (unfiled ? newsFallbackText(args[0]!) : null);
1415
1426
  const futuresCode = !!word && !!bareFuturesCode(word) && isFuturesInstrument(listing!.symbol, quote?.instrumentType);
1416
- const reason = !resolved ? "no ticker by that name" : futuresCode ? "a futures code, not a ticker" : null;
1427
+ const reason = commodity ? `the commodity ${future!.symbol} trades`
1428
+ : !resolved ? "no ticker by that name" : futuresCode ? "a futures code, not a ticker" : null;
1417
1429
  const emptyMessage = (searched: boolean) => listing && identity
1418
1430
  ? newsEmptyMessage(searched && word ? word : listing.symbol, listingTitle(identity), quote, resolved, searched)
1419
1431
  : undefined;
1420
1432
  if (word && reason) {
1421
1433
  // The lookup that found no ticker already answered; a search that fails leaves that answer.
1422
- const found = await searchNewsStories(word, limit).catch(logFailure("cli.commands-market", "Search news stories failed", []));
1423
- await printNewsSearch(word, found, { reason, empty: emptyMessage(true) }, ctx);
1434
+ const search = await searchNewsStories(word, limit).then((found) => ({ found, failure: undefined }), (error: unknown) => {
1435
+ logFailure("cli.commands-market", "Search news stories failed")(error);
1436
+ return { found: [], failure: errorMessage(error) ?? "no answer" };
1437
+ });
1438
+ const searched = search.failure === undefined;
1439
+ await printNewsSearch(word, search.found, { reason, empty: emptyMessage(searched && !commodity), commodity: !commodity, failure: search.failure }, ctx);
1424
1440
  return;
1425
1441
  }
1426
1442
  const empty = emptyMessage(false);
@@ -2009,11 +2025,109 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
2009
2025
  /** What an options chain export's `interval` says: one quote snapshot, not bars. */
2010
2026
  const OPTIONS_EXPORT_INTERVAL = "snapshot";
2011
2027
 
2028
+ /** `fx --date`: a past day as YYYY-MM-DD; today's close is not in yet, so it is refused, as is a day ahead. */
2029
+ function readFxDate(value: string, ctx: Parameters<CliCommandDef["execute"]>[1]): string {
2030
+ let date: string;
2031
+ try {
2032
+ date = isoDateOption(value, "date");
2033
+ } catch (error) {
2034
+ return ctx.fail(error instanceof Error ? error.message : String(error));
2035
+ }
2036
+ if (!date) return ctx.fail("--date takes a day, such as --date 2026-09-02.");
2037
+ const today = isoDateToday();
2038
+ if (date >= today) {
2039
+ return ctx.fail(`${date} has no daily close yet: --date takes a day before today (${today}).`, "Without --date, fx gives the latest rate.");
2040
+ }
2041
+ return date;
2042
+ }
2043
+
2044
+ /**
2045
+ * `fx <pair> --date <day>`: the rate at that day's daily FX closes, read as
2046
+ * a position's purchase-date cost reads them (`fxCloseOnDate`): each
2047
+ * currency's USD pair at its close of the day, or of the last trading day
2048
+ * before it, which the answer then says, crossed through USD as the latest
2049
+ * rate is.
2050
+ */
2051
+ async function runFxOnDate(
2052
+ parsed: Exclude<ReturnType<typeof parseFxArgs>, { error: string }>,
2053
+ date: string,
2054
+ args: readonly string[],
2055
+ market: MarketContext,
2056
+ ctx: Parameters<CliCommandDef["execute"]>[1],
2057
+ ) {
2058
+ const { request, amount, toUsdByDefault } = parsed;
2059
+ const { currency, baseCurrency } = request;
2060
+ const pair = `${currency}/${baseCurrency}`;
2061
+ const history = market.fxHistory ?? apiClient;
2062
+ const codes = [...new Set([currency, baseCurrency].filter((code) => code !== "USD"))];
2063
+ const legs = new Map<string, FxCloseReading>([["USD", { rate: 1, date }]]);
2064
+ if (currency !== baseCurrency) {
2065
+ for (const code of codes) {
2066
+ const leg = dailyFxLeg(code);
2067
+ if (!leg) return ctx.fail(`${code} has no daily FX closes to read a rate for ${date} from.`);
2068
+ let reading: FxCloseReading | null;
2069
+ try {
2070
+ reading = await fxCloseOnDate(leg, date, history);
2071
+ } catch (error) {
2072
+ return ctx.fail(`The daily ${code} FX closes could not be read.`, error instanceof Error ? error.message : String(error));
2073
+ }
2074
+ if (!reading) {
2075
+ return ctx.fail(`No ${leg.invert ? `USD/${code}` : `${code}/USD`} daily close in the week up to ${date}.`,
2076
+ "Without --date, fx gives the latest rate.");
2077
+ }
2078
+ legs.set(code, reading);
2079
+ }
2080
+ }
2081
+ const from = legs.get(currency), into = legs.get(baseCurrency);
2082
+ const rate = currency === baseCurrency ? 1 : crossRate(from?.rate, into?.rate);
2083
+ // A cross is as old as its older close; a weekend or holiday reads the trading day before it.
2084
+ const closeDate = [...legs.values()].map((leg) => leg.date).sort()[0]!;
2085
+ const converted = amount == null ? null : amount * rate;
2086
+ const row = {
2087
+ currency, baseCurrency, rate, asOf: closeDate, stale: false, pair, inverse: 1 / rate, date,
2088
+ ...(amount == null ? {} : { amount, converted }),
2089
+ };
2090
+ const warnings = [
2091
+ ...request.readAs ?? [],
2092
+ ...closeDate < date ? [`No daily close on ${date}, a weekend or market holiday: this is the close of ${closeDate}, the trading day before it.`] : [],
2093
+ ...toUsdByDefault && amount != null
2094
+ ? [`No target currency given, so ${currency} is converted to USD; for another, write fx ${args[0]!.trim()} ${currency}/<to> --date ${date}.`]
2095
+ : [],
2096
+ ];
2097
+ ctx.printResult({
2098
+ data: [row],
2099
+ // A past close is what was asked for, never stale.
2100
+ freshness: rowsFreshness([], { status: "not-a-feed", basis: "daily FX close", asOf: closeDate }),
2101
+ warnings: warnings.length > 0 ? warnings : undefined,
2102
+ }, {
2103
+ text: () => amount == null ? describeFxRate(request, rate) : `${describeFxConversion(request, amount, rate)}\n${describeFxRate(request, rate)}`,
2104
+ exportIdentity: { symbolOrSeries: pair, currencyOrUnit: baseCurrency, interval: "1day" },
2105
+ columns: [
2106
+ { key: "currency", header: "Currency" },
2107
+ { key: "baseCurrency", header: "Base" },
2108
+ { key: "rate", header: "Rate", align: "right" },
2109
+ { key: "date", header: "Date" },
2110
+ { key: "asOf", header: "Close" },
2111
+ { key: "stale", header: "Stale" },
2112
+ { key: "pair", header: "Pair" },
2113
+ { key: "inverse", header: "Inverse", align: "right" },
2114
+ ...(amount == null ? [] : [
2115
+ { key: "amount", header: "Amount", align: "right" as const },
2116
+ { key: "converted", header: "Converted", align: "right" as const },
2117
+ ]),
2118
+ ],
2119
+ });
2120
+ }
2121
+
2012
2122
  async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
2013
- if (rawArgs.length === 0) ctx.fail(`Usage: gloomberb ${FX_USAGE}`);
2123
+ const args = [...rawArgs];
2124
+ const dateOption = takeOption(args, "--date");
2125
+ if (args.length === 0) ctx.fail(`Usage: gloomberb ${FX_USAGE}`);
2126
+ const date = dateOption == null ? null : readFxDate(dateOption, ctx);
2014
2127
  await withMarketData(ctx, async (market) => {
2015
- const parsed = parseFxArgs(rawArgs, market.config.baseCurrency);
2128
+ const parsed = parseFxArgs(args, market.config.baseCurrency);
2016
2129
  if ("error" in parsed) return ctx.fail(parsed.error, `Usage: gloomberb ${FX_USAGE}`);
2130
+ if (date) return runFxOnDate(parsed, date, args, market, ctx);
2017
2131
  const { request, amount, toUsdByDefault } = parsed;
2018
2132
  const { currency, baseCurrency } = request;
2019
2133
  const pair = `${currency}/${baseCurrency}`;
@@ -2053,10 +2167,14 @@ async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]
2053
2167
  }] as const] : [];
2054
2168
  })));
2055
2169
  // An amount with one currency names no target; the answer says it went to USD.
2056
- const warnings = toUsdByDefault && amount != null
2057
- ? [`No target currency given, so ${currency} is converted to USD; for another, write fx ${rawArgs[0]!.trim()} ${currency}/<to>.`]
2058
- : undefined;
2059
- ctx.printResult({ data: [row], freshness, warnings }, {
2170
+ const warnings = [
2171
+ // A currency typed as its name (yen) says which code it was read as.
2172
+ ...request.readAs ?? [],
2173
+ ...toUsdByDefault && amount != null
2174
+ ? [`No target currency given, so ${currency} is converted to USD; for another, write fx ${args[0]!.trim()} ${currency}/<to>.`]
2175
+ : [],
2176
+ ];
2177
+ ctx.printResult({ data: [row], freshness, warnings: warnings.length > 0 ? warnings : undefined }, {
2060
2178
  // The conversion, then the rate it used; the source line under it dates that rate.
2061
2179
  text: () => amount == null ? describeFxRate(request, rate) : `${describeFxConversion(request, amount, rate)}\n${describeFxRate(request, rate)}`,
2062
2180
  // The rate is the base currency's price of one unit of the other, at the latest quote of each leg.
@@ -2171,7 +2289,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
2171
2289
  group: CLI_COMMAND_GROUPS.research,
2172
2290
  usage: [HISTORY_USAGE],
2173
2291
  options: [
2174
- { flags: "--range <range>", description: `${TIME_RANGES.join(", ")} (default 1Y); ${HISTORY_ALL_RANGE}` },
2292
+ { flags: "--range <range>", description: `${HISTORY_RANGES.join(", ")} (default 1Y); ${HISTORY_YTD_RANGE}; ${HISTORY_ALL_RANGE}` },
2175
2293
  EXCHANGE_OPTION,
2176
2294
  ],
2177
2295
  sections: [{
@@ -2437,11 +2555,15 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
2437
2555
  description: "Convert a currency into your base currency, quote a pair such as USD/NGN, or convert an amount such as fx 1234.5 EUR/USD",
2438
2556
  help: {
2439
2557
  group: CLI_COMMAND_GROUPS.markets,
2440
- usage: ["fx <currency>", "fx <base>/<quote>", "fx <amount> <from>/<to>", "fx <amount> <from> <to>"],
2558
+ usage: ["fx <currency>", "fx <base>/<quote>", "fx <amount> <from>/<to>", "fx <amount> <from> <to>", "fx <pair> --date <day>"],
2559
+ options: [
2560
+ { flags: "--date <day>", description: `The rate at that day's daily FX close, such as a transaction date (${DATE_OPTION_EXAMPLES}); a weekend or holiday reads the trading day before it` },
2561
+ ],
2441
2562
  sections: [{
2442
2563
  title: "Direction",
2443
2564
  lines: [
2444
2565
  "A bare code is one unit of it in your base currency. A pair reads as markets quote it: USD/NGN is how many NGN one USD buys. Two codes neither of which is USD, such as ZAR/NGN, cross through their USD rates.",
2566
+ "A currency's name stands for its code: fx yen is JPY, fx rand ZAR, fx Swiss franc CHF, and the answer says which it read. A name several currencies share (peso, dollar, franc, krona, rupee) lists them: write one as its code.",
2445
2567
  "The answer states both directions: 1 NGN = 0.000752791 USD (USD/NGN 1328.39). --json, --csv and --ndjson carry rate (base currency per currency), pair, inverse, asOf and stale.",
2446
2568
  ],
2447
2569
  }, {
@@ -2449,8 +2571,13 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
2449
2571
  lines: [
2450
2572
  "An amount in front converts it from the first currency to the second: fx 1234.5 EUR/USD, or fx 1234,5 EUR USD. It takes either decimal mark (1.234,5 and 1,234.5 are 1234.5) and reads as an amount of the first currency: 1,234 or 1.234 EUR are thousands, a third decimal being below a cent, while in a currency with three decimals (KWD) fx asks which. An amount and one currency converts to USD. The answer gives the converted amount, then the rate it used, dated by the source line; --json adds amount and converted.",
2451
2573
  ],
2574
+ }, {
2575
+ title: "A past day",
2576
+ lines: [
2577
+ "--date gives the rate at that day's daily FX close instead of the latest: fx USD/CAD --date 2026-09-02, or with an amount, fx 1250 USD/CAD --date 2026-09-02. Each currency's USD pair is read at its close of that day and crossed through USD, as the latest rate is. A weekend or market holiday has no close: the answer then gives the trading day before it and says so. The source line dates the close; --json adds date (the day asked for) and asOf (the close used).",
2578
+ ],
2452
2579
  }],
2453
- examples: ["fx EUR", "fx NGN", "fx USD/NGN", "fx ZAR/NGN", "fx 1234.5 EUR/USD", "fx 1234,5 EUR USD", "fx JPY --json"],
2580
+ examples: ["fx EUR", "fx NGN", "fx yen", "fx USD/NGN", "fx ZAR/NGN", "fx 1234.5 EUR/USD", "fx 1234,5 EUR USD", "fx USD/CAD --date 2026-09-02", "fx JPY --json"],
2454
2581
  },
2455
2582
  execute: runFx,
2456
2583
  },