From b773d7289a4402ccf6aeab59475aeb69ca6ebfde Mon Sep 17 00:00:00 2001 From: tpkeeper Date: Thu, 30 Oct 2025 15:46:17 +0800 Subject: [PATCH] Fix mcp defaultConfig override issue in multi-trader, multi-AI model scenario --- decision/engine.go | 4 +- mcp/client.go | 107 ++++++++++++++++++++++-------------------- trader/auto_trader.go | 76 ++++++++++++++++-------------- 3 files changed, 97 insertions(+), 90 deletions(-) diff --git a/decision/engine.go b/decision/engine.go index a25f3644..76bcffca 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -90,7 +90,7 @@ type FullDecision struct { } // GetFullDecision 获取AI的完整交易决策(批量分析所有币种和持仓) -func GetFullDecision(ctx *Context) (*FullDecision, error) { +func GetFullDecision(ctx *Context, mcpClient *mcp.Client) (*FullDecision, error) { // 1. 为所有币种获取市场数据 if err := fetchMarketDataForContext(ctx); err != nil { return nil, fmt.Errorf("获取市场数据失败: %w", err) @@ -101,7 +101,7 @@ func GetFullDecision(ctx *Context) (*FullDecision, error) { userPrompt := buildUserPrompt(ctx) // 3. 调用AI API(使用 system + user prompt) - aiResponse, err := mcp.CallWithMessages(systemPrompt, userPrompt) + aiResponse, err := mcpClient.CallWithMessages(systemPrompt, userPrompt) if err != nil { return nil, fmt.Errorf("调用AI API失败: %w", err) } diff --git a/mcp/client.go b/mcp/client.go index 7c8643eb..12973753 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -19,71 +19,74 @@ const ( ProviderCustom Provider = "custom" ) -// Config AI API配置 -type Config struct { - Provider Provider - APIKey string - SecretKey string // 阿里云需要 - BaseURL string - Model string - Timeout time.Duration +// Client AI API配置 +type Client struct { + Provider Provider + APIKey string + SecretKey string // 阿里云需要 + BaseURL string + Model string + Timeout time.Duration UseFullURL bool // 是否使用完整URL(不添加/chat/completions) } -// 默认配置 -var defaultConfig = Config{ - Provider: ProviderDeepSeek, - BaseURL: "https://api.deepseek.com/v1", - Model: "deepseek-chat", - Timeout: 120 * time.Second, // 增加到120秒,因为AI需要分析大量数据 +func New() *Client { + // 默认配置 + var defaultClient = Client{ + Provider: ProviderDeepSeek, + BaseURL: "https://api.deepseek.com/v1", + Model: "deepseek-chat", + Timeout: 120 * time.Second, // 增加到120秒,因为AI需要分析大量数据 + } + return &defaultClient } // SetDeepSeekAPIKey 设置DeepSeek API密钥 -func SetDeepSeekAPIKey(apiKey string) { - defaultConfig.Provider = ProviderDeepSeek - defaultConfig.APIKey = apiKey - defaultConfig.BaseURL = "https://api.deepseek.com/v1" - defaultConfig.Model = "deepseek-chat" +func (cfg *Client) SetDeepSeekAPIKey(apiKey string) { + cfg.Provider = ProviderDeepSeek + cfg.APIKey = apiKey + cfg.BaseURL = "https://api.deepseek.com/v1" + cfg.Model = "deepseek-chat" } // SetQwenAPIKey 设置阿里云Qwen API密钥 -func SetQwenAPIKey(apiKey, secretKey string) { - defaultConfig.Provider = ProviderQwen - defaultConfig.APIKey = apiKey - defaultConfig.SecretKey = secretKey - defaultConfig.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1" - defaultConfig.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max +func (cfg *Client) SetQwenAPIKey(apiKey, secretKey string) { + cfg.Provider = ProviderQwen + cfg.APIKey = apiKey + cfg.SecretKey = secretKey + cfg.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1" + cfg.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max } // SetCustomAPI 设置自定义OpenAI兼容API -func SetCustomAPI(apiURL, apiKey, modelName string) { - defaultConfig.Provider = ProviderCustom - defaultConfig.APIKey = apiKey +func (cfg *Client) SetCustomAPI(apiURL, apiKey, modelName string) { + cfg.Provider = ProviderCustom + cfg.APIKey = apiKey // 检查URL是否以#结尾,如果是则使用完整URL(不添加/chat/completions) if strings.HasSuffix(apiURL, "#") { - defaultConfig.BaseURL = strings.TrimSuffix(apiURL, "#") - defaultConfig.UseFullURL = true + cfg.BaseURL = strings.TrimSuffix(apiURL, "#") + cfg.UseFullURL = true } else { - defaultConfig.BaseURL = apiURL - defaultConfig.UseFullURL = false + cfg.BaseURL = apiURL + cfg.UseFullURL = false } - defaultConfig.Model = modelName - defaultConfig.Timeout = 120 * time.Second + cfg.Model = modelName + cfg.Timeout = 120 * time.Second } -// SetConfig 设置完整的AI配置(高级用户) -func SetConfig(config Config) { - if config.Timeout == 0 { - config.Timeout = 30 * time.Second +// SetClient 设置完整的AI配置(高级用户) +func (cfg *Client) SetClient(Client Client) { + if Client.Timeout == 0 { + Client.Timeout = 30 * time.Second } - defaultConfig = config + cfg = &Client } // CallWithMessages 使用 system + user prompt 调用AI API(推荐) -func CallWithMessages(systemPrompt, userPrompt string) (string, error) { - if defaultConfig.APIKey == "" { +func (cfg *Client) CallWithMessages(systemPrompt, userPrompt string) (string, error) { + if cfg.APIKey == "" { return "", fmt.Errorf("AI API密钥未设置,请先调用 SetDeepSeekAPIKey() 或 SetQwenAPIKey()") } @@ -96,7 +99,7 @@ func CallWithMessages(systemPrompt, userPrompt string) (string, error) { fmt.Printf("⚠️ AI API调用失败,正在重试 (%d/%d)...\n", attempt, maxRetries) } - result, err := callOnce(systemPrompt, userPrompt) + result, err := cfg.callOnce(systemPrompt, userPrompt) if err == nil { if attempt > 1 { fmt.Printf("✓ AI API重试成功\n") @@ -122,7 +125,7 @@ func CallWithMessages(systemPrompt, userPrompt string) (string, error) { } // callOnce 单次调用AI API(内部使用) -func callOnce(systemPrompt, userPrompt string) (string, error) { +func (cfg *Client) callOnce(systemPrompt, userPrompt string) (string, error) { // 构建 messages 数组 messages := []map[string]string{} @@ -142,7 +145,7 @@ func callOnce(systemPrompt, userPrompt string) (string, error) { // 构建请求体 requestBody := map[string]interface{}{ - "model": defaultConfig.Model, + "model": cfg.Model, "messages": messages, "temperature": 0.5, // 降低temperature以提高JSON格式稳定性 "max_tokens": 2000, @@ -158,12 +161,12 @@ func callOnce(systemPrompt, userPrompt string) (string, error) { // 创建HTTP请求 var url string - if defaultConfig.UseFullURL { + if cfg.UseFullURL { // 使用完整URL,不添加/chat/completions - url = defaultConfig.BaseURL + url = cfg.BaseURL } else { // 默认行为:添加/chat/completions - url = fmt.Sprintf("%s/chat/completions", defaultConfig.BaseURL) + url = fmt.Sprintf("%s/chat/completions", cfg.BaseURL) } req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) if err != nil { @@ -173,19 +176,19 @@ func callOnce(systemPrompt, userPrompt string) (string, error) { req.Header.Set("Content-Type", "application/json") // 根据不同的Provider设置认证方式 - switch defaultConfig.Provider { + switch cfg.Provider { case ProviderDeepSeek: - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey)) case ProviderQwen: // 阿里云Qwen使用API-Key认证 - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey)) // 注意:如果使用的不是兼容模式,可能需要不同的认证方式 default: - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey)) } // 发送请求 - client := &http.Client{Timeout: defaultConfig.Timeout} + client := &http.Client{Timeout: cfg.Timeout} resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("发送请求失败: %w", err) diff --git a/trader/auto_trader.go b/trader/auto_trader.go index cc39341b..30437933 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -66,21 +66,22 @@ type AutoTraderConfig struct { // AutoTrader 自动交易器 type AutoTrader struct { - id string // Trader唯一标识 - name string // Trader显示名称 - aiModel string // AI模型名称 - exchange string // 交易平台名称 - config AutoTraderConfig - trader Trader // 使用Trader接口(支持多平台) - decisionLogger *logger.DecisionLogger // 决策日志记录器 - initialBalance float64 - dailyPnL float64 - lastResetTime time.Time - stopUntil time.Time - isRunning bool - startTime time.Time // 系统启动时间 - callCount int // AI调用次数 - positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒) + id string // Trader唯一标识 + name string // Trader显示名称 + aiModel string // AI模型名称 + exchange string // 交易平台名称 + config AutoTraderConfig + trader Trader // 使用Trader接口(支持多平台) + mcpClient *mcp.Client + decisionLogger *logger.DecisionLogger // 决策日志记录器 + initialBalance float64 + dailyPnL float64 + lastResetTime time.Time + stopUntil time.Time + isRunning bool + startTime time.Time // 系统启动时间 + callCount int // AI调用次数 + positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒) } // NewAutoTrader 创建自动交易器 @@ -100,18 +101,20 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) { } } + mcpClient := mcp.New() + // 初始化AI if config.AIModel == "custom" { // 使用自定义API - mcp.SetCustomAPI(config.CustomAPIURL, config.CustomAPIKey, config.CustomModelName) + mcpClient.SetCustomAPI(config.CustomAPIURL, config.CustomAPIKey, config.CustomModelName) log.Printf("🤖 [%s] 使用自定义AI API: %s (模型: %s)", config.Name, config.CustomAPIURL, config.CustomModelName) } else if config.UseQwen || config.AIModel == "qwen" { // 使用Qwen - mcp.SetQwenAPIKey(config.QwenKey, "") + mcpClient.SetQwenAPIKey(config.QwenKey, "") log.Printf("🤖 [%s] 使用阿里云Qwen AI", config.Name) } else { // 默认使用DeepSeek - mcp.SetDeepSeekAPIKey(config.DeepSeekKey) + mcpClient.SetDeepSeekAPIKey(config.DeepSeekKey) log.Printf("🤖 [%s] 使用DeepSeek AI", config.Name) } @@ -159,18 +162,19 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) { decisionLogger := logger.NewDecisionLogger(logDir) return &AutoTrader{ - id: config.ID, - name: config.Name, - aiModel: config.AIModel, - exchange: config.Exchange, - config: config, - trader: trader, - decisionLogger: decisionLogger, - initialBalance: config.InitialBalance, - lastResetTime: time.Now(), - startTime: time.Now(), - callCount: 0, - isRunning: false, + id: config.ID, + name: config.Name, + aiModel: config.AIModel, + exchange: config.Exchange, + config: config, + trader: trader, + mcpClient: mcpClient, + decisionLogger: decisionLogger, + initialBalance: config.InitialBalance, + lastResetTime: time.Now(), + startTime: time.Now(), + callCount: 0, + isRunning: false, positionFirstSeenTime: make(map[string]int64), }, nil } @@ -282,7 +286,7 @@ func (at *AutoTrader) runCycle() error { // 4. 调用AI获取完整决策 log.Println("🤖 正在请求AI分析并决策...") - decision, err := decision.GetFullDecision(ctx) + decision, err := decision.GetFullDecision(ctx, at.mcpClient) // 即使有错误,也保存思维链、决策和输入prompt(用于debug) if decision != nil { @@ -515,11 +519,11 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { // 6. 构建上下文 ctx := &decision.Context{ - CurrentTime: time.Now().Format("2006-01-02 15:04:05"), - RuntimeMinutes: int(time.Since(at.startTime).Minutes()), - CallCount: at.callCount, - BTCETHLeverage: at.config.BTCETHLeverage, // 使用配置的杠杆倍数 - AltcoinLeverage: at.config.AltcoinLeverage, // 使用配置的杠杆倍数 + CurrentTime: time.Now().Format("2006-01-02 15:04:05"), + RuntimeMinutes: int(time.Since(at.startTime).Minutes()), + CallCount: at.callCount, + BTCETHLeverage: at.config.BTCETHLeverage, // 使用配置的杠杆倍数 + AltcoinLeverage: at.config.AltcoinLeverage, // 使用配置的杠杆倍数 Account: decision.AccountInfo{ TotalEquity: totalEquity, AvailableBalance: availableBalance,