gloomberb 0.3.7 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +19 -8
  2. package/package.json +1 -1
  3. package/src/app.tsx +45 -69
  4. package/src/cli/base-converter.ts +31 -0
  5. package/src/cli/commands/search.ts +11 -3
  6. package/src/cli/commands/ticker.ts +22 -11
  7. package/src/cli/context.ts +27 -32
  8. package/src/cli/helpers.ts +1 -4
  9. package/src/cli/index.ts +140 -123
  10. package/src/cli/registry.test.ts +226 -0
  11. package/src/cli/registry.ts +201 -0
  12. package/src/cli.test.ts +219 -0
  13. package/src/components/chart/chart-controller.ts +309 -0
  14. package/src/components/chart/chart-data.ts +5 -6
  15. package/src/components/chart/chart-pane-settings.ts +27 -0
  16. package/src/components/chart/chart-resolution.test.ts +56 -0
  17. package/src/components/chart/chart-resolution.ts +362 -0
  18. package/src/components/chart/chart-types.ts +12 -4
  19. package/src/components/chart/chart-viewport.test.ts +25 -0
  20. package/src/components/chart/chart-viewport.ts +28 -0
  21. package/src/components/chart/comparison-chart-data.ts +20 -27
  22. package/src/components/chart/comparison-stock-chart.tsx +374 -99
  23. package/src/components/chart/stock-chart-renderer-switch.test.tsx +124 -0
  24. package/src/components/chart/stock-chart.tsx +455 -106
  25. package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
  26. package/src/components/command-bar/command-bar.test.tsx +650 -5
  27. package/src/components/command-bar/command-bar.tsx +363 -63
  28. package/src/components/command-bar/command-registry.ts +8 -0
  29. package/src/components/command-bar/helpers.test.ts +23 -1
  30. package/src/components/command-bar/helpers.ts +14 -5
  31. package/src/components/command-bar/pane-template-display.ts +12 -0
  32. package/src/components/command-bar/root-shortcuts.ts +2 -1
  33. package/src/components/command-bar/workflow-ops.ts +4 -2
  34. package/src/components/detail-feed-view.test.tsx +124 -49
  35. package/src/components/detail-feed-view.tsx +396 -160
  36. package/src/components/layout/floating-pane.tsx +4 -2
  37. package/src/components/layout/header.tsx +3 -3
  38. package/src/components/layout/pane-header.tsx +26 -2
  39. package/src/components/layout/pane-sizing.ts +6 -0
  40. package/src/components/layout/pane.tsx +5 -1
  41. package/src/components/layout/shell.test.tsx +77 -5
  42. package/src/components/layout/shell.tsx +67 -52
  43. package/src/components/layout/status-bar.test.tsx +66 -0
  44. package/src/components/layout/status-bar.tsx +26 -5
  45. package/src/components/onboarding/onboarding-wizard.test.tsx +65 -0
  46. package/src/components/onboarding/onboarding-wizard.tsx +5 -6
  47. package/src/components/ticker-list-table.test.tsx +83 -0
  48. package/src/components/ticker-list-table.tsx +181 -97
  49. package/src/components/ui/data-table.tsx +50 -5
  50. package/src/components/ui/index.ts +1 -0
  51. package/src/components/ui/page-stack-view.tsx +60 -0
  52. package/src/components/use-double-click-activation.ts +31 -0
  53. package/src/index.tsx +10 -10
  54. package/src/market-data/coordinator.test.ts +92 -4
  55. package/src/market-data/coordinator.ts +32 -5
  56. package/src/market-data/hooks.test.tsx +120 -0
  57. package/src/market-data/hooks.tsx +53 -16
  58. package/src/market-data/request-types.ts +4 -2
  59. package/src/market-data/selectors.ts +24 -58
  60. package/src/plugins/builtin/chat-controller.test.ts +400 -3
  61. package/src/plugins/builtin/chat-controller.ts +328 -35
  62. package/src/plugins/builtin/chat.test.tsx +118 -6
  63. package/src/plugins/builtin/chat.tsx +180 -116
  64. package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
  65. package/src/plugins/builtin/comparison-chart.tsx +21 -1
  66. package/src/plugins/builtin/layout-manager.tsx +9 -0
  67. package/src/plugins/builtin/news.tsx +1 -11
  68. package/src/plugins/builtin/portfolio-list/cli/commands.ts +656 -0
  69. package/src/plugins/builtin/portfolio-list/command-bar.ts +122 -0
  70. package/src/plugins/builtin/portfolio-list/header.tsx +16 -34
  71. package/src/plugins/builtin/portfolio-list/index.tsx +5 -0
  72. package/src/plugins/builtin/portfolio-list/metrics.test.ts +61 -0
  73. package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
  74. package/src/plugins/builtin/portfolio-list/mutations.test.ts +187 -0
  75. package/src/plugins/builtin/portfolio-list/mutations.ts +226 -0
  76. package/src/plugins/builtin/portfolio-list/pane.tsx +39 -17
  77. package/src/plugins/builtin/portfolio-list/table.tsx +11 -2
  78. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +163 -0
  79. package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +81 -0
  80. package/src/plugins/builtin/sec.tsx +1 -11
  81. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +13 -2
  82. package/src/plugins/builtin/ticker-detail/financials-tab.tsx +21 -0
  83. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +45 -10
  84. package/src/plugins/builtin/ticker-detail/pane.tsx +35 -17
  85. package/src/plugins/builtin/ticker-detail/settings.ts +19 -1
  86. package/src/plugins/builtin/ticker-detail.test.tsx +47 -0
  87. package/src/plugins/catalog.ts +65 -0
  88. package/src/plugins/ibkr/broker-adapter.ts +28 -0
  89. package/src/plugins/ibkr/gateway-service.test.ts +67 -1
  90. package/src/plugins/ibkr/gateway-service.ts +147 -7
  91. package/src/plugins/ibkr/index.tsx +7 -1
  92. package/src/plugins/pane-manager.test.ts +1 -1
  93. package/src/plugins/plugin-runtime.tsx +27 -7
  94. package/src/plugins/prediction-markets/cli.ts +28 -0
  95. package/src/plugins/prediction-markets/controller-data.ts +132 -17
  96. package/src/plugins/prediction-markets/controller.ts +142 -122
  97. package/src/plugins/prediction-markets/index.tsx +7 -1
  98. package/src/plugins/prediction-markets/launch.ts +51 -16
  99. package/src/plugins/prediction-markets/metrics.ts +9 -5
  100. package/src/plugins/prediction-markets/pane.test.tsx +197 -57
  101. package/src/plugins/prediction-markets/pane.tsx +80 -155
  102. package/src/plugins/prediction-markets/services/fetch.ts +4 -0
  103. package/src/plugins/prediction-markets/table.tsx +6 -3
  104. package/src/plugins/prediction-markets/test-helpers.tsx +20 -5
  105. package/src/plugins/prediction-markets/types.ts +0 -1
  106. package/src/plugins/registry.ts +23 -0
  107. package/src/sources/gloomberb-cloud.test.ts +29 -0
  108. package/src/sources/gloomberb-cloud.ts +89 -1
  109. package/src/sources/provider-router.test.ts +265 -3
  110. package/src/sources/provider-router.ts +239 -11
  111. package/src/sources/yahoo-finance.test.ts +26 -0
  112. package/src/sources/yahoo-finance.ts +54 -3
  113. package/src/state/app-context-selector.test.tsx +99 -0
  114. package/src/state/app-context.test.ts +18 -0
  115. package/src/state/app-context.tsx +170 -65
  116. package/src/state/selectors.ts +25 -0
  117. package/src/theme/colors.ts +8 -8
  118. package/src/theme/themes.test.ts +2 -0
  119. package/src/theme/themes.ts +22 -10
  120. package/src/types/broker.ts +21 -0
  121. package/src/types/config.ts +17 -1
  122. package/src/types/data-provider.ts +18 -0
  123. package/src/types/financials.ts +31 -1
  124. package/src/types/plugin.ts +70 -0
  125. package/src/updater.test.ts +2 -0
  126. package/src/utils/api-client.test.ts +57 -0
  127. package/src/utils/api-client.ts +47 -12
  128. package/src/utils/back-navigation.ts +27 -0
  129. package/src/utils/format.test.ts +14 -0
  130. package/src/utils/format.ts +3 -3
  131. package/src/utils/market-status.ts +46 -1
  132. package/src/utils/quote-freshness.ts +127 -0
  133. package/src/utils/quote-resolution.test.ts +277 -0
  134. package/src/utils/quote-resolution.ts +641 -0
  135. package/src/utils/slugify.ts +3 -0
  136. package/src/utils/timestamp.ts +25 -0
  137. package/src/version.ts +1 -1
  138. package/src/cli/commands/portfolio.ts +0 -210
  139. package/src/cli/commands/watchlist.ts +0 -210
  140. package/src/plugins/builtin/data-providers.ts +0 -16
  141. package/src/sources/exchange-rates.ts +0 -18
  142. package/src/utils/ascii-chart.ts +0 -172
  143. package/src/utils/ticker-tokenizer.test.ts +0 -48
@@ -0,0 +1,309 @@
1
+ import type { QueryEntry } from "../../market-data/result-types";
2
+ import type { PricePoint } from "../../types/financials";
3
+ import { clampChartZoom, getVisiblePointCount } from "./chart-viewport";
4
+ import type { ChartResolution, ChartViewState, ComparisonChartViewState, TimeRange } from "./chart-types";
5
+ import {
6
+ clampTimeRangeToMaxRange,
7
+ getBestSupportedResolutionForPreset,
8
+ getCompatibleBufferRange,
9
+ getNextBufferRange,
10
+ getPresetResolution,
11
+ getSupportMaxRange,
12
+ getWidestPresetForResolution,
13
+ isDateWindowWithinTimeRange,
14
+ isTimeRangeAtOrBelow,
15
+ subtractTimeRange,
16
+ type ChartResolutionSupport,
17
+ type ManualChartResolution,
18
+ } from "./chart-resolution";
19
+
20
+ type ViewStateWithViewport = Pick<ChartViewState, "presetRange" | "bufferRange" | "activePreset" | "resolution" | "panOffset" | "zoomLevel" | "cursorX" | "cursorY">
21
+ | Pick<ComparisonChartViewState, "presetRange" | "bufferRange" | "activePreset" | "resolution" | "panOffset" | "zoomLevel" | "cursorX" | "cursorY">;
22
+
23
+ export interface VisibleDateWindow {
24
+ start: Date | null;
25
+ end: Date | null;
26
+ dates: Date[];
27
+ startIdx: number;
28
+ endIdx: number;
29
+ totalDates: number;
30
+ }
31
+
32
+ export interface ChartBodyState<T> {
33
+ data: T | null;
34
+ blocking: boolean;
35
+ updating: boolean;
36
+ emptyMessage: string | null;
37
+ errorMessage: string | null;
38
+ }
39
+
40
+ function clamp(value: number, min: number, max: number): number {
41
+ return Math.max(min, Math.min(max, value));
42
+ }
43
+
44
+ function coerceDate(value: Date | string | number): Date {
45
+ return value instanceof Date ? value : new Date(value);
46
+ }
47
+
48
+ export function getPointDates(points: readonly Pick<PricePoint, "date">[]): Date[] {
49
+ return points.map((point) => coerceDate(point.date as Date | string | number));
50
+ }
51
+
52
+ export function getCanonicalVisiblePointCount(dates: readonly Date[], presetRange: TimeRange): number {
53
+ if (dates.length === 0) return 0;
54
+ if (presetRange === "ALL") return dates.length;
55
+ const endDate = dates[dates.length - 1]!;
56
+ const threshold = subtractTimeRange(endDate, presetRange).getTime();
57
+ const firstVisibleIndex = dates.findIndex((date) => date.getTime() >= threshold);
58
+ if (firstVisibleIndex < 0) return dates.length;
59
+ return Math.max(dates.length - firstVisibleIndex, 1);
60
+ }
61
+
62
+ export function getCanonicalZoomLevel(dates: readonly Date[], presetRange: TimeRange): number {
63
+ if (dates.length === 0) return 1;
64
+ const visibleCount = getCanonicalVisiblePointCount(dates, presetRange);
65
+ return clampChartZoom(dates.length, dates.length / Math.max(visibleCount, 1));
66
+ }
67
+
68
+ export function buildVisibleDateWindow(
69
+ dates: readonly Date[],
70
+ panOffset: number,
71
+ zoomLevel: number,
72
+ ): VisibleDateWindow {
73
+ if (dates.length === 0) {
74
+ return { start: null, end: null, dates: [], startIdx: 0, endIdx: 0, totalDates: 0 };
75
+ }
76
+ const visibleCount = getVisiblePointCount(dates.length, zoomLevel);
77
+ const maxPan = Math.max(dates.length - visibleCount, 0);
78
+ const pan = clamp(panOffset, 0, maxPan);
79
+ const endIdx = dates.length - pan;
80
+ const startIdx = Math.max(endIdx - visibleCount, 0);
81
+ const visibleDates = dates.slice(startIdx, endIdx);
82
+ return {
83
+ start: visibleDates[0] ?? null,
84
+ end: visibleDates[visibleDates.length - 1] ?? null,
85
+ dates: visibleDates,
86
+ startIdx,
87
+ endIdx,
88
+ totalDates: dates.length,
89
+ };
90
+ }
91
+
92
+ export function formatVisibleSpanLabel(window: Pick<VisibleDateWindow, "start" | "end">): string {
93
+ if (!window.start || !window.end) return "view: --";
94
+ const sameYear = window.start.getFullYear() === window.end.getFullYear();
95
+ const sameMonth = sameYear && window.start.getMonth() === window.end.getMonth();
96
+ const startLabel = window.start.toLocaleDateString("en-US", {
97
+ month: "short",
98
+ day: "numeric",
99
+ ...(sameYear ? {} : { year: "numeric" }),
100
+ });
101
+ const endLabel = window.end.toLocaleDateString("en-US", {
102
+ month: sameMonth ? undefined : "short",
103
+ day: "numeric",
104
+ year: sameYear ? undefined : "numeric",
105
+ });
106
+ return `view:${startLabel}-${endLabel}`;
107
+ }
108
+
109
+ export function isCanonicalPresetViewport(
110
+ dates: readonly Date[],
111
+ state: Pick<ViewStateWithViewport, "activePreset" | "panOffset" | "zoomLevel" | "resolution">,
112
+ ): boolean {
113
+ if (!state.activePreset) return false;
114
+ if (state.panOffset !== 0) return false;
115
+ const canonicalZoom = getCanonicalZoomLevel(dates, state.activePreset);
116
+ return Math.abs(state.zoomLevel - canonicalZoom) < 0.001;
117
+ }
118
+
119
+ export function resolvePresetSelectionWithResolution<S extends ViewStateWithViewport>(
120
+ state: S,
121
+ presetRange: TimeRange,
122
+ resolution: ManualChartResolution,
123
+ supportMaxRange: TimeRange | null = null,
124
+ ): S {
125
+ return {
126
+ ...state,
127
+ presetRange,
128
+ bufferRange: getCompatibleBufferRange(presetRange, supportMaxRange),
129
+ activePreset: presetRange,
130
+ resolution,
131
+ panOffset: 0,
132
+ zoomLevel: 1,
133
+ cursorX: null,
134
+ cursorY: null,
135
+ };
136
+ }
137
+
138
+ export function resolvePresetSelection<S extends ViewStateWithViewport>(
139
+ state: S,
140
+ presetRange: TimeRange,
141
+ supportMaxRange: TimeRange | null = null,
142
+ ): S {
143
+ return resolvePresetSelectionWithResolution(
144
+ state,
145
+ presetRange,
146
+ getPresetResolution(presetRange),
147
+ supportMaxRange,
148
+ );
149
+ }
150
+
151
+ export function resolveResolutionSelection<S extends ViewStateWithViewport>(
152
+ state: S,
153
+ resolution: ChartResolution,
154
+ support: ReadonlyMap<ManualChartResolution, TimeRange>,
155
+ visibleWindow: Pick<VisibleDateWindow, "start" | "end"> | null,
156
+ ): S | null {
157
+ if (resolution === "auto") {
158
+ return {
159
+ ...state,
160
+ resolution,
161
+ bufferRange: getNextBufferRange(state.presetRange),
162
+ activePreset: null,
163
+ panOffset: 0,
164
+ zoomLevel: 1,
165
+ cursorX: null,
166
+ cursorY: null,
167
+ };
168
+ }
169
+
170
+ const maxRange = getSupportMaxRange(support, resolution);
171
+ if (!maxRange) return null;
172
+
173
+ const visibleSpanTooWide = visibleWindow?.start && visibleWindow.end
174
+ ? !isDateWindowWithinTimeRange(visibleWindow.start, visibleWindow.end, maxRange)
175
+ : false;
176
+ const shouldSnap = !isTimeRangeAtOrBelow(state.presetRange, maxRange) || visibleSpanTooWide;
177
+ const presetRange = shouldSnap
178
+ ? getWidestPresetForResolution(resolution, maxRange)
179
+ : clampTimeRangeToMaxRange(state.presetRange, maxRange);
180
+
181
+ return {
182
+ ...state,
183
+ presetRange,
184
+ bufferRange: getCompatibleBufferRange(presetRange, maxRange),
185
+ activePreset: shouldSnap ? presetRange : null,
186
+ resolution,
187
+ panOffset: 0,
188
+ zoomLevel: 1,
189
+ cursorX: null,
190
+ cursorY: null,
191
+ };
192
+ }
193
+
194
+ export function resolveStoredChartSelection<S extends ViewStateWithViewport>(
195
+ state: S,
196
+ presetRange: TimeRange,
197
+ resolution: ChartResolution,
198
+ support: ReadonlyMap<ManualChartResolution, TimeRange>,
199
+ ): S {
200
+ const resetState = {
201
+ ...state,
202
+ presetRange,
203
+ panOffset: 0,
204
+ zoomLevel: 1,
205
+ cursorX: null,
206
+ cursorY: null,
207
+ };
208
+
209
+ if (resolution === "auto") {
210
+ return {
211
+ ...resetState,
212
+ resolution,
213
+ bufferRange: getNextBufferRange(presetRange),
214
+ activePreset: null,
215
+ };
216
+ }
217
+
218
+ const resolvedSelection = resolveResolutionSelection(
219
+ {
220
+ ...resetState,
221
+ bufferRange: getNextBufferRange(presetRange),
222
+ activePreset: null,
223
+ resolution: "auto",
224
+ },
225
+ resolution,
226
+ support,
227
+ null,
228
+ );
229
+
230
+ if (!resolvedSelection) {
231
+ const fallbackResolution = getBestSupportedResolutionForPreset(presetRange, support);
232
+ if (!fallbackResolution) {
233
+ return {
234
+ ...resetState,
235
+ resolution: "auto",
236
+ bufferRange: getNextBufferRange(presetRange),
237
+ activePreset: null,
238
+ };
239
+ }
240
+ return resolvePresetSelectionWithResolution(
241
+ resetState,
242
+ presetRange,
243
+ fallbackResolution,
244
+ getSupportMaxRange(support, fallbackResolution),
245
+ );
246
+ }
247
+
248
+ return {
249
+ ...resolvedSelection,
250
+ activePreset: resolvedSelection.resolution === getBestSupportedResolutionForPreset(resolvedSelection.presetRange, support)
251
+ ? resolvedSelection.presetRange
252
+ : null,
253
+ };
254
+ }
255
+
256
+ export function clearActivePreset<S extends Pick<ViewStateWithViewport, "activePreset">>(state: S): S {
257
+ if (state.activePreset === null) return state;
258
+ return {
259
+ ...state,
260
+ activePreset: null,
261
+ };
262
+ }
263
+
264
+ export function resolveChartBodyState<T>(
265
+ entry: QueryEntry<T> | null | undefined,
266
+ hasData: (value: T | null) => boolean,
267
+ emptyMessage: string,
268
+ ): ChartBodyState<T> {
269
+ const data = entry?.data ?? null;
270
+ const hasCurrentData = hasData(data);
271
+
272
+ if (!entry || entry.phase === "idle" || ((entry.phase === "loading" || entry.phase === "refreshing") && !hasCurrentData)) {
273
+ return {
274
+ data: null,
275
+ blocking: true,
276
+ updating: false,
277
+ emptyMessage: null,
278
+ errorMessage: null,
279
+ };
280
+ }
281
+
282
+ if (entry.phase === "error" && !hasCurrentData) {
283
+ return {
284
+ data: null,
285
+ blocking: false,
286
+ updating: false,
287
+ emptyMessage: null,
288
+ errorMessage: entry.error?.message ?? "Chart data failed to load.",
289
+ };
290
+ }
291
+
292
+ if (!hasCurrentData) {
293
+ return {
294
+ data,
295
+ blocking: false,
296
+ updating: false,
297
+ emptyMessage,
298
+ errorMessage: null,
299
+ };
300
+ }
301
+
302
+ return {
303
+ data,
304
+ blocking: false,
305
+ updating: entry.phase === "refreshing",
306
+ emptyMessage: null,
307
+ errorMessage: null,
308
+ };
309
+ }
@@ -78,22 +78,21 @@ export function getVisibleWindow(
78
78
  viewState: ChartViewState,
79
79
  chartWidth: number,
80
80
  ): VisibleWindow {
81
- const filtered = filterByTimeRange(history, viewState.timeRange);
82
- if (filtered.length === 0) {
81
+ if (history.length === 0) {
83
82
  return { points: [], startIdx: 0, endIdx: 0 };
84
83
  }
85
84
 
86
- const visibleCount = getVisiblePointCount(filtered.length, viewState.zoomLevel);
85
+ const visibleCount = getVisiblePointCount(history.length, viewState.zoomLevel);
87
86
 
88
87
  // Clamp pan offset
89
- const maxPan = Math.max(filtered.length - visibleCount, 0);
88
+ const maxPan = Math.max(history.length - visibleCount, 0);
90
89
  const pan = Math.min(Math.max(viewState.panOffset, 0), maxPan);
91
90
 
92
- const endIdx = filtered.length - pan;
91
+ const endIdx = history.length - pan;
93
92
  const startIdx = Math.max(endIdx - visibleCount, 0);
94
93
 
95
94
  return {
96
- points: filtered.slice(startIdx, endIdx),
95
+ points: history.slice(startIdx, endIdx),
97
96
  startIdx,
98
97
  endIdx,
99
98
  };
@@ -0,0 +1,27 @@
1
+ import { saveConfig } from "../../data/config-store";
2
+ import { setPaneSettings } from "../../pane-settings";
3
+ import { useAppState, usePaneInstance, usePaneInstanceId } from "../../state/app-context";
4
+ import type { ChartResolution, TimeRange } from "./chart-types";
5
+
6
+ export function usePersistChartControlSelection(rangePresetKey: string): (
7
+ range: TimeRange,
8
+ resolution: ChartResolution,
9
+ ) => void {
10
+ const { state, dispatch } = useAppState();
11
+ const paneId = usePaneInstanceId();
12
+ const pane = usePaneInstance();
13
+
14
+ return (range, resolution) => {
15
+ const layout = setPaneSettings(state.config.layout, paneId, {
16
+ ...(pane?.settings ?? {}),
17
+ [rangePresetKey]: range,
18
+ chartResolution: resolution,
19
+ });
20
+ const layouts = state.config.layouts.map((savedLayout, index) => (
21
+ index === state.config.activeLayoutIndex ? { ...savedLayout, layout } : savedLayout
22
+ ));
23
+ const nextConfig = { ...state.config, layout, layouts };
24
+ dispatch({ type: "SET_CONFIG", config: nextConfig });
25
+ saveConfig(nextConfig).catch(() => {});
26
+ };
27
+ }
@@ -0,0 +1,56 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ clampTimeRangeForResolution,
4
+ getActiveRangePreset,
5
+ getPresetResolution,
6
+ intersectChartResolutions,
7
+ isRangePresetSupported,
8
+ normalizeChartResolution,
9
+ sortChartResolutions,
10
+ } from "./chart-resolution";
11
+
12
+ describe("chart-resolution", () => {
13
+ test("maps range presets to their default manual resolutions", () => {
14
+ expect(getPresetResolution("1W")).toBe("5m");
15
+ expect(getPresetResolution("1M")).toBe("15m");
16
+ expect(getPresetResolution("3M")).toBe("1h");
17
+ expect(getPresetResolution("6M")).toBe("1d");
18
+ expect(getPresetResolution("1Y")).toBe("1d");
19
+ expect(getPresetResolution("5Y")).toBe("1wk");
20
+ expect(getPresetResolution("ALL")).toBe("1mo");
21
+ });
22
+
23
+ test("derives the active range preset only for the exact reset preset pair", () => {
24
+ expect(getActiveRangePreset("1Y", "1d", 1, 0)).toBe("1Y");
25
+ expect(getActiveRangePreset("1Y", "auto", 1, 0)).toBeNull();
26
+ expect(getActiveRangePreset("1Y", "1d", 1.2, 0)).toBeNull();
27
+ expect(getActiveRangePreset("1Y", "1d", 1, 2)).toBeNull();
28
+ });
29
+
30
+ test("intersects and sorts provider capabilities", () => {
31
+ expect(intersectChartResolutions([
32
+ ["1d", "5m", "1wk", "nope"],
33
+ ["1wk", "1d", "1mo"],
34
+ ["1d", "1wk"],
35
+ ])).toEqual(["1d", "1wk"]);
36
+ });
37
+
38
+ test("checks whether a range preset is supported by the visible capability set", () => {
39
+ expect(isRangePresetSupported("1Y", ["1d", "1wk"])).toBe(true);
40
+ expect(isRangePresetSupported("ALL", ["1d", "1wk"])).toBe(false);
41
+ });
42
+
43
+ test("clamps overly-wide ranges for manual intraday resolutions", () => {
44
+ expect(clampTimeRangeForResolution("5Y", "5m")).toBe("1W");
45
+ expect(clampTimeRangeForResolution("6M", "1h")).toBe("3M");
46
+ expect(clampTimeRangeForResolution("1M", "15m")).toBe("1M");
47
+ expect(clampTimeRangeForResolution("5Y", "1d")).toBe("5Y");
48
+ expect(clampTimeRangeForResolution("5Y", "auto")).toBe("5Y");
49
+ });
50
+
51
+ test("normalizes and sorts chart resolution values", () => {
52
+ expect(normalizeChartResolution("1h")).toBe("1h");
53
+ expect(normalizeChartResolution("bogus", "1d")).toBe("1d");
54
+ expect(sortChartResolutions(["1wk", "auto", "15m", "1d"])).toEqual(["auto", "15m", "1d", "1wk"]);
55
+ });
56
+ });