-
Notifications
You must be signed in to change notification settings - Fork 0
/
util.go
139 lines (123 loc) · 6.53 KB
/
util.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
package keeper
import (
"errors"
"math/big"
"github.com/jinxprotocol/v4-chain/protocol/lib"
"github.com/jinxprotocol/v4-chain/protocol/testutil/constants"
"github.com/jinxprotocol/v4-chain/protocol/x/prices/types"
)
// getProposalPrice returns the proposed price update for the next block, which is either the smoothed price or the
// index price - whichever is closer to the current market price. In cases where the smoothed price and the index price
// are equidistant from the current market price, the smoothed price is chosen.
func getProposalPrice(smoothedPrice uint64, indexPrice uint64, marketPrice uint64) uint64 {
if lib.AbsDiffUint64(smoothedPrice, marketPrice) > lib.AbsDiffUint64(indexPrice, marketPrice) {
return indexPrice
}
return smoothedPrice
}
// isAboveRequiredMinPriceChange returns true if the new price meets the required min price change
// for the market. Otherwise, returns false.
func isAboveRequiredMinPriceChange(marketParamPrice types.MarketParamPrice, newPrice uint64) bool {
minChangeAmt := getMinPriceChangeAmountForMarket(marketParamPrice)
return lib.AbsDiffUint64(marketParamPrice.Price.Price, newPrice) >= minChangeAmt
}
// getMinPriceChangeAmountForMarket returns the amount of price change that is needed to trigger
// a price update in accordance with the min price change parts-per-million value. This method always rounds down,
// which slightly biases towards price updates.
func getMinPriceChangeAmountForMarket(marketParamPrice types.MarketParamPrice) uint64 {
bigPrice := new(big.Int).SetUint64(marketParamPrice.Price.Price)
// There's no need to multiply this by the market's exponent, because `Price` comparisons are
// done without the market's exponent.
bigMinChangeAmt := lib.BigIntMulPpm(bigPrice, marketParamPrice.Param.MinPriceChangePpm)
if !bigMinChangeAmt.IsUint64() {
// This means that the min change amount is greater than the max uint64. This can only
// happen if the `MinPriceChangePpm` > 1,000,000 and there's a validation when
// creating/modifying the `Market`.
panic(errors.New("getMinPriceChangeAmountForMarket: min price change amount is greater than max uint64 value"))
}
return bigMinChangeAmt.Uint64()
}
// PriceTuple labels and encapsulates the set of prices used for various price computations.
type PriceTuple struct {
OldPrice uint64
IndexPrice uint64
NewPrice uint64
}
// isTowardsIndexPrice returns true if the new price is between the current price and the index
// price, inclusive. Otherwise, it returns false.
func isTowardsIndexPrice(
priceTuple PriceTuple,
) bool {
return priceTuple.NewPrice <= lib.Max(priceTuple.OldPrice, priceTuple.IndexPrice) &&
priceTuple.NewPrice >= lib.Min(priceTuple.OldPrice, priceTuple.IndexPrice)
}
// isCrossingIndexPrice returns true if index price is between the current and the new price,
// noninclusive. Otherwise, returns false.
func isCrossingIndexPrice(
priceTuple PriceTuple,
) bool {
return isCrossingReferencePrice(priceTuple.OldPrice, priceTuple.IndexPrice, priceTuple.NewPrice)
}
// isCrossingOldPrice returns true if the old price is between the index price and the new
// price, noninclusive. Otherwise, returns false.
func isCrossingOldPrice(
priceTuple PriceTuple,
) bool {
return isCrossingReferencePrice(priceTuple.IndexPrice, priceTuple.OldPrice, priceTuple.NewPrice)
}
// isCrossingReferencePrice returns true if the reference price is between the base price and the
// test price, noninclusive. Otherwise, returns false.
func isCrossingReferencePrice(
basePrice uint64,
referencePrice uint64,
testPrice uint64,
) bool {
return referencePrice < lib.Max(basePrice, testPrice) && referencePrice > lib.Min(basePrice, testPrice)
}
// computeTickSizePpm calculates the tick_size of the currency at the current price, in ppm.
// We keep the tick_size multiplied by 10^6 to reduce divisions in our calculations and avoid rounding errors.
func computeTickSizePpm(oldPrice uint64, minPriceChangePpm uint32) *big.Int {
// tick_size = oldPrice * minPriceChangePpm / 1_000_000 ==>
// tick_size_ppm = oldPrice * minPriceChangePpm = tick_size * 1_000_000
return new(big.Int).Mul(
new(big.Int).SetUint64(oldPrice),
new(big.Int).SetUint64(uint64(minPriceChangePpm)))
}
// priceDeltaIsWithinOneTick returns true iff the price delta is within one tick, given the tick_size in ppm.
func priceDeltaIsWithinOneTick(priceDelta *big.Int, tickSizePpm *big.Int) bool {
// To compare if a price_delta > tick_size, let's multiply by 1_000_000 and compare against the
// tick size in ppm
priceDeltaPpm := new(big.Int).Mul(priceDelta, new(big.Int).SetUint64(constants.OneMillion))
return priceDeltaPpm.Cmp(tickSizePpm) <= 0
}
// newPriceMeetsSqrtCondition calculates the price acceptance condition when the new price crosses the index
// price and the price change from the current price to the index price, or old_ticks, is > 1 tick.
//
// Ticks are computed at the currency's current price.
//
// Under these conditions, price changes are valid when new_ticks <= sqrt(old_ticks)
func newPriceMeetsSqrtCondition(oldDelta *big.Int, newDelta *big.Int, tickSizePpm *big.Int) bool {
// In order to avoid division / sqrt, which is potentially lossy, use big.Ints and refactor:
// given that new_ticks = new_delta / tick_size, old_ticks = old_delta / tick_size
// new_ticks < sqrt(old_ticks) ==> sub in old_ticks, new_ticks
// new_delta / tick_size <= sqrt(old_delta / tick_size) ==>
// new_delta * new_delta / tick_size <= old_delta ==>
// new_delta * new_delta <= old_delta * tick_size ==>
// new_delta * new_delta * 1_000_000 <= old_delta * tickSizePpm
newDeltaSquaredPpm := new(big.Int).Mul(newDelta, newDelta)
newDeltaSquaredPpm.Mul(newDeltaSquaredPpm, new(big.Int).SetUint64(constants.OneMillion))
oldDeltaTimesTickSizePpm := new(big.Int).Mul(oldDelta, tickSizePpm)
return newDeltaSquaredPpm.Cmp(oldDeltaTimesTickSizePpm) <= 0
}
// maximumAllowedPriceDelta computes the maximum allowable value of new_delta under the conditions
// that the proposed price is crossing in the index price, and old_ticks > 1. This method uses potentially
// lossy arithmetic and is only for logging purposes.
func maximumAllowedPriceDelta(oldDelta *big.Int, tickSizePpm *big.Int) *big.Int {
// Compute maximum allowable new_delta, or price difference between the index price
// and the proposed price:
// max_allowed = sqrt(old_delta * tick_size_ppm / 1_000_000)
maxAllowed := new(big.Int).Mul(oldDelta, tickSizePpm)
maxAllowed.Div(maxAllowed, new(big.Int).SetUint64(constants.OneMillion))
maxAllowed.Sqrt(maxAllowed)
return maxAllowed
}