gloomberb 0.6.0 → 0.7.0
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 +2 -1
- package/patches/@opentui%2Fcore@0.1.90.patch +48 -24
- package/src/app.tsx +9 -6
- 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 +70 -0
- 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 +17 -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-stock-chart.tsx +62 -77
- package/src/components/chart/native/kitty.test.ts +23 -0
- package/src/components/chart/native/surface-visibility.ts +48 -0
- package/src/components/chart/static-chart-surface.tsx +28 -4
- package/src/components/chart/stock-chart.test.ts +0 -16
- package/src/components/chart/stock-chart.tsx +52 -71
- package/src/components/chart/time-axis-label.tsx +43 -0
- package/src/components/command-bar/command-bar.test.tsx +71 -8
- package/src/components/command-bar/command-bar.tsx +388 -713
- package/src/components/command-bar/confirm-body.tsx +51 -0
- package/src/components/command-bar/list-model.ts +77 -0
- package/src/components/command-bar/view-model.ts +0 -19
- package/src/components/command-bar/workflow-ops.ts +4 -0
- 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 +6 -2
- package/src/components/layout/floating-pane.tsx +30 -2
- package/src/components/layout/header.tsx +10 -7
- 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 +26 -4
- package/src/components/layout/pane-header.tsx +5 -5
- package/src/components/layout/pane-keyboard-scroll.test.tsx +226 -0
- package/src/components/layout/shell.test.tsx +66 -22
- package/src/components/layout/shell.tsx +13 -6
- 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 +558 -0
- package/src/components/table-view-shared.tsx +158 -0
- package/src/components/ticker-list-table-view.tsx +42 -72
- package/src/components/ui/confirm-dialog.tsx +66 -0
- package/src/components/ui/data-table.tsx +18 -10
- package/src/components/ui/index.ts +0 -1
- package/src/components/ui/page-stack-view.tsx +39 -10
- package/src/components/ui/remote-image.tsx +15 -40
- package/src/components/ui/table-layout.test.ts +16 -0
- package/src/components/ui/table-layout.ts +10 -3
- package/src/components/ui/ui.test.tsx +100 -20
- package/src/core/state/app-state.ts +6 -3
- package/src/market-data/coordinator.test.ts +1 -1
- package/src/market-data/coordinator.ts +0 -1
- package/src/news/aggregator.ts +0 -3
- 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 +0 -3
- 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 +105 -0
- package/src/plugins/builtin/chat-controller.ts +75 -6
- package/src/plugins/builtin/chat.test.tsx +6 -6
- package/src/plugins/builtin/chat.tsx +29 -20
- package/src/plugins/builtin/cloud-tweets.tsx +0 -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 +478 -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.tsx +10 -15
- package/src/plugins/builtin/insider/index.tsx +22 -89
- package/src/plugins/builtin/market-movers/index.tsx +1 -1
- package/src/plugins/builtin/news-wire/index.tsx +41 -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 +18 -55
- package/src/plugins/builtin/options.test.tsx +58 -2
- package/src/plugins/builtin/options.tsx +108 -49
- package/src/plugins/builtin/plugin-groups.ts +2 -0
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +1 -1
- package/src/plugins/builtin/research/index.tsx +5 -21
- 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 +36 -28
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +69 -4
- package/src/plugins/builtin/ticker-detail.test.tsx +102 -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 +56 -5
- package/src/plugins/ibkr/trade-tab.tsx +13 -11
- package/src/plugins/pane-manager.ts +0 -13
- package/src/plugins/plugin-runtime.test.tsx +6 -6
- package/src/plugins/plugin-runtime.tsx +13 -8
- package/src/plugins/prediction-markets/cli.ts +0 -1
- package/src/plugins/prediction-markets/controller-data.ts +0 -2
- 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 +10 -39
- package/src/react/input.ts +11 -2
- package/src/renderers/electrobun/view/data-table.tsx +28 -6
- package/src/renderers/electrobun/view/desktop-controls.tsx +34 -20
- package/src/renderers/electrobun/view/dialog-host.tsx +11 -1
- package/src/renderers/electrobun/view/input-host.tsx +123 -12
- package/src/renderers/electrobun/view/key-event.ts +1 -1
- package/src/renderers/electrobun/view/styles.css +331 -0
- package/src/renderers/electrobun/view/ui-host.tsx +22 -3
- package/src/renderers/opentui/chart-surface.tsx +179 -0
- package/src/renderers/opentui/host.tsx +6 -0
- package/src/renderers/opentui/image-loader.test.ts +25 -0
- package/src/renderers/opentui/image-loader.ts +105 -0
- package/src/renderers/opentui/image-surface.tsx +266 -0
- package/src/renderers/opentui/input-host.tsx +45 -6
- package/src/renderers/opentui/start.tsx +48 -30
- package/src/renderers/opentui/test-utils.tsx +17 -0
- package/src/renderers/opentui/ui-host.tsx +7 -3
- package/src/sources/provider-router.ts +0 -4
- package/src/state/app-context.test.ts +45 -0
- package/src/state/app-context.tsx +4 -0
- package/src/state/pane-scroll-registry.tsx +254 -0
- package/src/state/use-inline-tickers.test.tsx +73 -0
- package/src/state/use-inline-tickers.ts +7 -1
- package/src/test-support/plugin-runtime.ts +3 -2
- package/src/theme/colors.ts +12 -3
- 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/data-provider.ts +0 -1
- package/src/types/plugin.ts +0 -2
- package/src/ui/host.tsx +4 -0
- package/src/ui/index.tsx +9 -1
- package/src/utils/back-navigation.ts +24 -1
- 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
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Box, ScrollBox, Text, TextAttributes } from "../../../ui";
|
|
3
|
+
import { useShortcut } from "../../../react/input";
|
|
4
|
+
import { SpeedometerGauge, StaticChartSurface, usePaneFooter, type SpeedometerSegment } from "../../../components";
|
|
5
|
+
import type { GloomPlugin, PaneProps } from "../../../types/plugin";
|
|
6
|
+
import { colors, blendHex } from "../../../theme/colors";
|
|
7
|
+
import { resolveChartPalette } from "../../../components/chart/chart-renderer";
|
|
8
|
+
import type { ChartIndicatorOverlays } from "../../../components/chart/chart-types";
|
|
9
|
+
import { formatNumber } from "../../../utils/format";
|
|
10
|
+
import {
|
|
11
|
+
fetchFearGreedData,
|
|
12
|
+
type FearGreedData,
|
|
13
|
+
type FearGreedIndicator,
|
|
14
|
+
type FearGreedRating,
|
|
15
|
+
type FearGreedValueFormat,
|
|
16
|
+
} from "./fear-greed-data";
|
|
17
|
+
|
|
18
|
+
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
19
|
+
const FEAR_GREED_GAUGE_SEGMENTS: SpeedometerSegment[] = [
|
|
20
|
+
{ from: 0, to: 24.999, label: "EXTREME FEAR", color: colors.negative },
|
|
21
|
+
{ from: 25, to: 44.999, label: "FEAR", color: colors.warning },
|
|
22
|
+
{ from: 45, to: 55, label: "NEUTRAL", color: colors.neutral },
|
|
23
|
+
{ from: 55.001, to: 75, label: "GREED", color: colors.positive },
|
|
24
|
+
{ from: 75.001, to: 100, label: "EXTREME GREED", color: colors.positive },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
let sharedCache: { data: FearGreedData; fetchedAt: number } | null = null;
|
|
28
|
+
let activeFetch: Promise<FearGreedData> | null = null;
|
|
29
|
+
|
|
30
|
+
async function loadFearGreed(force = false): Promise<FearGreedData> {
|
|
31
|
+
if (!force && sharedCache && Date.now() - sharedCache.fetchedAt < CACHE_TTL_MS) {
|
|
32
|
+
return sharedCache.data;
|
|
33
|
+
}
|
|
34
|
+
if (activeFetch) return activeFetch;
|
|
35
|
+
|
|
36
|
+
activeFetch = fetchFearGreedData()
|
|
37
|
+
.then((data) => {
|
|
38
|
+
sharedCache = { data, fetchedAt: Date.now() };
|
|
39
|
+
return data;
|
|
40
|
+
})
|
|
41
|
+
.finally(() => {
|
|
42
|
+
activeFetch = null;
|
|
43
|
+
});
|
|
44
|
+
return activeFetch;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ratingLabel(rating: FearGreedRating): string {
|
|
48
|
+
return rating.toUpperCase();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ratingColor(rating: FearGreedRating): string {
|
|
52
|
+
switch (rating) {
|
|
53
|
+
case "extreme fear":
|
|
54
|
+
return colors.negative;
|
|
55
|
+
case "fear":
|
|
56
|
+
return colors.warning;
|
|
57
|
+
case "neutral":
|
|
58
|
+
return colors.neutral;
|
|
59
|
+
case "greed":
|
|
60
|
+
case "extreme greed":
|
|
61
|
+
return colors.positive;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function ratingTrend(rating: FearGreedRating): "positive" | "negative" | "neutral" {
|
|
66
|
+
switch (rating) {
|
|
67
|
+
case "extreme fear":
|
|
68
|
+
case "fear":
|
|
69
|
+
return "negative";
|
|
70
|
+
case "neutral":
|
|
71
|
+
return "neutral";
|
|
72
|
+
case "greed":
|
|
73
|
+
case "extreme greed":
|
|
74
|
+
return "positive";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatScore(score: number | null | undefined): string {
|
|
79
|
+
if (score == null || Number.isNaN(score)) return "--";
|
|
80
|
+
return String(Math.round(score));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatIndicatorValue(value: number | null | undefined, format: FearGreedValueFormat): string {
|
|
84
|
+
if (value == null || Number.isNaN(value)) return "--";
|
|
85
|
+
switch (format) {
|
|
86
|
+
case "score":
|
|
87
|
+
return formatScore(value);
|
|
88
|
+
case "percent":
|
|
89
|
+
return `${formatNumber(value, 2)}%`;
|
|
90
|
+
case "ratio":
|
|
91
|
+
return value.toFixed(2);
|
|
92
|
+
case "number": {
|
|
93
|
+
const abs = Math.abs(value);
|
|
94
|
+
if (abs >= 1000) return formatNumber(value, 0);
|
|
95
|
+
if (abs >= 100) return formatNumber(value, 1);
|
|
96
|
+
return formatNumber(value, 2);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function formatAxisValue(format: FearGreedValueFormat): (value: number) => string {
|
|
102
|
+
return (value) => formatIndicatorValue(value, format);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function formatUpdatedAt(date: Date | null): string {
|
|
106
|
+
if (!date) return "Last updated --";
|
|
107
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
108
|
+
timeZone: "America/New_York",
|
|
109
|
+
month: "short",
|
|
110
|
+
day: "numeric",
|
|
111
|
+
hour: "numeric",
|
|
112
|
+
minute: "2-digit",
|
|
113
|
+
hour12: true,
|
|
114
|
+
}).formatToParts(date);
|
|
115
|
+
const part = (type: Intl.DateTimeFormatPartTypes) => parts.find((entry) => entry.type === type)?.value ?? "";
|
|
116
|
+
return `Last updated ${part("month")} ${part("day")} at ${part("hour")}:${part("minute")} ${part("dayPeriod")} ET`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function formatAge(ms: number): string {
|
|
120
|
+
const secs = Math.max(0, Math.floor(ms / 1000));
|
|
121
|
+
if (secs < 60) return `${secs}s ago`;
|
|
122
|
+
const mins = Math.floor(secs / 60);
|
|
123
|
+
if (mins < 60) return `${mins}m ago`;
|
|
124
|
+
const hours = Math.floor(mins / 60);
|
|
125
|
+
return `${hours}h ago`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function SentimentBadge({ rating }: { rating: FearGreedRating }) {
|
|
129
|
+
const color = ratingColor(rating);
|
|
130
|
+
return (
|
|
131
|
+
<Box height={1} paddingX={1} backgroundColor={blendHex(colors.bg, color, 0.28)}>
|
|
132
|
+
<Text fg={color} attributes={TextAttributes.BOLD}>{ratingLabel(rating)}</Text>
|
|
133
|
+
</Box>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function PreviousScoreGrid({ data, width }: { data: FearGreedData; width: number }) {
|
|
138
|
+
const items = [
|
|
139
|
+
{ label: "Previous close", value: data.overall.previousClose },
|
|
140
|
+
{ label: "1 week ago", value: data.overall.previousWeek },
|
|
141
|
+
{ label: "1 month ago", value: data.overall.previousMonth },
|
|
142
|
+
{ label: "1 year ago", value: data.overall.previousYear },
|
|
143
|
+
];
|
|
144
|
+
const columns = width >= 84 ? 4 : 2;
|
|
145
|
+
const columnWidth = Math.max(18, Math.floor((width - 2) / columns));
|
|
146
|
+
const rows = Array.from({ length: Math.ceil(items.length / columns) }, (_, rowIndex) => (
|
|
147
|
+
items.slice(rowIndex * columns, rowIndex * columns + columns)
|
|
148
|
+
));
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<Box flexDirection="column" paddingX={1} marginTop={1}>
|
|
152
|
+
{rows.map((row, rowIndex) => (
|
|
153
|
+
<Box key={rowIndex} flexDirection="row" height={1}>
|
|
154
|
+
{row.map((item) => {
|
|
155
|
+
const value = item.value;
|
|
156
|
+
const color = value == null ? colors.textDim : ratingColor(value < 25 ? "extreme fear" : value < 45 ? "fear" : value <= 55 ? "neutral" : "greed");
|
|
157
|
+
return (
|
|
158
|
+
<Box key={item.label} width={columnWidth} flexShrink={0} flexDirection="row">
|
|
159
|
+
<Text fg={colors.textDim}>{item.label}: </Text>
|
|
160
|
+
<Text fg={color} attributes={TextAttributes.BOLD}>{formatScore(value)}</Text>
|
|
161
|
+
</Box>
|
|
162
|
+
);
|
|
163
|
+
})}
|
|
164
|
+
</Box>
|
|
165
|
+
))}
|
|
166
|
+
</Box>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function chartOverlay(indicator: FearGreedIndicator): ChartIndicatorOverlays | null {
|
|
171
|
+
if (indicator.secondaryPoints.length === 0) return null;
|
|
172
|
+
return {
|
|
173
|
+
smaLines: [{
|
|
174
|
+
period: 0,
|
|
175
|
+
points: indicator.secondaryPoints,
|
|
176
|
+
color: colors.warning,
|
|
177
|
+
}],
|
|
178
|
+
emaLines: [],
|
|
179
|
+
bollinger: null,
|
|
180
|
+
rsi: null,
|
|
181
|
+
macd: null,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function SentimentChart({
|
|
186
|
+
title,
|
|
187
|
+
subtitle,
|
|
188
|
+
rating,
|
|
189
|
+
score,
|
|
190
|
+
points,
|
|
191
|
+
width,
|
|
192
|
+
valueFormat,
|
|
193
|
+
updatedAt,
|
|
194
|
+
primaryLabel,
|
|
195
|
+
secondaryLabel,
|
|
196
|
+
secondaryValue,
|
|
197
|
+
overlays,
|
|
198
|
+
}: {
|
|
199
|
+
title: string;
|
|
200
|
+
subtitle: string;
|
|
201
|
+
rating: FearGreedRating;
|
|
202
|
+
score: number | null;
|
|
203
|
+
points: FearGreedIndicator["points"];
|
|
204
|
+
width: number;
|
|
205
|
+
valueFormat: FearGreedValueFormat;
|
|
206
|
+
updatedAt: Date | null;
|
|
207
|
+
primaryLabel: string;
|
|
208
|
+
secondaryLabel?: string;
|
|
209
|
+
secondaryValue?: number | null;
|
|
210
|
+
overlays?: ChartIndicatorOverlays | null;
|
|
211
|
+
}) {
|
|
212
|
+
const chartWidth = Math.max(24, width - 2);
|
|
213
|
+
const chartHeight = width >= 96 ? 12 : 10;
|
|
214
|
+
const color = ratingColor(rating);
|
|
215
|
+
const basePalette = resolveChartPalette(colors, ratingTrend(rating));
|
|
216
|
+
const palette = {
|
|
217
|
+
...basePalette,
|
|
218
|
+
lineColor: color,
|
|
219
|
+
fillColor: blendHex(colors.bg, color, 0.18),
|
|
220
|
+
gridColor: blendHex(colors.bg, colors.border, 0.55),
|
|
221
|
+
};
|
|
222
|
+
const latest = points.length > 0 ? points[points.length - 1]!.close : null;
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<Box flexDirection="column" marginTop={2} paddingX={1}>
|
|
226
|
+
<Box flexDirection="row" height={1}>
|
|
227
|
+
<Text fg={colors.textBright} attributes={TextAttributes.BOLD}>{title.toUpperCase()}</Text>
|
|
228
|
+
<Box flexGrow={1} />
|
|
229
|
+
<SentimentBadge rating={rating} />
|
|
230
|
+
</Box>
|
|
231
|
+
<Box height={1} marginTop={1}>
|
|
232
|
+
<Text fg={colors.text} attributes={TextAttributes.BOLD}>{subtitle}</Text>
|
|
233
|
+
</Box>
|
|
234
|
+
<Box flexDirection="row" height={1}>
|
|
235
|
+
<Text fg={color}>● </Text>
|
|
236
|
+
<Text fg={colors.textDim}>{primaryLabel}</Text>
|
|
237
|
+
{secondaryLabel ? (
|
|
238
|
+
<>
|
|
239
|
+
<Text fg={colors.warning}> ● </Text>
|
|
240
|
+
<Text fg={colors.textDim}>{secondaryLabel}</Text>
|
|
241
|
+
</>
|
|
242
|
+
) : null}
|
|
243
|
+
<Box flexGrow={1} />
|
|
244
|
+
<Text fg={colors.textDim}>score </Text>
|
|
245
|
+
<Text fg={color} attributes={TextAttributes.BOLD}>{formatScore(score)}</Text>
|
|
246
|
+
<Text fg={colors.textDim}> latest </Text>
|
|
247
|
+
<Text fg={colors.text}>{formatIndicatorValue(latest, valueFormat)}</Text>
|
|
248
|
+
{secondaryLabel && secondaryValue != null ? (
|
|
249
|
+
<>
|
|
250
|
+
<Text fg={colors.textDim}> avg </Text>
|
|
251
|
+
<Text fg={colors.text}>{formatIndicatorValue(secondaryValue, valueFormat)}</Text>
|
|
252
|
+
</>
|
|
253
|
+
) : null}
|
|
254
|
+
</Box>
|
|
255
|
+
{points.length >= 2 ? (
|
|
256
|
+
<Box marginTop={1}>
|
|
257
|
+
<StaticChartSurface
|
|
258
|
+
points={points}
|
|
259
|
+
width={chartWidth}
|
|
260
|
+
height={chartHeight}
|
|
261
|
+
mode="line"
|
|
262
|
+
colors={palette}
|
|
263
|
+
indicators={overlays}
|
|
264
|
+
showTimeAxis
|
|
265
|
+
timeAxisColor={colors.textDim}
|
|
266
|
+
yAxisColor={colors.textDim}
|
|
267
|
+
formatYAxisValue={formatAxisValue(valueFormat)}
|
|
268
|
+
/>
|
|
269
|
+
</Box>
|
|
270
|
+
) : (
|
|
271
|
+
<Box height={chartHeight} marginTop={1} justifyContent="center" alignItems="center">
|
|
272
|
+
<Text fg={colors.textMuted}>Not enough chart data</Text>
|
|
273
|
+
</Box>
|
|
274
|
+
)}
|
|
275
|
+
<Box height={1} marginTop={1}>
|
|
276
|
+
<Text fg={colors.textDim}>{formatUpdatedAt(updatedAt)}</Text>
|
|
277
|
+
</Box>
|
|
278
|
+
</Box>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function IndexHistoryChart({ data, width }: { data: FearGreedData; width: number }) {
|
|
283
|
+
const indicator: FearGreedIndicator = {
|
|
284
|
+
definition: {
|
|
285
|
+
id: "index-history",
|
|
286
|
+
title: "Index History",
|
|
287
|
+
subtitle: "Fear & Greed score over the past year",
|
|
288
|
+
primaryKey: "fear_and_greed_historical",
|
|
289
|
+
primaryLabel: "Fear & Greed",
|
|
290
|
+
valueFormat: "score",
|
|
291
|
+
},
|
|
292
|
+
score: data.overall.score,
|
|
293
|
+
rating: data.overall.rating,
|
|
294
|
+
updatedAt: data.overall.updatedAt,
|
|
295
|
+
points: data.overall.history,
|
|
296
|
+
secondaryPoints: [],
|
|
297
|
+
latestValue: data.overall.score,
|
|
298
|
+
latestSecondaryValue: null,
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
return (
|
|
302
|
+
<SentimentChart
|
|
303
|
+
title={indicator.definition.title}
|
|
304
|
+
subtitle={indicator.definition.subtitle}
|
|
305
|
+
rating={indicator.rating}
|
|
306
|
+
score={indicator.score}
|
|
307
|
+
points={indicator.points}
|
|
308
|
+
width={width}
|
|
309
|
+
valueFormat={indicator.definition.valueFormat}
|
|
310
|
+
updatedAt={indicator.updatedAt}
|
|
311
|
+
primaryLabel={indicator.definition.primaryLabel}
|
|
312
|
+
/>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function FearGreedPane({ paneId, focused, width, height }: PaneProps) {
|
|
317
|
+
const [data, setData] = useState<FearGreedData | null>(sharedCache?.data ?? null);
|
|
318
|
+
const [loading, setLoading] = useState(!sharedCache);
|
|
319
|
+
const [error, setError] = useState<string | null>(null);
|
|
320
|
+
const [lastRefreshed, setLastRefreshed] = useState<number | null>(sharedCache?.fetchedAt ?? null);
|
|
321
|
+
const [now, setNow] = useState(Date.now());
|
|
322
|
+
const fetchGenRef = useRef(0);
|
|
323
|
+
|
|
324
|
+
const load = useCallback(async (force = false) => {
|
|
325
|
+
fetchGenRef.current += 1;
|
|
326
|
+
const gen = fetchGenRef.current;
|
|
327
|
+
setLoading(true);
|
|
328
|
+
setError(null);
|
|
329
|
+
try {
|
|
330
|
+
const nextData = await loadFearGreed(force);
|
|
331
|
+
if (fetchGenRef.current !== gen) return;
|
|
332
|
+
setData(nextData);
|
|
333
|
+
setLastRefreshed(Date.now());
|
|
334
|
+
} catch (err) {
|
|
335
|
+
if (fetchGenRef.current !== gen) return;
|
|
336
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
337
|
+
} finally {
|
|
338
|
+
if (fetchGenRef.current === gen) setLoading(false);
|
|
339
|
+
}
|
|
340
|
+
}, []);
|
|
341
|
+
|
|
342
|
+
useEffect(() => {
|
|
343
|
+
if (!sharedCache) {
|
|
344
|
+
void load();
|
|
345
|
+
}
|
|
346
|
+
}, [load]);
|
|
347
|
+
|
|
348
|
+
useEffect(() => {
|
|
349
|
+
const interval = setInterval(() => setNow(Date.now()), 10_000);
|
|
350
|
+
return () => clearInterval(interval);
|
|
351
|
+
}, []);
|
|
352
|
+
|
|
353
|
+
const refresh = useCallback(() => {
|
|
354
|
+
void load(true);
|
|
355
|
+
}, [load]);
|
|
356
|
+
|
|
357
|
+
useShortcut((event) => {
|
|
358
|
+
if (!focused) return;
|
|
359
|
+
if (event.name === "r") {
|
|
360
|
+
event.preventDefault?.();
|
|
361
|
+
event.stopPropagation?.();
|
|
362
|
+
refresh();
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const footerAge = lastRefreshed ? `refreshed ${formatAge(now - lastRefreshed)}` : loading ? "loading" : "";
|
|
367
|
+
usePaneFooter(paneId, () => ({
|
|
368
|
+
info: [
|
|
369
|
+
...(data ? [{
|
|
370
|
+
id: "score",
|
|
371
|
+
parts: [
|
|
372
|
+
{ text: `${formatScore(data.overall.score)} ${ratingLabel(data.overall.rating)}`, color: ratingColor(data.overall.rating), bold: true },
|
|
373
|
+
],
|
|
374
|
+
}] : []),
|
|
375
|
+
...(footerAge ? [{ id: "age", parts: [{ text: footerAge, tone: loading ? "muted" as const : "value" as const }] }] : []),
|
|
376
|
+
...(error ? [{ id: "error", parts: [{ text: error, tone: "warning" as const }] }] : []),
|
|
377
|
+
],
|
|
378
|
+
hints: [{ id: "refresh", key: "r", label: "efresh", onPress: refresh, disabled: loading }],
|
|
379
|
+
}), [data, error, footerAge, loading, paneId, refresh]);
|
|
380
|
+
|
|
381
|
+
if (loading && !data) {
|
|
382
|
+
return (
|
|
383
|
+
<Box flexDirection="column" width={width} height={height}>
|
|
384
|
+
<Box flexGrow={1} justifyContent="center" alignItems="center">
|
|
385
|
+
<Text fg={colors.textMuted}>Loading Fear & Greed...</Text>
|
|
386
|
+
</Box>
|
|
387
|
+
</Box>
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (!data) {
|
|
392
|
+
return (
|
|
393
|
+
<Box flexDirection="column" width={width} height={height}>
|
|
394
|
+
<Box flexGrow={1} justifyContent="center" alignItems="center" paddingX={1}>
|
|
395
|
+
<Text fg={colors.negative}>{error ?? "Fear & Greed data unavailable"}</Text>
|
|
396
|
+
</Box>
|
|
397
|
+
</Box>
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return (
|
|
402
|
+
<Box flexDirection="column" width={width} height={height}>
|
|
403
|
+
<ScrollBox flexGrow={1} scrollY focusable={false}>
|
|
404
|
+
<Box flexDirection="column" paddingBottom={1}>
|
|
405
|
+
<SpeedometerGauge
|
|
406
|
+
value={data.overall.score}
|
|
407
|
+
valueLabel={ratingLabel(data.overall.rating)}
|
|
408
|
+
width={width}
|
|
409
|
+
segments={FEAR_GREED_GAUGE_SEGMENTS}
|
|
410
|
+
/>
|
|
411
|
+
{loading ? (
|
|
412
|
+
<Box height={1} paddingX={1} marginTop={1} justifyContent="center">
|
|
413
|
+
<Text fg={colors.textMuted}>refreshing...</Text>
|
|
414
|
+
</Box>
|
|
415
|
+
) : null}
|
|
416
|
+
<PreviousScoreGrid data={data} width={width} />
|
|
417
|
+
{error ? (
|
|
418
|
+
<Box paddingX={1} marginTop={1}>
|
|
419
|
+
<Text fg={colors.warning}>{error}</Text>
|
|
420
|
+
</Box>
|
|
421
|
+
) : null}
|
|
422
|
+
<IndexHistoryChart data={data} width={width} />
|
|
423
|
+
<Box flexDirection="row" paddingX={1} marginTop={2} height={1}>
|
|
424
|
+
<Text fg={colors.textBright} attributes={TextAttributes.BOLD}>7 FEAR & GREED INDICATORS</Text>
|
|
425
|
+
</Box>
|
|
426
|
+
{data.indicators.map((indicator) => (
|
|
427
|
+
<SentimentChart
|
|
428
|
+
key={indicator.definition.id}
|
|
429
|
+
title={indicator.definition.title}
|
|
430
|
+
subtitle={indicator.definition.subtitle}
|
|
431
|
+
rating={indicator.rating}
|
|
432
|
+
score={indicator.score}
|
|
433
|
+
points={indicator.points}
|
|
434
|
+
width={width}
|
|
435
|
+
valueFormat={indicator.definition.valueFormat}
|
|
436
|
+
updatedAt={indicator.updatedAt}
|
|
437
|
+
primaryLabel={indicator.definition.primaryLabel}
|
|
438
|
+
secondaryLabel={indicator.definition.secondaryLabel}
|
|
439
|
+
secondaryValue={indicator.latestSecondaryValue}
|
|
440
|
+
overlays={chartOverlay(indicator)}
|
|
441
|
+
/>
|
|
442
|
+
))}
|
|
443
|
+
</Box>
|
|
444
|
+
</ScrollBox>
|
|
445
|
+
</Box>
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export const fearGreedPlugin: GloomPlugin = {
|
|
450
|
+
id: "fear-greed",
|
|
451
|
+
name: "Fear & Greed",
|
|
452
|
+
version: "1.0.0",
|
|
453
|
+
description: "CNN Fear & Greed sentiment gauge and market indicator charts.",
|
|
454
|
+
toggleable: true,
|
|
455
|
+
|
|
456
|
+
panes: [
|
|
457
|
+
{
|
|
458
|
+
id: "fear-greed",
|
|
459
|
+
name: "Fear & Greed",
|
|
460
|
+
icon: "G",
|
|
461
|
+
component: FearGreedPane,
|
|
462
|
+
defaultPosition: "right",
|
|
463
|
+
defaultMode: "floating",
|
|
464
|
+
defaultFloatingSize: { width: 110, height: 36 },
|
|
465
|
+
},
|
|
466
|
+
],
|
|
467
|
+
|
|
468
|
+
paneTemplates: [
|
|
469
|
+
{
|
|
470
|
+
id: "fear-greed-pane",
|
|
471
|
+
paneId: "fear-greed",
|
|
472
|
+
label: "Fear & Greed",
|
|
473
|
+
description: "CNN Fear & Greed sentiment gauge with the seven indicator charts.",
|
|
474
|
+
keywords: ["fear", "greed", "sentiment", "cnn", "market", "indicators", "gauge"],
|
|
475
|
+
shortcut: { prefix: "FNG" },
|
|
476
|
+
},
|
|
477
|
+
],
|
|
478
|
+
};
|
|
@@ -6,7 +6,7 @@ import { usePaneFooter } from "../../../components";
|
|
|
6
6
|
import type { GloomPlugin, PaneProps } from "../../../types/plugin";
|
|
7
7
|
import { colors, blendHex } from "../../../theme/colors";
|
|
8
8
|
import { useAssetData } from "../../plugin-runtime";
|
|
9
|
-
import { MAJOR_CURRENCIES,
|
|
9
|
+
import { MAJOR_CURRENCIES, formatRate, type MajorCurrency } from "./pairs";
|
|
10
10
|
|
|
11
11
|
const REFRESH_INTERVAL_MS = 60_000;
|
|
12
12
|
|
|
@@ -18,7 +18,7 @@ function formatAge(ms: number): string {
|
|
|
18
18
|
return `${Math.floor(mins / 60)}h ago`;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
function FxMatrixPane({ focused, width, height }: PaneProps) {
|
|
22
22
|
const dataProvider = useAssetData();
|
|
23
23
|
const [rates, setRates] = useState<Map<MajorCurrency, number>>(new Map());
|
|
24
24
|
const [loading, setLoading] = useState(true);
|
|
@@ -144,7 +144,7 @@ describe("HelpPane", () => {
|
|
|
144
144
|
const calls: string[] = [];
|
|
145
145
|
const runtime = createTestPluginRuntime({
|
|
146
146
|
openCommandBar: (query?: string) => calls.push(`command:${query ?? ""}`),
|
|
147
|
-
|
|
147
|
+
showPane: (paneId: string) => calls.push(`pane:${paneId}`),
|
|
148
148
|
});
|
|
149
149
|
|
|
150
150
|
await renderHelpPane(runtime);
|
|
@@ -161,6 +161,6 @@ describe("HelpPane", () => {
|
|
|
161
161
|
await testSetup!.renderOnce();
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
expect(calls).toEqual(["
|
|
164
|
+
expect(calls).toEqual(["pane:debug"]);
|
|
165
165
|
});
|
|
166
166
|
});
|
|
@@ -211,7 +211,7 @@ function resolvePluginShortcuts(registry: ReturnType<typeof getSharedRegistry>):
|
|
|
211
211
|
|
|
212
212
|
export function HelpPane({ width, height }: PaneProps) {
|
|
213
213
|
const registry = getSharedRegistry();
|
|
214
|
-
const { openCommandBar,
|
|
214
|
+
const { openCommandBar, showPane } = usePluginAppActions();
|
|
215
215
|
const [hoveredAction, setHoveredAction] = useState<string | null>(null);
|
|
216
216
|
const commandShortcuts = resolveCommandShortcuts(registry);
|
|
217
217
|
const pluginShortcuts = resolvePluginShortcuts(registry);
|
|
@@ -232,7 +232,7 @@ export function HelpPane({ width, height }: PaneProps) {
|
|
|
232
232
|
: [platformShortcut("V")];
|
|
233
233
|
|
|
234
234
|
const openDebugLog = () => {
|
|
235
|
-
|
|
235
|
+
showPane("debug");
|
|
236
236
|
};
|
|
237
237
|
|
|
238
238
|
const openLayoutActions = () => {
|
|
@@ -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;
|
|
@@ -452,7 +453,7 @@ function Tile({ tile, selected, currency, onSelect }: {
|
|
|
452
453
|
width={renderWidth}
|
|
453
454
|
height={renderHeight}
|
|
454
455
|
backgroundColor={selected ? colors.selected : tileColor(tile.row)}
|
|
455
|
-
onMouseDown={(event) => {
|
|
456
|
+
onMouseDown={(event: PreventableMouseEvent) => {
|
|
456
457
|
event.preventDefault();
|
|
457
458
|
onSelect();
|
|
458
459
|
}}
|
|
@@ -538,7 +539,7 @@ function DesktopTile({ tile, chartWidth, chartHeight, selected, hovered, currenc
|
|
|
538
539
|
<Box
|
|
539
540
|
data-gloom-role="holders-desktop-tile"
|
|
540
541
|
style={style}
|
|
541
|
-
onMouseDown={(event) => {
|
|
542
|
+
onMouseDown={(event: PreventableMouseEvent) => {
|
|
542
543
|
event.preventDefault();
|
|
543
544
|
onSelect();
|
|
544
545
|
}}
|
|
@@ -681,7 +682,8 @@ function HoldersTreemap({ rows, width, height, selectedId, onSelect, currency }:
|
|
|
681
682
|
);
|
|
682
683
|
}
|
|
683
684
|
|
|
684
|
-
|
|
685
|
+
function HoldersView({ focused, width, height }: { focused: boolean; width: number; height: number }) {
|
|
686
|
+
const { nativePaneChrome } = useUiCapabilities();
|
|
685
687
|
const { symbol, ticker } = usePaneTicker();
|
|
686
688
|
const dataProvider = useAssetData();
|
|
687
689
|
const [viewMode, setViewMode] = usePluginPaneState<ViewMode>("viewMode", "chart");
|
|
@@ -853,6 +855,7 @@ export function HoldersView({ focused, width, height }: Pick<PaneProps, "focused
|
|
|
853
855
|
: loading
|
|
854
856
|
? "Loading holders..."
|
|
855
857
|
: error ?? "No holders available";
|
|
858
|
+
const chartHeight = Math.max(1, height - 1 - (nativePaneChrome ? 1 : 0));
|
|
856
859
|
|
|
857
860
|
return (
|
|
858
861
|
<Box flexDirection="column" width={width} height={height}>
|
|
@@ -889,7 +892,7 @@ export function HoldersView({ focused, width, height }: Pick<PaneProps, "focused
|
|
|
889
892
|
<HoldersTreemap
|
|
890
893
|
rows={sortedRows}
|
|
891
894
|
width={width}
|
|
892
|
-
height={
|
|
895
|
+
height={chartHeight}
|
|
893
896
|
selectedId={selectedId}
|
|
894
897
|
onSelect={(row) => setSelectedId(row.id)}
|
|
895
898
|
currency={currency}
|
|
@@ -899,14 +902,6 @@ export function HoldersView({ focused, width, height }: Pick<PaneProps, "focused
|
|
|
899
902
|
);
|
|
900
903
|
}
|
|
901
904
|
|
|
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
905
|
export const holdersPlugin: GloomPlugin = {
|
|
911
906
|
id: "holders",
|
|
912
907
|
name: "Holders",
|
|
@@ -919,7 +914,7 @@ export const holdersPlugin: GloomPlugin = {
|
|
|
919
914
|
id: "holders",
|
|
920
915
|
name: "Holders",
|
|
921
916
|
order: 42,
|
|
922
|
-
component:
|
|
917
|
+
component: HoldersView,
|
|
923
918
|
isVisible: ({ ticker }) => !!ticker,
|
|
924
919
|
});
|
|
925
920
|
},
|
|
@@ -929,7 +924,7 @@ export const holdersPlugin: GloomPlugin = {
|
|
|
929
924
|
id: "holders",
|
|
930
925
|
name: "Holders",
|
|
931
926
|
icon: "H",
|
|
932
|
-
component:
|
|
927
|
+
component: HoldersView,
|
|
933
928
|
defaultPosition: "right",
|
|
934
929
|
defaultMode: "floating",
|
|
935
930
|
defaultFloatingSize: { width: 105, height: 34 },
|