@@ -57,8 +57,8 @@ func OrderRequestToWire(req OrderRequest, meta map[string]AssetInfo, isSpot bool
5757 return OrderWire {
5858 Asset : assetId ,
5959 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 ),
6262 ReduceOnly : req .ReduceOnly ,
6363 OrderType : OrderTypeToWire (req .OrderType ),
6464 Cloid : req .Cloid ,
@@ -81,8 +81,8 @@ func ModifyOrderRequestToWire(req ModifyOrderRequest, meta map[string]AssetInfo,
8181 Order : OrderWire {
8282 Asset : assetId ,
8383 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 ),
8686 ReduceOnly : req .ReduceOnly ,
8787 OrderType : OrderTypeToWire (req .OrderType ),
8888 },
@@ -135,6 +135,90 @@ func FloatToWire(x float64, maxDecimals int, szDecimals int) string {
135135 return rounded
136136}
137137
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+
138222// To sign raw messages via EIP-712
139223func StructToMap (strct any ) (res map [string ]interface {}, err error ) {
140224 a , err := json .Marshal (strct )
@@ -144,38 +228,3 @@ func StructToMap(strct any) (res map[string]interface{}, err error) {
144228 json .Unmarshal (a , & res )
145229 return res , nil
146230}
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