From 7091f76ca80198f99c128b456664d3ed04eef0cc Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 20:43:16 +0800 Subject: [PATCH] =?UTF-8?q?feat(trader):=20add=20automatic=20balance=20syn?= =?UTF-8?q?c=20every=2010=20minutes=20##=20=E5=8A=9F=E8=83=BD=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=20=E8=87=AA=E5=8A=A8=E6=A3=80=E6=B5=8B=E4=BA=A4?= =?UTF-8?q?=E6=98=93=E6=89=80=E4=BD=99=E9=A2=9D=E5=8F=98=E5=8C=96=EF=BC=8C?= =?UTF-8?q?=E6=97=A0=E9=9C=80=E7=94=A8=E6=88=B7=E6=89=8B=E5=8A=A8=E6=93=8D?= =?UTF-8?q?=E4=BD=9C=20##=20=E6=A0=B8=E5=BF=83=E6=94=B9=E5=8A=A8=201.=20Au?= =?UTF-8?q?toTrader=20=E6=96=B0=E5=A2=9E=E5=AD=97=E6=AE=B5=EF=BC=9A=20=20?= =?UTF-8?q?=20=20-=20lastBalanceSyncTime:=20=E4=B8=8A=E6=AC=A1=E4=BD=99?= =?UTF-8?q?=E9=A2=9D=E5=90=8C=E6=AD=A5=E6=97=B6=E9=97=B4=20=20=20=20-=20da?= =?UTF-8?q?tabase:=20=E6=95=B0=E6=8D=AE=E5=BA=93=E5=BC=95=E7=94=A8?= =?UTF-8?q?=EF=BC=88=E7=94=A8=E4=BA=8E=E8=87=AA=E5=8A=A8=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=EF=BC=89=20=20=20=20-=20userID:=20=E7=94=A8=E6=88=B7ID=202.=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=96=B9=E6=B3=95=20autoSyncBalanceIfNeeded(?= =?UTF-8?q?):=20=20=20=20-=20=E6=AF=8F10=E5=88=86=E9=92=9F=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E4=B8=80=E6=AC=A1=EF=BC=88=E9=81=BF=E5=85=8D=E4=B8=8E?= =?UTF-8?q?3=E5=88=86=E9=92=9F=E6=89=AB=E6=8F=8F=E5=91=A8=E6=9C=9F?= =?UTF-8?q?=E9=87=8D=E5=8F=A0=EF=BC=89=20=20=20=20-=20=E4=BD=99=E9=A2=9D?= =?UTF-8?q?=E5=8F=98=E5=8C=96>5%=E6=89=8D=E6=9B=B4=E6=96=B0=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=20=20=20=20-=20=E6=99=BA=E8=83=BD=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E9=87=8D=E8=AF=95=EF=BC=88=E9=81=BF=E5=85=8D=E9=A2=91?= =?UTF-8?q?=E7=B9=81=E6=9F=A5=E8=AF=A2=EF=BC=89=20=20=20=20-=20=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E6=97=A5=E5=BF=97=E8=AE=B0=E5=BD=95=203.=20=E9=9B=86?= =?UTF-8?q?=E6=88=90=E5=88=B0=E4=BA=A4=E6=98=93=E5=BE=AA=E7=8E=AF:=20=20?= =?UTF-8?q?=20=20-=20=E5=9C=A8=20runCycle()=20=E4=B8=AD=E7=AC=AC3=E6=AD=A5?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=B0=83=E7=94=A8=20=20=20=20-=20=E5=85=88?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E4=BD=99=E9=A2=9D=EF=BC=8C=E5=86=8D=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E4=BA=A4=E6=98=93=E4=B8=8A=E4=B8=8B=E6=96=87=20=20=20?= =?UTF-8?q?=20-=20=E4=B8=8D=E5=BD=B1=E5=93=8D=E7=8E=B0=E6=9C=89=E4=BA=A4?= =?UTF-8?q?=E6=98=93=E9=80=BB=E8=BE=91=204.=20TraderManager=20=E6=9B=B4?= =?UTF-8?q?=E6=96=B0:=20=20=20=20-=20addTraderFromDB(),=20AddTraderFromDB(?= =?UTF-8?q?),=20loadSingleTrader()=20=20=20=20-=20=E6=96=B0=E5=A2=9E=20dat?= =?UTF-8?q?abase=20=E5=92=8C=20userID=20=E5=8F=82=E6=95=B0=20=20=20=20-=20?= =?UTF-8?q?=E6=AD=A3=E7=A1=AE=E4=BC=A0=E9=80=92=E5=88=B0=20NewAutoTrader()?= =?UTF-8?q?=205.=20Database=20=E6=96=B0=E5=A2=9E=E6=96=B9=E6=B3=95:=20=20?= =?UTF-8?q?=20=20-=20UpdateTraderInitialBalance(userID,=20id,=20newBalance?= =?UTF-8?q?)=20=20=20=20-=20=E5=AE=89=E5=85=A8=E6=9B=B4=E6=96=B0=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E4=BD=99=E9=A2=9D=20##=20=E4=B8=BA=E4=BB=80=E4=B9=88?= =?UTF-8?q?=E9=80=89=E6=8B=A910=E5=88=86=E9=92=9F=EF=BC=9F=201.=20?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E4=B8=8E3=E5=88=86=E9=92=9F=E6=89=AB?= =?UTF-8?q?=E6=8F=8F=E5=91=A8=E6=9C=9F=E9=87=8D=E5=8F=A0=EF=BC=88=E6=AF=8F?= =?UTF-8?q?30=E5=88=86=E9=92=9F=E4=BB=85=E9=87=8D=E5=8F=A01=E6=AC=A1?= =?UTF-8?q?=EF=BC=89=202.=20API=E5=BC=80=E9=94=80=E6=9C=80=E5=B0=8F?= =?UTF-8?q?=E5=8C=96=EF=BC=9A=E6=AF=8F=E5=B0=8F=E6=97=B6=E4=BB=856?= =?UTF-8?q?=E6=AC=A1=E9=A2=9D=E5=A4=96=E8=B0=83=E7=94=A8=203.=20=E5=85=85?= =?UTF-8?q?=E5=80=BC=E5=BB=B6=E8=BF=9F=E5=8F=AF=E6=8E=A5=E5=8F=97=EF=BC=9A?= =?UTF-8?q?=E6=9C=80=E5=A4=9A10=E5=88=86=E9=92=9F=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=204.=20API=E5=8D=A0=E7=94=A8=E7=8E=87?= =?UTF-8?q?=EF=BC=9A0.2%=EF=BC=88=E8=BF=9C=E4=BD=8E=E4=BA=8E=E5=B8=81?= =?UTF-8?q?=E5=AE=892400=E6=AC=A1/=E5=88=86=E9=92=9F=E9=99=90=E5=88=B6?= =?UTF-8?q?=EF=BC=89=20##=20API=E5=BC=80=E9=94=80=20-=20GetBalance()=20?= =?UTF-8?q?=E8=BD=BB=E9=87=8F=E7=BA=A7=E6=9F=A5=E8=AF=A2=EF=BC=88=E6=9D=83?= =?UTF-8?q?=E9=87=8D5-10=EF=BC=89=20-=20=E6=AF=8F=E5=B0=8F=E6=97=B6?= =?UTF-8?q?=E4=BB=856=E6=AC=A1=E9=A2=9D=E5=A4=96=E8=B0=83=E7=94=A8=20-=20?= =?UTF-8?q?=E6=80=BB=E8=B0=83=E7=94=A8=EF=BC=9A26=E6=AC=A1/=E5=B0=8F?= =?UTF-8?q?=E6=97=B6=EF=BC=88runCycle:20=20+=20autoSync:6=EF=BC=89=20-=20?= =?UTF-8?q?=E5=8D=A0=E7=94=A8=E7=8E=87=EF=BC=9A(10/2400)/60=20=3D=200.2%?= =?UTF-8?q?=20=E2=9C=85=20##=20=E7=94=A8=E6=88=B7=E4=BD=93=E9=AA=8C=20-=20?= =?UTF-8?q?=E5=85=85=E5=80=BC=E5=90=8E=E6=9C=80=E5=A4=9A10=E5=88=86?= =?UTF-8?q?=E9=92=9F=E8=87=AA=E5=8A=A8=E5=90=8C=E6=AD=A5=20-=20=E5=AE=8C?= =?UTF-8?q?=E5=85=A8=E8=87=AA=E5=8A=A8=E5=8C=96=EF=BC=8C=E6=97=A0=E9=9C=80?= =?UTF-8?q?=E6=89=8B=E5=8A=A8=E5=B9=B2=E9=A2=84=20-=20=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=AE=9E=E6=97=B6=E5=87=86=E7=A1=AE=20##=20?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E7=A4=BA=E4=BE=8B=20-=20=F0=9F=94=84=20?= =?UTF-8?q?=E5=BC=80=E5=A7=8B=E8=87=AA=E5=8A=A8=E6=A3=80=E6=9F=A5=E4=BD=99?= =?UTF-8?q?=E9=A2=9D=E5=8F=98=E5=8C=96...=20-=20=F0=9F=94=94=20=E6=A3=80?= =?UTF-8?q?=E6=B5=8B=E5=88=B0=E4=BD=99=E9=A2=9D=E5=A4=A7=E5=B9=85=E5=8F=98?= =?UTF-8?q?=E5=8C=96:=20693.00=20=E2=86=92=203693.00=20USDT=20(433.19%)=20?= =?UTF-8?q?-=20=E2=9C=85=20=E5=B7=B2=E8=87=AA=E5=8A=A8=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E4=BD=99=E9=A2=9D=E5=88=B0=E6=95=B0=E6=8D=AE=E5=BA=93=20-=20?= =?UTF-8?q?=E2=9C=93=20=E4=BD=99=E9=A2=9D=E5=8F=98=E5=8C=96=E4=B8=8D?= =?UTF-8?q?=E5=A4=A7=20(2.3%)=EF=BC=8C=E6=97=A0=E9=9C=80=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/database.go | 6 +++ manager/trader_manager.go | 16 ++++---- trader/auto_trader.go | 82 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 93 insertions(+), 11 deletions(-) diff --git a/config/database.go b/config/database.go index 651c425d..cffaabe9 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) diff --git a/manager/trader_manager.go b/manager/trader_manager.go index 4ebcf20b..e3c3b400 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -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) } diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 1e93ab5c..de7feda3 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "log" + "math" "nofx/decision" "nofx/logger" "nofx/market" @@ -98,10 +99,13 @@ type AutoTrader struct { startTime time.Time // 系统启动时间 callCount int // AI调用次数 positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒) + lastBalanceSyncTime time.Time // 上次余额同步时间 + database interface{} // 数据库引用(用于自动更新余额) + userID string // 用户ID } // NewAutoTrader 创建自动交易器 -func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) { +func NewAutoTrader(config AutoTraderConfig, database interface{}, userID string) (*AutoTrader, error) { // 设置默认值 if config.ID == "" { config.ID = "default_trader" @@ -216,6 +220,9 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) { callCount: 0, isRunning: false, positionFirstSeenTime: make(map[string]int64), + lastBalanceSyncTime: time.Now(), // 初始化为当前时间 + database: database, + userID: userID, }, nil } @@ -253,6 +260,72 @@ func (at *AutoTrader) Stop() { log.Println("⏹ 自动交易系统停止") } +// autoSyncBalanceIfNeeded 自动同步余额(每10分钟检查一次,变化>5%才更新) +func (at *AutoTrader) autoSyncBalanceIfNeeded() { + // 距离上次同步不足10分钟,跳过 + if time.Since(at.lastBalanceSyncTime) < 10*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 { + // 这里需要根据实际的数据库类型进行类型断言 + // 由于使用了 interface{},我们需要在 TraderManager 层面处理更新 + // 或者在这里进行类型检查 + type DatabaseUpdater interface { + UpdateTraderInitialBalance(userID, id string, newBalance float64) error + } + if db, ok := at.database.(DatabaseUpdater); ok { + err := db.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++ @@ -284,7 +357,10 @@ func (at *AutoTrader) runCycle() error { log.Println("📅 日盈亏已重置") } - // 3. 收集交易上下文 + // 3. 自动同步余额(每10分钟检查一次,充值/提现后自动更新) + at.autoSyncBalanceIfNeeded() + + // 4. 收集交易上下文 ctx, err := at.buildTradingContext() if err != nil { record.Success = false @@ -324,7 +400,7 @@ func (at *AutoTrader) runCycle() error { log.Printf("📊 账户净值: %.2f USDT | 可用: %.2f USDT | 持仓: %d", ctx.Account.TotalEquity, ctx.Account.AvailableBalance, ctx.Account.PositionCount) - // 4. 调用AI获取完整决策 + // 5. 调用AI获取完整决策 log.Printf("🤖 正在请求AI分析并决策... [模板: %s]", at.systemPromptTemplate) decision, err := decision.GetFullDecisionWithCustomPrompt(ctx, at.mcpClient, at.customPrompt, at.overrideBasePrompt, at.systemPromptTemplate)