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
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Box, Text } from "../../ui";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { TextAttributes } from "../../ui";
|
|
4
|
-
import type {
|
|
4
|
+
import type { GloomPlugin } from "../../types/plugin";
|
|
5
5
|
import type { OptionContract, OptionsChain } from "../../types/financials";
|
|
6
6
|
import { usePaneTicker } from "../../state/app-context";
|
|
7
|
-
import { blendHex, colors } from "../../theme/colors";
|
|
7
|
+
import { blendHex, colors, hoverBg } from "../../theme/colors";
|
|
8
|
+
import { blendForContrast, contrastRatio, higherContrast } from "../../theme/color-utils";
|
|
8
9
|
import { formatCompact, formatNumber } from "../../utils/format";
|
|
9
10
|
import { formatMarketPrice } from "../../utils/market-format";
|
|
10
11
|
import { formatExpDate, resolveOptionsTarget } from "../../utils/options";
|
|
@@ -12,7 +13,7 @@ import { useOptionsQuery, useResolvedEntryValue } from "../../market-data/hooks"
|
|
|
12
13
|
import { DataTableView, Spinner, Tabs, usePaneFooter, type DataTableCell, type DataTableColumn, type DataTableKeyEvent } from "../../components";
|
|
13
14
|
import { createTickerSurfacePaneTemplate } from "./ticker-surface";
|
|
14
15
|
|
|
15
|
-
type OptionColumnId =
|
|
16
|
+
export type OptionColumnId =
|
|
16
17
|
| "callLast"
|
|
17
18
|
| "callBid"
|
|
18
19
|
| "callAsk"
|
|
@@ -36,31 +37,40 @@ interface OptionTableRow {
|
|
|
36
37
|
isPositionStrike: boolean;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
type OptionsViewProps =
|
|
40
|
-
|
|
40
|
+
type OptionsViewProps = {
|
|
41
|
+
width: number;
|
|
42
|
+
height: number;
|
|
43
|
+
focused: boolean;
|
|
44
|
+
onCapture?: (capturing: boolean) => void;
|
|
41
45
|
};
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
{ id: "
|
|
58
|
-
{ id: "
|
|
59
|
-
{ id: "
|
|
60
|
-
{ id: "
|
|
61
|
-
{ id: "
|
|
62
|
-
{ id: "
|
|
63
|
-
{ id: "
|
|
47
|
+
type OptionColorRole = "call" | "put" | "price" | "activity" | "iv" | "strike";
|
|
48
|
+
|
|
49
|
+
const OPTION_TEXT_MIN_CONTRAST = 4.5;
|
|
50
|
+
|
|
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
|
+
const OPTION_COLUMNS: Array<Omit<OptionColumn, "headerColor">> = [
|
|
61
|
+
{ id: "callOpenInterest", label: "C OI", width: 6, align: "right" },
|
|
62
|
+
{ id: "callVolume", label: "C VOL", width: 6, align: "right" },
|
|
63
|
+
{ id: "callLast", label: "C LAST", width: 7, align: "right" },
|
|
64
|
+
{ id: "callIv", label: "C IV", width: 6, align: "right" },
|
|
65
|
+
{ id: "callBid", label: "C BID", width: 7, align: "right" },
|
|
66
|
+
{ id: "callAsk", label: "C ASK", width: 7, align: "right" },
|
|
67
|
+
{ id: "strike", label: "STRIKE", width: 9, align: "right" },
|
|
68
|
+
{ id: "putBid", label: "P BID", width: 7, align: "right" },
|
|
69
|
+
{ id: "putAsk", label: "P ASK", width: 7, align: "right" },
|
|
70
|
+
{ id: "putIv", label: "P IV", width: 6, align: "right" },
|
|
71
|
+
{ id: "putLast", label: "P LAST", width: 7, align: "right" },
|
|
72
|
+
{ id: "putVolume", label: "P VOL", width: 6, align: "right" },
|
|
73
|
+
{ id: "putOpenInterest", label: "P OI", width: 6, align: "right" },
|
|
64
74
|
];
|
|
65
75
|
|
|
66
76
|
export function OptionsView({ width, height, focused, onCapture = () => {} }: OptionsViewProps) {
|
|
@@ -161,6 +171,10 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
|
|
|
161
171
|
const selectedStrike = strikes[strikeIdx];
|
|
162
172
|
const selectedCall = selectedStrike != null ? callsByStrike.get(selectedStrike) : undefined;
|
|
163
173
|
const selectedPut = selectedStrike != null ? putsByStrike.get(selectedStrike) : undefined;
|
|
174
|
+
const optionColumns = OPTION_COLUMNS.map((column) => ({
|
|
175
|
+
...column,
|
|
176
|
+
headerColor: optionColumnColor(column.id, colors.panel),
|
|
177
|
+
}));
|
|
164
178
|
|
|
165
179
|
usePaneFooter("options", () => {
|
|
166
180
|
const info = [
|
|
@@ -232,23 +246,8 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
|
|
|
232
246
|
return true;
|
|
233
247
|
}
|
|
234
248
|
|
|
235
|
-
if (!interactive) return false;
|
|
236
|
-
|
|
237
|
-
const numExp = chain?.expirationDates.length ?? 0;
|
|
238
|
-
if (event.name === "h" || event.name === "left") {
|
|
239
|
-
event.preventDefault?.();
|
|
240
|
-
event.stopPropagation?.();
|
|
241
|
-
setExpIdx((i) => Math.max(i - 1, 0));
|
|
242
|
-
return true;
|
|
243
|
-
}
|
|
244
|
-
if (event.name === "l" || event.name === "right") {
|
|
245
|
-
event.preventDefault?.();
|
|
246
|
-
event.stopPropagation?.();
|
|
247
|
-
setExpIdx((i) => Math.min(i + 1, Math.max(numExp - 1, 0)));
|
|
248
|
-
return true;
|
|
249
|
-
}
|
|
250
249
|
return false;
|
|
251
|
-
}, [
|
|
250
|
+
}, [enterInteractive, exitInteractive, interactive, strikes.length]);
|
|
252
251
|
|
|
253
252
|
if (!ticker) return <Text fg={colors.textDim}>Select a ticker to view options.</Text>;
|
|
254
253
|
if (loading && !chain) return <Spinner label="Loading options chain..." />;
|
|
@@ -285,6 +284,7 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
|
|
|
285
284
|
}}
|
|
286
285
|
compact
|
|
287
286
|
variant="bare"
|
|
287
|
+
focused={focused && interactive}
|
|
288
288
|
/>
|
|
289
289
|
</Box>
|
|
290
290
|
{loading && <Spinner />}
|
|
@@ -303,7 +303,7 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
|
|
|
303
303
|
focused={focused}
|
|
304
304
|
selectedIndex={strikeIdx}
|
|
305
305
|
onRootKeyDown={handleTableKeyDown}
|
|
306
|
-
columns={
|
|
306
|
+
columns={optionColumns}
|
|
307
307
|
items={rows}
|
|
308
308
|
sortColumnId={null}
|
|
309
309
|
sortDirection="asc"
|
|
@@ -327,14 +327,6 @@ export function OptionsView({ width, height, focused, onCapture = () => {} }: Op
|
|
|
327
327
|
);
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
export function OptionsTab(props: DetailTabProps) {
|
|
331
|
-
return <OptionsView {...props} />;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
export function OptionsPane({ focused, width, height }: PaneProps) {
|
|
335
|
-
return <OptionsView focused={focused} width={width} height={height} />;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
330
|
function buildStrikeList(chain: OptionsChain): number[] {
|
|
339
331
|
const set = new Set<number>();
|
|
340
332
|
for (const c of chain.calls) set.add(c.strike);
|
|
@@ -378,12 +370,60 @@ function optionContractForColumn(row: OptionTableRow, columnId: OptionColumnId):
|
|
|
378
370
|
return columnId.startsWith("call") ? row.call : row.put;
|
|
379
371
|
}
|
|
380
372
|
|
|
381
|
-
function optionColumnColor(columnId: OptionColumnId): string {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
373
|
+
export function optionColumnColor(columnId: OptionColumnId, surface = colors.bg): string {
|
|
374
|
+
return optionRoleColor(optionColumnRole(columnId), surface);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function optionColumnRole(columnId: OptionColumnId): OptionColorRole {
|
|
378
|
+
if (columnId === "strike") return "strike";
|
|
379
|
+
if (columnId.endsWith("Iv")) return "iv";
|
|
380
|
+
if (columnId.endsWith("Volume") || columnId.endsWith("OpenInterest")) return "activity";
|
|
381
|
+
if (columnId.endsWith("Bid") || columnId.endsWith("Ask")) return "price";
|
|
382
|
+
return columnId.startsWith("call") ? "call" : "put";
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function optionRoleThemeColor(role: OptionColorRole): string {
|
|
386
|
+
switch (role) {
|
|
387
|
+
case "call":
|
|
388
|
+
case "iv":
|
|
389
|
+
return colors.positive;
|
|
390
|
+
case "put":
|
|
391
|
+
return colors.negative;
|
|
392
|
+
case "price":
|
|
393
|
+
return colors.warning;
|
|
394
|
+
case "activity":
|
|
395
|
+
case "strike":
|
|
396
|
+
return colors.borderFocused;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function mostReadableColor(surface: string, candidates: readonly string[]): string {
|
|
401
|
+
return candidates.reduce((best, candidate) =>
|
|
402
|
+
contrastRatio(candidate, surface) > contrastRatio(best, surface) ? candidate : best,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function optionRoleColor(role: OptionColorRole, surface: string): string {
|
|
407
|
+
const preferred = OPTION_BASE_COLORS[role];
|
|
408
|
+
const themeColor = optionRoleThemeColor(role);
|
|
409
|
+
const fallback = mostReadableColor(surface, [
|
|
410
|
+
preferred,
|
|
411
|
+
themeColor,
|
|
412
|
+
colors.text,
|
|
413
|
+
colors.textBright,
|
|
414
|
+
higherContrast("#ffffff", "#000000", surface),
|
|
415
|
+
]);
|
|
416
|
+
return blendForContrast(preferred, surface, fallback, OPTION_TEXT_MIN_CONTRAST);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function optionMutedColor(surface: string): string {
|
|
420
|
+
const fallback = mostReadableColor(surface, [
|
|
421
|
+
colors.textDim,
|
|
422
|
+
colors.text,
|
|
423
|
+
colors.textBright,
|
|
424
|
+
higherContrast("#ffffff", "#000000", surface),
|
|
425
|
+
]);
|
|
426
|
+
return blendForContrast(colors.textDim, surface, fallback, OPTION_TEXT_MIN_CONTRAST);
|
|
387
427
|
}
|
|
388
428
|
|
|
389
429
|
function optionMoneynessBackground(
|
|
@@ -443,23 +483,28 @@ function renderOptionCell(
|
|
|
443
483
|
rowState: { selected: boolean; hovered: boolean },
|
|
444
484
|
): DataTableCell {
|
|
445
485
|
const selectedColor = rowState.selected ? colors.selectedText : undefined;
|
|
486
|
+
const rowSurface = rowState.selected ? colors.selected : rowState.hovered ? hoverBg() : colors.bg;
|
|
446
487
|
|
|
447
488
|
if (column.id === "strike") {
|
|
489
|
+
const backgroundColor = rowState.selected || rowState.hovered
|
|
490
|
+
? undefined
|
|
491
|
+
: blendHex(colors.bg, row.isPositionStrike ? colors.borderFocused : colors.header, row.isPositionStrike ? 0.18 : 0.1);
|
|
492
|
+
const surface = backgroundColor ?? rowSurface;
|
|
448
493
|
return {
|
|
449
494
|
text: formatStrikeLabel(row.strike),
|
|
450
|
-
color: selectedColor ??
|
|
451
|
-
backgroundColor
|
|
452
|
-
? undefined
|
|
453
|
-
: blendHex(colors.bg, row.isPositionStrike ? colors.borderFocused : colors.header, row.isPositionStrike ? 0.18 : 0.1),
|
|
495
|
+
color: selectedColor ?? optionRoleColor("strike", surface),
|
|
496
|
+
backgroundColor,
|
|
454
497
|
attributes: rowState.selected || row.isPositionStrike ? TextAttributes.BOLD : TextAttributes.NONE,
|
|
455
498
|
};
|
|
456
499
|
}
|
|
457
500
|
|
|
458
501
|
const contract = optionContractForColumn(row, column.id);
|
|
502
|
+
const backgroundColor = optionMoneynessBackground(row, contract, column.id, rowState);
|
|
503
|
+
const surface = backgroundColor ?? rowSurface;
|
|
459
504
|
return {
|
|
460
505
|
text: formatOptionContractCell(contract, column),
|
|
461
|
-
color: selectedColor ?? (contract ?
|
|
462
|
-
backgroundColor
|
|
506
|
+
color: selectedColor ?? (contract ? optionRoleColor(optionColumnRole(column.id), surface) : optionMutedColor(surface)),
|
|
507
|
+
backgroundColor,
|
|
463
508
|
};
|
|
464
509
|
}
|
|
465
510
|
|
|
@@ -475,7 +520,7 @@ export const optionsPlugin: GloomPlugin = {
|
|
|
475
520
|
id: "options",
|
|
476
521
|
name: "Options",
|
|
477
522
|
icon: "O",
|
|
478
|
-
component:
|
|
523
|
+
component: OptionsView,
|
|
479
524
|
defaultPosition: "right",
|
|
480
525
|
defaultMode: "floating",
|
|
481
526
|
defaultFloatingSize: { width: 112, height: 28 },
|
|
@@ -498,7 +543,7 @@ export const optionsPlugin: GloomPlugin = {
|
|
|
498
543
|
id: "options",
|
|
499
544
|
name: "Options",
|
|
500
545
|
order: 35, // after Chart (30), before News (40)
|
|
501
|
-
component:
|
|
546
|
+
component: OptionsView,
|
|
502
547
|
isVisible: ({ hasOptionsChain }) => hasOptionsChain,
|
|
503
548
|
});
|
|
504
549
|
},
|
|
@@ -4,6 +4,7 @@ import { correlationPlugin } from "./correlation";
|
|
|
4
4
|
import { earningsPlugin } from "./earnings";
|
|
5
5
|
import { registerEconCalendarFeature } from "./econ";
|
|
6
6
|
import { fxMatrixPlugin } from "./fx-matrix";
|
|
7
|
+
import { fearGreedPlugin } from "./fear-greed";
|
|
7
8
|
import { holdersPlugin } from "./holders";
|
|
8
9
|
import { insiderPlugin } from "./insider";
|
|
9
10
|
import { marketMoversPlugin } from "./market-movers";
|
|
@@ -74,6 +75,7 @@ export const marketOverviewPlugin = createPluginGroup({
|
|
|
74
75
|
correlationPlugin,
|
|
75
76
|
worldIndicesPlugin,
|
|
76
77
|
marketMoversPlugin,
|
|
78
|
+
fearGreedPlugin,
|
|
77
79
|
sectorsPlugin,
|
|
78
80
|
fxMatrixPlugin,
|
|
79
81
|
],
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
renderTable,
|
|
17
17
|
} from "../../../../utils/cli-output";
|
|
18
18
|
import { exchangeShortName } from "../../../../utils/market-status";
|
|
19
|
-
import { formatCompact
|
|
19
|
+
import { formatCompact } from "../../../../utils/format";
|
|
20
20
|
import { formatMarketCostWithCurrency, formatMarketPriceWithCurrency, formatMarketQuantity } from "../../../../utils/market-format";
|
|
21
21
|
import type { AppConfig } from "../../../../types/config";
|
|
22
22
|
import type { CliCommandContext, CliCommandDef } from "../../../../types/plugin";
|
|
@@ -282,7 +282,6 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
282
282
|
[tickers, financialsMap, activeSort, columnContext, columns],
|
|
283
283
|
);
|
|
284
284
|
|
|
285
|
-
const currentTabIdx = visibleCollections.findIndex((collection) => collection.id === activeCollectionId);
|
|
286
285
|
const selectedIdx = sortedTickers.findIndex((ticker) => ticker.metadata.ticker === cursorSymbol);
|
|
287
286
|
const safeSelectedIdx = selectedIdx >= 0 ? selectedIdx : 0;
|
|
288
287
|
|
|
@@ -359,35 +358,15 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
359
358
|
return true;
|
|
360
359
|
}
|
|
361
360
|
|
|
362
|
-
if (showCollectionTabs && (key === "h" || key === "left")) {
|
|
363
|
-
event.preventDefault?.();
|
|
364
|
-
event.stopPropagation?.();
|
|
365
|
-
const previousCollection = visibleCollections[Math.max(currentTabIdx - 1, 0)];
|
|
366
|
-
if (previousCollection) handleCollectionSelect(previousCollection.id);
|
|
367
|
-
return true;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (showCollectionTabs && (key === "l" || key === "right")) {
|
|
371
|
-
event.preventDefault?.();
|
|
372
|
-
event.stopPropagation?.();
|
|
373
|
-
const nextCollection = visibleCollections[Math.min(currentTabIdx + 1, visibleCollections.length - 1)];
|
|
374
|
-
if (nextCollection) handleCollectionSelect(nextCollection.id);
|
|
375
|
-
return true;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
361
|
return false;
|
|
379
362
|
}, [
|
|
380
363
|
cashDrawerExpanded,
|
|
381
|
-
currentTabIdx,
|
|
382
364
|
focused,
|
|
383
365
|
openTickerFloating,
|
|
384
366
|
safeSelectedIdx,
|
|
385
367
|
setCashDrawerExpanded,
|
|
386
|
-
handleCollectionSelect,
|
|
387
368
|
showCashDrawer,
|
|
388
|
-
showCollectionTabs,
|
|
389
369
|
sortedTickers,
|
|
390
|
-
visibleCollections,
|
|
391
370
|
]);
|
|
392
371
|
|
|
393
372
|
useEffect(() => {
|
|
@@ -471,32 +450,29 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
471
450
|
if (queue.length === 0) return;
|
|
472
451
|
|
|
473
452
|
let cancelled = false;
|
|
474
|
-
const
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
453
|
+
const runBatch = async (): Promise<void> => {
|
|
454
|
+
const entries = queue.flatMap((ticker) => {
|
|
455
|
+
const instrument = instrumentFromTicker(ticker, ticker.metadata.ticker);
|
|
456
|
+
if (!instrument) return [];
|
|
457
|
+
const key = `${ticker.metadata.ticker}:${ticker.metadata.exchange ?? ""}`;
|
|
458
|
+
warmupInFlightRef.current.add(key);
|
|
459
|
+
warmupAttemptRef.current.set(key, nowTimestamp);
|
|
460
|
+
return [{ key, instrument }];
|
|
461
|
+
});
|
|
462
|
+
if (entries.length === 0) return;
|
|
483
463
|
try {
|
|
484
|
-
|
|
485
|
-
await sharedCoordinator.loadSnapshot(instrument);
|
|
486
|
-
}
|
|
464
|
+
await sharedCoordinator.loadSnapshotsBatch(entries.map((entry) => entry.instrument));
|
|
487
465
|
} catch {
|
|
488
466
|
// Best-effort warmup for visible rows only.
|
|
489
467
|
} finally {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
if (mountedRef.current && !cancelled) {
|
|
494
|
-
await runNext();
|
|
468
|
+
for (const entry of entries) {
|
|
469
|
+
warmupInFlightRef.current.delete(entry.key);
|
|
470
|
+
}
|
|
495
471
|
}
|
|
496
472
|
};
|
|
497
473
|
|
|
498
474
|
const timeoutId = setTimeout(() => {
|
|
499
|
-
void
|
|
475
|
+
if (!cancelled && mountedRef.current) void runBatch();
|
|
500
476
|
}, VISIBLE_FINANCIAL_WARMUP_DELAY_MS);
|
|
501
477
|
|
|
502
478
|
return () => {
|
|
@@ -590,6 +566,7 @@ export function PortfolioListPane({ focused, width, height }: PaneProps) {
|
|
|
590
566
|
activeValue={activeCollectionId}
|
|
591
567
|
onSelect={handleCollectionSelect}
|
|
592
568
|
compact
|
|
569
|
+
focused={focused}
|
|
593
570
|
/>
|
|
594
571
|
</Box>
|
|
595
572
|
</Box>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { AnalystRatingRecord } from "../../../types/financials";
|
|
3
|
+
import {
|
|
4
|
+
nextRatingSortPreference,
|
|
5
|
+
sortRatingRows,
|
|
6
|
+
type RatingSortPreference,
|
|
7
|
+
} from "./index";
|
|
8
|
+
|
|
9
|
+
const ratings: AnalystRatingRecord[] = [
|
|
10
|
+
{
|
|
11
|
+
date: "2026-05-06",
|
|
12
|
+
firm: "Beta Capital",
|
|
13
|
+
action: "Raises",
|
|
14
|
+
current: "Neutral",
|
|
15
|
+
prior: "Neutral",
|
|
16
|
+
currentPriceTarget: 385,
|
|
17
|
+
priorPriceTarget: 270,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
date: "2026-05-07",
|
|
21
|
+
firm: "Alpha Research",
|
|
22
|
+
action: "Downgrade",
|
|
23
|
+
current: "Hold",
|
|
24
|
+
prior: "Buy",
|
|
25
|
+
currentPriceTarget: 340,
|
|
26
|
+
priorPriceTarget: 335,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
date: "2026-05-06",
|
|
30
|
+
firm: "Zenith",
|
|
31
|
+
action: "Upgrade",
|
|
32
|
+
current: "Buy",
|
|
33
|
+
prior: "Neutral",
|
|
34
|
+
currentPriceTarget: 525,
|
|
35
|
+
priorPriceTarget: 265,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
date: "2026-05-05",
|
|
39
|
+
firm: "No Target",
|
|
40
|
+
action: "Reiterates",
|
|
41
|
+
current: "Buy",
|
|
42
|
+
prior: "Buy",
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
describe("analyst rating sorting", () => {
|
|
47
|
+
test("sorts date newest first by default", () => {
|
|
48
|
+
const preference: RatingSortPreference = { columnId: "date", direction: "desc" };
|
|
49
|
+
|
|
50
|
+
expect(sortRatingRows(ratings, preference).map((row) => row.firm)).toEqual([
|
|
51
|
+
"Alpha Research",
|
|
52
|
+
"Beta Capital",
|
|
53
|
+
"Zenith",
|
|
54
|
+
"No Target",
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("sorts target by current target value with missing targets last", () => {
|
|
59
|
+
const preference: RatingSortPreference = { columnId: "target", direction: "desc" };
|
|
60
|
+
|
|
61
|
+
expect(sortRatingRows(ratings, preference).map((row) => row.firm)).toEqual([
|
|
62
|
+
"Zenith",
|
|
63
|
+
"Beta Capital",
|
|
64
|
+
"Alpha Research",
|
|
65
|
+
"No Target",
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("sorts text columns alphabetically with recent dates as a tie-breaker", () => {
|
|
70
|
+
const preference: RatingSortPreference = { columnId: "firm", direction: "asc" };
|
|
71
|
+
|
|
72
|
+
expect(sortRatingRows(ratings, preference).map((row) => row.firm)).toEqual([
|
|
73
|
+
"Alpha Research",
|
|
74
|
+
"Beta Capital",
|
|
75
|
+
"No Target",
|
|
76
|
+
"Zenith",
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("uses sensible first-click directions per column", () => {
|
|
81
|
+
expect(nextRatingSortPreference({ columnId: "date", direction: "desc" }, "date")).toEqual({
|
|
82
|
+
columnId: "date",
|
|
83
|
+
direction: "asc",
|
|
84
|
+
});
|
|
85
|
+
expect(nextRatingSortPreference({ columnId: "date", direction: "desc" }, "target")).toEqual({
|
|
86
|
+
columnId: "target",
|
|
87
|
+
direction: "desc",
|
|
88
|
+
});
|
|
89
|
+
expect(nextRatingSortPreference({ columnId: "target", direction: "desc" }, "firm")).toEqual({
|
|
90
|
+
columnId: "firm",
|
|
91
|
+
direction: "asc",
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|