diff --git a/api/server.go b/api/server.go index 5f577529..41d490ce 100644 --- a/api/server.go +++ b/api/server.go @@ -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 { diff --git a/manager/trader_manager.go b/manager/trader_manager.go index 93dd1094..d59a2204 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -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 diff --git a/store/exchange.go b/store/exchange.go index e3b30e2f..2d18ca62 100644 --- a/store/exchange.go +++ b/store/exchange.go @@ -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 } diff --git a/trader/auto_trader.go b/trader/auto_trader.go index af31f5b2..d8ef87f2 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -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) } diff --git a/trader/hyperliquid/trader.go b/trader/hyperliquid/trader.go index 85496925..f5572c78 100644 --- a/trader/hyperliquid/trader.go +++ b/trader/hyperliquid/trader.go @@ -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)