Skip to content

Commit 06bcb38

Browse files
Update Spot methods
- Fix FloatToWire - Add tests - Add spot state method
1 parent 36dfc17 commit 06bcb38

File tree

7 files changed

+176
-60
lines changed

7 files changed

+176
-60
lines changed

hyperliquid/consts.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package hyperliquid
22

3-
const GLOBAL_DEBUG = false // Defualt debug that is used in all tests
3+
const GLOBAL_DEBUG = true // Defualt debug that is used in all tests
44

55
// Execution constants
66
const DEFAULT_SLIPPAGE = 0.005 // 0.5% default slippage
7-
var SZ_DECIMALS = 2 // Default decimals for size
7+
const SPOT_MAX_DECIMALS = 8 // Default decimals for spot
8+
const PERP_MAX_DECIMALS = 6 // Default decimals for perp
9+
var SZ_DECIMALS = 2 // Default decimals for usdc
810

911
// Signing constants
1012
const HYPERLIQUID_CHAIN_ID = 1337

hyperliquid/convert.go

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -43,27 +43,21 @@ func OrderWiresToOrderAction(orders []OrderWire, grouping Grouping) PlaceOrderAc
4343
}
4444

4545
func OrderRequestToWire(req OrderRequest, meta map[string]AssetInfo, isSpot bool) OrderWire {
46-
47-
var info AssetInfo
48-
for _, v := range meta {
49-
if v.SpotName == req.Coin {
50-
info = v
51-
break
52-
}
53-
}
54-
55-
var assetId int
46+
info := meta[req.Coin]
47+
var assetId, maxDecimals int
5648
if isSpot {
49+
// https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/asset-ids
5750
assetId = info.AssetId + 10000
51+
maxDecimals = SPOT_MAX_DECIMALS
5852
} else {
5953
assetId = info.AssetId
54+
maxDecimals = PERP_MAX_DECIMALS
6055
}
61-
6256
return OrderWire{
6357
Asset: assetId,
6458
IsBuy: req.IsBuy,
65-
LimitPx: FloatToWire(req.LimitPx, nil),
66-
SizePx: FloatToWire(req.Sz, &info.SzDecimals),
59+
LimitPx: FloatToWire(req.LimitPx, maxDecimals, info.SzDecimals),
60+
SizePx: FloatToWire(req.Sz, maxDecimals, info.SzDecimals),
6761
ReduceOnly: req.ReduceOnly,
6862
OrderType: OrderTypeToWire(req.OrderType),
6963
}
@@ -90,28 +84,28 @@ func OrderTypeToWire(orderType OrderType) OrderTypeWire {
9084
return OrderTypeWire{}
9185
}
9286

93-
// Format the float with custom decimal places, default is 6.
94-
// Hyperliquid only allows at most 6 digits.
95-
func FloatToWire(x float64, szDecimals *int) string {
87+
// Format the float with custom decimal places, default is 6 (perp), 8 (spot).
88+
// https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/tick-and-lot-size
89+
func FloatToWire(x float64, maxDecimals int, szDecimals int) string {
9690
bigf := big.NewFloat(x)
9791
var maxDecSz uint
98-
if szDecimals != nil {
99-
maxDecSz = uint(*szDecimals)
92+
intPart, _ := bigf.Int64()
93+
intSize := len(strconv.FormatInt(intPart, 10))
94+
if intSize >= maxDecimals {
95+
maxDecSz = 0
10096
} else {
101-
intPart, _ := bigf.Int64()
102-
intSize := len(strconv.FormatInt(intPart, 10))
103-
if intSize >= 6 {
104-
maxDecSz = 0
105-
} else {
106-
maxDecSz = uint(6 - intSize)
107-
}
97+
maxDecSz = uint(maxDecimals - intSize)
10898
}
10999
x, _ = bigf.Float64()
110100
rounded := fmt.Sprintf("%.*f", maxDecSz, x)
111-
for strings.HasSuffix(rounded, "0") {
112-
rounded = strings.TrimSuffix(rounded, "0")
101+
if strings.Contains(rounded, ".") {
102+
for strings.HasSuffix(rounded, "0") {
103+
rounded = strings.TrimSuffix(rounded, "0")
104+
}
105+
}
106+
if strings.HasSuffix(rounded, ".") {
107+
rounded = strings.TrimSuffix(rounded, ".")
113108
}
114-
rounded = strings.TrimSuffix(rounded, ".")
115109
return rounded
116110
}
117111

hyperliquid/exchange_service.go

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,13 @@ func (api *ExchangeAPI) MarketOrder(coin string, size float64, slippage *float64
120120

121121
// MarketOrderSpot is a market order for a spot coin.
122122
// It is used to buy/sell a spot coin.
123+
// Limit order with TIF=IOC and px=market price * (1 +- slippage).
124+
// Size determines the amount of the coin to buy/sell.
125+
//
126+
// MarketOrderSpot("HYPE", 0.1, nil) // Buy 0.1 HYPE
127+
// MarketOrderSpot("HYPE", -0.1, nil) // Sell 0.1 HYPE
128+
// MarketOrderSpot("HYPE", 0.1, &slippage) // Buy 0.1 HYPE with slippage
123129
func (api *ExchangeAPI) MarketOrderSpot(coin string, size float64, slippage *float64) (*PlaceOrderResponse, error) {
124-
spotName := api.spotMeta[coin].SpotName
125130
slpg := GetSlippage(slippage)
126131
isBuy := IsBuy(size)
127132
finalPx := api.SlippagePriceSpot(coin, isBuy, slpg)
@@ -131,7 +136,7 @@ func (api *ExchangeAPI) MarketOrderSpot(coin string, size float64, slippage *flo
131136
},
132137
}
133138
orderRequest := OrderRequest{
134-
Coin: spotName,
139+
Coin: coin,
135140
IsBuy: isBuy,
136141
Sz: math.Abs(size),
137142
LimitPx: finalPx,
@@ -208,42 +213,26 @@ func (api *ExchangeAPI) ClosePosition(coin string) (*PlaceOrderResponse, error)
208213

209214
// Place single order
210215
func (api *ExchangeAPI) Order(request OrderRequest, grouping Grouping) (*PlaceOrderResponse, error) {
211-
return api.BulkOrders([]OrderRequest{request}, grouping)
216+
return api.BulkOrders([]OrderRequest{request}, grouping, false)
212217
}
213218

214219
// OrderSpot places a spot order
215220
func (api *ExchangeAPI) OrderSpot(request OrderRequest, grouping Grouping) (*PlaceOrderResponse, error) {
216-
return api.BulkOrdersSpot([]OrderRequest{request}, grouping)
221+
return api.BulkOrders([]OrderRequest{request}, grouping, true)
217222
}
218223

219224
// Place orders in bulk
220225
// https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#place-an-order
221-
func (api *ExchangeAPI) BulkOrders(requests []OrderRequest, grouping Grouping) (*PlaceOrderResponse, error) {
226+
func (api *ExchangeAPI) BulkOrders(requests []OrderRequest, grouping Grouping, isSpot bool) (*PlaceOrderResponse, error) {
222227
var wires []OrderWire
223-
for _, req := range requests {
224-
wires = append(wires, OrderRequestToWire(req, api.meta, false))
228+
var meta map[string]AssetInfo
229+
if isSpot {
230+
meta = api.spotMeta
231+
} else {
232+
meta = api.meta
225233
}
226-
timestamp := GetNonce()
227-
action := OrderWiresToOrderAction(wires, grouping)
228-
v, r, s, err := api.SignL1Action(action, timestamp)
229-
if err != nil {
230-
api.debug("Error signing L1 action: %s", err)
231-
return nil, err
232-
}
233-
request := ExchangeRequest{
234-
Action: action,
235-
Nonce: timestamp,
236-
Signature: ToTypedSig(r, s, v),
237-
VaultAddress: nil,
238-
}
239-
return MakeUniversalRequest[PlaceOrderResponse](api, request)
240-
}
241-
242-
// BulkOrdersSpot places spot orders
243-
func (api *ExchangeAPI) BulkOrdersSpot(requests []OrderRequest, grouping Grouping) (*PlaceOrderResponse, error) {
244-
var wires []OrderWire
245234
for _, req := range requests {
246-
wires = append(wires, OrderRequestToWire(req, api.spotMeta, true))
235+
wires = append(wires, OrderRequestToWire(req, meta, isSpot))
247236
}
248237
timestamp := GetNonce()
249238
action := OrderWiresToOrderAction(wires, grouping)
@@ -353,7 +342,7 @@ func (api *ExchangeAPI) Withdraw(destination string, amount float64) (*WithdrawR
353342
action := WithdrawAction{
354343
Type: "withdraw3",
355344
Destination: destination,
356-
Amount: FloatToWire(amount, &SZ_DECIMALS),
345+
Amount: FloatToWire(amount, PERP_MAX_DECIMALS, SZ_DECIMALS),
357346
Time: nonce,
358347
}
359348
signatureChainID, chainType := api.getChainParams()

hyperliquid/exchange_test.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ func TestExchangeAPI_MarketOpen(testing *testing.T) {
8080
if totalSize != math.Abs(size) {
8181
testing.Errorf("res.Response.Data.Statuses[0].Filled.TotalSz = %v", totalSize)
8282
}
83+
time.Sleep(2 * time.Second) // wait to execute order
8384
accountState, err := exchangeAPI.infoAPI.GetUserState(exchangeAPI.AccountAddress())
8485
if err != nil {
8586
testing.Errorf("GetAccountState() error = %v", err)
@@ -157,7 +158,7 @@ func TestExchangeAPI_MarketClose(testing *testing.T) {
157158

158159
func TestExchangeAPI_TestWithdraw(testing *testing.T) {
159160
exchangeAPI := GetExchangeAPI()
160-
withdrawAmount := 2.0
161+
withdrawAmount := 10.0
161162
stateBefore, err := exchangeAPI.infoAPI.GetUserState(exchangeAPI.AccountAddress())
162163
if err != nil {
163164
testing.Errorf("GetAccountState() error = %v", err)
@@ -184,3 +185,18 @@ func TestExchangeAPI_TestWithdraw(testing *testing.T) {
184185
testing.Errorf("Balance not updated: %v", stateAfter)
185186
}
186187
}
188+
189+
func TestExchageAPI_TestMarketOrderSpot(testing *testing.T) {
190+
exchangeAPI := GetExchangeAPI()
191+
size := 1600.0
192+
coin := "YEETI"
193+
res, err := exchangeAPI.MarketOrderSpot(coin, size, nil)
194+
if err != nil {
195+
testing.Errorf("MakeOpen() error = %v", err)
196+
}
197+
testing.Logf("MakeOpen() = %v", res)
198+
avgPrice := res.Response.Data.Statuses[0].Filled.AvgPx
199+
if avgPrice == 0 {
200+
testing.Errorf("res.Response.Data.Statuses[0].Filled.AvgPx = %v", avgPrice)
201+
}
202+
}

hyperliquid/info_service.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,23 @@ func (api *InfoAPI) GetAccountState() (*UserState, error) {
218218
return api.GetUserState(api.AccountAddress())
219219
}
220220

221+
// Retrieve user's spot account summary
222+
// https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-a-users-token-balances
223+
func (api *InfoAPI) GetUserStateSpot(address string) (*UserStateSpot, error) {
224+
request := UserStateRequest{
225+
User: address,
226+
Typez: "spotClearinghouseState",
227+
}
228+
return MakeUniversalRequest[UserStateSpot](api, request)
229+
}
230+
231+
// Retrieve account's spot account summary
232+
// The same as GetUserStateSpot but user is set to the account address
233+
// Check AccountAddress() or SetAccountAddress() if there is a need to set the account address
234+
func (api *InfoAPI) GetAccountStateSpot() (*UserStateSpot, error) {
235+
return api.GetUserStateSpot(api.AccountAddress())
236+
}
237+
221238
// Retrieve a user's funding history
222239
// https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-a-users-funding-history-or-non-funding-ledger-updates
223240
func (api *InfoAPI) GetFundingUpdates(address string, startTime int64, endTime int64) (*[]FundingUpdate, error) {
@@ -269,6 +286,11 @@ func (api *InfoAPI) GetHistoricalFundingRates(coin string, startTime int64, endT
269286
}
270287

271288
// Helper function to get the market price of a given coin
289+
// The coin parameter is the name of the coin
290+
//
291+
// Example:
292+
//
293+
// api.GetMartketPx("BTC")
272294
func (api *InfoAPI) GetMartketPx(coin string) (float64, error) {
273295
allMids, err := api.GetAllMids()
274296
if err != nil {
@@ -282,12 +304,16 @@ func (api *InfoAPI) GetMartketPx(coin string) (float64, error) {
282304
}
283305

284306
// GetSpotMarketPx returns the market price of a given spot coin
307+
// The coin parameter is the name of the coin
308+
//
309+
// Example:
310+
//
311+
// api.GetSpotMarketPx("HYPE")
285312
func (api *InfoAPI) GetSpotMarketPx(coin string) (float64, error) {
286313
spotPrices, err := api.GetAllSpotPrices()
287314
if err != nil {
288315
return 0, err
289316
}
290-
291317
spotName := api.spotMeta[coin].SpotName
292318
parsed, err := strconv.ParseFloat((*spotPrices)[spotName], 32)
293319
if err != nil {

hyperliquid/info_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ func GetInfoAPI() *InfoAPI {
1010
if GLOBAL_DEBUG {
1111
api.SetDebugActive()
1212
}
13+
// It should be active account to pass all tests
14+
// like GetAccountFills, GetAccountWithdrawals, etc.
1315
TEST_ADDRESS := os.Getenv("TEST_ADDRESS")
1416
if TEST_ADDRESS == "" {
1517
panic("Set TEST_ADDRESS in .env file")
@@ -294,3 +296,71 @@ func TestInfoAPI_BuildMetaMap(t *testing.T) {
294296
}
295297
t.Logf("BuildMetaMap() = %v", res)
296298
}
299+
300+
func TestInfoAPI_BuildSpotMetaMap(t *testing.T) {
301+
api := GetInfoAPI()
302+
res, err := api.BuildSpotMetaMap()
303+
if err != nil {
304+
t.Errorf("BuildSpotMetaMap() error = %v", err)
305+
}
306+
if len(res) == 0 {
307+
t.Errorf("BuildSpotMetaMap() = %v, want > %v", res, 0)
308+
}
309+
// check PURR, HYPE in map
310+
if _, ok := res["PURR"]; !ok {
311+
t.Errorf("BuildSpotMetaMap() = %v, want %v", res, "PURR")
312+
}
313+
if _, ok := res["HYPE"]; !ok {
314+
t.Errorf("BuildSpotMetaMap() = %v, want %v", res, "HYPE")
315+
}
316+
t.Logf("map(PURR) = %+v", res["PURR"])
317+
t.Logf("BuildSpotMetaMap() = %+v", res)
318+
}
319+
320+
func TestInfoAPI_GetSpotMeta(t *testing.T) {
321+
api := GetInfoAPI()
322+
res, err := api.GetSpotMeta()
323+
if err != nil {
324+
t.Errorf("GetSpotMeta() error = %v", err)
325+
}
326+
if len(res.Tokens) == 0 {
327+
t.Errorf("GetSpotMeta() = %v, want > %v", res, 0)
328+
}
329+
t.Logf("GetSpotMeta() = %v", res)
330+
}
331+
332+
func TestInfoAPI_GetAllSpotPrices(t *testing.T) {
333+
api := GetInfoAPI()
334+
res, err := api.GetAllSpotPrices()
335+
if err != nil {
336+
t.Errorf("GetAllSpotPrices() error = %v", err)
337+
}
338+
if len(*res) == 0 {
339+
t.Errorf("GetAllSpotPrices() = %v, want > %v", res, 0)
340+
}
341+
t.Logf("GetAllSpotPrices() = %+v", res)
342+
}
343+
344+
func TestInfoAPI_GetSpotMarketPx(t *testing.T) {
345+
api := GetInfoAPI()
346+
res, err := api.GetSpotMarketPx("HYPE")
347+
if err != nil {
348+
t.Errorf("GetSpotMarketPx() error = %v", err)
349+
}
350+
if res < 0 {
351+
t.Errorf("GetSpotMarketPx() = %v, want > %v", res, 0)
352+
}
353+
t.Logf("GetSpotMarketPx(HYPE) = %v", res)
354+
}
355+
356+
func TestInfoAPI_GetUserStateSpot(t *testing.T) {
357+
api := GetInfoAPI()
358+
res, err := api.GetAccountStateSpot()
359+
if err != nil {
360+
t.Errorf("GetUserStateSpot() error = %v", err)
361+
}
362+
if len(res.Balances) == 0 {
363+
t.Errorf("GetUserStateSpot() = %v, want > %v", res, 0)
364+
}
365+
t.Logf("GetUserStateSpot() = %+v", res)
366+
}

hyperliquid/info_types.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,25 @@ type Position struct {
5454
} `json:"cumFunding"`
5555
}
5656

57+
type UserStateSpot struct {
58+
Balances []SpotAssetPosition `json:"balances"`
59+
}
60+
61+
type SpotAssetPosition struct {
62+
/*
63+
"coin": "USDC",
64+
"token": 0,
65+
"hold": "0.0",
66+
"total": "14.625485",
67+
"entryNtl": "0.0"
68+
*/
69+
Coin string `json:"coin"`
70+
Token int `json:"token"`
71+
Hold float64 `json:"hold,string"`
72+
Total float64 `json:"total,string"`
73+
EntryNtl float64 `json:"entryNtl,string"`
74+
}
75+
5776
type Order struct {
5877
Children []any `json:"children,omitempty"`
5978
Cloid string `json:"cloid,omitempty"`

0 commit comments

Comments
 (0)