@exponent-labs/market-three-math 0.9.18 → 0.9.19

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/src/swapV2.ts CHANGED
@@ -16,6 +16,112 @@ function computeFlashFeeOut(ytValue: number, grossOut: number, lpFeeRate: number
16
16
  return Math.min(grossOut, Math.max(1, getFeeFromAmount(ytValue, lpFeeRate)))
17
17
  }
18
18
 
19
+ function ceilNonNegative(value: number): number {
20
+ if (!Number.isFinite(value) || value <= 0) {
21
+ return 0
22
+ }
23
+
24
+ return Math.ceil(value)
25
+ }
26
+
27
+ function ytSyPriceFromSpot(currentPriceSpot: number, snapshot: EffSnap): number {
28
+ if (currentPriceSpot <= 0 || snapshot.syExchangeRate <= 0) {
29
+ return 0
30
+ }
31
+
32
+ const ptAssetPrice = Math.pow(currentPriceSpot, -snapshot.timeFactor)
33
+ if (!Number.isFinite(ptAssetPrice)) {
34
+ return 0
35
+ }
36
+
37
+ return Math.max(0, 1 - ptAssetPrice) / snapshot.syExchangeRate
38
+ }
39
+
40
+ function flashFeeBasisSy(ytAmount: number, ytSyPrice: number): number {
41
+ return ceilNonNegative(ytAmount * ytSyPrice)
42
+ }
43
+
44
+ function flashFeeBasisPt(ptOutGross: number, ytSyPrice: number, syExchangeRate: number): number {
45
+ return ceilNonNegative(ptOutGross * ytSyPrice * syExchangeRate)
46
+ }
47
+
48
+ function estimateExactOutGross(
49
+ targetNet: number,
50
+ maxGross: number,
51
+ lpFeeRate: number,
52
+ isFlashSwap: boolean,
53
+ ytSyPrice: number,
54
+ syExchangeRate: number,
55
+ ): number {
56
+ if (targetNet === 0 || maxGross === 0) {
57
+ return 0
58
+ }
59
+
60
+ const feeDelta = Math.max(0, lpFeeRate - 1)
61
+ const feeBasisPerGross = isFlashSwap ? Math.max(0, ytSyPrice * syExchangeRate) : 1
62
+ const netPerGross = 1 - feeDelta * feeBasisPerGross
63
+ if (!Number.isFinite(netPerGross) || netPerGross <= 0) {
64
+ return maxGross
65
+ }
66
+
67
+ const targetWithFeeFloor = isFlashSwap || feeDelta > 0 ? targetNet + 1 : targetNet
68
+ return Math.min(maxGross, ceilNonNegative(targetWithFeeFloor / netPerGross))
69
+ }
70
+
71
+ function solveExactOutGrossDirect(
72
+ targetNet: number,
73
+ minGross: number,
74
+ maxGross: number,
75
+ grossEstimate: number,
76
+ calculateNet: (gross: number) => number,
77
+ ): number {
78
+ if (targetNet === 0 || maxGross === 0) {
79
+ return 0
80
+ }
81
+
82
+ const correctionSteps = 64
83
+ let gross = Math.min(maxGross, Math.max(minGross, grossEstimate))
84
+ let net = calculateNet(gross)
85
+
86
+ for (let i = 0; i < correctionSteps; i++) {
87
+ if (net >= targetNet || gross >= maxGross) {
88
+ break
89
+ }
90
+ gross += 1
91
+ net = calculateNet(gross)
92
+ }
93
+
94
+ if (net < targetNet) {
95
+ let lo = gross
96
+ let hi = maxGross
97
+ if (calculateNet(hi) < targetNet) {
98
+ return maxGross
99
+ }
100
+ while (lo < hi) {
101
+ const mid = lo + Math.floor((hi - lo) / 2)
102
+ if (calculateNet(mid) >= targetNet) {
103
+ hi = mid
104
+ } else {
105
+ lo = mid + 1
106
+ }
107
+ }
108
+ gross = lo
109
+ }
110
+
111
+ for (let i = 0; i < correctionSteps; i++) {
112
+ if (gross === 0) {
113
+ break
114
+ }
115
+ const prevNet = calculateNet(gross - 1)
116
+ if (prevNet < targetNet) {
117
+ break
118
+ }
119
+ gross -= 1
120
+ }
121
+
122
+ return gross
123
+ }
124
+
19
125
  function boundaryPrincipalRemainder(
20
126
  computedGrossOut: number,
21
127
  fullOutgoingPrincipal: number,
@@ -214,6 +320,8 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
214
320
  continue
215
321
  }
216
322
 
323
+ const ytSyPrice = ytSyPriceFromSpot(currentPriceSpot, snapshot)
324
+
217
325
  if (args.direction === SwapDirection.PtToSy) {
218
326
  // PT -> SY swap
219
327
 
@@ -265,14 +373,10 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
265
373
  }
266
374
 
267
375
  // Fees in token_out (SY)
268
- // For flash swaps, fee is based on YT value = (pt - sy × sy_exchange_rate) / sy_exchange_rate (in SY terms)
269
376
  if (syOutGrossU64 > 0 || syRemainderAtBoundary > 0) {
270
377
  let totalFeeOut: number
271
378
  if (args.isCurrentFlashSwap) {
272
- const syOutBase = syOutGrossU64 * args.syExchangeRate
273
- const ytValueBase = ptInSegment - syOutBase
274
- const ytValueSy = ytValueBase / args.syExchangeRate
275
- totalFeeOut = getFeeFromAmount(Math.max(0, Math.floor(ytValueSy)), lpFeeRate)
379
+ totalFeeOut = computeFlashFeeOut(flashFeeBasisSy(ptInSegment, ytSyPrice), syOutGrossU64, lpFeeRate)
276
380
  } else {
277
381
  totalFeeOut = getFeeFromAmount(syOutGrossU64, lpFeeRate)
278
382
  }
@@ -393,12 +497,13 @@ export function simulateSwap(marketState: MarketThreeState, args: SwapArgs): Swa
393
497
 
394
498
  if (ptOutGrossU64 > 0 || ptRemainderAtBoundary > 0) {
395
499
  // Fees in token_out (PT)
396
- // For flash swaps, fee is based on YT value = pt - sy × sy_exchange_rate
397
500
  let totalFeeOut: number
398
501
  if (args.isCurrentFlashSwap) {
399
- const syInBase = syInSegmentU64 * args.syExchangeRate
400
- const ytValue = ptOutGrossU64 - syInBase
401
- totalFeeOut = computeFlashFeeOut(Math.max(0, Math.floor(ytValue)), ptOutGrossU64, lpFeeRate)
502
+ totalFeeOut = computeFlashFeeOut(
503
+ flashFeeBasisPt(ptOutGrossU64, ytSyPrice, args.syExchangeRate),
504
+ ptOutGrossU64,
505
+ lpFeeRate,
506
+ )
402
507
  } else {
403
508
  totalFeeOut = getFeeFromAmount(ptOutGrossU64, lpFeeRate)
404
509
  }
@@ -481,6 +586,7 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
481
586
  let currentLeftBoundaryKey = ticksWrapper.currentTickKey
482
587
  let activeLiquidityU64 = ticksWrapper.currentPrefixSum
483
588
  let activeLiquidityF64 = Number(activeLiquidityU64)
589
+ const ytSyPrice = ytSyPriceFromSpot(currentPriceSpot, snapshot)
484
590
 
485
591
  const lpFeeRate = calculateFeeRate(configurationOptions.lnFeeRateRoot, secondsRemaining)
486
592
  const protocolFeeBps = configurationOptions.treasuryFeeBps
@@ -517,7 +623,7 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
517
623
  let iterations = 0
518
624
  const MAX_ITERATIONS = 1000
519
625
 
520
- while (amountOutLeft > 2 && iterations < MAX_ITERATIONS) {
626
+ while (amountOutLeft > 0 && iterations < MAX_ITERATIONS) {
521
627
  iterations++
522
628
 
523
629
  const rightBoundaryKeyOpt = ticksWrapper.successorKey(currentLeftBoundaryKey)
@@ -671,6 +777,7 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
671
777
  cEffOld,
672
778
  lpFeeRate,
673
779
  args.isCurrentFlashSwap,
780
+ ytSyPrice,
674
781
  )
675
782
 
676
783
  const ptOutNetBoundaryActual = Math.max(0, boundarySegment.ptOutGrossActual - boundarySegment.totalFeeOut)
@@ -745,6 +852,7 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
745
852
  cEffOld,
746
853
  lpFeeRate,
747
854
  args.isCurrentFlashSwap,
855
+ ytSyPrice,
748
856
  )
749
857
  const ptOutNetBoundaryActual =
750
858
  ptOutNetBoundaryEffectiveSeed ?? Math.max(0, boundarySegment.ptOutGrossActual - boundarySegment.totalFeeOut)
@@ -755,20 +863,35 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
755
863
  }
756
864
 
757
865
  const targetNet = Math.min(amountOutLeft, ptOutNetBoundaryEffective)
866
+ const maxGross = ptOutGrossBoundaryU64
867
+ const minGross = Math.min(targetNet, maxGross)
868
+ const usedPrecomputedSegment = precomputedBoundarySegment !== null
758
869
  const grossCandidate = precomputedBoundarySegment
759
- ? ptOutGrossBoundaryU64
760
- : findGrossForTargetNet(targetNet, Math.min(targetNet, ptOutGrossBoundaryU64), ptOutGrossBoundaryU64, (gross) =>
761
- calculateNetPtOut(
762
- gross,
763
- lTradeF64,
764
- currentPriceSpot,
765
- duToLeft,
766
- anchorULeft,
870
+ ? maxGross
871
+ : solveExactOutGrossDirect(
872
+ targetNet,
873
+ minGross,
874
+ maxGross,
875
+ estimateExactOutGross(
876
+ targetNet,
877
+ maxGross,
767
878
  lpFeeRate,
768
879
  args.isCurrentFlashSwap,
769
- snapshot,
770
- cEffOld,
880
+ ytSyPrice,
881
+ snapshot.syExchangeRate,
771
882
  ),
883
+ (gross) =>
884
+ calculateNetPtOut(
885
+ gross,
886
+ lTradeF64,
887
+ currentPriceSpot,
888
+ duToLeft,
889
+ anchorULeft,
890
+ lpFeeRate,
891
+ args.isCurrentFlashSwap,
892
+ snapshot,
893
+ ytSyPrice,
894
+ ),
772
895
  )
773
896
 
774
897
  const segment =
@@ -783,6 +906,7 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
783
906
  cEffOld,
784
907
  lpFeeRate,
785
908
  args.isCurrentFlashSwap,
909
+ ytSyPrice,
786
910
  )
787
911
 
788
912
  let syInSegmentU64 = segment.syInSegmentU64
@@ -794,20 +918,41 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
794
918
  throw new Error("Insufficient SY budget for exact-out trade")
795
919
  }
796
920
 
797
- const netActual = segment.ptOutGrossActual - segment.totalFeeOut
798
- if (netActual < 0) {
921
+ const reachesLeftBoundary =
922
+ Math.abs(snapshot.getEffectivePrice(segment.spotPriceNew) - cEffLeft) <= eps ||
923
+ segment.ptOutGrossActual === ptOutGrossBoundaryU64
924
+ const rawNetActual = segment.ptOutGrossActual - segment.totalFeeOut
925
+ if (rawNetActual < 0) {
799
926
  throw new Error("Exact-out net underflow")
800
927
  }
801
- if (netActual > amountOutLeft) {
802
- throw new Error("Exact-out net exceeded remaining target")
928
+ if (rawNetActual === 0 && !reachesLeftBoundary) {
929
+ throw new Error("Exact-out segment produced no PT output")
930
+ }
931
+ if (rawNetActual < targetNet && !reachesLeftBoundary) {
932
+ throw new Error("Exact-out segment under-delivered target")
933
+ }
934
+ const netActual = Math.min(rawNetActual, targetNet)
935
+ const exactOutRoundingSurplus = rawNetActual - netActual
936
+ if (exactOutRoundingSurplus > 0 && !usedPrecomputedSegment && grossCandidate > 0) {
937
+ const prevNetActual = calculateNetPtOut(
938
+ grossCandidate - 1,
939
+ lTradeF64,
940
+ currentPriceSpot,
941
+ duToLeft,
942
+ anchorULeft,
943
+ lpFeeRate,
944
+ args.isCurrentFlashSwap,
945
+ snapshot,
946
+ ytSyPrice,
947
+ )
948
+ if (prevNetActual >= targetNet) {
949
+ throw new Error("Exact-out gross candidate is not minimal")
950
+ }
803
951
  }
804
952
  amountOutLeft -= netActual
805
953
  amountOutAccum += netActual
806
954
  amountInConsumed += syInSegmentU64
807
955
 
808
- const reachesLeftBoundary =
809
- Math.abs(snapshot.getEffectivePrice(segment.spotPriceNew) - cEffLeft) <= eps ||
810
- segment.ptOutGrossActual === ptOutGrossBoundaryU64
811
956
  const ptRemainderAtBoundary = boundaryPrincipalRemainder(
812
957
  segment.ptOutGrossActual,
813
958
  Number(principalPt),
@@ -818,7 +963,7 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
818
963
  ptRemainderAtBoundary,
819
964
  protocolFeeBps,
820
965
  )
821
- feeLpOutU64 += lpFeeOut
966
+ feeLpOutU64 += lpFeeOut + exactOutRoundingSurplus
822
967
  feeProtocolOutU64 += protocolFeeOut
823
968
  boundaryRemainderChargedOutToken += ptRemainderAtBoundary
824
969
 
@@ -832,7 +977,7 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
832
977
 
833
978
  const cEffNew = snapshot.getEffectivePrice(currentPriceSpot)
834
979
  if (
835
- amountOutLeft > 2 &&
980
+ amountOutLeft > 0 &&
836
981
  (Math.abs(cEffNew - cEffLeft) <= eps || segment.ptOutGrossActual === ptOutGrossBoundaryU64)
837
982
  ) {
838
983
  const crossed = crossOneBoundary(ticksWrapper, SwapDirection.SyToPt, currentLeftBoundaryKey, {
@@ -849,7 +994,7 @@ export function simulateSwapExactOut(marketState: MarketThreeState, args: SwapEx
849
994
  }
850
995
  }
851
996
 
852
- if (amountOutLeft > 2) {
997
+ if (amountOutLeft !== 0) {
853
998
  throw new Error("Insufficient PT output for exact-out trade")
854
999
  }
855
1000
 
@@ -905,120 +1050,6 @@ function computeKappaAndLTrade(
905
1050
  return lTradeF64
906
1051
  }
907
1052
 
908
- function findGrossForTargetNet(
909
- targetNet: number,
910
- minGross: number,
911
- maxGross: number,
912
- calculateNet: (gross: number) => number,
913
- ): number {
914
- const MAX_INTERPOLATION_STEPS = 6
915
- const MAX_LOCAL_ADJUST_STEPS = 24
916
- const MAX_BINARY_FALLBACK_STEPS = 12
917
-
918
- if (minGross >= maxGross) {
919
- return minGross
920
- }
921
-
922
- let left = minGross
923
- let leftNet = calculateNet(left)
924
- if (leftNet >= targetNet) {
925
- return left
926
- }
927
-
928
- let right = maxGross
929
- let rightNet = calculateNet(right)
930
- if (rightNet <= targetNet) {
931
- return right
932
- }
933
-
934
- for (let i = 0; i < MAX_INTERPOLATION_STEPS; i++) {
935
- if (right <= left + 1) {
936
- return left
937
- }
938
- if (rightNet <= leftNet) {
939
- break
940
- }
941
-
942
- const span = right - left
943
- const targetDelta = Math.max(0, targetNet - leftNet)
944
- const denominator = rightNet - leftNet
945
- if (denominator === 0) {
946
- break
947
- }
948
-
949
- const estimatedOffset = Number((BigInt(targetDelta) * BigInt(span)) / BigInt(denominator))
950
- let estimate = left + estimatedOffset
951
- if (estimate <= left) {
952
- estimate = left + 1
953
- }
954
- if (estimate >= right) {
955
- estimate = right - 1
956
- }
957
-
958
- const estimateNet = calculateNet(estimate)
959
- if (estimateNet === targetNet) {
960
- return estimate
961
- }
962
- if (estimateNet < targetNet) {
963
- left = estimate
964
- leftNet = estimateNet
965
- } else {
966
- right = estimate
967
- rightNet = estimateNet
968
- }
969
- }
970
-
971
- let best = left
972
- for (let i = 0; i < MAX_LOCAL_ADJUST_STEPS; i++) {
973
- if (best >= right) {
974
- break
975
- }
976
- const next = best + 1
977
- if (next > right) {
978
- break
979
- }
980
- const nextNet = calculateNet(next)
981
- if (nextNet > targetNet) {
982
- break
983
- }
984
- best = next
985
- if (nextNet === targetNet) {
986
- return best
987
- }
988
- }
989
-
990
- if (right <= best + 1) {
991
- return best
992
- }
993
-
994
- let low = best
995
- let high = right
996
- let bestGross = best
997
- let steps = 0
998
-
999
- while (low <= high && steps < MAX_BINARY_FALLBACK_STEPS) {
1000
- steps += 1
1001
- const mid = low + Math.floor((high - low) / 2)
1002
- const net = calculateNet(mid)
1003
-
1004
- if (net === targetNet) {
1005
- return mid
1006
- }
1007
-
1008
- if (net < targetNet) {
1009
- bestGross = mid
1010
- low = mid + 1
1011
- } else {
1012
- if (mid === 0) {
1013
- break
1014
- }
1015
- high = mid - 1
1016
- }
1017
- }
1018
-
1019
- return bestGross
1020
- }
1021
-
1022
1053
  function calculateNetPtOut(
1023
1054
  grossCandidate: number,
1024
1055
  lTradeF64: number,
@@ -1028,7 +1059,7 @@ function calculateNetPtOut(
1028
1059
  lpFeeRate: number,
1029
1060
  isCurrentFlashSwap: boolean,
1030
1061
  snapshot: EffSnap,
1031
- cEffOld: number,
1062
+ ytSyPrice: number,
1032
1063
  ): number {
1033
1064
  const duCandidate = grossCandidate / lTradeF64
1034
1065
  const duClamped = Math.min(duCandidate, duToLeft)
@@ -1040,13 +1071,11 @@ function calculateNetPtOut(
1040
1071
  const ptOutGrossActual = Math.max(0, Math.floor(lTradeF64 * (currentPriceSpot - spotPriceNew)))
1041
1072
 
1042
1073
  const totalFeeOut = isCurrentFlashSwap
1043
- ? (() => {
1044
- const cEffNew = snapshot.getEffectivePrice(spotPriceNew)
1045
- const syInSegmentF = lTradeF64 * (cEffNew - cEffOld)
1046
- const syInBase = Math.ceil(syInSegmentF) * snapshot.syExchangeRate
1047
- const ytValue = ptOutGrossActual - syInBase
1048
- return computeFlashFeeOut(Math.max(0, Math.floor(ytValue)), ptOutGrossActual, lpFeeRate)
1049
- })()
1074
+ ? computeFlashFeeOut(
1075
+ flashFeeBasisPt(ptOutGrossActual, ytSyPrice, snapshot.syExchangeRate),
1076
+ ptOutGrossActual,
1077
+ lpFeeRate,
1078
+ )
1050
1079
  : getFeeFromAmount(ptOutGrossActual, lpFeeRate)
1051
1080
 
1052
1081
  return Math.max(0, ptOutGrossActual - totalFeeOut)
@@ -1062,6 +1091,7 @@ function calculateSwapSegment(
1062
1091
  cEffOld: number,
1063
1092
  lpFeeRate: number,
1064
1093
  isCurrentFlashSwap: boolean,
1094
+ ytSyPrice: number,
1065
1095
  ): {
1066
1096
  spotPriceNew: number
1067
1097
  syInSegmentU64: number
@@ -1081,9 +1111,11 @@ function calculateSwapSegment(
1081
1111
  const ptOutGrossActual = Math.max(0, Math.floor(lTradeF64 * (currentPriceSpot - spotPriceNew)))
1082
1112
 
1083
1113
  const normalFee = getFeeFromAmount(ptOutGrossActual, lpFeeRate)
1084
- const syInBase = Math.ceil(syInSegmentF) * snapshot.syExchangeRate
1085
- const ytValue = ptOutGrossActual - syInBase
1086
- const flashFee = computeFlashFeeOut(Math.max(0, Math.floor(ytValue)), ptOutGrossActual, lpFeeRate)
1114
+ const flashFee = computeFlashFeeOut(
1115
+ flashFeeBasisPt(ptOutGrossActual, ytSyPrice, snapshot.syExchangeRate),
1116
+ ptOutGrossActual,
1117
+ lpFeeRate,
1118
+ )
1087
1119
  const totalFeeOut = isCurrentFlashSwap ? flashFee : normalFee
1088
1120
 
1089
1121
  return {
@@ -0,0 +1,119 @@
1
+ import type { Tick, Ticks } from "@exponent-labs/exponent-fetcher"
2
+
3
+ const SENTINEL_TICK_INDEX = 0
4
+
5
+ type TickTreeNode = {
6
+ left: number
7
+ right: number
8
+ parent: number
9
+ colorOrFree: number
10
+ }
11
+
12
+ type TreeTick = Tick & {
13
+ treeNode?: TickTreeNode
14
+ isInitialized?: boolean
15
+ }
16
+
17
+ const asTreeTick = (tick: Tick | null | undefined): TreeTick | null | undefined => tick as TreeTick | null | undefined
18
+
19
+ export const isLiveTick = (tick: Tick | null | undefined): tick is Tick => {
20
+ const treeTick = asTreeTick(tick)
21
+ if (!treeTick) {
22
+ return false
23
+ }
24
+ if (treeTick.isInitialized != null) {
25
+ return treeTick.isInitialized
26
+ }
27
+ if (treeTick.apyBasePoints > 0) {
28
+ return true
29
+ }
30
+
31
+ return (
32
+ treeTick.impliedRate > 0 ||
33
+ treeTick.liquidityGross !== 0n ||
34
+ treeTick.principalShareSupply !== 0n ||
35
+ treeTick.principalPt !== 0n ||
36
+ treeTick.principalSy !== 0n ||
37
+ treeTick.frozenLiquidity !== 0n
38
+ )
39
+ }
40
+
41
+ export const successorTickIdx = (ticks: Ticks, tickIdx: number): number | null => {
42
+ const tick = asTreeTick(ticks.ticksTree[tickIdx - 1])
43
+ if (!isLiveTick(tick)) {
44
+ return null
45
+ }
46
+
47
+ const node = tick.treeNode
48
+ if (node) {
49
+ let right = node.right
50
+ if (right !== SENTINEL_TICK_INDEX) {
51
+ while (asTreeTick(ticks.ticksTree[right - 1])?.treeNode?.left) {
52
+ right = asTreeTick(ticks.ticksTree[right - 1])!.treeNode!.left
53
+ }
54
+ return isLiveTick(ticks.ticksTree[right - 1]) ? right : null
55
+ }
56
+
57
+ let cursor = tickIdx
58
+ let parent = node.parent
59
+ while (parent !== SENTINEL_TICK_INDEX && cursor === asTreeTick(ticks.ticksTree[parent - 1])?.treeNode?.right) {
60
+ cursor = parent
61
+ parent = asTreeTick(ticks.ticksTree[parent - 1])?.treeNode?.parent ?? SENTINEL_TICK_INDEX
62
+ }
63
+
64
+ return parent !== SENTINEL_TICK_INDEX && isLiveTick(ticks.ticksTree[parent - 1]) ? parent : null
65
+ }
66
+
67
+ let best: { tickIdx: number; key: number } | null = null
68
+ for (let index = 0; index < ticks.ticksTree.length; index++) {
69
+ const candidate = ticks.ticksTree[index]
70
+ if (!isLiveTick(candidate) || candidate.apyBasePoints <= tick.apyBasePoints) {
71
+ continue
72
+ }
73
+ if (!best || candidate.apyBasePoints < best.key) {
74
+ best = { tickIdx: index + 1, key: candidate.apyBasePoints }
75
+ }
76
+ }
77
+
78
+ return best?.tickIdx ?? null
79
+ }
80
+
81
+ export const predecessorTickIdx = (ticks: Ticks, tickIdx: number): number | null => {
82
+ const tick = asTreeTick(ticks.ticksTree[tickIdx - 1])
83
+ if (!isLiveTick(tick)) {
84
+ return null
85
+ }
86
+
87
+ const node = tick.treeNode
88
+ if (node) {
89
+ let left = node.left
90
+ if (left !== SENTINEL_TICK_INDEX) {
91
+ while (asTreeTick(ticks.ticksTree[left - 1])?.treeNode?.right) {
92
+ left = asTreeTick(ticks.ticksTree[left - 1])!.treeNode!.right
93
+ }
94
+ return isLiveTick(ticks.ticksTree[left - 1]) ? left : null
95
+ }
96
+
97
+ let cursor = tickIdx
98
+ let parent = node.parent
99
+ while (parent !== SENTINEL_TICK_INDEX && cursor === asTreeTick(ticks.ticksTree[parent - 1])?.treeNode?.left) {
100
+ cursor = parent
101
+ parent = asTreeTick(ticks.ticksTree[parent - 1])?.treeNode?.parent ?? SENTINEL_TICK_INDEX
102
+ }
103
+
104
+ return parent !== SENTINEL_TICK_INDEX && isLiveTick(ticks.ticksTree[parent - 1]) ? parent : null
105
+ }
106
+
107
+ let best: { tickIdx: number; key: number } | null = null
108
+ for (let index = 0; index < ticks.ticksTree.length; index++) {
109
+ const candidate = ticks.ticksTree[index]
110
+ if (!isLiveTick(candidate) || candidate.apyBasePoints >= tick.apyBasePoints) {
111
+ continue
112
+ }
113
+ if (!best || candidate.apyBasePoints > best.key) {
114
+ best = { tickIdx: index + 1, key: candidate.apyBasePoints }
115
+ }
116
+ }
117
+
118
+ return best?.tickIdx ?? null
119
+ }
package/src/utils.ts CHANGED
@@ -4,6 +4,8 @@
4
4
  */
5
5
  import { Tick, Ticks } from "@exponent-labs/exponent-fetcher"
6
6
 
7
+ import { isLiveTick, predecessorTickIdx, successorTickIdx } from "./tickTree"
8
+
7
9
  const SECONDS_PER_YEAR = 365 * 24 * 60 * 60
8
10
  // const BASE_POINTS = 10000
9
11
 
@@ -117,7 +119,7 @@ export function getSuccessorTickKey(ticks: Ticks, currentTickKey: number): numbe
117
119
  let minKey: number | null = null
118
120
 
119
121
  for (const tick of ticks.ticksTree) {
120
- if (tick.apyBasePoints > currentTickKey) {
122
+ if (isLiveTick(tick) && tick.apyBasePoints > currentTickKey) {
121
123
  if (minKey === null || tick.apyBasePoints < minKey) {
122
124
  minKey = tick.apyBasePoints
123
125
  }
@@ -132,19 +134,7 @@ export function getSuccessorTickKey(ticks: Ticks, currentTickKey: number): numbe
132
134
  * This is almost equivalent to the Rust successor_idx method
133
135
  */
134
136
  export function getSuccessorTickIdxByIdx(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
137
+ return successorTickIdx(ticks, tickIdx)
148
138
  }
149
139
 
150
140
  /**
@@ -152,19 +142,7 @@ export function getSuccessorTickIdxByIdx(ticks: Ticks, tickIdx: number): number
152
142
  * This is the inverse of getSuccessorTickIdxByIdx - finds the tick with the largest apyBasePoints that is still less than the current tick
153
143
  */
154
144
  export function getPredecessorTickIdxByIdx(ticks: Ticks, tickIdx: number): number | null {
155
- //TODO Refactor to make it more CPU efficient
156
- const tick = ticks.ticksTree.at(tickIdx - 1) ?? null
157
-
158
- if (!tick) return null
159
-
160
- // Find ticks with apyBasePoints less than current tick
161
- const predecessorTicks = ticks.ticksTree
162
- .filter((t) => t.apyBasePoints < tick.apyBasePoints)
163
- .sort((a, b) => b.apyBasePoints - a.apyBasePoints) // Sort descending to get the largest that's still less
164
-
165
- const predecessorTick = predecessorTicks.at(0) ?? null
166
-
167
- return !!predecessorTick ? ticks.ticksTree.indexOf(predecessorTick) + 1 : null
145
+ return predecessorTickIdx(ticks, tickIdx)
168
146
  }
169
147
 
170
148
  /**
@@ -178,7 +156,7 @@ export function getPredecessorTickKey(ticks: Ticks, currentTickKey: number): num
178
156
  let maxKey: number | null = null
179
157
 
180
158
  for (const tick of ticks.ticksTree) {
181
- if (tick.apyBasePoints < currentTickKey) {
159
+ if (isLiveTick(tick) && tick.apyBasePoints < currentTickKey) {
182
160
  if (maxKey === null || tick.apyBasePoints > maxKey) {
183
161
  maxKey = tick.apyBasePoints
184
162
  }
package/src/utilsV2.ts CHANGED
@@ -5,6 +5,8 @@
5
5
  */
6
6
  import { Tick, Ticks } from "@exponent-labs/exponent-fetcher"
7
7
 
8
+ import { isLiveTick } from "./tickTree"
9
+
8
10
  const SECONDS_PER_YEAR = 365 * 24 * 60 * 60
9
11
  const TICK_KEY_BASE_POINTS = 1_000_000
10
12
  const PRECISE_NUMBER_DENOM = 1_000_000_000_000_000_000n
@@ -74,7 +76,7 @@ export class TicksWrapper {
74
76
 
75
77
  // Build map from apyBasePoints -> Tick
76
78
  for (const tick of this.baseTicksTree) {
77
- if (tick.apyBasePoints > 0) {
79
+ if (isLiveTick(tick)) {
78
80
  this.ticksByKey.set(tick.apyBasePoints, tick)
79
81
  }
80
82
  }
@@ -91,7 +93,7 @@ export class TicksWrapper {
91
93
  // In Rust, currentTick is stored as 1-based index into the ticks tree
92
94
  // We need to find the corresponding key (apyBasePoints)
93
95
  const currentTickFromArray = this.baseTicksTree[ticks.currentTick - 1]
94
- if (currentTickFromArray && currentTickFromArray.apyBasePoints > 0) {
96
+ if (isLiveTick(currentTickFromArray)) {
95
97
  this.currentTickKey = currentTickFromArray.apyBasePoints
96
98
  } else {
97
99
  // Fallback: find tick with spot price closest to currentSpotPrice