From e8f05d17610e2989746a1b5ba1378658791980df Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 20:31:40 +0800 Subject: [PATCH] feat(trader): add automatic balance sync every 30 minutes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Automatically sync trader's initial_balance when exchange balance changes significantly (>5%), eliminating the need for manual intervention after deposits/withdrawals. ## Changes ### trader/auto_trader.go - Add fields: lastBalanceSyncTime, database, userID - Add autoSyncBalanceIfNeeded() method: * Check every 30 minutes (配合3分钟扫描周期,约10次扫描触发一次) * Query exchange balance via trader.GetBalance() * Update if change >5% * Log changes with emoji indicators - Integrate into runCycle() before trading decisions ### manager/trader_manager.go - Update NewAutoTrader() calls to pass database and userID - Update method signatures: * addTraderFromDB() * AddTraderFromDB() * loadSingleTrader() ## Configuration - Check interval: 30 minutes - Change threshold: 5% - Automatic detection and update ## Benefits ✅ 完全自动化,无需手动调用API ✅ 30分钟检查间隔,及时又不频繁 ✅ 智能判断(变化>5%才更新) ✅ 充值/提现后自动更新initial_balance ✅ 前端P&L显示自动修正 ## Example Logs 🔄 [DS-BF] 开始自动检查余额变化... 🔔 [DS-BF] 检测到余额大幅变化: 693.00 → 3000.00 USDT (333.33%) ✅ [DS-BF] 已自动同步余额到数据库 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- manager/trader_manager.go | 22 ++++++------ trader/auto_trader.go | 71 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 80 insertions(+), 13 deletions(-) diff --git a/manager/trader_manager.go b/manager/trader_manager.go index 1da30436..5dbce31c 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -181,7 +181,7 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro } // 添加到TraderManager - err = tm.addTraderFromDB(traderCfg, aiModelCfg, exchangeCfg, newsCfg, coinPoolURL, oiTopURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, defaultCoins) + err = tm.addTraderFromDB(traderCfg, aiModelCfg, exchangeCfg, newsCfg, coinPoolURL, oiTopURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, defaultCoins, database, traderCfg.UserID) if err != nil { log.Printf("❌ 添加交易员 %s 失败: %v", traderCfg.Name, err) continue @@ -193,7 +193,7 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro } // addTraderFromConfig 内部方法:从配置添加交易员(不加锁,因为调用方已加锁) -func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, newsCfg []config.NewsConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string) error { +func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, newsCfg []config.NewsConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string, database *config.Database, userID string) error { if _, exists := tm.traders[traderCfg.ID]; exists { return fmt.Errorf("trader ID '%s' 已存在", traderCfg.ID) } @@ -273,8 +273,8 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel traderConfig.DeepSeekKey = aiModelCfg.APIKey } - // 创建trader实例 - at, err := trader.NewAutoTrader(traderConfig) + // 创建trader实例(传入database和userID用于自动余额同步) + at, err := trader.NewAutoTrader(traderConfig, database, userID) if err != nil { return fmt.Errorf("创建trader失败: %w", err) } @@ -298,7 +298,7 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel // AddTrader 从数据库配置添加trader (移除旧版兼容性) // AddTraderFromDB 从数据库配置添加trader -func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string) error { +func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string, database *config.Database, userID string) error { tm.mu.Lock() defer tm.mu.Unlock() @@ -379,8 +379,8 @@ func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModel traderConfig.DeepSeekKey = aiModelCfg.APIKey } - // 创建trader实例 - at, err := trader.NewAutoTrader(traderConfig) + // 创建trader实例(传入database和userID用于自动余额同步) + at, err := trader.NewAutoTrader(traderConfig, database, userID) if err != nil { return fmt.Errorf("创建trader失败: %w", err) } @@ -710,7 +710,7 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin } // 使用现有的方法加载交易员 - err = tm.loadSingleTrader(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, oiTopURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, defaultCoins) + err = tm.loadSingleTrader(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, oiTopURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, defaultCoins, database, userID) if err != nil { log.Printf("⚠️ 加载交易员 %s 失败: %v", traderCfg.Name, err) } @@ -720,7 +720,7 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin } // loadSingleTrader 加载单个交易员(从现有代码提取的公共逻辑) -func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string) error { +func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string, database *config.Database, userID string) error { // 处理交易币种列表 var tradingCoins []string if traderCfg.TradingSymbols != "" { @@ -789,8 +789,8 @@ func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiMode traderConfig.DeepSeekKey = aiModelCfg.APIKey } - // 创建trader实例 - at, err := trader.NewAutoTrader(traderConfig) + // 创建trader实例(传入database和userID用于自动余额同步) + at, err := trader.NewAutoTrader(traderConfig, database, userID) if err != nil { return fmt.Errorf("创建trader失败: %w", err) } diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 17dae19a..378bb2f1 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -118,10 +118,13 @@ type AutoTrader struct { positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒) lastPositions map[string]*PositionSnapshot // 上一个周期的持仓快照 (symbol_side -> snapshot) newsProcessor []news.Provider // 新闻 + lastBalanceSyncTime time.Time // 上次余额同步时间 + database *config.Database // 数据库引用(用于自动更新余额) + userID string // 用户ID } // NewAutoTrader 创建自动交易器 -func NewAutoTrader(traderConfig AutoTraderConfig) (*AutoTrader, error) { +func NewAutoTrader(traderConfig AutoTraderConfig, db *config.Database, userID string) (*AutoTrader, error) { // 设置默认值 if traderConfig.ID == "" { traderConfig.ID = "default_trader" @@ -261,6 +264,9 @@ func NewAutoTrader(traderConfig AutoTraderConfig) (*AutoTrader, error) { positionFirstSeenTime: make(map[string]int64), lastPositions: make(map[string]*PositionSnapshot), newsProcessor: newsProcessor, + lastBalanceSyncTime: time.Now(), // 初始化为当前时间 + database: db, + userID: userID, }, nil } @@ -298,6 +304,64 @@ func (at *AutoTrader) Stop() { log.Println("⏹ 自动交易系统停止") } +// autoSyncBalanceIfNeeded 自动同步余额(每30分钟检查一次,变化>5%才更新) +func (at *AutoTrader) autoSyncBalanceIfNeeded() { + // 距离上次同步不足30分钟,跳过 + if time.Since(at.lastBalanceSyncTime) < 30*time.Minute { + return + } + + log.Printf("🔄 [%s] 开始自动检查余额变化...", at.name) + + // 查询实际余额 + balanceInfo, err := at.trader.GetBalance() + if err != nil { + log.Printf("⚠️ [%s] 查询余额失败: %v", at.name, err) + at.lastBalanceSyncTime = time.Now() // 即使失败也更新时间,避免频繁重试 + 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 { + log.Printf("⚠️ [%s] 无法提取可用余额", at.name) + at.lastBalanceSyncTime = time.Now() + return + } + + oldBalance := at.initialBalance + changePercent := ((actualBalance - oldBalance) / oldBalance) * 100 + + // 变化超过5%才更新 + if math.Abs(changePercent) > 5.0 { + log.Printf("🔔 [%s] 检测到余额大幅变化: %.2f → %.2f USDT (%.2f%%)", + at.name, oldBalance, actualBalance, changePercent) + + // 更新内存中的 initialBalance + at.initialBalance = actualBalance + + // 更新数据库 + if at.database != nil { + err := at.database.UpdateTraderInitialBalance(at.userID, at.id, actualBalance) + if err != nil { + log.Printf("❌ [%s] 更新数据库失败: %v", at.name, err) + } else { + log.Printf("✅ [%s] 已自动同步余额到数据库", at.name) + } + } + } else { + log.Printf("✓ [%s] 余额变化不大 (%.2f%%),无需更新", at.name, changePercent) + } + + at.lastBalanceSyncTime = time.Now() +} + // runCycle 运行一个交易周期(使用AI全权决策) func (at *AutoTrader) runCycle() error { at.callCount++ @@ -329,7 +393,10 @@ func (at *AutoTrader) runCycle() error { log.Println("📅 日盈亏已重置") } - // 3. 收集交易上下文 + // 3. 自动同步余额(每30分钟检查一次,充值/提现后自动更新) + at.autoSyncBalanceIfNeeded() + + // 4. 收集交易上下文 ctx, err := at.buildTradingContext() if err != nil { record.Success = false