gloomberb 0.18.2 → 0.18.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/package.json +1 -1
  2. package/patches/electrobun@1.18.1.patch +106 -5
  3. package/src/api-client/data.ts +15 -0
  4. package/src/api-client/index.ts +2 -0
  5. package/src/api-client/request.ts +5 -3
  6. package/src/api-client/telemetry.ts +17 -0
  7. package/src/app/global-shortcuts.ts +1 -16
  8. package/src/app/runtime/update.ts +28 -8
  9. package/src/architecture/pane-tabs-allowlist.json +65 -0
  10. package/src/cli/commands/market.ts +73 -33
  11. package/src/cli/commands/ticker.ts +82 -25
  12. package/src/cli/help.ts +1 -1
  13. package/src/cli/index.ts +8 -0
  14. package/src/cli/issuer-membership.ts +103 -0
  15. package/src/cli/listing-arg.ts +15 -2
  16. package/src/cli/name-entries.ts +51 -12
  17. package/src/cli/pane-functions/capabilities.ts +2 -0
  18. package/src/cli/pane-functions/catalog.ts +16 -0
  19. package/src/cli/pane-functions/dom.ts +2 -1
  20. package/src/cli/pane-functions/headless.ts +25 -6
  21. package/src/cli/pane-functions/index.ts +64 -5
  22. package/src/cli/pane-functions/resolver.ts +19 -5
  23. package/src/cli/report-tables.ts +136 -18
  24. package/src/cli/result.ts +12 -4
  25. package/src/cli/symbol-list.ts +47 -11
  26. package/src/components/chart/composite/composite-chart.tsx +14 -5
  27. package/src/components/chart/composite/drawing-store.ts +26 -6
  28. package/src/components/chart/composite/interactions.ts +2 -1
  29. package/src/components/chart/composite/panel-surface.tsx +11 -0
  30. package/src/components/chart/composite/types.ts +6 -0
  31. package/src/components/chart/composite/use-composite-chart-keyboard.ts +1 -1
  32. package/src/components/command-bar/routes/root/command-items.ts +32 -6
  33. package/src/components/data-table/view.tsx +23 -3
  34. package/src/components/index.ts +1 -0
  35. package/src/components/layout/detached-pane-shell.tsx +1 -0
  36. package/src/components/layout/floating-pane.tsx +1 -0
  37. package/src/components/layout/header.tsx +228 -55
  38. package/src/components/layout/pane/header.tsx +10 -3
  39. package/src/components/layout/pane/index.tsx +1 -0
  40. package/src/components/layout/pane/pane-tabs.tsx +9 -1
  41. package/src/components/ui/stat-grid.tsx +28 -3
  42. package/src/components/ui/status.tsx +3 -1
  43. package/src/core/app-services.ts +4 -0
  44. package/src/i18n/es.ts +22 -0
  45. package/src/i18n/ja.ts +22 -0
  46. package/src/i18n/ko.ts +22 -0
  47. package/src/i18n/zh-cn.ts +22 -0
  48. package/src/i18n/zh-tw.ts +22 -0
  49. package/src/plugins/builtin/alerts/alert-engine.ts +3 -1
  50. package/src/plugins/builtin/alerts/events.ts +1 -0
  51. package/src/plugins/builtin/alerts/research-rules.ts +1 -0
  52. package/src/plugins/builtin/analytics/risk-pane.tsx +10 -7
  53. package/src/plugins/builtin/attention/pane.tsx +16 -14
  54. package/src/plugins/builtin/awards/pane.tsx +6 -6
  55. package/src/plugins/builtin/backtest/export.ts +1 -1
  56. package/src/plugins/builtin/broker-trading/dock.tsx +4 -2
  57. package/src/plugins/builtin/broker-trading/pane.tsx +6 -4
  58. package/src/plugins/builtin/broker-trading/ticket.tsx +10 -5
  59. package/src/plugins/builtin/buildout/pane/index.tsx +9 -6
  60. package/src/plugins/builtin/catalysts/client.ts +2 -2
  61. package/src/plugins/builtin/catalysts/pane.tsx +3 -3
  62. package/src/plugins/builtin/chart-composer/pane.tsx +1 -0
  63. package/src/plugins/builtin/commodity-balances/pane.tsx +3 -4
  64. package/src/plugins/builtin/company-attention/app-rank.tsx +4 -5
  65. package/src/plugins/builtin/company-attention/pane.tsx +13 -11
  66. package/src/plugins/builtin/company-kpis/pane.tsx +4 -3
  67. package/src/plugins/builtin/correlation/headless.ts +2 -1
  68. package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
  69. package/src/plugins/builtin/correlation/settings.ts +1 -0
  70. package/src/plugins/builtin/cot/pane.tsx +4 -3
  71. package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
  72. package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
  73. package/src/plugins/builtin/doe/pane.tsx +1 -1
  74. package/src/plugins/builtin/equity-screener/headless.ts +10 -0
  75. package/src/plugins/builtin/equity-screener/model.ts +28 -0
  76. package/src/plugins/builtin/equity-screener/pane.tsx +105 -98
  77. package/src/plugins/builtin/exposure/pane.tsx +10 -9
  78. package/src/plugins/builtin/gpu/board.tsx +4 -1
  79. package/src/plugins/builtin/gpu/changes.tsx +10 -10
  80. package/src/plugins/builtin/gpu/equities.tsx +4 -1
  81. package/src/plugins/builtin/gpu/history.tsx +4 -2
  82. package/src/plugins/builtin/gpu/pane.tsx +5 -3
  83. package/src/plugins/builtin/holders/client.ts +2 -1
  84. package/src/plugins/builtin/holders/headless.ts +2 -1
  85. package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
  86. package/src/plugins/builtin/insider/base-values.ts +11 -7
  87. package/src/plugins/builtin/insider/client.ts +3 -0
  88. package/src/plugins/builtin/insider/headless.ts +19 -6
  89. package/src/plugins/builtin/insider/index.tsx +17 -8
  90. package/src/plugins/builtin/insider/issuer.ts +34 -4
  91. package/src/plugins/builtin/insider/signals-client.ts +2 -2
  92. package/src/plugins/builtin/insider/table-model.ts +35 -13
  93. package/src/plugins/builtin/insider/table.tsx +5 -2
  94. package/src/plugins/builtin/iv-history/events.ts +145 -0
  95. package/src/plugins/builtin/iv-history/format.ts +3 -0
  96. package/src/plugins/builtin/iv-history/headless.ts +91 -7
  97. package/src/plugins/builtin/jodi/pane.tsx +4 -4
  98. package/src/plugins/builtin/market-movers/client.ts +4 -3
  99. package/src/plugins/builtin/market-movers/screener.ts +4 -3
  100. package/src/plugins/builtin/notes/cli.ts +1 -1
  101. package/src/plugins/builtin/notes/export.ts +17 -6
  102. package/src/plugins/builtin/notes/files.ts +16 -5
  103. package/src/plugins/builtin/options/enrichment-model.ts +10 -9
  104. package/src/plugins/builtin/options/model-spot.ts +70 -4
  105. package/src/plugins/builtin/options/view.tsx +41 -13
  106. package/src/plugins/builtin/options-calculator/pane.tsx +3 -2
  107. package/src/plugins/builtin/options-positioning/headless.ts +11 -3
  108. package/src/plugins/builtin/options-positioning/model.ts +24 -0
  109. package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
  110. package/src/plugins/builtin/options-scenario/client.ts +19 -1
  111. package/src/plugins/builtin/options-scenario/headless.ts +13 -6
  112. package/src/plugins/builtin/options-scenario/pane.tsx +13 -5
  113. package/src/plugins/builtin/plugin-marketplace/pane.tsx +5 -4
  114. package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
  115. package/src/plugins/builtin/power/client.ts +2 -2
  116. package/src/plugins/builtin/power/pane.tsx +36 -31
  117. package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
  118. package/src/plugins/builtin/reverse-dcf/model.ts +4 -3
  119. package/src/plugins/builtin/seasonality/client.ts +2 -1
  120. package/src/plugins/builtin/sectors/client.ts +4 -3
  121. package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
  122. package/src/plugins/builtin/shared/research-trading.tsx +2 -0
  123. package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
  124. package/src/plugins/builtin/shared/volatility/options.ts +2 -1
  125. package/src/plugins/builtin/social-mentions/client.ts +2 -1
  126. package/src/plugins/builtin/vol-surface/client.ts +23 -3
  127. package/src/plugins/builtin/vol-surface/headless.ts +33 -14
  128. package/src/plugins/builtin/vol-surface/model.ts +30 -10
  129. package/src/plugins/builtin/vol-surface/pane.tsx +33 -13
  130. package/src/plugins/builtin/vol-surface/stored.ts +34 -3
  131. package/src/plugins/builtin/vol-surface/tables.ts +14 -0
  132. package/src/renderers/dom/desktop/controls.tsx +1 -1
  133. package/src/renderers/dom/desktop/query-bar.tsx +2 -0
  134. package/src/renderers/dom/host/tabs.tsx +16 -5
  135. package/src/renderers/dom/host/top-surface.ts +127 -0
  136. package/src/renderers/dom/styles.css +39 -0
  137. package/src/renderers/electrobun/bun/core-capabilities.ts +9 -1
  138. package/src/renderers/electrobun/bun/desktop/backend.ts +10 -0
  139. package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
  140. package/src/renderers/electrobun/view/news-source.ts +4 -2
  141. package/src/renderers/electrobun/view/remote/asset-data-client.ts +8 -1
  142. package/src/renderers/opentui/start.tsx +7 -0
  143. package/src/sources/gloomberb-cloud/index.ts +7 -0
  144. package/src/sources/provider-router/index.ts +16 -0
  145. package/src/telemetry/crash-reports.ts +4 -0
  146. package/src/telemetry/update-failures-node.ts +59 -0
  147. package/src/telemetry/update-failures.ts +307 -0
  148. package/src/tickers/company-name-listing.ts +9 -7
  149. package/src/tickers/company-name.ts +43 -0
  150. package/src/tickers/listing-fate.ts +30 -0
  151. package/src/types/data-provider.ts +6 -0
  152. package/src/types/headless.ts +7 -0
  153. package/src/types/issuer-listings.ts +28 -0
  154. package/src/updater/failure.ts +163 -0
  155. package/src/updater/index.ts +87 -23
  156. package/src/utils/currency-pair.ts +26 -0
  157. package/src/utils/sec.ts +68 -0
@@ -1,5 +1,13 @@
1
1
  import type { UpdateChannel } from "../types/config";
2
+ import { reportUpdateFailure, type UpdateInstallKind } from "../telemetry/update-failures";
2
3
  import { compareSemver, isPrereleaseVersion } from "../utils/semver";
4
+ import {
5
+ classifyUpdateError,
6
+ httpStatusFailureCode,
7
+ type UpdateFailure,
8
+ type UpdateFailureCode,
9
+ type UpdateFailurePhase,
10
+ } from "./failure";
3
11
  import {
4
12
  GITHUB_LATEST_RELEASE_API_URL,
5
13
  GITHUB_RELEASES_API_URL,
@@ -49,6 +57,8 @@ export interface UpdateProgress {
49
57
  message?: string;
50
58
  /** With `ready`: this host can apply the update and relaunch on request. */
51
59
  canRestart?: boolean;
60
+ /** With `error`: the step that failed and why, which the header says in a few words. */
61
+ failure?: UpdateFailure;
52
62
  }
53
63
 
54
64
  export type UpdateAction =
@@ -68,7 +78,7 @@ export type UpdateCheckResult =
68
78
  /** `notice`: why a newer-looking state is not an update, such as a nightly build waiting for a stable release. */
69
79
  | { kind: "current"; notice?: string }
70
80
  | { kind: "disabled" }
71
- | { kind: "error"; error: string };
81
+ | { kind: "error"; error: string; failure?: UpdateFailure };
72
82
 
73
83
  /**
74
84
  * On the stable channel a nightly build is never replaced by an older stable
@@ -302,6 +312,23 @@ export function canSelfUpdate(release: Pick<ReleaseInfo, "updateAction"> | null
302
312
  return release?.updateAction.kind === "self" || release?.updateAction.kind === "desktop";
303
313
  }
304
314
 
315
+ /**
316
+ * Whether the install-update key has something to retry: a release this build
317
+ * can install itself whose last attempt failed. A fresh one downloads on its
318
+ * own, and a manual or managed one names its own command. (A downloaded
319
+ * desktop update has its own use of the key: restarting into it.)
320
+ */
321
+ export function canRetryUpdate(state: {
322
+ updateAvailable: ReleaseInfo | null;
323
+ updateProgress: UpdateProgress | null;
324
+ updateCheckInProgress: boolean;
325
+ }): boolean {
326
+ return !!state.updateAvailable
327
+ && state.updateProgress?.phase === "error"
328
+ && !state.updateCheckInProgress
329
+ && canSelfUpdate(state.updateAvailable);
330
+ }
331
+
305
332
  /** A downloaded update this host can install and relaunch into when the user asks. */
306
333
  export function canRestartToApply(progress: UpdateProgress | null | undefined): boolean {
307
334
  return progress?.phase === "ready" && progress.canRestart === true;
@@ -344,6 +371,11 @@ export async function checkForUpdateDetailed(
344
371
  return { kind: "disabled" };
345
372
  }
346
373
  const nightly = channel === "nightly" && updateAction.kind === "self";
374
+ const failed = (error: string, code: UpdateFailureCode, targetVersion?: string): UpdateCheckResult => {
375
+ const failure: UpdateFailure = { phase: "check", code };
376
+ reportUpdateFailure(failure, { targetVersion, channel: nightly ? "nightly" : "stable" });
377
+ return { kind: "error", error, failure };
378
+ };
347
379
 
348
380
  let timeout: ReturnType<typeof setTimeout> | undefined;
349
381
  try {
@@ -356,7 +388,7 @@ export async function checkForUpdateDetailed(
356
388
  });
357
389
 
358
390
  if (!res.ok) {
359
- return { kind: "error", error: `GitHub returned ${res.status}` };
391
+ return failed(`GitHub returned ${res.status}`, httpStatusFailureCode(res.status));
360
392
  }
361
393
 
362
394
  const payload = await res.json();
@@ -379,16 +411,10 @@ export async function checkForUpdateDetailed(
379
411
 
380
412
  const asset = resolveReleaseAsset(data.assets);
381
413
  if (!asset) {
382
- return {
383
- kind: "error",
384
- error: `No compatible release asset found for ${getAssetBaseName()}`,
385
- };
414
+ return failed(`No compatible release asset found for ${getAssetBaseName()}`, "no_build", version);
386
415
  }
387
416
  if (!asset.checksum) {
388
- return {
389
- kind: "error",
390
- error: `Release asset ${asset.name} is missing a valid SHA-256 digest`,
391
- };
417
+ return failed(`Release asset ${asset.name} is missing a valid SHA-256 digest`, "no_build", version);
392
418
  }
393
419
 
394
420
  return {
@@ -406,12 +432,10 @@ export async function checkForUpdateDetailed(
406
432
  };
407
433
  } catch (error: unknown) {
408
434
  if (error instanceof Error && error.name === "AbortError") {
409
- return { kind: "error", error: "Update check timed out" };
435
+ return failed("Update check timed out", "timeout");
410
436
  }
411
- return {
412
- kind: "error",
413
- error: error instanceof Error ? error.message : "Update check failed",
414
- };
437
+ const code = classifyUpdateError(error);
438
+ return failed(error instanceof Error ? error.message : "Update check failed", code === "unknown" && error instanceof SyntaxError ? "bad_response" : code);
415
439
  } finally {
416
440
  if (timeout) clearTimeout(timeout);
417
441
  }
@@ -507,14 +531,20 @@ export async function performUpdate(
507
531
  }
508
532
 
509
533
  const checksum = /^[a-f\d]{64}$/i.exec(release.checksum ?? "")?.[0].toLowerCase();
534
+ const fail = (failure: UpdateFailure, error: string) => {
535
+ reportUpdateFailure(failure, { targetVersion: release.version, channel: release.channel });
536
+ onProgress({ phase: "error", error, failure });
537
+ };
510
538
  if (!checksum) {
511
- onProgress({ phase: "error", error: "Self-update requires a valid SHA-256 checksum." });
539
+ fail({ phase: "verify", code: "no_build" }, "Self-update requires a valid SHA-256 checksum.");
512
540
  return;
513
541
  }
514
542
 
515
543
  const updatePath = execPath + ".update";
516
544
  const oldPath = execPath + ".old";
517
545
  let unlinkUpdatePath: ((path: string) => void) | null = null;
546
+ /** The step under way, which a thrown error is reported against. */
547
+ let phase: UpdateFailurePhase = "download";
518
548
 
519
549
  try {
520
550
  // Not import(): Bun folds even a specifier held in a variable and bundles
@@ -531,7 +561,7 @@ export async function performUpdate(
531
561
 
532
562
  const res = await fetch(release.downloadUrl);
533
563
  if (!res.ok || !res.body) {
534
- throw new Error(`Download failed: ${res.status}`);
564
+ throw new UpdateStepError({ phase: "download", code: httpStatusFailureCode(res.status) }, `Download failed: ${res.status}`);
535
565
  }
536
566
 
537
567
  const contentLength = Number(res.headers.get("content-length") || 0);
@@ -540,7 +570,18 @@ export async function performUpdate(
540
570
  let received = 0;
541
571
 
542
572
  while (true) {
543
- const { done, value } = await reader.read();
573
+ let chunk: Awaited<ReturnType<typeof reader.read>>;
574
+ try {
575
+ chunk = await reader.read();
576
+ } catch (error) {
577
+ // The connection broke partway: whatever it says, the file is incomplete.
578
+ const code = classifyUpdateError(error);
579
+ throw new UpdateStepError(
580
+ { phase: "download", code: code === "timeout" ? code : "interrupted" },
581
+ error instanceof Error ? error.message : "Download interrupted",
582
+ );
583
+ }
584
+ const { done, value } = chunk;
544
585
  if (done) break;
545
586
  chunks.push(value);
546
587
  received += value.length;
@@ -551,7 +592,14 @@ export async function performUpdate(
551
592
  });
552
593
  }
553
594
  }
595
+ if (contentLength > 0 && received < contentLength) {
596
+ throw new UpdateStepError(
597
+ { phase: "download", code: "interrupted" },
598
+ `Download stopped at ${received} of ${contentLength} bytes`,
599
+ );
600
+ }
554
601
 
602
+ phase = "verify";
555
603
  const downloaded = new Uint8Array(received);
556
604
  let offset = 0;
557
605
  for (const chunk of chunks) {
@@ -560,11 +608,12 @@ export async function performUpdate(
560
608
  }
561
609
  const actual = createHash("sha256").update(downloaded).digest("hex");
562
610
  if (actual !== checksum) {
563
- throw new Error(`Checksum mismatch: expected ${checksum}, got ${actual}`);
611
+ throw new UpdateStepError({ phase: "verify", code: "corrupt" }, `Checksum mismatch: expected ${checksum}, got ${actual}`);
564
612
  }
565
613
  const nextBinary = release.compressed
566
614
  ? new Uint8Array(gunzipSync(downloaded))
567
615
  : downloaded;
616
+ phase = "apply";
568
617
  await Bun.write(updatePath, nextBinary);
569
618
  chmodSync(updatePath, 0o755);
570
619
 
@@ -600,9 +649,24 @@ export async function performUpdate(
600
649
  try {
601
650
  unlinkUpdatePath?.(updatePath);
602
651
  } catch { /* Intentional ignore: Remove a partial update download while the original failure is reported through update progress. */ }
603
- onProgress({
604
- phase: "error",
605
- error: err instanceof Error ? err.message : "Update failed",
606
- });
652
+ const failure = err instanceof UpdateStepError ? err.failure : { phase, code: classifyUpdateError(err) };
653
+ fail(failure, err instanceof Error ? err.message : "Update failed");
607
654
  }
608
655
  }
656
+
657
+ /** A failed step whose code is known where it is thrown. */
658
+ class UpdateStepError extends Error {
659
+ constructor(readonly failure: UpdateFailure, message: string) {
660
+ super(message);
661
+ this.name = "UpdateStepError";
662
+ }
663
+ }
664
+
665
+ /** How the running terminal app was installed, as update failure reports name it. */
666
+ export function terminalInstallKind(action: UpdateAction | null = detectUpdateAction()): UpdateInstallKind {
667
+ if (!action) return "terminal-source";
668
+ if (action.kind === "self") return "terminal-binary";
669
+ if (action.kind === "managed") return "terminal-managed";
670
+ if (action.kind === "manual") return "terminal-package-manager";
671
+ return "unknown";
672
+ }
@@ -84,3 +84,29 @@ export function looksLikeCurrencyPair(symbol: string): boolean {
84
84
  const halves = /^([A-Z]{3})[^A-Z0-9]?([A-Z]{3})(?:=X)?$/.exec(code)?.slice(1) ?? /^([A-Z]{3})=X$/.exec(code)?.slice(1) ?? [];
85
85
  return halves.some(isCurrencyCode);
86
86
  }
87
+
88
+ /**
89
+ * Which currency of a pair markets quote it per: EUR, GBP, AUD and NZD go
90
+ * before the dollar, the dollar before CAD, CHF and the rest, and the yen goes
91
+ * last (EUR/USD, USD/CAD, USD/JPY, EUR/CAD, CAD/JPY).
92
+ */
93
+ const PAIR_BASE_ORDER = ["EUR", "GBP", "AUD", "NZD", "USD", "CAD", "CHF"];
94
+
95
+ function pairBaseRank(code: string): number {
96
+ const at = PAIR_BASE_ORDER.indexOf(code);
97
+ if (at >= 0) return at;
98
+ return code === "JPY" ? PAIR_BASE_ORDER.length + 1 : PAIR_BASE_ORDER.length;
99
+ }
100
+
101
+ /**
102
+ * A rate of `quote` per unit of `currency`, written the way markets quote
103
+ * the pair, as `fx` reads a pair: 0.0067 USD per JPY is USD/JPY 149.25, and
104
+ * 1.4 CAD per USD stays USD/CAD 1.4. Two codes of the same rank keep their
105
+ * order.
106
+ */
107
+ export function quotedFxPair(currency: string, quote: string, rate: number): { pair: string; rate: number } {
108
+ const from = currency.trim().toUpperCase(), into = quote.trim().toUpperCase();
109
+ return pairBaseRank(into) < pairBaseRank(from)
110
+ ? { pair: `${into}/${from}`, rate: 1 / rate }
111
+ : { pair: `${from}/${into}`, rate };
112
+ }
package/src/utils/sec.ts CHANGED
@@ -163,6 +163,74 @@ export function noSecReportsNotice(listing: { symbol: string; exchange: string;
163
163
  return `${shortListingTitle(listing)} files no SEC reports.`;
164
164
  }
165
165
 
166
+ /** The country of a venue abroad, ISO 3166 alpha-2, for the companies an SEC report may list there. */
167
+ const VENUE_COUNTRY: Readonly<Record<string, string>> = {
168
+ TSX: "CA", TSXV: "CA", CSE: "CA", NEO: "CA",
169
+ LSE: "GB",
170
+ ASX: "AU", CXA: "AU", NZX: "NZ",
171
+ SWX: "CH",
172
+ XETRA: "DE", FWB2: "DE", EPA: "FR", AMS: "NL", BRU: "BE", LIS: "PT", BIT: "IT", MCE: "ES", ISE: "IE",
173
+ SFB: "SE", HEL: "FI", CPH: "DK", OSL: "NO", ICEX: "IS", WSE: "PL", PSE: "CZ", VIE: "AT", WBDM: "AT",
174
+ JPX: "JP", HKEX: "HK", KRX: "KR", KOSDAQ: "KR", TWSE: "TW", TPEX: "TW", SSE: "CN", SZSE: "CN",
175
+ NSE: "IN", BSE: "IN", SGX: "SG", SET: "TH",
176
+ BMV: "MX", B3: "BR", BYMA: "AR", JSE: "ZA", TASE: "IL", BIST: "TR", TADAWUL: "SA",
177
+ };
178
+
179
+ /** The European Economic Area, whose issuers report managers' dealings under the Market Abuse Regulation. */
180
+ const EEA = new Set(["AT", "BE", "CZ", "DE", "DK", "ES", "FI", "FR", "IE", "IS", "IT", "NL", "NO", "PL", "PT", "SE"]);
181
+
182
+ /** Insider reports outside the SEC by the name they go by, by the issuer's country. */
183
+ const HOME_INSIDER_REPORTS: Readonly<Record<string, string>> = {
184
+ CA: "Canadian insider reports (SEDI)",
185
+ GB: "UK insider reports (PDMR notifications)",
186
+ AU: "Australian insider reports (Appendix 3Y)",
187
+ };
188
+
189
+ /** The country of a venue abroad that a company's insiders may report in (TSX is CA); null for a US venue or one not placed. */
190
+ export function venueHomeCountry(exchange: string | null | undefined): string | null {
191
+ return VENUE_COUNTRY[canonicalExchange(exchange ?? "")] ?? null;
192
+ }
193
+
194
+ /** A country's English name ("Japan"), or null when the runtime has none. */
195
+ function countryName(code: string): string | null {
196
+ try {
197
+ const name = new Intl.DisplayNames(["en"], { type: "region" }).of(code);
198
+ return name && name !== code ? name : null;
199
+ } catch {
200
+ // Intentional ignore: a runtime without region names leaves the home market unnamed.
201
+ return null;
202
+ }
203
+ }
204
+
205
+ /**
206
+ * What a company from abroad's insiders report at home, which Form 4 data
207
+ * does not cover: "Canadian insider reports (SEDI) not covered." Where home
208
+ * is comes from the company's country (Gloom Cloud's issuer listings: RY in
209
+ * New York is Royal Bank of Canada), else its listing's venue abroad (RY:TSX).
210
+ * A foreign private issuer placed by neither gets its home market unnamed.
211
+ * Null for a US company, and for a US listing of a company that files as a
212
+ * domestic one, whose insiders file Form 4.
213
+ */
214
+ export function homeInsiderReportsNote({ country, exchange, foreignPrivateIssuer = false }: {
215
+ /** The company's country, ISO 3166 alpha-2, when known. */
216
+ country?: string | null;
217
+ /** The listing's venue. */
218
+ exchange?: string | null;
219
+ foreignPrivateIssuer?: boolean;
220
+ }): string | null {
221
+ const venueHome = venueHomeCountry(exchange) ?? "";
222
+ if (!venueHome && !foreignPrivateIssuer) return null;
223
+ // ZZ is the service's own word for a country it does not know.
224
+ const given = normalize(country ?? "");
225
+ const home = (given !== "ZZ" ? given : "") || venueHome;
226
+ if (home === "US") return null;
227
+ if (!home) return foreignPrivateIssuer ? "Home-market insider reports not covered." : null;
228
+ const named = HOME_INSIDER_REPORTS[home] ?? (EEA.has(home) ? "European insider reports (MAR managers' transactions)" : null);
229
+ if (named) return `${named} not covered.`;
230
+ const place = countryName(home);
231
+ return place ? `Insider reports filed in ${place} not covered.` : "Home-market insider reports not covered.";
232
+ }
233
+
166
234
  /**
167
235
  * The key an issuer read is cached and deduplicated under: the ticker as given
168
236
  * for a US listing or a symbol with no venue, SYMBOL:VENUE for a listing