Skip to content

Commit fb48d91

Browse files
nkitlabsRogerKSI
andauthored
[TSS] update TickToPrice function (#169)
* fix PriceToTick and add TickToPriceX10E9 * add random test * fix comments and calcualte tick at small price --------- Co-authored-by: Kitipong Sirirueangsakul <[email protected]>
1 parent a4f0da8 commit fb48d91

File tree

2 files changed

+398
-57
lines changed

2 files changed

+398
-57
lines changed

x/feeds/types/price.go

Lines changed: 173 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,32 @@
11
package types
22

33
import (
4+
"errors"
45
"fmt"
5-
"math"
6+
"math/big"
67
)
78

89
const (
9-
TICK_SIZE float64 = 1.0001 // Equivalent to 10^(-4)
10-
MAX_PRICE float64 = 2.421902e11 // Equivalent to 1.0001 ** ((2**19) - 1 - (2**18))
11-
MIN_PRICE float64 = 4.128986e-12 // Equivalent to 1.0001 ** (1 - (2**18))
12-
OFFSET float64 = 262144 // Equivalent to 2**18
13-
BILLION uint64 = 1e9 // Equivalent to 10^9
10+
MaxTick int64 = 262143 // Equivalent to 2**18 - 1
11+
MinTick int64 = -MaxTick // Equivalent to -2**18 + 1
12+
Offset int64 = 262144 // Equivalent to 2**18
13+
)
14+
15+
var (
16+
priceX96AtBinaryTicks = getPricesX96AtBinaryTicks()
17+
18+
maxUint192, _ = new(big.Int).SetString("ffffffffffffffffffffffffffffffffffffffffffffffff", 16)
19+
maxUint96, _ = new(big.Int).SetString("ffffffffffffffffffffffff", 16)
20+
maxUint64, _ = new(big.Int).SetString("ffffffffffffffff", 16)
21+
q96, _ = new(big.Int).SetString("1000000000000000000000000", 16)
22+
zero = new(big.Int).SetUint64(0)
23+
one = new(big.Int).SetUint64(1)
24+
billion = new(big.Int).SetUint64(1000000000)
1425
)
1526

1627
// ToTick converts the price to tick
1728
func (p *Price) ToTick() error {
18-
price, err := PriceToTick(ConvertToRealPrice(p.Price))
29+
price, err := PriceToTick(p.Price)
1930
if err != nil {
2031
return err
2132
}
@@ -24,24 +35,165 @@ func (p *Price) ToTick() error {
2435
return nil
2536
}
2637

27-
// ConvertToRealPrice converts the price multiplied by 1e9 to real price
28-
func ConvertToRealPrice(price uint64) float64 {
29-
realPrice := float64(price) / float64(BILLION)
30-
return realPrice
31-
}
38+
// TickToPrice converts the tick to price with 10^9 precision. It will return an error
39+
// if the tick is out of range or the tick is so large that cannot be converted to uint64.
40+
// NOTE: the result is rounded up to the nearest integer, this is aligned with the UniswapV3 calculation.
41+
func TickToPrice(tick int64) (uint64, error) {
42+
priceX96, err := tickToPriceX96(tick)
43+
if err != nil {
44+
return 0, err
45+
}
3246

33-
// PriceToTick converts the price to tick
34-
func PriceToTick(price float64) (uint64, error) {
35-
// Check if price is less than or equal to zero to prevent NaN results
36-
if price <= 0 {
37-
return 0, fmt.Errorf("price must be greater than 0")
47+
// round up the price and convert to uint64
48+
// we round up in the division so PriceX1E9ToTick of the output price is always consistent
49+
// var price *big.Int
50+
price := new(big.Int).Div(priceX96, q96)
51+
if price.Cmp(zero) <= 0 {
52+
return 0, fmt.Errorf("price out of range")
3853
}
3954

40-
// For safely convert from i64 to u64 since the price is already checked
41-
// to ensure `tick` is always a positive value
42-
if price < MIN_PRICE || price > MAX_PRICE {
55+
if new(big.Int).Rem(priceX96, q96).Cmp(zero) > 0 {
56+
priceNextTickX96 := new(big.Int).Div(new(big.Int).Mul(priceX96, big.NewInt(10001)), big.NewInt(10000))
57+
priceNextTick := new(big.Int).Div(priceNextTickX96, q96)
58+
59+
if priceNextTick.Cmp(price) > 0 {
60+
price = new(big.Int).Add(price, one)
61+
}
62+
}
63+
64+
if price.Cmp(maxUint64) > 0 {
4365
return 0, fmt.Errorf("price out of range")
4466
}
67+
return price.Uint64(), nil
68+
}
69+
70+
// PriceToTick converts the price to tick, it will return the nearest tick that yields
71+
// less than or equal to the given price.
72+
// ref: https://en.wikipedia.org/wiki/Binary_logarithm#Iterative_approximation
73+
func PriceToTick(price uint64) (uint64, error) {
74+
if price == 0 {
75+
return 0, errors.New("price must be greater than 0")
76+
}
77+
78+
// find the most significant bit (msb) of the log2(price);
79+
msb := uint64(0)
80+
p := price
81+
bits := []uint64{4294967295, 65535, 255, 15, 3, 1}
82+
for i, bit := range bits {
83+
if p > bit {
84+
n := uint64(1 << (len(bits) - i - 1))
85+
msb += n
86+
p >>= n
87+
}
88+
}
89+
90+
// find the remaining r = price / 2^msb and shift significant bits to 2^31;
91+
r := uint64(0)
92+
if msb >= 32 {
93+
r = price >> (msb - 31)
94+
} else {
95+
r = price << (31 - msb)
96+
}
97+
98+
// approximate log2(r) using iterations of base-2 logarithm with 16-bit precision;
99+
log2 := int64(msb) << 16
100+
for i := 0; i < 16; i++ {
101+
r = (r * r) >> 31
102+
f := r >> 32
103+
log2 |= int64(f) << (15 - i)
104+
r >>= f
105+
}
106+
107+
// convert to tick value;
108+
// tick = (log2 - log2(10^9) *2^16) * (1/log2(1.0001))/(2^16/2^32)
109+
log1p0001 := (log2 - 1959352) * 454283648
110+
tick := log1p0001 >> 32
111+
if tick > MaxTick || tick < MinTick {
112+
return 0, fmt.Errorf("tick out of range")
113+
}
114+
115+
// the result will differ by 1 tick if the price is not exactly at the tick value;
116+
// it will return the largest tick whose price are less than or equal to the given price.
117+
// NOTE: cannot use the previous result divided by 1.0001 as the fraction has been reduced.
118+
expectPriceX96 := new(big.Int).Mul(new(big.Int).SetUint64(price), q96)
119+
for i := int64(1); i >= 0; i-- {
120+
t := tick + i
121+
pX96, err := tickToPriceX96(t)
122+
if err == nil && pX96.Cmp(expectPriceX96) <= 0 {
123+
return uint64(t + Offset), nil
124+
}
125+
}
126+
127+
return uint64(tick - 1 + Offset), nil
128+
}
129+
130+
// mulShift multiplies two big.Int and shifts the result to the right by 96 bits.
131+
// It returns a new big.Int object.
132+
func mulShift(val *big.Int, mulBy *big.Int) *big.Int {
133+
return new(big.Int).Rsh(new(big.Int).Mul(val, mulBy), 96)
134+
}
135+
136+
// tickToPriceX96 converts the tick to price in x96 (2^96) * 10^9 format.
137+
func tickToPriceX96(tick int64) (*big.Int, error) {
138+
if tick > MaxTick || tick < MinTick {
139+
return nil, fmt.Errorf("tick out of range")
140+
}
141+
142+
absTick := tick
143+
if tick < 0 {
144+
absTick = -tick
145+
}
146+
147+
// multiply the price ratio at each binary tick
148+
priceX96 := new(big.Int).Set(q96)
149+
for i, pX96 := range priceX96AtBinaryTicks {
150+
if absTick&(1<<uint(i)) != 0 {
151+
priceX96 = mulShift(priceX96, pX96)
152+
}
153+
}
154+
155+
// inverse the price if tick is positive.
156+
if tick > 0 {
157+
priceX96 = new(big.Int).Div(maxUint192, priceX96)
158+
}
159+
160+
priceX96 = new(big.Int).Mul(priceX96, billion)
161+
162+
return priceX96, nil
163+
}
164+
165+
// getPricesX96AtBinaryTicks returns the prices at each binary tick in x96 format.
166+
// the prices are in the term of 1.0001^-(2^i) * 2^96.
167+
func getPricesX96AtBinaryTicks() []*big.Int {
168+
x96Hexes := []string{
169+
"fff97272373d413259a46990",
170+
"fff2e50f5f656932ef12357c",
171+
"ffe5caca7e10e4e61c3624ea",
172+
"ffcb9843d60f6159c9db5883",
173+
"ff973b41fa98c081472e6896",
174+
"ff2ea16466c96a3843ec78b3",
175+
"fe5dee046a99a2a811c461f1",
176+
"fcbe86c7900a88aedcffc83b",
177+
"f987a7253ac413176f2b074c",
178+
"f3392b0822b70005940c7a39",
179+
"e7159475a2c29b7443b29c7f",
180+
"d097f3bdfd2022b8845ad8f7",
181+
"a9f746462d870fdf8a65dc1f",
182+
"70d869a156d2a1b890bb3df6",
183+
"31be135f97d08fd981231505",
184+
"9aa508b5b7a84e1c677de54",
185+
"5d6af8dedb81196699c329",
186+
"2216e584f5fa1ea92604",
187+
}
188+
189+
prices := make([]*big.Int, 0, len(x96Hexes))
190+
for _, x96Hex := range x96Hexes {
191+
p, ok := new(big.Int).SetString(x96Hex, 16)
192+
if !ok {
193+
panic("failed to parse hex string")
194+
}
195+
prices = append(prices, p)
196+
}
45197

46-
return uint64(math.Round(math.Log(price)/math.Log(TICK_SIZE)) + OFFSET), nil
198+
return prices
47199
}

0 commit comments

Comments
 (0)