gloomberb 0.17.2 → 0.17.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api-client/futures-curve.ts +16 -0
- package/src/api-client/index.ts +18 -8
- package/src/api-client/types.ts +3 -1
- package/src/cli/commands/market.ts +60 -9
- package/src/cli/commands/search.ts +43 -2
- package/src/cli/commands/ticker.ts +100 -21
- package/src/cli/desktop-pane-shot.ts +34 -9
- package/src/cli/fx-pair.ts +17 -0
- package/src/cli/glossary.ts +134 -6
- package/src/cli/history-rows.ts +16 -4
- package/src/cli/index.ts +44 -12
- package/src/cli/known-gaps.ts +21 -2
- package/src/cli/listing-arg.ts +19 -4
- package/src/cli/listing-suggestion.ts +33 -2
- package/src/cli/pane-functions/dom.ts +39 -6
- package/src/cli/pane-functions/freshness.ts +48 -5
- package/src/cli/pane-functions/function-help.ts +1 -1
- package/src/cli/pane-functions/headless.ts +85 -19
- package/src/cli/pane-functions/index.ts +60 -4
- package/src/cli/pane-functions/market-session.ts +69 -6
- package/src/cli/pane-functions/resolver.ts +4 -2
- package/src/cli/pane-functions/screenshot.ts +14 -1
- package/src/cli/scroll-truncation.ts +5 -1
- package/src/cli/symbol-args.ts +41 -3
- package/src/cli/symbol-list.ts +19 -7
- package/src/cli/unknown-command.ts +70 -0
- package/src/components/chart/native/surface/visibility.ts +37 -7
- package/src/components/layout/header.tsx +348 -102
- package/src/components/markdown-text.tsx +40 -66
- package/src/components/market-board/index.tsx +12 -6
- package/src/components/reader/detail-reader.tsx +28 -6
- package/src/components/ticker/badge/index.tsx +2 -0
- package/src/components/ui/display.tsx +6 -1
- package/src/components/ui/external-link.tsx +10 -2
- package/src/data/memory-resource-store.ts +1 -1
- package/src/data/resource-store.ts +13 -1
- package/src/docs/snapshot.json +13 -13
- package/src/i18n/es.ts +1 -0
- package/src/i18n/ja.ts +1 -0
- package/src/i18n/ko.ts +1 -0
- package/src/i18n/zh-cn.ts +1 -0
- package/src/i18n/zh-tw.ts +1 -0
- package/src/market-data/futures-contract.ts +10 -0
- package/src/market-data/market/format.ts +7 -2
- package/src/market-data/market/freshness.ts +25 -1
- package/src/market-data/quotes/regular-session-valuation.ts +32 -1
- package/src/market-data/quotes/resolution.ts +2 -0
- package/src/plugins/builtin/account-management/pane.tsx +17 -13
- package/src/plugins/builtin/attention/headless.ts +19 -10
- package/src/plugins/builtin/attention/model.ts +22 -0
- package/src/plugins/builtin/bond-calculator/pane.tsx +2 -1
- package/src/plugins/builtin/changelog/index.tsx +11 -8
- package/src/plugins/builtin/chart-composer/editor/desktop-view.tsx +2 -0
- package/src/plugins/builtin/chat/attachments/desktop.tsx +2 -1
- package/src/plugins/builtin/chat/attachments/model.ts +5 -3
- package/src/plugins/builtin/chat/content/footer.tsx +6 -0
- package/src/plugins/builtin/chat/content/open-targets.ts +13 -4
- package/src/plugins/builtin/chat/content/transcript.tsx +2 -0
- package/src/plugins/builtin/chat/content/whole-message-copy.ts +70 -0
- package/src/plugins/builtin/chat/layout.ts +91 -31
- package/src/plugins/builtin/chat/markdown.ts +94 -0
- package/src/plugins/builtin/chat/message/desktop.tsx +3 -2
- package/src/plugins/builtin/chat/message/inline-tokens.tsx +197 -80
- package/src/plugins/builtin/chat/message/render-state.ts +3 -0
- package/src/plugins/builtin/chat/message/terminal.tsx +4 -4
- package/src/plugins/builtin/cloud/thesis/exposure.ts +18 -2
- package/src/plugins/builtin/company-attention/headless.ts +8 -5
- package/src/plugins/builtin/company-attention/model.ts +53 -2
- package/src/plugins/builtin/company-attention/pane.tsx +1 -1
- package/src/plugins/builtin/congress-trades/detail.tsx +7 -4
- package/src/plugins/builtin/congress-trades/pane.tsx +3 -1
- package/src/plugins/builtin/correlation/relationship/pane.tsx +3 -7
- package/src/plugins/builtin/cot/client.ts +12 -4
- package/src/plugins/builtin/cot/pane.tsx +2 -2
- package/src/plugins/builtin/crypto-board/model.ts +7 -3
- package/src/plugins/builtin/debt-maturities/pane.tsx +1 -6
- package/src/plugins/builtin/debug/index.tsx +15 -15
- package/src/plugins/builtin/dividend-yield/client.ts +8 -1
- package/src/plugins/builtin/dividend-yield/headless.ts +4 -1
- package/src/plugins/builtin/dividend-yield/pane.tsx +6 -5
- package/src/plugins/builtin/docs/index.tsx +5 -3
- package/src/plugins/builtin/docs/pane.tsx +18 -1
- package/src/plugins/builtin/docs/targets.ts +5 -3
- package/src/plugins/builtin/earnings-calls/data.ts +22 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +24 -14
- package/src/plugins/builtin/equity-screener/headless.ts +4 -3
- package/src/plugins/builtin/equity-screener/live.ts +27 -1
- package/src/plugins/builtin/equity-screener/pane.tsx +21 -2
- package/src/plugins/builtin/fear-greed/charts.tsx +5 -1
- package/src/plugins/builtin/fear-greed/pane.tsx +1 -1
- package/src/plugins/builtin/futures/index.tsx +32 -4
- package/src/plugins/builtin/futures/model.ts +65 -3
- package/src/plugins/builtin/futures/table.tsx +31 -11
- package/src/plugins/builtin/futures/use-board-terms.ts +89 -0
- package/src/plugins/builtin/holders/pane.tsx +6 -1
- package/src/plugins/builtin/mna/client.ts +12 -2
- package/src/plugins/builtin/mna/detail.tsx +2 -2
- package/src/plugins/builtin/mna/pane.tsx +2 -2
- package/src/plugins/builtin/notes/owner.tsx +5 -4
- package/src/plugins/builtin/notes/ticker-notes-tab.tsx +2 -3
- package/src/plugins/builtin/options-calculator/pane.tsx +2 -3
- package/src/plugins/builtin/portfolio-list/cli/bulk-symbols.ts +8 -5
- package/src/plugins/builtin/portfolio-list/cli/portfolio-command.ts +4 -3
- package/src/plugins/builtin/portfolio-list/cli/shared.ts +9 -0
- package/src/plugins/builtin/portfolio-list/cli/watchlist-command.ts +6 -3
- package/src/plugins/builtin/research/equity-diagnostic-pane.tsx +2 -9
- package/src/plugins/builtin/research/event-model.ts +14 -0
- package/src/plugins/builtin/research/headless-schema.ts +31 -1
- package/src/plugins/builtin/research/relative-valuation-columns.ts +289 -0
- package/src/plugins/builtin/research/relative-valuation-currency.ts +164 -0
- package/src/plugins/builtin/research/relative-valuation-headless.ts +94 -11
- package/src/plugins/builtin/research/relative-valuation-model.ts +4 -1
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +13 -4
- package/src/plugins/builtin/research/relative-valuation-peers.ts +302 -0
- package/src/plugins/builtin/research-search/data.ts +11 -0
- package/src/plugins/builtin/research-search/pane.tsx +13 -2
- package/src/plugins/builtin/scanner/hilo-pane.tsx +7 -17
- package/src/plugins/builtin/seasonality/client.ts +23 -0
- package/src/plugins/builtin/seasonality/pane.tsx +4 -2
- package/src/plugins/builtin/sectors/index.tsx +7 -3
- package/src/plugins/builtin/shared/report-freshness.ts +6 -2
- package/src/plugins/builtin/short-interest/headless.ts +13 -3
- package/src/plugins/builtin/short-interest/index.tsx +1 -1
- package/src/plugins/builtin/short-interest/model.ts +62 -3
- package/src/plugins/builtin/short-interest/pane.tsx +2 -2
- package/src/plugins/builtin/short-interest/schedule.ts +107 -0
- package/src/plugins/builtin/short-interest/watch-client.ts +54 -8
- package/src/plugins/builtin/short-interest/watch-headless.ts +8 -3
- package/src/plugins/builtin/short-interest/watch-model.ts +52 -23
- package/src/plugins/builtin/short-interest/watch-pane.tsx +1 -1
- package/src/plugins/builtin/supply-chain/flow.tsx +14 -4
- package/src/plugins/builtin/themes/headless.ts +1 -1
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +23 -27
- package/src/plugins/builtin/ticker-detail/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/overview/model.ts +4 -3
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +4 -4
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +9 -4
- package/src/plugins/builtin/vol-surface/pane.tsx +16 -1
- package/src/plugins/builtin/volatility/chart-model.ts +1 -1
- package/src/plugins/builtin/volatility/charts.tsx +1 -1
- package/src/renderers/dom/styles.css +6 -0
- package/src/sources/gloomberb-cloud/normalizers.ts +1 -0
- package/src/sources/provider-router/batches.ts +2 -1
- package/src/sources/provider-router/cache.ts +3 -0
- package/src/sync/baseline.ts +19 -14
- package/src/sync/controller.ts +140 -40
- package/src/sync/core-contributors.ts +256 -32
- package/src/sync/types.ts +44 -0
- package/src/time-series/resolve.ts +19 -2
- package/src/types/data-provider.ts +8 -0
- package/src/types/financials.ts +40 -4
- package/src/types/headless.ts +43 -0
- package/src/types/plugin.ts +7 -0
- package/src/utils/declared-dividends.ts +39 -0
- package/src/utils/forward-dividend.ts +34 -11
- package/src/utils/fundamentals.ts +13 -2
- package/src/utils/markdown.ts +180 -0
- package/src/utils/market-capitalization.ts +22 -0
|
@@ -104,6 +104,7 @@ export function CongressTradesPane({ focused, width, height, tickerFilter }: Pan
|
|
|
104
104
|
const [lastLoadedAt, setLastLoadedAt] = useState<number | null>(null);
|
|
105
105
|
const [loadingMore, setLoadingMore] = useState(false);
|
|
106
106
|
const tradeScrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
107
|
+
const detailScrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
107
108
|
const [savedTab, setActiveTab] = usePluginPaneState<CongressTab>(`${statePrefix}activeTab`, "trades");
|
|
108
109
|
const activeTab = tickerFilter ? "trades" : savedTab;
|
|
109
110
|
const [selectedTradeId, setSelectedTradeId] = useDebouncedPluginPaneState<string | null>(`${statePrefix}selectedTradeId`, null);
|
|
@@ -382,7 +383,7 @@ export function CongressTradesPane({ focused, width, height, tickerFilter }: Pan
|
|
|
382
383
|
const detailContent = detailMode?.kind === "ticker" ? (
|
|
383
384
|
<CongressTradesPane focused={focused} width={width} height={height - 3} paneId="congress-ticker" paneType="congress-trades" tickerFilter={detailMode.ticker} />
|
|
384
385
|
) : detailTrade ? (
|
|
385
|
-
<TradeDetail trade={detailTrade} width={width} />
|
|
386
|
+
<TradeDetail trade={detailTrade} width={width} scrollRef={detailScrollRef} />
|
|
386
387
|
) : detailMember ? (
|
|
387
388
|
<MemberTradesDetail
|
|
388
389
|
focused={focused}
|
|
@@ -442,6 +443,7 @@ export function CongressTradesPane({ focused, width, height, tickerFilter }: Pan
|
|
|
442
443
|
setDetailMode({ kind: "trade", tradeId: trade.id });
|
|
443
444
|
}}
|
|
444
445
|
onRootKeyDown={handleFiltersKey}
|
|
446
|
+
detailScrollRef={detailScrollRef}
|
|
445
447
|
rootWidth={width}
|
|
446
448
|
rootBefore={filterBar}
|
|
447
449
|
resetScrollKey={`${filterKey}:${mine}`}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "../../../../components";
|
|
12
12
|
import { StaticScatterChartSurface } from "../../../../components/chart/static";
|
|
13
13
|
import { scalarPoint, staticSeries } from "../../../../components/chart/static/series";
|
|
14
|
+
import { usePaneRefreshKey } from "../../../../components/data-table/table-pane";
|
|
14
15
|
import { useShortcut, type KeyEventLike } from "../../../../react/input";
|
|
15
16
|
import { usePaneInstance } from "../../../../state/app/context";
|
|
16
17
|
import { colors } from "../../../../theme/colors";
|
|
@@ -58,13 +59,12 @@ type RelationshipGraphShortcut =
|
|
|
58
59
|
| "window"
|
|
59
60
|
| "correlation"
|
|
60
61
|
| "regression"
|
|
61
|
-
| "refresh"
|
|
62
62
|
| { range: RelationshipRange };
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
65
|
* [t]ime range cycles and 1-6 pick a range, [p]eriod cycles the correlation
|
|
66
66
|
* window, [c]orr and [f]it toggle their panels. The query bar shows all four,
|
|
67
|
-
* so none needs a footer hint. `r`
|
|
67
|
+
* so none needs a footer hint. Plain `r` is the pane refresh key.
|
|
68
68
|
*/
|
|
69
69
|
function resolveRelationshipGraphShortcut(
|
|
70
70
|
event: Pick<KeyEventLike, "name" | "key" | "ctrl" | "shift" | "alt" | "meta" | "super">,
|
|
@@ -73,8 +73,6 @@ function resolveRelationshipGraphShortcut(
|
|
|
73
73
|
|
|
74
74
|
const key = (event.name ?? event.key ?? "").toLowerCase();
|
|
75
75
|
switch (key) {
|
|
76
|
-
case "r":
|
|
77
|
-
return "refresh";
|
|
78
76
|
case "t":
|
|
79
77
|
return "range";
|
|
80
78
|
case "p":
|
|
@@ -232,6 +230,7 @@ export function RelationshipGraphPane({ focused, width, height }: PaneProps) {
|
|
|
232
230
|
return true;
|
|
233
231
|
}, [alignedDates]);
|
|
234
232
|
|
|
233
|
+
usePaneRefreshKey(() => { void reload(); }, { focused });
|
|
235
234
|
useShortcut((event) => {
|
|
236
235
|
if (!focused) return;
|
|
237
236
|
if (isPlainKey(event, "home", "end") && jumpCursor(event.name)) {
|
|
@@ -248,9 +247,6 @@ export function RelationshipGraphPane({ focused, width, height }: PaneProps) {
|
|
|
248
247
|
return;
|
|
249
248
|
}
|
|
250
249
|
switch (shortcut) {
|
|
251
|
-
case "refresh":
|
|
252
|
-
void reload();
|
|
253
|
-
return;
|
|
254
250
|
case "range":
|
|
255
251
|
cycleRange();
|
|
256
252
|
return;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { apiClient } from "../../../api-client";
|
|
2
2
|
import type { CotBoardPayload, CotClass, CotClassSummary, CotContractPayload, CotFamily, CotPayloadBase } from "../../../api-client/cot";
|
|
3
3
|
import { createPluginCache } from "../../../data/plugin-cache";
|
|
4
|
-
import { loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
|
|
4
|
+
import { cachedCloudResource, loadCloudResource, unavailableOnServer } from "../shared/cloud-resource";
|
|
5
5
|
import type { PricePoint } from "../../../types/financials";
|
|
6
6
|
import { COT_CLASSES, cotPriceMapping } from "./model";
|
|
7
7
|
|
|
@@ -67,10 +67,18 @@ export async function fetchCotBoard(family: CotFamily, traderClass: CotClass, cl
|
|
|
67
67
|
try { return validateCotBoard(await client.getCloudCotBoard(family, traderClass), family, traderClass); }
|
|
68
68
|
catch (error) { throw unavailable(error); }
|
|
69
69
|
}
|
|
70
|
+
const cotBoardKey = (family: CotFamily, traderClass: CotClass) => `${family}:${traderClass}`;
|
|
71
|
+
const presentCotBoard = (payload: CotBoardPayload, stale: boolean, refreshError: string | null) => ({
|
|
72
|
+
...payload, gaps: [...payload.gaps, ...(refreshError ? [refreshError] : [])],
|
|
73
|
+
status: stale && payload.status === "available" ? "partial" as const : payload.status,
|
|
74
|
+
});
|
|
75
|
+
export function cachedCotBoard(family: CotFamily, traderClass: CotClass) {
|
|
76
|
+
const cached = cachedCloudResource(cotBoardCache, cotBoardKey(family, traderClass), (payload) => validateCotBoard(payload, family, traderClass));
|
|
77
|
+
return cached ? presentCotBoard(cached.payload, false, null) : null;
|
|
78
|
+
}
|
|
70
79
|
export async function loadCotBoard(family: CotFamily, traderClass: CotClass, force = false) {
|
|
71
|
-
const { payload, stale, refreshError } = await loadCloudResource(cotBoardCache,
|
|
72
|
-
return
|
|
73
|
-
status: stale && payload.status === "available" ? "partial" as const : payload.status };
|
|
80
|
+
const { payload, stale, refreshError } = await loadCloudResource(cotBoardCache, cotBoardKey(family, traderClass), () => fetchCotBoard(family, traderClass), { force });
|
|
81
|
+
return presentCotBoard(payload, stale, refreshError);
|
|
74
82
|
}
|
|
75
83
|
export async function fetchCotContract(contractCode: string, family: CotFamily, client: Pick<typeof apiClient, "getCloudCotContract"> = apiClient) {
|
|
76
84
|
try { return validateCotContract(await client.getCloudCotContract(contractCode, family), family, contractCode); }
|
|
@@ -12,7 +12,7 @@ import { Box } from "../../../ui";
|
|
|
12
12
|
import { useAutoRefresh } from "../../../react/auto-refresh";
|
|
13
13
|
import { compareSortValues, nextHeaderSort, type SortPreference } from "../../../utils/sort-values";
|
|
14
14
|
import { useResearchCloudSession } from "../shared/research-cloud-session";
|
|
15
|
-
import { loadCotBoard, loadCotDetail } from "./client";
|
|
15
|
+
import { cachedCotBoard, loadCotBoard, loadCotDetail } from "./client";
|
|
16
16
|
import { COT_CLASSES, COT_MAJOR_CODES, COT_SCOPES, cotChartSeries, cotClass, cotInteger, cotLegendValue, cotMarketName, cotScope, type CotScope } from "./model";
|
|
17
17
|
|
|
18
18
|
const FAMILIES = [{ value: "legacy", label: "Legacy" }, { value: "disaggregated", label: "Disaggregated" }];
|
|
@@ -69,7 +69,7 @@ function CotBoard({ width, height, focused, family, initialCode }: PaneProps & {
|
|
|
69
69
|
const control = useRef<SelectControl>(null);
|
|
70
70
|
const scopeControl = useRef<SelectControl>(null);
|
|
71
71
|
const loader = useCallback((force: boolean) => loadCotBoard(family, traderClass, force), [family, traderClass, session.requestKey]);
|
|
72
|
-
const resource = useAsyncResource(loader, { clearOnError: isAccessDenied });
|
|
72
|
+
const resource = useAsyncResource(loader, { initialData: () => cachedCotBoard(family, traderClass), clearOnError: isAccessDenied });
|
|
73
73
|
const data = resource.data?.traderClass === traderClass ? resource.data : null;
|
|
74
74
|
const rows = useMemo(() => {
|
|
75
75
|
// A typed query searches every market; the scope only shapes the unsearched board.
|
|
@@ -105,9 +105,13 @@ function buildCryptoRow(
|
|
|
105
105
|
: isFiniteNumber(asset.circulatingSupply) && asset.circulatingSupply > 0 ? asset.circulatingSupply * price : null;
|
|
106
106
|
const start = asset.history ? Date.parse(`${asset.history.start}T00:00:00Z`) : 0;
|
|
107
107
|
const closes = completedCloses(asset);
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
const prior = rowsByAsset.get(asset)?.row.history;
|
|
109
|
+
const tail = prior && prior.length === closes.length + 1 ? prior.at(-1) : undefined;
|
|
110
|
+
const history: PricePoint[] = !closes.length
|
|
111
|
+
? closes
|
|
112
|
+
: tail && tail.close === price
|
|
113
|
+
? prior!
|
|
114
|
+
: [...closes, { date: new Date(Math.max(now, start + closes.length * DAY_MS)), close: price }];
|
|
111
115
|
const updatedAt = quote?.lastUpdated ?? (asset.quoteTime ? Date.parse(asset.quoteTime) : null);
|
|
112
116
|
// The digits follow a price that holds still for the day, so a tick across a
|
|
113
117
|
// power of ten (0.9998 to 1.0002) keeps the row's decimals.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useMemo, type ReactNode } from "react";
|
|
2
2
|
import { listingIdentity, useIssuerListing } from "../shared/ticker-request";
|
|
3
|
-
import { Box, ScrollBox,
|
|
3
|
+
import { Box, ScrollBox, useUiCapabilities } from "../../../ui";
|
|
4
4
|
import {
|
|
5
5
|
useAsyncResource,
|
|
6
6
|
useAutoRefresh,
|
|
@@ -405,9 +405,6 @@ export function DebtMaturitiesPane({ width, height, focused }: PaneProps) {
|
|
|
405
405
|
label: "Principal total", values, value: debtAmount(last), color: colors.warning,
|
|
406
406
|
};
|
|
407
407
|
}, [bars]);
|
|
408
|
-
const historyWindow = latest
|
|
409
|
-
? `${latest.totalPrincipal.percentile.windowStart} to ${latest.asOf}`
|
|
410
|
-
: "";
|
|
411
408
|
useAutoRefresh(resource.updatedAt, resource.load);
|
|
412
409
|
usePaneRefreshKey(() => void resource.reload(), { focused });
|
|
413
410
|
usePaneNoticeFooter({
|
|
@@ -598,8 +595,6 @@ export function DebtMaturitiesPane({ width, height, focused }: PaneProps) {
|
|
|
598
595
|
xAxis={barAxis}
|
|
599
596
|
formatValue={(value) => debtAmount(value)}
|
|
600
597
|
formatAxisValue={formatCompactAxis}
|
|
601
|
-
legendAccessory={<Text fg={colors.textMuted}>{historyWindow}</Text>}
|
|
602
|
-
legendAccessoryWidth={historyWindow.length}
|
|
603
598
|
remoteKind="debt-filing-history"
|
|
604
599
|
{...historyLink}
|
|
605
600
|
/>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { Box,
|
|
2
|
-
import { useState, useEffect, useCallback, useMemo, useRef } from "react";
|
|
1
|
+
import { Box, Text, type ScrollBoxRenderable } from "../../../ui";
|
|
2
|
+
import { useState, useEffect, useCallback, useMemo, useRef, type RefObject } from "react";
|
|
3
3
|
import { useShortcut } from "../../../react/input";
|
|
4
4
|
import { TextAttributes } from "../../../ui";
|
|
5
5
|
import type { AppNotificationRequest, GloomPlugin, PaneProps } from "../../../types/plugin";
|
|
6
6
|
import {
|
|
7
7
|
DataTableStackView,
|
|
8
|
+
DetailScrollBody,
|
|
8
9
|
isTableScrollNearEnd,
|
|
9
10
|
QueryBar,
|
|
10
11
|
usePaneFooter,
|
|
@@ -89,18 +90,16 @@ function renderDebugCell(entry: LogEntry, column: DebugColumn, _index: number, s
|
|
|
89
90
|
return { text: entry.message.replace(/\s+/g, " "), color: state.selected ? colors.selectedText : colors.text };
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
function DebugEntryDetail({ entry }: { entry: LogEntry }) {
|
|
93
|
+
function DebugEntryDetail({ entry, scrollRef }: { entry: LogEntry; scrollRef: RefObject<ScrollBoxRenderable | null> }) {
|
|
93
94
|
return (
|
|
94
|
-
<
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
{
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
</Box>
|
|
103
|
-
</ScrollBox>
|
|
95
|
+
<DetailScrollBody ref={scrollRef} resetScrollKey={entry.id}>
|
|
96
|
+
<Text fg={colors.text} wrapText selectable>{entry.message}</Text>
|
|
97
|
+
{entry.data !== undefined ? (
|
|
98
|
+
<Box marginTop={1}>
|
|
99
|
+
<Text fg={colors.textDim} wrapText selectable>{JSON.stringify(entry.data, null, 2)}</Text>
|
|
100
|
+
</Box>
|
|
101
|
+
) : null}
|
|
102
|
+
</DetailScrollBody>
|
|
104
103
|
);
|
|
105
104
|
}
|
|
106
105
|
|
|
@@ -116,6 +115,7 @@ function DebugPane({ focused, width, height }: PaneProps) {
|
|
|
116
115
|
const levelControl = useRef<SelectControl>(null);
|
|
117
116
|
const sourceControl = useRef<SelectControl>(null);
|
|
118
117
|
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
118
|
+
const detailScrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
119
119
|
// While on, the cursor rides the newest entry, which keeps the list pinned
|
|
120
120
|
// to the bottom as entries arrive.
|
|
121
121
|
const [autoScroll, setAutoScroll] = useState(true);
|
|
@@ -173,7 +173,6 @@ function DebugPane({ focused, width, height }: PaneProps) {
|
|
|
173
173
|
// chord never clears the log.
|
|
174
174
|
useShortcut((event) => {
|
|
175
175
|
if (!focused || openId || searching || event.targetEditable || event.ctrl || event.meta || event.alt || event.super) return;
|
|
176
|
-
if (event.name === "l") { event.preventDefault?.(); levelControl.current?.open(); return; }
|
|
177
176
|
if (event.name === "s") { event.preventDefault?.(); sourceControl.current?.open(); return; }
|
|
178
177
|
// Top and end of the log; G also resumes following new entries. Some
|
|
179
178
|
// terminals report Shift+G as an uppercase name.
|
|
@@ -268,8 +267,9 @@ function DebugPane({ focused, width, height }: PaneProps) {
|
|
|
268
267
|
emptyStateTitle={filtered ? "No log entries match the filter." : "No log entries."}
|
|
269
268
|
detailOpen={!!openEntry}
|
|
270
269
|
onBack={() => setOpenId(null)}
|
|
270
|
+
detailScrollRef={detailScrollRef}
|
|
271
271
|
detailTitle={openEntry ? `${formatTimestamp(openEntry.timestamp)} ${LEVEL_LABELS[openEntry.level]} ${openEntry.source}` : undefined}
|
|
272
|
-
detailContent={openEntry ? <DebugEntryDetail entry={openEntry} /> : null}
|
|
272
|
+
detailContent={openEntry ? <DebugEntryDetail entry={openEntry} scrollRef={detailScrollRef} /> : null}
|
|
273
273
|
/>
|
|
274
274
|
</Box>
|
|
275
275
|
);
|
|
@@ -8,6 +8,8 @@ import { inferCadence, trailingCashAt } from "./trailing-cash";
|
|
|
8
8
|
import { dividendQuotePriceMetadata } from "./reference-price";
|
|
9
9
|
import { mapQuote } from "../../../sources/gloomberb-cloud/normalizers";
|
|
10
10
|
import { regularSessionValuationPrice } from "../../../market-data/quotes/regular-session-valuation";
|
|
11
|
+
import { declaredDividends } from "../../../utils/declared-dividends";
|
|
12
|
+
import type { DeclaredDividendAction } from "../../../types/financials";
|
|
11
13
|
|
|
12
14
|
export const DIVIDENDS_CONNECTION_ID = "gloom-dividends";
|
|
13
15
|
export const INCOMPLETE_DIVIDEND_HISTORY = "Incomplete cash history; totals unavailable.";
|
|
@@ -55,6 +57,8 @@ export function toDividendPayment(
|
|
|
55
57
|
|
|
56
58
|
export interface DividendData {
|
|
57
59
|
payments: DividendPayment[];
|
|
60
|
+
/** Declared and unpaid, in `currency`; never in `payments` or any figure. Absent from older servers. */
|
|
61
|
+
declared?: DeclaredDividendAction[];
|
|
58
62
|
metrics: DividendMetrics;
|
|
59
63
|
price: number | null;
|
|
60
64
|
currency?: string;
|
|
@@ -113,8 +117,11 @@ export async function fetchDividendData(
|
|
|
113
117
|
if (!historyAvailable && !historyError && metrics.trailingRate == null && metrics.forwardRate == null) {
|
|
114
118
|
throw new Error(`No dividend data found for ${symbol}`);
|
|
115
119
|
}
|
|
120
|
+
// Stated in the cash history's units, read as its payments are.
|
|
121
|
+
const declared = actions && cashUnit.currency
|
|
122
|
+
? declaredDividends(actions).map((row) => ({ ...row, amount: row.amount / cashUnit.divisor })) : [];
|
|
116
123
|
return {
|
|
117
|
-
payments, metrics, price, currency: currency || undefined, historyAvailable,
|
|
124
|
+
payments, ...(declared.length > 0 ? { declared } : {}), metrics, price, currency: currency || undefined, historyAvailable,
|
|
118
125
|
...(suppliedPrice == null && price != null && quote ? dividendQuotePriceMetadata(quote) : {}),
|
|
119
126
|
...(historyError ? { historyError } : {}), ...(summaryError ? { summaryError } : {}),
|
|
120
127
|
providerId: actions?.providerId ?? "gloom", fetchedAt: actions?.fetchedAt,
|
|
@@ -101,7 +101,8 @@ export function projectDividendYieldHeadless(
|
|
|
101
101
|
: "Reference price time unavailable; cash yield may be out of date." }] : []),
|
|
102
102
|
{ label: "Trailing yield", value: metrics.trailingYield, formatted: formatDividendYield(metrics.trailingYield) },
|
|
103
103
|
{ label: "Forward yield", value: metrics.forwardYield, formatted: metrics.forwardYield == null ? formatDividendYield(null)
|
|
104
|
-
: `${formatDividendYield(metrics.forwardYield)} (${forwardDividendBasis(metrics.forwardRateAsOf,
|
|
104
|
+
: `${formatDividendYield(metrics.forwardYield)} (${forwardDividendBasis(metrics.forwardRateAsOf,
|
|
105
|
+
{ payDate: forwardPayDate(metrics), declared: data.declared, currency })})` },
|
|
105
106
|
{ label: "Trailing rate", value: metrics.trailingRate, formatted: currency ? formatDistributionAmount(metrics.trailingRate ?? undefined, currency) : "—" },
|
|
106
107
|
{ label: "Forward rate", value: metrics.forwardRate, formatted: currency ? formatDistributionAmount(metrics.forwardRate ?? undefined, currency) : "—" },
|
|
107
108
|
{ label: "Earnings Payout", value: metrics.payoutRatio, formatted: formatDividendYield(metrics.payoutRatio) },
|
|
@@ -138,6 +139,8 @@ export function projectDividendYieldHeadless(
|
|
|
138
139
|
limitations: data.notes ?? [],
|
|
139
140
|
yieldMethod: "Cash distributions with ex-dates in the preceding 12 months, one payment per period for a regular cadence, divided by the reference share price; excludes reinvestment and is not SEC yield or total return.",
|
|
140
141
|
forwardRateAsOf: metrics.forwardRateAsOf,
|
|
142
|
+
// Declared and unpaid, apart from the paid history the table and every figure read.
|
|
143
|
+
declaredDividends: data.declared ?? [],
|
|
141
144
|
forwardRateMethod: "Indicated annual cash rate, the one fundamentals use: the latest regular distribution annualized, or once the next one is declared that one, dated by its ex-date (forwardRateAsOf; a date still ahead is the declared distribution's). Only when its currency units are comparable with the share price.",
|
|
142
145
|
paymentFrequencyMethod: "Cadence inferred from ex-dates in the last two years, unavailable when no cash was reported in the last year; not an announced payment schedule.",
|
|
143
146
|
},
|
|
@@ -73,14 +73,15 @@ function formatFrequency(freq: DividendMetrics["paymentFrequency"]): string {
|
|
|
73
73
|
* clip them. The latest ex-date is the table's first row, so it shows only
|
|
74
74
|
* when there is no table.
|
|
75
75
|
*/
|
|
76
|
-
function buildMetricItems(metrics: DividendMetrics, currency: string, hasHistory: boolean, short: boolean
|
|
76
|
+
function buildMetricItems(metrics: DividendMetrics, currency: string, hasHistory: boolean, short: boolean,
|
|
77
|
+
declared: DividendData["declared"]): StatItem[] {
|
|
77
78
|
const exDate: StatItem[] = metrics.nextExDividendDate
|
|
78
79
|
? [{ id: "next-ex", label: "Next ex", value: formatDate(metrics.nextExDividendDate) }]
|
|
79
80
|
: hasHistory ? [] : [{ id: "last-ex", label: "Last ex", value: formatDate(metrics.lastExDividendDate) }];
|
|
80
|
-
const forwardNote = metrics.forwardYield != null ? forwardDividendNote(metrics.forwardRateAsOf) : null;
|
|
81
|
+
const forwardNote = metrics.forwardYield != null ? forwardDividendNote(metrics.forwardRateAsOf, { declared, currency }) : null;
|
|
81
82
|
return [
|
|
82
83
|
{ id: "ttm-yield", label: short ? "TTM yld" : "TTM yield", value: formatDividendYield(metrics.trailingYield), color: priceColor(metrics.trailingYield ?? 0) },
|
|
83
|
-
// Dated by the distribution it annualizes, or marked declared beside the next ex-date; docs/research-data.md says how it is built.
|
|
84
|
+
// Dated by the distribution it annualizes, or marked declared (with its stated amount) beside the next ex-date; docs/research-data.md says how it is built.
|
|
84
85
|
{ id: "forward-yield", label: short ? "Fwd yld" : "Fwd yield", value: formatDividendYield(metrics.forwardYield), color: priceColor(metrics.forwardYield ?? 0),
|
|
85
86
|
...(forwardNote ? { detail: forwardNote } : {}) },
|
|
86
87
|
{ id: "ttm-rate", label: short ? "TTM/sh" : "TTM/share", value: formatRate(metrics.trailingRate, currency) },
|
|
@@ -225,13 +226,13 @@ export function DividendYieldPane({ focused, width, height, loadData = fetchDivi
|
|
|
225
226
|
});
|
|
226
227
|
// The header row, plus the scrollbar row once the columns overflow the pane.
|
|
227
228
|
const tableChromeRows = chartTableChromeRows(columns, width);
|
|
228
|
-
const fullFigures = metrics ? buildMetricItems(metrics, currency, sortedRows.length > 0, false) : [];
|
|
229
|
+
const fullFigures = metrics ? buildMetricItems(metrics, currency, sortedRows.length > 0, false, data?.declared) : [];
|
|
229
230
|
const keptFigures = chartTableLayout({
|
|
230
231
|
width, height, figures: fullFigures, tableRows: sortedRows.length, tableChromeRows, chart: chartPoints.length >= 2 ? {} : null,
|
|
231
232
|
}).figures;
|
|
232
233
|
// The desktop grid never clips a label; the terminal shortens them rather than clip.
|
|
233
234
|
const figures = metrics && !nativePaneChrome && labelsClip(keptFigures, width)
|
|
234
|
-
? buildMetricItems(metrics, currency, sortedRows.length > 0, true) : fullFigures;
|
|
235
|
+
? buildMetricItems(metrics, currency, sortedRows.length > 0, true, data?.declared) : fullFigures;
|
|
235
236
|
|
|
236
237
|
const handleHeaderClick = useCallback((columnId: string) => {
|
|
237
238
|
setSortPreference((current) => nextHeaderSort(current, columnId as DividendColumnId, {
|
|
@@ -14,10 +14,10 @@ const DOCS_INSTANCE_ID = "docs";
|
|
|
14
14
|
|
|
15
15
|
function requestedTopic(context: PaneTemplateContext, options?: PaneTemplateCreateOptions): DocsTopic {
|
|
16
16
|
const registry = getSharedRegistry();
|
|
17
|
-
|
|
18
|
-
const index = buildRegistryHelpIndex(registry);
|
|
17
|
+
// The command line (`gloomberb fn DOCS RV`) has no registry: the argument goes to the reader as a search.
|
|
18
|
+
const index = registry ? buildRegistryHelpIndex(registry) : null;
|
|
19
19
|
const paneId = options?.values?.pane;
|
|
20
|
-
if (paneId) {
|
|
20
|
+
if (paneId && registry && index) {
|
|
21
21
|
const instance = findPaneInstance(context.layout, paneId);
|
|
22
22
|
return (instance ? docsTopicForPane(index, registry, instance) : null) ?? { kind: "home" };
|
|
23
23
|
}
|
|
@@ -40,6 +40,8 @@ export const docsModule: PluginModule = {
|
|
|
40
40
|
icon: "?",
|
|
41
41
|
component: DocsPane,
|
|
42
42
|
reportFreshness: { status: "not-a-feed", basis: "documentation" },
|
|
43
|
+
// A page reads whole in a report, not cut at the first screen.
|
|
44
|
+
shotFitsReader: true,
|
|
43
45
|
defaultPosition: "right",
|
|
44
46
|
defaultMode: "floating",
|
|
45
47
|
defaultFloatingSize: { width: 96, height: 34 },
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
type PaneHint,
|
|
31
31
|
type SectionedRow,
|
|
32
32
|
} from "../../../components";
|
|
33
|
+
import { usePaneRefreshKey } from "../../../components/data-table/table-pane";
|
|
33
34
|
import type { DocsFeed, DocsFunction } from "../../../docs/content";
|
|
34
35
|
import {
|
|
35
36
|
docsEntries,
|
|
@@ -48,6 +49,7 @@ import {
|
|
|
48
49
|
type DocsTopic,
|
|
49
50
|
} from "../../../docs/topics";
|
|
50
51
|
import { t, tf } from "../../../i18n";
|
|
52
|
+
import { useRemoteUiNode } from "../../../remote/semantic-tree";
|
|
51
53
|
import { usePaneInstance } from "../../../state/app/context";
|
|
52
54
|
import { useThemeColors } from "../../../theme/theme-context";
|
|
53
55
|
import type { PaneProps } from "../../../types/plugin";
|
|
@@ -257,7 +259,8 @@ export function DocsPane({ focused, width, height }: PaneProps) {
|
|
|
257
259
|
const instance = usePaneInstance();
|
|
258
260
|
const requestKey = instance?.params?.topic ?? null;
|
|
259
261
|
const requestAt = instance?.params?.at ?? null;
|
|
260
|
-
const { feed, offline } = useDocsFeed();
|
|
262
|
+
const { feed, offline, reload } = useDocsFeed();
|
|
263
|
+
usePaneRefreshKey(reload, { focused });
|
|
261
264
|
const [stack, setStack] = usePluginPaneState<string[]>("stack", []);
|
|
262
265
|
const [query, setQuery] = usePluginPaneState<string>("query", "");
|
|
263
266
|
const [show, setShow] = usePluginPaneState<DocsShow>("show", "all");
|
|
@@ -287,6 +290,12 @@ export function DocsPane({ focused, width, height }: PaneProps) {
|
|
|
287
290
|
openTopic({ kind: "page", slug: topic.query.toLowerCase() });
|
|
288
291
|
return;
|
|
289
292
|
}
|
|
293
|
+
// The command line cannot resolve a mnemonic before the reader runs, so a search that is a function's code opens its entry.
|
|
294
|
+
const code = topic.kind === "home" && topic.query ? findDocsFunction(feed, topic.query.trim()) ?? findDocsFunction(feed, topic.query.trim().split(/\s+/)[0] ?? "") : null;
|
|
295
|
+
if (code) {
|
|
296
|
+
openTopic({ kind: "function", code: code.code });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
290
299
|
if (topic.kind === "home") setShow("all");
|
|
291
300
|
openTopic(topic);
|
|
292
301
|
}, [feed, handledAt, openTopic, requestAt, requestKey, setHandledAt, setShow]);
|
|
@@ -327,6 +336,14 @@ export function DocsPane({ focused, width, height }: PaneProps) {
|
|
|
327
336
|
}, [entries, query, show]);
|
|
328
337
|
|
|
329
338
|
const visibleEntries = useMemo(() => rows.filter(isSectionedItemRow).map((row) => row.item), [rows]);
|
|
339
|
+
// A request that matches nothing fails a report of the pane (`gloomberb fn DOCS zzzz`) instead of listing the whole index.
|
|
340
|
+
const requested = requestKey ? parseDocsTopicKey(requestKey) : null;
|
|
341
|
+
const requestedQuery = requested?.kind === "home" ? requested.query?.trim() ?? "" : "";
|
|
342
|
+
useRemoteUiNode(feed && requestedQuery && stack.length === 0 && query.trim() === requestedQuery && visibleEntries.length === 0 ? {
|
|
343
|
+
role: "report-failure",
|
|
344
|
+
label: "No docs entry",
|
|
345
|
+
getMetadata: () => ({ message: `No docs entry for ${requestedQuery}.`, details: "Run gloomberb fn DOCS for the index." }),
|
|
346
|
+
} : null);
|
|
330
347
|
const effectiveSelectedKey = visibleEntries.some((entry) => entry.key === selectedKey)
|
|
331
348
|
? selectedKey
|
|
332
349
|
: visibleEntries[0]?.key ?? null;
|
|
@@ -60,12 +60,14 @@ export function docsTopicForPane(
|
|
|
60
60
|
/**
|
|
61
61
|
* What `DOCS <argument>` asks for: a mnemonic or one of its aliases
|
|
62
62
|
* (`DOCS OMON`, `DOCS ct`), with or without its own argument (`DOCS OMON
|
|
63
|
-
* NVDA`), or words to search the docs for.
|
|
63
|
+
* NVDA`), or words to search the docs for. Without an index every argument
|
|
64
|
+
* stays a search, which the reader opens on a matching entry.
|
|
64
65
|
*/
|
|
65
|
-
export function docsTopicForArgument(index: HelpFunctionIndex, argument: string): DocsTopic {
|
|
66
|
+
export function docsTopicForArgument(index: HelpFunctionIndex | null, argument: string): DocsTopic {
|
|
66
67
|
const query = argument.trim();
|
|
67
68
|
if (!query) return { kind: "home" };
|
|
68
|
-
|
|
69
|
+
// A command line with no registry (`gloomberb fn DOCS RV`) cannot resolve a mnemonic here: the reader does, from its own entries.
|
|
70
|
+
const fn = index?.get(query.toUpperCase()) ?? index?.get((query.split(/\s+/)[0] ?? "").toUpperCase());
|
|
69
71
|
if (fn) return docsTopicForFunction(fn);
|
|
70
72
|
return { kind: "home", query };
|
|
71
73
|
}
|
|
@@ -216,6 +216,28 @@ export function loadEarningsCalls(
|
|
|
216
216
|
return loadEarningsCallsWithClient(apiClient, ticker, options);
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
+
export function cachedEarningsCalls(
|
|
220
|
+
ticker: string | null,
|
|
221
|
+
options?: IssuerListingParams & { limit?: number },
|
|
222
|
+
): EarningsCallsResult | null {
|
|
223
|
+
const normalizedTicker = ticker?.trim().toUpperCase() || null;
|
|
224
|
+
const abroad = normalizedTicker ? listingAbroad(normalizedTicker, options?.exchange, options?.name) : null;
|
|
225
|
+
const limit = listLimit(options?.limit);
|
|
226
|
+
const cached = persistence?.getResource<CloudEarningsCallListPayload>(LIST_KIND, listKey(normalizedTicker, abroad, limit, 0), {
|
|
227
|
+
sourceKey: CACHE_SOURCE,
|
|
228
|
+
schemaVersion: LIST_CACHE_SCHEMA_VERSION,
|
|
229
|
+
allowExpired: true,
|
|
230
|
+
});
|
|
231
|
+
if (!cached) return null;
|
|
232
|
+
return {
|
|
233
|
+
...cached.value,
|
|
234
|
+
sourceLimit: limit,
|
|
235
|
+
sourceLimitReached: cached.value.calls.length >= limit,
|
|
236
|
+
fetchedAt: cached.fetchedAt,
|
|
237
|
+
stale: cached.stale === true,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
219
241
|
/**
|
|
220
242
|
* The server answers 202 with a pending marker when a call is known but not
|
|
221
243
|
* transcribed yet, having just queued it. That is not a transcript and must
|
|
@@ -35,6 +35,7 @@ import { usePlanAccess } from "../../../api-client/plan-access";
|
|
|
35
35
|
import { listingAbroad } from "../../../api-client/paths";
|
|
36
36
|
import { useBoundTicker as useSymbolBinding } from "../shared/ticker-request";
|
|
37
37
|
import {
|
|
38
|
+
cachedEarningsCalls,
|
|
38
39
|
callStatusLabel,
|
|
39
40
|
isPendingTranscript,
|
|
40
41
|
loadEarningsCalls,
|
|
@@ -179,19 +180,27 @@ interface EarningsCallsViewProps {
|
|
|
179
180
|
export function EarningsCallsPane({ focused, width, height }: EarningsCallsViewProps) {
|
|
180
181
|
const { symbol, exchange, ticker: boundTicker } = useSymbolBinding();
|
|
181
182
|
const access = usePlanAccess();
|
|
183
|
+
// Read once per mount: a binding change reads the cache again in its own effect.
|
|
184
|
+
const [initialShelf] = useState(() => {
|
|
185
|
+
const initialTicker = symbol ? symbol.toUpperCase() : null;
|
|
186
|
+
const initialAbroad = initialTicker ? listingAbroad(initialTicker, exchange, boundTicker?.metadata.name) : null;
|
|
187
|
+
return cachedEarningsCalls(initialTicker, {
|
|
188
|
+
limit: CALL_PAGE_SIZE, exchange: initialAbroad?.exchange, name: initialAbroad?.name,
|
|
189
|
+
});
|
|
190
|
+
});
|
|
182
191
|
|
|
183
|
-
const [calls, setCalls] = useState<CloudEarningsCallPayload[]>([]);
|
|
184
|
-
const [listStatus, setListStatus] = useState<"idle" | "loading" | "loaded" | "error">("idle");
|
|
192
|
+
const [calls, setCalls] = useState<CloudEarningsCallPayload[]>(initialShelf?.calls ?? []);
|
|
193
|
+
const [listStatus, setListStatus] = useState<"idle" | "loading" | "loaded" | "error">(initialShelf ? "loaded" : "idle");
|
|
185
194
|
const [listError, setListError] = useState<{ message: string; status?: number } | null>(null);
|
|
186
|
-
const [stale, setStale] = useState(
|
|
187
|
-
const [listFetchedAt, setListFetchedAt] = useState(0);
|
|
195
|
+
const [stale, setStale] = useState(initialShelf?.stale === true);
|
|
196
|
+
const [listFetchedAt, setListFetchedAt] = useState(initialShelf?.fetchedAt ?? 0);
|
|
188
197
|
const listRequestVersion = useRef(0);
|
|
189
198
|
const [listCompletedRequest, setListCompletedRequest] = useState(0);
|
|
190
199
|
// The shelf is paged: the server answers a page at a time and reports no
|
|
191
200
|
// total, so a full page means there is probably another one behind it.
|
|
192
|
-
const [hasMore, setHasMore] = useState(
|
|
201
|
+
const [hasMore, setHasMore] = useState(initialShelf?.sourceLimitReached === true);
|
|
193
202
|
const [loadingMore, setLoadingMore] = useState(false);
|
|
194
|
-
const [nextOffset, setNextOffset] = useState(0);
|
|
203
|
+
const [nextOffset, setNextOffset] = useState(initialShelf?.calls.length ?? 0);
|
|
195
204
|
const tableScrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
196
205
|
|
|
197
206
|
const [selectedId, setSelectedId] = usePluginPaneState<string | null>("selectedId", null);
|
|
@@ -224,7 +233,7 @@ export function EarningsCallsPane({ focused, width, height }: EarningsCallsViewP
|
|
|
224
233
|
// Set while a call opened without a transcript is being produced.
|
|
225
234
|
const [producing, setProducing] = useState(false);
|
|
226
235
|
// The list answered "pending": the server is still searching for calls.
|
|
227
|
-
const [listPending, setListPending] = useState(
|
|
236
|
+
const [listPending, setListPending] = useState(initialShelf?.pending === true && initialShelf.calls.length === 0);
|
|
228
237
|
|
|
229
238
|
const ticker = symbol ? symbol.toUpperCase() : null;
|
|
230
239
|
// A listing abroad asks for its own calls by venue and company, never a US namesake's.
|
|
@@ -307,14 +316,15 @@ export function EarningsCallsPane({ focused, width, height }: EarningsCallsViewP
|
|
|
307
316
|
useEffect(() => {
|
|
308
317
|
const keepRestoredCall = firstShelfLoadRef.current;
|
|
309
318
|
firstShelfLoadRef.current = false;
|
|
310
|
-
|
|
311
|
-
|
|
319
|
+
const held = cachedEarningsCalls(ticker, { limit: CALL_PAGE_SIZE, exchange: listingVenue, name: listingName });
|
|
320
|
+
setCalls(held?.calls ?? []);
|
|
321
|
+
setHasMore(held?.sourceLimitReached === true);
|
|
312
322
|
setLoadingMore(false);
|
|
313
|
-
setNextOffset(0);
|
|
314
|
-
setListStatus("idle");
|
|
315
|
-
setListPending(
|
|
316
|
-
setStale(
|
|
317
|
-
setListFetchedAt(0);
|
|
323
|
+
setNextOffset(held?.calls.length ?? 0);
|
|
324
|
+
setListStatus(held ? "loaded" : "idle");
|
|
325
|
+
setListPending(held?.pending === true && held.calls.length === 0);
|
|
326
|
+
setStale(held?.stale === true);
|
|
327
|
+
setListFetchedAt(held?.fetchedAt ?? 0);
|
|
318
328
|
setListError(null);
|
|
319
329
|
setLookup(null);
|
|
320
330
|
setLookupRefresh(null);
|
|
@@ -10,7 +10,7 @@ import { buildQuoteKey } from "../../../market-data/selectors";
|
|
|
10
10
|
import type { Quote } from "../../../types/financials";
|
|
11
11
|
import { formatCurrency, formatPercentRaw } from "../../../utils/format";
|
|
12
12
|
import { fetchScreen } from "./client";
|
|
13
|
-
import { overlayScreenQuotes, screenExtendedSessions } from "./live";
|
|
13
|
+
import { overlayScreenQuotes, screenExtendedSessions, sortLiveScreenRows } from "./live";
|
|
14
14
|
import {
|
|
15
15
|
DEFAULT_SCREEN,
|
|
16
16
|
EXAMPLE_SCREEN_DEFINITION,
|
|
@@ -125,7 +125,8 @@ export const equityScreenerHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
125
125
|
const metric = (args.options.metric ?? "marketCap") as NumericField;
|
|
126
126
|
// Last, Chg% and market cap are the regular session, as `quote` and the
|
|
127
127
|
// pane read each row's quote; an extended print is a column of its own.
|
|
128
|
-
|
|
128
|
+
// A sort on one of them is redone on those figures.
|
|
129
|
+
const rows = sortLiveScreenRows(overlayScreenQuotes(data.rows, await loadRowQuotes(data.rows, ctx)), data.definition.sort);
|
|
129
130
|
const extendedSessions = screenExtendedSessions(rows);
|
|
130
131
|
// Payload status also counts ordinary per-field sparsity; only generation
|
|
131
132
|
// warnings are failures, and more pages are not an error.
|
|
@@ -184,7 +185,7 @@ export const equityScreenerHeadless: HeadlessPaneDefinition<"bundle"> = {
|
|
|
184
185
|
covered: data.universe.covered,
|
|
185
186
|
returned: data.rows.length,
|
|
186
187
|
hasMore: !!data.nextCursor,
|
|
187
|
-
priceBasis: "price, changePercent and the market cap are the regular session, read from each row's quote as `quote` reads it: after the close, its close and that close's move. A pre-market, after-hours or overnight print is in extendedPrice and extendedChangePercent, measured from that close. Membership
|
|
188
|
+
priceBasis: "price, changePercent and the market cap are the regular session, read from each row's quote as `quote` reads it: after the close, its close and that close's move. A pre-market, after-hours or overnight print is in extendedPrice and extendedChangePercent, measured from that close. Membership and percentiles are the snapshot's, which ranks by the close; a sort on price, changePercent, volume or marketCap orders these rows by the figures shown.",
|
|
188
189
|
notices: data.nextCursor
|
|
189
190
|
? [
|
|
190
191
|
`Top ${data.rows.length} of ${data.universe.matched} matches; narrow the criteria to see the rest.`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ScreenMetric, ScreenRow } from "../../../api-client/equity-screener";
|
|
1
|
+
import type { ScreenDefinition, ScreenMetric, ScreenRow } from "../../../api-client/equity-screener";
|
|
2
2
|
import { getExtendedSessionDisplay, type ExtendedSession, type ExtendedSessionDisplay } from "../../../market-data/market/status";
|
|
3
3
|
import { quoteTradingDay } from "../../../market-data/quotes/day-range";
|
|
4
4
|
import { getRegularSessionQuoteDisplay } from "../../../market-data/quotes/screener-live-quotes";
|
|
@@ -112,6 +112,32 @@ export function overlayScreenQuotes(rows: readonly ScreenRow[], quotes: Readonly
|
|
|
112
112
|
return overlayRows(rows, (key) => quotes.get(key));
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/** The fields a quote moves on a row (overlayLiveScreenRow). */
|
|
116
|
+
const LIVE_SORT_FIELDS: ReadonlySet<string> = new Set(["price", "changePercent", "volume", "marketCap"]);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Rows in the screen's order by the figures they show. A sort on a field the
|
|
120
|
+
* quotes move (price, change, volume, market cap) is redone on the overlaid
|
|
121
|
+
* values, so the column reads monotone: GRAB's snapshot cap, two closes old,
|
|
122
|
+
* kept it above MANH after its quote moved it below (GLO-754). Rows without a
|
|
123
|
+
* value stay last, and ties keep the snapshot's order. Any other sort is the
|
|
124
|
+
* snapshot's, which the overlay does not touch.
|
|
125
|
+
*/
|
|
126
|
+
export function sortLiveScreenRows<T extends ScreenRow>(rows: readonly T[], sort: ScreenDefinition["sort"]): T[] {
|
|
127
|
+
if (!LIVE_SORT_FIELDS.has(sort.field)) return [...rows];
|
|
128
|
+
const field = sort.field as "price" | "changePercent" | "volume" | "marketCap";
|
|
129
|
+
const sign = sort.direction === "desc" ? -1 : 1;
|
|
130
|
+
return rows
|
|
131
|
+
.map((row, index) => ({ row, index, value: row.metrics[field]?.value }))
|
|
132
|
+
.sort((a, b) => {
|
|
133
|
+
const aKnown = isFiniteNumber(a.value), bKnown = isFiniteNumber(b.value);
|
|
134
|
+
if (aKnown !== bKnown) return aKnown ? -1 : 1;
|
|
135
|
+
if (aKnown && bKnown && a.value !== b.value) return sign * (a.value! - b.value!);
|
|
136
|
+
return a.index - b.index;
|
|
137
|
+
})
|
|
138
|
+
.map(({ row }) => row);
|
|
139
|
+
}
|
|
140
|
+
|
|
115
141
|
/** The extended sessions some row has a print for, in the order their columns go. */
|
|
116
142
|
export function screenExtendedSessions(rows: readonly LiveScreenRow[]): ExtendedSession[] {
|
|
117
143
|
return (["PRE", "POST", "OVERNIGHT"] as const).filter((session) => rows.some((row) => row.extended?.session === session));
|