gloomberb 0.7.2 → 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 +21 -3
- 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 +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/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/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
|
@@ -1,23 +1,38 @@
|
|
|
1
|
-
import { useCallback, useEffect, useMemo, useRef
|
|
2
|
-
import {
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { Box } from "../../../ui";
|
|
3
|
+
import { DataTableView, Tabs, usePaneFooter, type DataTableCell, type DataTableColumn, type DataTableKeyEvent } from "../../../components";
|
|
3
4
|
import type { GloomPlugin, PaneProps } from "../../../types/plugin";
|
|
5
|
+
import type { PricePoint } from "../../../types/financials";
|
|
4
6
|
import { colors, priceColor } from "../../../theme/colors";
|
|
5
7
|
import { formatCurrency, formatPercentRaw } from "../../../utils/format";
|
|
6
|
-
import { useAssetData, usePluginPaneState, usePluginTickerActions } from "../../plugin-runtime";
|
|
7
|
-
import {
|
|
8
|
+
import { useAssetData, useDebouncedPluginPaneState, usePluginPaneState, usePluginTickerActions } from "../../plugin-runtime";
|
|
9
|
+
import {
|
|
10
|
+
SECTOR_COLLECTIONS,
|
|
11
|
+
getSectorCollection,
|
|
12
|
+
type SectorCollectionId,
|
|
13
|
+
type SectorDef,
|
|
14
|
+
} from "./sector-data";
|
|
8
15
|
|
|
9
16
|
const REFRESH_INTERVAL_MS = 60_000;
|
|
17
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
18
|
+
const ONE_MONTH_DAYS = 30;
|
|
19
|
+
const ONE_YEAR_DAYS = 365;
|
|
20
|
+
const DEFAULT_COLLECTION_ID: SectorCollectionId = "sectors";
|
|
10
21
|
|
|
11
22
|
interface SectorRow extends SectorDef {
|
|
12
23
|
price: number | null;
|
|
13
24
|
changePercent: number | null;
|
|
25
|
+
return1M: number | null;
|
|
26
|
+
return1Y: number | null;
|
|
14
27
|
currency: string;
|
|
15
28
|
loading: boolean;
|
|
16
29
|
}
|
|
17
30
|
|
|
18
|
-
type SectorColumnId = "name" | "etf" | "price" | "changePercent" | "bar";
|
|
31
|
+
type SectorColumnId = "name" | "etf" | "price" | "changePercent" | "return1M" | "return1Y" | "bar";
|
|
19
32
|
type SectorColumn = DataTableColumn & { id: SectorColumnId };
|
|
20
33
|
type SortDirection = "asc" | "desc";
|
|
34
|
+
type SectorRowsByCollection = Record<SectorCollectionId, SectorRow[]>;
|
|
35
|
+
type SectorRefreshByCollection = Partial<Record<SectorCollectionId, number>>;
|
|
21
36
|
|
|
22
37
|
interface SectorSortPreference {
|
|
23
38
|
columnId: SectorColumnId;
|
|
@@ -40,22 +55,130 @@ function formatTime(date: Date): string {
|
|
|
40
55
|
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
41
56
|
}
|
|
42
57
|
|
|
58
|
+
function createLoadingRows(sectors: readonly SectorDef[]): SectorRow[] {
|
|
59
|
+
return sectors.map((sector) => ({
|
|
60
|
+
...sector,
|
|
61
|
+
price: null,
|
|
62
|
+
changePercent: null,
|
|
63
|
+
return1M: null,
|
|
64
|
+
return1Y: null,
|
|
65
|
+
currency: "USD",
|
|
66
|
+
loading: true,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function createRowsByCollection(): SectorRowsByCollection {
|
|
71
|
+
return {
|
|
72
|
+
sectors: createLoadingRows(getSectorCollection("sectors").items),
|
|
73
|
+
industries: createLoadingRows(getSectorCollection("industries").items),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const INITIAL_ROWS_BY_COLLECTION = createRowsByCollection();
|
|
78
|
+
const INITIAL_REFRESH_BY_COLLECTION: SectorRefreshByCollection = {};
|
|
79
|
+
|
|
80
|
+
function normalizeRowsForCollection(
|
|
81
|
+
rowsByCollection: SectorRowsByCollection,
|
|
82
|
+
collectionId: SectorCollectionId,
|
|
83
|
+
): SectorRow[] {
|
|
84
|
+
const collection = getSectorCollection(collectionId);
|
|
85
|
+
const rows = rowsByCollection[collectionId] ?? [];
|
|
86
|
+
return collection.items.map((sector) => {
|
|
87
|
+
const existing = rows.find((row) => row.etf === sector.etf);
|
|
88
|
+
return {
|
|
89
|
+
...sector,
|
|
90
|
+
price: existing?.price ?? null,
|
|
91
|
+
changePercent: existing?.changePercent ?? null,
|
|
92
|
+
return1M: existing?.return1M ?? null,
|
|
93
|
+
return1Y: existing?.return1Y ?? null,
|
|
94
|
+
currency: existing?.currency ?? "USD",
|
|
95
|
+
loading: existing?.loading ?? true,
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function updateRowsForCollection(
|
|
101
|
+
rowsByCollection: SectorRowsByCollection,
|
|
102
|
+
collectionId: SectorCollectionId,
|
|
103
|
+
updater: (rows: SectorRow[]) => SectorRow[],
|
|
104
|
+
): SectorRowsByCollection {
|
|
105
|
+
return {
|
|
106
|
+
...rowsByCollection,
|
|
107
|
+
[collectionId]: updater(normalizeRowsForCollection(rowsByCollection, collectionId)),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getPricePointTimestamp(point: PricePoint): number {
|
|
112
|
+
const value = point.date as Date | string | number | null | undefined;
|
|
113
|
+
if (value instanceof Date) return value.getTime();
|
|
114
|
+
if (value == null) return Number.NaN;
|
|
115
|
+
return new Date(value).getTime();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getSortedHistory(history: readonly PricePoint[]): Array<{ point: PricePoint; timestamp: number }> {
|
|
119
|
+
return history
|
|
120
|
+
.map((point) => ({ point, timestamp: getPricePointTimestamp(point) }))
|
|
121
|
+
.filter(({ point, timestamp }) => (
|
|
122
|
+
Number.isFinite(timestamp)
|
|
123
|
+
&& Number.isFinite(point.close)
|
|
124
|
+
&& point.close > 0
|
|
125
|
+
))
|
|
126
|
+
.sort((left, right) => left.timestamp - right.timestamp);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function latestHistoryClose(history: readonly PricePoint[]): number | null {
|
|
130
|
+
return getSortedHistory(history).at(-1)?.point.close ?? null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function computeTrailingReturn(
|
|
134
|
+
history: readonly PricePoint[],
|
|
135
|
+
days: number,
|
|
136
|
+
latestPrice?: number | null,
|
|
137
|
+
): number | null {
|
|
138
|
+
const points = getSortedHistory(history);
|
|
139
|
+
if (points.length < 2) return null;
|
|
140
|
+
|
|
141
|
+
const latest = points.at(-1)!;
|
|
142
|
+
const endPrice = latestPrice != null && Number.isFinite(latestPrice) && latestPrice > 0
|
|
143
|
+
? latestPrice
|
|
144
|
+
: latest.point.close;
|
|
145
|
+
const targetTimestamp = latest.timestamp - days * DAY_MS;
|
|
146
|
+
let baseline = points[0]!;
|
|
147
|
+
for (const point of points) {
|
|
148
|
+
if (point.timestamp > targetTimestamp) break;
|
|
149
|
+
baseline = point;
|
|
150
|
+
}
|
|
151
|
+
const baselinePrice = baseline.point.close;
|
|
152
|
+
if (!Number.isFinite(endPrice) || !Number.isFinite(baselinePrice) || baselinePrice <= 0) return null;
|
|
153
|
+
return (endPrice / baselinePrice - 1) * 100;
|
|
154
|
+
}
|
|
155
|
+
|
|
43
156
|
function buildColumns(width: number): SectorColumn[] {
|
|
44
|
-
const etfWidth =
|
|
45
|
-
const priceWidth =
|
|
46
|
-
const changeWidth =
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
157
|
+
const etfWidth = 4;
|
|
158
|
+
const priceWidth = 8;
|
|
159
|
+
const changeWidth = 8;
|
|
160
|
+
const returnWidth = 8;
|
|
161
|
+
const showBar = width >= 67;
|
|
162
|
+
const compactBar = width < 82;
|
|
163
|
+
const barWidth = showBar
|
|
164
|
+
? compactBar ? 6 : Math.max(8, Math.min(18, Math.floor(width * 0.16)))
|
|
165
|
+
: 0;
|
|
166
|
+
const columnCount = showBar ? 7 : 6;
|
|
167
|
+
const fixedWidth = etfWidth + priceWidth + changeWidth + returnWidth * 2 + barWidth;
|
|
168
|
+
const nameWidth = Math.max(12, Math.min(22, width - 2 - columnCount - fixedWidth));
|
|
169
|
+
|
|
170
|
+
const columns: SectorColumn[] = [
|
|
53
171
|
{ id: "name", label: "SECTOR", width: nameWidth, align: "left" },
|
|
54
172
|
{ id: "etf", label: "ETF", width: etfWidth, align: "left" },
|
|
55
173
|
{ id: "price", label: "LAST", width: priceWidth, align: "right" },
|
|
56
|
-
{ id: "changePercent", label: "
|
|
57
|
-
{ id: "
|
|
174
|
+
{ id: "changePercent", label: "1D", width: changeWidth, align: "right" },
|
|
175
|
+
{ id: "return1M", label: "1M", width: returnWidth, align: "right" },
|
|
176
|
+
{ id: "return1Y", label: "1Y", width: returnWidth, align: "right" },
|
|
58
177
|
];
|
|
178
|
+
if (showBar) {
|
|
179
|
+
columns.push({ id: "bar", label: "MOVE", width: barWidth, align: "left" });
|
|
180
|
+
}
|
|
181
|
+
return columns;
|
|
59
182
|
}
|
|
60
183
|
|
|
61
184
|
function getSortValue(columnId: SectorColumnId, row: SectorRow): string | number | null {
|
|
@@ -67,6 +190,11 @@ function getSortValue(columnId: SectorColumnId, row: SectorRow): string | number
|
|
|
67
190
|
case "price":
|
|
68
191
|
return row.price;
|
|
69
192
|
case "changePercent":
|
|
193
|
+
return row.changePercent;
|
|
194
|
+
case "return1M":
|
|
195
|
+
return row.return1M;
|
|
196
|
+
case "return1Y":
|
|
197
|
+
return row.return1Y;
|
|
70
198
|
case "bar":
|
|
71
199
|
return row.changePercent;
|
|
72
200
|
}
|
|
@@ -100,7 +228,12 @@ function nextSortPreference(current: SectorSortPreference, columnId: string): Se
|
|
|
100
228
|
if (current.columnId !== typedColumnId) {
|
|
101
229
|
return {
|
|
102
230
|
columnId: typedColumnId,
|
|
103
|
-
direction: typedColumnId === "changePercent"
|
|
231
|
+
direction: typedColumnId === "changePercent"
|
|
232
|
+
|| typedColumnId === "return1M"
|
|
233
|
+
|| typedColumnId === "return1Y"
|
|
234
|
+
|| typedColumnId === "bar"
|
|
235
|
+
? "desc"
|
|
236
|
+
: "asc",
|
|
104
237
|
};
|
|
105
238
|
}
|
|
106
239
|
if (current.direction === "desc") {
|
|
@@ -112,20 +245,39 @@ function nextSortPreference(current: SectorSortPreference, columnId: string): Se
|
|
|
112
245
|
function SectorPerformancePane({ focused, width, height }: PaneProps) {
|
|
113
246
|
const dataProvider = useAssetData();
|
|
114
247
|
const { navigateTicker } = usePluginTickerActions();
|
|
115
|
-
const [
|
|
116
|
-
|
|
248
|
+
const [activeCollectionId, setActiveCollectionId] = usePluginPaneState<SectorCollectionId>(
|
|
249
|
+
"activeCollectionId",
|
|
250
|
+
DEFAULT_COLLECTION_ID,
|
|
251
|
+
);
|
|
252
|
+
const activeCollection = getSectorCollection(activeCollectionId);
|
|
253
|
+
const [rowsByCollection, setRowsByCollection] = useDebouncedPluginPaneState<SectorRowsByCollection>(
|
|
254
|
+
"rowsByCollection:v1",
|
|
255
|
+
INITIAL_ROWS_BY_COLLECTION,
|
|
256
|
+
);
|
|
257
|
+
const [lastRefreshByCollection, setLastRefreshByCollection] = useDebouncedPluginPaneState<SectorRefreshByCollection>(
|
|
258
|
+
"lastRefreshByCollection:v1",
|
|
259
|
+
INITIAL_REFRESH_BY_COLLECTION,
|
|
117
260
|
);
|
|
118
261
|
const [selectedEtf, setSelectedEtf] = usePluginPaneState<string | null>("selectedEtf", null);
|
|
119
262
|
const [sortPreference, setSortPreference] = usePluginPaneState<SectorSortPreference>(
|
|
120
263
|
"sortPreference",
|
|
121
264
|
DEFAULT_SORT_PREFERENCE,
|
|
122
265
|
);
|
|
123
|
-
const [lastRefresh, setLastRefresh] = useState<Date | null>(null);
|
|
124
266
|
|
|
125
267
|
const fetchGenRef = useRef(0);
|
|
126
268
|
|
|
127
269
|
const columns = useMemo(() => buildColumns(width), [width]);
|
|
270
|
+
const rows = useMemo(
|
|
271
|
+
() => normalizeRowsForCollection(rowsByCollection, activeCollection.id),
|
|
272
|
+
[activeCollection.id, rowsByCollection],
|
|
273
|
+
);
|
|
128
274
|
const sortedRows = useMemo(() => sortRows(rows, sortPreference), [rows, sortPreference]);
|
|
275
|
+
const lastRefreshMs = lastRefreshByCollection[activeCollection.id];
|
|
276
|
+
const lastRefreshText = lastRefreshMs ? formatTime(new Date(lastRefreshMs)) : "loading";
|
|
277
|
+
const tabs = useMemo(() => SECTOR_COLLECTIONS.map((collection) => ({
|
|
278
|
+
label: collection.label,
|
|
279
|
+
value: collection.id,
|
|
280
|
+
})), []);
|
|
129
281
|
const selectedIdx = selectedEtf
|
|
130
282
|
? sortedRows.findIndex((row) => row.etf === selectedEtf)
|
|
131
283
|
: -1;
|
|
@@ -135,45 +287,66 @@ function SectorPerformancePane({ focused, width, height }: PaneProps) {
|
|
|
135
287
|
fetchGenRef.current += 1;
|
|
136
288
|
const gen = fetchGenRef.current;
|
|
137
289
|
if (!dataProvider) {
|
|
138
|
-
|
|
290
|
+
setRowsByCollection((prev) => updateRowsForCollection(prev, activeCollection.id, (rows) => (
|
|
291
|
+
rows.map((row) => ({ ...row, loading: false }))
|
|
292
|
+
)));
|
|
139
293
|
return;
|
|
140
294
|
}
|
|
141
295
|
|
|
142
|
-
|
|
296
|
+
const sectorDefs = activeCollection.items;
|
|
297
|
+
const collectionId = activeCollection.id;
|
|
143
298
|
|
|
144
|
-
|
|
299
|
+
setRowsByCollection((prev) => updateRowsForCollection(prev, collectionId, (rows) => (
|
|
300
|
+
rows.map((row) => ({ ...row, loading: true }))
|
|
301
|
+
)));
|
|
302
|
+
|
|
303
|
+
const fetches = sectorDefs.map(async (sector) => {
|
|
145
304
|
try {
|
|
146
|
-
const
|
|
305
|
+
const [quoteResult, historyResult] = await Promise.allSettled([
|
|
306
|
+
dataProvider.getQuote(sector.etf, ""),
|
|
307
|
+
dataProvider.getPriceHistory(sector.etf, "", "1Y"),
|
|
308
|
+
]);
|
|
147
309
|
if (fetchGenRef.current !== gen) return;
|
|
148
|
-
|
|
149
|
-
|
|
310
|
+
const quote = quoteResult.status === "fulfilled" ? quoteResult.value : null;
|
|
311
|
+
const history = historyResult.status === "fulfilled" ? historyResult.value : [];
|
|
312
|
+
const historyClose = latestHistoryClose(history);
|
|
313
|
+
const price = quote?.price ?? historyClose;
|
|
314
|
+
const return1M = computeTrailingReturn(history, ONE_MONTH_DAYS, price);
|
|
315
|
+
const return1Y = computeTrailingReturn(history, ONE_YEAR_DAYS, price);
|
|
316
|
+
setRowsByCollection((prev) => updateRowsForCollection(prev, collectionId, (rows) => (
|
|
317
|
+
rows.map((row) =>
|
|
150
318
|
row.etf === sector.etf
|
|
151
319
|
? {
|
|
152
320
|
...row,
|
|
153
|
-
price
|
|
321
|
+
price,
|
|
154
322
|
changePercent: quote?.changePercent ?? null,
|
|
323
|
+
return1M,
|
|
324
|
+
return1Y,
|
|
155
325
|
currency: quote?.currency ?? "USD",
|
|
156
326
|
loading: false,
|
|
157
327
|
}
|
|
158
328
|
: row,
|
|
159
|
-
)
|
|
160
|
-
);
|
|
329
|
+
)
|
|
330
|
+
)));
|
|
161
331
|
} catch {
|
|
162
332
|
if (fetchGenRef.current !== gen) return;
|
|
163
|
-
|
|
164
|
-
|
|
333
|
+
setRowsByCollection((prev) => updateRowsForCollection(prev, collectionId, (rows) => (
|
|
334
|
+
rows.map((row) =>
|
|
165
335
|
row.etf === sector.etf ? { ...row, loading: false } : row,
|
|
166
|
-
)
|
|
167
|
-
);
|
|
336
|
+
)
|
|
337
|
+
)));
|
|
168
338
|
}
|
|
169
339
|
});
|
|
170
340
|
|
|
171
341
|
Promise.allSettled(fetches).then(() => {
|
|
172
342
|
if (fetchGenRef.current === gen) {
|
|
173
|
-
|
|
343
|
+
setLastRefreshByCollection((prev) => ({
|
|
344
|
+
...prev,
|
|
345
|
+
[collectionId]: Date.now(),
|
|
346
|
+
}));
|
|
174
347
|
}
|
|
175
348
|
});
|
|
176
|
-
}, [dataProvider]);
|
|
349
|
+
}, [activeCollection, dataProvider, setLastRefreshByCollection, setRowsByCollection]);
|
|
177
350
|
|
|
178
351
|
useEffect(() => {
|
|
179
352
|
fetchAll();
|
|
@@ -181,6 +354,11 @@ function SectorPerformancePane({ focused, width, height }: PaneProps) {
|
|
|
181
354
|
return () => clearInterval(interval);
|
|
182
355
|
}, [fetchAll]);
|
|
183
356
|
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
if (activeCollectionId === activeCollection.id) return;
|
|
359
|
+
setActiveCollectionId(activeCollection.id);
|
|
360
|
+
}, [activeCollection.id, activeCollectionId, setActiveCollectionId]);
|
|
361
|
+
|
|
184
362
|
useEffect(() => {
|
|
185
363
|
if (selectedEtf && sortedRows.some((row) => row.etf === selectedEtf)) return;
|
|
186
364
|
const firstRow = sortedRows[0];
|
|
@@ -244,6 +422,16 @@ function SectorPerformancePane({ focused, width, height }: PaneProps) {
|
|
|
244
422
|
text: row.changePercent !== null ? formatPercentRaw(row.changePercent) : "—",
|
|
245
423
|
color: selectedColor ?? (row.changePercent !== null ? priceColor(row.changePercent) : colors.textDim),
|
|
246
424
|
};
|
|
425
|
+
case "return1M":
|
|
426
|
+
return {
|
|
427
|
+
text: row.loading && row.return1M === null ? "…" : row.return1M !== null ? formatPercentRaw(row.return1M) : "—",
|
|
428
|
+
color: selectedColor ?? (row.return1M !== null ? priceColor(row.return1M) : colors.textDim),
|
|
429
|
+
};
|
|
430
|
+
case "return1Y":
|
|
431
|
+
return {
|
|
432
|
+
text: row.loading && row.return1Y === null ? "…" : row.return1Y !== null ? formatPercentRaw(row.return1Y) : "—",
|
|
433
|
+
color: selectedColor ?? (row.return1Y !== null ? priceColor(row.return1Y) : colors.textDim),
|
|
434
|
+
};
|
|
247
435
|
case "bar": {
|
|
248
436
|
const bar = row.changePercent !== null ? buildBar(row.changePercent, column.width) : "";
|
|
249
437
|
const barColor = row.changePercent !== null && row.changePercent >= 0 ? colors.positive : colors.negative;
|
|
@@ -257,13 +445,34 @@ function SectorPerformancePane({ focused, width, height }: PaneProps) {
|
|
|
257
445
|
|
|
258
446
|
usePaneFooter("sectors", () => ({
|
|
259
447
|
info: [
|
|
448
|
+
{
|
|
449
|
+
id: "collection",
|
|
450
|
+
parts: [{ text: activeCollection.label, tone: "label" }],
|
|
451
|
+
},
|
|
260
452
|
{
|
|
261
453
|
id: "updated",
|
|
262
|
-
parts: [{ text:
|
|
454
|
+
parts: [{ text: lastRefreshText, tone: lastRefreshMs ? "value" : "muted" }],
|
|
263
455
|
},
|
|
264
456
|
],
|
|
265
457
|
hints: [{ id: "refresh", key: "r", label: "efresh", onPress: fetchAll }],
|
|
266
|
-
}), [fetchAll,
|
|
458
|
+
}), [activeCollection.label, fetchAll, lastRefreshMs, lastRefreshText]);
|
|
459
|
+
|
|
460
|
+
const rootBefore = (
|
|
461
|
+
<Box height={1} paddingX={1}>
|
|
462
|
+
<Tabs
|
|
463
|
+
tabs={tabs}
|
|
464
|
+
activeValue={activeCollection.id}
|
|
465
|
+
onSelect={(value) => {
|
|
466
|
+
const nextId = value as SectorCollectionId;
|
|
467
|
+
setActiveCollectionId(nextId);
|
|
468
|
+
setSelectedEtf(null);
|
|
469
|
+
}}
|
|
470
|
+
compact
|
|
471
|
+
variant="bare"
|
|
472
|
+
focused={focused}
|
|
473
|
+
/>
|
|
474
|
+
</Box>
|
|
475
|
+
);
|
|
267
476
|
|
|
268
477
|
return (
|
|
269
478
|
<DataTableView<SectorRow, SectorColumn>
|
|
@@ -272,8 +481,10 @@ function SectorPerformancePane({ focused, width, height }: PaneProps) {
|
|
|
272
481
|
onSelectIndex={selectIndex}
|
|
273
482
|
onActivateIndex={(_index, row) => openRow(row)}
|
|
274
483
|
onRootKeyDown={handleTableKeyDown}
|
|
484
|
+
rootBefore={rootBefore}
|
|
275
485
|
rootWidth={width}
|
|
276
486
|
rootHeight={height}
|
|
487
|
+
resetScrollKey={activeCollection.id}
|
|
277
488
|
columns={columns}
|
|
278
489
|
items={sortedRows}
|
|
279
490
|
sortColumnId={sortPreference.columnId}
|
|
@@ -293,7 +504,7 @@ export const sectorsPlugin: GloomPlugin = {
|
|
|
293
504
|
id: "sectors",
|
|
294
505
|
name: "Sector Performance",
|
|
295
506
|
version: "1.0.0",
|
|
296
|
-
description: "S&P 500 sector performance via
|
|
507
|
+
description: "S&P 500 sector and industry performance via ETF proxies",
|
|
297
508
|
toggleable: true,
|
|
298
509
|
|
|
299
510
|
panes: [
|
|
@@ -304,7 +515,7 @@ export const sectorsPlugin: GloomPlugin = {
|
|
|
304
515
|
component: SectorPerformancePane,
|
|
305
516
|
defaultPosition: "right",
|
|
306
517
|
defaultMode: "floating",
|
|
307
|
-
defaultFloatingSize: { width:
|
|
518
|
+
defaultFloatingSize: { width: 82, height: 18 },
|
|
308
519
|
},
|
|
309
520
|
],
|
|
310
521
|
|
|
@@ -313,8 +524,8 @@ export const sectorsPlugin: GloomPlugin = {
|
|
|
313
524
|
id: "sectors-pane",
|
|
314
525
|
paneId: "sectors",
|
|
315
526
|
label: "Sector Performance",
|
|
316
|
-
description: "S&P 500 sector performance sorted by daily change.",
|
|
317
|
-
keywords: ["sector", "sectors", "etf", "xlk", "xlv", "xlf", "performance", "spdr"],
|
|
527
|
+
description: "S&P 500 sector and industry performance sorted by daily change.",
|
|
528
|
+
keywords: ["sector", "sectors", "industry", "semis", "defense", "food", "leisure", "etf", "xlk", "xlv", "xlf", "performance", "spdr"],
|
|
318
529
|
shortcut: { prefix: "BI" },
|
|
319
530
|
},
|
|
320
531
|
],
|
|
@@ -4,7 +4,15 @@ export interface SectorDef {
|
|
|
4
4
|
etf: string;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
export
|
|
7
|
+
export type SectorCollectionId = "sectors" | "industries";
|
|
8
|
+
|
|
9
|
+
export interface SectorCollection {
|
|
10
|
+
id: SectorCollectionId;
|
|
11
|
+
label: string;
|
|
12
|
+
items: SectorDef[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const CORE_SECTORS: SectorDef[] = [
|
|
8
16
|
{ name: "Technology", etf: "XLK" },
|
|
9
17
|
{ name: "Healthcare", etf: "XLV" },
|
|
10
18
|
{ name: "Financials", etf: "XLF" },
|
|
@@ -17,3 +25,35 @@ export const SECTORS: SectorDef[] = [
|
|
|
17
25
|
{ name: "Real Estate", etf: "XLRE" },
|
|
18
26
|
{ name: "Materials", etf: "XLB" },
|
|
19
27
|
];
|
|
28
|
+
|
|
29
|
+
const INDUSTRIES: SectorDef[] = [
|
|
30
|
+
{ name: "Semiconductors", etf: "SMH" },
|
|
31
|
+
{ name: "Software", etf: "IGV" },
|
|
32
|
+
{ name: "Cybersecurity", etf: "CIBR" },
|
|
33
|
+
{ name: "Aero/Defense", etf: "ITA" },
|
|
34
|
+
{ name: "Banks", etf: "KBE" },
|
|
35
|
+
{ name: "Regional Banks", etf: "KRE" },
|
|
36
|
+
{ name: "Biotech", etf: "XBI" },
|
|
37
|
+
{ name: "Pharma", etf: "PPH" },
|
|
38
|
+
{ name: "Homebuilders", etf: "XHB" },
|
|
39
|
+
{ name: "Retail", etf: "XRT" },
|
|
40
|
+
{ name: "Food & Bev.", etf: "PBJ" },
|
|
41
|
+
{ name: "Leisure", etf: "PEJ" },
|
|
42
|
+
{ name: "Transportation", etf: "IYT" },
|
|
43
|
+
{ name: "Metals/Mining", etf: "XME" },
|
|
44
|
+
{ name: "Oil & Gas E&P", etf: "XOP" },
|
|
45
|
+
{ name: "Clean Energy", etf: "ICLN" },
|
|
46
|
+
{ name: "Infrastructure", etf: "PAVE" },
|
|
47
|
+
{ name: "Gold Miners", etf: "GDX" },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
export const SECTOR_COLLECTIONS: SectorCollection[] = [
|
|
51
|
+
{ id: "sectors", label: "Sectors", items: CORE_SECTORS },
|
|
52
|
+
{ id: "industries", label: "Industries", items: INDUSTRIES },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
export const SECTORS = CORE_SECTORS;
|
|
56
|
+
|
|
57
|
+
export function getSectorCollection(id: SectorCollectionId): SectorCollection {
|
|
58
|
+
return SECTOR_COLLECTIONS.find((collection) => collection.id === id) ?? SECTOR_COLLECTIONS[0]!;
|
|
59
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Box } from "../../../ui";
|
|
1
|
+
import { Box, useUiCapabilities } from "../../../ui";
|
|
2
2
|
import { useViewport } from "../../../react/input";
|
|
3
3
|
import { useCallback, useMemo } from "react";
|
|
4
4
|
import { ChartIndicatorSelector } from "../../../components/chart/chart-indicator-selector";
|
|
@@ -39,11 +39,12 @@ export function ChartTab({
|
|
|
39
39
|
financials: TickerFinancials | null;
|
|
40
40
|
}) {
|
|
41
41
|
const { width: termWidth, height: termHeight } = useViewport();
|
|
42
|
+
const { fractionalViewport = false } = useUiCapabilities();
|
|
42
43
|
const [rawIndicatorSelection] = usePluginConfigState<unknown>(CHART_INDICATORS_PLUGIN_CONFIG_KEY, null);
|
|
43
44
|
const [indicatorSelectionVersion] = usePluginConfigState<unknown>(CHART_INDICATORS_PLUGIN_CONFIG_VERSION_KEY, null);
|
|
44
45
|
const setPluginConfigStates = useSetPluginConfigStates();
|
|
45
46
|
|
|
46
|
-
const chartWidth = Math.max((width || Math.floor(termWidth * 0.55)) - 2, 30);
|
|
47
|
+
const chartWidth = Math.max((width || Math.floor(termWidth * 0.55)) - (fractionalViewport ? 1 : 2), 30);
|
|
47
48
|
const chartHeight = Math.max(height ?? termHeight - 8, 10);
|
|
48
49
|
const hasStoredIndicatorSelection = Array.isArray(rawIndicatorSelection);
|
|
49
50
|
const selectedIndicatorIds = useMemo(
|
|
@@ -4,9 +4,11 @@ import { TickerDetailPane } from "./pane";
|
|
|
4
4
|
import { QuoteMonitorPane } from "./quote-monitor";
|
|
5
5
|
import {
|
|
6
6
|
buildQuoteMonitorSettingsDef,
|
|
7
|
+
buildQuoteMonitorPaneTitle,
|
|
7
8
|
buildTickerDetailSettingsDef,
|
|
8
9
|
getTickerDetailPaneSettings,
|
|
9
10
|
} from "./settings";
|
|
11
|
+
import { formatTickerListInput } from "../../../utils/ticker-list";
|
|
10
12
|
|
|
11
13
|
export { FinancialsTab } from "./financials-tab";
|
|
12
14
|
export { QuoteMonitorPane } from "./quote-monitor";
|
|
@@ -34,7 +36,7 @@ export const tickerDetailPlugin: GloomPlugin = {
|
|
|
34
36
|
component: QuoteMonitorPane,
|
|
35
37
|
defaultPosition: "right",
|
|
36
38
|
defaultMode: "floating",
|
|
37
|
-
defaultFloatingSize: { width:
|
|
39
|
+
defaultFloatingSize: { width: 72, height: 10 },
|
|
38
40
|
settings: buildQuoteMonitorSettingsDef(),
|
|
39
41
|
},
|
|
40
42
|
],
|
|
@@ -59,17 +61,36 @@ export const tickerDetailPlugin: GloomPlugin = {
|
|
|
59
61
|
id: "quote-monitor-pane",
|
|
60
62
|
paneId: "quote-monitor",
|
|
61
63
|
label: "Quote Monitor",
|
|
62
|
-
description: "Open a compact quote monitor for
|
|
64
|
+
description: "Open a compact quote monitor for one or more tickers",
|
|
63
65
|
keywords: ["quote", "monitor", "price", "ticker", "pane"],
|
|
64
|
-
shortcut: { prefix: "QQ", argPlaceholder: "
|
|
65
|
-
|
|
66
|
+
shortcut: { prefix: "QQ", argPlaceholder: "tickers", argKind: "ticker-list" },
|
|
67
|
+
wizard: [
|
|
68
|
+
{
|
|
69
|
+
key: "tickers",
|
|
70
|
+
label: "Quote Tickers",
|
|
71
|
+
placeholder: "AAPL, MSFT, NVDA",
|
|
72
|
+
body: ["Enter one or more ticker symbols separated by commas."],
|
|
73
|
+
type: "text",
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
canCreate: (context, options) => (
|
|
77
|
+
(options?.symbols?.length ?? 0) > 0
|
|
78
|
+
|| normalizeTickerInput(context.activeTicker, options?.arg) !== null
|
|
79
|
+
),
|
|
66
80
|
createInstance: (context, options) => {
|
|
67
|
-
const
|
|
68
|
-
|
|
81
|
+
const symbols = options?.symbols?.length
|
|
82
|
+
? options.symbols
|
|
83
|
+
: [normalizeTickerInput(context.activeTicker, options?.arg)].filter((symbol): symbol is string => !!symbol);
|
|
84
|
+
const primarySymbol = symbols[0];
|
|
85
|
+
return primarySymbol
|
|
69
86
|
? {
|
|
70
|
-
title:
|
|
71
|
-
binding: { kind: "fixed", symbol:
|
|
72
|
-
settings: {
|
|
87
|
+
title: buildQuoteMonitorPaneTitle(symbols),
|
|
88
|
+
binding: { kind: "fixed", symbol: primarySymbol },
|
|
89
|
+
settings: {
|
|
90
|
+
symbol: primarySymbol,
|
|
91
|
+
symbols,
|
|
92
|
+
symbolsText: formatTickerListInput(symbols),
|
|
93
|
+
},
|
|
73
94
|
placement: "floating",
|
|
74
95
|
}
|
|
75
96
|
: null;
|