diff --git a/CUSTOM_API.md b/CUSTOM_API.md new file mode 100644 index 00000000..979a89c6 --- /dev/null +++ b/CUSTOM_API.md @@ -0,0 +1,185 @@ +# 自定义 AI API 使用指南 + +## 功能说明 + +现在 NOFX 支持使用任何 OpenAI 格式兼容的 API,包括: +- OpenAI 官方 API (gpt-4o, gpt-4-turbo 等) +- OpenRouter (可访问多种模型) +- 本地部署的模型 (Ollama, LM Studio 等) +- 其他兼容 OpenAI 格式的 API 服务 + +## 配置方式 + +在 `config.json` 中添加使用自定义 API 的 trader: + +```json +{ + "traders": [ + { + "id": "trader_custom", + "name": "My Custom AI Trader", + "ai_model": "custom", + "exchange": "binance", + + "binance_api_key": "your_binance_api_key", + "binance_secret_key": "your_binance_secret_key", + + "custom_api_url": "https://api.openai.com/v1", + "custom_api_key": "sk-your-openai-api-key", + "custom_model_name": "gpt-4o", + + "initial_balance": 1000, + "scan_interval_minutes": 3 + } + ] +} +``` + +## 配置字段说明 + +| 字段 | 类型 | 必需 | 说明 | +|-----|------|------|------| +| `ai_model` | string | ✅ | 设置为 `"custom"` 启用自定义 API | +| `custom_api_url` | string | ✅ | API 的 Base URL (不含 `/chat/completions`) | +| `custom_api_key` | string | ✅ | API 密钥 | +| `custom_model_name` | string | ✅ | 模型名称 (如 `gpt-4o`, `claude-3-5-sonnet` 等) | + +## 使用示例 + +### 1. OpenAI 官方 API + +```json +{ + "ai_model": "custom", + "custom_api_url": "https://api.openai.com/v1", + "custom_api_key": "sk-proj-xxxxx", + "custom_model_name": "gpt-4o" +} +``` + +### 2. OpenRouter + +```json +{ + "ai_model": "custom", + "custom_api_url": "https://openrouter.ai/api/v1", + "custom_api_key": "sk-or-xxxxx", + "custom_model_name": "anthropic/claude-3.5-sonnet" +} +``` + +### 3. 本地 Ollama + +```json +{ + "ai_model": "custom", + "custom_api_url": "http://localhost:11434/v1", + "custom_api_key": "ollama", + "custom_model_name": "llama3.1:70b" +} +``` + +### 4. Azure OpenAI + +```json +{ + "ai_model": "custom", + "custom_api_url": "https://your-resource.openai.azure.com/openai/deployments/your-deployment", + "custom_api_key": "your-azure-api-key", + "custom_model_name": "gpt-4" +} +``` + +## 兼容性要求 + +自定义 API 必须: +1. 支持 OpenAI Chat Completions 格式 +2. 接受 `POST /chat/completions` 端点 +3. 支持 `Authorization: Bearer {api_key}` 认证 +4. 返回标准的 OpenAI 响应格式 + +## 注意事项 + +1. **URL 格式**:`custom_api_url` 应该是 Base URL,系统会自动添加 `/chat/completions` + - ✅ 正确:`https://api.openai.com/v1` + - ❌ 错误:`https://api.openai.com/v1/chat/completions` + +2. **模型名称**:确保 `custom_model_name` 与 API 提供商支持的模型名称完全一致 + +3. **API 密钥**:某些本地部署的模型可能不需要真实的 API 密钥,可以填写任意字符串 + +4. **超时设置**:默认超时时间为 120 秒,如果模型响应较慢可能需要调整 + +## 多 AI 对比交易 + +你可以同时配置多个不同 AI 的 trader 进行对比: + +```json +{ + "traders": [ + { + "id": "deepseek_trader", + "ai_model": "deepseek", + "deepseek_key": "sk-xxxxx", + ... + }, + { + "id": "gpt4_trader", + "ai_model": "custom", + "custom_api_url": "https://api.openai.com/v1", + "custom_api_key": "sk-xxxxx", + "custom_model_name": "gpt-4o", + ... + }, + { + "id": "claude_trader", + "ai_model": "custom", + "custom_api_url": "https://openrouter.ai/api/v1", + "custom_api_key": "sk-or-xxxxx", + "custom_model_name": "anthropic/claude-3.5-sonnet", + ... + } + ] +} +``` + +## 故障排除 + +### 问题:配置验证失败 + +**错误信息**:`使用自定义API时必须配置custom_api_url` + +**解决方案**:确保设置了 `ai_model: "custom"` 后,同时配置了: +- `custom_api_url` +- `custom_api_key` +- `custom_model_name` + +### 问题:API 调用失败 + +**可能原因**: +1. URL 格式错误(检查是否包含了 `/chat/completions`) +2. API 密钥无效 +3. 模型名称错误 +4. 网络连接问题 + +**调试方法**:查看日志中的错误信息,通常会包含 HTTP 状态码和错误详情 + +## 向后兼容性 + +现有的 `deepseek` 和 `qwen` 配置完全不受影响,可以继续使用: + +```json +{ + "ai_model": "deepseek", + "deepseek_key": "sk-xxxxx" +} +``` + +或 + +```json +{ + "ai_model": "qwen", + "qwen_key": "sk-xxxxx" +} +``` diff --git a/config.json.example b/config.json.example index 60a494ea..d6865ec5 100644 --- a/config.json.example +++ b/config.json.example @@ -21,6 +21,19 @@ "qwen_key": "your_qwen_api_key", "initial_balance": 1000, "scan_interval_minutes": 3 + }, + { + "id": "binance_custom", + "name": "Binance Custom API Trader", + "ai_model": "custom", + "exchange": "binance", + "binance_api_key": "your_binance_api_key", + "binance_secret_key": "your_binance_secret_key", + "custom_api_url": "https://api.openai.com/v1", + "custom_api_key": "sk-your-api-key", + "custom_model_name": "gpt-4o", + "initial_balance": 1000, + "scan_interval_minutes": 3 } ], "leverage": { diff --git a/config/config.go b/config/config.go index 2e26c925..bed19a3b 100644 --- a/config/config.go +++ b/config/config.go @@ -28,6 +28,11 @@ type TraderConfig struct { QwenKey string `json:"qwen_key,omitempty"` DeepSeekKey string `json:"deepseek_key,omitempty"` + // 自定义AI API配置(支持任何OpenAI格式的API) + CustomAPIURL string `json:"custom_api_url,omitempty"` + CustomAPIKey string `json:"custom_api_key,omitempty"` + CustomModelName string `json:"custom_model_name,omitempty"` + InitialBalance float64 `json:"initial_balance"` ScanIntervalMinutes int `json:"scan_interval_minutes"` } @@ -95,8 +100,8 @@ func (c *Config) Validate() error { if trader.Name == "" { return fmt.Errorf("trader[%d]: Name不能为空", i) } - if trader.AIModel != "qwen" && trader.AIModel != "deepseek" { - return fmt.Errorf("trader[%d]: ai_model必须是 'qwen' 或 'deepseek'", i) + if trader.AIModel != "qwen" && trader.AIModel != "deepseek" && trader.AIModel != "custom" { + return fmt.Errorf("trader[%d]: ai_model必须是 'qwen', 'deepseek' 或 'custom'", i) } // 验证交易平台配置 @@ -124,6 +129,17 @@ func (c *Config) Validate() error { if trader.AIModel == "deepseek" && trader.DeepSeekKey == "" { return fmt.Errorf("trader[%d]: 使用DeepSeek时必须配置deepseek_key", i) } + if trader.AIModel == "custom" { + if trader.CustomAPIURL == "" { + return fmt.Errorf("trader[%d]: 使用自定义API时必须配置custom_api_url", i) + } + if trader.CustomAPIKey == "" { + return fmt.Errorf("trader[%d]: 使用自定义API时必须配置custom_api_key", i) + } + if trader.CustomModelName == "" { + return fmt.Errorf("trader[%d]: 使用自定义API时必须配置custom_model_name", i) + } + } if trader.InitialBalance <= 0 { return fmt.Errorf("trader[%d]: initial_balance必须大于0", i) } diff --git a/decision/engine.go b/decision/engine.go index 21967df9..f6676196 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -55,17 +55,17 @@ type OITopData struct { // Context 交易上下文(传递给AI的完整信息) type Context struct { - CurrentTime string `json:"current_time"` - RuntimeMinutes int `json:"runtime_minutes"` - CallCount int `json:"call_count"` - Account AccountInfo `json:"account"` - Positions []PositionInfo `json:"positions"` - CandidateCoins []CandidateCoin `json:"candidate_coins"` - MarketDataMap map[string]*market.Data `json:"-"` // 不序列化,但内部使用 - OITopDataMap map[string]*OITopData `json:"-"` // OI Top数据映射 - Performance interface{} `json:"-"` // 历史表现分析(logger.PerformanceAnalysis) - BTCETHLeverage int `json:"-"` // BTC/ETH杠杆倍数(从配置读取) - AltcoinLeverage int `json:"-"` // 山寨币杠杆倍数(从配置读取) + CurrentTime string `json:"current_time"` + RuntimeMinutes int `json:"runtime_minutes"` + CallCount int `json:"call_count"` + Account AccountInfo `json:"account"` + Positions []PositionInfo `json:"positions"` + CandidateCoins []CandidateCoin `json:"candidate_coins"` + MarketDataMap map[string]*market.Data `json:"-"` // 不序列化,但内部使用 + OITopDataMap map[string]*OITopData `json:"-"` // OI Top数据映射 + Performance interface{} `json:"-"` // 历史表现分析(logger.PerformanceAnalysis) + BTCETHLeverage int `json:"-"` // BTC/ETH杠杆倍数(从配置读取) + AltcoinLeverage int `json:"-"` // 山寨币杠杆倍数(从配置读取) } // Decision AI的交易决策 @@ -253,7 +253,7 @@ func buildSystemPrompt(accountEquity float64) string { sb.WriteString("- 💰 **资金序列**:成交量序列、持仓量(OI)序列、资金费率\n") sb.WriteString("- 🎯 **筛选标记**:AI500评分 / OI_Top排名(如果有标注)\n\n") sb.WriteString("**分析方法**(完全由你自主决定):\n") - sb.WriteString("- 自由运用序列数据,你可以做趋势分析、形态识别、支撑阻力计算\n") + sb.WriteString("- 自由运用序列数据,你可以做但不限于趋势分析、形态识别、支撑阻力、技术阻力位、斐波那契、波动带计算\n") sb.WriteString("- 多维度交叉验证(价格+量+OI+指标+序列形态)\n") sb.WriteString("- 用你认为最有效的方法发现高确定性机会\n") sb.WriteString("- 综合信心度 ≥ 75 才开仓\n\n") diff --git a/manager/trader_manager.go b/manager/trader_manager.go index 18373a13..cf41570f 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -45,6 +45,9 @@ func (tm *TraderManager) AddTrader(cfg config.TraderConfig, coinPoolURL string, UseQwen: cfg.AIModel == "qwen", DeepSeekKey: cfg.DeepSeekKey, QwenKey: cfg.QwenKey, + CustomAPIURL: cfg.CustomAPIURL, + CustomAPIKey: cfg.CustomAPIKey, + CustomModelName: cfg.CustomModelName, ScanInterval: cfg.GetScanInterval(), InitialBalance: cfg.InitialBalance, BTCETHLeverage: leverage.BTCETHLeverage, // 使用配置的杠杆倍数 diff --git a/mcp/client.go b/mcp/client.go index 55f1fb81..ead27384 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -16,6 +16,7 @@ type Provider string const ( ProviderDeepSeek Provider = "deepseek" ProviderQwen Provider = "qwen" + ProviderCustom Provider = "custom" ) // Config AI API配置 @@ -53,6 +54,15 @@ func SetQwenAPIKey(apiKey, secretKey string) { defaultConfig.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max } +// SetCustomAPI 设置自定义OpenAI兼容API +func SetCustomAPI(apiURL, apiKey, modelName string) { + defaultConfig.Provider = ProviderCustom + defaultConfig.APIKey = apiKey + defaultConfig.BaseURL = apiURL + defaultConfig.Model = modelName + defaultConfig.Timeout = 120 * time.Second +} + // SetConfig 设置完整的AI配置(高级用户) func SetConfig(config Config) { if config.Timeout == 0 { diff --git a/trader/auto_trader.go b/trader/auto_trader.go index c90ceef1..1e6b84a3 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -38,6 +38,11 @@ type AutoTraderConfig struct { DeepSeekKey string QwenKey string + // 自定义AI API配置 + CustomAPIURL string + CustomAPIKey string + CustomModelName string + // 扫描配置 ScanInterval time.Duration // 扫描间隔(建议3分钟) @@ -91,10 +96,16 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) { } // 初始化AI - if config.UseQwen { + if config.AIModel == "custom" { + // 使用自定义API + mcp.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, "") log.Printf("🤖 [%s] 使用阿里云Qwen AI", config.Name) } else { + // 默认使用DeepSeek mcp.SetDeepSeekAPIKey(config.DeepSeekKey) log.Printf("🤖 [%s] 使用DeepSeek AI", config.Name) } diff --git a/web/src/components/ComparisonChart.tsx b/web/src/components/ComparisonChart.tsx index 3c0b5999..4f489458 100644 --- a/web/src/components/ComparisonChart.tsx +++ b/web/src/components/ComparisonChart.tsx @@ -19,24 +19,32 @@ interface ComparisonChartProps { } export function ComparisonChart({ traders }: ComparisonChartProps) { - // 获取所有trader的历史数据 - 修复: 使用固定数量的Hook调用 - // 始终调用最多2个trader的useSWR,即使实际trader数量不同 - const trader1 = traders[0]; - const trader2 = traders[1]; + // 获取所有trader的历史数据 - 使用单个useSWR并发请求所有trader数据 + // 生成唯一的key,当traders变化时会触发重新请求 + const tradersKey = traders.map(t => t.trader_id).sort().join(','); - const history1 = useSWR( - trader1 ? `equity-history-${trader1.trader_id}` : null, - trader1 ? () => api.getEquityHistory(trader1.trader_id) : null, - { refreshInterval: 10000 } + const { data: allTraderHistories, isLoading } = useSWR( + traders.length > 0 ? `all-equity-histories-${tradersKey}` : null, + async () => { + // 并发请求所有trader的历史数据 + const promises = traders.map(trader => + api.getEquityHistory(trader.trader_id) + ); + return Promise.all(promises); + }, + { + refreshInterval: 10000, + revalidateOnFocus: false, + } ); - const history2 = useSWR( - trader2 ? `equity-history-${trader2.trader_id}` : null, - trader2 ? () => api.getEquityHistory(trader2.trader_id) : null, - { refreshInterval: 10000 } - ); - - const traderHistories = [history1, history2].slice(0, traders.length); + // 将数据转换为与原格式兼容的结构 + const traderHistories = useMemo(() => { + if (!allTraderHistories) { + return traders.map(() => ({ data: undefined })); + } + return allTraderHistories.map(data => ({ data })); + }, [allTraderHistories, traders.length]); // 使用useMemo自动处理数据合并,直接使用data对象作为依赖 const combinedData = useMemo(() => { @@ -115,12 +123,7 @@ export function ComparisonChart({ traders }: ComparisonChartProps) { } return combined; - }, [ - traderHistories[0]?.data, - traderHistories[1]?.data, - ]); - - const isLoading = traderHistories.some((h) => !h.data); + }, [allTraderHistories, traders]); if (isLoading) { return (