gloomberb 0.15.0 → 0.15.2

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 (189) hide show
  1. package/bin/gloomberb +9 -1
  2. package/package.json +1 -1
  3. package/src/api-client/central-bank-rates.ts +3 -0
  4. package/src/api-client/types.ts +1 -1
  5. package/src/app/pane-runtime/index.tsx +7 -5
  6. package/src/app/runtime/broker-import.test.tsx +89 -0
  7. package/src/app/runtime/broker-import.ts +29 -8
  8. package/src/brokers/sync-broker-instance.ts +67 -0
  9. package/src/cli/commands/overview.ts +6 -2
  10. package/src/cli/native-smoke.ts +2 -0
  11. package/src/components/chart/composite/composite-chart.tsx +21 -1
  12. package/src/components/chart/composite/format.ts +71 -8
  13. package/src/components/chart/composite/fx-precision.test.ts +21 -3
  14. package/src/components/chart/composite/scene.ts +13 -1
  15. package/src/components/chart/composite/types.ts +4 -0
  16. package/src/components/layout/market-summary.ts +4 -2
  17. package/src/components/ticker/badge/format.test.ts +3 -3
  18. package/src/components/ticker/badge/format.ts +11 -9
  19. package/src/components/ticker/badge/text.test.tsx +6 -6
  20. package/src/components/ui/index.ts +2 -1
  21. package/src/components/ui/multi-select/dialog.tsx +46 -4
  22. package/src/components/ui/number-prompt-dialog.tsx +71 -0
  23. package/src/core/state/app/state.ts +8 -2
  24. package/src/market-data/coordinator/events.ts +5 -0
  25. package/src/market-data/coordinator/index.ts +44 -10
  26. package/src/market-data/market/format.test.ts +54 -0
  27. package/src/market-data/market/format.ts +164 -5
  28. package/src/market-data/query-store.test.ts +87 -0
  29. package/src/market-data/query-store.ts +66 -9
  30. package/src/plugins/builtin/alerts/format.ts +4 -3
  31. package/src/plugins/builtin/analytics/display.ts +3 -3
  32. package/src/plugins/builtin/analytics/index.test.tsx +22 -22
  33. package/src/plugins/builtin/analytics/pane-model.test.ts +10 -10
  34. package/src/plugins/builtin/analytics/pane-model.ts +8 -8
  35. package/src/plugins/builtin/analytics/portfolio-identity.test.tsx +6 -6
  36. package/src/plugins/builtin/analytics/risk-client.ts +6 -8
  37. package/src/plugins/builtin/analytics/risk-model.test.ts +21 -2
  38. package/src/plugins/builtin/analytics/risk-model.ts +8 -0
  39. package/src/plugins/builtin/analytics/view.tsx +2 -2
  40. package/src/plugins/builtin/backtest/engine.test.ts +0 -1
  41. package/src/plugins/builtin/backtest/engine.ts +1 -4
  42. package/src/plugins/builtin/backtest/pane.tsx +1 -1
  43. package/src/plugins/builtin/buildout/detail/company.tsx +3 -3
  44. package/src/plugins/builtin/central-bank-rates/client.ts +2 -1
  45. package/src/plugins/builtin/central-bank-rates/headless.ts +2 -2
  46. package/src/plugins/builtin/central-bank-rates/model.test.ts +16 -1
  47. package/src/plugins/builtin/central-bank-rates/model.ts +4 -4
  48. package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -2
  49. package/src/plugins/builtin/chart-composer/pane.tsx +50 -3
  50. package/src/plugins/builtin/chart-composer/presets.test.ts +17 -0
  51. package/src/plugins/builtin/chart-composer/presets.ts +48 -0
  52. package/src/plugins/builtin/chart-composer/settings.ts +42 -11
  53. package/src/plugins/builtin/chat/chat.test.tsx +2 -2
  54. package/src/plugins/builtin/cloud/askg/pane.tsx +1 -1
  55. package/src/plugins/builtin/cloud/thesis/board-pane.test.tsx +25 -0
  56. package/src/plugins/builtin/cloud/thesis/board-pane.tsx +3 -2
  57. package/src/plugins/builtin/cloud/thesis/store.ts +2 -2
  58. package/src/plugins/builtin/congress-trades/detail.tsx +1 -1
  59. package/src/plugins/builtin/congress-trades/table.tsx +1 -1
  60. package/src/plugins/builtin/cot/client.ts +2 -1
  61. package/src/plugins/builtin/cot/model.test.ts +9 -0
  62. package/src/plugins/builtin/cot/model.ts +30 -1
  63. package/src/plugins/builtin/cot/pane.tsx +2 -4
  64. package/src/plugins/builtin/credit-conditions/client.ts +2 -1
  65. package/src/plugins/builtin/crypto-board/client.ts +2 -1
  66. package/src/plugins/builtin/crypto-board/model.test.ts +9 -0
  67. package/src/plugins/builtin/crypto-board/model.ts +11 -3
  68. package/src/plugins/builtin/custom-view/live-quotes.test.ts +6 -1
  69. package/src/plugins/builtin/custom-view/live-quotes.ts +26 -0
  70. package/src/plugins/builtin/custom-view/pane.tsx +18 -7
  71. package/src/plugins/builtin/custom-view/view-spec.test.ts +24 -6
  72. package/src/plugins/builtin/custom-view/view-spec.ts +53 -21
  73. package/src/plugins/builtin/debt-maturities/client.ts +16 -3
  74. package/src/plugins/builtin/debt-maturities/debt-maturities.test.ts +8 -0
  75. package/src/plugins/builtin/debt-maturities/headless.ts +3 -2
  76. package/src/plugins/builtin/debt-maturities/model.ts +2 -1
  77. package/src/plugins/builtin/debt-maturities/pane.tsx +0 -8
  78. package/src/plugins/builtin/earnings-calls/pane.test.tsx +14 -0
  79. package/src/plugins/builtin/earnings-calls/pane.tsx +3 -1
  80. package/src/plugins/builtin/econ/fred-client.test.ts +2 -0
  81. package/src/plugins/builtin/econ/fred-series-map.ts +0 -1
  82. package/src/plugins/builtin/econ/index.tsx +4 -2
  83. package/src/plugins/builtin/econ-statistics/pane.tsx +4 -2
  84. package/src/plugins/builtin/econ-statistics/stats.ts +29 -24
  85. package/src/plugins/builtin/econ-statistics/view.test.ts +16 -0
  86. package/src/plugins/builtin/equity-screener/headless.test.ts +22 -0
  87. package/src/plugins/builtin/equity-screener/headless.ts +18 -10
  88. package/src/plugins/builtin/estimate-revisions/client.ts +3 -1
  89. package/src/plugins/builtin/estimate-revisions/model.test.ts +7 -0
  90. package/src/plugins/builtin/estimate-revisions/model.ts +0 -5
  91. package/src/plugins/builtin/futures/table.tsx +4 -3
  92. package/src/plugins/builtin/futures-curve/client.ts +2 -1
  93. package/src/plugins/builtin/futures-curve/model.test.ts +6 -4
  94. package/src/plugins/builtin/futures-curve/model.ts +18 -14
  95. package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
  96. package/src/plugins/builtin/fx-matrix/index.tsx +21 -4
  97. package/src/plugins/builtin/fx-matrix/live-legs.ts +7 -0
  98. package/src/plugins/builtin/fx-matrix/pairs.test.ts +5 -0
  99. package/src/plugins/builtin/fx-matrix/pairs.ts +9 -3
  100. package/src/plugins/builtin/insider/index.tsx +15 -12
  101. package/src/plugins/builtin/insider/pane.test.tsx +24 -0
  102. package/src/plugins/builtin/market-movers/headless.ts +2 -2
  103. package/src/plugins/builtin/market-movers/model.ts +23 -10
  104. package/src/plugins/builtin/market-movers/screener.test.ts +5 -1
  105. package/src/plugins/builtin/market-movers/screener.ts +3 -0
  106. package/src/plugins/builtin/market-movers/table.ts +2 -2
  107. package/src/plugins/builtin/market-valuation/client.test.ts +3 -0
  108. package/src/plugins/builtin/market-valuation/client.ts +6 -1
  109. package/src/plugins/builtin/money-markets/client.ts +2 -1
  110. package/src/plugins/builtin/money-markets/pane.tsx +0 -1
  111. package/src/plugins/builtin/options/expiry-selection.test.tsx +1 -1
  112. package/src/plugins/builtin/options/footer.test.ts +2 -2
  113. package/src/plugins/builtin/options/footer.ts +2 -2
  114. package/src/plugins/builtin/options/market-reference.test.ts +2 -2
  115. package/src/plugins/builtin/options/market-reference.ts +14 -2
  116. package/src/plugins/builtin/options/table.ts +5 -8
  117. package/src/plugins/builtin/options/view.test.tsx +2 -2
  118. package/src/plugins/builtin/options/view.tsx +1 -1
  119. package/src/plugins/builtin/options-calculator/pane.test.tsx +6 -6
  120. package/src/plugins/builtin/options-calculator/pane.tsx +23 -8
  121. package/src/plugins/builtin/options-calculator/surface.test.ts +16 -1
  122. package/src/plugins/builtin/options-calculator/surface.ts +13 -6
  123. package/src/plugins/builtin/portfolio-list/column-values.ts +17 -9
  124. package/src/plugins/builtin/portfolio-list/index.test.ts +5 -5
  125. package/src/plugins/builtin/portfolio-list/metrics.test.ts +8 -8
  126. package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +12 -12
  127. package/src/plugins/builtin/portfolio-list/position-cost-integrity.test.ts +2 -2
  128. package/src/plugins/builtin/portfolio-list/summary/index.tsx +7 -6
  129. package/src/plugins/builtin/rate-path/client.ts +2 -1
  130. package/src/plugins/builtin/rate-path/headless.ts +1 -1
  131. package/src/plugins/builtin/rate-path/pane.tsx +0 -1
  132. package/src/plugins/builtin/realized-vol/client.test.ts +7 -0
  133. package/src/plugins/builtin/realized-vol/headless.ts +1 -1
  134. package/src/plugins/builtin/realized-vol/model.test.ts +16 -4
  135. package/src/plugins/builtin/realized-vol/model.ts +24 -9
  136. package/src/plugins/builtin/relative-rotation/client.ts +2 -1
  137. package/src/plugins/builtin/research/relative-valuation-pane.tsx +3 -10
  138. package/src/plugins/builtin/risk-factors/data.test.ts +7 -0
  139. package/src/plugins/builtin/risk-factors/data.ts +8 -1
  140. package/src/plugins/builtin/risk-factors/pane.test.tsx +3 -0
  141. package/src/plugins/builtin/shared/screener-live-quotes.test.ts +3 -3
  142. package/src/plugins/builtin/shared/screener-live-quotes.ts +16 -1
  143. package/src/plugins/builtin/short-volume/client.ts +2 -1
  144. package/src/plugins/builtin/short-volume/pane.tsx +0 -1
  145. package/src/plugins/builtin/thirteenf/headless.ts +3 -3
  146. package/src/plugins/builtin/thirteenf/model.ts +14 -4
  147. package/src/plugins/builtin/thirteenf/overlap-pane.tsx +12 -5
  148. package/src/plugins/builtin/thirteenf/overlap.test.ts +13 -1
  149. package/src/plugins/builtin/thirteenf/overlap.ts +31 -3
  150. package/src/plugins/builtin/thirteenf/pane.test.tsx +2 -1
  151. package/src/plugins/builtin/thirteenf/pane.tsx +12 -9
  152. package/src/plugins/builtin/thirteenf/table.ts +9 -3
  153. package/src/plugins/builtin/ticker-detail/financials/model.ts +0 -12
  154. package/src/plugins/builtin/ticker-detail/headless-schema.ts +4 -1
  155. package/src/plugins/builtin/ticker-detail/index.test.tsx +1 -1
  156. package/src/plugins/builtin/ticker-detail/overview/components.tsx +9 -10
  157. package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
  158. package/src/plugins/builtin/ticker-detail/overview-tab.tsx +6 -9
  159. package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +3 -2
  160. package/src/plugins/builtin/time-sales/model.test.ts +17 -1
  161. package/src/plugins/builtin/time-sales/model.ts +14 -3
  162. package/src/plugins/builtin/time-sales/pane.tsx +8 -4
  163. package/src/plugins/builtin/treasury-auctions/pane.tsx +2 -2
  164. package/src/plugins/builtin/vol-surface/client.ts +4 -1
  165. package/src/plugins/builtin/vol-surface/model.test.ts +4 -2
  166. package/src/plugins/builtin/vol-surface/model.ts +14 -2
  167. package/src/plugins/builtin/vol-surface/pane.tsx +9 -2
  168. package/src/plugins/builtin/volatility/client.test.ts +13 -8
  169. package/src/plugins/builtin/volatility/client.ts +3 -2
  170. package/src/plugins/builtin/volatility/headless.test.ts +2 -2
  171. package/src/plugins/builtin/volatility/model.test.ts +4 -0
  172. package/src/plugins/builtin/volatility/model.ts +6 -7
  173. package/src/renderers/electrobun/bun/desktop/backend-requests.ts +7 -2
  174. package/src/renderers/electrobun/bun/desktop/workspace/index.test.ts +33 -1
  175. package/src/renderers/electrobun/bun/desktop/workspace/index.ts +34 -1
  176. package/src/renderers/electrobun/bun/index.ts +2 -1
  177. package/src/renderers/opentui/render-timing-smoke.ts +20 -0
  178. package/src/sources/provider-router/profile-enrichment.test.ts +0 -3
  179. package/src/sources/yahoo-finance.test.ts +5 -1
  180. package/src/sources/yahoo-finance.ts +2 -1
  181. package/src/time-series/forward-valuation.ts +6 -0
  182. package/src/time-series/resolve.ts +5 -1
  183. package/src/time-series/valuation-currency.test.ts +2 -0
  184. package/src/time-series/valuation-currency.ts +3 -1
  185. package/src/utils/exchange-rate-snapshot.test.ts +12 -1
  186. package/src/utils/exchange-rate-snapshot.ts +3 -2
  187. package/src/utils/format.ts +55 -20
  188. package/src/utils/fx-market-hours.test.ts +28 -0
  189. package/src/utils/fx-market-hours.ts +42 -0
@@ -22,6 +22,10 @@ export interface MarketFormatOptions extends AssetDisplayContext {
22
22
  fixedFractionDigits?: number;
23
23
  /** Source units per displayed currency unit; a price quoted in pence keeps its pence decimals in pounds. */
24
24
  quotedUnitDivisor?: number;
25
+ /** A price that holds still for the session, such as the previous close. With
26
+ * `fixedFractionDigits`, the kind's decimal ceiling is read at this price
27
+ * instead of at the value, so a live price keeps its decimals as it moves. */
28
+ referencePrice?: number;
25
29
  }
26
30
 
27
31
  /** Current price fields use the quote's source metadata; stored cost/mark
@@ -117,6 +121,11 @@ function formatVariableNumber(
117
121
  if (fitsWidth(formatted, maxWidth)) return formatted;
118
122
  }
119
123
  }
124
+ // A cell too narrow for the minimum gives up decimals one at a time.
125
+ for (let decimals = minimumFractionDigits - 1; decimals > 0; decimals -= 1) {
126
+ const formatted = getNumberFormatter(decimals, decimals, false).format(value);
127
+ if (fitsWidth(formatted, maxWidth)) return formatted;
128
+ }
120
129
 
121
130
  return getNumberFormatter(0, 0, false).format(value);
122
131
  }
@@ -194,7 +203,11 @@ function formatPriceNumber(value: number, decimals: number, maxWidth: number | u
194
203
  ? "0"
195
204
  : formatVariableNumber(value, Math.max(decimals, tinyDecimals), tinyDecimals > 0 ? undefined : maxWidth, minimumDecimals);
196
205
  if (value === 0 || !Number.isFinite(value) || (Number(rendered.replaceAll(",", "")) !== 0 && fitsWidth(rendered, maxWidth))) return rendered;
197
- // A constrained cell must not imply a worthless asset or unchanged price.
206
+ return scientificPrice(value, maxWidth);
207
+ }
208
+
209
+ /** A constrained cell must not imply a worthless asset or unchanged price. */
210
+ function scientificPrice(value: number, maxWidth: number | undefined): string {
198
211
  for (let precision = 4; precision >= 1; precision -= 1) {
199
212
  const scientific = Number(value.toPrecision(precision)).toExponential();
200
213
  if (fitsWidth(scientific, maxWidth)) return scientific;
@@ -310,11 +323,21 @@ export function formatMarketPrice(value: number | undefined, options: MarketForm
310
323
  const quotedUnitDigits = kind === "equity" || kind === "other" ? quotedUnitFractionDigits(options.quotedUnitDivisor) : 0;
311
324
  const fixedFractionDigits = options.fixedFractionDigits;
312
325
  if (fixedFractionDigits !== undefined) {
326
+ const reference = positiveMagnitude(options.referencePrice);
313
327
  const maxFractionDigits = kind === "other" && options.priceRange !== undefined
314
328
  ? 6
315
- : getBasePriceMaxFractionDigits(kind, value) + quotedUnitDigits;
316
- const clampedFixedFractionDigits = Math.max(0, Math.min(fixedFractionDigits, maxFractionDigits));
317
- return formatVariableNumber(value, clampedFixedFractionDigits, options.maxWidth, clampedFixedFractionDigits);
329
+ : reference === undefined
330
+ ? getBasePriceMaxFractionDigits(kind, value) + quotedUnitDigits
331
+ : marketPriceFractionDigitCeiling(reference, options) + quotedUnitDigits;
332
+ let digits = Math.max(0, Math.min(fixedFractionDigits, maxFractionDigits));
333
+ // A live price keeps its instrument's decimals, but a trade finer than the
334
+ // session prices showed (a half-yen print after whole-yen closes) is not
335
+ // rounded to a price that never traded.
336
+ if (reference !== undefined) digits = Math.max(digits, writtenFractionDigits(Math.abs(value), maxFractionDigits));
337
+ const fitted = formatVariableNumber(value, digits, options.maxWidth, digits);
338
+ return hasNonZeroDigit(fitted) || Number(Math.abs(value).toFixed(digits)) === 0
339
+ ? fitted
340
+ : scientificPrice(value, options.maxWidth);
318
341
  }
319
342
  const minimumFractionDigits = Math.max(
320
343
  0,
@@ -344,6 +367,13 @@ export function formatMarketCost(value: number | undefined, options: MarketForma
344
367
  export function formatSignedMarketPrice(value: number | undefined, options: MarketFormatOptions = {}): string {
345
368
  if (value === undefined || value === null || Number.isNaN(value)) return "—";
346
369
  if (resolvePriceBasis(options.priceBasis, options.assetCategory) === null) return "—";
370
+ if (options.fixedFractionDigits !== undefined) {
371
+ // A move at the instrument's fixed decimals: the sign always has its column,
372
+ // and a move that rounds to zero is unsigned (0.00, never +0.00 or -0.00).
373
+ const maxWidth = options.maxWidth == null ? undefined : Math.max(1, options.maxWidth - 1);
374
+ const body = formatMarketPrice(Math.abs(value), { ...options, maxWidth });
375
+ return hasNonZeroDigit(body) ? `${value > 0 ? "+" : "-"}${body}` : body;
376
+ }
347
377
  if (value > 0) {
348
378
  const maxWidth = options.maxWidth == null ? undefined : Math.max(1, options.maxWidth - 1);
349
379
  return `+${formatMarketPrice(value, { ...options, maxWidth })}`;
@@ -363,6 +393,11 @@ export function formatMarketChangeWithCurrency(
363
393
  ): string {
364
394
  if (value == null || !Number.isFinite(value)) return "—";
365
395
  if (resolvePriceBasis(options.priceBasis, options.assetCategory) !== "per-unit") return formatSignedMarketPrice(value, options);
396
+ if (options.fixedFractionDigits !== undefined) {
397
+ const maxWidth = options.maxWidth == null ? undefined : Math.max(1, options.maxWidth - 1);
398
+ const body = formatMarketPriceWithCurrency(Math.abs(value), currency, { ...options, maxWidth });
399
+ return hasNonZeroDigit(body) ? `${value > 0 ? "+" : "-"}${body}` : body;
400
+ }
366
401
  if (resolveAssetDisplayKind(options) === "contract" || quotedUnitFractionDigits(options.quotedUnitDivisor) > 0) {
367
402
  return `${value > 0 ? "+" : ""}${formatMarketPriceWithCurrency(value, currency, {
368
403
  ...options, minimumFractionDigits: Math.max(2, options.minimumFractionDigits ?? 0),
@@ -396,6 +431,130 @@ export function withCurrencyMinorDigits(options: MarketFormatOptions, currency:
396
431
  return { ...options, minimumFractionDigits: Math.max(options.minimumFractionDigits ?? 0, Math.min(2, currencyMinorDigits(currency))) };
397
432
  }
398
433
 
434
+ function positiveMagnitude(value: number | null | undefined): number | undefined {
435
+ return value != null && Number.isFinite(value) && value !== 0 ? Math.abs(value) : undefined;
436
+ }
437
+
438
+ function hasNonZeroDigit(text: string): boolean {
439
+ return /[1-9]/.test(text);
440
+ }
441
+
442
+ /** Decimals a price was written with, ignoring binary noise: 6012.25 is 2, so
443
+ * is a provider's float32 157.8800048828125, and a change of
444
+ * 0.00004999999999988347 is 5. */
445
+ function writtenFractionDigits(value: number, cap: number): number {
446
+ const float32 = Math.fround(value) === value;
447
+ for (let digits = 0; digits < cap; digits += 1) {
448
+ const written = Number(value.toFixed(digits));
449
+ if (Math.abs(written - value) <= 1e-9 * value) return digits;
450
+ if (float32 && Math.fround(written) === value) return digits;
451
+ }
452
+ return cap;
453
+ }
454
+
455
+ const OPTION_CONTRACT_TYPES = new Set(["OPT", "OPTION", "OPTIONS", "FOP"]);
456
+
457
+ export interface StablePriceContext extends AssetDisplayContext {
458
+ /** The price's currency; its minor unit sets an equity's decimals (USD 2, JPY 0). */
459
+ currency?: string;
460
+ quotedUnitDivisor?: number;
461
+ /** A price that holds still for the session, such as the previous close.
462
+ * Never the live price: a precision read from each tick is what makes the
463
+ * digits jump. */
464
+ referencePrice?: number;
465
+ /** Session-fixed prices as the feed wrote them (previous close, open).
466
+ * Their decimals can ask for more than the default, such as a half-yen
467
+ * Tokyo line. */
468
+ sessionPrices?: readonly (number | null | undefined)[];
469
+ }
470
+
471
+ /**
472
+ * The decimals a live price keeps for its instrument, whatever the tick: an
473
+ * equity above $1 always shows cents (150.10, not 150.1), and a coin, pair or
474
+ * contract takes its count from the reference price, never from the latest
475
+ * trade, so crossing $100 or 1.0000 on a tick changes nothing. A floor per kind
476
+ * is raised to the decimals the reference and session prices were written
477
+ * with (a half-yen close, a 5-decimal FX feed), up to the kind's ceiling.
478
+ * Changes, bid, ask and ranges of the same instrument use the same count.
479
+ */
480
+ export function stablePriceFractionDigits(context: StablePriceContext): number {
481
+ const reference = positiveMagnitude(context.referencePrice);
482
+ const kind = resolveAssetDisplayKind(context);
483
+ const unitDigits = kind === "equity" || kind === "other" ? quotedUnitFractionDigits(context.quotedUnitDivisor) : 0;
484
+ const ceiling = reference === undefined ? 8 : marketPriceFractionDigitCeiling(reference, context) + unitDigits;
485
+ const below = (limit: number) => reference !== undefined && reference < limit;
486
+ // Four significant digits for a sub-unit price.
487
+ const subUnitDigits = reference === undefined ? 4 : Math.min(8, Math.max(4, 3 - Math.floor(Math.log10(reference))));
488
+ let digits: number;
489
+ if (resolvePriceBasis(context.priceBasis, context.assetCategory) === "percent-of-par") {
490
+ digits = 2;
491
+ } else if (kind === "equity" || kind === "other") {
492
+ // A sub-unit price keeps four decimals of the major unit, pence lines
493
+ // included; above one unit the currency's minor unit (and pence) apply.
494
+ digits = below(1) ? 4 : Math.min(2, currencyMinorDigits(context.currency)) + unitDigits;
495
+ } else if (kind === "cash") {
496
+ digits = below(10) ? 4 : 2;
497
+ } else if (kind === "crypto") {
498
+ digits = below(1) ? subUnitDigits : below(100) ? 4 : 2;
499
+ } else if (kind === "contract") {
500
+ const type = normalizeType(context.contractSecType || context.assetCategory);
501
+ digits = OPTION_CONTRACT_TYPES.has(type) || !CONTRACT_TYPES.has(type) || !below(1) ? 2 : subUnitDigits;
502
+ } else {
503
+ digits = 2;
504
+ }
505
+ if (kind !== "crypto" && reference !== undefined) digits = Math.max(digits, tinyPriceFractionDigits(reference));
506
+ for (const price of context.sessionPrices ?? []) {
507
+ const magnitude = positiveMagnitude(price);
508
+ if (magnitude !== undefined) digits = Math.max(digits, writtenFractionDigits(magnitude, ceiling));
509
+ }
510
+ return digits;
511
+ }
512
+
513
+ type LiveQuotePrices = Pick<Quote, "price" | "previousClose" | "regularClose" | "open">;
514
+
515
+ /** The session-fixed price a quote's decimals are chosen from. */
516
+ export function quoteReferencePrice(quote: Partial<LiveQuotePrices> | null | undefined): number | undefined {
517
+ return positiveMagnitude(quote?.previousClose)
518
+ ?? positiveMagnitude(quote?.regularClose)
519
+ ?? positiveMagnitude(quote?.open)
520
+ ?? positiveMagnitude(quote?.price);
521
+ }
522
+
523
+ /**
524
+ * Pins a live price's options to its instrument's decimals (see
525
+ * stablePriceFractionDigits). `prices` supplies the session-fixed prices, from
526
+ * the quote when there is one or a stored mark when there is not.
527
+ */
528
+ export function withStablePriceDigits(
529
+ options: MarketFormatOptions,
530
+ currency: string | undefined,
531
+ prices: Partial<LiveQuotePrices> | null | undefined,
532
+ ): MarketFormatOptions {
533
+ if (resolvePriceBasis(options.priceBasis, options.assetCategory) === null) return options;
534
+ // Without any session price, the live price can only give the magnitude.
535
+ const referencePrice = quoteReferencePrice(prices);
536
+ return {
537
+ ...options,
538
+ referencePrice,
539
+ fixedFractionDigits: stablePriceFractionDigits({
540
+ ...options,
541
+ currency,
542
+ referencePrice,
543
+ sessionPrices: [prices?.previousClose, prices?.regularClose, prices?.open],
544
+ }),
545
+ };
546
+ }
547
+
548
+ /** Options for a quote's live prices and changes: its instrument's fixed decimals, whatever the tick. */
549
+ export function liveQuoteFormatOptions(
550
+ quote: (Pick<Quote, "instrumentType" | "priceBasis" | "providerPriceDivisor"> & Partial<LiveQuotePrices>) | null | undefined,
551
+ currency: string | undefined,
552
+ fallbackAssetCategory?: string,
553
+ metadataInstrumentType?: string,
554
+ ): MarketFormatOptions {
555
+ return withStablePriceDigits(quoteFormatOptions(quote, fallbackAssetCategory, metadataInstrumentType), currency, quote);
556
+ }
557
+
399
558
  export function formatMarketPriceWithCurrency(
400
559
  value: number | undefined,
401
560
  currency = "USD",
@@ -410,7 +569,7 @@ export function formatMarketPriceWithCurrency(
410
569
  ? undefined
411
570
  : Math.max(1, options.maxWidth - sign.length - symbol.length);
412
571
  const body = formatMarketPrice(Math.abs(value), { ...options, maxWidth: numericWidth });
413
- return `${sign}${symbol}${body}`;
572
+ return `${hasNonZeroDigit(body) ? sign : ""}${symbol}${body}`;
414
573
  }
415
574
 
416
575
  export function formatMarketCostWithCurrency(
@@ -0,0 +1,87 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { QueryStore } from "./query-store";
3
+ import type { QueryEntry } from "./result-types";
4
+
5
+ function ready(value: string): QueryEntry<string> {
6
+ return {
7
+ phase: "ready",
8
+ data: value,
9
+ lastGoodData: value,
10
+ source: "test",
11
+ fetchedAt: 0,
12
+ staleAt: null,
13
+ error: null,
14
+ attempts: [],
15
+ };
16
+ }
17
+
18
+ describe("QueryStore retention", () => {
19
+ it("keeps every entry when no ceiling is configured", () => {
20
+ const store = new QueryStore<string>(() => {});
21
+ for (let index = 0; index < 500; index += 1) store.set(`k${index}`, ready(`v${index}`));
22
+ expect(store.size).toBe(500);
23
+ });
24
+
25
+ it("drops the oldest unwatched entries once past the ceiling", () => {
26
+ const store = new QueryStore<string>(() => {}, { maxRetainedEntries: 3 });
27
+ for (const key of ["a", "b", "c", "d", "e"]) store.set(key, ready(key));
28
+
29
+ expect(store.size).toBe(3);
30
+ expect(store.get("a").phase).toBe("idle");
31
+ expect(store.get("b").phase).toBe("idle");
32
+ expect(store.get("e").data).toBe("e");
33
+ });
34
+
35
+ it("never evicts a key a pane is watching, even past the ceiling", () => {
36
+ const watched = new Set(["a", "b", "c", "d"]);
37
+ const store = new QueryStore<string>(() => {}, {
38
+ maxRetainedEntries: 2,
39
+ isWatched: (key) => watched.has(key),
40
+ });
41
+ for (const key of ["a", "b", "c", "d"]) store.set(key, ready(key));
42
+
43
+ // Four visible panes outrank a ceiling of two: blanking one would be worse
44
+ // than holding the memory.
45
+ expect(store.size).toBe(4);
46
+
47
+ // Once they unmount, the next write collects them.
48
+ watched.clear();
49
+ store.set("e", ready("e"));
50
+ expect(store.size).toBe(2);
51
+ expect(store.get("e").data).toBe("e");
52
+ });
53
+
54
+ it("evicts around watched keys rather than stopping at the first one", () => {
55
+ const store = new QueryStore<string>(() => {}, {
56
+ maxRetainedEntries: 2,
57
+ isWatched: (key) => key === "a",
58
+ });
59
+ for (const key of ["a", "b", "c", "d"]) store.set(key, ready(key));
60
+
61
+ expect(store.size).toBe(2);
62
+ expect(store.get("a").data).toBe("a");
63
+ expect(store.get("d").data).toBe("d");
64
+ });
65
+
66
+ it("reports every evicted key so derived caches can drop it too", () => {
67
+ const evicted: string[] = [];
68
+ const store = new QueryStore<string>(() => {}, {
69
+ maxRetainedEntries: 2,
70
+ onEvict: (key) => evicted.push(key),
71
+ });
72
+ for (const key of ["a", "b", "c", "d"]) store.set(key, ready(key));
73
+
74
+ expect(evicted).toEqual(["a", "b"]);
75
+ });
76
+
77
+ it("returns an evicted key to idle so the next read refetches", () => {
78
+ const store = new QueryStore<string>(() => {}, { maxRetainedEntries: 1 });
79
+ store.set("a", ready("a"));
80
+ store.set("b", ready("b"));
81
+
82
+ const evicted = store.get("a");
83
+ expect(evicted.phase).toBe("idle");
84
+ expect(evicted.data).toBeNull();
85
+ expect(evicted.lastGoodData).toBeNull();
86
+ });
87
+ });
@@ -1,16 +1,35 @@
1
1
  import type { QueryEntry } from "./result-types";
2
2
  import { createIdleEntry } from "./result-types";
3
3
 
4
- export class QueryStore<T> {
5
- private readonly entries = new Map<string, QueryEntry<T>>();
4
+ /** How many entries a store keeps once nothing is watching them. */
5
+ export interface QueryStoreRetention {
6
+ /**
7
+ * Entries to keep once nothing on screen is watching them. Browsing a ticker
8
+ * adds an entry to every store its panes touch, and nothing used to remove
9
+ * one, so a session that researched a few hundred symbols held every price
10
+ * history, statement set and filing it had ever shown until exit (#452).
11
+ */
12
+ maxRetainedEntries?: number;
13
+ /** True while a mounted pane is subscribed to this key, so it must be kept. */
14
+ isWatched?: (key: string) => boolean;
15
+ /** Called for each evicted key, for anything derived from it and keyed alike. */
16
+ onEvict?: (key: string) => void;
17
+ }
6
18
 
19
+ export interface QueryStoreOptions<T> extends QueryStoreRetention {
7
20
  /**
8
- * `project` sees every entry before it is stored, so a store can overlay a
9
- * newer source (a streamed FX rate over a loaded one) on whatever writes it.
21
+ * Sees every entry before it is stored, so a store can overlay a newer
22
+ * source (a streamed FX rate over a loaded one) on whatever writes it.
10
23
  */
24
+ project?: (key: string, entry: QueryEntry<T>) => QueryEntry<T>;
25
+ }
26
+
27
+ export class QueryStore<T> {
28
+ private readonly entries = new Map<string, QueryEntry<T>>();
29
+
11
30
  constructor(
12
31
  private readonly onChange: (key: string) => void,
13
- private readonly project?: (key: string, entry: QueryEntry<T>) => QueryEntry<T>,
32
+ private readonly options: QueryStoreOptions<T> = {},
14
33
  ) {}
15
34
 
16
35
  get(key: string): QueryEntry<T> {
@@ -18,15 +37,53 @@ export class QueryStore<T> {
18
37
  }
19
38
 
20
39
  set(key: string, entry: QueryEntry<T>): void {
21
- this.entries.set(key, this.project ? this.project(key, entry) : entry);
40
+ this.store(key, entry);
22
41
  this.onChange(key);
23
42
  }
24
43
 
25
44
  update(key: string, updater: (current: QueryEntry<T>) => QueryEntry<T>): QueryEntry<T> {
26
- const updated = updater(this.get(key));
27
- const next = this.project ? this.project(key, updated) : updated;
28
- this.entries.set(key, next);
45
+ const next = this.store(key, updater(this.get(key)));
29
46
  this.onChange(key);
30
47
  return next;
31
48
  }
49
+
50
+ /**
51
+ * Only a new key can push the store over its ceiling, so only a new key
52
+ * runs the eviction scan. Streamed updates rewrite existing keys many times
53
+ * a second and skip it.
54
+ */
55
+ private store(key: string, entry: QueryEntry<T>): QueryEntry<T> {
56
+ const next = this.options.project ? this.options.project(key, entry) : entry;
57
+ const added = !this.entries.has(key);
58
+ this.entries.set(key, next);
59
+ if (added) this.evictUnwatched();
60
+ return next;
61
+ }
62
+
63
+ /** Entries held right now. */
64
+ get size(): number {
65
+ return this.entries.size;
66
+ }
67
+
68
+ /**
69
+ * Drop the oldest entries no pane is watching. A watched key is never
70
+ * evicted, so this cannot blank a visible pane; an evicted key returns to
71
+ * idle and refetches the next time something asks for it.
72
+ *
73
+ * ponytail: oldest-first over insertion order rather than true LRU. Reading
74
+ * an entry does not renew it, so a key held open only by a long read with no
75
+ * subscription can still age out and refetch. Move to touch-on-read if that
76
+ * refetch ever shows up in a profile.
77
+ */
78
+ private evictUnwatched(): void {
79
+ const limit = this.options.maxRetainedEntries;
80
+ if (limit === undefined || this.entries.size <= limit) return;
81
+ const isWatched = this.options.isWatched;
82
+ for (const key of [...this.entries.keys()]) {
83
+ if (this.entries.size <= limit) return;
84
+ if (isWatched?.(key)) continue;
85
+ this.entries.delete(key);
86
+ this.options.onEvict?.(key);
87
+ }
88
+ }
32
89
  }
@@ -39,9 +39,10 @@ export function formatAlertDistance(alert: AlertRule): string {
39
39
  }
40
40
  const percent = ((alert.targetPrice - currentPrice) / currentPrice) * 100;
41
41
  if (!Number.isFinite(percent)) return "-";
42
- const abs = Math.abs(percent);
43
- const decimals = abs < 10 ? 1 : 0;
44
- return `${percent >= 0 ? "+" : ""}${percent.toFixed(decimals)}%`;
42
+ // One decimal at any distance, so the column holds still as the price moves;
43
+ // a distance that rounds to zero is unsigned (0.0%, never -0.0%).
44
+ const fixed = Math.abs(percent).toFixed(1);
45
+ return `${/[1-9]/.test(fixed) ? (percent > 0 ? "+" : "-") : ""}${fixed}%`;
45
46
  }
46
47
 
47
48
  export function formatQuoteChecked(alert: AlertRule): string {
@@ -1,5 +1,5 @@
1
1
  import { colors } from "../../../theme/colors";
2
- import { formatCompact } from "../../../utils/format";
2
+ import { formatCompactAmount } from "../../../utils/format";
3
3
 
4
4
  export function sharpeColor(sharpe: number): string {
5
5
  if (sharpe > 1) return colors.positive;
@@ -25,9 +25,9 @@ export function betaColor(beta: number): string {
25
25
  return colors.positive;
26
26
  }
27
27
 
28
+ // P&L moves with every streamed price; fixed decimals keep the percent detail after it still.
28
29
  export function formatSignedCompact(value: number | null): string {
29
- if (value == null || !Number.isFinite(value)) return "—";
30
- return `${value >= 0 ? "+" : ""}${formatCompact(value)}`;
30
+ return formatCompactAmount(value ?? undefined, { signed: true });
31
31
  }
32
32
 
33
33
  export function formatWeight(weight: number | null): string {
@@ -224,9 +224,9 @@ describe("PortfolioAnalyticsPane", () => {
224
224
  });
225
225
  for (let index = 0; index < 6; index++) await flushFrame();
226
226
  const frame = testSetup!.captureCharFrame();
227
- expect(frame).toContain(currency ? "Net Liq 15k" : "Net Liq —");
227
+ expect(frame).toContain(currency ? "Net Liq 15.0k" : "Net Liq —");
228
228
  expect(frame).toContain(currency ? "Cash 13.5k" : "Cash —");
229
- expect(frame).not.toContain("Net Liq 20k");
229
+ expect(frame).not.toContain("Net Liq 20.0k");
230
230
  if (currency) expect(requested).toContain("CAD");
231
231
  else expect(frame).toContain("FX Unavailable");
232
232
  });
@@ -253,17 +253,17 @@ describe("PortfolioAnalyticsPane", () => {
253
253
  for (let index = 0; index < 6; index++) await flushFrame();
254
254
  const frame = testSetup!.captureCharFrame();
255
255
  if (withAccount) {
256
- expect(frame).toContain("Net Liq 6k");
257
- expect(frame).toContain("Cash 6k");
256
+ expect(frame).toContain("Net Liq 6.0k");
257
+ expect(frame).toContain("Cash 6.0k");
258
258
  } else {
259
259
  expect(frame).not.toContain("Net Liq");
260
260
  expect(frame).not.toContain("Cash");
261
261
  expect(frame).not.toContain("Val 0");
262
- expect(frame).not.toContain("P&L +0");
262
+ expect(frame).not.toContain("P&L 0");
263
263
  }
264
264
  expect(frame).toContain("Broker return +10.00%");
265
- expect(frame).not.toContain("Day +0");
266
- expect(frame).not.toContain("P&L +0");
265
+ expect(frame).not.toContain("Day 0");
266
+ expect(frame).not.toContain("P&L 0");
267
267
  expect(frame).toContain("PORTFOLIO HISTORY");
268
268
  expect(frame).toContain("Value (USD)");
269
269
  expect(frame).not.toContain("CURRENT-WEIGHT BASKET ESTIMATES");
@@ -284,8 +284,8 @@ describe("PortfolioAnalyticsPane", () => {
284
284
  const frame = testSetup!.captureCharFrame();
285
285
  expect(frame).toContain("Source Cached");
286
286
  expect(frame).not.toContain("Val 0");
287
- expect(frame).not.toContain("Day +0");
288
- expect(frame).not.toContain("P&L +0");
287
+ expect(frame).not.toContain("Day 0");
288
+ expect(frame).not.toContain("P&L 0");
289
289
  expect(frame).not.toContain("Net Liq");
290
290
  expect(frame).not.toContain("Cash");
291
291
  });
@@ -317,7 +317,7 @@ describe("PortfolioAnalyticsPane", () => {
317
317
  expect(frame).toContain("Account history unavailable.");
318
318
  expect(frame).toContain("Statement service unavailable");
319
319
  expect(frame).not.toContain("No positions");
320
- expect(frame).not.toContain("P&L +0");
320
+ expect(frame).not.toContain("P&L 0");
321
321
 
322
322
  await act(async () => { testSetup!.mockInput.pressArrow("left"); });
323
323
  await flushFrame();
@@ -386,7 +386,7 @@ describe("PortfolioAnalyticsPane", () => {
386
386
  expect(frame).toContain("Main Portfolio");
387
387
  expect(frame).toContain("Flex DU12345");
388
388
  expect(frame).toContain("Val 1.3k");
389
- expect(frame).toContain("P&L +250 (+25.00%)");
389
+ expect(frame).toContain("P&L +250.00 (+25.00%)");
390
390
  expect(frame).toContain("CURRENT-WEIGHT BASKET ESTIMATES");
391
391
  expect(frame).toContain("Est. Sharpe");
392
392
  expect(frame).toContain("Beta (SPY)");
@@ -467,17 +467,17 @@ describe("PortfolioAnalyticsPane", () => {
467
467
  await flushFrame();
468
468
 
469
469
  const frame = testSetup!.captureCharFrame();
470
- expect(frame).toContain("Net Liq 125k");
470
+ expect(frame).toContain("Net Liq 125.0k");
471
471
  expect(frame).toContain("Val 113.6k");
472
472
  expect(frame).toContain("Margin Lev 0.9x");
473
- expect(frame).toContain("Cash -50k");
474
- expect(frame).toContain("Day +900");
475
- expect(frame).toContain("P&L +777");
476
- expect(frame).toContain("Realized -25");
477
- expect(frame).toContain("Settled -45k");
478
- expect(frame).toContain("Avail 15k");
479
- expect(frame).toContain("Excess 12k");
480
- expect(frame).toContain("BP 30k");
473
+ expect(frame).toContain("Cash -50.0k");
474
+ expect(frame).toContain("Day +900.00");
475
+ expect(frame).toContain("P&L +777.00");
476
+ expect(frame).toContain("Realized -25.00");
477
+ expect(frame).toContain("Settled -45.0k");
478
+ expect(frame).toContain("Avail 15.0k");
479
+ expect(frame).toContain("Excess 12.0k");
480
+ expect(frame).toContain("BP 30.0k");
481
481
  expect(frame).toContain("As Of Mar 26");
482
482
  expect(frame).toContain("Source Flex Mar 26");
483
483
  });
@@ -593,8 +593,8 @@ describe("PortfolioAnalyticsPane", () => {
593
593
 
594
594
  const frame = testSetup!.captureCharFrame();
595
595
  expect(frame).toContain("Val 1.4k");
596
- expect(frame).toContain("P&L +400 (+40.00%)");
597
- expect(frame).toContain("Technology 100.0% 1.4k +400 +40.00%");
596
+ expect(frame).toContain("P&L +400.00 (+40.00%)");
597
+ expect(frame).toContain("Technology 100.0% 1.4k +400.00 +40.00%");
598
598
  expect(frame).not.toContain("1.3k");
599
599
  });
600
600
  });
@@ -74,13 +74,13 @@ test("converts every account balance while keeping leverage independent of displ
74
74
  expect(rows(1).get("margin-leverage")).toBe("1.5x");
75
75
  const converted = rows(1.2);
76
76
  expect(converted.get("margin-leverage")).toBe("1.5x");
77
- expect(converted.get("net-liquidation")).toBe("120k");
78
- expect(converted.get("total-value")).toBe("180k");
79
- expect(converted.get("cash")).toBe("-60k");
80
- expect(converted.get("settled-cash")).toBe("-48k");
81
- expect(converted.get("available-funds")).toBe("36k");
82
- expect(converted.get("excess-liquidity")).toBe("24k");
83
- expect(converted.get("buying-power")).toBe("72k");
77
+ expect(converted.get("net-liquidation")).toBe("120.0k");
78
+ expect(converted.get("total-value")).toBe("180.0k");
79
+ expect(converted.get("cash")).toBe("-60.0k");
80
+ expect(converted.get("settled-cash")).toBe("-48.0k");
81
+ expect(converted.get("available-funds")).toBe("36.0k");
82
+ expect(converted.get("excess-liquidity")).toBe("24.0k");
83
+ expect(converted.get("buying-power")).toBe("72.0k");
84
84
 
85
85
  accountState.account.netLiquidation = 0;
86
86
  expect(rows(1).has("margin-leverage")).toBe(false);
@@ -103,10 +103,10 @@ test("cash-only summary uses reported account metrics and preserves explicit zer
103
103
  accountState.account.grossPositionValue = 0;
104
104
  accountState.account.dailyPnl = 0;
105
105
  accountState.account.unrealizedPnl = 0;
106
- expect(rows().get("total-value")?.value).toBe("0");
106
+ expect(rows().get("total-value")?.value).toBe("0.00");
107
107
  expect(rows().get("margin-leverage")?.value).toBe("0.0x");
108
- expect(rows().get("day-pnl")).toMatchObject({ value: "+0", detail: "(0.00%)" });
109
- expect(rows().get("pnl")).toMatchObject({ value: "+0", detail: "(—)" });
108
+ expect(rows().get("day-pnl")).toMatchObject({ value: "0.00", detail: "(0.00%)" });
109
+ expect(rows().get("pnl")).toMatchObject({ value: "0.00", detail: "(—)" });
110
110
  delete accountState.account.netLiquidation;
111
111
  expect(rows().get("day-pnl")?.detail).toBe("(—)");
112
112
  expect(rows().has("margin-leverage")).toBe(false);
@@ -3,7 +3,7 @@ import { colors, priceColor } from "../../../theme/colors";
3
3
  import type { TickerFinancials, PricePoint } from "../../../types/financials";
4
4
  import type { BrokerAccount, BrokerPortfolioPerformance } from "../../../types/trading";
5
5
  import type { Portfolio, TickerRecord } from "../../../types/ticker";
6
- import { formatCompact, formatNumber, formatPercentRaw } from "../../../utils/format";
6
+ import { formatCompact, formatCompactAmount, formatNumber, formatPercentRaw } from "../../../utils/format";
7
7
  import { formatRelativeAge } from "../../../utils/relative-time";
8
8
  import type { PriceHistoryIntegrity } from "../../../utils/price-history-integrity";
9
9
  import { instrumentFromTicker, type ChartRequest, type TickerInstrumentOptions } from "../../../market-data/request-types";
@@ -240,7 +240,7 @@ export function buildAnalyticsSummaryRows({
240
240
  rows.push({
241
241
  id: "net-liquidation",
242
242
  label: "Net Liq",
243
- value: formatCompact(netLiquidation),
243
+ value: formatCompactAmount(netLiquidation),
244
244
  color: colors.text,
245
245
  });
246
246
  }
@@ -249,7 +249,7 @@ export function buildAnalyticsSummaryRows({
249
249
  rows.push({
250
250
  id: "total-value",
251
251
  label: "Val",
252
- value: formatCompact(totalMarketValue),
252
+ value: formatCompactAmount(totalMarketValue),
253
253
  color: colors.text,
254
254
  });
255
255
  }
@@ -268,7 +268,7 @@ export function buildAnalyticsSummaryRows({
268
268
  rows.push({
269
269
  id: "cash",
270
270
  label: "Cash",
271
- value: formatCompact(convertAccountValue(account.totalCashValue)),
271
+ value: formatCompactAmount(convertAccountValue(account.totalCashValue)),
272
272
  color: colors.text,
273
273
  });
274
274
  }
@@ -319,7 +319,7 @@ export function buildAnalyticsSummaryRows({
319
319
  rows.push({
320
320
  id: "settled-cash",
321
321
  label: "Settled",
322
- value: formatCompact(convertAccountValue(account.settledCash)),
322
+ value: formatCompactAmount(convertAccountValue(account.settledCash)),
323
323
  color: colors.text,
324
324
  });
325
325
  }
@@ -327,7 +327,7 @@ export function buildAnalyticsSummaryRows({
327
327
  rows.push({
328
328
  id: "available-funds",
329
329
  label: "Avail",
330
- value: formatCompact(convertAccountValue(account.availableFunds)),
330
+ value: formatCompactAmount(convertAccountValue(account.availableFunds)),
331
331
  color: colors.text,
332
332
  });
333
333
  }
@@ -335,7 +335,7 @@ export function buildAnalyticsSummaryRows({
335
335
  rows.push({
336
336
  id: "excess-liquidity",
337
337
  label: "Excess",
338
- value: formatCompact(convertAccountValue(account.excessLiquidity)),
338
+ value: formatCompactAmount(convertAccountValue(account.excessLiquidity)),
339
339
  color: colors.text,
340
340
  });
341
341
  }
@@ -343,7 +343,7 @@ export function buildAnalyticsSummaryRows({
343
343
  rows.push({
344
344
  id: "buying-power",
345
345
  label: "BP",
346
- value: formatCompact(convertAccountValue(account.buyingPower)),
346
+ value: formatCompactAmount(convertAccountValue(account.buyingPower)),
347
347
  color: colors.text,
348
348
  });
349
349
  }