gloomberb 0.7.1 → 0.7.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/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 +49 -15
- package/src/components/chart/chart-types.ts +2 -2
- package/src/components/chart/chart.test.ts +27 -8
- package/src/components/chart/comparison-stock-chart.tsx +84 -121
- package/src/components/chart/native/chart-rasterizer.ts +43 -17
- package/src/components/chart/price-axis-labels.tsx +25 -9
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +1 -1
- package/src/components/chart/stock-chart.tsx +59 -41
- package/src/components/command-bar/command-bar.test.tsx +446 -120
- package/src/components/command-bar/command-bar.tsx +528 -383
- package/src/components/command-bar/command-registry.ts +16 -6
- package/src/components/command-bar/root-shortcuts.ts +4 -5
- package/src/components/command-bar/view-model.test.ts +1 -0
- package/src/components/command-bar/view-model.ts +2 -1
- 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 +47 -7
- package/src/components/ticker-list-table-view.tsx +3 -2
- package/src/components/ticker-list-table.tsx +11 -51
- 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/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.ts +8 -1
- 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/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 +258 -44
- package/src/plugins/builtin/chat.test.tsx +257 -6
- package/src/plugins/builtin/chat.tsx +225 -60
- 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/news-wire/index.tsx +1 -1
- 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 +11 -0
- package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +117 -18
- 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 +282 -2
- package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
- package/src/plugins/builtin/sectors/index.tsx +252 -41
- 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 +0 -6
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +442 -63
- package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
- package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
- 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 +67 -11
- 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/state/app-bootstrap.ts +3 -3
- package/src/state/app-context.test.ts +113 -0
- package/src/state/app-context.tsx +8 -4
- package/src/theme/colors.ts +13 -14
- package/src/types/config.ts +9 -4
- package/src/types/plugin.ts +8 -1
- package/src/ui/context-menu.tsx +4 -4
- package/src/ui/host.tsx +8 -2
- package/src/ui/index.tsx +1 -0
- package/src/utils/api-client.test.ts +128 -0
- package/src/utils/api-client.ts +112 -7
- package/src/utils/fuzzy-search.ts +12 -1
- package/src/utils/keyboard.ts +31 -0
- package/src/utils/ticker-search.test.ts +2 -2
- package/src/utils/ticker-search.ts +48 -1
- package/src/version.ts +1 -1
|
@@ -11,9 +11,11 @@ import {
|
|
|
11
11
|
} from "../../../components";
|
|
12
12
|
import { useShortcut } from "../../../react/input";
|
|
13
13
|
import { blendHex, colors, priceColor } from "../../../theme/colors";
|
|
14
|
+
import { higherContrast } from "../../../theme/color-utils";
|
|
14
15
|
import type { HolderData, HolderRecord } from "../../../types/financials";
|
|
15
16
|
import type { GloomPlugin } from "../../../types/plugin";
|
|
16
17
|
import { formatCompact, formatPercent, formatPercentRaw, padTo } from "../../../utils/format";
|
|
18
|
+
import { isPlainKey } from "../../../utils/keyboard";
|
|
17
19
|
import { useAssetData, usePluginPaneState } from "../../plugin-runtime";
|
|
18
20
|
import { createTickerSurfacePaneTemplate } from "../ticker-surface";
|
|
19
21
|
|
|
@@ -445,6 +447,14 @@ function desktopTileColor(row: HolderRow): string {
|
|
|
445
447
|
return blendHex(colors.panel, row.changePercent > 0 ? colors.positive : colors.negative, intensity);
|
|
446
448
|
}
|
|
447
449
|
|
|
450
|
+
function tileTextColor(backgroundColor: string): string {
|
|
451
|
+
return higherContrast(
|
|
452
|
+
higherContrast(colors.text, colors.textBright, backgroundColor),
|
|
453
|
+
colors.selectedText,
|
|
454
|
+
backgroundColor,
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
448
458
|
function Tile({ tile, selected, currency, marketCap, onSelect }: {
|
|
449
459
|
tile: TileLayout;
|
|
450
460
|
selected: boolean;
|
|
@@ -455,7 +465,8 @@ function Tile({ tile, selected, currency, marketCap, onSelect }: {
|
|
|
455
465
|
const renderWidth = Math.max(1, tile.width - (tile.width > 2 ? 1 : 0));
|
|
456
466
|
const renderHeight = Math.max(1, tile.height - (tile.height > 2 ? 1 : 0));
|
|
457
467
|
const innerWidth = Math.max(1, renderWidth - 1);
|
|
458
|
-
const
|
|
468
|
+
const backgroundColor = selected ? colors.selected : tileColor(tile.row);
|
|
469
|
+
const textColor = tileTextColor(backgroundColor);
|
|
459
470
|
const attributes = selected ? TextAttributes.BOLD : TextAttributes.NONE;
|
|
460
471
|
const label = tile.row.name;
|
|
461
472
|
const amount = tile.row.value != null
|
|
@@ -471,7 +482,7 @@ function Tile({ tile, selected, currency, marketCap, onSelect }: {
|
|
|
471
482
|
top={tile.y}
|
|
472
483
|
width={renderWidth}
|
|
473
484
|
height={renderHeight}
|
|
474
|
-
backgroundColor={
|
|
485
|
+
backgroundColor={backgroundColor}
|
|
475
486
|
onMouseDown={(event: PreventableMouseEvent) => {
|
|
476
487
|
event.preventDefault();
|
|
477
488
|
onSelect();
|
|
@@ -516,6 +527,7 @@ function DesktopTile({ tile, chartWidth, chartHeight, selected, hovered, currenc
|
|
|
516
527
|
const canShowLabel = tile.width >= 4 && tile.height >= 1.4;
|
|
517
528
|
const isTiny = tile.width < 5 || tile.height < 2;
|
|
518
529
|
const backgroundColor = desktopTileColor(tile.row);
|
|
530
|
+
const textColor = tileTextColor(backgroundColor);
|
|
519
531
|
const style: CSSProperties = {
|
|
520
532
|
position: "absolute",
|
|
521
533
|
left: `calc(${pct(tile.x, chartWidth)} + 1px)`,
|
|
@@ -531,14 +543,14 @@ function DesktopTile({ tile, chartWidth, chartHeight, selected, hovered, currenc
|
|
|
531
543
|
? `1px solid ${colors.textBright}`
|
|
532
544
|
: hovered
|
|
533
545
|
? `1px solid ${blendHex(colors.textBright, backgroundColor, 0.55)}`
|
|
534
|
-
:
|
|
546
|
+
: `1px solid ${blendHex(backgroundColor, colors.textBright, 0.11)}`,
|
|
535
547
|
boxShadow: selected
|
|
536
|
-
?
|
|
548
|
+
? `inset 0 0 0 1px ${blendHex(backgroundColor, colors.textBright, 0.55)}`
|
|
537
549
|
: hovered
|
|
538
|
-
?
|
|
550
|
+
? `inset 0 0 0 1px ${blendHex(backgroundColor, colors.textBright, 0.18)}`
|
|
539
551
|
: "none",
|
|
540
552
|
backgroundColor,
|
|
541
|
-
color:
|
|
553
|
+
color: textColor,
|
|
542
554
|
display: "flex",
|
|
543
555
|
flexDirection: "column",
|
|
544
556
|
alignItems: "stretch",
|
|
@@ -557,7 +569,7 @@ function DesktopTile({ tile, chartWidth, chartHeight, selected, hovered, currenc
|
|
|
557
569
|
whiteSpace: "nowrap",
|
|
558
570
|
lineHeight: 1.1,
|
|
559
571
|
letterSpacing: 0,
|
|
560
|
-
textShadow:
|
|
572
|
+
textShadow: `0 1px 1px ${blendHex(backgroundColor, colors.bg, 0.35)}`,
|
|
561
573
|
};
|
|
562
574
|
|
|
563
575
|
return (
|
|
@@ -573,7 +585,7 @@ function DesktopTile({ tile, chartWidth, chartHeight, selected, hovered, currenc
|
|
|
573
585
|
>
|
|
574
586
|
{canShowLabel && (
|
|
575
587
|
<Text
|
|
576
|
-
fg=
|
|
588
|
+
fg={textColor}
|
|
577
589
|
attributes={selected ? TextAttributes.BOLD : TextAttributes.NONE}
|
|
578
590
|
style={{
|
|
579
591
|
...textStyle,
|
|
@@ -586,10 +598,10 @@ function DesktopTile({ tile, chartWidth, chartHeight, selected, hovered, currenc
|
|
|
586
598
|
)}
|
|
587
599
|
{canShowDetails && (
|
|
588
600
|
<>
|
|
589
|
-
<Text fg=
|
|
590
|
-
<Text fg=
|
|
601
|
+
<Text fg={textColor} style={{ ...textStyle, fontSize: 12, fontWeight: 600 }}>{amount}</Text>
|
|
602
|
+
<Text fg={textColor} style={{ ...textStyle, fontSize: 12, fontWeight: 600 }}>{ownership ?? change}</Text>
|
|
591
603
|
{ownership && canShowChange && (
|
|
592
|
-
<Text fg=
|
|
604
|
+
<Text fg={textColor} style={{ ...textStyle, fontSize: 12, fontWeight: 600 }}>{change}</Text>
|
|
593
605
|
)}
|
|
594
606
|
</>
|
|
595
607
|
)}
|
|
@@ -810,7 +822,7 @@ function HoldersView({ focused, width, height }: { focused: boolean; width: numb
|
|
|
810
822
|
|
|
811
823
|
useShortcut((event) => {
|
|
812
824
|
if (!focused || viewMode !== "chart") return;
|
|
813
|
-
if (event
|
|
825
|
+
if (isPlainKey(event, "j", "down")) {
|
|
814
826
|
event.preventDefault?.();
|
|
815
827
|
event.stopPropagation?.();
|
|
816
828
|
const nextIdx = Math.min((selectedIdx >= 0 ? selectedIdx : 0) + 1, sortedRows.length - 1);
|
|
@@ -818,7 +830,7 @@ function HoldersView({ focused, width, height }: { focused: boolean; width: numb
|
|
|
818
830
|
if (nextRow) setSelectedId(nextRow.id);
|
|
819
831
|
return;
|
|
820
832
|
}
|
|
821
|
-
if (event
|
|
833
|
+
if (isPlainKey(event, "k", "up")) {
|
|
822
834
|
event.preventDefault?.();
|
|
823
835
|
event.stopPropagation?.();
|
|
824
836
|
const nextIdx = Math.max((selectedIdx >= 0 ? selectedIdx : 0) - 1, 0);
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { saveConfig } from "../../data/config-store";
|
|
2
1
|
import { findPaneInstance, type LayoutConfig } from "../../types/config";
|
|
3
2
|
import type { AppNotificationRequest, GloomPlugin, GloomPluginContext } from "../../types/plugin";
|
|
4
3
|
import type { AppAction } from "../../state/app-context";
|
|
@@ -30,15 +29,10 @@ export function clearLayoutManagerDispatch() {
|
|
|
30
29
|
getStateRef = null;
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
function persistLayout(
|
|
32
|
+
function persistLayout(_ctx: Pick<GloomPluginContext, "getConfig">, layout: LayoutConfig) {
|
|
34
33
|
if (!dispatchRef) return;
|
|
35
34
|
dispatchRef({ type: "PUSH_LAYOUT_HISTORY" });
|
|
36
35
|
dispatchRef({ type: "UPDATE_LAYOUT", layout });
|
|
37
|
-
const config = ctx.getConfig();
|
|
38
|
-
const layouts = config.layouts.map((savedLayout, index) => (
|
|
39
|
-
index === config.activeLayoutIndex ? { ...savedLayout, layout } : savedLayout
|
|
40
|
-
));
|
|
41
|
-
saveConfig({ ...config, layout, layouts }).catch(() => {});
|
|
42
36
|
}
|
|
43
37
|
|
|
44
38
|
function getFocusedPane(layout: LayoutConfig, focusedPaneId: string | null) {
|
|
@@ -39,7 +39,7 @@ const TopPane = createNewsPresetPane({
|
|
|
39
39
|
paneKey: "top",
|
|
40
40
|
title: "Top News",
|
|
41
41
|
query: NEWS_QUERY_PRESETS.top,
|
|
42
|
-
columns: ["
|
|
42
|
+
columns: ["time", "title", "tickers", "importance"],
|
|
43
43
|
defaultSort: { columnId: "importance", direction: "desc" },
|
|
44
44
|
emptyStateTitle: "No top stories yet",
|
|
45
45
|
emptyStateHint: "Try refreshing later as new headlines are ranked.",
|
|
@@ -8,6 +8,7 @@ import { TickerBadge } from "../../../components/ticker-badge";
|
|
|
8
8
|
import { ExternalLink, ExternalLinkText } from "../../../components/ui";
|
|
9
9
|
import { collectNewsDisplayTickers } from "../../../news/ticker-symbols";
|
|
10
10
|
import { useInlineTickers } from "../../../state/use-inline-tickers";
|
|
11
|
+
import { isPlainKey } from "../../../utils/keyboard";
|
|
11
12
|
import { wrapTextLines } from "../../../utils/text-wrap";
|
|
12
13
|
|
|
13
14
|
function hasStoryItems(article: MarketNewsItem | null): boolean {
|
|
@@ -196,13 +197,13 @@ export function NewsDetailView({ item, focused, width, showTitle = true }: {
|
|
|
196
197
|
|
|
197
198
|
useShortcut((event) => {
|
|
198
199
|
if (!focused) return;
|
|
199
|
-
if (event
|
|
200
|
+
if (isPlainKey(event, "j", "down")) {
|
|
200
201
|
event.stopPropagation?.();
|
|
201
202
|
event.preventDefault?.();
|
|
202
203
|
scrollBy(1);
|
|
203
204
|
return;
|
|
204
205
|
}
|
|
205
|
-
if (event
|
|
206
|
+
if (isPlainKey(event, "k", "up")) {
|
|
206
207
|
event.stopPropagation?.();
|
|
207
208
|
event.preventDefault?.();
|
|
208
209
|
scrollBy(-1);
|
|
@@ -45,6 +45,9 @@ export function NewsPresetPane({
|
|
|
45
45
|
`${paneKey}:sort`,
|
|
46
46
|
defaultSort,
|
|
47
47
|
);
|
|
48
|
+
const effectiveSortPreference = columns.includes(sortPreference.columnId)
|
|
49
|
+
? sortPreference
|
|
50
|
+
: defaultSort;
|
|
48
51
|
const loadNewsStory = useLoadNewsStory();
|
|
49
52
|
const { detailArticle, openArticle, closeDetail } = useNewsArticleDetail(articles, loadNewsStory);
|
|
50
53
|
const { readArticleIds, markArticleRead } = useNewsReadState();
|
|
@@ -79,7 +82,7 @@ export function NewsPresetPane({
|
|
|
79
82
|
readArticleIds={readArticleIds}
|
|
80
83
|
selectedArticleId={selectedArticleId}
|
|
81
84
|
setSelectedArticleId={setSelectedArticleId}
|
|
82
|
-
sortPreference={
|
|
85
|
+
sortPreference={effectiveSortPreference}
|
|
83
86
|
setSortPreference={setSortPreference}
|
|
84
87
|
onOpenArticle={openArticle}
|
|
85
88
|
onArticleRead={markArticleRead}
|
|
@@ -65,10 +65,16 @@ function samePersistedArticles(left: PersistedNewsArticle[], right: PersistedNew
|
|
|
65
65
|
if (left.length !== right.length) return false;
|
|
66
66
|
return left.every((article, index) => {
|
|
67
67
|
const other = right[index];
|
|
68
|
-
return other?.id === article.id &&
|
|
68
|
+
return other?.id === article.id &&
|
|
69
|
+
other.publishedAt === article.publishedAt &&
|
|
70
|
+
storyItemsSignature(other.items) === storyItemsSignature(article.items);
|
|
69
71
|
});
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
function storyItemsSignature(items: PersistedNewsStoryItem[] | undefined): string {
|
|
75
|
+
return items?.map((item) => `${item.id}:${item.publishedAt}`).join("|") ?? "";
|
|
76
|
+
}
|
|
77
|
+
|
|
72
78
|
export function usePersistedNewsArticles(key: string, articles: MarketNewsItem[]): MarketNewsItem[] {
|
|
73
79
|
const [persistedArticles, setPersistedArticles] = usePluginPaneState<PersistedNewsArticle[]>(
|
|
74
80
|
key,
|
|
@@ -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
|
|
|
@@ -8,6 +8,7 @@ import { convertCurrency, formatCompact, formatNumber, formatPercentRaw } from "
|
|
|
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
|
}
|
|
@@ -387,6 +390,14 @@ export function getSortValue(
|
|
|
387
390
|
return null;
|
|
388
391
|
case "latency":
|
|
389
392
|
return quote?.lastUpdated != null ? ctx.now - (clampQuoteTimestamp(quote.lastUpdated, 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,
|
|
@@ -46,17 +46,47 @@ import { getMostRecentQuoteUpdate } from "../../../utils/quote-time";
|
|
|
46
46
|
import { priceColor } from "../../../theme/colors";
|
|
47
47
|
import { PortfolioTickerTable, type QuoteFlashDirection } from "./table";
|
|
48
48
|
import { useThrottledCursorSymbol } from "./use-throttled-cursor-symbol";
|
|
49
|
+
import { isManualPortfolio } from "./mutations";
|
|
50
|
+
import { QuickAddTickerInput, type QuickAddCollectionKind } from "./quick-add";
|
|
51
|
+
import { PRICE_SPARKLINE_COLUMN_ID } from "../../../components/price-sparkline-view";
|
|
49
52
|
|
|
50
53
|
const VISIBLE_FINANCIAL_REFRESH_COOLDOWN_MS = 5 * 60_000;
|
|
51
54
|
const VISIBLE_FINANCIAL_WARMUP_DELAY_MS = 350;
|
|
55
|
+
const VISIBLE_SNAPSHOT_WARMUP_BATCH_LIMIT = 3;
|
|
52
56
|
const STREAM_OVERSCAN_ROWS = 6;
|
|
57
|
+
const FUNDAMENTAL_COLUMN_IDS = new Set(["pe", "forward_pe", "dividend_yield"]);
|
|
53
58
|
const sortValueCache = createRowValueCache<string, ReturnType<typeof getSortValue>>(5000);
|
|
54
59
|
|
|
55
|
-
|
|
60
|
+
interface VisibleWarmupRequirements {
|
|
61
|
+
fundamentals: boolean;
|
|
62
|
+
priceHistory: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveVisibleWarmupRequirements(columns: ColumnConfig[]): VisibleWarmupRequirements {
|
|
66
|
+
return {
|
|
67
|
+
fundamentals: columns.some((column) => FUNDAMENTAL_COLUMN_IDS.has(column.id)),
|
|
68
|
+
priceHistory: columns.some((column) => column.id === PRICE_SPARKLINE_COLUMN_ID),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function visibleWarmupKey(kind: "quote" | "snapshot", ticker: TickerRecord): string {
|
|
73
|
+
return `${kind}:${ticker.metadata.ticker}:${ticker.metadata.exchange ?? ""}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function needsVisibleQuoteWarmup(ticker: TickerRecord, financials: TickerFinancials | undefined): boolean {
|
|
56
77
|
if (ticker.metadata.assetCategory === "OPT") return false;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
78
|
+
return !financials?.quote;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function needsVisibleSnapshotWarmup(
|
|
82
|
+
ticker: TickerRecord,
|
|
83
|
+
financials: TickerFinancials | undefined,
|
|
84
|
+
requirements: VisibleWarmupRequirements,
|
|
85
|
+
): boolean {
|
|
86
|
+
if (ticker.metadata.assetCategory === "OPT") return false;
|
|
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
|
|
@@ -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;
|
|
@@ -441,31 +480,64 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
441
480
|
if (!sharedCoordinator) return;
|
|
442
481
|
|
|
443
482
|
const nowTimestamp = Date.now();
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
483
|
+
const quoteQueue: TickerRecord[] = [];
|
|
484
|
+
const snapshotQueue: TickerRecord[] = [];
|
|
485
|
+
for (const ticker of visibleFinancialTickers) {
|
|
486
|
+
const financials = financialsMap.get(ticker.metadata.ticker);
|
|
487
|
+
const quoteKey = visibleWarmupKey("quote", ticker);
|
|
488
|
+
if (
|
|
489
|
+
needsVisibleQuoteWarmup(ticker, financials)
|
|
490
|
+
&& !warmupInFlightRef.current.has(quoteKey)
|
|
491
|
+
&& nowTimestamp - (warmupAttemptRef.current.get(quoteKey) ?? 0) >= VISIBLE_FINANCIAL_REFRESH_COOLDOWN_MS
|
|
492
|
+
) {
|
|
493
|
+
quoteQueue.push(ticker);
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const snapshotKey = visibleWarmupKey("snapshot", ticker);
|
|
498
|
+
if (
|
|
499
|
+
needsVisibleSnapshotWarmup(ticker, financials, visibleWarmupRequirements)
|
|
500
|
+
&& !warmupInFlightRef.current.has(snapshotKey)
|
|
501
|
+
&& nowTimestamp - (warmupAttemptRef.current.get(snapshotKey) ?? 0) >= VISIBLE_FINANCIAL_REFRESH_COOLDOWN_MS
|
|
502
|
+
) {
|
|
503
|
+
snapshotQueue.push(ticker);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const limitedSnapshotQueue = snapshotQueue.slice(0, VISIBLE_SNAPSHOT_WARMUP_BATCH_LIMIT);
|
|
507
|
+
if (quoteQueue.length === 0 && limitedSnapshotQueue.length === 0) return;
|
|
451
508
|
|
|
452
509
|
let cancelled = false;
|
|
453
510
|
const runBatch = async (): Promise<void> => {
|
|
454
|
-
const
|
|
511
|
+
const quoteEntries = quoteQueue.flatMap((ticker) => {
|
|
512
|
+
const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker);
|
|
513
|
+
if (!instrument) return [];
|
|
514
|
+
const key = visibleWarmupKey("quote", ticker);
|
|
515
|
+
warmupInFlightRef.current.add(key);
|
|
516
|
+
warmupAttemptRef.current.set(key, nowTimestamp);
|
|
517
|
+
return [{ key, instrument }];
|
|
518
|
+
});
|
|
519
|
+
const snapshotEntries = limitedSnapshotQueue.flatMap((ticker) => {
|
|
455
520
|
const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker);
|
|
456
521
|
if (!instrument) return [];
|
|
457
|
-
const key =
|
|
522
|
+
const key = visibleWarmupKey("snapshot", ticker);
|
|
458
523
|
warmupInFlightRef.current.add(key);
|
|
459
524
|
warmupAttemptRef.current.set(key, nowTimestamp);
|
|
460
525
|
return [{ key, instrument }];
|
|
461
526
|
});
|
|
462
|
-
if (
|
|
527
|
+
if (quoteEntries.length === 0 && snapshotEntries.length === 0) return;
|
|
463
528
|
try {
|
|
464
|
-
await
|
|
529
|
+
await Promise.allSettled([
|
|
530
|
+
quoteEntries.length > 0
|
|
531
|
+
? sharedCoordinator.loadQuotesBatch(quoteEntries.map((entry) => entry.instrument))
|
|
532
|
+
: Promise.resolve(),
|
|
533
|
+
snapshotEntries.length > 0
|
|
534
|
+
? sharedCoordinator.loadSnapshotsBatch(snapshotEntries.map((entry) => entry.instrument))
|
|
535
|
+
: Promise.resolve(),
|
|
536
|
+
]);
|
|
465
537
|
} catch {
|
|
466
538
|
// Best-effort warmup for visible rows only.
|
|
467
539
|
} finally {
|
|
468
|
-
for (const entry of
|
|
540
|
+
for (const entry of [...quoteEntries, ...snapshotEntries]) {
|
|
469
541
|
warmupInFlightRef.current.delete(entry.key);
|
|
470
542
|
}
|
|
471
543
|
}
|
|
@@ -479,7 +551,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
479
551
|
cancelled = true;
|
|
480
552
|
clearTimeout(timeoutId);
|
|
481
553
|
};
|
|
482
|
-
}, [appActive, financialsMap, focused, sharedCoordinator, visibleFinancialTickers]);
|
|
554
|
+
}, [appActive, financialsMap, focused, sharedCoordinator, visibleFinancialTickers, visibleWarmupRequirements]);
|
|
483
555
|
|
|
484
556
|
useQuoteStreaming(streamTargets);
|
|
485
557
|
|
|
@@ -555,6 +627,30 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
555
627
|
: [],
|
|
556
628
|
}), [cashDrawerExpanded, setCashDrawerExpanded, showCashDrawer, summaryFooterInfo]);
|
|
557
629
|
|
|
630
|
+
const quickAddCollectionKind = useMemo<QuickAddCollectionKind | null>(() => {
|
|
631
|
+
if (!activeCollectionId) return null;
|
|
632
|
+
const collectionType = getCollectionTypeFromConfig(config, activeCollectionId);
|
|
633
|
+
if (collectionType === "watchlist") return "watchlist";
|
|
634
|
+
if (collectionType === "portfolio" && currentPortfolio && isManualPortfolio(currentPortfolio)) {
|
|
635
|
+
return "portfolio";
|
|
636
|
+
}
|
|
637
|
+
return null;
|
|
638
|
+
}, [activeCollectionId, config, currentPortfolio]);
|
|
639
|
+
const showQuickAdd = !!(activeCollectionId && activeCollectionEntry && quickAddCollectionKind);
|
|
640
|
+
const quickAddHeight = showQuickAdd ? 1 : 0;
|
|
641
|
+
const tableHeight = Math.max(1, height - headerHeight - drawerHeight - quickAddHeight);
|
|
642
|
+
const quickAddRow = activeCollectionId && activeCollectionEntry && quickAddCollectionKind ? (
|
|
643
|
+
<QuickAddTickerInput
|
|
644
|
+
collectionId={activeCollectionId}
|
|
645
|
+
collectionKind={quickAddCollectionKind}
|
|
646
|
+
collectionName={activeCollectionEntry.name}
|
|
647
|
+
focused={focused}
|
|
648
|
+
width={width}
|
|
649
|
+
onAdded={handleTickerAdded}
|
|
650
|
+
onFocusChange={setQuickAddFocused}
|
|
651
|
+
/>
|
|
652
|
+
) : null;
|
|
653
|
+
|
|
558
654
|
return (
|
|
559
655
|
<Box flexDirection="column" width={width} height={height}>
|
|
560
656
|
<Box flexDirection="column" height={headerHeight}>
|
|
@@ -575,7 +671,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
575
671
|
|
|
576
672
|
<PortfolioTickerTable
|
|
577
673
|
columns={columns}
|
|
578
|
-
focused={focused}
|
|
674
|
+
focused={focused && !quickAddFocused}
|
|
579
675
|
sortColumnId={activeSort.columnId}
|
|
580
676
|
sortDirection={activeSort.direction}
|
|
581
677
|
onHeaderClick={handleHeaderClick}
|
|
@@ -590,8 +686,11 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
590
686
|
visibleRangeBuffer={3}
|
|
591
687
|
resetScrollKey={activeCollectionId}
|
|
592
688
|
onRowActivate={handleRowActivate}
|
|
689
|
+
rootHeight={tableHeight}
|
|
593
690
|
/>
|
|
594
691
|
|
|
692
|
+
{quickAddRow}
|
|
693
|
+
|
|
595
694
|
{showCashDrawer && accountState && (
|
|
596
695
|
<Box height={drawerHeight} paddingX={1}>
|
|
597
696
|
<PortfolioCashMarginDrawer
|