mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +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:
@@ -484,6 +484,7 @@ type UpdateExchangeConfigRequest struct {
|
|||||||
Passphrase string `json:"passphrase"` // OKX specific
|
Passphrase string `json:"passphrase"` // OKX specific
|
||||||
Testnet bool `json:"testnet"`
|
Testnet bool `json:"testnet"`
|
||||||
HyperliquidWalletAddr string `json:"hyperliquid_wallet_addr"`
|
HyperliquidWalletAddr string `json:"hyperliquid_wallet_addr"`
|
||||||
|
HyperliquidUnifiedAcct bool `json:"hyperliquid_unified_account"` // Unified Account mode
|
||||||
AsterUser string `json:"aster_user"`
|
AsterUser string `json:"aster_user"`
|
||||||
AsterSigner string `json:"aster_signer"`
|
AsterSigner string `json:"aster_signer"`
|
||||||
AsterPrivateKey string `json:"aster_private_key"`
|
AsterPrivateKey string `json:"aster_private_key"`
|
||||||
@@ -600,6 +601,7 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
|||||||
string(exchangeCfg.APIKey), // private key
|
string(exchangeCfg.APIKey), // private key
|
||||||
exchangeCfg.HyperliquidWalletAddr,
|
exchangeCfg.HyperliquidWalletAddr,
|
||||||
exchangeCfg.Testnet,
|
exchangeCfg.Testnet,
|
||||||
|
exchangeCfg.HyperliquidUnifiedAcct,
|
||||||
)
|
)
|
||||||
case "aster":
|
case "aster":
|
||||||
tempTrader, createErr = aster.NewAsterTrader(
|
tempTrader, createErr = aster.NewAsterTrader(
|
||||||
@@ -1169,6 +1171,7 @@ func (s *Server) handleSyncBalance(c *gin.Context) {
|
|||||||
string(exchangeCfg.APIKey),
|
string(exchangeCfg.APIKey),
|
||||||
exchangeCfg.HyperliquidWalletAddr,
|
exchangeCfg.HyperliquidWalletAddr,
|
||||||
exchangeCfg.Testnet,
|
exchangeCfg.Testnet,
|
||||||
|
exchangeCfg.HyperliquidUnifiedAcct,
|
||||||
)
|
)
|
||||||
case "aster":
|
case "aster":
|
||||||
tempTrader, createErr = aster.NewAsterTrader(
|
tempTrader, createErr = aster.NewAsterTrader(
|
||||||
@@ -1332,6 +1335,7 @@ func (s *Server) handleClosePosition(c *gin.Context) {
|
|||||||
string(exchangeCfg.APIKey),
|
string(exchangeCfg.APIKey),
|
||||||
exchangeCfg.HyperliquidWalletAddr,
|
exchangeCfg.HyperliquidWalletAddr,
|
||||||
exchangeCfg.Testnet,
|
exchangeCfg.Testnet,
|
||||||
|
exchangeCfg.HyperliquidUnifiedAcct,
|
||||||
)
|
)
|
||||||
case "aster":
|
case "aster":
|
||||||
tempTrader, createErr = aster.NewAsterTrader(
|
tempTrader, createErr = aster.NewAsterTrader(
|
||||||
@@ -1906,7 +1910,7 @@ func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) {
|
|||||||
tradersToReload[t.ID] = true
|
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 {
|
if err != nil {
|
||||||
SafeInternalError(c, fmt.Sprintf("Update exchange %s", exchangeID), err)
|
SafeInternalError(c, fmt.Sprintf("Update exchange %s", exchangeID), err)
|
||||||
return
|
return
|
||||||
@@ -1940,6 +1944,7 @@ type CreateExchangeRequest struct {
|
|||||||
Passphrase string `json:"passphrase"`
|
Passphrase string `json:"passphrase"`
|
||||||
Testnet bool `json:"testnet"`
|
Testnet bool `json:"testnet"`
|
||||||
HyperliquidWalletAddr string `json:"hyperliquid_wallet_addr"`
|
HyperliquidWalletAddr string `json:"hyperliquid_wallet_addr"`
|
||||||
|
HyperliquidUnifiedAcct bool `json:"hyperliquid_unified_account"` // Unified Account mode: Spot as Perp collateral
|
||||||
AsterUser string `json:"aster_user"`
|
AsterUser string `json:"aster_user"`
|
||||||
AsterSigner string `json:"aster_signer"`
|
AsterSigner string `json:"aster_signer"`
|
||||||
AsterPrivateKey string `json:"aster_private_key"`
|
AsterPrivateKey string `json:"aster_private_key"`
|
||||||
@@ -2014,7 +2019,8 @@ func (s *Server) handleCreateExchange(c *gin.Context) {
|
|||||||
id, err := s.store.Exchange().Create(
|
id, err := s.store.Exchange().Create(
|
||||||
userID, req.ExchangeType, req.AccountName, req.Enabled,
|
userID, req.ExchangeType, req.AccountName, req.Enabled,
|
||||||
req.APIKey, req.SecretKey, req.Passphrase, req.Testnet,
|
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,
|
req.LighterWalletAddr, req.LighterPrivateKey, req.LighterAPIKeyPrivateKey, req.LighterAPIKeyIndex,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -700,6 +700,7 @@ func (tm *TraderManager) addTraderFromStore(traderCfg *store.Trader, aiModelCfg
|
|||||||
case "hyperliquid":
|
case "hyperliquid":
|
||||||
traderConfig.HyperliquidPrivateKey = string(exchangeCfg.APIKey)
|
traderConfig.HyperliquidPrivateKey = string(exchangeCfg.APIKey)
|
||||||
traderConfig.HyperliquidWalletAddr = exchangeCfg.HyperliquidWalletAddr
|
traderConfig.HyperliquidWalletAddr = exchangeCfg.HyperliquidWalletAddr
|
||||||
|
traderConfig.HyperliquidUnifiedAcct = exchangeCfg.HyperliquidUnifiedAcct
|
||||||
case "aster":
|
case "aster":
|
||||||
traderConfig.AsterUser = exchangeCfg.AsterUser
|
traderConfig.AsterUser = exchangeCfg.AsterUser
|
||||||
traderConfig.AsterSigner = exchangeCfg.AsterSigner
|
traderConfig.AsterSigner = exchangeCfg.AsterSigner
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type Exchange struct {
|
|||||||
Passphrase crypto.EncryptedString `gorm:"column:passphrase;default:''" json:"passphrase"`
|
Passphrase crypto.EncryptedString `gorm:"column:passphrase;default:''" json:"passphrase"`
|
||||||
Testnet bool `gorm:"default:false" json:"testnet"`
|
Testnet bool `gorm:"default:false" json:"testnet"`
|
||||||
HyperliquidWalletAddr string `gorm:"column:hyperliquid_wallet_addr;default:''" json:"hyperliquidWalletAddr"`
|
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"`
|
AsterUser string `gorm:"column:aster_user;default:''" json:"asterUser"`
|
||||||
AsterSigner string `gorm:"column:aster_signer;default:''" json:"asterSigner"`
|
AsterSigner string `gorm:"column:aster_signer;default:''" json:"asterSigner"`
|
||||||
AsterPrivateKey crypto.EncryptedString `gorm:"column:aster_private_key;default:''" json:"asterPrivateKey"`
|
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
|
// Create creates a new exchange account with UUID
|
||||||
func (s *ExchangeStore) Create(userID, exchangeType, accountName string, enabled bool,
|
func (s *ExchangeStore) Create(userID, exchangeType, accountName string, enabled bool,
|
||||||
apiKey, secretKey, passphrase string, testnet 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) {
|
lighterWalletAddr, lighterPrivateKey, lighterApiKeyPrivateKey string, lighterApiKeyIndex int) (string, error) {
|
||||||
|
|
||||||
id := uuid.New().String()
|
id := uuid.New().String()
|
||||||
@@ -207,6 +209,7 @@ func (s *ExchangeStore) Create(userID, exchangeType, accountName string, enabled
|
|||||||
Passphrase: crypto.EncryptedString(passphrase),
|
Passphrase: crypto.EncryptedString(passphrase),
|
||||||
Testnet: testnet,
|
Testnet: testnet,
|
||||||
HyperliquidWalletAddr: hyperliquidWalletAddr,
|
HyperliquidWalletAddr: hyperliquidWalletAddr,
|
||||||
|
HyperliquidUnifiedAcct: hyperliquidUnifiedAcct,
|
||||||
AsterUser: asterUser,
|
AsterUser: asterUser,
|
||||||
AsterSigner: asterSigner,
|
AsterSigner: asterSigner,
|
||||||
AsterPrivateKey: crypto.EncryptedString(asterPrivateKey),
|
AsterPrivateKey: crypto.EncryptedString(asterPrivateKey),
|
||||||
@@ -224,15 +227,17 @@ func (s *ExchangeStore) Create(userID, exchangeType, accountName string, enabled
|
|||||||
|
|
||||||
// Update updates exchange configuration by UUID
|
// Update updates exchange configuration by UUID
|
||||||
func (s *ExchangeStore) Update(userID, id string, enabled bool, apiKey, secretKey, passphrase string, testnet bool,
|
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)
|
logger.Debugf("🔧 ExchangeStore.Update: userID=%s, id=%s, enabled=%v", userID, id, enabled)
|
||||||
|
|
||||||
updates := map[string]interface{}{
|
updates := map[string]interface{}{
|
||||||
"enabled": enabled,
|
"enabled": enabled,
|
||||||
"testnet": testnet,
|
"testnet": testnet,
|
||||||
"hyperliquid_wallet_addr": hyperliquidWalletAddr,
|
"hyperliquid_wallet_addr": hyperliquidWalletAddr,
|
||||||
"aster_user": asterUser,
|
"hyperliquid_unified_account": hyperliquidUnifiedAcct,
|
||||||
|
"aster_user": asterUser,
|
||||||
"aster_signer": asterSigner,
|
"aster_signer": asterSigner,
|
||||||
"lighter_wallet_addr": lighterWalletAddr,
|
"lighter_wallet_addr": lighterWalletAddr,
|
||||||
"lighter_api_key_index": lighterApiKeyIndex,
|
"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)
|
// 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" {
|
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,
|
_, 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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,9 +63,10 @@ type AutoTraderConfig struct {
|
|||||||
KuCoinPassphrase string
|
KuCoinPassphrase string
|
||||||
|
|
||||||
// Hyperliquid configuration
|
// Hyperliquid configuration
|
||||||
HyperliquidPrivateKey string
|
HyperliquidPrivateKey string
|
||||||
HyperliquidWalletAddr string
|
HyperliquidWalletAddr string
|
||||||
HyperliquidTestnet bool
|
HyperliquidTestnet bool
|
||||||
|
HyperliquidUnifiedAcct bool // Unified Account mode: Spot USDC as Perp collateral
|
||||||
|
|
||||||
// Aster configuration
|
// Aster configuration
|
||||||
AsterUser string // Aster main wallet address
|
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)
|
trader = kucoin.NewKuCoinTrader(config.KuCoinAPIKey, config.KuCoinSecretKey, config.KuCoinPassphrase)
|
||||||
case "hyperliquid":
|
case "hyperliquid":
|
||||||
logger.Infof("🏦 [%s] Using Hyperliquid trading", config.Name)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to initialize Hyperliquid trader: %w", err)
|
return nil, fmt.Errorf("failed to initialize Hyperliquid trader: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,12 +21,13 @@ import (
|
|||||||
|
|
||||||
// HyperliquidTrader Hyperliquid trader
|
// HyperliquidTrader Hyperliquid trader
|
||||||
type HyperliquidTrader struct {
|
type HyperliquidTrader struct {
|
||||||
exchange *hyperliquid.Exchange
|
exchange *hyperliquid.Exchange
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
walletAddr string
|
walletAddr string
|
||||||
meta *hyperliquid.Meta // Cache meta information (including precision)
|
meta *hyperliquid.Meta // Cache meta information (including precision)
|
||||||
metaMutex sync.RWMutex // Protect concurrent access to meta field
|
metaMutex sync.RWMutex // Protect concurrent access to meta field
|
||||||
isCrossMargin bool // Whether to use cross margin mode
|
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)
|
// xyz dex support (stocks, forex, commodities)
|
||||||
xyzMeta *xyzDexMeta
|
xyzMeta *xyzDexMeta
|
||||||
xyzMetaMutex sync.RWMutex
|
xyzMetaMutex sync.RWMutex
|
||||||
@@ -80,7 +81,8 @@ func isXyzDexAsset(symbol string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewHyperliquidTrader creates a Hyperliquid trader
|
// 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)
|
// Remove 0x prefix from private key (if present, case-insensitive)
|
||||||
privateKeyHex = strings.TrimPrefix(strings.ToLower(privateKeyHex), "0x")
|
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{
|
return &HyperliquidTrader{
|
||||||
exchange: exchange,
|
exchange: exchange,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
walletAddr: walletAddr,
|
walletAddr: walletAddr,
|
||||||
meta: meta,
|
meta: meta,
|
||||||
isCrossMargin: true, // Use cross margin mode by default
|
isCrossMargin: true, // Use cross margin mode by default
|
||||||
privateKey: privateKey,
|
isUnifiedAccount: unifiedAccount, // Unified Account: Spot as Perp collateral
|
||||||
isTestnet: testnet,
|
privateKey: privateKey,
|
||||||
|
isTestnet: testnet,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,9 +311,18 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
|
|||||||
// Note: totalWalletBalance + totalUnrealizedPnlAll should equal this
|
// Note: totalWalletBalance + totalUnrealizedPnlAll should equal this
|
||||||
totalEquityCalculated := accountValue + spotUSDCBalance + xyzAccountValue
|
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["totalWalletBalance"] = totalWalletBalance // Total assets (Perp + Spot + xyz) - unrealized
|
||||||
result["totalEquity"] = totalEquityCalculated // Total equity = Perp AV + Spot + xyz AV
|
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["totalUnrealizedProfit"] = totalUnrealizedPnlAll // Unrealized PnL (Perpetuals + xyz)
|
||||||
result["spotBalance"] = spotUSDCBalance // Spot balance
|
result["spotBalance"] = spotUSDCBalance // Spot balance
|
||||||
result["xyzDexBalance"] = xyzAccountValue // xyz dex equity (stock perps, forex, commodities)
|
result["xyzDexBalance"] = xyzAccountValue // xyz dex equity (stock perps, forex, commodities)
|
||||||
|
|||||||
Reference in New Issue
Block a user