@gloom-sh/terminal 0.19.0 → 0.19.1
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 +1 -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/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/architecture/core-builtin-allowlist.json +2 -2
- package/src/cli/commands/market.ts +35 -12
- package/src/cli/commands/news-search.ts +22 -0
- package/src/cli/commands/overview.ts +86 -20
- package/src/cli/commands/ticker.ts +32 -2
- package/src/cli/errors.ts +17 -2
- package/src/cli/glossary.ts +6 -0
- package/src/cli/index-roots.ts +0 -7
- package/src/cli/listing-arg.ts +57 -10
- 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 +1 -1
- 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 +6 -7
- package/src/components/command-bar/selection-runtime.ts +52 -3
- package/src/components/command-bar/surface/index.tsx +2 -0
- package/src/components/layout/pane/capture-stamp.ts +2 -2
- package/src/components/layout/pane/footer/index.tsx +22 -6
- package/src/components/layout/pane/footer/model.ts +83 -15
- package/src/components/layout/pane/status-footer.ts +35 -2
- package/src/components/layout/status-widget-space.ts +9 -0
- package/src/components/ticker/list-table-view.tsx +4 -20
- package/src/components/ui/display.tsx +18 -3
- package/src/components/ui/icon.tsx +4 -1
- package/src/data/plugin-cache.ts +5 -3
- package/src/data/saved-copies.ts +8 -0
- 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/news/aggregator.ts +7 -1
- package/src/news/types.ts +6 -0
- 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/chart-composer/follow-binding.ts +14 -2
- package/src/plugins/builtin/chart-composer/index.tsx +4 -5
- 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/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/credit-documents/headless.ts +3 -3
- package/src/plugins/builtin/doe/headless.ts +3 -2
- package/src/plugins/builtin/doe/pane.tsx +13 -3
- package/src/plugins/builtin/earnings-calls/data.ts +18 -6
- package/src/plugins/builtin/earnings-calls/headless.ts +13 -3
- package/src/plugins/builtin/earnings-calls/pane.tsx +7 -0
- 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 +2 -2
- 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/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 +3 -0
- 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/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 +2 -0
- package/src/plugins/builtin/market-movers/headless.ts +3 -1
- package/src/plugins/builtin/market-movers/screener.ts +8 -4
- package/src/plugins/builtin/mna/headless.ts +2 -0
- package/src/plugins/builtin/mna/pane.tsx +3 -1
- 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/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 +4 -2
- package/src/plugins/builtin/risk-factors/headless.ts +5 -2
- 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/sec/headless.ts +2 -1
- package/src/plugins/builtin/sec/index.tsx +3 -0
- package/src/plugins/builtin/shared/cloud-resource.ts +13 -5
- package/src/plugins/builtin/shared/headless-market-data.ts +2 -2
- 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/thirteenf/api.ts +12 -1
- package/src/plugins/builtin/thirteenf/data.ts +7 -7
- package/src/plugins/builtin/thirteenf/headless.ts +31 -4
- 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/headless.ts +21 -12
- package/src/plugins/builtin/treasury-auctions/pane.tsx +9 -3
- package/src/plugins/builtin/volatility/pane.tsx +5 -2
- package/src/plugins/builtin/yield-curve/client.ts +61 -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/portfolio/column-values.ts +11 -7
- package/src/renderers/dom/desktop/icons.tsx +7 -0
- package/src/renderers/electrobun/bun/desktop/backend.ts +9 -10
- package/src/renderers/electrobun/bun/desktop/host-requests.ts +8 -0
- 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/create.ts +43 -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/shared/protocol.ts +2 -0
- package/src/renderers/electrobun/view/news-source.ts +2 -0
- package/src/renderers/electrobun/view/ui-host.tsx +8 -0
- package/src/sources/gloomberb-cloud/fred-series.ts +3 -1
- package/src/sources/gloomberb-cloud/index.ts +42 -8
- package/src/tickers/company-name-listing.ts +53 -13
- package/src/tickers/company-name.ts +6 -1
- package/src/tickers/index-roots.ts +7 -0
- package/src/tickers/listing-fate.ts +2 -2
- package/src/tickers/search/index.ts +6 -2
- 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/types/company-from.ts +40 -0
- package/src/types/data-provider.ts +7 -0
- package/src/types/financials.ts +27 -0
- package/src/types/headless.ts +6 -0
- package/src/utils/crypto-pair.ts +121 -7
- package/src/utils/declared-dividends.ts +1 -1
- package/src/utils/fundamentals.ts +36 -5
- package/src/version.ts +2 -2
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { apiClient } from "../../../api-client";
|
|
2
|
+
import type { IpoDeal } from "../../../api-client/ipo";
|
|
3
|
+
import type { HeadlessPaneApiClient } from "../../../types/plugin";
|
|
4
|
+
import { canonicalExchange } from "../../../utils/exchanges";
|
|
5
|
+
import { fetchIpoCalendar } from "./client";
|
|
6
|
+
import {
|
|
7
|
+
formatIpoAmount,
|
|
8
|
+
formatIpoPrice,
|
|
9
|
+
formatIpoReturn,
|
|
10
|
+
ipoLockupBasis,
|
|
11
|
+
ipoLockupText,
|
|
12
|
+
ipoReturnSince,
|
|
13
|
+
ipoTickerKey,
|
|
14
|
+
isLockupStory,
|
|
15
|
+
} from "./model";
|
|
16
|
+
|
|
17
|
+
/** How far back a listing's IPO is looked for: the calendar keeps a year, and a lock-up runs about half of one. */
|
|
18
|
+
const IPO_LOOKBACK_DAYS = 400;
|
|
19
|
+
const LOCKUP_NEWS_LIMIT = 3;
|
|
20
|
+
|
|
21
|
+
/** A lock-up story filed under a listing: its headline, publisher and day. */
|
|
22
|
+
export interface IpoLockupStory {
|
|
23
|
+
title: string;
|
|
24
|
+
source: string;
|
|
25
|
+
publishedAt: string;
|
|
26
|
+
url: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The stories filed under a listing about its lock-up since it listed (an early release, a waiver), newest first. */
|
|
30
|
+
export async function loadIpoLockupNews(
|
|
31
|
+
client: Pick<HeadlessPaneApiClient, "searchCloudNews">,
|
|
32
|
+
deal: IpoDeal,
|
|
33
|
+
): Promise<IpoLockupStory[]> {
|
|
34
|
+
const key = ipoTickerKey(deal);
|
|
35
|
+
if (!key || !deal.lockup) return [];
|
|
36
|
+
const response = await client.searchCloudNews("lock-up", 20, undefined, { tickers: [key] });
|
|
37
|
+
const since = deal.listingDate ?? deal.firstDay?.session ?? "";
|
|
38
|
+
return response.items
|
|
39
|
+
.map((item) => ({
|
|
40
|
+
title: item.headline,
|
|
41
|
+
source: item.primaryPublisher || item.primarySource,
|
|
42
|
+
publishedAt: item.lastPublishedAt || item.firstPublishedAt,
|
|
43
|
+
url: item.primaryUrl,
|
|
44
|
+
}))
|
|
45
|
+
.filter((story) => isLockupStory(story.title) && story.publishedAt.slice(0, 10) >= since)
|
|
46
|
+
.slice(0, LOCKUP_NEWS_LIMIT);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function shiftDays(day: string, days: number): string {
|
|
50
|
+
return new Date(Date.parse(`${day}T00:00:00Z`) + days * 86_400_000).toISOString().slice(0, 10);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The IPO a listing came to market with in the last year or so, from the
|
|
55
|
+
* IPO calendar: the deal listed under its symbol on its exchange. Null for a
|
|
56
|
+
* listing older than that, one the calendar does not have, or one whose
|
|
57
|
+
* symbol two deals on its exchange share.
|
|
58
|
+
*/
|
|
59
|
+
export async function loadListingIpo(
|
|
60
|
+
client: Pick<HeadlessPaneApiClient, "getCloudIpoCalendar">,
|
|
61
|
+
listing: { symbol: string; exchange?: string | null },
|
|
62
|
+
{ signal, now = new Date() }: { signal?: AbortSignal; now?: Date } = {},
|
|
63
|
+
): Promise<IpoDeal | null> {
|
|
64
|
+
const symbol = listing.symbol.trim().toUpperCase();
|
|
65
|
+
if (!symbol) return null;
|
|
66
|
+
const today = now.toISOString().slice(0, 10);
|
|
67
|
+
const payload = await fetchIpoCalendar(client, signal, { q: symbol, from: shiftDays(today, -IPO_LOOKBACK_DAYS), to: today });
|
|
68
|
+
const exchange = listing.exchange ? canonicalExchange(listing.exchange) : "";
|
|
69
|
+
const deals = payload.deals.filter((deal) => (deal.status === "listed" || deal.status === "priced")
|
|
70
|
+
&& deal.symbol?.toUpperCase() === symbol
|
|
71
|
+
&& (!exchange || (deal.exchange != null && canonicalExchange(deal.exchange) === exchange)));
|
|
72
|
+
return deals.length === 1 ? deals[0]! : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A deal's IPO in rows of a report, label and text:
|
|
77
|
+
* - IPO date 2026-05-07 on NYSE
|
|
78
|
+
* - Offer $26 (or "direct listing, no offer price")
|
|
79
|
+
* - Since IPO +62.7% ($26 to $42.30)
|
|
80
|
+
* - Lockup lockup ends ~2026-11-02 (estimate)
|
|
81
|
+
*/
|
|
82
|
+
export function ipoMetricRows(deal: IpoDeal, price: number | null | undefined, now = new Date()): Array<[string, string]> {
|
|
83
|
+
const day = deal.listingDate ?? deal.firstDay?.session ?? null;
|
|
84
|
+
const rows: Array<[string, string]> = [];
|
|
85
|
+
if (day) rows.push(["IPO date", `${day}${deal.venue ? ` on ${deal.venue}` : ""}`]);
|
|
86
|
+
rows.push(["Offer", formatIpoPrice(deal, { full: true })]);
|
|
87
|
+
const since = ipoReturnSince(deal, price);
|
|
88
|
+
if (since) {
|
|
89
|
+
const from = since.basis === "offer" ? formatIpoAmount(deal, since.from) : `first trade ${formatIpoAmount(deal, since.from)}`;
|
|
90
|
+
rows.push(["Since IPO", `${formatIpoReturn(since.returnPct)} (${from} to ${formatIpoAmount(deal, price!)})`]);
|
|
91
|
+
}
|
|
92
|
+
const lockup = ipoLockupText(deal, now);
|
|
93
|
+
if (lockup) rows.push(["Lockup", lockup.replace(/^lockup /, "")]);
|
|
94
|
+
return rows;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The lines under those rows: what the lock-up estimate counts from, then the lock-up stories. */
|
|
98
|
+
export function ipoDetailNotes(deal: IpoDeal, news: readonly IpoLockupStory[]): string[] {
|
|
99
|
+
const basis = ipoLockupBasis(deal);
|
|
100
|
+
return [
|
|
101
|
+
...basis ? [`Lockup estimate: ${basis}; underwriters can release shares early.`] : [],
|
|
102
|
+
...news.map((story) => `Lock-up news ${story.publishedAt.slice(0, 10)}: ${story.title} (${story.source})`),
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A deal's IPO in sentences, for a report's notes:
|
|
108
|
+
* - "HAWK listed 2026-05-07 on NYSE at a $26 offer price; $42.30 now, +62.7% since the IPO."
|
|
109
|
+
* - "GYGY listed 2026-07-30 on Nasdaq: direct listing, no offer price; $30.10 now, +33.8% since the first trade at $22.50."
|
|
110
|
+
* - "HAWK lockup ends ~2026-11-02 (estimate): 180 days from the prospectus dated 2026-05-06; underwriters can release shares early."
|
|
111
|
+
* - "Lock-up news 2026-08-31: HawkEye 360 Confirms Early Lock-up Release for September 2nd (PR Newswire)"
|
|
112
|
+
*/
|
|
113
|
+
export function ipoDetailSentences(deal: IpoDeal, price: number | null | undefined, news: readonly IpoLockupStory[], now = new Date()): string[] {
|
|
114
|
+
const name = deal.symbol ?? deal.company;
|
|
115
|
+
const day = deal.listingDate ?? deal.firstDay?.session ?? null;
|
|
116
|
+
const listed = deal.status === "listed" ? "listed" : "prices to list";
|
|
117
|
+
const where = `${day ? ` ${day}` : ""}${deal.venue ? ` on ${deal.venue}` : ""}`;
|
|
118
|
+
const offer = deal.offerPrice != null ? ` at a ${formatIpoAmount(deal, deal.offerPrice)} offer price`
|
|
119
|
+
: deal.listingType === "direct" ? `: ${formatIpoPrice(deal, { full: true })}` : "";
|
|
120
|
+
// The table's Return is the first day's; the sentence says which is which.
|
|
121
|
+
const firstDay = deal.firstDay?.returnPct != null && deal.offerPrice != null
|
|
122
|
+
? `, ${formatIpoReturn(deal.firstDay.returnPct)} on the first day` : "";
|
|
123
|
+
const since = ipoReturnSince(deal, price);
|
|
124
|
+
const priceNow = since ? `; ${formatIpoAmount(deal, price!)} now, ${formatIpoReturn(since.returnPct)} since ${
|
|
125
|
+
since.basis === "offer" ? "the IPO" : `the first trade at ${formatIpoAmount(deal, since.from)}`}` : "";
|
|
126
|
+
const lockup = ipoLockupText(deal, now);
|
|
127
|
+
const basis = ipoLockupBasis(deal);
|
|
128
|
+
return [
|
|
129
|
+
`${name} ${listed}${where}${offer}${firstDay}${priceNow}.`,
|
|
130
|
+
...lockup ? [`${name} ${lockup}${basis ? `: ${basis}` : ""}; underwriters can release shares early.`] : [],
|
|
131
|
+
...news.map((story) => `Lock-up news ${story.publishedAt.slice(0, 10)}: ${story.title} (${story.source})`),
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** A listing's recent IPO and its lock-up news, as `ticker` reads them beside the rest. */
|
|
136
|
+
export interface TickerIpo {
|
|
137
|
+
deal: IpoDeal;
|
|
138
|
+
news: IpoLockupStory[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** How long `ticker` waits for the IPO calendar before leaving the section out. */
|
|
142
|
+
const TICKER_IPO_WAIT_MS = 6_000;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* A stock's IPO of the last year or so with its lock-up news, for `ticker`;
|
|
146
|
+
* null for any other listing, and when the calendar does not answer in time.
|
|
147
|
+
*/
|
|
148
|
+
export async function loadTickerIpo(
|
|
149
|
+
listing: { symbol: string; exchange?: string | null },
|
|
150
|
+
client: Pick<HeadlessPaneApiClient, "getCloudIpoCalendar" | "searchCloudNews"> = apiClient,
|
|
151
|
+
): Promise<TickerIpo | null> {
|
|
152
|
+
const signal = AbortSignal.timeout(TICKER_IPO_WAIT_MS);
|
|
153
|
+
const deal = await loadListingIpo(client, listing, { signal });
|
|
154
|
+
if (!deal) return null;
|
|
155
|
+
const news = await loadIpoLockupNews(client, deal).catch(() => {
|
|
156
|
+
// Intentional ignore: the IPO's rows stand without its lock-up news.
|
|
157
|
+
return [];
|
|
158
|
+
});
|
|
159
|
+
return { deal, news };
|
|
160
|
+
}
|
|
@@ -161,16 +161,36 @@ const REGION_WORDS: Partial<Record<IpoRegion, readonly string[]>> = {
|
|
|
161
161
|
apac: ["apac", "asia", "asia-pacific"],
|
|
162
162
|
};
|
|
163
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Words that name a kind of deal rather than one: `lockup` (`lock-up`,
|
|
166
|
+
* `lock up`, `lockups`) lists the deals with a lock-up estimate, `spac` the
|
|
167
|
+
* SPACs' own IPOs, and `de-spac` (`despac`) those too: a SPAC's merger with
|
|
168
|
+
* its target is no IPO, so the calendar has none of its own.
|
|
169
|
+
*/
|
|
170
|
+
export type IpoKeyword = "lockup" | "spac" | "de-spac";
|
|
171
|
+
|
|
172
|
+
export function ipoKeyword(query: string): IpoKeyword | null {
|
|
173
|
+
const word = query.trim().toLowerCase().replace(/[\s_-]+/g, "");
|
|
174
|
+
if (/^lockups?$/.test(word)) return "lockup";
|
|
175
|
+
if (/^spacs?$/.test(word)) return "spac";
|
|
176
|
+
if (/^despacs?$/.test(word)) return "de-spac";
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
164
180
|
/**
|
|
165
181
|
* Company in either script, symbol, venue, country code or name, and the
|
|
166
182
|
* status the board shows. The word `pending` is no status of its own: it is
|
|
167
183
|
* every deal not priced yet, offering and overdue ones included.
|
|
168
184
|
* A region's name (`europe`, `asia`) lists the region's deals, on whichever tab is open.
|
|
185
|
+
* A kind of deal (`lockup`, `spac`, `de-spac`, see ipoKeyword) lists that kind.
|
|
169
186
|
*/
|
|
170
187
|
function matchesIpoQuery(deal: IpoDeal, query: string, now: Date): boolean {
|
|
171
188
|
const needle = query.trim().toLowerCase();
|
|
172
189
|
if (!needle) return true;
|
|
173
190
|
if (needle === "pending") return NOT_PRICED_STATUSES.includes(deal.status);
|
|
191
|
+
const keyword = ipoKeyword(needle);
|
|
192
|
+
if (keyword === "lockup") return !!deal.lockup;
|
|
193
|
+
if (keyword) return deal.listingType === "spac";
|
|
174
194
|
if (REGION_WORDS[deal.region]?.includes(needle.replace(/\s+/g, "-"))) return true;
|
|
175
195
|
return [
|
|
176
196
|
deal.company, deal.companyLocal, deal.symbol, deal.venue, deal.mic, deal.exchange,
|
|
@@ -343,9 +363,17 @@ function amount(value: number): string {
|
|
|
343
363
|
return value.toLocaleString("en-US", { minimumFractionDigits: whole ? 0 : 2, maximumFractionDigits: 2 });
|
|
344
364
|
}
|
|
345
365
|
|
|
346
|
-
/**
|
|
347
|
-
|
|
366
|
+
/** What a direct listing with no price reads in a column; a report's row says why in full. */
|
|
367
|
+
const DIRECT_LISTING_PRICE = "direct listing";
|
|
368
|
+
const DIRECT_LISTING_NO_PRICE = "direct listing, no offer price";
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* "$18", "HK$26.20-28.50", "240-260p", "CHF 45": the offer price once set, else the range.
|
|
372
|
+
* A direct listing sells no shares at a price: "direct listing" (`full`: "direct listing, no offer price").
|
|
373
|
+
*/
|
|
374
|
+
export function formatIpoPrice(deal: IpoDeal, { full = false }: { full?: boolean } = {}): string {
|
|
348
375
|
const low = deal.offerPrice ?? deal.priceLow ?? deal.priceHigh;
|
|
376
|
+
if (low == null && deal.listingType === "direct") return full ? DIRECT_LISTING_NO_PRICE : DIRECT_LISTING_PRICE;
|
|
349
377
|
if (low == null) return MISSING;
|
|
350
378
|
const high = deal.offerPrice == null && deal.priceLow != null && deal.priceHigh != null && deal.priceHigh !== deal.priceLow
|
|
351
379
|
? deal.priceHigh
|
|
@@ -387,3 +415,71 @@ export function formatIpoDate(value: string | null, now = new Date()): string {
|
|
|
387
415
|
? `${month} ${date.getUTCDate()}`
|
|
388
416
|
: `${month} ${date.getUTCFullYear()}`;
|
|
389
417
|
}
|
|
418
|
+
|
|
419
|
+
/** "~2026-11-02": when a deal's lock-up is estimated to end; "—" without an estimate. */
|
|
420
|
+
export function formatIpoLockupEnd(endsOn: string | null | undefined): string {
|
|
421
|
+
return endsOn ? `~${endsOn}` : MISSING;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** Today in UTC, YYYY-MM-DD. */
|
|
425
|
+
function utcToday(now: Date): string {
|
|
426
|
+
return now.toISOString().slice(0, 10);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* "lockup ends ~2026-11-02 (estimate)", "lockup ended ~2026-07-28 (estimate)":
|
|
431
|
+
* a deal's lock-up estimate in words; null without one.
|
|
432
|
+
*/
|
|
433
|
+
export function ipoLockupText(deal: Pick<IpoDeal, "lockup">, now = new Date()): string | null {
|
|
434
|
+
if (!deal.lockup) return null;
|
|
435
|
+
const verb = deal.lockup.endsOn < utcToday(now) ? "ended" : "ends";
|
|
436
|
+
return `lockup ${verb} ~${deal.lockup.endsOn} (estimate)`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* What the estimate counts from, as the server made it. The period is the
|
|
441
|
+
* prospectus's own (`prospectus`) or the typical 180 days; it counts from
|
|
442
|
+
* the prospectus date when one was read, else from the first session:
|
|
443
|
+
* - "180 days from the prospectus dated 2026-05-06"
|
|
444
|
+
* - "90 days from the first session on 2026-05-07, the period the prospectus states"
|
|
445
|
+
* - "the typical 180 days from the prospectus dated 2026-05-06"
|
|
446
|
+
* - "the typical 180 days from the first session on 2026-05-07; the prospectus has not been read"
|
|
447
|
+
*/
|
|
448
|
+
export function ipoLockupBasis(deal: Pick<IpoDeal, "lockup" | "listingDate">): string | null {
|
|
449
|
+
const lockup = deal.lockup;
|
|
450
|
+
if (!lockup) return null;
|
|
451
|
+
const stated = lockup.basis === "prospectus";
|
|
452
|
+
const from = lockup.prospectusDate
|
|
453
|
+
? `from the prospectus dated ${lockup.prospectusDate}`
|
|
454
|
+
: `from the first session${deal.listingDate ? ` on ${deal.listingDate}` : ""}`;
|
|
455
|
+
if (stated) return `${lockup.days} days ${from}${lockup.prospectusDate ? "" : ", the period the prospectus states"}`;
|
|
456
|
+
// A prospectus read with no period it states (or none of a lock-up's length) is still read.
|
|
457
|
+
const read = lockup.prospectusDate != null || lockup.prospectusUrl != null;
|
|
458
|
+
return `the typical ${lockup.days} days ${from}${read ? "" : "; the prospectus has not been read"}`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** A price in the deal's own currency: "$26", "HK$7.20". */
|
|
462
|
+
export function formatIpoAmount(deal: Pick<IpoDeal, "currency">, value: number): string {
|
|
463
|
+
return formatIpoPrice({ offerPrice: value, priceLow: null, priceHigh: null, currency: deal.currency, listingType: "ipo" } as IpoDeal);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* What a listed deal's price has done since it listed: from the offer price,
|
|
468
|
+
* else (a direct listing) from its first trade. Null without a price now or a
|
|
469
|
+
* price to measure from.
|
|
470
|
+
*/
|
|
471
|
+
export function ipoReturnSince(deal: IpoDeal, price: number | null | undefined): { from: number; basis: "offer" | "first trade"; returnPct: number } | null {
|
|
472
|
+
if (price == null || !Number.isFinite(price) || price <= 0) return null;
|
|
473
|
+
const offer = deal.offerPrice;
|
|
474
|
+
if (offer != null && offer > 0) return { from: offer, basis: "offer", returnPct: price / offer - 1 };
|
|
475
|
+
const open = deal.firstDay?.open;
|
|
476
|
+
if (deal.listingType === "direct" && open != null && open > 0) return { from: open, basis: "first trade", returnPct: price / open - 1 };
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** The lock-up stories worth listing beside a deal: on or after its listing, about its lock-up. */
|
|
481
|
+
const LOCKUP_WORDS = /\block[\s-]?ups?\b/i;
|
|
482
|
+
|
|
483
|
+
export function isLockupStory(title: string): boolean {
|
|
484
|
+
return LOCKUP_WORDS.test(title);
|
|
485
|
+
}
|
|
@@ -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,6 +114,7 @@ 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",
|
|
@@ -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
|
};
|