@metamask-previews/perps-controller 3.0.0-preview-e61cfa5 → 3.1.0-preview-548bdd1d9

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 (90) hide show
  1. package/CHANGELOG.md +19 -1
  2. package/dist/PerpsController-method-action-types.cjs.map +1 -1
  3. package/dist/PerpsController-method-action-types.d.cts +8 -0
  4. package/dist/PerpsController-method-action-types.d.cts.map +1 -1
  5. package/dist/PerpsController-method-action-types.d.mts +8 -0
  6. package/dist/PerpsController-method-action-types.d.mts.map +1 -1
  7. package/dist/PerpsController-method-action-types.mjs.map +1 -1
  8. package/dist/PerpsController.cjs +117 -29
  9. package/dist/PerpsController.cjs.map +1 -1
  10. package/dist/PerpsController.d.cts +14 -2
  11. package/dist/PerpsController.d.cts.map +1 -1
  12. package/dist/PerpsController.d.mts +14 -2
  13. package/dist/PerpsController.d.mts.map +1 -1
  14. package/dist/PerpsController.mjs +118 -30
  15. package/dist/PerpsController.mjs.map +1 -1
  16. package/dist/constants/eventNames.cjs +1 -0
  17. package/dist/constants/eventNames.cjs.map +1 -1
  18. package/dist/constants/eventNames.d.cts +1 -0
  19. package/dist/constants/eventNames.d.cts.map +1 -1
  20. package/dist/constants/eventNames.d.mts +1 -0
  21. package/dist/constants/eventNames.d.mts.map +1 -1
  22. package/dist/constants/eventNames.mjs +1 -0
  23. package/dist/constants/eventNames.mjs.map +1 -1
  24. package/dist/constants/perpsConfig.cjs +46 -1
  25. package/dist/constants/perpsConfig.cjs.map +1 -1
  26. package/dist/constants/perpsConfig.d.cts +35 -0
  27. package/dist/constants/perpsConfig.d.cts.map +1 -1
  28. package/dist/constants/perpsConfig.d.mts +35 -0
  29. package/dist/constants/perpsConfig.d.mts.map +1 -1
  30. package/dist/constants/perpsConfig.mjs +43 -0
  31. package/dist/constants/perpsConfig.mjs.map +1 -1
  32. package/dist/constants/transactionsHistoryConfig.cjs +23 -4
  33. package/dist/constants/transactionsHistoryConfig.cjs.map +1 -1
  34. package/dist/constants/transactionsHistoryConfig.d.cts +23 -4
  35. package/dist/constants/transactionsHistoryConfig.d.cts.map +1 -1
  36. package/dist/constants/transactionsHistoryConfig.d.mts +23 -4
  37. package/dist/constants/transactionsHistoryConfig.d.mts.map +1 -1
  38. package/dist/constants/transactionsHistoryConfig.mjs +23 -4
  39. package/dist/constants/transactionsHistoryConfig.mjs.map +1 -1
  40. package/dist/index.cjs +14 -3
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.cts +3 -1
  43. package/dist/index.d.cts.map +1 -1
  44. package/dist/index.d.mts +3 -1
  45. package/dist/index.d.mts.map +1 -1
  46. package/dist/index.mjs +2 -1
  47. package/dist/index.mjs.map +1 -1
  48. package/dist/providers/HyperLiquidProvider.cjs +83 -27
  49. package/dist/providers/HyperLiquidProvider.cjs.map +1 -1
  50. package/dist/providers/HyperLiquidProvider.d.cts.map +1 -1
  51. package/dist/providers/HyperLiquidProvider.d.mts.map +1 -1
  52. package/dist/providers/HyperLiquidProvider.mjs +83 -27
  53. package/dist/providers/HyperLiquidProvider.mjs.map +1 -1
  54. package/dist/services/HyperLiquidSubscriptionService.cjs +6 -0
  55. package/dist/services/HyperLiquidSubscriptionService.cjs.map +1 -1
  56. package/dist/services/HyperLiquidSubscriptionService.d.cts.map +1 -1
  57. package/dist/services/HyperLiquidSubscriptionService.d.mts.map +1 -1
  58. package/dist/services/HyperLiquidSubscriptionService.mjs +6 -0
  59. package/dist/services/HyperLiquidSubscriptionService.mjs.map +1 -1
  60. package/dist/types/index.cjs.map +1 -1
  61. package/dist/types/index.d.cts +6 -0
  62. package/dist/types/index.d.cts.map +1 -1
  63. package/dist/types/index.d.mts +6 -0
  64. package/dist/types/index.d.mts.map +1 -1
  65. package/dist/types/index.mjs.map +1 -1
  66. package/dist/utils/index.cjs +2 -0
  67. package/dist/utils/index.cjs.map +1 -1
  68. package/dist/utils/index.d.cts +2 -0
  69. package/dist/utils/index.d.cts.map +1 -1
  70. package/dist/utils/index.d.mts +2 -0
  71. package/dist/utils/index.d.mts.map +1 -1
  72. package/dist/utils/index.mjs +2 -0
  73. package/dist/utils/index.mjs.map +1 -1
  74. package/dist/utils/perpsDiskPersistence.cjs +252 -0
  75. package/dist/utils/perpsDiskPersistence.cjs.map +1 -0
  76. package/dist/utils/perpsDiskPersistence.d.cts +108 -0
  77. package/dist/utils/perpsDiskPersistence.d.cts.map +1 -0
  78. package/dist/utils/perpsDiskPersistence.d.mts +108 -0
  79. package/dist/utils/perpsDiskPersistence.d.mts.map +1 -0
  80. package/dist/utils/perpsDiskPersistence.mjs +244 -0
  81. package/dist/utils/perpsDiskPersistence.mjs.map +1 -0
  82. package/dist/utils/perpsFormatters.cjs +498 -0
  83. package/dist/utils/perpsFormatters.cjs.map +1 -0
  84. package/dist/utils/perpsFormatters.d.cts +202 -0
  85. package/dist/utils/perpsFormatters.d.cts.map +1 -0
  86. package/dist/utils/perpsFormatters.d.mts +202 -0
  87. package/dist/utils/perpsFormatters.d.mts.map +1 -0
  88. package/dist/utils/perpsFormatters.mjs +489 -0
  89. package/dist/utils/perpsFormatters.mjs.map +1 -0
  90. package/package.json +3 -3
@@ -0,0 +1,498 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatFundingRate = exports.formatPercentage = exports.formatPnl = exports.formatPositionSize = exports.formatPerpsFiat = exports.PRICE_RANGES_UNIVERSAL = exports.PRICE_RANGES_MINIMAL_VIEW = exports.formatWithSignificantDigits = exports.PRICE_THRESHOLD = void 0;
4
+ /**
5
+ * Portable perps decimal formatters.
6
+ *
7
+ * These are the canonical implementations, exported from the controller so
8
+ * extension and any future consumer can import them directly.
9
+ * No mobile-specific imports — safe to sync to Core.
10
+ *
11
+ * Intl.NumberFormat instances are cached in a module-level Map keyed by
12
+ * serialized options, avoiding repeated construction costs.
13
+ */
14
+ const perpsConfig_1 = require("../constants/perpsConfig.cjs");
15
+ // Module-level Intl.NumberFormat cache (keyed by serialized options).
16
+ const _fmtCache = new Map();
17
+ function _formatCurrency(value, currency, opts) {
18
+ const key = `${currency}:${opts.minimumFractionDigits}:${opts.maximumFractionDigits}`;
19
+ let formatter = _fmtCache.get(key);
20
+ if (!formatter) {
21
+ formatter = new Intl.NumberFormat('en-US', {
22
+ style: 'currency',
23
+ currency,
24
+ currencyDisplay: 'narrowSymbol',
25
+ minimumFractionDigits: opts.minimumFractionDigits,
26
+ maximumFractionDigits: opts.maximumFractionDigits,
27
+ });
28
+ _fmtCache.set(key, formatter);
29
+ }
30
+ return formatter.format(value);
31
+ }
32
+ /**
33
+ * Internal equivalent of the mobile formatWithThreshold utility.
34
+ * Formats a currency value, returning "<$X.XX" for values below threshold.
35
+ *
36
+ * @param amount - The numeric amount to format.
37
+ * @param threshold - The threshold below which the "<" prefix is shown.
38
+ * @param options - Intl formatting options.
39
+ * @param options.currency - ISO 4217 currency code.
40
+ * @param options.minimumFractionDigits - Minimum decimal digits.
41
+ * @param options.maximumFractionDigits - Maximum decimal digits.
42
+ * @returns Formatted currency string.
43
+ */
44
+ function _formatWithThreshold(amount, threshold, options) {
45
+ const formatOpts = {
46
+ minimumFractionDigits: options.minimumFractionDigits,
47
+ maximumFractionDigits: options.maximumFractionDigits,
48
+ currencyDisplay: 'narrowSymbol',
49
+ };
50
+ if (amount === 0) {
51
+ return _formatCurrency(0, options.currency, formatOpts);
52
+ }
53
+ return Math.abs(amount) < threshold
54
+ ? `<${_formatCurrency(threshold, options.currency, formatOpts)}`
55
+ : _formatCurrency(amount, options.currency, formatOpts);
56
+ }
57
+ /**
58
+ * Price threshold constants for PRICE_RANGES_UNIVERSAL
59
+ * These define the boundaries between different formatting ranges
60
+ */
61
+ exports.PRICE_THRESHOLD = {
62
+ /** Very high values boundary (> $100k) */
63
+ VERY_HIGH: 100000,
64
+ /** High values boundary (> $10k) */
65
+ HIGH: 10000,
66
+ /** Large values boundary (> $1k) */
67
+ LARGE: 1000,
68
+ /** Medium values boundary (> $100) */
69
+ MEDIUM: 100,
70
+ /** Medium-low values boundary (> $10) */
71
+ MEDIUM_LOW: 10,
72
+ /** Low values boundary (>= $0.01) */
73
+ LOW: 0.01,
74
+ /**
75
+ * Very small values threshold (< $0.01)
76
+ * This is the minimum value for formatWithThreshold and should align with
77
+ * the 6 decimal maximum (0.000001 is the smallest representable value)
78
+ */
79
+ VERY_SMALL: 0.000001,
80
+ };
81
+ /**
82
+ * Formats a number to a specific number of significant digits
83
+ * Strips trailing zeros unless minDecimals requires them
84
+ *
85
+ * @param value - The numeric value to format
86
+ * @param significantDigits - Number of significant digits to maintain
87
+ * @param minDecimals - Minimum decimal places to show (may add zeros)
88
+ * @param maxDecimals - Maximum decimal places allowed
89
+ * @returns Formatted number with appropriate precision, trailing zeros removed
90
+ */
91
+ function formatWithSignificantDigits(value, significantDigits, minDecimals, maxDecimals) {
92
+ // Handle special cases
93
+ if (value === 0) {
94
+ // Return zero with no trailing decimals by default (matches stripTrailingZeros behavior)
95
+ // Can be overridden by explicit minDecimals if needed
96
+ return { value: 0, decimals: minDecimals ?? 0 };
97
+ }
98
+ const absValue = Math.abs(value);
99
+ // For numbers >= 1, calculate decimals based on magnitude to achieve target significant figures
100
+ // This ensures consistent precision across different price ranges:
101
+ // Examples with 4 significant figures:
102
+ // $123,456.78 → $123,456.78 (≥$1000: 2 decimals minimum, 8 sig figs)
103
+ // $456.12 → $456.12 (≥$10: 2 decimals minimum, 5 sig figs)
104
+ // $56.123 → $56.123 (≥$10: 2 decimals minimum, 5 sig figs)
105
+ // $5.123 → $5.123 ($1-$10: 3 decimals = 4 sig figs)
106
+ // $2.801 → $2.801 ($1-$10: 3 decimals = 4 sig figs)
107
+ // $1.234 → $1.234 ($1-$10: 3 decimals = 4 sig figs)
108
+ if (absValue >= 1) {
109
+ let targetDecimals;
110
+ // Calculate decimals needed based on integer digits to achieve target significant figures
111
+ // For $38.388 with 5 sig figs: 2 integer digits, need 3 decimals (3,8,3,8,8)
112
+ // For $123.45 with 5 sig figs: 3 integer digits, need 2 decimals (1,2,3,4,5)
113
+ const integerDigits = Math.floor(Math.log10(absValue)) + 1;
114
+ const decimalsNeeded = significantDigits - integerDigits;
115
+ targetDecimals = Math.max(decimalsNeeded, 0); // Can't have negative decimals
116
+ // Apply explicit minimum decimals constraint if provided (for special cases)
117
+ if (minDecimals !== undefined && targetDecimals < minDecimals) {
118
+ targetDecimals = minDecimals;
119
+ }
120
+ // Apply maximum decimals constraint if specified
121
+ const finalDecimals = maxDecimals === undefined
122
+ ? targetDecimals
123
+ : Math.min(targetDecimals, maxDecimals);
124
+ // Round to prevent floating-point artifacts (e.g., 2.820000000000003 → 2.82)
125
+ const roundedValue = Number(value.toFixed(finalDecimals));
126
+ return {
127
+ value: roundedValue,
128
+ decimals: finalDecimals,
129
+ };
130
+ }
131
+ // For numbers < 1, use toPrecision to limit to significantDigits
132
+ // Examples: 0.1234, 0.01234 should show exactly 4 sig figs
133
+ const precisionStr = absValue.toPrecision(significantDigits);
134
+ const precisionNum = parseFloat(precisionStr);
135
+ // Convert to string to count actual decimals after trailing zeros are removed
136
+ const valueStr = precisionNum.toString();
137
+ const [, decPart = ''] = valueStr.split('.');
138
+ let actualDecimals = decPart.length;
139
+ // Apply min/max decimal constraints
140
+ if (minDecimals !== undefined && actualDecimals < minDecimals) {
141
+ actualDecimals = minDecimals; // Will add zeros if needed
142
+ }
143
+ if (maxDecimals !== undefined && actualDecimals > maxDecimals) {
144
+ actualDecimals = maxDecimals;
145
+ }
146
+ // Return the value with sign restored and decimal count
147
+ return {
148
+ value: value < 0 ? -precisionNum : precisionNum,
149
+ decimals: actualDecimals,
150
+ };
151
+ }
152
+ exports.formatWithSignificantDigits = formatWithSignificantDigits;
153
+ /**
154
+ * Minimal view fiat range configuration
155
+ * Uses fiat-style stripping for clean currency display
156
+ * Strips only .00 to avoid partial decimals like $1,250.1
157
+ */
158
+ exports.PRICE_RANGES_MINIMAL_VIEW = [
159
+ {
160
+ // Large values (>= $1000): Strip .00 only ($5,000 not $5,000.00, but $5,000.10 stays)
161
+ condition: (val) => Math.abs(val) >= exports.PRICE_THRESHOLD.LARGE,
162
+ minimumDecimals: 2,
163
+ maximumDecimals: 2,
164
+ threshold: exports.PRICE_THRESHOLD.LARGE,
165
+ stripTrailingZeros: true,
166
+ fiatStyleStripping: true,
167
+ },
168
+ {
169
+ // Small values (< $1000): Also use fiat-style stripping ($100 not $100.00, but $13.40 stays)
170
+ condition: () => true,
171
+ minimumDecimals: 2,
172
+ maximumDecimals: 2,
173
+ threshold: exports.PRICE_THRESHOLD.LOW,
174
+ stripTrailingZeros: true,
175
+ fiatStyleStripping: true,
176
+ },
177
+ ];
178
+ /**
179
+ * Universal price range configuration following comprehensive rules from rules-decimals.md
180
+ *
181
+ * Rules:
182
+ * - Max 6 decimals across all ranges (Hyperliquid limit)
183
+ * - Strip trailing zeros by default
184
+ * - Use |v| (absolute value) for conditions
185
+ *
186
+ * Significant digits by range:
187
+ * - > $100,000: 6 sig digs
188
+ * - $100,000 > x > $0.01: 5 sig digs
189
+ * - < $0.01: 4 sig digs
190
+ *
191
+ * Decimal limits by price range:
192
+ * - |v| > 10,000: min 0, max 0 decimals; 5 sig digs (6 if >100k)
193
+ * - |v| > 1,000: min 0, max 1 decimal; 5 sig digs
194
+ * - |v| > 100: min 0, max 2 decimals; 5 sig digs
195
+ * - |v| > 10: min 0, max 4 decimals; 5 sig digs
196
+ * - |v| ≥ 0.01: 5 sig digs, min 2, max 6 decimals
197
+ * - |v| < 0.01: 4 sig digs, min 2, max 6 decimals
198
+ *
199
+ * Examples:
200
+ * - $123,456.78 → $123,457 (>$10k: 0 decimals, 6 sig figs)
201
+ * - $12,345.67 → $12,346 (>$10k: 0 decimals, 5 sig figs)
202
+ * - $1,234.56 → $1,234.6 ($1k-$10k: 1 decimal, 5 sig figs)
203
+ * - $123.456 → $123.46 ($100-$1k: 2 decimals, 5 sig figs)
204
+ * - $12.34567 → $12.346 ($10-$100: 4 decimals, 5 sig figs)
205
+ * - $1.3445555 → $1.3446 (≥$0.01: 5 sig figs)
206
+ * - $0.333333 → $0.33333 (≥$0.01: 5 sig figs)
207
+ * - $0.004236 → $0.004236 (<$0.01: 4 sig figs, max 6 decimals)
208
+ * - $0.0000006 → $0.000001 (<$0.01: 4 sig figs, rounds with max 6 decimals)
209
+ */
210
+ exports.PRICE_RANGES_UNIVERSAL = [
211
+ {
212
+ // Very high values (> $100,000): No decimals, 6 significant figures
213
+ // Ex: $123,456.78 → $123,457
214
+ condition: (val) => Math.abs(val) > exports.PRICE_THRESHOLD.VERY_HIGH,
215
+ minimumDecimals: 0,
216
+ maximumDecimals: 0,
217
+ significantDigits: 6,
218
+ threshold: exports.PRICE_THRESHOLD.VERY_HIGH,
219
+ },
220
+ {
221
+ // High values ($10,000-$100,000]: No decimals, 5 significant figures
222
+ // Ex: $12,345.67 → $12,346
223
+ condition: (val) => Math.abs(val) > exports.PRICE_THRESHOLD.HIGH,
224
+ minimumDecimals: 0,
225
+ maximumDecimals: 0,
226
+ significantDigits: 5,
227
+ threshold: exports.PRICE_THRESHOLD.HIGH,
228
+ },
229
+ {
230
+ // Large values ($1,000-$10,000]: Max 1 decimal, 5 significant figures
231
+ // Ex: $1,234.56 → $1,234.6
232
+ condition: (val) => Math.abs(val) > exports.PRICE_THRESHOLD.LARGE,
233
+ minimumDecimals: 0,
234
+ maximumDecimals: 1,
235
+ significantDigits: 5,
236
+ threshold: exports.PRICE_THRESHOLD.LARGE,
237
+ },
238
+ {
239
+ // Medium values ($100-$1,000]: Max 2 decimals, 5 significant figures
240
+ // Ex: $123.456 → $123.46
241
+ condition: (val) => Math.abs(val) > exports.PRICE_THRESHOLD.MEDIUM,
242
+ minimumDecimals: 0,
243
+ maximumDecimals: 2,
244
+ significantDigits: 5,
245
+ threshold: exports.PRICE_THRESHOLD.MEDIUM,
246
+ },
247
+ {
248
+ // Medium-low values ($10-$100]: Max 4 decimals, 5 significant figures
249
+ // Ex: $12.34567 → $12.346
250
+ condition: (val) => Math.abs(val) > exports.PRICE_THRESHOLD.MEDIUM_LOW,
251
+ minimumDecimals: 0,
252
+ maximumDecimals: 4,
253
+ significantDigits: 5,
254
+ threshold: exports.PRICE_THRESHOLD.MEDIUM_LOW,
255
+ },
256
+ {
257
+ // Low values ($0.01-$10]: 5 significant figures, min 2 max MAX_PRICE_DECIMALS decimals
258
+ // Ex: $1.3445555 → $1.3446 | $0.333333 → $0.33333
259
+ condition: (val) => Math.abs(val) >= exports.PRICE_THRESHOLD.LOW,
260
+ significantDigits: 5,
261
+ minimumDecimals: 2,
262
+ maximumDecimals: perpsConfig_1.DECIMAL_PRECISION_CONFIG.MaxPriceDecimals,
263
+ threshold: exports.PRICE_THRESHOLD.LOW,
264
+ },
265
+ {
266
+ // Very small values (< $0.01): 4 significant figures, min 2 max MAX_PRICE_DECIMALS decimals
267
+ // Ex: $0.004236 → $0.004236 | $0.0000006 → $0.000001
268
+ condition: () => true,
269
+ significantDigits: 4,
270
+ minimumDecimals: 2,
271
+ maximumDecimals: perpsConfig_1.DECIMAL_PRECISION_CONFIG.MaxPriceDecimals,
272
+ threshold: exports.PRICE_THRESHOLD.VERY_SMALL,
273
+ },
274
+ ];
275
+ /**
276
+ * Formats a balance value as USD currency with appropriate decimal places
277
+ *
278
+ * @param balance - Raw numeric balance value (e.g., 1234.56, not token minimal denomination)
279
+ * @param options - Optional formatting options
280
+ * @param options.minimumDecimals - Global minimum decimal places (overrides range configs)
281
+ * @param options.maximumDecimals - Global maximum decimal places (overrides range configs)
282
+ * @param options.significantDigits - Global significant digits (overrides decimal settings when set)
283
+ * @param options.ranges - Custom range configurations (defaults to PRICE_RANGES_MINIMAL_VIEW)
284
+ * @param options.currency - Currency code (default: 'USD')
285
+ * @param options.locale - Locale for formatting (default: 'en-US')
286
+ * @param options.stripTrailingZeros - Strip trailing zeros from output (default: false via PRICE_RANGES_MINIMAL_VIEW). When true, overrides minimumDecimals constraint.
287
+ * @returns Formatted currency string with variable decimals based on configured ranges
288
+ * @example
289
+ * // Using defaults (preserves trailing zeros for fiat)
290
+ * formatPerpsFiat(1234.56) => "$1,234.56"
291
+ * formatPerpsFiat(1250.00) => "$1,250.00" // Trailing zeros preserved
292
+ * formatPerpsFiat(50000) => "$50,000.00" // Trailing zeros preserved
293
+ *
294
+ * // Stripping trailing zeros when needed (e.g., for crypto)
295
+ * formatPerpsFiat(1250, { stripTrailingZeros: true }) => "$1,250"
296
+ *
297
+ * // With custom ranges
298
+ * formatPerpsFiat(0.00001, {
299
+ * ranges: [
300
+ * { condition: (v) => v < 0.001, minimumDecimals: 6, maximumDecimals: 8 },
301
+ * { condition: () => true, minimumDecimals: 2, maximumDecimals: 2 }
302
+ * ]
303
+ * }) => "$0.00001" // Trailing zero stripped
304
+ *
305
+ * // With significant digits
306
+ * formatPerpsFiat(1234.56789, { significantDigits: 5 }) => "$1,234.6"
307
+ * formatPerpsFiat(0.0001234, { significantDigits: 3 }) => "$0.000123"
308
+ */
309
+ const formatPerpsFiat = (balance, options) => {
310
+ const value = typeof balance === 'string' ? parseFloat(balance) : balance;
311
+ const currency = options?.currency ?? 'USD';
312
+ let formatted;
313
+ if (isNaN(value)) {
314
+ // Return placeholder for invalid values to avoid confusion with actual $0 values
315
+ return perpsConfig_1.PERPS_CONSTANTS.FallbackPriceDisplay;
316
+ }
317
+ // Use custom ranges or defaults
318
+ const ranges = options?.ranges ?? exports.PRICE_RANGES_MINIMAL_VIEW;
319
+ // Find the first matching range configuration
320
+ const rangeConfig = ranges.find((range) => range.condition(value));
321
+ if (rangeConfig) {
322
+ // Check for significant digits (global or range-specific)
323
+ const sigDigits = options?.significantDigits ?? rangeConfig.significantDigits;
324
+ // If significant digits are specified, use them
325
+ if (sigDigits) {
326
+ // Get min/max decimals (global overrides range, range overrides default)
327
+ const minDecimals = options?.minimumDecimals ?? rangeConfig.minimumDecimals;
328
+ const maxDecimals = options?.maximumDecimals ?? rangeConfig.maximumDecimals;
329
+ // Calculate appropriate formatting based on significant digits
330
+ const { value: formattedValue, decimals } = formatWithSignificantDigits(value, sigDigits, minDecimals, maxDecimals);
331
+ // Format with the calculated decimal places
332
+ formatted = _formatWithThreshold(formattedValue, rangeConfig.threshold ?? 0.01, {
333
+ currency,
334
+ minimumFractionDigits: decimals,
335
+ maximumFractionDigits: decimals,
336
+ });
337
+ }
338
+ else {
339
+ // Standard decimal-based formatting (existing logic)
340
+ const minDecimals = options?.minimumDecimals ?? rangeConfig.minimumDecimals;
341
+ const maxDecimals = options?.maximumDecimals ?? rangeConfig.maximumDecimals;
342
+ // Use custom formatting if provided
343
+ if (rangeConfig.customFormat) {
344
+ formatted = rangeConfig.customFormat(value, options?.locale ?? 'en-US', currency);
345
+ }
346
+ else {
347
+ // Use standard formatting with threshold
348
+ formatted = _formatWithThreshold(value, rangeConfig.threshold ?? 0.01, {
349
+ currency,
350
+ minimumFractionDigits: minDecimals,
351
+ maximumFractionDigits: maxDecimals,
352
+ });
353
+ }
354
+ }
355
+ }
356
+ else {
357
+ // Fallback if no range matches (shouldn't happen with proper default config)
358
+ const fallbackMin = options?.minimumDecimals ?? 2;
359
+ const fallbackMax = options?.maximumDecimals ?? 2;
360
+ formatted = _formatWithThreshold(value, 0.01, {
361
+ currency,
362
+ minimumFractionDigits: fallbackMin,
363
+ maximumFractionDigits: fallbackMax,
364
+ });
365
+ }
366
+ // Post-process: strip trailing zeros unless explicitly disabled
367
+ // Priority: explicit options.stripTrailingZeros false > rangeConfig > options default > true
368
+ // If options.stripTrailingZeros is explicitly false, skip stripping entirely
369
+ if (options?.stripTrailingZeros === false) {
370
+ return formatted;
371
+ }
372
+ // Otherwise check range config or default to true
373
+ const shouldStrip = rangeConfig?.stripTrailingZeros ?? options?.stripTrailingZeros ?? true;
374
+ if (shouldStrip) {
375
+ // Check if fiat-style stripping is enabled (only strips .00)
376
+ const useFiatStyle = rangeConfig?.fiatStyleStripping ?? false;
377
+ if (useFiatStyle) {
378
+ // Fiat-style: Only strip .00 (no meaningful decimals), preserve 2-decimal format
379
+ // Examples: $1,250.00 → $1,250 | $1,000.10 → $1,000.10 | $13.40 → $13.40
380
+ return formatted.replace(/\.00$/u, '');
381
+ }
382
+ // Standard: Strip all trailing zeros after decimal point
383
+ // Examples: $1,250.00 → $1,250 | $100.0 → $100 | $10.5 → $10.5 | $1.234 → $1.234
384
+ return formatted.replace(/(\.\d*?)0+$/u, '$1').replace(/\.$/u, '');
385
+ }
386
+ return formatted;
387
+ };
388
+ exports.formatPerpsFiat = formatPerpsFiat;
389
+ /**
390
+ * Formats position size with variable decimal precision based on magnitude or asset-specific decimals
391
+ * Removes trailing zeros to match task requirements
392
+ *
393
+ * @param size - Raw position size value
394
+ * @param szDecimals - Optional asset-specific decimal precision from Hyperliquid metadata (e.g., BTC=5, ETH=4, DOGE=1)
395
+ * @returns Format varies by size or uses asset-specific decimals, with trailing zeros removed:
396
+ * If szDecimals provided: Uses exact decimals (e.g., 0.00009 BTC with szDecimals=5 => "0.00009")
397
+ * Otherwise falls back to magnitude-based logic:
398
+ * - Size < 0.01: Up to 6 decimals (e.g., "0.00009" not "0.000090")
399
+ * - Size < 1: Up to 4 decimals (e.g., "0.0024" not "0.002400")
400
+ * - Size >= 1: Up to 2 decimals (e.g., "44" not "44.00")
401
+ * @example formatPositionSize(0.00009, 5) => "0.00009" (uses szDecimals)
402
+ * @example formatPositionSize(44.00, 1) => "44" (uses szDecimals, trailing zeros removed)
403
+ * @example formatPositionSize(0.0024) => "0.0024" (no szDecimals, uses magnitude logic)
404
+ * @example formatPositionSize(44.00) => "44" (no szDecimals, uses magnitude logic)
405
+ */
406
+ const formatPositionSize = (size, szDecimals) => {
407
+ const value = typeof size === 'string' ? parseFloat(size) : size;
408
+ if (isNaN(value) || value === 0) {
409
+ return '0';
410
+ }
411
+ // Use asset-specific decimals if provided (Hyperliquid metadata)
412
+ if (szDecimals !== undefined) {
413
+ return value.toFixed(szDecimals).replace(/\.?0+$/u, '');
414
+ }
415
+ // Fallback: magnitude-based decimal logic for backwards compatibility
416
+ const abs = Math.abs(value);
417
+ let formatted;
418
+ if (abs < 0.01) {
419
+ // For very small numbers, use more decimal places
420
+ formatted = value.toFixed(6);
421
+ }
422
+ else if (abs < 1) {
423
+ // For small numbers, use 4 decimal places
424
+ formatted = value.toFixed(4);
425
+ }
426
+ else {
427
+ // For normal numbers, use 2 decimal places
428
+ formatted = value.toFixed(2);
429
+ }
430
+ // Remove trailing zeros and unnecessary decimal point
431
+ return formatted.replace(/\.?0+$/u, '');
432
+ };
433
+ exports.formatPositionSize = formatPositionSize;
434
+ /**
435
+ * Formats a PnL (Profit and Loss) value with sign prefix
436
+ *
437
+ * @param pnl - Raw numeric PnL value (positive for profit, negative for loss)
438
+ * @returns Format: "+$X,XXX.XX" or "-$X,XXX.XX" (always shows sign, 2 decimals)
439
+ * @example formatPnl(1234.56) => "+$1,234.56"
440
+ * @example formatPnl(-500) => "-$500.00"
441
+ * @example formatPnl(0) => "+$0.00"
442
+ */
443
+ const formatPnl = (pnl) => {
444
+ const value = typeof pnl === 'string' ? parseFloat(pnl) : pnl;
445
+ if (isNaN(value)) {
446
+ return perpsConfig_1.PERPS_CONSTANTS.ZeroAmountDetailedDisplay;
447
+ }
448
+ const formatted = _formatCurrency(Math.abs(value), 'USD', {
449
+ minimumFractionDigits: 2,
450
+ maximumFractionDigits: 2,
451
+ });
452
+ return value >= 0 ? `+${formatted}` : `-${formatted}`;
453
+ };
454
+ exports.formatPnl = formatPnl;
455
+ /**
456
+ * Formats a percentage value with sign prefix
457
+ *
458
+ * @param value - Raw percentage value (e.g., 5.25 for 5.25%, not 0.0525)
459
+ * @param decimals - Number of decimal places to show (default: 2)
460
+ * @returns Format: "+X.XX%" or "-X.XX%" (always shows sign, 2 decimals)
461
+ * @example formatPercentage(5.25) => "+5.25%"
462
+ * @example formatPercentage(-2.75) => "-2.75%"
463
+ * @example formatPercentage(0) => "+0.00%"
464
+ */
465
+ const formatPercentage = (value, decimals = 2) => {
466
+ const parsed = typeof value === 'string' ? parseFloat(value) : value;
467
+ if (isNaN(parsed)) {
468
+ return '0.00%';
469
+ }
470
+ return `${parsed >= 0 ? '+' : ''}${parsed.toFixed(decimals)}%`;
471
+ };
472
+ exports.formatPercentage = formatPercentage;
473
+ /**
474
+ * Formats funding rate for display
475
+ *
476
+ * @param value - Raw funding rate value (decimal, not percentage)
477
+ * @param options - Optional formatting options
478
+ * @param options.showZero - Whether to return zero display value for zero/undefined (default: true)
479
+ * @returns Formatted funding rate as percentage string
480
+ * @example formatFundingRate(0.0005) => "0.0500%"
481
+ * @example formatFundingRate(-0.0001) => "-0.0100%"
482
+ * @example formatFundingRate(undefined) => "0.0000%"
483
+ */
484
+ const formatFundingRate = (value, options) => {
485
+ const showZero = options?.showZero ?? true;
486
+ if (value === undefined || value === null) {
487
+ return showZero ? perpsConfig_1.FUNDING_RATE_CONFIG.ZeroDisplay : '';
488
+ }
489
+ const percentage = value * perpsConfig_1.FUNDING_RATE_CONFIG.PercentageMultiplier;
490
+ const formatted = percentage.toFixed(perpsConfig_1.FUNDING_RATE_CONFIG.Decimals);
491
+ // Check if the result is effectively zero
492
+ if (showZero && parseFloat(formatted) === 0) {
493
+ return perpsConfig_1.FUNDING_RATE_CONFIG.ZeroDisplay;
494
+ }
495
+ return `${formatted}%`;
496
+ };
497
+ exports.formatFundingRate = formatFundingRate;
498
+ //# sourceMappingURL=perpsFormatters.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"perpsFormatters.cjs","sourceRoot":"","sources":["../../src/utils/perpsFormatters.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Portable perps decimal formatters.\n *\n * These are the canonical implementations, exported from the controller so\n * extension and any future consumer can import them directly.\n * No mobile-specific imports — safe to sync to Core.\n *\n * Intl.NumberFormat instances are cached in a module-level Map keyed by\n * serialized options, avoiding repeated construction costs.\n */\nimport {\n DECIMAL_PRECISION_CONFIG,\n FUNDING_RATE_CONFIG,\n PERPS_CONSTANTS,\n} from '../constants/perpsConfig';\n\n// Module-level Intl.NumberFormat cache (keyed by serialized options).\nconst _fmtCache = new Map<string, Intl.NumberFormat>();\n\nfunction _formatCurrency(\n value: number,\n currency: string,\n opts: { minimumFractionDigits: number; maximumFractionDigits: number },\n): string {\n const key = `${currency}:${opts.minimumFractionDigits}:${opts.maximumFractionDigits}`;\n let formatter = _fmtCache.get(key);\n if (!formatter) {\n formatter = new Intl.NumberFormat('en-US', {\n style: 'currency',\n currency,\n currencyDisplay: 'narrowSymbol',\n minimumFractionDigits: opts.minimumFractionDigits,\n maximumFractionDigits: opts.maximumFractionDigits,\n });\n _fmtCache.set(key, formatter);\n }\n return formatter.format(value);\n}\n\n/**\n * Internal equivalent of the mobile formatWithThreshold utility.\n * Formats a currency value, returning \"<$X.XX\" for values below threshold.\n *\n * @param amount - The numeric amount to format.\n * @param threshold - The threshold below which the \"<\" prefix is shown.\n * @param options - Intl formatting options.\n * @param options.currency - ISO 4217 currency code.\n * @param options.minimumFractionDigits - Minimum decimal digits.\n * @param options.maximumFractionDigits - Maximum decimal digits.\n * @returns Formatted currency string.\n */\nfunction _formatWithThreshold(\n amount: number,\n threshold: number,\n options: {\n currency: string;\n minimumFractionDigits: number;\n maximumFractionDigits: number;\n },\n): string {\n const formatOpts = {\n minimumFractionDigits: options.minimumFractionDigits,\n maximumFractionDigits: options.maximumFractionDigits,\n currencyDisplay: 'narrowSymbol' as const,\n };\n if (amount === 0) {\n return _formatCurrency(0, options.currency, formatOpts);\n }\n return Math.abs(amount) < threshold\n ? `<${_formatCurrency(threshold, options.currency, formatOpts)}`\n : _formatCurrency(amount, options.currency, formatOpts);\n}\n\n/**\n * Price threshold constants for PRICE_RANGES_UNIVERSAL\n * These define the boundaries between different formatting ranges\n */\nexport const PRICE_THRESHOLD = {\n /** Very high values boundary (> $100k) */\n VERY_HIGH: 100_000,\n /** High values boundary (> $10k) */\n HIGH: 10_000,\n /** Large values boundary (> $1k) */\n LARGE: 1_000,\n /** Medium values boundary (> $100) */\n MEDIUM: 100,\n /** Medium-low values boundary (> $10) */\n MEDIUM_LOW: 10,\n /** Low values boundary (>= $0.01) */\n LOW: 0.01,\n /**\n * Very small values threshold (< $0.01)\n * This is the minimum value for formatWithThreshold and should align with\n * the 6 decimal maximum (0.000001 is the smallest representable value)\n */\n VERY_SMALL: 0.000001,\n} as const;\n\n/**\n * Configuration for a specific number range formatting\n */\nexport type FiatRangeConfig = {\n /**\n * The condition to match for this range (e.g., < 0.0001, < 1, >= 1000)\n * Function should return true if this config should be applied\n */\n condition: (value: number) => boolean;\n /** Minimum decimal places for this range */\n minimumDecimals: number;\n /** Maximum decimal places for this range */\n maximumDecimals: number;\n /** Optional threshold for formatWithThreshold (defaults to the range boundary) */\n threshold?: number;\n /** Optional significant digits for this range (overrides decimal places when set) */\n significantDigits?: number;\n /** Optional custom formatting logic for this range */\n customFormat?: (value: number, locale: string, currency: string) => string;\n /** Optional flag to strip trailing zeros for this range (overrides global stripTrailingZeros option) */\n stripTrailingZeros?: boolean;\n /**\n * Optional flag for fiat-style stripping (only strips .00, preserves meaningful decimals like .10, .40)\n * When true, \"$1,250.00\" → \"$1,250\" but \"$1,250.10\" stays \"$1,250.10\"\n * When false (default), strips all trailing zeros: \"$1,250.10\" → \"$1,250.1\"\n */\n fiatStyleStripping?: boolean;\n};\n\n/**\n * Formats a number to a specific number of significant digits\n * Strips trailing zeros unless minDecimals requires them\n *\n * @param value - The numeric value to format\n * @param significantDigits - Number of significant digits to maintain\n * @param minDecimals - Minimum decimal places to show (may add zeros)\n * @param maxDecimals - Maximum decimal places allowed\n * @returns Formatted number with appropriate precision, trailing zeros removed\n */\nexport function formatWithSignificantDigits(\n value: number,\n significantDigits: number,\n minDecimals?: number,\n maxDecimals?: number,\n): { value: number; decimals: number } {\n // Handle special cases\n if (value === 0) {\n // Return zero with no trailing decimals by default (matches stripTrailingZeros behavior)\n // Can be overridden by explicit minDecimals if needed\n return { value: 0, decimals: minDecimals ?? 0 };\n }\n\n const absValue = Math.abs(value);\n\n // For numbers >= 1, calculate decimals based on magnitude to achieve target significant figures\n // This ensures consistent precision across different price ranges:\n // Examples with 4 significant figures:\n // $123,456.78 → $123,456.78 (≥$1000: 2 decimals minimum, 8 sig figs)\n // $456.12 → $456.12 (≥$10: 2 decimals minimum, 5 sig figs)\n // $56.123 → $56.123 (≥$10: 2 decimals minimum, 5 sig figs)\n // $5.123 → $5.123 ($1-$10: 3 decimals = 4 sig figs)\n // $2.801 → $2.801 ($1-$10: 3 decimals = 4 sig figs)\n // $1.234 → $1.234 ($1-$10: 3 decimals = 4 sig figs)\n if (absValue >= 1) {\n let targetDecimals: number;\n\n // Calculate decimals needed based on integer digits to achieve target significant figures\n // For $38.388 with 5 sig figs: 2 integer digits, need 3 decimals (3,8,3,8,8)\n // For $123.45 with 5 sig figs: 3 integer digits, need 2 decimals (1,2,3,4,5)\n const integerDigits = Math.floor(Math.log10(absValue)) + 1;\n const decimalsNeeded = significantDigits - integerDigits;\n targetDecimals = Math.max(decimalsNeeded, 0); // Can't have negative decimals\n\n // Apply explicit minimum decimals constraint if provided (for special cases)\n if (minDecimals !== undefined && targetDecimals < minDecimals) {\n targetDecimals = minDecimals;\n }\n\n // Apply maximum decimals constraint if specified\n const finalDecimals =\n maxDecimals === undefined\n ? targetDecimals\n : Math.min(targetDecimals, maxDecimals);\n\n // Round to prevent floating-point artifacts (e.g., 2.820000000000003 → 2.82)\n const roundedValue = Number(value.toFixed(finalDecimals));\n\n return {\n value: roundedValue,\n decimals: finalDecimals,\n };\n }\n\n // For numbers < 1, use toPrecision to limit to significantDigits\n // Examples: 0.1234, 0.01234 should show exactly 4 sig figs\n const precisionStr = absValue.toPrecision(significantDigits);\n const precisionNum = parseFloat(precisionStr);\n\n // Convert to string to count actual decimals after trailing zeros are removed\n const valueStr = precisionNum.toString();\n const [, decPart = ''] = valueStr.split('.');\n let actualDecimals = decPart.length;\n\n // Apply min/max decimal constraints\n if (minDecimals !== undefined && actualDecimals < minDecimals) {\n actualDecimals = minDecimals; // Will add zeros if needed\n }\n if (maxDecimals !== undefined && actualDecimals > maxDecimals) {\n actualDecimals = maxDecimals;\n }\n\n // Return the value with sign restored and decimal count\n return {\n value: value < 0 ? -precisionNum : precisionNum,\n decimals: actualDecimals,\n };\n}\n\n/**\n * Minimal view fiat range configuration\n * Uses fiat-style stripping for clean currency display\n * Strips only .00 to avoid partial decimals like $1,250.1\n */\nexport const PRICE_RANGES_MINIMAL_VIEW: FiatRangeConfig[] = [\n {\n // Large values (>= $1000): Strip .00 only ($5,000 not $5,000.00, but $5,000.10 stays)\n condition: (val: number) => Math.abs(val) >= PRICE_THRESHOLD.LARGE,\n minimumDecimals: 2,\n maximumDecimals: 2,\n threshold: PRICE_THRESHOLD.LARGE,\n stripTrailingZeros: true,\n fiatStyleStripping: true,\n },\n {\n // Small values (< $1000): Also use fiat-style stripping ($100 not $100.00, but $13.40 stays)\n condition: () => true,\n minimumDecimals: 2,\n maximumDecimals: 2,\n threshold: PRICE_THRESHOLD.LOW,\n stripTrailingZeros: true,\n fiatStyleStripping: true,\n },\n];\n\n/**\n * Universal price range configuration following comprehensive rules from rules-decimals.md\n *\n * Rules:\n * - Max 6 decimals across all ranges (Hyperliquid limit)\n * - Strip trailing zeros by default\n * - Use |v| (absolute value) for conditions\n *\n * Significant digits by range:\n * - > $100,000: 6 sig digs\n * - $100,000 > x > $0.01: 5 sig digs\n * - < $0.01: 4 sig digs\n *\n * Decimal limits by price range:\n * - |v| > 10,000: min 0, max 0 decimals; 5 sig digs (6 if >100k)\n * - |v| > 1,000: min 0, max 1 decimal; 5 sig digs\n * - |v| > 100: min 0, max 2 decimals; 5 sig digs\n * - |v| > 10: min 0, max 4 decimals; 5 sig digs\n * - |v| ≥ 0.01: 5 sig digs, min 2, max 6 decimals\n * - |v| < 0.01: 4 sig digs, min 2, max 6 decimals\n *\n * Examples:\n * - $123,456.78 → $123,457 (>$10k: 0 decimals, 6 sig figs)\n * - $12,345.67 → $12,346 (>$10k: 0 decimals, 5 sig figs)\n * - $1,234.56 → $1,234.6 ($1k-$10k: 1 decimal, 5 sig figs)\n * - $123.456 → $123.46 ($100-$1k: 2 decimals, 5 sig figs)\n * - $12.34567 → $12.346 ($10-$100: 4 decimals, 5 sig figs)\n * - $1.3445555 → $1.3446 (≥$0.01: 5 sig figs)\n * - $0.333333 → $0.33333 (≥$0.01: 5 sig figs)\n * - $0.004236 → $0.004236 (<$0.01: 4 sig figs, max 6 decimals)\n * - $0.0000006 → $0.000001 (<$0.01: 4 sig figs, rounds with max 6 decimals)\n */\nexport const PRICE_RANGES_UNIVERSAL: FiatRangeConfig[] = [\n {\n // Very high values (> $100,000): No decimals, 6 significant figures\n // Ex: $123,456.78 → $123,457\n condition: (val) => Math.abs(val) > PRICE_THRESHOLD.VERY_HIGH,\n minimumDecimals: 0,\n maximumDecimals: 0,\n significantDigits: 6,\n threshold: PRICE_THRESHOLD.VERY_HIGH,\n },\n {\n // High values ($10,000-$100,000]: No decimals, 5 significant figures\n // Ex: $12,345.67 → $12,346\n condition: (val) => Math.abs(val) > PRICE_THRESHOLD.HIGH,\n minimumDecimals: 0,\n maximumDecimals: 0,\n significantDigits: 5,\n threshold: PRICE_THRESHOLD.HIGH,\n },\n {\n // Large values ($1,000-$10,000]: Max 1 decimal, 5 significant figures\n // Ex: $1,234.56 → $1,234.6\n condition: (val) => Math.abs(val) > PRICE_THRESHOLD.LARGE,\n minimumDecimals: 0,\n maximumDecimals: 1,\n significantDigits: 5,\n threshold: PRICE_THRESHOLD.LARGE,\n },\n {\n // Medium values ($100-$1,000]: Max 2 decimals, 5 significant figures\n // Ex: $123.456 → $123.46\n condition: (val) => Math.abs(val) > PRICE_THRESHOLD.MEDIUM,\n minimumDecimals: 0,\n maximumDecimals: 2,\n significantDigits: 5,\n threshold: PRICE_THRESHOLD.MEDIUM,\n },\n {\n // Medium-low values ($10-$100]: Max 4 decimals, 5 significant figures\n // Ex: $12.34567 → $12.346\n condition: (val) => Math.abs(val) > PRICE_THRESHOLD.MEDIUM_LOW,\n minimumDecimals: 0,\n maximumDecimals: 4,\n significantDigits: 5,\n threshold: PRICE_THRESHOLD.MEDIUM_LOW,\n },\n {\n // Low values ($0.01-$10]: 5 significant figures, min 2 max MAX_PRICE_DECIMALS decimals\n // Ex: $1.3445555 → $1.3446 | $0.333333 → $0.33333\n condition: (val) => Math.abs(val) >= PRICE_THRESHOLD.LOW,\n significantDigits: 5,\n minimumDecimals: 2,\n maximumDecimals: DECIMAL_PRECISION_CONFIG.MaxPriceDecimals,\n threshold: PRICE_THRESHOLD.LOW,\n },\n {\n // Very small values (< $0.01): 4 significant figures, min 2 max MAX_PRICE_DECIMALS decimals\n // Ex: $0.004236 → $0.004236 | $0.0000006 → $0.000001\n condition: () => true,\n significantDigits: 4,\n minimumDecimals: 2,\n maximumDecimals: DECIMAL_PRECISION_CONFIG.MaxPriceDecimals,\n threshold: PRICE_THRESHOLD.VERY_SMALL,\n },\n];\n\n/**\n * Formats a balance value as USD currency with appropriate decimal places\n *\n * @param balance - Raw numeric balance value (e.g., 1234.56, not token minimal denomination)\n * @param options - Optional formatting options\n * @param options.minimumDecimals - Global minimum decimal places (overrides range configs)\n * @param options.maximumDecimals - Global maximum decimal places (overrides range configs)\n * @param options.significantDigits - Global significant digits (overrides decimal settings when set)\n * @param options.ranges - Custom range configurations (defaults to PRICE_RANGES_MINIMAL_VIEW)\n * @param options.currency - Currency code (default: 'USD')\n * @param options.locale - Locale for formatting (default: 'en-US')\n * @param options.stripTrailingZeros - Strip trailing zeros from output (default: false via PRICE_RANGES_MINIMAL_VIEW). When true, overrides minimumDecimals constraint.\n * @returns Formatted currency string with variable decimals based on configured ranges\n * @example\n * // Using defaults (preserves trailing zeros for fiat)\n * formatPerpsFiat(1234.56) => \"$1,234.56\"\n * formatPerpsFiat(1250.00) => \"$1,250.00\" // Trailing zeros preserved\n * formatPerpsFiat(50000) => \"$50,000.00\" // Trailing zeros preserved\n *\n * // Stripping trailing zeros when needed (e.g., for crypto)\n * formatPerpsFiat(1250, { stripTrailingZeros: true }) => \"$1,250\"\n *\n * // With custom ranges\n * formatPerpsFiat(0.00001, {\n * ranges: [\n * { condition: (v) => v < 0.001, minimumDecimals: 6, maximumDecimals: 8 },\n * { condition: () => true, minimumDecimals: 2, maximumDecimals: 2 }\n * ]\n * }) => \"$0.00001\" // Trailing zero stripped\n *\n * // With significant digits\n * formatPerpsFiat(1234.56789, { significantDigits: 5 }) => \"$1,234.6\"\n * formatPerpsFiat(0.0001234, { significantDigits: 3 }) => \"$0.000123\"\n */\nexport const formatPerpsFiat = (\n balance: string | number,\n options?: {\n minimumDecimals?: number;\n maximumDecimals?: number;\n significantDigits?: number;\n ranges?: FiatRangeConfig[];\n currency?: string;\n locale?: string;\n stripTrailingZeros?: boolean;\n },\n): string => {\n const value = typeof balance === 'string' ? parseFloat(balance) : balance;\n const currency = options?.currency ?? 'USD';\n\n let formatted: string;\n\n if (isNaN(value)) {\n // Return placeholder for invalid values to avoid confusion with actual $0 values\n return PERPS_CONSTANTS.FallbackPriceDisplay;\n }\n\n // Use custom ranges or defaults\n const ranges = options?.ranges ?? PRICE_RANGES_MINIMAL_VIEW;\n\n // Find the first matching range configuration\n const rangeConfig = ranges.find((range) => range.condition(value));\n\n if (rangeConfig) {\n // Check for significant digits (global or range-specific)\n const sigDigits =\n options?.significantDigits ?? rangeConfig.significantDigits;\n\n // If significant digits are specified, use them\n if (sigDigits) {\n // Get min/max decimals (global overrides range, range overrides default)\n const minDecimals =\n options?.minimumDecimals ?? rangeConfig.minimumDecimals;\n const maxDecimals =\n options?.maximumDecimals ?? rangeConfig.maximumDecimals;\n\n // Calculate appropriate formatting based on significant digits\n const { value: formattedValue, decimals } = formatWithSignificantDigits(\n value,\n sigDigits,\n minDecimals,\n maxDecimals,\n );\n\n // Format with the calculated decimal places\n formatted = _formatWithThreshold(\n formattedValue,\n rangeConfig.threshold ?? 0.01,\n {\n currency,\n minimumFractionDigits: decimals,\n maximumFractionDigits: decimals,\n },\n );\n } else {\n // Standard decimal-based formatting (existing logic)\n const minDecimals =\n options?.minimumDecimals ?? rangeConfig.minimumDecimals;\n const maxDecimals =\n options?.maximumDecimals ?? rangeConfig.maximumDecimals;\n\n // Use custom formatting if provided\n if (rangeConfig.customFormat) {\n formatted = rangeConfig.customFormat(\n value,\n options?.locale ?? 'en-US',\n currency,\n );\n } else {\n // Use standard formatting with threshold\n formatted = _formatWithThreshold(value, rangeConfig.threshold ?? 0.01, {\n currency,\n minimumFractionDigits: minDecimals,\n maximumFractionDigits: maxDecimals,\n });\n }\n }\n } else {\n // Fallback if no range matches (shouldn't happen with proper default config)\n const fallbackMin = options?.minimumDecimals ?? 2;\n const fallbackMax = options?.maximumDecimals ?? 2;\n formatted = _formatWithThreshold(value, 0.01, {\n currency,\n minimumFractionDigits: fallbackMin,\n maximumFractionDigits: fallbackMax,\n });\n }\n\n // Post-process: strip trailing zeros unless explicitly disabled\n // Priority: explicit options.stripTrailingZeros false > rangeConfig > options default > true\n // If options.stripTrailingZeros is explicitly false, skip stripping entirely\n if (options?.stripTrailingZeros === false) {\n return formatted;\n }\n\n // Otherwise check range config or default to true\n const shouldStrip =\n rangeConfig?.stripTrailingZeros ?? options?.stripTrailingZeros ?? true;\n\n if (shouldStrip) {\n // Check if fiat-style stripping is enabled (only strips .00)\n const useFiatStyle = rangeConfig?.fiatStyleStripping ?? false;\n\n if (useFiatStyle) {\n // Fiat-style: Only strip .00 (no meaningful decimals), preserve 2-decimal format\n // Examples: $1,250.00 → $1,250 | $1,000.10 → $1,000.10 | $13.40 → $13.40\n return formatted.replace(/\\.00$/u, '');\n }\n // Standard: Strip all trailing zeros after decimal point\n // Examples: $1,250.00 → $1,250 | $100.0 → $100 | $10.5 → $10.5 | $1.234 → $1.234\n return formatted.replace(/(\\.\\d*?)0+$/u, '$1').replace(/\\.$/u, '');\n }\n\n return formatted;\n};\n\n/**\n * Formats position size with variable decimal precision based on magnitude or asset-specific decimals\n * Removes trailing zeros to match task requirements\n *\n * @param size - Raw position size value\n * @param szDecimals - Optional asset-specific decimal precision from Hyperliquid metadata (e.g., BTC=5, ETH=4, DOGE=1)\n * @returns Format varies by size or uses asset-specific decimals, with trailing zeros removed:\n * If szDecimals provided: Uses exact decimals (e.g., 0.00009 BTC with szDecimals=5 => \"0.00009\")\n * Otherwise falls back to magnitude-based logic:\n * - Size < 0.01: Up to 6 decimals (e.g., \"0.00009\" not \"0.000090\")\n * - Size < 1: Up to 4 decimals (e.g., \"0.0024\" not \"0.002400\")\n * - Size >= 1: Up to 2 decimals (e.g., \"44\" not \"44.00\")\n * @example formatPositionSize(0.00009, 5) => \"0.00009\" (uses szDecimals)\n * @example formatPositionSize(44.00, 1) => \"44\" (uses szDecimals, trailing zeros removed)\n * @example formatPositionSize(0.0024) => \"0.0024\" (no szDecimals, uses magnitude logic)\n * @example formatPositionSize(44.00) => \"44\" (no szDecimals, uses magnitude logic)\n */\nexport const formatPositionSize = (\n size: string | number,\n szDecimals?: number,\n): string => {\n const value = typeof size === 'string' ? parseFloat(size) : size;\n\n if (isNaN(value) || value === 0) {\n return '0';\n }\n\n // Use asset-specific decimals if provided (Hyperliquid metadata)\n if (szDecimals !== undefined) {\n return value.toFixed(szDecimals).replace(/\\.?0+$/u, '');\n }\n\n // Fallback: magnitude-based decimal logic for backwards compatibility\n const abs = Math.abs(value);\n let formatted: string;\n\n if (abs < 0.01) {\n // For very small numbers, use more decimal places\n formatted = value.toFixed(6);\n } else if (abs < 1) {\n // For small numbers, use 4 decimal places\n formatted = value.toFixed(4);\n } else {\n // For normal numbers, use 2 decimal places\n formatted = value.toFixed(2);\n }\n\n // Remove trailing zeros and unnecessary decimal point\n return formatted.replace(/\\.?0+$/u, '');\n};\n\n/**\n * Formats a PnL (Profit and Loss) value with sign prefix\n *\n * @param pnl - Raw numeric PnL value (positive for profit, negative for loss)\n * @returns Format: \"+$X,XXX.XX\" or \"-$X,XXX.XX\" (always shows sign, 2 decimals)\n * @example formatPnl(1234.56) => \"+$1,234.56\"\n * @example formatPnl(-500) => \"-$500.00\"\n * @example formatPnl(0) => \"+$0.00\"\n */\nexport const formatPnl = (pnl: string | number): string => {\n const value = typeof pnl === 'string' ? parseFloat(pnl) : pnl;\n\n if (isNaN(value)) {\n return PERPS_CONSTANTS.ZeroAmountDetailedDisplay;\n }\n\n const formatted = _formatCurrency(Math.abs(value), 'USD', {\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n });\n\n return value >= 0 ? `+${formatted}` : `-${formatted}`;\n};\n\n/**\n * Formats a percentage value with sign prefix\n *\n * @param value - Raw percentage value (e.g., 5.25 for 5.25%, not 0.0525)\n * @param decimals - Number of decimal places to show (default: 2)\n * @returns Format: \"+X.XX%\" or \"-X.XX%\" (always shows sign, 2 decimals)\n * @example formatPercentage(5.25) => \"+5.25%\"\n * @example formatPercentage(-2.75) => \"-2.75%\"\n * @example formatPercentage(0) => \"+0.00%\"\n */\nexport const formatPercentage = (\n value: string | number,\n decimals: number = 2,\n): string => {\n const parsed = typeof value === 'string' ? parseFloat(value) : value;\n\n if (isNaN(parsed)) {\n return '0.00%';\n }\n\n return `${parsed >= 0 ? '+' : ''}${parsed.toFixed(decimals)}%`;\n};\n\n/**\n * Formats funding rate for display\n *\n * @param value - Raw funding rate value (decimal, not percentage)\n * @param options - Optional formatting options\n * @param options.showZero - Whether to return zero display value for zero/undefined (default: true)\n * @returns Formatted funding rate as percentage string\n * @example formatFundingRate(0.0005) => \"0.0500%\"\n * @example formatFundingRate(-0.0001) => \"-0.0100%\"\n * @example formatFundingRate(undefined) => \"0.0000%\"\n */\nexport const formatFundingRate = (\n value?: number | null,\n options?: { showZero?: boolean },\n): string => {\n const showZero = options?.showZero ?? true;\n\n if (value === undefined || value === null) {\n return showZero ? FUNDING_RATE_CONFIG.ZeroDisplay : '';\n }\n\n const percentage = value * FUNDING_RATE_CONFIG.PercentageMultiplier;\n const formatted = percentage.toFixed(FUNDING_RATE_CONFIG.Decimals);\n\n // Check if the result is effectively zero\n if (showZero && parseFloat(formatted) === 0) {\n return FUNDING_RATE_CONFIG.ZeroDisplay;\n }\n\n return `${formatted}%`;\n};\n"]}