From a7c276fe3c9dbddc1c13aa2ac62bba6b076ae3cc Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Wed, 29 Oct 2025 21:42:34 +0800 Subject: [PATCH 1/3] Fix: Support dynamic number of traders in ComparisonChart Previously, ComparisonChart was hardcoded to only fetch equity history data for the first 2 traders, causing the 3rd and subsequent traders' data to not be displayed on the chart. Changes: - Replaced multiple individual useSWR calls with single consolidated call - Use Promise.all() to fetch all traders' equity data concurrently - Generate dynamic cache key based on all trader IDs - Maintain backward compatibility with existing component structure - Update useMemo dependencies to properly track data changes This fix allows the comparison chart to properly display any number of competing traders, not just 2. Co-Authored-By: tinkle-community --- web/src/components/ComparisonChart.tsx | 45 ++++++++++++++------------ 1 file changed, 24 insertions(+), 21 deletions(-) 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 ( From 3735df24dc0df63c7ef95f9232d4e44bd599f42a Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Wed, 29 Oct 2025 22:05:58 +0800 Subject: [PATCH 2/3] Refactor: Improve AI prompt with more technical analysis methods Changes to decision/engine.go: - Clean up Context struct field alignment for better readability - Enhance system prompt to include more technical analysis methods: * Added: technical resistance levels, Fibonacci, volatility bands * Changed wording from "you can do X" to "you can do but not limited to X" to encourage AI to use broader range of analysis techniques This gives the AI decision engine more explicit guidance on available technical analysis tools while maintaining flexibility. Co-Authored-By: tinkle-community --- decision/engine.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) 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") From a13f39afdd79e9b366b9c401ab33e20746184bbf Mon Sep 17 00:00:00 2001 From: btcman <109521734+manwallet@users.noreply.github.com> Date: Wed, 29 Oct 2025 22:48:28 +0800 Subject: [PATCH 3/3] Feature: Add support for custom OpenAI-compatible API This update enables users to configure any OpenAI-compatible API endpoint, allowing the use of: - OpenAI official API (GPT-4, GPT-4o, etc.) - OpenRouter (access to multiple models) - Local deployed models (Ollama, LM Studio, etc.) - Other OpenAI-format compatible API services Changes: - config: Add custom_api_url, custom_api_key, custom_model_name fields - mcp: Add SetCustomAPI function and ProviderCustom constant - trader: Update AI initialization logic to support custom API - manager: Pass custom API config to trader instances - Add CUSTOM_API.md documentation with usage examples - Update config.json.example with custom API sample Co-Authored-By: tinkle-community --- CUSTOM_API.md | 185 ++++++++++++++++++++++++++++++++++++++ config.json.example | 13 +++ config/config.go | 20 ++++- manager/trader_manager.go | 3 + mcp/client.go | 10 +++ trader/auto_trader.go | 13 ++- 6 files changed, 241 insertions(+), 3 deletions(-) create mode 100644 CUSTOM_API.md 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/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) }