gloomberb 0.7.2 → 0.7.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/README.md +79 -132
- package/package.json +1 -1
- package/src/app.tsx +73 -67
- package/src/components/chart/chart-data.ts +7 -2
- package/src/components/chart/chart-pane-settings.ts +7 -4
- package/src/components/chart/chart-renderer.ts +158 -47
- package/src/components/chart/chart-types.ts +20 -2
- package/src/components/chart/chart.test.ts +179 -13
- package/src/components/chart/comparison-chart-renderer.test.ts +14 -30
- package/src/components/chart/comparison-chart-renderer.ts +50 -11
- package/src/components/chart/comparison-stock-chart.tsx +129 -129
- package/src/components/chart/market-session.ts +187 -0
- package/src/components/chart/native/chart-rasterizer.ts +108 -19
- package/src/components/chart/price-axis-labels.tsx +25 -9
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +97 -1
- package/src/components/chart/stock-chart.tsx +95 -49
- package/src/components/chart/time-axis-label.tsx +56 -2
- package/src/components/command-bar/command-bar.test.tsx +446 -120
- package/src/components/command-bar/command-bar.tsx +535 -384
- package/src/components/command-bar/command-registry.ts +16 -6
- package/src/components/command-bar/list-model.ts +8 -4
- package/src/components/command-bar/root-shortcuts.ts +4 -5
- package/src/components/command-bar/view-model.test.ts +21 -0
- package/src/components/command-bar/view-model.ts +17 -4
- package/src/components/command-bar/workflow-ops.ts +11 -32
- package/src/components/data-table-view.test.tsx +24 -1
- package/src/components/data-table-view.tsx +3 -2
- package/src/components/feed-data-table-stack-view.tsx +3 -2
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/floating-pane.tsx +9 -2
- package/src/components/layout/pane-footer.tsx +1 -1
- package/src/components/layout/pane-header.tsx +4 -2
- package/src/components/layout/pane-sizing.ts +12 -0
- package/src/components/layout/pane.tsx +24 -2
- package/src/components/layout/shell.tsx +24 -10
- package/src/components/layout/status-bar.test.tsx +4 -1
- package/src/components/layout/status-bar.tsx +4 -24
- package/src/components/markdown-text.tsx +53 -8
- package/src/components/onboarding/onboarding-wizard.tsx +7 -6
- package/src/components/pane-settings-dialog.tsx +9 -6
- package/src/components/pane-template-wizard.tsx +3 -2
- package/src/components/price-selector-dialog.tsx +3 -2
- package/src/components/price-sparkline-view.tsx +463 -0
- package/src/components/price-sparkline.ts +46 -0
- package/src/components/table-view-shared.tsx +21 -3
- package/src/components/ticker-list-table-view.tsx +3 -2
- package/src/components/ticker-list-table.tsx +15 -66
- package/src/components/toggle-list.tsx +3 -3
- package/src/components/ui/choice-dialog.tsx +3 -2
- package/src/components/ui/multi-select-dialog.tsx +3 -2
- package/src/components/ui/native-select.tsx +3 -3
- package/src/core/state/app-state.ts +210 -80
- package/src/core/state/session-persistence.ts +25 -0
- package/src/data/config-store-node.ts +113 -25
- package/src/data/config-store.test.ts +65 -3
- package/src/data/plugin-state-store.ts +39 -13
- package/src/data/sqlite-cache.test.ts +13 -0
- package/src/market-data/coordinator-subscriptions.test.ts +102 -18
- package/src/market-data/coordinator.test.ts +122 -2
- package/src/market-data/coordinator.ts +200 -56
- package/src/market-data/hooks.test.tsx +31 -0
- package/src/market-data/hooks.tsx +1 -0
- package/src/news/aggregator.test.ts +41 -0
- package/src/news/aggregator.ts +61 -9
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +25 -6
- package/src/plugins/builtin/ai/screener-pane.tsx +3 -0
- package/src/plugins/builtin/ai/settings.ts +3 -0
- package/src/plugins/builtin/broker-manager/index.tsx +4 -3
- package/src/plugins/builtin/chat-controller.test.ts +398 -17
- package/src/plugins/builtin/chat-controller.ts +263 -48
- package/src/plugins/builtin/chat.test.tsx +312 -6
- package/src/plugins/builtin/chat.tsx +245 -66
- package/src/plugins/builtin/comparison-chart.test.tsx +117 -3
- package/src/plugins/builtin/correlation/colors.test.ts +39 -0
- package/src/plugins/builtin/correlation/colors.ts +87 -0
- package/src/plugins/builtin/correlation/index.test.tsx +151 -0
- package/src/plugins/builtin/correlation/index.tsx +92 -21
- package/src/plugins/builtin/debug.tsx +4 -3
- package/src/plugins/builtin/econ/index.tsx +3 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/holders/index.tsx +25 -13
- package/src/plugins/builtin/layout-manager.tsx +1 -7
- package/src/plugins/builtin/market-movers/index.tsx +72 -40
- package/src/plugins/builtin/news-wire/breaking-pane.test.tsx +156 -0
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +0 -90
- package/src/plugins/builtin/news-wire/index.tsx +1 -3
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +3 -2
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +4 -1
- package/src/plugins/builtin/news-wire/persisted-articles.ts +7 -1
- package/src/plugins/builtin/options.tsx +9 -17
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +6 -10
- package/src/plugins/builtin/portfolio-list/metrics.ts +13 -2
- package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +139 -23
- package/src/plugins/builtin/portfolio-list/quick-add.tsx +422 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -12
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -3
- package/src/plugins/builtin/portfolio-list.test.tsx +311 -9
- package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
- package/src/plugins/builtin/sectors/index.tsx +275 -43
- package/src/plugins/builtin/sectors/sector-data.ts +41 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -2
- package/src/plugins/builtin/ticker-detail/index.tsx +30 -9
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -15
- package/src/plugins/builtin/ticker-detail/pane.tsx +18 -7
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +445 -63
- package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
- package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
- package/src/plugins/builtin/world-indices/index.tsx +58 -19
- package/src/plugins/ibkr/trading-pane.tsx +3 -2
- package/src/plugins/plugin-runtime.tsx +2 -2
- package/src/plugins/prediction-markets/settings.ts +4 -0
- package/src/plugins/registry.test.ts +55 -0
- package/src/plugins/registry.ts +11 -4
- package/src/plugins/ticker-navigation.test.ts +35 -5
- package/src/plugins/ticker-navigation.ts +16 -22
- package/src/renderers/electrobun/bun/application-menu-click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu-click.ts +7 -4
- package/src/renderers/electrobun/bun/application-menu.ts +6 -1
- package/src/renderers/electrobun/bun/desktop-workspace.ts +47 -29
- package/src/renderers/electrobun/bun/index.ts +52 -13
- package/src/renderers/electrobun/view/app-services.ts +51 -9
- package/src/renderers/electrobun/view/backend-rpc.ts +5 -8
- package/src/renderers/electrobun/view/desktop-controls.tsx +4 -4
- package/src/renderers/electrobun/view/main.tsx +0 -16
- package/src/renderers/electrobun/view/styles.css +23 -21
- package/src/renderers/electrobun/view/ui-host.tsx +6 -4
- package/src/renderers/opentui/host.tsx +2 -1
- package/src/renderers/opentui/start.tsx +1 -1
- package/src/renderers/opentui/test-utils.tsx +6 -5
- package/src/sources/gloomberb-cloud.test.ts +55 -2
- package/src/sources/gloomberb-cloud.ts +180 -29
- package/src/sources/provider-router.test.ts +567 -10
- package/src/sources/provider-router.ts +323 -93
- package/src/sources/yahoo-finance.test.ts +37 -0
- package/src/sources/yahoo-finance.ts +48 -2
- package/src/state/app-bootstrap.test.ts +50 -23
- package/src/state/app-bootstrap.ts +29 -13
- package/src/state/app-context.test.ts +113 -0
- package/src/state/app-context.tsx +8 -4
- package/src/state/session-persistence.test.ts +5 -3
- package/src/state/use-inline-tickers.ts +3 -0
- package/src/state/use-quote-streaming.ts +11 -0
- package/src/theme/colors.ts +13 -14
- package/src/types/config.ts +9 -4
- package/src/types/data-provider.ts +5 -1
- package/src/types/financials.ts +1 -0
- package/src/types/plugin.ts +8 -1
- package/src/ui/context-menu.tsx +4 -4
- package/src/utils/api-client.test.ts +248 -0
- package/src/utils/api-client.ts +178 -26
- package/src/utils/exchanges.test.ts +16 -0
- package/src/utils/exchanges.ts +33 -0
- package/src/utils/fuzzy-search.ts +12 -1
- package/src/utils/keyboard.ts +31 -0
- package/src/utils/options.ts +1 -1
- package/src/utils/price-history.test.ts +28 -1
- package/src/utils/price-history.ts +16 -0
- package/src/utils/quote-freshness.test.ts +150 -0
- package/src/utils/quote-freshness.ts +42 -1
- package/src/utils/quote-resolution.ts +2 -0
- package/src/utils/quote-time.test.ts +34 -0
- package/src/utils/quote-time.ts +18 -5
- package/src/utils/ticker-search.test.ts +2 -2
- package/src/utils/ticker-search.ts +48 -1
- package/src/version.ts +1 -1
- package/src/plugins/builtin/news-wire/digest-store.ts +0 -44
|
@@ -5,8 +5,9 @@ import type { GloomPlugin } from "../../types/plugin";
|
|
|
5
5
|
import type { OptionContract, OptionsChain } from "../../types/financials";
|
|
6
6
|
import { usePaneTicker } from "../../state/app-context";
|
|
7
7
|
import { blendHex, colors, hoverBg } from "../../theme/colors";
|
|
8
|
-
import { blendForContrast, contrastRatio
|
|
8
|
+
import { blendForContrast, contrastRatio } from "../../theme/color-utils";
|
|
9
9
|
import { formatCompact, formatNumber } from "../../utils/format";
|
|
10
|
+
import { isPlainKey } from "../../utils/keyboard";
|
|
10
11
|
import { formatMarketPrice } from "../../utils/market-format";
|
|
11
12
|
import { formatExpDate, resolveOptionsTarget } from "../../utils/options";
|
|
12
13
|
import { useOptionsQuery, useResolvedEntryValue } from "../../market-data/hooks";
|
|
@@ -48,15 +49,6 @@ type OptionColorRole = "call" | "put" | "price" | "activity" | "iv" | "strike";
|
|
|
48
49
|
|
|
49
50
|
const OPTION_TEXT_MIN_CONTRAST = 4.5;
|
|
50
51
|
|
|
51
|
-
const OPTION_BASE_COLORS: Record<OptionColorRole, string> = {
|
|
52
|
-
call: "#5ed69a",
|
|
53
|
-
put: "#ff9c7a",
|
|
54
|
-
price: "#dfc05b",
|
|
55
|
-
activity: "#35a7d6",
|
|
56
|
-
iv: "#8bd878",
|
|
57
|
-
strike: "#8fb7ff",
|
|
58
|
-
};
|
|
59
|
-
|
|
60
52
|
const OPTION_COLUMNS: Array<Omit<OptionColumn, "headerColor">> = [
|
|
61
53
|
{ id: "callOpenInterest", label: "C OI", width: 6, align: "right" },
|
|
62
54
|
{ id: "callVolume", label: "C VOL", width: 6, align: "right" },
|
|
@@ -227,7 +219,7 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
|
|
|
227
219
|
return true;
|
|
228
220
|
}
|
|
229
221
|
|
|
230
|
-
if (event
|
|
222
|
+
if (isPlainKey(event, "j", "down")) {
|
|
231
223
|
if (strikes.length === 0) return true;
|
|
232
224
|
event.preventDefault?.();
|
|
233
225
|
event.stopPropagation?.();
|
|
@@ -236,7 +228,7 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
|
|
|
236
228
|
setStrikeIdx((i) => Math.min(i + 1, strikes.length - 1));
|
|
237
229
|
return true;
|
|
238
230
|
}
|
|
239
|
-
if (event
|
|
231
|
+
if (isPlainKey(event, "k", "up")) {
|
|
240
232
|
if (strikes.length === 0) return true;
|
|
241
233
|
event.preventDefault?.();
|
|
242
234
|
event.stopPropagation?.();
|
|
@@ -404,14 +396,13 @@ function mostReadableColor(surface: string, candidates: readonly string[]): stri
|
|
|
404
396
|
}
|
|
405
397
|
|
|
406
398
|
export function optionRoleColor(role: OptionColorRole, surface: string): string {
|
|
407
|
-
const preferred =
|
|
408
|
-
const themeColor = optionRoleThemeColor(role);
|
|
399
|
+
const preferred = optionRoleThemeColor(role);
|
|
409
400
|
const fallback = mostReadableColor(surface, [
|
|
410
401
|
preferred,
|
|
411
|
-
themeColor,
|
|
412
402
|
colors.text,
|
|
413
403
|
colors.textBright,
|
|
414
|
-
|
|
404
|
+
colors.selectedText,
|
|
405
|
+
colors.neutral,
|
|
415
406
|
]);
|
|
416
407
|
return blendForContrast(preferred, surface, fallback, OPTION_TEXT_MIN_CONTRAST);
|
|
417
408
|
}
|
|
@@ -421,7 +412,8 @@ export function optionMutedColor(surface: string): string {
|
|
|
421
412
|
colors.textDim,
|
|
422
413
|
colors.text,
|
|
423
414
|
colors.textBright,
|
|
424
|
-
|
|
415
|
+
colors.selectedText,
|
|
416
|
+
colors.neutral,
|
|
425
417
|
]);
|
|
426
418
|
return blendForContrast(colors.textDim, surface, fallback, OPTION_TEXT_MIN_CONTRAST);
|
|
427
419
|
}
|
|
@@ -23,6 +23,7 @@ import type { CliCommandContext, CliCommandDef } from "../../../../types/plugin"
|
|
|
23
23
|
import type { TickerRecord } from "../../../../types/ticker";
|
|
24
24
|
import {
|
|
25
25
|
addTickerToPortfolio,
|
|
26
|
+
addTickerToWatchlist,
|
|
26
27
|
createManualPortfolio,
|
|
27
28
|
deleteManualPortfolio,
|
|
28
29
|
findPortfolio,
|
|
@@ -415,7 +416,7 @@ async function deleteWatchlist(name: string, ctx: CliCommandContext) {
|
|
|
415
416
|
persistence.close();
|
|
416
417
|
}
|
|
417
418
|
|
|
418
|
-
async function
|
|
419
|
+
async function addTickerToWatchlistCommand(watchlistName: string, symbol: string, ctx: CliCommandContext) {
|
|
419
420
|
const { config, store, dataProvider, persistence } = await ctx.initMarketData();
|
|
420
421
|
const watchlist = findWatchlist(config, watchlistName);
|
|
421
422
|
if (!watchlist) {
|
|
@@ -424,19 +425,14 @@ async function addTickerToWatchlist(watchlistName: string, symbol: string, ctx:
|
|
|
424
425
|
|
|
425
426
|
try {
|
|
426
427
|
const ticker = await resolveTickerForCli(symbol, store, dataProvider);
|
|
427
|
-
|
|
428
|
+
const result = addTickerToWatchlist(ticker, watchlist.id);
|
|
429
|
+
if (!result.changed) {
|
|
428
430
|
console.log(cliStyles.warning(`${ticker.metadata.ticker} is already in "${watchlist.name}".`));
|
|
429
431
|
persistence.close();
|
|
430
432
|
return;
|
|
431
433
|
}
|
|
432
434
|
|
|
433
|
-
const nextTicker
|
|
434
|
-
...ticker,
|
|
435
|
-
metadata: {
|
|
436
|
-
...ticker.metadata,
|
|
437
|
-
watchlists: [...ticker.metadata.watchlists, watchlist.id],
|
|
438
|
-
},
|
|
439
|
-
};
|
|
435
|
+
const nextTicker = result.ticker;
|
|
440
436
|
await store.saveTicker(nextTicker);
|
|
441
437
|
console.log(cliStyles.success(`Added ${nextTicker.metadata.ticker} to "${watchlist.name}".`));
|
|
442
438
|
if (nextTicker.metadata.name) {
|
|
@@ -644,7 +640,7 @@ export const watchlistCliCommand: CliCommandDef = {
|
|
|
644
640
|
const symbol = args.at(-1);
|
|
645
641
|
const name = args.slice(1, -1).join(" ");
|
|
646
642
|
if (!name || !symbol) ctx.fail("Usage: gloomberb watchlist add <watchlist> <ticker>");
|
|
647
|
-
await
|
|
643
|
+
await addTickerToWatchlistCommand(name!, symbol!, ctx);
|
|
648
644
|
return;
|
|
649
645
|
}
|
|
650
646
|
|
|
@@ -3,11 +3,12 @@ import type { ColumnConfig } from "../../../types/config";
|
|
|
3
3
|
import type { TickerFinancials } from "../../../types/financials";
|
|
4
4
|
import type { TickerRecord } from "../../../types/ticker";
|
|
5
5
|
import { priceColor } from "../../../theme/colors";
|
|
6
|
-
import {
|
|
6
|
+
import { formatQuoteAgeWithSource, resolveQuoteAgeTimestamp } from "../../../utils/quote-time";
|
|
7
7
|
import { convertCurrency, formatCompact, formatNumber, formatPercentRaw } from "../../../utils/format";
|
|
8
8
|
import { formatMarketCost, formatMarketPrice, formatMarketQuantity, formatSignedMarketPrice, type MarketFormatOptions } from "../../../utils/market-format";
|
|
9
9
|
import { getActiveQuoteDisplay, marketStateDot, type ActiveQuoteDisplay } from "../../../utils/market-status";
|
|
10
10
|
import { formatOptionTicker } from "../../../utils/options";
|
|
11
|
+
import { PRICE_SPARKLINE_COLUMN_ID } from "../../../components/price-sparkline-view";
|
|
11
12
|
|
|
12
13
|
export interface ColumnContext {
|
|
13
14
|
activeTab?: string;
|
|
@@ -293,6 +294,8 @@ export function getColumnValue(
|
|
|
293
294
|
return { text: "—" };
|
|
294
295
|
case "latency":
|
|
295
296
|
return { text: formatQuoteAgeWithSource(quote, ctx.now) };
|
|
297
|
+
case PRICE_SPARKLINE_COLUMN_ID:
|
|
298
|
+
return { text: "" };
|
|
296
299
|
default:
|
|
297
300
|
return { text: "—" };
|
|
298
301
|
}
|
|
@@ -386,7 +389,15 @@ export function getSortValue(
|
|
|
386
389
|
}
|
|
387
390
|
return null;
|
|
388
391
|
case "latency":
|
|
389
|
-
return quote
|
|
392
|
+
return quote ? ctx.now - (resolveQuoteAgeTimestamp(quote, ctx.now) ?? ctx.now) : null;
|
|
393
|
+
case PRICE_SPARKLINE_COLUMN_ID: {
|
|
394
|
+
const values = (financials?.priceHistory ?? [])
|
|
395
|
+
.map((point) => point.close)
|
|
396
|
+
.filter((value) => Number.isFinite(value));
|
|
397
|
+
const first = values[0];
|
|
398
|
+
const last = values.at(-1);
|
|
399
|
+
return first != null && last != null && first !== 0 ? ((last - first) / Math.abs(first)) * 100 : null;
|
|
400
|
+
}
|
|
390
401
|
default:
|
|
391
402
|
return null;
|
|
392
403
|
}
|
|
@@ -121,6 +121,23 @@ export function addTickerToPortfolio(
|
|
|
121
121
|
};
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
export function addTickerToWatchlist(
|
|
125
|
+
ticker: TickerRecord,
|
|
126
|
+
watchlistId: string,
|
|
127
|
+
): { changed: boolean; ticker: TickerRecord } {
|
|
128
|
+
if (ticker.metadata.watchlists.includes(watchlistId)) {
|
|
129
|
+
return { changed: false, ticker };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
changed: true,
|
|
134
|
+
ticker: replaceTickerMetadata(ticker, {
|
|
135
|
+
...ticker.metadata,
|
|
136
|
+
watchlists: [...ticker.metadata.watchlists, watchlistId],
|
|
137
|
+
}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
124
141
|
export function removeTickerFromPortfolio(
|
|
125
142
|
ticker: TickerRecord,
|
|
126
143
|
portfolioId: string,
|
|
@@ -43,20 +43,50 @@ import {
|
|
|
43
43
|
} from "./settings";
|
|
44
44
|
import { formatPercentRaw } from "../../../utils/format";
|
|
45
45
|
import { getMostRecentQuoteUpdate } from "../../../utils/quote-time";
|
|
46
|
+
import { isQuoteStaleForCurrentSession } from "../../../utils/quote-freshness";
|
|
46
47
|
import { priceColor } from "../../../theme/colors";
|
|
47
48
|
import { PortfolioTickerTable, type QuoteFlashDirection } from "./table";
|
|
48
49
|
import { useThrottledCursorSymbol } from "./use-throttled-cursor-symbol";
|
|
50
|
+
import { isManualPortfolio } from "./mutations";
|
|
51
|
+
import { QuickAddTickerInput, type QuickAddCollectionKind } from "./quick-add";
|
|
52
|
+
import { PRICE_SPARKLINE_COLUMN_ID } from "../../../components/price-sparkline-view";
|
|
49
53
|
|
|
50
54
|
const VISIBLE_FINANCIAL_REFRESH_COOLDOWN_MS = 5 * 60_000;
|
|
51
55
|
const VISIBLE_FINANCIAL_WARMUP_DELAY_MS = 350;
|
|
56
|
+
const VISIBLE_SNAPSHOT_WARMUP_BATCH_LIMIT = 3;
|
|
52
57
|
const STREAM_OVERSCAN_ROWS = 6;
|
|
58
|
+
const FUNDAMENTAL_COLUMN_IDS = new Set(["pe", "forward_pe", "dividend_yield"]);
|
|
53
59
|
const sortValueCache = createRowValueCache<string, ReturnType<typeof getSortValue>>(5000);
|
|
54
60
|
|
|
55
|
-
|
|
61
|
+
interface VisibleWarmupRequirements {
|
|
62
|
+
fundamentals: boolean;
|
|
63
|
+
priceHistory: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function resolveVisibleWarmupRequirements(columns: ColumnConfig[]): VisibleWarmupRequirements {
|
|
67
|
+
return {
|
|
68
|
+
fundamentals: columns.some((column) => FUNDAMENTAL_COLUMN_IDS.has(column.id)),
|
|
69
|
+
priceHistory: columns.some((column) => column.id === PRICE_SPARKLINE_COLUMN_ID),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function visibleWarmupKey(kind: "quote" | "snapshot", ticker: TickerRecord): string {
|
|
74
|
+
return `${kind}:${ticker.metadata.ticker}:${ticker.metadata.exchange ?? ""}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function needsVisibleQuoteWarmup(ticker: TickerRecord, financials: TickerFinancials | undefined, now = Date.now()): boolean {
|
|
78
|
+
return !financials?.quote || isQuoteStaleForCurrentSession(financials.quote, now);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function needsVisibleSnapshotWarmup(
|
|
82
|
+
ticker: TickerRecord,
|
|
83
|
+
financials: TickerFinancials | undefined,
|
|
84
|
+
requirements: VisibleWarmupRequirements,
|
|
85
|
+
): boolean {
|
|
56
86
|
if (ticker.metadata.assetCategory === "OPT") return false;
|
|
57
|
-
if (!financials) return
|
|
58
|
-
if (Object.keys(financials.fundamentals ?? {}).length === 0) return true;
|
|
59
|
-
return financials.priceHistory.length === 0;
|
|
87
|
+
if (!financials?.quote) return false;
|
|
88
|
+
if (requirements.fundamentals && Object.keys(financials.fundamentals ?? {}).length === 0) return true;
|
|
89
|
+
return requirements.priceHistory && financials.priceHistory.length === 0;
|
|
60
90
|
}
|
|
61
91
|
|
|
62
92
|
export function selectStreamTickers(
|
|
@@ -209,6 +239,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
209
239
|
const [now, setNow] = useState(Date.now());
|
|
210
240
|
const [flashSymbols, setFlashSymbols] = useState<Map<string, QuoteFlashDirection>>(new Map());
|
|
211
241
|
const [streamWindow, setStreamWindow] = useState({ start: 0, end: 24 });
|
|
242
|
+
const [quickAddFocused, setQuickAddFocused] = useState(false);
|
|
212
243
|
|
|
213
244
|
const previousPricesRef = useRef<Map<string, number>>(new Map());
|
|
214
245
|
const mountedRef = useRef(true);
|
|
@@ -235,6 +266,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
235
266
|
);
|
|
236
267
|
const activeCollectionId = resolveActiveCollectionId(currentCollectionId, visibleCollections);
|
|
237
268
|
const isPortfolioTab = getCollectionTypeFromConfig(config, activeCollectionId) === "portfolio";
|
|
269
|
+
const activeCollectionEntry = visibleCollections.find((collection) => collection.id === activeCollectionId) ?? null;
|
|
238
270
|
const currentPortfolio = useMemo(() => (
|
|
239
271
|
isPortfolioTab
|
|
240
272
|
? config.portfolios.find((portfolio) => portfolio.id === activeCollectionId) ?? null
|
|
@@ -248,12 +280,12 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
248
280
|
const marketFinancialsMap = useTickerFinancialsMap(tickers);
|
|
249
281
|
const sharedCoordinator = getSharedMarketDataCoordinator();
|
|
250
282
|
const financialsMap = useMemo(() => {
|
|
251
|
-
const merged =
|
|
283
|
+
const merged = new Map(cachedFinancials);
|
|
252
284
|
for (const [symbol, financials] of marketFinancialsMap) {
|
|
253
285
|
merged.set(symbol, financials);
|
|
254
286
|
}
|
|
255
287
|
return merged;
|
|
256
|
-
}, [cachedFinancials, marketFinancialsMap
|
|
288
|
+
}, [cachedFinancials, marketFinancialsMap]);
|
|
257
289
|
|
|
258
290
|
const accountStateInput = useMemo(() => ({ brokerAccounts, config }), [brokerAccounts, config]);
|
|
259
291
|
const accountState = usePortfolioAccountState(currentPortfolio, accountStateInput);
|
|
@@ -261,6 +293,10 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
261
293
|
() => resolveVisibleColumns(paneSettings.columnIds, isPortfolioTab),
|
|
262
294
|
[isPortfolioTab, paneSettings.columnIds],
|
|
263
295
|
);
|
|
296
|
+
const visibleWarmupRequirements = useMemo(
|
|
297
|
+
() => resolveVisibleWarmupRequirements(columns),
|
|
298
|
+
[columns],
|
|
299
|
+
);
|
|
264
300
|
|
|
265
301
|
const trackedCurrencies = useMemo(
|
|
266
302
|
() => buildTrackedCurrencies(tickers, financialsMap, accountState, config.baseCurrency),
|
|
@@ -334,6 +370,9 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
334
370
|
flushCursorSymbol(ticker.metadata.ticker);
|
|
335
371
|
openTickerFloating(ticker.metadata.ticker);
|
|
336
372
|
}, [flushCursorSymbol, openTickerFloating]);
|
|
373
|
+
const handleTickerAdded = useCallback((symbol: string) => {
|
|
374
|
+
setCursorSymbol(symbol, { immediate: true });
|
|
375
|
+
}, [setCursorSymbol]);
|
|
337
376
|
|
|
338
377
|
const handleTableKeyDown = useCallback((event: DataTableKeyEvent) => {
|
|
339
378
|
if (!focused) return;
|
|
@@ -425,11 +464,28 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
425
464
|
() => selectStreamTickers(sortedTickers, streamWindow, cursorSymbol),
|
|
426
465
|
[cursorSymbol, sortedTickers, streamWindow],
|
|
427
466
|
);
|
|
467
|
+
const priorityStreamSymbols = useMemo(
|
|
468
|
+
() => new Set(streamTickers.map((ticker) => ticker.metadata.ticker)),
|
|
469
|
+
[streamTickers],
|
|
470
|
+
);
|
|
471
|
+
const streamSurface = isPortfolioTab ? "portfolio" : "watchlist";
|
|
428
472
|
const streamTargets = useMemo(() => (
|
|
429
|
-
|
|
430
|
-
.map((ticker) =>
|
|
473
|
+
sortedTickers
|
|
474
|
+
.map((ticker) => {
|
|
475
|
+
const target = quoteSubscriptionTargetFromTicker(ticker, ticker.metadata.ticker, "provider");
|
|
476
|
+
if (!target) return null;
|
|
477
|
+
const selected = ticker.metadata.ticker === cursorSymbol;
|
|
478
|
+
const visible = priorityStreamSymbols.has(ticker.metadata.ticker);
|
|
479
|
+
return {
|
|
480
|
+
...target,
|
|
481
|
+
surface: streamSurface,
|
|
482
|
+
visible,
|
|
483
|
+
selected,
|
|
484
|
+
weight: selected ? 100 : visible ? 80 : 10,
|
|
485
|
+
};
|
|
486
|
+
})
|
|
431
487
|
.filter((target): target is NonNullable<typeof target> => target != null)
|
|
432
|
-
), [
|
|
488
|
+
), [cursorSymbol, priorityStreamSymbols, sortedTickers, streamSurface]);
|
|
433
489
|
const visibleFinancialTickers = useMemo(
|
|
434
490
|
() => sortedTickers.slice(streamWindow.start, streamWindow.end),
|
|
435
491
|
[sortedTickers, streamWindow.end, streamWindow.start],
|
|
@@ -441,31 +497,64 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
441
497
|
if (!sharedCoordinator) return;
|
|
442
498
|
|
|
443
499
|
const nowTimestamp = Date.now();
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
500
|
+
const quoteQueue: TickerRecord[] = [];
|
|
501
|
+
const snapshotQueue: TickerRecord[] = [];
|
|
502
|
+
for (const ticker of visibleFinancialTickers) {
|
|
503
|
+
const financials = financialsMap.get(ticker.metadata.ticker);
|
|
504
|
+
const quoteKey = visibleWarmupKey("quote", ticker);
|
|
505
|
+
if (
|
|
506
|
+
needsVisibleQuoteWarmup(ticker, financials, nowTimestamp)
|
|
507
|
+
&& !warmupInFlightRef.current.has(quoteKey)
|
|
508
|
+
&& nowTimestamp - (warmupAttemptRef.current.get(quoteKey) ?? 0) >= VISIBLE_FINANCIAL_REFRESH_COOLDOWN_MS
|
|
509
|
+
) {
|
|
510
|
+
quoteQueue.push(ticker);
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const snapshotKey = visibleWarmupKey("snapshot", ticker);
|
|
515
|
+
if (
|
|
516
|
+
needsVisibleSnapshotWarmup(ticker, financials, visibleWarmupRequirements)
|
|
517
|
+
&& !warmupInFlightRef.current.has(snapshotKey)
|
|
518
|
+
&& nowTimestamp - (warmupAttemptRef.current.get(snapshotKey) ?? 0) >= VISIBLE_FINANCIAL_REFRESH_COOLDOWN_MS
|
|
519
|
+
) {
|
|
520
|
+
snapshotQueue.push(ticker);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
const limitedSnapshotQueue = snapshotQueue.slice(0, VISIBLE_SNAPSHOT_WARMUP_BATCH_LIMIT);
|
|
524
|
+
if (quoteQueue.length === 0 && limitedSnapshotQueue.length === 0) return;
|
|
451
525
|
|
|
452
526
|
let cancelled = false;
|
|
453
527
|
const runBatch = async (): Promise<void> => {
|
|
454
|
-
const
|
|
528
|
+
const quoteEntries = quoteQueue.flatMap((ticker) => {
|
|
529
|
+
const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker);
|
|
530
|
+
if (!instrument) return [];
|
|
531
|
+
const key = visibleWarmupKey("quote", ticker);
|
|
532
|
+
warmupInFlightRef.current.add(key);
|
|
533
|
+
warmupAttemptRef.current.set(key, nowTimestamp);
|
|
534
|
+
return [{ key, instrument }];
|
|
535
|
+
});
|
|
536
|
+
const snapshotEntries = limitedSnapshotQueue.flatMap((ticker) => {
|
|
455
537
|
const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker);
|
|
456
538
|
if (!instrument) return [];
|
|
457
|
-
const key =
|
|
539
|
+
const key = visibleWarmupKey("snapshot", ticker);
|
|
458
540
|
warmupInFlightRef.current.add(key);
|
|
459
541
|
warmupAttemptRef.current.set(key, nowTimestamp);
|
|
460
542
|
return [{ key, instrument }];
|
|
461
543
|
});
|
|
462
|
-
if (
|
|
544
|
+
if (quoteEntries.length === 0 && snapshotEntries.length === 0) return;
|
|
463
545
|
try {
|
|
464
|
-
await
|
|
546
|
+
await Promise.allSettled([
|
|
547
|
+
quoteEntries.length > 0
|
|
548
|
+
? sharedCoordinator.loadQuotesBatch(quoteEntries.map((entry) => entry.instrument))
|
|
549
|
+
: Promise.resolve(),
|
|
550
|
+
snapshotEntries.length > 0
|
|
551
|
+
? sharedCoordinator.loadSnapshotsBatch(snapshotEntries.map((entry) => entry.instrument))
|
|
552
|
+
: Promise.resolve(),
|
|
553
|
+
]);
|
|
465
554
|
} catch {
|
|
466
555
|
// Best-effort warmup for visible rows only.
|
|
467
556
|
} finally {
|
|
468
|
-
for (const entry of
|
|
557
|
+
for (const entry of [...quoteEntries, ...snapshotEntries]) {
|
|
469
558
|
warmupInFlightRef.current.delete(entry.key);
|
|
470
559
|
}
|
|
471
560
|
}
|
|
@@ -479,7 +568,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
479
568
|
cancelled = true;
|
|
480
569
|
clearTimeout(timeoutId);
|
|
481
570
|
};
|
|
482
|
-
}, [appActive, financialsMap, focused, sharedCoordinator, visibleFinancialTickers]);
|
|
571
|
+
}, [appActive, financialsMap, focused, sharedCoordinator, visibleFinancialTickers, visibleWarmupRequirements]);
|
|
483
572
|
|
|
484
573
|
useQuoteStreaming(streamTargets);
|
|
485
574
|
|
|
@@ -555,6 +644,30 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
555
644
|
: [],
|
|
556
645
|
}), [cashDrawerExpanded, setCashDrawerExpanded, showCashDrawer, summaryFooterInfo]);
|
|
557
646
|
|
|
647
|
+
const quickAddCollectionKind = useMemo<QuickAddCollectionKind | null>(() => {
|
|
648
|
+
if (!activeCollectionId) return null;
|
|
649
|
+
const collectionType = getCollectionTypeFromConfig(config, activeCollectionId);
|
|
650
|
+
if (collectionType === "watchlist") return "watchlist";
|
|
651
|
+
if (collectionType === "portfolio" && currentPortfolio && isManualPortfolio(currentPortfolio)) {
|
|
652
|
+
return "portfolio";
|
|
653
|
+
}
|
|
654
|
+
return null;
|
|
655
|
+
}, [activeCollectionId, config, currentPortfolio]);
|
|
656
|
+
const showQuickAdd = !!(activeCollectionId && activeCollectionEntry && quickAddCollectionKind);
|
|
657
|
+
const quickAddHeight = showQuickAdd ? 1 : 0;
|
|
658
|
+
const tableHeight = Math.max(1, height - headerHeight - drawerHeight - quickAddHeight);
|
|
659
|
+
const quickAddRow = activeCollectionId && activeCollectionEntry && quickAddCollectionKind ? (
|
|
660
|
+
<QuickAddTickerInput
|
|
661
|
+
collectionId={activeCollectionId}
|
|
662
|
+
collectionKind={quickAddCollectionKind}
|
|
663
|
+
collectionName={activeCollectionEntry.name}
|
|
664
|
+
focused={focused}
|
|
665
|
+
width={width}
|
|
666
|
+
onAdded={handleTickerAdded}
|
|
667
|
+
onFocusChange={setQuickAddFocused}
|
|
668
|
+
/>
|
|
669
|
+
) : null;
|
|
670
|
+
|
|
558
671
|
return (
|
|
559
672
|
<Box flexDirection="column" width={width} height={height}>
|
|
560
673
|
<Box flexDirection="column" height={headerHeight}>
|
|
@@ -575,7 +688,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
575
688
|
|
|
576
689
|
<PortfolioTickerTable
|
|
577
690
|
columns={columns}
|
|
578
|
-
focused={focused}
|
|
691
|
+
focused={focused && !quickAddFocused}
|
|
579
692
|
sortColumnId={activeSort.columnId}
|
|
580
693
|
sortDirection={activeSort.direction}
|
|
581
694
|
onHeaderClick={handleHeaderClick}
|
|
@@ -590,8 +703,11 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
590
703
|
visibleRangeBuffer={3}
|
|
591
704
|
resetScrollKey={activeCollectionId}
|
|
592
705
|
onRowActivate={handleRowActivate}
|
|
706
|
+
rootHeight={tableHeight}
|
|
593
707
|
/>
|
|
594
708
|
|
|
709
|
+
{quickAddRow}
|
|
710
|
+
|
|
595
711
|
{showCashDrawer && accountState && (
|
|
596
712
|
<Box height={drawerHeight} paddingX={1}>
|
|
597
713
|
<PortfolioCashMarginDrawer
|