gloomberb 0.17.2 → 0.17.4
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 +2 -1
- package/src/api-client/chat.ts +28 -0
- package/src/api-client/errors.ts +11 -1
- package/src/api-client/futures-curve.ts +16 -0
- package/src/api-client/index.ts +22 -9
- package/src/api-client/normalizers.ts +21 -0
- package/src/api-client/socket.ts +27 -0
- package/src/api-client/types.ts +3 -1
- package/src/app/runtime/startup.ts +4 -0
- package/src/cli/commands/market.ts +60 -9
- package/src/cli/commands/search.ts +43 -2
- package/src/cli/commands/ticker.ts +100 -21
- package/src/cli/desktop-pane-shot.ts +34 -9
- package/src/cli/fx-pair.ts +17 -0
- package/src/cli/glossary.ts +134 -6
- package/src/cli/history-rows.ts +16 -4
- package/src/cli/index.ts +44 -12
- package/src/cli/known-gaps.ts +21 -2
- package/src/cli/listing-arg.ts +19 -4
- package/src/cli/listing-suggestion.ts +33 -2
- package/src/cli/pane-functions/dom.ts +39 -6
- package/src/cli/pane-functions/freshness.ts +48 -5
- package/src/cli/pane-functions/function-help.ts +1 -1
- package/src/cli/pane-functions/headless.ts +85 -19
- package/src/cli/pane-functions/index.ts +60 -4
- package/src/cli/pane-functions/market-session.ts +69 -6
- package/src/cli/pane-functions/resolver.ts +4 -2
- package/src/cli/pane-functions/screenshot.ts +14 -1
- package/src/cli/scroll-truncation.ts +5 -1
- package/src/cli/symbol-args.ts +41 -3
- package/src/cli/symbol-list.ts +19 -7
- package/src/cli/unknown-command.ts +70 -0
- package/src/components/chart/native/surface/visibility.ts +37 -7
- package/src/components/index.ts +1 -1
- package/src/components/layout/header.tsx +348 -102
- package/src/components/markdown-text.tsx +40 -66
- package/src/components/market-board/index.tsx +12 -6
- package/src/components/metric-treemap/heat-surface.tsx +76 -14
- package/src/components/metric-treemap/heat-tiles.ts +5 -3
- package/src/components/metric-treemap/index.tsx +1 -1
- package/src/components/reader/detail-reader.tsx +28 -6
- package/src/components/ticker/badge/index.tsx +2 -0
- package/src/components/ui/button.tsx +24 -9
- package/src/components/ui/display.tsx +6 -1
- package/src/components/ui/external-link.tsx +10 -2
- package/src/components/ui/index.ts +1 -1
- package/src/components/ui/status.tsx +148 -5
- package/src/data/memory-resource-store.ts +1 -1
- package/src/data/resource-store.ts +13 -1
- package/src/docs/snapshot.json +13 -13
- package/src/i18n/es.ts +1 -0
- package/src/i18n/ja.ts +1 -0
- package/src/i18n/ko.ts +1 -0
- package/src/i18n/zh-cn.ts +1 -0
- package/src/i18n/zh-tw.ts +1 -0
- package/src/layout-marketplace/gallery-desktop.tsx +2 -2
- package/src/market-data/futures-board.ts +44 -1
- package/src/market-data/futures-contract.ts +10 -0
- package/src/market-data/market/format.ts +7 -2
- package/src/market-data/market/freshness.ts +25 -1
- package/src/market-data/quotes/regular-session-valuation.ts +32 -1
- package/src/market-data/quotes/resolution.ts +2 -0
- package/src/plugins/builtin/account-management/calendar-tab.tsx +1 -1
- package/src/plugins/builtin/account-management/pane.tsx +17 -13
- package/src/plugins/builtin/alerts/cli.ts +72 -11
- package/src/plugins/builtin/attention/headless.ts +19 -10
- package/src/plugins/builtin/attention/model.ts +22 -0
- package/src/plugins/builtin/awards/headless.ts +5 -1
- package/src/plugins/builtin/bond-calculator/pane.tsx +2 -1
- package/src/plugins/builtin/catalysts/headless.ts +5 -1
- package/src/plugins/builtin/changelog/index.tsx +11 -8
- package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +2 -0
- package/src/plugins/builtin/chat/attachments/desktop.tsx +2 -1
- package/src/plugins/builtin/chat/attachments/model.ts +5 -3
- package/src/plugins/builtin/chat/content/footer.tsx +6 -0
- package/src/plugins/builtin/chat/content/open-targets.ts +13 -4
- package/src/plugins/builtin/chat/content/transcript.tsx +2 -0
- package/src/plugins/builtin/chat/content/whole-message-copy.ts +70 -0
- package/src/plugins/builtin/chat/controller/connections.ts +18 -0
- package/src/plugins/builtin/chat/controller/fetch.ts +5 -2
- package/src/plugins/builtin/chat/controller/index.ts +49 -21
- package/src/plugins/builtin/chat/controller/notifications.ts +132 -72
- package/src/plugins/builtin/chat/controller/realtime.ts +37 -1
- package/src/plugins/builtin/chat/layout.ts +91 -31
- package/src/plugins/builtin/chat/markdown.ts +94 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -2
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +197 -80
- package/src/plugins/builtin/chat/message/render-state.ts +3 -0
- package/src/plugins/builtin/chat/message/terminal.tsx +4 -4
- package/src/plugins/builtin/cloud/thesis/exposure.ts +18 -2
- package/src/plugins/builtin/company-attention/headless.ts +8 -5
- package/src/plugins/builtin/company-attention/model.ts +53 -2
- package/src/plugins/builtin/company-attention/pane.tsx +1 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +7 -4
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -1
- package/src/plugins/builtin/correlation/relationship/pane.tsx +3 -7
- package/src/plugins/builtin/cot/client.ts +12 -4
- package/src/plugins/builtin/cot/model.ts +20 -0
- package/src/plugins/builtin/cot/pane.tsx +2 -2
- package/src/plugins/builtin/crypto-board/model.ts +7 -3
- package/src/plugins/builtin/custom-view/pane.tsx +2 -2
- package/src/plugins/builtin/debt-maturities/pane.tsx +1 -6
- package/src/plugins/builtin/debug/index.tsx +15 -15
- package/src/plugins/builtin/dividend-yield/client.ts +8 -1
- package/src/plugins/builtin/dividend-yield/headless.ts +4 -1
- package/src/plugins/builtin/dividend-yield/pane.tsx +6 -5
- package/src/plugins/builtin/docs/index.tsx +5 -3
- package/src/plugins/builtin/docs/pane.tsx +19 -2
- package/src/plugins/builtin/docs/targets.ts +5 -3
- package/src/plugins/builtin/earnings-calls/data.ts +22 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +24 -14
- package/src/plugins/builtin/equity-screener/headless.ts +4 -3
- package/src/plugins/builtin/equity-screener/live.ts +27 -1
- package/src/plugins/builtin/equity-screener/pane.tsx +21 -2
- package/src/plugins/builtin/fear-greed/charts.tsx +5 -1
- package/src/plugins/builtin/fear-greed/pane.tsx +1 -1
- package/src/plugins/builtin/futures/index.tsx +33 -6
- package/src/plugins/builtin/futures/model.ts +69 -5
- package/src/plugins/builtin/futures/series.ts +3 -3
- package/src/plugins/builtin/futures/table.tsx +31 -11
- package/src/plugins/builtin/futures/use-board-terms.ts +89 -0
- package/src/plugins/builtin/holders/pane.tsx +6 -1
- package/src/plugins/builtin/jobs/pane.tsx +12 -9
- package/src/plugins/builtin/market-heatmap/index.tsx +6 -15
- package/src/plugins/builtin/mna/client.ts +12 -2
- package/src/plugins/builtin/mna/detail.tsx +2 -2
- package/src/plugins/builtin/mna/pane.tsx +2 -2
- package/src/plugins/builtin/news/headless.ts +4 -2
- package/src/plugins/builtin/notes/owner.tsx +5 -4
- package/src/plugins/builtin/notes/ticker-notes-tab.tsx +2 -3
- package/src/plugins/builtin/options-calculator/pane.tsx +2 -3
- package/src/plugins/builtin/plugin-marketplace/pane.tsx +1 -1
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -3
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +9 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +6 -3
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +4 -12
- package/src/plugins/builtin/research/event-model.ts +14 -0
- package/src/plugins/builtin/research/headless-schema.ts +31 -1
- package/src/plugins/builtin/research/relative-valuation-columns.ts +289 -0
- package/src/plugins/builtin/research/relative-valuation-currency.ts +164 -0
- package/src/plugins/builtin/research/relative-valuation-headless.ts +94 -11
- package/src/plugins/builtin/research/relative-valuation-model.ts +4 -1
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +13 -4
- package/src/plugins/builtin/research/relative-valuation-peers.ts +302 -0
- package/src/plugins/builtin/research-search/data.ts +11 -0
- package/src/plugins/builtin/research-search/pane.tsx +13 -2
- package/src/plugins/builtin/scanner/hilo-pane.tsx +7 -17
- package/src/plugins/builtin/seasonality/client.ts +23 -0
- package/src/plugins/builtin/seasonality/pane.tsx +4 -2
- package/src/plugins/builtin/sectors/index.tsx +7 -3
- package/src/plugins/builtin/shared/report-freshness.ts +6 -2
- package/src/plugins/builtin/short-interest/headless.ts +13 -3
- package/src/plugins/builtin/short-interest/index.tsx +1 -1
- package/src/plugins/builtin/short-interest/model.ts +62 -3
- package/src/plugins/builtin/short-interest/pane.tsx +2 -2
- package/src/plugins/builtin/short-interest/schedule.ts +107 -0
- package/src/plugins/builtin/short-interest/watch-client.ts +54 -8
- package/src/plugins/builtin/short-interest/watch-headless.ts +8 -3
- package/src/plugins/builtin/short-interest/watch-model.ts +52 -23
- package/src/plugins/builtin/short-interest/watch-pane.tsx +1 -1
- package/src/plugins/builtin/supply-chain/flow.tsx +14 -4
- package/src/plugins/builtin/themes/headless.ts +1 -1
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +23 -27
- package/src/plugins/builtin/ticker-detail/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/overview/model.ts +4 -3
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +4 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +9 -4
- package/src/plugins/builtin/vol-surface/pane.tsx +16 -1
- package/src/plugins/builtin/volatility/chart-model.ts +1 -1
- package/src/plugins/builtin/volatility/charts.tsx +1 -1
- package/src/renderers/dom/build-assets.ts +17 -4
- package/src/renderers/dom/desktop/controls.tsx +2 -0
- package/src/renderers/dom/styles.css +6 -0
- package/src/renderers/electrobun/bun/desktop/macos-dock-badge.ts +4 -3
- package/src/renderers/electrobun/bun/desktop/macos-dock-icon.ts +2 -3
- package/src/renderers/electrobun/bun/desktop/macos-objc.ts +17 -6
- package/src/sources/gloomberb-cloud/normalizers.ts +13 -4
- package/src/sources/provider-router/batches.ts +2 -1
- package/src/sources/provider-router/cache.ts +3 -0
- package/src/sync/baseline.ts +19 -14
- package/src/sync/controller.ts +140 -40
- package/src/sync/core-contributors.ts +256 -32
- package/src/sync/types.ts +44 -0
- package/src/time-series/resolve.ts +24 -5
- package/src/types/data-provider.ts +8 -0
- package/src/types/financials.ts +40 -4
- package/src/types/headless.ts +43 -0
- package/src/types/plugin.ts +7 -0
- package/src/ui/resize-settle.ts +128 -0
- package/src/utils/calendar-date.ts +56 -0
- package/src/utils/decimal-comma.ts +31 -0
- package/src/utils/declared-dividends.ts +39 -0
- package/src/utils/forward-dividend.ts +34 -11
- package/src/utils/fundamentals.ts +13 -2
- package/src/utils/futures-generic.ts +25 -4
- package/src/utils/markdown.ts +180 -0
- package/src/utils/market-capitalization.ts +22 -0
- package/src/utils/plugin-proxy-hosts.json +5 -0
- package/src/utils/utc-time.ts +14 -3
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import type { DeclaredDividendAction } from "../types/financials";
|
|
2
|
+
import { formatDistributionAmount } from "./format";
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* What a forward dividend yield is everywhere it shows (fundamentals, DVD):
|
|
3
6
|
* the indicated annual rate Gloom Cloud serves over the price, dated by the
|
|
4
7
|
* ex-date of the distribution it annualizes. While no other is announced that
|
|
5
8
|
* is the latest distribution, whose ex-date has passed. Once the next one is
|
|
6
9
|
* declared the rate annualizes it, and its ex-date is still ahead: the basis
|
|
7
|
-
* then names the declared distribution by its ex-date and pay date,
|
|
8
|
-
* rate "as of" a
|
|
10
|
+
* then names the declared distribution by its ex-date and pay date, and by its
|
|
11
|
+
* amount when Gloom Cloud states one for that ex-date, never a rate "as of" a
|
|
12
|
+
* day that has not come. The amount is only ever the stated one, never the
|
|
13
|
+
* rate divided back.
|
|
9
14
|
*/
|
|
10
15
|
const LATEST_DISTRIBUTION = "latest distribution annualized";
|
|
11
16
|
|
|
@@ -20,25 +25,43 @@ function isDeclared(exDate: string | null, now: Date): exDate is string {
|
|
|
20
25
|
return exDate != null && exDate > now.toISOString().slice(0, 10);
|
|
21
26
|
}
|
|
22
27
|
|
|
28
|
+
interface DeclaredBasisOptions {
|
|
29
|
+
/** The declared distributions Gloom Cloud states (`declaredDividends`); only the one on the rate's ex-date counts. */
|
|
30
|
+
declared?: readonly DeclaredDividendAction[];
|
|
31
|
+
/** The currency their amounts are in; without it no amount is named. */
|
|
32
|
+
currency?: string | null;
|
|
33
|
+
now?: Date;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The declared row on the rate's ex-date, as `$0.98`, with its pay date; null when none is stated. */
|
|
37
|
+
function statedDeclaration(exDate: string, options: DeclaredBasisOptions): { amount: string; payDate: string | null } | null {
|
|
38
|
+
const row = options.currency ? options.declared?.find((declared) => declared.exDate === exDate) : undefined;
|
|
39
|
+
return row ? { amount: formatDistributionAmount(row.amount, options.currency!), payDate: isoDay(row.payDate) } : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
23
42
|
/**
|
|
24
43
|
* `latest distribution annualized, as of 2026-08-20`, or for a rate dated
|
|
25
|
-
* ahead, `annualized from the declared
|
|
26
|
-
* 2026-11-19
|
|
27
|
-
* date
|
|
44
|
+
* ahead, `annualized from the declared $0.98 payable 2026-12-10, ex
|
|
45
|
+
* 2026-11-19`, the amount and pay date when they are stated; the basis alone
|
|
46
|
+
* when the date is unknown. `payDate` is the pay date a source states beside
|
|
47
|
+
* that ex-date without a declared row (DVD's summary).
|
|
28
48
|
*/
|
|
29
49
|
export function forwardDividendBasis(
|
|
30
50
|
asOf: string | Date | null | undefined,
|
|
31
|
-
options: { payDate?: string | Date | null
|
|
51
|
+
options: DeclaredBasisOptions & { payDate?: string | Date | null } = {},
|
|
32
52
|
): string {
|
|
33
53
|
const exDate = isoDay(asOf);
|
|
34
54
|
if (!isDeclared(exDate, options.now ?? new Date())) return exDate ? `${LATEST_DISTRIBUTION}, as of ${exDate}` : LATEST_DISTRIBUTION;
|
|
35
|
-
const
|
|
36
|
-
|
|
55
|
+
const stated = statedDeclaration(exDate, options);
|
|
56
|
+
const payDate = stated?.payDate ?? isoDay(options.payDate);
|
|
57
|
+
return `annualized from the declared ${stated?.amount ?? "distribution"}${payDate ? ` payable ${payDate}` : ""}, ex ${exDate}`;
|
|
37
58
|
}
|
|
38
59
|
|
|
39
|
-
/** The same basis as a pane's short note under the figure: `as of 2026-08-20`, or `declared` beside the next ex-date it shows. */
|
|
40
|
-
export function forwardDividendNote(asOf: string | Date | null | undefined,
|
|
60
|
+
/** The same basis as a pane's short note under the figure: `as of 2026-08-20`, or `declared $0.98` beside the next ex-date it shows. */
|
|
61
|
+
export function forwardDividendNote(asOf: string | Date | null | undefined, options: DeclaredBasisOptions = {}): string | null {
|
|
41
62
|
const exDate = isoDay(asOf);
|
|
42
63
|
if (!exDate) return null;
|
|
43
|
-
|
|
64
|
+
if (!isDeclared(exDate, options.now ?? new Date())) return `as of ${exDate}`;
|
|
65
|
+
const stated = statedDeclaration(exDate, options);
|
|
66
|
+
return stated ? `declared ${stated.amount}` : "declared";
|
|
44
67
|
}
|
|
@@ -40,18 +40,29 @@ export function enterpriseValueCheck(
|
|
|
40
40
|
return fundamentals?.enterpriseValue === check.source ? check : undefined;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Whether the provider's EV was withdrawn: retracted (`unavailableFields`) and none served in its place, by the
|
|
45
|
+
* served check or by Gloom Cloud's own validation of the source observation, which leaves no check.
|
|
46
|
+
*/
|
|
47
|
+
export function enterpriseValueWithdrawn(fundamentals: Parameters<typeof enterpriseValueCheck>[0]): boolean {
|
|
48
|
+
return fundamentals?.enterpriseValue === undefined && !!fundamentals?.unavailableFields?.includes("enterpriseValue");
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
/** "(provider)" beside an EV more than 5% from market cap + debt - cash, or "unavailable" for a withdrawn one. */
|
|
44
52
|
export function enterpriseValueLabel(fundamentals: Parameters<typeof enterpriseValueCheck>[0]): "provider" | "unavailable" | undefined {
|
|
45
53
|
const status = enterpriseValueCheck(fundamentals)?.status;
|
|
46
|
-
return status === "differs" ? "provider" : status === "withdrawn" ? "unavailable" : undefined;
|
|
54
|
+
return status === "differs" ? "provider" : status === "withdrawn" || enterpriseValueWithdrawn(fundamentals) ? "unavailable" : undefined;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
/**
|
|
50
58
|
* The reconciliation line under a labelled or withdrawn EV: "EV 61.75B USD (provider) vs 63.75B computed from market
|
|
51
|
-
* cap + total debt - cash (balance sheet 2026-06-30)."
|
|
59
|
+
* cap + total debt - cash (balance sheet 2026-06-30)." For an EV withdrawn without a check, that the source failed
|
|
60
|
+
* validation. Undefined when the EV matches, is unchecked, or has no check. Read it from the fundamentals the rows
|
|
61
|
+
* show (atRegularSessionPrice moves the check with the EV and the market cap), so its figures are theirs.
|
|
52
62
|
*/
|
|
53
63
|
export function enterpriseValueReconciliationNote(fundamentals: Parameters<typeof enterpriseValueCheck>[0]): string | undefined {
|
|
54
64
|
const check = enterpriseValueCheck(fundamentals);
|
|
65
|
+
if (!check && enterpriseValueWithdrawn(fundamentals)) return "EV unavailable: the provider's figure failed validation.";
|
|
55
66
|
if (!check || !finiteNumber(check.computed) || (check.status !== "differs" && check.status !== "withdrawn")) return undefined;
|
|
56
67
|
const source = `${formatCompact(check.source)} ${check.currency}`;
|
|
57
68
|
const computed = formatCompact(check.computed);
|
|
@@ -28,9 +28,11 @@ export interface FuturesGeneric {
|
|
|
28
28
|
|
|
29
29
|
/** Roots Gloom Cloud archives daily, by the venue that lists them. */
|
|
30
30
|
const ROOT_VENUES: Readonly<Record<string, string>> = Object.fromEntries(([
|
|
31
|
-
["CME", ["ES", "NQ", "RTY", "
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
["CME", ["ES", "NQ", "RTY", "EMD", "RS1", "NKD", "NIY", "MES", "MNQ", "M2K",
|
|
32
|
+
"XAE", "XAB", "XAI", "XAY", "XAP", "XAV", "XAF", "XAK", "XAZ", "XAU", "XAR", "SXO", "SXR", "SXT", "SXB", "SXI", "SOX",
|
|
33
|
+
"6E", "6J", "6B", "6A", "6C", "6S", "SR3", "LE", "GF", "HE", "LBR", "DC", "CSC", "GD", "BTC", "ETH", "SOL", "XRP"]],
|
|
34
|
+
["CBT", ["YM", "MYM", "RX", "ZT", "ZF", "ZN", "ZB", "UB", "ZQ", "ZC", "ZS", "ZW", "ZM", "ZL", "KE", "ZO", "ZR"]],
|
|
35
|
+
["NYM", ["CL", "QM", "MCL", "BZ", "NG", "QG", "MNG", "HH", "RB", "HO", "PL", "PA", "B0", "TTF"]],
|
|
34
36
|
["CMX", ["GC", "SI", "HG", "ALI", "HRC", "UX"]],
|
|
35
37
|
["NYB", ["KC", "SB", "CC", "CT", "OJ"]],
|
|
36
38
|
["CFE", ["VX"]],
|
|
@@ -128,16 +130,35 @@ function futuresGenericOrdinal(position: number): string {
|
|
|
128
130
|
return `${position}${tens >= 11 && tens <= 13 ? "th" : ["th", "st", "nd", "rd"][position % 10] ?? "th"}`;
|
|
129
131
|
}
|
|
130
132
|
|
|
133
|
+
/** Equity index, sector and industry futures, the S&P GSCI and VIX: their prices are index points. */
|
|
134
|
+
const POINT_ROOTS: ReadonlySet<string> = new Set(["ES", "NQ", "RTY", "YM", "GD", "VX", "EMD", "RS1", "NKD", "NIY", "MES", "MNQ", "M2K", "MYM", "RX",
|
|
135
|
+
"XAE", "XAB", "XAI", "XAY", "XAP", "XAV", "XAF", "XAK", "XAZ", "XAU", "XAR", "SXO", "SXR", "SXT", "SXB", "SXI", "SOX"]);
|
|
136
|
+
|
|
131
137
|
/**
|
|
132
138
|
* How a generic's price reads: index and VIX futures in points (not dollars),
|
|
133
139
|
* Treasury futures in 32nds of par; everything else in its quote currency.
|
|
134
140
|
*/
|
|
135
141
|
export function futuresGenericPriceBasis(generic: Pick<FuturesGeneric, "root">): "points" | "thirty-seconds" | null {
|
|
136
|
-
if (
|
|
142
|
+
if (POINT_ROOTS.has(generic.root)) return "points";
|
|
137
143
|
if (["ZT", "ZF", "ZN", "ZB", "UB"].includes(generic.root)) return "thirty-seconds";
|
|
138
144
|
return null;
|
|
139
145
|
}
|
|
140
146
|
|
|
147
|
+
/**
|
|
148
|
+
* How a futures listing's price reads, whichever form it takes: a generic by
|
|
149
|
+
* its root (VX1 points, TY1 32nds), and a continuous (`ES=F`, `SXO=F`) or
|
|
150
|
+
* listed contract (`SXOZ26.CME`) of an index, sector or industry root in index
|
|
151
|
+
* points, on the root's own venue or with none. The quote keeps its currency
|
|
152
|
+
* for what a tick is worth. Null for every other listing, a security that
|
|
153
|
+
* shares a root's code on another venue (XAU on Toronto, the XAR ETF) included.
|
|
154
|
+
*/
|
|
155
|
+
export function futuresListingPriceBasis(symbol: string, exchange?: string | null): "points" | "thirty-seconds" | null {
|
|
156
|
+
const generic = futuresGenericListing(symbol, exchange);
|
|
157
|
+
if (generic) return futuresGenericPriceBasis(generic);
|
|
158
|
+
const listing = futuresListingVenue(symbol, exchange);
|
|
159
|
+
return listing?.root && ROOT_VENUES[listing.root] === listing.venue && POINT_ROOTS.has(listing.root) ? "points" : null;
|
|
160
|
+
}
|
|
161
|
+
|
|
141
162
|
/** Whether a root's date rules count back from first notice or from last trade. */
|
|
142
163
|
export function futuresGenericNotice(root: string): "first notice" | "last trade" {
|
|
143
164
|
return FIRST_NOTICE_ROOTS.has(root) ? "first notice" : "last trade";
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The markdown every renderer of it reads: inline marks (the reader, buildout
|
|
3
|
+
* and Ask Gloom panes through components/markdown-text.tsx, and chat), and the
|
|
4
|
+
* line structure chat draws (headings, lists, quotes and fenced code).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface MarkdownSegment {
|
|
8
|
+
text: string;
|
|
9
|
+
bold?: boolean;
|
|
10
|
+
italic?: boolean;
|
|
11
|
+
strike?: boolean;
|
|
12
|
+
code?: boolean;
|
|
13
|
+
/** Where the segment links to, when links are read. */
|
|
14
|
+
link?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface InlineMarkdownOptions {
|
|
18
|
+
/**
|
|
19
|
+
* Reads `[label](href)` as a link: every one, or those whose href this
|
|
20
|
+
* accepts. A link not read stays the text it was written as.
|
|
21
|
+
*/
|
|
22
|
+
links?: boolean | ((href: string) => boolean);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// A backslash escapes the punctuation markdown would read ("\\*" is a star).
|
|
26
|
+
// A bracket after a bang is a picture, which stays as written. A bare address
|
|
27
|
+
// is read whole, so the underscores and stars in it are not marks. A mark
|
|
28
|
+
// opens before a character and closes after one, so "2 * 3 * 4" is
|
|
29
|
+
// arithmetic, and an underscore inside a word (snake_case, a_b_c) is not one.
|
|
30
|
+
// An italic span may hold a bold one ("*covers **96%** of value*").
|
|
31
|
+
const INLINE_RE = new RegExp([
|
|
32
|
+
String.raw`\\(?<escaped>[\\` + "`" + String.raw`*_~#>[\]()-])`,
|
|
33
|
+
String.raw`(?<!!)\[(?<linkLabel>[^\]]+)\]\((?<linkHref>(?:[^()\s]|\([^()\s]*\))+)\)`,
|
|
34
|
+
String.raw`(?<url>\b(?:https?:\/\/|www\.)[^\s<>"']+)`,
|
|
35
|
+
"(?<!`)(?<ticks>`{1,3})(?!`)(?<code>[^`]|[^`].*?[^`])\\k<ticks>(?!`)",
|
|
36
|
+
String.raw`\*\*(?![\s*])(?<bold>[^*]+?)(?<!\s)\*\*`,
|
|
37
|
+
String.raw`(?<!\*)\*(?![\s*])(?<italic>(?:[^*]|\*\*[^*]+\*\*)+?)(?<!\s)\*(?!\*)`,
|
|
38
|
+
String.raw`(?<![A-Za-z0-9_])_(?![\s_])(?<underscored>[^_]+?)(?<!\s)_(?![A-Za-z0-9_])`,
|
|
39
|
+
String.raw`~~(?![\s~])(?<strike>[^~]+?)(?<!\s)~~`,
|
|
40
|
+
].join("|"), "gi");
|
|
41
|
+
|
|
42
|
+
function readsLink(options: InlineMarkdownOptions, href: string): boolean {
|
|
43
|
+
return typeof options.links === "function" ? options.links(href) : options.links === true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Code keeps its text; one space inside each tick, as in "`` `a` ``", is padding. */
|
|
47
|
+
function codeText(code: string): string {
|
|
48
|
+
return code.length > 2 && code.startsWith(" ") && code.endsWith(" ") && code.trim() ? code.slice(1, -1) : code;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function parseInlineMarkdown(text: string, options: InlineMarkdownOptions = {}): MarkdownSegment[] {
|
|
52
|
+
const segments: MarkdownSegment[] = [];
|
|
53
|
+
let cursor = 0;
|
|
54
|
+
const marked = (inner: string, mark: Partial<MarkdownSegment>) => {
|
|
55
|
+
for (const segment of parseInlineMarkdown(inner, options)) segments.push({ ...segment, ...mark });
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const pattern = new RegExp(INLINE_RE.source, INLINE_RE.flags);
|
|
59
|
+
let match: RegExpExecArray | null;
|
|
60
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
61
|
+
const groups = match.groups ?? {};
|
|
62
|
+
if (groups.linkHref != null && !readsLink(options, groups.linkHref)) {
|
|
63
|
+
// Not read as a link: the bracket is text, and what is inside it still styles.
|
|
64
|
+
pattern.lastIndex = match.index + 1;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (match.index > cursor) {
|
|
68
|
+
segments.push({ text: text.slice(cursor, match.index) });
|
|
69
|
+
}
|
|
70
|
+
if (groups.escaped != null) {
|
|
71
|
+
segments.push({ text: groups.escaped });
|
|
72
|
+
} else if (groups.linkHref != null) {
|
|
73
|
+
for (const inner of parseInlineMarkdown(groups.linkLabel ?? "")) segments.push({ ...inner, link: groups.linkHref });
|
|
74
|
+
} else if (groups.url != null) {
|
|
75
|
+
segments.push({ text: groups.url });
|
|
76
|
+
} else if (groups.code != null) {
|
|
77
|
+
segments.push({ text: codeText(groups.code), code: true });
|
|
78
|
+
} else if (groups.bold != null) {
|
|
79
|
+
marked(groups.bold, { bold: true });
|
|
80
|
+
} else if (groups.italic != null) {
|
|
81
|
+
marked(groups.italic, { italic: true });
|
|
82
|
+
} else if (groups.underscored != null) {
|
|
83
|
+
marked(groups.underscored, { italic: true });
|
|
84
|
+
} else if (groups.strike != null) {
|
|
85
|
+
marked(groups.strike, { strike: true });
|
|
86
|
+
}
|
|
87
|
+
cursor = match.index + match[0].length;
|
|
88
|
+
}
|
|
89
|
+
if (cursor < text.length) {
|
|
90
|
+
segments.push({ text: text.slice(cursor) });
|
|
91
|
+
}
|
|
92
|
+
// Plain runs split by an address or an escape read as one.
|
|
93
|
+
return segments.reduce<MarkdownSegment[]>((joined, segment) => {
|
|
94
|
+
const previous = joined[joined.length - 1];
|
|
95
|
+
if (previous && isPlain(previous) && isPlain(segment)) previous.text += segment.text;
|
|
96
|
+
else joined.push({ ...segment });
|
|
97
|
+
return joined;
|
|
98
|
+
}, []);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isPlain(segment: MarkdownSegment): boolean {
|
|
102
|
+
return !segment.bold && !segment.italic && !segment.strike && !segment.code && segment.link === undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const MARKDOWN_HEADING_RE = /^(#{1,3})\s+(.+)$/;
|
|
106
|
+
|
|
107
|
+
const LIST_ITEM_RE = /^(\s*)([-*]|\d+\.)\s(.*)$/;
|
|
108
|
+
|
|
109
|
+
/** A list item's indent, marker ("-", "*" or "1.") and text, or null for any other line. */
|
|
110
|
+
export function markdownListItem(line: string): { indent: number; marker: string; text: string } | null {
|
|
111
|
+
const match = line.match(LIST_ITEM_RE);
|
|
112
|
+
return match ? { indent: match[1]!.length, marker: match[2]!, text: match[3]! } : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export type MarkdownLine =
|
|
116
|
+
| { kind: "text"; text: string }
|
|
117
|
+
| { kind: "blank" }
|
|
118
|
+
| { kind: "heading"; text: string }
|
|
119
|
+
| { kind: "list"; indent: number; marker: string; text: string }
|
|
120
|
+
| { kind: "quote"; text: string }
|
|
121
|
+
/** A line inside a fenced code block, as written. */
|
|
122
|
+
| { kind: "code"; text: string };
|
|
123
|
+
|
|
124
|
+
const FENCE_OPEN_RE = /^\s{0,3}```[^`]*$/;
|
|
125
|
+
const FENCE_CLOSE_RE = /^\s{0,3}```\s*$/;
|
|
126
|
+
const QUOTE_RE = /^>(?: (.*))?$/;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A message's lines and what each is. The fence lines of a code block are
|
|
130
|
+
* dropped; a fence that never closes stays a line of text, so it cannot turn
|
|
131
|
+
* the rest of the message into code.
|
|
132
|
+
*/
|
|
133
|
+
export function parseMarkdownLines(text: string): MarkdownLine[] {
|
|
134
|
+
const lines = text.split("\n");
|
|
135
|
+
const parsed: MarkdownLine[] = [];
|
|
136
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
137
|
+
const line = lines[index]!;
|
|
138
|
+
if (FENCE_OPEN_RE.test(line)) {
|
|
139
|
+
const close = lines.findIndex((candidate, next) => next > index && FENCE_CLOSE_RE.test(candidate));
|
|
140
|
+
if (close > index) {
|
|
141
|
+
for (const code of lines.slice(index + 1, close)) parsed.push({ kind: "code", text: code });
|
|
142
|
+
index = close;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (line.trim() === "") {
|
|
147
|
+
parsed.push({ kind: "blank" });
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const heading = line.match(MARKDOWN_HEADING_RE);
|
|
151
|
+
if (heading) {
|
|
152
|
+
parsed.push({ kind: "heading", text: heading[2]! });
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const quote = line.match(QUOTE_RE);
|
|
156
|
+
if (quote) {
|
|
157
|
+
parsed.push({ kind: "quote", text: quote[1] ?? "" });
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const item = markdownListItem(line);
|
|
161
|
+
parsed.push(item ? { kind: "list", ...item } : { kind: "text", text: line });
|
|
162
|
+
}
|
|
163
|
+
return parsed;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The text without its marks, for wherever it shows as one plain line (a
|
|
168
|
+
* reply's quote, a notification). A link reads as its label.
|
|
169
|
+
*/
|
|
170
|
+
export function markdownPlainText(text: string, options: InlineMarkdownOptions = {}): string {
|
|
171
|
+
const plain = (line: string) => parseInlineMarkdown(line, options).map((segment) => segment.text).join("");
|
|
172
|
+
return parseMarkdownLines(text).map((line) => {
|
|
173
|
+
switch (line.kind) {
|
|
174
|
+
case "blank": return "";
|
|
175
|
+
case "code": return line.text;
|
|
176
|
+
case "list": return `${" ".repeat(line.indent)}${line.marker} ${plain(line.text)}`;
|
|
177
|
+
default: return plain(line.text);
|
|
178
|
+
}
|
|
179
|
+
}).join("\n");
|
|
180
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Fundamentals, Quote } from "../types/financials";
|
|
2
|
+
import { formatCompact, formatNumber } from "./format";
|
|
2
3
|
|
|
3
4
|
export interface MarketCapitalization {
|
|
4
5
|
value: number;
|
|
@@ -64,3 +65,24 @@ export function convertMarketCapitalization(
|
|
|
64
65
|
export function listedMarketCapitalization(value: number | null): number | null {
|
|
65
66
|
return value != null && Number.isFinite(value) && value > 0 ? value : null;
|
|
66
67
|
}
|
|
68
|
+
|
|
69
|
+
/** How far a market cap may sit from the share count times the price and still rest on that count. */
|
|
70
|
+
const SHARE_COUNT_TOLERANCE = 0.02;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Where a market cap does not rest on the share count shown beside it (one class's count beside an all-class cap):
|
|
74
|
+
* "Market cap 137.20B USD is the provider's and does not rest on the 799.93M shares shown (x 110.11 USD = 88.08B)."
|
|
75
|
+
* Undefined when they tie, or without a count and a price in the cap's currency to test.
|
|
76
|
+
*/
|
|
77
|
+
export function marketCapShareBasisNote(
|
|
78
|
+
capitalization: Pick<MarketCapitalization, "value" | "currency"> | null,
|
|
79
|
+
shares: number | undefined,
|
|
80
|
+
price: number | null | undefined,
|
|
81
|
+
priceCurrency: string | undefined,
|
|
82
|
+
): string | undefined {
|
|
83
|
+
if (!capitalization || !(capitalization.value > 0) || capitalization.currency !== priceCurrency) return undefined;
|
|
84
|
+
if (shares == null || !Number.isFinite(shares) || !(shares > 0) || price == null || !Number.isFinite(price) || !(price > 0)) return undefined;
|
|
85
|
+
const product = shares * price;
|
|
86
|
+
if (Math.abs(capitalization.value / product - 1) <= SHARE_COUNT_TOLERANCE) return undefined;
|
|
87
|
+
return `Market cap ${formatCompact(capitalization.value)} ${capitalization.currency} is the provider's and does not rest on the ${formatCompact(shares)} shares shown (x ${formatNumber(price, price < 1 ? 4 : 2)} ${priceCurrency} = ${formatCompact(product)}).`;
|
|
88
|
+
}
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"hosts": [
|
|
3
|
+
"api.crypto.com",
|
|
3
4
|
"api.elections.kalshi.com",
|
|
4
5
|
"api.fda.gov",
|
|
5
6
|
"api.fiscaldata.treasury.gov",
|
|
7
|
+
"api.gemini.com",
|
|
8
|
+
"api.limitless.exchange",
|
|
6
9
|
"api.publisher.jodidata.org",
|
|
7
10
|
"api.votehub.com",
|
|
8
11
|
"apps.fas.usda.gov",
|
|
9
12
|
"clinicaltrials.gov",
|
|
10
13
|
"clob.polymarket.com",
|
|
11
14
|
"data-api.polymarket.com",
|
|
15
|
+
"data.pascal.trade",
|
|
12
16
|
"efts.sec.gov",
|
|
13
17
|
"gamma-api.polymarket.com",
|
|
18
|
+
"gateway.polymarket.us",
|
|
14
19
|
"markets.newyorkfed.org",
|
|
15
20
|
"nasdaqtrader.com",
|
|
16
21
|
"production.dataviz.cnn.io",
|
package/src/utils/utc-time.ts
CHANGED
|
@@ -83,13 +83,13 @@ export function setDisplayTimeZone(zone: string | null | undefined): void {
|
|
|
83
83
|
/**
|
|
84
84
|
* ` (Sun 11 Oct 01:56 Asia/Tokyo)`: an instant on the reader's clock, to follow
|
|
85
85
|
* its UTC time, with the weekday and date because the two can fall on
|
|
86
|
-
* different days
|
|
86
|
+
* different days; to the second with `seconds`. Empty when no zone is set.
|
|
87
87
|
*/
|
|
88
|
-
export function localTimeSuffix(time: number): string {
|
|
88
|
+
export function localTimeSuffix(time: number, seconds = false): string {
|
|
89
89
|
if (!displayZone || !Number.isFinite(time)) return "";
|
|
90
90
|
const local = zonedDateTimeParts(time, displayZone);
|
|
91
91
|
const weekday = WEEKDAYS[new Date(Date.UTC(local.year, local.month - 1, local.day)).getUTCDay()];
|
|
92
|
-
const clock =
|
|
92
|
+
const clock = [local.hour, local.minute, ...(seconds ? [local.second] : [])].map((part) => String(part).padStart(2, "0")).join(":");
|
|
93
93
|
return ` (${weekday} ${local.day} ${MONTHS[local.month - 1]} ${clock} ${displayZone})`;
|
|
94
94
|
}
|
|
95
95
|
|
|
@@ -106,6 +106,17 @@ export function formatUtcTime(value: ReportTime | Date | number | string): strin
|
|
|
106
106
|
return `${iso.slice(0, 16).replace("T", " ")} UTC${localTimeSuffix(parsed.time)}`;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* `2026-10-09 00:08:00 UTC`: `formatUtcTime` to the second, for a column whose
|
|
111
|
+
* times are to the second, so a time that fell on a whole minute reads like
|
|
112
|
+
* the rest. Text that is not a time comes back as it is.
|
|
113
|
+
*/
|
|
114
|
+
export function formatUtcSeconds(value: unknown): string {
|
|
115
|
+
const parsed = parseReportTime(value);
|
|
116
|
+
if (!parsed || parsed.dateOnly) return typeof value === "string" ? value : "";
|
|
117
|
+
return `${new Date(parsed.time).toISOString().slice(0, 19).replace("T", " ")} UTC${localTimeSuffix(parsed.time, true)}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
109
120
|
/**
|
|
110
121
|
* `Fri 9 Oct 23:59` in UTC, or `Fri 9 Oct` for a date, for a table cell whose
|
|
111
122
|
* header names the zone: a time without its zone, so each row stays short. The
|