@gloom-sh/terminal 0.19.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +23 -1
- package/src/api-client/app-attention.ts +3 -0
- package/src/api-client/beneficial-owners.ts +3 -0
- package/src/api-client/company-kpis.ts +3 -0
- package/src/api-client/data.ts +2 -1
- package/src/api-client/hiring.ts +3 -0
- package/src/api-client/index.ts +4 -0
- package/src/api-client/ipo.ts +24 -1
- package/src/api-client/mna.ts +3 -0
- package/src/api-client/paths.ts +9 -1
- package/src/api-client/roadmap.ts +85 -0
- package/src/api-client/types.ts +12 -0
- package/src/architecture/core-builtin-allowlist.json +2 -2
- package/src/cli/commands/market.ts +35 -12
- package/src/cli/commands/news-search.ts +22 -0
- package/src/cli/commands/overview.ts +86 -20
- package/src/cli/commands/ticker.ts +32 -2
- package/src/cli/errors.ts +17 -2
- package/src/cli/glossary.ts +6 -0
- package/src/cli/index-roots.ts +0 -7
- package/src/cli/listing-arg.ts +57 -10
- package/src/cli/pane-functions/capabilities.ts +1 -0
- package/src/cli/pane-functions/discovery.ts +21 -1
- package/src/cli/pane-functions/freshness.ts +7 -1
- package/src/cli/pane-functions/function-help.ts +7 -0
- package/src/cli/pane-functions/index.ts +6 -3
- package/src/cli/quote-answer.ts +1 -1
- package/src/cli/saved-copy-persistence.ts +88 -0
- package/src/components/command-bar/list/model.ts +5 -0
- package/src/components/command-bar/routes/root/runtime.ts +6 -7
- package/src/components/command-bar/selection-runtime.ts +52 -3
- package/src/components/command-bar/surface/index.tsx +2 -0
- package/src/components/layout/pane/capture-stamp.ts +2 -2
- package/src/components/layout/pane/footer/index.tsx +22 -6
- package/src/components/layout/pane/footer/model.ts +83 -15
- package/src/components/layout/pane/status-footer.ts +35 -2
- package/src/components/layout/status-widget-space.ts +9 -0
- package/src/components/ticker/list-table-view.tsx +4 -20
- package/src/components/ui/display.tsx +18 -3
- package/src/components/ui/icon.tsx +4 -1
- package/src/data/plugin-cache.ts +5 -3
- package/src/data/saved-copies.ts +8 -0
- package/src/docs/snapshot.json +32 -5
- package/src/i18n/es.ts +2 -4
- package/src/i18n/ja.ts +2 -4
- package/src/i18n/ko.ts +2 -4
- package/src/i18n/zh-cn.ts +2 -4
- package/src/i18n/zh-tw.ts +2 -4
- package/src/market-data/depositary-pair.ts +25 -1
- package/src/market-data/quotes/metadata.ts +7 -0
- package/src/news/aggregator.ts +7 -1
- package/src/news/types.ts +6 -0
- package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
- package/src/plugins/builtin/alerts/cli.ts +34 -11
- package/src/plugins/builtin/alerts/event-rows.ts +24 -3
- package/src/plugins/builtin/alerts/events-pane.tsx +15 -6
- package/src/plugins/builtin/alerts/headless.ts +10 -6
- package/src/plugins/builtin/cds/client.ts +142 -30
- package/src/plugins/builtin/cds/headless.ts +35 -8
- package/src/plugins/builtin/cds/model.ts +145 -18
- package/src/plugins/builtin/cds/pane.tsx +16 -5
- package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
- package/src/plugins/builtin/chart-composer/index.tsx +4 -5
- package/src/plugins/builtin/chat/controller/index.ts +5 -1
- package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
- package/src/plugins/builtin/chat/controller/state.ts +25 -2
- package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
- package/src/plugins/builtin/chat/controller/utils.ts +7 -2
- package/src/plugins/builtin/chat/status-preview.ts +79 -0
- package/src/plugins/builtin/chat/status-widget.tsx +147 -10
- package/src/plugins/builtin/chat/unread-badge.ts +51 -10
- package/src/plugins/builtin/clinical-trials/client.ts +49 -4
- package/src/plugins/builtin/clinical-trials/model.ts +86 -22
- package/src/plugins/builtin/clinical-trials/pane.tsx +46 -27
- package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +87 -9
- package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
- package/src/plugins/builtin/company-attention/headless.ts +2 -1
- package/src/plugins/builtin/company-attention/pane.tsx +3 -0
- package/src/plugins/builtin/company-kpis/headless.ts +8 -3
- package/src/plugins/builtin/company-kpis/model.ts +73 -5
- package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
- package/src/plugins/builtin/composite-plugins.ts +2 -1
- package/src/plugins/builtin/credit-documents/headless.ts +3 -3
- package/src/plugins/builtin/doe/headless.ts +3 -2
- package/src/plugins/builtin/doe/pane.tsx +13 -3
- package/src/plugins/builtin/earnings-calls/data.ts +18 -6
- package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
- package/src/plugins/builtin/earnings-calls/pane.tsx +7 -0
- package/src/plugins/builtin/econ/calendar-model.ts +48 -1
- package/src/plugins/builtin/econ/calendar-source.ts +4 -2
- package/src/plugins/builtin/econ/detail-view.tsx +2 -2
- package/src/plugins/builtin/econ/figures.ts +48 -0
- package/src/plugins/builtin/econ/index.tsx +14 -5
- package/src/plugins/builtin/econ/print-watch.ts +3 -1
- package/src/plugins/builtin/econ-statistics/client.ts +11 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +10 -2
- package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +3 -0
- package/src/plugins/builtin/help/index.tsx +61 -59
- package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
- package/src/plugins/builtin/help/table-cells.tsx +13 -2
- package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
- package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
- package/src/plugins/builtin/holders/headless.ts +4 -2
- package/src/plugins/builtin/holders/pane.tsx +3 -0
- package/src/plugins/builtin/holders/report-rows.ts +4 -1
- package/src/plugins/builtin/insider/headless.ts +2 -2
- package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
- package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
- package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
- package/src/plugins/builtin/macro-day/headless.ts +12 -5
- package/src/plugins/builtin/macro-day/index.tsx +1 -1
- package/src/plugins/builtin/macro-day/model.ts +39 -5
- package/src/plugins/builtin/macro-day/pane.tsx +27 -19
- package/src/plugins/builtin/market-movers/client.ts +2 -0
- package/src/plugins/builtin/market-movers/headless.ts +3 -1
- package/src/plugins/builtin/market-movers/screener.ts +8 -4
- package/src/plugins/builtin/mna/headless.ts +2 -0
- package/src/plugins/builtin/mna/pane.tsx +3 -1
- package/src/plugins/builtin/news/headless.ts +3 -1
- package/src/plugins/builtin/news/index.tsx +3 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -6
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
- package/src/plugins/builtin/portfolio-list/table.tsx +4 -6
- package/src/plugins/builtin/realized-vol/client.ts +13 -5
- package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
- package/src/plugins/builtin/realized-vol/headless.ts +8 -4
- package/src/plugins/builtin/realized-vol/model.ts +37 -6
- package/src/plugins/builtin/realized-vol/pane.tsx +9 -7
- package/src/plugins/builtin/research/event-model.ts +30 -4
- package/src/plugins/builtin/research/index.tsx +7 -3
- package/src/plugins/builtin/research/relative-valuation-headless.ts +8 -25
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +52 -15
- package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
- package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
- package/src/plugins/builtin/risk-factors/data.ts +4 -2
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- package/src/plugins/builtin/roadmap/index.tsx +28 -0
- package/src/plugins/builtin/roadmap/model.ts +67 -0
- package/src/plugins/builtin/roadmap/pane.tsx +244 -0
- package/src/plugins/builtin/scanner/live-headless.ts +21 -2
- package/src/plugins/builtin/sec/headless.ts +2 -1
- package/src/plugins/builtin/sec/index.tsx +3 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +13 -5
- package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
- package/src/plugins/builtin/shared/report-freshness.ts +8 -0
- package/src/plugins/builtin/shared/research-cells.tsx +1 -1
- package/src/plugins/builtin/shared/series-cache.ts +4 -4
- package/src/plugins/builtin/thirteenf/api.ts +12 -1
- package/src/plugins/builtin/thirteenf/data.ts +7 -7
- package/src/plugins/builtin/thirteenf/headless.ts +31 -4
- package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +11 -6
- package/src/plugins/builtin/treasury-auctions/headless.ts +21 -12
- package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
- package/src/plugins/builtin/volatility/pane.tsx +5 -2
- package/src/plugins/builtin/yield-curve/client.ts +61 -1
- package/src/plugins/builtin/yield-curve/headless.ts +9 -6
- package/src/plugins/builtin/yield-curve/index.tsx +34 -10
- package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
- package/src/plugins/catalog-browser.ts +2 -1
- package/src/plugins/host-link.ts +1 -1
- package/src/portfolio/column-values.ts +11 -7
- package/src/renderers/dom/desktop/icons.tsx +7 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
- package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
- package/src/renderers/electrobun/bun/index.ts +26 -2
- package/src/renderers/electrobun/bun/window/create.ts +43 -4
- package/src/renderers/electrobun/bun/window/events.ts +20 -0
- package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
- package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
- package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/news-source.ts +2 -0
- package/src/renderers/electrobun/view/ui-host.tsx +8 -0
- package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
- package/src/sources/gloomberb-cloud/index.ts +42 -8
- package/src/tickers/company-name-listing.ts +53 -13
- package/src/tickers/company-name.ts +6 -1
- package/src/tickers/index-roots.ts +7 -0
- package/src/tickers/listing-fate.ts +2 -2
- package/src/tickers/search/index.ts +6 -2
- package/src/tickers/search/ranking.ts +5 -1
- package/src/tickers/search/upsert.ts +13 -0
- package/src/tickers/ticker-changes.ts +8 -4
- package/src/types/company-from.ts +40 -0
- package/src/types/data-provider.ts +7 -0
- package/src/types/financials.ts +27 -0
- package/src/types/headless.ts +6 -0
- package/src/utils/crypto-pair.ts +121 -7
- package/src/utils/declared-dividends.ts +1 -1
- package/src/utils/fundamentals.ts +36 -5
- package/src/version.ts +2 -2
|
@@ -11,6 +11,9 @@ import { quoteAlertFields } from "./quotes";
|
|
|
11
11
|
import { createEventAlert } from "./events";
|
|
12
12
|
import { isResearchAlertKind, readResearchRule } from "./research-rules";
|
|
13
13
|
import { withConfigData, withMarketData } from "../../../cli/scoped-context";
|
|
14
|
+
import { resolvePersistedCloudSessionToken } from "../../../cli/pane-functions/cloud-session";
|
|
15
|
+
import type { ConfigContext } from "../../../cli/types";
|
|
16
|
+
import { checkedWhenSignedIn, NEEDS_SIGN_IN, needsSignInNote } from "./event-rows";
|
|
14
17
|
import { CLI_COMMAND_GROUPS } from "../../../cli/help";
|
|
15
18
|
import { dryRunNote } from "../../../cli/helpers";
|
|
16
19
|
import { requireArg, takeOption } from "../../../cli/commands/command-utils";
|
|
@@ -445,11 +448,13 @@ function priceRow(alert: AlertRule): AlertListRow {
|
|
|
445
448
|
}
|
|
446
449
|
|
|
447
450
|
function eventRow(
|
|
448
|
-
rule: EventAlertRule, now: number, eventDate: string | null | undefined, check?: IvLevelCheck, cloud?: CloudRuleStates,
|
|
451
|
+
rule: EventAlertRule, now: number, eventDate: string | null | undefined, check?: IvLevelCheck, cloud?: CloudRuleStates, signedIn = true,
|
|
449
452
|
): AlertListRow {
|
|
450
453
|
const scheduled = readScheduledRule(rule);
|
|
451
454
|
if (!scheduled) {
|
|
452
|
-
const
|
|
455
|
+
const triggered = rule.status === "active" && rule.triggeredAt ? "triggered" : rule.status;
|
|
456
|
+
// Signed out, nothing checks a rule only a signed-in account gets checked: it waits on a sign-in, not on its event.
|
|
457
|
+
const status = triggered === "active" && !signedIn && checkedWhenSignedIn(rule) ? NEEDS_SIGN_IN : triggered;
|
|
453
458
|
// An IV level is read by this app, so its last reading is this device's; Gloom Cloud reads the rest.
|
|
454
459
|
const state = cloud?.states.get(rule.id);
|
|
455
460
|
const reading = rule.kind === "iv_level" ? {
|
|
@@ -489,11 +494,14 @@ interface CloudRuleStates {
|
|
|
489
494
|
|
|
490
495
|
/**
|
|
491
496
|
* Gloom Cloud checks the event rules this app does not, for a signed-in account. With no session nothing has
|
|
492
|
-
* read them, so they are not checked yet; with one, their readings are Cloud's.
|
|
497
|
+
* read them, so they are not checked yet; with one (`session`, the app's saved one), their readings are Cloud's.
|
|
498
|
+
* The command runs with that session from here on, as market commands do (`initMarketData`): it is set once and
|
|
499
|
+
* never put back, so the earnings-date lookup running beside this never loses it midway.
|
|
493
500
|
*/
|
|
494
|
-
async function cloudRuleStates(rules: readonly EventAlertRule[]): Promise<CloudRuleStates> {
|
|
501
|
+
async function cloudRuleStates(rules: readonly EventAlertRule[], session: string | null): Promise<CloudRuleStates> {
|
|
495
502
|
const empty = { states: new Map<string, AlertRuleState>(), failed: false };
|
|
496
|
-
if (!
|
|
503
|
+
if (!session || !rules.some(checkedWhenSignedIn)) return empty;
|
|
504
|
+
apiClient.setSessionToken(session);
|
|
497
505
|
try {
|
|
498
506
|
const history = await fetchAlertHistory(0);
|
|
499
507
|
return { states: new Map(history.states.map((state) => [state.ruleId, state])), failed: false };
|
|
@@ -502,6 +510,12 @@ async function cloudRuleStates(rules: readonly EventAlertRule[]): Promise<CloudR
|
|
|
502
510
|
}
|
|
503
511
|
}
|
|
504
512
|
|
|
513
|
+
/** The Gloom Cloud session the app saved after sign-in, as `fn` and market commands read with; null signed out. */
|
|
514
|
+
function savedSession(config: Partial<Pick<ConfigContext, "persistence">>): string | null {
|
|
515
|
+
const persistence = config.persistence;
|
|
516
|
+
return apiClient.getSessionToken() ?? (persistence?.pluginState ? resolvePersistedCloudSessionToken({ persistence }) : null);
|
|
517
|
+
}
|
|
518
|
+
|
|
505
519
|
/** The listing a research rule follows (AAPL, ADYEN:AMS for a hiring rule); empty for one on no symbol. */
|
|
506
520
|
function eventRuleListing(rule: EventAlertRule): string {
|
|
507
521
|
if (rule.target === "ticker") return rule.value;
|
|
@@ -938,20 +952,23 @@ export const alertsCliCommand: CliCommandDef = {
|
|
|
938
952
|
|
|
939
953
|
if (action === "list") {
|
|
940
954
|
// An IV level's last reading is this device's, beside the rules.
|
|
941
|
-
const { alerts, alertsError, events, checks } = await withConfigData(ctx, (config) => ({
|
|
955
|
+
const { alerts, alertsError, events, checks, session } = await withConfigData(ctx, (config) => ({
|
|
942
956
|
...readAlertStores(config.config),
|
|
943
957
|
checks: readIvLevelChecks(config.persistence?.pluginState?.get(ALERTS_PLUGIN_ID, resumeStateKey(IV_LEVEL_CHECKS_KEY))?.value),
|
|
958
|
+
session: savedSession(config),
|
|
944
959
|
}));
|
|
945
960
|
// Earnings dates come through market data; CPI, FOMC and calendar dates do not need it.
|
|
946
961
|
const lookup = (targets: ScheduledEventTarget[]) => targets.some((target) => target.kind === "earnings")
|
|
947
962
|
? withMarketData(ctx, (market) => lookupScheduledDates(targets, scheduledEventSources(market.dataProvider), today, now))
|
|
948
963
|
: lookupScheduledDates(targets, scheduledEventSources(null), today, now);
|
|
949
|
-
const [{ dates, warnings }, cloud] = await Promise.all([listEventDates(events.rules, now, lookup), cloudRuleStates(events.rules)]);
|
|
964
|
+
const [{ dates, warnings }, cloud] = await Promise.all([listEventDates(events.rules, now, lookup), cloudRuleStates(events.rules, session)]);
|
|
950
965
|
const rows = [
|
|
951
966
|
...alerts.map(priceRow),
|
|
952
|
-
...events.rules.map((rule) => eventRow(rule, now, dates.has(rule.id) ? dates.get(rule.id) : rule.eventDate, checks[rule.id], cloud)),
|
|
967
|
+
...events.rules.map((rule) => eventRow(rule, now, dates.has(rule.id) ? dates.get(rule.id) : rule.eventDate, checks[rule.id], cloud, !!session)),
|
|
953
968
|
];
|
|
954
|
-
const
|
|
969
|
+
const waiting = rows.filter((row) => row.status === NEEDS_SIGN_IN).length;
|
|
970
|
+
const allWarnings = [...(alertsError ? [alertsError] : []), ...(events.error ? [events.error] : []), ...warnings, ...cloud.warning ? [cloud.warning] : [],
|
|
971
|
+
...waiting ? [needsSignInNote(waiting)] : []];
|
|
955
972
|
ctx.printResult({ data: rows, ...(allWarnings.length ? { warnings: allWarnings } : {}) }, {
|
|
956
973
|
rows: (data) => data.map(textRow),
|
|
957
974
|
textColumns: [
|
|
@@ -1048,8 +1065,14 @@ export const alertsCliCommand: CliCommandDef = {
|
|
|
1048
1065
|
await save({ events: [...rules, rule] });
|
|
1049
1066
|
record("created", rule);
|
|
1050
1067
|
const delivery = research ? researchDelivery(research.kind) : ruleDelivery(formRule!.event);
|
|
1051
|
-
|
|
1052
|
-
|
|
1068
|
+
// Saved signed out, it waits on a sign-in, and `alerts list` and ALRT say so until there is one.
|
|
1069
|
+
const needsSignIn = checkedWhenSignedIn(rule) && !savedSession(config);
|
|
1070
|
+
const signIn = needsSignIn
|
|
1071
|
+
? " It needs sign-in: nothing checks it while you are signed out. Open the app (gloom) and log in or sign up free from the command bar (Ctrl+P, Log In or Sign Up)."
|
|
1072
|
+
: "";
|
|
1073
|
+
ctx.printResult({ data: { changed: !ctx.cliOptions.dryRun, dryRun: ctx.cliOptions.dryRun, alert: rule, ...needsSignIn ? { status: NEEDS_SIGN_IN } : {} },
|
|
1074
|
+
...(read.length > 0 ? { warnings: read } : {}) }, {
|
|
1075
|
+
text: (data) => `Added alert ${data.alert.id}: ${alertKindLabel(rule.kind)} ${eventAlertTarget(rule)} (${delivery}).${signIn}${dryRunNote(data.dryRun)}`,
|
|
1053
1076
|
});
|
|
1054
1077
|
return;
|
|
1055
1078
|
}
|
|
@@ -8,13 +8,34 @@ export function ruleTarget(rule: EventAlertRule): string {
|
|
|
8
8
|
return `${target} · ${rule.eventDate ?? "date not known yet"}`;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Whether a rule is checked only for a signed-in account: Gloom Cloud checks every event rule but an IV level
|
|
13
|
+
* and an earnings, CPI, FOMC or calendar date, which the app reads itself, signed in or not, and the app reads
|
|
14
|
+
* catalysts only with a session. Signed out, such a rule never goes off.
|
|
15
|
+
*/
|
|
16
|
+
export function checkedWhenSignedIn(rule: EventAlertRule): boolean {
|
|
17
|
+
return rule.kind !== "iv_level" && !readScheduledRule(rule);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The state of a waiting rule nothing checks while signed out, as `alerts list` and its CSV write it. */
|
|
21
|
+
export const NEEDS_SIGN_IN = "needs sign-in";
|
|
22
|
+
|
|
23
|
+
/** What a list of rules says once when some of them need sign-in: what to do about it. */
|
|
24
|
+
export function needsSignInNote(count: number): string {
|
|
25
|
+
return `${count === 1 ? "1 event alert needs" : `${count} event alerts need`} sign-in: ${count === 1 ? "it is" : "they are"} checked only for a signed-in account. `
|
|
26
|
+
+ "Open the app (gloom) and log in or sign up free from the command bar (Ctrl+P, Log In or Sign Up).";
|
|
27
|
+
}
|
|
28
|
+
|
|
11
29
|
/**
|
|
12
30
|
* The state word the Events tab shows: a scheduled rule is Alerted once it has gone off for its coming
|
|
13
|
-
* event, an IV level once its level was reached, until it is re-armed.
|
|
31
|
+
* event, an IV level once its level was reached, until it is re-armed. Signed out, a waiting rule only a
|
|
32
|
+
* signed-in account gets checked needs sign-in.
|
|
14
33
|
*/
|
|
15
|
-
export function ruleState(rule: EventAlertRule, now: number = Date.now()): "Active" | "Paused" | "Alerted" {
|
|
34
|
+
export function ruleState(rule: EventAlertRule, now: number = Date.now(), signedIn = true): "Active" | "Paused" | "Alerted" | "Needs sign-in" {
|
|
16
35
|
const scheduled = readScheduledRule(rule);
|
|
17
36
|
const status = scheduled ? scheduledAlertStatus(rule, scheduled.leadDays, scheduledToday(scheduled.target, now), scheduled.trigger)
|
|
18
37
|
: rule.status === "active" && rule.kind === "iv_level" && rule.triggeredAt ? "triggered" : rule.status;
|
|
19
|
-
|
|
38
|
+
if (status === "paused") return "Paused";
|
|
39
|
+
if (status === "triggered") return "Alerted";
|
|
40
|
+
return !signedIn && checkedWhenSignedIn(rule) ? "Needs sign-in" : "Active";
|
|
20
41
|
}
|
|
@@ -31,6 +31,7 @@ import { alertKindLabel, fetchAlertHistory, ruleStateText } from "./history";
|
|
|
31
31
|
import { isResearchAlertKind } from "./research-rules";
|
|
32
32
|
import { useAsyncResource } from "../../../public/react";
|
|
33
33
|
import { usePlanAccess } from "../../../api-client/plan-access";
|
|
34
|
+
import { apiClient } from "../../../api-client";
|
|
34
35
|
import { useResearchCloudSession } from "../shared/research-cloud-session";
|
|
35
36
|
import { rearmScheduledAlert } from "./scheduled";
|
|
36
37
|
import { checkScheduledEventAlertsNow } from "./scheduled-check";
|
|
@@ -39,13 +40,14 @@ import { alertTarget, alertViewHints, eventRuleSubject, useOpenAlertTarget } fro
|
|
|
39
40
|
import { FIRED_LOG_KEY } from "./fired-log";
|
|
40
41
|
import { withAlertChange } from "./alert-log";
|
|
41
42
|
|
|
42
|
-
const columnsFor = (width: number, lastCheck: boolean): DataTableColumn[] => [
|
|
43
|
+
const columnsFor = (width: number, lastCheck: boolean, needsSignIn: boolean): DataTableColumn[] => [
|
|
43
44
|
{ id: "kind", label: "Event", width: 15, align: "left" },
|
|
44
45
|
{ id: "target", label: "Following", width: 28, flexGrow: 1, align: "left" },
|
|
45
46
|
...(lastCheck && width >= 96
|
|
46
47
|
? [{ id: "last", label: "Last check", width: 34, align: "left" as const }]
|
|
47
48
|
: []),
|
|
48
|
-
|
|
49
|
+
// Wide enough to say "Needs sign-in" only when a row does.
|
|
50
|
+
{ id: "status", label: "State", width: needsSignIn ? 13 : 8, align: "left" },
|
|
49
51
|
{ id: "created", label: "Since", width: 10, align: "left" },
|
|
50
52
|
];
|
|
51
53
|
|
|
@@ -97,10 +99,13 @@ export function EventAlertsPane({ focused, width, height }: PaneProps) {
|
|
|
97
99
|
const selectedRule = items[Math.min(selected, Math.max(0, items.length - 1))];
|
|
98
100
|
const now = Date.now();
|
|
99
101
|
const selectedFired = !!selectedRule && ruleState(selectedRule, now) === "Alerted";
|
|
100
|
-
const openTarget = useOpenAlertTarget();
|
|
101
|
-
const selectedTarget = useMemo(() => selectedRule ? alertTarget(eventRuleSubject(selectedRule)) : null, [selectedRule]);
|
|
102
102
|
// Research rules keep a dated observation in the Cloud; show its value and percentile.
|
|
103
103
|
const access = usePlanAccess();
|
|
104
|
+
// A saved session counts before its account has loaded, so a signed-in start does not flash "Needs sign-in".
|
|
105
|
+
const signedIn = access.signedIn || !!apiClient.getSessionToken();
|
|
106
|
+
const needsSignIn = !signedIn && rules.some((rule) => ruleState(rule, now, false) === "Needs sign-in");
|
|
107
|
+
const openTarget = useOpenAlertTarget();
|
|
108
|
+
const selectedTarget = useMemo(() => selectedRule ? alertTarget(eventRuleSubject(selectedRule)) : null, [selectedRule]);
|
|
104
109
|
const session = useResearchCloudSession();
|
|
105
110
|
const hasResearch = rules.some((rule) => isResearchAlertKind(rule.kind) && rule.kind !== "iv_level");
|
|
106
111
|
const stateLoader = useCallback(() => fetchAlertHistory(0), [session.requestKey]);
|
|
@@ -110,7 +115,7 @@ export function EventAlertsPane({ focused, width, height }: PaneProps) {
|
|
|
110
115
|
[history.data],
|
|
111
116
|
);
|
|
112
117
|
// An IV level is read by this app, so its last reading needs no session.
|
|
113
|
-
const columns = columnsFor(width, (hasResearch && !!history.data) || rules.some((rule) => rule.kind === "iv_level"));
|
|
118
|
+
const columns = columnsFor(width, (hasResearch && !!history.data) || rules.some((rule) => rule.kind === "iv_level"), needsSignIn);
|
|
114
119
|
const add = useCallback(
|
|
115
120
|
() => openPluginCommandWorkflow("set-event-alert"),
|
|
116
121
|
[openPluginCommandWorkflow],
|
|
@@ -216,7 +221,9 @@ export function EventAlertsPane({ focused, width, height }: PaneProps) {
|
|
|
216
221
|
// An IV level that could not be read says so on its own row: No data in State, the reason in Last check.
|
|
217
222
|
const check = rule.kind === "iv_level" ? checks[rule.id] : undefined;
|
|
218
223
|
const unread = rule.kind === "iv_level" && rule.status === "active" && !rule.triggeredAt && check?.value === null;
|
|
219
|
-
const state = ruleState(rule, now);
|
|
224
|
+
const state = ruleState(rule, now, signedIn);
|
|
225
|
+
// A rule nothing checks while signed out says so in its State, in the warning tone.
|
|
226
|
+
const waiting = state === "Needs sign-in" && column.id === "status";
|
|
220
227
|
return {
|
|
221
228
|
text:
|
|
222
229
|
column.id === "kind"
|
|
@@ -236,6 +243,8 @@ export function EventAlertsPane({ focused, width, height }: PaneProps) {
|
|
|
236
243
|
? colors.selectedText
|
|
237
244
|
: unread && (column.id === "status" || column.id === "last")
|
|
238
245
|
? colors.negative
|
|
246
|
+
: waiting
|
|
247
|
+
? colors.warning
|
|
239
248
|
: rule.status === "paused"
|
|
240
249
|
? colors.textMuted
|
|
241
250
|
: colors.text,
|
|
@@ -10,7 +10,7 @@ import type { HeadlessPaneColumn, HeadlessPaneContext, HeadlessPaneDefinition, H
|
|
|
10
10
|
import type { PaneLock } from "../../../types/plugin";
|
|
11
11
|
import { refusedLock } from "../shared/cloud-denied";
|
|
12
12
|
import { ALERT_TABS, ALERTS_PLUGIN_ID } from "./constants";
|
|
13
|
-
import { ruleState } from "./event-rows";
|
|
13
|
+
import { needsSignInNote, ruleState } from "./event-rows";
|
|
14
14
|
import { eventAlertTarget, type EventAlertRule } from "./events";
|
|
15
15
|
import {
|
|
16
16
|
alertListing,
|
|
@@ -141,7 +141,7 @@ function pricesResult(alerts: readonly AlertRule[], error: string | null, events
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
/** An event alert as the Events table lists it: a scheduled one with its event date and lead time apart. */
|
|
144
|
-
function eventRow(rule: EventAlertRule, now: number, checks: Record<string, IvLevelCheck
|
|
144
|
+
function eventRow(rule: EventAlertRule, now: number, checks: Record<string, IvLevelCheck>, signedIn: boolean): HeadlessPaneRow {
|
|
145
145
|
const scheduled = readScheduledRule(rule);
|
|
146
146
|
return {
|
|
147
147
|
id: rule.id,
|
|
@@ -151,22 +151,26 @@ function eventRow(rule: EventAlertRule, now: number, checks: Record<string, IvLe
|
|
|
151
151
|
eventDate: scheduled ? rule.eventDate ?? null : null,
|
|
152
152
|
leadDays: scheduled?.leadDays ?? null,
|
|
153
153
|
...(scheduled?.trigger === "results" ? { when: "results" } : {}),
|
|
154
|
-
state: ruleState(rule, now),
|
|
154
|
+
state: ruleState(rule, now, signedIn),
|
|
155
155
|
since: new Date(rule.createdAt).toISOString().slice(0, 10),
|
|
156
156
|
// An IV level's last reading, or why there was none, on its own row.
|
|
157
157
|
...rule.kind === "iv_level" ? { lastCheck: ivLevelCheckText(rule, checks[rule.id]) } : {},
|
|
158
158
|
};
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
function eventsResult(rules: readonly EventAlertRule[], error: string | null, checks: Record<string, IvLevelCheck
|
|
161
|
+
function eventsResult(rules: readonly EventAlertRule[], error: string | null, checks: Record<string, IvLevelCheck>, signedIn: boolean): HeadlessRowsResult {
|
|
162
162
|
const now = Date.now();
|
|
163
163
|
const checked = rules.some((rule) => rule.kind === "iv_level");
|
|
164
|
+
const rows = [...rules].sort((a, b) => b.createdAt - a.createdAt).map((rule) => eventRow(rule, now, checks, signedIn));
|
|
165
|
+
// Signed out, the rules only a signed-in account gets checked say so on their rows, and once what to do.
|
|
166
|
+
const waiting = rows.filter((row) => row.state === "Needs sign-in").length;
|
|
164
167
|
return {
|
|
165
168
|
// A Last check column only when an IV level rule has a reading to show.
|
|
166
169
|
columns: checked ? [...EVENT_COLUMNS.slice(0, -2), { key: "lastCheck", header: "Last check" }, ...EVENT_COLUMNS.slice(-2)] : EVENT_COLUMNS,
|
|
167
|
-
rows
|
|
170
|
+
rows,
|
|
168
171
|
empty: "No event alerts. Add one with gloom alerts add CRWV earnings --days 3, gloom alerts add CPI --days 1 or gloom alerts add CN PMI.",
|
|
169
172
|
...(error ? { errors: [error], complete: false } : {}),
|
|
173
|
+
...(waiting ? { notes: [needsSignInNote(waiting)] } : {}),
|
|
170
174
|
};
|
|
171
175
|
}
|
|
172
176
|
|
|
@@ -260,7 +264,7 @@ export const alertsHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
|
260
264
|
if (tab === "history") return historyResult(ctx);
|
|
261
265
|
const { alerts, alertsError, events } = readAlertStores(ctx.config);
|
|
262
266
|
return tab === "events"
|
|
263
|
-
? eventsResult(events.rules, events.error, readIvLevelChecks(ctx.readDeviceState?.(ALERTS_PLUGIN_ID, IV_LEVEL_CHECKS_KEY)))
|
|
267
|
+
? eventsResult(events.rules, events.error, readIvLevelChecks(ctx.readDeviceState?.(ALERTS_PLUGIN_ID, IV_LEVEL_CHECKS_KEY)), !!ctx.apiClient.getSessionToken?.())
|
|
264
268
|
: pricesResult(alerts, alertsError, events.rules.length);
|
|
265
269
|
},
|
|
266
270
|
};
|
|
@@ -9,6 +9,8 @@ import type { CloudCdsHistoryParams } from "../../../api-client/paths";
|
|
|
9
9
|
import type { InstrumentSearchResult } from "../../../types/instrument";
|
|
10
10
|
import {
|
|
11
11
|
cdsReferenceEntity,
|
|
12
|
+
closestCoveredIssuers,
|
|
13
|
+
coveredIssuerMatch,
|
|
12
14
|
issuerNameOf,
|
|
13
15
|
normalizeCdsTrades,
|
|
14
16
|
spreadLevelsOf,
|
|
@@ -29,6 +31,12 @@ export interface CdsActivity {
|
|
|
29
31
|
query: string | null;
|
|
30
32
|
/** The issuer name actually sent, after any symbol lookup. */
|
|
31
33
|
issuer: string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Set when a name matched no covered issuer and was sent as typed or as its
|
|
36
|
+
* listing's name: the covered issuers closest to what was typed, for the
|
|
37
|
+
* view to offer when the tape has nothing under it either.
|
|
38
|
+
*/
|
|
39
|
+
unmatched?: { nearest: string[] } | null;
|
|
32
40
|
/** How many days of the tape the trades cover. */
|
|
33
41
|
days: number;
|
|
34
42
|
trades: CdsTrade[];
|
|
@@ -38,61 +46,164 @@ export type CdsActivityLoader = (issuer: string | null) => Promise<CdsActivity>;
|
|
|
38
46
|
|
|
39
47
|
export type CdsFetch = (params: { issuer?: string; days?: number; limit?: number }) => Promise<CloudCdsResponse>;
|
|
40
48
|
export type InstrumentSearch = (query: string, limit?: number) => Promise<InstrumentSearchResult[]>;
|
|
49
|
+
/** The names of the issuers whose 5Y line has a level, as the tape spells them. */
|
|
50
|
+
export type CoveredIssuers = () => Promise<readonly string[]>;
|
|
41
51
|
|
|
42
52
|
/**
|
|
43
|
-
* A
|
|
44
|
-
*
|
|
45
|
-
* name it.
|
|
53
|
+
* A ticker as typed: symbol punctuation, no spaces, and no lowercase letter.
|
|
54
|
+
* `WHR` and `BRK.B` are tickers; `goodyear` and `Paramount` are names.
|
|
46
55
|
*/
|
|
47
|
-
const TICKER_LIKE = /^[A-
|
|
56
|
+
const TICKER_LIKE = /^[A-Z0-9][A-Z0-9.^:-]{0,11}$/;
|
|
48
57
|
const US_PRIMARY_EXCHANGES = new Set(["AMEX", "NASDAQ", "NYSE", "NYSEAMERICAN", "NYSEARCA"]);
|
|
49
58
|
|
|
59
|
+
/** The US venues besides the primary exchanges a listing can trade on: a receipt's or a delisted company's line. */
|
|
60
|
+
const US_OTHER_EXCHANGES = new Set(["ARCA", "BATS", "IEX", "OTC", "OTCMKTS", "PNK"]);
|
|
61
|
+
|
|
62
|
+
const listingExchange = (result: InstrumentSearchResult) => (result.primaryExchange || result.exchange).toUpperCase().replace(/[^A-Z]/g, "");
|
|
63
|
+
|
|
50
64
|
function isPrimaryUsCommonStock(result: InstrumentSearchResult): boolean {
|
|
51
|
-
const exchange = (result.primaryExchange || result.exchange).toUpperCase().replace(/[^A-Z]/g, "");
|
|
52
65
|
const type = result.type.toLowerCase();
|
|
53
66
|
return result.currency === "USD"
|
|
54
|
-
&& US_PRIMARY_EXCHANGES.has(
|
|
67
|
+
&& US_PRIMARY_EXCHANGES.has(listingExchange(result))
|
|
55
68
|
&& (type === "stk" || type === "equity" || type.includes("common stock"));
|
|
56
69
|
}
|
|
57
70
|
|
|
71
|
+
/** A line on a US venue in dollars (a receipt, a line over the counter); a listing abroad is not one. */
|
|
72
|
+
function isUsLine(result: InstrumentSearchResult): boolean {
|
|
73
|
+
const exchange = listingExchange(result);
|
|
74
|
+
return (!result.currency || result.currency === "USD") && (US_PRIMARY_EXCHANGES.has(exchange) || US_OTHER_EXCHANGES.has(exchange));
|
|
75
|
+
}
|
|
76
|
+
|
|
58
77
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* metadata to expand it, and its quote name never arrives for a fixed
|
|
64
|
-
* binding, so the shared instrument search is the only reliable expansion. A
|
|
65
|
-
* search outage falls back to the raw input rather than taking the CDS
|
|
66
|
-
* request down with it.
|
|
78
|
+
* Whether a listing carries what was typed: the words in its name or a name
|
|
79
|
+
* search knows it by (`searchAliases`), or a ticker it had (`formerIssuer`:
|
|
80
|
+
* BRCM on Broadcom Inc.). The first US hit for a name that does not is
|
|
81
|
+
* another company.
|
|
67
82
|
*/
|
|
83
|
+
function carriesTyped(result: InstrumentSearchResult, typed: string): boolean {
|
|
84
|
+
const symbol = typed.toUpperCase();
|
|
85
|
+
if (result.formerIssuer?.symbol?.toUpperCase() === symbol) return true;
|
|
86
|
+
return [result.name, ...result.searchAliases ?? [], result.formerIssuer?.name]
|
|
87
|
+
.some((name) => !!name && coveredIssuerMatch(typed, [name]) != null);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The covered issuers' names, from the same request as their levels. */
|
|
91
|
+
export function coveredIssuersFrom(fetchLevels: CdsLevelsFetch): CoveredIssuers {
|
|
92
|
+
return async () => ((await fetchLevels()).levels ?? []).flatMap((level) => (typeof level.issuer === "string" ? [level.issuer] : []));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const defaultCoveredIssuers = coveredIssuersFrom(() => apiClient.getCloudCdsLevels());
|
|
96
|
+
|
|
97
|
+
async function searchOrNone(searchInstruments: InstrumentSearch, query: string): Promise<InstrumentSearchResult[]> {
|
|
98
|
+
try {
|
|
99
|
+
return await searchInstruments(query);
|
|
100
|
+
} catch {
|
|
101
|
+
// Intentional ignore: a search outage sends the name as typed instead of failing the CDS request.
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function coveredOrNone(coveredIssuers: CoveredIssuers): Promise<readonly string[]> {
|
|
107
|
+
try {
|
|
108
|
+
return await coveredIssuers();
|
|
109
|
+
} catch {
|
|
110
|
+
// Intentional ignore: without the covered list the name goes on to the listing and the tape.
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface CdsIssuerResolution {
|
|
116
|
+
/** The name the tape is asked for. */
|
|
117
|
+
issuer: string;
|
|
118
|
+
/** Set when a name matched no covered issuer; see `CdsActivity.unmatched`. */
|
|
119
|
+
unmatched: { nearest: string[] } | null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The backend matches DTCC issuer names, so what was typed has to become one.
|
|
124
|
+
*
|
|
125
|
+
* Any one-word input, whatever its case (`CHTR`, `chtr`), is first looked up
|
|
126
|
+
* among the tickers whose debt sits with another entity: CHTR is CCO
|
|
127
|
+
* Holdings, LLC.
|
|
128
|
+
*
|
|
129
|
+
* A ticker (`WHR`) becomes the listed company's name without its share class
|
|
130
|
+
* ("Alphabet Inc.", not "Alphabet Inc. Class A Common Stock"): its primary US
|
|
131
|
+
* common stock where exchanges reuse the symbol, else its US line. An
|
|
132
|
+
* untracked symbol has no local metadata to expand it, and its quote name
|
|
133
|
+
* never arrives for a fixed binding, so the shared instrument search is the
|
|
134
|
+
* only reliable expansion.
|
|
135
|
+
*
|
|
136
|
+
* Then the names of the covered issuers (`goodyear`, `GOODYEAR`, `paramount `
|
|
137
|
+
* are The Goodyear Tire & Rubber Company and Paramount Global), by words of
|
|
138
|
+
* three letters or more: `f` and `ge` go to the symbol they are. Only then a
|
|
139
|
+
* listing abroad with that exact symbol, and only when it names a covered
|
|
140
|
+
* issuer: Goodyear India, the first hit for `goodyear`, is no CDS name. Last,
|
|
141
|
+
* the primary US common stock with the symbol, or the first one whose name
|
|
142
|
+
* carries what was typed. Nothing matched, the input goes to the tape as
|
|
143
|
+
* typed, with the covered issuers closest to it.
|
|
144
|
+
*
|
|
145
|
+
* A longer name in capitals and lowercase is already a company name and is
|
|
146
|
+
* sent as written. A search outage falls back to the raw input rather than
|
|
147
|
+
* taking the CDS request down with it.
|
|
148
|
+
*/
|
|
149
|
+
async function resolveCdsIssuer(
|
|
150
|
+
query: string,
|
|
151
|
+
searchInstruments: InstrumentSearch,
|
|
152
|
+
coveredIssuers: CoveredIssuers = defaultCoveredIssuers,
|
|
153
|
+
): Promise<CdsIssuerResolution> {
|
|
154
|
+
const ticker = TICKER_LIKE.test(query);
|
|
155
|
+
const oneWord = !/\s/.test(query);
|
|
156
|
+
if (!oneWord && query !== query.toLowerCase()) return { issuer: issuerNameOf(query), unmatched: null };
|
|
157
|
+
const referenceEntity = oneWord ? cdsReferenceEntity(query) : null;
|
|
158
|
+
if (referenceEntity) return { issuer: referenceEntity, unmatched: null };
|
|
159
|
+
const symbol = query.toUpperCase();
|
|
160
|
+
let results: InstrumentSearchResult[] | null = null;
|
|
161
|
+
let abroad: InstrumentSearchResult | undefined;
|
|
162
|
+
if (ticker) {
|
|
163
|
+
results = await searchOrNone(searchInstruments, query);
|
|
164
|
+
const exactMatches = results.filter((result) => result.symbol.toUpperCase() === symbol);
|
|
165
|
+
const name = (exactMatches.find(isPrimaryUsCommonStock) ?? exactMatches.find(isUsLine))?.name?.trim();
|
|
166
|
+
if (name) return { issuer: issuerNameOf(name), unmatched: null };
|
|
167
|
+
abroad = exactMatches.find((result) => !!result.name?.trim());
|
|
168
|
+
}
|
|
169
|
+
const covered = await coveredOrNone(coveredIssuers);
|
|
170
|
+
const named = coveredIssuerMatch(query, covered);
|
|
171
|
+
if (named) return { issuer: named, unmatched: null };
|
|
172
|
+
// A line abroad never wins on its own: only as the covered issuer it names.
|
|
173
|
+
const abroadIssuer = abroad ? coveredIssuerMatch(issuerNameOf(abroad.name.trim()), covered) : null;
|
|
174
|
+
if (abroadIssuer) return { issuer: abroadIssuer, unmatched: null };
|
|
175
|
+
results ??= await searchOrNone(searchInstruments, query);
|
|
176
|
+
const primary = results.filter(isPrimaryUsCommonStock);
|
|
177
|
+
const listing = (primary.find((result) => result.symbol.toUpperCase() === symbol) ?? primary.find((result) => carriesTyped(result, query)))?.name?.trim();
|
|
178
|
+
if (listing) {
|
|
179
|
+
const issuer = issuerNameOf(listing);
|
|
180
|
+
const listed = coveredIssuerMatch(issuer, covered);
|
|
181
|
+
return listed
|
|
182
|
+
? { issuer: listed, unmatched: null }
|
|
183
|
+
: { issuer, unmatched: { nearest: closestCoveredIssuers(query, covered) } };
|
|
184
|
+
}
|
|
185
|
+
return { issuer: ticker ? query : issuerNameOf(query), unmatched: { nearest: closestCoveredIssuers(query, covered) } };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The name `resolveCdsIssuer` sends to the tape, for a caller that only needs that. */
|
|
68
189
|
export async function resolveIssuerName(
|
|
69
190
|
issuer: string,
|
|
70
191
|
searchInstruments: InstrumentSearch,
|
|
192
|
+
coveredIssuers?: CoveredIssuers,
|
|
71
193
|
): Promise<string> {
|
|
72
|
-
|
|
73
|
-
const referenceEntity = cdsReferenceEntity(issuer);
|
|
74
|
-
if (referenceEntity) return referenceEntity;
|
|
75
|
-
try {
|
|
76
|
-
const results = await searchInstruments(issuer);
|
|
77
|
-
const symbol = issuer.toUpperCase();
|
|
78
|
-
const exactMatches = results.filter((result) => result.symbol.toUpperCase() === symbol);
|
|
79
|
-
const match = exactMatches.find(isPrimaryUsCommonStock) ?? exactMatches[0] ?? results[0];
|
|
80
|
-
const name = match?.name?.trim();
|
|
81
|
-
return name ? issuerNameOf(name) : issuer;
|
|
82
|
-
} catch {
|
|
83
|
-
return issuer;
|
|
84
|
-
}
|
|
194
|
+
return (await resolveCdsIssuer(issuer, searchInstruments, coveredIssuers)).issuer;
|
|
85
195
|
}
|
|
86
196
|
|
|
87
197
|
export async function loadCdsActivity(
|
|
88
198
|
issuer: string | null,
|
|
89
199
|
fetchCds: CdsFetch = (params) => apiClient.getCloudCds(params),
|
|
90
200
|
searchInstruments: InstrumentSearch = (query, limit) => apiClient.searchInstruments(query, limit),
|
|
201
|
+
coveredIssuers: CoveredIssuers = defaultCoveredIssuers,
|
|
91
202
|
): Promise<CdsActivity> {
|
|
92
203
|
const query = issuer?.trim() || null;
|
|
93
|
-
const resolved = query ? await
|
|
204
|
+
const resolved = query ? await resolveCdsIssuer(query, searchInstruments, coveredIssuers) : null;
|
|
94
205
|
const response = await fetchCds({
|
|
95
|
-
...(resolved ? { issuer: resolved } : {}),
|
|
206
|
+
...(resolved ? { issuer: resolved.issuer } : {}),
|
|
96
207
|
days: CDS_TRADE_DAYS,
|
|
97
208
|
limit: CDS_TRADE_LIMIT,
|
|
98
209
|
});
|
|
@@ -100,7 +211,8 @@ export async function loadCdsActivity(
|
|
|
100
211
|
source: response.source,
|
|
101
212
|
asOf: response.asOf,
|
|
102
213
|
query,
|
|
103
|
-
issuer: resolved,
|
|
214
|
+
issuer: resolved?.issuer ?? null,
|
|
215
|
+
...(resolved?.unmatched ? { unmatched: resolved.unmatched } : {}),
|
|
104
216
|
days: CDS_TRADE_DAYS,
|
|
105
217
|
trades: normalizeCdsTrades(response.trades ?? []),
|
|
106
218
|
};
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
} from "../../../types/plugin";
|
|
10
10
|
import {
|
|
11
11
|
CDS_TRADE_DAYS,
|
|
12
|
+
coveredIssuersFrom,
|
|
12
13
|
loadCdsActivity,
|
|
13
14
|
loadCdsSpreadHistory,
|
|
14
15
|
loadCdsSpreadLevels,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
type CdsHistoryFetch,
|
|
18
19
|
type CdsLevelsFetch,
|
|
19
20
|
type CdsSpreadHistory,
|
|
21
|
+
type CoveredIssuers,
|
|
20
22
|
type InstrumentSearch,
|
|
21
23
|
} from "./client";
|
|
22
24
|
import { formatContract } from "../credit-boards/model";
|
|
@@ -35,6 +37,7 @@ import {
|
|
|
35
37
|
formatSpreadMove,
|
|
36
38
|
formatSpreadRange,
|
|
37
39
|
formatUpfront,
|
|
40
|
+
hasPointsQuotes,
|
|
38
41
|
issuerListSort,
|
|
39
42
|
quotedLevel,
|
|
40
43
|
lastTradeOf,
|
|
@@ -154,7 +157,7 @@ function tradeForFormatting(row: Record<string, unknown>): CdsTrade {
|
|
|
154
157
|
return row as unknown as CdsTrade;
|
|
155
158
|
}
|
|
156
159
|
|
|
157
|
-
const TRADE_COLUMNS = [
|
|
160
|
+
const TRADE_COLUMNS: HeadlessPaneColumn[] = [
|
|
158
161
|
{ key: "eventAt", header: "Time UTC", shrink: false, format: (value: unknown) => formatEventTime(Number(value)) },
|
|
159
162
|
{ key: "issuer", header: "Issuer" },
|
|
160
163
|
{ key: "maturity", header: "Maturity" },
|
|
@@ -175,6 +178,22 @@ const TRADE_COLUMNS = [
|
|
|
175
178
|
},
|
|
176
179
|
];
|
|
177
180
|
|
|
181
|
+
/**
|
|
182
|
+
* A print that quoted points upfront in its spread field reads in a Points
|
|
183
|
+
* column, shown when one did; exports write the number as quoted.
|
|
184
|
+
*/
|
|
185
|
+
const POINTS_COLUMN: HeadlessPaneColumn = {
|
|
186
|
+
key: "pointsUpfront",
|
|
187
|
+
header: "Points",
|
|
188
|
+
align: "right",
|
|
189
|
+
format: (value: unknown) => (typeof value === "number" ? String(value) : "--"),
|
|
190
|
+
textFormat: (value: unknown) => formatPoints(numberOrNull(value)),
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const tradeColumns = (points: boolean): HeadlessPaneColumn[] => (
|
|
194
|
+
points ? [...TRADE_COLUMNS.slice(0, -1), POINTS_COLUMN, TRADE_COLUMNS.at(-1)!] : TRADE_COLUMNS
|
|
195
|
+
);
|
|
196
|
+
|
|
178
197
|
/**
|
|
179
198
|
* The pane's figures over its 5Y chart, as label and value lines: the level,
|
|
180
199
|
* its date, the 1W and 1M moves through the roll and the range charted, in
|
|
@@ -227,24 +246,29 @@ interface CdsReportInputs {
|
|
|
227
246
|
function projectCdsHeadless({ activity, history, levels, sort, days = DAILY_DAYS.defaultValue }: CdsReportInputs): HeadlessBundleResult {
|
|
228
247
|
const errors: string[] = [];
|
|
229
248
|
const notes: string[] = [];
|
|
230
|
-
const empty = noTradesMessage({ query: activity.query, issuer: activity.issuer, days: activity.days });
|
|
231
249
|
const metadata = { issuer: activity.issuer, days: activity.days };
|
|
232
250
|
if (activity.query) {
|
|
233
251
|
const line = history instanceof Error || !history ? null : history;
|
|
234
252
|
if (history instanceof Error) errors.push("5Y spread history unavailable.");
|
|
253
|
+
// A name that matched no covered issuer, with no trades and no line under it on the tape either.
|
|
254
|
+
const unmatched = activity.unmatched && !activity.trades.length && line && !line.issuer ? activity.unmatched : null;
|
|
255
|
+
const empty = noTradesMessage({ query: activity.query, issuer: activity.issuer, days: activity.days, unmatched });
|
|
235
256
|
const entries = line ? spreadEntries(line) : [];
|
|
236
|
-
// The name the tape was queried for heads the figures, as in the pane, so a ticker says what it became
|
|
237
|
-
|
|
257
|
+
// The name the tape was queried for heads the figures, as in the pane, so a ticker says what it became;
|
|
258
|
+
// a name sent as typed takes the one the tape answered with.
|
|
259
|
+
const name = displayIssuerName((activity.unmatched ? line?.issuer : null) ?? activity.issuer ?? line?.issuer ?? activity.query);
|
|
238
260
|
const why = line ? noLevelMessage(line.noLevelReason, line.points.at(-1)?.date ?? null) : null;
|
|
239
261
|
if (why) notes.push(`${why}.`);
|
|
240
|
-
else if (
|
|
262
|
+
else if (unmatched) {
|
|
263
|
+
if (!unmatched.nearest.length) notes.push("fn CDS lists the covered issuers.");
|
|
264
|
+
} else if (line && entries.length === 0) notes.push(`No 5Y spread level for ${name} in the last two years.`);
|
|
241
265
|
// The 5Y figures are dated by their level's day, each trade and each daily level by its own.
|
|
242
266
|
const levelDate = line ? spreadSummary(line)?.date : undefined;
|
|
243
267
|
const sections: HeadlessBundleSection[] = [
|
|
244
268
|
...(entries.length ? [{ title: name, entries, symbol: name, ...(levelDate ? { asOf: levelDate } : {}) }] : []),
|
|
245
269
|
{
|
|
246
270
|
title: "Trades",
|
|
247
|
-
columns:
|
|
271
|
+
columns: tradeColumns(hasPointsQuotes(activity.trades)),
|
|
248
272
|
rows: sortTrades(activity.trades, DEFAULT_TRADE_SORT).map((trade) => ({ ...trade })),
|
|
249
273
|
empty,
|
|
250
274
|
rowSymbolKey: "issuer",
|
|
@@ -265,6 +289,7 @@ function projectCdsHeadless({ activity, history, levels, sort, days = DAILY_DAYS
|
|
|
265
289
|
...(notes.length ? { notes } : {}),
|
|
266
290
|
};
|
|
267
291
|
}
|
|
292
|
+
const empty = noTradesMessage({ query: null, issuer: null, days: activity.days });
|
|
268
293
|
if (levels instanceof Error) errors.push("5Y spreads unavailable.");
|
|
269
294
|
const issuers = withSpreadLevels(summarizeIssuers(activity.trades), levels instanceof Error ? null : levels ?? null);
|
|
270
295
|
return {
|
|
@@ -294,14 +319,15 @@ export interface CdsHeadlessDependencies {
|
|
|
294
319
|
issuer: string | null,
|
|
295
320
|
fetchCds: CdsFetch,
|
|
296
321
|
searchInstruments: InstrumentSearch,
|
|
322
|
+
coveredIssuers: CoveredIssuers,
|
|
297
323
|
): Promise<CdsActivity>;
|
|
298
324
|
loadHistory?(issuer: string, fetchHistory: CdsHistoryFetch): Promise<CdsSpreadHistory>;
|
|
299
325
|
loadLevels?: typeof loadCdsSpreadLevels;
|
|
300
326
|
}
|
|
301
327
|
|
|
302
328
|
const defaultDependencies: CdsHeadlessDependencies = {
|
|
303
|
-
load: (_args, issuer, fetchCds, searchInstruments) => (
|
|
304
|
-
loadCdsActivity(issuer, fetchCds, searchInstruments)
|
|
329
|
+
load: (_args, issuer, fetchCds, searchInstruments, coveredIssuers) => (
|
|
330
|
+
loadCdsActivity(issuer, fetchCds, searchInstruments, coveredIssuers)
|
|
305
331
|
),
|
|
306
332
|
loadHistory: (issuer, fetchHistory) => loadCdsSpreadHistory(issuer, fetchHistory),
|
|
307
333
|
loadLevels: loadCdsSpreadLevels,
|
|
@@ -353,6 +379,7 @@ export function createCdsHeadless(
|
|
|
353
379
|
issuer,
|
|
354
380
|
(params) => ctx.apiClient.getCloudCds(params),
|
|
355
381
|
(query, limit) => ctx.apiClient.searchInstruments(query, limit),
|
|
382
|
+
coveredIssuersFrom(fetchLevels),
|
|
356
383
|
);
|
|
357
384
|
if (activity.query) {
|
|
358
385
|
// The line follows the name the tape was queried for, as the pane's does.
|