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
|
@@ -48,6 +48,7 @@ import { EXTENDED_SESSION_LABELS, extendedSessionStateLabel, type ExtendedSessio
|
|
|
48
48
|
import { useOpenDescription } from "../shared/open-description";
|
|
49
49
|
import { useResearchCloudSession } from "../shared/research-cloud-session";
|
|
50
50
|
import { INITIAL_STREAM_RANGE, streamWindowRows } from "../shared/use-quote-board";
|
|
51
|
+
import { useSampledValue } from "../../../state/hooks/live-ticker-financials";
|
|
51
52
|
import { usePlanAccess } from "../../../api-client/plan-access";
|
|
52
53
|
import { SignInWall } from "../cloud/auth-actions";
|
|
53
54
|
import { CriterionEditor } from "./criterion-editor";
|
|
@@ -71,12 +72,14 @@ import {
|
|
|
71
72
|
screenRowId,
|
|
72
73
|
} from "./model";
|
|
73
74
|
import { useScreenResults } from "./results";
|
|
74
|
-
import { overlayLiveScreenRows, screenExtendedMove, screenExtendedSessions, type LiveScreenRow } from "./live";
|
|
75
|
+
import { overlayLiveScreenRows, screenExtendedMove, screenExtendedSessions, sortLiveScreenRows, type LiveScreenRow } from "./live";
|
|
75
76
|
import { useLiveQuoteEntries } from "../../../state/hooks/quote-streaming";
|
|
76
77
|
import { buildScreenerQuoteTargets } from "../../../market-data/quotes/screener-live-quotes";
|
|
77
78
|
import { useLiveStreamingSetting } from "../../../state/hooks/live-streaming";
|
|
78
79
|
import { getTableWidth } from "../../../components/ui/table-layout";
|
|
79
80
|
|
|
81
|
+
/** How often a live sort re-reads the figures it orders by. */
|
|
82
|
+
const LIVE_ORDER_SAMPLE_MS = 5_000;
|
|
80
83
|
const TABS = [
|
|
81
84
|
{ value: "results", label: "Results" },
|
|
82
85
|
{ value: "criteria", label: "Criteria" },
|
|
@@ -290,7 +293,23 @@ function EquityScreenView({
|
|
|
290
293
|
freshnessScopeKey: `equity-screener:${JSON.stringify(definition)}`,
|
|
291
294
|
liveStreaming,
|
|
292
295
|
});
|
|
293
|
-
const
|
|
296
|
+
const liveRows = useMemo(() => overlayLiveScreenRows(snapshotRows, liveEntries), [liveEntries, snapshotRows]);
|
|
297
|
+
// A sort on a field the quotes move is redone on the figures shown, so the
|
|
298
|
+
// column reads monotone. Re-sorting on every tick would reshuffle rows under
|
|
299
|
+
// the cursor; the order is taken from values sampled every few seconds,
|
|
300
|
+
// and at once when the screen or its pages change.
|
|
301
|
+
const orderRows = useSampledValue(
|
|
302
|
+
liveRows,
|
|
303
|
+
LIVE_ORDER_SAMPLE_MS,
|
|
304
|
+
`${data?.snapshot?.id ?? ""}:${JSON.stringify(definition.sort)}:${snapshotRows.length}`,
|
|
305
|
+
);
|
|
306
|
+
const rows = useMemo(() => {
|
|
307
|
+
const order = new Map(sortLiveScreenRows(orderRows, definition.sort).map((row, index) => [screenRowId(row), index]));
|
|
308
|
+
return liveRows
|
|
309
|
+
.map((row, index) => ({ row, index, place: order.get(screenRowId(row)) ?? Number.POSITIVE_INFINITY }))
|
|
310
|
+
.sort((a, b) => a.place - b.place || a.index - b.index)
|
|
311
|
+
.map(({ row }) => row);
|
|
312
|
+
}, [definition.sort, liveRows, orderRows]);
|
|
294
313
|
const opened = rows.find((row) => screenRowId(row) === openId);
|
|
295
314
|
const selected =
|
|
296
315
|
opened ?? rows.find((row) => screenRowId(row) === selectedId) ?? rows[0];
|
|
@@ -111,6 +111,7 @@ function SentimentPlot({
|
|
|
111
111
|
valueFormat,
|
|
112
112
|
updatedAt,
|
|
113
113
|
overlays,
|
|
114
|
+
focused = false,
|
|
114
115
|
}: {
|
|
115
116
|
rating: FearGreedRating;
|
|
116
117
|
points: FearGreedIndicator["points"];
|
|
@@ -118,6 +119,7 @@ function SentimentPlot({
|
|
|
118
119
|
valueFormat: FearGreedValueFormat;
|
|
119
120
|
updatedAt: Date | null;
|
|
120
121
|
overlays?: StaticChartOverlay[];
|
|
122
|
+
focused?: boolean;
|
|
121
123
|
}) {
|
|
122
124
|
const chartWidth = Math.max(24, width - 2);
|
|
123
125
|
const chartHeight = width >= 96 ? 12 : 10;
|
|
@@ -143,6 +145,7 @@ function SentimentPlot({
|
|
|
143
145
|
mode="line"
|
|
144
146
|
colors={palette}
|
|
145
147
|
overlays={overlays}
|
|
148
|
+
focused={focused}
|
|
146
149
|
showTimeAxis
|
|
147
150
|
timeAxisColor={colors.textDim}
|
|
148
151
|
yAxisColor={colors.textDim}
|
|
@@ -224,7 +227,7 @@ function ChartStats({
|
|
|
224
227
|
|
|
225
228
|
// The gauge above already shows the score, its rating and what the series is,
|
|
226
229
|
// so the history is the plot alone.
|
|
227
|
-
export function IndexHistoryChart({ data, width }: { data: FearGreedData; width: number }) {
|
|
230
|
+
export function IndexHistoryChart({ data, width, focused = false }: { data: FearGreedData; width: number; focused?: boolean }) {
|
|
228
231
|
return (
|
|
229
232
|
<Box flexDirection="column" paddingX={1}>
|
|
230
233
|
<SentimentPlot
|
|
@@ -233,6 +236,7 @@ export function IndexHistoryChart({ data, width }: { data: FearGreedData; width:
|
|
|
233
236
|
width={width}
|
|
234
237
|
valueFormat="score"
|
|
235
238
|
updatedAt={data.overall.updatedAt}
|
|
239
|
+
focused={focused}
|
|
236
240
|
/>
|
|
237
241
|
</Box>
|
|
238
242
|
);
|
|
@@ -91,7 +91,7 @@ export function FearGreedPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
91
91
|
segments={FEAR_GREED_GAUGE_SEGMENTS}
|
|
92
92
|
minWidth={isDesktopWeb ? Math.min(34, Math.max(1, width - 2)) : undefined}
|
|
93
93
|
/>
|
|
94
|
-
<IndexHistoryChart data={data} width={width} />
|
|
94
|
+
<IndexHistoryChart data={data} width={width} focused={focused} />
|
|
95
95
|
<Box paddingX={1} marginTop={2}>
|
|
96
96
|
<SectionHeading title="Indicators" />
|
|
97
97
|
</Box>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Button,
|
|
4
4
|
DataTableView,
|
|
@@ -28,6 +28,7 @@ import { useLiveStreamingSetting } from "../../../state/hooks/live-streaming";
|
|
|
28
28
|
import { quoteObservation } from "../shared/report-freshness";
|
|
29
29
|
import {
|
|
30
30
|
boardErrorMessage,
|
|
31
|
+
boardHeadlineQuote,
|
|
31
32
|
quoteBoardFooterInfo,
|
|
32
33
|
quoteBoardStatus,
|
|
33
34
|
useQuoteBoard,
|
|
@@ -59,6 +60,7 @@ import {
|
|
|
59
60
|
usesSessionText,
|
|
60
61
|
type FuturesColumn,
|
|
61
62
|
} from "./table";
|
|
63
|
+
import { useFuturesBoardTerms } from "./use-board-terms";
|
|
62
64
|
import { useFrontContractReturns } from "./use-front-returns";
|
|
63
65
|
|
|
64
66
|
const FUTURES_PANE_ID = "futures";
|
|
@@ -134,6 +136,8 @@ export function FuturesPane({ focused, width, height }: PaneProps) {
|
|
|
134
136
|
const paneVisible = usePaneVisible();
|
|
135
137
|
const showsReturns = columns.some((column) => FUTURES_RETURN_COLUMNS[column.id]);
|
|
136
138
|
const returns = useFrontContractReturns(listedContracts, quotes, dataProvider, showsReturns && paneVisible);
|
|
139
|
+
// The unit a price counts and the month a symbol rolled past, as `quote` reads them.
|
|
140
|
+
const terms = useFuturesBoardTerms(listedContracts, !!dataProvider && paneVisible);
|
|
137
141
|
|
|
138
142
|
const visibleCollapsed = effectiveCollapsedSectors(collapsedSectors, searchQuery);
|
|
139
143
|
const rows = useMemo(
|
|
@@ -141,15 +145,38 @@ export function FuturesPane({ focused, width, height }: PaneProps) {
|
|
|
141
145
|
query: searchQuery,
|
|
142
146
|
collapsed: collapsedSectors,
|
|
143
147
|
returns,
|
|
148
|
+
terms,
|
|
144
149
|
}),
|
|
145
|
-
[collapsedSectors, contractsBySector, quotes, returns, searchQuery, sortPreference],
|
|
150
|
+
[collapsedSectors, contractsBySector, quotes, returns, searchQuery, sortPreference, terms],
|
|
146
151
|
);
|
|
147
152
|
|
|
148
153
|
const sessionText = usesSessionText(width);
|
|
154
|
+
const quotesRef = useRef(quotes);
|
|
155
|
+
const returnsRef = useRef(returns);
|
|
156
|
+
const termsRef = useRef(terms);
|
|
157
|
+
quotesRef.current = quotes;
|
|
158
|
+
returnsRef.current = returns;
|
|
159
|
+
termsRef.current = terms;
|
|
149
160
|
const renderCell = useCallback((
|
|
150
161
|
row: FuturesTableRow,
|
|
151
162
|
column: FuturesColumn,
|
|
152
|
-
) => renderFuturesCell(row, column,
|
|
163
|
+
) => renderFuturesCell(row, column, quotesRef.current, { sessionText, returns: returnsRef.current, terms: termsRef.current }), [sessionText]);
|
|
164
|
+
// Everything a cell reads: the headline figures the board shows, the quote's
|
|
165
|
+
// other fields, whether the last good quote is held over, and the returns.
|
|
166
|
+
const getRowVersion = useCallback((row: FuturesTableRow) => {
|
|
167
|
+
if (row.type !== "row") return row.sector;
|
|
168
|
+
const state = quotes.get(row.contract.symbol);
|
|
169
|
+
const quote = state?.quote ? boardHeadlineQuote(state.quote) : null;
|
|
170
|
+
const values = returns.get(row.contract.symbol);
|
|
171
|
+
const contract = terms.get(row.contract.symbol);
|
|
172
|
+
return [
|
|
173
|
+
state?.loading, state?.stale,
|
|
174
|
+
quote?.price, quote?.change, quote?.changePercent, quote?.previousClose, quote?.volume, quote?.lastUpdated, quote?.marketState, quote?.symbol, quote?.name,
|
|
175
|
+
values?.loading, values?.values["1W"], values?.values["1M"], values?.values.YTD,
|
|
176
|
+
contract?.loading, contract?.terms?.spec?.unit, contract?.terms?.deliveryMonth, contract?.terms?.nearest?.contract,
|
|
177
|
+
contract?.terms?.nearest?.lastTrade,
|
|
178
|
+
].join(":");
|
|
179
|
+
}, [quotes, returns, terms]);
|
|
153
180
|
|
|
154
181
|
const toggleSector = useCallback((sector: FuturesSector) => {
|
|
155
182
|
setCollapsedSectors((current) => {
|
|
@@ -297,6 +324,7 @@ export function FuturesPane({ focused, width, height }: PaneProps) {
|
|
|
297
324
|
onVisibleRangeChange={setVisibleRange}
|
|
298
325
|
renderSectionHeader={renderSectorHeader}
|
|
299
326
|
renderCell={renderCell}
|
|
327
|
+
getRowVersion={getRowVersion}
|
|
300
328
|
selectedTextOverridesCellColor
|
|
301
329
|
reportEveryRow
|
|
302
330
|
emptyStateTitle={searchQuery.trim()
|
|
@@ -361,7 +389,7 @@ export const futuresModule: PluginModule = {
|
|
|
361
389
|
paneId: FUTURES_PANE_ID,
|
|
362
390
|
label: "Futures Board",
|
|
363
391
|
description:
|
|
364
|
-
"
|
|
392
|
+
"Each root's most active futures contract across equity index, rates, energy, metals, agriculture, livestock, and FX with last price, unit, session change, search, and collapsible sectors.",
|
|
365
393
|
keywords: [
|
|
366
394
|
"futures",
|
|
367
395
|
"commodities",
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import type { FuturesContractTermsPayload } from "../../../api-client/futures-curve";
|
|
1
2
|
import type { Quote } from "../../../types/financials";
|
|
2
3
|
import { compareSortValues, type SortDirection } from "../../../utils/sort-values";
|
|
3
4
|
import type { BoardQuoteMap } from "../shared/use-quote-board";
|
|
4
5
|
import { FUTURES_SECTOR_ORDER, type FuturesContract, type FuturesSector } from "../../../market-data/futures-board";
|
|
5
6
|
import type { FuturesReturnHorizon } from "./returns";
|
|
7
|
+
import type { FuturesTermsMap } from "./use-board-terms";
|
|
6
8
|
import type { FrontReturnsMap } from "./use-front-returns";
|
|
7
9
|
|
|
8
10
|
export type FuturesTableRow =
|
|
@@ -14,6 +16,7 @@ export type FuturesColumnId =
|
|
|
14
16
|
| "code"
|
|
15
17
|
| "name"
|
|
16
18
|
| "price"
|
|
19
|
+
| "unit"
|
|
17
20
|
| "change"
|
|
18
21
|
| "changePercent"
|
|
19
22
|
| "return1w"
|
|
@@ -67,6 +70,59 @@ export function quotedContractMonth(
|
|
|
67
70
|
return { year, month };
|
|
68
71
|
}
|
|
69
72
|
|
|
73
|
+
/** "2026-10-20" as "20 Oct". */
|
|
74
|
+
function dayMonth(date: string): string {
|
|
75
|
+
return `${Number(date.slice(8, 10))} ${MONTH_NAMES[Number(date.slice(5, 7)) - 1]}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The row's headline: the contract its quote prices, as futuresContractName
|
|
80
|
+
* words it. While the continuous symbol has rolled to the most active month
|
|
81
|
+
* and a nearer one still trades, the headline says so, with the nearer one's
|
|
82
|
+
* last trade: "WTI Crude Oil Dec 26, most active (Nov expires 20 Oct)". The
|
|
83
|
+
* terms only add that note when they name the month the quote prices, so a
|
|
84
|
+
* roll between the two loads never mixes months, and place the month when the
|
|
85
|
+
* quote's name carries none. A name column too narrow for the note keeps the
|
|
86
|
+
* plain name.
|
|
87
|
+
*/
|
|
88
|
+
export function futuresContractHeadline(
|
|
89
|
+
contract: FuturesContract,
|
|
90
|
+
quote?: Quote | null,
|
|
91
|
+
terms?: Pick<FuturesContractTermsPayload, "deliveryMonth" | "nearest"> | null,
|
|
92
|
+
width = Number.POSITIVE_INFINITY,
|
|
93
|
+
): string {
|
|
94
|
+
const quoted = quotedContractMonth(contract, quote);
|
|
95
|
+
const termsMonth = terms?.deliveryMonth && /^\d{4}-\d{2}$/.test(terms.deliveryMonth)
|
|
96
|
+
? { year: Number(terms.deliveryMonth.slice(0, 4)), month: Number(terms.deliveryMonth.slice(5, 7)) - 1 }
|
|
97
|
+
: null;
|
|
98
|
+
const nearest = terms?.nearest;
|
|
99
|
+
const agree = quoted == null || termsMonth == null || (quoted.year === termsMonth.year && quoted.month === termsMonth.month);
|
|
100
|
+
// A quote whose name carries no month (or a cut one) is placed by its terms.
|
|
101
|
+
const month = agree ? quoted ?? termsMonth : quoted;
|
|
102
|
+
if (!month) return contract.name;
|
|
103
|
+
const active = `${MONTH_NAMES[month.month]} ${String(month.year).slice(-2)}`;
|
|
104
|
+
const plain = `${contract.name} ${active}`;
|
|
105
|
+
if (!nearest || !agree || nearest.deliveryMonth >= `${month.year}-${String(month.month + 1).padStart(2, "0")}`) return plain;
|
|
106
|
+
const nearer = MONTH_NAMES[Number(nearest.deliveryMonth.slice(5, 7)) - 1];
|
|
107
|
+
const expiry = nearest.lastTrade ? `expires ${dayMonth(nearest.lastTrade)}` : "still trading";
|
|
108
|
+
// The month the quote prices leads, so a column that clips the tail still shows it; one too narrow for
|
|
109
|
+
// "most active" shows the plain name.
|
|
110
|
+
const note = `, most active (${nearer} ${expiry})`;
|
|
111
|
+
return width >= plain.length + MOST_ACTIVE_MIN.length ? `${plain}${note}` : plain;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The least of the note a name column must fit to carry it. */
|
|
115
|
+
const MOST_ACTIVE_MIN = ", most active";
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* What a row's price counts, as `quote` says it: the exchange's unit from the
|
|
119
|
+
* contract terms ("USD / barrel"), the catalog's where the server has none
|
|
120
|
+
* ("EUR / MWh"), else null.
|
|
121
|
+
*/
|
|
122
|
+
export function futuresUnitLabel(contract: FuturesContract, terms?: Pick<FuturesContractTermsPayload, "spec"> | null): string | null {
|
|
123
|
+
return terms?.spec?.unit ?? contract.unit ?? null;
|
|
124
|
+
}
|
|
125
|
+
|
|
70
126
|
function matchesFuturesSearch(contract: FuturesContract, query: string, quote?: Quote | null): boolean {
|
|
71
127
|
const normalized = query.trim().toLowerCase();
|
|
72
128
|
if (!normalized) return true;
|
|
@@ -89,9 +145,12 @@ function getSortValue(
|
|
|
89
145
|
contract: FuturesContract,
|
|
90
146
|
quotes: BoardQuoteMap,
|
|
91
147
|
returns?: FrontReturnsMap,
|
|
148
|
+
terms?: FuturesTermsMap,
|
|
92
149
|
): string | number | null {
|
|
93
150
|
const quote = quotes.get(contract.symbol)?.quote;
|
|
94
151
|
switch (columnId) {
|
|
152
|
+
case "unit":
|
|
153
|
+
return futuresUnitLabel(contract, terms?.get(contract.symbol)?.terms);
|
|
95
154
|
case "status":
|
|
96
155
|
return quote?.marketState === "REGULAR" ? 0 : 1;
|
|
97
156
|
case "code":
|
|
@@ -122,12 +181,13 @@ function sortContracts(
|
|
|
122
181
|
sortPreference: FuturesSortPreference,
|
|
123
182
|
quotes: BoardQuoteMap,
|
|
124
183
|
returns?: FrontReturnsMap,
|
|
184
|
+
terms?: FuturesTermsMap,
|
|
125
185
|
): FuturesContract[] {
|
|
126
186
|
const columnId = sortPreference.columnId;
|
|
127
187
|
if (!columnId) return contracts;
|
|
128
188
|
return [...contracts].sort((left, right) => compareSortValues(
|
|
129
|
-
getSortValue(columnId, left, quotes, returns),
|
|
130
|
-
getSortValue(columnId, right, quotes, returns),
|
|
189
|
+
getSortValue(columnId, left, quotes, returns, terms),
|
|
190
|
+
getSortValue(columnId, right, quotes, returns, terms),
|
|
131
191
|
sortPreference.direction,
|
|
132
192
|
));
|
|
133
193
|
}
|
|
@@ -139,6 +199,8 @@ export interface BuildFuturesRowsOptions {
|
|
|
139
199
|
collapsed?: ReadonlySet<FuturesSector>;
|
|
140
200
|
/** Front-contract returns, for sorting on a returns column. */
|
|
141
201
|
returns?: FrontReturnsMap;
|
|
202
|
+
/** Each row's contract terms, for sorting on the unit. */
|
|
203
|
+
terms?: FuturesTermsMap;
|
|
142
204
|
}
|
|
143
205
|
|
|
144
206
|
const NO_COLLAPSED_SECTORS: ReadonlySet<FuturesSector> = new Set();
|
|
@@ -166,7 +228,7 @@ export function buildFuturesRows(
|
|
|
166
228
|
const query = options?.query ?? "";
|
|
167
229
|
const collapsed = effectiveCollapsedSectors(options?.collapsed, query);
|
|
168
230
|
for (const sector of FUTURES_SECTOR_ORDER) {
|
|
169
|
-
const contracts = sortContracts(contractsBySector.get(sector) ?? [], sortPreference, quotes, options?.returns)
|
|
231
|
+
const contracts = sortContracts(contractsBySector.get(sector) ?? [], sortPreference, quotes, options?.returns, options?.terms)
|
|
170
232
|
.filter((contract) => matchesFuturesSearch(contract, query, quotes.get(contract.symbol)?.quote));
|
|
171
233
|
if (contracts.length === 0) continue;
|
|
172
234
|
rows.push({ type: "header", sector });
|
|
@@ -5,7 +5,8 @@ import { TextAttributes } from "../../../ui";
|
|
|
5
5
|
import { formatCompact, formatNumber, formatPercentRaw } from "../../../utils/format";
|
|
6
6
|
import { isBoardRowLoading, renderQuoteBoardCell, type BoardQuoteMap } from "../shared/use-quote-board";
|
|
7
7
|
import { tickDecimals, type FuturesContract } from "../../../market-data/futures-board";
|
|
8
|
-
import { FUTURES_RETURN_COLUMNS,
|
|
8
|
+
import { FUTURES_RETURN_COLUMNS, futuresContractHeadline, futuresUnitLabel, type FuturesColumnId, type FuturesTableRow } from "./model";
|
|
9
|
+
import type { FuturesTermsMap } from "./use-board-terms";
|
|
9
10
|
import type { FrontReturnsMap } from "./use-front-returns";
|
|
10
11
|
|
|
11
12
|
export type FuturesColumn = DataTableColumn & { id: FuturesColumnId };
|
|
@@ -21,6 +22,7 @@ export const FUTURES_COLUMN_DEFS: readonly FuturesColumnDef[] = [
|
|
|
21
22
|
{ id: "code", label: "Symbol", description: "Exchange contract code." },
|
|
22
23
|
{ id: "name", label: "Contract", description: "Contract name." },
|
|
23
24
|
{ id: "price", label: "Last", description: "Last traded price." },
|
|
25
|
+
{ id: "unit", label: "Unit", description: "What a price counts, as the exchange quotes it (USD / barrel)." },
|
|
24
26
|
{ id: "change", label: "Change", description: "Change on the session." },
|
|
25
27
|
{ id: "changePercent", label: "Change %", description: "Percent change on the session." },
|
|
26
28
|
{ id: "return1w", label: "1W", description: "Front contract's return over one week." },
|
|
@@ -35,22 +37,32 @@ const DEFAULT_FUTURES_COLUMN_IDS = FUTURES_COLUMN_DEFS.map((column) => column.id
|
|
|
35
37
|
|
|
36
38
|
/**
|
|
37
39
|
* Extra columns only earn their width once the board is wide enough to keep a
|
|
38
|
-
* readable contract name; a narrow pane drops them instead
|
|
40
|
+
* readable contract name (24 cells or more); a narrow pane drops them instead
|
|
41
|
+
* of clipping. The unit a price counts outranks its session change in points,
|
|
42
|
+
* which the change in percent still gives. The previous close (the last price
|
|
43
|
+
* less the change) goes first, then each row's quote time (the status line
|
|
44
|
+
* dates the newest), which leaves a 1280-pixel shot room for "WTI Crude Oil
|
|
45
|
+
* Dec 26, most active (Nov expires 20 Oct)".
|
|
39
46
|
*/
|
|
40
47
|
const COLUMN_MIN_PANE_WIDTH: Partial<Record<FuturesColumnId, number>> = {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
time:
|
|
48
|
+
change: 100,
|
|
49
|
+
return1w: 108,
|
|
50
|
+
return1m: 117,
|
|
51
|
+
returnYtd: 126,
|
|
52
|
+
volume: 136,
|
|
53
|
+
time: 170,
|
|
54
|
+
prevClose: 200,
|
|
47
55
|
};
|
|
56
|
+
/** From this width the unit column fits lumber's "USD / 1,000 board ft" whole. */
|
|
57
|
+
const WIDE_UNIT_MIN_WIDTH = 200;
|
|
48
58
|
const SESSION_TEXT_MIN_WIDTH = 100;
|
|
49
59
|
|
|
50
60
|
const COLUMN_WIDTHS: Record<Exclude<FuturesColumnId, "name">, number> = {
|
|
51
61
|
status: 1,
|
|
52
62
|
code: 5,
|
|
53
63
|
price: 12,
|
|
64
|
+
// "US cents / bushel"; lumber's longer unit fits from WIDE_UNIT_MIN_WIDTH.
|
|
65
|
+
unit: 17,
|
|
54
66
|
change: 10,
|
|
55
67
|
changePercent: 9,
|
|
56
68
|
return1w: 8,
|
|
@@ -70,6 +82,7 @@ export function usesSessionText(width: number): boolean {
|
|
|
70
82
|
|
|
71
83
|
function columnWidth(id: Exclude<FuturesColumnId, "name">, paneWidth: number): number {
|
|
72
84
|
if (id === "status") return usesSessionText(paneWidth) ? 9 : 1;
|
|
85
|
+
if (id === "unit" && paneWidth >= WIDE_UNIT_MIN_WIDTH) return 20;
|
|
73
86
|
return COLUMN_WIDTHS[id];
|
|
74
87
|
}
|
|
75
88
|
|
|
@@ -95,7 +108,7 @@ export function createFuturesColumns(width: number, visibleIds?: readonly string
|
|
|
95
108
|
// TIME is left-aligned like the text columns: the shared table trims a few
|
|
96
109
|
// cells off the right edge of a floating pane, and a right-aligned value
|
|
97
110
|
// there would lose digits.
|
|
98
|
-
align: id === "code" || id === "status" || id === "time" ? "left" : "right",
|
|
111
|
+
align: id === "code" || id === "status" || id === "unit" || id === "time" ? "left" : "right",
|
|
99
112
|
};
|
|
100
113
|
});
|
|
101
114
|
}
|
|
@@ -105,6 +118,7 @@ const FUTURES_HEADER_LABELS: Record<FuturesColumnId, string> = {
|
|
|
105
118
|
code: "SYM",
|
|
106
119
|
name: "CONTRACT",
|
|
107
120
|
price: "LAST",
|
|
121
|
+
unit: "UNIT",
|
|
108
122
|
change: "CHG",
|
|
109
123
|
changePercent: "CHG%",
|
|
110
124
|
return1w: "1W",
|
|
@@ -200,7 +214,7 @@ export function renderFuturesCell(
|
|
|
200
214
|
row: FuturesTableRow,
|
|
201
215
|
column: FuturesColumn,
|
|
202
216
|
quotes: BoardQuoteMap,
|
|
203
|
-
options?: { sessionText?: boolean; returns?: FrontReturnsMap },
|
|
217
|
+
options?: { sessionText?: boolean; returns?: FrontReturnsMap; terms?: FuturesTermsMap },
|
|
204
218
|
): DataTableCell {
|
|
205
219
|
if (row.type === "header") return { text: "" };
|
|
206
220
|
|
|
@@ -216,7 +230,13 @@ export function renderFuturesCell(
|
|
|
216
230
|
attributes: TextAttributes.BOLD,
|
|
217
231
|
};
|
|
218
232
|
case "name":
|
|
219
|
-
return { text:
|
|
233
|
+
return { text: futuresContractHeadline(contract, quote, options?.terms?.get(contract.symbol)?.terms, column.width) };
|
|
234
|
+
case "unit": {
|
|
235
|
+
const terms = options?.terms?.get(contract.symbol);
|
|
236
|
+
const unit = futuresUnitLabel(contract, terms?.terms);
|
|
237
|
+
if (unit) return { text: unit, color: colors.textDim };
|
|
238
|
+
return terms?.loading ? { text: "…", color: colors.textDim } : { text: "—", color: colors.textDim };
|
|
239
|
+
}
|
|
220
240
|
case "volume":
|
|
221
241
|
if (isBoardRowLoading(state)) return { text: "…", color: colors.textDim };
|
|
222
242
|
if (!quote || quote.volume == null || !Number.isFinite(quote.volume)) {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from "react";
|
|
2
|
+
import type { FuturesContractTermsPayload } from "../../../api-client/futures-curve";
|
|
3
|
+
import type { FuturesContract } from "../../../market-data/futures-board";
|
|
4
|
+
import { loadFuturesContractTerms } from "../../../market-data/futures-contract";
|
|
5
|
+
|
|
6
|
+
interface FuturesTermsState {
|
|
7
|
+
/** The row's terms are still being asked for. */
|
|
8
|
+
loading: boolean;
|
|
9
|
+
/** Null when the server has no terms for the symbol, or could not answer. */
|
|
10
|
+
terms: FuturesContractTermsPayload | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type FuturesTermsMap = ReadonlyMap<string, FuturesTermsState>;
|
|
14
|
+
|
|
15
|
+
/** The terms change with a roll, and a roll is decided once a day. */
|
|
16
|
+
const TERMS_TTL_MS = 30 * 60_000;
|
|
17
|
+
/** Forty-odd rows open at once; a few requests at a time keep the board's quotes first. */
|
|
18
|
+
const TERMS_CONCURRENCY = 4;
|
|
19
|
+
|
|
20
|
+
interface CachedTerms {
|
|
21
|
+
at: number;
|
|
22
|
+
/** Null when the server has no terms for the symbol, or could not answer. */
|
|
23
|
+
terms: FuturesContractTermsPayload | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const termsCache = new Map<string, CachedTerms>();
|
|
27
|
+
const inFlight = new Map<string, Promise<void>>();
|
|
28
|
+
|
|
29
|
+
function fresh(symbol: string, now: number): CachedTerms | undefined {
|
|
30
|
+
const cached = termsCache.get(symbol);
|
|
31
|
+
return cached && now - cached.at < TERMS_TTL_MS ? cached : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function loadTerms(symbol: string): Promise<void> {
|
|
35
|
+
const pending = inFlight.get(symbol);
|
|
36
|
+
if (pending) return pending;
|
|
37
|
+
const request = loadFuturesContractTerms(symbol)
|
|
38
|
+
.then((terms) => { termsCache.set(symbol, { at: Date.now(), terms }); })
|
|
39
|
+
.catch(() => { termsCache.set(symbol, { at: Date.now(), terms: null }); })
|
|
40
|
+
.finally(() => { inFlight.delete(symbol); });
|
|
41
|
+
inFlight.set(symbol, request);
|
|
42
|
+
return request;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Each listed row's contract terms, as `quote` reads them: the unit its price
|
|
47
|
+
* counts, and the nearer month still trading when its continuous symbol has
|
|
48
|
+
* rolled to the most active one. Only the rows the board lists are asked for,
|
|
49
|
+
* and a row whose terms have not arrived (or that has none) renders without them.
|
|
50
|
+
*/
|
|
51
|
+
export function useFuturesBoardTerms(contracts: readonly FuturesContract[], enabled: boolean): FuturesTermsMap {
|
|
52
|
+
const symbolKey = contracts.map((contract) => contract.symbol).join(",");
|
|
53
|
+
const [version, setVersion] = useState(0);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!enabled || !symbolKey) return;
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const queue = symbolKey.split(",").filter((symbol) => !fresh(symbol, now));
|
|
59
|
+
if (queue.length === 0) return;
|
|
60
|
+
let cancelled = false;
|
|
61
|
+
const worker = async () => {
|
|
62
|
+
for (let symbol = queue.shift(); symbol; symbol = queue.shift()) {
|
|
63
|
+
await loadTerms(symbol);
|
|
64
|
+
if (!cancelled) setVersion((current) => current + 1);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
void Promise.all(Array.from({ length: Math.min(TERMS_CONCURRENCY, queue.length) }, worker));
|
|
68
|
+
return () => {
|
|
69
|
+
cancelled = true;
|
|
70
|
+
queue.length = 0;
|
|
71
|
+
};
|
|
72
|
+
}, [enabled, symbolKey]);
|
|
73
|
+
|
|
74
|
+
return useMemo(() => {
|
|
75
|
+
const result = new Map<string, FuturesTermsState>();
|
|
76
|
+
if (!enabled) return result;
|
|
77
|
+
for (const symbol of symbolKey ? symbolKey.split(",") : []) {
|
|
78
|
+
const cached = termsCache.get(symbol);
|
|
79
|
+
result.set(symbol, { loading: !cached, terms: cached?.terms ?? null });
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
// `version` re-reads the cache as loads land.
|
|
83
|
+
}, [enabled, symbolKey, version]);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Forgets every cached row's terms (tests). */
|
|
87
|
+
export function resetFuturesBoardTerms(): void {
|
|
88
|
+
termsCache.clear();
|
|
89
|
+
}
|
|
@@ -73,7 +73,12 @@ export function HoldersView({ focused, width, height }: { focused: boolean; widt
|
|
|
73
73
|
} : null, [dataProvider, exchange, symbol]);
|
|
74
74
|
// Another ticker's holders stay up until its own answer, and a failed
|
|
75
75
|
// refresh keeps the last holders; the failure goes to the footer.
|
|
76
|
-
const { data, loading, error, reload } = useAsyncResource<HolderData>(loadHolders, {
|
|
76
|
+
const { data, loading, error, reload } = useAsyncResource<HolderData>(loadHolders, {
|
|
77
|
+
initialData: () => symbol && dataProvider?.getCachedQuery
|
|
78
|
+
? dataProvider.getCachedQuery("getHolders", [symbol, exchange]).getSnapshot().result?.value ?? null
|
|
79
|
+
: null,
|
|
80
|
+
keepPreviousData: true,
|
|
81
|
+
});
|
|
77
82
|
|
|
78
83
|
// The values' own currency: dollars from the 13F filings on a London line, not its pence.
|
|
79
84
|
const currency = holderValueCurrency(data, ticker?.metadata.currency || "USD")!;
|
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
} from "../../../api-client/mna";
|
|
10
10
|
import type { HeadlessPaneApiClient } from "../../../types/plugin";
|
|
11
11
|
import { createPluginCache } from "../../../data/plugin-cache";
|
|
12
|
-
import { loadCloudResource, unavailableOnServer, type CloudResource } from "../shared/cloud-resource";
|
|
12
|
+
import { cachedCloudResource, loadCloudResource, unavailableOnServer, type CloudResource } from "../shared/cloud-resource";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Deals change when a story or a filing lands, a few times an hour at most,
|
|
@@ -119,6 +119,10 @@ const listKey = (params: MnaDealsParams, pro: boolean) =>
|
|
|
119
119
|
JSON.stringify([params.status ?? "pending", params.target ?? "all", params.region ?? "all",
|
|
120
120
|
params.symbol?.toUpperCase() ?? "", params.query?.trim().toLowerCase() ?? "", params.offset ?? 0, pro ? "full" : "delayed"]);
|
|
121
121
|
|
|
122
|
+
export function cachedMnaDeals(params: MnaDealsParams, pro: boolean) {
|
|
123
|
+
return cachedCloudResource(mnaDealsCache, listKey(params, pro), validateMnaDeals);
|
|
124
|
+
}
|
|
125
|
+
|
|
122
126
|
export function loadMnaDeals(params: MnaDealsParams, pro: boolean, force = false): Promise<CloudResource<MnaDealsPayload>> {
|
|
123
127
|
return loadCloudResource(mnaDealsCache, listKey(params, pro), () => fetchMnaDeals(params), {
|
|
124
128
|
force,
|
|
@@ -126,8 +130,14 @@ export function loadMnaDeals(params: MnaDealsParams, pro: boolean, force = false
|
|
|
126
130
|
});
|
|
127
131
|
}
|
|
128
132
|
|
|
133
|
+
const dealKey = (id: string, pro: boolean) => `${id}:${pro ? "full" : "delayed"}`;
|
|
134
|
+
|
|
135
|
+
export function cachedMnaDeal(id: string, pro: boolean) {
|
|
136
|
+
return cachedCloudResource(mnaDealCache, dealKey(id, pro), validateMnaDeal);
|
|
137
|
+
}
|
|
138
|
+
|
|
129
139
|
export function loadMnaDeal(id: string, pro: boolean, force = false): Promise<CloudResource<MnaDealPayload>> {
|
|
130
|
-
return loadCloudResource(mnaDealCache,
|
|
140
|
+
return loadCloudResource(mnaDealCache, dealKey(id, pro), () => fetchMnaDeal(id), {
|
|
131
141
|
force,
|
|
132
142
|
validate: validateMnaDeal,
|
|
133
143
|
});
|
|
@@ -22,7 +22,7 @@ import { Box, useRendererHost } from "../../../ui";
|
|
|
22
22
|
import { useAssetData, usePluginTickerActions } from "../../runtime";
|
|
23
23
|
import { useResearchCloudSession } from "../shared/research-cloud-session";
|
|
24
24
|
import { useQuoteBoard } from "../shared/use-quote-board";
|
|
25
|
-
import { loadMnaDeal } from "./client";
|
|
25
|
+
import { cachedMnaDeal, loadMnaDeal } from "./client";
|
|
26
26
|
import {
|
|
27
27
|
dealSpread,
|
|
28
28
|
formatDealValue,
|
|
@@ -136,7 +136,7 @@ export function MnaDealDetail({ id, seed, focused, width, height }: {
|
|
|
136
136
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
137
137
|
[id, pro, session.requestKey],
|
|
138
138
|
);
|
|
139
|
-
const resource = useAsyncResource(loader, { clearOnError: isAccessDenied });
|
|
139
|
+
const resource = useAsyncResource(loader, { initialData: () => cachedMnaDeal(id, pro), clearOnError: isAccessDenied });
|
|
140
140
|
const payload = resource.data?.payload ?? null;
|
|
141
141
|
const deal = payload?.deal ?? seed;
|
|
142
142
|
const events = payload?.events ?? NO_EVENTS;
|
|
@@ -37,7 +37,7 @@ import { useCloudAccessFooter } from "../shared/cloud-upgrade";
|
|
|
37
37
|
import { Blurred, UpgradeLabel } from "../shared/locked-rows";
|
|
38
38
|
import { useResearchCloudSession } from "../shared/research-cloud-session";
|
|
39
39
|
import { useQuoteBoard } from "../shared/use-quote-board";
|
|
40
|
-
import { appendMnaDeals, fetchMnaDeals, loadMnaDeals } from "./client";
|
|
40
|
+
import { appendMnaDeals, cachedMnaDeals, fetchMnaDeals, loadMnaDeals } from "./client";
|
|
41
41
|
import { MnaDealDetail, mnaDealTitle } from "./detail";
|
|
42
42
|
import { DistressBoard } from "./distress/pane";
|
|
43
43
|
import {
|
|
@@ -220,7 +220,7 @@ function MnaDealsView({ focused, width, height, symbol }: {
|
|
|
220
220
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
221
221
|
[paramsKey, pro, session.requestKey],
|
|
222
222
|
);
|
|
223
|
-
const resource = useAsyncResource(loader, { clearOnError: isAccessDenied });
|
|
223
|
+
const resource = useAsyncResource(loader, { initialData: () => cachedMnaDeals(params, pro), clearOnError: isAccessDenied });
|
|
224
224
|
const first = resource.data?.payload ?? null;
|
|
225
225
|
const [paged, setPaged] = useState<{ owner: MnaDealsPayload; payload: MnaDealsPayload } | null>(null);
|
|
226
226
|
const data = paged && paged.owner === first ? paged.payload : first;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { useSyncExternalStore } from "react";
|
|
2
2
|
import type { TeamSummary } from "../../../api-client";
|
|
3
3
|
import { ChoiceDialog } from "../../../components/ui/choice-dialog";
|
|
4
|
+
import { Notice } from "../../../components/ui/status";
|
|
4
5
|
import { QueryBar } from "../../../components/ui/query-bar";
|
|
5
|
-
import { Box
|
|
6
|
+
import { Box } from "../../../ui";
|
|
6
7
|
import type { DialogApi, PromptContext } from "../../../ui/dialog";
|
|
7
8
|
import { colors } from "../../../theme/colors";
|
|
8
9
|
import { teamAccentHex, teamPrefix } from "../../../team/model";
|
|
@@ -118,12 +119,12 @@ export async function resolveNoteConflict(dialog: DialogApi, error: NoteConflict
|
|
|
118
119
|
|
|
119
120
|
export function ReadOnlyBanner({ owner, teams }: { owner: NoteOwner; teams: readonly TeamSummary[] }) {
|
|
120
121
|
return (
|
|
121
|
-
<Box
|
|
122
|
-
<
|
|
122
|
+
<Box paddingLeft={1}>
|
|
123
|
+
<Notice>
|
|
123
124
|
{owner.kind === "user"
|
|
124
125
|
? "Offline: notes are read-only until Gloom Cloud is reachable."
|
|
125
126
|
: `Offline: ${ownerLabel(owner, teams)} notes are read-only.`}
|
|
126
|
-
</
|
|
127
|
+
</Notice>
|
|
127
128
|
</Box>
|
|
128
129
|
);
|
|
129
130
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { Box,
|
|
2
|
+
import { Box, type TextareaRenderable } from "../../../ui";
|
|
3
3
|
import { useShortcut } from "../../../react/input";
|
|
4
4
|
import { isPlainKey } from "../../../utils/keyboard";
|
|
5
5
|
import type { TickerResearchTabProps } from "../../../types/plugin";
|
|
6
|
-
import { colors } from "../../../theme/colors";
|
|
7
6
|
import { MarkdownEditor } from "../../../components/markdown-editor";
|
|
8
7
|
import { EmptyState, usePaneFooter } from "../../../components";
|
|
9
8
|
import { usePluginAppActions, usePluginPaneState } from "../../runtime";
|
|
@@ -178,7 +177,7 @@ export function createNotesTab(registry: NotesStoreRegistry) {
|
|
|
178
177
|
: [{ id: "mode", parts: [{ text: notesFiles.readOnly ? "read-only" : notesFocused ? "editing" : "viewing", tone: "muted" as const }] }],
|
|
179
178
|
}), [loadError, notesFiles.readOnly, notesFocused]);
|
|
180
179
|
|
|
181
|
-
if (!ticker) return <
|
|
180
|
+
if (!ticker) return <EmptyState title="Select a ticker." />;
|
|
182
181
|
|
|
183
182
|
return (
|
|
184
183
|
<Box flexDirection="column" flexGrow={1}>
|