mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 00:36:56 +08:00
Fix mcp defaultConfig override issue in multi-trader, multi-AI model scenario
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
107
mcp/client.go
107
mcp/client.go
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user