diff --git a/.dockerignore b/.dockerignore index 9d3ea1e6..9b9ec670 100644 --- a/.dockerignore +++ b/.dockerignore @@ -40,8 +40,9 @@ coin_pool_cache/ # Config files (should be mounted) config.json -# Web directory (has its own Dockerfile) -web/ +# Web build artifacts (but include source for multi-stage build) +web/node_modules/ +web/dist/ # Temporary files tmp/ diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..bcff8c82 --- /dev/null +++ b/.env.example @@ -0,0 +1,13 @@ +# NOFX Environment Variables Template +# Copy this file to .env and modify the values as needed + +# Ports Configuration +# Backend API server port (internal: 8080, external: configurable) +NOFX_BACKEND_PORT=8080 + +# Frontend web interface port (Nginx listens on port 80 internally) +NOFX_FRONTEND_PORT=3000 + +# Timezone Setting +# System timezone for container time synchronization +NOFX_TIMEZONE=Asia/Shanghai diff --git a/.gitignore b/.gitignore index 384b3f41..d501f8dd 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ Thumbs.db *.log *.tmp *.bak +*.backup # 环境变量 .env diff --git a/CUSTOM_API.md b/CUSTOM_API.md new file mode 100644 index 00000000..e08b25e8 --- /dev/null +++ b/CUSTOM_API.md @@ -0,0 +1,205 @@ +# 自定义 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`)。特殊用法:如果以 `#` 结尾,则使用完整 URL(不自动添加路径) | +| `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" +} +``` + +### 5. 使用完整自定义路径(末尾添加 #) + +对于某些特殊的 API 端点,如果已经包含完整路径(包括 `/chat/completions` 或其他自定义路径),可以在 URL 末尾添加 `#` 来强制使用完整 URL: + +```json +{ + "ai_model": "custom", + "custom_api_url": "https://api.example.com/v2/ai/chat/completions#", + "custom_api_key": "your-api-key", + "custom_model_name": "custom-model" +} +``` + +**注意**:`#` 会被自动去除,实际请求会发送到 `https://api.example.com/v2/ai/chat/completions` + +## 兼容性要求 + +自定义 API 必须: +1. 支持 OpenAI Chat Completions 格式 +2. 接受 `POST` 请求到 `/chat/completions` 端点(或在 URL 末尾添加 `#` 以使用自定义路径) +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` + - 🔧 **特殊用法**:如果需要使用完整的自定义路径(不自动添加 `/chat/completions`),可以在 URL 末尾添加 `#` + - 例如:`https://api.example.com/custom/path/chat/completions#` + - 系统会自动去掉 `#` 并直接使用该完整 URL + +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`(系统会自动添加) + - 特殊用法:如果需要完整路径,记得在 URL 末尾添加 `#` +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/DOCKER_DEPLOY.en.md b/DOCKER_DEPLOY.en.md index 1b9c42f2..bf8adf63 100644 --- a/DOCKER_DEPLOY.en.md +++ b/DOCKER_DEPLOY.en.md @@ -15,22 +15,33 @@ Before you begin, ensure your system has: Download and install [Docker Desktop](https://www.docker.com/products/docker-desktop/) #### Linux (Ubuntu/Debian) + +> #### Docker Compose Version Notes +> +> **New User Recommendation:** +> - **Use Docker Desktop**: Automatically includes latest Docker Compose, no separate installation needed +> - Simple installation, one-click setup, provides GUI management +> - Supports macOS, Windows, and some Linux distributions +> +> **Upgrading User Note:** +> - **Deprecating standalone docker-compose**: No longer recommended to download the independent Docker Compose binary +> - **Use built-in version**: Docker 20.10+ includes `docker compose` command (with space) +> - If still using old `docker-compose`, please upgrade to new syntax + +*Recommended: Use Docker Desktop (if available) or Docker CE with built-in Compose* + ```bash -# Install Docker +# Install Docker (includes compose) curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh -# Install Docker Compose -sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose -sudo chmod +x /usr/local/bin/docker-compose - -# Add current user to docker group +# Add user to docker group sudo usermod -aG docker $USER newgrp docker -# Verify installation +# Verify installation (new command) docker --version -docker-compose --version +docker compose --version # Docker 24+ includes this, no separate installation needed ``` ## 🚀 Quick Start (3 Steps) @@ -69,10 +80,10 @@ nano config.json # or use any other editor ```bash # Build and start all services (first run) -docker-compose up -d --build +docker compose up -d --build # Subsequent starts (without rebuilding) -docker-compose up -d +docker compose up -d ``` **Startup options:** @@ -91,49 +102,49 @@ Once deployed, open your browser and visit: ### View Running Status ```bash # View all container status -docker-compose ps +docker compose ps # View service health status -docker-compose ps --format json | jq +docker compose ps --format json | jq ``` ### View Logs ```bash # View all service logs -docker-compose logs -f +docker compose logs -f # View backend logs only -docker-compose logs -f backend +docker compose logs -f backend # View frontend logs only -docker-compose logs -f frontend +docker compose logs -f frontend # View last 100 lines -docker-compose logs --tail=100 +docker compose logs --tail=100 ``` ### Stop Services ```bash # Stop all services (keep data) -docker-compose stop +docker compose stop # Stop and remove containers (keep data) -docker-compose down +docker compose down # Stop and remove containers and volumes (clear all data) -docker-compose down -v +docker compose down -v ``` ### Restart Services ```bash # Restart all services -docker-compose restart +docker compose restart # Restart backend only -docker-compose restart backend +docker compose restart backend # Restart frontend only -docker-compose restart frontend +docker compose restart frontend ``` ### Update Services @@ -142,7 +153,7 @@ docker-compose restart frontend git pull # Rebuild and restart -docker-compose up -d --build +docker compose up -d --build ``` ## 🔧 Advanced Configuration @@ -226,14 +237,14 @@ tar -xzf backup_20241029.tar.gz ```bash # View detailed error messages -docker-compose logs backend -docker-compose logs frontend +docker compose logs backend +docker compose logs frontend # Check container status -docker-compose ps -a +docker compose ps -a # Rebuild (clear cache) -docker-compose build --no-cache +docker compose build --no-cache ``` ### Port Already in Use @@ -273,10 +284,10 @@ curl http://localhost:3000/health ```bash # Check network connectivity -docker-compose exec frontend ping backend +docker compose exec frontend ping backend # Check if backend service is running -docker-compose exec frontend wget -O- http://backend:8080/health +docker compose exec frontend wget -O- http://backend:8080/health ``` ### Clean Docker Resources @@ -321,8 +332,8 @@ docker system prune -a --volumes 4. **Regularly update images** ```bash - docker-compose pull - docker-compose up -d + docker compose pull + docker compose up -d ``` ## 🌐 Production Deployment @@ -391,7 +402,7 @@ logging: max-file: "3" # View log statistics -docker-compose logs --timestamps | wc -l +docker compose logs --timestamps | wc -l ``` ### Monitoring Tool Integration @@ -424,28 +435,28 @@ services: ```bash # Start -docker-compose up -d --build # Build and start -docker-compose up -d # Start (without rebuilding) +docker compose up -d --build # Build and start +docker compose up -d # Start (without rebuilding) # Stop -docker-compose stop # Stop services -docker-compose down # Stop and remove containers -docker-compose down -v # Stop and remove containers and data +docker compose stop # Stop services +docker compose down # Stop and remove containers +docker compose down -v # Stop and remove containers and data # View -docker-compose ps # View status -docker-compose logs -f # View logs -docker-compose top # View processes +docker compose ps # View status +docker compose logs -f # View logs +docker compose top # View processes # Restart -docker-compose restart # Restart all services -docker-compose restart backend # Restart backend +docker compose restart # Restart all services +docker compose restart backend # Restart backend # Update -git pull && docker-compose up -d --build +git pull && docker compose up -d --build # Clean -docker-compose down -v # Clear all data +docker compose down -v # Clear all data docker system prune -a # Clean Docker resources ``` diff --git a/DOCKER_DEPLOY.md b/DOCKER_DEPLOY.md index f48b005e..536ee159 100644 --- a/DOCKER_DEPLOY.md +++ b/DOCKER_DEPLOY.md @@ -11,26 +11,42 @@ ### 安装 Docker +> #### 提示:Docker Compose 版本说明 +> +> **新用户建议**: +> - **推荐使用 Docker Desktop**:自动包含最新 Docker Compose,无需单独安装 +> - 安装简单,一键搞定,提供图形界面管理 +> - 支持 macOS、Windows、部分 Linux 发行版 +> +> **旧用户提醒**: +> - **弃用独立 docker-compose**:不再推荐下载独立的 Docker Compose 二进制文件 +> - **使用内置版**:Docker 20.10+ 自带 `docker compose` 命令(注意是空格) +> - 如果还在使用旧的 `docker-compose`,请升级到新语法 + #### macOS / Windows 下载并安装 [Docker Desktop](https://www.docker.com/products/docker-desktop/) -#### Linux (Ubuntu/Debian) +**安装后验证:** ```bash -# 安装 Docker +docker --version +docker compose --version # 注意:使用空格,不再是连字符 +``` + +#### Linux (Ubuntu/Debian) +**推荐方式:使用 Docker Desktop(如果可用)或 Docker CE** + +```bash +# 安装 Docker (自动包含 compose) curl -fsSL https://get.docker.com -o get-docker.sh sudo sh get-docker.sh -# 安装 Docker Compose -sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose -sudo chmod +x /usr/local/bin/docker-compose - # 将当前用户加入 docker 组 sudo usermod -aG docker $USER newgrp docker -# 验证安装 +# 验证安装(新命令) docker --version -docker-compose --version +docker compose --version # Docker 24+ 自带,无需单独安装 ``` ## 🚀 快速开始(3步完成部署) @@ -69,10 +85,10 @@ nano config.json # 或使用其他编辑器 ```bash # 构建并启动所有服务(首次运行) -docker-compose up -d --build +docker compose up -d --build # 后续启动(不重新构建) -docker-compose up -d +docker compose up -d ``` **启动过程说明:** @@ -91,49 +107,49 @@ docker-compose up -d ### 查看运行状态 ```bash # 查看所有容器状态 -docker-compose ps +docker compose ps # 查看服务健康状态 -docker-compose ps --format json | jq +docker compose ps --format json | jq ``` ### 查看日志 ```bash # 查看所有服务日志 -docker-compose logs -f +docker compose logs -f # 只查看后端日志 -docker-compose logs -f backend +docker compose logs -f backend # 只查看前端日志 -docker-compose logs -f frontend +docker compose logs -f frontend # 查看最近 100 行日志 -docker-compose logs --tail=100 +docker compose logs --tail=100 ``` ### 停止服务 ```bash # 停止所有服务(保留数据) -docker-compose stop +docker compose stop # 停止并删除容器(保留数据) -docker-compose down +docker compose down # 停止并删除容器和卷(清除所有数据) -docker-compose down -v +docker compose down -v ``` ### 重启服务 ```bash # 重启所有服务 -docker-compose restart +docker compose restart # 只重启后端 -docker-compose restart backend +docker compose restart backend # 只重启前端 -docker-compose restart frontend +docker compose restart frontend ``` ### 更新服务 @@ -142,7 +158,7 @@ docker-compose restart frontend git pull # 重新构建并重启 -docker-compose up -d --build +docker compose up -d --build ``` ## 🔧 高级配置 @@ -226,14 +242,14 @@ tar -xzf backup_20241029.tar.gz ```bash # 查看详细错误信息 -docker-compose logs backend -docker-compose logs frontend +docker compose logs backend +docker compose logs frontend # 检查容器状态 -docker-compose ps -a +docker compose ps -a # 重新构建(清除缓存) -docker-compose build --no-cache +docker compose build --no-cache ``` ### 端口被占用 @@ -273,10 +289,10 @@ curl http://localhost:3000/health ```bash # 检查网络连接 -docker-compose exec frontend ping backend +docker compose exec frontend ping backend # 检查后端服务是否正常 -docker-compose exec frontend wget -O- http://backend:8080/health +docker compose exec frontend wget -O- http://backend:8080/health ``` ### 清理 Docker 资源 @@ -321,8 +337,8 @@ docker system prune -a --volumes 4. **定期更新镜像** ```bash - docker-compose pull - docker-compose up -d + docker compose pull + docker compose up -d ``` ## 🌐 生产环境部署 @@ -391,7 +407,7 @@ logging: max-file: "3" # 查看日志统计 -docker-compose logs --timestamps | wc -l +docker compose logs --timestamps | wc -l ``` ### 监控工具集成 @@ -424,28 +440,28 @@ services: ```bash # 启动 -docker-compose up -d --build # 构建并启动 -docker-compose up -d # 启动(不重新构建) +docker compose up -d --build # 构建并启动 +docker compose up -d # 启动(不重新构建) # 停止 -docker-compose stop # 停止服务 -docker-compose down # 停止并删除容器 -docker-compose down -v # 停止并删除容器和数据 +docker compose stop # 停止服务 +docker compose down # 停止并删除容器 +docker compose down -v # 停止并删除容器和数据 # 查看 -docker-compose ps # 查看状态 -docker-compose logs -f # 查看日志 -docker-compose top # 查看进程 +docker compose ps # 查看状态 +docker compose logs -f # 查看日志 +docker compose top # 查看进程 # 重启 -docker-compose restart # 重启所有服务 -docker-compose restart backend # 重启后端 +docker compose restart # 重启所有服务 +docker compose restart backend # 重启后端 # 更新 -git pull && docker-compose up -d --build +git pull && docker compose up -d --build # 清理 -docker-compose down -v # 清除所有数据 +docker compose down -v # 清除所有数据 docker system prune -a # 清理 Docker 资源 ``` diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 25c389c1..00000000 --- a/Dockerfile +++ /dev/null @@ -1,59 +0,0 @@ -# 构建阶段 -FROM golang:1.25-alpine AS builder - -# 安装必要的构建工具 -RUN apk add --no-cache git gcc musl-dev - -# 设置工作目录 -WORKDIR /app - -# 复制 go mod 文件 -COPY go.mod go.sum ./ - -# 下载依赖 -RUN go mod download - -# 复制源代码 -COPY . . - -# 构建应用 -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o nofx . - -# 运行阶段 -FROM alpine:latest - -# 安装 ca-certificates(HTTPS 请求需要) -RUN apk --no-cache add ca-certificates tzdata - -# 设置时区为上海 -ENV TZ=Asia/Shanghai - -# 创建非 root 用户 -RUN addgroup -g 1000 nofx && \ - adduser -D -u 1000 -G nofx nofx - -# 设置工作目录 -WORKDIR /app - -# 从构建阶段复制二进制文件 -COPY --from=builder /app/nofx . - -# 复制配置文件示例 -COPY config.json.example ./config.json.example - -# 创建必要的目录 -RUN mkdir -p decision_logs coin_pool_cache && \ - chown -R nofx:nofx /app - -# 切换到非 root 用户 -USER nofx - -# 暴露 API 端口 -EXPOSE 8080 - -# 健康检查 -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 - -# 启动应用 -CMD ["./nofx"] diff --git a/PM2_DEPLOYMENT.md b/PM2_DEPLOYMENT.md new file mode 100644 index 00000000..79af7e21 --- /dev/null +++ b/PM2_DEPLOYMENT.md @@ -0,0 +1,300 @@ +# NoFX Trading Bot - PM2 部署指南 + +使用 PM2 进行本地开发和生产部署的完整指南。 + +## 🚀 快速开始 + +### 1. 安装 PM2 + +```bash +npm install -g pm2 +``` + +### 2. 一键启动 + +```bash +./pm2.sh start +``` + +就这么简单!前后端将自动启动。 + +--- + +## 📋 所有命令 + +### 服务管理 + +```bash +# 启动服务 +./pm2.sh start + +# 停止服务 +./pm2.sh stop + +# 重启服务 +./pm2.sh restart + +# 查看状态 +./pm2.sh status + +# 删除服务 +./pm2.sh delete +``` + +### 日志查看 + +```bash +# 查看所有日志(实时) +./pm2.sh logs + +# 只看后端日志 +./pm2.sh logs backend + +# 只看前端日志 +./pm2.sh logs frontend +``` + +### 构建与编译 + +```bash +# 编译后端 +./pm2.sh build + +# 重新编译后端并重启 +./pm2.sh rebuild +``` + +### 监控 + +```bash +# 打开 PM2 监控面板(实时CPU/内存) +./pm2.sh monitor +``` + +--- + +## 📊 访问地址 + +启动成功后: + +- **前端 Web 界面**: http://localhost:3000 +- **后端 API**: http://localhost:8080 +- **健康检查**: http://localhost:8080/health + +--- + +## 🔧 配置文件 + +### pm2.config.js + +PM2 配置文件,定义了前后端的启动参数: + +```javascript +const path = require('path'); + +module.exports = { + apps: [ + { + name: 'nofx-backend', + script: './nofx', // Go 二进制文件 + cwd: __dirname, // 动态获取当前目录 + autorestart: true, + max_memory_restart: '500M' + }, + { + name: 'nofx-frontend', + script: 'npm', + args: 'run dev', // Vite 开发服务器 + cwd: path.join(__dirname, 'web'), // 动态拼接路径 + autorestart: true, + max_memory_restart: '300M' + } + ] +}; +``` + +**修改配置后需要重启:** +```bash +./pm2.sh restart +``` + +--- + +## 📝 日志文件位置 + +- **后端日志**: `./logs/backend-error.log` 和 `./logs/backend-out.log` +- **前端日志**: `./web/logs/frontend-error.log` 和 `./web/logs/frontend-out.log` + +--- + +## 🔄 开机自启动 + +设置 PM2 开机自启动: + +```bash +# 1. 启动服务 +./pm2.sh start + +# 2. 保存当前进程列表 +pm2 save + +# 3. 生成启动脚本 +pm2 startup + +# 4. 按照提示执行命令(需要 sudo) +``` + +**取消开机自启动:** +```bash +pm2 unstartup +``` + +--- + +## 🛠️ 常见操作 + +### 修改代码后重启 + +**后端修改:** +```bash +./pm2.sh rebuild # 自动编译并重启 +``` + +**前端修改:** +```bash +./pm2.sh restart # Vite 会自动热重载,无需重启 +``` + +### 查看实时资源占用 + +```bash +./pm2.sh monitor +``` + +### 查看详细信息 + +```bash +pm2 info nofx-backend # 后端详情 +pm2 info nofx-frontend # 前端详情 +``` + +### 清空日志 + +```bash +pm2 flush +``` + +--- + +## 🐛 故障排查 + +### 服务启动失败 + +```bash +# 1. 查看详细错误 +./pm2.sh logs + +# 2. 检查端口占用 +lsof -i :8080 # 后端端口 +lsof -i :3000 # 前端端口 + +# 3. 手动编译测试 +go build -o nofx +./nofx +``` + +### 后端无法启动 + +```bash +# 检查 config.json 是否存在 +ls -l config.json + +# 检查权限 +chmod +x nofx + +# 手动运行看报错 +./nofx +``` + +### 前端无法访问 + +```bash +# 检查 node_modules +cd web && npm install + +# 手动启动测试 +npm run dev +``` + +--- + +## 🎯 生产环境建议 + +### 1. 使用生产模式 + +修改 `pm2.config.js`: + +```javascript +{ + name: 'nofx-frontend', + script: 'npm', + args: 'run preview', // 改为 preview(需先 npm run build) + env: { + NODE_ENV: 'production' + } +} +``` + +### 2. 增加实例数(负载均衡) + +```javascript +{ + name: 'nofx-backend', + script: './nofx', + instances: 2, // 启动 2 个实例 + exec_mode: 'cluster' +} +``` + +### 3. 自动重启策略 + +```javascript +{ + autorestart: true, + max_restarts: 10, + min_uptime: '10s', + max_memory_restart: '500M' +} +``` + +--- + +## 📦 与 Docker 部署的对比 + +| 特性 | PM2 部署 | Docker 部署 | +|------|---------|------------| +| 启动速度 | ⚡ 快 | 🐌 较慢 | +| 资源占用 | 💚 低 | 🟡 中等 | +| 隔离性 | 🟡 中等 | 💚 高 | +| 适用场景 | 开发/单机 | 生产/集群 | +| 配置复杂度 | 💚 简单 | 🟡 中等 | + +**建议:** +- **开发环境**: 使用 `./pm2.sh` +- **生产环境**: 使用 `./start.sh` (Docker) + +--- + +## 🆘 获取帮助 + +```bash +./pm2.sh help +``` + +或查看 PM2 官方文档:https://pm2.keymetrics.io/ + +--- + +## 📄 License + +MIT diff --git a/README.md b/README.md index 2cf5671b..8bd3d5e1 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ --- -A modern automated crypto futures trading platform powered by **DeepSeek/Qwen AI**, supporting **Binance and Hyperliquid exchanges**. Create and manage multiple AI traders with dynamic configuration through a web interface. Features comprehensive market analysis, AI decision-making, and professional monitoring dashboard. +A modern automated crypto futures trading platform powered by **DeepSeek/Qwen AI**, supporting **Binance, Hyperliquid, and Aster DEX exchanges**. Create and manage multiple AI traders with dynamic configuration through a web interface. Features comprehensive market analysis, AI decision-making, **multi-AI model live trading competition**, **self-learning mechanism**, and professional monitoring dashboard. > ⚠️ **Risk Warning**: This system is experimental. AI auto-trading carries significant risks. Strongly recommended for learning/research purposes or testing with small amounts only! @@ -26,7 +26,15 @@ Join our Telegram developer community to discuss, share ideas, and get support: ### 🚀 Complete System Transformation - Web-Based Configuration! -NOFX has been **completely transformed** from a static config-based system to a **dynamic web-based trading platform**! +NOFX has been **completely transformed** from a static config-based system to a **dynamic web-based trading platform** with **multi-exchange support**! + +#### **Multi-Exchange Support** + +NOFX now supports **three major exchanges**: Binance, Hyperliquid, and Aster DEX! + +#### **Hyperliquid Exchange** + +A high-performance decentralized perpetual futures exchange! **Major Changes:** - ✅ **Web-Based Configuration**: Create and manage AI traders through a modern web interface @@ -50,6 +58,42 @@ NOFX has been **completely transformed** from a static config-based system to a See [Quick Start](#-quick-start) for the new setup process! +#### **Aster DEX Exchange** (NEW! v2.0.2) + +A Binance-compatible decentralized perpetual futures exchange! + +**Key Features:** +- ✅ Binance-style API (easy migration from Binance) +- ✅ Web3 wallet authentication (secure and decentralized) +- ✅ Full trading support with automatic precision handling +- ✅ Lower trading fees than CEX +- ✅ EVM-compatible (Ethereum, BSC, Polygon, etc.) + +**Why Aster?** +- 🎯 **Binance-compatible API** - minimal code changes required +- 🔐 **API Wallet System** - separate trading wallet for security +- 💰 **Competitive fees** - lower than most centralized exchanges +- 🌐 **Multi-chain support** - trade on your preferred EVM chain + +**Quick Start:** +1. Visit [Aster API Wallet](https://www.asterdex.com/en/api-wallet) +2. Connect your main wallet and create an API wallet +3. Copy the API Signer address and Private Key +4. Set `"exchange": "aster"` in config.json +5. Add `"aster_user"`, `"aster_signer"`, and `"aster_private_key"` + +--- + +## 📸 Screenshots + +### 🏆 Competition Mode - Real-time AI Battle +![Competition Page](screenshots/competition-page.png) +*Multi-AI leaderboard with real-time performance comparison charts showing Qwen vs DeepSeek live trading battle* + +### 📊 Trader Details - Complete Trading Dashboard +![Details Page](screenshots/details-page.png) +*Professional trading interface with equity curves, live positions, and AI decision logs with expandable input prompts & chain-of-thought reasoning* + --- ## ✨ Core Features @@ -81,7 +125,11 @@ See [Quick Start](#-quick-start) for the new setup process! - **Per-Coin Position Limit**: - Altcoins ≤ 1.5x account equity - BTC/ETH ≤ 10x account equity -- **Fixed Leverage**: Altcoins 20x | BTC/ETH 50x +- **Configurable Leverage** (v2.0.3+): + - Set maximum leverage in config.json + - Default: 5x for all coins (safe for subaccounts) + - Main accounts can increase: Altcoins up to 20x, BTC/ETH up to 50x + - ⚠️ Binance subaccounts restricted to ≤5x leverage - **Margin Management**: Total usage ≤90%, AI autonomous decision on usage rate - **Risk-Reward Ratio**: Mandatory ≥1:2 (stop-loss:take-profit) - **Prevent Position Stacking**: No duplicate opening of same coin/direction @@ -209,8 +257,14 @@ Docker automatically handles all dependencies (Go, Node.js, TA-Lib, SQLite) and chmod +x start.sh ./start.sh start --build -# Option 2: Use docker-compose directly -docker-compose up -d --build +> #### Docker Compose Version Notes +> +> **This project uses Docker Compose V2 syntax (with spaces)** +> +> If you have the older standalone `docker-compose` installed, please upgrade to Docker Desktop or Docker 20.10+ + +# Option 2: Use docker compose directly +docker compose up -d --build ``` #### Step 2: Access Web Interface @@ -439,6 +493,71 @@ Open your browser and visit: **🌐 http://localhost:3000** --- +#### 🔶 Alternative: Using Aster DEX Exchange + +**NOFX also supports Aster DEX** - a Binance-compatible decentralized perpetual futures exchange! + +**Why Choose Aster?** +- 🎯 Binance-compatible API (easy migration) +- 🔐 API Wallet security system +- 💰 Lower trading fees +- 🌐 Multi-chain support (ETH, BSC, Polygon) +- 🌍 No KYC required + +**Step 1**: Create Aster API Wallet + +1. Visit [Aster API Wallet](https://www.asterdex.com/en/api-wallet) +2. Connect your main wallet (MetaMask, WalletConnect, etc.) +3. Click "Create API Wallet" +4. **Save these 3 items immediately:** + - Main Wallet address (User) + - API Wallet address (Signer) + - API Wallet Private Key (⚠️ shown only once!) + +**Step 2**: Configure `config.json` for Aster + +```json +{ + "traders": [ + { + "id": "aster_deepseek", + "name": "Aster DeepSeek Trader", + "ai_model": "deepseek", + "exchange": "aster", + + "aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e", + "aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0", + "aster_private_key": "4fd0a42218f3eae43a6ce26d22544e986139a01e5b34a62db53757ffca81bae1", + + "deepseek_key": "sk-xxxxxxxxxxxxx", + "initial_balance": 1000.0, + "scan_interval_minutes": 3 + } + ], + "use_default_coins": true, + "api_server_port": 8080, + "leverage": { + "btc_eth_leverage": 5, + "altcoin_leverage": 5 + } +} +``` + +**Key Configuration Fields:** +- `"exchange": "aster"` - Set exchange to Aster +- `aster_user` - Your main wallet address +- `aster_signer` - API wallet address (from Step 1) +- `aster_private_key` - API wallet private key (without `0x` prefix) + +**📖 For detailed setup instructions, see**: [Aster Integration Guide](ASTER_INTEGRATION.md) + +**⚠️ Security Notes**: +- API wallet is separate from your main wallet (extra security layer) +- Never share your API private key +- You can revoke API wallet access anytime at [asterdex.com](https://www.asterdex.com/en/api-wallet) + +--- + #### ⚔️ Expert Mode: Multi-Trader Competition For running multiple AI traders competing against each other: @@ -499,6 +618,9 @@ For running multiple AI traders competing against each other: | `qwen_key` | Qwen API key | `"sk-xxx"` | If using Qwen | | `initial_balance` | Starting balance for P/L calculation | `1000.0` | ✅ Yes | | `scan_interval_minutes` | How often to make decisions | `3` (3-5 recommended) | ✅ Yes | +| **`leverage`** | **Leverage configuration (v2.0.3+)** | See below | ✅ Yes | +| `btc_eth_leverage` | Maximum leverage for BTC/ETH
⚠️ Subaccounts: ≤5x | `5` (default, safe)
`50` (main account max) | ✅ Yes | +| `altcoin_leverage` | Maximum leverage for altcoins
⚠️ Subaccounts: ≤5x | `5` (default, safe)
`20` (main account max) | ✅ Yes | | `use_default_coins` | Use built-in coin list
**✨ Smart Default: `true`** (v2.0.2+)
Auto-enabled if no API URL provided | `true` or omit | ❌ No
(Optional, auto-defaults) | | `coin_pool_api_url` | Custom coin pool API
*Only needed when `use_default_coins: false`* | `""` (empty) | ❌ No | | `oi_top_api_url` | Open interest API
*Optional supplement data* | `""` (empty) | ❌ No | @@ -509,6 +631,63 @@ For running multiple AI traders competing against each other: --- +#### ⚙️ Leverage Configuration (v2.0.3+) + +**What is leverage configuration?** + +The leverage settings control the maximum leverage the AI can use for each trade. This is crucial for risk management, especially for Binance subaccounts which have leverage restrictions. + +**Configuration format:** + +```json +"leverage": { + "btc_eth_leverage": 5, // Maximum leverage for BTC and ETH + "altcoin_leverage": 5 // Maximum leverage for all other coins +} +``` + +**⚠️ Important: Binance Subaccount Restrictions** + +- **Subaccounts**: Limited to **≤5x leverage** by Binance +- **Main accounts**: Can use up to 20x (altcoins) or 50x (BTC/ETH) +- If you're using a subaccount and set leverage >5x, trades will **fail** with error: `Subaccounts are restricted from using leverage greater than 5x` + +**Recommended settings:** + +| Account Type | BTC/ETH Leverage | Altcoin Leverage | Risk Level | +|-------------|------------------|------------------|------------| +| **Subaccount** | `5` | `5` | ✅ Safe (default) | +| **Main (Conservative)** | `10` | `10` | 🟡 Medium | +| **Main (Aggressive)** | `20` | `15` | 🔴 High | +| **Main (Maximum)** | `50` | `20` | 🔴🔴 Very High | + +**Examples:** + +**Safe configuration (subaccount or conservative):** +```json +"leverage": { + "btc_eth_leverage": 5, + "altcoin_leverage": 5 +} +``` + +**Aggressive configuration (main account only):** +```json +"leverage": { + "btc_eth_leverage": 20, + "altcoin_leverage": 15 +} +``` + +**How AI uses leverage:** + +- AI can choose **any leverage from 1x up to your configured maximum** +- For example, with `altcoin_leverage: 20`, AI might decide to use 5x, 10x, or 20x based on market conditions +- The configuration sets the **upper limit**, not a fixed value +- AI considers volatility, risk-reward ratio, and account balance when choosing leverage + +--- + #### ⚠️ Important: `use_default_coins` Field **Smart Default Behavior (v2.0.2+):** @@ -1096,6 +1275,19 @@ This version fixes **critical calculation errors** in the historical trade recor **Recommendation**: If you were running the system before this update, your historical statistics were inaccurate. After updating to v2.0.2, new trades will be calculated correctly. +### v2.0.2 (2025-10-29) + +**Bug Fixes:** +- ✅ Fixed Aster exchange precision error (code -1111: "Precision is over the maximum defined for this asset") +- ✅ Improved price and quantity formatting to match exchange precision requirements +- ✅ Added detailed precision processing logs for debugging +- ✅ Enhanced all order functions (OpenLong, OpenShort, CloseLong, CloseShort, SetStopLoss, SetTakeProfit) with proper precision handling + +**Technical Details:** +- Added `formatFloatWithPrecision` function to convert float64 to strings with correct precision +- Price and quantity parameters are now formatted according to exchange's `pricePrecision` and `quantityPrecision` specifications +- Trailing zeros are removed from formatted values to optimize API requests + ### v2.0.1 (2025-10-29) **Bug Fixes:** diff --git a/README.ru.md b/README.ru.md index f9737148..51d8054a 100644 --- a/README.ru.md +++ b/README.ru.md @@ -9,7 +9,7 @@ --- -Автоматизированная система торговли фьючерсами Binance на базе **DeepSeek/Qwen AI**, поддерживающая **конкуренцию нескольких AI-моделей в реальной торговле**, с полным анализом рынка, принятием решений AI, **механизмом самообучения** и профессиональным веб-интерфейсом мониторинга. +Автоматизированная система торговли криптовалютными фьючерсами на базе **DeepSeek/Qwen AI**, поддерживающая **Binance, Hyperliquid и Aster DEX биржи**, **конкуренцию нескольких AI-моделей в реальной торговле**, с полным анализом рынка, принятием решений AI, **механизмом самообучения** и профессиональным веб-интерфейсом мониторинга. > ⚠️ **Предупреждение о рисках**: Эта система экспериментальная. Автоматическая торговля с AI несет значительные риски. Настоятельно рекомендуется использовать только для обучения/исследований или тестирования с небольшими суммами! @@ -21,6 +21,75 @@ --- +## 🆕 Последние обновления + +### 🚀 Поддержка нескольких бирж! + +NOFX теперь поддерживает **три основные биржи**: Binance, Hyperliquid и Aster DEX! + +#### **Биржа Hyperliquid** + +Высокопроизводительная децентрализованная биржа бессрочных фьючерсов! + +**Ключевые особенности:** +- ✅ Полная поддержка торговли (лонг/шорт, плечо, стоп-лосс/тейк-профит) +- ✅ Автоматическая обработка точности (размер и цена ордера) +- ✅ Единый интерфейс трейдера (бесшовное переключение бирж) +- ✅ Поддержка мейннета и тестнета +- ✅ Не нужны API ключи - только приватный ключ Ethereum + +**Почему Hyperliquid?** +- 🔥 Более низкие комиссии чем на централизованных биржах +- 🔒 Без хранения - вы контролируете свои средства +- ⚡ Быстрое исполнение с расчетом на цепи +- 🌍 Не нужна KYC + +**Быстрый старт:** +1. Получите приватный ключ MetaMask (удалите префикс `0x`) +2. Установите `"exchange": "hyperliquid"` в config.json +3. Добавьте `"hyperliquid_private_key": "your_key"` +4. Начните торговать! + +См. [Руководство по конфигурации](#-альтернатива-использование-биржи-hyperliquid). + +#### **Биржа Aster DEX** (НОВОЕ! v2.0.2) + +Децентрализованная биржа бессрочных фьючерсов, совместимая с Binance! + +**Ключевые особенности:** +- ✅ API в стиле Binance (легкая миграция с Binance) +- ✅ Web3 аутентификация кошелька (безопасно и децентрализованно) +- ✅ Полная поддержка торговли с автоматической обработкой точности +- ✅ Более низкие комиссии за торговлю чем CEX +- ✅ Совместимость с EVM (Ethereum, BSC, Polygon и т.д.) + +**Почему Aster?** +- 🎯 **API совместимый с Binance** - нужны минимальные изменения кода +- 🔐 **Система API кошелька** - отдельный торговый кошелек для безопасности +- 💰 **Конкурентные комиссии** - ниже чем большинство централизованных бирж +- 🌐 **Поддержка нескольких цепей** - торгуйте на вашей любимой EVM цепи + +**Быстрый старт:** +1. Посетите [Aster API Wallet](https://www.asterdex.com/en/api-wallet) +2. Подключите основной кошелек и создайте API кошелек +3. Скопируйте адрес API Signer и приватный ключ +4. Установите `"exchange": "aster"` в config.json +5. Добавьте `"aster_user"`, `"aster_signer"` и `"aster_private_key"` + +--- + +## 📸 Скриншоты + +### 🏆 Режим конкуренции - Битва AI в реальном времени +![Страница конкуренции](screenshots/competition-page.png) +*Лидерборд с несколькими AI и графики сравнения производительности в реальном времени показывают битву Qwen против DeepSeek* + +### 📊 Детали трейдера - Полная торговая панель +![Страница деталей](screenshots/details-page.png) +*Профессиональный торговый интерфейс с кривыми капитала, живыми позициями и логами решений AI с раскрываемыми входными промптами и цепочкой рассуждений* + +--- + ## ✨ Основные возможности ### 🏆 Режим конкуренции нескольких AI @@ -50,7 +119,11 @@ - **Лимит позиции по монете**: - Альткоины ≤ 1.5x капитал счета - BTC/ETH ≤ 10x капитал счета -- **Фиксированное плечо**: Альткоины 20x | BTC/ETH 50x +- **Настраиваемое плечо** (v2.0.3+): + - Установите максимальное плечо в config.json + - По умолчанию: 5x для всех монет (безопасно для субаккаунтов) + - Основные аккаунты могут увеличить: Альткоины до 20x, BTC/ETH до 50x + - ⚠️ Субаккаунты Binance ограничены ≤5x плечом - **Управление маржой**: Общее использование ≤90%, AI принимает автономные решения - **Соотношение риск/доход**: Обязательное ≥1:2 (стоп-лосс:тейк-профит) - **Предотвращение накопления позиций**: Запрет дублирования открытия той же монеты/направления @@ -123,8 +196,10 @@ nano config.json # или используйте любой редактор chmod +x start.sh ./start.sh start --build -# Вариант 2: Используйте docker-compose напрямую -docker-compose up -d --build +# Вариант 2: Используйте docker compose напрямую +# Этот проект использует синтаксис Docker Compose V2 (с пробелами) +# Если у вас установлена старая версия `docker-compose`, обновитесь до Docker Desktop или Docker 20.10+ +docker compose up -d --build ``` #### Шаг 3: Доступ к панели @@ -268,6 +343,10 @@ cp config.json.example config.json "scan_interval_minutes": 3 } ], + "leverage": { + "btc_eth_leverage": 5, + "altcoin_leverage": 5 + }, "use_default_coins": true, "coin_pool_api_url": "", "oi_top_api_url": "", @@ -300,6 +379,111 @@ cp config.json.example config.json --- +#### 🔷 Альтернатива: Использование биржи Hyperliquid + +**NOFX также поддерживает Hyperliquid** - децентрализованную биржу бессрочных фьючерсов. Чтобы использовать Hyperliquid вместо Binance: + +**Шаг 1**: Получите приватный ключ Ethereum (для аутентификации Hyperliquid) + +1. Откройте **MetaMask** (или любой Ethereum кошелек) +2. Экспортируйте приватный ключ +3. **Удалите префикс `0x`** из ключа +4. Пополните кошелек на [Hyperliquid](https://hyperliquid.xyz) + +**Шаг 2**: Настройте `config.json` для Hyperliquid + +```json +{ + "traders": [ + { + "id": "hyperliquid_trader", + "name": "My Hyperliquid Trader", + "ai_model": "deepseek", + "exchange": "hyperliquid", + "hyperliquid_private_key": "your_private_key_without_0x", + "hyperliquid_testnet": false, + "deepseek_key": "sk-xxxxxxxxxxxxx", + "initial_balance": 1000.0, + "scan_interval_minutes": 3 + } + ], + "use_default_coins": true, + "api_server_port": 8080 +} +``` + +**Ключевые отличия от конфигурации Binance:** +- Замените `binance_api_key` + `binance_secret_key` на `hyperliquid_private_key` +- Добавьте поле `"exchange": "hyperliquid"` +- Установите `hyperliquid_testnet: false` для мейннета (или `true` для тестнета) + +**⚠️ Предупреждение безопасности**: Никогда не делитесь приватным ключом! Используйте отдельный кошелек для торговли, а не основной. + +--- + +#### 🔶 Альтернатива: Использование биржи Aster DEX + +**NOFX также поддерживает Aster DEX** - децентрализованную биржу бессрочных фьючерсов, совместимую с Binance! + +**Почему выбрать Aster?** +- 🎯 API совместимый с Binance (легкая миграция) +- 🔐 Система безопасности API кошелька +- 💰 Более низкие комиссии за торговлю +- 🌐 Поддержка нескольких цепей (ETH, BSC, Polygon) +- 🌍 Не нужна KYC + +**Шаг 1**: Создайте Aster API кошелек + +1. Посетите [Aster API Wallet](https://www.asterdex.com/en/api-wallet) +2. Подключите основной кошелек (MetaMask, WalletConnect и т.д.) +3. Нажмите "Создать API кошелек" +4. **Сохраните эти 3 элемента немедленно:** + - Адрес основного кошелька (User) + - Адрес API кошелька (Signer) + - Приватный ключ API кошелька (⚠️ показывается только один раз!) + +**Шаг 2**: Настройте `config.json` для Aster + +```json +{ + "traders": [ + { + "id": "aster_deepseek", + "name": "Aster DeepSeek Trader", + "ai_model": "deepseek", + "exchange": "aster", + + "aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e", + "aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0", + "aster_private_key": "4fd0a42218f3eae43a6ce26d22544e986139a01e5b34a62db53757ffca81bae1", + + "deepseek_key": "sk-xxxxxxxxxxxxx", + "initial_balance": 1000.0, + "scan_interval_minutes": 3 + } + ], + "use_default_coins": true, + "api_server_port": 8080, + "leverage": { + "btc_eth_leverage": 5, + "altcoin_leverage": 5 + } +} +``` + +**Ключевые поля конфигурации:** +- `"exchange": "aster"` - Установите биржу на Aster +- `aster_user` - Адрес вашего основного кошелька +- `aster_signer` - Адрес API кошелька (из Шага 1) +- `aster_private_key` - Приватный ключ API кошелька (без префикса `0x`) + +**⚠️ Примечания безопасности**: +- API кошелек отдельный от основного (дополнительный уровень безопасности) +- Никогда не делитесь приватным ключом API +- Вы можете отозвать доступ API кошелька в любое время на [asterdex.com](https://www.asterdex.com/en/api-wallet) + +--- + #### ⚔️ Экспертный режим: Конкуренция нескольких трейдеров Для запуска нескольких AI трейдеров, конкурирующих друг с другом: @@ -360,6 +544,9 @@ cp config.json.example config.json | `qwen_key` | Qwen API ключ | `"sk-xxx"` | Требуется при использовании Qwen | | `initial_balance` | Начальный баланс для расчета P/L | `1000.0` | ✅ Да | | `scan_interval_minutes` | Частота решений (минуты) | `3` (рекомендуется 3-5) | ✅ Да | +| **`leverage`** | **Конфигурация плеча (v2.0.3+)** | См. ниже | ✅ Да | +| `btc_eth_leverage` | Максимальное плечо для BTC/ETH
⚠️ Субаккаунты: ≤5x | `5` (по умолчанию, безопасно)
`50` (максимум для основного аккаунта) | ✅ Да | +| `altcoin_leverage` | Максимальное плечо для альткоинов
⚠️ Субаккаунты: ≤5x | `5` (по умолчанию, безопасно)
`20` (максимум для основного аккаунта) | ✅ Да | | `use_default_coins` | Использовать встроенный список монет
**✨ Умное значение по умолчанию: `true`** (v2.0.2+)
Автоматически включается без API | `true` или опустить | ❌ Нет
(Опционально, авто) | | `coin_pool_api_url` | API пользовательского пула монет
*Требуется только при `use_default_coins: false`* | `""` (пусто) | ❌ Нет | | `oi_top_api_url` | API открытого интереса
*Опциональные дополнительные данные* | `""` (пусто) | ❌ Нет | @@ -370,6 +557,63 @@ cp config.json.example config.json --- +#### ⚙️ Конфигурация плеча (v2.0.3+) + +**Что такое конфигурация плеча?** + +Настройки плеча контролируют максимальное плечо, которое AI может использовать для каждой сделки. Это критически важно для управления рисками, особенно для субаккаунтов Binance, которые имеют ограничения по плечу. + +**Формат конфигурации:** + +```json +"leverage": { + "btc_eth_leverage": 5, // Максимальное плечо для BTC и ETH + "altcoin_leverage": 5 // Максимальное плечо для всех других монет +} +``` + +**⚠️ Важно: Ограничения субаккаунтов Binance** + +- **Субаккаунты**: Ограничены **≤5x плечом** от Binance +- **Основные аккаунты**: Могут использовать до 20x (альткоины) или 50x (BTC/ETH) +- Если вы используете субаккаунт и установите плечо >5x, сделки будут **завершаться с ошибкой**: `Subaccounts are restricted from using leverage greater than 5x` + +**Рекомендуемые настройки:** + +| Тип аккаунта | Плечо BTC/ETH | Плечо альткоинов | Уровень риска | +|--------------|---------------|------------------|---------------| +| **Субаккаунт** | `5` | `5` | ✅ Безопасно (по умолчанию) | +| **Основной (Консервативно)** | `10` | `10` | 🟡 Средний | +| **Основной (Агрессивно)** | `20` | `15` | 🔴 Высокий | +| **Основной (Максимум)** | `50` | `20` | 🔴🔴 Очень высокий | + +**Примеры:** + +**Безопасная конфигурация (субаккаунт или консервативная):** +```json +"leverage": { + "btc_eth_leverage": 5, + "altcoin_leverage": 5 +} +``` + +**Агрессивная конфигурация (только основной аккаунт):** +```json +"leverage": { + "btc_eth_leverage": 20, + "altcoin_leverage": 15 +} +``` + +**Как AI использует плечо:** + +- AI может выбрать **любое плечо от 1x до вашего настроенного максимума** +- Например, с `altcoin_leverage: 20`, AI может решить использовать 5x, 10x или 20x в зависимости от рыночных условий +- Конфигурация устанавливает **верхний лимит**, а не фиксированное значение +- AI учитывает волатильность, соотношение риск/доход и баланс аккаунта при выборе плеча + +--- + #### ⚠️ Важно: Поле `use_default_coins` **Умное поведение по умолчанию (v2.0.2+):** diff --git a/README.uk.md b/README.uk.md index 7af93d25..d47c5921 100644 --- a/README.uk.md +++ b/README.uk.md @@ -9,7 +9,7 @@ --- -Автоматизована система торгівлі ф'ючерсами Binance на базі **DeepSeek/Qwen AI**, що підтримує **змагання кількох AI-моделей у реальній торгівлі**, з повним аналізом ринку, прийняттям рішень AI, **механізмом самонавчання** та професійним веб-інтерфейсом моніторингу. +Автоматизована система торгівлі криптовалютними ф'ючерсами на базі **DeepSeek/Qwen AI**, що підтримує **Binance, Hyperliquid та Aster DEX біржі**, **змагання кількох AI-моделей у реальній торгівлі**, з повним аналізом ринку, прийняттям рішень AI, **механізмом самонавчання** та професійним веб-інтерфейсом моніторингу. > ⚠️ **Попередження про ризики**: Ця система експериментальна. Автоматична торгівля з AI несе значні ризики. Наполегливо рекомендується використовувати лише для навчання/досліджень або тестування з невеликими сумами! @@ -21,6 +21,75 @@ --- +## 🆕 Останні оновлення + +### 🚀 Підтримка кількох бірж! + +NOFX тепер підтримує **три основні біржі**: Binance, Hyperliquid та Aster DEX! + +#### **Біржа Hyperliquid** + +Високопродуктивна децентралізована біржа безстрокових ф'ючерсів! + +**Ключові особливості:** +- ✅ Повна підтримка торгівлі (лонг/шорт, плече, стоп-лосс/тейк-профіт) +- ✅ Автоматична обробка точності (розмір та ціна ордера) +- ✅ Єдиний інтерфейс трейдера (безшовне перемикання бірж) +- ✅ Підтримка мейннету та тестнету +- ✅ Не потрібні API ключі - тільки приватний ключ Ethereum + +**Чому Hyperliquid?** +- 🔥 Нижчі комісії ніж на централізованих біржах +- 🔒 Без зберігання - ви контролюєте свої кошти +- ⚡ Швидке виконання з розрахунком на ланцюзі +- 🌍 Не потрібна KYC + +**Швидкий старт:** +1. Отримайте приватний ключ MetaMask (видаліть префікс `0x`) +2. Встановіть `"exchange": "hyperliquid"` в config.json +3. Додайте `"hyperliquid_private_key": "your_key"` +4. Почніть торгувати! + +Див. [Посібник з конфігурації](#-альтернатива-використання-біржі-hyperliquid). + +#### **Біржа Aster DEX** (НОВЕ! v2.0.2) + +Децентралізована біржа безстрокових ф'ючерсів, сумісна з Binance! + +**Ключові особливості:** +- ✅ API в стилі Binance (легка міграція з Binance) +- ✅ Web3 автентифікація гаманця (безпечно та децентралізовано) +- ✅ Повна підтримка торгівлі з автоматичною обробкою точності +- ✅ Нижчі комісії за торгівлю ніж CEX +- ✅ Сумісність з EVM (Ethereum, BSC, Polygon тощо) + +**Чому Aster?** +- 🎯 **API сумісний з Binance** - потрібні мінімальні зміни коду +- 🔐 **Система API гаманця** - окремий торговий гаманець для безпеки +- 💰 **Конкурентні комісії** - нижче ніж більшість централізованих бірж +- 🌐 **Підтримка кількох ланцюгів** - торгуйте на вашому улюбленому EVM ланцюзі + +**Швидкий старт:** +1. Відвідайте [Aster API Wallet](https://www.asterdex.com/en/api-wallet) +2. Підключіть основний гаманець і створіть API гаманець +3. Скопіюйте адресу API Signer та приватний ключ +4. Встановіть `"exchange": "aster"` в config.json +5. Додайте `"aster_user"`, `"aster_signer"` та `"aster_private_key"` + +--- + +## 📸 Скриншоти + +### 🏆 Режим змагання - Битва AI в реальному часі +![Сторінка змагання](screenshots/competition-page.png) +*Лідерборд з кількома AI та графіки порівняння продуктивності в реальному часі показують битву Qwen проти DeepSeek* + +### 📊 Деталі трейдера - Повна торгова панель +![Сторінка деталей](screenshots/details-page.png) +*Професійний торговий інтерфейс з кривими капіталу, живими позиціями та логами рішень AI з розкриваємими вхідними промптами та ланцюгом міркувань* + +--- + ## ✨ Основні можливості ### 🏆 Режим змагання кількох AI @@ -50,7 +119,11 @@ - **Ліміт позиції по монеті**: - Альткоїни ≤ 1.5x капітал рахунку - BTC/ETH ≤ 10x капітал рахунку -- **Фіксоване плече**: Альткоїни 20x | BTC/ETH 50x +- **Налаштовуване плече** (v2.0.3+): + - Встановіть максимальне плече в config.json + - За замовчуванням: 5x для всіх монет (безпечно для субакаунтів) + - Основні акаунти можуть збільшити: Альткоїни до 20x, BTC/ETH до 50x + - ⚠️ Субакаунти Binance обмежені ≤5x плечем - **Управління маржею**: Загальне використання ≤90%, AI приймає автономні рішення - **Співвідношення ризик/дохід**: Обов'язкове ≥1:2 (стоп-лосс:тейк-профіт) - **Запобігання накопиченню позицій**: Заборона дублювання відкриття тієї ж монети/напрямку @@ -123,8 +196,10 @@ nano config.json # або використайте будь-який редак chmod +x start.sh ./start.sh start --build -# Варіант 2: Використайте docker-compose безпосередньо -docker-compose up -d --build +# Варіант 2: Використайте docker compose безпосередньо +# Цей проект використовує синтаксис Docker Compose V2 (з пробілами) +# Якщо у вас встановлена стара версія `docker-compose`, оновіть до Docker Desktop або Docker 20.10+ +docker compose up -d --build ``` #### Крок 3: Доступ до панелі @@ -268,6 +343,10 @@ cp config.json.example config.json "scan_interval_minutes": 3 } ], + "leverage": { + "btc_eth_leverage": 5, + "altcoin_leverage": 5 + }, "use_default_coins": true, "coin_pool_api_url": "", "oi_top_api_url": "", @@ -300,6 +379,111 @@ cp config.json.example config.json --- +#### 🔷 Альтернатива: Використання біржі Hyperliquid + +**NOFX також підтримує Hyperliquid** - децентралізовану біржу безстрокових ф'ючерсів. Щоб використовувати Hyperliquid замість Binance: + +**Крок 1**: Отримайте приватний ключ Ethereum (для автентифікації Hyperliquid) + +1. Відкрийте **MetaMask** (або будь-який Ethereum гаманець) +2. Експортуйте приватний ключ +3. **Видаліть префікс `0x`** з ключа +4. Поповніть гаманець на [Hyperliquid](https://hyperliquid.xyz) + +**Крок 2**: Налаштуйте `config.json` для Hyperliquid + +```json +{ + "traders": [ + { + "id": "hyperliquid_trader", + "name": "My Hyperliquid Trader", + "ai_model": "deepseek", + "exchange": "hyperliquid", + "hyperliquid_private_key": "your_private_key_without_0x", + "hyperliquid_testnet": false, + "deepseek_key": "sk-xxxxxxxxxxxxx", + "initial_balance": 1000.0, + "scan_interval_minutes": 3 + } + ], + "use_default_coins": true, + "api_server_port": 8080 +} +``` + +**Ключові відмінності від конфігурації Binance:** +- Замініть `binance_api_key` + `binance_secret_key` на `hyperliquid_private_key` +- Додайте поле `"exchange": "hyperliquid"` +- Встановіть `hyperliquid_testnet: false` для мейннету (або `true` для тестнету) + +**⚠️ Попередження безпеки**: Ніколи не діліться приватним ключем! Використовуйте окремий гаманець для торгівлі, а не основний. + +--- + +#### 🔶 Альтернатива: Використання біржі Aster DEX + +**NOFX також підтримує Aster DEX** - децентралізовану біржу безстрокових ф'ючерсів, сумісну з Binance! + +**Чому обрати Aster?** +- 🎯 API сумісний з Binance (легка міграція) +- 🔐 Система безпеки API гаманця +- 💰 Нижчі комісії за торгівлю +- 🌐 Підтримка кількох ланцюгів (ETH, BSC, Polygon) +- 🌍 Не потрібна KYC + +**Крок 1**: Створіть Aster API гаманець + +1. Відвідайте [Aster API Wallet](https://www.asterdex.com/en/api-wallet) +2. Підключіть основний гаманець (MetaMask, WalletConnect тощо) +3. Натисніть "Створити API гаманець" +4. **Збережіть ці 3 елементи негайно:** + - Адреса основного гаманця (User) + - Адреса API гаманця (Signer) + - Приватний ключ API гаманця (⚠️ показується лише один раз!) + +**Крок 2**: Налаштуйте `config.json` для Aster + +```json +{ + "traders": [ + { + "id": "aster_deepseek", + "name": "Aster DeepSeek Trader", + "ai_model": "deepseek", + "exchange": "aster", + + "aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e", + "aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0", + "aster_private_key": "4fd0a42218f3eae43a6ce26d22544e986139a01e5b34a62db53757ffca81bae1", + + "deepseek_key": "sk-xxxxxxxxxxxxx", + "initial_balance": 1000.0, + "scan_interval_minutes": 3 + } + ], + "use_default_coins": true, + "api_server_port": 8080, + "leverage": { + "btc_eth_leverage": 5, + "altcoin_leverage": 5 + } +} +``` + +**Ключові поля конфігурації:** +- `"exchange": "aster"` - Встановіть біржу на Aster +- `aster_user` - Адреса вашого основного гаманця +- `aster_signer` - Адреса API гаманця (з Кроку 1) +- `aster_private_key` - Приватний ключ API гаманця (без префікса `0x`) + +**⚠️ Примітки безпеки**: +- API гаманець окремий від основного (додатковий рівень безпеки) +- Ніколи не діліться приватним ключем API +- Ви можете відкликати доступ API гаманця в будь-який час на [asterdex.com](https://www.asterdex.com/en/api-wallet) + +--- + #### ⚔️ Експертний режим: Змагання кількох трейдерів Для запуску кількох AI трейдерів, що змагаються один з одним: @@ -360,6 +544,9 @@ cp config.json.example config.json | `qwen_key` | Qwen API ключ | `"sk-xxx"` | Потрібно при використанні Qwen | | `initial_balance` | Початковий баланс для розрахунку P/L | `1000.0` | ✅ Так | | `scan_interval_minutes` | Частота рішень (хвилини) | `3` (рекомендується 3-5) | ✅ Так | +| **`leverage`** | **Конфігурація плеча (v2.0.3+)** | Див. нижче | ✅ Так | +| `btc_eth_leverage` | Максимальне плече для BTC/ETH
⚠️ Субакаунти: ≤5x | `5` (за замовчуванням, безпечно)
`50` (максимум для основного акаунта) | ✅ Так | +| `altcoin_leverage` | Максимальне плече для альткоїнів
⚠️ Субакаунти: ≤5x | `5` (за замовчуванням, безпечно)
`20` (максимум для основного акаунта) | ✅ Так | | `use_default_coins` | Використовувати вбудований список монет
**✨ Розумне значення за замовчуванням: `true`** (v2.0.2+)
Автоматично включається без API | `true` або опустити | ❌ Ні
(Опціонально, авто) | | `coin_pool_api_url` | API користувацького пулу монет
*Потрібно лише при `use_default_coins: false`* | `""` (пусто) | ❌ Ні | | `oi_top_api_url` | API відкритого інтересу
*Опціональні додаткові дані* | `""` (пусто) | ❌ Ні | @@ -370,6 +557,63 @@ cp config.json.example config.json --- +#### ⚙️ Конфігурація плеча (v2.0.3+) + +**Що таке конфігурація плеча?** + +Налаштування плеча контролюють максимальне плече, яке AI може використовувати для кожної угоди. Це критично важливо для управління ризиками, особливо для субакаунтів Binance, які мають обмеження по плечу. + +**Формат конфігурації:** + +```json +"leverage": { + "btc_eth_leverage": 5, // Максимальне плече для BTC та ETH + "altcoin_leverage": 5 // Максимальне плече для всіх інших монет +} +``` + +**⚠️ Важливо: Обмеження субакаунтів Binance** + +- **Субакаунти**: Обмежені **≤5x плечем** від Binance +- **Основні акаунти**: Можуть використовувати до 20x (альткоїни) або 50x (BTC/ETH) +- Якщо ви використовуєте субакаунт і встановите плече >5x, угоди будуть **завершуватися з помилкою**: `Subaccounts are restricted from using leverage greater than 5x` + +**Рекомендовані налаштування:** + +| Тип акаунта | Плече BTC/ETH | Плече альткоїнів | Рівень ризику | +|-------------|---------------|------------------|---------------| +| **Субакаунт** | `5` | `5` | ✅ Безпечно (за замовчуванням) | +| **Основний (Консервативно)** | `10` | `10` | 🟡 Середній | +| **Основний (Агресивно)** | `20` | `15` | 🔴 Високий | +| **Основний (Максимум)** | `50` | `20` | 🔴🔴 Дуже високий | + +**Приклади:** + +**Безпечна конфігурація (субакаунт або консервативна):** +```json +"leverage": { + "btc_eth_leverage": 5, + "altcoin_leverage": 5 +} +``` + +**Агресивна конфігурація (тільки основний акаунт):** +```json +"leverage": { + "btc_eth_leverage": 20, + "altcoin_leverage": 15 +} +``` + +**Як AI використовує плече:** + +- AI може вибрати **будь-яке плече від 1x до вашого налаштованого максимуму** +- Наприклад, з `altcoin_leverage: 20`, AI може вирішити використовувати 5x, 10x або 20x залежно від ринкових умов +- Конфігурація встановлює **верхню межу**, а не фіксоване значення +- AI враховує волатильність, співвідношення ризик/дохід та баланс акаунта при виборі плеча + +--- + #### ⚠️ Важливо: Поле `use_default_coins` **Розумна поведінка за замовчуванням (v2.0.2+):** diff --git a/README.zh-CN.md b/README.zh-CN.md index 66b4979e..9b47fa04 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -9,7 +9,7 @@ --- -一个基于 **DeepSeek/Qwen AI** 的币安合约自动交易系统,支持**多AI模型实盘竞赛**,具备完整的市场分析、AI决策、**自我学习机制**和专业的Web监控界面。 +一个基于 **DeepSeek/Qwen AI** 的加密货币期货自动交易系统,支持 **Binance、Hyperliquid和Aster DEX交易所**,**多AI模型实盘竞赛**,具备完整的市场分析、AI决策、**自我学习机制**和专业的Web监控界面。 > ⚠️ **风险提示**:本系统为实验性项目,AI自动交易存在重大风险,强烈建议仅用于学习研究或小额资金测试! @@ -21,6 +21,75 @@ --- +## 🆕 最新更新 + +### 🚀 多交易所支持! + +NOFX现已支持**三大交易所**:Binance、Hyperliquid和Aster DEX! + +#### **Hyperliquid交易所** + +高性能的去中心化永续期货交易所! + +**核心特性:** +- ✅ 完整交易支持(做多/做空、杠杆、止损/止盈) +- ✅ 自动精度处理(订单数量和价格) +- ✅ 统一trader接口(无缝切换交易所) +- ✅ 支持主网和测试网 +- ✅ 无需API密钥 - 只需以太坊私钥 + +**为什么选择Hyperliquid?** +- 🔥 比中心化交易所手续费更低 +- 🔒 非托管 - 你掌控自己的资金 +- ⚡ 快速执行与链上结算 +- 🌍 无需KYC + +**快速开始:** +1. 获取你的MetaMask私钥(去掉`0x`前缀) +2. 在config.json中设置`"exchange": "hyperliquid"` +3. 添加`"hyperliquid_private_key": "your_key"` +4. 开始交易! + +详见[配置指南](#-备选使用hyperliquid交易所)。 + +#### **Aster DEX交易所**(新!v2.0.2) + +兼容Binance的去中心化永续期货交易所! + +**核心特性:** +- ✅ Binance风格API(从Binance轻松迁移) +- ✅ Web3钱包认证(安全且去中心化) +- ✅ 完整交易支持,自动精度处理 +- ✅ 比中心化交易所手续费更低 +- ✅ 兼容EVM(以太坊、BSC、Polygon等) + +**为什么选择Aster?** +- 🎯 **兼容Binance API** - 需要最少的代码修改 +- 🔐 **API钱包系统** - 独立交易钱包提升安全性 +- 💰 **有竞争力的手续费** - 比大多数中心化交易所更低 +- 🌐 **多链支持** - 在你喜欢的EVM链上交易 + +**快速开始:** +1. 访问[Aster API钱包](https://www.asterdex.com/en/api-wallet) +2. 连接你的主钱包并创建API钱包 +3. 复制API Signer地址和私钥 +4. 在config.json中设置`"exchange": "aster"` +5. 添加`"aster_user"`、`"aster_signer"`和`"aster_private_key"` + +--- + +## 📸 系统截图 + +### 🏆 竞赛模式 - AI实时对战 +![竞赛页面](screenshots/competition-page.png) +*多AI排行榜和实时性能对比图表,展示Qwen vs DeepSeek实时交易对战* + +### 📊 交易详情 - 完整交易仪表盘 +![详情页面](screenshots/details-page.png) +*专业交易界面,包含权益曲线、实时持仓、AI决策日志,支持展开查看输入提示词和AI思维链推理过程* + +--- + ## ✨ 核心特性 ### 🏆 多AI竞赛模式 @@ -50,7 +119,11 @@ - **单币种仓位上限**: - 山寨币 ≤ 1.5倍账户净值 - BTC/ETH ≤ 10倍账户净值 -- **固定杠杆**: 山寨币20倍 | BTC/ETH 50倍 +- **可配置杠杆** (v2.0.3+): + - 在config.json中设置最大杠杆 + - 默认:所有币种5倍(子账户安全) + - 主账户可增加:山寨币最高20倍,BTC/ETH最高50倍 + - ⚠️ 币安子账户限制≤5倍杠杆 - **保证金管理**: 总使用率≤90%,AI自主决策使用率 - **风险回报比**: 强制≥1:2(止损:止盈) - **防止仓位叠加**: 同币种同方向不允许重复开仓 @@ -187,8 +260,10 @@ nano config.json # 或使用其他编辑器 chmod +x start.sh ./start.sh start --build -# 方式2:直接使用docker-compose -docker-compose up -d --build + +# 方式2:直接使用docker compose +# 如果您还在使用旧的独立 `docker-compose`,请升级到 Docker Desktop 或 Docker 20.10+ +docker compose up -d --build ``` #### 步骤3:访问控制台 @@ -331,6 +406,10 @@ cp config.json.example config.json "scan_interval_minutes": 3 } ], + "leverage": { + "btc_eth_leverage": 5, + "altcoin_leverage": 5 + }, "use_default_coins": true, "coin_pool_api_url": "", "oi_top_api_url": "", @@ -363,6 +442,111 @@ cp config.json.example config.json --- +#### 🔷 备选:使用Hyperliquid交易所 + +**NOFX也支持Hyperliquid** - 去中心化永续期货交易所。使用Hyperliquid而非Binance: + +**步骤1**:获取以太坊私钥(用于Hyperliquid身份验证) + +1. 打开**MetaMask**(或任何以太坊钱包) +2. 导出你的私钥 +3. **去掉`0x`前缀** +4. 在[Hyperliquid](https://hyperliquid.xyz)上为钱包充值 + +**步骤2**:为Hyperliquid配置`config.json` + +```json +{ + "traders": [ + { + "id": "hyperliquid_trader", + "name": "My Hyperliquid Trader", + "ai_model": "deepseek", + "exchange": "hyperliquid", + "hyperliquid_private_key": "your_private_key_without_0x", + "hyperliquid_testnet": false, + "deepseek_key": "sk-xxxxxxxxxxxxx", + "initial_balance": 1000.0, + "scan_interval_minutes": 3 + } + ], + "use_default_coins": true, + "api_server_port": 8080 +} +``` + +**与Binance配置的关键区别:** +- 用`hyperliquid_private_key`替换`binance_api_key` + `binance_secret_key` +- 添加`"exchange": "hyperliquid"`字段 +- 设置`hyperliquid_testnet: false`用于主网(或`true`用于测试网) + +**⚠️ 安全警告**:切勿分享你的私钥!使用专门的钱包进行交易,而非主钱包。 + +--- + +#### 🔶 备选:使用Aster DEX交易所 + +**NOFX也支持Aster DEX** - 兼容Binance的去中心化永续期货交易所! + +**为什么选择Aster?** +- 🎯 兼容Binance API(轻松迁移) +- 🔐 API钱包安全系统 +- 💰 更低的交易手续费 +- 🌐 多链支持(ETH、BSC、Polygon) +- 🌍 无需KYC + +**步骤1**:创建Aster API钱包 + +1. 访问[Aster API钱包](https://www.asterdex.com/en/api-wallet) +2. 连接你的主钱包(MetaMask、WalletConnect等) +3. 点击"创建API钱包" +4. **立即保存这3项:** + - 主钱包地址(User) + - API钱包地址(Signer) + - API钱包私钥(⚠️ 仅显示一次!) + +**步骤2**:为Aster配置`config.json` + +```json +{ + "traders": [ + { + "id": "aster_deepseek", + "name": "Aster DeepSeek Trader", + "ai_model": "deepseek", + "exchange": "aster", + + "aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e", + "aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0", + "aster_private_key": "4fd0a42218f3eae43a6ce26d22544e986139a01e5b34a62db53757ffca81bae1", + + "deepseek_key": "sk-xxxxxxxxxxxxx", + "initial_balance": 1000.0, + "scan_interval_minutes": 3 + } + ], + "use_default_coins": true, + "api_server_port": 8080, + "leverage": { + "btc_eth_leverage": 5, + "altcoin_leverage": 5 + } +} +``` + +**关键配置字段:** +- `"exchange": "aster"` - 设置交易所为Aster +- `aster_user` - 你的主钱包地址 +- `aster_signer` - API钱包地址(来自步骤1) +- `aster_private_key` - API钱包私钥(去掉`0x`前缀) + +**⚠️ 安全提示**: +- API钱包与主钱包分离(额外的安全层) +- 切勿分享API私钥 +- 你可以随时在[asterdex.com](https://www.asterdex.com/en/api-wallet)撤销API钱包访问 + +--- + #### ⚔️ 专家模式:多Trader竞赛 用于运行多个AI trader相互竞争: @@ -423,6 +607,9 @@ cp config.json.example config.json | `qwen_key` | Qwen API密钥 | `"sk-xxx"` | 使用Qwen时必填 | | `initial_balance` | 用于P/L计算的起始余额 | `1000.0` | ✅ 是 | | `scan_interval_minutes` | 决策频率(分钟) | `3`(建议3-5) | ✅ 是 | +| **`leverage`** | **杠杆配置 (v2.0.3+)** | 见下文 | ✅ 是 | +| `btc_eth_leverage` | BTC/ETH最大杠杆
⚠️ 子账户:≤5倍 | `5`(默认,安全)
`50`(主账户最大) | ✅ 是 | +| `altcoin_leverage` | 山寨币最大杠杆
⚠️ 子账户:≤5倍 | `5`(默认,安全)
`20`(主账户最大) | ✅ 是 | | `use_default_coins` | 使用内置币种列表
**✨ 智能默认:`true`** (v2.0.2+)
未提供API时自动启用 | `true` 或省略 | ❌ 否
(可选,自动默认) | | `coin_pool_api_url` | 自定义币种池API
*仅当`use_default_coins: false`时需要* | `""`(空) | ❌ 否 | | `oi_top_api_url` | 持仓量API
*可选补充数据* | `""`(空) | ❌ 否 | @@ -433,6 +620,63 @@ cp config.json.example config.json --- +#### ⚙️ 杠杆配置 (v2.0.3+) + +**什么是杠杆配置?** + +杠杆设置控制AI每次交易可以使用的最大杠杆。这对于风险管理至关重要,特别是对于有杠杆限制的币安子账户。 + +**配置格式:** + +```json +"leverage": { + "btc_eth_leverage": 5, // BTC和ETH的最大杠杆 + "altcoin_leverage": 5 // 所有其他币种的最大杠杆 +} +``` + +**⚠️ 重要:币安子账户限制** + +- **子账户**:币安限制为**≤5倍杠杆** +- **主账户**:可使用最高20倍(山寨币)或50倍(BTC/ETH) +- 如果您使用子账户并设置杠杆>5倍,交易将**失败**,错误信息:`Subaccounts are restricted from using leverage greater than 5x` + +**推荐设置:** + +| 账户类型 | BTC/ETH杠杆 | 山寨币杠杆 | 风险级别 | +|---------|------------|-----------|---------| +| **子账户** | `5` | `5` | ✅ 安全(默认) | +| **主账户(保守)** | `10` | `10` | 🟡 中等 | +| **主账户(激进)** | `20` | `15` | 🔴 高 | +| **主账户(最大)** | `50` | `20` | 🔴🔴 非常高 | + +**示例:** + +**安全配置(子账户或保守):** +```json +"leverage": { + "btc_eth_leverage": 5, + "altcoin_leverage": 5 +} +``` + +**激进配置(仅主账户):** +```json +"leverage": { + "btc_eth_leverage": 20, + "altcoin_leverage": 15 +} +``` + +**AI如何使用杠杆:** + +- AI可以选择**从1倍到您配置的最大值之间的任何杠杆** +- 例如,当`altcoin_leverage: 20`时,AI可能根据市场情况决定使用5倍、10倍或20倍 +- 配置设置的是**上限**,而不是固定值 +- AI在选择杠杆时会考虑波动性、风险回报比和账户余额 + +--- + #### ⚠️ 重要:`use_default_coins` 字段 **智能默认行为(v2.0.2+):** diff --git a/api/server.go b/api/server.go index 4d761585..c82c7bc1 100644 --- a/api/server.go +++ b/api/server.go @@ -61,7 +61,7 @@ func corsMiddleware() gin.HandlerFunc { // setupRoutes 设置路由 func (s *Server) setupRoutes() { // 健康检查 - s.router.GET("/health", s.handleHealth) + s.router.Any("/health", s.handleHealth) // API路由组 api := s.router.Group("/api") @@ -639,8 +639,9 @@ func (s *Server) handlePerformance(c *gin.Context) { return } - // 分析最近20个周期的交易表现 - performance, err := trader.GetDecisionLogger().AnalyzePerformance(20) + // 分析最近100个周期的交易表现(避免长期持仓的交易记录丢失) + // 假设每3分钟一个周期,100个周期 = 5小时,足够覆盖大部分交易 + performance, err := trader.GetDecisionLogger().AnalyzePerformance(100) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": fmt.Sprintf("分析历史表现失败: %v", err), diff --git a/config.json.example b/config.json.example index 3655f5a3..8f41523e 100644 --- a/config.json.example +++ b/config.json.example @@ -6,6 +6,7 @@ "ai_model": "deepseek", "exchange": "hyperliquid", "hyperliquid_private_key": "your_ethereum_private_key_without_0x_prefix", + "hyperliquid_wallet_addr": "your_ethereum_address", "hyperliquid_testnet": false, "deepseek_key": "your_deepseek_api_key", "initial_balance": 1000, @@ -21,9 +22,55 @@ "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 + }, + { + "id": "aster_deepseek", + "name": "Aster DeepSeek Trader", + "ai_model": "deepseek", + "exchange": "aster", + + // 注意请仔细阅读这三个提示 请进入https://www.asterdex.com/en/api-wallet网站 -> 选择专业api -> 创建新api获取以下信息 + // user: 主钱包地址 (登录地址/连接到aster的钱包地址) + // signer: API钱包地址 (点击生成地址后生成的地址) + // privateKey: API钱包私钥 (生成地址对应的私钥) + + "aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e", + "aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0", + "aster_private_key": "your_aster_api_wallet_private_key_without_0x_prefix", + + "deepseek_key": "your_deepseek_api_key", + "initial_balance": 1000.0, + "scan_interval_minutes": 3 } ], + "leverage": { + "btc_eth_leverage": 5, + "altcoin_leverage": 5 + }, "use_default_coins": true, + "default_coins": [ + "BTCUSDT", + "ETHUSDT", + "SOLUSDT", + "BNBUSDT", + "XRPUSDT", + "DOGEUSDT", + "ADAUSDT", + "HYPEUSDT", + ], "coin_pool_api_url": "", "oi_top_api_url": "", "api_server_port": 8080, diff --git a/decision/engine.go b/decision/engine.go index 14a6101b..76bcffca 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -55,15 +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) + 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的交易决策 @@ -88,24 +90,24 @@ type FullDecision struct { } // GetFullDecision 获取AI的完整交易决策(批量分析所有币种和持仓) -func GetFullDecision(ctx *Context) (*FullDecision, error) { +func GetFullDecision(ctx *Context, mcpClient *mcp.Client) (*FullDecision, error) { // 1. 为所有币种获取市场数据 if err := fetchMarketDataForContext(ctx); err != nil { return nil, fmt.Errorf("获取市场数据失败: %w", err) } // 2. 构建 System Prompt(固定规则)和 User Prompt(动态数据) - systemPrompt := buildSystemPrompt(ctx.Account.TotalEquity) + systemPrompt := buildSystemPrompt(ctx.Account.TotalEquity, ctx.BTCETHLeverage, ctx.AltcoinLeverage) userPrompt := buildUserPrompt(ctx) // 3. 调用AI API(使用 system + user prompt) - aiResponse, err := mcp.CallWithMessages(systemPrompt, userPrompt) + aiResponse, err := mcpClient.CallWithMessages(systemPrompt, userPrompt) if err != nil { return nil, fmt.Errorf("调用AI API失败: %w", err) } // 4. 解析AI响应 - decision, err := parseFullDecisionResponse(aiResponse, ctx.Account.TotalEquity) + decision, err := parseFullDecisionResponse(aiResponse, ctx.Account.TotalEquity, ctx.BTCETHLeverage, ctx.AltcoinLeverage) if err != nil { return nil, fmt.Errorf("解析AI响应失败: %w", err) } @@ -198,7 +200,7 @@ func calculateMaxCandidates(ctx *Context) int { } // buildSystemPrompt 构建 System Prompt(固定规则,可缓存) -func buildSystemPrompt(accountEquity float64) string { +func buildSystemPrompt(accountEquity float64, btcEthLeverage, altcoinLeverage int) string { var sb strings.Builder // === 核心使命 === @@ -220,8 +222,8 @@ func buildSystemPrompt(accountEquity float64) string { sb.WriteString("# ⚖️ 硬约束(风险控制)\n\n") sb.WriteString("1. **风险回报比**: 必须 ≥ 1:3(冒1%风险,赚3%+收益)\n") sb.WriteString("2. **最多持仓**: 3个币种(质量>数量)\n") - sb.WriteString(fmt.Sprintf("3. **单币仓位**: 山寨%.0f-%.0f U(20x杠杆) | BTC/ETH %.0f-%.0f U(50x杠杆)\n", - accountEquity*0.8, accountEquity*1.5, accountEquity*5, accountEquity*10)) + sb.WriteString(fmt.Sprintf("3. **单币仓位**: 山寨%.0f-%.0f U(%dx杠杆) | BTC/ETH %.0f-%.0f U(%dx杠杆)\n", + accountEquity*0.8, accountEquity*1.5, altcoinLeverage, accountEquity*5, accountEquity*10, btcEthLeverage)) sb.WriteString("4. **保证金**: 总使用率 ≤ 90%\n\n") // === 做空激励 === @@ -251,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") @@ -294,7 +296,7 @@ func buildSystemPrompt(accountEquity float64) string { sb.WriteString("简洁分析你的思考过程\n\n") sb.WriteString("**第二步: JSON决策数组**\n\n") sb.WriteString("```json\n[\n") - sb.WriteString(fmt.Sprintf(" {\"symbol\": \"BTCUSDT\", \"action\": \"open_short\", \"leverage\": 50, \"position_size_usd\": %.0f, \"stop_loss\": 97000, \"take_profit\": 91000, \"confidence\": 85, \"risk_usd\": 300, \"reasoning\": \"下跌趋势+MACD死叉\"},\n", accountEquity*5)) + sb.WriteString(fmt.Sprintf(" {\"symbol\": \"BTCUSDT\", \"action\": \"open_short\", \"leverage\": %d, \"position_size_usd\": %.0f, \"stop_loss\": 97000, \"take_profit\": 91000, \"confidence\": 85, \"risk_usd\": 300, \"reasoning\": \"下跌趋势+MACD死叉\"},\n", btcEthLeverage, accountEquity*5)) sb.WriteString(" {\"symbol\": \"ETHUSDT\", \"action\": \"close_long\", \"reasoning\": \"止盈离场\"}\n") sb.WriteString("]\n```\n\n") sb.WriteString("**字段说明**:\n") @@ -415,7 +417,7 @@ func buildUserPrompt(ctx *Context) string { } // parseFullDecisionResponse 解析AI的完整决策响应 -func parseFullDecisionResponse(aiResponse string, accountEquity float64) (*FullDecision, error) { +func parseFullDecisionResponse(aiResponse string, accountEquity float64, btcEthLeverage, altcoinLeverage int) (*FullDecision, error) { // 1. 提取思维链 cotTrace := extractCoTTrace(aiResponse) @@ -429,7 +431,7 @@ func parseFullDecisionResponse(aiResponse string, accountEquity float64) (*FullD } // 3. 验证决策 - if err := validateDecisions(decisions, accountEquity); err != nil { + if err := validateDecisions(decisions, accountEquity, btcEthLeverage, altcoinLeverage); err != nil { return &FullDecision{ CoTTrace: cotTrace, Decisions: decisions, @@ -496,10 +498,10 @@ func fixMissingQuotes(jsonStr string) string { return jsonStr } -// validateDecisions 验证所有决策(需要账户信息) -func validateDecisions(decisions []Decision, accountEquity float64) error { +// validateDecisions 验证所有决策(需要账户信息和杠杆配置) +func validateDecisions(decisions []Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error { for i, decision := range decisions { - if err := validateDecision(&decision, accountEquity); err != nil { + if err := validateDecision(&decision, accountEquity, btcEthLeverage, altcoinLeverage); err != nil { return fmt.Errorf("决策 #%d 验证失败: %w", i+1, err) } } @@ -529,7 +531,7 @@ func findMatchingBracket(s string, start int) int { } // validateDecision 验证单个决策的有效性 -func validateDecision(d *Decision, accountEquity float64) error { +func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error { // 验证action validActions := map[string]bool{ "open_long": true, @@ -546,16 +548,16 @@ func validateDecision(d *Decision, accountEquity float64) error { // 开仓操作必须提供完整参数 if d.Action == "open_long" || d.Action == "open_short" { - // 根据币种判断杠杆上限和仓位价值上限 - maxLeverage := 20 // 山寨币固定20倍 + // 根据币种使用配置的杠杆上限 + maxLeverage := altcoinLeverage // 山寨币使用配置的杠杆 maxPositionValue := accountEquity * 1.5 // 山寨币最多1.5倍账户净值 if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" { - maxLeverage = 50 // BTC和ETH固定50倍 + maxLeverage = btcEthLeverage // BTC和ETH使用配置的杠杆 maxPositionValue = accountEquity * 10 // BTC/ETH最多10倍账户净值 } if d.Leverage <= 0 || d.Leverage > maxLeverage { - return fmt.Errorf("杠杆必须在1-%d之间(%s): %d", maxLeverage, d.Symbol, d.Leverage) + return fmt.Errorf("杠杆必须在1-%d之间(%s,当前配置上限%d倍): %d", maxLeverage, d.Symbol, maxLeverage, d.Leverage) } if d.PositionSizeUSD <= 0 { return fmt.Errorf("仓位大小必须大于0: %.2f", d.PositionSizeUSD) diff --git a/docker-compose.yml b/docker-compose.yml index c063d1e8..608fdfd8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,68 +1,48 @@ services: - # 后端服务 - backend: + # Backend service (API and core logic) + nofx: build: context: . - dockerfile: Dockerfile - container_name: nofx-backend + dockerfile: ./docker/Dockerfile.backend + container_name: nofx-trading restart: unless-stopped ports: - - "8080:8080" + - "${NOFX_BACKEND_PORT:-8080}:8080" volumes: - # 挂载配置文件(必须) - ./config.json:/app/config.json:ro - # 持久化决策日志 - ./decision_logs:/app/decision_logs - # 持久化币种池缓存 - - ./coin_pool_cache:/app/coin_pool_cache + - /etc/localtime:/etc/localtime:ro # Sync host time environment: - - TZ=Asia/Shanghai + - TZ=${NOFX_TIMEZONE:-Asia/Shanghai} # Set timezone networks: - nofx-network healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"] + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s timeout: 10s retries: 3 - start_period: 10s - logging: - driver: "json-file" - options: - max-size: "10m" - max-file: "3" + start_period: 60s - # 前端服务 - frontend: + # Frontend service (static serving and proxy) + nofx-frontend: build: - context: ./web - dockerfile: Dockerfile + context: . + dockerfile: ./docker/Dockerfile.frontend container_name: nofx-frontend restart: unless-stopped ports: - - "3000:80" - depends_on: - backend: - condition: service_healthy + - "${NOFX_FRONTEND_PORT:-3000}:80" networks: - nofx-network - environment: - - TZ=Asia/Shanghai + depends_on: + - nofx healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/health"] interval: 30s timeout: 10s retries: 3 start_period: 5s - logging: - driver: "json-file" - options: - max-size: "10m" - max-file: "3" networks: nofx-network: - driver: bridge - -volumes: - decision_logs: - coin_pool_cache: + driver: bridge \ No newline at end of file diff --git a/docker/Dockerfile.backend b/docker/Dockerfile.backend new file mode 100644 index 00000000..18387f67 --- /dev/null +++ b/docker/Dockerfile.backend @@ -0,0 +1,68 @@ +# docker/backend/Dockerfile + +# ═══════════════════════════════════════════════════════════════ +# NOFX Backend Dockerfile (Go + TA-Lib) +# Multi-stage build with shared TA-Lib compilation stage +# Versions extracted as ARGs for maintainability +# ═══════════════════════════════════════════════════════════════ + +ARG GO_VERSION=1.25-alpine +ARG ALPINE_VERSION=latest +ARG TA_LIB_VERSION=0.4.0 + +# ────────────────────────────────────────────────────────────── +# TA-Lib Build Stage (shared across builds) +# ────────────────────────────────────────────────────────────── +FROM alpine:${ALPINE_VERSION} AS ta-lib-builder +ARG TA_LIB_VERSION + +RUN apk update && apk add --no-cache \ + wget tar make gcc g++ musl-dev autoconf automake + +RUN wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-${TA_LIB_VERSION}-src.tar.gz && \ + tar -xzf ta-lib-${TA_LIB_VERSION}-src.tar.gz && \ + cd ta-lib && \ + if [ "$(uname -m)" = "aarch64" ]; then \ + CONFIG_GUESS=$(find /usr/share -name config.guess | head -1) && \ + CONFIG_SUB=$(find /usr/share -name config.sub | head -1) && \ + cp "$CONFIG_GUESS" config.guess && \ + cp "$CONFIG_SUB" config.sub && \ + chmod +x config.guess config.sub; \ + fi && \ + ./configure --prefix=/usr/local && \ + make && make install && \ + cd .. && rm -rf ta-lib ta-lib-${TA_LIB_VERSION}-src.tar.gz + +# ────────────────────────────────────────────────────────────── +# Backend Build Stage (Go Application) +# ────────────────────────────────────────────────────────────── +FROM golang:${GO_VERSION} AS backend-builder + +RUN apk update && apk add --no-cache git make gcc g++ musl-dev + +COPY --from=ta-lib-builder /usr/local /usr/local + +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=1 GOOS=linux go build -trimpath -ldflags="-s -w" -o nofx . + +# ────────────────────────────────────────────────────────────── +# Runtime Stage (Minimal Executable Environment) +# ────────────────────────────────────────────────────────────── +FROM alpine:${ALPINE_VERSION} + +RUN apk update && apk add --no-cache ca-certificates tzdata + +COPY --from=ta-lib-builder /usr/local /usr/local +WORKDIR /app +COPY --from=backend-builder /app/nofx . + +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 + +CMD ["./nofx"] diff --git a/docker/Dockerfile.frontend b/docker/Dockerfile.frontend new file mode 100644 index 00000000..75098bd1 --- /dev/null +++ b/docker/Dockerfile.frontend @@ -0,0 +1,36 @@ +# docker/frontend/Dockerfile + +# ═══════════════════════════════════════════════════════════════ +# NOFX Frontend Dockerfile (Node Build → Nginx Runtime) +# Versions extracted as ARGs for consistency +# ═══════════════════════════════════════════════════════════════ + +ARG NODE_VERSION=20-alpine +ARG NGINX_VERSION=alpine + +# ────────────────────────────────────────────────────────────── +# Build Stage (Node) +# ────────────────────────────────────────────────────────────── +FROM node:${NODE_VERSION} AS builder +WORKDIR /build + +COPY web/package*.json ./ +RUN npm ci + +COPY web/ ./ +RUN npm run build + +# ────────────────────────────────────────────────────────────── +# Runtime Stage (Nginx) +# ────────────────────────────────────────────────────────────── +FROM nginx:${NGINX_VERSION} + +COPY nginx/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=builder /build/dist /usr/share/nginx/html + +EXPOSE 80 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost/health || exit 1 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/go.sum b/go.sum index eb440888..4bb6ff45 100644 --- a/go.sum +++ b/go.sum @@ -74,6 +74,8 @@ github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= diff --git a/logger/decision_logger.go b/logger/decision_logger.go index e5acba8b..ed446f20 100644 --- a/logger/decision_logger.go +++ b/logger/decision_logger.go @@ -269,16 +269,20 @@ type Statistics struct { // TradeOutcome 单笔交易结果 type TradeOutcome struct { - Symbol string `json:"symbol"` // 币种 - Side string `json:"side"` // long/short - OpenPrice float64 `json:"open_price"` // 开仓价 - ClosePrice float64 `json:"close_price"` // 平仓价 - PnL float64 `json:"pn_l"` // 盈亏(USDT) - PnLPct float64 `json:"pn_l_pct"` // 盈亏百分比 - Duration string `json:"duration"` // 持仓时长 - OpenTime time.Time `json:"open_time"` // 开仓时间 - CloseTime time.Time `json:"close_time"` // 平仓时间 - WasStopLoss bool `json:"was_stop_loss"` // 是否止损 + Symbol string `json:"symbol"` // 币种 + Side string `json:"side"` // long/short + Quantity float64 `json:"quantity"` // 仓位数量 + Leverage int `json:"leverage"` // 杠杆倍数 + OpenPrice float64 `json:"open_price"` // 开仓价 + ClosePrice float64 `json:"close_price"` // 平仓价 + PositionValue float64 `json:"position_value"` // 仓位价值(quantity × openPrice) + MarginUsed float64 `json:"margin_used"` // 保证金使用(positionValue / leverage) + PnL float64 `json:"pn_l"` // 盈亏(USDT) + PnLPct float64 `json:"pn_l_pct"` // 盈亏百分比(相对保证金) + Duration string `json:"duration"` // 持仓时长 + OpenTime time.Time `json:"open_time"` // 开仓时间 + CloseTime time.Time `json:"close_time"` // 平仓时间 + WasStopLoss bool `json:"was_stop_loss"` // 是否止损 } // PerformanceAnalysis 交易表现分析 @@ -330,7 +334,45 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna // 追踪持仓状态:symbol_side -> {side, openPrice, openTime, quantity, leverage} openPositions := make(map[string]map[string]interface{}) - // 遍历所有记录 + // 为了避免开仓记录在窗口外导致匹配失败,需要先从所有历史记录中找出未平仓的持仓 + // 获取更多历史记录来构建完整的持仓状态(使用更大的窗口) + allRecords, err := l.GetLatestRecords(lookbackCycles * 3) // 扩大3倍窗口 + if err == nil && len(allRecords) > len(records) { + // 先从扩大的窗口中收集所有开仓记录 + for _, record := range allRecords { + for _, action := range record.Decisions { + if !action.Success { + continue + } + + symbol := action.Symbol + side := "" + if action.Action == "open_long" || action.Action == "close_long" { + side = "long" + } else if action.Action == "open_short" || action.Action == "close_short" { + side = "short" + } + posKey := symbol + "_" + side + + switch action.Action { + case "open_long", "open_short": + // 记录开仓 + openPositions[posKey] = map[string]interface{}{ + "side": side, + "openPrice": action.Price, + "openTime": action.Timestamp, + "quantity": action.Quantity, + "leverage": action.Leverage, + } + case "close_long", "close_short": + // 移除已平仓记录 + delete(openPositions, posKey) + } + } + } + } + + // 遍历分析窗口内的记录,生成交易结果 for _, record := range records { for _, action := range record.Decisions { if !action.Success { @@ -348,7 +390,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna switch action.Action { case "open_long", "open_short": - // 记录开仓(包括数量和杠杆) + // 更新开仓记录(可能已经在预填充时记录过了) openPositions[posKey] = map[string]interface{}{ "side": side, "openPrice": action.Price, @@ -358,7 +400,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna } case "close_long", "close_short": - // 查找对应的开仓记录 + // 查找对应的开仓记录(可能来自预填充或当前窗口) if openPos, exists := openPositions[posKey]; exists { openPrice := openPos["openPrice"].(float64) openTime := openPos["openTime"].(time.Time) @@ -366,42 +408,53 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna quantity := openPos["quantity"].(float64) leverage := openPos["leverage"].(int) - // 计算盈亏百分比 - pnlPct := 0.0 + // 计算实际盈亏(USDT) + // 合约交易 PnL 计算:quantity × 价格差 + // 注意:杠杆不影响绝对盈亏,只影响保证金需求 + var pnl float64 if side == "long" { - pnlPct = ((action.Price - openPrice) / openPrice) * 100 + pnl = quantity * (action.Price - openPrice) } else { - pnlPct = ((openPrice - action.Price) / openPrice) * 100 + pnl = quantity * (openPrice - action.Price) } - // 计算实际盈亏(USDT) - // PnL = 仓位价值 × 价格变化百分比 × 杠杆倍数 + // 计算盈亏百分比(相对保证金) positionValue := quantity * openPrice - pnl := positionValue * (pnlPct / 100) * float64(leverage) + marginUsed := positionValue / float64(leverage) + pnlPct := 0.0 + if marginUsed > 0 { + pnlPct = (pnl / marginUsed) * 100 + } // 记录交易结果 outcome := TradeOutcome{ - Symbol: symbol, - Side: side, - OpenPrice: openPrice, - ClosePrice: action.Price, - PnL: pnl, - PnLPct: pnlPct, - Duration: action.Timestamp.Sub(openTime).String(), - OpenTime: openTime, - CloseTime: action.Timestamp, + Symbol: symbol, + Side: side, + Quantity: quantity, + Leverage: leverage, + OpenPrice: openPrice, + ClosePrice: action.Price, + PositionValue: positionValue, + MarginUsed: marginUsed, + PnL: pnl, + PnLPct: pnlPct, + Duration: action.Timestamp.Sub(openTime).String(), + OpenTime: openTime, + CloseTime: action.Timestamp, } analysis.RecentTrades = append(analysis.RecentTrades, outcome) analysis.TotalTrades++ + // 分类交易:盈利、亏损、持平(避免将pnl=0算入亏损) if pnl > 0 { analysis.WinningTrades++ analysis.AvgWin += pnl - } else { + } else if pnl < 0 { analysis.LosingTrades++ analysis.AvgLoss += pnl } + // pnl == 0 的交易不计入盈利也不计入亏损,但计入总交易数 // 更新币种统计 if _, exists := analysis.SymbolStats[symbol]; !exists { @@ -414,7 +467,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna stats.TotalPnL += pnl if pnl > 0 { stats.WinningTrades++ - } else { + } else if pnl < 0 { stats.LosingTrades++ } @@ -444,6 +497,9 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna // 注意:totalLossAmount 是负数,所以取负号得到绝对值 if totalLossAmount != 0 { analysis.ProfitFactor = totalWinAmount / (-totalLossAmount) + } else if totalWinAmount > 0 { + // 只有盈利没有亏损的情况,设置为一个很大的值表示完美策略 + analysis.ProfitFactor = 999.0 } } diff --git a/main.go b/main.go index 72fc9402..238a6d5c 100644 --- a/main.go +++ b/main.go @@ -41,6 +41,10 @@ func main() { log.Printf("✓ 配置数据库初始化成功") fmt.Println() + // 设置默认主流币种列表 + defaultCoins := []string{"BTC", "ETH", "SOL", "BNB", "XRP", "DOGE", "ADA", "HYPE"} + pool.SetDefaultCoins(defaultCoins) + // 设置是否使用默认主流币种 pool.SetUseDefaultCoins(useDefaultCoins) if useDefaultCoins { @@ -94,7 +98,7 @@ func main() { fmt.Println() fmt.Println("🤖 AI全权决策模式:") - fmt.Println(" • AI将自主决定每笔交易的杠杆倍数(山寨币1-20倍,BTC/ETH最高50倍)") + fmt.Printf(" • AI将自主决定每笔交易的杠杆倍数(山寨币最高5倍,BTC/ETH最高5倍)\n") fmt.Println(" • AI将自主决定每笔交易的仓位大小") fmt.Println(" • AI将自主设置止损和止盈价格") fmt.Println(" • AI将基于市场数据、技术指标、账户状态做出全面分析") diff --git a/mcp/client.go b/mcp/client.go index 55f1fb81..12973753 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -16,54 +16,77 @@ type Provider string const ( ProviderDeepSeek Provider = "deepseek" ProviderQwen Provider = "qwen" + ProviderCustom Provider = "custom" ) -// Config AI API配置 -type Config struct { - Provider Provider - APIKey string - SecretKey string // 阿里云需要 - BaseURL string - Model string - Timeout time.Duration +// Client AI API配置 +type Client struct { + Provider Provider + APIKey string + SecretKey string // 阿里云需要 + BaseURL string + Model string + Timeout time.Duration + UseFullURL bool // 是否使用完整URL(不添加/chat/completions) } -// 默认配置 -var defaultConfig = Config{ - Provider: ProviderDeepSeek, - BaseURL: "https://api.deepseek.com/v1", - Model: "deepseek-chat", - Timeout: 120 * time.Second, // 增加到120秒,因为AI需要分析大量数据 +func New() *Client { + // 默认配置 + var defaultClient = Client{ + Provider: ProviderDeepSeek, + BaseURL: "https://api.deepseek.com/v1", + Model: "deepseek-chat", + Timeout: 120 * time.Second, // 增加到120秒,因为AI需要分析大量数据 + } + return &defaultClient } // SetDeepSeekAPIKey 设置DeepSeek API密钥 -func SetDeepSeekAPIKey(apiKey string) { - defaultConfig.Provider = ProviderDeepSeek - defaultConfig.APIKey = apiKey - defaultConfig.BaseURL = "https://api.deepseek.com/v1" - defaultConfig.Model = "deepseek-chat" +func (cfg *Client) SetDeepSeekAPIKey(apiKey string) { + cfg.Provider = ProviderDeepSeek + cfg.APIKey = apiKey + cfg.BaseURL = "https://api.deepseek.com/v1" + cfg.Model = "deepseek-chat" } // SetQwenAPIKey 设置阿里云Qwen API密钥 -func SetQwenAPIKey(apiKey, secretKey string) { - defaultConfig.Provider = ProviderQwen - defaultConfig.APIKey = apiKey - defaultConfig.SecretKey = secretKey - defaultConfig.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1" - defaultConfig.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max +func (cfg *Client) SetQwenAPIKey(apiKey, secretKey string) { + cfg.Provider = ProviderQwen + cfg.APIKey = apiKey + cfg.SecretKey = secretKey + cfg.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1" + cfg.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max } -// SetConfig 设置完整的AI配置(高级用户) -func SetConfig(config Config) { - if config.Timeout == 0 { - config.Timeout = 30 * time.Second +// SetCustomAPI 设置自定义OpenAI兼容API +func (cfg *Client) SetCustomAPI(apiURL, apiKey, modelName string) { + cfg.Provider = ProviderCustom + cfg.APIKey = apiKey + + // 检查URL是否以#结尾,如果是则使用完整URL(不添加/chat/completions) + if strings.HasSuffix(apiURL, "#") { + cfg.BaseURL = strings.TrimSuffix(apiURL, "#") + cfg.UseFullURL = true + } else { + cfg.BaseURL = apiURL + cfg.UseFullURL = false } - defaultConfig = config + + cfg.Model = modelName + cfg.Timeout = 120 * time.Second +} + +// SetClient 设置完整的AI配置(高级用户) +func (cfg *Client) SetClient(Client Client) { + if Client.Timeout == 0 { + Client.Timeout = 30 * time.Second + } + cfg = &Client } // CallWithMessages 使用 system + user prompt 调用AI API(推荐) -func CallWithMessages(systemPrompt, userPrompt string) (string, error) { - if defaultConfig.APIKey == "" { +func (cfg *Client) CallWithMessages(systemPrompt, userPrompt string) (string, error) { + if cfg.APIKey == "" { return "", fmt.Errorf("AI API密钥未设置,请先调用 SetDeepSeekAPIKey() 或 SetQwenAPIKey()") } @@ -76,7 +99,7 @@ func CallWithMessages(systemPrompt, userPrompt string) (string, error) { fmt.Printf("⚠️ AI API调用失败,正在重试 (%d/%d)...\n", attempt, maxRetries) } - result, err := callOnce(systemPrompt, userPrompt) + result, err := cfg.callOnce(systemPrompt, userPrompt) if err == nil { if attempt > 1 { fmt.Printf("✓ AI API重试成功\n") @@ -102,7 +125,7 @@ func CallWithMessages(systemPrompt, userPrompt string) (string, error) { } // callOnce 单次调用AI API(内部使用) -func callOnce(systemPrompt, userPrompt string) (string, error) { +func (cfg *Client) callOnce(systemPrompt, userPrompt string) (string, error) { // 构建 messages 数组 messages := []map[string]string{} @@ -122,7 +145,7 @@ func callOnce(systemPrompt, userPrompt string) (string, error) { // 构建请求体 requestBody := map[string]interface{}{ - "model": defaultConfig.Model, + "model": cfg.Model, "messages": messages, "temperature": 0.5, // 降低temperature以提高JSON格式稳定性 "max_tokens": 2000, @@ -137,7 +160,14 @@ func callOnce(systemPrompt, userPrompt string) (string, error) { } // 创建HTTP请求 - url := fmt.Sprintf("%s/chat/completions", defaultConfig.BaseURL) + var url string + if cfg.UseFullURL { + // 使用完整URL,不添加/chat/completions + url = cfg.BaseURL + } else { + // 默认行为:添加/chat/completions + url = fmt.Sprintf("%s/chat/completions", cfg.BaseURL) + } req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) if err != nil { return "", fmt.Errorf("创建请求失败: %w", err) @@ -146,19 +176,19 @@ func callOnce(systemPrompt, userPrompt string) (string, error) { req.Header.Set("Content-Type", "application/json") // 根据不同的Provider设置认证方式 - switch defaultConfig.Provider { + switch cfg.Provider { case ProviderDeepSeek: - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey)) case ProviderQwen: // 阿里云Qwen使用API-Key认证 - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey)) // 注意:如果使用的不是兼容模式,可能需要不同的认证方式 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) if err != nil { return "", fmt.Errorf("发送请求失败: %w", err) diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 00000000..e09eec2c --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,53 @@ +# nginx.conf - Extracted Nginx configuration for NOFX Frontend +# This configuration merges enhancements from provided variants: improved gzip, static asset caching, adjusted API proxy (preserving /api/ path), extended timeouts, and a static health response for frontend independence. + +server { + listen 80; + server_name localhost; + + # Frontend root + root /usr/share/nginx/html; + index index.html; + + # Gzip compression (enhanced) + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/javascript application/json; + + # Frontend routes (SPA) with static asset caching + location / { + try_files $uri $uri/ /index.html; + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + } + + # Proxy API requests to backend (preserves /api/ path, with timeouts) + location /api/ { + proxy_pass http://nofx:8080/api/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Increase timeout for long-running API calls + proxy_connect_timeout 300s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + } + + # Health check endpoint (static response for frontend health, independent of backend) + location /health { + return 200 "OK\n"; + add_header Content-Type text/plain; + access_log off; + } +} \ No newline at end of file diff --git a/pm2.config.js b/pm2.config.js new file mode 100644 index 00000000..2f166388 --- /dev/null +++ b/pm2.config.js @@ -0,0 +1,41 @@ +const path = require('path'); + +module.exports = { + apps: [ + { + name: 'nofx-backend', + script: './nofx', + cwd: __dirname, // 使用当前目录(配置文件所在目录) + interpreter: 'none', // 不使用解释器,直接执行二进制文件 + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '500M', + env: { + NODE_ENV: 'production' + }, + error_file: './logs/backend-error.log', + out_file: './logs/backend-out.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss Z', + merge_logs: true + }, + { + name: 'nofx-frontend', + script: 'npm', + args: 'run dev', + cwd: path.join(__dirname, 'web'), // 动态拼接 web 目录 + instances: 1, + autorestart: true, + watch: false, + max_memory_restart: '300M', + env: { + NODE_ENV: 'development', + PORT: 3000 + }, + error_file: './logs/frontend-error.log', + out_file: './logs/frontend-out.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss Z', + merge_logs: true + } + ] +}; diff --git a/pm2.sh b/pm2.sh new file mode 100755 index 00000000..b55c8412 --- /dev/null +++ b/pm2.sh @@ -0,0 +1,258 @@ +#!/bin/bash + +# NoFX Trading Bot - PM2 管理脚本 +# 用法: ./pm2.sh [start|stop|restart|status|logs|build] + +set -e + +# 自动获取脚本所在目录(支持符号链接) +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$PROJECT_ROOT" + +# 颜色输出 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +PURPLE='\033[0;35m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# 函数:打印带颜色的消息 +print_info() { + echo -e "${BLUE}ℹ️ $1${NC}" +} + +print_success() { + echo -e "${GREEN}✅ $1${NC}" +} + +print_warning() { + echo -e "${YELLOW}⚠️ $1${NC}" +} + +print_error() { + echo -e "${RED}❌ $1${NC}" +} + +print_header() { + echo -e "${PURPLE}═══════════════════════════════════════${NC}" + echo -e "${PURPLE} 🤖 NoFX Trading Bot - PM2 Manager${NC}" + echo -e "${PURPLE}═══════════════════════════════════════${NC}" + echo "" +} + +# 函数:检查 PM2 是否安装 +check_pm2() { + if ! command -v pm2 &> /dev/null; then + print_error "PM2 未安装,请先安装: npm install -g pm2" + exit 1 + fi +} + +# 函数:确保日志目录存在 +ensure_log_dirs() { + mkdir -p "$PROJECT_ROOT/logs" + mkdir -p "$PROJECT_ROOT/web/logs" + print_info "日志目录已创建" +} + +# 函数:编译后端 +build_backend() { + print_info "正在编译后端..." + go build -o nofx + if [ $? -eq 0 ]; then + print_success "后端编译完成" + else + print_error "后端编译失败" + exit 1 + fi +} + +# 函数:构建前端(生产环境) +build_frontend() { + print_info "正在构建前端..." + cd web + npm run build + if [ $? -eq 0 ]; then + print_success "前端构建完成" + cd .. + else + print_error "前端构建失败" + exit 1 + fi +} + +# 函数:启动服务 +start_services() { + print_header + ensure_log_dirs + + # 检查后端二进制文件是否存在 + if [ ! -f "./nofx" ]; then + print_warning "后端二进制文件不存在,开始编译..." + build_backend + fi + + print_info "正在启动服务..." + pm2 start pm2.config.js + + sleep 2 + pm2 status + + echo "" + print_success "服务启动完成!" + echo "" + echo -e "${CYAN}📊 访问地址:${NC}" + echo -e " ${GREEN}前端:${NC} http://localhost:3000" + echo -e " ${GREEN}后端 API:${NC} http://localhost:8080" + echo "" + echo -e "${CYAN}📝 查看日志:${NC}" + echo -e " ${GREEN}实时日志:${NC} ./pm2.sh logs" + echo -e " ${GREEN}后端日志:${NC} ./pm2.sh logs backend" + echo -e " ${GREEN}前端日志:${NC} ./pm2.sh logs frontend" + echo "" +} + +# 函数:停止服务 +stop_services() { + print_header + print_info "正在停止服务..." + pm2 stop pm2.config.js + print_success "服务已停止" +} + +# 函数:重启服务 +restart_services() { + print_header + print_info "正在重启服务..." + pm2 restart pm2.config.js + sleep 2 + pm2 status + print_success "服务已重启" +} + +# 函数:删除服务 +delete_services() { + print_header + print_warning "正在删除 PM2 服务..." + pm2 delete pm2.config.js || true + print_success "PM2 服务已删除" +} + +# 函数:查看状态 +show_status() { + print_header + pm2 status + echo "" + print_info "详细信息:" + pm2 info nofx-backend + echo "" + pm2 info nofx-frontend +} + +# 函数:查看日志 +show_logs() { + if [ -z "$2" ]; then + # 显示所有日志 + pm2 logs + elif [ "$2" = "backend" ]; then + pm2 logs nofx-backend + elif [ "$2" = "frontend" ]; then + pm2 logs nofx-frontend + else + print_error "未知的日志类型: $2" + print_info "用法: ./pm2.sh logs [backend|frontend]" + exit 1 + fi +} + +# 函数:监控 +show_monitor() { + print_header + print_info "启动 PM2 监控面板..." + pm2 monit +} + +# 函数:重新编译并重启 +rebuild_and_restart() { + print_header + print_info "正在重新编译后端..." + build_backend + + print_info "正在重启后端服务..." + pm2 restart nofx-backend + + sleep 2 + pm2 status + print_success "后端已重新编译并重启" +} + +# 函数:显示帮助 +show_help() { + print_header + echo -e "${CYAN}使用方法:${NC}" + echo " ./pm2.sh [command]" + echo "" + echo -e "${CYAN}可用命令:${NC}" + echo -e " ${GREEN}start${NC} - 启动前后端服务" + echo -e " ${GREEN}stop${NC} - 停止所有服务" + echo -e " ${GREEN}restart${NC} - 重启所有服务" + echo -e " ${GREEN}status${NC} - 查看服务状态" + echo -e " ${GREEN}logs${NC} - 查看所有日志 (Ctrl+C 退出)" + echo -e " ${GREEN}logs backend${NC} - 查看后端日志" + echo -e " ${GREEN}logs frontend${NC} - 查看前端日志" + echo -e " ${GREEN}monitor${NC} - 打开 PM2 监控面板" + echo -e " ${GREEN}build${NC} - 编译后端" + echo -e " ${GREEN}rebuild${NC} - 重新编译后端并重启" + echo -e " ${GREEN}delete${NC} - 删除 PM2 服务" + echo -e " ${GREEN}help${NC} - 显示此帮助信息" + echo "" + echo -e "${CYAN}示例:${NC}" + echo " ./pm2.sh start # 启动服务" + echo " ./pm2.sh logs backend # 查看后端日志" + echo " ./pm2.sh rebuild # 重新编译后端并重启" + echo "" +} + +# 主逻辑 +check_pm2 + +case "${1:-help}" in + start) + start_services + ;; + stop) + stop_services + ;; + restart) + restart_services + ;; + status) + show_status + ;; + logs) + show_logs "$@" + ;; + monitor|mon) + show_monitor + ;; + build) + build_backend + ;; + rebuild) + rebuild_and_restart + ;; + delete|remove) + delete_services + ;; + help|--help|-h) + show_help + ;; + *) + print_error "未知命令: $1" + echo "" + show_help + exit 1 + ;; +esac diff --git a/pool/coin_pool.go b/pool/coin_pool.go index 6675013c..72ce5c72 100644 --- a/pool/coin_pool.go +++ b/pool/coin_pool.go @@ -12,7 +12,7 @@ import ( "time" ) -// defaultMainstreamCoins 默认主流币种池(当AI500和OI Top都失败时使用) +// defaultMainstreamCoins 默认主流币种池(从配置文件读取) var defaultMainstreamCoins = []string{ "BTCUSDT", "ETHUSDT", @@ -83,6 +83,14 @@ func SetUseDefaultCoins(useDefault bool) { coinPoolConfig.UseDefaultCoins = useDefault } +// SetDefaultCoins 设置默认主流币种列表 +func SetDefaultCoins(coins []string) { + if len(coins) > 0 { + defaultMainstreamCoins = coins + log.Printf("✓ 已设置默认币种池(共%d个币种): %v", len(coins), coins) + } +} + // GetCoinPool 获取币种池列表(带重试和缓存机制) func GetCoinPool() ([]CoinInfo, error) { // 优先检查是否启用默认币种列表 diff --git a/screenshots/competition-page.png b/screenshots/competition-page.png new file mode 100644 index 00000000..dad13d4e Binary files /dev/null and b/screenshots/competition-page.png differ diff --git a/screenshots/details-page.png b/screenshots/details-page.png new file mode 100644 index 00000000..e7c9328d Binary files /dev/null and b/screenshots/details-page.png differ diff --git a/start.sh b/start.sh index 1a4971e5..f4589f96 100755 --- a/start.sh +++ b/start.sh @@ -1,18 +1,24 @@ #!/bin/bash +# ═══════════════════════════════════════════════════════════════ # NOFX AI Trading System - Docker Quick Start Script -# 使用方法: ./start.sh [command] +# Usage: ./start.sh [command] +# ═══════════════════════════════════════════════════════════════ set -e -# 颜色定义 +# ------------------------------------------------------------------------ +# Color Definitions +# ------------------------------------------------------------------------ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color -# 打印带颜色的消息 +# ------------------------------------------------------------------------ +# Utility Functions: Colored Output +# ------------------------------------------------------------------------ print_info() { echo -e "${BLUE}[INFO]${NC} $1" } @@ -29,22 +35,51 @@ print_error() { echo -e "${RED}[ERROR]${NC} $1" } -# 检查 Docker 是否安装 +# ------------------------------------------------------------------------ +# Detection: Docker Compose Command (Backward Compatible) +# ------------------------------------------------------------------------ +detect_compose_cmd() { + if command -v docker compose &> /dev/null; then + COMPOSE_CMD="docker compose" + elif command -v docker-compose &> /dev/null; then + COMPOSE_CMD="docker-compose" + else + print_error "Docker Compose 未安装!请先安装 Docker Compose" + exit 1 + fi + print_info "使用 Docker Compose 命令: $COMPOSE_CMD" +} + +# ------------------------------------------------------------------------ +# Validation: Docker Installation +# ------------------------------------------------------------------------ check_docker() { if ! command -v docker &> /dev/null; then print_error "Docker 未安装!请先安装 Docker: https://docs.docker.com/get-docker/" exit 1 fi - if ! command -v docker-compose &> /dev/null; then - print_error "Docker Compose 未安装!请先安装 Docker Compose" - exit 1 - fi - + detect_compose_cmd print_success "Docker 和 Docker Compose 已安装" } -# 检查配置文件 +# ------------------------------------------------------------------------ +# Validation: Environment File (.env) +# ------------------------------------------------------------------------ +check_env() { + if [ ! -f ".env" ]; then + print_warning ".env 不存在,从模板复制..." + cp .env.example .env + print_info "请编辑 .env 填入你的环境变量配置" + print_info "运行: nano .env 或使用其他编辑器" + exit 1 + fi + print_success "环境变量文件存在" +} + +# ------------------------------------------------------------------------ +# Validation: Configuration File (config.json) +# ------------------------------------------------------------------------ check_config() { if [ ! -f "config.json" ]; then print_warning "config.json 不存在,从模板复制..." @@ -56,15 +91,53 @@ check_config() { print_success "配置文件存在" } -# 启动服务 +# ------------------------------------------------------------------------ +# Build: Frontend (Node.js Based) +# ------------------------------------------------------------------------ +# build_frontend() { +# print_info "检查前端构建环境..." + +# if ! command -v node &> /dev/null; then +# print_error "Node.js 未安装!请先安装 Node.js" +# exit 1 +# fi + +# if ! command -v npm &> /dev/null; then +# print_error "npm 未安装!请先安装 npm" +# exit 1 +# fi + +# print_info "正在构建前端..." +# cd web + +# print_info "安装 Node.js 依赖..." +# npm install + +# print_info "构建前端应用..." +# npm run build + +# cd .. +# print_success "前端构建完成" +# } + +# ------------------------------------------------------------------------ +# Service Management: Start +# ------------------------------------------------------------------------ start() { print_info "正在启动 NOFX AI Trading System..." + # Auto-build frontend if missing or forced + # if [ ! -d "web/dist" ] || [ "$1" == "--build" ]; then + # build_frontend + # fi + + # Rebuild images if flag set if [ "$1" == "--build" ]; then print_info "重新构建镜像..." - docker-compose up -d --build + $COMPOSE_CMD up -d --build else - docker-compose up -d + print_info "启动容器..." + $COMPOSE_CMD up -d fi print_success "服务已启动!" @@ -75,60 +148,74 @@ start() { print_info "停止服务: ./start.sh stop" } -# 停止服务 +# ------------------------------------------------------------------------ +# Service Management: Stop +# ------------------------------------------------------------------------ stop() { print_info "正在停止服务..." - docker-compose stop + $COMPOSE_CMD stop print_success "服务已停止" } -# 重启服务 +# ------------------------------------------------------------------------ +# Service Management: Restart +# ------------------------------------------------------------------------ restart() { print_info "正在重启服务..." - docker-compose restart + $COMPOSE_CMD restart print_success "服务已重启" } -# 查看日志 +# ------------------------------------------------------------------------ +# Monitoring: Logs +# ------------------------------------------------------------------------ logs() { if [ -z "$2" ]; then - docker-compose logs -f + $COMPOSE_CMD logs -f else - docker-compose logs -f "$2" + $COMPOSE_CMD logs -f "$2" fi } -# 查看状态 +# ------------------------------------------------------------------------ +# Monitoring: Status +# ------------------------------------------------------------------------ status() { print_info "服务状态:" - docker-compose ps + $COMPOSE_CMD ps echo "" print_info "健康检查:" curl -s http://localhost:8080/health | jq '.' || echo "后端未响应" } -# 清理 +# ------------------------------------------------------------------------ +# Maintenance: Clean (Destructive) +# ------------------------------------------------------------------------ clean() { print_warning "这将删除所有容器和数据!" read -p "确认删除?(yes/no): " confirm if [ "$confirm" == "yes" ]; then print_info "正在清理..." - docker-compose down -v + $COMPOSE_CMD down -v print_success "清理完成" else print_info "已取消" fi } -# 更新 +# ------------------------------------------------------------------------ +# Maintenance: Update +# ------------------------------------------------------------------------ update() { print_info "正在更新..." git pull - docker-compose up -d --build + $COMPOSE_CMD up -d --build print_success "更新完成" } -# 显示帮助 +# ------------------------------------------------------------------------ +# Help: Usage Information +# ------------------------------------------------------------------------ show_help() { echo "NOFX AI Trading System - Docker 管理脚本" echo "" @@ -150,12 +237,15 @@ show_help() { echo " ./start.sh status # 查看状态" } -# 主函数 +# ------------------------------------------------------------------------ +# Main: Command Dispatcher +# ------------------------------------------------------------------------ main() { check_docker case "${1:-start}" in start) + check_env check_config start "$2" ;; @@ -188,5 +278,5 @@ main() { esac } -# 运行主函数 -main "$@" +# Execute Main +main "$@" \ No newline at end of file diff --git a/trader/aster_trader.go b/trader/aster_trader.go new file mode 100644 index 00000000..b821be61 --- /dev/null +++ b/trader/aster_trader.go @@ -0,0 +1,959 @@ +package trader + +import ( + "context" + "crypto/ecdsa" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "math" + "math/big" + "net/http" + "net/url" + "sort" + "strconv" + "strings" + "sync" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +// AsterTrader Aster交易平台实现 +type AsterTrader struct { + ctx context.Context + user string // 主钱包地址 (ERC20) + signer string // API钱包地址 + privateKey *ecdsa.PrivateKey // API钱包私钥 + client *http.Client + baseURL string + + // 缓存交易对精度信息 + symbolPrecision map[string]SymbolPrecision + mu sync.RWMutex +} + +// SymbolPrecision 交易对精度信息 +type SymbolPrecision struct { + PricePrecision int + QuantityPrecision int + TickSize float64 // 价格步进值 + StepSize float64 // 数量步进值 +} + +// NewAsterTrader 创建Aster交易器 +// user: 主钱包地址 (登录地址) +// signer: API钱包地址 (从 https://www.asterdex.com/en/api-wallet 获取) +// privateKey: API钱包私钥 (从 https://www.asterdex.com/en/api-wallet 获取) +func NewAsterTrader(user, signer, privateKeyHex string) (*AsterTrader, error) { + // 解析私钥 + privKey, err := crypto.HexToECDSA(strings.TrimPrefix(privateKeyHex, "0x")) + if err != nil { + return nil, fmt.Errorf("解析私钥失败: %w", err) + } + + return &AsterTrader{ + ctx: context.Background(), + user: user, + signer: signer, + privateKey: privKey, + symbolPrecision: make(map[string]SymbolPrecision), + client: &http.Client{ + Timeout: 30 * time.Second, // 增加到30秒 + Transport: &http.Transport{ + TLSHandshakeTimeout: 10 * time.Second, + ResponseHeaderTimeout: 10 * time.Second, + IdleConnTimeout: 90 * time.Second, + }, + }, + baseURL: "https://fapi.asterdex.com", + }, nil +} + +// genNonce 生成微秒时间戳 +func (t *AsterTrader) genNonce() uint64 { + return uint64(time.Now().UnixMicro()) +} + +// getPrecision 获取交易对精度信息 +func (t *AsterTrader) getPrecision(symbol string) (SymbolPrecision, error) { + t.mu.RLock() + if prec, ok := t.symbolPrecision[symbol]; ok { + t.mu.RUnlock() + return prec, nil + } + t.mu.RUnlock() + + // 获取交易所信息 + resp, err := t.client.Get(t.baseURL + "/fapi/v3/exchangeInfo") + if err != nil { + return SymbolPrecision{}, err + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + var info struct { + Symbols []struct { + Symbol string `json:"symbol"` + PricePrecision int `json:"pricePrecision"` + QuantityPrecision int `json:"quantityPrecision"` + Filters []map[string]interface{} `json:"filters"` + } `json:"symbols"` + } + + if err := json.Unmarshal(body, &info); err != nil { + return SymbolPrecision{}, err + } + + // 缓存所有交易对的精度 + t.mu.Lock() + for _, s := range info.Symbols { + prec := SymbolPrecision{ + PricePrecision: s.PricePrecision, + QuantityPrecision: s.QuantityPrecision, + } + + // 解析filters获取tickSize和stepSize + for _, filter := range s.Filters { + filterType, _ := filter["filterType"].(string) + switch filterType { + case "PRICE_FILTER": + if tickSizeStr, ok := filter["tickSize"].(string); ok { + prec.TickSize, _ = strconv.ParseFloat(tickSizeStr, 64) + } + case "LOT_SIZE": + if stepSizeStr, ok := filter["stepSize"].(string); ok { + prec.StepSize, _ = strconv.ParseFloat(stepSizeStr, 64) + } + } + } + + t.symbolPrecision[s.Symbol] = prec + } + t.mu.Unlock() + + if prec, ok := t.symbolPrecision[symbol]; ok { + return prec, nil + } + + return SymbolPrecision{}, fmt.Errorf("未找到交易对 %s 的精度信息", symbol) +} + +// roundToTickSize 将价格/数量四舍五入到tick size/step size的整数倍 +func roundToTickSize(value float64, tickSize float64) float64 { + if tickSize <= 0 { + return value + } + // 计算有多少个tick size + steps := value / tickSize + // 四舍五入到最近的整数 + roundedSteps := math.Round(steps) + // 乘回tick size + return roundedSteps * tickSize +} + +// formatPrice 格式化价格到正确精度和tick size +func (t *AsterTrader) formatPrice(symbol string, price float64) (float64, error) { + prec, err := t.getPrecision(symbol) + if err != nil { + return 0, err + } + + // 优先使用tick size,确保价格是tick size的整数倍 + if prec.TickSize > 0 { + return roundToTickSize(price, prec.TickSize), nil + } + + // 如果没有tick size,则按精度四舍五入 + multiplier := math.Pow10(prec.PricePrecision) + return math.Round(price*multiplier) / multiplier, nil +} + +// formatQuantity 格式化数量到正确精度和step size +func (t *AsterTrader) formatQuantity(symbol string, quantity float64) (float64, error) { + prec, err := t.getPrecision(symbol) + if err != nil { + return 0, err + } + + // 优先使用step size,确保数量是step size的整数倍 + if prec.StepSize > 0 { + return roundToTickSize(quantity, prec.StepSize), nil + } + + // 如果没有step size,则按精度四舍五入 + multiplier := math.Pow10(prec.QuantityPrecision) + return math.Round(quantity*multiplier) / multiplier, nil +} + +// formatFloatWithPrecision 将浮点数格式化为指定精度的字符串(去除末尾的0) +func (t *AsterTrader) formatFloatWithPrecision(value float64, precision int) string { + // 使用指定精度格式化 + formatted := strconv.FormatFloat(value, 'f', precision, 64) + + // 去除末尾的0和小数点(如果有) + formatted = strings.TrimRight(formatted, "0") + formatted = strings.TrimRight(formatted, ".") + + return formatted +} + +// normalizeAndStringify 对参数进行规范化并序列化为JSON字符串(按key排序) +func (t *AsterTrader) normalizeAndStringify(params map[string]interface{}) (string, error) { + normalized, err := t.normalize(params) + if err != nil { + return "", err + } + bs, err := json.Marshal(normalized) + if err != nil { + return "", err + } + return string(bs), nil +} + +// normalize 递归规范化参数(按key排序,所有值转为字符串) +func (t *AsterTrader) normalize(v interface{}) (interface{}, error) { + switch val := v.(type) { + case map[string]interface{}: + keys := make([]string, 0, len(val)) + for k := range val { + keys = append(keys, k) + } + sort.Strings(keys) + newMap := make(map[string]interface{}, len(keys)) + for _, k := range keys { + nv, err := t.normalize(val[k]) + if err != nil { + return nil, err + } + newMap[k] = nv + } + return newMap, nil + case []interface{}: + out := make([]interface{}, 0, len(val)) + for _, it := range val { + nv, err := t.normalize(it) + if err != nil { + return nil, err + } + out = append(out, nv) + } + return out, nil + case string: + return val, nil + case int: + return fmt.Sprintf("%d", val), nil + case int64: + return fmt.Sprintf("%d", val), nil + case float64: + return fmt.Sprintf("%v", val), nil + case bool: + return fmt.Sprintf("%v", val), nil + default: + // 其他类型转为字符串 + return fmt.Sprintf("%v", val), nil + } +} + +// sign 对请求参数进行签名 +func (t *AsterTrader) sign(params map[string]interface{}, nonce uint64) error { + // 添加时间戳和接收窗口 + params["recvWindow"] = "50000" + params["timestamp"] = strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10) + + // 规范化参数为JSON字符串 + jsonStr, err := t.normalizeAndStringify(params) + if err != nil { + return err + } + + // ABI编码: (string, address, address, uint256) + addrUser := common.HexToAddress(t.user) + addrSigner := common.HexToAddress(t.signer) + nonceBig := new(big.Int).SetUint64(nonce) + + tString, _ := abi.NewType("string", "", nil) + tAddress, _ := abi.NewType("address", "", nil) + tUint256, _ := abi.NewType("uint256", "", nil) + + arguments := abi.Arguments{ + {Type: tString}, + {Type: tAddress}, + {Type: tAddress}, + {Type: tUint256}, + } + + packed, err := arguments.Pack(jsonStr, addrUser, addrSigner, nonceBig) + if err != nil { + return fmt.Errorf("ABI编码失败: %w", err) + } + + // Keccak256哈希 + hash := crypto.Keccak256(packed) + + // 以太坊签名消息前缀 + prefixedMsg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(hash), hash) + msgHash := crypto.Keccak256Hash([]byte(prefixedMsg)) + + // ECDSA签名 + sig, err := crypto.Sign(msgHash.Bytes(), t.privateKey) + if err != nil { + return fmt.Errorf("签名失败: %w", err) + } + + // 将v从0/1转换为27/28 + if len(sig) != 65 { + return fmt.Errorf("签名长度异常: %d", len(sig)) + } + sig[64] += 27 + + // 添加签名参数 + params["user"] = t.user + params["signer"] = t.signer + params["signature"] = "0x" + hex.EncodeToString(sig) + params["nonce"] = nonce + + return nil +} + +// request 发送HTTP请求(带重试机制) +func (t *AsterTrader) request(method, endpoint string, params map[string]interface{}) ([]byte, error) { + const maxRetries = 3 + var lastErr error + + for attempt := 1; attempt <= maxRetries; attempt++ { + // 每次重试都生成新的nonce和签名 + nonce := t.genNonce() + paramsCopy := make(map[string]interface{}) + for k, v := range params { + paramsCopy[k] = v + } + + // 签名 + if err := t.sign(paramsCopy, nonce); err != nil { + return nil, err + } + + body, err := t.doRequest(method, endpoint, paramsCopy) + if err == nil { + return body, nil + } + + lastErr = err + + // 如果是网络超时或临时错误,重试 + if strings.Contains(err.Error(), "timeout") || + strings.Contains(err.Error(), "connection reset") || + strings.Contains(err.Error(), "EOF") { + if attempt < maxRetries { + waitTime := time.Duration(attempt) * time.Second + time.Sleep(waitTime) + continue + } + } + + // 其他错误(如400/401等)不重试 + return nil, err + } + + return nil, fmt.Errorf("请求失败(已重试%d次): %w", maxRetries, lastErr) +} + +// doRequest 执行实际的HTTP请求 +func (t *AsterTrader) doRequest(method, endpoint string, params map[string]interface{}) ([]byte, error) { + fullURL := t.baseURL + endpoint + method = strings.ToUpper(method) + + switch method { + case "POST": + // POST请求:参数放在表单body中 + form := url.Values{} + for k, v := range params { + form.Set(k, fmt.Sprintf("%v", v)) + } + req, err := http.NewRequest("POST", fullURL, strings.NewReader(form.Encode())) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := t.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)) + } + return body, nil + + case "GET", "DELETE": + // GET/DELETE请求:参数放在querystring中 + q := url.Values{} + for k, v := range params { + q.Set(k, fmt.Sprintf("%v", v)) + } + u, _ := url.Parse(fullURL) + u.RawQuery = q.Encode() + + req, err := http.NewRequest(method, u.String(), nil) + if err != nil { + return nil, err + } + + resp, err := t.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)) + } + return body, nil + + default: + return nil, fmt.Errorf("不支持的HTTP方法: %s", method) + } +} + +// GetBalance 获取账户余额 +func (t *AsterTrader) GetBalance() (map[string]interface{}, error) { + params := make(map[string]interface{}) + body, err := t.request("GET", "/fapi/v3/balance", params) + if err != nil { + return nil, err + } + + var balances []map[string]interface{} + if err := json.Unmarshal(body, &balances); err != nil { + return nil, err + } + + // 查找USDT余额 + totalBalance := 0.0 + availableBalance := 0.0 + crossUnPnl := 0.0 + + for _, bal := range balances { + if asset, ok := bal["asset"].(string); ok && asset == "USDT" { + if wb, ok := bal["balance"].(string); ok { + totalBalance, _ = strconv.ParseFloat(wb, 64) + } + if avail, ok := bal["availableBalance"].(string); ok { + availableBalance, _ = strconv.ParseFloat(avail, 64) + } + if unpnl, ok := bal["crossUnPnl"].(string); ok { + crossUnPnl, _ = strconv.ParseFloat(unpnl, 64) + } + break + } + } + + // 返回与Binance相同的字段名,确保AutoTrader能正确解析 + return map[string]interface{}{ + "totalWalletBalance": totalBalance, + "availableBalance": availableBalance, + "totalUnrealizedProfit": crossUnPnl, + }, nil +} + +// GetPositions 获取持仓信息 +func (t *AsterTrader) GetPositions() ([]map[string]interface{}, error) { + params := make(map[string]interface{}) + body, err := t.request("GET", "/fapi/v3/positionRisk", params) + if err != nil { + return nil, err + } + + var positions []map[string]interface{} + if err := json.Unmarshal(body, &positions); err != nil { + return nil, err + } + + result := []map[string]interface{}{} + for _, pos := range positions { + posAmtStr, ok := pos["positionAmt"].(string) + if !ok { + continue + } + + posAmt, _ := strconv.ParseFloat(posAmtStr, 64) + if posAmt == 0 { + continue // 跳过空仓位 + } + + entryPrice, _ := strconv.ParseFloat(pos["entryPrice"].(string), 64) + markPrice, _ := strconv.ParseFloat(pos["markPrice"].(string), 64) + unRealizedProfit, _ := strconv.ParseFloat(pos["unRealizedProfit"].(string), 64) + leverageVal, _ := strconv.ParseFloat(pos["leverage"].(string), 64) + liquidationPrice, _ := strconv.ParseFloat(pos["liquidationPrice"].(string), 64) + + // 判断方向(与Binance一致) + side := "long" + if posAmt < 0 { + side = "short" + posAmt = -posAmt + } + + // 返回与Binance相同的字段名 + result = append(result, map[string]interface{}{ + "symbol": pos["symbol"], + "side": side, + "positionAmt": posAmt, + "entryPrice": entryPrice, + "markPrice": markPrice, + "unRealizedProfit": unRealizedProfit, + "leverage": leverageVal, + "liquidationPrice": liquidationPrice, + }) + } + + return result, nil +} + +// OpenLong 开多单 +func (t *AsterTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { + // 开仓前先取消所有挂单,防止残留挂单导致仓位叠加 + if err := t.CancelAllOrders(symbol); err != nil { + log.Printf(" ⚠ 取消挂单失败(继续开仓): %v", err) + } + + // 先设置杠杆 + if err := t.SetLeverage(symbol, leverage); err != nil { + return nil, fmt.Errorf("设置杠杆失败: %w", err) + } + + // 获取当前价格 + price, err := t.GetMarketPrice(symbol) + if err != nil { + return nil, err + } + + // 使用限价单模拟市价单(价格设置得稍高一些以确保成交) + limitPrice := price * 1.01 + + // 格式化价格和数量到正确精度 + formattedPrice, err := t.formatPrice(symbol, limitPrice) + if err != nil { + return nil, err + } + formattedQty, err := t.formatQuantity(symbol, quantity) + if err != nil { + return nil, err + } + + // 获取精度信息 + prec, err := t.getPrecision(symbol) + if err != nil { + return nil, err + } + + // 转换为字符串,使用正确的精度格式 + priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision) + qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision) + + log.Printf(" 📏 精度处理: 价格 %.8f -> %s (精度=%d), 数量 %.8f -> %s (精度=%d)", + limitPrice, priceStr, prec.PricePrecision, quantity, qtyStr, prec.QuantityPrecision) + + params := map[string]interface{}{ + "symbol": symbol, + "positionSide": "BOTH", + "type": "LIMIT", + "side": "BUY", + "timeInForce": "GTC", + "quantity": qtyStr, + "price": priceStr, + } + + body, err := t.request("POST", "/fapi/v3/order", params) + if err != nil { + return nil, err + } + + var result map[string]interface{} + if err := json.Unmarshal(body, &result); err != nil { + return nil, err + } + + return result, nil +} + +// OpenShort 开空单 +func (t *AsterTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) { + // 开仓前先取消所有挂单,防止残留挂单导致仓位叠加 + if err := t.CancelAllOrders(symbol); err != nil { + log.Printf(" ⚠ 取消挂单失败(继续开仓): %v", err) + } + + // 先设置杠杆 + if err := t.SetLeverage(symbol, leverage); err != nil { + return nil, fmt.Errorf("设置杠杆失败: %w", err) + } + + // 获取当前价格 + price, err := t.GetMarketPrice(symbol) + if err != nil { + return nil, err + } + + // 使用限价单模拟市价单(价格设置得稍低一些以确保成交) + limitPrice := price * 0.99 + + // 格式化价格和数量到正确精度 + formattedPrice, err := t.formatPrice(symbol, limitPrice) + if err != nil { + return nil, err + } + formattedQty, err := t.formatQuantity(symbol, quantity) + if err != nil { + return nil, err + } + + // 获取精度信息 + prec, err := t.getPrecision(symbol) + if err != nil { + return nil, err + } + + // 转换为字符串,使用正确的精度格式 + priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision) + qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision) + + log.Printf(" 📏 精度处理: 价格 %.8f -> %s (精度=%d), 数量 %.8f -> %s (精度=%d)", + limitPrice, priceStr, prec.PricePrecision, quantity, qtyStr, prec.QuantityPrecision) + + params := map[string]interface{}{ + "symbol": symbol, + "positionSide": "BOTH", + "type": "LIMIT", + "side": "SELL", + "timeInForce": "GTC", + "quantity": qtyStr, + "price": priceStr, + } + + body, err := t.request("POST", "/fapi/v3/order", params) + if err != nil { + return nil, err + } + + var result map[string]interface{} + if err := json.Unmarshal(body, &result); err != nil { + return nil, err + } + + return result, nil +} + +// CloseLong 平多单 +func (t *AsterTrader) CloseLong(symbol string, quantity float64) (map[string]interface{}, error) { + // 如果数量为0,获取当前持仓数量 + if quantity == 0 { + positions, err := t.GetPositions() + if err != nil { + return nil, err + } + + for _, pos := range positions { + if pos["symbol"] == symbol && pos["side"] == "long" { + quantity = pos["positionAmt"].(float64) + break + } + } + + if quantity == 0 { + return nil, fmt.Errorf("没有找到 %s 的多仓", symbol) + } + log.Printf(" 📊 获取到多仓数量: %.8f", quantity) + } + + price, err := t.GetMarketPrice(symbol) + if err != nil { + return nil, err + } + + limitPrice := price * 0.99 + + // 格式化价格和数量到正确精度 + formattedPrice, err := t.formatPrice(symbol, limitPrice) + if err != nil { + return nil, err + } + formattedQty, err := t.formatQuantity(symbol, quantity) + if err != nil { + return nil, err + } + + // 获取精度信息 + prec, err := t.getPrecision(symbol) + if err != nil { + return nil, err + } + + // 转换为字符串,使用正确的精度格式 + priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision) + qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision) + + log.Printf(" 📏 精度处理: 价格 %.8f -> %s (精度=%d), 数量 %.8f -> %s (精度=%d)", + limitPrice, priceStr, prec.PricePrecision, quantity, qtyStr, prec.QuantityPrecision) + + params := map[string]interface{}{ + "symbol": symbol, + "positionSide": "BOTH", + "type": "LIMIT", + "side": "SELL", + "timeInForce": "GTC", + "quantity": qtyStr, + "price": priceStr, + } + + body, err := t.request("POST", "/fapi/v3/order", params) + if err != nil { + return nil, err + } + + var result map[string]interface{} + if err := json.Unmarshal(body, &result); err != nil { + return nil, err + } + + log.Printf("✓ 平多仓成功: %s 数量: %s", symbol, qtyStr) + + // 平仓后取消该币种的所有挂单(止损止盈单) + if err := t.CancelAllOrders(symbol); err != nil { + log.Printf(" ⚠ 取消挂单失败: %v", err) + } + + return result, nil +} + +// CloseShort 平空单 +func (t *AsterTrader) CloseShort(symbol string, quantity float64) (map[string]interface{}, error) { + // 如果数量为0,获取当前持仓数量 + if quantity == 0 { + positions, err := t.GetPositions() + if err != nil { + return nil, err + } + + for _, pos := range positions { + if pos["symbol"] == symbol && pos["side"] == "short" { + // Aster的GetPositions已经将空仓数量转换为正数,直接使用 + quantity = pos["positionAmt"].(float64) + break + } + } + + if quantity == 0 { + return nil, fmt.Errorf("没有找到 %s 的空仓", symbol) + } + log.Printf(" 📊 获取到空仓数量: %.8f", quantity) + } + + price, err := t.GetMarketPrice(symbol) + if err != nil { + return nil, err + } + + limitPrice := price * 1.01 + + // 格式化价格和数量到正确精度 + formattedPrice, err := t.formatPrice(symbol, limitPrice) + if err != nil { + return nil, err + } + formattedQty, err := t.formatQuantity(symbol, quantity) + if err != nil { + return nil, err + } + + // 获取精度信息 + prec, err := t.getPrecision(symbol) + if err != nil { + return nil, err + } + + // 转换为字符串,使用正确的精度格式 + priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision) + qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision) + + log.Printf(" 📏 精度处理: 价格 %.8f -> %s (精度=%d), 数量 %.8f -> %s (精度=%d)", + limitPrice, priceStr, prec.PricePrecision, quantity, qtyStr, prec.QuantityPrecision) + + params := map[string]interface{}{ + "symbol": symbol, + "positionSide": "BOTH", + "type": "LIMIT", + "side": "BUY", + "timeInForce": "GTC", + "quantity": qtyStr, + "price": priceStr, + } + + body, err := t.request("POST", "/fapi/v3/order", params) + if err != nil { + return nil, err + } + + var result map[string]interface{} + if err := json.Unmarshal(body, &result); err != nil { + return nil, err + } + + log.Printf("✓ 平空仓成功: %s 数量: %s", symbol, qtyStr) + + // 平仓后取消该币种的所有挂单(止损止盈单) + if err := t.CancelAllOrders(symbol); err != nil { + log.Printf(" ⚠ 取消挂单失败: %v", err) + } + + return result, nil +} + +// SetLeverage 设置杠杆倍数 +func (t *AsterTrader) SetLeverage(symbol string, leverage int) error { + params := map[string]interface{}{ + "symbol": symbol, + "leverage": leverage, + } + + _, err := t.request("POST", "/fapi/v3/leverage", params) + return err +} + +// GetMarketPrice 获取市场价格 +func (t *AsterTrader) GetMarketPrice(symbol string) (float64, error) { + // 使用ticker接口获取当前价格 + resp, err := t.client.Get(fmt.Sprintf("%s/fapi/v3/ticker/price?symbol=%s", t.baseURL, symbol)) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return 0, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(body)) + } + + var result map[string]interface{} + if err := json.Unmarshal(body, &result); err != nil { + return 0, err + } + + priceStr, ok := result["price"].(string) + if !ok { + return 0, errors.New("无法获取价格") + } + + return strconv.ParseFloat(priceStr, 64) +} + +// SetStopLoss 设置止损 +func (t *AsterTrader) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error { + side := "SELL" + if positionSide == "SHORT" { + side = "BUY" + } + + // 格式化价格和数量到正确精度 + formattedPrice, err := t.formatPrice(symbol, stopPrice) + if err != nil { + return err + } + formattedQty, err := t.formatQuantity(symbol, quantity) + if err != nil { + return err + } + + // 获取精度信息 + prec, err := t.getPrecision(symbol) + if err != nil { + return err + } + + // 转换为字符串,使用正确的精度格式 + priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision) + qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision) + + params := map[string]interface{}{ + "symbol": symbol, + "positionSide": "BOTH", + "type": "STOP_MARKET", + "side": side, + "stopPrice": priceStr, + "quantity": qtyStr, + "timeInForce": "GTC", + } + + _, err = t.request("POST", "/fapi/v3/order", params) + return err +} + +// SetTakeProfit 设置止盈 +func (t *AsterTrader) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error { + side := "SELL" + if positionSide == "SHORT" { + side = "BUY" + } + + // 格式化价格和数量到正确精度 + formattedPrice, err := t.formatPrice(symbol, takeProfitPrice) + if err != nil { + return err + } + formattedQty, err := t.formatQuantity(symbol, quantity) + if err != nil { + return err + } + + // 获取精度信息 + prec, err := t.getPrecision(symbol) + if err != nil { + return err + } + + // 转换为字符串,使用正确的精度格式 + priceStr := t.formatFloatWithPrecision(formattedPrice, prec.PricePrecision) + qtyStr := t.formatFloatWithPrecision(formattedQty, prec.QuantityPrecision) + + params := map[string]interface{}{ + "symbol": symbol, + "positionSide": "BOTH", + "type": "TAKE_PROFIT_MARKET", + "side": side, + "stopPrice": priceStr, + "quantity": qtyStr, + "timeInForce": "GTC", + } + + _, err = t.request("POST", "/fapi/v3/order", params) + return err +} + +// CancelAllOrders 取消所有订单 +func (t *AsterTrader) CancelAllOrders(symbol string) error { + params := map[string]interface{}{ + "symbol": symbol, + } + + _, err := t.request("DELETE", "/fapi/v3/allOpenOrders", params) + return err +} + +// FormatQuantity 格式化数量(实现Trader接口) +func (t *AsterTrader) FormatQuantity(symbol string, quantity float64) (string, error) { + formatted, err := t.formatQuantity(symbol, quantity) + if err != nil { + return "", err + } + return fmt.Sprintf("%v", formatted), nil +} diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 64ad0e65..42bc2e69 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -21,7 +21,7 @@ type AutoTraderConfig struct { AIModel string // AI模型: "qwen" 或 "deepseek" // 交易平台选择 - Exchange string // "binance" 或 "hyperliquid" + Exchange string // "binance", "hyperliquid" 或 "aster" // 币安API配置 BinanceAPIKey string @@ -29,8 +29,14 @@ type AutoTraderConfig struct { // Hyperliquid配置 HyperliquidPrivateKey string + HyperliquidWalletAddr string HyperliquidTestnet bool + // Aster配置 + AsterUser string // Aster主钱包地址 + AsterSigner string // Aster API钱包地址 + AsterPrivateKey string // Aster API钱包私钥 + CoinPoolAPIURL string // AI配置 @@ -38,12 +44,21 @@ type AutoTraderConfig struct { DeepSeekKey string QwenKey string + // 自定义AI API配置 + CustomAPIURL string + CustomAPIKey string + CustomModelName string + // 扫描配置 ScanInterval time.Duration // 扫描间隔(建议3分钟) // 账户配置 InitialBalance float64 // 初始金额(用于计算盈亏,需手动设置) + // 杠杆配置 + BTCETHLeverage int // BTC和ETH的杠杆倍数 + AltcoinLeverage int // 山寨币的杠杆倍数 + // 风险控制(仅作为提示,AI可自主决定) MaxDailyLoss float64 // 最大日亏损百分比(提示) MaxDrawdown float64 // 最大回撤百分比(提示) @@ -52,21 +67,22 @@ type AutoTraderConfig struct { // AutoTrader 自动交易器 type AutoTrader struct { - id string // Trader唯一标识 - name string // Trader显示名称 - aiModel string // AI模型名称 - exchange string // 交易平台名称 - config AutoTraderConfig - trader Trader // 使用Trader接口(支持多平台) - decisionLogger *logger.DecisionLogger // 决策日志记录器 - initialBalance float64 - dailyPnL float64 - lastResetTime time.Time - stopUntil time.Time - isRunning bool - startTime time.Time // 系统启动时间 - callCount int // AI调用次数 - positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒) + id string // Trader唯一标识 + name string // Trader显示名称 + aiModel string // AI模型名称 + exchange string // 交易平台名称 + config AutoTraderConfig + trader Trader // 使用Trader接口(支持多平台) + mcpClient *mcp.Client + decisionLogger *logger.DecisionLogger // 决策日志记录器 + initialBalance float64 + dailyPnL float64 + lastResetTime time.Time + stopUntil time.Time + isRunning bool + startTime time.Time // 系统启动时间 + callCount int // AI调用次数 + positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒) } // NewAutoTrader 创建自动交易器 @@ -86,12 +102,20 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) { } } + mcpClient := mcp.New() + // 初始化AI - if config.UseQwen { - mcp.SetQwenAPIKey(config.QwenKey, "") + if config.AIModel == "custom" { + // 使用自定义API + mcpClient.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 + mcpClient.SetQwenAPIKey(config.QwenKey, "") log.Printf("🤖 [%s] 使用阿里云Qwen AI", config.Name) } else { - mcp.SetDeepSeekAPIKey(config.DeepSeekKey) + // 默认使用DeepSeek + mcpClient.SetDeepSeekAPIKey(config.DeepSeekKey) log.Printf("🤖 [%s] 使用DeepSeek AI", config.Name) } @@ -115,10 +139,16 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) { trader = NewFuturesTrader(config.BinanceAPIKey, config.BinanceSecretKey) case "hyperliquid": log.Printf("🏦 [%s] 使用Hyperliquid交易", config.Name) - trader, err = NewHyperliquidTrader(config.HyperliquidPrivateKey, config.HyperliquidTestnet) + trader, err = NewHyperliquidTrader(config.HyperliquidPrivateKey, config.HyperliquidWalletAddr, config.HyperliquidTestnet) if err != nil { return nil, fmt.Errorf("初始化Hyperliquid交易器失败: %w", err) } + case "aster": + log.Printf("🏦 [%s] 使用Aster交易", config.Name) + trader, err = NewAsterTrader(config.AsterUser, config.AsterSigner, config.AsterPrivateKey) + if err != nil { + return nil, fmt.Errorf("初始化Aster交易器失败: %w", err) + } default: return nil, fmt.Errorf("不支持的交易平台: %s", config.Exchange) } @@ -133,18 +163,19 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) { decisionLogger := logger.NewDecisionLogger(logDir) return &AutoTrader{ - id: config.ID, - name: config.Name, - aiModel: config.AIModel, - exchange: config.Exchange, - config: config, - trader: trader, - decisionLogger: decisionLogger, - initialBalance: config.InitialBalance, - lastResetTime: time.Now(), - startTime: time.Now(), - callCount: 0, - isRunning: false, + id: config.ID, + name: config.Name, + aiModel: config.AIModel, + exchange: config.Exchange, + config: config, + trader: trader, + mcpClient: mcpClient, + decisionLogger: decisionLogger, + initialBalance: config.InitialBalance, + lastResetTime: time.Now(), + startTime: time.Now(), + callCount: 0, + isRunning: false, positionFirstSeenTime: make(map[string]int64), }, nil } @@ -256,7 +287,7 @@ func (at *AutoTrader) runCycle() error { // 4. 调用AI获取完整决策 log.Println("🤖 正在请求AI分析并决策...") - decision, err := decision.GetFullDecision(ctx) + decision, err := decision.GetFullDecision(ctx, at.mcpClient) // 即使有错误,也保存思维链、决策和输入prompt(用于debug) if decision != nil { @@ -479,8 +510,9 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { marginUsedPct = (totalMarginUsed / totalEquity) * 100 } - // 5. 分析历史表现(最近20个周期) - performance, err := at.decisionLogger.AnalyzePerformance(20) + // 5. 分析历史表现(最近100个周期,避免长期持仓的交易记录丢失) + // 假设每3分钟一个周期,100个周期 = 5小时,足够覆盖大部分交易 + performance, err := at.decisionLogger.AnalyzePerformance(100) if err != nil { log.Printf("⚠️ 分析历史表现失败: %v", err) // 不影响主流程,继续执行(但设置performance为nil以避免传递错误数据) @@ -489,9 +521,11 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { // 6. 构建上下文 ctx := &decision.Context{ - CurrentTime: time.Now().Format("2006-01-02 15:04:05"), - RuntimeMinutes: int(time.Since(at.startTime).Minutes()), - CallCount: at.callCount, + CurrentTime: time.Now().Format("2006-01-02 15:04:05"), + RuntimeMinutes: int(time.Since(at.startTime).Minutes()), + CallCount: at.callCount, + BTCETHLeverage: at.config.BTCETHLeverage, // 使用配置的杠杆倍数 + AltcoinLeverage: at.config.AltcoinLeverage, // 使用配置的杠杆倍数 Account: decision.AccountInfo{ TotalEquity: totalEquity, AvailableBalance: availableBalance, diff --git a/trader/binance_futures.go b/trader/binance_futures.go index db6a443a..ae85afa4 100644 --- a/trader/binance_futures.go +++ b/trader/binance_futures.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "strconv" + "sync" "time" "github.com/adshao/go-binance/v2/futures" @@ -13,19 +14,44 @@ import ( // FuturesTrader 币安合约交易器 type FuturesTrader struct { client *futures.Client + + // 余额缓存 + cachedBalance map[string]interface{} + balanceCacheTime time.Time + balanceCacheMutex sync.RWMutex + + // 持仓缓存 + cachedPositions []map[string]interface{} + positionsCacheTime time.Time + positionsCacheMutex sync.RWMutex + + // 缓存有效期(15秒) + cacheDuration time.Duration } // NewFuturesTrader 创建合约交易器 func NewFuturesTrader(apiKey, secretKey string) *FuturesTrader { client := futures.NewClient(apiKey, secretKey) return &FuturesTrader{ - client: client, + client: client, + cacheDuration: 15 * time.Second, // 15秒缓存 } } -// GetBalance 获取账户余额 +// GetBalance 获取账户余额(带缓存) func (t *FuturesTrader) GetBalance() (map[string]interface{}, error) { - log.Printf("🔄 正在调用币安API获取账户余额...") + // 先检查缓存是否有效 + t.balanceCacheMutex.RLock() + if t.cachedBalance != nil && time.Since(t.balanceCacheTime) < t.cacheDuration { + cacheAge := time.Since(t.balanceCacheTime) + t.balanceCacheMutex.RUnlock() + log.Printf("✓ 使用缓存的账户余额(缓存时间: %.1f秒前)", cacheAge.Seconds()) + return t.cachedBalance, nil + } + t.balanceCacheMutex.RUnlock() + + // 缓存过期或不存在,调用API + log.Printf("🔄 缓存过期,正在调用币安API获取账户余额...") account, err := t.client.NewGetAccountService().Do(context.Background()) if err != nil { log.Printf("❌ 币安API调用失败: %v", err) @@ -42,11 +68,29 @@ func (t *FuturesTrader) GetBalance() (map[string]interface{}, error) { account.AvailableBalance, account.TotalUnrealizedProfit) + // 更新缓存 + t.balanceCacheMutex.Lock() + t.cachedBalance = result + t.balanceCacheTime = time.Now() + t.balanceCacheMutex.Unlock() + return result, nil } -// GetPositions 获取所有持仓 +// GetPositions 获取所有持仓(带缓存) func (t *FuturesTrader) GetPositions() ([]map[string]interface{}, error) { + // 先检查缓存是否有效 + t.positionsCacheMutex.RLock() + if t.cachedPositions != nil && time.Since(t.positionsCacheTime) < t.cacheDuration { + cacheAge := time.Since(t.positionsCacheTime) + t.positionsCacheMutex.RUnlock() + log.Printf("✓ 使用缓存的持仓信息(缓存时间: %.1f秒前)", cacheAge.Seconds()) + return t.cachedPositions, nil + } + t.positionsCacheMutex.RUnlock() + + // 缓存过期或不存在,调用API + log.Printf("🔄 缓存过期,正在调用币安API获取持仓信息...") positions, err := t.client.NewGetPositionRiskService().Do(context.Background()) if err != nil { return nil, fmt.Errorf("获取持仓失败: %w", err) @@ -78,6 +122,12 @@ func (t *FuturesTrader) GetPositions() ([]map[string]interface{}, error) { result = append(result, posMap) } + // 更新缓存 + t.positionsCacheMutex.Lock() + t.cachedPositions = result + t.positionsCacheTime = time.Now() + t.positionsCacheMutex.Unlock() + return result, nil } diff --git a/trader/hyperliquid_trader.go b/trader/hyperliquid_trader.go index a9402209..b3364eb2 100644 --- a/trader/hyperliquid_trader.go +++ b/trader/hyperliquid_trader.go @@ -2,7 +2,6 @@ package trader import ( "context" - "crypto/ecdsa" "fmt" "log" "strconv" @@ -20,7 +19,7 @@ type HyperliquidTrader struct { } // NewHyperliquidTrader 创建Hyperliquid交易器 -func NewHyperliquidTrader(privateKeyHex string, testnet bool) (*HyperliquidTrader, error) { +func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool) (*HyperliquidTrader, error) { // 解析私钥 privateKey, err := crypto.HexToECDSA(privateKeyHex) if err != nil { @@ -33,13 +32,13 @@ func NewHyperliquidTrader(privateKeyHex string, testnet bool) (*HyperliquidTrade apiURL = hyperliquid.TestnetAPIURL } - // 从私钥生成钱包地址 - pubKey := privateKey.Public() - publicKeyECDSA, ok := pubKey.(*ecdsa.PublicKey) - if !ok { - return nil, fmt.Errorf("无法转换公钥") - } - walletAddr := crypto.PubkeyToAddress(*publicKeyECDSA).Hex() + // // 从私钥生成钱包地址 + // pubKey := privateKey.Public() + // publicKeyECDSA, ok := pubKey.(*ecdsa.PublicKey) + // if !ok { + // return nil, fmt.Errorf("无法转换公钥") + // } + // walletAddr := crypto.PubkeyToAddress(*publicKeyECDSA).Hex() ctx := context.Background() @@ -86,6 +85,7 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) { accountValue, _ := strconv.ParseFloat(accountState.CrossMarginSummary.AccountValue, 64) totalMarginUsed, _ := strconv.ParseFloat(accountState.CrossMarginSummary.TotalMarginUsed, 64) + availableBalance, _ := strconv.ParseFloat(accountState.CrossMarginSummary.AccountValue, 64) // ⚠️ 关键修复:从所有持仓中累加真正的未实现盈亏 totalUnrealizedPnl := 0.0 @@ -99,9 +99,9 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) { // 钱包余额(已实现)= AccountValue - 未实现盈亏 walletBalance := accountValue - totalUnrealizedPnl - result["totalWalletBalance"] = walletBalance // 钱包余额(已实现部分) - result["availableBalance"] = accountValue - totalMarginUsed // 可用余额 - result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏 + result["totalWalletBalance"] = walletBalance // 钱包余额(已实现部分) + result["availableBalance"] = availableBalance - totalMarginUsed // 可用余额 + result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏 log.Printf("✓ Hyperliquid API返回: 账户净值=%.2f, 钱包余额=%.2f, 可用=%.2f, 未实现盈亏=%.2f", accountValue, @@ -515,8 +515,8 @@ func (t *HyperliquidTrader) SetStopLoss(symbol string, positionSide string, quan order := hyperliquid.CreateOrderRequest{ Coin: coin, IsBuy: isBuy, - Size: roundedQuantity, // 使用四舍五入后的数量 - Price: roundedStopPrice, // 使用处理后的价格 + Size: roundedQuantity, // 使用四舍五入后的数量 + Price: roundedStopPrice, // 使用处理后的价格 OrderType: hyperliquid.OrderType{ Trigger: &hyperliquid.TriggerOrderType{ TriggerPx: roundedStopPrice, @@ -552,8 +552,8 @@ func (t *HyperliquidTrader) SetTakeProfit(symbol string, positionSide string, qu order := hyperliquid.CreateOrderRequest{ Coin: coin, IsBuy: isBuy, - Size: roundedQuantity, // 使用四舍五入后的数量 - Price: roundedTakeProfitPrice, // 使用处理后的价格 + Size: roundedQuantity, // 使用四舍五入后的数量 + Price: roundedTakeProfitPrice, // 使用处理后的价格 OrderType: hyperliquid.OrderType{ Trigger: &hyperliquid.TriggerOrderType{ TriggerPx: roundedTakeProfitPrice, @@ -577,7 +577,7 @@ func (t *HyperliquidTrader) SetTakeProfit(symbol string, positionSide string, qu func (t *HyperliquidTrader) FormatQuantity(symbol string, quantity float64) (string, error) { coin := convertSymbolToHyperliquid(symbol) szDecimals := t.getSzDecimals(coin) - + // 使用szDecimals格式化数量 formatStr := fmt.Sprintf("%%.%df", szDecimals) return fmt.Sprintf(formatStr, quantity), nil @@ -604,13 +604,13 @@ func (t *HyperliquidTrader) getSzDecimals(coin string) int { // roundToSzDecimals 将数量四舍五入到正确的精度 func (t *HyperliquidTrader) roundToSzDecimals(coin string, quantity float64) float64 { szDecimals := t.getSzDecimals(coin) - + // 计算倍数(10^szDecimals) multiplier := 1.0 for i := 0; i < szDecimals; i++ { multiplier *= 10.0 } - + // 四舍五入 return float64(int(quantity*multiplier+0.5)) / multiplier } @@ -621,9 +621,9 @@ func (t *HyperliquidTrader) roundPriceToSigfigs(price float64) float64 { if price == 0 { return 0 } - + const sigfigs = 5 // Hyperliquid标准:5位有效数字 - + // 计算价格的数量级 var magnitude float64 if price < 0 { @@ -631,7 +631,7 @@ func (t *HyperliquidTrader) roundPriceToSigfigs(price float64) float64 { } else { magnitude = price } - + // 计算需要的倍数 multiplier := 1.0 for magnitude >= 10 { @@ -642,12 +642,12 @@ func (t *HyperliquidTrader) roundPriceToSigfigs(price float64) float64 { magnitude *= 10 multiplier *= 10 } - + // 应用有效数字精度 for i := 0; i < sigfigs-1; i++ { multiplier *= 10 } - + // 四舍五入 rounded := float64(int(price*multiplier+0.5)) / multiplier return rounded diff --git a/web/Dockerfile b/web/Dockerfile deleted file mode 100644 index 5219148c..00000000 --- a/web/Dockerfile +++ /dev/null @@ -1,72 +0,0 @@ -# 构建阶段 -FROM node:20-alpine AS builder - -# 设置工作目录 -WORKDIR /app - -# 复制 package 文件 -COPY package*.json ./ - -# 安装依赖 -RUN npm ci - -# 复制源代码 -COPY . . - -# 构建应用 -RUN npm run build - -# 运行阶段 -FROM nginx:alpine - -# 复制自定义 nginx 配置 -COPY < api.getStatus(selectedTraderId), { - refreshInterval: 5000, - revalidateOnFocus: true, - dedupingInterval: 0, + refreshInterval: 15000, // 15秒刷新(配合后端15秒缓存) + revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求 + dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求 } ); @@ -54,9 +54,9 @@ function App() { : null, () => api.getAccount(selectedTraderId), { - refreshInterval: 5000, - revalidateOnFocus: true, - dedupingInterval: 0, + refreshInterval: 15000, // 15秒刷新(配合后端15秒缓存) + revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求 + dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求 } ); @@ -66,9 +66,9 @@ function App() { : null, () => api.getPositions(selectedTraderId), { - refreshInterval: 5000, - revalidateOnFocus: true, - dedupingInterval: 0, + refreshInterval: 15000, // 15秒刷新(配合后端15秒缓存) + revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求 + dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求 } ); @@ -77,7 +77,11 @@ function App() { ? `decisions/latest-${selectedTraderId}` : null, () => api.getLatestDecisions(selectedTraderId), - { refreshInterval: 10000 } + { + refreshInterval: 30000, // 30秒刷新(决策更新频率较低) + revalidateOnFocus: false, + dedupingInterval: 20000, + } ); const { data: stats } = useSWR( @@ -85,7 +89,11 @@ function App() { ? `statistics-${selectedTraderId}` : null, () => api.getStatistics(selectedTraderId), - { refreshInterval: 10000 } + { + refreshInterval: 30000, // 30秒刷新(统计数据更新频率较低) + revalidateOnFocus: false, + dedupingInterval: 20000, + } ); useEffect(() => { @@ -101,55 +109,104 @@ function App() {
{/* Header - Binance Style */}
-
-
- {/* Left - Logo and Title */} -
-
+
+ {/* Mobile: Two rows, Desktop: Single row */} +
+ {/* Left: Logo and Title */} +
+
-

+

{t('appTitle', language)}

-

+

{t('subtitle', language)}

- - {/* Center - Page Toggle (absolutely positioned) */} -
- - -
- - {/* Right - Actions */} -
+ + + + GitHub + + + {/* Language Toggle */} +
+ + +
+ + {/* Page Toggle */} +
+ + +
+ {/* Trader Selector (only show on trader page) */} {currentPage === 'trader' && traders && traders.length > 0 && ( )} - {/* Language Toggle */} -
- - -
- {/* Status Indicator (only show on trader page) */} {currentPage === 'trader' && status && (
0 : false} + positive={(account?.total_pnl ?? 0) > 0} /> = 0 ? '+' : ''}${account?.total_pnl?.toFixed(2) || '0.00'} USDT`} + value={`${account?.total_pnl !== undefined && account.total_pnl >= 0 ? '+' : ''}${account?.total_pnl?.toFixed(2) || '0.00'} USDT`} change={account?.total_pnl_pct || 0} - positive={account ? (account.total_pnl || 0) >= 0 : false} + positive={(account?.total_pnl ?? 0) >= 0} />
@@ -511,7 +544,7 @@ function StatCard({ // Decision Card Component with CoT Trace - Binance Style function DecisionCard({ decision, language }: { decision: DecisionRecord; language: Language }) { - const [showInput, setShowInput] = useState(false); + const [showInputPrompt, setShowInputPrompt] = useState(false); const [showCoT, setShowCoT] = useState(false); return ( @@ -535,20 +568,20 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua
- {/* AI Input Prompt - Collapsible */} - {(decision as any).input_prompt && ( + {/* Input Prompt - Collapsible */} + {decision.input_prompt && (
- {showInput && ( + {showInputPrompt && (
- {(decision as any).input_prompt} + {decision.input_prompt}
)}
diff --git a/web/src/components/AILearning.tsx b/web/src/components/AILearning.tsx index e3806b36..20da61d2 100644 --- a/web/src/components/AILearning.tsx +++ b/web/src/components/AILearning.tsx @@ -1,12 +1,17 @@ import useSWR from 'swr'; import { useLanguage } from '../contexts/LanguageContext'; import { t } from '../i18n/translations'; +import { api } from '../lib/api'; interface TradeOutcome { symbol: string; side: string; + quantity: number; + leverage: number; open_price: number; close_price: number; + position_value: number; + margin_used: number; pn_l: number; pn_l_pct: number; duration: string; @@ -44,14 +49,16 @@ interface AILearningProps { traderId: string; } -const fetcher = (url: string) => fetch(url).then(res => res.json()); - export default function AILearning({ traderId }: AILearningProps) { const { language } = useLanguage(); const { data: performance, error } = useSWR( - `http://localhost:8080/api/performance?trader_id=${traderId}`, - fetcher, - { refreshInterval: 10000 } + traderId ? `performance-${traderId}` : 'performance', + () => api.getPerformance(traderId), + { + refreshInterval: 30000, // 30秒刷新(AI学习分析数据更新频率较低) + revalidateOnFocus: false, + dedupingInterval: 20000, + } ); if (error) { @@ -555,6 +562,34 @@ export default function AILearning({ traderId }: AILearningProps) {
+ {/* Position Details */} +
+
+
Quantity
+
+ {trade.quantity ? trade.quantity.toFixed(4) : '-'} +
+
+
+
Leverage
+
+ {trade.leverage ? `${trade.leverage}x` : '-'} +
+
+
+
Position Value
+
+ {trade.position_value ? `$${trade.position_value.toFixed(2)}` : '-'} +
+
+
+
Margin Used
+
+ {trade.margin_used ? `$${trade.margin_used.toFixed(2)}` : '-'} +
+
+
+
diff --git a/web/src/components/EquityChart.tsx b/web/src/components/EquityChart.tsx index 505069b9..b8b846cf 100644 --- a/web/src/components/EquityChart.tsx +++ b/web/src/components/EquityChart.tsx @@ -34,7 +34,9 @@ export function EquityChart({ traderId }: EquityChartProps) { traderId ? `equity-history-${traderId}` : 'equity-history', () => api.getEquityHistory(traderId), { - refreshInterval: 10000, // 每10秒刷新 + refreshInterval: 30000, // 30秒刷新(历史数据更新频率较低) + revalidateOnFocus: false, + dedupingInterval: 20000, } ); @@ -42,7 +44,9 @@ export function EquityChart({ traderId }: EquityChartProps) { traderId ? `account-${traderId}` : 'account', () => api.getAccount(traderId), { - refreshInterval: 5000, + refreshInterval: 15000, // 15秒刷新(配合后端缓存) + revalidateOnFocus: false, + dedupingInterval: 10000, } ); @@ -60,7 +64,10 @@ export function EquityChart({ traderId }: EquityChartProps) { ); } - if (!history || history.length === 0) { + // 过滤掉无效数据:total_equity为0或小于1的数据点(API失败导致) + const validHistory = history?.filter(point => point.total_equity > 1) || []; + + if (!validHistory || validHistory.length === 0) { return (

{t('accountEquityCurve', language)}

@@ -76,12 +83,14 @@ export function EquityChart({ traderId }: EquityChartProps) { // 限制显示最近的数据点(性能优化) // 如果数据超过2000个点,只显示最近2000个 const MAX_DISPLAY_POINTS = 2000; - const displayHistory = history.length > MAX_DISPLAY_POINTS - ? history.slice(-MAX_DISPLAY_POINTS) - : history; + const displayHistory = validHistory.length > MAX_DISPLAY_POINTS + ? validHistory.slice(-MAX_DISPLAY_POINTS) + : validHistory; - // 计算初始余额(使用第一个数据点) - const initialBalance = history[0]?.total_equity || 1000; + // 计算初始余额(使用第一个有效数据点,如果无数据则从account获取,最后才用默认值) + const initialBalance = validHistory[0]?.total_equity + || account?.total_equity + || 100; // 默认值改为100,与常见配置一致 // 转换数据格式 const chartData = displayHistory.map((point) => { @@ -152,19 +161,19 @@ export function EquityChart({ traderId }: EquityChartProps) { }; return ( -
+
{/* Header */} -
-
-

{t('accountEquityCurve', language)}

-
- +
+
+

{t('accountEquityCurve', language)}

+
+ {account?.total_equity.toFixed(2) || '0.00'} - USDT + USDT -
+
- + ({isProfit ? '+' : ''}{currentValue.raw_pnl.toFixed(2)} USDT)
@@ -182,10 +191,10 @@ export function EquityChart({ traderId }: EquityChartProps) {
{/* Display Mode Toggle */} -
+
{/* Footer Stats */} -
+
{t('initialBalance', language)}
-
+
{initialBalance.toFixed(2)} USDT
{t('currentEquity', language)}
-
+
{currentValue.raw_equity.toFixed(2)} USDT
{t('historicalCycles', language)}
-
{history.length} {t('cycles', language)}
+
{validHistory.length} {t('cycles', language)}
{t('displayRange', language)}
-
- {history.length > MAX_DISPLAY_POINTS +
+ {validHistory.length > MAX_DISPLAY_POINTS ? `${t('recent', language)} ${MAX_DISPLAY_POINTS}` : t('allData', language) } diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index d457755d..8c979f59 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -161,4 +161,14 @@ export const api = { if (!res.ok) throw new Error('获取历史数据失败'); return res.json(); }, + + // 获取AI学习表现分析(支持trader_id) + async getPerformance(traderId?: string): Promise { + const url = traderId + ? `${API_BASE}/performance?trader_id=${traderId}` + : `${API_BASE}/performance`; + const res = await fetch(url); + if (!res.ok) throw new Error('获取AI学习数据失败'); + return res.json(); + }, }; diff --git a/web/src/types.ts b/web/src/types.ts index 90d1e94f..5c9226b4 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -64,6 +64,7 @@ export interface AccountSnapshot { export interface DecisionRecord { timestamp: string; cycle_number: number; + input_prompt: string; cot_trace: string; decision_json: string; account_state: AccountSnapshot; diff --git a/web/src/types/index.ts b/web/src/types/index.ts index 94e59e9f..6e648e2f 100644 --- a/web/src/types/index.ts +++ b/web/src/types/index.ts @@ -56,6 +56,7 @@ export interface DecisionAction { export interface DecisionRecord { timestamp: string; cycle_number: number; + input_prompt: string; cot_trace: string; decision_json: string; account_state: { diff --git a/web/vite.config.ts b/web/vite.config.ts index 576ccf6c..74120a78 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -4,6 +4,7 @@ import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], server: { + host: '0.0.0.0', port: 3000, proxy: { '/api': { diff --git a/常见问题.md b/常见问题.md new file mode 100644 index 00000000..5d823205 --- /dev/null +++ b/常见问题.md @@ -0,0 +1,25 @@ +# 常见问题 + +## 币安持仓模式错误 (code=-4061) + +**错误信息**:`Order's position side does not match user's setting` + +**原因**:系统需要使用双向持仓模式,但您的币安账户设置为单向持仓。 + +### 解决方法 + +1. 登录 [币安合约交易平台](https://www.binance.com/zh-CN/futures/BTCUSDT) + +2. 点击右上角的 **⚙️ 偏好设置** + +3. 选择 **持仓模式** + +4. 切换为 **双向持仓** (Hedge Mode) + +5. 确认切换 + +**注意**:切换前必须先平掉所有持仓。 + +--- + +更多问题请查看 [GitHub Issues](https://github.com/tinkle-community/nofx/issues)