fix: HL balance now reads spot USDC via SpotUserState
HL testnet USDC lives in the spot account, not the perp clearinghouse. GetBalance() was calling UserState() (perp clearinghouseState), which returned /usr/bin/bash. Switched to SpotUserState() and parse USDC.total - USDC.hold. Also cleaned up .gitignore to exclude .env, binary, logs, data/.
This commit is contained in:
@@ -16,7 +16,6 @@ import (
|
||||
hl "github.com/sonirico/go-hyperliquid"
|
||||
)
|
||||
|
||||
// HyperLiquidTrade handles order placement on HyperLiquid using the SDK.
|
||||
type HyperLiquidTrade struct {
|
||||
exchange *hl.Exchange
|
||||
info *hl.Info
|
||||
@@ -40,24 +39,20 @@ func NewHyperLiquidTrade(privateKeyHex, mainAddress, apiAddress string) (*HyperL
|
||||
|
||||
privKey, err := crypto.ToECDSA(keyBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("convert to ECDSA: %w", err)
|
||||
return nil, fmt.Errorf("to ECDSA: %w", err)
|
||||
}
|
||||
|
||||
// Initialize SDK Info (auto-fetches meta + spotMeta)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
info := hl.NewInfo(ctx, hl.TestnetAPIURL, true, nil, nil, nil)
|
||||
|
||||
t := &HyperLiquidTrade{
|
||||
return &HyperLiquidTrade{
|
||||
privateKey: privKey,
|
||||
mainAddress: mainAddress,
|
||||
info: info,
|
||||
configured: true,
|
||||
}
|
||||
|
||||
// Initialize exchange lazily on first order
|
||||
return t, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *HyperLiquidTrade) initExchange() error {
|
||||
@@ -65,7 +60,7 @@ func (h *HyperLiquidTrade) initExchange() error {
|
||||
return nil
|
||||
}
|
||||
if !h.configured {
|
||||
return fmt.Errorf("HyperLiquid not configured")
|
||||
return fmt.Errorf("HL not configured")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
@@ -73,25 +68,14 @@ func (h *HyperLiquidTrade) initExchange() error {
|
||||
|
||||
meta, err := h.info.Meta(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch meta: %w", err)
|
||||
return fmt.Errorf("meta: %w", err)
|
||||
}
|
||||
|
||||
spotMeta, err := h.info.SpotMeta(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch spot meta: %w", err)
|
||||
return fmt.Errorf("spot meta: %w", err)
|
||||
}
|
||||
|
||||
h.exchange = hl.NewExchange(
|
||||
ctx,
|
||||
h.privateKey,
|
||||
hl.TestnetAPIURL,
|
||||
meta,
|
||||
"",
|
||||
h.mainAddress,
|
||||
spotMeta,
|
||||
nil,
|
||||
)
|
||||
|
||||
h.exchange = hl.NewExchange(ctx, h.privateKey, hl.TestnetAPIURL, meta, "", h.mainAddress, spotMeta, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -99,38 +83,30 @@ func (h *HyperLiquidTrade) IsConfigured() bool {
|
||||
return h.configured
|
||||
}
|
||||
|
||||
// PlaceMarketOrder places a market (IOC) order on HyperLiquid.
|
||||
func (h *HyperLiquidTrade) PlaceMarketOrder(coin, side, sz string) (string, error) {
|
||||
if !h.configured {
|
||||
return "", fmt.Errorf("HyperLiquid not configured")
|
||||
return "", fmt.Errorf("HL not configured")
|
||||
}
|
||||
|
||||
if err := h.initExchange(); err != nil {
|
||||
return "", fmt.Errorf("init exchange: %w", err)
|
||||
return "", fmt.Errorf("init: %w", err)
|
||||
}
|
||||
|
||||
isBuy := side == "buy"
|
||||
size, err := strconv.ParseFloat(sz, 64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse size %s: %w", sz, err)
|
||||
}
|
||||
size, _ := strconv.ParseFloat(sz, 64)
|
||||
|
||||
// Get current price for slippage
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
mids, err := h.info.AllMids(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fetch mids: %w", err)
|
||||
return "", fmt.Errorf("mids: %w", err)
|
||||
}
|
||||
|
||||
priceStr, ok := mids[coin]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("coin %s not found", coin)
|
||||
}
|
||||
midPx, _ := strconv.ParseFloat(priceStr, 64)
|
||||
|
||||
// Aggressive IOC: buy above market, sell below
|
||||
limitPx := midPx * 2.0
|
||||
if !isBuy {
|
||||
limitPx = midPx * 0.5
|
||||
@@ -140,51 +116,56 @@ func (h *HyperLiquidTrade) PlaceMarketOrder(coin, side, sz string) (string, erro
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("market open: %w", err)
|
||||
}
|
||||
|
||||
// Marshal response
|
||||
respJSON, _ := json.Marshal(result)
|
||||
return string(respJSON), nil
|
||||
}
|
||||
|
||||
// GetHLSize calculates size for a given USD amount on HyperLiquid.
|
||||
func (h *HyperLiquidTrade) GetBalance() (float64, error) {
|
||||
if !h.configured {
|
||||
return 0, fmt.Errorf("HL not configured")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// HL testnet USDC is on spot, not perp. Use SpotUserState.
|
||||
state, err := h.info.SpotUserState(ctx, h.mainAddress)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("spot user state: %w", err)
|
||||
}
|
||||
|
||||
for _, b := range state.Balances {
|
||||
if b.Coin == "USDC" {
|
||||
total, _ := strconv.ParseFloat(b.Total, 64)
|
||||
hold, _ := strconv.ParseFloat(b.Hold, 64)
|
||||
return total - hold, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("USDC balance not found in spot state")
|
||||
}
|
||||
|
||||
func GetHLSize(coin string, amountUSD, price float64) string {
|
||||
sz := amountUSD / price
|
||||
switch coin {
|
||||
case "DOGE":
|
||||
sz = math.Floor(sz)
|
||||
if sz < 1 {
|
||||
sz = 1
|
||||
}
|
||||
if sz < 1 { sz = 1 }
|
||||
return fmt.Sprintf("%.0f", sz)
|
||||
case "LINK":
|
||||
sz = math.Floor(sz*10) / 10
|
||||
if sz < 0.1 {
|
||||
sz = 0.1
|
||||
}
|
||||
return fmt.Sprintf("%.1f", sz)
|
||||
case "ONDO":
|
||||
sz = math.Floor(sz)
|
||||
if sz < 1 {
|
||||
sz = 1
|
||||
}
|
||||
if sz < 1 { sz = 1 }
|
||||
return fmt.Sprintf("%.0f", sz)
|
||||
case "OP":
|
||||
sz = math.Floor(sz*10) / 10
|
||||
if sz < 0.1 {
|
||||
sz = 0.1
|
||||
}
|
||||
sz = math.Floor(sz*10)/10
|
||||
if sz < 0.1 { sz = 0.1 }
|
||||
return fmt.Sprintf("%.1f", sz)
|
||||
case "WIF":
|
||||
sz = math.Floor(sz)
|
||||
if sz < 1 {
|
||||
sz = 1
|
||||
}
|
||||
if sz < 1 { sz = 1 }
|
||||
return fmt.Sprintf("%.0f", sz)
|
||||
case "ARB":
|
||||
sz = math.Floor(sz*10) / 10
|
||||
if sz < 0.1 {
|
||||
sz = 0.1
|
||||
}
|
||||
sz = math.Floor(sz*10)/10
|
||||
if sz < 0.1 { sz = 0.1 }
|
||||
return fmt.Sprintf("%.1f", sz)
|
||||
default:
|
||||
return fmt.Sprintf("%.4f", sz)
|
||||
|
||||
Reference in New Issue
Block a user