Files
exchange-monitor-go/exchange/hyperliquid_trade.go
T

230 lines
5.1 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"
)
// HyperLiquidTrade handles order placement on HyperLiquid using the SDK.
type HyperLiquidTrade struct {
exchange *hl.Exchange
info *hl.Info
privateKey *ecdsa.PrivateKey
mainAddress string // main account address
nonceMu sync.Mutex
lastNonce int64
configured bool
}
func NewHyperLiquidTrade(privateKeyHex, mainAddress, apiAddress string) (*HyperLiquidTrade, error) {
if privateKeyHex == "" {
return &HyperLiquidTrade{}, nil
}
// Parse ECDSA private key (supports 0x prefix)
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("convert to ECDSA: %w", err)
}
t := &HyperLiquidTrade{
privateKey: privKey,
mainAddress: mainAddress,
configured: true,
}
// Initialize SDK info (fetch meta for exchange)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
t.info = hl.NewInfo(ctx, hl.TestnetAPIURL, true, nil, nil, nil)
if t.info == nil {
return nil, fmt.Errorf("failed to create HL Info")
}
// Wait briefly for meta to be fetched (Info fetches meta in constructor)
time.Sleep(2 * time.Second)
return t, nil
}
func (h *HyperLiquidTrade) initExchange() error {
if h.exchange != nil {
return nil
}
if !h.configured {
return fmt.Errorf("HyperLiquid not configured")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
meta, err := h.fetchMeta(ctx)
if err != nil {
return fmt.Errorf("fetch meta: %w", err)
}
h.exchange = hl.NewExchange(
ctx,
h.privateKey,
hl.TestnetAPIURL,
meta,
"", // vault
h.mainAddress, // account address
nil, // spot meta
nil, // perp dex
)
return nil
}
func (h *HyperLiquidTrade) fetchMeta(ctx context.Context) (*hl.Meta, error) {
// Use info endpoint to get meta
resp, err := h.postInfo(ctx, map[string]any{"type": "meta"})
if err != nil {
return nil, err
}
// Parse as Meta struct
var meta hl.Meta
if err := json.Unmarshal(resp, &meta); err != nil {
return nil, fmt.Errorf("parse meta: %w", err)
}
return &meta, nil
}
func (h *HyperLiquidTrade) postInfo(ctx context.Context, payload map[string]any) ([]byte, error) {
// Simple HTTP POST to info endpoint
return nil, fmt.Errorf("not implemented via SDK - use Info directly")
}
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")
}
if err := h.initExchange(); err != nil {
return "", fmt.Errorf("init exchange: %w", err)
}
isBuy := side == "buy"
// Parse size from string to float64
size, err := strconv.ParseFloat(sz, 64)
if err != nil {
return "", fmt.Errorf("parse size %s: %w", sz, err)
}
// Get current price for slippage calculation
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
allMids, err := h.fetchAllMids(ctx)
if err != nil {
return "", fmt.Errorf("fetch prices: %w", err)
}
priceStr, ok := allMids[coin]
if !ok {
return "", fmt.Errorf("coin %s not found in allMids", coin)
}
midPx, _ := strconv.ParseFloat(priceStr, 64)
// Slippage price: buy = ask (mid * 1.02), sell = bid (mid * 0.98)
var limitPx float64
if isBuy {
limitPx = midPx * 2.0 // aggressive buy
} else {
limitPx = midPx * 0.5 // aggressive sell
}
result, err := h.exchange.MarketOpen(ctx, coin, isBuy, size, &limitPx, 0.05, nil, nil)
if err != nil {
return "", fmt.Errorf("market order: %w", err)
}
// Serialize response
respJSON, _ := json.Marshal(result)
return string(respJSON), nil
}
func (h *HyperLiquidTrade) fetchAllMids(ctx context.Context) (map[string]string, error) {
// Try to get allMids via the SDK's Info if available
if h.info != nil {
mids, err := h.info.AllMids(ctx)
if err != nil {
return nil, err
}
return mids, nil
}
return nil, fmt.Errorf("info not initialized")
}
// GetHLSize calculates size for a given USD amount on HyperLiquid.
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 "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
}
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)
}
}