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

## 功能说明
自动检测交易所余额变化,无需用户手动操作
## 核心改动
1. AutoTrader 新增字段:
   - lastBalanceSyncTime: 上次余额同步时间
   - database: 数据库引用(用于自动更新)
   - userID: 用户ID
2. 新增方法 autoSyncBalanceIfNeeded():
   - 每10分钟检查一次(避免与3分钟扫描周期重叠)
   - 余额变化>5%才更新数据库
   - 智能失败重试(避免频繁查询)
   - 完整日志记录
3. 集成到交易循环:
   - 在 runCycle() 中第3步自动调用
   - 先同步余额,再获取交易上下文
   - 不影响现有交易逻辑
4. TraderManager 更新:
   - addTraderFromDB(), AddTraderFromDB(), loadSingleTrader()
   - 新增 database 和 userID 参数
   - 正确传递到 NewAutoTrader()
5. Database 新增方法:
   - UpdateTraderInitialBalance(userID, id, newBalance)
   - 安全更新初始余额
## 为什么选择10分钟?
1. 避免与3分钟扫描周期重叠(每30分钟仅重叠1次)
2. API开销最小化:每小时仅6次额外调用
3. 充值延迟可接受:最多10分钟自动同步
4. API占用率:0.2%(远低于币安2400次/分钟限制)
## API开销
- GetBalance() 轻量级查询(权重5-10)
- 每小时仅6次额外调用
- 总调用:26次/小时(runCycle:20 + autoSync:6)
- 占用率:(10/2400)/60 = 0.2% 
## 用户体验
- 充值后最多10分钟自动同步
- 完全自动化,无需手动干预
- 前端数据实时准确
## 日志示例
- 🔄 开始自动检查余额变化...
- 🔔 检测到余额大幅变化: 693.00 → 3693.00 USDT (433.19%)
-  已自动同步余额到数据库
- ✓ 余额变化不大 (2.3%),无需更新
This commit is contained in:
ZhouYongyou
2025-11-04 20:43:16 +08:00
parent 735db88ae5
commit 7091f76ca8
3 changed files with 93 additions and 11 deletions

View File

@@ -170,7 +170,7 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro
}
// 添加到TraderManager
err = tm.addTraderFromDB(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, oiTopURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, defaultCoins)
err = tm.addTraderFromDB(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, oiTopURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, defaultCoins, database, traderCfg.UserID)
if err != nil {
log.Printf("❌ 添加交易员 %s 失败: %v", traderCfg.Name, err)
continue
@@ -182,7 +182,7 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro
}
// addTraderFromConfig 内部方法:从配置添加交易员(不加锁,因为调用方已加锁)
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 {
if _, exists := tm.traders[traderCfg.ID]; exists {
return fmt.Errorf("trader ID '%s' 已存在", traderCfg.ID)
}
@@ -262,7 +262,7 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel
}
// 创建trader实例
at, err := trader.NewAutoTrader(traderConfig)
at, err := trader.NewAutoTrader(traderConfig, database, userID)
if err != nil {
return fmt.Errorf("创建trader失败: %w", err)
}
@@ -286,7 +286,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()
@@ -368,7 +368,7 @@ func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModel
}
// 创建trader实例
at, err := trader.NewAutoTrader(traderConfig)
at, err := trader.NewAutoTrader(traderConfig, database, userID)
if err != nil {
return fmt.Errorf("创建trader失败: %w", err)
}
@@ -832,7 +832,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)
}
@@ -842,7 +842,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 != "" {
@@ -912,7 +912,7 @@ func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiMode
}
// 创建trader实例
at, err := trader.NewAutoTrader(traderConfig)
at, err := trader.NewAutoTrader(traderConfig, database, userID)
if err != nil {
return fmt.Errorf("创建trader失败: %w", err)
}