gloomberb 0.18.2 → 0.18.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +106 -5
- package/src/api-client/data.ts +15 -0
- package/src/api-client/index.ts +2 -0
- package/src/api-client/request.ts +5 -3
- package/src/api-client/telemetry.ts +17 -0
- package/src/app/global-shortcuts.ts +1 -16
- package/src/app/runtime/update.ts +28 -8
- package/src/architecture/pane-tabs-allowlist.json +65 -0
- package/src/cli/commands/market.ts +73 -33
- package/src/cli/commands/ticker.ts +82 -25
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +8 -0
- package/src/cli/issuer-membership.ts +103 -0
- package/src/cli/listing-arg.ts +15 -2
- package/src/cli/name-entries.ts +51 -12
- package/src/cli/pane-functions/capabilities.ts +2 -0
- package/src/cli/pane-functions/catalog.ts +16 -0
- package/src/cli/pane-functions/dom.ts +2 -1
- package/src/cli/pane-functions/headless.ts +25 -6
- package/src/cli/pane-functions/index.ts +64 -5
- package/src/cli/pane-functions/resolver.ts +19 -5
- package/src/cli/report-tables.ts +136 -18
- package/src/cli/result.ts +12 -4
- package/src/cli/symbol-list.ts +47 -11
- package/src/components/chart/composite/composite-chart.tsx +14 -5
- package/src/components/chart/composite/drawing-store.ts +26 -6
- package/src/components/chart/composite/interactions.ts +2 -1
- package/src/components/chart/composite/panel-surface.tsx +11 -0
- package/src/components/chart/composite/types.ts +6 -0
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +1 -1
- package/src/components/command-bar/routes/root/command-items.ts +32 -6
- package/src/components/data-table/view.tsx +23 -3
- package/src/components/index.ts +1 -0
- package/src/components/layout/detached-pane-shell.tsx +1 -0
- package/src/components/layout/floating-pane.tsx +1 -0
- package/src/components/layout/header.tsx +228 -55
- package/src/components/layout/pane/header.tsx +10 -3
- package/src/components/layout/pane/index.tsx +1 -0
- package/src/components/layout/pane/pane-tabs.tsx +9 -1
- package/src/components/ui/stat-grid.tsx +28 -3
- package/src/components/ui/status.tsx +3 -1
- package/src/core/app-services.ts +4 -0
- package/src/i18n/es.ts +22 -0
- package/src/i18n/ja.ts +22 -0
- package/src/i18n/ko.ts +22 -0
- package/src/i18n/zh-cn.ts +22 -0
- package/src/i18n/zh-tw.ts +22 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +3 -1
- package/src/plugins/builtin/alerts/events.ts +1 -0
- package/src/plugins/builtin/alerts/research-rules.ts +1 -0
- package/src/plugins/builtin/analytics/risk-pane.tsx +10 -7
- package/src/plugins/builtin/attention/pane.tsx +16 -14
- package/src/plugins/builtin/awards/pane.tsx +6 -6
- package/src/plugins/builtin/backtest/export.ts +1 -1
- package/src/plugins/builtin/broker-trading/dock.tsx +4 -2
- package/src/plugins/builtin/broker-trading/pane.tsx +6 -4
- package/src/plugins/builtin/broker-trading/ticket.tsx +10 -5
- package/src/plugins/builtin/buildout/pane/index.tsx +9 -6
- package/src/plugins/builtin/catalysts/client.ts +2 -2
- package/src/plugins/builtin/catalysts/pane.tsx +3 -3
- package/src/plugins/builtin/chart-composer/pane.tsx +1 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +3 -4
- package/src/plugins/builtin/company-attention/app-rank.tsx +4 -5
- package/src/plugins/builtin/company-attention/pane.tsx +13 -11
- package/src/plugins/builtin/company-kpis/pane.tsx +4 -3
- package/src/plugins/builtin/correlation/headless.ts +2 -1
- package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
- package/src/plugins/builtin/correlation/settings.ts +1 -0
- package/src/plugins/builtin/cot/pane.tsx +4 -3
- package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
- package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
- package/src/plugins/builtin/doe/pane.tsx +1 -1
- package/src/plugins/builtin/equity-screener/headless.ts +10 -0
- package/src/plugins/builtin/equity-screener/model.ts +28 -0
- package/src/plugins/builtin/equity-screener/pane.tsx +105 -98
- package/src/plugins/builtin/exposure/pane.tsx +10 -9
- package/src/plugins/builtin/gpu/board.tsx +4 -1
- package/src/plugins/builtin/gpu/changes.tsx +10 -10
- package/src/plugins/builtin/gpu/equities.tsx +4 -1
- package/src/plugins/builtin/gpu/history.tsx +4 -2
- package/src/plugins/builtin/gpu/pane.tsx +5 -3
- package/src/plugins/builtin/holders/client.ts +2 -1
- package/src/plugins/builtin/holders/headless.ts +2 -1
- package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
- package/src/plugins/builtin/insider/base-values.ts +11 -7
- package/src/plugins/builtin/insider/client.ts +3 -0
- package/src/plugins/builtin/insider/headless.ts +19 -6
- package/src/plugins/builtin/insider/index.tsx +17 -8
- package/src/plugins/builtin/insider/issuer.ts +34 -4
- package/src/plugins/builtin/insider/signals-client.ts +2 -2
- package/src/plugins/builtin/insider/table-model.ts +35 -13
- package/src/plugins/builtin/insider/table.tsx +5 -2
- package/src/plugins/builtin/iv-history/events.ts +145 -0
- package/src/plugins/builtin/iv-history/format.ts +3 -0
- package/src/plugins/builtin/iv-history/headless.ts +91 -7
- package/src/plugins/builtin/jodi/pane.tsx +4 -4
- package/src/plugins/builtin/market-movers/client.ts +4 -3
- package/src/plugins/builtin/market-movers/screener.ts +4 -3
- package/src/plugins/builtin/notes/cli.ts +1 -1
- package/src/plugins/builtin/notes/export.ts +17 -6
- package/src/plugins/builtin/notes/files.ts +16 -5
- package/src/plugins/builtin/options/enrichment-model.ts +10 -9
- package/src/plugins/builtin/options/model-spot.ts +70 -4
- package/src/plugins/builtin/options/view.tsx +41 -13
- package/src/plugins/builtin/options-calculator/pane.tsx +3 -2
- package/src/plugins/builtin/options-positioning/headless.ts +11 -3
- package/src/plugins/builtin/options-positioning/model.ts +24 -0
- package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
- package/src/plugins/builtin/options-scenario/client.ts +19 -1
- package/src/plugins/builtin/options-scenario/headless.ts +13 -6
- package/src/plugins/builtin/options-scenario/pane.tsx +13 -5
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +5 -4
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
- package/src/plugins/builtin/power/client.ts +2 -2
- package/src/plugins/builtin/power/pane.tsx +36 -31
- package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
- package/src/plugins/builtin/reverse-dcf/model.ts +4 -3
- package/src/plugins/builtin/seasonality/client.ts +2 -1
- package/src/plugins/builtin/sectors/client.ts +4 -3
- package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
- package/src/plugins/builtin/shared/research-trading.tsx +2 -0
- package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
- package/src/plugins/builtin/shared/volatility/options.ts +2 -1
- package/src/plugins/builtin/social-mentions/client.ts +2 -1
- package/src/plugins/builtin/vol-surface/client.ts +23 -3
- package/src/plugins/builtin/vol-surface/headless.ts +33 -14
- package/src/plugins/builtin/vol-surface/model.ts +30 -10
- package/src/plugins/builtin/vol-surface/pane.tsx +33 -13
- package/src/plugins/builtin/vol-surface/stored.ts +34 -3
- package/src/plugins/builtin/vol-surface/tables.ts +14 -0
- package/src/renderers/dom/desktop/controls.tsx +1 -1
- package/src/renderers/dom/desktop/query-bar.tsx +2 -0
- package/src/renderers/dom/host/tabs.tsx +16 -5
- package/src/renderers/dom/host/top-surface.ts +127 -0
- package/src/renderers/dom/styles.css +39 -0
- package/src/renderers/electrobun/bun/core-capabilities.ts +9 -1
- package/src/renderers/electrobun/bun/desktop/backend.ts +10 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
- package/src/renderers/electrobun/view/news-source.ts +4 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +8 -1
- package/src/renderers/opentui/start.tsx +7 -0
- package/src/sources/gloomberb-cloud/index.ts +7 -0
- package/src/sources/provider-router/index.ts +16 -0
- package/src/telemetry/crash-reports.ts +4 -0
- package/src/telemetry/update-failures-node.ts +59 -0
- package/src/telemetry/update-failures.ts +307 -0
- package/src/tickers/company-name-listing.ts +9 -7
- package/src/tickers/company-name.ts +43 -0
- package/src/tickers/listing-fate.ts +30 -0
- package/src/types/data-provider.ts +6 -0
- package/src/types/headless.ts +7 -0
- package/src/types/issuer-listings.ts +28 -0
- package/src/updater/failure.ts +163 -0
- package/src/updater/index.ts +87 -23
- package/src/utils/currency-pair.ts +26 -0
- package/src/utils/sec.ts +68 -0
|
@@ -36,7 +36,8 @@ import {
|
|
|
36
36
|
import type { FuturesContractTermsPayload } from "../../api-client/futures-curve";
|
|
37
37
|
import { changeBasisJson, quoteChangeBasis, quotePreviousCloseDate } from "../../market-data/market/change-basis";
|
|
38
38
|
import { futuresContractLines, futuresSettleText, isFuturesInstrument, loadFuturesContractTerms, minorUnitPricePer } from "../../market-data/futures-contract";
|
|
39
|
-
import { currencyCodeLabel } from "../../utils/currency-units";
|
|
39
|
+
import { currencyCodeLabel, heldCurrencyUnit } from "../../utils/currency-units";
|
|
40
|
+
import { quotedFxPair } from "../../utils/currency-pair";
|
|
40
41
|
import type { AppConfig } from "../../types/config";
|
|
41
42
|
import type { DeclaredDividendAction, FinancialStatement, Quote, TickerFinancials } from "../../types/financials";
|
|
42
43
|
import { atRegularSessionPrice, regularSessionValuationPrice } from "../../market-data/quotes/regular-session-valuation";
|
|
@@ -73,6 +74,7 @@ import {
|
|
|
73
74
|
resolveCliListing, type CliListing, type ListingDataRequest,
|
|
74
75
|
} from "../listing-arg";
|
|
75
76
|
import { movedAwayChange } from "../../tickers/ticker-changes";
|
|
77
|
+
import { companyMemberships, loadCompanyMemberships, type CompanyMemberships, type ListMembership } from "../issuer-membership";
|
|
76
78
|
import { isNotATickerMessage } from "../not-a-ticker";
|
|
77
79
|
import { sharesOutstandingInReceipts } from "../../utils/depositary-receipt";
|
|
78
80
|
import { cliFreshnessFooter } from "../result";
|
|
@@ -301,7 +303,7 @@ function shouldFetchSecFilings(
|
|
|
301
303
|
}
|
|
302
304
|
|
|
303
305
|
async function appendTickerPositions(lines: string[], tickerFile: TickerRecord | null, quote: TickerFinancials["quote"],
|
|
304
|
-
config: AppConfig, toBase: ReportBaseConverter): Promise<void> {
|
|
306
|
+
config: AppConfig, toBase: ReportBaseConverter, rates: ReportRateNotes): Promise<void> {
|
|
305
307
|
const quoteCurrency = quote?.currency?.trim() || tickerFile?.metadata.currency?.trim() || "";
|
|
306
308
|
const codes = writesCurrencyCodes(config);
|
|
307
309
|
const money = codes ? formatCurrencyCode : formatCurrency;
|
|
@@ -324,9 +326,10 @@ async function appendTickerPositions(lines: string[], tickerFile: TickerRecord |
|
|
|
324
326
|
if (quoteCurrency && currentPrice != null) converted.add(quoteCurrency.toUpperCase());
|
|
325
327
|
const baseMetrics = getPortfolioPositionMetrics({ ...tickerFile, metadata: { ...tickerFile.metadata, positions: [position] } }, undefined, quoteCurrency,
|
|
326
328
|
{ currency: config.baseCurrency, convert: value => value * positionRate }, quote);
|
|
327
|
-
// A cost bought at an entered rate into the base currency converts at it, as its P&L does; any other at today's.
|
|
329
|
+
// A cost bought at an entered rate into the base currency converts at it, as its P&L does, and says so; any other at today's.
|
|
330
|
+
const boughtRate = positionCurrency ? lotCostFxRate(position, positionCurrency, config.baseCurrency) : null;
|
|
328
331
|
const costBasisBase = !positionCurrency ? Number.NaN
|
|
329
|
-
:
|
|
332
|
+
: boughtRate !== null ? baseMetrics.signedCost
|
|
330
333
|
: await toBase(metrics.signedCost, positionCurrency);
|
|
331
334
|
const marketValueBase = resolvePortfolioMarketValue(baseMetrics, currentPriceBase)?.net ?? Number.NaN;
|
|
332
335
|
const selectedPnl = resolvePortfolioPositionPnl(baseMetrics, currentPriceBase);
|
|
@@ -339,7 +342,8 @@ async function appendTickerPositions(lines: string[], tickerFile: TickerRecord |
|
|
|
339
342
|
"Position",
|
|
340
343
|
`${formatMarketQuantity(metrics.totalShares, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis, quantityCurrency: positionCurrency })} ${metrics.priceBasis === "percent-of-par" ? "@" : `${tickerFile.metadata.assetCategory === "BOND" ? "units" : contracts ? "contracts" : "shares"} @`} ${positionCurrency ? formatMarketCostWithCurrency(position.avgCost, positionCurrency, { assetCategory: tickerFile.metadata.assetCategory, multiplier: position.multiplier, priceBasis: metrics.priceBasis, currencyCode: codes }) : "—"}`,
|
|
341
344
|
],
|
|
342
|
-
["Cost Basis", money(costBasisBase, config.baseCurrency)
|
|
345
|
+
["Cost Basis", `${money(costBasisBase, config.baseCurrency)}${boughtRate !== null
|
|
346
|
+
? ` (${formatFxRate(boughtRate)} ${config.baseCurrency.trim().toUpperCase()} per ${positionCurrency} when bought)` : ""}`],
|
|
343
347
|
["Market Value", money(marketValueBase, config.baseCurrency)],
|
|
344
348
|
[
|
|
345
349
|
selectedPnl.basis === "broker-snapshot" ? "Broker P&L" : "P&L",
|
|
@@ -354,7 +358,7 @@ async function appendTickerPositions(lines: string[], tickerFile: TickerRecord |
|
|
|
354
358
|
lines.push("");
|
|
355
359
|
}
|
|
356
360
|
}
|
|
357
|
-
for (const note of conversionNotes(toBase, converted)) lines.push(cliStyles.muted(note));
|
|
361
|
+
for (const note of conversionNotes(toBase, converted, rates)) lines.push(cliStyles.muted(note));
|
|
358
362
|
}
|
|
359
363
|
}
|
|
360
364
|
|
|
@@ -372,14 +376,41 @@ export function writesCurrencyCodes(config: Pick<AppConfig, "baseCurrency"> | un
|
|
|
372
376
|
}
|
|
373
377
|
|
|
374
378
|
/**
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
379
|
+
* What the report's rate notes have said and must say: each currency's rate
|
|
380
|
+
* is stated once, under the first section that converted it, and a lot whose
|
|
381
|
+
* cost converts at the rate entered when it was bought is left out of it.
|
|
378
382
|
*/
|
|
379
|
-
|
|
383
|
+
interface ReportRateNotes {
|
|
384
|
+
/** Currencies whose rate a note already gave. */
|
|
385
|
+
noted: Set<string>;
|
|
386
|
+
/** Currencies of lots whose Cost Basis is at their own entered rate. */
|
|
387
|
+
boughtAt: ReadonlySet<string>;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** The currencies of the shown lots whose cost converts into the base currency at the rate they were bought at. */
|
|
391
|
+
function boughtRateCurrencies(tickerFile: TickerRecord | null, quoteCurrency: string, baseCurrency: string): Set<string> {
|
|
392
|
+
return new Set((tickerFile?.metadata.positions ?? []).flatMap((position) => {
|
|
393
|
+
const currency = (position.currency || quoteCurrency).trim();
|
|
394
|
+
return position.shares !== 0 && currency && lotCostFxRate(position, currency, baseCurrency) !== null ? [currency.toUpperCase()] : [];
|
|
395
|
+
}));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* "CAD figures at USD/CAD 1.40623, as of 2026-10-21 04:46 UTC.": the rate
|
|
400
|
+
* and its date for each currency in `currencies` the report converted into
|
|
401
|
+
* the base currency, as markets quote the pair (USD/JPY, not JPY/USD), so a
|
|
402
|
+
* converted figure is never without what it was converted at. A rate already
|
|
403
|
+
* stated is not repeated, and a cost basis at its own rate is excepted.
|
|
404
|
+
*/
|
|
405
|
+
function conversionNotes(toBase: ReportBaseConverter, currencies: ReadonlySet<string>, rates: ReportRateNotes): string[] {
|
|
380
406
|
return (toBase.conversions?.() ?? [])
|
|
381
|
-
.filter((conversion) => currencies.has(conversion.currency) && conversion.currency !== conversion.baseCurrency)
|
|
382
|
-
.map(({ currency, baseCurrency, rate, asOf }) =>
|
|
407
|
+
.filter((conversion) => currencies.has(conversion.currency) && conversion.currency !== conversion.baseCurrency && !rates.noted.has(conversion.currency))
|
|
408
|
+
.map(({ currency, baseCurrency, rate, asOf }) => {
|
|
409
|
+
rates.noted.add(currency);
|
|
410
|
+
const quoted = quotedFxPair(currency, baseCurrency, rate);
|
|
411
|
+
const except = rates.boughtAt.has(currency) ? ", except a cost basis at the rate it was bought at" : "";
|
|
412
|
+
return `${baseCurrency} figures at ${quoted.pair} ${formatFxRate(quoted.rate)}${asOf != null ? `, as of ${formatTimestamp(asOf)}` : ""}${except}.`;
|
|
413
|
+
});
|
|
383
414
|
}
|
|
384
415
|
|
|
385
416
|
/** A source's enterprise value is in the units of the capitalization it reports beside it,
|
|
@@ -947,8 +978,11 @@ export async function buildTickerReport({
|
|
|
947
978
|
distributions = null,
|
|
948
979
|
statementFilings = null,
|
|
949
980
|
bondFund = null,
|
|
981
|
+
memberships,
|
|
950
982
|
}: {
|
|
951
983
|
symbol: string;
|
|
984
|
+
/** The lists holding the company, by any of its listings (loadCompanyMemberships); the saved listing's own lists when absent. */
|
|
985
|
+
memberships?: CompanyMemberships;
|
|
952
986
|
/** The filings behind the latest statements, and a newer quarter's results release. */
|
|
953
987
|
statementFilings?: StatementFilingsReport | null;
|
|
954
988
|
/** A bond fund's SEC yield, yield to worst and duration (loadReportBondFacts). */
|
|
@@ -981,6 +1015,10 @@ export async function buildTickerReport({
|
|
|
981
1015
|
const baseQuoteOptions = quoteFormatOptions(quote, tickerFile?.metadata.assetCategory, financials.quoteMetadata?.instrumentType);
|
|
982
1016
|
const baseCurrency = config.baseCurrency.trim().toUpperCase();
|
|
983
1017
|
const codes = writesCurrencyCodes(config);
|
|
1018
|
+
const rates: ReportRateNotes = {
|
|
1019
|
+
noted: new Set(),
|
|
1020
|
+
boughtAt: boughtRateCurrencies(tickerFile, quote?.currency?.trim() || tickerFile?.metadata.currency?.trim() || "", config.baseCurrency),
|
|
1021
|
+
};
|
|
984
1022
|
// Pad money prices to the currency's minor unit so a range reads £35.10 - £35.485, never past it (JPY has none).
|
|
985
1023
|
const quoteOptions = withCurrencyMinorDigits(codes ? { ...baseQuoteOptions, currencyCode: true } : baseQuoteOptions, quote?.currency);
|
|
986
1024
|
const lines: string[] = [];
|
|
@@ -1015,14 +1053,15 @@ export async function buildTickerReport({
|
|
|
1015
1053
|
lines.push(cliStyles.muted(metadataParts.join(METADATA_SEPARATOR)));
|
|
1016
1054
|
}
|
|
1017
1055
|
|
|
1018
|
-
|
|
1019
|
-
const
|
|
1056
|
+
// A list that holds the company as another of its listings says which: "Watchlists R (as RCI.B:TSX)".
|
|
1057
|
+
const lists = memberships ?? companyMemberships(config, tickerFile, [], null);
|
|
1058
|
+
const listNames = (held: readonly ListMembership[]) => held.map((list) => list.via ? `${list.name} (as ${list.via})` : list.name).join(", ");
|
|
1020
1059
|
const membershipParts = [
|
|
1021
|
-
|
|
1022
|
-
? `Portfolios ${
|
|
1060
|
+
lists.portfolios.length > 0
|
|
1061
|
+
? `Portfolios ${listNames(lists.portfolios)}`
|
|
1023
1062
|
: undefined,
|
|
1024
|
-
|
|
1025
|
-
? `Watchlists ${
|
|
1063
|
+
lists.watchlists.length > 0
|
|
1064
|
+
? `Watchlists ${listNames(lists.watchlists)}`
|
|
1026
1065
|
: undefined,
|
|
1027
1066
|
].filter((part): part is string => !!part);
|
|
1028
1067
|
if (membershipParts.length > 0) {
|
|
@@ -1036,10 +1075,13 @@ export async function buildTickerReport({
|
|
|
1036
1075
|
// with the base currency's figure after them when it differs: 627.2B USD (878.15B CAD).
|
|
1037
1076
|
const convertedCurrencies = new Set<string>();
|
|
1038
1077
|
const inBase = async (value: number, currency: string): Promise<string> => {
|
|
1039
|
-
|
|
1040
|
-
const
|
|
1078
|
+
// A minor unit converts as the hundredth of its currency it is: 2.75T GBp is 27.5B GBP, never 2.75T GBP.
|
|
1079
|
+
const unit = heldCurrencyUnit(currency);
|
|
1080
|
+
if (!unit.currency || (unit.currency === baseCurrency && unit.divisor === 1)) return "";
|
|
1081
|
+
const major = value / unit.divisor;
|
|
1082
|
+
const converted = unit.currency === baseCurrency ? major : await toBase(major, unit.currency);
|
|
1041
1083
|
if (!Number.isFinite(converted)) return "";
|
|
1042
|
-
convertedCurrencies.add(currency
|
|
1084
|
+
if (unit.currency !== baseCurrency) convertedCurrencies.add(unit.currency);
|
|
1043
1085
|
return ` (${formatCompact(converted)} ${baseCurrency})`;
|
|
1044
1086
|
};
|
|
1045
1087
|
const marketCapText = capitalization
|
|
@@ -1048,8 +1090,7 @@ export async function buildTickerReport({
|
|
|
1048
1090
|
// Shown in the same currency as the market cap, so the two can be compared.
|
|
1049
1091
|
const enterpriseValue = reportedEnterpriseValue(priced.fundamentals);
|
|
1050
1092
|
const evCurrency = enterpriseValueCurrency(quote, fundamentals);
|
|
1051
|
-
|
|
1052
|
-
const enterpriseValueText = `${formatReportedMoney(enterpriseValue, evCurrency)}${enterpriseValue != null && evCurrency && /^[A-Z]{3}$/.test(evCurrency)
|
|
1093
|
+
const enterpriseValueText = `${formatReportedMoney(enterpriseValue, evCurrency)}${enterpriseValue != null && evCurrency
|
|
1053
1094
|
? await inBase(enterpriseValue, evCurrency) : ""}`;
|
|
1054
1095
|
|
|
1055
1096
|
if (quote) {
|
|
@@ -1099,7 +1140,7 @@ export async function buildTickerReport({
|
|
|
1099
1140
|
const beforeFundamentals = lines.length;
|
|
1100
1141
|
appendMetricSection(lines, "Fundamentals", metricLines(metrics));
|
|
1101
1142
|
for (const note of fundamentalsNotes(priced, metrics, quote?.currency)) lines.push(cliStyles.muted(note));
|
|
1102
|
-
if (lines.length > beforeFundamentals) for (const note of conversionNotes(toBase, convertedCurrencies)) lines.push(cliStyles.muted(note));
|
|
1143
|
+
if (lines.length > beforeFundamentals) for (const note of conversionNotes(toBase, convertedCurrencies, rates)) lines.push(cliStyles.muted(note));
|
|
1103
1144
|
// Where a fund's facts would go: under its fundamentals, or in their place.
|
|
1104
1145
|
const fund = fundFactsNote(quote?.symbol ?? symbol,
|
|
1105
1146
|
quote?.tradingExchangeName || listingExchange || quote?.listingExchangeName || quote?.exchangeName || tickerFile?.metadata.exchange, financials, tickerFile);
|
|
@@ -1159,7 +1200,7 @@ export async function buildTickerReport({
|
|
|
1159
1200
|
link: filing.filingUrl,
|
|
1160
1201
|
})), hiddenFilingsNote(hiddenSecFilings));
|
|
1161
1202
|
|
|
1162
|
-
await appendTickerPositions(lines, tickerFile, quote, config, toBase);
|
|
1203
|
+
await appendTickerPositions(lines, tickerFile, quote, config, toBase, rates);
|
|
1163
1204
|
|
|
1164
1205
|
return lines.join("\n");
|
|
1165
1206
|
}
|
|
@@ -1178,9 +1219,11 @@ function buildTickerStructuredData({
|
|
|
1178
1219
|
distributions,
|
|
1179
1220
|
statementFilings,
|
|
1180
1221
|
bondFund = null,
|
|
1222
|
+
memberships,
|
|
1181
1223
|
}: {
|
|
1182
1224
|
symbol: string;
|
|
1183
1225
|
listing: CliListing;
|
|
1226
|
+
memberships: CompanyMemberships;
|
|
1184
1227
|
statementFilings: StatementFilingsReport | null;
|
|
1185
1228
|
bondFund?: FundBondFacts | null;
|
|
1186
1229
|
futuresContract: FuturesContractTermsPayload | null;
|
|
@@ -1236,6 +1279,8 @@ function buildTickerStructuredData({
|
|
|
1236
1279
|
watchlists: formatWatchlistNames(config, tickerFile.metadata.watchlists),
|
|
1237
1280
|
positions: tickerFile.metadata.positions,
|
|
1238
1281
|
} : null,
|
|
1282
|
+
// The lists holding the company by any of its listings; `via` names the other listing a list holds it as.
|
|
1283
|
+
memberships,
|
|
1239
1284
|
fundamentals: financials.fundamentals || priceReturns.return1Y != null || priceReturns.return3Y != null ? exportedDividendFundamentals(exportedFundamentals({
|
|
1240
1285
|
...priced.fundamentals,
|
|
1241
1286
|
...priceReturns,
|
|
@@ -1368,6 +1413,15 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
|
|
|
1368
1413
|
quarter: latestFinancialPeriod(resolvedFinancials.quarterlyStatements, (row) => row.date),
|
|
1369
1414
|
annual: latestFinancialPeriod(resolvedFinancials.annualStatements, (row) => row.date),
|
|
1370
1415
|
});
|
|
1416
|
+
// A list holding the company as another of its listings counts (a list of RCI.B:TSX holds RCI); read beside the rest. It never fails.
|
|
1417
|
+
const membershipsLoad = loadCompanyMemberships({
|
|
1418
|
+
config,
|
|
1419
|
+
own: tickerFile,
|
|
1420
|
+
symbol: listing.symbol,
|
|
1421
|
+
exchange: canonicalExchange(listing.exchange || quote?.listingExchangeName || quote?.exchangeName || tickerFile?.metadata.exchange),
|
|
1422
|
+
loadTickers: () => store.loadAllTickers(),
|
|
1423
|
+
dataProvider,
|
|
1424
|
+
});
|
|
1371
1425
|
const [notesResult, newsResult, secFilingsResult] = await Promise.allSettled([
|
|
1372
1426
|
notesFiles.load(tickerFile?.metadata.ticker ?? normalized),
|
|
1373
1427
|
dataProvider.getNews({
|
|
@@ -1401,6 +1455,7 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
|
|
|
1401
1455
|
const coinNote = listing.exchange ? null
|
|
1402
1456
|
: notTheCoinNote(listing.symbol, quote ?? (tickerFile ? { name: tickerFile.metadata.name, instrumentType: tickerFile.metadata.assetCategory } : null));
|
|
1403
1457
|
const listingNote = [choiceNote, coinNote].filter((note): note is string => !!note).join("\n") || undefined;
|
|
1458
|
+
const memberships = await membershipsLoad;
|
|
1404
1459
|
if (dependencies.printResult) {
|
|
1405
1460
|
const warnings = [choiceNote, coinNote, quoteNote].filter((note): note is string => !!note);
|
|
1406
1461
|
dependencies.printResult({
|
|
@@ -1420,6 +1475,7 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
|
|
|
1420
1475
|
distributions: await distributions,
|
|
1421
1476
|
statementFilings: await statementFilings,
|
|
1422
1477
|
bondFund: await bondFund,
|
|
1478
|
+
memberships,
|
|
1423
1479
|
}),
|
|
1424
1480
|
});
|
|
1425
1481
|
return;
|
|
@@ -1442,6 +1498,7 @@ export async function ticker(symbol: string, dependencies: TickerCommandDependen
|
|
|
1442
1498
|
distributions: await distributions,
|
|
1443
1499
|
statementFilings: await statementFilings,
|
|
1444
1500
|
bondFund: await bondFund,
|
|
1501
|
+
memberships,
|
|
1445
1502
|
}));
|
|
1446
1503
|
if (freshness) console.log(`\n${cliFreshnessFooter(freshness)}`);
|
|
1447
1504
|
});
|
package/src/cli/help.ts
CHANGED
|
@@ -58,7 +58,7 @@ const GLOBAL_OPTIONS: Array<[string, string]> = [
|
|
|
58
58
|
|
|
59
59
|
const EXIT_CODES: Array<[string, string]> = [
|
|
60
60
|
[String(CLI_EXIT_CODES.ok), "Success"],
|
|
61
|
-
[String(CLI_EXIT_CODES.error), "Failed: bad input, no data found or a failed check"],
|
|
61
|
+
[String(CLI_EXIT_CODES.error), "Failed: bad input, no data found or a failed check. fn exits 0 on partial data with warnings; --json ok is false exactly when this is 1"],
|
|
62
62
|
[String(CLI_EXIT_CODES.unusableShot), "shot --strict saved an image of a pane with no usable data"],
|
|
63
63
|
];
|
|
64
64
|
|
package/src/cli/index.ts
CHANGED
|
@@ -190,8 +190,16 @@ export function createCoreCliCommands(
|
|
|
190
190
|
"fn WEI --csv --section europe > europe.csv",
|
|
191
191
|
"fn 13F AAPL --view=ticker-holdings",
|
|
192
192
|
"fn OVME --spot 100 --strike 100 --days 30 --volatility 25",
|
|
193
|
+
"fn HIVG SPY --event fomc --offset -6 --lookback ALL --csv",
|
|
193
194
|
"fn AUCT --explain",
|
|
194
195
|
],
|
|
196
|
+
sections: [{
|
|
197
|
+
title: "Exit status",
|
|
198
|
+
lines: [
|
|
199
|
+
"0 when the report has data for what was asked, partial data with warnings about the rest included; 1 when it has none (an empty report, or rows with no value for a listing that has nothing) or the input is bad. --json ok is false exactly when the exit status is 1.",
|
|
200
|
+
"CSV headers are the same for every symbol: a reason a figure is missing goes to the status column, never into a number cell.",
|
|
201
|
+
],
|
|
202
|
+
}],
|
|
195
203
|
},
|
|
196
204
|
execute: async (args, ctx) => {
|
|
197
205
|
requirePaneFunctionTarget(args, "fn", ctx);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { AppConfig } from "../types/config";
|
|
2
|
+
import type { DataProvider } from "../types/data-provider";
|
|
3
|
+
import type { TickerRecord } from "../types/ticker";
|
|
4
|
+
import { canonicalExchange, isUsListingExchange, parsePublicTickerKey } from "../utils/exchanges";
|
|
5
|
+
import { logFailure } from "../utils/log-failure";
|
|
6
|
+
import { savedListingName } from "./listing-arg";
|
|
7
|
+
|
|
8
|
+
/** A list holding the company: its name, and the other saved listing it holds the company as (null for the listing asked about). */
|
|
9
|
+
export interface ListMembership {
|
|
10
|
+
name: string;
|
|
11
|
+
/** RCI.B:TSX when the list holds Rogers as its Toronto line and the report is of RCI. */
|
|
12
|
+
via: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CompanyMemberships {
|
|
16
|
+
portfolios: ListMembership[];
|
|
17
|
+
watchlists: ListMembership[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A listing as Gloom Cloud writes an issuer's listings: the bare ticker on a
|
|
22
|
+
* US venue or none (RCI), SYMBOL:EXCHANGE elsewhere (RCI.B:TSX), with a
|
|
23
|
+
* share class's dash or slash as a dot (BRK-B is BRK.B).
|
|
24
|
+
*/
|
|
25
|
+
export function issuerListingKey(symbol: string, exchange?: string): string {
|
|
26
|
+
const parsed = parsePublicTickerKey(symbol);
|
|
27
|
+
const venue = parsed.exchange ?? canonicalExchange(exchange);
|
|
28
|
+
const code = parsed.symbol.toUpperCase().replace(/[-/]/g, ".");
|
|
29
|
+
return venue && !isUsListingExchange(venue) ? `${code}:${venue}` : code;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function names(config: AppConfig, ids: readonly string[], kind: "portfolios" | "watchlists"): Map<string, string> {
|
|
33
|
+
const collections: ReadonlyArray<{ id: string; name: string }> = config[kind];
|
|
34
|
+
return new Map(ids.flatMap((id) => {
|
|
35
|
+
const name = collections.find((collection) => collection.id === id)?.name;
|
|
36
|
+
return name ? [[id, name] as const] : [];
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The portfolios and watchlists that hold a company, whichever of its
|
|
42
|
+
* listings they saved: the listing's own lists, then those holding another
|
|
43
|
+
* of `issuerListings` (its keys as `issuerListingKey` writes them), each
|
|
44
|
+
* once and saying which listing it holds. A restricted list holding RCI.B:TSX
|
|
45
|
+
* holds Rogers, which also trades as RCI in New York. Without the company's
|
|
46
|
+
* listings, only the listing's own lists.
|
|
47
|
+
*/
|
|
48
|
+
export function companyMemberships(
|
|
49
|
+
config: AppConfig,
|
|
50
|
+
own: TickerRecord | null,
|
|
51
|
+
saved: readonly TickerRecord[],
|
|
52
|
+
issuerListings: readonly string[] | null,
|
|
53
|
+
): CompanyMemberships {
|
|
54
|
+
const listings = new Set(issuerListings ?? []);
|
|
55
|
+
const ownKey = own ? savedListingName(own).key : null;
|
|
56
|
+
const others = listings.size === 0 ? [] : saved.filter((ticker) =>
|
|
57
|
+
savedListingName(ticker).key !== ownKey
|
|
58
|
+
&& listings.has(issuerListingKey(ticker.metadata.ticker, ticker.metadata.exchange)));
|
|
59
|
+
const collect = (kind: "portfolios" | "watchlists"): ListMembership[] => {
|
|
60
|
+
const held = new Map<string, ListMembership>();
|
|
61
|
+
for (const [id, name] of names(config, own?.metadata[kind] ?? [], kind)) held.set(id, { name, via: null });
|
|
62
|
+
for (const ticker of others) {
|
|
63
|
+
for (const [id, name] of names(config, ticker.metadata[kind], kind)) {
|
|
64
|
+
if (!held.has(id)) held.set(id, { name, via: savedListingName(ticker).key });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return [...held.values()];
|
|
68
|
+
};
|
|
69
|
+
return { portfolios: collect("portfolios"), watchlists: collect("watchlists") };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* `companyMemberships` for a listing, asking Gloom Cloud for the company's
|
|
74
|
+
* other listings only when another saved listing is in a list at all. A
|
|
75
|
+
* failed lookup leaves the listing's own lists.
|
|
76
|
+
*/
|
|
77
|
+
export async function loadCompanyMemberships({
|
|
78
|
+
config,
|
|
79
|
+
own,
|
|
80
|
+
symbol,
|
|
81
|
+
exchange,
|
|
82
|
+
loadTickers,
|
|
83
|
+
dataProvider,
|
|
84
|
+
}: {
|
|
85
|
+
config: AppConfig;
|
|
86
|
+
own: TickerRecord | null;
|
|
87
|
+
/** The listing the report is of, and its venue (RCI, NYSE). */
|
|
88
|
+
symbol: string;
|
|
89
|
+
exchange: string;
|
|
90
|
+
loadTickers: () => Promise<TickerRecord[]>;
|
|
91
|
+
dataProvider: Pick<DataProvider, "getIssuerListings">;
|
|
92
|
+
}): Promise<CompanyMemberships> {
|
|
93
|
+
const ownKey = own ? savedListingName(own).key : null;
|
|
94
|
+
const saved = await Promise.resolve().then(loadTickers)
|
|
95
|
+
.catch(logFailure("cli.issuer-membership", "Reading saved listings failed", [] as TickerRecord[]));
|
|
96
|
+
const listed = saved.filter((ticker) => savedListingName(ticker).key !== ownKey
|
|
97
|
+
&& (ticker.metadata.portfolios.length > 0 || ticker.metadata.watchlists.length > 0));
|
|
98
|
+
const { getIssuerListings } = dataProvider;
|
|
99
|
+
if (listed.length === 0 || !getIssuerListings) return companyMemberships(config, own, [], null);
|
|
100
|
+
const issuer = await Promise.resolve().then(() => getIssuerListings.call(dataProvider, symbol, exchange))
|
|
101
|
+
.catch(logFailure("cli.issuer-membership", "Reading the company's listings failed", null));
|
|
102
|
+
return companyMemberships(config, own, listed, issuer?.listings.map((listing) => issuerListingKey(listing.symbol, listing.exchange)) ?? null);
|
|
103
|
+
}
|
package/src/cli/listing-arg.ts
CHANGED
|
@@ -836,6 +836,13 @@ export async function loadSavedListing(store: ListingStore, listing: ListingArg)
|
|
|
836
836
|
return null;
|
|
837
837
|
}
|
|
838
838
|
|
|
839
|
+
/** A row saved under a vendor's suffixed key (RY.TO) for the listing it was read as: on that venue, or on none it names. */
|
|
840
|
+
async function loadSuffixedRow(store: ListingStore, key: string, exchange: string): Promise<TickerRecord | null> {
|
|
841
|
+
const ticker = await store.loadTicker(key);
|
|
842
|
+
const savedExchange = canonicalExchange(ticker?.metadata.exchange);
|
|
843
|
+
return ticker && (!savedExchange || savedExchange === exchange) ? ticker : null;
|
|
844
|
+
}
|
|
845
|
+
|
|
839
846
|
export interface CliListing extends ListingArg {
|
|
840
847
|
saved: TickerRecord | null;
|
|
841
848
|
/**
|
|
@@ -895,7 +902,11 @@ export async function resolveCliListing(
|
|
|
895
902
|
deps: ListingDeps,
|
|
896
903
|
options: {
|
|
897
904
|
ownExchangeWins?: boolean;
|
|
898
|
-
/**
|
|
905
|
+
/**
|
|
906
|
+
* Read a venue suffix (RY.TO) as the listing its quote names (RY:TSX).
|
|
907
|
+
* Only pane functions ask for it (`applyListingArgument`, which `fn` and
|
|
908
|
+
* `shot` run through), as their data is read by venue.
|
|
909
|
+
*/
|
|
899
910
|
venueSuffix?: boolean;
|
|
900
911
|
} = {},
|
|
901
912
|
): Promise<CliListing> {
|
|
@@ -913,6 +924,8 @@ export async function resolveCliListing(
|
|
|
913
924
|
const pair = parsed.exchange ? null : await readAmbiguousCurrencyPair(parsed.symbol, deps);
|
|
914
925
|
if (pair) parsed = { symbol: pair, exchange: "", key: pair };
|
|
915
926
|
const suffixed = pair || named || !options.venueSuffix ? null : await readVenueSuffix(parsed, deps);
|
|
927
|
+
// The row may be saved under the suffixed key itself: a link or a layout's key is kept as it came.
|
|
928
|
+
const typedKey = suffixed ? parsed.symbol : null;
|
|
916
929
|
if (suffixed) parsed = parsed.readAs ? { ...suffixed, readAs: `${parsed.readAs} ${suffixed.readAs}` } : suffixed;
|
|
917
930
|
try {
|
|
918
931
|
await checkListingExchange(parsed, deps);
|
|
@@ -922,7 +935,7 @@ export async function resolveCliListing(
|
|
|
922
935
|
parsed = market;
|
|
923
936
|
}
|
|
924
937
|
let listing = await settleSharedCode(parsed, deps);
|
|
925
|
-
const saved = await loadSavedListing(deps.store, listing);
|
|
938
|
+
const saved = await loadSavedListing(deps.store, listing) ?? (typedKey ? await loadSuffixedRow(deps.store, typedKey, listing.exchange) : null);
|
|
926
939
|
// A code nothing could read stays the venue it is saved on.
|
|
927
940
|
const savedExchange = canonicalExchange(saved?.metadata.exchange);
|
|
928
941
|
if (listing.sharedCode && savedExchange) listing = { symbol: listing.symbol, exchange: savedExchange, key: `${listing.symbol}:${savedExchange}` };
|
package/src/cli/name-entries.ts
CHANGED
|
@@ -6,26 +6,65 @@ import { splitSymbolWords } from "./symbol-args";
|
|
|
6
6
|
import { isProviderBusy } from "../sources/provider-errors";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* The entries of a list (`splitSymbolArgs
|
|
10
|
-
* resolves. An entry with a space in it is a company's name when
|
|
11
|
-
* the company (`Whirlpool Corporation`),
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
9
|
+
* The entries of a list (`splitSymbolArgs`, or a file's lines) as the symbols
|
|
10
|
+
* a command resolves. An entry with a space in it is a company's name when
|
|
11
|
+
* search finds the company (`Whirlpool Corporation`, `philip morris intl`),
|
|
12
|
+
* and stays whole for its listing to say what it was read as. One search
|
|
13
|
+
* finds no company for is its words, each a symbol, when each is written as
|
|
14
|
+
* a symbol or has a quote (`AAPL MSFT`, `brk.b bf-b`, `aapl msft` on a
|
|
15
|
+
* pasted line), and otherwise stays whole, a name nothing lists: its words
|
|
16
|
+
* are not taken for other companies' symbols (INTL, a fund, from a misspelt
|
|
17
|
+
* `phillip morris intl`). Only those entries cost a search. One search was
|
|
18
|
+
* too busy to look up also stays whole, so its lookup says so instead of
|
|
19
|
+
* resolving its words as symbols of other companies.
|
|
16
20
|
*/
|
|
17
21
|
export async function readNameEntries(
|
|
18
22
|
entries: readonly string[],
|
|
19
|
-
dataProvider: Pick<DataProvider, "search"
|
|
23
|
+
dataProvider: Pick<DataProvider, "search"> & Partial<Pick<DataProvider, "getQuotesBatch" | "getQuote">>,
|
|
20
24
|
): Promise<string[]> {
|
|
21
|
-
const read = await Promise.all(entries.map(async (entry) =>
|
|
25
|
+
const read = await Promise.all(entries.map(async (entry) => {
|
|
22
26
|
// A terminal's code for a listing ("JPM US Equity") is one entry, like a company's name.
|
|
23
|
-
await readTerminalCodeArg(dataProvider, entry)
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
if (await readTerminalCodeArg(dataProvider, entry)) return [entry];
|
|
28
|
+
if (!isCompanyNameInput(entry)) return splitSymbolWords([entry]);
|
|
29
|
+
if (await findListingOfCompanyName(dataProvider, entry).catch(busyAsFound)) return [entry];
|
|
30
|
+
const words = splitSymbolWords([entry]);
|
|
31
|
+
return await wordsAreSymbols(words, dataProvider) ? words : [entry];
|
|
32
|
+
}));
|
|
26
33
|
return read.flat();
|
|
27
34
|
}
|
|
28
35
|
|
|
36
|
+
/** A word typed as a ticker is: in capitals (AAPL, 7203.T), with a venue or class mark (brk.b, bf-b, SAN:EPA), or after a $. */
|
|
37
|
+
function writtenAsSymbol(word: string): boolean {
|
|
38
|
+
return /^\$|[:.\-/=^]/.test(word) || /\d/.test(word) || (/\p{Lu}/u.test(word) && !/\p{Ll}/u.test(word));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether the words of an entry search found no company for are symbols:
|
|
43
|
+
* each written as one (2222, BRK.B) or else with a quote, the others asked in
|
|
44
|
+
* one batch (`2222 amd pltr`). A batch that fails says nothing, and the
|
|
45
|
+
* entry stays a name.
|
|
46
|
+
*/
|
|
47
|
+
async function wordsAreSymbols(
|
|
48
|
+
words: readonly string[],
|
|
49
|
+
dataProvider: Partial<Pick<DataProvider, "getQuotesBatch" | "getQuote">>,
|
|
50
|
+
): Promise<boolean> {
|
|
51
|
+
const symbols = words.filter((word) => !writtenAsSymbol(word)).map((word) => word.replace(/^\$/, "").toUpperCase());
|
|
52
|
+
if (symbols.length === 0) return true;
|
|
53
|
+
if (dataProvider.getQuotesBatch) {
|
|
54
|
+
const results = await dataProvider.getQuotesBatch(symbols.map((symbol) => ({ symbol, exchange: "" })))
|
|
55
|
+
.catch(logFailure("cli.name-entries", "Read quotes for a pasted line failed", []));
|
|
56
|
+
return results.length === symbols.length && results.every(({ quote }) => !!quote);
|
|
57
|
+
}
|
|
58
|
+
// A source without batches is asked a quote at a time: the few words not written as symbols.
|
|
59
|
+
const { getQuote } = dataProvider;
|
|
60
|
+
if (!getQuote) return false;
|
|
61
|
+
const quoted = await Promise.all(symbols.map((symbol) => getQuote(symbol, "").then(
|
|
62
|
+
(quote) => !!quote,
|
|
63
|
+
logFailure("cli.name-entries", "Read a quote for a pasted line failed", false),
|
|
64
|
+
)));
|
|
65
|
+
return quoted.every(Boolean);
|
|
66
|
+
}
|
|
67
|
+
|
|
29
68
|
/** `findListingOfCompanyName`'s busy answer as a name to keep whole, any other failure as it is. */
|
|
30
69
|
function busyAsFound(error: unknown): true {
|
|
31
70
|
if (isProviderBusy(error)) return true;
|
|
@@ -18,6 +18,8 @@ export interface PaneFunctionCapability {
|
|
|
18
18
|
botSafe: boolean;
|
|
19
19
|
tickerCardinality: PaneFunctionTickerCardinality;
|
|
20
20
|
aliases: string[];
|
|
21
|
+
/** Words that are no code but ask for this function's data: `fn` and `catalog` answer them with it. */
|
|
22
|
+
suggestFor?: string[];
|
|
21
23
|
intents: string[];
|
|
22
24
|
outputKind: string;
|
|
23
25
|
reportReadiness: PaneFunctionReadiness;
|
|
@@ -210,6 +210,21 @@ export function nearFunctionCodes(
|
|
|
210
210
|
.sort((left, right) => left.code.localeCompare(right.code));
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
/**
|
|
214
|
+
* The functions a word that is no code asks for (`IVOL`, `IV`: HIVG and
|
|
215
|
+
* OVDV), as their `discovery.suggestFor` lists it, each by its code.
|
|
216
|
+
*/
|
|
217
|
+
export function suggestedFunctionCodes(registry: PaneFunctionCatalog, name: string): NearFunctionCode[] {
|
|
218
|
+
const wanted = normalizeLookupToken(name);
|
|
219
|
+
if (!wanted) return [];
|
|
220
|
+
return [...registry.paneTemplates.values()].flatMap((template) => {
|
|
221
|
+
const pane = registry.panes.get(template.paneId);
|
|
222
|
+
const words = pane ? getHeadlessPaneDefinition(template, pane)?.discovery?.suggestFor ?? [] : [];
|
|
223
|
+
const code = template.shortcut?.prefix;
|
|
224
|
+
return code && words.some((word) => normalizeLookupToken(word) === wanted) ? [{ code: code.toUpperCase(), label: template.label }] : [];
|
|
225
|
+
}).sort((left, right) => left.code.localeCompare(right.code));
|
|
226
|
+
}
|
|
227
|
+
|
|
213
228
|
function sampleArgForTemplate(template: PaneTemplateDef): string {
|
|
214
229
|
switch (template.shortcut?.argKind) {
|
|
215
230
|
case "ticker":
|
|
@@ -391,6 +406,7 @@ function scorePaneCatalogEntry(entry: PaneCatalogEntry, query: string): CatalogM
|
|
|
391
406
|
entry.token,
|
|
392
407
|
entry.shortcut,
|
|
393
408
|
...entry.aliases,
|
|
409
|
+
...entry.capability.suggestFor ?? [],
|
|
394
410
|
entry.templateId,
|
|
395
411
|
entry.paneId,
|
|
396
412
|
].filter((value): value is string => !!value).map((value) => normalizeLookupToken(value));
|
|
@@ -222,7 +222,8 @@ export async function buildDomPaneReportFromRender(
|
|
|
222
222
|
const truncated = truncationReasons.length > 0
|
|
223
223
|
|| (result.render.reportTruncationReasons ? false : result.render.truncated);
|
|
224
224
|
const failureReason = renderedFailureReason(result, rows);
|
|
225
|
-
|
|
225
|
+
// A pane that drew rows beside an error or empty marker ("Quote unavailable.") has data: only one with none has no data for its symbols.
|
|
226
|
+
const unavailableSymbols = failureReason && rows.length === 0 && result.symbols.length > 0 ? result.symbols : [];
|
|
226
227
|
// What the view leaves out and how to see more, such as OMON's strikes outside its window.
|
|
227
228
|
const rendered = renderedReportNotices(result.render.semanticUi ?? []);
|
|
228
229
|
const notices = [...(resolved.pane.reportNotices?.(resolved.instance.settings ?? {}) ?? []), ...rendered.notices];
|
|
@@ -89,17 +89,36 @@ function normalizeSymbolList(value: string): string[] {
|
|
|
89
89
|
return symbols;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
const COUNT_WORDS = ["zero", "one", "two", "three", "four", "five"];
|
|
93
|
+
const EXAMPLE_TICKERS = ["SPY", "QQQ", "IWM", "DIA", "TLT"];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* What a function that compares several tickers answers fewer: how many it
|
|
97
|
+
* needs and a command that runs, the reader's own tickers first (`CMP needs
|
|
98
|
+
* two or more tickers: fn CMP SPY,QQQ`).
|
|
99
|
+
*/
|
|
100
|
+
function tooFewTickersMessage(token: string, minimum: number, given: readonly string[], command: "fn" | "shot" = "fn"): string {
|
|
101
|
+
const example = [...given, ...EXAMPLE_TICKERS.filter((ticker) => !given.includes(ticker))].slice(0, Math.max(minimum, given.length + 1));
|
|
102
|
+
return `${token} needs ${COUNT_WORDS[minimum] ?? minimum} or more tickers: ${command} ${token} ${example.join(",")}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The tickers a function's argument lists, as its report reads them. */
|
|
106
|
+
export function argumentSymbols(rawArgument: string): string[] {
|
|
107
|
+
return normalizeSymbolList(rawArgument);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function validateSymbolCount(
|
|
93
111
|
definition: HeadlessPaneDefinition,
|
|
94
112
|
token: string,
|
|
95
113
|
symbols: string[],
|
|
114
|
+
command: "fn" | "shot" = "fn",
|
|
96
115
|
): void {
|
|
97
116
|
const minimum = definition.argument.minimum
|
|
98
117
|
?? (definition.argument.optional ? 0 : 1);
|
|
99
118
|
const maximum = definition.argument.maximum;
|
|
100
119
|
if (symbols.length < minimum) {
|
|
101
120
|
if (minimum <= 1) throw requiredArgumentError(definition, token);
|
|
102
|
-
throw new Error(
|
|
121
|
+
throw new Error(tooFewTickersMessage(token, minimum, symbols, command));
|
|
103
122
|
}
|
|
104
123
|
if (maximum != null && symbols.length > maximum) {
|
|
105
124
|
throw new Error(`${token} accepts at most ${maximum} symbols.`);
|
|
@@ -735,14 +754,14 @@ export function headlessReportTables(
|
|
|
735
754
|
const rowsResult = result as HeadlessRowsResult;
|
|
736
755
|
const columns = displayedColumns(columnsFor(rowsResult.rows, rowsResult.columns, definition.columns));
|
|
737
756
|
const rowIdentities = rowIdentitiesOf(rowsResult.rows, rowsResult);
|
|
738
|
-
const own = exportRowsTable(title, columns, rowsResult.rows);
|
|
757
|
+
const own = exportRowsTable(title, columns, rowsResult.rows, { status: true });
|
|
739
758
|
const interval = rowsResult.interval?.trim();
|
|
740
759
|
tables = [{ ...own, ...(rowIdentities ? { rowIdentities } : {}), ...(interval ? { identity: { interval } } : {}) }, ...(rowsResult.exportTables ?? []).map((extra) => {
|
|
741
760
|
const identity = {
|
|
742
761
|
...(extra.unit?.trim() ? { currencyOrUnit: extra.unit.trim() } : {}),
|
|
743
762
|
...(extra.interval?.trim() ? { interval: extra.interval.trim() } : {}),
|
|
744
763
|
};
|
|
745
|
-
const table = exportRowsTable(extra.title, displayedColumns(extra.columns), extra.rows);
|
|
764
|
+
const table = exportRowsTable(extra.title, displayedColumns(extra.columns), extra.rows, { status: true });
|
|
746
765
|
return Object.keys(identity).length > 0 ? { ...table, identity } : table;
|
|
747
766
|
})];
|
|
748
767
|
break;
|
|
@@ -758,7 +777,7 @@ export function headlessReportTables(
|
|
|
758
777
|
const entries = exportEntriesTable(section.title, section.entries);
|
|
759
778
|
return Object.keys(named).length > 0 ? { ...entries, identity: named } : entries;
|
|
760
779
|
}
|
|
761
|
-
const table = exportRowsTable(section.title, displayedColumns(columnsFor(section.rows, section.columns, definition.columns)), section.rows);
|
|
780
|
+
const table = exportRowsTable(section.title, displayedColumns(columnsFor(section.rows, section.columns, definition.columns)), section.rows, { status: true });
|
|
762
781
|
const identity = {
|
|
763
782
|
...named,
|
|
764
783
|
...(section.unit?.trim() ? { currencyOrUnit: section.unit.trim() } : {}),
|
|
@@ -776,7 +795,7 @@ export function headlessReportTables(
|
|
|
776
795
|
}
|
|
777
796
|
case "snapshot": {
|
|
778
797
|
const snapshot = result as HeadlessSnapshotResult;
|
|
779
|
-
tables = [exportRowsTable(title, displayedColumns(columnsFor(snapshot.items, undefined, definition.columns)), snapshot.items)];
|
|
798
|
+
tables = [exportRowsTable(title, displayedColumns(columnsFor(snapshot.items, undefined, definition.columns)), snapshot.items, { status: true })];
|
|
780
799
|
break;
|
|
781
800
|
}
|
|
782
801
|
default: {
|