gloomberb 0.19.1 → 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/package.json +2 -1
- package/src/api-client/errors.ts +38 -0
- package/src/app/runtime/startup.ts +3 -1
- package/src/capabilities/chart-series.ts +2 -1
- package/src/cli/commands/market.ts +9 -4
- package/src/cli/commands/overview.ts +5 -6
- package/src/cli/commands/search.ts +13 -13
- package/src/cli/context.ts +1 -0
- package/src/cli/glossary.ts +2 -1
- package/src/cli/history-rows.ts +35 -7
- package/src/cli/known-gaps.ts +11 -0
- package/src/cli/listing-arg.ts +17 -1
- package/src/cli/quote-answer.ts +29 -6
- package/src/components/command-bar/routes/root/runtime.ts +27 -5
- package/src/components/command-bar/selection-runtime.ts +12 -29
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/header.tsx +2 -2
- package/src/components/layout/pane/footer/index.tsx +1 -0
- package/src/components/layout/pane/footer/model.ts +33 -1
- package/src/components/layout/pane/header.tsx +2 -2
- package/src/components/layout/pane/status-footer.ts +2 -1
- package/src/components/onboarding/onboarding-frame.tsx +2 -1
- package/src/components/onboarding/onboarding-wizard.tsx +6 -3
- package/src/components/ui/display.tsx +16 -5
- package/src/components/ui/icon.tsx +8 -2
- package/src/core/app-services.ts +11 -2
- package/src/data/app-persistence.ts +9 -7
- package/src/data/plugin-cache.ts +15 -0
- package/src/data/plugin-state-store.ts +81 -24
- package/src/data/resource-store.ts +130 -58
- 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/market-data/realized-volatility.ts +4 -2
- package/src/plugins/auto-update-node.ts +10 -6
- 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/headless-schema.ts +5 -0
- 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/commodity-balances/client.ts +4 -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/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/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 +19 -11
- package/src/plugins/builtin/earnings-calls/pane.tsx +27 -14
- package/src/plugins/builtin/econ/detail-view.tsx +16 -2
- 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/holders/beneficial-view.tsx +1 -2
- package/src/plugins/builtin/jodi/client.ts +4 -1
- package/src/plugins/builtin/market-movers/client.ts +1 -0
- package/src/plugins/builtin/market-movers/index.tsx +10 -5
- package/src/plugins/builtin/market-movers/screener.ts +23 -6
- package/src/plugins/builtin/market-movers/session-body.tsx +2 -1
- package/src/plugins/builtin/money-markets/series.ts +3 -5
- package/src/plugins/builtin/power/gas-client.ts +2 -1
- package/src/plugins/builtin/power/prices-client.ts +3 -2
- package/src/plugins/builtin/risk-factors/data.ts +8 -4
- package/src/plugins/builtin/risk-factors/index.tsx +3 -1
- package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
- package/src/plugins/builtin/scanner/lock-note.ts +26 -3
- package/src/plugins/builtin/shared/cloud-resource.ts +13 -8
- package/src/plugins/builtin/shared/fred-points.ts +6 -1
- package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
- 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 +11 -16
- package/src/plugins/builtin/thirteenf/data.ts +34 -6
- 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/treasury-auctions/cache.ts +20 -8
- package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
- package/src/plugins/builtin/treasury-auctions/headless.ts +3 -1
- package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
- 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 +6 -2
- package/src/plugins/builtin/volatility/series.ts +2 -1
- package/src/plugins/builtin/yield-curve/client.ts +4 -6
- package/src/plugins/installer.ts +50 -39
- package/src/renderers/dom/desktop/icons.tsx +3 -2
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
- package/src/renderers/electrobun/bun/desktop/initialization.ts +9 -1
- package/src/renderers/electrobun/bun/window/attention.ts +54 -0
- package/src/renderers/electrobun/bun/window/create.ts +2 -0
- package/src/renderers/electrobun/launcher/preload.ts +13 -0
- package/src/renderers/electrobun/launcher/windows-emulation.ts +50 -0
- package/src/renderers/electrobun/view/ui-host.tsx +9 -2
- package/src/series/resolve.ts +2 -1
- package/src/series/types.ts +5 -0
- package/src/sources/gloomberb-cloud/fred-series.ts +5 -3
- 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 +59 -29
- package/src/tickers/company-name.ts +65 -1
- package/src/tickers/search/home-listing.ts +93 -8
- package/src/tickers/search/index.ts +1 -1
- 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/financials.ts +18 -3
- package/src/ui/host.tsx +6 -1
- package/src/utils/financial-statements.ts +73 -16
- package/src/utils/fundamentals.ts +17 -5
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gloomberb",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2",
|
|
4
4
|
"packageManager": "bun@1.3.11",
|
|
5
5
|
"description": "Market research and portfolio tracker for the terminal",
|
|
6
6
|
"module": "src/public/index.ts",
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
"check:pane-layout": "bun run scripts/pane-chrome/layout-sweep.ts",
|
|
92
92
|
"check:ui-tokens": "bun run scripts/pane-chrome/ui-tokens.ts",
|
|
93
93
|
"ui:benchmark": "bun run scripts/pane-chrome/ui-benchmark.ts",
|
|
94
|
+
"ui:badge-ink": "bun run scripts/pane-chrome/badge-ink.ts",
|
|
94
95
|
"audit:ui": "bun run scripts/pane-chrome/ui-audit.ts",
|
|
95
96
|
"check:call-player": "bun run scripts/pane-chrome/call-player.ts",
|
|
96
97
|
"check:chart-axis-zoom": "bun run scripts/pane-chrome/chart-axis-zoom.ts",
|
package/src/api-client/errors.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isNetworkFailure } from "../utils/network-failure";
|
|
2
|
+
|
|
1
3
|
const HARD_SESSION_INVALID_PATTERNS = [
|
|
2
4
|
/\b(user|account)\b.*\b(not found|deleted|removed|disabled|deactivated|suspended)\b/i,
|
|
3
5
|
/\b(user|account)\b.*\bdoes(?:\s+not|n't)\s+exist\b/i,
|
|
@@ -61,6 +63,42 @@ export function isRetryableApiError(error: unknown): boolean {
|
|
|
61
63
|
return error.code === "retryable_error" || status === 408 || status === 429 || (status !== undefined && status >= 500);
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
/**
|
|
67
|
+
* A 200 reply saying its data is unavailable this time (a board's `unavailable`
|
|
68
|
+
* status, a market answer's `retryable_error`): the server's own source did
|
|
69
|
+
* not answer. Like a 5xx it may pass, so a saved copy answers it with its note.
|
|
70
|
+
* A permanent reason (a route not served yet, an unsupported symbol or series,
|
|
71
|
+
* sign-in or entitlement) comes as its status, or as a plain error, and stays
|
|
72
|
+
* the error.
|
|
73
|
+
*/
|
|
74
|
+
export function sourceDidNotAnswer(message: string): ApiRequestError {
|
|
75
|
+
return new ApiRequestError(message, undefined, undefined, "retryable_error");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Codes of a connection that dropped or stalled after it opened: Node's and the system's, then Bun's. */
|
|
79
|
+
const INTERRUPTED_CODES = new Set(["ECONNRESET", "ECONNABORTED", "EPIPE", "ETIMEDOUT", "ConnectionClosed"]);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A failure asking again later may get past, which a copy saved earlier may
|
|
83
|
+
* answer for: no connection, a connection that dropped, a timeout, or a server
|
|
84
|
+
* that was rate limited or overloaded (408, 429, 5xx). Anything else the
|
|
85
|
+
* server answered (another 4xx) and an answer that failed validation are
|
|
86
|
+
* refusals to show as errors, saved copy or not. A bare throw with nothing to
|
|
87
|
+
* classify is not taken for a refusal.
|
|
88
|
+
*/
|
|
89
|
+
export function isTransientFailure(error: unknown): boolean {
|
|
90
|
+
if (!error || typeof error !== "object") return typeof error !== "string" || isNetworkFailure(error);
|
|
91
|
+
if (isNetworkFailure(error) || isRetryableApiError(error)) return true;
|
|
92
|
+
for (let current: unknown = error, depth = 0; current && typeof current === "object" && depth < 4; depth += 1) {
|
|
93
|
+
if (current instanceof ApiRequestError) return false;
|
|
94
|
+
const { name, code } = current as { name?: unknown; code?: unknown };
|
|
95
|
+
if (name === "AbortError" || name === "TimeoutError") return true;
|
|
96
|
+
if (typeof code === "string" && INTERRUPTED_CODES.has(code)) return true;
|
|
97
|
+
current = (current as { cause?: unknown }).cause;
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
64
102
|
/** Reads `Retry-After` as milliseconds, accepting seconds or an HTTP date. */
|
|
65
103
|
export function parseRetryAfterMs(
|
|
66
104
|
header: string | null,
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
type UsageFunction,
|
|
33
33
|
} from "../../telemetry/usage-counts";
|
|
34
34
|
import { debugLog } from "../../utils/debug-log";
|
|
35
|
-
import { startMainThreadStallMonitor } from "../../utils/main-thread-stall";
|
|
35
|
+
import { markStartupComplete, startMainThreadStallMonitor } from "../../utils/main-thread-stall";
|
|
36
36
|
import { measurePerfAsync } from "../../utils/perf-marks";
|
|
37
37
|
|
|
38
38
|
const appLog = debugLog.createLogger("app");
|
|
@@ -161,6 +161,8 @@ export function useAppStartupRuntime({
|
|
|
161
161
|
} catch (err) {
|
|
162
162
|
logFailure("app.runtime.startup", "Workspace startup hydration failed")(err);
|
|
163
163
|
}
|
|
164
|
+
// The workspace has its data (or gave up on it): stalls from here on are not startup ones.
|
|
165
|
+
markStartupComplete();
|
|
164
166
|
})();
|
|
165
167
|
}, [
|
|
166
168
|
autoImportBrokerPositions,
|
|
@@ -203,7 +203,7 @@ function resolvedOutput(value: unknown): ResolvedSeries {
|
|
|
203
203
|
const input = object(value, "resolve output");
|
|
204
204
|
onlyKeys(input, "resolve output", [
|
|
205
205
|
"id", "label", "color", "unit", "unitGroup", "nativeFrequency", "timestampMode", "dataShape", "style", "transform",
|
|
206
|
-
"axis", "panelId", "interpolation", "timeBasis", "latestChangePercent", "points", "warning", "hidden", "attribution",
|
|
206
|
+
"axis", "panelId", "interpolation", "timeBasis", "latestChangePercent", "points", "warning", "savedAt", "hidden", "attribution",
|
|
207
207
|
"priceOf",
|
|
208
208
|
]);
|
|
209
209
|
if (!Array.isArray(input.points)) fail("resolve output points", "expected an array.");
|
|
@@ -247,6 +247,7 @@ function resolvedOutput(value: unknown): ResolvedSeries {
|
|
|
247
247
|
: {}),
|
|
248
248
|
points: input.points.map(point),
|
|
249
249
|
...(warning !== undefined ? { warning } : {}),
|
|
250
|
+
...(input.savedAt !== undefined ? { savedAt: finiteNumber(input.savedAt, "resolve output saved copy time") as number } : {}),
|
|
250
251
|
...(attribution ? { attribution: {
|
|
251
252
|
source: boundedString(attribution.source, "resolve output source", 160),
|
|
252
253
|
...(publisher !== undefined ? { publisher } : {}),
|
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
quoteChangeText,
|
|
41
41
|
} from "../helpers";
|
|
42
42
|
import { cliArrow, cliStyles, colorBySign, renderSection, renderStats } from "../../utils/cli-output";
|
|
43
|
-
import { filledPeriodsNote, stalePeriodNote, statementHistoryNotes } from "../../utils/financial-statements";
|
|
43
|
+
import { filledPeriodsNote, hasOlderStatements, stalePeriodNote, statementHistoryNotes } from "../../utils/financial-statements";
|
|
44
44
|
import { formatPerShareNumber } from "../../utils/reported-money";
|
|
45
45
|
import { isCurrencyPairListing } from "../../utils/fx-market-hours";
|
|
46
46
|
import {
|
|
@@ -131,6 +131,7 @@ import {
|
|
|
131
131
|
historyCoverageWarning,
|
|
132
132
|
historyFacts,
|
|
133
133
|
historyFlagNote,
|
|
134
|
+
historyWickNote,
|
|
134
135
|
historyGapNote,
|
|
135
136
|
historyJumpNote,
|
|
136
137
|
historyUnbrokenFrom,
|
|
@@ -1464,7 +1465,7 @@ async function loadListingHistory(
|
|
|
1464
1465
|
...window ? [...windowHistoryNotes(window, data), coverage ?? []]
|
|
1465
1466
|
: range === "YTD" ? ytdHistoryNotes(data) : [...historyNotes(range, interval, data[0]?.date, Date.now(), coverage != null), coverage ?? []],
|
|
1466
1467
|
// The status line says how far a bar still forming runs; the note is for the bars it does not name.
|
|
1467
|
-
gaps ?? [], historyJumpNote(data) ?? [], historyFlagNote(data) ?? [], historyMissingVolumeNote(data) ?? [],
|
|
1468
|
+
gaps ?? [], historyJumpNote(data) ?? [], historyFlagNote(data) ?? [], historyWickNote(data) ?? [], historyMissingVolumeNote(data) ?? [],
|
|
1468
1469
|
historyVenueVolumeNote(data) ?? [],
|
|
1469
1470
|
historyBarStatusNote(forming ? served : data, { tradingDay: venueTradingDay(venue, Date.now()) }) ?? [],
|
|
1470
1471
|
].flat();
|
|
@@ -1881,7 +1882,11 @@ async function runFinancials(rawArgs: string[], ctx: Parameters<CliCommandDef["e
|
|
|
1881
1882
|
// Years filled from the listing's exchange, for the revenue and net income columns.
|
|
1882
1883
|
const filled = filledPeriodsNote(financials.annualStatements.filter((statement) => rows.some((row) => row.date === statement.date)),
|
|
1883
1884
|
new Set(["totalRevenue", "netIncome"]));
|
|
1884
|
-
|
|
1885
|
+
// GF is pointed to only when the listing's extended history goes further back than these years.
|
|
1886
|
+
const dates = rows.map((row) => row.date);
|
|
1887
|
+
const older = await hasOlderStatements(() => market.dataProvider.getTickerFinancials(listing.request.symbol, listing.request.exchange,
|
|
1888
|
+
{ statementHistory: "extended" }), "annual", dates);
|
|
1889
|
+
const historyNotes = [...stale ? [stale] : [], ...filled ? [filled] : [], ...statementHistoryNotes("annual", dates, listing.key, older)];
|
|
1885
1890
|
// NDJSON keeps the raw statement rows it always wrote; CSV gets the table and its closing lines.
|
|
1886
1891
|
const tables = tabular
|
|
1887
1892
|
? selectTables({
|
|
@@ -3280,7 +3285,7 @@ export const marketDataCliCommands: BuiltinCliCommandDef[] = [
|
|
|
3280
3285
|
title: "Bars",
|
|
3281
3286
|
lines: [
|
|
3282
3287
|
`Each range comes in one bar size: ${historyIntervalsByRange()}. The line under the heading, and the --json metadata, give the currency, the bar size actually served, the first and last bar and the bar count. A warning says when the bars are coarser, or start later, than the range asked for, and for a coin, which trades every day, names each day it has no bar for.`,
|
|
3283
|
-
"A bar whose prices contradict each other (high below the open or close, or low above them) is left blank and flagged, such as high<open, with the count in a warning. --json rows carry currency, unit, interval and flag.",
|
|
3288
|
+
"A bar whose prices contradict each other (high below the open or close, or low above them) is left blank and flagged, such as high<open, with the count in a warning. A low under half the open and close, or a high over twice them, is left blank alone and flagged low<close/2 or high>2*open. --json rows carry currency, unit, interval and flag.",
|
|
3284
3289
|
"A break in the series flags the bar after it, which keeps its prices: gap after more than three months with no bar (weeks for weekly, daily and intraday bars), jump when the close is over 100 times the one before or under a hundredth of it. A warning names each break, and the heading says the date the series runs unbroken from.",
|
|
3285
3290
|
"The heading's last line is the max drawdown of the bars shown: the largest fall from a close to a later close, with the peak and the trough, their dates, and the date a close got back to the peak, if one did (maxDrawdown in --json).",
|
|
3286
3291
|
],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { logFailure } from "../../utils/log-failure";
|
|
2
2
|
import { apiClient } from "../../api-client";
|
|
3
|
-
import { ApiRequestError
|
|
3
|
+
import { ApiRequestError } from "../../api-client/errors";
|
|
4
4
|
import type { CliCommandDef } from "../../types/plugin";
|
|
5
5
|
import { withCliServices, withConfigData, withMarketData } from "../context";
|
|
6
6
|
import { CLI_ERROR_CODE, failWithFields } from "../errors";
|
|
@@ -646,16 +646,15 @@ function withSavedFredSeries<T>(ctx: Parameters<CliCommandDef["execute"]>[1], ru
|
|
|
646
646
|
|
|
647
647
|
/**
|
|
648
648
|
* A FRED series from Gloom Cloud, or the copy an earlier read saved when it
|
|
649
|
-
* cannot be refreshed (marked as such). A refusal
|
|
650
|
-
*
|
|
649
|
+
* cannot be refreshed (marked as such). A refusal (a series Gloom Cloud no
|
|
650
|
+
* longer serves, access denied) is said, not covered: `loadCachedFredSeries`
|
|
651
|
+
* throws it.
|
|
651
652
|
*/
|
|
652
653
|
async function loadFredSeries(
|
|
653
654
|
seriesId: string,
|
|
654
655
|
params: { startDate?: string; limit?: number; sortOrder: "asc" | "desc" },
|
|
655
656
|
): Promise<CloudFredSeriesPayload> {
|
|
656
|
-
|
|
657
|
-
if (result.source === "stale-fallback" && isPermanentClientError(result.error)) throw result.error;
|
|
658
|
-
return result.data;
|
|
657
|
+
return (await loadCachedFredSeries({ seriesId, ...params }, () => apiClient.getCloudFredSeries(seriesId, params))).data;
|
|
659
658
|
}
|
|
660
659
|
|
|
661
660
|
async function runFredList(filter: string, ctx: Parameters<CliCommandDef["execute"]>[1]) {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
candidateExchange,
|
|
17
17
|
LISTING_SEARCH_LIMIT,
|
|
18
18
|
listingDistance,
|
|
19
|
+
primaryCompanyLine,
|
|
19
20
|
searchTickerCandidates,
|
|
20
21
|
type TickerSearchCandidate,
|
|
21
22
|
} from "../../tickers/search";
|
|
@@ -34,8 +35,7 @@ import { readBrokerIndexCode } from "../listing-arg";
|
|
|
34
35
|
import { knownGapMessage, commandPointer } from "../known-gaps";
|
|
35
36
|
import { instrumentTypeLabel } from "../../tickers/instrument-kind";
|
|
36
37
|
import { canonicalExchange, isUsListingExchange } from "../../utils/exchanges";
|
|
37
|
-
import { companyNameExtraWords, groupByCompany, sameCompany } from "../../tickers/company-name";
|
|
38
|
-
import { namedByService } from "../../tickers/company-name-listing";
|
|
38
|
+
import { companyLines, companyNameExtraWords, groupByCompany, namedByService, sameCompany } from "../../tickers/company-name";
|
|
39
39
|
import { findCatalogSeries, renderSeriesMatches, type SeriesMatch } from "../series-search";
|
|
40
40
|
import { centralBankAskedAbout, centralBankCiting, type CentralBank } from "../../utils/central-banks";
|
|
41
41
|
import { createPaneCatalog } from "../pane-functions/discovery";
|
|
@@ -283,17 +283,17 @@ function withHomeListingPrimary(query: string, candidates: TickerSearchCandidate
|
|
|
283
283
|
|| primary.symbol.toUpperCase() === query.toUpperCase()) return candidates;
|
|
284
284
|
const company = resolveSearchName(primary);
|
|
285
285
|
const marked = !!primary.result && namedByService(query, primary.result);
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
&& (
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if (
|
|
286
|
+
// Never a line of another business its name is in (`companyLines`): Siemens Energy AG is not Siemens AG's.
|
|
287
|
+
const lines = companyLines(primary, candidates.filter((candidate) => candidate === primary || (candidate.kind === "search"
|
|
288
|
+
&& candidate.category !== "Funds & Derivatives" && (candidate.instrumentClass === "equity" || candidate.instrumentClass === "other"))),
|
|
289
|
+
(candidate) => ({
|
|
290
|
+
name: resolveSearchName(candidate), exchange: candidateExchange(candidate),
|
|
291
|
+
receipt: listingDistance(candidate) === 2, home: candidate.result?.homeListing === true,
|
|
292
|
+
})).filter((candidate) => candidate === primary || !marked || companyNameExtraWords(resolveSearchName(candidate), company) != null);
|
|
293
|
+
const home = primaryCompanyLine(primary, lines, (candidate) => ({
|
|
294
|
+
distance: listingDistance(candidate), score: candidate.result?.popularity ?? candidate.popularity ?? null,
|
|
295
|
+
}), { named: marked });
|
|
296
|
+
if (home === primary) return candidates;
|
|
297
297
|
const rest = candidates.filter((candidate) => candidate !== home);
|
|
298
298
|
const at = rest.indexOf(primary);
|
|
299
299
|
return [...rest.slice(0, at), { ...home, category: "Primary Listing" as const }, { ...primary, category: "Other Listings" as const }, ...rest.slice(at + 1)];
|
package/src/cli/context.ts
CHANGED
|
@@ -141,6 +141,7 @@ export async function initCliServices(options: CliServicesOptions = {}) {
|
|
|
141
141
|
config,
|
|
142
142
|
plugins: getLoadablePlugins(options.externalPlugins ?? []),
|
|
143
143
|
externalPlugins: options.externalPlugins ?? [],
|
|
144
|
+
interactive: false,
|
|
144
145
|
});
|
|
145
146
|
services.providerRouter.setConfigAccessor(() => config);
|
|
146
147
|
services.providerRouter.setBackgroundRevalidation(false);
|
package/src/cli/glossary.ts
CHANGED
|
@@ -404,7 +404,8 @@ export const GLOSSARY: readonly GlossaryEntry[] = [
|
|
|
404
404
|
},
|
|
405
405
|
{
|
|
406
406
|
term: "Net debt", aliases: ["net debt/ebitda", "net debt to ebitda", "net leverage"], functions: ["FA"], shown: ["Net Debt"],
|
|
407
|
-
definition: "Total debt less cash and short-term investments
|
|
407
|
+
definition: "Total debt less cash and short-term investments, with the balance sheet's lease liabilities under IFRS (\"incl. leases\"); "
|
|
408
|
+
+ "a company reporting under US GAAP (a 10-K, or a 20-F such as Alibaba's) keeps its operating leases out (\"excl. op. leases\"). Negative means more cash than debt. "
|
|
408
409
|
+ "Net debt / EBITDA is how many years of EBITDA would repay it; FA divides by a year of EBITDA, the trailing four quarters in a quarterly column.",
|
|
409
410
|
},
|
|
410
411
|
{
|
package/src/cli/history-rows.ts
CHANGED
|
@@ -14,7 +14,9 @@ import { isRoundTheClockCoin } from "../utils/crypto-pair";
|
|
|
14
14
|
import { currencyPairCode } from "../utils/currency-pair";
|
|
15
15
|
import { currencyUnitLabel, resolveCurrencyUnit } from "../utils/currency-units";
|
|
16
16
|
import { getPricePointTimestamp } from "../utils/price-history";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
isWickFlag, latestTradeIndex, missingVolumeNotice, pricePointIntegrityFlags, pricePointValues, pricePointWickFlags,
|
|
19
|
+
} from "../utils/price-history-integrity";
|
|
18
20
|
import { formatUtcTime } from "../utils/utc-time";
|
|
19
21
|
import { historyCoverageNotice } from "../sources/history-coverage";
|
|
20
22
|
import { barLastSession, barPeriodToDate, type BarPeriodToDate } from "../market-data/market/bar-period";
|
|
@@ -253,10 +255,17 @@ function flagCodes(row: Pick<HistoryRow, "flag">): string[] {
|
|
|
253
255
|
return row.flag ? row.flag.split(",") : [];
|
|
254
256
|
}
|
|
255
257
|
|
|
258
|
+
/** The codes of a bar left blank for prices that contradict each other: not a break before it, nor a withheld wick. */
|
|
259
|
+
function blankingCodes(row: Pick<HistoryRow, "flag">): string[] {
|
|
260
|
+
return flagCodes(row).filter((code) => !BREAK_FLAGS.has(code) && !isWickFlag(code));
|
|
261
|
+
}
|
|
262
|
+
|
|
256
263
|
/**
|
|
257
264
|
* Intraday bars keep their UTC time; daily and longer bars are trading dates.
|
|
258
265
|
* A bar whose prices contradict each other is blanked exactly as HP blanks it,
|
|
259
|
-
* and says why in `flag`. A
|
|
266
|
+
* and says why in `flag`. A low or high too far from the open and close to
|
|
267
|
+
* have traded is left blank, the rest of the bar kept, flagged `low<close/2`
|
|
268
|
+
* or `high>2*open`. A bar after a gap or a jump in the series keeps its
|
|
260
269
|
* prices and is flagged `gap` or `jump`.
|
|
261
270
|
*/
|
|
262
271
|
export function historyRows(
|
|
@@ -277,7 +286,7 @@ export function historyRows(
|
|
|
277
286
|
const time = times[index]!;
|
|
278
287
|
const iso = Number.isFinite(time) ? new Date(time).toISOString() : "";
|
|
279
288
|
const values = pricePointValues(point);
|
|
280
|
-
const flags = pricePointIntegrityFlags(point);
|
|
289
|
+
const flags = [...pricePointIntegrityFlags(point), ...pricePointWickFlags(point)];
|
|
281
290
|
const price = (value: number | null) => value == null ? null : cleanBarPrice(value, priceDivisor);
|
|
282
291
|
return {
|
|
283
292
|
date: intraday ? iso.replace(".000Z", "Z") : iso.slice(0, 10),
|
|
@@ -448,7 +457,7 @@ export function historyMissingVolumeNote(
|
|
|
448
457
|
if (!historyReportsVolume(rows)) return null;
|
|
449
458
|
const latestTrade = latestTradeIndex(rows.map((row) => Date.parse(row.date)));
|
|
450
459
|
const missing = rows.filter((row, index) => row.volume == null && row.venueVolume == null && !row.partial && !row.filled
|
|
451
|
-
&&
|
|
460
|
+
&& blankingCodes(row).length === 0 && index !== latestTrade).length;
|
|
452
461
|
return missingVolumeNotice(missing, rows.length) ?? null;
|
|
453
462
|
}
|
|
454
463
|
|
|
@@ -833,10 +842,9 @@ export function historyGapNote(
|
|
|
833
842
|
* keep their prices and are not counted.
|
|
834
843
|
*/
|
|
835
844
|
export function historyFlagNote(rows: readonly Pick<HistoryRow, "flag">[]): string | null {
|
|
836
|
-
const
|
|
837
|
-
const flagged = rows.filter((row) => contradictions(row).length > 0);
|
|
845
|
+
const flagged = rows.filter((row) => blankingCodes(row).length > 0);
|
|
838
846
|
if (flagged.length === 0) return null;
|
|
839
|
-
const codes = new Set(flagged.flatMap(
|
|
847
|
+
const codes = new Set(flagged.flatMap(blankingCodes));
|
|
840
848
|
const kinds = [
|
|
841
849
|
codes.has("high<open") || codes.has("high<close") ? "high below open or close" : "",
|
|
842
850
|
codes.has("low>open") || codes.has("low>close") ? "low above open or close" : "",
|
|
@@ -845,3 +853,23 @@ export function historyFlagNote(rows: readonly Pick<HistoryRow, "flag">[]): stri
|
|
|
845
853
|
const count = `${flagged.length} of ${rows.length} ${flagged.length === 1 && rows.length === 1 ? "bar" : "bars"}`;
|
|
846
854
|
return `${count} ${flagged.length === 1 ? "has" : "have"} ${kinds.join(", or ")} in the source data; their prices are left blank`;
|
|
847
855
|
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* "The low of 2021-11-29 is under half its open and close, too far to have
|
|
859
|
+
* traded; left blank and out of ranges", or null when no bar has such a wick
|
|
860
|
+
* (pricePointWickFlags). Three or more bars are counted.
|
|
861
|
+
*/
|
|
862
|
+
export function historyWickNote(rows: readonly Pick<HistoryRow, "date" | "flag">[]): string | null {
|
|
863
|
+
const flagged = rows.filter((row) => flagCodes(row).some(isWickFlag));
|
|
864
|
+
if (flagged.length === 0) return null;
|
|
865
|
+
const sides = new Set(flagged.flatMap((row) => flagCodes(row).filter(isWickFlag).map((code) => code.slice(0, code.indexOf(code.startsWith("low") ? "<" : ">")))));
|
|
866
|
+
const reason = "too far to have traded; left blank and out of ranges";
|
|
867
|
+
const only = flagged[0]!;
|
|
868
|
+
if (flagged.length === 1 && sides.size === 1) {
|
|
869
|
+
return sides.has("low") ? `The low of ${only.date} is under half its open and close, ${reason}`
|
|
870
|
+
: `The high of ${only.date} is over twice its open and close, ${reason}`;
|
|
871
|
+
}
|
|
872
|
+
const kinds = [sides.has("low") ? "a low under half" : "", sides.has("high") ? "a high over twice" : ""].filter(Boolean).join(" or ");
|
|
873
|
+
const dates = flagged.length <= LISTED_GAPS ? ` (${listItems(flagged.map((row) => row.date))})` : "";
|
|
874
|
+
return `${flagged.length} of ${rows.length} bars have ${kinds} their open and close${dates}, ${reason}`;
|
|
875
|
+
}
|
package/src/cli/known-gaps.ts
CHANGED
|
@@ -222,6 +222,17 @@ const TOPIC_POINTERS: ReadonlyArray<{
|
|
|
222
222
|
closestLabel: "COMEX curves",
|
|
223
223
|
closest: [["CTM", "gloom fn CTM HG for copper, gloom fn CTM ALI for aluminium"]],
|
|
224
224
|
},
|
|
225
|
+
{
|
|
226
|
+
// No source serves a jet fuel (kerosene) price. Heating oil, NY Harbor ULSD, is what airlines hedge it with and
|
|
227
|
+
// analysts read it by; the EIA's weekly US stocks are a catalog series (checked 2026-10-29: quote HO=F,
|
|
228
|
+
// fn COT HO and fn G CAP:doe:jet all answer).
|
|
229
|
+
topic: "Jet fuel (kerosene)",
|
|
230
|
+
availability: "no price series; heating oil (NY Harbor ULSD) is the usual proxy, its price: gloom quote HO=F. "
|
|
231
|
+
+ "US jet fuel stocks (EIA, weekly): gloom fn G CAP:doe:jet",
|
|
232
|
+
phrases: ["jet fuel", "jet fuels", "kerosene", "kerosine", "jet kerosene", "aviation fuel", "aviation fuels", "avtur"],
|
|
233
|
+
closestLabel: "positioning",
|
|
234
|
+
closest: [["COT", "heating oil, the usual jet fuel proxy: gloom fn COT HO"]],
|
|
235
|
+
},
|
|
225
236
|
{
|
|
226
237
|
// "exchange reserves" alone reads as crypto's; "foreign exchange reserves" is a central bank's, so only whole.
|
|
227
238
|
topic: "Crypto exchange reserves and exchange flows",
|
package/src/cli/listing-arg.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
type ListingVenue,
|
|
16
16
|
} from "../tickers/listing-venues";
|
|
17
17
|
import { findUsListingOfCompany, type UsListing } from "../tickers/us-listing";
|
|
18
|
-
import { findListingOfCompanyName, isCompanyNameInput, readTerminalCodeArg, type CompanyNameListing } from "../tickers/company-name-listing";
|
|
18
|
+
import { findListingOfCompanyName, isCompanyNameInput, marksAnotherListing, readTerminalCodeArg, type CompanyNameListing } from "../tickers/company-name-listing";
|
|
19
19
|
import { movedAwayChange, startTickerChangeLookup, type MovedTickerChange } from "../tickers/ticker-changes";
|
|
20
20
|
import { lineMayHaveMoved, loadListingFate, MovedSymbolError, type ListingFate } from "../tickers/listing-fate";
|
|
21
21
|
import { quotedListingName } from "../tickers/listing-name";
|
|
@@ -1338,6 +1338,22 @@ export async function oneWordCompanyName(typed: string, deps: Pick<ListingDeps,
|
|
|
1338
1338
|
return found ? companyListingArg(typed, found) : null;
|
|
1339
1339
|
}
|
|
1340
1340
|
|
|
1341
|
+
/**
|
|
1342
|
+
* A word the service marks as a company's name (`searchAliases`), read as
|
|
1343
|
+
* that company's listing as `oneWordCompanyName` reads a word, for a bare
|
|
1344
|
+
* symbol no source quotes, before it is read on the symbol's listings abroad:
|
|
1345
|
+
* "siemens" is SIE on XETRA (Siemens AG), not SIEMENS on NSE (Siemens Ltd.).
|
|
1346
|
+
* Null when the service marks no listing with the word, or marks the word's
|
|
1347
|
+
* own symbol, and when search names no company or is too busy to say.
|
|
1348
|
+
*/
|
|
1349
|
+
export async function serviceNamedCompany(typed: string, deps: Pick<ListingDeps, "dataProvider">): Promise<ListingArg | null> {
|
|
1350
|
+
// Most symbols are no company's name: one search says so before any listing is weighed or quoted.
|
|
1351
|
+
if (!ONE_WORD_NAME.test(typed.trim()) || !await marksAnotherListing(deps.dataProvider, typed)) return null;
|
|
1352
|
+
const found = await findOneWordCompany(typed, deps);
|
|
1353
|
+
if (!found?.named || found.symbol.toUpperCase() === typed.trim().toUpperCase()) return null;
|
|
1354
|
+
return companyListingArg(typed, found);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1341
1357
|
/** The company `oneWordCompanyName` reads a word as, before it is read as a listing. */
|
|
1342
1358
|
async function findOneWordCompany(typed: string, deps: Pick<ListingDeps, "dataProvider">): Promise<CompanyNameListing | null> {
|
|
1343
1359
|
const word = typed.trim();
|
package/src/cli/quote-answer.ts
CHANGED
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
notTradedMessage,
|
|
22
22
|
oneWordCompanyName,
|
|
23
23
|
quoteBareListing,
|
|
24
|
+
serviceNamedCompany,
|
|
25
|
+
type ListingArg,
|
|
24
26
|
requireCliListing,
|
|
25
27
|
resolveCliListing,
|
|
26
28
|
tradingBoardNote,
|
|
@@ -64,7 +66,9 @@ export interface QuoteAnswer {
|
|
|
64
66
|
/**
|
|
65
67
|
* One symbol's row of `quote` and `compare`. A listing on an exchange its symbol
|
|
66
68
|
* is not listed on says so. A bare symbol the data service read on its home
|
|
67
|
-
* listing abroad names that venue (SXR8 -> XETRA). One no source quotes
|
|
69
|
+
* listing abroad names that venue (SXR8 -> XETRA). One no source quotes that
|
|
70
|
+
* the service marks as another listing's company name is that company
|
|
71
|
+
* (`serviceNamedCompany`: "siemens" is SIE:XETRA); else it is
|
|
68
72
|
* quoted on its listings (`quoteBareListing`), else names them, or asks for an
|
|
69
73
|
* exchange when several companies list it (2222); a bare index root (VIX) also
|
|
70
74
|
* names its index (^VIX), and a futures board code a security keeps (GC) its
|
|
@@ -98,6 +102,10 @@ export async function answerQuote(
|
|
|
98
102
|
const notTraded = await notTradedMessage(listing, market);
|
|
99
103
|
if (notTraded) return { result, listing, failure: { message: notTraded, oneLine: notTraded }, notes: [] };
|
|
100
104
|
if (!isNoProviderError(result.error)) return { result, listing, notes: [] };
|
|
105
|
+
// A word the service marks as a company's name is that company, before the symbol is read on its listings abroad.
|
|
106
|
+
const named = !listing.exchange && listing.typed ? await serviceNamedCompany(listing.typed, market) : null;
|
|
107
|
+
const meant = named ? await answerNamedListing(named, market, request, refresh, datesChangeBasis) : null;
|
|
108
|
+
if (meant) return meant;
|
|
101
109
|
const other = await quoteBareListing(listing, market, request, { refresh, datesChangeBasis });
|
|
102
110
|
if (other?.kind === "quoted") {
|
|
103
111
|
return {
|
|
@@ -155,17 +163,32 @@ async function answerCompanyName(
|
|
|
155
163
|
const named = await oneWordCompanyName(listing.typed, market);
|
|
156
164
|
// A real ticker a source failed to quote is not a company's name, even when the company carries the word.
|
|
157
165
|
if (!named || (unsettled && named.symbol.toUpperCase() === listing.symbol.toUpperCase())) return null;
|
|
158
|
-
|
|
166
|
+
const answer = await answerNamedListing(named, market, request, refresh, datesChangeBasis);
|
|
167
|
+
if (answer) return answer;
|
|
168
|
+
if (!unsettled) return null;
|
|
169
|
+
const sentence = await notATickerWords(listing.typed, market);
|
|
170
|
+
return { result: { ...result, error: new Error(sentence) }, listing, notes: [] };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The row of the listing a company's name was read as (`named`), saying so;
|
|
175
|
+
* null when the app cannot read its venue or it has no quote.
|
|
176
|
+
*/
|
|
177
|
+
async function answerNamedListing(
|
|
178
|
+
named: ListingArg,
|
|
179
|
+
market: MarketContext,
|
|
180
|
+
request: ListingDataRequest,
|
|
181
|
+
refresh: boolean,
|
|
182
|
+
datesChangeBasis: boolean,
|
|
183
|
+
): Promise<QuoteAnswer | null> {
|
|
184
|
+
// A listing on a venue the app cannot read is no answer: the word keeps its own.
|
|
159
185
|
const resolved = await resolveCliListing(named.key, null, market).catch((error: unknown) => {
|
|
160
186
|
if (error instanceof ListingArgError) return null;
|
|
161
187
|
throw error;
|
|
162
188
|
});
|
|
163
189
|
const found = resolved ? { ...resolved, readAs: named.readAs, ...named.meant ? { meant: named.meant } : {}, ...named.readFromName ? { readFromName: named.readFromName } : {} } : null;
|
|
164
190
|
const [quoted] = found ? await market.dataProvider.getQuotesBatch([found.request], { forceRefresh: refresh, revalidate: true, datesChangeBasis }) : [];
|
|
165
|
-
|
|
166
|
-
if (!unsettled) return null;
|
|
167
|
-
const sentence = await notATickerWords(listing.typed, market);
|
|
168
|
-
return { result: { ...result, error: new Error(sentence) }, listing, notes: [] };
|
|
191
|
+
return found && quoted?.quote ? answerQuote(quoted, found, market, request, refresh, null, { datesChangeBasis }) : null;
|
|
169
192
|
}
|
|
170
193
|
|
|
171
194
|
/** A listing a command found a quote for, with what `quote` says beside it. */
|
|
@@ -154,6 +154,13 @@ export function useCommandBarRootRuntime({
|
|
|
154
154
|
} {
|
|
155
155
|
const previousRootSelectionContextRef = useRef<{ query: string; mode: string } | null>(null);
|
|
156
156
|
const previousRootResultIdsRef = useRef<string[]>([]);
|
|
157
|
+
/**
|
|
158
|
+
* The row the untouched highlight rests on once the query's answer is in,
|
|
159
|
+
* and the answer it was on. A later answer that reorders the rows (a
|
|
160
|
+
* fresher search over a cached one, a broker's) moves the highlight with
|
|
161
|
+
* its row, so Enter opens the row that was highlighted.
|
|
162
|
+
*/
|
|
163
|
+
const pinnedRootRowRef = useRef<{ id: string; answer: string | null } | null>(null);
|
|
157
164
|
const activeMatch = matchPrefix(rootQuery, availableCommands);
|
|
158
165
|
|
|
159
166
|
const tickerSearchRouteQuery = currentRoute?.kind === "mode" && currentRoute.screen === "ticker-search"
|
|
@@ -326,6 +333,7 @@ export function useCommandBarRootRuntime({
|
|
|
326
333
|
previousRootSelectionContextRef.current = { query: rootQuery, mode: rootModeKind };
|
|
327
334
|
if (selectionContextChanged) {
|
|
328
335
|
rootSelectionNavigatedRef.current = false;
|
|
336
|
+
pinnedRootRowRef.current = null;
|
|
329
337
|
}
|
|
330
338
|
|
|
331
339
|
setRootSelectedIdx((current) => {
|
|
@@ -338,15 +346,23 @@ export function useCommandBarRootRuntime({
|
|
|
338
346
|
if (shiftedIdx >= 0) return shiftedIdx;
|
|
339
347
|
return clampIndex(current, resultIds.length);
|
|
340
348
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
//
|
|
344
|
-
//
|
|
349
|
+
const pinnedIdx = pinnedRootRowRef.current ? resultIds.indexOf(pinnedRootRowRef.current.id) : -1;
|
|
350
|
+
if (pinnedIdx >= 0) return pinnedIdx;
|
|
351
|
+
// Untouched, the selection follows the best row on offer until the
|
|
352
|
+
// answer is in. When every row is an offer or a placeholder, none of
|
|
353
|
+
// them is the answer and none is selected: Enter must not take one of
|
|
354
|
+
// them for the ticker still being searched.
|
|
345
355
|
const defaultIdx = orderedRootResults.findIndex(isDefaultSelectable);
|
|
346
356
|
if (defaultIdx < 0) return -1;
|
|
347
|
-
|
|
357
|
+
const selected = clampIndex(Math.max(rootResultModel.initialIdx, defaultIdx), resultIds.length);
|
|
358
|
+
pinnedRootRowRef.current = rootResultsCurrent && resultIds[selected]
|
|
359
|
+
? { id: resultIds[selected]!, answer: activeRootProviderResultsKey }
|
|
360
|
+
: null;
|
|
361
|
+
return selected;
|
|
348
362
|
});
|
|
349
363
|
}, [
|
|
364
|
+
activeRootProviderResultsKey,
|
|
365
|
+
rootResultsCurrent,
|
|
350
366
|
activeMatch?.command.id,
|
|
351
367
|
currentRoute,
|
|
352
368
|
orderedRootResults,
|
|
@@ -360,7 +376,13 @@ export function useCommandBarRootRuntime({
|
|
|
360
376
|
|
|
361
377
|
useEffect(() => {
|
|
362
378
|
if (!activeRootProviderResultsKey) return;
|
|
379
|
+
// A later answer that only reorders the rows leaves the highlight on its row.
|
|
380
|
+
const pinned = pinnedRootRowRef.current;
|
|
381
|
+
if (pinned && pinned.answer !== activeRootProviderResultsKey
|
|
382
|
+
&& orderedRootResults.some((item) => item.id === pinned.id)) return;
|
|
363
383
|
setRootSelectedIdx(0);
|
|
384
|
+
const top = orderedRootResults[0];
|
|
385
|
+
pinnedRootRowRef.current = top ? { id: top.id, answer: activeRootProviderResultsKey } : null;
|
|
364
386
|
setRootHoveredIdx(null);
|
|
365
387
|
nativeListScrollRef.current?.scrollTo(0);
|
|
366
388
|
}, [activeRootProviderResultsKey, nativeListScrollRef, setRootHoveredIdx, setRootSelectedIdx]);
|
|
@@ -2,8 +2,8 @@ import { useCallback, useEffect, useRef, type MutableRefObject } from "react";
|
|
|
2
2
|
import type { AppState } from "../../state/app/context";
|
|
3
3
|
import type { TickerRecord } from "../../types/ticker";
|
|
4
4
|
import { t } from "../../i18n";
|
|
5
|
-
import {
|
|
6
|
-
import { isKnownExchangeCode
|
|
5
|
+
import { listingChoiceQuery } from "../../tickers/search";
|
|
6
|
+
import { isKnownExchangeCode } from "../../utils/exchanges";
|
|
7
7
|
import type { PluginRegistry } from "../../plugins/registry";
|
|
8
8
|
import type { Command } from "./commands/registry";
|
|
9
9
|
import type {
|
|
@@ -294,37 +294,21 @@ export function useCommandBarSelectionRuntime({
|
|
|
294
294
|
const activateListSelection = useCallback((options?: { secondary?: boolean; item?: ResultItem }) => {
|
|
295
295
|
const listState = visibleListStateRef.current;
|
|
296
296
|
if (!listState) return;
|
|
297
|
-
// Enter
|
|
298
|
-
//
|
|
299
|
-
//
|
|
297
|
+
// Enter opens exactly the highlighted row. The typed command runs as typed
|
|
298
|
+
// only while no listing for it is highlighted: before its search answers
|
|
299
|
+
// (`DES HFC` then opens the listing `quote HFC` reads), or while the rows
|
|
300
|
+
// on screen are an earlier query's.
|
|
300
301
|
const queryChanged = rootQueryRef.current !== rootQuery;
|
|
301
302
|
const tickerArgument = rootShortcutIntent.kind !== "none" && rootShortcutIntent.argKind === "ticker"
|
|
302
303
|
? rootShortcutIntent.argText : null;
|
|
303
304
|
const listingQuery = listingChoiceQuery(tickerArgument ?? rootQueryRef.current);
|
|
304
305
|
const choosingVenue = listingQuery && !isKnownExchangeCode(listingQuery.suffix);
|
|
305
|
-
const
|
|
306
|
+
const highlighted = listState.results[listState.selectedIdx];
|
|
307
|
+
const listingHighlighted = !queryChanged && (highlighted?.kind === "ticker" || highlighted?.kind === "search");
|
|
308
|
+
const submitTyped = !options?.item && !currentRoute && !listingHighlighted
|
|
306
309
|
&& (queryChanged || (!rootSelectionNavigatedRef.current && !choosingVenue));
|
|
307
|
-
|
|
310
|
+
const typed = submitTyped && (queryChanged || tickerArgument !== null)
|
|
308
311
|
? resolveImmediateRootSelection(rootQueryRef.current) : null;
|
|
309
|
-
const first = listState.results[listState.selectedIdx];
|
|
310
|
-
if (typed && !queryChanged && rootShortcutIntent.kind !== "none" && rootShortcutIntent.argKind === "ticker"
|
|
311
|
-
&& first && (first.kind === "ticker" || first.kind === "search")) {
|
|
312
|
-
const argument = rootShortcutIntent.argText;
|
|
313
|
-
// Company names and partial symbols still accept the suggested match.
|
|
314
|
-
// An exact symbol or named venue goes through listing resolution.
|
|
315
|
-
if (!parsePublicTickerKey(argument).exchange && !findExactTickerSearchMatch(
|
|
316
|
-
listState.results.filter((item) => item.kind === "ticker" || item.kind === "search"), argument,
|
|
317
|
-
)) typed = null;
|
|
318
|
-
}
|
|
319
|
-
// A plain symbol follows the same path once it is the default ticker row.
|
|
320
|
-
// Commands and company-name searches keep their own default actions.
|
|
321
|
-
const plainSymbol = submitTyped && !typed && first && (first.kind === "ticker" || first.kind === "search")
|
|
322
|
-
&& findExactTickerSearchMatch([first], rootQueryRef.current);
|
|
323
|
-
if (plainSymbol && first) {
|
|
324
|
-
const symbol = rootQueryRef.current;
|
|
325
|
-
typed = { ...first, action: () => runSecurityDescriptionShortcut(symbol),
|
|
326
|
-
secondaryAction: () => runSecurityDescriptionShortcut(symbol, { forceNewPane: true }) };
|
|
327
|
-
}
|
|
328
312
|
const selected = options?.item ?? typed ?? listState.results[listState.selectedIdx];
|
|
329
313
|
if (!selected) {
|
|
330
314
|
if (searchPending && !currentRoute && !options?.item && !rootSelectionNavigatedRef.current) {
|
|
@@ -399,8 +383,8 @@ export function useCommandBarSelectionRuntime({
|
|
|
399
383
|
item: selected,
|
|
400
384
|
query,
|
|
401
385
|
rank: selected === typed ? 0 : Math.max(0, listState.results.indexOf(selected)),
|
|
402
|
-
// A
|
|
403
|
-
isShortcut: () => !
|
|
386
|
+
// A listing row stays a ticker choice; a submitted command is the shortcut itself.
|
|
387
|
+
isShortcut: () => !listingHighlighted && (selected === typed || resolveImmediateRootSelection(query)?.id === selected.id),
|
|
404
388
|
}, run);
|
|
405
389
|
}, [
|
|
406
390
|
closeAll,
|
|
@@ -416,7 +400,6 @@ export function useCommandBarSelectionRuntime({
|
|
|
416
400
|
rootSelectionNavigatedRef,
|
|
417
401
|
rootShortcutIntent,
|
|
418
402
|
runRootRow,
|
|
419
|
-
runSecurityDescriptionShortcut,
|
|
420
403
|
searchPending,
|
|
421
404
|
stateConfigLayout,
|
|
422
405
|
stateRef,
|
|
@@ -170,9 +170,9 @@ export function DetachedPaneShell({ pluginRegistry, desktopWindowBridge }: Detac
|
|
|
170
170
|
fireAndForget(desktopWindowBridge.closeDetachedPane?.(desktopWindowBridge.paneId), "desktop.closeDetachedPane");
|
|
171
171
|
}, [desktopWindowBridge, locked, pluginRegistry]);
|
|
172
172
|
|
|
173
|
-
const startWindowDrag = useCallback(() => {
|
|
173
|
+
const startWindowDrag = useCallback((press?: { detail?: number }) => {
|
|
174
174
|
focusPane();
|
|
175
|
-
if (nativeWindowChrome) void rendererHost.startWindowDrag?.();
|
|
175
|
+
if (nativeWindowChrome) void rendererHost.startWindowDrag?.(press);
|
|
176
176
|
}, [focusPane, nativeWindowChrome, rendererHost]);
|
|
177
177
|
|
|
178
178
|
const sharePane = useCallback(() => {
|
|
@@ -870,9 +870,9 @@ export function Header({
|
|
|
870
870
|
// bar itself wears the prompt's fill and reads as a single strip.
|
|
871
871
|
const headerBg = nativePaneChrome ? colors.header : headerSurface(colors);
|
|
872
872
|
|
|
873
|
-
const startWindowDrag = useCallback(() => {
|
|
873
|
+
const startWindowDrag = useCallback((press?: { detail?: number }) => {
|
|
874
874
|
if (!titleBarOverlay || !nativeWindowChrome) return;
|
|
875
|
-
void rendererHost.startWindowDrag?.();
|
|
875
|
+
void rendererHost.startWindowDrag?.(press);
|
|
876
876
|
}, [nativeWindowChrome, rendererHost, titleBarOverlay]);
|
|
877
877
|
|
|
878
878
|
const openCommandBar = useCallback((event?: HeaderActionEvent) => {
|