gloomberb 0.17.2 → 0.17.4
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 +2 -1
- package/src/api-client/chat.ts +28 -0
- package/src/api-client/errors.ts +11 -1
- package/src/api-client/futures-curve.ts +16 -0
- package/src/api-client/index.ts +22 -9
- package/src/api-client/normalizers.ts +21 -0
- package/src/api-client/socket.ts +27 -0
- package/src/api-client/types.ts +3 -1
- package/src/app/runtime/startup.ts +4 -0
- 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/index.ts +1 -1
- 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/metric-treemap/heat-surface.tsx +76 -14
- package/src/components/metric-treemap/heat-tiles.ts +5 -3
- package/src/components/metric-treemap/index.tsx +1 -1
- package/src/components/reader/detail-reader.tsx +28 -6
- package/src/components/ticker/badge/index.tsx +2 -0
- package/src/components/ui/button.tsx +24 -9
- package/src/components/ui/display.tsx +6 -1
- package/src/components/ui/external-link.tsx +10 -2
- package/src/components/ui/index.ts +1 -1
- package/src/components/ui/status.tsx +148 -5
- 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/layout-marketplace/gallery-desktop.tsx +2 -2
- package/src/market-data/futures-board.ts +44 -1
- 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/calendar-tab.tsx +1 -1
- package/src/plugins/builtin/account-management/pane.tsx +17 -13
- package/src/plugins/builtin/alerts/cli.ts +72 -11
- package/src/plugins/builtin/attention/headless.ts +19 -10
- package/src/plugins/builtin/attention/model.ts +22 -0
- package/src/plugins/builtin/awards/headless.ts +5 -1
- package/src/plugins/builtin/bond-calculator/pane.tsx +2 -1
- package/src/plugins/builtin/catalysts/headless.ts +5 -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/controller/connections.ts +18 -0
- package/src/plugins/builtin/chat/controller/fetch.ts +5 -2
- package/src/plugins/builtin/chat/controller/index.ts +49 -21
- package/src/plugins/builtin/chat/controller/notifications.ts +132 -72
- package/src/plugins/builtin/chat/controller/realtime.ts +37 -1
- 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/model.ts +20 -0
- package/src/plugins/builtin/cot/pane.tsx +2 -2
- package/src/plugins/builtin/crypto-board/model.ts +7 -3
- package/src/plugins/builtin/custom-view/pane.tsx +2 -2
- 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 +19 -2
- 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 +33 -6
- package/src/plugins/builtin/futures/model.ts +69 -5
- package/src/plugins/builtin/futures/series.ts +3 -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/jobs/pane.tsx +12 -9
- package/src/plugins/builtin/market-heatmap/index.tsx +6 -15
- 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/news/headless.ts +4 -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/plugin-marketplace/pane.tsx +1 -1
- 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 +4 -12
- 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/build-assets.ts +17 -4
- package/src/renderers/dom/desktop/controls.tsx +2 -0
- package/src/renderers/dom/styles.css +6 -0
- package/src/renderers/electrobun/bun/desktop/macos-dock-badge.ts +4 -3
- package/src/renderers/electrobun/bun/desktop/macos-dock-icon.ts +2 -3
- package/src/renderers/electrobun/bun/desktop/macos-objc.ts +17 -6
- package/src/sources/gloomberb-cloud/normalizers.ts +13 -4
- 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 +24 -5
- 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/ui/resize-settle.ts +128 -0
- package/src/utils/calendar-date.ts +56 -0
- package/src/utils/decimal-comma.ts +31 -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/futures-generic.ts +25 -4
- package/src/utils/markdown.ts +180 -0
- package/src/utils/market-capitalization.ts +22 -0
- package/src/utils/plugin-proxy-hosts.json +5 -0
- package/src/utils/utc-time.ts +14 -3
|
@@ -60,7 +60,7 @@ export type { TabsProps } from "./tabs";
|
|
|
60
60
|
export { SegmentedControl } from "./toggle";
|
|
61
61
|
export type { SegmentedControlProps } from "./toggle";
|
|
62
62
|
export { EmptyState, PaneStatusBody, Notice, loadingText, unavailableText } from "./status";
|
|
63
|
-
export type { EmptyStateProps, PaneStatusBodyProps, NoticeProps } from "./status";
|
|
63
|
+
export type { EmptyStateProps, PaneStatusBodyProps, NoticeProps, StatusRetry } from "./status";
|
|
64
64
|
export { Badge, BulletList, Divider, FigureList, KeyValueRow, Prose, READING_WIDTH, Section, SectionHeading } from "./display";
|
|
65
65
|
export type {
|
|
66
66
|
BadgeProps, BulletListProps, DividerProps, FigureListItem, FigureListProps, KeyValueRowProps, ProseProps, SectionProps,
|
|
@@ -1,21 +1,153 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { useEffect, useReducer, useRef, useState, type ReactNode } from "react";
|
|
2
2
|
import { t, tf } from "../../i18n";
|
|
3
3
|
import { useThemeColors } from "../../theme/theme-context";
|
|
4
4
|
import { Box, Text, useUiCapabilities } from "../../ui";
|
|
5
|
+
import { formatRelativeAge } from "../../utils/datetime-format";
|
|
5
6
|
import { Spinner } from "./loading";
|
|
6
7
|
import { ButtonActionScope } from "./action-scope";
|
|
8
|
+
import { Button } from "./button";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Asking again from an empty or failed state. The state draws the button
|
|
12
|
+
* (first, so it answers Enter), keeps it busy while the check runs, and when
|
|
13
|
+
* the answer comes back the same, says when it checked: otherwise a press that
|
|
14
|
+
* changes nothing on screen looks like a press that did nothing.
|
|
15
|
+
*/
|
|
16
|
+
export interface StatusRetry {
|
|
17
|
+
/** Asks again. A returned promise keeps the button busy until it settles. */
|
|
18
|
+
onRetry: () => unknown;
|
|
19
|
+
/** "Retry", unless the check has a better verb, such as "Look again". */
|
|
20
|
+
label?: string;
|
|
21
|
+
/** The label while it runs: "Retrying..." for the default label. */
|
|
22
|
+
pendingLabel?: string;
|
|
23
|
+
/**
|
|
24
|
+
* The same check running from anywhere (the button, `r`, a timer), such as
|
|
25
|
+
* a resource's `loading`. Needed when the old answer stays on screen while
|
|
26
|
+
* it runs, as an uncovered company does.
|
|
27
|
+
*/
|
|
28
|
+
pending?: boolean;
|
|
29
|
+
/** Said after "Checked just now." when an empty state comes back the same, such as "Still no careers system." */
|
|
30
|
+
unchanged?: string;
|
|
31
|
+
}
|
|
7
32
|
|
|
8
33
|
export interface EmptyStateProps {
|
|
9
34
|
title: string;
|
|
10
35
|
message?: string;
|
|
11
36
|
hint?: string;
|
|
12
37
|
actions?: ReactNode;
|
|
38
|
+
retry?: StatusRetry;
|
|
13
39
|
status?: "empty" | "error";
|
|
14
40
|
}
|
|
15
41
|
|
|
16
|
-
|
|
42
|
+
type ShownStatus = "empty" | "error" | null;
|
|
43
|
+
|
|
44
|
+
interface RetryFeedback {
|
|
45
|
+
pending: boolean;
|
|
46
|
+
press: () => void;
|
|
47
|
+
/** When a check ended on the state it started from; null once anything else is shown. */
|
|
48
|
+
checkedAt: number | null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isThenable(value: unknown): value is PromiseLike<unknown> {
|
|
52
|
+
return !!value && typeof (value as PromiseLike<unknown>).then === "function";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A check is the button's own promise, `retry.pending`, or the body showing
|
|
57
|
+
* its spinner (`shown === "loading"`) once a retry has been offered. When one
|
|
58
|
+
* ends on the empty or failed state it started from, the answer did not change.
|
|
59
|
+
*/
|
|
60
|
+
function useRetryFeedback(retry: StatusRetry | undefined, shown: ShownStatus | "loading"): RetryFeedback {
|
|
61
|
+
const [, rerender] = useReducer((count: number) => count + 1, 0);
|
|
62
|
+
const [checkedAt, setCheckedAt] = useState<number | null>(null);
|
|
63
|
+
const inFlight = useRef(0);
|
|
64
|
+
const mounted = useRef(true);
|
|
65
|
+
const track = useRef<{ offered: boolean; checking: boolean; from: ShownStatus; last: ShownStatus }>({
|
|
66
|
+
offered: false, checking: false, from: null, last: null,
|
|
67
|
+
});
|
|
68
|
+
if (retry) track.current.offered = true;
|
|
69
|
+
const pending = inFlight.current > 0 || retry?.pending === true;
|
|
70
|
+
const checking = pending || shown === "loading";
|
|
71
|
+
const begin = () => {
|
|
72
|
+
const state = track.current;
|
|
73
|
+
if (state.checking) return;
|
|
74
|
+
state.checking = true;
|
|
75
|
+
state.from = state.last;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
mounted.current = true;
|
|
80
|
+
return () => { mounted.current = false; };
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
// After every commit: a check that just ended either confirms the state on
|
|
84
|
+
// screen or clears the line, and anything else shown clears it. A body that
|
|
85
|
+
// never offered a retry skips all of it.
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
const state = track.current;
|
|
88
|
+
if (!state.offered) return;
|
|
89
|
+
if (checking) {
|
|
90
|
+
begin();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
// `shown` is not "loading" here: that counts as checking.
|
|
94
|
+
if (state.checking) {
|
|
95
|
+
state.checking = false;
|
|
96
|
+
setCheckedAt(shown !== null && shown === state.from ? Date.now() : null);
|
|
97
|
+
} else if (shown !== state.last) {
|
|
98
|
+
setCheckedAt(null);
|
|
99
|
+
}
|
|
100
|
+
state.last = shown;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// "just now" turns into "2m ago" while the line stays up.
|
|
104
|
+
const shownCheckedAt = checking ? null : checkedAt;
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (shownCheckedAt === null) return;
|
|
107
|
+
const timer = setInterval(rerender, 30_000);
|
|
108
|
+
return () => clearInterval(timer);
|
|
109
|
+
}, [shownCheckedAt]);
|
|
110
|
+
|
|
111
|
+
const press = () => {
|
|
112
|
+
if (!retry || pending) return;
|
|
113
|
+
const result = retry.onRetry();
|
|
114
|
+
if (!isThenable(result)) return;
|
|
115
|
+
begin();
|
|
116
|
+
inFlight.current += 1;
|
|
117
|
+
rerender();
|
|
118
|
+
const done = () => {
|
|
119
|
+
inFlight.current -= 1;
|
|
120
|
+
if (mounted.current) rerender();
|
|
121
|
+
};
|
|
122
|
+
result.then(done, done);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return { pending, press, checkedAt: shownCheckedAt };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function retryNote(retry: StatusRetry, status: "empty" | "error", checkedAt: number): string {
|
|
129
|
+
const age = formatRelativeAge(checkedAt, Math.max(checkedAt, Date.now()));
|
|
130
|
+
if (status === "error") return tf("Tried again {age}.", { age });
|
|
131
|
+
const checked = tf("Checked {age}.", { age });
|
|
132
|
+
return retry.unchanged ? `${checked} ${t(retry.unchanged)}` : checked;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function EmptyState(props: EmptyStateProps) {
|
|
136
|
+
const feedback = useRetryFeedback(props.retry, props.retry ? props.status ?? "empty" : null);
|
|
137
|
+
return <EmptyStateBody {...props} feedback={feedback} />;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function EmptyStateBody({ title, message, hint, actions, retry, status = "empty", feedback }: EmptyStateProps & { feedback: RetryFeedback }) {
|
|
17
141
|
const colors = useThemeColors();
|
|
18
142
|
const { nativePaneChrome } = useUiCapabilities();
|
|
143
|
+
const retryButton = retry ? (
|
|
144
|
+
<Button
|
|
145
|
+
label={retry.label ?? "Retry"}
|
|
146
|
+
pendingLabel={retry.pendingLabel ?? (retry.label ? undefined : "Retrying...")}
|
|
147
|
+
pending={feedback.pending}
|
|
148
|
+
onPress={feedback.press}
|
|
149
|
+
/>
|
|
150
|
+
) : null;
|
|
19
151
|
// Provider messages must wrap at narrow pane widths rather than lose their tail.
|
|
20
152
|
return (
|
|
21
153
|
<Box flexDirection="column" data-gloom-status={status} data-gloom-ui="empty-state">
|
|
@@ -23,11 +155,14 @@ export function EmptyState({ title, message, hint, actions, status = "empty" }:
|
|
|
23
155
|
{message && <Box><Text fg={colors.textMuted} wrapText>{t(message)}</Text></Box>}
|
|
24
156
|
{hint && <Box><Text fg={colors.textMuted} wrapText>{t(hint)}</Text></Box>}
|
|
25
157
|
{/* Wraps in a narrow pane rather than clipping the second action; a terminal wraps without a blank row. */}
|
|
26
|
-
{actions && (
|
|
158
|
+
{(retryButton || actions) && (
|
|
27
159
|
<Box flexDirection="row" flexWrap="wrap" gap={1} rowGap={nativePaneChrome ? undefined : 0} marginTop={1}>
|
|
28
|
-
<ButtonActionScope>{actions}</ButtonActionScope>
|
|
160
|
+
<ButtonActionScope>{retryButton}{actions}</ButtonActionScope>
|
|
29
161
|
</Box>
|
|
30
162
|
)}
|
|
163
|
+
{retry && feedback.checkedAt !== null && (
|
|
164
|
+
<Box><Text fg={colors.textMuted} wrapText>{retryNote(retry, status, feedback.checkedAt)}</Text></Box>
|
|
165
|
+
)}
|
|
31
166
|
</Box>
|
|
32
167
|
);
|
|
33
168
|
}
|
|
@@ -68,6 +203,8 @@ export interface PaneStatusBodyProps {
|
|
|
68
203
|
emptyTitle?: string;
|
|
69
204
|
emptyMessage?: string;
|
|
70
205
|
actions?: ReactNode;
|
|
206
|
+
/** Asking again from the error or empty state; see StatusRetry. */
|
|
207
|
+
retry?: StatusRetry;
|
|
71
208
|
align?: "start" | "center";
|
|
72
209
|
width?: number;
|
|
73
210
|
height?: number;
|
|
@@ -85,12 +222,16 @@ export function PaneStatusBody({
|
|
|
85
222
|
emptyTitle,
|
|
86
223
|
emptyMessage,
|
|
87
224
|
actions,
|
|
225
|
+
retry,
|
|
88
226
|
align = "start",
|
|
89
227
|
width,
|
|
90
228
|
height,
|
|
91
229
|
children,
|
|
92
230
|
}: PaneStatusBodyProps) {
|
|
93
231
|
const status = error ? "error" : loading ? "loading" : empty ? "empty" : null;
|
|
232
|
+
// Here rather than in the state, so a retry that shows the spinner while it
|
|
233
|
+
// runs still says it tried once the same failure is back.
|
|
234
|
+
const feedback = useRetryFeedback(retry, status);
|
|
94
235
|
if (!status) return <>{children}</>;
|
|
95
236
|
// Keyed so content that starts with a bare Box never reuses this padded node.
|
|
96
237
|
return (
|
|
@@ -107,11 +248,13 @@ export function PaneStatusBody({
|
|
|
107
248
|
data-gloom-ui="pane-status"
|
|
108
249
|
>
|
|
109
250
|
{status === "loading" ? <Spinner label={loadingLabel ?? loadingText(subject)} /> : (
|
|
110
|
-
<
|
|
251
|
+
<EmptyStateBody
|
|
111
252
|
status={status}
|
|
112
253
|
title={status === "error" ? errorTitle ?? (subject ? unavailableText(subject) : error!) : emptyTitle ?? t("Nothing to show yet.")}
|
|
113
254
|
message={status === "error" ? (errorTitle || subject ? error! : undefined) : emptyMessage}
|
|
114
255
|
actions={actions}
|
|
256
|
+
retry={retry}
|
|
257
|
+
feedback={feedback}
|
|
115
258
|
/>
|
|
116
259
|
)}
|
|
117
260
|
</Box>
|
|
@@ -45,7 +45,7 @@ export class MemoryResourceStore {
|
|
|
45
45
|
this.delete(key);
|
|
46
46
|
return null;
|
|
47
47
|
}
|
|
48
|
-
if (options.touch !== false) {
|
|
48
|
+
if (options.touch !== false && (options.touchIntervalMs == null || Date.now() - record.lastAccessedAt >= options.touchIntervalMs)) {
|
|
49
49
|
record.lastAccessedAt = Date.now();
|
|
50
50
|
}
|
|
51
51
|
record.stale = isStale(record);
|
|
@@ -63,6 +63,13 @@ export interface GetResourceOptions {
|
|
|
63
63
|
schemaVersion?: number;
|
|
64
64
|
allowExpired?: boolean;
|
|
65
65
|
touch?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* A read refreshes a row's place in the eviction order, one write per row. A
|
|
68
|
+
* row read many times a second (a quote, a price history) asks for it at most
|
|
69
|
+
* once per this many ms, so it stays recent without a write on every read.
|
|
70
|
+
* `touch: false` never refreshes it, and a row read that way looks old.
|
|
71
|
+
*/
|
|
72
|
+
touchIntervalMs?: number;
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
export interface ListResourceOptions extends GetResourceOptions {
|
|
@@ -141,6 +148,10 @@ export class ResourceStore {
|
|
|
141
148
|
});
|
|
142
149
|
}
|
|
143
150
|
|
|
151
|
+
private touchDue(record: { lastAccessedAt: number }, options: GetResourceOptions): boolean {
|
|
152
|
+
return options.touchIntervalMs == null || Date.now() - record.lastAccessedAt >= options.touchIntervalMs;
|
|
153
|
+
}
|
|
154
|
+
|
|
144
155
|
private touch(key: ResourceCacheKey): number | null {
|
|
145
156
|
const touchedAt = Date.now();
|
|
146
157
|
const result = trySqliteBusyOperation("touch cached resource", () => {
|
|
@@ -283,7 +294,7 @@ export class ResourceStore {
|
|
|
283
294
|
return null;
|
|
284
295
|
}
|
|
285
296
|
|
|
286
|
-
if (options.touch !== false) {
|
|
297
|
+
if (options.touch !== false && this.touchDue(record, options)) {
|
|
287
298
|
const touchedAt = this.touch(key);
|
|
288
299
|
if (touchedAt != null) {
|
|
289
300
|
record.lastAccessedAt = touchedAt;
|
|
@@ -340,6 +351,7 @@ export class ResourceStore {
|
|
|
340
351
|
}
|
|
341
352
|
if (options.touch !== false) {
|
|
342
353
|
for (const record of records) {
|
|
354
|
+
if (!this.touchDue(record, options)) continue;
|
|
343
355
|
const touchedAt = this.touch(record);
|
|
344
356
|
if (touchedAt != null) record.lastAccessedAt = touchedAt;
|
|
345
357
|
}
|
package/src/docs/snapshot.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"format": 1,
|
|
3
3
|
"site": "https://gloom.sh",
|
|
4
|
-
"updatedAt": "2026-10-
|
|
4
|
+
"updatedAt": "2026-10-14",
|
|
5
5
|
"categories": [
|
|
6
6
|
{
|
|
7
7
|
"id": "research",
|
|
@@ -618,7 +618,7 @@
|
|
|
618
618
|
"code": "NI",
|
|
619
619
|
"name": "Topic news",
|
|
620
620
|
"description": "News by topic code or sector",
|
|
621
|
-
"detail": "Market news by topic code (MNA, CB, ENERGY, REG, CRYPTO, EARN, IPO) or by sector.",
|
|
621
|
+
"detail": "Market news by topic code (MNA, CB, ENERGY, REG, CRYPTO, EARN, IPO) or by sector. LTR lists public fund and investor letters as links to the publisher's page.",
|
|
622
622
|
"category": "news",
|
|
623
623
|
"arg": "ENERGY",
|
|
624
624
|
"shot": "https://gloom.sh/screenshots/fn/NI.png",
|
|
@@ -777,8 +777,8 @@
|
|
|
777
777
|
{
|
|
778
778
|
"code": "FUT",
|
|
779
779
|
"name": "Futures board",
|
|
780
|
-
"description": "
|
|
781
|
-
"detail": "
|
|
780
|
+
"description": "Most active futures",
|
|
781
|
+
"detail": "Each root's most active contract across equity index, rates, energy, metals, agriculture, and FX with last price, unit and session change, grouped by sector, naming the nearer month while it still trades.",
|
|
782
782
|
"category": "markets",
|
|
783
783
|
"shot": "https://gloom.sh/screenshots/fn/FUT.png",
|
|
784
784
|
"lesson": "/academy/futures-commodities-energy/futures-from-zero"
|
|
@@ -816,7 +816,7 @@
|
|
|
816
816
|
"code": "POWER",
|
|
817
817
|
"name": "Power and grid buildout",
|
|
818
818
|
"description": "Grid queues and large loads",
|
|
819
|
-
"detail": "Grid interconnection queues, datacentre and other large-load requests, generation capacity and utility exposure, with a link to the source for every project. Pro shows the full data and history; Free shows three rows per section. EPWR opens the Prices tab, free: day-ahead power prices for 32 European markets in EUR/MWh and the EU carbon (EUA) auction clearing price in EUR/tCO2, each with its daily history.",
|
|
819
|
+
"detail": "Grid interconnection queues, datacentre and other large-load requests, generation capacity and utility exposure, with a link to the source for every project. Pro shows the full data and history; Free shows three rows per section. EPWR opens the Prices tab, free: day-ahead power prices for 32 European markets in EUR/MWh and the EU carbon (EUA) auction clearing price in EUR/tCO2, each with its daily history. EPWR GAS shows European gas storage from GIE AGSI+: % full, TWh and daily injection and withdrawal for the EU and each member state, against a year ago and the five-year range.",
|
|
820
820
|
"category": "markets",
|
|
821
821
|
"arg": "NEE",
|
|
822
822
|
"access": "pro",
|
|
@@ -6329,7 +6329,7 @@
|
|
|
6329
6329
|
"developer api",
|
|
6330
6330
|
"market news"
|
|
6331
6331
|
],
|
|
6332
|
-
"updatedAt": "2026-10-
|
|
6332
|
+
"updatedAt": "2026-10-14",
|
|
6333
6333
|
"blocks": [
|
|
6334
6334
|
{
|
|
6335
6335
|
"type": "heading",
|
|
@@ -6360,7 +6360,7 @@
|
|
|
6360
6360
|
[
|
|
6361
6361
|
"Free key",
|
|
6362
6362
|
"Free",
|
|
6363
|
-
"Delayed 12 hours."
|
|
6363
|
+
"Delayed 12 hours. Real-time with a paid [Gloom Datasets](/docs/datasets) plan."
|
|
6364
6364
|
],
|
|
6365
6365
|
[
|
|
6366
6366
|
"Unlimited",
|
|
@@ -6380,7 +6380,7 @@
|
|
|
6380
6380
|
},
|
|
6381
6381
|
{
|
|
6382
6382
|
"type": "paragraph",
|
|
6383
|
-
"text": "Every `/v1/news` request requires a News API key. Use either header style:"
|
|
6383
|
+
"text": "Every `/v1/news` request requires a News API key or a [Gloom Datasets](/docs/datasets) key (`gds_`). Use either header style:"
|
|
6384
6384
|
},
|
|
6385
6385
|
{
|
|
6386
6386
|
"type": "code",
|
|
@@ -6595,7 +6595,7 @@
|
|
|
6595
6595
|
"cftc",
|
|
6596
6596
|
"cot"
|
|
6597
6597
|
],
|
|
6598
|
-
"updatedAt": "2026-10-
|
|
6598
|
+
"updatedAt": "2026-10-14",
|
|
6599
6599
|
"blocks": [
|
|
6600
6600
|
{
|
|
6601
6601
|
"type": "heading",
|
|
@@ -6620,7 +6620,7 @@
|
|
|
6620
6620
|
"items": [
|
|
6621
6621
|
"**Alternative data:** GPU rental prices, supply chain, hiring postings and momentum, social mentions, app attention, power and grid, perpetuals, catalysts and litigation, research attention, congressional trades, operating exposure.",
|
|
6622
6622
|
"**Filings and company text:** KPIs and guidance, executive pay, risk factors, 8-K events, transcripts, credit documents, debt maturities, revenue breakdown, SEC filings, insiders and 13F, document search.",
|
|
6623
|
-
"**News:**
|
|
6623
|
+
"**News:** Real-time stories, sources and topics.",
|
|
6624
6624
|
"**Macro and positioning:** CPI, EIA inventories, COT, yield curves and macro series, central bank rates, short volume and interest, CDS, government awards.",
|
|
6625
6625
|
"**Market data and fundamentals:** Quotes and history, financial statements, statistics, profiles, earnings, estimates, IPOs, M&A, screener, options and gamma positioning, futures, rate path, crypto, session tape."
|
|
6626
6626
|
]
|
|
@@ -6644,7 +6644,7 @@
|
|
|
6644
6644
|
},
|
|
6645
6645
|
{
|
|
6646
6646
|
"type": "paragraph",
|
|
6647
|
-
"text": "They are different keys from the Gloom API's. A Gloom Pro key (`gloom_mcp_`) or a News API key (`gloom_news_`) gets `401 wrong_key` on a dataset route, and a `gds_` key gets `401` on the Gloom API's own routes, such as the [REST API](/docs/rest-api)
|
|
6647
|
+
"text": "They are different keys from the Gloom API's. A Gloom Pro key (`gloom_mcp_`) or a News API key (`gloom_news_`) gets `401 wrong_key` on a dataset route, and a `gds_` key gets `401` on the Gloom API's own routes, such as the [REST API](/docs/rest-api), but reads the [News API](/docs/news-api) at no credit cost."
|
|
6648
6648
|
},
|
|
6649
6649
|
{
|
|
6650
6650
|
"type": "heading",
|
|
@@ -7303,9 +7303,9 @@
|
|
|
7303
7303
|
{
|
|
7304
7304
|
"url": "/academy/futures-commodities-energy/futures-from-zero",
|
|
7305
7305
|
"title": "Futures from zero (FUT)",
|
|
7306
|
-
"summary": "A futures contract is a standard, exchange-traded promise to buy or sell a set quantity on a set month at a price agreed today, settled with daily margin, and FUT shows the
|
|
7306
|
+
"summary": "A futures contract is a standard, exchange-traded promise to buy or sell a set quantity on a set month at a price agreed today, settled with daily margin, and FUT shows the most active months across markets.",
|
|
7307
7307
|
"chapter": "Futures, commodities and energy",
|
|
7308
|
-
"minutes":
|
|
7308
|
+
"minutes": 5
|
|
7309
7309
|
},
|
|
7310
7310
|
{
|
|
7311
7311
|
"url": "/academy/futures-commodities-energy/curves-and-rolling",
|
package/src/i18n/es.ts
CHANGED
|
@@ -982,6 +982,7 @@ export const es: Record<string, string> = {
|
|
|
982
982
|
"Update ready, restart to apply": "Actualización lista; reinicia para aplicar",
|
|
983
983
|
"Update ready": "Actualización lista",
|
|
984
984
|
"Restart": "Reiniciar",
|
|
985
|
+
"Restart to apply v{version}": "Reinicia para aplicar v{version}",
|
|
985
986
|
"Checking for updates...": "Buscando actualizaciones...",
|
|
986
987
|
"Downloading v{version}: {percent}%": "Descargando v{version}: {percent}%",
|
|
987
988
|
"Update failed: {error}": "Error al actualizar: {error}",
|
package/src/i18n/ja.ts
CHANGED
|
@@ -996,6 +996,7 @@ export const ja: Record<string, string> = {
|
|
|
996
996
|
"Update ready, restart to apply": "アップデートの準備ができました。適用するには再起動してください",
|
|
997
997
|
"Update ready": "アップデートの準備完了",
|
|
998
998
|
"Restart": "再起動",
|
|
999
|
+
"Restart to apply v{version}": "再起動して v{version} を適用",
|
|
999
1000
|
"Checking for updates...": "アップデートを確認中...",
|
|
1000
1001
|
"Downloading v{version}: {percent}%": "v{version} をダウンロード中: {percent}%",
|
|
1001
1002
|
"Update failed: {error}": "更新に失敗しました: {error}",
|
package/src/i18n/ko.ts
CHANGED
|
@@ -994,6 +994,7 @@ export const ko: Record<string, string> = {
|
|
|
994
994
|
"Update ready, restart to apply": "업데이트 준비 완료. 적용하려면 다시 시작하세요",
|
|
995
995
|
"Update ready": "업데이트 준비 완료",
|
|
996
996
|
"Restart": "다시 시작",
|
|
997
|
+
"Restart to apply v{version}": "다시 시작하여 v{version} 적용",
|
|
997
998
|
"Checking for updates...": "업데이트 확인 중...",
|
|
998
999
|
"Downloading v{version}: {percent}%": "v{version} 다운로드 중: {percent}%",
|
|
999
1000
|
"Update failed: {error}": "업데이트 실패: {error}",
|
package/src/i18n/zh-cn.ts
CHANGED
|
@@ -996,6 +996,7 @@ export const zhCN: Record<string, string> = {
|
|
|
996
996
|
"Update ready, restart to apply": "更新已就绪,重启后生效",
|
|
997
997
|
"Update ready": "更新已就绪",
|
|
998
998
|
"Restart": "重启",
|
|
999
|
+
"Restart to apply v{version}": "重启以应用 v{version}",
|
|
999
1000
|
"Checking for updates...": "正在检查更新...",
|
|
1000
1001
|
"Downloading v{version}: {percent}%": "正在下载 v{version}:{percent}%",
|
|
1001
1002
|
"Update failed: {error}": "更新失败:{error}",
|
package/src/i18n/zh-tw.ts
CHANGED
|
@@ -996,6 +996,7 @@ export const zhTW: Record<string, string> = {
|
|
|
996
996
|
"Update ready, restart to apply": "更新已就緒,重新啟動後生效",
|
|
997
997
|
"Update ready": "更新已就緒",
|
|
998
998
|
"Restart": "重新啟動",
|
|
999
|
+
"Restart to apply v{version}": "重新啟動以套用 v{version}",
|
|
999
1000
|
"Checking for updates...": "正在檢查更新...",
|
|
1000
1001
|
"Downloading v{version}: {percent}%": "正在下載 v{version}:{percent}%",
|
|
1001
1002
|
"Update failed: {error}": "更新失敗:{error}",
|
|
@@ -193,7 +193,7 @@ function DiscoverStatus({ controller }: { controller: LayoutGalleryController })
|
|
|
193
193
|
<EmptyState
|
|
194
194
|
title={discover.state.error}
|
|
195
195
|
status="error"
|
|
196
|
-
|
|
196
|
+
retry={{ onRetry: discover.refresh }}
|
|
197
197
|
/>
|
|
198
198
|
</SidebarState>
|
|
199
199
|
);
|
|
@@ -219,7 +219,7 @@ function TeamStatus({ controller }: { controller: LayoutGalleryController }) {
|
|
|
219
219
|
<EmptyState
|
|
220
220
|
title={state.error}
|
|
221
221
|
status="error"
|
|
222
|
-
|
|
222
|
+
retry={{ onRetry: refresh }}
|
|
223
223
|
/>
|
|
224
224
|
</SidebarState>
|
|
225
225
|
);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export type FuturesSector =
|
|
2
2
|
| "equity-index"
|
|
3
|
+
| "equity-sectors"
|
|
3
4
|
| "rates"
|
|
4
5
|
| "energy"
|
|
5
6
|
| "metals"
|
|
@@ -38,7 +39,10 @@ export interface FuturesContract {
|
|
|
38
39
|
* unset: the security keeps the code and the future is its `=F` symbol.
|
|
39
40
|
* Checked with `gloomberb quote <code>` signed out on 2026-10-11: each of
|
|
40
41
|
* these was "Not a ticker", or for ZT and ZF a quote last updated in 2023 and
|
|
41
|
-
* 2021 that search no longer lists.
|
|
42
|
+
* 2021 that search no longer lists. The sector, index and energy rows were
|
|
43
|
+
* checked the same way on 2026-10-14: XAR is an ETF, XAU Goldmoney, SXI
|
|
44
|
+
* Standex, SXT Sensient, RX BioSyent, EMD a closed-end fund, SOX the PHLX
|
|
45
|
+
* index, and RS1, MES, MYM, M2K, MCL, MNG and HH are listed abroad.
|
|
42
46
|
*/
|
|
43
47
|
bareCode?: true;
|
|
44
48
|
}
|
|
@@ -58,6 +62,37 @@ export const FUTURES_CONTRACTS: FuturesContract[] = [
|
|
|
58
62
|
{ symbol: "NQ=F", code: "NQ", name: "E-Mini Nasdaq 100", sector: "equity-index", venue: "CME", tick: 0.25, bareCode: true },
|
|
59
63
|
{ symbol: "YM=F", code: "YM", name: "E-Mini Dow", sector: "equity-index", venue: "CBT", tick: 1, bareCode: true },
|
|
60
64
|
{ symbol: "RTY=F", code: "RTY", name: "E-Mini Russell 2000", sector: "equity-index", venue: "CME", tick: 0.1, bareCode: true },
|
|
65
|
+
{ symbol: "EMD=F", code: "EMD", name: "E-Mini S&P MidCap 400", sector: "equity-index", venue: "CME", tick: 0.1 },
|
|
66
|
+
{ symbol: "RS1=F", code: "RS1", name: "E-Mini Russell 1000", sector: "equity-index", venue: "CME", tick: 0.1 },
|
|
67
|
+
{ symbol: "NKD=F", code: "NKD", name: "Nikkei 225 (USD)", sector: "equity-index", venue: "CME", tick: 5, bareCode: true },
|
|
68
|
+
// Quoted in yen: the same index points, each worth 500 yen, so the server has no dollar terms for it.
|
|
69
|
+
{ symbol: "NIY=F", code: "NIY", name: "Nikkei 225 (Yen)", sector: "equity-index", venue: "CME", tick: 5, unit: "index points", bareCode: true },
|
|
70
|
+
{ symbol: "MES=F", code: "MES", name: "Micro E-Mini S&P 500", sector: "equity-index", venue: "CME", tick: 0.25 },
|
|
71
|
+
{ symbol: "MNQ=F", code: "MNQ", name: "Micro E-Mini Nasdaq 100", sector: "equity-index", venue: "CME", tick: 0.25, bareCode: true },
|
|
72
|
+
{ symbol: "MYM=F", code: "MYM", name: "Micro E-Mini Dow", sector: "equity-index", venue: "CBT", tick: 1 },
|
|
73
|
+
{ symbol: "M2K=F", code: "M2K", name: "Micro E-Mini Russell 2000", sector: "equity-index", venue: "CME", tick: 0.1 },
|
|
74
|
+
|
|
75
|
+
// CME's E-mini S&P Select Sector futures (XA*) and Select Industry futures
|
|
76
|
+
// (SX*), named by their S&P index: each sector followed by its industries in
|
|
77
|
+
// GICS order, then the Dow Jones U.S. Real Estate future on CBOT.
|
|
78
|
+
{ symbol: "XAE=F", code: "XAE", name: "S&P Energy", sector: "equity-sectors", venue: "CME", tick: 0.1, bareCode: true },
|
|
79
|
+
{ symbol: "SXO=F", code: "SXO", name: "S&P Oil & Gas E&P", sector: "equity-sectors", venue: "CME", tick: 0.5, bareCode: true },
|
|
80
|
+
{ symbol: "XAB=F", code: "XAB", name: "S&P Materials", sector: "equity-sectors", venue: "CME", tick: 0.1, bareCode: true },
|
|
81
|
+
{ symbol: "XAI=F", code: "XAI", name: "S&P Industrial", sector: "equity-sectors", venue: "CME", tick: 0.1, bareCode: true },
|
|
82
|
+
{ symbol: "XAY=F", code: "XAY", name: "S&P Consumer Discretionary", sector: "equity-sectors", venue: "CME", tick: 0.1, bareCode: true },
|
|
83
|
+
{ symbol: "SXR=F", code: "SXR", name: "S&P Retail", sector: "equity-sectors", venue: "CME", tick: 1, bareCode: true },
|
|
84
|
+
{ symbol: "XAP=F", code: "XAP", name: "S&P Consumer Staples", sector: "equity-sectors", venue: "CME", tick: 0.1, bareCode: true },
|
|
85
|
+
{ symbol: "XAV=F", code: "XAV", name: "S&P Health Care", sector: "equity-sectors", venue: "CME", tick: 0.1, bareCode: true },
|
|
86
|
+
{ symbol: "SXT=F", code: "SXT", name: "S&P Biotechnology", sector: "equity-sectors", venue: "CME", tick: 1 },
|
|
87
|
+
{ symbol: "XAF=F", code: "XAF", name: "S&P Financial", sector: "equity-sectors", venue: "CME", tick: 0.05, bareCode: true },
|
|
88
|
+
{ symbol: "SXB=F", code: "SXB", name: "S&P Regional Banks", sector: "equity-sectors", venue: "CME", tick: 0.25, bareCode: true },
|
|
89
|
+
{ symbol: "SXI=F", code: "SXI", name: "S&P Insurance", sector: "equity-sectors", venue: "CME", tick: 0.5 },
|
|
90
|
+
{ symbol: "XAK=F", code: "XAK", name: "S&P Technology", sector: "equity-sectors", venue: "CME", tick: 0.1, bareCode: true },
|
|
91
|
+
{ symbol: "SOX=F", code: "SOX", name: "PHLX Semiconductor", sector: "equity-sectors", venue: "CME", tick: 0.5 },
|
|
92
|
+
{ symbol: "XAZ=F", code: "XAZ", name: "S&P Communication Services", sector: "equity-sectors", venue: "CME", tick: 0.05, bareCode: true },
|
|
93
|
+
{ symbol: "XAU=F", code: "XAU", name: "S&P Utilities", sector: "equity-sectors", venue: "CME", tick: 0.1 },
|
|
94
|
+
{ symbol: "XAR=F", code: "XAR", name: "S&P Real Estate", sector: "equity-sectors", venue: "CME", tick: 0.05 },
|
|
95
|
+
{ symbol: "RX=F", code: "RX", name: "Dow Jones U.S. Real Estate", sector: "equity-sectors", venue: "CBT", tick: 0.1 },
|
|
61
96
|
|
|
62
97
|
{ symbol: "ZT=F", code: "ZT", name: "2-Year T-Note", sector: "rates", venue: "CBT", bareCode: true },
|
|
63
98
|
{ symbol: "ZF=F", code: "ZF", name: "5-Year T-Note", sector: "rates", venue: "CBT", bareCode: true },
|
|
@@ -72,6 +107,12 @@ export const FUTURES_CONTRACTS: FuturesContract[] = [
|
|
|
72
107
|
{ symbol: "TTF=F", code: "TTF", name: "Dutch TTF Gas", sector: "energy", venue: "NYM", everyMonth: true, tick: 0.001, unit: "EUR / MWh", bareCode: true },
|
|
73
108
|
{ symbol: "RB=F", code: "RB", name: "RBOB Gasoline", sector: "energy", venue: "NYM", everyMonth: true, tick: 0.0001 },
|
|
74
109
|
{ symbol: "HO=F", code: "HO", name: "Heating Oil", sector: "energy", venue: "NYM", everyMonth: true, tick: 0.0001 },
|
|
110
|
+
// Cash settled on the CL and NG final settlements.
|
|
111
|
+
{ symbol: "QM=F", code: "QM", name: "E-Mini Crude Oil", sector: "energy", venue: "NYM", everyMonth: true, tick: 0.025, bareCode: true },
|
|
112
|
+
{ symbol: "MCL=F", code: "MCL", name: "Micro WTI Crude Oil", sector: "energy", venue: "NYM", everyMonth: true, tick: 0.01 },
|
|
113
|
+
{ symbol: "QG=F", code: "QG", name: "E-Mini Natural Gas", sector: "energy", venue: "NYM", everyMonth: true, tick: 0.005, bareCode: true },
|
|
114
|
+
{ symbol: "MNG=F", code: "MNG", name: "Micro Henry Hub Natural Gas", sector: "energy", venue: "NYM", everyMonth: true, tick: 0.001 },
|
|
115
|
+
{ symbol: "HH=F", code: "HH", name: "Henry Hub Last Day Financial", sector: "energy", venue: "NYM", everyMonth: true, tick: 0.001 },
|
|
75
116
|
|
|
76
117
|
{ symbol: "GC=F", code: "GC", name: "Gold", sector: "metals", venue: "CMX", tick: 0.1 },
|
|
77
118
|
{ symbol: "SI=F", code: "SI", name: "Silver", sector: "metals", venue: "CMX", tick: 0.005 },
|
|
@@ -109,6 +150,7 @@ export const FUTURES_CONTRACTS: FuturesContract[] = [
|
|
|
109
150
|
|
|
110
151
|
export const FUTURES_SECTOR_LABELS: Record<FuturesSector, string> = {
|
|
111
152
|
"equity-index": "Equity Index",
|
|
153
|
+
"equity-sectors": "Equity Sectors",
|
|
112
154
|
rates: "Rates",
|
|
113
155
|
energy: "Energy",
|
|
114
156
|
metals: "Metals",
|
|
@@ -119,6 +161,7 @@ export const FUTURES_SECTOR_LABELS: Record<FuturesSector, string> = {
|
|
|
119
161
|
|
|
120
162
|
export const FUTURES_SECTOR_ORDER: FuturesSector[] = [
|
|
121
163
|
"equity-index",
|
|
164
|
+
"equity-sectors",
|
|
122
165
|
"rates",
|
|
123
166
|
"energy",
|
|
124
167
|
"metals",
|
|
@@ -128,6 +128,13 @@ export function isFuturesContractSpec(spec: unknown): spec is FuturesContractSpe
|
|
|
128
128
|
&& (row.settlement === "physical" || row.settlement === "cash");
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
function isNearestContract(value: unknown): boolean {
|
|
132
|
+
if (!value || typeof value !== "object") return false;
|
|
133
|
+
const row = value as Record<string, unknown>;
|
|
134
|
+
return typeof row.contract === "string" && typeof row.contractSymbol === "string"
|
|
135
|
+
&& typeof row.deliveryMonth === "string" && /^\d{4}-(0[1-9]|1[0-2])$/.test(row.deliveryMonth) && optionalDate(row.lastTrade);
|
|
136
|
+
}
|
|
137
|
+
|
|
131
138
|
export function validateFuturesContractTerms(data: FuturesContractTermsPayload): FuturesContractTermsPayload {
|
|
132
139
|
const settlement = data?.settlement as unknown;
|
|
133
140
|
if (!data || typeof data !== "object" || typeof data.root !== "string"
|
|
@@ -138,6 +145,9 @@ export function validateFuturesContractTerms(data: FuturesContractTermsPayload):
|
|
|
138
145
|
&& isDate((settlement as { date?: unknown }).date))) {
|
|
139
146
|
throw new Error("The server returned invalid futures contract terms");
|
|
140
147
|
}
|
|
148
|
+
// A nearest contract the server worded some other way is left out; the terms still stand.
|
|
149
|
+
const nearest = data.nearest as unknown;
|
|
150
|
+
if (nearest != null && !isNearestContract(nearest)) return { ...data, nearest: null };
|
|
141
151
|
return data;
|
|
142
152
|
}
|
|
143
153
|
|
|
@@ -428,7 +428,7 @@ export function formatMarketChangeWithCurrency(
|
|
|
428
428
|
referencePrice?: number,
|
|
429
429
|
): string {
|
|
430
430
|
if (value == null || !Number.isFinite(value)) return "—";
|
|
431
|
-
if (resolvePriceBasis(options.priceBasis, options.assetCategory) !== "per-unit") return formatSignedMarketPrice(value, options);
|
|
431
|
+
if (isIndexLevel(options) || resolvePriceBasis(options.priceBasis, options.assetCategory) !== "per-unit") return formatSignedMarketPrice(value, options);
|
|
432
432
|
if (options.fixedFractionDigits !== undefined) {
|
|
433
433
|
const maxWidth = options.maxWidth == null ? undefined : Math.max(1, options.maxWidth - 1);
|
|
434
434
|
const body = formatMarketPriceWithCurrency(Math.abs(value), currency, { ...options, maxWidth });
|
|
@@ -603,13 +603,18 @@ export function liveQuoteFormatOptions(
|
|
|
603
603
|
return withStablePriceDigits(quoteFormatOptions(quote, fallbackAssetCategory, metadataInstrumentType), currency, quote);
|
|
604
604
|
}
|
|
605
605
|
|
|
606
|
+
/** An index level is points, not money: ^GSPC at 7,805.45 has no currency to name, wherever it is shown. */
|
|
607
|
+
function isIndexLevel(options: MarketFormatOptions): boolean {
|
|
608
|
+
return options.assetCategory?.trim().toUpperCase() === "INDEX";
|
|
609
|
+
}
|
|
610
|
+
|
|
606
611
|
export function formatMarketPriceWithCurrency(
|
|
607
612
|
value: number | undefined,
|
|
608
613
|
currency = "USD",
|
|
609
614
|
options: MarketFormatOptions = {},
|
|
610
615
|
): string {
|
|
611
616
|
if (value === undefined || value === null || Number.isNaN(value)) return "—";
|
|
612
|
-
if (resolvePriceBasis(options.priceBasis, options.assetCategory) !== "per-unit") return formatMarketPrice(value, options);
|
|
617
|
+
if (isIndexLevel(options) || resolvePriceBasis(options.priceBasis, options.assetCategory) !== "per-unit") return formatMarketPrice(value, options);
|
|
613
618
|
const normalizedCurrency = currency.trim().toUpperCase() || "USD";
|
|
614
619
|
const sign = value < 0 ? "-" : "";
|
|
615
620
|
const symbol = getCurrencySymbol(normalizedCurrency);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MarketState } from "../../types/financials";
|
|
1
|
+
import type { MarketState, QuoteIndexSession } from "../../types/financials";
|
|
2
2
|
import { canonicalExchange, EXCHANGE_TIME_ZONES, isUsListingExchange } from "../../utils/exchanges";
|
|
3
3
|
import { hasPublishedApacCalendar, isPublishedApacClosure } from "../published-apac-sessions";
|
|
4
4
|
import { hasPublishedCnCalendar, isPublishedCnClosure } from "../published-cn-sessions";
|
|
@@ -675,3 +675,27 @@ function isTimestampStaleForExchangeSessionUnsafe(
|
|
|
675
675
|
}
|
|
676
676
|
return false;
|
|
677
677
|
}
|
|
678
|
+
|
|
679
|
+
function readSessionWindow(value: unknown): { start: number; end: number } | null {
|
|
680
|
+
const { start, end } = (value ?? {}) as { start?: unknown; end?: unknown };
|
|
681
|
+
return typeof start === "number" && typeof end === "number" && Number.isFinite(start) && Number.isFinite(end) && end > start
|
|
682
|
+
? { start, end }
|
|
683
|
+
: null;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* The regular sessions an index quote carries, read without trusting the
|
|
688
|
+
* shape it arrived in: its zone, the session its day follows and the latest
|
|
689
|
+
* ones. Null when the zone or the current session is missing.
|
|
690
|
+
*/
|
|
691
|
+
export function readIndexSession(value: unknown): QuoteIndexSession | null {
|
|
692
|
+
const { timeZone, current, regular } = (value ?? {}) as { timeZone?: unknown; current?: unknown; regular?: unknown };
|
|
693
|
+
const now = readSessionWindow(current);
|
|
694
|
+
if (typeof timeZone !== "string" || !timeZone.includes("/") || !now) return null;
|
|
695
|
+
return {
|
|
696
|
+
timeZone,
|
|
697
|
+
current: now,
|
|
698
|
+
// The latest two: the one a print after the close belongs to, and the one before it.
|
|
699
|
+
regular: Array.isArray(regular) ? regular.flatMap((window) => readSessionWindow(window) ?? []).slice(-2) : [],
|
|
700
|
+
};
|
|
701
|
+
}
|