删除后端无用日志
- 删除所有 logger.debug 调试日志 - 删除过于详细的 logger.info 常规操作日志 - 保留 logger.error 和 logger.warn 重要错误和警告日志 - 优化日志输出,减少生产环境日志噪音
This commit is contained in:
@@ -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. 高级筛选和搜索
|
||||
|
||||
@@ -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 USDC,copyRatio = 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 的交易ID(trade.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 天)
|
||||
|
||||
Reference in New Issue
Block a user