docs: 新增体育尾盘策略文档

- 新增 docs/sports-tail-strategy/ 目录
- 包含 README.md 目录说明
- 包含 zh/ 下的中文文档:
  - sports-tail-strategy-tasks.md (任务与验收)
  - sports-tail-strategy-ui-spec.md (UI 规格)
  - sports-tail-strategy-flow.md (流程说明)
  - sports-tail-strategy-market-data.md (市场数据与订阅)
  - sports-tail-strategy-api.md (后端 API 定义)

Made-with: Cursor
This commit is contained in:
WrBug
2026-03-07 03:16:36 +08:00
parent 46c32df421
commit e20d47ba4c
6 changed files with 1274 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
# 体育尾盘策略文档 (Sports Tail Strategy)
本目录集中存放与 Polymarket 体育市场尾盘策略相关的文档。
## 目录结构
```
sports-tail-strategy/
├── README.md # 本说明
└── zh/ # 中文文档
├── sports-tail-strategy-tasks.md # 任务与验收
├── sports-tail-strategy-ui-spec.md # UI 规格
├── sports-tail-strategy-flow.md # 流程说明
└── sports-tail-strategy-market-data.md # 市场数据与订阅
```
## 文档说明
| 文档 | 说明 |
|------|------|
| **tasks** (zh) | 开发任务与验收项 |
| **ui-spec** (zh) | 前端列表、表单、触发记录等 UI 规格 |
| **flow** (zh) | 策略整体流程(创建→触发→止盈止损→完成) |
| **market-data** (zh) | Gamma API 数据获取、WebSocket 订阅、价格监控 |
## 功能概述
体育尾盘策略用于在体育市场接近尾盘(胜率 90%+)时自动买入,利用高胜率市场低风险获利。
### 核心特性
1. **不区分方向**:只设置触发价格,系统自动监控两个方向,任意方向达到触发价即买入
2. **实时订阅**:通过 WebSocket 订阅订单簿,实时监控价格变化
3. **止盈止损**:支持设置止盈/止损价格,自动卖出
4. **订阅管理**:同一市场多策略共享订阅,无策略时自动取消订阅
### 适用场景
- 体育比赛接近尾声,一方胜率 90%+ 时买入
- 大小分市场接近尾盘时套利
- 低风险稳定收益场景
@@ -0,0 +1,356 @@
# 体育尾盘策略 - API 设计
## 一、后端 API
### 1.1 策略管理
#### 列表
```
POST /api/sports-tail-strategy/list
```
**请求**
```typescript
interface StrategyListRequest {
accountId?: number; // 筛选账户
sport?: string; // 筛选类别
}
```
**响应**
```typescript
interface StrategyListResponse {
list: StrategyDto[];
}
interface StrategyDto {
id: number;
accountId: number;
accountName: string;
conditionId: string;
marketTitle: string;
eventSlug: string;
triggerPrice: string;
amountMode: "FIXED" | "RATIO";
amountValue: string;
takeProfitPrice: string | null;
stopLossPrice: string | null;
// 成交信息
filled: boolean;
filledPrice: string | null;
filledOutcomeIndex: number | null;
filledOutcomeName: string | null;
filledAmount: string | null;
filledShares: string | null;
filledAt: number | null;
// 卖出信息
sold: boolean;
sellPrice: string | null;
sellType: string | null;
sellAmount: string | null;
realizedPnl: string | null;
soldAt: number | null;
// 实时价格(未成交时返回)
realtimeYesPrice: string | null;
realtimeNoPrice: string | null;
createdAt: number;
updatedAt: number;
}
```
#### 创建
```
POST /api/sports-tail-strategy/create
```
**请求**
```typescript
interface StrategyCreateRequest {
accountId: number; // 账户ID
conditionId: string; // 市场ID
marketTitle: string; // 市场标题
eventSlug?: string; // 事件slug
triggerPrice: string; // 触发价格
amountMode: "FIXED" | "RATIO";
amountValue: string; // 金额值
takeProfitPrice?: string; // 止盈价格
stopLossPrice?: string; // 止损价格
}
```
**响应**
```typescript
interface StrategyCreateResponse {
id: number;
}
```
#### 删除
```
POST /api/sports-tail-strategy/delete
```
**请求**
```typescript
interface StrategyDeleteRequest {
id: number;
}
```
**响应**
```typescript
interface StrategyDeleteResponse {
success: boolean;
}
```
---
### 1.2 市场数据
#### 体育类别列表
```
POST /api/sports-tail-strategy/sports-list
```
**响应**
```typescript
interface SportsListResponse {
list: SportDto[];
}
interface SportDto {
sport: string; // 类别标识:nba, nfl, epl...
image: string; // 图标URL
tagId: number; // 主Tag ID
name: string; // 显示名称(多语言)
}
```
#### 市场搜索
```
POST /api/sports-tail-strategy/market-search
```
**请求**
```typescript
interface MarketSearchRequest {
sport?: string; // 体育类别
endDateMin?: string; // 最小结束时间 ISO 8601
endDateMax?: string; // 最大结束时间 ISO 8601
minLiquidity?: string; // 最小流动性
keyword?: string; // 搜索关键词
limit?: number; // 返回数量,默认50
}
```
**响应**
```typescript
interface MarketSearchResponse {
list: MarketDto[];
}
interface MarketDto {
conditionId: string;
question: string;
outcomes: string[]; // ["Yes", "No"] 或 ["Over", "Under"]
outcomePrices: string[]; // 当前价格
endDate: string; // 结束时间 ISO 8601
liquidity: string; // 流动性
bestBid: number | null;
bestAsk: number | null;
yesTokenId: string;
noTokenId: string;
}
```
#### 市场详情
```
POST /api/sports-tail-strategy/market-detail
```
**请求**
```typescript
interface MarketDetailRequest {
conditionId: string;
}
```
**响应**
```typescript
interface MarketDetailResponse {
conditionId: string;
question: string;
outcomes: string[];
outcomePrices: string[];
endDate: string;
liquidity: string;
bestBid: number | null;
bestAsk: number | null;
yesTokenId: string;
noTokenId: string;
eventSlug: string | null;
}
```
---
### 1.3 触发记录
#### 全局记录列表
```
POST /api/sports-tail-strategy/triggers
```
**请求**
```typescript
interface TriggerListRequest {
accountId?: number; // 筛选账户
status?: string; // 筛选状态: SUCCESS/FAIL
startTime?: number; // 开始时间戳
endTime?: number; // 结束时间戳
page?: number; // 页码,默认1
pageSize?: number; // 每页数量,默认20
}
```
**响应**
```typescript
interface TriggerListResponse {
total: number;
list: TriggerDto[];
}
interface TriggerDto {
id: number;
strategyId: number;
// 市场信息
marketTitle: string;
conditionId: string;
// 买入信息
buyPrice: string;
outcomeIndex: number;
outcomeName: string | null;
buyAmount: string;
buyShares: string | null;
buyStatus: "PENDING" | "SUCCESS" | "FAIL";
// 卖出信息
sellPrice: string | null;
sellType: string | null; // TAKE_PROFIT/STOP_LOSS/MANUAL
sellAmount: string | null;
sellStatus: string | null;
// 盈亏
realizedPnl: string | null;
// 时间
triggeredAt: number;
soldAt: number | null;
}
```
---
## 二、前端 API 封装
### 2.1 apiService 方法
```typescript
// 策略管理
sportsTailStrategyList(params: StrategyListRequest): Promise<StrategyListResponse>
sportsTailStrategyCreate(data: StrategyCreateRequest): Promise<StrategyCreateResponse>
sportsTailStrategyDelete(id: number): Promise<StrategyDeleteResponse>
// 市场数据
sportsTailStrategySportsList(): Promise<SportsListResponse>
sportsTailStrategyMarketSearch(params: MarketSearchRequest): Promise<MarketSearchResponse>
sportsTailStrategyMarketDetail(conditionId: string): Promise<MarketDetailResponse>
// 触发记录
sportsTailStrategyTriggers(params: TriggerListRequest): Promise<TriggerListResponse>
```
---
## 三、多语言 Key
### 3.1 页面标题
```
sportsTailStrategy.list.title=体育尾盘策略
sportsTailStrategy.list.addStrategy=新增策略
sportsTailStrategy.list.filter.account=账户
sportsTailStrategy.list.filter.sport=类别
sportsTailStrategy.list.filter.all=全部
```
### 3.2 表单字段
```
sportsTailStrategy.form.account=账户
sportsTailStrategy.form.market=市场
sportsTailStrategy.form.triggerPrice=触发价格
sportsTailStrategy.form.amount=金额
sportsTailStrategy.form.amountMode=金额模式
sportsTailStrategy.form.fixed=固定金额
sportsTailStrategy.form.ratio=余额比例
sportsTailStrategy.form.takeProfit=止盈价格
sportsTailStrategy.form.stopLoss=止损价格
sportsTailStrategy.form.autoSell=自动卖出
```
### 3.3 列表字段
```
sportsTailStrategy.list.triggerPrice=触发价
sportsTailStrategy.list.amount=金额
sportsTailStrategy.list.takeProfitStopLoss=止盈/止损
sportsTailStrategy.list.filledPrice=成交价
sportsTailStrategy.list.shares=份
sportsTailStrategy.list.pnl=盈亏
sportsTailStrategy.list.realtimePrice=实时价格
sportsTailStrategy.list.pending=待结算
sportsTailStrategy.list.viewRecords=查看记录
sportsTailStrategy.list.delete=删除
```
### 3.4 市场筛选
```
sportsTailStrategy.market.filter.sport=类别
sportsTailStrategy.market.filter.allSports=全部类别
sportsTailStrategy.market.filter.endTime=结束时间
sportsTailStrategy.market.filter.today=今天
sportsTailStrategy.market.filter.next24h=未来24小时
sportsTailStrategy.market.filter.next7days=未来7天
sportsTailStrategy.market.filter.minLiquidity=最小流动性
sportsTailStrategy.market.filter.keyword=关键词
sportsTailStrategy.market.filter.search=搜索
sportsTailStrategy.market.select=选择市场
```
### 3.5 触发记录
```
sportsTailStrategy.records.title=触发记录
sportsTailStrategy.records.market=市场
sportsTailStrategy.records.direction=方向
sportsTailStrategy.records.buyPrice=买入价
sportsTailStrategy.records.buyAmount=买入金额
sportsTailStrategy.records.sellPrice=卖出价
sportsTailStrategy.records.sellType=卖出类型
sportsTailStrategy.records.pnl=盈亏
sportsTailStrategy.records.time=时间
sportsTailStrategy.records.status=状态
```
### 3.6 消息提示
```
sportsTailStrategy.message.createSuccess=策略创建成功
sportsTailStrategy.message.deleteSuccess=策略删除成功
sportsTailStrategy.message.deleteConfirm=确定删除该策略吗?
sportsTailStrategy.message.noMarketSelected=请选择市场
sportsTailStrategy.message.invalidPrice=价格格式无效
```
@@ -0,0 +1,262 @@
# 体育尾盘策略 - 流程说明
## 一、策略状态流转
```
┌─────────────┐ 价格>=触发价 ┌─────────────┐
│ 待触发 │ ──────────────────▶ │ 已成交 │
│ (filled=F) │ │ (filled=T) │
└─────────────┘ └─────────────┘
│ │
│ ┌─────────────────┼─────────────────┐
│ │ │ │
▼ │ 有止盈止损 │ 无止盈止损 │
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────────────────────────────┐
│ 保持订阅 │ │ 检查同市场是否有其他未完成策略 │
│ 监控卖出 │ │ - 有: 保持订阅 │
└─────────────┘ │ - 无: 取消订阅 │
│ └─────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ 价格 >= 止盈价 │
│ 或 价格 <= 止损价 │
└──────────────────────────────────────────────┘
┌─────────────┐
│ 自动卖出 │
│ (sold=T) │
└─────────────┘
```
---
## 二、订阅生命周期
### 2.1 策略创建时
```
策略创建
┌─────────────────────────────────────────────────────────────┐
│ SubscriptionManager.subscribeStrategy(strategy) │
│ 1. 检查市场是否已有订阅(marketSubscriptions 计数) │
│ 2. 如果市场已有订阅:仅增加计数,不新建连接 │
│ 3. 如果市场无订阅:建立 WebSocket 连接,订阅订单簿频道 │
└─────────────────────────────────────────────────────────────┘
```
### 2.2 实时监控
```
WebSocket 订单簿推送
┌─────────────────────────────────────────────────────────────┐
│ SportsTailWebSocketHandler.onOrderbookUpdate() │
│ 1. 解析消息,获取当前价格 │
│ 2. 查找该 Token ID 对应的所有未完成策略 │
│ 3. 遍历策略,检查触发条件: │
│ - 未成交:检查买入条件(价格 >= 触发价) │
│ - 已成交未卖出:检查卖出条件(止盈/止损) │
└─────────────────────────────────────────────────────────────┘
```
### 2.3 成交后处理
```
策略成交
┌─────────────────────────────────────────────────────────────┐
│ SubscriptionManager.onStrategyFilled(strategy) │
│ 1. 更新策略状态为已成交 │
│ 2. 检查是否需要保持订阅: │
│ - 有止盈止损:保持订阅,继续监控卖出条件 │
│ - 无止盈止损:检查同市场是否有其他未完成策略 │
│ - 有:保持订阅 │
│ - 无:取消订阅,关闭 WebSocket 连接 │
└─────────────────────────────────────────────────────────────┘
```
### 2.4 卖出后处理
```
策略卖出
┌─────────────────────────────────────────────────────────────┐
│ SubscriptionManager.onStrategySold(strategy) │
│ 1. 更新策略状态为已卖出 │
│ 2. 计算并记录盈亏 │
│ 3. 检查同市场是否有其他未完成策略: │
│ - 有:保持订阅 │
│ - 无:取消订阅,关闭 WebSocket 连接 │
└─────────────────────────────────────────────────────────────┘
```
### 2.5 策略删除时
```
策略删除
┌─────────────────────────────────────────────────────────────┐
│ SubscriptionManager.unsubscribeStrategy(strategy) │
│ 1. 减少市场的订阅计数 │
│ 2. 如果计数归零:取消订阅,关闭 WebSocket 连接 │
└─────────────────────────────────────────────────────────────┘
```
---
## 三、买入逻辑(不区分方向)
### 3.1 触发条件检查
```kotlin
fun checkBuyTrigger(strategy: SportsTailStrategy, yesPrice: BigDecimal, noPrice: BigDecimal): BuyTrigger? {
// 如果已成交,不检查
if (strategy.filled) return null
// 检查 YES 方向
if (yesPrice >= strategy.triggerPrice) {
return BuyTrigger(outcomeIndex = 0, price = yesPrice)
}
// 检查 NO 方向
if (noPrice >= strategy.triggerPrice) {
return BuyTrigger(outcomeIndex = 1, price = noPrice)
}
// 都不满足
return null
}
```
### 3.2 执行买入
```kotlin
suspend fun executeBuy(strategy: SportsTailStrategy, trigger: BuyTrigger) {
// 1. 创建市价单
val order = createMarketOrder(
tokenId = getTokenId(strategy.conditionId, trigger.outcomeIndex),
side = "BUY",
amount = calculateAmount(strategy)
)
// 2. 提交订单
val result = clobApi.createOrder(order)
// 3. 更新策略状态
strategy.filled = true
strategy.filledPrice = trigger.price
strategy.filledOutcomeIndex = trigger.outcomeIndex
strategy.filledAmount = order.amount
strategy.filledShares = calculateShares(order.amount, trigger.price)
strategy.filledAt = System.currentTimeMillis()
strategyRepository.save(strategy)
// 4. 记录触发记录
createTriggerRecord(strategy, trigger, result)
// 5. 通知订阅管理器
subscriptionManager.onStrategyFilled(strategy)
}
```
---
## 四、卖出逻辑(止盈止损)
### 4.1 止盈止损检查
```kotlin
fun checkSellTrigger(strategy: SportsTailStrategy, currentPrice: BigDecimal): SellTrigger? {
// 如果已卖出或未成交,不检查
if (strategy.sold || !strategy.filled) return null
// 只检查已买入方向的价格
val filledTokenId = getTokenId(strategy.conditionId, strategy.filledOutcomeIndex)
if (currentTokenId != filledTokenId) return null
// 检查止盈
if (strategy.takeProfitPrice != null && currentPrice >= strategy.takeProfitPrice) {
return SellTrigger(type = "TAKE_PROFIT", price = currentPrice)
}
// 检查止损
if (strategy.stopLossPrice != null && currentPrice <= strategy.stopLossPrice) {
return SellTrigger(type = "STOP_LOSS", price = currentPrice)
}
return null
}
```
### 4.2 执行卖出
```kotlin
suspend fun executeSell(strategy: SportsTailStrategy, trigger: SellTrigger) {
// 1. 创建市价单
val order = createMarketOrder(
tokenId = getTokenId(strategy.conditionId, strategy.filledOutcomeIndex),
side = "SELL",
shares = strategy.filledShares
)
// 2. 提交订单
val result = clobApi.createOrder(order)
// 3. 计算盈亏
val sellAmount = calculateSellAmount(trigger.price, strategy.filledShares)
val pnl = sellAmount - strategy.filledAmount
// 4. 更新策略状态
strategy.sold = true
strategy.sellPrice = trigger.price
strategy.sellType = trigger.type
strategy.sellAmount = sellAmount
strategy.realizedPnl = pnl
strategy.soldAt = System.currentTimeMillis()
strategyRepository.save(strategy)
// 5. 更新触发记录
updateTriggerRecord(strategy, trigger, result, pnl)
// 6. 通知订阅管理器
subscriptionManager.onStrategySold(strategy)
}
```
---
## 五、关键设计要点
### 5.1 订阅共享
- 同一市场(conditionId)多个策略共享一个 WebSocket 订阅
- 使用计数器管理订阅生命周期
- 避免重复连接和资源浪费
### 5.2 不区分方向
- 只设置触发价格,不选择 YES/NO
- 系统自动监控两个方向的价格
- 任意方向满足条件即买入该方向
### 5.3 实时订阅
- 使用 WebSocket 订阅订单簿,实时接收价格变化
- 不使用轮询方式
- 响应速度快,延迟低
### 5.4 智能取消
- 无策略时取消订阅
- 策略完成且无止盈止损时检查是否需要取消
- 有止盈止损时保持订阅直到卖出
@@ -0,0 +1,215 @@
# 体育尾盘策略 - 市场数据与订阅
> 本文档描述体育市场数据获取方式、 WebSocket 订阅管理逻辑。
## 一、Gamma API 数据获取
> 本文档描述如何从 Gamma API 获取体育市场数据。### 1.1 萜索条件
### 1.1 获取体育类别列表
> **前端选择体育类别时,需要获取可选的体育类别列表供用户选择。
```
GET https://gamma-api.polymarket.com/sports
```
> **返回示例**
```json
[
{"sport": "nba", "image": "https://...", "tags": "1,745,100639"},
{"sport": "nfl", "image": "https://...", "tags": "1,450,100639"},
{"sport": "epl", "image": "https://...", "tags": "1,82,306,100639"},
...
]
```
**主要类别**
| sport | 名称 | tag_id |
|-------|------|--------|
| nba | 篮球 NBA | 745 |
| nfl | 美式足球 NFL | 450 |
| epl | 英超 | 82 |
| lal | 西甲 | 780 |
| mlb | 棒球 MLB | 100381 |
| nhl | 冰球 NHL | 899 |
| ufc | 格斗 UFC | 100639 |
### 1.2 按类别筛选市场
> 根据用户选择的体育类别,使用 `tag_id` 参数筛选市场。
```
GET https://gamma-api.polymarket.com/markets?tag_id=745&active=true&closed=false&limit=50&order=endDate&ascending=true
```
> **返回字段**
- `id` - 市场ID
- `question` - 市场问题
- `conditionId` - 市场 conditionId
- `outcomes` - 结果选项 `["Yes", "No"]``["Over", "Under"]`
- `outcomePrices` - 当前价格 `["0.92", "0.08"]`
- `endDate` - 结束时间
- `bestBid` - 最佳买价
- `bestAsk` - 最佳卖价
- `clobTokenIds` - Token IDs
- `gameStartTime` - 比赛开始时间
- `sportsMarketType` - 市场类型
- `liquidityNum` - 流动性
- `volumeNum` - 成交量
- `events` - 关联事件信息
```
> **请求参数**
```kotlin
data class SportsMarketSearchRequest(
val sport: String? = null, // 体育类别: nba, nfl, epl...
val marketType: String? = null, // 市场类型: moneyline, spreads, totals
val endDateMin: String? = null, // 最小结束时间
val endDateMax: String? = null, // 最大结束时间
val minLiquidity: BigDecimal? = null, // 最小流动性
val keyword: String? = null, // 搜索关键词
val limit: Int = 50 // 返回数量
)
```
> **marketType 说明**
- `moneyline` - 胜负市场(谁会赢)
- `spreads` - 让分市场
- `totals` - 大小分市场
### 1.3 获取单个市场详情
```
GET https://gamma-api.polymarket.com/markets?condition_ids={conditionId}
```
> **用于**
- 创建策略时获取 `clobTokenIds`
- 监控价格时获取实时价格
- 获取 Token ID 用于下单
---
## 二、WebSocket 订阅管理
### 2.1 订阅策略
> 同一市场可能有多个策略,但只维护一个 WebSocket 订阅。
> 策略创建/删除时需要更新订阅计数。
```kotlin
// 市场订阅计数
private val marketSubscriptions = ConcurrentHashMap<String, Int>()
// 市场对应的 Token IDs 缓存
private val marketTokenIds = ConcurrentHashMap<String, Pair<String, String>>()
/**
* 订阅策略(创建策略时调用)
*/
fun subscribeStrategy(strategy: SportsTailStrategy) {
val conditionId = strategy.conditionId
marketSubscriptions.compute(conditionId) { _, count ->
val newCount = (count ?: 0) + 1
if (newCount == 1) {
// 首次订阅,建立 WebSocket 连接
subscribeMarket(conditionId)
}
newCount
}
}
```
> **订阅管理规则**
| 场景 | 操作 |
|------|------|
| 策略创建 | 如果市场无订阅,建立订阅;否则计数 +1 |
| 策略删除 | 计数 -1;如果计数为 0,取消订阅 |
| 策略成交(无止盈止损) | 检查同市场是否有其他未完成策略;无则取消订阅 |
| 策略卖出 | 检查同市场是否有其他未完成策略;无则取消订阅 |
### 2.2 讣阅流程
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 策略创建 │────▶│ 检查订阅计数 │────▶│ 首次? 建立连接 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ WebSocket 接收订单簿更新 │
│ 1. 解析消息,获取 tokenId 和价格 │
│ 2. 查找该 Token 对应的未完成策略 │
│ 3. 检查触发条件 │
│ 4. 满足条件则执行买入/卖出 │
└─────────────────────────────────────────────────────────────┘
```
> **WebSocket 消息格式**
```json
{
"event_type": "book",
"asset_id": "1234567890",
"market": {
"bids": [...],
"asks": [...],
"timestamp": 1234567890
}
}
```
---
## 三、价格监控与触发
### 3.1 买入触发逻辑
> 任意方向价格达到触发价即买入
```kotlin
fun checkBuyTrigger(
strategy: SportsTailStrategy,
yesPrice: BigDecimal,
noPrice: BigDecimal
): TriggerResult? {
// 检查 YES 方向
if (yesPrice >= strategy.triggerPrice) {
return TriggerResult(outcomeIndex = 0, price = yesPrice)
}
// 检查 NO 方向
if (noPrice >= strategy.triggerPrice) {
return TriggerResult(outcomeIndex = 1, price = noPrice)
}
return null
}
```
> **注意**:不区分方向,系统自动选择价格满足的方向买入。
### 3.2 止盈止损逻辑
> 已成交的策略,监控持仓方向的价格变化
```kotlin
fun checkSellTrigger(
strategy: SportsTailStrategy,
currentPrice: BigDecimal
): SellTrigger? {
if (!strategy.filled || strategy.sold) {
return null
}
// 止盈:当前价格 >= 止盈价
if (strategy.takeProfitPrice != null && currentPrice >= strategy.takeProfitPrice) {
return SellTrigger(type = "TAKE_PROFIT", price = currentPrice)
}
// 止损:当前价格 <= 止损价
if (strategy.stopLossPrice != null && currentPrice <= strategy.stopLossPrice) {
return SellTrigger(type = "STOP_LOSS", price = currentPrice)
}
return null
}
```
> **注意**:只监控已买入方向的价格,不是两个方向都监控。
### 3.3 订阅生命周期
```
┌─────────────┐ 价格>=触发价 ┌─────────────┐ 止盈/止损 ┌─────────────┐
│ 监控两个方向 │ ────────────────▶ │ 已成交 │ ────────────────▶ │ 已完成 │
└─────────────┘ └─────────────┘ └─────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────────────┐
│ 成交后检查止盈止损 │
│ - 有止盈止损:保持订阅,只监控买入方向 │
│ - 无止盈止损:检查同市场是否有其他未完成策略,无则取消订阅 │
└─────────────────────────────────────────────────────────────────────────┘
```
@@ -0,0 +1,141 @@
# 体育尾盘策略 - 任务梳理
> 需求与 UI 见 `sports-tail-strategy-ui-spec.md`,市场数据与订阅见 `sports-tail-strategy-market-data.md`。
以下按**数据库 / 后端 / 前端**拆分为可执行任务,便于排期与验收。
---
## 一、数据库
| 序号 | 任务 | 说明 |
|------|------|------|
| D1 | 策略表 migration | 新建表 `sports_tail_strategy`,字段:id, account_id, condition_id, market_title, event_slug, trigger_price, amount_mode(FIXED/RATIO), amount_value, take_profit_price, stop_loss_price, filled(BOOL), filled_price, filled_outcome_index, filled_amount, filled_shares, filled_at, sold(BOOL), sell_price, sell_type, sell_amount, realized_pnl, sold_at, created_at, updated_at |
| D2 | 触发记录表 migration | 新建表 `sports_tail_strategy_trigger`,字段:id, strategy_id, market_title, condition_id, account_id, buy_order_id, buy_price, outcome_index, outcome_name, buy_amount, buy_shares, buy_status(PENDING/SUCCESS/FAIL), sell_order_id, sell_price, sell_type(TAKE_PROFIT/STOP_LOSS/MANUAL), sell_amount, sell_status, realized_pnl, triggered_at, sold_at |
---
## 二、后端(Kotlin
### 2.1 实体与 Repository
| 序号 | 任务 | 说明 |
|------|------|------|
| B1 | 策略实体 Entity | 对应 `sports_tail_strategy` 表;ID 用 `Long?`;时间用 `Long` 时间戳;金额用 `BigDecimal`;遵守 backend.mdc 实体规范 |
| B2 | 触发记录实体 Entity | 对应 `sports_tail_strategy_trigger` 表 |
| B3 | JpaRepository | 策略、触发记录的 Repository;按 conditionId、accountId、filled、sold 等查询 |
### 2.2 Gamma API 扩展
| 序号 | 任务 | 说明 |
|------|------|------|
| B4 | 体育类别 API | `GET /sports` 获取体育元数据(sport, tags, image |
| B5 | 市场搜索 API | `GET /markets` 支持 tag_id、sports_market_types、end_date_min/max、liquidity_num_min 等筛选参数 |
| B6 | 事件列表 API | `GET /events` 支持 tag_slug、active、live 等参数,获取比赛状态 |
### 2.3 订阅管理
| 序号 | 任务 | 说明 |
|------|------|------|
| B7 | 订阅管理器 SubscriptionManager | 维护市场订阅计数,同一市场多策略共享订阅;无策略时自动取消订阅 |
| B8 | WebSocket 订单簿订阅 | 订阅订单簿 `channel: "book:<token_id>"`,接收实时价格更新 |
| B9 | 订阅生命周期管理 | 策略创建时订阅,成交后检查是否需要保持(有止盈止损则保持),卖出后检查是否需要取消 |
### 2.4 策略执行核心逻辑
| 序号 | 任务 | 说明 |
|------|------|------|
| B10 | 买入触发判断 | 接收订单簿更新,检查未成交策略;当任意方向价格 >= triggerPrice 时买入该方向 |
| B11 | 买入执行 | 调用 CLOB API 创建市价买单;记录成交价格、数量、方向;更新策略状态为已成交 |
| B12 | 止盈判断 | 已成交策略,当前价格 >= takeProfitPrice 时执行卖出 |
| B13 | 止损判断 | 已成交策略,当前价格 <= stopLossPrice 时执行卖出 |
| B14 | 卖出执行 | 调用 CLOB API 创建市价卖单;计算盈亏;更新策略状态为已卖出 |
### 2.5 API 与 DTO
| 序号 | 任务 | 说明 |
|------|------|------|
| B15 | 策略 CRUD API | 列表(分页/筛选)、创建、删除;统一 ApiResponse;错误码与 MessageSource |
| B16 | 策略 DTO | 创建请求:accountId, conditionId, marketTitle, eventSlug, triggerPrice, amountMode, amountValue, takeProfitPrice(可选), stopLossPrice(可选) |
| B17 | 触发记录 API | 全局触发记录列表;支持 accountId、status、时间筛选;返回市场信息、成交价、数量、盈亏等 |
| B18 | 市场搜索 API | 体育类别列表、市场搜索(支持筛选)、市场详情(含实时价格) |
---
## 三、前端(React + TypeScript
### 3.1 路由与导航
| 序号 | 任务 | 说明 |
|------|------|------|
| F1 | 路由 | App.tsx 增加 `/sports-tail-strategy` |
| F2 | 菜单 | Layout 中增加「体育尾盘策略」菜单项 |
### 3.2 列表页
| 序号 | 任务 | 说明 |
|------|------|------|
| F3 | 列表页组件 | SportsTailStrategyList.tsx;页面标题、新增按钮、筛选(账户、类别) |
| F4 | 列表展示 | 桌面 Table / 移动 Card:市场标题、账户、触发价、金额、止盈止损、成交价/数量(未成交显示实时价格)、操作(查看记录、删除) |
| F5 | 实时价格显示 | 未成交策略通过 WebSocket 获取实时价格并显示 |
### 3.3 新增/编辑表单
| 序号 | 任务 | 说明 |
|------|------|------|
| F6 | 表单弹窗 | 账户选择、市场搜索(支持筛选)、触发价格、下注金额、止盈止损(可选) |
| F7 | 市场筛选器 | 体育类别、市场类型、结束时间、最小流动性、搜索关键词 |
| F8 | 市场选择器 | 显示搜索结果列表,包含市场标题、当前价格、结束时间、流动性 |
| F9 | 预估收益 | 根据触发价、金额计算预估份额和收益 |
| F10 | 表单校验 | 触发价 0-1,止盈 > 触发价,止损 < 触发价 |
### 3.4 触发记录
| 序号 | 任务 | 说明 |
|------|------|------|
| F11 | 触发记录列表 | 全局记录列表(非单个市场);支持账户、状态、时间筛选 |
| F12 | 记录详情 | 市场标题、成交价格/方向、数量、卖出价格/类型、盈亏 |
### 3.5 通用
| 序号 | 任务 | 说明 |
|------|------|------|
| F13 | 类型定义 | 策略、触发记录、市场、体育类别等 TypeScript 类型 |
| F14 | API 封装 | apiService 中 sportsTailStrategy.* 方法 |
| F15 | 多语言 | zh-CN、zh-TW、en 的 sportsTailStrategy.* 文案 |
---
## 四、依赖关系简图
```
D1,D2 数据库
B1-B3 实体与 Repository
B4-B6 Gamma API 扩展
B7-B9 订阅管理
B10-B14 执行逻辑
B15-B18 API 与 DTO
F1-F2 路由与菜单
F13-F15 类型与 API 封装
F3-F5 列表与实时价格
F6-F10 表单(含市场筛选)
F11-F12 触发记录
```
---
## 五、验收要点
- **不区分方向**:只设置触发价格,系统自动监控两个方向,任意方向达到即买入
- **实时订阅**:通过 WebSocket 订阅订单簿,实时监控价格,不使用轮询
- **订阅管理**:同一市场多策略共享一个订阅;无策略或策略完成且无止盈止损时取消订阅
- **止盈止损**:有止盈止损的策略成交后保持订阅,直到卖出
- **列表显示**:已成交显示成交价和数量,未成交显示实时价格
- **触发记录**:全局记录列表,每条记录包含完整市场信息
@@ -0,0 +1,259 @@
# 体育尾盘策略 - UI 规格
## 一、策略列表页
### 1.1 页面布局
**路由**`/sports-tail-strategy`
**桌面端**
```
┌─────────────────────────────────────────────────────────────────────┐
│ 体育尾盘策略 [+ 新增策略] │
├─────────────────────────────────────────────────────────────────────┤
│ 筛选: [账户选择▼] [类别: 全部▼] │
├─────────────────────────────────────────────────────────────────────┤
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Lakers vs Bulls - Who will win? │ │
│ │ 账户: Account 1 │ │
│ │ 触发价: >=0.90 | 金额: 10 USDC │ │
│ │ 止盈: 0.98 | 止损: 0.85 │ │
│ │ ─────────────────────────────────────────────────────────── │ │
│ │ 成交价: 0.91 YES | 数量: 10.99 份 | 盈亏: 待结算 │ │
│ │ [查看记录] [删除] │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
**移动端**:使用卡片布局,信息折叠展示
### 1.2 列表字段
| 字段 | 说明 |
|------|------|
| 市场标题 | `marketTitle`,可点击跳转 Polymarket |
| 账户 | 关联的账户名称 |
| 触发价 | `>= {triggerPrice}` |
| 金额 | 固定金额 USDC 或 余额比例 % |
| 止盈/止损 | 配置的止盈止损价格,未配置显示 `-` |
| 成交信息 | 已成交:`{filledPrice} {YES/NO} \| {filledShares} 份`<br>未成交:`实时价格: YES {price} \| NO {price}` |
| 盈亏 | 已卖出:`+{realizedPnl} USDC`<br>已成交未卖出:`待结算`<br>未成交:`-` |
### 1.3 操作按钮
| 按钮 | 说明 |
|------|------|
| 查看记录 | 打开该策略的触发记录详情 |
| 删除 | 删除策略(需二次确认) |
**注意**:不支持启用/禁用功能,无状态列
---
## 二、新增策略表单
### 2.1 表单字段
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| 账户选择 | 下拉 | 是 | 选择账户 |
| 选择市场 | 搜索选择 | 是 | 从市场列表中选择(支持筛选) |
| 触发条件 | 复合输入 | 是 | `当价格 [>=] [0.90] 时触发买入` |
| 下注金额 | 单选+输入 | 是 | 固定金额 USDC 或 余额比例 % |
| 启用自动卖出 | 开关 | 否 | 开启后显示止盈止损 |
| 止盈价格 | 输入 | 否 | 价格上涨到此值时自动卖出 |
| 止损价格 | 输入 | 否 | 价格下跌到此值时自动卖出 |
**注意**
- 不选择方向(YES/NO),只设置触发价格
- 系统自动监控两个方向,任意方向达到触发价即买入
- 默认只触发一次
### 2.2 市场选择器
**筛选条件**
```
┌─────────────────────────────────────────────────────────────┐
│ 体育类别 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [全部 ▼] │ │
│ │ 选项: 全部 / NBA / NFL / 英超 / 西甲 / 棒球... │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 市场类型 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [全部 ▼] │ │
│ │ 选项: 全部 / 胜负 / 让分 / 大小分 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 结束时间 │
│ ○ 全部 ○ 今天 ○ 未来24小时 ○ 未来7天 │
│ │
│ 最小流动性 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [ ] USDC(留空表示不限制) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 搜索关键词 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [搜索球队、比赛...] [搜索] │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
**市场列表**
```
┌─────────────────────────────────────────────────────────────┐
│ ○ Lakers vs Bulls - Who will win? │
│ YES: 0.92 NO: 0.08 | 流动性: 50,000 USDC │
│ 结束: 2024-03-07 15:30 (剩余2小时30分) │
│ │
│ ○ Warriors @ Heat - O/U 220.5 │
│ Over: 0.55 Under: 0.45 | 流动性: 30,000 USDC │
│ 结束: 2024-03-07 18:00 (剩余5小时) │
└─────────────────────────────────────────────────────────────┘
```
### 2.3 预估收益展示
```
┌─────────────────────────────────────────────────────────────┐
│ 预估收益 │
│ ──────────────────────────────────────────────────────── │
│ 买入价格: 0.90 | 买入金额: 10 USDC │
│ 预计份额: 11.11 | 预计收益: +1.11 USDC (11.1%) │
│ │
│ 止盈 (0.98): 收益 +8.89 USDC (88.9%) │
│ 止损 (0.85): 亏损 -1.67 USDC (-16.7%) │
└─────────────────────────────────────────────────────────────┘
```
---
## 三、触发记录页(全局)
### 3.1 页面布局
**路由**`/sports-tail-strategy/records`
**说明**:全局记录列表,不是单个策略的记录
**桌面端表格**
```
┌────────────────────────────────────────────────────────────────────────────────────────┐
│ 触发记录 │
├────────────────────────────────────────────────────────────────────────────────────────┤
│ 筛选: [账户选择▼] [状态: 全部▼] [时间范围: 最近7天▼] │
├────────────────────────────────────────────────────────────────────────────────────────┤
│ 时间 │ 市场标题 │ 方向 │ 买入价 │ 金额 │ 卖出价 │ 盈亏 │
│ 03-07 15:30 │ Lakers vs Bulls │ YES │ 0.91 │ 10.00 │ 0.98 │ +7.89 USDC │
│ 03-07 14:20 │ Warriors @ Heat │ Over │ 0.55 │ 5.00 │ - │ 待结算 │
│ 03-06 18:45 │ Celtics vs Heat │ NO │ 0.92 │ 20.00 │ 0.85 │ -7.00 USDC │
└────────────────────────────────────────────────────────────────────────────────────────┘
```
**移动端**:使用卡片布局
### 3.2 记录字段
| 字段 | 说明 |
|------|------|
| 时间 | `triggeredAt` 格式化显示 |
| 市场标题 | `marketTitle`,可点击跳转 Polymarket |
| 方向 | `outcomeName` 或 YES/NO/Over/Under |
| 买入价 | `buyPrice` |
| 金额 | `buyAmount` USDC |
| 卖出价 | `sellPrice`,未卖出显示 `-` |
| 盈亏 | `realizedPnl`,未结算显示 `待结算` |
### 3.3 筛选条件
| 条件 | 说明 |
|------|------|
| 账户 | 按账户筛选 |
| 状态 | 全部 / 待结算 / 已止盈 / 已止损 / 已完成 |
| 时间范围 | 最近24小时 / 最近7天 / 最近30天 / 全部 |
---
## 四、响应式适配
### 4.1 断点
- 移动端: < 768px
- 桌面端: >= 768px
### 4.2 移动端适配
1. 列表使用卡片布局,信息可折叠
2. 表单使用分步或滚动布局
3. 触发记录使用卡片列表
4. 按钮最小触摸目标 44x44px
---
## 五、多语言 Key
```
sportsTailStrategy.list.title=体育尾盘策略
sportsTailStrategy.list.addStrategy=新增策略
sportsTailStrategy.list.filter.account=账户
sportsTailStrategy.list.filter.category=类别
sportsTailStrategy.list.filter.allCategory=全部
sportsTailStrategy.list.triggerPrice=触发价
sportsTailStrategy.list.amount=金额
sportsTailStrategy.list.takeProfitStopLoss=止盈/止损
sportsTailStrategy.list.filledPrice=成交价
sportsTailStrategy.list.realtimePrice=实时价格
sportsTailStrategy.list.shares=份
sportsTailStrategy.list.pnl=盈亏
sportsTailStrategy.list.pending=待结算
sportsTailStrategy.list.viewRecords=查看记录
sportsTailStrategy.list.delete=删除
sportsTailStrategy.list.deleteConfirm=确定删除该策略吗?
sportsTailStrategy.form.title=新增体育尾盘策略
sportsTailStrategy.form.account=账户
sportsTailStrategy.form.selectAccount=选择账户
sportsTailStrategy.form.selectMarket=选择市场
sportsTailStrategy.form.triggerCondition=触发条件
sportsTailStrategy.form.triggerPriceHelp=当任意方向价格达到触发价时买入
sportsTailStrategy.form.amount=下注金额
sportsTailStrategy.form.fixedAmount=固定金额
sportsTailStrategy.form.ratio=余额比例
sportsTailStrategy.form.autoSell=启用自动卖出
sportsTailStrategy.form.takeProfitPrice=止盈价格
sportsTailStrategy.form.takeProfitHelp=价格上涨到此值时自动卖出
sportsTailStrategy.form.stopLossPrice=止损价格
sportsTailStrategy.form.stopLossHelp=价格下跌到此值时自动卖出
sportsTailStrategy.form.estimatedReturn=预估收益
sportsTailStrategy.form.buyPrice=买入价格
sportsTailStrategy.form.buyAmount=买入金额
sportsTailStrategy.form.estimatedShares=预计份额
sportsTailStrategy.form.estimatedPnl=预计收益
sportsTailStrategy.marketSearch.sport=体育类别
sportsTailStrategy.marketSearch.marketType=市场类型
sportsTailStrategy.marketSearch.endTime=结束时间
sportsTailStrategy.marketSearch.minLiquidity=最小流动性
sportsTailStrategy.marketSearch.keyword=搜索关键词
sportsTailStrategy.marketSearch.search=搜索
sportsTailStrategy.marketSearch.liquidity=流动性
sportsTailStrategy.marketSearch.remaining=剩余
sportsTailStrategy.records.title=触发记录
sportsTailStrategy.records.time=时间
sportsTailStrategy.records.market=市场
sportsTailStrategy.records.direction=方向
sportsTailStrategy.records.buyPrice=买入价
sportsTailStrategy.records.amount=金额
sportsTailStrategy.records.sellPrice=卖出价
sportsTailStrategy.records.pnl=盈亏
sportsTailStrategy.records.pending=待结算
sportsTailStrategy.records.takeProfit=已止盈
sportsTailStrategy.records.stopLoss=已止损
```