Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions hyperliquid/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ func (client *Client) KeyManager() *PKeyManager {
// getAPIURL returns the API URL based on the network type.
func getURL(isMainnet bool) string {
if isMainnet {
return "https://api.hyperliquid.xyz"
return MAINNET_API_URL
} else {
return "https://api.hyperliquid-testnet.xyz"
return TESTNET_API_URL
}
}

Expand All @@ -65,6 +65,7 @@ func NewClient(isMainnet bool) *Client {
PadLevelText: true,
})
logger.SetOutput(os.Stdout)
logger.SetLevel(log.DebugLevel)
return &Client{
baseUrl: getURL(isMainnet),
httpClient: http.DefaultClient,
Expand All @@ -80,7 +81,7 @@ func NewClient(isMainnet bool) *Client {
// debug prints the debug messages.
func (client *Client) debug(format string, v ...interface{}) {
if client.Debug {
client.Logger.Printf(format, v...)
client.Logger.Debugf(format, v...)
}
}

Expand Down
6 changes: 5 additions & 1 deletion hyperliquid/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ package hyperliquid

const GLOBAL_DEBUG = false // Default debug that is used in all tests

// API constants
const MAINNET_API_URL = "https://api.hyperliquid.xyz"
const TESTNET_API_URL = "https://api.hyperliquid-testnet.xyz"

// Execution constants
const DEFAULT_SLIPPAGE = 0.005 // 0.5% default slippage
const SPOT_MAX_DECIMALS = 8 // Default decimals for spot
const PERP_MAX_DECIMALS = 6 // Default decimals for perp
var SZ_DECIMALS = 2 // Default decimals for usdc
var USDC_SZ_DECIMALS = 2 // Default decimals for usdc that is used for withdraw

// Signing constants
const HYPERLIQUID_CHAIN_ID = 1337
Expand Down
127 changes: 88 additions & 39 deletions hyperliquid/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ func OrderRequestToWire(req OrderRequest, meta map[string]AssetInfo, isSpot bool
return OrderWire{
Asset: assetId,
IsBuy: req.IsBuy,
LimitPx: RoundOrderPrice(req.LimitPx, info.SzDecimals, maxDecimals),
SizePx: RoundOrderSize(req.Sz, info.SzDecimals),
LimitPx: PriceToWire(req.LimitPx, maxDecimals, info.SzDecimals),
SizePx: SizeToWire(req.Sz, info.SzDecimals),
ReduceOnly: req.ReduceOnly,
OrderType: OrderTypeToWire(req.OrderType),
Cloid: req.Cloid,
Expand All @@ -81,8 +81,8 @@ func ModifyOrderRequestToWire(req ModifyOrderRequest, meta map[string]AssetInfo,
Order: OrderWire{
Asset: assetId,
IsBuy: req.IsBuy,
LimitPx: RoundOrderPrice(req.LimitPx, info.SzDecimals, maxDecimals),
SizePx: RoundOrderSize(req.Sz, info.SzDecimals),
LimitPx: PriceToWire(req.LimitPx, maxDecimals, info.SzDecimals),
SizePx: SizeToWire(req.Sz, info.SzDecimals),
ReduceOnly: req.ReduceOnly,
OrderType: OrderTypeToWire(req.OrderType),
},
Expand Down Expand Up @@ -135,6 +135,90 @@ func FloatToWire(x float64, maxDecimals int, szDecimals int) string {
return rounded
}

// fastPow10 returns 10^exp as a float64. For our purposes exp is small.
func pow10(exp int) float64 {
var res float64 = 1
for i := 0; i < exp; i++ {
res *= 10
}
return res
}

// PriceToWire converts a price value to its string representation per Hyperliquid rules.
// It enforces:
// - At most 5 significant figures,
// - And no more than (maxDecimals - szDecimals) decimal places.
//
// Integer prices are returned as is.
func PriceToWire(x float64, maxDecimals, szDecimals int) string {
// If the price is an integer, return it without decimals.
if x == math.Trunc(x) {
return strconv.FormatInt(int64(x), 10)
}

// Rule 1: The tick rule – maximum decimals allowed is (maxDecimals - szDecimals).
allowedTick := maxDecimals - szDecimals

// Rule 2: The significant figures rule – at most 5 significant digits.
var allowedSig int
if x >= 1 {
// Count digits in the integer part.
digits := int(math.Floor(math.Log10(x))) + 1
allowedSig = 5 - digits
if allowedSig < 0 {
allowedSig = 0
}
} else {
// For x < 1, determine the effective exponent.
exponent := int(math.Ceil(-math.Log10(x)))
allowedSig = 4 + exponent
}

// Final allowed decimals is the minimum of the tick rule and the significant figures rule.
allowedDecimals := allowedTick
if allowedSig < allowedDecimals {
allowedDecimals = allowedSig
}
if allowedDecimals < 0 {
allowedDecimals = 0
}

// Round the price to allowedDecimals decimals.
factor := pow10(allowedDecimals)
rounded := math.Round(x*factor) / factor

// Format the number with fixed precision.
s := strconv.FormatFloat(rounded, 'f', allowedDecimals, 64)
// Only trim trailing zeros if the formatted string contains a decimal point.
if strings.Contains(s, ".") {
s = strings.TrimRight(s, "0")
s = strings.TrimRight(s, ".")
}
return s
}

// SizeToWire converts a size value to its string representation,
// rounding it to exactly szDecimals decimals.
// Integer sizes are returned without decimals.
func SizeToWire(x float64, szDecimals int) string {
// Return integer sizes without decimals.
if szDecimals == 0 {
return strconv.FormatInt(int64(x), 10)
}
// Return integer sizes directly.
if x == math.Trunc(x) {
return strconv.FormatInt(int64(x), 10)
}

// Round the size value to szDecimals decimals.
factor := pow10(szDecimals)
rounded := math.Round(x*factor) / factor

// Format with fixed precision then trim any trailing zeros and the decimal point.
s := strconv.FormatFloat(rounded, 'f', szDecimals, 64)
return strings.TrimRight(strings.TrimRight(s, "0"), ".")
}

// To sign raw messages via EIP-712
func StructToMap(strct any) (res map[string]interface{}, err error) {
a, err := json.Marshal(strct)
Expand All @@ -144,38 +228,3 @@ func StructToMap(strct any) (res map[string]interface{}, err error) {
json.Unmarshal(a, &res)
return res, nil
}

// Round the order size to the nearest tick size
func RoundOrderSize(x float64, szDecimals int) string {
newX := math.Round(x*math.Pow10(szDecimals)) / math.Pow10(szDecimals)
// TODO: add rounding
return big.NewFloat(newX).Text('f', szDecimals)
}

// Round the order price to the nearest tick size
func RoundOrderPrice(x float64, szDecimals int, maxDecimals int) string {
maxSignFigures := 5
allowedDecimals := maxDecimals - szDecimals
numberOfDigitsInIntegerPart := len(strconv.Itoa(int(x)))
if numberOfDigitsInIntegerPart >= maxSignFigures {
return RoundOrderSize(x, 0)
}
allowedSignFigures := maxSignFigures - numberOfDigitsInIntegerPart
if x < 1 {
text := RoundOrderSize(x, allowedDecimals)
startSignFigures := false
for i := 2; i < len(text); i++ {
if text[i] == '0' && !startSignFigures {
continue
}
startSignFigures = true
allowedSignFigures--
if allowedSignFigures == 0 {
return text[:i+1]
}
}
return text
} else {
return RoundOrderSize(x, min(allowedSignFigures, allowedDecimals))
}
}
111 changes: 111 additions & 0 deletions hyperliquid/convert_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package hyperliquid

import (
"testing"
)

func TestConvert_SizeToWire(t *testing.T) {
testCases := []struct {
name string
input float64
szDec int
expected string
}{
{
name: "BTC Size",
input: 0.1,
szDec: 5,
expected: "0.1",
},
{
name: "PNUT Size",
input: 101.22,
szDec: 1,
expected: "101.2",
},
{
name: "ETH Size",
input: 0.1,
szDec: 4,
expected: "0.1",
},
{
name: "ADA Size",
input: 100.123456,
szDec: 0,
expected: "100",
},
{
name: "ETH Size",
input: 1.0,
szDec: 4,
expected: "1",
},
{
name: "ETH Size",
input: 10.0,
szDec: 4,
expected: "10",
},
{
name: "ETH Size",
input: 0.0100,
szDec: 4,
expected: "0.01",
},
{
name: "ETH Size",
input: 0.010000001,
szDec: 4,
expected: "0.01",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
res := SizeToWire(tc.input, tc.szDec)
if res != tc.expected {
t.Errorf("SizeToWire() = %v, want %v", res, tc.expected)
}
})
}
}

func TestConvert_PriceToWire(t *testing.T) {
testCases := []struct {
name string
input float64
maxDec int
szDec int
expected string
}{
{
name: "BTC Price",
input: 105000,
maxDec: 6,
szDec: 5,
expected: "105000",
},
{
name: "BTC Price",
input: 105000.1234,
maxDec: 6,
szDec: 5,
expected: "105000",
},
{
name: "BTC Price",
input: 95001.123456,
maxDec: 6,
szDec: 5,
expected: "95001",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
res := PriceToWire(tc.input, tc.maxDec, tc.szDec)
if res != tc.expected {
t.Errorf("PriceToWire() = %v, want %v", res, tc.expected)
}
})
}
}
Loading