mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 01:48:22 +08:00
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:
+3
-2
@@ -40,8 +40,9 @@ coin_pool_cache/
|
||||
# Config files (should be mounted)
|
||||
config.json
|
||||
|
||||
# Web directory (has its own Dockerfile)
|
||||
web/
|
||||
# Web build artifacts (but include source for multi-stage build)
|
||||
web/node_modules/
|
||||
web/dist/
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
|
||||
@@ -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
|
||||
@@ -21,6 +21,7 @@ Thumbs.db
|
||||
*.log
|
||||
*.tmp
|
||||
*.bak
|
||||
*.backup
|
||||
|
||||
# 环境变量
|
||||
.env
|
||||
|
||||
+205
@@ -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
@@ -15,22 +15,33 @@ Before you begin, ensure your system has:
|
||||
Download and install [Docker Desktop](https://www.docker.com/products/docker-desktop/)
|
||||
|
||||
#### Linux (Ubuntu/Debian)
|
||||
|
||||
> #### Docker Compose Version Notes
|
||||
>
|
||||
> **New User Recommendation:**
|
||||
> - **Use Docker Desktop**: Automatically includes latest Docker Compose, no separate installation needed
|
||||
> - Simple installation, one-click setup, provides GUI management
|
||||
> - Supports macOS, Windows, and some Linux distributions
|
||||
>
|
||||
> **Upgrading User Note:**
|
||||
> - **Deprecating standalone docker-compose**: No longer recommended to download the independent Docker Compose binary
|
||||
> - **Use built-in version**: Docker 20.10+ includes `docker compose` command (with space)
|
||||
> - If still using old `docker-compose`, please upgrade to new syntax
|
||||
|
||||
*Recommended: Use Docker Desktop (if available) or Docker CE with built-in Compose*
|
||||
|
||||
```bash
|
||||
# Install Docker
|
||||
# Install Docker (includes compose)
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sudo sh get-docker.sh
|
||||
|
||||
# Install Docker Compose
|
||||
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
|
||||
# Add current user to docker group
|
||||
# Add user to docker group
|
||||
sudo usermod -aG docker $USER
|
||||
newgrp docker
|
||||
|
||||
# Verify installation
|
||||
# Verify installation (new command)
|
||||
docker --version
|
||||
docker-compose --version
|
||||
docker compose --version # Docker 24+ includes this, no separate installation needed
|
||||
```
|
||||
|
||||
## 🚀 Quick Start (3 Steps)
|
||||
@@ -69,10 +80,10 @@ nano config.json # or use any other editor
|
||||
|
||||
```bash
|
||||
# Build and start all services (first run)
|
||||
docker-compose up -d --build
|
||||
docker compose up -d --build
|
||||
|
||||
# Subsequent starts (without rebuilding)
|
||||
docker-compose up -d
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**Startup options:**
|
||||
@@ -91,49 +102,49 @@ Once deployed, open your browser and visit:
|
||||
### View Running Status
|
||||
```bash
|
||||
# View all container status
|
||||
docker-compose ps
|
||||
docker compose ps
|
||||
|
||||
# View service health status
|
||||
docker-compose ps --format json | jq
|
||||
docker compose ps --format json | jq
|
||||
```
|
||||
|
||||
### View Logs
|
||||
```bash
|
||||
# View all service logs
|
||||
docker-compose logs -f
|
||||
docker compose logs -f
|
||||
|
||||
# View backend logs only
|
||||
docker-compose logs -f backend
|
||||
docker compose logs -f backend
|
||||
|
||||
# View frontend logs only
|
||||
docker-compose logs -f frontend
|
||||
docker compose logs -f frontend
|
||||
|
||||
# View last 100 lines
|
||||
docker-compose logs --tail=100
|
||||
docker compose logs --tail=100
|
||||
```
|
||||
|
||||
### Stop Services
|
||||
```bash
|
||||
# Stop all services (keep data)
|
||||
docker-compose stop
|
||||
docker compose stop
|
||||
|
||||
# Stop and remove containers (keep data)
|
||||
docker-compose down
|
||||
docker compose down
|
||||
|
||||
# Stop and remove containers and volumes (clear all data)
|
||||
docker-compose down -v
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
### Restart Services
|
||||
```bash
|
||||
# Restart all services
|
||||
docker-compose restart
|
||||
docker compose restart
|
||||
|
||||
# Restart backend only
|
||||
docker-compose restart backend
|
||||
docker compose restart backend
|
||||
|
||||
# Restart frontend only
|
||||
docker-compose restart frontend
|
||||
docker compose restart frontend
|
||||
```
|
||||
|
||||
### Update Services
|
||||
@@ -142,7 +153,7 @@ docker-compose restart frontend
|
||||
git pull
|
||||
|
||||
# Rebuild and restart
|
||||
docker-compose up -d --build
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
## 🔧 Advanced Configuration
|
||||
@@ -226,14 +237,14 @@ tar -xzf backup_20241029.tar.gz
|
||||
|
||||
```bash
|
||||
# View detailed error messages
|
||||
docker-compose logs backend
|
||||
docker-compose logs frontend
|
||||
docker compose logs backend
|
||||
docker compose logs frontend
|
||||
|
||||
# Check container status
|
||||
docker-compose ps -a
|
||||
docker compose ps -a
|
||||
|
||||
# Rebuild (clear cache)
|
||||
docker-compose build --no-cache
|
||||
docker compose build --no-cache
|
||||
```
|
||||
|
||||
### Port Already in Use
|
||||
@@ -273,10 +284,10 @@ curl http://localhost:3000/health
|
||||
|
||||
```bash
|
||||
# Check network connectivity
|
||||
docker-compose exec frontend ping backend
|
||||
docker compose exec frontend ping backend
|
||||
|
||||
# Check if backend service is running
|
||||
docker-compose exec frontend wget -O- http://backend:8080/health
|
||||
docker compose exec frontend wget -O- http://backend:8080/health
|
||||
```
|
||||
|
||||
### Clean Docker Resources
|
||||
@@ -321,8 +332,8 @@ docker system prune -a --volumes
|
||||
|
||||
4. **Regularly update images**
|
||||
```bash
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## 🌐 Production Deployment
|
||||
@@ -391,7 +402,7 @@ logging:
|
||||
max-file: "3"
|
||||
|
||||
# View log statistics
|
||||
docker-compose logs --timestamps | wc -l
|
||||
docker compose logs --timestamps | wc -l
|
||||
```
|
||||
|
||||
### Monitoring Tool Integration
|
||||
@@ -424,28 +435,28 @@ services:
|
||||
|
||||
```bash
|
||||
# Start
|
||||
docker-compose up -d --build # Build and start
|
||||
docker-compose up -d # Start (without rebuilding)
|
||||
docker compose up -d --build # Build and start
|
||||
docker compose up -d # Start (without rebuilding)
|
||||
|
||||
# Stop
|
||||
docker-compose stop # Stop services
|
||||
docker-compose down # Stop and remove containers
|
||||
docker-compose down -v # Stop and remove containers and data
|
||||
docker compose stop # Stop services
|
||||
docker compose down # Stop and remove containers
|
||||
docker compose down -v # Stop and remove containers and data
|
||||
|
||||
# View
|
||||
docker-compose ps # View status
|
||||
docker-compose logs -f # View logs
|
||||
docker-compose top # View processes
|
||||
docker compose ps # View status
|
||||
docker compose logs -f # View logs
|
||||
docker compose top # View processes
|
||||
|
||||
# Restart
|
||||
docker-compose restart # Restart all services
|
||||
docker-compose restart backend # Restart backend
|
||||
docker compose restart # Restart all services
|
||||
docker compose restart backend # Restart backend
|
||||
|
||||
# Update
|
||||
git pull && docker-compose up -d --build
|
||||
git pull && docker compose up -d --build
|
||||
|
||||
# Clean
|
||||
docker-compose down -v # Clear all data
|
||||
docker compose down -v # Clear all data
|
||||
docker system prune -a # Clean Docker resources
|
||||
```
|
||||
|
||||
|
||||
+60
-44
@@ -11,26 +11,42 @@
|
||||
|
||||
### 安装 Docker
|
||||
|
||||
> #### 提示:Docker Compose 版本说明
|
||||
>
|
||||
> **新用户建议**:
|
||||
> - **推荐使用 Docker Desktop**:自动包含最新 Docker Compose,无需单独安装
|
||||
> - 安装简单,一键搞定,提供图形界面管理
|
||||
> - 支持 macOS、Windows、部分 Linux 发行版
|
||||
>
|
||||
> **旧用户提醒**:
|
||||
> - **弃用独立 docker-compose**:不再推荐下载独立的 Docker Compose 二进制文件
|
||||
> - **使用内置版**:Docker 20.10+ 自带 `docker compose` 命令(注意是空格)
|
||||
> - 如果还在使用旧的 `docker-compose`,请升级到新语法
|
||||
|
||||
#### macOS / Windows
|
||||
下载并安装 [Docker Desktop](https://www.docker.com/products/docker-desktop/)
|
||||
|
||||
#### Linux (Ubuntu/Debian)
|
||||
**安装后验证:**
|
||||
```bash
|
||||
# 安装 Docker
|
||||
docker --version
|
||||
docker compose --version # 注意:使用空格,不再是连字符
|
||||
```
|
||||
|
||||
#### Linux (Ubuntu/Debian)
|
||||
**推荐方式:使用 Docker Desktop(如果可用)或 Docker CE**
|
||||
|
||||
```bash
|
||||
# 安装 Docker (自动包含 compose)
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sudo sh get-docker.sh
|
||||
|
||||
# 安装 Docker Compose
|
||||
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
|
||||
sudo chmod +x /usr/local/bin/docker-compose
|
||||
|
||||
# 将当前用户加入 docker 组
|
||||
sudo usermod -aG docker $USER
|
||||
newgrp docker
|
||||
|
||||
# 验证安装
|
||||
# 验证安装(新命令)
|
||||
docker --version
|
||||
docker-compose --version
|
||||
docker compose --version # Docker 24+ 自带,无需单独安装
|
||||
```
|
||||
|
||||
## 🚀 快速开始(3步完成部署)
|
||||
@@ -69,10 +85,10 @@ nano config.json # 或使用其他编辑器
|
||||
|
||||
```bash
|
||||
# 构建并启动所有服务(首次运行)
|
||||
docker-compose up -d --build
|
||||
docker compose up -d --build
|
||||
|
||||
# 后续启动(不重新构建)
|
||||
docker-compose up -d
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**启动过程说明:**
|
||||
@@ -91,49 +107,49 @@ docker-compose up -d
|
||||
### 查看运行状态
|
||||
```bash
|
||||
# 查看所有容器状态
|
||||
docker-compose ps
|
||||
docker compose ps
|
||||
|
||||
# 查看服务健康状态
|
||||
docker-compose ps --format json | jq
|
||||
docker compose ps --format json | jq
|
||||
```
|
||||
|
||||
### 查看日志
|
||||
```bash
|
||||
# 查看所有服务日志
|
||||
docker-compose logs -f
|
||||
docker compose logs -f
|
||||
|
||||
# 只查看后端日志
|
||||
docker-compose logs -f backend
|
||||
docker compose logs -f backend
|
||||
|
||||
# 只查看前端日志
|
||||
docker-compose logs -f frontend
|
||||
docker compose logs -f frontend
|
||||
|
||||
# 查看最近 100 行日志
|
||||
docker-compose logs --tail=100
|
||||
docker compose logs --tail=100
|
||||
```
|
||||
|
||||
### 停止服务
|
||||
```bash
|
||||
# 停止所有服务(保留数据)
|
||||
docker-compose stop
|
||||
docker compose stop
|
||||
|
||||
# 停止并删除容器(保留数据)
|
||||
docker-compose down
|
||||
docker compose down
|
||||
|
||||
# 停止并删除容器和卷(清除所有数据)
|
||||
docker-compose down -v
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
### 重启服务
|
||||
```bash
|
||||
# 重启所有服务
|
||||
docker-compose restart
|
||||
docker compose restart
|
||||
|
||||
# 只重启后端
|
||||
docker-compose restart backend
|
||||
docker compose restart backend
|
||||
|
||||
# 只重启前端
|
||||
docker-compose restart frontend
|
||||
docker compose restart frontend
|
||||
```
|
||||
|
||||
### 更新服务
|
||||
@@ -142,7 +158,7 @@ docker-compose restart frontend
|
||||
git pull
|
||||
|
||||
# 重新构建并重启
|
||||
docker-compose up -d --build
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
## 🔧 高级配置
|
||||
@@ -226,14 +242,14 @@ tar -xzf backup_20241029.tar.gz
|
||||
|
||||
```bash
|
||||
# 查看详细错误信息
|
||||
docker-compose logs backend
|
||||
docker-compose logs frontend
|
||||
docker compose logs backend
|
||||
docker compose logs frontend
|
||||
|
||||
# 检查容器状态
|
||||
docker-compose ps -a
|
||||
docker compose ps -a
|
||||
|
||||
# 重新构建(清除缓存)
|
||||
docker-compose build --no-cache
|
||||
docker compose build --no-cache
|
||||
```
|
||||
|
||||
### 端口被占用
|
||||
@@ -273,10 +289,10 @@ curl http://localhost:3000/health
|
||||
|
||||
```bash
|
||||
# 检查网络连接
|
||||
docker-compose exec frontend ping backend
|
||||
docker compose exec frontend ping backend
|
||||
|
||||
# 检查后端服务是否正常
|
||||
docker-compose exec frontend wget -O- http://backend:8080/health
|
||||
docker compose exec frontend wget -O- http://backend:8080/health
|
||||
```
|
||||
|
||||
### 清理 Docker 资源
|
||||
@@ -321,8 +337,8 @@ docker system prune -a --volumes
|
||||
|
||||
4. **定期更新镜像**
|
||||
```bash
|
||||
docker-compose pull
|
||||
docker-compose up -d
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## 🌐 生产环境部署
|
||||
@@ -391,7 +407,7 @@ logging:
|
||||
max-file: "3"
|
||||
|
||||
# 查看日志统计
|
||||
docker-compose logs --timestamps | wc -l
|
||||
docker compose logs --timestamps | wc -l
|
||||
```
|
||||
|
||||
### 监控工具集成
|
||||
@@ -424,28 +440,28 @@ services:
|
||||
|
||||
```bash
|
||||
# 启动
|
||||
docker-compose up -d --build # 构建并启动
|
||||
docker-compose up -d # 启动(不重新构建)
|
||||
docker compose up -d --build # 构建并启动
|
||||
docker compose up -d # 启动(不重新构建)
|
||||
|
||||
# 停止
|
||||
docker-compose stop # 停止服务
|
||||
docker-compose down # 停止并删除容器
|
||||
docker-compose down -v # 停止并删除容器和数据
|
||||
docker compose stop # 停止服务
|
||||
docker compose down # 停止并删除容器
|
||||
docker compose down -v # 停止并删除容器和数据
|
||||
|
||||
# 查看
|
||||
docker-compose ps # 查看状态
|
||||
docker-compose logs -f # 查看日志
|
||||
docker-compose top # 查看进程
|
||||
docker compose ps # 查看状态
|
||||
docker compose logs -f # 查看日志
|
||||
docker compose top # 查看进程
|
||||
|
||||
# 重启
|
||||
docker-compose restart # 重启所有服务
|
||||
docker-compose restart backend # 重启后端
|
||||
docker compose restart # 重启所有服务
|
||||
docker compose restart backend # 重启后端
|
||||
|
||||
# 更新
|
||||
git pull && docker-compose up -d --build
|
||||
git pull && docker compose up -d --build
|
||||
|
||||
# 清理
|
||||
docker-compose down -v # 清除所有数据
|
||||
docker compose down -v # 清除所有数据
|
||||
docker system prune -a # 清理 Docker 资源
|
||||
```
|
||||
|
||||
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
# 构建阶段
|
||||
FROM golang:1.25-alpine AS builder
|
||||
|
||||
# 安装必要的构建工具
|
||||
RUN apk add --no-cache git gcc musl-dev
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制 go mod 文件
|
||||
COPY go.mod go.sum ./
|
||||
|
||||
# 下载依赖
|
||||
RUN go mod download
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 构建应用
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o nofx .
|
||||
|
||||
# 运行阶段
|
||||
FROM alpine:latest
|
||||
|
||||
# 安装 ca-certificates(HTTPS 请求需要)
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
|
||||
# 设置时区为上海
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
# 创建非 root 用户
|
||||
RUN addgroup -g 1000 nofx && \
|
||||
adduser -D -u 1000 -G nofx nofx
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 从构建阶段复制二进制文件
|
||||
COPY --from=builder /app/nofx .
|
||||
|
||||
# 复制配置文件示例
|
||||
COPY config.json.example ./config.json.example
|
||||
|
||||
# 创建必要的目录
|
||||
RUN mkdir -p decision_logs coin_pool_cache && \
|
||||
chown -R nofx:nofx /app
|
||||
|
||||
# 切换到非 root 用户
|
||||
USER nofx
|
||||
|
||||
# 暴露 API 端口
|
||||
EXPOSE 8080
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
|
||||
|
||||
# 启动应用
|
||||
CMD ["./nofx"]
|
||||
@@ -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
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
---
|
||||
|
||||
A modern automated crypto futures trading platform powered by **DeepSeek/Qwen AI**, supporting **Binance and Hyperliquid exchanges**. Create and manage multiple AI traders with dynamic configuration through a web interface. Features comprehensive market analysis, AI decision-making, and professional monitoring dashboard.
|
||||
A modern automated crypto futures trading platform powered by **DeepSeek/Qwen AI**, supporting **Binance, Hyperliquid, and Aster DEX exchanges**. Create and manage multiple AI traders with dynamic configuration through a web interface. Features comprehensive market analysis, AI decision-making, **multi-AI model live trading competition**, **self-learning mechanism**, and professional monitoring dashboard.
|
||||
|
||||
> ⚠️ **Risk Warning**: This system is experimental. AI auto-trading carries significant risks. Strongly recommended for learning/research purposes or testing with small amounts only!
|
||||
|
||||
@@ -26,7 +26,15 @@ Join our Telegram developer community to discuss, share ideas, and get support:
|
||||
|
||||
### 🚀 Complete System Transformation - Web-Based Configuration!
|
||||
|
||||
NOFX has been **completely transformed** from a static config-based system to a **dynamic web-based trading platform**!
|
||||
NOFX has been **completely transformed** from a static config-based system to a **dynamic web-based trading platform** with **multi-exchange support**!
|
||||
|
||||
#### **Multi-Exchange Support**
|
||||
|
||||
NOFX now supports **three major exchanges**: Binance, Hyperliquid, and Aster DEX!
|
||||
|
||||
#### **Hyperliquid Exchange**
|
||||
|
||||
A high-performance decentralized perpetual futures exchange!
|
||||
|
||||
**Major Changes:**
|
||||
- ✅ **Web-Based Configuration**: Create and manage AI traders through a modern web interface
|
||||
@@ -50,6 +58,42 @@ NOFX has been **completely transformed** from a static config-based system to a
|
||||
|
||||
See [Quick Start](#-quick-start) for the new setup process!
|
||||
|
||||
#### **Aster DEX Exchange** (NEW! v2.0.2)
|
||||
|
||||
A Binance-compatible decentralized perpetual futures exchange!
|
||||
|
||||
**Key Features:**
|
||||
- ✅ Binance-style API (easy migration from Binance)
|
||||
- ✅ Web3 wallet authentication (secure and decentralized)
|
||||
- ✅ Full trading support with automatic precision handling
|
||||
- ✅ Lower trading fees than CEX
|
||||
- ✅ EVM-compatible (Ethereum, BSC, Polygon, etc.)
|
||||
|
||||
**Why Aster?**
|
||||
- 🎯 **Binance-compatible API** - minimal code changes required
|
||||
- 🔐 **API Wallet System** - separate trading wallet for security
|
||||
- 💰 **Competitive fees** - lower than most centralized exchanges
|
||||
- 🌐 **Multi-chain support** - trade on your preferred EVM chain
|
||||
|
||||
**Quick Start:**
|
||||
1. Visit [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
|
||||
2. Connect your main wallet and create an API wallet
|
||||
3. Copy the API Signer address and Private Key
|
||||
4. Set `"exchange": "aster"` in config.json
|
||||
5. Add `"aster_user"`, `"aster_signer"`, and `"aster_private_key"`
|
||||
|
||||
---
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
### 🏆 Competition Mode - Real-time AI Battle
|
||||

|
||||
*Multi-AI leaderboard with real-time performance comparison charts showing Qwen vs DeepSeek live trading battle*
|
||||
|
||||
### 📊 Trader Details - Complete Trading Dashboard
|
||||

|
||||
*Professional trading interface with equity curves, live positions, and AI decision logs with expandable input prompts & chain-of-thought reasoning*
|
||||
|
||||
---
|
||||
|
||||
## ✨ Core Features
|
||||
@@ -81,7 +125,11 @@ See [Quick Start](#-quick-start) for the new setup process!
|
||||
- **Per-Coin Position Limit**:
|
||||
- Altcoins ≤ 1.5x account equity
|
||||
- BTC/ETH ≤ 10x account equity
|
||||
- **Fixed Leverage**: Altcoins 20x | BTC/ETH 50x
|
||||
- **Configurable Leverage** (v2.0.3+):
|
||||
- Set maximum leverage in config.json
|
||||
- Default: 5x for all coins (safe for subaccounts)
|
||||
- Main accounts can increase: Altcoins up to 20x, BTC/ETH up to 50x
|
||||
- ⚠️ Binance subaccounts restricted to ≤5x leverage
|
||||
- **Margin Management**: Total usage ≤90%, AI autonomous decision on usage rate
|
||||
- **Risk-Reward Ratio**: Mandatory ≥1:2 (stop-loss:take-profit)
|
||||
- **Prevent Position Stacking**: No duplicate opening of same coin/direction
|
||||
@@ -209,8 +257,14 @@ Docker automatically handles all dependencies (Go, Node.js, TA-Lib, SQLite) and
|
||||
chmod +x start.sh
|
||||
./start.sh start --build
|
||||
|
||||
# Option 2: Use docker-compose directly
|
||||
docker-compose up -d --build
|
||||
> #### Docker Compose Version Notes
|
||||
>
|
||||
> **This project uses Docker Compose V2 syntax (with spaces)**
|
||||
>
|
||||
> If you have the older standalone `docker-compose` installed, please upgrade to Docker Desktop or Docker 20.10+
|
||||
|
||||
# Option 2: Use docker compose directly
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
#### Step 2: Access Web Interface
|
||||
@@ -439,6 +493,71 @@ Open your browser and visit: **🌐 http://localhost:3000**
|
||||
|
||||
---
|
||||
|
||||
#### 🔶 Alternative: Using Aster DEX Exchange
|
||||
|
||||
**NOFX also supports Aster DEX** - a Binance-compatible decentralized perpetual futures exchange!
|
||||
|
||||
**Why Choose Aster?**
|
||||
- 🎯 Binance-compatible API (easy migration)
|
||||
- 🔐 API Wallet security system
|
||||
- 💰 Lower trading fees
|
||||
- 🌐 Multi-chain support (ETH, BSC, Polygon)
|
||||
- 🌍 No KYC required
|
||||
|
||||
**Step 1**: Create Aster API Wallet
|
||||
|
||||
1. Visit [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
|
||||
2. Connect your main wallet (MetaMask, WalletConnect, etc.)
|
||||
3. Click "Create API Wallet"
|
||||
4. **Save these 3 items immediately:**
|
||||
- Main Wallet address (User)
|
||||
- API Wallet address (Signer)
|
||||
- API Wallet Private Key (⚠️ shown only once!)
|
||||
|
||||
**Step 2**: Configure `config.json` for Aster
|
||||
|
||||
```json
|
||||
{
|
||||
"traders": [
|
||||
{
|
||||
"id": "aster_deepseek",
|
||||
"name": "Aster DeepSeek Trader",
|
||||
"ai_model": "deepseek",
|
||||
"exchange": "aster",
|
||||
|
||||
"aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e",
|
||||
"aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0",
|
||||
"aster_private_key": "4fd0a42218f3eae43a6ce26d22544e986139a01e5b34a62db53757ffca81bae1",
|
||||
|
||||
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
||||
"initial_balance": 1000.0,
|
||||
"scan_interval_minutes": 3
|
||||
}
|
||||
],
|
||||
"use_default_coins": true,
|
||||
"api_server_port": 8080,
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5,
|
||||
"altcoin_leverage": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Configuration Fields:**
|
||||
- `"exchange": "aster"` - Set exchange to Aster
|
||||
- `aster_user` - Your main wallet address
|
||||
- `aster_signer` - API wallet address (from Step 1)
|
||||
- `aster_private_key` - API wallet private key (without `0x` prefix)
|
||||
|
||||
**📖 For detailed setup instructions, see**: [Aster Integration Guide](ASTER_INTEGRATION.md)
|
||||
|
||||
**⚠️ Security Notes**:
|
||||
- API wallet is separate from your main wallet (extra security layer)
|
||||
- Never share your API private key
|
||||
- You can revoke API wallet access anytime at [asterdex.com](https://www.asterdex.com/en/api-wallet)
|
||||
|
||||
---
|
||||
|
||||
#### ⚔️ Expert Mode: Multi-Trader Competition
|
||||
|
||||
For running multiple AI traders competing against each other:
|
||||
@@ -499,6 +618,9 @@ For running multiple AI traders competing against each other:
|
||||
| `qwen_key` | Qwen API key | `"sk-xxx"` | If using Qwen |
|
||||
| `initial_balance` | Starting balance for P/L calculation | `1000.0` | ✅ Yes |
|
||||
| `scan_interval_minutes` | How often to make decisions | `3` (3-5 recommended) | ✅ Yes |
|
||||
| **`leverage`** | **Leverage configuration (v2.0.3+)** | See below | ✅ Yes |
|
||||
| `btc_eth_leverage` | Maximum leverage for BTC/ETH<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) |
|
||||
| `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 |
|
||||
@@ -509,6 +631,63 @@ For running multiple AI traders competing against each other:
|
||||
|
||||
---
|
||||
|
||||
#### ⚙️ Leverage Configuration (v2.0.3+)
|
||||
|
||||
**What is leverage configuration?**
|
||||
|
||||
The leverage settings control the maximum leverage the AI can use for each trade. This is crucial for risk management, especially for Binance subaccounts which have leverage restrictions.
|
||||
|
||||
**Configuration format:**
|
||||
|
||||
```json
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5, // Maximum leverage for BTC and ETH
|
||||
"altcoin_leverage": 5 // Maximum leverage for all other coins
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ Important: Binance Subaccount Restrictions**
|
||||
|
||||
- **Subaccounts**: Limited to **≤5x leverage** by Binance
|
||||
- **Main accounts**: Can use up to 20x (altcoins) or 50x (BTC/ETH)
|
||||
- If you're using a subaccount and set leverage >5x, trades will **fail** with error: `Subaccounts are restricted from using leverage greater than 5x`
|
||||
|
||||
**Recommended settings:**
|
||||
|
||||
| Account Type | BTC/ETH Leverage | Altcoin Leverage | Risk Level |
|
||||
|-------------|------------------|------------------|------------|
|
||||
| **Subaccount** | `5` | `5` | ✅ Safe (default) |
|
||||
| **Main (Conservative)** | `10` | `10` | 🟡 Medium |
|
||||
| **Main (Aggressive)** | `20` | `15` | 🔴 High |
|
||||
| **Main (Maximum)** | `50` | `20` | 🔴🔴 Very High |
|
||||
|
||||
**Examples:**
|
||||
|
||||
**Safe configuration (subaccount or conservative):**
|
||||
```json
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5,
|
||||
"altcoin_leverage": 5
|
||||
}
|
||||
```
|
||||
|
||||
**Aggressive configuration (main account only):**
|
||||
```json
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 20,
|
||||
"altcoin_leverage": 15
|
||||
}
|
||||
```
|
||||
|
||||
**How AI uses leverage:**
|
||||
|
||||
- AI can choose **any leverage from 1x up to your configured maximum**
|
||||
- For example, with `altcoin_leverage: 20`, AI might decide to use 5x, 10x, or 20x based on market conditions
|
||||
- The configuration sets the **upper limit**, not a fixed value
|
||||
- AI considers volatility, risk-reward ratio, and account balance when choosing leverage
|
||||
|
||||
---
|
||||
|
||||
#### ⚠️ Important: `use_default_coins` Field
|
||||
|
||||
**Smart Default Behavior (v2.0.2+):**
|
||||
@@ -1096,6 +1275,19 @@ This version fixes **critical calculation errors** in the historical trade recor
|
||||
|
||||
**Recommendation**: If you were running the system before this update, your historical statistics were inaccurate. After updating to v2.0.2, new trades will be calculated correctly.
|
||||
|
||||
### v2.0.2 (2025-10-29)
|
||||
|
||||
**Bug Fixes:**
|
||||
- ✅ Fixed Aster exchange precision error (code -1111: "Precision is over the maximum defined for this asset")
|
||||
- ✅ Improved price and quantity formatting to match exchange precision requirements
|
||||
- ✅ Added detailed precision processing logs for debugging
|
||||
- ✅ Enhanced all order functions (OpenLong, OpenShort, CloseLong, CloseShort, SetStopLoss, SetTakeProfit) with proper precision handling
|
||||
|
||||
**Technical Details:**
|
||||
- Added `formatFloatWithPrecision` function to convert float64 to strings with correct precision
|
||||
- Price and quantity parameters are now formatted according to exchange's `pricePrecision` and `quantityPrecision` specifications
|
||||
- Trailing zeros are removed from formatted values to optimize API requests
|
||||
|
||||
### v2.0.1 (2025-10-29)
|
||||
|
||||
**Bug Fixes:**
|
||||
|
||||
+248
-4
@@ -9,7 +9,7 @@
|
||||
|
||||
---
|
||||
|
||||
Автоматизированная система торговли фьючерсами Binance на базе **DeepSeek/Qwen AI**, поддерживающая **конкуренцию нескольких AI-моделей в реальной торговле**, с полным анализом рынка, принятием решений AI, **механизмом самообучения** и профессиональным веб-интерфейсом мониторинга.
|
||||
Автоматизированная система торговли криптовалютными фьючерсами на базе **DeepSeek/Qwen AI**, поддерживающая **Binance, Hyperliquid и Aster DEX биржи**, **конкуренцию нескольких AI-моделей в реальной торговле**, с полным анализом рынка, принятием решений AI, **механизмом самообучения** и профессиональным веб-интерфейсом мониторинга.
|
||||
|
||||
> ⚠️ **Предупреждение о рисках**: Эта система экспериментальная. Автоматическая торговля с AI несет значительные риски. Настоятельно рекомендуется использовать только для обучения/исследований или тестирования с небольшими суммами!
|
||||
|
||||
@@ -21,6 +21,75 @@
|
||||
|
||||
---
|
||||
|
||||
## 🆕 Последние обновления
|
||||
|
||||
### 🚀 Поддержка нескольких бирж!
|
||||
|
||||
NOFX теперь поддерживает **три основные биржи**: Binance, Hyperliquid и Aster DEX!
|
||||
|
||||
#### **Биржа Hyperliquid**
|
||||
|
||||
Высокопроизводительная децентрализованная биржа бессрочных фьючерсов!
|
||||
|
||||
**Ключевые особенности:**
|
||||
- ✅ Полная поддержка торговли (лонг/шорт, плечо, стоп-лосс/тейк-профит)
|
||||
- ✅ Автоматическая обработка точности (размер и цена ордера)
|
||||
- ✅ Единый интерфейс трейдера (бесшовное переключение бирж)
|
||||
- ✅ Поддержка мейннета и тестнета
|
||||
- ✅ Не нужны API ключи - только приватный ключ Ethereum
|
||||
|
||||
**Почему Hyperliquid?**
|
||||
- 🔥 Более низкие комиссии чем на централизованных биржах
|
||||
- 🔒 Без хранения - вы контролируете свои средства
|
||||
- ⚡ Быстрое исполнение с расчетом на цепи
|
||||
- 🌍 Не нужна KYC
|
||||
|
||||
**Быстрый старт:**
|
||||
1. Получите приватный ключ MetaMask (удалите префикс `0x`)
|
||||
2. Установите `"exchange": "hyperliquid"` в config.json
|
||||
3. Добавьте `"hyperliquid_private_key": "your_key"`
|
||||
4. Начните торговать!
|
||||
|
||||
См. [Руководство по конфигурации](#-альтернатива-использование-биржи-hyperliquid).
|
||||
|
||||
#### **Биржа Aster DEX** (НОВОЕ! v2.0.2)
|
||||
|
||||
Децентрализованная биржа бессрочных фьючерсов, совместимая с Binance!
|
||||
|
||||
**Ключевые особенности:**
|
||||
- ✅ API в стиле Binance (легкая миграция с Binance)
|
||||
- ✅ Web3 аутентификация кошелька (безопасно и децентрализованно)
|
||||
- ✅ Полная поддержка торговли с автоматической обработкой точности
|
||||
- ✅ Более низкие комиссии за торговлю чем CEX
|
||||
- ✅ Совместимость с EVM (Ethereum, BSC, Polygon и т.д.)
|
||||
|
||||
**Почему Aster?**
|
||||
- 🎯 **API совместимый с Binance** - нужны минимальные изменения кода
|
||||
- 🔐 **Система API кошелька** - отдельный торговый кошелек для безопасности
|
||||
- 💰 **Конкурентные комиссии** - ниже чем большинство централизованных бирж
|
||||
- 🌐 **Поддержка нескольких цепей** - торгуйте на вашей любимой EVM цепи
|
||||
|
||||
**Быстрый старт:**
|
||||
1. Посетите [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
|
||||
2. Подключите основной кошелек и создайте API кошелек
|
||||
3. Скопируйте адрес API Signer и приватный ключ
|
||||
4. Установите `"exchange": "aster"` в config.json
|
||||
5. Добавьте `"aster_user"`, `"aster_signer"` и `"aster_private_key"`
|
||||
|
||||
---
|
||||
|
||||
## 📸 Скриншоты
|
||||
|
||||
### 🏆 Режим конкуренции - Битва AI в реальном времени
|
||||

|
||||
*Лидерборд с несколькими AI и графики сравнения производительности в реальном времени показывают битву Qwen против DeepSeek*
|
||||
|
||||
### 📊 Детали трейдера - Полная торговая панель
|
||||

|
||||
*Профессиональный торговый интерфейс с кривыми капитала, живыми позициями и логами решений AI с раскрываемыми входными промптами и цепочкой рассуждений*
|
||||
|
||||
---
|
||||
|
||||
## ✨ Основные возможности
|
||||
|
||||
### 🏆 Режим конкуренции нескольких AI
|
||||
@@ -50,7 +119,11 @@
|
||||
- **Лимит позиции по монете**:
|
||||
- Альткоины ≤ 1.5x капитал счета
|
||||
- BTC/ETH ≤ 10x капитал счета
|
||||
- **Фиксированное плечо**: Альткоины 20x | BTC/ETH 50x
|
||||
- **Настраиваемое плечо** (v2.0.3+):
|
||||
- Установите максимальное плечо в config.json
|
||||
- По умолчанию: 5x для всех монет (безопасно для субаккаунтов)
|
||||
- Основные аккаунты могут увеличить: Альткоины до 20x, BTC/ETH до 50x
|
||||
- ⚠️ Субаккаунты Binance ограничены ≤5x плечом
|
||||
- **Управление маржой**: Общее использование ≤90%, AI принимает автономные решения
|
||||
- **Соотношение риск/доход**: Обязательное ≥1:2 (стоп-лосс:тейк-профит)
|
||||
- **Предотвращение накопления позиций**: Запрет дублирования открытия той же монеты/направления
|
||||
@@ -123,8 +196,10 @@ nano config.json # или используйте любой редактор
|
||||
chmod +x start.sh
|
||||
./start.sh start --build
|
||||
|
||||
# Вариант 2: Используйте docker-compose напрямую
|
||||
docker-compose up -d --build
|
||||
# Вариант 2: Используйте docker compose напрямую
|
||||
# Этот проект использует синтаксис Docker Compose V2 (с пробелами)
|
||||
# Если у вас установлена старая версия `docker-compose`, обновитесь до Docker Desktop или Docker 20.10+
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
#### Шаг 3: Доступ к панели
|
||||
@@ -268,6 +343,10 @@ cp config.json.example config.json
|
||||
"scan_interval_minutes": 3
|
||||
}
|
||||
],
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5,
|
||||
"altcoin_leverage": 5
|
||||
},
|
||||
"use_default_coins": true,
|
||||
"coin_pool_api_url": "",
|
||||
"oi_top_api_url": "",
|
||||
@@ -300,6 +379,111 @@ cp config.json.example config.json
|
||||
|
||||
---
|
||||
|
||||
#### 🔷 Альтернатива: Использование биржи Hyperliquid
|
||||
|
||||
**NOFX также поддерживает Hyperliquid** - децентрализованную биржу бессрочных фьючерсов. Чтобы использовать Hyperliquid вместо Binance:
|
||||
|
||||
**Шаг 1**: Получите приватный ключ Ethereum (для аутентификации Hyperliquid)
|
||||
|
||||
1. Откройте **MetaMask** (или любой Ethereum кошелек)
|
||||
2. Экспортируйте приватный ключ
|
||||
3. **Удалите префикс `0x`** из ключа
|
||||
4. Пополните кошелек на [Hyperliquid](https://hyperliquid.xyz)
|
||||
|
||||
**Шаг 2**: Настройте `config.json` для Hyperliquid
|
||||
|
||||
```json
|
||||
{
|
||||
"traders": [
|
||||
{
|
||||
"id": "hyperliquid_trader",
|
||||
"name": "My Hyperliquid Trader",
|
||||
"ai_model": "deepseek",
|
||||
"exchange": "hyperliquid",
|
||||
"hyperliquid_private_key": "your_private_key_without_0x",
|
||||
"hyperliquid_testnet": false,
|
||||
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
||||
"initial_balance": 1000.0,
|
||||
"scan_interval_minutes": 3
|
||||
}
|
||||
],
|
||||
"use_default_coins": true,
|
||||
"api_server_port": 8080
|
||||
}
|
||||
```
|
||||
|
||||
**Ключевые отличия от конфигурации Binance:**
|
||||
- Замените `binance_api_key` + `binance_secret_key` на `hyperliquid_private_key`
|
||||
- Добавьте поле `"exchange": "hyperliquid"`
|
||||
- Установите `hyperliquid_testnet: false` для мейннета (или `true` для тестнета)
|
||||
|
||||
**⚠️ Предупреждение безопасности**: Никогда не делитесь приватным ключом! Используйте отдельный кошелек для торговли, а не основной.
|
||||
|
||||
---
|
||||
|
||||
#### 🔶 Альтернатива: Использование биржи Aster DEX
|
||||
|
||||
**NOFX также поддерживает Aster DEX** - децентрализованную биржу бессрочных фьючерсов, совместимую с Binance!
|
||||
|
||||
**Почему выбрать Aster?**
|
||||
- 🎯 API совместимый с Binance (легкая миграция)
|
||||
- 🔐 Система безопасности API кошелька
|
||||
- 💰 Более низкие комиссии за торговлю
|
||||
- 🌐 Поддержка нескольких цепей (ETH, BSC, Polygon)
|
||||
- 🌍 Не нужна KYC
|
||||
|
||||
**Шаг 1**: Создайте Aster API кошелек
|
||||
|
||||
1. Посетите [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
|
||||
2. Подключите основной кошелек (MetaMask, WalletConnect и т.д.)
|
||||
3. Нажмите "Создать API кошелек"
|
||||
4. **Сохраните эти 3 элемента немедленно:**
|
||||
- Адрес основного кошелька (User)
|
||||
- Адрес API кошелька (Signer)
|
||||
- Приватный ключ API кошелька (⚠️ показывается только один раз!)
|
||||
|
||||
**Шаг 2**: Настройте `config.json` для Aster
|
||||
|
||||
```json
|
||||
{
|
||||
"traders": [
|
||||
{
|
||||
"id": "aster_deepseek",
|
||||
"name": "Aster DeepSeek Trader",
|
||||
"ai_model": "deepseek",
|
||||
"exchange": "aster",
|
||||
|
||||
"aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e",
|
||||
"aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0",
|
||||
"aster_private_key": "4fd0a42218f3eae43a6ce26d22544e986139a01e5b34a62db53757ffca81bae1",
|
||||
|
||||
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
||||
"initial_balance": 1000.0,
|
||||
"scan_interval_minutes": 3
|
||||
}
|
||||
],
|
||||
"use_default_coins": true,
|
||||
"api_server_port": 8080,
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5,
|
||||
"altcoin_leverage": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Ключевые поля конфигурации:**
|
||||
- `"exchange": "aster"` - Установите биржу на Aster
|
||||
- `aster_user` - Адрес вашего основного кошелька
|
||||
- `aster_signer` - Адрес API кошелька (из Шага 1)
|
||||
- `aster_private_key` - Приватный ключ API кошелька (без префикса `0x`)
|
||||
|
||||
**⚠️ Примечания безопасности**:
|
||||
- API кошелек отдельный от основного (дополнительный уровень безопасности)
|
||||
- Никогда не делитесь приватным ключом API
|
||||
- Вы можете отозвать доступ API кошелька в любое время на [asterdex.com](https://www.asterdex.com/en/api-wallet)
|
||||
|
||||
---
|
||||
|
||||
#### ⚔️ Экспертный режим: Конкуренция нескольких трейдеров
|
||||
|
||||
Для запуска нескольких AI трейдеров, конкурирующих друг с другом:
|
||||
@@ -360,6 +544,9 @@ cp config.json.example config.json
|
||||
| `qwen_key` | Qwen API ключ | `"sk-xxx"` | Требуется при использовании Qwen |
|
||||
| `initial_balance` | Начальный баланс для расчета P/L | `1000.0` | ✅ Да |
|
||||
| `scan_interval_minutes` | Частота решений (минуты) | `3` (рекомендуется 3-5) | ✅ Да |
|
||||
| **`leverage`** | **Конфигурация плеча (v2.0.3+)** | См. ниже | ✅ Да |
|
||||
| `btc_eth_leverage` | Максимальное плечо для BTC/ETH<br>⚠️ Субаккаунты: ≤5x | `5` (по умолчанию, безопасно)<br>`50` (максимум для основного аккаунта) | ✅ Да |
|
||||
| `altcoin_leverage` | Максимальное плечо для альткоинов<br>⚠️ Субаккаунты: ≤5x | `5` (по умолчанию, безопасно)<br>`20` (максимум для основного аккаунта) | ✅ Да |
|
||||
| `use_default_coins` | Использовать встроенный список монет<br>**✨ Умное значение по умолчанию: `true`** (v2.0.2+)<br>Автоматически включается без API | `true` или опустить | ❌ Нет<br>(Опционально, авто) |
|
||||
| `coin_pool_api_url` | API пользовательского пула монет<br>*Требуется только при `use_default_coins: false`* | `""` (пусто) | ❌ Нет |
|
||||
| `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`
|
||||
|
||||
**Умное поведение по умолчанию (v2.0.2+):**
|
||||
|
||||
+248
-4
@@ -9,7 +9,7 @@
|
||||
|
||||
---
|
||||
|
||||
Автоматизована система торгівлі ф'ючерсами Binance на базі **DeepSeek/Qwen AI**, що підтримує **змагання кількох AI-моделей у реальній торгівлі**, з повним аналізом ринку, прийняттям рішень AI, **механізмом самонавчання** та професійним веб-інтерфейсом моніторингу.
|
||||
Автоматизована система торгівлі криптовалютними ф'ючерсами на базі **DeepSeek/Qwen AI**, що підтримує **Binance, Hyperliquid та Aster DEX біржі**, **змагання кількох AI-моделей у реальній торгівлі**, з повним аналізом ринку, прийняттям рішень AI, **механізмом самонавчання** та професійним веб-інтерфейсом моніторингу.
|
||||
|
||||
> ⚠️ **Попередження про ризики**: Ця система експериментальна. Автоматична торгівля з AI несе значні ризики. Наполегливо рекомендується використовувати лише для навчання/досліджень або тестування з невеликими сумами!
|
||||
|
||||
@@ -21,6 +21,75 @@
|
||||
|
||||
---
|
||||
|
||||
## 🆕 Останні оновлення
|
||||
|
||||
### 🚀 Підтримка кількох бірж!
|
||||
|
||||
NOFX тепер підтримує **три основні біржі**: Binance, Hyperliquid та Aster DEX!
|
||||
|
||||
#### **Біржа Hyperliquid**
|
||||
|
||||
Високопродуктивна децентралізована біржа безстрокових ф'ючерсів!
|
||||
|
||||
**Ключові особливості:**
|
||||
- ✅ Повна підтримка торгівлі (лонг/шорт, плече, стоп-лосс/тейк-профіт)
|
||||
- ✅ Автоматична обробка точності (розмір та ціна ордера)
|
||||
- ✅ Єдиний інтерфейс трейдера (безшовне перемикання бірж)
|
||||
- ✅ Підтримка мейннету та тестнету
|
||||
- ✅ Не потрібні API ключі - тільки приватний ключ Ethereum
|
||||
|
||||
**Чому Hyperliquid?**
|
||||
- 🔥 Нижчі комісії ніж на централізованих біржах
|
||||
- 🔒 Без зберігання - ви контролюєте свої кошти
|
||||
- ⚡ Швидке виконання з розрахунком на ланцюзі
|
||||
- 🌍 Не потрібна KYC
|
||||
|
||||
**Швидкий старт:**
|
||||
1. Отримайте приватний ключ MetaMask (видаліть префікс `0x`)
|
||||
2. Встановіть `"exchange": "hyperliquid"` в config.json
|
||||
3. Додайте `"hyperliquid_private_key": "your_key"`
|
||||
4. Почніть торгувати!
|
||||
|
||||
Див. [Посібник з конфігурації](#-альтернатива-використання-біржі-hyperliquid).
|
||||
|
||||
#### **Біржа Aster DEX** (НОВЕ! v2.0.2)
|
||||
|
||||
Децентралізована біржа безстрокових ф'ючерсів, сумісна з Binance!
|
||||
|
||||
**Ключові особливості:**
|
||||
- ✅ API в стилі Binance (легка міграція з Binance)
|
||||
- ✅ Web3 автентифікація гаманця (безпечно та децентралізовано)
|
||||
- ✅ Повна підтримка торгівлі з автоматичною обробкою точності
|
||||
- ✅ Нижчі комісії за торгівлю ніж CEX
|
||||
- ✅ Сумісність з EVM (Ethereum, BSC, Polygon тощо)
|
||||
|
||||
**Чому Aster?**
|
||||
- 🎯 **API сумісний з Binance** - потрібні мінімальні зміни коду
|
||||
- 🔐 **Система API гаманця** - окремий торговий гаманець для безпеки
|
||||
- 💰 **Конкурентні комісії** - нижче ніж більшість централізованих бірж
|
||||
- 🌐 **Підтримка кількох ланцюгів** - торгуйте на вашому улюбленому EVM ланцюзі
|
||||
|
||||
**Швидкий старт:**
|
||||
1. Відвідайте [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
|
||||
2. Підключіть основний гаманець і створіть API гаманець
|
||||
3. Скопіюйте адресу API Signer та приватний ключ
|
||||
4. Встановіть `"exchange": "aster"` в config.json
|
||||
5. Додайте `"aster_user"`, `"aster_signer"` та `"aster_private_key"`
|
||||
|
||||
---
|
||||
|
||||
## 📸 Скриншоти
|
||||
|
||||
### 🏆 Режим змагання - Битва AI в реальному часі
|
||||

|
||||
*Лідерборд з кількома AI та графіки порівняння продуктивності в реальному часі показують битву Qwen проти DeepSeek*
|
||||
|
||||
### 📊 Деталі трейдера - Повна торгова панель
|
||||

|
||||
*Професійний торговий інтерфейс з кривими капіталу, живими позиціями та логами рішень AI з розкриваємими вхідними промптами та ланцюгом міркувань*
|
||||
|
||||
---
|
||||
|
||||
## ✨ Основні можливості
|
||||
|
||||
### 🏆 Режим змагання кількох AI
|
||||
@@ -50,7 +119,11 @@
|
||||
- **Ліміт позиції по монеті**:
|
||||
- Альткоїни ≤ 1.5x капітал рахунку
|
||||
- BTC/ETH ≤ 10x капітал рахунку
|
||||
- **Фіксоване плече**: Альткоїни 20x | BTC/ETH 50x
|
||||
- **Налаштовуване плече** (v2.0.3+):
|
||||
- Встановіть максимальне плече в config.json
|
||||
- За замовчуванням: 5x для всіх монет (безпечно для субакаунтів)
|
||||
- Основні акаунти можуть збільшити: Альткоїни до 20x, BTC/ETH до 50x
|
||||
- ⚠️ Субакаунти Binance обмежені ≤5x плечем
|
||||
- **Управління маржею**: Загальне використання ≤90%, AI приймає автономні рішення
|
||||
- **Співвідношення ризик/дохід**: Обов'язкове ≥1:2 (стоп-лосс:тейк-профіт)
|
||||
- **Запобігання накопиченню позицій**: Заборона дублювання відкриття тієї ж монети/напрямку
|
||||
@@ -123,8 +196,10 @@ nano config.json # або використайте будь-який редак
|
||||
chmod +x start.sh
|
||||
./start.sh start --build
|
||||
|
||||
# Варіант 2: Використайте docker-compose безпосередньо
|
||||
docker-compose up -d --build
|
||||
# Варіант 2: Використайте docker compose безпосередньо
|
||||
# Цей проект використовує синтаксис Docker Compose V2 (з пробілами)
|
||||
# Якщо у вас встановлена стара версія `docker-compose`, оновіть до Docker Desktop або Docker 20.10+
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
#### Крок 3: Доступ до панелі
|
||||
@@ -268,6 +343,10 @@ cp config.json.example config.json
|
||||
"scan_interval_minutes": 3
|
||||
}
|
||||
],
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5,
|
||||
"altcoin_leverage": 5
|
||||
},
|
||||
"use_default_coins": true,
|
||||
"coin_pool_api_url": "",
|
||||
"oi_top_api_url": "",
|
||||
@@ -300,6 +379,111 @@ cp config.json.example config.json
|
||||
|
||||
---
|
||||
|
||||
#### 🔷 Альтернатива: Використання біржі Hyperliquid
|
||||
|
||||
**NOFX також підтримує Hyperliquid** - децентралізовану біржу безстрокових ф'ючерсів. Щоб використовувати Hyperliquid замість Binance:
|
||||
|
||||
**Крок 1**: Отримайте приватний ключ Ethereum (для автентифікації Hyperliquid)
|
||||
|
||||
1. Відкрийте **MetaMask** (або будь-який Ethereum гаманець)
|
||||
2. Експортуйте приватний ключ
|
||||
3. **Видаліть префікс `0x`** з ключа
|
||||
4. Поповніть гаманець на [Hyperliquid](https://hyperliquid.xyz)
|
||||
|
||||
**Крок 2**: Налаштуйте `config.json` для Hyperliquid
|
||||
|
||||
```json
|
||||
{
|
||||
"traders": [
|
||||
{
|
||||
"id": "hyperliquid_trader",
|
||||
"name": "My Hyperliquid Trader",
|
||||
"ai_model": "deepseek",
|
||||
"exchange": "hyperliquid",
|
||||
"hyperliquid_private_key": "your_private_key_without_0x",
|
||||
"hyperliquid_testnet": false,
|
||||
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
||||
"initial_balance": 1000.0,
|
||||
"scan_interval_minutes": 3
|
||||
}
|
||||
],
|
||||
"use_default_coins": true,
|
||||
"api_server_port": 8080
|
||||
}
|
||||
```
|
||||
|
||||
**Ключові відмінності від конфігурації Binance:**
|
||||
- Замініть `binance_api_key` + `binance_secret_key` на `hyperliquid_private_key`
|
||||
- Додайте поле `"exchange": "hyperliquid"`
|
||||
- Встановіть `hyperliquid_testnet: false` для мейннету (або `true` для тестнету)
|
||||
|
||||
**⚠️ Попередження безпеки**: Ніколи не діліться приватним ключем! Використовуйте окремий гаманець для торгівлі, а не основний.
|
||||
|
||||
---
|
||||
|
||||
#### 🔶 Альтернатива: Використання біржі Aster DEX
|
||||
|
||||
**NOFX також підтримує Aster DEX** - децентралізовану біржу безстрокових ф'ючерсів, сумісну з Binance!
|
||||
|
||||
**Чому обрати Aster?**
|
||||
- 🎯 API сумісний з Binance (легка міграція)
|
||||
- 🔐 Система безпеки API гаманця
|
||||
- 💰 Нижчі комісії за торгівлю
|
||||
- 🌐 Підтримка кількох ланцюгів (ETH, BSC, Polygon)
|
||||
- 🌍 Не потрібна KYC
|
||||
|
||||
**Крок 1**: Створіть Aster API гаманець
|
||||
|
||||
1. Відвідайте [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
|
||||
2. Підключіть основний гаманець (MetaMask, WalletConnect тощо)
|
||||
3. Натисніть "Створити API гаманець"
|
||||
4. **Збережіть ці 3 елементи негайно:**
|
||||
- Адреса основного гаманця (User)
|
||||
- Адреса API гаманця (Signer)
|
||||
- Приватний ключ API гаманця (⚠️ показується лише один раз!)
|
||||
|
||||
**Крок 2**: Налаштуйте `config.json` для Aster
|
||||
|
||||
```json
|
||||
{
|
||||
"traders": [
|
||||
{
|
||||
"id": "aster_deepseek",
|
||||
"name": "Aster DeepSeek Trader",
|
||||
"ai_model": "deepseek",
|
||||
"exchange": "aster",
|
||||
|
||||
"aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e",
|
||||
"aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0",
|
||||
"aster_private_key": "4fd0a42218f3eae43a6ce26d22544e986139a01e5b34a62db53757ffca81bae1",
|
||||
|
||||
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
||||
"initial_balance": 1000.0,
|
||||
"scan_interval_minutes": 3
|
||||
}
|
||||
],
|
||||
"use_default_coins": true,
|
||||
"api_server_port": 8080,
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5,
|
||||
"altcoin_leverage": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Ключові поля конфігурації:**
|
||||
- `"exchange": "aster"` - Встановіть біржу на Aster
|
||||
- `aster_user` - Адреса вашого основного гаманця
|
||||
- `aster_signer` - Адреса API гаманця (з Кроку 1)
|
||||
- `aster_private_key` - Приватний ключ API гаманця (без префікса `0x`)
|
||||
|
||||
**⚠️ Примітки безпеки**:
|
||||
- API гаманець окремий від основного (додатковий рівень безпеки)
|
||||
- Ніколи не діліться приватним ключем API
|
||||
- Ви можете відкликати доступ API гаманця в будь-який час на [asterdex.com](https://www.asterdex.com/en/api-wallet)
|
||||
|
||||
---
|
||||
|
||||
#### ⚔️ Експертний режим: Змагання кількох трейдерів
|
||||
|
||||
Для запуску кількох AI трейдерів, що змагаються один з одним:
|
||||
@@ -360,6 +544,9 @@ cp config.json.example config.json
|
||||
| `qwen_key` | Qwen API ключ | `"sk-xxx"` | Потрібно при використанні Qwen |
|
||||
| `initial_balance` | Початковий баланс для розрахунку P/L | `1000.0` | ✅ Так |
|
||||
| `scan_interval_minutes` | Частота рішень (хвилини) | `3` (рекомендується 3-5) | ✅ Так |
|
||||
| **`leverage`** | **Конфігурація плеча (v2.0.3+)** | Див. нижче | ✅ Так |
|
||||
| `btc_eth_leverage` | Максимальне плече для BTC/ETH<br>⚠️ Субакаунти: ≤5x | `5` (за замовчуванням, безпечно)<br>`50` (максимум для основного акаунта) | ✅ Так |
|
||||
| `altcoin_leverage` | Максимальне плече для альткоїнів<br>⚠️ Субакаунти: ≤5x | `5` (за замовчуванням, безпечно)<br>`20` (максимум для основного акаунта) | ✅ Так |
|
||||
| `use_default_coins` | Використовувати вбудований список монет<br>**✨ Розумне значення за замовчуванням: `true`** (v2.0.2+)<br>Автоматично включається без API | `true` або опустити | ❌ Ні<br>(Опціонально, авто) |
|
||||
| `coin_pool_api_url` | API користувацького пулу монет<br>*Потрібно лише при `use_default_coins: false`* | `""` (пусто) | ❌ Ні |
|
||||
| `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`
|
||||
|
||||
**Розумна поведінка за замовчуванням (v2.0.2+):**
|
||||
|
||||
+248
-4
@@ -9,7 +9,7 @@
|
||||
|
||||
---
|
||||
|
||||
一个基于 **DeepSeek/Qwen AI** 的币安合约自动交易系统,支持**多AI模型实盘竞赛**,具备完整的市场分析、AI决策、**自我学习机制**和专业的Web监控界面。
|
||||
一个基于 **DeepSeek/Qwen AI** 的加密货币期货自动交易系统,支持 **Binance、Hyperliquid和Aster DEX交易所**,**多AI模型实盘竞赛**,具备完整的市场分析、AI决策、**自我学习机制**和专业的Web监控界面。
|
||||
|
||||
> ⚠️ **风险提示**:本系统为实验性项目,AI自动交易存在重大风险,强烈建议仅用于学习研究或小额资金测试!
|
||||
|
||||
@@ -21,6 +21,75 @@
|
||||
|
||||
---
|
||||
|
||||
## 🆕 最新更新
|
||||
|
||||
### 🚀 多交易所支持!
|
||||
|
||||
NOFX现已支持**三大交易所**:Binance、Hyperliquid和Aster DEX!
|
||||
|
||||
#### **Hyperliquid交易所**
|
||||
|
||||
高性能的去中心化永续期货交易所!
|
||||
|
||||
**核心特性:**
|
||||
- ✅ 完整交易支持(做多/做空、杠杆、止损/止盈)
|
||||
- ✅ 自动精度处理(订单数量和价格)
|
||||
- ✅ 统一trader接口(无缝切换交易所)
|
||||
- ✅ 支持主网和测试网
|
||||
- ✅ 无需API密钥 - 只需以太坊私钥
|
||||
|
||||
**为什么选择Hyperliquid?**
|
||||
- 🔥 比中心化交易所手续费更低
|
||||
- 🔒 非托管 - 你掌控自己的资金
|
||||
- ⚡ 快速执行与链上结算
|
||||
- 🌍 无需KYC
|
||||
|
||||
**快速开始:**
|
||||
1. 获取你的MetaMask私钥(去掉`0x`前缀)
|
||||
2. 在config.json中设置`"exchange": "hyperliquid"`
|
||||
3. 添加`"hyperliquid_private_key": "your_key"`
|
||||
4. 开始交易!
|
||||
|
||||
详见[配置指南](#-备选使用hyperliquid交易所)。
|
||||
|
||||
#### **Aster DEX交易所**(新!v2.0.2)
|
||||
|
||||
兼容Binance的去中心化永续期货交易所!
|
||||
|
||||
**核心特性:**
|
||||
- ✅ Binance风格API(从Binance轻松迁移)
|
||||
- ✅ Web3钱包认证(安全且去中心化)
|
||||
- ✅ 完整交易支持,自动精度处理
|
||||
- ✅ 比中心化交易所手续费更低
|
||||
- ✅ 兼容EVM(以太坊、BSC、Polygon等)
|
||||
|
||||
**为什么选择Aster?**
|
||||
- 🎯 **兼容Binance API** - 需要最少的代码修改
|
||||
- 🔐 **API钱包系统** - 独立交易钱包提升安全性
|
||||
- 💰 **有竞争力的手续费** - 比大多数中心化交易所更低
|
||||
- 🌐 **多链支持** - 在你喜欢的EVM链上交易
|
||||
|
||||
**快速开始:**
|
||||
1. 访问[Aster API钱包](https://www.asterdex.com/en/api-wallet)
|
||||
2. 连接你的主钱包并创建API钱包
|
||||
3. 复制API Signer地址和私钥
|
||||
4. 在config.json中设置`"exchange": "aster"`
|
||||
5. 添加`"aster_user"`、`"aster_signer"`和`"aster_private_key"`
|
||||
|
||||
---
|
||||
|
||||
## 📸 系统截图
|
||||
|
||||
### 🏆 竞赛模式 - AI实时对战
|
||||

|
||||
*多AI排行榜和实时性能对比图表,展示Qwen vs DeepSeek实时交易对战*
|
||||
|
||||
### 📊 交易详情 - 完整交易仪表盘
|
||||

|
||||
*专业交易界面,包含权益曲线、实时持仓、AI决策日志,支持展开查看输入提示词和AI思维链推理过程*
|
||||
|
||||
---
|
||||
|
||||
## ✨ 核心特性
|
||||
|
||||
### 🏆 多AI竞赛模式
|
||||
@@ -50,7 +119,11 @@
|
||||
- **单币种仓位上限**:
|
||||
- 山寨币 ≤ 1.5倍账户净值
|
||||
- BTC/ETH ≤ 10倍账户净值
|
||||
- **固定杠杆**: 山寨币20倍 | BTC/ETH 50倍
|
||||
- **可配置杠杆** (v2.0.3+):
|
||||
- 在config.json中设置最大杠杆
|
||||
- 默认:所有币种5倍(子账户安全)
|
||||
- 主账户可增加:山寨币最高20倍,BTC/ETH最高50倍
|
||||
- ⚠️ 币安子账户限制≤5倍杠杆
|
||||
- **保证金管理**: 总使用率≤90%,AI自主决策使用率
|
||||
- **风险回报比**: 强制≥1:2(止损:止盈)
|
||||
- **防止仓位叠加**: 同币种同方向不允许重复开仓
|
||||
@@ -187,8 +260,10 @@ nano config.json # 或使用其他编辑器
|
||||
chmod +x start.sh
|
||||
./start.sh start --build
|
||||
|
||||
# 方式2:直接使用docker-compose
|
||||
docker-compose up -d --build
|
||||
|
||||
# 方式2:直接使用docker compose
|
||||
# 如果您还在使用旧的独立 `docker-compose`,请升级到 Docker Desktop 或 Docker 20.10+
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
#### 步骤3:访问控制台
|
||||
@@ -331,6 +406,10 @@ cp config.json.example config.json
|
||||
"scan_interval_minutes": 3
|
||||
}
|
||||
],
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5,
|
||||
"altcoin_leverage": 5
|
||||
},
|
||||
"use_default_coins": true,
|
||||
"coin_pool_api_url": "",
|
||||
"oi_top_api_url": "",
|
||||
@@ -363,6 +442,111 @@ cp config.json.example config.json
|
||||
|
||||
---
|
||||
|
||||
#### 🔷 备选:使用Hyperliquid交易所
|
||||
|
||||
**NOFX也支持Hyperliquid** - 去中心化永续期货交易所。使用Hyperliquid而非Binance:
|
||||
|
||||
**步骤1**:获取以太坊私钥(用于Hyperliquid身份验证)
|
||||
|
||||
1. 打开**MetaMask**(或任何以太坊钱包)
|
||||
2. 导出你的私钥
|
||||
3. **去掉`0x`前缀**
|
||||
4. 在[Hyperliquid](https://hyperliquid.xyz)上为钱包充值
|
||||
|
||||
**步骤2**:为Hyperliquid配置`config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"traders": [
|
||||
{
|
||||
"id": "hyperliquid_trader",
|
||||
"name": "My Hyperliquid Trader",
|
||||
"ai_model": "deepseek",
|
||||
"exchange": "hyperliquid",
|
||||
"hyperliquid_private_key": "your_private_key_without_0x",
|
||||
"hyperliquid_testnet": false,
|
||||
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
||||
"initial_balance": 1000.0,
|
||||
"scan_interval_minutes": 3
|
||||
}
|
||||
],
|
||||
"use_default_coins": true,
|
||||
"api_server_port": 8080
|
||||
}
|
||||
```
|
||||
|
||||
**与Binance配置的关键区别:**
|
||||
- 用`hyperliquid_private_key`替换`binance_api_key` + `binance_secret_key`
|
||||
- 添加`"exchange": "hyperliquid"`字段
|
||||
- 设置`hyperliquid_testnet: false`用于主网(或`true`用于测试网)
|
||||
|
||||
**⚠️ 安全警告**:切勿分享你的私钥!使用专门的钱包进行交易,而非主钱包。
|
||||
|
||||
---
|
||||
|
||||
#### 🔶 备选:使用Aster DEX交易所
|
||||
|
||||
**NOFX也支持Aster DEX** - 兼容Binance的去中心化永续期货交易所!
|
||||
|
||||
**为什么选择Aster?**
|
||||
- 🎯 兼容Binance API(轻松迁移)
|
||||
- 🔐 API钱包安全系统
|
||||
- 💰 更低的交易手续费
|
||||
- 🌐 多链支持(ETH、BSC、Polygon)
|
||||
- 🌍 无需KYC
|
||||
|
||||
**步骤1**:创建Aster API钱包
|
||||
|
||||
1. 访问[Aster API钱包](https://www.asterdex.com/en/api-wallet)
|
||||
2. 连接你的主钱包(MetaMask、WalletConnect等)
|
||||
3. 点击"创建API钱包"
|
||||
4. **立即保存这3项:**
|
||||
- 主钱包地址(User)
|
||||
- API钱包地址(Signer)
|
||||
- API钱包私钥(⚠️ 仅显示一次!)
|
||||
|
||||
**步骤2**:为Aster配置`config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"traders": [
|
||||
{
|
||||
"id": "aster_deepseek",
|
||||
"name": "Aster DeepSeek Trader",
|
||||
"ai_model": "deepseek",
|
||||
"exchange": "aster",
|
||||
|
||||
"aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e",
|
||||
"aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0",
|
||||
"aster_private_key": "4fd0a42218f3eae43a6ce26d22544e986139a01e5b34a62db53757ffca81bae1",
|
||||
|
||||
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
||||
"initial_balance": 1000.0,
|
||||
"scan_interval_minutes": 3
|
||||
}
|
||||
],
|
||||
"use_default_coins": true,
|
||||
"api_server_port": 8080,
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5,
|
||||
"altcoin_leverage": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**关键配置字段:**
|
||||
- `"exchange": "aster"` - 设置交易所为Aster
|
||||
- `aster_user` - 你的主钱包地址
|
||||
- `aster_signer` - API钱包地址(来自步骤1)
|
||||
- `aster_private_key` - API钱包私钥(去掉`0x`前缀)
|
||||
|
||||
**⚠️ 安全提示**:
|
||||
- API钱包与主钱包分离(额外的安全层)
|
||||
- 切勿分享API私钥
|
||||
- 你可以随时在[asterdex.com](https://www.asterdex.com/en/api-wallet)撤销API钱包访问
|
||||
|
||||
---
|
||||
|
||||
#### ⚔️ 专家模式:多Trader竞赛
|
||||
|
||||
用于运行多个AI trader相互竞争:
|
||||
@@ -423,6 +607,9 @@ cp config.json.example config.json
|
||||
| `qwen_key` | Qwen API密钥 | `"sk-xxx"` | 使用Qwen时必填 |
|
||||
| `initial_balance` | 用于P/L计算的起始余额 | `1000.0` | ✅ 是 |
|
||||
| `scan_interval_minutes` | 决策频率(分钟) | `3`(建议3-5) | ✅ 是 |
|
||||
| **`leverage`** | **杠杆配置 (v2.0.3+)** | 见下文 | ✅ 是 |
|
||||
| `btc_eth_leverage` | BTC/ETH最大杠杆<br>⚠️ 子账户:≤5倍 | `5`(默认,安全)<br>`50`(主账户最大) | ✅ 是 |
|
||||
| `altcoin_leverage` | 山寨币最大杠杆<br>⚠️ 子账户:≤5倍 | `5`(默认,安全)<br>`20`(主账户最大) | ✅ 是 |
|
||||
| `use_default_coins` | 使用内置币种列表<br>**✨ 智能默认:`true`** (v2.0.2+)<br>未提供API时自动启用 | `true` 或省略 | ❌ 否<br>(可选,自动默认) |
|
||||
| `coin_pool_api_url` | 自定义币种池API<br>*仅当`use_default_coins: false`时需要* | `""`(空) | ❌ 否 |
|
||||
| `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` 字段
|
||||
|
||||
**智能默认行为(v2.0.2+):**
|
||||
|
||||
+4
-3
@@ -61,7 +61,7 @@ func corsMiddleware() gin.HandlerFunc {
|
||||
// setupRoutes 设置路由
|
||||
func (s *Server) setupRoutes() {
|
||||
// 健康检查
|
||||
s.router.GET("/health", s.handleHealth)
|
||||
s.router.Any("/health", s.handleHealth)
|
||||
|
||||
// API路由组
|
||||
api := s.router.Group("/api")
|
||||
@@ -639,8 +639,9 @@ func (s *Server) handlePerformance(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 分析最近20个周期的交易表现
|
||||
performance, err := trader.GetDecisionLogger().AnalyzePerformance(20)
|
||||
// 分析最近100个周期的交易表现(避免长期持仓的交易记录丢失)
|
||||
// 假设每3分钟一个周期,100个周期 = 5小时,足够覆盖大部分交易
|
||||
performance, err := trader.GetDecisionLogger().AnalyzePerformance(100)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": fmt.Sprintf("分析历史表现失败: %v", err),
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"ai_model": "deepseek",
|
||||
"exchange": "hyperliquid",
|
||||
"hyperliquid_private_key": "your_ethereum_private_key_without_0x_prefix",
|
||||
"hyperliquid_wallet_addr": "your_ethereum_address",
|
||||
"hyperliquid_testnet": false,
|
||||
"deepseek_key": "your_deepseek_api_key",
|
||||
"initial_balance": 1000,
|
||||
@@ -21,9 +22,55 @@
|
||||
"qwen_key": "your_qwen_api_key",
|
||||
"initial_balance": 1000,
|
||||
"scan_interval_minutes": 3
|
||||
},
|
||||
{
|
||||
"id": "binance_custom",
|
||||
"name": "Binance Custom API Trader",
|
||||
"ai_model": "custom",
|
||||
"exchange": "binance",
|
||||
"binance_api_key": "your_binance_api_key",
|
||||
"binance_secret_key": "your_binance_secret_key",
|
||||
"custom_api_url": "https://api.openai.com/v1",
|
||||
"custom_api_key": "sk-your-api-key",
|
||||
"custom_model_name": "gpt-4o",
|
||||
"initial_balance": 1000,
|
||||
"scan_interval_minutes": 3
|
||||
},
|
||||
{
|
||||
"id": "aster_deepseek",
|
||||
"name": "Aster DeepSeek Trader",
|
||||
"ai_model": "deepseek",
|
||||
"exchange": "aster",
|
||||
|
||||
// 注意请仔细阅读这三个提示 请进入https://www.asterdex.com/en/api-wallet网站 -> 选择专业api -> 创建新api获取以下信息
|
||||
// user: 主钱包地址 (登录地址/连接到aster的钱包地址)
|
||||
// signer: API钱包地址 (点击生成地址后生成的地址)
|
||||
// privateKey: API钱包私钥 (生成地址对应的私钥)
|
||||
|
||||
"aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e",
|
||||
"aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0",
|
||||
"aster_private_key": "your_aster_api_wallet_private_key_without_0x_prefix",
|
||||
|
||||
"deepseek_key": "your_deepseek_api_key",
|
||||
"initial_balance": 1000.0,
|
||||
"scan_interval_minutes": 3
|
||||
}
|
||||
],
|
||||
"leverage": {
|
||||
"btc_eth_leverage": 5,
|
||||
"altcoin_leverage": 5
|
||||
},
|
||||
"use_default_coins": true,
|
||||
"default_coins": [
|
||||
"BTCUSDT",
|
||||
"ETHUSDT",
|
||||
"SOLUSDT",
|
||||
"BNBUSDT",
|
||||
"XRPUSDT",
|
||||
"DOGEUSDT",
|
||||
"ADAUSDT",
|
||||
"HYPEUSDT",
|
||||
],
|
||||
"coin_pool_api_url": "",
|
||||
"oi_top_api_url": "",
|
||||
"api_server_port": 8080,
|
||||
|
||||
+30
-28
@@ -55,15 +55,17 @@ type OITopData struct {
|
||||
|
||||
// Context 交易上下文(传递给AI的完整信息)
|
||||
type Context struct {
|
||||
CurrentTime string `json:"current_time"`
|
||||
RuntimeMinutes int `json:"runtime_minutes"`
|
||||
CallCount int `json:"call_count"`
|
||||
Account AccountInfo `json:"account"`
|
||||
Positions []PositionInfo `json:"positions"`
|
||||
CandidateCoins []CandidateCoin `json:"candidate_coins"`
|
||||
MarketDataMap map[string]*market.Data `json:"-"` // 不序列化,但内部使用
|
||||
OITopDataMap map[string]*OITopData `json:"-"` // OI Top数据映射
|
||||
Performance interface{} `json:"-"` // 历史表现分析(logger.PerformanceAnalysis)
|
||||
CurrentTime string `json:"current_time"`
|
||||
RuntimeMinutes int `json:"runtime_minutes"`
|
||||
CallCount int `json:"call_count"`
|
||||
Account AccountInfo `json:"account"`
|
||||
Positions []PositionInfo `json:"positions"`
|
||||
CandidateCoins []CandidateCoin `json:"candidate_coins"`
|
||||
MarketDataMap map[string]*market.Data `json:"-"` // 不序列化,但内部使用
|
||||
OITopDataMap map[string]*OITopData `json:"-"` // OI Top数据映射
|
||||
Performance interface{} `json:"-"` // 历史表现分析(logger.PerformanceAnalysis)
|
||||
BTCETHLeverage int `json:"-"` // BTC/ETH杠杆倍数(从配置读取)
|
||||
AltcoinLeverage int `json:"-"` // 山寨币杠杆倍数(从配置读取)
|
||||
}
|
||||
|
||||
// Decision AI的交易决策
|
||||
@@ -88,24 +90,24 @@ type FullDecision struct {
|
||||
}
|
||||
|
||||
// GetFullDecision 获取AI的完整交易决策(批量分析所有币种和持仓)
|
||||
func GetFullDecision(ctx *Context) (*FullDecision, error) {
|
||||
func GetFullDecision(ctx *Context, mcpClient *mcp.Client) (*FullDecision, error) {
|
||||
// 1. 为所有币种获取市场数据
|
||||
if err := fetchMarketDataForContext(ctx); err != nil {
|
||||
return nil, fmt.Errorf("获取市场数据失败: %w", err)
|
||||
}
|
||||
|
||||
// 2. 构建 System Prompt(固定规则)和 User Prompt(动态数据)
|
||||
systemPrompt := buildSystemPrompt(ctx.Account.TotalEquity)
|
||||
systemPrompt := buildSystemPrompt(ctx.Account.TotalEquity, ctx.BTCETHLeverage, ctx.AltcoinLeverage)
|
||||
userPrompt := buildUserPrompt(ctx)
|
||||
|
||||
// 3. 调用AI API(使用 system + user prompt)
|
||||
aiResponse, err := mcp.CallWithMessages(systemPrompt, userPrompt)
|
||||
aiResponse, err := mcpClient.CallWithMessages(systemPrompt, userPrompt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("调用AI API失败: %w", err)
|
||||
}
|
||||
|
||||
// 4. 解析AI响应
|
||||
decision, err := parseFullDecisionResponse(aiResponse, ctx.Account.TotalEquity)
|
||||
decision, err := parseFullDecisionResponse(aiResponse, ctx.Account.TotalEquity, ctx.BTCETHLeverage, ctx.AltcoinLeverage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析AI响应失败: %w", err)
|
||||
}
|
||||
@@ -198,7 +200,7 @@ func calculateMaxCandidates(ctx *Context) int {
|
||||
}
|
||||
|
||||
// buildSystemPrompt 构建 System Prompt(固定规则,可缓存)
|
||||
func buildSystemPrompt(accountEquity float64) string {
|
||||
func buildSystemPrompt(accountEquity float64, btcEthLeverage, altcoinLeverage int) string {
|
||||
var sb strings.Builder
|
||||
|
||||
// === 核心使命 ===
|
||||
@@ -220,8 +222,8 @@ func buildSystemPrompt(accountEquity float64) string {
|
||||
sb.WriteString("# ⚖️ 硬约束(风险控制)\n\n")
|
||||
sb.WriteString("1. **风险回报比**: 必须 ≥ 1:3(冒1%风险,赚3%+收益)\n")
|
||||
sb.WriteString("2. **最多持仓**: 3个币种(质量>数量)\n")
|
||||
sb.WriteString(fmt.Sprintf("3. **单币仓位**: 山寨%.0f-%.0f U(20x杠杆) | BTC/ETH %.0f-%.0f U(50x杠杆)\n",
|
||||
accountEquity*0.8, accountEquity*1.5, accountEquity*5, accountEquity*10))
|
||||
sb.WriteString(fmt.Sprintf("3. **单币仓位**: 山寨%.0f-%.0f U(%dx杠杆) | BTC/ETH %.0f-%.0f U(%dx杠杆)\n",
|
||||
accountEquity*0.8, accountEquity*1.5, altcoinLeverage, accountEquity*5, accountEquity*10, btcEthLeverage))
|
||||
sb.WriteString("4. **保证金**: 总使用率 ≤ 90%\n\n")
|
||||
|
||||
// === 做空激励 ===
|
||||
@@ -251,7 +253,7 @@ func buildSystemPrompt(accountEquity float64) string {
|
||||
sb.WriteString("- 💰 **资金序列**:成交量序列、持仓量(OI)序列、资金费率\n")
|
||||
sb.WriteString("- 🎯 **筛选标记**:AI500评分 / OI_Top排名(如果有标注)\n\n")
|
||||
sb.WriteString("**分析方法**(完全由你自主决定):\n")
|
||||
sb.WriteString("- 自由运用序列数据,你可以做趋势分析、形态识别、支撑阻力计算\n")
|
||||
sb.WriteString("- 自由运用序列数据,你可以做但不限于趋势分析、形态识别、支撑阻力、技术阻力位、斐波那契、波动带计算\n")
|
||||
sb.WriteString("- 多维度交叉验证(价格+量+OI+指标+序列形态)\n")
|
||||
sb.WriteString("- 用你认为最有效的方法发现高确定性机会\n")
|
||||
sb.WriteString("- 综合信心度 ≥ 75 才开仓\n\n")
|
||||
@@ -294,7 +296,7 @@ func buildSystemPrompt(accountEquity float64) string {
|
||||
sb.WriteString("简洁分析你的思考过程\n\n")
|
||||
sb.WriteString("**第二步: JSON决策数组**\n\n")
|
||||
sb.WriteString("```json\n[\n")
|
||||
sb.WriteString(fmt.Sprintf(" {\"symbol\": \"BTCUSDT\", \"action\": \"open_short\", \"leverage\": 50, \"position_size_usd\": %.0f, \"stop_loss\": 97000, \"take_profit\": 91000, \"confidence\": 85, \"risk_usd\": 300, \"reasoning\": \"下跌趋势+MACD死叉\"},\n", accountEquity*5))
|
||||
sb.WriteString(fmt.Sprintf(" {\"symbol\": \"BTCUSDT\", \"action\": \"open_short\", \"leverage\": %d, \"position_size_usd\": %.0f, \"stop_loss\": 97000, \"take_profit\": 91000, \"confidence\": 85, \"risk_usd\": 300, \"reasoning\": \"下跌趋势+MACD死叉\"},\n", btcEthLeverage, accountEquity*5))
|
||||
sb.WriteString(" {\"symbol\": \"ETHUSDT\", \"action\": \"close_long\", \"reasoning\": \"止盈离场\"}\n")
|
||||
sb.WriteString("]\n```\n\n")
|
||||
sb.WriteString("**字段说明**:\n")
|
||||
@@ -415,7 +417,7 @@ func buildUserPrompt(ctx *Context) string {
|
||||
}
|
||||
|
||||
// parseFullDecisionResponse 解析AI的完整决策响应
|
||||
func parseFullDecisionResponse(aiResponse string, accountEquity float64) (*FullDecision, error) {
|
||||
func parseFullDecisionResponse(aiResponse string, accountEquity float64, btcEthLeverage, altcoinLeverage int) (*FullDecision, error) {
|
||||
// 1. 提取思维链
|
||||
cotTrace := extractCoTTrace(aiResponse)
|
||||
|
||||
@@ -429,7 +431,7 @@ func parseFullDecisionResponse(aiResponse string, accountEquity float64) (*FullD
|
||||
}
|
||||
|
||||
// 3. 验证决策
|
||||
if err := validateDecisions(decisions, accountEquity); err != nil {
|
||||
if err := validateDecisions(decisions, accountEquity, btcEthLeverage, altcoinLeverage); err != nil {
|
||||
return &FullDecision{
|
||||
CoTTrace: cotTrace,
|
||||
Decisions: decisions,
|
||||
@@ -496,10 +498,10 @@ func fixMissingQuotes(jsonStr string) string {
|
||||
return jsonStr
|
||||
}
|
||||
|
||||
// validateDecisions 验证所有决策(需要账户信息)
|
||||
func validateDecisions(decisions []Decision, accountEquity float64) error {
|
||||
// validateDecisions 验证所有决策(需要账户信息和杠杆配置)
|
||||
func validateDecisions(decisions []Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error {
|
||||
for i, decision := range decisions {
|
||||
if err := validateDecision(&decision, accountEquity); err != nil {
|
||||
if err := validateDecision(&decision, accountEquity, btcEthLeverage, altcoinLeverage); err != nil {
|
||||
return fmt.Errorf("决策 #%d 验证失败: %w", i+1, err)
|
||||
}
|
||||
}
|
||||
@@ -529,7 +531,7 @@ func findMatchingBracket(s string, start int) int {
|
||||
}
|
||||
|
||||
// validateDecision 验证单个决策的有效性
|
||||
func validateDecision(d *Decision, accountEquity float64) error {
|
||||
func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error {
|
||||
// 验证action
|
||||
validActions := map[string]bool{
|
||||
"open_long": true,
|
||||
@@ -546,16 +548,16 @@ func validateDecision(d *Decision, accountEquity float64) error {
|
||||
|
||||
// 开仓操作必须提供完整参数
|
||||
if d.Action == "open_long" || d.Action == "open_short" {
|
||||
// 根据币种判断杠杆上限和仓位价值上限
|
||||
maxLeverage := 20 // 山寨币固定20倍
|
||||
// 根据币种使用配置的杠杆上限
|
||||
maxLeverage := altcoinLeverage // 山寨币使用配置的杠杆
|
||||
maxPositionValue := accountEquity * 1.5 // 山寨币最多1.5倍账户净值
|
||||
if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" {
|
||||
maxLeverage = 50 // BTC和ETH固定50倍
|
||||
maxLeverage = btcEthLeverage // BTC和ETH使用配置的杠杆
|
||||
maxPositionValue = accountEquity * 10 // BTC/ETH最多10倍账户净值
|
||||
}
|
||||
|
||||
if d.Leverage <= 0 || d.Leverage > maxLeverage {
|
||||
return fmt.Errorf("杠杆必须在1-%d之间(%s): %d", maxLeverage, d.Symbol, d.Leverage)
|
||||
return fmt.Errorf("杠杆必须在1-%d之间(%s,当前配置上限%d倍): %d", maxLeverage, d.Symbol, maxLeverage, d.Leverage)
|
||||
}
|
||||
if d.PositionSizeUSD <= 0 {
|
||||
return fmt.Errorf("仓位大小必须大于0: %.2f", d.PositionSizeUSD)
|
||||
|
||||
+17
-37
@@ -1,68 +1,48 @@
|
||||
services:
|
||||
# 后端服务
|
||||
backend:
|
||||
# Backend service (API and core logic)
|
||||
nofx:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: nofx-backend
|
||||
dockerfile: ./docker/Dockerfile.backend
|
||||
container_name: nofx-trading
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "${NOFX_BACKEND_PORT:-8080}:8080"
|
||||
volumes:
|
||||
# 挂载配置文件(必须)
|
||||
- ./config.json:/app/config.json:ro
|
||||
# 持久化决策日志
|
||||
- ./decision_logs:/app/decision_logs
|
||||
# 持久化币种池缓存
|
||||
- ./coin_pool_cache:/app/coin_pool_cache
|
||||
- /etc/localtime:/etc/localtime:ro # Sync host time
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
- TZ=${NOFX_TIMEZONE:-Asia/Shanghai} # Set timezone
|
||||
networks:
|
||||
- nofx-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
start_period: 60s
|
||||
|
||||
# 前端服务
|
||||
frontend:
|
||||
# Frontend service (static serving and proxy)
|
||||
nofx-frontend:
|
||||
build:
|
||||
context: ./web
|
||||
dockerfile: Dockerfile
|
||||
context: .
|
||||
dockerfile: ./docker/Dockerfile.frontend
|
||||
container_name: nofx-frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:80"
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
- "${NOFX_FRONTEND_PORT:-3000}:80"
|
||||
networks:
|
||||
- nofx-network
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
depends_on:
|
||||
- nofx
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
networks:
|
||||
nofx-network:
|
||||
driver: bridge
|
||||
|
||||
volumes:
|
||||
decision_logs:
|
||||
coin_pool_cache:
|
||||
driver: bridge
|
||||
@@ -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"]
|
||||
@@ -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;"]
|
||||
@@ -74,6 +74,8 @@ github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
|
||||
+87
-31
@@ -269,16 +269,20 @@ type Statistics struct {
|
||||
|
||||
// TradeOutcome 单笔交易结果
|
||||
type TradeOutcome struct {
|
||||
Symbol string `json:"symbol"` // 币种
|
||||
Side string `json:"side"` // long/short
|
||||
OpenPrice float64 `json:"open_price"` // 开仓价
|
||||
ClosePrice float64 `json:"close_price"` // 平仓价
|
||||
PnL float64 `json:"pn_l"` // 盈亏(USDT)
|
||||
PnLPct float64 `json:"pn_l_pct"` // 盈亏百分比
|
||||
Duration string `json:"duration"` // 持仓时长
|
||||
OpenTime time.Time `json:"open_time"` // 开仓时间
|
||||
CloseTime time.Time `json:"close_time"` // 平仓时间
|
||||
WasStopLoss bool `json:"was_stop_loss"` // 是否止损
|
||||
Symbol string `json:"symbol"` // 币种
|
||||
Side string `json:"side"` // long/short
|
||||
Quantity float64 `json:"quantity"` // 仓位数量
|
||||
Leverage int `json:"leverage"` // 杠杆倍数
|
||||
OpenPrice float64 `json:"open_price"` // 开仓价
|
||||
ClosePrice float64 `json:"close_price"` // 平仓价
|
||||
PositionValue float64 `json:"position_value"` // 仓位价值(quantity × openPrice)
|
||||
MarginUsed float64 `json:"margin_used"` // 保证金使用(positionValue / leverage)
|
||||
PnL float64 `json:"pn_l"` // 盈亏(USDT)
|
||||
PnLPct float64 `json:"pn_l_pct"` // 盈亏百分比(相对保证金)
|
||||
Duration string `json:"duration"` // 持仓时长
|
||||
OpenTime time.Time `json:"open_time"` // 开仓时间
|
||||
CloseTime time.Time `json:"close_time"` // 平仓时间
|
||||
WasStopLoss bool `json:"was_stop_loss"` // 是否止损
|
||||
}
|
||||
|
||||
// PerformanceAnalysis 交易表现分析
|
||||
@@ -330,7 +334,45 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
|
||||
// 追踪持仓状态:symbol_side -> {side, openPrice, openTime, quantity, leverage}
|
||||
openPositions := make(map[string]map[string]interface{})
|
||||
|
||||
// 遍历所有记录
|
||||
// 为了避免开仓记录在窗口外导致匹配失败,需要先从所有历史记录中找出未平仓的持仓
|
||||
// 获取更多历史记录来构建完整的持仓状态(使用更大的窗口)
|
||||
allRecords, err := l.GetLatestRecords(lookbackCycles * 3) // 扩大3倍窗口
|
||||
if err == nil && len(allRecords) > len(records) {
|
||||
// 先从扩大的窗口中收集所有开仓记录
|
||||
for _, record := range allRecords {
|
||||
for _, action := range record.Decisions {
|
||||
if !action.Success {
|
||||
continue
|
||||
}
|
||||
|
||||
symbol := action.Symbol
|
||||
side := ""
|
||||
if action.Action == "open_long" || action.Action == "close_long" {
|
||||
side = "long"
|
||||
} else if action.Action == "open_short" || action.Action == "close_short" {
|
||||
side = "short"
|
||||
}
|
||||
posKey := symbol + "_" + side
|
||||
|
||||
switch action.Action {
|
||||
case "open_long", "open_short":
|
||||
// 记录开仓
|
||||
openPositions[posKey] = map[string]interface{}{
|
||||
"side": side,
|
||||
"openPrice": action.Price,
|
||||
"openTime": action.Timestamp,
|
||||
"quantity": action.Quantity,
|
||||
"leverage": action.Leverage,
|
||||
}
|
||||
case "close_long", "close_short":
|
||||
// 移除已平仓记录
|
||||
delete(openPositions, posKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历分析窗口内的记录,生成交易结果
|
||||
for _, record := range records {
|
||||
for _, action := range record.Decisions {
|
||||
if !action.Success {
|
||||
@@ -348,7 +390,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
|
||||
|
||||
switch action.Action {
|
||||
case "open_long", "open_short":
|
||||
// 记录开仓(包括数量和杠杆)
|
||||
// 更新开仓记录(可能已经在预填充时记录过了)
|
||||
openPositions[posKey] = map[string]interface{}{
|
||||
"side": side,
|
||||
"openPrice": action.Price,
|
||||
@@ -358,7 +400,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
|
||||
}
|
||||
|
||||
case "close_long", "close_short":
|
||||
// 查找对应的开仓记录
|
||||
// 查找对应的开仓记录(可能来自预填充或当前窗口)
|
||||
if openPos, exists := openPositions[posKey]; exists {
|
||||
openPrice := openPos["openPrice"].(float64)
|
||||
openTime := openPos["openTime"].(time.Time)
|
||||
@@ -366,42 +408,53 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
|
||||
quantity := openPos["quantity"].(float64)
|
||||
leverage := openPos["leverage"].(int)
|
||||
|
||||
// 计算盈亏百分比
|
||||
pnlPct := 0.0
|
||||
// 计算实际盈亏(USDT)
|
||||
// 合约交易 PnL 计算:quantity × 价格差
|
||||
// 注意:杠杆不影响绝对盈亏,只影响保证金需求
|
||||
var pnl float64
|
||||
if side == "long" {
|
||||
pnlPct = ((action.Price - openPrice) / openPrice) * 100
|
||||
pnl = quantity * (action.Price - openPrice)
|
||||
} else {
|
||||
pnlPct = ((openPrice - action.Price) / openPrice) * 100
|
||||
pnl = quantity * (openPrice - action.Price)
|
||||
}
|
||||
|
||||
// 计算实际盈亏(USDT)
|
||||
// PnL = 仓位价值 × 价格变化百分比 × 杠杆倍数
|
||||
// 计算盈亏百分比(相对保证金)
|
||||
positionValue := quantity * openPrice
|
||||
pnl := positionValue * (pnlPct / 100) * float64(leverage)
|
||||
marginUsed := positionValue / float64(leverage)
|
||||
pnlPct := 0.0
|
||||
if marginUsed > 0 {
|
||||
pnlPct = (pnl / marginUsed) * 100
|
||||
}
|
||||
|
||||
// 记录交易结果
|
||||
outcome := TradeOutcome{
|
||||
Symbol: symbol,
|
||||
Side: side,
|
||||
OpenPrice: openPrice,
|
||||
ClosePrice: action.Price,
|
||||
PnL: pnl,
|
||||
PnLPct: pnlPct,
|
||||
Duration: action.Timestamp.Sub(openTime).String(),
|
||||
OpenTime: openTime,
|
||||
CloseTime: action.Timestamp,
|
||||
Symbol: symbol,
|
||||
Side: side,
|
||||
Quantity: quantity,
|
||||
Leverage: leverage,
|
||||
OpenPrice: openPrice,
|
||||
ClosePrice: action.Price,
|
||||
PositionValue: positionValue,
|
||||
MarginUsed: marginUsed,
|
||||
PnL: pnl,
|
||||
PnLPct: pnlPct,
|
||||
Duration: action.Timestamp.Sub(openTime).String(),
|
||||
OpenTime: openTime,
|
||||
CloseTime: action.Timestamp,
|
||||
}
|
||||
|
||||
analysis.RecentTrades = append(analysis.RecentTrades, outcome)
|
||||
analysis.TotalTrades++
|
||||
|
||||
// 分类交易:盈利、亏损、持平(避免将pnl=0算入亏损)
|
||||
if pnl > 0 {
|
||||
analysis.WinningTrades++
|
||||
analysis.AvgWin += pnl
|
||||
} else {
|
||||
} else if pnl < 0 {
|
||||
analysis.LosingTrades++
|
||||
analysis.AvgLoss += pnl
|
||||
}
|
||||
// pnl == 0 的交易不计入盈利也不计入亏损,但计入总交易数
|
||||
|
||||
// 更新币种统计
|
||||
if _, exists := analysis.SymbolStats[symbol]; !exists {
|
||||
@@ -414,7 +467,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
|
||||
stats.TotalPnL += pnl
|
||||
if pnl > 0 {
|
||||
stats.WinningTrades++
|
||||
} else {
|
||||
} else if pnl < 0 {
|
||||
stats.LosingTrades++
|
||||
}
|
||||
|
||||
@@ -444,6 +497,9 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna
|
||||
// 注意:totalLossAmount 是负数,所以取负号得到绝对值
|
||||
if totalLossAmount != 0 {
|
||||
analysis.ProfitFactor = totalWinAmount / (-totalLossAmount)
|
||||
} else if totalWinAmount > 0 {
|
||||
// 只有盈利没有亏损的情况,设置为一个很大的值表示完美策略
|
||||
analysis.ProfitFactor = 999.0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,10 @@ func main() {
|
||||
log.Printf("✓ 配置数据库初始化成功")
|
||||
fmt.Println()
|
||||
|
||||
// 设置默认主流币种列表
|
||||
defaultCoins := []string{"BTC", "ETH", "SOL", "BNB", "XRP", "DOGE", "ADA", "HYPE"}
|
||||
pool.SetDefaultCoins(defaultCoins)
|
||||
|
||||
// 设置是否使用默认主流币种
|
||||
pool.SetUseDefaultCoins(useDefaultCoins)
|
||||
if useDefaultCoins {
|
||||
@@ -94,7 +98,7 @@ func main() {
|
||||
|
||||
fmt.Println()
|
||||
fmt.Println("🤖 AI全权决策模式:")
|
||||
fmt.Println(" • AI将自主决定每笔交易的杠杆倍数(山寨币1-20倍,BTC/ETH最高50倍)")
|
||||
fmt.Printf(" • AI将自主决定每笔交易的杠杆倍数(山寨币最高5倍,BTC/ETH最高5倍)\n")
|
||||
fmt.Println(" • AI将自主决定每笔交易的仓位大小")
|
||||
fmt.Println(" • AI将自主设置止损和止盈价格")
|
||||
fmt.Println(" • AI将基于市场数据、技术指标、账户状态做出全面分析")
|
||||
|
||||
+71
-41
@@ -16,54 +16,77 @@ type Provider string
|
||||
const (
|
||||
ProviderDeepSeek Provider = "deepseek"
|
||||
ProviderQwen Provider = "qwen"
|
||||
ProviderCustom Provider = "custom"
|
||||
)
|
||||
|
||||
// Config AI API配置
|
||||
type Config struct {
|
||||
Provider Provider
|
||||
APIKey string
|
||||
SecretKey string // 阿里云需要
|
||||
BaseURL string
|
||||
Model string
|
||||
Timeout time.Duration
|
||||
// Client AI API配置
|
||||
type Client struct {
|
||||
Provider Provider
|
||||
APIKey string
|
||||
SecretKey string // 阿里云需要
|
||||
BaseURL string
|
||||
Model string
|
||||
Timeout time.Duration
|
||||
UseFullURL bool // 是否使用完整URL(不添加/chat/completions)
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
var defaultConfig = Config{
|
||||
Provider: ProviderDeepSeek,
|
||||
BaseURL: "https://api.deepseek.com/v1",
|
||||
Model: "deepseek-chat",
|
||||
Timeout: 120 * time.Second, // 增加到120秒,因为AI需要分析大量数据
|
||||
func New() *Client {
|
||||
// 默认配置
|
||||
var defaultClient = Client{
|
||||
Provider: ProviderDeepSeek,
|
||||
BaseURL: "https://api.deepseek.com/v1",
|
||||
Model: "deepseek-chat",
|
||||
Timeout: 120 * time.Second, // 增加到120秒,因为AI需要分析大量数据
|
||||
}
|
||||
return &defaultClient
|
||||
}
|
||||
|
||||
// SetDeepSeekAPIKey 设置DeepSeek API密钥
|
||||
func SetDeepSeekAPIKey(apiKey string) {
|
||||
defaultConfig.Provider = ProviderDeepSeek
|
||||
defaultConfig.APIKey = apiKey
|
||||
defaultConfig.BaseURL = "https://api.deepseek.com/v1"
|
||||
defaultConfig.Model = "deepseek-chat"
|
||||
func (cfg *Client) SetDeepSeekAPIKey(apiKey string) {
|
||||
cfg.Provider = ProviderDeepSeek
|
||||
cfg.APIKey = apiKey
|
||||
cfg.BaseURL = "https://api.deepseek.com/v1"
|
||||
cfg.Model = "deepseek-chat"
|
||||
}
|
||||
|
||||
// SetQwenAPIKey 设置阿里云Qwen API密钥
|
||||
func SetQwenAPIKey(apiKey, secretKey string) {
|
||||
defaultConfig.Provider = ProviderQwen
|
||||
defaultConfig.APIKey = apiKey
|
||||
defaultConfig.SecretKey = secretKey
|
||||
defaultConfig.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
defaultConfig.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max
|
||||
func (cfg *Client) SetQwenAPIKey(apiKey, secretKey string) {
|
||||
cfg.Provider = ProviderQwen
|
||||
cfg.APIKey = apiKey
|
||||
cfg.SecretKey = secretKey
|
||||
cfg.BaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
||||
cfg.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max
|
||||
}
|
||||
|
||||
// SetConfig 设置完整的AI配置(高级用户)
|
||||
func SetConfig(config Config) {
|
||||
if config.Timeout == 0 {
|
||||
config.Timeout = 30 * time.Second
|
||||
// SetCustomAPI 设置自定义OpenAI兼容API
|
||||
func (cfg *Client) SetCustomAPI(apiURL, apiKey, modelName string) {
|
||||
cfg.Provider = ProviderCustom
|
||||
cfg.APIKey = apiKey
|
||||
|
||||
// 检查URL是否以#结尾,如果是则使用完整URL(不添加/chat/completions)
|
||||
if strings.HasSuffix(apiURL, "#") {
|
||||
cfg.BaseURL = strings.TrimSuffix(apiURL, "#")
|
||||
cfg.UseFullURL = true
|
||||
} else {
|
||||
cfg.BaseURL = apiURL
|
||||
cfg.UseFullURL = false
|
||||
}
|
||||
defaultConfig = config
|
||||
|
||||
cfg.Model = modelName
|
||||
cfg.Timeout = 120 * time.Second
|
||||
}
|
||||
|
||||
// SetClient 设置完整的AI配置(高级用户)
|
||||
func (cfg *Client) SetClient(Client Client) {
|
||||
if Client.Timeout == 0 {
|
||||
Client.Timeout = 30 * time.Second
|
||||
}
|
||||
cfg = &Client
|
||||
}
|
||||
|
||||
// CallWithMessages 使用 system + user prompt 调用AI API(推荐)
|
||||
func CallWithMessages(systemPrompt, userPrompt string) (string, error) {
|
||||
if defaultConfig.APIKey == "" {
|
||||
func (cfg *Client) CallWithMessages(systemPrompt, userPrompt string) (string, error) {
|
||||
if cfg.APIKey == "" {
|
||||
return "", fmt.Errorf("AI API密钥未设置,请先调用 SetDeepSeekAPIKey() 或 SetQwenAPIKey()")
|
||||
}
|
||||
|
||||
@@ -76,7 +99,7 @@ func CallWithMessages(systemPrompt, userPrompt string) (string, error) {
|
||||
fmt.Printf("⚠️ AI API调用失败,正在重试 (%d/%d)...\n", attempt, maxRetries)
|
||||
}
|
||||
|
||||
result, err := callOnce(systemPrompt, userPrompt)
|
||||
result, err := cfg.callOnce(systemPrompt, userPrompt)
|
||||
if err == nil {
|
||||
if attempt > 1 {
|
||||
fmt.Printf("✓ AI API重试成功\n")
|
||||
@@ -102,7 +125,7 @@ func CallWithMessages(systemPrompt, userPrompt string) (string, error) {
|
||||
}
|
||||
|
||||
// callOnce 单次调用AI API(内部使用)
|
||||
func callOnce(systemPrompt, userPrompt string) (string, error) {
|
||||
func (cfg *Client) callOnce(systemPrompt, userPrompt string) (string, error) {
|
||||
// 构建 messages 数组
|
||||
messages := []map[string]string{}
|
||||
|
||||
@@ -122,7 +145,7 @@ func callOnce(systemPrompt, userPrompt string) (string, error) {
|
||||
|
||||
// 构建请求体
|
||||
requestBody := map[string]interface{}{
|
||||
"model": defaultConfig.Model,
|
||||
"model": cfg.Model,
|
||||
"messages": messages,
|
||||
"temperature": 0.5, // 降低temperature以提高JSON格式稳定性
|
||||
"max_tokens": 2000,
|
||||
@@ -137,7 +160,14 @@ func callOnce(systemPrompt, userPrompt string) (string, error) {
|
||||
}
|
||||
|
||||
// 创建HTTP请求
|
||||
url := fmt.Sprintf("%s/chat/completions", defaultConfig.BaseURL)
|
||||
var url string
|
||||
if cfg.UseFullURL {
|
||||
// 使用完整URL,不添加/chat/completions
|
||||
url = cfg.BaseURL
|
||||
} else {
|
||||
// 默认行为:添加/chat/completions
|
||||
url = fmt.Sprintf("%s/chat/completions", cfg.BaseURL)
|
||||
}
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建请求失败: %w", err)
|
||||
@@ -146,19 +176,19 @@ func callOnce(systemPrompt, userPrompt string) (string, error) {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// 根据不同的Provider设置认证方式
|
||||
switch defaultConfig.Provider {
|
||||
switch cfg.Provider {
|
||||
case ProviderDeepSeek:
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey))
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey))
|
||||
case ProviderQwen:
|
||||
// 阿里云Qwen使用API-Key认证
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey))
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey))
|
||||
// 注意:如果使用的不是兼容模式,可能需要不同的认证方式
|
||||
default:
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", defaultConfig.APIKey))
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.APIKey))
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
client := &http.Client{Timeout: defaultConfig.Timeout}
|
||||
client := &http.Client{Timeout: cfg.Timeout}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("发送请求失败: %w", err)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -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
@@ -12,7 +12,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// defaultMainstreamCoins 默认主流币种池(当AI500和OI Top都失败时使用)
|
||||
// defaultMainstreamCoins 默认主流币种池(从配置文件读取)
|
||||
var defaultMainstreamCoins = []string{
|
||||
"BTCUSDT",
|
||||
"ETHUSDT",
|
||||
@@ -83,6 +83,14 @@ func SetUseDefaultCoins(useDefault bool) {
|
||||
coinPoolConfig.UseDefaultCoins = useDefault
|
||||
}
|
||||
|
||||
// SetDefaultCoins 设置默认主流币种列表
|
||||
func SetDefaultCoins(coins []string) {
|
||||
if len(coins) > 0 {
|
||||
defaultMainstreamCoins = coins
|
||||
log.Printf("✓ 已设置默认币种池(共%d个币种): %v", len(coins), coins)
|
||||
}
|
||||
}
|
||||
|
||||
// GetCoinPool 获取币种池列表(带重试和缓存机制)
|
||||
func GetCoinPool() ([]CoinInfo, error) {
|
||||
// 优先检查是否启用默认币种列表
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 380 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 455 KiB |
@@ -1,18 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# NOFX AI Trading System - Docker Quick Start Script
|
||||
# 使用方法: ./start.sh [command]
|
||||
# Usage: ./start.sh [command]
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色定义
|
||||
# ------------------------------------------------------------------------
|
||||
# Color Definitions
|
||||
# ------------------------------------------------------------------------
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 打印带颜色的消息
|
||||
# ------------------------------------------------------------------------
|
||||
# Utility Functions: Colored Output
|
||||
# ------------------------------------------------------------------------
|
||||
print_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
@@ -29,22 +35,51 @@ print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# 检查 Docker 是否安装
|
||||
# ------------------------------------------------------------------------
|
||||
# Detection: Docker Compose Command (Backward Compatible)
|
||||
# ------------------------------------------------------------------------
|
||||
detect_compose_cmd() {
|
||||
if command -v docker compose &> /dev/null; then
|
||||
COMPOSE_CMD="docker compose"
|
||||
elif command -v docker-compose &> /dev/null; then
|
||||
COMPOSE_CMD="docker-compose"
|
||||
else
|
||||
print_error "Docker Compose 未安装!请先安装 Docker Compose"
|
||||
exit 1
|
||||
fi
|
||||
print_info "使用 Docker Compose 命令: $COMPOSE_CMD"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Validation: Docker Installation
|
||||
# ------------------------------------------------------------------------
|
||||
check_docker() {
|
||||
if ! command -v docker &> /dev/null; then
|
||||
print_error "Docker 未安装!请先安装 Docker: https://docs.docker.com/get-docker/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v docker-compose &> /dev/null; then
|
||||
print_error "Docker Compose 未安装!请先安装 Docker Compose"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_compose_cmd
|
||||
print_success "Docker 和 Docker Compose 已安装"
|
||||
}
|
||||
|
||||
# 检查配置文件
|
||||
# ------------------------------------------------------------------------
|
||||
# Validation: Environment File (.env)
|
||||
# ------------------------------------------------------------------------
|
||||
check_env() {
|
||||
if [ ! -f ".env" ]; then
|
||||
print_warning ".env 不存在,从模板复制..."
|
||||
cp .env.example .env
|
||||
print_info "请编辑 .env 填入你的环境变量配置"
|
||||
print_info "运行: nano .env 或使用其他编辑器"
|
||||
exit 1
|
||||
fi
|
||||
print_success "环境变量文件存在"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Validation: Configuration File (config.json)
|
||||
# ------------------------------------------------------------------------
|
||||
check_config() {
|
||||
if [ ! -f "config.json" ]; then
|
||||
print_warning "config.json 不存在,从模板复制..."
|
||||
@@ -56,15 +91,53 @@ check_config() {
|
||||
print_success "配置文件存在"
|
||||
}
|
||||
|
||||
# 启动服务
|
||||
# ------------------------------------------------------------------------
|
||||
# Build: Frontend (Node.js Based)
|
||||
# ------------------------------------------------------------------------
|
||||
# build_frontend() {
|
||||
# print_info "检查前端构建环境..."
|
||||
|
||||
# if ! command -v node &> /dev/null; then
|
||||
# print_error "Node.js 未安装!请先安装 Node.js"
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
# if ! command -v npm &> /dev/null; then
|
||||
# print_error "npm 未安装!请先安装 npm"
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
# print_info "正在构建前端..."
|
||||
# cd web
|
||||
|
||||
# print_info "安装 Node.js 依赖..."
|
||||
# npm install
|
||||
|
||||
# print_info "构建前端应用..."
|
||||
# npm run build
|
||||
|
||||
# cd ..
|
||||
# print_success "前端构建完成"
|
||||
# }
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Service Management: Start
|
||||
# ------------------------------------------------------------------------
|
||||
start() {
|
||||
print_info "正在启动 NOFX AI Trading System..."
|
||||
|
||||
# Auto-build frontend if missing or forced
|
||||
# if [ ! -d "web/dist" ] || [ "$1" == "--build" ]; then
|
||||
# build_frontend
|
||||
# fi
|
||||
|
||||
# Rebuild images if flag set
|
||||
if [ "$1" == "--build" ]; then
|
||||
print_info "重新构建镜像..."
|
||||
docker-compose up -d --build
|
||||
$COMPOSE_CMD up -d --build
|
||||
else
|
||||
docker-compose up -d
|
||||
print_info "启动容器..."
|
||||
$COMPOSE_CMD up -d
|
||||
fi
|
||||
|
||||
print_success "服务已启动!"
|
||||
@@ -75,60 +148,74 @@ start() {
|
||||
print_info "停止服务: ./start.sh stop"
|
||||
}
|
||||
|
||||
# 停止服务
|
||||
# ------------------------------------------------------------------------
|
||||
# Service Management: Stop
|
||||
# ------------------------------------------------------------------------
|
||||
stop() {
|
||||
print_info "正在停止服务..."
|
||||
docker-compose stop
|
||||
$COMPOSE_CMD stop
|
||||
print_success "服务已停止"
|
||||
}
|
||||
|
||||
# 重启服务
|
||||
# ------------------------------------------------------------------------
|
||||
# Service Management: Restart
|
||||
# ------------------------------------------------------------------------
|
||||
restart() {
|
||||
print_info "正在重启服务..."
|
||||
docker-compose restart
|
||||
$COMPOSE_CMD restart
|
||||
print_success "服务已重启"
|
||||
}
|
||||
|
||||
# 查看日志
|
||||
# ------------------------------------------------------------------------
|
||||
# Monitoring: Logs
|
||||
# ------------------------------------------------------------------------
|
||||
logs() {
|
||||
if [ -z "$2" ]; then
|
||||
docker-compose logs -f
|
||||
$COMPOSE_CMD logs -f
|
||||
else
|
||||
docker-compose logs -f "$2"
|
||||
$COMPOSE_CMD logs -f "$2"
|
||||
fi
|
||||
}
|
||||
|
||||
# 查看状态
|
||||
# ------------------------------------------------------------------------
|
||||
# Monitoring: Status
|
||||
# ------------------------------------------------------------------------
|
||||
status() {
|
||||
print_info "服务状态:"
|
||||
docker-compose ps
|
||||
$COMPOSE_CMD ps
|
||||
echo ""
|
||||
print_info "健康检查:"
|
||||
curl -s http://localhost:8080/health | jq '.' || echo "后端未响应"
|
||||
}
|
||||
|
||||
# 清理
|
||||
# ------------------------------------------------------------------------
|
||||
# Maintenance: Clean (Destructive)
|
||||
# ------------------------------------------------------------------------
|
||||
clean() {
|
||||
print_warning "这将删除所有容器和数据!"
|
||||
read -p "确认删除?(yes/no): " confirm
|
||||
if [ "$confirm" == "yes" ]; then
|
||||
print_info "正在清理..."
|
||||
docker-compose down -v
|
||||
$COMPOSE_CMD down -v
|
||||
print_success "清理完成"
|
||||
else
|
||||
print_info "已取消"
|
||||
fi
|
||||
}
|
||||
|
||||
# 更新
|
||||
# ------------------------------------------------------------------------
|
||||
# Maintenance: Update
|
||||
# ------------------------------------------------------------------------
|
||||
update() {
|
||||
print_info "正在更新..."
|
||||
git pull
|
||||
docker-compose up -d --build
|
||||
$COMPOSE_CMD up -d --build
|
||||
print_success "更新完成"
|
||||
}
|
||||
|
||||
# 显示帮助
|
||||
# ------------------------------------------------------------------------
|
||||
# Help: Usage Information
|
||||
# ------------------------------------------------------------------------
|
||||
show_help() {
|
||||
echo "NOFX AI Trading System - Docker 管理脚本"
|
||||
echo ""
|
||||
@@ -150,12 +237,15 @@ show_help() {
|
||||
echo " ./start.sh status # 查看状态"
|
||||
}
|
||||
|
||||
# 主函数
|
||||
# ------------------------------------------------------------------------
|
||||
# Main: Command Dispatcher
|
||||
# ------------------------------------------------------------------------
|
||||
main() {
|
||||
check_docker
|
||||
|
||||
case "${1:-start}" in
|
||||
start)
|
||||
check_env
|
||||
check_config
|
||||
start "$2"
|
||||
;;
|
||||
@@ -188,5 +278,5 @@ main() {
|
||||
esac
|
||||
}
|
||||
|
||||
# 运行主函数
|
||||
main "$@"
|
||||
# Execute Main
|
||||
main "$@"
|
||||
@@ -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
@@ -21,7 +21,7 @@ type AutoTraderConfig struct {
|
||||
AIModel string // AI模型: "qwen" 或 "deepseek"
|
||||
|
||||
// 交易平台选择
|
||||
Exchange string // "binance" 或 "hyperliquid"
|
||||
Exchange string // "binance", "hyperliquid" 或 "aster"
|
||||
|
||||
// 币安API配置
|
||||
BinanceAPIKey string
|
||||
@@ -29,8 +29,14 @@ type AutoTraderConfig struct {
|
||||
|
||||
// Hyperliquid配置
|
||||
HyperliquidPrivateKey string
|
||||
HyperliquidWalletAddr string
|
||||
HyperliquidTestnet bool
|
||||
|
||||
// Aster配置
|
||||
AsterUser string // Aster主钱包地址
|
||||
AsterSigner string // Aster API钱包地址
|
||||
AsterPrivateKey string // Aster API钱包私钥
|
||||
|
||||
CoinPoolAPIURL string
|
||||
|
||||
// AI配置
|
||||
@@ -38,12 +44,21 @@ type AutoTraderConfig struct {
|
||||
DeepSeekKey string
|
||||
QwenKey string
|
||||
|
||||
// 自定义AI API配置
|
||||
CustomAPIURL string
|
||||
CustomAPIKey string
|
||||
CustomModelName string
|
||||
|
||||
// 扫描配置
|
||||
ScanInterval time.Duration // 扫描间隔(建议3分钟)
|
||||
|
||||
// 账户配置
|
||||
InitialBalance float64 // 初始金额(用于计算盈亏,需手动设置)
|
||||
|
||||
// 杠杆配置
|
||||
BTCETHLeverage int // BTC和ETH的杠杆倍数
|
||||
AltcoinLeverage int // 山寨币的杠杆倍数
|
||||
|
||||
// 风险控制(仅作为提示,AI可自主决定)
|
||||
MaxDailyLoss float64 // 最大日亏损百分比(提示)
|
||||
MaxDrawdown float64 // 最大回撤百分比(提示)
|
||||
@@ -52,21 +67,22 @@ type AutoTraderConfig struct {
|
||||
|
||||
// AutoTrader 自动交易器
|
||||
type AutoTrader struct {
|
||||
id string // Trader唯一标识
|
||||
name string // Trader显示名称
|
||||
aiModel string // AI模型名称
|
||||
exchange string // 交易平台名称
|
||||
config AutoTraderConfig
|
||||
trader Trader // 使用Trader接口(支持多平台)
|
||||
decisionLogger *logger.DecisionLogger // 决策日志记录器
|
||||
initialBalance float64
|
||||
dailyPnL float64
|
||||
lastResetTime time.Time
|
||||
stopUntil time.Time
|
||||
isRunning bool
|
||||
startTime time.Time // 系统启动时间
|
||||
callCount int // AI调用次数
|
||||
positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒)
|
||||
id string // Trader唯一标识
|
||||
name string // Trader显示名称
|
||||
aiModel string // AI模型名称
|
||||
exchange string // 交易平台名称
|
||||
config AutoTraderConfig
|
||||
trader Trader // 使用Trader接口(支持多平台)
|
||||
mcpClient *mcp.Client
|
||||
decisionLogger *logger.DecisionLogger // 决策日志记录器
|
||||
initialBalance float64
|
||||
dailyPnL float64
|
||||
lastResetTime time.Time
|
||||
stopUntil time.Time
|
||||
isRunning bool
|
||||
startTime time.Time // 系统启动时间
|
||||
callCount int // AI调用次数
|
||||
positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒)
|
||||
}
|
||||
|
||||
// NewAutoTrader 创建自动交易器
|
||||
@@ -86,12 +102,20 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
|
||||
}
|
||||
}
|
||||
|
||||
mcpClient := mcp.New()
|
||||
|
||||
// 初始化AI
|
||||
if config.UseQwen {
|
||||
mcp.SetQwenAPIKey(config.QwenKey, "")
|
||||
if config.AIModel == "custom" {
|
||||
// 使用自定义API
|
||||
mcpClient.SetCustomAPI(config.CustomAPIURL, config.CustomAPIKey, config.CustomModelName)
|
||||
log.Printf("🤖 [%s] 使用自定义AI API: %s (模型: %s)", config.Name, config.CustomAPIURL, config.CustomModelName)
|
||||
} else if config.UseQwen || config.AIModel == "qwen" {
|
||||
// 使用Qwen
|
||||
mcpClient.SetQwenAPIKey(config.QwenKey, "")
|
||||
log.Printf("🤖 [%s] 使用阿里云Qwen AI", config.Name)
|
||||
} else {
|
||||
mcp.SetDeepSeekAPIKey(config.DeepSeekKey)
|
||||
// 默认使用DeepSeek
|
||||
mcpClient.SetDeepSeekAPIKey(config.DeepSeekKey)
|
||||
log.Printf("🤖 [%s] 使用DeepSeek AI", config.Name)
|
||||
}
|
||||
|
||||
@@ -115,10 +139,16 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
|
||||
trader = NewFuturesTrader(config.BinanceAPIKey, config.BinanceSecretKey)
|
||||
case "hyperliquid":
|
||||
log.Printf("🏦 [%s] 使用Hyperliquid交易", config.Name)
|
||||
trader, err = NewHyperliquidTrader(config.HyperliquidPrivateKey, config.HyperliquidTestnet)
|
||||
trader, err = NewHyperliquidTrader(config.HyperliquidPrivateKey, config.HyperliquidWalletAddr, config.HyperliquidTestnet)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("初始化Hyperliquid交易器失败: %w", err)
|
||||
}
|
||||
case "aster":
|
||||
log.Printf("🏦 [%s] 使用Aster交易", config.Name)
|
||||
trader, err = NewAsterTrader(config.AsterUser, config.AsterSigner, config.AsterPrivateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("初始化Aster交易器失败: %w", err)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的交易平台: %s", config.Exchange)
|
||||
}
|
||||
@@ -133,18 +163,19 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) {
|
||||
decisionLogger := logger.NewDecisionLogger(logDir)
|
||||
|
||||
return &AutoTrader{
|
||||
id: config.ID,
|
||||
name: config.Name,
|
||||
aiModel: config.AIModel,
|
||||
exchange: config.Exchange,
|
||||
config: config,
|
||||
trader: trader,
|
||||
decisionLogger: decisionLogger,
|
||||
initialBalance: config.InitialBalance,
|
||||
lastResetTime: time.Now(),
|
||||
startTime: time.Now(),
|
||||
callCount: 0,
|
||||
isRunning: false,
|
||||
id: config.ID,
|
||||
name: config.Name,
|
||||
aiModel: config.AIModel,
|
||||
exchange: config.Exchange,
|
||||
config: config,
|
||||
trader: trader,
|
||||
mcpClient: mcpClient,
|
||||
decisionLogger: decisionLogger,
|
||||
initialBalance: config.InitialBalance,
|
||||
lastResetTime: time.Now(),
|
||||
startTime: time.Now(),
|
||||
callCount: 0,
|
||||
isRunning: false,
|
||||
positionFirstSeenTime: make(map[string]int64),
|
||||
}, nil
|
||||
}
|
||||
@@ -256,7 +287,7 @@ func (at *AutoTrader) runCycle() error {
|
||||
|
||||
// 4. 调用AI获取完整决策
|
||||
log.Println("🤖 正在请求AI分析并决策...")
|
||||
decision, err := decision.GetFullDecision(ctx)
|
||||
decision, err := decision.GetFullDecision(ctx, at.mcpClient)
|
||||
|
||||
// 即使有错误,也保存思维链、决策和输入prompt(用于debug)
|
||||
if decision != nil {
|
||||
@@ -479,8 +510,9 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) {
|
||||
marginUsedPct = (totalMarginUsed / totalEquity) * 100
|
||||
}
|
||||
|
||||
// 5. 分析历史表现(最近20个周期)
|
||||
performance, err := at.decisionLogger.AnalyzePerformance(20)
|
||||
// 5. 分析历史表现(最近100个周期,避免长期持仓的交易记录丢失)
|
||||
// 假设每3分钟一个周期,100个周期 = 5小时,足够覆盖大部分交易
|
||||
performance, err := at.decisionLogger.AnalyzePerformance(100)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ 分析历史表现失败: %v", err)
|
||||
// 不影响主流程,继续执行(但设置performance为nil以避免传递错误数据)
|
||||
@@ -489,9 +521,11 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) {
|
||||
|
||||
// 6. 构建上下文
|
||||
ctx := &decision.Context{
|
||||
CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
|
||||
RuntimeMinutes: int(time.Since(at.startTime).Minutes()),
|
||||
CallCount: at.callCount,
|
||||
CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
|
||||
RuntimeMinutes: int(time.Since(at.startTime).Minutes()),
|
||||
CallCount: at.callCount,
|
||||
BTCETHLeverage: at.config.BTCETHLeverage, // 使用配置的杠杆倍数
|
||||
AltcoinLeverage: at.config.AltcoinLeverage, // 使用配置的杠杆倍数
|
||||
Account: decision.AccountInfo{
|
||||
TotalEquity: totalEquity,
|
||||
AvailableBalance: availableBalance,
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/adshao/go-binance/v2/futures"
|
||||
@@ -13,19 +14,44 @@ import (
|
||||
// FuturesTrader 币安合约交易器
|
||||
type FuturesTrader struct {
|
||||
client *futures.Client
|
||||
|
||||
// 余额缓存
|
||||
cachedBalance map[string]interface{}
|
||||
balanceCacheTime time.Time
|
||||
balanceCacheMutex sync.RWMutex
|
||||
|
||||
// 持仓缓存
|
||||
cachedPositions []map[string]interface{}
|
||||
positionsCacheTime time.Time
|
||||
positionsCacheMutex sync.RWMutex
|
||||
|
||||
// 缓存有效期(15秒)
|
||||
cacheDuration time.Duration
|
||||
}
|
||||
|
||||
// NewFuturesTrader 创建合约交易器
|
||||
func NewFuturesTrader(apiKey, secretKey string) *FuturesTrader {
|
||||
client := futures.NewClient(apiKey, secretKey)
|
||||
return &FuturesTrader{
|
||||
client: client,
|
||||
client: client,
|
||||
cacheDuration: 15 * time.Second, // 15秒缓存
|
||||
}
|
||||
}
|
||||
|
||||
// GetBalance 获取账户余额
|
||||
// GetBalance 获取账户余额(带缓存)
|
||||
func (t *FuturesTrader) GetBalance() (map[string]interface{}, error) {
|
||||
log.Printf("🔄 正在调用币安API获取账户余额...")
|
||||
// 先检查缓存是否有效
|
||||
t.balanceCacheMutex.RLock()
|
||||
if t.cachedBalance != nil && time.Since(t.balanceCacheTime) < t.cacheDuration {
|
||||
cacheAge := time.Since(t.balanceCacheTime)
|
||||
t.balanceCacheMutex.RUnlock()
|
||||
log.Printf("✓ 使用缓存的账户余额(缓存时间: %.1f秒前)", cacheAge.Seconds())
|
||||
return t.cachedBalance, nil
|
||||
}
|
||||
t.balanceCacheMutex.RUnlock()
|
||||
|
||||
// 缓存过期或不存在,调用API
|
||||
log.Printf("🔄 缓存过期,正在调用币安API获取账户余额...")
|
||||
account, err := t.client.NewGetAccountService().Do(context.Background())
|
||||
if err != nil {
|
||||
log.Printf("❌ 币安API调用失败: %v", err)
|
||||
@@ -42,11 +68,29 @@ func (t *FuturesTrader) GetBalance() (map[string]interface{}, error) {
|
||||
account.AvailableBalance,
|
||||
account.TotalUnrealizedProfit)
|
||||
|
||||
// 更新缓存
|
||||
t.balanceCacheMutex.Lock()
|
||||
t.cachedBalance = result
|
||||
t.balanceCacheTime = time.Now()
|
||||
t.balanceCacheMutex.Unlock()
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetPositions 获取所有持仓
|
||||
// GetPositions 获取所有持仓(带缓存)
|
||||
func (t *FuturesTrader) GetPositions() ([]map[string]interface{}, error) {
|
||||
// 先检查缓存是否有效
|
||||
t.positionsCacheMutex.RLock()
|
||||
if t.cachedPositions != nil && time.Since(t.positionsCacheTime) < t.cacheDuration {
|
||||
cacheAge := time.Since(t.positionsCacheTime)
|
||||
t.positionsCacheMutex.RUnlock()
|
||||
log.Printf("✓ 使用缓存的持仓信息(缓存时间: %.1f秒前)", cacheAge.Seconds())
|
||||
return t.cachedPositions, nil
|
||||
}
|
||||
t.positionsCacheMutex.RUnlock()
|
||||
|
||||
// 缓存过期或不存在,调用API
|
||||
log.Printf("🔄 缓存过期,正在调用币安API获取持仓信息...")
|
||||
positions, err := t.client.NewGetPositionRiskService().Do(context.Background())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取持仓失败: %w", err)
|
||||
@@ -78,6 +122,12 @@ func (t *FuturesTrader) GetPositions() ([]map[string]interface{}, error) {
|
||||
result = append(result, posMap)
|
||||
}
|
||||
|
||||
// 更新缓存
|
||||
t.positionsCacheMutex.Lock()
|
||||
t.cachedPositions = result
|
||||
t.positionsCacheTime = time.Now()
|
||||
t.positionsCacheMutex.Unlock()
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package trader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
@@ -20,7 +19,7 @@ type HyperliquidTrader struct {
|
||||
}
|
||||
|
||||
// NewHyperliquidTrader 创建Hyperliquid交易器
|
||||
func NewHyperliquidTrader(privateKeyHex string, testnet bool) (*HyperliquidTrader, error) {
|
||||
func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool) (*HyperliquidTrader, error) {
|
||||
// 解析私钥
|
||||
privateKey, err := crypto.HexToECDSA(privateKeyHex)
|
||||
if err != nil {
|
||||
@@ -33,13 +32,13 @@ func NewHyperliquidTrader(privateKeyHex string, testnet bool) (*HyperliquidTrade
|
||||
apiURL = hyperliquid.TestnetAPIURL
|
||||
}
|
||||
|
||||
// 从私钥生成钱包地址
|
||||
pubKey := privateKey.Public()
|
||||
publicKeyECDSA, ok := pubKey.(*ecdsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("无法转换公钥")
|
||||
}
|
||||
walletAddr := crypto.PubkeyToAddress(*publicKeyECDSA).Hex()
|
||||
// // 从私钥生成钱包地址
|
||||
// pubKey := privateKey.Public()
|
||||
// publicKeyECDSA, ok := pubKey.(*ecdsa.PublicKey)
|
||||
// if !ok {
|
||||
// return nil, fmt.Errorf("无法转换公钥")
|
||||
// }
|
||||
// walletAddr := crypto.PubkeyToAddress(*publicKeyECDSA).Hex()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -86,6 +85,7 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
|
||||
|
||||
accountValue, _ := strconv.ParseFloat(accountState.CrossMarginSummary.AccountValue, 64)
|
||||
totalMarginUsed, _ := strconv.ParseFloat(accountState.CrossMarginSummary.TotalMarginUsed, 64)
|
||||
availableBalance, _ := strconv.ParseFloat(accountState.CrossMarginSummary.AccountValue, 64)
|
||||
|
||||
// ⚠️ 关键修复:从所有持仓中累加真正的未实现盈亏
|
||||
totalUnrealizedPnl := 0.0
|
||||
@@ -99,9 +99,9 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
|
||||
// 钱包余额(已实现)= AccountValue - 未实现盈亏
|
||||
walletBalance := accountValue - totalUnrealizedPnl
|
||||
|
||||
result["totalWalletBalance"] = walletBalance // 钱包余额(已实现部分)
|
||||
result["availableBalance"] = accountValue - totalMarginUsed // 可用余额
|
||||
result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏
|
||||
result["totalWalletBalance"] = walletBalance // 钱包余额(已实现部分)
|
||||
result["availableBalance"] = availableBalance - totalMarginUsed // 可用余额
|
||||
result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏
|
||||
|
||||
log.Printf("✓ Hyperliquid API返回: 账户净值=%.2f, 钱包余额=%.2f, 可用=%.2f, 未实现盈亏=%.2f",
|
||||
accountValue,
|
||||
@@ -515,8 +515,8 @@ func (t *HyperliquidTrader) SetStopLoss(symbol string, positionSide string, quan
|
||||
order := hyperliquid.CreateOrderRequest{
|
||||
Coin: coin,
|
||||
IsBuy: isBuy,
|
||||
Size: roundedQuantity, // 使用四舍五入后的数量
|
||||
Price: roundedStopPrice, // 使用处理后的价格
|
||||
Size: roundedQuantity, // 使用四舍五入后的数量
|
||||
Price: roundedStopPrice, // 使用处理后的价格
|
||||
OrderType: hyperliquid.OrderType{
|
||||
Trigger: &hyperliquid.TriggerOrderType{
|
||||
TriggerPx: roundedStopPrice,
|
||||
@@ -552,8 +552,8 @@ func (t *HyperliquidTrader) SetTakeProfit(symbol string, positionSide string, qu
|
||||
order := hyperliquid.CreateOrderRequest{
|
||||
Coin: coin,
|
||||
IsBuy: isBuy,
|
||||
Size: roundedQuantity, // 使用四舍五入后的数量
|
||||
Price: roundedTakeProfitPrice, // 使用处理后的价格
|
||||
Size: roundedQuantity, // 使用四舍五入后的数量
|
||||
Price: roundedTakeProfitPrice, // 使用处理后的价格
|
||||
OrderType: hyperliquid.OrderType{
|
||||
Trigger: &hyperliquid.TriggerOrderType{
|
||||
TriggerPx: roundedTakeProfitPrice,
|
||||
@@ -577,7 +577,7 @@ func (t *HyperliquidTrader) SetTakeProfit(symbol string, positionSide string, qu
|
||||
func (t *HyperliquidTrader) FormatQuantity(symbol string, quantity float64) (string, error) {
|
||||
coin := convertSymbolToHyperliquid(symbol)
|
||||
szDecimals := t.getSzDecimals(coin)
|
||||
|
||||
|
||||
// 使用szDecimals格式化数量
|
||||
formatStr := fmt.Sprintf("%%.%df", szDecimals)
|
||||
return fmt.Sprintf(formatStr, quantity), nil
|
||||
@@ -604,13 +604,13 @@ func (t *HyperliquidTrader) getSzDecimals(coin string) int {
|
||||
// roundToSzDecimals 将数量四舍五入到正确的精度
|
||||
func (t *HyperliquidTrader) roundToSzDecimals(coin string, quantity float64) float64 {
|
||||
szDecimals := t.getSzDecimals(coin)
|
||||
|
||||
|
||||
// 计算倍数(10^szDecimals)
|
||||
multiplier := 1.0
|
||||
for i := 0; i < szDecimals; i++ {
|
||||
multiplier *= 10.0
|
||||
}
|
||||
|
||||
|
||||
// 四舍五入
|
||||
return float64(int(quantity*multiplier+0.5)) / multiplier
|
||||
}
|
||||
@@ -621,9 +621,9 @@ func (t *HyperliquidTrader) roundPriceToSigfigs(price float64) float64 {
|
||||
if price == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
const sigfigs = 5 // Hyperliquid标准:5位有效数字
|
||||
|
||||
|
||||
// 计算价格的数量级
|
||||
var magnitude float64
|
||||
if price < 0 {
|
||||
@@ -631,7 +631,7 @@ func (t *HyperliquidTrader) roundPriceToSigfigs(price float64) float64 {
|
||||
} else {
|
||||
magnitude = price
|
||||
}
|
||||
|
||||
|
||||
// 计算需要的倍数
|
||||
multiplier := 1.0
|
||||
for magnitude >= 10 {
|
||||
@@ -642,12 +642,12 @@ func (t *HyperliquidTrader) roundPriceToSigfigs(price float64) float64 {
|
||||
magnitude *= 10
|
||||
multiplier *= 10
|
||||
}
|
||||
|
||||
|
||||
// 应用有效数字精度
|
||||
for i := 0; i < sigfigs-1; i++ {
|
||||
multiplier *= 10
|
||||
}
|
||||
|
||||
|
||||
// 四舍五入
|
||||
rounded := float64(int(price*multiplier+0.5)) / multiplier
|
||||
return rounded
|
||||
|
||||
@@ -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
@@ -42,9 +42,9 @@ function App() {
|
||||
: null,
|
||||
() => api.getStatus(selectedTraderId),
|
||||
{
|
||||
refreshInterval: 5000,
|
||||
revalidateOnFocus: true,
|
||||
dedupingInterval: 0,
|
||||
refreshInterval: 15000, // 15秒刷新(配合后端15秒缓存)
|
||||
revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求
|
||||
dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求
|
||||
}
|
||||
);
|
||||
|
||||
@@ -54,9 +54,9 @@ function App() {
|
||||
: null,
|
||||
() => api.getAccount(selectedTraderId),
|
||||
{
|
||||
refreshInterval: 5000,
|
||||
revalidateOnFocus: true,
|
||||
dedupingInterval: 0,
|
||||
refreshInterval: 15000, // 15秒刷新(配合后端15秒缓存)
|
||||
revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求
|
||||
dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求
|
||||
}
|
||||
);
|
||||
|
||||
@@ -66,9 +66,9 @@ function App() {
|
||||
: null,
|
||||
() => api.getPositions(selectedTraderId),
|
||||
{
|
||||
refreshInterval: 5000,
|
||||
revalidateOnFocus: true,
|
||||
dedupingInterval: 0,
|
||||
refreshInterval: 15000, // 15秒刷新(配合后端15秒缓存)
|
||||
revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求
|
||||
dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求
|
||||
}
|
||||
);
|
||||
|
||||
@@ -77,7 +77,11 @@ function App() {
|
||||
? `decisions/latest-${selectedTraderId}`
|
||||
: null,
|
||||
() => api.getLatestDecisions(selectedTraderId),
|
||||
{ refreshInterval: 10000 }
|
||||
{
|
||||
refreshInterval: 30000, // 30秒刷新(决策更新频率较低)
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 20000,
|
||||
}
|
||||
);
|
||||
|
||||
const { data: stats } = useSWR<Statistics>(
|
||||
@@ -85,7 +89,11 @@ function App() {
|
||||
? `statistics-${selectedTraderId}`
|
||||
: null,
|
||||
() => api.getStatistics(selectedTraderId),
|
||||
{ refreshInterval: 10000 }
|
||||
{
|
||||
refreshInterval: 30000, // 30秒刷新(统计数据更新频率较低)
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 20000,
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -101,55 +109,104 @@ function App() {
|
||||
<div className="min-h-screen" style={{ background: '#0B0E11', color: '#EAECEF' }}>
|
||||
{/* Header - Binance Style */}
|
||||
<header className="glass sticky top-0 z-50 backdrop-blur-xl">
|
||||
<div className="max-w-[1920px] mx-auto px-6 py-4">
|
||||
<div className="relative flex items-center">
|
||||
{/* Left - Logo and Title */}
|
||||
<div className="flex items-center gap-3">
|
||||
<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="max-w-[1920px] mx-auto px-3 sm:px-6 py-3 sm:py-4">
|
||||
{/* Mobile: Two rows, Desktop: Single row */}
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
{/* Left: Logo and Title */}
|
||||
<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>
|
||||
<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)}
|
||||
</h1>
|
||||
<p className="text-xs mono" style={{ color: '#848E9C' }}>
|
||||
<p className="text-xs mono hidden sm:block" style={{ color: '#848E9C' }}>
|
||||
{t('subtitle', language)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Center - Page Toggle (absolutely positioned) */}
|
||||
<div className="absolute left-1/2 transform -translate-x-1/2 flex gap-1 rounded p-1" style={{ background: '#1E2329' }}>
|
||||
<button
|
||||
onClick={() => setCurrentPage('traders')}
|
||||
className={`px-4 py-2 rounded text-sm font-semibold transition-all`}
|
||||
style={currentPage === 'traders'
|
||||
? { background: '#F0B90B', color: '#000' }
|
||||
: { background: 'transparent', color: '#848E9C' }
|
||||
}
|
||||
|
||||
{/* Right: Controls - Wrap on mobile */}
|
||||
<div className="flex items-center gap-2 flex-wrap md:flex-nowrap">
|
||||
{/* GitHub Link - Hidden on mobile, icon only on tablet */}
|
||||
<a
|
||||
href="https://github.com/tinkle-community/nofx"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('trader')}
|
||||
className={`px-4 py-2 rounded text-sm font-semibold transition-all`}
|
||||
style={currentPage === 'trader'
|
||||
? { background: '#F0B90B', color: '#000' }
|
||||
: { background: 'transparent', color: '#848E9C' }
|
||||
}
|
||||
>
|
||||
{t('tradingPanel', language)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Right - Actions */}
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
<svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor">
|
||||
<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"/>
|
||||
</svg>
|
||||
<span className="hidden md:inline">GitHub</span>
|
||||
</a>
|
||||
|
||||
{/* Language Toggle */}
|
||||
<div className="flex gap-0.5 sm:gap-1 rounded p-0.5 sm:p-1" style={{ background: '#1E2329' }}>
|
||||
<button
|
||||
onClick={() => setLanguage('zh')}
|
||||
className="px-2 sm:px-3 py-1 sm: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-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) */}
|
||||
{currentPage === 'trader' && traders && traders.length > 0 && (
|
||||
<select
|
||||
value={selectedTraderId}
|
||||
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' }}
|
||||
>
|
||||
{traders.map((trader) => (
|
||||
@@ -160,34 +217,10 @@ function App() {
|
||||
</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) */}
|
||||
{currentPage === 'trader' && status && (
|
||||
<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
|
||||
? { 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)' }
|
||||
@@ -323,23 +356,23 @@ function TraderDetailsPage({
|
||||
title={t('totalEquity', language)}
|
||||
value={`${account?.total_equity?.toFixed(2) || '0.00'} USDT`}
|
||||
change={account?.total_pnl_pct || 0}
|
||||
positive={account ? (account.total_pnl || 0) > 0 : false}
|
||||
positive={(account?.total_pnl ?? 0) > 0}
|
||||
/>
|
||||
<StatCard
|
||||
title={t('availableBalance', language)}
|
||||
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
|
||||
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}
|
||||
positive={account ? (account.total_pnl || 0) >= 0 : false}
|
||||
positive={(account?.total_pnl ?? 0) >= 0}
|
||||
/>
|
||||
<StatCard
|
||||
title={t('positions', language)}
|
||||
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>
|
||||
|
||||
@@ -511,7 +544,7 @@ function StatCard({
|
||||
|
||||
// Decision Card Component with CoT Trace - Binance Style
|
||||
function DecisionCard({ decision, language }: { decision: DecisionRecord; language: Language }) {
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
const [showInputPrompt, setShowInputPrompt] = useState(false);
|
||||
const [showCoT, setShowCoT] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -535,20 +568,20 @@ function DecisionCard({ decision, language }: { decision: DecisionRecord; langua
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Input Prompt - Collapsible */}
|
||||
{(decision as any).input_prompt && (
|
||||
{/* Input Prompt - Collapsible */}
|
||||
{decision.input_prompt && (
|
||||
<div className="mb-3">
|
||||
<button
|
||||
onClick={() => setShowInput(!showInput)}
|
||||
onClick={() => setShowInputPrompt(!showInputPrompt)}
|
||||
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="text-xs">{showInput ? t('collapse', language) : t('expand', language)}</span>
|
||||
<span className="text-xs">{showInputPrompt ? t('collapse', language) : t('expand', language)}</span>
|
||||
</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' }}>
|
||||
{(decision as any).input_prompt}
|
||||
{decision.input_prompt}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import useSWR from 'swr';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { t } from '../i18n/translations';
|
||||
import { api } from '../lib/api';
|
||||
|
||||
interface TradeOutcome {
|
||||
symbol: string;
|
||||
side: string;
|
||||
quantity: number;
|
||||
leverage: number;
|
||||
open_price: number;
|
||||
close_price: number;
|
||||
position_value: number;
|
||||
margin_used: number;
|
||||
pn_l: number;
|
||||
pn_l_pct: number;
|
||||
duration: string;
|
||||
@@ -44,14 +49,16 @@ interface AILearningProps {
|
||||
traderId: string;
|
||||
}
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then(res => res.json());
|
||||
|
||||
export default function AILearning({ traderId }: AILearningProps) {
|
||||
const { language } = useLanguage();
|
||||
const { data: performance, error } = useSWR<PerformanceAnalysis>(
|
||||
`http://localhost:8080/api/performance?trader_id=${traderId}`,
|
||||
fetcher,
|
||||
{ refreshInterval: 10000 }
|
||||
traderId ? `performance-${traderId}` : 'performance',
|
||||
() => api.getPerformance(traderId),
|
||||
{
|
||||
refreshInterval: 30000, // 30秒刷新(AI学习分析数据更新频率较低)
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 20000,
|
||||
}
|
||||
);
|
||||
|
||||
if (error) {
|
||||
@@ -555,6 +562,34 @@ export default function AILearning({ traderId }: AILearningProps) {
|
||||
</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={{
|
||||
background: isProfitable ? 'rgba(16, 185, 129, 0.1)' : 'rgba(248, 113, 113, 0.1)'
|
||||
}}>
|
||||
|
||||
@@ -34,7 +34,9 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
||||
traderId ? `equity-history-${traderId}` : 'equity-history',
|
||||
() => api.getEquityHistory(traderId),
|
||||
{
|
||||
refreshInterval: 10000, // 每10秒刷新
|
||||
refreshInterval: 30000, // 30秒刷新(历史数据更新频率较低)
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 20000,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -42,7 +44,9 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
||||
traderId ? `account-${traderId}` : 'account',
|
||||
() => api.getAccount(traderId),
|
||||
{
|
||||
refreshInterval: 5000,
|
||||
refreshInterval: 15000, // 15秒刷新(配合后端缓存)
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 10000,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -60,7 +64,10 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (!history || history.length === 0) {
|
||||
// 过滤掉无效数据:total_equity为0或小于1的数据点(API失败导致)
|
||||
const validHistory = history?.filter(point => point.total_equity > 1) || [];
|
||||
|
||||
if (!validHistory || validHistory.length === 0) {
|
||||
return (
|
||||
<div className="binance-card p-6">
|
||||
<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个
|
||||
const MAX_DISPLAY_POINTS = 2000;
|
||||
const displayHistory = history.length > MAX_DISPLAY_POINTS
|
||||
? history.slice(-MAX_DISPLAY_POINTS)
|
||||
: history;
|
||||
const displayHistory = validHistory.length > MAX_DISPLAY_POINTS
|
||||
? validHistory.slice(-MAX_DISPLAY_POINTS)
|
||||
: validHistory;
|
||||
|
||||
// 计算初始余额(使用第一个数据点)
|
||||
const initialBalance = history[0]?.total_equity || 1000;
|
||||
// 计算初始余额(使用第一个有效数据点,如果无数据则从account获取,最后才用默认值)
|
||||
const initialBalance = validHistory[0]?.total_equity
|
||||
|| account?.total_equity
|
||||
|| 100; // 默认值改为100,与常见配置一致
|
||||
|
||||
// 转换数据格式
|
||||
const chartData = displayHistory.map((point) => {
|
||||
@@ -152,19 +161,19 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="binance-card p-5 animate-fade-in">
|
||||
<div className="binance-card p-3 sm:p-5 animate-fade-in">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold mb-2" style={{ color: '#EAECEF' }}>{t('accountEquityCurve', language)}</h3>
|
||||
<div className="flex items-baseline gap-4">
|
||||
<span className="text-3xl font-bold mono" style={{ color: '#EAECEF' }}>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between mb-4">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-base sm:text-lg font-bold mb-2" style={{ color: '#EAECEF' }}>{t('accountEquityCurve', language)}</h3>
|
||||
<div className="flex flex-col sm:flex-row sm:items-baseline gap-2 sm:gap-4">
|
||||
<span className="text-2xl sm:text-3xl font-bold mono" style={{ color: '#EAECEF' }}>
|
||||
{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>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<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={{
|
||||
color: isProfit ? '#0ECB81' : '#F6465D',
|
||||
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 ? '+' : ''}
|
||||
{currentValue.raw_pnl_pct}%
|
||||
</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)
|
||||
</span>
|
||||
</div>
|
||||
@@ -182,10 +191,10 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
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'
|
||||
? { background: '#F0B90B', color: '#000', boxShadow: '0 2px 8px rgba(240, 185, 11, 0.4)' }
|
||||
: { background: 'transparent', color: '#848E9C' }
|
||||
@@ -195,7 +204,7 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
||||
</button>
|
||||
<button
|
||||
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'
|
||||
? { background: '#F0B90B', color: '#000', boxShadow: '0 2px 8px rgba(240, 185, 11, 0.4)' }
|
||||
: { background: 'transparent', color: '#848E9C' }
|
||||
@@ -248,39 +257,40 @@ export function EquityChart({ traderId }: EquityChartProps) {
|
||||
}}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
type="natural"
|
||||
dataKey="value"
|
||||
stroke="url(#colorGradient)"
|
||||
strokeWidth={2.5}
|
||||
strokeWidth={3}
|
||||
dot={chartData.length > 50 ? false : { fill: '#F0B90B', r: 3 }}
|
||||
activeDot={{ r: 6, fill: '#FCD535', stroke: '#F0B90B', strokeWidth: 2 }}
|
||||
connectNulls={true}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* 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="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
|
||||
</div>
|
||||
</div>
|
||||
<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-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
|
||||
</div>
|
||||
</div>
|
||||
<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-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 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-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
||||
{history.length > MAX_DISPLAY_POINTS
|
||||
<div className="text-xs sm:text-sm font-bold mono" style={{ color: '#EAECEF' }}>
|
||||
{validHistory.length > MAX_DISPLAY_POINTS
|
||||
? `${t('recent', language)} ${MAX_DISPLAY_POINTS}`
|
||||
: t('allData', language)
|
||||
}
|
||||
|
||||
@@ -161,4 +161,14 @@ export const api = {
|
||||
if (!res.ok) throw new Error('获取历史数据失败');
|
||||
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();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -64,6 +64,7 @@ export interface AccountSnapshot {
|
||||
export interface DecisionRecord {
|
||||
timestamp: string;
|
||||
cycle_number: number;
|
||||
input_prompt: string;
|
||||
cot_trace: string;
|
||||
decision_json: string;
|
||||
account_state: AccountSnapshot;
|
||||
|
||||
@@ -56,6 +56,7 @@ export interface DecisionAction {
|
||||
export interface DecisionRecord {
|
||||
timestamp: string;
|
||||
cycle_number: number;
|
||||
input_prompt: string;
|
||||
cot_trace: string;
|
||||
decision_json: string;
|
||||
account_state: {
|
||||
|
||||
@@ -4,6 +4,7 @@ import react from '@vitejs/plugin-react'
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user