diff --git a/api/server.go b/api/server.go index 00feb8b6..a2ab1250 100644 --- a/api/server.go +++ b/api/server.go @@ -110,6 +110,7 @@ func (s *Server) setupRoutes() { protected.POST("/traders/:id/start", s.handleStartTrader) protected.POST("/traders/:id/stop", s.handleStopTrader) protected.PUT("/traders/:id/prompt", s.handleUpdateTraderPrompt) + protected.POST("/traders/:id/sync-balance", s.handleSyncBalance) // AI模型配置 protected.GET("/models", s.handleGetModelConfigs) @@ -709,6 +710,113 @@ func (s *Server) handleUpdateTraderPrompt(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "自定义prompt已更新"}) } +// handleSyncBalance 同步交易所余额到initial_balance(选项B:手动同步 + 选项C:智能检测) +func (s *Server) handleSyncBalance(c *gin.Context) { + userID := c.GetString("user_id") + traderID := c.Param("id") + + log.Printf("🔄 用户 %s 请求同步交易员 %s 的余额", userID, traderID) + + // 从数据库获取交易员配置(包含交易所信息) + traderConfig, _, exchangeCfg, err := s.database.GetTraderConfig(userID, traderID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) + return + } + + if exchangeCfg == nil || !exchangeCfg.Enabled { + c.JSON(http.StatusBadRequest, gin.H{"error": "交易所未配置或未启用"}) + return + } + + // 创建临时 trader 查询余额 + var tempTrader trader.Trader + var createErr error + + switch traderConfig.ExchangeID { + case "binance": + tempTrader = trader.NewFuturesTrader(exchangeCfg.APIKey, exchangeCfg.SecretKey) + case "hyperliquid": + tempTrader, createErr = trader.NewHyperliquidTrader( + exchangeCfg.APIKey, + exchangeCfg.HyperliquidWalletAddr, + exchangeCfg.Testnet, + ) + case "aster": + tempTrader, createErr = trader.NewAsterTrader( + exchangeCfg.AsterUser, + exchangeCfg.AsterSigner, + exchangeCfg.AsterPrivateKey, + ) + default: + c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的交易所类型"}) + return + } + + if createErr != nil { + log.Printf("⚠️ 创建临时 trader 失败: %v", createErr) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("连接交易所失败: %v", createErr)}) + return + } + + // 查询实际余额 + balanceInfo, balanceErr := tempTrader.GetBalance() + if balanceErr != nil { + log.Printf("⚠️ 查询交易所余额失败: %v", balanceErr) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("查询余额失败: %v", balanceErr)}) + return + } + + // 提取可用余额 + var actualBalance float64 + if availableBalance, ok := balanceInfo["available_balance"].(float64); ok && availableBalance > 0 { + actualBalance = availableBalance + } else if availableBalance, ok := balanceInfo["availableBalance"].(float64); ok && availableBalance > 0 { + actualBalance = availableBalance + } else if totalBalance, ok := balanceInfo["balance"].(float64); ok && totalBalance > 0 { + actualBalance = totalBalance + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": "无法获取可用余额"}) + return + } + + oldBalance := traderConfig.InitialBalance + + // ✅ 选项C:智能检测余额变化 + changePercent := ((actualBalance - oldBalance) / oldBalance) * 100 + changeType := "增加" + if changePercent < 0 { + changeType = "减少" + } + + log.Printf("✓ 查询到交易所实际余额: %.2f USDT (当前配置: %.2f USDT, 变化: %.2f%%)", + actualBalance, oldBalance, changePercent) + + // 更新数据库中的 initial_balance + err = s.database.UpdateTraderInitialBalance(userID, traderID, actualBalance) + if err != nil { + log.Printf("❌ 更新initial_balance失败: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "更新余额失败"}) + return + } + + // 重新加载交易员到内存 + err = s.traderManager.LoadUserTraders(s.database, userID) + if err != nil { + log.Printf("⚠️ 重新加载用户交易员到内存失败: %v", err) + } + + log.Printf("✅ 已同步余额: %.2f → %.2f USDT (%s %.2f%%)", oldBalance, actualBalance, changeType, changePercent) + + c.JSON(http.StatusOK, gin.H{ + "message": "余额同步成功", + "old_balance": oldBalance, + "new_balance": actualBalance, + "change_percent": changePercent, + "change_type": changeType, + }) +} + // handleGetModelConfigs 获取AI模型配置 func (s *Server) handleGetModelConfigs(c *gin.Context) { userID := c.GetString("user_id") diff --git a/config/database.go b/config/database.go index 052a52ff..bca8b1da 100644 --- a/config/database.go +++ b/config/database.go @@ -853,6 +853,12 @@ func (d *Database) UpdateTraderCustomPrompt(userID, id string, customPrompt stri return err } +// UpdateTraderInitialBalance 更新交易员初始余额(用于同步交易所实际余额) +func (d *Database) UpdateTraderInitialBalance(userID, id string, newBalance float64) error { + _, err := d.db.Exec(`UPDATE traders SET initial_balance = ? WHERE id = ? AND user_id = ?`, newBalance, id, userID) + return err +} + // DeleteTrader 删除交易员 func (d *Database) DeleteTrader(userID, id string) error { _, err := d.db.Exec(`DELETE FROM traders WHERE id = ? AND user_id = ?`, id, userID)