@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,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLMM Swap simulation
|
|
3
|
+
* Ported from exponent_clmm/src/state/market_three/helpers/swap.rs
|
|
4
|
+
*/
|
|
5
|
+
import { MarketThreeState, SwapArgs, SwapDirection, SwapOutcome } from "./types"
|
|
6
|
+
import {
|
|
7
|
+
EffSnap,
|
|
8
|
+
bigIntMax,
|
|
9
|
+
bigIntMin,
|
|
10
|
+
calculateFeeRate,
|
|
11
|
+
findTickByKey,
|
|
12
|
+
getFeeFromAmount,
|
|
13
|
+
getImpliedRate,
|
|
14
|
+
getPredecessorTickKey,
|
|
15
|
+
getSuccessorTickKey,
|
|
16
|
+
normalizedTimeRemaining,
|
|
17
|
+
} from "./utils"
|
|
18
|
+
|
|
19
|
+
const BASE_POINTS = 10000
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Simulate a swap on the CLMM market
|
|
23
|
+
* This is a pure function that does not mutate the market state
|
|
24
|
+
* Returns the swap outcome including amounts and final state
|
|
25
|
+
*/
|
|
26
|
+
export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): SwapOutcome {
|
|
27
|
+
const { financials, configurationOptions, ticks } = marketState
|
|
28
|
+
const secondsRemaining = Math.max(0, Number(financials.expirationTs) - Date.now() / 1000)
|
|
29
|
+
|
|
30
|
+
// Create effective price snapshot
|
|
31
|
+
const snapshot = new EffSnap(normalizedTimeRemaining(secondsRemaining), args.syExchangeRate)
|
|
32
|
+
|
|
33
|
+
// Current state
|
|
34
|
+
let currentPriceSpot: number = ticks.currentSpotPrice
|
|
35
|
+
let currentLeftBoundaryIndex: number = ticks.currentTick
|
|
36
|
+
|
|
37
|
+
// Use currentPrefixSum if available, otherwise fall back to calculating it
|
|
38
|
+
let activeLiquidityU64: bigint = ticks.currentPrefixSum ?? 0n
|
|
39
|
+
let activeLiquidityF64: number = Number(activeLiquidityU64)
|
|
40
|
+
|
|
41
|
+
// Fees
|
|
42
|
+
const lpFeeRate = calculateFeeRate(configurationOptions.lnFeeRateRoot, secondsRemaining)
|
|
43
|
+
const protocolFeeBps: number = configurationOptions.treasuryFeeBps
|
|
44
|
+
|
|
45
|
+
// Check price limits
|
|
46
|
+
if (args.priceSpotLimit !== undefined) {
|
|
47
|
+
if (args.direction === SwapDirection.PtToSy) {
|
|
48
|
+
if (args.priceSpotLimit < currentPriceSpot) {
|
|
49
|
+
throw new Error("Price limit violated: limit must be >= current price for PtToSy")
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
if (args.priceSpotLimit > currentPriceSpot) {
|
|
53
|
+
throw new Error("Price limit violated: limit must be <= current price for SyToPt")
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Accumulators
|
|
59
|
+
let amountOutNet: number = 0
|
|
60
|
+
let feeLpOut: number = 0
|
|
61
|
+
let feeProtocolOut: number = 0
|
|
62
|
+
let amountInLeft: number = args.amountIn
|
|
63
|
+
|
|
64
|
+
// Main loop across contiguous intervals
|
|
65
|
+
let iterations: number = 0
|
|
66
|
+
const maxIterations: number = 1000
|
|
67
|
+
|
|
68
|
+
while (amountInLeft > 0 && iterations < maxIterations) {
|
|
69
|
+
iterations++
|
|
70
|
+
|
|
71
|
+
// Get right boundary of current interval
|
|
72
|
+
const rightBoundaryIndexOpt = getSuccessorTickKey(ticks, currentLeftBoundaryIndex)
|
|
73
|
+
|
|
74
|
+
if (rightBoundaryIndexOpt === null) {
|
|
75
|
+
if (args.direction === SwapDirection.SyToPt) {
|
|
76
|
+
// Cross to create a new interval
|
|
77
|
+
const predecessor = getPredecessorTickKey(ticks, currentLeftBoundaryIndex)
|
|
78
|
+
if (predecessor === null) break
|
|
79
|
+
|
|
80
|
+
// When crossing downward (SyToPt), update state
|
|
81
|
+
currentPriceSpot = getImpliedRate(currentLeftBoundaryIndex) // Boundary we're crossing
|
|
82
|
+
currentLeftBoundaryIndex = predecessor // New left boundary
|
|
83
|
+
|
|
84
|
+
// Update active liquidity by subtracting liquidity_net at boundary
|
|
85
|
+
const boundaryTick = findTickByKey(ticks, predecessor)
|
|
86
|
+
if (boundaryTick) {
|
|
87
|
+
activeLiquidityU64 = bigIntMax(0n, activeLiquidityU64 - boundaryTick.tick.liquidityNet)
|
|
88
|
+
activeLiquidityF64 = Number(activeLiquidityU64)
|
|
89
|
+
}
|
|
90
|
+
continue
|
|
91
|
+
} else {
|
|
92
|
+
// No more liquidity available
|
|
93
|
+
break
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const rightBoundaryIndex = rightBoundaryIndexOpt
|
|
98
|
+
|
|
99
|
+
// Get anchor prices for interval boundaries
|
|
100
|
+
const anchorULeft: number = getImpliedRate(currentLeftBoundaryIndex)
|
|
101
|
+
const anchorURight: number = getImpliedRate(rightBoundaryIndex)
|
|
102
|
+
|
|
103
|
+
// Effective price at current spot
|
|
104
|
+
const cEffOld: number = snapshot.getEffectivePrice(currentPriceSpot)
|
|
105
|
+
|
|
106
|
+
// Get principal ledgers for the interval
|
|
107
|
+
const currentTickData = findTickByKey(ticks, currentLeftBoundaryIndex)
|
|
108
|
+
const principalPt: bigint = currentTickData?.tick.principalPt ?? 0n
|
|
109
|
+
const principalSy: bigint = currentTickData?.tick.principalSy ?? 0n
|
|
110
|
+
|
|
111
|
+
const eps: number = configurationOptions.epsilonClamp
|
|
112
|
+
|
|
113
|
+
// Calculate kappa (scaling factor based on available principal)
|
|
114
|
+
// Y_max = (L/τ) * (C(u_old) - C(u_right))
|
|
115
|
+
const cEffAtBoundary: number = snapshot.getEffectivePrice(anchorURight)
|
|
116
|
+
const yMaxToBoundaryF: number = (Number(activeLiquidityF64) / snapshot.timeFactor) * (cEffOld - cEffAtBoundary)
|
|
117
|
+
const kappaSy: number = yMaxToBoundaryF > 0 ? Number(principalSy) / Number(yMaxToBoundaryF) : 0
|
|
118
|
+
|
|
119
|
+
const duToLeft: number = currentPriceSpot - anchorULeft
|
|
120
|
+
const ptMaxToLeftF: number = Number(activeLiquidityF64) * duToLeft
|
|
121
|
+
const kappaPt: number = ptMaxToLeftF > 0 ? Number(principalPt) / ptMaxToLeftF : 0
|
|
122
|
+
|
|
123
|
+
const kappa: number = Math.min(kappaPt, kappaSy, 1.0)
|
|
124
|
+
const lTradeF64: number = Number(activeLiquidityF64) * kappa
|
|
125
|
+
|
|
126
|
+
if (args.direction === SwapDirection.PtToSy) {
|
|
127
|
+
// PT -> SY swap (buying SY with PT)
|
|
128
|
+
const duByInput = lTradeF64 > 0 ? amountInLeft / lTradeF64 : 0
|
|
129
|
+
const duToBoundary = anchorURight - currentPriceSpot
|
|
130
|
+
const duActual = Math.min(duByInput, duToBoundary)
|
|
131
|
+
|
|
132
|
+
if (duToBoundary <= eps) {
|
|
133
|
+
// Cross boundary
|
|
134
|
+
const boundaryTick = findTickByKey(ticks, rightBoundaryIndex)
|
|
135
|
+
if (boundaryTick) {
|
|
136
|
+
activeLiquidityU64 += boundaryTick.tick.liquidityNet
|
|
137
|
+
activeLiquidityF64 = Number(activeLiquidityU64)
|
|
138
|
+
}
|
|
139
|
+
currentLeftBoundaryIndex = rightBoundaryIndex
|
|
140
|
+
currentPriceSpot = anchorURight
|
|
141
|
+
continue
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Token flows for this segment
|
|
145
|
+
const ptInSegment: number = Math.floor(lTradeF64 * duActual)
|
|
146
|
+
const anchorUNew: number = currentPriceSpot + duActual
|
|
147
|
+
const cEffNew: number = snapshot.getEffectivePrice(anchorUNew)
|
|
148
|
+
const syOutGross: number = Math.floor((lTradeF64 / snapshot.timeFactor) * (cEffOld - cEffNew))
|
|
149
|
+
const syOutGrossClamped: number = Math.min(syOutGross, Number(principalSy))
|
|
150
|
+
|
|
151
|
+
if (syOutGrossClamped > 0) {
|
|
152
|
+
const totalFeeOut: number = getFeeFromAmount(syOutGrossClamped, lpFeeRate)
|
|
153
|
+
const protocolFeeOut: number = Math.floor((totalFeeOut * protocolFeeBps) / BASE_POINTS)
|
|
154
|
+
const lpFeeOut: number = totalFeeOut - protocolFeeOut
|
|
155
|
+
const syOutNet: number = syOutGrossClamped - totalFeeOut
|
|
156
|
+
|
|
157
|
+
amountOutNet += syOutNet
|
|
158
|
+
feeLpOut += lpFeeOut
|
|
159
|
+
feeProtocolOut += protocolFeeOut
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
amountInLeft -= ptInSegment
|
|
163
|
+
currentPriceSpot = anchorUNew
|
|
164
|
+
} else {
|
|
165
|
+
// SY -> PT swap (buying PT with SY)
|
|
166
|
+
const cEffLeft: number = snapshot.getEffectivePrice(anchorULeft)
|
|
167
|
+
const deltaCByInput: number = lTradeF64 > 0 ? (snapshot.timeFactor / lTradeF64) * amountInLeft : 0
|
|
168
|
+
const deltaCToLeftBoundary: number = Math.max(0, cEffLeft - cEffOld)
|
|
169
|
+
const deltaCActual: number = Math.min(deltaCByInput, deltaCToLeftBoundary)
|
|
170
|
+
|
|
171
|
+
if (deltaCToLeftBoundary <= eps) {
|
|
172
|
+
// Cross boundary to the left
|
|
173
|
+
const predecessor = getPredecessorTickKey(ticks, currentLeftBoundaryIndex)
|
|
174
|
+
if (predecessor === null) break
|
|
175
|
+
|
|
176
|
+
// Update active liquidity
|
|
177
|
+
const boundaryTick = findTickByKey(ticks, currentLeftBoundaryIndex)
|
|
178
|
+
if (boundaryTick) {
|
|
179
|
+
activeLiquidityU64 = bigIntMax(0n, activeLiquidityU64 - boundaryTick.tick.liquidityNet)
|
|
180
|
+
activeLiquidityF64 = Number(activeLiquidityU64)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
currentPriceSpot = getImpliedRate(currentLeftBoundaryIndex)
|
|
184
|
+
currentLeftBoundaryIndex = predecessor
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// New effective price and spot price after consuming ΔC
|
|
189
|
+
const cEffNew: number = cEffOld + deltaCActual
|
|
190
|
+
const spotPriceNew: number = snapshot.spotPriceFromEffectivePrice(cEffNew)
|
|
191
|
+
|
|
192
|
+
// Token flows
|
|
193
|
+
const syInSegmentF: number = (lTradeF64 / snapshot.timeFactor) * (cEffNew - cEffOld)
|
|
194
|
+
const duAbs: number = currentPriceSpot - spotPriceNew
|
|
195
|
+
const ptOutGrossF: number = lTradeF64 * duAbs
|
|
196
|
+
|
|
197
|
+
// Clamp gross PT by available principal
|
|
198
|
+
const ptOutGrossU64: bigint = bigIntMin(BigInt(Math.floor(ptOutGrossF)), principalPt)
|
|
199
|
+
const syInSegmentU64: bigint = BigInt(Math.floor(syInSegmentF))
|
|
200
|
+
|
|
201
|
+
if (ptOutGrossU64 === 0n) {
|
|
202
|
+
// Nothing to pay out; try to cross
|
|
203
|
+
const predecessor = getPredecessorTickKey(ticks, currentLeftBoundaryIndex)
|
|
204
|
+
if (predecessor === null) break
|
|
205
|
+
|
|
206
|
+
// Update active liquidity
|
|
207
|
+
const boundaryTick = findTickByKey(ticks, currentLeftBoundaryIndex)
|
|
208
|
+
if (boundaryTick) {
|
|
209
|
+
activeLiquidityU64 = bigIntMax(0n, activeLiquidityU64 - boundaryTick.tick.liquidityNet)
|
|
210
|
+
activeLiquidityF64 = Number(activeLiquidityU64)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
currentPriceSpot = getImpliedRate(currentLeftBoundaryIndex)
|
|
214
|
+
currentLeftBoundaryIndex = predecessor
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Fees in token_out (PT)
|
|
219
|
+
const totalFeeOut = getFeeFromAmount(Number(ptOutGrossU64), lpFeeRate)
|
|
220
|
+
const protocolFeeOut = Math.floor((totalFeeOut * protocolFeeBps) / BASE_POINTS)
|
|
221
|
+
const lpFeeOut = totalFeeOut - protocolFeeOut
|
|
222
|
+
const ptOutNet = Number(ptOutGrossU64) - totalFeeOut
|
|
223
|
+
|
|
224
|
+
// Accumulate to user
|
|
225
|
+
amountOutNet += ptOutNet
|
|
226
|
+
feeLpOut += lpFeeOut
|
|
227
|
+
feeProtocolOut += protocolFeeOut
|
|
228
|
+
|
|
229
|
+
// Consume input and advance state
|
|
230
|
+
amountInLeft -= Number(syInSegmentU64)
|
|
231
|
+
currentPriceSpot = spotPriceNew
|
|
232
|
+
|
|
233
|
+
// If we hit boundary, cross
|
|
234
|
+
if (Math.abs(currentPriceSpot - anchorULeft) <= eps && amountInLeft > 0) {
|
|
235
|
+
const predecessor = getPredecessorTickKey(ticks, currentLeftBoundaryIndex)
|
|
236
|
+
if (predecessor === null) break
|
|
237
|
+
|
|
238
|
+
// Update active liquidity
|
|
239
|
+
const boundaryTick = findTickByKey(ticks, currentLeftBoundaryIndex)
|
|
240
|
+
if (boundaryTick) {
|
|
241
|
+
activeLiquidityU64 = bigIntMax(0n, activeLiquidityU64 - boundaryTick.tick.liquidityNet)
|
|
242
|
+
activeLiquidityF64 = Number(activeLiquidityU64)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
currentPriceSpot = getImpliedRate(currentLeftBoundaryIndex)
|
|
246
|
+
currentLeftBoundaryIndex = predecessor
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
amountInConsumed: args.amountIn - amountInLeft,
|
|
253
|
+
amountOut: amountOutNet,
|
|
254
|
+
lpFeeChargedOutToken: feeLpOut,
|
|
255
|
+
protocolFeeChargedOutToken: feeProtocolOut,
|
|
256
|
+
finalSpotPrice: currentPriceSpot,
|
|
257
|
+
finalTickIndex: currentLeftBoundaryIndex,
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Calculate the expected output for a given input amount
|
|
263
|
+
* This is a convenience wrapper around simulateSwap
|
|
264
|
+
*/
|
|
265
|
+
export function getSwapQuote(marketState: MarketThreeState, amountIn: number, direction: SwapDirection): SwapOutcome {
|
|
266
|
+
return simulateSwap(marketState, {
|
|
267
|
+
direction,
|
|
268
|
+
amountIn,
|
|
269
|
+
syExchangeRate: marketState.currentSyExchangeRate,
|
|
270
|
+
isCurrentFlashSwap: false,
|
|
271
|
+
})
|
|
272
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { MarketConfigurationOptions, MarketThreeFinancials, Ticks } from "@exponent-labs/exponent-fetcher"
|
|
2
|
+
|
|
3
|
+
export enum SwapDirection {
|
|
4
|
+
PtToSy = "PtToSy",
|
|
5
|
+
SyToPt = "SyToPt",
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface MarketThreeState {
|
|
9
|
+
/** Market financials */
|
|
10
|
+
financials: MarketThreeFinancials
|
|
11
|
+
/** Configuration options */
|
|
12
|
+
configurationOptions: MarketConfigurationOptions
|
|
13
|
+
/** Ticks data */
|
|
14
|
+
ticks: Ticks
|
|
15
|
+
/** Current SY exchange rate */
|
|
16
|
+
currentSyExchangeRate: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SwapArgs {
|
|
20
|
+
/** Direction of the swap */
|
|
21
|
+
direction: SwapDirection
|
|
22
|
+
/** Exact amount in */
|
|
23
|
+
amountIn: number
|
|
24
|
+
/** Optional spot price limit (anti-sandwich) */
|
|
25
|
+
priceSpotLimit?: number
|
|
26
|
+
/** SY exchange rate */
|
|
27
|
+
syExchangeRate: number
|
|
28
|
+
/** Is this a flash swap? */
|
|
29
|
+
isCurrentFlashSwap: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SwapOutcome {
|
|
33
|
+
/** Amount of input consumed */
|
|
34
|
+
amountInConsumed: number
|
|
35
|
+
/** Amount out (after fees) */
|
|
36
|
+
amountOut: number
|
|
37
|
+
/** LP fee charged in out token */
|
|
38
|
+
lpFeeChargedOutToken: number
|
|
39
|
+
/** Protocol fee charged in out token */
|
|
40
|
+
protocolFeeChargedOutToken: number
|
|
41
|
+
/** Final spot price after swap */
|
|
42
|
+
finalSpotPrice: number
|
|
43
|
+
/** Final tick index after swap */
|
|
44
|
+
finalTickIndex: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface AddLiquidityArgs {
|
|
48
|
+
/** Lower tick key (ln implied rate in bps) */
|
|
49
|
+
lowerTick: number
|
|
50
|
+
/** Upper tick key (ln implied rate in bps) */
|
|
51
|
+
upperTick: number
|
|
52
|
+
/** Maximum SY to spend */
|
|
53
|
+
maxSy: number
|
|
54
|
+
/** Maximum PT to spend */
|
|
55
|
+
maxPt: number
|
|
56
|
+
/** SY exchange rate */
|
|
57
|
+
syExchangeRate: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface AddLiquidityOutcome {
|
|
61
|
+
/** Liquidity added */
|
|
62
|
+
deltaL: number
|
|
63
|
+
/** SY spent */
|
|
64
|
+
sySpent: number
|
|
65
|
+
/** PT spent */
|
|
66
|
+
ptSpent: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface LiquidityNeeds {
|
|
70
|
+
/** Target liquidity to add */
|
|
71
|
+
liquidityTarget: number
|
|
72
|
+
/** SY needed */
|
|
73
|
+
syNeeded: number
|
|
74
|
+
/** PT needed */
|
|
75
|
+
ptNeeded: number
|
|
76
|
+
/** Price split point for the need calculation */
|
|
77
|
+
priceSplitForNeed: number
|
|
78
|
+
/** Tick index of the split point */
|
|
79
|
+
priceSplitTickIdx: number
|
|
80
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for CLMM calculations
|
|
3
|
+
* Ported from exponent_clmm/src/utils/math.rs
|
|
4
|
+
*/
|
|
5
|
+
import { Tick, Ticks } from "@exponent-labs/exponent-fetcher"
|
|
6
|
+
|
|
7
|
+
const SECONDS_PER_YEAR = 365 * 24 * 60 * 60
|
|
8
|
+
// const BASE_POINTS = 10000
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Effective price snapshot
|
|
12
|
+
* This captures the time factor and SY exchange rate at a given moment
|
|
13
|
+
* Matches the Rust implementation from exponent_clmm/src/state/market_three/helpers/add_liquidity.rs
|
|
14
|
+
*/
|
|
15
|
+
export class EffSnap {
|
|
16
|
+
/** Time factor (tau = normalized time remaining) */
|
|
17
|
+
timeFactor: number
|
|
18
|
+
/** SY exchange rate (r) */
|
|
19
|
+
syExchangeRate: number
|
|
20
|
+
|
|
21
|
+
constructor(timeFactor: number, syExchangeRate: number) {
|
|
22
|
+
this.timeFactor = timeFactor
|
|
23
|
+
this.syExchangeRate = syExchangeRate
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Calculate effective price from spot price
|
|
28
|
+
* Rust formula: C(u) = u^(-(τ-1)) / (r * (τ - 1))
|
|
29
|
+
* where τ = time_factor, u = spot_price (ln implied rate), r = sy_exchange_rate
|
|
30
|
+
*/
|
|
31
|
+
getEffectivePrice(u: number): number {
|
|
32
|
+
if (this.timeFactor === 1.0) {
|
|
33
|
+
throw new Error("time_factor cannot be 1.0")
|
|
34
|
+
}
|
|
35
|
+
return Math.pow(u, -this.timeFactor + 1.0) / (this.syExchangeRate * (this.timeFactor - 1.0))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Convert effective price back to spot price
|
|
40
|
+
* Rust formula: u = (r * C * (τ - 1))^(1/(1-τ))
|
|
41
|
+
* In Rust: spot_price_from_effective_price
|
|
42
|
+
*/
|
|
43
|
+
spotPriceFromEffectivePrice(cEff: number): number {
|
|
44
|
+
const base = this.syExchangeRate * cEff * (this.timeFactor - 1.0)
|
|
45
|
+
return Math.pow(base, 1.0 / (1.0 - this.timeFactor))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Legacy alias for backwards compatibility
|
|
50
|
+
*/
|
|
51
|
+
impliedRateFromEffectivePrice(cEff: number): number {
|
|
52
|
+
return this.spotPriceFromEffectivePrice(cEff)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Calculate delta SY from change in effective price
|
|
57
|
+
* Rust formula: ΔSY = L * (C_old - C_new)
|
|
58
|
+
* Note: No division by time_factor in the Rust implementation
|
|
59
|
+
*/
|
|
60
|
+
deltaEffPriceToSy(liquidity: number, oldEffPrice: number, newEffPrice: number): number {
|
|
61
|
+
return liquidity * (oldEffPrice - newEffPrice)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Calculate normalized time remaining
|
|
67
|
+
* Returns seconds_remaining / SECONDS_PER_YEAR
|
|
68
|
+
*/
|
|
69
|
+
export function normalizedTimeRemaining(secondsRemaining: number): number {
|
|
70
|
+
return secondsRemaining / SECONDS_PER_YEAR
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Calculate current fee rate from fee rate root and time remaining
|
|
75
|
+
* fee_rate = e^(ln_fee_rate_root * time_factor)
|
|
76
|
+
*/
|
|
77
|
+
export function calculateFeeRate(lnFeeRateRoot: number, secondsRemaining: number): number {
|
|
78
|
+
const timeFactor = normalizedTimeRemaining(secondsRemaining)
|
|
79
|
+
return Math.exp(lnFeeRateRoot * timeFactor)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Calculate fee from amount
|
|
84
|
+
* Returns the fee to charge given an amount and fee rate
|
|
85
|
+
* fee = amount * (fee_rate - 1.0)
|
|
86
|
+
*/
|
|
87
|
+
export function getFeeFromAmount(amount: number, feeRate: number): number {
|
|
88
|
+
return Math.ceil(amount * (feeRate - 1.0))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Find the active liquidity at a given tick index
|
|
93
|
+
* This is computed by summing liquidity_net up to the current tick
|
|
94
|
+
*/
|
|
95
|
+
export function getActiveLiquidity(ticks: Ticks, tickIndex: number): bigint {
|
|
96
|
+
let activeLiquidity = 0n
|
|
97
|
+
|
|
98
|
+
// Sum all liquidity_net from ticks up to and including tickIndex
|
|
99
|
+
|
|
100
|
+
for (const tick of ticks.ticksTree) {
|
|
101
|
+
if (tick.apyBasePoints <= tickIndex) {
|
|
102
|
+
activeLiquidity += tick.liquidityNet
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return activeLiquidity
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Find the successor tick (next tick to the right)
|
|
111
|
+
* In Rust: ticks.successor_idx(current_left_boundary_index)
|
|
112
|
+
* @param ticks - The ticks data structure
|
|
113
|
+
* @param currentTickKey - The current tick key (apyBasePoints)
|
|
114
|
+
* @returns The next tick key (apyBasePoints) or null if none exists
|
|
115
|
+
*/
|
|
116
|
+
export function getSuccessorTickKey(ticks: Ticks, currentTickKey: number): number | null {
|
|
117
|
+
let minKey: number | null = null
|
|
118
|
+
|
|
119
|
+
for (const tick of ticks.ticksTree) {
|
|
120
|
+
if (tick.apyBasePoints > currentTickKey) {
|
|
121
|
+
if (minKey === null || tick.apyBasePoints < minKey) {
|
|
122
|
+
minKey = tick.apyBasePoints
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return minKey
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Find the successor tick by index in the tick tree
|
|
132
|
+
* This is almost equivalent to the Rust successor_idx method
|
|
133
|
+
*/
|
|
134
|
+
export function getSuccessorTickByIdx(ticks: Ticks, tickIdx: number): number | null {
|
|
135
|
+
//TODO Refactor to make it more CPU efficient
|
|
136
|
+
const tick = ticks.ticksTree.at(tickIdx - 1) ?? null
|
|
137
|
+
|
|
138
|
+
if (!tick) return null
|
|
139
|
+
|
|
140
|
+
// Find ticks with apyBasePoints greater than tickIdx
|
|
141
|
+
const successorTicks = ticks.ticksTree
|
|
142
|
+
.filter((t) => t.apyBasePoints > tick.apyBasePoints)
|
|
143
|
+
.sort((a, b) => a.apyBasePoints - b.apyBasePoints)
|
|
144
|
+
|
|
145
|
+
const successorTick = successorTicks.at(0) ?? null
|
|
146
|
+
|
|
147
|
+
return !!successorTick ? ticks.ticksTree.indexOf(successorTick) + 1 : null
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Find the predecessor tick (next tick to the left)
|
|
152
|
+
* In Rust: ticks.predecessor_idx(current_left_boundary_index)
|
|
153
|
+
* @param ticks - The ticks data structure
|
|
154
|
+
* @param currentTickKey - The current tick key (apyBasePoints)
|
|
155
|
+
* @returns The previous tick key (apyBasePoints) or null if none exists
|
|
156
|
+
*/
|
|
157
|
+
export function getPredecessorTickKey(ticks: Ticks, currentTickKey: number): number | null {
|
|
158
|
+
let maxKey: number | null = null
|
|
159
|
+
|
|
160
|
+
for (const tick of ticks.ticksTree) {
|
|
161
|
+
if (tick.apyBasePoints < currentTickKey) {
|
|
162
|
+
if (maxKey === null || tick.apyBasePoints > maxKey) {
|
|
163
|
+
maxKey = tick.apyBasePoints
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return maxKey
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get the spot price (implied rate) for a tick key
|
|
173
|
+
* Tick key represents APY in parts per million (1e6)
|
|
174
|
+
* spot_price = 1 + tick_key / 1e6
|
|
175
|
+
*
|
|
176
|
+
* This is equivalent to the Rust code:
|
|
177
|
+
* let spot_price = (1.0 + (key as f64) / TICK_KEY_BASE_POINTS);
|
|
178
|
+
* But Rust stores it pre-computed, we compute it here
|
|
179
|
+
*/
|
|
180
|
+
export function getImpliedRate(tickKey: number): number {
|
|
181
|
+
const TICK_KEY_BASE_POINTS = 1_000_000
|
|
182
|
+
return 1.0 + tickKey / TICK_KEY_BASE_POINTS
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Find a tick by its key
|
|
187
|
+
*/
|
|
188
|
+
export function findTickByKey(ticks: Ticks, tickKey: number): { tick: Tick; index: number } | null {
|
|
189
|
+
const index = ticks.ticksTree.findIndex((t) => t.apyBasePoints === tickKey)
|
|
190
|
+
if (index === -1) return null
|
|
191
|
+
return { tick: ticks.ticksTree[index - 1], index }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Find a tick by its index
|
|
196
|
+
*/
|
|
197
|
+
export function findTickByIndex(ticks: Ticks, index: number): Tick {
|
|
198
|
+
return ticks.ticksTree.at(index - 1) ?? null
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Convert APY percentage to basis points
|
|
203
|
+
*/
|
|
204
|
+
export function convertApyToApyBp(apyPercent: number): number {
|
|
205
|
+
return Math.round(apyPercent * 100)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Convert basis points to APY percentage
|
|
210
|
+
*/
|
|
211
|
+
export function convertApyBpToApy(apyBp: number): number {
|
|
212
|
+
return apyBp / 100
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function bigIntMax(...args: bigint[]): bigint {
|
|
216
|
+
return args.reduce((m, e) => (e > m ? e : m))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function bigIntMin(...args: bigint[]): bigint {
|
|
220
|
+
return args.reduce((m, e) => (e < m ? e : m))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Calculate PT price in asset from current spot price and expiration timestamp
|
|
225
|
+
* @param currentSpotPrice - Current spot price in format of 1 + percentage / 100
|
|
226
|
+
* @param expirationTs - Expiration timestamp in seconds
|
|
227
|
+
* @returns PT price in asset
|
|
228
|
+
*/
|
|
229
|
+
export function calcPtPriceInAsset(currentSpotPrice: number, expirationTs: number) {
|
|
230
|
+
const impliedApy = currentSpotPrice - 1
|
|
231
|
+
const secondsRemaining = Math.max(0, expirationTs)
|
|
232
|
+
const SECONDS_PER_YEAR = 365 * 86400
|
|
233
|
+
const ptPriceInAsset = Math.exp(Math.log(1 + impliedApy) * (-secondsRemaining / SECONDS_PER_YEAR))
|
|
234
|
+
return ptPriceInAsset
|
|
235
|
+
}
|