diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/FailedTrade.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/FailedTrade.kt deleted file mode 100644 index 4accb7d..0000000 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/entity/FailedTrade.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.wrbug.polymarketbot.entity - -import jakarta.persistence.* - -/** - * 失败交易实体 - * 记录处理失败的交易信息 - */ -@Entity -@Table(name = "failed_trade") -data class FailedTrade( - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - val id: Long? = null, - - @Column(name = "leader_id", nullable = false) - val leaderId: Long, - - @Column(name = "leader_trade_id", nullable = false, length = 100) - val leaderTradeId: String, // Leader 的交易ID - - @Column(name = "trade_type", nullable = false, length = 10) - val tradeType: String, // BUY 或 SELL - - @Column(name = "copy_trading_id", nullable = false) - val copyTradingId: Long, - - @Column(name = "account_id", nullable = false) - val accountId: Long, - - @Column(name = "market_id", nullable = false, length = 100) - val marketId: String, - - @Column(name = "side", nullable = false, length = 10) - val side: String, // YES/NO - - @Column(name = "price", nullable = false, length = 50) - val price: String, // 价格(字符串格式) - - @Column(name = "size", nullable = false, length = 50) - val size: String, // 数量(字符串格式) - - @Column(name = "error_message", columnDefinition = "TEXT") - val errorMessage: String? = null, // 错误信息 - - @Column(name = "retry_count", nullable = false) - val retryCount: Int = 0, // 重试次数 - - @Column(name = "failed_at", nullable = false) - val failedAt: Long = System.currentTimeMillis(), - - @Column(name = "created_at", nullable = false) - val createdAt: Long = System.currentTimeMillis() -) - diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/repository/FailedTradeRepository.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/repository/FailedTradeRepository.kt deleted file mode 100644 index e99974b..0000000 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/repository/FailedTradeRepository.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.wrbug.polymarketbot.repository - -import com.wrbug.polymarketbot.entity.FailedTrade -import org.springframework.data.jpa.repository.JpaRepository -import org.springframework.stereotype.Repository - -/** - * 失败交易Repository - */ -@Repository -interface FailedTradeRepository : JpaRepository { - - /** - * 根据Leader ID和交易ID查询 - */ - fun findByLeaderIdAndLeaderTradeId(leaderId: Long, leaderTradeId: String): FailedTrade? - - /** - * 检查是否存在失败的交易 - */ - fun existsByLeaderIdAndLeaderTradeId(leaderId: Long, leaderTradeId: String): Boolean -} - diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyOrderTrackingService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyOrderTrackingService.kt index ba1f088..73a6b21 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyOrderTrackingService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/CopyOrderTrackingService.kt @@ -37,7 +37,6 @@ open class CopyOrderTrackingService( private val sellMatchRecordRepository: SellMatchRecordRepository, private val sellMatchDetailRepository: SellMatchDetailRepository, private val processedTradeRepository: ProcessedTradeRepository, - private val failedTradeRepository: FailedTradeRepository, private val filteredOrderRepository: FilteredOrderRepository, private val copyTradingRepository: CopyTradingRepository, private val accountRepository: AccountRepository, @@ -135,12 +134,6 @@ open class CopyOrderTrackingService( return@withLock Result.success(Unit) } - // 检查是否已记录为失败交易 - val failedTrade = failedTradeRepository.findByLeaderIdAndLeaderTradeId(leaderId, trade.id) - if (failedTrade != null) { - return@withLock Result.success(Unit) - } - // 2. 处理交易逻辑 val result = when (trade.side.uppercase()) { "BUY" -> processBuyTrade(leaderId, trade) @@ -491,27 +484,6 @@ open class CopyOrderTrackingService( if (createOrderResult.isFailure) { // 提取错误信息(只保留 code 和 errorBody) val exception = createOrderResult.exceptionOrNull() - val errorMsg = buildFullErrorMessage( - exception, - "BUY", - buyPrice.toString(), - finalBuyQuantity.toString(), - trade.id - ) - - // 记录失败交易到数据库 - // retryCount = MAX_RETRY_ATTEMPTS - 1,表示已重试的次数 - recordFailedTrade( - leaderId = leaderId, - trade = trade, - copyTradingId = copyTrading.id!!, - accountId = copyTrading.accountId, - side = "BUY", - price = buyPrice.toString(), - size = finalBuyQuantity.toString(), - errorMessage = errorMsg, - retryCount = MAX_RETRY_ATTEMPTS - 1 // 已重试次数 - ) // 发送订单失败通知(异步,不阻塞,仅在 pushFailedOrders 为 true 时发送) if (copyTrading.pushFailedOrders) { @@ -971,26 +943,9 @@ open class CopyOrderTrackingService( ) if (createOrderResult.isFailure) { - // 创建订单失败,记录到失败表 + // 创建订单失败,记录错误日志 val exception = createOrderResult.exceptionOrNull() - val errorMsg = buildFullErrorMessage( - exception, - "SELL", - sellPrice.toString(), - totalMatched.toString(), - leaderSellTrade.id - ) - recordFailedTrade( - leaderId = copyTrading.leaderId, - trade = leaderSellTrade, - copyTradingId = copyTrading.id!!, - accountId = copyTrading.accountId, - side = "SELL", // 订单方向是SELL - price = sellPrice.toString(), - size = totalMatched.toString(), - errorMessage = errorMsg, - retryCount = 1 // 已重试一次 - ) + logger.error("创建卖出订单失败: copyTradingId=${copyTrading.id}, tradeId=${leaderSellTrade.id}, error=${exception?.message}") return } @@ -1248,98 +1203,6 @@ open class CopyOrderTrackingService( return "code=$code, errorBody=$errorBody" } - /** - * 记录失败交易到数据库 - * 注意:此方法在 @Transactional 方法中被调用,会自动继承事务 - */ - private suspend fun recordFailedTrade( - leaderId: Long, - trade: TradeResponse, - copyTradingId: Long, - accountId: Long, - side: String, - price: String, - size: String, - errorMessage: String, - retryCount: Int - ) { - try { - // 确保错误信息不超过数据库字段限制(TEXT类型通常支持65535字符) - val maxErrorMessageLength = 50000 // 保留一些余量 - val finalErrorMessage = if (errorMessage.length > maxErrorMessageLength) { - errorMessage.substring(0, maxErrorMessageLength) + "... (截断)" - } else { - errorMessage - } - - val failedTrade = FailedTrade( - leaderId = leaderId, - leaderTradeId = trade.id, - tradeType = trade.side.uppercase(), - copyTradingId = copyTradingId, - accountId = accountId, - marketId = trade.market, - side = side, - price = price, - size = size, - errorMessage = finalErrorMessage, - retryCount = retryCount, - failedAt = System.currentTimeMillis() - ) - failedTradeRepository.save(failedTrade) - - // 记录日志,确认已保存到数据库 - logger.info("失败交易已保存到数据库: leaderId=$leaderId, tradeId=${trade.id}, errorMessageLength=${finalErrorMessage.length}") - - // 标记为已处理(失败状态),避免重复处理 - // 注意:并发情况下可能多个请求同时处理同一笔交易,需要处理唯一约束冲突 - try { - val processed = ProcessedTrade( - leaderId = leaderId, - leaderTradeId = trade.id, - tradeType = trade.side.uppercase(), - source = "polling", - status = "FAILED", - processedAt = System.currentTimeMillis() - ) - processedTradeRepository.save(processed) - } catch (e: Exception) { - // 检查是否是唯一键冲突异常 - if (isUniqueConstraintViolation(e)) { - // 唯一约束冲突,说明已经处理过了(可能是并发请求) - // 检查现有记录的状态 - val existing = processedTradeRepository.findByLeaderIdAndLeaderTradeId(leaderId, trade.id) - if (existing != null) { - if (existing.status == "SUCCESS") { - logger.warn("交易已成功处理,但尝试记录为失败(并发冲突): leaderId=$leaderId, tradeId=${trade.id}") - } else { - logger.debug("交易已标记为失败(并发检测): leaderId=$leaderId, tradeId=${trade.id}") - } - } else { - // 如果查询不到,等待一下再查询(可能是事务隔离级别问题) - delay(100) - val existingAfterDelay = - processedTradeRepository.findByLeaderIdAndLeaderTradeId(leaderId, trade.id) - if (existingAfterDelay != null) { - logger.debug("延迟查询到记录(并发检测): leaderId=$leaderId, tradeId=${trade.id}, status=${existingAfterDelay.status}") - } else { - logger.warn( - "保存ProcessedTrade失败记录时发生唯一约束冲突,但查询不到记录: leaderId=$leaderId, tradeId=${trade.id}", - e - ) - } - } - } else { - // 其他类型的异常,记录但不抛出(避免影响其他交易的处理) - logger.warn("保存ProcessedTrade失败记录时发生异常: leaderId=$leaderId, tradeId=${trade.id}", e) - } - } - - logger.warn("已记录失败交易: leaderId=$leaderId, tradeId=${trade.id}, error=$errorMessage") - } catch (e: Exception) { - logger.error("记录失败交易异常: leaderId=$leaderId, tradeId=${trade.id}", e) - } - } /** * 更新订单状态 diff --git a/backend/src/main/resources/db/migration/V16__remove_failed_trade_table.sql b/backend/src/main/resources/db/migration/V16__remove_failed_trade_table.sql new file mode 100644 index 0000000..5b43e3d --- /dev/null +++ b/backend/src/main/resources/db/migration/V16__remove_failed_trade_table.sql @@ -0,0 +1,8 @@ +-- ============================================ +-- V16: 删除失败交易记录表 +-- 移除下单失败存储到数据库的功能 +-- ============================================ + +-- 删除失败交易记录表 +DROP TABLE IF EXISTS failed_trade; + diff --git a/backend/src/main/resources/db/migration/V1__init_database.sql b/backend/src/main/resources/db/migration/V1__init_database.sql index 4061c67..dc6af8a 100644 --- a/backend/src/main/resources/db/migration/V1__init_database.sql +++ b/backend/src/main/resources/db/migration/V1__init_database.sql @@ -172,32 +172,8 @@ CREATE TABLE IF NOT EXISTS processed_trade ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='已处理交易表(用于去重)'; -- ============================================ --- 9. 创建失败交易记录表 -- ============================================ -CREATE TABLE IF NOT EXISTS failed_trade ( - id BIGINT AUTO_INCREMENT PRIMARY KEY, - leader_id BIGINT NOT NULL COMMENT 'Leader ID', - leader_trade_id VARCHAR(100) NOT NULL COMMENT 'Leader 的交易ID', - trade_type VARCHAR(10) NOT NULL COMMENT '交易类型:BUY 或 SELL', - copy_trading_id BIGINT NOT NULL COMMENT '跟单关系ID', - account_id BIGINT NOT NULL COMMENT '账户ID', - market_id VARCHAR(100) NOT NULL COMMENT '市场地址', - side VARCHAR(10) NOT NULL COMMENT '方向:YES/NO', - price VARCHAR(50) NOT NULL COMMENT '价格', - size VARCHAR(50) NOT NULL COMMENT '数量', - error_message TEXT COMMENT '错误信息', - retry_count INT NOT NULL DEFAULT 0 COMMENT '重试次数', - failed_at BIGINT NOT NULL COMMENT '失败时间(毫秒时间戳)', - created_at BIGINT NOT NULL COMMENT '创建时间(毫秒时间戳)', - INDEX idx_leader_trade (leader_id, leader_trade_id), - INDEX idx_copy_trading (copy_trading_id), - INDEX idx_failed_at (failed_at), - FOREIGN KEY (copy_trading_id) REFERENCES copy_trading(id) ON DELETE CASCADE, - FOREIGN KEY (leader_id) REFERENCES copy_trading_leaders(id) ON DELETE CASCADE -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='失败交易记录表'; - --- ============================================ --- 10. 创建用户表(用于JWT登录鉴权) +-- 9. 创建用户表(用于JWT登录鉴权) -- ============================================ CREATE TABLE IF NOT EXISTS users ( id BIGINT AUTO_INCREMENT PRIMARY KEY, diff --git a/docs/zh/smart-money-analysis.md b/docs/zh/smart-money-analysis.md new file mode 100644 index 0000000..0b69605 --- /dev/null +++ b/docs/zh/smart-money-analysis.md @@ -0,0 +1,835 @@ +# Polymarket 聪明钱分析方案 + +## 1. 概述 + +聪明钱(Smart Money)分析是指识别和跟踪在 Polymarket 平台上表现优异的交易者,通过分析他们的交易行为、持仓和盈亏表现,来辅助投资决策。 + +## 2. 核心分析维度 + +### 2.1 交易表现指标 + +#### 2.1.1 胜率(Win Rate) +- **定义**:盈利交易数 / 总交易数 +- **计算方式**: + - 通过 `getUserActivity` API 获取用户历史交易 + - 筛选 `type = "TRADE"` 的活动 + - 计算每笔交易的盈亏(通过买入价和卖出价) + - 统计盈利交易数和总交易数 + +#### 2.1.2 平均盈亏比(Average PnL Ratio) +- **定义**:平均盈利金额 / 平均亏损金额 +- **计算方式**: + - 分别计算盈利交易和亏损交易的平均金额 + - 计算比值 + +#### 2.1.3 总盈亏(Total PnL) +- **定义**:所有已实现盈亏的总和 +- **数据来源**: + - 通过 `getPositions` API 获取 `realizedPnl` + - 或通过 `getUserActivity` API 计算历史交易的累计盈亏 + +#### 2.1.4 未实现盈亏(Unrealized PnL) +- **定义**:当前持仓的浮动盈亏 +- **数据来源**: + - 通过 `getPositions` API 获取 `cashPnl`(当前盈亏) + - 或通过 `currentValue - initialValue` 计算 + +#### 2.1.5 收益率(Return Rate) +- **定义**:总盈亏 / 总投入 +- **计算方式**: + - 总投入 = 所有买入交易的总金额 + - 总盈亏 = 已实现盈亏 + 未实现盈亏 + - 收益率 = 总盈亏 / 总投入 + +### 2.2 交易行为指标 + +#### 2.2.1 交易频率(Trading Frequency) +- **定义**:单位时间内的交易次数 +- **计算方式**: + - 通过 `getUserActivity` API 获取指定时间范围内的交易数 + - 计算日均/周均交易次数 + +#### 2.2.2 持仓周期(Holding Period) +- **定义**:平均持仓时间 +- **计算方式**: + - 跟踪每笔买入和对应的卖出时间 + - 计算平均持仓天数 + +#### 2.2.3 市场偏好(Market Preference) +- **定义**:交易者偏好的市场类型 +- **计算方式**: + - 统计交易者在不同分类(sports、crypto)的交易分布 + - 统计交易者偏好的市场主题 + +#### 2.2.4 仓位规模(Position Size) +- **定义**:平均单笔交易金额 +- **计算方式**: + - 通过 `getUserActivity` API 获取 `usdcSize` + - 计算平均交易金额 + +### 2.3 风险指标 + +#### 2.3.1 最大回撤(Maximum Drawdown) +- **定义**:从峰值到谷值的最大跌幅 +- **计算方式**: + - 跟踪账户价值的时序变化 + - 计算每个峰值的回撤幅度 + - 取最大值 + +#### 2.3.2 夏普比率(Sharpe Ratio) +- **定义**:风险调整后的收益率 +- **计算方式**: + - 收益率标准差 / 平均收益率 + - 需要足够的历史数据 + +#### 2.3.3 胜率稳定性(Win Rate Stability) +- **定义**:不同时间段胜率的一致性 +- **计算方式**: + - 按时间段(如每月)计算胜率 + - 计算胜率的方差或标准差 + +## 3. 数据收集方法 + +### 3.1 使用 Polymarket Data API + +#### 3.1.1 获取用户仓位 +```kotlin +// 接口:GET /positions +// 参数: +// - user: 用户钱包地址(必需) +// - market: 市场ID(可选) +// - limit: 限制数量(可选) +// - offset: 偏移量(可选) +// - sortBy: 排序字段(可选,如 "currentValue") +// - sortDirection: 排序方向(可选,如 "desc") + +val positions = dataApi.getPositions( + user = walletAddress, + limit = 100, + sortBy = "currentValue", + sortDirection = "desc" +) +``` + +**返回数据包含**: +- `currentValue`: 当前仓位价值 +- `cashPnl`: 当前盈亏(未实现) +- `realizedPnl`: 已实现盈亏 +- `percentPnl`: 盈亏百分比 +- `avgPrice`: 平均买入价 +- `curPrice`: 当前价格 + +#### 3.1.2 获取用户活动(交易历史) +```kotlin +// 接口:GET /activity +// 参数: +// - user: 用户钱包地址(必需) +// - type: 活动类型(可选,如 ["TRADE"]) +// - side: 交易方向(可选,如 "BUY" 或 "SELL") +// - start: 开始时间戳(可选) +// - end: 结束时间戳(可选) +// - limit: 限制数量(可选) +// - offset: 偏移量(可选) + +val activities = dataApi.getUserActivity( + user = walletAddress, + type = listOf("TRADE"), + side = "BUY", + start = startTimestamp, + end = endTimestamp, + limit = 1000 +) +``` + +**返回数据包含**: +- `type`: 活动类型(TRADE、SPLIT、MERGE、REDEEM等) +- `side`: 交易方向(BUY、SELL) +- `size`: 交易数量 +- `usdcSize`: 交易金额(USDC) +- `price`: 交易价格 +- `timestamp`: 交易时间戳 +- `title`: 市场标题 +- `slug`: 市场标识 + +#### 3.1.3 获取仓位总价值 +```kotlin +// 接口:GET /value +// 参数: +// - user: 用户钱包地址(必需) +// - market: 市场ID列表(可选) + +val totalValue = dataApi.getTotalValue( + user = walletAddress, + market = listOf("market1", "market2") +) +``` + +### 3.2 使用 Polymarket CLOB API + +#### 3.2.1 获取交易记录 +```kotlin +// 接口:GET /data/trades +// 参数: +// - maker_address: 交易者地址(可选) +// - market: 市场ID(可选) +// - before: 之前的时间戳(可选,用于分页) +// - after: 之后的时间戳(可选,用于分页) +// - next_cursor: 分页游标(可选) + +val trades = clobApi.getTrades( + maker_address = walletAddress, + market = marketId, + after = startTimestamp.toString() +) +``` + +**返回数据包含**: +- `id`: 交易ID +- `market`: 市场ID +- `side`: 交易方向(BUY、SELL) +- `price`: 交易价格 +- `size`: 交易数量 +- `timestamp`: 交易时间戳 +- `user`: 交易者地址 + +## 4. 聪明钱识别算法 + +### 4.1 基础筛选条件 + +#### 4.1.1 最低交易次数 +- **条件**:总交易数 >= 50 +- **目的**:确保有足够的数据进行统计分析 + +#### 4.1.2 最低胜率 +- **条件**:胜率 >= 55% +- **目的**:筛选出表现优于随机交易者 + +#### 4.1.3 最低总盈亏 +- **条件**:总盈亏 >= 1000 USDC +- **目的**:筛选出有实际盈利能力的交易者 + +#### 4.1.4 最低收益率 +- **条件**:收益率 >= 20% +- **目的**:筛选出有良好回报的交易者 + +### 4.2 综合评分算法 + +```kotlin +// 聪明钱评分算法 +fun calculateSmartMoneyScore( + winRate: Double, // 胜率(0-1) + totalPnl: Double, // 总盈亏(USDC) + returnRate: Double, // 收益率(0-1) + tradeCount: Int, // 交易次数 + avgPnlRatio: Double // 平均盈亏比 +): Double { + // 权重配置 + val winRateWeight = 0.3 + val totalPnlWeight = 0.25 + val returnRateWeight = 0.25 + val tradeCountWeight = 0.1 + val avgPnlRatioWeight = 0.1 + + // 归一化处理 + val normalizedWinRate = winRate * 100 // 转换为百分比 + val normalizedTotalPnl = min(totalPnl / 10000, 1.0) * 100 // 归一化到0-100 + val normalizedReturnRate = returnRate * 100 // 转换为百分比 + val normalizedTradeCount = min(tradeCount / 200, 1.0) * 100 // 归一化到0-100 + val normalizedAvgPnlRatio = min(avgPnlRatio / 3.0, 1.0) * 100 // 归一化到0-100 + + // 加权求和 + val score = normalizedWinRate * winRateWeight + + normalizedTotalPnl * totalPnlWeight + + normalizedReturnRate * returnRateWeight + + normalizedTradeCount * tradeCountWeight + + normalizedAvgPnlRatio * avgPnlRatioWeight + + return score +} +``` + +### 4.3 排名算法 + +1. **按综合评分排序**:计算所有候选交易者的综合评分,按降序排列 +2. **按分类排名**:分别计算 sports 和 crypto 分类的排名 +3. **按时间段排名**:分别计算最近7天、30天、90天的排名 + +## 5. 实时监控方案 + +### 5.1 监控目标 + +1. **新交易**:监控聪明钱交易者的新买入/卖出交易 +2. **持仓变化**:监控聪明钱交易者的持仓变化 +3. **市场关注**:监控聪明钱交易者关注的新市场 + +### 5.2 实现方式 + +#### 5.2.1 使用 WebSocket(推荐) +- 订阅 Polymarket WebSocket 的 User Channel +- 监听 `event_type = "trade"` 事件 +- 过滤出聪明钱交易者的交易 + +#### 5.2.2 使用轮询 +- 定期调用 `getUserActivity` API(如每5分钟) +- 比较时间戳,识别新交易 +- 使用 `after` 参数只获取新数据 + +### 5.3 跟单集成 + +聪明钱分析可以与现有的跟单系统集成: + +1. **自动添加 Leader**:识别到聪明钱交易者后,自动添加到 Leader 列表 +2. **智能跟单**:根据聪明钱交易者的表现,动态调整跟单比例 +3. **风险控制**:根据聪明钱交易者的风险指标,设置跟单限制 + +## 6. 实现示例 + +### 6.1 聪明钱分析服务 + +```kotlin +@Service +class SmartMoneyAnalysisService( + private val retrofitFactory: RetrofitFactory, + private val blockchainService: BlockchainService +) { + private val logger = LoggerFactory.getLogger(SmartMoneyAnalysisService::class.java) + private val dataApi = retrofitFactory.createDataApi() + + /** + * 分析单个交易者的表现 + */ + suspend fun analyzeTrader(walletAddress: String, days: Int = 90): Result { + return try { + val endTime = System.currentTimeMillis() + val startTime = endTime - (days * 24 * 60 * 60 * 1000L) + + // 1. 获取交易历史 + val activitiesResult = getTradeActivities(walletAddress, startTime, endTime) + if (activitiesResult.isFailure) { + return Result.failure(activitiesResult.exceptionOrNull() ?: Exception("获取交易历史失败")) + } + val activities = activitiesResult.getOrNull() ?: emptyList() + + // 2. 获取当前仓位 + val positionsResult = blockchainService.getPositions(walletAddress) + val positions = if (positionsResult.isSuccess) { + positionsResult.getOrNull() ?: emptyList() + } else { + emptyList() + } + + // 3. 计算指标 + val metrics = calculateMetrics(activities, positions) + + // 4. 计算综合评分 + val score = calculateSmartMoneyScore( + winRate = metrics.winRate, + totalPnl = metrics.totalPnl, + returnRate = metrics.returnRate, + tradeCount = metrics.tradeCount, + avgPnlRatio = metrics.avgPnlRatio + ) + + Result.success( + TraderAnalysis( + walletAddress = walletAddress, + metrics = metrics, + score = score, + positions = positions.size, + lastTradeTime = activities.maxByOrNull { it.timestamp }?.timestamp + ) + ) + } catch (e: Exception) { + logger.error("分析交易者失败: ${e.message}", e) + Result.failure(e) + } + } + + /** + * 获取交易活动 + */ + private suspend fun getTradeActivities( + walletAddress: String, + startTime: Long, + endTime: Long + ): Result> { + return try { + val response = dataApi.getUserActivity( + user = walletAddress, + type = listOf("TRADE"), + start = startTime, + end = endTime, + limit = 1000, + sortBy = "timestamp", + sortDirection = "desc" + ) + + if (response.isSuccessful && response.body() != null) { + Result.success(response.body()!!) + } else { + Result.failure(Exception("获取交易活动失败: ${response.code()} ${response.message()}")) + } + } catch (e: Exception) { + logger.error("获取交易活动异常: ${e.message}", e) + Result.failure(e) + } + } + + /** + * 计算交易指标 + */ + private fun calculateMetrics( + activities: List, + positions: List + ): TraderMetrics { + // 分离买入和卖出交易 + val buyTrades = activities.filter { it.side == "BUY" } + val sellTrades = activities.filter { it.side == "SELL" } + + // 计算总交易数 + val tradeCount = activities.size + + // 计算总投入(买入金额总和) + val totalInvested = buyTrades.sumOf { it.usdcSize ?: 0.0 } + + // 计算已实现盈亏(从仓位数据) + val realizedPnl = positions.sumOf { it.realizedPnl ?: 0.0 } + + // 计算未实现盈亏(从仓位数据) + val unrealizedPnl = positions.sumOf { it.cashPnl ?: 0.0 } + + // 计算总盈亏 + val totalPnl = realizedPnl + unrealizedPnl + + // 计算收益率 + val returnRate = if (totalInvested > 0) { + totalPnl / totalInvested + } else { + 0.0 + } + + // 计算胜率(需要匹配买入和卖出交易) + val winRate = calculateWinRate(buyTrades, sellTrades) + + // 计算平均盈亏比 + val avgPnlRatio = calculateAvgPnlRatio(buyTrades, sellTrades) + + return TraderMetrics( + tradeCount = tradeCount, + totalInvested = totalInvested, + totalPnl = totalPnl, + realizedPnl = realizedPnl, + unrealizedPnl = unrealizedPnl, + returnRate = returnRate, + winRate = winRate, + avgPnlRatio = avgPnlRatio + ) + } + + /** + * 计算胜率 + * 通过匹配买入和卖出交易来计算 + */ + private fun calculateWinRate( + buyTrades: List, + sellTrades: List + ): Double { + // 按市场分组买入和卖出交易 + val buyByMarket = buyTrades.groupBy { it.conditionId } + val sellByMarket = sellTrades.groupBy { it.conditionId } + + var winCount = 0 + var totalCount = 0 + + // 遍历每个市场 + buyByMarket.forEach { (marketId, buys) -> + val sells = sellByMarket[marketId] ?: emptyList() + + // 简单匹配:按时间顺序匹配买入和卖出 + // 实际应该使用更精确的匹配算法(如 FIFO) + var buyIndex = 0 + var sellIndex = 0 + + while (buyIndex < buys.size && sellIndex < sells.size) { + val buy = buys[buyIndex] + val sell = sells[sellIndex] + + // 计算盈亏 + val buyPrice = buy.price ?: 0.0 + val sellPrice = sell.price ?: 0.0 + val pnl = (sellPrice - buyPrice) * (buy.size ?: 0.0) + + if (pnl > 0) { + winCount++ + } + totalCount++ + + buyIndex++ + sellIndex++ + } + } + + return if (totalCount > 0) { + winCount.toDouble() / totalCount + } else { + 0.0 + } + } + + /** + * 计算平均盈亏比 + */ + private fun calculateAvgPnlRatio( + buyTrades: List, + sellTrades: List + ): Double { + // 类似胜率计算,分别计算盈利和亏损的平均金额 + val buyByMarket = buyTrades.groupBy { it.conditionId } + val sellByMarket = sellTrades.groupBy { it.conditionId } + + val profits = mutableListOf() + val losses = mutableListOf() + + buyByMarket.forEach { (marketId, buys) -> + val sells = sellByMarket[marketId] ?: emptyList() + + var buyIndex = 0 + var sellIndex = 0 + + while (buyIndex < buys.size && sellIndex < sells.size) { + val buy = buys[buyIndex] + val sell = sells[sellIndex] + + val buyPrice = buy.price ?: 0.0 + val sellPrice = sell.price ?: 0.0 + val pnl = (sellPrice - buyPrice) * (buy.size ?: 0.0) + + if (pnl > 0) { + profits.add(pnl) + } else if (pnl < 0) { + losses.add(-pnl) + } + + buyIndex++ + sellIndex++ + } + } + + val avgProfit = if (profits.isNotEmpty()) { + profits.average() + } else { + 0.0 + } + + val avgLoss = if (losses.isNotEmpty()) { + losses.average() + } else { + 0.0 + } + + return if (avgLoss > 0) { + avgProfit / avgLoss + } else { + if (avgProfit > 0) Double.MAX_VALUE else 0.0 + } + } + + /** + * 计算聪明钱评分 + */ + private fun calculateSmartMoneyScore( + winRate: Double, + totalPnl: Double, + returnRate: Double, + tradeCount: Int, + avgPnlRatio: Double + ): Double { + val winRateWeight = 0.3 + val totalPnlWeight = 0.25 + val returnRateWeight = 0.25 + val tradeCountWeight = 0.1 + val avgPnlRatioWeight = 0.1 + + val normalizedWinRate = winRate * 100 + val normalizedTotalPnl = min(totalPnl / 10000, 1.0) * 100 + val normalizedReturnRate = returnRate * 100 + val normalizedTradeCount = min(tradeCount / 200.0, 1.0) * 100 + val normalizedAvgPnlRatio = min(avgPnlRatio / 3.0, 1.0) * 100 + + val score = normalizedWinRate * winRateWeight + + normalizedTotalPnl * totalPnlWeight + + normalizedReturnRate * returnRateWeight + + normalizedTradeCount * tradeCountWeight + + normalizedAvgPnlRatio * avgPnlRatioWeight + + return score + } + + /** + * 批量分析交易者 + */ + suspend fun analyzeTraders( + walletAddresses: List, + days: Int = 90 + ): Result> { + return try { + val analyses = walletAddresses.mapNotNull { address -> + analyzeTrader(address, days).getOrNull() + } + Result.success(analyses.sortedByDescending { it.score }) + } catch (e: Exception) { + logger.error("批量分析交易者失败: ${e.message}", e) + Result.failure(e) + } + } +} + +/** + * 交易者分析结果 + */ +data class TraderAnalysis( + val walletAddress: String, + val metrics: TraderMetrics, + val score: Double, + val positions: Int, + val lastTradeTime: Long? +) + +/** + * 交易者指标 + */ +data class TraderMetrics( + val tradeCount: Int, + val totalInvested: Double, + val totalPnl: Double, + val realizedPnl: Double, + val unrealizedPnl: Double, + val returnRate: Double, + val winRate: Double, + val avgPnlRatio: Double +) +``` + +### 6.2 聪明钱排名服务 + +```kotlin +@Service +class SmartMoneyRankingService( + private val smartMoneyAnalysisService: SmartMoneyAnalysisService +) { + private val logger = LoggerFactory.getLogger(SmartMoneyRankingService::class.java) + + /** + * 获取聪明钱排名 + */ + suspend fun getRankings( + category: String? = null, // sports 或 crypto + days: Int = 90, + limit: Int = 100 + ): Result> { + return try { + // 1. 获取候选交易者列表 + // 这里需要从某个数据源获取(如数据库、API等) + val candidates = getCandidateTraders(category) + + // 2. 批量分析交易者 + val analysesResult = smartMoneyAnalysisService.analyzeTraders(candidates, days) + if (analysesResult.isFailure) { + return Result.failure(analysesResult.exceptionOrNull() ?: Exception("分析失败")) + } + val analyses = analysesResult.getOrNull() ?: emptyList() + + // 3. 筛选和排序 + val rankings = analyses + .filter { it.metrics.tradeCount >= 50 } // 最低交易次数 + .filter { it.metrics.winRate >= 0.55 } // 最低胜率 + .filter { it.metrics.totalPnl >= 1000 } // 最低总盈亏 + .sortedByDescending { it.score } + .take(limit) + .mapIndexed { index, analysis -> + TraderRanking( + rank = index + 1, + walletAddress = analysis.walletAddress, + score = analysis.score, + metrics = analysis.metrics, + positions = analysis.positions, + lastTradeTime = analysis.lastTradeTime + ) + } + + Result.success(rankings) + } catch (e: Exception) { + logger.error("获取排名失败: ${e.message}", e) + Result.failure(e) + } + } + + /** + * 获取候选交易者列表 + * 这里需要实现具体的获取逻辑(如从数据库、API等) + */ + private suspend fun getCandidateTraders(category: String?): List { + // TODO: 实现获取候选交易者的逻辑 + // 可以从以下来源获取: + // 1. 数据库中的 Leader 列表 + // 2. Polymarket 的公开数据 + // 3. 用户提交的交易者地址 + return emptyList() + } +} + +/** + * 交易者排名 + */ +data class TraderRanking( + val rank: Int, + val walletAddress: String, + val score: Double, + val metrics: TraderMetrics, + val positions: Int, + val lastTradeTime: Long? +) +``` + +## 7. 数据存储建议 + +### 7.1 数据库表设计 + +```sql +-- 聪明钱交易者表 +CREATE TABLE smart_money_traders ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + wallet_address VARCHAR(42) NOT NULL UNIQUE, + score DOUBLE NOT NULL, + win_rate DOUBLE NOT NULL, + total_pnl DECIMAL(20, 8) NOT NULL, + return_rate DOUBLE NOT NULL, + trade_count INT NOT NULL, + category VARCHAR(20), -- sports 或 crypto + last_analysis_time BIGINT NOT NULL, + created_at BIGINT NOT NULL, + updated_at BIGINT NOT NULL, + INDEX idx_score (score DESC), + INDEX idx_category (category), + INDEX idx_last_analysis_time (last_analysis_time) +); + +-- 交易者历史指标表(用于追踪指标变化) +CREATE TABLE trader_metrics_history ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + wallet_address VARCHAR(42) NOT NULL, + win_rate DOUBLE NOT NULL, + total_pnl DECIMAL(20, 8) NOT NULL, + return_rate DOUBLE NOT NULL, + trade_count INT NOT NULL, + recorded_at BIGINT NOT NULL, + INDEX idx_wallet_address (wallet_address), + INDEX idx_recorded_at (recorded_at) +); +``` + +### 7.2 缓存策略 + +- **Redis 缓存**:缓存聪明钱排名列表,减少数据库查询 +- **缓存过期时间**:建议 1 小时 +- **缓存键**:`smart_money:rankings:{category}:{days}` + +## 8. API 接口设计 + +### 8.1 获取聪明钱排名 + +```kotlin +@PostMapping("/smart-money/rankings") +fun getRankings(@RequestBody request: SmartMoneyRankingsRequest): ResponseEntity> { + // 实现逻辑 +} +``` + +**请求参数**: +```json +{ + "category": "sports", // 可选:sports 或 crypto + "days": 90, // 可选:分析时间范围(天) + "limit": 100, // 可选:返回数量 + "minScore": 50 // 可选:最低评分 +} +``` + +**响应数据**: +```json +{ + "code": 0, + "data": { + "rankings": [ + { + "rank": 1, + "walletAddress": "0x...", + "score": 85.5, + "metrics": { + "tradeCount": 150, + "winRate": 0.65, + "totalPnl": 5000.0, + "returnRate": 0.35, + "avgPnlRatio": 2.5 + }, + "positions": 10, + "lastTradeTime": 1234567890 + } + ], + "total": 100 + }, + "msg": "" +} +``` + +### 8.2 分析单个交易者 + +```kotlin +@PostMapping("/smart-money/analyze") +fun analyzeTrader(@RequestBody request: SmartMoneyAnalyzeRequest): ResponseEntity> { + // 实现逻辑 +} +``` + +**请求参数**: +```json +{ + "walletAddress": "0x...", + "days": 90 +} +``` + +## 9. 注意事项 + +### 9.1 API 限制 + +- **Data API 速率限制**:注意 API 调用频率,避免触发限流 +- **数据延迟**:Data API 的数据可能有延迟,不是实时的 +- **数据完整性**:某些历史数据可能不完整,需要处理缺失数据 + +### 9.2 计算精度 + +- **价格精度**:Polymarket 使用 0.01-0.99 的价格范围,注意精度问题 +- **金额精度**:使用 `BigDecimal` 进行金额计算,避免浮点数误差 +- **时间精度**:注意时间戳的精度(毫秒 vs 秒) + +### 9.3 性能优化 + +- **批量查询**:尽量批量查询多个交易者的数据 +- **缓存策略**:缓存分析结果,避免重复计算 +- **异步处理**:使用异步任务处理大量数据分析 + +### 9.4 数据质量 + +- **数据验证**:验证 API 返回的数据完整性 +- **异常处理**:处理 API 调用失败的情况 +- **数据清洗**:清洗异常数据(如价格为 0、数量为负数等) + +## 10. 后续优化方向 + +1. **机器学习模型**:使用机器学习模型预测交易者未来表现 +2. **实时监控**:集成 WebSocket 实现实时监控聪明钱交易 +3. **跟单推荐**:根据聪明钱分析结果,推荐适合跟单的交易者 +4. **风险预警**:监控聪明钱交易者的风险指标,及时预警 +5. **多维度分析**:增加更多分析维度(如市场类型、时间分布等) +