Files
exchange-monitor-go/exchange/hyperliquid_trade.go
T
jackyu66git 49da6fd35b 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/.
2026-05-04 17:48:14 +08:00

174 lines
3.9 KiB
Go

package exchange
import (
"context"
"crypto/ecdsa"
"encoding/hex"
"encoding/json"
"fmt"
"math"
"strconv"
"strings"
"sync"
"time"
"github.com/ethereum/go-ethereum/crypto"
hl "github.com/sonirico/go-hyperliquid"
)
type HyperLiquidTrade struct {
exchange *hl.Exchange
info *hl.Info
privateKey *ecdsa.PrivateKey
mainAddress string
nonceMu sync.Mutex
lastNonce int64
configured bool
}
func NewHyperLiquidTrade(privateKeyHex, mainAddress, apiAddress string) (*HyperLiquidTrade, error) {
if privateKeyHex == "" {
return &HyperLiquidTrade{}, nil
}
keyHex := strings.TrimPrefix(privateKeyHex, "0x")
keyBytes, err := hex.DecodeString(keyHex)
if err != nil {
return nil, fmt.Errorf("decode private key: %w", err)
}
privKey, err := crypto.ToECDSA(keyBytes)
if err != nil {
return nil, fmt.Errorf("to ECDSA: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
info := hl.NewInfo(ctx, hl.TestnetAPIURL, true, nil, nil, nil)
return &HyperLiquidTrade{
privateKey: privKey,
mainAddress: mainAddress,
info: info,
configured: true,
}, nil
}
func (h *HyperLiquidTrade) initExchange() error {
if h.exchange != nil {
return nil
}
if !h.configured {
return fmt.Errorf("HL not configured")
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
meta, err := h.info.Meta(ctx)
if err != nil {
return fmt.Errorf("meta: %w", err)
}
spotMeta, err := h.info.SpotMeta(ctx)
if err != nil {
return fmt.Errorf("spot meta: %w", err)
}
h.exchange = hl.NewExchange(ctx, h.privateKey, hl.TestnetAPIURL, meta, "", h.mainAddress, spotMeta, nil)
return nil
}
func (h *HyperLiquidTrade) IsConfigured() bool {
return h.configured
}
func (h *HyperLiquidTrade) PlaceMarketOrder(coin, side, sz string) (string, error) {
if !h.configured {
return "", fmt.Errorf("HL not configured")
}
if err := h.initExchange(); err != nil {
return "", fmt.Errorf("init: %w", err)
}
isBuy := side == "buy"
size, _ := strconv.ParseFloat(sz, 64)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
mids, err := h.info.AllMids(ctx)
if err != nil {
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)
limitPx := midPx * 2.0
if !isBuy {
limitPx = midPx * 0.5
}
result, err := h.exchange.MarketOpen(ctx, coin, isBuy, size, &limitPx, 0.05, nil, nil)
if err != nil {
return "", fmt.Errorf("market open: %w", err)
}
respJSON, _ := json.Marshal(result)
return string(respJSON), nil
}
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 }
return fmt.Sprintf("%.0f", sz)
case "ONDO":
sz = math.Floor(sz)
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 }
return fmt.Sprintf("%.1f", sz)
case "WIF":
sz = math.Floor(sz)
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 }
return fmt.Sprintf("%.1f", sz)
default:
return fmt.Sprintf("%.4f", sz)
}
}