@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.
Files changed (203) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +1 -1
  4. package/patches/electrobun@1.18.1.patch +23 -1
  5. package/src/api-client/app-attention.ts +3 -0
  6. package/src/api-client/beneficial-owners.ts +3 -0
  7. package/src/api-client/company-kpis.ts +3 -0
  8. package/src/api-client/data.ts +2 -1
  9. package/src/api-client/hiring.ts +3 -0
  10. package/src/api-client/index.ts +4 -0
  11. package/src/api-client/ipo.ts +24 -1
  12. package/src/api-client/mna.ts +3 -0
  13. package/src/api-client/paths.ts +9 -1
  14. package/src/api-client/roadmap.ts +85 -0
  15. package/src/api-client/types.ts +12 -0
  16. package/src/architecture/core-builtin-allowlist.json +2 -2
  17. package/src/cli/commands/market.ts +35 -12
  18. package/src/cli/commands/news-search.ts +22 -0
  19. package/src/cli/commands/overview.ts +86 -20
  20. package/src/cli/commands/ticker.ts +32 -2
  21. package/src/cli/errors.ts +17 -2
  22. package/src/cli/glossary.ts +6 -0
  23. package/src/cli/index-roots.ts +0 -7
  24. package/src/cli/listing-arg.ts +57 -10
  25. package/src/cli/pane-functions/capabilities.ts +1 -0
  26. package/src/cli/pane-functions/discovery.ts +21 -1
  27. package/src/cli/pane-functions/freshness.ts +7 -1
  28. package/src/cli/pane-functions/function-help.ts +7 -0
  29. package/src/cli/pane-functions/index.ts +6 -3
  30. package/src/cli/quote-answer.ts +1 -1
  31. package/src/cli/saved-copy-persistence.ts +88 -0
  32. package/src/components/command-bar/list/model.ts +5 -0
  33. package/src/components/command-bar/routes/root/runtime.ts +6 -7
  34. package/src/components/command-bar/selection-runtime.ts +52 -3
  35. package/src/components/command-bar/surface/index.tsx +2 -0
  36. package/src/components/layout/pane/capture-stamp.ts +2 -2
  37. package/src/components/layout/pane/footer/index.tsx +22 -6
  38. package/src/components/layout/pane/footer/model.ts +83 -15
  39. package/src/components/layout/pane/status-footer.ts +35 -2
  40. package/src/components/layout/status-widget-space.ts +9 -0
  41. package/src/components/ticker/list-table-view.tsx +4 -20
  42. package/src/components/ui/display.tsx +18 -3
  43. package/src/components/ui/icon.tsx +4 -1
  44. package/src/data/plugin-cache.ts +5 -3
  45. package/src/data/saved-copies.ts +8 -0
  46. package/src/docs/snapshot.json +32 -5
  47. package/src/i18n/es.ts +2 -4
  48. package/src/i18n/ja.ts +2 -4
  49. package/src/i18n/ko.ts +2 -4
  50. package/src/i18n/zh-cn.ts +2 -4
  51. package/src/i18n/zh-tw.ts +2 -4
  52. package/src/market-data/depositary-pair.ts +25 -1
  53. package/src/market-data/quotes/metadata.ts +7 -0
  54. package/src/news/aggregator.ts +7 -1
  55. package/src/news/types.ts +6 -0
  56. package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
  57. package/src/plugins/builtin/alerts/cli.ts +34 -11
  58. package/src/plugins/builtin/alerts/event-rows.ts +24 -3
  59. package/src/plugins/builtin/alerts/events-pane.tsx +15 -6
  60. package/src/plugins/builtin/alerts/headless.ts +10 -6
  61. package/src/plugins/builtin/cds/client.ts +142 -30
  62. package/src/plugins/builtin/cds/headless.ts +35 -8
  63. package/src/plugins/builtin/cds/model.ts +145 -18
  64. package/src/plugins/builtin/cds/pane.tsx +16 -5
  65. package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
  66. package/src/plugins/builtin/chart-composer/index.tsx +4 -5
  67. package/src/plugins/builtin/chat/controller/index.ts +5 -1
  68. package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
  69. package/src/plugins/builtin/chat/controller/state.ts +25 -2
  70. package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
  71. package/src/plugins/builtin/chat/controller/utils.ts +7 -2
  72. package/src/plugins/builtin/chat/status-preview.ts +79 -0
  73. package/src/plugins/builtin/chat/status-widget.tsx +147 -10
  74. package/src/plugins/builtin/chat/unread-badge.ts +51 -10
  75. package/src/plugins/builtin/clinical-trials/client.ts +49 -4
  76. package/src/plugins/builtin/clinical-trials/model.ts +86 -22
  77. package/src/plugins/builtin/clinical-trials/pane.tsx +46 -27
  78. package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
  79. package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +87 -9
  80. package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
  81. package/src/plugins/builtin/company-attention/headless.ts +2 -1
  82. package/src/plugins/builtin/company-attention/pane.tsx +3 -0
  83. package/src/plugins/builtin/company-kpis/headless.ts +8 -3
  84. package/src/plugins/builtin/company-kpis/model.ts +73 -5
  85. package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
  86. package/src/plugins/builtin/composite-plugins.ts +2 -1
  87. package/src/plugins/builtin/credit-documents/headless.ts +3 -3
  88. package/src/plugins/builtin/doe/headless.ts +3 -2
  89. package/src/plugins/builtin/doe/pane.tsx +13 -3
  90. package/src/plugins/builtin/earnings-calls/data.ts +18 -6
  91. package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
  92. package/src/plugins/builtin/earnings-calls/pane.tsx +7 -0
  93. package/src/plugins/builtin/econ/calendar-model.ts +48 -1
  94. package/src/plugins/builtin/econ/calendar-source.ts +4 -2
  95. package/src/plugins/builtin/econ/detail-view.tsx +2 -2
  96. package/src/plugins/builtin/econ/figures.ts +48 -0
  97. package/src/plugins/builtin/econ/index.tsx +14 -5
  98. package/src/plugins/builtin/econ/print-watch.ts +3 -1
  99. package/src/plugins/builtin/econ-statistics/client.ts +11 -0
  100. package/src/plugins/builtin/econ-statistics/headless.ts +10 -2
  101. package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
  102. package/src/plugins/builtin/econ-statistics/stats.ts +3 -0
  103. package/src/plugins/builtin/help/index.tsx +61 -59
  104. package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
  105. package/src/plugins/builtin/help/table-cells.tsx +13 -2
  106. package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
  107. package/src/plugins/builtin/holders/beneficial-view.tsx +3 -0
  108. package/src/plugins/builtin/holders/headless.ts +4 -2
  109. package/src/plugins/builtin/holders/pane.tsx +3 -0
  110. package/src/plugins/builtin/holders/report-rows.ts +4 -1
  111. package/src/plugins/builtin/insider/headless.ts +2 -2
  112. package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
  113. package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
  114. package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
  115. package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
  116. package/src/plugins/builtin/macro-day/headless.ts +12 -5
  117. package/src/plugins/builtin/macro-day/index.tsx +1 -1
  118. package/src/plugins/builtin/macro-day/model.ts +39 -5
  119. package/src/plugins/builtin/macro-day/pane.tsx +27 -19
  120. package/src/plugins/builtin/market-movers/client.ts +2 -0
  121. package/src/plugins/builtin/market-movers/headless.ts +3 -1
  122. package/src/plugins/builtin/market-movers/screener.ts +8 -4
  123. package/src/plugins/builtin/mna/headless.ts +2 -0
  124. package/src/plugins/builtin/mna/pane.tsx +3 -1
  125. package/src/plugins/builtin/news/headless.ts +3 -1
  126. package/src/plugins/builtin/news/index.tsx +3 -0
  127. package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -6
  128. package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
  129. package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
  130. package/src/plugins/builtin/portfolio-list/table.tsx +4 -6
  131. package/src/plugins/builtin/realized-vol/client.ts +13 -5
  132. package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
  133. package/src/plugins/builtin/realized-vol/headless.ts +8 -4
  134. package/src/plugins/builtin/realized-vol/model.ts +37 -6
  135. package/src/plugins/builtin/realized-vol/pane.tsx +9 -7
  136. package/src/plugins/builtin/research/event-model.ts +30 -4
  137. package/src/plugins/builtin/research/index.tsx +7 -3
  138. package/src/plugins/builtin/research/relative-valuation-headless.ts +8 -25
  139. package/src/plugins/builtin/research/relative-valuation-pane.tsx +52 -15
  140. package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
  141. package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
  142. package/src/plugins/builtin/risk-factors/data.ts +4 -2
  143. package/src/plugins/builtin/risk-factors/headless.ts +5 -2
  144. package/src/plugins/builtin/roadmap/index.tsx +28 -0
  145. package/src/plugins/builtin/roadmap/model.ts +67 -0
  146. package/src/plugins/builtin/roadmap/pane.tsx +244 -0
  147. package/src/plugins/builtin/scanner/live-headless.ts +21 -2
  148. package/src/plugins/builtin/sec/headless.ts +2 -1
  149. package/src/plugins/builtin/sec/index.tsx +3 -0
  150. package/src/plugins/builtin/shared/cloud-resource.ts +13 -5
  151. package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
  152. package/src/plugins/builtin/shared/report-freshness.ts +8 -0
  153. package/src/plugins/builtin/shared/research-cells.tsx +1 -1
  154. package/src/plugins/builtin/shared/series-cache.ts +4 -4
  155. package/src/plugins/builtin/thirteenf/api.ts +12 -1
  156. package/src/plugins/builtin/thirteenf/data.ts +7 -7
  157. package/src/plugins/builtin/thirteenf/headless.ts +31 -4
  158. package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
  159. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
  160. package/src/plugins/builtin/ticker-detail/pane.tsx +11 -6
  161. package/src/plugins/builtin/treasury-auctions/headless.ts +21 -12
  162. package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
  163. package/src/plugins/builtin/volatility/pane.tsx +5 -2
  164. package/src/plugins/builtin/yield-curve/client.ts +61 -1
  165. package/src/plugins/builtin/yield-curve/headless.ts +9 -6
  166. package/src/plugins/builtin/yield-curve/index.tsx +34 -10
  167. package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
  168. package/src/plugins/catalog-browser.ts +2 -1
  169. package/src/plugins/host-link.ts +1 -1
  170. package/src/portfolio/column-values.ts +11 -7
  171. package/src/renderers/dom/desktop/icons.tsx +7 -0
  172. package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
  173. package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
  174. package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
  175. package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
  176. package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
  177. package/src/renderers/electrobun/bun/index.ts +26 -2
  178. package/src/renderers/electrobun/bun/window/create.ts +43 -4
  179. package/src/renderers/electrobun/bun/window/events.ts +20 -0
  180. package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
  181. package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
  182. package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
  183. package/src/renderers/electrobun/shared/protocol.ts +2 -0
  184. package/src/renderers/electrobun/view/news-source.ts +2 -0
  185. package/src/renderers/electrobun/view/ui-host.tsx +8 -0
  186. package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
  187. package/src/sources/gloomberb-cloud/index.ts +42 -8
  188. package/src/tickers/company-name-listing.ts +53 -13
  189. package/src/tickers/company-name.ts +6 -1
  190. package/src/tickers/index-roots.ts +7 -0
  191. package/src/tickers/listing-fate.ts +2 -2
  192. package/src/tickers/search/index.ts +6 -2
  193. package/src/tickers/search/ranking.ts +5 -1
  194. package/src/tickers/search/upsert.ts +13 -0
  195. package/src/tickers/ticker-changes.ts +8 -4
  196. package/src/types/company-from.ts +40 -0
  197. package/src/types/data-provider.ts +7 -0
  198. package/src/types/financials.ts +27 -0
  199. package/src/types/headless.ts +6 -0
  200. package/src/utils/crypto-pair.ts +121 -7
  201. package/src/utils/declared-dividends.ts +1 -1
  202. package/src/utils/fundamentals.ts +36 -5
  203. 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 status = rule.status === "active" && rule.triggeredAt ? "triggered" : rule.status;
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 (!apiClient.getSessionToken() || !rules.some((rule) => rule.kind !== "iv_level" && !readScheduledRule(rule))) return empty;
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 allWarnings = [...(alertsError ? [alertsError] : []), ...(events.error ? [events.error] : []), ...warnings, ...cloud.warning ? [cloud.warning] : []];
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
- ctx.printResult({ data: { changed: !ctx.cliOptions.dryRun, dryRun: ctx.cliOptions.dryRun, alert: rule }, ...(read.length > 0 ? { warnings: read } : {}) }, {
1052
- text: (data) => `Added alert ${data.alert.id}: ${alertKindLabel(rule.kind)} ${eventAlertTarget(rule)} (${delivery}).${dryRunNote(data.dryRun)}`,
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
- return status === "paused" ? "Paused" : status === "triggered" ? "Alerted" : "Active";
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
- { id: "status", label: "State", width: 8, align: "left" },
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>): HeadlessPaneRow {
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>): HeadlessRowsResult {
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: [...rules].sort((a, b) => b.createdAt - a.createdAt).map((rule) => eventRow(rule, now, checks)),
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 bare ticker with normal symbol punctuation and no spaces. Anything longer or
44
- * containing a space is already a company name and is sent as the tape would
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-Za-z0-9][A-Za-z0-9.^:-]{0,11}$/;
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(exchange)
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
- * The backend matches DTCC issuer names, so a ticker has to become a company
60
- * name first: its reference entity when the debt sits with another entity,
61
- * else the listed company's name without its share class ("Alphabet Inc.",
62
- * not "Alphabet Inc. Class A Common Stock"). An untracked symbol has no local
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
- if (!TICKER_LIKE.test(issuer)) return issuerNameOf(issuer);
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 resolveIssuerName(query, searchInstruments) : null;
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
- const name = displayIssuerName(activity.issuer ?? line?.issuer ?? activity.query);
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 (line && entries.length === 0) notes.push(`No 5Y spread level for ${name} in the last two years.`);
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: TRADE_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.