gloomberb 0.6.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
- package/src/app.tsx +158 -57
- package/src/capabilities/factories.ts +3 -0
- package/src/cli/registry.test.ts +0 -1
- package/src/components/chart/chart-controller.ts +0 -1
- package/src/components/chart/chart-data.ts +1 -31
- package/src/components/chart/chart-renderer.ts +94 -12
- package/src/components/chart/chart-resolution.test.ts +0 -36
- package/src/components/chart/chart-resolution.ts +0 -27
- package/src/components/chart/chart.test.ts +60 -45
- package/src/components/chart/comparison-chart-data.test.ts +1 -5
- package/src/components/chart/comparison-chart-data.ts +1 -4
- package/src/components/chart/comparison-chart-renderer.test.ts +26 -0
- package/src/components/chart/comparison-chart-renderer.ts +13 -6
- package/src/components/chart/comparison-stock-chart.tsx +63 -39
- package/src/components/chart/price-axis-labels.tsx +114 -0
- package/src/components/chart/static-chart-surface.tsx +30 -4
- package/src/components/chart/stock-chart.test.ts +0 -16
- package/src/components/chart/stock-chart.tsx +69 -41
- package/src/components/chart/time-axis-label.tsx +120 -0
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +0 -28
- package/src/components/command-bar/command-bar.test.tsx +182 -36
- package/src/components/command-bar/command-bar.tsx +943 -1016
- package/src/components/command-bar/command-registry.ts +0 -10
- package/src/components/command-bar/confirm-body.tsx +51 -0
- package/src/components/command-bar/helpers.ts +2 -5
- package/src/components/command-bar/list-model.ts +76 -0
- package/src/components/command-bar/theme-picker.tsx +254 -0
- package/src/components/command-bar/view-model.test.ts +4 -4
- package/src/components/command-bar/view-model.ts +1 -20
- package/src/components/command-bar/workflow-ops.ts +4 -0
- package/src/components/command-bar/workflow-types.ts +1 -2
- package/src/components/data-table-stack-view.test.tsx +39 -4
- package/src/components/data-table-stack-view.tsx +11 -104
- package/src/components/data-table-view.tsx +43 -79
- package/src/components/feed-data-table-stack-view.tsx +10 -17
- package/src/components/feed-stack-controller.ts +39 -0
- package/src/components/index.ts +5 -1
- package/src/components/layout/detached-pane-shell.tsx +29 -10
- package/src/components/layout/floating-pane.tsx +35 -4
- package/src/components/layout/header.tsx +12 -7
- package/src/components/layout/pane-content.test.tsx +64 -0
- package/src/components/layout/pane-content.tsx +2 -0
- package/src/components/layout/pane-cycle-order.test.ts +30 -0
- package/src/components/layout/pane-cycle-order.ts +32 -0
- package/src/components/layout/pane-footer.tsx +11 -4
- package/src/components/layout/pane-header.tsx +5 -5
- package/src/components/layout/pane-sizing.ts +9 -2
- package/src/components/layout/pane-title.ts +4 -1
- package/src/components/layout/pane.tsx +11 -5
- package/src/components/layout/shell.test.tsx +264 -22
- package/src/components/layout/shell.tsx +46 -8
- package/src/components/layout/status-bar.tsx +2 -0
- package/src/components/onboarding/onboarding-wizard.tsx +1 -2
- package/src/components/pane-settings-dialog.tsx +12 -4
- package/src/components/pane-template-wizard.test.tsx +3 -4
- package/src/components/speedometer-gauge.test.tsx +45 -0
- package/src/components/speedometer-gauge.tsx +562 -0
- package/src/components/table-view-shared.tsx +218 -0
- package/src/components/ticker-list-table-view.tsx +57 -73
- package/src/components/ticker-list-table.test.tsx +82 -1
- package/src/components/ticker-list-table.tsx +22 -9
- package/src/components/ui/confirm-dialog.tsx +66 -0
- package/src/components/ui/data-table.tsx +30 -19
- package/src/components/ui/frame.tsx +2 -0
- package/src/components/ui/index.ts +0 -1
- package/src/components/ui/page-stack-view.tsx +39 -10
- package/src/components/ui/table-layout.test.ts +42 -0
- package/src/components/ui/table-layout.ts +49 -4
- package/src/components/ui/tabs.tsx +34 -1
- package/src/components/ui/ui.test.tsx +168 -21
- package/src/core/state/app-state.ts +10 -17
- package/src/market-data/coordinator.test.ts +76 -1
- package/src/market-data/coordinator.ts +218 -49
- package/src/news/aggregator.test.ts +49 -1
- package/src/news/aggregator.ts +48 -3
- package/src/news/hooks.ts +6 -2
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +46 -16
- package/src/plugins/builtin/ai/providers.ts +0 -17
- package/src/plugins/builtin/ai/screener-pane.test.tsx +3 -4
- package/src/plugins/builtin/ai/screener-pane.tsx +3 -5
- package/src/plugins/builtin/analytics/index.tsx +1 -18
- package/src/plugins/builtin/ask-ai.test.tsx +89 -2
- package/src/plugins/builtin/broker-manager/index.test.tsx +2 -2
- package/src/plugins/builtin/broker-manager/index.tsx +19 -36
- package/src/plugins/builtin/chat-controller.test.ts +188 -1
- package/src/plugins/builtin/chat-controller.ts +537 -220
- package/src/plugins/builtin/chat.test.tsx +431 -13
- package/src/plugins/builtin/chat.tsx +1134 -227
- package/src/plugins/builtin/cloud-tweets.tsx +1 -1
- package/src/plugins/builtin/comparison-chart.test.tsx +53 -2
- package/src/plugins/builtin/comparison-chart.tsx +11 -5
- package/src/plugins/builtin/correlation/index.tsx +1 -6
- package/src/plugins/builtin/debug.tsx +1 -3
- package/src/plugins/builtin/econ/index.tsx +1 -2
- package/src/plugins/builtin/fear-greed/fear-greed-data.test.ts +72 -0
- package/src/plugins/builtin/fear-greed/fear-greed-data.ts +366 -0
- package/src/plugins/builtin/fear-greed/index.tsx +587 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +2 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/help.tsx +2 -2
- package/src/plugins/builtin/holders/index.test.ts +13 -1
- package/src/plugins/builtin/holders/index.tsx +64 -29
- package/src/plugins/builtin/insider/index.tsx +22 -89
- package/src/plugins/builtin/market-movers/index.tsx +13 -19
- package/src/plugins/builtin/market-movers/screener.test.ts +39 -1
- package/src/plugins/builtin/market-movers/screener.ts +70 -17
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +3 -2
- package/src/plugins/builtin/news-wire/index.tsx +41 -2
- package/src/plugins/builtin/news-wire/industry-pane.tsx +4 -18
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +46 -4
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +3 -2
- package/src/plugins/builtin/news-wire/news-table.tsx +11 -11
- package/src/plugins/builtin/news.tsx +4 -16
- package/src/plugins/builtin/notes.tsx +19 -55
- package/src/plugins/builtin/options.test.tsx +58 -2
- package/src/plugins/builtin/options.tsx +110 -65
- package/src/plugins/builtin/plugin-groups.ts +2 -0
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
- package/src/plugins/builtin/portfolio-list/pane.tsx +16 -39
- package/src/plugins/builtin/research/index.test.ts +94 -0
- package/src/plugins/builtin/research/index.tsx +164 -33
- package/src/plugins/builtin/sec-filing-display.tsx +84 -0
- package/src/plugins/builtin/sec.tsx +13 -66
- package/src/plugins/builtin/sectors/index.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/index.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +0 -1
- package/src/plugins/builtin/ticker-detail/pane.tsx +28 -22
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +3 -4
- package/src/plugins/builtin/ticker-detail.test.tsx +38 -0
- package/src/plugins/builtin/world-indices/index.tsx +1 -1
- package/src/plugins/builtin/yield-curve/index.tsx +1 -1
- package/src/plugins/ibkr/broker-adapter.ts +1 -3
- package/src/plugins/ibkr/gateway-service-native.ts +1 -5
- package/src/plugins/ibkr/gateway-service.ts +1 -1
- package/src/plugins/ibkr/index.test.tsx +0 -2
- package/src/plugins/ibkr/trade-tab.test.tsx +3 -4
- package/src/plugins/pane-manager.ts +0 -13
- package/src/plugins/plugin-runtime.test.tsx +40 -6
- package/src/plugins/plugin-runtime.tsx +20 -10
- package/src/plugins/prediction-markets/cli.ts +0 -1
- package/src/plugins/prediction-markets/controller-data.ts +0 -2
- package/src/plugins/prediction-markets/controller.ts +0 -15
- package/src/plugins/prediction-markets/detail/pane.tsx +1 -0
- package/src/plugins/prediction-markets/pane.test.tsx +3 -11
- package/src/plugins/prediction-markets/test-helpers.tsx +1 -1
- package/src/plugins/registry.ts +13 -42
- package/src/plugins/ticker-navigation.test.ts +66 -0
- package/src/plugins/ticker-navigation.ts +44 -0
- package/src/react/input.ts +2 -1
- package/src/renderers/electrobun/bun/index.ts +32 -11
- package/src/renderers/electrobun/bun/window-focus.ts +37 -0
- package/src/renderers/electrobun/shared/protocol.ts +7 -1
- package/src/renderers/electrobun/view/app-services.ts +88 -31
- package/src/renderers/electrobun/view/backend-rpc.ts +15 -0
- package/src/renderers/electrobun/view/data-table.tsx +138 -101
- package/src/renderers/electrobun/view/desktop-controls.tsx +42 -20
- package/src/renderers/electrobun/view/desktop-window-bridge.ts +12 -1
- package/src/renderers/electrobun/view/dialog-host.tsx +13 -1
- package/src/renderers/electrobun/view/input-host.tsx +74 -10
- package/src/renderers/electrobun/view/key-event.ts +1 -1
- package/src/renderers/electrobun/view/main.tsx +18 -1
- package/src/renderers/electrobun/view/styles.css +329 -0
- package/src/renderers/electrobun/view/toast-host.tsx +29 -3
- package/src/renderers/electrobun/view/ui-host.tsx +42 -0
- package/src/renderers/opentui/chart-surface.tsx +179 -0
- package/src/renderers/opentui/input-host.tsx +1 -1
- package/src/renderers/opentui/start.tsx +48 -30
- package/src/renderers/opentui/test-utils.tsx +17 -0
- package/src/renderers/opentui/ui-host.tsx +5 -2
- package/src/sources/gloomberb-cloud.test.ts +86 -1
- package/src/sources/gloomberb-cloud.ts +117 -21
- package/src/sources/provider-router.test.ts +122 -0
- package/src/sources/provider-router.ts +198 -15
- package/src/sources/yahoo-finance.test.ts +44 -0
- package/src/sources/yahoo-finance.ts +6 -0
- package/src/state/app-bootstrap.ts +20 -7
- package/src/state/app-context-selector.test.tsx +189 -4
- package/src/state/app-context.test.ts +65 -0
- package/src/state/app-context.tsx +48 -16
- package/src/state/use-inline-tickers.test.tsx +73 -0
- package/src/state/use-inline-tickers.ts +19 -10
- package/src/test-support/plugin-runtime.ts +3 -2
- package/src/theme/colors.test.ts +75 -0
- package/src/theme/colors.ts +76 -6
- package/src/theme/theme-context.tsx +26 -0
- package/src/theme/themes.test.ts +23 -1
- package/src/theme/themes.ts +115 -0
- package/src/types/broker.ts +1 -1
- package/src/types/capability-route-source.ts +1 -0
- package/src/types/data-provider.ts +15 -2
- package/src/types/desktop-window.ts +6 -0
- package/src/types/financials.ts +2 -0
- package/src/types/plugin.ts +1 -3
- package/src/ui/host.tsx +7 -0
- package/src/utils/api-client.test.ts +32 -0
- package/src/utils/api-client.ts +119 -18
- package/src/utils/back-navigation.ts +24 -1
- package/src/utils/double-escape-close.test.ts +33 -0
- package/src/utils/double-escape-close.ts +40 -0
- package/src/utils/throttled-fetch.test.ts +22 -0
- package/src/utils/throttled-fetch.ts +3 -1
- package/src/utils/ticker-search.ts +53 -9
- package/src/version.ts +1 -1
- package/src/plugins/builtin/news-wire/feed-pane.tsx +0 -21
- package/src/plugins/builtin/news-wire/top-pane.tsx +0 -21
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import { useShortcut } from "../../../react/input";
|
|
13
13
|
import { blendHex, colors, priceColor } from "../../../theme/colors";
|
|
14
14
|
import type { HolderData, HolderRecord } from "../../../types/financials";
|
|
15
|
-
import type {
|
|
15
|
+
import type { GloomPlugin } from "../../../types/plugin";
|
|
16
16
|
import { formatCompact, formatPercent, formatPercentRaw, padTo } from "../../../utils/format";
|
|
17
17
|
import { useAssetData, usePluginPaneState } from "../../plugin-runtime";
|
|
18
18
|
import { createTickerSurfacePaneTemplate } from "../ticker-surface";
|
|
@@ -21,6 +21,7 @@ type ViewMode = "table" | "chart";
|
|
|
21
21
|
type HolderColumnId = "holder" | "value" | "shares" | "changeShares" | "changePercent" | "percentHeld" | "reportDate";
|
|
22
22
|
type HolderColumn = DataTableColumn & { id: HolderColumnId };
|
|
23
23
|
type SortDirection = "asc" | "desc";
|
|
24
|
+
type PreventableMouseEvent = { preventDefault(): void };
|
|
24
25
|
|
|
25
26
|
interface SortPreference {
|
|
26
27
|
columnId: HolderColumnId;
|
|
@@ -89,6 +90,23 @@ function formatMaybePercent(value: number | undefined): string {
|
|
|
89
90
|
return Math.abs(value) <= 1 ? formatPercent(value) : formatPercentRaw(value);
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
export function resolveHolderOwnershipPercent(row: Pick<HolderRecord, "percentHeld" | "value">, marketCap: number | undefined): number | undefined {
|
|
94
|
+
if (row.percentHeld != null) return row.percentHeld;
|
|
95
|
+
if (row.value == null || row.value < 0 || marketCap == null || marketCap <= 0) return undefined;
|
|
96
|
+
return row.value / marketCap;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function formatHolderOwnershipPercent(value: number | undefined): string {
|
|
100
|
+
if (value == null) return "-";
|
|
101
|
+
const percent = Math.abs(value) <= 1 ? value * 100 : value;
|
|
102
|
+
return `${percent.toFixed(2)}%`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function formatHolderOwnershipLine(row: HolderRow, marketCap: number | undefined): string | null {
|
|
106
|
+
const ownership = resolveHolderOwnershipPercent(row, marketCap);
|
|
107
|
+
return ownership == null ? null : `${formatHolderOwnershipPercent(ownership)} held`;
|
|
108
|
+
}
|
|
109
|
+
|
|
92
110
|
function formatSignedCompact(value: number | undefined): string {
|
|
93
111
|
if (value == null) return "-";
|
|
94
112
|
const sign = value > 0 ? "+" : "";
|
|
@@ -132,7 +150,7 @@ function buildColumns(width: number): HolderColumn[] {
|
|
|
132
150
|
];
|
|
133
151
|
}
|
|
134
152
|
|
|
135
|
-
function sortValue(row: HolderRow, columnId: HolderColumnId): string | number | null {
|
|
153
|
+
function sortValue(row: HolderRow, columnId: HolderColumnId, marketCap?: number): string | number | null {
|
|
136
154
|
switch (columnId) {
|
|
137
155
|
case "holder":
|
|
138
156
|
return row.name;
|
|
@@ -145,7 +163,7 @@ function sortValue(row: HolderRow, columnId: HolderColumnId): string | number |
|
|
|
145
163
|
case "changePercent":
|
|
146
164
|
return row.changePercent ?? null;
|
|
147
165
|
case "percentHeld":
|
|
148
|
-
return row
|
|
166
|
+
return resolveHolderOwnershipPercent(row, marketCap) ?? null;
|
|
149
167
|
case "reportDate":
|
|
150
168
|
return row.reportDate ?? null;
|
|
151
169
|
}
|
|
@@ -166,10 +184,10 @@ function compareSortValues(
|
|
|
166
184
|
return direction === "asc" ? comparison : -comparison;
|
|
167
185
|
}
|
|
168
186
|
|
|
169
|
-
function sortRows(rows: HolderRow[], preference: SortPreference): HolderRow[] {
|
|
187
|
+
function sortRows(rows: HolderRow[], preference: SortPreference, marketCap?: number): HolderRow[] {
|
|
170
188
|
return [...rows].sort((left, right) => compareSortValues(
|
|
171
|
-
sortValue(left, preference.columnId),
|
|
172
|
-
sortValue(right, preference.columnId),
|
|
189
|
+
sortValue(left, preference.columnId, marketCap),
|
|
190
|
+
sortValue(right, preference.columnId, marketCap),
|
|
173
191
|
preference.direction,
|
|
174
192
|
));
|
|
175
193
|
}
|
|
@@ -427,10 +445,11 @@ function desktopTileColor(row: HolderRow): string {
|
|
|
427
445
|
return blendHex(colors.panel, row.changePercent > 0 ? colors.positive : colors.negative, intensity);
|
|
428
446
|
}
|
|
429
447
|
|
|
430
|
-
function Tile({ tile, selected, currency, onSelect }: {
|
|
448
|
+
function Tile({ tile, selected, currency, marketCap, onSelect }: {
|
|
431
449
|
tile: TileLayout;
|
|
432
450
|
selected: boolean;
|
|
433
451
|
currency: string;
|
|
452
|
+
marketCap?: number;
|
|
434
453
|
onSelect: () => void;
|
|
435
454
|
}) {
|
|
436
455
|
const renderWidth = Math.max(1, tile.width - (tile.width > 2 ? 1 : 0));
|
|
@@ -442,6 +461,7 @@ function Tile({ tile, selected, currency, onSelect }: {
|
|
|
442
461
|
const amount = tile.row.value != null
|
|
443
462
|
? formatMoneyCompact(tile.row.value, currency)
|
|
444
463
|
: formatCompact(tile.row.shares);
|
|
464
|
+
const ownership = formatHolderOwnershipLine(tile.row, marketCap);
|
|
445
465
|
const change = tile.row.changePercent != null ? formatMaybePercent(tile.row.changePercent) : "No change";
|
|
446
466
|
|
|
447
467
|
return (
|
|
@@ -452,7 +472,7 @@ function Tile({ tile, selected, currency, onSelect }: {
|
|
|
452
472
|
width={renderWidth}
|
|
453
473
|
height={renderHeight}
|
|
454
474
|
backgroundColor={selected ? colors.selected : tileColor(tile.row)}
|
|
455
|
-
onMouseDown={(event) => {
|
|
475
|
+
onMouseDown={(event: PreventableMouseEvent) => {
|
|
456
476
|
event.preventDefault();
|
|
457
477
|
onSelect();
|
|
458
478
|
}}
|
|
@@ -462,6 +482,9 @@ function Tile({ tile, selected, currency, onSelect }: {
|
|
|
462
482
|
<Text fg={textColor} attributes={attributes}>{padTo(amount, innerWidth)}</Text>
|
|
463
483
|
)}
|
|
464
484
|
{renderHeight >= 3 && (
|
|
485
|
+
<Text fg={textColor} attributes={attributes}>{padTo(ownership ?? change, innerWidth)}</Text>
|
|
486
|
+
)}
|
|
487
|
+
{ownership && renderHeight >= 4 && (
|
|
465
488
|
<Text fg={textColor} attributes={attributes}>{padTo(change, innerWidth)}</Text>
|
|
466
489
|
)}
|
|
467
490
|
</Box>
|
|
@@ -472,21 +495,24 @@ function pct(value: number, total: number): string {
|
|
|
472
495
|
return `${total > 0 ? value / total * 100 : 0}%`;
|
|
473
496
|
}
|
|
474
497
|
|
|
475
|
-
function DesktopTile({ tile, chartWidth, chartHeight, selected, hovered, currency, onSelect, onHover }: {
|
|
498
|
+
function DesktopTile({ tile, chartWidth, chartHeight, selected, hovered, currency, marketCap, onSelect, onHover }: {
|
|
476
499
|
tile: FloatTileLayout;
|
|
477
500
|
chartWidth: number;
|
|
478
501
|
chartHeight: number;
|
|
479
502
|
selected: boolean;
|
|
480
503
|
hovered: boolean;
|
|
481
504
|
currency: string;
|
|
505
|
+
marketCap?: number;
|
|
482
506
|
onSelect: () => void;
|
|
483
507
|
onHover: (hovered: boolean) => void;
|
|
484
508
|
}) {
|
|
485
509
|
const amount = tile.row.value != null
|
|
486
510
|
? formatMoneyCompact(tile.row.value, currency)
|
|
487
511
|
: formatCompact(tile.row.shares);
|
|
512
|
+
const ownership = formatHolderOwnershipLine(tile.row, marketCap);
|
|
488
513
|
const change = tile.row.changePercent != null ? formatMaybePercent(tile.row.changePercent) : "No change";
|
|
489
514
|
const canShowDetails = tile.width >= 7 && tile.height >= 3;
|
|
515
|
+
const canShowChange = tile.height >= 3.7;
|
|
490
516
|
const canShowLabel = tile.width >= 4 && tile.height >= 1.4;
|
|
491
517
|
const isTiny = tile.width < 5 || tile.height < 2;
|
|
492
518
|
const backgroundColor = desktopTileColor(tile.row);
|
|
@@ -538,7 +564,7 @@ function DesktopTile({ tile, chartWidth, chartHeight, selected, hovered, currenc
|
|
|
538
564
|
<Box
|
|
539
565
|
data-gloom-role="holders-desktop-tile"
|
|
540
566
|
style={style}
|
|
541
|
-
onMouseDown={(event) => {
|
|
567
|
+
onMouseDown={(event: PreventableMouseEvent) => {
|
|
542
568
|
event.preventDefault();
|
|
543
569
|
onSelect();
|
|
544
570
|
}}
|
|
@@ -561,20 +587,24 @@ function DesktopTile({ tile, chartWidth, chartHeight, selected, hovered, currenc
|
|
|
561
587
|
{canShowDetails && (
|
|
562
588
|
<>
|
|
563
589
|
<Text fg="#ffffff" style={{ ...textStyle, fontSize: 12, fontWeight: 600 }}>{amount}</Text>
|
|
564
|
-
<Text fg="#ffffff" style={{ ...textStyle, fontSize: 12, fontWeight: 600 }}>{change}</Text>
|
|
590
|
+
<Text fg="#ffffff" style={{ ...textStyle, fontSize: 12, fontWeight: 600 }}>{ownership ?? change}</Text>
|
|
591
|
+
{ownership && canShowChange && (
|
|
592
|
+
<Text fg="#ffffff" style={{ ...textStyle, fontSize: 12, fontWeight: 600 }}>{change}</Text>
|
|
593
|
+
)}
|
|
565
594
|
</>
|
|
566
595
|
)}
|
|
567
596
|
</Box>
|
|
568
597
|
);
|
|
569
598
|
}
|
|
570
599
|
|
|
571
|
-
function DesktopHoldersTreemap({ rows, width, height, selectedId, onSelect, currency, cellAspect }: {
|
|
600
|
+
function DesktopHoldersTreemap({ rows, width, height, selectedId, onSelect, currency, marketCap, cellAspect }: {
|
|
572
601
|
rows: HolderRow[];
|
|
573
602
|
width: number;
|
|
574
603
|
height: number;
|
|
575
604
|
selectedId: string | null;
|
|
576
605
|
onSelect: (row: HolderRow) => void;
|
|
577
606
|
currency: string;
|
|
607
|
+
marketCap?: number;
|
|
578
608
|
cellAspect: number;
|
|
579
609
|
}) {
|
|
580
610
|
const [hoveredId, setHoveredId] = useState<string | null>(null);
|
|
@@ -620,6 +650,7 @@ function DesktopHoldersTreemap({ rows, width, height, selectedId, onSelect, curr
|
|
|
620
650
|
selected={tile.row.id === selectedId}
|
|
621
651
|
hovered={tile.row.id === hoveredId}
|
|
622
652
|
currency={currency}
|
|
653
|
+
marketCap={marketCap}
|
|
623
654
|
onSelect={() => onSelect(tile.row)}
|
|
624
655
|
onHover={(isHovered) => setHoveredId((current) => (isHovered ? tile.row.id : current === tile.row.id ? null : current))}
|
|
625
656
|
/>
|
|
@@ -629,13 +660,14 @@ function DesktopHoldersTreemap({ rows, width, height, selectedId, onSelect, curr
|
|
|
629
660
|
);
|
|
630
661
|
}
|
|
631
662
|
|
|
632
|
-
function HoldersTreemap({ rows, width, height, selectedId, onSelect, currency }: {
|
|
663
|
+
function HoldersTreemap({ rows, width, height, selectedId, onSelect, currency, marketCap }: {
|
|
633
664
|
rows: HolderRow[];
|
|
634
665
|
width: number;
|
|
635
666
|
height: number;
|
|
636
667
|
selectedId: string | null;
|
|
637
668
|
onSelect: (row: HolderRow) => void;
|
|
638
669
|
currency: string;
|
|
670
|
+
marketCap?: number;
|
|
639
671
|
}) {
|
|
640
672
|
const { cellWidthPx = 8, cellHeightPx = 18, nativePaneChrome } = useUiCapabilities();
|
|
641
673
|
const chartWidth = Math.max(1, width - 2);
|
|
@@ -651,6 +683,7 @@ function HoldersTreemap({ rows, width, height, selectedId, onSelect, currency }:
|
|
|
651
683
|
selectedId={selectedId}
|
|
652
684
|
onSelect={onSelect}
|
|
653
685
|
currency={currency}
|
|
686
|
+
marketCap={marketCap}
|
|
654
687
|
cellAspect={cellAspect}
|
|
655
688
|
/>
|
|
656
689
|
);
|
|
@@ -673,6 +706,7 @@ function HoldersTreemap({ rows, width, height, selectedId, onSelect, currency }:
|
|
|
673
706
|
tile={tile}
|
|
674
707
|
selected={tile.row.id === selectedId}
|
|
675
708
|
currency={currency}
|
|
709
|
+
marketCap={marketCap}
|
|
676
710
|
onSelect={() => onSelect(tile.row)}
|
|
677
711
|
/>
|
|
678
712
|
))}
|
|
@@ -681,8 +715,9 @@ function HoldersTreemap({ rows, width, height, selectedId, onSelect, currency }:
|
|
|
681
715
|
);
|
|
682
716
|
}
|
|
683
717
|
|
|
684
|
-
|
|
685
|
-
const {
|
|
718
|
+
function HoldersView({ focused, width, height }: { focused: boolean; width: number; height: number }) {
|
|
719
|
+
const { nativePaneChrome } = useUiCapabilities();
|
|
720
|
+
const { symbol, ticker, financials } = usePaneTicker();
|
|
686
721
|
const dataProvider = useAssetData();
|
|
687
722
|
const [viewMode, setViewMode] = usePluginPaneState<ViewMode>("viewMode", "chart");
|
|
688
723
|
const [sortPreference, setSortPreference] = usePluginPaneState<SortPreference>("sortPreference", DEFAULT_SORT);
|
|
@@ -693,9 +728,11 @@ export function HoldersView({ focused, width, height }: Pick<PaneProps, "focused
|
|
|
693
728
|
const fetchGenRef = useRef(0);
|
|
694
729
|
|
|
695
730
|
const currency = data?.currency ?? ticker?.metadata.currency ?? "USD";
|
|
731
|
+
const quoteMarketCap = financials?.quote?.marketCap;
|
|
732
|
+
const marketCap = financials?.quote?.currency && financials.quote.currency !== currency ? undefined : quoteMarketCap;
|
|
696
733
|
const exchange = ticker?.metadata.exchange ?? "";
|
|
697
734
|
const rows = useMemo(() => buildRows(data), [data]);
|
|
698
|
-
const sortedRows = useMemo(() => sortRows(rows, sortPreference), [rows, sortPreference]);
|
|
735
|
+
const sortedRows = useMemo(() => sortRows(rows, sortPreference, marketCap), [marketCap, rows, sortPreference]);
|
|
699
736
|
const columns = useMemo(() => buildColumns(width), [width]);
|
|
700
737
|
const selectedIdx = selectedId
|
|
701
738
|
? sortedRows.findIndex((row) => row.id === selectedId)
|
|
@@ -831,11 +868,14 @@ export function HoldersView({ focused, width, height }: Pick<PaneProps, "focused
|
|
|
831
868
|
color: selectedColor ?? (row.changePercent != null ? priceColor(row.changePercent) : colors.textDim),
|
|
832
869
|
};
|
|
833
870
|
case "percentHeld":
|
|
834
|
-
return {
|
|
871
|
+
return {
|
|
872
|
+
text: formatHolderOwnershipPercent(resolveHolderOwnershipPercent(row, marketCap)),
|
|
873
|
+
color: selectedColor ?? colors.textDim,
|
|
874
|
+
};
|
|
835
875
|
case "reportDate":
|
|
836
876
|
return { text: displayDate(row.reportDate), color: selectedColor ?? colors.textDim };
|
|
837
877
|
}
|
|
838
|
-
}, [currency]);
|
|
878
|
+
}, [currency, marketCap]);
|
|
839
879
|
|
|
840
880
|
usePaneFooter("holders", () => ({
|
|
841
881
|
info: [
|
|
@@ -853,6 +893,7 @@ export function HoldersView({ focused, width, height }: Pick<PaneProps, "focused
|
|
|
853
893
|
: loading
|
|
854
894
|
? "Loading holders..."
|
|
855
895
|
: error ?? "No holders available";
|
|
896
|
+
const chartHeight = Math.max(1, height - 1 - (nativePaneChrome ? 1 : 0));
|
|
856
897
|
|
|
857
898
|
return (
|
|
858
899
|
<Box flexDirection="column" width={width} height={height}>
|
|
@@ -863,6 +904,7 @@ export function HoldersView({ focused, width, height }: Pick<PaneProps, "focused
|
|
|
863
904
|
onSelect={(value) => setViewMode(value as ViewMode)}
|
|
864
905
|
compact
|
|
865
906
|
variant="bare"
|
|
907
|
+
focused={focused}
|
|
866
908
|
/>
|
|
867
909
|
</Box>
|
|
868
910
|
|
|
@@ -889,24 +931,17 @@ export function HoldersView({ focused, width, height }: Pick<PaneProps, "focused
|
|
|
889
931
|
<HoldersTreemap
|
|
890
932
|
rows={sortedRows}
|
|
891
933
|
width={width}
|
|
892
|
-
height={
|
|
934
|
+
height={chartHeight}
|
|
893
935
|
selectedId={selectedId}
|
|
894
936
|
onSelect={(row) => setSelectedId(row.id)}
|
|
895
937
|
currency={currency}
|
|
938
|
+
marketCap={marketCap}
|
|
896
939
|
/>
|
|
897
940
|
)}
|
|
898
941
|
</Box>
|
|
899
942
|
);
|
|
900
943
|
}
|
|
901
944
|
|
|
902
|
-
export function HoldersPane({ focused, width, height }: PaneProps) {
|
|
903
|
-
return <HoldersView focused={focused} width={width} height={height} />;
|
|
904
|
-
}
|
|
905
|
-
|
|
906
|
-
export function HoldersDetailTab({ focused, width, height }: DetailTabProps) {
|
|
907
|
-
return <HoldersView focused={focused} width={width} height={height} />;
|
|
908
|
-
}
|
|
909
|
-
|
|
910
945
|
export const holdersPlugin: GloomPlugin = {
|
|
911
946
|
id: "holders",
|
|
912
947
|
name: "Holders",
|
|
@@ -919,7 +954,7 @@ export const holdersPlugin: GloomPlugin = {
|
|
|
919
954
|
id: "holders",
|
|
920
955
|
name: "Holders",
|
|
921
956
|
order: 42,
|
|
922
|
-
component:
|
|
957
|
+
component: HoldersView,
|
|
923
958
|
isVisible: ({ ticker }) => !!ticker,
|
|
924
959
|
});
|
|
925
960
|
},
|
|
@@ -929,7 +964,7 @@ export const holdersPlugin: GloomPlugin = {
|
|
|
929
964
|
id: "holders",
|
|
930
965
|
name: "Holders",
|
|
931
966
|
icon: "H",
|
|
932
|
-
component:
|
|
967
|
+
component: HoldersView,
|
|
933
968
|
defaultPosition: "right",
|
|
934
969
|
defaultMode: "floating",
|
|
935
970
|
defaultFloatingSize: { width: 105, height: 34 },
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Text } from "../../../ui";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
|
-
import type {
|
|
3
|
+
import type { GloomPlugin } from "../../../types/plugin";
|
|
4
4
|
import type { SecFilingItem } from "../../../types/data-provider";
|
|
5
5
|
import {
|
|
6
6
|
useResolvedEntryValue,
|
|
@@ -14,50 +14,19 @@ import { FeedDataTableStackView, Spinner, useExternalLinkFooter, type FeedDataTa
|
|
|
14
14
|
import { usePluginPaneState } from "../../plugin-runtime";
|
|
15
15
|
import { isUsEquityTicker } from "../../../utils/sec";
|
|
16
16
|
import { formatCompact, formatCurrency } from "../../../utils/format";
|
|
17
|
-
import { parseForm4Xml,
|
|
17
|
+
import { parseForm4Xml, type InsiderTransaction } from "./insider-data";
|
|
18
18
|
import { createTickerSurfacePaneTemplate } from "../ticker-surface";
|
|
19
|
+
import {
|
|
20
|
+
buildInsiderTransactionDetailBody,
|
|
21
|
+
buildInsiderTransactionTitle,
|
|
22
|
+
formatFilingFormLabel,
|
|
23
|
+
formatFilingShortDate,
|
|
24
|
+
renderFilingNotice,
|
|
25
|
+
} from "../sec-filing-display";
|
|
19
26
|
|
|
20
27
|
const FORM4_LIMIT = 20;
|
|
21
28
|
const NINETY_DAYS_MS = 90 * 24 * 60 * 60 * 1000;
|
|
22
29
|
|
|
23
|
-
const MONTH_NAMES = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
|
|
24
|
-
|
|
25
|
-
function formatDate(d: Date | string | number): string {
|
|
26
|
-
const date = d instanceof Date ? d : new Date(d);
|
|
27
|
-
if (isNaN(date.getTime())) return "—";
|
|
28
|
-
return `${MONTH_NAMES[date.getMonth()]} ${String(date.getDate()).padStart(2, " ")} ${date.getFullYear()}`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function wrapLines(text: string, width: number): string[] {
|
|
32
|
-
const words = text.trim().split(/\s+/);
|
|
33
|
-
const lines: string[] = [];
|
|
34
|
-
let current = "";
|
|
35
|
-
for (const word of words) {
|
|
36
|
-
if (!current) { current = word; continue; }
|
|
37
|
-
if (current.length + 1 + word.length <= width) {
|
|
38
|
-
current = `${current} ${word}`;
|
|
39
|
-
} else {
|
|
40
|
-
lines.push(current);
|
|
41
|
-
current = word;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
if (current) lines.push(current);
|
|
45
|
-
return lines;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function renderNotice(message: string, width: number) {
|
|
49
|
-
const lines = wrapLines(message, width - 4);
|
|
50
|
-
return (
|
|
51
|
-
<Box flexDirection="column" paddingX={1} paddingY={1}>
|
|
52
|
-
{lines.map((line, i) => (
|
|
53
|
-
<Box key={i} height={1}>
|
|
54
|
-
<Text fg={colors.textDim}>{line}</Text>
|
|
55
|
-
</Box>
|
|
56
|
-
))}
|
|
57
|
-
</Box>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
30
|
interface ParsedFiling {
|
|
62
31
|
filing: SecFilingItem;
|
|
63
32
|
transaction: InsiderTransaction | null;
|
|
@@ -99,46 +68,18 @@ function truncateText(text: string, width: number): string {
|
|
|
99
68
|
return `${text.slice(0, width - 3)}...`;
|
|
100
69
|
}
|
|
101
70
|
|
|
102
|
-
function formatFilingForm(form: string): string {
|
|
103
|
-
const value = form.trim();
|
|
104
|
-
return value ? `FORM ${value}` : "FORM 4";
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function buildTransactionTitle(transaction: InsiderTransaction): string {
|
|
108
|
-
const type = transactionTypeLabel(transaction.transactionType);
|
|
109
|
-
const price = transaction.pricePerShare != null
|
|
110
|
-
? ` @ ${formatCurrency(transaction.pricePerShare)}`
|
|
111
|
-
: "";
|
|
112
|
-
const value = transaction.totalValue != null
|
|
113
|
-
? ` | ${formatCurrency(transaction.totalValue)}`
|
|
114
|
-
: "";
|
|
115
|
-
return `${type} ${formatCompact(transaction.shares)} shares${price}${value}`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function buildTransactionDetailBody(transaction: InsiderTransaction): string {
|
|
119
|
-
const lines = [
|
|
120
|
-
`Transaction: ${transactionTypeLabel(transaction.transactionType)}`,
|
|
121
|
-
`Date: ${formatDate(transaction.filingDate)}`,
|
|
122
|
-
`Shares: ${formatCompact(transaction.shares)}`,
|
|
123
|
-
`Price/Share: ${transaction.pricePerShare != null ? formatCurrency(transaction.pricePerShare) : "—"}`,
|
|
124
|
-
`Total Value: ${transaction.totalValue != null ? formatCurrency(transaction.totalValue) : "—"}`,
|
|
125
|
-
`Shares Owned After: ${transaction.sharesOwned != null ? formatCompact(transaction.sharesOwned) : "—"}`,
|
|
126
|
-
];
|
|
127
|
-
return lines.join("\n");
|
|
128
|
-
}
|
|
129
|
-
|
|
130
71
|
function toFeedItems(parsed: ParsedFiling[]): FeedDataTableItem[] {
|
|
131
72
|
return parsed.map(({ filing, transaction, isLoading }) => {
|
|
132
73
|
const filingMeta = [
|
|
133
|
-
`Filed ${
|
|
74
|
+
`Filed ${formatFilingShortDate(filing.filingDate)}`,
|
|
134
75
|
`Accession ${filing.accessionNumber}`,
|
|
135
|
-
|
|
76
|
+
formatFilingFormLabel(filing.form),
|
|
136
77
|
];
|
|
137
78
|
|
|
138
79
|
if (!transaction) {
|
|
139
80
|
return {
|
|
140
81
|
id: filing.accessionNumber,
|
|
141
|
-
eyebrow:
|
|
82
|
+
eyebrow: formatFilingFormLabel(filing.form),
|
|
142
83
|
title: isLoading ? "Loading Form 4 filing..." : "Form 4 transaction unavailable",
|
|
143
84
|
timestamp: filing.filingDate,
|
|
144
85
|
detailTitle: isLoading ? "Loading Form 4 filing..." : "Form 4 transaction unavailable",
|
|
@@ -152,7 +93,7 @@ function toFeedItems(parsed: ParsedFiling[]): FeedDataTableItem[] {
|
|
|
152
93
|
return {
|
|
153
94
|
id: filing.accessionNumber,
|
|
154
95
|
eyebrow: transaction.reportedName,
|
|
155
|
-
title:
|
|
96
|
+
title: buildInsiderTransactionTitle(transaction),
|
|
156
97
|
timestamp: transaction.filingDate,
|
|
157
98
|
preview: transaction.title || undefined,
|
|
158
99
|
detailTitle: transaction.reportedName,
|
|
@@ -160,12 +101,12 @@ function toFeedItems(parsed: ParsedFiling[]): FeedDataTableItem[] {
|
|
|
160
101
|
...(transaction.title ? [transaction.title] : []),
|
|
161
102
|
...filingMeta,
|
|
162
103
|
],
|
|
163
|
-
detailBody:
|
|
104
|
+
detailBody: buildInsiderTransactionDetailBody(transaction),
|
|
164
105
|
};
|
|
165
106
|
});
|
|
166
107
|
}
|
|
167
108
|
|
|
168
|
-
|
|
109
|
+
function InsiderView({ width, height, focused }: { width: number; height: number; focused: boolean }) {
|
|
169
110
|
const { ticker } = usePaneTicker();
|
|
170
111
|
const tickerKey = ticker?.metadata.ticker ?? "none";
|
|
171
112
|
const [selectedIdx, setSelectedIdx] = usePluginPaneState<number>(`insider:selectedIdx:${tickerKey}`, 0);
|
|
@@ -290,18 +231,18 @@ export function InsiderView({ width, height, focused }: Pick<DetailTabProps, "wi
|
|
|
290
231
|
registrationId: "insider",
|
|
291
232
|
focused,
|
|
292
233
|
url: error ? null : openFiling?.filingUrl,
|
|
293
|
-
source: openFiling?.form ?
|
|
234
|
+
source: openFiling?.form ? formatFilingFormLabel(openFiling.form) : null,
|
|
294
235
|
info: footerInfo,
|
|
295
236
|
hints: footerHints,
|
|
296
237
|
label: "filing",
|
|
297
238
|
});
|
|
298
239
|
|
|
299
240
|
if (!ticker) return <Text fg={colors.textDim}>Select a ticker to view insider activity.</Text>;
|
|
300
|
-
if (!eligibleTicker) return
|
|
241
|
+
if (!eligibleTicker) return renderFilingNotice("Insider transactions are only shown for US equities.", width);
|
|
301
242
|
if (loading && allFilings.length === 0) return <Spinner label="Loading insider filings..." />;
|
|
302
|
-
if (error) return
|
|
243
|
+
if (error) return renderFilingNotice(`Error: ${error}`, width);
|
|
303
244
|
if (!loading && form4Filings.length === 0) {
|
|
304
|
-
return
|
|
245
|
+
return renderFilingNotice(`No Form 4 filings found for ${ticker.metadata.ticker}.`, width);
|
|
305
246
|
}
|
|
306
247
|
|
|
307
248
|
return (
|
|
@@ -321,14 +262,6 @@ export function InsiderView({ width, height, focused }: Pick<DetailTabProps, "wi
|
|
|
321
262
|
);
|
|
322
263
|
}
|
|
323
264
|
|
|
324
|
-
function InsiderTab(props: DetailTabProps) {
|
|
325
|
-
return <InsiderView {...props} />;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function InsiderPane({ focused, width, height }: PaneProps) {
|
|
329
|
-
return <InsiderView focused={focused} width={width} height={height} />;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
265
|
export const insiderPlugin: GloomPlugin = {
|
|
333
266
|
id: "insider",
|
|
334
267
|
name: "Insider Trading",
|
|
@@ -340,7 +273,7 @@ export const insiderPlugin: GloomPlugin = {
|
|
|
340
273
|
id: "insider",
|
|
341
274
|
name: "Insider",
|
|
342
275
|
icon: "I",
|
|
343
|
-
component:
|
|
276
|
+
component: InsiderView,
|
|
344
277
|
defaultPosition: "right",
|
|
345
278
|
defaultMode: "floating",
|
|
346
279
|
defaultFloatingSize: { width: 100, height: 30 },
|
|
@@ -364,7 +297,7 @@ export const insiderPlugin: GloomPlugin = {
|
|
|
364
297
|
id: "insider",
|
|
365
298
|
name: "Insider",
|
|
366
299
|
order: 47,
|
|
367
|
-
component:
|
|
300
|
+
component: InsiderView,
|
|
368
301
|
isVisible: ({ ticker }) => isUsEquityTicker(ticker),
|
|
369
302
|
});
|
|
370
303
|
},
|
|
@@ -191,12 +191,13 @@ const INDEX_SHORT: Record<string, string> = {
|
|
|
191
191
|
"^RUT": "RUT",
|
|
192
192
|
};
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
function MarketMoversPane({ focused, width, height }: PaneProps) {
|
|
195
195
|
const dataProvider = useAssetData();
|
|
196
196
|
const { pinTicker } = usePluginTickerActions();
|
|
197
197
|
const [activeTab, setActiveTab] = useState<TabId>("gainers");
|
|
198
198
|
const [quotes, setQuotes] = useState<ScreenerQuote[]>([]);
|
|
199
199
|
const [loading, setLoading] = useState(false);
|
|
200
|
+
const [loadError, setLoadError] = useState<string | null>(null);
|
|
200
201
|
const [selectedSymbol, setSelectedSymbol] = useState<string | null>(null);
|
|
201
202
|
const [sortPreference, setSortPreference] = useState<MarketMoverSortPreference>(DEFAULT_SORT_PREFERENCE);
|
|
202
203
|
const [summaryQuotes, setSummaryQuotes] = useState<MarketSummaryQuote[]>([]);
|
|
@@ -260,12 +261,14 @@ export function MarketMoversPane({ focused, width, height }: PaneProps) {
|
|
|
260
261
|
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
|
|
261
262
|
setQuotes(cached.data);
|
|
262
263
|
setSelectedSymbol(null);
|
|
264
|
+
setLoadError(null);
|
|
263
265
|
return;
|
|
264
266
|
}
|
|
265
267
|
|
|
266
268
|
fetchGenRef.current += 1;
|
|
267
269
|
const gen = fetchGenRef.current;
|
|
268
270
|
setLoading(true);
|
|
271
|
+
setLoadError(null);
|
|
269
272
|
|
|
270
273
|
try {
|
|
271
274
|
let data: ScreenerQuote[];
|
|
@@ -318,7 +321,12 @@ export function MarketMoversPane({ focused, width, height }: PaneProps) {
|
|
|
318
321
|
cacheRef.current.set(tab, { data, fetchedAt: Date.now() });
|
|
319
322
|
setQuotes(data);
|
|
320
323
|
setSelectedSymbol(null);
|
|
321
|
-
|
|
324
|
+
setLoadError(null);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (fetchGenRef.current === gen) {
|
|
327
|
+
setLoadError(error instanceof Error ? error.message : "Market movers unavailable");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
322
330
|
finally {
|
|
323
331
|
if (fetchGenRef.current === gen) setLoading(false);
|
|
324
332
|
}
|
|
@@ -337,22 +345,6 @@ export function MarketMoversPane({ focused, width, height }: PaneProps) {
|
|
|
337
345
|
const handleTableKeyDown = useCallback((event: DataTableKeyEvent) => {
|
|
338
346
|
const key = event.name;
|
|
339
347
|
|
|
340
|
-
if (key === "tab" || key === "right" || key === "l") {
|
|
341
|
-
event.preventDefault?.();
|
|
342
|
-
event.stopPropagation?.();
|
|
343
|
-
const currentTabIdx = TABS.findIndex((t) => t.id === activeTab);
|
|
344
|
-
setActiveTab(TABS[(currentTabIdx + 1) % TABS.length]!.id);
|
|
345
|
-
setSelectedSymbol(null);
|
|
346
|
-
return true;
|
|
347
|
-
}
|
|
348
|
-
if (key === "left" || key === "h") {
|
|
349
|
-
event.preventDefault?.();
|
|
350
|
-
event.stopPropagation?.();
|
|
351
|
-
const currentTabIdx = TABS.findIndex((t) => t.id === activeTab);
|
|
352
|
-
setActiveTab(TABS[(currentTabIdx - 1 + TABS.length) % TABS.length]!.id);
|
|
353
|
-
setSelectedSymbol(null);
|
|
354
|
-
return true;
|
|
355
|
-
}
|
|
356
348
|
if (key === "r") {
|
|
357
349
|
event.preventDefault?.();
|
|
358
350
|
event.stopPropagation?.();
|
|
@@ -443,6 +435,7 @@ export function MarketMoversPane({ focused, width, height }: PaneProps) {
|
|
|
443
435
|
}}
|
|
444
436
|
compact
|
|
445
437
|
variant="bare"
|
|
438
|
+
focused={focused}
|
|
446
439
|
/>
|
|
447
440
|
</Box>
|
|
448
441
|
|
|
@@ -461,7 +454,8 @@ export function MarketMoversPane({ focused, width, height }: PaneProps) {
|
|
|
461
454
|
onSelect={(row) => setSelectedSymbol(row.symbol)}
|
|
462
455
|
onActivate={(row) => openSymbol(row.symbol)}
|
|
463
456
|
renderCell={renderCell}
|
|
464
|
-
emptyStateTitle={loading ? "Loading movers..." : "No data"}
|
|
457
|
+
emptyStateTitle={loading ? "Loading movers..." : loadError ?? "No data"}
|
|
458
|
+
emptyStateHint={loadError ? "Yahoo Finance did not return market movers." : undefined}
|
|
465
459
|
/>
|
|
466
460
|
</Box>
|
|
467
461
|
);
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createYahooScreenerApi,
|
|
4
|
+
fetchScreener,
|
|
5
|
+
parseScreenerResponse,
|
|
6
|
+
parseTrendingResponse,
|
|
7
|
+
} from "./screener";
|
|
3
8
|
|
|
4
9
|
const SAMPLE_SCREENER_RESPONSE = {
|
|
5
10
|
finance: {
|
|
@@ -123,3 +128,36 @@ describe("parseTrendingResponse", () => {
|
|
|
123
128
|
expect(results[0]!.symbol).toBe("SPY");
|
|
124
129
|
});
|
|
125
130
|
});
|
|
131
|
+
|
|
132
|
+
describe("fetchScreener", () => {
|
|
133
|
+
test("falls back to the secondary Yahoo host when the primary host fails", async () => {
|
|
134
|
+
const requestedHosts: string[] = [];
|
|
135
|
+
const userAgents: string[] = [];
|
|
136
|
+
const api = createYahooScreenerApi(async (url, init) => {
|
|
137
|
+
const parsed = new URL(url);
|
|
138
|
+
requestedHosts.push(parsed.host);
|
|
139
|
+
userAgents.push(String((init?.headers as Record<string, string> | undefined)?.["User-Agent"] ?? ""));
|
|
140
|
+
|
|
141
|
+
if (parsed.host === "query2.finance.yahoo.com") {
|
|
142
|
+
return new Response("Forbidden", { status: 403 });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
expect(parsed.pathname).toBe("/v1/finance/screener/predefined/saved");
|
|
146
|
+
expect(parsed.searchParams.get("formatted")).toBe("false");
|
|
147
|
+
expect(parsed.searchParams.get("lang")).toBe("en-US");
|
|
148
|
+
expect(parsed.searchParams.get("region")).toBe("US");
|
|
149
|
+
expect(parsed.searchParams.get("scrIds")).toBe("day_gainers");
|
|
150
|
+
expect(parsed.searchParams.get("count")).toBe("2");
|
|
151
|
+
return Response.json(SAMPLE_SCREENER_RESPONSE);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const results = await fetchScreener("day_gainers", 2, api);
|
|
155
|
+
|
|
156
|
+
expect(requestedHosts).toEqual([
|
|
157
|
+
"query2.finance.yahoo.com",
|
|
158
|
+
"query1.finance.yahoo.com",
|
|
159
|
+
]);
|
|
160
|
+
expect(userAgents.every((agent) => agent.includes("Mozilla/5.0"))).toBe(true);
|
|
161
|
+
expect(results.map((result) => result.symbol)).toEqual(["AAPL", "MSFT"]);
|
|
162
|
+
});
|
|
163
|
+
});
|