Fix mcp defaultConfig override issue in multi-trader, multi-AI model scenario

This commit is contained in:
tpkeeper
2025-10-30 15:46:17 +08:00
parent 897ec1b833
commit 1083c06d1f
3 changed files with 97 additions and 90 deletions

View File

@@ -90,7 +90,7 @@ type FullDecision struct {
} }
// GetFullDecision 获取AI的完整交易决策批量分析所有币种和持仓 // GetFullDecision 获取AI的完整交易决策批量分析所有币种和持仓
func GetFullDecision(ctx *Context) (*FullDecision, error) { func GetFullDecision(ctx *Context, mcpClient *mcp.Client) (*FullDecision, error) {
// 1. 为所有币种获取市场数据 // 1. 为所有币种获取市场数据
if err := fetchMarketDataForContext(ctx); err != nil { if err := fetchMarketDataForContext(ctx); err != nil {
return nil, fmt.Errorf("获取市场数据失败: %w", err) return nil, fmt.Errorf("获取市场数据失败: %w", err)
@@ -101,7 +101,7 @@ func GetFullDecision(ctx *Context) (*FullDecision, error) {
userPrompt := buildUserPrompt(ctx) userPrompt := buildUserPrompt(ctx)
// 3. 调用AI API使用 system + user prompt // 3. 调用AI API使用 system + user prompt
aiResponse, err := mcp.CallWithMessages(systemPrompt, userPrompt) aiResponse, err := mcpClient.CallWithMessages(systemPrompt, userPrompt)
if err != nil { if err != nil {
return nil, fmt.Errorf("调用AI API失败: %w", err) return nil, fmt.Errorf("调用AI API失败: %w", err)
} }

View File

@@ -19,71 +19,74 @@ const (
ProviderCustom Provider = "custom" ProviderCustom Provider = "custom"
) )
// Config AI API配置 // Client AI API配置
type Config struct { type Client struct {
Provider Provider Provider Provider
APIKey string APIKey string
SecretKey string // 阿里云需要 SecretKey string // 阿里云需要
BaseURL string BaseURL string
Model string Model string
Timeout time.Duration Timeout time.Duration
UseFullURL bool // 是否使用完整URL不添加/chat/completions UseFullURL bool // 是否使用完整URL不添加/chat/completions
} }
// 默认配置 func New() *Client {
var defaultConfig = Config{ // 默认配置
Provider: ProviderDeepSeek, var defaultClient = Client{
BaseURL: "https://api.deepseek.com/v1", Provider: ProviderDeepSeek,
Model: "deepseek-chat", BaseURL: "https://api.deepseek.com/v1",
Timeout: 120 * time.Second, // 增加到120秒因为AI需要分析大量数据 Model: "deepseek-chat",
Timeout: 120 * time.Second, // 增加到120秒因为AI需要分析大量数据
}
return &defaultClient
} }
// SetDeepSeekAPIKey 设置DeepSeek API密钥 // SetDeepSeekAPIKey 设置DeepSeek API密钥
func SetDeepSeekAPIKey(apiKey string) { func (cfg *Client) SetDeepSeekAPIKey(apiKey string) {
defaultConfig.Provider = ProviderDeepSeek cfg.Provider = ProviderDeepSeek
defaultConfig.APIKey = apiKey cfg.APIKey = apiKey
defaultConfig.BaseURL = "https://api.deepseek.com/v1" cfg.BaseURL = "https://api.deepseek.com/v1"
defaultConfig.Model = "deepseek-chat" cfg.Model = "deepseek-chat"
} }
// SetQwenAPIKey 设置阿里云Qwen API密钥 // SetQwenAPIKey 设置阿里云Qwen API密钥
func SetQwenAPIKey(apiKey, secretKey string) { func (cfg *Client) SetQwenAPIKey(apiKey, secretKey string) {
defaultConfig.Provider = ProviderQwen cfg.Provider = ProviderQwen
defaultConfig.APIKey = apiKey cfg.APIKey = apiKey
defaultConfig.SecretKey = secretKey cfg.SecretKey = secretKey
defaultConfig.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1" cfg.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
defaultConfig.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max cfg.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max
} }
// SetCustomAPI 设置自定义OpenAI兼容API // SetCustomAPI 设置自定义OpenAI兼容API
func SetCustomAPI(apiURL, apiKey, modelName string) { func (cfg *Client) SetCustomAPI(apiURL, apiKey, modelName string) {
defaultConfig.Provider = ProviderCustom cfg.Provider = ProviderCustom
defaultConfig.APIKey = apiKey cfg.APIKey = apiKey
// 检查URL是否以#结尾如果是则使用完整URL不添加/chat/completions // 检查URL是否以#结尾如果是则使用完整URL不添加/chat/completions
if strings.HasSuffix(apiURL, "#") { if strings.HasSuffix(apiURL, "#") {
defaultConfig.BaseURL = strings.TrimSuffix(apiURL, "#") cfg.BaseURL = strings.TrimSuffix(apiURL, "#")
defaultConfig.UseFullURL = true cfg.UseFullURL = true
} else { } else {
defaultConfig.BaseURL = apiURL cfg.BaseURL = apiURL
defaultConfig.UseFullURL = false cfg.UseFullURL = false
} }
defaultConfig.Model = modelName cfg.Model = modelName
defaultConfig.Timeout = 120 * time.Second cfg.Timeout = 120 * time.Second
} }
// SetConfig 设置完整的AI配置高级用户 // SetClient 设置完整的AI配置高级用户
func SetConfig(config Config) { func (cfg *Client) SetClient(Client Client) {
if config.Timeout == 0 { if Client.Timeout == 0 {
config.Timeout = 30 * time.Second Client.Timeout = 30 * time.Second
} }
defaultConfig = config cfg = &Client
} }
// CallWithMessages 使用 system + user prompt 调用AI API推荐 // CallWithMessages 使用 system + user prompt 调用AI API推荐
func CallWithMessages(systemPrompt, userPrompt string) (string, error) { func (cfg *Client) CallWithMessages(systemPrompt, userPrompt string) (string, error) {
if defaultConfig.APIKey == "" { if cfg.APIKey == "" {
return "", fmt.Errorf("AI API密钥未设置请先调用 SetDeepSeekAPIKey() 或 SetQwenAPIKey()") 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) fmt.Printf("⚠️ AI API调用失败正在重试 (%d/%d)...\n", attempt, maxRetries)
} }
result, err := callOnce(systemPrompt, userPrompt) result, err := cfg.callOnce(systemPrompt, userPrompt)
if err == nil { if err == nil {
if attempt > 1 { if attempt > 1 {
fmt.Printf("✓ AI API重试成功\n") fmt.Printf("✓ AI API重试成功\n")
@@ -122,7 +125,7 @@ func CallWithMessages(systemPrompt, userPrompt string) (string, error) {
} }
// callOnce 单次调用AI API内部使用 // callOnce 单次调用AI API内部使用
func callOnce(systemPrompt, userPrompt string) (string, error) { func (cfg *Client) callOnce(systemPrompt, userPrompt string) (string, error) {
// 构建 messages 数组 // 构建 messages 数组
messages := []map[string]string{} messages := []map[string]string{}
@@ -142,7 +145,7 @@ func callOnce(systemPrompt, userPrompt string) (string, error) {
// 构建请求体 // 构建请求体
requestBody := map[string]interface{}{ requestBody := map[string]interface{}{
"model": defaultConfig.Model, "model": cfg.Model,
"messages": messages, "messages": messages,
"temperature": 0.5, // 降低temperature以提高JSON格式稳定性 "temperature": 0.5, // 降低temperature以提高JSON格式稳定性
"max_tokens": 2000, "max_tokens": 2000,
@@ -158,12 +161,12 @@ func callOnce(systemPrompt, userPrompt string) (string, error) {
// 创建HTTP请求 // 创建HTTP请求
var url string var url string
if defaultConfig.UseFullURL { if cfg.UseFullURL {
// 使用完整URL不添加/chat/completions // 使用完整URL不添加/chat/completions
url = defaultConfig.BaseURL url = cfg.BaseURL
} else { } else {
// 默认行为:添加/chat/completions // 默认行为:添加/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)) req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil { if err != nil {
@@ -173,19 +176,19 @@ func callOnce(systemPrompt, userPrompt string) (string, error) {
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
// 根据不同的Provider设置认证方式 // 根据不同的Provider设置认证方式
switch defaultConfig.Provider { switch cfg.Provider {
case ProviderDeepSeek: case ProviderDeepSeek:
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey))
case ProviderQwen: case ProviderQwen:
// 阿里云Qwen使用API-Key认证 // 阿里云Qwen使用API-Key认证
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey))
// 注意:如果使用的不是兼容模式,可能需要不同的认证方式 // 注意:如果使用的不是兼容模式,可能需要不同的认证方式
default: 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) resp, err := client.Do(req)
if err != nil { if err != nil {
return "", fmt.Errorf("发送请求失败: %w", err) return "", fmt.Errorf("发送请求失败: %w", err)

View File

@@ -66,21 +66,22 @@ type AutoTraderConfig struct {
// AutoTrader 自动交易器 // AutoTrader 自动交易器
type AutoTrader struct { type AutoTrader struct {
id string // Trader唯一标识 id string // Trader唯一标识
name string // Trader显示名称 name string // Trader显示名称
aiModel string // AI模型名称 aiModel string // AI模型名称
exchange string // 交易平台名称 exchange string // 交易平台名称
config AutoTraderConfig config AutoTraderConfig
trader Trader // 使用Trader接口支持多平台 trader Trader // 使用Trader接口支持多平台
decisionLogger *logger.DecisionLogger // 决策日志记录器 mcpClient *mcp.Client
initialBalance float64 decisionLogger *logger.DecisionLogger // 决策日志记录器
dailyPnL float64 initialBalance float64
lastResetTime time.Time dailyPnL float64
stopUntil time.Time lastResetTime time.Time
isRunning bool stopUntil time.Time
startTime time.Time // 系统启动时间 isRunning bool
callCount int // AI调用次数 startTime time.Time // 系统启动时间
positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒) callCount int // AI调用次数
positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒)
} }
// NewAutoTrader 创建自动交易器 // NewAutoTrader 创建自动交易器
@@ -100,18 +101,20 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
} }
} }
mcpClient := mcp.New()
// 初始化AI // 初始化AI
if config.AIModel == "custom" { if config.AIModel == "custom" {
// 使用自定义API // 使用自定义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) log.Printf("🤖 [%s] 使用自定义AI API: %s (模型: %s)", config.Name, config.CustomAPIURL, config.CustomModelName)
} else if config.UseQwen || config.AIModel == "qwen" { } else if config.UseQwen || config.AIModel == "qwen" {
// 使用Qwen // 使用Qwen
mcp.SetQwenAPIKey(config.QwenKey, "") mcpClient.SetQwenAPIKey(config.QwenKey, "")
log.Printf("🤖 [%s] 使用阿里云Qwen AI", config.Name) log.Printf("🤖 [%s] 使用阿里云Qwen AI", config.Name)
} else { } else {
// 默认使用DeepSeek // 默认使用DeepSeek
mcp.SetDeepSeekAPIKey(config.DeepSeekKey) mcpClient.SetDeepSeekAPIKey(config.DeepSeekKey)
log.Printf("🤖 [%s] 使用DeepSeek AI", config.Name) log.Printf("🤖 [%s] 使用DeepSeek AI", config.Name)
} }
@@ -159,18 +162,19 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
decisionLogger := logger.NewDecisionLogger(logDir) decisionLogger := logger.NewDecisionLogger(logDir)
return &AutoTrader{ return &AutoTrader{
id: config.ID, id: config.ID,
name: config.Name, name: config.Name,
aiModel: config.AIModel, aiModel: config.AIModel,
exchange: config.Exchange, exchange: config.Exchange,
config: config, config: config,
trader: trader, trader: trader,
decisionLogger: decisionLogger, mcpClient: mcpClient,
initialBalance: config.InitialBalance, decisionLogger: decisionLogger,
lastResetTime: time.Now(), initialBalance: config.InitialBalance,
startTime: time.Now(), lastResetTime: time.Now(),
callCount: 0, startTime: time.Now(),
isRunning: false, callCount: 0,
isRunning: false,
positionFirstSeenTime: make(map[string]int64), positionFirstSeenTime: make(map[string]int64),
}, nil }, nil
} }
@@ -282,7 +286,7 @@ func (at *AutoTrader) runCycle() error {
// 4. 调用AI获取完整决策 // 4. 调用AI获取完整决策
log.Println("🤖 正在请求AI分析并决策...") log.Println("🤖 正在请求AI分析并决策...")
decision, err := decision.GetFullDecision(ctx) decision, err := decision.GetFullDecision(ctx, at.mcpClient)
// 即使有错误也保存思维链、决策和输入prompt用于debug // 即使有错误也保存思维链、决策和输入prompt用于debug
if decision != nil { if decision != nil {
@@ -515,11 +519,11 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) {
// 6. 构建上下文 // 6. 构建上下文
ctx := &decision.Context{ ctx := &decision.Context{
CurrentTime: time.Now().Format("2006-01-02 15:04:05"), CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
RuntimeMinutes: int(time.Since(at.startTime).Minutes()), RuntimeMinutes: int(time.Since(at.startTime).Minutes()),
CallCount: at.callCount, CallCount: at.callCount,
BTCETHLeverage: at.config.BTCETHLeverage, // 使用配置的杠杆倍数 BTCETHLeverage: at.config.BTCETHLeverage, // 使用配置的杠杆倍数
AltcoinLeverage: at.config.AltcoinLeverage, // 使用配置的杠杆倍数 AltcoinLeverage: at.config.AltcoinLeverage, // 使用配置的杠杆倍数
Account: decision.AccountInfo{ Account: decision.AccountInfo{
TotalEquity: totalEquity, TotalEquity: totalEquity,
AvailableBalance: availableBalance, AvailableBalance: availableBalance,