gloomberb 0.18.4 → 0.18.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +32 -1
- package/src/account/auth-model.ts +7 -3
- package/src/api-client/auth.ts +9 -4
- package/src/api-client/catalysts.ts +2 -1
- package/src/api-client/data.ts +14 -0
- package/src/api-client/index.ts +160 -11
- package/src/api-client/live-scan.ts +91 -0
- package/src/api-client/members.ts +12 -0
- package/src/api-client/request.ts +222 -41
- package/src/api-client/research-activity.ts +3 -10
- package/src/api-client/types.ts +6 -0
- package/src/app/keybindings/index.ts +3 -0
- package/src/app/keybindings/labels.ts +18 -1
- package/src/app/keybindings/resolve.ts +14 -0
- package/src/app/runtime/broker-import.ts +43 -26
- package/src/architecture/core-builtin-allowlist.json +1 -1
- package/src/cli/commands/fred-series.ts +5 -0
- package/src/cli/commands/keybindings.ts +23 -0
- package/src/cli/commands/market.ts +68 -22
- package/src/cli/commands/options-chain.ts +59 -3
- package/src/cli/commands/overview.ts +119 -31
- package/src/cli/commands/search.ts +18 -2
- package/src/cli/commands/ticker.ts +2 -1
- package/src/cli/errors.ts +2 -2
- package/src/cli/export-source.ts +29 -1
- package/src/cli/glossary.ts +49 -0
- package/src/cli/history-rows.ts +30 -7
- package/src/cli/known-gaps.ts +5 -2
- package/src/cli/listing-arg.ts +16 -5
- package/src/cli/pane-functions/freshness.ts +25 -13
- package/src/cli/pane-functions/function-help.ts +10 -3
- package/src/cli/pane-functions/headless.ts +39 -5
- package/src/cli/pane-functions/index.ts +10 -3
- package/src/cli/pane-functions/market-session.ts +15 -5
- package/src/cli/pane-functions/report.ts +5 -1
- package/src/cli/pane-functions/resolver.ts +18 -2
- package/src/cli/quote-answer.ts +10 -4
- package/src/cli/report-tables.ts +18 -7
- package/src/cli/restore-plugins.ts +13 -1
- package/src/cli/result.ts +57 -5
- package/src/components/chart/composite/composite-chart.tsx +22 -4
- package/src/components/chart/composite/format.ts +27 -0
- package/src/components/chart/composite/levels.ts +2 -0
- package/src/components/chart/composite/panel-series.ts +1 -0
- package/src/components/chart/composite/panel-surface.tsx +9 -2
- package/src/components/chart/composite/range-statistics.ts +62 -0
- package/src/components/chart/composite/scene.ts +7 -3
- package/src/components/chart/composite/series-axis-tags.tsx +16 -9
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/command-bar/panel/prompt-binding.ts +0 -8
- package/src/components/command-bar/workflow/alert.ts +1 -1
- package/src/components/layout/header.tsx +26 -40
- package/src/components/layout/pane/footer/index.tsx +1 -0
- package/src/components/layout/pane/footer/model.ts +27 -0
- package/src/components/onboarding/onboarding-frame.tsx +1 -1
- package/src/components/onboarding/portfolio-step/positions-panel.tsx +2 -2
- package/src/components/ui/display.tsx +5 -1
- package/src/core/state/app/layout.ts +2 -0
- package/src/core/state/app/state.ts +9 -0
- package/src/core/state/app/types.ts +3 -0
- package/src/data/saved-copies.ts +55 -16
- package/src/docs/snapshot.json +361 -84
- package/src/i18n/es.ts +1 -1
- package/src/i18n/ja.ts +1 -1
- package/src/i18n/ko.ts +1 -1
- package/src/i18n/zh-cn.ts +1 -1
- package/src/i18n/zh-tw.ts +1 -1
- package/src/market-data/coordinator/financials.ts +10 -2
- package/src/market-data/coordinator/index.ts +3 -1
- package/src/market-data/coordinator/quotes.ts +4 -2
- package/src/market-data/market/change-basis.ts +33 -5
- package/src/market-data/market/dated-previous-close.ts +203 -0
- package/src/market-data/market/futures-week.ts +68 -0
- package/src/market-data/market/status.ts +16 -4
- package/src/market-data/options-alternatives.ts +60 -0
- package/src/market-data/quotes/contributions.ts +20 -2
- package/src/market-data/quotes/resolution.ts +3 -0
- package/src/market-data/selectors.ts +3 -0
- package/src/notifications/notification-log.ts +6 -4
- package/src/plugins/builtin/alerts/alert-engine.ts +5 -1
- package/src/plugins/builtin/alerts/calendar-events.ts +57 -6
- package/src/plugins/builtin/alerts/cli.ts +59 -20
- package/src/plugins/builtin/alerts/command.ts +7 -0
- package/src/plugins/builtin/alerts/constants.ts +1 -1
- package/src/plugins/builtin/alerts/fired-log.ts +45 -4
- package/src/plugins/builtin/alerts/format.ts +8 -2
- package/src/plugins/builtin/alerts/headless.ts +10 -5
- package/src/plugins/builtin/alerts/history-rows.ts +5 -2
- package/src/plugins/builtin/alerts/history.ts +2 -0
- package/src/plugins/builtin/alerts/index.tsx +66 -16
- package/src/plugins/builtin/alerts/live.ts +3 -3
- package/src/plugins/builtin/alerts/metric-command.ts +61 -0
- package/src/plugins/builtin/alerts/metric-data.ts +70 -0
- package/src/plugins/builtin/alerts/metric-model.ts +175 -0
- package/src/plugins/builtin/alerts/metric-parity-fixture.json +725 -0
- package/src/plugins/builtin/alerts/pane.tsx +42 -29
- package/src/plugins/builtin/alerts/storage.ts +33 -10
- package/src/plugins/builtin/alerts/types.ts +3 -0
- package/src/plugins/builtin/backtest/headless.ts +10 -5
- package/src/plugins/builtin/backtest/pane.tsx +9 -4
- package/src/plugins/builtin/backtest/presets.ts +29 -3
- package/src/plugins/builtin/broker-manager/index.tsx +3 -2
- package/src/plugins/builtin/broker-manager/model.ts +6 -1
- package/src/plugins/builtin/broker-trading/controller.ts +22 -12
- package/src/plugins/builtin/broker-trading/index.tsx +13 -1
- package/src/plugins/builtin/broker-trading/order-feed.ts +9 -4
- package/src/plugins/builtin/broker-trading/pane.tsx +1 -1
- package/src/plugins/builtin/builtin-plugin-meta.ts +1 -1
- package/src/plugins/builtin/catalysts/model.ts +2 -1
- package/src/plugins/builtin/chart-composer/catalog-inventory.ts +3 -1
- package/src/plugins/builtin/chart-composer/chart-spec-edit.ts +1 -1
- package/src/plugins/builtin/chart-composer/cli-options.ts +5 -5
- package/src/plugins/builtin/chart-composer/data-catalog-pane.tsx +5 -2
- package/src/plugins/builtin/chart-composer/editor/controller.ts +2 -0
- package/src/plugins/builtin/chart-composer/editor/model.ts +2 -0
- package/src/plugins/builtin/chart-composer/editor.tsx +7 -5
- package/src/plugins/builtin/chart-composer/formula.ts +71 -18
- package/src/plugins/builtin/chart-composer/headless-schema.ts +14 -3
- package/src/plugins/builtin/chart-composer/headless.ts +13 -2
- package/src/plugins/builtin/chart-composer/pane.tsx +17 -2
- package/src/plugins/builtin/chart-composer/presets.ts +50 -15
- package/src/plugins/builtin/chart-composer/quick-add.tsx +40 -25
- package/src/plugins/builtin/chart-composer/session-history.ts +18 -9
- package/src/plugins/builtin/chart-composer/use-series-catalog.ts +40 -26
- package/src/plugins/builtin/chat/content/command-suggestions.ts +92 -0
- package/src/plugins/builtin/chat/content/composer.tsx +13 -6
- package/src/plugins/builtin/chat/content/footer.tsx +6 -3
- package/src/plugins/builtin/chat/content/index.tsx +50 -21
- package/src/plugins/builtin/chat/content/open-targets.ts +14 -3
- package/src/plugins/builtin/chat/controller/session-runtime.ts +4 -2
- package/src/plugins/builtin/chat/index.tsx +23 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +1 -0
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +23 -0
- package/src/plugins/builtin/chat/message/pane-chip.tsx +50 -20
- package/src/plugins/builtin/chat/pane-links/commands.ts +265 -0
- package/src/plugins/builtin/chat/pane-links/model.ts +12 -3
- package/src/plugins/builtin/chat/pane-links/open.ts +8 -1
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +10 -1
- package/src/plugins/builtin/cloud/plugin.tsx +1 -1
- package/src/plugins/builtin/econ/calendar-model.ts +14 -9
- package/src/plugins/builtin/econ/figures.ts +72 -0
- package/src/plugins/builtin/econ/index.tsx +33 -3
- package/src/plugins/builtin/econ/types.ts +2 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +79 -31
- package/src/plugins/builtin/econ-statistics/stats.ts +1 -0
- package/src/plugins/builtin/econ-statistics/view.ts +3 -0
- package/src/plugins/builtin/equity-screener/pane.tsx +2 -2
- package/src/plugins/builtin/exposure/charts.tsx +2 -2
- package/src/plugins/builtin/exposure/model.ts +13 -1
- package/src/plugins/builtin/exposure/pane.tsx +4 -4
- package/src/plugins/builtin/futures/index.tsx +1 -0
- package/src/plugins/builtin/futures/model.ts +2 -1
- package/src/plugins/builtin/futures/table.tsx +2 -2
- package/src/plugins/builtin/fx-matrix/client.ts +6 -1
- package/src/plugins/builtin/fx-matrix/headless.ts +5 -2
- package/src/plugins/builtin/fx-matrix/index.tsx +13 -3
- package/src/plugins/builtin/fx-matrix/live-legs.ts +12 -0
- package/src/plugins/builtin/help/keybindings-editor.tsx +18 -14
- package/src/plugins/builtin/iv-history/headless.ts +3 -0
- package/src/plugins/builtin/macro-day/pane.tsx +7 -5
- package/src/plugins/builtin/market-movers/footer.ts +2 -2
- package/src/plugins/builtin/members/breadth-pane.tsx +81 -0
- package/src/plugins/builtin/members/client.ts +19 -4
- package/src/plugins/builtin/members/headless.ts +20 -7
- package/src/plugins/builtin/members/index.tsx +9 -9
- package/src/plugins/builtin/members/model.ts +23 -4
- package/src/plugins/builtin/members/pane.tsx +25 -15
- package/src/plugins/builtin/options/analytics.ts +26 -2
- package/src/plugins/builtin/options/model-spot.ts +5 -2
- package/src/plugins/builtin/options/strike-window.ts +2 -1
- package/src/plugins/builtin/options/table.ts +1 -7
- package/src/plugins/builtin/options/view.tsx +25 -8
- package/src/plugins/builtin/options-positioning/client.ts +19 -0
- package/src/plugins/builtin/options-positioning/headless.ts +54 -14
- package/src/plugins/builtin/options-positioning/index.tsx +3 -3
- package/src/plugins/builtin/options-positioning/model.ts +47 -8
- package/src/plugins/builtin/options-positioning/pane.tsx +68 -38
- package/src/plugins/builtin/perps/freshness.ts +12 -10
- package/src/plugins/builtin/perps/live-rows.ts +10 -3
- package/src/plugins/builtin/perps/pane.tsx +15 -11
- package/src/plugins/builtin/portfolio-list/cli/render.ts +5 -2
- package/src/plugins/builtin/portfolio-list/headless.ts +1 -1
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +11 -8
- package/src/plugins/builtin/rate-path/pane.tsx +2 -2
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +4 -4
- package/src/plugins/builtin/scanner/feed.ts +2 -2
- package/src/plugins/builtin/scanner/index.ts +24 -1
- package/src/plugins/builtin/scanner/live-condition-editor.tsx +222 -0
- package/src/plugins/builtin/scanner/live-feed.ts +168 -0
- package/src/plugins/builtin/scanner/live-headless.ts +114 -0
- package/src/plugins/builtin/scanner/live-model.ts +351 -0
- package/src/plugins/builtin/scanner/live-pane.tsx +677 -0
- package/src/plugins/builtin/scanner/live-track.ts +186 -0
- package/src/plugins/builtin/series-inventory.ts +1 -0
- package/src/plugins/builtin/shared/macro-releases.ts +65 -0
- package/src/plugins/builtin/shared/scanner-capture.ts +3 -0
- package/src/plugins/builtin/shared/use-quote-board.ts +8 -2
- package/src/plugins/builtin/shared/volatility/pricing.ts +7 -1
- package/src/plugins/builtin/themes/headless.ts +6 -4
- package/src/plugins/builtin/themes/model.ts +1 -1
- package/src/plugins/builtin/themes/pane.tsx +3 -3
- package/src/plugins/builtin/ticker-detail/quote-footer.ts +6 -1
- package/src/plugins/builtin/vol-surface/charts.tsx +9 -4
- package/src/plugins/builtin/vol-surface/client.ts +6 -4
- package/src/plugins/builtin/vol-surface/headless.ts +62 -13
- package/src/plugins/builtin/vol-surface/index.tsx +2 -2
- package/src/plugins/builtin/vol-surface/model.ts +22 -7
- package/src/plugins/builtin/vol-surface/pane.tsx +44 -7
- package/src/plugins/builtin/vol-surface/tables.ts +31 -3
- package/src/plugins/builtin/volatility/dvol.ts +46 -0
- package/src/plugins/builtin/volatility/pane.tsx +2 -2
- package/src/plugins/builtin/volatility/series.ts +26 -0
- package/src/plugins/builtin/world-indices/footer.ts +1 -1
- package/src/plugins/catalog-browser.ts +13 -8
- package/src/portfolio/column-values.ts +5 -5
- package/src/public/__snapshots__/public-api.test.ts.snap +1 -0
- package/src/renderers/browser/app-services.ts +1 -7
- package/src/renderers/browser/config-host.ts +2 -55
- package/src/renderers/browser/main.tsx +7 -57
- package/src/renderers/electrobun/bun/desktop/app-name.ts +222 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +17 -7
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +74 -25
- package/src/renderers/electrobun/bun/desktop/state-broadcaster.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +70 -3
- package/src/renderers/electrobun/bun/desktop/workspace/index.ts +2 -0
- package/src/renderers/electrobun/bun/index.ts +25 -1
- package/src/renderers/electrobun/shared/app-name.ts +17 -0
- package/src/renderers/electrobun/view/cli-pane-shot-frame.tsx +3 -3
- package/src/renderers/electrobun/view/desktop/window/snapshot.ts +1 -0
- package/src/renderers/opentui/toast-host.tsx +71 -43
- package/src/sources/gloomberb-cloud/normalizers.ts +2 -0
- package/src/sources/provider-router/batches.ts +10 -2
- package/src/sources/provider-router/financial-routes.ts +3 -3
- package/src/sources/provider-router/history.ts +28 -7
- package/src/sources/provider-router/index.ts +146 -10
- package/src/sources/provider-router/search.ts +13 -2
- package/src/state/app/context/index.tsx +3 -0
- package/src/sync/auth-transition.ts +19 -9
- package/src/sync/baseline.ts +10 -4
- package/src/sync/controller.ts +66 -16
- package/src/sync/react.ts +4 -1
- package/src/sync/types.ts +3 -0
- package/src/telemetry/usage-counts.ts +0 -18
- package/src/theme/colors.ts +8 -0
- package/src/tickers/company-name.ts +59 -0
- package/src/tickers/listing-venues.ts +23 -0
- package/src/tickers/search/bare-symbol.ts +36 -0
- package/src/tickers/search/index.ts +36 -7
- package/src/tickers/search/ranking.ts +10 -1
- package/src/tickers/search/types.ts +5 -0
- package/src/time-series/base-series.ts +13 -5
- package/src/time-series/daily-valuation.ts +62 -0
- package/src/time-series/field-catalog.ts +1 -3
- package/src/time-series/forward-valuation.ts +32 -0
- package/src/time-series/fundamentals.ts +155 -7
- package/src/time-series/hooks.ts +1 -0
- package/src/time-series/price-units.ts +72 -2
- package/src/time-series/resolve-context.ts +5 -1
- package/src/time-series/resolve.ts +61 -12
- package/src/time-series/series-presentation.ts +40 -6
- package/src/time-series/spec.ts +2 -1
- package/src/time-series/studies.ts +270 -72
- package/src/time-series/transforms.ts +1 -0
- package/src/time-series/types.ts +5 -0
- package/src/time-series/valuation-releases.ts +39 -0
- package/src/types/data-provider.ts +9 -1
- package/src/types/desktop-window.ts +2 -0
- package/src/types/financials.ts +41 -1
- package/src/types/headless.ts +42 -4
- package/src/types/instrument.ts +3 -0
- package/src/types/plugin.ts +1 -1
- package/src/updater/command-names.ts +12 -7
- package/src/utils/financial-statements.ts +6 -1
- package/src/utils/network-failure.ts +9 -6
- package/src/utils/option-expiry.ts +48 -1
- package/src/utils/options.ts +10 -0
- package/src/utils/plugin-proxy-hosts.json +1 -0
- package/src/utils/price-history.ts +5 -0
- package/src/renderers/browser/first-screen.ts +0 -193
|
@@ -53,7 +53,9 @@ import {
|
|
|
53
53
|
} from "../../plugins/builtin/research/analyst-model";
|
|
54
54
|
import { getPublishedUsEquityCalendarYears, getPublishedUsEquitySession } from "../../market-data/published-us-sessions";
|
|
55
55
|
import { isQuoteStaleForCurrentSession, noPrintsTodayText } from "../../market-data/quotes/freshness";
|
|
56
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
coinOptionsAlternativesText, isCoinWithoutOptions, isOptionsUnavailableError, listedOptionsAlternative, optionsUnavailableTitle,
|
|
58
|
+
} from "../../market-data/options-alternatives";
|
|
57
59
|
import {
|
|
58
60
|
atReportTtmRate, cliFuturesContract, futuresContractMetrics, exportedDividendFundamentals, fundamentalsReportTables, loadDividendDistributions,
|
|
59
61
|
loadReportBondFacts, renderFundamentalsReport, writesCurrencyCodes,
|
|
@@ -72,9 +74,15 @@ import {
|
|
|
72
74
|
formatExpiryList,
|
|
73
75
|
loadOptionModelInputs,
|
|
74
76
|
loadOptionsChainOpenInterest,
|
|
77
|
+
chainCoinCurrency,
|
|
78
|
+
formatCoinPremiumCell,
|
|
75
79
|
formatOptionDeltaCell,
|
|
80
|
+
formatOptionDollarCell,
|
|
81
|
+
formatOptionGammaCell,
|
|
76
82
|
formatOptionIvCell,
|
|
77
83
|
formatOptionQuoteCell,
|
|
84
|
+
isMarkedChain,
|
|
85
|
+
MARKED_OPTION_FIELDS,
|
|
78
86
|
OPTION_MODEL_RATE,
|
|
79
87
|
OPTION_VOLATILITY_FIELDS,
|
|
80
88
|
OPTIONS_USAGE,
|
|
@@ -87,7 +95,7 @@ import {
|
|
|
87
95
|
} from "./options-chain";
|
|
88
96
|
import {
|
|
89
97
|
expirationHint, expiredExpiryText, expiryIsoDate, isLiveOptionExpiry, liveDefaultExpiry, liveOptionExpiries, missingExpiryMessage,
|
|
90
|
-
OPTION_EXPIRATION_FORMAT, OPTION_EXPIRATION_PLACEHOLDER, withLiveExpiryList,
|
|
98
|
+
OPTION_EXPIRATION_FORMAT, OPTION_EXPIRATION_PLACEHOLDER, optionExpiryVenue, withLiveExpiryList,
|
|
91
99
|
} from "../../utils/option-expiry";
|
|
92
100
|
import { DATE_OPTION_EXAMPLES, isoDateOption, isoDateToday } from "../../utils/calendar-date";
|
|
93
101
|
import { apiClient } from "../../api-client";
|
|
@@ -115,6 +123,7 @@ import {
|
|
|
115
123
|
closedPeriodBarDay,
|
|
116
124
|
formingPeriodBar,
|
|
117
125
|
historyBarStatusNote,
|
|
126
|
+
venueTradingDay,
|
|
118
127
|
historyCoverageWarning,
|
|
119
128
|
historyFacts,
|
|
120
129
|
historyFlagNote,
|
|
@@ -683,7 +692,7 @@ function changeBasisNotes(results: QuoteCliRecord[], now = Date.now()): string[]
|
|
|
683
692
|
function withPreviousCloseDate(quote: Quote | null | undefined): (Quote & { previousCloseDate?: string | null }) | null {
|
|
684
693
|
if (!quote) return null;
|
|
685
694
|
const previousCloseDate = quotePreviousCloseDate(quote);
|
|
686
|
-
const entries = Object.entries(quote);
|
|
695
|
+
const entries = Object.entries(quote).filter(([key]) => key !== "previousCloseDate");
|
|
687
696
|
const at = entries.findIndex(([key]) => key === "previousClose");
|
|
688
697
|
entries.splice(at < 0 ? entries.length : at + 1, 0, ["previousCloseDate", previousCloseDate]);
|
|
689
698
|
return Object.fromEntries(entries) as Quote & { previousCloseDate?: string | null };
|
|
@@ -1007,13 +1016,14 @@ async function runQuote(rawArgs: string[], ctx: Parameters<CliCommandDef["execut
|
|
|
1007
1016
|
: listings.map((listing) => [listing, startTickerChangeLookup(market.dataProvider, listing.symbol, listing.exchange)] as const));
|
|
1008
1017
|
// The service's quote of the moment, not each form's local copy from whenever it was last asked for:
|
|
1009
1018
|
// BRK.B saved on NYSE, BRK-B and BRK.B:NYSE print one quote. A local copy answers only offline.
|
|
1010
|
-
|
|
1019
|
+
// Each row prints what its move is from (Chg Basis).
|
|
1020
|
+
const results = await market.dataProvider.getQuotesBatch(targets, { forceRefresh: ctx.cliOptions.refresh, revalidate: true, datesChangeBasis: true });
|
|
1011
1021
|
const listingOf = (result: QuoteBatchResult, index: number) => listings[targets.indexOf(result.target)] ?? listings[index]!;
|
|
1012
1022
|
const asked = results.map(listingOf);
|
|
1013
1023
|
const request = { command: commandName, noun: "quote" };
|
|
1014
1024
|
const answers = await Promise.all(results.map((result, index) => {
|
|
1015
1025
|
const listing = asked[index]!;
|
|
1016
|
-
return answerQuote(result, listing, market, request, ctx.cliOptions.refresh, venues.get(listing) ?? null);
|
|
1026
|
+
return answerQuote(result, listing, market, request, ctx.cliOptions.refresh, venues.get(listing) ?? null, { datesChangeBasis: true });
|
|
1017
1027
|
}));
|
|
1018
1028
|
const now = Date.now();
|
|
1019
1029
|
const symbolChanges = await Promise.all(answers.map(async ({ result }, index) => {
|
|
@@ -1436,7 +1446,7 @@ async function loadListingHistory(
|
|
|
1436
1446
|
// The status line says how far a bar still forming runs; the note is for the bars it does not name.
|
|
1437
1447
|
gaps ?? [], historyJumpNote(data) ?? [], historyFlagNote(data) ?? [], historyMissingVolumeNote(data) ?? [],
|
|
1438
1448
|
historyVenueVolumeNote(data) ?? [],
|
|
1439
|
-
historyBarStatusNote(forming ? served : data) ?? [],
|
|
1449
|
+
historyBarStatusNote(forming ? served : data, { tradingDay: venueTradingDay(venue, Date.now()) }) ?? [],
|
|
1440
1450
|
].flat();
|
|
1441
1451
|
return {
|
|
1442
1452
|
listing, identity, unit, data, resolution, interval, intraday, volume, freshness, notes,
|
|
@@ -1641,7 +1651,7 @@ async function runHistoryList(
|
|
|
1641
1651
|
oldest: null,
|
|
1642
1652
|
});
|
|
1643
1653
|
// A saved copy is marked on the bars a symbol loaded, not on the rows drawn from them (loadListingHistory
|
|
1644
|
-
// reads it there): the list ends "couldn't refresh; saved <time>" for the oldest copy among its symbols.
|
|
1654
|
+
// reads it there): the list ends "couldn't refresh; saved copy from <time>" for the oldest copy among its symbols.
|
|
1645
1655
|
const savedAt = served.flatMap((history) => history.freshness?.savedAt ?? []).sort()[0];
|
|
1646
1656
|
const freshness = listFreshness && savedAt && !listFreshness.savedAt ? { ...listFreshness, savedAt } : listFreshness;
|
|
1647
1657
|
const decimals = (row: Record<string, unknown>) => bySymbol.get(String(row.symbol))?.decimals ?? 2;
|
|
@@ -2642,6 +2652,8 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
2642
2652
|
const fate = listingFateLoader(listing, market, quotePromise);
|
|
2643
2653
|
const inputsPromise = loadOptionModelInputs(market.dataProvider, listing, quotePromise, refresh);
|
|
2644
2654
|
const now = Date.now();
|
|
2655
|
+
// Expiries close on the listing's clock: a coin's options (Deribit) settle at 08:00 UTC on any day.
|
|
2656
|
+
const expiryVenue = optionExpiryVenue(symbol, exchange);
|
|
2645
2657
|
const readChain = async (expiry: number | undefined) => {
|
|
2646
2658
|
const cached = await market.dataProvider.getCachedQuery?.("getOptionsChain", [symbol, exchange, expiry, undefined])
|
|
2647
2659
|
.load({ force: refresh });
|
|
@@ -2653,17 +2665,17 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
2653
2665
|
};
|
|
2654
2666
|
};
|
|
2655
2667
|
// An expiry whose last session has closed has no chain to show: it says so, and names the nearest live one.
|
|
2656
|
-
if (expirationDate != null && !isLiveOptionExpiry(expirationDate, now,
|
|
2668
|
+
if (expirationDate != null && !isLiveOptionExpiry(expirationDate, now, expiryVenue)) {
|
|
2657
2669
|
const listed = await readChain(undefined).then(({ chain }) => chain.expirationDates,
|
|
2658
2670
|
logFailure("cli-market", "Read available option expirations failed", []));
|
|
2659
2671
|
ctx.fail(`expiry ${expiryIsoDate(expirationDate)} for ${listing.key} `
|
|
2660
|
-
+ expiredExpiryText(expirationDate, listed, now, { exchange, flag: "expiration" }),
|
|
2672
|
+
+ expiredExpiryText(expirationDate, listed, now, { exchange: expiryVenue, flag: "expiration" }),
|
|
2661
2673
|
`Run gloom options ${listing.key} to list the live expiries.`);
|
|
2662
2674
|
}
|
|
2663
2675
|
const { result, chain: read } = await loadForListing(listing, market, ctx, async () => {
|
|
2664
2676
|
const first = await readChain(expirationDate);
|
|
2665
2677
|
// A source can still default to the expiry that lapsed at Friday's close; the nearest live one is shown instead.
|
|
2666
|
-
const live = expirationDate == null ? liveDefaultExpiry(first.chain, now,
|
|
2678
|
+
const live = expirationDate == null ? liveDefaultExpiry(first.chain, now, expiryVenue) : undefined;
|
|
2667
2679
|
if (live === null) return { ...first, chain: { ...first.chain, calls: [], puts: [] } };
|
|
2668
2680
|
if (live == null) return first;
|
|
2669
2681
|
// The default's list stays the catalogue: a dated chain can list fewer expiries.
|
|
@@ -2672,6 +2684,8 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
2672
2684
|
expirationDates: [...new Set([...first.chain.expirationDates, ...shown.chain.expirationDates])].sort((a, b) => a - b) } };
|
|
2673
2685
|
}, undefined, { command: "options", noun: "options chain", fate }).catch((error: unknown) => {
|
|
2674
2686
|
if (!isOptionsUnavailableError(error)) throw error;
|
|
2687
|
+
// A coin other than BTC and ETH: say which coins have options, and the bitcoin-linked listings.
|
|
2688
|
+
if (isCoinWithoutOptions(listing.key)) return ctx.fail(optionsUnavailableTitle(listing.key), coinOptionsAlternativesText("options"));
|
|
2675
2689
|
// A future or commodity with no chain of its own: name the listed fund whose options stand in.
|
|
2676
2690
|
const alternative = listedOptionsAlternative(listing.key);
|
|
2677
2691
|
// A name read as a fund or an index asks about the company's stock it means, whose options are listed.
|
|
@@ -2681,13 +2695,13 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
2681
2695
|
+ ` Strategy payoffs: gloom fn OSA ${alternative.symbol}`
|
|
2682
2696
|
: "Strategy payoffs on a listed underlying: gloom fn OSA <ticker>");
|
|
2683
2697
|
});
|
|
2684
|
-
const loaded = withLiveExpiryList(read, now,
|
|
2698
|
+
const loaded = withLiveExpiryList(read, now, expiryVenue);
|
|
2685
2699
|
const inputs = await inputsPromise;
|
|
2686
2700
|
if (expirationDate != null && !chainHasExpiry(loaded, expirationDate)) {
|
|
2687
2701
|
// An unlisted date comes back as an empty chain; its own list of expiries, else the default chain's, says what to pick.
|
|
2688
2702
|
const listed = loaded.expirationDates.length > 0 ? loaded.expirationDates : await market.dataProvider
|
|
2689
2703
|
.getOptionsChain(symbol, exchange, undefined, { cacheMode: "default" })
|
|
2690
|
-
.then((fallback) => liveOptionExpiries(fallback.expirationDates, now,
|
|
2704
|
+
.then((fallback) => liveOptionExpiries(fallback.expirationDates, now, expiryVenue),
|
|
2691
2705
|
logFailure("cli-market", "Read available option expirations failed", []));
|
|
2692
2706
|
const miss = missingExpiryMessage(listing.key, expirationDate, listed);
|
|
2693
2707
|
ctx.fail(miss.message, miss.details);
|
|
@@ -2706,7 +2720,10 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
2706
2720
|
: `Options refresh failed; showing the chain stored ${new Date(result.fetchedAt).toISOString()}`
|
|
2707
2721
|
+ (chain.asOf ? ` (last trade ${chain.asOf})` : "");
|
|
2708
2722
|
const sessionWarning = refreshWarning ? null : priorSessionChainWarning(chain, identity.exchange, Date.now());
|
|
2709
|
-
|
|
2723
|
+
// A venue that marks its contracts (Deribit) states IV and greeks without a quote.
|
|
2724
|
+
const marked = isMarkedChain(chain);
|
|
2725
|
+
const coin = chainCoinCurrency(chain);
|
|
2726
|
+
const modelWarning = !marked && inputs.spot == null && (chain.calls.length > 0 || chain.puts.length > 0)
|
|
2710
2727
|
? `IV and delta need a current ${listing.key} quote; those columns are blank`
|
|
2711
2728
|
: null;
|
|
2712
2729
|
const listed = chain.expirationDates.length > 0 || chain.calls.length > 0 || chain.puts.length > 0;
|
|
@@ -2733,25 +2750,28 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
2733
2750
|
// `iv` is annualised volatility and `delta` the option's change per 1.00 move in the underlying, both decimals.
|
|
2734
2751
|
units: { iv: "annualized decimal", delta: "decimal per 1.00 move in the underlying" },
|
|
2735
2752
|
// What each contract's `iv` and `impliedVolatility` are, so neither is read as the other.
|
|
2736
|
-
fields: OPTION_VOLATILITY_FIELDS,
|
|
2753
|
+
fields: marked ? MARKED_OPTION_FIELDS : OPTION_VOLATILITY_FIELDS,
|
|
2737
2754
|
// What `iv` and `delta` on each contract are valued from; the provider's own impliedVolatility is left as sent.
|
|
2738
|
-
model:
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2755
|
+
model: marked
|
|
2756
|
+
? { venue: chain.venue ?? null, marks: "venue", rate: chain.rate ?? 0, basis: "the expiry's forward", coin }
|
|
2757
|
+
: {
|
|
2758
|
+
spot: basisFields.spot, spotBasis: basisFields.spotBasis, spotAsOf: basisFields.spotAsOf,
|
|
2759
|
+
valuationTime: modelSpot ? new Date(modelSpot.valuationTime).toISOString() : null,
|
|
2760
|
+
dividendYield: inputs.dividendYield ?? null, rate: OPTION_MODEL_RATE,
|
|
2761
|
+
},
|
|
2743
2762
|
openInterest: { asOf: basisFields.openInterestAsOf, listedAsOf: basis.openInterest?.listedDate ?? null,
|
|
2744
2763
|
compared: basis.openInterest?.compared ?? 0, matched: basis.openInterest?.matched ?? 0 },
|
|
2745
2764
|
},
|
|
2746
2765
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
2747
2766
|
freshness,
|
|
2748
2767
|
}, {
|
|
2749
|
-
|
|
2768
|
+
// Options a venue other than the US exchanges lists (Deribit's) are headed by that venue, not the coin's board.
|
|
2769
|
+
heading: chain.venue ? `${cliStyles.bold(identity.name || identity.symbol)}${cliStyles.muted(` · ${chain.venue}`)}` : listingHeading(identity),
|
|
2750
2770
|
rows: (data: OptionsChain) => optionRows(data).map((row) => ({ ...row, ...basisFields })),
|
|
2751
2771
|
// Choosing an expiry needs the list; once one is chosen the table is that expiry.
|
|
2752
2772
|
summary: (data: OptionsChain) => expirationDate == null ? formatExpiryList(data.expirationDates) : "",
|
|
2753
2773
|
empty: `No option contracts for ${listingTitle(identity)}.`,
|
|
2754
|
-
...(contractCount > 0 ? { footnote: optionsBasisFootnote(basis) } : {}),
|
|
2774
|
+
...(contractCount > 0 ? { footnote: optionsBasisFootnote(basis, chain) } : {}),
|
|
2755
2775
|
exportIdentity: { symbolOrSeries: listing.key, currencyOrUnit: currency, interval: OPTIONS_EXPORT_INTERVAL },
|
|
2756
2776
|
// Exports state each decimal's unit in its header and end every row with the spot and dates it is as of.
|
|
2757
2777
|
columns: [
|
|
@@ -2770,8 +2790,34 @@ async function runOptions(rawArgs: string[], ctx: Parameters<CliCommandDef["exec
|
|
|
2770
2790
|
{ key: "spotBasis", header: "Spot basis" },
|
|
2771
2791
|
{ key: "spotAsOf", header: "Spot as of" },
|
|
2772
2792
|
{ key: "openInterestAsOf", header: "OI as of" },
|
|
2793
|
+
// A venue's marks and its coin prices (Deribit's BTC and ETH options).
|
|
2794
|
+
...(marked ? [
|
|
2795
|
+
{ key: "mark", header: "Mark (USD)" }, { key: "gamma", header: "Gamma (per 1.00)" },
|
|
2796
|
+
{ key: "vega", header: "Vega (USD per vol point)" }, { key: "theta", header: "Theta (USD per day)" },
|
|
2797
|
+
{ key: "forward", header: "Forward" }, { key: "expiresAt", header: "Settles" },
|
|
2798
|
+
] : []),
|
|
2799
|
+
...(coin ? [
|
|
2800
|
+
{ key: "coinBid", header: `Bid (${coin})` }, { key: "coinAsk", header: `Ask (${coin})` },
|
|
2801
|
+
{ key: "coinMark", header: `Mark (${coin})` }, { key: "coinLast", header: `Last (${coin})` },
|
|
2802
|
+
] : []),
|
|
2773
2803
|
],
|
|
2774
|
-
|
|
2804
|
+
// A coin's options are quoted in the coin: its bid, ask and mark, then the mark in dollars and the venue's greeks.
|
|
2805
|
+
textColumns: coin ? [
|
|
2806
|
+
{ key: "side", header: "Side" },
|
|
2807
|
+
{ key: "contract", header: "Contract", shrink: false },
|
|
2808
|
+
{ key: "strike", header: "Strike", align: "right" },
|
|
2809
|
+
{ key: "coinBid", header: `Bid ${coin}`, align: "right", format: formatCoinPremiumCell },
|
|
2810
|
+
{ key: "coinAsk", header: `Ask ${coin}`, align: "right", format: formatCoinPremiumCell },
|
|
2811
|
+
{ key: "coinMark", header: `Mark ${coin}`, align: "right", format: formatCoinPremiumCell },
|
|
2812
|
+
{ key: "mark", header: "Mark $", align: "right", format: formatOptionDollarCell },
|
|
2813
|
+
{ key: "iv", header: "IV", align: "right", format: formatOptionIvCell },
|
|
2814
|
+
{ key: "delta", header: "Delta", align: "right", format: formatOptionDeltaCell },
|
|
2815
|
+
{ key: "gamma", header: "Gamma", align: "right", format: formatOptionGammaCell, optional: true, dropPriority: 3 },
|
|
2816
|
+
{ key: "vega", header: "Vega $", align: "right", format: formatOptionDollarCell, optional: true, dropPriority: 2 },
|
|
2817
|
+
{ key: "theta", header: "Theta $", align: "right", format: formatOptionDollarCell, optional: true, dropPriority: 2 },
|
|
2818
|
+
{ key: "volume", header: "Vol", align: "right", format: formatCountCell, optional: true, dropPriority: 1 },
|
|
2819
|
+
{ key: "openInterest", header: "OI", align: "right", format: formatCountCell, optional: true, dropPriority: 1 },
|
|
2820
|
+
] : [
|
|
2775
2821
|
{ key: "side", header: "Side" },
|
|
2776
2822
|
{ key: "contract", header: "Contract", shrink: false },
|
|
2777
2823
|
// The contract symbol already carries the date, so this goes first when the table is narrow.
|
|
@@ -13,8 +13,9 @@ import {
|
|
|
13
13
|
chainOpenInterestAsOf, loadListedOpenInterest, openInterestAsOfLabel, type OpenInterestAsOf, statedOpenInterestAsOf,
|
|
14
14
|
} from "../../plugins/builtin/options/open-interest-as-of";
|
|
15
15
|
import { DEFAULT_OPTION_CALC_DRAFT } from "../../plugins/builtin/options-calculator/model";
|
|
16
|
-
import { expiryIsoDate, OPTION_EXPIRATION_PLACEHOLDER, parseOptionExpiration } from "../../utils/option-expiry";
|
|
16
|
+
import { expiryIsoDate, OPTION_EXPIRATION_PLACEHOLDER, parseOptionExpiration, settlementStampTime } from "../../utils/option-expiry";
|
|
17
17
|
import { loadModelDividendYield } from "../../market-data/model-dividend-yield";
|
|
18
|
+
import { formatUtcTime } from "../../utils/utc-time";
|
|
18
19
|
|
|
19
20
|
export const OPTIONS_USAGE = `options <symbol> [--expiration <${OPTION_EXPIRATION_PLACEHOLDER}>]`;
|
|
20
21
|
|
|
@@ -68,6 +69,24 @@ interface OptionModelFigures {
|
|
|
68
69
|
/** The risk-free rate every options valuation in the app assumes. */
|
|
69
70
|
export const OPTION_MODEL_RATE = DEFAULT_OPTION_CALC_DRAFT.rate;
|
|
70
71
|
|
|
72
|
+
/** The venue's own marks a chain carries (Deribit's), stated in its JSON metadata in place of OPTION_VOLATILITY_FIELDS. */
|
|
73
|
+
export const MARKED_OPTION_FIELDS = {
|
|
74
|
+
iv: "The venue's mark implied volatility (decimal), one per contract. Delta and the other greeks under mark are the venue's too: "
|
|
75
|
+
+ "Black-76 at the expiry's forward with no discounting, to its settlement.",
|
|
76
|
+
bid: "Prices in USD are the coin prices (coin) at the expiry's forward (mark.forward), the venue's own model price.",
|
|
77
|
+
mark: "price in USD; iv; delta and gamma per 1.00 move in the underlying; vega per volatility point and theta per calendar day, in USD.",
|
|
78
|
+
} as const;
|
|
79
|
+
|
|
80
|
+
/** Whether a venue marks the chain's contracts itself (Deribit): its IV and greeks are then used as they are. */
|
|
81
|
+
export function isMarkedChain(chain: OptionsChain): boolean {
|
|
82
|
+
return [...chain.calls, ...chain.puts].some((contract) => contract.mark != null);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The coin a chain's premiums are quoted in (BTC), when a venue quotes them in one. */
|
|
86
|
+
export function chainCoinCurrency(chain: OptionsChain): string | null {
|
|
87
|
+
return [...chain.calls, ...chain.puts].find((contract) => contract.coin)?.coin?.currency ?? null;
|
|
88
|
+
}
|
|
89
|
+
|
|
71
90
|
/** What each contract's two volatilities are, stated in the chain's JSON metadata so neither is read as the other. */
|
|
72
91
|
export const OPTION_VOLATILITY_FIELDS = {
|
|
73
92
|
iv: "Annualized volatility (decimal) solved from the quote midpoint, one per strike from its out-of-the-money side, at the "
|
|
@@ -189,10 +208,23 @@ export async function loadOptionsChainOpenInterest(symbol: string, chain: Option
|
|
|
189
208
|
|
|
190
209
|
/**
|
|
191
210
|
* 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`.
|
|
192
|
-
* An open interest whose date cannot be established says why.
|
|
211
|
+
* An open interest whose date cannot be established says why. A chain its
|
|
212
|
+
* venue marks says whose marks they are: `Deribit marks: IV and greeks at
|
|
213
|
+
* each expiry's forward, rate 0%; $ is the BTC price at the forward · OI live, Thu 22 Oct 22:42 UTC`.
|
|
193
214
|
*/
|
|
194
|
-
export function optionsBasisFootnote(basis: OptionsChainBasis): string {
|
|
215
|
+
export function optionsBasisFootnote(basis: OptionsChainBasis, chain?: OptionsChain): string {
|
|
195
216
|
const parts: string[] = [];
|
|
217
|
+
if (chain && isMarkedChain(chain)) {
|
|
218
|
+
const coin = chainCoinCurrency(chain);
|
|
219
|
+
parts.push(`${chain.venue ?? "Venue"} marks: IV and greeks at each expiry's forward, rate ${Number(((chain.rate ?? 0) * 100).toFixed(2))}%`
|
|
220
|
+
+ (coin ? `; $ is the ${coin} price at the forward` : ""));
|
|
221
|
+
// Its expiries settle at a stated time (08:00 UTC), which the expiration stamps carry.
|
|
222
|
+
const settles = [...chain.calls, ...chain.puts].map((contract) => settlementStampTime(contract.expiration)).find(Boolean);
|
|
223
|
+
if (settles) parts.push(`expiries settle ${settles}`);
|
|
224
|
+
const asOf = chain.asOf ? Date.parse(chain.asOf) : Number.NaN;
|
|
225
|
+
parts.push(Number.isFinite(asOf) ? `OI live, ${formatUtcTime(new Date(asOf).toISOString())}` : openInterestAsOfLabel(basis.openInterest));
|
|
226
|
+
return parts.join(" · ");
|
|
227
|
+
}
|
|
196
228
|
if (basis.spot) {
|
|
197
229
|
parts.push(`Greeks at spot ${formatModelSpotPrice(basis.spot.price)} (${modelSpotLabel(basis.spot)}), `
|
|
198
230
|
// An unknown yield is valued as none, and says so rather than read as a stated 0%.
|
|
@@ -236,10 +268,34 @@ export function optionRows(chain: OptionsChain) {
|
|
|
236
268
|
iv: modeled.iv ?? null,
|
|
237
269
|
delta: modeled.delta ?? null,
|
|
238
270
|
expiration: expiryIsoDate(contract.expiration),
|
|
271
|
+
// A venue's marks and coin prices (Deribit), beside the dollar ones.
|
|
272
|
+
...(contract.mark ? {
|
|
273
|
+
mark: contract.mark.price, gamma: contract.mark.gamma, vega: contract.mark.vega, theta: contract.mark.theta,
|
|
274
|
+
forward: contract.mark.forward, expiresAt: new Date(contract.expiration * 1000).toISOString(),
|
|
275
|
+
} : {}),
|
|
276
|
+
...(contract.coin ? {
|
|
277
|
+
coinCurrency: contract.coin.currency, coinBid: contract.coin.bid, coinAsk: contract.coin.ask,
|
|
278
|
+
coinMark: contract.coin.mark, coinLast: contract.coin.last,
|
|
279
|
+
} : {}),
|
|
239
280
|
};
|
|
240
281
|
});
|
|
241
282
|
}
|
|
242
283
|
|
|
284
|
+
/** A premium in a coin, to the venue's tick (0.0001 BTC); a dash for none. */
|
|
285
|
+
export function formatCoinPremiumCell(value: unknown): string {
|
|
286
|
+
return isFiniteNumber(value) && value > 0 ? value.toFixed(4) : "—";
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** A dollar greek or mark (vega, theta, mark) to cents; a dash for none. */
|
|
290
|
+
export function formatOptionDollarCell(value: unknown): string {
|
|
291
|
+
return isFiniteNumber(value) ? value.toFixed(2) : "—";
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Gamma per 1.00 move, tiny for a coin worth 80,000 dollars: to six places, as the venue shows it to five. */
|
|
295
|
+
export function formatOptionGammaCell(value: unknown): string {
|
|
296
|
+
return isFiniteNumber(value) ? value.toFixed(6) : "—";
|
|
297
|
+
}
|
|
298
|
+
|
|
243
299
|
export function formatOptionQuoteCell(row: Record<string, unknown>, side: "bid" | "ask"): string {
|
|
244
300
|
const quote = optionQuoteSide({ bid: Number(row.bid), ask: Number(row.ask) }, side);
|
|
245
301
|
return quote == null ? "—" : String(quote);
|
|
@@ -3,6 +3,8 @@ import { apiClient } from "../../api-client";
|
|
|
3
3
|
import { ApiRequestError } from "../../api-client/errors";
|
|
4
4
|
import type { CliCommandDef } from "../../types/plugin";
|
|
5
5
|
import { withCliServices, withMarketData } from "../context";
|
|
6
|
+
import { CLI_ERROR_CODE, failWithFields } from "../errors";
|
|
7
|
+
import type { CliResultColumn } from "../result";
|
|
6
8
|
import { formatCompact } from "../../utils/format";
|
|
7
9
|
import { DATE_OPTION_EXAMPLES, DATE_OPTION_FORMATS, dateOptionError, readCalendarDate } from "../../utils/calendar-date";
|
|
8
10
|
import {
|
|
@@ -40,6 +42,7 @@ import {
|
|
|
40
42
|
type ImpactFilter,
|
|
41
43
|
} from "../../plugins/builtin/econ/calendar-model";
|
|
42
44
|
import { hasNoReleaseTime, hasNoStatedTime } from "../../plugins/builtin/econ/types";
|
|
45
|
+
import { econRowFigures } from "../../plugins/builtin/econ/figures";
|
|
43
46
|
import { isoDate, rejectExtraArgs, requireArg, requireOneArg, takeFlag, takeOption } from "./command-utils";
|
|
44
47
|
import { buildCorrelationSeries } from "../../plugins/builtin/correlation/matrix/model";
|
|
45
48
|
import { alignDailyCloses, correlateDailyCloses } from "../../plugins/builtin/correlation/compute";
|
|
@@ -70,6 +73,7 @@ import {
|
|
|
70
73
|
fredExportValue,
|
|
71
74
|
fredHistoryNotes,
|
|
72
75
|
fredInterval,
|
|
76
|
+
fredObservesDays,
|
|
73
77
|
fredObservationsFromStart,
|
|
74
78
|
fredProvenance,
|
|
75
79
|
fredPublicationTime,
|
|
@@ -299,6 +303,54 @@ function publisherClock(localTime: unknown, timeZone: unknown): string {
|
|
|
299
303
|
}
|
|
300
304
|
const DAY_MS = 86_400_000;
|
|
301
305
|
|
|
306
|
+
/** The calendar's columns up to the event: shared by the text table and the exports, which keep the raw values under them. */
|
|
307
|
+
function econHeadColumns(viewerZone: string | null): CliResultColumn[] {
|
|
308
|
+
return [
|
|
309
|
+
// Text shows both halves of the event timestamp in UTC, never the host's zone; exports keep the source values.
|
|
310
|
+
{ key: "date", header: "Date", format: (value) => utcDateTimePart(value, "date") },
|
|
311
|
+
// The weekday sits before the clock it is on: the UTC time's, or with --tz the viewer's.
|
|
312
|
+
...viewerZone ? [] : [ECON_DAY_COLUMN],
|
|
313
|
+
// With --tz the viewer's clock has its own column, so the UTC time does not repeat it.
|
|
314
|
+
{ key: "time", header: "Time", format: (value, row) => value === "All Day" ? "All day" : value === ""
|
|
315
|
+
? "" : utcDateTimePart(row.date, "time", !viewerZone) },
|
|
316
|
+
...viewerZone ? [ECON_DAY_COLUMN] : [],
|
|
317
|
+
// Only --tz fills it: the viewer's clock, its weekday in Day before it.
|
|
318
|
+
{ key: "viewerTime", header: viewerZone ? timeZoneCity(viewerZone) : "Viewer", shrink: false,
|
|
319
|
+
format: (value) => typeof value === "string" && viewerZone ? value : "" },
|
|
320
|
+
// Only rows a publisher times itself fill it, so the feeds' calendar prints as before.
|
|
321
|
+
{ key: "localTime", header: "Local", optional: true, dropPriority: 1, format: (value, row) => publisherClock(value, row.timeZone) },
|
|
322
|
+
{ key: "country", header: "Country" },
|
|
323
|
+
{ key: "impact", header: "Impact" },
|
|
324
|
+
{ key: "event", header: "Event" },
|
|
325
|
+
];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Actual, Forecast and Prior: as the text draws them (`30.3K`, `-4.1M bbl`) in the table, as
|
|
330
|
+
* numbers in the exports (30300, -4100000), where Unit and Scale say what they count and how they were drawn.
|
|
331
|
+
*/
|
|
332
|
+
function econFigureColumns(text: boolean): CliResultColumn[] {
|
|
333
|
+
return text
|
|
334
|
+
? [{ key: "actualText", header: "Actual" }, { key: "forecastText", header: "Forecast" }, { key: "priorText", header: "Prior" }]
|
|
335
|
+
: [{ key: "actual", header: "Actual" }, { key: "forecast", header: "Forecast" }, { key: "prior", header: "Prior" }];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const ECON_PUBLISHER_COLUMN: CliResultColumn = {
|
|
339
|
+
key: "publisher", header: "Publisher", format: (value) => typeof value === "string" ? value : "",
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
/** What only the exports say about a row: how to read its figures, who publishes it and where, and the figures as drawn. */
|
|
343
|
+
const econExportColumns: CliResultColumn[] = [
|
|
344
|
+
{ key: "unit", header: "Unit" },
|
|
345
|
+
{ key: "scale", header: "Scale" },
|
|
346
|
+
{ key: "publisher", header: "Publisher" },
|
|
347
|
+
{ key: "sourceUrl", header: "Link" },
|
|
348
|
+
// The figures as the calendar draws them, so a bound (`<1.25%`) that is no number keeps its text.
|
|
349
|
+
{ key: "actualText", header: "actual_text" },
|
|
350
|
+
{ key: "forecastText", header: "forecast_text" },
|
|
351
|
+
{ key: "priorText", header: "prior_text" },
|
|
352
|
+
];
|
|
353
|
+
|
|
302
354
|
/**
|
|
303
355
|
* What an empty calendar says. A window that starts after the last day listed
|
|
304
356
|
* says where the listing stops, whatever the filters, rather than nothing.
|
|
@@ -357,6 +409,22 @@ function blankTimeNotes(rows: ReadonlyArray<{ event: string; country: string; ti
|
|
|
357
409
|
return [...byNote].map(([note, events]) => `No time for ${events.join(", ")}: ${note.charAt(0).toLowerCase()}${note.slice(1)}.`);
|
|
358
410
|
}
|
|
359
411
|
|
|
412
|
+
/**
|
|
413
|
+
* A `--from` the stored calendar does not reach back to, which Gloom Cloud refuses
|
|
414
|
+
* with "The calendar reaches back 400 days": an input error naming the earliest
|
|
415
|
+
* day it takes, and `--json` carries it as `earliest`. Any other failure goes on as it was.
|
|
416
|
+
*/
|
|
417
|
+
function failBeforeCalendar(error: unknown, from: string, todayStart: number): never {
|
|
418
|
+
const days = error instanceof ApiRequestError && error.status === 422 ? /reaches back (\d+) days?/i.exec(error.message)?.[1] : undefined;
|
|
419
|
+
if (!days) throw error;
|
|
420
|
+
const earliest = isoDate(new Date(todayStart - Number(days) * DAY_MS)).slice(0, 10);
|
|
421
|
+
return failWithFields(
|
|
422
|
+
`--from ${from} is before the earliest day the calendar reaches back to, ${earliest}. Use --from ${earliest} or later.`,
|
|
423
|
+
CLI_ERROR_CODE,
|
|
424
|
+
{ earliest, days: Number(days) },
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
360
428
|
async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>[1]) {
|
|
361
429
|
const rawArgs = [...args];
|
|
362
430
|
const country = parseCountry(takeOption(rawArgs, "--country"), ctx);
|
|
@@ -372,7 +440,9 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
|
|
|
372
440
|
const todayStart = Math.floor(Date.now() / DAY_MS) * DAY_MS;
|
|
373
441
|
// An earlier day reads the stored calendar, which keeps past events after the week's listing drops them.
|
|
374
442
|
// A server without it, a server error or no network lists the current calendar (cached when offline) instead.
|
|
375
|
-
const stored = fromOption != null && fromOption < isoDate(new Date(todayStart))
|
|
443
|
+
const stored = fromOption != null && fromOption < isoDate(new Date(todayStart))
|
|
444
|
+
? await fetchEconCalendarFrom(fromOption).catch((error: unknown) => failBeforeCalendar(error, fromOption, todayStart))
|
|
445
|
+
: null;
|
|
376
446
|
const past = stored?.events ? stored : null;
|
|
377
447
|
const unread = stored && !stored.events && stored.failure ? `The stored calendar could not be read (${stored.failure}); listing the current calendar` : null;
|
|
378
448
|
const { data: events, fetchedAt, stale } = past ? { data: past.events, fetchedAt: Date.now(), stale: false } : await loadCalendar(ctx.cliOptions.refresh);
|
|
@@ -400,6 +470,8 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
|
|
|
400
470
|
.map((event) => {
|
|
401
471
|
// The viewer's clock (--tz) for a row with a time: its date and time there.
|
|
402
472
|
const clock = viewerZone && !hasNoReleaseTime(event) ? zonedClock(event.date.getTime(), viewerZone) : null;
|
|
473
|
+
// The figures as numbers, scale multiplied out, with their unit and the scale they are drawn in; the text stays as drawn.
|
|
474
|
+
const figures = econRowFigures(event);
|
|
403
475
|
return {
|
|
404
476
|
date: isoDate(event.date),
|
|
405
477
|
// The weekday on the viewer's clock (--tz), else the UTC day's; a row with no time is its day's.
|
|
@@ -409,9 +481,14 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
|
|
|
409
481
|
country: eventCountry(event),
|
|
410
482
|
impact: event.impact,
|
|
411
483
|
event: event.event,
|
|
412
|
-
actual:
|
|
413
|
-
forecast:
|
|
414
|
-
prior:
|
|
484
|
+
actual: figures.actual,
|
|
485
|
+
forecast: figures.forecast,
|
|
486
|
+
prior: figures.prior,
|
|
487
|
+
unit: figures.unit,
|
|
488
|
+
scale: figures.scale,
|
|
489
|
+
actualText: event.actual ?? null,
|
|
490
|
+
forecastText: event.forecast ?? null,
|
|
491
|
+
priorText: event.prior ?? null,
|
|
415
492
|
// The publisher's own clock, for rows Gloom Cloud adds from a bank's or a statistics office's calendar.
|
|
416
493
|
localTime: event.localTime ?? null,
|
|
417
494
|
timeZone: event.timeZone ?? null,
|
|
@@ -420,15 +497,19 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
|
|
|
420
497
|
// Who publishes the figure (Freddie Mac for the 30-year mortgage rate), the period it covers and its page.
|
|
421
498
|
publisher: event.source?.name ?? null,
|
|
422
499
|
referencePeriod: event.referencePeriod ?? null,
|
|
423
|
-
|
|
500
|
+
// When the figure came out: the row's time once it has an actual (its day alone when it states no time), null before.
|
|
501
|
+
released: event.actual?.trim() ? (hasNoReleaseTime(event) ? isoDate(event.date).slice(0, 10) : isoDate(event.date)) : null,
|
|
424
502
|
sourceUrl: event.source?.url ?? null,
|
|
503
|
+
// The same indicator from one release to the next; empty until Gloom Cloud names it.
|
|
504
|
+
indicatorId: event.indicatorId ?? null,
|
|
425
505
|
};
|
|
426
506
|
});
|
|
427
507
|
// A filter of countries only, none of which has anything in the window, says so as its empty list.
|
|
428
508
|
const allUnscheduled = unscheduled.length > 0 && unscheduled.length === country.split(",").length;
|
|
429
|
-
const
|
|
509
|
+
const notes = [listedBefore, listedAfter, ...(rows.length > 0 ? blankTimeNotes(rows) : []),
|
|
430
510
|
rows.length > 0 && unscheduled.length > 0 ? noScheduledReleasesMessage(unscheduled) : null]
|
|
431
|
-
.filter((note): note is string => !!note)
|
|
511
|
+
.filter((note): note is string => !!note);
|
|
512
|
+
const footnote = notes.join("\n");
|
|
432
513
|
ctx.printResult({
|
|
433
514
|
data: rows,
|
|
434
515
|
...(unread ? { warnings: [unread] } : {}),
|
|
@@ -440,28 +521,21 @@ async function runEcon(args: string[], ctx: Parameters<CliCommandDef["execute"]>
|
|
|
440
521
|
defaultLimit: 50,
|
|
441
522
|
empty: allUnscheduled ? noScheduledReleasesMessage(unscheduled) : econEmptyMessage(from, listedThrough, listedAfter != null),
|
|
442
523
|
...(footnote ? { footnote } : {}),
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
{ key: "impact", header: "Impact" },
|
|
459
|
-
{ key: "event", header: "Event" },
|
|
460
|
-
{ key: "actual", header: "Actual" },
|
|
461
|
-
{ key: "forecast", header: "Forecast" },
|
|
462
|
-
{ key: "prior", header: "Prior" },
|
|
463
|
-
{ key: "publisher", header: "Publisher", format: (value) => typeof value === "string" ? value : "" },
|
|
464
|
-
],
|
|
524
|
+
// What the text says under the table, a script reads in the CSV's closing lines.
|
|
525
|
+
exportNotes: notes,
|
|
526
|
+
// `as_of` is the release: when a row's figure came out, blank for one that has none yet.
|
|
527
|
+
exportDated: true,
|
|
528
|
+
exportIdentity: (row) => ({
|
|
529
|
+
symbolOrSeries: typeof row.indicatorId === "string" ? row.indicatorId : null,
|
|
530
|
+
// A percent is `pct` here as in `fred` and `yield-curve`; `unit` keeps the `%` the calendar draws.
|
|
531
|
+
currencyOrUnit: row.unit === "%" ? "pct" : typeof row.unit === "string" ? row.unit : null,
|
|
532
|
+
asOf: typeof row.released === "string" ? row.released : null,
|
|
533
|
+
released: typeof row.released === "string" ? row.released : null,
|
|
534
|
+
period: typeof row.referencePeriod === "string" ? row.referencePeriod : null,
|
|
535
|
+
}),
|
|
536
|
+
// The exports add what a script needs to read a row; the text table keeps the columns it had.
|
|
537
|
+
columns: [...econHeadColumns(viewerZone), ...econFigureColumns(false), ...econExportColumns],
|
|
538
|
+
textColumns: [...econHeadColumns(viewerZone), ...econFigureColumns(true), ECON_PUBLISHER_COLUMN],
|
|
465
539
|
});
|
|
466
540
|
});
|
|
467
541
|
}
|
|
@@ -545,12 +619,16 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
|
|
|
545
619
|
if (mismatch) ctx.fail(mismatch);
|
|
546
620
|
// Each row's change in the unit its value exports in, and the date it is from; a holiday's stays blank.
|
|
547
621
|
const changes = horizon ? fredChanges(seriesId, data.observations, horizon) : null;
|
|
622
|
+
const releasedOf = (observation: { publisher?: string; publishedAt?: string; releasedOn?: string }) =>
|
|
623
|
+
observation.releasedOn ?? (observation.publisher?.trim() ? observation.publishedAt : undefined);
|
|
548
624
|
// A value FRED does not list yet carries its publisher and when it published, in every export.
|
|
549
|
-
const exportRow = (observation: { date: string; value: number | null; publisher?: string; publishedAt?: string }) => ({
|
|
625
|
+
const exportRow = (observation: { date: string; value: number | null; publisher?: string; publishedAt?: string; releasedOn?: string }) => ({
|
|
550
626
|
date: observation.date,
|
|
551
627
|
value: fredExportValue(seriesId, observation.value),
|
|
552
628
|
...(changes ? { change: changes.get(observation.date)?.change ?? null, changeFrom: changes.get(observation.date)?.changeFrom ?? null } : {}),
|
|
553
629
|
...(observation.publisher?.trim() ? { publisher: observation.publisher.trim(), ...(observation.publishedAt ? { publishedAt: observation.publishedAt } : {}) } : {}),
|
|
630
|
+
// When the figure came out, where the data says: only a series' newest observation does. `date` is the period it covers.
|
|
631
|
+
...releasedOf(observation) ? { released: releasedOf(observation) } : {},
|
|
554
632
|
});
|
|
555
633
|
// A --start before the series begins comes back padded with blank days: the rows start at its first observation.
|
|
556
634
|
const rows = fredObservationsFromStart(data.observations, data.info?.observationStart)
|
|
@@ -570,6 +648,7 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
|
|
|
570
648
|
ctx.fail(`No ${seriesId} observations on or after ${startDate}.`, fredEmptyAdvice(seriesId, latest));
|
|
571
649
|
}
|
|
572
650
|
const periodicity = periodicityOf(data.info?.frequency);
|
|
651
|
+
const observesDays = fredObservesDays(data.info?.frequency);
|
|
573
652
|
const newestDate = rows.reduce<string | null>((newest, row) => (!newest || row.date > newest ? row.date : newest), null);
|
|
574
653
|
// A newest value FRED does not list yet names its publisher, Treasury for a yield; FRED's own stay "FRED".
|
|
575
654
|
const ahead = fredProvenance(data.observations.filter((observation) => observation.date === newestDate), seriesId);
|
|
@@ -598,7 +677,16 @@ async function runFred(rawArgs: string[], ctx: Parameters<CliCommandDef["execute
|
|
|
598
677
|
dateKey: "date",
|
|
599
678
|
empty: fredEmptyMessage(startDate, latest == null ? null : exportRow(latest)),
|
|
600
679
|
...(notes.length ? { footnote: notes.join("\n"), exportNotes: notes } : {}),
|
|
601
|
-
|
|
680
|
+
// `as_of` is the release. A figure with none stated is as of its own day when the series is known to observe
|
|
681
|
+
// days (a daily or weekly rate), and of nothing otherwise, never the first day of the period it covers.
|
|
682
|
+
exportDated: true,
|
|
683
|
+
exportIdentity: (row) => {
|
|
684
|
+
const released = typeof row.released === "string" ? row.released : null;
|
|
685
|
+
const date = typeof row.date === "string" ? row.date : null;
|
|
686
|
+
return { symbolOrSeries: seriesId, currencyOrUnit: unit, interval, asOf: released ?? (observesDays ? date : null), period: date, released };
|
|
687
|
+
},
|
|
688
|
+
// The release closes the row with the other dating columns, so it is not one of the observation's own.
|
|
689
|
+
columns: [...new Set(rows.flatMap((row) => Object.keys(row)))].filter((key) => key !== "released").map((key) => ({ key, header: key })),
|
|
602
690
|
textColumns: [
|
|
603
691
|
{ key: "date", header: "Date" },
|
|
604
692
|
isSpreadSeries(seriesId)
|
|
@@ -25,6 +25,7 @@ import { formatMarketPriceWithCurrency, quoteFormatOptions } from "../../market-
|
|
|
25
25
|
import { formerIssuerLine, startTickerChangeLookup, type TickerChangeLookup } from "../../tickers/ticker-changes";
|
|
26
26
|
import { fail, failRetryable } from "../errors";
|
|
27
27
|
import { isProviderBusy, SEARCH_BUSY_CODE, SEARCH_BUSY_MESSAGE } from "../../sources/provider-errors";
|
|
28
|
+
import { isNetworkFailure } from "../../utils/network-failure";
|
|
28
29
|
import type { MarketContext } from "../types";
|
|
29
30
|
import type { CliCommandContext, GloomPlugin } from "../../types/plugin";
|
|
30
31
|
import { indexRootTryLine } from "../index-roots";
|
|
@@ -137,6 +138,8 @@ export async function searchCandidatesForCli({
|
|
|
137
138
|
} catch (error) {
|
|
138
139
|
// Not "No matches found": a script or a reader would take that for a name nothing lists.
|
|
139
140
|
if (isProviderBusy(error)) failRetryable(SEARCH_BUSY_MESSAGE, SEARCH_BUSY_CODE);
|
|
141
|
+
// Nor is no answer at all: the command fails as "couldn't reach", as any command offline does.
|
|
142
|
+
if (isNetworkFailure(error)) throw error;
|
|
140
143
|
logFailure("cli.commands-search", "Search ticker candidates failed")(error);
|
|
141
144
|
return [];
|
|
142
145
|
}
|
|
@@ -204,9 +207,19 @@ function formerIssuerNote(candidate: TickerSearchCandidate): string | null {
|
|
|
204
207
|
return formerIssuerLine(former.symbol, former, former.successor ?? { symbol: candidate.label, name: resolveSearchName(candidate) });
|
|
205
208
|
}
|
|
206
209
|
|
|
207
|
-
/**
|
|
210
|
+
/**
|
|
211
|
+
* A row search found by the name its company had before it was renamed
|
|
212
|
+
* ("cassava" finds Filana Therapeutics' FLNA): says what it was called.
|
|
213
|
+
*/
|
|
214
|
+
function formerNameNote(candidate: TickerSearchCandidate): string | null {
|
|
215
|
+
const formerName = candidate.result?.formerName;
|
|
216
|
+
if (typeof formerName !== "string" || !formerName.trim()) return null;
|
|
217
|
+
return `${candidate.label} is ${resolveSearchName(candidate)}, formerly ${formerName.trim()}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The merged companies' and renamed companies' lines, once each, unless the symbol's own line already says it. */
|
|
208
221
|
function formerIssuerNotes(candidates: readonly TickerSearchCandidate[], fate: SearchFate): string[] {
|
|
209
|
-
const notes = new Set(candidates.flatMap((candidate) => formerIssuerNote(candidate) ?? []));
|
|
222
|
+
const notes = new Set(candidates.flatMap((candidate) => formerIssuerNote(candidate) ?? formerNameNote(candidate) ?? []));
|
|
210
223
|
return [...notes].filter((note) => !fate.notes.some((line) => line.split(" ")[0] === note.split(" ")[0]));
|
|
211
224
|
}
|
|
212
225
|
|
|
@@ -366,6 +379,9 @@ function searchCandidateRows(candidates: TickerSearchCandidate[]) {
|
|
|
366
379
|
saved: candidate.kind === "ticker",
|
|
367
380
|
// The company that merged into this one, whose old name the query is.
|
|
368
381
|
...candidate.result?.formerIssuer ? { formerIssuer: candidate.result.formerIssuer } : {},
|
|
382
|
+
// The name a renamed company had, which the query is.
|
|
383
|
+
...typeof candidate.result?.formerName === "string" && candidate.result.formerName.trim()
|
|
384
|
+
? { formerName: candidate.result.formerName.trim() } : {},
|
|
369
385
|
}));
|
|
370
386
|
}
|
|
371
387
|
|