gloomberb 0.4.0 → 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.
Files changed (218) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/app.tsx +184 -69
  4. package/src/cli/commands/ticker.ts +45 -15
  5. package/src/cli/helpers.ts +4 -2
  6. package/src/cli/registry.test.ts +42 -35
  7. package/src/cli.test.ts +4 -2
  8. package/src/components/chart/chart-control-hint.tsx +39 -0
  9. package/src/components/chart/chart-controller.test.ts +110 -0
  10. package/src/components/chart/chart-controller.ts +364 -2
  11. package/src/components/chart/chart-data.ts +177 -29
  12. package/src/components/chart/chart-indicator-selector.tsx +53 -0
  13. package/src/components/chart/chart-pane-settings.ts +20 -0
  14. package/src/components/chart/chart-renderer.ts +220 -30
  15. package/src/components/chart/chart-resolution.test.ts +67 -0
  16. package/src/components/chart/chart-resolution.ts +74 -4
  17. package/src/components/chart/chart-scroll.test.ts +69 -0
  18. package/src/components/chart/chart-scroll.ts +104 -0
  19. package/src/components/chart/chart-types.ts +12 -0
  20. package/src/components/chart/chart-viewport.test.ts +5 -1
  21. package/src/components/chart/chart-viewport.ts +1 -1
  22. package/src/components/chart/chart.test.ts +249 -10
  23. package/src/components/chart/comparison-chart-renderer.test.ts +71 -1
  24. package/src/components/chart/comparison-chart-renderer.ts +40 -3
  25. package/src/components/chart/comparison-stock-chart.tsx +218 -123
  26. package/src/components/chart/indicators/bands.test.ts +50 -0
  27. package/src/components/chart/indicators/bands.ts +29 -0
  28. package/src/components/chart/indicators/moving-averages.test.ts +54 -0
  29. package/src/components/chart/indicators/moving-averages.ts +42 -0
  30. package/src/components/chart/indicators/options.test.ts +32 -0
  31. package/src/components/chart/indicators/options.ts +107 -0
  32. package/src/components/chart/indicators/oscillators.test.ts +79 -0
  33. package/src/components/chart/indicators/oscillators.ts +92 -0
  34. package/src/components/chart/indicators/types.ts +36 -0
  35. package/src/components/chart/native/chart-rasterizer.ts +51 -0
  36. package/src/components/chart/native/kitty.test.ts +68 -0
  37. package/src/components/chart/stock-chart-renderer-switch.test.tsx +740 -9
  38. package/src/components/chart/stock-chart.test.ts +239 -0
  39. package/src/components/chart/stock-chart.tsx +2257 -372
  40. package/src/components/command-bar/command-bar.test.tsx +337 -5
  41. package/src/components/command-bar/command-bar.tsx +296 -120
  42. package/src/components/command-bar/helpers.ts +1 -1
  43. package/src/components/command-bar/root-shortcuts.ts +33 -4
  44. package/src/components/command-bar/view-model.test.ts +3 -3
  45. package/src/components/command-bar/view-model.ts +4 -4
  46. package/src/components/command-bar/workflow-ops.ts +10 -0
  47. package/src/components/layout/floating-pane.tsx +3 -1
  48. package/src/components/layout/header.tsx +5 -4
  49. package/src/components/layout/pane-sizing.ts +6 -0
  50. package/src/components/layout/pane.tsx +5 -1
  51. package/src/components/layout/shell.test.tsx +263 -32
  52. package/src/components/layout/shell.tsx +10 -6
  53. package/src/components/layout/status-bar.test.tsx +16 -7
  54. package/src/components/layout/status-bar.tsx +4 -1
  55. package/src/components/markdown-editor.tsx +1 -0
  56. package/src/components/pane-settings-dialog.tsx +13 -152
  57. package/src/components/pane-template-wizard.tsx +1 -0
  58. package/src/components/price-selector-dialog.tsx +12 -7
  59. package/src/components/ticker-badge-text.test.tsx +50 -0
  60. package/src/components/ticker-badge-text.tsx +17 -2
  61. package/src/components/ticker-badge.tsx +2 -2
  62. package/src/components/ticker-list-table.tsx +62 -2
  63. package/src/components/toggle-list.tsx +15 -2
  64. package/src/components/ui/data-table.tsx +82 -5
  65. package/src/components/ui/external-link.tsx +48 -11
  66. package/src/components/ui/fields.tsx +2 -1
  67. package/src/components/ui/index.ts +8 -1
  68. package/src/components/ui/multi-select-chips.tsx +69 -0
  69. package/src/components/ui/multi-select-dialog.tsx +267 -0
  70. package/src/components/ui/multi-select.ts +78 -0
  71. package/src/components/ui/ui.test.tsx +343 -1
  72. package/src/data/plugin-state-store.ts +30 -19
  73. package/src/data/resource-store.ts +133 -103
  74. package/src/data/session-store.ts +19 -12
  75. package/src/data/sqlite-cache.test.ts +37 -2
  76. package/src/data/sqlite-database.ts +8 -2
  77. package/src/data/sqlite-retry.test.ts +63 -0
  78. package/src/data/sqlite-retry.ts +66 -0
  79. package/src/data/ticker-store.ts +29 -18
  80. package/src/index.tsx +6 -1
  81. package/src/market-data/coordinator.test.ts +114 -5
  82. package/src/market-data/coordinator.ts +70 -1
  83. package/src/market-data/hooks.test.tsx +73 -2
  84. package/src/market-data/hooks.tsx +16 -4
  85. package/src/notifications/app-notifier.test.ts +177 -0
  86. package/src/notifications/app-notifier.ts +241 -0
  87. package/src/pane-settings.test.ts +2 -0
  88. package/src/plugins/builtin/ai/index.test.tsx +1 -1
  89. package/src/plugins/builtin/ai/screener-pane.test.tsx +2 -0
  90. package/src/plugins/builtin/ai/screener-pane.tsx +1 -0
  91. package/src/plugins/builtin/ai/ticker-context.ts +3 -2
  92. package/src/plugins/builtin/alerts/alert-engine.test.ts +90 -0
  93. package/src/plugins/builtin/alerts/alert-engine.ts +55 -0
  94. package/src/plugins/builtin/alerts/index.test.tsx +357 -0
  95. package/src/plugins/builtin/alerts/index.tsx +519 -0
  96. package/src/plugins/builtin/alerts/types.ts +14 -0
  97. package/src/plugins/builtin/analytics/index.test.tsx +300 -0
  98. package/src/plugins/builtin/analytics/index.tsx +775 -0
  99. package/src/plugins/builtin/analytics/metrics.test.ts +125 -0
  100. package/src/plugins/builtin/analytics/metrics.ts +155 -0
  101. package/src/plugins/builtin/ask-ai.test.tsx +2 -0
  102. package/src/plugins/builtin/chat-controller.test.ts +92 -18
  103. package/src/plugins/builtin/chat-controller.ts +40 -11
  104. package/src/plugins/builtin/chat.test.tsx +715 -4
  105. package/src/plugins/builtin/chat.tsx +420 -119
  106. package/src/plugins/builtin/comparison-chart.test.tsx +97 -57
  107. package/src/plugins/builtin/comparison-chart.tsx +1 -2
  108. package/src/plugins/builtin/correlation/compute.test.ts +147 -0
  109. package/src/plugins/builtin/correlation/compute.ts +115 -0
  110. package/src/plugins/builtin/correlation/index.tsx +385 -0
  111. package/src/plugins/builtin/correlation/plugin.test.ts +44 -0
  112. package/src/plugins/builtin/correlation/settings.test.ts +34 -0
  113. package/src/plugins/builtin/correlation/settings.ts +95 -0
  114. package/src/plugins/builtin/debug.tsx +36 -50
  115. package/src/plugins/builtin/earnings/earnings-cache.test.ts +181 -0
  116. package/src/plugins/builtin/earnings/earnings-cache.ts +136 -0
  117. package/src/plugins/builtin/earnings/earnings-data.test.ts +59 -0
  118. package/src/plugins/builtin/earnings/earnings-data.ts +50 -0
  119. package/src/plugins/builtin/earnings/index.tsx +337 -0
  120. package/src/plugins/builtin/econ/calendar-source.test.ts +219 -0
  121. package/src/plugins/builtin/econ/calendar-source.ts +121 -0
  122. package/src/plugins/builtin/econ/fred-client.test.ts +63 -0
  123. package/src/plugins/builtin/econ/fred-client.ts +79 -0
  124. package/src/plugins/builtin/econ/fred-series-map.ts +88 -0
  125. package/src/plugins/builtin/econ/index.tsx +999 -0
  126. package/src/plugins/builtin/econ/types.ts +13 -0
  127. package/src/plugins/builtin/fred-settings.ts +10 -0
  128. package/src/plugins/builtin/fx-matrix/index.tsx +178 -0
  129. package/src/plugins/builtin/fx-matrix/pairs.ts +30 -0
  130. package/src/plugins/builtin/help.test.tsx +1 -5
  131. package/src/plugins/builtin/help.tsx +1 -16
  132. package/src/plugins/builtin/insider/index.tsx +417 -0
  133. package/src/plugins/builtin/insider/insider-data.test.ts +55 -0
  134. package/src/plugins/builtin/insider/insider-data.ts +49 -0
  135. package/src/plugins/builtin/layout-manager.tsx +33 -16
  136. package/src/plugins/builtin/market-movers/index.tsx +573 -0
  137. package/src/plugins/builtin/market-movers/screener.test.ts +143 -0
  138. package/src/plugins/builtin/market-movers/screener.ts +105 -0
  139. package/src/plugins/builtin/options.tsx +4 -3
  140. package/src/plugins/builtin/portfolio-list/cli/commands.ts +11 -6
  141. package/src/plugins/builtin/portfolio-list/command-bar.ts +6 -3
  142. package/src/plugins/builtin/portfolio-list/header.tsx +3 -2
  143. package/src/plugins/builtin/portfolio-list/metrics.ts +22 -11
  144. package/src/plugins/builtin/portfolio-list/pane.tsx +3 -9
  145. package/src/plugins/builtin/portfolio-list/settings.ts +9 -0
  146. package/src/plugins/builtin/portfolio-list/table.tsx +3 -0
  147. package/src/plugins/builtin/portfolio-list.test.tsx +3 -1
  148. package/src/plugins/builtin/portfolio-metrics.test.ts +13 -1
  149. package/src/plugins/builtin/sec.tsx +114 -7
  150. package/src/plugins/builtin/sectors/index.tsx +359 -0
  151. package/src/plugins/builtin/sectors/sector-data.ts +18 -0
  152. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +75 -13
  153. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +316 -80
  154. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +4 -6
  155. package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +17 -12
  156. package/src/plugins/builtin/ticker-detail.test.tsx +3 -3
  157. package/src/plugins/builtin/world-indices/index.tsx +410 -0
  158. package/src/plugins/builtin/world-indices/indices.ts +52 -0
  159. package/src/plugins/builtin/yield-curve/index.tsx +242 -0
  160. package/src/plugins/builtin/yield-curve/treasury-data.test.ts +212 -0
  161. package/src/plugins/builtin/yield-curve/treasury-data.ts +107 -0
  162. package/src/plugins/catalog.ts +22 -0
  163. package/src/plugins/gridlock-notification.ts +15 -0
  164. package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +5 -5
  165. package/src/plugins/ibkr/gateway-service.test.ts +22 -0
  166. package/src/plugins/ibkr/gateway-service.ts +6 -0
  167. package/src/plugins/ibkr/index.test.tsx +1 -1
  168. package/src/plugins/ibkr/index.tsx +8 -2
  169. package/src/plugins/ibkr/trade-tab.tsx +23 -5
  170. package/src/plugins/ibkr/trade-utils.test.ts +1 -1
  171. package/src/plugins/ibkr/trade-utils.ts +7 -6
  172. package/src/plugins/ibkr/trading-pane.tsx +8 -7
  173. package/src/plugins/plugin-runtime.test.tsx +2 -0
  174. package/src/plugins/plugin-runtime.tsx +10 -0
  175. package/src/plugins/prediction-markets/controller-data.ts +129 -14
  176. package/src/plugins/prediction-markets/controller.ts +1 -1
  177. package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
  178. package/src/plugins/prediction-markets/index.tsx +5 -1
  179. package/src/plugins/prediction-markets/pane.test.tsx +78 -0
  180. package/src/plugins/prediction-markets/pane.tsx +7 -3
  181. package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
  182. package/src/plugins/prediction-markets/services/fetch.ts +17 -7
  183. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  184. package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
  185. package/src/plugins/prediction-markets/test-helpers.tsx +22 -5
  186. package/src/plugins/registry.ts +45 -13
  187. package/src/sources/provider-router.test.ts +3 -2
  188. package/src/sources/sec-edgar.ts +13 -1
  189. package/src/sources/yahoo-finance.ts +75 -1
  190. package/src/state/app-context-selector.test.tsx +17 -1
  191. package/src/state/app-context.test.tsx +24 -0
  192. package/src/state/app-context.tsx +29 -6
  193. package/src/theme/colors.ts +5 -0
  194. package/src/theme/themes.ts +25 -0
  195. package/src/types/config.test.ts +107 -0
  196. package/src/types/config.ts +30 -1
  197. package/src/types/data-provider.ts +13 -0
  198. package/src/types/plugin.ts +35 -1
  199. package/src/updater.test.ts +2 -0
  200. package/src/utils/api-client.ts +12 -0
  201. package/src/utils/format.test.ts +11 -1
  202. package/src/utils/format.ts +86 -8
  203. package/src/utils/inline-content-tokenizer.test.ts +22 -0
  204. package/src/utils/inline-content-tokenizer.ts +76 -0
  205. package/src/utils/link-tokenizer.test.ts +27 -0
  206. package/src/utils/link-tokenizer.ts +93 -0
  207. package/src/utils/market-format.test.ts +109 -0
  208. package/src/utils/market-format.ts +299 -0
  209. package/src/utils/market-status.ts +3 -2
  210. package/src/utils/selection-clipboard.test.ts +43 -0
  211. package/src/utils/selection-clipboard.ts +88 -0
  212. package/src/utils/terminal-input-reset.test.ts +50 -0
  213. package/src/utils/terminal-input-reset.ts +24 -0
  214. package/src/utils/throttled-fetch.test.ts +145 -0
  215. package/src/utils/throttled-fetch.ts +212 -0
  216. package/src/utils/ticker-tokenizer.test.ts +31 -0
  217. package/src/utils/ticker-tokenizer.ts +7 -42
  218. package/src/version.ts +1 -1
@@ -0,0 +1,385 @@
1
+ import { useMemo } from "react";
2
+ import { TextAttributes } from "@opentui/core";
3
+ import type { GloomPlugin, PaneProps } from "../../../types/plugin";
4
+ import { colors } from "../../../theme/colors";
5
+ import { getSharedRegistry } from "../../registry";
6
+ import { useAppSelector } from "../../../state/app-context";
7
+ import { useChartQueries } from "../../../market-data/hooks";
8
+ import { buildChartKey } from "../../../market-data/selectors";
9
+ import type { PricePoint } from "../../../types/financials";
10
+ import type { QueryEntry } from "../../../market-data/result-types";
11
+ import { formatTickerListInput } from "../../../utils/ticker-list";
12
+ import {
13
+ computeDatedReturns,
14
+ correlateDatedReturns,
15
+ formatCorrelation,
16
+ correlationColor,
17
+ type CorrelationResult,
18
+ type DatedReturn,
19
+ } from "./compute";
20
+ import {
21
+ DEFAULT_CORRELATION_SYMBOLS,
22
+ MAX_CORRELATION_TICKERS,
23
+ buildCorrelationSettingsDef,
24
+ getCorrelationPaneSettings,
25
+ type CorrelationRangePreset,
26
+ } from "./settings";
27
+
28
+ const ROW_HEADER_WIDTH = 7;
29
+ const MATRIX_CELL_WIDTH = 10;
30
+ const MIN_MATRIX_CELL_WIDTH = 7;
31
+ const MIN_CORRELATION_OBSERVATIONS = 5;
32
+
33
+ type SeriesStatus = "loading" | "ready" | "insufficient" | "empty" | "error";
34
+
35
+ interface CorrelationInstrument {
36
+ symbol: string;
37
+ exchange: string;
38
+ }
39
+
40
+ interface CorrelationSeries {
41
+ symbol: string;
42
+ returns: DatedReturn[];
43
+ status: SeriesStatus;
44
+ observationCount: number;
45
+ }
46
+
47
+ function displaySymbol(symbol: string): string {
48
+ return symbol.length > 5 ? symbol.slice(0, 5) : symbol;
49
+ }
50
+
51
+ function pairKey(left: string, right: string): string {
52
+ return `${left}\u0000${right}`;
53
+ }
54
+
55
+ function formatSymbolList(symbols: string[]): string {
56
+ if (symbols.length <= 3) return symbols.join(", ");
57
+ return `${symbols.slice(0, 3).join(", ")} +${symbols.length - 3}`;
58
+ }
59
+
60
+ function formatSeriesSymbolList(symbols: string[], seriesBySymbol: Map<string, CorrelationSeries>, includeCounts = false): string {
61
+ return formatSymbolList(symbols.map((symbol) => {
62
+ const series = seriesBySymbol.get(symbol);
63
+ return includeCounts && series ? `${symbol}(${series.observationCount})` : symbol;
64
+ }));
65
+ }
66
+
67
+ function getSeriesForEntry(
68
+ symbol: string,
69
+ entry: QueryEntry<PricePoint[]> | undefined,
70
+ ): CorrelationSeries {
71
+ const priceHistory = entry?.data ?? entry?.lastGoodData ?? null;
72
+
73
+ if (!priceHistory || priceHistory.length === 0) {
74
+ if (entry?.error?.reasonCode === "NO_DATA") {
75
+ return { symbol, returns: [], status: "empty", observationCount: 0 };
76
+ }
77
+ if (entry?.phase === "error" || entry?.error) {
78
+ return {
79
+ symbol,
80
+ returns: [],
81
+ status: "error",
82
+ observationCount: 0,
83
+ };
84
+ }
85
+ return { symbol, returns: [], status: "loading", observationCount: 0 };
86
+ }
87
+
88
+ const returns = computeDatedReturns(priceHistory);
89
+ if (returns.length < MIN_CORRELATION_OBSERVATIONS) {
90
+ return {
91
+ symbol,
92
+ returns,
93
+ status: "insufficient",
94
+ observationCount: returns.length,
95
+ };
96
+ }
97
+
98
+ return { symbol, returns, status: "ready", observationCount: returns.length };
99
+ }
100
+
101
+ function rowHeaderColor(status: SeriesStatus): string {
102
+ switch (status) {
103
+ case "loading":
104
+ return colors.textDim;
105
+ case "error":
106
+ case "empty":
107
+ return colors.negative;
108
+ case "insufficient":
109
+ return colors.textMuted;
110
+ case "ready":
111
+ return colors.textBright;
112
+ }
113
+ }
114
+
115
+ function buildStatusSummary(
116
+ symbols: string[],
117
+ seriesBySymbol: Map<string, CorrelationSeries>,
118
+ sampleMin: number | null,
119
+ sampleMax: number | null,
120
+ ): string {
121
+ const parts: string[] = [];
122
+ const byStatus = (status: SeriesStatus) => symbols.filter((symbol) => seriesBySymbol.get(symbol)?.status === status);
123
+
124
+ const loading = byStatus("loading");
125
+ const errors = [...byStatus("error"), ...byStatus("empty")];
126
+ const insufficient = byStatus("insufficient");
127
+
128
+ if (loading.length > 0) parts.push(`Loading: ${formatSeriesSymbolList(loading, seriesBySymbol)}`);
129
+ if (errors.length > 0) parts.push(`No data: ${formatSeriesSymbolList(errors, seriesBySymbol)}`);
130
+ if (insufficient.length > 0) parts.push(`Need history: ${formatSeriesSymbolList(insufficient, seriesBySymbol, true)}`);
131
+
132
+ if (sampleMin != null && sampleMax != null) {
133
+ parts.push(sampleMin === sampleMax ? `obs ${sampleMin}` : `obs ${sampleMin}-${sampleMax}`);
134
+ } else if (symbols.length >= 2) {
135
+ parts.push("No paired dates yet");
136
+ }
137
+
138
+ parts.push(`— <${MIN_CORRELATION_OBSERVATIONS} shared`);
139
+ return parts.join(" · ");
140
+ }
141
+
142
+ function buildCorrelationPaneTitle(symbols: string[], rangePreset: CorrelationRangePreset): string {
143
+ if (symbols.length === 0) return `Correlation ${rangePreset}`;
144
+ if (symbols.length <= 3) return `${symbols.join(" · ")} ${rangePreset}`;
145
+ return `${symbols.slice(0, 2).join(" · ")} +${symbols.length - 2} ${rangePreset}`;
146
+ }
147
+
148
+ export function CorrelationMatrixPane({ paneId, width, height }: PaneProps) {
149
+ const state = useAppSelector((s) => s);
150
+ const pane = state.config.layout.instances.find((instance) => instance.instanceId === paneId);
151
+ const settings = useMemo(() => getCorrelationPaneSettings(pane?.settings), [pane?.settings]);
152
+
153
+ const instruments = useMemo(() => {
154
+ if (settings.symbolsError) return [];
155
+ return settings.symbols.map((symbol) => {
156
+ const ticker = state.tickers.get(symbol);
157
+ return {
158
+ symbol,
159
+ exchange: ticker?.metadata.exchange ?? "",
160
+ };
161
+ });
162
+ }, [state.tickers, settings.symbols.join(","), settings.symbolsError]);
163
+
164
+ const instrumentKey = instruments.map((instrument) => `${instrument.symbol}|${instrument.exchange}`).join(",");
165
+
166
+ const chartRequests = useMemo(
167
+ () => instruments.map((instrument) => ({
168
+ instrument: {
169
+ symbol: instrument.symbol,
170
+ exchange: instrument.exchange,
171
+ },
172
+ bufferRange: settings.rangePreset,
173
+ granularity: "resolution" as const,
174
+ resolution: "1d" as const,
175
+ })),
176
+ [instrumentKey, settings.rangePreset],
177
+ );
178
+
179
+ const chartEntries = useChartQueries(chartRequests);
180
+
181
+ const seriesBySymbol = useMemo(() => {
182
+ const map = new Map<string, CorrelationSeries>();
183
+ for (let i = 0; i < instruments.length; i++) {
184
+ const instrument = instruments[i]!;
185
+ const request = chartRequests[i]!;
186
+ const key = buildChartKey(request);
187
+ const entry = chartEntries.get(key);
188
+ map.set(instrument.symbol, getSeriesForEntry(instrument.symbol, entry));
189
+ }
190
+ return map;
191
+ }, [chartEntries, chartRequests, instruments]);
192
+
193
+ const symbols = instruments.map((instrument) => instrument.symbol);
194
+ const symbolsKey = symbols.join(",");
195
+
196
+ const matrix = useMemo(() => {
197
+ const results = new Map<string, CorrelationResult>();
198
+ const sampleSizes: number[] = [];
199
+
200
+ for (let rowIndex = 0; rowIndex < symbols.length; rowIndex++) {
201
+ for (let colIndex = 0; colIndex < symbols.length; colIndex++) {
202
+ if (rowIndex === colIndex) continue;
203
+ const rowSym = symbols[rowIndex]!;
204
+ const colSym = symbols[colIndex]!;
205
+ const rowSeries = seriesBySymbol.get(rowSym);
206
+ const colSeries = seriesBySymbol.get(colSym);
207
+ const result = rowSeries && colSeries
208
+ ? correlateDatedReturns(rowSeries.returns, colSeries.returns, MIN_CORRELATION_OBSERVATIONS)
209
+ : { correlation: null, sampleSize: 0 };
210
+ results.set(pairKey(rowSym, colSym), result);
211
+ if (rowIndex < colIndex && result.sampleSize > 0) {
212
+ sampleSizes.push(result.sampleSize);
213
+ }
214
+ }
215
+ }
216
+
217
+ return {
218
+ results,
219
+ sampleMin: sampleSizes.length > 0 ? Math.min(...sampleSizes) : null,
220
+ sampleMax: sampleSizes.length > 0 ? Math.max(...sampleSizes) : null,
221
+ };
222
+ }, [symbolsKey, seriesBySymbol]);
223
+
224
+ const statusSummary = useMemo(
225
+ () => buildStatusSummary(symbols, seriesBySymbol, matrix.sampleMin, matrix.sampleMax),
226
+ [symbolsKey, seriesBySymbol, matrix.sampleMin, matrix.sampleMax],
227
+ );
228
+
229
+ if (settings.symbolsError) {
230
+ return (
231
+ <box flexDirection="column" width={width} height={height} paddingX={2} paddingY={1}>
232
+ <text fg={colors.negative}>Invalid CORR tickers: {settings.symbolsError}</text>
233
+ <text fg={colors.textMuted}>Open pane settings and enter tickers like AAPL, MSFT, NVDA.</text>
234
+ </box>
235
+ );
236
+ }
237
+
238
+ if (symbols.length < 2) {
239
+ return (
240
+ <box flexDirection="column" width={width} height={height} paddingX={2} paddingY={1}>
241
+ <text fg={colors.textMuted}>Enter at least 2 tickers in pane settings</text>
242
+ </box>
243
+ );
244
+ }
245
+
246
+ const headerBg = colors.panel;
247
+ const rowHeaderWidth = Math.max(
248
+ ROW_HEADER_WIDTH,
249
+ Math.min(12, Math.max(...symbols.map((symbol) => displaySymbol(symbol).length)) + 2),
250
+ );
251
+ const availableCellWidth = Math.floor((Math.max(width - rowHeaderWidth - 4, symbols.length * MIN_MATRIX_CELL_WIDTH)) / symbols.length);
252
+ const cellWidth = Math.max(MIN_MATRIX_CELL_WIDTH, Math.min(MATRIX_CELL_WIDTH, availableCellWidth));
253
+
254
+ return (
255
+ <box flexDirection="column" width={width} height={height}>
256
+ {/* Title */}
257
+ <box flexDirection="row" height={1} paddingX={1}>
258
+ <text fg={colors.textMuted}>{symbols.length} tickers · {settings.rangePreset} daily returns</text>
259
+ </box>
260
+ <box flexDirection="row" height={1} paddingX={1}>
261
+ <text fg={colors.textDim}>{statusSummary}</text>
262
+ </box>
263
+
264
+ {/* Column header row */}
265
+ <box flexDirection="row" paddingX={1} height={1} backgroundColor={headerBg}>
266
+ <box width={rowHeaderWidth} flexShrink={0} />
267
+ {symbols.map((sym) => (
268
+ <box key={sym} width={cellWidth} justifyContent="flex-end" paddingRight={1} overflow="hidden">
269
+ <text fg={colors.textDim} attributes={TextAttributes.BOLD}>
270
+ {displaySymbol(sym)}
271
+ </text>
272
+ </box>
273
+ ))}
274
+ </box>
275
+
276
+ {/* Matrix rows */}
277
+ <scrollbox flexGrow={1} scrollY focusable={false}>
278
+ <box flexDirection="column">
279
+ {symbols.map((rowSym, rowIndex) => (
280
+ <box key={rowSym} flexDirection="row" paddingX={1} backgroundColor={rowIndex % 2 === 0 ? colors.bg : undefined}>
281
+ {/* Row header */}
282
+ <box
283
+ width={rowHeaderWidth}
284
+ flexShrink={0}
285
+ overflow="hidden"
286
+ onMouseDown={() => getSharedRegistry()?.navigateTickerFn(rowSym)}
287
+ >
288
+ <text
289
+ fg={rowHeaderColor(seriesBySymbol.get(rowSym)?.status ?? "loading")}
290
+ attributes={TextAttributes.BOLD | TextAttributes.UNDERLINE}
291
+ >
292
+ {displaySymbol(rowSym)}
293
+ </text>
294
+ </box>
295
+ {/* Cells */}
296
+ {symbols.map((colSym) => {
297
+ const isDiag = rowSym === colSym;
298
+ let r: number | null = null;
299
+ if (isDiag) {
300
+ r = 1;
301
+ } else {
302
+ r = matrix.results.get(pairKey(rowSym, colSym))?.correlation ?? null;
303
+ }
304
+ const cellColor = isDiag
305
+ ? colors.textDim
306
+ : correlationColor(r, colors.positive, colors.negative, colors.textMuted);
307
+ const text = isDiag ? " 1.00" : formatCorrelation(r);
308
+ return (
309
+ <box
310
+ key={colSym}
311
+ width={cellWidth}
312
+ justifyContent="flex-end"
313
+ paddingRight={1}
314
+ backgroundColor={isDiag ? headerBg : undefined}
315
+ >
316
+ <text fg={cellColor}>{text}</text>
317
+ </box>
318
+ );
319
+ })}
320
+ </box>
321
+ ))}
322
+ </box>
323
+ </scrollbox>
324
+
325
+ </box>
326
+ );
327
+ }
328
+
329
+ export const correlationPlugin: GloomPlugin = {
330
+ id: "correlation",
331
+ name: "Correlation Matrix",
332
+ version: "1.0.0",
333
+ description: "NxN Pearson correlation matrix for tickers in portfolios/watchlists",
334
+ toggleable: true,
335
+
336
+ panes: [
337
+ {
338
+ id: "correlation",
339
+ name: "Correlation Matrix",
340
+ icon: "C",
341
+ component: CorrelationMatrixPane,
342
+ defaultPosition: "right",
343
+ defaultMode: "floating",
344
+ defaultFloatingSize: { width: 90, height: 18 },
345
+ settings: buildCorrelationSettingsDef(),
346
+ },
347
+ ],
348
+
349
+ paneTemplates: [
350
+ {
351
+ id: "correlation-pane",
352
+ paneId: "correlation",
353
+ label: "Correlation Matrix",
354
+ description: "Date-aligned Pearson correlation matrix for ticker returns.",
355
+ keywords: ["correlation", "corr", "matrix", "pearson", "returns", "covariance"],
356
+ shortcut: { prefix: "CORR", argPlaceholder: "tickers", argKind: "ticker-list" },
357
+ wizard: [
358
+ {
359
+ key: "tickers",
360
+ label: "Correlation Tickers",
361
+ placeholder: formatTickerListInput(DEFAULT_CORRELATION_SYMBOLS),
362
+ defaultValue: formatTickerListInput(DEFAULT_CORRELATION_SYMBOLS),
363
+ body: [
364
+ `Enter 2-${MAX_CORRELATION_TICKERS} ticker symbols separated by commas.`,
365
+ ],
366
+ type: "text",
367
+ },
368
+ ],
369
+ createInstance: (_context, options) => {
370
+ const symbols = options?.symbols && options.symbols.length >= 2
371
+ ? options.symbols
372
+ : DEFAULT_CORRELATION_SYMBOLS;
373
+ return {
374
+ title: buildCorrelationPaneTitle(symbols, "1Y"),
375
+ placement: "floating",
376
+ settings: {
377
+ rangePreset: "1Y",
378
+ symbols,
379
+ symbolsText: formatTickerListInput(symbols),
380
+ },
381
+ };
382
+ },
383
+ },
384
+ ],
385
+ };
@@ -0,0 +1,44 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { correlationPlugin } from "./index";
3
+
4
+ describe("correlationPlugin", () => {
5
+ test("creates a configured pane from CMP-style ticker-list options", async () => {
6
+ const template = correlationPlugin.paneTemplates?.find((entry) => entry.id === "correlation-pane");
7
+ const instance = await template?.createInstance?.({} as any, { symbols: ["AAPL", "MSFT", "NVDA"] });
8
+
9
+ expect(instance).toMatchObject({
10
+ title: "AAPL · MSFT · NVDA 1Y",
11
+ placement: "floating",
12
+ settings: {
13
+ rangePreset: "1Y",
14
+ symbols: ["AAPL", "MSFT", "NVDA"],
15
+ symbolsText: "AAPL, MSFT, NVDA",
16
+ },
17
+ });
18
+ });
19
+
20
+ test("uses the default CORR preset without explicit tickers", async () => {
21
+ const template = correlationPlugin.paneTemplates?.find((entry) => entry.id === "correlation-pane");
22
+ const instance = await template?.createInstance?.({} as any, undefined);
23
+
24
+ expect(instance).toMatchObject({
25
+ title: "AAPL · MSFT +2 1Y",
26
+ settings: {
27
+ symbols: ["AAPL", "MSFT", "NVDA", "AMD"],
28
+ symbolsText: "AAPL, MSFT, NVDA, AMD",
29
+ },
30
+ });
31
+ });
32
+
33
+ test("uses the default CORR preset when the inferred ticker list is too small", async () => {
34
+ const template = correlationPlugin.paneTemplates?.find((entry) => entry.id === "correlation-pane");
35
+ const instance = await template?.createInstance?.({} as any, { symbols: ["AAPL"] });
36
+
37
+ expect(instance).toMatchObject({
38
+ title: "AAPL · MSFT +2 1Y",
39
+ settings: {
40
+ symbols: ["AAPL", "MSFT", "NVDA", "AMD"],
41
+ },
42
+ });
43
+ });
44
+ });
@@ -0,0 +1,34 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ DEFAULT_CORRELATION_SYMBOLS,
4
+ getCorrelationPaneSettings,
5
+ parseCorrelationSymbolsInput,
6
+ } from "./settings";
7
+
8
+ describe("parseCorrelationSymbolsInput", () => {
9
+ test("normalizes and de-duplicates comma-separated tickers like CMP", () => {
10
+ expect(parseCorrelationSymbolsInput(" msft, aapl,\nMSFT, nvda ")).toEqual(["MSFT", "AAPL", "NVDA"]);
11
+ });
12
+
13
+ test("rejects an empty list", () => {
14
+ expect(() => parseCorrelationSymbolsInput(" , \n ")).toThrow("Enter at least one ticker.");
15
+ });
16
+ });
17
+
18
+ describe("getCorrelationPaneSettings", () => {
19
+ test("uses the default CORR preset when no symbols are configured", () => {
20
+ expect(getCorrelationPaneSettings({}).symbols).toEqual(DEFAULT_CORRELATION_SYMBOLS);
21
+ });
22
+
23
+ test("treats cleared text as the default CORR preset", () => {
24
+ expect(getCorrelationPaneSettings({ symbols: ["AAPL", "MSFT"], symbolsText: "" }).symbols).toEqual(DEFAULT_CORRELATION_SYMBOLS);
25
+ });
26
+
27
+ test("normalizes range and text symbols", () => {
28
+ expect(getCorrelationPaneSettings({ rangePreset: "5Y", symbolsText: "aapl, msft" })).toMatchObject({
29
+ rangePreset: "5Y",
30
+ symbols: ["AAPL", "MSFT"],
31
+ symbolsError: null,
32
+ });
33
+ });
34
+ });
@@ -0,0 +1,95 @@
1
+ import type { TimeRange } from "../../../components/chart/chart-types";
2
+ import type { PaneSettingsDef } from "../../../types/plugin";
3
+ import { formatTickerListInput, MAX_TICKER_LIST_SIZE, parseTickerListInput } from "../../../utils/ticker-list";
4
+
5
+ export const MAX_CORRELATION_TICKERS = MAX_TICKER_LIST_SIZE;
6
+ export const DEFAULT_CORRELATION_RANGE: CorrelationRangePreset = "1Y";
7
+ export const CORRELATION_RANGE_OPTIONS = ["1M", "3M", "6M", "1Y", "5Y"] as const;
8
+ export const DEFAULT_CORRELATION_SYMBOLS = ["AAPL", "MSFT", "NVDA", "AMD"];
9
+
10
+ export type CorrelationRangePreset = Extract<TimeRange, typeof CORRELATION_RANGE_OPTIONS[number]>;
11
+
12
+ export interface CorrelationPaneSettings {
13
+ rangePreset: CorrelationRangePreset;
14
+ symbols: string[];
15
+ symbolsText: string;
16
+ symbolsError: string | null;
17
+ }
18
+
19
+ export function isCorrelationRangePreset(value: unknown): value is CorrelationRangePreset {
20
+ return CORRELATION_RANGE_OPTIONS.includes(value as CorrelationRangePreset);
21
+ }
22
+
23
+ export function normalizeCorrelationRange(value: unknown): CorrelationRangePreset {
24
+ return isCorrelationRangePreset(value) ? value : DEFAULT_CORRELATION_RANGE;
25
+ }
26
+
27
+ export function parseCorrelationSymbolsInput(raw: string, maxTickers = MAX_CORRELATION_TICKERS): string[] {
28
+ return parseTickerListInput(raw, maxTickers);
29
+ }
30
+
31
+ function coerceStoredSymbols(value: unknown): string[] {
32
+ if (!Array.isArray(value)) return [];
33
+ const text = value.filter((entry): entry is string => typeof entry === "string").join(",");
34
+ try {
35
+ return parseCorrelationSymbolsInput(text, MAX_CORRELATION_TICKERS);
36
+ } catch {
37
+ return [];
38
+ }
39
+ }
40
+
41
+ export function getCorrelationPaneSettings(settings: Record<string, unknown> | undefined): CorrelationPaneSettings {
42
+ const rangePreset = normalizeCorrelationRange(settings?.rangePreset);
43
+ const storedText = typeof settings?.symbolsText === "string" ? settings.symbolsText : null;
44
+ const storedSymbols = coerceStoredSymbols(settings?.symbols);
45
+ let symbols = storedSymbols.length > 0 ? storedSymbols : DEFAULT_CORRELATION_SYMBOLS;
46
+ let symbolsText = storedText ?? formatTickerListInput(symbols);
47
+ let symbolsError: string | null = null;
48
+
49
+ if (storedText !== null) {
50
+ if (storedText.trim().length === 0) {
51
+ symbols = DEFAULT_CORRELATION_SYMBOLS;
52
+ symbolsText = formatTickerListInput(symbols);
53
+ } else {
54
+ try {
55
+ symbols = parseCorrelationSymbolsInput(storedText, MAX_CORRELATION_TICKERS);
56
+ symbolsText = storedText;
57
+ } catch (error) {
58
+ symbols = [];
59
+ symbolsError = error instanceof Error ? error.message : "Invalid ticker list.";
60
+ }
61
+ }
62
+ }
63
+
64
+ return {
65
+ rangePreset,
66
+ symbols,
67
+ symbolsText,
68
+ symbolsError,
69
+ };
70
+ }
71
+
72
+ export function buildCorrelationSettingsDef(): PaneSettingsDef {
73
+ return {
74
+ title: "Correlation Matrix Settings",
75
+ fields: [
76
+ {
77
+ key: "symbolsText",
78
+ label: "Tickers",
79
+ description: `Enter up to ${MAX_CORRELATION_TICKERS} tickers. Empty uses the default CORR preset.`,
80
+ type: "text",
81
+ placeholder: formatTickerListInput(DEFAULT_CORRELATION_SYMBOLS),
82
+ },
83
+ {
84
+ key: "rangePreset",
85
+ label: "Range",
86
+ description: "Use daily closes over this range.",
87
+ type: "select",
88
+ options: CORRELATION_RANGE_OPTIONS.map((range) => ({
89
+ value: range,
90
+ label: range,
91
+ })),
92
+ },
93
+ ],
94
+ };
95
+ }