gloomberb 0.4.1 → 0.5.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/README.md +2 -2
- package/package.json +1 -1
- package/src/app.tsx +168 -65
- package/src/cli/commands/ticker.ts +45 -15
- package/src/cli/helpers.ts +4 -2
- package/src/cli/registry.test.ts +42 -35
- package/src/cli.test.ts +4 -2
- package/src/components/chart/chart-control-hint.tsx +39 -0
- package/src/components/chart/chart-controller.test.ts +110 -0
- package/src/components/chart/chart-controller.ts +364 -2
- package/src/components/chart/chart-data.ts +177 -29
- package/src/components/chart/chart-indicator-selector.tsx +53 -0
- package/src/components/chart/chart-pane-settings.ts +20 -0
- package/src/components/chart/chart-renderer.ts +220 -30
- package/src/components/chart/chart-resolution.test.ts +67 -0
- package/src/components/chart/chart-resolution.ts +74 -4
- package/src/components/chart/chart-scroll.test.ts +69 -0
- package/src/components/chart/chart-scroll.ts +104 -0
- package/src/components/chart/chart-types.ts +12 -0
- package/src/components/chart/chart-viewport.test.ts +5 -1
- package/src/components/chart/chart-viewport.ts +1 -1
- package/src/components/chart/chart.test.ts +249 -10
- package/src/components/chart/comparison-chart-renderer.test.ts +71 -1
- package/src/components/chart/comparison-chart-renderer.ts +40 -3
- package/src/components/chart/comparison-stock-chart.tsx +218 -123
- package/src/components/chart/indicators/bands.test.ts +50 -0
- package/src/components/chart/indicators/bands.ts +29 -0
- package/src/components/chart/indicators/moving-averages.test.ts +54 -0
- package/src/components/chart/indicators/moving-averages.ts +42 -0
- package/src/components/chart/indicators/options.test.ts +32 -0
- package/src/components/chart/indicators/options.ts +107 -0
- package/src/components/chart/indicators/oscillators.test.ts +79 -0
- package/src/components/chart/indicators/oscillators.ts +92 -0
- package/src/components/chart/indicators/types.ts +36 -0
- package/src/components/chart/native/chart-rasterizer.ts +51 -0
- package/src/components/chart/native/kitty.test.ts +68 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +740 -9
- package/src/components/chart/stock-chart.test.ts +239 -0
- package/src/components/chart/stock-chart.tsx +2257 -372
- package/src/components/command-bar/command-bar.test.tsx +337 -5
- package/src/components/command-bar/command-bar.tsx +292 -120
- package/src/components/command-bar/helpers.ts +1 -1
- package/src/components/command-bar/root-shortcuts.ts +33 -4
- package/src/components/command-bar/view-model.test.ts +3 -3
- package/src/components/command-bar/view-model.ts +4 -4
- package/src/components/command-bar/workflow-ops.ts +10 -0
- package/src/components/layout/header.tsx +2 -1
- package/src/components/layout/shell.test.tsx +186 -27
- package/src/components/layout/shell.tsx +5 -4
- package/src/components/layout/status-bar.test.tsx +16 -7
- package/src/components/layout/status-bar.tsx +4 -1
- package/src/components/markdown-editor.tsx +1 -0
- package/src/components/pane-settings-dialog.tsx +13 -152
- package/src/components/pane-template-wizard.tsx +1 -0
- package/src/components/price-selector-dialog.tsx +12 -7
- package/src/components/ticker-badge-text.test.tsx +50 -0
- package/src/components/ticker-badge-text.tsx +17 -2
- package/src/components/ticker-badge.tsx +2 -2
- package/src/components/ticker-list-table.tsx +54 -1
- package/src/components/toggle-list.tsx +15 -2
- package/src/components/ui/data-table.tsx +60 -3
- package/src/components/ui/external-link.tsx +48 -11
- package/src/components/ui/fields.tsx +2 -1
- package/src/components/ui/index.ts +8 -1
- package/src/components/ui/multi-select-chips.tsx +69 -0
- package/src/components/ui/multi-select-dialog.tsx +267 -0
- package/src/components/ui/multi-select.ts +78 -0
- package/src/components/ui/ui.test.tsx +343 -1
- package/src/data/plugin-state-store.ts +30 -19
- package/src/data/resource-store.ts +133 -103
- package/src/data/session-store.ts +19 -12
- package/src/data/sqlite-cache.test.ts +37 -2
- package/src/data/sqlite-database.ts +8 -2
- package/src/data/sqlite-retry.test.ts +63 -0
- package/src/data/sqlite-retry.ts +66 -0
- package/src/data/ticker-store.ts +29 -18
- package/src/index.tsx +6 -1
- package/src/market-data/coordinator.test.ts +114 -5
- package/src/market-data/coordinator.ts +70 -1
- package/src/market-data/hooks.test.tsx +73 -2
- package/src/market-data/hooks.tsx +16 -4
- package/src/notifications/app-notifier.test.ts +177 -0
- package/src/notifications/app-notifier.ts +241 -0
- package/src/pane-settings.test.ts +2 -0
- package/src/plugins/builtin/ai/index.test.tsx +1 -1
- package/src/plugins/builtin/ai/screener-pane.test.tsx +2 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +1 -0
- package/src/plugins/builtin/ai/ticker-context.ts +3 -2
- package/src/plugins/builtin/alerts/alert-engine.test.ts +90 -0
- package/src/plugins/builtin/alerts/alert-engine.ts +55 -0
- package/src/plugins/builtin/alerts/index.test.tsx +357 -0
- package/src/plugins/builtin/alerts/index.tsx +519 -0
- package/src/plugins/builtin/alerts/types.ts +14 -0
- package/src/plugins/builtin/analytics/index.test.tsx +300 -0
- package/src/plugins/builtin/analytics/index.tsx +775 -0
- package/src/plugins/builtin/analytics/metrics.test.ts +125 -0
- package/src/plugins/builtin/analytics/metrics.ts +155 -0
- package/src/plugins/builtin/ask-ai.test.tsx +2 -0
- package/src/plugins/builtin/chat-controller.test.ts +62 -18
- package/src/plugins/builtin/chat-controller.ts +23 -12
- package/src/plugins/builtin/chat.test.tsx +715 -4
- package/src/plugins/builtin/chat.tsx +415 -119
- package/src/plugins/builtin/comparison-chart.test.tsx +97 -57
- package/src/plugins/builtin/comparison-chart.tsx +1 -2
- package/src/plugins/builtin/correlation/compute.test.ts +147 -0
- package/src/plugins/builtin/correlation/compute.ts +115 -0
- package/src/plugins/builtin/correlation/index.tsx +385 -0
- package/src/plugins/builtin/correlation/plugin.test.ts +44 -0
- package/src/plugins/builtin/correlation/settings.test.ts +34 -0
- package/src/plugins/builtin/correlation/settings.ts +95 -0
- package/src/plugins/builtin/debug.tsx +36 -50
- package/src/plugins/builtin/earnings/earnings-cache.test.ts +181 -0
- package/src/plugins/builtin/earnings/earnings-cache.ts +136 -0
- package/src/plugins/builtin/earnings/earnings-data.test.ts +59 -0
- package/src/plugins/builtin/earnings/earnings-data.ts +50 -0
- package/src/plugins/builtin/earnings/index.tsx +337 -0
- package/src/plugins/builtin/econ/calendar-source.test.ts +219 -0
- package/src/plugins/builtin/econ/calendar-source.ts +121 -0
- package/src/plugins/builtin/econ/fred-client.test.ts +63 -0
- package/src/plugins/builtin/econ/fred-client.ts +79 -0
- package/src/plugins/builtin/econ/fred-series-map.ts +88 -0
- package/src/plugins/builtin/econ/index.tsx +999 -0
- package/src/plugins/builtin/econ/types.ts +13 -0
- package/src/plugins/builtin/fred-settings.ts +10 -0
- package/src/plugins/builtin/fx-matrix/index.tsx +178 -0
- package/src/plugins/builtin/fx-matrix/pairs.ts +30 -0
- package/src/plugins/builtin/help.test.tsx +1 -5
- package/src/plugins/builtin/help.tsx +1 -16
- package/src/plugins/builtin/insider/index.tsx +417 -0
- package/src/plugins/builtin/insider/insider-data.test.ts +55 -0
- package/src/plugins/builtin/insider/insider-data.ts +49 -0
- package/src/plugins/builtin/layout-manager.tsx +24 -16
- package/src/plugins/builtin/market-movers/index.tsx +573 -0
- package/src/plugins/builtin/market-movers/screener.test.ts +143 -0
- package/src/plugins/builtin/market-movers/screener.ts +105 -0
- package/src/plugins/builtin/options.tsx +4 -3
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +11 -6
- package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -3
- package/src/plugins/builtin/portfolio-list/header.tsx +3 -2
- package/src/plugins/builtin/portfolio-list/metrics.ts +22 -11
- package/src/plugins/builtin/portfolio-list/pane.tsx +3 -9
- package/src/plugins/builtin/portfolio-list/settings.ts +9 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -0
- package/src/plugins/builtin/portfolio-list.test.tsx +3 -1
- package/src/plugins/builtin/portfolio-metrics.test.ts +13 -1
- package/src/plugins/builtin/sec.tsx +114 -7
- package/src/plugins/builtin/sectors/index.tsx +359 -0
- package/src/plugins/builtin/sectors/sector-data.ts +18 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +75 -13
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +316 -80
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -6
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +17 -12
- package/src/plugins/builtin/ticker-detail.test.tsx +3 -3
- package/src/plugins/builtin/world-indices/index.tsx +410 -0
- package/src/plugins/builtin/world-indices/indices.ts +52 -0
- package/src/plugins/builtin/yield-curve/index.tsx +242 -0
- package/src/plugins/builtin/yield-curve/treasury-data.test.ts +212 -0
- package/src/plugins/builtin/yield-curve/treasury-data.ts +107 -0
- package/src/plugins/catalog.ts +22 -0
- package/src/plugins/gridlock-notification.ts +15 -0
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
- package/src/plugins/ibkr/index.test.tsx +1 -1
- package/src/plugins/ibkr/index.tsx +1 -1
- package/src/plugins/ibkr/trade-tab.tsx +23 -5
- package/src/plugins/ibkr/trade-utils.test.ts +1 -1
- package/src/plugins/ibkr/trade-utils.ts +7 -6
- package/src/plugins/ibkr/trading-pane.tsx +8 -7
- package/src/plugins/plugin-runtime.test.tsx +2 -0
- package/src/plugins/plugin-runtime.tsx +10 -0
- package/src/plugins/prediction-markets/controller-data.ts +8 -0
- package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
- package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
- package/src/plugins/prediction-markets/services/fetch.ts +13 -7
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
- package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +2 -0
- package/src/plugins/registry.ts +22 -13
- package/src/sources/provider-router.test.ts +3 -2
- package/src/sources/sec-edgar.ts +13 -1
- package/src/sources/yahoo-finance.ts +75 -1
- package/src/state/app-context-selector.test.tsx +17 -1
- package/src/state/app-context.test.tsx +24 -0
- package/src/state/app-context.tsx +29 -6
- package/src/theme/colors.ts +5 -0
- package/src/theme/themes.ts +25 -0
- package/src/types/config.test.ts +107 -0
- package/src/types/config.ts +30 -1
- package/src/types/data-provider.ts +13 -0
- package/src/types/plugin.ts +35 -1
- package/src/utils/format.test.ts +11 -1
- package/src/utils/format.ts +86 -8
- package/src/utils/inline-content-tokenizer.test.ts +22 -0
- package/src/utils/inline-content-tokenizer.ts +76 -0
- package/src/utils/link-tokenizer.test.ts +27 -0
- package/src/utils/link-tokenizer.ts +93 -0
- package/src/utils/market-format.test.ts +109 -0
- package/src/utils/market-format.ts +299 -0
- package/src/utils/market-status.ts +3 -2
- package/src/utils/selection-clipboard.test.ts +43 -0
- package/src/utils/selection-clipboard.ts +88 -0
- package/src/utils/terminal-input-reset.test.ts +50 -0
- package/src/utils/terminal-input-reset.ts +24 -0
- package/src/utils/throttled-fetch.test.ts +145 -0
- package/src/utils/throttled-fetch.ts +212 -0
- package/src/utils/ticker-tokenizer.test.ts +31 -0
- package/src/utils/ticker-tokenizer.ts +7 -42
- package/src/version.ts +1 -1
|
@@ -9,8 +9,12 @@ import { colors } from "../../theme/colors";
|
|
|
9
9
|
import { Spinner } from "../../components/spinner";
|
|
10
10
|
import { DetailFeedView, type DetailFeedItem } from "../../components/detail-feed-view";
|
|
11
11
|
import { isUsEquityTicker } from "../../utils/sec";
|
|
12
|
+
import { getSharedMarketDataCoordinator } from "../../market-data/coordinator";
|
|
13
|
+
import { parseForm4Xml, transactionTypeLabel } from "./insider/insider-data";
|
|
14
|
+
import { formatCompact, formatCurrency } from "../../utils/format";
|
|
12
15
|
|
|
13
16
|
const SEC_FILING_LIMIT = 50;
|
|
17
|
+
const OWNERSHIP_FORMS = new Set(["3", "4", "5"]);
|
|
14
18
|
|
|
15
19
|
function getDisplayFormLabel(form: string): string {
|
|
16
20
|
const trimmed = form.trim();
|
|
@@ -111,6 +115,64 @@ function buildDetailBody(filing: SecFilingItem): string {
|
|
|
111
115
|
: "No additional SEC filing description is available for this entry.";
|
|
112
116
|
}
|
|
113
117
|
|
|
118
|
+
function buildForm4Preview(content: string | null): string | null {
|
|
119
|
+
if (!content) return null;
|
|
120
|
+
const tx = parseForm4Xml(content);
|
|
121
|
+
if (!tx) return null;
|
|
122
|
+
const type = transactionTypeLabel(tx.transactionType);
|
|
123
|
+
const shares = formatCompact(tx.shares);
|
|
124
|
+
const price = tx.pricePerShare != null ? ` @ ${formatCurrency(tx.pricePerShare)}` : "";
|
|
125
|
+
return `${tx.reportedName} — ${type} ${shares} shares${price}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildForm4Detail(content: string | null, filing: SecFilingItem): string {
|
|
129
|
+
if (!content) return buildDetailBody(filing);
|
|
130
|
+
const tx = parseForm4Xml(content);
|
|
131
|
+
if (!tx) return buildDetailBody(filing);
|
|
132
|
+
|
|
133
|
+
const lines: string[] = [];
|
|
134
|
+
lines.push(`Insider: ${tx.reportedName}`);
|
|
135
|
+
if (tx.title) lines.push(`Title: ${tx.title}`);
|
|
136
|
+
lines.push(`Transaction: ${transactionTypeLabel(tx.transactionType)}`);
|
|
137
|
+
lines.push(`Shares: ${formatCompact(tx.shares)}`);
|
|
138
|
+
if (tx.pricePerShare != null) lines.push(`Price/Share: ${formatCurrency(tx.pricePerShare)}`);
|
|
139
|
+
if (tx.totalValue != null) lines.push(`Total Value: ${formatCurrency(tx.totalValue)}`);
|
|
140
|
+
if (tx.sharesOwned != null) lines.push(`Shares Owned After: ${formatCompact(tx.sharesOwned)}`);
|
|
141
|
+
return lines.join("\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function getFormCategory(form: string): "annual" | "quarterly" | "current" | "ownership" | "other" {
|
|
145
|
+
const f = form.trim().toUpperCase();
|
|
146
|
+
if (f === "10-K" || f === "10-K/A" || f === "20-F") return "annual";
|
|
147
|
+
if (f === "10-Q" || f === "10-Q/A") return "quarterly";
|
|
148
|
+
if (f.startsWith("8-K")) return "current";
|
|
149
|
+
if (OWNERSHIP_FORMS.has(f) || f === "SC 13G" || f === "SC 13D" || f === "SC 13G/A" || f === "SC 13D/A") return "ownership";
|
|
150
|
+
return "other";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function getFormDescription(form: string): string {
|
|
154
|
+
const f = form.trim().toUpperCase();
|
|
155
|
+
switch (f) {
|
|
156
|
+
case "10-K": return "Annual Report";
|
|
157
|
+
case "10-K/A": return "Annual Report (Amended)";
|
|
158
|
+
case "10-Q": return "Quarterly Report";
|
|
159
|
+
case "10-Q/A": return "Quarterly Report (Amended)";
|
|
160
|
+
case "8-K": return "Current Report";
|
|
161
|
+
case "8-K/A": return "Current Report (Amended)";
|
|
162
|
+
case "4": return "Insider Transaction";
|
|
163
|
+
case "3": return "Initial Insider Ownership";
|
|
164
|
+
case "5": return "Annual Insider Ownership";
|
|
165
|
+
case "SC 13G": return "Beneficial Ownership (Passive)";
|
|
166
|
+
case "SC 13G/A": return "Beneficial Ownership (Amended)";
|
|
167
|
+
case "SC 13D": return "Beneficial Ownership (Active)";
|
|
168
|
+
case "SC 13D/A": return "Beneficial Ownership (Amended)";
|
|
169
|
+
case "DEF 14A": return "Proxy Statement";
|
|
170
|
+
case "S-1": return "Registration Statement";
|
|
171
|
+
case "20-F": return "Annual Report (Foreign)";
|
|
172
|
+
default: return "";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
114
176
|
function toFeedItems(
|
|
115
177
|
filings: SecFilingItem[],
|
|
116
178
|
selectedAccessionNumber: string | undefined,
|
|
@@ -119,33 +181,47 @@ function toFeedItems(
|
|
|
119
181
|
): DetailFeedItem[] {
|
|
120
182
|
return filings.map((filing) => {
|
|
121
183
|
const displayTitle = getFilingDisplayTitle(filing);
|
|
184
|
+
const formDesc = getFormDescription(filing.form);
|
|
122
185
|
const hasFetchedContent = contentCache.has(filing.accessionNumber);
|
|
123
186
|
const fetchedContent = contentCache.get(filing.accessionNumber);
|
|
187
|
+
const isOwnership = OWNERSHIP_FORMS.has(filing.form.trim());
|
|
124
188
|
const fallbackBody = hasFetchedContent && !loadingContent && !fetchedContent
|
|
125
189
|
? `${buildDetailBody(filing)}\n\nReadable filing content was not available for this document.`
|
|
126
190
|
: buildDetailBody(filing);
|
|
127
191
|
|
|
192
|
+
// For Form 4s, build structured preview and detail from parsed XML
|
|
193
|
+
const form4Preview = isOwnership && hasFetchedContent
|
|
194
|
+
? buildForm4Preview(fetchedContent ?? null)
|
|
195
|
+
: null;
|
|
196
|
+
const form4Detail = isOwnership && hasFetchedContent
|
|
197
|
+
? buildForm4Detail(fetchedContent ?? null, filing)
|
|
198
|
+
: null;
|
|
199
|
+
|
|
200
|
+
const enrichedTitle = formDesc
|
|
201
|
+
? `${displayTitle} — ${formDesc}`
|
|
202
|
+
: displayTitle;
|
|
203
|
+
|
|
128
204
|
return {
|
|
129
205
|
id: filing.accessionNumber,
|
|
130
206
|
eyebrow: filing.form,
|
|
131
|
-
title: displayTitle,
|
|
207
|
+
title: form4Preview ? `${displayTitle} | ${form4Preview}` : enrichedTitle,
|
|
132
208
|
timestamp: filing.filingDate,
|
|
133
|
-
preview:
|
|
134
|
-
|
|
209
|
+
preview: form4Preview
|
|
210
|
+
?? (filing.items ? `Items ${filing.items}` : (formDesc || filing.primaryDocument)),
|
|
211
|
+
detailTitle: enrichedTitle,
|
|
135
212
|
detailMeta: [
|
|
136
213
|
`Filed ${formatFiledAt(filing)}`,
|
|
137
214
|
`Accession ${filing.accessionNumber}`,
|
|
138
215
|
...(filing.items ? [`Items ${filing.items}`] : []),
|
|
139
|
-
...(filing.primaryDocument ? [`Primary document ${filing.primaryDocument}`] : []),
|
|
140
216
|
],
|
|
141
217
|
detailBody: filing.accessionNumber === selectedAccessionNumber
|
|
142
218
|
? (
|
|
143
219
|
loadingContent
|
|
144
220
|
? "Loading filing content..."
|
|
145
|
-
: fetchedContent ?? fallbackBody
|
|
221
|
+
: form4Detail ?? fetchedContent ?? fallbackBody
|
|
146
222
|
)
|
|
147
|
-
: fallbackBody,
|
|
148
|
-
detailNote:
|
|
223
|
+
: form4Detail ?? fallbackBody,
|
|
224
|
+
detailNote: filing.filingUrl || undefined,
|
|
149
225
|
};
|
|
150
226
|
});
|
|
151
227
|
}
|
|
@@ -200,6 +276,37 @@ function SecTab({ width, height, focused }: DetailTabProps) {
|
|
|
200
276
|
}
|
|
201
277
|
}, [filings.length, selectedIdx, setSelectedIdx]);
|
|
202
278
|
|
|
279
|
+
// Background prefetch Form 4 content for enriched previews
|
|
280
|
+
const prefetchStartedRef = useRef(new Set<string>());
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
if (filings.length === 0) return;
|
|
283
|
+
const coordinator = getSharedMarketDataCoordinator();
|
|
284
|
+
if (!coordinator) return;
|
|
285
|
+
const gen = contentFetchRef.current;
|
|
286
|
+
const form4s = filings.filter((f) =>
|
|
287
|
+
OWNERSHIP_FORMS.has(f.form.trim())
|
|
288
|
+
&& !contentCache.has(f.accessionNumber)
|
|
289
|
+
&& !prefetchStartedRef.current.has(f.accessionNumber),
|
|
290
|
+
);
|
|
291
|
+
if (form4s.length === 0) return;
|
|
292
|
+
|
|
293
|
+
for (const filing of form4s) {
|
|
294
|
+
prefetchStartedRef.current.add(filing.accessionNumber);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
(async () => {
|
|
298
|
+
for (const filing of form4s) {
|
|
299
|
+
if (contentFetchRef.current !== gen) return;
|
|
300
|
+
try {
|
|
301
|
+
const entry = await coordinator.loadSecFilingContent(filing);
|
|
302
|
+
if (contentFetchRef.current !== gen) return;
|
|
303
|
+
const content = entry?.data ?? null;
|
|
304
|
+
setContentCache((prev) => new Map(prev).set(filing.accessionNumber, content));
|
|
305
|
+
} catch { /* skip */ }
|
|
306
|
+
}
|
|
307
|
+
})();
|
|
308
|
+
}, [filings]);
|
|
309
|
+
|
|
203
310
|
if (!ticker) return <text fg={colors.textDim}>Select a ticker to view SEC filings.</text>;
|
|
204
311
|
if (!eligibleTicker) return renderNotice("SEC filings are only shown for US equities.", width);
|
|
205
312
|
if (loading && filings.length === 0) return <Spinner label="Loading SEC filings..." />;
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
3
|
+
import { useKeyboard } from "@opentui/react";
|
|
4
|
+
import { DataTable, type DataTableCell, type DataTableColumn } from "../../../components";
|
|
5
|
+
import type { GloomPlugin, PaneProps } from "../../../types/plugin";
|
|
6
|
+
import { colors, priceColor } from "../../../theme/colors";
|
|
7
|
+
import { formatCurrency, formatPercentRaw } from "../../../utils/format";
|
|
8
|
+
import { usePluginPaneState, usePluginTickerActions } from "../../plugin-runtime";
|
|
9
|
+
import { getSharedDataProvider } from "../../registry";
|
|
10
|
+
import { SECTORS, type SectorDef } from "./sector-data";
|
|
11
|
+
|
|
12
|
+
const REFRESH_INTERVAL_MS = 60_000;
|
|
13
|
+
|
|
14
|
+
interface SectorRow extends SectorDef {
|
|
15
|
+
price: number | null;
|
|
16
|
+
changePercent: number | null;
|
|
17
|
+
currency: string;
|
|
18
|
+
loading: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type SectorColumnId = "name" | "etf" | "price" | "changePercent" | "bar";
|
|
22
|
+
type SectorColumn = DataTableColumn & { id: SectorColumnId };
|
|
23
|
+
type SortDirection = "asc" | "desc";
|
|
24
|
+
|
|
25
|
+
interface SectorSortPreference {
|
|
26
|
+
columnId: SectorColumnId;
|
|
27
|
+
direction: SortDirection;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DEFAULT_SORT_PREFERENCE: SectorSortPreference = {
|
|
31
|
+
columnId: "changePercent",
|
|
32
|
+
direction: "desc",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function buildBar(changePercent: number, barWidth: number): string {
|
|
36
|
+
if (barWidth <= 0) return "";
|
|
37
|
+
const filled = Math.round(Math.abs(changePercent) / 5 * barWidth);
|
|
38
|
+
const clamped = Math.min(filled, barWidth);
|
|
39
|
+
return "━".repeat(clamped);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatTime(date: Date): string {
|
|
43
|
+
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildColumns(width: number): SectorColumn[] {
|
|
47
|
+
const etfWidth = 6;
|
|
48
|
+
const priceWidth = 11;
|
|
49
|
+
const changeWidth = 9;
|
|
50
|
+
const barWidth = Math.max(4, Math.min(24, Math.floor(width * 0.24)));
|
|
51
|
+
const columnCount = 5;
|
|
52
|
+
const fixedWidth = etfWidth + priceWidth + changeWidth + barWidth;
|
|
53
|
+
const nameWidth = Math.max(12, width - 2 - columnCount - fixedWidth);
|
|
54
|
+
|
|
55
|
+
return [
|
|
56
|
+
{ id: "name", label: "SECTOR", width: nameWidth, align: "left" },
|
|
57
|
+
{ id: "etf", label: "ETF", width: etfWidth, align: "left" },
|
|
58
|
+
{ id: "price", label: "LAST", width: priceWidth, align: "right" },
|
|
59
|
+
{ id: "changePercent", label: "CHG%", width: changeWidth, align: "right" },
|
|
60
|
+
{ id: "bar", label: "MOVE", width: barWidth, align: "left" },
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getSortValue(columnId: SectorColumnId, row: SectorRow): string | number | null {
|
|
65
|
+
switch (columnId) {
|
|
66
|
+
case "name":
|
|
67
|
+
return row.name;
|
|
68
|
+
case "etf":
|
|
69
|
+
return row.etf;
|
|
70
|
+
case "price":
|
|
71
|
+
return row.price;
|
|
72
|
+
case "changePercent":
|
|
73
|
+
case "bar":
|
|
74
|
+
return row.changePercent;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function compareSortValues(
|
|
79
|
+
left: string | number | null,
|
|
80
|
+
right: string | number | null,
|
|
81
|
+
direction: SortDirection,
|
|
82
|
+
): number {
|
|
83
|
+
if (left == null && right == null) return 0;
|
|
84
|
+
if (left == null) return 1;
|
|
85
|
+
if (right == null) return -1;
|
|
86
|
+
|
|
87
|
+
const comparison = typeof left === "string" && typeof right === "string"
|
|
88
|
+
? left.localeCompare(right)
|
|
89
|
+
: Number(left) - Number(right);
|
|
90
|
+
return direction === "asc" ? comparison : -comparison;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sortRows(rows: SectorRow[], sortPreference: SectorSortPreference): SectorRow[] {
|
|
94
|
+
return [...rows].sort((left, right) => compareSortValues(
|
|
95
|
+
getSortValue(sortPreference.columnId, left),
|
|
96
|
+
getSortValue(sortPreference.columnId, right),
|
|
97
|
+
sortPreference.direction,
|
|
98
|
+
));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function nextSortPreference(current: SectorSortPreference, columnId: string): SectorSortPreference {
|
|
102
|
+
const typedColumnId = columnId as SectorColumnId;
|
|
103
|
+
if (current.columnId !== typedColumnId) {
|
|
104
|
+
return {
|
|
105
|
+
columnId: typedColumnId,
|
|
106
|
+
direction: typedColumnId === "changePercent" || typedColumnId === "bar" ? "desc" : "asc",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (current.direction === "desc") {
|
|
110
|
+
return { columnId: typedColumnId, direction: "asc" };
|
|
111
|
+
}
|
|
112
|
+
return DEFAULT_SORT_PREFERENCE;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function SectorPerformancePane({ focused, width, height }: PaneProps) {
|
|
116
|
+
const { navigateTicker } = usePluginTickerActions();
|
|
117
|
+
const [rows, setRows] = useState<SectorRow[]>(
|
|
118
|
+
SECTORS.map((sector) => ({ ...sector, price: null, changePercent: null, currency: "USD", loading: true })),
|
|
119
|
+
);
|
|
120
|
+
const [selectedEtf, setSelectedEtf] = usePluginPaneState<string | null>("selectedEtf", null);
|
|
121
|
+
const [sortPreference, setSortPreference] = usePluginPaneState<SectorSortPreference>(
|
|
122
|
+
"sortPreference",
|
|
123
|
+
DEFAULT_SORT_PREFERENCE,
|
|
124
|
+
);
|
|
125
|
+
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
|
|
126
|
+
const [lastRefresh, setLastRefresh] = useState<Date | null>(null);
|
|
127
|
+
|
|
128
|
+
const fetchGenRef = useRef(0);
|
|
129
|
+
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
130
|
+
const headerScrollRef = useRef<ScrollBoxRenderable>(null);
|
|
131
|
+
|
|
132
|
+
const columns = useMemo(() => buildColumns(width), [width]);
|
|
133
|
+
const sortedRows = useMemo(() => sortRows(rows, sortPreference), [rows, sortPreference]);
|
|
134
|
+
const selectedIdx = selectedEtf
|
|
135
|
+
? sortedRows.findIndex((row) => row.etf === selectedEtf)
|
|
136
|
+
: -1;
|
|
137
|
+
const activeIdx = selectedIdx >= 0 ? selectedIdx : (sortedRows.length > 0 ? 0 : -1);
|
|
138
|
+
|
|
139
|
+
const syncHeaderScroll = useCallback(() => {
|
|
140
|
+
const bodyScrollBox = scrollRef.current;
|
|
141
|
+
const headerScrollBox = headerScrollRef.current;
|
|
142
|
+
if (bodyScrollBox && headerScrollBox && headerScrollBox.scrollLeft !== bodyScrollBox.scrollLeft) {
|
|
143
|
+
headerScrollBox.scrollLeft = bodyScrollBox.scrollLeft;
|
|
144
|
+
}
|
|
145
|
+
}, []);
|
|
146
|
+
|
|
147
|
+
const handleBodyScrollActivity = useCallback(() => {
|
|
148
|
+
syncHeaderScroll();
|
|
149
|
+
}, [syncHeaderScroll]);
|
|
150
|
+
|
|
151
|
+
const fetchAll = useCallback(() => {
|
|
152
|
+
fetchGenRef.current += 1;
|
|
153
|
+
const gen = fetchGenRef.current;
|
|
154
|
+
const provider = getSharedDataProvider();
|
|
155
|
+
if (!provider) {
|
|
156
|
+
setRows((prev) => prev.map((row) => ({ ...row, loading: false })));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
setRows((prev) => prev.map((row) => ({ ...row, loading: true })));
|
|
161
|
+
|
|
162
|
+
const fetches = SECTORS.map(async (sector) => {
|
|
163
|
+
try {
|
|
164
|
+
const quote = await provider.getQuote(sector.etf, "");
|
|
165
|
+
if (fetchGenRef.current !== gen) return;
|
|
166
|
+
setRows((prev) =>
|
|
167
|
+
prev.map((row) =>
|
|
168
|
+
row.etf === sector.etf
|
|
169
|
+
? {
|
|
170
|
+
...row,
|
|
171
|
+
price: quote?.price ?? null,
|
|
172
|
+
changePercent: quote?.changePercent ?? null,
|
|
173
|
+
currency: quote?.currency ?? "USD",
|
|
174
|
+
loading: false,
|
|
175
|
+
}
|
|
176
|
+
: row,
|
|
177
|
+
),
|
|
178
|
+
);
|
|
179
|
+
} catch {
|
|
180
|
+
if (fetchGenRef.current !== gen) return;
|
|
181
|
+
setRows((prev) =>
|
|
182
|
+
prev.map((row) =>
|
|
183
|
+
row.etf === sector.etf ? { ...row, loading: false } : row,
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
Promise.allSettled(fetches).then(() => {
|
|
190
|
+
if (fetchGenRef.current === gen) {
|
|
191
|
+
setLastRefresh(new Date());
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}, []);
|
|
195
|
+
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
fetchAll();
|
|
198
|
+
const interval = setInterval(fetchAll, REFRESH_INTERVAL_MS);
|
|
199
|
+
return () => clearInterval(interval);
|
|
200
|
+
}, [fetchAll]);
|
|
201
|
+
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
if (selectedEtf && sortedRows.some((row) => row.etf === selectedEtf)) return;
|
|
204
|
+
const firstRow = sortedRows[0];
|
|
205
|
+
if (firstRow && selectedEtf !== firstRow.etf) {
|
|
206
|
+
setSelectedEtf(firstRow.etf);
|
|
207
|
+
}
|
|
208
|
+
}, [selectedEtf, setSelectedEtf, sortedRows]);
|
|
209
|
+
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
const scrollBox = scrollRef.current;
|
|
212
|
+
if (!scrollBox?.viewport || activeIdx < 0) return;
|
|
213
|
+
const viewportHeight = Math.max(scrollBox.viewport.height, 1);
|
|
214
|
+
if (activeIdx < scrollBox.scrollTop) {
|
|
215
|
+
scrollBox.scrollTo(activeIdx);
|
|
216
|
+
} else if (activeIdx >= scrollBox.scrollTop + viewportHeight) {
|
|
217
|
+
scrollBox.scrollTo(activeIdx - viewportHeight + 1);
|
|
218
|
+
}
|
|
219
|
+
}, [activeIdx, sortedRows.length]);
|
|
220
|
+
|
|
221
|
+
const openRow = useCallback((row: SectorRow) => {
|
|
222
|
+
navigateTicker(row.etf);
|
|
223
|
+
}, [navigateTicker]);
|
|
224
|
+
|
|
225
|
+
const handleHeaderClick = useCallback((columnId: string) => {
|
|
226
|
+
setSortPreference((current) => nextSortPreference(current, columnId));
|
|
227
|
+
}, [setSortPreference]);
|
|
228
|
+
|
|
229
|
+
const selectIndex = useCallback((index: number) => {
|
|
230
|
+
const row = sortedRows[index];
|
|
231
|
+
if (row) setSelectedEtf(row.etf);
|
|
232
|
+
}, [setSelectedEtf, sortedRows]);
|
|
233
|
+
|
|
234
|
+
const selectRow = useCallback((row: SectorRow) => {
|
|
235
|
+
if (row.etf === selectedEtf) {
|
|
236
|
+
openRow(row);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
setSelectedEtf(row.etf);
|
|
240
|
+
}, [openRow, selectedEtf, setSelectedEtf]);
|
|
241
|
+
|
|
242
|
+
useKeyboard((event) => {
|
|
243
|
+
if (!focused) return;
|
|
244
|
+
|
|
245
|
+
if (event.name === "j" || event.name === "down") {
|
|
246
|
+
selectIndex(Math.min(activeIdx + 1, sortedRows.length - 1));
|
|
247
|
+
} else if (event.name === "k" || event.name === "up") {
|
|
248
|
+
selectIndex(Math.max(activeIdx - 1, 0));
|
|
249
|
+
} else if (event.name === "return") {
|
|
250
|
+
const row = sortedRows[activeIdx];
|
|
251
|
+
if (row) openRow(row);
|
|
252
|
+
} else if (event.name === "r") {
|
|
253
|
+
fetchAll();
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const renderCell = useCallback((
|
|
258
|
+
row: SectorRow,
|
|
259
|
+
column: SectorColumn,
|
|
260
|
+
_index: number,
|
|
261
|
+
rowState: { selected: boolean },
|
|
262
|
+
): DataTableCell => {
|
|
263
|
+
const selectedColor = rowState.selected ? colors.selectedText : undefined;
|
|
264
|
+
switch (column.id) {
|
|
265
|
+
case "name":
|
|
266
|
+
return { text: row.name, color: selectedColor ?? colors.text };
|
|
267
|
+
case "etf":
|
|
268
|
+
return { text: row.etf, color: selectedColor ?? colors.textDim };
|
|
269
|
+
case "price":
|
|
270
|
+
if (row.loading && row.price === null) {
|
|
271
|
+
return { text: "…", color: selectedColor ?? colors.textDim };
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
text: row.price !== null ? formatCurrency(row.price, row.currency) : "—",
|
|
275
|
+
color: selectedColor ?? (row.price !== null ? colors.text : colors.textDim),
|
|
276
|
+
};
|
|
277
|
+
case "changePercent":
|
|
278
|
+
return {
|
|
279
|
+
text: row.changePercent !== null ? formatPercentRaw(row.changePercent) : "—",
|
|
280
|
+
color: selectedColor ?? (row.changePercent !== null ? priceColor(row.changePercent) : colors.textDim),
|
|
281
|
+
};
|
|
282
|
+
case "bar": {
|
|
283
|
+
const bar = row.changePercent !== null ? buildBar(row.changePercent, column.width) : "";
|
|
284
|
+
const barColor = row.changePercent !== null && row.changePercent >= 0 ? colors.positive : colors.negative;
|
|
285
|
+
return {
|
|
286
|
+
text: bar,
|
|
287
|
+
color: selectedColor ?? (bar ? barColor : colors.textDim),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}, []);
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
<box flexDirection="column" width={width} height={height}>
|
|
295
|
+
<box flexDirection="row" height={1} paddingX={1}>
|
|
296
|
+
{lastRefresh ? (
|
|
297
|
+
<text fg={colors.textMuted}>{formatTime(lastRefresh)}</text>
|
|
298
|
+
) : (
|
|
299
|
+
<text fg={colors.textMuted}>loading…</text>
|
|
300
|
+
)}
|
|
301
|
+
</box>
|
|
302
|
+
|
|
303
|
+
<DataTable<SectorRow, SectorColumn>
|
|
304
|
+
columns={columns}
|
|
305
|
+
items={sortedRows}
|
|
306
|
+
sortColumnId={sortPreference.columnId}
|
|
307
|
+
sortDirection={sortPreference.direction}
|
|
308
|
+
onHeaderClick={handleHeaderClick}
|
|
309
|
+
headerScrollRef={headerScrollRef}
|
|
310
|
+
scrollRef={scrollRef}
|
|
311
|
+
syncHeaderScroll={syncHeaderScroll}
|
|
312
|
+
onBodyScrollActivity={handleBodyScrollActivity}
|
|
313
|
+
hoveredIdx={hoveredIdx}
|
|
314
|
+
setHoveredIdx={setHoveredIdx}
|
|
315
|
+
getItemKey={(row) => row.etf}
|
|
316
|
+
isSelected={(row) => row.etf === selectedEtf}
|
|
317
|
+
onSelect={selectRow}
|
|
318
|
+
renderCell={renderCell}
|
|
319
|
+
emptyStateTitle="No sector data available"
|
|
320
|
+
showHorizontalScrollbar={false}
|
|
321
|
+
/>
|
|
322
|
+
|
|
323
|
+
<box height={1} paddingX={1}>
|
|
324
|
+
<text fg={colors.textMuted}>[r]efresh</text>
|
|
325
|
+
</box>
|
|
326
|
+
</box>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export const sectorsPlugin: GloomPlugin = {
|
|
331
|
+
id: "sectors",
|
|
332
|
+
name: "Sector Performance",
|
|
333
|
+
version: "1.0.0",
|
|
334
|
+
description: "S&P 500 sector performance via sector ETF proxies",
|
|
335
|
+
toggleable: true,
|
|
336
|
+
|
|
337
|
+
panes: [
|
|
338
|
+
{
|
|
339
|
+
id: "sectors",
|
|
340
|
+
name: "Sector Performance",
|
|
341
|
+
icon: "S",
|
|
342
|
+
component: SectorPerformancePane,
|
|
343
|
+
defaultPosition: "right",
|
|
344
|
+
defaultMode: "floating",
|
|
345
|
+
defaultFloatingSize: { width: 75, height: 16 },
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
|
|
349
|
+
paneTemplates: [
|
|
350
|
+
{
|
|
351
|
+
id: "sectors-pane",
|
|
352
|
+
paneId: "sectors",
|
|
353
|
+
label: "Sector Performance",
|
|
354
|
+
description: "S&P 500 sector performance sorted by daily change.",
|
|
355
|
+
keywords: ["sector", "sectors", "etf", "xlk", "xlv", "xlf", "performance", "spdr"],
|
|
356
|
+
shortcut: { prefix: "BI" },
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface SectorDef {
|
|
2
|
+
name: string;
|
|
3
|
+
etf: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export const SECTORS: SectorDef[] = [
|
|
7
|
+
{ name: "Technology", etf: "XLK" },
|
|
8
|
+
{ name: "Healthcare", etf: "XLV" },
|
|
9
|
+
{ name: "Financials", etf: "XLF" },
|
|
10
|
+
{ name: "Consumer Disc.", etf: "XLY" },
|
|
11
|
+
{ name: "Communication", etf: "XLC" },
|
|
12
|
+
{ name: "Industrials", etf: "XLI" },
|
|
13
|
+
{ name: "Consumer Staples", etf: "XLP" },
|
|
14
|
+
{ name: "Energy", etf: "XLE" },
|
|
15
|
+
{ name: "Utilities", etf: "XLU" },
|
|
16
|
+
{ name: "Real Estate", etf: "XLRE" },
|
|
17
|
+
{ name: "Materials", etf: "XLB" },
|
|
18
|
+
];
|
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
import { useTerminalDimensions } from "@opentui/react";
|
|
2
|
+
import { useCallback, useMemo } from "react";
|
|
3
|
+
import { saveConfig } from "../../../data/config-store";
|
|
4
|
+
import { ChartIndicatorSelector } from "../../../components/chart/chart-indicator-selector";
|
|
5
|
+
import {
|
|
6
|
+
buildIndicatorConfigFromSelection,
|
|
7
|
+
CHART_INDICATORS_PLUGIN_CONFIG_VERSION_KEY,
|
|
8
|
+
CURRENT_CHART_INDICATORS_CONFIG_VERSION,
|
|
9
|
+
CHART_INDICATORS_PLUGIN_CONFIG_KEY,
|
|
10
|
+
resolveChartIndicatorSelection,
|
|
11
|
+
normalizeChartIndicatorSelection,
|
|
12
|
+
type ChartIndicatorId,
|
|
13
|
+
} from "../../../components/chart/indicators/options";
|
|
2
14
|
import type { ChartAxisMode } from "../../../components/chart/chart-types";
|
|
3
15
|
import { ResolvedStockChart } from "../../../components/chart/stock-chart";
|
|
16
|
+
import { useAppState } from "../../../state/app-context";
|
|
4
17
|
import type { TickerFinancials } from "../../../types/financials";
|
|
5
18
|
import type { TickerRecord } from "../../../types/ticker";
|
|
19
|
+
import { getSharedRegistry } from "../../registry";
|
|
20
|
+
|
|
21
|
+
const TICKER_DETAIL_PLUGIN_ID = "ticker-detail";
|
|
6
22
|
|
|
7
23
|
export function ChartTab({
|
|
8
24
|
width,
|
|
@@ -26,29 +42,75 @@ export function ChartTab({
|
|
|
26
42
|
financials: TickerFinancials | null;
|
|
27
43
|
}) {
|
|
28
44
|
const { width: termWidth, height: termHeight } = useTerminalDimensions();
|
|
45
|
+
const { state, dispatch } = useAppState();
|
|
29
46
|
|
|
30
47
|
const chartWidth = Math.max((width || Math.floor(termWidth * 0.55)) - 2, 30);
|
|
31
48
|
const chartHeight = Math.max((height || termHeight - 8) - 2, 10);
|
|
49
|
+
const rawIndicatorSelection = state.config.pluginConfig[TICKER_DETAIL_PLUGIN_ID]?.[CHART_INDICATORS_PLUGIN_CONFIG_KEY];
|
|
50
|
+
const indicatorSelectionVersion = state.config.pluginConfig[TICKER_DETAIL_PLUGIN_ID]?.[CHART_INDICATORS_PLUGIN_CONFIG_VERSION_KEY];
|
|
51
|
+
const hasStoredIndicatorSelection = Array.isArray(rawIndicatorSelection);
|
|
52
|
+
const selectedIndicatorIds = useMemo(
|
|
53
|
+
() => resolveChartIndicatorSelection(rawIndicatorSelection, indicatorSelectionVersion),
|
|
54
|
+
[indicatorSelectionVersion, rawIndicatorSelection],
|
|
55
|
+
);
|
|
56
|
+
const selectedIndicatorConfig = useMemo(
|
|
57
|
+
() => buildIndicatorConfigFromSelection(selectedIndicatorIds),
|
|
58
|
+
[selectedIndicatorIds],
|
|
59
|
+
);
|
|
60
|
+
const showVolume = selectedIndicatorIds.includes("volume");
|
|
61
|
+
const persistIndicatorSelection = useCallback((nextSelection: ChartIndicatorId[]) => {
|
|
62
|
+
const normalized = normalizeChartIndicatorSelection(nextSelection);
|
|
63
|
+
const nextConfig = {
|
|
64
|
+
...state.config,
|
|
65
|
+
pluginConfig: {
|
|
66
|
+
...state.config.pluginConfig,
|
|
67
|
+
[TICKER_DETAIL_PLUGIN_ID]: {
|
|
68
|
+
...(state.config.pluginConfig[TICKER_DETAIL_PLUGIN_ID] ?? {}),
|
|
69
|
+
[CHART_INDICATORS_PLUGIN_CONFIG_KEY]: normalized,
|
|
70
|
+
[CHART_INDICATORS_PLUGIN_CONFIG_VERSION_KEY]: CURRENT_CHART_INDICATORS_CONFIG_VERSION,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
dispatch({ type: "SET_CONFIG", config: nextConfig });
|
|
75
|
+
saveConfig(nextConfig).catch(() => {});
|
|
76
|
+
getSharedRegistry()?.events.emit("config:changed", { config: nextConfig });
|
|
77
|
+
}, [dispatch, state.config]);
|
|
32
78
|
|
|
33
79
|
return (
|
|
34
80
|
<box
|
|
35
81
|
flexDirection="column"
|
|
36
82
|
paddingX={1}
|
|
37
83
|
flexGrow={1}
|
|
38
|
-
onMouseDown={() => {
|
|
39
|
-
if (!interactive) onActivate?.();
|
|
40
|
-
}}
|
|
41
84
|
>
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
85
|
+
<box
|
|
86
|
+
flexDirection="column"
|
|
87
|
+
flexGrow={1}
|
|
88
|
+
onMouseDown={() => {
|
|
89
|
+
if (!interactive) onActivate?.();
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<ResolvedStockChart
|
|
93
|
+
width={chartWidth}
|
|
94
|
+
height={chartHeight}
|
|
95
|
+
focused={focused}
|
|
96
|
+
interactive={interactive}
|
|
97
|
+
axisMode={axisMode}
|
|
98
|
+
symbol={symbol}
|
|
99
|
+
ticker={ticker}
|
|
100
|
+
financials={financials}
|
|
101
|
+
indicatorConfig={hasStoredIndicatorSelection ? selectedIndicatorConfig : undefined}
|
|
102
|
+
showVolume={showVolume}
|
|
103
|
+
footerControls={(
|
|
104
|
+
<ChartIndicatorSelector
|
|
105
|
+
width={chartWidth}
|
|
106
|
+
selectedIds={selectedIndicatorIds}
|
|
107
|
+
onChange={persistIndicatorSelection}
|
|
108
|
+
variant="hint"
|
|
109
|
+
shortcutActive={focused}
|
|
110
|
+
/>
|
|
111
|
+
)}
|
|
112
|
+
/>
|
|
113
|
+
</box>
|
|
52
114
|
</box>
|
|
53
115
|
);
|
|
54
116
|
}
|