gloomberb 0.4.1 → 0.5.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 (212) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/app.tsx +168 -65
  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 +292 -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 -2
  48. package/src/components/layout/header.tsx +2 -1
  49. package/src/components/layout/pane-sizing.ts +10 -0
  50. package/src/components/layout/pane.tsx +9 -2
  51. package/src/components/layout/shell.test.tsx +282 -28
  52. package/src/components/layout/shell.tsx +10 -7
  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 +54 -1
  63. package/src/components/toggle-list.tsx +15 -2
  64. package/src/components/ui/data-table.tsx +60 -3
  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 +83 -18
  103. package/src/plugins/builtin/chat-controller.ts +26 -14
  104. package/src/plugins/builtin/chat.test.tsx +715 -4
  105. package/src/plugins/builtin/chat.tsx +424 -120
  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 +24 -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/index.test.tsx +1 -1
  166. package/src/plugins/ibkr/index.tsx +1 -1
  167. package/src/plugins/ibkr/trade-tab.tsx +23 -5
  168. package/src/plugins/ibkr/trade-utils.test.ts +1 -1
  169. package/src/plugins/ibkr/trade-utils.ts +7 -6
  170. package/src/plugins/ibkr/trading-pane.tsx +8 -7
  171. package/src/plugins/plugin-runtime.test.tsx +2 -0
  172. package/src/plugins/plugin-runtime.tsx +10 -0
  173. package/src/plugins/prediction-markets/controller-data.ts +8 -0
  174. package/src/plugins/prediction-markets/detail/shared.tsx +7 -6
  175. package/src/plugins/prediction-markets/plugin.test.tsx +48 -1
  176. package/src/plugins/prediction-markets/services/fetch.ts +13 -7
  177. package/src/plugins/prediction-markets/services/polymarket-adapter.ts +29 -11
  178. package/src/plugins/prediction-markets/services/polymarket-ws.ts +20 -0
  179. package/src/plugins/prediction-markets/test-helpers.tsx +2 -0
  180. package/src/plugins/registry.ts +22 -13
  181. package/src/sources/provider-router.test.ts +3 -2
  182. package/src/sources/sec-edgar.ts +13 -1
  183. package/src/sources/yahoo-finance.ts +75 -1
  184. package/src/state/app-context-selector.test.tsx +17 -1
  185. package/src/state/app-context.test.tsx +24 -0
  186. package/src/state/app-context.tsx +29 -6
  187. package/src/theme/colors.ts +5 -0
  188. package/src/theme/themes.ts +25 -0
  189. package/src/types/config.test.ts +107 -0
  190. package/src/types/config.ts +30 -1
  191. package/src/types/data-provider.ts +13 -0
  192. package/src/types/plugin.ts +35 -1
  193. package/src/utils/api-client.test.ts +90 -0
  194. package/src/utils/api-client.ts +56 -16
  195. package/src/utils/format.test.ts +11 -1
  196. package/src/utils/format.ts +86 -8
  197. package/src/utils/inline-content-tokenizer.test.ts +22 -0
  198. package/src/utils/inline-content-tokenizer.ts +76 -0
  199. package/src/utils/link-tokenizer.test.ts +27 -0
  200. package/src/utils/link-tokenizer.ts +93 -0
  201. package/src/utils/market-format.test.ts +109 -0
  202. package/src/utils/market-format.ts +299 -0
  203. package/src/utils/market-status.ts +3 -2
  204. package/src/utils/selection-clipboard.test.ts +43 -0
  205. package/src/utils/selection-clipboard.ts +88 -0
  206. package/src/utils/terminal-input-reset.test.ts +50 -0
  207. package/src/utils/terminal-input-reset.ts +24 -0
  208. package/src/utils/throttled-fetch.test.ts +145 -0
  209. package/src/utils/throttled-fetch.ts +212 -0
  210. package/src/utils/ticker-tokenizer.test.ts +31 -0
  211. package/src/utils/ticker-tokenizer.ts +7 -42
  212. package/src/version.ts +1 -1
@@ -19,6 +19,23 @@ export interface ChartProjection {
19
19
  fallbackMode: ChartRenderMode | null;
20
20
  }
21
21
 
22
+ export interface ProjectChartDataOptions {
23
+ ohlcBucketWidth?: number;
24
+ ohlcSourceBucketSize?: number;
25
+ ohlcTargetBucketCount?: number;
26
+ sourceIndexOffset?: number;
27
+ }
28
+
29
+ export interface StableOhlcProjectionOptionsInput {
30
+ pointCount: number;
31
+ sourceIndexOffset: number;
32
+ bucketWidth: number;
33
+ navigationPointCount?: number;
34
+ countRatioTolerance?: number;
35
+ }
36
+
37
+ const DEFAULT_OHLC_COUNT_RATIO_TOLERANCE = 1.2;
38
+
22
39
  function coerceDate(value: Date | string | number): Date {
23
40
  return value instanceof Date ? value : new Date(value);
24
41
  }
@@ -113,6 +130,104 @@ function normalizePoint(point: PricePoint): ProjectedChartPoint {
113
130
  };
114
131
  }
115
132
 
133
+ function getMinimumPositiveDateGapMs(points: readonly ProjectedChartPoint[]): number | null {
134
+ let minimum = Number.POSITIVE_INFINITY;
135
+ for (let index = 1; index < points.length; index += 1) {
136
+ const previous = points[index - 1]!.date.getTime();
137
+ const current = points[index]!.date.getTime();
138
+ const gap = current - previous;
139
+ if (Number.isFinite(gap) && gap > 0 && gap < minimum) {
140
+ minimum = gap;
141
+ }
142
+ }
143
+ return Number.isFinite(minimum) ? minimum : null;
144
+ }
145
+
146
+ function aggregateOhlcBucket(bucket: readonly ProjectedChartPoint[]): ProjectedChartPoint {
147
+ const first = bucket[0]!;
148
+ const last = bucket[bucket.length - 1]!;
149
+ let high = first.high;
150
+ let low = first.low;
151
+ let volume = 0;
152
+
153
+ for (const point of bucket) {
154
+ high = Math.max(high, point.high);
155
+ low = Math.min(low, point.low);
156
+ volume += point.volume;
157
+ }
158
+
159
+ return {
160
+ date: last.date,
161
+ open: first.open,
162
+ high,
163
+ low,
164
+ close: last.close,
165
+ volume,
166
+ };
167
+ }
168
+
169
+ function bucketOhlcBySourceIndex(
170
+ normalizedPoints: readonly ProjectedChartPoint[],
171
+ bucketSize: number,
172
+ sourceIndexOffset: number,
173
+ targetBucketCount?: number,
174
+ ): ProjectedChartPoint[] {
175
+ const normalizedBucketSize = Math.max(Math.floor(bucketSize), 1);
176
+ const normalizedSourceIndexOffset = Math.max(Math.floor(sourceIndexOffset), 0);
177
+ const maximumBucketCount = targetBucketCount === undefined
178
+ ? Number.POSITIVE_INFINITY
179
+ : Math.max(Math.floor(targetBucketCount), 1);
180
+ const result: ProjectedChartPoint[] = [];
181
+ const firstBucketStart = Math.floor(normalizedSourceIndexOffset / normalizedBucketSize) * normalizedBucketSize;
182
+ const endSourceIndex = normalizedSourceIndexOffset + normalizedPoints.length;
183
+
184
+ for (
185
+ let bucketStart = firstBucketStart;
186
+ bucketStart < endSourceIndex && result.length < maximumBucketCount;
187
+ bucketStart += normalizedBucketSize
188
+ ) {
189
+ const bucketEnd = bucketStart + normalizedBucketSize;
190
+ const start = Math.max(bucketStart - normalizedSourceIndexOffset, 0);
191
+ const end = Math.min(bucketEnd - normalizedSourceIndexOffset, normalizedPoints.length);
192
+ if (end <= start) continue;
193
+ const bucket = normalizedPoints.slice(start, end);
194
+ if (bucket.length === 0) continue;
195
+ result.push(aggregateOhlcBucket(bucket));
196
+ }
197
+
198
+ return result;
199
+ }
200
+
201
+ export function resolveStableOhlcProjectionOptions({
202
+ pointCount,
203
+ sourceIndexOffset,
204
+ bucketWidth,
205
+ navigationPointCount = 0,
206
+ countRatioTolerance = DEFAULT_OHLC_COUNT_RATIO_TOLERANCE,
207
+ }: StableOhlcProjectionOptionsInput): ProjectChartDataOptions {
208
+ const ohlcBucketWidth = Math.max(Math.floor(bucketWidth), 1);
209
+ const ohlcProjectionWidth = Math.max(Math.floor(ohlcBucketWidth / 2), 1);
210
+ const normalizedPointCount = Math.max(Math.floor(pointCount), 0);
211
+ const normalizedNavigationPointCount = Math.max(Math.floor(navigationPointCount), 0);
212
+ const canUseNavigationCount = normalizedNavigationPointCount > 0
213
+ && normalizedPointCount > 0
214
+ && Math.max(normalizedNavigationPointCount, normalizedPointCount)
215
+ / Math.max(Math.min(normalizedNavigationPointCount, normalizedPointCount), 1)
216
+ <= countRatioTolerance;
217
+ const stableSourceCount = canUseNavigationCount
218
+ ? normalizedNavigationPointCount
219
+ : normalizedPointCount;
220
+ const ohlcSourceBucketSize = Math.max(Math.ceil(stableSourceCount / ohlcProjectionWidth), 1);
221
+ const ohlcTargetBucketCount = Math.max(Math.ceil(stableSourceCount / ohlcSourceBucketSize), 1);
222
+
223
+ return {
224
+ ohlcBucketWidth,
225
+ ohlcSourceBucketSize,
226
+ ohlcTargetBucketCount,
227
+ sourceIndexOffset,
228
+ };
229
+ }
230
+
116
231
  /**
117
232
  * Downsample close-driven chart data to fit the target width.
118
233
  * Uses representative closes so line/area charts preserve the broad shape.
@@ -156,44 +271,75 @@ export function projectCloseSeries(
156
271
 
157
272
  /**
158
273
  * Bucket OHLC data so each rendered column preserves open, close, high, low, and volume.
274
+ *
275
+ * The default path aligns buckets by timestamp. Interactive manual panning can pass
276
+ * ohlcSourceBucketSize/ohlcTargetBucketCount to align buckets by source bar index,
277
+ * which keeps the rendered candle count stable as partial edge buckets enter/leave.
159
278
  */
160
279
  export function bucketOhlcSeries(
161
280
  points: PricePoint[],
162
281
  targetWidth: number,
282
+ options: ProjectChartDataOptions = {},
163
283
  ): ProjectedChartPoint[] {
164
284
  if (points.length === 0) return [];
165
- if (points.length <= targetWidth) return points.map(normalizePoint);
285
+ const normalizedPoints = points.map(normalizePoint);
286
+ if (points.length <= targetWidth) return normalizedPoints;
287
+ if (options.ohlcSourceBucketSize !== undefined) {
288
+ return bucketOhlcBySourceIndex(
289
+ normalizedPoints,
290
+ options.ohlcSourceBucketSize,
291
+ options.sourceIndexOffset ?? 0,
292
+ options.ohlcTargetBucketCount,
293
+ );
294
+ }
166
295
 
167
296
  const result: ProjectedChartPoint[] = [];
168
- const bucketSize = points.length / targetWidth;
169
-
170
- for (let i = 0; i < targetWidth; i++) {
171
- const start = Math.floor(i * bucketSize);
172
- const end = Math.min(points.length, Math.max(Math.floor((i + 1) * bucketSize), start + 1));
173
- const bucket = points.slice(start, end);
174
- if (bucket.length === 0) continue;
175
-
176
- const first = bucket[0]!;
177
- const last = bucket[bucket.length - 1]!;
178
-
179
- let high = first.high ?? first.close;
180
- let low = first.low ?? first.close;
181
- let volume = 0;
297
+ const minimumGapMs = getMinimumPositiveDateGapMs(normalizedPoints);
298
+ const firstTime = normalizedPoints[0]!.date.getTime();
299
+ const lastTime = normalizedPoints[normalizedPoints.length - 1]!.date.getTime();
300
+ if (
301
+ minimumGapMs
302
+ && Number.isFinite(firstTime)
303
+ && Number.isFinite(lastTime)
304
+ && lastTime > firstTime
305
+ ) {
306
+ const spanMs = Math.max(lastTime - firstTime, minimumGapMs);
307
+ const rawBucketDurationMs = spanMs / Math.max(targetWidth, 1);
308
+ const bucketDurationMs = Math.max(Math.ceil(rawBucketDurationMs / minimumGapMs) * minimumGapMs, minimumGapMs);
309
+ let currentBucketKey: number | null = null;
310
+ let currentBucket: ProjectedChartPoint[] = [];
311
+
312
+ for (const point of normalizedPoints) {
313
+ const time = point.date.getTime();
314
+ const bucketKey = Math.floor(time / bucketDurationMs);
315
+ if (currentBucketKey !== null && bucketKey !== currentBucketKey) {
316
+ result.push(aggregateOhlcBucket(currentBucket));
317
+ currentBucket = [];
318
+ }
319
+ currentBucketKey = bucketKey;
320
+ currentBucket.push(point);
321
+ }
182
322
 
183
- for (const point of bucket) {
184
- high = Math.max(high, point.high ?? point.close);
185
- low = Math.min(low, point.low ?? point.close);
186
- volume += point.volume ?? 0;
323
+ if (currentBucket.length > 0) {
324
+ result.push(aggregateOhlcBucket(currentBucket));
187
325
  }
188
326
 
189
- result.push({
190
- date: coerceDate(last.date as Date | string | number),
191
- open: first.open ?? first.close,
192
- high,
193
- low,
194
- close: last.close,
195
- volume,
196
- });
327
+ return result;
328
+ }
329
+
330
+ const bucketSize = Math.max(Math.ceil(normalizedPoints.length / Math.max(targetWidth, 1)), 1);
331
+ const sourceIndexOffset = Math.max(Math.floor(options.sourceIndexOffset ?? 0), 0);
332
+ const firstBucketStart = Math.floor(sourceIndexOffset / bucketSize) * bucketSize;
333
+ const endSourceIndex = sourceIndexOffset + normalizedPoints.length;
334
+
335
+ for (let bucketStart = firstBucketStart; bucketStart < endSourceIndex; bucketStart += bucketSize) {
336
+ const bucketEnd = bucketStart + bucketSize;
337
+ const start = Math.max(bucketStart - sourceIndexOffset, 0);
338
+ const end = Math.min(bucketEnd - sourceIndexOffset, normalizedPoints.length);
339
+ if (end <= start) continue;
340
+ const bucket = normalizedPoints.slice(start, end);
341
+ if (bucket.length === 0) continue;
342
+ result.push(aggregateOhlcBucket(bucket));
197
343
  }
198
344
 
199
345
  return result;
@@ -219,14 +365,16 @@ export function projectChartData(
219
365
  targetWidth: number,
220
366
  mode: ChartRenderMode | undefined,
221
367
  compact = false,
368
+ options: ProjectChartDataOptions = {},
222
369
  ): ChartProjection {
223
370
  const { requestedMode, effectiveMode, fallbackMode } = resolveRenderMode(mode, targetWidth, compact);
371
+ const ohlcBucketWidth = options.ohlcBucketWidth ?? targetWidth;
224
372
  const projectionWidth = effectiveMode === "candles" || effectiveMode === "ohlc"
225
- ? Math.max(Math.floor(targetWidth / 2), 1)
373
+ ? Math.max(Math.floor(ohlcBucketWidth / 2), 1)
226
374
  : targetWidth;
227
375
 
228
376
  const projectedPoints = effectiveMode === "candles" || effectiveMode === "ohlc"
229
- ? bucketOhlcSeries(points, projectionWidth)
377
+ ? bucketOhlcSeries(points, projectionWidth, options)
230
378
  : projectCloseSeries(points, projectionWidth);
231
379
 
232
380
  return {
@@ -0,0 +1,53 @@
1
+ import { MultiSelectDialogButton } from "../ui";
2
+ import { ChartControlHint } from "./chart-control-hint";
3
+ import {
4
+ CHART_INDICATOR_OPTIONS,
5
+ normalizeChartIndicatorSelection,
6
+ type ChartIndicatorId,
7
+ } from "./indicators/options";
8
+
9
+ interface ChartIndicatorSelectorProps {
10
+ selectedIds: ChartIndicatorId[];
11
+ onChange: (selectedIds: ChartIndicatorId[]) => void;
12
+ width: number;
13
+ variant?: "button" | "hint";
14
+ shortcutActive?: boolean;
15
+ }
16
+
17
+ export function ChartIndicatorSelector({
18
+ selectedIds,
19
+ onChange,
20
+ width,
21
+ variant = "button",
22
+ shortcutActive = false,
23
+ }: ChartIndicatorSelectorProps) {
24
+ const compact = width < 72;
25
+ const options = CHART_INDICATOR_OPTIONS.map((option) => ({
26
+ value: option.id,
27
+ label: compact ? option.compactLabel : option.label,
28
+ description: option.description,
29
+ }));
30
+
31
+ return (
32
+ <MultiSelectDialogButton
33
+ label={compact ? "IND" : "Indicators"}
34
+ title="Chart Indicators"
35
+ options={options}
36
+ selectedValues={selectedIds}
37
+ onChange={(values) => onChange(normalizeChartIndicatorSelection(values))}
38
+ idPrefix="chart-indicators"
39
+ shortcutKey={variant === "hint" ? "i" : undefined}
40
+ shortcutActive={shortcutActive}
41
+ renderTrigger={variant === "hint"
42
+ ? ({ disabled, openDialog }) => (
43
+ <ChartControlHint
44
+ hotkey="i"
45
+ label="ndicators"
46
+ disabled={disabled}
47
+ onPress={openDialog}
48
+ />
49
+ )
50
+ : undefined}
51
+ />
52
+ );
53
+ }
@@ -2,6 +2,7 @@ import { saveConfig } from "../../data/config-store";
2
2
  import { setPaneSettings } from "../../pane-settings";
3
3
  import { useAppState, usePaneInstance, usePaneInstanceId } from "../../state/app-context";
4
4
  import type { ChartResolution, TimeRange } from "./chart-types";
5
+ import type { IndicatorConfig } from "./indicators/types";
5
6
 
6
7
  export function usePersistChartControlSelection(rangePresetKey: string): (
7
8
  range: TimeRange,
@@ -25,3 +26,22 @@ export function usePersistChartControlSelection(rangePresetKey: string): (
25
26
  saveConfig(nextConfig).catch(() => {});
26
27
  };
27
28
  }
29
+
30
+ export function usePersistIndicatorConfig(): (config: IndicatorConfig) => void {
31
+ const { state, dispatch } = useAppState();
32
+ const paneId = usePaneInstanceId();
33
+ const pane = usePaneInstance();
34
+
35
+ return (indicatorConfig) => {
36
+ const layout = setPaneSettings(state.config.layout, paneId, {
37
+ ...(pane?.settings ?? {}),
38
+ indicators: indicatorConfig,
39
+ });
40
+ const layouts = state.config.layouts.map((savedLayout, index) => (
41
+ index === state.config.activeLayoutIndex ? { ...savedLayout, layout } : savedLayout
42
+ ));
43
+ const nextConfig = { ...state.config, layout, layouts };
44
+ dispatch({ type: "SET_CONFIG", config: nextConfig });
45
+ saveConfig(nextConfig).catch(() => {});
46
+ };
47
+ }