mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
fix: OKX trading issues and improve position tracking
- Add maxMktSz check for OKX market orders to prevent exceeding limits - Increase margin safety buffer (0.1% fee + 1% buffer) for all exchanges - Fix Binance position closure detection with direct trade queries - Move Recent Completed Trades before Current Positions in AI prompt - Update README screenshots with table layout for better alignment
This commit is contained in:
+198
-31
@@ -139,7 +139,9 @@ func (s *Server) setupRoutes() {
|
||||
|
||||
// Exchange configuration
|
||||
protected.GET("/exchanges", s.handleGetExchangeConfigs)
|
||||
protected.POST("/exchanges", s.handleCreateExchange)
|
||||
protected.PUT("/exchanges", s.handleUpdateExchangeConfigs)
|
||||
protected.DELETE("/exchanges/:id", s.handleDeleteExchange)
|
||||
|
||||
// Strategy management
|
||||
protected.GET("/strategies", s.handleGetStrategies)
|
||||
@@ -392,14 +394,17 @@ type ExchangeConfig struct {
|
||||
|
||||
// SafeExchangeConfig Safe exchange configuration structure (does not contain sensitive information)
|
||||
type SafeExchangeConfig struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"` // "cex" or "dex"
|
||||
ID string `json:"id"` // UUID
|
||||
ExchangeType string `json:"exchange_type"` // "binance", "bybit", "okx", "hyperliquid", "aster", "lighter"
|
||||
AccountName string `json:"account_name"` // User-defined account name
|
||||
Name string `json:"name"` // Display name
|
||||
Type string `json:"type"` // "cex" or "dex"
|
||||
Enabled bool `json:"enabled"`
|
||||
Testnet bool `json:"testnet,omitempty"`
|
||||
HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"` // Hyperliquid wallet address (not sensitive)
|
||||
AsterUser string `json:"asterUser"` // Aster username (not sensitive)
|
||||
AsterSigner string `json:"asterSigner"` // Aster signer (not sensitive)
|
||||
HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"` // Hyperliquid wallet address (not sensitive)
|
||||
AsterUser string `json:"asterUser"` // Aster username (not sensitive)
|
||||
AsterSigner string `json:"asterSigner"` // Aster signer (not sensitive)
|
||||
LighterWalletAddr string `json:"lighterWalletAddr"` // LIGHTER wallet address (not sensitive)
|
||||
}
|
||||
|
||||
type UpdateModelConfigRequest struct {
|
||||
@@ -459,8 +464,12 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Generate trader ID
|
||||
traderID := fmt.Sprintf("%s_%s_%d", req.ExchangeID, req.AIModelID, time.Now().Unix())
|
||||
// Generate trader ID (use short UUID prefix for readability)
|
||||
exchangeIDShort := req.ExchangeID
|
||||
if len(exchangeIDShort) > 8 {
|
||||
exchangeIDShort = exchangeIDShort[:8]
|
||||
}
|
||||
traderID := fmt.Sprintf("%s_%s_%d", exchangeIDShort, req.AIModelID, time.Now().Unix())
|
||||
|
||||
// Set default values
|
||||
isCrossMargin := true // Default to cross margin mode
|
||||
@@ -515,7 +524,8 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
||||
var tempTrader trader.Trader
|
||||
var createErr error
|
||||
|
||||
switch req.ExchangeID {
|
||||
// Use ExchangeType (e.g., "binance") instead of ID (UUID)
|
||||
switch exchangeCfg.ExchangeType {
|
||||
case "binance":
|
||||
tempTrader = trader.NewFuturesTrader(exchangeCfg.APIKey, exchangeCfg.SecretKey, userID)
|
||||
case "hyperliquid":
|
||||
@@ -535,8 +545,29 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
||||
exchangeCfg.APIKey,
|
||||
exchangeCfg.SecretKey,
|
||||
)
|
||||
case "okx":
|
||||
tempTrader = trader.NewOKXTrader(
|
||||
exchangeCfg.APIKey,
|
||||
exchangeCfg.SecretKey,
|
||||
exchangeCfg.Passphrase,
|
||||
)
|
||||
case "lighter":
|
||||
if exchangeCfg.LighterAPIKeyPrivateKey != "" {
|
||||
tempTrader, createErr = trader.NewLighterTraderV2(
|
||||
exchangeCfg.LighterPrivateKey,
|
||||
exchangeCfg.LighterWalletAddr,
|
||||
exchangeCfg.LighterAPIKeyPrivateKey,
|
||||
exchangeCfg.Testnet,
|
||||
)
|
||||
} else {
|
||||
tempTrader, createErr = trader.NewLighterTrader(
|
||||
exchangeCfg.LighterPrivateKey,
|
||||
exchangeCfg.LighterWalletAddr,
|
||||
exchangeCfg.Testnet,
|
||||
)
|
||||
}
|
||||
default:
|
||||
logger.Infof("⚠️ Unsupported exchange type: %s, using user input for initial balance", req.ExchangeID)
|
||||
logger.Infof("⚠️ Unsupported exchange type: %s, using user input for initial balance", exchangeCfg.ExchangeType)
|
||||
}
|
||||
|
||||
if createErr != nil {
|
||||
@@ -951,7 +982,8 @@ func (s *Server) handleSyncBalance(c *gin.Context) {
|
||||
var tempTrader trader.Trader
|
||||
var createErr error
|
||||
|
||||
switch traderConfig.ExchangeID {
|
||||
// Use ExchangeType (e.g., "binance") instead of ExchangeID (which is now UUID)
|
||||
switch exchangeCfg.ExchangeType {
|
||||
case "binance":
|
||||
tempTrader = trader.NewFuturesTrader(exchangeCfg.APIKey, exchangeCfg.SecretKey, userID)
|
||||
case "hyperliquid":
|
||||
@@ -1066,7 +1098,6 @@ func (s *Server) handleClosePosition(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
traderConfig := fullConfig.Trader
|
||||
exchangeCfg := fullConfig.Exchange
|
||||
|
||||
if exchangeCfg == nil || !exchangeCfg.Enabled {
|
||||
@@ -1078,7 +1109,8 @@ func (s *Server) handleClosePosition(c *gin.Context) {
|
||||
var tempTrader trader.Trader
|
||||
var createErr error
|
||||
|
||||
switch traderConfig.ExchangeID {
|
||||
// Use ExchangeType (e.g., "binance") instead of ExchangeID (which is now UUID)
|
||||
switch exchangeCfg.ExchangeType {
|
||||
case "binance":
|
||||
tempTrader = trader.NewFuturesTrader(exchangeCfg.APIKey, exchangeCfg.SecretKey, userID)
|
||||
case "hyperliquid":
|
||||
@@ -1293,18 +1325,10 @@ func (s *Server) handleGetExchangeConfigs(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// If no exchanges in database, return default exchanges
|
||||
// If no exchanges in database, return empty array (user needs to create accounts)
|
||||
if len(exchanges) == 0 {
|
||||
logger.Infof("⚠️ No exchanges in database, returning defaults")
|
||||
defaultExchanges := []SafeExchangeConfig{
|
||||
{ID: "binance", Name: "Binance", Type: "cex", Enabled: false},
|
||||
{ID: "bybit", Name: "Bybit", Type: "cex", Enabled: false},
|
||||
{ID: "okx", Name: "OKX", Type: "cex", Enabled: false},
|
||||
{ID: "hyperliquid", Name: "Hyperliquid", Type: "dex", Enabled: false},
|
||||
{ID: "aster", Name: "Aster", Type: "dex", Enabled: false},
|
||||
{ID: "lighter", Name: "LIGHTER", Type: "dex", Enabled: false},
|
||||
}
|
||||
c.JSON(http.StatusOK, defaultExchanges)
|
||||
logger.Infof("⚠️ No exchanges in database for user %s", userID)
|
||||
c.JSON(http.StatusOK, []SafeExchangeConfig{})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1315,6 +1339,8 @@ func (s *Server) handleGetExchangeConfigs(c *gin.Context) {
|
||||
for i, exchange := range exchanges {
|
||||
safeExchanges[i] = SafeExchangeConfig{
|
||||
ID: exchange.ID,
|
||||
ExchangeType: exchange.ExchangeType,
|
||||
AccountName: exchange.AccountName,
|
||||
Name: exchange.Name,
|
||||
Type: exchange.Type,
|
||||
Enabled: exchange.Enabled,
|
||||
@@ -1322,6 +1348,7 @@ func (s *Server) handleGetExchangeConfigs(c *gin.Context) {
|
||||
HyperliquidWalletAddr: exchange.HyperliquidWalletAddr,
|
||||
AsterUser: exchange.AsterUser,
|
||||
AsterSigner: exchange.AsterSigner,
|
||||
LighterWalletAddr: exchange.LighterWalletAddr,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1408,6 +1435,145 @@ func (s *Server) handleUpdateExchangeConfigs(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Exchange configuration updated"})
|
||||
}
|
||||
|
||||
// CreateExchangeRequest request structure for creating a new exchange account
|
||||
type CreateExchangeRequest struct {
|
||||
ExchangeType string `json:"exchange_type" binding:"required"` // "binance", "bybit", "okx", "hyperliquid", "aster", "lighter"
|
||||
AccountName string `json:"account_name"` // User-defined account name
|
||||
Enabled bool `json:"enabled"`
|
||||
APIKey string `json:"api_key"`
|
||||
SecretKey string `json:"secret_key"`
|
||||
Passphrase string `json:"passphrase"`
|
||||
Testnet bool `json:"testnet"`
|
||||
HyperliquidWalletAddr string `json:"hyperliquid_wallet_addr"`
|
||||
AsterUser string `json:"aster_user"`
|
||||
AsterSigner string `json:"aster_signer"`
|
||||
AsterPrivateKey string `json:"aster_private_key"`
|
||||
LighterWalletAddr string `json:"lighter_wallet_addr"`
|
||||
LighterPrivateKey string `json:"lighter_private_key"`
|
||||
LighterAPIKeyPrivateKey string `json:"lighter_api_key_private_key"`
|
||||
}
|
||||
|
||||
// handleCreateExchange Create a new exchange account
|
||||
func (s *Server) handleCreateExchange(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
cfg := config.Get()
|
||||
|
||||
// Read raw request body
|
||||
bodyBytes, err := c.GetRawData()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read request body"})
|
||||
return
|
||||
}
|
||||
|
||||
var req CreateExchangeRequest
|
||||
|
||||
// Check if transport encryption is enabled
|
||||
if !cfg.TransportEncryption {
|
||||
// Transport encryption disabled, accept plain JSON
|
||||
if err := json.Unmarshal(bodyBytes, &req); err != nil {
|
||||
logger.Infof("❌ Failed to parse plain JSON request: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format"})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Transport encryption enabled, require encrypted payload
|
||||
var encryptedPayload crypto.EncryptedPayload
|
||||
if err := json.Unmarshal(bodyBytes, &encryptedPayload); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request format, encrypted transmission required"})
|
||||
return
|
||||
}
|
||||
|
||||
if encryptedPayload.WrappedKey == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "This endpoint only supports encrypted transmission",
|
||||
"code": "ENCRYPTION_REQUIRED",
|
||||
"message": "Encrypted transmission is required for security reasons",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
decrypted, err := s.cryptoHandler.cryptoService.DecryptSensitiveData(&encryptedPayload)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to decrypt data"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(decrypted), &req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse decrypted data"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Validate exchange type
|
||||
validTypes := map[string]bool{
|
||||
"binance": true, "bybit": true, "okx": true,
|
||||
"hyperliquid": true, "aster": true, "lighter": true,
|
||||
}
|
||||
if !validTypes[req.ExchangeType] {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid exchange type: %s", req.ExchangeType)})
|
||||
return
|
||||
}
|
||||
|
||||
// Create new exchange account
|
||||
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.LighterWalletAddr, req.LighterPrivateKey, req.LighterAPIKeyPrivateKey,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Infof("❌ Failed to create exchange account: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to create exchange account: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof("✓ Created exchange account: type=%s, name=%s, id=%s", req.ExchangeType, req.AccountName, id)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Exchange account created",
|
||||
"id": id,
|
||||
})
|
||||
}
|
||||
|
||||
// handleDeleteExchange Delete an exchange account
|
||||
func (s *Server) handleDeleteExchange(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
exchangeID := c.Param("id")
|
||||
|
||||
if exchangeID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Exchange ID is required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if any traders are using this exchange
|
||||
traders, err := s.store.Trader().List(userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to check traders"})
|
||||
return
|
||||
}
|
||||
|
||||
for _, trader := range traders {
|
||||
if trader.ExchangeID == exchangeID {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Cannot delete exchange account that is in use by traders",
|
||||
"trader_id": trader.ID,
|
||||
"trader_name": trader.Name,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Delete exchange account
|
||||
err = s.store.Exchange().Delete(userID, exchangeID)
|
||||
if err != nil {
|
||||
logger.Infof("❌ Failed to delete exchange account: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to delete exchange account: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof("✓ Deleted exchange account: id=%s", exchangeID)
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Exchange account deleted"})
|
||||
}
|
||||
|
||||
// handleTraderList Trader list
|
||||
func (s *Server) handleTraderList(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
@@ -2083,14 +2249,15 @@ func (s *Server) handleGetSupportedModels(c *gin.Context) {
|
||||
|
||||
// handleGetSupportedExchanges Get list of exchanges supported by the system
|
||||
func (s *Server) handleGetSupportedExchanges(c *gin.Context) {
|
||||
// Return static list of supported exchanges
|
||||
// Return static list of supported exchange types
|
||||
// Note: ID is empty for supported exchanges (they are templates, not actual accounts)
|
||||
supportedExchanges := []SafeExchangeConfig{
|
||||
{ID: "binance", Name: "Binance Futures", Type: "binance"},
|
||||
{ID: "bybit", Name: "Bybit Futures", Type: "bybit"},
|
||||
{ID: "okx", Name: "OKX Futures", Type: "okx"},
|
||||
{ID: "hyperliquid", Name: "Hyperliquid", Type: "hyperliquid"},
|
||||
{ID: "aster", Name: "Aster DEX", Type: "aster"},
|
||||
{ID: "lighter", Name: "LIGHTER DEX", Type: "lighter"},
|
||||
{ExchangeType: "binance", Name: "Binance Futures", Type: "cex"},
|
||||
{ExchangeType: "bybit", Name: "Bybit Futures", Type: "cex"},
|
||||
{ExchangeType: "okx", Name: "OKX Futures", Type: "cex"},
|
||||
{ExchangeType: "hyperliquid", Name: "Hyperliquid", Type: "dex"},
|
||||
{ExchangeType: "aster", Name: "Aster DEX", Type: "dex"},
|
||||
{ExchangeType: "lighter", Name: "LIGHTER DEX", Type: "dex"},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, supportedExchanges)
|
||||
|
||||
Reference in New Issue
Block a user