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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { DataTableStackView, DataTableView, KeyValueRow, QueryBar, usePaneNoticeFooter, useQueryBarSearch, useTableLoadMore, type DataTableColumn } from "../../../components";
|
|
2
|
+
import { DataTableStackView, DataTableView, KeyValueRow, QueryBar, usePagedRows, usePaneNoticeFooter, useQueryBarSearch, useTableLoadMore, type DataTableColumn, type PageRequest } from "../../../components";
|
|
3
3
|
import { useShortcut } from "../../../react/input";
|
|
4
4
|
import { colors } from "../../../theme/colors";
|
|
5
5
|
import { Box, type ScrollBoxRenderable } from "../../../ui";
|
|
@@ -28,47 +28,42 @@ export function FundOverlapView({ data, focused, width }: { data: FundDetailData
|
|
|
28
28
|
const [target, setTarget] = usePluginPaneState<ThirteenFFund | null>("overlap:target", null);
|
|
29
29
|
const [selectedId, setSelectedId] = usePluginPaneState<string | null>("overlap:selected", null);
|
|
30
30
|
const [selectedPositionId, setSelectedPositionId] = usePluginPaneState<string | null>("overlap:position", null);
|
|
31
|
-
const [funds, setFunds] = useState<ThirteenFFund[]>([]);
|
|
32
31
|
const [peer, setPeer] = useState<FundDetailData | null>(null);
|
|
33
32
|
const [loading, setLoading] = useState(false);
|
|
34
33
|
const [error, setError] = useState<string | null>(null);
|
|
35
34
|
const { active: searchFocused, focus: focusSearch, blur: blurSearch, searchProps } = useQueryBarSearch();
|
|
36
35
|
const [fundSort, setFundSort] = useState<{ columnId: "name" | "cik"; direction: SortDirection }>({ columnId: "name", direction: "asc" });
|
|
37
|
-
const [hasMore, setHasMore] = useState(false);
|
|
38
|
-
const [offset, setOffset] = useState(0);
|
|
39
|
-
const moreController = useRef<AbortController | null>(null);
|
|
40
36
|
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
41
|
-
const [loadingMore, setLoadingMore] = useState(false);
|
|
42
37
|
const [mineOnly, setMineOnly] = usePluginPaneState<boolean>("overlap:mine", false);
|
|
43
38
|
const [sort, setSort] = usePluginPaneState<{ id: "weight" | "comparedWeight"; desc: boolean }>("overlap:sort", { id: "weight", desc: true });
|
|
44
39
|
const mine = useMineTickers();
|
|
45
40
|
const { pinTicker } = usePluginTickerActions();
|
|
46
41
|
const [refresh, setRefresh] = useState(0);
|
|
47
42
|
const lastRefresh = useRef(0);
|
|
43
|
+
const searchPage = useCallback(async ({ offset, signal }: PageRequest) => {
|
|
44
|
+
const result = await searchThirteenFFunds(query, 50, signal, { offset });
|
|
45
|
+
const rows = result.filter(fund => fund.cik !== data.cik);
|
|
46
|
+
return { rows, hasMore: result.length === 50, nextOffset: offset + result.length };
|
|
47
|
+
}, [query, data.cik]);
|
|
48
|
+
const paged = usePagedRows(target || !query || isCikQuery(query) ? null : searchPage, { getId: (fund) => fund.cik });
|
|
49
|
+
const cikFunds = useMemo(() => isCikQuery(query) && !target ? [{ cik: normalizeCik(query), name: query }] : [], [query, target]);
|
|
50
|
+
const funds = target ? [] : cikFunds.length ? cikFunds : paged.rows;
|
|
48
51
|
useEffect(() => {
|
|
49
52
|
const controller = new AbortController();
|
|
50
|
-
setError(null); setPeer(null);
|
|
51
|
-
if (!target
|
|
53
|
+
setError(null); setPeer(null);
|
|
54
|
+
if (!target) { setLoading(false); return; }
|
|
52
55
|
setLoading(true);
|
|
53
56
|
const forceRefresh = refresh !== lastRefresh.current;
|
|
54
57
|
lastRefresh.current = refresh;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return () => { controller.abort();
|
|
60
|
-
}, [
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
void searchThirteenFFunds(query, 50, request.signal, { offset }).then(result => {
|
|
65
|
-
if (request.signal.aborted) return;
|
|
66
|
-
setFunds(current => [...new Map([...current, ...result.filter(fund => fund.cik !== data.cik)].map(fund => [fund.cik, fund])).values()]);
|
|
67
|
-
setHasMore(result.length === 50); setOffset(offset + result.length);
|
|
68
|
-
}).catch(cause => { if (!request.signal.aborted) setError(cause instanceof Error ? cause.message : String(cause)); }).finally(() => { if (!request.signal.aborted) setLoadingMore(false); });
|
|
69
|
-
}, [hasMore, loadingMore, loading, target, query, offset, data.cik]);
|
|
70
|
-
const onScroll = useTableLoadMore(scrollRef, hasMore && !loadingMore && !loading && !target, loadMore);
|
|
71
|
-
const refreshData = useCallback(() => setRefresh(value => value + 1), []);
|
|
58
|
+
void loadFundDetail(target.cik, target.name, controller.signal, { forceRefresh })
|
|
59
|
+
.then(result => { if (!controller.signal.aborted) setPeer(result); })
|
|
60
|
+
.catch(cause => { if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : String(cause)); })
|
|
61
|
+
.finally(() => { if (!controller.signal.aborted) setLoading(false); });
|
|
62
|
+
return () => { controller.abort(); };
|
|
63
|
+
}, [target, refresh]);
|
|
64
|
+
const loadingMore = paged.loadingMore;
|
|
65
|
+
const onScroll = useTableLoadMore(scrollRef, paged.hasMore && !loadingMore && !loading && !target, paged.loadMore);
|
|
66
|
+
const refreshData = useCallback(() => { setRefresh(value => value + 1); paged.reload(); }, [paged.reload]);
|
|
72
67
|
useShortcut(event => {
|
|
73
68
|
if (!focused || !target || event.targetEditable || !isDetailBackNavigationKey(event)) return;
|
|
74
69
|
event.preventDefault?.(); event.stopPropagation?.(); setTarget(null);
|
|
@@ -80,7 +75,8 @@ export function FundOverlapView({ data, focused, width }: { data: FundDetailData
|
|
|
80
75
|
});
|
|
81
76
|
const period = peer ? overlapPeriod(data, peer) : null;
|
|
82
77
|
const mismatch = peer && !period;
|
|
83
|
-
|
|
78
|
+
const listError = error ?? paged.error?.message ?? paged.moreError?.message ?? null;
|
|
79
|
+
usePaneStatusFooter({ registrationId: "13f-overlap", loading: loading || loadingMore || paged.loading, error: listError, hints: target ? [{ id: "mine", key: "m", label: mineOnly ? "all tickers" : "mine", title: mineOnly ? "All Tickers" : "Mine Only", onPress: () => setMineOnly(value => !value) }] : [{ id: "search", key: "/", label: "search", onPress: focusSearch }] });
|
|
84
80
|
usePaneNoticeFooter({ registrationId: "13f-overlap-notice", focused, notices: [...(peer?.warnings ?? []), ...(mismatch ? ["No reporting quarter is loaded for both funds; overlap is unavailable."] : [])] });
|
|
85
81
|
const rows = useMemo(() => (peer ? buildFundOverlap(data, peer) : []).filter(row => !mineOnly || mine.has(row.ticker)).sort((a, b) => ((a[sort.id] ?? -1) - (b[sort.id] ?? -1)) * (sort.desc ? -1 : 1)), [peer, data, mineOnly, mine, sort]);
|
|
86
82
|
const positionColumns: DataTableColumn[] = [{ id: "mine", label: "MINE", width: 5, align: "left" }, { id: "ticker", label: "TICKER", width: 10, align: "left" }, { id: "type", label: "TYPE", width: 6, align: "left" }, { id: "issuer", label: "ISSUER", width: Math.max(18, width - 58), align: "left" }, { id: "weight", label: "FIRST %", width: 12, align: "right" }, { id: "comparedWeight", label: "SECOND %", width: 12, align: "right" }];
|
|
@@ -106,6 +102,6 @@ export function FundOverlapView({ data, focused, width }: { data: FundDetailData
|
|
|
106
102
|
onActivate={fund => { blurSearch(); setTarget(fund); }}
|
|
107
103
|
rootBefore={<QueryBar width={width} search={{ value: query, onChange: setQuery, placeholder: "Second fund name or CIK", focused, ...searchProps, debounceMs: 250 }} />}
|
|
108
104
|
renderCell={(row, column, _index, state) => ({ text: column.id === "name" ? row.name : row.cik, color: state.selected ? colors.selectedText : colors.text })}
|
|
109
|
-
emptyStateTitle={loading ? "Searching funds..." : query ? "No matching funds." : "Search for a second fund."}
|
|
105
|
+
emptyStateTitle={loading || paged.loading ? "Searching funds..." : query ? "No matching funds." : "Search for a second fund."}
|
|
110
106
|
/>;
|
|
111
107
|
}
|
|
@@ -115,7 +115,7 @@ export const tickerDetailModule: PluginModule = {
|
|
|
115
115
|
label: "Ticker Research",
|
|
116
116
|
description: "Open another research pane for the selected ticker or current collection",
|
|
117
117
|
keywords: ["new", "ticker", "research", "detail", "pane", "inspector"],
|
|
118
|
-
shortcut: { prefix: "T", argPlaceholder: "ticker", argKind: "ticker" },
|
|
118
|
+
shortcut: { prefix: "T", aliases: ["DES"], argPlaceholder: "ticker", argKind: "ticker" },
|
|
119
119
|
canCreate: (context) => context.activeTicker !== null || context.activeCollectionId !== null,
|
|
120
120
|
createInstance: (context) => (
|
|
121
121
|
context.activeTicker
|
|
@@ -183,13 +183,14 @@ export function buildOverviewStats({
|
|
|
183
183
|
valueColor: priceColor(fundamentals.revenueGrowth),
|
|
184
184
|
});
|
|
185
185
|
}
|
|
186
|
-
// At the regular session price, as the market cap above it is.
|
|
187
|
-
const
|
|
186
|
+
// At the regular session price, as the market cap above it is, with the check moved beside it.
|
|
187
|
+
const priced = atRegularSessionPrice({ quote, fundamentals }).fundamentals;
|
|
188
|
+
const enterpriseValue = reportedEnterpriseValue(priced);
|
|
188
189
|
if (fundamentals?.unavailableFields?.includes("enterpriseValue")) {
|
|
189
190
|
stats.push({ label: "EV", value: "—" });
|
|
190
191
|
} else if (enterpriseValue != null) {
|
|
191
192
|
// More than 5% from market cap + debt - cash: the provider's, with the computed figure where it fits.
|
|
192
|
-
const check = enterpriseValueCheck(
|
|
193
|
+
const check = enterpriseValueCheck(priced);
|
|
193
194
|
const computed = check?.status === "differs" && check.computed != null ? formatCompact(check.computed) : undefined;
|
|
194
195
|
stats.push({
|
|
195
196
|
label: "EV",
|
|
@@ -28,6 +28,7 @@ import { Box, ScrollBox, Text, useUiCapabilities } from "../../../ui";
|
|
|
28
28
|
import { publicTickerKey, resolveExchangeTimeZone } from "../../../utils/exchanges";
|
|
29
29
|
import { convertCurrency, displayWidth, formatPercentRaw, truncateToDisplayWidth } from "../../../utils/format";
|
|
30
30
|
import { enterpriseValueReconciliationNote } from "../../../utils/fundamentals";
|
|
31
|
+
import { atRegularSessionPrice } from "../../../market-data/quotes/regular-session-valuation";
|
|
31
32
|
import { zonedDateKey } from "../../../utils/zoned-date-time";
|
|
32
33
|
import { sharesOutstandingInReceipts } from "../../../utils/depositary-receipt";
|
|
33
34
|
import {
|
|
@@ -144,10 +145,9 @@ function ResolvedOverviewTab({ width, height, focused = false, ticker, financial
|
|
|
144
145
|
panelId: "price",
|
|
145
146
|
timeBasis: chartTimeZone ? { kind: "market", timeZone: chartTimeZone } : undefined,
|
|
146
147
|
});
|
|
147
|
-
// A provider EV off market cap + debt - cash, or why a withdrawn one is unavailable
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
? t("Enterprise value unavailable: the source observation failed validation.") : undefined);
|
|
148
|
+
// A provider EV off market cap + debt - cash, or why a withdrawn one is unavailable, at the
|
|
149
|
+
// regular session price the EV beside it is shown at.
|
|
150
|
+
const enterpriseValueNote = enterpriseValueReconciliationNote(atRegularSessionPrice({ quote, fundamentals }).fundamentals);
|
|
151
151
|
usePaneNoticeFooter({
|
|
152
152
|
registrationId: "overview-notices",
|
|
153
153
|
notices: [
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import { useMemo } from "react";
|
|
2
|
+
import { useMemo, useRef } from "react";
|
|
3
3
|
import { Box, Text, TextAttributes, useUiCapabilities } from "../../../../ui";
|
|
4
4
|
import type { PricePoint, Quote, TickerFinancials } from "../../../../types/financials";
|
|
5
5
|
import type { TickerRecord } from "../../../../types/ticker";
|
|
@@ -22,7 +22,7 @@ import { t } from "../../../../i18n";
|
|
|
22
22
|
import { isQuoteStaleForCurrentSession } from "../../../../market-data/quotes/freshness";
|
|
23
23
|
import { formatQuoteNavAsOf } from "../../../../market-data/quotes/time";
|
|
24
24
|
import { useQuoteFlashDirection } from "../../../../components/quote-flash";
|
|
25
|
-
import {
|
|
25
|
+
import { followLiveSparklinePrice } from "../../../../components/price-sparkline/model";
|
|
26
26
|
import {
|
|
27
27
|
PriceAreaSparklineBackground,
|
|
28
28
|
PriceSparkline,
|
|
@@ -145,8 +145,13 @@ export function QuoteMonitorCard({
|
|
|
145
145
|
: cachedFinancials?.priceHistory;
|
|
146
146
|
const assetCategory = quote?.instrumentType ?? ticker?.metadata.assetCategory;
|
|
147
147
|
// Completed bars end at the prior close; the live price closes the line and the stated range.
|
|
148
|
-
const
|
|
149
|
-
|
|
148
|
+
const sparklineRef = useRef<PricePoint[] | undefined>(undefined);
|
|
149
|
+
const priceHistory = useMemo(() => {
|
|
150
|
+
if (!barHistory) return undefined;
|
|
151
|
+
const next = followLiveSparklinePrice(barHistory, quote, { assetCategory, period: chartPeriod, previous: sparklineRef.current });
|
|
152
|
+
sparklineRef.current = next;
|
|
153
|
+
return next;
|
|
154
|
+
}, [assetCategory, barHistory, chartPeriod, quote]);
|
|
150
155
|
const flashFinancials = useMemo<TickerFinancials | null>(
|
|
151
156
|
() => quote ? { quote, annualStatements: [], quarterlyStatements: [], priceHistory: [] } : null,
|
|
152
157
|
[quote],
|
|
@@ -21,6 +21,8 @@ import { useAutoRefresh } from "../../../react/auto-refresh";
|
|
|
21
21
|
import { useLiveStreamingSetting } from "../../../state/hooks/live-streaming";
|
|
22
22
|
import { CLOUD_QUOTE_DELAY_MINUTES } from "../../../api-client/plan-access";
|
|
23
23
|
import { useLiveSessionRefresh } from "../shared/volatility/live-session";
|
|
24
|
+
import { getSharedMarketDataCoordinator } from "../../../market-data/coordinator";
|
|
25
|
+
import { hasFreshReadyEntry } from "../../../market-data/coordinator/entries";
|
|
24
26
|
import { createSurfaceDependencies, loadVolatilitySurface, withReusedYieldCurve, type SurfaceLoadRequest } from "./client";
|
|
25
27
|
import { stableSurfaceSheet, SURFACE_LIVE_RELOAD_MS, surfaceFreshnessLabel, type SurfaceSheetAxes } from "./live";
|
|
26
28
|
import { loadStoredSurface, loadSurfaceDates } from "../iv-history/client";
|
|
@@ -120,8 +122,20 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
|
|
|
120
122
|
// never collapses to the first few expiries while the rest reload.
|
|
121
123
|
const [live, setLive] = useState<{ key: string; snapshot: SurfaceSnapshot; at: number } | null>(null);
|
|
122
124
|
const liveController = useRef<AbortController | null>(null);
|
|
125
|
+
const settledRef = useRef<SurfaceSnapshot | null | undefined>(null);
|
|
123
126
|
const liveDependencies = useMemo(() => withReusedYieldCurve(createSurfaceDependencies()), []);
|
|
124
127
|
const reloadLive = useCallback(async () => {
|
|
128
|
+
const coordinator = getSharedMarketDataCoordinator();
|
|
129
|
+
const instrument = target?.instrument;
|
|
130
|
+
const chainInstrument = { symbol: target?.effectiveTicker ?? symbol!, exchange: target?.effectiveExchange ?? "",
|
|
131
|
+
brokerId: instrument?.brokerId, brokerInstanceId: instrument?.brokerInstanceId, instrument };
|
|
132
|
+
const shown = settledRef.current;
|
|
133
|
+
const expiries = shown?.expiries.map((entry) => entry.expiration) ?? [];
|
|
134
|
+
const freshChain = coordinator != null && expiries.length > 0 && expiries.every((expiration) => {
|
|
135
|
+
const entry = coordinator.getOptionsEntry({ instrument: chainInstrument, expirationDate: expiration });
|
|
136
|
+
return hasFreshReadyEntry(entry, SURFACE_LIVE_RELOAD_MS);
|
|
137
|
+
});
|
|
138
|
+
if (freshChain) return;
|
|
125
139
|
liveController.current?.abort();
|
|
126
140
|
const abort = new AbortController();
|
|
127
141
|
liveController.current = abort;
|
|
@@ -129,10 +143,11 @@ export function VolSurfacePane({ focused, width, height }: PaneProps) {
|
|
|
129
143
|
// Each expiry is fitted as its chain arrives, never all together at the end.
|
|
130
144
|
onSnapshot: () => {} }, liveDependencies);
|
|
131
145
|
if (!abort.signal.aborted) setLive({ key: requestKey, snapshot, at: Date.now() });
|
|
132
|
-
}, [surfaceRequest, liveDependencies]);
|
|
146
|
+
}, [surfaceRequest, liveDependencies, symbol, target]);
|
|
133
147
|
useEffect(() => () => liveController.current?.abort(), [reloadLive]);
|
|
134
148
|
const liveReload = live?.key === requestKey && (resource.updatedAt == null || live.at > resource.updatedAt) ? live.snapshot : null;
|
|
135
149
|
const settledSnapshot = liveReload ?? resource.data;
|
|
150
|
+
settledRef.current = settledSnapshot;
|
|
136
151
|
// Stored close surfaces and IV rank come from the Cloud IV history of the underlying.
|
|
137
152
|
const underlying = (target?.effectiveTicker ?? symbol ?? "").toUpperCase();
|
|
138
153
|
const datesLoader = useCallback(async () => (await loadSurfaceDates(underlying)).dates, [underlying]);
|
|
@@ -141,7 +141,7 @@ export function volatilityHistorySeries(fred: FredChartInput, colors: Volatility
|
|
|
141
141
|
const value = values.get(date);
|
|
142
142
|
return { date: new Date(date), observedAt: new Date(date), value: value != null && Number.isFinite(value) ? value : null };
|
|
143
143
|
}), { id, label, color, calendarSpaced: true }),
|
|
144
|
-
unit: unitGroup === "volatility" ? "
|
|
144
|
+
unit: unitGroup === "volatility" ? "index" : "", unitGroup, panelId,
|
|
145
145
|
});
|
|
146
146
|
const first = dates[0], last = dates.at(-1);
|
|
147
147
|
return [
|
|
@@ -26,7 +26,7 @@ export function VolatilityHistoryChart({ fred, width, height, focused = false }:
|
|
|
26
26
|
return <CompositeChart series={series} legendSeries={legendSeries} panels={HISTORY_PANELS} width={width} height={height}
|
|
27
27
|
focused={focused} navigable={false} showLegend showTimeAxis remoteKind="vix-history"
|
|
28
28
|
formatValue={(value, entry) => entry.unitGroup === "ratio" ? value.toFixed(2) : formatCompositeSeriesValue(value, entry)}
|
|
29
|
-
formatAxisValue={(value, domain) => domain.unitGroup === "ratio" ? value.toFixed(2) :
|
|
29
|
+
formatAxisValue={(value, domain) => domain.unitGroup === "ratio" ? value.toFixed(2) : value.toFixed(0)}
|
|
30
30
|
colors={{ background: palette.bgColor, grid: palette.gridColor, crosshair: palette.crosshairColor, text: colors.text,
|
|
31
31
|
textDim: palette.axisColor, negative: colors.negative }} />;
|
|
32
32
|
}
|
|
@@ -25,6 +25,18 @@ interface ViewBundleOptions {
|
|
|
25
25
|
aliasRules?: AliasRule[];
|
|
26
26
|
failureMessage: string;
|
|
27
27
|
missingEntryMessage: string;
|
|
28
|
+
/**
|
|
29
|
+
* Leave identifiers unminified (whitespace and syntax still are), so the
|
|
30
|
+
* components keep their names and a page audit can say which one drew an
|
|
31
|
+
* element (`scripts/pane-chrome/ui-audit.ts`). Bun's `keepNames` alone did
|
|
32
|
+
* not keep them.
|
|
33
|
+
*/
|
|
34
|
+
keepNames?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Bun's minify setting: everything, or everything but identifiers when names are kept. */
|
|
38
|
+
function minifyOption(keepNames: boolean | undefined) {
|
|
39
|
+
return keepNames ? { whitespace: true, syntax: true, identifiers: false } : true;
|
|
28
40
|
}
|
|
29
41
|
|
|
30
42
|
// From this file rather than the working directory, so a build started from
|
|
@@ -60,6 +72,7 @@ export async function buildViewBundle({
|
|
|
60
72
|
aliasRules = [],
|
|
61
73
|
failureMessage,
|
|
62
74
|
missingEntryMessage,
|
|
75
|
+
keepNames,
|
|
63
76
|
}: ViewBundleOptions): Promise<{ script: string; stylesheet: string | null }> {
|
|
64
77
|
const chunks = new Map<string, string>();
|
|
65
78
|
const result = await Bun.build({
|
|
@@ -69,7 +82,7 @@ export async function buildViewBundle({
|
|
|
69
82
|
format: "esm",
|
|
70
83
|
splitting: false,
|
|
71
84
|
sourcemap,
|
|
72
|
-
minify:
|
|
85
|
+
minify: minifyOption(keepNames),
|
|
73
86
|
define: {
|
|
74
87
|
"process.env.NODE_ENV": "\"production\"",
|
|
75
88
|
__GLOOM_WORLD_MAP_DATA__: JSON.stringify(WORLD_MAP_DATA_SPECIFIER),
|
|
@@ -88,7 +101,7 @@ export async function buildViewBundle({
|
|
|
88
101
|
|
|
89
102
|
const script = result.outputs.find((output) => output.kind === "entry-point" && output.path.endsWith(".js"));
|
|
90
103
|
if (!script) throw new Error(missingEntryMessage);
|
|
91
|
-
await buildLazyChunks(outdir, chunks, failureMessage);
|
|
104
|
+
await buildLazyChunks(outdir, chunks, failureMessage, keepNames);
|
|
92
105
|
|
|
93
106
|
const stylesheet = result.outputs.find((output) => output.path.endsWith(".css"))?.path ?? null;
|
|
94
107
|
if (stylesheet) await writeFile(stylesheet, withTitlebarOverlayHeight(await readFile(stylesheet, "utf8")));
|
|
@@ -181,7 +194,7 @@ function lazyChunkPlugin(rootOutdir: string, emitDir: string, chunks: Map<string
|
|
|
181
194
|
}
|
|
182
195
|
|
|
183
196
|
/** Builds each lazy chunk on its own, and the chunks those load in turn. */
|
|
184
|
-
async function buildLazyChunks(rootOutdir: string, chunks: Map<string, string>, failureMessage: string): Promise<void> {
|
|
197
|
+
async function buildLazyChunks(rootOutdir: string, chunks: Map<string, string>, failureMessage: string, keepNames?: boolean): Promise<void> {
|
|
185
198
|
const built = new Set<string>();
|
|
186
199
|
let pending = [...chunks.entries()];
|
|
187
200
|
while (pending.length) {
|
|
@@ -197,7 +210,7 @@ async function buildLazyChunks(rootOutdir: string, chunks: Map<string, string>,
|
|
|
197
210
|
format: "esm",
|
|
198
211
|
splitting: false,
|
|
199
212
|
sourcemap: "none",
|
|
200
|
-
minify:
|
|
213
|
+
minify: minifyOption(keepNames),
|
|
201
214
|
plugins: [lazyChunkPlugin(rootOutdir, dirname(output), nested)],
|
|
202
215
|
});
|
|
203
216
|
if (!result.success) {
|
|
@@ -41,6 +41,7 @@ export function WebButton({
|
|
|
41
41
|
onPress,
|
|
42
42
|
variant = "secondary",
|
|
43
43
|
disabled = false,
|
|
44
|
+
pending = false,
|
|
44
45
|
active = false,
|
|
45
46
|
shortcut,
|
|
46
47
|
width,
|
|
@@ -61,6 +62,7 @@ export function WebButton({
|
|
|
61
62
|
aria-keyshortcuts={ariaKeyShortcut(shortcut)}
|
|
62
63
|
title={title}
|
|
63
64
|
aria-expanded={expanded}
|
|
65
|
+
aria-busy={pending || undefined}
|
|
64
66
|
disabled={disabled}
|
|
65
67
|
onMouseDown={(event) => {
|
|
66
68
|
if (stopPropagation) event.stopPropagation();
|
|
@@ -81,6 +81,12 @@ button, [data-gloom-interactive="true"] { cursor: pointer; }
|
|
|
81
81
|
[data-titlebar-overlay="true"] * {
|
|
82
82
|
cursor: inherit;
|
|
83
83
|
}
|
|
84
|
+
/* A row of text grows with the text size and the title bar does not: at the
|
|
85
|
+
largest sizes the top bar's buttons and prompt would stand out of it. */
|
|
86
|
+
[data-gloom-role="app-header"][data-titlebar-overlay="true"] button,
|
|
87
|
+
[data-gloom-role="app-header"][data-titlebar-overlay="true"] [role="button"] {
|
|
88
|
+
max-height: __TITLEBAR_OVERLAY_HEIGHT_PX__px;
|
|
89
|
+
}
|
|
84
90
|
[data-titlebar-overlay="true"] button,
|
|
85
91
|
[data-titlebar-overlay="true"] input,
|
|
86
92
|
[data-titlebar-overlay="true"] textarea,
|
|
@@ -15,14 +15,15 @@ export function setMacosDockBadge(label: string): void {
|
|
|
15
15
|
const dockTile = objc.send(app, objc.selector("dockTile"));
|
|
16
16
|
if (!dockTile) return;
|
|
17
17
|
const text = label
|
|
18
|
-
? objc.
|
|
18
|
+
? objc.sendCString(
|
|
19
19
|
objc.send(objc.getClass("NSString"), objc.selector("alloc")),
|
|
20
20
|
objc.selector("initWithUTF8String:"),
|
|
21
21
|
cString(label),
|
|
22
22
|
)
|
|
23
|
-
:
|
|
23
|
+
: 0n;
|
|
24
24
|
// AppKit only takes this on the main thread, and Bun runs on another one.
|
|
25
|
-
// The call retains the string until it has run;
|
|
25
|
+
// The call retains the string until it has run; no label is nil. A label as
|
|
26
|
+
// short as a count is a tagged pointer, which is why objects travel as ids.
|
|
26
27
|
objc.sendOnMainThread(
|
|
27
28
|
dockTile,
|
|
28
29
|
objc.selector("performSelectorOnMainThread:withObject:waitUntilDone:"),
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import type { Pointer } from "bun:ffi";
|
|
2
1
|
import { appIconColors, appIconSvg } from "./app-icon";
|
|
3
2
|
import { getTheme } from "../../../../theme/themes";
|
|
4
|
-
import { objcOrNull, type Objc } from "./macos-objc";
|
|
3
|
+
import { objcOrNull, type Objc, type ObjcId } from "./macos-objc";
|
|
5
4
|
|
|
6
5
|
let appliedThemeId: string | null = null;
|
|
7
6
|
|
|
8
|
-
function createImage(objc: Objc, svg: string):
|
|
7
|
+
function createImage(objc: Objc, svg: string): ObjcId | null {
|
|
9
8
|
const bytes = Buffer.from(svg);
|
|
10
9
|
const data = objc.sendBytes(
|
|
11
10
|
objc.send(objc.getClass("NSData"), objc.selector("alloc")),
|
|
@@ -4,28 +4,39 @@ const LIBOBJC = "/usr/lib/libobjc.A.dylib";
|
|
|
4
4
|
|
|
5
5
|
export type Objc = ReturnType<typeof loadObjc>;
|
|
6
6
|
|
|
7
|
+
/** An Objective-C object or class as its address, `0n` for nil. */
|
|
8
|
+
export type ObjcId = bigint;
|
|
9
|
+
|
|
7
10
|
let cachedObjc: Objc | null | undefined;
|
|
8
11
|
|
|
9
12
|
export const cString = (value: string) => Buffer.from(`${value}\0`);
|
|
10
13
|
|
|
14
|
+
// Objects cross the FFI as u64, never as ptr. Bun hands a ptr result to
|
|
15
|
+
// JavaScript as a double, exact only up to 2**53, and Foundation keeps short
|
|
16
|
+
// strings and small numbers (a Dock badge label such as "3") in tagged
|
|
17
|
+
// pointers whose data sits in the top bits. Through a ptr they come back
|
|
18
|
+
// rounded, and the next message to one crashes the process.
|
|
19
|
+
const ID = FFIType.u64;
|
|
20
|
+
|
|
11
21
|
// objc_msgSend has to be called with each method's exact C signature, so it is
|
|
12
22
|
// bound once per call shape.
|
|
13
23
|
function loadObjc() {
|
|
14
24
|
const runtime = dlopen(LIBOBJC, {
|
|
15
|
-
objc_getClass: { args: [FFIType.ptr], returns:
|
|
25
|
+
objc_getClass: { args: [FFIType.ptr], returns: ID },
|
|
16
26
|
sel_registerName: { args: [FFIType.ptr], returns: FFIType.ptr },
|
|
17
27
|
}).symbols;
|
|
18
28
|
const msgSend = <const Args extends FFIType[], Returns extends FFIType>(args: Args, returns: Returns) =>
|
|
19
|
-
dlopen(LIBOBJC, { objc_msgSend: { args: [
|
|
29
|
+
dlopen(LIBOBJC, { objc_msgSend: { args: [ID, FFIType.ptr, ...args], returns } }).symbols.objc_msgSend;
|
|
20
30
|
|
|
21
31
|
return {
|
|
22
32
|
getClass: (name: string) => runtime.objc_getClass(cString(name)),
|
|
23
33
|
selector: (name: string) => runtime.sel_registerName(cString(name)),
|
|
24
|
-
send: msgSend([],
|
|
34
|
+
send: msgSend([], ID),
|
|
25
35
|
sendVoid: msgSend([], FFIType.void),
|
|
26
|
-
sendObject: msgSend([
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
sendObject: msgSend([ID], ID),
|
|
37
|
+
sendCString: msgSend([FFIType.ptr], ID),
|
|
38
|
+
sendBytes: msgSend([FFIType.ptr, FFIType.u64], ID),
|
|
39
|
+
sendOnMainThread: msgSend([FFIType.ptr, ID, FFIType.bool], FFIType.void),
|
|
29
40
|
};
|
|
30
41
|
}
|
|
31
42
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { futuresListingPriceBasis } from "../../utils/futures-generic";
|
|
2
2
|
import type { TimeRange } from "../../time-series/range";
|
|
3
3
|
import { RANGE_HISTORY_RESOLUTION } from "../../time-series/resolution";
|
|
4
4
|
import type {
|
|
@@ -55,6 +55,12 @@ function isImpossiblePreviousClose(
|
|
|
55
55
|
&& previousClose * IMPOSSIBLE_CLOSE_FACTOR < low52w;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/** A quote that says it is something other than a future (an equity, a fund) keeps its own price basis. */
|
|
59
|
+
function isNonFutureType(instrumentType: string | undefined): boolean {
|
|
60
|
+
const type = instrumentType?.trim().toUpperCase();
|
|
61
|
+
return !!type && type !== "FUTURE" && type !== "FUTURES" && type !== "FUT";
|
|
62
|
+
}
|
|
63
|
+
|
|
58
64
|
export function mapQuote(
|
|
59
65
|
quote: CloudQuotePayload,
|
|
60
66
|
providerMeta?: CloudProviderMeta,
|
|
@@ -76,12 +82,14 @@ export function mapQuote(
|
|
|
76
82
|
const changePercent = !impossibleClose && typeof quote.changePercent === "number" && Number.isFinite(quote.changePercent)
|
|
77
83
|
? quote.changePercent
|
|
78
84
|
: Number.NaN;
|
|
79
|
-
// A generic future (VX1, TY1) reads in points or 32nds,
|
|
80
|
-
|
|
85
|
+
// A generic future (VX1, TY1) reads in points or 32nds, and an index future's
|
|
86
|
+
// continuous symbol or contract (ES=F, SXOZ26.CME) in points, not dollars.
|
|
87
|
+
const futuresBasis = isNonFutureType(quote.instrumentType) ? null
|
|
88
|
+
: futuresListingPriceBasis(quote.symbol, quote.listingExchangeName ?? quote.exchangeName);
|
|
81
89
|
const priceObservation = quote.priceBasis === "nav" ? "nav" : undefined;
|
|
82
90
|
const priceBasis = quote.priceBasis === "nav"
|
|
83
91
|
? "per-unit"
|
|
84
|
-
:
|
|
92
|
+
: futuresBasis ?? quote.priceBasis;
|
|
85
93
|
return withoutUndefinedFields(reconcileQuoteDayRange({
|
|
86
94
|
...quote,
|
|
87
95
|
priceBasis,
|
|
@@ -222,6 +230,7 @@ export function mapPricePoint(
|
|
|
222
230
|
volume: point.volume,
|
|
223
231
|
...(point.partial === true ? { partial: true } : {}),
|
|
224
232
|
...(point.filled === true ? { filled: true } : {}),
|
|
233
|
+
...(point.unsettled === true ? { unsettled: true } : {}),
|
|
225
234
|
};
|
|
226
235
|
}
|
|
227
236
|
|
|
@@ -73,7 +73,7 @@ export class ProviderRouterBatchRoutes {
|
|
|
73
73
|
: isProviderQuoteUsableForCurrentSession(rawCached.value, target.exchange, target.symbol, { recentAnswer: !rawCached.stale }))
|
|
74
74
|
? rawCached
|
|
75
75
|
: null;
|
|
76
|
-
if (cached && !forceRefresh && !cached.stale) {
|
|
76
|
+
if (cached && !forceRefresh && !cached.stale && !options.revalidate) {
|
|
77
77
|
if (brokerSourceKeys.includes(cached.sourceKey)) {
|
|
78
78
|
misses.push({ index, target });
|
|
79
79
|
return;
|
|
@@ -81,6 +81,7 @@ export class ProviderRouterBatchRoutes {
|
|
|
81
81
|
results[index] = { target, quote: cached.value };
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
|
+
// Revalidated, a fresh copy is still what the quote route answers when no source does.
|
|
84
85
|
misses.push({ index, target });
|
|
85
86
|
});
|
|
86
87
|
|
|
@@ -9,6 +9,8 @@ import { brokerContractIdentityKey } from "../../utils/instrument-identity";
|
|
|
9
9
|
import { providerFinancialsMatchTarget, providerQuoteMatchesTarget } from "./financials";
|
|
10
10
|
|
|
11
11
|
const MARKET_NAMESPACE = "market";
|
|
12
|
+
/** Quotes and price histories are read on every tick; their eviction order is refreshed at most this often. */
|
|
13
|
+
const HOT_READ_TOUCH_MS = 10 * 60_000;
|
|
12
14
|
// A record written under another version is a miss: bump these instead of
|
|
13
15
|
// repairing older records on read.
|
|
14
16
|
export const FINANCIALS_SCHEMA_VERSION = 13;
|
|
@@ -174,6 +176,7 @@ export function listCachedResources<T>(
|
|
|
174
176
|
sourceKeys,
|
|
175
177
|
allowExpired,
|
|
176
178
|
...(SCHEMA_VERSIONS[kind] ? { schemaVersion: SCHEMA_VERSIONS[kind] } : {}),
|
|
179
|
+
...(kind === "quote" || kind === "price-history" || kind === "price-history-recheck" ? { touchIntervalMs: HOT_READ_TOUCH_MS } : {}),
|
|
177
180
|
}).filter((record) => {
|
|
178
181
|
if (!record.sourceKey.startsWith("provider:") || entityKey.startsWith("contract:")
|
|
179
182
|
|| (kind !== "financials" && kind !== "quote")) return true;
|
package/src/sync/baseline.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { SyncBaselineStore } from "./types";
|
|
|
3
3
|
|
|
4
4
|
const BASELINE_PLUGIN_ID = "core.sync";
|
|
5
5
|
const BASELINE_KEY = "last-synced-payloads";
|
|
6
|
+
const MEMOS_KEY = "contributor-memos";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Persists what this device last pushed so the next pull can tell "the cloud is
|
|
@@ -13,20 +14,24 @@ const BASELINE_KEY = "last-synced-payloads";
|
|
|
13
14
|
* local state, never to discarding it.
|
|
14
15
|
*/
|
|
15
16
|
export function createSyncBaselineStore(pluginState: AppPluginStateStorePort): SyncBaselineStore {
|
|
17
|
+
const load = (key: string) => {
|
|
18
|
+
try {
|
|
19
|
+
return pluginState.get<Record<string, unknown>>(BASELINE_PLUGIN_ID, key)?.value ?? null;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const save = (key: string, value: Record<string, unknown>) => {
|
|
25
|
+
try {
|
|
26
|
+
pluginState.set(BASELINE_PLUGIN_ID, key, value);
|
|
27
|
+
} catch {
|
|
28
|
+
// Ignored: a stale baseline or memo only makes the next pull more conservative.
|
|
29
|
+
}
|
|
30
|
+
};
|
|
16
31
|
return {
|
|
17
|
-
load: () =>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
save: (payloads) => {
|
|
25
|
-
try {
|
|
26
|
-
pluginState.set(BASELINE_PLUGIN_ID, BASELINE_KEY, payloads);
|
|
27
|
-
} catch {
|
|
28
|
-
// Ignored: a stale baseline only makes the next pull more conservative.
|
|
29
|
-
}
|
|
30
|
-
},
|
|
32
|
+
load: () => load(BASELINE_KEY),
|
|
33
|
+
save: (payloads) => save(BASELINE_KEY, payloads),
|
|
34
|
+
loadMemos: () => load(MEMOS_KEY),
|
|
35
|
+
saveMemos: (memos) => save(MEMOS_KEY, memos),
|
|
31
36
|
};
|
|
32
37
|
}
|