gloomberb 0.17.1 → 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 (101) 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/earnings.ts +4 -2
  4. package/src/app/global-shortcuts.ts +10 -0
  5. package/src/app/keybindings/actions.ts +16 -0
  6. package/src/app/runtime/desktop-deeplink.ts +1 -1
  7. package/src/app.tsx +1 -1
  8. package/src/architecture/core-builtin-allowlist.json +4 -9
  9. package/src/cli/commands/market.ts +34 -16
  10. package/src/cli/commands/options-chain.ts +29 -33
  11. package/src/cli/commands/options-leaps.ts +462 -63
  12. package/src/cli/commands/search.ts +10 -7
  13. package/src/cli/commands/ticker.ts +1 -1
  14. package/src/cli/known-gaps.ts +22 -0
  15. package/src/cli/pane-functions/catalog.ts +6 -3
  16. package/src/cli/pane-functions/dom.ts +17 -2
  17. package/src/cli/pane-functions/function-help.ts +1 -1
  18. package/src/cli/pane-functions/index.ts +5 -3
  19. package/src/cli/pane-functions/report-notices.ts +15 -0
  20. package/src/cli/symbol-args.ts +19 -0
  21. package/src/components/command-bar/workflow/builtin.ts +2 -2
  22. package/src/components/command-bar/workflow/collection-actions.ts +1 -1
  23. package/src/components/feedback-dialog.tsx +1 -1
  24. package/src/components/layout/shell/index.tsx +1 -1
  25. package/src/components/layout/status-bar-groups.ts +1 -1
  26. package/src/components/layout/status-bar.tsx +2 -2
  27. package/src/components/markdown-text.tsx +14 -2
  28. package/src/components/reader/article-reader.tsx +1 -1
  29. package/src/components/reader/detail-reader.tsx +10 -7
  30. package/src/components/reader/reader-text.tsx +32 -17
  31. package/src/layout-marketplace/gallery.tsx +2 -2
  32. package/src/layout-marketplace/linked-sync.ts +1 -1
  33. package/src/market-data/market/freshness.ts +3 -0
  34. package/src/market-data/published-apac-sessions.ts +12 -2
  35. package/src/plugins/builtin/chart-composer/index.tsx +1 -1
  36. package/src/plugins/builtin/chart-composer/presets.ts +33 -1
  37. package/src/plugins/builtin/chat/channel-labels.ts +2 -2
  38. package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -1
  39. package/src/plugins/builtin/chat/content/index.tsx +2 -2
  40. package/src/plugins/builtin/chat/conversations.ts +1 -1
  41. package/src/plugins/builtin/chat/index.tsx +2 -2
  42. package/src/plugins/builtin/chat/share/flow.tsx +1 -1
  43. package/src/plugins/builtin/chat/sidebar-rows.ts +1 -1
  44. package/src/plugins/builtin/chat/sidebar.tsx +2 -2
  45. package/src/plugins/builtin/chat/unread-inbox-pane.tsx +2 -2
  46. package/src/plugins/builtin/cloud/auth-actions.tsx +1 -1
  47. package/src/plugins/builtin/cloud/auth-commands.ts +1 -1
  48. package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -1
  49. package/src/plugins/builtin/cloud/team/acm-tab.tsx +3 -3
  50. package/src/plugins/builtin/cloud/team/capability.ts +2 -2
  51. package/src/plugins/builtin/cloud/team/command.ts +4 -4
  52. package/src/plugins/builtin/cloud/team/module.tsx +3 -3
  53. package/src/plugins/builtin/cloud/team/pane-model.ts +2 -2
  54. package/src/plugins/builtin/cloud/team/pane-sections.tsx +1 -1
  55. package/src/plugins/builtin/cloud/team/pane-ui.tsx +1 -1
  56. package/src/plugins/builtin/cloud/team/pane.tsx +3 -3
  57. package/src/plugins/builtin/cloud/team/status-widget.tsx +3 -3
  58. package/src/plugins/builtin/cloud/team/team-state-host.ts +1 -1
  59. package/src/plugins/builtin/cloud/team/views.ts +2 -2
  60. package/src/plugins/builtin/cloud/thesis/board-pane.tsx +1 -1
  61. package/src/plugins/builtin/cloud/thesis/detail.tsx +1 -1
  62. package/src/plugins/builtin/cloud/thesis/ticker-tab.tsx +1 -1
  63. package/src/plugins/builtin/cloud/upgrade-command.ts +1 -1
  64. package/src/plugins/builtin/custom-view/pane.tsx +2 -2
  65. package/src/plugins/builtin/dividend-yield/headless.ts +8 -2
  66. package/src/plugins/builtin/dividend-yield/pane.tsx +4 -2
  67. package/src/plugins/builtin/docs/blocks.tsx +13 -2
  68. package/src/plugins/builtin/docs/pane.tsx +6 -5
  69. package/src/plugins/builtin/earnings/board-model.ts +18 -1
  70. package/src/plugins/builtin/earnings/board-table.ts +18 -3
  71. package/src/plugins/builtin/earnings/headless.ts +13 -2
  72. package/src/plugins/builtin/earnings/history-model.ts +1 -1
  73. package/src/plugins/builtin/earnings/history-table.ts +2 -2
  74. package/src/plugins/builtin/help/feedback-tab.tsx +1 -1
  75. package/src/plugins/builtin/notes/index.tsx +1 -1
  76. package/src/plugins/builtin/notes/owner.tsx +2 -2
  77. package/src/plugins/builtin/options/carry.ts +36 -0
  78. package/src/plugins/builtin/options/open-interest-as-of.ts +16 -5
  79. package/src/plugins/builtin/options/view.tsx +19 -7
  80. package/src/plugins/builtin/options-calculator/pane.tsx +56 -1
  81. package/src/plugins/builtin/options-calculator/premium-history.ts +34 -0
  82. package/src/plugins/builtin/portfolio-list/mutations.ts +1 -1
  83. package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
  84. package/src/plugins/builtin/research/corporate-actions-pane.tsx +4 -2
  85. package/src/plugins/builtin/research/event-model.ts +7 -4
  86. package/src/plugins/builtin/series-inventory.ts +1 -0
  87. package/src/plugins/builtin/shared/earnings-report.ts +10 -4
  88. package/src/renderers/dom/styles.css +4 -2
  89. package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
  90. package/src/{plugins/builtin/cloud/team → team}/collections-sync.ts +6 -6
  91. package/src/{plugins/builtin/cloud/team → team}/collections.ts +3 -3
  92. package/src/{plugins/builtin/cloud/team → team}/model.ts +2 -2
  93. package/src/{plugins/builtin/cloud/team → team}/pane-request.ts +1 -1
  94. package/src/{plugins/builtin/cloud/team → team}/store.ts +3 -3
  95. package/src/types/data-provider.ts +3 -0
  96. package/src/types/financials.ts +6 -0
  97. package/src/utils/decimal-comma.ts +18 -0
  98. package/src/utils/exchanges.ts +7 -0
  99. package/src/utils/forward-dividend.ts +39 -9
  100. package/src/utils/option-expiry.ts +16 -0
  101. package/src/utils/sec.ts +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gloomberb",
3
- "version": "0.17.1",
3
+ "version": "0.17.2",
4
4
  "packageManager": "bun@1.3.11",
5
5
  "description": "Market research and portfolio tracker for the terminal",
6
6
  "module": "src/public/index.ts",
@@ -6,13 +6,13 @@
6
6
  * hosted terminal's sign-in gate reuses.
7
7
  */
8
8
  import { useEffect, useRef, useState } from "react";
9
- import type { AuthUser } from "../../../api-client";
10
- import { DialogFrame } from "../../../components/ui/frame";
11
- import { useAppLanguage } from "../../../i18n/react";
12
- import { useDialog, type PromptContext } from "../../../ui/dialog";
13
- import { AuthForm, authFormTitle } from "../../../account/auth-form";
14
- import type { AccountMode } from "../../../account/auth-model";
15
- import { CloudVerificationPanel } from "../../../account/verification-panel";
9
+ import type { AuthUser } from "../api-client";
10
+ import { DialogFrame } from "../components/ui/frame";
11
+ import { useAppLanguage } from "../i18n/react";
12
+ import { useDialog, type PromptContext } from "../ui/dialog";
13
+ import { AuthForm, authFormTitle } from "./auth-form";
14
+ import type { AccountMode } from "./auth-model";
15
+ import { CloudVerificationPanel } from "./verification-panel";
16
16
 
17
17
  function AuthDialog({
18
18
  initialMode,
@@ -38,7 +38,9 @@ export interface EarningsReport {
38
38
  reportedAt: string | null;
39
39
  /** Fiscal quarter end month, YYYY-MM. */
40
40
  fiscalPeriod: string | null;
41
+ /** The company's cap as RV reads it: its latest snapshot at the regular session's price. */
41
42
  marketCap: number | null;
43
+ /** For an upcoming report, the consensus every earnings view shows, with `epsAnalysts` behind it. */
42
44
  epsEstimate: number | null;
43
45
  epsActual: number | null;
44
46
  epsAnalysts: number | null;
@@ -48,6 +50,8 @@ export interface EarningsReport {
48
50
  realized: EarningsRealizedMove | null;
49
51
  /** Whether the date is out, announced or projected; absent from older servers. */
50
52
  dateStatus?: EarningsDateStatus;
53
+ /** The reported figure's basis, null when it cannot be determined; absent from older servers. */
54
+ epsBasis?: EarningsEpsBasis | null;
51
55
  }
52
56
 
53
57
  export interface EarningsCalendarReport extends EarningsReport {
@@ -65,8 +69,6 @@ export interface EarningsCalendarPayload {
65
69
 
66
70
  interface EarningsHistoryReport extends EarningsReport {
67
71
  revenueActual: number | null;
68
- /** Null when the basis cannot be determined; absent from older servers. */
69
- epsBasis?: EarningsEpsBasis | null;
70
72
  }
71
73
 
72
74
  export interface EarningsHistoryPayload {
@@ -212,6 +212,16 @@ export function useAppGlobalShortcuts({
212
212
  return;
213
213
  }
214
214
 
215
+ // Text fields keep Cmd+Z. Everywhere else it restores the previous layout.
216
+ // A detached window has no layout history of its own.
217
+ if (action === "layout-undo" || action === "layout-redo") {
218
+ if (isDetachedWindow || dialogOpen || state.commandBarOpen || state.inputCaptured || event.targetEditable === true) return;
219
+ event.preventDefault();
220
+ event.stopPropagation();
221
+ dispatch({ type: action === "layout-undo" ? "UNDO_LAYOUT" : "REDO_LAYOUT" });
222
+ return;
223
+ }
224
+
215
225
  if (dialogOpen) return;
216
226
 
217
227
  if (!isDetachedWindow && action === "command-bar") {
@@ -30,6 +30,8 @@ export type CoreKeybindingActionId =
30
30
  | "pane-share"
31
31
  | "pane-ask"
32
32
  | "layout-gallery"
33
+ | "layout-undo"
34
+ | "layout-redo"
33
35
  | "tidy-windows"
34
36
  | "window-move-mode"
35
37
  | "window-resize-mode";
@@ -62,6 +64,8 @@ export const PANE_ACTION_IDS = new Set<CoreKeybindingActionId>([
62
64
  "pane-share",
63
65
  "pane-ask",
64
66
  "layout-gallery",
67
+ "layout-undo",
68
+ "layout-redo",
65
69
  "tidy-windows",
66
70
  "window-move-mode",
67
71
  "window-resize-mode",
@@ -245,6 +249,18 @@ export const KEYBINDING_ACTIONS: readonly KeybindingActionDef[] = [
245
249
  description: "Open layout actions.",
246
250
  defaults: ["CmdOrCtrl+Shift+L"],
247
251
  },
252
+ {
253
+ id: "layout-undo",
254
+ category: "Pane Management",
255
+ description: "Undo the last layout change.",
256
+ defaults: ["CmdOrCtrl+Z"],
257
+ },
258
+ {
259
+ id: "layout-redo",
260
+ category: "Pane Management",
261
+ description: "Redo the last undone layout change.",
262
+ defaults: ["CmdOrCtrl+Shift+Z"],
263
+ },
248
264
  {
249
265
  id: "tidy-windows",
250
266
  category: "Pane Management",
@@ -11,7 +11,7 @@ import {
11
11
  import type { DesktopDeepLinkBridge } from "../../types/desktop-deeplink";
12
12
  import type { DesktopWindowBridge } from "../../types/desktop-window";
13
13
  import { requestAccountManagementTab } from "../../plugins/builtin/account-management/navigation";
14
- import { openTeamPane } from "../../plugins/builtin/cloud/team/pane-request";
14
+ import { openTeamPane } from "../../team/pane-request";
15
15
  import { chatController } from "../../plugins/builtin/chat/controller";
16
16
  import { getShare, type ShareRecord } from "../../shares/api";
17
17
  import { openShareLink, registerShareOpener } from "../../shares/open";
package/src/app.tsx CHANGED
@@ -14,7 +14,7 @@ import { Header } from "./components/layout/header";
14
14
  import { StatusBar } from "./components/layout/status-bar";
15
15
  import { useAppChromeShown } from "./components/layout/presentation";
16
16
  import { useLinkedLayoutSync } from "./layout-marketplace/linked-sync";
17
- import { useTeamCollectionsSync } from "./plugins/builtin/cloud/team/collections-sync";
17
+ import { useTeamCollectionsSync } from "./team/collections-sync";
18
18
  import { Shell } from "./components/layout/shell";
19
19
  import { DetachedPaneShell } from "./components/layout/detached-pane-shell";
20
20
  import { TransientLayoutProvider } from "./components/layout/transient-layout";
@@ -4,11 +4,11 @@
4
4
  "src/app/global-shortcuts.ts": ["help"],
5
5
  "src/app/pane-runtime/plugin-bindings.ts": ["broker-manager","layout-manager"],
6
6
  "src/app/runtime/broker-import.ts": ["portfolio-list"],
7
- "src/app/runtime/desktop-deeplink.ts": ["account-management","chat","cloud"],
7
+ "src/app/runtime/desktop-deeplink.ts": ["account-management","chat"],
8
8
  "src/app/runtime/startup.ts": ["chat"],
9
9
  "src/cli/commands/market.ts": ["holders","insider","research","sec"],
10
10
  "src/cli/commands/options-chain.ts": ["options","options-calculator"],
11
- "src/cli/commands/options-leaps.ts": ["options","vol-surface"],
11
+ "src/cli/commands/options-leaps.ts": ["iv-history","options","vol-surface"],
12
12
  "src/cli/commands/overview.ts": ["correlation","econ","market-movers","sectors","world-indices"],
13
13
  "src/cli/commands/ticker.ts": ["notes"],
14
14
  "src/cli/desktop-pane-shot.ts": ["market-valuation","time-sales"],
@@ -24,21 +24,16 @@
24
24
  "src/components/command-bar/routes/root/command-items.ts": ["portfolio-list"],
25
25
  "src/components/command-bar/routes/root/help-items.ts": ["help"],
26
26
  "src/components/command-bar/routes/root/shortcut-items.ts": ["portfolio-list"],
27
- "src/components/command-bar/workflow/builtin.ts": ["cloud","portfolio-list"],
28
- "src/components/command-bar/workflow/collection-actions.ts": ["cloud","portfolio-list"],
27
+ "src/components/command-bar/workflow/builtin.ts": ["portfolio-list"],
28
+ "src/components/command-bar/workflow/collection-actions.ts": ["portfolio-list"],
29
29
  "src/components/command-bar/workflow/ops.ts": ["correlation","portfolio-list","ticker-detail"],
30
30
  "src/components/command-bar/workflow/tickers.ts": ["portfolio-list"],
31
- "src/components/feedback-dialog.tsx": ["cloud"],
32
31
  "src/components/form-modal/routes.ts": ["portfolio-list"],
33
32
  "src/components/index.ts": ["portfolio-list"],
34
33
  "src/components/layout/shell/index.tsx": ["cloud"],
35
- "src/components/layout/status-bar-groups.ts": ["cloud"],
36
- "src/components/layout/status-bar.tsx": ["cloud"],
37
34
  "src/components/onboarding/use-onboarding-upgrade.ts": ["chat"],
38
35
  "src/components/onboarding/wizard-model.ts": ["portfolio-list"],
39
36
  "src/components/onboarding/wizard-positions.ts": ["portfolio-list"],
40
- "src/layout-marketplace/gallery.tsx": ["cloud"],
41
- "src/layout-marketplace/linked-sync.ts": ["cloud"],
42
37
  "src/layout/desks.ts": ["chart-composer"],
43
38
  "src/plugins/catalog-browser.ts": ["alerts","analytics","attention","awards","backtest","bond-calculator","catalysts","cds","central-bank-rates","changelog","chart-composer","clinical-trials","cloud","comment-letters","commodities-energy","commodity-balances","company-attention","company-kpis","connections","correlation","cot","cpi","credit-boards","credit-conditions","credit-documents","crypto-board","debt-maturities","dividend-yield","docs","doe","earnings-calls","earnings-ripple","econ","econ-statistics","equity-screener","estimate-revisions","executives","exposure","fear-greed","filing-events","fixed-income","futures","futures-curve","fx-matrix","gpu","help","insider","ipo-calendar","iv-history","jobs","jodi","kelly-sizer","layout-manager","macro-day","market-halts","market-heatmap","market-valuation","members","mna","money-markets","news","openfda","options","options-calculator","options-positioning","options-scenario","pe-band","perps","portfolio-list","power","primary-dealers","rate-path","realized-vol","relative-rotation","research","research-search","revenue-breakdown","reverse-dcf","risk-factors","scanner","seasonality","sec","sectors","short-interest","short-volume","social-mentions","supply-chain","ticker-detail","time-sales","trace-bonds","treasury-auctions","treasury-daily","vol-surface","volatility","world-indices","world-venue-map","yield-curve"],
44
39
  "src/plugins/catalog-ui.ts": ["alerts","clinical-trials","cloud","comment-letters","custom-view","fear-greed","ipo-calendar","market-halts","market-heatmap","members","news","notes","openfda","research-search"],
@@ -55,18 +55,19 @@ import {
55
55
  formatOptionDeltaCell,
56
56
  formatOptionIvCell,
57
57
  formatOptionQuoteCell,
58
- missingExpiryMessage,
59
58
  OPTION_MODEL_RATE,
60
59
  OPTIONS_USAGE,
61
60
  optionRows,
62
61
  optionsBasisFields,
63
62
  optionsBasisFootnote,
64
63
  parseOptionExpiration,
65
- requestListedOpenInterest,
66
64
  valueChainAtModelSpot,
67
65
  type OptionsChainBasis,
68
66
  } from "./options-chain";
69
- 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";
70
71
  import type { CliResultColumn } from "../result";
71
72
  import type { PriceHistoryResult } from "../../types/price-history";
72
73
  import {
@@ -143,7 +144,7 @@ import { formatBasisPoints, toBasisPoints } from "../../utils/basis-points";
143
144
  import { notATickerSymbol, providerMissReason } from "../../sources/provider-errors";
144
145
  import { isNotATickerMessage, withListingSuggestion } from "../not-a-ticker";
145
146
  import { describeSuggestion, suggestListing } from "../listing-suggestion";
146
- import { splitSymbolArgs } from "../symbol-args";
147
+ import { splitSymbolArgs, uniqueSecuritySymbols } from "../symbol-args";
147
148
  import { cliPerpetualContract, perpetualContractStats, type PerpetualContractPayload } from "../perpetual-contract";
148
149
  import { subMinuteDataMessage } from "../known-gaps";
149
150
  import { secRegistrantMismatchMessage, SecRegistrantMismatchError, areDifferentCompanies } from "../../sources/sec-registrant";
@@ -224,9 +225,13 @@ function parseNewsFeed(value: string | undefined): NewsQuery["feed"] | undefined
224
225
  return value && VALID_NEWS_FEEDS.has(value as NewsFeed) ? value as NewsQuery["feed"] : undefined;
225
226
  }
226
227
 
227
- /** 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
+ */
228
233
  function normalizeSymbols(args: string[]): string[] {
229
- return splitSymbolArgs(args).map((symbol) => symbol.toUpperCase());
234
+ return uniqueSecuritySymbols(splitSymbolArgs(args).map((symbol) => symbol.toUpperCase()));
230
235
  }
231
236
 
232
237
  /** What JSON metadata says about the listing a command resolved. */
@@ -1472,10 +1477,9 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1472
1477
  const expiration = takeOption(args, "--expiration");
1473
1478
  const exchangeOption = takeOption(args, "--exchange");
1474
1479
  if (leapsOptions.leaps) {
1475
- if (expiration != null || rawArgs.includes("--expiration")) {
1476
- ctx.fail("--leaps reads every expiry more than a year out; drop --expiration.", `Usage: gloomberb ${LEAPS_USAGE}`);
1477
- }
1478
- 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 ?? "" } : {} });
1479
1483
  if ("error" in criteria) ctx.fail(criteria.error, `Usage: gloomberb ${LEAPS_USAGE}`);
1480
1484
  const symbols = normalizeSymbols(args);
1481
1485
  if (symbols.length === 0) ctx.fail("--leaps needs at least one symbol.", `Usage: gloomberb ${LEAPS_USAGE}`);
@@ -1505,8 +1509,6 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1505
1509
  const quotePromise = loadListingQuote(market.dataProvider, listing);
1506
1510
  const fate = listingFateLoader(listing, market, quotePromise);
1507
1511
  const inputsPromise = loadOptionModelInputs(market.dataProvider, listing, quotePromise, refresh);
1508
- // OPX's listing dates the chain's open interest; asked for alongside the chain when the expiry is known.
1509
- const listedOpenInterest = expirationDate != null ? requestListedOpenInterest(symbol, expirationDate) : null;
1510
1512
  const { result, chain: loaded } = await loadForListing(listing, market, ctx, async () => {
1511
1513
  const cached = await market.dataProvider.getCachedQuery?.("getOptionsChain", [symbol, exchange, expirationDate, undefined])
1512
1514
  .load({ force: refresh });
@@ -1539,7 +1541,7 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
1539
1541
  // IV and delta are valued at one spot and instant, named under the table and on every exported row.
1540
1542
  const { chain, spot: modelSpot } = valueChainAtModelSpot(loaded, inputs, Date.now(), identity.exchange);
1541
1543
  const basis: OptionsChainBasis = { spot: modelSpot, dividendYield: inputs.dividendYield ?? null,
1542
- openInterest: await loadOptionsChainOpenInterest(symbol, chain, listedOpenInterest) };
1544
+ openInterest: await loadOptionsChainOpenInterest(symbol, chain) };
1543
1545
  const basisFields = optionsBasisFields(basis);
1544
1546
  const currency = [...chain.calls, ...chain.puts].find((contract) => contract.currency?.trim())?.currency.trim().toUpperCase()
1545
1547
  ?? quote?.currency ?? null;
@@ -1815,10 +1817,10 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
1815
1817
  options: [
1816
1818
  {
1817
1819
  flags: "--expiration <YYYY-MM-DD|unix>",
1818
- 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",
1819
1821
  },
1820
1822
  EXCHANGE_OPTION,
1821
- { 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" },
1822
1824
  { flags: "--side <calls|puts>", description: `With --leaps: the side to rank (default ${DEFAULT_LEAPS_CRITERIA.side}s)` },
1823
1825
  {
1824
1826
  flags: "--delta <low-high>",
@@ -1829,6 +1831,17 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
1829
1831
  description: `With --leaps: widest bid/ask spread, in percent of the midpoint (default ${DEFAULT_LEAPS_CRITERIA.maxSpreadPercent})`,
1830
1832
  },
1831
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
+ },
1832
1845
  {
1833
1846
  flags: "--sort <order>",
1834
1847
  description: "With --leaps: carry (extrinsic per year, lowest first; the default), spread, oi, delta, expiry or symbol",
@@ -1837,7 +1850,10 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
1837
1850
  sections: [{
1838
1851
  title: "LEAPS",
1839
1852
  lines: [
1840
- "--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.`,
1841
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.",
1842
1858
  ],
1843
1859
  }],
@@ -1847,6 +1863,8 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
1847
1863
  "options AAPL:NASDAQ --json",
1848
1864
  "options AAPL MSFT NVDA GOOGL AMZN META --leaps",
1849
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",
1850
1868
  ],
1851
1869
  },
1852
1870
  execute: runOptions,
@@ -1,4 +1,4 @@
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";
@@ -9,10 +9,10 @@ import {
9
9
  formatModelSpotPrice, modelSpotLabel, optionModelSpot, type OptionModelSpot,
10
10
  } from "../../plugins/builtin/options/model-spot";
11
11
  import {
12
- chainOpenInterestAsOf, loadListedOpenInterest, openInterestAsOfLabel, type OpenInterestAsOf,
12
+ chainOpenInterestAsOf, loadListedOpenInterest, openInterestAsOfLabel, type OpenInterestAsOf, statedOpenInterestAsOf,
13
13
  } from "../../plugins/builtin/options/open-interest-as-of";
14
14
  import { DEFAULT_OPTION_CALC_DRAFT } from "../../plugins/builtin/options-calculator/model";
15
- import { expiryIsoDate, missingExpiryText, parseOptionExpiration } from "../../utils/option-expiry";
15
+ import { expiryIsoDate, parseOptionExpiration } from "../../utils/option-expiry";
16
16
 
17
17
  export const OPTIONS_USAGE = "options <symbol> [--expiration <YYYY-MM-DD|unix>]";
18
18
 
@@ -29,22 +29,6 @@ function listedDates(expirations: readonly number[]): string[] {
29
29
  return [...new Set(expirations.map((expiration) => expiryIsoDate(expiration)).filter(Boolean))].sort();
30
30
  }
31
31
 
32
- /**
33
- * Why a chain request for one expiry came back without contracts, and which
34
- * expiries it could have been: the few nearest the requested date, so a typo or
35
- * an unlisted LEAPS date points at the neighbours to pick.
36
- */
37
- export function missingExpiryMessage(
38
- symbol: string,
39
- requested: number,
40
- expirations: readonly number[],
41
- ): { message: string; details?: string } {
42
- return {
43
- message: missingExpiryText(requested, expirations, symbol),
44
- details: `Run gloomberb options ${symbol} to list every expiry (--json carries them too).`,
45
- };
46
- }
47
-
48
32
  const EXPIRY_LIST_LIMIT = 40;
49
33
  const EXPIRY_LIST_WIDTH = 100;
50
34
  const EXPIRY_GAP = 2;
@@ -113,16 +97,30 @@ function optionModelFigures(chain: OptionsChain, inputs: OptionModelInputs): Map
113
97
  return figures;
114
98
  }
115
99
 
116
- /** 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
+ */
117
116
  export async function loadOptionModelInputs(
118
117
  dataProvider: Pick<DataProvider, "getTickerFinancials">,
119
118
  listing: CliListing,
120
119
  quote: Promise<Quote | null>,
121
120
  refresh: boolean,
121
+ underlying: Promise<TickerFinancials | null> = loadUnderlyingFinancials(dataProvider, listing, refresh),
122
122
  ): Promise<OptionModelInputs> {
123
- const financials = await Promise.resolve().then(() => dataProvider.getTickerFinancials(
124
- listing.request.symbol, listing.request.exchange, { cacheMode: refresh ? "refresh" : "default" },
125
- )).catch(() => null);
123
+ const financials = await underlying;
126
124
  const current = [await quote, financials?.quote]
127
125
  .find((candidate) => candidate && candidate.stale !== true && isFiniteNumber(candidate.price) && candidate.price > 0);
128
126
  const dividendYield = financials?.fundamentals?.dividendYield;
@@ -161,21 +159,19 @@ export function valueChainAtModelSpot(
161
159
  };
162
160
  }
163
161
 
164
- /** The open-interest date of the chain's expiry, from OPX's listing of it; null when the listing cannot be read. */
165
- export async function loadOptionsChainOpenInterest(
166
- symbol: string, chain: OptionsChain, listed?: ReturnType<typeof loadListedOpenInterest> | null,
167
- ): Promise<OpenInterestAsOf | null> {
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;
168
169
  const expiration = [...chain.calls, ...chain.puts][0]?.expiration;
169
170
  if (expiration == null) return null;
170
- const payload = await (listed ?? loadListedOpenInterest(symbol, expiryIsoDate(expiration)));
171
+ const payload = await loadListedOpenInterest(symbol, expiryIsoDate(expiration));
171
172
  return payload ? chainOpenInterestAsOf(chain, payload) : null;
172
173
  }
173
174
 
174
- /** OPX's listing of an expiry, requested while the chain loads. */
175
- export function requestListedOpenInterest(symbol: string, expiration: number) {
176
- return loadListedOpenInterest(symbol, expiryIsoDate(expiration));
177
- }
178
-
179
175
  /**
180
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`.
181
177
  * An open interest whose date cannot be established says why.