gloomberb 0.17.2 → 0.17.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api-client/futures-curve.ts +16 -0
- package/src/api-client/index.ts +18 -8
- package/src/api-client/types.ts +3 -1
- package/src/cli/commands/market.ts +60 -9
- package/src/cli/commands/search.ts +43 -2
- package/src/cli/commands/ticker.ts +100 -21
- package/src/cli/desktop-pane-shot.ts +34 -9
- package/src/cli/fx-pair.ts +17 -0
- package/src/cli/glossary.ts +134 -6
- package/src/cli/history-rows.ts +16 -4
- package/src/cli/index.ts +44 -12
- package/src/cli/known-gaps.ts +21 -2
- package/src/cli/listing-arg.ts +19 -4
- package/src/cli/listing-suggestion.ts +33 -2
- package/src/cli/pane-functions/dom.ts +39 -6
- package/src/cli/pane-functions/freshness.ts +48 -5
- package/src/cli/pane-functions/function-help.ts +1 -1
- package/src/cli/pane-functions/headless.ts +85 -19
- package/src/cli/pane-functions/index.ts +60 -4
- package/src/cli/pane-functions/market-session.ts +69 -6
- package/src/cli/pane-functions/resolver.ts +4 -2
- package/src/cli/pane-functions/screenshot.ts +14 -1
- package/src/cli/scroll-truncation.ts +5 -1
- package/src/cli/symbol-args.ts +41 -3
- package/src/cli/symbol-list.ts +19 -7
- package/src/cli/unknown-command.ts +70 -0
- package/src/components/chart/native/surface/visibility.ts +37 -7
- package/src/components/layout/header.tsx +348 -102
- package/src/components/markdown-text.tsx +40 -66
- package/src/components/market-board/index.tsx +12 -6
- package/src/components/reader/detail-reader.tsx +28 -6
- package/src/components/ticker/badge/index.tsx +2 -0
- package/src/components/ui/display.tsx +6 -1
- package/src/components/ui/external-link.tsx +10 -2
- package/src/data/memory-resource-store.ts +1 -1
- package/src/data/resource-store.ts +13 -1
- package/src/docs/snapshot.json +13 -13
- package/src/i18n/es.ts +1 -0
- package/src/i18n/ja.ts +1 -0
- package/src/i18n/ko.ts +1 -0
- package/src/i18n/zh-cn.ts +1 -0
- package/src/i18n/zh-tw.ts +1 -0
- package/src/market-data/futures-contract.ts +10 -0
- package/src/market-data/market/format.ts +7 -2
- package/src/market-data/market/freshness.ts +25 -1
- package/src/market-data/quotes/regular-session-valuation.ts +32 -1
- package/src/market-data/quotes/resolution.ts +2 -0
- package/src/plugins/builtin/account-management/pane.tsx +17 -13
- package/src/plugins/builtin/attention/headless.ts +19 -10
- package/src/plugins/builtin/attention/model.ts +22 -0
- package/src/plugins/builtin/bond-calculator/pane.tsx +2 -1
- package/src/plugins/builtin/changelog/index.tsx +11 -8
- package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +2 -0
- package/src/plugins/builtin/chat/attachments/desktop.tsx +2 -1
- package/src/plugins/builtin/chat/attachments/model.ts +5 -3
- package/src/plugins/builtin/chat/content/footer.tsx +6 -0
- package/src/plugins/builtin/chat/content/open-targets.ts +13 -4
- package/src/plugins/builtin/chat/content/transcript.tsx +2 -0
- package/src/plugins/builtin/chat/content/whole-message-copy.ts +70 -0
- package/src/plugins/builtin/chat/layout.ts +91 -31
- package/src/plugins/builtin/chat/markdown.ts +94 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -2
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +197 -80
- package/src/plugins/builtin/chat/message/render-state.ts +3 -0
- package/src/plugins/builtin/chat/message/terminal.tsx +4 -4
- package/src/plugins/builtin/cloud/thesis/exposure.ts +18 -2
- package/src/plugins/builtin/company-attention/headless.ts +8 -5
- package/src/plugins/builtin/company-attention/model.ts +53 -2
- package/src/plugins/builtin/company-attention/pane.tsx +1 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +7 -4
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -1
- package/src/plugins/builtin/correlation/relationship/pane.tsx +3 -7
- package/src/plugins/builtin/cot/client.ts +12 -4
- package/src/plugins/builtin/cot/pane.tsx +2 -2
- package/src/plugins/builtin/crypto-board/model.ts +7 -3
- package/src/plugins/builtin/debt-maturities/pane.tsx +1 -6
- package/src/plugins/builtin/debug/index.tsx +15 -15
- package/src/plugins/builtin/dividend-yield/client.ts +8 -1
- package/src/plugins/builtin/dividend-yield/headless.ts +4 -1
- package/src/plugins/builtin/dividend-yield/pane.tsx +6 -5
- package/src/plugins/builtin/docs/index.tsx +5 -3
- package/src/plugins/builtin/docs/pane.tsx +18 -1
- package/src/plugins/builtin/docs/targets.ts +5 -3
- package/src/plugins/builtin/earnings-calls/data.ts +22 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +24 -14
- package/src/plugins/builtin/equity-screener/headless.ts +4 -3
- package/src/plugins/builtin/equity-screener/live.ts +27 -1
- package/src/plugins/builtin/equity-screener/pane.tsx +21 -2
- package/src/plugins/builtin/fear-greed/charts.tsx +5 -1
- package/src/plugins/builtin/fear-greed/pane.tsx +1 -1
- package/src/plugins/builtin/futures/index.tsx +32 -4
- package/src/plugins/builtin/futures/model.ts +65 -3
- package/src/plugins/builtin/futures/table.tsx +31 -11
- package/src/plugins/builtin/futures/use-board-terms.ts +89 -0
- package/src/plugins/builtin/holders/pane.tsx +6 -1
- package/src/plugins/builtin/mna/client.ts +12 -2
- package/src/plugins/builtin/mna/detail.tsx +2 -2
- package/src/plugins/builtin/mna/pane.tsx +2 -2
- package/src/plugins/builtin/notes/owner.tsx +5 -4
- package/src/plugins/builtin/notes/ticker-notes-tab.tsx +2 -3
- package/src/plugins/builtin/options-calculator/pane.tsx +2 -3
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -3
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +9 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +6 -3
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +2 -9
- package/src/plugins/builtin/research/event-model.ts +14 -0
- package/src/plugins/builtin/research/headless-schema.ts +31 -1
- package/src/plugins/builtin/research/relative-valuation-columns.ts +289 -0
- package/src/plugins/builtin/research/relative-valuation-currency.ts +164 -0
- package/src/plugins/builtin/research/relative-valuation-headless.ts +94 -11
- package/src/plugins/builtin/research/relative-valuation-model.ts +4 -1
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +13 -4
- package/src/plugins/builtin/research/relative-valuation-peers.ts +302 -0
- package/src/plugins/builtin/research-search/data.ts +11 -0
- package/src/plugins/builtin/research-search/pane.tsx +13 -2
- package/src/plugins/builtin/scanner/hilo-pane.tsx +7 -17
- package/src/plugins/builtin/seasonality/client.ts +23 -0
- package/src/plugins/builtin/seasonality/pane.tsx +4 -2
- package/src/plugins/builtin/sectors/index.tsx +7 -3
- package/src/plugins/builtin/shared/report-freshness.ts +6 -2
- package/src/plugins/builtin/short-interest/headless.ts +13 -3
- package/src/plugins/builtin/short-interest/index.tsx +1 -1
- package/src/plugins/builtin/short-interest/model.ts +62 -3
- package/src/plugins/builtin/short-interest/pane.tsx +2 -2
- package/src/plugins/builtin/short-interest/schedule.ts +107 -0
- package/src/plugins/builtin/short-interest/watch-client.ts +54 -8
- package/src/plugins/builtin/short-interest/watch-headless.ts +8 -3
- package/src/plugins/builtin/short-interest/watch-model.ts +52 -23
- package/src/plugins/builtin/short-interest/watch-pane.tsx +1 -1
- package/src/plugins/builtin/supply-chain/flow.tsx +14 -4
- package/src/plugins/builtin/themes/headless.ts +1 -1
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +23 -27
- package/src/plugins/builtin/ticker-detail/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/overview/model.ts +4 -3
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +4 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +9 -4
- package/src/plugins/builtin/vol-surface/pane.tsx +16 -1
- package/src/plugins/builtin/volatility/chart-model.ts +1 -1
- package/src/plugins/builtin/volatility/charts.tsx +1 -1
- package/src/renderers/dom/styles.css +6 -0
- package/src/sources/gloomberb-cloud/normalizers.ts +1 -0
- package/src/sources/provider-router/batches.ts +2 -1
- package/src/sources/provider-router/cache.ts +3 -0
- package/src/sync/baseline.ts +19 -14
- package/src/sync/controller.ts +140 -40
- package/src/sync/core-contributors.ts +256 -32
- package/src/sync/types.ts +44 -0
- package/src/time-series/resolve.ts +19 -2
- package/src/types/data-provider.ts +8 -0
- package/src/types/financials.ts +40 -4
- package/src/types/headless.ts +43 -0
- package/src/types/plugin.ts +7 -0
- package/src/utils/declared-dividends.ts +39 -0
- package/src/utils/forward-dividend.ts +34 -11
- package/src/utils/fundamentals.ts +13 -2
- package/src/utils/markdown.ts +180 -0
- package/src/utils/market-capitalization.ts +22 -0
|
@@ -60,9 +60,12 @@ export interface DesktopPaneShotPayload {
|
|
|
60
60
|
statusLine?: boolean;
|
|
61
61
|
/**
|
|
62
62
|
* The tallest the capture may grow, in cells, to show every row its tables
|
|
63
|
-
* scroll past
|
|
63
|
+
* scroll past or all the text its reader scrolls past; absent keeps the
|
|
64
|
+
* requested height (`--height`, most panes).
|
|
64
65
|
*/
|
|
65
66
|
maxHeightCells?: number;
|
|
67
|
+
/** The `data-gloom-role`s of the scrolling boxes the capture grows to fit; the tables' bodies when absent. */
|
|
68
|
+
fitRoles?: string[];
|
|
66
69
|
tickers: TickerRecord[];
|
|
67
70
|
/** Broker accounts as last synced, by profile, so a broker portfolio shows its equity and cash. */
|
|
68
71
|
brokerAccounts?: Record<string, BrokerAccount[]>;
|
|
@@ -127,6 +130,12 @@ interface DesktopPaneShotSurface {
|
|
|
127
130
|
|
|
128
131
|
export interface DesktopPaneShotRenderResult {
|
|
129
132
|
visibleText: string;
|
|
133
|
+
/**
|
|
134
|
+
* The visible text a line per block of the page, as the browser lays it out,
|
|
135
|
+
* where `visibleText` has run them together: the words of a pane that is prose
|
|
136
|
+
* and not a table, to be read as paragraphs. Absent in a capture that did not read it.
|
|
137
|
+
*/
|
|
138
|
+
visibleLines?: string[];
|
|
130
139
|
/**
|
|
131
140
|
* The rows a report reads, when a table reports every row (an option chain's
|
|
132
141
|
* strike window) rather than those in the viewport; `rows` stays what the
|
|
@@ -222,7 +231,13 @@ export async function renderDesktopPaneScreenshot(
|
|
|
222
231
|
userDataDir: join(tempDir, "chrome-profile"),
|
|
223
232
|
captureImage: options.captureImage !== false,
|
|
224
233
|
statusLine: payload.statusLine ? options.statusLine : undefined,
|
|
225
|
-
...(payload.maxHeightCells ? {
|
|
234
|
+
...(payload.maxHeightCells ? {
|
|
235
|
+
fitRows: {
|
|
236
|
+
heightCells: payload.heightCells,
|
|
237
|
+
maxHeightCells: payload.maxHeightCells,
|
|
238
|
+
roles: payload.fitRoles ?? ["data-table-body-scroll"],
|
|
239
|
+
},
|
|
240
|
+
} : {}),
|
|
226
241
|
});
|
|
227
242
|
} finally {
|
|
228
243
|
server?.stop(true);
|
|
@@ -458,7 +473,7 @@ async function capturePageScreenshot({
|
|
|
458
473
|
userDataDir: string;
|
|
459
474
|
captureImage: boolean;
|
|
460
475
|
statusLine?: ShotStatusLine;
|
|
461
|
-
fitRows?: { heightCells: number; maxHeightCells: number };
|
|
476
|
+
fitRows?: { heightCells: number; maxHeightCells: number; roles: readonly string[] };
|
|
462
477
|
}): Promise<DesktopPaneShotRenderResult> {
|
|
463
478
|
await mkdir(userDataDir, { recursive: true });
|
|
464
479
|
const port = 43000 + Math.floor(Math.random() * 10000);
|
|
@@ -531,9 +546,10 @@ async function capturePageScreenshot({
|
|
|
531
546
|
}
|
|
532
547
|
|
|
533
548
|
/**
|
|
534
|
-
* Grows the viewport and the pane by the rows their tables scroll past,
|
|
535
|
-
*
|
|
536
|
-
* default capture would cut (FUT's last
|
|
549
|
+
* Grows the viewport and the pane by the rows their tables scroll past, or the
|
|
550
|
+
* text a reader's body scrolls past, up to `maxHeightCells`, then waits for the
|
|
551
|
+
* taller pane to settle: a board a default capture would cut (FUT's last
|
|
552
|
+
* sectors) shows every row, and a docs page its whole text.
|
|
537
553
|
*/
|
|
538
554
|
async function growToFitRows(session: CdpSession, size: {
|
|
539
555
|
widthPx: number;
|
|
@@ -541,9 +557,11 @@ async function growToFitRows(session: CdpSession, size: {
|
|
|
541
557
|
deviceScaleFactor: number;
|
|
542
558
|
heightCells: number;
|
|
543
559
|
maxHeightCells: number;
|
|
560
|
+
roles: readonly string[];
|
|
544
561
|
}): Promise<void> {
|
|
562
|
+
const selector = size.roles.map((role) => `[data-gloom-role="${role}"]`).join(", ");
|
|
545
563
|
const result = await session.send("Runtime.evaluate", {
|
|
546
|
-
expression: `Math.max(0, ...[...document.querySelectorAll(
|
|
564
|
+
expression: `Math.max(0, ...[...document.querySelectorAll(${JSON.stringify(selector)})]
|
|
547
565
|
.map((element) => element.scrollHeight - element.clientHeight))`,
|
|
548
566
|
returnByValue: true,
|
|
549
567
|
}) as { result?: { value?: unknown } };
|
|
@@ -646,6 +664,7 @@ async function readRenderedPaneState(session: CdpSession): Promise<DesktopPaneSh
|
|
|
646
664
|
const table = element.closest('[data-gloom-role="data-table"]');
|
|
647
665
|
return !!table && reportedTables.has(table);
|
|
648
666
|
};
|
|
667
|
+
const managesRows = (element) => element.getAttribute("data-gloom-role") === "data-table-body-scroll";
|
|
649
668
|
const markScrollTruncation = (element) => {
|
|
650
669
|
if (!(element instanceof HTMLElement) || !isVisible(element)) return;
|
|
651
670
|
const reasons = scrollReasons({
|
|
@@ -655,8 +674,10 @@ async function readRenderedPaneState(session: CdpSession): Promise<DesktopPaneSh
|
|
|
655
674
|
scrollLeft: element.scrollLeft,
|
|
656
675
|
scrollWidth: element.scrollWidth,
|
|
657
676
|
clientWidth: element.clientWidth,
|
|
658
|
-
|
|
659
|
-
|
|
677
|
+
// A scroll box draws its scrollbar by default and its content is all in the page, so its
|
|
678
|
+
// measures say what is cut; only a table's body holds rows it has not drawn.
|
|
679
|
+
scrollbarY: managesRows(element) && element.getAttribute("data-gloom-scrollbar-y") === "visible",
|
|
680
|
+
scrollbarX: managesRows(element) && element.getAttribute("data-gloom-scrollbar-x") === "visible",
|
|
660
681
|
});
|
|
661
682
|
reasons.forEach((reason) => truncationReasons.add(reason));
|
|
662
683
|
if (!inReportedTable(element)) reasons.forEach((reason) => reportTruncationReasons.add(reason));
|
|
@@ -810,8 +831,12 @@ async function readRenderedPaneState(session: CdpSession): Promise<DesktopPaneSh
|
|
|
810
831
|
if (textShowsEllipsis && !truncationReasons.includes("one or more cells are visibly clipped")) {
|
|
811
832
|
truncationReasons.push("one or more cells are visibly clipped");
|
|
812
833
|
}
|
|
834
|
+
const visibleLines = (value?.visibleText ?? "").split("\n")
|
|
835
|
+
.map((line) => line.replace(/\u00a0/g, " ").trimEnd())
|
|
836
|
+
.filter((line) => line.trim().length > 0);
|
|
813
837
|
return {
|
|
814
838
|
visibleText,
|
|
839
|
+
visibleLines,
|
|
815
840
|
footerText: typeof value?.footerText === "string" ? value.footerText : "",
|
|
816
841
|
visibleKeyValues: Array.isArray(value?.visibleKeyValues) ? value.visibleKeyValues : [],
|
|
817
842
|
...(Array.isArray(value?.surfaces) && value.surfaces.length > 0 ? { surfaces: value.surfaces } : {}),
|
package/src/cli/fx-pair.ts
CHANGED
|
@@ -13,11 +13,28 @@ export interface FxRequest {
|
|
|
13
13
|
|
|
14
14
|
const CURRENCY_CODE = /^[A-Z]{3}$/;
|
|
15
15
|
|
|
16
|
+
let knownCurrencies: ReadonlySet<string> | undefined;
|
|
17
|
+
|
|
18
|
+
/** An ISO 4217 code the runtime lists, or the offshore yuan it has none for. */
|
|
19
|
+
function isKnownCurrency(code: string): boolean {
|
|
20
|
+
if (!knownCurrencies) {
|
|
21
|
+
try {
|
|
22
|
+
knownCurrencies = new Set([...Intl.supportedValuesOf("currency"), "CNH"]);
|
|
23
|
+
} catch {
|
|
24
|
+
knownCurrencies = new Set();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return knownCurrencies.has(code);
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
/** The request, or the sentence saying what is wrong with the argument. */
|
|
17
31
|
export function parseFxRequest(raw: string, baseCurrency: string): FxRequest | { error: string } {
|
|
18
32
|
const text = raw.trim().toUpperCase();
|
|
19
33
|
const parts = text.split("/").map((part) => part.trim());
|
|
20
34
|
if (parts.length > 2 || parts.some((part) => !part)) return { error: `"${raw}" is not a currency or a pair such as USD/NGN.` };
|
|
35
|
+
// EURUSD is the pair typed without its slash: say how to write it.
|
|
36
|
+
const joined = parts.length === 1 && /^[A-Z]{6}$/.test(text) && isKnownCurrency(text.slice(0, 3)) && isKnownCurrency(text.slice(3));
|
|
37
|
+
if (joined) return { error: `${text} is not a currency code; write the pair as ${text.slice(0, 3)}/${text.slice(3)}.` };
|
|
21
38
|
const bad = parts.filter((part) => !CURRENCY_CODE.test(part));
|
|
22
39
|
if (bad.length > 0) return { error: `${bad.join(" and ")} ${bad.length > 1 ? "are not currency codes" : "is not a currency code"}; use three letters such as NGN.` };
|
|
23
40
|
return parts.length === 2
|
package/src/cli/glossary.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { cliStyles, cliTerminalWidth, renderDefinitions, renderSection, wrapText } from "../utils/cli-output";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Plain-words meanings of the rates, auction, energy, perpetual-futures, portfolio
|
|
5
|
-
*
|
|
4
|
+
* Plain-words meanings of the rates, auction, energy, perpetual-futures, portfolio,
|
|
5
|
+
* fund, short interest, quote-board, futures and economic-calendar terms the functions
|
|
6
|
+
* show in their rows, headers and footers.
|
|
6
7
|
* `catalog glossary` lists them, `catalog explain <term>` prints one, and
|
|
7
8
|
* `fn <function> --explain` and `portfolio show --explain` follow a report with
|
|
8
9
|
* the ones it shows. Only terms a pane or report actually prints belong here.
|
|
@@ -286,8 +287,11 @@ export const GLOSSARY: readonly GlossaryEntry[] = [
|
|
|
286
287
|
definition: "EU Allowance: the right to emit one tonne of CO2 under the EU Emissions Trading System, priced in euros per tonne.",
|
|
287
288
|
},
|
|
288
289
|
{
|
|
289
|
-
term: "Front month", aliases: ["front-month", "front contract", "continuous contract", "front-month contract"
|
|
290
|
-
|
|
290
|
+
term: "Front month", aliases: ["front-month", "front contract", "continuous contract", "front-month contract", "most active", "most active contract"],
|
|
291
|
+
functions: ["FUT", "CTM"], shown: ["most active"],
|
|
292
|
+
definition: "The nearest contract still trading, which is the one about to expire. Rolls follow volume: a continuous symbol (CL=F), "
|
|
293
|
+
+ "and FUT with it, moves to the next contract when that one becomes the most active (it holds more open interest, or trades more "
|
|
294
|
+
+ "where none is reported), usually days before expiry, and until then FUT names the nearer contract and its expiry.",
|
|
291
295
|
},
|
|
292
296
|
// Perpetual futures (PERP).
|
|
293
297
|
{
|
|
@@ -370,6 +374,122 @@ export const GLOSSARY: readonly GlossaryEntry[] = [
|
|
|
370
374
|
definition: "How loosely a fund follows its index: how much the gap between the fund's return and the index's swings, usually over a year. "
|
|
371
375
|
+ "Fees, cash and holdings that differ from the index widen it.",
|
|
372
376
|
},
|
|
377
|
+
// Short interest (SI, SIW) and daily short volume (SIV).
|
|
378
|
+
{
|
|
379
|
+
term: "Short interest", short: "SI", aliases: ["shares short", "short position", "short positions", "short shares"],
|
|
380
|
+
functions: ["SI", "SIW"], shown: ["Shares Short", "Short interest"],
|
|
381
|
+
definition: "The number of shares sold short and not yet bought back, counted across US broker-dealers on each settlement date. "
|
|
382
|
+
+ "It is a position, not trading volume, and it is published seven business days after the date it counts.",
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
term: "Settlement", aliases: ["settlement date", "settlement dates", "settled", "reporting settlement date"],
|
|
386
|
+
functions: ["SI", "SIW"], shown: ["Date", "Settled", "settlement"],
|
|
387
|
+
definition: "The date short positions are counted, twice a month: the 15th (the business day before when it is not one) and the month's last business day. "
|
|
388
|
+
+ "FINRA publishes each count seven business days later, and the report's source line gives that date and the next settlement's.",
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
term: "Days to cover", aliases: ["short ratio", "short interest ratio", "days-to-cover", "dtc"], functions: ["SI", "SIW"],
|
|
392
|
+
shown: ["Days to cover"],
|
|
393
|
+
definition: "Shares short divided by the average daily volume FINRA reports with the settlement: how many days of normal trading it would take "
|
|
394
|
+
+ "the shorts to buy their shares back. The overview, fundamentals, SI, SIW and EQS all give FINRA's figure. "
|
|
395
|
+
+ "A higher number means a squeeze would take longer to unwind.",
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
term: "Average daily volume", short: "ADV", aliases: ["avg daily vol", "avg daily volume", "average daily share volume", "avg volume"], functions: ["SI"],
|
|
399
|
+
shown: ["Avg Daily Vol"],
|
|
400
|
+
definition: "The shares traded per day on average, as FINRA reports it with each settlement. Days to cover divides shares short by it.",
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
term: "Float", aliases: ["free float", "public float", "float shares", "floating stock", "% float", "percent of float"],
|
|
404
|
+
functions: ["SI", "SIW"], shown: ["Float"],
|
|
405
|
+
definition: "The shares open to public trading: those outstanding less shares held by insiders and strategic holders, never more than the shares outstanding. "
|
|
406
|
+
+ "% Float is shares short over today's float, so a settlement is measured against the float as it is now, not as it was then; "
|
|
407
|
+
+ "SI gives it for the latest settlement only.",
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
// SIW's Setup column prints these two words.
|
|
411
|
+
term: "Crowded", aliases: ["crowded short", "crowded shorts", "crowded trade", "crowded, rising", "crowded rising", "setup"], functions: ["SIW"],
|
|
412
|
+
shown: ["Crowded", "Crowded, rising"],
|
|
413
|
+
definition: "SIW's flag for a heavily shorted name: at least 10% of the float short, or five or more days to cover with at least 5% of the float short "
|
|
414
|
+
+ "(days to cover alone when the float is unknown). Crowded, rising adds a price up over the month. A fixed threshold, not a squeeze forecast.",
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
term: "Squeeze", aliases: ["short squeeze", "squeezes", "short covering"], functions: ["SIW"], shown: ["Short squeeze watch"],
|
|
418
|
+
definition: "A price rise that forces short sellers to buy shares back to limit their losses, which lifts the price further. "
|
|
419
|
+
+ "It needs a crowded short and little trading to absorb the buying; SIW lists the names where that could happen and does not predict one.",
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
term: "SI change", aliases: ["si chg", "short interest change"], functions: ["SIW"], shown: ["SI Chg %"],
|
|
423
|
+
definition: "The change in shares short from the settlement before, in percent. A split between the two settlements is not adjusted.",
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
term: "1M return", aliases: ["1m", "one month return", "month move"], functions: ["SIW"], shown: ["1M %"],
|
|
427
|
+
definition: "The price change over one calendar month, from the latest daily close to the last close on or before the same day a month earlier.",
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
term: "Short volume", aliases: ["daily short volume", "short sale volume", "exempt volume"], functions: ["SIV"],
|
|
431
|
+
shown: ["Short volume", "Short %", "Exempt volume"],
|
|
432
|
+
definition: "Shares sold short in a day's off-exchange trading, which FINRA publishes daily; Short % is its share of the day's volume there. "
|
|
433
|
+
+ "It is a flow of trades, not the open position short interest counts, and it includes exempt volume.",
|
|
434
|
+
},
|
|
435
|
+
// Quote boards (QQ). The words a beginner meets in a quote's rows and its status line.
|
|
436
|
+
{
|
|
437
|
+
term: "Ticker", aliases: ["tickers", "ticker symbol", "symbol", "stock symbol"], functions: ["QQ"], shown: ["Ticker", "Symbol"],
|
|
438
|
+
definition: "The short code a security trades under, such as AAPL for Apple or ^GSPC for the S&P 500. Type it after a function: fn QQ AAPL.",
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
term: "Pre-market", aliases: ["pre-mkt", "pre-market session", "pre-market trading", "pre-market price"], functions: ["QQ"],
|
|
442
|
+
shown: ["Pre-Market", "pre-market", "PRE-MKT"],
|
|
443
|
+
definition: "Trading in a US stock before the regular session opens, from 4:00 to 9:30 am New York time, when fewer trades happen and prices can jump. "
|
|
444
|
+
+ "An index does not trade then: its pre-market status means it has not opened, and its level is still the last close.",
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
term: "After hours", aliases: ["after-hrs", "post-market", "extended hours", "after-hours trading"], functions: ["QQ"],
|
|
448
|
+
shown: ["After Hours", "after hours", "AFTER-HRS"],
|
|
449
|
+
definition: "Trading in a US stock after the regular session closes, from 4:00 to 8:00 pm New York time. "
|
|
450
|
+
+ "Like the pre-market it is thin, so a price there can move more than the regular close suggests.",
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
term: "Regular close", aliases: ["close", "closing price", "last close", "prior close", "regular session", "regular session close"],
|
|
454
|
+
functions: ["QQ"], shown: ["close", "Close"],
|
|
455
|
+
definition: "The last price of the regular session, 9:30 am to 4:00 pm New York time for US stocks. "
|
|
456
|
+
+ "From the close until the next open a quote's Last and change stay at it, and pre-market and after-hours prices are shown beside it.",
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
term: "Delayed", aliases: ["delayed quote", "delayed quotes", "delay", "15 min delayed", "live", "real-time"], functions: ["QQ", "FUT"],
|
|
460
|
+
shown: ["delayed", "Delayed"],
|
|
461
|
+
definition: "A price that reaches you some minutes after it traded; the status line gives the lag, such as 15 min delayed. A live (real-time) price has no lag.",
|
|
462
|
+
},
|
|
463
|
+
// Futures (FUT, CTM).
|
|
464
|
+
{
|
|
465
|
+
term: "Futures contract", aliases: ["futures", "future", "futures contracts", "contract", "contract month", "expiry", "expiration"],
|
|
466
|
+
functions: ["FUT", "CTM"], shown: ["CONTRACT", "Contract"],
|
|
467
|
+
definition: "An agreement to buy or sell something, such as oil, gold or the S&P 500, at a set price on a set date. Each contract has a month (Dec 26) and expires, "
|
|
468
|
+
+ "and most traders close out before then. A contract's own Settlement line says how it ends: in cash or by physical delivery.",
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
term: "Settle (futures)", aliases: ["settle", "settle price", "settlement price", "futures settle", "daily settlement price"],
|
|
472
|
+
functions: ["FUT", "CTM"], shown: ["Settle", "PREV"],
|
|
473
|
+
definition: "A futures contract's official price for the day, set by its exchange from trading near the close. "
|
|
474
|
+
+ "The day's change and margin are measured from it, and it can differ from the last trade. FUT's PREV is the previous one.",
|
|
475
|
+
},
|
|
476
|
+
// The economic calendar (ECO).
|
|
477
|
+
{
|
|
478
|
+
term: "Actual", aliases: ["actual release", "actual value"], functions: ["ECO"], shown: ["ACTUAL", "Actual"],
|
|
479
|
+
definition: "The figure a release came in at, filled in once it is published. A dash means it is still to come.",
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
term: "Forecast", aliases: ["forecasts", "consensus", "expected"], functions: ["ECO"], shown: ["FORECAST", "Forecast"],
|
|
483
|
+
definition: "What economists expected the release to show, the consensus before it comes out. Markets move on the gap between Actual and Forecast more than on the number alone.",
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
term: "Prior", aliases: ["previous", "prior value", "previous release"], functions: ["ECO"], shown: ["PRIOR", "Prior"],
|
|
487
|
+
definition: "The previous release's figure, the one Actual and Forecast are compared with.",
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
term: "Impact", short: "IMP", aliases: ["importance", "event impact", "high impact"], functions: ["ECO"],
|
|
491
|
+
definition: "How much a release usually moves markets: HIGH for the big ones (CPI, jobs, rate decisions), MED and LOW for speeches and smaller data.",
|
|
492
|
+
},
|
|
373
493
|
];
|
|
374
494
|
|
|
375
495
|
const ANSI = /\x1b\[[0-9;]*m/g;
|
|
@@ -390,7 +510,10 @@ function names(entry: GlossaryEntry): string[] {
|
|
|
390
510
|
export interface GlossaryLookup {
|
|
391
511
|
/** The entries the query names: by the term, its abbreviation or an alias. */
|
|
392
512
|
exact: GlossaryEntry[];
|
|
393
|
-
/**
|
|
513
|
+
/**
|
|
514
|
+
* The others whose names start a word with each word of the query: `stop-out` finds stop-out vs average.
|
|
515
|
+
* Those that hold the words whole come first.
|
|
516
|
+
*/
|
|
394
517
|
partial: GlossaryEntry[];
|
|
395
518
|
}
|
|
396
519
|
|
|
@@ -407,7 +530,12 @@ export function lookupGlossary(query: string): GlossaryLookup {
|
|
|
407
530
|
const have = names(entry).flatMap(words);
|
|
408
531
|
return wanted.every((word) => have.some((name) => name.startsWith(word)));
|
|
409
532
|
});
|
|
410
|
-
|
|
533
|
+
// The entries that hold the words whole come before those that only start with them: `float` is the float, not the floating rate note.
|
|
534
|
+
const whole = (entry: GlossaryEntry) => {
|
|
535
|
+
const have = names(entry).flatMap(words);
|
|
536
|
+
return wanted.every((word) => have.includes(word));
|
|
537
|
+
};
|
|
538
|
+
return { exact, partial: partial.sort((left, right) => Number(whole(right)) - Number(whole(left))) };
|
|
411
539
|
}
|
|
412
540
|
|
|
413
541
|
function escapeRegExp(text: string): string {
|
package/src/cli/history-rows.ts
CHANGED
|
@@ -67,6 +67,8 @@ export interface HistoryRow {
|
|
|
67
67
|
partial?: true;
|
|
68
68
|
/** The bar's prices were rebuilt from hourly bars where the source left the day empty. */
|
|
69
69
|
filled?: true;
|
|
70
|
+
/** A future's session has closed but its settlement is not confirmed yet: the close is the last trade. */
|
|
71
|
+
unsettled?: true;
|
|
70
72
|
/** What the volume counts, when the history says: a coin's is traded value in its quote currency (`USD`). */
|
|
71
73
|
volumeUnit?: string;
|
|
72
74
|
/** Why a coin's bar has no volume: `day in progress`, `rebuilt from hourly bars`, `not reported`. */
|
|
@@ -216,6 +218,7 @@ export function historyRows(
|
|
|
216
218
|
flag: flags.length > 0 ? flags.join(",") : null,
|
|
217
219
|
...(point.partial ? { partial: true as const } : {}),
|
|
218
220
|
...(point.filled ? { filled: true as const } : {}),
|
|
221
|
+
...(point.unsettled ? { unsettled: true as const } : {}),
|
|
219
222
|
...(volume ? { volumeUnit: volume.unit } : {}),
|
|
220
223
|
// A coin trades every day, so a bar without volume says why.
|
|
221
224
|
...(volume && values.volume == null
|
|
@@ -255,13 +258,18 @@ function listDates(dates: readonly string[]): string {
|
|
|
255
258
|
}
|
|
256
259
|
|
|
257
260
|
/**
|
|
258
|
-
* What the partial and
|
|
259
|
-
* (partial), with no volume until the day closes",
|
|
260
|
-
* hourly bars
|
|
261
|
+
* What the partial, filled and unsettled bars are: "2026-10-12 is still
|
|
262
|
+
* forming (partial), with no volume until the day closes", the bars rebuilt
|
|
263
|
+
* from hourly bars, and a future's closed session that "closes on its last
|
|
264
|
+
* trade until its settlement is confirmed (unsettled)". Null when no bar is
|
|
265
|
+
* any of them.
|
|
261
266
|
*/
|
|
262
|
-
export function historyBarStatusNote(
|
|
267
|
+
export function historyBarStatusNote(
|
|
268
|
+
rows: readonly Pick<HistoryRow, "date" | "partial" | "filled" | "unsettled" | "volume">[],
|
|
269
|
+
): string | null {
|
|
263
270
|
const partial = rows.filter((row) => row.partial);
|
|
264
271
|
const filled = rows.filter((row) => row.filled);
|
|
272
|
+
const unsettled = rows.filter((row) => row.unsettled);
|
|
265
273
|
const notes = [
|
|
266
274
|
partial.length > 0
|
|
267
275
|
? `${listDates(partial.map((row) => row.date))} ${partial.length === 1 ? "is" : "are"} still forming (partial)`
|
|
@@ -271,6 +279,10 @@ export function historyBarStatusNote(rows: readonly Pick<HistoryRow, "date" | "p
|
|
|
271
279
|
? `${listDates(filled.map((row) => row.date))} ${filled.length === 1 ? "was" : "were"} rebuilt from hourly bars (filled)`
|
|
272
280
|
+ (filled.every((row) => row.volume == null) ? ", without volume" : "")
|
|
273
281
|
: null,
|
|
282
|
+
unsettled.length > 0
|
|
283
|
+
? `${listDates(unsettled.map((row) => row.date))} ${unsettled.length === 1 ? "closes" : "close"} on the last trade until the`
|
|
284
|
+
+ ` settlement is confirmed (unsettled)`
|
|
285
|
+
: null,
|
|
274
286
|
].filter((note): note is string => note != null);
|
|
275
287
|
return notes.length > 0 ? notes.join("; ") : null;
|
|
276
288
|
}
|
package/src/cli/index.ts
CHANGED
|
@@ -37,7 +37,8 @@ import { createSystemCliCommands } from "./commands/system";
|
|
|
37
37
|
import { brokerCliCommand, ibkrCliCommand } from "./commands/broker";
|
|
38
38
|
import { listPlugins, updatePlugins } from "./commands/plugins";
|
|
39
39
|
import { installListedPlugin, removePlugin } from "../plugins/installer";
|
|
40
|
-
import { functionCodeCommand, runPaneCatalog, runPaneFunction, runPaneScreenshot } from "./pane-functions";
|
|
40
|
+
import { catalogCommandHits, functionCodeCommand, runPaneCatalog, runPaneFunction, runPaneScreenshot } from "./pane-functions";
|
|
41
|
+
import { catalogPointer, type CatalogCommandHit } from "./unknown-command";
|
|
41
42
|
import { catalogFredSeriesIds } from "./series-search";
|
|
42
43
|
|
|
43
44
|
const { width: SHOT_WIDTH, height: SHOT_HEIGHT } = SHOT_SIZE_LIMITS;
|
|
@@ -64,7 +65,7 @@ function createCoreCliCommands(
|
|
|
64
65
|
const topic = args[0];
|
|
65
66
|
if (topic) {
|
|
66
67
|
const command = lookupCommand(topic);
|
|
67
|
-
if (!command) failUnknownCommand(topic, allCommands(), await
|
|
68
|
+
if (!command) failUnknownCommand(topic, allCommands(), await unknownCommandPointers(ctx, topic, [], allCommands()));
|
|
68
69
|
printCommandHelp(command, ctx);
|
|
69
70
|
return;
|
|
70
71
|
}
|
|
@@ -306,18 +307,47 @@ function printCommandHelp(command: CliCommandDef, ctx: CliCommandContext): void
|
|
|
306
307
|
ctx.printResult({ data: { ...describeCliCommand(command), sections: command.help?.sections ?? [] } });
|
|
307
308
|
}
|
|
308
309
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (fn) fail(`${fn.code} is a function: ${fn.command}`);
|
|
312
|
-
const suggestion = suggestCliCommand(
|
|
310
|
+
function nearCommand(token: string, commands: CliCommandDef[]): string | null {
|
|
311
|
+
return suggestCliCommand(
|
|
313
312
|
normalizeCliCommandToken(token),
|
|
314
313
|
commands.flatMap((command) => [command.name, ...(command.aliases ?? [])]),
|
|
315
314
|
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
interface UnknownCommandPointers {
|
|
318
|
+
/** The word is a function's code: the command that runs it. */
|
|
319
|
+
fn: { code: string; command: string } | null;
|
|
320
|
+
/** Functions the catalog finds for the word, read only when it is no code and no command is close to it. */
|
|
321
|
+
hits: CatalogCommandHit[];
|
|
322
|
+
/** What the reader typed after the word, which a pointer runs the function with. */
|
|
323
|
+
args: readonly string[];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** What an unknown command word may point at: a function's code first, then what the catalog finds for it. */
|
|
327
|
+
async function unknownCommandPointers(
|
|
328
|
+
ctx: CliCommandContext,
|
|
329
|
+
token: string,
|
|
330
|
+
args: readonly string[],
|
|
331
|
+
commands: CliCommandDef[],
|
|
332
|
+
): Promise<UnknownCommandPointers> {
|
|
333
|
+
const fn = await functionCodeCommand(ctx, token, args);
|
|
334
|
+
const hits = fn || nearCommand(token, commands) ? [] : await catalogCommandHits(ctx, token);
|
|
335
|
+
return { fn, hits, args };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* A function's code is pointed at `fn`; a typo of a command at the command;
|
|
340
|
+
* a word the catalog knows (`comps`, `peers`) at the functions it finds.
|
|
341
|
+
*/
|
|
342
|
+
function failUnknownCommand(token: string, commands: CliCommandDef[], pointers: UnknownCommandPointers): never {
|
|
343
|
+
if (pointers.fn) fail(`${pointers.fn.code} is a function: ${pointers.fn.command}`);
|
|
344
|
+
const suggestion = nearCommand(token, commands);
|
|
345
|
+
const catalog = suggestion ? null : catalogPointer(token, pointers.hits, pointers.args);
|
|
316
346
|
fail(
|
|
317
347
|
`Unknown command "${token}".`,
|
|
318
348
|
suggestion
|
|
319
349
|
? `Did you mean ${suggestion}? Run gloomberb help to see every command.`
|
|
320
|
-
: "Run gloomberb help to see every command.",
|
|
350
|
+
: catalog ? `${catalog}\nRun gloomberb help to see every command.` : "Run gloomberb help to see every command.",
|
|
321
351
|
);
|
|
322
352
|
}
|
|
323
353
|
|
|
@@ -408,11 +438,13 @@ export async function failUnknownCliCommand(args: string[], options: DispatchCli
|
|
|
408
438
|
// dispatchCli already reported flags that do not parse.
|
|
409
439
|
}
|
|
410
440
|
const registry = await createRegistry(options);
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
441
|
+
const commands = listedCommands(registry).map((entry) => entry.command);
|
|
442
|
+
// A function's code typed as a command is pointed at `fn`, and a word the catalog knows at its
|
|
443
|
+
// functions, with the arguments the reader gave.
|
|
444
|
+
const pointers = token && parsed
|
|
445
|
+
? await unknownCommandPointers(createCliCommandContext("core", registry, parsed.options, parsed.args.slice(1)), token, parsed.args.slice(1), commands)
|
|
446
|
+
: { fn: null, hits: [], args: [] };
|
|
447
|
+
return failUnknownCommand(token, commands, pointers);
|
|
416
448
|
}
|
|
417
449
|
|
|
418
450
|
export async function runCli(args: string[], options: DispatchCliOptions = {}): Promise<boolean> {
|
package/src/cli/known-gaps.ts
CHANGED
|
@@ -49,7 +49,11 @@ const KNOWN_GAPS: ReadonlyArray<{ terms: readonly string[]; message: string }> =
|
|
|
49
49
|
*/
|
|
50
50
|
const TOPIC_POINTERS: ReadonlyArray<{
|
|
51
51
|
topic: string;
|
|
52
|
+
/** What is true of it, when "not in the terminal yet" is not: no source carries it. */
|
|
53
|
+
availability?: string;
|
|
52
54
|
phrases: readonly string[];
|
|
55
|
+
/** Words that ask for the topic only as the whole query: "utilization" alone, but "capacity utilization" is another thing. */
|
|
56
|
+
whole?: readonly string[];
|
|
53
57
|
closest: ReadonlyArray<readonly [code: string, gives: string]>;
|
|
54
58
|
}> = [
|
|
55
59
|
{
|
|
@@ -58,6 +62,16 @@ const TOPIC_POINTERS: ReadonlyArray<{
|
|
|
58
62
|
"military budget", "military spending", "pentagon budget", "pentagon spending"],
|
|
59
63
|
closest: [["AWARDS", "government contract awards by company"], ["THEM", "defense and aerospace theme: fn THEM defense"]],
|
|
60
64
|
},
|
|
65
|
+
{
|
|
66
|
+
// SI's own limitations say the same, so a search for a borrow fee does not read SI as a source of one.
|
|
67
|
+
topic: "borrow fee, utilization and locate",
|
|
68
|
+
availability: "not available (no securities-lending data)",
|
|
69
|
+
phrases: ["borrow fee", "borrow fees", "borrow rate", "borrow rates", "borrow cost", "borrow costs", "cost to borrow", "short borrow",
|
|
70
|
+
"stock borrow", "borrow availability", "borrow utilization", "borrow utilisation", "shares to borrow", "hard to borrow",
|
|
71
|
+
"easy to borrow", "securities lending", "stock loan", "stock lending", "locate fee", "short locate"],
|
|
72
|
+
whole: ["borrow", "borrowing", "utilization", "utilisation", "locate", "locates", "htb", "lendable shares"],
|
|
73
|
+
closest: [["SI", "FINRA short interest and days to cover"], ["SIW", "crowded shorts across your names"], ["SIV", "daily short volume"]],
|
|
74
|
+
},
|
|
61
75
|
];
|
|
62
76
|
|
|
63
77
|
/** The words of a text, lower case: `DoD-budget` and `dod budget` read alike. */
|
|
@@ -76,10 +90,15 @@ function containsWords(query: readonly string[], phrase: readonly string[]): boo
|
|
|
76
90
|
/** The pointer for a query that asks about a topic no function covers, with the function codes it names. */
|
|
77
91
|
export function topicPointer(query: string): { message: string; codes: string[] } | null {
|
|
78
92
|
const asked = words(query);
|
|
79
|
-
const
|
|
93
|
+
const key = lookupKey(query);
|
|
94
|
+
const pointer = TOPIC_POINTERS.find(({ phrases, whole }) => phrases.some((phrase) => containsWords(asked, words(phrase)))
|
|
95
|
+
|| whole?.some((word) => lookupKey(word) === key));
|
|
80
96
|
if (!pointer) return null;
|
|
81
97
|
const closest = pointer.closest.map(([code, gives]) => `${code} (${gives})`).join(", ");
|
|
82
|
-
return {
|
|
98
|
+
return {
|
|
99
|
+
message: `${pointer.topic}: ${pointer.availability ?? "not in the terminal yet"}; closest: ${closest}`,
|
|
100
|
+
codes: pointer.closest.map(([code]) => code),
|
|
101
|
+
};
|
|
83
102
|
}
|
|
84
103
|
|
|
85
104
|
/**
|
package/src/cli/listing-arg.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
typedExchangeCode,
|
|
40
40
|
} from "../utils/exchanges";
|
|
41
41
|
import { withMarketData } from "./scoped-context";
|
|
42
|
+
import { stripStrayEdges } from "./symbol-args";
|
|
42
43
|
import { isNoProviderMessage } from "../sources/provider-errors";
|
|
43
44
|
import { dashedCoinPair } from "../utils/crypto-pair";
|
|
44
45
|
import { currencyPairSpelling } from "../utils/currency-pair";
|
|
@@ -91,9 +92,21 @@ export interface ListingArg {
|
|
|
91
92
|
* `resolveCliListing` asks search before reading them as USDNGN=X.
|
|
92
93
|
*/
|
|
93
94
|
export function parseListingArg(
|
|
94
|
-
|
|
95
|
+
rawArg: string,
|
|
95
96
|
exchangeOption?: string | null,
|
|
96
97
|
{ ownExchangeWins = false }: { ownExchangeWins?: boolean } = {},
|
|
98
|
+
): ListingArg {
|
|
99
|
+
// A pasted ticker can bring the separator or quote marks that sat beside it (`UPST;`); search would ask for them.
|
|
100
|
+
const stripped = stripStrayEdges(rawArg);
|
|
101
|
+
const raw = stripped.text || rawArg;
|
|
102
|
+
const listing = parseCleanListingArg(raw, exchangeOption, { ownExchangeWins });
|
|
103
|
+
return stripped.text && stripped.notes.length > 0 ? { ...listing, readAs: stripped.notes.join(" ") } : listing;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function parseCleanListingArg(
|
|
107
|
+
raw: string,
|
|
108
|
+
exchangeOption: string | null | undefined,
|
|
109
|
+
{ ownExchangeWins }: { ownExchangeWins: boolean },
|
|
97
110
|
): ListingArg {
|
|
98
111
|
const optionCode = (exchangeOption ?? "").trim().toUpperCase();
|
|
99
112
|
const option = canonicalExchange(optionCode);
|
|
@@ -167,9 +180,11 @@ async function settleSharedCode(listing: ListingArg, deps: ListingDeps): Promise
|
|
|
167
180
|
}
|
|
168
181
|
const { exchange } = reading;
|
|
169
182
|
const key = `${symbol}:${exchange}`;
|
|
170
|
-
const readAs =
|
|
171
|
-
|
|
172
|
-
|
|
183
|
+
const readAs = [
|
|
184
|
+
listing.readAs,
|
|
185
|
+
exchange === meanings[0] ? null : `Read ${symbol}:${code} as ${key}: ${code} is ${venueChoices(meanings)}, and ${symbol} trades on ${exchange}.`,
|
|
186
|
+
].filter(Boolean).join(" ");
|
|
187
|
+
return { symbol, exchange, key, ...readAs ? { readAs } : {} };
|
|
173
188
|
}
|
|
174
189
|
|
|
175
190
|
/**
|
|
@@ -2,6 +2,8 @@ import type { DataProvider } from "../types/data-provider";
|
|
|
2
2
|
import type { TickerRecord } from "../types/ticker";
|
|
3
3
|
import { LISTING_SEARCH_LIMIT, searchTickerCandidates, type TickerSearchCandidate } from "../tickers/search";
|
|
4
4
|
import { mapWithConcurrency } from "../utils/concurrency";
|
|
5
|
+
import { isUsListingExchange } from "../utils/exchanges";
|
|
6
|
+
import { STARTER_SYMBOLS } from "../tickers/starter-symbols";
|
|
5
7
|
import { TickerNotFoundError } from "./ticker-resolution";
|
|
6
8
|
|
|
7
9
|
/** How long a suggestion waits for search; a slow search leaves the plain "not found" as it was. */
|
|
@@ -118,13 +120,18 @@ async function searchAsCommand(query: string, { store, dataProvider }: Suggestio
|
|
|
118
120
|
export async function suggestListing(input: string, deps: SuggestionDeps): Promise<ListingSuggestion | null> {
|
|
119
121
|
const query = input.trim().replace(/^\$/, "");
|
|
120
122
|
if (!query) return null;
|
|
123
|
+
const candidates = await searchCandidates(query, deps);
|
|
124
|
+
return candidates ? nearestListing(query, candidates) : null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The search's candidates for a query, or null when it fails or is slow; it never throws and never waits longer than the timeout. */
|
|
128
|
+
async function searchCandidates(query: string, deps: SuggestionDeps): Promise<TickerSearchCandidate[] | null> {
|
|
121
129
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
122
130
|
try {
|
|
123
|
-
|
|
131
|
+
return await Promise.race([
|
|
124
132
|
Promise.resolve().then(() => (deps.search ?? ((text: string) => searchAsCommand(text, deps)))(query)),
|
|
125
133
|
new Promise<null>((resolve) => { timer = setTimeout(() => resolve(null), deps.timeoutMs ?? SUGGESTION_TIMEOUT_MS); }),
|
|
126
134
|
]);
|
|
127
|
-
return candidates ? nearestListing(query, candidates) : null;
|
|
128
135
|
} catch {
|
|
129
136
|
return null;
|
|
130
137
|
} finally {
|
|
@@ -132,6 +139,30 @@ export async function suggestListing(input: string, deps: SuggestionDeps): Promi
|
|
|
132
139
|
}
|
|
133
140
|
}
|
|
134
141
|
|
|
142
|
+
/**
|
|
143
|
+
* The US ticker a symbol that quoted on a listing abroad was most likely typed
|
|
144
|
+
* for: APPL, which is Aptus Pharma on BSE, for AAPL. Search lists no AAPL for
|
|
145
|
+
* APPL, so the match comes from the US symbols a person is likely to mean (the
|
|
146
|
+
* starter watchlist and their own saved US tickers) that are one slip away,
|
|
147
|
+
* and only a single one counts. Search then names it; a US listing it does not
|
|
148
|
+
* find is no suggestion. Null when nothing is near, and for a symbol too short
|
|
149
|
+
* for one edit to leave it recognisable.
|
|
150
|
+
*/
|
|
151
|
+
export async function suggestUsTicker(typed: string, deps: SuggestionDeps): Promise<ListingSuggestion | null> {
|
|
152
|
+
const wanted = compact(typed);
|
|
153
|
+
if (wanted.length < MIN_TYPO_LENGTH) return null;
|
|
154
|
+
const saved = (await deps.store.loadAllTickers().catch(() => [])).filter((ticker) => isUsListingExchange(ticker.metadata.exchange));
|
|
155
|
+
const near = new Set([...STARTER_SYMBOLS, ...saved.map((ticker) => ticker.metadata.ticker)]
|
|
156
|
+
.map((symbol) => symbol.trim().toUpperCase())
|
|
157
|
+
.filter((symbol) => compact(symbol).length >= MIN_TYPO_LENGTH && editDistance(wanted, compact(symbol)) === 1));
|
|
158
|
+
if (near.size !== 1) return null;
|
|
159
|
+
const [symbol] = near;
|
|
160
|
+
const candidates = await searchCandidates(symbol!, deps);
|
|
161
|
+
if (!candidates) return { symbol: symbol!, name: "" };
|
|
162
|
+
const listed = candidates.find((candidate) => (candidate.label || candidate.symbol).toUpperCase() === symbol && isUsListingExchange(candidate.exchangeLabel));
|
|
163
|
+
return listed ? { symbol: symbol!, name: candidateName(listed) } : null;
|
|
164
|
+
}
|
|
165
|
+
|
|
135
166
|
/** 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
167
|
const PLAIN_NO_MATCH = /^No ticker match found for ".*"\.$/s;
|
|
137
168
|
|