@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.
- package/dist/compute.d.ts +326 -0
- package/dist/compute.d.ts.map +1 -0
- package/dist/compute.js +786 -0
- package/dist/compute.js.map +1 -0
- package/dist/effect.d.ts +26 -0
- package/dist/effect.d.ts.map +1 -0
- package/dist/effect.js +38 -0
- package/dist/effect.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/verify.d.ts +96 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +155 -0
- package/dist/verify.js.map +1 -0
- package/package.json +30 -0
package/dist/compute.js
ADDED
|
@@ -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
|