gloomberb 0.17.2 → 0.17.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/api-client/chat.ts +28 -0
- package/src/api-client/errors.ts +11 -1
- package/src/api-client/futures-curve.ts +16 -0
- package/src/api-client/index.ts +22 -9
- package/src/api-client/normalizers.ts +21 -0
- package/src/api-client/socket.ts +27 -0
- package/src/api-client/types.ts +3 -1
- package/src/app/runtime/startup.ts +4 -0
- 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/index.ts +1 -1
- 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/metric-treemap/heat-surface.tsx +76 -14
- package/src/components/metric-treemap/heat-tiles.ts +5 -3
- package/src/components/metric-treemap/index.tsx +1 -1
- package/src/components/reader/detail-reader.tsx +28 -6
- package/src/components/ticker/badge/index.tsx +2 -0
- package/src/components/ui/button.tsx +24 -9
- package/src/components/ui/display.tsx +6 -1
- package/src/components/ui/external-link.tsx +10 -2
- package/src/components/ui/index.ts +1 -1
- package/src/components/ui/status.tsx +148 -5
- 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/layout-marketplace/gallery-desktop.tsx +2 -2
- package/src/market-data/futures-board.ts +44 -1
- 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/calendar-tab.tsx +1 -1
- package/src/plugins/builtin/account-management/pane.tsx +17 -13
- package/src/plugins/builtin/alerts/cli.ts +72 -11
- package/src/plugins/builtin/attention/headless.ts +19 -10
- package/src/plugins/builtin/attention/model.ts +22 -0
- package/src/plugins/builtin/awards/headless.ts +5 -1
- package/src/plugins/builtin/bond-calculator/pane.tsx +2 -1
- package/src/plugins/builtin/catalysts/headless.ts +5 -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/controller/connections.ts +18 -0
- package/src/plugins/builtin/chat/controller/fetch.ts +5 -2
- package/src/plugins/builtin/chat/controller/index.ts +49 -21
- package/src/plugins/builtin/chat/controller/notifications.ts +132 -72
- package/src/plugins/builtin/chat/controller/realtime.ts +37 -1
- 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/model.ts +20 -0
- package/src/plugins/builtin/cot/pane.tsx +2 -2
- package/src/plugins/builtin/crypto-board/model.ts +7 -3
- package/src/plugins/builtin/custom-view/pane.tsx +2 -2
- 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 +19 -2
- 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 +33 -6
- package/src/plugins/builtin/futures/model.ts +69 -5
- package/src/plugins/builtin/futures/series.ts +3 -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/jobs/pane.tsx +12 -9
- package/src/plugins/builtin/market-heatmap/index.tsx +6 -15
- 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/news/headless.ts +4 -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/plugin-marketplace/pane.tsx +1 -1
- 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 +4 -12
- 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/build-assets.ts +17 -4
- package/src/renderers/dom/desktop/controls.tsx +2 -0
- package/src/renderers/dom/styles.css +6 -0
- package/src/renderers/electrobun/bun/desktop/macos-dock-badge.ts +4 -3
- package/src/renderers/electrobun/bun/desktop/macos-dock-icon.ts +2 -3
- package/src/renderers/electrobun/bun/desktop/macos-objc.ts +17 -6
- package/src/sources/gloomberb-cloud/normalizers.ts +13 -4
- 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 +24 -5
- 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/ui/resize-settle.ts +128 -0
- package/src/utils/calendar-date.ts +56 -0
- package/src/utils/decimal-comma.ts +31 -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/futures-generic.ts +25 -4
- package/src/utils/markdown.ts +180 -0
- package/src/utils/market-capitalization.ts +22 -0
- package/src/utils/plugin-proxy-hosts.json +5 -0
- package/src/utils/utc-time.ts +14 -3
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
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DesktopPaneShotRenderedRow } from "../desktop-pane-shot";
|
|
2
|
-
import { cliStyles, renderSection, renderTable } from "../../utils/cli-output";
|
|
2
|
+
import { cliStyles, cliTerminalWidth, renderSection, renderTable, wrapText } from "../../utils/cli-output";
|
|
3
3
|
import { formatUtcTime } from "../../utils/utc-time";
|
|
4
4
|
import type { MarketContext } from "../types";
|
|
5
5
|
import type { PaneFunctionReport } from "./report";
|
|
@@ -14,10 +14,14 @@ import { renderedReportFailure, renderedReportFreshness, renderedReportNotices,
|
|
|
14
14
|
|
|
15
15
|
const DOM_REPORT_WIDTH = 1280;
|
|
16
16
|
const DOM_REPORT_HEIGHT = 720;
|
|
17
|
+
/** A pane whose reader has a page open grows to show all of it, up to about 18,000 px: a report saves no image to bound it. */
|
|
18
|
+
const DOM_REPORT_MAX_HEIGHT_CELLS = 1000;
|
|
17
19
|
const DOM_LIMITATION =
|
|
18
20
|
"Only values visible in the rendered pane are returned; clipped or off-screen data may be omitted.";
|
|
19
21
|
const DOM_EVERY_ROW =
|
|
20
22
|
"Every row of the pane's table is returned, including rows scrolled out of view.";
|
|
23
|
+
const DOM_WHOLE_PANE =
|
|
24
|
+
"The pane was grown to fit the page it has open, so all of its text is returned.";
|
|
21
25
|
|
|
22
26
|
/** The sign-in wall's buttons, which a rendered view reads as words: `Log in ↵ Sign up free`. */
|
|
23
27
|
const SIGN_IN_WALL_BUTTONS = /\s*\b(?:Log in|Sign up free|Resend Verification Email)(?:\s*\u21B5)?(?=\s|$)/g;
|
|
@@ -39,6 +43,28 @@ function signInWallText(visibleText: string): string {
|
|
|
39
43
|
return headline ? `${headline} ${needs}` : needs;
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
const LIST_MARKER = /^\s*[\u2022\u25E6\u25AA\u00B7\u2013-]\s*$/;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The words of a pane that is prose, not a table, a line to each block it draws
|
|
50
|
+
* and wrapped to the terminal's width (100 where it has none): the pane's own title,
|
|
51
|
+
* which the report's heading says already, is left off its top.
|
|
52
|
+
*/
|
|
53
|
+
function proseLines(render: PaneScreenshotResult["render"], title: string): string[] {
|
|
54
|
+
const lines = render.visibleLines?.length ? render.visibleLines : [render.visibleText];
|
|
55
|
+
const width = Math.min(cliTerminalWidth() ?? 100, 100);
|
|
56
|
+
const titled = lines[0]?.trim() === title ? lines.slice(1) : lines;
|
|
57
|
+
const body: string[] = [];
|
|
58
|
+
// A stack's detail opens under its Back button, a control and not part of the page.
|
|
59
|
+
for (const line of titled[0] === "Back" ? titled.slice(1) : titled) {
|
|
60
|
+
// A list marker is drawn as a block of its own, ahead of the item's words.
|
|
61
|
+
const marker = body.at(-1);
|
|
62
|
+
if (marker !== undefined && LIST_MARKER.test(marker)) body[body.length - 1] = `${marker.trim()} ${line}`;
|
|
63
|
+
else body.push(line);
|
|
64
|
+
}
|
|
65
|
+
return body.flatMap((line) => wrapText(line, width));
|
|
66
|
+
}
|
|
67
|
+
|
|
42
68
|
function rowsContainEllipsis(rows: DesktopPaneShotRenderedRow[]): boolean {
|
|
43
69
|
return rows.some((row) => row.cells.some((cell) => /\u2026|\.\.\./.test(cell.text)));
|
|
44
70
|
}
|
|
@@ -215,7 +241,7 @@ export function buildDomPaneReportFromRender(
|
|
|
215
241
|
} else if (lockNote) {
|
|
216
242
|
textLines.push(...lockNoteLines(lockNote));
|
|
217
243
|
} else if (result.render.visibleText) {
|
|
218
|
-
textLines.push(gate === "sign-in" ? signInWallText(result.render.visibleText) : result.render.
|
|
244
|
+
textLines.push(...(gate === "sign-in" ? [signInWallText(result.render.visibleText)] : proseLines(result.render, resolved.label)));
|
|
219
245
|
} else {
|
|
220
246
|
textLines.push(failureReason ?? "No rendered values were available.");
|
|
221
247
|
}
|
|
@@ -231,6 +257,8 @@ export function buildDomPaneReportFromRender(
|
|
|
231
257
|
const complete = failureReason === null && !truncated;
|
|
232
258
|
// Each table published its rows and nothing else was cut: the report is the whole view.
|
|
233
259
|
const everyRow = complete && result.render.reportRows !== undefined;
|
|
260
|
+
// A reader grown to fit its page (a docs page) and not cut once grown is read whole.
|
|
261
|
+
const wholePane = complete && !everyRow && resolved.pane.shotFitsReader === true;
|
|
234
262
|
return {
|
|
235
263
|
data: {
|
|
236
264
|
kind: "rendered-view",
|
|
@@ -247,7 +275,8 @@ export function buildDomPaneReportFromRender(
|
|
|
247
275
|
truncated,
|
|
248
276
|
truncationReasons,
|
|
249
277
|
...(everyRow ? { everyRow } : {}),
|
|
250
|
-
|
|
278
|
+
...(wholePane ? { wholePane } : {}),
|
|
279
|
+
limitation: everyRow ? DOM_EVERY_ROW : wholePane ? DOM_WHOLE_PANE : DOM_LIMITATION,
|
|
251
280
|
...(lockReason ?? failureReason ? { reason: lockReason ?? failureReason } : {}),
|
|
252
281
|
...(notices.length > 0 ? { metadata: { ...rendered.facts, notices } } : {}),
|
|
253
282
|
...(locked ? { locked } : { freshness }),
|
|
@@ -278,12 +307,12 @@ export function appendDomReportFooter(
|
|
|
278
307
|
].join("\n");
|
|
279
308
|
}
|
|
280
309
|
|
|
281
|
-
/** How much of the pane a rendered-view report holds: none behind a wall, cut, every row its tables publish, or what the viewport shows. */
|
|
282
|
-
export type DomReportView = "locked" | "clipped" | "every-row" | "visible";
|
|
310
|
+
/** How much of the pane a rendered-view report holds: none behind a wall, cut, every row its tables publish, all of a pane grown to fit, or what the viewport shows. */
|
|
311
|
+
export type DomReportView = "locked" | "clipped" | "every-row" | "whole" | "visible";
|
|
283
312
|
|
|
284
313
|
export function domReportView(data: Readonly<Record<string, unknown>>): DomReportView {
|
|
285
314
|
if (data.locked) return "locked";
|
|
286
|
-
return data.truncated === true ? "clipped" : data.everyRow === true ? "every-row" : "visible";
|
|
315
|
+
return data.truncated === true ? "clipped" : data.everyRow === true ? "every-row" : data.wholePane === true ? "whole" : "visible";
|
|
287
316
|
}
|
|
288
317
|
|
|
289
318
|
/** What a rendered-view report says about where its values come from. */
|
|
@@ -292,6 +321,8 @@ export function domReportNote(elapsedMs: number, view: DomReportView): string {
|
|
|
292
321
|
? "Rendered view: the pane is locked, so no values were read."
|
|
293
322
|
: view === "every-row"
|
|
294
323
|
? "Rendered view: every row of the pane's table, including rows scrolled out of view."
|
|
324
|
+
: view === "whole"
|
|
325
|
+
? "Rendered view: the pane grown to fit the whole page."
|
|
295
326
|
: `Rendered view: values come from the visible pane. ${view === "clipped" ? "The rendered view is clipped." : "The rendered view may be clipped."}`;
|
|
296
327
|
return `${source} Render time: ${elapsedMs} ms.`;
|
|
297
328
|
}
|
|
@@ -356,6 +387,8 @@ export async function buildDomFunctionReport(
|
|
|
356
387
|
theme: null,
|
|
357
388
|
scale: 1,
|
|
358
389
|
watermark: null,
|
|
390
|
+
fitReader: true,
|
|
391
|
+
maxHeightCells: DOM_REPORT_MAX_HEIGHT_CELLS,
|
|
359
392
|
options: {},
|
|
360
393
|
captureImage: false,
|
|
361
394
|
});
|
|
@@ -10,8 +10,8 @@ import type {
|
|
|
10
10
|
} from "../../types/plugin";
|
|
11
11
|
import { localTimeSuffix, parseReportTime, type ReportTime } from "../../utils/utc-time";
|
|
12
12
|
import { isRecord } from "../../utils/guards";
|
|
13
|
-
import { latestRegularSessionClose } from "../../market-data/market/freshness";
|
|
14
|
-
import { reportMarketSession, type ReportMarket, type SessionObservation } from "./market-session";
|
|
13
|
+
import { latestRegularSessionClose, readIndexSession } from "../../market-data/market/freshness";
|
|
14
|
+
import { reportMarketSession, reportVenueSessionDates, type ReportMarket, type SessionObservation, type VenueSessionDate } from "./market-session";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* The four states of docs/usage.md#how-current-a-report-is, plus `unreported`
|
|
@@ -53,6 +53,12 @@ export interface ReportFreshness {
|
|
|
53
53
|
tradingDayMarket?: string;
|
|
54
54
|
/** Where the venues of a quote report stand now, from their session calendars. */
|
|
55
55
|
market?: ReportMarket;
|
|
56
|
+
/**
|
|
57
|
+
* With a declared `datesEachVenue`, when the rows trade on more than one
|
|
58
|
+
* market: each market's session date, which the status line gives in place
|
|
59
|
+
* of the newest row's alone.
|
|
60
|
+
*/
|
|
61
|
+
venueSessions?: VenueSessionDate[];
|
|
56
62
|
/**
|
|
57
63
|
* For a dataset (declared `dataset`, with no quote among its rows): whole UTC
|
|
58
64
|
* days from the last day its newest observation covers to when this report
|
|
@@ -76,6 +82,8 @@ export interface ReportFreshness {
|
|
|
76
82
|
oldestAgeMinutes?: number;
|
|
77
83
|
/** What the figures are, said after the status: `funding = current estimate, pays 08:00 UTC`. */
|
|
78
84
|
note?: string;
|
|
85
|
+
/** When the figures reach readers and when the next are due, said right after the as-of date. */
|
|
86
|
+
schedule?: string;
|
|
79
87
|
/** Who published the figures, when the data names them. */
|
|
80
88
|
publisher?: string;
|
|
81
89
|
/** When the publisher released the data, an ISO instant in UTC or an ISO date, when the data states it. */
|
|
@@ -145,10 +153,12 @@ function readFeedSignals(record: Record<string, unknown>, signals: FeedSignals):
|
|
|
145
153
|
function readSession(record: Record<string, unknown>, time: ReportTime | null, signals: FeedSignals): void {
|
|
146
154
|
const exchange = record.sessionExchange;
|
|
147
155
|
if (typeof exchange !== "string" || !exchange.trim()) return;
|
|
156
|
+
const calendar = readIndexSession(record.sessionCalendar);
|
|
148
157
|
signals.sessions.push({
|
|
149
158
|
exchange,
|
|
150
159
|
...(typeof record.marketState === "string" ? { marketState: record.marketState } : {}),
|
|
151
160
|
time,
|
|
161
|
+
...(calendar ? { calendar } : {}),
|
|
152
162
|
});
|
|
153
163
|
}
|
|
154
164
|
|
|
@@ -404,6 +414,7 @@ function resolveFreshness({ declared, signals, observations, now }: FreshnessInp
|
|
|
404
414
|
}
|
|
405
415
|
}
|
|
406
416
|
const { market, closeDate, closeMarket } = reportMarketSession(signals.sessions, now);
|
|
417
|
+
const venueSessions = declared.datesEachVenue && !declaredAsOf ? reportVenueSessionDates(signals.sessions, now) : [];
|
|
407
418
|
// The close wording dates the newest observation, so only when that is what `asOf` is.
|
|
408
419
|
const asOfClose = closeDate && asOf && !declaredAsOf ? closeDate : null;
|
|
409
420
|
const tradingDayMarket = asOfClose ? closeMarket
|
|
@@ -417,6 +428,7 @@ function resolveFreshness({ declared, signals, observations, now }: FreshnessInp
|
|
|
417
428
|
const observation = lagDays != null ? declared.observation?.trim() : undefined;
|
|
418
429
|
|
|
419
430
|
const note = declared.note?.trim();
|
|
431
|
+
const schedule = declared.schedule?.trim();
|
|
420
432
|
const publisher = declared.publisher?.trim();
|
|
421
433
|
const publishedAt = parseReportTime(declared.publishedAt);
|
|
422
434
|
return {
|
|
@@ -434,11 +446,13 @@ function resolveFreshness({ declared, signals, observations, now }: FreshnessInp
|
|
|
434
446
|
...(asOfClose ? { asOfClose } : {}),
|
|
435
447
|
...(tradingDayMarket ? { tradingDayMarket } : {}),
|
|
436
448
|
...(market ? { market } : {}),
|
|
449
|
+
...(venueSessions.length > 0 ? { venueSessions } : {}),
|
|
437
450
|
...(lagDays != null ? { lagDays } : {}),
|
|
438
451
|
...(observation ? { observation } : {}),
|
|
439
452
|
...(sources.length ? { sources: sources.map(({ source, time }) => ({ source, asOf: isoTime(time) })) } : {}),
|
|
440
453
|
...(oldestAgeMinutes != null ? { oldestAgeMinutes } : {}),
|
|
441
454
|
...(note ? { note } : {}),
|
|
455
|
+
...(schedule ? { schedule } : {}),
|
|
442
456
|
...(publisher ? { publisher } : {}),
|
|
443
457
|
...(publishedAt ? { publishedAt: isoTime(publishedAt) } : {}),
|
|
444
458
|
retrievedAt: new Date(now).toISOString(),
|
|
@@ -628,6 +642,26 @@ function onRetrievalDay(iso: string, retrievedAt: string): boolean {
|
|
|
628
642
|
return iso.slice(0, 10) === retrievedAt.slice(0, 10);
|
|
629
643
|
}
|
|
630
644
|
|
|
645
|
+
/** `US and XETRA`, `US, XETRA and EPA`. */
|
|
646
|
+
function listText(items: readonly string[]): string {
|
|
647
|
+
return items.length <= 1 ? items.join("") : `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Each market's session date, markets on the same day and state said once:
|
|
652
|
+
* `US, XETRA and EPA Tue 13 Oct 2026 close`, or `US Tue 13 Oct 2026 close,
|
|
653
|
+
* XETRA Wed 14 Oct 2026 session` while Xetra trades.
|
|
654
|
+
*/
|
|
655
|
+
function venueSessionsText(sessions: readonly VenueSessionDate[]): string {
|
|
656
|
+
const groups: Array<{ markets: string[]; date: string; close: boolean }> = [];
|
|
657
|
+
for (const { market, date, close } of sessions) {
|
|
658
|
+
const group = groups.find((entry) => entry.date === date && entry.close === close);
|
|
659
|
+
if (group) group.markets.push(market);
|
|
660
|
+
else groups.push({ markets: [market], date, close });
|
|
661
|
+
}
|
|
662
|
+
return groups.map(({ markets, date, close }) => `${listText(markets)} ${dayText(date)} ${close ? "close" : "session"}`).join(", ");
|
|
663
|
+
}
|
|
664
|
+
|
|
631
665
|
/**
|
|
632
666
|
* The dated as-of: a session close, a UTC time, a date, or when the report was
|
|
633
667
|
* retrieved; with the oldest observation unless `short`, or when the line says
|
|
@@ -636,6 +670,7 @@ function onRetrievalDay(iso: string, retrievedAt: string): boolean {
|
|
|
636
670
|
*/
|
|
637
671
|
function asOfText(freshness: ReportFreshness, short: boolean, local: boolean, undated = false): string {
|
|
638
672
|
if (!freshness.asOf) return `retrieved ${timeText(freshness.retrievedAt, local)}`;
|
|
673
|
+
if (freshness.venueSessions?.length) return venueSessionsText(freshness.venueSessions);
|
|
639
674
|
const market = freshness.tradingDayMarket;
|
|
640
675
|
const period = freshness.periodicity;
|
|
641
676
|
const time = parseReportTime(freshness.asOf);
|
|
@@ -713,6 +748,8 @@ interface StatusLineDetail {
|
|
|
713
748
|
date?: "none";
|
|
714
749
|
/** Leave out what the figures are (`funding = current estimate`). */
|
|
715
750
|
note?: "none";
|
|
751
|
+
/** Leave out when the figures are published and the next are due. */
|
|
752
|
+
schedule?: "none";
|
|
716
753
|
}
|
|
717
754
|
|
|
718
755
|
/**
|
|
@@ -728,6 +765,7 @@ function statusLineParts(freshness: ReportFreshness, detail: StatusLineDetail =
|
|
|
728
765
|
return [
|
|
729
766
|
...(freshness.sources?.length ? sourcesAsOfParts(freshness, freshness.sources, undated)
|
|
730
767
|
: [asOfText(freshness, detail.asOf === "short", local, undated)]),
|
|
768
|
+
...(freshness.schedule && detail.schedule !== "none" ? [freshness.schedule] : []),
|
|
731
769
|
...(freshness.oldestAgeMinutes != null ? [`oldest ${ageText(freshness.oldestAgeMinutes)}`] : []),
|
|
732
770
|
statusText(freshness, detail.status === "short"),
|
|
733
771
|
...(freshness.market && detail.market !== "none" ? [marketText(freshness.market, now, detail.market === "short", local)] : []),
|
|
@@ -812,13 +850,13 @@ export function paneStatusLine(freshness: ReportFreshness): string {
|
|
|
812
850
|
/**
|
|
813
851
|
* `paneStatusLine` from longest to shortest, for a footer to show the first
|
|
814
852
|
* that fits beside its keys: the market part shortens, the stale total goes,
|
|
815
|
-
* then what the figures are.
|
|
853
|
+
* then what the figures are, then when they are published.
|
|
816
854
|
*/
|
|
817
855
|
export function paneStatusLineVariants(freshness: ReportFreshness): string[] {
|
|
818
856
|
// A dataset's line has no market or local time to leave out: it reads as its report and capture do.
|
|
819
857
|
if (datesObservation(freshness)) return statusLineVariants(freshness);
|
|
820
858
|
const pane: StatusLineDetail = { local: "none", date: "none" };
|
|
821
|
-
const details: StatusLineDetail[] = [pane, { ...pane, market: "short" }, { ...pane, market: "short", status: "short" }, { ...pane, market: "short", status: "short", note: "none" }];
|
|
859
|
+
const details: StatusLineDetail[] = [pane, { ...pane, market: "short" }, { ...pane, market: "short", status: "short" }, { ...pane, market: "short", status: "short", note: "none" }, { ...pane, market: "short", status: "short", note: "none", schedule: "none" }];
|
|
822
860
|
return [...new Set(details.map((detail) => capitalize(statusLineParts(freshness, detail).join(" · "))))];
|
|
823
861
|
}
|
|
824
862
|
|
|
@@ -855,9 +893,14 @@ export function statusLineVariants(freshness: ReportFreshness, { source = false
|
|
|
855
893
|
// A line that dates each source keeps every source's time and their age to
|
|
856
894
|
// the last: its day goes first, then what the figures are.
|
|
857
895
|
const shortest = details.at(-1)!;
|
|
896
|
+
// What the figures are goes before when they are published.
|
|
897
|
+
const trailing: StatusLineDetail[] = [
|
|
898
|
+
...(freshness.note ? [{ ...shortest, note: "none" as const }] : []),
|
|
899
|
+
...(freshness.schedule ? [{ ...shortest, note: "none" as const, schedule: "none" as const }] : []),
|
|
900
|
+
];
|
|
858
901
|
const variants: StatusLineDetail[] = freshness.sources?.length
|
|
859
902
|
? [{}, ...details.map((detail) => ({ ...detail, date: "none" as const })), { ...shortest, date: "none", note: "none" }]
|
|
860
|
-
:
|
|
903
|
+
: [...details, ...trailing];
|
|
861
904
|
const lines = [...new Set(variants.map((detail) => capitalize(statusLineParts(freshness, detail).join(" · "))))];
|
|
862
905
|
// A line that dates each source names them already.
|
|
863
906
|
if (!source || freshness.sources?.length) return lines;
|
|
@@ -749,7 +749,7 @@ export const FUNCTION_HELP: Readonly<Record<string, FunctionHelp>> = {
|
|
|
749
749
|
bloomberg: [],
|
|
750
750
|
},
|
|
751
751
|
FUT: {
|
|
752
|
-
summary: "
|
|
752
|
+
summary: "Each root's most active futures contract across equity index, equity sectors, rates, energy, metals, agriculture, livestock and FX with last price, unit and session change, grouped by sector.",
|
|
753
753
|
usage: ["FUT"],
|
|
754
754
|
keys: [SEARCH, OPEN],
|
|
755
755
|
data: same(DELAYED),
|