feat: 实现 NBA 量化交易系统
- 后端实现: - 实现 NBA 比赛数据服务,从 Polymarket API 获取数据 - 实现数据库存储和增量拉取逻辑(优先从 DB 获取,数据不足时增量拉取) - 使用 sports_market_types 参数直接筛选 moneyline 类型 - 实现分页拉取逻辑(基于 gameStartTime 和 createdAt) - 移除 nba_markets 相关的外键约束(V12 迁移) - 修复数据拉取逻辑:超过 3 天的数据不拉取 - 前端实现: - 实现策略创建/编辑/列表页面 - 实现交易信号展示页面和统计页面 - 修复重复请求问题(使用 useCallback 包装 fetchGames) - 支持选择单场比赛进行配置 - 使用西8区时间格式化显示 - 数据库: - 创建 NBA 量化交易相关表(V11 迁移) - 移除外键约束(V12 迁移) - 文档: - 添加产品需求文档、技术方案、算法文档等
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
package com.wrbug.polymarketbot.api
|
||||
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
/**
|
||||
* NBA Stats API 接口
|
||||
* Base URL: https://stats.nba.com/stats/
|
||||
*
|
||||
* 注意:NBA Stats API 需要设置正确的请求头:
|
||||
* - User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
|
||||
* - Referer: https://www.nba.com/
|
||||
* - Accept: application/json
|
||||
*/
|
||||
interface NbaStatsApi {
|
||||
|
||||
/**
|
||||
* 获取赛程和比分
|
||||
* @param GameDate 比赛日期,格式:YYYY-MM-DD,不传则获取今天的比赛
|
||||
* @param LeagueID 联盟ID,默认:00 (NBA)
|
||||
* @param DayOffset 日期偏移,默认:0
|
||||
* @return ScoreboardResponse
|
||||
*/
|
||||
@GET("Scoreboard")
|
||||
suspend fun getScoreboard(
|
||||
@Query("GameDate") gameDate: String? = null,
|
||||
@Query("LeagueID") leagueId: String = "00",
|
||||
@Query("DayOffset") dayOffset: Int = 0
|
||||
): Response<ScoreboardResponse>
|
||||
}
|
||||
|
||||
/**
|
||||
* NBA Stats API Scoreboard 响应
|
||||
*/
|
||||
data class ScoreboardResponse(
|
||||
val resultSets: List<ResultSet>
|
||||
)
|
||||
|
||||
/**
|
||||
* Result Set
|
||||
*/
|
||||
data class ResultSet(
|
||||
val name: String,
|
||||
val headers: List<String>,
|
||||
val rowSet: List<List<Any?>>
|
||||
)
|
||||
|
||||
/**
|
||||
* Game Header (从 Scoreboard 的 resultSets[0] 获取)
|
||||
* Headers: ["GAME_DATE_EST", "GAME_SEQUENCE", "GAME_ID", "GAME_STATUS_ID", "GAME_STATUS_TEXT",
|
||||
* "GAMECODE", "HOME_TEAM_ID", "VISITOR_TEAM_ID", "SEASON", "LIVE_PERIOD",
|
||||
* "LIVE_PC_TIME", "NATL_TV_BROADCASTER_ABBREV", "LIVE_PERIOD_TIME_BCAST", "WH_STATUS"]
|
||||
*/
|
||||
data class GameHeader(
|
||||
val gameDateEst: String,
|
||||
val gameSequence: Int,
|
||||
val gameId: String,
|
||||
val gameStatusId: Int,
|
||||
val gameStatusText: String,
|
||||
val gameCode: String,
|
||||
val homeTeamId: Int,
|
||||
val visitorTeamId: Int,
|
||||
val season: String,
|
||||
val livePeriod: Int?,
|
||||
val livePcTime: String?,
|
||||
val natlTvBroadcasterAbbrev: String?,
|
||||
val livePeriodTimeBcast: String?,
|
||||
val whStatus: Int?
|
||||
)
|
||||
|
||||
/**
|
||||
* Line Score (从 Scoreboard 的 resultSets[1] 获取)
|
||||
* Headers: ["GAME_DATE_EST", "GAME_SEQUENCE", "GAME_ID", "TEAM_ID", "TEAM_ABBREVIATION",
|
||||
* "TEAM_NAME", "PTS_QTR1", "PTS_QTR2", "PTS_QTR3", "PTS_QTR4", "PTS_OT1",
|
||||
* "PTS_OT2", "PTS_OT3", "PTS_OT4", "PTS", "FG_PCT", "FT_PCT", "FG3_PCT",
|
||||
* "AST", "REB", "TOV"]
|
||||
*/
|
||||
data class LineScore(
|
||||
val gameDateEst: String,
|
||||
val gameSequence: Int,
|
||||
val gameId: String,
|
||||
val teamId: Int,
|
||||
val teamAbbreviation: String,
|
||||
val teamName: String,
|
||||
val ptsQtr1: Int?,
|
||||
val ptsQtr2: Int?,
|
||||
val ptsQtr3: Int?,
|
||||
val ptsQtr4: Int?,
|
||||
val ptsOt1: Int?,
|
||||
val ptsOt2: Int?,
|
||||
val ptsOt3: Int?,
|
||||
val ptsOt4: Int?,
|
||||
val pts: Int,
|
||||
val fgPct: Double?,
|
||||
val ftPct: Double?,
|
||||
val fg3Pct: Double?,
|
||||
val ast: Int?,
|
||||
val reb: Int?,
|
||||
val tov: Int?
|
||||
)
|
||||
|
||||
@@ -13,19 +13,54 @@ import retrofit2.http.Query
|
||||
interface PolymarketGammaApi {
|
||||
|
||||
/**
|
||||
* 根据 condition ID 列表获取市场信息
|
||||
* 获取体育元数据信息
|
||||
* 文档: https://docs.polymarket.com/api-reference/sports/get-sports-metadata-information
|
||||
* @return 体育元数据数组
|
||||
*/
|
||||
@GET("/sports")
|
||||
suspend fun getSports(): Response<List<SportsMetadataResponse>>
|
||||
|
||||
/**
|
||||
* 根据条件获取市场信息
|
||||
* 文档: https://docs.polymarket.com/api-reference/markets/list-markets
|
||||
* @param conditionIds condition ID 数组(16 进制字符串,如 "0x...")
|
||||
* @param includeTag 是否包含标签信息
|
||||
* @param tags 标签 ID 数组,用于过滤市场(如 NBA 的 tag ID)
|
||||
* @param active 是否只返回活跃的市场
|
||||
* @param closed 是否包含已关闭的市场
|
||||
* @param archived 是否包含已归档的市场
|
||||
* @param limit 返回的市场数量限制
|
||||
* @param startDateMin 最小开始日期(ISO 8601 格式,UTC 时区,如 "2025-12-01T00:00:00Z")
|
||||
* @param sportsMarketTypes 体育市场类型数组(如 ["moneyline"] 用于筛选 moneyline 类型)
|
||||
* @return 市场信息数组
|
||||
*/
|
||||
@GET("/markets")
|
||||
suspend fun listMarkets(
|
||||
@Query("condition_ids") conditionIds: List<String>? = null,
|
||||
@Query("include_tag") includeTag: Boolean? = null
|
||||
@Query("include_tag") includeTag: Boolean? = null,
|
||||
@Query("tags") tags: List<String>? = null,
|
||||
@Query("active") active: Boolean? = null,
|
||||
@Query("closed") closed: Boolean? = null,
|
||||
@Query("archived") archived: Boolean? = null,
|
||||
@Query("limit") limit: Int? = null,
|
||||
@Query("start_date_min") startDateMin: String? = null,
|
||||
@Query("sports_market_types") sportsMarketTypes: List<String>? = null,
|
||||
): Response<List<MarketResponse>>
|
||||
}
|
||||
|
||||
/**
|
||||
* 体育元数据响应
|
||||
* 文档: https://docs.polymarket.com/api-reference/sports/get-sports-metadata-information
|
||||
*/
|
||||
data class SportsMetadataResponse(
|
||||
val sport: String? = null, // 体育标识符或缩写(如 "NBA")
|
||||
val image: String? = null, // 体育 logo 或图片 URL
|
||||
val resolution: String? = null, // 官方决议源 URL
|
||||
val ordering: String? = null, // 显示顺序(通常是 "home" 或 "away")
|
||||
val tags: String? = null, // 逗号分隔的标签 ID 列表
|
||||
val series: String? = null // 系列标识符
|
||||
)
|
||||
|
||||
/**
|
||||
* 市场响应(根据 Gamma API 文档)
|
||||
*/
|
||||
@@ -51,6 +86,20 @@ data class MarketResponse(
|
||||
val liquidityNum: Double? = null,
|
||||
val lastTradePrice: Double? = null,
|
||||
val bestBid: Double? = null,
|
||||
val bestAsk: Double? = null
|
||||
val bestAsk: Double? = null,
|
||||
val tags: List<MarketTag>? = null, // 市场标签列表
|
||||
val sportsMarketType: String? = null, // 市场类型:moneyline, spread 等
|
||||
val gameStartTime: String? = null, // 比赛开始时间(格式:2025-12-13 00:00:00+00)
|
||||
val createdAt: String? = null, // 市场创建时间(ISO 8601 格式)
|
||||
val resolutionSource: String? = null // 决议源 URL(如 "https://www.nba.com/")
|
||||
)
|
||||
|
||||
/**
|
||||
* 市场标签
|
||||
*/
|
||||
data class MarketTag(
|
||||
val id: String? = null,
|
||||
val label: String? = null,
|
||||
val slug: String? = null
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.wrbug.polymarketbot.controller.nba
|
||||
|
||||
import com.wrbug.polymarketbot.dto.*
|
||||
import com.wrbug.polymarketbot.enums.ErrorCode
|
||||
import com.wrbug.polymarketbot.service.nba.NbaGameService
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.MessageSource
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
/**
|
||||
* NBA 比赛控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/nba/games")
|
||||
class NbaGameController(
|
||||
private val nbaGameService: NbaGameService,
|
||||
private val messageSource: MessageSource
|
||||
) {
|
||||
private val logger = LoggerFactory.getLogger(NbaGameController::class.java)
|
||||
|
||||
/**
|
||||
* 获取 NBA 比赛列表
|
||||
*/
|
||||
@PostMapping("/list")
|
||||
fun getNbaGames(@RequestBody request: NbaGameListRequest): ResponseEntity<ApiResponse<NbaGameListResponse>> {
|
||||
return try {
|
||||
val result = runBlocking {
|
||||
nbaGameService.getNbaGames(request)
|
||||
}
|
||||
|
||||
result.fold(
|
||||
onSuccess = { ResponseEntity.ok(ApiResponse.success(it)) },
|
||||
onFailure = { e ->
|
||||
logger.error("获取 NBA 比赛列表失败: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("获取 NBA 比赛列表异常: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 7 天内的所有球队(用于策略配置)
|
||||
*/
|
||||
@PostMapping("/teams")
|
||||
fun getTeamsInNext7Days(): ResponseEntity<ApiResponse<List<String>>> {
|
||||
return try {
|
||||
val result = runBlocking {
|
||||
nbaGameService.getTeamsInNext7Days()
|
||||
}
|
||||
|
||||
result.fold(
|
||||
onSuccess = { ResponseEntity.ok(ApiResponse.success(it)) },
|
||||
onFailure = { e ->
|
||||
logger.error("获取球队列表失败: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("获取球队列表异常: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.wrbug.polymarketbot.controller.nba
|
||||
|
||||
import com.wrbug.polymarketbot.dto.*
|
||||
import com.wrbug.polymarketbot.enums.ErrorCode
|
||||
import com.wrbug.polymarketbot.service.nba.NbaMarketService
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.MessageSource
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
/**
|
||||
* NBA 市场控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/nba/markets")
|
||||
class NbaMarketController(
|
||||
private val nbaMarketService: NbaMarketService,
|
||||
private val messageSource: MessageSource
|
||||
) {
|
||||
private val logger = LoggerFactory.getLogger(NbaMarketController::class.java)
|
||||
|
||||
/**
|
||||
* 获取 NBA 市场列表
|
||||
*/
|
||||
@PostMapping("/list")
|
||||
fun getNbaMarkets(@RequestBody request: NbaMarketListRequest): ResponseEntity<ApiResponse<NbaMarketListResponse>> {
|
||||
return try {
|
||||
val result = runBlocking {
|
||||
nbaMarketService.getNbaMarkets(request)
|
||||
}
|
||||
|
||||
result.fold(
|
||||
onSuccess = { ResponseEntity.ok(ApiResponse.success(it)) },
|
||||
onFailure = { e ->
|
||||
logger.error("获取 NBA 市场列表失败: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("获取 NBA 市场列表异常: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 NBA 市场中获取球队列表(用于策略配置)
|
||||
* 从市场名称中解析出所有唯一的球队名称
|
||||
*/
|
||||
@PostMapping("/teams")
|
||||
fun getTeamsFromMarkets(): ResponseEntity<ApiResponse<List<String>>> {
|
||||
return try {
|
||||
val result = runBlocking {
|
||||
nbaMarketService.getTeamsFromMarkets(active = true)
|
||||
}
|
||||
|
||||
result.fold(
|
||||
onSuccess = { ResponseEntity.ok(ApiResponse.success(it)) },
|
||||
onFailure = { e ->
|
||||
logger.error("获取球队列表失败: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("获取球队列表异常: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
package com.wrbug.polymarketbot.controller.nba
|
||||
|
||||
import com.wrbug.polymarketbot.dto.*
|
||||
import com.wrbug.polymarketbot.enums.ErrorCode
|
||||
import com.wrbug.polymarketbot.service.nba.NbaQuantitativeStrategyService
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.MessageSource
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
/**
|
||||
* NBA 量化策略控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/nba/strategies")
|
||||
class NbaQuantitativeStrategyController(
|
||||
private val strategyService: NbaQuantitativeStrategyService,
|
||||
private val messageSource: MessageSource
|
||||
) {
|
||||
private val logger = LoggerFactory.getLogger(NbaQuantitativeStrategyController::class.java)
|
||||
|
||||
/**
|
||||
* 创建策略
|
||||
*/
|
||||
@PostMapping("/create")
|
||||
fun createStrategy(@RequestBody request: NbaQuantitativeStrategyCreateRequest): ResponseEntity<ApiResponse<NbaQuantitativeStrategyDto>> {
|
||||
return try {
|
||||
if (request.strategyName.isBlank()) {
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_EMPTY, customMsg = "策略名称不能为空", messageSource = messageSource))
|
||||
}
|
||||
if (request.accountId <= 0) {
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, customMsg = "账户ID无效", messageSource = messageSource))
|
||||
}
|
||||
|
||||
val result = runBlocking { strategyService.createStrategy(request) }
|
||||
result.fold(
|
||||
onSuccess = { strategy ->
|
||||
ResponseEntity.ok(ApiResponse.success(strategy))
|
||||
},
|
||||
onFailure = { e ->
|
||||
logger.error("创建策略失败: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("创建策略异常: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新策略
|
||||
*/
|
||||
@PostMapping("/update")
|
||||
fun updateStrategy(@RequestBody request: NbaQuantitativeStrategyUpdateRequest): ResponseEntity<ApiResponse<NbaQuantitativeStrategyDto>> {
|
||||
return try {
|
||||
if (request.id <= 0) {
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, customMsg = "策略ID无效", messageSource = messageSource))
|
||||
}
|
||||
|
||||
val result = runBlocking { strategyService.updateStrategy(request) }
|
||||
result.fold(
|
||||
onSuccess = { strategy ->
|
||||
ResponseEntity.ok(ApiResponse.success(strategy))
|
||||
},
|
||||
onFailure = { e ->
|
||||
logger.error("更新策略失败: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("更新策略异常: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取策略列表
|
||||
*/
|
||||
@PostMapping("/list")
|
||||
fun getStrategyList(@RequestBody request: NbaQuantitativeStrategyListRequest): ResponseEntity<ApiResponse<NbaQuantitativeStrategyListResponse>> {
|
||||
return try {
|
||||
val result = runBlocking { strategyService.getStrategyList(request) }
|
||||
result.fold(
|
||||
onSuccess = { response ->
|
||||
ResponseEntity.ok(ApiResponse.success(response))
|
||||
},
|
||||
onFailure = { e ->
|
||||
logger.error("获取策略列表失败: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("获取策略列表异常: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取策略详情
|
||||
*/
|
||||
@PostMapping("/detail")
|
||||
fun getStrategyDetail(@RequestBody request: NbaQuantitativeStrategyDetailRequest): ResponseEntity<ApiResponse<NbaQuantitativeStrategyDto>> {
|
||||
return try {
|
||||
if (request.id == null || request.id <= 0) {
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_EMPTY, customMsg = "策略ID不能为空", messageSource = messageSource))
|
||||
}
|
||||
|
||||
val result = runBlocking { strategyService.getStrategyDetail(request.id) }
|
||||
result.fold(
|
||||
onSuccess = { strategy ->
|
||||
ResponseEntity.ok(ApiResponse.success(strategy))
|
||||
},
|
||||
onFailure = { e ->
|
||||
logger.error("获取策略详情失败: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("获取策略详情异常: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除策略
|
||||
*/
|
||||
@PostMapping("/delete")
|
||||
fun deleteStrategy(@RequestBody request: NbaQuantitativeStrategyDeleteRequest): ResponseEntity<ApiResponse<Unit>> {
|
||||
return try {
|
||||
if (request.id == null || request.id <= 0) {
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_EMPTY, customMsg = "策略ID不能为空", messageSource = messageSource))
|
||||
}
|
||||
|
||||
val result = runBlocking { strategyService.deleteStrategy(request.id) }
|
||||
result.fold(
|
||||
onSuccess = {
|
||||
ResponseEntity.ok(ApiResponse.success(Unit))
|
||||
},
|
||||
onFailure = { e ->
|
||||
logger.error("删除策略失败: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("删除策略异常: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 策略详情请求
|
||||
*/
|
||||
data class NbaQuantitativeStrategyDetailRequest(
|
||||
val id: Long?
|
||||
)
|
||||
|
||||
/**
|
||||
* 策略删除请求
|
||||
*/
|
||||
data class NbaQuantitativeStrategyDeleteRequest(
|
||||
val id: Long?
|
||||
)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.wrbug.polymarketbot.dto
|
||||
|
||||
import java.time.LocalDate
|
||||
|
||||
/**
|
||||
* NBA 比赛 DTO
|
||||
*/
|
||||
data class NbaGameDto(
|
||||
val id: Long?,
|
||||
val nbaGameId: String?,
|
||||
val homeTeam: String,
|
||||
val awayTeam: String,
|
||||
val gameDate: LocalDate,
|
||||
val gameTime: Long?,
|
||||
val gameStatus: String,
|
||||
val homeScore: Int,
|
||||
val awayScore: Int,
|
||||
val period: Int,
|
||||
val timeRemaining: String?,
|
||||
val polymarketMarketId: String?
|
||||
)
|
||||
|
||||
/**
|
||||
* NBA 比赛列表响应
|
||||
*/
|
||||
data class NbaGameListResponse(
|
||||
val list: List<NbaGameDto>,
|
||||
val total: Long
|
||||
)
|
||||
|
||||
/**
|
||||
* NBA 比赛列表请求
|
||||
* 前端传递时间戳(毫秒),后端转换为西8区时间
|
||||
*/
|
||||
data class NbaGameListRequest(
|
||||
val startTimestamp: Long? = null, // 开始时间戳(毫秒)
|
||||
val endTimestamp: Long? = null, // 结束时间戳(毫秒)
|
||||
val gameStatus: String? = null
|
||||
)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.wrbug.polymarketbot.dto
|
||||
|
||||
/**
|
||||
* NBA 市场 DTO
|
||||
*/
|
||||
data class NbaMarketDto(
|
||||
val id: String? = null,
|
||||
val question: String? = null,
|
||||
val conditionId: String? = null,
|
||||
val slug: String? = null,
|
||||
val description: String? = null,
|
||||
val category: String? = null,
|
||||
val active: Boolean? = null,
|
||||
val closed: Boolean? = null,
|
||||
val archived: Boolean? = null,
|
||||
val volume: String? = null,
|
||||
val liquidity: String? = null,
|
||||
val endDate: String? = null,
|
||||
val startDate: String? = null,
|
||||
val outcomes: String? = null,
|
||||
val outcomePrices: String? = null,
|
||||
val volumeNum: Double? = null,
|
||||
val liquidityNum: Double? = null,
|
||||
val lastTradePrice: Double? = null,
|
||||
val bestBid: Double? = null,
|
||||
val bestAsk: Double? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* NBA 市场列表响应
|
||||
*/
|
||||
data class NbaMarketListResponse(
|
||||
val list: List<NbaMarketDto>,
|
||||
val total: Long
|
||||
)
|
||||
|
||||
/**
|
||||
* NBA 市场列表请求
|
||||
*/
|
||||
data class NbaMarketListRequest(
|
||||
val active: Boolean? = true,
|
||||
val closed: Boolean? = false,
|
||||
val archived: Boolean? = false
|
||||
)
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
package com.wrbug.polymarketbot.dto
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDate
|
||||
|
||||
/**
|
||||
* NBA 量化策略 DTO
|
||||
*/
|
||||
data class NbaQuantitativeStrategyDto(
|
||||
val id: Long?,
|
||||
val strategyName: String,
|
||||
val strategyDescription: String?,
|
||||
val accountId: Long,
|
||||
val accountName: String?,
|
||||
val enabled: Boolean,
|
||||
val filterTeams: List<String>?,
|
||||
val filterDateFrom: LocalDate?,
|
||||
val filterDateTo: LocalDate?,
|
||||
val filterGameImportance: String?,
|
||||
val minWinProbabilityDiff: BigDecimal,
|
||||
val minWinProbability: BigDecimal?,
|
||||
val maxWinProbability: BigDecimal?,
|
||||
val minTradeValue: BigDecimal,
|
||||
val minRemainingTime: Int?,
|
||||
val maxRemainingTime: Int?,
|
||||
val minScoreDiff: Int?,
|
||||
val maxScoreDiff: Int?,
|
||||
val buyAmountStrategy: String,
|
||||
val fixedBuyAmount: BigDecimal?,
|
||||
val buyRatio: BigDecimal?,
|
||||
val baseBuyAmount: BigDecimal?,
|
||||
val buyTiming: String,
|
||||
val delayBuySeconds: Int,
|
||||
val buyDirection: String,
|
||||
val enableSell: Boolean,
|
||||
val takeProfitThreshold: BigDecimal?,
|
||||
val stopLossThreshold: BigDecimal?,
|
||||
val probabilityReversalThreshold: BigDecimal?,
|
||||
val sellRatio: BigDecimal,
|
||||
val sellTiming: String,
|
||||
val delaySellSeconds: Int,
|
||||
val priceStrategy: String,
|
||||
val fixedPrice: BigDecimal?,
|
||||
val priceOffset: BigDecimal,
|
||||
val maxPosition: BigDecimal,
|
||||
val minPosition: BigDecimal,
|
||||
val maxGamePosition: BigDecimal?,
|
||||
val maxDailyLoss: BigDecimal?,
|
||||
val maxDailyOrders: Int?,
|
||||
val maxDailyProfit: BigDecimal?,
|
||||
val priceTolerance: BigDecimal,
|
||||
val minProbabilityThreshold: BigDecimal?,
|
||||
val maxProbabilityThreshold: BigDecimal?,
|
||||
val baseStrengthWeight: BigDecimal,
|
||||
val recentFormWeight: BigDecimal,
|
||||
val lineupIntegrityWeight: BigDecimal,
|
||||
val starStatusWeight: BigDecimal,
|
||||
val environmentWeight: BigDecimal,
|
||||
val matchupAdvantageWeight: BigDecimal,
|
||||
val scoreDiffWeight: BigDecimal,
|
||||
val momentumWeight: BigDecimal,
|
||||
val dataUpdateFrequency: Int,
|
||||
val analysisFrequency: Int,
|
||||
val pushFailedOrders: Boolean,
|
||||
val pushFrequency: String,
|
||||
val batchPushInterval: Int,
|
||||
val createdAt: Long,
|
||||
val updatedAt: Long
|
||||
)
|
||||
|
||||
/**
|
||||
* NBA 量化策略创建请求
|
||||
*/
|
||||
data class NbaQuantitativeStrategyCreateRequest(
|
||||
val strategyName: String,
|
||||
val strategyDescription: String? = null,
|
||||
val accountId: Long,
|
||||
val enabled: Boolean = true,
|
||||
val filterTeams: List<String>? = null,
|
||||
val filterDateFrom: LocalDate? = null,
|
||||
val filterDateTo: LocalDate? = null,
|
||||
val filterGameImportance: String? = null,
|
||||
val minWinProbabilityDiff: BigDecimal? = null,
|
||||
val minWinProbability: BigDecimal? = null,
|
||||
val maxWinProbability: BigDecimal? = null,
|
||||
val minTradeValue: BigDecimal? = null,
|
||||
val minRemainingTime: Int? = null,
|
||||
val maxRemainingTime: Int? = null,
|
||||
val minScoreDiff: Int? = null,
|
||||
val maxScoreDiff: Int? = null,
|
||||
val buyAmountStrategy: String? = null,
|
||||
val fixedBuyAmount: BigDecimal? = null,
|
||||
val buyRatio: BigDecimal? = null,
|
||||
val baseBuyAmount: BigDecimal? = null,
|
||||
val buyTiming: String? = null,
|
||||
val delayBuySeconds: Int? = null,
|
||||
val buyDirection: String? = null,
|
||||
val enableSell: Boolean? = null,
|
||||
val takeProfitThreshold: BigDecimal? = null,
|
||||
val stopLossThreshold: BigDecimal? = null,
|
||||
val probabilityReversalThreshold: BigDecimal? = null,
|
||||
val sellRatio: BigDecimal? = null,
|
||||
val sellTiming: String? = null,
|
||||
val delaySellSeconds: Int? = null,
|
||||
val priceStrategy: String? = null,
|
||||
val fixedPrice: BigDecimal? = null,
|
||||
val priceOffset: BigDecimal? = null,
|
||||
val maxPosition: BigDecimal? = null,
|
||||
val minPosition: BigDecimal? = null,
|
||||
val maxGamePosition: BigDecimal? = null,
|
||||
val maxDailyLoss: BigDecimal? = null,
|
||||
val maxDailyOrders: Int? = null,
|
||||
val maxDailyProfit: BigDecimal? = null,
|
||||
val priceTolerance: BigDecimal? = null,
|
||||
val minProbabilityThreshold: BigDecimal? = null,
|
||||
val maxProbabilityThreshold: BigDecimal? = null,
|
||||
val baseStrengthWeight: BigDecimal? = null,
|
||||
val recentFormWeight: BigDecimal? = null,
|
||||
val lineupIntegrityWeight: BigDecimal? = null,
|
||||
val starStatusWeight: BigDecimal? = null,
|
||||
val environmentWeight: BigDecimal? = null,
|
||||
val matchupAdvantageWeight: BigDecimal? = null,
|
||||
val scoreDiffWeight: BigDecimal? = null,
|
||||
val momentumWeight: BigDecimal? = null,
|
||||
val dataUpdateFrequency: Int? = null,
|
||||
val analysisFrequency: Int? = null,
|
||||
val pushFailedOrders: Boolean? = null,
|
||||
val pushFrequency: String? = null,
|
||||
val batchPushInterval: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* NBA 量化策略更新请求
|
||||
*/
|
||||
data class NbaQuantitativeStrategyUpdateRequest(
|
||||
val id: Long,
|
||||
val strategyName: String? = null,
|
||||
val strategyDescription: String? = null,
|
||||
val enabled: Boolean? = null,
|
||||
val filterTeams: List<String>? = null,
|
||||
val filterDateFrom: LocalDate? = null,
|
||||
val filterDateTo: LocalDate? = null,
|
||||
val filterGameImportance: String? = null,
|
||||
val minWinProbabilityDiff: BigDecimal? = null,
|
||||
val minWinProbability: BigDecimal? = null,
|
||||
val maxWinProbability: BigDecimal? = null,
|
||||
val minTradeValue: BigDecimal? = null,
|
||||
val minRemainingTime: Int? = null,
|
||||
val maxRemainingTime: Int? = null,
|
||||
val minScoreDiff: Int? = null,
|
||||
val maxScoreDiff: Int? = null,
|
||||
val buyAmountStrategy: String? = null,
|
||||
val fixedBuyAmount: BigDecimal? = null,
|
||||
val buyRatio: BigDecimal? = null,
|
||||
val baseBuyAmount: BigDecimal? = null,
|
||||
val buyTiming: String? = null,
|
||||
val delayBuySeconds: Int? = null,
|
||||
val buyDirection: String? = null,
|
||||
val enableSell: Boolean? = null,
|
||||
val takeProfitThreshold: BigDecimal? = null,
|
||||
val stopLossThreshold: BigDecimal? = null,
|
||||
val probabilityReversalThreshold: BigDecimal? = null,
|
||||
val sellRatio: BigDecimal? = null,
|
||||
val sellTiming: String? = null,
|
||||
val delaySellSeconds: Int? = null,
|
||||
val priceStrategy: String? = null,
|
||||
val fixedPrice: BigDecimal? = null,
|
||||
val priceOffset: BigDecimal? = null,
|
||||
val maxPosition: BigDecimal? = null,
|
||||
val minPosition: BigDecimal? = null,
|
||||
val maxGamePosition: BigDecimal? = null,
|
||||
val maxDailyLoss: BigDecimal? = null,
|
||||
val maxDailyOrders: Int? = null,
|
||||
val maxDailyProfit: BigDecimal? = null,
|
||||
val priceTolerance: BigDecimal? = null,
|
||||
val minProbabilityThreshold: BigDecimal? = null,
|
||||
val maxProbabilityThreshold: BigDecimal? = null,
|
||||
val baseStrengthWeight: BigDecimal? = null,
|
||||
val recentFormWeight: BigDecimal? = null,
|
||||
val lineupIntegrityWeight: BigDecimal? = null,
|
||||
val starStatusWeight: BigDecimal? = null,
|
||||
val environmentWeight: BigDecimal? = null,
|
||||
val matchupAdvantageWeight: BigDecimal? = null,
|
||||
val scoreDiffWeight: BigDecimal? = null,
|
||||
val momentumWeight: BigDecimal? = null,
|
||||
val dataUpdateFrequency: Int? = null,
|
||||
val analysisFrequency: Int? = null,
|
||||
val pushFailedOrders: Boolean? = null,
|
||||
val pushFrequency: String? = null,
|
||||
val batchPushInterval: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* NBA 量化策略列表请求
|
||||
*/
|
||||
data class NbaQuantitativeStrategyListRequest(
|
||||
val accountId: Long? = null,
|
||||
val enabled: Boolean? = null,
|
||||
val strategyName: String? = null,
|
||||
val page: Int? = 1,
|
||||
val limit: Int? = 20
|
||||
)
|
||||
|
||||
/**
|
||||
* NBA 量化策略列表响应
|
||||
*/
|
||||
data class NbaQuantitativeStrategyListResponse(
|
||||
val list: List<NbaQuantitativeStrategyDto>,
|
||||
val total: Long,
|
||||
val page: Int,
|
||||
val limit: Int
|
||||
)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.wrbug.polymarketbot.entity
|
||||
|
||||
import jakarta.persistence.*
|
||||
import java.time.LocalDate
|
||||
|
||||
/**
|
||||
* NBA 比赛实体
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "nba_games")
|
||||
data class NbaGame(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
val id: Long? = null,
|
||||
|
||||
@Column(name = "nba_game_id", unique = true, length = 100)
|
||||
val nbaGameId: String? = null,
|
||||
|
||||
@Column(name = "home_team", nullable = false, length = 100)
|
||||
val homeTeam: String,
|
||||
|
||||
@Column(name = "away_team", nullable = false, length = 100)
|
||||
val awayTeam: String,
|
||||
|
||||
@Column(name = "game_date", nullable = false)
|
||||
val gameDate: LocalDate,
|
||||
|
||||
@Column(name = "game_time")
|
||||
val gameTime: Long? = null,
|
||||
|
||||
@Column(name = "game_status", length = 50)
|
||||
val gameStatus: String = "scheduled",
|
||||
|
||||
@Column(name = "home_score")
|
||||
val homeScore: Int = 0,
|
||||
|
||||
@Column(name = "away_score")
|
||||
val awayScore: Int = 0,
|
||||
|
||||
@Column(name = "period")
|
||||
val period: Int = 0,
|
||||
|
||||
@Column(name = "time_remaining", length = 50)
|
||||
val timeRemaining: String? = null,
|
||||
|
||||
@Column(name = "polymarket_market_id", length = 100)
|
||||
val polymarketMarketId: String? = null,
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
val createdAt: Long = System.currentTimeMillis(),
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
var updatedAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.wrbug.polymarketbot.entity
|
||||
|
||||
import jakarta.persistence.*
|
||||
|
||||
/**
|
||||
* NBA 市场实体(Polymarket 市场信息)
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "nba_markets")
|
||||
data class NbaMarket(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
val id: Long? = null,
|
||||
|
||||
@Column(name = "polymarket_market_id", unique = true, nullable = false, length = 100)
|
||||
val polymarketMarketId: String,
|
||||
|
||||
@Column(name = "condition_id", unique = true, nullable = false, length = 100)
|
||||
val conditionId: String,
|
||||
|
||||
@Column(name = "market_slug", length = 255)
|
||||
val marketSlug: String? = null,
|
||||
|
||||
@Column(name = "market_question", columnDefinition = "TEXT")
|
||||
val marketQuestion: String? = null,
|
||||
|
||||
@Column(name = "market_description", columnDefinition = "TEXT")
|
||||
val marketDescription: String? = null,
|
||||
|
||||
@Column(name = "category", length = 50)
|
||||
val category: String = "sports",
|
||||
|
||||
@Column(name = "active")
|
||||
val active: Boolean = true,
|
||||
|
||||
@Column(name = "closed")
|
||||
val closed: Boolean = false,
|
||||
|
||||
@Column(name = "archived")
|
||||
val archived: Boolean = false,
|
||||
|
||||
@Column(name = "volume", length = 50)
|
||||
val volume: String? = null,
|
||||
|
||||
@Column(name = "liquidity", length = 50)
|
||||
val liquidity: String? = null,
|
||||
|
||||
@Column(name = "outcomes", columnDefinition = "TEXT")
|
||||
val outcomes: String? = null,
|
||||
|
||||
@Column(name = "end_date", length = 50)
|
||||
val endDate: String? = null,
|
||||
|
||||
@Column(name = "start_date", length = 50)
|
||||
val startDate: String? = null,
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
val createdAt: Long = System.currentTimeMillis(),
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
var updatedAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
package com.wrbug.polymarketbot.entity
|
||||
|
||||
import jakarta.persistence.*
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDate
|
||||
|
||||
/**
|
||||
* NBA 量化策略配置实体
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "nba_quantitative_strategies")
|
||||
data class NbaQuantitativeStrategy(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
val id: Long? = null,
|
||||
|
||||
@Column(name = "strategy_name", nullable = false, length = 100)
|
||||
val strategyName: String,
|
||||
|
||||
@Column(name = "strategy_description", columnDefinition = "TEXT")
|
||||
val strategyDescription: String? = null,
|
||||
|
||||
@Column(name = "account_id", nullable = false)
|
||||
val accountId: Long,
|
||||
|
||||
@Column(name = "enabled")
|
||||
val enabled: Boolean = true,
|
||||
|
||||
// 比赛筛选参数
|
||||
@Column(name = "filter_teams", columnDefinition = "TEXT")
|
||||
val filterTeams: String? = null,
|
||||
|
||||
@Column(name = "filter_date_from")
|
||||
val filterDateFrom: LocalDate? = null,
|
||||
|
||||
@Column(name = "filter_date_to")
|
||||
val filterDateTo: LocalDate? = null,
|
||||
|
||||
@Column(name = "filter_game_importance", length = 50)
|
||||
val filterGameImportance: String? = null,
|
||||
|
||||
// 触发条件参数
|
||||
@Column(name = "min_win_probability_diff", precision = 5, scale = 4)
|
||||
val minWinProbabilityDiff: BigDecimal = BigDecimal("0.1"),
|
||||
|
||||
@Column(name = "min_win_probability", precision = 5, scale = 4)
|
||||
val minWinProbability: BigDecimal? = null,
|
||||
|
||||
@Column(name = "max_win_probability", precision = 5, scale = 4)
|
||||
val maxWinProbability: BigDecimal? = null,
|
||||
|
||||
@Column(name = "min_trade_value", precision = 5, scale = 4)
|
||||
val minTradeValue: BigDecimal = BigDecimal("0.05"),
|
||||
|
||||
@Column(name = "min_remaining_time")
|
||||
val minRemainingTime: Int? = null,
|
||||
|
||||
@Column(name = "max_remaining_time")
|
||||
val maxRemainingTime: Int? = null,
|
||||
|
||||
@Column(name = "min_score_diff")
|
||||
val minScoreDiff: Int? = null,
|
||||
|
||||
@Column(name = "max_score_diff")
|
||||
val maxScoreDiff: Int? = null,
|
||||
|
||||
// 买入规则参数
|
||||
@Column(name = "buy_amount_strategy", length = 20)
|
||||
val buyAmountStrategy: String = "FIXED",
|
||||
|
||||
@Column(name = "fixed_buy_amount", precision = 20, scale = 8)
|
||||
val fixedBuyAmount: BigDecimal? = null,
|
||||
|
||||
@Column(name = "buy_ratio", precision = 5, scale = 4)
|
||||
val buyRatio: BigDecimal? = null,
|
||||
|
||||
@Column(name = "base_buy_amount", precision = 20, scale = 8)
|
||||
val baseBuyAmount: BigDecimal? = null,
|
||||
|
||||
@Column(name = "buy_timing", length = 20)
|
||||
val buyTiming: String = "IMMEDIATE",
|
||||
|
||||
@Column(name = "delay_buy_seconds")
|
||||
val delayBuySeconds: Int = 0,
|
||||
|
||||
@Column(name = "buy_direction", length = 10)
|
||||
val buyDirection: String = "AUTO",
|
||||
|
||||
// 卖出规则参数
|
||||
@Column(name = "enable_sell")
|
||||
val enableSell: Boolean = true,
|
||||
|
||||
@Column(name = "take_profit_threshold", precision = 5, scale = 4)
|
||||
val takeProfitThreshold: BigDecimal? = null,
|
||||
|
||||
@Column(name = "stop_loss_threshold", precision = 5, scale = 4)
|
||||
val stopLossThreshold: BigDecimal? = null,
|
||||
|
||||
@Column(name = "probability_reversal_threshold", precision = 5, scale = 4)
|
||||
val probabilityReversalThreshold: BigDecimal? = null,
|
||||
|
||||
@Column(name = "sell_ratio", precision = 5, scale = 4)
|
||||
val sellRatio: BigDecimal = BigDecimal("1.0"),
|
||||
|
||||
@Column(name = "sell_timing", length = 20)
|
||||
val sellTiming: String = "IMMEDIATE",
|
||||
|
||||
@Column(name = "delay_sell_seconds")
|
||||
val delaySellSeconds: Int = 0,
|
||||
|
||||
// 价格策略参数
|
||||
@Column(name = "price_strategy", length = 20)
|
||||
val priceStrategy: String = "MARKET",
|
||||
|
||||
@Column(name = "fixed_price", precision = 5, scale = 4)
|
||||
val fixedPrice: BigDecimal? = null,
|
||||
|
||||
@Column(name = "price_offset", precision = 5, scale = 4)
|
||||
val priceOffset: BigDecimal = BigDecimal.ZERO,
|
||||
|
||||
// 风险控制参数
|
||||
@Column(name = "max_position", precision = 20, scale = 8)
|
||||
val maxPosition: BigDecimal = BigDecimal("50"),
|
||||
|
||||
@Column(name = "min_position", precision = 20, scale = 8)
|
||||
val minPosition: BigDecimal = BigDecimal("5"),
|
||||
|
||||
@Column(name = "max_game_position", precision = 20, scale = 8)
|
||||
val maxGamePosition: BigDecimal? = null,
|
||||
|
||||
@Column(name = "max_daily_loss", precision = 20, scale = 8)
|
||||
val maxDailyLoss: BigDecimal? = null,
|
||||
|
||||
@Column(name = "max_daily_orders")
|
||||
val maxDailyOrders: Int? = null,
|
||||
|
||||
@Column(name = "max_daily_profit", precision = 20, scale = 8)
|
||||
val maxDailyProfit: BigDecimal? = null,
|
||||
|
||||
@Column(name = "price_tolerance", precision = 5, scale = 4)
|
||||
val priceTolerance: BigDecimal = BigDecimal("0.05"),
|
||||
|
||||
@Column(name = "min_probability_threshold", precision = 5, scale = 4)
|
||||
val minProbabilityThreshold: BigDecimal? = null,
|
||||
|
||||
@Column(name = "max_probability_threshold", precision = 5, scale = 4)
|
||||
val maxProbabilityThreshold: BigDecimal? = null,
|
||||
|
||||
// 算法权重参数
|
||||
@Column(name = "base_strength_weight", precision = 5, scale = 4)
|
||||
val baseStrengthWeight: BigDecimal = BigDecimal("0.3"),
|
||||
|
||||
@Column(name = "recent_form_weight", precision = 5, scale = 4)
|
||||
val recentFormWeight: BigDecimal = BigDecimal("0.25"),
|
||||
|
||||
@Column(name = "lineup_integrity_weight", precision = 5, scale = 4)
|
||||
val lineupIntegrityWeight: BigDecimal = BigDecimal("0.2"),
|
||||
|
||||
@Column(name = "star_status_weight", precision = 5, scale = 4)
|
||||
val starStatusWeight: BigDecimal = BigDecimal("0.15"),
|
||||
|
||||
@Column(name = "environment_weight", precision = 5, scale = 4)
|
||||
val environmentWeight: BigDecimal = BigDecimal("0.1"),
|
||||
|
||||
@Column(name = "matchup_advantage_weight", precision = 5, scale = 4)
|
||||
val matchupAdvantageWeight: BigDecimal = BigDecimal("0.2"),
|
||||
|
||||
@Column(name = "score_diff_weight", precision = 5, scale = 4)
|
||||
val scoreDiffWeight: BigDecimal = BigDecimal("0.3"),
|
||||
|
||||
@Column(name = "momentum_weight", precision = 5, scale = 4)
|
||||
val momentumWeight: BigDecimal = BigDecimal("0.2"),
|
||||
|
||||
// 系统配置参数
|
||||
@Column(name = "data_update_frequency")
|
||||
val dataUpdateFrequency: Int = 30,
|
||||
|
||||
@Column(name = "analysis_frequency")
|
||||
val analysisFrequency: Int = 30,
|
||||
|
||||
@Column(name = "push_failed_orders")
|
||||
val pushFailedOrders: Boolean = false,
|
||||
|
||||
@Column(name = "push_frequency", length = 20)
|
||||
val pushFrequency: String = "REALTIME",
|
||||
|
||||
@Column(name = "batch_push_interval")
|
||||
val batchPushInterval: Int = 1,
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
val createdAt: Long = System.currentTimeMillis(),
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
var updatedAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.wrbug.polymarketbot.entity
|
||||
|
||||
import jakarta.persistence.*
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDate
|
||||
|
||||
/**
|
||||
* NBA 策略执行统计实体
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "nba_strategy_statistics")
|
||||
data class NbaStrategyStatistics(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
val id: Long? = null,
|
||||
|
||||
@Column(name = "strategy_id", nullable = false)
|
||||
val strategyId: Long,
|
||||
|
||||
@Column(name = "stat_date", nullable = false)
|
||||
val statDate: LocalDate,
|
||||
|
||||
@Column(name = "total_signals")
|
||||
val totalSignals: Int = 0,
|
||||
|
||||
@Column(name = "buy_signals")
|
||||
val buySignals: Int = 0,
|
||||
|
||||
@Column(name = "sell_signals")
|
||||
val sellSignals: Int = 0,
|
||||
|
||||
@Column(name = "success_signals")
|
||||
val successSignals: Int = 0,
|
||||
|
||||
@Column(name = "failed_signals")
|
||||
val failedSignals: Int = 0,
|
||||
|
||||
@Column(name = "total_profit", precision = 20, scale = 8)
|
||||
val totalProfit: BigDecimal = BigDecimal.ZERO,
|
||||
|
||||
@Column(name = "total_volume", precision = 20, scale = 8)
|
||||
val totalVolume: BigDecimal = BigDecimal.ZERO,
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
val createdAt: Long = System.currentTimeMillis(),
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
var updatedAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.wrbug.polymarketbot.entity
|
||||
|
||||
import jakarta.persistence.*
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* NBA 交易信号实体
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "nba_trading_signals")
|
||||
data class NbaTradingSignal(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
val id: Long? = null,
|
||||
|
||||
@Column(name = "strategy_id", nullable = false)
|
||||
val strategyId: Long,
|
||||
|
||||
@Column(name = "game_id")
|
||||
val gameId: Long? = null,
|
||||
|
||||
@Column(name = "market_id")
|
||||
val marketId: Long? = null,
|
||||
|
||||
@Column(name = "signal_type", nullable = false, length = 10)
|
||||
val signalType: String,
|
||||
|
||||
@Column(name = "direction", nullable = false, length = 10)
|
||||
val direction: String,
|
||||
|
||||
@Column(name = "price", nullable = false, precision = 5, scale = 4)
|
||||
val price: BigDecimal,
|
||||
|
||||
@Column(name = "quantity", nullable = false, precision = 20, scale = 8)
|
||||
val quantity: BigDecimal,
|
||||
|
||||
@Column(name = "total_amount", nullable = false, precision = 20, scale = 8)
|
||||
val totalAmount: BigDecimal,
|
||||
|
||||
@Column(name = "reason", columnDefinition = "TEXT")
|
||||
val reason: String? = null,
|
||||
|
||||
@Column(name = "win_probability", precision = 5, scale = 4)
|
||||
val winProbability: BigDecimal? = null,
|
||||
|
||||
@Column(name = "trade_value", precision = 5, scale = 4)
|
||||
val tradeValue: BigDecimal? = null,
|
||||
|
||||
@Column(name = "signal_status", length = 20)
|
||||
val signalStatus: String = "GENERATED",
|
||||
|
||||
@Column(name = "execution_result", columnDefinition = "TEXT")
|
||||
val executionResult: String? = null,
|
||||
|
||||
@Column(name = "error_message", columnDefinition = "TEXT")
|
||||
val errorMessage: String? = null,
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
val createdAt: Long = System.currentTimeMillis(),
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
var updatedAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.wrbug.polymarketbot.enums
|
||||
|
||||
/**
|
||||
* Polymarket 体育项目 Tag ID 枚举
|
||||
* 用于标识不同体育项目在 Polymarket 中的 tag ID
|
||||
*/
|
||||
enum class SportsTagId(val tagId: String, val displayName: String) {
|
||||
/**
|
||||
* 美国职业篮球联赛
|
||||
*/
|
||||
NBA("745", "NBA"),
|
||||
|
||||
/**
|
||||
* 美国职业棒球大联盟
|
||||
*/
|
||||
MLB("100381", "MLB"),
|
||||
|
||||
/**
|
||||
* 美国国家橄榄球联盟
|
||||
*/
|
||||
NFL("450", "NFL"),
|
||||
|
||||
/**
|
||||
* 美国大学橄榄球
|
||||
*/
|
||||
CFB("100351", "CFB"),
|
||||
|
||||
/**
|
||||
* 美国国家冰球联盟
|
||||
*/
|
||||
NHL("899", "NHL"),
|
||||
|
||||
/**
|
||||
* 游戏/电子竞技
|
||||
*/
|
||||
GAMES("100639", "GAMES"),
|
||||
|
||||
/**
|
||||
* 美国大学篮球
|
||||
*/
|
||||
CBB("101178", "CBB");
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* 根据 tag ID 查找枚举
|
||||
*/
|
||||
fun fromTagId(tagId: String): SportsTagId? {
|
||||
return values().find { it.tagId == tagId }
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据显示名称查找枚举
|
||||
*/
|
||||
fun fromDisplayName(displayName: String): SportsTagId? {
|
||||
return values().find { it.displayName.equals(displayName, ignoreCase = true) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有 tag IDs 列表
|
||||
*/
|
||||
fun getAllTagIds(): List<String> {
|
||||
return values().map { it.tagId }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.wrbug.polymarketbot.repository
|
||||
|
||||
import com.wrbug.polymarketbot.entity.NbaGame
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.time.LocalDate
|
||||
|
||||
@Repository
|
||||
interface NbaGameRepository : JpaRepository<NbaGame, Long> {
|
||||
fun findByNbaGameId(nbaGameId: String): NbaGame?
|
||||
fun findByGameDate(gameDate: LocalDate): List<NbaGame>
|
||||
fun findByGameDateBetween(startDate: LocalDate, endDate: LocalDate): List<NbaGame>
|
||||
fun findByGameStatus(gameStatus: String): List<NbaGame>
|
||||
fun findByHomeTeamAndAwayTeamAndGameDate(homeTeam: String, awayTeam: String, gameDate: LocalDate): NbaGame?
|
||||
fun findByPolymarketMarketId(polymarketMarketId: String): NbaGame?
|
||||
|
||||
/**
|
||||
* 查询最新的比赛(按创建时间倒序)
|
||||
*/
|
||||
fun findFirstByOrderByCreatedAtDesc(): NbaGame?
|
||||
|
||||
/**
|
||||
* 根据创建时间查询比赛
|
||||
*/
|
||||
fun findByCreatedAtGreaterThan(createdAt: Long): List<NbaGame>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.wrbug.polymarketbot.repository
|
||||
|
||||
import com.wrbug.polymarketbot.entity.NbaMarket
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface NbaMarketRepository : JpaRepository<NbaMarket, Long> {
|
||||
fun findByConditionId(conditionId: String): NbaMarket?
|
||||
fun findByPolymarketMarketId(polymarketMarketId: String): NbaMarket?
|
||||
fun findByActiveAndClosed(active: Boolean, closed: Boolean): List<NbaMarket>
|
||||
fun findByCategory(category: String): List<NbaMarket>
|
||||
fun findByActive(active: Boolean): List<NbaMarket>
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.wrbug.polymarketbot.repository
|
||||
|
||||
import com.wrbug.polymarketbot.entity.NbaQuantitativeStrategy
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface NbaQuantitativeStrategyRepository : JpaRepository<NbaQuantitativeStrategy, Long> {
|
||||
fun findByAccountId(accountId: Long): List<NbaQuantitativeStrategy>
|
||||
fun findByAccountIdAndEnabled(accountId: Long, enabled: Boolean): List<NbaQuantitativeStrategy>
|
||||
fun findByEnabled(enabled: Boolean): List<NbaQuantitativeStrategy>
|
||||
fun findByStrategyName(strategyName: String): NbaQuantitativeStrategy?
|
||||
}
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package com.wrbug.polymarketbot.repository
|
||||
|
||||
import com.wrbug.polymarketbot.entity.NbaStrategyStatistics
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.time.LocalDate
|
||||
|
||||
@Repository
|
||||
interface NbaStrategyStatisticsRepository : JpaRepository<NbaStrategyStatistics, Long> {
|
||||
fun findByStrategyId(strategyId: Long): List<NbaStrategyStatistics>
|
||||
fun findByStrategyIdAndStatDate(strategyId: Long, statDate: LocalDate): NbaStrategyStatistics?
|
||||
fun findByStrategyIdAndStatDateBetween(strategyId: Long, startDate: LocalDate, endDate: LocalDate): List<NbaStrategyStatistics>
|
||||
}
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.wrbug.polymarketbot.repository
|
||||
|
||||
import com.wrbug.polymarketbot.entity.NbaTradingSignal
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.time.Instant
|
||||
|
||||
@Repository
|
||||
interface NbaTradingSignalRepository : JpaRepository<NbaTradingSignal, Long> {
|
||||
fun findByStrategyId(strategyId: Long): List<NbaTradingSignal>
|
||||
fun findByGameId(gameId: Long): List<NbaTradingSignal>
|
||||
fun findByMarketId(marketId: Long): List<NbaTradingSignal>
|
||||
fun findBySignalType(signalType: String): List<NbaTradingSignal>
|
||||
fun findBySignalStatus(signalStatus: String): List<NbaTradingSignal>
|
||||
fun findByStrategyIdAndSignalType(strategyId: Long, signalType: String): List<NbaTradingSignal>
|
||||
fun findByStrategyIdAndCreatedAtBetween(strategyId: Long, startTime: Long, endTime: Long): List<NbaTradingSignal>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,579 @@
|
||||
package com.wrbug.polymarketbot.service.nba
|
||||
|
||||
import com.wrbug.polymarketbot.api.PolymarketGammaApi
|
||||
import com.wrbug.polymarketbot.dto.NbaGameDto
|
||||
import com.wrbug.polymarketbot.dto.NbaGameListRequest
|
||||
import com.wrbug.polymarketbot.dto.NbaGameListResponse
|
||||
import com.wrbug.polymarketbot.entity.NbaGame
|
||||
import com.wrbug.polymarketbot.enums.SportsTagId
|
||||
import com.wrbug.polymarketbot.repository.NbaGameRepository
|
||||
import com.wrbug.polymarketbot.util.RetrofitFactory
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
/**
|
||||
* NBA 比赛服务
|
||||
* 从数据库和 Polymarket API 获取比赛数据
|
||||
* 优先从数据库获取,如果数据不足则增量拉取 API 数据
|
||||
*/
|
||||
@Service
|
||||
class NbaGameService(
|
||||
private val retrofitFactory: RetrofitFactory,
|
||||
private val nbaGameRepository: NbaGameRepository
|
||||
) {
|
||||
private val logger = LoggerFactory.getLogger(NbaGameService::class.java)
|
||||
|
||||
|
||||
/**
|
||||
* 获取 NBA 比赛列表
|
||||
* 优先从数据库获取,如果数据不足则增量拉取 API 数据
|
||||
* 前端传递时间戳,后端转换为西8区时间用于过滤
|
||||
*/
|
||||
suspend fun getNbaGames(request: NbaGameListRequest): Result<NbaGameListResponse> {
|
||||
return try {
|
||||
// 将时间戳转换为西8区(PST/PDT)的日期范围
|
||||
val pstZone = ZoneId.of("America/Los_Angeles")
|
||||
|
||||
val startTimestamp = request.startTimestamp ?: ZonedDateTime.now(pstZone).toInstant().toEpochMilli()
|
||||
val endTimestamp = request.endTimestamp ?: ZonedDateTime.now(pstZone).plusDays(7).toInstant().toEpochMilli()
|
||||
|
||||
val startDate = Instant.ofEpochMilli(startTimestamp).atZone(pstZone).toLocalDate()
|
||||
val endDate = Instant.ofEpochMilli(endTimestamp).atZone(pstZone).toLocalDate()
|
||||
|
||||
// 1. 先从数据库获取数据
|
||||
val dbGames = nbaGameRepository.findByGameDateBetween(startDate, endDate)
|
||||
logger.info("从数据库获取到 ${dbGames.size} 个比赛(日期范围:$startDate 到 $endDate)")
|
||||
|
||||
// 2. 检查是否需要增量拉取
|
||||
val needFetch = shouldFetchFromApi(dbGames)
|
||||
|
||||
if (needFetch) {
|
||||
logger.info("数据库数据不足,开始增量拉取 API 数据")
|
||||
|
||||
// 3. 获取数据库最新的 createdAt,用于增量拉取
|
||||
val latestGame = nbaGameRepository.findFirstByOrderByCreatedAtDesc()
|
||||
val incrementalStartDateMin = latestGame?.createdAt?.let {
|
||||
// 将数据库的 createdAt(时间戳)转换为 UTC ISO 8601 格式
|
||||
Instant.ofEpochMilli(it)
|
||||
.atZone(java.time.ZoneOffset.UTC)
|
||||
.format(DateTimeFormatter.ISO_INSTANT)
|
||||
} ?: run {
|
||||
// 如果没有数据库数据,使用一周前的时间
|
||||
Instant.now()
|
||||
.minusSeconds(7 * 24 * 60 * 60)
|
||||
.atZone(java.time.ZoneOffset.UTC)
|
||||
.format(DateTimeFormatter.ISO_INSTANT)
|
||||
}
|
||||
|
||||
logger.info("使用增量拉取起始时间: $incrementalStartDateMin")
|
||||
|
||||
// 4. 增量拉取 API 数据
|
||||
val apiGames = fetchGamesFromApi(startDate, endDate, incrementalStartDateMin)
|
||||
|
||||
// 5. 保存新数据到数据库
|
||||
if (apiGames.isNotEmpty()) {
|
||||
saveGamesToDatabase(apiGames)
|
||||
}
|
||||
|
||||
// 6. 合并数据库数据和 API 数据
|
||||
val allGames = (dbGames + apiGames.map { dtoToEntity(it) }).distinctBy {
|
||||
"${it.homeTeam}_${it.awayTeam}_${it.gameDate}"
|
||||
}
|
||||
|
||||
// 转换为 DTO
|
||||
val gameDtos = allGames.map { entityToDto(it) }
|
||||
|
||||
// 根据状态过滤
|
||||
val filteredGames = if (request.gameStatus != null) {
|
||||
gameDtos.filter { it.gameStatus == request.gameStatus }
|
||||
} else {
|
||||
gameDtos
|
||||
}
|
||||
|
||||
Result.success(
|
||||
NbaGameListResponse(
|
||||
list = filteredGames,
|
||||
total = filteredGames.size.toLong()
|
||||
)
|
||||
)
|
||||
} else {
|
||||
// 数据库数据充足,直接返回
|
||||
val gameDtos = dbGames.map { entityToDto(it) }
|
||||
|
||||
// 根据状态过滤
|
||||
val filteredGames = if (request.gameStatus != null) {
|
||||
gameDtos.filter { it.gameStatus == request.gameStatus }
|
||||
} else {
|
||||
gameDtos
|
||||
}
|
||||
|
||||
Result.success(
|
||||
NbaGameListResponse(
|
||||
list = filteredGames,
|
||||
total = filteredGames.size.toLong()
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("获取 NBA 比赛列表失败: ${e.message}", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否需要从 API 拉取数据
|
||||
* 逻辑:
|
||||
* 1. 如果数据库没有数据,需要拉取
|
||||
* 2. 如果数据库最新数据的 gameTime 在未来 3 天内(0-3 天),不需要拉取
|
||||
* 3. 如果数据库最新数据的 gameTime 超过 3 天(>3 天),不需要拉取(数据太远)
|
||||
* 4. 如果数据库最新数据的 gameTime 已经过去(<0),需要拉取(数据过期)
|
||||
*/
|
||||
private fun shouldFetchFromApi(dbGames: List<NbaGame>): Boolean {
|
||||
if (dbGames.isEmpty()) {
|
||||
logger.info("数据库没有数据,需要从 API 拉取")
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查最新数据的 gameTime(未来最远的比赛)
|
||||
val latestGame = dbGames.maxByOrNull { it.gameTime ?: 0L }
|
||||
if (latestGame?.gameTime == null) {
|
||||
logger.info("数据库最新数据没有 gameTime,需要从 API 拉取")
|
||||
return true
|
||||
}
|
||||
|
||||
// 计算最新数据的 gameTime 距离现在的时间(以天为单位)
|
||||
val now = Instant.now().toEpochMilli()
|
||||
val gameTime = latestGame.gameTime
|
||||
val daysDiff = (gameTime - now) / (24 * 60 * 60 * 1000)
|
||||
|
||||
// 如果数据已经过去(daysDiff < 0),需要拉取
|
||||
if (daysDiff < 0) {
|
||||
logger.info("数据库最新数据的 gameTime 已经过去(${daysDiff} 天前),需要从 API 拉取")
|
||||
return true
|
||||
}
|
||||
|
||||
// 如果数据在未来 3 天内(0 <= daysDiff <= 3),不需要拉取
|
||||
if (daysDiff >= 0 && daysDiff <= 3) {
|
||||
logger.info("数据库数据充足(最新数据 ${daysDiff} 天后,在未来 3 天内),无需从 API 拉取")
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果数据超过 3 天(daysDiff > 3),不需要拉取(数据太远)
|
||||
logger.info("数据库最新数据的 gameTime 超过 3 天(${daysDiff} 天后),数据太远,不需要从 API 拉取")
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 API 拉取比赛数据
|
||||
*/
|
||||
private suspend fun fetchGamesFromApi(
|
||||
startDate: LocalDate,
|
||||
endDate: LocalDate,
|
||||
startDateMin: String
|
||||
): List<NbaGameDto> {
|
||||
// 从 Polymarket API 获取 NBA 市场(分页拉取)
|
||||
val gammaApi = retrofitFactory.createGammaApi()
|
||||
val nbaTagId = SportsTagId.NBA.tagId
|
||||
|
||||
// 计算未来3天的时间点(UTC)
|
||||
val threeDaysLater = Instant.now()
|
||||
.plusSeconds(3 * 24 * 60 * 60) // 加上3天(秒数)
|
||||
|
||||
val allMarkets = mutableListOf<com.wrbug.polymarketbot.api.MarketResponse>()
|
||||
var hasMore = true
|
||||
var pageCount = 0
|
||||
var currentStartDateMin = startDateMin
|
||||
|
||||
while (hasMore) {
|
||||
pageCount++
|
||||
logger.debug("分页拉取第 $pageCount 页,start_date_min: $currentStartDateMin")
|
||||
|
||||
val response = gammaApi.listMarkets(
|
||||
conditionIds = null,
|
||||
includeTag = true,
|
||||
tags = listOf(nbaTagId),
|
||||
active = true, // 只获取活跃的市场
|
||||
closed = false,
|
||||
archived = false,
|
||||
limit = 500, // 使用 500 作为 limit
|
||||
startDateMin = currentStartDateMin,
|
||||
sportsMarketTypes = listOf("moneyline") // 直接通过 API 筛选 moneyline 类型
|
||||
)
|
||||
|
||||
if (!response.isSuccessful || response.body() == null) {
|
||||
logger.error("获取 NBA 市场失败: ${response.code()} ${response.message()}")
|
||||
break
|
||||
}
|
||||
|
||||
val markets = response.body()!!
|
||||
logger.info("第 $pageCount 页获取到 ${markets.size} 个市场")
|
||||
|
||||
if (markets.isEmpty()) {
|
||||
// 没有更多数据了
|
||||
hasMore = false
|
||||
break
|
||||
}
|
||||
|
||||
// 先记录最后一项的 createdAt(用于下一次分页)
|
||||
val lastMarket = markets.last()
|
||||
val lastCreatedAt = lastMarket.createdAt
|
||||
|
||||
if (lastCreatedAt == null) {
|
||||
// 如果最后一个元素没有 createdAt,停止分页
|
||||
hasMore = false
|
||||
logger.warn("数组最后一个元素缺少 createdAt,停止分页")
|
||||
break
|
||||
}
|
||||
|
||||
// 移除非 NBA 项(根据 resolutionSource 判断)
|
||||
val nbaMarkets = markets.filter { market ->
|
||||
!market.resolutionSource.isNullOrBlank() &&
|
||||
market.resolutionSource!!.lowercase().contains("nba")
|
||||
}
|
||||
logger.info("第 $pageCount 页过滤后剩余 ${nbaMarkets.size} 个 NBA 市场")
|
||||
|
||||
// 添加到总列表(只添加 NBA 市场)
|
||||
allMarkets.addAll(nbaMarkets)
|
||||
|
||||
// 从后往前遍历,找到第一个有 gameStartTime 字段的数据(在 NBA 市场中查找)
|
||||
var foundGameStartTime: String? = null
|
||||
for (i in nbaMarkets.size - 1 downTo 0) {
|
||||
val market = nbaMarkets[i]
|
||||
if (!market.gameStartTime.isNullOrBlank()) {
|
||||
foundGameStartTime = market.gameStartTime
|
||||
logger.debug("从后往前找到第 ${i + 1} 个有 gameStartTime 的 NBA 市场: $foundGameStartTime")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (foundGameStartTime == null) {
|
||||
// 如果整页都没有 gameStartTime,使用 createdAt 继续分页
|
||||
currentStartDateMin = lastCreatedAt
|
||||
logger.debug("本页没有找到 gameStartTime,使用最后一个元素的 createdAt 继续分页")
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析 gameStartTime(格式:2025-12-13 00:00:00+00)
|
||||
val gameStartDate = try {
|
||||
// 尝试解析格式 "2025-12-13 00:00:00+00"
|
||||
val dateTimeStr = foundGameStartTime.replace(" ", "T")
|
||||
// 如果时区是 +00,转换为 Z
|
||||
val normalizedStr = if (dateTimeStr.endsWith("+00")) {
|
||||
dateTimeStr.replace("+00", "Z")
|
||||
} else if (dateTimeStr.endsWith("-00")) {
|
||||
dateTimeStr.replace("-00", "Z")
|
||||
} else {
|
||||
dateTimeStr
|
||||
}
|
||||
val instant = Instant.parse(normalizedStr)
|
||||
// 转换为日期(以天为单位,不考虑时间)
|
||||
instant.atZone(java.time.ZoneOffset.UTC).toLocalDate()
|
||||
} catch (e: Exception) {
|
||||
logger.warn("解析 gameStartTime 失败: $foundGameStartTime, error: ${e.message}")
|
||||
null
|
||||
}
|
||||
|
||||
if (gameStartDate == null) {
|
||||
// 无法解析 gameStartTime,使用 createdAt 继续分页
|
||||
currentStartDateMin = lastCreatedAt
|
||||
logger.debug("无法解析 gameStartTime,使用最后一个元素的 createdAt 继续分页")
|
||||
continue
|
||||
}
|
||||
|
||||
// 计算未来 3 天的日期(以天为单位,不考虑时间)
|
||||
val threeDaysLaterDate = Instant.now()
|
||||
.plusSeconds(3 * 24 * 60 * 60) // 加上3天(秒数)
|
||||
.atZone(java.time.ZoneOffset.UTC)
|
||||
.toLocalDate()
|
||||
|
||||
// 判断 gameStartDate 是否在未来 3 天以内(包括第 3 天)
|
||||
val daysBetween = java.time.temporal.ChronoUnit.DAYS.between(
|
||||
Instant.now().atZone(java.time.ZoneOffset.UTC).toLocalDate(),
|
||||
gameStartDate
|
||||
)
|
||||
|
||||
if (daysBetween <= 3 && daysBetween >= 0) {
|
||||
// 如果在 3 天内(包括第 3 天),使用数组最后一个元素的 createdAt 继续分页
|
||||
currentStartDateMin = lastCreatedAt
|
||||
logger.info("找到的 gameStartTime ($foundGameStartTime, 日期: $gameStartDate) 在未来 ${daysBetween} 天内,继续分页")
|
||||
} else {
|
||||
// 如果不在 3 天内,停止分页
|
||||
hasMore = false
|
||||
logger.info("找到的 gameStartTime ($foundGameStartTime, 日期: $gameStartDate) 不在未来 3 天内(相差 ${daysBetween} 天),停止分页")
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("分页拉取完成,共获取 ${allMarkets.size} 个 NBA moneyline 市场(${pageCount} 页)")
|
||||
|
||||
// 注意:allMarkets 已经通过 API 的 sports_market_types 参数过滤了 moneyline 类型
|
||||
// 并且已经过滤了非 NBA 项(根据 resolutionSource),这里直接使用即可
|
||||
|
||||
// 将市场转换为比赛数据
|
||||
val games = allMarkets.mapNotNull { market ->
|
||||
convertMarketToGame(market, startDate, endDate)
|
||||
}
|
||||
|
||||
// 去重:相同的主队、客队和日期只保留一个
|
||||
val uniqueGames = games.groupBy { "${it.homeTeam}_${it.awayTeam}_${it.gameDate}" }
|
||||
.map { it.value.first() }
|
||||
|
||||
return uniqueGames
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存比赛数据到数据库
|
||||
*/
|
||||
@Transactional
|
||||
private fun saveGamesToDatabase(games: List<NbaGameDto>) {
|
||||
if (games.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
var savedCount = 0
|
||||
var updatedCount = 0
|
||||
|
||||
games.forEach { dto ->
|
||||
try {
|
||||
// 尝试根据 nbaGameId 或 polymarketMarketId 查找现有记录
|
||||
val existing = dto.nbaGameId?.let {
|
||||
nbaGameRepository.findByNbaGameId(it)
|
||||
} ?: dto.polymarketMarketId?.let {
|
||||
nbaGameRepository.findByPolymarketMarketId(it)
|
||||
}
|
||||
|
||||
if (existing != null) {
|
||||
// 更新现有记录(data class 的 copy 方法)
|
||||
val updated = NbaGame(
|
||||
id = existing.id,
|
||||
nbaGameId = existing.nbaGameId,
|
||||
homeTeam = dto.homeTeam,
|
||||
awayTeam = dto.awayTeam,
|
||||
gameDate = dto.gameDate,
|
||||
gameTime = dto.gameTime,
|
||||
gameStatus = dto.gameStatus,
|
||||
homeScore = dto.homeScore,
|
||||
awayScore = dto.awayScore,
|
||||
period = dto.period,
|
||||
timeRemaining = dto.timeRemaining,
|
||||
polymarketMarketId = dto.polymarketMarketId,
|
||||
createdAt = existing.createdAt,
|
||||
updatedAt = System.currentTimeMillis()
|
||||
)
|
||||
nbaGameRepository.save(updated)
|
||||
updatedCount++
|
||||
} else {
|
||||
// 创建新记录
|
||||
val entity = dtoToEntity(dto)
|
||||
nbaGameRepository.save(entity)
|
||||
savedCount++
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("保存比赛数据失败: ${dto.nbaGameId}, error: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("保存比赛数据完成:新增 $savedCount 条,更新 $updatedCount 条")
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO 转实体
|
||||
*/
|
||||
private fun dtoToEntity(dto: NbaGameDto): NbaGame {
|
||||
return NbaGame(
|
||||
id = null,
|
||||
nbaGameId = dto.nbaGameId,
|
||||
homeTeam = dto.homeTeam,
|
||||
awayTeam = dto.awayTeam,
|
||||
gameDate = dto.gameDate,
|
||||
gameTime = dto.gameTime,
|
||||
gameStatus = dto.gameStatus,
|
||||
homeScore = dto.homeScore,
|
||||
awayScore = dto.awayScore,
|
||||
period = dto.period,
|
||||
timeRemaining = dto.timeRemaining,
|
||||
polymarketMarketId = dto.polymarketMarketId,
|
||||
createdAt = System.currentTimeMillis(),
|
||||
updatedAt = System.currentTimeMillis()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体转 DTO
|
||||
*/
|
||||
private fun entityToDto(entity: NbaGame): NbaGameDto {
|
||||
return NbaGameDto(
|
||||
id = entity.id,
|
||||
nbaGameId = entity.nbaGameId,
|
||||
homeTeam = entity.homeTeam,
|
||||
awayTeam = entity.awayTeam,
|
||||
gameDate = entity.gameDate,
|
||||
gameTime = entity.gameTime,
|
||||
gameStatus = entity.gameStatus,
|
||||
homeScore = entity.homeScore,
|
||||
awayScore = entity.awayScore,
|
||||
period = entity.period,
|
||||
timeRemaining = entity.timeRemaining,
|
||||
polymarketMarketId = entity.polymarketMarketId
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 7 天内的所有球队(去重)
|
||||
*/
|
||||
suspend fun getTeamsInNext7Days(): Result<List<String>> {
|
||||
return try {
|
||||
// 使用当前西8区时间计算7天范围
|
||||
val pstZone = ZoneId.of("America/Los_Angeles")
|
||||
val now = ZonedDateTime.now(pstZone)
|
||||
val startTimestamp = now.toInstant().toEpochMilli()
|
||||
val endTimestamp = now.plusDays(7).toInstant().toEpochMilli()
|
||||
|
||||
val gamesResult = getNbaGames(
|
||||
NbaGameListRequest(
|
||||
startTimestamp = startTimestamp,
|
||||
endTimestamp = endTimestamp
|
||||
)
|
||||
)
|
||||
|
||||
gamesResult.fold(
|
||||
onSuccess = { response ->
|
||||
val teams = mutableSetOf<String>()
|
||||
response.list.forEach { game ->
|
||||
teams.add(game.homeTeam)
|
||||
teams.add(game.awayTeam)
|
||||
}
|
||||
Result.success(teams.sorted())
|
||||
},
|
||||
onFailure = { exception -> Result.failure(exception) }
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("获取球队列表失败: ${e.message}", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Polymarket 市场转换为比赛数据
|
||||
*/
|
||||
private fun convertMarketToGame(
|
||||
market: com.wrbug.polymarketbot.api.MarketResponse,
|
||||
startDate: LocalDate,
|
||||
endDate: LocalDate
|
||||
): NbaGameDto? {
|
||||
if (market.question.isNullOrBlank()) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 解析市场名称,提取球队和日期信息
|
||||
val parsed = NbaMarketNameParser.parse(market.question)
|
||||
|
||||
if (parsed.homeTeam == null || parsed.awayTeam == null) {
|
||||
// 无法解析出两个球队,跳过
|
||||
return null
|
||||
}
|
||||
|
||||
// 确定比赛日期
|
||||
val gameDate = parsed.gameDate ?: run {
|
||||
// 如果没有解析出日期,尝试从 startDate 或 endDate 中提取
|
||||
parseDateFromMarketDates(market.startDate, market.endDate) ?: return null
|
||||
}
|
||||
|
||||
// 检查日期是否在请求范围内
|
||||
if (gameDate.isBefore(startDate) || gameDate.isAfter(endDate)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 解析比赛时间(从 endDate 或 startDate 中提取,转换为西8区时间戳)
|
||||
val gameTime = parseGameTimeFromMarket(market.startDate, market.endDate, gameDate)
|
||||
|
||||
// 确定比赛状态
|
||||
val gameStatus = when {
|
||||
market.closed == true -> "finished"
|
||||
market.archived == true -> "finished"
|
||||
market.active == true -> "scheduled"
|
||||
else -> "scheduled"
|
||||
}
|
||||
|
||||
return NbaGameDto(
|
||||
id = null,
|
||||
nbaGameId = market.conditionId ?: market.id, // 使用 conditionId 或 id 作为 gameId
|
||||
homeTeam = parsed.homeTeam,
|
||||
awayTeam = parsed.awayTeam,
|
||||
gameDate = gameDate,
|
||||
gameTime = gameTime, // 西8区时间戳(毫秒)
|
||||
gameStatus = gameStatus,
|
||||
homeScore = 0, // Polymarket 不提供比分
|
||||
awayScore = 0,
|
||||
period = 0,
|
||||
timeRemaining = null,
|
||||
polymarketMarketId = market.id
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从市场的 startDate 或 endDate 中解析日期
|
||||
*/
|
||||
private fun parseDateFromMarketDates(startDate: String?, endDate: String?): LocalDate? {
|
||||
val dateStr = endDate ?: startDate ?: return null
|
||||
|
||||
return try {
|
||||
// 尝试解析 ISO 8601 格式
|
||||
if (dateStr.contains("T")) {
|
||||
val instant = Instant.parse(dateStr)
|
||||
val pstZone = ZoneId.of("America/Los_Angeles")
|
||||
instant.atZone(pstZone).toLocalDate()
|
||||
} else {
|
||||
// 尝试解析日期字符串
|
||||
LocalDate.parse(dateStr, DateTimeFormatter.ISO_DATE)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.debug("解析市场日期失败: $dateStr, error: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从市场的日期时间中解析比赛时间,转换为西8区时间戳
|
||||
*/
|
||||
private fun parseGameTimeFromMarket(
|
||||
startDate: String?,
|
||||
endDate: String?,
|
||||
gameDate: LocalDate
|
||||
): Long? {
|
||||
val dateTimeStr = endDate ?: startDate ?: return null
|
||||
|
||||
return try {
|
||||
val pstZone = ZoneId.of("America/Los_Angeles")
|
||||
|
||||
// 尝试解析 ISO 8601 格式
|
||||
val instant = if (dateTimeStr.contains("T")) {
|
||||
Instant.parse(dateTimeStr)
|
||||
} else {
|
||||
// 如果没有时间部分,使用默认时间(晚上8点)
|
||||
val defaultTime = gameDate.atTime(20, 0)
|
||||
defaultTime.atZone(pstZone).toInstant()
|
||||
}
|
||||
|
||||
// 转换为西8区时间戳
|
||||
instant.atZone(pstZone).toInstant().toEpochMilli()
|
||||
} catch (e: Exception) {
|
||||
logger.debug("解析比赛时间失败: $dateTimeStr, error: ${e.message}")
|
||||
// 解析失败时,使用默认时间(晚上8点 PST)
|
||||
try {
|
||||
val defaultTime = gameDate.atTime(20, 0)
|
||||
val pstZone = ZoneId.of("America/Los_Angeles")
|
||||
defaultTime.atZone(pstZone).toInstant().toEpochMilli()
|
||||
} catch (e2: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
package com.wrbug.polymarketbot.service.nba
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.regex.Pattern
|
||||
|
||||
/**
|
||||
* NBA 市场名称解析器
|
||||
* 从 Polymarket 市场名称中提取球队和日期信息
|
||||
*/
|
||||
object NbaMarketNameParser {
|
||||
private val logger = LoggerFactory.getLogger(NbaMarketNameParser::class.java)
|
||||
|
||||
// NBA 球队名称映射(支持多种格式)
|
||||
private val teamNameMapping = mapOf(
|
||||
// 完整名称
|
||||
"atlanta hawks" to "Atlanta Hawks",
|
||||
"boston celtics" to "Boston Celtics",
|
||||
"brooklyn nets" to "Brooklyn Nets",
|
||||
"charlotte hornets" to "Charlotte Hornets",
|
||||
"chicago bulls" to "Chicago Bulls",
|
||||
"cleveland cavaliers" to "Cleveland Cavaliers",
|
||||
"dallas mavericks" to "Dallas Mavericks",
|
||||
"denver nuggets" to "Denver Nuggets",
|
||||
"detroit pistons" to "Detroit Pistons",
|
||||
"golden state warriors" to "Golden State Warriors",
|
||||
"houston rockets" to "Houston Rockets",
|
||||
"indiana pacers" to "Indiana Pacers",
|
||||
"la clippers" to "LA Clippers",
|
||||
"los angeles lakers" to "Los Angeles Lakers",
|
||||
"memphis grizzlies" to "Memphis Grizzlies",
|
||||
"miami heat" to "Miami Heat",
|
||||
"milwaukee bucks" to "Milwaukee Bucks",
|
||||
"minnesota timberwolves" to "Minnesota Timberwolves",
|
||||
"new orleans pelicans" to "New Orleans Pelicans",
|
||||
"new york knicks" to "New York Knicks",
|
||||
"oklahoma city thunder" to "Oklahoma City Thunder",
|
||||
"orlando magic" to "Orlando Magic",
|
||||
"philadelphia 76ers" to "Philadelphia 76ers",
|
||||
"phoenix suns" to "Phoenix Suns",
|
||||
"portland trail blazers" to "Portland Trail Blazers",
|
||||
"sacramento kings" to "Sacramento Kings",
|
||||
"san antonio spurs" to "San Antonio Spurs",
|
||||
"toronto raptors" to "Toronto Raptors",
|
||||
"utah jazz" to "Utah Jazz",
|
||||
"washington wizards" to "Washington Wizards",
|
||||
// 常见缩写和别名
|
||||
"hawks" to "Atlanta Hawks",
|
||||
"celtics" to "Boston Celtics",
|
||||
"nets" to "Brooklyn Nets",
|
||||
"hornets" to "Charlotte Hornets",
|
||||
"bulls" to "Chicago Bulls",
|
||||
"cavaliers" to "Cleveland Cavaliers",
|
||||
"cavs" to "Cleveland Cavaliers",
|
||||
"mavericks" to "Dallas Mavericks",
|
||||
"mavs" to "Dallas Mavericks",
|
||||
"nuggets" to "Denver Nuggets",
|
||||
"pistons" to "Detroit Pistons",
|
||||
"warriors" to "Golden State Warriors",
|
||||
"rockets" to "Houston Rockets",
|
||||
"pacers" to "Indiana Pacers",
|
||||
"clippers" to "LA Clippers",
|
||||
"lakers" to "Los Angeles Lakers",
|
||||
"grizzlies" to "Memphis Grizzlies",
|
||||
"heat" to "Miami Heat",
|
||||
"bucks" to "Milwaukee Bucks",
|
||||
"timberwolves" to "Minnesota Timberwolves",
|
||||
"wolves" to "Minnesota Timberwolves",
|
||||
"pelicans" to "New Orleans Pelicans",
|
||||
"knicks" to "New York Knicks",
|
||||
"thunder" to "Oklahoma City Thunder",
|
||||
"magic" to "Orlando Magic",
|
||||
"76ers" to "Philadelphia 76ers",
|
||||
"sixers" to "Philadelphia 76ers",
|
||||
"suns" to "Phoenix Suns",
|
||||
"trail blazers" to "Portland Trail Blazers",
|
||||
"blazers" to "Portland Trail Blazers",
|
||||
"kings" to "Sacramento Kings",
|
||||
"spurs" to "San Antonio Spurs",
|
||||
"raptors" to "Toronto Raptors",
|
||||
"jazz" to "Utah Jazz",
|
||||
"wizards" to "Washington Wizards",
|
||||
"wiz" to "Washington Wizards"
|
||||
)
|
||||
|
||||
/**
|
||||
* 解析结果
|
||||
*/
|
||||
data class ParsedMarketInfo(
|
||||
val homeTeam: String?,
|
||||
val awayTeam: String?,
|
||||
val gameDate: LocalDate?,
|
||||
val confidence: Double // 置信度 0.0-1.0
|
||||
)
|
||||
|
||||
/**
|
||||
* 解析市场名称
|
||||
* @param marketName 市场名称
|
||||
* @return 解析结果
|
||||
*/
|
||||
fun parse(marketName: String?): ParsedMarketInfo {
|
||||
if (marketName.isNullOrBlank()) {
|
||||
return ParsedMarketInfo(null, null, null, 0.0)
|
||||
}
|
||||
|
||||
val normalized = marketName.lowercase()
|
||||
var homeTeam: String? = null
|
||||
var awayTeam: String? = null
|
||||
var gameDate: LocalDate? = null
|
||||
var confidence = 0.0
|
||||
|
||||
// 尝试提取球队名称
|
||||
val teams = extractTeams(normalized)
|
||||
if (teams.size >= 2) {
|
||||
// 通常第一个是客队,第二个是主队
|
||||
awayTeam = teams[0]
|
||||
homeTeam = teams[1]
|
||||
confidence += 0.5
|
||||
} else if (teams.size == 1) {
|
||||
// 只有一个球队,无法确定主客场
|
||||
awayTeam = teams[0]
|
||||
confidence += 0.2
|
||||
}
|
||||
|
||||
// 尝试提取日期
|
||||
val date = extractDate(normalized)
|
||||
if (date != null) {
|
||||
gameDate = date
|
||||
confidence += 0.3
|
||||
}
|
||||
|
||||
return ParsedMarketInfo(homeTeam, awayTeam, gameDate, confidence.coerceAtMost(1.0))
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取球队名称
|
||||
*/
|
||||
private fun extractTeams(text: String): List<String> {
|
||||
val teams = mutableListOf<String>()
|
||||
|
||||
// 常见的球队名称模式
|
||||
val patterns = listOf(
|
||||
// "Team1 vs Team2" 或 "Team1 @ Team2"
|
||||
Pattern.compile("(\\w+(?:\\s+\\w+)*?)\\s+(?:vs|@|v\\.?|versus)\\s+(\\w+(?:\\s+\\w+)*?)", Pattern.CASE_INSENSITIVE),
|
||||
// "Will Team1 beat Team2"
|
||||
Pattern.compile("will\\s+(\\w+(?:\\s+\\w+)*?)\\s+beat\\s+(\\w+(?:\\s+\\w+)*?)", Pattern.CASE_INSENSITIVE),
|
||||
// "Team1 win" 或 "Team1 wins"
|
||||
Pattern.compile("(\\w+(?:\\s+\\w+)*?)\\s+win", Pattern.CASE_INSENSITIVE)
|
||||
)
|
||||
|
||||
for (pattern in patterns) {
|
||||
val matcher = pattern.matcher(text)
|
||||
if (matcher.find()) {
|
||||
val team1 = normalizeTeamName(matcher.group(1) ?: "")
|
||||
val team2 = if (matcher.groupCount() >= 2) {
|
||||
normalizeTeamName(matcher.group(2) ?: "")
|
||||
} else null
|
||||
|
||||
if (team1 != null) {
|
||||
teams.add(team1)
|
||||
}
|
||||
if (team2 != null) {
|
||||
teams.add(team2)
|
||||
}
|
||||
|
||||
if (teams.size >= 2) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果模式匹配失败,尝试直接查找球队名称
|
||||
if (teams.isEmpty()) {
|
||||
for ((key, value) in teamNameMapping) {
|
||||
if (text.contains(key, ignoreCase = true)) {
|
||||
if (!teams.contains(value)) {
|
||||
teams.add(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return teams.distinct()
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化球队名称
|
||||
*/
|
||||
private fun normalizeTeamName(name: String): String? {
|
||||
val normalized = name.trim().lowercase()
|
||||
return teamNameMapping[normalized] ?: teamNameMapping.entries.firstOrNull {
|
||||
normalized.contains(it.key, ignoreCase = true)
|
||||
}?.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取日期
|
||||
*/
|
||||
private fun extractDate(text: String): LocalDate? {
|
||||
// 尝试多种日期格式
|
||||
try {
|
||||
// 格式1: "Dec 15, 2024" 或 "December 15, 2024"
|
||||
val pattern1 = Pattern.compile("(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\\s+(\\d{1,2}),?\\s+(\\d{4})", Pattern.CASE_INSENSITIVE)
|
||||
val matcher1 = pattern1.matcher(text)
|
||||
if (matcher1.find()) {
|
||||
val monthStr = matcher1.group(1)?.lowercase() ?: return null
|
||||
val day = matcher1.group(2)?.toIntOrNull() ?: return null
|
||||
val year = matcher1.group(3)?.toIntOrNull() ?: return null
|
||||
|
||||
val monthMap = mapOf(
|
||||
"jan" to 1, "january" to 1,
|
||||
"feb" to 2, "february" to 2,
|
||||
"mar" to 3, "march" to 3,
|
||||
"apr" to 4, "april" to 4,
|
||||
"may" to 5,
|
||||
"jun" to 6, "june" to 6,
|
||||
"jul" to 7, "july" to 7,
|
||||
"aug" to 8, "august" to 8,
|
||||
"sep" to 9, "september" to 9,
|
||||
"oct" to 10, "october" to 10,
|
||||
"nov" to 11, "november" to 11,
|
||||
"dec" to 12, "december" to 12
|
||||
)
|
||||
|
||||
val month = monthMap.entries.firstOrNull { monthStr.startsWith(it.key) }?.value
|
||||
if (month != null) {
|
||||
return try {
|
||||
LocalDate.of(year, month, day)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 格式2: "2024-12-15"
|
||||
val pattern2 = Pattern.compile("(\\d{4})[-/](\\d{1,2})[-/](\\d{1,2})")
|
||||
val matcher2 = pattern2.matcher(text)
|
||||
if (matcher2.find()) {
|
||||
val year = matcher2.group(1)?.toIntOrNull() ?: return null
|
||||
val month = matcher2.group(2)?.toIntOrNull() ?: return null
|
||||
val day = matcher2.group(3)?.toIntOrNull() ?: return null
|
||||
return try {
|
||||
LocalDate.of(year, month, day)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// 格式3: "12/15/2024" 或 "12/15/24"
|
||||
val pattern3 = Pattern.compile("(\\d{1,2})/(\\d{1,2})/(\\d{2,4})")
|
||||
val matcher3 = pattern3.matcher(text)
|
||||
if (matcher3.find()) {
|
||||
val month = matcher3.group(1)?.toIntOrNull() ?: return null
|
||||
val day = matcher3.group(2)?.toIntOrNull() ?: return null
|
||||
val yearStr = matcher3.group(3) ?: return null
|
||||
val year = if (yearStr.length == 2) {
|
||||
// 两位年份,假设是 2000-2099
|
||||
val y = yearStr.toIntOrNull() ?: return null
|
||||
if (y < 50) 2000 + y else 1900 + y
|
||||
} else {
|
||||
yearStr.toIntOrNull() ?: return null
|
||||
}
|
||||
return try {
|
||||
LocalDate.of(year, month, day)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.debug("解析日期失败: ${e.message}")
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 匹配比赛和市场
|
||||
* @param homeTeam 主队名称
|
||||
* @param awayTeam 客队名称
|
||||
* @param gameDate 比赛日期
|
||||
* @param parsedMarket 解析的市场信息
|
||||
* @return 是否匹配
|
||||
*/
|
||||
fun matchGame(
|
||||
homeTeam: String,
|
||||
awayTeam: String,
|
||||
gameDate: LocalDate,
|
||||
parsedMarket: ParsedMarketInfo
|
||||
): Boolean {
|
||||
// 检查日期是否匹配(允许1天误差)
|
||||
val dateMatch = parsedMarket.gameDate?.let { marketDate ->
|
||||
val daysDiff = kotlin.math.abs(java.time.temporal.ChronoUnit.DAYS.between(gameDate, marketDate))
|
||||
daysDiff <= 1
|
||||
} ?: false
|
||||
|
||||
if (!dateMatch && parsedMarket.gameDate != null) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查球队是否匹配
|
||||
val homeMatch = parsedMarket.homeTeam?.let {
|
||||
normalizeTeamName(it)?.equals(normalizeTeamName(homeTeam), ignoreCase = true)
|
||||
} ?: false
|
||||
|
||||
val awayMatch = parsedMarket.awayTeam?.let {
|
||||
normalizeTeamName(it)?.equals(normalizeTeamName(awayTeam), ignoreCase = true)
|
||||
} ?: false
|
||||
|
||||
// 如果两个球队都匹配,或者至少一个匹配且日期匹配
|
||||
return (homeMatch && awayMatch) || ((homeMatch || awayMatch) && dateMatch)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
package com.wrbug.polymarketbot.service.nba
|
||||
|
||||
import com.wrbug.polymarketbot.api.MarketResponse
|
||||
import com.wrbug.polymarketbot.api.PolymarketGammaApi
|
||||
import com.wrbug.polymarketbot.dto.NbaMarketDto
|
||||
import com.wrbug.polymarketbot.dto.NbaMarketListRequest
|
||||
import com.wrbug.polymarketbot.dto.NbaMarketListResponse
|
||||
import com.wrbug.polymarketbot.enums.SportsTagId
|
||||
import com.wrbug.polymarketbot.util.RetrofitFactory
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
/**
|
||||
* NBA 市场服务
|
||||
* 用于从 Polymarket 获取 NBA 相关的市场信息
|
||||
*/
|
||||
@Service
|
||||
class NbaMarketService(
|
||||
private val retrofitFactory: RetrofitFactory
|
||||
) {
|
||||
private val logger = LoggerFactory.getLogger(NbaMarketService::class.java)
|
||||
|
||||
/**
|
||||
* 获取 NBA 的 tag ID 列表
|
||||
* 直接使用枚举中定义的已知 tag ID,无需调用 API
|
||||
*/
|
||||
suspend fun getNbaTagIds(): Result<List<String>> {
|
||||
// 直接使用枚举中定义的 NBA tag ID
|
||||
val nbaTagId = SportsTagId.NBA.tagId
|
||||
logger.debug("使用枚举中的 NBA tag ID: $nbaTagId")
|
||||
return Result.success(listOf(nbaTagId))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 NBA 市场列表
|
||||
* 使用 NBA 的 tag IDs 过滤市场
|
||||
*
|
||||
* @param request 请求参数
|
||||
* @return NBA 市场列表响应
|
||||
*/
|
||||
suspend fun getNbaMarkets(request: NbaMarketListRequest): Result<NbaMarketListResponse> {
|
||||
return try {
|
||||
// 先获取 NBA 的 tag IDs
|
||||
val tagIdsResult = getNbaTagIds()
|
||||
if (tagIdsResult.isFailure) {
|
||||
return Result.failure(tagIdsResult.exceptionOrNull() ?: Exception("无法获取 NBA tag IDs"))
|
||||
}
|
||||
|
||||
val tagIds = tagIdsResult.getOrNull() ?: return Result.failure(IllegalStateException("NBA tag IDs 为空"))
|
||||
|
||||
if (tagIds.isEmpty()) {
|
||||
logger.warn("NBA tag IDs 为空,无法过滤市场")
|
||||
return Result.success(NbaMarketListResponse(
|
||||
list = emptyList(),
|
||||
total = 0L
|
||||
))
|
||||
}
|
||||
|
||||
// 调用 /markets 接口,使用 tag IDs 过滤
|
||||
val gammaApi = retrofitFactory.createGammaApi()
|
||||
val response = gammaApi.listMarkets(
|
||||
conditionIds = null,
|
||||
includeTag = true,
|
||||
tags = tagIds,
|
||||
active = request.active,
|
||||
closed = request.closed,
|
||||
archived = request.archived
|
||||
)
|
||||
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
val markets = response.body()!!
|
||||
logger.info("获取到 ${markets.size} 个 NBA 市场")
|
||||
|
||||
// 转换为 DTO
|
||||
val marketDtos = markets.map { market ->
|
||||
NbaMarketDto(
|
||||
id = market.id,
|
||||
question = market.question,
|
||||
conditionId = market.conditionId,
|
||||
slug = market.slug,
|
||||
description = market.description,
|
||||
category = market.category,
|
||||
active = market.active,
|
||||
closed = market.closed,
|
||||
archived = market.archived,
|
||||
volume = market.volume,
|
||||
liquidity = market.liquidity,
|
||||
endDate = market.endDate,
|
||||
startDate = market.startDate,
|
||||
outcomes = market.outcomes,
|
||||
outcomePrices = market.outcomePrices,
|
||||
volumeNum = market.volumeNum,
|
||||
liquidityNum = market.liquidityNum,
|
||||
lastTradePrice = market.lastTradePrice,
|
||||
bestBid = market.bestBid,
|
||||
bestAsk = market.bestAsk
|
||||
)
|
||||
}
|
||||
|
||||
Result.success(NbaMarketListResponse(
|
||||
list = marketDtos,
|
||||
total = marketDtos.size.toLong()
|
||||
))
|
||||
} else {
|
||||
logger.error("获取 NBA 市场失败: ${response.code()} ${response.message()}")
|
||||
val errorBody = response.errorBody()?.string()
|
||||
logger.error("错误响应体: $errorBody")
|
||||
Result.failure(Exception("获取 NBA 市场失败: ${response.code()} ${response.message()}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("获取 NBA 市场异常: ${e.message}", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 NBA 市场中提取球队列表
|
||||
* 解析市场名称,提取所有唯一的球队名称
|
||||
*
|
||||
* @param active 是否只从活跃市场提取(默认 true)
|
||||
* @return 球队名称列表(去重、排序)
|
||||
*/
|
||||
suspend fun getTeamsFromMarkets(active: Boolean = true): Result<List<String>> {
|
||||
return try {
|
||||
// 获取 NBA 市场列表
|
||||
val marketsResult = getNbaMarkets(
|
||||
NbaMarketListRequest(
|
||||
active = active,
|
||||
closed = false,
|
||||
archived = false
|
||||
)
|
||||
)
|
||||
|
||||
if (marketsResult.isFailure) {
|
||||
return Result.failure(marketsResult.exceptionOrNull() ?: Exception("无法获取 NBA 市场"))
|
||||
}
|
||||
|
||||
val markets = marketsResult.getOrNull()?.list ?: return Result.success(emptyList())
|
||||
|
||||
// 使用市场名称解析器提取球队
|
||||
val teams = mutableSetOf<String>()
|
||||
|
||||
markets.forEach { market ->
|
||||
if (!market.question.isNullOrBlank()) {
|
||||
val parsed = NbaMarketNameParser.parse(market.question)
|
||||
if (parsed != null) {
|
||||
// 提取主队和客队
|
||||
parsed.homeTeam?.let { teams.add(it) }
|
||||
parsed.awayTeam?.let { teams.add(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 排序并返回
|
||||
val sortedTeams = teams.sorted()
|
||||
logger.info("从 ${markets.size} 个市场中提取到 ${sortedTeams.size} 个球队")
|
||||
Result.success(sortedTeams)
|
||||
} catch (e: Exception) {
|
||||
logger.error("从市场提取球队列表失败: ${e.message}", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+346
@@ -0,0 +1,346 @@
|
||||
package com.wrbug.polymarketbot.service.nba
|
||||
|
||||
import com.wrbug.polymarketbot.dto.*
|
||||
import com.wrbug.polymarketbot.entity.NbaQuantitativeStrategy
|
||||
import com.wrbug.polymarketbot.repository.AccountRepository
|
||||
import com.wrbug.polymarketbot.repository.NbaQuantitativeStrategyRepository
|
||||
import com.wrbug.polymarketbot.util.JsonUtils
|
||||
import com.wrbug.polymarketbot.util.toSafeBigDecimal
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import org.springframework.data.domain.Sort
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* NBA 量化策略服务
|
||||
*/
|
||||
@Service
|
||||
class NbaQuantitativeStrategyService(
|
||||
private val strategyRepository: NbaQuantitativeStrategyRepository,
|
||||
private val accountRepository: AccountRepository
|
||||
) {
|
||||
private val logger = LoggerFactory.getLogger(NbaQuantitativeStrategyService::class.java)
|
||||
|
||||
/**
|
||||
* 创建策略
|
||||
*/
|
||||
@Transactional
|
||||
suspend fun createStrategy(request: NbaQuantitativeStrategyCreateRequest): Result<NbaQuantitativeStrategyDto> {
|
||||
return try {
|
||||
// 验证账户是否存在
|
||||
val account = accountRepository.findById(request.accountId).orElse(null)
|
||||
if (account == null) {
|
||||
return Result.failure(IllegalArgumentException("账户不存在"))
|
||||
}
|
||||
|
||||
// 验证策略名称是否重复
|
||||
val existing = strategyRepository.findByStrategyName(request.strategyName)
|
||||
if (existing != null) {
|
||||
return Result.failure(IllegalArgumentException("策略名称已存在"))
|
||||
}
|
||||
|
||||
// 创建策略实体
|
||||
val strategy = NbaQuantitativeStrategy(
|
||||
strategyName = request.strategyName,
|
||||
strategyDescription = request.strategyDescription,
|
||||
accountId = request.accountId,
|
||||
enabled = request.enabled,
|
||||
filterTeams = request.filterTeams?.let { JsonUtils.toJson(it) },
|
||||
filterDateFrom = request.filterDateFrom,
|
||||
filterDateTo = request.filterDateTo,
|
||||
filterGameImportance = request.filterGameImportance,
|
||||
minWinProbabilityDiff = request.minWinProbabilityDiff ?: BigDecimal("0.1"),
|
||||
minWinProbability = request.minWinProbability,
|
||||
maxWinProbability = request.maxWinProbability,
|
||||
minTradeValue = request.minTradeValue ?: BigDecimal("0.05"),
|
||||
minRemainingTime = request.minRemainingTime,
|
||||
maxRemainingTime = request.maxRemainingTime,
|
||||
minScoreDiff = request.minScoreDiff,
|
||||
maxScoreDiff = request.maxScoreDiff,
|
||||
buyAmountStrategy = request.buyAmountStrategy ?: "FIXED",
|
||||
fixedBuyAmount = request.fixedBuyAmount,
|
||||
buyRatio = request.buyRatio,
|
||||
baseBuyAmount = request.baseBuyAmount,
|
||||
buyTiming = request.buyTiming ?: "IMMEDIATE",
|
||||
delayBuySeconds = request.delayBuySeconds ?: 0,
|
||||
buyDirection = request.buyDirection ?: "AUTO",
|
||||
enableSell = request.enableSell ?: true,
|
||||
takeProfitThreshold = request.takeProfitThreshold,
|
||||
stopLossThreshold = request.stopLossThreshold,
|
||||
probabilityReversalThreshold = request.probabilityReversalThreshold,
|
||||
sellRatio = request.sellRatio ?: BigDecimal("1.0"),
|
||||
sellTiming = request.sellTiming ?: "IMMEDIATE",
|
||||
delaySellSeconds = request.delaySellSeconds ?: 0,
|
||||
priceStrategy = request.priceStrategy ?: "MARKET",
|
||||
fixedPrice = request.fixedPrice,
|
||||
priceOffset = request.priceOffset ?: BigDecimal.ZERO,
|
||||
maxPosition = request.maxPosition ?: BigDecimal("50"),
|
||||
minPosition = request.minPosition ?: BigDecimal("5"),
|
||||
maxGamePosition = request.maxGamePosition,
|
||||
maxDailyLoss = request.maxDailyLoss,
|
||||
maxDailyOrders = request.maxDailyOrders,
|
||||
maxDailyProfit = request.maxDailyProfit,
|
||||
priceTolerance = request.priceTolerance ?: BigDecimal("0.05"),
|
||||
minProbabilityThreshold = request.minProbabilityThreshold,
|
||||
maxProbabilityThreshold = request.maxProbabilityThreshold,
|
||||
baseStrengthWeight = request.baseStrengthWeight ?: BigDecimal("0.3"),
|
||||
recentFormWeight = request.recentFormWeight ?: BigDecimal("0.25"),
|
||||
lineupIntegrityWeight = request.lineupIntegrityWeight ?: BigDecimal("0.2"),
|
||||
starStatusWeight = request.starStatusWeight ?: BigDecimal("0.15"),
|
||||
environmentWeight = request.environmentWeight ?: BigDecimal("0.1"),
|
||||
matchupAdvantageWeight = request.matchupAdvantageWeight ?: BigDecimal("0.2"),
|
||||
scoreDiffWeight = request.scoreDiffWeight ?: BigDecimal("0.3"),
|
||||
momentumWeight = request.momentumWeight ?: BigDecimal("0.2"),
|
||||
dataUpdateFrequency = request.dataUpdateFrequency ?: 30,
|
||||
analysisFrequency = request.analysisFrequency ?: 30,
|
||||
pushFailedOrders = request.pushFailedOrders ?: false,
|
||||
pushFrequency = request.pushFrequency ?: "REALTIME",
|
||||
batchPushInterval = request.batchPushInterval ?: 1
|
||||
)
|
||||
|
||||
val saved = strategyRepository.save(strategy)
|
||||
Result.success(toDto(saved))
|
||||
} catch (e: Exception) {
|
||||
logger.error("创建策略失败: ${e.message}", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新策略
|
||||
*/
|
||||
@Transactional
|
||||
suspend fun updateStrategy(request: NbaQuantitativeStrategyUpdateRequest): Result<NbaQuantitativeStrategyDto> {
|
||||
return try {
|
||||
val strategy = strategyRepository.findById(request.id).orElse(null)
|
||||
if (strategy == null) {
|
||||
return Result.failure(IllegalArgumentException("策略不存在"))
|
||||
}
|
||||
|
||||
// 更新字段(只更新提供的字段)
|
||||
val updated = strategy.copy(
|
||||
strategyName = request.strategyName ?: strategy.strategyName,
|
||||
strategyDescription = request.strategyDescription ?: strategy.strategyDescription,
|
||||
enabled = request.enabled ?: strategy.enabled,
|
||||
filterTeams = request.filterTeams?.let { JsonUtils.toJson(it) } ?: strategy.filterTeams,
|
||||
filterDateFrom = request.filterDateFrom ?: strategy.filterDateFrom,
|
||||
filterDateTo = request.filterDateTo ?: strategy.filterDateTo,
|
||||
filterGameImportance = request.filterGameImportance ?: strategy.filterGameImportance,
|
||||
minWinProbabilityDiff = request.minWinProbabilityDiff ?: strategy.minWinProbabilityDiff,
|
||||
minWinProbability = request.minWinProbability ?: strategy.minWinProbability,
|
||||
maxWinProbability = request.maxWinProbability ?: strategy.maxWinProbability,
|
||||
minTradeValue = request.minTradeValue ?: strategy.minTradeValue,
|
||||
minRemainingTime = request.minRemainingTime ?: strategy.minRemainingTime,
|
||||
maxRemainingTime = request.maxRemainingTime ?: strategy.maxRemainingTime,
|
||||
minScoreDiff = request.minScoreDiff ?: strategy.minScoreDiff,
|
||||
maxScoreDiff = request.maxScoreDiff ?: strategy.maxScoreDiff,
|
||||
buyAmountStrategy = request.buyAmountStrategy ?: strategy.buyAmountStrategy,
|
||||
fixedBuyAmount = request.fixedBuyAmount ?: strategy.fixedBuyAmount,
|
||||
buyRatio = request.buyRatio ?: strategy.buyRatio,
|
||||
baseBuyAmount = request.baseBuyAmount ?: strategy.baseBuyAmount,
|
||||
buyTiming = request.buyTiming ?: strategy.buyTiming,
|
||||
delayBuySeconds = request.delayBuySeconds ?: strategy.delayBuySeconds,
|
||||
buyDirection = request.buyDirection ?: strategy.buyDirection,
|
||||
enableSell = request.enableSell ?: strategy.enableSell,
|
||||
takeProfitThreshold = request.takeProfitThreshold ?: strategy.takeProfitThreshold,
|
||||
stopLossThreshold = request.stopLossThreshold ?: strategy.stopLossThreshold,
|
||||
probabilityReversalThreshold = request.probabilityReversalThreshold ?: strategy.probabilityReversalThreshold,
|
||||
sellRatio = request.sellRatio ?: strategy.sellRatio,
|
||||
sellTiming = request.sellTiming ?: strategy.sellTiming,
|
||||
delaySellSeconds = request.delaySellSeconds ?: strategy.delaySellSeconds,
|
||||
priceStrategy = request.priceStrategy ?: strategy.priceStrategy,
|
||||
fixedPrice = request.fixedPrice ?: strategy.fixedPrice,
|
||||
priceOffset = request.priceOffset ?: strategy.priceOffset,
|
||||
maxPosition = request.maxPosition ?: strategy.maxPosition,
|
||||
minPosition = request.minPosition ?: strategy.minPosition,
|
||||
maxGamePosition = request.maxGamePosition ?: strategy.maxGamePosition,
|
||||
maxDailyLoss = request.maxDailyLoss ?: strategy.maxDailyLoss,
|
||||
maxDailyOrders = request.maxDailyOrders ?: strategy.maxDailyOrders,
|
||||
maxDailyProfit = request.maxDailyProfit ?: strategy.maxDailyProfit,
|
||||
priceTolerance = request.priceTolerance ?: strategy.priceTolerance,
|
||||
minProbabilityThreshold = request.minProbabilityThreshold ?: strategy.minProbabilityThreshold,
|
||||
maxProbabilityThreshold = request.maxProbabilityThreshold ?: strategy.maxProbabilityThreshold,
|
||||
baseStrengthWeight = request.baseStrengthWeight ?: strategy.baseStrengthWeight,
|
||||
recentFormWeight = request.recentFormWeight ?: strategy.recentFormWeight,
|
||||
lineupIntegrityWeight = request.lineupIntegrityWeight ?: strategy.lineupIntegrityWeight,
|
||||
starStatusWeight = request.starStatusWeight ?: strategy.starStatusWeight,
|
||||
environmentWeight = request.environmentWeight ?: strategy.environmentWeight,
|
||||
matchupAdvantageWeight = request.matchupAdvantageWeight ?: strategy.matchupAdvantageWeight,
|
||||
scoreDiffWeight = request.scoreDiffWeight ?: strategy.scoreDiffWeight,
|
||||
momentumWeight = request.momentumWeight ?: strategy.momentumWeight,
|
||||
dataUpdateFrequency = request.dataUpdateFrequency ?: strategy.dataUpdateFrequency,
|
||||
analysisFrequency = request.analysisFrequency ?: strategy.analysisFrequency,
|
||||
pushFailedOrders = request.pushFailedOrders ?: strategy.pushFailedOrders,
|
||||
pushFrequency = request.pushFrequency ?: strategy.pushFrequency,
|
||||
batchPushInterval = request.batchPushInterval ?: strategy.batchPushInterval,
|
||||
updatedAt = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
val saved = strategyRepository.save(updated)
|
||||
Result.success(toDto(saved))
|
||||
} catch (e: Exception) {
|
||||
logger.error("更新策略失败: ${e.message}", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取策略列表
|
||||
*/
|
||||
suspend fun getStrategyList(request: NbaQuantitativeStrategyListRequest): Result<NbaQuantitativeStrategyListResponse> {
|
||||
return try {
|
||||
val page = request.page ?: 1
|
||||
val limit = request.limit ?: 20
|
||||
val pageable = PageRequest.of(page - 1, limit, Sort.by(Sort.Direction.DESC, "createdAt"))
|
||||
|
||||
val strategies = when {
|
||||
request.accountId != null && request.enabled != null -> {
|
||||
strategyRepository.findByAccountIdAndEnabled(request.accountId, request.enabled)
|
||||
}
|
||||
request.accountId != null -> {
|
||||
strategyRepository.findByAccountId(request.accountId)
|
||||
}
|
||||
request.enabled != null -> {
|
||||
strategyRepository.findByEnabled(request.enabled)
|
||||
}
|
||||
else -> {
|
||||
strategyRepository.findAll(pageable).content
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤策略名称(如果提供)
|
||||
val filtered = if (request.strategyName != null) {
|
||||
strategies.filter { it.strategyName.contains(request.strategyName, ignoreCase = true) }
|
||||
} else {
|
||||
strategies
|
||||
}
|
||||
|
||||
val total = filtered.size.toLong()
|
||||
val dtoList = filtered.map { toDto(it) }
|
||||
|
||||
Result.success(
|
||||
NbaQuantitativeStrategyListResponse(
|
||||
list = dtoList,
|
||||
total = total,
|
||||
page = page,
|
||||
limit = limit
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("获取策略列表失败: ${e.message}", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取策略详情
|
||||
*/
|
||||
suspend fun getStrategyDetail(id: Long): Result<NbaQuantitativeStrategyDto> {
|
||||
return try {
|
||||
val strategy = strategyRepository.findById(id).orElse(null)
|
||||
if (strategy == null) {
|
||||
return Result.failure(IllegalArgumentException("策略不存在"))
|
||||
}
|
||||
Result.success(toDto(strategy))
|
||||
} catch (e: Exception) {
|
||||
logger.error("获取策略详情失败: ${e.message}", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除策略
|
||||
*/
|
||||
@Transactional
|
||||
suspend fun deleteStrategy(id: Long): Result<Unit> {
|
||||
return try {
|
||||
val strategy = strategyRepository.findById(id).orElse(null)
|
||||
if (strategy == null) {
|
||||
return Result.failure(IllegalArgumentException("策略不存在"))
|
||||
}
|
||||
strategyRepository.delete(strategy)
|
||||
Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
logger.error("删除策略失败: ${e.message}", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启用的策略列表
|
||||
*/
|
||||
suspend fun getEnabledStrategies(): List<NbaQuantitativeStrategy> {
|
||||
return strategyRepository.findByEnabled(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 DTO
|
||||
*/
|
||||
private fun toDto(strategy: NbaQuantitativeStrategy): NbaQuantitativeStrategyDto {
|
||||
val account = accountRepository.findById(strategy.accountId).orElse(null)
|
||||
return NbaQuantitativeStrategyDto(
|
||||
id = strategy.id,
|
||||
strategyName = strategy.strategyName,
|
||||
strategyDescription = strategy.strategyDescription,
|
||||
accountId = strategy.accountId,
|
||||
accountName = account?.accountName,
|
||||
enabled = strategy.enabled,
|
||||
filterTeams = strategy.filterTeams?.let { JsonUtils.parseStringList(it) },
|
||||
filterDateFrom = strategy.filterDateFrom,
|
||||
filterDateTo = strategy.filterDateTo,
|
||||
filterGameImportance = strategy.filterGameImportance,
|
||||
minWinProbabilityDiff = strategy.minWinProbabilityDiff,
|
||||
minWinProbability = strategy.minWinProbability,
|
||||
maxWinProbability = strategy.maxWinProbability,
|
||||
minTradeValue = strategy.minTradeValue,
|
||||
minRemainingTime = strategy.minRemainingTime,
|
||||
maxRemainingTime = strategy.maxRemainingTime,
|
||||
minScoreDiff = strategy.minScoreDiff,
|
||||
maxScoreDiff = strategy.maxScoreDiff,
|
||||
buyAmountStrategy = strategy.buyAmountStrategy,
|
||||
fixedBuyAmount = strategy.fixedBuyAmount,
|
||||
buyRatio = strategy.buyRatio,
|
||||
baseBuyAmount = strategy.baseBuyAmount,
|
||||
buyTiming = strategy.buyTiming,
|
||||
delayBuySeconds = strategy.delayBuySeconds,
|
||||
buyDirection = strategy.buyDirection,
|
||||
enableSell = strategy.enableSell,
|
||||
takeProfitThreshold = strategy.takeProfitThreshold,
|
||||
stopLossThreshold = strategy.stopLossThreshold,
|
||||
probabilityReversalThreshold = strategy.probabilityReversalThreshold,
|
||||
sellRatio = strategy.sellRatio,
|
||||
sellTiming = strategy.sellTiming,
|
||||
delaySellSeconds = strategy.delaySellSeconds,
|
||||
priceStrategy = strategy.priceStrategy,
|
||||
fixedPrice = strategy.fixedPrice,
|
||||
priceOffset = strategy.priceOffset,
|
||||
maxPosition = strategy.maxPosition,
|
||||
minPosition = strategy.minPosition,
|
||||
maxGamePosition = strategy.maxGamePosition,
|
||||
maxDailyLoss = strategy.maxDailyLoss,
|
||||
maxDailyOrders = strategy.maxDailyOrders,
|
||||
maxDailyProfit = strategy.maxDailyProfit,
|
||||
priceTolerance = strategy.priceTolerance,
|
||||
minProbabilityThreshold = strategy.minProbabilityThreshold,
|
||||
maxProbabilityThreshold = strategy.maxProbabilityThreshold,
|
||||
baseStrengthWeight = strategy.baseStrengthWeight,
|
||||
recentFormWeight = strategy.recentFormWeight,
|
||||
lineupIntegrityWeight = strategy.lineupIntegrityWeight,
|
||||
starStatusWeight = strategy.starStatusWeight,
|
||||
environmentWeight = strategy.environmentWeight,
|
||||
matchupAdvantageWeight = strategy.matchupAdvantageWeight,
|
||||
scoreDiffWeight = strategy.scoreDiffWeight,
|
||||
momentumWeight = strategy.momentumWeight,
|
||||
dataUpdateFrequency = strategy.dataUpdateFrequency,
|
||||
analysisFrequency = strategy.analysisFrequency,
|
||||
pushFailedOrders = strategy.pushFailedOrders,
|
||||
pushFrequency = strategy.pushFrequency,
|
||||
batchPushInterval = strategy.batchPushInterval,
|
||||
createdAt = strategy.createdAt,
|
||||
updatedAt = strategy.updatedAt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,5 +28,29 @@ object JsonUtils {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 JSON 字符串列表(parseStringArray 的别名)
|
||||
*/
|
||||
fun parseStringList(jsonString: String?): List<String> {
|
||||
return parseStringArray(jsonString)
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象转换为 JSON 字符串
|
||||
* @param obj 要转换的对象
|
||||
* @return JSON 字符串
|
||||
*/
|
||||
fun toJson(obj: Any?): String? {
|
||||
if (obj == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return try {
|
||||
gson.toJson(obj)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.wrbug.polymarketbot.util
|
||||
|
||||
import com.wrbug.polymarketbot.api.NbaStatsApi
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
/**
|
||||
* NBA API 验证工具
|
||||
* 用于验证 API 调用是否正确
|
||||
*/
|
||||
object NbaApiValidator {
|
||||
private val logger = LoggerFactory.getLogger(NbaApiValidator::class.java)
|
||||
|
||||
/**
|
||||
* 验证 API 调用
|
||||
*/
|
||||
suspend fun validateApi(nbaStatsApi: NbaStatsApi): Boolean {
|
||||
return try {
|
||||
val today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
|
||||
logger.info("验证 NBA Stats API,日期: $today")
|
||||
|
||||
val response = nbaStatsApi.getScoreboard(gameDate = today)
|
||||
|
||||
logger.info("API 响应状态码: ${response.code()}")
|
||||
logger.info("API 响应消息: ${response.message()}")
|
||||
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
val scoreboard = response.body()!!
|
||||
logger.info("ResultSets 数量: ${scoreboard.resultSets.size}")
|
||||
|
||||
scoreboard.resultSets.forEachIndexed { index, resultSet ->
|
||||
logger.info("ResultSet[$index]: name=${resultSet.name}, headers=${resultSet.headers.size}, rows=${resultSet.rowSet.size}")
|
||||
if (resultSet.headers.isNotEmpty()) {
|
||||
logger.info(" Headers: ${resultSet.headers.take(10)}")
|
||||
}
|
||||
if (resultSet.rowSet.isNotEmpty()) {
|
||||
val firstRow = resultSet.rowSet.first()
|
||||
logger.info(" First row size: ${firstRow.size}")
|
||||
logger.info(" First row (first 5): ${firstRow.take(5)}")
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有 GameHeader 和 LineScore
|
||||
val hasGameHeader = scoreboard.resultSets.any { it.name == "GameHeader" }
|
||||
val hasLineScore = scoreboard.resultSets.any { it.name == "LineScore" }
|
||||
|
||||
logger.info("包含 GameHeader: $hasGameHeader")
|
||||
logger.info("包含 LineScore: $hasLineScore")
|
||||
|
||||
hasGameHeader && hasLineScore
|
||||
} else {
|
||||
logger.error("API 调用失败")
|
||||
val errorBody = response.errorBody()?.string()
|
||||
logger.error("错误响应体: $errorBody")
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("验证 API 异常: ${e.message}", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.google.gson.GsonBuilder
|
||||
import com.wrbug.polymarketbot.api.BuilderRelayerApi
|
||||
import com.wrbug.polymarketbot.api.EthereumRpcApi
|
||||
import com.wrbug.polymarketbot.api.GitHubApi
|
||||
import com.wrbug.polymarketbot.api.NbaStatsApi
|
||||
import com.wrbug.polymarketbot.api.PolymarketClobApi
|
||||
import com.wrbug.polymarketbot.api.PolymarketDataApi
|
||||
import com.wrbug.polymarketbot.api.PolymarketGammaApi
|
||||
@@ -237,6 +238,44 @@ class RetrofitFactory(
|
||||
.build()
|
||||
.create(GitHubApi::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 NBA Stats API 客户端
|
||||
* NBA Stats API 是公开 API,但需要设置正确的请求头
|
||||
* @return NbaStatsApi 客户端
|
||||
*/
|
||||
fun createNbaStatsApi(): NbaStatsApi {
|
||||
val baseUrl = "https://stats.nba.com/stats/"
|
||||
|
||||
// 添加拦截器,设置 NBA Stats API 需要的请求头
|
||||
val nbaStatsInterceptor = object : Interceptor {
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request().newBuilder()
|
||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
||||
.header("Referer", "https://www.nba.com/")
|
||||
.header("Accept", "application/json")
|
||||
.header("Accept-Language", "en-US,en;q=0.9")
|
||||
.header("Origin", "https://www.nba.com")
|
||||
.build()
|
||||
return chain.proceed(request)
|
||||
}
|
||||
}
|
||||
|
||||
val okHttpClient = createClient()
|
||||
.addInterceptor(nbaStatsInterceptor)
|
||||
.build()
|
||||
|
||||
val gson = GsonBuilder()
|
||||
.setLenient()
|
||||
.create()
|
||||
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.build()
|
||||
.create(NbaStatsApi::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
-- NBA 量化交易系统数据库表
|
||||
|
||||
-- 1. NBA 市场表(Polymarket 市场信息)
|
||||
CREATE TABLE IF NOT EXISTS nba_markets (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
polymarket_market_id VARCHAR(100) UNIQUE NOT NULL COMMENT 'Polymarket 市场 ID',
|
||||
condition_id VARCHAR(100) UNIQUE NOT NULL COMMENT 'Condition ID',
|
||||
market_slug VARCHAR(255) COMMENT '市场 slug',
|
||||
market_question TEXT COMMENT '市场名称/问题',
|
||||
market_description TEXT COMMENT '市场描述',
|
||||
category VARCHAR(50) DEFAULT 'sports' COMMENT '分类',
|
||||
active BOOLEAN DEFAULT true COMMENT '是否活跃',
|
||||
closed BOOLEAN DEFAULT false COMMENT '是否已关闭',
|
||||
archived BOOLEAN DEFAULT false COMMENT '是否已归档',
|
||||
volume VARCHAR(50) COMMENT '交易量',
|
||||
liquidity VARCHAR(50) COMMENT '流动性',
|
||||
outcomes TEXT COMMENT '结果选项(JSON)',
|
||||
end_date VARCHAR(50) COMMENT '结束日期',
|
||||
start_date VARCHAR(50) COMMENT '开始日期',
|
||||
created_at BIGINT NOT NULL,
|
||||
updated_at BIGINT NOT NULL,
|
||||
INDEX idx_condition_id (condition_id),
|
||||
INDEX idx_active (active),
|
||||
INDEX idx_closed (closed),
|
||||
INDEX idx_category (category)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='NBA市场表(Polymarket市场)';
|
||||
|
||||
-- 2. NBA 比赛表(NBA 比赛信息)
|
||||
CREATE TABLE IF NOT EXISTS nba_games (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
nba_game_id VARCHAR(100) UNIQUE COMMENT 'NBA 比赛 ID(来自 NBA API)',
|
||||
home_team VARCHAR(100) NOT NULL COMMENT '主队名称',
|
||||
away_team VARCHAR(100) NOT NULL COMMENT '客队名称',
|
||||
game_date DATE NOT NULL COMMENT '比赛日期',
|
||||
game_time BIGINT COMMENT '比赛时间(时间戳,毫秒)',
|
||||
game_status VARCHAR(50) DEFAULT 'scheduled' COMMENT '比赛状态:scheduled/active/finished',
|
||||
home_score INT DEFAULT 0 COMMENT '主队得分',
|
||||
away_score INT DEFAULT 0 COMMENT '客队得分',
|
||||
period INT DEFAULT 0 COMMENT '当前节次',
|
||||
time_remaining VARCHAR(50) COMMENT '剩余时间',
|
||||
polymarket_market_id VARCHAR(100) COMMENT '关联的 Polymarket 市场 ID',
|
||||
created_at BIGINT NOT NULL,
|
||||
updated_at BIGINT NOT NULL,
|
||||
INDEX idx_game_date (game_date),
|
||||
INDEX idx_game_status (game_status),
|
||||
INDEX idx_home_team (home_team),
|
||||
INDEX idx_away_team (away_team),
|
||||
INDEX idx_polymarket_market_id (polymarket_market_id),
|
||||
FOREIGN KEY (polymarket_market_id) REFERENCES nba_markets(polymarket_market_id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='NBA比赛表';
|
||||
|
||||
-- 3. 量化策略配置表
|
||||
CREATE TABLE IF NOT EXISTS nba_quantitative_strategies (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
strategy_name VARCHAR(100) NOT NULL COMMENT '策略名称',
|
||||
strategy_description TEXT COMMENT '策略描述',
|
||||
account_id BIGINT NOT NULL COMMENT '关联账户 ID',
|
||||
enabled BOOLEAN DEFAULT true COMMENT '是否启用',
|
||||
|
||||
-- 比赛筛选参数
|
||||
filter_teams TEXT COMMENT '关注的球队列表(JSON)',
|
||||
filter_date_from DATE COMMENT '日期范围开始',
|
||||
filter_date_to DATE COMMENT '日期范围结束',
|
||||
filter_game_importance VARCHAR(50) COMMENT '比赛重要性:all/regular/playoff/key',
|
||||
|
||||
-- 触发条件参数
|
||||
min_win_probability_diff DECIMAL(5, 4) DEFAULT 0.1000 COMMENT '最小获胜概率差异',
|
||||
min_win_probability DECIMAL(5, 4) COMMENT '最小获胜概率',
|
||||
max_win_probability DECIMAL(5, 4) COMMENT '最大获胜概率',
|
||||
min_trade_value DECIMAL(5, 4) DEFAULT 0.0500 COMMENT '最小交易价值',
|
||||
min_remaining_time INT COMMENT '最小剩余时间(分钟)',
|
||||
max_remaining_time INT COMMENT '最大剩余时间(分钟)',
|
||||
min_score_diff INT COMMENT '最小分差',
|
||||
max_score_diff INT COMMENT '最大分差',
|
||||
|
||||
-- 买入规则参数
|
||||
buy_amount_strategy VARCHAR(20) DEFAULT 'FIXED' COMMENT '买入金额策略:FIXED/RATIO/DYNAMIC',
|
||||
fixed_buy_amount DECIMAL(20, 8) COMMENT '固定买入金额(USDC)',
|
||||
buy_ratio DECIMAL(5, 4) COMMENT '买入比例(0-1)',
|
||||
base_buy_amount DECIMAL(20, 8) COMMENT '基础买入金额(USDC)',
|
||||
buy_timing VARCHAR(20) DEFAULT 'IMMEDIATE' COMMENT '买入时机:IMMEDIATE/DELAYED',
|
||||
delay_buy_seconds INT DEFAULT 0 COMMENT '延迟买入时间(秒)',
|
||||
buy_direction VARCHAR(10) DEFAULT 'AUTO' COMMENT '买入方向:AUTO/YES/NO',
|
||||
|
||||
-- 卖出规则参数
|
||||
enable_sell BOOLEAN DEFAULT true COMMENT '是否启用卖出',
|
||||
take_profit_threshold DECIMAL(5, 4) COMMENT '止盈阈值(0-1)',
|
||||
stop_loss_threshold DECIMAL(5, 4) COMMENT '止损阈值(-1-0)',
|
||||
probability_reversal_threshold DECIMAL(5, 4) COMMENT '概率反转阈值(0-1)',
|
||||
sell_ratio DECIMAL(5, 4) DEFAULT 1.0000 COMMENT '卖出比例(0-1)',
|
||||
sell_timing VARCHAR(20) DEFAULT 'IMMEDIATE' COMMENT '卖出时机:IMMEDIATE/DELAYED',
|
||||
delay_sell_seconds INT DEFAULT 0 COMMENT '延迟卖出时间(秒)',
|
||||
|
||||
-- 价格策略参数
|
||||
price_strategy VARCHAR(20) DEFAULT 'MARKET' COMMENT '价格策略:FIXED/MARKET/DYNAMIC',
|
||||
fixed_price DECIMAL(5, 4) COMMENT '固定价格(0-1)',
|
||||
price_offset DECIMAL(5, 4) DEFAULT 0.0000 COMMENT '价格偏移(-0.1-0.1)',
|
||||
|
||||
-- 风险控制参数
|
||||
max_position DECIMAL(20, 8) DEFAULT 50.00000000 COMMENT '最大持仓(USDC)',
|
||||
min_position DECIMAL(20, 8) DEFAULT 5.00000000 COMMENT '最小持仓(USDC)',
|
||||
max_game_position DECIMAL(20, 8) COMMENT '单场比赛最大持仓(USDC)',
|
||||
max_daily_loss DECIMAL(20, 8) COMMENT '每日亏损限制(USDC)',
|
||||
max_daily_orders INT COMMENT '每日订单限制',
|
||||
max_daily_profit DECIMAL(20, 8) COMMENT '每日盈利目标(USDC)',
|
||||
price_tolerance DECIMAL(5, 4) DEFAULT 0.0500 COMMENT '价格容忍度(0-1)',
|
||||
min_probability_threshold DECIMAL(5, 4) COMMENT '最小概率阈值(0.5-1.0)',
|
||||
max_probability_threshold DECIMAL(5, 4) COMMENT '最大概率阈值(0.0-0.5)',
|
||||
|
||||
-- 算法权重参数(高级)
|
||||
base_strength_weight DECIMAL(5, 4) DEFAULT 0.3000 COMMENT '基础实力权重',
|
||||
recent_form_weight DECIMAL(5, 4) DEFAULT 0.2500 COMMENT '近期状态权重',
|
||||
lineup_integrity_weight DECIMAL(5, 4) DEFAULT 0.2000 COMMENT '阵容完整度权重',
|
||||
star_status_weight DECIMAL(5, 4) DEFAULT 0.1500 COMMENT '球星状态权重',
|
||||
environment_weight DECIMAL(5, 4) DEFAULT 0.1000 COMMENT '环境因素权重',
|
||||
matchup_advantage_weight DECIMAL(5, 4) DEFAULT 0.2000 COMMENT '对位优势权重',
|
||||
score_diff_weight DECIMAL(5, 4) DEFAULT 0.3000 COMMENT '分差调整权重',
|
||||
momentum_weight DECIMAL(5, 4) DEFAULT 0.2000 COMMENT '势头调整权重',
|
||||
|
||||
-- 系统配置参数
|
||||
data_update_frequency INT DEFAULT 30 COMMENT '数据更新频率(秒)',
|
||||
analysis_frequency INT DEFAULT 30 COMMENT '分析频率(秒)',
|
||||
push_failed_orders BOOLEAN DEFAULT false COMMENT '是否推送失败订单',
|
||||
push_frequency VARCHAR(20) DEFAULT 'REALTIME' COMMENT '推送频率:REALTIME/BATCH',
|
||||
batch_push_interval INT DEFAULT 1 COMMENT '批量推送间隔(秒)',
|
||||
|
||||
created_at BIGINT NOT NULL,
|
||||
updated_at BIGINT NOT NULL,
|
||||
INDEX idx_account_id (account_id),
|
||||
INDEX idx_enabled (enabled),
|
||||
INDEX idx_strategy_name (strategy_name),
|
||||
FOREIGN KEY (account_id) REFERENCES wallet_accounts(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='NBA量化策略配置表';
|
||||
|
||||
-- 4. 交易信号表
|
||||
CREATE TABLE IF NOT EXISTS nba_trading_signals (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
strategy_id BIGINT NOT NULL COMMENT '策略 ID',
|
||||
game_id BIGINT COMMENT '比赛 ID',
|
||||
market_id BIGINT COMMENT '市场 ID',
|
||||
signal_type VARCHAR(10) NOT NULL COMMENT '信号类型:BUY/SELL',
|
||||
direction VARCHAR(10) NOT NULL COMMENT '方向:YES/NO',
|
||||
price DECIMAL(5, 4) NOT NULL COMMENT '价格(0-1)',
|
||||
quantity DECIMAL(20, 8) NOT NULL COMMENT '数量',
|
||||
total_amount DECIMAL(20, 8) NOT NULL COMMENT '总金额(USDC)',
|
||||
reason TEXT COMMENT '触发原因',
|
||||
win_probability DECIMAL(5, 4) COMMENT '获胜概率',
|
||||
trade_value DECIMAL(5, 4) COMMENT '交易价值',
|
||||
signal_status VARCHAR(20) DEFAULT 'GENERATED' COMMENT '信号状态:GENERATED/EXECUTING/SUCCESS/FAILED',
|
||||
execution_result TEXT COMMENT '执行结果',
|
||||
error_message TEXT COMMENT '错误信息',
|
||||
created_at BIGINT NOT NULL,
|
||||
updated_at BIGINT NOT NULL,
|
||||
INDEX idx_strategy_id (strategy_id),
|
||||
INDEX idx_game_id (game_id),
|
||||
INDEX idx_market_id (market_id),
|
||||
INDEX idx_signal_type (signal_type),
|
||||
INDEX idx_signal_status (signal_status),
|
||||
INDEX idx_created_at (created_at),
|
||||
FOREIGN KEY (strategy_id) REFERENCES nba_quantitative_strategies(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (game_id) REFERENCES nba_games(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (market_id) REFERENCES nba_markets(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='NBA交易信号表';
|
||||
|
||||
-- 5. 策略执行统计表
|
||||
CREATE TABLE IF NOT EXISTS nba_strategy_statistics (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
strategy_id BIGINT NOT NULL COMMENT '策略 ID',
|
||||
stat_date DATE NOT NULL COMMENT '统计日期',
|
||||
total_signals INT DEFAULT 0 COMMENT '总信号数',
|
||||
buy_signals INT DEFAULT 0 COMMENT '买入信号数',
|
||||
sell_signals INT DEFAULT 0 COMMENT '卖出信号数',
|
||||
success_signals INT DEFAULT 0 COMMENT '成功信号数',
|
||||
failed_signals INT DEFAULT 0 COMMENT '失败信号数',
|
||||
total_profit DECIMAL(20, 8) DEFAULT 0.00000000 COMMENT '总盈亏(USDC)',
|
||||
total_volume DECIMAL(20, 8) DEFAULT 0.00000000 COMMENT '总交易量(USDC)',
|
||||
created_at BIGINT NOT NULL,
|
||||
updated_at BIGINT NOT NULL,
|
||||
UNIQUE KEY uk_strategy_date (strategy_id, stat_date),
|
||||
INDEX idx_strategy_id (strategy_id),
|
||||
INDEX idx_stat_date (stat_date),
|
||||
FOREIGN KEY (strategy_id) REFERENCES nba_quantitative_strategies(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='NBA策略执行统计表';
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
-- 移除 NBA 市场相关的外键约束
|
||||
-- 由于不再在数据库中存储市场信息,需要移除这些外键约束
|
||||
|
||||
-- 1. 移除 nba_games 表的外键约束
|
||||
SET @fk_name = (SELECT CONSTRAINT_NAME
|
||||
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'nba_games'
|
||||
AND COLUMN_NAME = 'polymarket_market_id'
|
||||
AND REFERENCED_TABLE_NAME = 'nba_markets'
|
||||
LIMIT 1);
|
||||
|
||||
SET @sql = IF(@fk_name IS NOT NULL,
|
||||
CONCAT('ALTER TABLE nba_games DROP FOREIGN KEY ', @fk_name),
|
||||
'SELECT "Foreign key constraint not found"');
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 2. 移除 nba_trading_signals 表的外键约束(如果存在)
|
||||
SET @fk_name = (SELECT CONSTRAINT_NAME
|
||||
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'nba_trading_signals'
|
||||
AND COLUMN_NAME = 'market_id'
|
||||
AND REFERENCED_TABLE_NAME = 'nba_markets'
|
||||
LIMIT 1);
|
||||
|
||||
SET @sql = IF(@fk_name IS NOT NULL,
|
||||
CONCAT('ALTER TABLE nba_trading_signals DROP FOREIGN KEY ', @fk_name),
|
||||
'SELECT "Foreign key constraint not found"');
|
||||
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.wrbug.polymarketbot.service.nba
|
||||
|
||||
import com.wrbug.polymarketbot.util.RetrofitFactory
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
|
||||
/**
|
||||
* NBA 比赛服务测试
|
||||
* 用于验证 API 调用是否正确
|
||||
*/
|
||||
@SpringBootTest
|
||||
class NbaGameServiceTest {
|
||||
|
||||
@Autowired
|
||||
private lateinit var retrofitFactory: RetrofitFactory
|
||||
|
||||
@Test
|
||||
fun testNbaStatsApi() {
|
||||
runBlocking {
|
||||
try {
|
||||
val nbaStatsApi = retrofitFactory.createNbaStatsApi()
|
||||
|
||||
// 测试获取今天的比赛
|
||||
val today = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"))
|
||||
println("测试日期: $today")
|
||||
|
||||
val response = nbaStatsApi.getScoreboard(gameDate = today)
|
||||
|
||||
println("响应状态码: ${response.code()}")
|
||||
println("响应消息: ${response.message()}")
|
||||
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
val scoreboard = response.body()!!
|
||||
println("ResultSets 数量: ${scoreboard.resultSets.size}")
|
||||
|
||||
scoreboard.resultSets.forEachIndexed { index, resultSet ->
|
||||
println("ResultSet[$index]: name=${resultSet.name}, headers=${resultSet.headers.size}, rows=${resultSet.rowSet.size}")
|
||||
if (resultSet.headers.isNotEmpty()) {
|
||||
println(" Headers: ${resultSet.headers.take(5)}...")
|
||||
}
|
||||
if (resultSet.rowSet.isNotEmpty()) {
|
||||
println(" First row size: ${resultSet.rowSet.first().size}")
|
||||
println(" First row: ${resultSet.rowSet.first().take(5)}...")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println("API 调用失败")
|
||||
println("错误响应体: ${response.errorBody()?.string()}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
println("测试异常: ${e.message}")
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user