@exponent-labs/market-three-math 0.1.8
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 +8 -0
- package/README.md +197 -0
- package/build/addLiquidity.d.ts +67 -0
- package/build/addLiquidity.js +269 -0
- package/build/addLiquidity.js.map +1 -0
- package/build/bisect.d.ts +1 -0
- package/build/bisect.js +62 -0
- package/build/bisect.js.map +1 -0
- package/build/index.d.ts +24 -0
- package/build/index.js +76 -0
- package/build/index.js.map +1 -0
- package/build/liquidityHistogram.d.ts +50 -0
- package/build/liquidityHistogram.js +162 -0
- package/build/liquidityHistogram.js.map +1 -0
- package/build/quote.d.ts +18 -0
- package/build/quote.js +106 -0
- package/build/quote.js.map +1 -0
- package/build/swap-v2.d.ts +20 -0
- package/build/swap-v2.js +261 -0
- package/build/swap-v2.js.map +1 -0
- package/build/swap.d.ts +15 -0
- package/build/swap.js +249 -0
- package/build/swap.js.map +1 -0
- package/build/swapLegacy.d.ts +16 -0
- package/build/swapLegacy.js +229 -0
- package/build/swapLegacy.js.map +1 -0
- package/build/swapV2.d.ts +11 -0
- package/build/swapV2.js +406 -0
- package/build/swapV2.js.map +1 -0
- package/build/types.d.ts +73 -0
- package/build/types.js +9 -0
- package/build/types.js.map +1 -0
- package/build/utils.d.ts +119 -0
- package/build/utils.js +219 -0
- package/build/utils.js.map +1 -0
- package/build/utilsV2.d.ts +88 -0
- package/build/utilsV2.js +180 -0
- package/build/utilsV2.js.map +1 -0
- package/build/withdrawLiquidity.d.ts +8 -0
- package/build/withdrawLiquidity.js +174 -0
- package/build/withdrawLiquidity.js.map +1 -0
- package/build/ytTrades.d.ts +106 -0
- package/build/ytTrades.js +292 -0
- package/build/ytTrades.js.map +1 -0
- package/build/ytTradesLegacy.d.ts +106 -0
- package/build/ytTradesLegacy.js +292 -0
- package/build/ytTradesLegacy.js.map +1 -0
- package/examples/.env.example +1 -0
- package/examples/test-histogram-simple.ts +172 -0
- package/examples/test-histogram.ts +112 -0
- package/package.json +26 -0
- package/src/addLiquidity.ts +384 -0
- package/src/bisect.ts +72 -0
- package/src/index.ts +74 -0
- package/src/liquidityHistogram.ts +192 -0
- package/src/quote.ts +128 -0
- package/src/swap.ts +299 -0
- package/src/swapLegacy.ts +272 -0
- package/src/types.ts +80 -0
- package/src/utils.ts +235 -0
- package/src/withdrawLiquidity.ts +240 -0
- package/src/ytTrades.ts +419 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLMM Add Liquidity simulation
|
|
3
|
+
* Ported from exponent_clmm/src/state/market_three/helpers/add_liquidity.rs
|
|
4
|
+
*/
|
|
5
|
+
import { AddLiquidityArgs, AddLiquidityOutcome, LiquidityNeeds, MarketThreeState } from "./types"
|
|
6
|
+
import { EffSnap, normalizedTimeRemaining } from "./utils"
|
|
7
|
+
|
|
8
|
+
const TICK_KEY_BASE_POINTS = 1_000_000
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Compute liquidity target and token needs based on position range and budgets
|
|
12
|
+
* Ported from compute_liquidity_target_and_token_needs in math.rs
|
|
13
|
+
*/
|
|
14
|
+
export function computeLiquidityTargetAndTokenNeeds(
|
|
15
|
+
snap: EffSnap,
|
|
16
|
+
spotPriceCurrent: number,
|
|
17
|
+
priceEffLower: number,
|
|
18
|
+
priceEffUpper: number,
|
|
19
|
+
lowerPrice: number,
|
|
20
|
+
upperPrice: number,
|
|
21
|
+
lowerTickIdx: number,
|
|
22
|
+
upperTickIdx: number,
|
|
23
|
+
currentIndex: number,
|
|
24
|
+
maxSy: number,
|
|
25
|
+
maxPt: number,
|
|
26
|
+
epsilonClamp: number,
|
|
27
|
+
): LiquidityNeeds {
|
|
28
|
+
// Decide region and compute target ΔL and token needs
|
|
29
|
+
// IMPORTANT: Price below range = SY only; Price above range = PT only
|
|
30
|
+
|
|
31
|
+
if (spotPriceCurrent <= lowerPrice) {
|
|
32
|
+
// Below range: SY only (NOT PT!)
|
|
33
|
+
const deltaCTotal = Math.max(priceEffLower - priceEffUpper, epsilonClamp)
|
|
34
|
+
const liquidityFromSy = maxSy / deltaCTotal
|
|
35
|
+
const liquidityTarget = liquidityFromSy
|
|
36
|
+
const syNeed = liquidityTarget * (priceEffLower - priceEffUpper)
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
liquidityTarget: Math.floor(liquidityTarget),
|
|
40
|
+
syNeeded: Math.floor(syNeed),
|
|
41
|
+
ptNeeded: 0,
|
|
42
|
+
priceSplitForNeed: lowerPrice,
|
|
43
|
+
priceSplitTickIdx: lowerTickIdx,
|
|
44
|
+
}
|
|
45
|
+
} else if (spotPriceCurrent >= upperPrice) {
|
|
46
|
+
// Above range: PT only (NOT SY!)
|
|
47
|
+
const duTotal = Math.max(upperPrice - lowerPrice, epsilonClamp)
|
|
48
|
+
const liquidityFromPt = maxPt / duTotal
|
|
49
|
+
const liquidityTarget = liquidityFromPt
|
|
50
|
+
const ptNeed = liquidityTarget * duTotal
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
liquidityTarget: Math.floor(liquidityTarget),
|
|
54
|
+
syNeeded: 0,
|
|
55
|
+
ptNeeded: Math.floor(ptNeed),
|
|
56
|
+
priceSplitForNeed: upperPrice,
|
|
57
|
+
priceSplitTickIdx: upperTickIdx,
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
// Inside range: both sides
|
|
61
|
+
const duLeft = Math.max(spotPriceCurrent - lowerPrice, epsilonClamp)
|
|
62
|
+
const liquidityFromPt = maxPt / duLeft
|
|
63
|
+
|
|
64
|
+
const priceEffCurrent = snap.getEffectivePrice(spotPriceCurrent)
|
|
65
|
+
const deltaCRight = Math.max(priceEffCurrent - priceEffUpper, epsilonClamp)
|
|
66
|
+
const liquidityFromSy = maxSy / deltaCRight
|
|
67
|
+
|
|
68
|
+
const liquidityTarget = Math.min(liquidityFromPt, liquidityFromSy)
|
|
69
|
+
const ptNeed = liquidityTarget * duLeft
|
|
70
|
+
const syNeed = liquidityTarget * deltaCRight
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
liquidityTarget: Math.floor(liquidityTarget),
|
|
74
|
+
syNeeded: Math.floor(syNeed),
|
|
75
|
+
ptNeeded: Math.floor(ptNeed),
|
|
76
|
+
priceSplitForNeed: spotPriceCurrent,
|
|
77
|
+
priceSplitTickIdx: currentIndex,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Simulate adding liquidity to the CLMM market
|
|
84
|
+
* This is a pure function that does not mutate the market state
|
|
85
|
+
*/
|
|
86
|
+
export function simulateAddLiquidity(marketState: MarketThreeState, args: AddLiquidityArgs): AddLiquidityOutcome {
|
|
87
|
+
const { financials, configurationOptions, ticks } = marketState
|
|
88
|
+
const secondsRemaining = Math.max(0, Number(financials.expirationTs) - Date.now() / 1000)
|
|
89
|
+
|
|
90
|
+
// Create effective price snapshot
|
|
91
|
+
const snap = new EffSnap(normalizedTimeRemaining(secondsRemaining), args.syExchangeRate)
|
|
92
|
+
|
|
93
|
+
// Current spot price
|
|
94
|
+
const currentSpot = ticks.currentSpotPrice
|
|
95
|
+
|
|
96
|
+
// Resolve tick prices - convert tick keys (parts per million) to spot prices
|
|
97
|
+
const lowerPrice = 1 + args.lowerTick / TICK_KEY_BASE_POINTS
|
|
98
|
+
const upperPrice = 1 + args.upperTick / TICK_KEY_BASE_POINTS
|
|
99
|
+
|
|
100
|
+
// Precompute effective prices
|
|
101
|
+
const priceEffLower = snap.getEffectivePrice(lowerPrice)
|
|
102
|
+
const priceEffUpper = snap.getEffectivePrice(upperPrice)
|
|
103
|
+
|
|
104
|
+
// Calculate liquidity needs
|
|
105
|
+
const liquidityNeeds = computeLiquidityTargetAndTokenNeeds(
|
|
106
|
+
snap,
|
|
107
|
+
currentSpot,
|
|
108
|
+
priceEffLower,
|
|
109
|
+
priceEffUpper,
|
|
110
|
+
lowerPrice,
|
|
111
|
+
upperPrice,
|
|
112
|
+
args.lowerTick,
|
|
113
|
+
args.upperTick,
|
|
114
|
+
ticks.currentTick,
|
|
115
|
+
args.maxSy,
|
|
116
|
+
args.maxPt,
|
|
117
|
+
configurationOptions.epsilonClamp,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
// Enforce budgets
|
|
121
|
+
const sySpent = liquidityNeeds.syNeeded
|
|
122
|
+
const ptSpent = liquidityNeeds.ptNeeded
|
|
123
|
+
|
|
124
|
+
if (sySpent > args.maxSy) {
|
|
125
|
+
throw new Error(`Insufficient SY budget: need ${sySpent}, have ${args.maxSy}`)
|
|
126
|
+
}
|
|
127
|
+
if (ptSpent > args.maxPt) {
|
|
128
|
+
throw new Error(`Insufficient PT budget: need ${ptSpent}, have ${args.maxPt}`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
deltaL: liquidityNeeds.liquidityTarget,
|
|
133
|
+
sySpent,
|
|
134
|
+
ptSpent,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Calculate the LP tokens that will be received for a given deposit
|
|
140
|
+
* This is useful for UI display and slippage calculations
|
|
141
|
+
*/
|
|
142
|
+
export function calculateLpOut(liquidityAdded: number, totalLiquidityBefore: number): number {
|
|
143
|
+
if (totalLiquidityBefore === 0) {
|
|
144
|
+
// First deposit
|
|
145
|
+
return liquidityAdded
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// LP tokens are proportional to liquidity added
|
|
149
|
+
return liquidityAdded
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Estimate the balanced amounts of PT and SY needed for a liquidity deposit
|
|
154
|
+
* Given a target liquidity amount, calculate how much PT and SY are needed
|
|
155
|
+
*/
|
|
156
|
+
export function estimateBalancedDeposit(
|
|
157
|
+
marketState: MarketThreeState,
|
|
158
|
+
targetLiquidity: number,
|
|
159
|
+
lowerTickApy: number,
|
|
160
|
+
upperTickApy: number,
|
|
161
|
+
): { ptNeeded: number; syNeeded: number } {
|
|
162
|
+
const { financials, ticks } = marketState
|
|
163
|
+
const secondsRemaining = Math.max(0, Number(financials.expirationTs) - Date.now() / 1000)
|
|
164
|
+
|
|
165
|
+
const snap = new EffSnap(normalizedTimeRemaining(secondsRemaining), marketState.currentSyExchangeRate)
|
|
166
|
+
|
|
167
|
+
const currentSpot = ticks.currentSpotPrice
|
|
168
|
+
// Convert APY (in %) to spot price: 1 + apy/100
|
|
169
|
+
const lowerPrice = 1.0 + lowerTickApy / 100
|
|
170
|
+
const upperPrice = 1.0 + upperTickApy / 100
|
|
171
|
+
|
|
172
|
+
const priceEffLower = snap.getEffectivePrice(lowerPrice)
|
|
173
|
+
const priceEffUpper = snap.getEffectivePrice(upperPrice)
|
|
174
|
+
|
|
175
|
+
if (currentSpot <= lowerPrice) {
|
|
176
|
+
// Below range: SY only
|
|
177
|
+
const syNeeded = (targetLiquidity / snap.timeFactor) * (priceEffLower - priceEffUpper)
|
|
178
|
+
return { ptNeeded: 0, syNeeded }
|
|
179
|
+
} else if (currentSpot >= upperPrice) {
|
|
180
|
+
// Above range: PT only
|
|
181
|
+
const ptNeeded = targetLiquidity * (upperPrice - lowerPrice)
|
|
182
|
+
return { ptNeeded, syNeeded: 0 }
|
|
183
|
+
} else {
|
|
184
|
+
// Inside range: both
|
|
185
|
+
const ptNeeded = targetLiquidity * (upperPrice - currentSpot)
|
|
186
|
+
const priceEffCurrent = snap.getEffectivePrice(currentSpot)
|
|
187
|
+
const syNeeded = (targetLiquidity / snap.timeFactor) * (priceEffLower - priceEffCurrent)
|
|
188
|
+
return { ptNeeded, syNeeded }
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Calculate SY and PT needed to deposit into liquidity pool from base token amount */
|
|
193
|
+
/** Off-chain analogue of on-chain wrapper_provide_liquidity function */
|
|
194
|
+
export function calcDepositSyAndPtFromBaseAmount(params: {
|
|
195
|
+
lowerPrice: number
|
|
196
|
+
upperPrice: number
|
|
197
|
+
baseTokenAmount: number
|
|
198
|
+
expirationTs: number
|
|
199
|
+
currentSpotPrice: number
|
|
200
|
+
syExchangeRate: number
|
|
201
|
+
}) {
|
|
202
|
+
const { expirationTs, currentSpotPrice, syExchangeRate, lowerPrice, upperPrice, baseTokenAmount } = params
|
|
203
|
+
|
|
204
|
+
if (baseTokenAmount <= 0 || syExchangeRate <= 0) {
|
|
205
|
+
return {
|
|
206
|
+
syNeeded: 0,
|
|
207
|
+
ptNeeded: 0,
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const secondsRemaining = Math.max(0, expirationTs)
|
|
212
|
+
const effSnap = new EffSnap(normalizedTimeRemaining(secondsRemaining), syExchangeRate)
|
|
213
|
+
|
|
214
|
+
const priceEffLower = effSnap.getEffectivePrice(lowerPrice)
|
|
215
|
+
const priceEffUpper = effSnap.getEffectivePrice(upperPrice)
|
|
216
|
+
|
|
217
|
+
// We mirror the on-chain logic in `wrapper_provide_liquidity`:
|
|
218
|
+
// 1. Use a large mock amount for both SY and PT to infer the *ratio* of SY/PT
|
|
219
|
+
// the market "wants" for this price range (compute_token_needs on-chain).
|
|
220
|
+
// 2. Use that ratio plus the current SY exchange rate to decide how much of the
|
|
221
|
+
// minted SY should be stripped into PT to keep the user-close to pool proportions.
|
|
222
|
+
const { syNeeded: syMock, ptNeeded: ptMock } = computeLiquidityTargetAndTokenNeeds(
|
|
223
|
+
effSnap,
|
|
224
|
+
currentSpotPrice,
|
|
225
|
+
priceEffLower,
|
|
226
|
+
priceEffUpper,
|
|
227
|
+
lowerPrice,
|
|
228
|
+
upperPrice,
|
|
229
|
+
0,
|
|
230
|
+
0,
|
|
231
|
+
0,
|
|
232
|
+
1e9,
|
|
233
|
+
1e9,
|
|
234
|
+
1e-18,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
// Total SY the user would get by minting SY from base off-chain
|
|
238
|
+
// (we approximate on-chain `mint_sy_return_data.sy_out_amount`).
|
|
239
|
+
const syAmount = Math.floor(baseTokenAmount * syExchangeRate)
|
|
240
|
+
|
|
241
|
+
// Off-chain analogue of on-chain `calc_strip_amount`:
|
|
242
|
+
// B = (amt_sy * market_pt_liq) / (market_pt_liq + market_sy_liq * cur_sy_rate)
|
|
243
|
+
// here: amt_sy = syAmount, market_pt_liq = ptMock, market_sy_liq = syMock
|
|
244
|
+
const denominator = syExchangeRate * syMock + ptMock
|
|
245
|
+
const toStrip = denominator > 0 ? Math.ceil((syAmount * ptMock) / denominator) : 0
|
|
246
|
+
|
|
247
|
+
const syForLiquidity = Math.max(0, syAmount - toStrip)
|
|
248
|
+
|
|
249
|
+
// We approximate PT created from the stripped SY as:
|
|
250
|
+
// ptAmount ≈ toStrip * syExchangeRate
|
|
251
|
+
// which is consistent with PT being a claim on base and `syExchangeRate`
|
|
252
|
+
// giving the base value of 1 SY.
|
|
253
|
+
const ptFromStrip = Math.floor(toStrip * syExchangeRate)
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
syNeeded: syForLiquidity,
|
|
257
|
+
ptNeeded: ptFromStrip,
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Calculate the amount of SY to strip into PT based on market liquidity ratio
|
|
262
|
+
* This matches the Rust implementation: calc_strip_amount in wrapper_provide_liquidity.rs
|
|
263
|
+
*
|
|
264
|
+
* The formula splits totalAmountSy into two lots: A (remains as SY) and B (stripped to PT)
|
|
265
|
+
* such that: A / (B * curSyRate) = marketSyLiq / marketPtLiq
|
|
266
|
+
*
|
|
267
|
+
* Solution: B = (totalAmountSy * marketPtLiq) / (marketPtLiq + marketSyLiq * curSyRate)
|
|
268
|
+
*/
|
|
269
|
+
function calcStripAmount(totalAmountSy: number, curSyRate: number, marketPtLiq: number, marketSyLiq: number): number {
|
|
270
|
+
const toStrip = (totalAmountSy * marketPtLiq) / (curSyRate * marketSyLiq + marketPtLiq)
|
|
271
|
+
return Math.ceil(toStrip)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Simulate wrapper_provide_liquidity instruction
|
|
276
|
+
* This matches the flow in wrapper_provide_liquidity.rs:
|
|
277
|
+
* 1. Mints SY from base asset
|
|
278
|
+
* 2. Calculates mock token needs for the tick range
|
|
279
|
+
* 3. Strips some SY into PT/YT based on the ratio
|
|
280
|
+
* 4. Deposits remaining SY + PT into concentrated liquidity position
|
|
281
|
+
* 5. Returns YT to user
|
|
282
|
+
*
|
|
283
|
+
* @param marketState - Current market state
|
|
284
|
+
* @param amountBase - Amount of base tokens (in asset terms, will be converted to SY)
|
|
285
|
+
* @param lowerTick - Lower tick (APY in basis points)
|
|
286
|
+
* @param upperTick - Upper tick (APY in basis points)
|
|
287
|
+
* @param syExchangeRate - SY exchange rate
|
|
288
|
+
* @returns Simulation result with LP out, YT out, amounts spent, etc.
|
|
289
|
+
*/
|
|
290
|
+
export function simulateWrapperProvideLiquidity(
|
|
291
|
+
marketState: MarketThreeState,
|
|
292
|
+
amountBase: number,
|
|
293
|
+
lowerTick: number,
|
|
294
|
+
upperTick: number,
|
|
295
|
+
syExchangeRate: number,
|
|
296
|
+
): {
|
|
297
|
+
lpOut: number
|
|
298
|
+
ytOut: number
|
|
299
|
+
syToStrip: number
|
|
300
|
+
ptFromStrip: number
|
|
301
|
+
syRemainder: number
|
|
302
|
+
ptDeposited: number
|
|
303
|
+
syDeposited: number
|
|
304
|
+
} | null {
|
|
305
|
+
try {
|
|
306
|
+
const { financials, configurationOptions, ticks } = marketState
|
|
307
|
+
const secondsRemaining = Math.max(0, Number(financials.expirationTs) - Date.now() / 1000)
|
|
308
|
+
|
|
309
|
+
// Step 1: Convert base to SY (simulating mint_sy)
|
|
310
|
+
const syAmount = amountBase / syExchangeRate
|
|
311
|
+
|
|
312
|
+
// Step 2: Create effective price snapshot
|
|
313
|
+
const snap = new EffSnap(normalizedTimeRemaining(secondsRemaining), syExchangeRate)
|
|
314
|
+
|
|
315
|
+
// Current spot price
|
|
316
|
+
const currentSpot = ticks.currentSpotPrice
|
|
317
|
+
|
|
318
|
+
// Convert tick keys to prices using compute_spot_price formula
|
|
319
|
+
// Rust: (1.0 + (key as f64) / TICK_KEY_BASE_POINTS).ln().exp() which simplifies to 1 + key/1_000_000
|
|
320
|
+
const lowerPrice = 1.0 + lowerTick / TICK_KEY_BASE_POINTS
|
|
321
|
+
const upperPrice = 1.0 + upperTick / TICK_KEY_BASE_POINTS
|
|
322
|
+
|
|
323
|
+
// Precompute effective prices
|
|
324
|
+
const priceEffLower = snap.getEffectivePrice(lowerPrice)
|
|
325
|
+
const priceEffUpper = snap.getEffectivePrice(upperPrice)
|
|
326
|
+
|
|
327
|
+
// Step 3: Calculate "mock" token needs using MOCK_AMOUNT (1e9)
|
|
328
|
+
// This gives us the ratio of SY:PT needed for this tick range
|
|
329
|
+
const MOCK_AMOUNT = 1e9
|
|
330
|
+
const mockNeeds = computeLiquidityTargetAndTokenNeeds(
|
|
331
|
+
snap,
|
|
332
|
+
currentSpot,
|
|
333
|
+
priceEffLower,
|
|
334
|
+
priceEffUpper,
|
|
335
|
+
lowerPrice,
|
|
336
|
+
upperPrice,
|
|
337
|
+
lowerTick,
|
|
338
|
+
upperTick,
|
|
339
|
+
ticks.currentTick,
|
|
340
|
+
MOCK_AMOUNT,
|
|
341
|
+
MOCK_AMOUNT,
|
|
342
|
+
configurationOptions.epsilonClamp,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
// Step 4: Calculate how much SY to strip
|
|
346
|
+
// Uses the mock amounts to determine the ratio
|
|
347
|
+
const syToStrip = calcStripAmount(syAmount, syExchangeRate, mockNeeds.ptNeeded, mockNeeds.syNeeded)
|
|
348
|
+
|
|
349
|
+
console.log("[simulateWrapperProvideLiquidity] Strip calculation:", {
|
|
350
|
+
syToStrip,
|
|
351
|
+
formula: `(${syAmount} * ${mockNeeds.ptNeeded}) / (${syExchangeRate} * ${mockNeeds.syNeeded} + ${mockNeeds.ptNeeded})`,
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
// Step 5: Calculate PT and YT from stripping
|
|
355
|
+
// When you strip SY, you get PT = SY * syExchangeRate and YT = SY * syExchangeRate
|
|
356
|
+
const ptFromStrip = syToStrip * syExchangeRate
|
|
357
|
+
const ytOut = ptFromStrip // YT amount equals PT amount from strip
|
|
358
|
+
|
|
359
|
+
// Step 6: Calculate remaining SY after strip
|
|
360
|
+
const syRemainder = syAmount - syToStrip
|
|
361
|
+
|
|
362
|
+
// Step 7: Simulate deposit liquidity with remaining SY and PT
|
|
363
|
+
const depositResult = simulateAddLiquidity(marketState, {
|
|
364
|
+
lowerTick,
|
|
365
|
+
upperTick,
|
|
366
|
+
maxSy: Math.floor(syRemainder),
|
|
367
|
+
maxPt: Math.floor(ptFromStrip),
|
|
368
|
+
syExchangeRate,
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
lpOut: depositResult.deltaL,
|
|
373
|
+
ytOut: Math.floor(ytOut),
|
|
374
|
+
syToStrip: Math.floor(syToStrip),
|
|
375
|
+
ptFromStrip: Math.floor(ptFromStrip),
|
|
376
|
+
syRemainder: Math.floor(syRemainder),
|
|
377
|
+
ptDeposited: depositResult.ptSpent,
|
|
378
|
+
syDeposited: depositResult.sySpent,
|
|
379
|
+
}
|
|
380
|
+
} catch (error) {
|
|
381
|
+
console.error("[simulateWrapperProvideLiquidity] Error:", error)
|
|
382
|
+
return null
|
|
383
|
+
}
|
|
384
|
+
}
|
package/src/bisect.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export function bisectSearch2(
|
|
2
|
+
func: (x: number) => number,
|
|
3
|
+
lo: number,
|
|
4
|
+
hi: number,
|
|
5
|
+
epsilon: number = 0.0001,
|
|
6
|
+
maxIterations: number = 2000,
|
|
7
|
+
): number | null {
|
|
8
|
+
if (lo > hi) {
|
|
9
|
+
throw new Error("lo must be less than hi")
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let iterations = 0
|
|
13
|
+
|
|
14
|
+
// Evaluate boundaries once
|
|
15
|
+
const fLo = func(lo)
|
|
16
|
+
const fHi = func(hi)
|
|
17
|
+
|
|
18
|
+
// Check if the target is bracketed between lo and hi
|
|
19
|
+
if (fLo * fHi > 0) {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Check if we're already at a root
|
|
24
|
+
if (Math.abs(fLo) < epsilon) return lo
|
|
25
|
+
if (Math.abs(fHi) < epsilon) return hi
|
|
26
|
+
|
|
27
|
+
// Track best approximation in case we don't converge exactly
|
|
28
|
+
let bestMid = (lo + hi) / 2
|
|
29
|
+
let bestError = Infinity
|
|
30
|
+
|
|
31
|
+
// Binary search within the range
|
|
32
|
+
while (iterations < maxIterations) {
|
|
33
|
+
// Use midpoint for standard bisection
|
|
34
|
+
const mid = (lo + hi) / 2
|
|
35
|
+
const fMid = func(mid)
|
|
36
|
+
const absFMid = Math.abs(fMid)
|
|
37
|
+
|
|
38
|
+
iterations++
|
|
39
|
+
|
|
40
|
+
// Track best approximation
|
|
41
|
+
if (absFMid < bestError) {
|
|
42
|
+
bestError = absFMid
|
|
43
|
+
bestMid = mid
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check convergence
|
|
47
|
+
if (absFMid < epsilon) {
|
|
48
|
+
return mid
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Early termination if range is too small relative to epsilon
|
|
52
|
+
const rangeSize = hi - lo
|
|
53
|
+
if (rangeSize < epsilon * 0.01) {
|
|
54
|
+
return bestMid
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Adjust the search range based on the target
|
|
58
|
+
// Use fLo instead of recalculating func(lo)
|
|
59
|
+
if (fLo * fMid < 0) {
|
|
60
|
+
// eslint-disable-next-line no-param-reassign
|
|
61
|
+
hi = mid
|
|
62
|
+
// Note: fHi would be fMid, but we don't need to track it
|
|
63
|
+
} else {
|
|
64
|
+
// eslint-disable-next-line no-param-reassign
|
|
65
|
+
lo = mid
|
|
66
|
+
// Note: fLo would be fMid, but we recalculate in the condition anyway
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Return best approximation if we didn't converge
|
|
71
|
+
return bestMid
|
|
72
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Market Three Math - CLMM Simulation Package
|
|
3
|
+
*
|
|
4
|
+
* This package provides TypeScript implementations of the CLMM (Concentrated Liquidity Market Maker)
|
|
5
|
+
* mathematical functions from the Exponent CLMM program. It allows for client-side simulation of:
|
|
6
|
+
* - Swap operations (PT <-> SY trades)
|
|
7
|
+
* - Liquidity deposits (add_liquidity)
|
|
8
|
+
* - Price and liquidity calculations
|
|
9
|
+
*
|
|
10
|
+
* The implementations are ported from Rust code in solana/programs/exponent_clmm
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Export types
|
|
14
|
+
export * from "./types"
|
|
15
|
+
|
|
16
|
+
// Export utility functions
|
|
17
|
+
export {
|
|
18
|
+
EffSnap,
|
|
19
|
+
normalizedTimeRemaining,
|
|
20
|
+
calculateFeeRate,
|
|
21
|
+
getFeeFromAmount,
|
|
22
|
+
getActiveLiquidity,
|
|
23
|
+
getSuccessorTickKey,
|
|
24
|
+
getPredecessorTickKey,
|
|
25
|
+
getImpliedRate,
|
|
26
|
+
findTickByKey,
|
|
27
|
+
convertApyToApyBp,
|
|
28
|
+
convertApyBpToApy,
|
|
29
|
+
calcPtPriceInAsset,
|
|
30
|
+
getSuccessorTickByIdx,
|
|
31
|
+
} from "./utils"
|
|
32
|
+
|
|
33
|
+
// Export swap functions
|
|
34
|
+
export { simulateSwap } from "./swap"
|
|
35
|
+
export { getSwapQuote, QuoteDirection } from "./quote"
|
|
36
|
+
|
|
37
|
+
// Export YT trade functions
|
|
38
|
+
export { simulateBuyYt, simulateSellYt, simulateBuyYtWithSyIn } from "./ytTrades"
|
|
39
|
+
export type {
|
|
40
|
+
BuyYtSimulationArgs,
|
|
41
|
+
BuyYtSimulationResult,
|
|
42
|
+
SellYtSimulationArgs,
|
|
43
|
+
SellYtSimulationResult,
|
|
44
|
+
} from "./ytTrades"
|
|
45
|
+
|
|
46
|
+
// Export add liquidity functions
|
|
47
|
+
export {
|
|
48
|
+
simulateAddLiquidity,
|
|
49
|
+
computeLiquidityTargetAndTokenNeeds,
|
|
50
|
+
calculateLpOut,
|
|
51
|
+
estimateBalancedDeposit,
|
|
52
|
+
calcDepositSyAndPtFromBaseAmount,
|
|
53
|
+
simulateWrapperProvideLiquidity,
|
|
54
|
+
} from "./addLiquidity"
|
|
55
|
+
|
|
56
|
+
export { getPtAndSyOnWithdrawLiquidity } from "./withdrawLiquidity"
|
|
57
|
+
|
|
58
|
+
// Export bisect search utility
|
|
59
|
+
export { bisectSearch2 } from "./bisect"
|
|
60
|
+
|
|
61
|
+
// Export liquidity histogram functions
|
|
62
|
+
export { buildLiquidityHistogram, buildLiquidityHistogramSimple } from "./liquidityHistogram"
|
|
63
|
+
export type { LiquidityHistogramBin } from "./liquidityHistogram"
|
|
64
|
+
|
|
65
|
+
// Re-export common types for convenience
|
|
66
|
+
export type {
|
|
67
|
+
SwapArgs,
|
|
68
|
+
SwapOutcome,
|
|
69
|
+
AddLiquidityArgs,
|
|
70
|
+
AddLiquidityOutcome,
|
|
71
|
+
LiquidityNeeds,
|
|
72
|
+
MarketThreeState,
|
|
73
|
+
} from "./types"
|
|
74
|
+
export { SwapDirection } from "./types"
|