@vultisig/rujira 12.0.0 → 13.0.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/CHANGELOG.md CHANGED
@@ -1,5 +1,78 @@
1
1
  # @vultisig/rujira
2
2
 
3
+ ## 13.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - [#299](https://github.com/vultisig/vultisig-sdk/pull/299) [`4af5bb8`](https://github.com/vultisig/vultisig-sdk/commit/4af5bb8043da7dab15b5e1a135e5195d2dd1d7cc) Thanks [@gomesalexandre](https://github.com/gomesalexandre)! - feat(rujira)!: drop RujiraPerps
8
+
9
+ **BREAKING CHANGE.** The `RujiraPerps` module is removed. Its only
10
+ consumer was vultisig-mcp-ts' `src/tools/rujira/perps.ts`, which
11
+ [mcp-ts#36](https://github.com/vultisig/mcp-ts/pull/36) deleted
12
+ (commit `e5ecb58`). No known external consumers.
13
+
14
+ Removed:
15
+ - `RujiraPerps` class export
16
+ - `PerpsMarket` type export
17
+ - `PerpsTransactionParams` type export
18
+ - `client.perps` field on `RujiraClient`
19
+ - `@vultisig/rujira/perps` subpath export
20
+
21
+ No replacement API. Consumers that still need perps-style interactions
22
+ should open an issue — the module was a thin wrapper around on-chain
23
+ calls that can be reconstructed if there's demand.
24
+
25
+ All other Rujira surfaces (swap, orderbook, staking, ghost, deposit,
26
+ withdraw, discovery) are unchanged.
27
+
28
+ ### Patch Changes
29
+
30
+ - Updated dependencies [[`2018787`](https://github.com/vultisig/vultisig-sdk/commit/2018787f8101ea9a98e975c0e7477245c3f86fad), [`f52057b`](https://github.com/vultisig/vultisig-sdk/commit/f52057b4af859018d1c180fa6db9ce15e153409f)]:
31
+ - @vultisig/sdk@0.18.0
32
+
33
+ ## 12.1.0
34
+
35
+ ### Minor Changes
36
+
37
+ - [#300](https://github.com/vultisig/vultisig-sdk/pull/300) [`2da432d`](https://github.com/vultisig/vultisig-sdk/commit/2da432d3e972802ee246584971c5abca05b49797) Thanks [@gomesalexandre](https://github.com/gomesalexandre)! - feat(rujira): CCL (Custom Concentrated Liquidity) support
38
+
39
+ Rujira shipped [Custom Concentrated Liquidity](https://rujira.network/trade) on RUJI Trade
40
+ on 2026-04-20. This release adds SDK support for range-position management on
41
+ `rujira-fin` pair contracts via a new `range` ExecuteMsg variant.
42
+
43
+ New surface:
44
+ - **`client.range: RujiraRange`** — pure builders (no signer needed):
45
+ - `buildCreatePosition({ pairAddress, config, base, quote })`
46
+ - `buildDeposit({ pairAddress, idx, base, quote })`
47
+ - `buildWithdraw({ pairAddress, idx, share })`
48
+ - `buildClaim({ pairAddress, idx })`
49
+ - `buildTransfer({ pairAddress, idx, to })`
50
+ - `buildWithdrawAll({ pairAddress, idx })` — returns `RangeMultiTransactionParams`
51
+ with `[claim, withdraw('1')]`. Callers MUST sign + broadcast both msgs in a
52
+ single cosmos tx for atomicity (`wasm_execute_multi`).
53
+ - **GraphQL helpers** (against `api.vultisig.com/ruji/api/graphql`):
54
+ - `client.range.getPositions(owner)` — list all range positions
55
+ - `client.range.getPosition(pairAddress, idx)` — single position analytics
56
+ - `client.range.getPairAddress(base, quote)` — resolve FIN pair contract
57
+ from tickers / denoms (exact-match preferred, single-candidate fuzzy
58
+ match fallback; ambiguous hits throw `INVALID_PARAMS`)
59
+ - **`@vultisig/rujira/ccl` subpath export** — CCL math module ported from
60
+ rujira-ui (MIT): linear + quadratic weight models, √price Newton-Raphson
61
+ price recovery, bucket distribution generator. 90 tests pass.
62
+ - **`@vultisig/rujira/range` subpath export** — just the RujiraRange class
63
+ - types for consumers that want to avoid pulling the full entry point.
64
+ - **`RujiraErrorCode.INVALID_PARAMS`** — new error code for the input
65
+ validation surface (Decimal12 for config fields, Decimal4 + `(0, 1]` for
66
+ withdraw share, `idx` strictly `/^\d+$/`, `thor1` prefix on pair addresses).
67
+
68
+ No change to existing `swap` / `orderbook` / `staking` / `ghost` / `deposit` /
69
+ `withdraw` / `discovery` surfaces.
70
+
71
+ ### Patch Changes
72
+
73
+ - Updated dependencies []:
74
+ - @vultisig/sdk@0.17.1
75
+
3
76
  ## 12.0.0
4
77
 
5
78
  ### Patch Changes
@@ -0,0 +1,17 @@
1
+ export declare abstract class Ccl {
2
+ readonly high: number;
3
+ readonly low: number;
4
+ protected sA: number;
5
+ protected sB: number;
6
+ constructor(high: number, low: number);
7
+ protected abstract yAt(s: number): number;
8
+ protected abstract xAt(s: number): number;
9
+ protected abstract yPrime(s: number): number;
10
+ protected abstract xPrime(s: number): number;
11
+ abstract weight(p: number): number;
12
+ price(base: number, quote: number): number;
13
+ ask(price: number, spread: number): number;
14
+ bid(price: number, spread: number): number;
15
+ balanceRatio(price: number): number | null;
16
+ }
17
+ //# sourceMappingURL=base.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/ccl/base.ts"],"names":[],"mappings":"AAIA,8BAAsB,GAAG;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,EAAE,MAAM,CAAA;gBAER,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM;IAarC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM;IACzC,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM;IACzC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM;IAC5C,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM;IAC5C,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM;IAGlC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;IAsC1C,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;IAI1C,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM;IAI1C,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;CAM3C"}
@@ -0,0 +1,60 @@
1
+ import { RujiraError, RujiraErrorCode } from '../errors.js';
2
+ const NEWTON_ITERATIONS = 6;
3
+ export class Ccl {
4
+ constructor(high, low) {
5
+ if (!Number.isFinite(high) || !Number.isFinite(low) || low < 0 || high < low) {
6
+ throw new RujiraError(RujiraErrorCode.INVALID_PARAMS, `CCL range bounds must be finite and satisfy 0 <= low <= high (got high=${high}, low=${low})`);
7
+ }
8
+ this.high = high;
9
+ this.low = low;
10
+ this.sA = Math.sqrt(low);
11
+ this.sB = Math.sqrt(high);
12
+ }
13
+ // Newton-Raphson: solve F(s) = x·Y(s) - y·X(s) = 0
14
+ price(base, quote) {
15
+ // Reject non-finite / negative inputs up front so a bad parsed
16
+ // number from upstream (NaN from a failed parseFloat, or an
17
+ // Infinity from a bogus scaling) can't propagate through the
18
+ // solver and produce a NaN price. Exported helpers MUST fail
19
+ // loudly on invalid inputs — silent NaN is worse than a throw.
20
+ if (!Number.isFinite(base) || !Number.isFinite(quote) || base < 0 || quote < 0) {
21
+ throw new RujiraError(RujiraErrorCode.INVALID_PARAMS, `price() requires finite non-negative base/quote (got base=${base}, quote=${quote})`);
22
+ }
23
+ if (quote === 0)
24
+ return this.sA * this.sA;
25
+ if (base === 0)
26
+ return this.sB * this.sB;
27
+ let s = Math.sqrt(this.sA * this.sB);
28
+ for (let i = 0; i < NEWTON_ITERATIONS; i++) {
29
+ const f = base * this.yAt(s) - quote * this.xAt(s);
30
+ const fPrime = base * this.yPrime(s) - quote * this.xPrime(s);
31
+ if (fPrime === 0)
32
+ break;
33
+ const delta = f / fPrime;
34
+ if (delta < 0) {
35
+ s = s + Math.abs(delta);
36
+ }
37
+ else if (Math.abs(delta) > s) {
38
+ s = this.sA;
39
+ }
40
+ else {
41
+ s = s - Math.abs(delta);
42
+ }
43
+ s = Math.max(this.sA, Math.min(this.sB, s));
44
+ }
45
+ return s * s;
46
+ }
47
+ ask(price, spread) {
48
+ return price + (price * spread) / 2;
49
+ }
50
+ bid(price, spread) {
51
+ return Math.max(0, price - (price * spread) / 2);
52
+ }
53
+ balanceRatio(price) {
54
+ const s = Math.sqrt(price);
55
+ const xS = this.xAt(s);
56
+ if (xS === 0)
57
+ return null;
58
+ return this.yAt(s) / xS;
59
+ }
60
+ }
@@ -0,0 +1,5 @@
1
+ import type { Ccl } from './base.js';
2
+ import type { CclDistribution, CclModel, CclRangeConfig } from './types.js';
3
+ export declare function createCcl(high: number, low: number, sigma: number, model?: CclModel): Ccl;
4
+ export declare function generateCclDistribution(config: CclRangeConfig): CclDistribution;
5
+ //# sourceMappingURL=ccl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ccl.d.ts","sourceRoot":"","sources":["../../src/ccl/ccl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAGpC,OAAO,KAAK,EAAE,eAAe,EAAyB,QAAQ,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAElG,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,GAAE,QAAmB,GAAG,GAAG,CAEnG;AAGD,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,cAAc,GAAG,eAAe,CA6E/E"}
@@ -0,0 +1,73 @@
1
+ import { CclLinear } from './linear.js';
2
+ import { CclQuadratic } from './quadratic.js';
3
+ export function createCcl(high, low, sigma, model = 'linear') {
4
+ return model === 'quadratic' ? new CclQuadratic(high, low, sigma) : new CclLinear(high, low, sigma);
5
+ }
6
+ // Generates distribution using geometric (delta-based) stepping matching Rust RangeOfferIter
7
+ export function generateCclDistribution(config) {
8
+ const { high, low, price, sigma, spread, delta, model } = config;
9
+ const empty = {
10
+ asks: [],
11
+ bids: [],
12
+ price: 0,
13
+ askPrice: 0,
14
+ bidPrice: 0,
15
+ avgAskFillPrice: 0,
16
+ avgBidFillPrice: 0,
17
+ balanceRatio: null,
18
+ };
19
+ if (![high, low, price, sigma, spread, delta].every(Number.isFinite) ||
20
+ high <= low ||
21
+ low <= 0 ||
22
+ price <= 0 ||
23
+ spread < 0 ||
24
+ spread >= 1 ||
25
+ delta <= 0) {
26
+ return empty;
27
+ }
28
+ const ccl = createCcl(high, low, sigma, model);
29
+ const askPrice = ccl.ask(price, spread);
30
+ const bidPrice = ccl.bid(price, spread);
31
+ const asks = [];
32
+ const bids = [];
33
+ let askWeight = 0;
34
+ let bidWeight = 0;
35
+ // Asks: geometric steps ascending from askPrice to high
36
+ let p = askPrice;
37
+ while (p < high) {
38
+ const next = Math.min(p + p * delta, high);
39
+ if (next <= p)
40
+ break;
41
+ const pMid = (p + next) / 2;
42
+ const w = Math.max(0, ccl.weight(pMid));
43
+ asks.push({ pStart: p, pEnd: next, pMid, weight: w, pct: 0, side: 'ask' });
44
+ askWeight += w;
45
+ p = next;
46
+ }
47
+ // Bids: geometric steps descending from bidPrice to low
48
+ p = bidPrice;
49
+ while (p > low) {
50
+ const next = Math.max(p - p * delta, low);
51
+ if (next <= 0 || next >= p)
52
+ break;
53
+ const pMid = (next + p) / 2;
54
+ const w = Math.max(0, ccl.weight(pMid));
55
+ bids.push({ pStart: next, pEnd: p, pMid, weight: w, pct: 0, side: 'bid' });
56
+ bidWeight += w;
57
+ p = next;
58
+ }
59
+ if (askWeight > 0) {
60
+ for (const bucket of asks) {
61
+ bucket.pct = (bucket.weight / askWeight) * 100;
62
+ }
63
+ }
64
+ if (bidWeight > 0) {
65
+ for (const bucket of bids) {
66
+ bucket.pct = (bucket.weight / bidWeight) * 100;
67
+ }
68
+ }
69
+ const avgAskFillPrice = askWeight > 0 ? asks.reduce((sum, b) => sum + b.pMid * b.weight, 0) / askWeight : 0;
70
+ const avgBidFillPrice = bidWeight > 0 ? bids.reduce((sum, b) => sum + b.pMid * b.weight, 0) / bidWeight : 0;
71
+ const balanceRatio = ccl.balanceRatio(price);
72
+ return { asks, bids, price, askPrice, bidPrice, avgAskFillPrice, avgBidFillPrice, balanceRatio };
73
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ccl.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ccl.test.d.ts","sourceRoot":"","sources":["../../src/ccl/ccl.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,281 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createCcl, generateCclDistribution } from './ccl.js';
3
+ import { CclLinear } from './linear.js';
4
+ import { CclQuadratic } from './quadratic.js';
5
+ const models = [
6
+ { name: 'quadratic', create: (h, l, s) => new CclQuadratic(h, l, s) },
7
+ { name: 'linear', create: (h, l, s) => new CclLinear(h, l, s) },
8
+ ];
9
+ // ============================================================
10
+ // Shared base class behavior — runs against BOTH models
11
+ // ============================================================
12
+ describe.each(models)('Ccl base [$name]', ({ create }) => {
13
+ describe('price', () => {
14
+ it('returns low when quote=0', () => {
15
+ const ccl = create(4000, 3000, 0);
16
+ expect(ccl.price(100000, 0)).toBeCloseTo(3000, 0);
17
+ });
18
+ it('returns high when base=0', () => {
19
+ const ccl = create(4000, 3000, 0);
20
+ expect(ccl.price(0, 401992761)).toBeCloseTo(4000, 0);
21
+ });
22
+ it('stays within [low, high] for all skew values', () => {
23
+ const cases = [
24
+ [0.9, 100, 1000000],
25
+ [-0.9, 100, 1000000],
26
+ [0.5, 1000000, 1],
27
+ [0.99, 500000, 500000],
28
+ [-0.99, 500000, 500000],
29
+ ];
30
+ for (const [sigma, b, q] of cases) {
31
+ const ccl = create(4, 3, sigma);
32
+ const price = ccl.price(b, q);
33
+ expect(price).toBeGreaterThanOrEqual(3);
34
+ expect(price).toBeLessThanOrEqual(4);
35
+ }
36
+ });
37
+ });
38
+ describe('ask / bid', () => {
39
+ it('ask = price + price * spread', () => {
40
+ const ccl = create(4, 3, 0);
41
+ const price = 3.5;
42
+ const spread = 0.01;
43
+ expect(ccl.ask(price, spread)).toBeCloseTo(price + (price * spread) / 2);
44
+ });
45
+ it('bid = price - price * spread', () => {
46
+ const ccl = create(4, 3, 0);
47
+ const price = 3.5;
48
+ const spread = 0.01;
49
+ expect(ccl.bid(price, spread)).toBeCloseTo(price - (price * spread) / 2);
50
+ });
51
+ it('bid floors at 0', () => {
52
+ const ccl = create(4, 3, 0);
53
+ expect(ccl.bid(0.01, 2)).toBe(0);
54
+ });
55
+ it('ask returns price between center and high', () => {
56
+ const ccl = create(4, 3, 0);
57
+ const base = 100000000;
58
+ const quote = 401992761;
59
+ const price = ccl.price(base, quote);
60
+ const askPrice = ccl.ask(price, 0.01);
61
+ expect(askPrice).toBeGreaterThan(price);
62
+ });
63
+ it('bid returns price between low and center', () => {
64
+ const ccl = create(4, 3, 0);
65
+ const base = 100000000;
66
+ const quote = 401992761;
67
+ const price = ccl.price(base, quote);
68
+ const bidPrice = ccl.bid(price, 0.01);
69
+ expect(bidPrice).toBeLessThan(price);
70
+ expect(bidPrice).toBeGreaterThanOrEqual(0);
71
+ });
72
+ });
73
+ describe('balanceRatio', () => {
74
+ it('returns positive ratio at mid-range price', () => {
75
+ const ccl = create(4, 3, 0);
76
+ const ratio = ccl.balanceRatio(3.5);
77
+ expect(ratio).not.toBeNull();
78
+ expect(ratio).toBeGreaterThan(0);
79
+ });
80
+ it('returns null at upper bound (x=0)', () => {
81
+ const ccl = create(4, 3, 0);
82
+ expect(ccl.balanceRatio(4)).toBeNull();
83
+ });
84
+ it('ratio increases as price moves toward high', () => {
85
+ const ccl = create(4, 3, 0);
86
+ const ratioLow = ccl.balanceRatio(3.2);
87
+ const ratioHigh = ccl.balanceRatio(3.8);
88
+ expect(ratioHigh).toBeGreaterThan(ratioLow);
89
+ });
90
+ });
91
+ });
92
+ // ============================================================
93
+ // Roundtrip: balanceRatio(p) → price(base, quote) ≈ p
94
+ // ============================================================
95
+ describe.each(models)('balanceRatio roundtrip [$name]', ({ create }) => {
96
+ const prices = [3.2, 3.5, 3.8];
97
+ const sigmas = [0, 0.5, -0.5, 0.8, -0.8];
98
+ const base = 100_000_000;
99
+ it.each(sigmas.flatMap(s => prices.map(p => ({ s, p }))))('σ=$s p=$p → quoted price matches', ({ s, p }) => {
100
+ const ccl = create(4, 3, s);
101
+ const ratio = ccl.balanceRatio(p);
102
+ expect(ratio).not.toBeNull();
103
+ const quote = Math.round(base * ratio);
104
+ const quoted = ccl.price(base, quote);
105
+ expect(quoted).toBeCloseTo(p, 4);
106
+ });
107
+ });
108
+ // ============================================================
109
+ // Rust reference values
110
+ // ============================================================
111
+ describe('CclQuadratic price reference', () => {
112
+ it('computes price matching Rust cl_pricing test', () => {
113
+ const ccl = new CclQuadratic(4000, 3000, 0);
114
+ const price = ccl.price(100000, 401992761);
115
+ expect(price).toBeGreaterThanOrEqual(3499);
116
+ expect(price).toBeLessThanOrEqual(3501);
117
+ });
118
+ });
119
+ // ============================================================
120
+ // createCcl factory
121
+ // ============================================================
122
+ describe('createCcl factory', () => {
123
+ it('defaults to linear', () => {
124
+ expect(createCcl(4, 3, 0)).toBeInstanceOf(CclLinear);
125
+ });
126
+ it('creates quadratic when specified', () => {
127
+ expect(createCcl(4, 3, 0, 'quadratic')).toBeInstanceOf(CclQuadratic);
128
+ });
129
+ it('creates linear when specified', () => {
130
+ expect(createCcl(4, 3, 0, 'linear')).toBeInstanceOf(CclLinear);
131
+ });
132
+ });
133
+ // ============================================================
134
+ // generateCclDistribution — delta-based stepping
135
+ // ============================================================
136
+ describe.each(models.map(m => m.name))('generateCclDistribution [%s]', model => {
137
+ const baseConfig = {
138
+ high: 4,
139
+ low: 3,
140
+ price: 3.5,
141
+ sigma: 0,
142
+ spread: 0.01,
143
+ delta: 0.05,
144
+ model,
145
+ };
146
+ it('mid price equals provided price', () => {
147
+ const dist = generateCclDistribution(baseConfig);
148
+ expect(dist.price).toBe(3.5);
149
+ });
150
+ it('returns buckets that sum to 100%', () => {
151
+ const dist = generateCclDistribution(baseConfig);
152
+ const askSum = dist.asks.reduce((s, b) => s + b.pct, 0);
153
+ const bidSum = dist.bids.reduce((s, b) => s + b.pct, 0);
154
+ expect(askSum).toBeCloseTo(100, 1);
155
+ expect(bidSum).toBeCloseTo(100, 1);
156
+ });
157
+ it('ask buckets cover [askPrice, high]', () => {
158
+ const dist = generateCclDistribution(baseConfig);
159
+ expect(dist.asks.length).toBeGreaterThan(0);
160
+ expect(dist.asks[0].pStart).toBeCloseTo(dist.askPrice, 6);
161
+ expect(dist.asks[dist.asks.length - 1].pEnd).toBeCloseTo(4, 6);
162
+ });
163
+ it('bid buckets cover [low, bidPrice]', () => {
164
+ const dist = generateCclDistribution(baseConfig);
165
+ expect(dist.bids.length).toBeGreaterThan(0);
166
+ // bids are ordered descending (first bucket is closest to spread)
167
+ expect(dist.bids[dist.bids.length - 1].pStart).toBeCloseTo(3, 1);
168
+ expect(dist.bids[0].pEnd).toBeCloseTo(dist.bidPrice, 6);
169
+ });
170
+ it('geometric steps: each ask bucket is wider than the previous', () => {
171
+ const dist = generateCclDistribution({ ...baseConfig, delta: 0.05 });
172
+ for (let i = 1; i < dist.asks.length - 1; i++) {
173
+ const prevWidth = dist.asks[i - 1].pEnd - dist.asks[i - 1].pStart;
174
+ const currWidth = dist.asks[i].pEnd - dist.asks[i].pStart;
175
+ expect(currWidth).toBeGreaterThanOrEqual(prevWidth - 1e-10);
176
+ }
177
+ });
178
+ it('σ=0 produces uniform distribution', () => {
179
+ const dist = generateCclDistribution(baseConfig);
180
+ const allBuckets = [...dist.asks, ...dist.bids];
181
+ const weights = allBuckets.map(b => b.weight);
182
+ const avg = weights.reduce((s, w) => s + w, 0) / weights.length;
183
+ for (const w of weights) {
184
+ expect(w).toBeCloseTo(avg, 1);
185
+ }
186
+ });
187
+ it('returns empty for invalid inputs', () => {
188
+ const dist = generateCclDistribution({ ...baseConfig, high: 2, low: 3 });
189
+ expect(dist.asks).toHaveLength(0);
190
+ expect(dist.bids).toHaveLength(0);
191
+ });
192
+ it('returns empty for delta=0', () => {
193
+ const dist = generateCclDistribution({ ...baseConfig, delta: 0 });
194
+ expect(dist.asks).toHaveLength(0);
195
+ });
196
+ it('avgAskFillPrice is between askPrice and high', () => {
197
+ const dist = generateCclDistribution(baseConfig);
198
+ expect(dist.avgAskFillPrice).toBeGreaterThanOrEqual(dist.askPrice);
199
+ expect(dist.avgAskFillPrice).toBeLessThanOrEqual(baseConfig.high);
200
+ });
201
+ it('avgBidFillPrice is between low and bidPrice', () => {
202
+ const dist = generateCclDistribution(baseConfig);
203
+ expect(dist.avgBidFillPrice).toBeGreaterThanOrEqual(baseConfig.low);
204
+ expect(dist.avgBidFillPrice).toBeLessThanOrEqual(dist.bidPrice);
205
+ });
206
+ it('avgAskFillPrice and avgBidFillPrice are 0 for invalid inputs', () => {
207
+ const dist = generateCclDistribution({ ...baseConfig, high: 2, low: 3 });
208
+ expect(dist.avgAskFillPrice).toBe(0);
209
+ expect(dist.avgBidFillPrice).toBe(0);
210
+ });
211
+ it('σ=0 avgAskFillPrice is near midpoint of ask range', () => {
212
+ const dist = generateCclDistribution(baseConfig);
213
+ const expectedMid = (dist.askPrice + baseConfig.high) / 2;
214
+ expect(dist.avgAskFillPrice).toBeCloseTo(expectedMid, 1);
215
+ });
216
+ it('σ=0 avgBidFillPrice is near midpoint of bid range', () => {
217
+ const dist = generateCclDistribution(baseConfig);
218
+ const expectedMid = (baseConfig.low + dist.bidPrice) / 2;
219
+ expect(dist.avgBidFillPrice).toBeCloseTo(expectedMid, 1);
220
+ });
221
+ it('balanceRatio roundtrips to price', () => {
222
+ const dist = generateCclDistribution(baseConfig);
223
+ expect(dist.balanceRatio).not.toBeNull();
224
+ const ccl = createCcl(4, 3, 0, model);
225
+ const base = 1e8;
226
+ const quote = Math.round(base * dist.balanceRatio);
227
+ expect(ccl.price(base, quote)).toBeCloseTo(3.5, 4);
228
+ });
229
+ it('smaller delta produces more buckets', () => {
230
+ const distSmall = generateCclDistribution({ ...baseConfig, delta: 0.01 });
231
+ const distLarge = generateCclDistribution({ ...baseConfig, delta: 0.1 });
232
+ const smallTotal = distSmall.asks.length + distSmall.bids.length;
233
+ const largeTotal = distLarge.asks.length + distLarge.bids.length;
234
+ expect(smallTotal).toBeGreaterThan(largeTotal);
235
+ });
236
+ });
237
+ // ============================================================
238
+ // Distribution skew behavior
239
+ // ============================================================
240
+ describe('distribution skew (quadratic — symmetric)', () => {
241
+ const cfg = {
242
+ high: 4,
243
+ low: 3,
244
+ price: 3.5,
245
+ sigma: 0,
246
+ spread: 0.01,
247
+ delta: 0.05,
248
+ model: 'quadratic',
249
+ };
250
+ it('σ>0 concentrates asks near spread', () => {
251
+ const dist = generateCclDistribution({ ...cfg, sigma: 0.8 });
252
+ const asks = dist.asks;
253
+ expect(asks[0].pct).toBeGreaterThan(asks[asks.length - 1].pct);
254
+ });
255
+ it('σ<0 concentrates asks near edge', () => {
256
+ const dist = generateCclDistribution({ ...cfg, sigma: -0.8 });
257
+ const asks = dist.asks;
258
+ expect(asks[asks.length - 1].pct).toBeGreaterThan(asks[0].pct);
259
+ });
260
+ });
261
+ describe('distribution skew (linear — directional)', () => {
262
+ const cfg = {
263
+ high: 4,
264
+ low: 3,
265
+ price: 3.5,
266
+ sigma: 0,
267
+ spread: 0.01,
268
+ delta: 0.05,
269
+ model: 'linear',
270
+ };
271
+ it('σ>0 concentrates asks toward high', () => {
272
+ const dist = generateCclDistribution({ ...cfg, sigma: 0.8 });
273
+ const asks = dist.asks;
274
+ expect(asks[asks.length - 1].pct).toBeGreaterThan(asks[0].pct);
275
+ });
276
+ it('σ<0 concentrates asks toward low (near spread)', () => {
277
+ const dist = generateCclDistribution({ ...cfg, sigma: -0.8 });
278
+ const asks = dist.asks;
279
+ expect(asks[0].pct).toBeGreaterThan(asks[asks.length - 1].pct);
280
+ });
281
+ });
@@ -0,0 +1,6 @@
1
+ export * from './base.js';
2
+ export * from './ccl.js';
3
+ export { CclLinear } from './linear.js';
4
+ export { CclQuadratic } from './quadratic.js';
5
+ export * from './types.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ccl/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,UAAU,CAAA;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAC7C,cAAc,YAAY,CAAA"}
@@ -0,0 +1,5 @@
1
+ export * from './base.js';
2
+ export * from './ccl.js';
3
+ export { CclLinear } from './linear.js';
4
+ export { CclQuadratic } from './quadratic.js';
5
+ export * from './types.js';
@@ -0,0 +1,12 @@
1
+ import { Ccl } from './base.js';
2
+ export declare class CclLinear extends Ccl {
3
+ private alpha;
4
+ private beta;
5
+ constructor(high: number, low: number, sigma: number);
6
+ protected yAt(s: number): number;
7
+ protected xAt(s: number): number;
8
+ protected yPrime(s: number): number;
9
+ protected xPrime(s: number): number;
10
+ weight(p: number): number;
11
+ }
12
+ //# sourceMappingURL=linear.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linear.d.ts","sourceRoot":"","sources":["../../src/ccl/linear.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAE/B,qBAAa,SAAU,SAAQ,GAAG;IAChC,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,IAAI,CAAQ;gBAER,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAgBpD,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM;IAQhC,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM;IAQhC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM;IAKnC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM;IAMnC,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM;CAG1B"}
@@ -0,0 +1,47 @@
1
+ // Linear weight: w(p) = 1 + σ·(p - p_m)/Δp
2
+ // Coefficients: β = σ/Δp, α = 1 - β·p_m
3
+ import { Ccl } from './base.js';
4
+ export class CclLinear extends Ccl {
5
+ constructor(high, low, sigma) {
6
+ super(high, low);
7
+ const pM = (high + low) / 2;
8
+ const deltaP = high - low;
9
+ if (deltaP === 0) {
10
+ this.beta = 0;
11
+ this.alpha = 1;
12
+ }
13
+ else {
14
+ this.beta = sigma / deltaP;
15
+ this.alpha = 1 - this.beta * pM;
16
+ }
17
+ }
18
+ // Y(s) = α·(s - s_a) + (β/3)·(s³ - s_a³)
19
+ yAt(s) {
20
+ const diff = s - this.sA;
21
+ const sCubed = s * s * s;
22
+ const sACubed = this.sA * this.sA * this.sA;
23
+ return this.alpha * diff + (this.beta / 3) * (sCubed - sACubed);
24
+ }
25
+ // X(s) = (s_b - s)·(α/(s·s_b) + β)
26
+ xAt(s) {
27
+ const diff = this.sB - s;
28
+ const denom = s * this.sB;
29
+ if (denom === 0)
30
+ return 0;
31
+ return diff * (this.alpha / denom + this.beta);
32
+ }
33
+ // Y'(s) = α + β·s²
34
+ yPrime(s) {
35
+ return this.alpha + this.beta * s * s;
36
+ }
37
+ // X'(s) = -α/s² - β
38
+ xPrime(s) {
39
+ const s2 = s * s;
40
+ if (s2 === 0)
41
+ return 0;
42
+ return -this.alpha / s2 - this.beta;
43
+ }
44
+ weight(p) {
45
+ return this.alpha + this.beta * p;
46
+ }
47
+ }
@@ -0,0 +1,13 @@
1
+ import { Ccl } from './base.js';
2
+ export declare class CclQuadratic extends Ccl {
3
+ private c0;
4
+ private c2;
5
+ private c4;
6
+ constructor(high: number, low: number, sigma: number);
7
+ protected yAt(s: number): number;
8
+ protected xAt(s: number): number;
9
+ protected yPrime(s: number): number;
10
+ protected xPrime(s: number): number;
11
+ weight(p: number): number;
12
+ }
13
+ //# sourceMappingURL=quadratic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quadratic.d.ts","sourceRoot":"","sources":["../../src/ccl/quadratic.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAE/B,qBAAa,YAAa,SAAQ,GAAG;IACnC,OAAO,CAAC,EAAE,CAAQ;IAClB,OAAO,CAAC,EAAE,CAAQ;IAClB,OAAO,CAAC,EAAE,CAAQ;gBAEN,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAmBpD,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM;IAShC,SAAS,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM;IAQhC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM;IAMnC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM;IAMnC,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM;CAG1B"}