gloomberb 0.17.0 → 0.17.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 (201) hide show
  1. package/package.json +1 -1
  2. package/src/{plugins/builtin/cloud → account}/auth-dialog.tsx +7 -7
  3. package/src/api-client/data.ts +5 -1
  4. package/src/api-client/doe.ts +19 -0
  5. package/src/api-client/earnings.ts +4 -2
  6. package/src/api-client/index.ts +1 -0
  7. package/src/app/global-shortcuts.ts +10 -0
  8. package/src/app/keybindings/actions.ts +16 -0
  9. package/src/app/runtime/desktop-deeplink.ts +1 -1
  10. package/src/app/runtime/update.ts +16 -0
  11. package/src/app.tsx +1 -1
  12. package/src/architecture/core-builtin-allowlist.json +4 -9
  13. package/src/architecture/dom-cell-metrics-allowlist.json +100 -26
  14. package/src/cli/commands/market.ts +87 -17
  15. package/src/cli/commands/options-chain.ts +93 -23
  16. package/src/cli/commands/options-leaps.ts +462 -63
  17. package/src/cli/commands/search.ts +10 -7
  18. package/src/cli/commands/ticker.ts +19 -10
  19. package/src/cli/glossary.ts +1 -1
  20. package/src/cli/index.ts +14 -6
  21. package/src/cli/known-gaps.ts +22 -0
  22. package/src/cli/pane-functions/catalog.ts +45 -10
  23. package/src/cli/pane-functions/dom.ts +36 -10
  24. package/src/cli/pane-functions/function-help.ts +4 -2
  25. package/src/cli/pane-functions/headless.ts +12 -1
  26. package/src/cli/pane-functions/index.ts +44 -3
  27. package/src/cli/pane-functions/lock-note.ts +56 -0
  28. package/src/cli/pane-functions/report-notices.ts +15 -0
  29. package/src/cli/symbol-args.ts +19 -0
  30. package/src/components/command-bar/list/view.tsx +2 -1
  31. package/src/components/command-bar/workflow/builtin.ts +2 -2
  32. package/src/components/command-bar/workflow/collection-actions.ts +1 -1
  33. package/src/components/feedback-dialog.tsx +1 -1
  34. package/src/components/index.ts +3 -2
  35. package/src/components/layout/header.tsx +58 -14
  36. package/src/components/layout/shell/chrome.ts +10 -4
  37. package/src/components/layout/shell/index.tsx +1 -1
  38. package/src/components/layout/status-bar-groups.ts +1 -1
  39. package/src/components/layout/status-bar.tsx +2 -2
  40. package/src/components/markdown-text.tsx +14 -2
  41. package/src/components/reader/article-reader.tsx +1 -1
  42. package/src/components/reader/detail-reader.tsx +10 -7
  43. package/src/components/reader/reader-text.tsx +32 -17
  44. package/src/components/ui/column-grid.tsx +206 -0
  45. package/src/components/ui/display.tsx +40 -3
  46. package/src/components/ui/fill.ts +8 -0
  47. package/src/components/ui/index.ts +2 -0
  48. package/src/layout-marketplace/gallery.tsx +2 -2
  49. package/src/layout-marketplace/linked-sync.ts +1 -1
  50. package/src/market-data/market/freshness.ts +3 -0
  51. package/src/market-data/published-apac-sessions.ts +12 -2
  52. package/src/market-data/quotes/screener-live-quotes.ts +31 -7
  53. package/src/plugins/builtin/account-management/form-components.tsx +4 -7
  54. package/src/plugins/builtin/account-management/password-dialog.tsx +3 -2
  55. package/src/plugins/builtin/backtest/pane.tsx +9 -3
  56. package/src/plugins/builtin/buildout/detail/company.tsx +4 -3
  57. package/src/plugins/builtin/buildout/detail/ui.tsx +9 -12
  58. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  59. package/src/plugins/builtin/chart-composer/presets.ts +33 -1
  60. package/src/plugins/builtin/chat/channel-labels.ts +2 -2
  61. package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -1
  62. package/src/plugins/builtin/chat/content/index.tsx +2 -2
  63. package/src/plugins/builtin/chat/conversations.ts +1 -1
  64. package/src/plugins/builtin/chat/index.tsx +2 -2
  65. package/src/plugins/builtin/chat/share/flow.tsx +1 -1
  66. package/src/plugins/builtin/chat/sidebar-rows.ts +1 -1
  67. package/src/plugins/builtin/chat/sidebar.tsx +2 -2
  68. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +2 -2
  69. package/src/plugins/builtin/cloud/askg/pane.tsx +2 -1
  70. package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +3 -2
  71. package/src/plugins/builtin/cloud/auth-actions.tsx +1 -1
  72. package/src/plugins/builtin/cloud/auth-commands.ts +1 -1
  73. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -1
  74. package/src/plugins/builtin/cloud/team/acm-tab.tsx +3 -3
  75. package/src/plugins/builtin/cloud/team/capability.ts +2 -2
  76. package/src/plugins/builtin/cloud/team/command.ts +4 -4
  77. package/src/plugins/builtin/cloud/team/module.tsx +3 -3
  78. package/src/plugins/builtin/cloud/team/pane-model.ts +2 -2
  79. package/src/plugins/builtin/cloud/team/pane-sections.tsx +1 -1
  80. package/src/plugins/builtin/cloud/team/pane-ui.tsx +1 -1
  81. package/src/plugins/builtin/cloud/team/pane.tsx +3 -3
  82. package/src/plugins/builtin/cloud/team/status-widget.tsx +3 -3
  83. package/src/plugins/builtin/cloud/team/team-state-host.ts +1 -1
  84. package/src/plugins/builtin/cloud/team/views.ts +2 -2
  85. package/src/plugins/builtin/cloud/thesis/board-pane.tsx +1 -1
  86. package/src/plugins/builtin/cloud/thesis/detail.tsx +12 -10
  87. package/src/plugins/builtin/cloud/thesis/ticker-tab.tsx +1 -1
  88. package/src/plugins/builtin/cloud/upgrade-command.ts +1 -1
  89. package/src/plugins/builtin/congress-trades/detail.tsx +8 -4
  90. package/src/plugins/builtin/connections/pane.tsx +16 -5
  91. package/src/plugins/builtin/cpi/pane.tsx +5 -4
  92. package/src/plugins/builtin/custom-view/pane.tsx +2 -2
  93. package/src/plugins/builtin/dividend-yield/headless.ts +8 -2
  94. package/src/plugins/builtin/dividend-yield/pane.tsx +4 -2
  95. package/src/plugins/builtin/docs/blocks.tsx +13 -2
  96. package/src/plugins/builtin/docs/pane.tsx +6 -5
  97. package/src/plugins/builtin/doe/index.tsx +11 -4
  98. package/src/plugins/builtin/doe/pane.tsx +5 -4
  99. package/src/plugins/builtin/doe/prices.ts +84 -0
  100. package/src/plugins/builtin/doe/series.ts +4 -1
  101. package/src/plugins/builtin/earnings/board-model.ts +18 -1
  102. package/src/plugins/builtin/earnings/board-table.ts +18 -3
  103. package/src/plugins/builtin/earnings/headless.ts +13 -2
  104. package/src/plugins/builtin/earnings/history-model.ts +1 -1
  105. package/src/plugins/builtin/earnings/history-table.ts +2 -2
  106. package/src/plugins/builtin/earnings-calls/headless.ts +7 -21
  107. package/src/plugins/builtin/earnings-calls/index.tsx +2 -0
  108. package/src/plugins/builtin/earnings-calls/lock-note.ts +17 -0
  109. package/src/plugins/builtin/equity-screener/headless.ts +59 -1
  110. package/src/plugins/builtin/equity-screener/live.ts +82 -31
  111. package/src/plugins/builtin/equity-screener/pane.tsx +60 -23
  112. package/src/plugins/builtin/estimate-revisions/client.ts +1 -1
  113. package/src/plugins/builtin/estimate-revisions/headless.ts +9 -2
  114. package/src/plugins/builtin/estimate-revisions/index.tsx +3 -0
  115. package/src/plugins/builtin/estimate-revisions/lock-note.ts +23 -0
  116. package/src/plugins/builtin/executives/index.tsx +18 -0
  117. package/src/plugins/builtin/exposure/charts.tsx +4 -3
  118. package/src/plugins/builtin/filing-events/index.tsx +8 -10
  119. package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
  120. package/src/plugins/builtin/gpu/equities.tsx +9 -6
  121. package/src/plugins/builtin/help/feedback-tab.tsx +1 -1
  122. package/src/plugins/builtin/iv-history/format.ts +3 -0
  123. package/src/plugins/builtin/iv-history/headless.ts +3 -1
  124. package/src/plugins/builtin/iv-history/index.tsx +3 -1
  125. package/src/plugins/builtin/iv-history/model.ts +3 -1
  126. package/src/plugins/builtin/iv-history/screen-pane.tsx +3 -1
  127. package/src/plugins/builtin/iv-history/universe.ts +1 -1
  128. package/src/plugins/builtin/jobs/headless.ts +6 -20
  129. package/src/plugins/builtin/jobs/index.tsx +2 -0
  130. package/src/plugins/builtin/jobs/lock-note.ts +16 -0
  131. package/src/plugins/builtin/notes/index.tsx +1 -1
  132. package/src/plugins/builtin/notes/owner.tsx +2 -2
  133. package/src/plugins/builtin/options/carry.ts +36 -0
  134. package/src/plugins/builtin/options/index.tsx +2 -1
  135. package/src/plugins/builtin/options/model-spot.ts +94 -0
  136. package/src/plugins/builtin/options/open-interest-as-of.ts +74 -0
  137. package/src/plugins/builtin/options/view.tsx +69 -11
  138. package/src/plugins/builtin/options-calculator/pane.tsx +56 -1
  139. package/src/plugins/builtin/options-calculator/premium-history.ts +34 -0
  140. package/src/plugins/builtin/options-scenario/client.ts +33 -9
  141. package/src/plugins/builtin/options-scenario/csp.ts +168 -0
  142. package/src/plugins/builtin/options-scenario/evidence.ts +3 -0
  143. package/src/plugins/builtin/options-scenario/headless.ts +24 -8
  144. package/src/plugins/builtin/options-scenario/index.tsx +3 -1
  145. package/src/plugins/builtin/options-scenario/pane.tsx +32 -6
  146. package/src/plugins/builtin/portfolio-list/mutations.ts +1 -1
  147. package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
  148. package/src/plugins/builtin/research/corporate-actions-pane.tsx +4 -2
  149. package/src/plugins/builtin/research/event-model.ts +7 -4
  150. package/src/plugins/builtin/risk-factors/index.tsx +18 -0
  151. package/src/plugins/builtin/risk-factors/pane.tsx +2 -1
  152. package/src/plugins/builtin/scanner/headless.ts +9 -2
  153. package/src/plugins/builtin/scanner/index.ts +3 -0
  154. package/src/plugins/builtin/scanner/lock-note.ts +17 -0
  155. package/src/plugins/builtin/series-inventory.ts +2 -1
  156. package/src/plugins/builtin/shared/cloud-denied.ts +39 -8
  157. package/src/plugins/builtin/shared/earnings-report.ts +10 -4
  158. package/src/plugins/builtin/shared/pro-lock-note.ts +58 -0
  159. package/src/plugins/builtin/shared/series-list-detail.tsx +10 -5
  160. package/src/plugins/builtin/social-mentions/client.ts +3 -3
  161. package/src/plugins/builtin/social-mentions/headless.ts +6 -2
  162. package/src/plugins/builtin/social-mentions/index.tsx +2 -1
  163. package/src/plugins/builtin/social-mentions/lock-note.ts +15 -0
  164. package/src/plugins/builtin/supply-chain/flow.tsx +15 -13
  165. package/src/plugins/builtin/supply-chain/graph.tsx +6 -4
  166. package/src/plugins/builtin/supply-chain/pane.tsx +8 -5
  167. package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +14 -1
  168. package/src/plugins/builtin/ticker-detail/financials/ratios.ts +9 -7
  169. package/src/plugins/builtin/ticker-detail/headless.ts +4 -1
  170. package/src/plugins/builtin/ticker-detail/overview/components.tsx +105 -104
  171. package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -2
  172. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +34 -8
  173. package/src/plugins/builtin/vol-surface/pane.tsx +6 -3
  174. package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
  175. package/src/renderers/dom/data-table/index.tsx +48 -15
  176. package/src/renderers/dom/data-table/row.tsx +1 -1
  177. package/src/renderers/dom/desktop/popover.tsx +6 -15
  178. package/src/renderers/dom/host/input.tsx +27 -3
  179. package/src/renderers/dom/host/surface-3d.tsx +6 -4
  180. package/src/renderers/dom/styles.css +26 -6
  181. package/src/renderers/electrobun/view/app-services.ts +2 -22
  182. package/src/renderers/electrobun/view/news-source.ts +42 -0
  183. package/src/series/validate.ts +5 -0
  184. package/src/shares/dialog.tsx +2 -1
  185. package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
  186. package/src/sources/provider-router/financials.ts +5 -0
  187. package/src/{plugins/builtin/cloud/team → team}/collections-sync.ts +6 -6
  188. package/src/{plugins/builtin/cloud/team → team}/collections.ts +3 -3
  189. package/src/{plugins/builtin/cloud/team → team}/model.ts +2 -2
  190. package/src/{plugins/builtin/cloud/team → team}/pane-request.ts +1 -1
  191. package/src/{plugins/builtin/cloud/team → team}/store.ts +3 -3
  192. package/src/types/data-provider.ts +3 -0
  193. package/src/types/financials.ts +28 -0
  194. package/src/types/plugin.ts +17 -4
  195. package/src/utils/decimal-comma.ts +18 -0
  196. package/src/utils/exchanges.ts +7 -0
  197. package/src/utils/financial-statements.ts +25 -0
  198. package/src/utils/forward-dividend.ts +39 -9
  199. package/src/utils/fundamentals.ts +41 -1
  200. package/src/utils/option-expiry.ts +16 -0
  201. package/src/utils/sec.ts +2 -1
@@ -31,6 +31,7 @@ import {
31
31
  formatPriceRange,
32
32
  } from "../helpers";
33
33
  import { cliStyles, colorBySign, renderSection, renderStats } from "../../utils/cli-output";
34
+ import { statementHistoryNotes } from "../../utils/financial-statements";
34
35
  import { formatPerShareNumber } from "../../utils/reported-money";
35
36
  import {
36
37
  analystTargetCurrency,
@@ -48,19 +49,25 @@ import type { FuturesContractTermsPayload } from "../../api-client/futures-curve
48
49
  import { futuresSettleText, isFuturesInstrument } from "../../market-data/futures-contract";
49
50
  import {
50
51
  chainHasExpiry,
51
- chainWithModelFigures,
52
52
  formatExpiryList,
53
53
  loadOptionModelInputs,
54
+ loadOptionsChainOpenInterest,
54
55
  formatOptionDeltaCell,
55
56
  formatOptionIvCell,
56
57
  formatOptionQuoteCell,
57
- missingExpiryMessage,
58
58
  OPTION_MODEL_RATE,
59
59
  OPTIONS_USAGE,
60
60
  optionRows,
61
+ optionsBasisFields,
62
+ optionsBasisFootnote,
61
63
  parseOptionExpiration,
64
+ valueChainAtModelSpot,
65
+ type OptionsChainBasis,
62
66
  } from "./options-chain";
63
- import { DEFAULT_LEAPS_CRITERIA, LEAPS_USAGE, parseLeapsCriteria, runLeapsScreen, takeLeapsOptions } from "./options-leaps";
67
+ import { missingExpiryMessage } from "../../utils/option-expiry";
68
+ import {
69
+ DEFAULT_LEAPS_CRITERIA, LEAPS_EXTRA_COLUMNS, LEAPS_USAGE, parseLeapsCriteria, runLeapsScreen, takeLeapsOptions,
70
+ } from "./options-leaps";
64
71
  import type { CliResultColumn } from "../result";
65
72
  import type { PriceHistoryResult } from "../../types/price-history";
66
73
  import {
@@ -137,7 +144,7 @@ import { formatBasisPoints, toBasisPoints } from "../../utils/basis-points";
137
144
  import { notATickerSymbol, providerMissReason } from "../../sources/provider-errors";
138
145
  import { isNotATickerMessage, withListingSuggestion } from "../not-a-ticker";
139
146
  import { describeSuggestion, suggestListing } from "../listing-suggestion";
140
- import { splitSymbolArgs } from "../symbol-args";
147
+ import { splitSymbolArgs, uniqueSecuritySymbols } from "../symbol-args";
141
148
  import { cliPerpetualContract, perpetualContractStats, type PerpetualContractPayload } from "../perpetual-contract";
142
149
  import { subMinuteDataMessage } from "../known-gaps";
143
150
  import { secRegistrantMismatchMessage, SecRegistrantMismatchError, areDifferentCompanies } from "../../sources/sec-registrant";
@@ -218,9 +225,13 @@ function parseNewsFeed(value: string | undefined): NewsQuery["feed"] | undefined
218
225
  return value && VALID_NEWS_FEEDS.has(value as NewsFeed) ? value as NewsQuery["feed"] : undefined;
219
226
  }
220
227
 
221
- /** The symbols of a command's arguments, one each however they were separated: spaces, commas or pasted lines. */
228
+ /**
229
+ * The symbols of a command's arguments, one each however they were separated
230
+ * (spaces, commas or pasted lines), and each security once however its case or
231
+ * share class was written: `AAPL aapl BRK.B BRK-B` is AAPL and BRK.B.
232
+ */
222
233
  function normalizeSymbols(args: string[]): string[] {
223
- return splitSymbolArgs(args).map((symbol) => symbol.toUpperCase());
234
+ return uniqueSecuritySymbols(splitSymbolArgs(args).map((symbol) => symbol.toUpperCase()));
224
235
  }
225
236
 
226
237
  /** What JSON metadata says about the listing a command resolved. */
@@ -950,11 +961,13 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
950
961
  const freshness = rowsFreshness(rows, {
951
962
  ...REPORTED_DATA, basis: "financial statements", observedKey: "date", oldest: null,
952
963
  });
964
+ // The rows are the annual years served; older ones are not, and the closing lines say so.
965
+ const historyNotes = statementHistoryNotes("annual", rows.map((row) => row.date), listing.key);
953
966
  // NDJSON keeps the raw statement rows it always wrote; CSV gets the table and its closing lines.
954
967
  const tables = tabular
955
968
  ? selectTables({
956
969
  tables: [exportRowsTable("Annual statements", STATEMENT_COLUMNS, rows)],
957
- footer: reportFooterLines({ freshness }),
970
+ footer: reportFooterLines({ freshness, notes: historyNotes }),
958
971
  })
959
972
  : undefined;
960
973
  ctx.printResult({
@@ -966,6 +979,7 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
966
979
  quarterlyStatements: financials.quarterlyStatements.length,
967
980
  fundamentals: exportedFundamentals(financials.fundamentals),
968
981
  profile: financials.profile,
982
+ ...(historyNotes.length > 0 ? { notes: historyNotes } : {}),
969
983
  },
970
984
  freshness,
971
985
  ...(choice.length > 0 ? { warnings: choice } : {}),
@@ -973,6 +987,7 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
973
987
  heading: listingHeading(identity),
974
988
  rows: () => rows,
975
989
  columns: STATEMENT_COLUMNS,
990
+ ...(historyNotes.length > 0 ? { footnote: historyNotes.join("\n"), exportNotes: historyNotes } : {}),
976
991
  ...(tables && ctx.cliOptions.format === "csv" ? { tables } : {}),
977
992
  // Annual statements in the company's reporting currency.
978
993
  exportIdentity: { symbolOrSeries: listing.key, currencyOrUnit: rows.find((row) => row.currency)?.currency ?? null, interval: "annual" },
@@ -1462,10 +1477,9 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1462
1477
  const expiration = takeOption(args, "--expiration");
1463
1478
  const exchangeOption = takeOption(args, "--exchange");
1464
1479
  if (leapsOptions.leaps) {
1465
- if (expiration != null || rawArgs.includes("--expiration")) {
1466
- ctx.fail("--leaps reads every expiry more than a year out; drop --expiration.", `Usage: gloomberb ${LEAPS_USAGE}`);
1467
- }
1468
- const criteria = parseLeapsCriteria(leapsOptions.raw);
1480
+ // With --leaps, --expiration is a window of one expiry.
1481
+ const expirationGiven = expiration != null || rawArgs.some((arg) => arg === "--expiration" || arg.startsWith("--expiration="));
1482
+ const criteria = parseLeapsCriteria({ ...leapsOptions.raw, ...expirationGiven ? { "--expiration": expiration ?? "" } : {} });
1469
1483
  if ("error" in criteria) ctx.fail(criteria.error, `Usage: gloomberb ${LEAPS_USAGE}`);
1470
1484
  const symbols = normalizeSymbols(args);
1471
1485
  if (symbols.length === 0) ctx.fail("--leaps needs at least one symbol.", `Usage: gloomberb ${LEAPS_USAGE}`);
@@ -1524,7 +1538,13 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1524
1538
  }
1525
1539
  const quote = await quotePromise;
1526
1540
  const identity = listingIdentity(listing, quote);
1527
- const chain = chainWithModelFigures(loaded, inputs);
1541
+ // IV and delta are valued at one spot and instant, named under the table and on every exported row.
1542
+ const { chain, spot: modelSpot } = valueChainAtModelSpot(loaded, inputs, Date.now(), identity.exchange);
1543
+ const basis: OptionsChainBasis = { spot: modelSpot, dividendYield: inputs.dividendYield ?? null,
1544
+ openInterest: await loadOptionsChainOpenInterest(symbol, chain) };
1545
+ const basisFields = optionsBasisFields(basis);
1546
+ const currency = [...chain.calls, ...chain.puts].find((contract) => contract.currency?.trim())?.currency.trim().toUpperCase()
1547
+ ?? quote?.currency ?? null;
1528
1548
  // A failed refresh falls back to the stored chain, which can be days old.
1529
1549
  const refreshWarning = result?.refreshError == null ? null
1530
1550
  : `Options refresh failed; showing the chain stored ${new Date(result.fetchedAt).toISOString()}`
@@ -1546,23 +1566,54 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1546
1566
  ...(chain.delayMinutes != null ? { delayMinutes: chain.delayMinutes } : {}),
1547
1567
  stale: refreshWarning != null,
1548
1568
  });
1569
+ const contractCount = chain.calls.length + chain.puts.length;
1549
1570
  ctx.printResult({
1550
1571
  data: chain,
1551
1572
  metadata: {
1552
1573
  ...listingMetadata(identity),
1553
1574
  expirations: chain.expirationDates,
1575
+ currency,
1576
+ interval: OPTIONS_EXPORT_INTERVAL,
1577
+ // `iv` is annualised volatility and `delta` the option's change per 1.00 move in the underlying, both decimals.
1578
+ units: { iv: "annualized decimal", delta: "decimal per 1.00 move in the underlying" },
1554
1579
  // What `iv` and `delta` on each contract are valued from; the provider's own impliedVolatility is left as sent.
1555
- model: { spot: inputs.spot ?? null, dividendYield: inputs.dividendYield ?? null, rate: OPTION_MODEL_RATE },
1580
+ model: {
1581
+ spot: basisFields.spot, spotBasis: basisFields.spotBasis, spotAsOf: basisFields.spotAsOf,
1582
+ valuationTime: modelSpot ? new Date(modelSpot.valuationTime).toISOString() : null,
1583
+ dividendYield: inputs.dividendYield ?? null, rate: OPTION_MODEL_RATE,
1584
+ },
1585
+ openInterest: { asOf: basisFields.openInterestAsOf, listedAsOf: basis.openInterest?.listedDate ?? null,
1586
+ compared: basis.openInterest?.compared ?? 0, matched: basis.openInterest?.matched ?? 0 },
1556
1587
  },
1557
1588
  ...(warnings.length > 0 ? { warnings } : {}),
1558
1589
  freshness,
1559
1590
  }, {
1560
1591
  heading: listingHeading(identity),
1561
- rows: optionRows,
1592
+ rows: (data: OptionsChain) => optionRows(data).map((row) => ({ ...row, ...basisFields })),
1562
1593
  // Choosing an expiry needs the list; once one is chosen the table is that expiry.
1563
1594
  summary: (data: OptionsChain) => expirationDate == null ? formatExpiryList(data.expirationDates) : "",
1564
1595
  empty: `No option contracts for ${listingTitle(identity)}.`,
1596
+ ...(contractCount > 0 ? { footnote: optionsBasisFootnote(basis) } : {}),
1597
+ exportIdentity: { symbolOrSeries: listing.key, currencyOrUnit: currency, interval: OPTIONS_EXPORT_INTERVAL },
1598
+ // Exports state each decimal's unit in its header and end every row with the spot and dates it is as of.
1565
1599
  columns: [
1600
+ { key: "side", header: "Side" },
1601
+ { key: "contract", header: "Contract" },
1602
+ { key: "expiration", header: "Expiry" },
1603
+ { key: "strike", header: "Strike" },
1604
+ { key: "last", header: "Last" },
1605
+ { key: "bid", header: "Bid" },
1606
+ { key: "ask", header: "Ask" },
1607
+ { key: "iv", header: "IV (annualized decimal)" },
1608
+ { key: "delta", header: "Delta (decimal)" },
1609
+ { key: "volume", header: "Vol" },
1610
+ { key: "openInterest", header: "OI" },
1611
+ { key: "spot", header: "Spot" },
1612
+ { key: "spotBasis", header: "Spot basis" },
1613
+ { key: "spotAsOf", header: "Spot as of" },
1614
+ { key: "openInterestAsOf", header: "OI as of" },
1615
+ ],
1616
+ textColumns: [
1566
1617
  { key: "side", header: "Side" },
1567
1618
  { key: "contract", header: "Contract", shrink: false },
1568
1619
  // The contract symbol already carries the date, so this goes first when the table is narrow.
@@ -1580,6 +1631,9 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1580
1631
  });
1581
1632
  }
1582
1633
 
1634
+ /** What an options chain export's `interval` says: one quote snapshot, not bars. */
1635
+ const OPTIONS_EXPORT_INTERVAL = "snapshot";
1636
+
1583
1637
  const FX_USAGE = "fx <currency> | fx <base>/<quote>";
1584
1638
 
1585
1639
  async function runFx(rawArgs: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
@@ -1763,10 +1817,10 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
1763
1817
  options: [
1764
1818
  {
1765
1819
  flags: "--expiration <YYYY-MM-DD|unix>",
1766
- description: "Expiration as a date (2028-01-21) or Unix seconds; defaults to the nearest one, and lists the others",
1820
+ description: "Expiration as a date (2028-01-21) or Unix seconds; defaults to the nearest one, and lists the others. With --leaps: rank that expiry alone",
1767
1821
  },
1768
1822
  EXCHANGE_OPTION,
1769
- { flags: "--leaps", description: "Rank contracts more than a year out across the symbols given, as a stock replacement" },
1823
+ { flags: "--leaps", description: "Rank long-dated contracts across the symbols given, as a stock replacement; more than a year out unless a window says otherwise" },
1770
1824
  { flags: "--side <calls|puts>", description: `With --leaps: the side to rank (default ${DEFAULT_LEAPS_CRITERIA.side}s)` },
1771
1825
  {
1772
1826
  flags: "--delta <low-high>",
@@ -1777,6 +1831,17 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
1777
1831
  description: `With --leaps: widest bid/ask spread, in percent of the midpoint (default ${DEFAULT_LEAPS_CRITERIA.maxSpreadPercent})`,
1778
1832
  },
1779
1833
  { flags: "--min-oi <contracts>", description: `With --leaps: least open interest (default ${DEFAULT_LEAPS_CRITERIA.minOpenInterest})` },
1834
+ {
1835
+ flags: "--min-days <days>",
1836
+ description: `With --leaps: fewest calendar days to expiry (default ${DEFAULT_LEAPS_CRITERIA.minDays}); 180 reaches six months out`,
1837
+ },
1838
+ { flags: "--max-days <days>", description: "With --leaps: most calendar days to expiry; --min-days 365 --max-days 730 is 12 to 24 months" },
1839
+ { flags: "--expiry-from <YYYY-MM-DD>", description: "With --leaps: first expiry date kept; replaces the one-year floor" },
1840
+ { flags: "--expiry-to <YYYY-MM-DD>", description: "With --leaps: last expiry date kept" },
1841
+ {
1842
+ flags: "--columns <list>",
1843
+ description: `With --leaps: add figures, comma-separated: ${LEAPS_EXTRA_COLUMNS.join(", ")}`,
1844
+ },
1780
1845
  {
1781
1846
  flags: "--sort <order>",
1782
1847
  description: "With --leaps: carry (extrinsic per year, lowest first; the default), spread, oi, delta, expiry or symbol",
@@ -1785,7 +1850,10 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
1785
1850
  sections: [{
1786
1851
  title: "LEAPS",
1787
1852
  lines: [
1788
- "--leaps reads every expiry more than a year out for each symbol and keeps the liquid contracts in the delta band, ranked by extrinsic per year: time value (midpoint less intrinsic) as a percent of spot, per year to expiry.",
1853
+ "--leaps reads each symbol's expiries in the window and keeps the liquid contracts in the delta band, ranked by extrinsic per year: time value (midpoint less intrinsic) as a percent of spot, per year to expiry.",
1854
+ "--min-days, --max-days, --expiry-from and --expiry-to set the window together; without a lower bound it starts a year out. Each symbol reads at most 8 expiries, the ones nearest the window's start, and a warning names any it left.",
1855
+ "--columns adds iv (the contract's implied vol), ivr (the underlying's IV rank), cost (midpoint x 100 per contract), breakeven (at the midpoint, and its distance from spot), next-earnings, ex-dividend and carry-net "
1856
+ + `(extrinsic per year net of the ${Number((OPTION_MODEL_RATE * 100).toFixed(2))}% rate on the strike and the dividend yield). A figure a symbol lacks is blank, with a warning.`,
1789
1857
  "A symbol without LEAPS, a quote or a chain is named in a warning and the others still rank. docs/research-data.md defines each figure.",
1790
1858
  ],
1791
1859
  }],
@@ -1795,6 +1863,8 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
1795
1863
  "options AAPL:NASDAQ --json",
1796
1864
  "options AAPL MSFT NVDA GOOGL AMZN META --leaps",
1797
1865
  "options SPY QQQ --leaps --side puts --delta 0.20-0.40 --csv",
1866
+ "options MSFT --leaps --min-days 365 --max-days 730",
1867
+ "options AAPL MSFT NVDA --leaps --min-days 180 --columns iv,ivr,cost,breakeven,carry-net",
1798
1868
  ],
1799
1869
  },
1800
1870
  execute: runOptions,
@@ -1,12 +1,18 @@
1
- import type { OptionContract, OptionsChain, Quote } from "../../types/financials";
1
+ import type { OptionContract, OptionsChain, Quote, TickerFinancials } from "../../types/financials";
2
2
  import type { DataProvider } from "../../types/data-provider";
3
3
  import type { CliListing } from "../listing-arg";
4
4
  import { cliStyles, cliTerminalWidth, wrapText } from "../../utils/cli-output";
5
5
  import { isFiniteNumber } from "../../utils/guards";
6
6
  import { calculateOptionGreeks, solveChainVolatilities } from "../../plugins/builtin/options/analytics";
7
7
  import { optionQuoteSide } from "../../plugins/builtin/options/market-reference";
8
+ import {
9
+ formatModelSpotPrice, modelSpotLabel, optionModelSpot, type OptionModelSpot,
10
+ } from "../../plugins/builtin/options/model-spot";
11
+ import {
12
+ chainOpenInterestAsOf, loadListedOpenInterest, openInterestAsOfLabel, type OpenInterestAsOf, statedOpenInterestAsOf,
13
+ } from "../../plugins/builtin/options/open-interest-as-of";
8
14
  import { DEFAULT_OPTION_CALC_DRAFT } from "../../plugins/builtin/options-calculator/model";
9
- import { expiryIsoDate, missingExpiryText, parseOptionExpiration } from "../../utils/option-expiry";
15
+ import { expiryIsoDate, parseOptionExpiration } from "../../utils/option-expiry";
10
16
 
11
17
  export const OPTIONS_USAGE = "options <symbol> [--expiration <YYYY-MM-DD|unix>]";
12
18
 
@@ -23,22 +29,6 @@ function listedDates(expirations: readonly number[]): string[] {
23
29
  return [...new Set(expirations.map((expiration) => expiryIsoDate(expiration)).filter(Boolean))].sort();
24
30
  }
25
31
 
26
- /**
27
- * Why a chain request for one expiry came back without contracts, and which
28
- * expiries it could have been: the few nearest the requested date, so a typo or
29
- * an unlisted LEAPS date points at the neighbours to pick.
30
- */
31
- export function missingExpiryMessage(
32
- symbol: string,
33
- requested: number,
34
- expirations: readonly number[],
35
- ): { message: string; details?: string } {
36
- return {
37
- message: missingExpiryText(requested, expirations, symbol),
38
- details: `Run gloomberb options ${symbol} to list every expiry (--json carries them too).`,
39
- };
40
- }
41
-
42
32
  const EXPIRY_LIST_LIMIT = 40;
43
33
  const EXPIRY_LIST_WIDTH = 100;
44
34
  const EXPIRY_GAP = 2;
@@ -61,6 +51,8 @@ export function formatExpiryList(expirations: readonly number[], width = cliTerm
61
51
  export interface OptionModelInputs {
62
52
  /** The underlying's current price; absent when the quote is missing or stale. */
63
53
  spot?: number;
54
+ /** The quote `spot` is read from, for the spot the chain is valued at (`optionModelSpot`). */
55
+ quote?: Quote;
64
56
  dividendYield?: number;
65
57
  now?: number;
66
58
  }
@@ -105,21 +97,35 @@ function optionModelFigures(chain: OptionsChain, inputs: OptionModelInputs): Map
105
97
  return figures;
106
98
  }
107
99
 
108
- /** Spot and dividend yield for IV and delta, read from the underlying the way the options pane does; missing ones stay missing. */
100
+ /** The underlying's financials, as DES reads them: its dividend yield, ex-dividend date and next report. Null when they cannot be read. */
101
+ export function loadUnderlyingFinancials(
102
+ dataProvider: Pick<DataProvider, "getTickerFinancials">,
103
+ listing: CliListing,
104
+ refresh: boolean,
105
+ ): Promise<TickerFinancials | null> {
106
+ return Promise.resolve().then(() => dataProvider.getTickerFinancials(
107
+ listing.request.symbol, listing.request.exchange, { cacheMode: refresh ? "refresh" : "default" },
108
+ )).catch(() => null);
109
+ }
110
+
111
+ /**
112
+ * Spot and dividend yield for IV and delta, read from the underlying the way the
113
+ * options pane does; missing ones stay missing. `financials` shares one read of
114
+ * them with a caller that needs more of them.
115
+ */
109
116
  export async function loadOptionModelInputs(
110
117
  dataProvider: Pick<DataProvider, "getTickerFinancials">,
111
118
  listing: CliListing,
112
119
  quote: Promise<Quote | null>,
113
120
  refresh: boolean,
121
+ underlying: Promise<TickerFinancials | null> = loadUnderlyingFinancials(dataProvider, listing, refresh),
114
122
  ): Promise<OptionModelInputs> {
115
- const financials = await Promise.resolve().then(() => dataProvider.getTickerFinancials(
116
- listing.request.symbol, listing.request.exchange, { cacheMode: refresh ? "refresh" : "default" },
117
- )).catch(() => null);
123
+ const financials = await underlying;
118
124
  const current = [await quote, financials?.quote]
119
125
  .find((candidate) => candidate && candidate.stale !== true && isFiniteNumber(candidate.price) && candidate.price > 0);
120
126
  const dividendYield = financials?.fundamentals?.dividendYield;
121
127
  return {
122
- ...(current ? { spot: current.price } : {}),
128
+ ...(current ? { spot: current.price, quote: current } : {}),
123
129
  ...(isFiniteNumber(dividendYield) && dividendYield >= 0 ? { dividendYield } : {}),
124
130
  };
125
131
  }
@@ -131,6 +137,70 @@ export function chainWithModelFigures(chain: OptionsChain, inputs: OptionModelIn
131
137
  return { ...chain, calls: chain.calls.map(add), puts: chain.puts.map(add) };
132
138
  }
133
139
 
140
+ /**
141
+ * What a chain's IV, delta and open interest are as of: the spot and instant
142
+ * they are valued at, and the session the open interest counts. The rows and
143
+ * the closing lines of every format say it.
144
+ */
145
+ export interface OptionsChainBasis {
146
+ spot: OptionModelSpot | null;
147
+ dividendYield: number | null;
148
+ openInterest: OpenInterestAsOf | null;
149
+ }
150
+
151
+ /** The chain with `iv` and `delta` valued at the spot `optionModelSpot` picks for it, and that spot. */
152
+ export function valueChainAtModelSpot(
153
+ chain: OptionsChain, inputs: OptionModelInputs, now: number, exchange?: string,
154
+ ): { chain: OptionsChain; spot: OptionModelSpot | null } {
155
+ const spot = optionModelSpot(inputs.quote, chain, now, exchange);
156
+ return {
157
+ chain: chainWithModelFigures(chain, spot ? { spot: spot.price, dividendYield: inputs.dividendYield, now: spot.valuationTime } : inputs),
158
+ spot,
159
+ };
160
+ }
161
+
162
+ /**
163
+ * The open-interest date of the chain's expiry: the chain's own, else (a chain
164
+ * that does not say) from OPX's listing of it; null when the listing cannot be read.
165
+ */
166
+ export async function loadOptionsChainOpenInterest(symbol: string, chain: OptionsChain): Promise<OpenInterestAsOf | null> {
167
+ const stated = statedOpenInterestAsOf(chain);
168
+ if (stated) return stated;
169
+ const expiration = [...chain.calls, ...chain.puts][0]?.expiration;
170
+ if (expiration == null) return null;
171
+ const payload = await loadListedOpenInterest(symbol, expiryIsoDate(expiration));
172
+ return payload ? chainOpenInterestAsOf(chain, payload) : null;
173
+ }
174
+
175
+ /**
176
+ * The line under the chain: `Greeks at spot 89.79 (close Mon 12 Oct 20:00 UTC), dividend yield 2.34%, rate 4% · OI as of Fri 9 Oct`.
177
+ * An open interest whose date cannot be established says why.
178
+ */
179
+ export function optionsBasisFootnote(basis: OptionsChainBasis): string {
180
+ const parts: string[] = [];
181
+ if (basis.spot) {
182
+ parts.push(`Greeks at spot ${formatModelSpotPrice(basis.spot.price)} (${modelSpotLabel(basis.spot)}), `
183
+ + `dividend yield ${basis.dividendYield == null ? "0%" : `${Number((basis.dividendYield * 100).toFixed(2))}%`}, `
184
+ + `rate ${Number((OPTION_MODEL_RATE * 100).toFixed(2))}%`);
185
+ }
186
+ const oi = basis.openInterest;
187
+ parts.push(oi?.date ? openInterestAsOfLabel(oi)
188
+ : oi?.listedDate && oi.compared > 0
189
+ ? `OI date not published: the counts differ from OPX's ${oi.listedDate} counts on ${oi.compared - oi.matched} of ${oi.compared} contracts`
190
+ : openInterestAsOfLabel(null));
191
+ return parts.join(" · ");
192
+ }
193
+
194
+ /** The spot and open-interest fields every exported row carries. */
195
+ export function optionsBasisFields(basis: OptionsChainBasis) {
196
+ return {
197
+ spot: basis.spot?.price ?? null,
198
+ spotBasis: basis.spot ? basis.spot.basis === "close" ? "close" : basis.spot.session ?? "last" : null,
199
+ spotAsOf: basis.spot ? new Date(basis.spot.observedAt).toISOString() : null,
200
+ openInterestAsOf: basis.openInterest?.date ?? null,
201
+ };
202
+ }
203
+
134
204
  export function optionRows(chain: OptionsChain) {
135
205
  const contracts = [
136
206
  ...chain.calls.map((contract) => ({ side: "call", contract })),