@reyaxyz/common 0.364.0 → 0.364.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.
@@ -1,8 +1,6 @@
1
1
  import { mergeMap } from './struct';
2
2
  import BigNumber from 'bignumber.js';
3
3
 
4
- export const POOL_IMR = 9.1;
5
-
6
4
  type MarketId = string;
7
5
  type Address = string;
8
6
 
@@ -53,148 +51,6 @@ export const calculateMaxWithdraw = ({
53
51
  return Math.max(0, Math.min(realBalance, maxFromMargin));
54
52
  };
55
53
 
56
- export const calculateMaxExposure = ({
57
- poolBalance,
58
- riskFactor,
59
- poolIMR = POOL_IMR,
60
- }: {
61
- poolBalance: number;
62
- poolIMR?: number;
63
- riskFactor: number;
64
- }) => poolBalance / (poolIMR * Math.sqrt(riskFactor));
65
-
66
- /**
67
- * Splits a trade exposure into rebalancing and unbalancing portions relative to pool net exposure.
68
- * Rebalancing reduces pool's absolute exposure; unbalancing increases it.
69
- */
70
- export const splitTradeExposure = (
71
- tradeExposure: number,
72
- poolNetExposure: number,
73
- ): { rebalancing: number; unbalancing: number } => {
74
- if (tradeExposure === 0) {
75
- return { rebalancing: 0, unbalancing: 0 };
76
- }
77
-
78
- // If pool has no exposure, entire trade is unbalancing
79
- if (poolNetExposure === 0) {
80
- return { rebalancing: 0, unbalancing: tradeExposure };
81
- }
82
-
83
- // Opposite sign means trade increases pool exposure → pure unbalancing
84
- const sameSign =
85
- (poolNetExposure > 0 && tradeExposure > 0) ||
86
- (poolNetExposure < 0 && tradeExposure < 0);
87
- if (!sameSign) {
88
- return { rebalancing: 0, unbalancing: tradeExposure };
89
- }
90
-
91
- // Same sign: trade reduces pool exposure (pool takes opposite side)
92
- const absPool = Math.abs(poolNetExposure);
93
- const absTrade = Math.abs(tradeExposure);
94
-
95
- if (absTrade <= absPool) {
96
- // Pure rebalancing — trade doesn't cross zero
97
- return { rebalancing: tradeExposure, unbalancing: 0 };
98
- }
99
-
100
- // Mixed: rebalance up to pool exposure, then unbalance the rest
101
- const rebalancing = poolNetExposure;
102
- const unbalancing = tradeExposure - rebalancing;
103
- return { rebalancing, unbalancing };
104
- };
105
-
106
- /**
107
- * Computes the new log price multiplier (logF) after a trade, handling both
108
- * rebalancing and unbalancing portions.
109
- *
110
- * Mirrors on-chain logic in PriceMultiplier.sol:computeNewLogPriceMultiplier.
111
- */
112
- export const computeNewLogF = ({
113
- currentLogF,
114
- tradeExposure,
115
- poolNetExposure,
116
- depthFactor,
117
- maxExposure,
118
- }: {
119
- currentLogF: number;
120
- tradeExposure: number;
121
- poolNetExposure: number;
122
- depthFactor: number;
123
- maxExposure: number;
124
- }): number => {
125
- if (tradeExposure === 0) return currentLogF;
126
-
127
- const { rebalancing, unbalancing } = splitTradeExposure(
128
- tradeExposure,
129
- poolNetExposure,
130
- );
131
-
132
- // Case 1: Has unbalancing portion (includes mixed trades)
133
- if (unbalancing !== 0) {
134
- // If also rebalancing, it brings logF to 0 by definition (trade crosses zero)
135
- const startLogF = rebalancing === 0 ? currentLogF : 0;
136
- return startLogF + unbalancing / (depthFactor * maxExposure);
137
- }
138
-
139
- // Case 2: Only rebalancing
140
- // Both rebalancing and poolNetExposure have the same sign, so ratio is in [0, 1]
141
- if (rebalancing !== 0 && poolNetExposure !== 0) {
142
- const ratio = rebalancing / poolNetExposure;
143
- return currentLogF * (1 - ratio);
144
- }
145
-
146
- return currentLogF;
147
- };
148
-
149
- /**
150
- * Estimates the execution price for a trade using the exponential logPriceMultiplier formula.
151
- *
152
- * Mirrors on-chain logic:
153
- * 1. computeNewLogPriceMultiplier (PriceMultiplier.sol)
154
- * 2. computeAmmPrice: ammPrice = oraclePrice * exp(logF)
155
- * 3. computeSignedSpread: executionPrice = ammPrice * (1 + signedSpread)
156
- *
157
- * @audit spreadDiscount not applied — known limitation, to be addressed later
158
- * @audit priceSpacing rounding not applied
159
- */
160
- export const calculateEstimatedExecutionPrice = ({
161
- oraclePrice,
162
- currentLogF,
163
- tradeExposure,
164
- poolNetExposure,
165
- depthFactor,
166
- maxExposure,
167
- priceSpread,
168
- }: {
169
- oraclePrice: number;
170
- currentLogF: number;
171
- tradeExposure: number;
172
- poolNetExposure: number;
173
- depthFactor: number;
174
- maxExposure: number;
175
- priceSpread: number;
176
- }): number => {
177
- if (tradeExposure === 0) {
178
- return oraclePrice * Math.exp(currentLogF);
179
- }
180
-
181
- // 1. Compute new logF after trade impact
182
- const newLogF = computeNewLogF({
183
- currentLogF,
184
- tradeExposure,
185
- poolNetExposure,
186
- depthFactor,
187
- maxExposure,
188
- });
189
-
190
- // 2. Compute AMM price: oraclePrice * exp(newLogF)
191
- const ammPrice = oraclePrice * Math.exp(newLogF);
192
-
193
- // 3. Apply signed spread
194
- const signedSpread = tradeExposure > 0 ? priceSpread : -priceSpread;
195
- return Math.max(0, ammPrice * (1 + signedSpread));
196
- };
197
-
198
54
  export const calculateTradeFee = ({
199
55
  feeRate,
200
56
  tradeNotional,
@@ -203,42 +59,6 @@ export const calculateTradeFee = ({
203
59
  tradeNotional: number;
204
60
  }) => feeRate * Math.abs(tradeNotional);
205
61
 
206
- export const calculateSpread = ({
207
- depthFactor,
208
- maxExposure,
209
- oraclePrice,
210
- poolPrice,
211
- tradeNotional,
212
- }: {
213
- depthFactor: number;
214
- maxExposure: number;
215
- oraclePrice: number;
216
- poolPrice: number;
217
- tradeNotional: number;
218
- }) =>
219
- (poolPrice / oraclePrice) *
220
- (1 + tradeNotional / (depthFactor * maxExposure) - 1) *
221
- tradeNotional;
222
-
223
- export const calculateEstimatedLiquidationPrice = ({
224
- LMR,
225
- accountBalance,
226
- isShort,
227
- notional,
228
- oraclePrice,
229
- }: {
230
- LMR: number;
231
- accountBalance: number;
232
- isShort: boolean;
233
- notional: number;
234
- oraclePrice: number;
235
- }) =>
236
- Math.max(
237
- 0,
238
- oraclePrice *
239
- (1 - ((accountBalance - LMR) / notional) * (isShort ? -1 : 1)),
240
- );
241
-
242
62
  export const calculateLeverage = ({
243
63
  imrMultiplier,
244
64
  riskFactor,
@@ -262,212 +82,6 @@ export const calculateRequiredMargin = ({
262
82
  (Math.abs(currentNotional + tradeNotional) - Math.abs(currentNotional)) /
263
83
  leverage;
264
84
 
265
- /**
266
- * Calculates the maximum notional trade size (unsigned, always ≥ 0).
267
- *
268
- * Implements the UI quant spec v2 ("Maximum trade") with two cases:
269
- * Case 1 — Extending: trade same direction as existing position.
270
- * Case 2 — Reverting: decompose into (a) close position exactly, then
271
- * (b) extend from zero in the opposite direction.
272
- *
273
- * All quadratics use the unsigned convention (t = |tradeNotional|) so the
274
- * positive root always corresponds to the correct direction.
275
- */
276
- export const calculateMaxOrderSize = ({
277
- currIMR,
278
- currNodeBalance,
279
- currNotional,
280
- depthFactor,
281
- feeRate,
282
- isShortTrade,
283
- leverage,
284
- maxExposure,
285
- oraclePrice,
286
- poolPrice,
287
- currentLogF,
288
- netExposure,
289
- }: {
290
- currIMR: number;
291
- currNodeBalance: number;
292
- currNotional: number;
293
- depthFactor: number;
294
- feeRate: number;
295
- isShortTrade: boolean;
296
- leverage: number;
297
- maxExposure: number;
298
- oraclePrice: number;
299
- poolPrice: number;
300
- currentLogF: number;
301
- netExposure: number;
302
- }): number => {
303
- const D = depthFactor * maxExposure;
304
- const rho = poolPrice / oraclePrice;
305
- const tradeSign = isShortTrade ? -1 : 1;
306
- const s0 = currNotional > 0 ? 1 : currNotional < 0 ? -1 : tradeSign;
307
- const absN0 = Math.abs(currNotional);
308
-
309
- /**
310
- * Solve the extending quadratic (spec §Case 1) for max trade size.
311
- * a·t² + b·t + c = 0 where a = ρ·γ, b = s₀·(1/lev + fee) + (ρ−1), c = −surplus.
312
- * Root selection: pick the root whose sign matches s₀ (dir).
313
- * For rebalancing (a < 0), caps at |NE| (beyond which γ switches to unbalancing).
314
- */
315
- const solveExtending = (
316
- dir: number,
317
- surplus: number, // aka, IMRDelta
318
- rho_: number,
319
- gamma_: number,
320
- isRebal: boolean,
321
- absNE: number,
322
- ): number => {
323
- if (surplus <= 0) return 0;
324
- const a = rho_ * gamma_;
325
- const b = dir * (1 / leverage + feeRate) + (rho_ - 1);
326
- const c = -surplus;
327
- // Degenerate: γ = 0 → linear equation b·t + c = 0 (rebalancing with f = 0)
328
- if (a === 0) {
329
- if (b === 0) return isRebal ? absNE : 0;
330
- const t = -c / b;
331
- if (t * dir <= 0) return isRebal ? absNE : 0;
332
- return isRebal ? Math.min(Math.abs(t), absNE) : Math.abs(t);
333
- }
334
- const disc = b * b - 4 * a * c;
335
- if (isRebal) {
336
- if (disc < 0) return absNE;
337
- const t = (-b + dir * Math.sqrt(disc)) / (2 * a);
338
- return t * dir <= 0 ? absNE : Math.min(Math.abs(t), absNE);
339
- }
340
- if (disc < 0)
341
- throw new Error(`calculateMaxOrderSize: negative discriminant (${disc})`);
342
- const t = (-b + dir * Math.sqrt(disc)) / (2 * a);
343
- if (t * dir <= 0)
344
- throw new Error(`calculateMaxOrderSize: no valid root (${t})`);
345
- return Math.abs(t);
346
- };
347
-
348
- // Determine γ for a trade of given direction against the current pool state
349
- const getGamma = (dir: number, logF: number, ne: number) => {
350
- const isRebal = ne !== 0 && dir === Math.sign(ne);
351
- const gamma = isRebal ? -logF / ne : 1 / D;
352
- return { gamma, isRebal };
353
- };
354
-
355
- /**
356
- * Like solveExtending, but when the rebalancing leg is capped at |NE|
357
- * (pool-flip), continues with the unbalancing leg from NE = 0.
358
- *
359
- * For the exact rebalancing trade to NE = 0:
360
- * execRatio = ρ·exp(γ·dir·|NE|) = exp(f)·exp(−f) = 1
361
- * so only fee and IMR consume margin. Then extend with γ = 1/D and ρ' = 1.
362
- */
363
- const extendWithFlip = (
364
- dir: number,
365
- surplus: number,
366
- rho_: number,
367
- gamma_: number,
368
- isRebal: boolean,
369
- absNE: number,
370
- ): number => {
371
- const result = solveExtending(dir, surplus, rho_, gamma_, isRebal, absNE);
372
- if (!isRebal || result < absNE || absNE === 0) return result;
373
-
374
- // Pool-flip: rebalancing consumed absNE, continue unbalancing from NE = 0
375
- const surplusAfter = surplus - absNE * (1 / leverage + feeRate);
376
- if (surplusAfter <= 0) return absNE;
377
- return absNE + solveExtending(dir, surplusAfter, 1, 1 / D, false, 0);
378
- };
379
-
380
- // ── Case 1: Extending ──────────────────────────────────────────────────
381
- if (tradeSign === s0) {
382
- const { gamma, isRebal } = getGamma(tradeSign, currentLogF, netExposure);
383
- return extendWithFlip(
384
- s0,
385
- currNodeBalance - currIMR,
386
- rho,
387
- gamma,
388
- isRebal,
389
- Math.abs(netExposure),
390
- );
391
- }
392
-
393
- // ── Case 2: Reverting — close N₀, then re-extend ──────────────────────
394
-
395
- // Step 1: Close N₀ entirely (spec §Case 2, Step 1)
396
- const closeTrade = -currNotional;
397
- const closeDir = -s0;
398
- const { gamma: gammaC, isRebal: closeIsRebal } = getGamma(
399
- closeDir,
400
- currentLogF,
401
- netExposure,
402
- );
403
- const closeFlipsPool = closeIsRebal && absN0 > Math.abs(netExposure);
404
-
405
- // Exec price ratio: ρ·exp(γ_c·closeTrade) for non-flip, dedicated formula for flip
406
- const closeExecRatio = closeFlipsPool
407
- ? Math.exp((closeTrade - netExposure) / D)
408
- : rho * Math.exp(gammaC * closeTrade);
409
-
410
- const balClose =
411
- currNodeBalance - feeRate * absN0 + closeTrade * (1 - closeExecRatio);
412
- const imrClose = currIMR - absN0 / leverage;
413
- const surplusClose = balClose - imrClose;
414
-
415
- // Insolvency: can't fully close without going under margin.
416
- // Solve partial-close quadratic (spec appendix): b has flipped feeRate sign.
417
- // Falls back to full close (|N₀|) when no valid partial-close root exists.
418
- if (surplusClose <= 0) {
419
- const IMRDelta = currNodeBalance - currIMR;
420
- const { gamma: gammaP } = getGamma(closeDir, currentLogF, netExposure);
421
- const a = rho * gammaP;
422
- const b = s0 * (1 / leverage - feeRate) + (rho - 1);
423
- const c = -IMRDelta;
424
-
425
- if (a === 0) {
426
- if (b === 0) return absN0;
427
- const t = -c / b;
428
- if (t * closeDir <= 0) return absN0;
429
- return Math.min(Math.abs(t), absN0);
430
- }
431
-
432
- const disc = b * b - 4 * a * c;
433
- if (disc < 0) return absN0;
434
- const t = (-b + closeDir * Math.sqrt(disc)) / (2 * a);
435
- if (t * closeDir <= 0) return absN0;
436
- return Math.min(Math.abs(t), absN0);
437
- }
438
-
439
- // Step 2: Extend from zero in -s₀ direction with post-close pool state
440
- const neAfter = netExposure + currNotional;
441
- let fPrime: number;
442
- if (closeFlipsPool) {
443
- fPrime = (closeTrade - netExposure) / D;
444
- } else if (closeIsRebal) {
445
- fPrime = currentLogF * (1 + currNotional / netExposure);
446
- } else {
447
- fPrime = currentLogF + closeTrade / D;
448
- }
449
- const rhoPrime = Math.exp(fPrime);
450
-
451
- const extDir = -s0;
452
- const { gamma: gammaPrime, isRebal: isExtRebal } = getGamma(
453
- extDir,
454
- fPrime,
455
- neAfter,
456
- );
457
-
458
- return (
459
- absN0 +
460
- extendWithFlip(
461
- extDir,
462
- surplusClose,
463
- rhoPrime,
464
- gammaPrime,
465
- isExtRebal,
466
- Math.abs(neAfter),
467
- )
468
- );
469
- };
470
-
471
85
  export const calculateIMR = ({
472
86
  marketLeverages,
473
87
  marketPrices,
@@ -520,29 +134,6 @@ export const calculateLMR = ({
520
134
  : sum;
521
135
  }, 0);
522
136
 
523
- // if account balance is below LMR position is liquidable
524
-
525
- export const calculateMarginRatio = ({
526
- LMR,
527
- totalBalance,
528
- tradeCost,
529
- }: {
530
- LMR: number;
531
- totalBalance: number;
532
- tradeCost: number;
533
- }) => {
534
- if (LMR === 0) {
535
- return 0;
536
- }
537
- if (tradeCost > totalBalance) {
538
- return 1;
539
- }
540
- if (totalBalance === tradeCost) {
541
- return Infinity;
542
- }
543
- return Math.min(1, LMR / (totalBalance - tradeCost));
544
- };
545
-
546
137
  export const calculateTotalMargin = ({
547
138
  marketsTrackers,
548
139
  accountBalances,
@@ -703,94 +294,6 @@ export const calculatePositionAdjustedQty = (
703
294
  return origQty.times(marketBaseMultiplier).div(positionBaseMultiplier);
704
295
  };
705
296
 
706
- /**
707
- * Linearly interpolate a depth ratio from a (thresholds, ratios) curve.
708
- * The curve implicitly starts at (exposure=0, ratio=1.0).
709
- * - Between thresholds: linear interpolation
710
- * - Above last threshold: last ratio (flat extrapolation)
711
- *
712
- * @param thresholds - Sorted ascending exposure thresholds in notional USD (must be non-empty)
713
- * @param ratios - Depth ratio at each threshold (same length as thresholds, must be non-empty)
714
- * @param exposure - Absolute exposure notional (must be >= 0)
715
- * @throws if thresholds or ratios are empty, or if they have different lengths
716
- */
717
- export const interpolateDepthRatio = (
718
- thresholds: number[],
719
- ratios: number[],
720
- exposure: number,
721
- ): number => {
722
- if (exposure < 0) {
723
- throw new Error('interpolateDepthRatio: exposure must be >= 0');
724
- }
725
- if (thresholds.length === 0 || ratios.length === 0) {
726
- throw new Error(
727
- 'interpolateDepthRatio: thresholds and ratios must be non-empty',
728
- );
729
- }
730
- if (thresholds.length !== ratios.length) {
731
- throw new Error(
732
- 'interpolateDepthRatio: thresholds and ratios must have equal length',
733
- );
734
- }
735
-
736
- const allThresholds = [0, ...thresholds];
737
- const allRatios = [1.0, ...ratios];
738
-
739
- if (exposure === 0) return 1.0;
740
- if (exposure >= allThresholds[allThresholds.length - 1]) {
741
- return allRatios[allRatios.length - 1];
742
- }
743
-
744
- for (let i = 0; i < allThresholds.length - 1; i++) {
745
- if (exposure <= allThresholds[i + 1]) {
746
- const t =
747
- (exposure - allThresholds[i]) /
748
- (allThresholds[i + 1] - allThresholds[i]);
749
- return allRatios[i] + t * (allRatios[i + 1] - allRatios[i]);
750
- }
751
- }
752
-
753
- throw new Error('interpolateDepthRatio: unreachable');
754
- };
755
-
756
- /**
757
- * Calculate the dynamic depth factor for a trade given pool state and config.
758
- *
759
- * The pool takes the opposite side of the trade, so:
760
- * poolNotionalAfterTrade = currentPoolNotional - signedTradeNotional
761
- *
762
- * The depth ratio is interpolated from the exposure curve, then multiplied
763
- * by baseDepth to produce the final depth factor.
764
- *
765
- * @param baseDepth - Base depth multiplier for this market
766
- * @param thresholds - Sorted ascending exposure thresholds (notional USD)
767
- * @param ratios - Depth ratio at each threshold
768
- * @param currentPoolNotional - Pool's current notional in this market (signed)
769
- * @param signedTradeNotional - Trade notional (positive=buy, negative=sell)
770
- */
771
- export const calculateDynamicDepthFactor = ({
772
- baseDepth,
773
- thresholds,
774
- ratios,
775
- currentPoolNotional,
776
- signedTradeNotional,
777
- }: {
778
- baseDepth: number;
779
- thresholds: number[];
780
- ratios: number[];
781
- currentPoolNotional: number;
782
- signedTradeNotional: number;
783
- }): number => {
784
- const poolNotionalAfterTrade = currentPoolNotional - signedTradeNotional;
785
- const absExposureNotional = Math.abs(poolNotionalAfterTrade);
786
- const depthRatio = interpolateDepthRatio(
787
- thresholds,
788
- ratios,
789
- absExposureNotional,
790
- );
791
- return depthRatio * baseDepth;
792
- };
793
-
794
297
  export const calculatePositionAdlPnL = ({
795
298
  marketTrackerAdlUnwindPrice,
796
299
  marketTrackerBaseMultiplier,