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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { act } from "react";
|
|
3
|
-
import {
|
|
2
|
+
import { act, type ReactElement } from "react";
|
|
3
|
+
import { createTestRenderer } from "@opentui/core/testing";
|
|
4
|
+
import { createRoot } from "@opentui/react";
|
|
4
5
|
import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../../market-data/coordinator";
|
|
5
6
|
import {
|
|
6
7
|
AppContext,
|
|
@@ -23,8 +24,10 @@ import {
|
|
|
23
24
|
|
|
24
25
|
const TEST_PANE_ID = "comparison-chart:test";
|
|
25
26
|
|
|
26
|
-
let testSetup: Awaited<ReturnType<typeof
|
|
27
|
+
let testSetup: Awaited<ReturnType<typeof createTestRenderer>> | undefined;
|
|
28
|
+
let root: ReturnType<typeof createRoot> | undefined;
|
|
27
29
|
let sharedCoordinator: MarketDataCoordinator | null = null;
|
|
30
|
+
const actEnvironment = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean };
|
|
28
31
|
|
|
29
32
|
function makeTicker(symbol: string, currency: string): TickerRecord {
|
|
30
33
|
return {
|
|
@@ -96,8 +99,16 @@ function createProvider(historyBySymbol: Record<string, number[]>, currencyBySym
|
|
|
96
99
|
|
|
97
100
|
function createRegistrySpy(spy: { selected: string[]; focused: string[] }): PluginRegistry {
|
|
98
101
|
return {
|
|
102
|
+
navigateTickerFn: (symbol: string) => {
|
|
103
|
+
spy.selected.push(symbol);
|
|
104
|
+
spy.focused.push("ticker-detail");
|
|
105
|
+
},
|
|
99
106
|
selectTickerFn: (symbol: string) => { spy.selected.push(symbol); },
|
|
100
107
|
focusPaneFn: (paneId: string) => { spy.focused.push(paneId); },
|
|
108
|
+
navigateTickerFn: (symbol: string) => {
|
|
109
|
+
spy.selected.push(symbol);
|
|
110
|
+
spy.focused.push("ticker-detail");
|
|
111
|
+
},
|
|
101
112
|
} as unknown as PluginRegistry;
|
|
102
113
|
}
|
|
103
114
|
|
|
@@ -132,7 +143,7 @@ function createComparisonHarness(
|
|
|
132
143
|
focused: boolean;
|
|
133
144
|
width: number;
|
|
134
145
|
height: number;
|
|
135
|
-
}) =>
|
|
146
|
+
}) => ReactElement;
|
|
136
147
|
|
|
137
148
|
return (
|
|
138
149
|
<AppContext value={{ state, dispatch: () => {} }}>
|
|
@@ -153,12 +164,47 @@ async function flushFrames(count = 3) {
|
|
|
153
164
|
for (let index = 0; index < count; index += 1) {
|
|
154
165
|
await act(async () => {
|
|
155
166
|
await Promise.resolve();
|
|
167
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
156
168
|
await testSetup!.renderOnce();
|
|
169
|
+
await Promise.resolve();
|
|
157
170
|
});
|
|
158
171
|
}
|
|
159
172
|
}
|
|
160
173
|
|
|
161
|
-
|
|
174
|
+
async function mountComparisonHarness(
|
|
175
|
+
settings: Record<string, unknown>,
|
|
176
|
+
tickers: TickerRecord[],
|
|
177
|
+
financials: Array<[string, TickerFinancials]>,
|
|
178
|
+
) {
|
|
179
|
+
actEnvironment.IS_REACT_ACT_ENVIRONMENT = true;
|
|
180
|
+
testSetup = await createTestRenderer({ width: 120, height: 20 });
|
|
181
|
+
root = createRoot(testSetup.renderer);
|
|
182
|
+
await act(async () => {
|
|
183
|
+
root!.render(
|
|
184
|
+
createComparisonHarness(settings, tickers, financials),
|
|
185
|
+
);
|
|
186
|
+
await Promise.resolve();
|
|
187
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
188
|
+
await testSetup!.renderOnce();
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function pressComparisonInput(action: () => void) {
|
|
193
|
+
await act(async () => {
|
|
194
|
+
action();
|
|
195
|
+
await testSetup!.renderOnce();
|
|
196
|
+
await Promise.resolve();
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
afterEach(async () => {
|
|
201
|
+
if (root) {
|
|
202
|
+
await act(async () => {
|
|
203
|
+
root!.unmount();
|
|
204
|
+
await Promise.resolve();
|
|
205
|
+
});
|
|
206
|
+
root = undefined;
|
|
207
|
+
}
|
|
162
208
|
if (testSetup) {
|
|
163
209
|
testSetup.renderer.destroy();
|
|
164
210
|
testSetup = undefined;
|
|
@@ -172,7 +218,7 @@ afterEach(() => {
|
|
|
172
218
|
describe("comparisonChartPlugin", () => {
|
|
173
219
|
test("creates a configured comparison pane with percent as the default axis", () => {
|
|
174
220
|
const template = comparisonChartPlugin.paneTemplates?.find((entry) => entry.id === COMPARISON_CHART_TEMPLATE_ID);
|
|
175
|
-
const paneDef = comparisonChartPlugin.panes
|
|
221
|
+
const paneDef = comparisonChartPlugin.panes?.find((entry) => entry.id === COMPARISON_CHART_PANE_ID);
|
|
176
222
|
|
|
177
223
|
expect(template).toBeDefined();
|
|
178
224
|
expect(paneDef?.defaultMode).toBe("floating");
|
|
@@ -223,30 +269,32 @@ describe("comparisonChartPlugin", () => {
|
|
|
223
269
|
setSharedMarketDataCoordinator(sharedCoordinator);
|
|
224
270
|
setSharedRegistryForTests(createRegistrySpy({ selected: [], focused: [] }));
|
|
225
271
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
]),
|
|
238
|
-
{ width: 120, height: 20 },
|
|
239
|
-
);
|
|
272
|
+
await mountComparisonHarness({
|
|
273
|
+
axisMode: "price",
|
|
274
|
+
symbols: ["AAPL", "7203"],
|
|
275
|
+
symbolsText: "AAPL, 7203",
|
|
276
|
+
}, [
|
|
277
|
+
makeTicker("AAPL", "USD"),
|
|
278
|
+
makeTicker("7203", "JPY"),
|
|
279
|
+
], [
|
|
280
|
+
["AAPL", makeFinancials("AAPL", "USD", [100, 102, 104, 106])],
|
|
281
|
+
["7203", makeFinancials("7203", "JPY", [2000, 2020, 2050, 2100])],
|
|
282
|
+
]);
|
|
240
283
|
|
|
241
284
|
await flushFrames();
|
|
242
285
|
|
|
243
|
-
const frame = testSetup
|
|
286
|
+
const frame = testSetup!.captureCharFrame();
|
|
244
287
|
expect(frame).toContain("Mixed currencies detected; showing percent change.");
|
|
245
288
|
expect(frame).toContain("1:1D");
|
|
246
289
|
expect(frame).toContain("2:1W");
|
|
247
290
|
expect(frame).toContain("1D");
|
|
248
291
|
expect(frame).toContain("view:");
|
|
249
|
-
expect(frame).toContain("
|
|
292
|
+
expect(frame).toContain("[m]ode");
|
|
293
|
+
expect(frame).toContain("[r]es");
|
|
294
|
+
expect(frame).toContain("[up/down]legend");
|
|
295
|
+
expect(frame).not.toContain("arrows legend");
|
|
296
|
+
expect(frame).not.toContain("wheel pan");
|
|
297
|
+
expect(frame).not.toContain("wheel zoom");
|
|
250
298
|
expect(frame).not.toContain("side by side");
|
|
251
299
|
});
|
|
252
300
|
|
|
@@ -263,24 +311,21 @@ describe("comparisonChartPlugin", () => {
|
|
|
263
311
|
setSharedMarketDataCoordinator(sharedCoordinator);
|
|
264
312
|
setSharedRegistryForTests(createRegistrySpy({ selected: [], focused: [] }));
|
|
265
313
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
]),
|
|
278
|
-
{ width: 120, height: 20 },
|
|
279
|
-
);
|
|
314
|
+
await mountComparisonHarness({
|
|
315
|
+
axisMode: "price",
|
|
316
|
+
symbols: ["AAPL", "MSFT"],
|
|
317
|
+
symbolsText: "AAPL, MSFT",
|
|
318
|
+
}, [
|
|
319
|
+
makeTicker("AAPL", "USD"),
|
|
320
|
+
makeTicker("MSFT", "USD"),
|
|
321
|
+
], [
|
|
322
|
+
["AAPL", makeFinancials("AAPL", "USD", [])],
|
|
323
|
+
["MSFT", makeFinancials("MSFT", "USD", [])],
|
|
324
|
+
]);
|
|
280
325
|
|
|
281
326
|
await flushFrames();
|
|
282
327
|
|
|
283
|
-
const frame = testSetup
|
|
328
|
+
const frame = testSetup!.captureCharFrame();
|
|
284
329
|
expect(frame).toContain("No chart data yet.");
|
|
285
330
|
expect(frame).toContain("1:1D");
|
|
286
331
|
expect(frame).toContain("2:1W");
|
|
@@ -303,28 +348,23 @@ describe("comparisonChartPlugin", () => {
|
|
|
303
348
|
setSharedMarketDataCoordinator(sharedCoordinator);
|
|
304
349
|
setSharedRegistryForTests(createRegistrySpy(spy));
|
|
305
350
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
]),
|
|
320
|
-
{ width: 120, height: 20 },
|
|
321
|
-
);
|
|
351
|
+
await mountComparisonHarness({
|
|
352
|
+
axisMode: "percent",
|
|
353
|
+
symbols: ["AAPL", "MSFT", "NVDA"],
|
|
354
|
+
symbolsText: "AAPL, MSFT, NVDA",
|
|
355
|
+
}, [
|
|
356
|
+
makeTicker("AAPL", "USD"),
|
|
357
|
+
makeTicker("MSFT", "USD"),
|
|
358
|
+
makeTicker("NVDA", "USD"),
|
|
359
|
+
], [
|
|
360
|
+
["AAPL", makeFinancials("AAPL", "USD", [100, 102, 104])],
|
|
361
|
+
["MSFT", makeFinancials("MSFT", "USD", [200, 202, 204])],
|
|
362
|
+
["NVDA", makeFinancials("NVDA", "USD", [300, 305, 310])],
|
|
363
|
+
]);
|
|
322
364
|
|
|
323
365
|
await flushFrames();
|
|
324
|
-
testSetup
|
|
325
|
-
await
|
|
326
|
-
testSetup.mockInput.pressEnter();
|
|
327
|
-
await flushFrames();
|
|
366
|
+
await pressComparisonInput(() => testSetup!.mockInput.pressArrow("right"));
|
|
367
|
+
await pressComparisonInput(() => testSetup!.mockInput.pressEnter());
|
|
328
368
|
|
|
329
369
|
expect(spy.selected).toEqual(["MSFT"]);
|
|
330
370
|
expect(spy.focused).toEqual(["ticker-detail"]);
|
|
@@ -106,8 +106,7 @@ function ComparisonChartPane({ paneId, focused, width, height }: PaneProps) {
|
|
|
106
106
|
const settings = useMemo(() => getComparisonChartPaneSettings(pane?.settings), [pane?.settings]);
|
|
107
107
|
|
|
108
108
|
const openTicker = useCallback((symbol: string) => {
|
|
109
|
-
registry?.
|
|
110
|
-
registry?.focusPaneFn("ticker-detail");
|
|
109
|
+
registry?.navigateTickerFn(symbol);
|
|
111
110
|
}, [registry]);
|
|
112
111
|
|
|
113
112
|
if (settings.symbols.length === 0) {
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
alignDatedReturns,
|
|
4
|
+
computeDatedReturns,
|
|
5
|
+
computeReturns,
|
|
6
|
+
correlateDatedReturns,
|
|
7
|
+
formatCorrelation,
|
|
8
|
+
pearsonCorrelation,
|
|
9
|
+
} from "./compute";
|
|
10
|
+
|
|
11
|
+
describe("computeReturns", () => {
|
|
12
|
+
test("computes simple returns", () => {
|
|
13
|
+
const returns = computeReturns([100, 110, 105, 115]);
|
|
14
|
+
expect(returns).toHaveLength(3);
|
|
15
|
+
expect(returns[0]).toBeCloseTo(0.1, 5);
|
|
16
|
+
expect(returns[1]).toBeCloseTo(-0.0455, 3);
|
|
17
|
+
expect(returns[2]).toBeCloseTo(0.0952, 3);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("returns empty for single value", () => {
|
|
21
|
+
expect(computeReturns([100])).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("skips zero or invalid previous closes", () => {
|
|
25
|
+
expect(computeReturns([0, 10, 20, Number.NaN, 30])).toEqual([1]);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("computeDatedReturns", () => {
|
|
30
|
+
test("keys returns by the current close date", () => {
|
|
31
|
+
const returns = computeDatedReturns([
|
|
32
|
+
{ date: new Date("2024-01-01T00:00:00Z"), close: 100 },
|
|
33
|
+
{ date: new Date("2024-01-02T00:00:00Z"), close: 110 },
|
|
34
|
+
{ date: new Date("2024-01-03T00:00:00Z"), close: 121 },
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
expect(returns).toEqual([
|
|
38
|
+
{ dateKey: "2024-01-02", value: 0.1 },
|
|
39
|
+
{ dateKey: "2024-01-03", value: 0.1 },
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("sorts points before computing returns", () => {
|
|
44
|
+
const returns = computeDatedReturns([
|
|
45
|
+
{ date: new Date("2024-01-03T00:00:00Z"), close: 121 },
|
|
46
|
+
{ date: new Date("2024-01-01T00:00:00Z"), close: 100 },
|
|
47
|
+
{ date: new Date("2024-01-02T00:00:00Z"), close: 110 },
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
expect(returns.map((entry) => entry.dateKey)).toEqual(["2024-01-02", "2024-01-03"]);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("alignDatedReturns", () => {
|
|
55
|
+
test("aligns series by shared date keys", () => {
|
|
56
|
+
const aligned = alignDatedReturns(
|
|
57
|
+
[
|
|
58
|
+
{ dateKey: "2024-01-02", value: 1 },
|
|
59
|
+
{ dateKey: "2024-01-03", value: 2 },
|
|
60
|
+
{ dateKey: "2024-01-04", value: 3 },
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
{ dateKey: "2024-01-01", value: 99 },
|
|
64
|
+
{ dateKey: "2024-01-02", value: 10 },
|
|
65
|
+
{ dateKey: "2024-01-04", value: 30 },
|
|
66
|
+
],
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
expect(aligned).toEqual({
|
|
70
|
+
x: [1, 3],
|
|
71
|
+
y: [10, 30],
|
|
72
|
+
sampleSize: 2,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("pearsonCorrelation", () => {
|
|
78
|
+
test("perfect positive correlation", () => {
|
|
79
|
+
const x = [1, 2, 3, 4, 5];
|
|
80
|
+
const y = [2, 4, 6, 8, 10];
|
|
81
|
+
expect(pearsonCorrelation(x, y)).toBeCloseTo(1.0, 5);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("perfect negative correlation", () => {
|
|
85
|
+
const x = [1, 2, 3, 4, 5];
|
|
86
|
+
const y = [10, 8, 6, 4, 2];
|
|
87
|
+
expect(pearsonCorrelation(x, y)).toBeCloseTo(-1.0, 5);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("returns null for insufficient data", () => {
|
|
91
|
+
expect(pearsonCorrelation([1, 2], [3, 4])).toBeNull();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("returns null for zero variance", () => {
|
|
95
|
+
expect(pearsonCorrelation([5, 5, 5, 5, 5], [1, 2, 3, 4, 5])).toBeNull();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("correlateDatedReturns", () => {
|
|
100
|
+
test("correlates only shared trading dates", () => {
|
|
101
|
+
const result = correlateDatedReturns(
|
|
102
|
+
[
|
|
103
|
+
{ dateKey: "2024-01-02", value: 1 },
|
|
104
|
+
{ dateKey: "2024-01-03", value: 2 },
|
|
105
|
+
{ dateKey: "2024-01-04", value: 3 },
|
|
106
|
+
{ dateKey: "2024-01-05", value: 4 },
|
|
107
|
+
{ dateKey: "2024-01-06", value: 5 },
|
|
108
|
+
],
|
|
109
|
+
[
|
|
110
|
+
{ dateKey: "2024-01-01", value: 999 },
|
|
111
|
+
{ dateKey: "2024-01-02", value: 2 },
|
|
112
|
+
{ dateKey: "2024-01-03", value: 4 },
|
|
113
|
+
{ dateKey: "2024-01-04", value: 6 },
|
|
114
|
+
{ dateKey: "2024-01-05", value: 8 },
|
|
115
|
+
{ dateKey: "2024-01-06", value: 10 },
|
|
116
|
+
],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
expect(result.sampleSize).toBe(5);
|
|
120
|
+
expect(result.correlation).toBeCloseTo(1, 5);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("returns sample size even when shared observations are insufficient", () => {
|
|
124
|
+
expect(correlateDatedReturns(
|
|
125
|
+
[
|
|
126
|
+
{ dateKey: "2024-01-02", value: 1 },
|
|
127
|
+
{ dateKey: "2024-01-03", value: 2 },
|
|
128
|
+
],
|
|
129
|
+
[
|
|
130
|
+
{ dateKey: "2024-01-02", value: 1 },
|
|
131
|
+
{ dateKey: "2024-01-03", value: 2 },
|
|
132
|
+
],
|
|
133
|
+
)).toEqual({ correlation: null, sampleSize: 2 });
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("formatCorrelation", () => {
|
|
138
|
+
test("formats positive", () => {
|
|
139
|
+
expect(formatCorrelation(0.85)).toContain("0.85");
|
|
140
|
+
});
|
|
141
|
+
test("formats negative", () => {
|
|
142
|
+
expect(formatCorrelation(-0.42)).toContain("-0.42");
|
|
143
|
+
});
|
|
144
|
+
test("formats null as dash", () => {
|
|
145
|
+
expect(formatCorrelation(null)).toContain("—");
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { PricePoint } from "../../../types/financials";
|
|
2
|
+
|
|
3
|
+
export interface DatedReturn {
|
|
4
|
+
dateKey: string;
|
|
5
|
+
value: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface CorrelationResult {
|
|
9
|
+
correlation: number | null;
|
|
10
|
+
sampleSize: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getPointTimestamp(point: PricePoint): number {
|
|
14
|
+
const value = point.date as Date | string | number | null | undefined;
|
|
15
|
+
if (value instanceof Date) return value.getTime();
|
|
16
|
+
if (value == null) return Number.NaN;
|
|
17
|
+
return new Date(value).getTime();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function toDateKey(point: PricePoint): string | null {
|
|
21
|
+
const timestamp = getPointTimestamp(point);
|
|
22
|
+
if (!Number.isFinite(timestamp)) return null;
|
|
23
|
+
return new Date(timestamp).toISOString().slice(0, 10);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function comparePricePointsByDate(left: PricePoint, right: PricePoint): number {
|
|
27
|
+
return getPointTimestamp(left) - getPointTimestamp(right);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function computeReturns(closes: number[]): number[] {
|
|
31
|
+
const returns: number[] = [];
|
|
32
|
+
for (let i = 1; i < closes.length; i++) {
|
|
33
|
+
const previous = closes[i - 1]!;
|
|
34
|
+
const current = closes[i]!;
|
|
35
|
+
if (!Number.isFinite(previous) || !Number.isFinite(current) || previous === 0) continue;
|
|
36
|
+
returns.push((current - previous) / previous);
|
|
37
|
+
}
|
|
38
|
+
return returns;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function computeDatedReturns(points: PricePoint[]): DatedReturn[] {
|
|
42
|
+
const sorted = [...points]
|
|
43
|
+
.filter((point) => Number.isFinite(getPointTimestamp(point)) && Number.isFinite(point.close))
|
|
44
|
+
.sort(comparePricePointsByDate);
|
|
45
|
+
const byDate = new Map<string, number>();
|
|
46
|
+
|
|
47
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
48
|
+
const previous = sorted[i - 1]!;
|
|
49
|
+
const current = sorted[i]!;
|
|
50
|
+
if (previous.close === 0) continue;
|
|
51
|
+
const value = (current.close - previous.close) / previous.close;
|
|
52
|
+
const dateKey = toDateKey(current);
|
|
53
|
+
if (!dateKey || !Number.isFinite(value)) continue;
|
|
54
|
+
byDate.set(dateKey, value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return [...byDate.entries()].map(([dateKey, value]) => ({ dateKey, value }));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function alignDatedReturns(x: DatedReturn[], y: DatedReturn[]): { x: number[]; y: number[]; sampleSize: number } {
|
|
61
|
+
const yByDate = new Map(y.map((entry) => [entry.dateKey, entry.value] as const));
|
|
62
|
+
const alignedX: number[] = [];
|
|
63
|
+
const alignedY: number[] = [];
|
|
64
|
+
|
|
65
|
+
for (const left of x) {
|
|
66
|
+
const rightValue = yByDate.get(left.dateKey);
|
|
67
|
+
if (rightValue === undefined) continue;
|
|
68
|
+
alignedX.push(left.value);
|
|
69
|
+
alignedY.push(rightValue);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
x: alignedX,
|
|
74
|
+
y: alignedY,
|
|
75
|
+
sampleSize: alignedX.length,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function pearsonCorrelation(x: number[], y: number[], minObservations = 5): number | null {
|
|
80
|
+
const n = Math.min(x.length, y.length);
|
|
81
|
+
if (n < minObservations) return null;
|
|
82
|
+
|
|
83
|
+
let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0, sumY2 = 0;
|
|
84
|
+
for (let i = 0; i < n; i++) {
|
|
85
|
+
sumX += x[i]!;
|
|
86
|
+
sumY += y[i]!;
|
|
87
|
+
sumXY += x[i]! * y[i]!;
|
|
88
|
+
sumX2 += x[i]! * x[i]!;
|
|
89
|
+
sumY2 += y[i]! * y[i]!;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const denom = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY));
|
|
93
|
+
if (denom === 0) return null;
|
|
94
|
+
return (n * sumXY - sumX * sumY) / denom;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function correlateDatedReturns(x: DatedReturn[], y: DatedReturn[], minObservations = 5): CorrelationResult {
|
|
98
|
+
const aligned = alignDatedReturns(x, y);
|
|
99
|
+
return {
|
|
100
|
+
correlation: pearsonCorrelation(aligned.x, aligned.y, minObservations),
|
|
101
|
+
sampleSize: aligned.sampleSize,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function formatCorrelation(r: number | null): string {
|
|
106
|
+
if (r === null) return " — ";
|
|
107
|
+
return r >= 0 ? ` ${r.toFixed(2)}` : r.toFixed(2);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function correlationColor(r: number | null, positive: string, negative: string, neutral: string): string {
|
|
111
|
+
if (r === null) return neutral;
|
|
112
|
+
if (r > 0.7) return positive;
|
|
113
|
+
if (r < -0.7) return negative;
|
|
114
|
+
return neutral;
|
|
115
|
+
}
|