gloomberb 0.19.0 → 0.19.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/package.json +2 -1
- package/patches/electrobun@1.18.1.patch +23 -1
- package/src/api-client/app-attention.ts +3 -0
- package/src/api-client/beneficial-owners.ts +3 -0
- package/src/api-client/company-kpis.ts +3 -0
- package/src/api-client/data.ts +2 -1
- package/src/api-client/errors.ts +38 -0
- package/src/api-client/hiring.ts +3 -0
- package/src/api-client/index.ts +4 -0
- package/src/api-client/ipo.ts +24 -1
- package/src/api-client/mna.ts +3 -0
- package/src/api-client/paths.ts +9 -1
- package/src/api-client/roadmap.ts +85 -0
- package/src/api-client/types.ts +12 -0
- package/src/app/runtime/startup.ts +3 -1
- package/src/architecture/core-builtin-allowlist.json +2 -2
- package/src/capabilities/chart-series.ts +2 -1
- package/src/cli/commands/market.ts +44 -16
- package/src/cli/commands/news-search.ts +22 -0
- package/src/cli/commands/overview.ts +84 -19
- package/src/cli/commands/search.ts +13 -13
- package/src/cli/commands/ticker.ts +32 -2
- package/src/cli/context.ts +1 -0
- package/src/cli/errors.ts +17 -2
- package/src/cli/glossary.ts +8 -1
- package/src/cli/history-rows.ts +35 -7
- package/src/cli/index-roots.ts +0 -7
- package/src/cli/known-gaps.ts +11 -0
- package/src/cli/listing-arg.ts +74 -11
- package/src/cli/pane-functions/capabilities.ts +1 -0
- package/src/cli/pane-functions/discovery.ts +21 -1
- package/src/cli/pane-functions/freshness.ts +7 -1
- package/src/cli/pane-functions/function-help.ts +7 -0
- package/src/cli/pane-functions/index.ts +6 -3
- package/src/cli/quote-answer.ts +30 -7
- package/src/cli/saved-copy-persistence.ts +88 -0
- package/src/components/command-bar/list/model.ts +5 -0
- package/src/components/command-bar/routes/root/runtime.ts +29 -8
- package/src/components/command-bar/selection-runtime.ts +64 -32
- package/src/components/command-bar/surface/index.tsx +2 -0
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/header.tsx +2 -2
- package/src/components/layout/pane/capture-stamp.ts +2 -2
- package/src/components/layout/pane/footer/index.tsx +23 -6
- package/src/components/layout/pane/footer/model.ts +116 -16
- package/src/components/layout/pane/header.tsx +2 -2
- package/src/components/layout/pane/status-footer.ts +37 -3
- package/src/components/layout/status-widget-space.ts +9 -0
- package/src/components/onboarding/onboarding-frame.tsx +2 -1
- package/src/components/onboarding/onboarding-wizard.tsx +6 -3
- package/src/components/ticker/list-table-view.tsx +4 -20
- package/src/components/ui/display.tsx +29 -3
- package/src/components/ui/icon.tsx +12 -3
- package/src/core/app-services.ts +11 -2
- package/src/data/app-persistence.ts +9 -7
- package/src/data/plugin-cache.ts +20 -3
- package/src/data/plugin-state-store.ts +81 -24
- package/src/data/resource-store.ts +130 -58
- package/src/data/saved-copies.ts +8 -0
- package/src/data/session-store.ts +31 -8
- package/src/data/sqlite/access.ts +313 -0
- package/src/data/sqlite/checkpointer.ts +198 -0
- package/src/data/sqlite/database.ts +47 -4
- package/src/data/sqlite/retry.ts +50 -9
- package/src/data/ticker-store.ts +57 -23
- package/src/docs/snapshot.json +32 -5
- package/src/i18n/es.ts +2 -4
- package/src/i18n/ja.ts +2 -4
- package/src/i18n/ko.ts +2 -4
- package/src/i18n/zh-cn.ts +2 -4
- package/src/i18n/zh-tw.ts +2 -4
- package/src/market-data/depositary-pair.ts +25 -1
- package/src/market-data/quotes/metadata.ts +7 -0
- package/src/market-data/realized-volatility.ts +4 -2
- package/src/news/aggregator.ts +7 -1
- package/src/news/types.ts +6 -0
- package/src/plugins/auto-update-node.ts +10 -6
- package/src/plugins/builtin/alerts/calendar-events.ts +10 -4
- package/src/plugins/builtin/alerts/cli.ts +34 -11
- package/src/plugins/builtin/alerts/event-rows.ts +24 -3
- package/src/plugins/builtin/alerts/events-pane.tsx +15 -6
- package/src/plugins/builtin/alerts/headless.ts +10 -6
- package/src/plugins/builtin/cds/client.ts +142 -30
- package/src/plugins/builtin/cds/headless.ts +35 -8
- package/src/plugins/builtin/cds/model.ts +145 -18
- package/src/plugins/builtin/cds/pane.tsx +16 -5
- package/src/plugins/builtin/central-bank-rates/series.ts +2 -3
- package/src/plugins/builtin/chart-composer/cli-options.ts +16 -0
- package/src/plugins/builtin/chart-composer/follow-binding.ts +14 -2
- package/src/plugins/builtin/chart-composer/headless-schema.ts +5 -0
- package/src/plugins/builtin/chart-composer/index.tsx +4 -5
- package/src/plugins/builtin/chart-composer/pane.tsx +5 -1
- package/src/plugins/builtin/chart-composer/session-range.ts +5 -2
- package/src/plugins/builtin/chat/controller/index.ts +5 -1
- package/src/plugins/builtin/chat/controller/notifications.ts +33 -3
- package/src/plugins/builtin/chat/controller/state.ts +25 -2
- package/src/plugins/builtin/chat/controller/unread-people.ts +89 -23
- package/src/plugins/builtin/chat/controller/utils.ts +7 -2
- package/src/plugins/builtin/chat/status-preview.ts +79 -0
- package/src/plugins/builtin/chat/status-widget.tsx +147 -10
- package/src/plugins/builtin/chat/unread-badge.ts +51 -10
- package/src/plugins/builtin/clinical-trials/client.ts +49 -4
- package/src/plugins/builtin/clinical-trials/model.ts +86 -22
- package/src/plugins/builtin/clinical-trials/pane.tsx +46 -27
- package/src/plugins/builtin/clinical-trials/sponsor-listing.ts +90 -0
- package/src/plugins/builtin/clinical-trials/sponsor-tickers.ts +87 -9
- package/src/plugins/builtin/clinical-trials/trial-pages.ts +50 -0
- package/src/plugins/builtin/commodity-balances/client.ts +4 -1
- package/src/plugins/builtin/company-attention/headless.ts +2 -1
- package/src/plugins/builtin/company-attention/pane.tsx +3 -0
- package/src/plugins/builtin/company-kpis/headless.ts +8 -3
- package/src/plugins/builtin/company-kpis/model.ts +73 -5
- package/src/plugins/builtin/company-kpis/pane.tsx +15 -7
- package/src/plugins/builtin/composite-plugins.ts +2 -1
- package/src/plugins/builtin/cot/client.ts +28 -2
- package/src/plugins/builtin/cot/headless.ts +4 -1
- package/src/plugins/builtin/cot/index.tsx +4 -2
- package/src/plugins/builtin/cot/model.ts +58 -7
- package/src/plugins/builtin/cot/pane.tsx +6 -2
- package/src/plugins/builtin/cpi/client.ts +6 -5
- package/src/plugins/builtin/credit-boards/series.ts +3 -4
- package/src/plugins/builtin/credit-conditions/client.ts +22 -14
- package/src/plugins/builtin/credit-documents/headless.ts +3 -3
- package/src/plugins/builtin/crypto-board/client.ts +5 -5
- package/src/plugins/builtin/crypto-board/series.ts +4 -4
- package/src/plugins/builtin/debt-maturities/series.ts +3 -3
- package/src/plugins/builtin/doe/client.ts +6 -5
- package/src/plugins/builtin/doe/headless.ts +3 -2
- package/src/plugins/builtin/doe/pane.tsx +13 -3
- package/src/plugins/builtin/doe/prices.ts +2 -3
- package/src/plugins/builtin/doe/series.ts +4 -4
- package/src/plugins/builtin/earnings-calls/calendar.tsx +10 -2
- package/src/plugins/builtin/earnings-calls/data.ts +37 -17
- package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
- package/src/plugins/builtin/earnings-calls/pane.tsx +34 -14
- package/src/plugins/builtin/econ/calendar-model.ts +48 -1
- package/src/plugins/builtin/econ/calendar-source.ts +4 -2
- package/src/plugins/builtin/econ/detail-view.tsx +18 -4
- package/src/plugins/builtin/econ/figures.ts +48 -0
- package/src/plugins/builtin/econ/index.tsx +14 -5
- package/src/plugins/builtin/econ/print-watch.ts +3 -1
- package/src/plugins/builtin/econ-statistics/client.ts +11 -0
- package/src/plugins/builtin/econ-statistics/headless.ts +10 -2
- package/src/plugins/builtin/econ-statistics/pane.tsx +8 -4
- package/src/plugins/builtin/econ-statistics/stats.ts +3 -0
- package/src/plugins/builtin/executives/data.ts +6 -1
- package/src/plugins/builtin/executives/pane.tsx +9 -21
- package/src/plugins/builtin/fear-greed/cache.ts +4 -0
- package/src/plugins/builtin/fear-greed/series.ts +6 -4
- package/src/plugins/builtin/gpu/client.ts +4 -1
- package/src/plugins/builtin/gpu/series.ts +2 -3
- package/src/plugins/builtin/help/index.tsx +61 -59
- package/src/plugins/builtin/help/shortcut-table.tsx +5 -6
- package/src/plugins/builtin/help/table-cells.tsx +13 -2
- package/src/plugins/builtin/holders/beneficial-client.ts +5 -1
- package/src/plugins/builtin/holders/beneficial-view.tsx +4 -2
- package/src/plugins/builtin/holders/headless.ts +4 -2
- package/src/plugins/builtin/holders/pane.tsx +3 -0
- package/src/plugins/builtin/holders/report-rows.ts +4 -1
- package/src/plugins/builtin/insider/headless.ts +2 -2
- package/src/plugins/builtin/ipo-calendar/client.ts +18 -2
- package/src/plugins/builtin/ipo-calendar/headless.ts +75 -11
- package/src/plugins/builtin/ipo-calendar/listing-ipo.ts +160 -0
- package/src/plugins/builtin/ipo-calendar/model.ts +98 -2
- package/src/plugins/builtin/jodi/client.ts +4 -1
- package/src/plugins/builtin/macro-day/headless.ts +12 -5
- package/src/plugins/builtin/macro-day/index.tsx +1 -1
- package/src/plugins/builtin/macro-day/model.ts +39 -5
- package/src/plugins/builtin/macro-day/pane.tsx +27 -19
- package/src/plugins/builtin/market-movers/client.ts +3 -0
- package/src/plugins/builtin/market-movers/headless.ts +3 -1
- package/src/plugins/builtin/market-movers/index.tsx +10 -5
- package/src/plugins/builtin/market-movers/screener.ts +27 -6
- package/src/plugins/builtin/market-movers/session-body.tsx +2 -1
- package/src/plugins/builtin/mna/headless.ts +2 -0
- package/src/plugins/builtin/mna/pane.tsx +3 -1
- package/src/plugins/builtin/money-markets/series.ts +3 -5
- package/src/plugins/builtin/news/headless.ts +3 -1
- package/src/plugins/builtin/news/index.tsx +3 -0
- package/src/plugins/builtin/portfolio-list/pane/index.tsx +2 -6
- package/src/plugins/builtin/portfolio-list/quick-add/index.tsx +0 -14
- package/src/plugins/builtin/portfolio-list/quick-add/resolution.ts +2 -1
- package/src/plugins/builtin/portfolio-list/table.tsx +4 -6
- package/src/plugins/builtin/power/gas-client.ts +2 -1
- package/src/plugins/builtin/power/prices-client.ts +3 -2
- package/src/plugins/builtin/realized-vol/client.ts +13 -5
- package/src/plugins/builtin/realized-vol/evidence.ts +29 -4
- package/src/plugins/builtin/realized-vol/headless.ts +8 -4
- package/src/plugins/builtin/realized-vol/model.ts +37 -6
- package/src/plugins/builtin/realized-vol/pane.tsx +9 -7
- package/src/plugins/builtin/research/event-model.ts +30 -4
- package/src/plugins/builtin/research/index.tsx +7 -3
- package/src/plugins/builtin/research/relative-valuation-headless.ts +8 -25
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +52 -15
- package/src/plugins/builtin/research/relative-valuation-peers.ts +217 -20
- package/src/plugins/builtin/research/relative-valuation-settings.ts +44 -0
- package/src/plugins/builtin/risk-factors/data.ts +12 -6
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- package/src/plugins/builtin/risk-factors/index.tsx +3 -1
- package/src/plugins/builtin/risk-factors/pane.tsx +5 -8
- package/src/plugins/builtin/roadmap/index.tsx +28 -0
- package/src/plugins/builtin/roadmap/model.ts +67 -0
- package/src/plugins/builtin/roadmap/pane.tsx +244 -0
- package/src/plugins/builtin/scanner/live-headless.ts +21 -2
- package/src/plugins/builtin/scanner/lock-note.ts +26 -3
- package/src/plugins/builtin/sec/headless.ts +2 -1
- package/src/plugins/builtin/sec/index.tsx +3 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +20 -7
- package/src/plugins/builtin/shared/fred-points.ts +6 -1
- package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
- package/src/plugins/builtin/shared/pro-lock-note.ts +23 -8
- package/src/plugins/builtin/shared/report-freshness.ts +8 -0
- package/src/plugins/builtin/shared/research-cells.tsx +1 -1
- package/src/plugins/builtin/shared/series-cache.ts +4 -4
- package/src/plugins/builtin/shared/series-warning.ts +13 -4
- package/src/plugins/builtin/short-volume/series.ts +2 -3
- package/src/plugins/builtin/social-mentions/client.ts +15 -10
- package/src/plugins/builtin/social-mentions/series.ts +2 -3
- package/src/plugins/builtin/thirteenf/api.ts +15 -9
- package/src/plugins/builtin/thirteenf/data.ts +39 -11
- package/src/plugins/builtin/thirteenf/headless.ts +31 -4
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +5 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +21 -5
- package/src/plugins/builtin/thirteenf/types.ts +2 -0
- package/src/plugins/builtin/ticker-detail/data-panes/historical-prices.tsx +5 -2
- package/src/plugins/builtin/ticker-detail/financials/model.ts +35 -2
- package/src/plugins/builtin/ticker-detail/financials/ratios.ts +53 -18
- package/src/plugins/builtin/ticker-detail/headless.ts +59 -8
- package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +2 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +11 -6
- package/src/plugins/builtin/treasury-auctions/cache.ts +20 -8
- package/src/plugins/builtin/treasury-auctions/client.ts +3 -3
- package/src/plugins/builtin/treasury-auctions/headless.ts +24 -13
- package/src/plugins/builtin/treasury-auctions/pane.tsx +18 -6
- package/src/plugins/builtin/volatility/client.ts +43 -16
- package/src/plugins/builtin/volatility/headless.ts +5 -2
- package/src/plugins/builtin/volatility/model.ts +17 -6
- package/src/plugins/builtin/volatility/pane.tsx +11 -4
- package/src/plugins/builtin/volatility/series.ts +2 -1
- package/src/plugins/builtin/yield-curve/client.ts +59 -1
- package/src/plugins/builtin/yield-curve/headless.ts +9 -6
- package/src/plugins/builtin/yield-curve/index.tsx +34 -10
- package/src/plugins/builtin/yield-curve/swaps.tsx +9 -5
- package/src/plugins/catalog-browser.ts +2 -1
- package/src/plugins/host-link.ts +1 -1
- package/src/plugins/installer.ts +50 -39
- package/src/portfolio/column-values.ts +11 -7
- package/src/renderers/dom/desktop/icons.tsx +10 -2
- package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
- package/src/renderers/electrobun/bun/desktop/detached-windows.ts +23 -1
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
- package/src/renderers/electrobun/bun/desktop/initialization.ts +9 -1
- package/src/renderers/electrobun/bun/desktop/installer-folder.ts +731 -0
- package/src/renderers/electrobun/bun/desktop/relaunch.ts +102 -10
- package/src/renderers/electrobun/bun/desktop/windows-script.ts +92 -0
- package/src/renderers/electrobun/bun/index.ts +26 -2
- package/src/renderers/electrobun/bun/window/attention.ts +54 -0
- package/src/renderers/electrobun/bun/window/create.ts +45 -4
- package/src/renderers/electrobun/bun/window/events.ts +20 -0
- package/src/renderers/electrobun/bun/window/windows-custom-chrome.ts +9 -2
- package/src/renderers/electrobun/bun/window/windows-native.ts +78 -0
- package/src/renderers/electrobun/bun/window/windows-quit.ts +57 -0
- package/src/renderers/electrobun/launcher/preload.ts +13 -0
- package/src/renderers/electrobun/launcher/windows-emulation.ts +50 -0
- package/src/renderers/electrobun/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/news-source.ts +2 -0
- package/src/renderers/electrobun/view/ui-host.tsx +16 -1
- package/src/series/resolve.ts +2 -1
- package/src/series/types.ts +5 -0
- package/src/sources/gloomberb-cloud/fred-series.ts +8 -4
- package/src/sources/gloomberb-cloud/index.ts +42 -8
- package/src/telemetry/stall-reports-node.ts +13 -2
- package/src/telemetry/stall-reports.ts +189 -13
- package/src/test-support/outage.ts +8 -0
- package/src/tickers/company-name-listing.ts +110 -40
- package/src/tickers/company-name.ts +71 -2
- package/src/tickers/index-roots.ts +7 -0
- package/src/tickers/listing-fate.ts +2 -2
- package/src/tickers/search/home-listing.ts +93 -8
- package/src/tickers/search/index.ts +7 -3
- package/src/tickers/search/ranking.ts +5 -1
- package/src/tickers/search/upsert.ts +13 -0
- package/src/tickers/ticker-changes.ts +8 -4
- package/src/time-series/market.ts +5 -3
- package/src/time-series/price-comparison.ts +20 -2
- package/src/time-series/resolve.ts +20 -1
- package/src/time-series/types.ts +7 -0
- package/src/types/company-from.ts +40 -0
- package/src/types/data-provider.ts +7 -0
- package/src/types/financials.ts +45 -3
- package/src/types/headless.ts +6 -0
- package/src/ui/host.tsx +6 -1
- package/src/utils/crypto-pair.ts +121 -7
- package/src/utils/declared-dividends.ts +1 -1
- package/src/utils/financial-statements.ts +73 -16
- package/src/utils/fundamentals.ts +53 -10
- package/src/utils/lease-liabilities.ts +54 -0
- package/src/utils/main-thread-stall.ts +47 -2
- package/src/utils/perf-marks.ts +23 -11
- package/src/utils/price-history-integrity.ts +47 -1
- package/src/version.ts +2 -2
|
@@ -1,31 +1,88 @@
|
|
|
1
|
-
import type { FinancialStatement, StatementFill, StatementFillField } from "../types/financials";
|
|
1
|
+
import type { FinancialStatement, StatementFill, StatementFillField, TickerFinancials } from "../types/financials";
|
|
2
|
+
import { settleWithin } from "./async-deadline";
|
|
3
|
+
import { logFailure } from "./log-failure";
|
|
2
4
|
import { copyIncomeField, incomeFieldOwner, INCOME_STATEMENT_FIELDS, isIncomeStatementField } from "./income-statement";
|
|
3
5
|
import { mergeStatementGaps } from "./statement-gaps";
|
|
4
6
|
|
|
5
7
|
export const FINANCIAL_VINTAGE_NOTICE = "Latest available statements may include restatements. Historical as-of values are not reconstructed.";
|
|
6
8
|
|
|
9
|
+
/** The oldest dated period of a report's columns (a TTM column has no date), or null when none has one. */
|
|
10
|
+
function oldestPeriod(dates: readonly string[]): string | null {
|
|
11
|
+
return dates.filter((date) => /^\d{4}-\d{2}-\d{2}/.test(date)).map((date) => date.slice(0, 10)).sort()[0] ?? null;
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
/**
|
|
8
15
|
* The closing lines of a statements report that say how far back it goes, from
|
|
9
|
-
* the oldest period it served: the report holds no older ones
|
|
10
|
-
*
|
|
11
|
-
*
|
|
16
|
+
* the oldest period it served: the report holds no older ones. `older` says
|
|
17
|
+
* whether `GF`, which reads one metric from the listing's extended history
|
|
18
|
+
* (`hasOlderStatements`), goes further back: it is pointed to only then, and
|
|
19
|
+
* when it does not, the listing has no older ones at all (IAG on LSE: its
|
|
20
|
+
* statements start in 2022, in GF too). Null when that could not be told in
|
|
21
|
+
* time: the report then says nothing of older periods. Empty too when no
|
|
22
|
+
* period has a date, so a report with nothing in it says nothing.
|
|
12
23
|
*/
|
|
13
24
|
export function statementHistoryNotes(
|
|
14
25
|
period: "annual" | "quarterly",
|
|
15
26
|
dates: readonly string[],
|
|
16
27
|
symbol: string,
|
|
28
|
+
older: boolean | null,
|
|
17
29
|
): string[] {
|
|
18
|
-
const oldest = dates
|
|
19
|
-
if (!oldest) return [];
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
const oldest = oldestPeriod(dates);
|
|
31
|
+
if (!oldest || older === null) return [];
|
|
32
|
+
const [from, unit] = period === "annual" ? [oldest.slice(0, 4), "years"] : [oldest, "quarters"];
|
|
33
|
+
if (older === false) return [`${period} history from ${from}; older ${unit} not available for this listing`];
|
|
34
|
+
return [
|
|
35
|
+
`${period} history from ${from}; older ${unit} not served`,
|
|
36
|
+
...older ? [`earlier ${unit}, one metric at a time: gloom fn GF ${symbol} --period ${period} --periods 40`] : [],
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* How long a statements report waits on the extended history for its
|
|
42
|
+
* older-periods note, as a lock note waits on its routes' checks: a read still
|
|
43
|
+
* out then leaves the report on time, without the note. The read goes on and
|
|
44
|
+
* warms the cache for the next report.
|
|
45
|
+
*/
|
|
46
|
+
const OLDER_STATEMENTS_WAIT_MS = 3_000;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether the listing's extended statement history, which `GF` reads, has a
|
|
50
|
+
* period older than the oldest of `dates`, a report's columns: a US filer's
|
|
51
|
+
* filings reach years back; a listing abroad has nothing past what the report
|
|
52
|
+
* served. Null when the history could not be read within `waitMs`.
|
|
53
|
+
*/
|
|
54
|
+
export async function hasOlderStatements(
|
|
55
|
+
loadExtended: () => Promise<Pick<TickerFinancials, "annualStatements" | "quarterlyStatements">>,
|
|
56
|
+
period: "annual" | "quarterly",
|
|
57
|
+
dates: readonly string[],
|
|
58
|
+
waitMs = OLDER_STATEMENTS_WAIT_MS,
|
|
59
|
+
): Promise<boolean | null> {
|
|
60
|
+
const oldest = oldestPeriod(dates);
|
|
61
|
+
if (!oldest) return null;
|
|
62
|
+
const read = Promise.resolve().then(loadExtended).catch((error: unknown) => {
|
|
63
|
+
logFailure("utils.financial-statements", "Reading the extended statement history failed")(error);
|
|
64
|
+
throw error;
|
|
65
|
+
});
|
|
66
|
+
const extended = await settleWithin(read, waitMs);
|
|
67
|
+
if (!extended) return null;
|
|
68
|
+
const rows = period === "annual" ? extended.annualStatements : extended.quarterlyStatements;
|
|
69
|
+
return rows.some((row) => /^\d{4}-\d{2}-\d{2}/.test(row.date) && row.date.slice(0, 10) < oldest);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Venues whose companies report half-yearly and yearly, with no quarterly statements: Australia's (ASX, and Cboe Australia, which trades ASX companies) and New Zealand's. */
|
|
73
|
+
const HALF_YEARLY_VENUES = new Set(["ASX", "CXA", "NZX"]);
|
|
74
|
+
const DAY_MS = 86_400_000;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether a listing's company reports half-yearly rather than quarterly: it
|
|
78
|
+
* lists on a venue whose companies do (Qantas on ASX), or the periods of its
|
|
79
|
+
* "quarterly" statements are half a year apart.
|
|
80
|
+
*/
|
|
81
|
+
export function reportsHalfYearly(exchange: string | null | undefined, quarterly: readonly Pick<FinancialStatement, "date">[]): boolean {
|
|
82
|
+
if (exchange && HALF_YEARLY_VENUES.has(exchange.toUpperCase())) return true;
|
|
83
|
+
const times = quarterly.map(({ date }) => Date.parse(`${date.slice(0, 10)}T00:00:00Z`)).filter(Number.isFinite).sort((a, b) => a - b);
|
|
84
|
+
const gaps = times.slice(1).map((time, index) => (time - times[index]!) / DAY_MS);
|
|
85
|
+
return gaps.length > 0 && gaps.every((days) => days > 150 && days < 215);
|
|
29
86
|
}
|
|
30
87
|
|
|
31
88
|
/** How old a statement's latest period may be before a report says how old it is. */
|
|
@@ -106,7 +163,7 @@ function retainedFill(merged: FinancialStatement, rows: Array<FinancialStatement
|
|
|
106
163
|
|
|
107
164
|
export const SEC_EPS_BASIS_NOTICE = "SEC EPS uses corroborated split-adjusted share bases. Unverified bases are unavailable.";
|
|
108
165
|
|
|
109
|
-
const STATEMENT_METADATA_KEYS = new Set(["date", "dateSource", "providerDate", "dateEvidence", "currency", "availableAt", "fieldAvailability", "epsBasis", "fieldSources", "unavailableFields", "withdrawnObservations", "operatingResult", "operatingResultAggregation", "earningsResult", "unavailableEarnings", "filledFrom"]);
|
|
166
|
+
const STATEMENT_METADATA_KEYS = new Set(["date", "dateSource", "accountingStandard", "providerDate", "dateEvidence", "currency", "availableAt", "fieldAvailability", "epsBasis", "fieldSources", "unavailableFields", "withdrawnObservations", "operatingResult", "operatingResultAggregation", "earningsResult", "unavailableEarnings", "filledFrom"]);
|
|
110
167
|
const NEARBY_PERIOD_END_MS = 7 * 24 * 60 * 60 * 1_000;
|
|
111
168
|
|
|
112
169
|
/** An explicit field map is authoritative: omitted fields have unknown availability. */
|
|
@@ -2,6 +2,7 @@ import { formatFxRate } from "../cli/fx-pair";
|
|
|
2
2
|
import type { EnterpriseValueCheck, Fundamentals, Quote, TickerFinancials } from "../types/financials";
|
|
3
3
|
import { quotedFxPair } from "./currency-pair";
|
|
4
4
|
import { resolveCurrencyUnit } from "./currency-units";
|
|
5
|
+
import { isUsListingExchange } from "./exchanges";
|
|
5
6
|
import { formatCompact } from "./format";
|
|
6
7
|
import { formatUtcTime } from "./utc-time";
|
|
7
8
|
|
|
@@ -44,7 +45,10 @@ export function enterpriseValueCheck(
|
|
|
44
45
|
// Not meaningful needs no figure of its own: a source EV below zero may come with no cap or currency.
|
|
45
46
|
if (check.status === "not-meaningful") return withdrawn ? check : undefined;
|
|
46
47
|
if (!check.currency) return undefined;
|
|
47
|
-
|
|
48
|
+
// A withdrawn EV comes with the figure it was checked against, or with why no balance sheet checked it.
|
|
49
|
+
if (check.status === "withdrawn") {
|
|
50
|
+
return withdrawn && ((finiteNumber(check.computed) && finiteNumber(check.source)) || !!check.reason?.trim()) ? check : undefined;
|
|
51
|
+
}
|
|
48
52
|
if (check.status === "computed") {
|
|
49
53
|
return finiteNumber(check.computed) && fundamentals?.enterpriseValue === check.computed ? check : undefined;
|
|
50
54
|
}
|
|
@@ -86,8 +90,9 @@ export function enterpriseValueLabel(fundamentals: Parameters<typeof enterpriseV
|
|
|
86
90
|
/**
|
|
87
91
|
* The reconciliation line under a labelled, withdrawn or not meaningful EV: "EV 61.75B USD (provider) vs 63.75B
|
|
88
92
|
* computed from market cap + total debt - cash (balance sheet 2026-06-30).", "EV N/M: cash and short-term investments
|
|
89
|
-
* exceed market cap + total debt (balance sheet 2026-06-30)."
|
|
90
|
-
*
|
|
93
|
+
* exceed market cap + total debt (balance sheet 2026-06-30)." A balance sheet in another currency names the rate it
|
|
94
|
+
* was converted at ("balance sheet 2026-06-30, EUR at 0.8614 GBP"). A withdrawn EV no balance sheet checked gives
|
|
95
|
+
* Gloom Cloud's reason; one withdrawn without a check, that the source failed validation. Undefined when the EV matches, is unchecked or computed, or has no check. Read it from the
|
|
91
96
|
* fundamentals the rows show (atRegularSessionPrice moves the check with the EV and the market cap), so its figures
|
|
92
97
|
* are theirs.
|
|
93
98
|
*/
|
|
@@ -96,14 +101,22 @@ export function enterpriseValueReconciliationNote(fundamentals: Parameters<typeo
|
|
|
96
101
|
if (notMeaningful) return `EV N/M: ${notMeaningful.charAt(0).toLowerCase()}${notMeaningful.slice(1)}.`;
|
|
97
102
|
const check = enterpriseValueCheck(fundamentals);
|
|
98
103
|
if (!check && enterpriseValueWithdrawn(fundamentals)) return "EV unavailable: the provider's figure failed validation.";
|
|
104
|
+
const reason = check?.reason?.trim().replace(/\.$/, "");
|
|
105
|
+
if (check?.status === "withdrawn" && !finiteNumber(check.computed) && reason) {
|
|
106
|
+
return `EV unavailable: ${reason.charAt(0).toLowerCase()}${reason.slice(1)}.`;
|
|
107
|
+
}
|
|
99
108
|
if (!check || !finiteNumber(check.computed) || !finiteNumber(check.source)
|
|
100
109
|
|| (check.status !== "differs" && check.status !== "withdrawn")) return undefined;
|
|
101
110
|
const source = `${formatCompact(check.source)} ${check.currency}`;
|
|
102
111
|
const computed = formatCompact(check.computed);
|
|
103
|
-
const
|
|
112
|
+
const leases = finiteNumber(check.leases) && check.leases > 0 ? " + leases" : "";
|
|
113
|
+
const fx = check.fx && finiteNumber(check.fx.rate) ? `, ${check.fx.from} at ${check.fx.rate.toPrecision(4)} ${check.currency}` : "";
|
|
114
|
+
const sheet = check.balanceSheetDate ? ` (balance sheet ${check.balanceSheetDate}${fx})` : "";
|
|
115
|
+
const basis = `market cap + total debt${leases} - cash${sheet}`;
|
|
104
116
|
if (check.status === "differs") return `EV ${source} (provider) vs ${computed} computed from ${basis}.`;
|
|
105
117
|
const ratio = check.computed > 0 ? ` is ${(check.source / check.computed).toFixed(1)}x` : " is far from";
|
|
106
|
-
|
|
118
|
+
const twice = check.convertedTwice && check.convertedFrom ? `; it converts ${check.convertedFrom} to ${check.currency} twice` : "";
|
|
119
|
+
return `EV unavailable: the provider's ${source}${ratio} the ${computed} computed from ${basis}${twice}.`;
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
/**
|
|
@@ -119,24 +132,53 @@ export function computedEnterpriseValueNote(fundamentals: Parameters<typeof ente
|
|
|
119
132
|
}
|
|
120
133
|
|
|
121
134
|
/** What names the currency of the trailing figures: the fundamentals' own labels, else the statements' on a listing that trades in theirs. */
|
|
122
|
-
type FundamentalsCurrencies = Pick<TickerFinancials, "financialCurrency"> & {
|
|
135
|
+
type FundamentalsCurrencies = Pick<TickerFinancials, "financialCurrency" | "statementHistory"> & {
|
|
123
136
|
fundamentals?: Pick<Fundamentals, "financialCurrency" | "epsCurrency" | "marketCapCurrency"> | null;
|
|
124
|
-
quote?: Pick<Quote, "currency"> | null;
|
|
137
|
+
quote?: Pick<Quote, "currency" | "exchangeName" | "listingExchangeName" | "isDepositaryReceipt"> | null;
|
|
125
138
|
};
|
|
126
139
|
|
|
140
|
+
/** What a caller knows of the listing beyond its quote: the day it listed (its IPO's), when known. */
|
|
141
|
+
interface ListingAge {
|
|
142
|
+
listedSince?: string | null;
|
|
143
|
+
now?: Date;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** A listing that started within about a year: its statements may not be served yet. */
|
|
147
|
+
const RECENT_LISTING_DAYS = 372;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The listing's own currency, standing in for figures with no statements to tell their currency by: only for a
|
|
151
|
+
* listing of the last year or so (a recent IPO such as HAWK) or a US listing in dollars that is no receipt, and
|
|
152
|
+
* never after a statements read that failed. A minor unit (GBp) does not say whether figures are pence or pounds.
|
|
153
|
+
*/
|
|
154
|
+
function listingCurrencyFallback(financials: FundamentalsCurrencies | null | undefined, listing: ListingAge): string | undefined {
|
|
155
|
+
const listed = financials?.fundamentals?.marketCapCurrency?.trim() || financials?.quote?.currency?.trim();
|
|
156
|
+
if (!listed || resolveCurrencyUnit(listed).divisor !== 1) return undefined;
|
|
157
|
+
if (financials?.statementHistory?.status === "retryable-failure") return undefined;
|
|
158
|
+
const currency = resolveCurrencyUnit(listed).currency;
|
|
159
|
+
const since = listing.listedSince ? Date.parse(`${listing.listedSince.slice(0, 10)}T00:00:00Z`) : NaN;
|
|
160
|
+
const recent = Number.isFinite(since) && (listing.now ?? new Date()).getTime() - since <= RECENT_LISTING_DAYS * 86_400_000;
|
|
161
|
+
const quote = financials?.quote;
|
|
162
|
+
const home = currency === "USD" && quote?.isDepositaryReceipt !== true
|
|
163
|
+
&& isUsListingExchange(quote?.listingExchangeName || quote?.exchangeName);
|
|
164
|
+
return recent || home ? currency : undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
127
167
|
/**
|
|
128
168
|
* The currency of the trailing EPS, revenue, income and cash flows: the one the fundamentals declare for themselves.
|
|
129
169
|
* A block that declares none is in the statements' currency only when the listing trades in it; on a listing quoted
|
|
130
170
|
* in another (a receipt in USD beside TWD books, YPFD in ARS beside USD statements) its currency is unknown, since
|
|
131
|
-
* Gloom Cloud labels every block whose currency it can tell.
|
|
171
|
+
* Gloom Cloud labels every block whose currency it can tell. With no statements to tell by, the listing's own
|
|
172
|
+
* currency where that is safe (listingCurrencyFallback).
|
|
132
173
|
*/
|
|
133
174
|
export function fundamentalsCurrency(
|
|
134
175
|
financials: FundamentalsCurrencies | null | undefined,
|
|
176
|
+
listing: ListingAge = {},
|
|
135
177
|
): string | undefined {
|
|
136
178
|
const declared = financials?.fundamentals?.financialCurrency?.trim();
|
|
137
179
|
if (declared) return declared;
|
|
138
180
|
const statements = financials?.financialCurrency?.trim();
|
|
139
|
-
if (!statements) return
|
|
181
|
+
if (!statements) return listingCurrencyFallback(financials, listing);
|
|
140
182
|
const listed = financials?.fundamentals?.marketCapCurrency?.trim() || financials?.quote?.currency?.trim();
|
|
141
183
|
return !listed || resolveCurrencyUnit(listed).currency === resolveCurrencyUnit(statements).currency ? statements : undefined;
|
|
142
184
|
}
|
|
@@ -147,8 +189,9 @@ export function fundamentalsCurrency(
|
|
|
147
189
|
*/
|
|
148
190
|
export function fundamentalsEpsCurrency(
|
|
149
191
|
financials: FundamentalsCurrencies | null | undefined,
|
|
192
|
+
listing: ListingAge = {},
|
|
150
193
|
): string | undefined {
|
|
151
|
-
return financials?.fundamentals?.epsCurrency?.trim() || fundamentalsCurrency(financials);
|
|
194
|
+
return financials?.fundamentals?.epsCurrency?.trim() || fundamentalsCurrency(financials, listing);
|
|
152
195
|
}
|
|
153
196
|
|
|
154
197
|
const ISO_DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { FinancialStatement } from "../types/financials";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How net debt treats the lease liabilities a listing's balance sheets report (docs/financial-ratios.md#leases):
|
|
5
|
+
*
|
|
6
|
+
* - `included`: added to total debt. IFRS 16 puts every lessee lease on the balance sheet and reports EBITDA before
|
|
7
|
+
* lease costs, so leverage with the leases in is like for like (IAG's June 2026 balance sheet: 6.96bn of
|
|
8
|
+
* borrowings, 6.91bn of leases, 9.18bn of cash).
|
|
9
|
+
* - `excluded`: a company reporting under US GAAP, whatever form it files (a 10-K, or a 20-F as BABA, PDD and JD
|
|
10
|
+
* do). Its finance leases are already in total debt and its operating lease cost is in EBITDA, so its operating
|
|
11
|
+
* leases stay out.
|
|
12
|
+
*
|
|
13
|
+
* The standard is the one Gloom Cloud reads from the issuer's latest annual report to the SEC
|
|
14
|
+
* (`accountingStandard`), the signal its enterprise value check bridges by. Where no row states it, a row dated by
|
|
15
|
+
* a 10-K or 10-Q (`dateSource: "sec"`) is US GAAP, and a company that files none with the SEC is IFRS.
|
|
16
|
+
*/
|
|
17
|
+
export type LeaseBasis = "included" | "excluded";
|
|
18
|
+
|
|
19
|
+
type LeaseLines = Pick<FinancialStatement, "capitalLeaseObligations" | "dateSource" | "accountingStandard">;
|
|
20
|
+
|
|
21
|
+
const finite = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
|
|
22
|
+
|
|
23
|
+
/** The listing's lease basis, or undefined when no balance sheet on file reports lease liabilities. */
|
|
24
|
+
export function leaseBasis(statements: Iterable<LeaseLines>): LeaseBasis | undefined {
|
|
25
|
+
let leases = false;
|
|
26
|
+
let standard: FinancialStatement["accountingStandard"];
|
|
27
|
+
let secDated = false;
|
|
28
|
+
for (const statement of statements) {
|
|
29
|
+
if (finite(statement.capitalLeaseObligations) && statement.capitalLeaseObligations > 0) leases = true;
|
|
30
|
+
standard ??= statement.accountingStandard;
|
|
31
|
+
if (statement.dateSource === "sec") secDated = true;
|
|
32
|
+
}
|
|
33
|
+
if (!leases) return undefined;
|
|
34
|
+
return (standard ? standard === "us-gaap" : secDated) ? "excluded" : "included";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Amounts are whole units; some providers round a leg to the thousand. */
|
|
38
|
+
const close = (a: number, b: number) => Math.abs(a - b) <= Math.max(1_000, Math.abs(a) * 1e-6);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A balance sheet's total debt and lease liabilities. A total that already holds the lease line (long-term and
|
|
42
|
+
* current debt plus leases) is read as the borrowings beside it, so the leases count once. Undefined without both
|
|
43
|
+
* lines: a missing lease line is never zero.
|
|
44
|
+
*/
|
|
45
|
+
export function debtAndLeases(
|
|
46
|
+
statement: Pick<FinancialStatement, "totalDebt" | "capitalLeaseObligations" | "longTermDebt" | "currentDebt">,
|
|
47
|
+
): { totalDebt: number; leases: number } | undefined {
|
|
48
|
+
const total = statement.totalDebt;
|
|
49
|
+
const leases = statement.capitalLeaseObligations;
|
|
50
|
+
if (!finite(total) || !finite(leases) || leases < 0) return undefined;
|
|
51
|
+
const { longTermDebt: long, currentDebt: current } = statement;
|
|
52
|
+
const holds = leases > 0 && (finite(long) || finite(current)) && close(total, (long ?? 0) + (current ?? 0) + leases);
|
|
53
|
+
return { totalDebt: holds ? total - leases : total, leases };
|
|
54
|
+
}
|
|
@@ -9,15 +9,31 @@ const SAMPLE_INTERVAL_MS = 250;
|
|
|
9
9
|
*/
|
|
10
10
|
const STALL_THRESHOLD_MS = 400;
|
|
11
11
|
|
|
12
|
+
/** The facts a `sqlite.blocked` step was measured with (data/sqlite/retry.ts). */
|
|
13
|
+
interface StallSqliteDetail {
|
|
14
|
+
/** The statement's fixed name, such as "touch cached resource". */
|
|
15
|
+
operation: string;
|
|
16
|
+
attempts: number;
|
|
17
|
+
/** Whether it waited on another connection's lock, rather than being slow on its own. */
|
|
18
|
+
lockWait: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
12
21
|
export interface StallEvent {
|
|
13
22
|
blockedMs: number;
|
|
14
23
|
/** The last measured section before the stall (perf-marks), if any. */
|
|
15
24
|
lastMeasured?: string;
|
|
16
25
|
/** How long before the stall was noticed that section ended. */
|
|
17
26
|
lastMeasuredEndedMsAgo?: number;
|
|
27
|
+
/** When that section was a SQLite statement that held the thread. */
|
|
28
|
+
sqlite?: StallSqliteDetail;
|
|
29
|
+
/** How long the process had been running when the stall was noticed. */
|
|
30
|
+
uptimeMs?: number;
|
|
31
|
+
/** Whether the process had finished starting (`markStartupComplete`). */
|
|
32
|
+
phase?: "startup" | "steady";
|
|
18
33
|
}
|
|
19
34
|
|
|
20
35
|
let stallSink: ((event: StallEvent) => void) | null = null;
|
|
36
|
+
let startupComplete = false;
|
|
21
37
|
|
|
22
38
|
/**
|
|
23
39
|
* Where this process's stalls go besides the Debug Log: the stall log and its
|
|
@@ -31,6 +47,20 @@ export function setStallSink(next: ((event: StallEvent) => void) | null): () =>
|
|
|
31
47
|
};
|
|
32
48
|
}
|
|
33
49
|
|
|
50
|
+
/**
|
|
51
|
+
* The process is up: its first screen has its data, or (the desktop app's
|
|
52
|
+
* Bun process) the main window has what it asked for. Stalls before this are
|
|
53
|
+
* reported as startup ones.
|
|
54
|
+
*/
|
|
55
|
+
export function markStartupComplete(): void {
|
|
56
|
+
startupComplete = true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Test seam. */
|
|
60
|
+
export function resetStartupCompleteForTests(): void {
|
|
61
|
+
startupComplete = false;
|
|
62
|
+
}
|
|
63
|
+
|
|
34
64
|
function noteStall(event: StallEvent): void {
|
|
35
65
|
try {
|
|
36
66
|
stallSink?.(event);
|
|
@@ -39,6 +69,14 @@ function noteStall(event: StallEvent): void {
|
|
|
39
69
|
}
|
|
40
70
|
}
|
|
41
71
|
|
|
72
|
+
/** The facts of a `sqlite.blocked` sample, when they have the shape retry.ts gives them. */
|
|
73
|
+
function sqliteDetail(metadata: Record<string, unknown> | undefined): StallSqliteDetail | undefined {
|
|
74
|
+
if (!metadata) return undefined;
|
|
75
|
+
const { operation, attempts, lockWait } = metadata;
|
|
76
|
+
if (typeof operation !== "string" || typeof attempts !== "number") return undefined;
|
|
77
|
+
return { operation, attempts, lockWait: lockWait === true };
|
|
78
|
+
}
|
|
79
|
+
|
|
42
80
|
export interface MainThreadStallMonitorOptions {
|
|
43
81
|
intervalMs?: number;
|
|
44
82
|
thresholdMs?: number;
|
|
@@ -60,7 +98,9 @@ function defaultNow(): number {
|
|
|
60
98
|
* comes back late, the app was unresponsive for exactly that long. The report
|
|
61
99
|
* carries the last measured section and how long before the stall it ended,
|
|
62
100
|
* which is usually enough to tell "the cache read did it" from "the render
|
|
63
|
-
* did it" in a report that only says the app hung for a few seconds
|
|
101
|
+
* did it" in a report that only says the app hung for a few seconds, and how
|
|
102
|
+
* long the process had been up, so a slow start reads apart from a hang
|
|
103
|
+
* hours in.
|
|
64
104
|
*/
|
|
65
105
|
export function startMainThreadStallMonitor(
|
|
66
106
|
options: MainThreadStallMonitorOptions = {},
|
|
@@ -72,7 +112,8 @@ export function startMainThreadStallMonitor(
|
|
|
72
112
|
const cancel = options.cancel ?? ((handle) => clearInterval(handle as ReturnType<typeof setInterval>));
|
|
73
113
|
const report = options.report
|
|
74
114
|
?? ((blockedMs, context) => {
|
|
75
|
-
|
|
115
|
+
// Logged, but not a step: the next stall still names the work before it.
|
|
116
|
+
recordPerfSample("runtime.stall", blockedMs, context, { lastStep: false });
|
|
76
117
|
noteStall({ blockedMs, ...context });
|
|
77
118
|
});
|
|
78
119
|
|
|
@@ -83,10 +124,14 @@ export function startMainThreadStallMonitor(
|
|
|
83
124
|
previous = current;
|
|
84
125
|
if (blockedMs < thresholdMs) return;
|
|
85
126
|
const sample = lastPerfSample();
|
|
127
|
+
const sqlite = sample?.name === "sqlite.blocked" ? sqliteDetail(sample.metadata) : undefined;
|
|
86
128
|
report(blockedMs, {
|
|
87
129
|
...(sample
|
|
88
130
|
? { lastMeasured: sample.name, lastMeasuredEndedMsAgo: Math.round(current - sample.endedAt) }
|
|
89
131
|
: {}),
|
|
132
|
+
...(sqlite ? { sqlite } : {}),
|
|
133
|
+
uptimeMs: Math.round(current),
|
|
134
|
+
phase: startupComplete ? "steady" : "startup",
|
|
90
135
|
});
|
|
91
136
|
}, intervalMs);
|
|
92
137
|
if (handle && typeof (handle as { unref?: () => void }).unref === "function") {
|
package/src/utils/perf-marks.ts
CHANGED
|
@@ -20,27 +20,34 @@ function now(): number {
|
|
|
20
20
|
: Date.now();
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
//
|
|
24
|
-
// only the stall monitor ever reads them.
|
|
23
|
+
// Scalars rather than an object: every measured section writes them, and
|
|
24
|
+
// only the stall monitor ever reads them. The metadata is the caller's own
|
|
25
|
+
// object, kept by reference.
|
|
25
26
|
let lastSampleName: string | null = null;
|
|
26
27
|
let lastSampleEndedAt = 0;
|
|
28
|
+
let lastSampleMetadata: Record<string, unknown> | undefined;
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
|
-
* The most recent measured section
|
|
30
|
-
* it so a report of "the app froze" carries the
|
|
31
|
-
* have been doing.
|
|
31
|
+
* The most recent measured section, when it ended and what it was measured
|
|
32
|
+
* with. The stall monitor reads it so a report of "the app froze" carries the
|
|
33
|
+
* last thing the app is known to have been doing.
|
|
32
34
|
*/
|
|
33
|
-
export function lastPerfSample(): { name: string; endedAt: number } | null {
|
|
34
|
-
|
|
35
|
+
export function lastPerfSample(): { name: string; endedAt: number; metadata?: Record<string, unknown> } | null {
|
|
36
|
+
if (lastSampleName == null) return null;
|
|
37
|
+
return { name: lastSampleName, endedAt: lastSampleEndedAt, ...(lastSampleMetadata ? { metadata: lastSampleMetadata } : {}) };
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
function logSlowPerfSample(
|
|
38
41
|
name: string,
|
|
39
42
|
durationMs: number,
|
|
40
43
|
metadata?: Record<string, unknown>,
|
|
44
|
+
lastStep = true,
|
|
41
45
|
): void {
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
if (lastStep) {
|
|
47
|
+
lastSampleName = name;
|
|
48
|
+
lastSampleEndedAt = now();
|
|
49
|
+
lastSampleMetadata = metadata;
|
|
50
|
+
}
|
|
44
51
|
const payload = {
|
|
45
52
|
durationMs: Math.round(durationMs * 10) / 10,
|
|
46
53
|
...(metadata ?? {}),
|
|
@@ -60,13 +67,18 @@ export function isPerfTraceEnabled(): boolean {
|
|
|
60
67
|
return traceToStderr;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* A sample measured elsewhere (a React Profiler callback, for instance).
|
|
72
|
+
* `lastStep: false` logs it without making it the step a later stall names:
|
|
73
|
+
* the stall monitor's own samples, which measure no work.
|
|
74
|
+
*/
|
|
64
75
|
export function recordPerfSample(
|
|
65
76
|
name: string,
|
|
66
77
|
durationMs: number,
|
|
67
78
|
metadata?: Record<string, unknown>,
|
|
79
|
+
options?: { lastStep?: boolean },
|
|
68
80
|
): void {
|
|
69
|
-
logSlowPerfSample(name, durationMs, metadata);
|
|
81
|
+
logSlowPerfSample(name, durationMs, metadata, options?.lastStep !== false);
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
export function measurePerf<T>(
|
|
@@ -46,6 +46,50 @@ export function pricePointIntegrityFlags(point: PricePoint): string[] {
|
|
|
46
46
|
return flags;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* A wick too far from its bar's body to have traded: a low under half the lower
|
|
51
|
+
* of open and close, or a high over twice the higher, as `low<close/2` or
|
|
52
|
+
* `high>2*open` (IAG on LSE, 2021-11-29: a low of 0.0155 under a close of
|
|
53
|
+
* 1.316). Only that wick is withheld: the open and close stand, so closes,
|
|
54
|
+
* returns and drawdowns read the bar while lows, highs and ranges do not.
|
|
55
|
+
* Empty when the wicks are plausible, or when the bar's prices contradict
|
|
56
|
+
* each other, which blanks it whole (pricePointIntegrityFlags).
|
|
57
|
+
*/
|
|
58
|
+
export function pricePointWickFlags(point: PricePoint): string[] {
|
|
59
|
+
const { open, high, low, close } = point;
|
|
60
|
+
if (pricePointIntegrityFlags(point).length > 0) return [];
|
|
61
|
+
const body = [["open", open], ["close", close]].filter((entry): entry is [string, number] => isFiniteNumber(entry[1]));
|
|
62
|
+
if (body.length === 0) return [];
|
|
63
|
+
// On a tie the close names the side, as the price the bar is read by.
|
|
64
|
+
const [bottomName, bottom] = body.reduce((least, entry) => entry[1] <= least[1] ? entry : least);
|
|
65
|
+
const [topName, top] = body.reduce((most, entry) => entry[1] >= most[1] ? entry : most);
|
|
66
|
+
const flags: string[] = [];
|
|
67
|
+
if (isFiniteNumber(low) && bottom > 0 && low < bottom / 2) flags.push(`low<${bottomName}/2`);
|
|
68
|
+
if (isFiniteNumber(high) && top > 0 && high > top * 2) flags.push(`high>2*${topName}`);
|
|
69
|
+
return flags;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Whether a flag code is a withheld wick (pricePointWickFlags), whose bar keeps its open and close. */
|
|
73
|
+
export function isWickFlag(code: string): boolean {
|
|
74
|
+
return /^(low<(open|close)\/2|high>2\*(open|close))$/.test(code);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A bar's high and low with a wick too far from its body to have traded (pricePointWickFlags) left out. */
|
|
78
|
+
export function pricePointWicks(point: PricePoint): { high: number | undefined; low: number | undefined } {
|
|
79
|
+
const flags = pricePointWickFlags(point);
|
|
80
|
+
return {
|
|
81
|
+
high: flags.some((flag) => flag.startsWith("high")) ? undefined : point.high,
|
|
82
|
+
low: flags.some((flag) => flag.startsWith("low")) ? undefined : point.low,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** "1 bar has a low or high too far from its open and close to have traded; left out of ranges.", or undefined for none. */
|
|
87
|
+
export function priceHistoryWickNotice(count: number): string | undefined {
|
|
88
|
+
return count > 0
|
|
89
|
+
? `${count === 1 ? "1 bar has a low or high" : `${count} bars have lows or highs`} too far from ${count === 1 ? "its" : "their"} open and close to have traded; left out of ranges.`
|
|
90
|
+
: undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
49
93
|
export function pricePointIntegrity(point: PricePoint): PriceHistoryIntegrity | undefined {
|
|
50
94
|
if (pricePointIntegrityFlags(point).length === 0) return undefined;
|
|
51
95
|
const date = new Date(point.date);
|
|
@@ -58,8 +102,10 @@ export function pricePointIntegrity(point: PricePoint): PriceHistoryIntegrity |
|
|
|
58
102
|
/** Calculation/display fields deliberately exclude the contradictory source row. */
|
|
59
103
|
export function pricePointValues(point: PricePoint, integrity = pricePointIntegrity(point)) {
|
|
60
104
|
const value = (number: number | undefined) => !integrity && isFiniteNumber(number) ? number : null;
|
|
105
|
+
// A wick too far from the body to have traded is left out; the rest of the bar stands.
|
|
106
|
+
const wicks = pricePointWicks(point);
|
|
61
107
|
return {
|
|
62
|
-
open: value(point.open), high: value(
|
|
108
|
+
open: value(point.open), high: value(wicks.high), low: value(wicks.low),
|
|
63
109
|
close: value(point.close), volume: value(point.volume),
|
|
64
110
|
...(integrity ? { integrity } : {}),
|
|
65
111
|
};
|
package/src/version.ts
CHANGED
|
@@ -3,7 +3,7 @@ import packageJson from "../package.json" with { type: "json" };
|
|
|
3
3
|
export const VERSION = packageJson.version;
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* The npm package this copy came from:
|
|
7
|
-
*
|
|
6
|
+
* The npm package this copy came from: `@gloom-sh/terminal` or `gloomberb`,
|
|
7
|
+
* which every release publishes with the same build.
|
|
8
8
|
*/
|
|
9
9
|
export const PACKAGE_NAME = packageJson.name;
|