Files
PolyHermes/docs/position-sell-order-design.md
T
WrBug 5b458b9b0c feat: 实现订单推送服务和 WebSocket 重连机制
- 实现订单推送服务,支持多账户订单实时推送
- 添加 WebSocket 自动重连机制,支持指数退避策略
- 修复订单详情接口 L2 认证问题,通过 PolymarketClobService 获取
- 配置 Gson lenient 模式,支持解析格式不严格的 JSON
- 添加响应日志拦截器,便于调试 API 响应问题
- 使用 Gamma API 获取市场信息,支持通过 condition_ids 查询
- 修复字段映射问题,使用 @SerializedName 替代 @JsonProperty
- 订单推送消息包含订单详情和市场信息
2025-11-27 14:51:46 +08:00

11 KiB
Raw Blame History

仓位出售订单功能设计文档

1. 功能概述

为仓位管理页面添加出售功能,支持用户对当前仓位进行市价或限价卖出操作。

2. 后端接口设计

2.1 创建卖出订单接口

接口路径: POST /api/copy-trading/positions/sell

请求体:

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      // 限价价格(限价订单必需,市价订单不需要)
)

响应体:

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

请求体:

data class MarketPriceRequest(
    val marketId: String  // 市场ID
)

响应体:

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类型

/**
 * 仓位卖出请求
 */
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服务

// 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方法

@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 平仓收益实时计算

计算逻辑:

// 获取仓位信息
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. 网络错误处理