mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
Merge branch 'tinkle-community:main' into main
This commit is contained in:
+185
@@ -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"
|
||||
}
|
||||
```
|
||||
@@ -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": {
|
||||
|
||||
+18
-2
@@ -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)
|
||||
}
|
||||
|
||||
+12
-12
@@ -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")
|
||||
|
||||
@@ -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, // 使用配置的杠杆倍数
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+12
-1
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user