docs: 重构文档结构,使用 zh/ 和 en/ 目录区分中英文文档

- 创建 docs/zh/ 和 docs/en/ 目录结构
- 将所有中文文档移动到 docs/zh/
- 创建主要文档的英文版本:
  - DEPLOYMENT.md (651行)
  - DEVELOPMENT.md (514行)
  - VERSION_MANAGEMENT.md (已有)
- 更新所有文档中的内部链接
- 更新 README.md 和 README_EN.md 中的文档链接
- 在文档中添加中英文版本互链
This commit is contained in:
WrBug
2025-12-07 18:01:11 +08:00
parent 62d13001cd
commit a222c9a52f
19 changed files with 1541 additions and 20 deletions
+652
View File
@@ -0,0 +1,652 @@
# PolyHermes 部署文档
本文档介绍如何部署 PolyHermes 项目,包括后端和前端的不同部署方式。
## 目录
- [一体化部署(推荐)](#一体化部署推荐)
- [使用 Docker Hub 镜像](#使用-docker-hub-镜像推荐生产环境首选)
- [使用外部 Nginx 反向代理](#使用外部-nginx-反向代理生产环境推荐)
- [后端部署](#后端部署)
- [Java 直接部署](#java-直接部署)
- [Docker 部署](#docker-部署)
- [前端部署](#前端部署)
- [环境配置](#环境配置)
- [常见问题](#常见问题)
## 一体化部署(推荐)
将前后端一起部署到一个 Docker 容器中,使用 Nginx 提供前端静态文件并代理后端 API。
### 前置要求
- Docker 20.10+
- Docker Compose 2.0+
### 部署步骤
1. **使用 Docker Hub 镜像(推荐,生产环境首选)**
使用官方构建的 Docker 镜像,无需本地构建,快速部署。
**方式 1:独立部署(无需 clone 代码,推荐生产环境)**
适用于生产环境,无需下载项目代码,只需配置文件即可部署。
```bash
# 1. 创建部署目录
mkdir polyhermes && cd polyhermes
# 2. 下载生产环境配置文件
# 从 GitHub 下载 docker-compose.prod.yml 和 docker-compose.prod.env.example
curl -O https://raw.githubusercontent.com/WrBug/PolyHermes/main/docker-compose.prod.yml
curl -O https://raw.githubusercontent.com/WrBug/PolyHermes/main/docker-compose.prod.env.example
# 3. 创建配置文件
cp docker-compose.prod.env.example .env
# 4. 编辑 .env 文件,修改以下必需配置:
# - DB_PASSWORD: 数据库密码(建议使用强密码)
# - JWT_SECRET: JWT 密钥(使用 openssl rand -hex 64 生成)
# - ADMIN_RESET_PASSWORD_KEY: 管理员密码重置密钥(使用 openssl rand -hex 32 生成)
#
# 生成随机密钥示例:
# openssl rand -hex 64 # 用于 JWT_SECRET
# openssl rand -hex 32 # 用于 ADMIN_RESET_PASSWORD_KEY
# 5. 启动服务
docker-compose -f docker-compose.prod.yml up -d
# 6. 查看日志
docker-compose -f docker-compose.prod.yml logs -f
# 7. 停止服务
docker-compose -f docker-compose.prod.yml down
```
**方式 2:使用部署脚本(需要 clone 代码)**
```bash
# 如果已经 clone 了代码
./deploy.sh --use-docker-hub
```
**方式 3:修改现有 docker-compose.yml**
```bash
# 1. 修改 docker-compose.yml
# 取消注释:image: wrbug/polyhermes:latest
# 注释掉 build 部分
# 2. 创建 .env 文件(见下方环境配置)
# 3. 启动服务
docker-compose up -d
```
**优势**
- ✅ 无需本地构建,快速部署
- ✅ 无需 clone 代码,只需配置文件即可部署
- ✅ 使用官方构建的镜像,包含正确的版本号
- ✅ 支持多架构(amd64、arm64),自动选择匹配的架构
- ✅ 生产环境推荐方式
**拉取特定版本**
```bash
# 修改 docker-compose.prod.yml 中的镜像标签
# image: wrbug/polyhermes:v1.0.0
# 或使用环境变量
export IMAGE_TAG=v1.0.0
# 在 docker-compose.prod.yml 中使用: image: wrbug/polyhermes:${IMAGE_TAG:-latest}
```
2. **本地构建部署(开发环境)**
适用于开发环境或需要自定义构建的场景。
```bash
# 使用部署脚本
./deploy.sh
```
脚本会自动:
- 检查 Docker 环境
- 创建 `.env` 配置文件(如果不存在)
- 构建 Docker 镜像(包含前后端)
- 启动服务(应用 + MySQL
**注意**:本地构建的版本号会显示为 `dev`
3. **手动部署**
```bash
# 创建 .env 文件
cat > .env <<EOF
DB_URL=jdbc:mysql://mysql:3306/polyhermes?useSSL=false&serverTimezone=UTC&characterEncoding=utf8&allowPublicKeyRetrieval=true
DB_USERNAME=root
DB_PASSWORD=your_password_here
SPRING_PROFILES_ACTIVE=prod
SERVER_PORT=80
POLYGON_RPC_URL=https://polygon-rpc.com
JWT_SECRET=your-jwt-secret-key-change-in-production
ADMIN_RESET_PASSWORD_KEY=your-admin-reset-key-change-in-production
EOF
# 构建并启动
docker-compose build
docker-compose up -d
# 查看日志
docker-compose logs -f
# 停止服务
docker-compose down
```
4. **访问应用**
- 前端和后端统一访问:`http://localhost:80`
- Nginx 自动处理:
- `/api/*` → 后端 API`localhost:8000`
- `/ws` → 后端 WebSocket`localhost:8000`
- 其他路径 → 前端静态文件
### 架构说明
```
用户请求
Nginx (端口 80)
├─ /api/* → 后端服务 (localhost:8000)
├─ /ws → 后端 WebSocket (localhost:8000)
└─ /* → 前端静态文件 (/usr/share/nginx/html)
```
### 优势
- ✅ 单一容器,简化部署
- ✅ 统一端口,无需配置 CORS
- ✅ 自动处理前后端路由
- ✅ 生产环境就绪
### 使用外部 Nginx 反向代理(生产环境推荐)
在生产环境中,建议在 Docker 容器外部部署 Nginx 作为反向代理,用于:
- **SSL/TLS 终止**:处理 HTTPS 请求
- **域名绑定**:绑定自定义域名
- **负载均衡**:支持多个后端实例
- **更灵活的配置**:更细粒度的控制
**部署架构**
```
用户请求 (HTTPS)
外部 Nginx (443) - SSL 终止
Docker 容器 (80) - 内部 Nginx + 后端
├─ /api/* → 后端服务 (localhost:8000)
├─ /ws → 后端 WebSocket (localhost:8000)
└─ /* → 前端静态文件
```
**部署步骤**
1. **部署 Docker 容器**
```bash
# 使用 docker-compose.prod.yml 部署
docker-compose -f docker-compose.prod.yml up -d
```
2. **配置外部 Nginx**
```bash
# 1. 下载 Nginx 配置示例
curl -O https://raw.githubusercontent.com/WrBug/PolyHermes/main/docs/zh/nginx-reverse-proxy.conf
# 2. 复制到 Nginx 配置目录
sudo cp nginx-reverse-proxy.conf /etc/nginx/sites-available/polyhermes
# 3. 编辑配置文件,修改域名和 SSL 证书路径
sudo nano /etc/nginx/sites-available/polyhermes
# 修改以下内容:
# - server_name: 改为你的域名
# - ssl_certificate: SSL 证书路径
# - ssl_certificate_key: SSL 私钥路径
# - upstream server: 如果 Docker 容器端口不是 80,需要修改
# 4. 创建软链接
sudo ln -s /etc/nginx/sites-available/polyhermes /etc/nginx/sites-enabled/
# 5. 测试配置
sudo nginx -t
# 6. 重载配置
sudo systemctl reload nginx
```
3. **配置 SSL 证书(使用 Let's Encrypt**
```bash
# 安装 Certbot
sudo apt-get update
sudo apt-get install certbot python3-certbot-nginx
# 获取 SSL 证书
sudo certbot --nginx -d your-domain.com -d www.your-domain.com
# 证书会自动配置到 Nginx,并设置自动续期
```
4. **修改 Docker 端口映射(可选)**
如果使用外部 Nginx,可以将 Docker 容器的端口改为内部端口,不对外暴露:
```yaml
# 在 docker-compose.prod.yml 中
ports:
- "127.0.0.1:80:80" # 只绑定到本地,不对外暴露
```
**Nginx 配置说明**
- 配置文件位置:`docs/zh/nginx-reverse-proxy.conf`
- 支持 HTTPSSSL/TLS
- 支持 WebSocket 代理
- 包含安全头设置
- 支持负载均衡(可配置多个后端)
详细配置示例请参考:[Nginx 反向代理配置](nginx-reverse-proxy.conf)
> 📖 **English Version**: [Deployment Guide (English)](../en/DEPLOYMENT.md)
## 后端部署
### Java 直接部署
#### 前置要求
- JDK 17+
- MySQL 8.0+
- Gradle 7.5+(或使用 Gradle Wrapper
#### 部署步骤
1. **构建应用**
```bash
cd backend
./gradlew clean bootJar
```
构建产物位于 `build/libs/polyhermes-backend-1.0.0.jar`
2. **使用部署脚本(推荐)**
```bash
# 构建并创建部署文件
./deploy.sh java
# 或仅构建
./deploy.sh build
```
脚本会自动:
- 检查 Java 环境
- 构建应用
- 创建部署目录和启动脚本
- 生成 systemd 服务文件(可选)
3. **手动启动**
```bash
# 开发环境
java -jar build/libs/polyhermes-backend-1.0.0.jar --spring.profiles.active=dev
# 生产环境
java -jar build/libs/polyhermes-backend-1.0.0.jar --spring.profiles.active=prod
```
4. **使用 systemd 管理(Linux**
```bash
# 复制服务文件
sudo cp deploy/polyhermes-backend.service /etc/systemd/system/
# 编辑服务文件,修改路径和用户
sudo nano /etc/systemd/system/polyhermes-backend.service
# 启动服务
sudo systemctl daemon-reload
sudo systemctl enable polyhermes-backend
sudo systemctl start polyhermes-backend
# 查看日志
sudo journalctl -u polyhermes-backend -f
```
### Docker 部署
#### 前置要求
- Docker 20.10+
- Docker Compose 2.0+
#### 部署步骤
1. **使用部署脚本(推荐)**
```bash
cd backend
./deploy.sh docker
```
脚本会自动:
- 检查 Docker 环境
- 创建 `.env` 配置文件(如果不存在)
- 构建 Docker 镜像
- 启动服务
2. **手动部署**
```bash
# 创建 .env 文件
cat > .env <<EOF
DB_URL=jdbc:mysql://mysql:3306/polyhermes?useSSL=false&serverTimezone=UTC&characterEncoding=utf8&allowPublicKeyRetrieval=true
DB_USERNAME=root
DB_PASSWORD=your_password_here
SPRING_PROFILES_ACTIVE=prod
SERVER_PORT=8000
POLYGON_RPC_URL=https://polygon-rpc.com
JWT_SECRET=your-jwt-secret-key-change-in-production
ADMIN_RESET_PASSWORD_KEY=your-admin-reset-key-change-in-production
EOF
# 构建并启动
docker-compose up -d
# 查看日志
docker-compose logs -f
# 停止服务
docker-compose down
```
3. **仅构建镜像**
```bash
docker build -t polyhermes-backend:latest .
```
4. **运行容器**
```bash
docker run -d \
--name polyhermes-backend \
-p 8000:8000 \
-e SPRING_PROFILES_ACTIVE=prod \
-e DB_URL=jdbc:mysql://host.docker.internal:3306/polyhermes?useSSL=false&allowPublicKeyRetrieval=true \
-e DB_USERNAME=root \
-e DB_PASSWORD=your_password \
-e JWT_SECRET=your-jwt-secret \
polyhermes-backend:latest
```
## 前端部署
### 构建步骤
1. **使用构建脚本(推荐)**
```bash
cd frontend
# 使用默认后端地址(http://127.0.0.1:8000
./build.sh
# 或指定自定义后端地址
./build.sh --api-url http://your-backend-server.com:8000
# 或使用环境变量
VITE_API_URL=http://your-backend-server.com:8000 ./build.sh
```
2. **手动构建**
```bash
cd frontend
# 创建环境配置文件
cat > .env.production <<EOF
VITE_API_URL=http://your-backend-server.com:8000
VITE_WS_URL=ws://your-backend-server.com:8000
EOF
# 安装依赖(首次)
npm install
# 构建
npm run build
```
构建产物位于 `dist/` 目录。
### 部署方式
#### 方式1Nginx 部署
```nginx
server {
listen 80;
server_name your-domain.com;
root /path/to/frontend/dist;
index index.html;
# API 代理
location /api {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
# WebSocket 代理
location /ws {
proxy_pass http://localhost:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
# 前端路由(SPA
location / {
try_files $uri $uri/ /index.html;
}
}
```
#### 方式2Apache 部署
```apache
<VirtualHost *:80>
ServerName your-domain.com
DocumentRoot /path/to/frontend/dist
# API 代理
ProxyPass /api http://localhost:8000/api
ProxyPassReverse /api http://localhost:8000/api
# WebSocket 代理
ProxyPass /ws ws://localhost:8000/ws
ProxyPassReverse /ws ws://localhost:8000/ws
# 前端路由(SPA
<Directory /path/to/frontend/dist>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</Directory>
</VirtualHost>
```
#### 方式3:使用 serve(开发/测试)
```bash
# 安装 serve
npm install -g serve
# 启动服务
serve -s dist -l 3000
```
## 环境配置
### 后端环境变量
| 变量名 | 说明 | 默认值 | 必需 |
|--------|------|--------|------|
| `SPRING_PROFILES_ACTIVE` | Spring Profile | `dev` | 否 |
| `DB_URL` | 数据库连接 URL | - | 是(生产) |
| `DB_USERNAME` | 数据库用户名 | `root` | 是(生产) |
| `DB_PASSWORD` | 数据库密码 | - | 是(生产) |
| `SERVER_PORT` | 服务器端口 | `8000` | 否 |
| `POLYGON_RPC_URL` | Polygon RPC 地址 | `https://polygon-rpc.com` | 否 |
| `JWT_SECRET` | JWT 密钥 | - | 是(生产) |
| `ADMIN_RESET_PASSWORD_KEY` | 管理员密码重置密钥 | - | 是(生产) |
### 前端环境变量
| 变量名 | 说明 | 默认值 |
|--------|------|--------|
| `VITE_API_URL` | 后端 API 地址 | `http://127.0.0.1:8000` |
| `VITE_WS_URL` | WebSocket 地址 | `ws://127.0.0.1:8000` |
### 配置文件说明
#### 后端配置文件
- `application.properties` - 基础配置(所有环境共享)
- `application-dev.properties` - 开发环境配置
- `application-prod.properties` - 生产环境配置
通过 `--spring.profiles.active=prod` 或环境变量 `SPRING_PROFILES_ACTIVE=prod` 切换环境。
#### 前端环境变量
Vite 使用 `.env.production` 文件在构建时注入环境变量。构建脚本会自动创建此文件。
## 常见问题
### 1. 数据库连接失败
**问题**: 后端无法连接数据库
**解决方案**:
- 检查数据库服务是否运行
- 检查数据库连接 URL、用户名、密码是否正确
- 检查防火墙是否允许连接
- 对于 Docker 部署,确保使用正确的数据库地址(`mysql` 而非 `localhost`
### 2. 前端无法连接后端
**问题**: 前端请求后端 API 失败
**解决方案**:
- 检查后端服务是否运行
- 检查 `VITE_API_URL` 配置是否正确
- 检查 CORS 配置(如果跨域)
- 检查网络连接和防火墙
### 3. WebSocket 连接失败
**问题**: WebSocket 无法建立连接
**解决方案**:
- 检查 `VITE_WS_URL` 配置是否正确
- 检查 WebSocket 代理配置(Nginx/Apache
- 检查防火墙是否允许 WebSocket 连接
- 检查后端 WebSocket 服务是否正常
### 4. Docker 容器无法访问数据库
**问题**: Docker 容器中的后端无法连接宿主机数据库
**解决方案**:
- 使用 `host.docker.internal` 作为数据库地址(Mac/Windows
- 使用 Docker 网络连接(推荐使用 docker-compose
- 检查数据库是否允许远程连接
### 5. 构建失败
**问题**: 前端或后端构建失败
**解决方案**:
- 检查 Node.js 版本(需要 18+
- 检查 Java 版本(需要 17+)
- 清理缓存后重新构建:
```bash
# 前端
rm -rf node_modules dist
npm install
npm run build
# 后端
./gradlew clean build
```
## 生产环境检查清单
- [ ] 修改所有默认密码和密钥(JWT_SECRET、ADMIN_RESET_PASSWORD_KEY、数据库密码)
- [ ] 配置正确的数据库连接(使用 SSL)
- [ ] 设置正确的 Spring Profile`prod`
- [ ] 配置正确的后端 API 地址(前端)
- [ ] 配置反向代理(Nginx/Apache
- [ ] 配置 HTTPS(生产环境推荐)
- [ ] 配置防火墙规则
- [ ] 设置日志轮转
- [ ] 配置监控和告警
- [ ] 定期备份数据库
## 性能优化建议
### 后端
- 调整 JVM 参数(堆内存、GC 策略)
- 配置数据库连接池大小
- 启用 HTTP 压缩
- 配置缓存策略
### 前端
- 启用 Gzip 压缩(Nginx
- 配置静态资源缓存
- 使用 CDN 加速
- 启用 HTTP/2
## 安全建议
- 使用 HTTPS(生产环境必须)
- 配置 CORS 白名单
- 定期更新依赖包
- 使用强密码和密钥
- 限制数据库访问权限
- 配置防火墙规则
- 定期备份数据
- 监控异常访问
## 技术支持
如有问题,请提交 Issue 到 [GitHub](https://github.com/WrBug/PolyHermes) 或联系 [Twitter](https://x.com/quant_tr)。
+514
View File
@@ -0,0 +1,514 @@
# PolyHermes 开发文档
本文档介绍 PolyHermes 项目的开发指南,包括项目结构、开发环境配置、代码规范、API 接口等。
## 📋 目录
- [项目结构](#项目结构)
- [开发环境配置](#开发环境配置)
- [代码规范](#代码规范)
- [API 接口文档](#api-接口文档)
- [数据库设计](#数据库设计)
- [前端开发指南](#前端开发指南)
- [后端开发指南](#后端开发指南)
- [常见问题](#常见问题)
## 📦 项目结构
```
polyhermes/
├── backend/ # 后端服务
│ ├── src/main/kotlin/
│ │ └── com/wrbug/polymarketbot/
│ │ ├── api/ # API 接口定义(Retrofit
│ │ ├── config/ # 配置类
│ │ ├── controller/ # REST 控制器
│ │ ├── dto/ # 数据传输对象
│ │ ├── entity/ # 数据库实体
│ │ ├── repository/ # 数据访问层
│ │ ├── service/ # 业务逻辑服务
│ │ ├── util/ # 工具类
│ │ └── websocket/ # WebSocket 处理
│ └── src/main/resources/
│ ├── application.properties
│ └── db/migration/ # Flyway 数据库迁移脚本
├── frontend/ # 前端应用
│ ├── src/
│ │ ├── components/ # 公共组件
│ │ ├── pages/ # 页面组件
│ │ ├── services/ # API 服务
│ │ ├── store/ # 状态管理(Zustand
│ │ ├── types/ # TypeScript 类型定义
│ │ ├── utils/ # 工具函数
│ │ ├── hooks/ # React Hooks
│ │ ├── locales/ # 多语言资源
│ │ └── styles/ # 样式文件
│ └── public/ # 静态资源
├── docs/ # 文档
│ ├── DEPLOYMENT.md # 部署文档
│ ├── VERSION_MANAGEMENT.md # 版本号管理文档
│ ├── copy-trading-requirements.md # 跟单系统需求文档
│ └── ... # 其他文档
├── .github/workflows/ # GitHub Actions 工作流
└── README.md # 项目说明
```
## 🛠️ 开发环境配置
### 前置要求
- **JDK**: 17+
- **Node.js**: 18+
- **MySQL**: 8.0+
- **Gradle**: 7.5+(或使用 Gradle Wrapper
- **Docker**: 20.10+(可选,用于容器化部署)
### 后端开发环境
1. **克隆仓库**
```bash
git clone https://github.com/WrBug/PolyHermes.git
cd PolyHermes
```
2. **配置数据库**
创建 MySQL 数据库:
```sql
CREATE DATABASE polyhermes CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
3. **配置环境变量**
编辑 `backend/src/main/resources/application.properties` 或使用环境变量:
```properties
# 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/polyhermes?useSSL=false&serverTimezone=UTC&characterEncoding=utf8mb4
spring.datasource.username=${DB_USERNAME:root}
spring.datasource.password=${DB_PASSWORD:password}
# 服务器端口
server.port=${SERVER_PORT:8000}
# Polygon RPC
polygon.rpc.url=${POLYGON_RPC_URL:https://polygon-rpc.com}
# JWT 密钥
jwt.secret=${JWT_SECRET:change-me-in-production}
# 加密密钥(用于加密存储私钥和 API Key)
crypto.secret.key=${CRYPTO_SECRET_KEY:change-me-in-production}
```
4. **启动后端服务**
```bash
cd backend
./gradlew bootRun
```
后端服务将在 `http://localhost:8000` 启动。
### 前端开发环境
1. **安装依赖**
```bash
cd frontend
npm install
```
2. **配置环境变量(可选)**
创建 `.env` 文件:
```env
VITE_API_URL=http://localhost:8000
VITE_WS_URL=ws://localhost:8000
```
3. **启动开发服务器**
```bash
npm run dev
```
前端应用将在 `http://localhost:3000` 启动。
## 📝 代码规范
### 后端开发规范
详细规范请参考:[后端开发规范](.cursor/rules/backend.mdc)
**核心规范**
- 使用 Kotlin 编码规范
- Controller 方法**禁止**使用 `suspend`
- 实体类 ID 字段使用 `Long? = null`
- 所有时间字段使用 `Long` 时间戳(毫秒)
- 数值计算使用 `BigDecimal`
- 使用 `ErrorCode` 枚举定义错误码和消息
- **禁止**在代码中添加 TODO 注释
- **禁止**直接返回 mock 数据
### 前端开发规范
详细规范请参考:[前端开发规范](.cursor/rules/frontend.mdc)
**核心规范**
- 使用 TypeScript 类型定义
- 使用函数式组件和 Hooks
- **禁止**使用 `any` 类型
- **必须**使用多语言(i18n)进行所有文本显示
- **必须**使用 `formatUSDC` 函数格式化 USDC 金额
- **必须**支持移动端和桌面端
- **禁止**在代码中添加 TODO 注释
### 提交规范
遵循 [Conventional Commits](https://www.conventionalcommits.org/) 规范:
- `feat`: 新功能
- `fix`: 修复 bug
- `docs`: 文档更新
- `style`: 代码格式调整
- `refactor`: 代码重构
- `test`: 测试相关
- `chore`: 构建/工具相关
示例:
```bash
git commit -m "feat: 添加版本号显示功能"
git commit -m "fix: 修复订单状态更新问题"
```
## 📡 API 接口文档
### 统一响应格式
所有 API 接口统一使用 POST 方法,响应格式如下:
```json
{
"code": 0,
"data": {},
"msg": ""
}
```
- `code`: 响应码,0 表示成功,非 0 表示失败
- `data`: 响应数据,可以是任意类型
- `msg`: 响应消息,成功时通常为空,失败时包含错误提示
### 错误码规范
- `0`: 成功
- `1001-1999`: 参数错误
- `2001-2999`: 认证/权限错误
- `3001-3999`: 资源不存在
- `4001-4999`: 业务逻辑错误
- `5001-5999`: 服务器内部错误
### 主要 API 接口
#### 账户管理
- `POST /api/accounts/list` - 获取账户列表
- `POST /api/accounts/import` - 导入账户(通过私钥)
- `POST /api/accounts/detail` - 获取账户详情
- `POST /api/accounts/edit` - 编辑账户
- `POST /api/accounts/delete` - 删除账户
- `POST /api/accounts/balance` - 获取账户余额
#### Leader 管理
- `POST /api/leaders/list` - 获取 Leader 列表
- `POST /api/leaders/add` - 添加 Leader
- `POST /api/leaders/edit` - 编辑 Leader
- `POST /api/leaders/delete` - 删除 Leader
#### 跟单模板
- `POST /api/templates/list` - 获取模板列表
- `POST /api/templates/add` - 添加模板
- `POST /api/templates/edit` - 编辑模板
- `POST /api/templates/delete` - 删除模板
#### 跟单配置
- `POST /api/copy-trading/list` - 获取跟单配置列表
- `POST /api/copy-trading/add` - 添加跟单配置
- `POST /api/copy-trading/edit` - 编辑跟单配置
- `POST /api/copy-trading/delete` - 删除跟单配置
- `POST /api/copy-trading/enable` - 启用跟单
- `POST /api/copy-trading/disable` - 禁用跟单
#### 订单管理
- `POST /api/copy-trading/orders/buy` - 获取买入订单列表
- `POST /api/copy-trading/orders/sell` - 获取卖出订单列表
- `POST /api/copy-trading/orders/matched` - 获取匹配订单列表
#### 统计分析
- `POST /api/statistics/global` - 获取全局统计
- `POST /api/statistics/leader` - 获取 Leader 统计
- `POST /api/statistics/category` - 获取分类统计
- `POST /api/copy-trading/statistics` - 获取跟单关系统计
#### 仓位管理
- `POST /api/positions/list` - 获取仓位列表
- `POST /api/positions/sell` - 卖出仓位
- `POST /api/positions/redeem` - 赎回仓位
#### 系统管理
- `POST /api/system-settings/proxy` - 配置代理
- `POST /api/system-settings/api-health` - 获取 API 健康状态
- `POST /api/users/list` - 获取用户列表
- `POST /api/users/add` - 添加用户
- `POST /api/users/edit` - 编辑用户
- `POST /api/users/delete` - 删除用户
详细 API 接口文档请参考:[跟单系统需求文档](copy-trading-requirements.md)
## 🗄️ 数据库设计
### 主要数据表
- `accounts` - 账户表
- `leaders` - Leader 表
- `templates` - 跟单模板表
- `copy_trading` - 跟单配置表
- `copy_orders` - 跟单订单表
- `positions` - 仓位表
- `users` - 用户表
- `system_settings` - 系统设置表
数据库迁移脚本位于 `backend/src/main/resources/db/migration/`,使用 Flyway 管理。
## 🎨 前端开发指南
### 项目结构
```
frontend/src/
├── components/ # 公共组件
│ ├── Layout.tsx # 布局组件(支持移动端)
│ └── Logo.tsx # Logo 组件
├── pages/ # 页面组件
│ ├── AccountList.tsx
│ ├── LeaderList.tsx
│ ├── CopyTradingList.tsx
│ └── ...
├── services/ # API 服务
│ ├── api.ts # API 服务定义
│ └── websocket.ts # WebSocket 服务
├── store/ # 状态管理(Zustand
├── types/ # TypeScript 类型定义
├── utils/ # 工具函数
│ ├── index.ts # 统一导出
│ ├── ethers.ts # 以太坊相关工具
│ ├── auth.ts # 认证相关工具
│ └── version.ts # 版本号工具
├── hooks/ # React Hooks
├── locales/ # 多语言资源
│ ├── zh-CN/
│ ├── zh-TW/
│ └── en/
└── styles/ # 样式文件
```
### 多语言支持
项目支持多语言(中文简体、中文繁体、英文),使用 `react-i18next`
**添加新翻译**
1.`src/locales/{locale}/common.json` 中添加翻译
2. 在组件中使用 `useTranslation` Hook
```typescript
import { useTranslation } from 'react-i18next'
const MyComponent: React.FC = () => {
const { t } = useTranslation()
return <div>{t('key')}</div>
}
```
### 移动端适配
- 使用 `react-responsive` 检测设备类型
- 断点设置:移动端 < 768px,桌面端 >= 768px
- 使用响应式布局和组件
### 工具函数
**USDC 金额格式化**
```typescript
import { formatUSDC } from '../utils'
const balance = formatUSDC('1.23456') // "1.2345"
```
**以太坊地址验证**
```typescript
import { isValidWalletAddress } from '../utils'
if (isValidWalletAddress(address)) {
// 地址有效
}
```
## ⚙️ 后端开发指南
### 项目结构
```
backend/src/main/kotlin/com/wrbug/polymarketbot/
├── api/ # API 接口定义(Retrofit
│ ├── PolymarketClobApi.kt
│ ├── PolymarketGammaApi.kt
│ └── GitHubApi.kt
├── controller/ # REST 控制器
├── service/ # 业务逻辑服务
├── entity/ # 数据库实体
├── repository/ # 数据访问层
├── dto/ # 数据传输对象
├── util/ # 工具类
│ ├── CryptoUtils.kt # 加密工具
│ ├── RetrofitFactory.kt # Retrofit 工厂
│ └── ...
└── websocket/ # WebSocket 处理
```
### 创建新 API 接口
1. **定义 Retrofit 接口**(在 `api/` 目录):
```kotlin
interface MyApi {
@POST("/endpoint")
suspend fun myMethod(@Body request: MyRequest): Response<MyResponse>
}
```
2. **创建 Service**(在 `service/` 目录):
```kotlin
@Service
class MyService(
private val myApi: MyApi
) {
suspend fun doSomething(): Result<MyResponse> {
// 业务逻辑
}
}
```
3. **创建 Controller**(在 `controller/` 目录):
```kotlin
@RestController
@RequestMapping("/api/my")
class MyController(
private val myService: MyService,
private val messageSource: MessageSource
) {
@PostMapping("/list")
fun list(@RequestBody request: MyListRequest): ResponseEntity<ApiResponse<MyListResponse>> {
return try {
val data = runBlocking { myService.getList(request) }
ResponseEntity.ok(ApiResponse.success(data))
} catch (e: Exception) {
logger.error("获取列表失败", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, messageSource = messageSource))
}
}
}
```
### 数据库操作
使用 Spring Data JPA
```kotlin
@Repository
interface MyRepository : JpaRepository<MyEntity, Long> {
fun findByCode(code: String): MyEntity?
fun findByCategory(category: String): List<MyEntity>
}
```
### 加密存储
使用 `CryptoUtils` 加密敏感数据:
```kotlin
@Autowired
private lateinit var cryptoUtils: CryptoUtils
// 加密
val encrypted = cryptoUtils.encrypt("sensitive-data")
// 解密
val decrypted = cryptoUtils.decrypt(encrypted)
```
## 🔧 常见问题
### Q1: 如何添加新的页面?
1.`frontend/src/pages/` 创建页面组件
2.`frontend/src/App.tsx` 添加路由
3.`frontend/src/components/Layout.tsx` 添加菜单项(如需要)
### Q2: 如何添加新的 API 接口?
1.`backend/src/main/kotlin/.../controller/` 创建 Controller
2.`backend/src/main/kotlin/.../service/` 创建 Service
3.`frontend/src/services/api.ts` 添加 API 调用方法
### Q3: 如何添加数据库表?
1. 创建 Entity 类(在 `entity/` 目录)
2. 创建 Repository 接口(在 `repository/` 目录)
3. 创建 Flyway 迁移脚本(在 `resources/db/migration/`
### Q4: 如何测试 WebSocket
使用浏览器控制台或 WebSocket 客户端工具连接到 `ws://localhost:8000/ws`
### Q5: 如何调试后端代码?
1. 使用 IDE 的调试功能(IntelliJ IDEA、VS Code 等)
2. 在代码中添加日志:`logger.debug("调试信息")`
3. 查看日志输出:`./gradlew bootRun` 或查看日志文件
## 📚 相关文档
- [部署文档](DEPLOYMENT.md) / [English](../en/DEPLOYMENT.md) - 详细的部署指南
- [版本号管理文档](VERSION_MANAGEMENT.md) / [English](../en/VERSION_MANAGEMENT.md) - 版本号管理和自动构建
- [开发文档](DEVELOPMENT.md) / [English](../en/DEVELOPMENT.md) - 开发指南
- [跟单系统需求文档](copy-trading-requirements.md) - 后端 API 接口文档
- [前端需求文档](copy-trading-frontend-requirements.md) - 前端功能文档
## 🤝 贡献指南
欢迎贡献代码!请遵循以下步骤:
1. Fork 本仓库
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
3. 遵循代码规范
4. 提交更改 (`git commit -m 'feat: Add some AmazingFeature'`)
5. 推送到分支 (`git push origin feature/AmazingFeature`)
6. 开启 Pull Request
---
**Happy Coding! 🚀**
+268
View File
@@ -0,0 +1,268 @@
# 版本号管理说明
> 📖 **English Version**: [Version Management Guide (English)](../en/VERSION_MANAGEMENT.md)
## 概述
本项目支持自动版本号管理和显示。当在 GitHub 创建 release tag 时,会自动触发 GitHub Actions 构建 Docker 镜像并推送到 Docker Hub,同时在前端标题后显示版本号。
## 功能特性
1. **自动构建**:创建 release tag 时自动触发 GitHub Actions
2. **版本号显示**:前端标题后显示版本号(小字号)
3. **点击跳转**:点击版本号跳转到对应的 GitHub tag 页面
4. **Docker 推送**:自动构建并推送到 Docker Hub
5. **自动删除**:删除 release 时自动删除对应的 Docker 镜像标签
6. **版本号验证**:精准匹配版本号格式 `v数字.数字.数字``v数字.数字.数字-后缀`(例如:`v1.0.0`, `v1.0.0-beta`
7. **独立脚本**:构建和删除功能分离到不同的 workflow 文件,便于管理和维护
## Workflow 文件说明
项目使用两个独立的 GitHub Actions workflow 文件:
- **`.github/workflows/docker-build.yml`**:负责构建和推送 Docker 镜像
- 触发条件:`release: published`(创建 release 时)
- 功能:提取版本号、构建多架构镜像、推送到 Docker Hub
- **`.github/workflows/docker-delete.yml`**:负责删除 Docker 镜像
- 触发条件:`release: deleted`(删除 release 时)
- 功能:验证版本号格式、删除对应的 Docker 镜像标签
## 使用方法
### 1. 配置 Docker Hub 凭证
在 GitHub 仓库设置中添加以下 Secrets:
- `DOCKER_USERNAME`: Docker Hub 用户名(例如:`wrbug`
- `DOCKER_PASSWORD`: Docker Hub 访问令牌(推荐)或密码
**设置步骤**
1. **创建 Docker Hub Access Token**(推荐):
- 访问:https://hub.docker.com/settings/security
- 点击 "New Access Token"
- 填写描述(如:`GitHub Actions PolyHermes`
- **重要**:勾选以下权限:
-`Read & Write`(用于推送镜像)
-`Delete repository tags`(用于删除镜像)
- 点击 "Generate"
- **立即复制令牌**(只显示一次)
2. **在 GitHub 中添加 Secrets**
- 访问 GitHub 仓库 → Settings → Secrets and variables → Actions
- 点击 "New repository secret"
- 添加 `DOCKER_USERNAME`:你的 Docker Hub 用户名
- 添加 `DOCKER_PASSWORD`:刚才创建的 Access Token(不是密码)
**注意**
- ⚠️ 如果使用密码而不是 Access Token,删除镜像功能可能无法正常工作
- ✅ 推荐使用 Access Token,并确保有 `Delete repository tags` 权限
### 2. 创建 Release(必须通过 GitHub Releases 页面)
**重要**:只有通过 [GitHub Releases 页面](https://github.com/WrBug/PolyHermes/releases/new) 创建 release 时才会触发自动构建。
**Workflow 说明**
- 创建 release 时,会触发 `docker-build.yml` workflow,自动构建并推送镜像
- 删除 release 时,会触发 `docker-delete.yml` workflow,自动删除对应的镜像标签
**创建步骤**
1. 访问 [GitHub Releases 页面](https://github.com/WrBug/PolyHermes/releases/new)
2. 点击 "Choose a tag" 下拉菜单,输入新的 tag 名称(例如:`v1.0.0``v1.0.0-beta`
- 如果 tag 不存在,GitHub 会自动创建
- Tag 格式:`v数字.数字.数字``v数字.数字.数字-后缀`(例如:`v1.0.0`, `v1.0.0-beta`, `v2.10.102-rc.1`
3. 填写 Release 标题(例如:`v1.0.0``v1.0.0-beta`
4. 填写 Release 描述(可选,建议填写更新内容)
5. 点击 "Publish release" 按钮
**注意**
- ⚠️ 直接通过 `git push` 推送 tag **不会**触发构建
- ✅ 只有通过 Releases 页面点击 "Publish release" 才会触发构建
- 这样可以确保只有正式发布的版本才会构建 Docker 镜像
### 3. 自动构建流程
点击 "Publish release" 后,GitHub Actions 会自动:
1. **提取版本号**:从 tag 中提取版本号(例如:`v1.0.0``1.0.0`
2. **构建 Docker 镜像**:使用版本号作为构建参数
3. **注入版本号**:在构建前端时注入版本号到代码中
4. **推送镜像**:推送到 Docker Hub,标签为:
- `wrbug/polyhermes:v1.0.0`(具体版本)
- `wrbug/polyhermes:latest`(最新版本)
### 4. 版本号显示
前端会在标题 "PolyHermes" 后显示版本号,格式为:`PolyHermes v1.0.0`
- **显示位置**:桌面端左侧导航栏标题,移动端顶部标题
- **样式**:小字号,半透明,正常展示(无下划线等特殊样式)
- **点击行为**:点击版本号跳转到对应的 GitHub tag 页面
### 5. 删除 Release 和 Docker 镜像
当在 GitHub Releases 页面删除 release 时,会自动删除对应的 Docker 镜像标签:
1. 访问 [GitHub Releases 页面](https://github.com/WrBug/PolyHermes/releases)
2. 找到要删除的 release
3. 点击 "Delete" 按钮
4. GitHub Actions 会自动触发删除流程
5. 删除对应的 Docker 镜像标签(例如:`wrbug/polyhermes:v1.0.0`
**注意事项**
- ⚠️ 只有格式为 `v数字.数字.数字``v数字.数字.数字-后缀` 的版本号才会被删除(例如:`v1.0.0`, `v1.0.0-beta`, `v2.10.102`
- ⚠️ 如果镜像标签不存在,会显示警告但不会失败
- ⚠️ `latest` 标签不会被删除(即使删除最新的 release)
## 技术实现
### 版本号注入流程
1. **GitHub Actions** 提取 tag 中的版本号
2. **Dockerfile** 接收构建参数(`VERSION``GIT_TAG``GITHUB_REPO_URL`
3. **Vite 构建** 通过环境变量注入版本号到 `window.__VERSION__`
4. **前端代码**`window.__VERSION__` 读取版本号并显示
### 文件说明
- `.github/workflows/docker-build.yml`: GitHub Actions 工作流配置
- `Dockerfile`: 支持版本号构建参数
- `frontend/vite.config.ts`: Vite 配置,注入版本号到全局变量
- `frontend/src/utils/version.ts`: 版本号工具函数
- `frontend/src/components/Layout.tsx`: 显示版本号的组件
### 环境变量
构建时使用的环境变量:
- `VERSION`: 版本号(例如:`1.0.0`
- `GIT_TAG`: Git tag(例如:`v1.0.0`
- `GITHUB_REPO_URL`: GitHub 仓库 URL(默认:`https://github.com/WrBug/PolyHermes`
## 开发环境
在开发环境中,版本号默认为 `dev`,不会显示为链接。
如果需要测试版本号显示,可以在 `.env` 文件中设置:
```env
VITE_APP_VERSION=1.0.0
VITE_APP_GIT_TAG=v1.0.0
VITE_APP_GITHUB_REPO_URL=https://github.com/WrBug/PolyHermes
```
## 常见问题
### Q1: 创建 release 后没有触发构建?
**A:** 检查以下几点:
1. 确认是通过 [GitHub Releases 页面](https://github.com/WrBug/PolyHermes/releases/new) 创建的 release,而不是直接推送 tag
2. 确认点击了 "Publish release" 按钮(不是 "Save draft"
3. 检查 GitHub Actions 是否已启用
4. 查看 Actions 标签页中的工作流运行情况
5. 确认 release 状态为 "Published"(不是 "Draft" 或 "Prerelease"
### Q2: Docker 推送失败?
**A:** 检查以下几点:
1. 确认已正确配置 `DOCKER_USERNAME``DOCKER_PASSWORD` Secrets
2. 确认 Docker Hub 账户有权限推送镜像
3. 检查 Docker Hub 仓库名称是否正确(`wrbug/polyhermes`
### Q3: 前端没有显示版本号?
**A:** 检查以下几点:
1. 确认构建时传递了版本号环境变量
2. 检查浏览器控制台是否有错误
3. 确认使用的是构建后的镜像,而不是开发环境
### Q4: 版本号点击没有跳转?
**A:** 检查以下几点:
1. 确认 `GIT_TAG` 环境变量已正确设置
2. 确认 GitHub 仓库 URL 正确
3. 检查浏览器是否阻止了弹窗
### Q5: 删除 release 后 Docker 镜像没有被删除?
**A:** 检查以下几点:
1. 确认版本号格式为 `v数字.数字.数字``v数字.数字.数字-后缀`(例如:`v1.0.0`, `v1.0.0-beta`
2. 确认 Docker Hub 凭证(`DOCKER_USERNAME``DOCKER_PASSWORD`)正确配置
3. **确认 Docker Hub 访问令牌有删除镜像的权限**
- 如果使用 Access Token,需要确保有 `Delete repository tags` 权限
- 访问 Docker Hub → Account Settings → Security → Access Tokens
- 创建或编辑访问令牌,确保勾选 `Delete repository tags` 权限
4. 如果遇到 401 错误,可能是:
- 访问令牌过期,需要重新生成
- 访问令牌权限不足,需要添加删除权限
- 用户名或密码/令牌错误
5. 查看 GitHub Actions 日志,确认删除操作是否执行
6. 如果镜像标签不存在,会显示警告但不会失败(这是正常的)
### Q7: 删除镜像时遇到 401 未授权错误?
**A:** 这通常是因为认证失败,请检查:
1. **如果使用 Access Token**
- 确保访问令牌未过期
- 确保访问令牌有 `Delete repository tags` 权限
- 在 Docker Hub → Account Settings → Security → Access Tokens 中检查权限
2. **如果使用密码**
- 确保用户名和密码正确
- 如果启用了 2FA,需要使用 Access Token 而不是密码
3. **创建新的 Access Token**
- 访问:https://hub.docker.com/settings/security
- 点击 "New Access Token"
- 填写描述(如:`GitHub Actions Delete Images`
- **重要**:勾选 `Delete repository tags` 权限
- 复制生成的令牌,更新 GitHub Secrets 中的 `DOCKER_PASSWORD`
### Q6: 版本号格式要求是什么?
**A:** 版本号必须严格匹配格式:`v数字.数字.数字``v数字.数字.数字-后缀`
- ✅ 正确:`v1.0.0`, `v2.10.102`, `v1.0.0-beta`, `v1.0.0-rc.1`, `v2.10.102-alpha`
- ❌ 错误:`v1.0`, `1.0.0`, `v1.0.0.1`, `v1.0.0_beta`(下划线不支持)
## 示例
### 创建 Release 示例
**步骤 1:访问 Releases 页面**
访问:https://github.com/WrBug/PolyHermes/releases/new
**步骤 2:创建 Release**
1. 在 "Choose a tag" 中输入 `v1.0.0`(如果不存在会自动创建)
2. 填写 Release 标题:`v1.0.0`
3. 填写 Release 描述(可选)
4. 点击 "Publish release"
**步骤 3:自动构建**
- GitHub Actions 会自动触发构建
- 构建完成后,Docker 镜像会自动推送到 Docker Hub
- 前端会显示 "PolyHermes v1.0.0"
**注意**:直接通过 `git push` 推送 tag 不会触发构建,必须通过 Releases 页面创建。
### 使用 Docker 镜像示例
```bash
# 拉取特定版本
docker pull wrbug/polyhermes:v1.0.0
# 拉取最新版本
docker pull wrbug/polyhermes:latest
# 运行容器
docker run -d -p 80:80 wrbug/polyhermes:v1.0.0
```
## 注意事项
1. **Tag 格式**:必须使用 `v*` 格式(例如:`v1.0.0`),否则不会触发构建
2. **版本号格式**:建议使用语义化版本号(Semantic Versioning
3. **Docker Hub**:确保 Docker Hub 仓库已创建
4. **权限**:确保 GitHub Actions 有权限访问 Docker Hub
@@ -0,0 +1,355 @@
# 跟单系统前端需求文档
## 1. 页面概述
基于订单跟踪与统计设计,前端需要实现以下页面和功能:
- 跟单关系统计页面
- 买入订单列表页面
- 卖出订单列表页面
- 匹配关系列表页面
## 2. 跟单关系统计页面
### 2.1 页面路径
`/copy-trading/statistics/:copyTradingId`
### 2.2 显示内容
#### 2.2.1 基本信息卡片
- 账户名称
- Leader 名称
- 模板名称
- 跟单状态(启用/禁用)
#### 2.2.2 买入统计卡片
- **总买入数量**:所有买入订单的数量总和
- **总买入金额**:所有买入订单的金额总和(数量 × 价格)
- **总买入订单数**:买入订单的数量
- **平均买入价格**:总买入金额 / 总买入数量
#### 2.2.3 卖出统计卡片
- **总卖出数量**:所有卖出订单的数量总和
- **总卖出金额**:所有卖出订单的金额总和
- **总卖出订单数**:卖出订单的数量
#### 2.2.4 持仓统计卡片
- **当前持仓数量**:未匹配的买入数量总和
- **当前持仓价值**:当前持仓数量 × 当前市场价格
- **平均买入价格**:已买入订单的平均价格
#### 2.2.5 盈亏统计卡片
- **总已实现盈亏**:所有已匹配订单的盈亏总和
- 颜色:盈利绿色,亏损红色
- 图标:盈利↑,亏损↓
- **总未实现盈亏**:当前持仓的盈亏(持仓数量 × (当前价格 - 平均买入价格))
- 颜色:盈利绿色,亏损红色
- **总盈亏**:已实现盈亏 + 未实现盈亏
- 颜色:盈利绿色,亏损红色
- 图标:盈利↑,亏损↓
- **总盈亏百分比**:总盈亏 / 总买入金额 × 100%
- 颜色:盈利绿色,亏损红色
### 2.3 UI 布局
**桌面端**
- 使用 `Row``Col` 布局,每行 3-4 个统计卡片
- 卡片使用 `Statistic` 组件显示数据
**移动端**
- 每行 1-2 个统计卡片
- 卡片内容简化,重要数据突出显示
### 2.4 数据格式化
- **数量**:使用 `formatUSDC` 格式化(最多 4 位小数,自动去除尾随零)
- **金额**:使用 `formatUSDC` 格式化,后缀 "USDC"
- **百分比**:显示 2 位小数,后缀 "%"
- **价格**:使用 `formatUSDC` 格式化
## 3. 买入订单列表页面
### 3.1 页面路径
`/copy-trading/orders/buy/:copyTradingId`
### 3.2 表格列
| 列名 | 字段 | 说明 |
|------|------|------|
| 订单ID | buyOrderId | 跟单买入订单ID(可点击查看详情) |
| Leader 交易ID | leaderBuyTradeId | Leader 的买入交易ID |
| 市场 | marketId | 市场地址(可点击查看市场详情) |
| 方向 | side | YES/NO 标签 |
| 买入数量 | quantity | 使用 formatUSDC 格式化 |
| 买入价格 | price | 使用 formatUSDC 格式化 |
| 买入金额 | amount | quantity × price,使用 formatUSDC 格式化 |
| 已匹配数量 | matchedQuantity | 已匹配的卖出数量,使用 formatUSDC 格式化 |
| 剩余数量 | remainingQuantity | 未匹配的数量,使用 formatUSDC 格式化 |
| 订单状态 | status | 标签显示:filled(已完成)、partially_matched(部分匹配)、fully_matched(完全匹配) |
| 创建时间 | createdAt | 时间戳转换为可读格式 |
### 3.3 状态标签颜色
- `filled`:蓝色(processing
- `partially_matched`:橙色(warning
- `fully_matched`:绿色(success
### 3.4 功能
- **分页**:支持分页查询
- **排序**:默认按创建时间倒序
- **筛选**:可按市场、方向、状态筛选
- **详情**:点击订单ID查看详情(可选)
## 4. 卖出订单列表页面
### 4.1 页面路径
`/copy-trading/orders/sell/:copyTradingId`
### 4.2 表格列
| 列名 | 字段 | 说明 |
|------|------|------|
| 订单ID | sellOrderId | 跟单卖出订单ID(可点击查看详情) |
| Leader 交易ID | leaderSellTradeId | Leader 的卖出交易ID |
| 市场 | marketId | 市场地址(可点击查看市场详情) |
| 方向 | side | YES/NO 标签 |
| 卖出数量 | quantity | 使用 formatUSDC 格式化 |
| 卖出价格 | price | 使用 formatUSDC 格式化 |
| 卖出金额 | amount | quantity × price,使用 formatUSDC 格式化 |
| 已实现盈亏 | realizedPnl | 该卖出订单的盈亏,使用 formatUSDC 格式化,颜色:盈利绿色,亏损红色 |
| 创建时间 | createdAt | 时间戳转换为可读格式 |
### 4.3 功能
- **分页**:支持分页查询
- **排序**:默认按创建时间倒序
- **筛选**:可按市场、方向筛选
- **详情**:点击订单ID查看匹配明细(可选)
## 5. 匹配关系列表页面
### 5.1 页面路径
`/copy-trading/orders/matched/:copyTradingId`
### 5.2 表格列
| 列名 | 字段 | 说明 |
|------|------|------|
| 卖出订单ID | sellOrderId | 跟单卖出订单ID(可点击查看详情) |
| 买入订单ID | buyOrderId | 匹配的买入订单ID(可点击查看详情) |
| 匹配数量 | matchedQuantity | 匹配的数量,使用 formatUSDC 格式化 |
| 买入价格 | buyPrice | 买入价格,使用 formatUSDC 格式化 |
| 卖出价格 | sellPrice | 卖出价格,使用 formatUSDC 格式化 |
| 盈亏 | realizedPnl | (卖出价格 - 买入价格) × 匹配数量,使用 formatUSDC 格式化,颜色:盈利绿色,亏损红色 |
| 匹配时间 | matchedAt | 时间戳转换为可读格式 |
### 5.3 功能
- **分页**:支持分页查询
- **排序**:默认按匹配时间倒序
- **筛选**:可按卖出订单ID、买入订单ID筛选
- **详情**:点击订单ID查看详情(可选)
## 6. 跟单列表页面增强
### 6.1 在跟单列表中添加统计入口
`CopyTradingList` 页面中,每个跟单关系添加:
- **查看统计**按钮:跳转到统计页面
- **查看订单**按钮:跳转到订单列表页面(可选择买入/卖出/匹配)
### 6.2 快速统计显示
在跟单列表表格中,可添加快速统计列:
- **总盈亏**:显示该跟单关系的总盈亏(颜色标识)
- **订单数**:买入订单数 / 卖出订单数
- **持仓**:当前持仓数量
## 7. 类型定义
### 7.1 跟单关系统计响应
```typescript
export interface CopyTradingStatistics {
copyTradingId: number
accountId: number
accountName: string
leaderId: number
leaderName: string
templateId: number
templateName: string
// 买入统计
totalBuyQuantity: string
totalBuyOrders: number
totalBuyAmount: string
// 卖出统计
totalSellQuantity: string
totalSellOrders: number
totalSellAmount: string
// 持仓统计
currentPositionQuantity: string
currentPositionValue: string
avgBuyPrice: string
// 盈亏统计
totalRealizedPnl: string
totalUnrealizedPnl: string
totalPnl: string
totalPnlPercent: string
}
```
### 7.2 买入订单信息
```typescript
export interface BuyOrderInfo {
orderId: string
leaderTradeId: string
marketId: string
side: string
quantity: string
price: string
amount: string
matchedQuantity: string
remainingQuantity: string
status: 'filled' | 'partially_matched' | 'fully_matched'
createdAt: number
}
```
### 7.3 卖出订单信息
```typescript
export interface SellOrderInfo {
orderId: string
leaderTradeId: string
marketId: string
side: string
quantity: string
price: string
amount: string
realizedPnl: string
createdAt: number
}
```
### 7.4 匹配订单信息
```typescript
export interface MatchedOrderInfo {
sellOrderId: string
buyOrderId: string
matchedQuantity: string
buyPrice: string
sellPrice: string
realizedPnl: string
matchedAt: number
}
```
## 8. API 接口
### 8.1 查询跟单统计
```
POST /api/copy-trading/statistics/detail
Request: { copyTradingId: number }
Response: ApiResponse<CopyTradingStatistics>
```
### 8.2 查询买入订单列表
```
POST /api/copy-trading/orders/tracking
Request: {
copyTradingId: number
type: 'buy'
page?: number
limit?: number
marketId?: string
side?: string
status?: string
}
Response: ApiResponse<{ list: BuyOrderInfo[], total: number }>
```
### 8.3 查询卖出订单列表
```
POST /api/copy-trading/orders/tracking
Request: {
copyTradingId: number
type: 'sell'
page?: number
limit?: number
marketId?: string
side?: string
}
Response: ApiResponse<{ list: SellOrderInfo[], total: number }>
```
### 8.4 查询匹配关系列表
```
POST /api/copy-trading/orders/tracking
Request: {
copyTradingId: number
type: 'matched'
page?: number
limit?: number
sellOrderId?: string
buyOrderId?: string
}
Response: ApiResponse<{ list: MatchedOrderInfo[], total: number }>
```
## 9. UI/UX 要求
### 9.1 移动端适配
- **响应式布局**:使用 `useMediaQuery` 检测移动端
- **表格优化**:移动端使用卡片布局或横向滚动
- **统计卡片**:移动端每行 1-2 个,简化显示
### 9.2 数据格式化
- **统一使用 `formatUSDC`**:所有 USDC 金额显示
- **时间格式化**:使用相对时间或标准时间格式
- **百分比显示**:保留 2 位小数
### 9.3 颜色规范
- **盈利**:绿色(#3f8600
- **亏损**:红色(#cf1322
- **状态标签**
- filled: 蓝色
- partially_matched: 橙色
- fully_matched: 绿色
### 9.4 交互优化
- **加载状态**:使用 `loading` 属性显示加载中
- **错误处理**:使用 `message.error` 显示错误信息
- **空状态**:显示友好的空状态提示
- **分页**:支持每页数量调整
## 10. 实现优先级
### Phase 1: 核心功能
1. 跟单关系统计页面(基础统计)
2. 买入订单列表页面
3. 卖出订单列表页面
### Phase 2: 增强功能
4. 匹配关系列表页面
5. 跟单列表页面增强(快速统计)
6. 订单详情页面(可选)
### Phase 3: 优化功能
7. 数据可视化(图表展示)
8. 导出功能(导出统计报表)
9. 高级筛选和搜索
+550
View File
@@ -0,0 +1,550 @@
# 跟单买入和卖出实现方案
## 1. 核心思路
### 1.1 基本原理
- **买入跟单**:当 Leader 执行 `BUY` 交易时,系统自动创建 `BUY` 订单
- **卖出跟单**:当 Leader 执行 `SELL` 交易时,系统自动创建 `SELL` 订单
- **方向复制**:直接复制 Leader 交易的 `side` 字段(BUY 或 SELL
### 1.2 数据来源
- **方式1(优先)**WebSocket 推送(RTDS API
- 实时接收 Leader 的交易推送
- WebSocket URL: `wss://ws-live-data.polymarket.com`
- 订阅用户交易频道,实时获取交易数据
- **方式2(备选)**:轮询 CLOB API
- 通过 CLOB API `/trades?user={leaderAddress}` 获取 Leader 的交易记录
- 定期轮询(默认每 5 秒)
**交易数据包含**
- `side`: "BUY" 或 "SELL"(直接复制)
- `market`: 市场地址(直接复制)
- `price`: 交易价格(可调整)
- `size`: 交易数量(按比例或固定金额计算)
## 2. 实现流程
### 2.1 监控 Leader 交易
#### 2.1.1 WebSocket 推送模式(优先)
```kotlin
/**
* WebSocket 推送监控服务
*/
@Service
class CopyTradingWebSocketService(
private val leaderRepository: LeaderRepository,
private val configRepository: CopyTradingConfigRepository
) {
private var webSocketClient: WebSocketClient? = null
private val subscribedLeaders = mutableSetOf<String>()
/**
* 初始化 WebSocket 连接
*/
@PostConstruct
fun initWebSocket() {
val config = configRepository.findFirstByOrderByIdAsc() ?: getDefaultConfig()
if (config.useWebSocket) {
connectWebSocket()
}
}
/**
* 连接 WebSocket
*/
private fun connectWebSocket() {
try {
val wsUrl = "wss://ws-live-data.polymarket.com"
webSocketClient = WebSocketClient(wsUrl)
webSocketClient?.onMessage { message ->
handleWebSocketMessage(message)
}
webSocketClient?.onError { error ->
logger.error("WebSocket 连接错误", error)
// 降级到轮询模式
fallbackToPolling()
}
webSocketClient?.onClose {
logger.warn("WebSocket 连接关闭,尝试重连")
reconnectWebSocket()
}
// 订阅所有启用的 Leader
subscribeAllLeaders()
} catch (e: Exception) {
logger.error("WebSocket 连接失败,降级到轮询模式", e)
fallbackToPolling()
}
}
/**
* 订阅所有启用的 Leader
*/
private fun subscribeAllLeaders() {
val enabledLeaders = leaderRepository.findByEnabledTrue()
enabledLeaders.forEach { leader ->
subscribeLeader(leader.leaderAddress)
}
}
/**
* 订阅单个 Leader
*/
fun subscribeLeader(leaderAddress: String) {
val subscribeMessage = jsonObjectOf(
"type" to "subscribe",
"channel" to "user",
"user" to leaderAddress
)
webSocketClient?.send(subscribeMessage.toString())
subscribedLeaders.add(leaderAddress)
}
/**
* 处理 WebSocket 消息
*/
private fun handleWebSocketMessage(message: String) {
try {
val json = JSONObject(message)
val channel = json.optString("channel")
val eventType = json.optString("event")
if (channel == "user" && eventType == "trade") {
val trade = parseTradeMessage(json)
// 触发跟单逻辑
processTradeFromWebSocket(trade)
}
} catch (e: Exception) {
logger.error("处理 WebSocket 消息失败", e)
}
}
/**
* 重连 WebSocket
*/
private fun reconnectWebSocket() {
val config = configRepository.findFirstByOrderByIdAsc() ?: getDefaultConfig()
var retryCount = 0
while (retryCount < config.websocketMaxRetries) {
try {
Thread.sleep(config.websocketReconnectInterval.toLong())
connectWebSocket()
return
} catch (e: Exception) {
retryCount++
logger.warn("WebSocket 重连失败 (${retryCount}/${config.websocketMaxRetries})", e)
}
}
// 重连失败,降级到轮询
logger.error("WebSocket 重连失败,降级到轮询模式")
fallbackToPolling()
}
}
```
#### 2.1.2 轮询模式(备选)
```kotlin
/**
* 轮询监控服务(WebSocket 不可用时的备选方案)
*/
@Service
class CopyTradingPollingService(
private val clobService: PolymarketClobService,
private val leaderRepository: LeaderRepository,
private val configRepository: CopyTradingConfigRepository
) {
/**
* 定期轮询所有启用的 Leader 的交易
*/
@Scheduled(fixedDelayString = "\${copy.trading.poll.interval:5000}")
suspend fun monitorLeaders() {
val config = configRepository.findFirstByOrderByIdAsc() ?: getDefaultConfig()
// 如果配置了使用 WebSocket 且 WebSocket 可用,则不轮询
if (config.useWebSocket && isWebSocketAvailable()) {
return
}
val enabledLeaders = leaderRepository.findByEnabledTrue()
enabledLeaders.forEach { leader ->
try {
processLeaderTrades(leader)
} catch (e: Exception) {
logger.error("处理 Leader ${leader.id} 交易失败", e)
}
}
}
/**
* 处理单个 Leader 的交易
*/
private suspend fun processLeaderTrades(leader: Leader) {
// 1. 获取 Leader 的最新交易记录
val tradesResult = clobService.getTrades(
market = null,
user = leader.leaderAddress,
limit = 50,
offset = 0
)
tradesResult.fold(
onSuccess = { trades ->
trades.forEach { trade ->
// 2. 检查是否已处理过(去重)
if (!isProcessed(leader.id, trade.id)) {
// 3. 处理交易(买入或卖出)
processTrade(leader, trade)
// 4. 标记为已处理
markAsProcessed(leader.id, trade.id)
}
}
},
onFailure = { e ->
logger.error("获取 Leader ${leader.id} 交易失败", e)
}
)
}
}
```
### 2.2 处理交易(买入/卖出)
```kotlin
/**
* 处理单笔交易,自动识别买入或卖出
*/
private suspend fun processTrade(leader: Leader, trade: TradeResponse) {
// 1. 验证分类筛选
if (leader.category != null) {
val marketCategory = getMarketCategory(trade.market)
if (marketCategory != leader.category) {
logger.debug("跳过交易:分类不匹配 ${trade.market}")
return
}
}
// 2. 验证风险控制
if (!checkRiskControl(leader)) {
logger.warn("风险控制限制,跳过跟单 Leader ${leader.id}")
return
}
// 3. 确定使用的账户
val account = getAccountForLeader(leader)
if (account == null) {
logger.error("无法获取账户,跳过跟单 Leader ${leader.id}")
return
}
// 4. 计算跟单订单参数
val orderParams = calculateOrderParams(leader, trade)
// 5. 创建跟单订单(买入或卖出)
createCopyOrder(leader, account, trade, orderParams)
}
/**
* 计算跟单订单参数
*/
private fun calculateOrderParams(leader: Leader, trade: TradeResponse): OrderParams {
// 获取配置
val globalConfig = configRepository.findFirstByOrderByIdAsc() ?: getDefaultConfig()
// 计算订单大小
val leaderSize = trade.size.toSafeBigDecimal()
val copyRatio = leader.copyRatio ?: globalConfig.copyRatio
var orderSize = leaderSize.multiply(copyRatio)
// 应用限制
val maxSize = leader.maxOrderSize ?: globalConfig.maxOrderSize
val minSize = leader.minOrderSize ?: globalConfig.minOrderSize
orderSize = orderSize.coerceIn(minSize, maxSize)
// 计算价格(默认使用 Leader 的价格)
val price = trade.price.toSafeBigDecimal()
// TODO: 可以根据价格容忍度调整价格
return OrderParams(
market = trade.market,
side = trade.side, // 直接复制 BUY 或 SELL
price = price,
size = orderSize
)
}
data class OrderParams(
val market: String,
val side: String, // "BUY" 或 "SELL"
val price: BigDecimal,
val size: BigDecimal
)
```
### 2.3 创建跟单订单
```kotlin
/**
* 创建跟单订单(买入或卖出)
*/
private suspend fun createCopyOrder(
leader: Leader,
account: Account,
trade: TradeResponse,
params: OrderParams
) {
try {
// 1. 创建订单请求
val orderRequest = CreateOrderRequest(
market = params.market,
side = params.side, // "BUY" 或 "SELL"
price = params.price.toPlainString(),
size = params.size.toPlainString(),
type = "LIMIT"
)
// 2. 使用账户的 API Key 创建订单
val apiKey = account.apiKey ?: throw IllegalStateException("账户 ${account.id} 未配置 API Key")
val clobApi = createClobApiWithApiKey(apiKey)
val orderResult = clobApi.createOrder(orderRequest)
orderResult.fold(
onSuccess = { orderResponse ->
// 3. 保存跟单记录
val copyOrder = CopyOrder(
accountId = account.id!!,
leaderId = leader.id!!,
leaderAddress = leader.leaderAddress,
leaderTradeId = trade.id,
marketId = params.market,
category = getMarketCategory(params.market),
side = params.side, // 保存买入或卖出方向
price = params.price,
size = params.size,
copyRatio = leader.copyRatio ?: BigDecimal.ONE,
orderId = orderResponse.id,
status = "created"
)
copyOrderRepository.save(copyOrder)
logger.info("成功创建跟单订单: Leader=${leader.id}, Side=${params.side}, Market=${params.market}")
},
onFailure = { e ->
logger.error("创建跟单订单失败: Leader=${leader.id}, Side=${params.side}", e)
// 记录失败的跟单订单
val copyOrder = CopyOrder(
accountId = account.id!!,
leaderId = leader.id!!,
leaderAddress = leader.leaderAddress,
leaderTradeId = trade.id,
marketId = params.market,
category = getMarketCategory(params.market),
side = params.side,
price = params.price,
size = params.size,
copyRatio = leader.copyRatio ?: BigDecimal.ONE,
status = "failed"
)
copyOrderRepository.save(copyOrder)
}
)
} catch (e: Exception) {
logger.error("创建跟单订单异常: Leader=${leader.id}, Side=${params.side}", e)
}
}
```
## 3. 关键实现细节
### 3.1 买入和卖出的区别
**买入跟单(BUY**
- Leader 执行 `BUY` 交易 → 系统创建 `BUY` 订单
- 订单参数:`side = "BUY"`
- 表示买入该市场的 YES 或 NO 代币
**卖出跟单(SELL**
- Leader 执行 `SELL` 交易 → 系统创建 `SELL` 订单
- 订单参数:`side = "SELL"`
- 表示卖出持有的代币
**实现上无区别**
- 买入和卖出的处理逻辑完全相同
- 只是 `side` 字段的值不同("BUY" 或 "SELL"
- 都通过 `createOrder` API 创建订单
### 3.2 去重机制
```kotlin
/**
* 检查交易是否已处理
*/
private suspend fun isProcessed(leaderId: Long, tradeId: String): Boolean {
return processedTradeRepository.existsByLeaderIdAndTradeId(leaderId, tradeId)
}
/**
* 标记交易为已处理
*/
private suspend fun markAsProcessed(leaderId: Long, tradeId: String) {
val processed = ProcessedTrade(
leaderId = leaderId,
tradeId = tradeId,
processedAt = System.currentTimeMillis()
)
processedTradeRepository.save(processed)
}
```
### 3.3 账户选择
```kotlin
/**
* 获取 Leader 使用的账户
*/
private suspend fun getAccountForLeader(leader: Leader): Account? {
return if (leader.accountId != null) {
// 使用 Leader 指定的账户
accountRepository.findById(leader.accountId)
} else {
// 使用默认账户
accountRepository.findByIsDefaultTrue()
}
}
```
### 3.4 风险控制检查
```kotlin
/**
* 检查风险控制限制
*/
private suspend fun checkRiskControl(leader: Leader): Boolean {
val config = configRepository.findFirstByOrderByIdAsc() ?: getDefaultConfig()
// 检查每日亏损限制
val todayLoss = getTodayLoss(leader.accountId ?: getDefaultAccountId())
if (todayLoss >= config.maxDailyLoss) {
logger.warn("达到每日亏损限制: $todayLoss >= ${config.maxDailyLoss}")
return false
}
// 检查每日订单数限制
val todayOrderCount = getTodayOrderCount(leader.accountId ?: getDefaultAccountId())
if (todayOrderCount >= config.maxDailyOrders) {
logger.warn("达到每日订单数限制: $todayOrderCount >= ${config.maxDailyOrders}")
return false
}
return true
}
```
## 4. 数据模型
### 4.1 ProcessedTrade(已处理交易)
```kotlin
@Entity
@Table(name = "copy_trading_processed_trades")
data class ProcessedTrade(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
@Column(name = "leader_id", nullable = false)
val leaderId: Long,
@Column(name = "trade_id", nullable = false, length = 100)
val tradeId: String,
@Column(name = "processed_at", nullable = false)
val processedAt: Long = System.currentTimeMillis(),
@UniqueConstraint(columnNames = ["leader_id", "trade_id"])
)
```
## 5. 完整示例
### 5.1 买入跟单示例
```
1. Leader 执行买入交易:
- Trade: { id: "trade_123", market: "0x...", side: "BUY", price: "0.5", size: "100" }
2. 系统检测到交易:
- 验证分类、风险控制
- 计算跟单参数:size = 100 × 1.0 = 100
3. 创建跟单订单:
- CreateOrderRequest: { market: "0x...", side: "BUY", price: "0.5", size: "100" }
- 调用 CLOB API 创建订单
4. 保存记录:
- CopyOrder: { side: "BUY", ... }
```
### 5.2 卖出跟单示例
```
1. Leader 执行卖出交易:
- Trade: { id: "trade_456", market: "0x...", side: "SELL", price: "0.6", size: "50" }
2. 系统检测到交易:
- 验证分类、风险控制
- 计算跟单参数:size = 50 × 1.0 = 50
3. 创建跟单订单:
- CreateOrderRequest: { market: "0x...", side: "SELL", price: "0.6", size: "50" }
- 调用 CLOB API 创建订单
4. 保存记录:
- CopyOrder: { side: "SELL", ... }
```
## 6. 注意事项
### 6.1 交易 vs 订单
- **交易(Trade)**:已成交的记录,包含 `side` 字段
- **订单(Order)**:挂单,可能未成交
- 跟单系统基于**交易记录**触发,创建**订单**
### 6.2 价格和数量
- **价格**:默认使用 Leader 的交易价格,可配置价格容忍度
- **数量**:按跟单比例计算,应用最大/最小限制
### 6.3 错误处理
- API 调用失败时记录失败状态
- 网络异常时重试机制
- 记录详细日志便于排查
### 6.4 性能优化
- 批量查询多个 Leader 的交易
- 使用缓存减少重复查询
- 异步处理订单创建
## 7. 总结
**买入和卖出的实现完全相同**
- 都通过监控 Leader 的交易记录触发
- 都通过 `side` 字段区分("BUY" 或 "SELL"
- 都调用相同的 `createOrder` API
- 区别仅在于 `side` 参数的值
**核心流程**
1. 轮询 Leader 交易 → 2. 识别买入/卖出 → 3. 计算参数 → 4. 创建订单 → 5. 保存记录
@@ -0,0 +1,533 @@
# 跟单订单跟踪与统计设计文档
## 1. 方案概述
采用**订单跟踪匹配方案**,精确追踪每笔买入订单,当 Leader 卖出时进行精确匹配,实现:
- 精确的买入-卖出匹配关系
- 准确的盈亏计算(已实现/未实现)
- 完整的订单统计信息
- 多维度数据统计
## 2. 核心思路
### 2.1 事件监听
**当前监听的事件类型****交易事件(trade**
- **事件来源**
- WebSocket User Channel`event_type = "trade"`
- 轮询 CLOB API`GET /trades?user={leaderAddress}`
- **触发时机**:交易已成交
- **数据字段**`id`trade_id)、`market``side`BUY/SELL)、`price``size``timestamp`
- **去重标识**`leader_id + trade_id`trade.id
**说明**
- 只监听已成交的交易事件,不监听订单创建事件
- 交易事件表示 Leader 已经完成买入或卖出操作
- 通过 `trade.id` 进行去重,确保同一笔交易只处理一次
### 2.2 买入订单跟踪
当 Leader 买入时(通过交易事件):
1. 检测到 `side = "BUY"` 的交易事件
2. 创建跟单买入订单
3. 记录到 `copy_order_tracking`
4. 记录买入数量、价格、状态等信息
### 2.3 卖出订单匹配
当 Leader 卖出时(通过交易事件):
1. 检测到 `side = "SELL"` 的交易事件
2. 查找未匹配的买入订单(FIFO 策略)
3. 按比例匹配卖出数量
4. 更新买入订单的匹配状态
5. 记录匹配关系到 `sell_match_record``sell_match_detail`
### 2.4 匹配策略
- **FIFO(先进先出)**:按买入时间顺序匹配
- **部分匹配**:支持一个买入订单被多次卖出匹配
- **状态管理**`filled``partially_matched``fully_matched`
## 3. 数据模型
### 3.1 订单跟踪表(copy_order_tracking
```sql
CREATE TABLE copy_order_tracking (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
copy_trading_id BIGINT NOT NULL, -- 跟单关系ID
account_id BIGINT NOT NULL,
leader_id BIGINT NOT NULL,
template_id BIGINT NOT NULL,
market_id VARCHAR(100) NOT NULL,
side VARCHAR(10) NOT NULL, -- YES/NO
buy_order_id VARCHAR(100) NOT NULL, -- 跟单买入订单ID
leader_buy_trade_id VARCHAR(100) NOT NULL, -- Leader 买入交易ID
quantity DECIMAL(20, 8) NOT NULL, -- 买入数量
price DECIMAL(20, 8) NOT NULL, -- 买入价格
matched_quantity DECIMAL(20, 8) NOT NULL DEFAULT 0, -- 已匹配卖出数量
remaining_quantity DECIMAL(20, 8) NOT NULL, -- 剩余未匹配数量
status VARCHAR(20) NOT NULL, -- filled, fully_matched, partially_matched
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL,
INDEX idx_copy_trading (copy_trading_id),
INDEX idx_remaining (remaining_quantity, status)
);
```
### 3.2 卖出匹配记录表(sell_match_record
```sql
CREATE TABLE sell_match_record (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
copy_trading_id BIGINT NOT NULL,
sell_order_id VARCHAR(100) NOT NULL, -- 跟单卖出订单ID
leader_sell_trade_id VARCHAR(100) NOT NULL, -- Leader 卖出交易ID
market_id VARCHAR(100) NOT NULL,
side VARCHAR(10) NOT NULL,
total_matched_quantity DECIMAL(20, 8) NOT NULL, -- 总匹配数量
sell_price DECIMAL(20, 8) NOT NULL, -- 卖出价格
total_realized_pnl DECIMAL(20, 8) NOT NULL, -- 总已实现盈亏
created_at BIGINT NOT NULL,
INDEX idx_copy_trading (copy_trading_id)
);
```
### 3.3 匹配明细表(sell_match_detail
```sql
CREATE TABLE sell_match_detail (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
match_record_id BIGINT NOT NULL, -- 关联 sell_match_record.id
tracking_id BIGINT NOT NULL, -- 关联 copy_order_tracking.id
buy_order_id VARCHAR(100) NOT NULL,
matched_quantity DECIMAL(20, 8) NOT NULL, -- 匹配的数量
buy_price DECIMAL(20, 8) NOT NULL,
sell_price DECIMAL(20, 8) NOT NULL,
realized_pnl DECIMAL(20, 8) NOT NULL, -- 盈亏 = (sell_price - buy_price) * matched_quantity
created_at BIGINT NOT NULL,
FOREIGN KEY (match_record_id) REFERENCES sell_match_record(id),
FOREIGN KEY (tracking_id) REFERENCES copy_order_tracking(id)
);
```
## 4. 核心流程
### 4.1 买入订单跟踪流程
```
Leader 买入交易
检测到交易,计算跟单数量
根据模板模式计算:
- RATIO 模式: 数量 = Leader 数量 × copyRatio
- FIXED 模式: 数量 = fixedAmount / 买入价格
创建跟单买入订单
记录到 copy_order_tracking
- quantity: 买入数量
- price: 买入价格
- remaining_quantity: 初始等于 quantity
- status: "filled"
```
### 4.2 卖出订单匹配流程
```
Leader 卖出交易
查找未匹配的买入订单(remaining_quantity > 0
按 FIFO 顺序匹配
计算匹配数量(统一按比例,不区分模式)
- 需要匹配数量 = Leader 卖出数量 × copyRatio
- 实际匹配数量 = min(需要匹配数量, 剩余持仓数量)
更新买入订单状态
- matched_quantity += 匹配数量
- remaining_quantity -= 匹配数量
- status: 根据剩余数量更新
记录匹配关系
- sell_match_record: 卖出订单记录
- sell_match_detail: 匹配明细(每笔买入订单的匹配)
```
**重要说明**
- **买入时**:根据模板的 `copyMode` 计算(RATIO 按比例,FIXED 按固定金额)
- **卖出时**:统一按比例计算(`Leader 卖出数量 × copyRatio`),不区分模式
- **固定金额模式**:只影响买入时的计算,卖出时仍然按比例
### 4.3 匹配计算示例
#### 示例1:比例模式
```
场景(比例模式,copyRatio = 100%):
- 买入订单1: quantity=100, remaining=100
- 买入订单2: quantity=50, remaining=50
- Leader 卖出: 120
匹配过程:
1. 计算需要匹配:120 × 100% = 120
2. 订单1: 匹配 min(100, 120) = 100,剩余需匹配 = 20
3. 订单2: 匹配 min(50, 20) = 20,剩余需匹配 = 0
结果:
- 订单1: remaining = 0, status = "fully_matched"
- 订单2: remaining = 30, status = "partially_matched"
- 跟单卖出: 120
```
#### 示例2:固定金额模式
```
场景(固定金额模式,fixedAmount = 15 USDCcopyRatio = 100%):
- Leader 买入: 100 数量,价格 0.5
- 跟单买入: 15 / 0.5 = 30 数量(固定金额)
- Leader 卖出: 50 数量,价格 0.7
匹配过程:
1. 计算需要匹配:50 × 100% = 50(按比例,不按固定金额)
2. 订单1: 匹配 min(30, 50) = 30,剩余需匹配 = 20
结果:
- 订单1: remaining = 0, status = "fully_matched"
- 跟单卖出: 30(不超过持仓)
- 注意:虽然买入时是固定金额,但卖出时按比例计算
```
#### 示例3:部分比例模式
```
场景(比例模式,copyRatio = 30%):
- Leader 买入: 100 数量
- 跟单买入: 100 × 30% = 30 数量
- Leader 卖出: 50 数量
匹配过程:
1. 计算需要匹配:50 × 30% = 15
2. 订单1: 匹配 min(30, 15) = 15,剩余需匹配 = 0
结果:
- 订单1: remaining = 15, status = "partially_matched"
- 跟单卖出: 15
```
## 5. 统计功能
### 5.1 跟单关系统计
**统计维度**
- 总买入数量/金额/订单数
- 总卖出数量/金额/订单数
- 当前持仓数量
- 平均买入价格
- 总已实现盈亏
- 总未实现盈亏(持仓盈亏)
- 总盈亏及百分比
**计算方式**
```kotlin
// 使用 util 方法进行数值计算
val totalBuyQuantity = buyOrders.sumOf { it.quantity.toSafeBigDecimal() }
val totalSellQuantity = sellOrders.sumOf { it.quantity.toSafeBigDecimal() }
val currentPosition = buyOrders.sumOf { it.remainingQuantity.toSafeBigDecimal() }
// 已实现盈亏
val totalRealizedPnl = matchDetails.sumOf { it.realizedPnl.toSafeBigDecimal() }
// 未实现盈亏(需要当前市场价格)
val currentPrice = getMarketCurrentPrice(marketId)
val avgBuyPrice = totalBuyAmount.div(totalBuyQuantity)
val unrealizedPnl = currentPosition.multi(currentPrice.subtract(avgBuyPrice))
// 总盈亏
val totalPnl = totalRealizedPnl.add(totalUnrealizedPnl)
```
### 5.2 订单信息
**买入订单列表**
- 订单ID、Leader 交易ID
- 市场、方向、数量、价格
- 已匹配数量、剩余数量
- 订单状态
**卖出订单列表**
- 订单ID、Leader 交易ID
- 市场、方向、数量、价格
- 已实现盈亏
**匹配关系列表**
- 卖出订单ID
- 匹配的买入订单ID
- 匹配数量
- 买入价格、卖出价格
- 盈亏
## 6. 数值计算规范
**使用 util 扩展方法**
- `toSafeBigDecimal()`: 安全转换为 BigDecimal
- `multi()`: 乘法运算
- `div()`: 除法运算
- `eq()`, `lt()`, `gt()`, `gte()`, `lte()`: 比较运算
**示例**
```kotlin
// 计算匹配数量
val matchedQty = min(remainingQty.toSafeBigDecimal(), needMatchQty.toSafeBigDecimal())
// 计算盈亏
val pnl = sellPrice.toSafeBigDecimal()
.subtract(buyPrice.toSafeBigDecimal())
.multi(matchedQty)
// 比较数量
if (remainingQty.toSafeBigDecimal().gt(BigDecimal.ZERO)) {
// 还有剩余
}
```
## 7. 关键实现点
### 7.1 买入数量计算
```kotlin
// 买入时根据模式计算
fun calculateBuyQuantity(leaderTrade: Trade, template: CopyTradingTemplate): BigDecimal {
return when (template.copyMode) {
"RATIO" -> {
// 比例模式:Leader 数量 × 比例
leaderTrade.size.toSafeBigDecimal()
.multi(template.copyRatio)
}
"FIXED" -> {
// 固定金额模式:固定金额 / 买入价格
val fixedAmount = template.fixedAmount?.toSafeBigDecimal()
?: throw IllegalStateException("固定金额模式下 fixedAmount 不能为空")
val buyPrice = leaderTrade.price.toSafeBigDecimal()
fixedAmount.div(buyPrice)
}
else -> throw IllegalArgumentException("不支持的 copyMode: ${template.copyMode}")
}
}
```
### 7.2 卖出匹配算法
```kotlin
// 卖出时统一按比例计算(不区分模式)
fun matchSellOrder(leaderSellTrade: Trade, copyTrading: CopyTrading, template: CopyTradingTemplate): BigDecimal {
// 统一按比例计算,不区分 RATIO 或 FIXED 模式
val needMatch = leaderSellTrade.size.toSafeBigDecimal()
.multi(template.copyRatio)
val unmatchedOrders = findUnmatchedBuyOrders(copyTrading.id, leaderSellTrade.market, leaderSellTrade.side)
var totalMatched = BigDecimal.ZERO
var remaining = needMatch
for (order in unmatchedOrders) {
if (remaining.lte(BigDecimal.ZERO)) break
val matchQty = min(order.remainingQuantity.toSafeBigDecimal(), remaining)
totalMatched = totalMatched.add(matchQty)
remaining = remaining.subtract(matchQty)
updateOrderTracking(order, matchQty)
recordMatchDetail(order, matchQty, leaderSellTrade)
}
return totalMatched
}
```
### 7.3 状态更新
```kotlin
fun updateOrderStatus(tracking: CopyOrderTracking) {
when {
tracking.remainingQuantity.toSafeBigDecimal().eq(BigDecimal.ZERO) -> {
tracking.status = "fully_matched"
}
tracking.matchedQuantity.toSafeBigDecimal().gt(BigDecimal.ZERO) -> {
tracking.status = "partially_matched"
}
else -> {
tracking.status = "filled"
}
}
}
```
## 8. API 设计
### 8.1 查询跟单统计
```
POST /api/copy-trading/statistics/detail
Request: { copyTradingId: Long }
Response: CopyTradingStatisticsResponse
```
### 8.2 查询订单列表
```
POST /api/copy-trading/orders/tracking
Request: { copyTradingId: Long, type: "buy" | "sell" | "matched" }
Response: OrderListResponse
```
## 9. 优势
1. **精确匹配**:每笔卖出都能追溯到对应的买入订单
2. **准确盈亏**:可以精确计算每笔交易的盈亏
3. **完整统计**:支持多维度数据统计和分析
4. **可追溯性**:完整的买入-卖出匹配关系,便于审计
## 10. WebSocket 与轮询去重机制
### 10.1 同时运行策略
**WebSocket 和轮询可以同时运行**
- **WebSocket**:作为主要数据源,实时接收交易推送
- **轮询**:作为补充数据源,定期查询确保不遗漏
- **去重机制**:通过 trade_id 确保同一笔交易只处理一次
### 10.2 去重数据模型
#### 已处理交易表(processed_trade
```sql
CREATE TABLE processed_trade (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
leader_id BIGINT NOT NULL,
leader_trade_id VARCHAR(100) NOT NULL, -- Leader 的交易IDtrade.id,唯一标识)
trade_type VARCHAR(10) NOT NULL, -- BUY 或 SELL
source VARCHAR(20) NOT NULL, -- 'websocket' 或 'polling'
processed_at BIGINT NOT NULL,
created_at BIGINT NOT NULL,
UNIQUE KEY uk_leader_trade (leader_id, leader_trade_id),
INDEX idx_processed_at (processed_at)
);
```
**唯一标识**`leader_id + leader_trade_id` 组合作为唯一键
**重要说明**
- `leader_trade_id` 对应 `TradeResponse.id`(交易ID
- 交易事件(trade)只有 `id` 字段,没有 `order_id` 字段
- 通过 `trade.id` 进行去重,确保同一笔交易只处理一次
### 10.3 去重流程
```kotlin
/**
* 处理交易事件(WebSocket 或轮询)
*/
suspend fun processTrade(leaderId: Long, trade: TradeResponse, source: String) {
// 1. 检查是否已处理(去重)
// 使用 trade.id 作为唯一标识(TradeResponse 只有 id 字段,没有 order_id
val isProcessed = processedTradeRepository.existsByLeaderIdAndLeaderTradeId(
leaderId,
trade.id // trade.id 是交易ID,用于去重
)
if (isProcessed) {
logger.debug("交易已处理,跳过: leaderId=$leaderId, tradeId=${trade.id}, source=$source")
return
}
// 2. 处理交易逻辑
try {
// 根据 side 判断是买入还是卖出
when (trade.side.uppercase()) {
"BUY" -> processBuyTrade(leaderId, trade)
"SELL" -> processSellTrade(leaderId, trade)
else -> {
logger.warn("未知的交易方向: ${trade.side}")
return
}
}
// 3. 标记为已处理
val processed = ProcessedTrade(
leaderId = leaderId,
leaderTradeId = trade.id, // 使用 trade.id 作为唯一标识
tradeType = trade.side,
source = source,
processedAt = System.currentTimeMillis()
)
processedTradeRepository.save(processed)
logger.info("成功处理交易: leaderId=$leaderId, tradeId=${trade.id}, source=$source, side=${trade.side}")
} catch (e: Exception) {
logger.error("处理交易失败: leaderId=$leaderId, tradeId=${trade.id}", e)
// 失败时不标记为已处理,允许重试
}
}
```
### 10.4 并发安全
**使用数据库唯一约束保证并发安全**
- 数据库唯一约束:`UNIQUE KEY uk_leader_trade (leader_id, leader_trade_id)`
- 如果 WebSocket 和轮询同时收到同一笔交易:
- 第一个请求:成功处理并插入记录
- 第二个请求:插入失败(唯一约束),跳过处理
**或者使用分布式锁**
```kotlin
// 使用 Redis 分布式锁
val lockKey = "trade:${leaderId}:${trade.id}"
if (redisLock.tryLock(lockKey, 5, TimeUnit.SECONDS)) {
try {
if (!isProcessed(leaderId, trade.id)) {
processTrade(leaderId, trade)
markAsProcessed(leaderId, trade.id)
}
} finally {
redisLock.unlock(lockKey)
}
}
```
### 10.5 清理策略
**定期清理过期记录**
```kotlin
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨 2 点
fun cleanupProcessedTrades() {
val expireTime = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7) // 保留 7 天
processedTradeRepository.deleteByProcessedAtBefore(expireTime)
}
```
### 10.6 优势
1. **高可用性**:WebSocket 断开时,轮询继续工作
2. **数据完整性**:轮询确保不遗漏任何交易
3. **实时性**WebSocket 提供实时推送
4. **去重保证**:通过唯一标识确保不重复处理
## 11. 注意事项
1. **匹配策略**:默认使用 FIFO,可根据需求调整
2. **部分匹配**:支持一个买入订单被多次卖出匹配
3. **数量计算**:使用 util 方法确保数值计算安全
4. **状态同步**:及时更新订单状态,确保数据一致性
5. **模式区别**
- **买入时**RATIO 模式按比例计算,FIXED 模式按固定金额计算
- **卖出时**:统一按比例计算(`Leader 卖出数量 × copyRatio`),不区分模式
- **固定金额模式**:只影响买入时的计算,卖出时仍然按比例
6. **去重机制**
- WebSocket 和轮询可以同时运行
- 使用 `leader_id + leader_trade_id` 作为唯一标识去重
- 数据库唯一约束保证并发安全
- 定期清理过期记录(建议保留 7 天)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,489 @@
# GitHub API 限流问题及替代方案
## 📊 GitHub API 限流情况
### REST API 限流规则
- **未认证请求**:每小时 60 次
- **认证请求(使用 Token**:每小时 5,000 次
- **限流检测**:响应头 `X-RateLimit-Remaining` 显示剩余次数
- **限流重置**:响应头 `X-RateLimit-Reset` 显示重置时间(Unix 时间戳)
### GraphQL API 限流规则(基于点数)
#### 主要限流规则
- **未认证请求**:每小时 60 次(与 REST API 相同)
- **认证请求(使用 Token**
- 个人用户/应用:5,000 点/小时
- 组织拥有的应用:10,000 点/小时
- **每分钟点数限制**:2,000 点/分钟(仅限认证请求)
#### 点数计算规则
- **查询请求(Query)**:每个查询消耗 **1 点**
- **变更请求(Mutation)**:每个变更消耗 **5 点**
- **复杂度计算**:查询的复杂度会影响点数消耗(但基础查询通常为 1 点)
#### 次要限流规则
- **并发请求限制**:同时进行的请求不得超过 100 个
- **CPU 时间限制**:每 60 秒实际时间内,最大 CPU 时间为 90 秒(GraphQL API 为 60 秒)
- **内容创建限制**
- 每分钟不超过 80 个内容生成请求
- 每小时不超过 500 个内容生成请求
#### 查询限制
- 必须在连接上提供 `first``last` 参数
- `first``last` 的值必须在 1 到 100 之间
- 单个调用请求的节点总数不能超过 500,000
#### 限流检测
GraphQL API 的限流信息在响应中返回:
```json
{
"data": { ... },
"extensions": {
"rateLimit": {
"limit": 5000,
"remaining": 4998,
"resetAt": "2024-12-07T15:00:00Z",
"used": 2
}
}
}
```
### 当前使用场景(REST API
- 获取 Issue 信息(获取 assignees):每次请求 1 次
- 获取 Issue 评论列表:每次请求 1 次
- **总计**:每次获取公告列表需要 2 次 API 调用
- **缓存时间**:1 分钟(已实现)
### 限流风险分析
#### REST API
- **未认证**:60 次/小时 ÷ 2 次/请求 = 最多 30 次请求/小时
- **认证后**5,000 次/小时 ÷ 2 次/请求 = 最多 2,500 次请求/小时
- **实际使用**:用户刷新 + 自动加载,可能触发限流
#### GraphQL API(如果迁移)
- **未认证**60 次/小时(与 REST API 相同)
- **认证后**
- 查询消耗:1 个 GraphQL 查询 = 1 点(获取 Issue + Comments + Reactions
- 限流容量:5,000 点/小时 = 最多 5,000 次请求/小时
- **优势**:单次请求获取所有数据,请求次数减少 50%
- **实际使用**:认证后 5,000 次/小时足够使用
---
## 🔧 替代方案对比
### 方案 1:使用 GitHub Token 认证(推荐 ⭐⭐⭐⭐⭐)
**优点:**
- ✅ 实现简单,只需添加 Token
- ✅ 限流提升:60 → 5,000 次/小时(提升 83 倍)
- ✅ 无需额外服务
- ✅ 成本低(免费)
**缺点:**
- ❌ 需要用户提供 GitHub Token
- ❌ Token 需要存储(建议加密)
**实现方式:**
```kotlin
// 在拦截器中添加 Authorization 头
.header("Authorization", "token $githubToken")
```
**适用场景:** 推荐作为首选方案
---
### 方案 2:使用缓存机制(已实现 ⭐⭐⭐⭐)
**优点:**
- ✅ 已实现 1 分钟缓存
- ✅ 减少 API 调用次数
- ✅ 提升响应速度
**缺点:**
- ❌ 数据可能不是最新的
- ❌ 缓存时间需要平衡
**优化建议:**
- 可以延长缓存时间到 5-10 分钟(公告更新频率低)
- 实现多级缓存(内存 + Redis)
**适用场景:** 配合其他方案使用
---
### 方案 3:使用 GraphQL API(推荐 ⭐⭐⭐⭐)
**优点:**
- ✅ 单次请求获取所有数据(Issue + Comments + Reactions
- ✅ 减少请求次数:2 次 → 1 次
- ✅ 可以精确控制返回字段
- ✅ 限流更宽松:5,000 点/小时(查询消耗 1 点/次)
- ✅ 认证后限流充足(5,000 次/小时)
**缺点:**
- ❌ 需要学习 GraphQL 语法
- ❌ 需要修改现有代码
- ❌ 需要处理 GraphQL 响应格式
**限流对比:**
- REST API(认证):5,000 次/小时 ÷ 2 次/请求 = 2,500 次完整请求/小时
- GraphQL API(认证):5,000 点/小时 ÷ 1 点/请求 = 5,000 次完整请求/小时
- **提升**GraphQL 比 REST 多 100% 的请求容量
**实现方式:**
```graphql
query {
repository(owner: "WrBug", name: "PolyHermes") {
issue(number: 1) {
assignees(first: 10) {
nodes {
login
}
}
comments(first: 100) {
nodes {
id
body
createdAt
updatedAt
issue {
id
}
author {
login
avatarUrl
}
reactions(first: 100) {
totalCount
nodes {
content
}
}
}
}
}
}
}
```
**响应格式:**
```json
{
"data": {
"repository": {
"issue": {
"assignees": {
"nodes": [
{ "login": "WrBug" }
]
},
"comments": {
"nodes": [
{
"id": "123",
"body": "...",
"reactions": {
"totalCount": 10,
"nodes": [
{ "content": "THUMBS_UP" },
{ "content": "HEART" }
]
}
}
]
}
}
}
},
"extensions": {
"rateLimit": {
"limit": 5000,
"remaining": 4999,
"resetAt": "2024-12-07T15:00:00Z"
}
}
}
```
**适用场景:** 适合需要优化请求次数和限流容量的场景
---
### 方案 4:自建代理服务 ⭐⭐⭐
**优点:**
- ✅ 可以添加额外缓存层(5-30 分钟)
- ✅ 可以聚合多个请求
- ✅ 可以添加限流保护
- ✅ 可以 Token 轮换(多个 Token 共享限流)
- ✅ 免费额度充足(Cloudflare Workers 100,000 次/天)
**缺点:**
- ❌ 需要额外部署服务
- ❌ 增加系统复杂度
- ❌ 需要维护
**实现方式:**
```
用户请求 → 自建代理(Cloudflare Workers/Vercel → GitHub API
← 缓存响应(5-30分钟) ←
```
**可选平台:**
- **Cloudflare Workers**:免费 100,000 次/天
- **Vercel Edge Functions**:免费额度充足
- **Netlify Functions**:免费额度充足
- **自建 Node.js 服务**:完全控制
**适用场景:** 需要更高可用性和更长缓存时间的场景
---
### 方案 4.1:第三方 GitHub API 代理服务 ❌
**结论:没有可用的第三方服务**
**原因:**
1.**数据源限制**:公告数据在 GitHub Issue,无法迁移到其他平台
2.**认证问题**GitHub API 需要 Token,第三方服务无法安全共享用户 Token
3.**服务缺失**:没有公开的、稳定的第三方 GitHub API 代理服务
4.**商业限制**:GitHub 不允许第三方服务代理其 API(违反 ToS)
**为什么不可行:**
- 数据在 GitHub,必须调用 GitHub API
- Token 是个人凭证,不能共享给第三方
- 没有公开的代理服务(违反 GitHub ToS)
**可行的替代思路:**
-**自建代理服务**(方案 4):使用 Cloudflare Workers 等平台
-**使用 GitHub Token**(方案 1):直接认证,限流提升 83 倍
-**使用 GraphQL API**(方案 3):减少请求次数,提升限流容量
---
### 方案 5:使用 GitHub Webhook(不适用)❌
**说明:**
- Webhook 是事件驱动的,不适合主动获取数据
- 公告功能需要主动查询,不适合 Webhook
**适用场景:** 不适用于当前需求
---
### 方案 6:使用其他代码托管平台 API(不适用)❌
**说明:**
- GitLab、Bitbucket、Gitee 等不包含 GitHub 的 Issue 数据
- 公告数据在 GitHub,无法迁移到其他平台
- 这些平台的 API 无法访问 GitHub 的数据
**适用场景:** 不适用于当前需求(数据在 GitHub)
---
## 🎯 推荐方案组合
### 方案 A:Token + 缓存(推荐)⭐⭐⭐⭐⭐
**组合:**
1. 使用 GitHub Token 认证(提升限流到 5,000/小时)
2. 保持 1-5 分钟缓存
3. 添加限流检测和错误处理
**优点:**
- 实现简单
- 限流充足(5,000/小时足够使用)
- 响应快速(缓存)
**实现成本:**
---
### 方案 BGraphQL + Token + 缓存 ⭐⭐⭐⭐⭐
**组合:**
1. 使用 GraphQL API(减少请求次数,提升限流容量)
2. 使用 GitHub Token 认证
3. 保持缓存机制
**优点:**
- 请求次数最少(1 次/请求)
- 限流容量最大(5,000 次/小时,比 REST 多 100%
- 数据获取更高效(单次请求获取所有数据)
- 可以精确控制返回字段
**实现成本:** 中等(需要学习 GraphQL
**限流对比:**
- REST API2,500 次完整请求/小时
- GraphQL API5,000 次完整请求/小时
- **提升**:100% 的请求容量提升
---
### 方案 C:自建代理服务 + 缓存 ⭐⭐⭐
**组合:**
1. 使用 Cloudflare Workers / Vercel Edge Functions 自建代理
2. 在代理层添加缓存(5-30 分钟)
3. 聚合请求(可选)
4. Token 轮换(可选,多个 Token 共享限流)
**优点:**
- 可以添加更长的缓存时间(5-30 分钟)
- 可以聚合多个请求
- 可以添加限流保护
- 可以 Token 轮换(多个 Token 共享限流容量)
**实现成本:** 中等(需要部署,但平台提供免费额度)
**实现示例(Cloudflare Workers):**
```javascript
// cloudflare-worker.js
export default {
async fetch(request) {
const cacheKey = request.url;
const cache = caches.default;
// 检查缓存(5 分钟)
let response = await cache.match(cacheKey);
if (response) {
return response;
}
// 转发到 GitHub API
const githubResponse = await fetch(request, {
headers: {
'Authorization': `Bearer ${GITHUB_TOKEN}`,
'Accept': 'application/vnd.github+json'
}
});
// 缓存响应(5 分钟)
response = new Response(githubResponse.body, githubResponse);
response.headers.set('Cache-Control', 'public, max-age=300');
await cache.put(cacheKey, response.clone());
return response;
}
}
```
---
## 📝 实现建议
### 短期方案(立即实施)
1. **添加 GitHub Token 支持**
- 在配置文件中添加 `github.token` 配置项
- 在拦截器中添加 Authorization 头
- 限流从 60 → 5,000/小时
2. **优化缓存时间**
- 将缓存时间从 1 分钟延长到 5-10 分钟
- 公告更新频率低,5-10 分钟足够
3. **添加限流检测**
- 检查响应头 `X-RateLimit-Remaining`
- 当剩余次数 < 10 时,延长缓存时间
- 当触发限流时,返回缓存数据
### 中期方案(可选)
1. **迁移到 GraphQL API**
- 学习 GraphQL 语法
- 重写 API 调用
- 减少请求次数
2. **实现多级缓存**
- 内存缓存(快速)
- Redis 缓存(持久化)
### 长期方案(如需要)
1. **自建代理服务**
- 使用 Cloudflare Workers / Vercel Edge Functions
- 添加更长的缓存时间(5-30 分钟)
- 聚合多个请求
- Token 轮换(多个 Token 共享限流)
---
## 🔍 限流检测实现
### 响应头说明
- `X-RateLimit-Limit`: 总限制次数
- `X-RateLimit-Remaining`: 剩余次数
- `X-RateLimit-Used`: 已使用次数
- `X-RateLimit-Reset`: 重置时间(Unix 时间戳)
### 错误处理
当触发限流时,GitHub API 返回:
- HTTP 403 Forbidden
- 响应头 `X-RateLimit-Remaining: 0`
- 响应体包含限流信息
---
## 💡 总结
### 第三方 API 服务情况
**结论:没有可用的第三方 GitHub API 代理服务**
**原因:**
1. ❌ 数据源限制:公告数据在 GitHub,无法迁移
2. ❌ 认证问题:GitHub API 需要 Token,第三方无法安全共享
3. ❌ 服务缺失:没有公开的、稳定的第三方代理服务
**可行的替代方案:**
-**自建代理服务**:使用 Cloudflare Workers / Vercel 等平台
-**使用 GitHub Token**:直接认证,限流提升 83 倍
-**使用 GraphQL API**:减少请求次数,提升限流容量
---
### 限流对比表
| 方案 | API 类型 | 认证 | 请求次数 | 限流容量 | 完整请求数/小时 | 实现难度 |
|------|---------|------|---------|---------|----------------|---------|
| 当前 | REST | 否 | 2 次/请求 | 60 次/小时 | 30 次 | - |
| REST + Token | REST | 是 | 2 次/请求 | 5,000 次/小时 | 2,500 次 | ⭐ 简单 |
| GraphQL | GraphQL | 否 | 1 次/请求 | 60 次/小时 | 60 次 | ⭐⭐ 中等 |
| GraphQL + Token | GraphQL | 是 | 1 次/请求 | 5,000 点/小时 | 5,000 次 | ⭐⭐ 中等 |
| 自建代理 + Token | REST/GraphQL | 是 | 1-2 次/请求 | 5,000+ 次/小时 | 5,000+ 次 | ⭐⭐⭐ 中等 |
### 推荐方案
**最佳方案:** 方案 A(Token + 缓存)⭐⭐⭐⭐⭐
- 实现简单(只需添加 Token
- 效果显著(限流提升 83 倍:60 → 5,000/小时)
- 成本低
- 适合当前需求
- **限流容量**:2,500 次完整请求/小时
**优化方案:** 方案 BGraphQL + Token + 缓存)⭐⭐⭐⭐⭐
- 请求次数最少(1 次/请求)
- 限流容量最大(5,000 次/小时,比 REST 多 100%
- 数据获取更高效(单次请求获取所有数据)
- 适合长期优化
- **限流容量**:5,000 次完整请求/小时
**备选方案:** 方案 C(代理服务)⭐⭐⭐
- 适合需要更高可用性的场景
- 需要额外部署和维护
### 建议实施顺序
1. **立即实施**:方案 AToken + 缓存)
- 快速解决限流问题
- 实现成本低
2. **中期优化**:方案 BGraphQL + Token + 缓存)
- 进一步提升限流容量
- 优化请求效率
+298
View File
@@ -0,0 +1,298 @@
# GitHub Token 获取和配置指南
## 📋 概述
GitHub Personal Access Token (PAT) 用于提高 API 限流容量:
- **未认证**60 次/小时
- **使用 Token**5,000 次/小时(REST API)或 5,000 点/小时(GraphQL API
---
## 🔑 获取 GitHub Token
### 方法 1:通过 GitHub 网站创建(推荐)
#### 步骤 1:登录 GitHub
1. 访问 [GitHub](https://github.com)
2. 登录您的账户
#### 步骤 2:进入开发者设置
1. 点击右上角头像
2. 选择 **Settings**(设置)
3. 在左侧菜单中,滚动到底部
4. 点击 **Developer settings**(开发者设置)
#### 步骤 3:创建 Personal Access Token
1. 在左侧菜单中,点击 **Personal access tokens**
2. 选择 **Tokens (classic)****Fine-grained tokens**
**推荐使用 Fine-grained tokens(更安全):**
- 点击 **Generate new token****Generate new token (fine-grained)**
- 填写 Token 名称(如:`PolyHermes Announcements API`
- 设置过期时间(建议:90 天或自定义)
- 选择资源所有者(Repository access):
- 如果公告在您的仓库:选择 **Only select repositories**,然后选择 `WrBug/PolyHermes`
- 如果公告在公共仓库:选择 **Public repositories (read-only)**
- 设置权限(Repository permissions):
- **Metadata**: Read(必需)
- **Contents**: Read(如果需要读取 Issue 内容)
- **Issues**: Read(必需,用于读取 Issue 和评论)
- 点击 **Generate token**
**或使用 Classic tokens(更简单):**
- 点击 **Generate new token (classic)**
- 填写 Token 名称(如:`PolyHermes Announcements API`
- 设置过期时间
- 选择权限(Scopes):
-**public_repo**(读取公共仓库的 Issue 和评论)
- 如果仓库是私有的,需要选择 **repo**
- 点击 **Generate token**
#### 步骤 4:复制并保存 Token
⚠️ **重要**:Token 只会显示一次,请立即复制并保存到安全的地方!
```
ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
---
### 方法 2:通过 GitHub CLI 创建
如果您安装了 GitHub CLI (`gh`),可以使用命令行创建:
```bash
# 登录 GitHub CLI
gh auth login
# 创建 Token
gh auth token
```
---
## 🔐 所需权限说明
### Fine-grained Token 权限
- **Metadata**: Read(必需,读取仓库基本信息)
- **Contents**: Read(可选,读取仓库内容)
- **Issues**: Read(必需,读取 Issue 和评论)
### Classic Token 权限
- **public_repo**(公共仓库)
- **repo**(私有仓库,如果需要)
---
## ⚙️ 在项目中使用 Token
### 方式 1:环境变量(推荐)
#### 1. 在配置文件中添加 Token 配置
编辑 `backend/src/main/resources/application.properties`
```properties
# GitHub 配置(用于公告功能)
github.repo.owner=WrBug
github.repo.name=PolyHermes
github.announcement.issue.number=1
github.token=${GITHUB_TOKEN:} # 从环境变量读取,如果未设置则为空
```
#### 2. 设置环境变量
**Linux/macOS**
```bash
export GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
**Windows (PowerShell)**
```powershell
$env:GITHUB_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```
**Windows (CMD)**
```cmd
set GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
#### 3. 在 Docker 中使用
`docker-compose.yml` 或启动命令中添加:
```yaml
environment:
- GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
或在启动命令中:
```bash
docker run -e GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ...
```
---
### 方式 2:直接配置(不推荐,仅用于测试)
⚠️ **不推荐**:Token 会暴露在配置文件中,存在安全风险。
编辑 `backend/src/main/resources/application.properties`
```properties
github.token=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
---
## 💻 代码实现
### 更新 RetrofitFactory
`RetrofitFactory.kt` 中添加 Token 支持:
```kotlin
fun createGitHubApi(): GitHubApi {
val baseUrl = "https://api.github.com"
// 从配置读取 Token
val githubToken = githubToken // 从 @Value 注入
// 添加拦截器
val githubInterceptor = object : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val requestBuilder = chain.request().newBuilder()
.header("Accept", "application/vnd.github+json")
// 如果配置了 Token,添加认证头
if (githubToken.isNotBlank()) {
requestBuilder.header("Authorization", "Bearer $githubToken")
}
return chain.proceed(requestBuilder.build())
}
}
val okHttpClient = createClient()
.addInterceptor(githubInterceptor)
.build()
// ... 其余代码
}
```
---
## 🔒 安全注意事项
### 1. Token 存储
-**推荐**:使用环境变量存储 Token
-**推荐**:使用密钥管理服务(如 AWS Secrets Manager、HashiCorp Vault
-**禁止**:将 Token 提交到 Git 仓库
-**禁止**:在日志中输出 Token
### 2. Token 权限
-**最小权限原则**:只授予必要的权限
-**定期轮换**:建议每 90 天更新一次 Token
-**监控使用**:定期检查 Token 的使用情况
### 3. 配置文件
- ✅ 将 `application.properties` 添加到 `.gitignore`(如果包含 Token
- ✅ 使用 `application-local.properties` 存储本地配置
- ✅ 使用环境变量覆盖配置
---
## 🧪 测试 Token
### 使用 curl 测试
```bash
# 测试 REST API
curl -H "Authorization: Bearer YOUR_TOKEN" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/repos/WrBug/PolyHermes/issues/1
# 测试 GraphQL API
curl -X POST \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "query { viewer { login } }"}' \
https://api.github.com/graphql
```
### 检查限流
响应头中包含限流信息:
```
X-RateLimit-Limit: 5000
X-RateLimit-Remaining: 4999
X-RateLimit-Used: 1
X-RateLimit-Reset: 1701964800
```
---
## 📝 完整配置示例
### application.properties
```properties
# GitHub 配置(用于公告功能)
github.repo.owner=WrBug
github.repo.name=PolyHermes
github.announcement.issue.number=1
github.token=${GITHUB_TOKEN:} # 从环境变量读取
```
### .env 文件(用于本地开发)
```env
GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
### docker-compose.yml
```yaml
services:
backend:
environment:
- GITHUB_TOKEN=${GITHUB_TOKEN}
```
---
## 🚨 常见问题
### Q1: Token 过期了怎么办?
**A:** 重新生成新的 Token,更新环境变量或配置文件。
### Q2: Token 泄露了怎么办?
**A:** 立即在 GitHub 设置中删除该 Token,然后生成新 Token。
### Q3: 如何查看 Token 的使用情况?
**A:** 在 GitHub Settings → Developer settings → Personal access tokens 中查看 Token 的最后使用时间。
### Q4: 可以使用 GitHub App 吗?
**A:** 可以,GitHub App 的限流更高(组织应用 10,000 点/小时),但实现更复杂。
### Q5: Token 需要哪些权限?
**A:** 对于公共仓库,只需要 `public_repo` 权限;对于私有仓库,需要 `repo` 权限。
---
## 📚 参考链接
- [GitHub Personal Access Tokens 文档](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token)
- [GitHub API 认证文档](https://docs.github.com/en/rest/authentication/authenticating-to-the-rest-api)
- [GitHub API 限流文档](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting)
---
## ✅ 检查清单
- [ ] 已创建 GitHub Personal Access Token
- [ ] Token 已保存到安全的地方
- [ ] 已在环境变量中配置 Token
- [ ] 已更新 `application.properties` 配置
- [ ] 已更新代码支持 Token 认证
- [ ] 已测试 Token 是否生效
- [ ] 已检查限流是否提升(从 60 → 5,000)
- [ ] 已将 Token 相关配置添加到 `.gitignore`
+170
View File
@@ -0,0 +1,170 @@
# PolyHermes Nginx 反向代理配置示例
#
# 适用于生产环境,在 Docker 容器外部部署 Nginx 作为反向代理
#
# 使用场景:
# - SSL/TLS 终止(HTTPS
# - 域名绑定
# - 负载均衡
# - 更灵活的配置
#
# 部署步骤:
# 1. 将本文件复制到 /etc/nginx/sites-available/polyhermes
# 2. 创建软链接: ln -s /etc/nginx/sites-available/polyhermes /etc/nginx/sites-enabled/
# 3. 修改配置中的域名和 SSL 证书路径
# 4. 测试配置: nginx -t
# 5. 重载配置: systemctl reload nginx
# HTTP 服务器(可选:用于重定向到 HTTPS)
server {
listen 80;
server_name your-domain.com www.your-domain.com;
# 重定向到 HTTPS
return 301 https://$server_name$request_uri;
}
# HTTPS 服务器
server {
listen 443 ssl http2;
server_name your-domain.com www.your-domain.com;
# SSL 证书配置(使用 Let's Encrypt 或其他证书)
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
# SSL 安全配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# 安全头
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# 日志
access_log /var/log/nginx/polyhermes-access.log;
error_log /var/log/nginx/polyhermes-error.log;
# 客户端最大上传大小
client_max_body_size 10M;
# 上游服务(Docker 容器)
# 如果使用 docker-compose,容器名是 polyhermes,端口是 80
upstream polyhermes_backend {
server 127.0.0.1:80;
# 如果需要负载均衡,可以添加多个后端:
# server 127.0.0.1:8001;
# server 127.0.0.1:8002;
}
# API 代理
location /api {
proxy_pass http://polyhermes_backend;
proxy_set_header Host $host;
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;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# 缓冲设置
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
}
# WebSocket 代理
location /ws {
proxy_pass http://polyhermes_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
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;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# WebSocket 超时设置(长连接)
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
}
# 前端静态文件代理
location / {
proxy_pass http://polyhermes_backend;
proxy_set_header Host $host;
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;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# 静态资源缓存(由后端 Nginx 处理)
proxy_cache_valid 200 1y;
}
# 健康检查(可选)
location /health {
proxy_pass http://polyhermes_backend;
access_log off;
}
}
# 如果不需要 HTTPS,可以使用以下简化配置
# server {
# listen 80;
# server_name your-domain.com www.your-domain.com;
#
# access_log /var/log/nginx/polyhermes-access.log;
# error_log /var/log/nginx/polyhermes-error.log;
#
# client_max_body_size 10M;
#
# upstream polyhermes_backend {
# server 127.0.0.1:80;
# }
#
# location /api {
# proxy_pass http://polyhermes_backend;
# proxy_set_header Host $host;
# 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 /ws {
# proxy_pass http://polyhermes_backend;
# proxy_http_version 1.1;
# proxy_set_header Upgrade $http_upgrade;
# proxy_set_header Connection "upgrade";
# proxy_set_header Host $host;
# 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;
# proxy_read_timeout 86400;
# proxy_send_timeout 86400;
# }
#
# location / {
# proxy_pass http://polyhermes_backend;
# proxy_set_header Host $host;
# 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;
# }
# }
+357
View File
@@ -0,0 +1,357 @@
# 仓位出售订单功能设计文档
## 1. 功能概述
为仓位管理页面添加出售功能,支持用户对当前仓位进行市价或限价卖出操作。
## 2. 后端接口设计
### 2.1 创建卖出订单接口
**接口路径**: `POST /api/copy-trading/positions/sell`
**请求体**:
```kotlin
data class PositionSellRequest(
val accountId: Long, // 账户ID(必需)
val marketId: String, // 市场ID(必需)
val side: String, // 方向:YES 或 NO(必需)
val orderType: String, // 订单类型:MARKET(市价)或 LIMIT(限价)(必需)
val quantity: String, // 卖出数量(必需,BigDecimal字符串)
val price: String? = null // 限价价格(限价订单必需,市价订单不需要)
)
```
**响应体**:
```kotlin
data class PositionSellResponse(
val orderId: String, // 订单ID
val marketId: String, // 市场ID
val side: String, // 方向
val orderType: String, // 订单类型
val quantity: String, // 订单数量
val price: String?, // 订单价格(限价订单)
val status: String, // 订单状态
val createdAt: Long // 创建时间戳
)
```
**业务逻辑**:
1. 验证账户是否存在且已配置API凭证
2. 验证仓位是否存在且数量足够
3. 验证订单参数(数量、价格等)
4. 市价订单:获取当前最优卖价(bestBid)作为价格
5. 限价订单:验证价格是否合理
6. 调用Polymarket CLOB API创建订单
7. 返回订单信息
**错误处理**:
- 账户不存在或未配置API凭证:返回错误码 2001
- 仓位不存在或数量不足:返回错误码 4001
- 价格或数量格式错误:返回错误码 1001
- API调用失败:返回错误码 5001
### 2.2 获取市场当前价格接口(可选,用于显示参考价格)
**接口路径**: `POST /api/copy-trading/markets/price`
**请求体**:
```kotlin
data class MarketPriceRequest(
val marketId: String // 市场ID
)
```
**响应体**:
```kotlin
data class MarketPriceResponse(
val marketId: String,
val lastPrice: String?, // 最新成交价
val bestBid: String?, // 最优买价(用于卖出参考)
val bestAsk: String?, // 最优卖价(用于买入参考)
val midpoint: String? // 中间价
)
```
## 3. 前端交互设计
### 3.1 UI组件设计
#### 3.1.1 出售按钮
- **位置**: 每个仓位卡片/列表项的操作区域
- **样式**:
- 卡片视图:卡片底部或操作区域
- 列表视图:操作列
- **显示条件**: 仅当前仓位显示(历史仓位不显示)
- **按钮文本**: "卖出" 或 "出售"
#### 3.1.2 出售模态框
**布局结构**:
```
┌─────────────────────────────────┐
│ 出售仓位 - [市场标题] │
├─────────────────────────────────┤
│ 账户: [账户名称] │
│ 方向: [YES/NO标签] │
│ 当前持仓: [数量] │
│ 平均价格: [平均买入价格] │
│ 当前价格: [当前市场价格] │
├─────────────────────────────────┤
│ 订单类型: │
│ ○ 市价出售 ○ 限价出售 │
├─────────────────────────────────┤
│ 卖出数量: │
│ [输入框] │
│ [20%] [50%] [80%] [100%] │
├─────────────────────────────────┤
│ 限价价格: (限价时显示) │
│ [输入框] │
│ 参考价格: [当前最优买价] │
├─────────────────────────────────┤
│ 预计平仓收益: │
│ 收益金额: [+/-XXX.XX USDC] │
│ 收益率: [+/-XX.XX%] │
│ (实时计算,根据数量和价格更新) │
├─────────────────────────────────┤
│ [取消] [确认卖出] │
└─────────────────────────────────┘
```
**字段说明**:
1. **订单类型选择**:
- 单选按钮:市价出售 / 限价出售
- 默认:限价出售
- 切换时显示/隐藏限价输入框
2. **卖出数量**:
- 输入框:支持手动输入
- 快捷按钮:20%, 50%, 80%, 100%
- 点击快捷按钮自动填充到输入框
- 验证:不能超过当前持仓数量,不能为0
3. **限价价格**(限价订单时显示):
- 输入框:支持手动输入
- 显示参考价格:当前最优买价(bestBid)
- 验证:价格必须大于0
4. **按钮**:
- 取消:关闭模态框
- 确认卖出:提交订单(加载状态)
### 3.2 交互流程
1. **打开模态框**:
- 点击"卖出"按钮
- 加载市场当前价格(用于显示参考价格)
- 初始化表单(默认限价,数量为空)
2. **选择订单类型**:
- 切换市价/限价
- 市价:隐藏限价输入框
- 限价:显示限价输入框和参考价格
3. **设置数量**:
- 点击快捷按钮(20%, 50%, 80%, 100%
- 自动计算并填充到输入框
- 实时验证数量是否有效
4. **设置限价**(限价订单):
- 手动输入价格
- 显示参考价格提示
- 实时更新平仓收益
5. **查看平仓收益**(实时计算):
- 根据卖出数量和价格实时计算
- 计算公式:
- 收益金额 = (卖出价格 - 平均买入价格) × 卖出数量
- 收益率 = (卖出价格 - 平均买入价格) / 平均买入价格 × 100%
- 市价订单:使用当前最优买价计算
- 限价订单:使用输入的限价计算
- 颜色显示:盈利为绿色,亏损为红色
6. **提交订单**:
- 点击"确认卖出"
- 显示加载状态
- 调用后端接口
- 成功:显示成功提示,关闭模态框,刷新仓位列表
- 失败:显示错误提示
### 3.3 数据验证
**前端验证**:
- 数量:必填,大于0,不超过当前持仓数量
- 限价价格:限价订单必填,大于0
- 账户:必须已配置API凭证
**后端验证**:
- 账户存在且已配置API凭证
- 仓位存在且数量足够
- 价格和数量格式正确
- 市价订单自动获取最优价格
## 4. 类型定义
### 4.1 前端TypeScript类型
```typescript
/**
* 仓位卖出请求
*/
export interface PositionSellRequest {
accountId: number
marketId: string
side: 'YES' | 'NO'
orderType: 'MARKET' | 'LIMIT'
quantity: string
price?: string // 限价订单必需
}
/**
* 仓位卖出响应
*/
export interface PositionSellResponse {
orderId: string
marketId: string
side: string
orderType: string
quantity: string
price?: string
status: string
createdAt: number
}
/**
* 市场价格请求
*/
export interface MarketPriceRequest {
marketId: string
}
/**
* 市场价格响应
*/
export interface MarketPriceResponse {
marketId: string
lastPrice?: string
bestBid?: string
bestAsk?: string
midpoint?: string
}
```
## 5. API服务方法
### 5.1 前端API服务
```typescript
// frontend/src/services/api.ts
export const apiService = {
positions: {
/**
* 卖出仓位
*/
sell: (data: PositionSellRequest) =>
apiClient.post<ApiResponse<PositionSellResponse>>('/copy-trading/positions/sell', data),
/**
* 获取市场价格
*/
getMarketPrice: (data: MarketPriceRequest) =>
apiClient.post<ApiResponse<MarketPriceResponse>>('/copy-trading/markets/price', data)
}
}
```
### 5.2 后端Controller方法
```kotlin
@PostMapping("/positions/sell")
suspend fun sellPosition(@RequestBody request: PositionSellRequest): ResponseEntity<ApiResponse<PositionSellResponse>>
@PostMapping("/markets/price")
suspend fun getMarketPrice(@RequestBody request: MarketPriceRequest): ResponseEntity<ApiResponse<MarketPriceResponse>>
```
## 6. 实现细节
### 6.1 市价订单处理
- 市价订单需要获取当前最优买价(bestBid)作为卖出价格
- 如果无法获取最优买价,使用最新成交价(lastPrice)
- 如果都没有,返回错误提示
### 6.2 数量计算
- 快捷按钮计算:`数量 = 当前持仓数量 × 百分比`
- 保留4位小数(与仓位数量精度一致)
- 验证:不能超过当前持仓数量
### 6.3 平仓收益实时计算
**计算逻辑**:
```typescript
// 获取仓位信息
const avgPrice = parseFloat(position.avgPrice) // 平均买入价格
const quantity = parseFloat(sellQuantity) // 卖出数量
const sellPrice = orderType === 'MARKET'
? parseFloat(marketPrice.bestBid) // 市价:使用最优买价
: parseFloat(limitPrice) // 限价:使用输入价格
// 计算收益
const pnl = (sellPrice - avgPrice) * quantity
const percentPnl = ((sellPrice - avgPrice) / avgPrice) * 100
// 显示格式
const pnlDisplay = `${pnl >= 0 ? '+' : ''}${pnl.toFixed(2)} USDC`
const percentPnlDisplay = `${percentPnl >= 0 ? '+' : ''}${percentPnl.toFixed(2)}%`
```
**更新时机**:
- 数量输入框值变化时
- 限价价格输入框值变化时(限价订单)
- 订单类型切换时(市价/限价)
- 市场价格更新时(市价订单,如果支持实时更新)
**显示样式**:
- 盈利:绿色文字(#52c41a
- 亏损:红色文字(#f5222d
- 字体:加粗显示,突出重要性
### 6.4 错误处理
- 网络错误:显示"网络错误,请重试"
- API错误:显示后端返回的错误信息
- 验证错误:显示具体的验证失败原因
### 6.5 用户体验优化
- 提交订单时禁用按钮,显示加载状态
- 成功后自动刷新仓位列表
- 提供清晰的成功/失败提示
- 模态框支持ESC键关闭
## 7. 安全考虑
1. **权限验证**: 验证账户是否属于当前用户
2. **数量验证**: 确保卖出数量不超过持仓数量
3. **价格验证**: 限价订单验证价格合理性
4. **API凭证**: 确保账户已配置有效的API凭证
## 8. 测试要点
1. 市价订单创建成功
2. 限价订单创建成功
3. 数量快捷按钮功能
4. 数量验证(超过持仓、为0等)
5. 价格验证(限价订单)
6. **平仓收益实时计算**:
- 数量变化时收益更新
- 限价变化时收益更新
- 订单类型切换时收益更新
- 收益金额和收益率计算正确
- 盈利/亏损颜色显示正确
7. 账户未配置API凭证的错误处理
8. 仓位不存在的错误处理
9. 网络错误处理
+213
View File
@@ -0,0 +1,213 @@
# 三元市场订单簿实现说明
## 概述
Polymarket 的三元市场(Ternary Market)是指具有三个或更多可能结果的市场。与二元市场(YES/NO)不同,三元市场需要为每个 outcome 维护独立的订单簿。
## 核心概念
### 1. TokenId 与 Outcome 的关系
在 Polymarket 中,每个 outcome 都有唯一的 `tokenId`
- **二元市场**
- YES (outcomeIndex = 0) → tokenId_0
- NO (outcomeIndex = 1) → tokenId_1
- **三元市场**
- Outcome A (outcomeIndex = 0) → tokenId_0
- Outcome B (outcomeIndex = 1) → tokenId_1
- Outcome C (outcomeIndex = 2) → tokenId_2
- **多元市场**N 个结果):
- Outcome 0 → tokenId_0
- Outcome 1 → tokenId_1
- ...
- Outcome N-1 → tokenId_N-1
### 2. TokenId 的计算方式
`tokenId` 通过以下步骤计算:
```kotlin
// 1. 计算 indexSetindexSet = 2^outcomeIndex
val indexSet = BigInteger.TWO.pow(outcomeIndex)
// 2. 调用链上合约 getCollectionId(EMPTY_SET, conditionId, indexSet)
val collectionId = getCollectionId(EMPTY_SET, conditionId, indexSet)
// 3. 调用链上合约 getPositionId(collateralToken, collectionId)
val tokenId = getPositionId(collateralToken, collectionId)
```
**示例**
- outcomeIndex = 0 → indexSet = 1 (2^0)
- outcomeIndex = 1 → indexSet = 2 (2^1)
- outcomeIndex = 2 → indexSet = 4 (2^2)
- outcomeIndex = 3 → indexSet = 8 (2^3)
## 订单簿结构
### API 接口
Polymarket CLOB API 提供 `/book` 接口获取订单簿:
```kotlin
@GET("/book")
suspend fun getOrderbook(
@Query("token_id") tokenId: String? = null,
@Query("market") market: String? = null
): Response<OrderbookResponse>
```
### 订单簿响应结构
```kotlin
data class OrderbookResponse(
val bids: List<OrderbookEntry>, // 买入订单列表(按价格从高到低排序)
val asks: List<OrderbookEntry> // 卖出订单列表(按价格从低到高排序)
)
data class OrderbookEntry(
val price: String, // 价格(0.01 - 0.99
val size: String // 数量(shares
)
```
### 订单簿排序规则
1. **Bids(买入订单)**
- 按价格从高到低排序
- 第一个元素是 `bestBid`(最高买入价)
2. **Asks(卖出订单)**
- 按价格从低到高排序
- 第一个元素是 `bestAsk`(最低卖出价)
## 三元市场订单簿实现
### 1. 获取特定 Outcome 的订单簿
对于三元市场,需要为每个 outcome 单独获取订单簿:
```kotlin
// 示例:三元市场 "谁会赢得选举?"
// - Outcome 0: "候选人A"
// - Outcome 1: "候选人B"
// - Outcome 2: "候选人C"
// 获取 Outcome 0 的订单簿
val tokenId0 = blockchainService.getTokenId(conditionId, 0)
val orderbook0 = clobService.getOrderbookByTokenId(tokenId0)
// orderbook0.bids[0].price 是 Outcome 0 的 bestBid
// orderbook0.asks[0].price 是 Outcome 0 的 bestAsk
// 获取 Outcome 1 的订单簿
val tokenId1 = blockchainService.getTokenId(conditionId, 1)
val orderbook1 = clobService.getOrderbookByTokenId(tokenId1)
// 获取 Outcome 2 的订单簿
val tokenId2 = blockchainService.getTokenId(conditionId, 2)
val orderbook2 = clobService.getOrderbookByTokenId(tokenId2)
```
### 2. 市价单价格获取
`AccountService.getOptimalPriceFromOrderbook` 方法中:
```kotlin
private suspend fun getOptimalPriceFromOrderbook(tokenId: String, isSellOrder: Boolean): String {
// 通过 tokenId 获取特定 outcome 的订单簿
val orderbookResult = clobService.getOrderbookByTokenId(tokenId)
if (orderbookResult.isSuccess) {
val orderbook = orderbookResult.getOrNull()
if (orderbook != null) {
if (isSellOrder) {
// 市价卖单:需要 bestBid(最高买入价)
val bestBid = orderbook.bids.firstOrNull()?.price
// 返回 bestBid 或后备价格
} else {
// 市价买单:需要 bestAsk(最低卖出价)
val bestAsk = orderbook.asks.firstOrNull()?.price
// 返回 bestAsk 或后备价格
}
}
}
// 如果获取失败,返回后备价格
return fallbackPrice
}
```
### 3. 完整流程示例
```kotlin
// 1. 用户请求卖出 Outcome 2 的仓位
val request = PositionSellRequest(
accountId = 1,
marketId = "0x123...", // conditionId
side = "候选人C",
outcomeIndex = 2, // 关键:指定 outcome 索引
orderType = "MARKET",
quantity = "100"
)
// 2. 计算 tokenId
val tokenId = blockchainService.getTokenId(request.marketId, request.outcomeIndex)
// tokenId = "87660119269436753918591605029528224889066452434179554814663664703244066132110"
// 3. 获取订单簿并提取最优价
val optimalPrice = getOptimalPriceFromOrderbook(tokenId, isSellOrder = true)
// 从 orderbook.bids[0].price 获取 bestBid
// 4. 创建并提交订单
val signedOrder = orderSigningService.createAndSignOrder(
tokenId = tokenId,
side = "SELL",
price = optimalPrice,
size = request.quantity
)
```
## 与二元市场的区别
### 二元市场(YES/NO
- 只有 2 个 outcomeoutcomeIndex = 0, 1
- 可以通过 `market` 参数获取整个市场的订单簿
- Gamma API 提供 `bestBid``bestAsk`(但可能只针对主要 outcome
### 三元及以上市场
- 有 3 个或更多 outcomeoutcomeIndex = 0, 1, 2, ...
- **必须**通过 `tokenId` 参数获取特定 outcome 的订单簿
- 每个 outcome 都有独立的订单簿
- 需要明确指定 `outcomeIndex` 来计算 `tokenId`
## 注意事项
1. **必须提供 outcomeIndex**
- 三元及以上市场无法通过 `side` 字符串推断 `outcomeIndex`
- 必须明确提供 `outcomeIndex` 参数
2. **每个 Outcome 独立订单簿**
- 不同 outcome 的订单簿是独立的
- 不能通过 `market` 参数获取所有 outcome 的订单簿
3. **价格范围**
- 所有 outcome 的价格都在 0.01 - 0.99 范围内
- 所有 outcome 的价格之和应该接近 1.0(考虑套利机会)
4. **后备价格机制**
- 如果无法获取订单簿,使用后备价格:
- 市价卖单:0.06
- 市价买单:1.0
## 代码位置
- **TokenId 计算**`BlockchainService.getTokenId()`
- **订单簿获取**`PolymarketClobService.getOrderbookByTokenId()`
- **最优价获取**`AccountService.getOptimalPriceFromOrderbook()`
- **订单创建**`AccountService.sellPosition()`
File diff suppressed because it is too large Load Diff