feat(trader): add automatic balance sync every 30 minutes

## 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 <noreply@anthropic.com>
This commit is contained in:
ZhouYongyou
2025-11-04 20:31:40 +08:00
parent e36ffc5a18
commit e8f05d1761
2 changed files with 80 additions and 13 deletions

View File

@@ -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)
}