gloomberb 0.19.0 → 0.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +2 -1
- package/patches/electrobun@1.18.1.patch +23 -1
- package/src/api-client/app-attention.ts +3 -0
- package/src/api-client/beneficial-owners.ts +3 -0
- package/src/api-client/company-kpis.ts +3 -0
- package/src/api-client/data.ts +2 -1
- package/src/api-client/errors.ts +38 -0
- package/src/api-client/hiring.ts +3 -0
- package/src/api-client/index.ts +4 -0
- package/src/api-client/ipo.ts +24 -1
- package/src/api-client/mna.ts +3 -0
- package/src/api-client/paths.ts +9 -1
- package/src/api-client/roadmap.ts +85 -0
- package/src/api-client/types.ts +12 -0
- package/src/app/runtime/startup.ts +3 -1
- package/src/architecture/core-builtin-allowlist.json +2 -2
- package/src/capabilities/chart-series.ts +2 -1
- package/src/cli/commands/market.ts +44 -16
- package/src/cli/commands/news-search.ts +22 -0
- package/src/cli/commands/overview.ts +84 -19
- package/src/cli/commands/search.ts +13 -13
- package/src/cli/commands/ticker.ts +32 -2
- package/src/cli/context.ts +1 -0
- package/src/cli/errors.ts +17 -2
- package/src/cli/glossary.ts +8 -1
- package/src/cli/history-rows.ts +35 -7
- package/src/cli/index-roots.ts +0 -7
- package/src/cli/known-gaps.ts +11 -0
- package/src/cli/listing-arg.ts +74 -11
- package/src/cli/pane-functions/capabilities.ts +1 -0
- package/src/cli/pane-functions/discovery.ts +21 -1
- package/src/cli/pane-functions/freshness.ts +7 -1
- package/src/cli/pane-functions/function-help.ts +7 -0
- package/src/cli/pane-functions/index.ts +6 -3
- package/src/cli/quote-answer.ts +30 -7
- package/src/cli/saved-copy-persistence.ts +88 -0
- package/src/components/command-bar/list/model.ts +5 -0
- package/src/components/command-bar/routes/root/runtime.ts +29 -8
- package/src/components/command-bar/selection-runtime.ts +64 -32
- package/src/components/command-bar/surface/index.tsx +2 -0
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/header.tsx +2 -2
- package/src/components/layout/pane/capture-stamp.ts +2 -2
- package/src/components/layout/pane/footer/index.tsx +23 -6
- package/src/components/layout/pane/footer/model.ts +116 -16
- package/src/components/layout/pane/header.tsx +2 -2
- package/src/components/layout/pane/status-footer.ts +37 -3
- package/src/components/layout/status-widget-space.ts +9 -0
- package/src/components/onboarding/onboarding-frame.tsx +2 -1
- package/src/components/onboarding/onboarding-wizard.tsx +6 -3
- package/src/components/ticker/list-table-view.tsx +4 -20
- package/src/components/ui/display.tsx +29 -3
- package/src/components/ui/icon.tsx +12 -3
- package/src/core/app-services.ts +11 -2
- package/src/data/app-persistence.ts +9 -7
- package/src/data/plugin-cache.ts +20 -3
- package/src/data/plugin-state-store.ts +81 -24
- package/src/data/resource-store.ts +130 -58
- package/src/data/saved-copies.ts +8 -0
- package/src/data/session-store.ts +31 -8
- package/src/data/sqlite/access.ts +313 -0
- package/src/data/sqlite/checkpointer.ts +198 -0
- package/src/data/sqlite/database.ts +47 -4
- package/src/data/sqlite/retry.ts +50 -9
- package/src/data/ticker-store.ts +57 -23
- package/src/docs/snapshot.json +32 -5
- package/src/i18n/es.ts +2 -4
- package/src/i18n/ja.ts +2 -4
- package/src/i18n/ko.ts +2 -4
- package/src/i18n/zh-cn.ts +2 -4
- package/src/i18n/zh-tw.ts +2 -4
- package/src/market-data/depositary-pair.ts +25 -1
- package/src/market-data/quotes/metadata.ts +7 -0
- package/src/market-data/realized-volatility.ts +4 -2
- package/src/news/aggregator.ts +7 -1
- package/src/news/types.ts +6 -0
- package/src/plugins/auto-update-node.ts +10 -6
- package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
- package/src/plugins/builtin/alerts/cli.ts +34 -11
- package/src/plugins/builtin/alerts/event-rows.ts +24 -3
- package/src/plugins/builtin/alerts/events-pane.tsx +15 -6
- package/src/plugins/builtin/alerts/headless.ts +10 -6
- package/src/plugins/builtin/cds/client.ts +142 -30
- package/src/plugins/builtin/cds/headless.ts +35 -8
- package/src/plugins/builtin/cds/model.ts +145 -18
- package/src/plugins/builtin/cds/pane.tsx +16 -5
- package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
- package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
- package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +5 -0
- package/src/plugins/builtin/chart-composer/index.tsx +4 -5
- package/src/plugins/builtin/chart-composer/pane.tsx +5 -1
- package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
- package/src/plugins/builtin/chat/controller/index.ts +5 -1
- package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
- package/src/plugins/builtin/chat/controller/state.ts +25 -2
- package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
- package/src/plugins/builtin/chat/controller/utils.ts +7 -2
- package/src/plugins/builtin/chat/status-preview.ts +79 -0
- package/src/plugins/builtin/chat/status-widget.tsx +147 -10
- package/src/plugins/builtin/chat/unread-badge.ts +51 -10
- package/src/plugins/builtin/clinical-trials/client.ts +49 -4
- package/src/plugins/builtin/clinical-trials/model.ts +86 -22
- package/src/plugins/builtin/clinical-trials/pane.tsx +46 -27
- package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +87 -9
- package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
- package/src/plugins/builtin/commodity-balances/client.ts +4 -1
- package/src/plugins/builtin/company-attention/headless.ts +2 -1
- package/src/plugins/builtin/company-attention/pane.tsx +3 -0
- package/src/plugins/builtin/company-kpis/headless.ts +8 -3
- package/src/plugins/builtin/company-kpis/model.ts +73 -5
- package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
- package/src/plugins/builtin/composite-plugins.ts +2 -1
- package/src/plugins/builtin/cot/client.ts +28 -2
- package/src/plugins/builtin/cot/headless.ts +4 -1
- package/src/plugins/builtin/cot/index.tsx +4 -2
- package/src/plugins/builtin/cot/model.ts +58 -7
- package/src/plugins/builtin/cot/pane.tsx +6 -2
- package/src/plugins/builtin/cpi/client.ts +6 -5
- package/src/plugins/builtin/credit-boards/series.ts +3 -4
- package/src/plugins/builtin/credit-conditions/client.ts +22 -14
- package/src/plugins/builtin/credit-documents/headless.ts +3 -3
- package/src/plugins/builtin/crypto-board/client.ts +5 -5
- package/src/plugins/builtin/crypto-board/series.ts +4 -4
- package/src/plugins/builtin/debt-maturities/series.ts +3 -3
- package/src/plugins/builtin/doe/client.ts +6 -5
- package/src/plugins/builtin/doe/headless.ts +3 -2
- package/src/plugins/builtin/doe/pane.tsx +13 -3
- package/src/plugins/builtin/doe/prices.ts +2 -3
- package/src/plugins/builtin/doe/series.ts +4 -4
- package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
- package/src/plugins/builtin/earnings-calls/data.ts +37 -17
- package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
- package/src/plugins/builtin/earnings-calls/pane.tsx +34 -14
- package/src/plugins/builtin/econ/calendar-model.ts +48 -1
- package/src/plugins/builtin/econ/calendar-source.ts +4 -2
- package/src/plugins/builtin/econ/detail-view.tsx +18 -4
- package/src/plugins/builtin/econ/figures.ts +48 -0
- package/src/plugins/builtin/econ/index.tsx +14 -5
- package/src/plugins/builtin/econ/print-watch.ts +3 -1
- package/src/plugins/builtin/econ-statistics/client.ts +11 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +10 -2
- package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +3 -0
- package/src/plugins/builtin/executives/data.ts +6 -1
- package/src/plugins/builtin/executives/pane.tsx +9 -21
- package/src/plugins/builtin/fear-greed/cache.ts +4 -0
- package/src/plugins/builtin/fear-greed/series.ts +6 -4
- package/src/plugins/builtin/gpu/client.ts +4 -1
- package/src/plugins/builtin/gpu/series.ts +2 -3
- package/src/plugins/builtin/help/index.tsx +61 -59
- package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
- package/src/plugins/builtin/help/table-cells.tsx +13 -2
- package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
- package/src/plugins/builtin/holders/beneficial-view.tsx +4 -2
- package/src/plugins/builtin/holders/headless.ts +4 -2
- package/src/plugins/builtin/holders/pane.tsx +3 -0
- package/src/plugins/builtin/holders/report-rows.ts +4 -1
- package/src/plugins/builtin/insider/headless.ts +2 -2
- package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
- package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
- package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
- package/src/plugins/builtin/jodi/client.ts +4 -1
- package/src/plugins/builtin/macro-day/headless.ts +12 -5
- package/src/plugins/builtin/macro-day/index.tsx +1 -1
- package/src/plugins/builtin/macro-day/model.ts +39 -5
- package/src/plugins/builtin/macro-day/pane.tsx +27 -19
- package/src/plugins/builtin/market-movers/client.ts +3 -0
- package/src/plugins/builtin/market-movers/headless.ts +3 -1
- package/src/plugins/builtin/market-movers/index.tsx +10 -5
- package/src/plugins/builtin/market-movers/screener.ts +27 -6
- package/src/plugins/builtin/market-movers/session-body.tsx +2 -1
- package/src/plugins/builtin/mna/headless.ts +2 -0
- package/src/plugins/builtin/mna/pane.tsx +3 -1
- package/src/plugins/builtin/money-markets/series.ts +3 -5
- package/src/plugins/builtin/news/headless.ts +3 -1
- package/src/plugins/builtin/news/index.tsx +3 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -6
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
- package/src/plugins/builtin/portfolio-list/table.tsx +4 -6
- package/src/plugins/builtin/power/gas-client.ts +2 -1
- package/src/plugins/builtin/power/prices-client.ts +3 -2
- package/src/plugins/builtin/realized-vol/client.ts +13 -5
- package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
- package/src/plugins/builtin/realized-vol/headless.ts +8 -4
- package/src/plugins/builtin/realized-vol/model.ts +37 -6
- package/src/plugins/builtin/realized-vol/pane.tsx +9 -7
- package/src/plugins/builtin/research/event-model.ts +30 -4
- package/src/plugins/builtin/research/index.tsx +7 -3
- package/src/plugins/builtin/research/relative-valuation-headless.ts +8 -25
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +52 -15
- package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
- package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
- package/src/plugins/builtin/risk-factors/data.ts +12 -6
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- package/src/plugins/builtin/risk-factors/index.tsx +3 -1
- package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
- package/src/plugins/builtin/roadmap/index.tsx +28 -0
- package/src/plugins/builtin/roadmap/model.ts +67 -0
- package/src/plugins/builtin/roadmap/pane.tsx +244 -0
- package/src/plugins/builtin/scanner/live-headless.ts +21 -2
- package/src/plugins/builtin/scanner/lock-note.ts +26 -3
- package/src/plugins/builtin/sec/headless.ts +2 -1
- package/src/plugins/builtin/sec/index.tsx +3 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +20 -7
- package/src/plugins/builtin/shared/fred-points.ts +6 -1
- package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
- package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
- package/src/plugins/builtin/shared/report-freshness.ts +8 -0
- package/src/plugins/builtin/shared/research-cells.tsx +1 -1
- package/src/plugins/builtin/shared/series-cache.ts +4 -4
- package/src/plugins/builtin/shared/series-warning.ts +13 -4
- package/src/plugins/builtin/short-volume/series.ts +2 -3
- package/src/plugins/builtin/social-mentions/client.ts +15 -10
- package/src/plugins/builtin/social-mentions/series.ts +2 -3
- package/src/plugins/builtin/thirteenf/api.ts +15 -9
- package/src/plugins/builtin/thirteenf/data.ts +39 -11
- package/src/plugins/builtin/thirteenf/headless.ts +31 -4
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +21 -5
- package/src/plugins/builtin/thirteenf/types.ts +2 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -2
- package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
- package/src/plugins/builtin/ticker-detail/headless.ts +59 -8
- package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +11 -6
- package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
- package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
- package/src/plugins/builtin/treasury-auctions/headless.ts +24 -13
- package/src/plugins/builtin/treasury-auctions/pane.tsx +18 -6
- package/src/plugins/builtin/volatility/client.ts +43 -16
- package/src/plugins/builtin/volatility/headless.ts +5 -2
- package/src/plugins/builtin/volatility/model.ts +17 -6
- package/src/plugins/builtin/volatility/pane.tsx +11 -4
- package/src/plugins/builtin/volatility/series.ts +2 -1
- package/src/plugins/builtin/yield-curve/client.ts +59 -1
- package/src/plugins/builtin/yield-curve/headless.ts +9 -6
- package/src/plugins/builtin/yield-curve/index.tsx +34 -10
- package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
- package/src/plugins/catalog-browser.ts +2 -1
- package/src/plugins/host-link.ts +1 -1
- package/src/plugins/installer.ts +50 -39
- package/src/portfolio/column-values.ts +11 -7
- package/src/renderers/dom/desktop/icons.tsx +10 -2
- package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +9 -1
- package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
- package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
- package/src/renderers/electrobun/bun/index.ts +26 -2
- package/src/renderers/electrobun/bun/window/attention.ts +54 -0
- package/src/renderers/electrobun/bun/window/create.ts +45 -4
- package/src/renderers/electrobun/bun/window/events.ts +20 -0
- package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
- package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
- package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
- package/src/renderers/electrobun/launcher/preload.ts +13 -0
- package/src/renderers/electrobun/launcher/windows-emulation.ts +50 -0
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/news-source.ts +2 -0
- package/src/renderers/electrobun/view/ui-host.tsx +16 -1
- package/src/series/resolve.ts +2 -1
- package/src/series/types.ts +5 -0
- package/src/sources/gloomberb-cloud/fred-series.ts +8 -4
- package/src/sources/gloomberb-cloud/index.ts +42 -8
- package/src/telemetry/stall-reports-node.ts +13 -2
- package/src/telemetry/stall-reports.ts +189 -13
- package/src/test-support/outage.ts +8 -0
- package/src/tickers/company-name-listing.ts +110 -40
- package/src/tickers/company-name.ts +71 -2
- package/src/tickers/index-roots.ts +7 -0
- package/src/tickers/listing-fate.ts +2 -2
- package/src/tickers/search/home-listing.ts +93 -8
- package/src/tickers/search/index.ts +7 -3
- package/src/tickers/search/ranking.ts +5 -1
- package/src/tickers/search/upsert.ts +13 -0
- package/src/tickers/ticker-changes.ts +8 -4
- package/src/time-series/market.ts +5 -3
- package/src/time-series/price-comparison.ts +20 -2
- package/src/time-series/resolve.ts +20 -1
- package/src/time-series/types.ts +7 -0
- package/src/types/company-from.ts +40 -0
- package/src/types/data-provider.ts +7 -0
- package/src/types/financials.ts +45 -3
- package/src/types/headless.ts +6 -0
- package/src/ui/host.tsx +6 -1
- package/src/utils/crypto-pair.ts +121 -7
- package/src/utils/declared-dividends.ts +1 -1
- package/src/utils/financial-statements.ts +73 -16
- package/src/utils/fundamentals.ts +53 -10
- package/src/utils/lease-liabilities.ts +54 -0
- package/src/utils/main-thread-stall.ts +47 -2
- package/src/utils/perf-marks.ts +23 -11
- package/src/utils/price-history-integrity.ts +47 -1
- package/src/version.ts +2 -2
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { logFailure } from "../utils/log-failure";
|
|
2
2
|
import type { DataProvider } from "../types/data-provider";
|
|
3
|
+
import type { Quote } from "../types/financials";
|
|
3
4
|
import type { InstrumentSearchResult } from "../types/instrument";
|
|
4
5
|
import { canonicalExchange, isUsListingExchange, parsePublicTickerKey } from "../utils/exchanges";
|
|
5
|
-
import { companyNameMatch,
|
|
6
|
+
import { companyLines, companyNameExtraWords, companyNameMatch, namedByService, runTogether, shortCompanyName } from "./company-name";
|
|
6
7
|
import { LISTING_SEARCH_LIMIT } from "./search";
|
|
7
|
-
import { lineDistance } from "./search/home-listing";
|
|
8
|
+
import { lineDistance, primaryCompanyLine } from "./search/home-listing";
|
|
8
9
|
import { classifyInstrumentKind, isCryptoInstrumentType } from "./search/ranking";
|
|
9
10
|
import { isProviderBusy, type ProviderBusyError } from "../sources/provider-errors";
|
|
10
11
|
import { readTerminalListingCode, type TerminalListing, type TerminalListingCode } from "./terminal-codes";
|
|
@@ -12,18 +13,36 @@ import { hasNonLatinLetters } from "../utils/typed-text";
|
|
|
12
13
|
|
|
13
14
|
export interface CompanyNameListing {
|
|
14
15
|
symbol: string;
|
|
15
|
-
/** Canonical code of its exchange. */
|
|
16
|
+
/** Canonical code of its exchange; empty for a US symbol read bare (a line its company `moved` to). */
|
|
16
17
|
exchange: string;
|
|
17
18
|
/** The listing's name as search gives it. */
|
|
18
19
|
name: string;
|
|
20
|
+
/** The service marked the listing with the name as typed (`searchAliases`): the company it means by it. */
|
|
21
|
+
named?: true;
|
|
19
22
|
/**
|
|
20
23
|
* When the listing is a fund or an index carrying the name ("JP Morgan" read
|
|
21
24
|
* as ^JTEK, JP MORGAN ETF TRUS), the company's common stock the name says
|
|
22
25
|
* run together (JPM, JPMorgan Chase & Co.), which the reader is asked about.
|
|
23
26
|
*/
|
|
24
27
|
meant?: Omit<CompanyNameListing, "meant">;
|
|
28
|
+
/**
|
|
29
|
+
* When the company's US line, which search still lists, moved to another
|
|
30
|
+
* symbol and this is the line it trades as now (GETY became GETYQ): the
|
|
31
|
+
* change in one line, "GETY became GETYQ on 2026-10-22".
|
|
32
|
+
*/
|
|
33
|
+
moved?: string;
|
|
25
34
|
}
|
|
26
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Where a listing's company trades now when it moved off the symbol (GETY
|
|
38
|
+
* became GETYQ on 2026-10-22), from the line's quote (null when it has none)
|
|
39
|
+
* and the name search gives it: the symbol it took, and the change in one
|
|
40
|
+
* line. Null when it did not move, or that cannot be told.
|
|
41
|
+
*/
|
|
42
|
+
export type MovedLineLookup = (
|
|
43
|
+
listing: { symbol: string; exchange: string; quote: Quote | null; name: string | null },
|
|
44
|
+
) => Promise<{ symbol: string; note: string } | null>;
|
|
45
|
+
|
|
27
46
|
/** Share lines that are not the company's common stock, though search may type them as such. */
|
|
28
47
|
const NOT_COMMON = /preferred|warrant|right|unit|note|bond|debenture/i;
|
|
29
48
|
const RECEIPT = /depositary|^(?:ADR|ADS|GDR|CDR|BDR)$/i;
|
|
@@ -50,7 +69,10 @@ export function isCompanyNameInput(input: string): boolean {
|
|
|
50
69
|
* depositary receipt (SAP's NYSE line for "SAP SE"), so a receipt is the
|
|
51
70
|
* listing only of a company listed by receipts alone (PDD); then the row the
|
|
52
71
|
* service marks with the name, a US line, the shortest symbol (WHR before
|
|
53
|
-
* WHR-A) and search's order. The
|
|
72
|
+
* WHR-A) and search's order. The first then gives way to the line `search`
|
|
73
|
+
* calls the company's primary listing (`primaryCompanyLine`), though its name
|
|
74
|
+
* says more: "lufthansa" is LHA on XETRA, not LUFTHANSA on Budapest, a
|
|
75
|
+
* cross-listing with no search score. The row the service marks with the name as typed
|
|
54
76
|
* (`searchAliases`: JPM for "JP Morgan", SIE for "SIEMENS AG", though its name
|
|
55
77
|
* is Siemens Aktiengesellschaft) is named by it exactly. Contracts, preferred
|
|
56
78
|
* shares and notes are never the listing. With `quoted`, a listing with no
|
|
@@ -70,7 +92,7 @@ export function isCompanyNameInput(input: string): boolean {
|
|
|
70
92
|
export async function findListingOfCompanyName(
|
|
71
93
|
dataProvider: Pick<DataProvider, "search"> & Partial<Pick<DataProvider, "getQuote">>,
|
|
72
94
|
typed: string,
|
|
73
|
-
options: { meant?: boolean; oneWord?: boolean; quoted?: boolean } = {},
|
|
95
|
+
options: { meant?: boolean; oneWord?: boolean; quoted?: boolean; movedLine?: MovedLineLookup } = {},
|
|
74
96
|
): Promise<CompanyNameListing | null> {
|
|
75
97
|
const name = typed.trim();
|
|
76
98
|
if (!name) return null;
|
|
@@ -78,40 +100,62 @@ export async function findListingOfCompanyName(
|
|
|
78
100
|
const answers = await Promise.all(queries.map((query) => searchListings(dataProvider, query)));
|
|
79
101
|
const busy = answers.find(isProviderBusy);
|
|
80
102
|
const results = answers.flatMap((answer) => Array.isArray(answer) ? answer : []);
|
|
81
|
-
const
|
|
103
|
+
const listed = results.flatMap((result, order) => {
|
|
82
104
|
const exchange = listedExchange(result);
|
|
83
105
|
const type = listedType(result);
|
|
84
106
|
const named = namedByService(name, result);
|
|
85
107
|
const match = named ? { extra: 0, abbreviated: 0 } : companyNameMatch(name, result.name);
|
|
86
108
|
if (!exchange || match == null || classifyInstrumentKind(type) === "derivative" || NOT_COMMON.test(type)
|
|
87
109
|
|| parsePublicTickerKey(result.symbol).exchange) return [];
|
|
88
|
-
|
|
110
|
+
// A word alone names a company by these lines only; the company's other lines are still its lines.
|
|
111
|
+
const namesCompany = !(options.oneWord && (match.abbreviated > 0 || isCryptoInstrumentType(type) || !(order === 0 || named || match.extra === 0)));
|
|
89
112
|
// A receipt is a company's shares too, however search types it ("Depositary Receipt" or "American Depositary Receipt").
|
|
90
113
|
const stock = classifyInstrumentKind(type) === "equity" || RECEIPT.test(type) ? 0 : 1;
|
|
91
114
|
const home = result.homeListing === true;
|
|
92
115
|
const line = home ? 0 : 1 + lineDistance(type, exchange, result.symbol);
|
|
93
116
|
return [{
|
|
94
|
-
result, exchange, stock, home,
|
|
117
|
+
result, exchange, stock, home, namesCompany,
|
|
95
118
|
rank: [match.extra, match.abbreviated, stock, line, named ? 0 : 1, isUsListingExchange(exchange) ? 0 : 1, result.symbol.length, order],
|
|
96
119
|
}];
|
|
97
120
|
}).sort((left, right) => {
|
|
98
121
|
const at = left.rank.findIndex((value, index) => value !== right.rank[index]);
|
|
99
122
|
return at < 0 ? 0 : left.rank[at]! - right.rank[at]!;
|
|
100
123
|
});
|
|
101
|
-
const first =
|
|
124
|
+
const first = listed.find((listing) => listing.namesCompany);
|
|
102
125
|
// Listings are named in Latin letters: a name in another script ("任天堂 ソニー") names none, answered or not.
|
|
103
126
|
if (!first && busy && !hasNonLatinLetters(name)) throw busy;
|
|
104
127
|
if (!first) return null;
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const
|
|
128
|
+
// Another line the service marks as a home is another company's (Siemens Ltd. on NSE, for "SIEMENS AG"), and
|
|
129
|
+
// so is a line named as that one is (Siemens Ltd. on BSE).
|
|
130
|
+
const otherHomesThan = (lead: typeof first) => {
|
|
131
|
+
const homes = new Set(listed.filter((listing) => listing.home && listing !== lead).map(({ result }) => plainName(result.name)));
|
|
132
|
+
homes.delete(plainName(lead.result.name));
|
|
133
|
+
return homes;
|
|
134
|
+
};
|
|
135
|
+
// The listing `search` calls the company's primary leads, though its name says more than the one typed: the
|
|
136
|
+
// name typed opens what a search for it lists first (LHA on XETRA for "lufthansa", not LUFTHANSA on Budapest).
|
|
137
|
+
const named = namedByService(name, first.result);
|
|
138
|
+
const firstHomes = otherHomesThan(first);
|
|
139
|
+
// Never a line of another business its name is in (`companyLines`): Siemens Energy AG is not Siemens AG's.
|
|
140
|
+
const lead = first.home ? first : primaryCompanyLine(first, companyLines(first, listed, lineFacts).filter((listing) => listing === first
|
|
141
|
+
|| (!listing.home && !firstHomes.has(plainName(listing.result.name)) && listing.stock === first.stock
|
|
142
|
+
&& (!named || companyNameExtraWords(listing.result.name, first.result.name) != null))),
|
|
143
|
+
({ result, exchange }) => ({ distance: lineDistance(listedType(result), exchange, result.symbol), score: result.popularity ?? null }),
|
|
144
|
+
{ named });
|
|
145
|
+
// The lead may give way to its company's other lines: as near the name and of its kind.
|
|
146
|
+
const otherHomes = otherHomesThan(lead);
|
|
147
|
+
const lines = [lead, ...companyLines(lead, listed, lineFacts).filter((listing) => listing !== lead && !listing.home
|
|
148
|
+
&& !otherHomes.has(plainName(listing.result.name))
|
|
149
|
+
&& listing.rank.slice(0, NAME_RANKS).every((value, index) => value === lead.rank[index]))];
|
|
150
|
+
const quoted = options.quoted ? await firstQuoted(dataProvider, lines, options.movedLine) : { line: lead, moved: null };
|
|
151
|
+
const best = quoted.line;
|
|
152
|
+
const found: CompanyNameListing = quoted.moved
|
|
153
|
+
// The line it moved to is a US symbol, read bare as `quote` reads one (GETYQ, which trades over the counter).
|
|
154
|
+
? { symbol: quoted.moved.symbol, exchange: "", name: best.result.name, moved: quoted.moved.note }
|
|
155
|
+
: {
|
|
156
|
+
symbol: best.result.symbol.toUpperCase(), exchange: best.exchange, name: best.result.name,
|
|
157
|
+
...namedByService(name, best.result) ? { named: true as const } : {},
|
|
158
|
+
};
|
|
115
159
|
const meant = best.stock && options.meant ? await stockNamedRunTogether(dataProvider, name, results) : null;
|
|
116
160
|
return meant ? { ...found, meant } : found;
|
|
117
161
|
}
|
|
@@ -127,22 +171,41 @@ interface RankedListing {
|
|
|
127
171
|
exchange: string;
|
|
128
172
|
}
|
|
129
173
|
|
|
174
|
+
/** A US exchange, or the US over-the-counter market a line moves to when it leaves one. */
|
|
175
|
+
function isUsVenue(exchange: string): boolean {
|
|
176
|
+
return isUsListingExchange(exchange) || exchange.startsWith("OTC") || exchange === "PNK";
|
|
177
|
+
}
|
|
178
|
+
|
|
130
179
|
/**
|
|
131
180
|
* The first of a company's lines, in their order, that has a quote: the first
|
|
132
181
|
* itself, or when it has none, the first of the next few that does, asked
|
|
133
|
-
* together.
|
|
134
|
-
*
|
|
182
|
+
* together. A first US line whose company moved to another symbol (GETY
|
|
183
|
+
* became GETYQ) is followed there before any other line, so a name never lands
|
|
184
|
+
* on a listing abroad that still quotes the old shares. The first stands when none has a quote,
|
|
185
|
+
* when the source is too busy to say, and when nothing here can ask for quotes.
|
|
135
186
|
*/
|
|
136
|
-
async function firstQuoted<T extends RankedListing>(
|
|
187
|
+
async function firstQuoted<T extends RankedListing>(
|
|
188
|
+
dataProvider: Partial<Pick<DataProvider, "getQuote">>,
|
|
189
|
+
lines: readonly T[],
|
|
190
|
+
movedLine?: MovedLineLookup,
|
|
191
|
+
): Promise<{ line: T; moved: { symbol: string; note: string } | null }> {
|
|
137
192
|
const first = lines[0]!;
|
|
138
|
-
|
|
139
|
-
|
|
193
|
+
const only = (line: T) => ({ line, moved: null });
|
|
194
|
+
if (!dataProvider.getQuote) return only(first);
|
|
195
|
+
const quoteOf = ({ result, exchange }: RankedListing) => dataProvider.getQuote!(result.symbol.toUpperCase(), exchange);
|
|
196
|
+
let quote: Quote | null = null;
|
|
140
197
|
try {
|
|
141
|
-
|
|
198
|
+
quote = await quoteOf(first) ?? null;
|
|
142
199
|
} catch (error) {
|
|
143
200
|
// A busy source says nothing of the listing; any other failure is a listing with no quote.
|
|
144
|
-
if (isProviderBusy(error)) return first;
|
|
201
|
+
if (isProviderBusy(error)) return only(first);
|
|
202
|
+
}
|
|
203
|
+
if (movedLine && isUsVenue(first.exchange)) {
|
|
204
|
+
const symbol = first.result.symbol.toUpperCase();
|
|
205
|
+
const moved = await movedLine({ symbol, exchange: first.exchange, quote, name: first.result.name?.trim() || null });
|
|
206
|
+
if (moved && moved.symbol.toUpperCase() !== symbol) return { line: first, moved: { ...moved, symbol: moved.symbol.toUpperCase() } };
|
|
145
207
|
}
|
|
208
|
+
if (hasPrice(quote)) return only(first);
|
|
146
209
|
const keys = new Set([listingKey(first)]);
|
|
147
210
|
const next: T[] = [];
|
|
148
211
|
for (const line of lines) {
|
|
@@ -151,14 +214,19 @@ async function firstQuoted<T extends RankedListing>(dataProvider: Partial<Pick<D
|
|
|
151
214
|
keys.add(listingKey(line));
|
|
152
215
|
next.push(line);
|
|
153
216
|
}
|
|
154
|
-
const priced = await Promise.all(next.map((line) =>
|
|
217
|
+
const priced = await Promise.all(next.map((line) => quoteOf(line).then(hasPrice).catch(() => {
|
|
155
218
|
// Intentional ignore: a line whose quote fails is a line with no quote.
|
|
156
219
|
return false;
|
|
157
220
|
})));
|
|
158
|
-
return next.find((_, index) => priced[index]) ?? first;
|
|
221
|
+
return only(next.find((_, index) => priced[index]) ?? first);
|
|
159
222
|
}
|
|
160
223
|
|
|
161
224
|
/** A name as two venues may spell it alike: "SIEMENS LTD" for Siemens Ltd. */
|
|
225
|
+
/** A listing as `companyLines` weighs it. */
|
|
226
|
+
function lineFacts({ result, exchange, home }: { result: InstrumentSearchResult; exchange: string; home: boolean }) {
|
|
227
|
+
return { name: result.name, exchange, receipt: RECEIPT.test(listedType(result)), home };
|
|
228
|
+
}
|
|
229
|
+
|
|
162
230
|
function plainName(name: string | null | undefined): string {
|
|
163
231
|
return (name ?? "").normalize("NFKD").replace(/\p{M}/gu, "").toUpperCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
|
|
164
232
|
}
|
|
@@ -171,6 +239,20 @@ function hasPrice(quote: { price?: number } | null | undefined): boolean {
|
|
|
171
239
|
return !!quote && Number.isFinite(quote.price) && quote.price !== 0;
|
|
172
240
|
}
|
|
173
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Whether search marks a listing under another symbol with the word as typed
|
|
244
|
+
* (`searchAliases`): "siemens" marks SIE on XETRA, whose symbol is not the
|
|
245
|
+
* word. One search, the one `findListingOfCompanyName` asks first, so asking
|
|
246
|
+
* this before it costs nothing more.
|
|
247
|
+
*/
|
|
248
|
+
export async function marksAnotherListing(dataProvider: Partial<Pick<DataProvider, "search">>, word: string): Promise<boolean> {
|
|
249
|
+
// A source that cannot search marks nothing.
|
|
250
|
+
if (typeof dataProvider.search !== "function") return false;
|
|
251
|
+
const answer = await searchListings(dataProvider as Pick<DataProvider, "search">, word.trim());
|
|
252
|
+
return Array.isArray(answer) && answer.some((result) =>
|
|
253
|
+
result.symbol.toUpperCase() !== word.trim().toUpperCase() && namedByService(word, result));
|
|
254
|
+
}
|
|
255
|
+
|
|
174
256
|
function searchListings(dataProvider: Pick<DataProvider, "search">, query: string): Promise<InstrumentSearchResult[] | ProviderBusyError> {
|
|
175
257
|
return dataProvider.search(query, { limit: LISTING_SEARCH_LIMIT })
|
|
176
258
|
.catch((error: unknown): InstrumentSearchResult[] | ProviderBusyError => {
|
|
@@ -188,18 +270,6 @@ function listedType(result: InstrumentSearchResult): string {
|
|
|
188
270
|
return result.brokerContract?.secType || result.type || "";
|
|
189
271
|
}
|
|
190
272
|
|
|
191
|
-
/** Letters and digits only, in capitals: "JP Morgan" and "JPMorgan" are JPMORGAN. */
|
|
192
|
-
function runTogether(text: string): string {
|
|
193
|
-
return text.normalize("NFKD").replace(/\p{M}/gu, "").toUpperCase().replace(/[^\p{L}\p{N}]/gu, "");
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/** Whether the service marked the row with the name as typed: the company it means by it. */
|
|
197
|
-
export function namedByService(typed: string, result: InstrumentSearchResult): boolean {
|
|
198
|
-
const wanted = runTogether(typed);
|
|
199
|
-
return !!wanted && Array.isArray(result.searchAliases)
|
|
200
|
-
&& result.searchAliases.some((alias) => typeof alias === "string" && runTogether(alias) === wanted);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
273
|
/**
|
|
204
274
|
* The company's common stock on a US exchange whose name starts with the
|
|
205
275
|
* typed words run together ("JPMorgan Chase & Co." for "JP Morgan"), first in
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { InstrumentSearchResult } from "../types/instrument";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Company names as venues spell them, compared and shortened for sentences.
|
|
3
5
|
* One company's listings carry its name with different legal forms and share
|
|
@@ -9,7 +11,7 @@
|
|
|
9
11
|
const NAME_NOISE = new Set([
|
|
10
12
|
"co", "company", "corp", "corporation", "inc", "incorporated", "ltd", "limited", "llc", "lp", "plc", "sa", "ag", "nv", "se",
|
|
11
13
|
"spa", "ab", "as", "asa", "oyj", "bv", "gmbh", "kgaa", "bhd", "berhad", "pcl", "the", "adr", "ads", "cedear", "cedears",
|
|
12
|
-
"sponsored", "unsponsored", "cdr", "cdi",
|
|
14
|
+
"sponsored", "unsponsored", "cdr", "cdi", "aktiengesellschaft", "aktiebolag",
|
|
13
15
|
]);
|
|
14
16
|
// Words a venue adds to the share it lists ("Microsoft Corporation Common Stock"), never part of whose it is.
|
|
15
17
|
const SHARE_NOISE = new Set(["common", "ordinary", "stock", "shares"]);
|
|
@@ -43,6 +45,56 @@ export function sameCompany(left: string | null | undefined, right: string | nul
|
|
|
43
45
|
return [...fewer].every((word) => more.has(word));
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
// Words that tell one class of a company's shares from another ("Berkshire Hathaway Inc. Class B"), never its business.
|
|
49
|
+
const SHARE_CLASS_WORD = /^(?:class|series|[a-z]|\d+)$/;
|
|
50
|
+
|
|
51
|
+
/** Whether two names differ only in how they name a class of shares, so they are one company's whatever the venue. */
|
|
52
|
+
function sameBusinessWords(left: Set<string>, right: Set<string>): boolean {
|
|
53
|
+
const differ = [...left].filter((word) => !right.has(word)).concat([...right].filter((word) => !left.has(word)));
|
|
54
|
+
return differ.every((word) => SHARE_CLASS_WORD.test(word));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A company line as `companyLines` weighs it. */
|
|
58
|
+
export interface CompanyLineFacts {
|
|
59
|
+
name: string | null | undefined;
|
|
60
|
+
/** The line's canonical venue. */
|
|
61
|
+
exchange: string;
|
|
62
|
+
/** A depositary receipt, whose name says whose shares it holds rather than whose it is. */
|
|
63
|
+
receipt: boolean;
|
|
64
|
+
/** The service marks it as its company's home line (`homeListing`). */
|
|
65
|
+
home: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The lines among `lines` that are `lead`'s company's own (`sameCompany`),
|
|
70
|
+
* less those under a name that adds words for another business, though one
|
|
71
|
+
* name's words are all in the other's: Siemens Energy AG and Siemens
|
|
72
|
+
* Healthineers AG are not Siemens AG, nor Sony Financial Group Sony Group. A
|
|
73
|
+
* name is another business's where a venue lists a line under it beside a
|
|
74
|
+
* line under the lead's own name (both on XETRA, both in Tokyo), or where it
|
|
75
|
+
* and a line under the lead's name are each marked as a home line, since a
|
|
76
|
+
* company has one (Siemens Ltd. on NSE beside Siemens AG on XETRA). Receipts
|
|
77
|
+
* are named for the shares they hold, so they never tell two businesses
|
|
78
|
+
* apart, and nor do words that name a class of shares (Class B). A name with
|
|
79
|
+
* more words on no shared venue stays the company's (Deutsche Lufthansa AG on
|
|
80
|
+
* XETRA for LUFTHANSA on Budapest). `lead` is always one of them.
|
|
81
|
+
*/
|
|
82
|
+
export function companyLines<T>(lead: T, lines: readonly T[], facts: (line: T) => CompanyLineFacts): T[] {
|
|
83
|
+
const own = facts(lead);
|
|
84
|
+
const ownWords = companyWords(own.name);
|
|
85
|
+
const ownName = (line: CompanyLineFacts) => sameBusinessWords(companyWords(line.name), ownWords);
|
|
86
|
+
const candidates = lines.filter((line) => line === lead || sameCompany(own.name, facts(line).name));
|
|
87
|
+
const read = candidates.map((line) => ({ line, facts: facts(line), words: companyWords(facts(line).name) }));
|
|
88
|
+
const ownShares = read.filter((entry) => !entry.facts.receipt && ownName(entry.facts));
|
|
89
|
+
const ownVenues = new Set(ownShares.map((entry) => entry.facts.exchange).filter(Boolean));
|
|
90
|
+
const ownHome = ownShares.some((entry) => entry.facts.home);
|
|
91
|
+
// A name another business goes by, once one of its share lines gives it away: all its lines are that business's.
|
|
92
|
+
const others = read.filter((entry) => !entry.facts.receipt && !ownName(entry.facts)
|
|
93
|
+
&& ((!!entry.facts.exchange && ownVenues.has(entry.facts.exchange)) || (entry.facts.home && ownHome)));
|
|
94
|
+
const otherBusiness = (words: Set<string>) => others.some((other) => sameBusinessWords(other.words, words));
|
|
95
|
+
return read.filter((entry) => entry.line === lead || ownName(entry.facts) || !otherBusiness(entry.words)).map((entry) => entry.line);
|
|
96
|
+
}
|
|
97
|
+
|
|
46
98
|
/**
|
|
47
99
|
* How many words a listing's name has beyond a company name typed for it,
|
|
48
100
|
* which it must have every word of: 0 for the same name however each spells
|
|
@@ -215,10 +267,15 @@ function nameWord(word: string, opening: boolean, domain: boolean): string {
|
|
|
215
267
|
* the way its source means it and comes back as it is.
|
|
216
268
|
*/
|
|
217
269
|
export function companyNameCase(name: string): string {
|
|
218
|
-
|
|
270
|
+
// A brand that opens in lower case before capitals (aTYR, iROBOT) is still a name in capitals: aTyr, iRobot.
|
|
271
|
+
const brand = /^\p{Ll}(?=\p{Lu}{2})/u;
|
|
272
|
+
const capitals = name.split(/\s+/).map((word) => word.replace(brand, "")).join(" ");
|
|
273
|
+
if (!/\p{Lu}/u.test(name) || capitals !== capitals.toUpperCase()) return name;
|
|
219
274
|
let words = 0;
|
|
220
275
|
return name.replace(/\S+/g, (token) => {
|
|
221
276
|
const opening = words++ === 0;
|
|
277
|
+
const lead = brand.exec(token)?.[0];
|
|
278
|
+
if (lead) return `${lead}${token.charAt(1)}${token.slice(2).toLowerCase()}`;
|
|
222
279
|
// A separator inside a word (Coca-Cola, AT&T, Amazon.com, McDonald's) cuts it into words of its own.
|
|
223
280
|
return token.split(/([-/.&,'\u2019]+)/).map((part, index, parts) => {
|
|
224
281
|
if (index % 2 === 1) return part;
|
|
@@ -229,3 +286,15 @@ export function companyNameCase(name: string): string {
|
|
|
229
286
|
}).join("");
|
|
230
287
|
});
|
|
231
288
|
}
|
|
289
|
+
|
|
290
|
+
/** Letters and digits only, in capitals: "JP Morgan" and "JPMorgan" are JPMORGAN. */
|
|
291
|
+
export function runTogether(text: string): string {
|
|
292
|
+
return text.normalize("NFKD").replace(/\p{M}/gu, "").toUpperCase().replace(/[^\p{L}\p{N}]/gu, "");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Whether the service marked the row with the name as typed: the company it means by it. */
|
|
296
|
+
export function namedByService(typed: string, result: Pick<InstrumentSearchResult, "searchAliases">): boolean {
|
|
297
|
+
const wanted = runTogether(typed);
|
|
298
|
+
return !!wanted && Array.isArray(result.searchAliases)
|
|
299
|
+
&& result.searchAliases.some((alias) => typeof alias === "string" && runTogether(alias) === wanted);
|
|
300
|
+
}
|
|
@@ -45,3 +45,10 @@ export function indexRootSuggestion(symbol: string): string | null {
|
|
|
45
45
|
const root = indexRootFor(symbol);
|
|
46
46
|
return root ? `Did you mean ${root.symbol} (${root.name})?` : null;
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
/** Cboe's Treasury yield indices: 13-week bill, 5-, 10- and 30-year notes. Each level is a yield in percent. */
|
|
50
|
+
const YIELD_INDEX_SYMBOLS: ReadonlySet<string> = new Set(["^IRX", "^FVX", "^TNX", "^TYX"]);
|
|
51
|
+
|
|
52
|
+
export function isYieldIndexSymbol(symbol: string): boolean {
|
|
53
|
+
return YIELD_INDEX_SYMBOLS.has(symbol.trim().toUpperCase());
|
|
54
|
+
}
|
|
@@ -182,7 +182,7 @@ export function describeListingFate(
|
|
|
182
182
|
* ended long ago. Only such a line can be a company that moved away, so only
|
|
183
183
|
* its change lookup is waited for.
|
|
184
184
|
*/
|
|
185
|
-
function
|
|
185
|
+
export function lineMayHaveMoved(quote: Quote | null, now = Date.now()): boolean {
|
|
186
186
|
return !!quote && (isQuoteBeforeLatestSession(quote, now) || endedTradingDate(quote, now) != null);
|
|
187
187
|
}
|
|
188
188
|
|
|
@@ -206,7 +206,7 @@ export async function loadListingFate(
|
|
|
206
206
|
// A lookup started beside the command's request gets a short grace once that is ready, so a slow
|
|
207
207
|
// one never holds a symbol that trades. A line that stopped is waited for: only its company can
|
|
208
208
|
// have moved, and then the answer decides whether the command serves anything at all.
|
|
209
|
-
const changes = await lookup.settle(!options.changes ||
|
|
209
|
+
const changes = await lookup.settle(!options.changes || lineMayHaveMoved(quote) ? LATE_CHANGE_WAIT_MS : undefined);
|
|
210
210
|
return describeListingFate(listing, quote, changes);
|
|
211
211
|
} catch (error) {
|
|
212
212
|
logFailure("tickers-listing-fate", "Could not resolve listing fate")(error);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { canonicalExchange } from "../../utils/exchanges";
|
|
2
|
-
import { companyNameExtraWords,
|
|
1
|
+
import { canonicalExchange, isUsListingExchange } from "../../utils/exchanges";
|
|
2
|
+
import { companyLines, companyNameExtraWords, namedByService, type CompanyLineFacts } from "../company-name";
|
|
3
3
|
import { answersOnlyByKeyword, isQualifiedTickerQuery } from "./ranking";
|
|
4
4
|
import type { TickerSearchCandidate } from "./types";
|
|
5
5
|
|
|
@@ -29,6 +29,69 @@ export function lineDistance(type: string, exchange: string, symbol: string): 0
|
|
|
29
29
|
return SECONDARY_VENUES.has(exchange) || /\d/.test(symbol) && /[A-Za-z]/.test(symbol) ? 1 : 0;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/** How the primary-listing choice weighs one of a company's lines. */
|
|
33
|
+
export interface WeighedCompanyLine {
|
|
34
|
+
/** `listingDistance`: 0 its own line, 1 a line quoting shares listed elsewhere, 2 a receipt. */
|
|
35
|
+
distance: number;
|
|
36
|
+
/** The search score the service gives the listings people look up most; null when it gives none. */
|
|
37
|
+
score: number | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The line a company's search lists as its primary listing instead of `lead`,
|
|
42
|
+
* the line search put first, often the one-off cross-listing whose short name
|
|
43
|
+
* matched the query (LUFTHANSA on Budapest): its lines go by how far each is
|
|
44
|
+
* from being its own line, then by the search score. The lead gives way only
|
|
45
|
+
* to a closer line, or to one searched at least twice as often, or any scored
|
|
46
|
+
* line when it has no score. With `named` (the service marked the lead with
|
|
47
|
+
* the name as typed) no score outweighs the mark: only a closer line does.
|
|
48
|
+
* `lines` holds the lead. `search` and a company's name typed for a listing
|
|
49
|
+
* (`findListingOfCompanyName`) choose by this one rule, so both open the same
|
|
50
|
+
* listing.
|
|
51
|
+
*/
|
|
52
|
+
export function primaryCompanyLine<T>(
|
|
53
|
+
lead: T,
|
|
54
|
+
lines: readonly T[],
|
|
55
|
+
weigh: (line: T) => WeighedCompanyLine,
|
|
56
|
+
options: { named?: boolean } = {},
|
|
57
|
+
): T {
|
|
58
|
+
const order = (line: T) => lines.indexOf(line);
|
|
59
|
+
const [home] = [...lines].sort((a, b) => weigh(a).distance - weigh(b).distance
|
|
60
|
+
|| (weigh(b).score ?? -1) - (weigh(a).score ?? -1) || order(a) - order(b));
|
|
61
|
+
if (!home || home === lead) return lead;
|
|
62
|
+
const [best, first] = [weigh(home), weigh(lead)];
|
|
63
|
+
const closer = best.distance < first.distance;
|
|
64
|
+
const moreSearched = !options.named && best.distance === first.distance && best.score != null
|
|
65
|
+
&& (first.score == null || best.score >= first.score * 2);
|
|
66
|
+
return closer || moreSearched ? home : lead;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** A row as `companyLines` weighs it. */
|
|
70
|
+
function lineFacts(candidate: TickerSearchCandidate): CompanyLineFacts {
|
|
71
|
+
return {
|
|
72
|
+
name: candidateName(candidate),
|
|
73
|
+
exchange: candidateExchange(candidate),
|
|
74
|
+
receipt: listingDistance(candidate) === 2,
|
|
75
|
+
home: candidate.result?.homeListing === true,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The rows of `lead`'s company among `items` (`companyLines`): never a line of another business its name is in (Siemens Energy for Siemens). */
|
|
80
|
+
function companyRows<T extends TickerSearchCandidate>(lead: T, items: readonly T[]): T[] {
|
|
81
|
+
return companyLines(lead, items.filter((item) => item === lead || isListedEquity(item)), lineFacts);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** `primaryCompanyLine` among `items` of `start`'s company, for the command bar's rows. */
|
|
85
|
+
function primaryLineOf<T extends TickerSearchCandidate>(start: T, items: readonly T[], query: string): T {
|
|
86
|
+
const name = candidateName(start);
|
|
87
|
+
const named = !!start.result && namedByService(query, start.result);
|
|
88
|
+
const lines = companyRows(start, items).filter((item) => item === start
|
|
89
|
+
|| !named || companyNameExtraWords(candidateName(item), name) != null);
|
|
90
|
+
return primaryCompanyLine(start, lines, (item) => ({
|
|
91
|
+
distance: listingDistance(item), score: item.result?.popularity ?? item.popularity ?? null,
|
|
92
|
+
}), { named });
|
|
93
|
+
}
|
|
94
|
+
|
|
32
95
|
function candidateName(candidate: TickerSearchCandidate): string {
|
|
33
96
|
return candidate.result?.name || candidate.detail.split(" | ")[0] || "";
|
|
34
97
|
}
|
|
@@ -51,24 +114,46 @@ function isListedEquity(candidate: TickerSearchCandidate): boolean {
|
|
|
51
114
|
* service marks as the company's home (`homeListing`) takes the lead from
|
|
52
115
|
* the company's first row. Without that mark, a receipt in the lead gives
|
|
53
116
|
* way to the company's own shares on an exchange (`listingDistance` 0, not
|
|
54
|
-
* an OTC or regional line), the name nearest the receipt's first.
|
|
55
|
-
*
|
|
117
|
+
* an OTC or regional line), the name nearest the receipt's first. Then the
|
|
118
|
+
* line `search` calls the company's primary listing (`primaryCompanyLine`)
|
|
119
|
+
* takes the lead, so a name opens what `search` lists first: LHA on XETRA
|
|
120
|
+
* for "lufthansa", not LUFTHANSA on Budapest. A company's lines are never
|
|
121
|
+
* another business's its name is in (`companyLines`: Siemens Energy AG for
|
|
122
|
+
* Siemens AG). The row the service marks with the word as typed is the
|
|
123
|
+
* company the word means, ahead of a line abroad whose symbol is the word:
|
|
124
|
+
* "siemens" is SIE on XETRA, not SIEMENS on NSE (Siemens Ltd.); a US line
|
|
125
|
+
* keeps its ticker. A query that names a venue (LVMH:TSX, 7203.T) asked for
|
|
126
|
+
* that line, and a saved row keeps the lead.
|
|
56
127
|
*/
|
|
57
128
|
export function withHomeListingLead<T extends TickerSearchCandidate>(items: T[], query: string): T[] {
|
|
58
129
|
if (isQualifiedTickerQuery(query)) return items;
|
|
59
130
|
const lead = items.find((item) => item.instrumentClass !== "fund" && item.instrumentClass !== "derivative"
|
|
60
131
|
&& !answersOnlyByKeyword(item, query));
|
|
61
132
|
if (!lead || !isListedEquity(lead)) return items;
|
|
133
|
+
const typedSymbol = lead.symbol.toUpperCase() === query.trim().toUpperCase();
|
|
134
|
+
const meant = typedSymbol && !isUsListingExchange(candidateExchange(lead)) && !(lead.result && namedByService(query, lead.result))
|
|
135
|
+
// A company's shares under another symbol: an index the code also names (SPX) or the word's own line elsewhere (SAP) is not it.
|
|
136
|
+
? items.find((item) => item !== lead && isListedEquity(item) && item.instrumentClass === "equity" && !!item.result
|
|
137
|
+
&& item.symbol.toUpperCase() !== lead.symbol.toUpperCase() && namedByService(query, item.result))
|
|
138
|
+
: undefined;
|
|
139
|
+
if (meant) return moveAhead(items, meant, lead);
|
|
62
140
|
const name = candidateName(lead);
|
|
63
|
-
const lines = items.filter((item) => item !== lead
|
|
141
|
+
const lines = companyRows(lead, items).filter((item) => item !== lead);
|
|
64
142
|
const nearest = (candidates: T[]) => [...candidates].sort((a, b) => nameGap(name, candidateName(a)) - nameGap(name, candidateName(b))
|
|
65
143
|
|| items.indexOf(a) - items.indexOf(b))[0];
|
|
66
144
|
const marked = lines.filter((item) => item.result?.homeListing === true);
|
|
145
|
+
const own = listingDistance(lead) === 2 ? nearest(lines.filter((item) => listingDistance(item) === 0)) : undefined;
|
|
67
146
|
const home = lead.result?.homeListing === true ? undefined
|
|
68
147
|
: marked.length ? nearest(marked)
|
|
69
|
-
|
|
70
|
-
:
|
|
71
|
-
|
|
148
|
+
// A listing searched by its own symbol is that listing, as `search` keeps it.
|
|
149
|
+
: typedSymbol ? own
|
|
150
|
+
: primaryLineOf(own ?? lead, [lead, ...lines], query);
|
|
151
|
+
if (!home || home === lead) return items;
|
|
152
|
+
return moveAhead(items, home, lead);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** `items` with `home` moved just ahead of `lead`. */
|
|
156
|
+
function moveAhead<T>(items: readonly T[], home: T, lead: T): T[] {
|
|
72
157
|
const rest = items.filter((item) => item !== home);
|
|
73
158
|
const at = rest.indexOf(lead);
|
|
74
159
|
return [...rest.slice(0, at), home, ...rest.slice(at)];
|
|
@@ -7,6 +7,7 @@ import { getListingSymbol, listingSuffixConflictsWithExchange, tickerHasListingS
|
|
|
7
7
|
import { parseOptionSymbol } from "../../utils/options";
|
|
8
8
|
import { resolveCurrencyUnit } from "../../utils/currency-units";
|
|
9
9
|
import { currencyPairSpelling } from "../../utils/currency-pair";
|
|
10
|
+
import { typedCoinPairListing } from "../../utils/crypto-pair";
|
|
10
11
|
import { assetClassMarketSymbol, parseAssetClassQuery } from "./asset-classes";
|
|
11
12
|
import { bareFuturesCode, futuresContractByCode } from "../../market-data/futures-board";
|
|
12
13
|
import { searchContractKey, searchInstrumentKey } from "./identity";
|
|
@@ -53,7 +54,7 @@ export {
|
|
|
53
54
|
rankTickerSearchItems,
|
|
54
55
|
} from "./ranking";
|
|
55
56
|
export { upsertTickerFromSearchResult } from "./upsert";
|
|
56
|
-
export { candidateExchange, listingDistance } from "./home-listing";
|
|
57
|
+
export { candidateExchange, listingDistance, primaryCompanyLine } from "./home-listing";
|
|
57
58
|
|
|
58
59
|
const MAX_NAMED_LISTINGS = 6;
|
|
59
60
|
|
|
@@ -387,9 +388,12 @@ export function resolveTickerSearch(options: ResolveTickerSearchOptions): Promis
|
|
|
387
388
|
// JPY=X are EURUSD=X and USDJPY=X. USD-NGN and USDNGN, which a security could also be written as
|
|
388
389
|
// (MNT-USD is a coin), are the pair only when nothing is listed under those letters.
|
|
389
390
|
const pair = pickListing || future ? null : currencyPairSpelling(typed);
|
|
390
|
-
|
|
391
|
+
// A coin pair written as crypto exchanges write it (BTC/GBP, or BTC/GBP:KRAKEN) is the crypto venue's BTC-GBP,
|
|
392
|
+
// and what was typed when the venue has no such coin.
|
|
393
|
+
const coin = pickListing || future || pair || options.tickers.has(typed) ? null : typedCoinPairListing(typed);
|
|
394
|
+
const symbol = future?.symbol ?? coin?.symbol ?? (pair && !pair.ambiguous ? pair.symbol : typed);
|
|
391
395
|
const literal = resolveTickerSymbol(symbol, options, pickListing ? requested : null);
|
|
392
|
-
const alternatives = pickListing ? [] : [...dottedVenueQueries(symbol), ...pair?.ambiguous ? [pair.symbol] : []];
|
|
396
|
+
const alternatives = pickListing ? [] : [...dottedVenueQueries(symbol), ...pair?.ambiguous ? [pair.symbol] : [], ...coin ? [typed] : []];
|
|
393
397
|
// A listing suffix search does not carry (CEZ.PR) is read from its own quote, as `quote` reads it.
|
|
394
398
|
const quoted = !pickListing && tickerHasListingSuffix(symbol) && !parsePublicTickerKey(symbol).exchange;
|
|
395
399
|
// Most input has no venue code to retry: hand back the literal lookup
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
} from "./types";
|
|
5
5
|
import { getListingSymbol, tickerHasListingSuffix } from "../../sources/listing-symbols";
|
|
6
6
|
import { currencyPairCode } from "../../utils/currency-pair";
|
|
7
|
+
import { typedCoinPairListing } from "../../utils/crypto-pair";
|
|
7
8
|
import { canonicalExchange, exchangeCodeVenues, isUsListingExchange, parsePublicTickerKey, typedExchangeCode } from "../../utils/exchanges";
|
|
8
9
|
import { assetClassKeeps, assetClassMarketSymbol, instrumentClassCode, parseAssetClassQuery } from "./asset-classes";
|
|
9
10
|
import { futuresContractByCode } from "../../market-data/futures-board";
|
|
@@ -322,9 +323,12 @@ export function rankTickerSearchItems<T extends Pick<TickerSearchRankableItem, "
|
|
|
322
323
|
.map(({ item }) => getTickerSearchListingKey(item)),
|
|
323
324
|
);
|
|
324
325
|
|
|
326
|
+
// BTC/GBP reads as a currency pair's spelling, and is the crypto venue's BTC-GBP.
|
|
327
|
+
const coinListing = typedCoinPairListing(query)?.symbol;
|
|
325
328
|
const filtered = ranked.filter(({ item, textScore }) => {
|
|
326
329
|
if (textScore <= 0) return false;
|
|
327
|
-
if (isExplicitMarketSymbol(query) && !isExplicitMarketSymbol(item.symbol || item.label)
|
|
330
|
+
if (isExplicitMarketSymbol(query) && !isExplicitMarketSymbol(item.symbol || item.label)
|
|
331
|
+
&& parsePublicTickerKey(item.symbol || item.label).symbol !== coinListing) return false;
|
|
328
332
|
if (item.kind !== "search") return true;
|
|
329
333
|
return !matchedLocalListings.has(getTickerSearchListingKey(item));
|
|
330
334
|
});
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import { canonicalExchange, parsePublicTickerKey, publicTickerKey } from "../../utils/exchanges";
|
|
12
12
|
import { scopedBrokerContractIdentityKey } from "../../utils/instrument-identity";
|
|
13
13
|
import { listingSuffixVenue } from "../../sources/listing-symbols";
|
|
14
|
+
import { coinPairQuoteCurrency, exchangeLineCoinListing, isCoinRowType } from "../../utils/crypto-pair";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* The key a result is saved under when its symbol is already saved for
|
|
@@ -36,6 +37,18 @@ export async function upsertTickerFromSearchResult(
|
|
|
36
37
|
result: InstrumentSearchResult,
|
|
37
38
|
options: { tickerSymbol?: string } = {},
|
|
38
39
|
): Promise<{ ticker: TickerRecord; created: boolean }> {
|
|
40
|
+
// A crypto exchange's line of a coin pair (BTC/GBP on COINBASE PRO), which no source quotes, is saved as the
|
|
41
|
+
// crypto venue's listing of the pair (BTC-GBP on CCC), in the currency the pair is quoted in.
|
|
42
|
+
const coin = result.brokerContract ? null
|
|
43
|
+
: exchangeLineCoinListing(getSearchResultSymbol(result), listingVenue(result), { coinRow: isCoinRowType(result.type) });
|
|
44
|
+
if (coin) {
|
|
45
|
+
result = { ...result, symbol: coin.symbol, exchange: coin.exchange, primaryExchange: undefined, currency: coin.currency };
|
|
46
|
+
if (options.tickerSymbol) options = { ...options, tickerSymbol: publicTickerKey(coin.symbol, coin.exchange) };
|
|
47
|
+
} else if (!result.currency && !result.brokerContract && canonicalExchange(listingVenue(result)) === "CCC") {
|
|
48
|
+
// The venue's own row of a pair (BTC-GBP) often comes without its currency, which the pair names.
|
|
49
|
+
const currency = coinPairQuoteCurrency(getSearchResultSymbol(result));
|
|
50
|
+
if (currency) result = { ...result, currency };
|
|
51
|
+
}
|
|
39
52
|
let symbol = options.tickerSymbol ?? getSearchResultSymbol(result);
|
|
40
53
|
// A vendor's suffixed symbol (GARAN.IS) is saved as the listing it is (GARAN on BIST), under the key
|
|
41
54
|
// GARAN:BIST is saved under, so one listing is one row. A key the caller names is kept as given.
|