mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
feat(hyperliquid): Add Unified Account support for Spot as Perp collateral (#1387)
This PR adds support for Hyperliquid's Unified Account mode where Spot USDC balance can be used as collateral for Perpetual trading. Changes: - Add HyperliquidUnifiedAcct field to Exchange config (default: true) - Update HyperliquidTrader to support unified account mode - When enabled, Spot USDC balance is added to available trading balance - Update API request/response structs for unified account toggle - Update trader config propagation from exchange config This aligns with Hyperliquid's roadmap to make Unified Account the default.
This commit is contained in:
+8
-2
@@ -484,6 +484,7 @@ type UpdateExchangeConfigRequest struct {
|
||||
Passphrase string `json:"passphrase"` // OKX specific
|
||||
Testnet bool `json:"testnet"`
|
||||
HyperliquidWalletAddr string `json:"hyperliquid_wallet_addr"`
|
||||
HyperliquidUnifiedAcct bool `json:"hyperliquid_unified_account"` // Unified Account mode
|
||||
AsterUser string `json:"aster_user"`
|
||||
AsterSigner string `json:"aster_signer"`
|
||||
AsterPrivateKey string `json:"aster_private_key"`
|
||||
@@ -600,6 +601,7 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
||||
string(exchangeCfg.APIKey), // private key
|
||||
exchangeCfg.HyperliquidWalletAddr,
|
||||
exchangeCfg.Testnet,
|
||||
exchangeCfg.HyperliquidUnifiedAcct,
|
||||
)
|
||||
case "aster":
|
||||
tempTrader, createErr = aster.NewAsterTrader(
|
||||
@@ -1169,6 +1171,7 @@ func (s *Server) handleSyncBalance(c *gin.Context) {
|
||||
string(exchangeCfg.APIKey),
|
||||
exchangeCfg.HyperliquidWalletAddr,
|
||||
exchangeCfg.Testnet,
|
||||
exchangeCfg.HyperliquidUnifiedAcct,
|
||||
)
|
||||
case "aster":
|
||||
tempTrader, createErr = aster.NewAsterTrader(
|
||||
@@ -1332,6 +1335,7 @@ func (s *Server) handleClosePosition(c *gin.Context) {
|
||||
string(exchangeCfg.APIKey),
|
||||
exchangeCfg.HyperliquidWalletAddr,
|
||||
exchangeCfg.Testnet,
|
||||
exchangeCfg.HyperliquidUnifiedAcct,
|
||||
)
|
||||
case "aster":
|
||||
tempTrader, createErr = aster.NewAsterTrader(
|
||||
@@ -1906,7 +1910,7 @@ func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) {
|
||||
tradersToReload[t.ID] = true
|
||||
}
|
||||
|
||||
err := s.store.Exchange().Update(userID, exchangeID, exchangeData.Enabled, exchangeData.APIKey, exchangeData.SecretKey, exchangeData.Passphrase, exchangeData.Testnet, exchangeData.HyperliquidWalletAddr, exchangeData.AsterUser, exchangeData.AsterSigner, exchangeData.AsterPrivateKey, exchangeData.LighterWalletAddr, exchangeData.LighterPrivateKey, exchangeData.LighterAPIKeyPrivateKey, exchangeData.LighterAPIKeyIndex)
|
||||
err := s.store.Exchange().Update(userID, exchangeID, exchangeData.Enabled, exchangeData.APIKey, exchangeData.SecretKey, exchangeData.Passphrase, exchangeData.Testnet, exchangeData.HyperliquidWalletAddr, exchangeData.HyperliquidUnifiedAcct, exchangeData.AsterUser, exchangeData.AsterSigner, exchangeData.AsterPrivateKey, exchangeData.LighterWalletAddr, exchangeData.LighterPrivateKey, exchangeData.LighterAPIKeyPrivateKey, exchangeData.LighterAPIKeyIndex)
|
||||
if err != nil {
|
||||
SafeInternalError(c, fmt.Sprintf("Update exchange %s", exchangeID), err)
|
||||
return
|
||||
@@ -1940,6 +1944,7 @@ type CreateExchangeRequest struct {
|
||||
Passphrase string `json:"passphrase"`
|
||||
Testnet bool `json:"testnet"`
|
||||
HyperliquidWalletAddr string `json:"hyperliquid_wallet_addr"`
|
||||
HyperliquidUnifiedAcct bool `json:"hyperliquid_unified_account"` // Unified Account mode: Spot as Perp collateral
|
||||
AsterUser string `json:"aster_user"`
|
||||
AsterSigner string `json:"aster_signer"`
|
||||
AsterPrivateKey string `json:"aster_private_key"`
|
||||
@@ -2014,7 +2019,8 @@ func (s *Server) handleCreateExchange(c *gin.Context) {
|
||||
id, err := s.store.Exchange().Create(
|
||||
userID, req.ExchangeType, req.AccountName, req.Enabled,
|
||||
req.APIKey, req.SecretKey, req.Passphrase, req.Testnet,
|
||||
req.HyperliquidWalletAddr, req.AsterUser, req.AsterSigner, req.AsterPrivateKey,
|
||||
req.HyperliquidWalletAddr, req.HyperliquidUnifiedAcct,
|
||||
req.AsterUser, req.AsterSigner, req.AsterPrivateKey,
|
||||
req.LighterWalletAddr, req.LighterPrivateKey, req.LighterAPIKeyPrivateKey, req.LighterAPIKeyIndex,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@@ -700,6 +700,7 @@ func (tm *TraderManager) addTraderFromStore(traderCfg *store.Trader, aiModelCfg
|
||||
case "hyperliquid":
|
||||
traderConfig.HyperliquidPrivateKey = string(exchangeCfg.APIKey)
|
||||
traderConfig.HyperliquidWalletAddr = exchangeCfg.HyperliquidWalletAddr
|
||||
traderConfig.HyperliquidUnifiedAcct = exchangeCfg.HyperliquidUnifiedAcct
|
||||
case "aster":
|
||||
traderConfig.AsterUser = exchangeCfg.AsterUser
|
||||
traderConfig.AsterSigner = exchangeCfg.AsterSigner
|
||||
|
||||
+13
-7
@@ -29,6 +29,7 @@ type Exchange struct {
|
||||
Passphrase crypto.EncryptedString `gorm:"column:passphrase;default:''" json:"passphrase"`
|
||||
Testnet bool `gorm:"default:false" json:"testnet"`
|
||||
HyperliquidWalletAddr string `gorm:"column:hyperliquid_wallet_addr;default:''" json:"hyperliquidWalletAddr"`
|
||||
HyperliquidUnifiedAcct bool `gorm:"column:hyperliquid_unified_account;default:true" json:"hyperliquidUnifiedAccount"` // Unified Account mode (Spot as collateral)
|
||||
AsterUser string `gorm:"column:aster_user;default:''" json:"asterUser"`
|
||||
AsterSigner string `gorm:"column:aster_signer;default:''" json:"asterSigner"`
|
||||
AsterPrivateKey crypto.EncryptedString `gorm:"column:aster_private_key;default:''" json:"asterPrivateKey"`
|
||||
@@ -181,7 +182,8 @@ func getExchangeNameAndType(exchangeType string) (name string, typ string) {
|
||||
// Create creates a new exchange account with UUID
|
||||
func (s *ExchangeStore) Create(userID, exchangeType, accountName string, enabled bool,
|
||||
apiKey, secretKey, passphrase string, testnet bool,
|
||||
hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey,
|
||||
hyperliquidWalletAddr string, hyperliquidUnifiedAcct bool,
|
||||
asterUser, asterSigner, asterPrivateKey,
|
||||
lighterWalletAddr, lighterPrivateKey, lighterApiKeyPrivateKey string, lighterApiKeyIndex int) (string, error) {
|
||||
|
||||
id := uuid.New().String()
|
||||
@@ -207,6 +209,7 @@ func (s *ExchangeStore) Create(userID, exchangeType, accountName string, enabled
|
||||
Passphrase: crypto.EncryptedString(passphrase),
|
||||
Testnet: testnet,
|
||||
HyperliquidWalletAddr: hyperliquidWalletAddr,
|
||||
HyperliquidUnifiedAcct: hyperliquidUnifiedAcct,
|
||||
AsterUser: asterUser,
|
||||
AsterSigner: asterSigner,
|
||||
AsterPrivateKey: crypto.EncryptedString(asterPrivateKey),
|
||||
@@ -224,15 +227,17 @@ func (s *ExchangeStore) Create(userID, exchangeType, accountName string, enabled
|
||||
|
||||
// Update updates exchange configuration by UUID
|
||||
func (s *ExchangeStore) Update(userID, id string, enabled bool, apiKey, secretKey, passphrase string, testnet bool,
|
||||
hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, lighterWalletAddr, lighterPrivateKey, lighterApiKeyPrivateKey string, lighterApiKeyIndex int) error {
|
||||
hyperliquidWalletAddr string, hyperliquidUnifiedAcct bool,
|
||||
asterUser, asterSigner, asterPrivateKey, lighterWalletAddr, lighterPrivateKey, lighterApiKeyPrivateKey string, lighterApiKeyIndex int) error {
|
||||
|
||||
logger.Debugf("🔧 ExchangeStore.Update: userID=%s, id=%s, enabled=%v", userID, id, enabled)
|
||||
|
||||
updates := map[string]interface{}{
|
||||
"enabled": enabled,
|
||||
"testnet": testnet,
|
||||
"hyperliquid_wallet_addr": hyperliquidWalletAddr,
|
||||
"aster_user": asterUser,
|
||||
"enabled": enabled,
|
||||
"testnet": testnet,
|
||||
"hyperliquid_wallet_addr": hyperliquidWalletAddr,
|
||||
"hyperliquid_unified_account": hyperliquidUnifiedAcct,
|
||||
"aster_user": asterUser,
|
||||
"aster_signer": asterSigner,
|
||||
"lighter_wallet_addr": lighterWalletAddr,
|
||||
"lighter_api_key_index": lighterApiKeyIndex,
|
||||
@@ -307,7 +312,8 @@ func (s *ExchangeStore) CreateLegacy(userID, id, name, typ string, enabled bool,
|
||||
// Check if this is an old-style ID (exchange type as ID)
|
||||
if id == "binance" || id == "bybit" || id == "okx" || id == "bitget" || id == "hyperliquid" || id == "aster" || id == "lighter" {
|
||||
_, err := s.Create(userID, id, "Default", enabled, apiKey, secretKey, "", testnet,
|
||||
hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, "", "", "", 0)
|
||||
hyperliquidWalletAddr, true, // Default to Unified Account mode
|
||||
asterUser, asterSigner, asterPrivateKey, "", "", "", 0)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -63,9 +63,10 @@ type AutoTraderConfig struct {
|
||||
KuCoinPassphrase string
|
||||
|
||||
// Hyperliquid configuration
|
||||
HyperliquidPrivateKey string
|
||||
HyperliquidWalletAddr string
|
||||
HyperliquidTestnet bool
|
||||
HyperliquidPrivateKey string
|
||||
HyperliquidWalletAddr string
|
||||
HyperliquidTestnet bool
|
||||
HyperliquidUnifiedAcct bool // Unified Account mode: Spot USDC as Perp collateral
|
||||
|
||||
// Aster configuration
|
||||
AsterUser string // Aster main wallet address
|
||||
@@ -260,7 +261,7 @@ func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*Au
|
||||
trader = kucoin.NewKuCoinTrader(config.KuCoinAPIKey, config.KuCoinSecretKey, config.KuCoinPassphrase)
|
||||
case "hyperliquid":
|
||||
logger.Infof("🏦 [%s] Using Hyperliquid trading", config.Name)
|
||||
trader, err = hyperliquid.NewHyperliquidTrader(config.HyperliquidPrivateKey, config.HyperliquidWalletAddr, config.HyperliquidTestnet)
|
||||
trader, err = hyperliquid.NewHyperliquidTrader(config.HyperliquidPrivateKey, config.HyperliquidWalletAddr, config.HyperliquidTestnet, config.HyperliquidUnifiedAcct)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize Hyperliquid trader: %w", err)
|
||||
}
|
||||
|
||||
@@ -21,12 +21,13 @@ import (
|
||||
|
||||
// HyperliquidTrader Hyperliquid trader
|
||||
type HyperliquidTrader struct {
|
||||
exchange *hyperliquid.Exchange
|
||||
ctx context.Context
|
||||
walletAddr string
|
||||
meta *hyperliquid.Meta // Cache meta information (including precision)
|
||||
metaMutex sync.RWMutex // Protect concurrent access to meta field
|
||||
isCrossMargin bool // Whether to use cross margin mode
|
||||
exchange *hyperliquid.Exchange
|
||||
ctx context.Context
|
||||
walletAddr string
|
||||
meta *hyperliquid.Meta // Cache meta information (including precision)
|
||||
metaMutex sync.RWMutex // Protect concurrent access to meta field
|
||||
isCrossMargin bool // Whether to use cross margin mode
|
||||
isUnifiedAccount bool // Whether to use Unified Account mode (Spot as collateral for Perps)
|
||||
// xyz dex support (stocks, forex, commodities)
|
||||
xyzMeta *xyzDexMeta
|
||||
xyzMetaMutex sync.RWMutex
|
||||
@@ -80,7 +81,8 @@ func isXyzDexAsset(symbol string) bool {
|
||||
}
|
||||
|
||||
// NewHyperliquidTrader creates a Hyperliquid trader
|
||||
func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool) (*HyperliquidTrader, error) {
|
||||
// unifiedAccount: when true, Spot USDC balance is used as collateral for Perp trading
|
||||
func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool, unifiedAccount bool) (*HyperliquidTrader, error) {
|
||||
// Remove 0x prefix from private key (if present, case-insensitive)
|
||||
privateKeyHex = strings.TrimPrefix(strings.ToLower(privateKeyHex), "0x")
|
||||
|
||||
@@ -175,14 +177,19 @@ func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool)
|
||||
}
|
||||
}
|
||||
|
||||
if unifiedAccount {
|
||||
logger.Infof("✓ Unified Account mode enabled: Spot USDC will be used as collateral for Perp trading")
|
||||
}
|
||||
|
||||
return &HyperliquidTrader{
|
||||
exchange: exchange,
|
||||
ctx: ctx,
|
||||
walletAddr: walletAddr,
|
||||
meta: meta,
|
||||
isCrossMargin: true, // Use cross margin mode by default
|
||||
privateKey: privateKey,
|
||||
isTestnet: testnet,
|
||||
exchange: exchange,
|
||||
ctx: ctx,
|
||||
walletAddr: walletAddr,
|
||||
meta: meta,
|
||||
isCrossMargin: true, // Use cross margin mode by default
|
||||
isUnifiedAccount: unifiedAccount, // Unified Account: Spot as Perp collateral
|
||||
privateKey: privateKey,
|
||||
isTestnet: testnet,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -304,9 +311,18 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
|
||||
// Note: totalWalletBalance + totalUnrealizedPnlAll should equal this
|
||||
totalEquityCalculated := accountValue + spotUSDCBalance + xyzAccountValue
|
||||
|
||||
// ✅ Step 7: Unified Account mode - Spot USDC is used as collateral for Perps
|
||||
// In this mode, available balance includes Spot USDC since it can be used for Perp margin
|
||||
if t.isUnifiedAccount && spotUSDCBalance > 0 {
|
||||
// Add Spot balance to available balance for trading
|
||||
availableBalance = availableBalance + spotUSDCBalance
|
||||
logger.Infof("✓ Unified Account: Spot %.2f USDC added to available balance (total: %.2f)",
|
||||
spotUSDCBalance, availableBalance)
|
||||
}
|
||||
|
||||
result["totalWalletBalance"] = totalWalletBalance // Total assets (Perp + Spot + xyz) - unrealized
|
||||
result["totalEquity"] = totalEquityCalculated // Total equity = Perp AV + Spot + xyz AV
|
||||
result["availableBalance"] = availableBalance // Available balance (Perpetuals only)
|
||||
result["availableBalance"] = availableBalance // Available balance (Perp + Spot if unified)
|
||||
result["totalUnrealizedProfit"] = totalUnrealizedPnlAll // Unrealized PnL (Perpetuals + xyz)
|
||||
result["spotBalance"] = spotUSDCBalance // Spot balance
|
||||
result["xyzDexBalance"] = xyzAccountValue // xyz dex equity (stock perps, forex, commodities)
|
||||
|
||||
Reference in New Issue
Block a user