gloomberb 0.16.6 → 0.17.0
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 +2 -1
- package/src/api-client/chat-pane-link.ts +47 -0
- package/src/api-client/chat.ts +9 -1
- package/src/api-client/data.ts +37 -1
- package/src/api-client/equity-screener.ts +3 -0
- package/src/api-client/europe-energy.ts +91 -0
- package/src/api-client/index.ts +6 -0
- package/src/api-client/letters.ts +41 -0
- package/src/api-client/normalizers.ts +36 -6
- package/src/api-client/organization-tickers.ts +44 -0
- package/src/api-client/request.ts +3 -1
- package/src/api-client/types.ts +27 -1
- package/src/app/runtime/desktop-deeplink.ts +48 -15
- package/src/architecture/core-builtin-allowlist.json +1 -1
- package/src/architecture/dom-cell-metrics-allowlist.json +28 -0
- package/src/ask-gloom/pane-context.ts +23 -19
- package/src/ask-gloom/pane-request.ts +2 -2
- package/src/cli/commands/fred-series.ts +77 -11
- package/src/cli/commands/market.ts +77 -20
- package/src/cli/commands/overview.ts +29 -20
- package/src/cli/commands/ticker.ts +64 -11
- package/src/cli/desktop-pane-shot-view.ts +114 -0
- package/src/cli/desktop-pane-shot.ts +30 -31
- package/src/cli/entry.ts +7 -0
- package/src/cli/export-source.ts +43 -0
- package/src/cli/freshness.ts +18 -3
- package/src/cli/glossary.ts +17 -0
- package/src/cli/history-rows.ts +174 -13
- package/src/cli/known-gaps.ts +14 -3
- package/src/cli/listing-arg.ts +20 -2
- package/src/cli/listing-suggestion.ts +159 -0
- package/src/cli/native-smoke.ts +13 -0
- package/src/cli/not-a-ticker.ts +13 -1
- package/src/cli/pane-functions/capabilities.ts +16 -8
- package/src/cli/pane-functions/dom.ts +28 -13
- package/src/cli/pane-functions/function-help.ts +51 -6
- package/src/cli/pane-functions/headless.ts +26 -7
- package/src/cli/pane-functions/index.ts +46 -3
- package/src/cli/pane-functions/report-notices.ts +6 -3
- package/src/cli/pane-functions/report.ts +16 -2
- package/src/cli/pane-functions/resolver.ts +7 -4
- package/src/cli/report-tables.ts +31 -7
- package/src/cli/result.ts +54 -11
- package/src/cli/symbol-rows.ts +18 -1
- package/src/components/chart/composite/axis-overlays.tsx +2 -0
- package/src/components/chart/composite/composite-chart.tsx +4 -21
- package/src/components/chart/composite/legend.tsx +10 -6
- package/src/components/chart/composite/price-axis-labels.tsx +1 -0
- package/src/components/command-bar/list/model.ts +2 -0
- package/src/components/command-bar/pane-templates/items.ts +1 -1
- package/src/components/command-bar/routes/ticker-search/actions.ts +1 -0
- package/src/components/command-bar/routes/ticker-search/results.ts +3 -1
- package/src/components/command-bar/workflow/ops.ts +8 -97
- package/src/components/command-bar/workflow/pane-reuse.ts +177 -0
- package/src/components/data-table/view.tsx +1 -0
- package/src/components/feed-data-table/stack-view.tsx +18 -42
- package/src/components/layout/missing-pane.tsx +2 -0
- package/src/components/layout/shell/index.tsx +1 -0
- package/src/components/layout/shell/pane/ask.ts +12 -7
- package/src/components/layout/shell/pane/capture.ts +8 -34
- package/src/components/markdown-text.tsx +37 -6
- package/src/components/reader/article-reader.tsx +91 -266
- package/src/components/reader/detail-reader.tsx +210 -0
- package/src/components/reader/reader-text.tsx +239 -0
- package/src/components/table-view-shared.tsx +8 -4
- package/src/components/ticker/badge/list.tsx +15 -4
- package/src/components/ui/choice-dialog.tsx +3 -0
- package/src/components/ui/data-table/types.ts +7 -0
- package/src/components/ui/detail-scroll-body.tsx +31 -4
- package/src/components/ui/display.tsx +46 -1
- package/src/components/ui/icon.tsx +11 -1
- package/src/components/ui/list-view.tsx +3 -1
- package/src/components/ui/query-bar-keys.tsx +2 -0
- package/src/components/ui/query-bar.tsx +9 -1
- package/src/components/ui/select-button.tsx +3 -0
- package/src/data/config/store/fold-panes.ts +202 -11
- package/src/data/config/store/migrations.ts +13 -1
- package/src/docs/snapshot.json +29 -0
- package/src/i18n/es.ts +14 -0
- package/src/i18n/ja.ts +14 -0
- package/src/i18n/ko.ts +14 -0
- package/src/i18n/zh-cn.ts +14 -0
- package/src/i18n/zh-tw.ts +14 -0
- package/src/layout/desk-keys.ts +7 -0
- package/src/layout/desks.ts +2 -2
- package/src/market-data/market/status.ts +67 -6
- package/src/market-data/quotes/contributions.ts +15 -0
- package/src/market-data/quotes/regular-session-valuation.ts +131 -0
- package/src/market-data/quotes/resolution.ts +4 -0
- package/src/market-data/snapshot-provider.ts +6 -3
- package/src/notifications/app-badge.ts +30 -0
- package/src/plugins/builtin/account-management/calendar-tab.tsx +1 -1
- package/src/plugins/builtin/account-management/form-components.tsx +53 -1
- package/src/plugins/builtin/account-management/keyboard.ts +5 -0
- package/src/plugins/builtin/account-management/model.ts +27 -0
- package/src/plugins/builtin/account-management/pane.tsx +62 -2
- package/src/plugins/builtin/alerts/cli.ts +6 -4
- package/src/plugins/builtin/analytics/index.tsx +1 -1
- package/src/plugins/builtin/awards/pane.tsx +29 -18
- package/src/plugins/builtin/backtest/pane.tsx +2 -1
- package/src/plugins/builtin/bond-calculator/index.tsx +3 -5
- package/src/plugins/builtin/buildout/detail/company.tsx +7 -4
- package/src/plugins/builtin/builtin-plugin-meta.ts +5 -4
- package/src/plugins/builtin/catalysts/model.ts +92 -10
- package/src/plugins/builtin/catalysts/pane.tsx +6 -4
- package/src/plugins/builtin/cds/index.tsx +21 -30
- package/src/plugins/builtin/changelog/index.tsx +11 -12
- package/src/plugins/builtin/chart-composer/headless-schema.ts +2 -0
- package/src/plugins/builtin/chart-composer/headless.ts +87 -5
- package/src/plugins/builtin/chart-composer/pane.tsx +3 -2
- package/src/plugins/builtin/chart-composer/session-history.ts +20 -5
- package/src/plugins/builtin/chat/command-bar.ts +6 -8
- package/src/plugins/builtin/chat/content/composer-runtime.ts +2 -2
- package/src/plugins/builtin/chat/content/composer.tsx +13 -4
- package/src/plugins/builtin/chat/content/footer.tsx +15 -5
- package/src/plugins/builtin/chat/content/handle-prompt.tsx +326 -0
- package/src/plugins/builtin/chat/content/index.tsx +113 -9
- package/src/plugins/builtin/chat/content/layout-metrics.ts +4 -0
- package/src/plugins/builtin/chat/content/open-targets.ts +20 -4
- package/src/plugins/builtin/chat/content/scroll.ts +6 -1
- package/src/plugins/builtin/chat/content/transcript.tsx +11 -2
- package/src/plugins/builtin/chat/ghost-user.ts +1 -1
- package/src/plugins/builtin/chat/handle-prompt.ts +128 -0
- package/src/plugins/builtin/chat/index.tsx +44 -21
- package/src/plugins/builtin/chat/layout.ts +7 -14
- package/src/plugins/builtin/chat/message/desktop.tsx +59 -11
- package/src/plugins/builtin/chat/message/header.tsx +69 -21
- package/src/plugins/builtin/chat/message/pane-chip.tsx +223 -0
- package/src/plugins/builtin/chat/message/person-icons.tsx +53 -0
- package/src/plugins/builtin/chat/message/profile-popover.tsx +73 -9
- package/src/plugins/builtin/chat/message/render-state.ts +15 -2
- package/src/plugins/builtin/chat/message/terminal.tsx +33 -8
- package/src/plugins/builtin/chat/message/types.ts +3 -0
- package/src/plugins/builtin/chat/pane-links/model.ts +149 -0
- package/src/plugins/builtin/chat/pane-links/open.ts +29 -0
- package/src/plugins/builtin/chat/pane-links/previews.ts +119 -0
- package/src/plugins/builtin/chat/pane-state.ts +5 -8
- package/src/plugins/builtin/chat/people.ts +202 -0
- package/src/plugins/builtin/chat/profile-popover.ts +18 -2
- package/src/plugins/builtin/chat/status-widget.tsx +3 -8
- package/src/plugins/builtin/chat/unread-badge.ts +65 -0
- package/src/plugins/builtin/chat/unread-inbox-pane.tsx +9 -6
- package/src/plugins/builtin/clinical-trials/client.ts +26 -1
- package/src/plugins/builtin/clinical-trials/index.tsx +10 -2
- package/src/plugins/builtin/clinical-trials/model.ts +269 -0
- package/src/plugins/builtin/clinical-trials/pane.tsx +247 -62
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +156 -0
- package/src/plugins/builtin/cloud/askg/attached-pane.tsx +3 -3
- package/src/plugins/builtin/cloud/askg/pane-attachment.ts +3 -3
- package/src/plugins/builtin/cloud/mcp-connect/dialog.tsx +1 -0
- package/src/plugins/builtin/comment-letters/pane.tsx +30 -11
- package/src/plugins/builtin/commodities-energy/index.tsx +179 -0
- package/src/plugins/builtin/commodity-balances/client.ts +208 -0
- package/src/plugins/builtin/commodity-balances/headless.ts +66 -0
- package/src/plugins/builtin/commodity-balances/index.tsx +32 -0
- package/src/plugins/builtin/commodity-balances/model.ts +261 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +227 -0
- package/src/plugins/builtin/composite-plugins.ts +26 -1
- package/src/plugins/builtin/credit-boards/index.tsx +21 -42
- package/src/plugins/builtin/credit-boards/model.ts +0 -2
- package/src/plugins/builtin/credit-conditions/index.tsx +10 -19
- package/src/plugins/builtin/credit-documents/index.tsx +27 -10
- package/src/plugins/builtin/debt-maturities/index.tsx +3 -17
- package/src/plugins/builtin/debug/index.tsx +2 -2
- package/src/plugins/builtin/dividend-yield/client.ts +3 -1
- package/src/plugins/builtin/dividend-yield/headless.ts +3 -1
- package/src/plugins/builtin/dividend-yield/pane.tsx +3 -1
- package/src/plugins/builtin/dividend-yield/provider-client.ts +3 -1
- package/src/plugins/builtin/docs/blocks.tsx +4 -3
- package/src/plugins/builtin/docs/pane.tsx +16 -8
- package/src/plugins/builtin/earnings/index.tsx +2 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +2 -12
- package/src/plugins/builtin/equity-screener/client.ts +1 -1
- package/src/plugins/builtin/equity-screener/headless.ts +36 -2
- package/src/plugins/builtin/equity-screener/model.ts +121 -68
- package/src/plugins/builtin/executives/pane.tsx +3 -1
- package/src/plugins/builtin/filing-events/index.tsx +20 -0
- package/src/plugins/builtin/fixed-income/index.tsx +46 -0
- package/src/plugins/builtin/fixed-income/model.ts +27 -0
- package/src/plugins/builtin/fixed-income/pane.tsx +162 -0
- package/src/plugins/builtin/fixed-income/shortcut.ts +52 -0
- package/src/plugins/builtin/futures/index.tsx +24 -7
- package/src/plugins/builtin/futures/series.ts +23 -2
- package/src/plugins/builtin/futures/use-front-returns.ts +3 -2
- package/src/plugins/builtin/futures-curve/pane.tsx +6 -4
- package/src/plugins/builtin/help/shortcut-table.tsx +4 -2
- package/src/plugins/builtin/insider/detail.tsx +20 -12
- package/src/plugins/builtin/insider/index.tsx +2 -2
- package/src/plugins/builtin/iv-history/index.tsx +3 -0
- package/src/plugins/builtin/iv-history/screen-pane.tsx +13 -5
- package/src/plugins/builtin/jodi/client.ts +220 -0
- package/src/plugins/builtin/jodi/headless.ts +63 -0
- package/src/plugins/builtin/jodi/index.tsx +32 -0
- package/src/plugins/builtin/jodi/model.ts +75 -0
- package/src/plugins/builtin/jodi/pane.tsx +141 -0
- package/src/plugins/builtin/market-heatmap/evidence.ts +13 -4
- package/src/plugins/builtin/market-heatmap/headless.ts +3 -3
- package/src/plugins/builtin/market-heatmap/index.tsx +41 -23
- package/src/plugins/builtin/market-heatmap/live.ts +2 -1
- package/src/plugins/builtin/market-heatmap/portfolio.ts +67 -21
- package/src/plugins/builtin/market-movers/table.ts +2 -2
- package/src/plugins/builtin/market-valuation/align.ts +10 -9
- package/src/plugins/builtin/mna/distress/model.ts +52 -1
- package/src/plugins/builtin/mna/distress/pane.tsx +17 -12
- package/src/plugins/builtin/mna/index.tsx +2 -0
- package/src/plugins/builtin/news/wire/index.tsx +2 -2
- package/src/plugins/builtin/news/wire/industry-pane.tsx +28 -14
- package/src/plugins/builtin/news/wire/letters/client.ts +108 -0
- package/src/plugins/builtin/news/wire/letters/model.ts +69 -0
- package/src/plugins/builtin/news/wire/letters/pane.tsx +155 -0
- package/src/plugins/builtin/news/wire/mutes.ts +2 -1
- package/src/plugins/builtin/news/wire/news/query-presets.ts +4 -1
- package/src/plugins/builtin/openfda/client.ts +123 -15
- package/src/plugins/builtin/openfda/model.ts +163 -0
- package/src/plugins/builtin/openfda/pane.tsx +109 -65
- package/src/plugins/builtin/openfda/types.ts +25 -0
- package/src/plugins/builtin/options/view.tsx +1 -1
- package/src/plugins/builtin/pe-band/model.ts +3 -2
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +1 -1
- package/src/plugins/builtin/plugin-module.ts +3 -1
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +1 -1
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +19 -4
- package/src/plugins/builtin/portfolio-list/cli/render.ts +2 -2
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +39 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +18 -5
- package/src/plugins/builtin/portfolio-list/live-valuation.ts +25 -13
- package/src/plugins/builtin/power/evidence.ts +32 -3
- package/src/plugins/builtin/power/gas-client.ts +110 -0
- package/src/plugins/builtin/power/gas-model.ts +194 -0
- package/src/plugins/builtin/power/gas.tsx +145 -0
- package/src/plugins/builtin/power/headless.ts +4 -1
- package/src/plugins/builtin/power/index.tsx +19 -9
- package/src/plugins/builtin/power/pane.tsx +2 -3
- package/src/plugins/builtin/power/prices-headless.ts +75 -7
- package/src/plugins/builtin/power/prices.tsx +36 -13
- package/src/plugins/builtin/primary-dealers/client.ts +108 -0
- package/src/plugins/builtin/primary-dealers/headless.ts +53 -0
- package/src/plugins/builtin/primary-dealers/index.tsx +17 -0
- package/src/plugins/builtin/primary-dealers/model.ts +206 -0
- package/src/plugins/builtin/primary-dealers/pane.tsx +149 -0
- package/src/plugins/builtin/relative-rotation/index.tsx +3 -0
- package/src/plugins/builtin/research/analyst-model.ts +9 -5
- package/src/plugins/builtin/research/headless-schema.ts +2 -1
- package/src/plugins/builtin/research/relative-valuation-headless.ts +68 -32
- package/src/plugins/builtin/research/relative-valuation-model.ts +45 -24
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +39 -4
- package/src/plugins/builtin/scanner/flow-pane.tsx +8 -1
- package/src/plugins/builtin/sec/filing-display.tsx +3 -36
- package/src/plugins/builtin/sec/index.tsx +131 -43
- package/src/plugins/builtin/sec/model.ts +148 -2
- package/src/plugins/builtin/sec/row-summaries.ts +105 -0
- package/src/plugins/builtin/sectors/index.tsx +2 -0
- package/src/plugins/builtin/sectors/sector-model.ts +2 -2
- package/src/plugins/builtin/series-inventory.ts +1 -0
- package/src/plugins/builtin/shared/locked-rows.tsx +1 -1
- package/src/plugins/builtin/shared/use-quote-board.ts +179 -43
- package/src/plugins/builtin/shared/wall-teaser.tsx +2 -2
- package/src/plugins/builtin/shared/zip-text.ts +46 -0
- package/src/plugins/builtin/short-interest/watch-model.ts +11 -2
- package/src/plugins/builtin/themes/model.ts +2 -2
- package/src/plugins/builtin/ticker-detail/financials/aggregation.ts +11 -0
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +81 -19
- package/src/plugins/builtin/ticker-detail/financials/tab.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/headless.ts +2 -2
- package/src/plugins/builtin/ticker-detail/overview/model.ts +3 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +7 -5
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +8 -6
- package/src/plugins/builtin/time-sales/headless.ts +22 -0
- package/src/plugins/builtin/trace-bonds/client.ts +196 -0
- package/src/plugins/builtin/trace-bonds/headless.ts +69 -0
- package/src/plugins/builtin/trace-bonds/index.tsx +33 -0
- package/src/plugins/builtin/trace-bonds/model.ts +115 -0
- package/src/plugins/builtin/trace-bonds/pane.tsx +191 -0
- package/src/plugins/builtin/treasury-auctions/index.tsx +33 -47
- package/src/plugins/builtin/treasury-daily/client.ts +96 -0
- package/src/plugins/builtin/treasury-daily/headless.ts +69 -0
- package/src/plugins/builtin/treasury-daily/index.tsx +17 -0
- package/src/plugins/builtin/treasury-daily/model.ts +69 -0
- package/src/plugins/builtin/treasury-daily/pane.tsx +140 -0
- package/src/plugins/builtin/world-venue-map/entity-detail.tsx +15 -2
- package/src/plugins/builtin/world-venue-map/map.tsx +1 -0
- package/src/plugins/builtin-editorial.ts +1 -1
- package/src/plugins/catalog-browser.ts +25 -2
- package/src/plugins/marketplace/model.ts +57 -21
- package/src/renderers/dom/build-assets.ts +14 -5
- package/src/renderers/dom/data-table/index.tsx +5 -3
- package/src/renderers/dom/desktop/controls.tsx +2 -1
- package/src/renderers/dom/desktop/icons.tsx +25 -0
- package/src/renderers/dom/desktop/list-view.tsx +1 -1
- package/src/renderers/dom/desktop/menu.tsx +1 -1
- package/src/renderers/dom/desktop/query-bar.tsx +3 -1
- package/src/renderers/dom/host/chart-surface.tsx +7 -2
- package/src/renderers/dom/host/resize-rows.ts +111 -0
- package/src/renderers/dom/host/sticky-scroll.ts +22 -3
- package/src/renderers/dom/host/style.ts +14 -0
- package/src/renderers/dom/host/surface-3d.tsx +2 -1
- package/src/renderers/dom/host/surface-fit.ts +10 -6
- package/src/renderers/dom/host/text.tsx +2 -2
- package/src/renderers/dom/styles.css +63 -6
- package/src/renderers/electrobun/bun/desktop/app-badge.ts +37 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +1 -0
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/macos-dock-badge.ts +37 -0
- package/src/renderers/electrobun/bun/desktop/macos-dock-icon.ts +2 -40
- package/src/renderers/electrobun/bun/desktop/macos-objc.ts +41 -0
- package/src/renderers/electrobun/bun/tui-entry.ts +18 -1
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/app-badge-host.ts +17 -0
- package/src/renderers/electrobun/view/cli-pane-shot-entry.tsx +8 -3
- package/src/renderers/electrobun/view/main.tsx +2 -0
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +5 -1
- package/src/series/validate.ts +1 -1
- package/src/shares/api.ts +5 -1
- package/src/shares/location.ts +31 -0
- package/src/shares/open.ts +26 -0
- package/src/shares/pane-image.ts +18 -6
- package/src/shares/personal-pane.ts +42 -54
- package/src/sources/gloomberb-cloud/index.ts +7 -2
- package/src/sources/gloomberb-cloud/normalizers.ts +5 -0
- package/src/sources/history-coverage.ts +22 -4
- package/src/sources/history-result.ts +3 -1
- package/src/sources/provider-errors.ts +2 -1
- package/src/sources/provider-router/batches.ts +18 -2
- package/src/sources/provider-router/index.ts +2 -1
- package/src/state/app/context/index.tsx +3 -2
- package/src/test-support/builtin-panes.ts +31 -0
- package/src/tickers/listing-venues.ts +22 -0
- package/src/tickers/search/index.ts +7 -2
- package/src/tickers/search/ranking.ts +19 -0
- package/src/time-series/chart-data.ts +2 -0
- package/src/time-series/parsed-history-cache.ts +41 -11
- package/src/time-series/resolve.ts +47 -15
- package/src/types/config.ts +1 -1
- package/src/types/data-provider.ts +13 -1
- package/src/types/financials.ts +13 -0
- package/src/types/headless.ts +22 -0
- package/src/types/plugin.ts +33 -6
- package/src/types/price-history.ts +2 -0
- package/src/ui/host.tsx +6 -0
- package/src/utils/crypto-pair.ts +1 -1
- package/src/utils/currency-pair.ts +66 -0
- package/src/utils/decimal-comma.ts +57 -0
- package/src/utils/guards.ts +25 -0
- package/src/utils/local-utc-offset.ts +37 -0
- package/src/utils/plugin-proxy-hosts.json +8 -1
- package/src/components/chart/composite/crosshair-readout.tsx +0 -150
- package/src/plugins/builtin/chat/message/ai-badge.tsx +0 -18
- package/src/plugins/builtin/credit-boards/pane.tsx +0 -30
package/src/cli/history-rows.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { currencyUnitLabel } from "../utils/currency-units";
|
|
|
15
15
|
import { getPricePointTimestamp } from "../utils/price-history";
|
|
16
16
|
import { pricePointIntegrityFlags, pricePointValues } from "../utils/price-history-integrity";
|
|
17
17
|
import { formatUtcTime } from "../utils/utc-time";
|
|
18
|
+
import { historyCoverageNotice } from "../sources/history-coverage";
|
|
18
19
|
|
|
19
20
|
const PRICE_KEYS = ["open", "high", "low", "close"] as const;
|
|
20
21
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
@@ -24,6 +25,13 @@ const INTRADAY_RESOLUTIONS = ["1m", "5m", "15m", "30m", "45m", "1h"] as const;
|
|
|
24
25
|
/** A first bar later than this after the range's start is a late start, not a weekend or holiday. */
|
|
25
26
|
const LATE_START_MIN_MS = 10 * DAY_MS;
|
|
26
27
|
const LATE_START_MIN_BARS = 3;
|
|
28
|
+
/** A bar with more than this many calendar periods (`barPeriod`) since the one before follows a gap in the series. */
|
|
29
|
+
const BREAK_GAP_PERIODS = 3;
|
|
30
|
+
/** A close more than this many times the close before it, or less than its hundredth, follows a jump. */
|
|
31
|
+
const BREAK_JUMP_RATIO = 100;
|
|
32
|
+
/** Flag codes for a break before a bar, whose prices stay as served; every other code blanks them. */
|
|
33
|
+
const BREAK_FLAGS: ReadonlySet<string> = new Set(["gap", "jump"]);
|
|
34
|
+
const FIRST_MONDAY_MS = Date.UTC(1970, 0, 5);
|
|
27
35
|
|
|
28
36
|
const BAR_INTERVAL_LABELS: Record<ManualChartResolution, string> = {
|
|
29
37
|
"1m": "1-minute",
|
|
@@ -50,7 +58,10 @@ export interface HistoryRow {
|
|
|
50
58
|
unit: string | null;
|
|
51
59
|
/** Bar size, in the codes of `resolution`: 5m, 1h, 1d, 1wk, 1mo. */
|
|
52
60
|
interval: ManualChartResolution | null;
|
|
53
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Each way the bar's prices contradict each other, such as `high<open`, and
|
|
63
|
+
* `gap` or `jump` for a break in the series just before it; null when none.
|
|
64
|
+
*/
|
|
54
65
|
flag: string | null;
|
|
55
66
|
/** The bar's period had not ended when it was served: the close is the latest price. */
|
|
56
67
|
partial?: true;
|
|
@@ -122,10 +133,59 @@ function historyInterval(times: readonly number[], resolution: ManualChartResolu
|
|
|
122
133
|
return days >= 25 ? "1mo" : days >= 6 ? "1wk" : "1d";
|
|
123
134
|
}
|
|
124
135
|
|
|
136
|
+
/**
|
|
137
|
+
* The calendar period a bar falls in, numbered so that consecutive periods
|
|
138
|
+
* differ by one: its month for monthly bars, its week (Monday to Sunday, UTC)
|
|
139
|
+
* for weekly and anything finer. Daily and intraday bars are counted in weeks
|
|
140
|
+
* because a market's own closures leave holes that are no break in the
|
|
141
|
+
* series: nights, weekends, holidays (Lunar New Year and Golden Week close
|
|
142
|
+
* Chinese markets for up to nine days, Eid the Gulf markets about as long),
|
|
143
|
+
* and a thin listing that goes a week or two without a trade. A daily series
|
|
144
|
+
* then breaks exactly where its weekly bars would.
|
|
145
|
+
*/
|
|
146
|
+
function barPeriod(time: number, interval: ManualChartResolution): number {
|
|
147
|
+
if (interval !== "1mo") return Math.floor((time - FIRST_MONDAY_MS) / (7 * DAY_MS));
|
|
148
|
+
const date = new Date(time);
|
|
149
|
+
return date.getUTCFullYear() * 12 + date.getUTCMonth();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The breaks before each bar: `gap` after more than three calendar periods
|
|
154
|
+
* with no bar (`barPeriod`), `jump` when its close is over 100 times the last
|
|
155
|
+
* close before it, or under a hundredth of it. Closes that are blank, zero or
|
|
156
|
+
* negative are skipped.
|
|
157
|
+
*/
|
|
158
|
+
function historyBreakFlags(
|
|
159
|
+
times: readonly number[],
|
|
160
|
+
closes: readonly (number | null)[],
|
|
161
|
+
interval: ManualChartResolution | null,
|
|
162
|
+
): string[][] {
|
|
163
|
+
let previousTime: number | null = null;
|
|
164
|
+
let previousClose: number | null = null;
|
|
165
|
+
return times.map((time, index) => {
|
|
166
|
+
const flags: string[] = [];
|
|
167
|
+
if (interval && Number.isFinite(time)) {
|
|
168
|
+
if (previousTime != null && barPeriod(time, interval) - barPeriod(previousTime, interval) > BREAK_GAP_PERIODS) flags.push("gap");
|
|
169
|
+
previousTime = time;
|
|
170
|
+
}
|
|
171
|
+
const close = closes[index];
|
|
172
|
+
if (close != null && close > 0) {
|
|
173
|
+
if (previousClose != null && Math.max(close / previousClose, previousClose / close) > BREAK_JUMP_RATIO) flags.push("jump");
|
|
174
|
+
previousClose = close;
|
|
175
|
+
}
|
|
176
|
+
return flags;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function flagCodes(row: Pick<HistoryRow, "flag">): string[] {
|
|
181
|
+
return row.flag ? row.flag.split(",") : [];
|
|
182
|
+
}
|
|
183
|
+
|
|
125
184
|
/**
|
|
126
185
|
* Intraday bars keep their UTC time; daily and longer bars are trading dates.
|
|
127
186
|
* A bar whose prices contradict each other is blanked exactly as HP blanks it,
|
|
128
|
-
* and says why in `flag`.
|
|
187
|
+
* and says why in `flag`. A bar after a gap or a jump in the series keeps its
|
|
188
|
+
* prices and is flagged `gap` or `jump`.
|
|
129
189
|
*/
|
|
130
190
|
export function historyRows(
|
|
131
191
|
points: readonly PricePoint[],
|
|
@@ -137,7 +197,7 @@ export function historyRows(
|
|
|
137
197
|
const times = points.map(getPricePointTimestamp);
|
|
138
198
|
const intraday = resolution ? isIntradayResolution(resolution) : looksIntraday(times);
|
|
139
199
|
const interval = historyInterval(times, resolution);
|
|
140
|
-
|
|
200
|
+
const rows = points.map((point, index) => {
|
|
141
201
|
const time = times[index]!;
|
|
142
202
|
const iso = Number.isFinite(time) ? new Date(time).toISOString() : "";
|
|
143
203
|
const values = pricePointValues(point);
|
|
@@ -163,6 +223,11 @@ export function historyRows(
|
|
|
163
223
|
: {}),
|
|
164
224
|
};
|
|
165
225
|
});
|
|
226
|
+
const breaks = historyBreakFlags(times, rows.map((row) => row.close), interval);
|
|
227
|
+
return rows.map((row, index) => {
|
|
228
|
+
const flags = [...flagCodes(row), ...breaks[index]!];
|
|
229
|
+
return flags.length > 0 ? { ...row, flag: flags.join(",") } : row;
|
|
230
|
+
});
|
|
166
231
|
}
|
|
167
232
|
|
|
168
233
|
/**
|
|
@@ -292,16 +357,28 @@ function historyDate(value: string): string {
|
|
|
292
357
|
return value.length > 10 ? formatUtcTime(value) : value;
|
|
293
358
|
}
|
|
294
359
|
|
|
295
|
-
/** The
|
|
296
|
-
export function
|
|
360
|
+
/** The first bar after the series' last gap or jump, which reads on without a break; null when it has none. */
|
|
361
|
+
export function historyUnbrokenFrom(rows: readonly Pick<HistoryRow, "date" | "flag">[]): string | null {
|
|
362
|
+
const last = rows.findLastIndex((row) => flagCodes(row).some((code) => BREAK_FLAGS.has(code)));
|
|
363
|
+
return last >= 0 ? rows[last]!.date : null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* The facts line under a history heading: "Currency USD", "daily bars",
|
|
368
|
+
* "2025-10-09 to 2026-10-08", "251 bars", and "unbroken from 1992-12-01"
|
|
369
|
+
* after a gap or jump.
|
|
370
|
+
*/
|
|
371
|
+
export function historyFacts(unit: HistoryUnit, rows: readonly Pick<HistoryRow, "date" | "interval" | "flag">[]): string[] {
|
|
297
372
|
const first = rows[0]?.date;
|
|
298
373
|
const last = rows.at(-1)?.date;
|
|
299
374
|
const interval = rows[0]?.interval;
|
|
375
|
+
const unbroken = historyUnbrokenFrom(rows);
|
|
300
376
|
return [
|
|
301
377
|
unit.kind === "pair" ? unit.unit! : unit.kind === "points" ? "Index points" : `Currency ${unit.unit ?? "unknown"}`,
|
|
302
378
|
interval ? `${BAR_INTERVAL_LABELS[interval]} bars` : "",
|
|
303
379
|
first && last ? (first === last ? historyDate(first) : `${historyDate(first)} to ${historyDate(last)}`) : "",
|
|
304
380
|
`${rows.length} ${rows.length === 1 ? "bar" : "bars"}`,
|
|
381
|
+
unbroken ? `unbroken from ${historyDate(unbroken)}` : "",
|
|
305
382
|
].filter(Boolean);
|
|
306
383
|
}
|
|
307
384
|
|
|
@@ -333,22 +410,102 @@ export function historyNotes(
|
|
|
333
410
|
return notes;
|
|
334
411
|
}
|
|
335
412
|
|
|
413
|
+
/**
|
|
414
|
+
* "Price history starts 1992-12-01; old-shekel quotes before 1992 are not
|
|
415
|
+
* served.": the source serves nothing before a date the range reaches back
|
|
416
|
+
* past, and why when it says. Null for a range that starts after it.
|
|
417
|
+
*/
|
|
418
|
+
export function historyCoverageWarning(
|
|
419
|
+
range: TimeRange,
|
|
420
|
+
coverage: { coverageStart?: string; coverageNote?: string },
|
|
421
|
+
now = Date.now(),
|
|
422
|
+
): string | null {
|
|
423
|
+
return historyCoverageNotice(coverage.coverageStart, subtractTimeRange(new Date(now), range).getTime(), coverage.coverageNote);
|
|
424
|
+
}
|
|
425
|
+
|
|
336
426
|
/** Gaps listed by date before the rest are only counted. */
|
|
337
427
|
const LISTED_GAPS = 3;
|
|
338
428
|
|
|
429
|
+
function listItems(items: readonly string[]): string {
|
|
430
|
+
return items.length > 1 ? `${items.slice(0, -1).join(", ")} and ${items.at(-1)}` : items[0] ?? "";
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function utcDay(time: number): string {
|
|
434
|
+
return new Date(time).toISOString().slice(0, 10);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** The bars flagged `code`, each with the bar before it. */
|
|
438
|
+
function breaksFlagged<Row extends Pick<HistoryRow, "flag">>(rows: readonly Row[], code: string): Array<[Row, Row]> {
|
|
439
|
+
return rows.flatMap((row, index) => index > 0 && flagCodes(row).includes(code) ? [[rows[index - 1]!, row] as [Row, Row]] : []);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* "No bars for 1980-08 to 1992-11 (148 months)": the periods with no bar
|
|
444
|
+
* before each bar flagged `gap`, the latest few by date and the rest counted.
|
|
445
|
+
*/
|
|
446
|
+
function historyBreakGapNote(rows: readonly Pick<HistoryRow, "date" | "interval" | "flag">[]): string | null {
|
|
447
|
+
const interval = rows[0]?.interval;
|
|
448
|
+
const gaps = interval ? breaksFlagged(rows, "gap") : [];
|
|
449
|
+
if (!interval || gaps.length === 0) return null;
|
|
450
|
+
const monthly = interval === "1mo";
|
|
451
|
+
const spans = gaps.map(([before, after]) => {
|
|
452
|
+
const [from, to] = [Date.parse(before.date), Date.parse(after.date)];
|
|
453
|
+
const periods = barPeriod(to, interval) - barPeriod(from, interval) - 1;
|
|
454
|
+
const length = `${periods} ${monthly ? "month" : "week"}${periods === 1 ? "" : "s"}`;
|
|
455
|
+
if (monthly) {
|
|
456
|
+
const month = (time: number, step: number) => {
|
|
457
|
+
const date = new Date(time);
|
|
458
|
+
return utcDay(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + step, 1)).slice(0, 7);
|
|
459
|
+
};
|
|
460
|
+
return `${month(from, 1)} to ${month(to, -1)} (${length})`;
|
|
461
|
+
}
|
|
462
|
+
return `${utcDay(from + (interval === "1wk" ? 7 : 1) * DAY_MS)} to ${utcDay(to - DAY_MS)} (${length})`;
|
|
463
|
+
});
|
|
464
|
+
const shown = spans.slice(-LISTED_GAPS);
|
|
465
|
+
const earlier = spans.length - shown.length;
|
|
466
|
+
return `No bars for ${listItems([...earlier > 0 ? [`${earlier} earlier ${earlier === 1 ? "gap" : "gaps"}`] : [], ...shown])}`;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const closeFormat = new Intl.NumberFormat("en-US", { maximumSignificantDigits: 7, useGrouping: false });
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* "Close rises 564x from 0.0049 on 1980-07-01 to 2.763 on 1992-12-01": each
|
|
473
|
+
* bar flagged `jump`, against the last close before it. Neutral on the cause,
|
|
474
|
+
* which can be a change of unit, a different series or a real collapse.
|
|
475
|
+
*/
|
|
476
|
+
export function historyJumpNote(rows: readonly Pick<HistoryRow, "date" | "close" | "flag">[]): string | null {
|
|
477
|
+
const jumps: string[] = [];
|
|
478
|
+
let previous: { date: string; close: number } | null = null;
|
|
479
|
+
for (const row of rows) {
|
|
480
|
+
if (row.close == null || !(row.close > 0)) continue;
|
|
481
|
+
if (previous && flagCodes(row).includes("jump")) {
|
|
482
|
+
const rises = row.close > previous.close;
|
|
483
|
+
const ratio = Math.round(rises ? row.close / previous.close : previous.close / row.close).toLocaleString("en-US");
|
|
484
|
+
jumps.push(`${rises ? "rises" : "falls"} ${ratio}x from ${closeFormat.format(previous.close)} on ${historyDate(previous.date)}`
|
|
485
|
+
+ ` to ${closeFormat.format(row.close)} on ${historyDate(row.date)}`);
|
|
486
|
+
}
|
|
487
|
+
previous = { date: row.date, close: row.close };
|
|
488
|
+
}
|
|
489
|
+
if (jumps.length === 0) return null;
|
|
490
|
+
const shown = jumps.slice(-LISTED_GAPS);
|
|
491
|
+
const earlier = jumps.length - shown.length;
|
|
492
|
+
return `Close ${earlier > 0 ? `jumps over 100x at ${earlier} earlier ${earlier === 1 ? "bar" : "bars"}, then ` : ""}${listItems(shown)}`;
|
|
493
|
+
}
|
|
494
|
+
|
|
339
495
|
/**
|
|
340
496
|
* "No bar for 2026-10-10": the days a coin, which trades every day, has no
|
|
341
497
|
* daily bar for between its first and last bar. Days before the first bar are
|
|
342
498
|
* before the listing. A market that closes skips weekends and holidays, so its
|
|
343
|
-
* gaps say nothing and
|
|
499
|
+
* gaps say nothing on their own: for it, and for a coin's other bar sizes, the
|
|
500
|
+
* note names the gaps flagged `gap`, "No bars for 1980-08 to 1992-11 (148 months)".
|
|
344
501
|
*/
|
|
345
502
|
export function historyGapNote(
|
|
346
|
-
rows: readonly Pick<HistoryRow, "date" | "interval">[],
|
|
503
|
+
rows: readonly Pick<HistoryRow, "date" | "interval" | "flag">[],
|
|
347
504
|
market: { listing: string; exchange?: string; instrumentType?: string },
|
|
348
505
|
): string | null {
|
|
349
506
|
const everyDay = isRoundTheClockCoin(market.listing, market.exchange)
|
|
350
507
|
|| classifyInstrumentType(market.instrumentType) === "crypto";
|
|
351
|
-
if (!everyDay || rows[0]?.interval !== "1d") return
|
|
508
|
+
if (!everyDay || rows[0]?.interval !== "1d") return historyBreakGapNote(rows);
|
|
352
509
|
const days = new Set(rows.map((row) => Date.parse(`${row.date.slice(0, 10)}T00:00:00Z`)).filter(Number.isFinite));
|
|
353
510
|
const sorted = [...days].sort((left, right) => left - right);
|
|
354
511
|
const gaps: Array<[number, number]> = [];
|
|
@@ -366,15 +523,19 @@ export function historyGapNote(
|
|
|
366
523
|
...earlier > 0 ? [`${earlier} earlier ${earlier === 1 ? "day" : "days"}`] : [],
|
|
367
524
|
...shown.map(([from, to]) => from === to ? date(from) : `${date(from)} to ${date(to)}`),
|
|
368
525
|
];
|
|
369
|
-
|
|
370
|
-
return `${length(gaps) === 1 ? "No bar" : "No bars"} for ${list}`;
|
|
526
|
+
return `${length(gaps) === 1 ? "No bar" : "No bars"} for ${listItems(listed)}`;
|
|
371
527
|
}
|
|
372
528
|
|
|
373
|
-
/**
|
|
529
|
+
/**
|
|
530
|
+
* "37 of 251 bars have high below open or close in the source data; ...", or
|
|
531
|
+
* null when every bar agrees. Bars flagged only for a gap or jump before them
|
|
532
|
+
* keep their prices and are not counted.
|
|
533
|
+
*/
|
|
374
534
|
export function historyFlagNote(rows: readonly Pick<HistoryRow, "flag">[]): string | null {
|
|
375
|
-
const
|
|
535
|
+
const contradictions = (row: Pick<HistoryRow, "flag">) => flagCodes(row).filter((code) => !BREAK_FLAGS.has(code));
|
|
536
|
+
const flagged = rows.filter((row) => contradictions(row).length > 0);
|
|
376
537
|
if (flagged.length === 0) return null;
|
|
377
|
-
const codes = new Set(flagged.flatMap(
|
|
538
|
+
const codes = new Set(flagged.flatMap(contradictions));
|
|
378
539
|
const kinds = [
|
|
379
540
|
codes.has("high<open") || codes.has("high<close") ? "high below open or close" : "",
|
|
380
541
|
codes.has("low>open") || codes.has("low>close") ? "low above open or close" : "",
|
package/src/cli/known-gaps.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Where data finer than a minute is: nowhere as bars. GIP draws 1-minute
|
|
3
|
-
* bars and QR lists each trade
|
|
3
|
+
* bars, and QR lists each trade of a US listing. `history --interval`, GIP's
|
|
4
|
+
* `--chartResolution 1s` and QR on a listing abroad all say this.
|
|
4
5
|
*/
|
|
5
6
|
export function subMinuteDataMessage(symbol = "<ticker>"): string {
|
|
6
|
-
return `No second or tick bars;
|
|
7
|
-
|
|
7
|
+
return `No second or tick bars; 1-minute bars: fn GIP ${symbol}; trades exist for US listings only.`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** A bar size finer than a minute, as people type one: 1s, 30s, 5sec, 500ms, tick, ticks, tick-by-tick, seconds. */
|
|
11
|
+
export function isSubMinuteInterval(value: string): boolean {
|
|
12
|
+
return /^(\d+(\.\d+)?\s*(ms|msecs?|millis(econds?)?|s|secs?|seconds?)|\d*\s*ticks?|tick[\s-]*by[\s-]*tick|seconds?|sub[\s-]*(minute|second)s?)$/i
|
|
13
|
+
.test(value.trim());
|
|
8
14
|
}
|
|
9
15
|
|
|
10
16
|
/**
|
|
@@ -22,6 +28,11 @@ const KNOWN_GAPS: ReadonlyArray<{ terms: readonly string[]; message: string }> =
|
|
|
22
28
|
"carbon price", "carbon prices", "carbon credits", "emission allowances", "emissions allowances"],
|
|
23
29
|
message: "EU carbon (EUA): the auction price is in EPWR, chart CAP:carbon:eua. Traded proxies: CO2:LSE, KRBN.",
|
|
24
30
|
},
|
|
31
|
+
{
|
|
32
|
+
terms: ["EU gas storage", "European gas storage", "Europe gas storage", "EU gas inventories", "European gas inventories",
|
|
33
|
+
"AGSI", "AGSI+", "GIE AGSI", "gas storage Europe"],
|
|
34
|
+
message: "European gas storage: EPWR GAS, or EPWR GAS DE for one country; chart CAP:gas-storage:eu. US storage is NGS.",
|
|
35
|
+
},
|
|
25
36
|
{
|
|
26
37
|
terms: ["tick", "ticks", "tick data", "tick bars", "tick by tick", "second", "seconds", "second bars", "1s", "1 second",
|
|
27
38
|
"1 second bars", "sub-minute", "sub minute bars", "subsecond"],
|
package/src/cli/listing-arg.ts
CHANGED
|
@@ -3,7 +3,15 @@ import type { Quote } from "../types/financials";
|
|
|
3
3
|
import type { CliCommandContext } from "../types/plugin";
|
|
4
4
|
import type { TickerRecord } from "../types/ticker";
|
|
5
5
|
import { perpetualListingOfMarket, typedPerpetualListing } from "../tickers/perpetual-listing";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
listingVenues,
|
|
8
|
+
quotedOnVenue,
|
|
9
|
+
readAmbiguousCurrencyPair,
|
|
10
|
+
readSharedCode,
|
|
11
|
+
type ListingDeps,
|
|
12
|
+
type ListingStore,
|
|
13
|
+
type ListingVenue,
|
|
14
|
+
} from "../tickers/listing-venues";
|
|
7
15
|
import { findUsListingOfCompany, type UsListing } from "../tickers/us-listing";
|
|
8
16
|
import {
|
|
9
17
|
describeTickerChange,
|
|
@@ -33,6 +41,7 @@ import {
|
|
|
33
41
|
import { withMarketData } from "./scoped-context";
|
|
34
42
|
import { isNoProviderMessage } from "../sources/provider-errors";
|
|
35
43
|
import { dashedCoinPair } from "../utils/crypto-pair";
|
|
44
|
+
import { currencyPairSpelling } from "../utils/currency-pair";
|
|
36
45
|
import { isQuoteStaleForCurrentSession } from "../market-data/quotes/freshness";
|
|
37
46
|
import { formatMarketPriceWithCurrency, quoteFormatOptions } from "../market-data/market/format";
|
|
38
47
|
import { zonedDateKey } from "../utils/zoned-date-time";
|
|
@@ -75,7 +84,11 @@ export interface ListingArg {
|
|
|
75
84
|
* the command takes several symbols and `--exchange` only fills in the ones
|
|
76
85
|
* without an exchange of their own. A code that names several venues (TSE)
|
|
77
86
|
* agrees with any of them. A futures board code no listed security uses is
|
|
78
|
-
* its future: TTF is TTF=F, while NG stays NovaGold.
|
|
87
|
+
* its future: TTF is TTF=F, while NG stays NovaGold. A currency pair is the
|
|
88
|
+
* one listing the service has for it, however it is written: EUR/USD,
|
|
89
|
+
* EURUSD:CCY and JPY=X are EURUSD=X and USDJPY=X, so every spelling asks for
|
|
90
|
+
* the same quote. USD-NGN and USDNGN could be a security's symbol, so
|
|
91
|
+
* `resolveCliListing` asks search before reading them as USDNGN=X.
|
|
79
92
|
*/
|
|
80
93
|
export function parseListingArg(
|
|
81
94
|
raw: string,
|
|
@@ -106,6 +119,8 @@ export function parseListingArg(
|
|
|
106
119
|
// TSE and --exchange JPX agree on Tokyo.
|
|
107
120
|
if (agreed.length === 1) [exchange, code] = [agreed[0]!, agreed[0]!];
|
|
108
121
|
}
|
|
122
|
+
const pair = currencyPairSpelling(parsed.symbol, exchange);
|
|
123
|
+
if (pair && !pair.ambiguous) return { symbol: pair.symbol, exchange: "", key: pair.symbol };
|
|
109
124
|
const shared = isSharedExchangeCode(code) ? { sharedCode: code } : {};
|
|
110
125
|
if (exchange) return { symbol: parsed.symbol, exchange, key: `${parsed.symbol}:${exchange}`, ...shared };
|
|
111
126
|
// A futures board code no security uses is its future: TTF is TTF=F.
|
|
@@ -855,6 +870,9 @@ export async function resolveCliListing(
|
|
|
855
870
|
): Promise<CliListing> {
|
|
856
871
|
let parsed = parseListingArg(raw, exchangeOption, options);
|
|
857
872
|
if (!parsed.symbol) throw new ListingArgError("Ticker symbol is required.");
|
|
873
|
+
// USD-NGN and USDNGN are the pair USDNGN=X unless a security is listed under those letters.
|
|
874
|
+
const pair = parsed.exchange ? null : await readAmbiguousCurrencyPair(parsed.symbol, deps);
|
|
875
|
+
if (pair) parsed = { symbol: pair, exchange: "", key: pair };
|
|
858
876
|
try {
|
|
859
877
|
await checkListingExchange(parsed, deps);
|
|
860
878
|
} catch (error) {
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { DataProvider } from "../types/data-provider";
|
|
2
|
+
import type { TickerRecord } from "../types/ticker";
|
|
3
|
+
import { LISTING_SEARCH_LIMIT, searchTickerCandidates, type TickerSearchCandidate } from "../tickers/search";
|
|
4
|
+
import { mapWithConcurrency } from "../utils/concurrency";
|
|
5
|
+
import { TickerNotFoundError } from "./ticker-resolution";
|
|
6
|
+
|
|
7
|
+
/** How long a suggestion waits for search; a slow search leaves the plain "not found" as it was. */
|
|
8
|
+
const SUGGESTION_TIMEOUT_MS = 6_000;
|
|
9
|
+
/** Searches in flight at once while a list's missing symbols are looked up. */
|
|
10
|
+
const SUGGESTION_CONCURRENCY = 4;
|
|
11
|
+
/** A list with more missing symbols than this is an import with stale codes, not a typo; it gets no suggestions and no extra searches. */
|
|
12
|
+
export const MAX_SUGGESTED_SYMBOLS = 10;
|
|
13
|
+
/** Symbols shorter than this are too short for one edit to leave them recognisable. */
|
|
14
|
+
const MIN_TYPO_LENGTH = 4;
|
|
15
|
+
|
|
16
|
+
export interface ListingSuggestion {
|
|
17
|
+
/** The listing as `search` prints it: USDNGN=X. */
|
|
18
|
+
symbol: string;
|
|
19
|
+
name: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface SuggestionDeps {
|
|
23
|
+
store: { loadAllTickers(): Promise<TickerRecord[]> };
|
|
24
|
+
dataProvider: DataProvider;
|
|
25
|
+
/** Replaces the search, for a caller that already has the candidates. */
|
|
26
|
+
search?: (query: string) => Promise<TickerSearchCandidate[]>;
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The text of a symbol or a name with its case, spaces, punctuation and a leading `$` gone: `USD-NGN` and `USD/NGN` are `USDNGN`. */
|
|
31
|
+
function compact(text: string): string {
|
|
32
|
+
return text.toUpperCase().replace(/[^\p{L}\p{N}]/gu, "");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A symbol as people type it: without the `=X` or `=F` a pair or a future carries, and without its venue (`BTCUSDT:BYBIT`). */
|
|
36
|
+
function typedForm(symbol: string): string {
|
|
37
|
+
return compact(symbol.split(":")[0]!.replace(/=[A-Z]?$/i, ""));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The edits (insert, delete, replace, swap two neighbours) between two strings, up to two; anything further apart is 2. */
|
|
41
|
+
function editDistance(left: string, right: string): number {
|
|
42
|
+
if (left === right) return 0;
|
|
43
|
+
if (Math.abs(left.length - right.length) > 1) return 2;
|
|
44
|
+
const rows: number[][] = Array.from({ length: left.length + 1 }, (_, row) => [row, ...new Array<number>(right.length).fill(0)]);
|
|
45
|
+
for (let column = 1; column <= right.length; column += 1) rows[0]![column] = column;
|
|
46
|
+
for (let row = 1; row <= left.length; row += 1) {
|
|
47
|
+
for (let column = 1; column <= right.length; column += 1) {
|
|
48
|
+
const same = left[row - 1] === right[column - 1] ? 0 : 1;
|
|
49
|
+
let best = Math.min(rows[row - 1]![column]! + 1, rows[row]![column - 1]! + 1, rows[row - 1]![column - 1]! + same);
|
|
50
|
+
if (row > 1 && column > 1 && left[row - 1] === right[column - 2] && left[row - 2] === right[column - 1]) {
|
|
51
|
+
best = Math.min(best, rows[row - 2]![column - 2]! + 1);
|
|
52
|
+
}
|
|
53
|
+
rows[row]![column] = best;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return rows[left.length]![right.length]!;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function candidateName(candidate: TickerSearchCandidate): string {
|
|
60
|
+
return candidate.detail.split(" | ")[0]?.trim() ?? "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 0 when the candidate is the typed symbol with other punctuation (`USD_NGN` for `USDNGN=X`) or named so, 1 for one slip of the keys, null for anything else. */
|
|
64
|
+
function nearness(typed: string, candidate: TickerSearchCandidate): number | null {
|
|
65
|
+
const wanted = compact(typed);
|
|
66
|
+
if (!wanted) return null;
|
|
67
|
+
const symbol = typedForm(candidate.label || candidate.symbol);
|
|
68
|
+
if (wanted === symbol || wanted === compact(candidateName(candidate))) return 0;
|
|
69
|
+
return wanted.length >= MIN_TYPO_LENGTH && symbol.length >= MIN_TYPO_LENGTH && editDistance(wanted, symbol) === 1 ? 1 : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The search hit a symbol that was not found most likely meant, or null.
|
|
74
|
+
* Only a real near match counts: the same letters and digits with other
|
|
75
|
+
* punctuation, or one slip in a symbol of four characters or more. The first
|
|
76
|
+
* result is never taken for being first, and a tie between different symbols
|
|
77
|
+
* is no suggestion at all.
|
|
78
|
+
*/
|
|
79
|
+
export function nearestListing(typed: string, candidates: readonly TickerSearchCandidate[]): ListingSuggestion | null {
|
|
80
|
+
const typedKey = typed.trim().replace(/^\$/, "").toUpperCase();
|
|
81
|
+
const near = candidates
|
|
82
|
+
.filter((candidate) => (candidate.label || candidate.symbol).toUpperCase() !== typedKey)
|
|
83
|
+
.flatMap((candidate) => {
|
|
84
|
+
const tier = nearness(typed, candidate);
|
|
85
|
+
return tier == null ? [] : [{ candidate, tier }];
|
|
86
|
+
});
|
|
87
|
+
if (near.length === 0) return null;
|
|
88
|
+
const best = Math.min(...near.map((entry) => entry.tier));
|
|
89
|
+
const top = near.filter((entry) => entry.tier === best).map((entry) => entry.candidate);
|
|
90
|
+
if (new Set(top.map((candidate) => typedForm(candidate.label || candidate.symbol))).size > 1) return null;
|
|
91
|
+
const first = top[0]!;
|
|
92
|
+
return { symbol: first.label || first.symbol, name: candidateName(first) };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The suggestion as a line says it: `USDNGN=X (US Dollar/Nigerian Naira)`. */
|
|
96
|
+
export function describeSuggestion({ symbol, name }: ListingSuggestion): string {
|
|
97
|
+
return name && name.toUpperCase() !== symbol.toUpperCase() ? `${symbol} (${name})` : symbol;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The search the `search` command runs for a query: saved tickers and the data service's matches, as many as it lists. */
|
|
101
|
+
async function searchAsCommand(query: string, { store, dataProvider }: SuggestionDeps): Promise<TickerSearchCandidate[]> {
|
|
102
|
+
const tickers = new Map((await store.loadAllTickers()).map((ticker) => [ticker.metadata.ticker.toUpperCase(), ticker] as const));
|
|
103
|
+
return searchTickerCandidates({
|
|
104
|
+
query,
|
|
105
|
+
tickers,
|
|
106
|
+
dataProvider,
|
|
107
|
+
localLimit: 6,
|
|
108
|
+
totalLimit: LISTING_SEARCH_LIMIT,
|
|
109
|
+
searchContext: { limit: LISTING_SEARCH_LIMIT },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The listing search finds for a symbol that resolved to nothing, when it is a
|
|
115
|
+
* near match; null when search finds nothing near, fails or is slow. It never
|
|
116
|
+
* throws and never waits longer than the timeout.
|
|
117
|
+
*/
|
|
118
|
+
export async function suggestListing(input: string, deps: SuggestionDeps): Promise<ListingSuggestion | null> {
|
|
119
|
+
const query = input.trim().replace(/^\$/, "");
|
|
120
|
+
if (!query) return null;
|
|
121
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
122
|
+
try {
|
|
123
|
+
const candidates = await Promise.race([
|
|
124
|
+
Promise.resolve().then(() => (deps.search ?? ((text: string) => searchAsCommand(text, deps)))(query)),
|
|
125
|
+
new Promise<null>((resolve) => { timer = setTimeout(() => resolve(null), deps.timeoutMs ?? SUGGESTION_TIMEOUT_MS); }),
|
|
126
|
+
]);
|
|
127
|
+
return candidates ? nearestListing(query, candidates) : null;
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
} finally {
|
|
131
|
+
if (timer) clearTimeout(timer);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The answer for a symbol search lists nothing for, with nothing else said (a company that left its symbol, a wrong exchange, a stopped listing each say more). */
|
|
136
|
+
const PLAIN_NO_MATCH = /^No ticker match found for ".*"\.$/s;
|
|
137
|
+
|
|
138
|
+
/** Whether the message is the plain no-match answer, the one a suggestion can sit beside. */
|
|
139
|
+
export function isPlainNoMatch(message: string | null | undefined): boolean {
|
|
140
|
+
return message != null && PLAIN_NO_MATCH.test(message);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The error a single symbol failed with, and when it is search finding no
|
|
145
|
+
* listing for the symbol, that error with the nearest listing named in its
|
|
146
|
+
* message: `No ticker match found for "USD_NGN"; did you mean USDNGN=X (US
|
|
147
|
+
* Dollar/Nigerian Naira)?`. Any other error comes back as it is.
|
|
148
|
+
*/
|
|
149
|
+
export async function withSuggestion(error: unknown, input: string, deps: SuggestionDeps): Promise<unknown> {
|
|
150
|
+
if (!(error instanceof TickerNotFoundError) || !isPlainNoMatch(error.message)) return error;
|
|
151
|
+
const suggestion = await suggestListing(input, deps);
|
|
152
|
+
return suggestion ? new TickerNotFoundError(`${error.message.replace(/\.$/, "")}; did you mean ${describeSuggestion(suggestion)}?`) : error;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The suggestion of each input, null where there is none; none at all for more than `MAX_SUGGESTED_SYMBOLS`. */
|
|
156
|
+
export async function suggestListings(inputs: readonly string[], deps: SuggestionDeps): Promise<Array<ListingSuggestion | null>> {
|
|
157
|
+
if (inputs.length > MAX_SUGGESTED_SYMBOLS) return inputs.map(() => null);
|
|
158
|
+
return mapWithConcurrency(inputs, SUGGESTION_CONCURRENCY, (input) => suggestListing(input, deps));
|
|
159
|
+
}
|
package/src/cli/native-smoke.ts
CHANGED
|
@@ -26,3 +26,16 @@ export async function smokePluginHost(): Promise<void> {
|
|
|
26
26
|
const { smokePluginHostLoad } = await import("../plugins/host-smoke");
|
|
27
27
|
await smokePluginHostLoad();
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
export const SHOT_VIEW_SMOKE_COMMAND = "__gloomberb-smoke-shot-view";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Reads the page `shot` and rendered `fn` reports draw panes in. The release
|
|
34
|
+
* binary has no sources to build it from, so it carries a prebuilt copy; run
|
|
35
|
+
* from outside a checkout, this fails when that copy is missing or incomplete
|
|
36
|
+
* rather than on a user's first capture.
|
|
37
|
+
*/
|
|
38
|
+
export async function smokeShotView(): Promise<void> {
|
|
39
|
+
const { loadShotView } = await import("./desktop-pane-shot-view");
|
|
40
|
+
await loadShotView();
|
|
41
|
+
}
|
package/src/cli/not-a-ticker.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { notATickerSymbol } from "../sources/provider-errors";
|
|
|
2
2
|
import { splitSymbolArgs } from "./symbol-args";
|
|
3
3
|
|
|
4
4
|
// "Not a ticker: APPLE." as the router words it; the symbol ends at the sentence's own period.
|
|
5
|
-
|
|
5
|
+
// One that already names what to try (a search, a FRED series, the pair search offered) is left alone.
|
|
6
|
+
const NOT_A_TICKER_SENTENCE = /Not a ticker: ((?:(?!\.\s)[^\n`\x1b])+?)\.(?=\s|\x1b|$)(?! Try `gloomberb (?:search|fred) | Did you mean )/g;
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* The symbol as the user typed it on the command line (`Apple`), not as the router saw it (`APPLE`).
|
|
@@ -46,6 +47,17 @@ export function withSearchHints(text: string, args: readonly string[], isFredSer
|
|
|
46
47
|
: text;
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
/**
|
|
51
|
+
* "Not a ticker: USD_NGN. Did you mean USDNGN=X (US Dollar/Nigerian Naira)?",
|
|
52
|
+
* the listing search suggests, which takes the place of the search hint.
|
|
53
|
+
*/
|
|
54
|
+
export function withListingSuggestion(message: string, suggestion: string): string {
|
|
55
|
+
const symbol = notATickerSymbol(message);
|
|
56
|
+
if (!symbol || message.includes(" Did you mean ")) return message;
|
|
57
|
+
const sentence = `Not a ticker: ${symbol}.`;
|
|
58
|
+
return message.replace(sentence, `${sentence} Did you mean ${suggestion}?`);
|
|
59
|
+
}
|
|
60
|
+
|
|
49
61
|
/** Whether an error message says the symbol is not a ticker. */
|
|
50
62
|
export function isNotATickerMessage(message: string | null | undefined): boolean {
|
|
51
63
|
return message != null && notATickerSymbol(message) !== null;
|
|
@@ -140,19 +140,24 @@ export function capabilityHasOption(capability: PaneFunctionCapability, key: str
|
|
|
140
140
|
return resolveOptionDef(capability, key) !== undefined;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
function normalizeEnumValue(option: PaneFunctionOptionDef, rawValue: string): string {
|
|
143
|
+
function normalizeEnumValue(option: PaneFunctionOptionDef, rawValue: string, argument: string | null): string {
|
|
144
144
|
const token = optionToken(rawValue);
|
|
145
145
|
const match = option.values?.find((candidate) => (
|
|
146
146
|
optionToken(candidate.value) === token
|
|
147
147
|
|| candidate.aliases?.some((alias) => optionToken(alias) === token)
|
|
148
148
|
));
|
|
149
149
|
if (match) return match.value;
|
|
150
|
+
const hint = option.invalidValueHint?.(rawValue, argument);
|
|
150
151
|
throw new Error(
|
|
151
|
-
`Invalid --${option.key} value "${rawValue}"
|
|
152
|
+
`Invalid --${option.key} value "${rawValue}".${hint ? ` ${hint}` : ""} Use one of: ${option.values?.map(({ value }) => value).join(", ") ?? ""}.`,
|
|
152
153
|
);
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
function normalizeOptionValue(
|
|
156
|
+
function normalizeOptionValue(
|
|
157
|
+
option: PaneFunctionOptionDef,
|
|
158
|
+
rawValue: string | true,
|
|
159
|
+
argument: string | null = null,
|
|
160
|
+
): string | number | boolean {
|
|
156
161
|
if (option.type === "boolean") {
|
|
157
162
|
if (rawValue === true) return true;
|
|
158
163
|
if (/^true$/i.test(rawValue)) return true;
|
|
@@ -161,7 +166,7 @@ function normalizeOptionValue(option: PaneFunctionOptionDef, rawValue: string |
|
|
|
161
166
|
}
|
|
162
167
|
if (rawValue === true) throw new Error(`--${option.key} requires a value.`);
|
|
163
168
|
if (option.normalize) return option.normalize(rawValue);
|
|
164
|
-
if (option.type === "enum") return normalizeEnumValue(option, rawValue);
|
|
169
|
+
if (option.type === "enum") return normalizeEnumValue(option, rawValue, argument);
|
|
165
170
|
if (option.type === "integer") {
|
|
166
171
|
const value = Number(rawValue);
|
|
167
172
|
if (!Number.isInteger(value)) throw new Error(`--${option.key} must be an integer.`);
|
|
@@ -179,14 +184,17 @@ function normalizeOptionValue(option: PaneFunctionOptionDef, rawValue: string |
|
|
|
179
184
|
export function normalizeCapabilityOptions(
|
|
180
185
|
capability: PaneFunctionCapability,
|
|
181
186
|
options: Record<string, string | true>,
|
|
182
|
-
/**
|
|
183
|
-
|
|
187
|
+
/**
|
|
188
|
+
* `code` is what the user types for the function (`CDX`), which an error names; the capability id stands in when
|
|
189
|
+
* there is none. `argument` is what the function was given (`RMS.AX`), which an option's pointer names.
|
|
190
|
+
*/
|
|
191
|
+
settings: { strict?: boolean; code?: string; argument?: string | null } = {},
|
|
184
192
|
): NormalizedPaneFunctionOptions {
|
|
185
193
|
if (!capability.botSafe) {
|
|
186
194
|
// A rendered view takes any pane setting; the options it declares are checked.
|
|
187
195
|
return Object.fromEntries(Object.entries(options).map(([key, value]) => {
|
|
188
196
|
const option = resolveOptionDef(capability, key);
|
|
189
|
-
return option ? [option.key, normalizeOptionValue(option, value)] : [key, value];
|
|
197
|
+
return option ? [option.key, normalizeOptionValue(option, value, settings.argument)] : [key, value];
|
|
190
198
|
}));
|
|
191
199
|
}
|
|
192
200
|
|
|
@@ -206,7 +214,7 @@ export function normalizeCapabilityOptions(
|
|
|
206
214
|
`${settings.code ?? capability.id} does not support --${rawKey}.${supported ? ` Supported options: ${supported}.` : " It has no options."}`,
|
|
207
215
|
);
|
|
208
216
|
}
|
|
209
|
-
normalized[option.key] = normalizeOptionValue(option, rawValue);
|
|
217
|
+
normalized[option.key] = normalizeOptionValue(option, rawValue, settings.argument);
|
|
210
218
|
}
|
|
211
219
|
return normalized;
|
|
212
220
|
}
|