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:
tinkle-community
2025-12-10 22:01:57 +08:00
parent 870faa0843
commit ecbedc6525
29 changed files with 2141 additions and 1647 deletions
+198 -31
View File
@@ -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)