Merge remote tracking branch into local development

- Resolved conflicts in README.md: Combined web-based config updates with multi-exchange support
- Resolved conflicts in main.go: Fixed database initialization and default coin settings
- Resolved conflicts in manager/trader_manager.go: Updated trader management for new database structure
- Resolved conflicts in web/src/App.tsx: Combined UI improvements with responsive design
- Resolved conflicts in web/.dockerignore: Merged dependency exclusions
- Removed deprecated files: Dockerfile, config/config.go, web/Dockerfile, ComparisonChart.tsx, CompetitionPage.tsx
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: tinkle-community <tinklefund@gmail.com>
This commit is contained in:
icy
2025-10-30 20:57:57 +08:00
42 changed files with 3768 additions and 593 deletions
+3 -2
View File
@@ -40,8 +40,9 @@ coin_pool_cache/
# Config files (should be mounted) # Config files (should be mounted)
config.json config.json
# Web directory (has its own Dockerfile) # Web build artifacts (but include source for multi-stage build)
web/ web/node_modules/
web/dist/
# Temporary files # Temporary files
tmp/ tmp/
+13
View File
@@ -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
+1
View File
@@ -21,6 +21,7 @@ Thumbs.db
*.log *.log
*.tmp *.tmp
*.bak *.bak
*.backup
# 环境变量 # 环境变量
.env .env
+205
View File
@@ -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"
}
```
+55 -44
View File
@@ -15,22 +15,33 @@ Before you begin, ensure your system has:
Download and install [Docker Desktop](https://www.docker.com/products/docker-desktop/) Download and install [Docker Desktop](https://www.docker.com/products/docker-desktop/)
#### Linux (Ubuntu/Debian) #### 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 ```bash
# Install Docker # Install Docker (includes compose)
curl -fsSL https://get.docker.com -o get-docker.sh curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh sudo sh get-docker.sh
# Install Docker Compose # Add user to docker group
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
sudo usermod -aG docker $USER sudo usermod -aG docker $USER
newgrp docker newgrp docker
# Verify installation # Verify installation (new command)
docker --version docker --version
docker-compose --version docker compose --version # Docker 24+ includes this, no separate installation needed
``` ```
## 🚀 Quick Start (3 Steps) ## 🚀 Quick Start (3 Steps)
@@ -69,10 +80,10 @@ nano config.json # or use any other editor
```bash ```bash
# Build and start all services (first run) # Build and start all services (first run)
docker-compose up -d --build docker compose up -d --build
# Subsequent starts (without rebuilding) # Subsequent starts (without rebuilding)
docker-compose up -d docker compose up -d
``` ```
**Startup options:** **Startup options:**
@@ -91,49 +102,49 @@ Once deployed, open your browser and visit:
### View Running Status ### View Running Status
```bash ```bash
# View all container status # View all container status
docker-compose ps docker compose ps
# View service health status # View service health status
docker-compose ps --format json | jq docker compose ps --format json | jq
``` ```
### View Logs ### View Logs
```bash ```bash
# View all service logs # View all service logs
docker-compose logs -f docker compose logs -f
# View backend logs only # View backend logs only
docker-compose logs -f backend docker compose logs -f backend
# View frontend logs only # View frontend logs only
docker-compose logs -f frontend docker compose logs -f frontend
# View last 100 lines # View last 100 lines
docker-compose logs --tail=100 docker compose logs --tail=100
``` ```
### Stop Services ### Stop Services
```bash ```bash
# Stop all services (keep data) # Stop all services (keep data)
docker-compose stop docker compose stop
# Stop and remove containers (keep data) # Stop and remove containers (keep data)
docker-compose down docker compose down
# Stop and remove containers and volumes (clear all data) # Stop and remove containers and volumes (clear all data)
docker-compose down -v docker compose down -v
``` ```
### Restart Services ### Restart Services
```bash ```bash
# Restart all services # Restart all services
docker-compose restart docker compose restart
# Restart backend only # Restart backend only
docker-compose restart backend docker compose restart backend
# Restart frontend only # Restart frontend only
docker-compose restart frontend docker compose restart frontend
``` ```
### Update Services ### Update Services
@@ -142,7 +153,7 @@ docker-compose restart frontend
git pull git pull
# Rebuild and restart # Rebuild and restart
docker-compose up -d --build docker compose up -d --build
``` ```
## 🔧 Advanced Configuration ## 🔧 Advanced Configuration
@@ -226,14 +237,14 @@ tar -xzf backup_20241029.tar.gz
```bash ```bash
# View detailed error messages # View detailed error messages
docker-compose logs backend docker compose logs backend
docker-compose logs frontend docker compose logs frontend
# Check container status # Check container status
docker-compose ps -a docker compose ps -a
# Rebuild (clear cache) # Rebuild (clear cache)
docker-compose build --no-cache docker compose build --no-cache
``` ```
### Port Already in Use ### Port Already in Use
@@ -273,10 +284,10 @@ curl http://localhost:3000/health
```bash ```bash
# Check network connectivity # Check network connectivity
docker-compose exec frontend ping backend docker compose exec frontend ping backend
# Check if backend service is running # 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 ### Clean Docker Resources
@@ -321,8 +332,8 @@ docker system prune -a --volumes
4. **Regularly update images** 4. **Regularly update images**
```bash ```bash
docker-compose pull docker compose pull
docker-compose up -d docker compose up -d
``` ```
## 🌐 Production Deployment ## 🌐 Production Deployment
@@ -391,7 +402,7 @@ logging:
max-file: "3" max-file: "3"
# View log statistics # View log statistics
docker-compose logs --timestamps | wc -l docker compose logs --timestamps | wc -l
``` ```
### Monitoring Tool Integration ### Monitoring Tool Integration
@@ -424,28 +435,28 @@ services:
```bash ```bash
# Start # Start
docker-compose up -d --build # Build and start docker compose up -d --build # Build and start
docker-compose up -d # Start (without rebuilding) docker compose up -d # Start (without rebuilding)
# Stop # Stop
docker-compose stop # Stop services docker compose stop # Stop services
docker-compose down # Stop and remove containers docker compose down # Stop and remove containers
docker-compose down -v # Stop and remove containers and data docker compose down -v # Stop and remove containers and data
# View # View
docker-compose ps # View status docker compose ps # View status
docker-compose logs -f # View logs docker compose logs -f # View logs
docker-compose top # View processes docker compose top # View processes
# Restart # Restart
docker-compose restart # Restart all services docker compose restart # Restart all services
docker-compose restart backend # Restart backend docker compose restart backend # Restart backend
# Update # Update
git pull && docker-compose up -d --build git pull && docker compose up -d --build
# Clean # Clean
docker-compose down -v # Clear all data docker compose down -v # Clear all data
docker system prune -a # Clean Docker resources docker system prune -a # Clean Docker resources
``` ```
+60 -44
View File
@@ -11,26 +11,42 @@
### 安装 Docker ### 安装 Docker
> #### 提示:Docker Compose 版本说明
>
> **新用户建议**
> - **推荐使用 Docker Desktop**:自动包含最新 Docker Compose,无需单独安装
> - 安装简单,一键搞定,提供图形界面管理
> - 支持 macOS、Windows、部分 Linux 发行版
>
> **旧用户提醒**
> - **弃用独立 docker-compose**:不再推荐下载独立的 Docker Compose 二进制文件
> - **使用内置版**Docker 20.10+ 自带 `docker compose` 命令(注意是空格)
> - 如果还在使用旧的 `docker-compose`,请升级到新语法
#### macOS / Windows #### macOS / Windows
下载并安装 [Docker Desktop](https://www.docker.com/products/docker-desktop/) 下载并安装 [Docker Desktop](https://www.docker.com/products/docker-desktop/)
#### Linux (Ubuntu/Debian) **安装后验证:**
```bash ```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 curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh 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 组 # 将当前用户加入 docker 组
sudo usermod -aG docker $USER sudo usermod -aG docker $USER
newgrp docker newgrp docker
# 验证安装 # 验证安装(新命令)
docker --version docker --version
docker-compose --version docker compose --version # Docker 24+ 自带,无需单独安装
``` ```
## 🚀 快速开始(3步完成部署) ## 🚀 快速开始(3步完成部署)
@@ -69,10 +85,10 @@ nano config.json # 或使用其他编辑器
```bash ```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 ```bash
# 查看所有容器状态 # 查看所有容器状态
docker-compose ps docker compose ps
# 查看服务健康状态 # 查看服务健康状态
docker-compose ps --format json | jq docker compose ps --format json | jq
``` ```
### 查看日志 ### 查看日志
```bash ```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 行日志 # 查看最近 100 行日志
docker-compose logs --tail=100 docker compose logs --tail=100
``` ```
### 停止服务 ### 停止服务
```bash ```bash
# 停止所有服务(保留数据) # 停止所有服务(保留数据)
docker-compose stop docker compose stop
# 停止并删除容器(保留数据) # 停止并删除容器(保留数据)
docker-compose down docker compose down
# 停止并删除容器和卷(清除所有数据) # 停止并删除容器和卷(清除所有数据)
docker-compose down -v docker compose down -v
``` ```
### 重启服务 ### 重启服务
```bash ```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 git pull
# 重新构建并重启 # 重新构建并重启
docker-compose up -d --build docker compose up -d --build
``` ```
## 🔧 高级配置 ## 🔧 高级配置
@@ -226,14 +242,14 @@ tar -xzf backup_20241029.tar.gz
```bash ```bash
# 查看详细错误信息 # 查看详细错误信息
docker-compose logs backend docker compose logs backend
docker-compose logs frontend 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 ```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 资源 ### 清理 Docker 资源
@@ -321,8 +337,8 @@ docker system prune -a --volumes
4. **定期更新镜像** 4. **定期更新镜像**
```bash ```bash
docker-compose pull docker compose pull
docker-compose up -d docker compose up -d
``` ```
## 🌐 生产环境部署 ## 🌐 生产环境部署
@@ -391,7 +407,7 @@ logging:
max-file: "3" max-file: "3"
# 查看日志统计 # 查看日志统计
docker-compose logs --timestamps | wc -l docker compose logs --timestamps | wc -l
``` ```
### 监控工具集成 ### 监控工具集成
@@ -424,28 +440,28 @@ services:
```bash ```bash
# 启动 # 启动
docker-compose up -d --build # 构建并启动 docker compose up -d --build # 构建并启动
docker-compose up -d # 启动(不重新构建) docker compose up -d # 启动(不重新构建)
# 停止 # 停止
docker-compose stop # 停止服务 docker compose stop # 停止服务
docker-compose down # 停止并删除容器 docker compose down # 停止并删除容器
docker-compose down -v # 停止并删除容器和数据 docker compose down -v # 停止并删除容器和数据
# 查看 # 查看
docker-compose ps # 查看状态 docker compose ps # 查看状态
docker-compose logs -f # 查看日志 docker compose logs -f # 查看日志
docker-compose top # 查看进程 docker compose top # 查看进程
# 重启 # 重启
docker-compose restart # 重启所有服务 docker compose restart # 重启所有服务
docker-compose restart backend # 重启后端 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 资源 docker system prune -a # 清理 Docker 资源
``` ```
-59
View File
@@ -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-certificatesHTTPS 请求需要)
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"]
+300
View File
@@ -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
+197 -5
View File
@@ -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! > ⚠️ **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! ### 🚀 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:** **Major Changes:**
-**Web-Based Configuration**: Create and manage AI traders through a modern web interface -**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! 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 ## ✨ Core Features
@@ -81,7 +125,11 @@ See [Quick Start](#-quick-start) for the new setup process!
- **Per-Coin Position Limit**: - **Per-Coin Position Limit**:
- Altcoins ≤ 1.5x account equity - Altcoins ≤ 1.5x account equity
- BTC/ETH ≤ 10x 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 - **Margin Management**: Total usage ≤90%, AI autonomous decision on usage rate
- **Risk-Reward Ratio**: Mandatory ≥1:2 (stop-loss:take-profit) - **Risk-Reward Ratio**: Mandatory ≥1:2 (stop-loss:take-profit)
- **Prevent Position Stacking**: No duplicate opening of same coin/direction - **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 chmod +x start.sh
./start.sh start --build ./start.sh start --build
# Option 2: Use docker-compose directly > #### Docker Compose Version Notes
docker-compose up -d --build >
> **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 #### 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 #### ⚔️ Expert Mode: Multi-Trader Competition
For running multiple AI traders competing against each other: 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 | | `qwen_key` | Qwen API key | `"sk-xxx"` | If using Qwen |
| `initial_balance` | Starting balance for P/L calculation | `1000.0` | ✅ Yes | | `initial_balance` | Starting balance for P/L calculation | `1000.0` | ✅ Yes |
| `scan_interval_minutes` | How often to make decisions | `3` (3-5 recommended) | ✅ 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<br>⚠️ Subaccounts: ≤5x | `5` (default, safe)<br>`50` (main account max) | ✅ Yes |
| `altcoin_leverage` | Maximum leverage for altcoins<br>⚠️ Subaccounts: ≤5x | `5` (default, safe)<br>`20` (main account max) | ✅ Yes |
| `use_default_coins` | Use built-in coin list<br>**✨ Smart Default: `true`** (v2.0.2+)<br>Auto-enabled if no API URL provided | `true` or omit | ❌ No<br>(Optional, auto-defaults) | | `use_default_coins` | Use built-in coin list<br>**✨ Smart Default: `true`** (v2.0.2+)<br>Auto-enabled if no API URL provided | `true` or omit | ❌ No<br>(Optional, auto-defaults) |
| `coin_pool_api_url` | Custom coin pool API<br>*Only needed when `use_default_coins: false`* | `""` (empty) | ❌ No | | `coin_pool_api_url` | Custom coin pool API<br>*Only needed when `use_default_coins: false`* | `""` (empty) | ❌ No |
| `oi_top_api_url` | Open interest API<br>*Optional supplement data* | `""` (empty) | ❌ No | | `oi_top_api_url` | Open interest API<br>*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 #### ⚠️ Important: `use_default_coins` Field
**Smart Default Behavior (v2.0.2+):** **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. **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) ### v2.0.1 (2025-10-29)
**Bug Fixes:** **Bug Fixes:**
+248 -4
View File
@@ -9,7 +9,7 @@
--- ---
Автоматизированная система торговли фьючерсами Binance на базе **DeepSeek/Qwen AI**, поддерживающая **конкуренцию нескольких AI-моделей в реальной торговле**, с полным анализом рынка, принятием решений AI, **механизмом самообучения** и профессиональным веб-интерфейсом мониторинга. Автоматизированная система торговли криптовалютными фьючерсами на базе **DeepSeek/Qwen AI**, поддерживающая **Binance, Hyperliquid и Aster DEX биржи**, **конкуренцию нескольких AI-моделей в реальной торговле**, с полным анализом рынка, принятием решений 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 ### 🏆 Режим конкуренции нескольких AI
@@ -50,7 +119,11 @@
- **Лимит позиции по монете**: - **Лимит позиции по монете**:
- Альткоины ≤ 1.5x капитал счета - Альткоины ≤ 1.5x капитал счета
- BTC/ETH ≤ 10x капитал счета - BTC/ETH ≤ 10x капитал счета
- **Фиксированное плечо**: Альткоины 20x | BTC/ETH 50x - **Настраиваемое плечо** (v2.0.3+):
- Установите максимальное плечо в config.json
- По умолчанию: 5x для всех монет (безопасно для субаккаунтов)
- Основные аккаунты могут увеличить: Альткоины до 20x, BTC/ETH до 50x
- ⚠️ Субаккаунты Binance ограничены ≤5x плечом
- **Управление маржой**: Общее использование ≤90%, AI принимает автономные решения - **Управление маржой**: Общее использование ≤90%, AI принимает автономные решения
- **Соотношение риск/доход**: Обязательное ≥1:2 (стоп-лосс:тейк-профит) - **Соотношение риск/доход**: Обязательное ≥1:2 (стоп-лосс:тейк-профит)
- **Предотвращение накопления позиций**: Запрет дублирования открытия той же монеты/направления - **Предотвращение накопления позиций**: Запрет дублирования открытия той же монеты/направления
@@ -123,8 +196,10 @@ nano config.json # или используйте любой редактор
chmod +x start.sh chmod +x start.sh
./start.sh start --build ./start.sh start --build
# Вариант 2: Используйте docker-compose напрямую # Вариант 2: Используйте docker compose напрямую
docker-compose up -d --build # Этот проект использует синтаксис Docker Compose V2 (с пробелами)
# Если у вас установлена старая версия `docker-compose`, обновитесь до Docker Desktop или Docker 20.10+
docker compose up -d --build
``` ```
#### Шаг 3: Доступ к панели #### Шаг 3: Доступ к панели
@@ -268,6 +343,10 @@ cp config.json.example config.json
"scan_interval_minutes": 3 "scan_interval_minutes": 3
} }
], ],
"leverage": {
"btc_eth_leverage": 5,
"altcoin_leverage": 5
},
"use_default_coins": true, "use_default_coins": true,
"coin_pool_api_url": "", "coin_pool_api_url": "",
"oi_top_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 трейдеров, конкурирующих друг с другом: Для запуска нескольких AI трейдеров, конкурирующих друг с другом:
@@ -360,6 +544,9 @@ cp config.json.example config.json
| `qwen_key` | Qwen API ключ | `"sk-xxx"` | Требуется при использовании Qwen | | `qwen_key` | Qwen API ключ | `"sk-xxx"` | Требуется при использовании Qwen |
| `initial_balance` | Начальный баланс для расчета P/L | `1000.0` | ✅ Да | | `initial_balance` | Начальный баланс для расчета P/L | `1000.0` | ✅ Да |
| `scan_interval_minutes` | Частота решений (минуты) | `3` (рекомендуется 3-5) | ✅ Да | | `scan_interval_minutes` | Частота решений (минуты) | `3` (рекомендуется 3-5) | ✅ Да |
| **`leverage`** | **Конфигурация плеча (v2.0.3+)** | См. ниже | ✅ Да |
| `btc_eth_leverage` | Максимальное плечо для BTC/ETH<br>⚠️ Субаккаунты: ≤5x | `5` (по умолчанию, безопасно)<br>`50` (максимум для основного аккаунта) | ✅ Да |
| `altcoin_leverage` | Максимальное плечо для альткоинов<br>⚠️ Субаккаунты: ≤5x | `5` (по умолчанию, безопасно)<br>`20` (максимум для основного аккаунта) | ✅ Да |
| `use_default_coins` | Использовать встроенный список монет<br>**✨ Умное значение по умолчанию: `true`** (v2.0.2+)<br>Автоматически включается без API | `true` или опустить | ❌ Нет<br>(Опционально, авто) | | `use_default_coins` | Использовать встроенный список монет<br>**✨ Умное значение по умолчанию: `true`** (v2.0.2+)<br>Автоматически включается без API | `true` или опустить | ❌ Нет<br>(Опционально, авто) |
| `coin_pool_api_url` | API пользовательского пула монет<br>*Требуется только при `use_default_coins: false`* | `""` (пусто) | ❌ Нет | | `coin_pool_api_url` | API пользовательского пула монет<br>*Требуется только при `use_default_coins: false`* | `""` (пусто) | ❌ Нет |
| `oi_top_api_url` | API открытого интереса<br>*Опциональные дополнительные данные* | `""` (пусто) | ❌ Нет | | `oi_top_api_url` | API открытого интереса<br>*Опциональные дополнительные данные* | `""` (пусто) | ❌ Нет |
@@ -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` #### ⚠️ Важно: Поле `use_default_coins`
**Умное поведение по умолчанию (v2.0.2+):** **Умное поведение по умолчанию (v2.0.2+):**
+248 -4
View File
@@ -9,7 +9,7 @@
--- ---
Автоматизована система торгівлі ф'ючерсами Binance на базі **DeepSeek/Qwen AI**, що підтримує **змагання кількох AI-моделей у реальній торгівлі**, з повним аналізом ринку, прийняттям рішень AI, **механізмом самонавчання** та професійним веб-інтерфейсом моніторингу. Автоматизована система торгівлі криптовалютними ф'ючерсами на базі **DeepSeek/Qwen AI**, що підтримує **Binance, Hyperliquid та Aster DEX біржі**, **змагання кількох AI-моделей у реальній торгівлі**, з повним аналізом ринку, прийняттям рішень 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 ### 🏆 Режим змагання кількох AI
@@ -50,7 +119,11 @@
- **Ліміт позиції по монеті**: - **Ліміт позиції по монеті**:
- Альткоїни ≤ 1.5x капітал рахунку - Альткоїни ≤ 1.5x капітал рахунку
- BTC/ETH ≤ 10x капітал рахунку - BTC/ETH ≤ 10x капітал рахунку
- **Фіксоване плече**: Альткоїни 20x | BTC/ETH 50x - **Налаштовуване плече** (v2.0.3+):
- Встановіть максимальне плече в config.json
- За замовчуванням: 5x для всіх монет (безпечно для субакаунтів)
- Основні акаунти можуть збільшити: Альткоїни до 20x, BTC/ETH до 50x
- ⚠️ Субакаунти Binance обмежені ≤5x плечем
- **Управління маржею**: Загальне використання ≤90%, AI приймає автономні рішення - **Управління маржею**: Загальне використання ≤90%, AI приймає автономні рішення
- **Співвідношення ризик/дохід**: Обов'язкове ≥1:2 (стоп-лосс:тейк-профіт) - **Співвідношення ризик/дохід**: Обов'язкове ≥1:2 (стоп-лосс:тейк-профіт)
- **Запобігання накопиченню позицій**: Заборона дублювання відкриття тієї ж монети/напрямку - **Запобігання накопиченню позицій**: Заборона дублювання відкриття тієї ж монети/напрямку
@@ -123,8 +196,10 @@ nano config.json # або використайте будь-який редак
chmod +x start.sh chmod +x start.sh
./start.sh start --build ./start.sh start --build
# Варіант 2: Використайте docker-compose безпосередньо # Варіант 2: Використайте docker compose безпосередньо
docker-compose up -d --build # Цей проект використовує синтаксис Docker Compose V2 (з пробілами)
# Якщо у вас встановлена стара версія `docker-compose`, оновіть до Docker Desktop або Docker 20.10+
docker compose up -d --build
``` ```
#### Крок 3: Доступ до панелі #### Крок 3: Доступ до панелі
@@ -268,6 +343,10 @@ cp config.json.example config.json
"scan_interval_minutes": 3 "scan_interval_minutes": 3
} }
], ],
"leverage": {
"btc_eth_leverage": 5,
"altcoin_leverage": 5
},
"use_default_coins": true, "use_default_coins": true,
"coin_pool_api_url": "", "coin_pool_api_url": "",
"oi_top_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 трейдерів, що змагаються один з одним: Для запуску кількох AI трейдерів, що змагаються один з одним:
@@ -360,6 +544,9 @@ cp config.json.example config.json
| `qwen_key` | Qwen API ключ | `"sk-xxx"` | Потрібно при використанні Qwen | | `qwen_key` | Qwen API ключ | `"sk-xxx"` | Потрібно при використанні Qwen |
| `initial_balance` | Початковий баланс для розрахунку P/L | `1000.0` | ✅ Так | | `initial_balance` | Початковий баланс для розрахунку P/L | `1000.0` | ✅ Так |
| `scan_interval_minutes` | Частота рішень (хвилини) | `3` (рекомендується 3-5) | ✅ Так | | `scan_interval_minutes` | Частота рішень (хвилини) | `3` (рекомендується 3-5) | ✅ Так |
| **`leverage`** | **Конфігурація плеча (v2.0.3+)** | Див. нижче | ✅ Так |
| `btc_eth_leverage` | Максимальне плече для BTC/ETH<br>⚠️ Субакаунти: ≤5x | `5` (за замовчуванням, безпечно)<br>`50` (максимум для основного акаунта) | ✅ Так |
| `altcoin_leverage` | Максимальне плече для альткоїнів<br>⚠️ Субакаунти: ≤5x | `5` (за замовчуванням, безпечно)<br>`20` (максимум для основного акаунта) | ✅ Так |
| `use_default_coins` | Використовувати вбудований список монет<br>**✨ Розумне значення за замовчуванням: `true`** (v2.0.2+)<br>Автоматично включається без API | `true` або опустити | ❌ Ні<br>(Опціонально, авто) | | `use_default_coins` | Використовувати вбудований список монет<br>**✨ Розумне значення за замовчуванням: `true`** (v2.0.2+)<br>Автоматично включається без API | `true` або опустити | ❌ Ні<br>(Опціонально, авто) |
| `coin_pool_api_url` | API користувацького пулу монет<br>*Потрібно лише при `use_default_coins: false`* | `""` (пусто) | ❌ Ні | | `coin_pool_api_url` | API користувацького пулу монет<br>*Потрібно лише при `use_default_coins: false`* | `""` (пусто) | ❌ Ні |
| `oi_top_api_url` | API відкритого інтересу<br>*Опціональні додаткові дані* | `""` (пусто) | ❌ Ні | | `oi_top_api_url` | API відкритого інтересу<br>*Опціональні додаткові дані* | `""` (пусто) | ❌ Ні |
@@ -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` #### ⚠️ Важливо: Поле `use_default_coins`
**Розумна поведінка за замовчуванням (v2.0.2+):** **Розумна поведінка за замовчуванням (v2.0.2+):**
+248 -4
View File
@@ -9,7 +9,7 @@
--- ---
一个基于 **DeepSeek/Qwen AI**币安合约自动交易系统,支持**多AI模型实盘竞赛**,具备完整的市场分析、AI决策、**自我学习机制**和专业的Web监控界面。 一个基于 **DeepSeek/Qwen AI**加密货币期货自动交易系统,支持 **Binance、Hyperliquid和Aster DEX交易所****多AI模型实盘竞赛**,具备完整的市场分析、AI决策、**自我学习机制**和专业的Web监控界面。
> ⚠️ **风险提示**:本系统为实验性项目,AI自动交易存在重大风险,强烈建议仅用于学习研究或小额资金测试! > ⚠️ **风险提示**:本系统为实验性项目,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竞赛模式 ### 🏆 多AI竞赛模式
@@ -50,7 +119,11 @@
- **单币种仓位上限**: - **单币种仓位上限**:
- 山寨币 ≤ 1.5倍账户净值 - 山寨币 ≤ 1.5倍账户净值
- BTC/ETH ≤ 10倍账户净值 - BTC/ETH ≤ 10倍账户净值
- **固定杠杆**: 山寨币20倍 | BTC/ETH 50倍 - **可配置杠杆** (v2.0.3+):
- 在config.json中设置最大杠杆
- 默认:所有币种5倍(子账户安全)
- 主账户可增加:山寨币最高20倍,BTC/ETH最高50倍
- ⚠️ 币安子账户限制≤5倍杠杆
- **保证金管理**: 总使用率≤90%,AI自主决策使用率 - **保证金管理**: 总使用率≤90%,AI自主决策使用率
- **风险回报比**: 强制≥1:2(止损:止盈) - **风险回报比**: 强制≥1:2(止损:止盈)
- **防止仓位叠加**: 同币种同方向不允许重复开仓 - **防止仓位叠加**: 同币种同方向不允许重复开仓
@@ -187,8 +260,10 @@ nano config.json # 或使用其他编辑器
chmod +x start.sh chmod +x start.sh
./start.sh start --build ./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:访问控制台 #### 步骤3:访问控制台
@@ -331,6 +406,10 @@ cp config.json.example config.json
"scan_interval_minutes": 3 "scan_interval_minutes": 3
} }
], ],
"leverage": {
"btc_eth_leverage": 5,
"altcoin_leverage": 5
},
"use_default_coins": true, "use_default_coins": true,
"coin_pool_api_url": "", "coin_pool_api_url": "",
"oi_top_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竞赛 #### ⚔️ 专家模式:多Trader竞赛
用于运行多个AI trader相互竞争: 用于运行多个AI trader相互竞争:
@@ -423,6 +607,9 @@ cp config.json.example config.json
| `qwen_key` | Qwen API密钥 | `"sk-xxx"` | 使用Qwen时必填 | | `qwen_key` | Qwen API密钥 | `"sk-xxx"` | 使用Qwen时必填 |
| `initial_balance` | 用于P/L计算的起始余额 | `1000.0` | ✅ 是 | | `initial_balance` | 用于P/L计算的起始余额 | `1000.0` | ✅ 是 |
| `scan_interval_minutes` | 决策频率(分钟) | `3`(建议3-5 | ✅ 是 | | `scan_interval_minutes` | 决策频率(分钟) | `3`(建议3-5 | ✅ 是 |
| **`leverage`** | **杠杆配置 (v2.0.3+)** | 见下文 | ✅ 是 |
| `btc_eth_leverage` | BTC/ETH最大杠杆<br>⚠️ 子账户:≤5倍 | `5`(默认,安全)<br>`50`(主账户最大) | ✅ 是 |
| `altcoin_leverage` | 山寨币最大杠杆<br>⚠️ 子账户:≤5倍 | `5`(默认,安全)<br>`20`(主账户最大) | ✅ 是 |
| `use_default_coins` | 使用内置币种列表<br>**✨ 智能默认:`true`** (v2.0.2+)<br>未提供API时自动启用 | `true` 或省略 | ❌ 否<br>(可选,自动默认) | | `use_default_coins` | 使用内置币种列表<br>**✨ 智能默认:`true`** (v2.0.2+)<br>未提供API时自动启用 | `true` 或省略 | ❌ 否<br>(可选,自动默认) |
| `coin_pool_api_url` | 自定义币种池API<br>*仅当`use_default_coins: false`时需要* | `""`(空) | ❌ 否 | | `coin_pool_api_url` | 自定义币种池API<br>*仅当`use_default_coins: false`时需要* | `""`(空) | ❌ 否 |
| `oi_top_api_url` | 持仓量API<br>*可选补充数据* | `""`(空) | ❌ 否 | | `oi_top_api_url` | 持仓量API<br>*可选补充数据* | `""`(空) | ❌ 否 |
@@ -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` 字段 #### ⚠️ 重要:`use_default_coins` 字段
**智能默认行为(v2.0.2+):** **智能默认行为(v2.0.2+):**
+4 -3
View File
@@ -61,7 +61,7 @@ func corsMiddleware() gin.HandlerFunc {
// setupRoutes 设置路由 // setupRoutes 设置路由
func (s *Server) setupRoutes() { func (s *Server) setupRoutes() {
// 健康检查 // 健康检查
s.router.GET("/health", s.handleHealth) s.router.Any("/health", s.handleHealth)
// API路由组 // API路由组
api := s.router.Group("/api") api := s.router.Group("/api")
@@ -639,8 +639,9 @@ func (s *Server) handlePerformance(c *gin.Context) {
return return
} }
// 分析最近20个周期的交易表现 // 分析最近100个周期的交易表现(避免长期持仓的交易记录丢失)
performance, err := trader.GetDecisionLogger().AnalyzePerformance(20) // 假设每3分钟一个周期,100个周期 = 5小时,足够覆盖大部分交易
performance, err := trader.GetDecisionLogger().AnalyzePerformance(100)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("分析历史表现失败: %v", err), "error": fmt.Sprintf("分析历史表现失败: %v", err),
+47
View File
@@ -6,6 +6,7 @@
"ai_model": "deepseek", "ai_model": "deepseek",
"exchange": "hyperliquid", "exchange": "hyperliquid",
"hyperliquid_private_key": "your_ethereum_private_key_without_0x_prefix", "hyperliquid_private_key": "your_ethereum_private_key_without_0x_prefix",
"hyperliquid_wallet_addr": "your_ethereum_address",
"hyperliquid_testnet": false, "hyperliquid_testnet": false,
"deepseek_key": "your_deepseek_api_key", "deepseek_key": "your_deepseek_api_key",
"initial_balance": 1000, "initial_balance": 1000,
@@ -21,9 +22,55 @@
"qwen_key": "your_qwen_api_key", "qwen_key": "your_qwen_api_key",
"initial_balance": 1000, "initial_balance": 1000,
"scan_interval_minutes": 3 "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, "use_default_coins": true,
"default_coins": [
"BTCUSDT",
"ETHUSDT",
"SOLUSDT",
"BNBUSDT",
"XRPUSDT",
"DOGEUSDT",
"ADAUSDT",
"HYPEUSDT",
],
"coin_pool_api_url": "", "coin_pool_api_url": "",
"oi_top_api_url": "", "oi_top_api_url": "",
"api_server_port": 8080, "api_server_port": 8080,
+30 -28
View File
@@ -55,15 +55,17 @@ type OITopData struct {
// Context 交易上下文(传递给AI的完整信息) // Context 交易上下文(传递给AI的完整信息)
type Context struct { type Context struct {
CurrentTime string `json:"current_time"` CurrentTime string `json:"current_time"`
RuntimeMinutes int `json:"runtime_minutes"` RuntimeMinutes int `json:"runtime_minutes"`
CallCount int `json:"call_count"` CallCount int `json:"call_count"`
Account AccountInfo `json:"account"` Account AccountInfo `json:"account"`
Positions []PositionInfo `json:"positions"` Positions []PositionInfo `json:"positions"`
CandidateCoins []CandidateCoin `json:"candidate_coins"` CandidateCoins []CandidateCoin `json:"candidate_coins"`
MarketDataMap map[string]*market.Data `json:"-"` // 不序列化,但内部使用 MarketDataMap map[string]*market.Data `json:"-"` // 不序列化,但内部使用
OITopDataMap map[string]*OITopData `json:"-"` // OI Top数据映射 OITopDataMap map[string]*OITopData `json:"-"` // OI Top数据映射
Performance interface{} `json:"-"` // 历史表现分析(logger.PerformanceAnalysis Performance interface{} `json:"-"` // 历史表现分析(logger.PerformanceAnalysis
BTCETHLeverage int `json:"-"` // BTC/ETH杠杆倍数(从配置读取)
AltcoinLeverage int `json:"-"` // 山寨币杠杆倍数(从配置读取)
} }
// Decision AI的交易决策 // Decision AI的交易决策
@@ -88,24 +90,24 @@ type FullDecision struct {
} }
// GetFullDecision 获取AI的完整交易决策(批量分析所有币种和持仓) // GetFullDecision 获取AI的完整交易决策(批量分析所有币种和持仓)
func GetFullDecision(ctx *Context) (*FullDecision, error) { func GetFullDecision(ctx *Context, mcpClient *mcp.Client) (*FullDecision, error) {
// 1. 为所有币种获取市场数据 // 1. 为所有币种获取市场数据
if err := fetchMarketDataForContext(ctx); err != nil { if err := fetchMarketDataForContext(ctx); err != nil {
return nil, fmt.Errorf("获取市场数据失败: %w", err) return nil, fmt.Errorf("获取市场数据失败: %w", err)
} }
// 2. 构建 System Prompt(固定规则)和 User Prompt(动态数据) // 2. 构建 System Prompt(固定规则)和 User Prompt(动态数据)
systemPrompt := buildSystemPrompt(ctx.Account.TotalEquity) systemPrompt := buildSystemPrompt(ctx.Account.TotalEquity, ctx.BTCETHLeverage, ctx.AltcoinLeverage)
userPrompt := buildUserPrompt(ctx) userPrompt := buildUserPrompt(ctx)
// 3. 调用AI API(使用 system + user prompt // 3. 调用AI API(使用 system + user prompt
aiResponse, err := mcp.CallWithMessages(systemPrompt, userPrompt) aiResponse, err := mcpClient.CallWithMessages(systemPrompt, userPrompt)
if err != nil { if err != nil {
return nil, fmt.Errorf("调用AI API失败: %w", err) return nil, fmt.Errorf("调用AI API失败: %w", err)
} }
// 4. 解析AI响应 // 4. 解析AI响应
decision, err := parseFullDecisionResponse(aiResponse, ctx.Account.TotalEquity) decision, err := parseFullDecisionResponse(aiResponse, ctx.Account.TotalEquity, ctx.BTCETHLeverage, ctx.AltcoinLeverage)
if err != nil { if err != nil {
return nil, fmt.Errorf("解析AI响应失败: %w", err) return nil, fmt.Errorf("解析AI响应失败: %w", err)
} }
@@ -198,7 +200,7 @@ func calculateMaxCandidates(ctx *Context) int {
} }
// buildSystemPrompt 构建 System Prompt(固定规则,可缓存) // buildSystemPrompt 构建 System Prompt(固定规则,可缓存)
func buildSystemPrompt(accountEquity float64) string { func buildSystemPrompt(accountEquity float64, btcEthLeverage, altcoinLeverage int) string {
var sb strings.Builder var sb strings.Builder
// === 核心使命 === // === 核心使命 ===
@@ -220,8 +222,8 @@ func buildSystemPrompt(accountEquity float64) string {
sb.WriteString("# ⚖️ 硬约束(风险控制)\n\n") sb.WriteString("# ⚖️ 硬约束(风险控制)\n\n")
sb.WriteString("1. **风险回报比**: 必须 ≥ 1:3(冒1%风险,赚3%+收益)\n") sb.WriteString("1. **风险回报比**: 必须 ≥ 1:3(冒1%风险,赚3%+收益)\n")
sb.WriteString("2. **最多持仓**: 3个币种(质量>数量)\n") sb.WriteString("2. **最多持仓**: 3个币种(质量>数量)\n")
sb.WriteString(fmt.Sprintf("3. **单币仓位**: 山寨%.0f-%.0f U(20x杠杆) | BTC/ETH %.0f-%.0f U(50x杠杆)\n", sb.WriteString(fmt.Sprintf("3. **单币仓位**: 山寨%.0f-%.0f U(%dx杠杆) | BTC/ETH %.0f-%.0f U(%dx杠杆)\n",
accountEquity*0.8, accountEquity*1.5, accountEquity*5, accountEquity*10)) accountEquity*0.8, accountEquity*1.5, altcoinLeverage, accountEquity*5, accountEquity*10, btcEthLeverage))
sb.WriteString("4. **保证金**: 总使用率 ≤ 90%\n\n") sb.WriteString("4. **保证金**: 总使用率 ≤ 90%\n\n")
// === 做空激励 === // === 做空激励 ===
@@ -251,7 +253,7 @@ func buildSystemPrompt(accountEquity float64) string {
sb.WriteString("- 💰 **资金序列**:成交量序列、持仓量(OI)序列、资金费率\n") sb.WriteString("- 💰 **资金序列**:成交量序列、持仓量(OI)序列、资金费率\n")
sb.WriteString("- 🎯 **筛选标记**AI500评分 / OI_Top排名(如果有标注)\n\n") sb.WriteString("- 🎯 **筛选标记**AI500评分 / OI_Top排名(如果有标注)\n\n")
sb.WriteString("**分析方法**(完全由你自主决定):\n") sb.WriteString("**分析方法**(完全由你自主决定):\n")
sb.WriteString("- 自由运用序列数据,你可以做趋势分析、形态识别、支撑阻力计算\n") sb.WriteString("- 自由运用序列数据,你可以做但不限于趋势分析、形态识别、支撑阻力、技术阻力位、斐波那契、波动带计算\n")
sb.WriteString("- 多维度交叉验证(价格+量+OI+指标+序列形态)\n") sb.WriteString("- 多维度交叉验证(价格+量+OI+指标+序列形态)\n")
sb.WriteString("- 用你认为最有效的方法发现高确定性机会\n") sb.WriteString("- 用你认为最有效的方法发现高确定性机会\n")
sb.WriteString("- 综合信心度 ≥ 75 才开仓\n\n") sb.WriteString("- 综合信心度 ≥ 75 才开仓\n\n")
@@ -294,7 +296,7 @@ func buildSystemPrompt(accountEquity float64) string {
sb.WriteString("简洁分析你的思考过程\n\n") sb.WriteString("简洁分析你的思考过程\n\n")
sb.WriteString("**第二步: JSON决策数组**\n\n") sb.WriteString("**第二步: JSON决策数组**\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(" {\"symbol\": \"ETHUSDT\", \"action\": \"close_long\", \"reasoning\": \"止盈离场\"}\n")
sb.WriteString("]\n```\n\n") sb.WriteString("]\n```\n\n")
sb.WriteString("**字段说明**:\n") sb.WriteString("**字段说明**:\n")
@@ -415,7 +417,7 @@ func buildUserPrompt(ctx *Context) string {
} }
// parseFullDecisionResponse 解析AI的完整决策响应 // parseFullDecisionResponse 解析AI的完整决策响应
func parseFullDecisionResponse(aiResponse string, accountEquity float64) (*FullDecision, error) { func parseFullDecisionResponse(aiResponse string, accountEquity float64, btcEthLeverage, altcoinLeverage int) (*FullDecision, error) {
// 1. 提取思维链 // 1. 提取思维链
cotTrace := extractCoTTrace(aiResponse) cotTrace := extractCoTTrace(aiResponse)
@@ -429,7 +431,7 @@ func parseFullDecisionResponse(aiResponse string, accountEquity float64) (*FullD
} }
// 3. 验证决策 // 3. 验证决策
if err := validateDecisions(decisions, accountEquity); err != nil { if err := validateDecisions(decisions, accountEquity, btcEthLeverage, altcoinLeverage); err != nil {
return &FullDecision{ return &FullDecision{
CoTTrace: cotTrace, CoTTrace: cotTrace,
Decisions: decisions, Decisions: decisions,
@@ -496,10 +498,10 @@ func fixMissingQuotes(jsonStr string) string {
return jsonStr return jsonStr
} }
// validateDecisions 验证所有决策(需要账户信息) // validateDecisions 验证所有决策(需要账户信息和杠杆配置
func validateDecisions(decisions []Decision, accountEquity float64) error { func validateDecisions(decisions []Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error {
for i, decision := range decisions { 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) return fmt.Errorf("决策 #%d 验证失败: %w", i+1, err)
} }
} }
@@ -529,7 +531,7 @@ func findMatchingBracket(s string, start int) int {
} }
// validateDecision 验证单个决策的有效性 // validateDecision 验证单个决策的有效性
func validateDecision(d *Decision, accountEquity float64) error { func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error {
// 验证action // 验证action
validActions := map[string]bool{ validActions := map[string]bool{
"open_long": true, "open_long": true,
@@ -546,16 +548,16 @@ func validateDecision(d *Decision, accountEquity float64) error {
// 开仓操作必须提供完整参数 // 开仓操作必须提供完整参数
if d.Action == "open_long" || d.Action == "open_short" { if d.Action == "open_long" || d.Action == "open_short" {
// 根据币种判断杠杆上限和仓位价值上限 // 根据币种使用配置的杠杆上限
maxLeverage := 20 // 山寨币固定20倍 maxLeverage := altcoinLeverage // 山寨币使用配置的杠杆
maxPositionValue := accountEquity * 1.5 // 山寨币最多1.5倍账户净值 maxPositionValue := accountEquity * 1.5 // 山寨币最多1.5倍账户净值
if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" { if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" {
maxLeverage = 50 // BTC和ETH固定50倍 maxLeverage = btcEthLeverage // BTC和ETH使用配置的杠杆
maxPositionValue = accountEquity * 10 // BTC/ETH最多10倍账户净值 maxPositionValue = accountEquity * 10 // BTC/ETH最多10倍账户净值
} }
if d.Leverage <= 0 || d.Leverage > maxLeverage { 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 { if d.PositionSizeUSD <= 0 {
return fmt.Errorf("仓位大小必须大于0: %.2f", d.PositionSizeUSD) return fmt.Errorf("仓位大小必须大于0: %.2f", d.PositionSizeUSD)
+17 -37
View File
@@ -1,68 +1,48 @@
services: services:
# 后端服务 # Backend service (API and core logic)
backend: nofx:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: ./docker/Dockerfile.backend
container_name: nofx-backend container_name: nofx-trading
restart: unless-stopped restart: unless-stopped
ports: ports:
- "8080:8080" - "${NOFX_BACKEND_PORT:-8080}:8080"
volumes: volumes:
# 挂载配置文件(必须)
- ./config.json:/app/config.json:ro - ./config.json:/app/config.json:ro
# 持久化决策日志
- ./decision_logs:/app/decision_logs - ./decision_logs:/app/decision_logs
# 持久化币种池缓存 - /etc/localtime:/etc/localtime:ro # Sync host time
- ./coin_pool_cache:/app/coin_pool_cache
environment: environment:
- TZ=Asia/Shanghai - TZ=${NOFX_TIMEZONE:-Asia/Shanghai} # Set timezone
networks: networks:
- nofx-network - nofx-network
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"] test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 10s start_period: 60s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# 前端服务 # Frontend service (static serving and proxy)
frontend: nofx-frontend:
build: build:
context: ./web context: .
dockerfile: Dockerfile dockerfile: ./docker/Dockerfile.frontend
container_name: nofx-frontend container_name: nofx-frontend
restart: unless-stopped restart: unless-stopped
ports: ports:
- "3000:80" - "${NOFX_FRONTEND_PORT:-3000}:80"
depends_on:
backend:
condition: service_healthy
networks: networks:
- nofx-network - nofx-network
environment: depends_on:
- TZ=Asia/Shanghai - nofx
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/health"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 5s start_period: 5s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks: networks:
nofx-network: nofx-network:
driver: bridge driver: bridge
volumes:
decision_logs:
coin_pool_cache:
+68
View File
@@ -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"]
+36
View File
@@ -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;"]
+2
View File
@@ -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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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.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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+87 -31
View File
@@ -269,16 +269,20 @@ type Statistics struct {
// TradeOutcome 单笔交易结果 // TradeOutcome 单笔交易结果
type TradeOutcome struct { type TradeOutcome struct {
Symbol string `json:"symbol"` // 币种 Symbol string `json:"symbol"` // 币种
Side string `json:"side"` // long/short Side string `json:"side"` // long/short
OpenPrice float64 `json:"open_price"` // 开仓价 Quantity float64 `json:"quantity"` // 仓位数量
ClosePrice float64 `json:"close_price"` // 平仓价 Leverage int `json:"leverage"` // 杠杆倍数
PnL float64 `json:"pn_l"` // 盈亏(USDT OpenPrice float64 `json:"open_price"` // 开仓价
PnLPct float64 `json:"pn_l_pct"` // 盈亏百分比 ClosePrice float64 `json:"close_price"` // 平仓价
Duration string `json:"duration"` // 持仓时长 PositionValue float64 `json:"position_value"` // 仓位价值(quantity × openPrice
OpenTime time.Time `json:"open_time"` // 开仓时间 MarginUsed float64 `json:"margin_used"` // 保证金使用(positionValue / leverage
CloseTime time.Time `json:"close_time"` // 平仓时间 PnL float64 `json:"pn_l"` // 盈亏(USDT
WasStopLoss bool `json:"was_stop_loss"` // 是否止损 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 交易表现分析 // PerformanceAnalysis 交易表现分析
@@ -330,7 +334,45 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
// 追踪持仓状态:symbol_side -> {side, openPrice, openTime, quantity, leverage} // 追踪持仓状态:symbol_side -> {side, openPrice, openTime, quantity, leverage}
openPositions := make(map[string]map[string]interface{}) 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 _, record := range records {
for _, action := range record.Decisions { for _, action := range record.Decisions {
if !action.Success { if !action.Success {
@@ -348,7 +390,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
switch action.Action { switch action.Action {
case "open_long", "open_short": case "open_long", "open_short":
// 记录开仓(包括数量和杠杆 // 更新开仓记录(可能已经在预填充时记录过了
openPositions[posKey] = map[string]interface{}{ openPositions[posKey] = map[string]interface{}{
"side": side, "side": side,
"openPrice": action.Price, "openPrice": action.Price,
@@ -358,7 +400,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
} }
case "close_long", "close_short": case "close_long", "close_short":
// 查找对应的开仓记录 // 查找对应的开仓记录(可能来自预填充或当前窗口)
if openPos, exists := openPositions[posKey]; exists { if openPos, exists := openPositions[posKey]; exists {
openPrice := openPos["openPrice"].(float64) openPrice := openPos["openPrice"].(float64)
openTime := openPos["openTime"].(time.Time) openTime := openPos["openTime"].(time.Time)
@@ -366,42 +408,53 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
quantity := openPos["quantity"].(float64) quantity := openPos["quantity"].(float64)
leverage := openPos["leverage"].(int) leverage := openPos["leverage"].(int)
// 计算盈亏百分比 // 计算实际盈亏(USDT
pnlPct := 0.0 // 合约交易 PnL 计算:quantity × 价格差
// 注意:杠杆不影响绝对盈亏,只影响保证金需求
var pnl float64
if side == "long" { if side == "long" {
pnlPct = ((action.Price - openPrice) / openPrice) * 100 pnl = quantity * (action.Price - openPrice)
} else { } else {
pnlPct = ((openPrice - action.Price) / openPrice) * 100 pnl = quantity * (openPrice - action.Price)
} }
// 计算实际盈亏(USDT // 计算盈亏百分比(相对保证金
// PnL = 仓位价值 × 价格变化百分比 × 杠杆倍数
positionValue := quantity * openPrice 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{ outcome := TradeOutcome{
Symbol: symbol, Symbol: symbol,
Side: side, Side: side,
OpenPrice: openPrice, Quantity: quantity,
ClosePrice: action.Price, Leverage: leverage,
PnL: pnl, OpenPrice: openPrice,
PnLPct: pnlPct, ClosePrice: action.Price,
Duration: action.Timestamp.Sub(openTime).String(), PositionValue: positionValue,
OpenTime: openTime, MarginUsed: marginUsed,
CloseTime: action.Timestamp, PnL: pnl,
PnLPct: pnlPct,
Duration: action.Timestamp.Sub(openTime).String(),
OpenTime: openTime,
CloseTime: action.Timestamp,
} }
analysis.RecentTrades = append(analysis.RecentTrades, outcome) analysis.RecentTrades = append(analysis.RecentTrades, outcome)
analysis.TotalTrades++ analysis.TotalTrades++
// 分类交易:盈利、亏损、持平(避免将pnl=0算入亏损)
if pnl > 0 { if pnl > 0 {
analysis.WinningTrades++ analysis.WinningTrades++
analysis.AvgWin += pnl analysis.AvgWin += pnl
} else { } else if pnl < 0 {
analysis.LosingTrades++ analysis.LosingTrades++
analysis.AvgLoss += pnl analysis.AvgLoss += pnl
} }
// pnl == 0 的交易不计入盈利也不计入亏损,但计入总交易数
// 更新币种统计 // 更新币种统计
if _, exists := analysis.SymbolStats[symbol]; !exists { if _, exists := analysis.SymbolStats[symbol]; !exists {
@@ -414,7 +467,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
stats.TotalPnL += pnl stats.TotalPnL += pnl
if pnl > 0 { if pnl > 0 {
stats.WinningTrades++ stats.WinningTrades++
} else { } else if pnl < 0 {
stats.LosingTrades++ stats.LosingTrades++
} }
@@ -444,6 +497,9 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
// 注意:totalLossAmount 是负数,所以取负号得到绝对值 // 注意:totalLossAmount 是负数,所以取负号得到绝对值
if totalLossAmount != 0 { if totalLossAmount != 0 {
analysis.ProfitFactor = totalWinAmount / (-totalLossAmount) analysis.ProfitFactor = totalWinAmount / (-totalLossAmount)
} else if totalWinAmount > 0 {
// 只有盈利没有亏损的情况,设置为一个很大的值表示完美策略
analysis.ProfitFactor = 999.0
} }
} }
+5 -1
View File
@@ -41,6 +41,10 @@ func main() {
log.Printf("✓ 配置数据库初始化成功") log.Printf("✓ 配置数据库初始化成功")
fmt.Println() fmt.Println()
// 设置默认主流币种列表
defaultCoins := []string{"BTC", "ETH", "SOL", "BNB", "XRP", "DOGE", "ADA", "HYPE"}
pool.SetDefaultCoins(defaultCoins)
// 设置是否使用默认主流币种 // 设置是否使用默认主流币种
pool.SetUseDefaultCoins(useDefaultCoins) pool.SetUseDefaultCoins(useDefaultCoins)
if useDefaultCoins { if useDefaultCoins {
@@ -94,7 +98,7 @@ func main() {
fmt.Println() fmt.Println()
fmt.Println("🤖 AI全权决策模式:") 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将自主设置止损和止盈价格") fmt.Println(" • AI将自主设置止损和止盈价格")
fmt.Println(" • AI将基于市场数据、技术指标、账户状态做出全面分析") fmt.Println(" • AI将基于市场数据、技术指标、账户状态做出全面分析")
+71 -41
View File
@@ -16,54 +16,77 @@ type Provider string
const ( const (
ProviderDeepSeek Provider = "deepseek" ProviderDeepSeek Provider = "deepseek"
ProviderQwen Provider = "qwen" ProviderQwen Provider = "qwen"
ProviderCustom Provider = "custom"
) )
// Config AI API配置 // Client AI API配置
type Config struct { type Client struct {
Provider Provider Provider Provider
APIKey string APIKey string
SecretKey string // 阿里云需要 SecretKey string // 阿里云需要
BaseURL string BaseURL string
Model string Model string
Timeout time.Duration Timeout time.Duration
UseFullURL bool // 是否使用完整URL(不添加/chat/completions
} }
// 默认配置 func New() *Client {
var defaultConfig = Config{ // 默认配置
Provider: ProviderDeepSeek, var defaultClient = Client{
BaseURL: "https://api.deepseek.com/v1", Provider: ProviderDeepSeek,
Model: "deepseek-chat", BaseURL: "https://api.deepseek.com/v1",
Timeout: 120 * time.Second, // 增加到120秒,因为AI需要分析大量数据 Model: "deepseek-chat",
Timeout: 120 * time.Second, // 增加到120秒,因为AI需要分析大量数据
}
return &defaultClient
} }
// SetDeepSeekAPIKey 设置DeepSeek API密钥 // SetDeepSeekAPIKey 设置DeepSeek API密钥
func SetDeepSeekAPIKey(apiKey string) { func (cfg *Client) SetDeepSeekAPIKey(apiKey string) {
defaultConfig.Provider = ProviderDeepSeek cfg.Provider = ProviderDeepSeek
defaultConfig.APIKey = apiKey cfg.APIKey = apiKey
defaultConfig.BaseURL = "https://api.deepseek.com/v1" cfg.BaseURL = "https://api.deepseek.com/v1"
defaultConfig.Model = "deepseek-chat" cfg.Model = "deepseek-chat"
} }
// SetQwenAPIKey 设置阿里云Qwen API密钥 // SetQwenAPIKey 设置阿里云Qwen API密钥
func SetQwenAPIKey(apiKey, secretKey string) { func (cfg *Client) SetQwenAPIKey(apiKey, secretKey string) {
defaultConfig.Provider = ProviderQwen cfg.Provider = ProviderQwen
defaultConfig.APIKey = apiKey cfg.APIKey = apiKey
defaultConfig.SecretKey = secretKey cfg.SecretKey = secretKey
defaultConfig.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1" cfg.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
defaultConfig.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max cfg.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max
} }
// SetConfig 设置完整的AI配置(高级用户) // SetCustomAPI 设置自定义OpenAI兼容API
func SetConfig(config Config) { func (cfg *Client) SetCustomAPI(apiURL, apiKey, modelName string) {
if config.Timeout == 0 { cfg.Provider = ProviderCustom
config.Timeout = 30 * time.Second 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(推荐) // CallWithMessages 使用 system + user prompt 调用AI API(推荐)
func CallWithMessages(systemPrompt, userPrompt string) (string, error) { func (cfg *Client) CallWithMessages(systemPrompt, userPrompt string) (string, error) {
if defaultConfig.APIKey == "" { if cfg.APIKey == "" {
return "", fmt.Errorf("AI API密钥未设置,请先调用 SetDeepSeekAPIKey() 或 SetQwenAPIKey()") return "", fmt.Errorf("AI API密钥未设置,请先调用 SetDeepSeekAPIKey() 或 SetQwenAPIKey()")
} }
@@ -76,7 +99,7 @@ func CallWithMessages(systemPrompt, userPrompt string) (string, error) {
fmt.Printf("⚠️ AI API调用失败,正在重试 (%d/%d)...\n", attempt, maxRetries) fmt.Printf("⚠️ AI API调用失败,正在重试 (%d/%d)...\n", attempt, maxRetries)
} }
result, err := callOnce(systemPrompt, userPrompt) result, err := cfg.callOnce(systemPrompt, userPrompt)
if err == nil { if err == nil {
if attempt > 1 { if attempt > 1 {
fmt.Printf("✓ AI API重试成功\n") fmt.Printf("✓ AI API重试成功\n")
@@ -102,7 +125,7 @@ func CallWithMessages(systemPrompt, userPrompt string) (string, error) {
} }
// callOnce 单次调用AI API(内部使用) // callOnce 单次调用AI API(内部使用)
func callOnce(systemPrompt, userPrompt string) (string, error) { func (cfg *Client) callOnce(systemPrompt, userPrompt string) (string, error) {
// 构建 messages 数组 // 构建 messages 数组
messages := []map[string]string{} messages := []map[string]string{}
@@ -122,7 +145,7 @@ func callOnce(systemPrompt, userPrompt string) (string, error) {
// 构建请求体 // 构建请求体
requestBody := map[string]interface{}{ requestBody := map[string]interface{}{
"model": defaultConfig.Model, "model": cfg.Model,
"messages": messages, "messages": messages,
"temperature": 0.5, // 降低temperature以提高JSON格式稳定性 "temperature": 0.5, // 降低temperature以提高JSON格式稳定性
"max_tokens": 2000, "max_tokens": 2000,
@@ -137,7 +160,14 @@ func callOnce(systemPrompt, userPrompt string) (string, error) {
} }
// 创建HTTP请求 // 创建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)) req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
if err != nil { if err != nil {
return "", fmt.Errorf("创建请求失败: %w", err) return "", fmt.Errorf("创建请求失败: %w", err)
@@ -146,19 +176,19 @@ func callOnce(systemPrompt, userPrompt string) (string, error) {
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
// 根据不同的Provider设置认证方式 // 根据不同的Provider设置认证方式
switch defaultConfig.Provider { switch cfg.Provider {
case ProviderDeepSeek: case ProviderDeepSeek:
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey))
case ProviderQwen: case ProviderQwen:
// 阿里云Qwen使用API-Key认证 // 阿里云Qwen使用API-Key认证
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey))
// 注意:如果使用的不是兼容模式,可能需要不同的认证方式 // 注意:如果使用的不是兼容模式,可能需要不同的认证方式
default: default:
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey))
} }
// 发送请求 // 发送请求
client := &http.Client{Timeout: defaultConfig.Timeout} client := &http.Client{Timeout: cfg.Timeout}
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return "", fmt.Errorf("发送请求失败: %w", err) return "", fmt.Errorf("发送请求失败: %w", err)
+53
View File
@@ -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;
}
}
+41
View File
@@ -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
}
]
};
Executable
+258
View File
@@ -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
+9 -1
View File
@@ -12,7 +12,7 @@ import (
"time" "time"
) )
// defaultMainstreamCoins 默认主流币种池(当AI500和OI Top都失败时使用 // defaultMainstreamCoins 默认主流币种池(从配置文件读取
var defaultMainstreamCoins = []string{ var defaultMainstreamCoins = []string{
"BTCUSDT", "BTCUSDT",
"ETHUSDT", "ETHUSDT",
@@ -83,6 +83,14 @@ func SetUseDefaultCoins(useDefault bool) {
coinPoolConfig.UseDefaultCoins = useDefault coinPoolConfig.UseDefaultCoins = useDefault
} }
// SetDefaultCoins 设置默认主流币种列表
func SetDefaultCoins(coins []string) {
if len(coins) > 0 {
defaultMainstreamCoins = coins
log.Printf("✓ 已设置默认币种池(共%d个币种): %v", len(coins), coins)
}
}
// GetCoinPool 获取币种池列表(带重试和缓存机制) // GetCoinPool 获取币种池列表(带重试和缓存机制)
func GetCoinPool() ([]CoinInfo, error) { func GetCoinPool() ([]CoinInfo, error) {
// 优先检查是否启用默认币种列表 // 优先检查是否启用默认币种列表
Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

+120 -30
View File
@@ -1,18 +1,24 @@
#!/bin/bash #!/bin/bash
# ═══════════════════════════════════════════════════════════════
# NOFX AI Trading System - Docker Quick Start Script # NOFX AI Trading System - Docker Quick Start Script
# 使用方法: ./start.sh [command] # Usage: ./start.sh [command]
# ═══════════════════════════════════════════════════════════════
set -e set -e
# 颜色定义 # ------------------------------------------------------------------------
# Color Definitions
# ------------------------------------------------------------------------
RED='\033[0;31m' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
BLUE='\033[0;34m' BLUE='\033[0;34m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
# 打印带颜色的消息 # ------------------------------------------------------------------------
# Utility Functions: Colored Output
# ------------------------------------------------------------------------
print_info() { print_info() {
echo -e "${BLUE}[INFO]${NC} $1" echo -e "${BLUE}[INFO]${NC} $1"
} }
@@ -29,22 +35,51 @@ print_error() {
echo -e "${RED}[ERROR]${NC} $1" 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() { check_docker() {
if ! command -v docker &> /dev/null; then if ! command -v docker &> /dev/null; then
print_error "Docker 未安装!请先安装 Docker: https://docs.docker.com/get-docker/" print_error "Docker 未安装!请先安装 Docker: https://docs.docker.com/get-docker/"
exit 1 exit 1
fi fi
if ! command -v docker-compose &> /dev/null; then detect_compose_cmd
print_error "Docker Compose 未安装!请先安装 Docker Compose"
exit 1
fi
print_success "Docker 和 Docker Compose 已安装" 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() { check_config() {
if [ ! -f "config.json" ]; then if [ ! -f "config.json" ]; then
print_warning "config.json 不存在,从模板复制..." print_warning "config.json 不存在,从模板复制..."
@@ -56,15 +91,53 @@ check_config() {
print_success "配置文件存在" 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() { start() {
print_info "正在启动 NOFX AI Trading System..." 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 if [ "$1" == "--build" ]; then
print_info "重新构建镜像..." print_info "重新构建镜像..."
docker-compose up -d --build $COMPOSE_CMD up -d --build
else else
docker-compose up -d print_info "启动容器..."
$COMPOSE_CMD up -d
fi fi
print_success "服务已启动!" print_success "服务已启动!"
@@ -75,60 +148,74 @@ start() {
print_info "停止服务: ./start.sh stop" print_info "停止服务: ./start.sh stop"
} }
# 停止服务 # ------------------------------------------------------------------------
# Service Management: Stop
# ------------------------------------------------------------------------
stop() { stop() {
print_info "正在停止服务..." print_info "正在停止服务..."
docker-compose stop $COMPOSE_CMD stop
print_success "服务已停止" print_success "服务已停止"
} }
# 重启服务 # ------------------------------------------------------------------------
# Service Management: Restart
# ------------------------------------------------------------------------
restart() { restart() {
print_info "正在重启服务..." print_info "正在重启服务..."
docker-compose restart $COMPOSE_CMD restart
print_success "服务已重启" print_success "服务已重启"
} }
# 查看日志 # ------------------------------------------------------------------------
# Monitoring: Logs
# ------------------------------------------------------------------------
logs() { logs() {
if [ -z "$2" ]; then if [ -z "$2" ]; then
docker-compose logs -f $COMPOSE_CMD logs -f
else else
docker-compose logs -f "$2" $COMPOSE_CMD logs -f "$2"
fi fi
} }
# 查看状态 # ------------------------------------------------------------------------
# Monitoring: Status
# ------------------------------------------------------------------------
status() { status() {
print_info "服务状态:" print_info "服务状态:"
docker-compose ps $COMPOSE_CMD ps
echo "" echo ""
print_info "健康检查:" print_info "健康检查:"
curl -s http://localhost:8080/health | jq '.' || echo "后端未响应" curl -s http://localhost:8080/health | jq '.' || echo "后端未响应"
} }
# 清理 # ------------------------------------------------------------------------
# Maintenance: Clean (Destructive)
# ------------------------------------------------------------------------
clean() { clean() {
print_warning "这将删除所有容器和数据!" print_warning "这将删除所有容器和数据!"
read -p "确认删除?(yes/no): " confirm read -p "确认删除?(yes/no): " confirm
if [ "$confirm" == "yes" ]; then if [ "$confirm" == "yes" ]; then
print_info "正在清理..." print_info "正在清理..."
docker-compose down -v $COMPOSE_CMD down -v
print_success "清理完成" print_success "清理完成"
else else
print_info "已取消" print_info "已取消"
fi fi
} }
# 更新 # ------------------------------------------------------------------------
# Maintenance: Update
# ------------------------------------------------------------------------
update() { update() {
print_info "正在更新..." print_info "正在更新..."
git pull git pull
docker-compose up -d --build $COMPOSE_CMD up -d --build
print_success "更新完成" print_success "更新完成"
} }
# 显示帮助 # ------------------------------------------------------------------------
# Help: Usage Information
# ------------------------------------------------------------------------
show_help() { show_help() {
echo "NOFX AI Trading System - Docker 管理脚本" echo "NOFX AI Trading System - Docker 管理脚本"
echo "" echo ""
@@ -150,12 +237,15 @@ show_help() {
echo " ./start.sh status # 查看状态" echo " ./start.sh status # 查看状态"
} }
# 主函数 # ------------------------------------------------------------------------
# Main: Command Dispatcher
# ------------------------------------------------------------------------
main() { main() {
check_docker check_docker
case "${1:-start}" in case "${1:-start}" in
start) start)
check_env
check_config check_config
start "$2" start "$2"
;; ;;
@@ -188,5 +278,5 @@ main() {
esac esac
} }
# 运行主函数 # Execute Main
main "$@" main "$@"
+959
View File
@@ -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
}
+72 -38
View File
@@ -21,7 +21,7 @@ type AutoTraderConfig struct {
AIModel string // AI模型: "qwen" 或 "deepseek" AIModel string // AI模型: "qwen" 或 "deepseek"
// 交易平台选择 // 交易平台选择
Exchange string // "binance" "hyperliquid" Exchange string // "binance", "hyperliquid" 或 "aster"
// 币安API配置 // 币安API配置
BinanceAPIKey string BinanceAPIKey string
@@ -29,8 +29,14 @@ type AutoTraderConfig struct {
// Hyperliquid配置 // Hyperliquid配置
HyperliquidPrivateKey string HyperliquidPrivateKey string
HyperliquidWalletAddr string
HyperliquidTestnet bool HyperliquidTestnet bool
// Aster配置
AsterUser string // Aster主钱包地址
AsterSigner string // Aster API钱包地址
AsterPrivateKey string // Aster API钱包私钥
CoinPoolAPIURL string CoinPoolAPIURL string
// AI配置 // AI配置
@@ -38,12 +44,21 @@ type AutoTraderConfig struct {
DeepSeekKey string DeepSeekKey string
QwenKey string QwenKey string
// 自定义AI API配置
CustomAPIURL string
CustomAPIKey string
CustomModelName string
// 扫描配置 // 扫描配置
ScanInterval time.Duration // 扫描间隔(建议3分钟) ScanInterval time.Duration // 扫描间隔(建议3分钟)
// 账户配置 // 账户配置
InitialBalance float64 // 初始金额(用于计算盈亏,需手动设置) InitialBalance float64 // 初始金额(用于计算盈亏,需手动设置)
// 杠杆配置
BTCETHLeverage int // BTC和ETH的杠杆倍数
AltcoinLeverage int // 山寨币的杠杆倍数
// 风险控制(仅作为提示,AI可自主决定) // 风险控制(仅作为提示,AI可自主决定)
MaxDailyLoss float64 // 最大日亏损百分比(提示) MaxDailyLoss float64 // 最大日亏损百分比(提示)
MaxDrawdown float64 // 最大回撤百分比(提示) MaxDrawdown float64 // 最大回撤百分比(提示)
@@ -52,21 +67,22 @@ type AutoTraderConfig struct {
// AutoTrader 自动交易器 // AutoTrader 自动交易器
type AutoTrader struct { type AutoTrader struct {
id string // Trader唯一标识 id string // Trader唯一标识
name string // Trader显示名称 name string // Trader显示名称
aiModel string // AI模型名称 aiModel string // AI模型名称
exchange string // 交易平台名称 exchange string // 交易平台名称
config AutoTraderConfig config AutoTraderConfig
trader Trader // 使用Trader接口(支持多平台) trader Trader // 使用Trader接口(支持多平台)
decisionLogger *logger.DecisionLogger // 决策日志记录器 mcpClient *mcp.Client
initialBalance float64 decisionLogger *logger.DecisionLogger // 决策日志记录器
dailyPnL float64 initialBalance float64
lastResetTime time.Time dailyPnL float64
stopUntil time.Time lastResetTime time.Time
isRunning bool stopUntil time.Time
startTime time.Time // 系统启动时间 isRunning bool
callCount int // AI调用次数 startTime time.Time // 系统启动时间
positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒) callCount int // AI调用次数
positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒)
} }
// NewAutoTrader 创建自动交易器 // NewAutoTrader 创建自动交易器
@@ -86,12 +102,20 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
} }
} }
mcpClient := mcp.New()
// 初始化AI // 初始化AI
if config.UseQwen { if config.AIModel == "custom" {
mcp.SetQwenAPIKey(config.QwenKey, "") // 使用自定义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) log.Printf("🤖 [%s] 使用阿里云Qwen AI", config.Name)
} else { } else {
mcp.SetDeepSeekAPIKey(config.DeepSeekKey) // 默认使用DeepSeek
mcpClient.SetDeepSeekAPIKey(config.DeepSeekKey)
log.Printf("🤖 [%s] 使用DeepSeek AI", config.Name) log.Printf("🤖 [%s] 使用DeepSeek AI", config.Name)
} }
@@ -115,10 +139,16 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
trader = NewFuturesTrader(config.BinanceAPIKey, config.BinanceSecretKey) trader = NewFuturesTrader(config.BinanceAPIKey, config.BinanceSecretKey)
case "hyperliquid": case "hyperliquid":
log.Printf("🏦 [%s] 使用Hyperliquid交易", config.Name) 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 { if err != nil {
return nil, fmt.Errorf("初始化Hyperliquid交易器失败: %w", err) 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: default:
return nil, fmt.Errorf("不支持的交易平台: %s", config.Exchange) return nil, fmt.Errorf("不支持的交易平台: %s", config.Exchange)
} }
@@ -133,18 +163,19 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
decisionLogger := logger.NewDecisionLogger(logDir) decisionLogger := logger.NewDecisionLogger(logDir)
return &AutoTrader{ return &AutoTrader{
id: config.ID, id: config.ID,
name: config.Name, name: config.Name,
aiModel: config.AIModel, aiModel: config.AIModel,
exchange: config.Exchange, exchange: config.Exchange,
config: config, config: config,
trader: trader, trader: trader,
decisionLogger: decisionLogger, mcpClient: mcpClient,
initialBalance: config.InitialBalance, decisionLogger: decisionLogger,
lastResetTime: time.Now(), initialBalance: config.InitialBalance,
startTime: time.Now(), lastResetTime: time.Now(),
callCount: 0, startTime: time.Now(),
isRunning: false, callCount: 0,
isRunning: false,
positionFirstSeenTime: make(map[string]int64), positionFirstSeenTime: make(map[string]int64),
}, nil }, nil
} }
@@ -256,7 +287,7 @@ func (at *AutoTrader) runCycle() error {
// 4. 调用AI获取完整决策 // 4. 调用AI获取完整决策
log.Println("🤖 正在请求AI分析并决策...") log.Println("🤖 正在请求AI分析并决策...")
decision, err := decision.GetFullDecision(ctx) decision, err := decision.GetFullDecision(ctx, at.mcpClient)
// 即使有错误,也保存思维链、决策和输入prompt(用于debug // 即使有错误,也保存思维链、决策和输入prompt(用于debug
if decision != nil { if decision != nil {
@@ -479,8 +510,9 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) {
marginUsedPct = (totalMarginUsed / totalEquity) * 100 marginUsedPct = (totalMarginUsed / totalEquity) * 100
} }
// 5. 分析历史表现(最近20个周期) // 5. 分析历史表现(最近100个周期,避免长期持仓的交易记录丢失
performance, err := at.decisionLogger.AnalyzePerformance(20) // 假设每3分钟一个周期,100个周期 = 5小时,足够覆盖大部分交易
performance, err := at.decisionLogger.AnalyzePerformance(100)
if err != nil { if err != nil {
log.Printf("⚠️ 分析历史表现失败: %v", err) log.Printf("⚠️ 分析历史表现失败: %v", err)
// 不影响主流程,继续执行(但设置performance为nil以避免传递错误数据) // 不影响主流程,继续执行(但设置performance为nil以避免传递错误数据)
@@ -489,9 +521,11 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) {
// 6. 构建上下文 // 6. 构建上下文
ctx := &decision.Context{ ctx := &decision.Context{
CurrentTime: time.Now().Format("2006-01-02 15:04:05"), CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
RuntimeMinutes: int(time.Since(at.startTime).Minutes()), RuntimeMinutes: int(time.Since(at.startTime).Minutes()),
CallCount: at.callCount, CallCount: at.callCount,
BTCETHLeverage: at.config.BTCETHLeverage, // 使用配置的杠杆倍数
AltcoinLeverage: at.config.AltcoinLeverage, // 使用配置的杠杆倍数
Account: decision.AccountInfo{ Account: decision.AccountInfo{
TotalEquity: totalEquity, TotalEquity: totalEquity,
AvailableBalance: availableBalance, AvailableBalance: availableBalance,
+54 -4
View File
@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"log" "log"
"strconv" "strconv"
"sync"
"time" "time"
"github.com/adshao/go-binance/v2/futures" "github.com/adshao/go-binance/v2/futures"
@@ -13,19 +14,44 @@ import (
// FuturesTrader 币安合约交易器 // FuturesTrader 币安合约交易器
type FuturesTrader struct { type FuturesTrader struct {
client *futures.Client 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 创建合约交易器 // NewFuturesTrader 创建合约交易器
func NewFuturesTrader(apiKey, secretKey string) *FuturesTrader { func NewFuturesTrader(apiKey, secretKey string) *FuturesTrader {
client := futures.NewClient(apiKey, secretKey) client := futures.NewClient(apiKey, secretKey)
return &FuturesTrader{ return &FuturesTrader{
client: client, client: client,
cacheDuration: 15 * time.Second, // 15秒缓存
} }
} }
// GetBalance 获取账户余额 // GetBalance 获取账户余额(带缓存)
func (t *FuturesTrader) GetBalance() (map[string]interface{}, error) { 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()) account, err := t.client.NewGetAccountService().Do(context.Background())
if err != nil { if err != nil {
log.Printf("❌ 币安API调用失败: %v", err) log.Printf("❌ 币安API调用失败: %v", err)
@@ -42,11 +68,29 @@ func (t *FuturesTrader) GetBalance() (map[string]interface{}, error) {
account.AvailableBalance, account.AvailableBalance,
account.TotalUnrealizedProfit) account.TotalUnrealizedProfit)
// 更新缓存
t.balanceCacheMutex.Lock()
t.cachedBalance = result
t.balanceCacheTime = time.Now()
t.balanceCacheMutex.Unlock()
return result, nil return result, nil
} }
// GetPositions 获取所有持仓 // GetPositions 获取所有持仓(带缓存)
func (t *FuturesTrader) GetPositions() ([]map[string]interface{}, error) { 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()) positions, err := t.client.NewGetPositionRiskService().Do(context.Background())
if err != nil { if err != nil {
return nil, fmt.Errorf("获取持仓失败: %w", err) return nil, fmt.Errorf("获取持仓失败: %w", err)
@@ -78,6 +122,12 @@ func (t *FuturesTrader) GetPositions() ([]map[string]interface{}, error) {
result = append(result, posMap) result = append(result, posMap)
} }
// 更新缓存
t.positionsCacheMutex.Lock()
t.cachedPositions = result
t.positionsCacheTime = time.Now()
t.positionsCacheMutex.Unlock()
return result, nil return result, nil
} }
+24 -24
View File
@@ -2,7 +2,6 @@ package trader
import ( import (
"context" "context"
"crypto/ecdsa"
"fmt" "fmt"
"log" "log"
"strconv" "strconv"
@@ -20,7 +19,7 @@ type HyperliquidTrader struct {
} }
// NewHyperliquidTrader 创建Hyperliquid交易器 // 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) privateKey, err := crypto.HexToECDSA(privateKeyHex)
if err != nil { if err != nil {
@@ -33,13 +32,13 @@ func NewHyperliquidTrader(privateKeyHex string, testnet bool) (*HyperliquidTrade
apiURL = hyperliquid.TestnetAPIURL apiURL = hyperliquid.TestnetAPIURL
} }
// 从私钥生成钱包地址 // // 从私钥生成钱包地址
pubKey := privateKey.Public() // pubKey := privateKey.Public()
publicKeyECDSA, ok := pubKey.(*ecdsa.PublicKey) // publicKeyECDSA, ok := pubKey.(*ecdsa.PublicKey)
if !ok { // if !ok {
return nil, fmt.Errorf("无法转换公钥") // return nil, fmt.Errorf("无法转换公钥")
} // }
walletAddr := crypto.PubkeyToAddress(*publicKeyECDSA).Hex() // walletAddr := crypto.PubkeyToAddress(*publicKeyECDSA).Hex()
ctx := context.Background() ctx := context.Background()
@@ -86,6 +85,7 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
accountValue, _ := strconv.ParseFloat(accountState.CrossMarginSummary.AccountValue, 64) accountValue, _ := strconv.ParseFloat(accountState.CrossMarginSummary.AccountValue, 64)
totalMarginUsed, _ := strconv.ParseFloat(accountState.CrossMarginSummary.TotalMarginUsed, 64) totalMarginUsed, _ := strconv.ParseFloat(accountState.CrossMarginSummary.TotalMarginUsed, 64)
availableBalance, _ := strconv.ParseFloat(accountState.CrossMarginSummary.AccountValue, 64)
// ⚠️ 关键修复:从所有持仓中累加真正的未实现盈亏 // ⚠️ 关键修复:从所有持仓中累加真正的未实现盈亏
totalUnrealizedPnl := 0.0 totalUnrealizedPnl := 0.0
@@ -99,9 +99,9 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
// 钱包余额(已实现)= AccountValue - 未实现盈亏 // 钱包余额(已实现)= AccountValue - 未实现盈亏
walletBalance := accountValue - totalUnrealizedPnl walletBalance := accountValue - totalUnrealizedPnl
result["totalWalletBalance"] = walletBalance // 钱包余额(已实现部分) result["totalWalletBalance"] = walletBalance // 钱包余额(已实现部分)
result["availableBalance"] = accountValue - totalMarginUsed // 可用余额 result["availableBalance"] = availableBalance - totalMarginUsed // 可用余额
result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏 result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏
log.Printf("✓ Hyperliquid API返回: 账户净值=%.2f, 钱包余额=%.2f, 可用=%.2f, 未实现盈亏=%.2f", log.Printf("✓ Hyperliquid API返回: 账户净值=%.2f, 钱包余额=%.2f, 可用=%.2f, 未实现盈亏=%.2f",
accountValue, accountValue,
@@ -515,8 +515,8 @@ func (t *HyperliquidTrader) SetStopLoss(symbol string, positionSide string, quan
order := hyperliquid.CreateOrderRequest{ order := hyperliquid.CreateOrderRequest{
Coin: coin, Coin: coin,
IsBuy: isBuy, IsBuy: isBuy,
Size: roundedQuantity, // 使用四舍五入后的数量 Size: roundedQuantity, // 使用四舍五入后的数量
Price: roundedStopPrice, // 使用处理后的价格 Price: roundedStopPrice, // 使用处理后的价格
OrderType: hyperliquid.OrderType{ OrderType: hyperliquid.OrderType{
Trigger: &hyperliquid.TriggerOrderType{ Trigger: &hyperliquid.TriggerOrderType{
TriggerPx: roundedStopPrice, TriggerPx: roundedStopPrice,
@@ -552,8 +552,8 @@ func (t *HyperliquidTrader) SetTakeProfit(symbol string, positionSide string, qu
order := hyperliquid.CreateOrderRequest{ order := hyperliquid.CreateOrderRequest{
Coin: coin, Coin: coin,
IsBuy: isBuy, IsBuy: isBuy,
Size: roundedQuantity, // 使用四舍五入后的数量 Size: roundedQuantity, // 使用四舍五入后的数量
Price: roundedTakeProfitPrice, // 使用处理后的价格 Price: roundedTakeProfitPrice, // 使用处理后的价格
OrderType: hyperliquid.OrderType{ OrderType: hyperliquid.OrderType{
Trigger: &hyperliquid.TriggerOrderType{ Trigger: &hyperliquid.TriggerOrderType{
TriggerPx: roundedTakeProfitPrice, 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) { func (t *HyperliquidTrader) FormatQuantity(symbol string, quantity float64) (string, error) {
coin := convertSymbolToHyperliquid(symbol) coin := convertSymbolToHyperliquid(symbol)
szDecimals := t.getSzDecimals(coin) szDecimals := t.getSzDecimals(coin)
// 使用szDecimals格式化数量 // 使用szDecimals格式化数量
formatStr := fmt.Sprintf("%%.%df", szDecimals) formatStr := fmt.Sprintf("%%.%df", szDecimals)
return fmt.Sprintf(formatStr, quantity), nil return fmt.Sprintf(formatStr, quantity), nil
@@ -604,13 +604,13 @@ func (t *HyperliquidTrader) getSzDecimals(coin string) int {
// roundToSzDecimals 将数量四舍五入到正确的精度 // roundToSzDecimals 将数量四舍五入到正确的精度
func (t *HyperliquidTrader) roundToSzDecimals(coin string, quantity float64) float64 { func (t *HyperliquidTrader) roundToSzDecimals(coin string, quantity float64) float64 {
szDecimals := t.getSzDecimals(coin) szDecimals := t.getSzDecimals(coin)
// 计算倍数(10^szDecimals // 计算倍数(10^szDecimals
multiplier := 1.0 multiplier := 1.0
for i := 0; i < szDecimals; i++ { for i := 0; i < szDecimals; i++ {
multiplier *= 10.0 multiplier *= 10.0
} }
// 四舍五入 // 四舍五入
return float64(int(quantity*multiplier+0.5)) / multiplier return float64(int(quantity*multiplier+0.5)) / multiplier
} }
@@ -621,9 +621,9 @@ func (t *HyperliquidTrader) roundPriceToSigfigs(price float64) float64 {
if price == 0 { if price == 0 {
return 0 return 0
} }
const sigfigs = 5 // Hyperliquid标准:5位有效数字 const sigfigs = 5 // Hyperliquid标准:5位有效数字
// 计算价格的数量级 // 计算价格的数量级
var magnitude float64 var magnitude float64
if price < 0 { if price < 0 {
@@ -631,7 +631,7 @@ func (t *HyperliquidTrader) roundPriceToSigfigs(price float64) float64 {
} else { } else {
magnitude = price magnitude = price
} }
// 计算需要的倍数 // 计算需要的倍数
multiplier := 1.0 multiplier := 1.0
for magnitude >= 10 { for magnitude >= 10 {
@@ -642,12 +642,12 @@ func (t *HyperliquidTrader) roundPriceToSigfigs(price float64) float64 {
magnitude *= 10 magnitude *= 10
multiplier *= 10 multiplier *= 10
} }
// 应用有效数字精度 // 应用有效数字精度
for i := 0; i < sigfigs-1; i++ { for i := 0; i < sigfigs-1; i++ {
multiplier *= 10 multiplier *= 10
} }
// 四舍五入 // 四舍五入
rounded := float64(int(price*multiplier+0.5)) / multiplier rounded := float64(int(price*multiplier+0.5)) / multiplier
return rounded return rounded
-72
View File
@@ -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 <<EOF /etc/nginx/conf.d/default.conf
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# 启用 gzip 压缩
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
gzip_min_length 1000;
# SPA 路由支持
location / {
try_files \$uri \$uri/ /index.html;
}
# API 代理到后端
location /api/ {
proxy_pass http://backend: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;
}
# 健康检查端点
location /health {
proxy_pass http://backend:8080/health;
access_log off;
}
}
EOF
# 从构建阶段复制构建产物
COPY --from=builder /app/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
# 启动 nginx
CMD ["nginx", "-g", "daemon off;"]
+116 -83
View File
@@ -42,9 +42,9 @@ function App() {
: null, : null,
() => api.getStatus(selectedTraderId), () => api.getStatus(selectedTraderId),
{ {
refreshInterval: 5000, refreshInterval: 15000, // 15秒刷新(配合后端15秒缓存)
revalidateOnFocus: true, revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求
dedupingInterval: 0, dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求
} }
); );
@@ -54,9 +54,9 @@ function App() {
: null, : null,
() => api.getAccount(selectedTraderId), () => api.getAccount(selectedTraderId),
{ {
refreshInterval: 5000, refreshInterval: 15000, // 15秒刷新(配合后端15秒缓存)
revalidateOnFocus: true, revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求
dedupingInterval: 0, dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求
} }
); );
@@ -66,9 +66,9 @@ function App() {
: null, : null,
() => api.getPositions(selectedTraderId), () => api.getPositions(selectedTraderId),
{ {
refreshInterval: 5000, refreshInterval: 15000, // 15秒刷新(配合后端15秒缓存)
revalidateOnFocus: true, revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求
dedupingInterval: 0, dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求
} }
); );
@@ -77,7 +77,11 @@ function App() {
? `decisions/latest-${selectedTraderId}` ? `decisions/latest-${selectedTraderId}`
: null, : null,
() => api.getLatestDecisions(selectedTraderId), () => api.getLatestDecisions(selectedTraderId),
{ refreshInterval: 10000 } {
refreshInterval: 30000, // 30秒刷新(决策更新频率较低)
revalidateOnFocus: false,
dedupingInterval: 20000,
}
); );
const { data: stats } = useSWR<Statistics>( const { data: stats } = useSWR<Statistics>(
@@ -85,7 +89,11 @@ function App() {
? `statistics-${selectedTraderId}` ? `statistics-${selectedTraderId}`
: null, : null,
() => api.getStatistics(selectedTraderId), () => api.getStatistics(selectedTraderId),
{ refreshInterval: 10000 } {
refreshInterval: 30000, // 30秒刷新(统计数据更新频率较低)
revalidateOnFocus: false,
dedupingInterval: 20000,
}
); );
useEffect(() => { useEffect(() => {
@@ -101,55 +109,104 @@ function App() {
<div className="min-h-screen" style={{ background: '#0B0E11', color: '#EAECEF' }}> <div className="min-h-screen" style={{ background: '#0B0E11', color: '#EAECEF' }}>
{/* Header - Binance Style */} {/* Header - Binance Style */}
<header className="glass sticky top-0 z-50 backdrop-blur-xl"> <header className="glass sticky top-0 z-50 backdrop-blur-xl">
<div className="max-w-[1920px] mx-auto px-6 py-4"> <div className="max-w-[1920px] mx-auto px-3 sm:px-6 py-3 sm:py-4">
<div className="relative flex items-center"> {/* Mobile: Two rows, Desktop: Single row */}
{/* Left - Logo and Title */} <div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div className="flex items-center gap-3"> {/* Left: Logo and Title */}
<div className="w-8 h-8 rounded-full flex items-center justify-center text-xl" style={{ background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)' }}> <div className="flex items-center gap-2 sm:gap-3 flex-shrink-0">
<div className="w-7 h-7 sm:w-8 sm:h-8 rounded-full flex items-center justify-center text-lg sm:text-xl" style={{ background: 'linear-gradient(135deg, #F0B90B 0%, #FCD535 100%)' }}>
</div> </div>
<div> <div>
<h1 className="text-xl font-bold" style={{ color: '#EAECEF' }}> <h1 className="text-base sm:text-xl font-bold leading-tight" style={{ color: '#EAECEF' }}>
{t('appTitle', language)} {t('appTitle', language)}
</h1> </h1>
<p className="text-xs mono" style={{ color: '#848E9C' }}> <p className="text-xs mono hidden sm:block" style={{ color: '#848E9C' }}>
{t('subtitle', language)} {t('subtitle', language)}
</p> </p>
</div> </div>
</div> </div>
{/* Center - Page Toggle (absolutely positioned) */} {/* Right: Controls - Wrap on mobile */}
<div className="absolute left-1/2 transform -translate-x-1/2 flex gap-1 rounded p-1" style={{ background: '#1E2329' }}> <div className="flex items-center gap-2 flex-wrap md:flex-nowrap">
<button {/* GitHub Link - Hidden on mobile, icon only on tablet */}
onClick={() => setCurrentPage('traders')} <a
className={`px-4 py-2 rounded text-sm font-semibold transition-all`} href="https://github.com/tinkle-community/nofx"
style={currentPage === 'traders' target="_blank"
? { background: '#F0B90B', color: '#000' } rel="noopener noreferrer"
: { background: 'transparent', color: '#848E9C' } className="hidden sm:flex items-center gap-2 px-2 md:px-3 py-1.5 md:py-2 rounded text-sm font-semibold transition-all hover:scale-105"
} style={{ background: '#1E2329', color: '#848E9C', border: '1px solid #2B3139' }}
onMouseEnter={(e) => {
e.currentTarget.style.background = '#2B3139';
e.currentTarget.style.color = '#EAECEF';
e.currentTarget.style.borderColor = '#F0B90B';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = '#1E2329';
e.currentTarget.style.color = '#848E9C';
e.currentTarget.style.borderColor = '#2B3139';
}}
> >
{t('aiTraders', language)} <svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor">
</button> <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
<button </svg>
onClick={() => setCurrentPage('trader')} <span className="hidden md:inline">GitHub</span>
className={`px-4 py-2 rounded text-sm font-semibold transition-all`} </a>
style={currentPage === 'trader'
? { background: '#F0B90B', color: '#000' } {/* Language Toggle */}
: { background: 'transparent', color: '#848E9C' } <div className="flex gap-0.5 sm:gap-1 rounded p-0.5 sm:p-1" style={{ background: '#1E2329' }}>
} <button
> onClick={() => setLanguage('zh')}
{t('tradingPanel', language)} className="px-2 sm:px-3 py-1 sm:py-1.5 rounded text-xs font-semibold transition-all"
</button> style={language === 'zh'
</div> ? { background: '#F0B90B', color: '#000' }
: { background: 'transparent', color: '#848E9C' }
{/* Right - Actions */} }
<div className="ml-auto flex items-center gap-3"> >
</button>
<button
onClick={() => setLanguage('en')}
className="px-2 sm:px-3 py-1 sm:py-1.5 rounded text-xs font-semibold transition-all"
style={language === 'en'
? { background: '#F0B90B', color: '#000' }
: { background: 'transparent', color: '#848E9C' }
}
>
EN
</button>
</div>
{/* Page Toggle */}
<div className="flex gap-0.5 sm:gap-1 rounded p-0.5 sm:p-1" style={{ background: '#1E2329' }}>
<button
onClick={() => setCurrentPage('traders')}
className="px-2 sm:px-4 py-1.5 sm:py-2 rounded text-xs sm:text-sm font-semibold transition-all"
style={currentPage === 'traders'
? { background: '#F0B90B', color: '#000' }
: { background: 'transparent', color: '#848E9C' }
}
>
{t('aiTraders', language)}
</button>
<button
onClick={() => setCurrentPage('trader')}
className="px-2 sm:px-4 py-1.5 sm:py-2 rounded text-xs sm:text-sm font-semibold transition-all"
style={currentPage === 'trader'
? { background: '#F0B90B', color: '#000' }
: { background: 'transparent', color: '#848E9C' }
}
>
{t('tradingPanel', language)}
</button>
</div>
{/* Trader Selector (only show on trader page) */} {/* Trader Selector (only show on trader page) */}
{currentPage === 'trader' && traders && traders.length > 0 && ( {currentPage === 'trader' && traders && traders.length > 0 && (
<select <select
value={selectedTraderId} value={selectedTraderId}
onChange={(e) => setSelectedTraderId(e.target.value)} onChange={(e) => setSelectedTraderId(e.target.value)}
className="rounded px-3 py-2 text-sm font-medium cursor-pointer transition-colors" className="rounded px-2 sm:px-3 py-1.5 sm:py-2 text-xs sm:text-sm font-medium cursor-pointer transition-colors flex-1 sm:flex-initial"
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }} style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
> >
{traders.map((trader) => ( {traders.map((trader) => (
@@ -160,34 +217,10 @@ function App() {
</select> </select>
)} )}
{/* Language Toggle */}
<div className="flex gap-1 rounded p-1" style={{ background: '#1E2329' }}>
<button
onClick={() => setLanguage('zh')}
className="px-3 py-1.5 rounded text-xs font-semibold transition-all"
style={language === 'zh'
? { background: '#F0B90B', color: '#000' }
: { background: 'transparent', color: '#848E9C' }
}
>
</button>
<button
onClick={() => setLanguage('en')}
className="px-3 py-1.5 rounded text-xs font-semibold transition-all"
style={language === 'en'
? { background: '#F0B90B', color: '#000' }
: { background: 'transparent', color: '#848E9C' }
}
>
EN
</button>
</div>
{/* Status Indicator (only show on trader page) */} {/* Status Indicator (only show on trader page) */}
{currentPage === 'trader' && status && ( {currentPage === 'trader' && status && (
<div <div
className="flex items-center gap-2 px-3 py-2 rounded" className="flex items-center gap-1.5 sm:gap-2 px-2 sm:px-3 py-1.5 sm:py-2 rounded"
style={status.is_running style={status.is_running
? { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81', border: '1px solid rgba(14, 203, 129, 0.2)' } ? { background: 'rgba(14, 203, 129, 0.1)', color: '#0ECB81', border: '1px solid rgba(14, 203, 129, 0.2)' }
: { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.2)' } : { background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.2)' }
@@ -323,23 +356,23 @@ function TraderDetailsPage({
title={t('totalEquity', language)} title={t('totalEquity', language)}
value={`${account?.total_equity?.toFixed(2) || '0.00'} USDT`} value={`${account?.total_equity?.toFixed(2) || '0.00'} USDT`}
change={account?.total_pnl_pct || 0} change={account?.total_pnl_pct || 0}
positive={account ? (account.total_pnl || 0) > 0 : false} positive={(account?.total_pnl ?? 0) > 0}
/> />
<StatCard <StatCard
title={t('availableBalance', language)} title={t('availableBalance', language)}
value={`${account?.available_balance?.toFixed(2) || '0.00'} USDT`} value={`${account?.available_balance?.toFixed(2) || '0.00'} USDT`}
subtitle={`${((account?.available_balance && account?.total_equity ? (account.available_balance / account.total_equity) * 100 : 0)).toFixed(1)}% ${t('free', language)}`} subtitle={`${(account?.available_balance && account?.total_equity ? ((account.available_balance / account.total_equity) * 100).toFixed(1) : '0.0')}% ${t('free', language)}`}
/> />
<StatCard <StatCard
title={t('totalPnL', language)} title={t('totalPnL', language)}
value={`${(account?.total_pnl || 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} change={account?.total_pnl_pct || 0}
positive={account ? (account.total_pnl || 0) >= 0 : false} positive={(account?.total_pnl ?? 0) >= 0}
/> />
<StatCard <StatCard
title={t('positions', language)} title={t('positions', language)}
value={`${account?.position_count || 0}`} value={`${account?.position_count || 0}`}
subtitle={`${t('margin', language)}: ${account?.margin_used_pct.toFixed(1) || '0.0'}%`} subtitle={`${t('margin', language)}: ${account?.margin_used_pct?.toFixed(1) || '0.0'}%`}
/> />
</div> </div>
@@ -511,7 +544,7 @@ function StatCard({
// Decision Card Component with CoT Trace - Binance Style // Decision Card Component with CoT Trace - Binance Style
function DecisionCard({ decision, language }: { decision: DecisionRecord; language: Language }) { function DecisionCard({ decision, language }: { decision: DecisionRecord; language: Language }) {
const [showInput, setShowInput] = useState(false); const [showInputPrompt, setShowInputPrompt] = useState(false);
const [showCoT, setShowCoT] = useState(false); const [showCoT, setShowCoT] = useState(false);
return ( return (
@@ -535,20 +568,20 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua
</div> </div>
</div> </div>
{/* AI Input Prompt - Collapsible */} {/* Input Prompt - Collapsible */}
{(decision as any).input_prompt && ( {decision.input_prompt && (
<div className="mb-3"> <div className="mb-3">
<button <button
onClick={() => setShowInput(!showInput)} onClick={() => setShowInputPrompt(!showInputPrompt)}
className="flex items-center gap-2 text-sm transition-colors" className="flex items-center gap-2 text-sm transition-colors"
style={{ color: '#8B5CF6' }} style={{ color: '#60a5fa' }}
> >
<span className="font-semibold">📥 {t('inputPrompt', language)}</span> <span className="font-semibold">📥 {t('inputPrompt', language)}</span>
<span className="text-xs">{showInput ? t('collapse', language) : t('expand', language)}</span> <span className="text-xs">{showInputPrompt ? t('collapse', language) : t('expand', language)}</span>
</button> </button>
{showInput && ( {showInputPrompt && (
<div className="mt-2 rounded p-4 text-sm font-mono whitespace-pre-wrap max-h-96 overflow-y-auto" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}> <div className="mt-2 rounded p-4 text-sm font-mono whitespace-pre-wrap max-h-96 overflow-y-auto" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}>
{(decision as any).input_prompt} {decision.input_prompt}
</div> </div>
)} )}
</div> </div>
+40 -5
View File
@@ -1,12 +1,17 @@
import useSWR from 'swr'; import useSWR from 'swr';
import { useLanguage } from '../contexts/LanguageContext'; import { useLanguage } from '../contexts/LanguageContext';
import { t } from '../i18n/translations'; import { t } from '../i18n/translations';
import { api } from '../lib/api';
interface TradeOutcome { interface TradeOutcome {
symbol: string; symbol: string;
side: string; side: string;
quantity: number;
leverage: number;
open_price: number; open_price: number;
close_price: number; close_price: number;
position_value: number;
margin_used: number;
pn_l: number; pn_l: number;
pn_l_pct: number; pn_l_pct: number;
duration: string; duration: string;
@@ -44,14 +49,16 @@ interface AILearningProps {
traderId: string; traderId: string;
} }
const fetcher = (url: string) => fetch(url).then(res => res.json());
export default function AILearning({ traderId }: AILearningProps) { export default function AILearning({ traderId }: AILearningProps) {
const { language } = useLanguage(); const { language } = useLanguage();
const { data: performance, error } = useSWR<PerformanceAnalysis>( const { data: performance, error } = useSWR<PerformanceAnalysis>(
`http://localhost:8080/api/performance?trader_id=${traderId}`, traderId ? `performance-${traderId}` : 'performance',
fetcher, () => api.getPerformance(traderId),
{ refreshInterval: 10000 } {
refreshInterval: 30000, // 30秒刷新(AI学习分析数据更新频率较低)
revalidateOnFocus: false,
dedupingInterval: 20000,
}
); );
if (error) { if (error) {
@@ -555,6 +562,34 @@ export default function AILearning({ traderId }: AILearningProps) {
</div> </div>
</div> </div>
{/* Position Details */}
<div className="grid grid-cols-2 gap-2 mb-3 text-xs">
<div>
<div style={{ color: '#94A3B8' }}>Quantity</div>
<div className="font-mono font-semibold" style={{ color: '#CBD5E1' }}>
{trade.quantity ? trade.quantity.toFixed(4) : '-'}
</div>
</div>
<div className="text-right">
<div style={{ color: '#94A3B8' }}>Leverage</div>
<div className="font-mono font-semibold" style={{ color: '#FCD34D' }}>
{trade.leverage ? `${trade.leverage}x` : '-'}
</div>
</div>
<div>
<div style={{ color: '#94A3B8' }}>Position Value</div>
<div className="font-mono font-semibold" style={{ color: '#CBD5E1' }}>
{trade.position_value ? `$${trade.position_value.toFixed(2)}` : '-'}
</div>
</div>
<div className="text-right">
<div style={{ color: '#94A3B8' }}>Margin Used</div>
<div className="font-mono font-semibold" style={{ color: '#A78BFA' }}>
{trade.margin_used ? `$${trade.margin_used.toFixed(2)}` : '-'}
</div>
</div>
</div>
<div className="rounded-lg p-2 mb-2" style={{ <div className="rounded-lg p-2 mb-2" style={{
background: isProfitable ? 'rgba(16, 185, 129, 0.1)' : 'rgba(248, 113, 113, 0.1)' background: isProfitable ? 'rgba(16, 185, 129, 0.1)' : 'rgba(248, 113, 113, 0.1)'
}}> }}>
+39 -29
View File
@@ -34,7 +34,9 @@ export function EquityChart({ traderId }: EquityChartProps) {
traderId ? `equity-history-${traderId}` : 'equity-history', traderId ? `equity-history-${traderId}` : 'equity-history',
() => api.getEquityHistory(traderId), () => 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', traderId ? `account-${traderId}` : 'account',
() => api.getAccount(traderId), () => 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 ( return (
<div className="binance-card p-6"> <div className="binance-card p-6">
<h3 className="text-lg font-semibold mb-6" style={{ color: '#EAECEF' }}>{t('accountEquityCurve', language)}</h3> <h3 className="text-lg font-semibold mb-6" style={{ color: '#EAECEF' }}>{t('accountEquityCurve', language)}</h3>
@@ -76,12 +83,14 @@ export function EquityChart({ traderId }: EquityChartProps) {
// 限制显示最近的数据点(性能优化) // 限制显示最近的数据点(性能优化)
// 如果数据超过2000个点,只显示最近2000个 // 如果数据超过2000个点,只显示最近2000个
const MAX_DISPLAY_POINTS = 2000; const MAX_DISPLAY_POINTS = 2000;
const displayHistory = history.length > MAX_DISPLAY_POINTS const displayHistory = validHistory.length > MAX_DISPLAY_POINTS
? history.slice(-MAX_DISPLAY_POINTS) ? validHistory.slice(-MAX_DISPLAY_POINTS)
: history; : validHistory;
// 计算初始余额(使用第一个数据点) // 计算初始余额(使用第一个有效数据点,如果无数据则从account获取,最后才用默认值
const initialBalance = history[0]?.total_equity || 1000; const initialBalance = validHistory[0]?.total_equity
|| account?.total_equity
|| 100; // 默认值改为100,与常见配置一致
// 转换数据格式 // 转换数据格式
const chartData = displayHistory.map((point) => { const chartData = displayHistory.map((point) => {
@@ -152,19 +161,19 @@ export function EquityChart({ traderId }: EquityChartProps) {
}; };
return ( return (
<div className="binance-card p-5 animate-fade-in"> <div className="binance-card p-3 sm:p-5 animate-fade-in">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-4"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between mb-4">
<div> <div className="flex-1">
<h3 className="text-lg font-bold mb-2" style={{ color: '#EAECEF' }}>{t('accountEquityCurve', language)}</h3> <h3 className="text-base sm:text-lg font-bold mb-2" style={{ color: '#EAECEF' }}>{t('accountEquityCurve', language)}</h3>
<div className="flex items-baseline gap-4"> <div className="flex flex-col sm:flex-row sm:items-baseline gap-2 sm:gap-4">
<span className="text-3xl font-bold mono" style={{ color: '#EAECEF' }}> <span className="text-2xl sm:text-3xl font-bold mono" style={{ color: '#EAECEF' }}>
{account?.total_equity.toFixed(2) || '0.00'} {account?.total_equity.toFixed(2) || '0.00'}
<span className="text-lg ml-1" style={{ color: '#848E9C' }}>USDT</span> <span className="text-base sm:text-lg ml-1" style={{ color: '#848E9C' }}>USDT</span>
</span> </span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 flex-wrap">
<span <span
className="text-lg font-bold mono px-3 py-1 rounded" className="text-sm sm:text-lg font-bold mono px-2 sm:px-3 py-1 rounded"
style={{ style={{
color: isProfit ? '#0ECB81' : '#F6465D', color: isProfit ? '#0ECB81' : '#F6465D',
background: isProfit ? 'rgba(14, 203, 129, 0.1)' : 'rgba(246, 70, 93, 0.1)', background: isProfit ? 'rgba(14, 203, 129, 0.1)' : 'rgba(246, 70, 93, 0.1)',
@@ -174,7 +183,7 @@ export function EquityChart({ traderId }: EquityChartProps) {
{isProfit ? '▲' : '▼'} {isProfit ? '+' : ''} {isProfit ? '▲' : '▼'} {isProfit ? '+' : ''}
{currentValue.raw_pnl_pct}% {currentValue.raw_pnl_pct}%
</span> </span>
<span className="text-sm mono" style={{ color: '#848E9C' }}> <span className="text-xs sm:text-sm mono" style={{ color: '#848E9C' }}>
({isProfit ? '+' : ''}{currentValue.raw_pnl.toFixed(2)} USDT) ({isProfit ? '+' : ''}{currentValue.raw_pnl.toFixed(2)} USDT)
</span> </span>
</div> </div>
@@ -182,10 +191,10 @@ export function EquityChart({ traderId }: EquityChartProps) {
</div> </div>
{/* Display Mode Toggle */} {/* Display Mode Toggle */}
<div className="flex gap-1 rounded p-1" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}> <div className="flex gap-0.5 sm:gap-1 rounded p-0.5 sm:p-1 self-start sm:self-auto" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
<button <button
onClick={() => setDisplayMode('dollar')} onClick={() => setDisplayMode('dollar')}
className="px-4 py-2 rounded text-sm font-bold transition-all" className="px-3 sm:px-4 py-1.5 sm:py-2 rounded text-xs sm:text-sm font-bold transition-all"
style={displayMode === 'dollar' style={displayMode === 'dollar'
? { background: '#F0B90B', color: '#000', boxShadow: '0 2px 8px rgba(240, 185, 11, 0.4)' } ? { background: '#F0B90B', color: '#000', boxShadow: '0 2px 8px rgba(240, 185, 11, 0.4)' }
: { background: 'transparent', color: '#848E9C' } : { background: 'transparent', color: '#848E9C' }
@@ -195,7 +204,7 @@ export function EquityChart({ traderId }: EquityChartProps) {
</button> </button>
<button <button
onClick={() => setDisplayMode('percent')} onClick={() => setDisplayMode('percent')}
className="px-4 py-2 rounded text-sm font-bold transition-all" className="px-3 sm:px-4 py-1.5 sm:py-2 rounded text-xs sm:text-sm font-bold transition-all"
style={displayMode === 'percent' style={displayMode === 'percent'
? { background: '#F0B90B', color: '#000', boxShadow: '0 2px 8px rgba(240, 185, 11, 0.4)' } ? { background: '#F0B90B', color: '#000', boxShadow: '0 2px 8px rgba(240, 185, 11, 0.4)' }
: { background: 'transparent', color: '#848E9C' } : { background: 'transparent', color: '#848E9C' }
@@ -248,39 +257,40 @@ export function EquityChart({ traderId }: EquityChartProps) {
}} }}
/> />
<Line <Line
type="monotone" type="natural"
dataKey="value" dataKey="value"
stroke="url(#colorGradient)" stroke="url(#colorGradient)"
strokeWidth={2.5} strokeWidth={3}
dot={chartData.length > 50 ? false : { fill: '#F0B90B', r: 3 }} dot={chartData.length > 50 ? false : { fill: '#F0B90B', r: 3 }}
activeDot={{ r: 6, fill: '#FCD535', stroke: '#F0B90B', strokeWidth: 2 }} activeDot={{ r: 6, fill: '#FCD535', stroke: '#F0B90B', strokeWidth: 2 }}
connectNulls={true}
/> />
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
{/* Footer Stats */} {/* Footer Stats */}
<div className="mt-3 grid grid-cols-4 gap-3 pt-3" style={{ borderTop: '1px solid #2B3139' }}> <div className="mt-3 grid grid-cols-2 sm:grid-cols-4 gap-2 sm:gap-3 pt-3" style={{ borderTop: '1px solid #2B3139' }}>
<div className="p-2 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}> <div className="p-2 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('initialBalance', language)}</div> <div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('initialBalance', language)}</div>
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}> <div className="text-xs sm:text-sm font-bold mono" style={{ color: '#EAECEF' }}>
{initialBalance.toFixed(2)} USDT {initialBalance.toFixed(2)} USDT
</div> </div>
</div> </div>
<div className="p-2 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}> <div className="p-2 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('currentEquity', language)}</div> <div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('currentEquity', language)}</div>
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}> <div className="text-xs sm:text-sm font-bold mono" style={{ color: '#EAECEF' }}>
{currentValue.raw_equity.toFixed(2)} USDT {currentValue.raw_equity.toFixed(2)} USDT
</div> </div>
</div> </div>
<div className="p-2 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}> <div className="p-2 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('historicalCycles', language)}</div> <div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('historicalCycles', language)}</div>
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}>{history.length} {t('cycles', language)}</div> <div className="text-xs sm:text-sm font-bold mono" style={{ color: '#EAECEF' }}>{validHistory.length} {t('cycles', language)}</div>
</div> </div>
<div className="p-2 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}> <div className="p-2 rounded transition-all hover:bg-opacity-50" style={{ background: 'rgba(240, 185, 11, 0.05)' }}>
<div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('displayRange', language)}</div> <div className="text-xs mb-1 uppercase tracking-wider" style={{ color: '#848E9C' }}>{t('displayRange', language)}</div>
<div className="text-sm font-bold mono" style={{ color: '#EAECEF' }}> <div className="text-xs sm:text-sm font-bold mono" style={{ color: '#EAECEF' }}>
{history.length > MAX_DISPLAY_POINTS {validHistory.length > MAX_DISPLAY_POINTS
? `${t('recent', language)} ${MAX_DISPLAY_POINTS}` ? `${t('recent', language)} ${MAX_DISPLAY_POINTS}`
: t('allData', language) : t('allData', language)
} }
+10
View File
@@ -161,4 +161,14 @@ export const api = {
if (!res.ok) throw new Error('获取历史数据失败'); if (!res.ok) throw new Error('获取历史数据失败');
return res.json(); return res.json();
}, },
// 获取AI学习表现分析(支持trader_id
async getPerformance(traderId?: string): Promise<any> {
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();
},
}; };
+1
View File
@@ -64,6 +64,7 @@ export interface AccountSnapshot {
export interface DecisionRecord { export interface DecisionRecord {
timestamp: string; timestamp: string;
cycle_number: number; cycle_number: number;
input_prompt: string;
cot_trace: string; cot_trace: string;
decision_json: string; decision_json: string;
account_state: AccountSnapshot; account_state: AccountSnapshot;
+1
View File
@@ -56,6 +56,7 @@ export interface DecisionAction {
export interface DecisionRecord { export interface DecisionRecord {
timestamp: string; timestamp: string;
cycle_number: number; cycle_number: number;
input_prompt: string;
cot_trace: string; cot_trace: string;
decision_json: string; decision_json: string;
account_state: { account_state: {
+1
View File
@@ -4,6 +4,7 @@ import react from '@vitejs/plugin-react'
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: { server: {
host: '0.0.0.0',
port: 3000, port: 3000,
proxy: { proxy: {
'/api': { '/api': {
+25
View File
@@ -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)