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.
- package/bin/gloomberb +9 -1
- package/package.json +1 -1
- package/src/api-client/central-bank-rates.ts +3 -0
- package/src/api-client/types.ts +1 -1
- package/src/app/pane-runtime/index.tsx +7 -5
- package/src/app/runtime/broker-import.test.tsx +89 -0
- package/src/app/runtime/broker-import.ts +29 -8
- package/src/brokers/sync-broker-instance.ts +67 -0
- package/src/cli/commands/overview.ts +6 -2
- package/src/cli/native-smoke.ts +2 -0
- package/src/components/chart/composite/composite-chart.tsx +21 -1
- package/src/components/chart/composite/format.ts +71 -8
- package/src/components/chart/composite/fx-precision.test.ts +21 -3
- package/src/components/chart/composite/scene.ts +13 -1
- package/src/components/chart/composite/types.ts +4 -0
- package/src/components/layout/market-summary.ts +4 -2
- package/src/components/ticker/badge/format.test.ts +3 -3
- package/src/components/ticker/badge/format.ts +11 -9
- package/src/components/ticker/badge/text.test.tsx +6 -6
- package/src/components/ui/index.ts +2 -1
- package/src/components/ui/multi-select/dialog.tsx +46 -4
- package/src/components/ui/number-prompt-dialog.tsx +71 -0
- package/src/core/state/app/state.ts +8 -2
- package/src/market-data/coordinator/events.ts +5 -0
- package/src/market-data/coordinator/index.ts +44 -10
- package/src/market-data/market/format.test.ts +54 -0
- package/src/market-data/market/format.ts +164 -5
- package/src/market-data/query-store.test.ts +87 -0
- package/src/market-data/query-store.ts +66 -9
- package/src/plugins/builtin/alerts/format.ts +4 -3
- package/src/plugins/builtin/analytics/display.ts +3 -3
- package/src/plugins/builtin/analytics/index.test.tsx +22 -22
- package/src/plugins/builtin/analytics/pane-model.test.ts +10 -10
- package/src/plugins/builtin/analytics/pane-model.ts +8 -8
- package/src/plugins/builtin/analytics/portfolio-identity.test.tsx +6 -6
- package/src/plugins/builtin/analytics/risk-client.ts +6 -8
- package/src/plugins/builtin/analytics/risk-model.test.ts +21 -2
- package/src/plugins/builtin/analytics/risk-model.ts +8 -0
- package/src/plugins/builtin/analytics/view.tsx +2 -2
- package/src/plugins/builtin/backtest/engine.test.ts +0 -1
- package/src/plugins/builtin/backtest/engine.ts +1 -4
- package/src/plugins/builtin/backtest/pane.tsx +1 -1
- package/src/plugins/builtin/buildout/detail/company.tsx +3 -3
- package/src/plugins/builtin/central-bank-rates/client.ts +2 -1
- package/src/plugins/builtin/central-bank-rates/headless.ts +2 -2
- package/src/plugins/builtin/central-bank-rates/model.test.ts +16 -1
- package/src/plugins/builtin/central-bank-rates/model.ts +4 -4
- package/src/plugins/builtin/central-bank-rates/pane.tsx +2 -2
- package/src/plugins/builtin/chart-composer/pane.tsx +50 -3
- package/src/plugins/builtin/chart-composer/presets.test.ts +17 -0
- package/src/plugins/builtin/chart-composer/presets.ts +48 -0
- package/src/plugins/builtin/chart-composer/settings.ts +42 -11
- package/src/plugins/builtin/chat/chat.test.tsx +2 -2
- package/src/plugins/builtin/cloud/askg/pane.tsx +1 -1
- package/src/plugins/builtin/cloud/thesis/board-pane.test.tsx +25 -0
- package/src/plugins/builtin/cloud/thesis/board-pane.tsx +3 -2
- package/src/plugins/builtin/cloud/thesis/store.ts +2 -2
- package/src/plugins/builtin/congress-trades/detail.tsx +1 -1
- package/src/plugins/builtin/congress-trades/table.tsx +1 -1
- package/src/plugins/builtin/cot/client.ts +2 -1
- package/src/plugins/builtin/cot/model.test.ts +9 -0
- package/src/plugins/builtin/cot/model.ts +30 -1
- package/src/plugins/builtin/cot/pane.tsx +2 -4
- package/src/plugins/builtin/credit-conditions/client.ts +2 -1
- package/src/plugins/builtin/crypto-board/client.ts +2 -1
- package/src/plugins/builtin/crypto-board/model.test.ts +9 -0
- package/src/plugins/builtin/crypto-board/model.ts +11 -3
- package/src/plugins/builtin/custom-view/live-quotes.test.ts +6 -1
- package/src/plugins/builtin/custom-view/live-quotes.ts +26 -0
- package/src/plugins/builtin/custom-view/pane.tsx +18 -7
- package/src/plugins/builtin/custom-view/view-spec.test.ts +24 -6
- package/src/plugins/builtin/custom-view/view-spec.ts +53 -21
- package/src/plugins/builtin/debt-maturities/client.ts +16 -3
- package/src/plugins/builtin/debt-maturities/debt-maturities.test.ts +8 -0
- package/src/plugins/builtin/debt-maturities/headless.ts +3 -2
- package/src/plugins/builtin/debt-maturities/model.ts +2 -1
- package/src/plugins/builtin/debt-maturities/pane.tsx +0 -8
- package/src/plugins/builtin/earnings-calls/pane.test.tsx +14 -0
- package/src/plugins/builtin/earnings-calls/pane.tsx +3 -1
- package/src/plugins/builtin/econ/fred-client.test.ts +2 -0
- package/src/plugins/builtin/econ/fred-series-map.ts +0 -1
- package/src/plugins/builtin/econ/index.tsx +4 -2
- package/src/plugins/builtin/econ-statistics/pane.tsx +4 -2
- package/src/plugins/builtin/econ-statistics/stats.ts +29 -24
- package/src/plugins/builtin/econ-statistics/view.test.ts +16 -0
- package/src/plugins/builtin/equity-screener/headless.test.ts +22 -0
- package/src/plugins/builtin/equity-screener/headless.ts +18 -10
- package/src/plugins/builtin/estimate-revisions/client.ts +3 -1
- package/src/plugins/builtin/estimate-revisions/model.test.ts +7 -0
- package/src/plugins/builtin/estimate-revisions/model.ts +0 -5
- package/src/plugins/builtin/futures/table.tsx +4 -3
- package/src/plugins/builtin/futures-curve/client.ts +2 -1
- package/src/plugins/builtin/futures-curve/model.test.ts +6 -4
- package/src/plugins/builtin/futures-curve/model.ts +18 -14
- package/src/plugins/builtin/futures-curve/pane.tsx +2 -2
- package/src/plugins/builtin/fx-matrix/index.tsx +21 -4
- package/src/plugins/builtin/fx-matrix/live-legs.ts +7 -0
- package/src/plugins/builtin/fx-matrix/pairs.test.ts +5 -0
- package/src/plugins/builtin/fx-matrix/pairs.ts +9 -3
- package/src/plugins/builtin/insider/index.tsx +15 -12
- package/src/plugins/builtin/insider/pane.test.tsx +24 -0
- package/src/plugins/builtin/market-movers/headless.ts +2 -2
- package/src/plugins/builtin/market-movers/model.ts +23 -10
- package/src/plugins/builtin/market-movers/screener.test.ts +5 -1
- package/src/plugins/builtin/market-movers/screener.ts +3 -0
- package/src/plugins/builtin/market-movers/table.ts +2 -2
- package/src/plugins/builtin/market-valuation/client.test.ts +3 -0
- package/src/plugins/builtin/market-valuation/client.ts +6 -1
- package/src/plugins/builtin/money-markets/client.ts +2 -1
- package/src/plugins/builtin/money-markets/pane.tsx +0 -1
- package/src/plugins/builtin/options/expiry-selection.test.tsx +1 -1
- package/src/plugins/builtin/options/footer.test.ts +2 -2
- package/src/plugins/builtin/options/footer.ts +2 -2
- package/src/plugins/builtin/options/market-reference.test.ts +2 -2
- package/src/plugins/builtin/options/market-reference.ts +14 -2
- package/src/plugins/builtin/options/table.ts +5 -8
- package/src/plugins/builtin/options/view.test.tsx +2 -2
- package/src/plugins/builtin/options/view.tsx +1 -1
- package/src/plugins/builtin/options-calculator/pane.test.tsx +6 -6
- package/src/plugins/builtin/options-calculator/pane.tsx +23 -8
- package/src/plugins/builtin/options-calculator/surface.test.ts +16 -1
- package/src/plugins/builtin/options-calculator/surface.ts +13 -6
- package/src/plugins/builtin/portfolio-list/column-values.ts +17 -9
- package/src/plugins/builtin/portfolio-list/index.test.ts +5 -5
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +8 -8
- package/src/plugins/builtin/portfolio-list/pane/index.test.tsx +12 -12
- package/src/plugins/builtin/portfolio-list/position-cost-integrity.test.ts +2 -2
- package/src/plugins/builtin/portfolio-list/summary/index.tsx +7 -6
- package/src/plugins/builtin/rate-path/client.ts +2 -1
- package/src/plugins/builtin/rate-path/headless.ts +1 -1
- package/src/plugins/builtin/rate-path/pane.tsx +0 -1
- package/src/plugins/builtin/realized-vol/client.test.ts +7 -0
- package/src/plugins/builtin/realized-vol/headless.ts +1 -1
- package/src/plugins/builtin/realized-vol/model.test.ts +16 -4
- package/src/plugins/builtin/realized-vol/model.ts +24 -9
- package/src/plugins/builtin/relative-rotation/client.ts +2 -1
- package/src/plugins/builtin/research/relative-valuation-pane.tsx +3 -10
- package/src/plugins/builtin/risk-factors/data.test.ts +7 -0
- package/src/plugins/builtin/risk-factors/data.ts +8 -1
- package/src/plugins/builtin/risk-factors/pane.test.tsx +3 -0
- package/src/plugins/builtin/shared/screener-live-quotes.test.ts +3 -3
- package/src/plugins/builtin/shared/screener-live-quotes.ts +16 -1
- package/src/plugins/builtin/short-volume/client.ts +2 -1
- package/src/plugins/builtin/short-volume/pane.tsx +0 -1
- package/src/plugins/builtin/thirteenf/headless.ts +3 -3
- package/src/plugins/builtin/thirteenf/model.ts +14 -4
- package/src/plugins/builtin/thirteenf/overlap-pane.tsx +12 -5
- package/src/plugins/builtin/thirteenf/overlap.test.ts +13 -1
- package/src/plugins/builtin/thirteenf/overlap.ts +31 -3
- package/src/plugins/builtin/thirteenf/pane.test.tsx +2 -1
- package/src/plugins/builtin/thirteenf/pane.tsx +12 -9
- package/src/plugins/builtin/thirteenf/table.ts +9 -3
- package/src/plugins/builtin/ticker-detail/financials/model.ts +0 -12
- package/src/plugins/builtin/ticker-detail/headless-schema.ts +4 -1
- package/src/plugins/builtin/ticker-detail/index.test.tsx +1 -1
- package/src/plugins/builtin/ticker-detail/overview/components.tsx +9 -10
- package/src/plugins/builtin/ticker-detail/overview/model.ts +10 -2
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +6 -9
- package/src/plugins/builtin/ticker-detail/quote-monitor/card.tsx +3 -2
- package/src/plugins/builtin/time-sales/model.test.ts +17 -1
- package/src/plugins/builtin/time-sales/model.ts +14 -3
- package/src/plugins/builtin/time-sales/pane.tsx +8 -4
- package/src/plugins/builtin/treasury-auctions/pane.tsx +2 -2
- package/src/plugins/builtin/vol-surface/client.ts +4 -1
- package/src/plugins/builtin/vol-surface/model.test.ts +4 -2
- package/src/plugins/builtin/vol-surface/model.ts +14 -2
- package/src/plugins/builtin/vol-surface/pane.tsx +9 -2
- package/src/plugins/builtin/volatility/client.test.ts +13 -8
- package/src/plugins/builtin/volatility/client.ts +3 -2
- package/src/plugins/builtin/volatility/headless.test.ts +2 -2
- package/src/plugins/builtin/volatility/model.test.ts +4 -0
- package/src/plugins/builtin/volatility/model.ts +6 -7
- package/src/renderers/electrobun/bun/desktop/backend-requests.ts +7 -2
- package/src/renderers/electrobun/bun/desktop/workspace/index.test.ts +33 -1
- package/src/renderers/electrobun/bun/desktop/workspace/index.ts +34 -1
- package/src/renderers/electrobun/bun/index.ts +2 -1
- package/src/renderers/opentui/render-timing-smoke.ts +20 -0
- package/src/sources/provider-router/profile-enrichment.test.ts +0 -3
- package/src/sources/yahoo-finance.test.ts +5 -1
- package/src/sources/yahoo-finance.ts +2 -1
- package/src/time-series/forward-valuation.ts +6 -0
- package/src/time-series/resolve.ts +5 -1
- package/src/time-series/valuation-currency.test.ts +2 -0
- package/src/time-series/valuation-currency.ts +3 -1
- package/src/utils/exchange-rate-snapshot.test.ts +12 -1
- package/src/utils/exchange-rate-snapshot.ts +3 -2
- package/src/utils/format.ts +55 -20
- package/src/utils/fx-market-hours.test.ts +28 -0
- 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
|
-
|
|
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
|
-
:
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
5
|
-
|
|
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
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
257
|
-
expect(frame).toContain("Cash
|
|
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
|
|
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
|
|
266
|
-
expect(frame).not.toContain("P&L
|
|
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
|
|
288
|
-
expect(frame).not.toContain("P&L
|
|
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
|
|
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
|
|
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 -
|
|
474
|
-
expect(frame).toContain("Day +900");
|
|
475
|
-
expect(frame).toContain("P&L +777");
|
|
476
|
-
expect(frame).toContain("Realized -25");
|
|
477
|
-
expect(frame).toContain("Settled -
|
|
478
|
-
expect(frame).toContain("Avail
|
|
479
|
-
expect(frame).toContain("Excess
|
|
480
|
-
expect(frame).toContain("BP
|
|
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
|
|
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("
|
|
78
|
-
expect(converted.get("total-value")).toBe("
|
|
79
|
-
expect(converted.get("cash")).toBe("-
|
|
80
|
-
expect(converted.get("settled-cash")).toBe("-
|
|
81
|
-
expect(converted.get("available-funds")).toBe("
|
|
82
|
-
expect(converted.get("excess-liquidity")).toBe("
|
|
83
|
-
expect(converted.get("buying-power")).toBe("
|
|
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: "
|
|
109
|
-
expect(rows().get("pnl")).toMatchObject({ value: "
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
346
|
+
value: formatCompactAmount(convertAccountValue(account.buyingPower)),
|
|
347
347
|
color: colors.text,
|
|
348
348
|
});
|
|
349
349
|
}
|