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.
- package/package.json +1 -1
- package/src/{plugins/builtin/cloud → account}/auth-dialog.tsx +7 -7
- package/src/api-client/data.ts +5 -1
- package/src/api-client/doe.ts +19 -0
- package/src/api-client/earnings.ts +4 -2
- package/src/api-client/index.ts +1 -0
- package/src/app/global-shortcuts.ts +10 -0
- package/src/app/keybindings/actions.ts +16 -0
- package/src/app/runtime/desktop-deeplink.ts +1 -1
- package/src/app/runtime/update.ts +16 -0
- package/src/app.tsx +1 -1
- package/src/architecture/core-builtin-allowlist.json +4 -9
- package/src/architecture/dom-cell-metrics-allowlist.json +100 -26
- package/src/cli/commands/market.ts +87 -17
- package/src/cli/commands/options-chain.ts +93 -23
- package/src/cli/commands/options-leaps.ts +462 -63
- package/src/cli/commands/search.ts +10 -7
- package/src/cli/commands/ticker.ts +19 -10
- package/src/cli/glossary.ts +1 -1
- package/src/cli/index.ts +14 -6
- package/src/cli/known-gaps.ts +22 -0
- package/src/cli/pane-functions/catalog.ts +45 -10
- package/src/cli/pane-functions/dom.ts +36 -10
- package/src/cli/pane-functions/function-help.ts +4 -2
- package/src/cli/pane-functions/headless.ts +12 -1
- package/src/cli/pane-functions/index.ts +44 -3
- package/src/cli/pane-functions/lock-note.ts +56 -0
- package/src/cli/pane-functions/report-notices.ts +15 -0
- package/src/cli/symbol-args.ts +19 -0
- package/src/components/command-bar/list/view.tsx +2 -1
- package/src/components/command-bar/workflow/builtin.ts +2 -2
- package/src/components/command-bar/workflow/collection-actions.ts +1 -1
- package/src/components/feedback-dialog.tsx +1 -1
- package/src/components/index.ts +3 -2
- package/src/components/layout/header.tsx +58 -14
- package/src/components/layout/shell/chrome.ts +10 -4
- package/src/components/layout/shell/index.tsx +1 -1
- package/src/components/layout/status-bar-groups.ts +1 -1
- package/src/components/layout/status-bar.tsx +2 -2
- package/src/components/markdown-text.tsx +14 -2
- package/src/components/reader/article-reader.tsx +1 -1
- package/src/components/reader/detail-reader.tsx +10 -7
- package/src/components/reader/reader-text.tsx +32 -17
- package/src/components/ui/column-grid.tsx +206 -0
- package/src/components/ui/display.tsx +40 -3
- package/src/components/ui/fill.ts +8 -0
- package/src/components/ui/index.ts +2 -0
- package/src/layout-marketplace/gallery.tsx +2 -2
- package/src/layout-marketplace/linked-sync.ts +1 -1
- package/src/market-data/market/freshness.ts +3 -0
- package/src/market-data/published-apac-sessions.ts +12 -2
- package/src/market-data/quotes/screener-live-quotes.ts +31 -7
- package/src/plugins/builtin/account-management/form-components.tsx +4 -7
- package/src/plugins/builtin/account-management/password-dialog.tsx +3 -2
- package/src/plugins/builtin/backtest/pane.tsx +9 -3
- package/src/plugins/builtin/buildout/detail/company.tsx +4 -3
- package/src/plugins/builtin/buildout/detail/ui.tsx +9 -12
- package/src/plugins/builtin/chart-composer/index.tsx +1 -1
- package/src/plugins/builtin/chart-composer/presets.ts +33 -1
- package/src/plugins/builtin/chat/channel-labels.ts +2 -2
- package/src/plugins/builtin/chat/content/channel-navigation.ts +1 -1
- package/src/plugins/builtin/chat/content/index.tsx +2 -2
- package/src/plugins/builtin/chat/conversations.ts +1 -1
- package/src/plugins/builtin/chat/index.tsx +2 -2
- package/src/plugins/builtin/chat/share/flow.tsx +1 -1
- package/src/plugins/builtin/chat/sidebar-rows.ts +1 -1
- package/src/plugins/builtin/chat/sidebar.tsx +2 -2
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +2 -2
- package/src/plugins/builtin/cloud/askg/pane.tsx +2 -1
- package/src/plugins/builtin/cloud/askg/tool-timeline.tsx +3 -2
- package/src/plugins/builtin/cloud/auth-actions.tsx +1 -1
- package/src/plugins/builtin/cloud/auth-commands.ts +1 -1
- package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -1
- package/src/plugins/builtin/cloud/team/acm-tab.tsx +3 -3
- package/src/plugins/builtin/cloud/team/capability.ts +2 -2
- package/src/plugins/builtin/cloud/team/command.ts +4 -4
- package/src/plugins/builtin/cloud/team/module.tsx +3 -3
- package/src/plugins/builtin/cloud/team/pane-model.ts +2 -2
- package/src/plugins/builtin/cloud/team/pane-sections.tsx +1 -1
- package/src/plugins/builtin/cloud/team/pane-ui.tsx +1 -1
- package/src/plugins/builtin/cloud/team/pane.tsx +3 -3
- package/src/plugins/builtin/cloud/team/status-widget.tsx +3 -3
- package/src/plugins/builtin/cloud/team/team-state-host.ts +1 -1
- package/src/plugins/builtin/cloud/team/views.ts +2 -2
- package/src/plugins/builtin/cloud/thesis/board-pane.tsx +1 -1
- package/src/plugins/builtin/cloud/thesis/detail.tsx +12 -10
- package/src/plugins/builtin/cloud/thesis/ticker-tab.tsx +1 -1
- package/src/plugins/builtin/cloud/upgrade-command.ts +1 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +8 -4
- package/src/plugins/builtin/connections/pane.tsx +16 -5
- package/src/plugins/builtin/cpi/pane.tsx +5 -4
- package/src/plugins/builtin/custom-view/pane.tsx +2 -2
- package/src/plugins/builtin/dividend-yield/headless.ts +8 -2
- package/src/plugins/builtin/dividend-yield/pane.tsx +4 -2
- package/src/plugins/builtin/docs/blocks.tsx +13 -2
- package/src/plugins/builtin/docs/pane.tsx +6 -5
- package/src/plugins/builtin/doe/index.tsx +11 -4
- package/src/plugins/builtin/doe/pane.tsx +5 -4
- package/src/plugins/builtin/doe/prices.ts +84 -0
- package/src/plugins/builtin/doe/series.ts +4 -1
- package/src/plugins/builtin/earnings/board-model.ts +18 -1
- package/src/plugins/builtin/earnings/board-table.ts +18 -3
- package/src/plugins/builtin/earnings/headless.ts +13 -2
- package/src/plugins/builtin/earnings/history-model.ts +1 -1
- package/src/plugins/builtin/earnings/history-table.ts +2 -2
- package/src/plugins/builtin/earnings-calls/headless.ts +7 -21
- package/src/plugins/builtin/earnings-calls/index.tsx +2 -0
- package/src/plugins/builtin/earnings-calls/lock-note.ts +17 -0
- package/src/plugins/builtin/equity-screener/headless.ts +59 -1
- package/src/plugins/builtin/equity-screener/live.ts +82 -31
- package/src/plugins/builtin/equity-screener/pane.tsx +60 -23
- package/src/plugins/builtin/estimate-revisions/client.ts +1 -1
- package/src/plugins/builtin/estimate-revisions/headless.ts +9 -2
- package/src/plugins/builtin/estimate-revisions/index.tsx +3 -0
- package/src/plugins/builtin/estimate-revisions/lock-note.ts +23 -0
- package/src/plugins/builtin/executives/index.tsx +18 -0
- package/src/plugins/builtin/exposure/charts.tsx +4 -3
- package/src/plugins/builtin/filing-events/index.tsx +8 -10
- package/src/plugins/builtin/fixed-income/pane.tsx +2 -1
- package/src/plugins/builtin/gpu/equities.tsx +9 -6
- package/src/plugins/builtin/help/feedback-tab.tsx +1 -1
- package/src/plugins/builtin/iv-history/format.ts +3 -0
- package/src/plugins/builtin/iv-history/headless.ts +3 -1
- package/src/plugins/builtin/iv-history/index.tsx +3 -1
- package/src/plugins/builtin/iv-history/model.ts +3 -1
- package/src/plugins/builtin/iv-history/screen-pane.tsx +3 -1
- package/src/plugins/builtin/iv-history/universe.ts +1 -1
- package/src/plugins/builtin/jobs/headless.ts +6 -20
- package/src/plugins/builtin/jobs/index.tsx +2 -0
- package/src/plugins/builtin/jobs/lock-note.ts +16 -0
- package/src/plugins/builtin/notes/index.tsx +1 -1
- package/src/plugins/builtin/notes/owner.tsx +2 -2
- package/src/plugins/builtin/options/carry.ts +36 -0
- package/src/plugins/builtin/options/index.tsx +2 -1
- package/src/plugins/builtin/options/model-spot.ts +94 -0
- package/src/plugins/builtin/options/open-interest-as-of.ts +74 -0
- package/src/plugins/builtin/options/view.tsx +69 -11
- package/src/plugins/builtin/options-calculator/pane.tsx +56 -1
- package/src/plugins/builtin/options-calculator/premium-history.ts +34 -0
- package/src/plugins/builtin/options-scenario/client.ts +33 -9
- package/src/plugins/builtin/options-scenario/csp.ts +168 -0
- package/src/plugins/builtin/options-scenario/evidence.ts +3 -0
- package/src/plugins/builtin/options-scenario/headless.ts +24 -8
- package/src/plugins/builtin/options-scenario/index.tsx +3 -1
- package/src/plugins/builtin/options-scenario/pane.tsx +32 -6
- package/src/plugins/builtin/portfolio-list/mutations.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane/remove-ticker.ts +1 -1
- package/src/plugins/builtin/research/corporate-actions-pane.tsx +4 -2
- package/src/plugins/builtin/research/event-model.ts +7 -4
- package/src/plugins/builtin/risk-factors/index.tsx +18 -0
- package/src/plugins/builtin/risk-factors/pane.tsx +2 -1
- package/src/plugins/builtin/scanner/headless.ts +9 -2
- package/src/plugins/builtin/scanner/index.ts +3 -0
- package/src/plugins/builtin/scanner/lock-note.ts +17 -0
- package/src/plugins/builtin/series-inventory.ts +2 -1
- package/src/plugins/builtin/shared/cloud-denied.ts +39 -8
- package/src/plugins/builtin/shared/earnings-report.ts +10 -4
- package/src/plugins/builtin/shared/pro-lock-note.ts +58 -0
- package/src/plugins/builtin/shared/series-list-detail.tsx +10 -5
- package/src/plugins/builtin/social-mentions/client.ts +3 -3
- package/src/plugins/builtin/social-mentions/headless.ts +6 -2
- package/src/plugins/builtin/social-mentions/index.tsx +2 -1
- package/src/plugins/builtin/social-mentions/lock-note.ts +15 -0
- package/src/plugins/builtin/supply-chain/flow.tsx +15 -13
- package/src/plugins/builtin/supply-chain/graph.tsx +6 -4
- package/src/plugins/builtin/supply-chain/pane.tsx +8 -5
- package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +14 -1
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +9 -7
- package/src/plugins/builtin/ticker-detail/headless.ts +4 -1
- package/src/plugins/builtin/ticker-detail/overview/components.tsx +105 -104
- package/src/plugins/builtin/ticker-detail/overview/model.ts +9 -2
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +34 -8
- package/src/plugins/builtin/vol-surface/pane.tsx +6 -3
- package/src/public/__snapshots__/public-api.test.ts.snap +2 -0
- package/src/renderers/dom/data-table/index.tsx +48 -15
- package/src/renderers/dom/data-table/row.tsx +1 -1
- package/src/renderers/dom/desktop/popover.tsx +6 -15
- package/src/renderers/dom/host/input.tsx +27 -3
- package/src/renderers/dom/host/surface-3d.tsx +6 -4
- package/src/renderers/dom/styles.css +26 -6
- package/src/renderers/electrobun/view/app-services.ts +2 -22
- package/src/renderers/electrobun/view/news-source.ts +42 -0
- package/src/series/validate.ts +5 -0
- package/src/shares/dialog.tsx +2 -1
- package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
- package/src/sources/provider-router/financials.ts +5 -0
- package/src/{plugins/builtin/cloud/team → team}/collections-sync.ts +6 -6
- package/src/{plugins/builtin/cloud/team → team}/collections.ts +3 -3
- package/src/{plugins/builtin/cloud/team → team}/model.ts +2 -2
- package/src/{plugins/builtin/cloud/team → team}/pane-request.ts +1 -1
- package/src/{plugins/builtin/cloud/team → team}/store.ts +3 -3
- package/src/types/data-provider.ts +3 -0
- package/src/types/financials.ts +28 -0
- package/src/types/plugin.ts +17 -4
- package/src/utils/decimal-comma.ts +18 -0
- package/src/utils/exchanges.ts +7 -0
- package/src/utils/financial-statements.ts +25 -0
- package/src/utils/forward-dividend.ts +39 -9
- package/src/utils/fundamentals.ts +41 -1
- package/src/utils/option-expiry.ts +16 -0
- 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 {
|
|
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
|
-
/**
|
|
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
|
-
|
|
1466
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
/**
|
|
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
|
|
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 })),
|