@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +197 -0
  3. package/build/addLiquidity.d.ts +67 -0
  4. package/build/addLiquidity.js +269 -0
  5. package/build/addLiquidity.js.map +1 -0
  6. package/build/bisect.d.ts +1 -0
  7. package/build/bisect.js +62 -0
  8. package/build/bisect.js.map +1 -0
  9. package/build/index.d.ts +24 -0
  10. package/build/index.js +76 -0
  11. package/build/index.js.map +1 -0
  12. package/build/liquidityHistogram.d.ts +50 -0
  13. package/build/liquidityHistogram.js +162 -0
  14. package/build/liquidityHistogram.js.map +1 -0
  15. package/build/quote.d.ts +18 -0
  16. package/build/quote.js +106 -0
  17. package/build/quote.js.map +1 -0
  18. package/build/swap-v2.d.ts +20 -0
  19. package/build/swap-v2.js +261 -0
  20. package/build/swap-v2.js.map +1 -0
  21. package/build/swap.d.ts +15 -0
  22. package/build/swap.js +249 -0
  23. package/build/swap.js.map +1 -0
  24. package/build/swapLegacy.d.ts +16 -0
  25. package/build/swapLegacy.js +229 -0
  26. package/build/swapLegacy.js.map +1 -0
  27. package/build/swapV2.d.ts +11 -0
  28. package/build/swapV2.js +406 -0
  29. package/build/swapV2.js.map +1 -0
  30. package/build/types.d.ts +73 -0
  31. package/build/types.js +9 -0
  32. package/build/types.js.map +1 -0
  33. package/build/utils.d.ts +119 -0
  34. package/build/utils.js +219 -0
  35. package/build/utils.js.map +1 -0
  36. package/build/utilsV2.d.ts +88 -0
  37. package/build/utilsV2.js +180 -0
  38. package/build/utilsV2.js.map +1 -0
  39. package/build/withdrawLiquidity.d.ts +8 -0
  40. package/build/withdrawLiquidity.js +174 -0
  41. package/build/withdrawLiquidity.js.map +1 -0
  42. package/build/ytTrades.d.ts +106 -0
  43. package/build/ytTrades.js +292 -0
  44. package/build/ytTrades.js.map +1 -0
  45. package/build/ytTradesLegacy.d.ts +106 -0
  46. package/build/ytTradesLegacy.js +292 -0
  47. package/build/ytTradesLegacy.js.map +1 -0
  48. package/examples/.env.example +1 -0
  49. package/examples/test-histogram-simple.ts +172 -0
  50. package/examples/test-histogram.ts +112 -0
  51. package/package.json +26 -0
  52. package/src/addLiquidity.ts +384 -0
  53. package/src/bisect.ts +72 -0
  54. package/src/index.ts +74 -0
  55. package/src/liquidityHistogram.ts +192 -0
  56. package/src/quote.ts +128 -0
  57. package/src/swap.ts +299 -0
  58. package/src/swapLegacy.ts +272 -0
  59. package/src/types.ts +80 -0
  60. package/src/utils.ts +235 -0
  61. package/src/withdrawLiquidity.ts +240 -0
  62. package/src/ytTrades.ts +419 -0
  63. 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
+ }