gloomberb 0.7.2 → 0.7.4

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 (167) hide show
  1. package/README.md +79 -132
  2. package/package.json +1 -1
  3. package/src/app.tsx +73 -67
  4. package/src/components/chart/chart-data.ts +7 -2
  5. package/src/components/chart/chart-pane-settings.ts +7 -4
  6. package/src/components/chart/chart-renderer.ts +158 -47
  7. package/src/components/chart/chart-types.ts +20 -2
  8. package/src/components/chart/chart.test.ts +179 -13
  9. package/src/components/chart/comparison-chart-renderer.test.ts +14 -30
  10. package/src/components/chart/comparison-chart-renderer.ts +50 -11
  11. package/src/components/chart/comparison-stock-chart.tsx +129 -129
  12. package/src/components/chart/market-session.ts +187 -0
  13. package/src/components/chart/native/chart-rasterizer.ts +108 -19
  14. package/src/components/chart/price-axis-labels.tsx +25 -9
  15. package/src/components/chart/stock-chart-renderer-switch.test.tsx +97 -1
  16. package/src/components/chart/stock-chart.tsx +95 -49
  17. package/src/components/chart/time-axis-label.tsx +56 -2
  18. package/src/components/command-bar/command-bar.test.tsx +446 -120
  19. package/src/components/command-bar/command-bar.tsx +535 -384
  20. package/src/components/command-bar/command-registry.ts +16 -6
  21. package/src/components/command-bar/list-model.ts +8 -4
  22. package/src/components/command-bar/root-shortcuts.ts +4 -5
  23. package/src/components/command-bar/view-model.test.ts +21 -0
  24. package/src/components/command-bar/view-model.ts +17 -4
  25. package/src/components/command-bar/workflow-ops.ts +11 -32
  26. package/src/components/data-table-view.test.tsx +24 -1
  27. package/src/components/data-table-view.tsx +3 -2
  28. package/src/components/feed-data-table-stack-view.tsx +3 -2
  29. package/src/components/layout/detached-pane-shell.tsx +2 -2
  30. package/src/components/layout/floating-pane.tsx +9 -2
  31. package/src/components/layout/pane-footer.tsx +1 -1
  32. package/src/components/layout/pane-header.tsx +4 -2
  33. package/src/components/layout/pane-sizing.ts +12 -0
  34. package/src/components/layout/pane.tsx +24 -2
  35. package/src/components/layout/shell.tsx +24 -10
  36. package/src/components/layout/status-bar.test.tsx +4 -1
  37. package/src/components/layout/status-bar.tsx +4 -24
  38. package/src/components/markdown-text.tsx +53 -8
  39. package/src/components/onboarding/onboarding-wizard.tsx +7 -6
  40. package/src/components/pane-settings-dialog.tsx +9 -6
  41. package/src/components/pane-template-wizard.tsx +3 -2
  42. package/src/components/price-selector-dialog.tsx +3 -2
  43. package/src/components/price-sparkline-view.tsx +463 -0
  44. package/src/components/price-sparkline.ts +46 -0
  45. package/src/components/table-view-shared.tsx +21 -3
  46. package/src/components/ticker-list-table-view.tsx +3 -2
  47. package/src/components/ticker-list-table.tsx +15 -66
  48. package/src/components/toggle-list.tsx +3 -3
  49. package/src/components/ui/choice-dialog.tsx +3 -2
  50. package/src/components/ui/multi-select-dialog.tsx +3 -2
  51. package/src/components/ui/native-select.tsx +3 -3
  52. package/src/core/state/app-state.ts +210 -80
  53. package/src/core/state/session-persistence.ts +25 -0
  54. package/src/data/config-store-node.ts +113 -25
  55. package/src/data/config-store.test.ts +65 -3
  56. package/src/data/plugin-state-store.ts +39 -13
  57. package/src/data/sqlite-cache.test.ts +13 -0
  58. package/src/market-data/coordinator-subscriptions.test.ts +102 -18
  59. package/src/market-data/coordinator.test.ts +122 -2
  60. package/src/market-data/coordinator.ts +200 -56
  61. package/src/market-data/hooks.test.tsx +31 -0
  62. package/src/market-data/hooks.tsx +1 -0
  63. package/src/news/aggregator.test.ts +41 -0
  64. package/src/news/aggregator.ts +61 -9
  65. package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +25 -6
  66. package/src/plugins/builtin/ai/screener-pane.tsx +3 -0
  67. package/src/plugins/builtin/ai/settings.ts +3 -0
  68. package/src/plugins/builtin/broker-manager/index.tsx +4 -3
  69. package/src/plugins/builtin/chat-controller.test.ts +398 -17
  70. package/src/plugins/builtin/chat-controller.ts +263 -48
  71. package/src/plugins/builtin/chat.test.tsx +312 -6
  72. package/src/plugins/builtin/chat.tsx +245 -66
  73. package/src/plugins/builtin/comparison-chart.test.tsx +117 -3
  74. package/src/plugins/builtin/correlation/colors.test.ts +39 -0
  75. package/src/plugins/builtin/correlation/colors.ts +87 -0
  76. package/src/plugins/builtin/correlation/index.test.tsx +151 -0
  77. package/src/plugins/builtin/correlation/index.tsx +92 -21
  78. package/src/plugins/builtin/debug.tsx +4 -3
  79. package/src/plugins/builtin/econ/index.tsx +3 -2
  80. package/src/plugins/builtin/help.test.tsx +2 -2
  81. package/src/plugins/builtin/holders/index.tsx +25 -13
  82. package/src/plugins/builtin/layout-manager.tsx +1 -7
  83. package/src/plugins/builtin/market-movers/index.tsx +72 -40
  84. package/src/plugins/builtin/news-wire/breaking-pane.test.tsx +156 -0
  85. package/src/plugins/builtin/news-wire/breaking-pane.tsx +0 -90
  86. package/src/plugins/builtin/news-wire/index.tsx +1 -3
  87. package/src/plugins/builtin/news-wire/news-detail-view.tsx +3 -2
  88. package/src/plugins/builtin/news-wire/news-preset-pane.tsx +4 -1
  89. package/src/plugins/builtin/news-wire/persisted-articles.ts +7 -1
  90. package/src/plugins/builtin/options.tsx +9 -17
  91. package/src/plugins/builtin/portfolio-list/cli/commands.ts +6 -10
  92. package/src/plugins/builtin/portfolio-list/metrics.ts +13 -2
  93. package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
  94. package/src/plugins/builtin/portfolio-list/pane.tsx +139 -23
  95. package/src/plugins/builtin/portfolio-list/quick-add.tsx +422 -0
  96. package/src/plugins/builtin/portfolio-list/settings.ts +20 -12
  97. package/src/plugins/builtin/portfolio-list/table.tsx +3 -3
  98. package/src/plugins/builtin/portfolio-list.test.tsx +311 -9
  99. package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
  100. package/src/plugins/builtin/sectors/index.tsx +275 -43
  101. package/src/plugins/builtin/sectors/sector-data.ts +41 -1
  102. package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -2
  103. package/src/plugins/builtin/ticker-detail/index.tsx +30 -9
  104. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -15
  105. package/src/plugins/builtin/ticker-detail/pane.tsx +18 -7
  106. package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +445 -63
  107. package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
  108. package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
  109. package/src/plugins/builtin/world-indices/index.tsx +58 -19
  110. package/src/plugins/ibkr/trading-pane.tsx +3 -2
  111. package/src/plugins/plugin-runtime.tsx +2 -2
  112. package/src/plugins/prediction-markets/settings.ts +4 -0
  113. package/src/plugins/registry.test.ts +55 -0
  114. package/src/plugins/registry.ts +11 -4
  115. package/src/plugins/ticker-navigation.test.ts +35 -5
  116. package/src/plugins/ticker-navigation.ts +16 -22
  117. package/src/renderers/electrobun/bun/application-menu-click.test.ts +9 -0
  118. package/src/renderers/electrobun/bun/application-menu-click.ts +7 -4
  119. package/src/renderers/electrobun/bun/application-menu.ts +6 -1
  120. package/src/renderers/electrobun/bun/desktop-workspace.ts +47 -29
  121. package/src/renderers/electrobun/bun/index.ts +52 -13
  122. package/src/renderers/electrobun/view/app-services.ts +51 -9
  123. package/src/renderers/electrobun/view/backend-rpc.ts +5 -8
  124. package/src/renderers/electrobun/view/desktop-controls.tsx +4 -4
  125. package/src/renderers/electrobun/view/main.tsx +0 -16
  126. package/src/renderers/electrobun/view/styles.css +23 -21
  127. package/src/renderers/electrobun/view/ui-host.tsx +6 -4
  128. package/src/renderers/opentui/host.tsx +2 -1
  129. package/src/renderers/opentui/start.tsx +1 -1
  130. package/src/renderers/opentui/test-utils.tsx +6 -5
  131. package/src/sources/gloomberb-cloud.test.ts +55 -2
  132. package/src/sources/gloomberb-cloud.ts +180 -29
  133. package/src/sources/provider-router.test.ts +567 -10
  134. package/src/sources/provider-router.ts +323 -93
  135. package/src/sources/yahoo-finance.test.ts +37 -0
  136. package/src/sources/yahoo-finance.ts +48 -2
  137. package/src/state/app-bootstrap.test.ts +50 -23
  138. package/src/state/app-bootstrap.ts +29 -13
  139. package/src/state/app-context.test.ts +113 -0
  140. package/src/state/app-context.tsx +8 -4
  141. package/src/state/session-persistence.test.ts +5 -3
  142. package/src/state/use-inline-tickers.ts +3 -0
  143. package/src/state/use-quote-streaming.ts +11 -0
  144. package/src/theme/colors.ts +13 -14
  145. package/src/types/config.ts +9 -4
  146. package/src/types/data-provider.ts +5 -1
  147. package/src/types/financials.ts +1 -0
  148. package/src/types/plugin.ts +8 -1
  149. package/src/ui/context-menu.tsx +4 -4
  150. package/src/utils/api-client.test.ts +248 -0
  151. package/src/utils/api-client.ts +178 -26
  152. package/src/utils/exchanges.test.ts +16 -0
  153. package/src/utils/exchanges.ts +33 -0
  154. package/src/utils/fuzzy-search.ts +12 -1
  155. package/src/utils/keyboard.ts +31 -0
  156. package/src/utils/options.ts +1 -1
  157. package/src/utils/price-history.test.ts +28 -1
  158. package/src/utils/price-history.ts +16 -0
  159. package/src/utils/quote-freshness.test.ts +150 -0
  160. package/src/utils/quote-freshness.ts +42 -1
  161. package/src/utils/quote-resolution.ts +2 -0
  162. package/src/utils/quote-time.test.ts +34 -0
  163. package/src/utils/quote-time.ts +18 -5
  164. package/src/utils/ticker-search.test.ts +2 -2
  165. package/src/utils/ticker-search.ts +48 -1
  166. package/src/version.ts +1 -1
  167. package/src/plugins/builtin/news-wire/digest-store.ts +0 -44
@@ -1,7 +1,17 @@
1
1
  import { RGBA } from "../../ui";
2
- import type { ChartAxisMode, ChartColors, ChartIndicatorOverlays, Pixel, PixelBuffer, ChartRenderMode } from "./chart-types";
2
+ import type {
3
+ ChartAxisMode,
4
+ ChartColors,
5
+ ChartIndicatorOverlays,
6
+ ChartMarketSession,
7
+ ChartSessionBackgroundSpan,
8
+ Pixel,
9
+ PixelBuffer,
10
+ ChartRenderMode,
11
+ } from "./chart-types";
3
12
  import type { ProjectedChartPoint } from "./chart-data";
4
13
  import { formatCompactMarketPriceWithCurrency, formatMarketPrice, resolveAssetDisplayKind } from "../../utils/market-format";
14
+ import { resolveExtendedHoursBackgroundSpans } from "./market-session";
5
15
 
6
16
  // ---------------------------------------------------------------------------
7
17
  // Styled output types (unchanged public API)
@@ -26,7 +36,7 @@ export interface StyledContent {
26
36
  const LAYER_GRID = 0;
27
37
  const LAYER_FILL = 1;
28
38
  const LAYER_DATA = 2;
29
- const LAYER_OVERLAY = 2;
39
+ const LAYER_OVERLAY = 1;
30
40
  const LAYER_CROSSHAIR = 3;
31
41
 
32
42
  function normalizeCount(value: number, min: number): number {
@@ -135,6 +145,8 @@ export function resolveChartPalette(
135
145
  axisColor: baseColors.textDim,
136
146
  activeRangeColor: baseColors.text,
137
147
  inactiveRangeColor: blendHex(baseColors.bg, baseColors.textDim, 0.75),
148
+ preMarketBgColor: blendHex(baseColors.bg, "#8a641f", 0.28),
149
+ postMarketBgColor: blendHex(baseColors.bg, "#7a4624", 0.28),
138
150
  candleUp: baseColors.positive,
139
151
  candleDown: baseColors.negative,
140
152
  wickUp: blendHex(baseColors.positive, baseColors.text, 0.35),
@@ -150,10 +162,12 @@ export function createPixelBuffer(width: number, heightPixels: number): PixelBuf
150
162
  const bufferWidth = normalizeCount(width, 0);
151
163
  const bufferHeight = normalizeCount(heightPixels, 0);
152
164
  const pixels: (Pixel | null)[][] = [];
165
+ const backgrounds: (string | null)[][] = [];
153
166
  for (let y = 0; y < bufferHeight; y++) {
154
167
  pixels.push(new Array(bufferWidth).fill(null));
168
+ backgrounds.push(new Array(bufferWidth).fill(null));
155
169
  }
156
- return { width: bufferWidth, height: bufferHeight, pixels };
170
+ return { width: bufferWidth, height: bufferHeight, pixels, backgrounds };
157
171
  }
158
172
 
159
173
  function setPixel(buf: PixelBuffer, x: number, y: number, color: string, layer: number) {
@@ -224,6 +238,27 @@ function fillRect(buf: PixelBuffer, x0: number, y0: number, x1: number, y1: numb
224
238
  }
225
239
  }
226
240
 
241
+ function setBackgroundPixel(buf: PixelBuffer, x: number, y: number, color: string) {
242
+ const px = Math.round(x);
243
+ const py = Math.round(y);
244
+ if (px < 0 || px >= buf.width || py < 0 || py >= buf.height) return;
245
+ const row = buf.backgrounds[py];
246
+ if (!row) return;
247
+ row[px] = color;
248
+ }
249
+
250
+ function fillBackgroundRect(buf: PixelBuffer, x0: number, y0: number, x1: number, y1: number, color: string) {
251
+ const left = Math.max(Math.min(Math.round(x0), Math.round(x1)), 0);
252
+ const right = Math.min(Math.max(Math.round(x0), Math.round(x1)), Math.max(buf.width - 1, 0));
253
+ const top = Math.max(Math.min(Math.round(y0), Math.round(y1)), 0);
254
+ const bottom = Math.min(Math.max(Math.round(y0), Math.round(y1)), Math.max(buf.height - 1, 0));
255
+ for (let y = top; y <= bottom; y += 1) {
256
+ for (let x = left; x <= right; x += 1) {
257
+ setBackgroundPixel(buf, x, y, color);
258
+ }
259
+ }
260
+ }
261
+
227
262
  function drawWickOutsideBody(
228
263
  buf: PixelBuffer,
229
264
  x: number,
@@ -278,15 +313,41 @@ function getBarHalfWidth(pointCount: number, bufWidth: number): number {
278
313
  return Math.floor(barW / 2);
279
314
  }
280
315
 
316
+ function isOhlcLikeMode(mode: ChartRenderMode): boolean {
317
+ return mode === "ohlc" || mode === "hlc";
318
+ }
319
+
320
+ function isHighLowMode(mode: ChartRenderMode): boolean {
321
+ return mode === "candles" || isOhlcLikeMode(mode);
322
+ }
323
+
324
+ function getOhlcTickLength(pointCount: number, bufWidth: number): number {
325
+ if (pointCount <= 1) return 4;
326
+ const spacing = bufWidth / pointCount;
327
+ return Math.min(Math.max(Math.round(spacing * 0.45), 3), 5);
328
+ }
329
+
330
+ function getOhlcStemWidth(pointCount: number, bufWidth: number): number {
331
+ if (pointCount <= 1) return 3;
332
+ const spacing = bufWidth / pointCount;
333
+ return Math.min(Math.max(Math.round(spacing * 0.2), 2), 3);
334
+ }
335
+
336
+ function getOhlcHorizontalPad(pointCount: number, bufWidth: number): number {
337
+ const tickLen = getOhlcTickLength(pointCount, bufWidth);
338
+ const stemWidth = getOhlcStemWidth(pointCount, bufWidth);
339
+ return Math.max(tickLen - 1, Math.ceil((stemWidth - 1) / 2), 0);
340
+ }
341
+
281
342
  function getDotX(index: number, pointCount: number, width: number, mode: ChartRenderMode): number {
282
343
  switch (mode) {
283
344
  case "candles": {
284
345
  const pad = getCandleHalfWidth(pointCount, width);
285
346
  return getSeriesPosition(index, pointCount, width, pad, pad);
286
347
  }
287
- case "ohlc": {
288
- const tickLen = Math.max(getCandleHalfWidth(pointCount, width), 2);
289
- const pad = Math.max(tickLen - 1, 0);
348
+ case "ohlc":
349
+ case "hlc": {
350
+ const pad = getOhlcHorizontalPad(pointCount, width);
290
351
  return getSeriesPosition(index, pointCount, width, pad, pad);
291
352
  }
292
353
  default:
@@ -303,6 +364,38 @@ export function getPointTerminalColumn(index: number, pointCount: number, width:
303
364
  return Math.min(Math.max(Math.floor(getPointDotX(index, pointCount, width, mode) / 2), 0), width - 1);
304
365
  }
305
366
 
367
+ function getTimePointBand(index: number, pointCount: number, width: number): { left: number; right: number } {
368
+ const currentX = getSeriesPosition(index, pointCount, width);
369
+ const previousX = index > 0 ? getSeriesPosition(index - 1, pointCount, width) : currentX;
370
+ const nextX = index < pointCount - 1 ? getSeriesPosition(index + 1, pointCount, width) : currentX;
371
+ return {
372
+ left: index === 0 ? 0 : Math.floor((previousX + currentX) / 2) + 1,
373
+ right: index === pointCount - 1 ? Math.max(width - 1, 0) : Math.floor((currentX + nextX) / 2),
374
+ };
375
+ }
376
+
377
+ function getSessionBackgroundColor(span: ChartSessionBackgroundSpan, colors: ChartColors): string {
378
+ return span.kind === "pre" ? colors.preMarketBgColor : colors.postMarketBgColor;
379
+ }
380
+
381
+ function drawSessionBackgrounds(
382
+ buf: PixelBuffer,
383
+ points: ProjectedChartPoint[],
384
+ spans: readonly ChartSessionBackgroundSpan[],
385
+ yTop: number,
386
+ yBottom: number,
387
+ colors: ChartColors,
388
+ ) {
389
+ if (spans.length === 0 || points.length === 0) return;
390
+ for (const span of spans) {
391
+ const startIndex = Math.max(Math.min(span.startIndex, points.length - 1), 0);
392
+ const endIndex = Math.max(Math.min(span.endIndex, points.length - 1), startIndex);
393
+ const startBand = getTimePointBand(startIndex, points.length, buf.width);
394
+ const endBand = getTimePointBand(endIndex, points.length, buf.width);
395
+ fillBackgroundRect(buf, startBand.left, yTop, endBand.right, yBottom, getSessionBackgroundColor(span, colors));
396
+ }
397
+ }
398
+
306
399
  // ---------------------------------------------------------------------------
307
400
  // Chart mode renderers
308
401
  // ---------------------------------------------------------------------------
@@ -415,12 +508,16 @@ function drawOhlcChart(
415
508
  palette: ResolvedChartPalette,
416
509
  min: number,
417
510
  max: number,
511
+ mode: Extract<ChartRenderMode, "ohlc" | "hlc">,
418
512
  ) {
419
- const tickLen = Math.max(getCandleHalfWidth(points.length, buf.width), 2);
513
+ const tickLen = getOhlcTickLength(points.length, buf.width);
514
+ const stemWidth = getOhlcStemWidth(points.length, buf.width);
515
+ const stemLeft = Math.floor((stemWidth - 1) / 2);
516
+ const stemRight = Math.ceil((stemWidth - 1) / 2);
420
517
 
421
518
  for (let i = 0; i < points.length; i++) {
422
519
  const point = points[i]!;
423
- const x = getDotX(i, points.length, buf.width, "ohlc");
520
+ const x = getDotX(i, points.length, buf.width, mode);
424
521
  const highY = getScaledY(point.high, min, max, chartTop, chartBottom);
425
522
  const lowY = getScaledY(point.low, min, max, chartTop, chartBottom);
426
523
  const openY = getScaledY(point.open, min, max, chartTop, chartBottom);
@@ -428,14 +525,15 @@ function drawOhlcChart(
428
525
  const isUp = point.close >= point.open;
429
526
  const color = isUp ? palette.candleUp : palette.candleDown;
430
527
 
431
- // Vertical line
432
- drawLine(buf, x, highY, x, lowY, color, LAYER_DATA);
528
+ for (let dx = -stemLeft; dx <= stemRight; dx++) {
529
+ drawLine(buf, x + dx, highY, x + dx, lowY, color, LAYER_DATA);
530
+ }
433
531
 
434
- // Open tick extends left
435
- for (let dx = 0; dx < tickLen; dx++) {
436
- setPixel(buf, x - dx, openY, color, LAYER_DATA);
532
+ if (mode === "ohlc") {
533
+ for (let dx = 0; dx < tickLen; dx++) {
534
+ setPixel(buf, x - dx, openY, color, LAYER_DATA);
535
+ }
437
536
  }
438
- // Close tick extends right
439
537
  for (let dx = 0; dx < tickLen; dx++) {
440
538
  setPixel(buf, x + dx, closeY, color, LAYER_DATA);
441
539
  }
@@ -542,14 +640,19 @@ export function bufferToBrailleLines(buf: PixelBuffer): StyledContent[] {
542
640
  let topLayer = -1;
543
641
 
544
642
  // Track dots per layer so we can render only the top layer's dots
545
- const dotsByLayer: Map<number, number> = new Map(); // layer → braille bits
643
+ const dotsByLayer: Map<number, number> = new Map(); // layer -> braille bits
546
644
  const colorByLayer: Map<number, Map<string, number>> = new Map();
645
+ const backgroundCounts: Map<string, number> = new Map();
547
646
 
548
647
  for (let dy = 0; dy < 4; dy++) {
549
648
  for (let dx = 0; dx < 2; dx++) {
550
649
  const px = col * 2 + dx;
551
650
  const py = row * 4 + dy;
552
651
  if (px >= buf.width || py >= buf.height) continue;
652
+ const bg = buf.backgrounds[py]?.[px] ?? null;
653
+ if (bg) {
654
+ backgroundCounts.set(bg, (backgroundCounts.get(bg) || 0) + 1);
655
+ }
553
656
  const pixel = buf.pixels[py]?.[px] ?? null;
554
657
  if (pixel) {
555
658
  const bit = BRAILLE_DOT[dy]![dx]!;
@@ -567,14 +670,19 @@ export function bufferToBrailleLines(buf: PixelBuffer): StyledContent[] {
567
670
  let char: string;
568
671
  let cellFg: string | undefined;
569
672
  let cellBg: string | undefined;
673
+ let bestBackgroundCount = 0;
674
+ for (const [bg, count] of backgroundCounts) {
675
+ if (count > bestBackgroundCount) {
676
+ bestBackgroundCount = count;
677
+ cellBg = bg;
678
+ }
679
+ }
570
680
 
571
681
  if (topLayer < 0) {
572
682
  char = " ";
573
683
  cellFg = undefined;
574
- cellBg = undefined;
575
684
  } else {
576
- // Only show dots from the highest layer — keeps lines thin in mixed
577
- // cells (e.g. area chart where line and fill overlap).
685
+ // Only show dots from the highest layer so lines stay thin in mixed cells.
578
686
  const pattern = dotsByLayer.get(topLayer) || 0;
579
687
  char = String.fromCharCode(BRAILLE_BASE + pattern);
580
688
 
@@ -586,7 +694,6 @@ export function bufferToBrailleLines(buf: PixelBuffer): StyledContent[] {
586
694
  }
587
695
 
588
696
  cellFg = topColor;
589
- cellBg = undefined;
590
697
  }
591
698
 
592
699
  if (char === runChar && cellFg === runFg && cellBg === runBg) {
@@ -792,6 +899,10 @@ function formatClockTime(date: Date, unit: AxisLabelUnit): string {
792
899
  }
793
900
  }
794
901
 
902
+ function formatFullCalendarDate(date: Date): string {
903
+ return `${MONTHS[date.getMonth()]} ${date.getDate()} ${date.getFullYear()}`;
904
+ }
905
+
795
906
  function isSameCalendarDay(a: Date, b: Date): boolean {
796
907
  return a.getFullYear() === b.getFullYear()
797
908
  && a.getMonth() === b.getMonth()
@@ -954,6 +1065,10 @@ function formatCursorTimeAxisValue(
954
1065
  const minGapMs = getMinPositiveGapMs(validDates);
955
1066
  const effectiveStepMs = Math.max(spanMs / Math.max(roughLabelCount - 1, 1), minGapMs || 0);
956
1067
  const unit = resolveAxisLabelUnit(effectiveStepMs);
1068
+ if (unit === "year" || unit === "month") {
1069
+ return formatFullCalendarDate(date);
1070
+ }
1071
+
957
1072
  const counterpart = isSameCalendarDay(first, last)
958
1073
  ? first
959
1074
  : isSameCalendarDay(date, first)
@@ -1121,6 +1236,7 @@ export interface RenderChartOptions {
1121
1236
  colors: ResolvedChartPalette;
1122
1237
  timeAxisDates?: Array<Date | string | number>;
1123
1238
  indicators?: ChartIndicatorOverlays | null;
1239
+ marketSession?: ChartMarketSession | null;
1124
1240
  }
1125
1241
 
1126
1242
  export interface ChartScene {
@@ -1133,6 +1249,7 @@ export interface ChartScene {
1133
1249
  mode: ChartRenderMode;
1134
1250
  colors: ResolvedChartPalette;
1135
1251
  indicators: ChartIndicatorOverlays | null;
1252
+ sessionBackgroundSpans: ChartSessionBackgroundSpan[];
1136
1253
  min: number;
1137
1254
  max: number;
1138
1255
  activeIdx: number;
@@ -1194,27 +1311,6 @@ export function getActivePointIndex(
1194
1311
  return bestIndex;
1195
1312
  }
1196
1313
 
1197
- function getIndicatorOverlayValues(indicators: ChartIndicatorOverlays | null | undefined): number[] {
1198
- if (!indicators) return [];
1199
-
1200
- const values: number[] = [];
1201
- for (const line of indicators.smaLines) {
1202
- values.push(...line.points.map((point) => point.value));
1203
- }
1204
- for (const line of indicators.emaLines) {
1205
- values.push(...line.points.map((point) => point.value));
1206
- }
1207
- if (indicators.bollinger) {
1208
- values.push(
1209
- ...indicators.bollinger.upper.map((point) => point.value),
1210
- ...indicators.bollinger.middle.map((point) => point.value),
1211
- ...indicators.bollinger.lower.map((point) => point.value),
1212
- );
1213
- }
1214
-
1215
- return values.filter((value) => Number.isFinite(value));
1216
- }
1217
-
1218
1314
  export function buildChartScene(
1219
1315
  points: ProjectedChartPoint[],
1220
1316
  opts: RenderChartOptions,
@@ -1222,15 +1318,14 @@ export function buildChartScene(
1222
1318
  if (points.length === 0) return null;
1223
1319
 
1224
1320
  const dimensions = normalizeRenderDimensions(opts);
1225
- const dataMin = opts.mode === "candles" || opts.mode === "ohlc"
1321
+ const dataMin = isHighLowMode(opts.mode)
1226
1322
  ? Math.min(...points.map((point) => point.low))
1227
1323
  : Math.min(...points.map((point) => point.close));
1228
- const dataMax = opts.mode === "candles" || opts.mode === "ohlc"
1324
+ const dataMax = isHighLowMode(opts.mode)
1229
1325
  ? Math.max(...points.map((point) => point.high))
1230
1326
  : Math.max(...points.map((point) => point.close));
1231
- const indicatorValues = getIndicatorOverlayValues(opts.indicators);
1232
- const min = indicatorValues.length > 0 ? Math.min(dataMin, ...indicatorValues) : dataMin;
1233
- const max = indicatorValues.length > 0 ? Math.max(dataMax, ...indicatorValues) : dataMax;
1327
+ const min = dataMin;
1328
+ const max = dataMax;
1234
1329
  const activeIdx = getActivePointIndex(points.length, dimensions.width, opts.cursorX, opts.mode);
1235
1330
  const activePoint = points[activeIdx]!;
1236
1331
  const range = max - min || 1;
@@ -1259,6 +1354,10 @@ export function buildChartScene(
1259
1354
  : max - (cursorY / Math.max(dimensions.chartRows - 1, 1)) * range;
1260
1355
 
1261
1356
  const timeAxisDates = opts.timeAxisDates ?? points.map((point) => point.date);
1357
+ const sessionBackgroundSpans = resolveExtendedHoursBackgroundSpans(
1358
+ points.map((point) => point.date),
1359
+ opts.marketSession,
1360
+ );
1262
1361
 
1263
1362
  return {
1264
1363
  points,
@@ -1270,6 +1369,7 @@ export function buildChartScene(
1270
1369
  mode: opts.mode,
1271
1370
  colors: opts.colors,
1272
1371
  indicators: opts.indicators ?? null,
1372
+ sessionBackgroundSpans,
1273
1373
  min,
1274
1374
  max,
1275
1375
  activeIdx,
@@ -1381,6 +1481,14 @@ export function renderChart(
1381
1481
  getAxisFractionDigitFloor(assetCategory, priceRange),
1382
1482
  )
1383
1483
  : null;
1484
+ drawSessionBackgrounds(
1485
+ buf,
1486
+ points,
1487
+ scene.sessionBackgroundSpans,
1488
+ 0,
1489
+ showVolume ? volDotBottom : chartDotBottom,
1490
+ palette,
1491
+ );
1384
1492
  drawGridLines(buf, gridLines.map((line) => line.y), palette.gridColor);
1385
1493
 
1386
1494
  switch (mode) {
@@ -1394,7 +1502,10 @@ export function renderChart(
1394
1502
  drawCandlestickChart(buf, points, 0, chartDotBottom, palette, min, max);
1395
1503
  break;
1396
1504
  case "ohlc":
1397
- drawOhlcChart(buf, points, 0, chartDotBottom, palette, min, max);
1505
+ drawOhlcChart(buf, points, 0, chartDotBottom, palette, min, max, "ohlc");
1506
+ break;
1507
+ case "hlc":
1508
+ drawOhlcChart(buf, points, 0, chartDotBottom, palette, min, max, "hlc");
1398
1509
  break;
1399
1510
  }
1400
1511
 
@@ -1406,7 +1517,7 @@ export function renderChart(
1406
1517
  volDotBottom,
1407
1518
  palette.volumeUp,
1408
1519
  palette.volumeDown,
1409
- mode === "candles" || mode === "ohlc" ? "openClose" : "previousClose",
1520
+ isHighLowMode(mode) ? "openClose" : "previousClose",
1410
1521
  mode,
1411
1522
  );
1412
1523
  }
@@ -2,14 +2,15 @@ import type { PricePoint } from "../../types/financials";
2
2
 
3
3
  export type TimeRange = "1D" | "1W" | "1M" | "3M" | "6M" | "1Y" | "5Y" | "ALL";
4
4
  export type ChartResolution = "auto" | "1m" | "5m" | "15m" | "30m" | "45m" | "1h" | "1d" | "1wk" | "1mo";
5
- export type ChartRenderMode = "area" | "line" | "candles" | "ohlc";
5
+ export type ChartRenderMode = "area" | "line" | "candles" | "ohlc" | "hlc";
6
6
  export type ChartAxisMode = "price" | "percent";
7
7
  export type ChartRendererPreference = "auto" | "kitty" | "braille";
8
8
  export type ResolvedChartRenderer = "kitty" | "braille";
9
9
  export type ComparisonChartRenderMode = "area" | "line";
10
+ export type ChartSessionBackgroundKind = "pre" | "post";
10
11
 
11
12
  export const TIME_RANGES: TimeRange[] = ["1D", "1W", "1M", "3M", "6M", "1Y", "5Y", "ALL"];
12
- export const CHART_RENDER_MODES: ChartRenderMode[] = ["area", "line", "candles", "ohlc"];
13
+ export const CHART_RENDER_MODES: ChartRenderMode[] = ["area", "line", "candles", "ohlc", "hlc"];
13
14
  export const CHART_RENDERER_PREFERENCES: ChartRendererPreference[] = ["auto", "kitty", "braille"];
14
15
  export const COMPARISON_RENDER_MODES: ComparisonChartRenderMode[] = ["area", "line"];
15
16
 
@@ -46,6 +47,7 @@ export interface PixelBuffer {
46
47
  width: number;
47
48
  height: number; // virtual pixel rows (4x terminal rows for braille rendering)
48
49
  pixels: (Pixel | null)[][]; // [y][x] = pixel with color and layer, or null
50
+ backgrounds: (string | null)[][];
49
51
  }
50
52
 
51
53
  export interface ChartColors {
@@ -59,6 +61,22 @@ export interface ChartColors {
59
61
  axisColor: string;
60
62
  activeRangeColor: string;
61
63
  inactiveRangeColor: string;
64
+ preMarketBgColor: string;
65
+ postMarketBgColor: string;
66
+ }
67
+
68
+ export interface ChartMarketSession {
69
+ timeZone: string;
70
+ regularStartMinutes: number;
71
+ regularEndMinutes: number;
72
+ preMarketStartMinutes: number;
73
+ postMarketEndMinutes: number;
74
+ }
75
+
76
+ export interface ChartSessionBackgroundSpan {
77
+ kind: ChartSessionBackgroundKind;
78
+ startIndex: number;
79
+ endIndex: number;
62
80
  }
63
81
 
64
82
  export interface VisibleWindow {