gloomberb 0.18.2 → 0.18.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/patches/electrobun@1.18.1.patch +106 -5
- package/src/api-client/data.ts +15 -0
- package/src/api-client/index.ts +2 -0
- package/src/api-client/request.ts +5 -3
- package/src/api-client/telemetry.ts +17 -0
- package/src/app/global-shortcuts.ts +1 -16
- package/src/app/runtime/update.ts +28 -8
- package/src/architecture/pane-tabs-allowlist.json +65 -0
- package/src/cli/commands/market.ts +73 -33
- package/src/cli/commands/ticker.ts +82 -25
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +8 -0
- package/src/cli/issuer-membership.ts +103 -0
- package/src/cli/listing-arg.ts +15 -2
- package/src/cli/name-entries.ts +51 -12
- package/src/cli/pane-functions/capabilities.ts +2 -0
- package/src/cli/pane-functions/catalog.ts +16 -0
- package/src/cli/pane-functions/dom.ts +2 -1
- package/src/cli/pane-functions/headless.ts +25 -6
- package/src/cli/pane-functions/index.ts +64 -5
- package/src/cli/pane-functions/resolver.ts +19 -5
- package/src/cli/report-tables.ts +136 -18
- package/src/cli/result.ts +12 -4
- package/src/cli/symbol-list.ts +47 -11
- package/src/components/chart/composite/composite-chart.tsx +14 -5
- package/src/components/chart/composite/drawing-store.ts +26 -6
- package/src/components/chart/composite/interactions.ts +2 -1
- package/src/components/chart/composite/panel-surface.tsx +11 -0
- package/src/components/chart/composite/types.ts +6 -0
- package/src/components/chart/composite/use-composite-chart-keyboard.ts +1 -1
- package/src/components/command-bar/routes/root/command-items.ts +32 -6
- package/src/components/data-table/view.tsx +23 -3
- package/src/components/index.ts +1 -0
- package/src/components/layout/detached-pane-shell.tsx +1 -0
- package/src/components/layout/floating-pane.tsx +1 -0
- package/src/components/layout/header.tsx +228 -55
- package/src/components/layout/pane/header.tsx +10 -3
- package/src/components/layout/pane/index.tsx +1 -0
- package/src/components/layout/pane/pane-tabs.tsx +9 -1
- package/src/components/ui/stat-grid.tsx +28 -3
- package/src/components/ui/status.tsx +3 -1
- package/src/core/app-services.ts +4 -0
- package/src/i18n/es.ts +22 -0
- package/src/i18n/ja.ts +22 -0
- package/src/i18n/ko.ts +22 -0
- package/src/i18n/zh-cn.ts +22 -0
- package/src/i18n/zh-tw.ts +22 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +3 -1
- package/src/plugins/builtin/alerts/events.ts +1 -0
- package/src/plugins/builtin/alerts/research-rules.ts +1 -0
- package/src/plugins/builtin/analytics/risk-pane.tsx +10 -7
- package/src/plugins/builtin/attention/pane.tsx +16 -14
- package/src/plugins/builtin/awards/pane.tsx +6 -6
- package/src/plugins/builtin/backtest/export.ts +1 -1
- package/src/plugins/builtin/broker-trading/dock.tsx +4 -2
- package/src/plugins/builtin/broker-trading/pane.tsx +6 -4
- package/src/plugins/builtin/broker-trading/ticket.tsx +10 -5
- package/src/plugins/builtin/buildout/pane/index.tsx +9 -6
- package/src/plugins/builtin/catalysts/client.ts +2 -2
- package/src/plugins/builtin/catalysts/pane.tsx +3 -3
- package/src/plugins/builtin/chart-composer/pane.tsx +1 -0
- package/src/plugins/builtin/commodity-balances/pane.tsx +3 -4
- package/src/plugins/builtin/company-attention/app-rank.tsx +4 -5
- package/src/plugins/builtin/company-attention/pane.tsx +13 -11
- package/src/plugins/builtin/company-kpis/pane.tsx +4 -3
- package/src/plugins/builtin/correlation/headless.ts +2 -1
- package/src/plugins/builtin/correlation/relationship/model.ts +1 -0
- package/src/plugins/builtin/correlation/settings.ts +1 -0
- package/src/plugins/builtin/cot/pane.tsx +4 -3
- package/src/plugins/builtin/credit-documents/pane.tsx +8 -7
- package/src/plugins/builtin/debt-maturities/pane.tsx +4 -3
- package/src/plugins/builtin/doe/pane.tsx +1 -1
- package/src/plugins/builtin/equity-screener/headless.ts +10 -0
- package/src/plugins/builtin/equity-screener/model.ts +28 -0
- package/src/plugins/builtin/equity-screener/pane.tsx +105 -98
- package/src/plugins/builtin/exposure/pane.tsx +10 -9
- package/src/plugins/builtin/gpu/board.tsx +4 -1
- package/src/plugins/builtin/gpu/changes.tsx +10 -10
- package/src/plugins/builtin/gpu/equities.tsx +4 -1
- package/src/plugins/builtin/gpu/history.tsx +4 -2
- package/src/plugins/builtin/gpu/pane.tsx +5 -3
- package/src/plugins/builtin/holders/client.ts +2 -1
- package/src/plugins/builtin/holders/headless.ts +2 -1
- package/src/plugins/builtin/holders/thirteenf-match.ts +3 -1
- package/src/plugins/builtin/insider/base-values.ts +11 -7
- package/src/plugins/builtin/insider/client.ts +3 -0
- package/src/plugins/builtin/insider/headless.ts +19 -6
- package/src/plugins/builtin/insider/index.tsx +17 -8
- package/src/plugins/builtin/insider/issuer.ts +34 -4
- package/src/plugins/builtin/insider/signals-client.ts +2 -2
- package/src/plugins/builtin/insider/table-model.ts +35 -13
- package/src/plugins/builtin/insider/table.tsx +5 -2
- package/src/plugins/builtin/iv-history/events.ts +145 -0
- package/src/plugins/builtin/iv-history/format.ts +3 -0
- package/src/plugins/builtin/iv-history/headless.ts +91 -7
- package/src/plugins/builtin/jodi/pane.tsx +4 -4
- package/src/plugins/builtin/market-movers/client.ts +4 -3
- package/src/plugins/builtin/market-movers/screener.ts +4 -3
- package/src/plugins/builtin/notes/cli.ts +1 -1
- package/src/plugins/builtin/notes/export.ts +17 -6
- package/src/plugins/builtin/notes/files.ts +16 -5
- package/src/plugins/builtin/options/enrichment-model.ts +10 -9
- package/src/plugins/builtin/options/model-spot.ts +70 -4
- package/src/plugins/builtin/options/view.tsx +41 -13
- package/src/plugins/builtin/options-calculator/pane.tsx +3 -2
- package/src/plugins/builtin/options-positioning/headless.ts +11 -3
- package/src/plugins/builtin/options-positioning/model.ts +24 -0
- package/src/plugins/builtin/options-positioning/pane.tsx +10 -12
- package/src/plugins/builtin/options-scenario/client.ts +19 -1
- package/src/plugins/builtin/options-scenario/headless.ts +13 -6
- package/src/plugins/builtin/options-scenario/pane.tsx +13 -5
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +5 -4
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
- package/src/plugins/builtin/power/client.ts +2 -2
- package/src/plugins/builtin/power/pane.tsx +36 -31
- package/src/plugins/builtin/reverse-dcf/client.ts +2 -1
- package/src/plugins/builtin/reverse-dcf/model.ts +4 -3
- package/src/plugins/builtin/seasonality/client.ts +2 -1
- package/src/plugins/builtin/sectors/client.ts +4 -3
- package/src/plugins/builtin/shared/cloud-resource.ts +15 -2
- package/src/plugins/builtin/shared/research-trading.tsx +2 -0
- package/src/plugins/builtin/shared/series-list-detail.tsx +3 -1
- package/src/plugins/builtin/shared/volatility/options.ts +2 -1
- package/src/plugins/builtin/social-mentions/client.ts +2 -1
- package/src/plugins/builtin/vol-surface/client.ts +23 -3
- package/src/plugins/builtin/vol-surface/headless.ts +33 -14
- package/src/plugins/builtin/vol-surface/model.ts +30 -10
- package/src/plugins/builtin/vol-surface/pane.tsx +33 -13
- package/src/plugins/builtin/vol-surface/stored.ts +34 -3
- package/src/plugins/builtin/vol-surface/tables.ts +14 -0
- package/src/renderers/dom/desktop/controls.tsx +1 -1
- package/src/renderers/dom/desktop/query-bar.tsx +2 -0
- package/src/renderers/dom/host/tabs.tsx +16 -5
- package/src/renderers/dom/host/top-surface.ts +127 -0
- package/src/renderers/dom/styles.css +39 -0
- package/src/renderers/electrobun/bun/core-capabilities.ts +9 -1
- package/src/renderers/electrobun/bun/desktop/backend.ts +10 -0
- package/src/renderers/electrobun/bun/desktop/update.ts +210 -30
- package/src/renderers/electrobun/view/news-source.ts +4 -2
- package/src/renderers/electrobun/view/remote/asset-data-client.ts +8 -1
- package/src/renderers/opentui/start.tsx +7 -0
- package/src/sources/gloomberb-cloud/index.ts +7 -0
- package/src/sources/provider-router/index.ts +16 -0
- package/src/telemetry/crash-reports.ts +4 -0
- package/src/telemetry/update-failures-node.ts +59 -0
- package/src/telemetry/update-failures.ts +307 -0
- package/src/tickers/company-name-listing.ts +9 -7
- package/src/tickers/company-name.ts +43 -0
- package/src/tickers/listing-fate.ts +30 -0
- package/src/types/data-provider.ts +6 -0
- package/src/types/headless.ts +7 -0
- package/src/types/issuer-listings.ts +28 -0
- package/src/updater/failure.ts +163 -0
- package/src/updater/index.ts +87 -23
- package/src/utils/currency-pair.ts +26 -0
- package/src/utils/sec.ts +68 -0
package/src/updater/index.ts
CHANGED
|
@@ -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
|
|
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
|
|
435
|
+
return failed("Update check timed out", "timeout");
|
|
410
436
|
}
|
|
411
|
-
|
|
412
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
604
|
-
|
|
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
|