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
|
@@ -24,7 +24,8 @@ import {
|
|
|
24
24
|
barPriceDecimals, formatBarPrice, historyAllStart, historyBarsAfterHole, historyChangeBaseNote, historyMissingPeriodsNote, quotedPriceDivisor,
|
|
25
25
|
} from "../../../../cli/history-rows";
|
|
26
26
|
import {
|
|
27
|
-
MISSING_VOLUME_TEXT, missingVolumeCount, missingVolumeNotice, pricePointValues, priceHistoryIntegrityNotice,
|
|
27
|
+
MISSING_VOLUME_TEXT, missingVolumeCount, missingVolumeNotice, pricePointValues, pricePointWickFlags, priceHistoryIntegrityNotice,
|
|
28
|
+
priceHistoryWickNotice,
|
|
28
29
|
} from "../../../../utils/price-history-integrity";
|
|
29
30
|
import {
|
|
30
31
|
useAssetData,
|
|
@@ -257,6 +258,7 @@ export function HistoricalPricesPane({ focused, width, height }: PaneProps) {
|
|
|
257
258
|
[points, venue],
|
|
258
259
|
);
|
|
259
260
|
const integrityNotice = priceHistoryIntegrityNotice(rows.filter((row) => row.point.integrity).length);
|
|
261
|
+
const wickNotice = useMemo(() => priceHistoryWickNotice(points.filter((point) => pricePointWickFlags(point).length > 0).length), [points]);
|
|
260
262
|
const volumeMissing = useMemo(() => missingVolumeCount(rows.map((row) => ({
|
|
261
263
|
...row.point, date: row.time, partial: row.partial, filled: row.filled,
|
|
262
264
|
}))), [rows]);
|
|
@@ -356,7 +358,7 @@ export function HistoricalPricesPane({ focused, width, height }: PaneProps) {
|
|
|
356
358
|
|
|
357
359
|
usePaneNoticeFooter({
|
|
358
360
|
registrationId: "historical-prices:integrity",
|
|
359
|
-
notices: [integrityNotice, volumeNotice, ...gapNotices].filter((notice): notice is string => !!notice),
|
|
361
|
+
notices: [integrityNotice, wickNotice, volumeNotice, ...gapNotices].filter((notice): notice is string => !!notice),
|
|
360
362
|
focused,
|
|
361
363
|
title: "Price history data",
|
|
362
364
|
});
|
|
@@ -404,6 +406,7 @@ export function HistoricalPricesPane({ focused, width, height }: PaneProps) {
|
|
|
404
406
|
: loading ? (rows.length ? "Refreshing" : "Loading") : rows.length ? "Available" : "Empty"],
|
|
405
407
|
...(allStart ? [["Source starts", allStart]] : []),
|
|
406
408
|
...(integrityNotice ? [["Warning", integrityNotice]] : []),
|
|
409
|
+
...(wickNotice ? [["Warning", wickNotice]] : []),
|
|
407
410
|
...gapNotices.map((notice) => ["Gap note", notice]),
|
|
408
411
|
...(volumeNotice ? [["Volume note", volumeNotice]] : []),
|
|
409
412
|
...(error ? [["Error", error]] : []),
|
|
@@ -505,13 +505,46 @@ function coversLatestYear(ttm: FinancialTableStatement, latestYear: FinancialSta
|
|
|
505
505
|
return days <= 7;
|
|
506
506
|
}
|
|
507
507
|
|
|
508
|
+
/** Counts of the shares a company has issued or has outstanding, which are never zero; treasury shares can be. */
|
|
509
|
+
const OUTSTANDING_COUNT_FIELDS = ["basicShares", "dilutedShares", "shareIssued", "ordinarySharesNumber"] as const;
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Share counts that cannot be the company's: zero, or under a thousandth of its
|
|
513
|
+
* typical count (IAG's 4,642 for Q3 2025, millions read as shares, which shows
|
|
514
|
+
* as 0.00 bn and a fall of 100%). They are missing, not a value to grow from.
|
|
515
|
+
*/
|
|
516
|
+
function withoutImpossibleShareCounts(
|
|
517
|
+
annualStatements: FinancialStatement[],
|
|
518
|
+
quarterlyStatements: FinancialStatement[],
|
|
519
|
+
): { annualStatements: FinancialStatement[]; quarterlyStatements: FinancialStatement[] } {
|
|
520
|
+
const all = [...annualStatements, ...quarterlyStatements];
|
|
521
|
+
const floors = new Map<string, number>();
|
|
522
|
+
for (const field of OUTSTANDING_COUNT_FIELDS) {
|
|
523
|
+
const counts = all.map((row) => row[field]).filter((value): value is number => typeof value === "number" && value > 0).sort((a, b) => a - b);
|
|
524
|
+
floors.set(field, counts.length ? counts[Math.floor(counts.length / 2)]! / 1000 : 0);
|
|
525
|
+
}
|
|
526
|
+
const impossible = (row: FinancialStatement, field: typeof OUTSTANDING_COUNT_FIELDS[number]) => {
|
|
527
|
+
const value = row[field];
|
|
528
|
+
return typeof value === "number" && (value <= 0 || value < floors.get(field)!);
|
|
529
|
+
};
|
|
530
|
+
const clean = (rows: FinancialStatement[]) => rows.map((row) => {
|
|
531
|
+
const drop = OUTSTANDING_COUNT_FIELDS.filter((field) => impossible(row, field));
|
|
532
|
+
if (!drop.length) return row;
|
|
533
|
+
const next = { ...row };
|
|
534
|
+
for (const field of drop) delete next[field];
|
|
535
|
+
return next;
|
|
536
|
+
});
|
|
537
|
+
return { annualStatements: clean(annualStatements), quarterlyStatements: clean(quarterlyStatements) };
|
|
538
|
+
}
|
|
539
|
+
|
|
508
540
|
export function selectFinancialStatements(
|
|
509
541
|
period: FinancialPeriod,
|
|
510
542
|
statement: string,
|
|
511
|
-
|
|
512
|
-
|
|
543
|
+
servedAnnual: FinancialStatement[],
|
|
544
|
+
servedQuarterly: FinancialStatement[],
|
|
513
545
|
limit = period === "annual" ? 5 : 6,
|
|
514
546
|
) {
|
|
547
|
+
const { annualStatements, quarterlyStatements } = withoutImpossibleShareCounts(servedAnnual, servedQuarterly);
|
|
515
548
|
// A period with nothing on this statement (a fiscal year-end balance point on
|
|
516
549
|
// the income statement) would render as an all-dash column.
|
|
517
550
|
const tabRows = FINANCIAL_SUB_TABS.find((tab) => tab.key === statement)?.rows;
|
|
@@ -4,6 +4,7 @@ import { formatNumber, pickUnit } from "../../../../utils/format";
|
|
|
4
4
|
import { pricePointIntegrity } from "../../../../utils/price-history-integrity";
|
|
5
5
|
import { formatPerShareNumber } from "../../../../utils/reported-money";
|
|
6
6
|
import { hasStatementGap } from "../../../../utils/statement-gaps";
|
|
7
|
+
import { debtAndLeases, leaseBasis, type LeaseBasis } from "../../../../utils/lease-liabilities";
|
|
7
8
|
import { cashAndShortTermInvestments, excludesCashRunway, formatRunwayMonths, SHORT_RUNWAY_HISTORY, monthlyOperatingBurn, QUARTER_MONTHS, reportsShortTermTotal, runwayMonths, YEAR_MONTHS } from "../../../../utils/cash-runway";
|
|
8
9
|
import {
|
|
9
10
|
buildPreviousStatementMap,
|
|
@@ -58,6 +59,8 @@ export interface RatioPeriod {
|
|
|
58
59
|
price: RatioAmount;
|
|
59
60
|
/** The filer reports cash and short-term investments as a total in some period on file (utils/cash-runway). */
|
|
60
61
|
shortTermTotalOnFile?: boolean;
|
|
62
|
+
/** How net debt treats the lease liabilities the listing's balance sheets report; absent when none do. */
|
|
63
|
+
leaseBasis?: LeaseBasis;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
interface RatioInputDef {
|
|
@@ -75,6 +78,13 @@ interface RatioInputDef {
|
|
|
75
78
|
trailing?: boolean;
|
|
76
79
|
/** A shorter name for quarterly columns, so the label, TTM and its unit fit the label column. */
|
|
77
80
|
trailingLabel?: string;
|
|
81
|
+
/** Debt net of cash: where the listing reports lease liabilities, a row naming them follows it (withLeaseRow). */
|
|
82
|
+
netDebt?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Not an operand: a line the input before it includes or leaves out, shown under it. Neither its value nor
|
|
85
|
+
* its gap enters the ratio.
|
|
86
|
+
*/
|
|
87
|
+
memo?: boolean;
|
|
78
88
|
value: (period: RatioPeriod) => RatioAmount;
|
|
79
89
|
}
|
|
80
90
|
|
|
@@ -245,12 +255,41 @@ function marketCap(period: RatioPeriod): RatioAmount {
|
|
|
245
255
|
return period.price * shares;
|
|
246
256
|
}
|
|
247
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Total debt less cash and short-term investments, with the balance sheet's lease liabilities where the listing's
|
|
260
|
+
* are included (utils/lease-liabilities). A balance sheet without its lease line then has no net debt.
|
|
261
|
+
*/
|
|
262
|
+
function netDebt({ statement, leaseBasis: basis }: RatioPeriod): RatioAmount {
|
|
263
|
+
const cash = cashAndShortTerm(statement);
|
|
264
|
+
if (basis === "included") {
|
|
265
|
+
const lines = debtAndLeases(statement);
|
|
266
|
+
return lines === undefined || cash === undefined ? "not-reported" : lines.totalDebt + lines.leases - cash;
|
|
267
|
+
}
|
|
268
|
+
const debt = finite(statement.totalDebt);
|
|
269
|
+
return debt === undefined || cash === undefined ? "not-reported" : debt - cash;
|
|
270
|
+
}
|
|
271
|
+
|
|
248
272
|
function enterpriseValue(period: RatioPeriod): RatioAmount {
|
|
249
|
-
const debt =
|
|
250
|
-
|
|
251
|
-
if (debt === undefined || cash === undefined) return "not-reported";
|
|
273
|
+
const debt = netDebt(period);
|
|
274
|
+
if (isGap(debt)) return debt;
|
|
252
275
|
const cap = marketCap(period);
|
|
253
|
-
return isGap(cap) ? cap : cap + debt
|
|
276
|
+
return isGap(cap) ? cap : cap + debt;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** The lease liabilities net debt includes, or a US GAAP filer's operating leases it leaves out. */
|
|
280
|
+
const LEASE_ROW_LABEL: Record<LeaseBasis, string> = { included: "incl. leases", excluded: "excl. op. leases" };
|
|
281
|
+
|
|
282
|
+
/** The ratio with a row naming the listing's lease liabilities under its net debt, when it reports any. */
|
|
283
|
+
function withLeaseRow(def: RatioDef, basis: LeaseBasis | undefined): RatioDef {
|
|
284
|
+
const at = def.inputs.findIndex((input) => input.netDebt);
|
|
285
|
+
if (!basis || at < 0) return def;
|
|
286
|
+
const row: RatioInputDef = {
|
|
287
|
+
label: LEASE_ROW_LABEL[basis],
|
|
288
|
+
format: "money",
|
|
289
|
+
memo: true,
|
|
290
|
+
value: ({ statement }) => reported(finite(statement.capitalLeaseObligations)),
|
|
291
|
+
};
|
|
292
|
+
return { ...def, inputs: [...def.inputs.slice(0, at + 1), row, ...def.inputs.slice(at + 1)] };
|
|
254
293
|
}
|
|
255
294
|
|
|
256
295
|
const DSO: RatioDef = {
|
|
@@ -378,15 +417,7 @@ export const RATIO_TABS: RatioTabDef[] = [
|
|
|
378
417
|
label: "Net Debt / EBITDA",
|
|
379
418
|
format: "multiple",
|
|
380
419
|
inputs: [
|
|
381
|
-
{
|
|
382
|
-
label: "Net Debt",
|
|
383
|
-
format: "money",
|
|
384
|
-
value: ({ statement }) => {
|
|
385
|
-
const debt = finite(statement.totalDebt);
|
|
386
|
-
const cash = cashAndShortTerm(statement);
|
|
387
|
-
return debt === undefined || cash === undefined ? "not-reported" : debt - cash;
|
|
388
|
-
},
|
|
389
|
-
},
|
|
420
|
+
{ label: "Net Debt", format: "money", netDebt: true, value: netDebt },
|
|
390
421
|
trailingFlow("EBITDA", line("ebitda"), "÷"),
|
|
391
422
|
],
|
|
392
423
|
compute: ([debt, ebitda]) => over(debt!, ebitda!),
|
|
@@ -517,7 +548,7 @@ export const RATIO_TABS: RatioTabDef[] = [
|
|
|
517
548
|
label: "EV / EBITDA",
|
|
518
549
|
format: "multiple",
|
|
519
550
|
inputs: [
|
|
520
|
-
{ label: "Enterprise Value", format: "money", value: enterpriseValue },
|
|
551
|
+
{ label: "Enterprise Value", format: "money", netDebt: true, value: enterpriseValue },
|
|
521
552
|
trailingFlow("EBITDA", line("ebitda"), "÷"),
|
|
522
553
|
],
|
|
523
554
|
// A negative enterprise value makes the multiple meaningless too.
|
|
@@ -568,10 +599,11 @@ function inputAmount(input: RatioInputDef, period: RatioPeriod): RatioAmount {
|
|
|
568
599
|
|
|
569
600
|
export function evaluateRatio(def: RatioDef, period: RatioPeriod): RatioCell {
|
|
570
601
|
const inputs = def.inputs.map((input) => inputAmount(input, period));
|
|
571
|
-
const
|
|
602
|
+
const operands = inputs.filter((_, index) => !def.inputs[index]!.memo);
|
|
603
|
+
const gap = GAP_ORDER.find((candidate) => operands.includes(candidate));
|
|
572
604
|
if (gap) return { value: gap, inputs };
|
|
573
|
-
const values = inputs.
|
|
574
|
-
|
|
605
|
+
const values = def.inputs.flatMap((input, index) => (
|
|
606
|
+
input.memo ? [] : [input.annualized && period.quarterly ? (inputs[index] as number) * QUARTERS_PER_YEAR : inputs[index] as number]
|
|
575
607
|
));
|
|
576
608
|
const value = def.compute(values, period);
|
|
577
609
|
return { value: isGap(value) || Number.isFinite(value) ? value : "not-meaningful", inputs };
|
|
@@ -702,6 +734,7 @@ export function ratioPeriods(
|
|
|
702
734
|
onFile: { annual: readonly FinancialStatement[]; quarterly: readonly FinancialStatement[] } = { annual: [], quarterly: [] },
|
|
703
735
|
): RatioPeriod[] {
|
|
704
736
|
const shortTermTotalOnFile = reportsShortTermTotal([...onFile.annual, ...onFile.quarterly, ...statements]);
|
|
737
|
+
const leases = leaseBasis([...onFile.annual, ...onFile.quarterly, ...statements]);
|
|
705
738
|
return statements.map((statement) => {
|
|
706
739
|
const quarterly = period === "quarterly" && statement.date !== "TTM";
|
|
707
740
|
return {
|
|
@@ -711,6 +744,7 @@ export function ratioPeriods(
|
|
|
711
744
|
...(quarterly ? { trailing: trailingYear(statement, onFile.annual, onFile.quarterly) } : {}),
|
|
712
745
|
price: price(statement),
|
|
713
746
|
shortTermTotalOnFile,
|
|
747
|
+
...(leases ? { leaseBasis: leases } : {}),
|
|
714
748
|
};
|
|
715
749
|
});
|
|
716
750
|
}
|
|
@@ -758,11 +792,12 @@ export function buildRatioTableModel(
|
|
|
758
792
|
): RatioTableModel {
|
|
759
793
|
const quarterly = periods.some((period) => period.quarterly);
|
|
760
794
|
const totalOnFile = periods.some((period) => period.shortTermTotalOnFile);
|
|
795
|
+
const leases = periods.find((period) => period.leaseBasis)?.leaseBasis;
|
|
761
796
|
const ratios = visibleRatios(tab, quarterly)
|
|
762
797
|
.filter((def) => def.format !== "months" || !excludesCashRunway(profile))
|
|
763
798
|
.map((def) => def.format === "months" && !totalOnFile
|
|
764
799
|
? { ...def, inputs: def.inputs.map((input, index) => index === 0 ? { ...input, label: "Cash" } : input) }
|
|
765
|
-
: def);
|
|
800
|
+
: withLeaseRow(def, leases));
|
|
766
801
|
const cells = new Map(ratios.map((def) => [def.id, periods.map((period) => evaluateRatio(def, period))]));
|
|
767
802
|
const inputs = ratios.flatMap((def) => def.inputs.map((input, index) => {
|
|
768
803
|
const values = cells.get(def.id)!.map((cell) => cell.inputs[index]).filter((value) => typeof value === "number");
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
FINANCIAL_VINTAGE_NOTICE, filledPeriodsNote, hasOlderStatements, reportsHalfYearly, stalePeriodNote, statementHistoryNotes,
|
|
3
|
+
} from "../../../utils/financial-statements";
|
|
4
|
+
import { canonicalExchange } from "../../../utils/exchanges";
|
|
2
5
|
import { hasValidQuoteObservationTime } from "../../../market-data/quotes/freshness";
|
|
3
6
|
import { EXTENDED_SESSION_LABELS, getQuoteSessionFields, quoteHeadlineTime, type ExtendedSession } from "../../../market-data/market/status";
|
|
4
7
|
import type { HeadlessPaneColumn, HeadlessPaneDefinition, HeadlessPaneFreshness, HeadlessPaneOptionValues } from "../../../types/headless";
|
|
@@ -6,7 +9,8 @@ import type { TimeRange } from "../../../time-series/range";
|
|
|
6
9
|
import { formatNumber, formatPercentRaw } from "../../../utils/format";
|
|
7
10
|
import { formatMarketPrice, formatMarketPriceWithCurrency, formatPriceObservation, penceFigure, quoteFormatOptions, withCurrencyMinorDigits, type MarketFormatOptions } from "../../../market-data/market/format";
|
|
8
11
|
import {
|
|
9
|
-
MISSING_VOLUME_TEXT, missingVolumeCount, missingVolumeNotice, pricePointValues, priceHistoryIntegrityNotice,
|
|
12
|
+
MISSING_VOLUME_TEXT, missingVolumeCount, missingVolumeNotice, pricePointValues, pricePointWickFlags, priceHistoryIntegrityNotice,
|
|
13
|
+
priceHistoryWickNotice,
|
|
10
14
|
} from "../../../utils/price-history-integrity";
|
|
11
15
|
import {
|
|
12
16
|
buildFinancialTableModel, financialStatementCurrency, financialStatementDateNotice, financialStatementLimitations,
|
|
@@ -151,18 +155,23 @@ export const financialStatementsHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
|
151
155
|
const stale = table ? stalePeriodNote(financialStatementPeriods(table.statements, table.subTab)) : null;
|
|
152
156
|
// Years filled from the listing's exchange, with the lines this table shows.
|
|
153
157
|
const filled = table ? filledPeriodsNote(table.statements, financialSubTabFields(table.subTab)) : null;
|
|
158
|
+
const periodDates = dates.map(({ date }) => date);
|
|
159
|
+
const missing = rows.length ? null : await missingPeriodsError(ctx, symbol, financials, requestedPeriod, hasRequestedStatements, options.statement);
|
|
154
160
|
return {
|
|
155
161
|
rows, columns, unavailableSymbols: rows.length ? [] : [symbol],
|
|
156
|
-
...(
|
|
162
|
+
...(missing ? { errors: [missing] } : {}),
|
|
157
163
|
// The report closes with how old its latest period is, how far back its periods go, and how to read further.
|
|
158
|
-
notes: [...stale ? [stale] : [], ...filled ? [filled] : [], ...offering ? [offering] : [],
|
|
164
|
+
notes: [...stale ? [stale] : [], ...filled ? [filled] : [], ...offering ? [offering] : [],
|
|
165
|
+
...statementHistoryNotes(requestedPeriod, periodDates, symbol, await reportOlderStatements(ctx, symbol, requestedPeriod, periodDates))],
|
|
159
166
|
metadata: {
|
|
160
167
|
symbol, name: financials.quote?.name ?? symbol, currency: statementCurrency ?? null, unit: table?.unit ?? null, quoteCurrency: financials.quote?.currency ?? null,
|
|
161
168
|
statement: table?.subTab.key ?? options.statement, statementLabel: table?.subTab.name ?? null,
|
|
162
169
|
period: requestedPeriod, growthBasis: requestedPeriod === "quarterly" ? "QoQ" : "YoY", columns: dates,
|
|
163
170
|
shareBasis: shareBases.size === 1 ? [...shareBases][0] ?? null : null,
|
|
164
171
|
...(receiptPeriods.length > 0 ? { adrRatio: financials.fundamentals?.adrRatio ?? financials.quote?.adrRatio ?? null } : {}),
|
|
165
|
-
|
|
172
|
+
// A quarter's change is against the quarter before it, which a reader of a table of quarters must be told.
|
|
173
|
+
notices: rows.length ? [FINANCIAL_VINTAGE_NOTICE, ...requestedPeriod === "quarterly" ? [QUARTERLY_GROWTH_NOTICE] : [],
|
|
174
|
+
...(shareBasisNotice ? [shareBasisNotice] : [])] : [],
|
|
166
175
|
limitations: financialStatementLimitations(financials),
|
|
167
176
|
dateProvenance: financialStatementDateNotice(table?.statements ?? []),
|
|
168
177
|
},
|
|
@@ -172,6 +181,44 @@ export const financialStatementsHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
|
172
181
|
|
|
173
182
|
const reportAmount = (amount: RatioAmount) => (typeof amount === "number" ? amount : null);
|
|
174
183
|
|
|
184
|
+
/** What a table of quarters says its growth is measured against. */
|
|
185
|
+
const QUARTERLY_GROWTH_NOTICE = "Growth is vs prior quarter.";
|
|
186
|
+
|
|
187
|
+
type HeadlessContext = Parameters<typeof financialStatementsHeadless.load>[1];
|
|
188
|
+
|
|
189
|
+
/** Whether GF, reading the listing's extended statement history, goes back further than the report (statementHistoryNotes). */
|
|
190
|
+
function reportOlderStatements(ctx: HeadlessContext, symbol: string, period: "annual" | "quarterly", dates: readonly string[]) {
|
|
191
|
+
return hasOlderStatements(async () => {
|
|
192
|
+
const { symbol: bare, exchange } = await resolveHeadlessInstrument(ctx, symbol);
|
|
193
|
+
return ctx.marketData.getTickerFinancials(bare, exchange ?? "", { statementHistory: "extended" });
|
|
194
|
+
}, period, dates);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Why a statements report has no rows, and what to read instead: a listing
|
|
199
|
+
* with years but no quarters names its years, and says so of a company that
|
|
200
|
+
* reports half-yearly (Qantas on ASX). Null when it has years and quarters,
|
|
201
|
+
* whatever this table lacks.
|
|
202
|
+
*/
|
|
203
|
+
async function missingPeriodsError(
|
|
204
|
+
ctx: HeadlessContext,
|
|
205
|
+
symbol: string,
|
|
206
|
+
financials: Awaited<ReturnType<typeof loadHeadlessFinancials>>,
|
|
207
|
+
period: "annual" | "quarterly",
|
|
208
|
+
hasRequestedStatements: boolean,
|
|
209
|
+
statement: unknown,
|
|
210
|
+
): Promise<string | null> {
|
|
211
|
+
const hasYears = financials.annualStatements.length > 0;
|
|
212
|
+
if (period === "annual" || !hasYears) return hasRequestedStatements ? null : `${symbol}: No ${period} financial statement coverage.`;
|
|
213
|
+
const exchange = await resolveHeadlessInstrument(ctx, symbol).then(({ exchange }) => canonicalExchange(exchange ?? ""))
|
|
214
|
+
.catch(logFailure("ticker-detail.headless", "Resolving the listing of a statements report failed", ""));
|
|
215
|
+
const halfYearly = reportsHalfYearly(exchange, financials.quarterlyStatements);
|
|
216
|
+
if (hasRequestedStatements && !halfYearly) return null;
|
|
217
|
+
const tab = typeof statement === "string" && statement && statement !== "income" ? ` --statement ${statement}` : "";
|
|
218
|
+
const years = `For its years: gloom fn FA ${symbol} --period annual${tab}`;
|
|
219
|
+
return halfYearly ? `${symbol} reports half-yearly, not quarterly. ${years}` : `${symbol}: No quarterly financial statement coverage. ${years}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
175
222
|
/** The note that an offering filed after the latest balance sheet is not in its cash yet; a failed read leaves it out. */
|
|
176
223
|
async function reportOfferingNote(
|
|
177
224
|
ctx: Parameters<typeof financialStatementsHeadless.load>[1],
|
|
@@ -240,16 +287,18 @@ async function loadRatioReport(
|
|
|
240
287
|
const runwayShown = liquidity && table.ratios.some((def) => def.format === "months"
|
|
241
288
|
&& table.cells.get(def.id)!.some((cell) => typeof cell.value === "number"));
|
|
242
289
|
const offering = liquidity ? await reportOfferingNote(ctx, symbol, financials) : null;
|
|
290
|
+
const periodDates = dates.map(({ date }) => date);
|
|
291
|
+
const missing = rows.length ? null : await missingPeriodsError(ctx, symbol, financials, period, hasRequestedStatements, tab.key);
|
|
243
292
|
return {
|
|
244
293
|
rows, columns, unavailableSymbols: rows.length ? [] : [symbol],
|
|
245
|
-
...(
|
|
294
|
+
...(missing ? { errors: [missing] } : {}),
|
|
246
295
|
...(tab.key === "valuation" && hasRequestedStatements && !history?.length ? { complete: false } : {}),
|
|
247
296
|
notes: [
|
|
248
297
|
...[stalePeriodNote(dates.map(({ periodEnd }) => ({ date: periodEnd, lead: true })))].filter((note): note is string => !!note),
|
|
249
298
|
...[filledPeriodsNote(table?.periods.map(({ statement }) => statement) ?? [])].filter((note): note is string => !!note),
|
|
250
299
|
...offering ? [offering] : [],
|
|
251
300
|
...runwayShown ? [runwayMethod(reportsShortTermTotal([...financials.quarterlyStatements, ...financials.annualStatements]))] : [],
|
|
252
|
-
...statementHistoryNotes(period,
|
|
301
|
+
...statementHistoryNotes(period, periodDates, symbol, await reportOlderStatements(ctx, symbol, period, periodDates)),
|
|
253
302
|
],
|
|
254
303
|
metadata: {
|
|
255
304
|
symbol, name: financials.quote?.name ?? symbol, currency: statementCurrency ?? null,
|
|
@@ -453,7 +502,9 @@ export const historicalPricesHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
|
453
502
|
const drawdown = maxDrawdown(shown);
|
|
454
503
|
const drawdownLine = drawdown ? maxDrawdownText(drawdown, { basis: `${historyBarSizeWord(size)} closes`, price: (value) => formatBarPrice(value, decimals),
|
|
455
504
|
date: (value) => isDailyOrLongerBar(size) ? value.slice(0, 10) : formatUtcTime(value) }) : null;
|
|
456
|
-
|
|
505
|
+
// A wick left out of the range is said, though the bar's close still counts.
|
|
506
|
+
const wickNotice = priceHistoryWickNotice(points.filter((point) => pricePointWickFlags(point).length > 0).length);
|
|
507
|
+
const reportNotices = [drawdownLine, notice, wickNotice].filter((line): line is string => line != null);
|
|
457
508
|
return {
|
|
458
509
|
rows: shown,
|
|
459
510
|
columns: historyColumns(size, marked, false, false, decimals),
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { formatPriceEarnings, NOT_MEANINGFUL } from "../../../../utils/price-earnings";
|
|
2
2
|
import { convertMarketCapitalization } from "../../../../utils/market-capitalization";
|
|
3
3
|
import { priceColor } from "../../../../theme/colors";
|
|
4
|
-
import type { CompanyProfile, DepositaryPair, NextEarnings, Quote, TickerFinancials } from "../../../../types/financials";
|
|
4
|
+
import type { CedearRatio, CompanyProfile, DepositaryPair, NextEarnings, Quote, TickerFinancials } from "../../../../types/financials";
|
|
5
5
|
import { adrRatioText } from "../../../../utils/depositary-receipt";
|
|
6
|
-
import { ratioCount } from "../../../../market-data/depositary-pair";
|
|
6
|
+
import { cedearRatioText, ratioCount } from "../../../../market-data/depositary-pair";
|
|
7
7
|
import { ADR_PAIR_TAB_ID, ADR_PAIR_TEMPLATE_ID } from "../adr-pair/model";
|
|
8
8
|
import type { Portfolio, TickerPosition, TickerRecord } from "../../../../types/ticker";
|
|
9
9
|
import type { BrokerInstanceConfig } from "../../../../types/config";
|
|
@@ -151,6 +151,7 @@ export function buildOverviewStats({
|
|
|
151
151
|
nextEarnings,
|
|
152
152
|
depositaryReceipt = false,
|
|
153
153
|
depositaryPair = null,
|
|
154
|
+
cedearRatio = null,
|
|
154
155
|
today,
|
|
155
156
|
dividend,
|
|
156
157
|
bond = null,
|
|
@@ -169,6 +170,8 @@ export function buildOverviewStats({
|
|
|
169
170
|
depositaryReceipt?: boolean;
|
|
170
171
|
/** The receipt pair the listing is a side of, as Gloom Cloud serves it; `receiptSide` when the listing is the receipt. */
|
|
171
172
|
depositaryPair?: { pair: DepositaryPair; receiptSide: boolean } | null;
|
|
173
|
+
/** A Buenos Aires CEDEAR's ratio in its company's shares, as Gloom Cloud serves it. */
|
|
174
|
+
cedearRatio?: CedearRatio | null;
|
|
172
175
|
/** The listing's calendar day, YYYY-MM-DD; dates before it are left out. */
|
|
173
176
|
today?: string;
|
|
174
177
|
/** The history the dividend yield reads (useOverviewDividend): a trailing one is measured from its cash, a forward one stops with it; without it the yield stands. */
|
|
@@ -223,6 +226,11 @@ export function buildOverviewStats({
|
|
|
223
226
|
? { label: "ADR Ratio", value: `1:${count}`, detail: [adrRatioText(pair.ratio)!, `${count} sh per ADR`], link: ADR_PAIR }
|
|
224
227
|
: { label: "ADR", value: pair.receipt, detail: [`1 ADR = ${count} of these shares`, `1:${count}`], link: ADR_PAIR });
|
|
225
228
|
}
|
|
229
|
+
if (cedearRatio) {
|
|
230
|
+
const receipts = ratioCount(cedearRatio.receipts);
|
|
231
|
+
const shares = ratioCount(cedearRatio.shares);
|
|
232
|
+
stats.push({ label: "CEDEAR Ratio", value: `${receipts}:${shares}`, detail: [cedearRatioText(cedearRatio), `${receipts} per ${shares} sh`] });
|
|
233
|
+
}
|
|
226
234
|
const trailingPE = liveTrailingPE(quote, fundamentals);
|
|
227
235
|
if (trailingPE != null) {
|
|
228
236
|
stats.push({ label: "P/E (TTM)", value: formatPriceEarnings(trailingPE) });
|
|
@@ -42,7 +42,7 @@ import { appTtmRate, enterpriseValueReconciliationNote, ttmAtAppRate, ttmConvers
|
|
|
42
42
|
import { atRegularSessionPrice } from "../../../market-data/quotes/regular-session-valuation";
|
|
43
43
|
import { zonedDateKey } from "../../../utils/zoned-date-time";
|
|
44
44
|
import { sharesOutstandingInReceipts } from "../../../utils/depositary-receipt";
|
|
45
|
-
import { isReceiptSide, listingDepositaryPair } from "../../../market-data/depositary-pair";
|
|
45
|
+
import { isReceiptSide, listingCedearRatio, listingDepositaryPair } from "../../../market-data/depositary-pair";
|
|
46
46
|
import {
|
|
47
47
|
CompactRangeBar,
|
|
48
48
|
FundamentalsGrid,
|
|
@@ -335,6 +335,7 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
335
335
|
nextEarnings: financials?.nextEarnings,
|
|
336
336
|
depositaryReceipt: sharesOutstandingInReceipts(quote, fundamentals, description),
|
|
337
337
|
depositaryPair: depositaryPair ? { pair: depositaryPair, receiptSide: isReceiptSide(financials) } : null,
|
|
338
|
+
cedearRatio: listingCedearRatio(financials),
|
|
338
339
|
// Report and ex-dividend dates are the listing's calendar days.
|
|
339
340
|
today: zonedDateKey(Date.now(), chartTimeZone ?? "America/New_York"),
|
|
340
341
|
dividend,
|
|
@@ -326,16 +326,21 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
|
|
|
326
326
|
selectResearchTab(tabId);
|
|
327
327
|
return true;
|
|
328
328
|
}, [selectResearchTab, visibleTabIds]);
|
|
329
|
+
// Without a ticker the pane draws only its empty state, and the tab list has
|
|
330
|
+
// no Overview, so the fallback above lands on the first tab that needs no
|
|
331
|
+
// ticker. Nobody opened it, and keeping it mounted would load its data on
|
|
332
|
+
// every ticker switch.
|
|
333
|
+
const shownTabId = ticker && visibleTabIds.has(resolvedTabId) ? resolvedTabId : null;
|
|
329
334
|
const renderedTabIds = useMemo(() => {
|
|
330
335
|
const next = new Set<string>();
|
|
331
336
|
for (const tabId of mountedTabIds) {
|
|
332
337
|
if (visibleTabIds.has(tabId)) next.add(tabId);
|
|
333
338
|
}
|
|
334
|
-
if (
|
|
335
|
-
next.add(
|
|
339
|
+
if (shownTabId) {
|
|
340
|
+
next.add(shownTabId);
|
|
336
341
|
}
|
|
337
342
|
return next;
|
|
338
|
-
}, [mountedTabIds,
|
|
343
|
+
}, [mountedTabIds, shownTabId, visibleTabIds]);
|
|
339
344
|
|
|
340
345
|
const handlePluginCapture = useCallback((capturing: boolean) => {
|
|
341
346
|
setPluginCaptured(capturing);
|
|
@@ -354,12 +359,12 @@ export function TickerResearchPane({ focused, width, height }: PaneProps) {
|
|
|
354
359
|
for (const tabId of current) {
|
|
355
360
|
if (visibleTabIds.has(tabId)) next.add(tabId);
|
|
356
361
|
}
|
|
357
|
-
if (
|
|
358
|
-
next.add(
|
|
362
|
+
if (shownTabId) {
|
|
363
|
+
next.add(shownTabId);
|
|
359
364
|
}
|
|
360
365
|
return sameStringSet(current, next) ? current : next;
|
|
361
366
|
});
|
|
362
|
-
}, [
|
|
367
|
+
}, [shownTabId, visibleTabIds]);
|
|
363
368
|
|
|
364
369
|
if (!ticker) {
|
|
365
370
|
const isEmptyFollowCollection = paneInstance?.binding?.kind === "follow" && !!collectionId && collectionTickerCount === 0;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createPluginCache } from "../../../data/plugin-cache";
|
|
1
|
+
import { createPluginCache, savedCopyForTransientFailure } from "../../../data/plugin-cache";
|
|
2
|
+
import { servedFromSavedCopy } from "../../../data/saved-copies";
|
|
2
3
|
import type { ConnectionHealthRegistry } from "../../../core/connection-health";
|
|
3
4
|
import type { PluginPersistence } from "../../../types/plugin";
|
|
4
5
|
import {
|
|
@@ -37,6 +38,8 @@ export interface TreasuryAuctionsResult {
|
|
|
37
38
|
fetchedAt: number;
|
|
38
39
|
/** True when the network failed and expired cache was served instead. */
|
|
39
40
|
stale: boolean;
|
|
41
|
+
/** The refresh failed and the copy saved at this time (ms) answered instead. */
|
|
42
|
+
savedAt?: number;
|
|
40
43
|
refreshError?: string;
|
|
41
44
|
}
|
|
42
45
|
|
|
@@ -84,11 +87,18 @@ export async function loadTreasuryAuctions(
|
|
|
84
87
|
window: AuctionHistoryWindow = AUCTION_HISTORY_DAYS,
|
|
85
88
|
): Promise<TreasuryAuctionsResult> {
|
|
86
89
|
if (window === AUCTION_HISTORY_ALL) return loadAllTreasuryAuctions(force, loader);
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
// A saved board answers only for a failure asking again may get past; a refusal or an invalid page is the error.
|
|
91
|
+
const result = savedCopyForTransientFailure(await cache.load(`recent:${window}`, walk(loader, () => auctionWindowQuery(window)), { force }));
|
|
92
|
+
return answered({
|
|
89
93
|
auctions: result.data, fetchedAt: result.fetchedAt, stale: result.stale,
|
|
94
|
+
...(result.source === "stale-fallback" ? { savedAt: result.fetchedAt } : {}),
|
|
90
95
|
...(result.refreshError ? { refreshError: result.refreshError } : {}),
|
|
91
|
-
};
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** A board a saved copy answered: marked as one, so a command ends by saying when it was saved, not with its failure. */
|
|
100
|
+
function answered(result: TreasuryAuctionsResult): TreasuryAuctionsResult {
|
|
101
|
+
return result.savedAt != null ? servedFromSavedCopy(result, result.savedAt) : result;
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
/**
|
|
@@ -100,16 +110,18 @@ export async function loadTreasuryAuctions(
|
|
|
100
110
|
async function loadAllTreasuryAuctions(force: boolean, loader: AuctionLoader): Promise<TreasuryAuctionsResult> {
|
|
101
111
|
const boundary = auctionArchiveBoundary();
|
|
102
112
|
const [recent, archive] = await Promise.all([
|
|
103
|
-
cache.load(`since:${boundary}`, walk(loader, () => ({ from: boundary })), { force }),
|
|
113
|
+
cache.load(`since:${boundary}`, walk(loader, () => ({ from: boundary })), { force }).then(savedCopyForTransientFailure),
|
|
104
114
|
archiveCache.load(`archive:${boundary}`, walk(loader, () => ({
|
|
105
115
|
from: AUCTION_HISTORY_START, before: boundary, pageSize: AUCTION_ARCHIVE_PAGE_SIZE,
|
|
106
|
-
}))),
|
|
116
|
+
}))).then(savedCopyForTransientFailure),
|
|
107
117
|
]);
|
|
108
118
|
const seen = new Set<string>();
|
|
109
119
|
const auctions = [...recent.data, ...archive.data].filter((auction) => !seen.has(auction.id) && Boolean(seen.add(auction.id)));
|
|
110
120
|
const refreshError = recent.refreshError ?? archive.refreshError;
|
|
111
|
-
|
|
121
|
+
const savedAt = Math.min(...[recent, archive].map((part) => part.source === "stale-fallback" ? part.fetchedAt : Infinity));
|
|
122
|
+
return answered({
|
|
112
123
|
auctions, fetchedAt: recent.fetchedAt, stale: recent.stale || archive.stale,
|
|
124
|
+
...(Number.isFinite(savedAt) ? { savedAt } : {}),
|
|
113
125
|
...(refreshError ? { refreshError } : {}),
|
|
114
|
-
};
|
|
126
|
+
});
|
|
115
127
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ApiRequestError } from "../../../api-client/errors";
|
|
1
2
|
import { createThrottledFetch } from "../../../utils/throttled-fetch";
|
|
2
3
|
import type { TreasuryAuction, TreasuryAuctionRaw } from "./types";
|
|
3
4
|
|
|
@@ -222,9 +223,8 @@ export async function fetchTreasuryAuctions(
|
|
|
222
223
|
): Promise<TreasuryAuction[]> {
|
|
223
224
|
return fetchAuctionPages(async (page) => {
|
|
224
225
|
const response = await TREASURY_FETCH.fetch(buildAuctionsUrl(query, page));
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
226
|
+
// The status stays: an overloaded server is a failure a saved board may answer for, a refusal is not.
|
|
227
|
+
if (!response.ok) throw new ApiRequestError(`Treasury Fiscal Data request failed (${response.status})`, response.status);
|
|
228
228
|
return response.json();
|
|
229
229
|
});
|
|
230
230
|
}
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
import { formatCompact } from "../../../utils/format";
|
|
6
6
|
import { AUCTION_HISTORY_ALL, AUCTION_HISTORY_START, fetchTreasuryAuctions, type AuctionHistoryWindow } from "./client";
|
|
7
7
|
import { loadTreasuryAuctions, type TreasuryAuctionsResult } from "./cache";
|
|
8
|
+
import { unansweredRefreshError } from "../../../data/saved-copies";
|
|
8
9
|
import type { TreasuryAuction } from "./types";
|
|
9
10
|
import { FIXED_INCOME_VIEW_STATE } from "../fixed-income/model";
|
|
10
11
|
import {
|
|
@@ -28,27 +29,34 @@ import {
|
|
|
28
29
|
} from "./model";
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
|
-
* A
|
|
32
|
-
*
|
|
33
|
-
*
|
|
32
|
+
* A missing bid-to-cover or bidder split is a blank cell: the source has
|
|
33
|
+
* bid-to-cover regularly from late 1999 and the split only from 2008-04-07,
|
|
34
|
+
* and an announced auction has no results to fill them; its Rate says so.
|
|
34
35
|
*/
|
|
35
|
-
const
|
|
36
|
-
|
|
36
|
+
const formatPercent = (value: unknown) => value == null ? "" : `${Number(value).toFixed(1)}%`;
|
|
37
|
+
|
|
38
|
+
/** What an announced auction's Rate reads until the source publishes its results. */
|
|
39
|
+
const RESULTS_NOT_PUBLISHED = "results not published";
|
|
40
|
+
const pending = (row: Record<string, unknown>) => isPendingAuction(row as unknown as TreasuryAuction);
|
|
37
41
|
|
|
38
42
|
/** Why the stop-out column is not a tail; the docs page says the same at length. */
|
|
39
43
|
const STOP_OUT_NOTE = "Stop-out vs avg is the high yield minus the average/median yield, in bp (bills: discount rates). "
|
|
40
44
|
+ "A true tail, the stop-out against the 1 pm when-issued yield, needs the when-issued yield, which the source does not carry.";
|
|
41
45
|
|
|
42
46
|
const MISSING_METRICS_NOTE = "A blank B/C or bidder share is one the source does not have for that auction: "
|
|
43
|
-
+ "bid-to-cover is regular from late 1999, the indirect, direct and dealer split from 2008-04-07.
|
|
47
|
+
+ "bid-to-cover is regular from late 1999, the indirect, direct and dealer split from 2008-04-07.";
|
|
44
48
|
|
|
45
49
|
/** A completed auction the source gives no bid-to-cover or no bidder split for. */
|
|
46
50
|
const lacksMetrics = (auction: TreasuryAuction) => !isPendingAuction(auction)
|
|
47
51
|
&& (auction.bidToCoverRatio == null || indirectPct(auction) == null || directPct(auction) == null || dealerPct(auction) == null);
|
|
48
52
|
|
|
49
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* The rate as the table prints it; a TIPS or FRN row says what its number is
|
|
55
|
+
* (`2.653% real`, `4.0bp spread`), and an announced auction that its results
|
|
56
|
+
* are not published yet.
|
|
57
|
+
*/
|
|
50
58
|
const rateCell = (value: unknown, row: Record<string, unknown>) => {
|
|
51
|
-
const text = formatAuctionRate(row as unknown as TreasuryAuction, value == null ? null : Number(value), "-");
|
|
59
|
+
const text = formatAuctionRate(row as unknown as TreasuryAuction, value == null ? null : Number(value), pending(row) ? RESULTS_NOT_PUBLISHED : "-");
|
|
52
60
|
const qualifier = value == null ? null : rateQualifier(row as unknown as TreasuryAuction);
|
|
53
61
|
return qualifier ? `${text} ${qualifier}` : text;
|
|
54
62
|
};
|
|
@@ -61,14 +69,16 @@ const COLUMNS = [
|
|
|
61
69
|
key: "rate",
|
|
62
70
|
header: "Rate",
|
|
63
71
|
align: "right" as const,
|
|
64
|
-
format: (value: unknown, row: Record<string, unknown>) => formatAuctionRate(
|
|
72
|
+
format: (value: unknown, row: Record<string, unknown>) => formatAuctionRate(
|
|
73
|
+
row as unknown as TreasuryAuction, value == null ? null : Number(value), pending(row) ? RESULTS_NOT_PUBLISHED : "-",
|
|
74
|
+
),
|
|
65
75
|
textFormat: rateCell,
|
|
66
76
|
},
|
|
67
|
-
{ key: "stopOutVsAvgBp", header: "Stop-out vs avg (bp)", align: "right" as const, format: (value: unknown) => value == null ? "-" : Number(value).toFixed(1) },
|
|
68
77
|
{
|
|
69
|
-
key: "
|
|
70
|
-
format: (value: unknown, row: Record<string, unknown>) => value == null ?
|
|
78
|
+
key: "stopOutVsAvgBp", header: "Stop-out vs avg (bp)", align: "right" as const,
|
|
79
|
+
format: (value: unknown, row: Record<string, unknown>) => value == null ? pending(row) ? "" : "-" : Number(value).toFixed(1),
|
|
71
80
|
},
|
|
81
|
+
{ key: "bidToCoverRatio", header: "B/C", align: "right" as const, format: (value: unknown) => value == null ? "" : Number(value).toFixed(2) },
|
|
72
82
|
{ key: "indirectPercent", header: "Indirect", align: "right" as const, format: formatPercent },
|
|
73
83
|
{ key: "directPercent", header: "Direct", align: "right" as const, format: formatPercent },
|
|
74
84
|
{ key: "dealerPercent", header: "Dealer", align: "right" as const, format: formatPercent },
|
|
@@ -147,7 +157,8 @@ function createTreasuryAuctionsHeadless(
|
|
|
147
157
|
];
|
|
148
158
|
return {
|
|
149
159
|
complete: !result.refreshError,
|
|
150
|
-
|
|
160
|
+
// A saved copy answering the failed refresh ends the report by saying when it was saved instead.
|
|
161
|
+
errors: [unansweredRefreshError(result)].filter((error): error is string => !!error),
|
|
151
162
|
notes,
|
|
152
163
|
rows: auctions.map((auction) => ({
|
|
153
164
|
...auction,
|