@the-situation/collateral 0.5.0-alpha.0

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.
@@ -0,0 +1,786 @@
1
+ /**
2
+ * Off-chain collateral computation for trading between normal distributions.
3
+ *
4
+ * Computes the collateral requirement for transitioning from distribution f to g:
5
+ * C = max(0, sup_x[f(x) - g(x)]) = max(0, -inf_x[g(x) - f(x)])
6
+ *
7
+ * Uses Newton-Raphson minimization to find x* where d(x) = g(x) - f(x) achieves
8
+ * its infimum. The smart contracts verify but don't compute - this computation
9
+ * must be done locally.
10
+ *
11
+ * @module
12
+ */
13
+ import { HALF, ONE, SQ128x128, ZERO } from '@the-situation/core';
14
+ // ============================================================================
15
+ // AMM Scaling Constants
16
+ // ============================================================================
17
+ /**
18
+ * sqrt(π) ≈ 1.7724538509055159
19
+ *
20
+ * Used for L2 norm and lambda computation.
21
+ */
22
+ export const SQRT_PI = Math.sqrt(Math.PI);
23
+ /**
24
+ * Computes the L2 norm of a normal distribution's PDF.
25
+ *
26
+ * For a normal PDF with standard deviation σ:
27
+ * ||p||_2 = 1 / sqrt(2 * σ * sqrt(π))
28
+ *
29
+ * @param sigma - The standard deviation (as number)
30
+ * @returns The L2 norm
31
+ */
32
+ export function computeL2NormNumber(sigma) {
33
+ if (sigma <= 0) {
34
+ return 0;
35
+ }
36
+ // ||p||_2 = 1 / sqrt(2 * sigma * sqrt(pi))
37
+ const denomSq = 2 * sigma * SQRT_PI;
38
+ return 1 / Math.sqrt(denomSq);
39
+ }
40
+ /**
41
+ * Computes the lambda scaling factor for a distribution.
42
+ *
43
+ * lambda = k / ||p||_2 = k * sqrt(2 * σ * sqrt(π))
44
+ *
45
+ * @param sigma - The distribution's standard deviation (as number)
46
+ * @param k - The AMM invariant parameter (as number)
47
+ * @returns The lambda scaling factor
48
+ */
49
+ export function computeLambdaNumber(sigma, k) {
50
+ const l2Norm = computeL2NormNumber(sigma);
51
+ if (l2Norm <= 0) {
52
+ return 0;
53
+ }
54
+ return k / l2Norm;
55
+ }
56
+ /**
57
+ * Computes the L2 norm of a normal distribution's PDF.
58
+ *
59
+ * For a normal PDF with standard deviation σ:
60
+ * ||p||_2 = 1 / sqrt(2 * σ * sqrt(π))
61
+ *
62
+ * @param sigma - The standard deviation
63
+ * @returns The L2 norm, or null if computation fails
64
+ */
65
+ export function computeL2Norm(sigma) {
66
+ if (sigma.isZero()) {
67
+ return null;
68
+ }
69
+ const sigmaNum = sigma.toNumber();
70
+ const l2NormNum = computeL2NormNumber(sigmaNum);
71
+ return SQ128x128.fromNumber(l2NormNum);
72
+ }
73
+ /**
74
+ * Computes the lambda scaling factor for a distribution.
75
+ *
76
+ * lambda = k / ||p||_2 = k * sqrt(2 * σ * sqrt(π))
77
+ *
78
+ * This is the multiplier applied to the base PDF to create a scaled position
79
+ * that satisfies the AMM invariant ||f||_2 = k.
80
+ *
81
+ * @param sigma - The distribution's standard deviation
82
+ * @param k - The AMM invariant parameter
83
+ * @returns The lambda scaling factor, or null if computation fails
84
+ */
85
+ export function computeLambda(sigma, k) {
86
+ const l2Norm = computeL2Norm(sigma);
87
+ if (!l2Norm || l2Norm.isZero()) {
88
+ return null;
89
+ }
90
+ return k.div(l2Norm);
91
+ }
92
+ /**
93
+ * Returns the default minimization policy.
94
+ */
95
+ export function defaultPolicy() {
96
+ const four = SQ128x128.fromNumber(4);
97
+ const maxMean = SQ128x128.fromNumber(1048576); // 2^20
98
+ return {
99
+ maxSigmaRatio: four,
100
+ maxMeanSepSigmas: four,
101
+ maxAbsoluteMean: maxMean,
102
+ };
103
+ }
104
+ /**
105
+ * Returns a relaxed minimization policy for close position scenarios.
106
+ *
107
+ * The default policy (4σ limits) may be too restrictive for closing positions
108
+ * when the market has moved significantly. This relaxed policy allows the
109
+ * offchain solver to attempt computation for larger separations.
110
+ *
111
+ * The actual validation is done by calling `check_close_position` on-chain,
112
+ * which validates the computed x* without the artificial solver limits.
113
+ */
114
+ export function relaxedPolicy() {
115
+ const sixteen = SQ128x128.fromNumber(16);
116
+ const maxMean = SQ128x128.fromNumber(1048576); // 2^20
117
+ return {
118
+ maxSigmaRatio: sixteen,
119
+ maxMeanSepSigmas: sixteen,
120
+ maxAbsoluteMean: maxMean,
121
+ };
122
+ }
123
+ // ============================================================================
124
+ // PDF Difference Functions
125
+ // ============================================================================
126
+ /**
127
+ * Computes the pointwise PDF difference d(x) = g(x) - f(x).
128
+ *
129
+ * @param f - The "from" distribution
130
+ * @param g - The "to" distribution
131
+ * @param x - The point at which to evaluate
132
+ * @returns The difference g(x) - f(x), or null if computation fails
133
+ */
134
+ export function pdfDifference(f, g, x) {
135
+ const gPdf = g.pdf(x);
136
+ const fPdf = f.pdf(x);
137
+ if (!gPdf || !fPdf) {
138
+ return null;
139
+ }
140
+ return gPdf.sub(fPdf);
141
+ }
142
+ /**
143
+ * Computes d'(x) = g'(x) - f'(x), the derivative of the PDF difference.
144
+ *
145
+ * At a stationary point x*, d'(x*) = 0.
146
+ *
147
+ * @param f - The "from" distribution
148
+ * @param g - The "to" distribution
149
+ * @param x - The point at which to evaluate
150
+ * @returns The derivative difference, or null if computation fails
151
+ */
152
+ export function pdfDifferenceDerivative(f, g, x) {
153
+ const gDeriv = g.pdfDerivative(x);
154
+ const fDeriv = f.pdfDerivative(x);
155
+ if (!gDeriv || !fDeriv) {
156
+ return null;
157
+ }
158
+ return gDeriv.sub(fDeriv);
159
+ }
160
+ /**
161
+ * Computes d''(x) = g''(x) - f''(x), the second derivative of the PDF difference.
162
+ *
163
+ * At a minimum x*, d''(x*) > 0.
164
+ *
165
+ * @param f - The "from" distribution
166
+ * @param g - The "to" distribution
167
+ * @param x - The point at which to evaluate
168
+ * @returns The second derivative difference, or null if computation fails
169
+ */
170
+ export function pdfDifferenceSecondDerivative(f, g, x) {
171
+ const gSecond = g.pdfSecondDerivative(x);
172
+ const fSecond = f.pdfSecondDerivative(x);
173
+ if (!gSecond || !fSecond) {
174
+ return null;
175
+ }
176
+ return gSecond.sub(fSecond);
177
+ }
178
+ // ============================================================================
179
+ // Initial Guess and Collateral Helpers
180
+ // ============================================================================
181
+ /**
182
+ * Suggests an initial guess for Newton-Raphson minimization.
183
+ *
184
+ * The minimum of d(x) = g(x) - f(x) depends on the relationship between f and g:
185
+ *
186
+ * 1. Different means: minimum is on the side of f opposite to g, close to f's mean
187
+ * 2. Same mean (or nearly equal), narrow to wide (σ_f < σ_g): minimum is at the mean
188
+ * 3. Same mean (or nearly equal), wide to narrow (σ_f > σ_g): minimum is in the tails
189
+ *
190
+ * @param f - The "from" distribution
191
+ * @param g - The "to" distribution
192
+ * @returns The suggested initial guess
193
+ */
194
+ export function suggestInitialGuess(f, g) {
195
+ const muF = f.mean;
196
+ const muG = g.mean;
197
+ const sigmaF = f.sigma;
198
+ const sigmaG = g.sigma;
199
+ // Check for near-equal means (within 1% of narrower sigma)
200
+ // This tolerance-based check ensures variance-only trades work even when
201
+ // means differ by tiny floating-point amounts
202
+ const meanDiff = muF.gt(muG) ? muF.sub(muG) : muG.sub(muF);
203
+ const sigmaNarrow = sigmaF.lt(sigmaG) ? sigmaF : sigmaG;
204
+ const thresholdFactor = SQ128x128.fromNumber(0.01);
205
+ const threshold = thresholdFactor ? sigmaNarrow.mul(thresholdFactor) : null;
206
+ const nearlyEqualMeans = !meanDiff || (threshold && meanDiff.lt(threshold));
207
+ // For same mean or nearly equal means (only variance differs)
208
+ if (nearlyEqualMeans) {
209
+ if (sigmaF.lt(sigmaG)) {
210
+ // Narrow to wide: minimum is at the mean
211
+ return muF;
212
+ }
213
+ // Wide to narrow: minimum is in the tails
214
+ // Use 1.0 * σ_f as initial guess - this is closer to the actual minimum
215
+ // than 1.5σ and works better across different variance ratios
216
+ const offset = sigmaF;
217
+ // Return positive side (symmetric case, either side works)
218
+ return muF.add(offset) ?? muF;
219
+ }
220
+ // Mean separation (not near-equal)
221
+ if (!meanDiff) {
222
+ return muF;
223
+ }
224
+ // For combined mean shift + variance change, the minimum location depends on both.
225
+ // When going wide-to-narrow with mean shift, the minimum is in the tail of f
226
+ // on the opposite side of the mean shift, typically around 1σ_f from the mean.
227
+ // When going narrow-to-wide with mean shift, minimum is closer to f's mean.
228
+ const wideToNarrow = sigmaF.gt(sigmaG);
229
+ let offset;
230
+ if (wideToNarrow) {
231
+ // Wide to narrow: minimum is further out in f's tail
232
+ // Use σ_f as the offset - this gives a better starting point
233
+ offset = sigmaF;
234
+ }
235
+ else {
236
+ // Narrow to wide: minimum is closer to f's mean
237
+ // Use half the mean difference, capped at σ_f
238
+ const halfDiff = meanDiff.mul(HALF);
239
+ offset = halfDiff && halfDiff.lt(sigmaF) ? halfDiff : sigmaF;
240
+ }
241
+ if (muG.gt(muF)) {
242
+ // g is to the right, minimum is to the left of f
243
+ const result = muF.sub(offset);
244
+ return result ?? muF;
245
+ }
246
+ // g is to the left, minimum is to the right of f
247
+ const result = muF.add(offset);
248
+ return result ?? muF;
249
+ }
250
+ /**
251
+ * Computes collateral from the minimum value of d(x).
252
+ *
253
+ * C = max(0, -d_min) = max(0, f(x*) - g(x*))
254
+ *
255
+ * @param dMin - The minimum value of d(x) = g(x) - f(x)
256
+ * @returns The collateral requirement
257
+ */
258
+ export function collateralFromMinimum(dMin) {
259
+ const negDMin = dMin.neg();
260
+ if (!negDMin) {
261
+ return ZERO;
262
+ }
263
+ return negDMin.gt(ZERO) ? negDMin : ZERO;
264
+ }
265
+ /**
266
+ * Checks if x is on the correct side of μ_f relative to μ_g.
267
+ *
268
+ * The minimum of d(x) = g(x) - f(x) occurs on the side of f opposite to g:
269
+ * - If μ_g > μ_f, then x should be < μ_f (left of f, away from g)
270
+ * - If μ_g < μ_f, then x should be > μ_f (right of f, away from g)
271
+ * - If μ_g = μ_f, any side is valid (symmetric case)
272
+ *
273
+ * @param f - The "from" distribution
274
+ * @param g - The "to" distribution
275
+ * @param x - The point to check
276
+ * @returns True if x is on the correct side
277
+ */
278
+ export function isOnCorrectSide(f, g, x) {
279
+ const muF = f.mean;
280
+ const muG = g.mean;
281
+ if (muG.gt(muF)) {
282
+ return x.lt(muF);
283
+ }
284
+ if (muG.lt(muF)) {
285
+ return x.gt(muF);
286
+ }
287
+ // Same mean - any side is valid
288
+ return true;
289
+ }
290
+ // ============================================================================
291
+ // Precheck: Validate Inputs Against Policy
292
+ // ============================================================================
293
+ /**
294
+ * Checks if inputs are within the safe operating envelope.
295
+ *
296
+ * @returns null if inputs pass, or a rejection reason if they should be rejected
297
+ */
298
+ function precheck(f, g, policy) {
299
+ // Reject non-positive variance
300
+ if (f.variance.lte(ZERO) || g.variance.lte(ZERO)) {
301
+ return 'degenerate_distribution';
302
+ }
303
+ // Check sigma ratio
304
+ const varF = f.variance;
305
+ const varG = g.variance;
306
+ const [varLarge, varSmall] = varF.gt(varG) ? [varF, varG] : [varG, varF];
307
+ // Skip ratio check for equal variance
308
+ if (!varF.eq(varG)) {
309
+ const maxRatioSq = policy.maxSigmaRatio.pow2();
310
+ if (!maxRatioSq) {
311
+ return 'computation_failed';
312
+ }
313
+ const threshold = maxRatioSq.mul(varSmall);
314
+ if (threshold && varLarge.gt(threshold)) {
315
+ return 'sigma_ratio_too_high';
316
+ }
317
+ }
318
+ // Check absolute magnitude of means
319
+ const absMuF = f.mean.abs();
320
+ const absMuG = g.mean.abs();
321
+ if (absMuF.gt(policy.maxAbsoluteMean) || absMuG.gt(policy.maxAbsoluteMean)) {
322
+ return 'mean_magnitude_too_large';
323
+ }
324
+ // Check mean separation
325
+ const muSep = f.mean.gt(g.mean) ? f.mean.sub(g.mean) : g.mean.sub(f.mean);
326
+ if (!muSep) {
327
+ return 'computation_failed';
328
+ }
329
+ // Use the narrower distribution's sigma for the threshold
330
+ const sigmaNarrow = varF.lte(varG) ? f.sigma : g.sigma;
331
+ const maxSep = policy.maxMeanSepSigmas.mul(sigmaNarrow);
332
+ if (maxSep && muSep.gt(maxSep)) {
333
+ return 'mean_separation_too_large';
334
+ }
335
+ return null;
336
+ }
337
+ // ============================================================================
338
+ // Newton-Raphson Minimization
339
+ // ============================================================================
340
+ /** Default convergence tolerance */
341
+ export const DEFAULT_TOLERANCE = SQ128x128.fromNumber(1e-10);
342
+ /** Default maximum iterations */
343
+ export const DEFAULT_MAX_ITERATIONS = 50;
344
+ /**
345
+ * Maximum step size as a multiple of sigma.
346
+ * Prevents Newton from overshooting when d''(x) is very small.
347
+ */
348
+ const MAX_STEP_SIGMAS = SQ128x128.fromNumber(0.5);
349
+ /**
350
+ * Performs one damped Newton-Raphson step for minimizing d(x) = g(x) - f(x).
351
+ *
352
+ * For minimization: x_next = x - d'(x) / d''(x)
353
+ *
354
+ * Step damping: Limits step size to MAX_STEP_SIGMAS * max(σ_f, σ_g)
355
+ * to prevent overshooting when the second derivative is very small.
356
+ *
357
+ * @param f - The "from" distribution
358
+ * @param g - The "to" distribution
359
+ * @param x - Current guess
360
+ * @returns Next guess, or null if step fails
361
+ */
362
+ function newtonStep(f, g, x) {
363
+ const dPrime = pdfDifferenceDerivative(f, g, x);
364
+ const dDoublePrime = pdfDifferenceSecondDerivative(f, g, x);
365
+ if (!dPrime || !dDoublePrime || dDoublePrime.isZero()) {
366
+ return null;
367
+ }
368
+ // Newton step: x - f'(x)/f''(x)
369
+ let step = dPrime.div(dDoublePrime);
370
+ if (!step) {
371
+ return null;
372
+ }
373
+ // Damp step size to prevent overshooting
374
+ // Max step = 0.5 * max(σ_f, σ_g)
375
+ const maxSigma = f.sigma.gt(g.sigma) ? f.sigma : g.sigma;
376
+ const maxStep = MAX_STEP_SIGMAS.mul(maxSigma);
377
+ if (maxStep) {
378
+ const absStep = step.abs();
379
+ if (absStep.gt(maxStep)) {
380
+ // Scale down the step while preserving direction
381
+ if (step.isNegative()) {
382
+ step = maxStep.neg();
383
+ }
384
+ else {
385
+ step = maxStep;
386
+ }
387
+ }
388
+ }
389
+ return x.sub(step);
390
+ }
391
+ /**
392
+ * Validates a candidate minimum against stationary, curvature, and side constraints.
393
+ *
394
+ * @param f - The "from" distribution
395
+ * @param g - The "to" distribution
396
+ * @param xMin - The candidate minimum point
397
+ * @param tolerance - Maximum allowed |d'(x*)|
398
+ * @returns null if valid, or a reason if invalid
399
+ */
400
+ function validateCandidate(f, g, xMin, tolerance) {
401
+ // 1. Stationary check: |d'(x_min)| ≤ tolerance
402
+ const dPrime = pdfDifferenceDerivative(f, g, xMin);
403
+ if (!dPrime) {
404
+ return 'computation_failed';
405
+ }
406
+ const absDPrime = dPrime.abs();
407
+ if (absDPrime.gt(tolerance)) {
408
+ return 'not_stationary';
409
+ }
410
+ // 2. Curvature check: d''(x*) > 0
411
+ const dDoublePrime = pdfDifferenceSecondDerivative(f, g, xMin);
412
+ if (!dDoublePrime) {
413
+ return 'computation_failed';
414
+ }
415
+ if (dDoublePrime.lte(ZERO)) {
416
+ return 'curvature_not_positive';
417
+ }
418
+ // 3. Side check
419
+ if (!isOnCorrectSide(f, g, xMin)) {
420
+ return 'wrong_side';
421
+ }
422
+ return null;
423
+ }
424
+ /**
425
+ * Finds the minimum of d(x) = g(x) - f(x) using Newton-Raphson.
426
+ *
427
+ * This is the primary entry point for collateral computation.
428
+ *
429
+ * @param f - The "from" distribution
430
+ * @param g - The "to" distribution
431
+ * @param x0 - Optional initial guess (will be computed if not provided)
432
+ * @param tolerance - Convergence threshold (default: 1e-10)
433
+ * @param maxIterations - Maximum Newton iterations (default: 50)
434
+ * @param policy - Safe envelope policy (default: defaultPolicy())
435
+ * @returns The minimization result
436
+ */
437
+ export function findMinimum(f, g, x0, tolerance = DEFAULT_TOLERANCE, maxIterations = DEFAULT_MAX_ITERATIONS, policy = defaultPolicy()) {
438
+ // Precheck: reject inputs outside safe envelope
439
+ const precheckReason = precheck(f, g, policy);
440
+ if (precheckReason) {
441
+ return { type: 'rejected', reason: precheckReason };
442
+ }
443
+ // Special case: identical distributions
444
+ if (f.isIdentical(g)) {
445
+ return {
446
+ type: 'verified',
447
+ result: {
448
+ xMin: f.mean,
449
+ dMin: ZERO,
450
+ collateral: ZERO,
451
+ tolerance,
452
+ iterations: 0,
453
+ method: 'closed-form',
454
+ },
455
+ };
456
+ }
457
+ // Use provided or suggested initial guess
458
+ let x = x0 ?? suggestInitialGuess(f, g);
459
+ let iterations = 0;
460
+ let converged = false;
461
+ // Newton-Raphson iteration
462
+ for (let i = 0; i < maxIterations; i++) {
463
+ const xNext = newtonStep(f, g, x);
464
+ if (!xNext) {
465
+ break;
466
+ }
467
+ iterations = i + 1;
468
+ // Check convergence: |x_next - x| < tolerance
469
+ const diff = xNext.sub(x);
470
+ if (diff && diff.abs().lt(tolerance)) {
471
+ converged = true;
472
+ x = xNext;
473
+ break;
474
+ }
475
+ x = xNext;
476
+ }
477
+ // Compute d(x*) and collateral
478
+ const dMin = pdfDifference(f, g, x) ?? ZERO;
479
+ const collateral = collateralFromMinimum(dMin);
480
+ const candidate = {
481
+ x,
482
+ d: dMin,
483
+ collateral,
484
+ iterations,
485
+ method: 'newton',
486
+ };
487
+ // Check convergence
488
+ if (!converged) {
489
+ return {
490
+ type: 'not_verified',
491
+ reason: 'newton_did_not_converge',
492
+ candidate,
493
+ };
494
+ }
495
+ // Validate the candidate
496
+ const validationReason = validateCandidate(f, g, x, tolerance);
497
+ if (validationReason) {
498
+ return {
499
+ type: 'not_verified',
500
+ reason: validationReason,
501
+ candidate,
502
+ };
503
+ }
504
+ // All checks passed
505
+ return {
506
+ type: 'verified',
507
+ result: {
508
+ xMin: x,
509
+ dMin,
510
+ collateral,
511
+ tolerance,
512
+ iterations,
513
+ method: 'newton',
514
+ },
515
+ };
516
+ }
517
+ /**
518
+ * Convenience function to compute collateral directly.
519
+ *
520
+ * Returns the verified collateral amount, or null if verification fails.
521
+ *
522
+ * @param f - The "from" distribution
523
+ * @param g - The "to" distribution
524
+ * @returns The collateral requirement, or null if computation fails
525
+ */
526
+ export function computeCollateral(f, g) {
527
+ const result = findMinimum(f, g);
528
+ if (result.type === 'verified') {
529
+ return result.result.collateral;
530
+ }
531
+ return null;
532
+ }
533
+ /**
534
+ * Extracts the verified minimum from a result, if available.
535
+ *
536
+ * @param result - The FindMinimumResult
537
+ * @returns The VerifiedMinimum if verified, null otherwise
538
+ */
539
+ export function getVerifiedMinimum(result) {
540
+ if (result.type === 'verified') {
541
+ return result.result;
542
+ }
543
+ return null;
544
+ }
545
+ /**
546
+ * Checks if a FindMinimumResult is verified.
547
+ *
548
+ * @param result - The FindMinimumResult
549
+ * @returns True if the result is verified
550
+ */
551
+ export function isVerified(result) {
552
+ return result.type === 'verified';
553
+ }
554
+ // ============================================================================
555
+ // Scaled PDF Difference Functions
556
+ // ============================================================================
557
+ /**
558
+ * Computes the scaled PDF difference: d_s(x) = lambda_g * g(x) - lambda_f * f(x)
559
+ *
560
+ * When variances differ, finding the minimum of this scaled difference
561
+ * is necessary for correct collateral computation.
562
+ */
563
+ export function scaledPdfDifference(f, g, lambdaF, lambdaG, x) {
564
+ const fPdf = f.pdf(x);
565
+ const gPdf = g.pdf(x);
566
+ if (!fPdf || !gPdf)
567
+ return null;
568
+ return lambdaG * gPdf.toNumber() - lambdaF * fPdf.toNumber();
569
+ }
570
+ /**
571
+ * Computes the derivative of scaled PDF difference:
572
+ * d_s'(x) = lambda_g * g'(x) - lambda_f * f'(x)
573
+ */
574
+ export function scaledPdfDifferenceDerivative(f, g, lambdaF, lambdaG, x) {
575
+ const fDeriv = f.pdfDerivative(x);
576
+ const gDeriv = g.pdfDerivative(x);
577
+ if (!fDeriv || !gDeriv)
578
+ return null;
579
+ return lambdaG * gDeriv.toNumber() - lambdaF * fDeriv.toNumber();
580
+ }
581
+ /**
582
+ * Computes the second derivative of scaled PDF difference:
583
+ * d_s''(x) = lambda_g * g''(x) - lambda_f * f''(x)
584
+ */
585
+ export function scaledPdfDifferenceSecondDerivative(f, g, lambdaF, lambdaG, x) {
586
+ const fSecond = f.pdfSecondDerivative(x);
587
+ const gSecond = g.pdfSecondDerivative(x);
588
+ if (!fSecond || !gSecond)
589
+ return null;
590
+ return lambdaG * gSecond.toNumber() - lambdaF * fSecond.toNumber();
591
+ }
592
+ /**
593
+ * Performs one damped Newton-Raphson step for the scaled PDF difference.
594
+ *
595
+ * For minimization: x_next = x - d_s'(x) / d_s''(x)
596
+ */
597
+ function scaledNewtonStep(f, g, lambdaF, lambdaG, x, maxStepSize) {
598
+ const dPrime = scaledPdfDifferenceDerivative(f, g, lambdaF, lambdaG, x);
599
+ const dDoublePrime = scaledPdfDifferenceSecondDerivative(f, g, lambdaF, lambdaG, x);
600
+ if (dPrime === null || dDoublePrime === null || Math.abs(dDoublePrime) < 1e-20) {
601
+ return null;
602
+ }
603
+ // Newton step: x - f'(x)/f''(x)
604
+ let step = dPrime / dDoublePrime;
605
+ // Damp step size to prevent overshooting
606
+ if (Math.abs(step) > maxStepSize) {
607
+ step = step > 0 ? maxStepSize : -maxStepSize;
608
+ }
609
+ const xNum = x.toNumber();
610
+ const nextX = xNum - step;
611
+ return SQ128x128.fromNumber(nextX);
612
+ }
613
+ /**
614
+ * Finds the minimum of the scaled PDF difference using Newton-Raphson.
615
+ *
616
+ * This is the correct approach when lambda_f ≠ lambda_g (different variances).
617
+ * The minimum of d_s(x) = lambda_g * g(x) - lambda_f * f(x) may be at a
618
+ * different location than the minimum of d(x) = g(x) - f(x).
619
+ */
620
+ function findScaledMinimum(f, g, lambdaF, lambdaG, tolerance = 1e-10, maxIterations = 50) {
621
+ // Initial guess - start on the side of f away from g
622
+ const muF = f.mean.toNumber();
623
+ const muG = g.mean.toNumber();
624
+ const sigmaF = f.sigma.toNumber();
625
+ const sigmaG = g.sigma.toNumber();
626
+ const maxSigma = Math.max(sigmaF, sigmaG);
627
+ let x;
628
+ // Check for near-equal means (within 1% of narrower sigma)
629
+ // This tolerance-based check ensures variance-only trades work even when
630
+ // means differ by tiny floating-point amounts
631
+ const meanDiff = Math.abs(muF - muG);
632
+ const sigmaNarrow = Math.min(sigmaF, sigmaG);
633
+ const nearlyEqualMeans = meanDiff < 0.01 * sigmaNarrow;
634
+ // For same mean or near-equal means (variance only change)
635
+ if (nearlyEqualMeans) {
636
+ if (sigmaF < sigmaG) {
637
+ // Narrow to wide: minimum is near the mean
638
+ x = f.mean;
639
+ }
640
+ else {
641
+ // Wide to narrow: minimum is in the tails
642
+ // Use 1.0 * σ_f as initial guess - closer to actual minimum
643
+ const xNum = muF + sigmaF;
644
+ x = SQ128x128.fromNumber(xNum);
645
+ }
646
+ }
647
+ else {
648
+ // Mean differs significantly: start on the opposite side of f from g
649
+ // For wide-to-narrow with mean shift, use σ_f as offset (minimum is further out)
650
+ // For narrow-to-wide, use smaller offset
651
+ const wideToNarrow = sigmaF > sigmaG;
652
+ const offset = wideToNarrow ? sigmaF : Math.min(sigmaF * 0.5, meanDiff * 0.5);
653
+ const xNum = muG > muF ? muF - offset : muF + offset;
654
+ x = SQ128x128.fromNumber(xNum);
655
+ }
656
+ const maxStepSize = 0.5 * maxSigma;
657
+ let iterations = 0;
658
+ let converged = false;
659
+ for (let i = 0; i < maxIterations; i++) {
660
+ const xNext = scaledNewtonStep(f, g, lambdaF, lambdaG, x, maxStepSize);
661
+ if (!xNext) {
662
+ break;
663
+ }
664
+ iterations = i + 1;
665
+ // Check convergence
666
+ const diff = Math.abs(xNext.toNumber() - x.toNumber());
667
+ if (diff < tolerance) {
668
+ converged = true;
669
+ x = xNext;
670
+ break;
671
+ }
672
+ x = xNext;
673
+ }
674
+ const xStar = x.toNumber();
675
+ const dMin = scaledPdfDifference(f, g, lambdaF, lambdaG, x);
676
+ if (dMin === null) {
677
+ return null;
678
+ }
679
+ return { xStar, dMin, iterations, converged };
680
+ }
681
+ /**
682
+ * Computes the scaled collateral for AMM trading.
683
+ *
684
+ * The AMM uses lambda-scaled PDFs where lambda = k / ||p||_2.
685
+ *
686
+ * For distributions with the same variance, lambda_f = lambda_g and we can
687
+ * use the unscaled minimum.
688
+ *
689
+ * For distributions with different variances, we find x* by minimizing
690
+ * the scaled difference: d_s(x) = lambda_g * g(x) - lambda_f * f(x)
691
+ *
692
+ * Collateral = max(0, lambda_f * f(x*) - lambda_g * g(x*)) = max(0, -d_s(x*))
693
+ *
694
+ * @param f - The "from" distribution (current market)
695
+ * @param g - The "to" distribution (candidate)
696
+ * @param k - The AMM invariant parameter (default: 1)
697
+ * @param policy - The minimization policy (default: defaultPolicy())
698
+ * @returns The scaled collateral result, or null if computation fails
699
+ *
700
+ * @example
701
+ * ```typescript
702
+ * const f = NormalDistribution.create(mean100, variance100);
703
+ * const g = NormalDistribution.create(mean105, variance100);
704
+ * const result = computeScaledCollateral(f, g, SQ128x128.fromNumber(1));
705
+ * if (result) {
706
+ * console.log('Collateral:', result.scaledCollateral.toNumber());
707
+ * console.log('x*:', result.xStar.toNumber());
708
+ * }
709
+ * ```
710
+ */
711
+ export function computeScaledCollateral(f, g, k = ONE, policy = defaultPolicy()) {
712
+ const kNum = k.toNumber();
713
+ // Compute lambda for both distributions
714
+ const sigmaF = f.sigma.toNumber();
715
+ const sigmaG = g.sigma.toNumber();
716
+ const lambdaFNum = computeLambdaNumber(sigmaF, kNum);
717
+ const lambdaGNum = computeLambdaNumber(sigmaG, kNum);
718
+ // Check if lambdas are approximately equal (same variance case)
719
+ const lambdaRatio = lambdaFNum / lambdaGNum;
720
+ const sameVariance = Math.abs(lambdaRatio - 1.0) < 0.001;
721
+ let xStarNum;
722
+ let iterations;
723
+ let scaledCollateralNum;
724
+ let unscaledCollateralNum;
725
+ if (sameVariance) {
726
+ // Same variance: use unscaled minimization (faster, same result)
727
+ const result = findMinimum(f, g, undefined, DEFAULT_TOLERANCE, DEFAULT_MAX_ITERATIONS, policy);
728
+ if (result.type !== 'verified') {
729
+ return null;
730
+ }
731
+ xStarNum = result.result.xMin.toNumber();
732
+ iterations = result.result.iterations;
733
+ unscaledCollateralNum = result.result.collateral.toNumber();
734
+ // Scale the collateral: C_scaled = lambda * C_unscaled
735
+ scaledCollateralNum = lambdaFNum * unscaledCollateralNum;
736
+ }
737
+ else {
738
+ // Different variances: minimize the scaled difference directly
739
+ const scaledResult = findScaledMinimum(f, g, lambdaFNum, lambdaGNum);
740
+ if (!scaledResult || !scaledResult.converged) {
741
+ // Fallback to unscaled with a safety buffer
742
+ const result = findMinimum(f, g, undefined, DEFAULT_TOLERANCE, DEFAULT_MAX_ITERATIONS, policy);
743
+ if (result.type !== 'verified') {
744
+ return null;
745
+ }
746
+ xStarNum = result.result.xMin.toNumber();
747
+ iterations = result.result.iterations;
748
+ unscaledCollateralNum = result.result.collateral.toNumber();
749
+ // Compute actual scaled collateral at unscaled x*
750
+ const xStar = result.result.xMin;
751
+ const fPdf = f.pdf(xStar)?.toNumber() ?? 0;
752
+ const gPdf = g.pdf(xStar)?.toNumber() ?? 0;
753
+ scaledCollateralNum = Math.max(0, lambdaFNum * fPdf - lambdaGNum * gPdf);
754
+ }
755
+ else {
756
+ xStarNum = scaledResult.xStar;
757
+ iterations = scaledResult.iterations;
758
+ // Compute collateral from scaled minimum
759
+ // C_scaled = max(0, -d_s(x*)) = max(0, lambda_f * f(x*) - lambda_g * g(x*))
760
+ scaledCollateralNum = Math.max(0, -scaledResult.dMin);
761
+ // Also compute unscaled for reference
762
+ const xStar = SQ128x128.fromNumber(xStarNum);
763
+ const fPdf = f.pdf(xStar)?.toNumber() ?? 0;
764
+ const gPdf = g.pdf(xStar)?.toNumber() ?? 0;
765
+ unscaledCollateralNum = Math.max(0, fPdf - gPdf);
766
+ }
767
+ }
768
+ const xStar = SQ128x128.fromNumber(xStarNum);
769
+ const scaledCollateral = SQ128x128.fromNumber(scaledCollateralNum);
770
+ const unscaledCollateral = SQ128x128.fromNumber(unscaledCollateralNum ?? scaledCollateralNum / lambdaFNum);
771
+ const lambdaF = SQ128x128.fromNumber(lambdaFNum);
772
+ const lambdaG = SQ128x128.fromNumber(lambdaGNum);
773
+ if (!xStar || !scaledCollateral || !unscaledCollateral || !lambdaF || !lambdaG) {
774
+ return null;
775
+ }
776
+ return {
777
+ unscaledCollateral,
778
+ scaledCollateral,
779
+ lambda: lambdaF, // For backward compatibility
780
+ xStar,
781
+ iterations,
782
+ lambdaF,
783
+ lambdaG,
784
+ };
785
+ }
786
+ //# sourceMappingURL=compute.js.map