@@ -57,8 +57,8 @@ func OrderRequestToWire(req OrderRequest, meta map[string]AssetInfo, isSpot bool
57
57
return OrderWire {
58
58
Asset : assetId ,
59
59
IsBuy : req .IsBuy ,
60
- LimitPx : RoundOrderPrice (req .LimitPx , info .SzDecimals , maxDecimals ),
61
- SizePx : RoundOrderSize (req .Sz , info .SzDecimals ),
60
+ LimitPx : PriceToWire (req .LimitPx , maxDecimals , info .SzDecimals ),
61
+ SizePx : SizeToWire (req .Sz , info .SzDecimals ),
62
62
ReduceOnly : req .ReduceOnly ,
63
63
OrderType : OrderTypeToWire (req .OrderType ),
64
64
Cloid : req .Cloid ,
@@ -81,8 +81,8 @@ func ModifyOrderRequestToWire(req ModifyOrderRequest, meta map[string]AssetInfo,
81
81
Order : OrderWire {
82
82
Asset : assetId ,
83
83
IsBuy : req .IsBuy ,
84
- LimitPx : RoundOrderPrice (req .LimitPx , info .SzDecimals , maxDecimals ),
85
- SizePx : RoundOrderSize (req .Sz , info .SzDecimals ),
84
+ LimitPx : PriceToWire (req .LimitPx , maxDecimals , info .SzDecimals ),
85
+ SizePx : SizeToWire (req .Sz , info .SzDecimals ),
86
86
ReduceOnly : req .ReduceOnly ,
87
87
OrderType : OrderTypeToWire (req .OrderType ),
88
88
},
@@ -135,6 +135,90 @@ func FloatToWire(x float64, maxDecimals int, szDecimals int) string {
135
135
return rounded
136
136
}
137
137
138
+ // fastPow10 returns 10^exp as a float64. For our purposes exp is small.
139
+ func pow10 (exp int ) float64 {
140
+ var res float64 = 1
141
+ for i := 0 ; i < exp ; i ++ {
142
+ res *= 10
143
+ }
144
+ return res
145
+ }
146
+
147
+ // PriceToWire converts a price value to its string representation per Hyperliquid rules.
148
+ // It enforces:
149
+ // - At most 5 significant figures,
150
+ // - And no more than (maxDecimals - szDecimals) decimal places.
151
+ //
152
+ // Integer prices are returned as is.
153
+ func PriceToWire (x float64 , maxDecimals , szDecimals int ) string {
154
+ // If the price is an integer, return it without decimals.
155
+ if x == math .Trunc (x ) {
156
+ return strconv .FormatInt (int64 (x ), 10 )
157
+ }
158
+
159
+ // Rule 1: The tick rule – maximum decimals allowed is (maxDecimals - szDecimals).
160
+ allowedTick := maxDecimals - szDecimals
161
+
162
+ // Rule 2: The significant figures rule – at most 5 significant digits.
163
+ var allowedSig int
164
+ if x >= 1 {
165
+ // Count digits in the integer part.
166
+ digits := int (math .Floor (math .Log10 (x ))) + 1
167
+ allowedSig = 5 - digits
168
+ if allowedSig < 0 {
169
+ allowedSig = 0
170
+ }
171
+ } else {
172
+ // For x < 1, determine the effective exponent.
173
+ exponent := int (math .Ceil (- math .Log10 (x )))
174
+ allowedSig = 4 + exponent
175
+ }
176
+
177
+ // Final allowed decimals is the minimum of the tick rule and the significant figures rule.
178
+ allowedDecimals := allowedTick
179
+ if allowedSig < allowedDecimals {
180
+ allowedDecimals = allowedSig
181
+ }
182
+ if allowedDecimals < 0 {
183
+ allowedDecimals = 0
184
+ }
185
+
186
+ // Round the price to allowedDecimals decimals.
187
+ factor := pow10 (allowedDecimals )
188
+ rounded := math .Round (x * factor ) / factor
189
+
190
+ // Format the number with fixed precision.
191
+ s := strconv .FormatFloat (rounded , 'f' , allowedDecimals , 64 )
192
+ // Only trim trailing zeros if the formatted string contains a decimal point.
193
+ if strings .Contains (s , "." ) {
194
+ s = strings .TrimRight (s , "0" )
195
+ s = strings .TrimRight (s , "." )
196
+ }
197
+ return s
198
+ }
199
+
200
+ // SizeToWire converts a size value to its string representation,
201
+ // rounding it to exactly szDecimals decimals.
202
+ // Integer sizes are returned without decimals.
203
+ func SizeToWire (x float64 , szDecimals int ) string {
204
+ // Return integer sizes without decimals.
205
+ if szDecimals == 0 {
206
+ return strconv .FormatInt (int64 (x ), 10 )
207
+ }
208
+ // Return integer sizes directly.
209
+ if x == math .Trunc (x ) {
210
+ return strconv .FormatInt (int64 (x ), 10 )
211
+ }
212
+
213
+ // Round the size value to szDecimals decimals.
214
+ factor := pow10 (szDecimals )
215
+ rounded := math .Round (x * factor ) / factor
216
+
217
+ // Format with fixed precision then trim any trailing zeros and the decimal point.
218
+ s := strconv .FormatFloat (rounded , 'f' , szDecimals , 64 )
219
+ return strings .TrimRight (strings .TrimRight (s , "0" ), "." )
220
+ }
221
+
138
222
// To sign raw messages via EIP-712
139
223
func StructToMap (strct any ) (res map [string ]interface {}, err error ) {
140
224
a , err := json .Marshal (strct )
@@ -144,38 +228,3 @@ func StructToMap(strct any) (res map[string]interface{}, err error) {
144
228
json .Unmarshal (a , & res )
145
229
return res , nil
146
230
}
147
-
148
- // Round the order size to the nearest tick size
149
- func RoundOrderSize (x float64 , szDecimals int ) string {
150
- newX := math .Round (x * math .Pow10 (szDecimals )) / math .Pow10 (szDecimals )
151
- // TODO: add rounding
152
- return big .NewFloat (newX ).Text ('f' , szDecimals )
153
- }
154
-
155
- // Round the order price to the nearest tick size
156
- func RoundOrderPrice (x float64 , szDecimals int , maxDecimals int ) string {
157
- maxSignFigures := 5
158
- allowedDecimals := maxDecimals - szDecimals
159
- numberOfDigitsInIntegerPart := len (strconv .Itoa (int (x )))
160
- if numberOfDigitsInIntegerPart >= maxSignFigures {
161
- return RoundOrderSize (x , 0 )
162
- }
163
- allowedSignFigures := maxSignFigures - numberOfDigitsInIntegerPart
164
- if x < 1 {
165
- text := RoundOrderSize (x , allowedDecimals )
166
- startSignFigures := false
167
- for i := 2 ; i < len (text ); i ++ {
168
- if text [i ] == '0' && ! startSignFigures {
169
- continue
170
- }
171
- startSignFigures = true
172
- allowedSignFigures --
173
- if allowedSignFigures == 0 {
174
- return text [:i + 1 ]
175
- }
176
- }
177
- return text
178
- } else {
179
- return RoundOrderSize (x , min (allowedSignFigures , allowedDecimals ))
180
- }
181
- }
0 commit comments