@gloom-sh/terminal 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,4 +1,5 @@
|
|
|
1
1
|
import { apiClient } from "../../../api-client";
|
|
2
|
+
import { sourceDidNotAnswer } from "../../../api-client/errors";
|
|
2
3
|
import type { JodiBoardPayload } from "../../../api-client/commodity-balances";
|
|
3
4
|
import { unavailableOnServer } from "../shared/cloud-resource";
|
|
4
5
|
import type { BalanceCommodity, Board } from "./model";
|
|
@@ -32,7 +33,9 @@ function readBoard(payload: JodiBoardPayload): Balances {
|
|
|
32
33
|
|| (payload.gas != null && !validBoard(payload.gas, "gas"))) {
|
|
33
34
|
throw new Error("The server returned an invalid oil and gas balance board");
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
+
// The server's source did not answer this time, which may pass; a board with no oil has none yet.
|
|
37
|
+
if (payload.status === "unavailable") throw sourceDidNotAnswer(JODI_NOT_AVAILABLE);
|
|
38
|
+
if (payload.oil == null) throw new Error(JODI_NOT_AVAILABLE);
|
|
36
39
|
if (!validBoard(payload.oil, "oil")) throw new Error("The server returned an invalid oil and gas balance board");
|
|
37
40
|
return { oil: payload.oil, gas: payload.gas ?? null, stale: payload.stale === true, gaps: payload.gaps };
|
|
38
41
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { HeadlessPaneDefinition } from "../../../types/plugin";
|
|
2
2
|
import { resolveHeadlessInstrument } from "../shared/headless-market-data";
|
|
3
3
|
import { loadMacroDayEarnings, loadMacroDayHistory, loadMacroReleases, macroDayEarningsNotice } from "./client";
|
|
4
|
-
import { macroDayEventLabel, macroDayStats, matchesSurprise, projectMacroDays, type MacroDayStats } from "./model";
|
|
4
|
+
import { formatMacroDayMove, macroDayEventLabel, macroDayStats, matchesSurprise, projectMacroDays, type MacroDayStats } from "./model";
|
|
5
5
|
import { MACRO_EVENT_KINDS, MACRO_EVENT_LABELS, MACRO_RELEASE_SOURCES, MACRO_SURPRISE_LABELS, MACRO_SURPRISES, staleMacroReleasesNotice,
|
|
6
6
|
type MacroEventKind, type MacroSurprise } from "./releases";
|
|
7
7
|
|
|
@@ -39,6 +39,12 @@ export const macroDayHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
39
39
|
releases: releases.releases, coveredThrough: releases.coveredThrough, earnings: earnings.reports });
|
|
40
40
|
const notes = [staleMacroReleasesNotice(releases), macroDayEarningsNotice(earnings)].filter((note): note is string => !!note);
|
|
41
41
|
const row = (label: string, stats: MacroDayStats) => ({ label, ...stats });
|
|
42
|
+
// A yield index (^TNX) moves by its level: its columns are headed bp and read the change in the yield.
|
|
43
|
+
const yieldMoves = model.basis === "yield";
|
|
44
|
+
const bp = (signed: boolean) => (value: unknown) => typeof value === "number" ? formatMacroDayMove(value, "yield", { signed, unit: false }) : "--";
|
|
45
|
+
const move = yieldMoves ? bp(true) : percent;
|
|
46
|
+
const absMove = yieldMoves ? bp(false) : percent;
|
|
47
|
+
const header = (label: string) => yieldMoves ? `${label} bp` : label;
|
|
42
48
|
const release = String(args.options.release ?? "all");
|
|
43
49
|
const surprise = String(args.options.surprise ?? "all") as MacroSurprise | "all";
|
|
44
50
|
const events = model.events.filter((event) => (release === "all" || event.kind === release) && matchesSurprise(event, surprise));
|
|
@@ -48,14 +54,14 @@ export const macroDayHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
48
54
|
return {
|
|
49
55
|
sections: [
|
|
50
56
|
{ title: "By release", columns: [{ key: "label", header: "Day" }, { key: "count", header: "Days" },
|
|
51
|
-
{ key: "meanAbs", header: "Avg |move|", format:
|
|
52
|
-
{ key: "mean", header: "Avg move", format:
|
|
57
|
+
{ key: "meanAbs", header: header("Avg |move|"), format: absMove }, { key: "multiple", header: "vs normal", format: multiple },
|
|
58
|
+
{ key: "mean", header: header("Avg move"), format: move }, { key: "hitRate", header: "Up", format: share }],
|
|
53
59
|
rows: [...model.kinds.map((kind) => row(MACRO_EVENT_LABELS[kind], model.byKind[kind])), ...selected,
|
|
54
60
|
row(model.kinds.includes("ipca") ? "Any US release" : "Any release", model.allEvents), ...(model.earnings.count ? [row("Earnings", model.earnings)] : []), row("Normal day", model.normal)] },
|
|
55
61
|
{ title: "Release days", columns: [{ key: "date", header: "Date" }, { key: "event", header: "Release" },
|
|
56
62
|
{ key: "actual", header: "Actual" }, { key: "forecast", header: "Forecast" }, { key: "prior", header: "Prior" },
|
|
57
63
|
{ key: "surprise", header: "Surprise" },
|
|
58
|
-
{ key: "session", header: "Session" }, { key: "move", header: "Move", format:
|
|
64
|
+
{ key: "session", header: "Session" }, { key: "move", header: header("Move"), format: move }, { key: "multiple", header: "vs normal", format: multiple }],
|
|
59
65
|
rows: events.map(({ figures, ...event }) => ({ ...event, event: macroDayEventLabel(event), actual: figures?.actual ?? null,
|
|
60
66
|
forecast: figures?.forecast ?? null, prior: figures?.prior ?? null, surprise: figures?.surprise ?? null,
|
|
61
67
|
...(figures ? { headline: figures.event } : {}) })) },
|
|
@@ -64,7 +70,8 @@ export const macroDayHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
64
70
|
unavailableSymbols: model.events.length ? [] : [instrument.symbol],
|
|
65
71
|
errors: history.error ? [history.error] : [],
|
|
66
72
|
...(notes.length ? { notes } : {}),
|
|
67
|
-
metadata: { unit: "
|
|
73
|
+
metadata: { unit: yieldMoves ? "change in the yield in percentage points, close to close on the release day"
|
|
74
|
+
: "decimal return, close to close on the release day", start: model.start, asOf: model.asOf,
|
|
68
75
|
releasesThrough: releases.coveredThrough, releaseSources: Object.values(MACRO_RELEASE_SOURCES),
|
|
69
76
|
stale: history.stale || earnings.stale, fetchedAt: history.fetchedAt, methodology: "docs/research-data.md#macro-day-reaction" },
|
|
70
77
|
};
|
|
@@ -16,7 +16,7 @@ export const macroDayModule: PluginModule = {
|
|
|
16
16
|
paneTemplates: [{ ...createTickerSurfacePaneTemplate({
|
|
17
17
|
id: "macro-day-pane", paneId: "macro-day", label: "Macro-Day Reaction",
|
|
18
18
|
description: "How a name moves on CPI, jobs, FOMC and Brazil IPCA days against a normal day: average move, direction, hit rate, and every release day with its actual, forecast and prior.",
|
|
19
|
-
keywords: ["mday", "macro day", "cpi day", "fomc day", "jobs day", "nfp", "payrolls", "event study", "release reaction", "ipca",
|
|
19
|
+
keywords: ["mday", "macro day", "cpi day", "fomc day", "jobs day", "jobs report", "nfp", "payrolls", "nonfarm", "event study", "release reaction", "ipca",
|
|
20
20
|
"hot cpi", "cpi surprise", "release surprise"],
|
|
21
21
|
shortcut: "MDAY", publicShare: true,
|
|
22
22
|
}), headless: macroDayHeadless }],
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { EarningsTiming } from "../../../api-client/earnings";
|
|
2
2
|
import type { PricePoint } from "../../../types/financials";
|
|
3
3
|
import { zonedDateTimeParts } from "../../../utils/zoned-date-time";
|
|
4
|
+
import { toBasisPoints } from "../../../utils/basis-points";
|
|
5
|
+
import { isYieldIndexSymbol } from "../../../tickers/index-roots";
|
|
4
6
|
import { MACRO_EVENT_KINDS, MACRO_EVENT_LABELS, US_MACRO_EVENT_KINDS, type MacroEventKind, type MacroRelease, type MacroReleaseFigures,
|
|
5
7
|
type MacroSurprise } from "./releases";
|
|
6
8
|
|
|
@@ -17,6 +19,35 @@ export interface MacroDayEarningsReport {
|
|
|
17
19
|
reactionSession?: string | null;
|
|
18
20
|
}
|
|
19
21
|
|
|
22
|
+
/**
|
|
23
|
+
* How a day's move is measured: a price's return, or for a yield (^TNX, whose
|
|
24
|
+
* level is the yield in percent) the change in that yield, in percentage
|
|
25
|
+
* points, which the pane and the report read in basis points.
|
|
26
|
+
*/
|
|
27
|
+
export type MacroDayMoveBasis = "return" | "yield";
|
|
28
|
+
|
|
29
|
+
/** A Treasury yield index moves by its level: 5.24 to 5.28 is +4bp, not +0.76%. */
|
|
30
|
+
const macroDayMoveBasis = (symbol: string): MacroDayMoveBasis => isYieldIndexSymbol(symbol) ? "yield" : "return";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A move as it reads: a return in percent (`+0.76%`), a yield's change in
|
|
34
|
+
* basis points (`+4.0bp`, or `+4.0` under a column headed bp). One that
|
|
35
|
+
* rounds to zero is unsigned.
|
|
36
|
+
*/
|
|
37
|
+
export function formatMacroDayMove(value: number | null | undefined, basis: MacroDayMoveBasis,
|
|
38
|
+
{ signed = true, unit = true }: { signed?: boolean; unit?: boolean } = {}): string {
|
|
39
|
+
if (value == null) return "--";
|
|
40
|
+
if (basis === "yield") {
|
|
41
|
+
const bp = toBasisPoints(value, 1);
|
|
42
|
+
return `${signed && bp > 0 ? "+" : ""}${bp.toFixed(1)}${unit ? "bp" : ""}`;
|
|
43
|
+
}
|
|
44
|
+
const fixed = (value * 100).toFixed(2);
|
|
45
|
+
return /[1-9]/.test(fixed) ? `${signed && value > 0 ? "+" : ""}${fixed}%` : "0.00%";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The decimals a move shows in hundredths (percent of a return, basis points of a yield), for its sign and colour. */
|
|
49
|
+
export const macroDayMoveDecimals = (basis: MacroDayMoveBasis) => basis === "yield" ? 1 : 2;
|
|
50
|
+
|
|
20
51
|
export interface MacroDayEvent {
|
|
21
52
|
kind: MacroEventKind;
|
|
22
53
|
/** Release day, New York date. */
|
|
@@ -25,7 +56,7 @@ export interface MacroDayEvent {
|
|
|
25
56
|
session: string;
|
|
26
57
|
/** The session is also the reaction session of the ticker's own earnings report: the move is the report's, not the release's. */
|
|
27
58
|
earnings: boolean;
|
|
28
|
-
/** Close before the release to the session's close. */
|
|
59
|
+
/** Close before the release to the session's close: a return, or a yield's change in percentage points (`basis`). */
|
|
29
60
|
move: number;
|
|
30
61
|
/** |move| over the normal day's average |move|. */
|
|
31
62
|
multiple: number | null;
|
|
@@ -45,6 +76,7 @@ export interface MacroDayStats {
|
|
|
45
76
|
|
|
46
77
|
export interface MacroDayModel {
|
|
47
78
|
symbol: string;
|
|
79
|
+
basis: MacroDayMoveBasis;
|
|
48
80
|
/** Newest first. */
|
|
49
81
|
events: MacroDayEvent[];
|
|
50
82
|
/** The kinds the list carries: the US releases, and IPCA when Gloom Cloud lists its days. */
|
|
@@ -140,17 +172,19 @@ export function projectMacroDays(history: readonly PricePoint[], options: {
|
|
|
140
172
|
earnings?: readonly MacroDayEarningsReport[];
|
|
141
173
|
}): MacroDayModel {
|
|
142
174
|
const { coveredThrough } = options;
|
|
175
|
+
const basis = macroDayMoveBasis(options.symbol);
|
|
143
176
|
const byDate = new Map<string, number>();
|
|
144
177
|
for (const point of history) {
|
|
145
178
|
const time = new Date(point.date).getTime();
|
|
146
|
-
|
|
179
|
+
// A yield can sit at zero or below it; a price cannot.
|
|
180
|
+
if (!Number.isFinite(time) || !Number.isFinite(point.close) || (basis === "return" && point.close <= 0)) continue;
|
|
147
181
|
byDate.set(sessionDate(time), point.close);
|
|
148
182
|
}
|
|
149
183
|
const sessions = [...byDate.keys()].sort();
|
|
150
184
|
const last = sessions.at(-1) ?? null;
|
|
151
185
|
const empty = (): MacroDayStats => stats([], null);
|
|
152
186
|
const kinds = MACRO_EVENT_KINDS.filter((kind) => US_KINDS.has(kind) || options.releases.some((release) => release.kind === kind));
|
|
153
|
-
if (!last) return { symbol: options.symbol, events: [], kinds, byKind: { cpi: empty(), jobs: empty(), fomc: empty(), ipca: empty() },
|
|
187
|
+
if (!last) return { symbol: options.symbol, basis, events: [], kinds, byKind: { cpi: empty(), jobs: empty(), fomc: empty(), ipca: empty() },
|
|
154
188
|
allEvents: empty(), earnings: empty(), normal: empty(), start: null, asOf: null };
|
|
155
189
|
|
|
156
190
|
// Sessions after the published lists end may be unlisted release days, so they are left out of both sides.
|
|
@@ -166,7 +200,7 @@ export function projectMacroDays(history: readonly PricePoint[], options: {
|
|
|
166
200
|
for (let index = 1; index < sessions.length; index++) {
|
|
167
201
|
const date = sessions[index]!, previous = sessions[index - 1]!;
|
|
168
202
|
if (date <= start || date > end || dayNumber(date) - dayNumber(previous) > MAX_SESSION_GAP_DAYS) continue;
|
|
169
|
-
moves.set(date, byDate.get(date)! / byDate.get(previous)! - 1);
|
|
203
|
+
moves.set(date, basis === "yield" ? byDate.get(date)! - byDate.get(previous)! : byDate.get(date)! / byDate.get(previous)! - 1);
|
|
170
204
|
}
|
|
171
205
|
|
|
172
206
|
const reactionSessions = new Set<string>();
|
|
@@ -191,7 +225,7 @@ export function projectMacroDays(history: readonly PricePoint[], options: {
|
|
|
191
225
|
const byKind = Object.fromEntries(MACRO_EVENT_KINDS.map((kind) =>
|
|
192
226
|
[kind, stats(events.filter((event) => event.kind === kind && !event.earnings).map((event) => event.move), normalAbs)])) as Record<MacroEventKind, MacroDayStats>;
|
|
193
227
|
const sessionMoves = (flagged: boolean) => [...reactionSessions].filter((session) => reacted.has(session) === flagged).map((session) => moves.get(session)!);
|
|
194
|
-
return { symbol: options.symbol, events, kinds, byKind, allEvents: stats(sessionMoves(false), normalAbs), earnings: stats(sessionMoves(true), normalAbs),
|
|
228
|
+
return { symbol: options.symbol, basis, events, kinds, byKind, allEvents: stats(sessionMoves(false), normalAbs), earnings: stats(sessionMoves(true), normalAbs),
|
|
195
229
|
normal, start, asOf: end };
|
|
196
230
|
}
|
|
197
231
|
|
|
@@ -13,7 +13,10 @@ import { useThemeColors } from "../../../theme/theme-context";
|
|
|
13
13
|
import type { PaneProps } from "../../../types/plugin";
|
|
14
14
|
import { Box } from "../../../ui";
|
|
15
15
|
import { loadMacroDayEarnings, loadMacroDayHistory, loadMacroReleases, macroDayEarningsNotice } from "./client";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
formatMacroDayMove, macroDayEventLabel, macroDayMoveDecimals, macroDayStats, matchesSurprise, projectMacroDays, type MacroDayEvent,
|
|
18
|
+
type MacroDayModel, type MacroDayMoveBasis,
|
|
19
|
+
} from "./model";
|
|
17
20
|
import { MACRO_EVENT_KINDS, MACRO_EVENT_LABELS, MACRO_SURPRISE_LABELS, MACRO_SURPRISES, staleMacroReleasesNotice, type MacroEventKind,
|
|
18
21
|
type MacroSurprise } from "./releases";
|
|
19
22
|
|
|
@@ -21,17 +24,13 @@ export const LOOKBACK_OPTIONS = [{ value: "1", label: "1Y" }, { value: "2", labe
|
|
|
21
24
|
const RELEASE_OPTIONS = [{ value: "all", label: "All" }, ...MACRO_EVENT_KINDS.map((kind) => ({ value: kind, label: MACRO_EVENT_LABELS[kind] }))];
|
|
22
25
|
const SURPRISE_OPTIONS = [{ value: "all", label: "All" }, ...MACRO_SURPRISES.map((surprise) => ({ value: surprise, label: MACRO_SURPRISE_LABELS[surprise] }))];
|
|
23
26
|
|
|
24
|
-
function pct(value: number | null | undefined, signed = true): string {
|
|
25
|
-
if (value == null) return "--";
|
|
26
|
-
const fixed = (value * 100).toFixed(2);
|
|
27
|
-
return /[1-9]/.test(fixed) ? `${signed && value > 0 ? "+" : ""}${fixed}%` : "0.00%";
|
|
28
|
-
}
|
|
29
27
|
const times = (value: number | null | undefined) => value == null ? "--" : `${value.toFixed(1)}x`;
|
|
30
28
|
|
|
31
29
|
const EARNINGS_MARK = "earnings";
|
|
32
30
|
const DATE: DataTableColumn = { id: "date", label: "Date", width: 19, align: "left" };
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
/** The move and its multiple of a normal day's; a yield's move is in basis points, and its header says so. */
|
|
32
|
+
const moveColumns = (basis: MacroDayMoveBasis): DataTableColumn[] => [
|
|
33
|
+
{ id: "move", label: basis === "yield" ? "Move bp" : "Move", width: 9, align: "right" },
|
|
35
34
|
{ id: "multiple", label: "vs Normal", width: 10, align: "right" },
|
|
36
35
|
];
|
|
37
36
|
/** What each release came out with, under the labels the economic calendar gives them. */
|
|
@@ -48,23 +47,25 @@ function figureColumns(rows: readonly MacroDayEvent[]): DataTableColumn[] {
|
|
|
48
47
|
* With every release listed, the release's name, widened to take the earnings marker where a row has one. With one
|
|
49
48
|
* selected, a column only for the marker: every row would repeat the name.
|
|
50
49
|
*/
|
|
51
|
-
export function columnsFor(release: string, rows: readonly MacroDayEvent[]): DataTableColumn[] {
|
|
50
|
+
export function columnsFor(release: string, rows: readonly MacroDayEvent[], basis: MacroDayMoveBasis = "return"): DataTableColumn[] {
|
|
52
51
|
const marked = rows.some((event) => event.earnings);
|
|
53
|
-
const values = [...figureColumns(rows), ...
|
|
52
|
+
const values = [...figureColumns(rows), ...moveColumns(basis)];
|
|
54
53
|
if (release === "all") return [DATE, { id: "release", label: "Release", width: marked ? 15 : 8, align: "left" }, ...values];
|
|
55
54
|
return marked ? [DATE, { id: "release", label: "Note", width: EARNINGS_MARK.length, align: "left" }, ...values] : [DATE, ...values];
|
|
56
55
|
}
|
|
57
56
|
|
|
58
|
-
/** By the sign the
|
|
59
|
-
export const moveTone = (value: number | null
|
|
60
|
-
|
|
57
|
+
/** By the sign the text shows: an average that rounds to 0.00% (or 0.0bp) is neither up nor down. */
|
|
58
|
+
export const moveTone = (value: number | null, basis: MacroDayMoveBasis = "return") =>
|
|
59
|
+
value == null || !/[1-9]/.test((value * 100).toFixed(macroDayMoveDecimals(basis))) ? undefined
|
|
60
|
+
: value > 0 ? "positive" as const : "negative" as const;
|
|
61
61
|
|
|
62
62
|
/** The figures over the days a surprise filter keeps: the same four as one release's. */
|
|
63
63
|
function surpriseFigures(model: MacroDayModel, rows: readonly MacroDayEvent[]): StatItem[] {
|
|
64
64
|
const stats = macroDayStats(rows, model.normal.meanAbs);
|
|
65
|
+
const pct = (value: number | null, signed = true) => formatMacroDayMove(value, model.basis, { signed });
|
|
65
66
|
return [
|
|
66
67
|
{ id: "abs", label: "Avg |move|", value: pct(stats.meanAbs, false), detail: stats.count ? `${times(stats.multiple)} normal` : undefined },
|
|
67
|
-
{ id: "mean", label: "Avg move", value: pct(stats.mean), tone: moveTone(stats.mean) },
|
|
68
|
+
{ id: "mean", label: "Avg move", value: pct(stats.mean), tone: moveTone(stats.mean, model.basis) },
|
|
68
69
|
{ id: "up", label: "Up", value: stats.hitRate == null ? "--" : `${Math.round(stats.hitRate * 100)}%`,
|
|
69
70
|
detail: stats.count ? `${Math.round(stats.hitRate! * stats.count)} of ${stats.count}` : undefined },
|
|
70
71
|
{ id: "normal", label: "Normal day", value: pct(model.normal.meanAbs, false), detail: "avg |move|" },
|
|
@@ -72,6 +73,7 @@ function surpriseFigures(model: MacroDayModel, rows: readonly MacroDayEvent[]):
|
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
function figures(model: MacroDayModel, release: string): StatItem[] {
|
|
76
|
+
const pct = (value: number | null, signed = true) => formatMacroDayMove(value, model.basis, { signed });
|
|
75
77
|
const normal: StatItem = { id: "normal", label: "Normal day", value: pct(model.normal.meanAbs, false), detail: "avg |move|" };
|
|
76
78
|
if (release === "all") {
|
|
77
79
|
return [...model.kinds.map((kind): StatItem => {
|
|
@@ -84,7 +86,7 @@ function figures(model: MacroDayModel, release: string): StatItem[] {
|
|
|
84
86
|
const stats = model.byKind[release as MacroEventKind];
|
|
85
87
|
return [
|
|
86
88
|
{ id: "abs", label: "Avg |move|", value: pct(stats.meanAbs, false), detail: stats.count ? `${times(stats.multiple)} normal` : undefined },
|
|
87
|
-
{ id: "mean", label: "Avg move", value: pct(stats.mean), tone: moveTone(stats.mean) },
|
|
89
|
+
{ id: "mean", label: "Avg move", value: pct(stats.mean), tone: moveTone(stats.mean, model.basis) },
|
|
88
90
|
{ id: "up", label: "Up", value: stats.hitRate == null ? "--" : `${Math.round(stats.hitRate * 100)}%`,
|
|
89
91
|
detail: stats.count ? `${Math.round(stats.hitRate! * stats.count)} of ${stats.count}` : undefined },
|
|
90
92
|
normal,
|
|
@@ -123,7 +125,7 @@ export function MacroDayPane({ width, height, focused }: PaneProps) {
|
|
|
123
125
|
&& matchesSurprise(event, surprise)) : [], [model, release, surprise]);
|
|
124
126
|
const stats = useMemo(() => !model?.events.length ? [] : surprise === "all" ? figures(model, release) : surpriseFigures(model, rows),
|
|
125
127
|
[model, release, surprise, rows]);
|
|
126
|
-
const columns = useMemo(() => columnsFor(release, rows), [release, rows]);
|
|
128
|
+
const columns = useMemo(() => columnsFor(release, rows, model?.basis), [model?.basis, release, rows]);
|
|
127
129
|
|
|
128
130
|
usePaneNoticeFooter({ registrationId: "macro-day-notices", focused,
|
|
129
131
|
notices: [...new Set([identityError, history.error, history.data?.error, releasesNotice, earningsNotice].filter((value): value is string => !!value))] });
|
|
@@ -141,9 +143,14 @@ export function MacroDayPane({ width, height, focused }: PaneProps) {
|
|
|
141
143
|
: event.figures?.surprise === "cool" ? colors.negative : undefined };
|
|
142
144
|
if (column.id === "forecast") return { text: event.figures?.forecast ?? "" };
|
|
143
145
|
if (column.id === "prior") return { text: event.figures?.prior ?? "" };
|
|
144
|
-
if (column.id === "move")
|
|
146
|
+
if (column.id === "move") {
|
|
147
|
+
const basis = model?.basis ?? "return";
|
|
148
|
+
// Under a column headed bp, a yield's move is the number alone.
|
|
149
|
+
return { text: formatMacroDayMove(event.move, basis, { unit: false }), value: event.move * 100,
|
|
150
|
+
color: shownPriceColor(event.move * 100, macroDayMoveDecimals(basis), colors) };
|
|
151
|
+
}
|
|
145
152
|
return { text: times(event.multiple), value: event.multiple };
|
|
146
|
-
}, [colors, release]);
|
|
153
|
+
}, [colors, model?.basis, release]);
|
|
147
154
|
|
|
148
155
|
const contentHeight = Math.max(4, height - 1 - statGridRows(stats, width));
|
|
149
156
|
const key = (event: MacroDayEvent) => `${event.kind}:${event.date}`;
|
|
@@ -162,7 +169,8 @@ export function MacroDayPane({ width, height, focused }: PaneProps) {
|
|
|
162
169
|
getItemKey={key} emptyStateTitle={surprise === "all" ? "No release days." : `No ${MACRO_SURPRISE_LABELS[surprise].toLowerCase()} prints.`} sortColumnId={null} sortDirection="desc" selectedTextOverridesCellColor
|
|
163
170
|
selection={{ kind: "id", selectedId: selectedRow, getId: key, onChange: (id) => setSelectedRow(id) }}
|
|
164
171
|
getExportMetadata={() => [["symbol", model.symbol], ["from", model.start ?? ""], ["to", model.asOf ?? ""],
|
|
165
|
-
["units", "
|
|
172
|
+
["units", model.basis === "yield" ? "percentage points of yield, close to close on the release day"
|
|
173
|
+
: "percent, close to close on the release day"]]}
|
|
166
174
|
renderCell={renderCell} />
|
|
167
175
|
</> : null}
|
|
168
176
|
</PaneStatusBody>}
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
} from "../../../api-client/market-movers";
|
|
7
7
|
import type { DataProvider } from "../../../types/data-provider";
|
|
8
8
|
import { logFailure } from "../../../utils/log-failure";
|
|
9
|
+
import { savedCopyMark } from "../../../data/saved-copies";
|
|
9
10
|
import {
|
|
10
11
|
CATEGORY_MAP,
|
|
11
12
|
moverRegularSession,
|
|
@@ -113,10 +114,12 @@ export async function loadMarketMoverTab(
|
|
|
113
114
|
const trending = await dependencies.fetchTrending(LIST_ROWS, fetchOptions);
|
|
114
115
|
const hydrated = await hydrateTrending(trending.data, provider);
|
|
115
116
|
return {
|
|
117
|
+
...savedCopyMark(trending),
|
|
116
118
|
tab,
|
|
117
119
|
...hydrated,
|
|
118
120
|
source: "gloom",
|
|
119
121
|
stale: trending.stale || !!hydrated.unavailableSymbols?.length,
|
|
122
|
+
...(trending.savedAt != null ? { savedAt: trending.savedAt } : {}),
|
|
120
123
|
};
|
|
121
124
|
}
|
|
122
125
|
const category = CATEGORY_MAP[tab];
|
|
@@ -15,6 +15,7 @@ import { EXTENDED_SESSION_LABELS, type ExtendedSession } from "../../../market-d
|
|
|
15
15
|
import type { HeadlessPaneColumn } from "../../../types/headless";
|
|
16
16
|
import { isSessionTab, resolveSide } from "./session";
|
|
17
17
|
import { hasPartialFloat, minFloatShares, normalizeMinFloat, PARTIAL_FLOAT_NOTE } from "./min-float";
|
|
18
|
+
import { savedCopyMark } from "../../../data/saved-copies";
|
|
18
19
|
|
|
19
20
|
const rowPrice = (value: unknown, row: Record<string, unknown>) => formatMoverPrice(typeof value === "number" ? value : null, typeof row.currency === "string" ? row.currency : "", moverReferencePrice(row as unknown as MarketMoverRow));
|
|
20
21
|
|
|
@@ -197,7 +198,8 @@ export function createMarketMoversHeadless(
|
|
|
197
198
|
rows,
|
|
198
199
|
...(quoteError ? { complete: false, unavailableSymbols: result.unavailableSymbols, errors: [quoteError] } : {}),
|
|
199
200
|
...(notes.length > 0 ? { notes } : {}),
|
|
200
|
-
|
|
201
|
+
// A list served from a saved copy ends with when it was saved.
|
|
202
|
+
metadata: { ...savedCopyMark(result), list: tab, source: result.source, stale: result.stale, rankedBy, minFloat: floatFiltered ? minFloat : null },
|
|
201
203
|
// A Pro account's Gainers, Losers and Most Active are the real-time lists.
|
|
202
204
|
...(result.source === "cloud" ? { freshness: { status: "live" as const } } : {}),
|
|
203
205
|
};
|
|
@@ -2,6 +2,8 @@ import { Box, useUiCapabilities } from "../../../ui";
|
|
|
2
2
|
import { nextHeaderSort } from "../../../utils/sort-values";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { DataTableView, EmptyState, QueryBar, usePaneFooter, usePaneTabs, type DataTableKeyEvent } from "../../../components";
|
|
5
|
+
import { savedCopyFooterInfo } from "../../../components/layout/pane/footer";
|
|
6
|
+
import { STALE_SEGMENT } from "../../../components/layout/pane/status-footer";
|
|
5
7
|
import { handleRefreshKey } from "../../../components/data-table/table-pane";
|
|
6
8
|
import type { PaneProps } from "../../../types/plugin";
|
|
7
9
|
import type { PluginModule } from "../plugin-module";
|
|
@@ -167,6 +169,8 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
|
|
|
167
169
|
const [selectedSymbol, setSelectedSymbol] = useState<string | null>(null);
|
|
168
170
|
const [sortPreference, setSortPreference] = useState<MarketMoverSortPreference>(DEFAULT_SORT_PREFERENCE);
|
|
169
171
|
const [moversStale, setMoversStale] = useState(false);
|
|
172
|
+
// The list's refresh failed and a copy saved at this time (ms) answered instead.
|
|
173
|
+
const [moversSavedAt, setMoversSavedAt] = useState<number | null>(null);
|
|
170
174
|
const [lastSuccess, setLastSuccess] = useState<{ key: string; at: number } | null>(null);
|
|
171
175
|
|
|
172
176
|
const fetchGenRef = useRef(0);
|
|
@@ -241,11 +245,13 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
|
|
|
241
245
|
if (!options?.background) setSelectedSymbol(null);
|
|
242
246
|
}
|
|
243
247
|
setMoversStale(result.stale);
|
|
248
|
+
setMoversSavedAt(result.savedAt ?? null);
|
|
244
249
|
setLoadError(quoteError);
|
|
245
250
|
if (!quoteError && !result.stale) setLastSuccess({ key: `${tab}:${minFloat ?? ""}`, at: Date.now() });
|
|
246
251
|
} catch {
|
|
247
252
|
if (fetchGenRef.current !== gen) return;
|
|
248
253
|
setMoversStale(true);
|
|
254
|
+
setMoversSavedAt(null);
|
|
249
255
|
setLoadError("Market movers temporarily unavailable");
|
|
250
256
|
}
|
|
251
257
|
finally {
|
|
@@ -288,14 +294,13 @@ function ScreenerMoversBody({ activeTab, focused, width, summaryQuotes, liveStre
|
|
|
288
294
|
id: "feed",
|
|
289
295
|
parts: [{ text: feedStatus, tone: feedStatus === "live" ? "value" as const : "muted" as const }],
|
|
290
296
|
}] : []),
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}] : []),
|
|
297
|
+
// A saved copy answering a failed refresh says when it was saved; other stale rows say only that.
|
|
298
|
+
...(loadedKey === listKey ? savedCopyFooterInfo([moversSavedAt]) : []),
|
|
299
|
+
...(moversStale && moversSavedAt == null && loadedKey === listKey ? [STALE_SEGMENT] : []),
|
|
295
300
|
// The FLOAT column shows while a minimum is set; a float the source does not date says so once.
|
|
296
301
|
...partialFloatFooter(columns.some((column) => column.id === "floatShares"), rows),
|
|
297
302
|
],
|
|
298
|
-
}), [columns, feedStatus, listKey, loadedKey, loadError, loading, moversStale, rows, summaryQuotes]);
|
|
303
|
+
}), [columns, feedStatus, listKey, loadedKey, loadError, loading, moversStale, moversSavedAt, rows, summaryQuotes]);
|
|
299
304
|
|
|
300
305
|
return (
|
|
301
306
|
<DataTableView<MarketMoverRow, MarketMoverColumn>
|
|
@@ -2,8 +2,10 @@ import type { ScreenerQuote } from "../../../api-client/market-discovery";
|
|
|
2
2
|
export type { ScreenerQuote } from "../../../api-client/market-discovery";
|
|
3
3
|
import { resolveCurrencyUnit } from "../../../utils/currency-units";
|
|
4
4
|
import { logFailure } from "../../../utils/log-failure";
|
|
5
|
+
import { savedCopyMark, servedFromSavedCopy } from "../../../data/saved-copies";
|
|
5
6
|
import type { PluginPersistence } from "../../../types/plugin";
|
|
6
7
|
import { apiClient } from "../../../api-client";
|
|
8
|
+
import { isTransientFailure, sourceDidNotAnswer } from "../../../api-client/errors";
|
|
7
9
|
import type {
|
|
8
10
|
CloudMarketResponse,
|
|
9
11
|
CloudMarketScreenerCategory,
|
|
@@ -68,7 +70,7 @@ function shouldUseCache(api: MarketScreenerApi, options?: FetchCacheOptions): bo
|
|
|
68
70
|
return options?.cache === true || (options?.cache !== false && api === screenerApi);
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
function readCache<T>(key: string, options?: { allowExpired?: boolean }): { data: T; stale: boolean } | null {
|
|
73
|
+
function readCache<T>(key: string, options?: { allowExpired?: boolean }): { data: T; stale: boolean; fetchedAt: number } | null {
|
|
72
74
|
const record = marketMoversPersistence?.getResource<CachedResult<T>>(CACHE_KIND, key, {
|
|
73
75
|
sourceKey: CACHE_SOURCE,
|
|
74
76
|
schemaVersion: CACHE_SCHEMA_VERSION,
|
|
@@ -79,6 +81,7 @@ function readCache<T>(key: string, options?: { allowExpired?: boolean }): { data
|
|
|
79
81
|
return {
|
|
80
82
|
data: record.value.data,
|
|
81
83
|
stale: record.value.stale || !!record.stale || failedFetches.has(key),
|
|
84
|
+
fetchedAt: record.fetchedAt,
|
|
82
85
|
};
|
|
83
86
|
}
|
|
84
87
|
|
|
@@ -94,6 +97,8 @@ function writeCache<T>(key: string, data: T): void {
|
|
|
94
97
|
export interface CachedResult<T> {
|
|
95
98
|
data: T;
|
|
96
99
|
stale: boolean;
|
|
100
|
+
/** Its refresh failed and the copy saved at this time (ms) answered instead. */
|
|
101
|
+
savedAt?: number;
|
|
97
102
|
}
|
|
98
103
|
|
|
99
104
|
async function loadCached<T>(
|
|
@@ -117,10 +122,13 @@ async function loadCached<T>(
|
|
|
117
122
|
return result;
|
|
118
123
|
})
|
|
119
124
|
.catch((error) => {
|
|
120
|
-
//
|
|
121
|
-
if (fallback)
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
// The next read asks again rather than read the saved copy as current.
|
|
126
|
+
if (fallback) failedFetches.add(key);
|
|
127
|
+
// Serving the expired copy is right for a failure asking again may get past; hiding that it is
|
|
128
|
+
// expired is not. A command that shows it ends with when the copy was saved. A refusal or a
|
|
129
|
+
// reply that fails validation is the error, saved copy or not.
|
|
130
|
+
if (fallback && isTransientFailure(error)) {
|
|
131
|
+
return servedFromSavedCopy({ data: fallback.data, stale: true, savedAt: fallback.fetchedAt }, fallback.fetchedAt);
|
|
124
132
|
}
|
|
125
133
|
throw error;
|
|
126
134
|
})
|
|
@@ -143,6 +151,8 @@ export interface MarketMoversResult {
|
|
|
143
151
|
quotes: ScreenerQuote[];
|
|
144
152
|
source: MarketMoversDataSource;
|
|
145
153
|
stale: boolean;
|
|
154
|
+
/** Its refresh failed and the list saved at this time (ms) answered instead. */
|
|
155
|
+
savedAt?: number;
|
|
146
156
|
}
|
|
147
157
|
|
|
148
158
|
export interface PreferredMarketMoverSources {
|
|
@@ -265,6 +275,8 @@ export async function fetchScreenerResult(
|
|
|
265
275
|
const session = category === "most_actives" ? undefined : options?.session;
|
|
266
276
|
const load = async () => {
|
|
267
277
|
const response = await api.getMarketMovers(category, count, options?.forceRefresh, session);
|
|
278
|
+
// The service's own source did not answer this time: a saved list answers, as it does a 5xx.
|
|
279
|
+
if (!response.data && response.status === "retryable_error") throw sourceDidNotAnswer("Market movers temporarily unavailable");
|
|
268
280
|
if (!response.data) throw new Error("Invalid market movers response");
|
|
269
281
|
return { data: parseScreenerResponse(response.data), stale: response.stale === true || response.data.stale === true };
|
|
270
282
|
};
|
|
@@ -389,7 +401,13 @@ export async function fetchPreferredMarketMovers(
|
|
|
389
401
|
const eligibility = sources.isCloudEligible();
|
|
390
402
|
if (!(typeof eligibility === "boolean" ? eligibility : await eligibility)) {
|
|
391
403
|
const result = await sources.fetchMarket(category, count, options);
|
|
392
|
-
return {
|
|
404
|
+
return {
|
|
405
|
+
...savedCopyMark(result),
|
|
406
|
+
quotes: rankScreenerQuotes(category, result.data),
|
|
407
|
+
source: "gloom",
|
|
408
|
+
stale: result.stale,
|
|
409
|
+
...(result.savedAt != null ? { savedAt: result.savedAt } : {}),
|
|
410
|
+
};
|
|
393
411
|
}
|
|
394
412
|
|
|
395
413
|
const marketMetadata = sources.fetchMarket(
|
|
@@ -425,9 +443,11 @@ export async function fetchPreferredMarketMovers(
|
|
|
425
443
|
|
|
426
444
|
const fallback = await marketMetadata;
|
|
427
445
|
return {
|
|
446
|
+
...savedCopyMark(fallback),
|
|
428
447
|
quotes: rankScreenerQuotes(category, fallback.data),
|
|
429
448
|
source: "gloom",
|
|
430
449
|
stale: fallback.stale,
|
|
450
|
+
...(fallback.savedAt != null ? { savedAt: fallback.savedAt } : {}),
|
|
431
451
|
};
|
|
432
452
|
}
|
|
433
453
|
|
|
@@ -444,6 +464,7 @@ export async function fetchTrendingResult(
|
|
|
444
464
|
const load = async () => {
|
|
445
465
|
const response = await (api.getMarketTrending ?? apiClient.getMarketTrending)(count);
|
|
446
466
|
if (response.status === "empty") return { data: [], stale: response.stale === true };
|
|
467
|
+
if (!response.data && response.status === "retryable_error") throw sourceDidNotAnswer("Market trends temporarily unavailable");
|
|
447
468
|
if (!response.data) throw new Error("Market trends unavailable");
|
|
448
469
|
return { data: parseTrendingResponse(response.data), stale: response.stale === true };
|
|
449
470
|
};
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
} from "../../../api-client/market-movers";
|
|
7
7
|
import { usePlanAccess } from "../../../api-client/plan-access";
|
|
8
8
|
import { DataTableView, EmptyState, QueryBar, usePaneFooter, type DataTableKeyEvent } from "../../../components";
|
|
9
|
+
import { STALE_SEGMENT } from "../../../components/layout/pane/status-footer";
|
|
9
10
|
import { handleRefreshKey } from "../../../components/data-table/table-pane";
|
|
10
11
|
import {
|
|
11
12
|
buildScreenerQuoteTargets,
|
|
@@ -201,7 +202,7 @@ function SessionMoversTable({ view, session, focused, width, summaryQuotes, live
|
|
|
201
202
|
id: "feed",
|
|
202
203
|
parts: [{ text: feedStatus, tone: feedStatus === "live" ? "value" as const : "muted" as const }],
|
|
203
204
|
}] : []),
|
|
204
|
-
...(payload?.stale ? [
|
|
205
|
+
...(payload?.stale ? [STALE_SEGMENT] : []),
|
|
205
206
|
// A float the source does not date says so once, while the FLOAT column shows.
|
|
206
207
|
...partialFloatFooter(columns.some((column) => column.id === "floatShares"), rows),
|
|
207
208
|
],
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { MnaDealsParams, MnaRegionFilter, MnaStatusFilter, MnaTargetFilter } from "../../../api-client/mna";
|
|
2
2
|
import type { HeadlessPaneColumn, HeadlessPaneDefinition } from "../../../types/plugin";
|
|
3
3
|
import { fetchMnaDeals } from "./client";
|
|
4
|
+
import { companyDataNotes } from "../../../types/company-from";
|
|
4
5
|
import { dealsSymbol, formatDealValue, formatExpectedClose, stageLabel, termsLabel } from "./model";
|
|
5
6
|
import { REPORT_ROWS } from "../shared/headless-options";
|
|
6
7
|
|
|
@@ -92,6 +93,7 @@ export const mnaHeadless: HeadlessPaneDefinition<"rows"> = {
|
|
|
92
93
|
headline: deal.headline,
|
|
93
94
|
})),
|
|
94
95
|
errors: payload.lockedDeals > 0 ? [`${payload.lockedDeals} deals from the last ${payload.delayDays} days need Gloom Pro`] : [],
|
|
96
|
+
...(payload.companyFrom ? { notes: companyDataNotes(payload.companyFrom) } : {}),
|
|
95
97
|
metadata: {
|
|
96
98
|
asOf: payload.asOf, access: payload.access, delayDays: payload.delayDays, hasMore: payload.hasMore,
|
|
97
99
|
},
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
type PaneHint,
|
|
13
13
|
type SelectControl,
|
|
14
14
|
} from "../../../components";
|
|
15
|
-
import { usePaneStatusFooter } from "../../../components/layout/pane/status-footer";
|
|
15
|
+
import { useCompanyDataFooter, usePaneStatusFooter } from "../../../components/layout/pane/status-footer";
|
|
16
16
|
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
17
17
|
import { isAccessDenied } from "../../../api-client/errors";
|
|
18
18
|
import type {
|
|
@@ -337,6 +337,8 @@ function MnaDealsView({ focused, width, height, symbol }: {
|
|
|
337
337
|
hints,
|
|
338
338
|
});
|
|
339
339
|
usePaneFooter("mna:access", () => (access.segment && !openId ? { order: 5, info: [access.segment] } : null), [access.segment, openId]);
|
|
340
|
+
// A receipt's or foreign line's deals include its company's, read from the home line.
|
|
341
|
+
useCompanyDataFooter(data?.companyFrom, !openId);
|
|
340
342
|
|
|
341
343
|
const placeholder = blendHex(colors.bg, colors.textMuted, 0.35);
|
|
342
344
|
const renderCell = useCallback((item: Item, column: DataTableColumn, _index: number, state: { selected: boolean }): DataTableCell => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MoneyMarketObservation } from "../../../api-client/money-markets";
|
|
2
2
|
import type { SeriesDefinition } from "../../../series/types";
|
|
3
|
-
import {
|
|
3
|
+
import { cachedSeriesNote } from "../shared/series-warning";
|
|
4
4
|
import { loadMoneyMarkets } from "./client";
|
|
5
5
|
|
|
6
6
|
function observations(history: readonly MoneyMarketObservation[]) {
|
|
@@ -21,8 +21,7 @@ export const MONEY_MARKET_SERIES: readonly SeriesDefinition[] = [
|
|
|
21
21
|
tags: ["liquidity", "Fed balance sheet", "TGA", "RRP", "money markets"],
|
|
22
22
|
load: async () => {
|
|
23
23
|
const resource = await loadMoneyMarkets();
|
|
24
|
-
|
|
25
|
-
return { points: observations(resource.payload.netLiquidity.history), ...(warning ? { warning } : {}) };
|
|
24
|
+
return { points: observations(resource.payload.netLiquidity.history), ...cachedSeriesNote(resource) };
|
|
26
25
|
},
|
|
27
26
|
},
|
|
28
27
|
{
|
|
@@ -34,8 +33,7 @@ export const MONEY_MARKET_SERIES: readonly SeriesDefinition[] = [
|
|
|
34
33
|
tags: ["bills", "curve", "money markets"],
|
|
35
34
|
load: async () => {
|
|
36
35
|
const resource = await loadMoneyMarkets();
|
|
37
|
-
|
|
38
|
-
return { points: observations(resource.payload.billsCurve.slope.history), ...(warning ? { warning } : {}) };
|
|
36
|
+
return { points: observations(resource.payload.billsCurve.slope.history), ...cachedSeriesNote(resource) };
|
|
39
37
|
},
|
|
40
38
|
},
|
|
41
39
|
];
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type CloudNewsRead,
|
|
13
13
|
} from "../../../sources/gloomberb-cloud/news";
|
|
14
14
|
import { parsePublicTickerKey } from "../../../utils/exchanges";
|
|
15
|
+
import { companyDataNotes, readCompanyFrom } from "../../../types/company-from";
|
|
15
16
|
import { formatUtcSeconds } from "../../../utils/utc-time";
|
|
16
17
|
import { cloudNewsFreshness, cloudRealtimeAccess } from "../shared/report-freshness";
|
|
17
18
|
import { REPORT_FILTER, REPORT_ROWS } from "../shared/headless-options";
|
|
@@ -95,7 +96,8 @@ function projectNewsHeadless(
|
|
|
95
96
|
// A coin pair no story is filed under shows the stories that mention the coin, and says so.
|
|
96
97
|
...(response.searchedFor && args.symbols[0]
|
|
97
98
|
? { notes: [coinNewsFallbackNotice(parsePublicTickerKey(args.symbols[0]).symbol, response.searchedFor)] }
|
|
98
|
-
|
|
99
|
+
// A receipt's or foreign line's stories are its company's, read from the home line.
|
|
100
|
+
: response.companyFrom ? { notes: companyDataNotes(readCompanyFrom(response.companyFrom)) } : {}),
|
|
99
101
|
metadata: {
|
|
100
102
|
nextCursor: response.nextCursor,
|
|
101
103
|
sentiment,
|
|
@@ -6,6 +6,7 @@ import { instrumentFromTicker } from "../../../market-data/request-types";
|
|
|
6
6
|
import { useCallback } from "react";
|
|
7
7
|
import { useDebouncedPluginPaneState, usePluginPaneState } from "../../runtime";
|
|
8
8
|
import { EmptyState, PaneStatusBody, QueryBar, usePaneNoticeFooter } from "../../../components";
|
|
9
|
+
import { useCompanyDataFooter } from "../../../components/layout/pane/status-footer";
|
|
9
10
|
import { coinNewsFallbackNotice } from "../../../sources/gloomberb-cloud/news";
|
|
10
11
|
import { newsMuteSettingsDef, newsMutesApplyToFeed, useNewsMuteFilter } from "./wire/mutes";
|
|
11
12
|
import { newsListEmptyCopy } from "./wire/filter-articles";
|
|
@@ -114,6 +115,8 @@ function TickerNewsView({ width, height, focused }: { width: number; height: num
|
|
|
114
115
|
onPopOut: readableArticle ? popOutReadable : undefined,
|
|
115
116
|
feedAccess: newsState.access,
|
|
116
117
|
});
|
|
118
|
+
// A receipt's or foreign line's stories are its company's, read from the home line.
|
|
119
|
+
useCompanyDataFooter(newsState.companyFrom, !detailOpen);
|
|
117
120
|
|
|
118
121
|
if (!ticker) {
|
|
119
122
|
return (
|